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 2015/11/24 10:52:44 UTC

svn commit: r1716095 - in /subversion/trunk: subversion/include/private/ subversion/libsvn_delta/ subversion/tests/cmdline/ subversion/tests/cmdline/svntest/ tools/dev/svnmover/

Author: julianfoad
Date: Tue Nov 24 09:52:44 2015
New Revision: 1716095

URL: http://svn.apache.org/viewvc?rev=1716095&view=rev
Log:
In 'svnmover', start tracking complete merges.

Track the source-branch end point of each merge. Add an 'automerge' command
that uses this as the ancestor for the next merge from the same source.

This is a very primitive initial implementation. It only enables each branch
to track complete merges from a single source branch. Complete merges here
refers to completeness in revisions (all revisions up to N, no cherry
picking) and also to the complete tree (no subtree tracking). It overwrites
the previous merge history regardless whether the new merge is from the same
source branch, and regardless whether it is in fact a complete merge.

* subversion/include/private/svn_branch.h
  (svn_branch__rev_bid_equal): New.
  (svn_branch__state_get_merge_ancestor,
   svn_branch__state_set_merge_ancestor): New.

* subversion/include/private/svn_branch_impl.h
  (svn_branch__state_v_get_merge_ancestor_t,
   svn_branch__state_v_set_merge_ancestor_t): New.
  (svn_branch__state_vtable_t): Add the new methods to the vtable.

* subversion/libsvn_delta/branch.c
  (svn_branch__state_priv_t): Add storage for a merge ancestor (just one, so
    far).
  (svn_branch__rev_bid_equal): New.
  (branch_state_get_merge_ancestor,
   branch_state_set_merge_ancestor): New.
  (svn_branch__state_get_merge_ancestor,
   svn_branch__state_set_merge_ancestor): New.
  (branch_state_create): Add the new methods to the vtable.
  (svn_branch__get_default_r0_metadata): Update.
  (merge_history_parse): New.
  (parse_element_line): Take and use a result pool for the results, in order
    to avoid surprises in future.
  (svn_branch__state_parse): Parse the merge history.
  (merge_history_serialize): New.
  (svn_branch__state_serialize): Serialize the merge history.

* subversion/tests/cmdline/svnmover_tests.py
  (reported_mg_diff): New.
  (...everywhere...): Expect a merge-history diff whenever there is a merge.

* subversion/tests/cmdline/svntest/wc.py
  (_re_parse_eid_merge_history): New.
  (State.from_eids): Parse and ignore merge history lines.

* tools/dev/svnmover/merge3.c
  (merge_subbranch): Correct a comment.

* tools/dev/svnmover/svnmover.c
  (rev_bid_str,
   merge_history_diff): New.
  (txn_is_changed): Also check for any change in merge history.
  (svn_branch__replay): Also replay any change in merge history.
  (action_code_t,
   action_defn): Define a new 'automerge' command. Rename a constant.
  (do_merge): Also record the merge history.
  (do_auto_merge): New.
  (branch_diff_r): Also diff the merge history.
  (execute): Expand the 'info-wc' command to show merge history status.
    Implement the 'automerge' command. Track the rename.

Modified:
    subversion/trunk/subversion/include/private/svn_branch.h
    subversion/trunk/subversion/include/private/svn_branch_impl.h
    subversion/trunk/subversion/libsvn_delta/branch.c
    subversion/trunk/subversion/tests/cmdline/svnmover_tests.py
    subversion/trunk/subversion/tests/cmdline/svntest/wc.py
    subversion/trunk/tools/dev/svnmover/merge3.c
    subversion/trunk/tools/dev/svnmover/svnmover.c

Modified: subversion/trunk/subversion/include/private/svn_branch.h
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/include/private/svn_branch.h?rev=1716095&r1=1716094&r2=1716095&view=diff
==============================================================================
--- subversion/trunk/subversion/include/private/svn_branch.h (original)
+++ subversion/trunk/subversion/include/private/svn_branch.h Tue Nov 24 09:52:44 2015
@@ -487,6 +487,9 @@ svn_branch__rev_bid_t *
 svn_branch__rev_bid_dup(const svn_branch__rev_bid_t *old_id,
                         apr_pool_t *result_pool);
 
+svn_boolean_t
+svn_branch__rev_bid_equal(const svn_branch__rev_bid_t *id1,
+                          const svn_branch__rev_bid_t *id2);
 
 /* Return the mapping of elements in branch BRANCH.
  */
@@ -589,6 +592,27 @@ svn_error_t *
 svn_branch__state_purge(svn_branch__state_t *branch,
                         apr_pool_t *scratch_pool);
 
+/* Get the merge ancestor(s).
+ */
+svn_error_t *
+svn_branch__state_get_merge_ancestor(svn_branch__state_t *branch,
+                                     svn_branch__rev_bid_t **merge_ancestor_p,
+                                     apr_pool_t *result_pool);
+
+/* Set a merge ancestor.
+ *
+ * Currently only one is allowed; this overwrites it if it was already set.
+ *
+ * TODO: Allow adding multiple ancestors on different branches. When
+ * there is an existing ancestor that is earlier along the same branch
+ * (line of history) as MERGE_ANCESTOR, then update (replace) it instead
+ * of just adding another one.
+ */
+svn_error_t *
+svn_branch__state_add_merge_ancestor(svn_branch__state_t *branch,
+                                     const svn_branch__rev_bid_t *merge_ancestor,
+                                     apr_pool_t *scratch_pool);
+
 /* Return the branch-relative path of element EID in BRANCH.
  *
  * If the element EID does not currently exist in BRANCH, return NULL.

Modified: subversion/trunk/subversion/include/private/svn_branch_impl.h
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/include/private/svn_branch_impl.h?rev=1716095&r1=1716094&r2=1716095&view=diff
==============================================================================
--- subversion/trunk/subversion/include/private/svn_branch_impl.h (original)
+++ subversion/trunk/subversion/include/private/svn_branch_impl.h Tue Nov 24 09:52:44 2015
@@ -193,6 +193,16 @@ typedef svn_error_t *(*svn_branch__state
   svn_branch__state_t *branch,
   apr_pool_t *scratch_pool);
 
+typedef svn_error_t *(*svn_branch__state_v_get_merge_ancestor_t)(
+  svn_branch__state_t *branch,
+  svn_branch__rev_bid_t **merge_ancestor_p,
+  apr_pool_t *scratch_pool);
+
+typedef svn_error_t *(*svn_branch__state_v_add_merge_ancestor_t)(
+  svn_branch__state_t *branch,
+  const svn_branch__rev_bid_t *merge_ancestor,
+  apr_pool_t *scratch_pool);
+
 struct svn_branch__state_vtable_t
 {
   svn_branch__vtable_priv_t vpriv;
@@ -204,6 +214,8 @@ struct svn_branch__state_vtable_t
   svn_branch__state_v_copy_tree_t copy_tree;
   svn_branch__state_v_delete_one_t delete_one;
   svn_branch__state_v_purge_t purge;
+  svn_branch__state_v_get_merge_ancestor_t get_merge_ancestor;
+  svn_branch__state_v_add_merge_ancestor_t add_merge_ancestor;
 
 };
 

Modified: subversion/trunk/subversion/libsvn_delta/branch.c
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_delta/branch.c?rev=1716095&r1=1716094&r2=1716095&view=diff
==============================================================================
--- subversion/trunk/subversion/libsvn_delta/branch.c (original)
+++ subversion/trunk/subversion/libsvn_delta/branch.c Tue Nov 24 09:52:44 2015
@@ -69,6 +69,11 @@ struct svn_branch__state_priv_t
   /* EID -> svn_element__content_t mapping. */
   svn_element__tree_t *element_tree;
 
+  /* Youngest ancestor, with respect to a complete merge, on another branch.
+     (REV = -1 means "in this txn".)
+     ### TODO: Multiple ancestors, corresponding to multiple source branches. */
+  svn_branch__rev_bid_t *merge_ancestor;
+
   svn_boolean_t is_flat;
 
 };
@@ -915,6 +920,14 @@ svn_branch__rev_bid_dup(const svn_branch
   return id;
 }
 
+svn_boolean_t
+svn_branch__rev_bid_equal(const svn_branch__rev_bid_t *id1,
+                          const svn_branch__rev_bid_t *id2)
+{
+  return (id1->rev == id2->rev
+          && strcmp(id1->bid, id2->bid) == 0);
+}
+
 
 /*
  * ========================================================================
@@ -1066,6 +1079,30 @@ branch_state_purge(svn_branch__state_t *
   return SVN_NO_ERROR;
 }
 
+/* An #svn_branch__state_t method. */
+static svn_error_t *
+branch_state_get_merge_ancestor(svn_branch__state_t *branch,
+                                svn_branch__rev_bid_t **merge_ancestor_p,
+                                apr_pool_t *result_pool)
+{
+  *merge_ancestor_p = svn_branch__rev_bid_dup(branch->priv->merge_ancestor,
+                                              result_pool);
+  return SVN_NO_ERROR;
+}
+
+/* An #svn_branch__state_t method. */
+static svn_error_t *
+branch_state_add_merge_ancestor(svn_branch__state_t *branch,
+                                const svn_branch__rev_bid_t *merge_ancestor,
+                                apr_pool_t *scratch_pool)
+{
+  apr_pool_t *branch_pool = branch_state_pool_get(branch);
+
+  branch->priv->merge_ancestor = svn_branch__rev_bid_dup(merge_ancestor,
+                                                         branch_pool);
+  return SVN_NO_ERROR;
+}
+
 const char *
 svn_branch__get_path_by_eid(const svn_branch__state_t *branch,
                             int eid,
@@ -1276,6 +1313,28 @@ svn_branch__state_purge(svn_branch__stat
   return SVN_NO_ERROR;
 }
 
+svn_error_t *
+svn_branch__state_get_merge_ancestor(svn_branch__state_t *branch,
+                                     svn_branch__rev_bid_t **merge_ancestor_p,
+                                     apr_pool_t *result_pool)
+{
+  SVN_ERR(branch->vtable->get_merge_ancestor(branch,
+                                             merge_ancestor_p,
+                                             result_pool));
+  return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_branch__state_add_merge_ancestor(svn_branch__state_t *branch,
+                                     const svn_branch__rev_bid_t *merge_ancestor,
+                                     apr_pool_t *scratch_pool)
+{
+  SVN_ERR(branch->vtable->add_merge_ancestor(branch,
+                                             merge_ancestor,
+                                             scratch_pool));
+  return SVN_NO_ERROR;
+}
+
 svn_branch__state_t *
 svn_branch__state_create(const svn_branch__state_vtable_t *vtable,
                          svn_cancel_func_t cancel_func,
@@ -1318,6 +1377,8 @@ branch_state_create(const char *bid,
     branch_state_copy_tree,
     branch_state_delete_one,
     branch_state_purge,
+    branch_state_get_merge_ancestor,
+    branch_state_add_merge_ancestor,
   };
   svn_branch__state_t *b
     = svn_branch__state_create(&vtable, NULL, NULL, result_pool);
@@ -1344,6 +1405,7 @@ svn_branch__get_default_r0_metadata(apr_
   static const char *default_repos_info
     = "r0: eids 0 1 branches 1\n"
       "B0 root-eid 0 num-eids 1\n"
+      "merge-history: merge-ancestors 0\n"
       "e0: normal -1 .\n";
 
   return svn_string_create(default_repos_info, result_pool);
@@ -1385,13 +1447,56 @@ parse_branch_line(char *bid_p,
   return SVN_NO_ERROR;
 }
 
-/*  */
+/* Parse the merge history for BRANCH.
+ */
+static svn_error_t *
+merge_history_parse(svn_branch__state_t *branch_state,
+                    svn_stream_t *stream,
+                    apr_pool_t *result_pool,
+                    apr_pool_t *scratch_pool)
+{
+  svn_stringbuf_t *line;
+  svn_boolean_t eof;
+  int n;
+  int num_merge_ancestors;
+  int i;
+
+  /* Read a line */
+  SVN_ERR(svn_stream_readline(stream, &line, "\n", &eof, scratch_pool));
+  SVN_ERR_ASSERT(!eof);
+
+  n = sscanf(line->data, "merge-history: merge-ancestors %d",
+             &num_merge_ancestors);
+  SVN_ERR_ASSERT(n == 1);
+
+  for (i = 0; i < num_merge_ancestors; i++)
+    {
+      svn_revnum_t rev;
+      char bid[100];
+
+      SVN_ERR(svn_stream_readline(stream, &line, "\n", &eof, scratch_pool));
+      SVN_ERR_ASSERT(!eof);
+
+      n = sscanf(line->data, "merge-ancestor: r%ld.%99s",
+                 &rev, bid);
+      SVN_ERR_ASSERT(n == 2);
+
+      branch_state->priv->merge_ancestor
+        = svn_branch__rev_bid_create(rev, bid, result_pool);
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* Parse the mapping for one element.
+ */
 static svn_error_t *
 parse_element_line(int *eid_p,
                    svn_boolean_t *is_subbranch_p,
                    int *parent_eid_p,
                    const char **name_p,
                    svn_stream_t *stream,
+                   apr_pool_t *result_pool,
                    apr_pool_t *scratch_pool)
 {
   svn_stringbuf_t *line;
@@ -1409,8 +1514,8 @@ parse_element_line(int *eid_p,
              kind, parent_eid_p, &offset);
   SVN_ERR_ASSERT(n >= 3);  /* C std is unclear on whether '%n' counts */
   SVN_ERR_ASSERT(line->data[offset] == ' ');
-  *name_p = line->data + offset + 1;
 
+  *name_p = apr_pstrdup(result_pool, line->data + offset + 1);
   *is_subbranch_p = (strcmp(kind, "subbranch") == 0);
 
   if (strcmp(*name_p, "(null)") == 0)
@@ -1474,6 +1579,10 @@ svn_branch__state_parse(svn_branch__stat
   branch_state = branch_state_create(bid, predecessor, root_eid, txn,
                                      result_pool);
 
+  /* Read in the merge history. */
+  SVN_ERR(merge_history_parse(branch_state,
+                              stream, result_pool, scratch_pool));
+
   /* Read in the structure. Set the payload of each normal element to a
      (branch-relative) reference. */
   for (i = 0; i < num_eids; i++)
@@ -1484,7 +1593,7 @@ svn_branch__state_parse(svn_branch__stat
 
       SVN_ERR(parse_element_line(&eid,
                                  &is_subbranch, &this_parent_eid, &this_name,
-                                 stream, scratch_pool));
+                                 stream, scratch_pool, scratch_pool));
 
       if (this_name)
         {
@@ -1553,6 +1662,30 @@ svn_branch__txn_parse(svn_branch__txn_t
   return SVN_NO_ERROR;
 }
 
+/* Serialize the merge history information for BRANCH.
+ */
+static svn_error_t *
+merge_history_serialize(svn_stream_t *stream,
+                        svn_branch__state_t *branch,
+                        apr_pool_t *scratch_pool)
+{
+  int num_merge_ancestors = (branch->priv->merge_ancestor) ? 1 : 0;
+  int i;
+
+  SVN_ERR(svn_stream_printf(stream, scratch_pool,
+                            "merge-history: merge-ancestors %d\n",
+                            num_merge_ancestors));
+  for (i = 0; i < num_merge_ancestors; i++)
+    {
+      SVN_ERR(svn_stream_printf(stream, scratch_pool,
+                                "merge-ancestor: r%ld.%s\n",
+                                branch->priv->merge_ancestor->rev,
+                                branch->priv->merge_ancestor->bid));
+    }
+
+  return SVN_NO_ERROR;
+}
+
 /* Write to STREAM a parseable representation of BRANCH.
  */
 svn_error_t *
@@ -1580,6 +1713,8 @@ svn_branch__state_serialize(svn_stream_t
                             apr_hash_count(branch->priv->element_tree->e_map),
                             predecessor_str));
 
+  SVN_ERR(merge_history_serialize(stream, branch, scratch_pool));
+
   for (SVN_EID__HASH_ITER_SORTED_BY_EID(ei, branch->priv->element_tree->e_map,
                                         scratch_pool))
     {

Modified: subversion/trunk/subversion/tests/cmdline/svnmover_tests.py
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svnmover_tests.py?rev=1716095&r1=1716094&r2=1716095&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svnmover_tests.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svnmover_tests.py Tue Nov 24 09:52:44 2015
@@ -558,6 +558,9 @@ def reported_br_params(path1, path2):
     subbranch_fullpath = path1 + '/' + path2
   return subbranch_rpath, subbranch_fullpath
 
+def reported_mg_diff():
+  return [r'--- .*merge history.*']
+
 def reported_br_diff(path1, path2=None):
   """Return expected header lines for diff of a branch, or subtree in a branch.
 
@@ -704,12 +707,14 @@ def merge_edits_with_move(sbox):
 
   # merge the move to trunk (r6)
   test_svnmover2(sbox, '',
+                 reported_mg_diff() +
                  reported_br_diff('trunk') +
                  reported_move('lib/foo', 'bar'),
                 'merge branches/br1@5 trunk trunk@2')
 
   # merge the edits in trunk (excluding the merge r6) to branch (r7)
   test_svnmover2(sbox, '',
+                 reported_mg_diff() +
                  reported_br_diff('branches/br1') +
                  reported_del('bar/x') +
                  reported_move('bar/y', 'bar/y2') +
@@ -983,6 +988,7 @@ def restructure_repo_ttb_projects_to_pro
 
   # merge the branch to trunk (r7)
   test_svnmover2(sbox, '',
+                 reported_mg_diff() +
                  reported_br_diff('trunk') +
                  reported_move('proj1/lib/foo', 'proj1/bar') +
                  reported_add('proj2') +
@@ -992,6 +998,7 @@ def restructure_repo_ttb_projects_to_pro
 
   # merge the edits in trunk (excluding the merge r6) to branch (r7)
   test_svnmover2(sbox, '',
+                 reported_mg_diff() +
                  reported_br_diff('branches/br1') +
                  reported_del('proj1/bar/x') +
                  reported_move('proj1/bar/y', 'proj1/bar/y2') +
@@ -1078,6 +1085,7 @@ def restructure_repo_projects_ttb_to_ttb
 
     # merge trunk to branch
     test_svnmover2(sbox, proj,
+                   reported_mg_diff() +
                    reported_br_diff(proj + '/branches/br1') +
                    reported_del('bar/x') +
                    reported_move('bar/y', 'bar/y2') +
@@ -1187,6 +1195,7 @@ def subbranches1(sbox):
 
   # merge 'branches/foo' to 'trunk'
   test_svnmover2(sbox, '',
+                 reported_mg_diff() +
                  reported_br_diff('trunk') +
                  reported_add('docs') +
                  reported_move('libsvn_fs_fs/file.c', 'libsvn_fs_fs/file2.c') +
@@ -1197,6 +1206,7 @@ def subbranches1(sbox):
 
   # merge 'trunk/libsvn_fs_fs' to 'trunk/libsvn_fs_x'
   test_svnmover2(sbox, '',
+                 reported_mg_diff() +
                  reported_br_diff('trunk/libsvn_fs_x') +
                  reported_move('reps/file.c', 'reps/file2.c'),
                  'merge trunk/libsvn_fs_fs trunk/libsvn_fs_x trunk/libsvn_fs_fs@4')
@@ -1224,6 +1234,7 @@ def merge_deleted_subbranch(sbox):
   #
   # This should delete the subbranch 'lib2'
   test_svnmover2(sbox, '',
+                 reported_mg_diff() +
                  reported_br_diff('branches/foo') +
                  reported_br_del('branches/foo', 'lib2'),
                  'merge trunk branches/foo trunk@' + str(yca_rev))
@@ -1247,6 +1258,7 @@ def merge_added_subbranch(sbox):
   #
   # This should add the subbranch 'lib2'
   test_svnmover2(sbox, '',
+                 reported_mg_diff() +
                  reported_br_diff('branches/foo') +
                  reported_br_add('branches/foo', 'lib2'),
                  'merge trunk branches/foo trunk@' + str(yca_rev))
@@ -1299,6 +1311,7 @@ def merge_from_subbranch_to_subtree(sbox
   # nil on the merge source-right, and tried to make that same change in the
   # target.
   test_svnmover2(sbox, '',
+                 reported_mg_diff() +
                  reported_br_diff('') +
                  reported_add('A/B1/C1/D'),
                  'merge A/B1/C2 A/B1/C1 A/B1/C1@2')
@@ -1375,6 +1388,7 @@ def merge_swap_abc(sbox):
     'B0.8/A/B/C' : 'B0.8/A',
   })
   test_svnmover3(sbox, '',
+                 reported_mg_diff() +
                  reported_br_diff('Y') +
                  reported_move('A/B/C', 'A') +
                  reported_move('A/B', 'A/B') +

Modified: subversion/trunk/subversion/tests/cmdline/svntest/wc.py
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svntest/wc.py?rev=1716095&r1=1716094&r2=1716095&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svntest/wc.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svntest/wc.py Tue Nov 24 09:52:44 2015
@@ -121,6 +121,7 @@ _re_parse_eid_header = re.compile('^r(-1
                                   'branches ([0-9]+)$')
 # B0.2 root-eid 3
 _re_parse_eid_branch = re.compile('^(B[0-9.]+) root-eid ([0-9]+) num-eids ([0-9]+)( from [^ ]*)?$')
+_re_parse_eid_merge_history = re.compile('merge-history: merge-ancestors ([0-9]+)')
 # e4: normal 6 C
 _re_parse_eid_ele = re.compile('^e([0-9]+): (none|normal|subbranch) '
                                '(-1|[0-9]+) (.*)$')
@@ -830,6 +831,11 @@ class State:
         branch_id = match.group(1)
         root_eid = match.group(2)
 
+      match = _re_parse_eid_merge_history.search(line)
+      if match:
+        ### TODO: store the merge history
+        pass
+
     add_to_desc(eids, desc, branch_id)
 
     return cls('', desc)

Modified: subversion/trunk/tools/dev/svnmover/merge3.c
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/dev/svnmover/merge3.c?rev=1716095&r1=1716094&r2=1716095&view=diff
==============================================================================
--- subversion/trunk/tools/dev/svnmover/merge3.c (original)
+++ subversion/trunk/tools/dev/svnmover/merge3.c Tue Nov 24 09:52:44 2015
@@ -915,7 +915,7 @@ merge_subbranch(svn_branch__txn_t *edit_
                                          svn_branch__root_eid(src_subbranch),
                                          scratch_pool);
 
-      SVN_ERR(svn_branch__txn_branch(edit_txn, NULL /*new_branch_id_p*/, from,
+      SVN_ERR(svn_branch__txn_branch(edit_txn, NULL /*new_branch_p*/, from,
                                      new_branch_id, scratch_pool, scratch_pool));
     }
   else if (subbr_tgt)  /* added on target branch */

Modified: subversion/trunk/tools/dev/svnmover/svnmover.c
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/dev/svnmover/svnmover.c?rev=1716095&r1=1716094&r2=1716095&view=diff
==============================================================================
--- subversion/trunk/tools/dev/svnmover/svnmover.c (original)
+++ subversion/trunk/tools/dev/svnmover/svnmover.c Tue Nov 24 09:52:44 2015
@@ -349,6 +349,39 @@ svnmover_element_differences(apr_hash_t
   return SVN_NO_ERROR;
 }
 
+/*  */
+static const char *
+rev_bid_str(const svn_branch__rev_bid_t *rev_bid,
+            apr_pool_t *result_pool)
+{
+  if (!rev_bid)
+    return "<nil>";
+  return apr_psprintf(result_pool, "r%ld.%s", rev_bid->rev, rev_bid->bid);
+}
+
+/* Set *DIFFERENCE_P to some sort of indication of the difference between
+ * MERGE_HISTORY1 and MERGE_HISTORY2, or to null if there is no difference.
+ *
+ * Inputs may be null.
+ */
+static svn_error_t *
+merge_history_diff(const char **difference_p,
+                   svn_branch__rev_bid_t *merge_history1,
+                   svn_branch__rev_bid_t *merge_history2,
+                   apr_pool_t *result_pool)
+{
+  *difference_p = NULL;
+  if ((merge_history1 || merge_history2)
+      && !(merge_history1 && merge_history2
+           && svn_branch__rev_bid_equal(merge_history1, merge_history2)))
+    {
+      *difference_p = apr_psprintf(result_pool, "%s -> %s",
+                                   rev_bid_str(merge_history1, result_pool),
+                                   rev_bid_str(merge_history2, result_pool));
+    }
+  return SVN_NO_ERROR;
+}
+
 /* Set *IS_CHANGED to true if EDIT_TXN differs from its base txn, else to
  * false.
  */
@@ -389,6 +422,9 @@ txn_is_changed(svn_branch__txn_t *edit_t
       svn_branch__state_t *base_branch
         = svn_branch__txn_get_branch_by_id(base_txn, edit_branch->bid,
                                            scratch_pool);
+      svn_branch__rev_bid_t *edit_branch_merge_history;
+      svn_branch__rev_bid_t *base_branch_merge_history;
+      const char *merge_history_difference;
       svn_element__tree_t *edit_branch_elements, *base_branch_elements;
       apr_hash_t *diff;
 
@@ -398,6 +434,22 @@ txn_is_changed(svn_branch__txn_t *edit_t
           return SVN_NO_ERROR;
         }
 
+      /* Compare merge histories */
+      SVN_ERR(svn_branch__state_get_merge_ancestor(
+                edit_branch, &edit_branch_merge_history, scratch_pool));
+      SVN_ERR(svn_branch__state_get_merge_ancestor(
+                base_branch, &base_branch_merge_history, scratch_pool));
+      SVN_ERR(merge_history_diff(&merge_history_difference,
+                                 edit_branch_merge_history,
+                                 base_branch_merge_history,
+                                 scratch_pool));
+      if (merge_history_difference)
+        {
+          *is_changed = TRUE;
+          return SVN_NO_ERROR;
+        }
+
+      /* Compare elements */
       SVN_ERR(svn_branch__state_get_elements(edit_branch, &edit_branch_elements,
                                              scratch_pool));
       SVN_ERR(svn_branch__state_get_elements(base_branch, &base_branch_elements,
@@ -591,6 +643,27 @@ svn_branch__replay(svn_branch__txn_t *ed
         }
     }
 
+  /* Replay any change in merge history */
+  {
+    svn_branch__rev_bid_t *left_merge_history = NULL;
+    svn_branch__rev_bid_t *right_merge_history = NULL;
+    const char *merge_history_difference;
+
+    if (left_branch)
+      SVN_ERR(svn_branch__state_get_merge_ancestor(
+                left_branch, &left_merge_history, scratch_pool));
+    if (right_branch)
+      SVN_ERR(svn_branch__state_get_merge_ancestor(
+                right_branch, &right_merge_history, scratch_pool));
+    SVN_ERR(merge_history_diff(&merge_history_difference,
+              left_merge_history, right_merge_history, scratch_pool));
+    if (merge_history_difference)
+      {
+        SVN_ERR(svn_branch__state_add_merge_ancestor(
+                  edit_branch, right_merge_history, scratch_pool));
+      }
+  }
+
   return SVN_NO_ERROR;
 }
 
@@ -784,7 +857,8 @@ typedef enum action_code_t {
   ACTION_BRANCH,
   ACTION_BRANCH_INTO,
   ACTION_MKBRANCH,
-  ACTION_MERGE,
+  ACTION_MERGE3,
+  ACTION_AUTO_MERGE,
   ACTION_MV,
   ACTION_MKDIR,
   ACTION_PUT_FILE,
@@ -841,8 +915,10 @@ static const action_defn_t action_defn[]
     "make a directory that's the root of a new subbranch"},
   {ACTION_DIFF,             "diff", 2, "LEFT@REV RIGHT@REV",
     "show differences from subtree LEFT to subtree RIGHT"},
-  {ACTION_MERGE,            "merge", 3, "FROM TO YCA@REV",
+  {ACTION_MERGE3,           "merge", 3, "FROM TO YCA@REV",
     "3-way merge YCA->FROM into TO"},
+  {ACTION_AUTO_MERGE,       "automerge", 2, "FROM TO",
+    "automatic merge FROM into TO"},
   {ACTION_CP,               "cp", 2, "REV SRC DST",
     "copy SRC@REV to DST"},
   {ACTION_MV,               "mv", 2, "SRC DST",
@@ -1379,6 +1455,8 @@ do_merge(svnmover_wc_t *wc,
          svn_branch__el_rev_id_t *yca,
          apr_pool_t *scratch_pool)
 {
+  svn_branch__rev_bid_t *new_ancestor;
+
   if (src->eid != tgt->eid || src->eid != yca->eid)
     {
       svnmover_notify(_("Warning: root elements differ in the requested merge "
@@ -1391,6 +1469,16 @@ do_merge(svnmover_wc_t *wc,
                                 src, tgt, yca,
                                 wc->pool, scratch_pool));
 
+  /* Update the merge history */
+  /* ### Assume this was a complete merge -- i.e. all changes up to YCA were
+     previously merged, so now SRC is a new ancestor. */
+  new_ancestor = svn_branch__rev_bid_create(src->rev, src->branch->bid,
+                                              scratch_pool);
+  SVN_ERR(svn_branch__state_add_merge_ancestor(wc->working->branch, new_ancestor,
+                                               scratch_pool));
+  svnmover_notify_v(_("--- recorded merge ancestor as: %ld.%s"),
+                    new_ancestor->rev, new_ancestor->bid);
+
   if (svnmover_any_conflicts(wc->conflicts))
     {
       SVN_ERR(svnmover_display_conflicts(wc->conflicts, scratch_pool));
@@ -1399,6 +1487,43 @@ do_merge(svnmover_wc_t *wc,
   return SVN_NO_ERROR;
 }
 
+/*
+ */
+static svn_error_t *
+do_auto_merge(svnmover_wc_t *wc,
+              svn_branch__el_rev_id_t *src,
+              svn_branch__el_rev_id_t *tgt,
+              apr_pool_t *scratch_pool)
+{
+  svn_branch__rev_bid_t *yca;
+
+  SVN_ERR(svn_branch__state_get_merge_ancestor(tgt->branch, &yca,
+                                               scratch_pool));
+  if (yca)
+    {
+      svn_branch__repos_t *repos = wc->working->branch->txn->repos;
+      svn_branch__state_t *yca_branch;
+      svn_branch__el_rev_id_t *_yca;
+
+      SVN_ERR(svn_branch__repos_get_branch_by_id(&yca_branch, repos,
+                                                 yca->rev, yca->bid,
+                                                 scratch_pool));
+      _yca = svn_branch__el_rev_id_create(yca_branch,
+                                          svn_branch__root_eid(yca_branch),
+                                          yca->rev, scratch_pool);
+
+      SVN_ERR(do_merge(wc, src, tgt, _yca, scratch_pool));
+    }
+  else
+    {
+      return svn_error_create(SVN_BRANCH__ERR, NULL,
+                              _("Cannot perform automatic merge: "
+                                "no YCA found"));
+    }
+
+  return SVN_NO_ERROR;
+}
+
 /*  */
 typedef struct diff_item_t
 {
@@ -1721,9 +1846,23 @@ branch_diff_r(svn_branch__el_rev_id_t *l
               const char *prefix,
               apr_pool_t *scratch_pool)
 {
+  svn_branch__rev_bid_t *merge_history1, *merge_history2;
+  const char *merge_history_difference;
   svn_branch__subtree_t *s_left;
   svn_branch__subtree_t *s_right;
 
+  /* ### This should be done for each branch, e.g. in subtree_diff_r(). */
+  /* ### This notification should start with a '--- diff branch ...' line. */
+  SVN_ERR(svn_branch__state_get_merge_ancestor(left->branch, &merge_history1,
+                                               scratch_pool));
+  SVN_ERR(svn_branch__state_get_merge_ancestor(right->branch, &merge_history2,
+                                               scratch_pool));
+  SVN_ERR(merge_history_diff(&merge_history_difference,
+                             merge_history1, merge_history2, scratch_pool));
+  if (merge_history_difference)
+    svnmover_notify("%s--- merge history is different: %s", prefix,
+                    merge_history_difference);
+
   SVN_ERR(svn_branch__get_subtree(left->branch, &s_left, left->eid,
                                   scratch_pool));
   SVN_ERR(svn_branch__get_subtree(right->branch, &s_right, right->eid,
@@ -2768,14 +2907,25 @@ execute(svnmover_wc_t *wc,
         case ACTION_INFO_WC:
           {
             svn_boolean_t is_modified;
+            svn_branch__rev_bid_t *merge_ancestor;
 
             SVN_ERR(txn_is_changed(wc->working->branch->txn, &is_modified,
                                    iterpool));
             svnmover_notify("Repository Root: %s", wc->repos_root_url);
             svnmover_notify("Base Revision: %ld", wc->base->revision);
             svnmover_notify("Base Branch:    %s", wc->base->branch->bid);
+            SVN_ERR(svn_branch__state_get_merge_ancestor(
+                      wc->base->branch, &merge_ancestor, iterpool));
+            if (merge_ancestor)
+              svnmover_notify("  merge ancestor: %ld.%s",
+                              merge_ancestor->rev, merge_ancestor->bid);
             svnmover_notify("Working Branch: %s", wc->working->branch->bid);
             svnmover_notify("Modified:       %s", is_modified ? "yes" : "no");
+            SVN_ERR(svn_branch__state_get_merge_ancestor(
+                      wc->working->branch, &merge_ancestor, iterpool));
+            if (merge_ancestor)
+              svnmover_notify("  merge ancestor: %ld.%s",
+                              merge_ancestor->rev, merge_ancestor->bid);
           }
           break;
 
@@ -2971,7 +3121,7 @@ execute(svnmover_wc_t *wc,
           }
           break;
 
-        case ACTION_MERGE:
+        case ACTION_MERGE3:
           {
             VERIFY_EID_EXISTS("merge", 0);
             VERIFY_EID_EXISTS("merge", 1);
@@ -2985,6 +3135,18 @@ execute(svnmover_wc_t *wc,
           }
           break;
 
+        case ACTION_AUTO_MERGE:
+          {
+            VERIFY_EID_EXISTS("merge", 0);
+            VERIFY_EID_EXISTS("merge", 1);
+
+            SVN_ERR(do_auto_merge(wc,
+                                  arg[0]->el_rev /*from*/,
+                                  arg[1]->el_rev /*to*/,
+                                  iterpool));
+          }
+          break;
+
         case ACTION_MV:
           SVN_ERR(point_to_outer_element_instead(arg[0]->el_rev, "mv",
                                                  iterpool));