On the 1.6.x-issue3648 branch: Merge r958024, r961055, and r964167 from
trunk.

These three revs are the sum of the issue #3648 fixes on trunk.  None of them
merge cleanly; just about everything that could go wrong did, e.g. tree
conflicts, text conflicts, new API backports...

...so I merged all three changes, mostly via subtree merges,
resolved the numerous conflicts, made the public API private, removed the 
subtree mergeinfo, and finally did a a --record-only merge of the three
revs to the root to keep things tidy.

* src-branch-1.6.x-backport
* CHANGES
  Mergeinfo changes only.

* subversion/include/private/svn_mergeinfo_private.h
  r961055 added a new public API, svn_mergeinfo_catalog_merge, which we can't
  backport, so instead it is declared here as svn_mergeinfo__catalog_merge.

* subversion/libsvn_client/merge.c
  Resolved numerous conflicts due to WCNG work on trunk.
  Tweak the doc strings to note the pre-1.7 madness of WC path arguments
  which are either absolute or relative to the current working
  directory...Ugh, how did we ever live with this?! :-)

* subversion/libsvn_subr/mergeinfo.c
  Clean merge.

* subversion/tests/cmdline/merge_tests.py
  All three changes added/modified a test in merge_reintegrate_tests.py,
  which doesn't exist on 1.6.x.  So I merged all three directly to
  merge_tests.py.  Also adjusted the call to run_and_verify_merge to work
  properly on 1.6.x (i.e. no expected elision or mergeinfo output, use
  run_and_verify_merge2 to support 2-URL merge).


git-svn-id: https://svn.apache.org/repos/asf/subversion/branches/1.6.x-issue3648@964472 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/subversion/include/private/svn_mergeinfo_private.h b/subversion/include/private/svn_mergeinfo_private.h
index 0eb67d7..c5f5305 100644
--- a/subversion/include/private/svn_mergeinfo_private.h
+++ b/subversion/include/private/svn_mergeinfo_private.h
@@ -152,6 +152,18 @@
   svn_revnum_t oldest_rev,
   apr_pool_t *pool);
 
+/* Combine one mergeinfo catalog, CHANGES_CATALOG, into another mergeinfo
+  catalog MERGEINFO_CATALOG.  If both catalogs have mergeinfo for the same
+  key, use svn_mergeinfo_merge() to combine the mergeinfos.
+ 
+  Additions to MERGEINFO_CATALOG are deep copies allocated in
+  RESULT_POOL.  Temporary allocations are made in SCRATCH_POOL. */
+svn_error_t *
+svn_mergeinfo__catalog_merge(svn_mergeinfo_catalog_t mergeinfo_catalog,
+                             svn_mergeinfo_catalog_t changes_catalog,
+                             apr_pool_t *result_pool,
+                             apr_pool_t *scratch_pool);
+                            
 /* Removes ERASER (the subtrahend) from WHITEBOARD (the
    minuend), and places the resulting difference in *MERGEINFO.
    Allocates *MERGEINFO in RESULT_POOL.  Temporary allocations
diff --git a/subversion/libsvn_client/merge.c b/subversion/libsvn_client/merge.c
index 3eb923e..dab1ee7 100644
--- a/subversion/libsvn_client/merge.c
+++ b/subversion/libsvn_client/merge.c
@@ -1085,8 +1085,12 @@
                                       ctx->cancel_baton, subpool));
 
       /* If this is a forward merge then don't add new mergeinfo to
-         PATH that is already part of PATH's own history. */
-      if (merge_b->merge_source.rev1 < merge_b->merge_source.rev2)
+         PATH that is already part of PATH's own history, see
+         http://svn.haxx.se/dev/archive-2008-09/0006.shtml.  If the
+         merge sources are not ancestral then there is no concept of a
+         'forward' or 'reverse' merge and we filter unconditionally. */
+      if (merge_b->merge_source.rev1 < merge_b->merge_source.rev2
+          || !merge_b->sources_ancestral)
         SVN_ERR(filter_self_referential_mergeinfo(&props, path, merge_b,
                                                   adm_access, subpool));
 
@@ -3991,12 +3995,23 @@
   return SVN_NO_ERROR;
 }
 
-/* Calculate the new mergeinfo for the target tree based on the merge
-   info for TARGET_WCPATH and MERGES (a mapping of WC paths to range
-   lists), and record it in the WC (at, and possibly below,
-   TARGET_WCPATH). */
+/* Calculate the new mergeinfo for the target tree rooted at TARGET_WCPATH
+   based on MERGES (a mapping of WC paths to rangelists representing
+   a merge from the source REPOS_REL_PATH).
+
+   If RESULT_CATALOG is NULL, then record the new mergeinfo in the WC (at,
+   and possibly below, TARGET_WCPATH).
+
+   If RESULT_CATALOG is not NULL, then don't record the new mergeinfo on the
+   WC, but instead record it in RESULT_CATALOG, with key TARGET_WCPATH and the
+   value the new mergeinfo for that path.  If TARGET_WCPATH is already
+   present in RESULT_CATALOG, then merge the new mergeinfo together with
+   the existing mergeinfo and store the result in RESULT_CATALOG.
+   Allocate additions to RESULT_CATALOG in pool which RESULT_CATALOG was
+   created in. */
 static svn_error_t *
-update_wc_mergeinfo(const char *target_wcpath, const svn_wc_entry_t *entry,
+update_wc_mergeinfo(svn_mergeinfo_catalog_t result_catalog,
+                    const char *target_wcpath, const svn_wc_entry_t *entry,
                     const char *repos_rel_path, apr_hash_t *merges,
                     svn_boolean_t is_rollback,
                     svn_wc_adm_access_t *adm_access,
@@ -4095,25 +4110,42 @@
 
       svn_mergeinfo__remove_empty_rangelists(mergeinfo, pool);
 
-      err = svn_client__record_wc_mergeinfo(path, mergeinfo,
-                                            adm_access, subpool);
-
-      if (err && err->apr_err == SVN_ERR_ENTRY_NOT_FOUND)
+      if (result_catalog)
         {
-          /* PATH isn't just missing, it's not even versioned as far
-             as this working copy knows.  But it was included in
-             MERGES, which means that the server knows about it.
-             Likely we don't have access to the source due to authz
-             restrictions.  For now just clear the error and
-             continue...
+          svn_mergeinfo_t existing_mergeinfo =
+            apr_hash_get(result_catalog, target_wcpath, APR_HASH_KEY_STRING);
+          apr_pool_t *result_catalog_pool = apr_hash_pool_get(result_catalog);
 
-             ### TODO:  Set non-inheritable mergeinfo on PATH's immediate
-             ### parent and normal mergeinfo on PATH's siblings which we
-             ### do have access to. */
-          svn_error_clear(err);
+          if (existing_mergeinfo)
+            SVN_ERR(svn_mergeinfo_merge(mergeinfo, existing_mergeinfo,
+                                        result_catalog_pool));
+          apr_hash_set(result_catalog,
+                       apr_pstrdup(result_catalog_pool, target_wcpath),
+                       APR_HASH_KEY_STRING,
+                       svn_mergeinfo_dup(mergeinfo, result_catalog_pool));
         }
       else
-        SVN_ERR(err);
+        {
+          err = svn_client__record_wc_mergeinfo(path, mergeinfo,
+                                                adm_access, subpool);
+
+          if (err && err->apr_err == SVN_ERR_ENTRY_NOT_FOUND)
+            {
+              /* PATH isn't just missing, it's not even versioned as far
+                 as this working copy knows.  But it was included in
+                 MERGES, which means that the server knows about it.
+                 Likely we don't have access to the source due to authz
+                 restrictions.  For now just clear the error and
+                 continue...
+
+                 ### TODO:  Set non-inheritable mergeinfo on PATH's immediate
+                 ### parent and normal mergeinfo on PATH's siblings which we
+                 ### do have access to. */
+              svn_error_clear(err);
+            }
+          else
+            SVN_ERR(err);
+        }
     }
 
   svn_pool_destroy(subpool);
@@ -4191,7 +4223,7 @@
                ### skipped? */
             ;
         }
-      SVN_ERR(update_wc_mergeinfo(merge_b->target, target_entry,
+      SVN_ERR(update_wc_mergeinfo(NULL, merge_b->target, target_entry,
                                   mergeinfo_path, merges,
                                   is_rollback, adm_access,
                                   merge_b->ctx, pool));
@@ -5994,11 +6026,18 @@
    cousins thrice removed, etc...).  (This is used to simulate the
    history checks that the repository logic does in the directory case.)
 
+   If mergeinfo is being recorded to describe this merge, and RESULT_CATALOG
+   is not NULL, then don't record the new mergeinfo on the TARGET_WCPATH,
+   but instead record it in RESULT_CATALOG, where the key is TARGET_WCPATH
+   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.
 */
 static svn_error_t *
-do_file_merge(const char *url1,
+do_file_merge(svn_mergeinfo_catalog_t result_catalog,
+              const char *url1,
               svn_revnum_t revision1,
               const char *url2,
               svn_revnum_t revision2,
@@ -6307,9 +6346,9 @@
 
           apr_hash_set(merges, target_wcpath, APR_HASH_KEY_STRING,
                        filtered_rangelist);
-          SVN_ERR(update_wc_mergeinfo(target_wcpath, entry, mergeinfo_path,
-                                      merges, is_rollback, adm_access,
-                                      ctx, subpool));
+          SVN_ERR(update_wc_mergeinfo(result_catalog, target_wcpath, entry,
+                                      mergeinfo_path, merges, is_rollback,
+                                      adm_access, ctx, subpool));
         }
     }
 
@@ -6525,17 +6564,29 @@
 
 /* Helper for do_directory_merge().
 
-   Record mergeinfo describing a merge of MERGED_RANGE->START:
-   MERGED_RANGE->END from the repository relative path MERGEINFO_PATH to
-   the directory represented by TARGET_ENTRY.  Obviously this should only
+   If RESULT_CATALOG is NULL then record mergeinfo describing a merge of
+   MERGED_RANGE->START:MERGED_RANGE->END from the repository relative path
+   MERGEINFO_PATH to the merge target (and possibly its subtrees) described
+   by NOTIFY_B->CHILDREN_WITH_MERGEINFO -- see the global comment
+   'THE CHILDREN_WITH_MERGEINFO ARRAY'.  Obviously this should only
    be called if recording mergeinfo -- see doc string for
    mergeinfo_behavior().
 
+   If RESULT_CATALOG is not NULL, then don't record the new mergeinfo on the
+   WC, but instead record it in RESULT_CATALOG, where the keys are
+   working copy paths and the values are the new mergeinfos for each.
+   The WC path keys are either absolute or relative to the current working
+   directory in the same way NOTIFY_B->CHILDREN_WITH_MERGEINFO's
+   svn_client__merge_path_t's PATH member is.
+   Allocate additions to RESULT_CATALOG in pool which RESULT_CATALOG was
+   created in.
+
    TARGET_ENTRY, DEPTH, NOTIFY_B, MERGE_B, and ADM_ACCESS are all cascaded
    from do_directory_merge's arguments of the same names.
 */
 static svn_error_t *
-record_mergeinfo_for_dir_merge(const svn_wc_entry_t *target_entry,
+record_mergeinfo_for_dir_merge(svn_mergeinfo_catalog_t result_catalog,
+                               const svn_wc_entry_t *target_entry,
                                svn_merge_range_t *merged_range,
                                const char *mergeinfo_path,
                                svn_depth_t depth,
@@ -6636,7 +6687,7 @@
       child_merges = apr_hash_make(iterpool);
       apr_hash_set(child_merges, child->path, APR_HASH_KEY_STRING,
                    child_merge_rangelist);
-      SVN_ERR(update_wc_mergeinfo(child->path, child_entry,
+      SVN_ERR(update_wc_mergeinfo(result_catalog, child->path, child_entry,
                                   child_merge_src_canon_path,
                                   child_merges, is_rollback,
                                   adm_access, merge_b->ctx, iterpool));
@@ -6828,6 +6879,15 @@
    requirements around the values of URL1, REVISION1, URL2, and REVISION2
    in this case).
 
+   If mergeinfo is being recorded to describe this merge, and RESULT_CATALOG
+   is not NULL, then don't record the new mergeinfo on the WC, but instead
+   record it in RESULT_CATALOG, where the keys are working copy paths and the
+   values are the new mergeinfos for each.  The WC path keys are either
+   absolute or relative to the current working directory in the same way
+   NOTIFY_B->CHILDREN_WITH_MERGEINFO's svn_client__merge_path_t's PATH member
+   is.  Allocate additions to RESULT_CATALOG in pool which RESULT_CATALOG was
+   created in.
+
    Handle DEPTH as documented for svn_client_merge3().
 
    NOTE: This is a wrapper around drive_merge_report_editor() which
@@ -6836,7 +6896,8 @@
    meet one or more of the criteria described in get_mergeinfo_paths()).
 */
 static svn_error_t *
-do_directory_merge(const char *url1,
+do_directory_merge(svn_mergeinfo_catalog_t result_catalog,
+                   const char *url1,
                    svn_revnum_t revision1,
                    const char *url2,
                    svn_revnum_t revision2,
@@ -7099,7 +7160,8 @@
   /* Record mergeinfo where appropriate.*/
   if (record_mergeinfo)
     {
-      SVN_ERR(record_mergeinfo_for_dir_merge(target_entry,
+      SVN_ERR(record_mergeinfo_for_dir_merge(result_catalog,
+                                             target_entry,
                                              &range,
                                              mergeinfo_path,
                                              depth,
@@ -7190,6 +7252,14 @@
    repository as the one from which the target working copy has been
    checked out.
 
+   If mergeinfo is being recorded to describe this merge, and RESULT_CATALOG
+   is not NULL, then don't record the new mergeinfo on the WC, but instead
+   record it in RESULT_CATALOG, where the keys are working copy paths and the
+   values are the new mergeinfos for each.  The WC path keys are absolute if
+   TARGET is absolute and relative to the current working directory otherwise.
+   Allocate additions to RESULT_CATALOG in pool which RESULT_CATALOG was
+   created in.
+
    FORCE, DRY_RUN, RECORD_ONLY, IGNORE_ANCESTRY, DEPTH, MERGE_OPTIONS,
    and CTX are as described in the docstring for svn_client_merge_peg3().
 
@@ -7199,7 +7269,8 @@
    integrity, *USE_SLEEP will be unchanged if no sleep is required.
 */
 static svn_error_t *
-do_merge(apr_array_header_t *merge_sources,
+do_merge(svn_mergeinfo_catalog_t result_catalog,
+         apr_array_header_t *merge_sources,
          const char *target,
          const svn_wc_entry_t *target_entry,
          svn_wc_adm_access_t *adm_access,
@@ -7342,13 +7413,15 @@
       /* Call our merge helpers based on entry kind. */
       if (target_entry->kind == svn_node_file)
         {
-          SVN_ERR(do_file_merge(url1, rev1, url2, rev2, target,
+          SVN_ERR(do_file_merge(result_catalog,
+                                url1, rev1, url2, rev2, target,
                                 sources_related, adm_access, &notify_baton,
                                 &merge_cmd_baton, subpool));
         }
       else if (target_entry->kind == svn_node_dir)
         {
-          SVN_ERR(do_directory_merge(url1, rev1, url2, rev2, target_entry,
+          SVN_ERR(do_directory_merge(result_catalog,
+                                     url1, rev1, url2, rev2, target_entry,
                                      adm_access, depth, &notify_baton,
                                      &merge_cmd_baton, subpool));
         }
@@ -7480,7 +7553,7 @@
       faux_source->rev1 = rev1;
       faux_source->rev2 = rev2;
       APR_ARRAY_PUSH(faux_sources, merge_source_t *) = faux_source;
-      SVN_ERR(do_merge(faux_sources, target_wcpath, entry, adm_access,
+      SVN_ERR(do_merge(NULL, faux_sources, target_wcpath, entry, adm_access,
                        FALSE, TRUE, same_repos,
                        ignore_ancestry, force, dry_run, FALSE, TRUE,
                        depth, merge_options, use_sleep, ctx, pool));
@@ -7496,17 +7569,61 @@
      pair of record-only merges using the real sources we've
      calculated.  (We know that each tong in our fork of our merge
      source history tree has an ancestral relationship with the common
-     ancestral, so we force ancestral=TRUE here.) */
+     ancestral, so we force ancestral=TRUE here.)
+
+     Issue #3648: We don't actually perform these two record-only merges
+     on the WC at first, but rather see what each would do and store that
+     in two mergeinfo catalogs.  We then merge the catalogs together and
+     then record the result in the WC.  This prevents the second record
+     only merge from removing legitimate mergeinfo history, from the same
+     source, that was made in prior merges. */
   if (same_repos)
     {
-      SVN_ERR(do_merge(add_sources, target_wcpath, entry,
+      svn_mergeinfo_catalog_t add_result_catalog = apr_hash_make(pool);
+      svn_mergeinfo_catalog_t remove_result_catalog = apr_hash_make(pool);
+
+      SVN_ERR(do_merge(add_result_catalog, add_sources, target_wcpath, entry,
                        adm_access, TRUE, TRUE, same_repos,
                        ignore_ancestry, force, dry_run, TRUE, TRUE,
                        depth, merge_options, use_sleep, ctx, pool));
-      SVN_ERR(do_merge(remove_sources, target_wcpath, entry,
-                       adm_access, TRUE, TRUE, same_repos,
+      SVN_ERR(do_merge(remove_result_catalog, remove_sources, target_wcpath,
+                       entry, adm_access, TRUE, TRUE, same_repos,
                        ignore_ancestry, force, dry_run, TRUE, TRUE,
                        depth, merge_options, use_sleep, ctx, pool));
+      SVN_ERR(svn_mergeinfo__catalog_merge(add_result_catalog,
+                                           remove_result_catalog,
+                                           pool, pool));
+
+      if (apr_hash_count(add_result_catalog))
+        {
+          int i;
+          apr_array_header_t *sorted_cat;
+
+          sorted_cat = svn_sort__hash(add_result_catalog,
+                                      svn_sort_compare_items_as_paths, pool);
+          for (i = 0; i < sorted_cat->nelts; i++)
+            {
+              svn_sort__item_t elt = APR_ARRAY_IDX(sorted_cat, i,
+                                                   svn_sort__item_t);
+              svn_error_t *err = svn_client__record_wc_mergeinfo(elt.key,
+                                                                 elt.value,
+                                                                 adm_access,
+                                                                 pool);
+
+              if (err && err->apr_err == SVN_ERR_ENTRY_NOT_FOUND)
+                {
+                  /* PATH isn't just missing, it's not even versioned as far
+                     as this working copy knows.  But it was included in
+                     MERGES, which means that the server knows about it.
+                     Likely we don't have access to the source due to authz
+                     restrictions.  For now just clear the error and
+                     continue... */
+                  svn_error_clear(err);
+                }
+              else
+                SVN_ERR(err);
+                }
+        }
     }
   return SVN_NO_ERROR;
 }
@@ -7755,7 +7872,7 @@
   /* Close our temporary RA sessions. */
   svn_pool_destroy(sesspool);
 
-  err = do_merge(merge_sources, target_wcpath, entry, adm_access,
+  err = do_merge(NULL, merge_sources, target_wcpath, entry, adm_access,
                  ancestral, related, same_repos,
                  ignore_ancestry, force, dry_run,
                  record_only, FALSE, depth, merge_options,
@@ -8759,7 +8876,7 @@
 
   /* Do the real merge!  (We say with confidence that our merge
      sources are both ancestral and related.) */
-  err = do_merge(merge_sources, target_wcpath, entry, adm_access,
+  err = do_merge(NULL, merge_sources, target_wcpath, entry, adm_access,
                  TRUE, TRUE, same_repos, ignore_ancestry, force, dry_run,
                  record_only, FALSE, depth, merge_options,
                  &use_sleep, ctx, pool);
diff --git a/subversion/libsvn_subr/mergeinfo.c b/subversion/libsvn_subr/mergeinfo.c
index 0aa79f6..1668888 100644
--- a/subversion/libsvn_subr/mergeinfo.c
+++ b/subversion/libsvn_subr/mergeinfo.c
@@ -1292,6 +1292,69 @@
 }
 
 svn_error_t *
+svn_mergeinfo__catalog_merge(svn_mergeinfo_catalog_t mergeinfo_cat,
+                             svn_mergeinfo_catalog_t changes_cat,
+                             apr_pool_t *result_pool,
+                             apr_pool_t *scratch_pool)
+{
+  int i = 0;
+  int j = 0;
+  apr_array_header_t *sorted_cat =
+    svn_sort__hash(mergeinfo_cat, svn_sort_compare_items_as_paths,
+                   scratch_pool);
+  apr_array_header_t *sorted_changes =
+    svn_sort__hash(changes_cat, svn_sort_compare_items_as_paths,
+                   scratch_pool);
+
+  while (i < sorted_cat->nelts && j < sorted_changes->nelts)
+    {
+      svn_sort__item_t cat_elt, change_elt;
+      int res;
+
+      cat_elt = APR_ARRAY_IDX(sorted_cat, i, svn_sort__item_t);
+      change_elt = APR_ARRAY_IDX(sorted_changes, j, svn_sort__item_t);
+      res = svn_sort_compare_items_as_paths(&cat_elt, &change_elt);
+
+      if (res == 0) /* Both catalogs have mergeinfo for a give path. */
+        {
+          svn_mergeinfo_t mergeinfo = cat_elt.value;
+          svn_mergeinfo_t changes_mergeinfo = change_elt.value;
+
+          SVN_ERR(svn_mergeinfo_merge(mergeinfo, changes_mergeinfo,
+                                      result_pool));
+          apr_hash_set(mergeinfo_cat, cat_elt.key, cat_elt.klen, mergeinfo);
+          i++;
+          j++;
+        }
+      else if (res < 0) /* Only MERGEINFO_CAT has mergeinfo for this path. */
+        {
+          i++;
+        }
+      else /* Only CHANGES_CAT has mergeinfo for this path. */
+        {
+          apr_hash_set(mergeinfo_cat,
+                       apr_pstrdup(result_pool, change_elt.key),
+                       change_elt.klen,
+                       svn_mergeinfo_dup(change_elt.value, result_pool));
+          j++;
+        }
+    }
+
+  /* Copy back any remaining elements from the CHANGES_CAT catalog. */
+  for (; j < sorted_changes->nelts; j++)
+    {
+      svn_sort__item_t elt = APR_ARRAY_IDX(sorted_changes, j,
+                                           svn_sort__item_t);
+      apr_hash_set(mergeinfo_cat,
+                   apr_pstrdup(result_pool, elt.key),
+                   elt.klen,
+                   svn_mergeinfo_dup(elt.value, result_pool));
+    }
+
+  return SVN_NO_ERROR;
+}
+
+svn_error_t *
 svn_mergeinfo_intersect(svn_mergeinfo_t *mergeinfo,
                         svn_mergeinfo_t mergeinfo1,
                         svn_mergeinfo_t mergeinfo2,
diff --git a/subversion/tests/cmdline/merge_tests.py b/subversion/tests/cmdline/merge_tests.py
index 99e4b45..307a3a3 100755
--- a/subversion/tests/cmdline/merge_tests.py
+++ b/subversion/tests/cmdline/merge_tests.py
@@ -16600,7 +16600,180 @@
                                        expected_skip,
                                        None, None, None, None,
                                        None, 1, 1, "--reintegrate")
+
+#----------------------------------------------------------------------
+# Test for issue #3648 '2-URL merges incorrectly reverse-merge mergeinfo
+# for merge target'.
+def two_URL_merge_removes_valid_mergefino_from_target(sbox):
+  "2-URL merge removes valid mergefino from target"
+
+  sbox.build()
+  wc_dir = sbox.wc_dir
+
+  # Some paths we'll care about
+  lambda_COPY_path = os.path.join(wc_dir, "A_COPY", "B", "lambda")
+  mu_path          = os.path.join(wc_dir, "A", "mu")
+  A_COPY_path      = os.path.join(wc_dir, "A_COPY")
+  A_COPY_2_path    = os.path.join(wc_dir, "A_COPY_2")
   
+  # Branch A@1 to A_COPY r2
+  # Branch A@1 to A_COPY_2 in r3.
+  # Make some changes under 'A' in r4-7.
+  wc_disk, wc_status = set_up_branch(sbox, nbr_of_branches=2)
+
+  # r8 - A simple text edit on the A_COPY branch.
+  svntest.main.file_write(lambda_COPY_path, "Edit on 'branch 1'.\n")
+  svntest.actions.run_and_verify_svn(None, None, [], 'ci',
+                                     '-m', "Work on 'branch 1'.",
+                                     wc_dir)
+
+  # r9 - Sync the A_COPY branch with A up the HEAD (r8).  Now A_COPY
+  # differs from A only by the change made in r8 and by the mergeinfo
+  # '/A:2-8' on A_COPY which was set to describe the merge.
+  svntest.actions.run_and_verify_svn(None, None, [], 'up', wc_dir)
+  svntest.actions.run_and_verify_svn(None, svntest.verify.AnyOutput, [],
+                                     'merge', sbox.repo_url + '/A', A_COPY_path)
+  svntest.actions.run_and_verify_svn(None, None, [], 'ci',
+                                     '-m', 'Sync A to A_COPY.',
+                                     wc_dir)
+
+  # r10 - A simple text edit on our "trunk" A.
+  svntest.main.file_write(mu_path, "Edit on 'trunk'.\n")
+  svntest.actions.run_and_verify_svn(None, None, [], 'ci',
+                                     '-m', "Work on 'trunk'",
+                                     wc_dir)
+
+  # r11 - Sync the A_COPY_2 branch with A up to HEAD (r10).
+  svntest.actions.run_and_verify_svn(None, None, [], 'up', wc_dir)
+  svntest.actions.run_and_verify_svn(None, svntest.verify.AnyOutput, [],
+                                     'merge', sbox.repo_url + '/A',
+                                     A_COPY_2_path)
+  svntest.actions.run_and_verify_svn(None, None, [], 'ci',
+                                     '-m', 'Sync A to A_COPY_2.',
+                                     wc_dir)
+
+  # Confirm that the mergeinfo on each branch is what we expect.
+  svntest.actions.run_and_verify_svn(None,
+                                     [A_COPY_path + ' - /A:2-8\n'],
+                                     [], 'pg', SVN_PROP_MERGEINFO,
+                                     '-R', A_COPY_path)
+  svntest.actions.run_and_verify_svn(None,
+                                     [A_COPY_2_path + ' - /A:3-10\n'],
+                                     [], 'pg', SVN_PROP_MERGEINFO,
+                                     '-R', A_COPY_2_path)
+
+  # Now say we want to apply the changes made on the first branch (A_COPY)
+  # to the second branch (A_COPY_2).  One way to do this is a 2-URL merge
+  # between A at the revision last synced to A_COPY and A_COPY_2 at HEAD (r11),
+  # i.e.:
+  #
+  #   svn merge ^/A@8 ^/A_COPY@11 A_COPY_2_WC
+  #
+  # Recall from the note on r9 that this diff is simply the one text change
+  # made on branch 1 and some mergeinfo:
+  # 
+  #   >svn diff ^/A@8 ^/A_COPY@11
+  #   Index: B/lambda
+  #   ===================================================================
+  #   --- B/lambda    (.../A) (revision 8)
+  #   +++ B/lambda    (.../A_COPY)    (revision 11)
+  #   @@ -1 +1 @@
+  #   -This is the file 'lambda'.
+  #   +Edit on 'branch 1'.
+  #
+  #   Property changes on: .
+  #   ___________________________________________________________________
+  #   Added: svn:mergeinfo
+  #      Merged /A:r2-8
+  #
+  # The mergeinfo diff is already represented in A_COPY_2's mergeinfo, so the
+  # result of the merge should be the text change to lambda and the addition
+  # of mergeinfo showing that the history of A_COPY is now part of A_COPY_2,
+  # i.e. '/A_COPY:2-11'
+  #
+  # This test is currently marked as XFail because this is not what happens.
+  # Well, actually, all the above *does* happen, but as discussed in
+  # http://svn.haxx.se/dev/archive-2010-05/0292.shtml, the merge removes some
+  # of the valid mergeinfo on A_COPY_2 that describes the sync merge made in
+  # r9:
+  #
+  #   >svn pl -vR A_COPY_2
+  #   Properties on 'A_COPY_2':
+  #     svn:mergeinfo
+  #       /A:9-10
+  #       /A_COPY:2-11
+  #
+  #   >svn diff --depth empty A_COPY_2
+  #
+  #   Property changes on: A_COPY_2
+  #   ___________________________________________________________________
+  #   Modified: svn:mergeinfo
+  #      Reverse-merged /A:r3-8
+  #      Merged /A_COPY:r2-11
+  #
+  # '/A:r3-8' represents valid, operative changes merged from A to A_COPY_2!
+  # If this merge was committed, subsequent merges would try to reapply the
+  # diff, possibly leading to spurious conflicts.
+  svntest.actions.run_and_verify_svn(None, None, [], 'up', wc_dir)
+  expected_output = wc.State(A_COPY_2_path, {
+    ''         : Item(status=' G'),
+    'B/lambda' : Item(status='U '),
+    })
+  expected_status = wc.State(A_COPY_2_path, {
+    ''          : Item(status=' M'),
+    'B'         : Item(status='  '),
+    'mu'        : Item(status='  '),
+    'B/E'       : Item(status='  '),
+    'B/E/alpha' : Item(status='  '),
+    'B/E/beta'  : Item(status='  '),
+    'B/lambda'  : Item(status='M '),
+    'B/F'       : Item(status='  '),
+    'C'         : Item(status='  '),
+    'D'         : Item(status='  '),
+    'D/G'       : Item(status='  '),
+    'D/G/pi'    : Item(status='  '),
+    'D/G/rho'   : Item(status='  '),
+    'D/G/tau'   : Item(status='  '),
+    'D/gamma'   : Item(status='  '),
+    'D/H'       : Item(status='  '),
+    'D/H/chi'   : Item(status='  '),
+    'D/H/psi'   : Item(status='  '),
+    'D/H/omega' : Item(status='  '),
+    })
+  expected_status.tweak(wc_rev=11)
+  expected_disk = wc.State('', {
+    ''          : Item(props={SVN_PROP_MERGEINFO :
+                              '/A:3-10\n/A_COPY:2-11'}),
+    'B'         : Item(),
+    'mu'        : Item("Edit on 'trunk'.\n"),
+    'B/E'       : Item(),
+    'B/E/alpha' : Item("This is the file 'alpha'.\n"),
+    'B/E/beta'  : Item("New content"),
+    'B/lambda'  : Item("Edit on 'branch 1'.\n"),
+    'B/F'       : Item(),
+    'C'         : Item(),
+    'D'         : Item(),
+    'D/G'       : Item(),
+    'D/G/pi'    : Item("This is the file 'pi'.\n"),
+    'D/G/rho'   : Item("New content"),
+    'D/G/tau'   : Item("This is the file 'tau'.\n"),
+    'D/gamma'   : Item("This is the file 'gamma'.\n"),
+    'D/H'       : Item(),
+    'D/H/chi'   : Item("This is the file 'chi'.\n"),
+    'D/H/psi'   : Item("New content"),
+    'D/H/omega' : Item("New content"),
+    })
+  expected_skip = wc.State(A_COPY_path, {})
+  svntest.actions.run_and_verify_merge2(A_COPY_2_path, 8, 11,
+                                        sbox.repo_url + '/A',
+                                        sbox.repo_url + '/A_COPY',
+                                        expected_output,
+                                        expected_disk,
+                                        expected_status,
+                                        expected_skip,
+                                        None, None, None, None,
+                                        None, 1, 1)
+
 ########################################################################
 # Run the tests
 
@@ -16825,6 +16998,7 @@
                          server_has_mergeinfo),
               reintegrate_with_self_referential_mergeinfo,
               added_subtrees_with_mergeinfo_break_reintegrate,
+              two_URL_merge_removes_valid_mergefino_from_target,
              ]
 
 if __name__ == '__main__':