You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by ju...@apache.org on 2011/12/13 10:20:53 UTC

svn commit: r1213622 [2/3] - in /subversion/branches/showing-merge-info: ./ subversion/libsvn_client/ subversion/libsvn_fs_fs/ subversion/libsvn_wc/ subversion/svn/ subversion/tests/cmdline/ subversion/tests/cmdline/getopt_tests_data/ subversion/tests/...

Modified: subversion/branches/showing-merge-info/subversion/libsvn_client/merge.c
URL: http://svn.apache.org/viewvc/subversion/branches/showing-merge-info/subversion/libsvn_client/merge.c?rev=1213622&r1=1213621&r2=1213622&view=diff
==============================================================================
--- subversion/branches/showing-merge-info/subversion/libsvn_client/merge.c (original)
+++ subversion/branches/showing-merge-info/subversion/libsvn_client/merge.c Tue Dec 13 09:20:52 2011
@@ -3288,10 +3288,9 @@ adjust_deleted_subtree_ranges(svn_client
 
 /* Helper for do_directory_merge().
 
-   URL1, REVISION1, URL2, REVISION2, and MERGE_B are
-   cascaded from the arguments of the same name in do_directory_merge().
-   RA_SESSION is the session for the younger of URL1@REVISION1 and
-   URL2@REVISION2.
+   SOURCE and MERGE_B are cascaded from the arguments of the same name in
+   do_directory_merge().  RA_SESSION is the session for the younger of
+   SOURCE->url1@rev1 and SOURCE->url2@rev2.
 
    Adjust the subtrees in CHILDREN_WITH_MERGEINFO so that we don't
    later try to describe invalid paths in drive_merge_report_editor().
@@ -3302,10 +3301,7 @@ adjust_deleted_subtree_ranges(svn_client
    CHILDREN_WITH_MERGEINFO are allocated in RESULT_POOL.
 */
 static svn_error_t *
-fix_deleted_subtree_ranges(const char *url1,
-                           svn_revnum_t revision1,
-                           const char *url2,
-                           svn_revnum_t revision2,
+fix_deleted_subtree_ranges(const merge_source_t *source,
                            svn_ra_session_t *ra_session,
                            apr_array_header_t *children_with_mergeinfo,
                            merge_cmd_baton_t *merge_b,
@@ -3314,7 +3310,7 @@ fix_deleted_subtree_ranges(const char *u
 {
   int i;
   apr_pool_t *iterpool = svn_pool_create(scratch_pool);
-  svn_boolean_t is_rollback = revision2 < revision1;
+  svn_boolean_t is_rollback = source->rev2 < source->rev1;
 
   /* CHILDREN_WITH_MERGEINFO is sorted in depth-first order, so
      start at index 1 to examine only subtrees. */
@@ -3359,16 +3355,15 @@ fix_deleted_subtree_ranges(const char *u
           SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges, iterpool));
         }
 
-      /* If CHILD is the merge target we then know that URL1, URL2,
-         REVISION1, and REVISION2 are provided by normalize_merge_sources()
-         -- see 'MERGEINFO MERGE SOURCE NORMALIZATION'.  Due to this
-         normalization we know that URL1@REVISION1 and URL2@REVISION2
-         describe an unbroken line of history such that the entire range
-         described by REVISION1:REVISION2 can potentially be merged to CHILD.
+      /* If CHILD is the merge target we then know that SOURCE is provided
+         by normalize_merge_sources() -- see 'MERGEINFO MERGE SOURCE
+         NORMALIZATION'.  Due to this normalization we know that SOURCE
+         describes an unbroken line of history such that the entire range
+         described by SOURCE can potentially be merged to CHILD.
 
          But if CHILD is a subtree we don't have the same guarantees about
-         URL1, URL2, REVISION1, and REVISION2 as we do for the merge target.
-         URL1@REVSION1 and/or URL2@REVSION2 might not exist.
+         SOURCE as we do for the merge target.  SOURCE->url1@rev1 and/or
+         SOURCE->url2@rev2 might not exist.
 
          If one or both doesn't exist, then adjust CHILD->REMAINING_RANGES
          such that we don't later try to describe invalid subtrees in
@@ -3390,7 +3385,8 @@ fix_deleted_subtree_ranges(const char *u
           SVN_ERR_ASSERT(child_repos_src_path);
 
           child_primary_source_url =
-            svn_path_url_add_component2((revision1 < revision2) ? url2 : url1,
+            svn_path_url_add_component2((source->rev1 < source->rev2)
+                                        ? source->url2 : source->url1,
                                         child_repos_src_path, iterpool);
 
           SVN_ERR(svn_ra__get_fspath_relative_to_root(ra_session,
@@ -3399,7 +3395,7 @@ fix_deleted_subtree_ranges(const char *u
                                                       iterpool));
           SVN_ERR(adjust_deleted_subtree_ranges(child, parent,
                                                 child_mergeinfo_path,
-                                                revision1, revision2,
+                                                source->rev1, source->rev2,
                                                 child_primary_source_url,
                                                 ra_session,
                                                 merge_b->ctx, result_pool,
@@ -3908,13 +3904,12 @@ filter_merged_revisions(svn_client__merg
 /* Helper for do_file_merge and do_directory_merge (by way of
    populate_remaining_ranges() for the latter).
 
-   Determine what portions of URL1@REVISION1 -> URL2@REVISION2 have already
+   Determine what portions of SOURCE have already
    been merged to CHILD->ABSPATH and populate CHILD->REMAINING_RANGES with
    the ranges that still need merging.
 
-   URL1, REVISION1, URL2, REVISION2, and CTX are all cascaded
-   from the caller's arguments of the same names.  Note that this means URL1,
-   REVISION1, URL2, and REVISION2 adhere to the requirements noted in
+   SOURCE and CTX are all cascaded from the caller's arguments of the same
+   names.  Note that this means SOURCE adheres to the requirements noted in
    `MERGEINFO MERGE SOURCE NORMALIZATION'.
 
    CHILD represents a working copy path which is the merge target or one of
@@ -3922,8 +3917,8 @@ filter_merged_revisions(svn_client__merg
    ancestor - see 'THE CHILDREN_WITH_MERGEINFO ARRAY'.  TARGET_MERGEINFO is
    the working mergeinfo on CHILD.
 
-   RA_SESSION is the session for the younger of URL1@REVISION1 and
-   URL2@REVISION2.
+   RA_SESSION is the session for the younger of SOURCE->url1@rev1 and
+   SOURCE->url2@rev2.
 
    If the function needs to consider CHILD->IMPLICIT_MERGEINFO and
    CHILD_INHERITS_IMPLICIT is true, then set CHILD->IMPLICIT_MERGEINFO to the
@@ -3931,7 +3926,7 @@ filter_merged_revisions(svn_client__merg
    the repository for CHILD->IMPLICIT_MERGEINFO.
 
    If not null, IMPLICIT_SRC_GAP is the gap, if any, in the natural history
-   of URL1@REVISION1:URL2@REVISION2, see merge_cmd_baton_t.implicit_src_gap.
+   of SOURCE, see merge_cmd_baton_t.implicit_src_gap.
 
    SCRATCH_POOL is used for all temporary allocations.  Changes to CHILD and
    PARENT are made in RESULT_POOL.
@@ -3944,18 +3939,15 @@ filter_merged_revisions(svn_client__merg
    in depth-first order.
 
    NOTE: When performing reverse merges, return
-   SVN_ERR_CLIENT_NOT_READY_TO_MERGE if URL1@REVISION1, URL2@REVISION2, and
+   SVN_ERR_CLIENT_NOT_READY_TO_MERGE if both locations in SOURCE and
    CHILD->ABSPATH are all on the same line of history but CHILD->ABSPATH's
-   base revision is older than the REVISION1-REVISION2 range, see comment re
+   base revision is older than the SOURCE->rev1:rev2 range, see comment re
    issue #2973 below.
 */
 static svn_error_t *
 calculate_remaining_ranges(svn_client__merge_path_t *parent,
                            svn_client__merge_path_t *child,
-                           const char *url1,
-                           svn_revnum_t revision1,
-                           const char *url2,
-                           svn_revnum_t revision2,
+                           const merge_source_t *source,
                            svn_mergeinfo_t target_mergeinfo,
                            const apr_array_header_t *implicit_src_gap,
                            svn_boolean_t child_inherits_implicit,
@@ -3965,7 +3957,8 @@ calculate_remaining_ranges(svn_client__m
                            apr_pool_t *scratch_pool)
 {
   const char *mergeinfo_path;
-  const char *primary_url = (revision1 < revision2) ? url2 : url1;
+  const char *primary_url = (source->rev1 < source->rev2)
+                            ? source->url2 : source->url1;
   svn_mergeinfo_t adjusted_target_mergeinfo = NULL;
   svn_revnum_t child_base_revision;
 
@@ -3974,10 +3967,10 @@ calculate_remaining_ranges(svn_client__m
                                               primary_url, result_pool));
 
   /* Consider: CHILD might have explicit mergeinfo '/MERGEINFO_PATH:M-N'
-     where M-N fall into the gap in URL1@REVISION1:URL2@REVISION2's natural
+     where M-N fall into the gap in SOURCE's natural
      history allowed by 'MERGEINFO MERGE SOURCE NORMALIZATION'.  If this is
      the case, then '/MERGEINFO_PATH:N' actually refers to a completely
-     different line of history than URL1@REVISION1:URL2@REVISION2 and we
+     different line of history than SOURCE and we
      *don't* want to consider those revisions merged already. */
   if (implicit_src_gap && child->pre_merge_mergeinfo)
     {
@@ -4005,7 +3998,7 @@ calculate_remaining_ranges(svn_client__m
      merged (or, in the case of reverse merges, ranges not yet merged). */
   SVN_ERR(filter_merged_revisions(parent, child, mergeinfo_path,
                                   adjusted_target_mergeinfo,
-                                  revision1, revision2,
+                                  source->rev1, source->rev2,
                                   child_inherits_implicit,
                                   ra_session, ctx, result_pool,
                                   scratch_pool));
@@ -4042,8 +4035,8 @@ calculate_remaining_ranges(svn_client__m
      can't have any "future" history. */
   if (SVN_IS_VALID_REVNUM(child_base_revision)
       && ((child->remaining_ranges)->nelts == 0) /* Inoperative merge */
-      && (revision2 < revision1)                 /* Reverse merge */
-      && (child_base_revision <= revision2))     /* From CHILD's future */
+      && (source->rev2 < source->rev1)                 /* Reverse merge */
+      && (child_base_revision <= source->rev2))     /* From CHILD's future */
     {
       /* Hmmm, an inoperative reverse merge from the "future".  If it is
          from our own future return a helpful error. */
@@ -4051,7 +4044,7 @@ calculate_remaining_ranges(svn_client__m
       const char *start_url;
 
       err = svn_client__repos_location(&start_url,
-                                       ra_session, url1, revision1,
+                                       ra_session, source->url1, source->rev1,
                                        child_base_revision,
                                        ctx, scratch_pool, scratch_pool);
       if (err)
@@ -4081,24 +4074,23 @@ calculate_remaining_ranges(svn_client__m
 
 /* Helper for populate_remaining_ranges().
 
-   URL1, REVISION1, URL2, REVISION2, RA_SESSION, MERGE_SRC_CANON_PATH,
+   SOURCE, RA_SESSION, MERGE_SRC_CANON_PATH,
    and MERGE_B are all cascaded from the arguments of the same name in
    populate_remaining_ranges().  MERGE_SRC_CANON_PATH is the absolute
-   repository path of URL2.
+   repository path of SOURCE->url2.
 
-   Note: The following comments assume a forward merge, i.e.
-   REVISION1 < REVISION2.  If this is a reverse merge then all the following
-   comments still apply, but with URL1 switched with URL2 and REVISION1
-   switched with REVISION2.
-
-   Like populate_remaining_ranges(), URL1@REVISION1:URL2@REVISION2 must adhere
-   to the restrictions documented in 'MERGEINFO MERGE SOURCE NORMALIZATION'.
-   These restrictions allow for a *single* gap, URL@GAP_REV1:URL2@GAP_REV2,
-   (where REVISION1 < GAP_REV1 <= GAP_REV2 < REVISION2) in
-   URL1@REVISION1:URL2@REVISION2 if URL2@REVISION2 was copied from
-   URL1@REVISION1.  If such a gap exists, set *GAP_START and *GAP_END to the
-   starting and ending revisions of the gap.  Otherwise set both to
-   SVN_INVALID_REVNUM.
+   Note: The following comments assume a forward merge, i.e. SOURCE->rev1
+   < SOURCE->rev2.  If this is a reverse merge then all the following
+   comments still apply, but with SOURCE->url1 switched with SOURCE->url2
+   and SOURCE->rev1 switched with SOURCE->rev2.
+
+   Like populate_remaining_ranges(), SOURCE must adhere to the restrictions
+   documented in 'MERGEINFO MERGE SOURCE NORMALIZATION'.  These restrictions
+   allow for a *single* gap, URL@GAP_REV1:URL2@GAP_REV2, (where SOURCE->rev1
+   < GAP_REV1 <= GAP_REV2 < SOURCE->rev2) in SOURCE if SOURCE->url2@rev2 was
+   copied from SOURCE->url1@rev1.  If such a gap exists, set *GAP_START and
+   *GAP_END to the starting and ending revisions of the gap.  Otherwise set
+   both to SVN_INVALID_REVNUM.
 
    For example, if the natural history of URL@2:URL@9 is 'trunk/:2,7-9' this
    would indicate that trunk@7 was copied from trunk@2.  This function would
@@ -4109,23 +4101,20 @@ static svn_error_t *
 find_gaps_in_merge_source_history(svn_revnum_t *gap_start,
                                   svn_revnum_t *gap_end,
                                   const char *merge_src_canon_path,
-                                  const char *url1,
-                                  svn_revnum_t revision1,
-                                  const char *url2,
-                                  svn_revnum_t revision2,
+                                  const merge_source_t *source,
                                   svn_ra_session_t *ra_session,
                                   merge_cmd_baton_t *merge_b,
                                   apr_pool_t *scratch_pool)
 {
   svn_mergeinfo_t implicit_src_mergeinfo;
-  svn_revnum_t young_rev = MAX(revision1, revision2);
-  svn_revnum_t old_rev = MIN(revision1, revision2);
+  svn_revnum_t young_rev = MAX(source->rev1, source->rev2);
+  svn_revnum_t old_rev = MIN(source->rev1, source->rev2);
   apr_array_header_t *rangelist;
 
   /* Start by assuming there is no gap. */
   *gap_start = *gap_end = SVN_INVALID_REVNUM;
 
-  /* Get URL1@REVISION1:URL2@REVISION2 as mergeinfo. */
+  /* Get SOURCE as mergeinfo. */
   SVN_ERR(svn_client__get_history_as_mergeinfo(&implicit_src_mergeinfo, NULL,
                                                young_rev, young_rev,
                                                old_rev, ra_session,
@@ -4165,7 +4154,7 @@ find_gaps_in_merge_source_history(svn_re
       /* As mentioned above, multiple gaps *shouldn't* be possible. */
       SVN_ERR_ASSERT(apr_hash_count(implicit_src_mergeinfo) == 1);
 
-      *gap_start = MIN(revision1, revision2);
+      *gap_start = MIN(source->rev1, source->rev2);
       *gap_end = (APR_ARRAY_IDX(rangelist,
                                 rangelist->nelts - 1,
                                 svn_merge_range_t *))->start;
@@ -4173,8 +4162,8 @@ find_gaps_in_merge_source_history(svn_re
   else if (apr_hash_count(implicit_src_mergeinfo) > 1) /* Rename */
     {
       apr_array_header_t *requested_rangelist =
-        svn_rangelist__initialize(MIN(revision1, revision2),
-                                  MAX(revision1, revision2),
+        svn_rangelist__initialize(MIN(source->rev1, source->rev2),
+                                  MAX(source->rev1, source->rev2),
                                   TRUE, scratch_pool);
       apr_array_header_t *implicit_rangelist =
         apr_array_make(scratch_pool, 2, sizeof(svn_merge_range_t *));
@@ -4211,24 +4200,21 @@ find_gaps_in_merge_source_history(svn_re
    If HONOR_MERGEINFO is set, this function will actually try to be
    intelligent about populating remaining_ranges list.  Otherwise, it
    will claim that each child has a single remaining range, from
-   revision1, to revision2.
+   SOURCE->rev1, to SOURCE->rev2.
 
    SCRATCH_POOL is used for all temporary allocations.  Changes to
    CHILDREN_WITH_MERGEINFO are made in RESULT_POOL.
 
-   Note that if REVISION1 > REVISION2, then each child's remaining_ranges
+   Note that if SOURCE->rev1 > SOURCE->rev2, then each child's remaining_ranges
    member does not adhere to the API rules for rangelists described in
    svn_mergeinfo.h -- See svn_client__merge_path_t.
 
    See `MERGEINFO MERGE SOURCE NORMALIZATION' for more requirements
-   around the values of URL1, REVISION1, URL2, and REVISION2.
+   around SOURCE.
 */
 static svn_error_t *
 populate_remaining_ranges(apr_array_header_t *children_with_mergeinfo,
-                          const char *url1,
-                          svn_revnum_t revision1,
-                          const char *url2,
-                          svn_revnum_t revision2,
+                          const merge_source_t *source,
                           svn_boolean_t honor_mergeinfo,
                           svn_ra_session_t *ra_session,
                           const char *parent_merge_src_canon_path,
@@ -4241,7 +4227,7 @@ populate_remaining_ranges(apr_array_head
   svn_revnum_t gap_start, gap_end;
 
   /* If we aren't honoring mergeinfo or this is a --record-only merge,
-     we'll make quick work of this by simply adding dummy REVISION1:REVISION2
+     we'll make quick work of this by simply adding dummy SOURCE->rev1:rev2
      ranges for all children. */
   if (! honor_mergeinfo || merge_b->record_only)
     {
@@ -4264,8 +4250,8 @@ populate_remaining_ranges(apr_array_head
                                          NULL, /* child->inherited_mergeinfo */
                                          svn_mergeinfo_inherited, ra_session,
                                          child->abspath,
-                                         MAX(revision1, revision2),
-                                         MIN(revision1, revision2),
+                                         MAX(source->rev1, source->rev2),
+                                         MIN(source->rev1, source->rev2),
                                          merge_b->ctx, result_pool,
                                          iterpool));
             }
@@ -4286,13 +4272,13 @@ populate_remaining_ranges(apr_array_head
               child_inherits_implicit = (parent && !child->switched);
               SVN_ERR(ensure_implicit_mergeinfo(parent, child,
                                                 child_inherits_implicit,
-                                                revision1, revision2,
+                                                source->rev1, source->rev2,
                                                 ra_session, merge_b->ctx,
                                                 result_pool, iterpool));
             }
 
-          child->remaining_ranges = svn_rangelist__initialize(revision1,
-                                                              revision2,
+          child->remaining_ranges = svn_rangelist__initialize(source->rev1,
+                                                              source->rev2,
                                                               TRUE,
                                                               result_pool);
         }
@@ -4301,21 +4287,20 @@ populate_remaining_ranges(apr_array_head
     }
 
   /* If, in the merge source's history, there was a copy from a older
-     revision, then URL2 won't exist at some range M:N, where
-     REVISION1 < M < N < REVISION2. The rules of 'MERGEINFO MERGE SOURCE
-     NORMALIZATION' allow this, but we must ignore these gaps when
-     calculating what ranges remain to be merged from
-     URL1@REVISION1:URL2@REVISION2.  If we don't and try to merge any part
-     of URL2@M:URL2@N we would break the editor since no part of that
-     actually exists.  See http://svn.haxx.se/dev/archive-2008-11/0618.shtml.
+     revision, then SOURCE->url2 won't exist at some range M:N, where
+     source->rev1 < M < N < source->rev2. The rules of 'MERGEINFO MERGE
+     SOURCE NORMALIZATION' allow this, but we must ignore these gaps when
+     calculating what ranges remain to be merged from SOURCE. If we don't
+     and try to merge any part of SOURCE->url2@M:N we would break the
+     editor since no part of that actually exists.  See
+     http://svn.haxx.se/dev/archive-2008-11/0618.shtml.
 
      Find the gaps in the merge target's history, if any.  Eventually
      we will adjust CHILD->REMAINING_RANGES such that we don't describe
      non-existent paths to the editor. */
   SVN_ERR(find_gaps_in_merge_source_history(&gap_start, &gap_end,
                                             parent_merge_src_canon_path,
-                                            url1, revision1,
-                                            url2, revision2,
+                                            source,
                                             ra_session, merge_b,
                                             iterpool));
 
@@ -4328,7 +4313,7 @@ populate_remaining_ranges(apr_array_head
   for (i = 0; i < children_with_mergeinfo->nelts; i++)
     {
       const char *child_repos_path;
-      const char *child_url1, *child_url2;
+      merge_source_t child_source = *source;
       svn_client__merge_path_t *child =
         APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *);
       svn_client__merge_path_t *parent = NULL;
@@ -4346,10 +4331,10 @@ populate_remaining_ranges(apr_array_head
       child_repos_path = svn_dirent_skip_ancestor(merge_b->target_abspath,
                                                   child->abspath);
       SVN_ERR_ASSERT(child_repos_path != NULL);
-      child_url1 = svn_path_url_add_component2(url1, child_repos_path,
-                                               iterpool);
-      child_url2 = svn_path_url_add_component2(url2, child_repos_path,
-                                               iterpool);
+      child_source.url1 = svn_path_url_add_component2(source->url1, child_repos_path,
+                                                      iterpool);
+      child_source.url2 = svn_path_url_add_component2(source->url2, child_repos_path,
+                                                      iterpool);
 
       /* Get the explicit/inherited mergeinfo for CHILD.  If CHILD is the
          merge target then also get its implicit mergeinfo.  Otherwise defer
@@ -4362,8 +4347,8 @@ populate_remaining_ranges(apr_array_head
         &(child->inherited_mergeinfo),
         svn_mergeinfo_inherited, ra_session,
         child->abspath,
-        MAX(revision1, revision2),
-        MIN(revision1, revision2),
+        MAX(source->rev1, source->rev2),
+        MIN(source->rev1, source->rev2),
         merge_b->ctx, result_pool, iterpool));
 
       /* If CHILD isn't the merge target find its parent. */
@@ -4385,8 +4370,7 @@ populate_remaining_ranges(apr_array_head
       child_inherits_implicit = (parent && !child->switched);
 
       SVN_ERR(calculate_remaining_ranges(parent, child,
-                                         child_url1, revision1,
-                                         child_url2, revision2,
+                                         &child_source,
                                          child->pre_merge_mergeinfo,
                                          merge_b->implicit_src_gap,
                                          child_inherits_implicit,
@@ -4394,7 +4378,7 @@ populate_remaining_ranges(apr_array_head
                                          merge_b->ctx, result_pool,
                                          iterpool));
 
-      /* Deal with any gap in URL1@REVISION1:URL2@REVISION2's natural history.
+      /* Deal with any gap in SOURCE's natural history.
 
          If the gap is a proper subset of CHILD->REMAINING_RANGES then we can
          safely ignore it since we won't describe this path/rev pair.
@@ -4415,7 +4399,7 @@ populate_remaining_ranges(apr_array_head
 
           /* If this is a reverse merge reorder CHILD->REMAINING_RANGES
               so it will work with the svn_rangelist_* APIs below. */
-          if (revision1 > revision2)
+          if (source->rev1 > source->rev2)
             SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, iterpool));
 
           for (j = 0; j < child->remaining_ranges->nelts; j++)
@@ -4457,7 +4441,7 @@ populate_remaining_ranges(apr_array_head
                                              result_pool));
             }
 
-          if (revision1 > revision2) /* Reverse merge */
+          if (source->rev1 > source->rev2) /* Reverse merge */
             SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, iterpool));
         }
     }
@@ -4777,13 +4761,12 @@ remove_children_with_deleted_mergeinfo(m
 
 /* Helper for do_directory_merge().
 
-   Set up the diff editor report to merge URL1@REVISION1 to URL2@REVISION2
+   Set up the diff editor report to merge the SOURCE diff
    into TARGET_ABSPATH and drive it.
 
    If mergeinfo is not being honored based on MERGE_B, see the doc string for
    HONOR_MERGEINFO() for how this is determined, then ignore
-   CHILDREN_WITH_MERGEINFO and merge the diff between URL1@REVISION1 and
-   URL2@REVISION2 to TARGET_ABSPATH.
+   CHILDREN_WITH_MERGEINFO and merge the SOURCE diff to TARGET_ABSPATH.
 
    If mergeinfo is being honored then perform a history-aware merge,
    describing TARGET_ABSPATH and its subtrees to the reporter in such as way
@@ -4795,36 +4778,36 @@ remove_children_with_deleted_mergeinfo(m
    svn_client__merge_path_t * -- see 'THE CHILDREN_WITH_MERGEINFO ARRAY'
    comment at the top of this file for more info.  Note that it is possible
    TARGET_ABSPATH and/or some of its subtrees need only a subset, or no part,
-   of REVISION1:REVISION2 to be merged.  Though there is little point to
+   of SOURCE to be merged.  Though there is little point to
    calling this function if TARGET_ABSPATH and all its subtrees have already
-   had URL1@REVISION1 to URL2@REVISION2 merged, this will work but is a no-op.
+   had SOURCE merged, this will work but is a no-op.
 
-   REVISION1 and REVISION2 must be bound by the set of remaining_ranges
+   SOURCE->rev1 and SOURCE->rev2 must be bound by the set of remaining_ranges
    fields in CHILDREN_WITH_MERGEINFO's elements, specifically:
 
-   For forward merges (REVISION1 < REVISION2):
+   For forward merges (SOURCE->rev1 < SOURCE->rev2):
 
      1) The first svn_merge_range_t * element of each child's remaining_ranges
         array must meet one of the following conditions:
 
-        a) The range's start field is greater than or equal to REVISION2.
+        a) The range's start field is greater than or equal to SOURCE->rev2.
 
-        b) The range's end field is REVISION2.
+        b) The range's end field is SOURCE->rev2.
 
      2) Among all the ranges that meet condition 'b' the oldest start
-        revision must equal REVISION1.
+        revision must equal SOURCE->rev1.
 
-   For reverse merges (REVISION1 > REVISION2):
+   For reverse merges (SOURCE->rev1 > SOURCE->rev2):
 
      1) The first svn_merge_range_t * element of each child's remaining_ranges
         array must meet one of the following conditions:
 
-        a) The range's start field is less than or equal to REVISION2.
+        a) The range's start field is less than or equal to SOURCE->rev2.
 
-        b) The range's end field is REVISION2.
+        b) The range's end field is SOURCE->rev2.
 
      2) Among all the ranges that meet condition 'b' the youngest start
-        revision must equal REVISION1.
+        revision must equal SOURCE->rev1.
 
    Note: If the first svn_merge_range_t * element of some subtree child's
    remaining_ranges array is the same as the first range of that child's
@@ -4834,17 +4817,14 @@ remove_children_with_deleted_mergeinfo(m
    DEPTH, NOTIFY_B, and MERGE_B are cascasded from do_directory_merge(), see
    that function for more info.
 
-   If MERGE_B->sources_ancestral is set, then URL1@REVISION1 must be a
-   historical ancestor of URL2@REVISION2, or vice-versa (see
+   If MERGE_B->sources_ancestral is set, then SOURCE->url1@rev1 must be a
+   historical ancestor of SOURCE->url2@rev2, or vice-versa (see
    `MERGEINFO MERGE SOURCE NORMALIZATION' for more requirements around
-   the values of URL1, REVISION1, URL2, and REVISION2 in this case).
+   SOURCE in this case).
 */
 static svn_error_t *
 drive_merge_report_editor(const char *target_abspath,
-                          const char *url1,
-                          svn_revnum_t revision1,
-                          const char *url2,
-                          svn_revnum_t revision2,
+                          const merge_source_t *source,
                           const apr_array_header_t *children_with_mergeinfo,
                           svn_depth_t depth,
                           notification_receiver_baton_t *notify_b,
@@ -4858,18 +4838,18 @@ drive_merge_report_editor(const char *ta
   svn_revnum_t target_start;
   svn_boolean_t honor_mergeinfo;
   const char *old_sess2_url;
-  svn_boolean_t is_rollback = revision1 > revision2;
+  svn_boolean_t is_rollback = source->rev1 > source->rev2;
 
   honor_mergeinfo = HONOR_MERGEINFO(merge_b);
 
   /* Start with a safe default starting revision for the editor and the
      merge target. */
-  target_start = revision1;
+  target_start = source->rev1;
 
   /* If we are honoring mergeinfo the starting revision for the merge target
-     might not be REVISION1, in fact the merge target might not need *any*
-     part of REVISION1:REVISION2 merged -- Instead some subtree of the target
-     needs REVISION1:REVISION2 -- So get the right starting revision for the
+     might not be SOURCE->rev1, in fact the merge target might not need *any*
+     part of SOURCE merged -- Instead some subtree of the target
+     needs SOURCE -- So get the right starting revision for the
      target. */
   if (honor_mergeinfo)
     {
@@ -4890,22 +4870,22 @@ drive_merge_report_editor(const char *ta
       if (child->remaining_ranges->nelts == 0)
         {
           /* The merge target doesn't need anything merged. */
-          target_start = revision2;
+          target_start = source->rev2;
         }
       else
         {
           /* The merge target has remaining revisions to merge.  These
              ranges may fully or partially overlap the range described
-             by REVISION1:REVISION2 or may not intersect that range at
+             by SOURCE->rev1:rev2 or may not intersect that range at
              all. */
           svn_merge_range_t *range =
             APR_ARRAY_IDX(child->remaining_ranges, 0,
                           svn_merge_range_t *);
-          if ((!is_rollback && range->start > revision2)
-              || (is_rollback && range->start < revision2))
+          if ((!is_rollback && range->start > source->rev2)
+              || (is_rollback && range->start < source->rev2))
             {
               /* Merge target's first remaining range doesn't intersect. */
-              target_start = revision2;
+              target_start = source->rev2;
             }
           else
             {
@@ -4916,17 +4896,17 @@ drive_merge_report_editor(const char *ta
         }
     }
 
-  /* Temporarily point our second RA session to URL1, too.  We use
+  /* Temporarily point our second RA session to SOURCE->url1, too.  We use
      this to request individual file contents. */
   SVN_ERR(svn_client__ensure_ra_session_url(&old_sess2_url,
                                             merge_b->ra_session2,
-                                            url1, scratch_pool));
+                                            source->url1, scratch_pool));
 
   /* Get the diff editor and a reporter with which to, ultimately,
      drive it. */
   SVN_ERR(svn_client__get_diff_editor(&diff_editor, &diff_edit_baton,
                                       depth,
-                                      merge_b->ra_session2, revision1,
+                                      merge_b->ra_session2, source->rev1,
                                       FALSE /* walk_deleted_dirs */,
                                       TRUE /* text_deltas */,
                                       &merge_callbacks, merge_b,
@@ -4935,10 +4915,11 @@ drive_merge_report_editor(const char *ta
                                       notification_receiver, notify_b,
                                       scratch_pool));
   SVN_ERR(svn_ra_do_diff3(merge_b->ra_session1,
-                          &reporter, &report_baton, revision2,
+                          &reporter, &report_baton, source->rev2,
                           "", depth, merge_b->ignore_ancestry,
                           TRUE,  /* text_deltas */
-                          url2, diff_editor, diff_edit_baton, scratch_pool));
+                          source->url2, diff_editor, diff_edit_baton,
+                          scratch_pool));
 
   /* Drive the reporter. */
   SVN_ERR(reporter->set_path(report_baton, "", target_start, depth,
@@ -4974,16 +4955,16 @@ drive_merge_report_editor(const char *ta
 
           /* If a subtree needs the same range applied as its nearest parent
              with mergeinfo or neither the subtree nor this parent need
-             REVISION1:REVISION2 merged, then we don't need to describe the
+             SOURCE->rev1:rev2 merged, then we don't need to describe the
              subtree separately.  In the latter case this could break the
-             editor if child->abspath didn't exist at REVISION2 and we
+             editor if child->abspath didn't exist at SOURCE->rev2 and we
              attempt to describe it via a reporter set_path call. */
           if (child->remaining_ranges->nelts)
             {
               range = APR_ARRAY_IDX(child->remaining_ranges, 0,
                                     svn_merge_range_t *);
-              if ((!is_rollback && range->start > revision2)
-                  || (is_rollback && range->start < revision2))
+              if ((!is_rollback && range->start > source->rev2)
+                  || (is_rollback && range->start < source->rev2))
                 {
                   /* This child's first remaining range comes after the range
                      we are currently merging, so skip it. We expect to get
@@ -5021,14 +5002,14 @@ drive_merge_report_editor(const char *ta
           SVN_ERR_ASSERT(child_repos_path);
 
           if ((child->remaining_ranges->nelts == 0)
-              || (is_rollback && (range->start < revision2))
-              || (!is_rollback && (range->start > revision2)))
+              || (is_rollback && (range->start < source->rev2))
+              || (!is_rollback && (range->start > source->rev2)))
             {
               /* Nothing to merge to this child.  We'll claim we have
                  it up to date so the server doesn't send us
                  anything. */
               SVN_ERR(reporter->set_path(report_baton, child_repos_path,
-                                         revision2, depth, FALSE,
+                                         source->rev2, depth, FALSE,
                                          NULL, iterpool));
             }
           else
@@ -5624,24 +5605,12 @@ get_wc_explicit_mergeinfo_catalog(apr_ha
    depth-first ordering it is guaranteed to be the first element in
    *CHILDREN_WITH_MERGEINFO.
 
-   MERGE_COMMAND_BATON, URL1, URL2, REVISION1, and REVISION2 are
-   cascaded from the arguments of the same name in do_directory_merge().
-
-   RA_SESSION is the session for the younger of URL1@REVISION1 and
-   URL2@REVISION2.
-
-   MERGE_SRC_CANON_PATH is the path of of the younger of URL1@REVISION1 and
-   URL2@REVISION2, relative to RA_SESSION's repos root (with a leading '/').
+   MERGE_CMD_BATON is cascaded from the argument of the same name in
+   do_directory_merge().
 */
 static svn_error_t *
 get_mergeinfo_paths(apr_array_header_t *children_with_mergeinfo,
                     merge_cmd_baton_t *merge_cmd_baton,
-                    const char *merge_src_canon_path,
-                    const char *url1,
-                    const char *url2,
-                    svn_revnum_t revision1,
-                    svn_revnum_t revision2,
-                    svn_ra_session_t *ra_session,
                     svn_depth_t depth,
                     apr_pool_t *result_pool,
                     apr_pool_t *scratch_pool)
@@ -6455,14 +6424,10 @@ normalize_merge_sources_internal(apr_arr
    requested merges; order the objects from oldest to youngest.
 
    Determine the requested merges by examining SOURCE_PATH_OR_URL (and its
-   associated URL, SOURCE_URL) and SOURCE_PEG_REVISION (which specifies the
-   line of history from which merges will be pulled) and
+   associated URL and revision, SOURCE_URL and SOURCE_PEG_REVNUM) (which
+   specifies the line of history from which merges will be pulled) and
    RANGES_TO_MERGE (a list of svn_opt_revision_range_t's which provide
-   revision ranges).  Note that SOURCE_PATH_OR_URL may itself be either a
-   working copy path or a URL; in the latter case, SOURCE_URL is probably
-   identical to SOURCE_PATH_OR_URL.
-
-   If SOURCE_PEG_REVISION is unspecified, treat that it as HEAD.
+   revision ranges).
 
    Use RA_SESSION -- whose session URL matches SOURCE_URL -- to answer
    historical questions.
@@ -6479,7 +6444,7 @@ static svn_error_t *
 normalize_merge_sources(apr_array_header_t **merge_sources_p,
                         const char *source_path_or_url,
                         const char *source_url,
-                        const svn_opt_revision_t *source_peg_revision,
+                        svn_revnum_t source_peg_revnum,
                         const apr_array_header_t *ranges_to_merge,
                         svn_ra_session_t *ra_session,
                         svn_client_ctx_t *ctx,
@@ -6487,7 +6452,6 @@ normalize_merge_sources(apr_array_header
                         apr_pool_t *scratch_pool)
 {
   const char *source_abspath_or_url;
-  svn_revnum_t source_peg_revnum;
   svn_revnum_t youngest_rev = SVN_INVALID_REVNUM;
   apr_array_header_t *merge_range_ts;
   int i;
@@ -6499,15 +6463,6 @@ normalize_merge_sources(apr_array_header
   else
     source_abspath_or_url = source_path_or_url;
 
-  /* Resolve our SOURCE_PEG_REVISION to a real number. */
-  SVN_ERR(svn_client__get_revision_number(&source_peg_revnum, &youngest_rev,
-                                          ctx->wc_ctx,
-                                          source_abspath_or_url,
-                                          ra_session, source_peg_revision,
-                                          iterpool));
-  if (! SVN_IS_VALID_REVNUM(source_peg_revnum))
-    return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
-
   /* Create a list to hold svn_merge_range_t's. */
   merge_range_ts = apr_array_make(scratch_pool, ranges_to_merge->nelts,
                                   sizeof(svn_merge_range_t *));
@@ -6619,8 +6574,8 @@ filter_natural_history_from_mergeinfo(ap
 
    Additional parameters:
 
-   If SOURCES_RELATED is set, the "left" and "right" sides of the
-   merge source are historically related (ancestors, uncles, second
+   If SOURCES_RELATED is set, the "left" and "right" sides of SOURCE are
+   historically related (ancestors, uncles, second
    cousins thrice removed, etc...).  (This is used to simulate the
    history checks that the repository logic does in the directory case.)
 
@@ -6630,15 +6585,12 @@ filter_natural_history_from_mergeinfo(ap
    and the value is the new mergeinfo for that path.  Allocate additions
    to RESULT_CATALOG in pool which RESULT_CATALOG was created in.
 
-   Note: MERGE_B->RA_SESSION1 must be associated with URL1 and
-   MERGE_B->RA_SESSION2 with URL2.
+   Note: MERGE_B->RA_SESSION1 must be associated with SOURCE->url1 and
+   MERGE_B->RA_SESSION2 with SOURCE->url2.
 */
 static svn_error_t *
 do_file_merge(svn_mergeinfo_catalog_t result_catalog,
-              const char *url1,
-              svn_revnum_t revision1,
-              const char *url2,
-              svn_revnum_t revision2,
+              const merge_source_t *source,
               const char *target_abspath,
               svn_boolean_t sources_related,
               svn_boolean_t squelch_mergeinfo_notifications,
@@ -6653,8 +6605,8 @@ do_file_merge(svn_mergeinfo_catalog_t re
   svn_mergeinfo_t target_mergeinfo;
   svn_merge_range_t *conflicted_range = NULL;
   svn_boolean_t inherited = FALSE;
-  svn_boolean_t is_rollback = (revision1 > revision2);
-  const char *primary_url = is_rollback ? url1 : url2;
+  svn_boolean_t is_rollback = (source->rev1 > source->rev2);
+  const char *primary_url = is_rollback ? source->url1 : source->url2;
   const char *target_url;
   svn_boolean_t honor_mergeinfo, record_mergeinfo;
   svn_client__merge_path_t *merge_target = NULL;
@@ -6672,8 +6624,8 @@ do_file_merge(svn_mergeinfo_catalog_t re
                                target_abspath,
                                iterpool, iterpool));
 
-  range.start = revision1;
-  range.end = revision2;
+  range.start = source->rev1;
+  range.end = source->rev2;
   range.inheritable = TRUE;
   if (honor_mergeinfo)
     {
@@ -6694,8 +6646,8 @@ do_file_merge(svn_mergeinfo_catalog_t re
                                &(merge_target->implicit_mergeinfo),
                                &inherited, svn_mergeinfo_inherited,
                                merge_b->ra_session1, target_abspath,
-                               MAX(revision1, revision2),
-                               MIN(revision1, revision2),
+                               MAX(source->rev1, source->rev2),
+                               MIN(source->rev1, source->rev2),
                                ctx, scratch_pool, iterpool);
 
       if (err)
@@ -6711,15 +6663,15 @@ do_file_merge(svn_mergeinfo_catalog_t re
           return svn_error_trace(err);
         }
 
-      SVN_ERR(svn_ra_reparent(merge_b->ra_session1, url1, iterpool));
+      SVN_ERR(svn_ra_reparent(merge_b->ra_session1, source->url1, iterpool));
 
       /* Calculate remaining merges unless this is a record only merge.
          In that case the remaining range is the whole range described
-         by REVISION1:REVISION2. */
+         by SOURCE->rev1:rev2. */
       if (!merge_b->record_only)
         {
           SVN_ERR(calculate_remaining_ranges(NULL, merge_target,
-                                             url1, revision1, url2, revision2,
+                                             source,
                                              target_mergeinfo,
                                              merge_b->implicit_src_gap, FALSE,
                                              merge_b->ra_session1,
@@ -6729,7 +6681,7 @@ do_file_merge(svn_mergeinfo_catalog_t re
         }
     }
 
-  /* The simple cases where our remaining range is REVISION1:REVISION2. */
+  /* The simple cases where our remaining range is SOURCE->rev1:rev2. */
   if (!honor_mergeinfo || merge_b->record_only)
     {
       remaining_ranges = apr_array_make(scratch_pool, 1, sizeof(&range));
@@ -6777,23 +6729,22 @@ do_file_merge(svn_mergeinfo_catalog_t re
 
           svn_pool_clear(iterpool);
 
-          /* Issue #3174: If we are honoring mergeinfo, then URL1, URL2,
-             REVISION1, and REVISION2 meet the conditions described in
-             'MERGEINFO MERGE SOURCE NORMALIZATION'.  This means that
-             URL1@REVISION1 may be the copy source of URL2@REVISION2.
-             If this is the case, then URL1 != URL2.  Since
-             MERGE_B->RA_SESSION1 is always opened with URL1, the only time
-             we can safely call single_file_merge_get_file() with that RA
-             session is for REVISION1 (or REVISION2 if this is a reverse
-             merge). */
+          /* Issue #3174: If we are honoring mergeinfo, then SOURCE meets the
+             conditions described in 'MERGEINFO MERGE SOURCE NORMALIZATION'.
+             This means that @SOURCE->url1@rev1 may be the copy source of
+             SOURCE->url2@rev2. If this is the case, then ->url1 != ->url2.
+             Since MERGE_B->RA_SESSION1 is always opened with ->url1, the
+             only time we can safely call single_file_merge_get_file() with
+             that RA session is for SOURCE->rev1 (or ->rev2 if this is a
+             reverse merge). */
           ra_session1 = merge_b->ra_session1;
           ra_session2 = merge_b->ra_session2;
-          if (honor_mergeinfo && strcmp(url1, url2) != 0)
+          if (honor_mergeinfo && strcmp(source->url1, source->url2) != 0)
             {
-              if (!is_rollback && r->start != revision1)
-                ra_session1 = ra_session2; /* Use URL2's RA session. */
-              else if (is_rollback && r->end != revision2)
-                ra_session2 = ra_session1; /* Use URL1's RA session. */
+              if (!is_rollback && r->start != source->rev1)
+                ra_session1 = ra_session2; /* Use SOURCE->url2's RA session. */
+              else if (is_rollback && r->end != source->rev2)
+                ra_session2 = ra_session1; /* Use SOURCE->url1's RA session. */
             }
 
           /* While we currently don't allow it, in theory we could be
@@ -7139,9 +7090,9 @@ subtree_touched_by_merge(const char *loc
 
 /* Helper for do_directory_merge() when performing mergeinfo unaware merges.
 
-   Merge the diff of URL1@REVISION1:URL2@REVISION2 to TARGET_DIR_WCPATH.
+   Merge the SOURCE diff into TARGET_DIR_WCPATH.
 
-   URL1, REVISION1, URL2, REVISION2, DEPTH, NOTIFY_B, and MERGE_B
+   SOURCE, DEPTH, NOTIFY_B, and MERGE_B
    are all cascaded from do_directory_merge's arguments of the same names.
 
    NOTE: This is a very thin wrapper around drive_merge_report_editor() and
@@ -7149,10 +7100,7 @@ subtree_touched_by_merge(const char *loc
    element expected during mergeinfo unaware merges.
 */
 static svn_error_t *
-do_mergeinfo_unaware_dir_merge(const char *url1,
-                               svn_revnum_t revision1,
-                               const char *url2,
-                               svn_revnum_t revision2,
+do_mergeinfo_unaware_dir_merge(const merge_source_t *source,
                                const char *target_dir_wcpath,
                                svn_depth_t depth,
                                notification_receiver_baton_t *notify_b,
@@ -7160,17 +7108,17 @@ do_mergeinfo_unaware_dir_merge(const cha
                                apr_pool_t *pool)
 {
   /* Initialize NOTIFY_B->CHILDREN_WITH_MERGEINFO and populate it with
-     one element describing the merge of REVISION1:REVISION2 to
+     one element describing the merge of SOURCE->rev1:rev2 to
      TARGET_DIR_WCPATH. */
   svn_client__merge_path_t *item
     = svn_client__merge_path_create(target_dir_wcpath, pool);
 
-  item->remaining_ranges = svn_rangelist__initialize(revision1, revision2,
+  item->remaining_ranges = svn_rangelist__initialize(source->rev1, source->rev2,
                                                      TRUE, pool);
   APR_ARRAY_PUSH(notify_b->children_with_mergeinfo,
                  svn_client__merge_path_t *) = item;
   return drive_merge_report_editor(target_dir_wcpath,
-                                   url1, revision1, url2, revision2,
+                                   source,
                                    NULL, depth, notify_b,
                                    merge_b, pool);
 }
@@ -7187,6 +7135,7 @@ typedef struct log_find_operative_subtre
   const char *merge_source_fspath;
   const char *merge_target_abspath;
   svn_depth_t depth;
+  svn_wc_context_t *wc_ctx;
 
   /* A pool to allocate additions to the hashes in. */
   apr_pool_t *result_pool;
@@ -7235,10 +7184,39 @@ log_find_operative_subtree_revs(void *ba
           child = svn_relpath_dirname(rel_path, iterpool);
           if (child[0] == '\0')
             {
+              /* The svn_log_changed_path2_t.node_kind members in
+                 LOG_ENTRY->CHANGED_PATHS2 may be set to
+                 svn_node_unknown, see svn_log_changed_path2_t and
+                 svn_fs_paths_changed2.  In that case we check the
+                 type of the corresponding subtree in the merge
+                 target. */
+              svn_node_kind_t node_kind;
+
+              if (change->node_kind == svn_node_unknown)
+                {
+                  const char *wc_child_abspath =
+                    svn_dirent_join(log_baton->merge_target_abspath,
+                                    rel_path, iterpool);
+
+                  /* ### ptb - svn_wc_read_kind is very tolerant when we ask
+                     ### it about unversioned, non-existent, and missing WC
+                     ### paths, simply setting *NODE_KIND svn_kind_none in
+                     ### those cases.  Is there any legitimate error we
+                     ### might enocunter during a merge where we'd want
+                     ### to clear the error and continue? */
+                  SVN_ERR(svn_wc_read_kind(&node_kind, log_baton->wc_ctx,
+                                           wc_child_abspath, FALSE,
+                                           iterpool));
+                }
+              else
+                {
+                  node_kind = change->node_kind;
+                }
+
               /* We only care about immediate directory children if
                  DEPTH is svn_depth_files. */
               if (log_baton->depth == svn_depth_files
-                  && change->node_kind == svn_node_file)
+                  && node_kind != svn_node_dir)
                 continue;
 
               /* If depth is svn_depth_immediates, then we only care
@@ -7328,6 +7306,7 @@ get_operative_immediate_children(apr_has
   log_baton.merge_source_fspath = merge_source_fspath;
   log_baton.merge_target_abspath = merge_target_abspath;
   log_baton.depth = depth;
+  log_baton.wc_ctx = wc_ctx;
   log_baton.result_pool = result_pool;
   log_targets = apr_array_make(scratch_pool, 1, sizeof(const char *));
   APR_ARRAY_PUSH(log_targets, const char *) = "";
@@ -8192,9 +8171,8 @@ log_noop_revs(void *baton,
 
 /* Helper for do_directory_merge().
 
-   URL1, REVISION1, URL2, REVISION2, and MERGE_B are
-   cascaded from the arguments of the same name in do_directory_merge().
-   RA_SESSION is the session for URL2@REVISION2.
+   SOURCE and MERGE_B are cascaded from the arguments of the same name in
+   do_directory_merge().  RA_SESSION is the session for SOURCE->url2@rev2.
 
    Find all the ranges required by subtrees in
    CHILDREN_WITH_MERGEINFO that are *not* required by
@@ -8204,13 +8182,10 @@ log_noop_revs(void *baton,
    these ranges from all of the subtree's REMAINING_RANGES.
 
    This function should only be called when honoring mergeinfo during
-   forward merges (i.e. REVISION1 < REVSION2).
+   forward merges (i.e. SOURCE->rev1 < SOURCE->rev2).
 */
 static svn_error_t *
-remove_noop_subtree_ranges(const char *url1,
-                           svn_revnum_t revision1,
-                           const char *url2,
-                           svn_revnum_t revision2,
+remove_noop_subtree_ranges(const merge_source_t *source,
                            svn_ra_session_t *ra_session,
                            apr_array_header_t *children_with_mergeinfo,
                            merge_cmd_baton_t *merge_b,
@@ -8233,7 +8208,7 @@ remove_noop_subtree_ranges(const char *u
 
 
   /* This function is only intended to work with forward merges. */
-  if (revision1 > revision2)
+  if (source->rev1 > source->rev2)
     return SVN_NO_ERROR;
 
   /* Another easy out: There are no subtrees. */
@@ -8244,10 +8219,10 @@ remove_noop_subtree_ranges(const char *u
                                             sizeof(svn_merge_range_t *));
   log_targets = apr_array_make(scratch_pool, 1, sizeof(const char *));
 
-  /* Given the requested merge of REVISION1:REVISION2 might there be any
+  /* Given the requested merge of SOURCE->rev1:rev2 might there be any
      part of this range required for subtrees but not for the target? */
-  requested_ranges = svn_rangelist__initialize(MIN(revision1, revision2),
-                                               MAX(revision1, revision2),
+  requested_ranges = svn_rangelist__initialize(MIN(source->rev1, source->rev2),
+                                               MAX(source->rev1, source->rev2),
                                                TRUE, scratch_pool);
   SVN_ERR(svn_rangelist_remove(&subtree_gap_ranges,
                                root_child->remaining_ranges,
@@ -8278,7 +8253,7 @@ remove_noop_subtree_ranges(const char *u
   if (!subtree_remaining_ranges->nelts)
     return SVN_NO_ERROR;
 
-  /* Ok, *finally* we can answer what part(s) of REVISION1:REVISION2 are
+  /* Ok, *finally* we can answer what part(s) of SOURCE->rev1:rev2 are
      required for the subtrees but not the target. */
   SVN_ERR(svn_rangelist_intersect(&subtree_gap_ranges,
                                   subtree_gap_ranges,
@@ -8305,7 +8280,7 @@ remove_noop_subtree_ranges(const char *u
                     result_pool, scratch_pool));
   SVN_ERR(svn_client__path_relative_to_root(
                     &(log_gap_baton.source_fspath), merge_b->ctx->wc_ctx,
-                    url2, merge_b->repos_root_url, TRUE, NULL,
+                    source->url2, merge_b->repos_root_url, TRUE, NULL,
                     result_pool, scratch_pool));
   log_gap_baton.merged_ranges = apr_array_make(scratch_pool, 0,
                                                sizeof(svn_revnum_t *));
@@ -8360,17 +8335,16 @@ remove_noop_subtree_ranges(const char *u
 
 /* Helper for do_merge() when the merge target is a directory.
 
-   Perform a merge of changes between URL1@REVISION1 and URL2@REVISION2
-   to the working copy path TARGET_ABSPATH.
-   URL1, URL2, and TARGET_ABSPATH all represent directories -- for the
-   single file case, the caller should use do_file_merge().
+   Perform a merge of changes in SOURCE to the working copy path
+   TARGET_ABSPATH. Both URLs in SOURCE, and TARGET_ABSPATH all represent
+   directories -- for the single file case, the caller should use
+   do_file_merge().
 
    MERGE_B is the merge_cmd_baton_t created by do_merge() that describes
    the merge being performed.  If MERGE_B->sources_ancestral is set, then
-   URL1@REVISION1 must be a historical ancestor of URL2@REVISION2, or
+   SOURCE->url1@rev1 must be a historical ancestor of SOURCE->url2@rev2, or
    vice-versa (see `MERGEINFO MERGE SOURCE NORMALIZATION' for more
-   requirements around the values of URL1, REVISION1, URL2, and REVISION2
-   in this case).
+   requirements around SOURCE in this case).
 
    If mergeinfo is being recorded, SQUELCH_MERGEINFO_NOTIFICATIONS is FALSE,
    and MERGE_B->CTX->NOTIFY_FUNC2 is not NULL, then call
@@ -8398,10 +8372,7 @@ remove_noop_subtree_ranges(const char *u
 */
 static svn_error_t *
 do_directory_merge(svn_mergeinfo_catalog_t result_catalog,
-                   const char *url1,
-                   svn_revnum_t revision1,
-                   const char *url2,
-                   svn_revnum_t revision2,
+                   const merge_source_t *source,
                    const char *target_abspath,
                    svn_depth_t depth,
                    svn_boolean_t squelch_mergeinfo_notifications,
@@ -8416,9 +8387,9 @@ do_directory_merge(svn_mergeinfo_catalog
   /* The range defining the mergeinfo we will record to describe the merge
      (assuming we are recording mergeinfo
 
-     Note: This may be a subset of REVISION1:REVISION2 if
+     Note: This may be a subset of SOURCE->rev1:rev2 if
      populate_remaining_ranges() determines that some part of
-     REVISION1:REVISION2 has already been wholly merged to TARGET_ABSPATH.
+     SOURCE->rev1:rev2 has already been wholly merged to TARGET_ABSPATH.
      Also, the actual editor drive(s) may be a subset of RANGE, if
      remove_noop_subtree_ranges() and/or fix_deleted_subtree_ranges()
      further tweak things. */
@@ -8426,11 +8397,11 @@ do_directory_merge(svn_mergeinfo_catalog
 
   svn_ra_session_t *ra_session;
   svn_client__merge_path_t *target_merge_path;
-  svn_boolean_t is_rollback = (revision1 > revision2);
-  const char *primary_url = is_rollback ? url1 : url2;
+  svn_boolean_t is_rollback = (source->rev1 > source->rev2);
+  const char *primary_url = is_rollback ? source->url1 : source->url2;
   const char *mergeinfo_path;
   svn_boolean_t honor_mergeinfo, record_mergeinfo;
-  svn_boolean_t same_urls = (strcmp(url1, url2) == 0);
+  svn_boolean_t same_urls = (strcmp(source->url1, source->url2) == 0);
 
   honor_mergeinfo = HONOR_MERGEINFO(merge_b);
   record_mergeinfo = RECORD_MERGEINFO(merge_b);
@@ -8446,7 +8417,7 @@ do_directory_merge(svn_mergeinfo_catalog
   /* If we are not honoring mergeinfo we can skip right to the
      business of merging changes! */
   if (!honor_mergeinfo)
-    return do_mergeinfo_unaware_dir_merge(url1, revision1, url2, revision2,
+    return do_mergeinfo_unaware_dir_merge(source,
                                           target_abspath, depth,
                                           notify_b, merge_b, scratch_pool);
 
@@ -8466,9 +8437,7 @@ do_directory_merge(svn_mergeinfo_catalog
                                               primary_url, scratch_pool));
 
   SVN_ERR(get_mergeinfo_paths(notify_b->children_with_mergeinfo, merge_b,
-                              mergeinfo_path,
-                              url1, url2, revision1, revision2,
-                              ra_session, depth, scratch_pool, scratch_pool));
+                              depth, scratch_pool, scratch_pool));
 
   /* The first item from the NOTIFY_B->CHILDREN_WITH_MERGEINFO is always
      the target thanks to depth-first ordering. */
@@ -8482,15 +8451,15 @@ do_directory_merge(svn_mergeinfo_catalog
      merged, and then merge it.  Otherwise, we just merge what we were asked
      to merge across the whole tree.  */
   SVN_ERR(populate_remaining_ranges(notify_b->children_with_mergeinfo,
-                                    url1, revision1, url2, revision2,
+                                    source,
                                     honor_mergeinfo,
                                     ra_session, mergeinfo_path,
                                     merge_b, scratch_pool, scratch_pool));
 
   /* Always start with a range which describes the most inclusive merge
-     possible, i.e. REVISION1:REVISION2. */
-  range.start = revision1;
-  range.end = revision2;
+     possible, i.e. SOURCE->rev1:rev2. */
+  range.start = source->rev1;
+  range.end = source->rev2;
   range.inheritable = TRUE;
 
   if (honor_mergeinfo && !merge_b->reintegrate_merge)
@@ -8499,7 +8468,7 @@ do_directory_merge(svn_mergeinfo_catalog
       apr_pool_t *iterpool = svn_pool_create(scratch_pool);
 
       /* The merge target TARGET_ABSPATH and/or its subtrees may not need all
-         of REVISION1:REVISION2 applied.  So examine
+         of SOURCE->rev1:rev2 applied.  So examine
          NOTIFY_B->CHILDREN_WITH_MERGEINFO to find the oldest starting
          revision that actually needs to be merged (for reverse merges this is
          the youngest starting revision).
@@ -8515,13 +8484,13 @@ do_directory_merge(svn_mergeinfo_catalog
       /* Remove inoperative ranges from any subtrees' remaining_ranges
          to spare the expense of noop editor drives. */
       if (!is_rollback)
-        SVN_ERR(remove_noop_subtree_ranges(url1, revision1, url2, revision2,
+        SVN_ERR(remove_noop_subtree_ranges(source,
                                            ra_session,
                                            notify_b->children_with_mergeinfo,
                                            merge_b, scratch_pool, iterpool));
 
       /* Adjust subtrees' remaining_ranges to deal with issue #3067 */
-      SVN_ERR(fix_deleted_subtree_ranges(url1, revision1, url2, revision2,
+      SVN_ERR(fix_deleted_subtree_ranges(source,
                                          ra_session,
                                          notify_b->children_with_mergeinfo,
                                          merge_b, scratch_pool, iterpool));
@@ -8573,7 +8542,7 @@ do_directory_merge(svn_mergeinfo_catalog
           while (end_rev != SVN_INVALID_REVNUM)
             {
               svn_revnum_t next_end_rev;
-              const char *real_url1 = url1, *real_url2 = url2;
+              merge_source_t real_source;
               const char *old_sess1_url = NULL, *old_sess2_url = NULL;
               svn_merge_range_t *first_target_range
                 = (target_merge_path->remaining_ranges->nelts == 0 ? NULL
@@ -8611,47 +8580,51 @@ do_directory_merge(svn_mergeinfo_catalog
                                      is_rollback, end_rev, scratch_pool);
               notify_b->cur_ancestor_abspath = NULL;
 
-              /* URL1@REVISION1 is a real location; URL2@REVISION2 is a
+              /* In SOURCE: URL1@REV1 is a real location; URL2@REV2 is a
                  real location -- that much we know (thanks to the merge
                  source normalization code).  But for revisions between
                  them, the URLs might differ.  Here are the rules:
 
-                   * If URL1 == URL2, then all URLs between REVISION1 and
-                     REVISION2 also match URL1/URL2.
+                   * If URL1 == URL2, then all URLs between REV1 and
+                     REV2 also match URL1/URL2.
 
                    * If URL1 != URL2, then:
 
-                       * If REVISION1 < REVISION2, only REVISION1 maps to
-                         URL1.  The revisions between REVISION1+1 and
-                         REVISION2 (inclusive) map to URL2.
-
-                       * If REVISION1 > REVISION2, Only REVISION2 maps to
-                         URL2.  The revisions between REVISION1 and
-                         REVISION2+1 (inclusive) map to URL1.
+                       * If REV1 < REV2, only REV1 maps to
+                         URL1.  The revisions between REV1+1 and
+                         REV2 (inclusive) map to URL2.
+
+                       * If REV1 > REV2, Only REV2 maps to
+                         URL2.  The revisions between REV1 and
+                         REV2+1 (inclusive) map to URL1.
 
                  We need to adjust our URLs accordingly, here.
               */
+              real_source.url1 = source->url1;
+              real_source.rev1 = start_rev;
+              real_source.url2 = source->url2;
+              real_source.rev2 = end_rev;
               if (! same_urls)
                 {
-                  if (is_rollback && (end_rev != revision2))
+                  if (is_rollback && (end_rev != source->rev2))
                     {
-                      real_url2 = url1;
+                      real_source.url2 = source->url1;
                       SVN_ERR(svn_client__ensure_ra_session_url
                               (&old_sess2_url, merge_b->ra_session2,
-                               real_url2, iterpool));
+                               real_source.url2, iterpool));
                     }
-                  if ((! is_rollback) && (start_rev != revision1))
+                  if ((! is_rollback) && (start_rev != source->rev1))
                     {
-                      real_url1 = url2;
+                      real_source.url1 = source->url2;
                       SVN_ERR(svn_client__ensure_ra_session_url
                               (&old_sess1_url, merge_b->ra_session1,
-                               real_url1, iterpool));
+                               real_source.url1, iterpool));
                     }
                 }
+
               SVN_ERR(drive_merge_report_editor(
                 merge_b->target_abspath,
-                real_url1, start_rev,
-                real_url2, end_rev,
+                &real_source,
                 notify_b->children_with_mergeinfo,
                 depth, notify_b,
                 merge_b,
@@ -8717,7 +8690,7 @@ do_directory_merge(svn_mergeinfo_catalog
           notify_b->cur_ancestor_abspath = NULL;
 
           SVN_ERR(drive_merge_report_editor(merge_b->target_abspath,
-                                            url1, revision1, url2, revision2,
+                                            source,
                                             NULL,
                                             depth, notify_b,
                                             merge_b,
@@ -8809,8 +8782,8 @@ ensure_ra_session_url(svn_ra_session_t *
    by the merge.  Keys and values of the hash are both (const char *)
    absolute paths.  The contents of the hash are allocated in RESULT_POOL.
 
-   If SOURCES_ANCESTRAL is set, then for every merge source in
-   MERGE_SOURCES, the "left" and "right" side of the merge source are
+   If SOURCES_ANCESTRAL is set, then for every (const merge_source_t *)
+   merge source in MERGE_SOURCES, the "left" and "right" side are
    ancestrally related.  (See 'MERGEINFO MERGE SOURCE NORMALIZATION'
    for more on what that means and how it matters.)
 
@@ -8976,31 +8949,26 @@ do_merge(apr_hash_t **modified_subtrees,
 
   for (i = 0; i < merge_sources->nelts; i++)
     {
-      merge_source_t *merge_source =
+      merge_source_t *source =
         APR_ARRAY_IDX(merge_sources, i, merge_source_t *);
-      const char *url1, *url2;
-      svn_revnum_t rev1, rev2;
 
       svn_pool_clear(iterpool);
 
-      /* Convenience variables. */
-      url1 = merge_source->url1;
-      url2 = merge_source->url2;
-      rev1 = merge_source->rev1;
-      rev2 = merge_source->rev2;
-
       /* Sanity check:  if our left- and right-side merge sources are
          the same, there's nothing to here. */
-      if ((strcmp(url1, url2) == 0) && (rev1 == rev2))
+      if ((strcmp(source->url1, source->url2) == 0)
+          && (source->rev1 == source->rev2))
         continue;
 
       /* Establish RA sessions to our URLs, reuse where possible. */
-      SVN_ERR(ensure_ra_session_url(&ra_session1, url1, ctx, scratch_pool));
-      SVN_ERR(ensure_ra_session_url(&ra_session2, url2, ctx, scratch_pool));
+      SVN_ERR(ensure_ra_session_url(&ra_session1, source->url1,
+                                    ctx, scratch_pool));
+      SVN_ERR(ensure_ra_session_url(&ra_session2, source->url2,
+                                    ctx, scratch_pool));
 
       /* Populate the portions of the merge context baton that need to
          be reset for each merge source iteration. */
-      merge_cmd_baton.merge_source = *merge_source;
+      merge_cmd_baton.merge_source = *source;
       merge_cmd_baton.implicit_src_gap = NULL;
       merge_cmd_baton.added_path = NULL;
       merge_cmd_baton.add_necessitated_merge = FALSE;
@@ -9029,7 +8997,7 @@ do_merge(apr_hash_t **modified_subtrees,
       if (target_kind == svn_node_file)
         {
           SVN_ERR(do_file_merge(result_catalog,
-                                url1, rev1, url2, rev2, target_abspath,
+                                source, target_abspath,
                                 sources_related,
                                 squelch_mergeinfo_notifications,
                                 &notify_baton,
@@ -9044,7 +9012,7 @@ do_merge(apr_hash_t **modified_subtrees,
           svn_boolean_t abort_on_conflicts = (i < merge_sources->nelts - 1);
 
           SVN_ERR(do_directory_merge(result_catalog,
-                                     url1, rev1, url2, rev2, target_abspath,
+                                     source, target_abspath,
                                      depth, squelch_mergeinfo_notifications,
                                      abort_on_conflicts,
                                      &notify_baton, &merge_cmd_baton,
@@ -9091,8 +9059,9 @@ do_merge(apr_hash_t **modified_subtrees,
    merge (unless this is record-only), followed by record-only merges
    to represent the changed mergeinfo.
 
-   The merge is between URL1@REV1 (in URL1_RA_SESSION1) and URL2@REV2 (in
-   URL2_RA_SESSION2); YC_REV is their youngest common ancestor.
+   The merge is between SOURCE->url1@rev1 (in URL1_RA_SESSION1) and
+   SOURCE->url2@rev2 (in URL2_RA_SESSION2); YC_REV is their youngest
+   common ancestor.
    SAME_REPOS must be true if and only if the source URLs are in the same
    repository as the target working copy.  Other arguments are as in
    all of the public merge APIs.
@@ -9106,10 +9075,7 @@ static svn_error_t *
 merge_cousins_and_supplement_mergeinfo(const char *target_abspath,
                                        svn_ra_session_t *URL1_ra_session,
                                        svn_ra_session_t *URL2_ra_session,
-                                       const char *URL1,
-                                       svn_revnum_t rev1,
-                                       const char *URL2,
-                                       svn_revnum_t rev2,
+                                       const merge_source_t *source,
                                        svn_revnum_t yc_rev,
                                        svn_boolean_t same_repos,
                                        svn_depth_t depth,
@@ -9134,24 +9100,23 @@ merge_cousins_and_supplement_mergeinfo(c
   SVN_ERR_ASSERT(svn_dirent_is_absolute(target_abspath));
 
   SVN_ERR(normalize_merge_sources_internal(
-            &remove_sources, URL1, rev1,
-            svn_rangelist__initialize(rev1, yc_rev, TRUE, scratch_pool),
+            &remove_sources, source->url1, source->rev1,
+            svn_rangelist__initialize(source->rev1, yc_rev, TRUE, scratch_pool),
             URL1_ra_session, ctx, scratch_pool, subpool));
 
   SVN_ERR(normalize_merge_sources_internal(
-            &add_sources, URL2, rev2,
-            svn_rangelist__initialize(yc_rev, rev2, TRUE, scratch_pool),
+            &add_sources, source->url2, source->rev2,
+            svn_rangelist__initialize(yc_rev, source->rev2, TRUE, scratch_pool),
             URL2_ra_session, ctx, scratch_pool, subpool));
 
   /* If this isn't a record-only merge, we'll first do a stupid
      point-to-point merge... */
   if (! record_only)
     {
-      merge_source_t faux_source = { URL1, rev1, URL2, rev2 };
       apr_array_header_t *faux_sources =
         apr_array_make(scratch_pool, 1, sizeof(merge_source_t *));
       modified_subtrees = apr_hash_make(scratch_pool);
-      APR_ARRAY_PUSH(faux_sources, merge_source_t *) = &faux_source;
+      APR_ARRAY_PUSH(faux_sources, const merge_source_t *) = source;
       SVN_ERR(do_merge(&modified_subtrees, NULL, faux_sources, target_abspath,
                        FALSE, TRUE, same_repos,
                        ignore_ancestry, force, dry_run, FALSE, NULL, TRUE,
@@ -9210,7 +9175,7 @@ merge_cousins_and_supplement_mergeinfo(c
   return SVN_NO_ERROR;
 }
 
-/* Perform checks to determine whether of the working copy at TARGET_ABSPATH
+/* Perform checks to determine whether the working copy at TARGET_ABSPATH
  * can safely be used as a merge target. Checks are performed according to
  * the ALLOW_MIXED_REV, ALLOW_LOCAL_MODS, and ALLOW_SWITCHED_SUBTREES
  * parameters. If any checks fail, raise SVN_ERR_CLIENT_NOT_READY_TO_MERGE.
@@ -9227,9 +9192,21 @@ ensure_wc_is_suitable_merge_target(const
                                    svn_boolean_t allow_switched_subtrees,
                                    apr_pool_t *scratch_pool)
 {
-  /* Avoid the following checks if we don't need them. */
-  if (allow_mixed_rev && allow_local_mods && allow_switched_subtrees)
-    return SVN_NO_ERROR;
+  svn_node_kind_t target_kind;
+
+  /* Check the target exists. */
+  SVN_ERR(svn_io_check_path(target_abspath, &target_kind, scratch_pool));
+  if (target_kind == svn_node_none)
+    return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL,
+                             _("Path '%s' does not exist"),
+                             svn_dirent_local_style(target_abspath,
+                                                    scratch_pool));
+  SVN_ERR(svn_wc_read_kind(&target_kind, ctx->wc_ctx, target_abspath, FALSE,
+                           scratch_pool));
+  if (target_kind != svn_node_dir && target_kind != svn_node_file)
+    return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+                             _("Merge target '%s' does not exist in the "
+                               "working copy"), target_abspath);
 
   /* Perform the mixed-revision check first because it's the cheapest one. */
   if (! allow_mixed_rev)
@@ -9295,11 +9272,31 @@ ensure_wc_is_suitable_merge_target(const
   return SVN_NO_ERROR;
 }
 
+/* Throw an error if PATH_OR_URL is a path and REVISION isn't a repository
+ * revision. */
+static svn_error_t *
+ensure_wc_path_has_repo_revision(const char *path_or_url,
+                                 const svn_opt_revision_t *revision,
+                                 apr_pool_t *scratch_pool)
+{
+  if (revision->kind != svn_opt_revision_number
+      && revision->kind != svn_opt_revision_date
+      && revision->kind != svn_opt_revision_head
+      && ! svn_path_is_url(path_or_url))
+    return svn_error_createf(
+      SVN_ERR_CLIENT_BAD_REVISION, NULL,
+      _("Invalid merge source '%s'; a working copy path can only be "
+        "used with a repository revision (a number, a date, or head)"),
+      svn_dirent_local_style(path_or_url, scratch_pool));
+  return SVN_NO_ERROR;
+}
+
 
 /*-----------------------------------------------------------------------*/
 
 /*** Public APIs ***/
 
+/* The body of svn_client_merge4(), which see for details. */
 static svn_error_t *
 merge_locked(const char *source1,
              const svn_opt_revision_t *revision1,
@@ -9316,75 +9313,23 @@ merge_locked(const char *source1,
              svn_client_ctx_t *ctx,
              apr_pool_t *scratch_pool)
 {
-  const char *URL1, *URL2;
-  svn_revnum_t rev1, rev2;
+  merge_source_t source;
   svn_boolean_t related = FALSE, ancestral = FALSE;
   url_uuid_t wc_repos_root, source_repos_root, source_repos_root2;
-  svn_revnum_t youngest_rev = SVN_INVALID_REVNUM;
   svn_ra_session_t *ra_session1, *ra_session2;
   apr_array_header_t *merge_sources;
   svn_error_t *err;
   svn_boolean_t use_sleep = FALSE;
-  const char *yc_path = NULL;
+  const char *yc_relpath = NULL;
   svn_revnum_t yc_rev = SVN_INVALID_REVNUM;
   apr_pool_t *sesspool;
   svn_boolean_t same_repos;
-  svn_node_kind_t target_kind;
-
-  /* Make sure the target is really there. */
-  SVN_ERR(svn_io_check_path(target_abspath, &target_kind, scratch_pool));
-  if (target_kind == svn_node_none)
-    return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL,
-                             _("Path '%s' does not exist"),
-                             svn_dirent_local_style(target_abspath,
-                                                    scratch_pool));
-
-  /* Sanity check our input -- we require specified revisions,
-   * and either 2 paths or 2 URLs. */
-  if ((revision1->kind == svn_opt_revision_unspecified)
-      || (revision2->kind == svn_opt_revision_unspecified))
-    return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
-                            _("Not all required revisions are specified"));
-
-  if (svn_path_is_url(source1) != svn_path_is_url(source2))
-    return svn_error_create(SVN_ERR_ILLEGAL_TARGET, NULL,
-                            _("Merge sources must both be "
-                              "either paths or URLs"));
 
   /* ### FIXME: This function really ought to do a history check on
      the left and right sides of the merge source, and -- if one is an
      ancestor of the other -- just call svn_client_merge_peg3() with
      the appropriate args. */
 
-  /* If source1 or source2 are paths, we need to get the underlying
-     URL from the wc and save the initial path we were passed so we
-     can use it as a path parameter (either in the baton or not).
-     otherwise, the path will just be NULL, which means we won't be
-     able to figure out some kind of revision specifications, but in
-     that case it won't matter, because those ways of specifying a
-     revision are meaningless for a url. */
-  SVN_ERR(svn_client_url_from_path2(&URL1, source1, ctx,
-                                    scratch_pool, scratch_pool));
-  if (! URL1)
-    return svn_error_createf(SVN_ERR_ENTRY_MISSING_URL, NULL,
-                             _("'%s' has no URL"),
-                             svn_dirent_local_style(source1, scratch_pool));
-
-  SVN_ERR(svn_client_url_from_path2(&URL2, source2, ctx,
-                                    scratch_pool, scratch_pool));
-  if (! URL2)
-    return svn_error_createf(SVN_ERR_ENTRY_MISSING_URL, NULL,
-                             _("'%s' has no URL"),
-                             svn_dirent_local_style(source2, scratch_pool));
-
-  SVN_ERR(svn_wc_read_kind(&target_kind, ctx->wc_ctx, target_abspath, FALSE,
-                           scratch_pool));
-
-  if (target_kind != svn_node_dir && target_kind != svn_node_file)
-    return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
-                             _("Merge target '%s' does not exist in the "
-                               "working copy"), target_abspath);
-
   /* Do not allow merges into mixed-revision working copies. */
   SVN_ERR(ensure_wc_is_suitable_merge_target(target_abspath, ctx,
                                              allow_mixed_rev, TRUE, TRUE,
@@ -9395,22 +9340,17 @@ merge_locked(const char *source1,
                                     target_abspath,
                                     ctx, scratch_pool, scratch_pool));
 
-  /* Open some RA sessions to our merge source sides. */
+  /* Open RA sessions to both sides of our merge source, and resolve URLs
+   * and revisions. */
   sesspool = svn_pool_create(scratch_pool);
-  SVN_ERR(svn_client__open_ra_session_internal(&ra_session1, NULL, URL1,
-                                               NULL, NULL, FALSE, TRUE,
-                                               ctx, sesspool));
-  SVN_ERR(svn_client__open_ra_session_internal(&ra_session2, NULL, URL2,
-                                               NULL, NULL, FALSE, TRUE,
-                                               ctx, sesspool));
-
-  /* Resolve revisions to real numbers. */
-  SVN_ERR(svn_client__get_revision_number(&rev1, &youngest_rev, ctx->wc_ctx,
-                                          NULL, ra_session1, revision1,
-                                          sesspool));
-  SVN_ERR(svn_client__get_revision_number(&rev2, &youngest_rev, ctx->wc_ctx,
-                                          NULL, ra_session2, revision2,
-                                          sesspool));
+  SVN_ERR(svn_client__ra_session_from_path(&ra_session1,
+                                           &source.rev1, &source.url1,
+                                           source1, NULL, revision1, revision1,
+                                           ctx, sesspool));
+  SVN_ERR(svn_client__ra_session_from_path(&ra_session2,
+                                           &source.rev2, &source.url2,
+                                           source2, NULL, revision2, revision2,
+                                           ctx, sesspool));
 
   SVN_ERR(svn_ra_get_repos_root2(ra_session1, &source_repos_root.url,
                                  sesspool));
@@ -9426,8 +9366,8 @@ merge_locked(const char *source1,
    *     as we are only carrying around a single source-repos-root from now
    *     on, and URL calculations will go wrong if they differ.
    *     Alternatively, teach the code to cope with differing root URLs. */
-  SVN_ERR(check_same_repos(&source_repos_root, URL1,
-                           &source_repos_root2, URL2,
+  SVN_ERR(check_same_repos(&source_repos_root, source.url1,
+                           &source_repos_root2, source.url2,
                            FALSE /* strict_urls */, scratch_pool));
 
   /* Do our working copy and sources come from the same repository? */
@@ -9436,9 +9376,9 @@ merge_locked(const char *source1,
 
   /* Unless we're ignoring ancestry, see if the two sources are related.  */
   if (! ignore_ancestry)
-    SVN_ERR(svn_client__get_youngest_common_ancestor(&yc_path, &yc_rev,
-                                                     URL1, rev1,
-                                                     URL2, rev2,
+    SVN_ERR(svn_client__get_youngest_common_ancestor(&yc_relpath, &yc_rev,
+                                                     source.url1, source.rev1,
+                                                     source.url2, source.rev2,
                                                      ctx, scratch_pool));
 
   /* Check for a youngest common ancestor.  If we have one, we'll be
@@ -9458,33 +9398,33 @@ merge_locked(const char *source1,
                       merge recording, then record-only two merges:
                       from A to C, and from C to B
   */
-  if (yc_path && SVN_IS_VALID_REVNUM(yc_rev))
+  if (yc_relpath && SVN_IS_VALID_REVNUM(yc_rev))
     {
+      /* Make YC_RELPATH into a full URL. */
+      const char *yc_url = svn_path_url_add_component2(source_repos_root.url,
+                                                       yc_relpath, scratch_pool);
+
       /* Note that our merge sources are related. */
       related = TRUE;
 
-      /* Make YC_PATH into a full URL. */
-      yc_path = svn_path_url_add_component2(source_repos_root.url, yc_path,
-                                            scratch_pool);
-
       /* If the common ancestor matches the right side of our merge,
          then we only need to reverse-merge the left side. */
-      if ((strcmp(yc_path, URL2) == 0) && (yc_rev == rev2))
+      if ((strcmp(yc_url, source.url2) == 0) && (yc_rev == source.rev2))
         {
           ancestral = TRUE;
           SVN_ERR(normalize_merge_sources_internal(
-                    &merge_sources, URL1, rev1,
-                    svn_rangelist__initialize(rev1, yc_rev, TRUE, scratch_pool),
+                    &merge_sources, source.url1, source.rev1,
+                    svn_rangelist__initialize(source.rev1, yc_rev, TRUE, scratch_pool),
                     ra_session1, ctx, scratch_pool, scratch_pool));
         }
       /* If the common ancestor matches the left side of our merge,
          then we only need to merge the right side. */
-      else if ((strcmp(yc_path, URL1) == 0) && (yc_rev == rev1))
+      else if ((strcmp(yc_url, source.url1) == 0) && (yc_rev == source.rev1))
         {
           ancestral = TRUE;
           SVN_ERR(normalize_merge_sources_internal(
-                    &merge_sources, URL2, rev2,
-                    svn_rangelist__initialize(yc_rev, rev2, TRUE, scratch_pool),
+                    &merge_sources, source.url2, source.rev2,
+                    svn_rangelist__initialize(yc_rev, source.rev2, TRUE, scratch_pool),
                     ra_session2, ctx, scratch_pool, scratch_pool));
         }
       /* And otherwise, we need to do both: reverse merge the left
@@ -9494,8 +9434,7 @@ merge_locked(const char *source1,
           err = merge_cousins_and_supplement_mergeinfo(target_abspath,
                                                        ra_session1,
                                                        ra_session2,
-                                                       URL1, rev1,
-                                                       URL2, rev2,
+                                                       &source,
                                                        yc_rev,
                                                        same_repos,
                                                        depth,
@@ -9523,14 +9462,8 @@ merge_locked(const char *source1,
   else
     {
       /* Build a single-item merge_source_t array. */
-      merge_source_t *merge_source;
       merge_sources = apr_array_make(scratch_pool, 1, sizeof(merge_source_t *));
-      merge_source = apr_pcalloc(scratch_pool, sizeof(*merge_source));
-      merge_source->url1 = URL1;
-      merge_source->url2 = URL2;
-      merge_source->rev1 = rev1;
-      merge_source->rev2 = rev2;
-      APR_ARRAY_PUSH(merge_sources, merge_source_t *) = merge_source;
+      APR_ARRAY_PUSH(merge_sources, merge_source_t *) = &source;
     }
 
   /* Close our temporary RA sessions. */
@@ -9591,6 +9524,22 @@ svn_client_merge4(const char *source1,
 {
   const char *target_abspath, *lock_abspath;
 
+  /* Sanity check our input -- we require specified revisions,
+   * and either 2 paths or 2 URLs. */
+  if ((revision1->kind == svn_opt_revision_unspecified)
+      || (revision2->kind == svn_opt_revision_unspecified))
+    return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
+                            _("Not all required revisions are specified"));
+  if (svn_path_is_url(source1) != svn_path_is_url(source2))
+    return svn_error_create(SVN_ERR_ILLEGAL_TARGET, NULL,
+                            _("Merge sources must both be "
+                              "either paths or URLs"));
+  /* A WC path must be used with a repository revision, as we can't
+   * (currently) use the WC itself as a source, we can only read the URL
+   * from it and use that. */
+  SVN_ERR(ensure_wc_path_has_repo_revision(source1, revision1, pool));
+  SVN_ERR(ensure_wc_path_has_repo_revision(source2, revision2, pool));
+
   SVN_ERR(get_target_and_lock_abspath(&target_abspath, &lock_abspath,
                                       target_wcpath, ctx, pool));
 
@@ -10318,7 +10267,7 @@ calculate_left_hand_side(const char **ur
   apr_hash_t *segments_hash = apr_hash_make(scratch_pool);
   svn_boolean_t never_synced;
   svn_revnum_t youngest_merged_rev;
-  const char *yc_ancestor_path;
+  const char *yc_ancestor_relpath;
   svn_revnum_t yc_ancestor_rev;
   const char *source_url;
   const char *target_url;
@@ -10377,12 +10326,12 @@ calculate_left_hand_side(const char **ur
   target_url = svn_path_url_add_component2(source_repos_root,
                                            target_repos_rel_path,
                                            iterpool);
-  SVN_ERR(svn_client__get_youngest_common_ancestor(&yc_ancestor_path,
+  SVN_ERR(svn_client__get_youngest_common_ancestor(&yc_ancestor_relpath,
                                                    &yc_ancestor_rev,
                                                    source_url, source_rev,
                                                    target_url, target_rev,
                                                    ctx, iterpool));
-  if (!(yc_ancestor_path && SVN_IS_VALID_REVNUM(yc_ancestor_rev)))
+  if (!(yc_ancestor_relpath && SVN_IS_VALID_REVNUM(yc_ancestor_rev)))
     return svn_error_createf(SVN_ERR_CLIENT_NOT_READY_TO_MERGE, NULL,
                              _("'%s@%ld' must be ancestrally related to "
                                "'%s@%ld'"), source_url, source_rev,
@@ -10445,7 +10394,7 @@ calculate_left_hand_side(const char **ur
     {
       /* We never merged to the source.  Just return the branch point. */
       *url_left = svn_path_url_add_component2(source_repos_root,
-                                              yc_ancestor_path, result_pool);
+                                              yc_ancestor_relpath, result_pool);
       *rev_left = yc_ancestor_rev;
     }
   else
@@ -10468,10 +10417,7 @@ calculate_left_hand_side(const char **ur
 static svn_error_t *
 find_reintegrate_merge(svn_ra_session_t **source_ra_session_p,
                        svn_ra_session_t **target_ra_session_p,
-                       const char **url1_p,
-                       svn_revnum_t *rev1_p,
-                       const char **url2_p,
-                       svn_revnum_t *rev2_p,
+                       merge_source_t *source_p,
                        svn_revnum_t *yc_ancestor_rev_p,
                        /* inputs */
                        const char *source_path_or_url,
@@ -10485,30 +10431,20 @@ find_reintegrate_merge(svn_ra_session_t 
   svn_ra_session_t *target_ra_session;
   svn_ra_session_t *source_ra_session;
   const char *source_repos_rel_path, *target_repos_rel_path;
-  const char *yc_ancestor_path;
+  const char *yc_ancestor_relpath;
   svn_revnum_t yc_ancestor_rev;
-  const char *url1, *url2;
-  svn_revnum_t rev1, rev2;
+  merge_source_t source;
   svn_mergeinfo_t unmerged_to_source_mergeinfo_catalog;
   svn_mergeinfo_t merged_to_source_mergeinfo_catalog;
   svn_error_t *err;
   apr_hash_t *subtrees_with_mergeinfo;
   const char *target_url;
   svn_revnum_t target_base_rev;
-  svn_node_kind_t kind;
-
-  /* Make sure the target is really there. */
-  SVN_ERR(svn_io_check_path(target_abspath, &kind, scratch_pool));
-  if (kind == svn_node_none)
-    return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL,
-                             _("Path '%s' does not exist"),
-                             svn_dirent_local_style(target_abspath,
-                                                    scratch_pool));
 
   /* Make sure we're dealing with a real URL. */
-  SVN_ERR(svn_client_url_from_path2(&url2, source_path_or_url, ctx,
+  SVN_ERR(svn_client_url_from_path2(&source.url2, source_path_or_url, ctx,
                                     scratch_pool, scratch_pool));
-  if (! url2)
+  if (! source.url2)
     return svn_error_createf(SVN_ERR_ENTRY_MISSING_URL, NULL,
                              _("'%s' has no URL"),
                              svn_dirent_local_style(source_path_or_url,
@@ -10521,7 +10457,7 @@ find_reintegrate_merge(svn_ra_session_t 
 
   /* Determine the source's repository root URL. */
   SVN_ERR(svn_client_get_repos_root(&source_repos_root.url,
-                                    &source_repos_root.uuid, url2,
+                                    &source_repos_root.uuid, source.url2,
                                     ctx, result_pool, scratch_pool));
 
   /* source_repos_root and wc_repos_root are required to be the same,
@@ -10545,9 +10481,9 @@ find_reintegrate_merge(svn_ra_session_t 
      the default revision for the left side of our merge, since that's
      what the repository sub-tree is required to be up to date with
      (with regard to the WC). */
-  rev1 = target_base_rev;
+  source.rev1 = target_base_rev;
 
-  source_repos_rel_path = svn_uri_skip_ancestor(wc_repos_root.url, url2,

[... 287 lines stripped ...]