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 2013/02/04 21:48:13 UTC

svn commit: r1442344 [8/39] - in /subversion/branches/fsfs-format7: ./ build/ build/ac-macros/ build/generator/ build/generator/templates/ build/win32/ contrib/client-side/emacs/ contrib/server-side/fsfsfixer/fixer/ contrib/server-side/svncutter/ doc/ ...

Modified: subversion/branches/fsfs-format7/subversion/libsvn_client/merge.c
URL: http://svn.apache.org/viewvc/subversion/branches/fsfs-format7/subversion/libsvn_client/merge.c?rev=1442344&r1=1442343&r2=1442344&view=diff
==============================================================================
--- subversion/branches/fsfs-format7/subversion/libsvn_client/merge.c (original)
+++ subversion/branches/fsfs-format7/subversion/libsvn_client/merge.c Mon Feb  4 20:48:05 2013
@@ -64,6 +64,7 @@
 
 #include "svn_private_config.h"
 
+
 /*-----------------------------------------------------------------------*/
 
 /* MERGEINFO MERGE SOURCE NORMALIZATION
@@ -245,7 +246,7 @@ typedef struct merge_target_t
 } merge_target_t;
 
 typedef struct merge_cmd_baton_t {
-  svn_boolean_t force;
+  svn_boolean_t force_delete;         /* Delete a file/dir even if modified */
   svn_boolean_t dry_run;
   svn_boolean_t record_only;          /* Whether to merge only mergeinfo
                                          differences. */
@@ -255,16 +256,19 @@ typedef struct merge_cmd_baton_t {
                                          is TRUE.*/
   svn_boolean_t mergeinfo_capable;    /* Whether the merge source server
                                          is capable of Merge Tracking. */
-  svn_boolean_t ignore_ancestry;      /* Are we ignoring ancestry (and by
-                                         extension, mergeinfo)?  FALSE if
-                                         SOURCES_ANCESTRAL is FALSE. */
+  svn_boolean_t ignore_mergeinfo;     /* Don't honor mergeinfo; see
+                                         doc string of do_merge().  FALSE if
+                                         MERGE_SOURCE->ancestral is FALSE. */
+  svn_boolean_t diff_ignore_ancestry; /* Diff unrelated nodes as if related; see
+                                         doc string of do_merge().  FALSE if
+                                         MERGE_SOURCE->ancestral is FALSE. */
   svn_boolean_t reintegrate_merge;    /* Whether this is a --reintegrate
                                          merge or not. */
-  const char *added_path;             /* Set to the dir path whenever the
-                                         dir is added as a child of a
-                                         versioned dir (dry-run only) */
   const merge_target_t *target;       /* Description of merge target node */
 
+  const svn_merge_range_t *current_range; /* For single file merges the current
+                                             revision range */
+
   /* The left and right URLs and revs.  The value of this field changes to
      reflect the merge_source_t *currently* being merged by do_merge(). */
   merge_source_t merge_source;
@@ -278,25 +282,12 @@ typedef struct merge_cmd_baton_t {
 
   svn_client_ctx_t *ctx;              /* Client context for callbacks, etc. */
 
-  /* Whether invocation of the merge_file_added() callback required
-     delegation to the merge_file_changed() function for the file
-     currently being merged.  This info is used to detect whether a
-     file on the left side of a 3-way merge actually exists (important
-     because it's created as an empty temp file on disk regardless).*/
-  svn_boolean_t add_necessitated_merge;
-
-  /* The list of paths for entries we've deleted, used only when in
-     dry_run mode. */
-  apr_hash_t *dry_run_deletions;
-
-  /* The list of paths for entries 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
-     when in dry_run mode. */
+     when in dry_run mode.
+     ### And because we only want to resolve conflicts that were
+         generated by this merge, not pre-existing ones? */
   apr_hash_t *conflicted_paths;
 
   /* A list of absolute paths which had no explicit mergeinfo prior to the
@@ -306,13 +297,31 @@ typedef struct merge_cmd_baton_t {
      meet the criteria or DRY_RUN is true. */
   apr_hash_t *paths_with_new_mergeinfo;
 
-  /* A list of absolute paths which had explicit mergeinfo prior to the merge
-     but had this mergeinfo deleted by the merge.  This is populated by
-     merge_change_props() and is allocated in POOL so it is subject to the
+  /* A list of absolute paths whose mergeinfo doesn't need updating after
+     the merge. This can be caused by the removal of mergeinfo by the merge
+     or by deleting the node itself.  This is populated by merge_change_props()
+     and the delete callbacks and is allocated in POOL so it is subject to the
      lifetime limitations of POOL.  Is NULL if no paths are found which
      meet the criteria or DRY_RUN is true. */
   apr_hash_t *paths_with_deleted_mergeinfo;
 
+  /* The list of absolute skipped paths, which should be examined and
+     cleared after each invocation of the callback.  The paths
+     are absolute.  Is NULL if MERGE_B->MERGE_SOURCE->ancestral and
+     MERGE_B->REINTEGRATE_MERGE are both false. */
+  apr_hash_t *skipped_abspaths;
+
+  /* The list of absolute merged paths.  Unused if MERGE_B->MERGE_SOURCE->ancestral
+     and MERGE_B->REINTEGRATE_MERGE are both false. */
+  apr_hash_t *merged_abspaths;
+
+  /* A hash of (const char *) absolute WC paths mapped to the same which
+     represent the roots of subtrees added by the merge. */
+  apr_hash_t *added_abspaths;
+
+  /* A list of tree conflict victim absolute paths which may be NULL. */
+  apr_hash_t *tree_conflicted_abspaths;
+
   /* The diff3_cmd in ctx->config, if any, else null.  We could just
      extract this as needed, but since more than one caller uses it,
      we just set it up when this baton is created. */
@@ -338,6 +347,27 @@ typedef struct merge_cmd_baton_t {
      merge source, i.e. it is cleared on every call to do_directory_merge()
      or do_file_merge() in do_merge(). */
   apr_pool_t *pool;
+
+  /* Boolean indicating whether the information header for the current
+     operation has already been notified */
+  svn_boolean_t notified_merge_begin;
+
+  /* Contains any state collected while receiving path 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;
 } merge_cmd_baton_t;
 
 
@@ -345,12 +375,12 @@ typedef struct merge_cmd_baton_t {
    changes to merge, for the merge described by MERGE_B.  Specifically, that
    is if the merge source server is capable of merge tracking, the left-side
    merge source is an ancestor of the right-side (or vice-versa), the merge
-   source is in the same repository as the merge target, and ancestry is
-   being considered. */
+   source is in the same repository as the merge target, and we are not
+   ignoring mergeinfo. */
 #define HONOR_MERGEINFO(merge_b) ((merge_b)->mergeinfo_capable      \
                                   && (merge_b)->merge_source.ancestral  \
                                   && (merge_b)->same_repos          \
-                                  && (! (merge_b)->ignore_ancestry))
+                                  && (! (merge_b)->ignore_mergeinfo))
 
 
 /* Return TRUE iff we should be recording mergeinfo for the merge described
@@ -462,32 +492,35 @@ check_same_repos(const svn_client__pathr
   return SVN_NO_ERROR;
 }
 
-/* Return true iff we're in dry-run mode and WCPATH 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).  Assumes that WCPATH is
-   still versioned (e.g. has an associated entry). */
-static APR_INLINE svn_boolean_t
-dry_run_deleted_p(const merge_cmd_baton_t *merge_b, const char *wcpath)
+/* Store LOCAL_ABSPATH in PATH_HASH after duplicating it into the pool
+   containing PATH_HASH. */
+static APR_INLINE void
+store_path(apr_hash_t *path_hash, const char *local_abspath)
+{
+  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);
+}
+
+/* Store LOCAL_ABSPATH in *PATH_HASH_P after duplicating it into the pool
+   containing *PATH_HASH_P.  If *PATH_HASH_P is NULL, then first set
+   *PATH_HASH_P to a new hash allocated from POOL.  */
+static APR_INLINE void
+alloc_and_store_path(apr_hash_t **path_hash_p,
+                     const char *local_abspath,
+                     apr_pool_t *pool)
 {
-  return (merge_b->dry_run &&
-          apr_hash_get(merge_b->dry_run_deletions, wcpath,
-                       APR_HASH_KEY_STRING) != NULL);
+  if (! *path_hash_p)
+    *path_hash_p = apr_hash_make(pool);
+  store_path(*path_hash_p, local_abspath);
 }
 
-/* Return true iff we're in dry-run mode and WCPATH 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).  Assumes that WCPATH is
-   still versioned (e.g. has an associated entry). */
+/* Helper function to easy in checking if a path is in a path hash */
 static APR_INLINE svn_boolean_t
-dry_run_added_p(const merge_cmd_baton_t *merge_b, const char *wcpath)
+contains_path(apr_hash_t *path_hash, const char *local_abspath)
 {
-  return (merge_b->dry_run &&
-          apr_hash_get(merge_b->dry_run_added, wcpath,
-                       APR_HASH_KEY_STRING) != NULL);
+  return apr_hash_get(path_hash, local_abspath, APR_HASH_KEY_STRING) != NULL;
 }
 
 /* Return whether any WC path was put in conflict by the merge
@@ -516,7 +549,6 @@ 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 *added,
                           svn_boolean_t *deleted,
                           svn_node_kind_t *kind,
                           const merge_cmd_baton_t *merge_b,
@@ -532,41 +564,11 @@ perform_obstruction_check(svn_wc_notify_
 
   *obstruction_state = svn_wc_notify_state_inapplicable;
 
-  if (added)
-    *added = FALSE;
   if (deleted)
     *deleted = FALSE;
   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))
-        {
-          *obstruction_state = svn_wc_notify_state_inapplicable;
-
-          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))
-        {
-          *obstruction_state = svn_wc_notify_state_inapplicable;
-
-          if (added)
-            *added = TRUE;
-          if (kind)
-            *kind = svn_node_dir; /* Currently only used for dirs */
-
-          return SVN_NO_ERROR;
-        }
-     }
-
   if (kind == NULL)
     kind = &wc_kind;
 
@@ -574,7 +576,6 @@ perform_obstruction_check(svn_wc_notify_
 
   SVN_ERR(svn_wc__check_for_obstructions(obstruction_state,
                                          kind,
-                                         added,
                                          deleted,
                                          wc_ctx, local_abspath,
                                          check_root,
@@ -676,7 +677,8 @@ 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)
+              svn_wc_conflict_reason_t reason,
+              const char *moved_away_op_root_abspath)
 {
   const svn_wc_conflict_description2_t *existing_conflict;
   svn_error_t *err;
@@ -707,14 +709,11 @@ tree_conflict(merge_cmd_baton_t *merge_b
                                  &merge_b->merge_source, merge_b->target,
                                  merge_b->pool));
       SVN_ERR(svn_wc__add_tree_conflict(merge_b->ctx->wc_ctx, conflict,
+                                        moved_away_op_root_abspath,
                                         merge_b->pool));
 
-      if (merge_b->conflicted_paths == NULL)
-        merge_b->conflicted_paths = apr_hash_make(merge_b->pool);
-      victim_abspath = apr_pstrdup(merge_b->pool, victim_abspath);
-
-      apr_hash_set(merge_b->conflicted_paths, victim_abspath,
-                   APR_HASH_KEY_STRING, victim_abspath);
+      alloc_and_store_path(&merge_b->conflicted_paths, victim_abspath,
+                           merge_b->pool);
     }
 
   return SVN_NO_ERROR;
@@ -751,14 +750,11 @@ tree_conflict_on_add(merge_cmd_baton_t *
     {
       /* 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,
+                                        NULL,
                                         merge_b->pool));
 
-      if (merge_b->conflicted_paths == NULL)
-        merge_b->conflicted_paths = apr_hash_make(merge_b->pool);
-      victim_abspath = apr_pstrdup(merge_b->pool, victim_abspath);
-
-      apr_hash_set(merge_b->conflicted_paths, victim_abspath,
-                   APR_HASH_KEY_STRING, victim_abspath);
+      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)
@@ -782,14 +778,11 @@ tree_conflict_on_add(merge_cmd_baton_t *
                                      merge_b->pool);
 
       SVN_ERR(svn_wc__add_tree_conflict(merge_b->ctx->wc_ctx, conflict,
+                                        NULL,
                                         merge_b->pool));
 
-      if (merge_b->conflicted_paths == NULL)
-        merge_b->conflicted_paths = apr_hash_make(merge_b->pool);
-      victim_abspath = apr_pstrdup(merge_b->pool, victim_abspath);
-
-      apr_hash_set(merge_b->conflicted_paths, victim_abspath,
-                   APR_HASH_KEY_STRING, victim_abspath);
+      alloc_and_store_path(&merge_b->conflicted_paths, victim_abspath,
+                           merge_b->pool);
     }
 
   /* In any other cases, we don't touch the existing conflict. */
@@ -974,6 +967,11 @@ filter_self_referential_mergeinfo(apr_ar
       svn_mergeinfo_t filtered_younger_mergeinfo = NULL;
       svn_error_t *err;
 
+      /* If this property isn't mergeinfo or is NULL valued (i.e. prop removal)
+         or empty mergeinfo it does not require any special handling.  There
+         is nothing to filter out of empty mergeinfo and the concept of
+         filtering doesn't apply if we are trying to remove mergeinfo
+         entirely.  */
       if ((strcmp(prop->name, SVN_PROP_MERGEINFO) != 0)
           || (! prop->value)       /* Removal of mergeinfo */
           || (! prop->value->len)) /* Empty mergeinfo */
@@ -1192,8 +1190,14 @@ filter_self_referential_mergeinfo(apr_ar
 }
 
 /* Prepare a set of property changes PROPCHANGES to be used for a merge
-   operation on LOCAL_ABSPATH. Store the result in *PROP_UPDATES.
+   operation on LOCAL_ABSPATH.
+
+   Remove all non-regular prop-changes (entry-props and WC-props).
+   Remove all non-mergeinfo prop-changes if it's a record-only merge.
+   Remove self-referential mergeinfo (### in some cases...)
+   Remove foreign-repository mergeinfo (### in some cases...)
 
+   Store the resulting property changes in *PROP_UPDATES.
    Store information on where mergeinfo is updated in MERGE_B.
 
    Used for both file and directory property merges. */
@@ -1209,6 +1213,8 @@ prepare_merge_props_changed(const apr_ar
 
   SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
 
+  /* We only want to merge "regular" version properties:  by
+     definition, 'svn merge' shouldn't touch any data within .svn/  */
   SVN_ERR(svn_categorize_props(propchanges, NULL, NULL, &props,
                                result_pool));
 
@@ -1233,10 +1239,15 @@ prepare_merge_props_changed(const apr_ar
       props = mergeinfo_props;
     }
 
-  /* We only want to merge "regular" version properties:  by
-     definition, 'svn merge' shouldn't touch any data within .svn/  */
   if (props->nelts)
     {
+      /* Issue #3383: We don't want mergeinfo from a foreign repos.
+
+         If this is a merge from a foreign repository we must strip all
+         incoming mergeinfo (including mergeinfo deletions). */
+      if (! merge_b->same_repos)
+        SVN_ERR(omit_mergeinfo_changes(&props, props, result_pool));
+
       /* If this is a forward merge then don't add new mergeinfo to
          PATH that is already part of PATH's own history, see
          http://svn.haxx.se/dev/archive-2008-09/0006.shtml.  If the
@@ -1245,18 +1256,7 @@ prepare_merge_props_changed(const apr_ar
       if (merge_b->merge_source.loc1->rev < merge_b->merge_source.loc2->rev
           || !merge_b->merge_source.ancestral)
         {
-          /* Issue #3383: We don't want mergeinfo from a foreign repos.
-
-             If this is a merge from a foreign repository we must strip all
-             incoming mergeinfo (including mergeinfo deletions).  Otherwise if
-             this property isn't mergeinfo or is NULL valued (i.e. prop removal)
-             or empty mergeinfo it does not require any special handling.  There
-             is nothing to filter out of empty mergeinfo and the concept of
-             filtering doesn't apply if we are trying to remove mergeinfo
-             entirely.  */
-          if (! merge_b->same_repos)
-            SVN_ERR(omit_mergeinfo_changes(&props, props, result_pool));
-          else if (HONOR_MERGEINFO(merge_b) || merge_b->reintegrate_merge)
+          if (HONOR_MERGEINFO(merge_b) || merge_b->reintegrate_merge)
             SVN_ERR(filter_self_referential_mergeinfo(&props,
                                                       local_abspath,
                                                       merge_b->ra_session2,
@@ -1296,29 +1296,13 @@ prepare_merge_props_changed(const apr_ar
 
               if (!has_pristine_mergeinfo && prop->value)
                 {
-                  /* If BATON->PATHS_WITH_NEW_MERGEINFO needs to be
-                     allocated do so in BATON->POOL so it has a
-                     sufficient lifetime. */
-                  if (!merge_b->paths_with_new_mergeinfo)
-                    merge_b->paths_with_new_mergeinfo =
-                      apr_hash_make(merge_b->pool);
-
-                  apr_hash_set(merge_b->paths_with_new_mergeinfo,
-                               apr_pstrdup(merge_b->pool, local_abspath),
-                               APR_HASH_KEY_STRING, local_abspath);
+                  alloc_and_store_path(&merge_b->paths_with_new_mergeinfo,
+                                       local_abspath, merge_b->pool);
                 }
               else if (has_pristine_mergeinfo && !prop->value)
                 {
-                  /* If BATON->PATHS_WITH_DELETED_MERGEINFO needs to be
-                     allocated do so in BATON->POOL so it has a
-                     sufficient lifetime. */
-                  if (!merge_b->paths_with_deleted_mergeinfo)
-                    merge_b->paths_with_deleted_mergeinfo =
-                      apr_hash_make(merge_b->pool);
-
-                  apr_hash_set(merge_b->paths_with_deleted_mergeinfo,
-                               apr_pstrdup(merge_b->pool, local_abspath),
-                               APR_HASH_KEY_STRING, local_abspath);
+                  alloc_and_store_path(&merge_b->paths_with_deleted_mergeinfo,
+                                       local_abspath, merge_b->pool);
                 }
             }
         }
@@ -1327,375 +1311,864 @@ prepare_merge_props_changed(const apr_ar
   return SVN_NO_ERROR;
 }
 
-/* An svn_wc_diff_callbacks4_t function. */
+/* Set *OP_ROOT_ABSPATH if the node at LOCAL_ABSPATH was moved away
+ * locally, or set *OP_ROOT_ABSPATH to NULL otherwise. Do not raise an
+ * error if the node at LOCAL_ABSPATH does not exist.
+ *
+ * ### Currently this looks at the highest moved-to but should merge
+ * ### be looking at the lowest moved-to?  Or perhaps only the base
+ * ### moved-to? */
 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)
+check_moved_away(const char **op_root_abspath,
+                 svn_wc_context_t *wc_ctx,
+                 const char *local_abspath,
+                 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;
+  const char *moved_to_abspath;
 
-  SVN_ERR(perform_obstruction_check(&obstr_state, NULL, &is_deleted,
-                                    &kind,
-                                    merge_b, local_abspath, svn_node_dir,
-                                    scratch_pool));
+  SVN_ERR(svn_wc__node_was_moved_away(&moved_to_abspath, op_root_abspath,
+                                      wc_ctx, local_abspath,
+                                      scratch_pool, scratch_pool));
+  
+  return SVN_NO_ERROR;
+}
+
+/* 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;
+
+  *moved_here = FALSE;
 
-  if (obstr_state != svn_wc_notify_state_inapplicable)
+  err = svn_wc__node_was_moved_here(&moved_from_abspath, NULL,
+                                    wc_ctx, local_abspath,
+                                    scratch_pool, scratch_pool);
+  if (err)
     {
-      if (state)
-        *state = obstr_state;
-      return SVN_NO_ERROR;
+      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;
 
-  if (kind != svn_node_dir || is_deleted)
-    {
-      svn_wc_conflict_reason_t reason;
+  return SVN_NO_ERROR;
+}
 
-      if (is_deleted)
-        reason = svn_wc_conflict_reason_deleted;
-      else
-        reason = svn_wc_conflict_reason_missing;
+#define CONFLICT_REASON_NONE       ((svn_wc_conflict_reason_t)-1)
+#define CONFLICT_REASON_EXCLUDED   ((svn_wc_conflict_reason_t)-2)
+#define CONFLICT_REASON_SKIP       ((svn_wc_conflict_reason_t)-3)
+#define CONFLICT_REASON_SKIP_WC    ((svn_wc_conflict_reason_t)-4)
+
+/* 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;
 
-      SVN_ERR(tree_conflict(merge_b, local_abspath, svn_node_file,
-                            svn_wc_conflict_action_edit, reason));
+  /* The pool containing this baton. Use for RESULT_POOL for storing in this
+     baton */
+  apr_pool_t *pool;
 
-      if (tree_conflicted)
-        *tree_conflicted = TRUE;
-      if (state)
-        *state = svn_wc_notify_state_missing;
+  /* 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_EXCLUDED:
+            The node has been excluded (or depth filtered) and the node will
+            only be reported as skipped.
+
+       CONFLICT_REASON_SKIP:
+            The node will be skipped with content and property state as stored in
+            SKIP_REASON.
 
-      return SVN_NO_ERROR;
-    }
+       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;
+
+  /* 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;
+};
+
+/* 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 */
+};
+
+/* 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(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 (dir_was_added
-      && merge_b->dry_run
-      && dry_run_added_p(merge_b, local_abspath))
+  if (merge_b->merge_source.ancestral
+      || merge_b->reintegrate_merge)
     {
-      return SVN_NO_ERROR; /* We can't do a real prop merge for added dirs */
+      store_path(merge_b->skipped_abspaths, local_abspath);
     }
 
-  SVN_ERR(prepare_merge_props_changed(&props, local_abspath, propchanges,
-                                      merge_b, scratch_pool, scratch_pool));
-
-  /* We only want to merge "regular" version properties:  by
-     definition, 'svn merge' shouldn't touch any pristine data  */
-  if (props->nelts)
+  if (merge_b->ctx->notify_func2)
     {
-      const svn_wc_conflict_version_t *left;
-      const svn_wc_conflict_version_t *right;
-      svn_client_ctx_t *ctx = merge_b->ctx;
+      svn_wc_notify_t *notify;
 
-      SVN_ERR(make_conflict_versions(&left, &right, local_abspath,
-                                     svn_node_dir, &merge_b->merge_source,
-                                     merge_b->target, merge_b->pool));
+      SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, 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));
-    }
-  else if (state)
-    *state = svn_wc_notify_state_unchanged;
+      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);
+    }
   return SVN_NO_ERROR;
 }
 
-/* Contains any state collected while resolving conflicts. */
-typedef struct conflict_resolver_baton_t
+/* Record the skip for future processing and (later) produce the
+   tree conflict notification */
+static svn_error_t *
+record_tree_conflict(merge_cmd_baton_t *merge_b,
+                     const char *local_abspath,
+                     svn_node_kind_t kind,
+                     apr_pool_t *scratch_pool)
 {
-  /* The wrapped callback and baton. */
-  svn_wc_conflict_resolver_func2_t wrapped_func;
-  void *wrapped_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))
+    {
+      svn_wc_notify_t *notify;
 
-  /* The list of any paths which remained in conflict after a
-     resolution attempt was made. */
-  apr_hash_t **conflicted_paths;
+      SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
 
-  /* Pool with a sufficient lifetime to be used for output members such as
-   * *CONFLICTED_PATHS. */
-  apr_pool_t *pool;
-} conflict_resolver_baton_t;
+      notify = svn_wc_create_notify(local_abspath, svn_wc_notify_tree_conflict,
+                                    scratch_pool);
+      notify->kind = kind;
 
-/* An implementation of the svn_wc_conflict_resolver_func_t interface.
-   We keep a record of paths which remain in conflict after any
-   resolution attempt from BATON->wrapped_func. */
-static svn_error_t *
-conflict_resolver(svn_wc_conflict_result_t **result,
-                  const svn_wc_conflict_description2_t *description,
-                  void *baton,
-                  apr_pool_t *result_pool,
+      (*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->tree_conflicted_abspaths, local_abspath);
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* Record the add for future processing and (later) produce the
+   update_add notification
+
+   Originally a helper for notification_receiver: Cache the roots of
+   subtrees added under TARGET_ABSPATH.
+ */
+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_error_t *err;
-  conflict_resolver_baton_t *conflict_b = baton;
+  svn_boolean_t root_of_added_subtree = TRUE;
 
-  if (conflict_b->wrapped_func)
-    err = (*conflict_b->wrapped_func)(result, description,
-                                      conflict_b->wrapped_baton,
-                                      result_pool,
-                                      scratch_pool);
-  else
+  if (merge_b->merge_source.ancestral || merge_b->reintegrate_merge)
     {
-      /* 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;
+      /* 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);
     }
 
-  /* Keep a record of paths still in conflict after the resolution attempt. */
-  if ((! conflict_b->wrapped_func)
-      || (*result && ((*result)->choice == svn_wc_conflict_choose_postpone)))
+  if (merge_b->ctx->notify_func2)
     {
-      const char *conflicted_path = apr_pstrdup(conflict_b->pool,
-                                                description->local_abspath);
+      svn_wc_notify_t *notify;
+      svn_wc_notify_action_t action = svn_wc_notify_update_add;
+
+      SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
 
-      if (*conflict_b->conflicted_paths == NULL)
-        *conflict_b->conflicted_paths = apr_hash_make(conflict_b->pool);
+      if (notify_replaced)
+        action = svn_wc_notify_update_replace;
 
-      apr_hash_set(*conflict_b->conflicted_paths, conflicted_path,
-                   APR_HASH_KEY_STRING, conflicted_path);
+      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);
     }
 
-  return svn_error_trace(err);
+  return SVN_NO_ERROR;
 }
 
-/* An svn_wc_diff_callbacks4_t function. */
+/* Record the update for future processing and (later) produce the
+   update_add notification */
 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)
+record_update_update(merge_cmd_baton_t *merge_b,
+                     const char *local_abspath,
+                     svn_node_kind_t kind,
+                     svn_wc_notify_state_t content_state,
+                     svn_wc_notify_state_t prop_state,
+                     apr_pool_t *scratch_pool)
 {
-  return SVN_NO_ERROR;
-}
+  if (merge_b->merge_source.ancestral || merge_b->reintegrate_merge)
+    {
+      store_path(merge_b->merged_abspaths, local_abspath);
+    }
 
+  if (merge_b->ctx->notify_func2)
+    {
+      svn_wc_notify_t *notify;
 
-/* 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;
-  svn_error_t *err;
+      SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
 
-  *moved_away = FALSE;
+      notify = svn_wc_create_notify(local_abspath, svn_wc_notify_update_update,
+                                    scratch_pool);
+      notify->kind = kind;
+      notify->content_state = content_state;
+      notify->prop_state = prop_state;
 
-  err = svn_wc__node_was_moved_away(&moved_to_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);
+      (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
+                                    scratch_pool);
     }
-  else if (moved_to_abspath)
-    *moved_away = TRUE;
 
   return SVN_NO_ERROR;
 }
 
-/* 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. */
+/* Record the delete for future processing and (later) produce the
+   update_delete notification */
 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)
+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)
 {
-  const char *moved_from_abspath;
-  svn_error_t *err;
+  /* Update the lists of merged, skipped, tree-conflicted and added paths. */
+  if (merge_b->merge_source.ancestral
+      || merge_b->reintegrate_merge)
+    {
+      /* 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);
+      store_path(merge_b->merged_abspaths, local_abspath);
+    }
 
-  *moved_here = FALSE;
+  SVN_ERR(notify_merge_begin(merge_b, local_abspath, TRUE, scratch_pool));
 
-  err = svn_wc__node_was_moved_here(&moved_from_abspath, NULL,
-                                    wc_ctx, local_abspath,
-                                    scratch_pool, scratch_pool);
-  if (err)
+  if (parent_db)
     {
-      if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
-        svn_error_clear(err);
-      else
-        return svn_error_trace(err);
+      const char *dup_abspath = apr_pstrdup(parent_db->pool, local_abspath);
+
+      if (!parent_db->pending_deletes)
+        parent_db->pending_deletes = apr_hash_make(parent_db->pool);
+
+      apr_hash_set(parent_db->pending_deletes, dup_abspath, APR_HASH_KEY_STRING,
+                   svn_node_kind_to_word(kind));
     }
-  else if (moved_from_abspath)
-    *moved_here = TRUE;
 
   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_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)
+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 = 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;
-  const svn_wc_conflict_version_t *left;
-  const svn_wc_conflict_version_t *right;
-
-  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));
-
-  if (tree_conflicted)
-    *tree_conflicted = FALSE;
+  if (merge_b->ctx->notify_func2 && db->pending_deletes)
+    {
+      apr_hash_index_t *hi;
 
-  /* Check for an obstructed or missing node on disk. */
-  {
-    svn_wc_notify_state_t obstr_state;
+      for (hi = apr_hash_first(scratch_pool, db->pending_deletes);
+           hi;
+           hi = apr_hash_next(hi))
+        {
+          const char *del_abspath = svn__apr_hash_index_key(hi);
+          svn_wc_notify_t *notify;
 
-    SVN_ERR(perform_obstruction_check(&obstr_state, NULL,
-                                      &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? */
-
-        if (content_state)
-          *content_state = obstr_state;
-        if (prop_state && obstr_state == svn_wc_notify_state_missing)
-          *prop_state = svn_wc_notify_state_missing;
-        return SVN_NO_ERROR;
-      }
-  }
+          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));
 
-  /* 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)
-            {
-              if (content_state)
-                *content_state = svn_wc_notify_state_missing;
-              if (prop_state)
-                *prop_state = svn_wc_notify_state_missing;
-              return SVN_NO_ERROR;
-            }
+          (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2,
+                                        notify, scratch_pool);
         }
 
-      /* This is use case 4 described in the paper attached to issue
-       * #2282.  See also notes/tree-conflicts/detection.txt
-       */
-      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 if (is_deleted)
-        reason = svn_wc_conflict_reason_deleted;
-      else
-        reason = svn_wc_conflict_reason_missing;
-      SVN_ERR(tree_conflict(merge_b, local_abspath, svn_node_file,
-                            svn_wc_conflict_action_edit, reason));
-      if (tree_conflicted)
-        *tree_conflicted = TRUE;
-      if (content_state)
-        *content_state = svn_wc_notify_state_missing;
-      if (prop_state)
-        *prop_state = svn_wc_notify_state_missing;
-      return SVN_NO_ERROR;
+      db->pending_deletes = NULL;
     }
-
-  /* ### 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;
+}
+
+/* Helper function for the merge_dir_*() and merge_file_*() functions.
+
+   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)
+    {
+      const char *dir_abspath = svn_dirent_dirname(local_abspath,
+                                                   scratch_pool);
+
+      SVN_ERR(mark_dir_edited(merge_b, db->parent_baton, dir_abspath,
+                              scratch_pool));
+    }
+
+  db->edited = TRUE;
+
+  if (! db->shadowed)
+    return SVN_NO_ERROR; /* Easy out */
+
+  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 */
+
+      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,
+                            (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;
+
+          (*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 if (db->tree_conflict_reason == CONFLICT_REASON_EXCLUDED)
+    {
+      /* open_directory() decided not to flag a tree conflict, while
+         there really should be one.
+
+         Ok: The skip for the descendant will do the right thing anyway!
+       */
+
+      /* We don't register the node in merge_b->skipped_abspaths here, as the
+         node itself is not skipped: the operation on a descendant is */
+    }
+  else if (db->tree_conflict_reason != CONFLICT_REASON_NONE)
+    {
+      /* open_directory() decided that a tree conflict should be raised */
+      const char *moved_away_abspath = NULL;
+
+      if (db->tree_conflict_reason == svn_wc_conflict_reason_deleted)
+        {
+          const char *ignored_path;
+          SVN_ERR(check_moved_away(&ignored_path, merge_b->ctx->wc_ctx,
+                                   local_abspath, scratch_pool));
+
+          if (ignored_path)
+            {
+              /* Local abspath has been moved away itself, as we only create
+                 tree conflict on the obstructing op-root.
+
+                 If only a descendant is moved away, we call the node itself
+                 deleted. */
+              db->tree_conflict_reason = svn_wc_conflict_reason_moved_away;
+              moved_away_abspath = local_abspath;
+            }
+        }
+
+      SVN_ERR(tree_conflict(merge_b, local_abspath, svn_node_dir,
+                            db->tree_conflict_action, db->tree_conflict_reason,
+                            moved_away_abspath));
+
+      SVN_ERR(record_tree_conflict(merge_b, local_abspath, svn_node_dir, scratch_pool));
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* 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 char *dir_abspath = svn_dirent_dirname(local_abspath,
+                                                   scratch_pool);
+
+      SVN_ERR(mark_dir_edited(merge_b, fb->parent_baton, dir_abspath,
+                              scratch_pool));
+    }
+
+  fb->edited = TRUE;
+
+  if (! fb->shadowed)
+    return SVN_NO_ERROR; /* Easy out */
+
+  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 (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_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 if (fb->tree_conflict_reason != CONFLICT_REASON_NONE
+           && fb->tree_conflict_reason != CONFLICT_REASON_EXCLUDED)
+    {
+      /* open_directory() decided that a tree conflict should be raised */
+      const char *moved_away_abspath = NULL;
+
+      if (fb->tree_conflict_reason == svn_wc_conflict_reason_deleted)
+        {
+          const char *ignored_path;
+          SVN_ERR(check_moved_away(&ignored_path, merge_b->ctx->wc_ctx,
+                                   local_abspath, scratch_pool));
+
+          if (ignored_path)
+            {
+              /* Local abspath has been moved away itself, as we only create
+                 tree conflict on the obstructing op-root.
+
+                 If only a descendant is moved away, we call the node itself
+                 deleted. */
+              fb->tree_conflict_reason = svn_wc_conflict_reason_moved_away;
+              moved_away_abspath = local_abspath;
+            }
+        }
+
+      SVN_ERR(tree_conflict(merge_b, local_abspath, svn_node_file,
+                            fb->tree_conflict_action, fb->tree_conflict_reason,
+                            moved_away_abspath));
+
+      SVN_ERR(record_tree_conflict(merge_b, local_abspath, svn_node_dir, scratch_pool));
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* An svn_diff_tree_processor_t function.
+
+   Called before either merge_file_changed(), merge_file_added(),
+   merge_file_deleted() or merge_file_closed(), unless it sets *SKIP to TRUE.
+
+   When *SKIP is TRUE, the diff driver avoids work on getting the details
+   for the closing callbacks.
+ */
+static svn_error_t *
+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)
+{
+  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);
+
+  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)
+    {
+      fb->parent_baton = pdb;
+      fb->shadowed = pdb->shadowed;
+      fb->skip_reason = pdb->skip_reason;
+    }
+
+  if (fb->shadowed)
+    {
+      /* An ancestor is tree conflicted. Nothing to do here. */
+    }
+  else if (left_source != NULL)
+    {
+      /* Node is expected to be a file, which will be changed or deleted. */
+      svn_node_kind_t kind;
+      svn_boolean_t is_deleted;
+
+      {
+        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)
+          {
+            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 != NULL)
+            {
+              svn_depth_t parent_depth;
+              const char *dir_abspath = svn_dirent_dirname(local_abspath,
+                                                           scratch_pool);
+
+              SVN_ERR(svn_wc__node_get_depth(&parent_depth,
+                                             merge_b->ctx->wc_ctx,
+                                             dir_abspath, scratch_pool));
+              if (parent_depth != svn_depth_unknown &&
+                  parent_depth < svn_depth_immediates)
+                {
+                  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 */
+            }
+
+          /* TODO: Start comparison mode to verify for delete tree conflict */
+        }
+    }
+  else
+    {
+      /* 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
+          && contains_path(pdb->pending_deletes, local_abspath))
+        {
+          fb->add_is_replace = TRUE;
+
+          apr_hash_set(pdb->pending_deletes, local_abspath,
+                       APR_HASH_KEY_STRING, NULL);
+        }
+
+      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, &kind,
+                                            merge_b, local_abspath,
+                                            svn_node_unknown,
+                                            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));
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* 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(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 = 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,
+                                              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(!left_file || svn_dirent_is_absolute(left_file));
+  SVN_ERR_ASSERT(!right_file || svn_dirent_is_absolute(right_file));
+
+  SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+
+  if (fb->shadowed)
+    {
+      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_update,
+                              fb->skip_reason, scratch_pool));
+        }
+
+      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! */
 
-  if (prop_state)
-    *prop_state = svn_wc_notify_state_unchanged;
+  property_state = svn_wc_notify_state_unchanged;
+  text_state = svn_wc_notify_state_unchanged;
 
-  if (prop_changes->nelts > 0)
-    {
-      /* Filter entry-props and unneeded properties in case of a record only
-         merge */
-      SVN_ERR(prepare_merge_props_changed(&prop_changes, local_abspath,
-                                          prop_changes, merge_b,
-                                          scratch_pool, scratch_pool));
-    }
+  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));
 
   /* 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,
@@ -1705,12 +2178,9 @@ merge_file_changed(svn_wc_notify_state_t
   /* Easy out: We are only applying mergeinfo differences. */
   if (merge_b->record_only)
     {
-      if (content_state)
-        *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;
@@ -1721,321 +2191,241 @@ 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));
 
-      /* Postpone all 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_state)
+      if (content_outcome == svn_wc_merge_conflict
+          || property_state == svn_wc_notify_state_conflicted)
         {
-          if (content_outcome == svn_wc_merge_conflict)
-            *content_state = svn_wc_notify_state_conflicted;
-          else if (has_local_mods
-                   && content_outcome != svn_wc_merge_unchanged)
-            *content_state = svn_wc_notify_state_merged;
-          else if (content_outcome == svn_wc_merge_merged)
-            *content_state = svn_wc_notify_state_changed;
-          else if (content_outcome == svn_wc_merge_no_merge)
-            *content_state = svn_wc_notify_state_missing;
-          else /* merge_outcome == svn_wc_merge_unchanged */
-            *content_state = svn_wc_notify_state_unchanged;
+          alloc_and_store_path(&merge_b->conflicted_paths, local_abspath,
+                               merge_b->pool);
         }
+
+      if (content_outcome == svn_wc_merge_conflict)
+        text_state = svn_wc_notify_state_conflicted;
+      else if (has_local_mods
+               && content_outcome != svn_wc_merge_unchanged)
+        text_state = svn_wc_notify_state_merged;
+      else if (content_outcome == svn_wc_merge_merged)
+        text_state = svn_wc_notify_state_changed;
+      else if (content_outcome == svn_wc_merge_no_merge)
+        text_state = svn_wc_notify_state_missing;
+      else /* merge_outcome == svn_wc_merge_unchanged */
+        text_state = svn_wc_notify_state_unchanged;
+    }
+
+  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,
+                                   text_state, property_state,
+                                   scratch_pool));
     }
 
   return SVN_NO_ERROR;
 }
 
-/* An svn_wc_diff_callbacks4_t function. */
-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,
+/* 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(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;
-  const char *mine_abspath = svn_dirent_join(merge_b->target->abspath,
-                                             mine_relpath, scratch_pool);
-  svn_node_kind_t wc_kind;
-  svn_node_kind_t kind;
-  int i;
-  apr_hash_t *file_props;
+  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,
+                                              relpath, scratch_pool);
+  apr_hash_t *pristine_props;
+  apr_hash_t *new_props;
+
+  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+  SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
 
-  SVN_ERR_ASSERT(svn_dirent_is_absolute(mine_abspath));
+  if (fb->shadowed)
+    {
+      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));
+        }
 
-  if (tree_conflicted)
-    *tree_conflicted = FALSE;
+      return SVN_NO_ERROR;
+    }
 
   /* Easy out: We are only applying mergeinfo differences. */
   if (merge_b->record_only)
     {
-      if (content_state)
-        *content_state = svn_wc_notify_state_unchanged;
-      if (prop_state)
-        *prop_state = svn_wc_notify_state_unchanged;
       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. */
-  if (prop_state)
-    *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;
+  if (!merge_b->dry_run)
+    {
+      const char *copyfrom_url;
+      svn_revnum_t copyfrom_rev;
+      svn_stream_t *new_contents, *pristine_contents;
+      svn_boolean_t existing_tree_conflict;
+      svn_error_t *err;
 
-      /* Issue #3383: We don't want mergeinfo from a foreign repository. */
-      if ((! merge_b->same_repos)
-          && strcmp(prop->name, SVN_PROP_MERGEINFO) == 0)
-        continue;
+      /* If this is a merge from the same repository as our
+         working copy, we handle adds as add-with-history.
+         Otherwise, we'll use a pure add. */
+      if (merge_b->same_repos)
+        {
+          const char *child =
+            svn_dirent_skip_ancestor(merge_b->target->abspath,
+                                     local_abspath);
+          SVN_ERR_ASSERT(child != NULL);
+          copyfrom_url = svn_path_url_add_component2(
+                                       merge_b->merge_source.loc2->url,
+                                       child, scratch_pool);
+          copyfrom_rev = right_source->revision;
+          SVN_ERR(check_repos_match(merge_b->target, local_abspath,
+                                    copyfrom_url, scratch_pool));
+          SVN_ERR(svn_stream_open_readonly(&pristine_contents,
+                                           right_file,
+                                           scratch_pool,
+                                           scratch_pool));
+          new_contents = NULL; /* inherit from new_base_contents */
 
-      apr_hash_set(file_props, prop->name, APR_HASH_KEY_STRING, prop->value);
-    }
+          pristine_props = right_props; /* Includes last_* information */
+          new_props = NULL; /* No local changes */
 
-  /* Check for an obstructed or missing node on disk. */
-  {
-    svn_wc_notify_state_t obstr_state;
+          if (apr_hash_get(pristine_props, SVN_PROP_MERGEINFO,
+                           APR_HASH_KEY_STRING))
+            {
+              alloc_and_store_path(&merge_b->paths_with_new_mergeinfo,
+                                   local_abspath, merge_b->pool);
+            }
+        }
+      else
+        {
+          apr_array_header_t *regular_props;
 
-    SVN_ERR(perform_obstruction_check(&obstr_state, NULL, NULL,
-                                      &wc_kind,
-                                      merge_b, mine_abspath, svn_node_unknown,
-                                      scratch_pool));
+          copyfrom_url = NULL;
+          copyfrom_rev = SVN_INVALID_REVNUM;
 
-    if (obstr_state != svn_wc_notify_state_inapplicable)
-      {
-        if (merge_b->dry_run && merge_b->added_path
-            && svn_dirent_is_child(merge_b->added_path, mine_abspath, NULL))
-          {
-            if (content_state)
-              *content_state = svn_wc_notify_state_changed;
-            if (prop_state && apr_hash_count(file_props))
-              *prop_state = svn_wc_notify_state_changed;
-          }
-        else if (content_state)
-          *content_state = obstr_state;
+          pristine_contents = svn_stream_empty(scratch_pool);
+          SVN_ERR(svn_stream_open_readonly(&new_contents, right_file,
+                                           scratch_pool, scratch_pool));
 
-        return SVN_NO_ERROR;
-      }
-  }
+          pristine_props = apr_hash_make(scratch_pool); /* Local addition */
 
-  SVN_ERR(svn_io_check_path(mine_abspath, &kind, scratch_pool));
-  switch (kind)
-    {
-    case svn_node_none:
-      {
-        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;
-
-            /* If this is a merge from the same repository as our
-               working copy, we handle adds as add-with-history.
-               Otherwise, we'll use a pure add. */
-            if (merge_b->same_repos)
-              {
-                const char *child =
-                  svn_dirent_skip_ancestor(merge_b->target->abspath,
-                                           mine_abspath);
-                SVN_ERR_ASSERT(child != NULL);
-                copyfrom_url = svn_path_url_add_component2(
-                                             merge_b->merge_source.loc2->url,
-                                             child, scratch_pool);
-                copyfrom_rev = rev2;
-                SVN_ERR(check_repos_match(merge_b->target, mine_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,
-                                                 scratch_pool,
-                                                 scratch_pool));
-                new_contents = NULL; /* inherit from new_base_contents */
-              }
-            else
-              {
-                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,
-                                                 scratch_pool, scratch_pool));
-              }
-
-            err = svn_wc_conflicted_p3(NULL, NULL, &existing_tree_conflict,
-                                       merge_b->ctx->wc_ctx, mine_abspath,
-                                       merge_b->pool);
+          /* 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));
 
-            if (err)
-              {
-                if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND)
-                  return svn_error_trace(err);
+          new_props = svn_prop_array_to_hash(regular_props, scratch_pool);
 
-                svn_error_clear(err);
-                existing_tree_conflict = FALSE;
-              }
+          /* Issue #3383: We don't want mergeinfo from a foreign repository. */
+          apr_hash_set(new_props, SVN_PROP_MERGEINFO, APR_HASH_KEY_STRING,
+                       NULL);
+        }
 
-            if (existing_tree_conflict)
-              {
-                svn_boolean_t moved_here;
-                svn_wc_conflict_reason_t reason;
+      err = svn_wc_conflicted_p3(NULL, NULL, &existing_tree_conflict,
+                                 merge_b->ctx->wc_ctx, local_abspath,
+                                 merge_b->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,
-                                         mine_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, mine_abspath,
-                                             svn_node_file,
-                                             svn_wc_conflict_action_add,
-                                             reason));
-                if (tree_conflicted)
-                  *tree_conflicted = TRUE;
-              }
-            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,
-                                               mine_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));
+      if (err)
+        {
+          if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND)
+            return svn_error_trace(err);
 
-                /* ### delete 'yours' ? */
-              }
-          }
-        if (content_state)
-          *content_state = svn_wc_notify_state_changed;
-        if (prop_state && apr_hash_count(file_props))
-          *prop_state = svn_wc_notify_state_changed;
-      }
-      break;
-    case svn_node_dir:
-      /* The file add the merge wants to carry out is obstructed by
-       * a directory, 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, mine_abspath, svn_node_file,
-                                   svn_wc_conflict_action_add,
-                                   svn_wc_conflict_reason_obstructed));
-      if (tree_conflicted)
-        *tree_conflicted = TRUE;
-      if (content_state)
-        {
-          /* directory already exists, is it under version control? */
-          if ((wc_kind != svn_node_none)
-              && dry_run_deleted_p(merge_b, mine_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_error_clear(err);
+          existing_tree_conflict = FALSE;
         }
-      break;
-    case svn_node_file:
-      {
-            if (dry_run_deleted_p(merge_b, mine_abspath))
-              {
-                if (content_state)
-                  *content_state = svn_wc_notify_state_changed;
-              }
-            else
-              {
-                svn_boolean_t moved_here;
-                svn_wc_conflict_reason_t reason;
 
-                /* The file add the merge wants to carry out is obstructed by
-                 * a versioned file. This file must have been added in the
-                 * history of the merge target, hence we flag a tree conflict
-                 * with reason 'added'. */
-                SVN_ERR(check_moved_here(&moved_here, merge_b->ctx->wc_ctx,
-                                         mine_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, mine_abspath, svn_node_file,
-                          svn_wc_conflict_action_add, reason));
+      if (existing_tree_conflict)
+        {
+          svn_boolean_t moved_here;
+          svn_wc_conflict_reason_t reason;
 
-                if (tree_conflicted)
-                  *tree_conflicted = TRUE;
-              }
-        break;
-      }
-    default:
-      if (content_state)
-        *content_state = svn_wc_notify_state_unknown;
-      break;
+          /* 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));
+
+          SVN_ERR(record_tree_conflict(merge_b, local_abspath, svn_node_file,
+                                       scratch_pool));
+          return SVN_NO_ERROR;
+        }
+      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,
+                                         pristine_contents,
+                                         new_contents,
+                                         pristine_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); */
+    }
+
+  SVN_ERR(record_update_add(merge_b, local_abspath, svn_node_file,
+                            fb->add_is_replace, scratch_pool));
 
   return SVN_NO_ERROR;
 }
@@ -2107,705 +2497,761 @@ files_same_p(svn_boolean_t *same,
                                                   FALSE, TRUE, NULL, NULL,
                                                   scratch_pool, scratch_pool));
 
-      SVN_ERR(svn_stream_open_readonly(&older_stream, older_abspath,
-                                       scratch_pool, scratch_pool));
+      SVN_ERR(svn_stream_open_readonly(&older_stream, older_abspath,
+                                       scratch_pool, scratch_pool));
+
+      SVN_ERR(svn_stream_contents_same2(same, mine_stream, older_stream,
+                                        scratch_pool));
+
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* 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(const char *relpath,
+                   const svn_diff_source_t *left_source,
+                   const char *left_file,
+                   /*const*/ apr_hash_t *left_props,
+                   void *file_baton,
+                   const struct svn_diff_tree_processor_t *processor,
+                   apr_pool_t *scratch_pool)
+{
+  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,
+                                              relpath, scratch_pool);
+  svn_boolean_t same;
+
+  SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+
+  if (fb->shadowed)
+    {
+      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,

[... 3729 lines stripped ...]