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 2014/09/30 19:43:28 UTC

svn commit: r1628497 - /subversion/branches/move-tracking-2/subversion/svnmover/svnmover.c

Author: julianfoad
Date: Tue Sep 30 17:43:28 2014
New Revision: 1628497

URL: http://svn.apache.org/r1628497
Log:
On the 'move-tracking-2' branch: Add the beginnings of a 'merge' command
to the 'svnmover' program.

Modified:
    subversion/branches/move-tracking-2/subversion/svnmover/svnmover.c

Modified: subversion/branches/move-tracking-2/subversion/svnmover/svnmover.c
URL: http://svn.apache.org/viewvc/subversion/branches/move-tracking-2/subversion/svnmover/svnmover.c?rev=1628497&r1=1628496&r2=1628497&view=diff
==============================================================================
--- subversion/branches/move-tracking-2/subversion/svnmover/svnmover.c (original)
+++ subversion/branches/move-tracking-2/subversion/svnmover/svnmover.c Tue Sep 30 17:43:28 2014
@@ -151,6 +151,24 @@ txn_path_dirname(svn_editor3_txn_path_t 
   return parent_loc;
 }
 
+/* Return the txn-path of the parent directory of LOC.
+ */
+static const char *
+txn_path_basename(svn_editor3_txn_path_t loc)
+{
+  const char *base;
+
+  if (*loc.relpath)
+    {
+      base = svn_relpath_basename(loc.relpath, NULL);
+    }
+  else
+    {
+      base = svn_relpath_basename(loc.peg.relpath, NULL);
+    }
+  return base;
+}
+
 /* ====================================================================== */
 
 typedef struct mtcc_t
@@ -270,6 +288,7 @@ typedef enum action_code_t {
   ACTION_BRANCH,
   ACTION_BRANCHIFY,
   ACTION_DISSOLVE,
+  ACTION_MERGE,
   ACTION_MV,
   ACTION_MKDIR,
   ACTION_CP,
@@ -282,18 +301,19 @@ struct action {
   /* revision (copy-from-rev of path[0] for cp) */
   svn_revnum_t rev;
 
-  /* action    path[0]  path[1]
-   * ------    -------  -------
+  /* action    path[0]  path[1]  path[2]
+   * ------    -------  -------  -------
    * list_br   path
    * branch    source   target
    * branchify path
    * dissolve  path
+   * merge     from     to       yca@rev
    * mv        source   target
-   * mkdir     target   (null)
+   * mkdir     target
    * cp        source   target
-   * rm        target   (null)
+   * rm        target
    */
-  const char *path[2];
+  const char *path[3];
 };
 
 /* ====================================================================== */
@@ -316,6 +336,8 @@ typedef struct svn_branch_repos_t
   /* The range of family ids assigned within this repos (starts at 0). */
   int next_fid;
 
+  apr_array_header_t *rev_info;
+
   /* The pool in which this object lives. */
   apr_pool_t *pool;
 } svn_branch_repos_t;
@@ -359,9 +381,10 @@ typedef struct svn_branch_family_t
 
 /* A branch.
  *
- * A branch definition object describes the characteristics common to all
- * instances of a branch (with a given BID) in its family. (There is one
- * instance of this branch within each branch of its outer families.)
+ * A branch definition object describes the characteristics of a branch
+ * in a given family with a given BID. This definition is common to each
+ * branch that has this same family and BID: there can be one such instance
+ * within each branch of its outer families.
  *
  * Often, all branches in a family have the same root element. For example,
  * branching /trunk to /branches/br1 results in:
@@ -439,6 +462,27 @@ typedef struct svn_branch_element_t
 } svn_branch_element_t;
 */
 
+/* Branch-Revision-Element */
+typedef struct svn_branch_el_rev_id_t
+{
+  /* The branch-instance that applies to REV. */
+  svn_branch_instance_t *branch;
+  /* Element. */
+  int eid;
+  /* Revision. SVN_INVALID_REVNUM means 'in this transaction', not 'head'.
+     ### Do we need this if BRANCH refers to a particular branch-revision? */
+  svn_revnum_t rev;
+
+} svn_branch_el_rev_id_t;
+
+typedef struct svn_branch_el_rev_content_t
+{
+  int parent_eid;
+  const char *name;
+  svn_editor3_node_content_t *content;
+
+} svn_branch_el_rev_content_t;
+
 /* Create a new branching metadata object */
 static svn_branch_repos_t *
 svn_branch_repos_create(svn_ra_session_t *ra_session,
@@ -449,6 +493,7 @@ svn_branch_repos_create(svn_ra_session_t
 
   repos->ra_session = ra_session;
   repos->editor = editor;
+  repos->rev_info = apr_array_make(result_pool, 1, sizeof(void *));
   repos->pool = result_pool;
   return repos;
 }
@@ -632,8 +677,10 @@ branch_get_eid_by_path(const svn_branch_
 {
   int *eid_p = svn_hash_gets(branch->path_to_eid, path);
 
-  SVN_ERR_ASSERT_NO_RETURN(svn_relpath_skip_ancestor(
-                             branch_get_root_path(branch), path));
+  /* If the branch knows its root path, PATH must be within that. */
+  SVN_ERR_ASSERT_NO_RETURN(! branch_get_root_path(branch)
+                           || svn_relpath_skip_ancestor(
+                                branch_get_root_path(branch), path));
 
   if (! eid_p)
     return -1;
@@ -711,6 +758,8 @@ branch_mappings_delete(svn_branch_instan
  *
  * FROM_PATH MUST be an existing path in BRANCH, and may be the root path
  * of BRANCH.
+ *
+ * TO_PATH MUST be a path in TO_BRANCH at which nothing currently exists.
  */
 static svn_error_t *
 branch_mappings_move(svn_branch_instance_t *branch,
@@ -720,6 +769,9 @@ branch_mappings_move(svn_branch_instance
 {
   apr_hash_index_t *hi;
 
+  SVN_ERR_ASSERT(branch_get_eid_by_path(branch, from_path) >= 0);
+  SVN_ERR_ASSERT(branch_get_eid_by_path(branch, to_path) == -1);
+
   for (hi = apr_hash_first(scratch_pool, branch->eid_to_path);
        hi; hi = apr_hash_next(hi))
     {
@@ -753,6 +805,9 @@ family_add_new_element(svn_branch_family
  * FROM_PATH MUST be an existing path in FROM_BRANCH, and may be the
  * root path of FROM_BRANCH.
  *
+ * TO_PATH MUST be a path in TO_BRANCH at which nothing currently exists
+ * if INCLUDE_SELF, or an existing path if not INCLUDE_SELF.
+ *
  * If INCLUDE_SELF is true, include the element at FROM_PATH, otherwise
  * only act on children (recursively) of FROM_PATH.
  */
@@ -766,6 +821,9 @@ branch_mappings_copy(svn_branch_instance
 {
   apr_hash_index_t *hi;
 
+  SVN_ERR_ASSERT(branch_get_eid_by_path(from_branch, from_path) >= 0);
+  SVN_ERR_ASSERT((branch_get_eid_by_path(to_branch, to_path) >= 0) == (! include_self));
+
   for (hi = apr_hash_first(scratch_pool, from_branch->eid_to_path);
        hi; hi = apr_hash_next(hi))
     {
@@ -794,18 +852,25 @@ branch_mappings_copy(svn_branch_instance
  * FROM_PATH MUST be an existing path in FROM_BRANCH, and may be the
  * root path of FROM_BRANCH.
  *
- * TO_PATH MUST be a path in TO_BRANCH
+ * TO_PATH MUST be a path in TO_BRANCH at which nothing currently exists
+ * if INCLUDE_SELF, or an existing path if not INCLUDE_SELF.
+ *
+ * If INCLUDE_SELF is true, include the element at FROM_PATH, otherwise
+ * only act on children (recursively) of FROM_PATH.
  */
 static svn_error_t *
 branch_mappings_branch(svn_branch_instance_t *from_branch,
                        const char *from_path,
                        svn_branch_instance_t *to_branch,
                        const char *to_path,
+                       svn_boolean_t include_self,
                        apr_pool_t *scratch_pool)
 {
   apr_hash_index_t *hi;
 
   SVN_ERR_ASSERT(from_branch->definition->family == to_branch->definition->family);
+  SVN_ERR_ASSERT(branch_get_eid_by_path(from_branch, from_path) >= 0);
+  SVN_ERR_ASSERT((branch_get_eid_by_path(to_branch, to_path) >= 0) == (! include_self));
 
   for (hi = apr_hash_first(scratch_pool, from_branch->eid_to_path);
        hi; hi = apr_hash_next(hi))
@@ -814,7 +879,7 @@ branch_mappings_branch(svn_branch_instan
       const char *this_from_path = apr_hash_this_val(hi);
       const char *remainder = svn_relpath_skip_ancestor(from_path, this_from_path);
 
-      if (remainder)
+      if (remainder && (include_self || remainder[0]))
         {
           const char *this_to_path = svn_relpath_join(to_path, remainder,
                                                       scratch_pool);
@@ -946,9 +1011,10 @@ family_add_new_subfamily(svn_branch_fami
   return family;
 }
 
-/* Create a new branch definition in FAMILY, with root EID ROOT_EID.
- * Create a new, empty branch instance in FAMILY, empty except for the
- * root element which is at path ROOT_RRPATH.
+/* Create a new branch definition in FAMILY, with root element ROOT_EID at
+ * ROOT_RRPATH.
+ *
+ * Create a new, empty branch instance in FAMILY.
  */
 static svn_branch_instance_t *
 family_add_new_branch(svn_branch_family_t *family,
@@ -961,8 +1027,9 @@ family_add_new_branch(svn_branch_family_
   svn_branch_instance_t *branch_instance
     = svn_branch_instance_create(branch_definition, family->pool);
 
+  /* The root EID must be an existing EID. */
   SVN_ERR_ASSERT_NO_RETURN(root_eid >= family->first_eid
-                           && root_eid < family->next_eid);
+                           /*&& root_eid < family->next_eid*/);
   /* ROOT_RRPATH must not be within another branch of the family. */
 
   /* Register the branch */
@@ -1189,12 +1256,12 @@ write_repos_info(svn_stream_t *stream,
  * branch-tracking metadata from the repository into it.
  */
 static svn_error_t *
-fetch_repos_info(svn_branch_repos_t **repos_p,
-                 svn_ra_session_t *ra_session,
-                 svn_revnum_t base_revision,
-                 svn_editor3_t *editor,
-                 apr_pool_t *result_pool,
-                 apr_pool_t *scratch_pool)
+fetch_per_revision_info(svn_branch_repos_t **repos_p,
+                        svn_ra_session_t *ra_session,
+                        svn_revnum_t revision,
+                        svn_editor3_t *editor,
+                        apr_pool_t *result_pool,
+                        apr_pool_t *scratch_pool)
 {
   svn_branch_repos_t *repos
     = svn_branch_repos_create(ra_session, editor, result_pool);
@@ -1202,12 +1269,7 @@ fetch_repos_info(svn_branch_repos_t **re
   svn_stream_t *stream;
 
   /* Read initial state from repository */
-  if (base_revision == SVN_INVALID_REVNUM)
-    {
-      SVN_ERR(svn_ra_get_latest_revnum(ra_session, &base_revision,
-                                       scratch_pool));
-    }
-  SVN_ERR(svn_ra_rev_prop(ra_session, base_revision, "svn-br-info", &value,
+  SVN_ERR(svn_ra_rev_prop(ra_session, revision, "svn-br-info", &value,
                           scratch_pool));
   if (! value)
     {
@@ -1234,6 +1296,43 @@ fetch_repos_info(svn_branch_repos_t **re
   return SVN_NO_ERROR;
 }
 
+/* Create a new repository object and read the move-tracking /
+ * branch-tracking metadata from the repository into it.
+ */
+static svn_error_t *
+fetch_repos_info(svn_branch_repos_t **repos_p,
+                 svn_ra_session_t *ra_session,
+                 svn_revnum_t base_revision,
+                 svn_editor3_t *editor,
+                 apr_pool_t *result_pool,
+                 apr_pool_t *scratch_pool)
+{
+  svn_branch_repos_t *repos;
+  svn_revnum_t r;
+
+  if (base_revision == SVN_INVALID_REVNUM)
+    {
+      SVN_ERR(svn_ra_get_latest_revnum(ra_session, &base_revision,
+                                       scratch_pool));
+    }
+
+  SVN_ERR(fetch_per_revision_info(&repos, ra_session, base_revision,
+                                  editor, result_pool, scratch_pool));
+
+  APR_ARRAY_PUSH(repos->rev_info, void *) = NULL; /* r0 */
+  for (r = 1; r <= base_revision; r++)
+    {
+      svn_branch_repos_t *rev_info;
+
+      SVN_ERR(fetch_per_revision_info(&rev_info, ra_session, r,
+                                      editor, result_pool, scratch_pool));
+      APR_ARRAY_PUSH(repos->rev_info, void *) = rev_info;
+    }
+
+  *repos_p = repos;
+  return SVN_NO_ERROR;
+}
+
 /* Write to STREAM a parseable representation of BRANCH.
  */
 static svn_error_t *
@@ -1467,6 +1566,51 @@ branch_find_nested_branch_element_by_pat
     *eid_p = branch_get_eid_by_path(root_branch, rrpath);
 }
 
+/* Find the deepest branch in REPOS (recursively) of which RRPATH_REV is
+ * either the root element or a normal, non-sub-branch element.
+ *
+ * RRPATH_REV is a repository-relative path with an optional "@REV" suffix.
+ *
+ * Return the location of the element at RRPATH_REV in that branch, or with
+ * EID=-1 if no element exists there.
+ *
+ * The result will never be NULL.
+ */
+static svn_error_t *
+repos_get_el_rev_by_loc(svn_branch_el_rev_id_t **el_rev_p,
+                        const svn_branch_repos_t *repos,
+                        const char *rrpath_rev,
+                        apr_pool_t *result_pool,
+                        apr_pool_t *scratch_pool)
+{
+  svn_branch_el_rev_id_t *el_rev = apr_palloc(result_pool, sizeof(*el_rev));
+  svn_branch_instance_t *branch;
+  const char *rrpath;
+  svn_opt_revision_t rev;
+  const svn_branch_repos_t *rev_info;
+
+  SVN_ERR(svn_opt_parse_path(&rev, &rrpath, rrpath_rev, scratch_pool));
+  if (rev.kind == svn_opt_revision_number)
+    {
+      el_rev->rev = rev.value.number;
+      rev_info = APR_ARRAY_IDX(repos->rev_info, rev.value.number, void *);
+    }
+  else
+    {
+      el_rev->rev = SVN_INVALID_REVNUM;
+      rev_info = repos;
+    }
+  branch = APR_ARRAY_IDX(rev_info->root_family->branch_instances, 0, void *);
+  branch_find_nested_branch_element_by_path(&el_rev->branch, &el_rev->eid,
+                                            branch, rrpath,
+                                            scratch_pool);
+
+  /* Any path must at least be within the repository root branch */
+  SVN_ERR_ASSERT_NO_RETURN(el_rev->branch);
+  *el_rev_p = el_rev;
+  return SVN_NO_ERROR;
+}
+
 /* Find the deepest branch in REPOS (recursively) of which RRPATH is
  * either the root element or a normal, non-sub-branch element.
  * If EID_P is not null, set *EID_P to the EID of RRPATH in that branch.
@@ -1823,7 +1967,8 @@ branch_branch_subtree_r(svn_branch_insta
 
   /* populate new branch instance with path mappings */
   SVN_ERR(branch_mappings_branch(from_branch, from_path,
-                                 new_branch, to_path, scratch_pool));
+                                 new_branch, to_path,
+                                 FALSE /*include_self*/, scratch_pool));
 
   /* branch any subbranches of FROM_BRANCH */
   subbranches = branch_get_sub_branches(from_branch, scratch_pool, scratch_pool);
@@ -1906,6 +2051,573 @@ branch_copy_subtree_r(svn_branch_instanc
   return SVN_NO_ERROR;
 }
 
+/* Return TRUE iff EL_REV is the root element of its branch. */
+static svn_boolean_t
+svn_branch_el_rev_is_root(const svn_branch_el_rev_id_t *el_rev)
+{
+  return el_rev->eid == el_rev->branch->definition->root_eid;
+}
+
+/* Return the repos-relpath for element EL_REV, or NULL if EL_REV
+ * is not currently present in BRANCH.
+ *
+ * EL_REV->EID MUST be a valid EID belonging to BRANCH's family, but the
+ * element need not be present in any branch.
+ */
+static const char *
+svn_branch_el_rev_get_rrpath(const svn_branch_el_rev_id_t *el_rev)
+{
+  /* EL_REV->BRANCH presently points to mappings for the specific rev. */
+  return branch_get_path_by_eid(el_rev->branch, el_rev->eid);
+}
+
+/*  */
+static int
+svn_branch_el_rev_get_parent_eid(const svn_branch_el_rev_id_t *el_rev,
+                                 apr_pool_t *scratch_pool)
+{
+  int parent_eid;
+
+  if (svn_branch_el_rev_is_root(el_rev))
+    {
+      parent_eid = -1;
+    }
+  else
+    {
+      const char *rrpath = svn_branch_el_rev_get_rrpath(el_rev);
+
+      rrpath = svn_relpath_dirname(rrpath, scratch_pool);
+      parent_eid = branch_get_eid_by_path(el_rev->branch, rrpath);
+    }
+
+  return parent_eid;
+}
+
+/*  */
+static const char *
+svn_branch_el_rev_get_basename(const svn_branch_el_rev_id_t *el_rev,
+                               apr_pool_t *result_pool)
+{
+  const char *name;
+
+  if (svn_branch_el_rev_is_root(el_rev))
+    {
+      name = "";
+    }
+  else
+    {
+      const char *rrpath = svn_branch_el_rev_get_rrpath(el_rev);
+
+      name = svn_relpath_basename(rrpath, result_pool);
+    }
+
+  return name;
+}
+
+/*  */
+static svn_error_t *
+svn_branch_el_rev_get_node_content(svn_editor3_node_content_t **content_p,
+                                   const svn_branch_el_rev_id_t *el_rev,
+                                   apr_pool_t *result_pool,
+                                   apr_pool_t *scratch_pool)
+{
+  const char *rrpath = svn_branch_el_rev_get_rrpath(el_rev);
+  svn_editor3_node_content_t *content = NULL;
+  struct fb_baton
+  {
+    svn_ra_session_t *session;
+  } fbb = { el_rev->branch->definition->family->repos->ra_session };
+
+  if (rrpath)
+    {
+      content = apr_palloc(result_pool, sizeof(*content));
+      SVN_ERR(svn_ra_fetch(&content->kind,
+                           &content->props,
+                           &content->text, NULL,
+                           &fbb, rrpath, el_rev->rev,
+                           result_pool, scratch_pool));
+    }
+
+  *content_p = content;
+  return SVN_NO_ERROR;
+}
+
+/*  */
+static svn_error_t *
+svn_branch_el_rev_get(svn_branch_el_rev_content_t **content_p,
+                      const svn_branch_el_rev_id_t *el_rev,
+                      apr_pool_t *result_pool,
+                      apr_pool_t *scratch_pool)
+{
+  const char *rrpath = svn_branch_el_rev_get_rrpath(el_rev);
+  svn_branch_el_rev_content_t *content = NULL;
+
+  if (rrpath)
+    {
+      content = apr_palloc(result_pool, sizeof(*content));
+      content->parent_eid = svn_branch_el_rev_get_parent_eid(el_rev,
+                                                             scratch_pool);
+      content->name = svn_branch_el_rev_get_basename(el_rev, result_pool);
+      SVN_ERR(svn_branch_el_rev_get_node_content(&content->content, el_rev,
+                                                 result_pool, scratch_pool));
+    }
+
+  *content_p = content;
+  return SVN_NO_ERROR;
+}
+
+/*  */
+static svn_boolean_t
+svn_editor3_node_content_equal(const svn_editor3_node_content_t *content_left,
+                               const svn_editor3_node_content_t *content_right,
+                               apr_pool_t *scratch_pool)
+{
+  apr_array_header_t *prop_diffs;
+
+  if (content_left->kind != content_right->kind)
+    {
+      /*SVN_DBG(("node_content_equal: kind %d != %d", content_left->kind, content_right->kind));*/
+      return FALSE;
+    }
+  svn_error_clear(svn_prop_diffs(&prop_diffs,
+                                 content_left->props, content_right->props,
+                                 scratch_pool));
+  if (prop_diffs->nelts != 0)
+    {
+      /*SVN_DBG(("node_content_equal: props different"));*/
+      return FALSE;
+    }
+  switch (content_left->kind)
+    {
+    case svn_node_dir:
+      break;
+    case svn_node_file:
+      if (! svn_stringbuf_compare(content_left->text, content_right->text))
+        {
+          /*SVN_DBG(("node_content_equal: text different"));*/
+          return FALSE;
+        }
+      break;
+    case svn_node_symlink:
+      if (strcmp(content_left->target, content_right->target) != 0)
+        {
+          return FALSE;
+        }
+      break;
+    default:
+      break;
+    }
+
+  return TRUE;
+}
+
+/* Return the relative path to element EID within SUBTREE. */
+static const char *
+element_relpath_in_subtree(const svn_branch_el_rev_id_t *subtree,
+                           int eid)
+{
+  const char *subtree_path = branch_get_path_by_eid(subtree->branch, subtree->eid);
+  const char *element_path = branch_get_path_by_eid(subtree->branch, eid);
+  const char *relpath = NULL;
+
+  SVN_ERR_ASSERT_NO_RETURN(subtree_path);
+
+  if (element_path)
+    relpath = svn_relpath_skip_ancestor(subtree_path, element_path);
+
+  return relpath;
+}
+
+/*  */
+static svn_boolean_t
+svn_branch_el_rev_content_equal(const svn_branch_el_rev_content_t *content_left,
+                                const svn_branch_el_rev_content_t *content_right,
+                                apr_pool_t *scratch_pool)
+{
+  if (content_left->parent_eid != content_right->parent_eid)
+    {
+      /*SVN_DBG(("el_rev_content_equal: parent e%d != e%d", content_left->parent_eid, content_right->parent_eid));*/
+      return FALSE;
+    }
+  if (strcmp(content_left->name, content_right->name) != 0)
+    {
+      /*SVN_DBG(("el_rev_content_equal: name %s != %s", content_left->name, content_right->name));*/
+      return FALSE;
+    }
+  if (! svn_editor3_node_content_equal(content_left->content,
+                                       content_right->content,
+                                       scratch_pool))
+    {
+      /*SVN_DBG(("el_rev_content_equal: content differs"));*/
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+/*  */
+static svn_boolean_t
+branch_element_equal(int eid,
+                     const svn_branch_el_rev_content_t *content_left,
+                     const svn_branch_el_rev_content_t *content_right,
+                     apr_pool_t *scratch_pool)
+{
+  svn_boolean_t same;
+
+  if (!content_left && !content_right)
+    {
+      same = TRUE;
+    }
+  else if (!content_left || !content_right
+           || !svn_branch_el_rev_content_equal(content_left, content_right,
+                                               scratch_pool))
+    {
+      /*if (!content_right)
+        SVN_DBG(("  e%d left-only", eid));
+      else if (!content_left)
+        SVN_DBG(("  e%d right-only", eid));
+      else
+        SVN_DBG(("  e%d different: parent? %+d, name? %+d, content?",
+                 eid, content_right->parent_eid - content_left->parent_eid,
+                 strcmp(content_left->name, content_right->name)));*/
+      same = FALSE;
+    }
+  else
+    {
+      /*SVN_DBG(("  e%d same", eid));*/
+      same = TRUE;
+    }
+  return same;
+}
+
+/* Return (left, right) pairs of element content that differ between
+ * subtrees LEFT and RIGHT.
+ */
+static svn_error_t *
+branch_subtree_differences(apr_hash_t **h_left_p,
+                           const svn_branch_el_rev_id_t *left,
+                           const svn_branch_el_rev_id_t *right,
+                           apr_pool_t *result_pool,
+                           apr_pool_t *scratch_pool)
+{
+  apr_hash_t *h_left = apr_hash_make(result_pool);
+  int first_eid, next_eid;
+  int e;
+
+  /*SVN_DBG(("branch_element_differences(b%d r%ld, b%d r%ld, e%d)",
+           left->branch->definition->bid, left->rev,
+           right->branch->definition->bid, right->rev, right->eid));*/
+  SVN_ERR_ASSERT(left->branch->definition->family->fid
+                 == right->branch->definition->family->fid);
+
+  first_eid = left->branch->definition->family->first_eid;
+  next_eid = MAX(left->branch->definition->family->next_eid,
+                 right->branch->definition->family->next_eid);
+
+  for (e = first_eid; e < next_eid; e++)
+    {
+      svn_branch_el_rev_content_t *content_left = NULL;
+      svn_branch_el_rev_content_t *content_right = NULL;
+
+      if (e < left->branch->definition->family->next_eid
+          && element_relpath_in_subtree(left, e))
+        {
+          svn_branch_el_rev_id_t el_rev = *left;
+          el_rev.eid = e;
+          SVN_ERR(svn_branch_el_rev_get(&content_left, &el_rev,
+                                        result_pool, scratch_pool));
+        }
+      if (e < right->branch->definition->family->next_eid
+          && element_relpath_in_subtree(right, e))
+        {
+          svn_branch_el_rev_id_t el_rev = *right;
+          el_rev.eid = e;
+          SVN_ERR(svn_branch_el_rev_get(&content_right, &el_rev,
+                                        result_pool, scratch_pool));
+        }
+
+      if (! branch_element_equal(e, content_left, content_right, scratch_pool))
+        {
+          int *eid_stored = apr_pmemdup(result_pool, &e, sizeof(e));
+          svn_branch_el_rev_content_t **contents
+            = apr_palloc(result_pool, 2 * sizeof(void *));
+
+          contents[0] = content_left;
+          contents[1] = content_right;
+          apr_hash_set(h_left, eid_stored, sizeof(*eid_stored), contents);
+        }
+    }
+
+  *h_left_p = h_left;
+  return SVN_NO_ERROR;
+}
+
+/* Merge the content for one element.
+ */
+static void
+element_merge(svn_branch_el_rev_content_t **result_p,
+              svn_boolean_t *conflict_p,
+              int eid,
+              svn_branch_el_rev_content_t *side1,
+              svn_branch_el_rev_content_t *side2,
+              svn_branch_el_rev_content_t *yca,
+              apr_pool_t *result_pool,
+              apr_pool_t *scratch_pool)
+{
+  svn_boolean_t same1 = branch_element_equal(eid, yca, side1, scratch_pool);
+  svn_boolean_t same2 = branch_element_equal(eid, yca, side2, scratch_pool);
+  svn_boolean_t conflict = FALSE;
+  svn_branch_el_rev_content_t *result;
+
+  if (same1)
+    {
+      result = side2;
+    }
+  else if (same2)
+    {
+      result = side1;
+    }
+  else if (! yca || ! side1 || ! side2)
+    {
+      /* Side 1 and side 2 both changed; not all of them exist */
+      /* ### Need not be a conflict if side1 == side2. */
+      result = yca;
+      conflict = TRUE;
+    }
+  else
+    {
+      /* All three sides are different, and all exist */
+      result = apr_pmemdup(result_pool, yca, sizeof(*result));
+
+      /* merge the parent-eid */
+      if (side1->parent_eid == yca->parent_eid)
+        {
+          result->parent_eid = side2->parent_eid;
+        }
+      else if (side2->parent_eid == yca->parent_eid)
+        {
+          result->parent_eid = side1->parent_eid;
+        }
+      else
+        {
+          conflict = TRUE;
+        }
+
+      /* merge the name */
+      if (strcmp(side1->name, yca->name) == 0)
+        {
+          result->name = side2->name;
+        }
+      else if (strcmp(side2->name, yca->name) == 0)
+        {
+          result->name = side1->name;
+        }
+      else
+        {
+          conflict = TRUE;
+        }
+
+      /* merge the content */
+      if (svn_editor3_node_content_equal(side1->content, yca->content,
+                                         scratch_pool))
+        {
+          result->content = side2->content;
+        }
+      else if (svn_editor3_node_content_equal(side2->content, yca->content,
+                                              scratch_pool))
+        {
+          result->content = side1->content;
+        }
+      else
+        {
+          /* ### Need not conflict if can merge props and text separately. */
+          conflict = TRUE;
+        }
+    }
+
+  if (conflict)
+    {
+      /*SVN_DBG(("  e%d: conflict!", eid));*/
+    }
+
+  *result_p = result;
+  *conflict_p = conflict;
+}
+
+/* Merge ...
+ *
+ * Merge any sub-branches in the same way, recursively.
+ */
+static svn_error_t *
+branch_merge_subtree_r(const svn_branch_el_rev_id_t *src,
+                       const svn_branch_el_rev_id_t *tgt,
+                       const svn_branch_el_rev_id_t *yca,
+                       apr_pool_t *scratch_pool)
+{
+  /*svn_editor3_t *editor = src->branch->definition->family->repos->editor;*/
+  apr_hash_t *diff_yca_src, *diff_yca_tgt;
+  svn_boolean_t had_conflict = FALSE;
+  int first_eid, next_eid, eid;
+
+  SVN_ERR_ASSERT(src->branch->definition->family->fid
+                 == tgt->branch->definition->family->fid);
+  SVN_ERR_ASSERT(src->branch->definition->family->fid
+                 == yca->branch->definition->family->fid);
+  SVN_ERR_ASSERT(src->eid == tgt->eid);
+  SVN_ERR_ASSERT(src->eid == yca->eid);
+
+  SVN_DBG(("merge src: r%2ld f%d b%2d e%3d",
+           src->rev, src->branch->definition->family->fid,
+           src->branch->definition->bid, src->eid));
+  SVN_DBG(("merge tgt: r%2ld f%d b%2d e%3d",
+           tgt->rev, tgt->branch->definition->family->fid,
+           tgt->branch->definition->bid, tgt->eid));
+  SVN_DBG(("merge yca: r%2ld f%d b%2d e%3d",
+           yca->rev, yca->branch->definition->family->fid,
+           yca->branch->definition->bid, yca->eid));
+
+  /*
+      for (eid, diff1) in element_differences(YCA, FROM):
+        diff2 = element_diff(eid, YCA, TO)
+        if diff1 and diff2:
+          result := element_merge(diff1, diff2)
+        elif diff1:
+          result := diff1.right
+        # else no change
+   */
+  SVN_ERR(branch_subtree_differences(&diff_yca_src,
+                                     yca, src,
+                                     scratch_pool, scratch_pool));
+  /* ### We only need to query for YCA:TO differences in elements that are
+         different in YCA:FROM, but right now we ask for all differences. */
+  SVN_ERR(branch_subtree_differences(&diff_yca_tgt,
+                                     yca, tgt,
+                                     scratch_pool, scratch_pool));
+
+  first_eid = yca->branch->definition->family->first_eid;
+  next_eid = yca->branch->definition->family->next_eid;
+  next_eid = MAX(next_eid, src->branch->definition->family->next_eid);
+  next_eid = MAX(next_eid, tgt->branch->definition->family->next_eid);
+
+  for (eid = first_eid; eid < next_eid; eid++)
+    {
+      svn_branch_el_rev_content_t **e_yca_src
+        = apr_hash_get(diff_yca_src, &eid, sizeof(eid));
+      svn_branch_el_rev_content_t **e_yca_tgt
+        = apr_hash_get(diff_yca_tgt, &eid, sizeof(eid));
+      svn_branch_el_rev_content_t *e_yca;
+      svn_branch_el_rev_content_t *e_src;
+      svn_branch_el_rev_content_t *e_tgt;
+      svn_branch_el_rev_content_t *result;
+      svn_boolean_t conflict = FALSE;
+
+      /* If an element hasn't changed in the source branch, there is
+         no need to do anything with it in the target branch. */
+      if (! e_yca_src)
+        {
+          continue;
+        }
+
+      e_yca = e_yca_src[0];
+      e_src = e_yca_src[1];
+      e_tgt = e_yca_tgt ? e_yca_tgt[1] : e_yca_src[0];
+
+      element_merge(&result, &conflict,
+                    eid, e_src, e_tgt, e_yca,
+                    scratch_pool, scratch_pool);
+
+      if (conflict)
+        {
+          SVN_DBG(("merged: e%d => conflict", eid));
+          had_conflict = TRUE;
+        }
+      else if (e_tgt && result)
+        {
+          SVN_DBG(("merged: e%d => parent=e%d, name=%s, content=...",
+                   eid, result->parent_eid, result->name));
+
+          /*SVN_ERR(svn_editor3_alter(editor, loc.peg.rev, nbid,
+                                    new_parent_nbid, result->name,
+                                    result->content));*/
+        }
+      else if (e_tgt)
+        {
+          SVN_DBG(("merged: e%d => <deleted>", eid));
+          /*SVN_ERR(svn_editor3_delete(editor, loc.peg.rev, nbid));*/
+        }
+      else if (result)
+        {
+          SVN_DBG(("merged: e%d => <added>", eid));
+          /*SVN_ERR(svn_editor3_add(editor, nbid, result->content->kind,
+                                  new_parent_nbid, result->name,
+                                  result->content));*/
+        }
+    }
+
+  if (had_conflict)
+    {
+      return svn_error_createf(SVN_ERR_BRANCHING, NULL,
+                               _("merge failed: conflict(s) occurred"));
+    }
+  else
+    {
+      SVN_DBG(("merge completed: no conflicts"));
+    }
+
+  /* ### TODO: subbranches */
+
+  return SVN_NO_ERROR;
+}
+
+/* Merge SRC into TGT, using the common ancestor YCA.
+ *
+ * Merge the two sets of changes: YCA -> SRC and YCA -> TGT, applying
+ * the result to the transaction at TGT.
+ *
+ * If conflicts arise, just fail.
+ *
+ * SRC->BRANCH, TGT->BRANCH and YCA->BRANCH must be in the same family.
+ *
+ * SRC, TGT and YCA must be existing and corresponding (same EID) elements
+ * of the branch family.
+ *
+ * None of SRC, TGT and YCA is a subbranch root element.
+ *
+ * ### TODO:
+ *     If ... contains nested subbranches, these will also be merged.
+ */
+static svn_error_t *
+svn_branch_merge(svn_branch_el_rev_id_t *src,
+                 svn_branch_el_rev_id_t *tgt,
+                 svn_branch_el_rev_id_t *yca,
+                 apr_pool_t *scratch_pool)
+{
+  if (src->branch->definition->family->fid != tgt->branch->definition->family->fid
+      || src->branch->definition->family->fid != yca->branch->definition->family->fid)
+    return svn_error_createf(SVN_ERR_BRANCHING, NULL,
+                             _("Merge branches must all be in same family "
+                               "(from: f%d, to: f%d, yca: f%d)"),
+                             src->branch->definition->family->fid,
+                             tgt->branch->definition->family->fid,
+                             yca->branch->definition->family->fid);
+
+  /*SVN_ERR(verify_exists_in_branch(from, scratch_pool));*/
+  /*SVN_ERR(verify_exists_in_branch(to, scratch_pool));*/
+  /*SVN_ERR(verify_exists_in_branch(yca, scratch_pool));*/
+  if (src->eid != tgt->eid || src->eid != yca->eid)
+    return svn_error_createf(SVN_ERR_BRANCHING, NULL,
+                             _("Merge branches must all be same element "
+                               "(from: e%d, to: e%d, yca: e%d)"),
+                             src->eid, tgt->eid, yca->eid);
+  /*SVN_ERR(verify_not_subbranch_root(from, scratch_pool));*/
+  /*SVN_ERR(verify_not_subbranch_root(to, scratch_pool));*/
+  /*SVN_ERR(verify_not_subbranch_root(yca, scratch_pool));*/
+
+  SVN_ERR(branch_merge_subtree_r(src, tgt, yca, scratch_pool));
+
+  return SVN_NO_ERROR;
+}
+
 /* In BRANCH, move the subtree at FROM_LOC to PARENT_LOC:NEW_NAME.
  *
  * FROM_LOC must be an existing non-root element of BRANCH. It may also
@@ -2133,6 +2845,7 @@ execute(const char *branch_rrpath,
       const char *path1_name = NULL;
       svn_editor3_txn_path_t path2_parent = {{0},0};
       const char *path2_name = NULL;
+      svn_branch_el_rev_id_t *el_rev1, *el_rev2, *el_rev3;
 
       svn_pool_clear(iterpool);
 
@@ -2140,6 +2853,8 @@ execute(const char *branch_rrpath,
         {
           const char *rrpath1
             = svn_uri_skip_ancestor(repos_root_url, action->path[0], pool);
+
+          SVN_ERR(repos_get_el_rev_by_loc(&el_rev1, repos, rrpath1, pool, pool));
           path1_txn_loc = txn_path(rrpath1, base_revision, ""); /* ### need to
             find which part of given path was pre-existing and which was created */
           path1_parent = txn_path(svn_relpath_dirname(rrpath1, pool), base_revision, ""); /* ### need to
@@ -2150,10 +2865,20 @@ execute(const char *branch_rrpath,
         {
           const char *rrpath2
             = svn_uri_skip_ancestor(repos_root_url, action->path[1], pool);
+
+          SVN_ERR(repos_get_el_rev_by_loc(&el_rev2, repos, rrpath2, pool, pool));
           path2_parent = txn_path(svn_relpath_dirname(rrpath2, pool), base_revision, ""); /* ### need to
             find which part of given path was pre-existing and which was created */
           path2_name = svn_relpath_basename(rrpath2, NULL);
         }
+      if (action->path[2])
+        {
+          const char *rrpath_rev3
+            = svn_uri_skip_ancestor(repos_root_url, action->path[2], pool);
+
+          SVN_ERR(repos_get_el_rev_by_loc(&el_rev3, repos, rrpath_rev3,
+                                          pool, pool));
+        }
       switch (action->action)
         {
         case ACTION_LIST_BRANCHES:
@@ -2181,6 +2906,11 @@ execute(const char *branch_rrpath,
                                   _("'dissolve' operation not implemented"));
           made_changes = TRUE;
           break;
+        case ACTION_MERGE:
+          SVN_ERR(svn_branch_merge(el_rev1 /*from*/, el_rev2 /*to*/,
+                                   el_rev3 /*yca*/, iterpool));
+          /*made_changes = TRUE;*/
+          break;
         case ACTION_MV:
           SVN_ERR(svn_branch_mv(branch,
                                 path1_txn_loc, path2_parent, path2_name,
@@ -2260,6 +2990,7 @@ usage(FILE *stream, apr_pool_t *pool)
       "                           a sub-branch (presently, in a new branch family)\n"
       "  dissolve BR-ROOT       : change the existing sub-branch at SRC into a\n"
       "                           simple sub-tree of its parent branch\n"
+      "  merge FROM TO YCA@REV  : merge changes YCA->FROM and YCA->TO into TO\n"
       "  cp REV SRC-URL DST-URL : copy SRC-URL@REV to DST-URL\n"
       "  mv SRC-URL DST-URL     : move SRC-URL to DST-URL\n"
       "  rm URL                 : delete URL\n"
@@ -2687,6 +3418,8 @@ sub_main(int *exit_code, int argc, const
         action->action = ACTION_BRANCHIFY;
       else if (! strcmp(action_string, "dissolve"))
         action->action = ACTION_DISSOLVE;
+      else if (! strcmp(action_string, "merge"))
+        action->action = ACTION_MERGE;
       else if (! strcmp(action_string, "mv"))
         action->action = ACTION_MV;
       else if (! strcmp(action_string, "cp"))
@@ -2746,6 +3479,8 @@ sub_main(int *exit_code, int argc, const
       else if (action->action == ACTION_LIST_BRANCHES
                || action->action == ACTION_LIST_BRANCHES_R)
         num_url_args = 0;
+      else if (action->action == ACTION_MERGE)
+        num_url_args = 3;
       else
         num_url_args = 2;