You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by st...@apache.org on 2017/08/31 17:42:53 UTC

svn commit: r1806831 - in /subversion/trunk/subversion: libsvn_client/conflicts.c tests/libsvn_client/conflicts-test.c

Author: stsp
Date: Thu Aug 31 17:42:53 2017
New Revision: 1806831

URL: http://svn.apache.org/viewvc?rev=1806831&view=rev
Log:
Teach the conflict resolver to detect merged moves.

If a move is merged without conflict, the client creates copyfrom info
which points back into a different branch, such that matching a copyfrom
path to the deleted conflict victim's path won't work (see issue #2685).

While scanning for moves, the resolver now also investigates whether any
copies within a revision are related to any deleted paths within that same
revision. If so, it assumes a merged move and constructs move information
which looks as if this move had occured directly on the local branch.

* subversion/libsvn_client/conflicts.c
  (push_move): New function, extracted from find_moves_in_revision().
  (find_yca): Move upwards in the file so it is declared earlier.
  (find_related_move): New function which registers deletions and related
   copies as a move in the moves table.
  (match_copies_to_deletion): New function, taken from find_moves_in_revision()
   in order to make it easier to use nested iterpools. Note that this now loops
   over all copies in a revision, not just those with copyfrom paths matching
   the deleted path being searched.
  (find_moves_in_revision): Expect repository UUID and client context params.
   With most code migrated elsewhere what remains amounts to a small loop
   around match_copies_to_deletion().
  (find_moves): Update caller.

* subversion/tests/libsvn_client/conflicts-test.c
  (A_branch2): New constant. 
  (create_wc_with_incoming_delete_dir_conflict_across_branches,
   test_merge_incoming_move_dir_across_branches): New test which runs a
    basic exercise for the new code.
  (test_funcs): Add new test.

Modified:
    subversion/trunk/subversion/libsvn_client/conflicts.c
    subversion/trunk/subversion/tests/libsvn_client/conflicts-test.c

Modified: subversion/trunk/subversion/libsvn_client/conflicts.c
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_client/conflicts.c?rev=1806831&r1=1806830&r2=1806831&view=diff
==============================================================================
--- subversion/trunk/subversion/libsvn_client/conflicts.c (original)
+++ subversion/trunk/subversion/libsvn_client/conflicts.c Thu Aug 31 17:42:53 2017
@@ -430,6 +430,195 @@ add_new_move(struct repos_move_info **ne
   return SVN_NO_ERROR;
 }
 
+/* Push a MOVE into the MOVES_TABLE. */
+static void
+push_move(struct repos_move_info *move, apr_hash_t *moves_table,
+          apr_pool_t *result_pool)
+{
+  apr_array_header_t *moves;
+
+  /* Add this move to the list of moves in the revision. */
+  moves = apr_hash_get(moves_table, &move->rev, sizeof(svn_revnum_t));
+  if (moves == NULL)
+    {
+      /* It is the first move in this revision. Create the list. */
+      moves = apr_array_make(result_pool, 1, sizeof(struct repos_move_info *));
+      apr_hash_set(moves_table, &move->rev, sizeof(svn_revnum_t), moves);
+    }
+  APR_ARRAY_PUSH(moves, struct repos_move_info *) = move;
+}
+
+/* Find the youngest common ancestor of REPOS_RELPATH1@PEG_REV1 and
+ * REPOS_RELPATH2@PEG_REV2. Return the result in *YCA_LOC.
+ * Set *YCA_LOC to NULL if no common ancestor exists. */
+static svn_error_t *
+find_yca(svn_client__pathrev_t **yca_loc,
+         const char *repos_relpath1,
+         svn_revnum_t peg_rev1,
+         const char *repos_relpath2,
+         svn_revnum_t peg_rev2,
+         const char *repos_root_url,
+         const char *repos_uuid,
+         svn_ra_session_t *ra_session,
+         svn_client_ctx_t *ctx,
+         apr_pool_t *result_pool,
+         apr_pool_t *scratch_pool)
+{
+  svn_client__pathrev_t *loc1;
+  svn_client__pathrev_t *loc2;
+
+  *yca_loc = NULL;
+
+  loc1 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid,
+                                                 peg_rev1, repos_relpath1,
+                                                 scratch_pool);
+  loc2 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid,
+                                                 peg_rev2, repos_relpath2,
+                                                 scratch_pool);
+  SVN_ERR(svn_client__get_youngest_common_ancestor(yca_loc, loc1, loc2,
+                                                   ra_session, ctx,
+                                                   result_pool, scratch_pool));
+
+  return SVN_NO_ERROR;
+}
+
+/* Check if the copied node described by COPY and the DELETED_PATH@DELETED_REV
+ * share a common ancestor. If so, return new repos_move_info in *MOVE which
+ * describes a move from the deleted path to that copy's destination. */
+static svn_error_t *
+find_related_move(struct repos_move_info **move,
+                  struct copy_info *copy,
+                  const char *deleted_repos_relpath,
+                  svn_revnum_t deleted_rev,
+                  const char *author,
+                  apr_hash_t *moved_paths,
+                  const char *repos_root_url,
+                  const char *repos_uuid,
+                  svn_client_ctx_t *ctx,
+                  svn_ra_session_t *ra_session,
+                  apr_pool_t *result_pool,
+                  apr_pool_t *scratch_pool)
+{
+  svn_client__pathrev_t *yca_loc;
+  svn_error_t *err;
+
+  *move = NULL;
+  err = find_yca(&yca_loc, copy->copyfrom_path, copy->copyfrom_rev,
+                 deleted_repos_relpath, rev_below(deleted_rev),
+                 repos_root_url, repos_uuid, ra_session, ctx,
+                 scratch_pool, scratch_pool);
+  if (err)
+    {
+      if (err->apr_err == SVN_ERR_FS_NOT_FOUND)
+        {
+          svn_error_clear(err);
+          yca_loc = NULL;
+        }
+      else
+        return svn_error_trace(err);
+    }
+
+  if (yca_loc)
+    SVN_ERR(add_new_move(move, deleted_repos_relpath,
+                         copy->copyto_path, copy->copyfrom_rev,
+                         deleted_rev, author,
+                         moved_paths, ra_session, repos_root_url,
+                         result_pool, scratch_pool));
+
+  return SVN_NO_ERROR;
+}
+
+/* Detect moves by matching DELETED_REPOS_RELPATH@DELETED_REV to the copies
+ * in COPIES. Add any moves found to MOVES_TABLE and update MOVED_PATHS. */
+static svn_error_t *
+match_copies_to_deletion(const char *deleted_repos_relpath,
+                         svn_revnum_t deleted_rev,
+                         const char *author,
+                         apr_hash_t *copies,
+                         apr_hash_t *moves_table,
+                         apr_hash_t *moved_paths,
+                         const char *repos_root_url,
+                         const char *repos_uuid,
+                         svn_ra_session_t *ra_session,
+                         svn_client_ctx_t *ctx,
+                         apr_pool_t *result_pool,
+                         apr_pool_t *scratch_pool)
+{
+  apr_hash_index_t *hi;
+  apr_pool_t *iterpool;
+
+  iterpool = svn_pool_create(scratch_pool);
+  for (hi = apr_hash_first(scratch_pool, copies);
+       hi != NULL;
+       hi = apr_hash_next(hi))
+    {
+      const char *copyfrom_path = apr_hash_this_key(hi);
+      apr_array_header_t *copies_with_same_source_path;
+      int i;
+
+      svn_pool_clear(iterpool);
+
+      copies_with_same_source_path = apr_hash_this_val(hi);
+
+      if (strcmp(copyfrom_path, deleted_repos_relpath) == 0)
+        {
+          /* We found a copyfrom path which matches a deleted node.
+           * Check if the deleted node is an ancestor of the copied node. */
+          for (i = 0; i < copies_with_same_source_path->nelts; i++)
+            {
+              struct copy_info *copy;
+              svn_boolean_t related;
+              struct repos_move_info *move;
+
+              copy = APR_ARRAY_IDX(copies_with_same_source_path, i,
+                                   struct copy_info *);
+              SVN_ERR(check_move_ancestry(&related,
+                                          ra_session, repos_root_url,
+                                          deleted_repos_relpath,
+                                          deleted_rev,
+                                          copy->copyfrom_path,
+                                          copy->copyfrom_rev,
+                                          TRUE, iterpool));
+              if (!related)
+                continue;
+              
+              /* Remember details of this move. */
+              SVN_ERR(add_new_move(&move, deleted_repos_relpath,
+                                   copy->copyto_path, copy->copyfrom_rev,
+                                   deleted_rev, author,
+                                   moved_paths, ra_session, repos_root_url,
+                                   result_pool, iterpool));
+              push_move(move, moves_table, result_pool);
+            } 
+        }
+      else
+        {
+          /* Check if this deleted node is related to any copies in this
+           * revision. These could be moves of the deleted node which
+           * were merged here from other lines of history. */
+          for (i = 0; i < copies_with_same_source_path->nelts; i++)
+            {
+              struct copy_info *copy;
+              struct repos_move_info *move = NULL;
+
+              copy = APR_ARRAY_IDX(copies_with_same_source_path, i,
+                                   struct copy_info *);
+              SVN_ERR(find_related_move(&move, copy, deleted_repos_relpath,
+                                        deleted_rev, author,
+                                        moved_paths,
+                                        repos_root_url, repos_uuid,
+                                        ctx, ra_session,
+                                        result_pool, iterpool));
+              if (move)
+                push_move(move, moves_table, result_pool);
+            }
+        }
+    }
+  svn_pool_destroy(iterpool);
+
+  return SVN_NO_ERROR;
+}
+
 /* Update MOVES_TABLE and MOVED_PATHS based on information from
  * revision data in LOG_ENTRY, COPIES, and DELETED_PATHS.
  * Use RA_SESSION to perform the necessary requests. */
@@ -441,73 +630,31 @@ find_moves_in_revision(svn_ra_session_t
                        apr_hash_t *copies,
                        apr_array_header_t *deleted_paths,
                        const char *repos_root_url,
+                       const char *repos_uuid,
+                       svn_client_ctx_t *ctx,
                        apr_pool_t *result_pool,
                        apr_pool_t *scratch_pool)
 {
   apr_pool_t *iterpool;
-  svn_boolean_t related;
   int i;
+  const svn_string_t *author;
 
+  author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR);
   iterpool = svn_pool_create(scratch_pool);
   for (i = 0; i < deleted_paths->nelts; i++)
     {
       const char *deleted_repos_relpath;
-      struct repos_move_info *move;
-      apr_array_header_t *moves;
-      apr_array_header_t *copies_with_same_source_path;
-      int j;
 
       svn_pool_clear(iterpool);
 
       deleted_repos_relpath = APR_ARRAY_IDX(deleted_paths, i, const char *);
-
-      copies_with_same_source_path = svn_hash_gets(copies,
-                                                   deleted_repos_relpath);
-      if (copies_with_same_source_path == NULL)
-        continue; /* Not a move, or a nested move we handle later on. */
-
-      for (j = 0; j < copies_with_same_source_path->nelts; j++)
-        {
-          struct copy_info *copy;
-
-          /* We found a copy with a copyfrom path which matches a deleted node.
-           * Verify that the deleted node is an ancestor of the copied node. */
-          copy = APR_ARRAY_IDX(copies_with_same_source_path, j,
-                               struct copy_info *);
-          SVN_ERR(check_move_ancestry(&related, ra_session, repos_root_url,
-                                      deleted_repos_relpath,
-                                      log_entry->revision,
-                                      copy->copyfrom_path,
-                                      copy->copyfrom_rev,
-                                      TRUE, iterpool));
-          if (related)
-            {
-              const svn_string_t *author;
-              
-              author = svn_hash_gets(log_entry->revprops,
-                                     SVN_PROP_REVISION_AUTHOR);
-              /* Remember details of this move. */
-              SVN_ERR(add_new_move(&move, deleted_repos_relpath,
-                                   copy->copyto_path, copy->copyfrom_rev,
-                                   log_entry->revision,
-                                   author ? author->data : _("unknown author"),
-                                   moved_paths, ra_session, repos_root_url,
-                                   result_pool, iterpool));
-
-              /* Add this move to the list of moves in this revision. */
-              moves = apr_hash_get(moves_table, &move->rev,
-                                   sizeof(svn_revnum_t));
-              if (moves == NULL)
-                {
-                  /* It is the first move in this revision. Create the list. */
-                  moves = apr_array_make(result_pool, 1,
-                                         sizeof(struct repos_move_info *));
-                  apr_hash_set(moves_table, &move->rev, sizeof(svn_revnum_t),
-                               moves);
-                }
-              APR_ARRAY_PUSH(moves, struct repos_move_info *) = move;
-            }
-        }
+      SVN_ERR(match_copies_to_deletion(deleted_repos_relpath,
+                                       log_entry->revision,
+                                       author ? author->data
+                                              : _("unknown author"),
+                                       copies, moves_table, moved_paths,
+                                       repos_root_url, repos_uuid, ra_session,
+                                       ctx, result_pool, iterpool));
     }
   svn_pool_destroy(iterpool);
 
@@ -539,40 +686,6 @@ struct find_deleted_rev_baton
   svn_ra_session_t *extra_ra_session;
 };
 
-/* Find the youngest common ancestor of REPOS_RELPATH1@PEG_REV1 and
- * REPOS_RELPATH2@PEG_REV2. Return the result in *YCA_LOC.
- * Set *YCA_LOC to NULL if no common ancestor exists. */
-static svn_error_t *
-find_yca(svn_client__pathrev_t **yca_loc,
-         const char *repos_relpath1,
-         svn_revnum_t peg_rev1,
-         const char *repos_relpath2,
-         svn_revnum_t peg_rev2,
-         const char *repos_root_url,
-         const char *repos_uuid,
-         svn_ra_session_t *ra_session,
-         svn_client_ctx_t *ctx,
-         apr_pool_t *result_pool,
-         apr_pool_t *scratch_pool)
-{
-  svn_client__pathrev_t *loc1;
-  svn_client__pathrev_t *loc2;
-
-  *yca_loc = NULL;
-
-  loc1 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid,
-                                                 peg_rev1, repos_relpath1,
-                                                 scratch_pool);
-  loc2 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid,
-                                                 peg_rev2, repos_relpath2,
-                                                 scratch_pool);
-  SVN_ERR(svn_client__get_youngest_common_ancestor(yca_loc, loc1, loc2,
-                                                   ra_session, ctx,
-                                                   result_pool, scratch_pool));
-
-  return SVN_NO_ERROR;
-}
-
 /* If DELETED_RELPATH matches the moved-from path of a move in MOVES,
  * or if DELETED_RELPATH is a child of a moved-to path in MOVES, return
  * a struct move_info for the corresponding move. Else, return NULL. */
@@ -1478,8 +1591,8 @@ find_moves(void *baton, svn_log_entry_t
   SVN_ERR(find_moves_in_revision(b->extra_ra_session,
                                  b->moves_table, b->moved_paths,
                                  log_entry, copies, deleted_paths,
-                                 b->repos_root_url,
-                                 b->result_pool, scratch_pool));
+                                 b->repos_root_url, b->repos_uuid,
+                                 b->ctx, b->result_pool, scratch_pool));
 
   moves = apr_hash_get(b->moves_table, &log_entry->revision,
                        sizeof(svn_revnum_t));

Modified: subversion/trunk/subversion/tests/libsvn_client/conflicts-test.c
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/libsvn_client/conflicts-test.c?rev=1806831&r1=1806830&r2=1806831&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/libsvn_client/conflicts-test.c (original)
+++ subversion/trunk/subversion/tests/libsvn_client/conflicts-test.c Thu Aug 31 17:42:53 2017
@@ -162,6 +162,7 @@ assert_text_conflict_options(svn_client_
 /* Some paths we'll care about. */
 static const char *trunk_path = "A";
 static const char *branch_path = "A_branch";
+static const char *branch2_path = "A_branch2";
 static const char *new_file_name = "newfile.txt";
 static const char *new_file_name_branch = "newfile-on-branch.txt";
 static const char *deleted_file_name = "mu";
@@ -4976,6 +4977,190 @@ test_cherry_pick_post_move_edit(const sv
   return SVN_NO_ERROR;
 }
 
+/* A helper function which prepares a working copy for the tests below. */
+static svn_error_t *
+create_wc_with_incoming_delete_dir_conflict_across_branches(
+  svn_test__sandbox_t *b)
+{
+  svn_client_ctx_t *ctx;
+  const char *trunk_url;
+  const char *branch_url;
+  svn_opt_revision_t opt_rev;
+  const char *deleted_path;
+  const char *deleted_child_path;
+  const char *move_target_path;
+
+  SVN_ERR(sbox_add_and_commit_greek_tree(b));
+
+  /* Create a branch of node "A". */
+  SVN_ERR(sbox_wc_copy(b, trunk_path, branch_path));
+  SVN_ERR(sbox_wc_commit(b, ""));
+
+  /* Create a second branch ("branch2") of the first branch. */
+  SVN_ERR(sbox_wc_copy(b, branch_path, branch2_path));
+  SVN_ERR(sbox_wc_commit(b, ""));
+
+  /* Move a directory on the trunk. */
+  deleted_path = svn_relpath_join(trunk_path, deleted_dir_name, b->pool);
+  move_target_path = svn_relpath_join(trunk_path, new_dir_name, b->pool);
+  SVN_ERR(sbox_wc_move(b, deleted_path, move_target_path));
+  SVN_ERR(sbox_wc_commit(b, ""));
+
+  /* Modify a file in that directory on branch2. */
+  deleted_child_path = svn_relpath_join(branch2_path,
+                                        svn_relpath_join(deleted_dir_name,
+                                                         deleted_dir_child,
+                                                         b->pool),
+                                        b->pool);
+  SVN_ERR(sbox_file_write(b, deleted_child_path,
+                          modified_file_on_branch_content));
+
+  SVN_ERR(svn_test__create_client_ctx(&ctx, b, b->pool));
+  opt_rev.kind = svn_opt_revision_head;
+  opt_rev.value.number = SVN_INVALID_REVNUM;
+  trunk_url = apr_pstrcat(b->pool, b->repos_url, "/", trunk_path,
+                          SVN_VA_NULL);
+  branch_url = apr_pstrcat(b->pool, b->repos_url, "/", branch_path,
+                          SVN_VA_NULL);
+
+  /* Commit modification and run a merge from the trunk to the branch.
+   * This merge should not raise a conflict. */
+  SVN_ERR(sbox_wc_commit(b, ""));
+  SVN_ERR(sbox_wc_update(b, "", SVN_INVALID_REVNUM));
+  SVN_ERR(svn_client_merge_peg5(trunk_url, NULL, &opt_rev,
+                                sbox_wc_path(b, branch_path),
+                                svn_depth_infinity,
+                                FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
+                                NULL, ctx, b->pool));
+
+  /* Commit merge result end run a merge from branch to branch2. */
+  SVN_ERR(sbox_wc_commit(b, ""));
+  SVN_ERR(sbox_wc_update(b, "", SVN_INVALID_REVNUM));
+
+  /* This should raise an "incoming delete vs local edit" tree conflict. */
+  SVN_ERR(svn_client_merge_peg5(branch_url, NULL, &opt_rev,
+                                sbox_wc_path(b, branch2_path),
+                                svn_depth_infinity,
+                                FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
+                                NULL, ctx, b->pool));
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+test_merge_incoming_move_dir_across_branches(const svn_test_opts_t *opts,
+                                             apr_pool_t *pool)
+{
+  svn_test__sandbox_t *b = apr_palloc(pool, sizeof(*b));
+  svn_client_ctx_t *ctx;
+  const char *deleted_path;
+  const char *moved_to_path;
+  const char *child_path;
+  svn_client_conflict_t *conflict;
+  struct status_baton sb;
+  struct svn_client_status_t *status;
+  svn_stringbuf_t *buf;
+  svn_opt_revision_t opt_rev;
+  apr_array_header_t *options;
+  svn_client_conflict_option_t *option;
+  apr_array_header_t *possible_moved_to_abspaths;
+
+  SVN_ERR(svn_test__sandbox_create(b,
+                                   "merge_incoming_move_dir accross branches",
+                                   opts, pool));
+
+  SVN_ERR(create_wc_with_incoming_delete_dir_conflict_across_branches(b));
+
+  deleted_path = svn_relpath_join(branch2_path, deleted_dir_name, b->pool);
+  moved_to_path = svn_relpath_join(branch2_path, new_dir_name, b->pool);
+
+  SVN_ERR(svn_test__create_client_ctx(&ctx, b, b->pool));
+  SVN_ERR(svn_client_conflict_get(&conflict, sbox_wc_path(b, deleted_path),
+                                  ctx, b->pool, b->pool));
+  SVN_ERR(svn_client_conflict_tree_get_details(conflict, ctx, b->pool));
+
+  /* Check possible move destinations for the directory. */
+  SVN_ERR(svn_client_conflict_tree_get_resolution_options(&options, conflict,
+                                                          ctx, b->pool,
+                                                          b->pool));
+  option = svn_client_conflict_option_find_by_id(
+             options, svn_client_conflict_option_incoming_move_dir_merge);
+  SVN_TEST_ASSERT(option != NULL);
+
+  SVN_ERR(svn_client_conflict_option_get_moved_to_abspath_candidates(
+            &possible_moved_to_abspaths, option, b->pool, b->pool));
+
+  /* The resolver finds two possible destinations for the moved folder:
+   *
+   *   Possible working copy destinations for moved-away 'A_branch/B' are:
+   *    (1): 'A_branch2/newdir'
+   *    (2): 'A_branch/newdir'
+   *   Only one destination can be a move; the others are copies.
+   */
+  SVN_TEST_INT_ASSERT(possible_moved_to_abspaths->nelts, 2);
+  SVN_TEST_STRING_ASSERT(
+    APR_ARRAY_IDX(possible_moved_to_abspaths, 0, const char *),
+    sbox_wc_path(b, moved_to_path));
+  SVN_TEST_STRING_ASSERT(
+    APR_ARRAY_IDX(possible_moved_to_abspaths, 1, const char *),
+    sbox_wc_path(b, svn_relpath_join(branch_path, new_dir_name, b->pool)));
+
+  /* Resolve the tree conflict. */
+  SVN_ERR(svn_client_conflict_option_set_moved_to_abspath(option, 0,
+                                                          ctx, b->pool));
+  SVN_ERR(svn_client_conflict_tree_resolve(conflict, option, ctx, b->pool));
+
+  /* Ensure that the moved-away directory has the expected status. */
+  sb.result_pool = b->pool;
+  opt_rev.kind = svn_opt_revision_working;
+  SVN_ERR(svn_client_status6(NULL, ctx, sbox_wc_path(b, deleted_path),
+                             &opt_rev, svn_depth_empty, TRUE, TRUE,
+                             TRUE, TRUE, FALSE, TRUE, NULL,
+                             status_func, &sb, b->pool));
+  status = sb.status;
+  SVN_TEST_ASSERT(status->kind == svn_node_dir);
+  SVN_TEST_ASSERT(status->versioned);
+  SVN_TEST_ASSERT(!status->conflicted);
+  SVN_TEST_ASSERT(status->node_status == svn_wc_status_deleted);
+  SVN_TEST_ASSERT(status->text_status == svn_wc_status_normal);
+  SVN_TEST_ASSERT(status->prop_status == svn_wc_status_none);
+  SVN_TEST_ASSERT(!status->copied);
+  SVN_TEST_ASSERT(!status->switched);
+  SVN_TEST_ASSERT(!status->file_external);
+  SVN_TEST_ASSERT(status->moved_from_abspath == NULL);
+  SVN_TEST_STRING_ASSERT(status->moved_to_abspath,
+                         sbox_wc_path(b, moved_to_path));
+
+  /* Ensure that the moved-here directory has the expected status. */
+  sb.result_pool = b->pool;
+  opt_rev.kind = svn_opt_revision_working;
+  SVN_ERR(svn_client_status6(NULL, ctx, sbox_wc_path(b, moved_to_path),
+                             &opt_rev, svn_depth_empty, TRUE, TRUE,
+                             TRUE, TRUE, FALSE, TRUE, NULL,
+                             status_func, &sb, b->pool));
+  status = sb.status;
+  SVN_TEST_ASSERT(status->kind == svn_node_dir);
+  SVN_TEST_ASSERT(status->versioned);
+  SVN_TEST_ASSERT(!status->conflicted);
+  SVN_TEST_ASSERT(status->node_status == svn_wc_status_added);
+  SVN_TEST_ASSERT(status->text_status == svn_wc_status_normal);
+  SVN_TEST_ASSERT(status->prop_status == svn_wc_status_none);
+  SVN_TEST_ASSERT(status->copied);
+  SVN_TEST_ASSERT(!status->switched);
+  SVN_TEST_ASSERT(!status->file_external);
+  SVN_TEST_STRING_ASSERT(status->moved_from_abspath,
+                         sbox_wc_path(b, deleted_path));
+  SVN_TEST_ASSERT(status->moved_to_abspath == NULL);
+
+  /* Ensure that the edited file has the expected content. */
+  child_path = svn_relpath_join(moved_to_path, deleted_dir_child,
+                                b->pool);
+  SVN_ERR(svn_stringbuf_from_file2(&buf, sbox_wc_path(b, child_path),
+                                   b->pool));
+  SVN_TEST_STRING_ASSERT(buf->data, modified_file_on_branch_content);
+
+  return SVN_NO_ERROR;
+}
+
 /* ========================================================================== */
 
 
@@ -5064,6 +5249,8 @@ static struct svn_test_descriptor_t test
                        "merge incoming move file merge with native eols"),
     SVN_TEST_OPTS_XFAIL(test_cherry_pick_post_move_edit,
                         "cherry-pick edit from moved file"),
+    SVN_TEST_OPTS_PASS(test_merge_incoming_move_dir_across_branches,
+                        "merge incoming dir move across branches"),
     SVN_TEST_NULL
   };