On Tue, Jan 17, 2012 at 10:12 AM, Julian Foad
<julianf...@btopenworld.com> wrote:
> I (Julian Foad) wrote:
>> I think merge_reintegrate_tests.py 19 demonstrates it better than 1000 words 
>> so
>> I'll leave it at that for today.
>
> For people who'd like to follow along without having to apply and run the 
> patch, that test does:
>
> Rev  A     A_COPY
>      |
> 02   |--cp-->X     # every X is a commit
> 3~6  X       |
> 07   |       X
> 08   |       |->X  # side-branch 'BRANCH_DEV1'
> 09   |       |  X
> 10   |       X<-|
> 11   X       |
> 12   |->X    |     # side-branch 'TRUNK_DEV1'
> 13   |  X    |
> 14   X<-|    |
>      |       |
> 15   |--mrg->X     # 'merge ^/A A_COPY'
>      |       |
> 16   |       X
> 17   |       |->X  # side-branch 'BRANCH_DEV2'
> 18   |       |  X
> 19   |       X<-|
> 20   X       |
> 21   |->X    |     # side-branch 'TRUNK_DEV2'
> 22   |  X    |
> 23   X<-|    |
>      |       |
> 24   X<-mrg--|     # 'merge --reintegrate ^/A_COPY A'
>      |       |     # no keep-alive dance here
> 25   |       X
> 26   |       |->X  # side-branch 'BRANCH_DEV3'
> 27   |       |  X
> 28   |       X<-|
> 29   X       |
> 30   |->X    |
> 31   |  X    |
> 32   X<-|    |     # side-branch 'TRUNK_DEV3'
>      |       |
> WC   |--mrg->X     # 'merge ^/A A_COPY'

Hi Julian,

I think I understand now.  Your patch works fine when the only cyclic
merges are done via reintegrate (i.e. r24).

When you said this though,

"However, if change A:40 had instead been a different merge into A,
let's say from C, it is still possible that merge might have brought
along some new mergeinfo describing merges from B, because of the way
mergeinfo is propagated from branch to branch.  Therefore, if we find
that change A:40 adds new mergeinfo about merges from B, we cannot
simply say that A:40 describes a reintegrate merge from B.  We need to
look more closely.  That's what I'm currently working on."

I thought you were going to tackle more complex reflective merge
cases, such as this example:

A@1---r4---r5---r6---r7----------------r11----------->
 |\                                     ^          |
 | \                                    |          |
 |  \                              reintegrate     |
 |   V                                  |          |
 |   A_COPY_2-----------------r9---r10---          |
 |                            ^                sync merge
 |                           /                     |
 |                  cherry-pick merge of r8        |
 V                         /                       V
 A_COPY-------------------r8------------------------->

As your patch stands, the above use-case causes some serious problems.

(The attached patch is an update of yours with a new
merge_reintegrate.py test #20, which demonstrates the above use case)

Here is the mergeinfo right before the final sync merge:

>svn pl -vR
Properties on 'A':
  svn:mergeinfo
    /A_COPY:8
    /A_COPY_2:3-10
Properties on 'A_COPY_2':
  svn:mergeinfo
    /A_COPY:8

Now perform the sync merge, your patch skips r11 because the naive
view of the mergeinfo change in that rev makes it look reflective.  So
the change made in r10 on the A_COPY_2 branch, which should be applied
to A_COPY, is not:

  >svn merge ^^/A A_COPY
  DBG: merge.c:8464: r1:11 mi added:
  DBG:   /A_COPY:8
  DBG: merge.c:8464: r6:11 mi added:
  DBG:   /A_COPY:8
  DBG: merge.c:8464: r8:11 mi added:
  DBG:   /A_COPY:8
  DBG: merge.c:8464: r9:11 mi added:
  DBG:   /A_COPY:8
  DBG: merge.c:8464: r10:11 mi added:
  DBG:   /A_COPY:8
  DBG: merge.c:8633: Skipping reflective revision r11
                     ^^^^^^^^
                Oops, there goes r10
  --- Merging r2 through r10 into 'A_COPY':
  U    A_COPY\B\E\beta
  U    A_COPY\D\G\rho
  U    A_COPY\D\H\omega
  U    A_COPY\D\H\psi
  --- Recording mergeinfo for merge of r2 through r11 into 'A_COPY':
   U   A_COPY

Definitely not what the user intended I think.  Worse, if the user
doesn't catch it and commits the sync merge...

  >svn ci -m "Sync merge from /A to A_COPY"
  Sending        A_COPY
  Sending        A_COPY\B\E\beta
  Sending        A_COPY\D\G\rho
  Sending        A_COPY\D\H\omega
  Sending        A_COPY\D\H\psi
  Transmitting file data ....
  Committed revision 12.

...and then reintegrates A_COPY to A, then the change from r10 is
actually removed from A:

  >svn up -q

  >svn merge ^^/A_COPY A --reintegrate
  --- Merging differences between repository URLs into 'A':
  U    A\D\H\chi
  --- Recording mergeinfo for merge between repository URLs into 'A':
   U   A

  >svn diff A\D\H\chi
  Index: A/D/H/chi
  ===================================================================
  --- A/D/H/chi   (revision 12)
  +++ A/D/H/chi   (working copy)
  @@ -1 +1 @@
  -Branch 2 edit.  <--- edit made in r10
  +This is the file 'chi'.

Again, probably not what was intended.

Paul

> and the tail output for the last merge is:
>
> DBG: merge.c:8464: r23:24 mi added:
> DBG:   /A_COPY:2-23
> DBG: merge.c:8633: Skipping reflective revision r24
> --- Merging r15 through r23 into 'A_COPY':
> A    A_COPY/TrunkFix2
> A    A_COPY/NewViaTRUNK_DEV2
>  G   A_COPY
> --- Merging r25 through r32 into 'A_COPY':
> A    A_COPY/TrunkFix3
> A    A_COPY/NewViaTRUNK_DEV3
>  G   A_COPY
> --- Recording mergeinfo for merge of r15 through r32 into 'A_COPY':
>  G   A_COPY
>
>
> mergeinfo on 'A':
>     /A_COPY:2-23
>     /BRANCH_DEV1:8-9
>     /BRANCH_DEV2:17-18
>     /TRUNK_DEV1:12-13
>     /TRUNK_DEV2:21-22
>     /TRUNK_DEV3:30-31
> mergeinfo on 'A_COPY@BASE':
>     /A:2-14
>     /BRANCH_DEV1:8-9
>     /BRANCH_DEV2:17-18
>     /BRANCH_DEV3:26-27
>     /TRUNK_DEV1:12-13
> mergeinfo on 'A_COPY' (working version):
>     /A:2-32
>     /BRANCH_DEV1:8-9
>     /BRANCH_DEV2:17-18
>     /BRANCH_DEV3:26-27
>     /TRUNK_DEV1:12-13
>     /TRUNK_DEV2:21-22
>     /TRUNK_DEV3:30-31
>
>
> - Julian
Index: subversion/libsvn_client/merge.c
===================================================================
--- subversion/libsvn_client/merge.c    (revision 1232664)
+++ subversion/libsvn_client/merge.c    (working copy)
@@ -8348,6 +8348,317 @@
   return SVN_NO_ERROR;
 }
 
+/* ### for debug prints */
+static const char *
+show_mergeinfo(svn_mergeinfo_t mergeinfo,
+               apr_pool_t *scratch_pool)
+{
+  svn_string_t *str;
+
+  svn_error_clear(svn_mergeinfo__to_formatted_string(
+                    &str, mergeinfo, "DBG:   ", scratch_pool));
+  return str->data;
+}
+
+/* Set *MERGEINFO to the mergeinfo from the repository node URL at REVNUM.
+ * If there is no mergeinfo, the result is empty (not null).
+ * Use RA_SESSION, reparenting it temporarily.
+ */
+static svn_error_t *
+fetch_repos_mergeinfo(svn_mergeinfo_t *mergeinfo,
+                      const char *url,
+                      svn_revnum_t revnum,
+                      svn_ra_session_t *ra_session,
+                      apr_pool_t *result_pool)
+{
+  SVN_ERR(svn_client__get_repos_mergeinfo(
+            mergeinfo, ra_session, url, revnum,
+            svn_mergeinfo_inherited, TRUE /* squelch_incapable */,
+            result_pool));
+
+  if (! *mergeinfo)
+    *mergeinfo = apr_hash_make(result_pool);
+  return SVN_NO_ERROR;
+}
+
+/* Find the changes in mergeinfo between the left and right sides of SOURCE.
+ * Set *DELETED to the deleted mergeinfo and *ADDED to the added mergeinfo;
+ * neither of these results will be null.
+ *
+ * ### TODO: Optimization: cache the mergeinfo (in SOURCE?), instead of
+ * always fetching from the repo.
+ */
+static svn_error_t *
+incoming_mergeinfo_diff(svn_mergeinfo_t *deleted,
+                        svn_mergeinfo_t *added,
+                        const merge_source_t *source,
+                        svn_ra_session_t *ra_session,
+                        svn_client_ctx_t *ctx,
+                        apr_pool_t *result_pool,
+                        apr_pool_t *scratch_pool)
+{
+  svn_mergeinfo_t mergeinfo_left, mergeinfo_right;
+
+  SVN_ERR(fetch_repos_mergeinfo(&mergeinfo_left,
+                                source->url1, source->rev1,
+                                ra_session, scratch_pool));
+  SVN_ERR(fetch_repos_mergeinfo(&mergeinfo_right,
+                                source->url2, source->rev2,
+                                ra_session, scratch_pool));
+
+  SVN_ERR(svn_mergeinfo_diff2(deleted, added,
+                              mergeinfo_left, mergeinfo_right,
+                              TRUE /* consider_inheritance */,
+                              result_pool, scratch_pool));
+  return SVN_NO_ERROR;
+}
+
+
+/* Look at the overall change in mergeinfo (that is, compare
+ * mergeinfo in SOURCE left with SOURCE right), and see if
+ * there is any addition of mergeinfo about merges *from* TARGET.
+ *
+ * Set *reflected_completely to true if the change in mergeinfo contains
+ * any merges from TARGET.
+ *
+ * TODO: Set *reflected_completely to true if the change in mergeinfo
+ * consists of nothing more that changes that are present in TARGET,
+ * taking into consideration that a merge 'MB' from another branch 'B' might
+ * show up because of having been merged via TARGET.  So, if the merge
+ * directly from TARGET is 'MT', and the corresponding change(s) on TARGET
+ * brought in the change 'MB', then we can ignore 'MB' because we know it
+ * was a transitive merge.  (### What if in fact MB was transitive in the
+ * other direction?)
+ */
+static svn_error_t *
+is_reflected_merge(svn_boolean_t *reflected,
+                   svn_boolean_t *reflected_completely,
+                   apr_array_header_t *children_with_mergeinfo,
+                   const merge_source_t *source,
+                   svn_ra_session_t *ra_session,
+                   svn_client_ctx_t *ctx,
+                   apr_pool_t *scratch_pool)
+{
+  svn_client__merge_path_t *root_child =
+    APR_ARRAY_IDX(children_with_mergeinfo, 0, svn_client__merge_path_t *);
+  svn_mergeinfo_t deleted;
+  svn_mergeinfo_t added;
+
+  SVN_ERR(incoming_mergeinfo_diff(&deleted, &added, source, ra_session,
+                                  ctx, scratch_pool, scratch_pool));
+  /* Find which differences represent merges from TARGET_LOCATIONS. */
+  SVN_ERR(svn_mergeinfo_intersect2(&deleted, deleted,
+                                   root_child->implicit_mergeinfo,
+                                   FALSE /* consider_inheritance */,
+                                   scratch_pool, scratch_pool));
+  SVN_ERR(svn_mergeinfo_intersect2(&added, added,
+                                   root_child->implicit_mergeinfo,
+                                   FALSE /* consider_inheritance */,
+                                   scratch_pool, scratch_pool));
+
+  if (apr_hash_count(deleted))
+    SVN_DBG(("r%ld:%ld mi deleted:\n%s",
+             source->rev1, source->rev2, show_mergeinfo(deleted, 
scratch_pool)));
+  if (apr_hash_count(added))
+    SVN_DBG(("r%ld:%ld mi added:\n%s",
+           source->rev1, source->rev2, show_mergeinfo(added, scratch_pool)));
+
+  *reflected = (apr_hash_count(deleted) || apr_hash_count(added));
+  *reflected_completely = FALSE;
+  return SVN_NO_ERROR;
+}
+
+
+/* Are we trying to merge into TGT a change that contains a merge
+ * *from* TGT?  If so, that is probably not desired
+ * and will probably result in conflicts; certainly it is logically
+ * wrong.  The exception is if rollback, reverse-merging or other
+ * forms of undoing are involved; but these are not fully supported
+ * by merge tracking.
+ *
+ * At the very least we should detect and warn.  Better, we could
+ * automatically skip such already-present changes.
+ *
+ * If the entire incoming change (or any single revision of it) says
+ * it is (or includes) a merge from TGT, then we know it is not a
+ * safe change to merge, and we can at least warn about it.
+ *
+ * If we can detect that one revision of the incoming change consists
+ * entirely of a change merged from TGT then we should skip it.  That
+ * will enable us to automatically ignore the following changes in SRC:
+ *
+ *   * the result of a reintegrate merge from TGT
+ *   * the result of a cherry-pick merge from TGT
+ *   * the result of any tracked merge from some other branch, that
+ *     was equivalent (in the merge tracking sense) to a merge from TGT
+ */
+
+/* Given an incoming (multi-rev) change SOURCE, and a target described by
+ * (the first item of) CHILDREN_WITH_MERGEINFO, set *REV to the first
+ * reflective merge in SOURCE, or SVN_INVALID_REVNUM if none.
+ *
+ * This implements algorithm 1.
+ *
+ * ### TODO: Support a reverse range. Presently assumes a forward range.
+ *
+ * Algorithm 1:
+ *
+ *  def find(RANGE):
+ *     # Precondition: rev range RANGE = SOURCE may contain a reflected merge.
+ *     # Postcondition: returns revision R which is in RANGE, or NULL.
+ *     if is_reflected_merge(RANGE):
+ *       if RANGE is a single (operative) revision:
+ *         return R = that revision
+ *       else:
+ *         R := find(first half of RANGE)
+ *         if R:
+ *           return R
+ *         R := find(second half of RANGE)
+ *         assert(R)  # because if RANGE not reflective we should
+ *                      have taken the outer 'else' branch
+ *         return R
+ *     else:
+ *       return NULL
+ *
+ * Algorithm 2:
+ *
+ *   Note: more efficient than algorithm 1.
+ *
+ *   def find(RANGE):
+ *     # Precondition: RANGE contains a reflected merge.
+ *     # Postcondition: returns revision R which is in RANGE.
+ *     if RANGE is a single (operative) revision:
+ *       return R = that revision
+ *     else:
+ *       RANGE1 := first half of RANGE
+ *       if is_reflected_merge(RANGE1):
+ *         return find(RANGE1)
+ *       else:
+ *         RANGE2 := second half of RANGE
+ *         return find(RANGE2)
+ *
+ *   Start:
+ *     if is_reflected_merge(SOURCE):
+ *       return find(SOURCE)
+ *     else:
+ *       return NULL
+ */
+static svn_error_t *
+find_reflected_rev(svn_revnum_t *rev,
+                   apr_array_header_t *children_with_mergeinfo,
+                   const merge_source_t *source,
+                   svn_ra_session_t *ra_session,
+                   svn_client_ctx_t *ctx,
+                   apr_pool_t *scratch_pool)
+{
+  svn_boolean_t reflected, reflected_completely;
+
+  SVN_ERR(is_reflected_merge(&reflected, &reflected_completely,
+                             children_with_mergeinfo, source,
+                             ra_session, ctx, scratch_pool));
+  /* SVN_DBG(("r=%ld:%ld reflected=%d\n", source->rev1, source->rev2, 
reflected)); */
+
+  if (reflected)
+    {
+      if (source->rev1 + 1 == source->rev2)
+        {
+          *rev = source->rev2;
+        }
+      else
+        {
+          svn_revnum_t half_way = (source->rev1 + source->rev2) / 2;
+          merge_source_t *source1 = subrange_source(source, source->rev1,
+                                                    half_way, scratch_pool);
+          SVN_ERR(find_reflected_rev(rev, children_with_mergeinfo, source1,
+                                     ra_session, ctx, scratch_pool));
+          if (! SVN_IS_VALID_REVNUM(*rev))
+            {
+              merge_source_t *source2
+                = subrange_source(source, half_way, source->rev2, 
scratch_pool);
+              SVN_ERR(find_reflected_rev(rev, children_with_mergeinfo, source2,
+                                         ra_session, ctx, scratch_pool));
+              SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(*rev));
+            }
+        }
+    }
+  else
+    {
+      *rev = SVN_INVALID_REVNUM;
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* Skip any individual change (revision) on the source that was the
+ * result of a merge from the (current) target branch.  This is a
+ * special case of support for 'reflective' merges.  In particular,
+ * we want to skip any source change that was the result of a
+ * reintegrate merge from the (current) target.
+ *
+ * If it's a record-only merge, then we don't care and we must
+ * allow this case because it's the historically recommended
+ * way of keeping a reintegrated branch alive.
+ */
+static svn_error_t *
+remove_reflected_revs(const merge_source_t *source,
+                      svn_ra_session_t *ra_session,
+                      apr_array_header_t *children_with_mergeinfo,
+                      merge_cmd_baton_t *merge_b,
+                      apr_pool_t *result_pool,
+                      apr_pool_t *scratch_pool)
+{
+  svn_client__merge_path_t *root_child =
+    APR_ARRAY_IDX(children_with_mergeinfo, 0, svn_client__merge_path_t *);
+  const merge_source_t *this_source = source;
+  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+  if (! root_child->implicit_mergeinfo || merge_b->record_only)
+    return SVN_NO_ERROR;
+
+  while (this_source->rev1 < this_source->rev2)
+    {
+      svn_revnum_t rev;
+      int i;
+
+      svn_pool_clear(iterpool);
+      SVN_ERR(find_reflected_rev(&rev, children_with_mergeinfo, this_source,
+                                 merge_b->ra_session1,
+                                 merge_b->ctx, iterpool));
+      if (rev == SVN_INVALID_REVNUM)
+        {
+          break;
+        }
+
+      /* Part of the incoming change is reflective. */
+      SVN_DBG((_("Skipping reflective revision r%ld\n"), rev));
+
+      /* Remove this revision from target and all children. */
+      for (i = 0; i < children_with_mergeinfo->nelts; i++)
+        {
+          svn_client__merge_path_t *child
+            = APR_ARRAY_IDX(children_with_mergeinfo, i,
+                            svn_client__merge_path_t *);
+
+          if (child->remaining_ranges && child->remaining_ranges->nelts)
+            {
+              SVN_ERR(svn_rangelist_remove(&child->remaining_ranges,
+                                           svn_rangelist__initialize(
+                                             rev - 1, rev, TRUE, iterpool),
+                                           child->remaining_ranges,
+                                           FALSE, result_pool));
+            }
+        }
+
+      this_source = subrange_source(this_source,
+                                    rev /*exclusive*/, source->rev2 
/*inclusive*/,
+                                    scratch_pool);
+    }
+
+  svn_pool_destroy(iterpool);
+  return SVN_NO_ERROR;
+}
+
+
 /* Helper for do_merge() when the merge target is a directory.
 
    Perform a merge of changes in SOURCE to the working copy path
@@ -8486,6 +8797,13 @@
       if (SVN_IS_VALID_REVNUM(new_range_start))
         range.start = new_range_start;
 
+      /* Don't merge any revision that represents a reflected merge (a merge
+         of a change that was earlier merged *from* the current target). */
+      SVN_ERR(remove_reflected_revs(source,
+                                    ra_session,
+                                    notify_b->children_with_mergeinfo,
+                                    merge_b, scratch_pool, iterpool));
+
       /* Remove inoperative ranges from any subtrees' remaining_ranges
          to spare the expense of noop editor drives. */
       if (!is_rollback)
Index: subversion/tests/cmdline/merge_reintegrate_tests.py
===================================================================
--- subversion/tests/cmdline/merge_reintegrate_tests.py (revision 1232664)
+++ subversion/tests/cmdline/merge_reintegrate_tests.py (working copy)
@@ -2541,7 +2541,228 @@
                                        expected_skip,
                                        [], None, None, None, None, True, True,
                                        '--reintegrate', A_path)
+
+#----------------------------------------------------------------------
+
+def simple_reintegrate(sbox, source, target_wcpath):
+  """"""
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  svntest.actions.run_and_verify_svn(None, None, [],
+                                     'merge', '--reintegrate',
+                                     source, target_wcpath)
+  os.chdir(was_cwd)
+
+def simple_sync_merge(sbox, source, target_wcpath):
+  """"""
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  svntest.actions.run_and_verify_svn(None, None, [],
+                                     'merge',
+                                     source, target_wcpath)
+  os.chdir(was_cwd)
+
+@SkipUnless(server_has_mergeinfo)
+def reintegrate_keep_alive1(sbox):
+  "reintegrate with automatic keep-alive"
+
+  # Test the ability to keep using a branch after reintegrating it.
+  # Make a branch, reintegrate it to trunk, and then sync the branch without
+  # first doing the 'keep-alive dance' (a record-only merge) that was
+  # required in the past.
+
+  # Make A_COPY branch in r2, and do a few more commits to A in r3-6.
+  sbox.build()
+  wc_dir = sbox.wc_dir
+  expected_disk, expected_status = set_up_branch(sbox)
+
+  # Make a change on the branch
+  sbox.simple_mkdir('A_COPY/NewDir')
+  sbox.simple_commit()
+
+  # Sync the branch
+  sbox.simple_update()
+  simple_sync_merge(sbox, '^/A', 'A_COPY')
+  sbox.simple_commit()
+
+  # Reintegrate
+  simple_reintegrate(sbox, '^/A_COPY', 'A')
+  sbox.simple_commit()
+
+  # Try to sync the branch again, without first doing a keep-alive dance.
+  # If the 'keep alive' works automatically then there should be no
+  # conflicts. In fact in this case there should be no change at all
+  # because it is already up to date and the reintegrate commit that
+  # happened on the trunk should be ignored.
+  sbox.simple_update()
+  simple_sync_merge(sbox, '^/A', 'A_COPY')
+
+@SkipUnless(server_has_mergeinfo)
+def reintegrate_keep_alive2(sbox):
+  "reintegrate with automatic keep-alive"
+
+  # Test the ability to keep using a branch after reintegrating it.
+  #
+  # Like reintegrate_keep_alive1(), but with more complex merges involving
+  # branches other than the two principal branches. Specifically, changes on
+  # 'trunk' (A) will include changes made in a temporary side-branch
+  # (SIDE_BRANCHn) off A, and changes on the child branch (A_COPY) will
+  # include changes made in a temporary side-branch (FEATn) off A_COPY.
+
+  # Make a branch A_COPY in r2, and then a few more commits to A, ending at r7.
+  # Also make a branch A_COPY_2 in r3, which we'll use as a short-term feature 
branch.
+  sbox.build()
+  wc_dir = sbox.wc_dir
+  expected_disk, expected_status = set_up_branch(sbox)
+
+  def change_via_side_branch(branch, side_branch):
+    """Work on the branch BRANCH, through a short-term feature branch named
+       SIDE_BRANCH."""
+    sbox.simple_repo_copy(branch, side_branch)
+    sbox.simple_update()
+    sbox.simple_mkdir(side_branch + '/NewVia' + side_branch)
+    sbox.simple_commit(side_branch)
+    simple_reintegrate(sbox, '^/' + side_branch, branch)
+    sbox.simple_commit()
+
+  # Work on the branch
+  sbox.simple_mkdir('A_COPY/BranchFix1')
+  sbox.simple_commit()
+  change_via_side_branch('A_COPY', 'BRANCH_DEV1')
+
+  # Work on trunk
+  sbox.simple_mkdir('A/TrunkFix1')
+  sbox.simple_commit()
+  change_via_side_branch('A', 'TRUNK_DEV1')
+
+  # Sync the branch
+  sbox.simple_update()
+  simple_sync_merge(sbox, '^/A', 'A_COPY')
+  sbox.simple_commit()
+
+  # Work on the branch
+  sbox.simple_mkdir('A_COPY/BranchFix2')
+  sbox.simple_commit()
+  change_via_side_branch('A_COPY', 'BRANCH_DEV2')
+
+  # Work on trunk (after the latest sync, but still allowed by reintegrate)
+  sbox.simple_mkdir('A/TrunkFix2')
+  sbox.simple_commit()
+  change_via_side_branch('A', 'TRUNK_DEV2')
+
+  # Reintegrate
+  sbox.simple_update()
+  simple_reintegrate(sbox, '^/A_COPY', 'A')
+  sbox.simple_commit()
+
+  # Work on the branch
+  sbox.simple_mkdir('A_COPY/BranchFix3')
+  sbox.simple_commit()
+  change_via_side_branch('A_COPY', 'BRANCH_DEV3')
+
+  # Work on trunk
+  sbox.simple_mkdir('A/TrunkFix3')
+  sbox.simple_commit()
+  change_via_side_branch('A', 'TRUNK_DEV3')
+
+  # Try to sync the branch again, without first doing a keep-alive dance.
+  # Subversion should automatically skip the reintegrate merge revisions and
+  # merge the other changes (TrunkFix2, TRUNK_DEV2, TrunkFix3, TRUNK_DEV3)
+  # with no conflicts.
+  sbox.simple_update()
+  simple_sync_merge(sbox, '^/A', 'A_COPY')
+
+@SkipUnless(server_has_mergeinfo)
+def reintegrate_keep_alive3(sbox):
+  "reintegrate with automatic keep-alive 3"
+
+  sbox.build()
+  wc_dir = sbox.wc_dir
+
+
+  gamma_COPY_path = os.path.join(sbox.wc_dir, "A_COPY", "D", "gamma")
+  chi_COPY_2_path = os.path.join(sbox.wc_dir, "A_COPY_2", "D", "H", "chi")
+  A_COPY_2_path   = os.path.join(sbox.wc_dir, "A_COPY_2")
+  A_path          = os.path.join(sbox.wc_dir, "A")
+  A_COPY_path     = os.path.join(sbox.wc_dir, "A_COPY")
   
+  # A@1---r4---r5---r6---r7----------------r11----------->
+  #  |\                                     ^          |
+  #  | \                                    |          |
+  #  |  \                              reintegrate     |
+  #  |   V                                  |          |
+  #  |   A_COPY_2-----------------r9---r10---          |
+  #  |                            ^                sync merge
+  #  |                           /                     |
+  #  |                  cherry-pick merge of r8        |
+  #  V                         /                       V
+  #  A_COPY-------------------r8------------------------->
+  #
+  # 
+  # Make a branch A_COPY in r2, and A_COPY_2 in r3, and then a few more
+  # commits to A in r4 through r7.
+  expected_disk, expected_status = set_up_branch(sbox, nbr_of_branches=2)
+
+  # r8 - Make an edit on the first branch to A_COPY/D/gamma
+  svntest.main.file_write(gamma_COPY_path, "Branch 1 edit.\n")
+  svntest.main.run_svn(None, "ci", "-m", "Branch 1 edit", wc_dir)
+
+  # r9 - Cherry pick r8 from ^/A_COPY to A_COPY_2  
+  svntest.actions.run_and_verify_svn(None, None, [], 'merge',
+                                     sbox.repo_url + '/A_COPY',
+                                     A_COPY_2_path, '-c8')
+  svntest.main.run_svn(None, "ci", "-m",
+                       "Cherry pick r8 from A_COPY to A_COPY_2", wc_dir)
+
+  # r10 - Make an edit on the second branch to A_COPY_2/D/H/chi  
+  svntest.main.file_write(chi_COPY_2_path, "Branch 2 edit.\n")
+  svntest.main.run_svn(None, "ci", "-m", "Branch 2 edit", wc_dir)
+
+  # r11 - Reintegrate A_COPY_2 to A.
+  svntest.actions.run_and_verify_svn(None, None, [], 'up', wc_dir)
+  svntest.actions.run_and_verify_svn(None, None, [], 'merge',
+                                     sbox.repo_url + '/A_COPY_2',
+                                     A_path, '--reintegrate')
+  svntest.main.run_svn(None, "ci", "-m", "Reintegrate A_COPY_2 back to A",
+                       wc_dir)
+
+  # Now try to sync merge ^/A to A_COPY.
+  #
+  # Revision r11 is skipped so the change from r10 never makes it into A_COPY.
+  #
+  # >svn merge ^^/A A_COPY
+  # DBG: merge.c:8464: r1:11 mi added:
+  # DBG:   /A_COPY:8
+  # DBG: merge.c:8464: r6:11 mi added:
+  # DBG:   /A_COPY:8
+  # DBG: merge.c:8464: r8:11 mi added:
+  # DBG:   /A_COPY:8
+  # DBG: merge.c:8464: r9:11 mi added:
+  # DBG:   /A_COPY:8
+  # DBG: merge.c:8464: r10:11 mi added:
+  # DBG:   /A_COPY:8
+  # DBG: merge.c:8633: Skipping reflective revision r11
+  # --- Merging r2 through r10 into 'A_COPY':
+  # U    A_COPY\B\E\beta
+  # U    A_COPY\D\G\rho
+  # U    A_COPY\D\H\omega
+  # U    A_COPY\D\H\psi
+  # --- Recording mergeinfo for merge of r2 through r11 into 'A_COPY':
+  #  U   A_COPY
+  svntest.actions.run_and_verify_svn(None, None, [], 'up', wc_dir)
+  svntest.actions.run_and_verify_svn(None, None, [], 'merge',
+                                     sbox.repo_url + '/A', A_COPY_path)
+  
+  # And what's more worrisome is that if we commit this sync merge and later
+  # reintegrate ^/A_COPY back to A, the change in r10 is removed from A
+  #
+  #
+  ###svntest.main.run_svn(None, "ci", "-m", "Sync A to A_COPY",
+  ###                     wc_dir)
+  ###svntest.actions.run_and_verify_svn(None, None, [], 'merge',
+  ###                                   sbox.repo_url + '/A_COPY',
+  ###                                   A_path, '--reintegrate')
+
 ########################################################################
 # Run the tests
 
@@ -2565,6 +2786,9 @@
               reintegrate_creates_bogus_mergeinfo,
               no_source_subtree_mergeinfo,
               reintegrate_replaced_source,
+              reintegrate_keep_alive1,
+              reintegrate_keep_alive2,
+              reintegrate_keep_alive3,
              ]
 
 if __name__ == '__main__':

Reply via email to