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 2016/06/05 15:03:54 UTC

svn commit: r1746927 [3/8] - in /subversion/branches/authzperf: ./ build/ contrib/client-side/ contrib/client-side/svnmerge/ contrib/hook-scripts/ contrib/server-side/ contrib/server-side/fsfsfixer/fixer/ notes/directory-index/ notes/move-tracking/ sub...

Modified: subversion/branches/authzperf/subversion/libsvn_client/conflicts.c
URL: http://svn.apache.org/viewvc/subversion/branches/authzperf/subversion/libsvn_client/conflicts.c?rev=1746927&r1=1746926&r2=1746927&view=diff
==============================================================================
--- subversion/branches/authzperf/subversion/libsvn_client/conflicts.c (original)
+++ subversion/branches/authzperf/subversion/libsvn_client/conflicts.c Sun Jun  5 15:03:52 2016
@@ -234,26 +234,278 @@ static const svn_token_map_t map_conflic
   { NULL,               0 }
 };
 
+/* Describes a server-side move (really a copy+delete within the same
+ * revision) which was identified by scanning the revision log. */
+struct repos_move_info {
+  /* The repository relpath the node was moved from. */
+  const char *moved_from_repos_relpath;
+
+  /* The repository relpath the node was moved to. */
+  const char *moved_to_repos_relpath;
+ 
+  /* The revision in which this move was committed. */
+  svn_revnum_t rev;
+
+  /* The author who commited the revision in which this move was committed. */
+  const char *rev_author;
+
+  /* The copyfrom revision of the moved-to path. */
+  svn_revnum_t copyfrom_rev;
+
+  /* Prev and next pointers. NULL if no prior or next move exists. */
+  struct repos_move_info *prev;
+  struct repos_move_info *next;
+};
+
+/* Set *RELATED to true if the deleted node at repository relpath
+ * DELETED_REPOS_RELPATH@DELETED_REV is ancestrally related to the node at
+ * repository relpath COPYFROM_PATH@COPYFROM_REV, else set it to false. */
+static svn_error_t *
+check_move_ancestry(svn_boolean_t *related,
+                    const char *repos_root_url,
+                    const char *deleted_repos_relpath,
+                    svn_revnum_t deleted_rev,
+                    const char *copyfrom_path,
+                    svn_revnum_t copyfrom_rev,
+                    svn_client_ctx_t *ctx,
+                    apr_pool_t *scratch_pool)
+{
+  apr_hash_t *locations;
+  const char *deleted_url;
+  const char *deleted_location;
+  svn_ra_session_t *ra_session;
+  const char *corrected_url;
+  apr_array_header_t *location_revisions;
+
+  *related = FALSE;
+
+  location_revisions = apr_array_make(scratch_pool, 1, sizeof(svn_revnum_t));
+  APR_ARRAY_PUSH(location_revisions, svn_revnum_t) = copyfrom_rev;
+  deleted_url = svn_uri_canonicalize(apr_pstrcat(scratch_pool,
+                                                 repos_root_url, "/",
+                                                 deleted_repos_relpath,
+                                                 NULL),
+                                     scratch_pool);
+  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
+                                               deleted_url, NULL,
+                                               NULL, FALSE, FALSE,
+                                               ctx, scratch_pool,
+                                               scratch_pool));
+  SVN_ERR(svn_ra_get_locations(ra_session, &locations, "",
+                               deleted_rev - 1, location_revisions,
+                               scratch_pool));
+
+  deleted_location = apr_hash_get(locations, &copyfrom_rev,
+                                  sizeof(svn_revnum_t));
+  if (deleted_location)
+    {
+      if (deleted_location[0] == '/')
+        deleted_location++;
+      *related = (strcmp(deleted_location, copyfrom_path) == 0);
+    }
+
+  return SVN_NO_ERROR;
+}
+
+struct copy_info {
+  const char *copyto_path;
+  const char *copyfrom_path;
+  svn_revnum_t copyfrom_rev;
+};
+
+/* Update MOVES_TABLE and MOVED_PATHS based on information from
+ * revision data in LOG_ENTRY, COPIES, and DELETED_PATHS. */
+static svn_error_t *
+find_moves_in_revision(apr_hash_t *moves_table,
+                       apr_hash_t *moved_paths,
+                       svn_log_entry_t *log_entry,
+                       apr_hash_t *copies,
+                       apr_array_header_t *deleted_paths,
+                       const char *repos_root_url,
+                       svn_client_ctx_t *ctx,
+                       apr_pool_t *result_pool,
+                       apr_pool_t *scratch_pool)
+{
+  apr_pool_t *iterpool;
+  apr_array_header_t *copies_with_same_source_path;
+  svn_boolean_t related;
+  int i, j;
+
+  iterpool = svn_pool_create(scratch_pool);
+  for (i = 0; i < deleted_paths->nelts; i++)
+    {
+      const char *deleted_repos_relpath;
+
+      deleted_repos_relpath = APR_ARRAY_IDX(deleted_paths, i, const char *);
+
+      /* See if we can match any copies to this deleted path. */
+      copies_with_same_source_path = apr_hash_get(copies,
+                                                  deleted_repos_relpath,
+                                                  APR_HASH_KEY_STRING);
+      if (copies_with_same_source_path == NULL)
+        continue;
+
+      for (j = 0; j < copies_with_same_source_path->nelts; j++)
+        {
+          struct copy_info *copy;
+          struct repos_move_info *move;
+          struct repos_move_info *next_move;
+          svn_string_t *author;
+          apr_array_header_t *moves;
+          
+          svn_pool_clear(iterpool);
+
+          copy = APR_ARRAY_IDX(copies_with_same_source_path, j,
+                               struct copy_info *);
+
+          /* We found a deleted node which matches the copyfrom path of
+           * a copied node. Verify that the deleted node is an ancestor
+           * of the copied node. When tracing back history of the deleted node
+           * from revision log_entry->revision-1 (where the deleted node is
+           * guaranteed to exist) to the copyfrom-revision, we must end up
+           * at the copyfrom-path. */
+          SVN_ERR(check_move_ancestry(&related, repos_root_url,
+                                      deleted_repos_relpath,
+                                      log_entry->revision,
+                                      copy->copyfrom_path,
+                                      copy->copyfrom_rev,
+                                      ctx, iterpool));
+          if (!related)
+            continue;
+
+          /* ### TODO:
+           * If the node was not copied from the most recent last-changed
+           * revision of the deleted node, this is not a move but a
+           * "copy from the past + delete". */
+
+          /* Remember details of this move. */
+          move = apr_pcalloc(result_pool, sizeof(*move));
+          move->moved_from_repos_relpath = apr_pstrdup(result_pool,
+                                                       deleted_repos_relpath);
+          move->moved_to_repos_relpath = apr_pstrdup(result_pool,
+                                                     copy->copyto_path);
+          move->rev = log_entry->revision;
+          author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR);
+          move->rev_author = apr_pstrdup(result_pool, author->data);
+          move->copyfrom_rev = copy->copyfrom_rev;
+
+          /* Link together multiple moves of the same node.
+           * Note that we're traversing history backwards, so moves already
+           * present in the list happened in younger revisions. */
+          next_move = svn_hash_gets(moved_paths, move->moved_to_repos_relpath);
+          if (next_move)
+            {
+              /* Tracing back history of the delete-half of the next move
+               * to the copyfrom-revision of the prior move we must end up
+               * at the delete-half of the prior move. */
+              SVN_ERR(check_move_ancestry(&related, repos_root_url,
+                                          next_move->moved_from_repos_relpath,
+                                          next_move->rev,
+                                          move->moved_from_repos_relpath,
+                                          move->copyfrom_rev,
+                                          ctx, iterpool));
+              if (related)
+                {
+                  SVN_ERR_ASSERT(move->rev < next_move->rev);
+
+                  /* Prepend this move to the linked list. */
+                  move->next = next_move;
+                  next_move->prev = move;
+                }
+            }
+
+          /* Make this move the head of our next-move linking map. */
+          svn_hash_sets(moved_paths, move->moved_from_repos_relpath, move);
+
+          /* 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)
+            {
+              /* This 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_pool_destroy(iterpool);
+
+  return SVN_NO_ERROR;
+}
+
 struct find_deleted_rev_baton
 {
+  /* Variables below are arguments provided by the caller of
+   * svn_ra_get_log2(). */
   const char *deleted_repos_relpath;
   const char *related_repos_relpath;
   svn_revnum_t related_repos_peg_rev;
+  const char *repos_root_url;
+  const char *repos_uuid;
+  svn_client_ctx_t *ctx;
 
+  /* Variables below are results for the caller of svn_ra_get_log2(). */
   svn_revnum_t deleted_rev;
   const char *deleted_rev_author;
   svn_node_kind_t replacing_node_kind;
-
-  const char *repos_root_url;
-  const char *repos_uuid;
-  svn_client_ctx_t *ctx;
   apr_pool_t *result_pool;
+
+  /* A hash table mapping a revision number to an array of struct
+   * repos_move_info * elements, describing moves.
+   *
+   * Must be allocated in RESULT_POOL by the caller of svn_ra_get_log2().
+   *
+   * If the node was moved, the DELETED_REV is present in this table,
+   * perhaps along with additional revisions.
+   *
+   * Given a sequence of moves which happened in the repository, such as:
+   *   rA: mv x->z
+   *   rA: mv a->b
+   *   rB: mv b->c
+   *   rC: mv c->d
+   * we map each revision number to all the moves which happened in the
+   * revision, which looks as follows: 
+   *   rA : [(x->z), (a->b)]
+   *   rB : [(b->c)]
+   *   rC : [(c->d)]
+   * This allows us to later find relevant moves based on a revision number.
+   *
+   * Additionally, we embed the number of the revision in which a move was
+   * found inside the repos_move_info structure:
+   *   rA : [(rA, x->z), (rA, a->b)]
+   *   rB : [(rB, b->c)]
+   *   rC : [(rC, c->d)]
+   * And also, all moves pertaining to the same node are chained into a
+   * doubly-linked list via 'next' and 'prev' pointers (see definition of
+   * struct repos_move_info). This can be visualized as follows:
+   *   rA : [(rA, x->z, prev=>NULL, next=>NULL),
+   *         (rA, a->b, prev=>NULL, next=>(rB, b->c))]
+   *   rB : [(rB, b->c), prev=>(rA, a->b), next=>(rC, c->d)]
+   *   rC : [(rC, c->d), prev=>(rB, c->d), next=>NULL]
+   * This way, we can look up all moves relevant to a node, forwards and
+   * backwards in history, once we have located one move in the chain.
+   *
+   * In the above example, the data tells us that within the revision
+   * range rA:C, a was moved to d. However, within the revision range
+   * rA;B, a was moved to b.
+   */
+  apr_hash_t *moves_table;
+
+  /* Variables below hold state for find_deleted_rev() and are not
+   * intended to be used by the caller of svn_ra_get_log2().
+   * Like all other variables, they must be initialized, however. */
+
+  /* Temporary map of moved paths to struct repos_move_info.
+   * Used to link multiple moves of the same node across revisions. */
+  apr_hash_t *moved_paths;
 };
 
 /* Implements svn_log_entry_receiver_t.
  *
- * Find the revision in which a node, ancestrally related to the node
- * specified via find_deleted_rev_baton, was deleted, When the revision
+ * Find the revision in which a node, optionally ancestrally related to the
+ * node specified via find_deleted_rev_baton, was deleted, When the revision
  * was found, store it in BATON->DELETED_REV and abort the log operation
  * by raising SVN_ERR_CANCELLED.
  *
@@ -266,7 +518,11 @@ struct find_deleted_rev_baton
  *
  * This function answers the same question as svn_ra_get_deleted_rev() but
  * works in cases where we do not already know a revision in which the deleted
- * node once used to exist. */
+ * node once used to exist.
+ * 
+ * If the node node was moved, rather than deleted, return move information
+ * in BATON->MOVE.
+ */
 static svn_error_t *
 find_deleted_rev(void *baton,
                  svn_log_entry_t *log_entry,
@@ -275,57 +531,110 @@ find_deleted_rev(void *baton,
   struct find_deleted_rev_baton *b = baton;
   apr_hash_index_t *hi;
   apr_pool_t *iterpool;
+  svn_boolean_t deleted_node_found = FALSE;
+  apr_array_header_t *deleted_paths;
+  apr_hash_t *copies;
 
   /* No paths were changed in this revision.  Nothing to do. */
   if (! log_entry->changed_paths2)
     return SVN_NO_ERROR;
 
+  copies = apr_hash_make(scratch_pool);
+  deleted_paths = apr_array_make(scratch_pool, 0, sizeof(const char *));
   iterpool = svn_pool_create(scratch_pool);
   for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2);
        hi != NULL;
        hi = apr_hash_next(hi))
     {
-      void *val;
-      const char *path;
-      svn_log_changed_path2_t *log_item;
+      const char *changed_path = apr_hash_this_key(hi);
+      svn_log_changed_path2_t *log_item = apr_hash_this_val(hi);
 
       svn_pool_clear(iterpool);
 
+      /* ### Remove leading slash from paths in log entries. */
+      if (changed_path[0] == '/')
+          changed_path++;
 
-      apr_hash_this(hi, (void *) &path, NULL, &val);
-      log_item = val;
+      /* For move detection, scan for copied nodes in this revision. */
+      if (log_item->action == 'A' && log_item->copyfrom_path)
+        {
+          struct copy_info *copy;
+          apr_array_header_t *copies_with_same_source_path;
 
-      /* ### Remove leading slash from paths in log entries. */
-      if (path[0] == '/')
-          path = svn_relpath_canonicalize(path, iterpool);
+          if (log_item->copyfrom_path[0] == '/')
+            log_item->copyfrom_path++;
+
+          copy = apr_palloc(scratch_pool, sizeof(*copy));
+          copy->copyto_path = changed_path;
+          copy->copyfrom_path = log_item->copyfrom_path;
+          copy->copyfrom_rev = log_item->copyfrom_rev;
+          copies_with_same_source_path = apr_hash_get(copies,
+                                                      log_item->copyfrom_path,
+                                                      APR_HASH_KEY_STRING);
+          if (copies_with_same_source_path == NULL)
+            {
+              copies_with_same_source_path = apr_array_make(
+                                               scratch_pool, 1,
+                                               sizeof(struct copy_info *));
+              apr_hash_set(copies, copy->copyfrom_path, APR_HASH_KEY_STRING,
+                           copies_with_same_source_path);
+            }
+          APR_ARRAY_PUSH(copies_with_same_source_path,
+                         struct copy_info *) = copy;
+        }
 
-      if (svn_path_compare_paths(b->deleted_repos_relpath, path) == 0
-          && (log_item->action == 'D' || log_item->action == 'R'))
+      /* For move detection, store all deleted_paths.
+       *
+       * ### This also stores deletions which happened inside copies.
+       * ### But we are not able to handle them at present.
+       * ### Consider: cp A B; mv B/foo C/foo
+       * ### Copyfrom for C/foo is now A/foo, even though C/foo was moved
+       * ### here from B/foo. We don't detect such moves at present since
+       * ### A/foo was not deleted. It is B/foo which was deleted.
+       */
+      if (log_item->action == 'D' || log_item->action == 'R')
+        APR_ARRAY_PUSH(deleted_paths, const char *) =
+          apr_pstrdup(scratch_pool, changed_path);
+
+      /* Check if we found the deleted node we're looking for. */
+      if (!deleted_node_found &&
+          svn_path_compare_paths(b->deleted_repos_relpath, changed_path) == 0 &&
+          (log_item->action == 'D' || log_item->action == 'R'))
         {
-          svn_client__pathrev_t *yca_loc;
-          svn_client__pathrev_t *loc1;
-          svn_client__pathrev_t *loc2;
-
-          /* We found a deleted node which occupies the correct path.
-           * To be certain that this is the deleted node we're looking for,
-           * we must establish whether it is ancestrally related to the
-           * "related node" specified in our baton. */
-          loc1 = svn_client__pathrev_create_with_relpath(
-                   b->repos_root_url, b->repos_uuid, b->related_repos_peg_rev,
-                   b->related_repos_relpath, iterpool);
-          loc2 = svn_client__pathrev_create_with_relpath(
-                   b->repos_root_url, b->repos_uuid, log_entry->revision - 1,
-                   b->deleted_repos_relpath, iterpool);
-          SVN_ERR(svn_client__get_youngest_common_ancestor(&yca_loc, loc1, loc2,
-                                                           NULL, b->ctx,
-                                                           iterpool,
-                                                           iterpool));
-          if (yca_loc != NULL)
+          deleted_node_found = TRUE;
+
+          if (b->related_repos_relpath != NULL &&
+              b->related_repos_peg_rev != SVN_INVALID_REVNUM)
+            {
+              svn_client__pathrev_t *yca_loc = NULL;
+              svn_client__pathrev_t *loc1;
+              svn_client__pathrev_t *loc2;
+
+              /* We found a deleted node which occupies the correct path.
+               * To be certain that this is the deleted node we're looking for,
+               * we must establish whether it is ancestrally related to the
+               * "related node" specified in our baton. */
+              loc1 = svn_client__pathrev_create_with_relpath(
+                       b->repos_root_url, b->repos_uuid,
+                       b->related_repos_peg_rev,
+                       b->related_repos_relpath, iterpool);
+              loc2 = svn_client__pathrev_create_with_relpath(
+                       b->repos_root_url, b->repos_uuid,
+                       log_entry->revision - 1,
+                       b->deleted_repos_relpath, iterpool);
+              SVN_ERR(svn_client__get_youngest_common_ancestor(&yca_loc,
+                                                               loc1, loc2,
+                                                               NULL, b->ctx,
+                                                               iterpool,
+                                                               iterpool));
+              deleted_node_found = (yca_loc != NULL);
+            }
+
+          if (deleted_node_found)
             {
               svn_string_t *author;
-              /* Found the correct node, we are done. */
-              b->deleted_rev = log_entry->revision;
 
+              b->deleted_rev = log_entry->revision;
               author = svn_hash_gets(log_entry->revprops,
                                      SVN_PROP_REVISION_AUTHOR);
               b->deleted_rev_author = apr_pstrdup(b->result_pool, author->data);
@@ -334,12 +643,22 @@ find_deleted_rev(void *baton,
                 b->replacing_node_kind = log_item->node_kind;
               else
                 b->replacing_node_kind = svn_node_none;
-              return svn_error_create(SVN_ERR_CANCELLED, NULL, NULL);
             }
         }
     }
   svn_pool_destroy(iterpool);
 
+  /* Check for moves in this revision */
+  SVN_ERR(find_moves_in_revision(b->moves_table, b->moved_paths,
+                                 log_entry, copies, deleted_paths,
+                                 b->repos_root_url, b->ctx,
+                                 b->result_pool, scratch_pool));
+  if (deleted_node_found)
+    {
+      /* We're done. Abort the log operation. */
+      return svn_error_create(SVN_ERR_CANCELLED, NULL, NULL);
+    }
+
   return SVN_NO_ERROR;
 }
 
@@ -776,11 +1095,14 @@ describe_local_dir_node_change(const cha
  * If the node was replaced rather than deleted, set *REPLACING_NODE_KIND to
  * the node kind of the replacing node. Else, set it to svn_node_unknown.
  * Only request the log for revisions up to END_REV from the server.
+ * If the deleted node was moved, provide move information in *MOVE.
+ * If the node was not moved,set *MOVE to NULL.
  */
 static svn_error_t *
 find_revision_for_suspected_deletion(svn_revnum_t *deleted_rev,
                                      const char **deleted_rev_author,
                                      svn_node_kind_t *replacing_node_kind,
+                                     struct repos_move_info **move,
                                      svn_client_conflict_t *conflict,
                                      const char *deleted_basename,
                                      const char *parent_repos_relpath,
@@ -826,7 +1148,9 @@ find_revision_for_suspected_deletion(svn
   b.repos_root_url = repos_root_url;
   b.repos_uuid = repos_uuid;
   b.ctx = conflict->ctx;
-  b.result_pool = scratch_pool;
+  b.moves_table = apr_hash_make(result_pool);
+  b.moved_paths = apr_hash_make(scratch_pool);
+  b.result_pool = result_pool;
 
   err = svn_ra_get_log2(ra_session, paths, start_rev, end_rev,
                         0, /* no limit */
@@ -841,7 +1165,7 @@ find_revision_for_suspected_deletion(svn
       if (err->apr_err == SVN_ERR_CANCELLED &&
           b.deleted_rev != SVN_INVALID_REVNUM)
         {
-          /* Log operation was aborted because we found a YCA. */
+          /* Log operation was aborted because we found deleted rev. */
           svn_error_clear(err);
         }
       else
@@ -855,12 +1179,42 @@ find_revision_for_suspected_deletion(svn
       *deleted_rev = SVN_INVALID_REVNUM;
       *deleted_rev_author = NULL;
       *replacing_node_kind = svn_node_unknown;
+      *move = NULL;
       return SVN_NO_ERROR;
     }
+  else
+    {
+      apr_array_header_t *moves;
+
+      *deleted_rev = b.deleted_rev;
+      *deleted_rev_author = b.deleted_rev_author;
+      *replacing_node_kind = b.replacing_node_kind;
+
+      /* Look for a move which affects the deleted node. */
+      moves = apr_hash_get(b.moves_table, &b.deleted_rev,
+                           sizeof(svn_revnum_t));
+      if (moves)
+        {
+          int i;
 
-  *deleted_rev = b.deleted_rev;
-  *deleted_rev_author = b.deleted_rev_author;
-  *replacing_node_kind = b.replacing_node_kind;
+          for (i = 0; i < moves->nelts; i++)
+            {
+              struct repos_move_info *this_move;
+              
+              this_move = APR_ARRAY_IDX(moves, i, struct repos_move_info *);
+              if (strcmp(b.deleted_repos_relpath,
+                         this_move->moved_from_repos_relpath) == 0)
+                {
+                  /* Because b->moves_table lives in result_pool
+                   * there is no need to deep-copy here. */
+                  *move = this_move;
+                  break;
+                }
+            }
+        }
+      else
+        *move = NULL;
+    }
 
   return SVN_NO_ERROR;
 }
@@ -873,6 +1227,10 @@ struct conflict_tree_local_missing_detai
 
   /* Author who committed DELETED_REV. */
   const char *deleted_rev_author;
+
+  /* Move information. If not NULL, the first move happened in DELETED_REV.
+   * Follow MOVE->NEXT for subsequent moves in later revisions. */
+  struct repos_move_info *move;
 };
 
 /* Implements tree_conflict_get_details_func_t. */
@@ -890,6 +1248,7 @@ conflict_tree_get_details_local_missing(
   svn_node_kind_t replacing_node_kind;
   const char *deleted_basename;
   struct conflict_tree_local_missing_details *details;
+  struct repos_move_info *move;
 
   /* We only handle merges here. */
   if (svn_client_conflict_get_operation(conflict) != svn_wc_operation_merge)
@@ -916,19 +1275,20 @@ conflict_tree_get_details_local_missing(
                                       scratch_pool,
                                       scratch_pool));
   SVN_ERR(find_revision_for_suspected_deletion(
-            &deleted_rev, &deleted_rev_author, &replacing_node_kind,
+            &deleted_rev, &deleted_rev_author, &replacing_node_kind, &move,
             conflict, deleted_basename, parent_repos_relpath,
             old_rev < new_rev ? new_rev : old_rev, 0,
             old_rev < new_rev ? new_repos_relpath : old_repos_relpath,
             old_rev < new_rev ? new_rev : old_rev,
-            scratch_pool, scratch_pool));
+            conflict->pool, scratch_pool));
 
   if (deleted_rev == SVN_INVALID_REVNUM)
     return SVN_NO_ERROR;
 
   details = apr_pcalloc(conflict->pool, sizeof(*details));
   details->deleted_rev = deleted_rev;
-  details->deleted_rev_author = apr_pstrdup(conflict->pool, deleted_rev_author);
+  details->deleted_rev_author = deleted_rev_author;
+  details->move = move;
                                          
   conflict->tree_conflict_local_details = details;
 
@@ -968,10 +1328,13 @@ describe_local_none_node_change(const ch
         *description = _("No such file or directory was found in the "
                          "working copy.");
       else if (operation == svn_wc_operation_merge)
-        *description = _("No such file or directory was found in the "
-                         "merge target working copy.\nThe item may "
-                         "have been deleted or moved away in the "
-                         "repository's history.");
+        {
+          /* ### display deleted revision */
+          *description = _("No such file or directory was found in the "
+                           "merge target working copy.\nThe item may "
+                           "have been deleted or moved away in the "
+                           "repository's history.");
+        }
       break;
     case svn_wc_conflict_reason_unversioned:
       *description = _("An unversioned item was found in the working "
@@ -996,6 +1359,29 @@ describe_local_none_node_change(const ch
   return SVN_NO_ERROR;
 }
 
+/* Append a description of all moves in the MOVE chain to DESCRIPTION. */
+static const char *
+append_moved_to_chain_description(const char *description,
+                                  struct repos_move_info *move,
+                                  apr_pool_t *result_pool,
+                                  apr_pool_t *scratch_pool)
+{
+  if (move == NULL)
+    return description;
+
+  while (move)
+    {
+      description = apr_psprintf(scratch_pool,
+                                 _("%s\nAnd then moved away to '^/%s' by "
+                                   "%s in r%ld."),
+                                 description, move->moved_to_repos_relpath,
+                                 move->rev_author, move->rev);
+      move = move->next;
+    }
+
+  return apr_pstrdup(result_pool, description);
+}
+
 /* Implements tree_conflict_get_description_func_t. */
 static svn_error_t *
 conflict_tree_get_local_description_generic(const char **description,
@@ -1044,12 +1430,27 @@ conflict_tree_get_description_local_miss
     return svn_error_trace(conflict_tree_get_local_description_generic(
                              description, conflict, result_pool, scratch_pool));
 
-  *description = apr_psprintf(
-                   result_pool,
-                   _("No such file or directory was found in the "
-                     "merge target working copy.\nThe item was "
-                     "deleted or moved away in r%ld by %s."),
-                   details->deleted_rev, details->deleted_rev_author);
+  if (details->move)
+    {
+      *description = apr_psprintf(
+                       result_pool,
+                       _("No such file or directory was found in the "
+                         "merge target working copy.\nThe item was "
+                         "moved away to '^/%s' in r%ld by %s."),
+                       details->move->moved_to_repos_relpath,
+                       details->move->rev, details->move->rev_author);
+      *description = append_moved_to_chain_description(*description,
+                                                       details->move->next,
+                                                       result_pool,
+                                                       scratch_pool);
+    }
+  else
+    *description = apr_psprintf(
+                     result_pool,
+                     _("No such file or directory was found in the "
+                       "merge target working copy.\nThe item was "
+                       "deleted in r%ld by %s."),
+                     details->deleted_rev, details->deleted_rev_author);
 
   return SVN_NO_ERROR;
 }
@@ -1365,6 +1766,11 @@ struct conflict_tree_incoming_delete_det
 
   /* New node kind for a replaced node. This is svn_node_none for deletions. */
   svn_node_kind_t replacing_node_kind;
+
+  /* Move information. If not NULL, the first move happened in DELETED_REV
+   * or in ADDED_REV (in which case moves should be interpreted in reverse).
+   * Follow MOVE->NEXT for subsequent moves in later revisions. */
+  struct repos_move_info *move;
 };
 
 static const char *
@@ -1373,7 +1779,8 @@ describe_incoming_deletion_upon_update(
   svn_node_kind_t victim_node_kind,
   svn_revnum_t old_rev,
   svn_revnum_t new_rev,
-  apr_pool_t *result_pool)
+  apr_pool_t *result_pool,
+  apr_pool_t *scratch_pool)
 {
   if (details->replacing_node_kind == svn_node_file ||
       details->replacing_node_kind == svn_node_symlink)
@@ -1423,22 +1830,71 @@ describe_incoming_deletion_upon_update(
   else
     {
       if (victim_node_kind == svn_node_dir)
-        return apr_psprintf(result_pool,
-                            _("Directory updated from r%ld to r%ld was "
-                              "deleted or moved by %s in r%ld."),
-                            old_rev, new_rev,
-                            details->rev_author, details->deleted_rev);
+        {
+          if (details->move)
+            {
+              const char *description =
+                apr_psprintf(result_pool,
+                             _("Directory updated from r%ld to r%ld was "
+                               "moved to '^/%s' by %s in r%ld."),
+                             old_rev, new_rev,
+                             details->move->moved_to_repos_relpath,
+                             details->rev_author, details->deleted_rev);
+              return append_moved_to_chain_description(description,
+                                                       details->move->next,
+                                                       result_pool,
+                                                       scratch_pool);
+            }
+          else
+            return apr_psprintf(result_pool,
+                                _("Directory updated from r%ld to r%ld was "
+                                  "deleted by %s in r%ld."),
+                                old_rev, new_rev,
+                                details->rev_author, details->deleted_rev);
+        }
       else if (victim_node_kind == svn_node_file ||
                victim_node_kind == svn_node_symlink)
-        return apr_psprintf(result_pool,
-                            _("File updated from r%ld to r%ld was deleted or "
-                              "moved by %s in r%ld."), old_rev, new_rev,
-                            details->rev_author, details->deleted_rev);
+        {
+          if (details->move)
+            {
+              const char *description =
+                apr_psprintf(result_pool,
+                             _("File updated from r%ld to r%ld was moved "
+                               "to '^/%s' by %s in r%ld."), old_rev, new_rev,
+                             details->move->moved_to_repos_relpath,
+                             details->rev_author, details->deleted_rev);
+              return append_moved_to_chain_description(description,
+                                                       details->move->next,
+                                                       result_pool,
+                                                       scratch_pool);
+            }
+          else
+            return apr_psprintf(result_pool,
+                                _("File updated from r%ld to r%ld was "
+                                  "deleted by %s in r%ld."), old_rev, new_rev,
+                                details->rev_author, details->deleted_rev);
+        }
       else
-        return apr_psprintf(result_pool,
-                            _("Item updated from r%ld to r%ld was deleted or "
-                              "moved by %s in r%ld."), old_rev, new_rev,
-                            details->rev_author, details->deleted_rev);
+        {
+          if (details->move)
+            {
+              const char *description = 
+                apr_psprintf(result_pool,
+                             _("Item updated from r%ld to r%ld was moved "
+                               "to '^/%s' by %s in r%ld."), old_rev, new_rev,
+                             details->move->moved_to_repos_relpath,
+                             details->rev_author, details->deleted_rev);
+              return append_moved_to_chain_description(description,
+                                                       details->move->next,
+                                                       result_pool,
+                                                       scratch_pool);
+            }
+          else
+            return apr_psprintf(result_pool,
+                                _("Item updated from r%ld to r%ld was "
+                                  "deleted by %s in r%ld."), old_rev, new_rev,
+                                details->rev_author, details->deleted_rev);
+        }
     }
 }
 
@@ -1529,7 +1985,8 @@ describe_incoming_deletion_upon_switch(
   svn_revnum_t old_rev,
   const char *new_repos_relpath,
   svn_revnum_t new_rev,
-  apr_pool_t *result_pool)
+  apr_pool_t *result_pool,
+  apr_pool_t *scratch_pool)
 {
   if (details->replacing_node_kind == svn_node_file ||
       details->replacing_node_kind == svn_node_symlink)
@@ -1593,30 +2050,87 @@ describe_incoming_deletion_upon_switch(
   else
     {
       if (victim_node_kind == svn_node_dir)
-        return apr_psprintf(result_pool,
-                            _("Directory switched from\n"
-                              "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
-                              "was deleted or moved by %s in r%ld."),
-                            old_repos_relpath, old_rev,
-                            new_repos_relpath, new_rev,
-                            details->rev_author, details->deleted_rev);
+        {
+          if (details->move)
+            {
+              const char *description =
+                apr_psprintf(result_pool,
+                             _("Directory switched from\n"
+                               "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
+                               "was moved to '^/%s' by %s in r%ld."),
+                             old_repos_relpath, old_rev,
+                             new_repos_relpath, new_rev,
+                             details->move->moved_to_repos_relpath,
+                             details->rev_author, details->deleted_rev);
+              return append_moved_to_chain_description(description,
+                                                       details->move->next,
+                                                       result_pool,
+                                                       scratch_pool);
+            }
+          else
+            return apr_psprintf(result_pool,
+                                _("Directory switched from\n"
+                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
+                                  "was deleted by %s in r%ld."),
+                                old_repos_relpath, old_rev,
+                                new_repos_relpath, new_rev,
+                                details->rev_author, details->deleted_rev);
+        }
       else if (victim_node_kind == svn_node_file ||
                victim_node_kind == svn_node_symlink)
-        return apr_psprintf(result_pool,
-                            _("File switched from\n"
-                              "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
-                              "deleted or moved by %s in r%ld."),
-                            old_repos_relpath, old_rev,
-                            new_repos_relpath, new_rev,
-                            details->rev_author, details->deleted_rev);
+        {
+          if (details->move)
+            {
+              const char *description =
+                apr_psprintf(result_pool,
+                             _("File switched from\n"
+                               "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
+                               "moved to '^/%s' by %s in r%ld."),
+                             old_repos_relpath, old_rev,
+                             new_repos_relpath, new_rev,
+                             details->move->moved_to_repos_relpath,
+                             details->rev_author, details->deleted_rev);
+              return append_moved_to_chain_description(description,
+                                                       details->move->next,
+                                                       result_pool,
+                                                       scratch_pool);
+            }
+          else
+            return apr_psprintf(result_pool,
+                                _("File switched from\n"
+                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
+                                  "deleted by %s in r%ld."),
+                                old_repos_relpath, old_rev,
+                                new_repos_relpath, new_rev,
+                                details->rev_author, details->deleted_rev);
+        }
       else
-        return apr_psprintf(result_pool,
-                            _("Item switched from\n"
-                              "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
-                              "deleted or moved by %s in r%ld."),
-                            old_repos_relpath, old_rev,
-                            new_repos_relpath, new_rev,
-                            details->rev_author, details->deleted_rev);
+        {
+          if (details->move)
+            {
+              const char *description =
+                apr_psprintf(result_pool,
+                             _("Item switched from\n"
+                               "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
+                               "moved to '^/%s' by %s in r%ld."),
+                             old_repos_relpath, old_rev,
+                             new_repos_relpath, new_rev,
+                             details->move->moved_to_repos_relpath,
+                             details->rev_author, details->deleted_rev);
+              return append_moved_to_chain_description(description,
+                                                       details->move->next,
+                                                       result_pool,
+                                                       scratch_pool);
+            }
+          else
+            return apr_psprintf(result_pool,
+                                _("Item switched from\n"
+                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
+                                  "deleted by %s in r%ld."),
+                                old_repos_relpath, old_rev,
+                                new_repos_relpath, new_rev,
+                                details->rev_author, details->deleted_rev);
+        }
     }
 }
 
@@ -1732,7 +2246,8 @@ describe_incoming_deletion_upon_merge(
   svn_revnum_t old_rev,
   const char *new_repos_relpath,
   svn_revnum_t new_rev,
-  apr_pool_t *result_pool)
+  apr_pool_t *result_pool,
+  apr_pool_t *scratch_pool)
 {
   if (details->replacing_node_kind == svn_node_file ||
       details->replacing_node_kind == svn_node_symlink)
@@ -1796,30 +2311,87 @@ describe_incoming_deletion_upon_merge(
   else
     {
       if (victim_node_kind == svn_node_dir)
-        return apr_psprintf(result_pool,
-                            _("Directory merged from\n"
-                              "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
-                              "deleted or moved by %s in r%ld."),
-                            old_repos_relpath, old_rev,
-                            new_repos_relpath, new_rev,
-                            details->rev_author, details->deleted_rev);
+        {
+          if (details->move)
+            {
+              const char *description =
+                apr_psprintf(result_pool,
+                             _("Directory merged from\n"
+                               "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
+                               "moved to '^/%s' by %s in r%ld."),
+                             old_repos_relpath, old_rev,
+                             new_repos_relpath, new_rev,
+                             details->move->moved_to_repos_relpath,
+                             details->rev_author, details->deleted_rev);
+              return append_moved_to_chain_description(description,
+                                                       details->move->next,
+                                                       result_pool,
+                                                       scratch_pool);
+            }
+          else
+            return apr_psprintf(result_pool,
+                                _("Directory merged from\n"
+                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
+                                  "deleted by %s in r%ld."),
+                                old_repos_relpath, old_rev,
+                                new_repos_relpath, new_rev,
+                                details->rev_author, details->deleted_rev);
+        }
       else if (victim_node_kind == svn_node_file ||
                victim_node_kind == svn_node_symlink)
-        return apr_psprintf(result_pool,
-                            _("File merged from\n"
-                              "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
-                              "deleted or moved by %s in r%ld."),
-                            old_repos_relpath, old_rev,
-                            new_repos_relpath, new_rev,
-                            details->rev_author, details->deleted_rev);
+        {
+          if (details->move)
+            {
+              const char *description =
+                apr_psprintf(result_pool,
+                             _("File merged from\n"
+                               "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
+                               "moved to '^/%s' by %s in r%ld."),
+                             old_repos_relpath, old_rev,
+                             new_repos_relpath, new_rev,
+                             details->move->moved_to_repos_relpath,
+                             details->rev_author, details->deleted_rev);
+              return append_moved_to_chain_description(description,
+                                                       details->move->next,
+                                                       result_pool,
+                                                       scratch_pool);
+            }
+          else
+            return apr_psprintf(result_pool,
+                                _("File merged from\n"
+                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
+                                  "deleted by %s in r%ld."),
+                                old_repos_relpath, old_rev,
+                                new_repos_relpath, new_rev,
+                                details->rev_author, details->deleted_rev);
+        }
       else
-        return apr_psprintf(result_pool,
-                            _("Item merged from\n"
-                              "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
-                              "deleted or moved by %s in r%ld."),
-                            old_repos_relpath, old_rev,
-                            new_repos_relpath, new_rev,
-                            details->rev_author, details->deleted_rev);
+        {
+          if (details->move)
+            {
+              const char *description =
+                apr_psprintf(result_pool,
+                             _("Item merged from\n"
+                               "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
+                               "moved to '^/%s' by %s in r%ld."),
+                             old_repos_relpath, old_rev,
+                             new_repos_relpath, new_rev,
+                             details->move->moved_to_repos_relpath,
+                             details->rev_author, details->deleted_rev);
+              return append_moved_to_chain_description(description,
+                                                       details->move->next,
+                                                       result_pool,
+                                                       scratch_pool);
+            }
+          else
+            return apr_psprintf(result_pool,
+                                _("Item merged from\n"
+                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
+                                  "deleted by %s in r%ld."),
+                                old_repos_relpath, old_rev,
+                                new_repos_relpath, new_rev,
+                                details->rev_author, details->deleted_rev);
+        }
     }
 }
 
@@ -1965,7 +2537,8 @@ conflict_tree_get_description_incoming_d
                                                           victim_node_kind,
                                                           old_rev,
                                                           new_rev,
-                                                          result_pool);
+                                                          result_pool,
+                                                          scratch_pool);
         }
       else /* details->added_rev != SVN_INVALID_REVNUM */
         {
@@ -1984,7 +2557,8 @@ conflict_tree_get_description_incoming_d
                                                           old_rev,
                                                           new_repos_relpath,
                                                           new_rev,
-                                                          result_pool);
+                                                          result_pool,
+                                                          scratch_pool);
         }
       else /* details->added_rev != SVN_INVALID_REVNUM */
         {
@@ -2000,12 +2574,13 @@ conflict_tree_get_description_incoming_d
       if (details->deleted_rev != SVN_INVALID_REVNUM)
         {
           action = describe_incoming_deletion_upon_merge(details,
-                                                          victim_node_kind,
-                                                          old_repos_relpath,
-                                                          old_rev,
-                                                          new_repos_relpath,
-                                                          new_rev,
-                                                          result_pool);
+                                                         victim_node_kind,
+                                                         old_repos_relpath,
+                                                         old_rev,
+                                                         new_repos_relpath,
+                                                         new_rev,
+                                                         result_pool,
+                                                         scratch_pool);
         }
       else /* details->added_rev != SVN_INVALID_REVNUM */
         {
@@ -2026,12 +2601,15 @@ struct find_added_rev_baton
 {
   svn_revnum_t added_rev;
   const char *repos_relpath;
+  const char *parent_repos_relpath;
   apr_pool_t *pool;
 };
 
 /* Implements svn_location_segment_receiver_t.
  * Finds the revision in which a node was added by tracing 'start'
- * revisions in location segments reported for the node. */
+ * revisions in location segments reported for the node.
+ * If the PARENT_REPOS_RELPATH in the baton is not NULL, only consider
+ * segments in which the node existed somwhere beneath this path. */
 static svn_error_t *
 find_added_rev(svn_location_segment_t *segment,
                void *baton,
@@ -2041,8 +2619,13 @@ find_added_rev(svn_location_segment_t *s
 
   if (segment->path) /* not interested in gaps */
     {
-      b->added_rev = segment->range_start;
-      b->repos_relpath = apr_pstrdup(b->pool, segment->path);
+      if (b->parent_repos_relpath == NULL ||
+          svn_relpath_skip_ancestor(b->parent_repos_relpath,
+                                    segment->path) != NULL)
+        {
+          b->added_rev = segment->range_start;
+          b->repos_relpath = apr_pstrdup(b->pool, segment->path);
+        }
     }
 
   return SVN_NO_ERROR;
@@ -2081,6 +2664,7 @@ get_incoming_delete_details_for_reverse_
   *details = apr_pcalloc(result_pool, sizeof(**details));
   b.added_rev = SVN_INVALID_REVNUM;
   b.repos_relpath = NULL;
+  b.parent_repos_relpath = NULL;
   b.pool = scratch_pool;
   /* Figure out when this node was added. */
   SVN_ERR(svn_ra_get_location_segments(ra_session, "", old_rev,
@@ -2142,39 +2726,45 @@ conflict_tree_get_details_incoming_delet
     {
       if (old_rev < new_rev)
         {
-          svn_ra_session_t *ra_session;
-          const char *url;
-          const char *corrected_url;
+          const char *parent_repos_relpath;
           svn_revnum_t deleted_rev;
-          svn_string_t *author_revprop;
+          const char *deleted_rev_author;
+          svn_node_kind_t replacing_node_kind;
+          struct repos_move_info *move;
 
           /* The update operation went forward in history. */
-          url = svn_path_url_add_component2(repos_root_url, new_repos_relpath,
-                                            scratch_pool);
-          SVN_ERR(svn_client__open_ra_session_internal(&ra_session,
-                                                       &corrected_url,
-                                                       url, NULL, NULL,
-                                                       FALSE,
-                                                       FALSE,
-                                                       conflict->ctx,
-                                                       scratch_pool,
-                                                       scratch_pool));
-          SVN_ERR(svn_ra_get_deleted_rev(ra_session, "", old_rev, new_rev,
-                                         &deleted_rev, scratch_pool));
-          SVN_ERR(svn_ra_rev_prop(ra_session, deleted_rev,
-                                  SVN_PROP_REVISION_AUTHOR,
-                                  &author_revprop, scratch_pool));
+
+          SVN_ERR(svn_wc__node_get_repos_info(NULL, &parent_repos_relpath,
+                                              NULL, NULL,
+                                              conflict->ctx->wc_ctx,
+                                              svn_dirent_dirname(
+                                                conflict->local_abspath,
+                                                scratch_pool),
+                                              scratch_pool,
+                                              scratch_pool));
+          SVN_ERR(find_revision_for_suspected_deletion(
+                    &deleted_rev, &deleted_rev_author, &replacing_node_kind,
+                    &move, conflict,
+                    svn_dirent_basename(conflict->local_abspath, scratch_pool),
+                    parent_repos_relpath, old_rev, new_rev,
+                    NULL, SVN_INVALID_REVNUM, /* related to self */
+                    conflict->pool, scratch_pool));
+          if (deleted_rev == SVN_INVALID_REVNUM)
+            {
+              /* We could not determine the revision in which the node was
+               * deleted. We cannot provide the required details so the best
+               * we can do is fall back to the default description. */
+              return SVN_NO_ERROR;
+            }
+
           details = apr_pcalloc(conflict->pool, sizeof(*details));
           details->deleted_rev = deleted_rev;
           details->added_rev = SVN_INVALID_REVNUM;
           details->repos_relpath = apr_pstrdup(conflict->pool,
                                                new_repos_relpath);
-          details->rev_author = apr_pstrdup(conflict->pool,
-                                            author_revprop->data);
-          /* Check for replacement. */
-          SVN_ERR(svn_ra_check_path(ra_session, "", deleted_rev,
-                                    &details->replacing_node_kind,
-                                    scratch_pool));
+          details->rev_author = deleted_rev_author;
+          details->replacing_node_kind = replacing_node_kind;
+          details->move = move;
         }
       else /* new_rev < old_rev */
         {
@@ -2194,6 +2784,7 @@ conflict_tree_get_details_incoming_delet
           svn_revnum_t deleted_rev;
           const char *deleted_rev_author;
           svn_node_kind_t replacing_node_kind;
+          struct repos_move_info *move;
 
           /* The switch/merge operation went forward in history.
            *
@@ -2202,7 +2793,7 @@ conflict_tree_get_details_incoming_delet
            * the revision which deleted the node. */
           SVN_ERR(find_revision_for_suspected_deletion(
                     &deleted_rev, &deleted_rev_author, &replacing_node_kind,
-                    conflict,
+                    &move, conflict,
                     svn_relpath_basename(new_repos_relpath, scratch_pool),
                     svn_relpath_dirname(new_repos_relpath, scratch_pool),
                     new_rev, old_rev, old_repos_relpath, old_rev,
@@ -2223,6 +2814,7 @@ conflict_tree_get_details_incoming_delet
           details->rev_author = apr_pstrdup(conflict->pool,
                                             deleted_rev_author);
           details->replacing_node_kind = replacing_node_kind;
+          details->move = move;
         }
       else /* new_rev < old_rev */
         {
@@ -2262,6 +2854,10 @@ struct conflict_tree_incoming_add_detail
   /* Authors who committed ADDED_REV/DELETED_REV. */
   const char *added_rev_author;
   const char *deleted_rev_author;
+
+  /* Move information, in case the item was not added/deleted but moved here
+   * or moved away. Else NULL. */
+  struct repos_move_info *move;
 };
 
 /* Implements tree_conflict_get_details_func_t.
@@ -2323,6 +2919,7 @@ conflict_tree_get_details_incoming_add(s
       details = apr_pcalloc(conflict->pool, sizeof(*details));
       b.added_rev = SVN_INVALID_REVNUM;
       b.repos_relpath = NULL;
+      b.parent_repos_relpath = NULL;
       b.pool = scratch_pool;
       /* Figure out when this node was added. */
       SVN_ERR(svn_ra_get_location_segments(ra_session, "", new_rev,
@@ -2386,6 +2983,7 @@ conflict_tree_get_details_incoming_add(s
           details = apr_pcalloc(conflict->pool, sizeof(*details));
           b.added_rev = SVN_INVALID_REVNUM;
           b.repos_relpath = NULL;
+          b.parent_repos_relpath = NULL;
           b.pool = scratch_pool;
           /* Figure out when this node was added. */
           SVN_ERR(svn_ra_get_location_segments(ra_session, "", new_rev,
@@ -2409,36 +3007,37 @@ conflict_tree_get_details_incoming_add(s
            * This addition is in fact a deletion, applied in reverse,
            * which happened on the branch we merged from.
            * Find the revision which deleted the node. */
-          svn_ra_session_t *ra_session;
-          const char *url;
-          const char *corrected_url;
-          svn_string_t *author_revprop;
           svn_revnum_t deleted_rev;
+          const char *deleted_rev_author;
+          svn_node_kind_t replacing_node_kind;
+          struct repos_move_info *move;
+
+          SVN_ERR(find_revision_for_suspected_deletion(
+                    &deleted_rev, &deleted_rev_author, &replacing_node_kind,
+                    &move, conflict, svn_relpath_basename(new_repos_relpath,
+                                                   scratch_pool),
+                    svn_relpath_dirname(new_repos_relpath, scratch_pool),
+                    new_rev, old_rev,
+                    NULL, SVN_INVALID_REVNUM, /* related to self */
+                    conflict->pool, scratch_pool));
+          if (deleted_rev == SVN_INVALID_REVNUM)
+            {
+              /* We could not determine the revision in which the node was
+               * deleted. We cannot provide the required details so the best
+               * we can do is fall back to the default description. */
+              return SVN_NO_ERROR;
+            }
 
-          url = svn_path_url_add_component2(repos_root_url, new_repos_relpath,
-                                            scratch_pool);
-          SVN_ERR(svn_client__open_ra_session_internal(&ra_session,
-                                                       &corrected_url,
-                                                       url, NULL, NULL,
-                                                       FALSE,
-                                                       FALSE,
-                                                       conflict->ctx,
-                                                       scratch_pool,
-                                                       scratch_pool));
-          SVN_ERR(svn_ra_get_deleted_rev(ra_session, "", new_rev, old_rev,
-                                         &deleted_rev, scratch_pool));
-          SVN_ERR(svn_ra_rev_prop(ra_session, deleted_rev,
-                                  SVN_PROP_REVISION_AUTHOR,
-                                  &author_revprop, scratch_pool));
           details = apr_pcalloc(conflict->pool, sizeof(*details));
           details->repos_relpath = apr_pstrdup(conflict->pool,
                                                new_repos_relpath);
           details->deleted_rev = deleted_rev;
           details->deleted_rev_author = apr_pstrdup(conflict->pool,
-                                                    author_revprop->data);
+                                                    deleted_rev_author);
 
           details->added_rev = SVN_INVALID_REVNUM;
           details->added_rev_author = NULL;
+          details->move = move;
         }
     }
   else
@@ -4024,6 +4623,8 @@ resolve_merge_incoming_added_dir_merge(s
 {
   const char *repos_root_url;
   const char *repos_uuid;
+  const char *incoming_old_repos_relpath;
+  svn_revnum_t incoming_old_pegrev;
   const char *incoming_new_repos_relpath;
   svn_revnum_t incoming_new_pegrev;
   const char *local_abspath;
@@ -4044,7 +4645,7 @@ resolve_merge_incoming_added_dir_merge(s
     return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                              _("Conflict resolution option '%d' requires "
                                "details for tree conflict at '%s' to be "
-                               "fetched from the repository."),
+                               "fetched from the repository"),
                             option->id,
                             svn_dirent_local_style(local_abspath,
                                                    scratch_pool));
@@ -4057,15 +4658,44 @@ resolve_merge_incoming_added_dir_merge(s
                                         details->repos_relpath,
                                         scratch_pool);
   revision1.kind = svn_opt_revision_number;
-  revision1.value.number = details->added_rev;
+  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
+            &incoming_old_repos_relpath, &incoming_old_pegrev,
+            NULL, conflict, scratch_pool, scratch_pool));
   SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
             &incoming_new_repos_relpath, &incoming_new_pegrev,
             NULL, conflict, scratch_pool, scratch_pool));
-  source2 = svn_path_url_add_component2(repos_root_url,
-                                        incoming_new_repos_relpath,
-                                        scratch_pool);
-  revision2.kind = svn_opt_revision_number;
-  revision2.value.number = incoming_new_pegrev;
+  if (incoming_old_pegrev < incoming_new_pegrev) /* forward merge */
+    {
+      if (details->added_rev == SVN_INVALID_REVNUM)
+        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                 _("Could not determine when '%s' was "
+                                   "added the repository"),
+                                 svn_dirent_local_style(local_abspath,
+                                                        scratch_pool));
+      revision1.value.number = details->added_rev;
+
+      source2 = svn_path_url_add_component2(repos_root_url,
+                                            incoming_new_repos_relpath,
+                                            scratch_pool);
+      revision2.kind = svn_opt_revision_number;
+      revision2.value.number = incoming_new_pegrev;
+    }
+  else /* reverse-merge */
+    {
+      if (details->deleted_rev == SVN_INVALID_REVNUM)
+        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                 _("Could not determine when '%s' was "
+                                   "deleted from the repository"),
+                                 svn_dirent_local_style(local_abspath,
+                                                        scratch_pool));
+      revision1.value.number = details->deleted_rev;
+
+      source2 = svn_path_url_add_component2(repos_root_url,
+                                            incoming_old_repos_relpath,
+                                            scratch_pool);
+      revision2.kind = svn_opt_revision_number;
+      revision2.value.number = incoming_old_pegrev;
+    }
 
   /* ### The following WC modifications should be atomic. */
   SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
@@ -4122,6 +4752,559 @@ resolve_merge_incoming_added_dir_merge(s
   return SVN_NO_ERROR;
 }
 
+/* A baton for notification_adjust_func(). */
+struct notification_adjust_baton
+{
+  svn_wc_notify_func2_t inner_func;
+  void *inner_baton;
+  const char *checkout_abspath;
+  const char *final_abspath;
+};
+
+/* A svn_wc_notify_func2_t function that wraps BATON->inner_func (whose
+ * baton is BATON->inner_baton) and adjusts the notification paths that
+ * start with BATON->checkout_abspath to start instead with
+ * BATON->final_abspath. */
+static void
+notification_adjust_func(void *baton,
+                         const svn_wc_notify_t *notify,
+                         apr_pool_t *pool)
+{
+  struct notification_adjust_baton *nb = baton;
+  svn_wc_notify_t *inner_notify = svn_wc_dup_notify(notify, pool);
+  const char *relpath;
+
+  relpath = svn_dirent_skip_ancestor(nb->checkout_abspath, notify->path);
+  inner_notify->path = svn_dirent_join(nb->final_abspath, relpath, pool);
+
+  if (nb->inner_func)
+    nb->inner_func(nb->inner_baton, inner_notify, pool);
+}
+
+/* Resolve a dir/dir "incoming add vs local obstruction" tree conflict by
+ * replacing the local directory with the incoming directory.
+ * If MERGE_DIRS is set, also merge the directories after replacing. */
+static svn_error_t *
+merge_incoming_added_dir_replace(svn_client_conflict_option_t *option,
+                                 svn_client_conflict_t *conflict,
+                                 svn_boolean_t merge_dirs,
+                                 apr_pool_t *scratch_pool)
+{
+  svn_ra_session_t *ra_session;
+  const char *url;
+  const char *corrected_url;
+  const char *repos_root_url;
+  const char *repos_uuid;
+  const char *incoming_new_repos_relpath;
+  svn_revnum_t incoming_new_pegrev;
+  const char *local_abspath;
+  const char *lock_abspath;
+  svn_client_ctx_t *ctx = conflict->ctx;
+  const char *tmpdir_abspath, *tmp_abspath;
+  svn_error_t *err;
+  svn_revnum_t copy_src_revnum;
+  svn_opt_revision_t copy_src_peg_revision;
+  svn_boolean_t timestamp_sleep;
+  svn_wc_notify_func2_t old_notify_func2 = ctx->notify_func2;
+  void *old_notify_baton2 = ctx->notify_baton2;
+  struct notification_adjust_baton nb;
+
+  local_abspath = svn_client_conflict_get_local_abspath(conflict);
+
+  /* Find the URL of the incoming added directory in the repository. */
+  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
+            &incoming_new_repos_relpath, &incoming_new_pegrev,
+            NULL, conflict, scratch_pool,
+            scratch_pool));
+  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, &repos_uuid,
+                                             conflict, scratch_pool,
+                                             scratch_pool));
+  url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath,
+                                    scratch_pool);
+  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
+                                               url, NULL, NULL, FALSE, FALSE,
+                                               conflict->ctx, scratch_pool,
+                                               scratch_pool));
+  if (corrected_url)
+    url = corrected_url;
+
+
+  /* Find a temporary location in which to check out the copy source. */
+  SVN_ERR(svn_wc__get_tmpdir(&tmpdir_abspath, ctx->wc_ctx, local_abspath,
+                             scratch_pool, scratch_pool));
+
+  SVN_ERR(svn_io_open_unique_file3(NULL, &tmp_abspath, tmpdir_abspath,
+                                   svn_io_file_del_on_close,
+                                   scratch_pool, scratch_pool));
+
+  /* Make a new checkout of the requested source. While doing so,
+   * resolve copy_src_revnum to an actual revision number in case it
+   * was until now 'invalid' meaning 'head'.  Ask this function not to
+   * sleep for timestamps, by passing a sleep_needed output param.
+   * Send notifications for all nodes except the root node, and adjust
+   * them to refer to the destination rather than this temporary path. */
+
+  nb.inner_func = ctx->notify_func2;
+  nb.inner_baton = ctx->notify_baton2;
+  nb.checkout_abspath = tmp_abspath;
+  nb.final_abspath = local_abspath;
+  ctx->notify_func2 = notification_adjust_func;
+  ctx->notify_baton2 = &nb;
+
+  copy_src_peg_revision.kind = svn_opt_revision_number;
+  copy_src_peg_revision.value.number = incoming_new_pegrev;
+
+  err = svn_client__checkout_internal(&copy_src_revnum, &timestamp_sleep,
+                                      url, tmp_abspath,
+                                      &copy_src_peg_revision,
+                                      &copy_src_peg_revision,
+                                      svn_depth_infinity,
+                                      TRUE, /* we want to ignore externals */
+                                      FALSE, /* we don't allow obstructions */
+                                      ra_session, ctx, scratch_pool);
+
+  ctx->notify_func2 = old_notify_func2;
+  ctx->notify_baton2 = old_notify_baton2;
+
+  SVN_ERR(err);
+
+  /* ### The following WC modifications should be atomic. */
+
+  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
+                                                 svn_dirent_dirname(
+                                                   local_abspath,
+                                                   scratch_pool),
+                                                 scratch_pool, scratch_pool));
+
+  /* Remove the working directory. */
+  err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE,
+                       ctx->cancel_func, ctx->cancel_baton,
+                       ctx->notify_func2, ctx->notify_baton2,
+                       scratch_pool);
+  if (err)
+    goto unlock_wc;
+
+  /* Schedule dst_path for addition in parent, with copy history.
+     Don't send any notification here.
+     Then remove the temporary checkout's .svn dir in preparation for
+     moving the rest of it into the final destination. */
+  err = svn_wc_copy3(ctx->wc_ctx, tmp_abspath, local_abspath,
+                     TRUE /* metadata_only */,
+                     ctx->cancel_func, ctx->cancel_baton,
+                     NULL, NULL, scratch_pool);
+  if (err)
+    goto unlock_wc;
+
+  err = svn_wc__acquire_write_lock(NULL, ctx->wc_ctx, tmp_abspath,
+                                   FALSE, scratch_pool, scratch_pool);
+  if (err)
+    goto unlock_wc;
+  err = svn_wc_remove_from_revision_control2(ctx->wc_ctx,
+                                             tmp_abspath,
+                                             FALSE, FALSE,
+                                             ctx->cancel_func,
+                                             ctx->cancel_baton,
+                                             scratch_pool);
+  if (err)
+    goto unlock_wc;
+
+  /* Move the temporary disk tree into place. */
+  err = svn_io_file_rename2(tmp_abspath, local_abspath, FALSE, scratch_pool);
+  if (err)
+    goto unlock_wc;
+
+  if (ctx->notify_func2)
+    {
+      svn_wc_notify_t *notify = svn_wc_create_notify(local_abspath,
+                                                     svn_wc_notify_add,
+                                                     scratch_pool);
+      notify->kind = svn_node_dir;
+      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
+    }
+
+  /* Resolve to current working copy state.
+  * svn_client__merge_locked() requires this. */
+  err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool);
+  if (err)
+    goto unlock_wc;
+
+  if (merge_dirs)
+    {
+      svn_client__conflict_report_t *conflict_report;
+      const char *source1;
+      svn_opt_revision_t revision1;
+      const char *source2;
+      svn_opt_revision_t revision2;
+      svn_revnum_t base_revision;
+      const char *base_repos_relpath;
+      struct find_added_rev_baton b;
+
+      /* Find the URL and revision of the directory we have just replaced. */
+      err = svn_wc__node_get_base(NULL, &base_revision, &base_repos_relpath,
+                                  NULL, NULL, NULL, ctx->wc_ctx, local_abspath,
+                                  FALSE, scratch_pool, scratch_pool);
+      if (err)
+        goto unlock_wc;
+
+      url = svn_path_url_add_component2(repos_root_url, base_repos_relpath,
+                                        scratch_pool);
+
+      /* Trace the replaced directory's history to its origin. */
+      err = svn_ra_reparent(ra_session, url, scratch_pool);
+      if (err)
+        goto unlock_wc;
+      b.added_rev = SVN_INVALID_REVNUM;
+      b.repos_relpath = NULL;
+      b.parent_repos_relpath = svn_relpath_dirname(base_repos_relpath,
+                                                   scratch_pool);
+      b.pool = scratch_pool;
+      err = svn_ra_get_location_segments(ra_session, "", base_revision,
+                                         base_revision, SVN_INVALID_REVNUM,
+                                         find_added_rev, &b,
+                                         scratch_pool);
+      if (err)
+        goto unlock_wc;
+
+      if (b.added_rev == SVN_INVALID_REVNUM)
+        {
+          err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                  _("Could not determine the revision in "
+                                    "which '^/%s' was added to the "
+                                    "repository.\n"),
+                                  base_repos_relpath);
+          goto unlock_wc;
+        }
+
+      /* Merge the replaced directory into the directory which replaced it.
+       * We do not need to consider a reverse-merge here since the source of
+       * this merge was part of the merge target working copy, not a branch
+       * in the repository. */
+      source1 = url;
+      revision1.kind = svn_opt_revision_number;
+      /* ### Our merge logic doesn't support the merge -c ADDED_REV case.
+       * ### It errors out with 'path not found', unlike diff -c ADDED_REV. */
+      if (b.added_rev == base_revision)
+        revision1.value.number = b.added_rev - 1; /* merge -c ADDED_REV */
+      else
+        revision1.value.number = b.added_rev; /* merge -r ADDED_REV:BASE_REV */
+      source2 = url;
+      revision2.kind = svn_opt_revision_number;
+      revision2.value.number = base_revision;
+      
+      err = svn_client__merge_locked(&conflict_report,
+                                     source1, &revision1,
+                                     source2, &revision2,
+                                     local_abspath, svn_depth_infinity,
+                                     TRUE, TRUE, /* do a no-ancestry merge */
+                                     FALSE, FALSE, FALSE,
+                                     FALSE, /* no need to allow mixed-rev */
+                                     NULL, ctx, scratch_pool, scratch_pool);
+      err = svn_error_compose_create(err,
+                                     svn_client__make_merge_conflict_error(
+                                       conflict_report, scratch_pool));
+    }
+
+unlock_wc:
+  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
+                                                                 lock_abspath,
+                                                                 scratch_pool));
+  svn_io_sleep_for_timestamps(local_abspath, scratch_pool);
+  SVN_ERR(err);
+
+  if (ctx->notify_func2)
+    {
+      svn_wc_notify_t *notify = svn_wc_create_notify(
+                                  local_abspath,
+                                  svn_wc_notify_resolved_tree,
+                                  scratch_pool);
+
+      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
+    }
+
+  conflict->resolution_tree = svn_client_conflict_option_get_id(option);
+
+  return SVN_NO_ERROR;
+}
+
+/* Implements conflict_option_resolve_func_t. */
+static svn_error_t *
+resolve_merge_incoming_added_dir_replace(svn_client_conflict_option_t *option,
+                                         svn_client_conflict_t *conflict,
+                                         apr_pool_t *scratch_pool)
+{
+  return svn_error_trace(merge_incoming_added_dir_replace(option,
+                                                          conflict,
+                                                          FALSE,
+                                                          scratch_pool));
+}
+
+/* Implements conflict_option_resolve_func_t. */
+static svn_error_t *
+resolve_merge_incoming_added_dir_replace_and_merge(
+  svn_client_conflict_option_t *option,
+  svn_client_conflict_t *conflict,
+  apr_pool_t *scratch_pool)
+{
+  return svn_error_trace(merge_incoming_added_dir_replace(option,
+                                                          conflict,
+                                                          TRUE,
+                                                          scratch_pool));
+}
+
+/* Verify the local working copy state matches what we expect when an
+ * incoming deletion tree conflict exists.
+ * We assume update/merge/switch operations leave the working copy in a
+ * state which prefers the local change and cancels the deletion.
+ * Run a quick sanity check and error out if it looks as if the
+ * working copy was modified since, even though it's not easy to make
+ * such modifications without also clearing the conflict marker. */
+static svn_error_t *
+verify_local_state_for_incoming_delete(svn_client_conflict_t *conflict,
+                                       svn_client_conflict_option_t *option,
+                                       apr_pool_t *scratch_pool)
+{
+  const char *local_abspath;
+  const char *wcroot_abspath;
+  svn_wc_operation_t operation;
+  svn_client_ctx_t *ctx = conflict->ctx;
+
+  local_abspath = svn_client_conflict_get_local_abspath(conflict);
+  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
+                             local_abspath, scratch_pool,
+                             scratch_pool));
+  operation = svn_client_conflict_get_operation(conflict);
+
+  if (operation == svn_wc_operation_update ||
+      operation == svn_wc_operation_switch)
+    {
+      struct conflict_tree_incoming_delete_details *details;
+      svn_boolean_t is_copy;
+      svn_revnum_t copyfrom_rev;
+      const char *copyfrom_repos_relpath;
+
+      details = conflict->tree_conflict_incoming_details;
+      if (details == NULL)
+        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                 _("Conflict resolution option '%d' requires "
+                                   "details for tree conflict at '%s' to be "
+                                   "fetched from the repository."),
+                                option->id,
+                                svn_dirent_local_style(local_abspath,
+                                                       scratch_pool));
+
+      /* Ensure that the item is a copy of itself from before it was deleted.
+       * Update and switch are supposed to set this up when flagging the
+       * conflict. */
+      SVN_ERR(svn_wc__node_get_origin(&is_copy, &copyfrom_rev,
+                                      &copyfrom_repos_relpath,
+                                      NULL, NULL, NULL, NULL,
+                                      ctx->wc_ctx, local_abspath, FALSE,
+                                      scratch_pool, scratch_pool));
+      if (!is_copy)
+        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                 _("Cannot resolve tree conflict on '%s' by "
+                                   "ignoring the incoming deletion "
+                                   "(expected a copied item, but the item "
+                                   "is not a copy)"),
+                                 svn_dirent_local_style(
+                                   svn_dirent_skip_ancestor(
+                                     wcroot_abspath,
+                                     conflict->local_abspath),
+                                 scratch_pool));
+      else if (details->deleted_rev == SVN_INVALID_REVNUM &&
+               details->added_rev == SVN_INVALID_REVNUM)
+        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                 _("Could not find the revision in which '%s' "
+                                   "was deleted from the repository"),
+                                 svn_dirent_local_style(
+                                   svn_dirent_skip_ancestor(
+                                     wcroot_abspath,
+                                     conflict->local_abspath),
+                                   scratch_pool));
+      else if (details->deleted_rev != SVN_INVALID_REVNUM &&
+               copyfrom_rev >= details->deleted_rev)
+        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                 _("Cannot resolve tree conflict on '%s' by "
+                                   "ignoring the incoming deletion (expected "
+                                   "an item copied from a revision smaller "
+                                   "than r%ld, but the item was copied from "
+                                   "r%ld)"),
+                                 svn_dirent_local_style(
+                                   svn_dirent_skip_ancestor(
+                                     wcroot_abspath, conflict->local_abspath),
+                                   scratch_pool),
+                                 details->deleted_rev, copyfrom_rev);
+
+      else if (details->added_rev != SVN_INVALID_REVNUM &&
+               copyfrom_rev < details->added_rev)
+        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                 _("Cannot resolve tree conflict on '%s' by "
+                                   "ignoring the incoming deletion (expected "
+                                   "an item copied from a revision larger than "
+                                   "r%ld, but the item was copied from r%ld)"),
+                                 svn_dirent_local_style(
+                                   svn_dirent_skip_ancestor(
+                                     wcroot_abspath, conflict->local_abspath),
+                                   scratch_pool),
+                                  details->added_rev, copyfrom_rev);
+
+      else if (operation == svn_wc_operation_update &&
+               strcmp(copyfrom_repos_relpath, details->repos_relpath) != 0)
+        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                 _("Cannot resolve tree conflict on '%s' by "
+                                   "ignoring the incoming deletion (expected "
+                                   "an item copied from '^/%s', but the item "
+                                   "was copied from '^/%s@%ld')"),
+                                 svn_dirent_local_style(
+                                   svn_dirent_skip_ancestor(
+                                     wcroot_abspath, conflict->local_abspath),
+                                   scratch_pool),
+                                 details->repos_relpath,
+                                 copyfrom_repos_relpath, copyfrom_rev);
+      else if (operation == svn_wc_operation_switch)
+        {
+          const char *old_repos_relpath;
+
+          SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
+                    &old_repos_relpath, NULL, NULL, conflict,
+                    scratch_pool, scratch_pool));
+
+          if (strcmp(copyfrom_repos_relpath, old_repos_relpath) != 0)
+            return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                     _("Cannot resolve tree conflict on '%s' "
+                                       "by ignoring the incoming deletion "
+                                       "(expected an item copied from '^/%s', "
+                                       "but the item was copied from "
+                                        "'^/%s@%ld')"),
+                                     svn_dirent_local_style(
+                                       svn_dirent_skip_ancestor(
+                                         wcroot_abspath,
+                                         conflict->local_abspath),
+                                       scratch_pool),
+                                     old_repos_relpath,
+                                     copyfrom_repos_relpath, copyfrom_rev);
+        }
+    }
+  else if (operation == svn_wc_operation_merge)
+    {
+      svn_node_kind_t victim_node_kind;
+      svn_node_kind_t on_disk_kind;
+
+      /* For merge, all we can do is ensure that the item still exists. */
+      victim_node_kind =
+        svn_client_conflict_tree_get_victim_node_kind(conflict);
+      SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool));
+
+      if (victim_node_kind != on_disk_kind)
+        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
+                                 _("Cannot resolve tree conflict on '%s' by "
+                                   "ignoring the incoming deletion (expected "
+                                   "node kind '%s' but found '%s')"),
+                                 svn_dirent_local_style(
+                                   svn_dirent_skip_ancestor(
+                                     wcroot_abspath, conflict->local_abspath),
+                                   scratch_pool),
+                                 svn_node_kind_to_word(victim_node_kind),
+                                 svn_node_kind_to_word(on_disk_kind));
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* Implements conflict_option_resolve_func_t. */
+static svn_error_t *
+resolve_incoming_delete_ignore(svn_client_conflict_option_t *option,
+                               svn_client_conflict_t *conflict,
+                               apr_pool_t *scratch_pool)
+{
+  svn_client_conflict_option_id_t option_id;
+  const char *local_abspath;
+  const char *lock_abspath;
+  svn_client_ctx_t *ctx = conflict->ctx;
+  svn_error_t *err;
+
+  option_id = svn_client_conflict_option_get_id(option);
+  local_abspath = svn_client_conflict_get_local_abspath(conflict);
+
+  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
+                                                 local_abspath,
+                                                 scratch_pool, scratch_pool));
+
+  err = verify_local_state_for_incoming_delete(conflict, option, scratch_pool);
+  if (err)
+    goto unlock_wc;
+
+  /* Resolve to the current working copy state. */
+  err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool);
+
+  /* svn_wc__del_tree_conflict doesn't handle notification for us */
+  if (ctx->notify_func2)
+    ctx->notify_func2(ctx->notify_baton2,
+                      svn_wc_create_notify(local_abspath,
+                                           svn_wc_notify_resolved_tree,
+                                           scratch_pool),
+                      scratch_pool);
+
+unlock_wc:
+  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
+                                                                 lock_abspath,
+                                                                 scratch_pool));
+  SVN_ERR(err);
+
+  conflict->resolution_tree = option_id;
+
+  return SVN_NO_ERROR;
+}
+
+/* Implements conflict_option_resolve_func_t. */
+static svn_error_t *
+resolve_incoming_delete_accept(svn_client_conflict_option_t *option,
+                               svn_client_conflict_t *conflict,
+                               apr_pool_t *scratch_pool)
+{
+  svn_client_conflict_option_id_t option_id;
+  const char *local_abspath;
+  const char *lock_abspath;
+  svn_client_ctx_t *ctx = conflict->ctx;
+  svn_error_t *err;
+
+  option_id = svn_client_conflict_option_get_id(option);
+  local_abspath = svn_client_conflict_get_local_abspath(conflict);
+
+  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
+                                                 local_abspath,
+                                                 scratch_pool, scratch_pool));
+
+  err = verify_local_state_for_incoming_delete(conflict, option, scratch_pool);
+  if (err)
+    goto unlock_wc;
+
+  /* Delete the tree conflict victim. Marks the conflict resolved. */
+  err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE,
+                       ctx->cancel_func, ctx->cancel_baton,
+                       ctx->notify_func2, ctx->notify_baton2,
+                       scratch_pool);
+  if (err)
+    goto unlock_wc;
+
+  if (ctx->notify_func2)
+    ctx->notify_func2(ctx->notify_baton2,
+                      svn_wc_create_notify(local_abspath,
+                                           svn_wc_notify_resolved_tree,
+                                           scratch_pool),
+                      scratch_pool);
+
+unlock_wc:
+  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
+                                                                 lock_abspath,
+                                                                 scratch_pool));
+  SVN_ERR(err);
+
+  conflict->resolution_tree = option_id;
+
+  return SVN_NO_ERROR;
+}
+
 /* Resolver options for a text conflict */
 static const svn_client_conflict_option_t text_conflict_options[] =
 {
@@ -4753,6 +5936,200 @@ configure_option_merge_incoming_added_di
   return SVN_NO_ERROR;
 }
 
+/* Configure 'incoming added dir replace' resolution option for a tree
+ * conflict. */
+static svn_error_t *
+configure_option_merge_incoming_added_dir_replace(
+  svn_client_conflict_t *conflict,
+  apr_array_header_t *options,
+  apr_pool_t *scratch_pool)
+{
+  svn_wc_operation_t operation;
+  svn_wc_conflict_action_t incoming_change;
+  svn_wc_conflict_reason_t local_change;
+  svn_node_kind_t victim_node_kind;
+  const char *incoming_new_repos_relpath;
+  svn_revnum_t incoming_new_pegrev;
+  svn_node_kind_t incoming_new_kind;
+
+  operation = svn_client_conflict_get_operation(conflict);
+  incoming_change = svn_client_conflict_get_incoming_change(conflict);
+  local_change = svn_client_conflict_get_local_change(conflict);
+  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
+  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
+            &incoming_new_repos_relpath, &incoming_new_pegrev,
+            &incoming_new_kind, conflict, scratch_pool,
+            scratch_pool));
+
+  if (operation == svn_wc_operation_merge &&
+      victim_node_kind == svn_node_dir &&
+      incoming_new_kind == svn_node_dir &&
+      incoming_change == svn_wc_conflict_action_add &&
+      local_change == svn_wc_conflict_reason_obstructed)
+    {
+      svn_client_conflict_option_t *option;
+      const char *wcroot_abspath;
+

[... 179 lines stripped ...]