You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by pr...@apache.org on 2013/02/13 11:21:36 UTC

svn commit: r1445542 [4/16] - in /subversion/branches/verify-keep-going: ./ build/generator/ build/generator/swig/ build/generator/templates/ build/win32/ contrib/server-side/fsfsfixer/fixer/ contrib/server-side/svncutter/ notes/ notes/api-errata/1.7/ ...

Modified: subversion/branches/verify-keep-going/subversion/libsvn_client/merge.c
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/libsvn_client/merge.c?rev=1445542&r1=1445541&r2=1445542&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/libsvn_client/merge.c (original)
+++ subversion/branches/verify-keep-going/subversion/libsvn_client/merge.c Wed Feb 13 10:21:33 2013
@@ -64,6 +64,7 @@
 
 #include "svn_private_config.h"
 
+
 /*-----------------------------------------------------------------------*/
 
 /* MERGEINFO MERGE SOURCE NORMALIZATION
@@ -197,9 +198,9 @@
  * merge target and any of its subtrees which have explicit mergeinfo
  * or otherwise need special attention during a merge.
  *
- * During mergeinfo unaware merges, CHILDREN_WITH_MERGEINFO is created by
- * do_mergeinfo_unaware_dir_merge and contains only one element describing
- * a contiguous range to be merged to the WC merge target.
+ * During mergeinfo unaware merges, CHILDREN_WITH_MERGEINFO contains
+ * contains only one element (added by do_mergeinfo_unaware_dir_merge)
+ * describing a contiguous range to be merged to the WC merge target.
  *
  * During mergeinfo aware merges CHILDREN_WITH_MERGEINFO is created
  * by get_mergeinfo_paths() and outside of that function and its helpers
@@ -234,9 +235,6 @@ typedef struct merge_target_t
   /* Absolute path to the WC node */
   const char *abspath;
 
-  /* Node kind of the WC node (at the start of the merge) */
-  svn_node_kind_t kind;
-
   /* The repository location of the base node of the target WC.  If the node
    * is locally added, then URL & REV are NULL & SVN_INVALID_REVNUM.
    * REPOS_ROOT_URL and REPOS_UUID are always valid. */
@@ -278,14 +276,6 @@ typedef struct merge_cmd_baton_t {
 
   svn_client_ctx_t *ctx;              /* Client context for callbacks, etc. */
 
-  /* The list of paths for nodes we've deleted, used only when in
-     dry_run mode. */
-  apr_hash_t *dry_run_deletions;
-
-  /* The list of paths for nodes we've added, used only when in
-     dry_run mode. */
-  apr_hash_t *dry_run_added;
-
   /* The list of any paths which remained in conflict after a
      resolution attempt was made.  We track this in-memory, rather
      than just using WC entry state, since the latter doesn't help us
@@ -352,36 +342,17 @@ typedef struct merge_cmd_baton_t {
      or do_file_merge() in do_merge(). */
   apr_pool_t *pool;
 
-  /* Contains any state collected while receiving path notifications. */
-  struct notification_receiver_baton_t
+
+  /* State for notify_merge_begin() */
+  struct notify_begin_state_t
   {
-    /* The wrapped callback and baton. */
-    svn_wc_notify_func2_t wrapped_func;
-    void *wrapped_baton;
-
-    /* The number of operative notifications received. */
-    apr_uint32_t nbr_operative_notifications;
-
-    /* Flag indicating whether it is a single file merge or not. */
-    svn_boolean_t is_single_file_merge;
-
-    /* Depth first ordered list of paths that needs special care while merging.
-       ### And ...? This is not just a list of paths. See the global comment
-           'THE CHILDREN_WITH_MERGEINFO ARRAY'.
-       This defaults to NULL. For 'same_url' merge alone we set it to
-       proper array. This is used by notification_receiver to put a
-       merge notification begin lines. */
-    apr_array_header_t *children_with_mergeinfo;
-
-    /* The path in CHILDREN_WITH_MERGEINFO where we found the nearest ancestor
-       for merged path. Default value is null. */
-    const char *cur_ancestor_abspath;
-
-    /* Pool with a sufficient lifetime to be used for output members such as
-     * MERGED_ABSPATHS. */
-    apr_pool_t *pool;
+    /* Cache of which abspath was last notified. */
+    const char *last_abspath;
 
-  } nrb;
+    /* Reference to the one-and-only CHILDREN_WITH_MERGEINFO (see global
+       comment) or a similar list for single-file-merges */
+    const apr_array_header_t *nodes_with_mergeinfo;
+  } notify_begin;
 
 } merge_cmd_baton_t;
 
@@ -515,7 +486,7 @@ store_path(apr_hash_t *path_hash, const 
   const char *dup_path = apr_pstrdup(apr_hash_pool_get(path_hash),
                                      local_abspath);
 
-  apr_hash_set(path_hash, dup_path, APR_HASH_KEY_STRING, dup_path);
+  svn_hash_sets(path_hash, dup_path, dup_path);
 }
 
 /* Store LOCAL_ABSPATH in *PATH_HASH_P after duplicating it into the pool
@@ -531,41 +502,6 @@ alloc_and_store_path(apr_hash_t **path_h
   store_path(*path_hash_p, local_abspath);
 }
 
-/* Helper function to easy in checking if a path is in a path hash */
-static APR_INLINE svn_boolean_t
-contains_path(apr_hash_t *path_hash, const char *local_abspath)
-{
-  return apr_hash_get(path_hash, local_abspath, APR_HASH_KEY_STRING) != NULL;
-}
-
-/* Return true iff we're in dry-run mode and LOCAL_ABSPATH would have been
-   deleted by now if we weren't in dry-run mode.
-   Used to avoid spurious notifications (e.g. conflicts) from a merge
-   attempt into an existing target which would have been deleted if we
-   weren't in dry_run mode (issue #2584). */
-static APR_INLINE svn_boolean_t
-dry_run_deleted_p(const merge_cmd_baton_t *merge_b,
-                  const char *local_abspath)
-{
-  return (merge_b->dry_run &&
-          apr_hash_get(merge_b->dry_run_deletions, local_abspath,
-                       APR_HASH_KEY_STRING) != NULL);
-}
-
-/* Return true iff we're in dry-run mode and LOCAL_ABSPATH would have been
-   added by now if we weren't in dry-run mode.
-   Used to avoid spurious notifications (e.g. conflicts) from a merge
-   attempt into an existing target which would have been deleted if we
-   weren't in dry_run mode (issue #2584). */
-static APR_INLINE svn_boolean_t
-dry_run_added_p(const merge_cmd_baton_t *merge_b,
-                const char *local_abspath)
-{
-  return (merge_b->dry_run &&
-          apr_hash_get(merge_b->dry_run_added, local_abspath,
-                       APR_HASH_KEY_STRING) != NULL);
-}
-
 /* Return whether any WC path was put in conflict by the merge
    operation corresponding to MERGE_B. */
 static APR_INLINE svn_boolean_t
@@ -593,10 +529,11 @@ is_path_conflicted_by_merge(merge_cmd_ba
 static svn_error_t *
 perform_obstruction_check(svn_wc_notify_state_t *obstruction_state,
                           svn_boolean_t *deleted,
+                          svn_boolean_t *excluded,
                           svn_node_kind_t *kind,
+                          svn_depth_t *parent_depth,
                           const merge_cmd_baton_t *merge_b,
                           const char *local_abspath,
-                          svn_node_kind_t expected_kind,
                           apr_pool_t *scratch_pool)
 {
   svn_wc_context_t *wc_ctx = merge_b->ctx->wc_ctx;
@@ -612,42 +549,6 @@ perform_obstruction_check(svn_wc_notify_
   if (kind)
     *kind = svn_node_none;
 
-  /* In a dry run, make as if nodes "deleted" by the dry run appear so. */
-  if (merge_b->dry_run)
-    {
-      if (dry_run_deleted_p(merge_b, local_abspath))
-        {
-          /* svn_wc_notify_state_inapplicable */
-          /* svn_node_none */
-
-          if (deleted)
-            *deleted = TRUE;
-
-          if (expected_kind != svn_node_unknown
-              && expected_kind != svn_node_none)
-            *obstruction_state = svn_wc_notify_state_obstructed;
-          return SVN_NO_ERROR;
-        }
-      else if (dry_run_added_p(merge_b, local_abspath))
-        {
-          /* svn_wc_notify_state_inapplicable */
-
-          if (kind)
-            *kind = svn_node_dir; /* Currently only used for dirs */
-
-          return SVN_NO_ERROR;
-        }
-      else if (dry_run_added_p(merge_b,
-                               svn_dirent_dirname(local_abspath,
-                                                  scratch_pool)))
-        {
-          /* svn_wc_notify_state_inapplicable */
-          /* svn_node_none */
-
-          return SVN_NO_ERROR;
-        }
-     }
-
   if (kind == NULL)
     kind = &wc_kind;
 
@@ -656,24 +557,18 @@ perform_obstruction_check(svn_wc_notify_
   SVN_ERR(svn_wc__check_for_obstructions(obstruction_state,
                                          kind,
                                          deleted,
+                                         excluded,
+                                         parent_depth,
                                          wc_ctx, local_abspath,
                                          check_root,
                                          scratch_pool));
-
-  if (*obstruction_state == svn_wc_notify_state_inapplicable
-      && expected_kind != svn_node_unknown
-      && *kind != expected_kind)
-    {
-      *obstruction_state = svn_wc_notify_state_obstructed;
-    }
-
   return SVN_NO_ERROR;
 }
 
 /* Create *LEFT and *RIGHT conflict versions for conflict victim
  * at VICTIM_ABSPATH, with kind NODE_KIND, using information obtained
  * from MERGE_SOURCE and TARGET.
- * Allocate returned conflict versions in POOL. */
+ * Allocate returned conflict versions in RESULT_POOL. */
 static svn_error_t *
 make_conflict_versions(const svn_wc_conflict_version_t **left,
                        const svn_wc_conflict_version_t **right,
@@ -681,7 +576,8 @@ make_conflict_versions(const svn_wc_conf
                        svn_node_kind_t node_kind,
                        const merge_source_t *merge_source,
                        const merge_target_t *target,
-                       apr_pool_t *pool)
+                       apr_pool_t *result_pool,
+                       apr_pool_t *scratch_pool)
 {
   const char *child = svn_dirent_skip_ancestor(target->abspath,
                                                victim_abspath);
@@ -689,182 +585,25 @@ make_conflict_versions(const svn_wc_conf
 
   SVN_ERR_ASSERT(child != NULL);
   left_relpath = svn_client__pathrev_relpath(merge_source->loc1,
-                                             pool);
+                                             scratch_pool);
   right_relpath = svn_client__pathrev_relpath(merge_source->loc2,
-                                              pool);
+                                              scratch_pool);
 
   *left = svn_wc_conflict_version_create2(
             merge_source->loc1->repos_root_url,
             merge_source->loc1->repos_uuid,
-            svn_relpath_join(left_relpath, child, pool),
-            merge_source->loc1->rev, node_kind, pool);
+            svn_relpath_join(left_relpath, child, scratch_pool),
+            merge_source->loc1->rev, node_kind, result_pool);
 
   *right = svn_wc_conflict_version_create2(
              merge_source->loc2->repos_root_url,
              merge_source->loc2->repos_uuid,
-             svn_relpath_join(right_relpath, child, pool),
-             merge_source->loc2->rev, node_kind, pool);
-
-  return SVN_NO_ERROR;
-}
-
-/* Set *CONFLICT to a new tree-conflict description allocated in POOL,
- * populated with information from the other parameters.
- * See tree_conflict() for the other parameters.
- */
-static svn_error_t*
-make_tree_conflict(svn_wc_conflict_description2_t **conflict,
-                   const char *victim_abspath,
-                   svn_node_kind_t node_kind,
-                   svn_wc_conflict_action_t action,
-                   svn_wc_conflict_reason_t reason,
-                   const merge_source_t *merge_source,
-                   const merge_target_t *target,
-                   apr_pool_t *pool)
-{
-  const svn_wc_conflict_version_t *left;
-  const svn_wc_conflict_version_t *right;
-
-  SVN_ERR(make_conflict_versions(&left, &right, victim_abspath, node_kind,
-                                 merge_source, target, pool));
-
-  *conflict = svn_wc_conflict_description_create_tree2(
-                    victim_abspath, node_kind, svn_wc_operation_merge,
-                    left, right, pool);
-
-  (*conflict)->action = action;
-  (*conflict)->reason = reason;
-
-  return SVN_NO_ERROR;
-}
-
-/* Record a tree conflict in the WC, unless this is a dry run or a record-
- * only merge, or if a tree conflict is already flagged for the VICTIM_PATH.
- * (The latter can happen if a merge-tracking-aware merge is doing multiple
- * editor drives because of a gap in the range of eligible revisions.)
- *
- * The tree conflict, with its victim specified by VICTIM_PATH, is
- * assumed to have happened during a merge using merge baton MERGE_B.
- *
- * NODE_KIND must be the node kind of "old" and "theirs" and "mine";
- * this function cannot cope with node kind clashes.
- * ACTION and REASON correspond to the fields
- * of the same names in svn_wc_tree_conflict_description_t.
- */
-static svn_error_t*
-tree_conflict(merge_cmd_baton_t *merge_b,
-              const char *victim_abspath,
-              svn_node_kind_t node_kind,
-              svn_wc_conflict_action_t action,
-              svn_wc_conflict_reason_t reason)
-{
-  const svn_wc_conflict_description2_t *existing_conflict;
-  svn_error_t *err;
-
-  if (merge_b->record_only || merge_b->dry_run)
-    return SVN_NO_ERROR;
-
-  err = svn_wc__get_tree_conflict(&existing_conflict, merge_b->ctx->wc_ctx,
-                                  victim_abspath, merge_b->pool,
-                                  merge_b->pool);
-
-  if (err)
-    {
-      if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND)
-        return svn_error_trace(err);
-
-      svn_error_clear(err);
-      existing_conflict = FALSE;
-    }
-
-  if (existing_conflict == NULL)
-    {
-      svn_wc_conflict_description2_t *conflict;
-
-      /* There is no existing tree conflict so it is safe to add one. */
-      SVN_ERR(make_tree_conflict(&conflict, victim_abspath,
-                                 node_kind, action, reason,
-                                 &merge_b->merge_source, merge_b->target,
-                                 merge_b->pool));
-      SVN_ERR(svn_wc__add_tree_conflict(merge_b->ctx->wc_ctx, conflict,
-                                        merge_b->pool));
-
-      alloc_and_store_path(&merge_b->conflicted_paths, victim_abspath,
-                           merge_b->pool);
-    }
-
-  return SVN_NO_ERROR;
-}
-
-/* Similar to tree_conflict(), but if this is an "add" action and there
-   is an existing tree conflict on the victim with a "delete" action, then
-   combine the two conflicts into a single conflict with a "replace" action. */
-static svn_error_t*
-tree_conflict_on_add(merge_cmd_baton_t *merge_b,
-                     const char *victim_abspath,
-                     svn_node_kind_t node_kind,
-                     svn_wc_conflict_action_t action,
-                     svn_wc_conflict_reason_t reason)
-{
-  const svn_wc_conflict_description2_t *existing_conflict;
-  svn_wc_conflict_description2_t *conflict;
+             svn_relpath_join(right_relpath, child, scratch_pool),
+             merge_source->loc2->rev, node_kind, result_pool);
 
-  if (merge_b->record_only || merge_b->dry_run)
-    return SVN_NO_ERROR;
-
-  /* Construct the new conflict first  compare the new conflict with
-     a possibly existing one. */
-  SVN_ERR(make_tree_conflict(&conflict, victim_abspath,
-                             node_kind, action, reason,
-                             &merge_b->merge_source, merge_b->target,
-                             merge_b->pool));
-
-  SVN_ERR(svn_wc__get_tree_conflict(&existing_conflict, merge_b->ctx->wc_ctx,
-                                    victim_abspath, merge_b->pool,
-                                    merge_b->pool));
-
-  if (existing_conflict == NULL)
-    {
-      /* There is no existing tree conflict so it is safe to add one. */
-      SVN_ERR(svn_wc__add_tree_conflict(merge_b->ctx->wc_ctx, conflict,
-                                        merge_b->pool));
-
-      alloc_and_store_path(&merge_b->conflicted_paths, victim_abspath,
-                           merge_b->pool);
-    }
-  else if (existing_conflict->action == svn_wc_conflict_action_delete &&
-           conflict->action == svn_wc_conflict_action_add)
-    {
-      /* There is already a tree conflict raised by a previous incoming
-       * change that attempted to delete the item (whether in this same
-       * merge operation or not). Change the existing conflict to note
-       * that the incoming change is replacement. */
-
-      /* Remove the existing tree-conflict so we can add a new one.*/
-      SVN_ERR(svn_wc__del_tree_conflict(merge_b->ctx->wc_ctx,
-                                        victim_abspath, merge_b->pool));
-
-      /* Preserve the reason which caused the first conflict,
-       * re-label the incoming change as 'replacement', and update
-       * version info for the left version of the conflict. */
-      conflict->reason = existing_conflict->reason;
-      conflict->action = svn_wc_conflict_action_replace;
-      conflict->src_left_version = svn_wc_conflict_version_dup(
-                                     existing_conflict->src_left_version,
-                                     merge_b->pool);
-
-      SVN_ERR(svn_wc__add_tree_conflict(merge_b->ctx->wc_ctx, conflict,
-                                        merge_b->pool));
-
-      alloc_and_store_path(&merge_b->conflicted_paths, victim_abspath,
-                           merge_b->pool);
-    }
-
-  /* In any other cases, we don't touch the existing conflict. */
   return SVN_NO_ERROR;
 }
 
-
 /* Helper for filter_self_referential_mergeinfo()
 
    *MERGEINFO is a non-empty, non-null collection of mergeinfo.
@@ -938,8 +677,8 @@ split_mergeinfo_on_revision(svn_mergeinf
                  ranges from *MERGEINFO */
               if (!(*younger_mergeinfo))
                 *younger_mergeinfo = apr_hash_make(pool);
-              apr_hash_set(*younger_mergeinfo, merge_source_path,
-                           APR_HASH_KEY_STRING, younger_rangelist);
+              svn_hash_sets(*younger_mergeinfo, merge_source_path,
+                            younger_rangelist);
               SVN_ERR(svn_mergeinfo_remove2(mergeinfo, *younger_mergeinfo,
                                             *mergeinfo, TRUE, pool, iterpool));
               break; /* ...out of for (i = 0; i < rangelist->nelts; i++) */
@@ -1209,8 +948,8 @@ filter_self_referential_mergeinfo(apr_ar
                 {
                   if (!filtered_younger_mergeinfo)
                     filtered_younger_mergeinfo = apr_hash_make(iterpool);
-                  apr_hash_set(filtered_younger_mergeinfo, source_path,
-                               APR_HASH_KEY_STRING, adjusted_rangelist);
+                  svn_hash_sets(filtered_younger_mergeinfo, source_path,
+                                adjusted_rangelist);
                 }
 
             } /* Iteration over each merge source in younger_mergeinfo. */
@@ -1341,10 +1080,10 @@ prepare_merge_props_changed(const apr_ar
     }
   *prop_updates = props;
 
-  /* If this is not a dry run then make a record in BATON if we find a
-     PATH where mergeinfo is added where none existed previously or PATH
-     is having its existing mergeinfo deleted. */
-  if (!merge_b->dry_run && props->nelts)
+  /* Make a record in BATON if we find a PATH where mergeinfo is added
+     where none existed previously or PATH is having its existing
+     mergeinfo deleted. */
+  if (props->nelts)
     {
       int i;
 
@@ -1365,8 +1104,7 @@ prepare_merge_props_changed(const apr_ar
                                                 scratch_pool));
 
               if (pristine_props
-                  && apr_hash_get(pristine_props, SVN_PROP_MERGEINFO,
-                                  APR_HASH_KEY_STRING))
+                  && svn_hash_gets(pristine_props, SVN_PROP_MERGEINFO))
                 has_pristine_mergeinfo = TRUE;
 
               if (!has_pristine_mergeinfo && prop->value)
@@ -1386,132 +1124,286 @@ prepare_merge_props_changed(const apr_ar
   return SVN_NO_ERROR;
 }
 
-/* Indicate in *MOVED_AWAY whether the node at LOCAL_ABSPATH was
- * moved away locally. Do not raise an error if the node at LOCAL_ABSPATH
- * does not exist. */
-static svn_error_t *
-check_moved_away(svn_boolean_t *moved_away,
-                 svn_wc_context_t *wc_ctx,
-                 const char *local_abspath,
-                 apr_pool_t *scratch_pool)
-{
-  const char *moved_to_abspath;
+#define CONFLICT_REASON_NONE       ((svn_wc_conflict_reason_t)-1)
+#define CONFLICT_REASON_SKIP       ((svn_wc_conflict_reason_t)-2)
+#define CONFLICT_REASON_SKIP_WC    ((svn_wc_conflict_reason_t)-3)
+
+/* Baton used for testing trees for being editted while performing tree
+   conflict detection for incoming deletes */
+struct dir_delete_baton_t
+{
+  /* Reference to dir baton of directory that is the root of the deletion */
+  struct merge_dir_baton_t *del_root;
+
+  /* Boolean indicating that some edit is found. Allows avoiding more work */
+  svn_boolean_t found_edit;
+
+  /* A list of paths that are compared. Kept up to date until FOUND_EDIT is
+     set to TRUE */
+  apr_hash_t *compared_abspaths;
+};
 
-  SVN_ERR(svn_wc__node_was_moved_away(&moved_to_abspath, NULL,
-                                      wc_ctx, local_abspath,
-                                      scratch_pool, scratch_pool));
-  
-  *moved_away = (moved_to_abspath != NULL);
+/* Baton for the merge_dir_*() functions. Initialized in merge_dir_opened() */
+struct merge_dir_baton_t
+{
+  /* Reference to the parent baton, unless the parent is the anchor, in which
+     case PARENT_BATON is NULL */
+  struct merge_dir_baton_t *parent_baton;
 
-  return SVN_NO_ERROR;
-}
+  /* The pool containing this baton. Use for RESULT_POOL for storing in this
+     baton */
+  apr_pool_t *pool;
 
-/* Indicate in *MOVED_HERE whether the node at LOCAL_ABSPATH was
- * moved here locally. Do not raise an error if the node at LOCAL_ABSPATH
- * does not exist. */
-static svn_error_t *
-check_moved_here(svn_boolean_t *moved_here,
-                 svn_wc_context_t *wc_ctx,
-                 const char *local_abspath,
-                 apr_pool_t *scratch_pool)
-{
-  const char *moved_from_abspath;
-  svn_error_t *err;
+  /* This directory doesn't have a representation in the working copy, so any
+     operation on it will be skipped and possibly cause a tree conflict on the
+     shadow root */
+  svn_boolean_t shadowed;
+
+  /* This node or one of its descendants received operational changes from the
+     merge. If this node is the shadow root its tree conflict status has been
+     applied */
+  svn_boolean_t edited;
+
+  /* If a tree conflict will be installed once edited, it's reason. If a skip
+     should be produced its reason. Otherwise CONFLICT_REASON_NONE for no tree
+     conflict.
+
+     Special values:
+       CONFLICT_REASON_SKIP:
+            The node will be skipped with content and property state as stored in
+            SKIP_REASON.
 
-  *moved_here = FALSE;
+       CONFLICT_REASON_SKIP_WC:
+            The node will be skipped as an obstructing working copy.
+   */
+  svn_wc_conflict_reason_t tree_conflict_reason;
+  svn_wc_conflict_action_t tree_conflict_action;
 
-  err = svn_wc__node_was_moved_here(&moved_from_abspath, NULL,
-                                    wc_ctx, local_abspath,
-                                    scratch_pool, scratch_pool);
-  if (err)
-    {
-      if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
-        svn_error_clear(err);
-      else
-        return svn_error_trace(err);
-    }
-  else if (moved_from_abspath)
-    *moved_here = TRUE;
+  /* When TREE_CONFLICT_REASON is CONFLICT_REASON_SKIP, the skip state to
+     add to the notification */
+  svn_wc_notify_state_t skip_reason;
+
+  /* TRUE if the node was added by this merge. Otherwise FALSE */
+  svn_boolean_t added;
+  svn_boolean_t add_is_replace; /* Add is second part of replace */
+
+  /* TRUE if we are taking over an existing directory as addition, otherwise
+     FALSE. */
+  svn_boolean_t add_existing;
+
+  /* NULL, or an hashtable mapping const char * local_abspaths to
+     const char *kind mapping, containing deleted nodes that still need a delete
+     notification (which may be a replaced notification if the node is not just
+     deleted) */
+  apr_hash_t *pending_deletes;
+
+  /* NULL, or an hashtable mapping const char * LOCAL_ABSPATHs to
+     a const svn_wc_conflict_description2_t * instance, describing the just
+     installed conflict */
+  apr_hash_t *new_tree_conflicts;
+
+  /* If not NULL, a reference to the information of the delete test that is
+     currently in progress. Allocated in the root-directory baton, referenced
+     from all descendants */
+  struct dir_delete_baton_t *delete_state;
+};
 
-  return SVN_NO_ERROR;
-}
+/* Baton for the merge_dir_*() functions. Initialized in merge_file_opened() */
+struct merge_file_baton_t
+{
+  /* Reference to the parent baton, unless the parent is the anchor, in which
+     case PARENT_BATON is NULL */
+  struct merge_dir_baton_t *parent_baton;
+
+  /* This file doesn't have a representation in the working copy, so any
+     operation on it will be skipped and possibly cause a tree conflict
+     on the shadow root */
+  svn_boolean_t shadowed;
+
+  /* This node received operational changes from the merge. If this node
+     is the shadow root its tree conflict status has been applied */
+  svn_boolean_t edited;
+
+  /* If a tree conflict will be installed once edited, it's reason. If a skip
+     should be produced its reason. Some special values are defined. See the
+     merge_tree_baton_t for an explanation. */
+  svn_wc_conflict_reason_t tree_conflict_reason;
+  svn_wc_conflict_action_t tree_conflict_action;
+
+  /* When TREE_CONFLICT_REASON is CONFLICT_REASON_SKIP, the skip state to
+     add to the notification */
+  svn_wc_notify_state_t skip_reason;
+
+  /* TRUE if the node was added by this merge. Otherwise FALSE */
+  svn_boolean_t added;
+  svn_boolean_t add_is_replace; /* Add is second part of replace */
+};
 
-/* #defined HANDLE_NOTIFY_FROM_MERGE */
+/* Forward declaration */
+static svn_error_t *
+notify_merge_begin(merge_cmd_baton_t *merge_b,
+                   const char *local_abspath,
+                   svn_boolean_t delete_action,
+                   apr_pool_t *scratch_pool);
 
 /* Record the skip for future processing and (later) produce the
    skip notification */
 static svn_error_t *
-record_skip(const merge_cmd_baton_t *merge_b,
+record_skip(merge_cmd_baton_t *merge_b,
             const char *local_abspath,
             svn_node_kind_t kind,
+            svn_wc_notify_action_t action,
             svn_wc_notify_state_t state,
             apr_pool_t *scratch_pool)
 {
+  if (merge_b->record_only)
+    return SVN_NO_ERROR; /* ### Why? - Legacy compatibility */
+
   if (merge_b->merge_source.ancestral
       || merge_b->reintegrate_merge)
     {
       store_path(merge_b->skipped_abspaths, local_abspath);
     }
 
-#ifdef HANDLE_NOTIFY_FROM_MERGE
   if (merge_b->ctx->notify_func2)
     {
       svn_wc_notify_t *notify;
 
-      notify = svn_wc_create_notify(local_abspath, svn_wc_notify_skip,
-                                    scratch_pool);
+      SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
+
+      notify = svn_wc_create_notify(local_abspath, action, scratch_pool);
       notify->kind = kind;
       notify->content_state = notify->prop_state = state;
 
       (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
                                     scratch_pool);
     }
-#endif
   return SVN_NO_ERROR;
 }
 
-/* Record the skip for future processing and (later) produce the
-   tree conflict notification */
+/* Record a tree conflict in the WC, unless this is a dry run or a record-
+ * only merge, or if a tree conflict is already flagged for the VICTIM_PATH.
+ * (The latter can happen if a merge-tracking-aware merge is doing multiple
+ * editor drives because of a gap in the range of eligible revisions.)
+ *
+ * The tree conflict, with its victim specified by VICTIM_PATH, is
+ * assumed to have happened during a merge using merge baton MERGE_B.
+ *
+ * NODE_KIND must be the node kind of "old" and "theirs" and "mine";
+ * this function cannot cope with node kind clashes.
+ * ACTION and REASON correspond to the fields
+ * of the same names in svn_wc_tree_conflict_description_t.
+ */
 static svn_error_t *
-record_tree_conflict(const merge_cmd_baton_t *merge_b,
+record_tree_conflict(merge_cmd_baton_t *merge_b,
                      const char *local_abspath,
-                     svn_node_kind_t kind,
+                     struct merge_dir_baton_t *parent_baton,
+                     svn_node_kind_t node_kind,
+                     svn_wc_conflict_action_t action,
+                     svn_wc_conflict_reason_t reason,
+                     const svn_wc_conflict_description2_t *existing_conflict,
+                     svn_boolean_t notify_tc,
                      apr_pool_t *scratch_pool)
 {
-#ifdef HANDLE_NOTIFY_FROM_MERGE
+  svn_wc_context_t *wc_ctx = merge_b->ctx->wc_ctx;
+
+  if (merge_b->merge_source.ancestral
+      || merge_b->reintegrate_merge)
+    {
+      store_path(merge_b->tree_conflicted_abspaths, local_abspath);
+    }
+
+  alloc_and_store_path(&merge_b->conflicted_paths, local_abspath,
+                       merge_b->pool);
+
+
+  if (!merge_b->record_only && !merge_b->dry_run)
+    {
+       svn_wc_conflict_description2_t *conflict;
+       const svn_wc_conflict_version_t *left;
+       const svn_wc_conflict_version_t *right;
+       apr_pool_t *result_pool = parent_baton ? parent_baton->pool
+                                              : scratch_pool;
+
+      if (reason == svn_wc_conflict_reason_deleted)
+        {
+          const char *moved_to_abspath;
+
+          SVN_ERR(svn_wc__node_was_moved_away(&moved_to_abspath, NULL,
+                                              wc_ctx, local_abspath,
+                                              scratch_pool, scratch_pool));
+
+          if (moved_to_abspath)
+            {
+              /* Local abspath itself has been moved away. If only a
+                 descendant is moved away, we call the node itself deleted */
+              reason = svn_wc_conflict_reason_moved_away;
+            }
+        }
+      else if (reason == svn_wc_conflict_reason_added)
+        {
+          const char *moved_from_abspath;
+          SVN_ERR(svn_wc__node_was_moved_here(&moved_from_abspath, NULL,
+                                              wc_ctx, local_abspath,
+                                              scratch_pool, scratch_pool));
+          if (moved_from_abspath)
+            reason = svn_wc_conflict_reason_moved_here;
+        }
+
+      SVN_ERR(make_conflict_versions(&left, &right, local_abspath, node_kind,
+                                     &merge_b->merge_source, merge_b->target,
+                                     result_pool, scratch_pool));
+
+      /* Fix up delete of file, add of dir replacement (or other way around) */
+      if (existing_conflict != NULL && existing_conflict->src_left_version)
+          left = existing_conflict->src_left_version;
+
+      conflict = svn_wc_conflict_description_create_tree2(
+                        local_abspath, node_kind, svn_wc_operation_merge,
+                        left, right, result_pool);
+
+      conflict->action = action;
+      conflict->reason = reason;
+
+      /* May return SVN_ERR_WC_PATH_UNEXPECTED_STATUS */
+      if (existing_conflict)
+        SVN_ERR(svn_wc__del_tree_conflict(wc_ctx, local_abspath,
+                                          scratch_pool));
+
+      SVN_ERR(svn_wc__add_tree_conflict(merge_b->ctx->wc_ctx, conflict,
+                                        scratch_pool));
+
+      if (parent_baton)
+        {
+          if (! parent_baton->new_tree_conflicts)
+            parent_baton->new_tree_conflicts = apr_hash_make(result_pool);
+
+          svn_hash_sets(parent_baton->new_tree_conflicts,
+                        apr_pstrdup(result_pool, local_abspath),
+                        conflict);
+        }
+
+      /* ### TODO: Store in parent baton */
+    }
+
   /* On a replacement we currently get two tree conflicts */
-  if (merge_b->ctx->notify_func2
-      && !contains_path(merge_b->tree_conflicted_abspaths,
-                        local_abspath))
+  if (merge_b->ctx->notify_func2 && notify_tc)
     {
       svn_wc_notify_t *notify;
 
+      SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
+
       notify = svn_wc_create_notify(local_abspath, svn_wc_notify_tree_conflict,
                                     scratch_pool);
-      notify->kind = kind;
+      notify->kind = node_kind;
 
       (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
                                     scratch_pool);
     }
-#endif
-
-  if (merge_b->merge_source.ancestral
-      || merge_b->reintegrate_merge)
-    {
-      store_path(merge_b->tree_conflicted_abspaths, local_abspath);
-    }
 
   return SVN_NO_ERROR;
 }
 
-/* Forward declaration */
-static svn_error_t *
-record_operative_merge_action(merge_cmd_baton_t *merge_b,
-                              const char *local_abspath,
-                              svn_boolean_t delete_action,
-                              apr_pool_t *scratch_pool);
-
-
 /* Record the add for future processing and (later) produce the
    update_add notification
 
@@ -1522,63 +1414,30 @@ static svn_error_t *
 record_update_add(merge_cmd_baton_t *merge_b,
                   const char *local_abspath,
                   svn_node_kind_t kind,
+                  svn_boolean_t notify_replaced,
                   apr_pool_t *scratch_pool)
 {
-  svn_boolean_t root_of_added_subtree = TRUE;
-
   if (merge_b->merge_source.ancestral || merge_b->reintegrate_merge)
     {
-      /* Stash the root path of any added subtrees. */
-      if (merge_b->added_abspaths == NULL)
-        {
-          /* The first added path is always a root. */
-          merge_b->added_abspaths = apr_hash_make(merge_b->pool);
-        }
-      else
-        {
-          const char *added_path_dir = svn_dirent_dirname(local_abspath,
-                                                          scratch_pool);
-
-          /* Is NOTIFY->PATH the root of an added subtree? */
-          while (strcmp(merge_b->target->abspath, added_path_dir))
-            {
-              if (contains_path(merge_b->added_abspaths, added_path_dir))
-                {
-                  root_of_added_subtree = FALSE;
-                  break;
-                }
-
-              if (svn_dirent_is_root(added_path_dir, strlen(added_path_dir)))
-                break;
-              added_path_dir = svn_dirent_dirname(added_path_dir,
-                                                  scratch_pool);
-            }
-        }
-
-      if (root_of_added_subtree)
-        {
-          store_path(merge_b->added_abspaths, local_abspath);
-        }
-
       store_path(merge_b->merged_abspaths, local_abspath);
     }
 
-  SVN_ERR(record_operative_merge_action(merge_b, local_abspath, FALSE,
-                                        scratch_pool));
-
-#ifdef HANDLE_NOTIFY_FROM_MERGE
   if (merge_b->ctx->notify_func2)
     {
       svn_wc_notify_t *notify;
+      svn_wc_notify_action_t action = svn_wc_notify_update_add;
 
-      notify = svn_wc_create_notify(local_abspath, svn_wc_notify_update_add,
-                                    scratch_pool);
+      SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
+
+      if (notify_replaced)
+        action = svn_wc_notify_update_replace;
+
+      notify = svn_wc_create_notify(local_abspath, action, scratch_pool);
       notify->kind = kind;
 
       (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
                                     scratch_pool);
     }
-#endif
 
   return SVN_NO_ERROR;
 }
@@ -1598,14 +1457,12 @@ record_update_update(merge_cmd_baton_t *
       store_path(merge_b->merged_abspaths, local_abspath);
     }
 
-  SVN_ERR(record_operative_merge_action(merge_b, local_abspath,
-                                        FALSE, scratch_pool));
-
-#ifdef HANDLE_NOTIFY_FROM_MERGE
   if (merge_b->ctx->notify_func2)
     {
       svn_wc_notify_t *notify;
 
+      SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
+
       notify = svn_wc_create_notify(local_abspath, svn_wc_notify_update_update,
                                     scratch_pool);
       notify->kind = kind;
@@ -1615,7 +1472,6 @@ record_update_update(merge_cmd_baton_t *
       (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
                                     scratch_pool);
     }
-#endif
 
   return SVN_NO_ERROR;
 }
@@ -1624,6 +1480,7 @@ record_update_update(merge_cmd_baton_t *
    update_delete notification */
 static svn_error_t *
 record_update_delete(merge_cmd_baton_t *merge_b,
+                     struct merge_dir_baton_t *parent_db,
                      const char *local_abspath,
                      svn_node_kind_t kind,
                      apr_pool_t *scratch_pool)
@@ -1635,416 +1492,518 @@ record_update_delete(merge_cmd_baton_t *
       /* Issue #4166: If a previous merge added NOTIFY_ABSPATH, but we
          are now deleting it, then remove it from the list of added
          paths. */
-      apr_hash_set(merge_b->added_abspaths, local_abspath,
-                   APR_HASH_KEY_STRING, NULL);
+      svn_hash_sets(merge_b->added_abspaths, local_abspath, NULL);
       store_path(merge_b->merged_abspaths, local_abspath);
     }
 
-  SVN_ERR(record_operative_merge_action(merge_b, local_abspath,
-                                        TRUE, scratch_pool));
+  SVN_ERR(notify_merge_begin(merge_b, local_abspath, TRUE, scratch_pool));
 
-#ifdef HANDLE_NOTIFY_FROM_MERGE
-  if (merge_b->ctx->notify_func2)
+  if (parent_db)
     {
-      svn_wc_notify_t *notify;
+      const char *dup_abspath = apr_pstrdup(parent_db->pool, local_abspath);
 
-      notify = svn_wc_create_notify(local_abspath, svn_wc_notify_update_delete,
-                                    scratch_pool);
-      notify->kind = kind;
+      if (!parent_db->pending_deletes)
+        parent_db->pending_deletes = apr_hash_make(parent_db->pool);
 
-      (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
-                                    scratch_pool);
+      svn_hash_sets(parent_db->pending_deletes, dup_abspath,
+                    svn_node_kind_to_word(kind));
     }
-#endif
 
   return SVN_NO_ERROR;
 }
 
-/* An svn_wc_diff_callbacks4_t function. */
+/* Notify the pending 'D'eletes, that were waiting to see if a matching 'A'dd
+   might make them a 'R'eplace. */
 static svn_error_t *
-merge_dir_props_changed(svn_wc_notify_state_t *state,
-                        svn_boolean_t *tree_conflicted,
-                        const char *local_relpath,
-                        svn_boolean_t dir_was_added,
-                        const apr_array_header_t *propchanges,
-                        apr_hash_t *original_props,
-                        void *diff_baton,
-                        apr_pool_t *scratch_pool)
+handle_pending_notifications(merge_cmd_baton_t *merge_b,
+                             struct merge_dir_baton_t *db,
+                             apr_pool_t *scratch_pool)
 {
-  merge_cmd_baton_t *merge_b = diff_baton;
-  const apr_array_header_t *props;
-  const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
-                                              local_relpath, scratch_pool);
-  svn_wc_notify_state_t obstr_state;
-  svn_boolean_t is_deleted;
-  svn_node_kind_t kind;
-
-  SVN_ERR(perform_obstruction_check(&obstr_state, &is_deleted, &kind,
-                                    merge_b, local_abspath, svn_node_dir,
-                                    scratch_pool));
-
-  if (obstr_state != svn_wc_notify_state_inapplicable)
+  if (merge_b->ctx->notify_func2 && db->pending_deletes)
     {
-      *state = obstr_state;
-      SVN_ERR(record_skip(merge_b, local_abspath, svn_node_dir,
-                          obstr_state, scratch_pool));
-      return SVN_NO_ERROR;
-    }
-
-  if (kind != svn_node_dir || is_deleted)
-    {
-      svn_wc_conflict_reason_t reason;
+      apr_hash_index_t *hi;
 
-      if (is_deleted)
+      for (hi = apr_hash_first(scratch_pool, db->pending_deletes);
+           hi;
+           hi = apr_hash_next(hi))
         {
-          svn_boolean_t moved_away;
+          const char *del_abspath = svn__apr_hash_index_key(hi);
+          svn_wc_notify_t *notify;
 
-          SVN_ERR(check_moved_away(&moved_away, merge_b->ctx->wc_ctx,
-                                   local_abspath, scratch_pool));
+          notify = svn_wc_create_notify(del_abspath,
+                                        svn_wc_notify_update_delete,
+                                        scratch_pool);
+          notify->kind = svn_node_kind_from_word(
+                                    svn__apr_hash_index_val(hi));
 
-          if (moved_away)
-            reason = svn_wc_conflict_reason_moved_away;
-          else
-            reason = svn_wc_conflict_reason_deleted;
+          (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2,
+                                        notify, scratch_pool);
         }
-      else
-        reason = svn_wc_conflict_reason_missing;
 
-      SVN_ERR(tree_conflict(merge_b, local_abspath, svn_node_file,
-                            svn_wc_conflict_action_edit, reason));
-
-      *tree_conflicted = TRUE;
-      *state = svn_wc_notify_state_missing;
-      SVN_ERR(record_tree_conflict(merge_b, local_abspath, svn_node_dir,
-                                   scratch_pool));
-
-      return SVN_NO_ERROR;
+      db->pending_deletes = NULL;
     }
+  return SVN_NO_ERROR;
+}
 
-  if (dir_was_added
-      && merge_b->dry_run
-      && dry_run_added_p(merge_b, local_abspath))
-    {
-      return SVN_NO_ERROR; /* We can't do a real prop merge for added dirs */
-    }
+/* Helper function for the merge_dir_*() and merge_file_*() functions.
 
-  if (dir_was_added && merge_b->same_repos)
+   Installs and notifies pre-recorded tree conflicts and skips for
+   ancestors of operational merges
+ */
+static svn_error_t *
+mark_dir_edited(merge_cmd_baton_t *merge_b,
+                struct merge_dir_baton_t *db,
+                const char *local_abspath,
+                apr_pool_t *scratch_pool)
+{
+  /* ### Too much common code with mark_file_edited */
+  if (db->edited)
+    return SVN_NO_ERROR;
+
+  if (db->parent_baton && !db->parent_baton->edited)
     {
-      /* When the directory was added in merge_dir_added() we didn't update its
-         pristine properties. Instead we receive the property changes later and
-         apply them in this function.
+      const char *dir_abspath = svn_dirent_dirname(local_abspath,
+                                                   scratch_pool);
 
-         If we would apply them as changes (such as before fixing issue #3405),
-         we would see the unmodified properties as local changes, and the
-         pristine properties would be out of sync with what the repository
-         expects for this directory.
+      SVN_ERR(mark_dir_edited(merge_b, db->parent_baton, dir_abspath,
+                              scratch_pool));
+    }
 
-         Instead of doing that we now simply set the properties as the pristine
-         properties via a private libsvn_wc api.
-      */
+  db->edited = TRUE;
 
-      const char *copyfrom_url;
-      svn_revnum_t copyfrom_rev;
-      const char *parent_abspath;
-      const char *child;
+  if (! db->shadowed)
+    return SVN_NO_ERROR; /* Easy out */
 
-      /* Creating a hash containing regular and entry props */
-      apr_hash_t *new_pristine_props = svn_prop__patch(original_props, propchanges,
-                                                       scratch_pool);
+  if (db->parent_baton
+      && db->parent_baton->delete_state
+      && db->tree_conflict_reason != CONFLICT_REASON_NONE)
+    {
+      db->parent_baton->delete_state->found_edit = TRUE;
+    }
+  else if (db->tree_conflict_reason == CONFLICT_REASON_SKIP
+           || db->tree_conflict_reason == CONFLICT_REASON_SKIP_WC)
+    {
+      /* open_directory() decided not to flag a tree conflict, but
+         for clarity we produce a skip for this node that
+         most likely isn't touched by the merge itself */
 
-      parent_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
-      child = svn_dirent_is_child(merge_b->target->abspath, local_abspath, NULL);
-      SVN_ERR_ASSERT(child != NULL);
+      if (merge_b->ctx->notify_func2)
+        {
+          svn_wc_notify_t *notify;
 
-      copyfrom_url = svn_path_url_add_component2(merge_b->merge_source.loc2->url,
-                                                 child, scratch_pool);
-      copyfrom_rev = merge_b->merge_source.loc2->rev;
+          SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE,
+                                     scratch_pool));
 
-      SVN_ERR(check_repos_match(merge_b->target, parent_abspath, copyfrom_url,
-                                scratch_pool));
+          notify = svn_wc_create_notify(
+                            local_abspath,
+                            (db->tree_conflict_reason == CONFLICT_REASON_SKIP)
+                                ? svn_wc_notify_skip
+                                : svn_wc_notify_state_obstructed,
+                            scratch_pool);
+          notify->kind = svn_node_dir;
+          notify->content_state = notify->prop_state = db->skip_reason;
 
-      if (!merge_b->dry_run)
-        {
-          SVN_ERR(svn_wc__complete_directory_add(merge_b->ctx->wc_ctx,
-                                                local_abspath,
-                                                new_pristine_props,
-                                                copyfrom_url, copyfrom_rev,
-                                                scratch_pool));
+          (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2,
+                                        notify,
+                                        scratch_pool);
         }
-      *state = svn_wc_notify_state_changed; /* same as merge_props did */
 
-      /* Until issue #3405 was fixed the code for changed directories was
-         used for directory deletions, which made use apply svn:mergeinfo as if
-         additional changes were applied by the merge, which really weren't.
-
-         ### I wouldn't be surprised if we somehow relied on these for correct
-         ### merges, but in this case the fix added here should also be applied
-         ### for added files! */
-
-      /* ### The old code performed (via prepare_merge_props_changed):
-      if (apr_hash_get(new_pristine_props, SVN_PROP_MERGEINFO,
-                       APR_HASH_KEY_STRING))
+      if (merge_b->merge_source.ancestral
+          || merge_b->reintegrate_merge)
         {
-          alloc_and_store_path(&merge_b->paths_with_new_mergeinfo,
-                               local_abspath, merge_b->pool);
+          store_path(merge_b->skipped_abspaths, local_abspath);
         }
-         ### which is something merge_add_file() doesn't do, but
-         ### merge_tests.py 95: no self referential filtering on added path
-         ### fails if this block is not enabled.
-      */
+    }
+  else if (db->tree_conflict_reason != CONFLICT_REASON_NONE)
+    {
+      /* open_directory() decided that a tree conflict should be raised */
 
-      return SVN_NO_ERROR;
+      SVN_ERR(record_tree_conflict(merge_b, local_abspath, db->parent_baton,
+                                   svn_node_dir, db->tree_conflict_action,
+                                   db->tree_conflict_reason,
+                                   NULL, TRUE,
+                                   scratch_pool));
     }
 
-  SVN_ERR(prepare_merge_props_changed(&props, local_abspath, propchanges,
-                                      merge_b, scratch_pool, scratch_pool));
+  return SVN_NO_ERROR;
+}
 
-  if (props->nelts)
+/* Helper function for the merge_file_*() functions.
+
+   Installs and notifies pre-recorded tree conflicts and skips for
+   ancestors of operational merges
+ */
+static svn_error_t *
+mark_file_edited(merge_cmd_baton_t *merge_b,
+                 struct merge_file_baton_t *fb,
+                 const char *local_abspath,
+                 apr_pool_t *scratch_pool)
+{
+  /* ### Too much common code with mark_dir_edited */
+  if (fb->edited)
+    return SVN_NO_ERROR;
+
+  if (fb->parent_baton && !fb->parent_baton->edited)
     {
-      const svn_wc_conflict_version_t *left;
-      const svn_wc_conflict_version_t *right;
-      svn_client_ctx_t *ctx = merge_b->ctx;
+      const char *dir_abspath = svn_dirent_dirname(local_abspath,
+                                                   scratch_pool);
 
-      SVN_ERR(make_conflict_versions(&left, &right, local_abspath,
-                                     svn_node_dir, &merge_b->merge_source,
-                                     merge_b->target, merge_b->pool));
+      SVN_ERR(mark_dir_edited(merge_b, fb->parent_baton, dir_abspath,
+                              scratch_pool));
+    }
 
-      SVN_ERR(svn_wc_merge_props3(state, ctx->wc_ctx, local_abspath,
-                                  left, right,
-                                  original_props, props,
-                                  merge_b->dry_run,
-                                  ctx->conflict_func2, ctx->conflict_baton2,
-                                  ctx->cancel_func, ctx->cancel_baton,
-                                  scratch_pool));
+  fb->edited = TRUE;
+
+  if (! fb->shadowed)
+    return SVN_NO_ERROR; /* Easy out */
+
+  if (fb->parent_baton
+      && fb->parent_baton->delete_state
+      && fb->tree_conflict_reason != CONFLICT_REASON_NONE)
+    {
+      fb->parent_baton->delete_state->found_edit = TRUE;
+    }
+  else if (fb->tree_conflict_reason == CONFLICT_REASON_SKIP
+           || fb->tree_conflict_reason == CONFLICT_REASON_SKIP_WC)
+    {
+      /* open_directory() decided not to flag a tree conflict, but
+         for clarity we produce a skip for this node that
+         most likely isn't touched by the merge itself */
 
-      if (*state == svn_wc_notify_state_conflicted
-          || *state == svn_wc_notify_state_merged
-          || *state == svn_wc_notify_state_changed)
+      if (merge_b->ctx->notify_func2)
         {
-          SVN_ERR(record_update_update(merge_b, local_abspath, svn_node_file,
-                                       svn_wc_notify_state_inapplicable,
-                                       *state, scratch_pool));
+          svn_wc_notify_t *notify;
+
+          SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE,
+                                     scratch_pool));
+
+          notify = svn_wc_create_notify(local_abspath, svn_wc_notify_skip,
+                                        scratch_pool);
+          notify->kind = svn_node_file;
+          notify->content_state = notify->prop_state = fb->skip_reason;
+
+          (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2,
+                                        notify,
+                                        scratch_pool);
+        }
+
+      if (merge_b->merge_source.ancestral
+          || merge_b->reintegrate_merge)
+        {
+          store_path(merge_b->skipped_abspaths, local_abspath);
         }
     }
-  else
-    *state = svn_wc_notify_state_unchanged;
+  else if (fb->tree_conflict_reason != CONFLICT_REASON_NONE)
+    {
+      /* open_file() decided that a tree conflict should be raised */
+
+      SVN_ERR(record_tree_conflict(merge_b, local_abspath, fb->parent_baton,
+                                   svn_node_file, fb->tree_conflict_action,
+                                   fb->tree_conflict_reason,
+                                   NULL, TRUE,
+                                   scratch_pool));
+    }
 
   return SVN_NO_ERROR;
 }
 
-/* Contains any state collected while resolving conflicts. */
-typedef struct conflict_resolver_baton_t
-{
-  /* The wrapped callback and baton. If func is null, postpone conflicts. */
-  svn_wc_conflict_resolver_func2_t wrapped_func;
-  void *wrapped_baton;
-
-  /* The list of any paths which remained in conflict after a
-     resolution attempt was made. Non-null ptr to possibly-null ptr to hash. */
-  apr_hash_t **conflicted_paths;
-
-  /* Pool with a sufficient lifetime to be used for output members such as
-   * *CONFLICTED_PATHS. */
-  apr_pool_t *pool;
-} conflict_resolver_baton_t;
-
-/* An implementation of the svn_wc_conflict_resolver_func_t interface.
+/* An svn_diff_tree_processor_t function.
 
-   This is called by the WC layer when a file merge conflicts.  We call
-   the wrapped conflict callback, BATON->wrapped_func, or, if that is
-   null, we postpone the conflict.
+   Called before either merge_file_changed(), merge_file_added(),
+   merge_file_deleted() or merge_file_closed(), unless it sets *SKIP to TRUE.
 
-   We keep a record of paths which remain in conflict.
-*/
+   When *SKIP is TRUE, the diff driver avoids work on getting the details
+   for the closing callbacks.
+ */
 static svn_error_t *
-conflict_resolver(svn_wc_conflict_result_t **result,
-                  const svn_wc_conflict_description2_t *description,
-                  void *baton,
+merge_file_opened(void **new_file_baton,
+                  svn_boolean_t *skip,
+                  const char *relpath,
+                  const svn_diff_source_t *left_source,
+                  const svn_diff_source_t *right_source,
+                  const svn_diff_source_t *copyfrom_source,
+                  void *dir_baton,
+                  const struct svn_diff_tree_processor_t *processor,
                   apr_pool_t *result_pool,
                   apr_pool_t *scratch_pool)
 {
-  svn_error_t *err;
-  conflict_resolver_baton_t *conflict_b = baton;
+  merge_cmd_baton_t *merge_b = processor->baton;
+  struct merge_dir_baton_t *pdb = dir_baton;
+  struct merge_file_baton_t *fb;
+  const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
+                                              relpath, scratch_pool);
 
-  if (conflict_b->wrapped_func)
-    err = (*conflict_b->wrapped_func)(result, description,
-                                      conflict_b->wrapped_baton,
-                                      result_pool,
-                                      scratch_pool);
-  else
+  fb = apr_pcalloc(result_pool, sizeof(*fb));
+  fb->tree_conflict_reason = CONFLICT_REASON_NONE;
+  fb->tree_conflict_action = svn_wc_conflict_action_edit;
+  fb->skip_reason = svn_wc_notify_state_unknown;
+
+  *new_file_baton = fb;
+
+  if (pdb)
     {
-      /* If we have no wrapped callback to invoke, then we still need
-         to behave like a proper conflict-callback ourselves.  */
-      *result = svn_wc_create_conflict_result(svn_wc_conflict_choose_postpone,
-                                              NULL, result_pool);
-      err = SVN_NO_ERROR;
+      fb->parent_baton = pdb;
+      fb->shadowed = pdb->shadowed;
+      fb->skip_reason = pdb->skip_reason;
     }
 
-  /* Keep a record of paths still in conflict after the resolution attempt. */
-  if ((*result)->choice == svn_wc_conflict_choose_postpone)
+  if (fb->shadowed)
+    {
+      /* An ancestor is tree conflicted. Nothing to do here. */
+    }
+  else if (left_source != NULL)
     {
-      alloc_and_store_path(conflict_b->conflicted_paths,
-                           description->local_abspath, conflict_b->pool);
+      /* Node is expected to be a file, which will be changed or deleted. */
+      svn_node_kind_t kind;
+      svn_boolean_t is_deleted;
+      svn_boolean_t excluded;
+      svn_depth_t parent_depth;
+
+      if (! right_source)
+        fb->tree_conflict_action = svn_wc_conflict_action_delete;
+
+      {
+        svn_wc_notify_state_t obstr_state;
+
+        SVN_ERR(perform_obstruction_check(&obstr_state, &is_deleted, &excluded,
+                                          &kind, &parent_depth,
+                                          merge_b, local_abspath,
+                                          scratch_pool));
+
+        if (obstr_state != svn_wc_notify_state_inapplicable)
+          {
+            fb->shadowed = TRUE;
+            fb->tree_conflict_reason = CONFLICT_REASON_SKIP;
+            fb->skip_reason = obstr_state;
+            return SVN_NO_ERROR;
+          }
+
+        if (is_deleted)
+          kind = svn_node_none;
+      }
+
+      if (kind == svn_node_none)
+        {
+          fb->shadowed = TRUE;
+
+          /* If this is not the merge target and the parent is too shallow to
+             contain this directory, and the directory is not present
+             via exclusion or depth filtering, skip it instead of recording
+             a tree conflict.
+
+             Non-inheritable mergeinfo will be recorded, allowing
+             future merges into non-shallow working copies to merge
+             changes we missed this time around. */
+          if (pdb && (excluded
+                      || (parent_depth != svn_depth_unknown &&
+                          parent_depth < svn_depth_files)))
+            {
+                fb->shadowed = TRUE;
+
+                fb->tree_conflict_reason = CONFLICT_REASON_SKIP;
+                fb->skip_reason = svn_wc_notify_state_missing;
+                return SVN_NO_ERROR;
+            }
+
+          if (is_deleted)
+            fb->tree_conflict_reason = svn_wc_conflict_reason_deleted;
+          else
+            fb->tree_conflict_reason = svn_wc_conflict_reason_missing;
+
+          /* ### Similar to directory */
+          *skip = TRUE;
+          SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+          return SVN_NO_ERROR;
+          /* ### /Similar */
+        }
+      else if (kind != svn_node_file)
+        {
+          fb->shadowed = TRUE;
+
+          fb->tree_conflict_reason = svn_wc_conflict_reason_obstructed;
+
+          /* ### Similar to directory */
+          *skip = TRUE;
+          SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+          return SVN_NO_ERROR;
+          /* ### /Similar */
+        }
+
+      if (! right_source)
+        {
+          /* We want to delete the directory */
+          fb->tree_conflict_action = svn_wc_conflict_action_delete;
+          SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+
+          if (fb->shadowed)
+            {
+              return SVN_NO_ERROR; /* Already set a tree conflict */
+            }
+
+          /* Comparison mode to verify for delete tree conflicts? */
+          if (pdb && pdb->delete_state
+              && pdb->delete_state->found_edit)
+            {
+              /* Earlier nodes found a conflict. Done. */
+              *skip = TRUE;
+            }
+        }
     }
+  else
+    {
+      const svn_wc_conflict_description2_t *old_tc = NULL;
 
-  return svn_error_trace(err);
-}
+      /* The node doesn't exist pre-merge: We have an addition */
+      fb->added = TRUE;
+      fb->tree_conflict_action = svn_wc_conflict_action_add;
 
+      if (pdb && pdb->pending_deletes
+          && svn_hash_gets(pdb->pending_deletes, local_abspath))
+        {
+          fb->add_is_replace = TRUE;
+          fb->tree_conflict_action = svn_wc_conflict_action_replace;
+
+          svn_hash_sets(pdb->pending_deletes, local_abspath, NULL);
+        }
+
+      if (pdb
+          && pdb->new_tree_conflicts
+          && (old_tc = svn_hash_gets(pdb->new_tree_conflicts, local_abspath)))
+        {
+          fb->tree_conflict_action = svn_wc_conflict_action_replace;
+          fb->tree_conflict_reason = old_tc->reason;
+
+          /* Update the tree conflict to store that this is a replace */
+          SVN_ERR(record_tree_conflict(merge_b, local_abspath, pdb,
+                                       svn_node_file,
+                                       fb->tree_conflict_action,
+                                       fb->tree_conflict_reason,
+                                       old_tc, FALSE,
+                                       scratch_pool));
+
+          if (old_tc->reason == svn_wc_conflict_reason_deleted
+              || old_tc->reason == svn_wc_conflict_reason_moved_away)
+            {
+              /* Issue #3806: Incoming replacements on local deletes produce
+                 inconsistent result.
+
+                 In this specific case we can continue applying the add part
+                 of the replacement. */
+            }
+          else
+            {
+              *skip = TRUE;
+
+              return SVN_NO_ERROR;
+            }
+        }
+      else if (! (merge_b->dry_run
+                  && ((pdb && pdb->added) || fb->add_is_replace)))
+        {
+          svn_wc_notify_state_t obstr_state;
+          svn_node_kind_t kind;
+          svn_boolean_t is_deleted;
+
+          SVN_ERR(perform_obstruction_check(&obstr_state, &is_deleted, NULL,
+                                            &kind, NULL,
+                                            merge_b, local_abspath,
+                                            scratch_pool));
+
+          if (obstr_state != svn_wc_notify_state_inapplicable)
+            {
+              /* Skip the obstruction */
+              fb->shadowed = TRUE;
+              fb->tree_conflict_reason = CONFLICT_REASON_SKIP;
+              fb->skip_reason = obstr_state;
+            }
+          else if (kind != svn_node_none && !is_deleted)
+            {
+              /* Set a tree conflict */
+              fb->shadowed = TRUE;
+              fb->tree_conflict_reason = svn_wc_conflict_reason_obstructed;
+            }
+        }
+
+      /* Handle pending conflicts */
+      SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+    }
 
-/* An svn_wc_diff_callbacks4_t function. */
-static svn_error_t *
-merge_file_opened(svn_boolean_t *tree_conflicted,
-                  svn_boolean_t *skip,
-                  const char *path,
-                  svn_revnum_t rev,
-                  void *diff_baton,
-                  apr_pool_t *scratch_pool)
-{
   return SVN_NO_ERROR;
 }
 
-/* An svn_wc_diff_callbacks4_t function. */
+/* An svn_diff_tree_processor_t function.
+ *
+ * Called after merge_file_opened() when a node receives only text and/or
+ * property changes between LEFT_SOURCE and RIGHT_SOURCE.
+ *
+ * left_file and right_file can be NULL when the file is not modified.
+ * left_props and right_props are always available.
+ */
 static svn_error_t *
-merge_file_changed(svn_wc_notify_state_t *content_state,
-                   svn_wc_notify_state_t *prop_state,
-                   svn_boolean_t *tree_conflicted,
-                   const char *mine_relpath,
-                   const char *older_abspath,
-                   const char *yours_abspath,
-                   svn_revnum_t older_rev,
-                   svn_revnum_t yours_rev,
-                   const char *mimetype1,
-                   const char *mimetype2,
-                   const apr_array_header_t *prop_changes,
-                   apr_hash_t *original_props,
-                   void *baton,
-                   apr_pool_t *scratch_pool)
+merge_file_changed(const char *relpath,
+                  const svn_diff_source_t *left_source,
+                  const svn_diff_source_t *right_source,
+                  const char *left_file,
+                  const char *right_file,
+                  /*const*/ apr_hash_t *left_props,
+                  /*const*/ apr_hash_t *right_props,
+                  svn_boolean_t file_modified,
+                  const apr_array_header_t *prop_changes,
+                  void *file_baton,
+                  const struct svn_diff_tree_processor_t *processor,
+                  apr_pool_t *scratch_pool)
 {
-  merge_cmd_baton_t *merge_b = baton;
+  merge_cmd_baton_t *merge_b = processor->baton;
+  struct merge_file_baton_t *fb = file_baton;
   svn_client_ctx_t *ctx = merge_b->ctx;
   const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
-                                              mine_relpath, scratch_pool);
-  svn_node_kind_t wc_kind;
-  svn_boolean_t is_deleted;
+                                              relpath, scratch_pool);
   const svn_wc_conflict_version_t *left;
   const svn_wc_conflict_version_t *right;
+  svn_wc_notify_state_t text_state;
+  svn_wc_notify_state_t property_state;
 
   SVN_ERR_ASSERT(local_abspath && svn_dirent_is_absolute(local_abspath));
-  SVN_ERR_ASSERT(!older_abspath || svn_dirent_is_absolute(older_abspath));
-  SVN_ERR_ASSERT(!yours_abspath || svn_dirent_is_absolute(yours_abspath));
-
-  /* Check for an obstructed or missing node on disk. */
-  {
-    svn_wc_notify_state_t obstr_state;
-
-    SVN_ERR(perform_obstruction_check(&obstr_state, &is_deleted, &wc_kind,
-                                      merge_b, local_abspath, svn_node_unknown,
-                                      scratch_pool));
-    if (obstr_state != svn_wc_notify_state_inapplicable)
-      {
-        /* ### a future thought:  if the file is under version control,
-         * but the working file is missing, maybe we can 'restore' the
-         * working file from the text-base, and then allow the merge to run? */
-
-        *content_state = obstr_state;
-        if (obstr_state == svn_wc_notify_state_missing)
-          *prop_state = svn_wc_notify_state_missing;
-        SVN_ERR(record_skip(merge_b, local_abspath, svn_node_file,
-                            obstr_state, scratch_pool));
-        return SVN_NO_ERROR;
-      }
-  }
+  SVN_ERR_ASSERT(!left_file || svn_dirent_is_absolute(left_file));
+  SVN_ERR_ASSERT(!right_file || svn_dirent_is_absolute(right_file));
 
-  /* Other easy outs:  if the merge target isn't under version
-     control, or is just missing from disk, fogettaboutit.  There's no
-     way svn_wc_merge5() can do the merge. */
-  if (wc_kind != svn_node_file || is_deleted)
-    {
-      svn_boolean_t moved_away;
-      svn_wc_conflict_reason_t reason;
-
-      /* Maybe the node is excluded via depth filtering? */
-
-      if (wc_kind == svn_node_none)
-        {
-          svn_depth_t parent_depth;
-
-          /* If the file isn't there due to depth restrictions, do not flag
-           * a conflict. Non-inheritable mergeinfo will be recorded, allowing
-           * future merges into non-shallow working copies to merge changes
-           * we missed this time around. */
-          SVN_ERR(svn_wc__node_get_depth(&parent_depth, ctx->wc_ctx,
-                                         svn_dirent_dirname(local_abspath,
-                                                            scratch_pool),
-                                         scratch_pool));
-          if (parent_depth < svn_depth_files
-              && parent_depth != svn_depth_unknown)
-            {
-              *content_state = svn_wc_notify_state_missing;
-              *prop_state = svn_wc_notify_state_missing;
-              SVN_ERR(record_skip(merge_b, local_abspath, svn_node_file,
-                                  svn_wc_notify_state_missing, scratch_pool));
-              return SVN_NO_ERROR;
-            }
-        }
+  SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
 
-      /* This is use case 4 described in the paper attached to issue
-       * #2282.  See also notes/tree-conflicts/detection.txt
-       */
-      if (is_deleted)
+  if (fb->shadowed)
+    {
+      if (fb->tree_conflict_reason == CONFLICT_REASON_NONE)
         {
-          SVN_ERR(check_moved_away(&moved_away, ctx->wc_ctx,
-                                   local_abspath, scratch_pool));
-          if (moved_away)
-            reason = svn_wc_conflict_reason_moved_away;
-          else
-            reason = svn_wc_conflict_reason_deleted;
+          /* We haven't notified for this node yet: report a skip */
+          SVN_ERR(record_skip(merge_b, local_abspath, svn_node_file,
+                              svn_wc_notify_update_shadowed_update,
+                              fb->skip_reason, scratch_pool));
         }
-      else
-        reason = svn_wc_conflict_reason_missing;
-      SVN_ERR(tree_conflict(merge_b, local_abspath, svn_node_file,
-                            svn_wc_conflict_action_edit, reason));
-      *tree_conflicted = TRUE;
-      *content_state = svn_wc_notify_state_missing;
-      *prop_state = svn_wc_notify_state_missing;
-      SVN_ERR(record_tree_conflict(merge_b, local_abspath, svn_node_file,
-                                   scratch_pool));
-      return SVN_NO_ERROR;
-    }
 
-  /* ### TODO: Thwart attempts to merge into a path that has
-     ### unresolved conflicts.  This needs to be smart enough to deal
-     ### with tree conflicts!
-  if (is_path_conflicted_by_merge(merge_b, mine))
-    {
-      *content_state = svn_wc_notify_state_conflicted;
-      return svn_error_createf(SVN_ERR_WC_FOUND_CONFLICT, NULL,
-                               _("Path '%s' is in conflict, and must be "
-                                 "resolved before the remainder of the "
-                                 "requested merge can be applied"), mine);
+      return SVN_NO_ERROR;
     }
-  */
 
   /* This callback is essentially no more than a wrapper around
      svn_wc_merge5().  Thank goodness that all the
      diff-editor-mechanisms are doing the hard work of getting the
      fulltexts! */
 
-  *prop_state = svn_wc_notify_state_unchanged;
+  property_state = svn_wc_notify_state_unchanged;
+  text_state = svn_wc_notify_state_unchanged;
 
   SVN_ERR(prepare_merge_props_changed(&prop_changes, local_abspath,
                                       prop_changes, merge_b,
                                       scratch_pool, scratch_pool));
 
   SVN_ERR(make_conflict_versions(&left, &right, local_abspath,
-                                 svn_node_file, &merge_b->merge_source, merge_b->target, merge_b->pool));
+                                 svn_node_file, &merge_b->merge_source, merge_b->target,
+                                 scratch_pool, scratch_pool));
 
   /* Do property merge now, if we are not going to perform a text merge */
-  if ((merge_b->record_only || !older_abspath) && prop_changes->nelts)
+  if ((merge_b->record_only || !left_file) && prop_changes->nelts)
     {
-      SVN_ERR(svn_wc_merge_props3(prop_state, ctx->wc_ctx, local_abspath,
+      SVN_ERR(svn_wc_merge_props3(&property_state, ctx->wc_ctx, local_abspath,
                                   left, right,
-                                  original_props, prop_changes,
+                                  left_props, prop_changes,
                                   merge_b->dry_run,
                                   ctx->conflict_func2, ctx->conflict_baton2,
                                   ctx->cancel_func, ctx->cancel_baton,
@@ -2054,11 +2013,9 @@ merge_file_changed(svn_wc_notify_state_t
   /* Easy out: We are only applying mergeinfo differences. */
   if (merge_b->record_only)
     {
-      *content_state = svn_wc_notify_state_unchanged;
-      return SVN_NO_ERROR;
+      /* NO-OP */
     }
-
-  if (older_abspath)
+  else if (left_file)
     {
       svn_boolean_t has_local_mods;
       enum svn_wc_merge_outcome_t content_outcome;
@@ -2069,206 +2026,125 @@ merge_file_changed(svn_wc_notify_state_t
       const char *target_label = _(".working");
       const char *left_label = apr_psprintf(scratch_pool,
                                             _(".merge-left.r%ld"),
-                                            older_rev);
+                                            left_source->revision);
       const char *right_label = apr_psprintf(scratch_pool,
                                              _(".merge-right.r%ld"),
-                                             yours_rev);
-      conflict_resolver_baton_t conflict_baton = { 0 };
+                                             right_source->revision);
 
       SVN_ERR(svn_wc_text_modified_p2(&has_local_mods, ctx->wc_ctx,
                                       local_abspath, FALSE, scratch_pool));
 
-      /* Wrap the resolver so as to keep a note of all postponed conflicts. */
-      conflict_baton.wrapped_func = ctx->conflict_func2;
-      conflict_baton.wrapped_baton = ctx->conflict_baton2;
-
-      conflict_baton.conflicted_paths = &merge_b->conflicted_paths;
-      conflict_baton.pool = merge_b->pool;
-
       /* Do property merge and text merge in one step so that keyword expansion
          takes into account the new property values. */
-      SVN_ERR(svn_wc_merge5(&content_outcome, prop_state, ctx->wc_ctx,
-                            older_abspath, yours_abspath, local_abspath,
+      SVN_ERR(svn_wc_merge5(&content_outcome, &property_state, ctx->wc_ctx,
+                            left_file, right_file, local_abspath,
                             left_label, right_label, target_label,
                             left, right,
                             merge_b->dry_run, merge_b->diff3_cmd,
                             merge_b->merge_options,
-                            original_props, prop_changes,
-                            conflict_resolver, &conflict_baton,
+                            left_props, prop_changes,
+                            ctx->conflict_func2, ctx->conflict_baton2,
                             ctx->cancel_func,
                             ctx->cancel_baton,
                             scratch_pool));
 
+      if (content_outcome == svn_wc_merge_conflict
+          || property_state == svn_wc_notify_state_conflicted)
+        {
+          alloc_and_store_path(&merge_b->conflicted_paths, local_abspath,
+                               merge_b->pool);
+        }
+
       if (content_outcome == svn_wc_merge_conflict)
-        *content_state = svn_wc_notify_state_conflicted;
+        text_state = svn_wc_notify_state_conflicted;
       else if (has_local_mods
                && content_outcome != svn_wc_merge_unchanged)
-        *content_state = svn_wc_notify_state_merged;
+        text_state = svn_wc_notify_state_merged;
       else if (content_outcome == svn_wc_merge_merged)
-        *content_state = svn_wc_notify_state_changed;
+        text_state = svn_wc_notify_state_changed;
       else if (content_outcome == svn_wc_merge_no_merge)
-        *content_state = svn_wc_notify_state_missing;
+        text_state = svn_wc_notify_state_missing;
       else /* merge_outcome == svn_wc_merge_unchanged */
-        *content_state = svn_wc_notify_state_unchanged;
+        text_state = svn_wc_notify_state_unchanged;
     }
 
-  if (*content_state == svn_wc_notify_state_conflicted
-      || *content_state == svn_wc_notify_state_merged
-      || *content_state == svn_wc_notify_state_changed
-      || *prop_state == svn_wc_notify_state_conflicted
-      || *prop_state == svn_wc_notify_state_merged
-      || *prop_state == svn_wc_notify_state_changed)
+  if (text_state == svn_wc_notify_state_conflicted
+      || text_state == svn_wc_notify_state_merged
+      || text_state == svn_wc_notify_state_changed
+      || property_state == svn_wc_notify_state_conflicted
+      || property_state == svn_wc_notify_state_merged
+      || property_state == svn_wc_notify_state_changed)
     {
       SVN_ERR(record_update_update(merge_b, local_abspath, svn_node_file,
-                                   *content_state, *prop_state,
+                                   text_state, property_state,
                                    scratch_pool));
     }
 
   return SVN_NO_ERROR;
 }
 
-/* An svn_wc_diff_callbacks4_t function. */
+/* An svn_diff_tree_processor_t function.
+ *
+ * Called after merge_file_opened() when a node doesn't exist in LEFT_SOURCE,
+ * but does in RIGHT_SOURCE.
+ *
+ * When a node is replaced instead of just added a separate opened+deleted will
+ * be invoked before the current open+added.
+ */
 static svn_error_t *
-merge_file_added(svn_wc_notify_state_t *content_state,
-                 svn_wc_notify_state_t *prop_state,
-                 svn_boolean_t *tree_conflicted,
-                 const char *mine_relpath,
-                 const char *older_abspath,
-                 const char *yours_abspath,
-                 svn_revnum_t rev1,
-                 svn_revnum_t rev2,
-                 const char *mimetype1,
-                 const char *mimetype2,
-                 const char *copyfrom_path,
-                 svn_revnum_t copyfrom_revision,
-                 const apr_array_header_t *prop_changes,
-                 apr_hash_t *original_props,
-                 void *baton,
+merge_file_added(const char *relpath,
+                 const svn_diff_source_t *copyfrom_source,
+                 const svn_diff_source_t *right_source,
+                 const char *copyfrom_file,
+                 const char *right_file,
+                 /*const*/ apr_hash_t *copyfrom_props,
+                 /*const*/ apr_hash_t *right_props,
+                 void *file_baton,
+                 const struct svn_diff_tree_processor_t *processor,
                  apr_pool_t *scratch_pool)
 {
-  merge_cmd_baton_t *merge_b = baton;
+  merge_cmd_baton_t *merge_b = processor->baton;
+  struct merge_file_baton_t *fb = file_baton;
   const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
-                                              mine_relpath, scratch_pool);
-  svn_node_kind_t kind;
-  svn_boolean_t is_deleted;
-  int i;
-  apr_hash_t *file_props;
+                                              relpath, scratch_pool);
+  apr_hash_t *pristine_props;
+  apr_hash_t *new_props;
 
   SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
 
-  /* Easy out: We are only applying mergeinfo differences. */
-  if (merge_b->record_only)
+  SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+
+  if (fb->shadowed)
     {
-      *content_state = svn_wc_notify_state_unchanged;
-      *prop_state = svn_wc_notify_state_unchanged;
+      if (fb->tree_conflict_reason == CONFLICT_REASON_NONE)
+        {
+          /* We haven't notified for this node yet: report a skip */
+          SVN_ERR(record_skip(merge_b, local_abspath, svn_node_file,
+                              svn_wc_notify_update_shadowed_add,
+                              fb->skip_reason, scratch_pool));
+        }
+
       return SVN_NO_ERROR;
     }
 
-  /* In most cases, we just leave prop_state as unknown, and let the
-     content_state what happened, so we set prop_state here to avoid that
-     below. */
-  *prop_state = svn_wc_notify_state_unknown;
-
-  /* Apply the prop changes to a new hash table. */
-  file_props = apr_hash_copy(scratch_pool, original_props);
-  for (i = 0; i < prop_changes->nelts; ++i)
-    {
-      const svn_prop_t *prop = &APR_ARRAY_IDX(prop_changes, i, svn_prop_t);
-
-      /* We don't want any DAV wcprops related to this file because
-         they'll point to the wrong repository (in the
-         merge-from-foreign-repository scenario) or wrong place in the
-         right repository (in the same-repos scenario).  So we'll
-         strip them.  (Is this a layering violation?)  */
-      if (svn_property_kind2(prop->name) == svn_prop_wc_kind)
-        continue;
-
-      /* And in the foreign repository merge case, we only want
-         regular properties. */
-      if ((! merge_b->same_repos)
-          && (svn_property_kind2(prop->name) != svn_prop_regular_kind))
-        continue;
-
-      /* Issue #3383: We don't want mergeinfo from a foreign repository. */
-      if ((! merge_b->same_repos)
-          && strcmp(prop->name, SVN_PROP_MERGEINFO) == 0)
-        continue;
-
-      apr_hash_set(file_props, prop->name, APR_HASH_KEY_STRING, prop->value);
+  /* Easy out: We are only applying mergeinfo differences. */
+  if (merge_b->record_only)
+    {
+      return SVN_NO_ERROR;
     }
 
-  /* Check for an obstructed or missing node on disk. */
-  {
-    svn_wc_notify_state_t obstr_state;
-
-    SVN_ERR(perform_obstruction_check(&obstr_state, &is_deleted, &kind,
-                                      merge_b, local_abspath, svn_node_unknown,
-                                      scratch_pool));
-
-    if (obstr_state != svn_wc_notify_state_inapplicable)
-      {
-        *content_state = obstr_state;
-
-        SVN_ERR(record_skip(merge_b, local_abspath, svn_node_file,
-                            obstr_state, scratch_pool));
-
-        return SVN_NO_ERROR;
-      }
-
-    if (is_deleted)
-      kind = svn_node_none;
-  }
-
-  if (kind != svn_node_none)
+  if ((merge_b->merge_source.ancestral || merge_b->reintegrate_merge)
+      && ( !fb->parent_baton || !fb->parent_baton->added))
     {
-      /* The file add the merge wants to carry out is obstructed, so the
-       * file the merge wants to add is a tree conflict victim.
-       * See notes about obstructions in notes/tree-conflicts/detection.txt.
-       */
-      SVN_ERR(tree_conflict_on_add(merge_b, local_abspath, svn_node_file,
-                                   svn_wc_conflict_action_add,
-                                   svn_wc_conflict_reason_obstructed));
-      *tree_conflicted = TRUE;
-      
-      /* directory already exists, is it under version control? */
-      if ((kind != svn_node_none)
-          && dry_run_deleted_p(merge_b, local_abspath))
-        *content_state = svn_wc_notify_state_changed;
-      else
-        /* this will make the repos_editor send a 'skipped' message */
-        *content_state = svn_wc_notify_state_obstructed;
-
-      SVN_ERR(record_tree_conflict(merge_b, local_abspath, svn_node_file,
-                                   scratch_pool));
-
-      return SVN_NO_ERROR;
+      /* Store the roots of added subtrees */
+      store_path(merge_b->added_abspaths, local_abspath);
     }
 
   if (!merge_b->dry_run)
     {
       const char *copyfrom_url;
       svn_revnum_t copyfrom_rev;
-      svn_stream_t *new_contents, *new_base_contents;
-      apr_hash_t *new_base_props, *new_props;
-      svn_boolean_t existing_tree_conflict;
-      svn_error_t *err;
-      svn_node_kind_t parent_kind;
-
-
-      /* Does the parent exist on disk (vs missing). If no we should
-         report an obstruction. Or svn_wc_add_repos_file4() will just
-         do its work and the workqueue will create the missing dirs */
-      SVN_ERR(svn_io_check_path(
-                  svn_dirent_dirname(local_abspath, scratch_pool), 
-                  &parent_kind, scratch_pool));
-
-      if (parent_kind != svn_node_dir)
-        {
-          *content_state = svn_wc_notify_state_obstructed;
-          SVN_ERR(record_skip(merge_b, local_abspath, svn_node_file,
-                              svn_wc_notify_state_obstructed, scratch_pool));
-          return SVN_NO_ERROR;
-        }
+      svn_stream_t *new_contents, *pristine_contents;
 
       /* If this is a merge from the same repository as our
          working copy, we handle adds as add-with-history.
@@ -2282,92 +2158,66 @@ merge_file_added(svn_wc_notify_state_t *
           copyfrom_url = svn_path_url_add_component2(
                                        merge_b->merge_source.loc2->url,
                                        child, scratch_pool);
-          copyfrom_rev = rev2;
+          copyfrom_rev = right_source->revision;
           SVN_ERR(check_repos_match(merge_b->target, local_abspath,
                                     copyfrom_url, scratch_pool));
-          new_base_props = file_props;
-          new_props = NULL; /* inherit from new_base_props */
-          SVN_ERR(svn_stream_open_readonly(&new_base_contents,
-                                           yours_abspath,
+          SVN_ERR(svn_stream_open_readonly(&pristine_contents,
+                                           right_file,
                                            scratch_pool,
                                            scratch_pool));
           new_contents = NULL; /* inherit from new_base_contents */
+
+          pristine_props = right_props; /* Includes last_* information */
+          new_props = NULL; /* No local changes */
+
+          if (svn_hash_gets(pristine_props, SVN_PROP_MERGEINFO))
+            {
+              alloc_and_store_path(&merge_b->paths_with_new_mergeinfo,
+                                   local_abspath, merge_b->pool);
+            }
         }
       else
         {
+          apr_array_header_t *regular_props;
+
           copyfrom_url = NULL;
           copyfrom_rev = SVN_INVALID_REVNUM;
-          new_base_props = apr_hash_make(scratch_pool);
-          new_props = file_props;
-          new_base_contents = svn_stream_empty(scratch_pool);
-          SVN_ERR(svn_stream_open_readonly(&new_contents, yours_abspath,
+
+          pristine_contents = svn_stream_empty(scratch_pool);
+          SVN_ERR(svn_stream_open_readonly(&new_contents, right_file,
                                            scratch_pool, scratch_pool));
-        }
 
-      err = svn_wc_conflicted_p3(NULL, NULL, &existing_tree_conflict,
-                                 merge_b->ctx->wc_ctx, local_abspath,
-                                 merge_b->pool);
+          pristine_props = apr_hash_make(scratch_pool); /* Local addition */
 
-      if (err)
-        {
-          if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND)
-            return svn_error_trace(err);
+          /* We don't want any foreign properties */
+          SVN_ERR(svn_categorize_props(svn_prop_hash_to_array(right_props,
+                                                              scratch_pool),
+                                       NULL, NULL, &regular_props,
+                                       scratch_pool));
 
-          svn_error_clear(err);
-          existing_tree_conflict = FALSE;
+          new_props = svn_prop_array_to_hash(regular_props, scratch_pool);
+
+          /* Issue #3383: We don't want mergeinfo from a foreign repository. */
+          svn_hash_sets(new_props, SVN_PROP_MERGEINFO, NULL);
         }
 
-      if (existing_tree_conflict)
-        {
-          svn_boolean_t moved_here;
-          svn_wc_conflict_reason_t reason;
+      /* Do everything like if we had called 'svn cp PATH1 PATH2'. */
+      SVN_ERR(svn_wc_add_repos_file4(merge_b->ctx->wc_ctx,
+                                      local_abspath,
+                                      pristine_contents,
+                                      new_contents,
+                                      pristine_props, new_props,
+                                      copyfrom_url, copyfrom_rev,
+                                      merge_b->ctx->cancel_func,
+                                      merge_b->ctx->cancel_baton,
+                                      scratch_pool));
 
-          /* Possibly collapse the existing conflict into a 'replace'
-           * tree conflict. The conflict reason is 'added' because
-           * the now-deleted tree conflict victim must have been
-           * added in the history of the merge target. */
-          SVN_ERR(check_moved_here(&moved_here, merge_b->ctx->wc_ctx,
-                                   local_abspath, scratch_pool));
-          reason = moved_here ? svn_wc_conflict_reason_moved_here
-                              : svn_wc_conflict_reason_added;
-          SVN_ERR(tree_conflict_on_add(merge_b, local_abspath,
-                                       svn_node_file,
-                                       svn_wc_conflict_action_add,
-                                       reason));
-          *tree_conflicted = TRUE;
-          SVN_ERR(record_tree_conflict(merge_b, local_abspath, svn_node_file,
-                                       scratch_pool));
-        }
-      else
-        {
-          /* Since 'mine' doesn't exist, and this is
-             'merge_file_added', I hope it's safe to assume that
-             'older' is empty, and 'yours' is the full file.  Merely
-             copying 'yours' to 'mine', isn't enough; we need to get
-             the whole text-base and props installed too, just as if
-             we had called 'svn cp wc wc'. */
-          SVN_ERR(svn_wc_add_repos_file4(merge_b->ctx->wc_ctx,
-                                         local_abspath,
-                                         new_base_contents,
-                                         new_contents,
-                                         new_base_props, new_props,
-                                         copyfrom_url, copyfrom_rev,
-                                         merge_b->ctx->cancel_func,
-                                         merge_b->ctx->cancel_baton,
-                                         scratch_pool));
-        }
-    }
-  else /* dry_run */
-    {
-      /* ### Should we do this?
-      store_path(merge_b->dry_run_added, local_abspath); */
+      /* Caller must call svn_sleep_for_timestamps() */
+      *merge_b->use_sleep = TRUE;
     }
 
   SVN_ERR(record_update_add(merge_b, local_abspath, svn_node_file,
-                            scratch_pool));
-
-  *content_state = svn_wc_notify_state_changed;
-  *prop_state = svn_wc_notify_state_changed;
+                            fb->add_is_replace, scratch_pool));
 
   return SVN_NO_ERROR;
 }
@@ -2450,566 +2300,961 @@ files_same_p(svn_boolean_t *same,
   return SVN_NO_ERROR;
 }
 
-/* An svn_wc_diff_callbacks4_t function. */
+/* An svn_diff_tree_processor_t function.
+ *
+ * Called after merge_file_opened() when a node does exist in LEFT_SOURCE, but
+ * no longer exists (or is replaced) in RIGHT_SOURCE.
+ *
+ * When a node is replaced instead of just added a separate opened+added will
+ * be invoked after the current open+deleted.
+ */
 static svn_error_t *
-merge_file_deleted(svn_wc_notify_state_t *state,

[... 3754 lines stripped ...]