/*
 * conflicts.c:  conflict resolver implementation
 *
 * ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); you may not use this file except in compliance
 *    with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing,
 *    software distributed under the License is distributed on an
 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *    KIND, either express or implied.  See the License for the
 *    specific language governing permissions and limitations
 *    under the License.
 * ====================================================================
 */

/* ==================================================================== */



/*** Includes. ***/

#include "svn_types.h"
#include "svn_wc.h"
#include "svn_client.h"
#include "svn_error.h"
#include "svn_dirent_uri.h"
#include "svn_path.h"
#include "svn_pools.h"
#include "svn_props.h"
#include "svn_hash.h"
#include "svn_sorts.h"
#include "svn_subst.h"
#include "client.h"

#include "private/svn_diff_tree.h"
#include "private/svn_ra_private.h"
#include "private/svn_sorts_private.h"
#include "private/svn_token.h"
#include "private/svn_wc_private.h"

#include "svn_private_config.h"

#define ARRAY_LEN(ary) ((sizeof (ary)) / (sizeof ((ary)[0])))


/*** Dealing with conflicts. ***/

/* Describe a tree conflict. */
typedef svn_error_t *(*tree_conflict_get_description_func_t)(
  const char **change_description,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool);

/* Get more information about a tree conflict.
 * This function may contact the repository. */
typedef svn_error_t *(*tree_conflict_get_details_func_t)(
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool);

struct svn_client_conflict_t
{
  const char *local_abspath;
  apr_hash_t *prop_conflicts;

  /* Indicate which options were chosen to resolve a text or tree conflict
   * on the conflicted node. */
  svn_client_conflict_option_id_t resolution_text;
  svn_client_conflict_option_id_t resolution_tree;

  /* A mapping from const char* property name to pointers to
   * svn_client_conflict_option_t for all properties which had their
   * conflicts resolved. Indicates which options were chosen to resolve
   * the property conflicts. */
  apr_hash_t *resolved_props;

  /* Ask a tree conflict to describe itself. */
  tree_conflict_get_description_func_t
    tree_conflict_get_incoming_description_func;
  tree_conflict_get_description_func_t
    tree_conflict_get_local_description_func;

  /* Ask a tree conflict to find out more information about itself
   * by contacting the repository. */
  tree_conflict_get_details_func_t tree_conflict_get_incoming_details_func;
  tree_conflict_get_details_func_t tree_conflict_get_local_details_func;

  /* Any additional information found can be stored here and may be used
   * when describing a tree conflict. */
  void *tree_conflict_incoming_details;
  void *tree_conflict_local_details;

  /* The pool this conflict was allocated from. */
  apr_pool_t *pool;

  /* Conflict data provided by libsvn_wc. */
  const svn_wc_conflict_description2_t *legacy_text_conflict;
  const char *legacy_prop_conflict_propname;
  const svn_wc_conflict_description2_t *legacy_tree_conflict;

  /* The recommended resolution option's ID. */
  svn_client_conflict_option_id_t recommended_option_id;
};

/* Resolves conflict to OPTION and sets CONFLICT->RESOLUTION accordingly.
 *
 * May raise an error in case the conflict could not be resolved. A common
 * case would be a tree conflict the resolution of which depends on other
 * tree conflicts to be resolved first. */
typedef svn_error_t *(*conflict_option_resolve_func_t)(
  svn_client_conflict_option_t *option,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool);

struct svn_client_conflict_option_t
{
  svn_client_conflict_option_id_t id;
  const char *label;
  const char *description;

  svn_client_conflict_t *conflict;
  conflict_option_resolve_func_t do_resolve_func;

  /* The pool this option was allocated from. */
  apr_pool_t *pool;

  /* Data which is specific to particular conflicts and options. */
  union {
    struct {
      /* Indicates the property to resolve in case of a property conflict.
       * If set to "", all properties are resolved to this option. */
      const char *propname;

      /* A merged property value, if supplied by the API user, else NULL. */
      const svn_string_t *merged_propval;
    } prop;
  } type_data;

};

/*
 * Return a legacy conflict choice corresponding to OPTION_ID.
 * Return svn_wc_conflict_choose_undefined if no corresponding
 * legacy conflict choice exists.
 */
static svn_wc_conflict_choice_t
conflict_option_id_to_wc_conflict_choice(
  svn_client_conflict_option_id_t option_id)
{

  switch (option_id)
    {
      case svn_client_conflict_option_undefined:
        return svn_wc_conflict_choose_undefined;

      case svn_client_conflict_option_postpone:
        return svn_wc_conflict_choose_postpone;

      case svn_client_conflict_option_base_text:
        return svn_wc_conflict_choose_base;

      case svn_client_conflict_option_incoming_text:
        return svn_wc_conflict_choose_theirs_full;

      case svn_client_conflict_option_working_text:
        return svn_wc_conflict_choose_mine_full;

      case svn_client_conflict_option_incoming_text_where_conflicted:
        return svn_wc_conflict_choose_theirs_conflict;

      case svn_client_conflict_option_working_text_where_conflicted:
        return svn_wc_conflict_choose_mine_conflict;

      case svn_client_conflict_option_merged_text:
        return svn_wc_conflict_choose_merged;

      case svn_client_conflict_option_unspecified:
        return svn_wc_conflict_choose_unspecified;

      default:
        break;
    }

  return svn_wc_conflict_choose_undefined;
}

static void
add_legacy_desc_to_conflict(const svn_wc_conflict_description2_t *desc,
                            svn_client_conflict_t *conflict,
                            apr_pool_t *result_pool)
{
  switch (desc->kind)
    {
      case svn_wc_conflict_kind_text:
        conflict->legacy_text_conflict = desc;
        break;

      case svn_wc_conflict_kind_property:
        if (conflict->prop_conflicts == NULL)
          conflict->prop_conflicts = apr_hash_make(result_pool);
        svn_hash_sets(conflict->prop_conflicts, desc->property_name, desc);
        conflict->legacy_prop_conflict_propname = desc->property_name;
        break;

      case svn_wc_conflict_kind_tree:
        conflict->legacy_tree_conflict = desc;
        break;

      default:
        SVN_ERR_ASSERT_NO_RETURN(FALSE); /* unknown kind of conflict */
    }
}

/* A map for svn_wc_conflict_action_t values to strings */
static const svn_token_map_t map_conflict_action[] =
{
  { "edit",             svn_wc_conflict_action_edit },
  { "delete",           svn_wc_conflict_action_delete },
  { "add",              svn_wc_conflict_action_add },
  { "replace",          svn_wc_conflict_action_replace },
  { NULL,               0 }
};

/* A map for svn_wc_conflict_reason_t values to strings */
static const svn_token_map_t map_conflict_reason[] =
{
  { "edit",             svn_wc_conflict_reason_edited },
  { "delete",           svn_wc_conflict_reason_deleted },
  { "missing",          svn_wc_conflict_reason_missing },
  { "obstruction",      svn_wc_conflict_reason_obstructed },
  { "add",              svn_wc_conflict_reason_added },
  { "replace",          svn_wc_conflict_reason_replaced },
  { "unversioned",      svn_wc_conflict_reason_unversioned },
  { "moved-away",       svn_wc_conflict_reason_moved_away },
  { "moved-here",       svn_wc_conflict_reason_moved_here },
  { NULL,               0 }
};

/* Describes a server-side move (really a copy+delete within the same
 * revision) which was identified by scanning the revision log.
 * This structure can represent one or more "chains" of moves, i.e.
 * multiple move operations which occurred across a range of revisions. */
struct repos_move_info {
  /* The revision in which this move was committed. */
  svn_revnum_t rev;

  /* The author who committed the revision in which this move was committed. */
  const char *rev_author;

  /* The repository relpath the node was moved from in this revision. */
  const char *moved_from_repos_relpath;

  /* The repository relpath the node was moved to in this revision. */
  const char *moved_to_repos_relpath;

  /* The copyfrom revision of the moved-to path. */
  svn_revnum_t copyfrom_rev;

  /* The node kind of the item being moved. */
  svn_node_kind_t node_kind;

  /* Prev pointer. NULL if no prior move exists in the chain. */
  struct repos_move_info *prev;

  /* An array of struct repos_move_info * elements, each representing
   * a possible way forward in the move chain. NULL if no next move
   * exists in this chain. If the deleted node was copied only once in
   * this revision, then this array has only one element and the move
   * chain does not fork. But if this revision contains multiple copies of
   * the deleted node, each of these copies appears as an element of this
   * array, and each element represents a different path the next move
   * might have taken. */
  apr_array_header_t *next;
};

static svn_revnum_t
rev_below(svn_revnum_t rev)
{
  SVN_ERR_ASSERT_NO_RETURN(rev != SVN_INVALID_REVNUM);
  SVN_ERR_ASSERT_NO_RETURN(rev > 0);

  return rev == 1 ? 1 : rev - 1;
}

/* Set *RELATED to true if the deleted node DELETED_REPOS_RELPATH@DELETED_REV
 * is an ancestor of the copied node COPYFROM_PATH@COPYFROM_REV.
 * If CHECK_LAST_CHANGED_REV is non-zero, also ensure that the copied node
 * is a copy of the deleted node's last-changed revision's content, rather
 * than a copy of some older content. If it's not, set *RELATED to false. */
static svn_error_t *
check_move_ancestry(svn_boolean_t *related,
                    svn_ra_session_t *ra_session,
                    const char *repos_root_url,
                    const char *deleted_repos_relpath,
                    svn_revnum_t deleted_rev,
                    const char *copyfrom_path,
                    svn_revnum_t copyfrom_rev,
                    svn_boolean_t check_last_changed_rev,
                    apr_pool_t *scratch_pool)
{
  apr_hash_t *locations;
  const char *deleted_url;
  const char *deleted_location;
  apr_array_header_t *location_revisions;
  const char *old_session_url;

  location_revisions = apr_array_make(scratch_pool, 1, sizeof(svn_revnum_t));
  APR_ARRAY_PUSH(location_revisions, svn_revnum_t) = copyfrom_rev;
  deleted_url = svn_uri_canonicalize(apr_pstrcat(scratch_pool,
                                                 repos_root_url, "/",
                                                 deleted_repos_relpath,
                                                 NULL),
                                     scratch_pool);
  SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session,
                                            deleted_url, scratch_pool));
  SVN_ERR(svn_ra_get_locations(ra_session, &locations, "",
                               rev_below(deleted_rev), location_revisions,
                               scratch_pool));

  deleted_location = apr_hash_get(locations, &copyfrom_rev,
                                  sizeof(svn_revnum_t));
  if (deleted_location)
    {
      if (deleted_location[0] == '/')
        deleted_location++;
      if (strcmp(deleted_location, copyfrom_path) != 0)
        {
          *related = FALSE;
          return SVN_NO_ERROR;
        }
    }
  else
    {
      *related = FALSE;
      return SVN_NO_ERROR;
    }

  if (check_last_changed_rev)
    {
      svn_dirent_t *dirent;

      /* Verify that copyfrom_rev >= last-changed revision of the
       * deleted node. */
      SVN_ERR(svn_ra_stat(ra_session, "", rev_below(deleted_rev), &dirent,
                          scratch_pool));
      if (dirent == NULL || copyfrom_rev < dirent->created_rev)
        {
          *related = FALSE;
          return SVN_NO_ERROR;
        }
    }

  *related = TRUE;
  return SVN_NO_ERROR;
}

struct copy_info {
  const char *copyto_path;
  const char *copyfrom_path;
  svn_revnum_t copyfrom_rev;
  svn_node_kind_t node_kind;
};

/* Allocate and return a NEW_MOVE, and update MOVED_PATHS with this new move. */
static svn_error_t *
add_new_move(struct repos_move_info **new_move,
             const char *deleted_repos_relpath,
             const char *copyto_path,
             svn_revnum_t copyfrom_rev,
             svn_node_kind_t node_kind,
             svn_revnum_t revision,
             const char *author,
             apr_hash_t *moved_paths,
             svn_ra_session_t *ra_session,
             const char *repos_root_url,
             apr_pool_t *result_pool,
             apr_pool_t *scratch_pool)
{
  struct repos_move_info *move;
  struct repos_move_info *next_move;

  move = apr_pcalloc(result_pool, sizeof(*move));
  move->moved_from_repos_relpath = apr_pstrdup(result_pool,
                                               deleted_repos_relpath);
  move->moved_to_repos_relpath = apr_pstrdup(result_pool, copyto_path);
  move->rev = revision;
  move->rev_author = apr_pstrdup(result_pool, author);
  move->copyfrom_rev = copyfrom_rev;
  move->node_kind = node_kind;

  /* Link together multiple moves of the same node.
   * Note that we're traversing history backwards, so moves already
   * present in the list happened in younger revisions. */
  next_move = svn_hash_gets(moved_paths, move->moved_to_repos_relpath);
  if (next_move)
    {
      svn_boolean_t related;

      /* Tracing back history of the delete-half of the next move
       * to the copyfrom-revision of the prior move we must end up
       * at the delete-half of the prior move. */
      SVN_ERR(check_move_ancestry(&related, ra_session, repos_root_url,
                                  next_move->moved_from_repos_relpath,
                                  next_move->rev,
                                  move->moved_from_repos_relpath,
                                  move->copyfrom_rev,
                                  FALSE, scratch_pool));
      if (related)
        {
          SVN_ERR_ASSERT(move->rev < next_move->rev);

          /* Prepend this move to the linked list. */
          if (move->next == NULL)
            move->next = apr_array_make(result_pool, 1,
                                        sizeof (struct repos_move_info *));
          APR_ARRAY_PUSH(move->next, struct repos_move_info *) = next_move;
          next_move->prev = move;
        }
    }

  /* Make this move the head of our next-move linking map. */
  svn_hash_sets(moved_paths, move->moved_from_repos_relpath, move);

  *new_move = move;
  return SVN_NO_ERROR;
}

/* Push a MOVE into the MOVES_TABLE. */
static void
push_move(struct repos_move_info *move, apr_hash_t *moves_table,
          apr_pool_t *result_pool)
{
  apr_array_header_t *moves;

  /* Add this move to the list of moves in the revision. */
  moves = apr_hash_get(moves_table, &move->rev, sizeof(svn_revnum_t));
  if (moves == NULL)
    {
      /* It is the first move in this revision. Create the list. */
      moves = apr_array_make(result_pool, 1, sizeof(struct repos_move_info *));
      apr_hash_set(moves_table, &move->rev, sizeof(svn_revnum_t), moves);
    }
  APR_ARRAY_PUSH(moves, struct repos_move_info *) = move;
}

/* Find the youngest common ancestor of REPOS_RELPATH1@PEG_REV1 and
 * REPOS_RELPATH2@PEG_REV2. Return the result in *YCA_LOC.
 * Set *YCA_LOC to NULL if no common ancestor exists. */
static svn_error_t *
find_yca(svn_client__pathrev_t **yca_loc,
         const char *repos_relpath1,
         svn_revnum_t peg_rev1,
         const char *repos_relpath2,
         svn_revnum_t peg_rev2,
         const char *repos_root_url,
         const char *repos_uuid,
         svn_ra_session_t *ra_session,
         svn_client_ctx_t *ctx,
         apr_pool_t *result_pool,
         apr_pool_t *scratch_pool)
{
  svn_client__pathrev_t *loc1;
  svn_client__pathrev_t *loc2;

  *yca_loc = NULL;

  loc1 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid,
                                                 peg_rev1, repos_relpath1,
                                                 scratch_pool);
  loc2 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid,
                                                 peg_rev2, repos_relpath2,
                                                 scratch_pool);
  SVN_ERR(svn_client__get_youngest_common_ancestor(yca_loc, loc1, loc2,
                                                   ra_session, ctx,
                                                   result_pool, scratch_pool));

  return SVN_NO_ERROR;
}

/* Like find_yca, expect that a YCA could also be found via a brute-force
 * search of parents of REPOS_RELPATH1 and REPOS_RELPATH2, if no "direct"
 * YCA exists. An implicit assumption is that some parent of REPOS_RELPATH1
 * is a branch of some parent of REPOS_RELPATH2.
 *
 * This function can guess a "good enough" YCA for 'missing nodes' which do
 * not exist in the working copy, e.g. when a file edit is merged to a path
 * which does not exist in the working copy.
 */
static svn_error_t *
find_nearest_yca(svn_client__pathrev_t **yca_locp,
                 const char *repos_relpath1,
                 svn_revnum_t peg_rev1,
                 const char *repos_relpath2,
                 svn_revnum_t peg_rev2,
                 const char *repos_root_url,
                 const char *repos_uuid,
                 svn_ra_session_t *ra_session,
                 svn_client_ctx_t *ctx,
                 apr_pool_t *result_pool,
                 apr_pool_t *scratch_pool)
{
  svn_client__pathrev_t *yca_loc;
  svn_error_t *err;
  apr_pool_t *iterpool;
  const char *p1, *p2;
  apr_size_t c1, c2;

  *yca_locp = NULL;

  iterpool = svn_pool_create(scratch_pool);

  p1 = repos_relpath1;
  c1 = svn_path_component_count(repos_relpath1);
  while (c1--)
    {
      svn_pool_clear(iterpool);

      p2 = repos_relpath2;
      c2 = svn_path_component_count(repos_relpath2);
      while (c2--)
        {
          err = find_yca(&yca_loc, p1, peg_rev1, p2, peg_rev2,
                         repos_root_url, repos_uuid, ra_session, ctx,
                         result_pool, iterpool);
          if (err)
            {
              if (err->apr_err == SVN_ERR_FS_NOT_FOUND)
                {
                  svn_error_clear(err);
                  yca_loc = NULL;
                }
              else
                return svn_error_trace(err);
            }

          if (yca_loc)
            {
              *yca_locp = yca_loc;
              svn_pool_destroy(iterpool);
              return SVN_NO_ERROR;
            }

          p2 = svn_relpath_dirname(p2, scratch_pool);
        }

      p1 = svn_relpath_dirname(p1, scratch_pool);
    }

  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

/* Check if the copied node described by COPY and the DELETED_PATH@DELETED_REV
 * share a common ancestor. If so, return new repos_move_info in *MOVE which
 * describes a move from the deleted path to that copy's destination. */
static svn_error_t *
find_related_move(struct repos_move_info **move,
                  struct copy_info *copy,
                  const char *deleted_repos_relpath,
                  svn_revnum_t deleted_rev,
                  const char *author,
                  apr_hash_t *moved_paths,
                  const char *repos_root_url,
                  const char *repos_uuid,
                  svn_client_ctx_t *ctx,
                  svn_ra_session_t *ra_session,
                  apr_pool_t *result_pool,
                  apr_pool_t *scratch_pool)
{
  svn_client__pathrev_t *yca_loc;
  svn_error_t *err;

  *move = NULL;
  err = find_yca(&yca_loc, copy->copyfrom_path, copy->copyfrom_rev,
                 deleted_repos_relpath, rev_below(deleted_rev),
                 repos_root_url, repos_uuid, ra_session, ctx,
                 scratch_pool, scratch_pool);
  if (err)
    {
      if (err->apr_err == SVN_ERR_FS_NOT_FOUND)
        {
          svn_error_clear(err);
          yca_loc = NULL;
        }
      else
        return svn_error_trace(err);
    }

  if (yca_loc)
    SVN_ERR(add_new_move(move, deleted_repos_relpath,
                         copy->copyto_path, copy->copyfrom_rev,
                         copy->node_kind, deleted_rev, author,
                         moved_paths, ra_session, repos_root_url,
                         result_pool, scratch_pool));

  return SVN_NO_ERROR;
}

/* Detect moves by matching DELETED_REPOS_RELPATH@DELETED_REV to the copies
 * in COPIES. Add any moves found to MOVES_TABLE and update MOVED_PATHS. */
static svn_error_t *
match_copies_to_deletion(const char *deleted_repos_relpath,
                         svn_revnum_t deleted_rev,
                         const char *author,
                         apr_hash_t *copies,
                         apr_hash_t *moves_table,
                         apr_hash_t *moved_paths,
                         const char *repos_root_url,
                         const char *repos_uuid,
                         svn_ra_session_t *ra_session,
                         svn_client_ctx_t *ctx,
                         apr_pool_t *result_pool,
                         apr_pool_t *scratch_pool)
{
  apr_hash_index_t *hi;
  apr_pool_t *iterpool;

  iterpool = svn_pool_create(scratch_pool);
  for (hi = apr_hash_first(scratch_pool, copies);
       hi != NULL;
       hi = apr_hash_next(hi))
    {
      const char *copyfrom_path = apr_hash_this_key(hi);
      apr_array_header_t *copies_with_same_source_path;
      int i;

      svn_pool_clear(iterpool);

      copies_with_same_source_path = apr_hash_this_val(hi);

      if (strcmp(copyfrom_path, deleted_repos_relpath) == 0)
        {
          /* We found a copyfrom path which matches a deleted node.
           * Check if the deleted node is an ancestor of the copied node. */
          for (i = 0; i < copies_with_same_source_path->nelts; i++)
            {
              struct copy_info *copy;
              svn_boolean_t related;
              struct repos_move_info *move;

              copy = APR_ARRAY_IDX(copies_with_same_source_path, i,
                                   struct copy_info *);
              SVN_ERR(check_move_ancestry(&related,
                                          ra_session, repos_root_url,
                                          deleted_repos_relpath,
                                          deleted_rev,
                                          copy->copyfrom_path,
                                          copy->copyfrom_rev,
                                          TRUE, iterpool));
              if (!related)
                continue;

              /* Remember details of this move. */
              SVN_ERR(add_new_move(&move, deleted_repos_relpath,
                                   copy->copyto_path, copy->copyfrom_rev,
                                   copy->node_kind, deleted_rev, author,
                                   moved_paths, ra_session, repos_root_url,
                                   result_pool, iterpool));
              push_move(move, moves_table, result_pool);
            }
        }
      else
        {
          /* Check if this deleted node is related to any copies in this
           * revision. These could be moves of the deleted node which
           * were merged here from other lines of history. */
          for (i = 0; i < copies_with_same_source_path->nelts; i++)
            {
              struct copy_info *copy;
              struct repos_move_info *move = NULL;

              copy = APR_ARRAY_IDX(copies_with_same_source_path, i,
                                   struct copy_info *);
              SVN_ERR(find_related_move(&move, copy, deleted_repos_relpath,
                                        deleted_rev, author,
                                        moved_paths,
                                        repos_root_url, repos_uuid,
                                        ctx, ra_session,
                                        result_pool, iterpool));
              if (move)
                push_move(move, moves_table, result_pool);
            }
        }
    }
  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

/* Update MOVES_TABLE and MOVED_PATHS based on information from
 * revision data in LOG_ENTRY, COPIES, and DELETED_PATHS.
 * Use RA_SESSION to perform the necessary requests. */
static svn_error_t *
find_moves_in_revision(svn_ra_session_t *ra_session,
                       apr_hash_t *moves_table,
                       apr_hash_t *moved_paths,
                       svn_log_entry_t *log_entry,
                       apr_hash_t *copies,
                       apr_array_header_t *deleted_paths,
                       const char *repos_root_url,
                       const char *repos_uuid,
                       svn_client_ctx_t *ctx,
                       apr_pool_t *result_pool,
                       apr_pool_t *scratch_pool)
{
  apr_pool_t *iterpool;
  int i;
  const svn_string_t *author;

  author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR);
  iterpool = svn_pool_create(scratch_pool);
  for (i = 0; i < deleted_paths->nelts; i++)
    {
      const char *deleted_repos_relpath;

      svn_pool_clear(iterpool);

      deleted_repos_relpath = APR_ARRAY_IDX(deleted_paths, i, const char *);
      SVN_ERR(match_copies_to_deletion(deleted_repos_relpath,
                                       log_entry->revision,
                                       author ? author->data
                                              : _("unknown author"),
                                       copies, moves_table, moved_paths,
                                       repos_root_url, repos_uuid, ra_session,
                                       ctx, result_pool, iterpool));
    }
  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

struct find_deleted_rev_baton
{
  /* Variables below are arguments provided by the caller of
   * svn_ra_get_log2(). */
  const char *deleted_repos_relpath;
  const char *related_repos_relpath;
  svn_revnum_t related_peg_rev;
  const char *repos_root_url;
  const char *repos_uuid;
  svn_client_ctx_t *ctx;
  const char *victim_abspath; /* for notifications */

  /* Variables below are results for the caller of svn_ra_get_log2(). */
  svn_revnum_t deleted_rev;
  const char *deleted_rev_author;
  svn_node_kind_t replacing_node_kind;
  apr_pool_t *result_pool;

  apr_hash_t *moves_table; /* Obtained from find_moves_in_revision(). */
  struct repos_move_info *move; /* Last known move which affected the node. */

  /* Extra RA session that can be used to make additional requests. */
  svn_ra_session_t *extra_ra_session;
};

/* If DELETED_RELPATH matches the moved-from path of a move in MOVES,
 * or if DELETED_RELPATH is a child of a moved-to path in MOVES, return
 * a struct move_info for the corresponding move. Else, return NULL. */
static struct repos_move_info *
map_deleted_path_to_move(const char *deleted_relpath,
                         apr_array_header_t *moves,
                         apr_pool_t *scratch_pool)
{
  struct repos_move_info *closest_move = NULL;
  apr_size_t min_components = 0;
  int i;

  for (i = 0; i < moves->nelts; i++)
    {
      const char *relpath;
      struct repos_move_info *move;

      move = APR_ARRAY_IDX(moves, i, struct repos_move_info *);
      if (strcmp(move->moved_from_repos_relpath, deleted_relpath) == 0)
        return move;

      relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath,
                                          deleted_relpath);
      if (relpath)
        {
          /* This could be a nested move. Return the path-wise closest move. */
          const apr_size_t c = svn_path_component_count(relpath);
          if (c == 0)
             return move;
          else if (min_components == 0 || c < min_components)
            {
              min_components = c;
              closest_move = move;
            }
        }
    }

  if (closest_move)
    {
      const char *relpath;

      /* See if we can find an even closer move for this moved-along path. */
      relpath = svn_relpath_skip_ancestor(closest_move->moved_to_repos_relpath,
                                          deleted_relpath);
      if (relpath && relpath[0] != '\0')
        {
          struct repos_move_info *move;
          const char *moved_along_path =
            svn_relpath_join(closest_move->moved_from_repos_relpath, relpath,
                             scratch_pool);
          move = map_deleted_path_to_move(moved_along_path, moves, scratch_pool);
          if (move)
            return move;
        }
    }

  return closest_move;
}

/* Search for nested moves in REVISION, given the already found MOVES,
 * all DELETED_PATHS, and all COPIES, from the same revision.
 * Append any nested moves to the MOVES array. */
static svn_error_t *
find_nested_moves(apr_array_header_t *moves,
                  apr_hash_t *copies,
                  apr_array_header_t *deleted_paths,
                  apr_hash_t *moved_paths,
                  svn_revnum_t revision,
                  const char *author,
                  const char *repos_root_url,
                  const char *repos_uuid,
                  svn_ra_session_t *ra_session,
                  svn_client_ctx_t *ctx,
                  apr_pool_t *result_pool,
                  apr_pool_t *scratch_pool)
{
  apr_array_header_t *nested_moves;
  int i;
  apr_pool_t *iterpool;

  nested_moves = apr_array_make(result_pool, 0,
                                sizeof(struct repos_move_info *));
  iterpool = svn_pool_create(scratch_pool);
  for (i = 0; i < deleted_paths->nelts; i++)
    {
      const char *deleted_path;
      const char *child_relpath;
      const char *moved_along_repos_relpath;
      struct repos_move_info *move;
      apr_array_header_t *copies_with_same_source_path;
      int j;
      svn_boolean_t related;

      svn_pool_clear(iterpool);

      deleted_path = APR_ARRAY_IDX(deleted_paths, i, const char *);
      move = map_deleted_path_to_move(deleted_path, moves, iterpool);
      if (move == NULL)
        continue;
      child_relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath,
                                                deleted_path);
      if (child_relpath == NULL || child_relpath[0] == '\0')
        continue; /* not a nested move */

      /* Consider: svn mv A B; svn mv B/foo C/foo
       * Copyfrom for C/foo is A/foo, even though C/foo was moved here from
       * B/foo. A/foo was not deleted. It is B/foo which was deleted.
       * We now know about the move A->B and moved-along child_relpath "foo".
       * Try to detect an ancestral relationship between A/foo and the
       * moved-along path. */
      moved_along_repos_relpath =
        svn_relpath_join(move->moved_from_repos_relpath, child_relpath,
                         iterpool);
      copies_with_same_source_path = svn_hash_gets(copies,
                                                   moved_along_repos_relpath);
      if (copies_with_same_source_path == NULL)
        continue; /* not a nested move */

      for (j = 0; j < copies_with_same_source_path->nelts; j++)
        {
          struct copy_info *copy;

          copy = APR_ARRAY_IDX(copies_with_same_source_path, j,
                               struct copy_info *);
          SVN_ERR(check_move_ancestry(&related, ra_session, repos_root_url,
                                      moved_along_repos_relpath,
                                      revision,
                                      copy->copyfrom_path,
                                      copy->copyfrom_rev,
                                      TRUE, iterpool));
          if (related)
            {
              struct repos_move_info *nested_move;

              /* Remember details of this move. */
              SVN_ERR(add_new_move(&nested_move, moved_along_repos_relpath,
                                   copy->copyto_path, copy->copyfrom_rev,
                                   copy->node_kind,
                                   revision, author, moved_paths,
                                   ra_session, repos_root_url,
                                   result_pool, iterpool));

              /* Add this move to the list of nested moves in this revision. */
              APR_ARRAY_PUSH(nested_moves, struct repos_move_info *) =
                nested_move;
            }
        }
    }
  svn_pool_destroy(iterpool);

  /* Add all nested moves found to the list of all moves in this revision. */
  apr_array_cat(moves, nested_moves);

  return SVN_NO_ERROR;
}

/* Make a shallow copy of the copied LOG_ITEM in COPIES. */
static void
cache_copied_item(apr_hash_t *copies, const char *changed_path,
                  svn_log_changed_path2_t *log_item)
{
  apr_pool_t *result_pool = apr_hash_pool_get(copies);
  struct copy_info *copy = apr_palloc(result_pool, sizeof(*copy));
  apr_array_header_t *copies_with_same_source_path;

  copy->copyfrom_path = log_item->copyfrom_path;
  if (log_item->copyfrom_path[0] == '/')
    copy->copyfrom_path++;
  copy->copyto_path = changed_path;
  copy->copyfrom_rev = log_item->copyfrom_rev;
  copy->node_kind = log_item->node_kind;

  copies_with_same_source_path = apr_hash_get(copies, copy->copyfrom_path,
                                              APR_HASH_KEY_STRING);
  if (copies_with_same_source_path == NULL)
    {
      copies_with_same_source_path = apr_array_make(result_pool, 1,
                                                    sizeof(struct copy_info *));
      apr_hash_set(copies, copy->copyfrom_path, APR_HASH_KEY_STRING,
                   copies_with_same_source_path);
    }
  APR_ARRAY_PUSH(copies_with_same_source_path, struct copy_info *) = copy;
}

/* Implements svn_log_entry_receiver_t.
 *
 * Find the revision in which a node, optionally ancestrally related to the
 * node specified via find_deleted_rev_baton, was deleted, When the revision
 * was found, store it in BATON->DELETED_REV and abort the log operation
 * by raising SVN_ERR_CEASE_INVOCATION.
 *
 * If no such revision can be found, leave BATON->DELETED_REV and
 * BATON->REPLACING_NODE_KIND alone.
 *
 * If the node was replaced, set BATON->REPLACING_NODE_KIND to the node
 * kind of the node which replaced the original node. If the node was not
 * replaced, set BATON->REPLACING_NODE_KIND to svn_node_none.
 *
 * This function answers the same question as svn_ra_get_deleted_rev() but
 * works in cases where we do not already know a revision in which the deleted
 * node once used to exist.
 *
 * If the node was moved, rather than deleted, return move information
 * in BATON->MOVE.
 */
static svn_error_t *
find_deleted_rev(void *baton,
                 svn_log_entry_t *log_entry,
                 apr_pool_t *scratch_pool)
{
  struct find_deleted_rev_baton *b = baton;
  apr_hash_index_t *hi;
  apr_pool_t *iterpool;
  svn_boolean_t deleted_node_found = FALSE;
  svn_node_kind_t replacing_node_kind = svn_node_none;

  if (b->ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(
                 b->victim_abspath,
                 svn_wc_notify_tree_conflict_details_progress,
                 scratch_pool),
      notify->revision = log_entry->revision;
      b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool);
    }

  /* No paths were changed in this revision.  Nothing to do. */
  if (! log_entry->changed_paths2)
    return SVN_NO_ERROR;

  iterpool = svn_pool_create(scratch_pool);
  for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2);
       hi != NULL;
       hi = apr_hash_next(hi))
    {
      const char *changed_path = apr_hash_this_key(hi);
      svn_log_changed_path2_t *log_item = apr_hash_this_val(hi);

      svn_pool_clear(iterpool);

      /* ### Remove leading slash from paths in log entries. */
      if (changed_path[0] == '/')
          changed_path++;

      /* Check if we already found the deleted node we're looking for. */
      if (!deleted_node_found &&
          svn_path_compare_paths(b->deleted_repos_relpath, changed_path) == 0 &&
          (log_item->action == 'D' || log_item->action == 'R'))
        {
          deleted_node_found = TRUE;

          if (b->related_repos_relpath != NULL &&
              b->related_peg_rev != SVN_INVALID_REVNUM)
            {
              svn_client__pathrev_t *yca_loc;
              svn_error_t *err;

              /* We found a deleted node which occupies the correct path.
               * To be certain that this is the deleted node we're looking for,
               * we must establish whether it is ancestrally related to the
               * "related node" specified in our baton. */
              err = find_yca(&yca_loc,
                             b->related_repos_relpath,
                             b->related_peg_rev,
                             b->deleted_repos_relpath,
                             rev_below(log_entry->revision),
                             b->repos_root_url, b->repos_uuid,
                             b->extra_ra_session, b->ctx, iterpool, iterpool);
              if (err)
                {
                  /* ### Happens for moves within other moves and copies. */
                  if (err->apr_err == SVN_ERR_FS_NOT_FOUND)
                    {
                      svn_error_clear(err);
                      yca_loc = NULL;
                    }
                  else
                    return svn_error_trace(err);
                }

              deleted_node_found = (yca_loc != NULL);
            }

          if (deleted_node_found && log_item->action == 'R')
            replacing_node_kind = log_item->node_kind;
        }
    }
  svn_pool_destroy(iterpool);

  if (!deleted_node_found)
    {
      apr_array_header_t *moves;

      if (b->moves_table == NULL)
        return SVN_NO_ERROR;

      moves = apr_hash_get(b->moves_table, &log_entry->revision,
                           sizeof(svn_revnum_t));
      if (moves)
        {
          struct repos_move_info *move;

          move = map_deleted_path_to_move(b->deleted_repos_relpath,
                                          moves, scratch_pool);
          if (move)
            {
              const char *relpath;

              /* The node was moved. Update our search path accordingly. */
              b->move = move;
              relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath,
                                                  b->deleted_repos_relpath);
              if (relpath)
                b->deleted_repos_relpath =
                  svn_relpath_join(move->moved_from_repos_relpath, relpath,
                                   b->result_pool);
            }
        }
    }
  else
    {
      svn_string_t *author;

      b->deleted_rev = log_entry->revision;
      author = svn_hash_gets(log_entry->revprops,
                             SVN_PROP_REVISION_AUTHOR);
      if (author)
        b->deleted_rev_author = apr_pstrdup(b->result_pool, author->data);
      else
        b->deleted_rev_author = _("unknown author");

      b->replacing_node_kind = replacing_node_kind;

      /* We're done. Abort the log operation. */
      return svn_error_create(SVN_ERR_CEASE_INVOCATION, NULL, NULL);
    }

  return SVN_NO_ERROR;
}

/* Return a localised string representation of the local part of a tree
   conflict on a file. */
static svn_error_t *
describe_local_file_node_change(const char **description,
                                svn_client_conflict_t *conflict,
                                svn_client_ctx_t *ctx,
                                apr_pool_t *result_pool,
                                apr_pool_t *scratch_pool)
{
  svn_wc_conflict_reason_t local_change;
  svn_wc_operation_t operation;

  local_change = svn_client_conflict_get_local_change(conflict);
  operation = svn_client_conflict_get_operation(conflict);

  switch (local_change)
    {
      case svn_wc_conflict_reason_edited:
        if (operation == svn_wc_operation_update ||
            operation == svn_wc_operation_switch)
          *description = _("A file containing uncommitted changes was "
                           "found in the working copy.");
        else if (operation == svn_wc_operation_merge)
          *description = _("A file which differs from the corresponding "
                           "file on the merge source branch was found "
                           "in the working copy.");
        break;
      case svn_wc_conflict_reason_obstructed:
        *description = _("A file which already occupies this path was found "
                         "in the working copy.");
        break;
      case svn_wc_conflict_reason_unversioned:
        *description = _("An unversioned file was found in the working "
                         "copy.");
        break;
      case svn_wc_conflict_reason_deleted:
        *description = _("A deleted file was found in the working copy.");
        break;
      case svn_wc_conflict_reason_missing:
        if (operation == svn_wc_operation_update ||
            operation == svn_wc_operation_switch)
          *description = _("No such file was found in the working copy.");
        else if (operation == svn_wc_operation_merge)
          {
            /* ### display deleted revision */
            *description = _("No such file was found in the merge target "
                             "working copy.\nPerhaps the file has been "
                             "deleted or moved away in the repository's "
                             "history?");
          }
        break;
      case svn_wc_conflict_reason_added:
      case svn_wc_conflict_reason_replaced:
        {
          /* ### show more details about copies or replacements? */
          *description = _("A file scheduled to be added to the "
                           "repository in the next commit was found in "
                           "the working copy.");
        }
        break;
      case svn_wc_conflict_reason_moved_away:
        {
          const char *moved_to_abspath;
          svn_error_t *err;

          err = svn_wc__node_was_moved_away(&moved_to_abspath, NULL,
                                            ctx->wc_ctx,
                                            conflict->local_abspath,
                                            scratch_pool,
                                            scratch_pool);
          if (err)
            {
              if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
                {
                  moved_to_abspath = NULL;
                  svn_error_clear(err);
                }
              else
                return svn_error_trace(err);
            }
          if (operation == svn_wc_operation_update ||
              operation == svn_wc_operation_switch)
            {
              if (moved_to_abspath == NULL)
                {
                  /* The move no longer exists. */
                  *description = _("The file in the working copy had "
                                   "been moved away at the time this "
                                   "conflict was recorded.");
                }
              else
                {
                  const char *wcroot_abspath;

                  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath,
                                             ctx->wc_ctx,
                                             conflict->local_abspath,
                                             scratch_pool,
                                             scratch_pool));
                  *description = apr_psprintf(
                                   result_pool,
                                   _("The file in the working copy was "
                                     "moved away to\n'%s'."),
                                   svn_dirent_local_style(
                                     svn_dirent_skip_ancestor(
                                       wcroot_abspath,
                                       moved_to_abspath),
                                     scratch_pool));
                }
            }
          else if (operation == svn_wc_operation_merge)
            {
              if (moved_to_abspath == NULL)
                {
                  /* The move probably happened in branch history.
                   * This case cannot happen until we detect incoming
                   * moves, which we currently don't do. */
                  /* ### find deleted/moved revision? */
                  *description = _("The file in the working copy had "
                                   "been moved away at the time this "
                                   "conflict was recorded.");
                }
              else
                {
                  /* This is a local move in the working copy. */
                  const char *wcroot_abspath;

                  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath,
                                             ctx->wc_ctx,
                                             conflict->local_abspath,
                                             scratch_pool,
                                             scratch_pool));
                  *description = apr_psprintf(
                                   result_pool,
                                   _("The file in the working copy was "
                                     "moved away to\n'%s'."),
                                   svn_dirent_local_style(
                                     svn_dirent_skip_ancestor(
                                       wcroot_abspath,
                                       moved_to_abspath),
                                     scratch_pool));
                }
            }
          break;
        }
      case svn_wc_conflict_reason_moved_here:
        {
          const char *moved_from_abspath;

          SVN_ERR(svn_wc__node_was_moved_here(&moved_from_abspath, NULL,
                                              ctx->wc_ctx,
                                              conflict->local_abspath,
                                              scratch_pool,
                                              scratch_pool));
          if (operation == svn_wc_operation_update ||
              operation == svn_wc_operation_switch)
            {
              if (moved_from_abspath == NULL)
                {
                  /* The move no longer exists. */
                  *description = _("A file had been moved here in the "
                                   "working copy at the time this "
                                   "conflict was recorded.");
                }
              else
                {
                  const char *wcroot_abspath;

                  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath,
                                             ctx->wc_ctx,
                                             conflict->local_abspath,
                                             scratch_pool,
                                             scratch_pool));
                  *description = apr_psprintf(
                                   result_pool,
                                   _("A file was moved here in the "
                                     "working copy from\n'%s'."),
                                   svn_dirent_local_style(
                                     svn_dirent_skip_ancestor(
                                       wcroot_abspath,
                                       moved_from_abspath),
                                     scratch_pool));
                }
            }
          else if (operation == svn_wc_operation_merge)
            {
              if (moved_from_abspath == NULL)
                {
                  /* The move probably happened in branch history.
                   * This case cannot happen until we detect incoming
                   * moves, which we currently don't do. */
                  /* ### find deleted/moved revision? */
                  *description = _("A file had been moved here in the "
                                   "working copy at the time this "
                                   "conflict was recorded.");
                }
              else
                {
                  const char *wcroot_abspath;

                  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath,
                                             ctx->wc_ctx,
                                             conflict->local_abspath,
                                             scratch_pool,
                                             scratch_pool));
                  /* This is a local move in the working copy. */
                  *description = apr_psprintf(
                                   result_pool,
                                   _("A file was moved here in the "
                                     "working copy from\n'%s'."),
                                   svn_dirent_local_style(
                                     svn_dirent_skip_ancestor(
                                       wcroot_abspath,
                                       moved_from_abspath),
                                     scratch_pool));
                }
            }
          break;
        }
    }

  return SVN_NO_ERROR;
}

/* Return a localised string representation of the local part of a tree
   conflict on a directory. */
static svn_error_t *
describe_local_dir_node_change(const char **description,
                               svn_client_conflict_t *conflict,
                               svn_client_ctx_t *ctx,
                               apr_pool_t *result_pool,
                               apr_pool_t *scratch_pool)
{
  svn_wc_conflict_reason_t local_change;
  svn_wc_operation_t operation;

  local_change = svn_client_conflict_get_local_change(conflict);
  operation = svn_client_conflict_get_operation(conflict);

  switch (local_change)
    {
      case svn_wc_conflict_reason_edited:
        if (operation == svn_wc_operation_update ||
            operation == svn_wc_operation_switch)
          *description = _("A directory containing uncommitted changes "
                           "was found in the working copy.");
        else if (operation == svn_wc_operation_merge)
          *description = _("A directory which differs from the "
                           "corresponding directory on the merge source "
                           "branch was found in the working copy.");
        break;
      case svn_wc_conflict_reason_obstructed:
        *description = _("A directory which already occupies this path was "
                         "found in the working copy.");
        break;
      case svn_wc_conflict_reason_unversioned:
        *description = _("An unversioned directory was found in the "
                         "working copy.");
        break;
      case svn_wc_conflict_reason_deleted:
        *description = _("A deleted directory was found in the "
                         "working copy.");
        break;
      case svn_wc_conflict_reason_missing:
        if (operation == svn_wc_operation_update ||
            operation == svn_wc_operation_switch)
          *description = _("No such directory was found in the working copy.");
        else if (operation == svn_wc_operation_merge)
          {
            /* ### display deleted revision */
            *description = _("No such directory was found in the merge "
                             "target working copy.\nPerhaps the "
                             "directory has been deleted or moved away "
                             "in the repository's history?");
          }
        break;
      case svn_wc_conflict_reason_added:
      case svn_wc_conflict_reason_replaced:
        {
          /* ### show more details about copies or replacements? */
          *description = _("A directory scheduled to be added to the "
                           "repository in the next commit was found in "
                           "the working copy.");
        }
        break;
      case svn_wc_conflict_reason_moved_away:
        {
          const char *moved_to_abspath;
          svn_error_t *err;

          err = svn_wc__node_was_moved_away(&moved_to_abspath, NULL,
                                            ctx->wc_ctx,
                                            conflict->local_abspath,
                                            scratch_pool,
                                            scratch_pool);
          if (err)
            {
              if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
                {
                  moved_to_abspath = NULL;
                  svn_error_clear(err);
                }
              else
                return svn_error_trace(err);
            }

          if (operation == svn_wc_operation_update ||
              operation == svn_wc_operation_switch)
            {
              if (moved_to_abspath == NULL)
                {
                  /* The move no longer exists. */
                  *description = _("The directory in the working copy "
                                   "had been moved away at the time "
                                   "this conflict was recorded.");
                }
              else
                {
                  const char *wcroot_abspath;

                  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath,
                                             ctx->wc_ctx,
                                             conflict->local_abspath,
                                             scratch_pool,
                                             scratch_pool));
                  *description = apr_psprintf(
                                   result_pool,
                                   _("The directory in the working copy "
                                     "was moved away to\n'%s'."),
                                   svn_dirent_local_style(
                                     svn_dirent_skip_ancestor(
                                       wcroot_abspath,
                                       moved_to_abspath),
                                     scratch_pool));
                }
            }
          else if (operation == svn_wc_operation_merge)
            {
              if (moved_to_abspath == NULL)
                {
                  /* The move probably happened in branch history.
                   * This case cannot happen until we detect incoming
                   * moves, which we currently don't do. */
                  /* ### find deleted/moved revision? */
                  *description = _("The directory had been moved away "
                                   "at the time this conflict was "
                                   "recorded.");
                }
              else
                {
                  /* This is a local move in the working copy. */
                  const char *wcroot_abspath;

                  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath,
                                             ctx->wc_ctx,
                                             conflict->local_abspath,
                                             scratch_pool,
                                             scratch_pool));
                  *description = apr_psprintf(
                                   result_pool,
                                   _("The directory was moved away to\n"
                                     "'%s'."),
                                   svn_dirent_local_style(
                                     svn_dirent_skip_ancestor(
                                       wcroot_abspath,
                                       moved_to_abspath),
                                     scratch_pool));
                }
            }
          }
          break;
      case svn_wc_conflict_reason_moved_here:
        {
          const char *moved_from_abspath;

          SVN_ERR(svn_wc__node_was_moved_here(&moved_from_abspath, NULL,
                                              ctx->wc_ctx,
                                              conflict->local_abspath,
                                              scratch_pool,
                                              scratch_pool));
          if (operation == svn_wc_operation_update ||
              operation == svn_wc_operation_switch)
            {
              if (moved_from_abspath == NULL)
                {
                  /* The move no longer exists. */
                  *description = _("A directory had been moved here at "
                                   "the time this conflict was "
                                   "recorded.");
                }
              else
                {
                  const char *wcroot_abspath;

                  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath,
                                             ctx->wc_ctx,
                                             conflict->local_abspath,
                                             scratch_pool,
                                             scratch_pool));
                  *description = apr_psprintf(
                                   result_pool,
                                   _("A directory was moved here from\n"
                                     "'%s'."),
                                   svn_dirent_local_style(
                                     svn_dirent_skip_ancestor(
                                       wcroot_abspath,
                                       moved_from_abspath),
                                     scratch_pool));
                }
            }
          else if (operation == svn_wc_operation_merge)
            {
              if (moved_from_abspath == NULL)
                {
                  /* The move probably happened in branch history.
                   * This case cannot happen until we detect incoming
                   * moves, which we currently don't do. */
                  /* ### find deleted/moved revision? */
                  *description = _("A directory had been moved here at "
                                   "the time this conflict was "
                                   "recorded.");
                }
              else
                {
                  /* This is a local move in the working copy. */
                  const char *wcroot_abspath;

                  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath,
                                             ctx->wc_ctx,
                                             conflict->local_abspath,
                                             scratch_pool,
                                             scratch_pool));
                  *description = apr_psprintf(
                                   result_pool,
                                   _("A directory was moved here in "
                                     "the working copy from\n'%s'."),
                                   svn_dirent_local_style(
                                     svn_dirent_skip_ancestor(
                                       wcroot_abspath,
                                       moved_from_abspath),
                                     scratch_pool));
                }
            }
        }
    }

  return SVN_NO_ERROR;
}

struct find_moves_baton
{
  /* Variables below are arguments provided by the caller of
   * svn_ra_get_log2(). */
  const char *repos_root_url;
  const char *repos_uuid;
  svn_client_ctx_t *ctx;
  const char *victim_abspath; /* for notifications */
  apr_pool_t *result_pool;

  /* A hash table mapping a revision number to an array of struct
   * repos_move_info * elements, describing moves.
   *
   * Must be allocated in RESULT_POOL by the caller of svn_ra_get_log2().
   *
   * If the node was moved, the DELETED_REV is present in this table,
   * perhaps along with additional revisions.
   *
   * Given a sequence of moves which happened in the repository, such as:
   *   rA: mv x->z
   *   rA: mv a->b
   *   rB: mv b->c
   *   rC: mv c->d
   * we map each revision number to all the moves which happened in the
   * revision, which looks as follows:
   *   rA : [(x->z), (a->b)]
   *   rB : [(b->c)]
   *   rC : [(c->d)]
   * This allows us to later find relevant moves based on a revision number.
   *
   * Additionally, we embed the number of the revision in which a move was
   * found inside the repos_move_info structure:
   *   rA : [(rA, x->z), (rA, a->b)]
   *   rB : [(rB, b->c)]
   *   rC : [(rC, c->d)]
   * And also, all moves pertaining to the same node are chained into a
   * doubly-linked list via 'next' and 'prev' pointers (see definition of
   * struct repos_move_info). This can be visualized as follows:
   *   rA : [(rA, x->z, prev=>NULL, next=>NULL),
   *         (rA, a->b, prev=>NULL, next=>(rB, b->c))]
   *   rB : [(rB, b->c), prev=>(rA, a->b), next=>(rC, c->d)]
   *   rC : [(rC, c->d), prev=>(rB, c->d), next=>NULL]
   * This way, we can look up all moves relevant to a node, forwards and
   * backwards in history, once we have located one move in the chain.
   *
   * In the above example, the data tells us that within the revision
   * range rA:C, a was moved to d. However, within the revision range
   * rA;B, a was moved to b.
   */
  apr_hash_t *moves_table;

  /* Variables below hold state for find_moves() and are not
   * intended to be used by the caller of svn_ra_get_log2().
   * Like all other variables, they must be initialized, however. */

  /* Temporary map of moved paths to struct repos_move_info.
   * Used to link multiple moves of the same node across revisions. */
  apr_hash_t *moved_paths;

  /* Extra RA session that can be used to make additional requests. */
  svn_ra_session_t *extra_ra_session;
};

/* Implements svn_log_entry_receiver_t. */
static svn_error_t *
find_moves(void *baton, svn_log_entry_t *log_entry, apr_pool_t *scratch_pool)
{
  struct find_moves_baton *b = baton;
  apr_hash_index_t *hi;
  apr_pool_t *iterpool;
  apr_array_header_t *deleted_paths;
  apr_hash_t *copies;
  apr_array_header_t *moves;

  if (b->ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(
                 b->victim_abspath,
                 svn_wc_notify_tree_conflict_details_progress,
                 scratch_pool),
      notify->revision = log_entry->revision;
      b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool);
    }

  /* No paths were changed in this revision.  Nothing to do. */
  if (! log_entry->changed_paths2)
    return SVN_NO_ERROR;

  copies = apr_hash_make(scratch_pool);
  deleted_paths = apr_array_make(scratch_pool, 0, sizeof(const char *));
  iterpool = svn_pool_create(scratch_pool);
  for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2);
       hi != NULL;
       hi = apr_hash_next(hi))
    {
      const char *changed_path = apr_hash_this_key(hi);
      svn_log_changed_path2_t *log_item = apr_hash_this_val(hi);

      svn_pool_clear(iterpool);

      /* ### Remove leading slash from paths in log entries. */
      if (changed_path[0] == '/')
          changed_path++;

      /* For move detection, scan for copied nodes in this revision. */
      if (log_item->action == 'A' && log_item->copyfrom_path)
        cache_copied_item(copies, changed_path, log_item);

      /* For move detection, store all deleted_paths. */
      if (log_item->action == 'D' || log_item->action == 'R')
        APR_ARRAY_PUSH(deleted_paths, const char *) =
          apr_pstrdup(scratch_pool, changed_path);
    }
  svn_pool_destroy(iterpool);

  /* Check for moves in this revision */
  SVN_ERR(find_moves_in_revision(b->extra_ra_session,
                                 b->moves_table, b->moved_paths,
                                 log_entry, copies, deleted_paths,
                                 b->repos_root_url, b->repos_uuid,
                                 b->ctx, b->result_pool, scratch_pool));

  moves = apr_hash_get(b->moves_table, &log_entry->revision,
                       sizeof(svn_revnum_t));
  if (moves)
    {
      const svn_string_t *author;

      author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR);
      SVN_ERR(find_nested_moves(moves, copies, deleted_paths,
                                b->moved_paths, log_entry->revision,
                                author ? author->data : _("unknown author"),
                                b->repos_root_url,
                                b->repos_uuid,
                                b->extra_ra_session, b->ctx,
                                b->result_pool, scratch_pool));
    }

  return SVN_NO_ERROR;
}

/* Find all moves which occured in repository history starting at
 * REPOS_RELPATH@START_REV until END_REV (where START_REV > END_REV).
 * Return results in *MOVES_TABLE (see struct find_moves_baton for details). */
static svn_error_t *
find_moves_in_revision_range(struct apr_hash_t **moves_table,
                             const char *repos_relpath,
                             const char *repos_root_url,
                             const char *repos_uuid,
                             const char *victim_abspath,
                             svn_revnum_t start_rev,
                             svn_revnum_t end_rev,
                             svn_client_ctx_t *ctx,
                             apr_pool_t *result_pool,
                             apr_pool_t *scratch_pool)
{
  svn_ra_session_t *ra_session;
  const char *url;
  const char *corrected_url;
  apr_array_header_t *paths;
  apr_array_header_t *revprops;
  struct find_moves_baton b = { 0 };

  SVN_ERR_ASSERT(start_rev > end_rev);

  url = svn_path_url_add_component2(repos_root_url, repos_relpath,
                                    scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               url, NULL, NULL, FALSE, FALSE,
                                               ctx, scratch_pool,
                                               scratch_pool));

  paths = apr_array_make(scratch_pool, 1, sizeof(const char *));
  APR_ARRAY_PUSH(paths, const char *) = "";

  revprops = apr_array_make(scratch_pool, 1, sizeof(const char *));
  APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR;

  b.repos_root_url = repos_root_url;
  b.repos_uuid = repos_uuid;
  b.ctx = ctx;
  b.victim_abspath = victim_abspath;
  b.moves_table = apr_hash_make(result_pool);
  b.moved_paths = apr_hash_make(scratch_pool);
  b.result_pool = result_pool;
  SVN_ERR(svn_ra__dup_session(&b.extra_ra_session, ra_session, NULL,
                              scratch_pool, scratch_pool));

  SVN_ERR(svn_ra_get_log2(ra_session, paths, start_rev, end_rev,
                          0, /* no limit */
                          TRUE, /* need the changed paths list */
                          FALSE, /* need to traverse copies */
                          FALSE, /* no need for merged revisions */
                          revprops,
                          find_moves, &b,
                          scratch_pool));

  *moves_table = b.moves_table;

  return SVN_NO_ERROR;
}

/* Return new move information for a moved-along child MOVED_ALONG_RELPATH.
 * Set MOVE->NODE_KIND to MOVED_ALONG_NODE_KIND.
 * Do not copy MOVE->NEXT and MOVE-PREV.
 * If MOVED_ALONG_RELPATH is empty, this effectively copies MOVE to
 * RESULT_POOL with NEXT and PREV pointers cleared. */
static struct repos_move_info *
new_path_adjusted_move(struct repos_move_info *move,
                       const char *moved_along_relpath,
                       svn_node_kind_t moved_along_node_kind,
                       apr_pool_t *result_pool)
{
  struct repos_move_info *new_move;

  new_move = apr_pcalloc(result_pool, sizeof(*new_move));
  new_move->moved_from_repos_relpath =
    svn_relpath_join(move->moved_from_repos_relpath, moved_along_relpath,
                     result_pool);
  new_move->moved_to_repos_relpath =
    svn_relpath_join(move->moved_to_repos_relpath, moved_along_relpath,
                     result_pool);
  new_move->rev = move->rev;
  new_move->rev_author = apr_pstrdup(result_pool, move->rev_author);
  new_move->copyfrom_rev = move->copyfrom_rev;
  new_move->node_kind = moved_along_node_kind;
  /* Ignore prev and next pointers. Caller will set them if needed. */

  return new_move;
}

/* Given a list of MOVES_IN_REVISION, figure out which of these moves again
 * move the node which was already moved by PREV_MOVE in the past . */
static svn_error_t *
find_next_moves_in_revision(apr_array_header_t **next_moves,
                            apr_array_header_t *moves_in_revision,
                            struct repos_move_info *prev_move,
                            svn_ra_session_t *ra_session,
                            const char *repos_root_url,
                            apr_pool_t *result_pool,
                            apr_pool_t *scratch_pool)
{
  int i;
  apr_pool_t *iterpool;

  iterpool = svn_pool_create(scratch_pool);
  for (i = 0; i < moves_in_revision->nelts; i++)
    {
      struct repos_move_info *move;
      const char *relpath;
      const char *deleted_repos_relpath;
      svn_boolean_t related;
      svn_error_t *err;

      svn_pool_clear(iterpool);

      /* Check if this move affects the current known path of our node. */
      move = APR_ARRAY_IDX(moves_in_revision, i, struct repos_move_info *);
      relpath = svn_relpath_skip_ancestor(move->moved_from_repos_relpath,
                                          prev_move->moved_to_repos_relpath);
      if (relpath == NULL)
        continue;

      /* It does. So our node must have been deleted again. */
      deleted_repos_relpath = svn_relpath_join(move->moved_from_repos_relpath,
                                               relpath, iterpool);

      /* Tracing back history of the delete-half of this move to the
       * copyfrom-revision of the prior move we must end up at the
       * delete-half of the prior move. */
      err = check_move_ancestry(&related, ra_session, repos_root_url,
                                deleted_repos_relpath, move->rev,
                                prev_move->moved_from_repos_relpath,
                                prev_move->copyfrom_rev,
                                FALSE, scratch_pool);
      if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
        {
          svn_error_clear(err);
          continue;
        }
      else
        SVN_ERR(err);

      if (related)
        {
          struct repos_move_info *new_move;

          /* We have a winner. */
          new_move = new_path_adjusted_move(move, relpath, prev_move->node_kind,
                                            result_pool);
          if (*next_moves == NULL)
            *next_moves = apr_array_make(result_pool, 1,
                                         sizeof(struct repos_move_info *));
          APR_ARRAY_PUSH(*next_moves, struct repos_move_info *) = new_move;
        }
    }
  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

static int
compare_items_as_revs(const svn_sort__item_t *a, const svn_sort__item_t *b)
{
  return svn_sort_compare_revisions(a->key, b->key);
}

/* Starting at MOVE->REV, loop over future revisions which contain moves,
 * and look for matching next moves in each. Once found, return a list of
 * (ambiguous, if more than one) moves in *NEXT_MOVES. */
static svn_error_t *
find_next_moves(apr_array_header_t **next_moves,
                apr_hash_t *moves_table,
                struct repos_move_info *move,
                svn_ra_session_t *ra_session,
                const char *repos_root_url,
                apr_pool_t *result_pool,
                apr_pool_t *scratch_pool)
{
  apr_array_header_t *moves;
  apr_array_header_t *revisions;
  apr_pool_t *iterpool;
  int i;

  *next_moves = NULL;
  revisions = svn_sort__hash(moves_table, compare_items_as_revs, scratch_pool);
  iterpool = svn_pool_create(scratch_pool);
  for (i = 0; i < revisions->nelts; i++)
    {
      svn_sort__item_t item = APR_ARRAY_IDX(revisions, i, svn_sort__item_t);
      svn_revnum_t rev = *(svn_revnum_t *)item.key;

      svn_pool_clear(iterpool);

      if (rev <= move->rev)
        continue;

      moves = apr_hash_get(moves_table, &rev, sizeof(rev));
      SVN_ERR(find_next_moves_in_revision(next_moves, moves, move,
                                          ra_session, repos_root_url,
                                          result_pool, iterpool));
      if (*next_moves)
        break;
    }
  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

/* Trace all future moves of the node moved by MOVE.
 * Update MOVE->PREV and MOVE->NEXT accordingly. */
static svn_error_t *
trace_moved_node(apr_hash_t *moves_table,
                 struct repos_move_info *move,
                 svn_ra_session_t *ra_session,
                 const char *repos_root_url,
                 apr_pool_t *result_pool,
                 apr_pool_t *scratch_pool)
{
  apr_array_header_t *next_moves;

  SVN_ERR(find_next_moves(&next_moves, moves_table, move,
                          ra_session, repos_root_url,
                          result_pool, scratch_pool));
  if (next_moves)
    {
      int i;
      apr_pool_t *iterpool;

      move->next = next_moves;
      iterpool = svn_pool_create(scratch_pool);
      for (i = 0; i < next_moves->nelts; i++)
        {
          struct repos_move_info *next_move;

          svn_pool_clear(iterpool);
          next_move = APR_ARRAY_IDX(next_moves, i, struct repos_move_info *);
          next_move->prev = move;
          SVN_ERR(trace_moved_node(moves_table, next_move,
                                   ra_session, repos_root_url,
                                   result_pool, iterpool));
        }
      svn_pool_destroy(iterpool);
    }

  return SVN_NO_ERROR;
}

/* Given a list of MOVES_IN_REVISION, figure out which of these moves
 * move the node which was later on moved by NEXT_MOVE. */
static svn_error_t *
find_prev_move_in_revision(struct repos_move_info **prev_move,
                           apr_array_header_t *moves_in_revision,
                           struct repos_move_info *next_move,
                           svn_ra_session_t *ra_session,
                           const char *repos_root_url,
                           apr_pool_t *result_pool,
                           apr_pool_t *scratch_pool)
{
  int i;
  apr_pool_t *iterpool;

  *prev_move = NULL;

  iterpool = svn_pool_create(scratch_pool);
  for (i = 0; i < moves_in_revision->nelts; i++)
    {
      struct repos_move_info *move;
      const char *relpath;
      const char *deleted_repos_relpath;
      svn_boolean_t related;
      svn_error_t *err;

      svn_pool_clear(iterpool);

      /* Check if this move affects the current known path of our node. */
      move = APR_ARRAY_IDX(moves_in_revision, i, struct repos_move_info *);
      relpath = svn_relpath_skip_ancestor(next_move->moved_from_repos_relpath,
                                          move->moved_to_repos_relpath);
      if (relpath == NULL)
        continue;

      /* It does. So our node must have been deleted. */
      deleted_repos_relpath = svn_relpath_join(
                                next_move->moved_from_repos_relpath,
                                relpath, iterpool);

      /* Tracing back history of the delete-half of the next move to the
       * copyfrom-revision of the prior move we must end up at the
       * delete-half of the prior move. */
      err = check_move_ancestry(&related, ra_session, repos_root_url,
                                deleted_repos_relpath, next_move->rev,
                                move->moved_from_repos_relpath,
                                move->copyfrom_rev,
                                FALSE, scratch_pool);
      if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
        {
          svn_error_clear(err);
          continue;
        }
      else
        SVN_ERR(err);

      if (related)
        {
          /* We have a winner. */
          *prev_move = new_path_adjusted_move(move, relpath,
                                              next_move->node_kind,
                                              result_pool);
          break;
        }
    }
  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

static int
compare_items_as_revs_reverse(const svn_sort__item_t *a,
                              const svn_sort__item_t *b)
{
  int c = svn_sort_compare_revisions(a->key, b->key);
  if (c < 0)
    return 1;
  if (c > 0)
    return -1;
  return c;
}

/* Starting at MOVE->REV, loop over past revisions which contain moves,
 * and look for a matching previous move in each. Once found, return
 * it in *PREV_MOVE */
static svn_error_t *
find_prev_move(struct repos_move_info **prev_move,
               apr_hash_t *moves_table,
               struct repos_move_info *move,
               svn_ra_session_t *ra_session,
               const char *repos_root_url,
               apr_pool_t *result_pool,
               apr_pool_t *scratch_pool)
{
  apr_array_header_t *moves;
  apr_array_header_t *revisions;
  apr_pool_t *iterpool;
  int i;

  *prev_move = NULL;
  revisions = svn_sort__hash(moves_table, compare_items_as_revs_reverse,
                             scratch_pool);
  iterpool = svn_pool_create(scratch_pool);
  for (i = 0; i < revisions->nelts; i++)
    {
      svn_sort__item_t item = APR_ARRAY_IDX(revisions, i, svn_sort__item_t);
      svn_revnum_t rev = *(svn_revnum_t *)item.key;

      svn_pool_clear(iterpool);

      if (rev >= move->rev)
        continue;

      moves = apr_hash_get(moves_table, &rev, sizeof(rev));
      SVN_ERR(find_prev_move_in_revision(prev_move, moves, move,
                                         ra_session, repos_root_url,
                                         result_pool, iterpool));
      if (*prev_move)
        break;
    }
  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}


/* Trace all past moves of the node moved by MOVE.
 * Update MOVE->PREV and MOVE->NEXT accordingly. */
static svn_error_t *
trace_moved_node_backwards(apr_hash_t *moves_table,
                           struct repos_move_info *move,
                           svn_ra_session_t *ra_session,
                           const char *repos_root_url,
                           apr_pool_t *result_pool,
                           apr_pool_t *scratch_pool)
{
  struct repos_move_info *prev_move;

  SVN_ERR(find_prev_move(&prev_move, moves_table, move,
                         ra_session, repos_root_url,
                         result_pool, scratch_pool));
  if (prev_move)
    {
      move->prev = prev_move;
      prev_move->next = apr_array_make(result_pool, 1,
                                       sizeof(struct repos_move_info *));
      APR_ARRAY_PUSH(prev_move->next, struct repos_move_info *) = move;

      SVN_ERR(trace_moved_node_backwards(moves_table, prev_move,
                                         ra_session, repos_root_url,
                                         result_pool, scratch_pool));
    }

  return SVN_NO_ERROR;
}

/* Scan MOVES_TABLE for moves which affect a particular deleted node, and
 * build a set of new move information for this node.
 * Return heads of all possible move chains in *MOVES.
 *
 * MOVES_TABLE describes moves which happened at arbitrary paths in the
 * repository. DELETED_REPOS_RELPATH may have been moved directly or it
 * may have been moved along with a parent path. Move information returned
 * from this function represents how DELETED_REPOS_RELPATH itself was moved
 * from one path to another, effectively "zooming in" on the effective move
 * operations which occurred for this particular node. */
static svn_error_t *
find_operative_moves(apr_array_header_t **moves,
                     apr_hash_t *moves_table,
                     const char *deleted_repos_relpath,
                     svn_revnum_t deleted_rev,
                     svn_ra_session_t *ra_session,
                     const char *repos_root_url,
                     apr_pool_t *result_pool,
                     apr_pool_t *scratch_pool)
{
  apr_array_header_t *moves_in_deleted_rev;
  int i;
  apr_pool_t *iterpool;
  const char *session_url, *url = NULL;

  moves_in_deleted_rev = apr_hash_get(moves_table, &deleted_rev,
                                      sizeof(deleted_rev));
  if (moves_in_deleted_rev == NULL)
    {
      *moves = NULL;
      return SVN_NO_ERROR;
    }

  SVN_ERR(svn_ra_get_session_url(ra_session, &session_url, scratch_pool));

  /* Look for operative moves in the revision where the node was deleted. */
  *moves = apr_array_make(scratch_pool, 0, sizeof(struct repos_move_info *));
  iterpool = svn_pool_create(scratch_pool);
  for (i = 0; i < moves_in_deleted_rev->nelts; i++)
    {
      struct repos_move_info *move;
      const char *relpath;

      svn_pool_clear(iterpool);

      move = APR_ARRAY_IDX(moves_in_deleted_rev, i, struct repos_move_info *);
      if (strcmp(move->moved_from_repos_relpath, deleted_repos_relpath) == 0)
        {
          APR_ARRAY_PUSH(*moves, struct repos_move_info *) = move;
          continue;
        }

      /* Test for an operative nested move. */
      relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath,
                                          deleted_repos_relpath);
      if (relpath && relpath[0] != '\0')
        {
          struct repos_move_info *nested_move;
          const char *actual_deleted_repos_relpath;

          actual_deleted_repos_relpath =
              svn_relpath_join(move->moved_from_repos_relpath, relpath,
                               iterpool);
          nested_move = map_deleted_path_to_move(actual_deleted_repos_relpath,
                                                 moves_in_deleted_rev,
                                                 iterpool);
          if (nested_move)
            APR_ARRAY_PUSH(*moves, struct repos_move_info *) = nested_move;
        }
    }

  if (url != NULL)
    SVN_ERR(svn_ra_reparent(ra_session, session_url, scratch_pool));

  /* If we didn't find any applicable moves, return NULL. */
  if ((*moves)->nelts == 0)
    {
      *moves = NULL;
      svn_pool_destroy(iterpool);
      return SVN_NO_ERROR;
   }

  /* Figure out what happened to these moves in future revisions. */
  for (i = 0; i < (*moves)->nelts; i++)
    {
      struct repos_move_info *move;

      svn_pool_clear(iterpool);

      move = APR_ARRAY_IDX(*moves, i, struct repos_move_info *);
      SVN_ERR(trace_moved_node(moves_table, move, ra_session, repos_root_url,
                               result_pool, iterpool));
    }

  svn_pool_destroy(iterpool);
  return SVN_NO_ERROR;
}

/* Try to find a revision older than START_REV, and its author, which deleted
 * DELETED_BASENAME in the directory PARENT_REPOS_RELPATH. Assume the deleted
 * node is ancestrally related to RELATED_REPOS_RELPATH@RELATED_PEG_REV.
 * If no such revision can be found, set *DELETED_REV to SVN_INVALID_REVNUM
 * and *DELETED_REV_AUTHOR to NULL.
 * If the node was replaced rather than deleted, set *REPLACING_NODE_KIND to
 * the node kind of the replacing node. Else, set it to svn_node_unknown.
 * Only request the log for revisions up to END_REV from the server.
 * If MOVES it not NULL, and the deleted node was moved, provide heads of
 * move chains in *MOVES, or, if the node was not moved, set *MOVES to NULL.
 */
static svn_error_t *
find_revision_for_suspected_deletion(svn_revnum_t *deleted_rev,
                                     const char **deleted_rev_author,
                                     svn_node_kind_t *replacing_node_kind,
                                     struct apr_array_header_t **moves,
                                     svn_client_conflict_t *conflict,
                                     const char *deleted_basename,
                                     const char *parent_repos_relpath,
                                     svn_revnum_t start_rev,
                                     svn_revnum_t end_rev,
                                     const char *related_repos_relpath,
                                     svn_revnum_t related_peg_rev,
                                     svn_client_ctx_t *ctx,
                                     apr_pool_t *result_pool,
                                     apr_pool_t *scratch_pool)
{
  svn_ra_session_t *ra_session;
  const char *url;
  const char *corrected_url;
  apr_array_header_t *paths;
  apr_array_header_t *revprops;
  const char *repos_root_url;
  const char *repos_uuid;
  struct find_deleted_rev_baton b = { 0 };
  const char *victim_abspath;
  svn_error_t *err;
  apr_hash_t *moves_table;

  SVN_ERR_ASSERT(start_rev > end_rev);

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, &repos_uuid,
                                             conflict, scratch_pool,
                                             scratch_pool));
  victim_abspath = svn_client_conflict_get_local_abspath(conflict);

  if (moves)
    SVN_ERR(find_moves_in_revision_range(&moves_table, parent_repos_relpath,
                                         repos_root_url, repos_uuid,
                                         victim_abspath, start_rev, end_rev,
                                         ctx, result_pool, scratch_pool));

  url = svn_path_url_add_component2(repos_root_url, parent_repos_relpath,
                                    scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               url, NULL, NULL, FALSE, FALSE,
                                               ctx, scratch_pool,
                                               scratch_pool));

  paths = apr_array_make(scratch_pool, 1, sizeof(const char *));
  APR_ARRAY_PUSH(paths, const char *) = "";

  revprops = apr_array_make(scratch_pool, 1, sizeof(const char *));
  APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR;

  b.victim_abspath = victim_abspath;
  b.deleted_repos_relpath = svn_relpath_join(parent_repos_relpath,
                                             deleted_basename, scratch_pool);
  b.related_repos_relpath = related_repos_relpath;
  b.related_peg_rev = related_peg_rev;
  b.deleted_rev = SVN_INVALID_REVNUM;
  b.replacing_node_kind = svn_node_unknown;
  b.repos_root_url = repos_root_url;
  b.repos_uuid = repos_uuid;
  b.ctx = ctx;
  if (moves)
    b.moves_table = moves_table;
  b.result_pool = result_pool;
  SVN_ERR(svn_ra__dup_session(&b.extra_ra_session, ra_session, NULL,
                              scratch_pool, scratch_pool));

  err = svn_ra_get_log2(ra_session, paths, start_rev, end_rev,
                        0, /* no limit */
                        TRUE, /* need the changed paths list */
                        FALSE, /* need to traverse copies */
                        FALSE, /* no need for merged revisions */
                        revprops,
                        find_deleted_rev, &b,
                        scratch_pool);
  if (err)
    {
      if (err->apr_err == SVN_ERR_CEASE_INVOCATION &&
          b.deleted_rev != SVN_INVALID_REVNUM)

        {
          /* Log operation was aborted because we found deleted rev. */
          svn_error_clear(err);
        }
      else
        return svn_error_trace(err);
    }

  if (b.deleted_rev == SVN_INVALID_REVNUM)
    {
      struct repos_move_info *move = b.move;

      if (moves && move)
        {
          *deleted_rev = move->rev;
          *deleted_rev_author = move->rev_author;
          *replacing_node_kind = b.replacing_node_kind;
          SVN_ERR(find_operative_moves(moves, moves_table,
                                       b.deleted_repos_relpath,
                                       move->rev,
                                       ra_session, repos_root_url,
                                       result_pool, scratch_pool));
        }
      else
        {
          /* We could not determine the revision in which the node was
           * deleted. */
          *deleted_rev = SVN_INVALID_REVNUM;
          *deleted_rev_author = NULL;
          *replacing_node_kind = svn_node_unknown;
          if (moves)
            *moves = NULL;
        }
      return SVN_NO_ERROR;
    }
  else
    {
      *deleted_rev = b.deleted_rev;
      *deleted_rev_author = b.deleted_rev_author;
      *replacing_node_kind = b.replacing_node_kind;
      if (moves)
        SVN_ERR(find_operative_moves(moves, moves_table,
                                     b.deleted_repos_relpath, b.deleted_rev,
                                     ra_session, repos_root_url,
                                     result_pool, scratch_pool));
    }

  return SVN_NO_ERROR;
}

/* Details for tree conflicts involving a locally missing node. */
struct conflict_tree_local_missing_details
{
  /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. */
  svn_revnum_t deleted_rev;

  /* ### Add 'added_rev', like in conflict_tree_incoming_delete_details? */

  /* Author who committed DELETED_REV. */
  const char *deleted_rev_author;

  /* The path which was deleted relative to the repository root. */
  const char *deleted_repos_relpath;

  /* Move information about the conflict victim. If not NULL, this is an
   * array of 'struct repos_move_info *' elements. Each element is the
   * head of a move chain which starts in DELETED_REV. */
  apr_array_header_t *moves;

  /* If moves is not NULL, a map of repos_relpaths and working copy nodes.
  *
   * Each key is a "const char *" repository relpath corresponding to a
   * possible repository-side move destination node in the revision which
   * is the merge-right revision in case of a merge.
   *
   * Each value is an apr_array_header_t *.
   * Each array consists of "const char *" absolute paths to working copy
   * nodes which correspond to the repository node selected by the map key.
   * Each such working copy node is a potential local move target which can
   * be chosen to find a suitable merge target when resolving a tree conflict.
   *
   * This may be an empty hash map in case if there is no move target path
   * in the working copy. */
  apr_hash_t *wc_move_targets;

  /* If not NULL, the preferred move target repository relpath. This is our key
   * into the WC_MOVE_TARGETS map above (can be overridden by the user). */
  const char *move_target_repos_relpath;

  /* The current index into the list of working copy nodes corresponding to
   * MOVE_TARGET_REPOS_REPLATH (can be overridden by the user). */
  int wc_move_target_idx;

  /* Move information about siblings. Siblings are nodes which share
   * a youngest common ancestor with the conflict victim. E.g. in case
   * of a merge operation they are part of the merge source branch.
   * If not NULL, this is an array of 'struct repos_move_info *' elements.
   * Each element is the head of a move chain, which starts at some
   * point in history after siblings and conflict victim forked off
   * their common ancestor. */
  apr_array_header_t *sibling_moves;

  /* List of nodes in the WC which are suitable merge targets for changes
   * merged from any moved sibling. Array elements are 'const char *'
   * absolute paths of working copy nodes. This array contains multiple
   * elements only if ambiguous matches were found in the WC. */
  apr_array_header_t *wc_siblings;
  int preferred_sibling_idx;
};

static svn_error_t *
find_related_node(const char **related_repos_relpath,
                  svn_revnum_t *related_peg_rev,
                  const char *younger_related_repos_relpath,
                  svn_revnum_t younger_related_peg_rev,
                  const char *older_repos_relpath,
                  svn_revnum_t older_peg_rev,
                  svn_client_conflict_t *conflict,
                  svn_client_ctx_t *ctx,
                  apr_pool_t *result_pool,
                  apr_pool_t *scratch_pool)
{
  const char *repos_root_url;
  const char *related_url;
  const char *corrected_url;
  svn_node_kind_t related_node_kind;
  svn_ra_session_t *ra_session;

  *related_repos_relpath = NULL;
  *related_peg_rev = SVN_INVALID_REVNUM;

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict,
                                             scratch_pool, scratch_pool));
  related_url = svn_path_url_add_component2(repos_root_url,
                                            younger_related_repos_relpath,
                                            scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session,
                                               &corrected_url,
                                               related_url, NULL,
                                               NULL,
                                               FALSE,
                                               FALSE,
                                               ctx,
                                               scratch_pool,
                                               scratch_pool));
  SVN_ERR(svn_ra_check_path(ra_session, "", younger_related_peg_rev,
                            &related_node_kind, scratch_pool));
  if (related_node_kind == svn_node_none)
    {
      svn_revnum_t related_deleted_rev;
      const char *related_deleted_rev_author;
      svn_node_kind_t related_replacing_node_kind;
      const char *related_basename;
      const char *related_parent_repos_relpath;
      apr_array_header_t *related_moves;

      /* Looks like the younger node, which we'd like to use as our
       * 'related node', was deleted. Try to find its deleted revision
       *  so we can calculate a peg revision at which it exists.
       * The younger node is related to the older node, so we can use
       * the older node to guide us in our search. */
      related_basename = svn_relpath_basename(younger_related_repos_relpath,
                                              scratch_pool);
      related_parent_repos_relpath =
        svn_relpath_dirname(younger_related_repos_relpath, scratch_pool);
      SVN_ERR(find_revision_for_suspected_deletion(
                &related_deleted_rev, &related_deleted_rev_author,
                &related_replacing_node_kind, &related_moves,
                conflict, related_basename,
                related_parent_repos_relpath,
                younger_related_peg_rev, 0,
                older_repos_relpath, older_peg_rev,
                ctx, conflict->pool, scratch_pool));

      /* If we can't find a related node, bail. */
      if (related_deleted_rev == SVN_INVALID_REVNUM)
        return SVN_NO_ERROR;

      /* The node should exist in the revision before it was deleted. */
      *related_repos_relpath = younger_related_repos_relpath;
      *related_peg_rev = rev_below(related_deleted_rev);
    }
  else
    {
      *related_repos_relpath = younger_related_repos_relpath;
      *related_peg_rev = younger_related_peg_rev;
    }

  return SVN_NO_ERROR;
}

/* Determine if REPOS_RELPATH@PEG_REV was moved at some point in its history.
 * History's range of interest ends at END_REV which must be older than PEG_REV.
 *
 * VICTIM_ABSPATH is the abspath of a conflict victim in the working copy and
 * will be used in notifications.
 *
 * Return any applicable move chain heads in *MOVES.
 * If no moves can be found, set *MOVES to NULL. */
static svn_error_t *
find_moves_in_natural_history(apr_array_header_t **moves,
                              const char *repos_relpath,
                              svn_revnum_t peg_rev,
                              svn_node_kind_t node_kind,
                              svn_revnum_t end_rev,
                              const char *victim_abspath,
                              const char *repos_root_url,
                              const char *repos_uuid,
                              svn_ra_session_t *ra_session,
                              svn_client_ctx_t *ctx,
                              apr_pool_t *result_pool,
                              apr_pool_t *scratch_pool)
{
  apr_hash_t *moves_table;
  apr_array_header_t *revs;
  apr_array_header_t *most_recent_moves = NULL;
  int i;
  apr_pool_t *iterpool;

  *moves = NULL;

  SVN_ERR(find_moves_in_revision_range(&moves_table, repos_relpath,
                                       repos_root_url, repos_uuid,
                                       victim_abspath, peg_rev, end_rev,
                                       ctx, scratch_pool, scratch_pool));

  iterpool = svn_pool_create(scratch_pool);

  /* Scan the moves table for applicable moves. */
  revs = svn_sort__hash(moves_table, compare_items_as_revs, scratch_pool);
  for (i = revs->nelts - 1; i >= 0; i--)
    {
      svn_sort__item_t item = APR_ARRAY_IDX(revs, i, svn_sort__item_t);
      apr_array_header_t *moves_in_rev = apr_hash_get(moves_table, item.key,
                                                      sizeof(svn_revnum_t));
      int j;

      svn_pool_clear(iterpool);

      /* Was repos relpath moved to its location in this revision? */
      for (j = 0; j < moves_in_rev->nelts; j++)
        {
          struct repos_move_info *move;
          const char *relpath;

          move = APR_ARRAY_IDX(moves_in_rev, j, struct repos_move_info *);
          relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath,
                                              repos_relpath);
          if (relpath)
            {
              /* If the move did not happen in our peg revision, make
               * sure this move happened on the same line of history. */
              if (move->rev != peg_rev)
                {
                  svn_client__pathrev_t *yca_loc;
                  svn_error_t *err;

                  err = find_yca(&yca_loc, repos_relpath, peg_rev,
                                 repos_relpath, move->rev,
                                 repos_root_url, repos_uuid,
                                 NULL, ctx, iterpool, iterpool);
                  if (err)
                    {
                      if (err->apr_err == SVN_ERR_FS_NOT_FOUND)
                        {
                          svn_error_clear(err);
                          yca_loc = NULL;
                        }
                      else
                        return svn_error_trace(err);
                    }

                  if (yca_loc == NULL || yca_loc->rev != move->rev)
                    continue;
                }

              if (most_recent_moves == NULL)
                most_recent_moves =
                  apr_array_make(result_pool, 1,
                                 sizeof(struct repos_move_info *));

              /* Copy the move to result pool (even if relpath is ""). */
              move = new_path_adjusted_move(move, relpath, node_kind,
                                            result_pool);
              APR_ARRAY_PUSH(most_recent_moves,
                             struct repos_move_info *) = move;
            }
        }

      /* If we found one move, or several ambiguous moves, we're done. */
      if (most_recent_moves)
        break;
    }

  if (most_recent_moves && most_recent_moves->nelts > 0)
    {
      *moves = apr_array_make(result_pool, 1,
                              sizeof(struct repos_move_info *));

      /* Figure out what happened to the most recent moves in prior
       * revisions and build move chains. */
      for (i = 0; i < most_recent_moves->nelts; i++)
        {
          struct repos_move_info *move;

          svn_pool_clear(iterpool);

          move = APR_ARRAY_IDX(most_recent_moves, i, struct repos_move_info *);
          SVN_ERR(trace_moved_node_backwards(moves_table, move,
                                             ra_session, repos_root_url,
                                             result_pool, iterpool));
          /* Follow the move chain backwards. */
          while (move->prev)
            move = move->prev;

          /* Return move heads. */
          APR_ARRAY_PUSH(*moves, struct repos_move_info *) = move;
        }
    }

  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

static svn_error_t *
collect_sibling_move_candidates(apr_array_header_t *candidates,
                                const char *victim_abspath,
                                svn_node_kind_t victim_kind,
                                struct repos_move_info *move,
                                svn_client_ctx_t *ctx,
                                apr_pool_t *result_pool,
                                apr_pool_t *scratch_pool)
{
  const char *basename;
  apr_array_header_t *abspaths;
  int i;

  basename = svn_relpath_basename(move->moved_from_repos_relpath, scratch_pool);
  SVN_ERR(svn_wc__find_working_nodes_with_basename(&abspaths, victim_abspath,
                                                   basename, victim_kind,
                                                   ctx->wc_ctx, result_pool,
                                                   scratch_pool));
  apr_array_cat(candidates, abspaths);

  if (move->next)
    {
      apr_pool_t *iterpool = svn_pool_create(scratch_pool);
      for (i = 0; i < move->next->nelts; i++)
        {
          struct repos_move_info *next_move;
          next_move = APR_ARRAY_IDX(move->next, i, struct repos_move_info *);
          SVN_ERR(collect_sibling_move_candidates(candidates, victim_abspath,
                                                  victim_kind, next_move, ctx,
                                                  result_pool, iterpool));
          svn_pool_clear(iterpool);
        }
      svn_pool_destroy(iterpool);
    }

  return SVN_NO_ERROR;
}

/* Follow each move chain starting a MOVE all the way to the end to find
 * the possible working copy locations for VICTIM_ABSPATH which corresponds
 * to VICTIM_REPOS_REPLATH@VICTIM_REVISION.
 * Add each such location to the WC_MOVE_TARGETS hash table, keyed on the
 * repos_relpath which is the corresponding move destination in the repository.
 * This function is recursive. */
static svn_error_t *
follow_move_chains(apr_hash_t *wc_move_targets,
                   struct repos_move_info *move,
                   svn_client_ctx_t *ctx,
                   const char *victim_abspath,
                   svn_node_kind_t victim_node_kind,
                   const char *victim_repos_relpath,
                   svn_revnum_t victim_revision,
                   apr_pool_t *result_pool,
                   apr_pool_t *scratch_pool)
{
  apr_array_header_t *candidate_abspaths;

  /* Gather candidate nodes which represent this moved_to_repos_relpath. */
  SVN_ERR(svn_wc__guess_incoming_move_target_nodes(
            &candidate_abspaths, ctx->wc_ctx,
            victim_abspath, victim_node_kind,
            move->moved_to_repos_relpath,
            scratch_pool, scratch_pool));

  if (candidate_abspaths->nelts > 0)
    {
      apr_array_header_t *moved_to_abspaths;
      int i;
      apr_pool_t *iterpool = svn_pool_create(scratch_pool);

      moved_to_abspaths = apr_array_make(result_pool, 1,
                                         sizeof (const char *));

      for (i = 0; i < candidate_abspaths->nelts; i++)
        {
          const char *candidate_abspath;
          const char *repos_root_url;
          const char *repos_uuid;
          const char *candidate_repos_relpath;
          svn_revnum_t candidate_revision;

          svn_pool_clear(iterpool);

          candidate_abspath = APR_ARRAY_IDX(candidate_abspaths, i,
                                            const char *);
          SVN_ERR(svn_wc__node_get_origin(NULL, &candidate_revision,
                                          &candidate_repos_relpath,
                                          &repos_root_url,
                                          &repos_uuid,
                                          NULL, NULL,
                                          ctx->wc_ctx,
                                          candidate_abspath,
                                          FALSE,
                                          iterpool, iterpool));

          if (candidate_revision == SVN_INVALID_REVNUM)
            continue;

          /* If the conflict victim and the move target candidate
           * are not from the same revision we must ensure that
           * they are related. */
           if (candidate_revision != victim_revision)
            {
              svn_client__pathrev_t *yca_loc;
              svn_error_t *err;

              err = find_yca(&yca_loc, victim_repos_relpath,
                             victim_revision,
                             candidate_repos_relpath,
                             candidate_revision,
                             repos_root_url, repos_uuid,
                             NULL, ctx, iterpool, iterpool);
              if (err)
                {
                  if (err->apr_err == SVN_ERR_FS_NOT_FOUND)
                    {
                      svn_error_clear(err);
                      yca_loc = NULL;
                    }
                  else
                    return svn_error_trace(err);
                }

              if (yca_loc == NULL)
                continue;
            }

          APR_ARRAY_PUSH(moved_to_abspaths, const char *) =
            apr_pstrdup(result_pool, candidate_abspath);
        }
      svn_pool_destroy(iterpool);

      svn_hash_sets(wc_move_targets, move->moved_to_repos_relpath,
                    moved_to_abspaths);
    }

  if (move->next)
    {
      int i;
      apr_pool_t *iterpool;

      /* Recurse into each of the possible move chains. */
      iterpool = svn_pool_create(scratch_pool);
      for (i = 0; i < move->next->nelts; i++)
        {
          struct repos_move_info *next_move;

          svn_pool_clear(iterpool);

          next_move = APR_ARRAY_IDX(move->next, i, struct repos_move_info *);
          SVN_ERR(follow_move_chains(wc_move_targets, next_move,
                                     ctx, victim_abspath, victim_node_kind,
                                     victim_repos_relpath, victim_revision,
                                     result_pool, iterpool));

        }
      svn_pool_destroy(iterpool);
    }

  return SVN_NO_ERROR;
}

/* Implements tree_conflict_get_details_func_t. */
static svn_error_t *
conflict_tree_get_details_local_missing(svn_client_conflict_t *conflict,
                                        svn_client_ctx_t *ctx,
                                        apr_pool_t *scratch_pool)
{
  const char *old_repos_relpath;
  const char *new_repos_relpath;
  const char *parent_repos_relpath;
  svn_revnum_t parent_peg_rev;
  svn_revnum_t old_rev;
  svn_revnum_t new_rev;
  svn_revnum_t deleted_rev;
  svn_node_kind_t old_kind;
  svn_node_kind_t new_kind;
  const char *deleted_rev_author;
  svn_node_kind_t replacing_node_kind;
  const char *deleted_basename;
  struct conflict_tree_local_missing_details *details;
  apr_array_header_t *moves = NULL;
  apr_array_header_t *sibling_moves = NULL;
  apr_array_header_t *wc_siblings = NULL;
  const char *related_repos_relpath;
  svn_revnum_t related_peg_rev;
  const char *repos_root_url;
  const char *repos_uuid;
  const char *url, *corrected_url;
  svn_ra_session_t *ra_session;
  svn_client__pathrev_t *yca_loc;
  svn_revnum_t end_rev;

  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &old_repos_relpath, &old_rev, &old_kind, conflict,
            scratch_pool, scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &new_repos_relpath, &new_rev, &new_kind, conflict,
            scratch_pool, scratch_pool));

  /* Scan the conflict victim's parent's log to find a revision which
   * deleted the node. */
  deleted_basename = svn_dirent_basename(conflict->local_abspath,
                                         scratch_pool);
  SVN_ERR(svn_wc__node_get_repos_info(&parent_peg_rev, &parent_repos_relpath,
                                      &repos_root_url, &repos_uuid,
                                      ctx->wc_ctx,
                                      svn_dirent_dirname(
                                        conflict->local_abspath,
                                        scratch_pool),
                                      scratch_pool,
                                      scratch_pool));

  /* If the parent is not part of the repository-side tree checked out
   * into this working copy, then bail. We do not support this case yet. */
  if (parent_peg_rev == SVN_INVALID_REVNUM)
    return SVN_NO_ERROR;

  /* Pick the younger incoming node as our 'related node' which helps
   * pin-pointing the deleted conflict victim in history. */
  related_repos_relpath =
            (old_rev < new_rev ? new_repos_relpath : old_repos_relpath);
  related_peg_rev = (old_rev < new_rev ? new_rev : old_rev);

  /* Make sure we're going to search the related node in a revision where
   * it exists. The younger incoming node might have been deleted in HEAD. */
  if (related_repos_relpath != NULL && related_peg_rev != SVN_INVALID_REVNUM)
    SVN_ERR(find_related_node(
              &related_repos_relpath, &related_peg_rev,
              related_repos_relpath, related_peg_rev,
              (old_rev < new_rev ? old_repos_relpath : new_repos_relpath),
              (old_rev < new_rev ? old_rev : new_rev),
              conflict, ctx, scratch_pool, scratch_pool));

  /* Set END_REV to our best guess of the nearest YCA revision. */
  url = svn_path_url_add_component2(repos_root_url, related_repos_relpath,
                                    scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session,
                                               &corrected_url,
                                               url, NULL, NULL,
                                               FALSE,
                                               FALSE,
                                               ctx,
                                               scratch_pool,
                                               scratch_pool));
  SVN_ERR(find_nearest_yca(&yca_loc, related_repos_relpath, related_peg_rev,
                           parent_repos_relpath, parent_peg_rev,
                           repos_root_url, repos_uuid, ra_session, ctx,
                           scratch_pool, scratch_pool));
  if (yca_loc)
   {
     end_rev = yca_loc->rev;

    /* END_REV must be smaller than PARENT_PEG_REV, else the call to
     * find_revision_for_suspected_deletion() below will abort. */
    if (end_rev >= parent_peg_rev)
      end_rev = parent_peg_rev > 0 ? parent_peg_rev - 1 : 0;
   }
  else
    end_rev = 0; /* ### We might walk through all of history... */

  SVN_ERR(find_revision_for_suspected_deletion(
            &deleted_rev, &deleted_rev_author, &replacing_node_kind,
            yca_loc ? &moves : NULL,
            conflict, deleted_basename, parent_repos_relpath,
            parent_peg_rev, end_rev, related_repos_relpath, related_peg_rev,
            ctx, conflict->pool, scratch_pool));

  /* If the victim was not deleted then check if the related path was moved. */
  if (deleted_rev == SVN_INVALID_REVNUM)
    {
      const char *victim_abspath;
      svn_node_kind_t related_node_kind;
      apr_array_header_t *candidates;
      int i;
      apr_pool_t *iterpool;

      /* ### The following describes all moves in terms of forward-merges,
       * should do we something else for reverse-merges? */

      victim_abspath = svn_client_conflict_get_local_abspath(conflict);

      if (yca_loc)
       {
          end_rev = yca_loc->rev;

          /* END_REV must be smaller than RELATED_PEG_REV, else the call
             to find_moves_in_natural_history() below will error out. */
          if (end_rev >= related_peg_rev)
            end_rev = related_peg_rev > 0 ? related_peg_rev - 1 : 0;
       }
      else
        end_rev = 0; /* ### We might walk through all of history... */

      SVN_ERR(svn_ra_check_path(ra_session, "", related_peg_rev,
                                &related_node_kind, scratch_pool));
      SVN_ERR(find_moves_in_natural_history(&sibling_moves,
                                            related_repos_relpath,
                                            related_peg_rev,
                                            related_node_kind,
                                            end_rev,
                                            victim_abspath,
                                            repos_root_url, repos_uuid,
                                            ra_session, ctx,
                                            conflict->pool, scratch_pool));

      if (sibling_moves == NULL)
        return SVN_NO_ERROR;

      /* Find the missing node in the WC. In theory, this requires tracing
       * back history of every node in the WC to check for a YCA with the
       * conflict victim. This operation would obviously be quite expensive.
       *
       * However, assuming that the victim was not moved in the merge target,
       * we can take a short-cut: The basename of the node cannot have changed,
       * so we can limit history tracing to nodes with a matching basename.
       *
       * This approach solves the conflict case where an edit to a file which
       * was moved on one branch is cherry-picked to another branch where the
       * corresponding file has not been moved (yet). It does not solve move
       * vs. move conflicts, but such conflicts are not yet supported by the
       * resolver anyway and are hard to solve without server-side support. */
      iterpool = svn_pool_create(scratch_pool);
      for (i = 0; i < sibling_moves->nelts; i++)
        {
          struct repos_move_info *move;
          int j;

          svn_pool_clear(iterpool);

          move = APR_ARRAY_IDX(sibling_moves, i, struct repos_move_info *);
          candidates = apr_array_make(iterpool, 1, sizeof(const char *));
          SVN_ERR(collect_sibling_move_candidates(candidates, victim_abspath,
                                                  old_rev < new_rev
                                                    ? new_kind : old_kind,
                                                  move, ctx, iterpool,
                                                  iterpool));

          /* Determine whether a candidate node shares a YCA with the victim. */
          for (j = 0; j < candidates->nelts; j++)
            {
              const char *candidate_abspath;
              const char *candidate_repos_relpath;
              svn_revnum_t candidate_revision;
              svn_error_t *err;

              candidate_abspath = APR_ARRAY_IDX(candidates, j, const char *);
              SVN_ERR(svn_wc__node_get_origin(NULL, &candidate_revision,
                                              &candidate_repos_relpath,
                                              NULL, NULL, NULL, NULL,
                                              ctx->wc_ctx,
                                              candidate_abspath,
                                              FALSE,
                                              iterpool, iterpool));
              err = find_yca(&yca_loc,
                             old_rev < new_rev
                               ? new_repos_relpath : old_repos_relpath,
                             old_rev < new_rev ? new_rev : old_rev,
                             candidate_repos_relpath,
                             candidate_revision,
                             repos_root_url, repos_uuid,
                             NULL, ctx, iterpool, iterpool);
              if (err)
                {
                  if (err->apr_err == SVN_ERR_FS_NOT_FOUND)
                    {
                      svn_error_clear(err);
                      yca_loc = NULL;
                    }
                  else
                    return svn_error_trace(err);
                }

              if (yca_loc)
                {
                  if (wc_siblings == NULL)
                    wc_siblings = apr_array_make(conflict->pool, 1,
                                                 sizeof(const char *));
                  APR_ARRAY_PUSH(wc_siblings, const char *) =
                    apr_pstrdup(conflict->pool, candidate_abspath);
                }
            }
        }
      svn_pool_destroy(iterpool);
    }

  details = apr_pcalloc(conflict->pool, sizeof(*details));
  details->deleted_rev = deleted_rev;
  details->deleted_rev_author = deleted_rev_author;
  if (deleted_rev != SVN_INVALID_REVNUM)
    details->deleted_repos_relpath = svn_relpath_join(parent_repos_relpath,
                                                      deleted_basename,
                                                      conflict->pool);
  details->moves = moves;
  details->wc_move_targets = apr_hash_make(conflict->pool);
  if (details->moves != NULL)
    {
      apr_pool_t *iterpool;
      int i;

      iterpool = svn_pool_create(scratch_pool);
      for (i = 0; i < details->moves->nelts; i++)
        {
          struct repos_move_info *move;

          svn_pool_clear(iterpool);
          move = APR_ARRAY_IDX(details->moves, i, struct repos_move_info *);
          SVN_ERR(follow_move_chains(details->wc_move_targets, move, ctx,
                                     conflict->local_abspath,
                                     new_kind,
                                     new_repos_relpath,
                                     new_rev,
                                     scratch_pool, iterpool));
        }
      svn_pool_destroy(iterpool);

      if (apr_hash_count(details->wc_move_targets) > 0)
        {
          apr_array_header_t *move_target_repos_relpaths;
          const svn_sort__item_t *item;

          /* Initialize to the first possible move target. Hopefully,
           * in most cases there will only be one candidate anyway. */
          move_target_repos_relpaths = svn_sort__hash(
                                         details->wc_move_targets,
                                         svn_sort_compare_items_as_paths,
                                         scratch_pool);
          item = &APR_ARRAY_IDX(move_target_repos_relpaths,
                                0, svn_sort__item_t);
          details->move_target_repos_relpath = item->key;
          details->wc_move_target_idx = 0;
        }
      else
        {
          details->move_target_repos_relpath = NULL;
          details->wc_move_target_idx = 0;
        }
    }

  details->sibling_moves = sibling_moves;
  details->wc_siblings = wc_siblings;
  if (details->wc_move_targets && apr_hash_count(details->wc_move_targets) == 1)
    {
      apr_array_header_t *wc_abspaths;

      wc_abspaths = svn_hash_gets(details->wc_move_targets,
                                  details->move_target_repos_relpath);
      if (wc_abspaths->nelts == 1)
        {
          svn_node_kind_t kind = old_rev < new_rev ? new_kind : old_kind;

          if (kind == svn_node_file)
              conflict->recommended_option_id =
                  svn_client_conflict_option_local_move_file_text_merge;
          else if (kind == svn_node_dir)
              conflict->recommended_option_id =
                svn_client_conflict_option_local_move_dir_merge;
      }
    }
  else if (details->wc_siblings && details->wc_siblings->nelts == 1)
    {
      svn_node_kind_t kind = old_rev < new_rev ? new_kind : old_kind;

      if (kind == svn_node_file)
          conflict->recommended_option_id =
              svn_client_conflict_option_sibling_move_file_text_merge;
      else if (kind == svn_node_dir)
          conflict->recommended_option_id =
            svn_client_conflict_option_sibling_move_dir_merge;
    }

  conflict->tree_conflict_local_details = details;

  return SVN_NO_ERROR;
}

/* Return a localised string representation of the local part of a tree
   conflict on a non-existent node. */
static svn_error_t *
describe_local_none_node_change(const char **description,
                                svn_client_conflict_t *conflict,
                                apr_pool_t *result_pool,
                                apr_pool_t *scratch_pool)
{
  svn_wc_conflict_reason_t local_change;
  svn_wc_operation_t operation;

  local_change = svn_client_conflict_get_local_change(conflict);
  operation = svn_client_conflict_get_operation(conflict);

  switch (local_change)
    {
    case svn_wc_conflict_reason_edited:
      *description = _("An item containing uncommitted changes was "
                       "found in the working copy.");
      break;
    case svn_wc_conflict_reason_obstructed:
      *description = _("An item which already occupies this path was found in "
                       "the working copy.");
      break;
    case svn_wc_conflict_reason_deleted:
      *description = _("A deleted item was found in the working copy.");
      break;
    case svn_wc_conflict_reason_missing:
      if (operation == svn_wc_operation_update ||
          operation == svn_wc_operation_switch)
        *description = _("No such file or directory was found in the "
                         "working copy.");
      else if (operation == svn_wc_operation_merge)
        {
          /* ### display deleted revision */
          *description = _("No such file or directory was found in the "
                           "merge target working copy.\nThe item may "
                           "have been deleted or moved away in the "
                           "repository's history.");
        }
      break;
    case svn_wc_conflict_reason_unversioned:
      *description = _("An unversioned item was found in the working "
                       "copy.");
      break;
    case svn_wc_conflict_reason_added:
    case svn_wc_conflict_reason_replaced:
      *description = _("An item scheduled to be added to the repository "
                       "in the next commit was found in the working "
                       "copy.");
      break;
    case svn_wc_conflict_reason_moved_away:
      *description = _("The item in the working copy had been moved "
                       "away at the time this conflict was recorded.");
      break;
    case svn_wc_conflict_reason_moved_here:
      *description = _("An item had been moved here in the working copy "
                       "at the time this conflict was recorded.");
      break;
    }

  return SVN_NO_ERROR;
}

/* Append a description of a move chain beginning at NEXT to DESCRIPTION. */
static const char *
append_moved_to_chain_description(const char *description,
                                  apr_array_header_t *next,
                                  apr_pool_t *result_pool,
                                  apr_pool_t *scratch_pool)
{
  if (next == NULL)
    return description;

  while (next)
    {
      struct repos_move_info *move;

      /* Describe the first possible move chain only. Adding multiple chains
       * to the description would just be confusing. The user may select a
       * different move destination while resolving the conflict. */
      move = APR_ARRAY_IDX(next, 0, struct repos_move_info *);

      description = apr_psprintf(scratch_pool,
                                 _("%s\nAnd then moved away to '^/%s' by "
                                   "%s in r%ld."),
                                 description, move->moved_to_repos_relpath,
                                 move->rev_author, move->rev);
      next = move->next;
    }

  return apr_pstrdup(result_pool, description);
}

/* Implements tree_conflict_get_description_func_t. */
static svn_error_t *
conflict_tree_get_local_description_generic(const char **description,
                                            svn_client_conflict_t *conflict,
                                            svn_client_ctx_t *ctx,
                                            apr_pool_t *result_pool,
                                            apr_pool_t *scratch_pool)
{
  svn_node_kind_t victim_node_kind;

  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);

  *description = NULL;

  switch (victim_node_kind)
    {
      case svn_node_file:
      case svn_node_symlink:
        SVN_ERR(describe_local_file_node_change(description, conflict, ctx,
                                                result_pool, scratch_pool));
        break;
      case svn_node_dir:
        SVN_ERR(describe_local_dir_node_change(description, conflict, ctx,
                                               result_pool, scratch_pool));
        break;
      case svn_node_none:
      case svn_node_unknown:
        SVN_ERR(describe_local_none_node_change(description, conflict,
                                                result_pool, scratch_pool));
        break;
    }

  return SVN_NO_ERROR;
}

/* Implements tree_conflict_get_description_func_t. */
static svn_error_t *
conflict_tree_get_description_local_missing(const char **description,
                                            svn_client_conflict_t *conflict,
                                            svn_client_ctx_t *ctx,
                                            apr_pool_t *result_pool,
                                            apr_pool_t *scratch_pool)
{
  struct conflict_tree_local_missing_details *details;

  details = conflict->tree_conflict_local_details;
  if (details == NULL)
    return svn_error_trace(conflict_tree_get_local_description_generic(
                             description, conflict, ctx,
                             result_pool, scratch_pool));

  if (details->moves || details->sibling_moves)
    {
      struct repos_move_info *move;

      *description = _("No such file or directory was found in the "
                       "merge target working copy.\n");

      if (details->moves)
        {
          move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
          if (move->node_kind == svn_node_file)
            *description = apr_psprintf(
                             result_pool,
                             _("%sThe file was moved to '^/%s' in r%ld by %s."),
                             *description, move->moved_to_repos_relpath,
                             move->rev, move->rev_author);
          else if (move->node_kind == svn_node_dir)
            *description = apr_psprintf(
                             result_pool,
                             _("%sThe directory was moved to '^/%s' in "
                               "r%ld by %s."),
                             *description, move->moved_to_repos_relpath,
                             move->rev, move->rev_author);
          else
            *description = apr_psprintf(
                             result_pool,
                             _("%sThe item was moved to '^/%s' in r%ld by %s."),
                             *description, move->moved_to_repos_relpath,
                             move->rev, move->rev_author);
          *description = append_moved_to_chain_description(*description,
                                                           move->next,
                                                           result_pool,
                                                           scratch_pool);
        }

      if (details->sibling_moves)
        {
          move = APR_ARRAY_IDX(details->sibling_moves, 0,
                               struct repos_move_info *);
          if (move->node_kind == svn_node_file)
            *description = apr_psprintf(
                             result_pool,
                             _("%sThe file '^/%s' was moved to '^/%s' "
                               "in r%ld by %s."),
                             *description, move->moved_from_repos_relpath,
                             move->moved_to_repos_relpath,
                             move->rev, move->rev_author);
          else if (move->node_kind == svn_node_dir)
            *description = apr_psprintf(
                             result_pool,
                             _("%sThe directory '^/%s' was moved to '^/%s' "
                               "in r%ld by %s."),
                             *description, move->moved_from_repos_relpath,
                             move->moved_to_repos_relpath,
                             move->rev, move->rev_author);
          else
            *description = apr_psprintf(
                             result_pool,
                             _("%sThe item '^/%s' was moved to '^/%s' "
                               "in r%ld by %s."),
                             *description, move->moved_from_repos_relpath,
                             move->moved_to_repos_relpath,
                             move->rev, move->rev_author);
          *description = append_moved_to_chain_description(*description,
                                                           move->next,
                                                           result_pool,
                                                           scratch_pool);
        }
    }
  else
    *description = apr_psprintf(
                     result_pool,
                     _("No such file or directory was found in the "
                       "merge target working copy.\n'^/%s' was deleted "
                       "in r%ld by %s."),
                     details->deleted_repos_relpath,
                     details->deleted_rev, details->deleted_rev_author);

  return SVN_NO_ERROR;
}

/* Return a localised string representation of the incoming part of a
   conflict; NULL for non-localised odd cases. */
static const char *
describe_incoming_change(svn_node_kind_t kind, svn_wc_conflict_action_t action,
                         svn_wc_operation_t operation)
{
  switch (kind)
    {
      case svn_node_file:
      case svn_node_symlink:
        if (operation == svn_wc_operation_update)
          {
            switch (action)
              {
                case svn_wc_conflict_action_edit:
                  return _("An update operation tried to edit a file.");
                case svn_wc_conflict_action_add:
                  return _("An update operation tried to add a file.");
                case svn_wc_conflict_action_delete:
                  return _("An update operation tried to delete or move "
                           "a file.");
                case svn_wc_conflict_action_replace:
                  return _("An update operation tried to replace a file.");
              }
          }
        else if (operation == svn_wc_operation_switch)
          {
            switch (action)
              {
                case svn_wc_conflict_action_edit:
                  return _("A switch operation tried to edit a file.");
                case svn_wc_conflict_action_add:
                  return _("A switch operation tried to add a file.");
                case svn_wc_conflict_action_delete:
                  return _("A switch operation tried to delete or move "
                           "a file.");
                case svn_wc_conflict_action_replace:
                  return _("A switch operation tried to replace a file.");
              }
          }
        else if (operation == svn_wc_operation_merge)
          {
            switch (action)
              {
                case svn_wc_conflict_action_edit:
                  return _("A merge operation tried to edit a file.");
                case svn_wc_conflict_action_add:
                  return _("A merge operation tried to add a file.");
                case svn_wc_conflict_action_delete:
                  return _("A merge operation tried to delete or move "
                           "a file.");
                case svn_wc_conflict_action_replace:
                  return _("A merge operation tried to replace a file.");
            }
          }
        break;
      case svn_node_dir:
        if (operation == svn_wc_operation_update)
          {
            switch (action)
              {
                case svn_wc_conflict_action_edit:
                  return _("An update operation tried to change a directory.");
                case svn_wc_conflict_action_add:
                  return _("An update operation tried to add a directory.");
                case svn_wc_conflict_action_delete:
                  return _("An update operation tried to delete or move "
                           "a directory.");
                case svn_wc_conflict_action_replace:
                  return _("An update operation tried to replace a directory.");
              }
          }
        else if (operation == svn_wc_operation_switch)
          {
            switch (action)
              {
                case svn_wc_conflict_action_edit:
                  return _("A switch operation tried to edit a directory.");
                case svn_wc_conflict_action_add:
                  return _("A switch operation tried to add a directory.");
                case svn_wc_conflict_action_delete:
                  return _("A switch operation tried to delete or move "
                           "a directory.");
                case svn_wc_conflict_action_replace:
                  return _("A switch operation tried to replace a directory.");
              }
          }
        else if (operation == svn_wc_operation_merge)
          {
            switch (action)
              {
                case svn_wc_conflict_action_edit:
                  return _("A merge operation tried to edit a directory.");
                case svn_wc_conflict_action_add:
                  return _("A merge operation tried to add a directory.");
                case svn_wc_conflict_action_delete:
                  return _("A merge operation tried to delete or move "
                           "a directory.");
                case svn_wc_conflict_action_replace:
                  return _("A merge operation tried to replace a directory.");
            }
          }
        break;
      case svn_node_none:
      case svn_node_unknown:
        if (operation == svn_wc_operation_update)
          {
            switch (action)
              {
                case svn_wc_conflict_action_edit:
                  return _("An update operation tried to edit an item.");
                case svn_wc_conflict_action_add:
                  return _("An update operation tried to add an item.");
                case svn_wc_conflict_action_delete:
                  return _("An update operation tried to delete or move "
                           "an item.");
                case svn_wc_conflict_action_replace:
                  return _("An update operation tried to replace an item.");
              }
          }
        else if (operation == svn_wc_operation_switch)
          {
            switch (action)
              {
                case svn_wc_conflict_action_edit:
                  return _("A switch operation tried to edit an item.");
                case svn_wc_conflict_action_add:
                  return _("A switch operation tried to add an item.");
                case svn_wc_conflict_action_delete:
                  return _("A switch operation tried to delete or move "
                           "an item.");
                case svn_wc_conflict_action_replace:
                  return _("A switch operation tried to replace an item.");
              }
          }
        else if (operation == svn_wc_operation_merge)
          {
            switch (action)
              {
                case svn_wc_conflict_action_edit:
                  return _("A merge operation tried to edit an item.");
                case svn_wc_conflict_action_add:
                  return _("A merge operation tried to add an item.");
                case svn_wc_conflict_action_delete:
                  return _("A merge operation tried to delete or move "
                           "an item.");
                case svn_wc_conflict_action_replace:
                  return _("A merge operation tried to replace an item.");
              }
          }
        break;
    }

  return NULL;
}

/* Return a localised string representation of the operation part of a
   conflict. */
static const char *
operation_str(svn_wc_operation_t operation)
{
  switch (operation)
    {
    case svn_wc_operation_update: return _("upon update");
    case svn_wc_operation_switch: return _("upon switch");
    case svn_wc_operation_merge:  return _("upon merge");
    case svn_wc_operation_none:   return _("upon none");
    }
  SVN_ERR_MALFUNCTION_NO_RETURN();
  return NULL;
}

svn_error_t *
svn_client_conflict_prop_get_description(const char **description,
                                         svn_client_conflict_t *conflict,
                                         apr_pool_t *result_pool,
                                         apr_pool_t *scratch_pool)
{
  const char *reason_str, *action_str;

  /* We provide separately translatable strings for the values that we
   * know about, and a fall-back in case any other values occur. */
  switch (svn_client_conflict_get_local_change(conflict))
    {
      case svn_wc_conflict_reason_edited:
        reason_str = _("local edit");
        break;
      case svn_wc_conflict_reason_added:
        reason_str = _("local add");
        break;
      case svn_wc_conflict_reason_deleted:
        reason_str = _("local delete");
        break;
      case svn_wc_conflict_reason_obstructed:
        reason_str = _("local obstruction");
        break;
      default:
        reason_str = apr_psprintf(
                       scratch_pool, _("local %s"),
                       svn_token__to_word(
                         map_conflict_reason,
                         svn_client_conflict_get_local_change(conflict)));
        break;
    }
  switch (svn_client_conflict_get_incoming_change(conflict))
    {
      case svn_wc_conflict_action_edit:
        action_str = _("incoming edit");
        break;
      case svn_wc_conflict_action_add:
        action_str = _("incoming add");
        break;
      case svn_wc_conflict_action_delete:
        action_str = _("incoming delete");
        break;
      default:
        action_str = apr_psprintf(
                       scratch_pool, _("incoming %s"),
                       svn_token__to_word(
                         map_conflict_action,
                         svn_client_conflict_get_incoming_change(conflict)));
        break;
    }
  SVN_ERR_ASSERT(reason_str && action_str);

  *description = apr_psprintf(result_pool, _("%s, %s %s"),
                              reason_str, action_str,
                              operation_str(
                                svn_client_conflict_get_operation(conflict)));

  return SVN_NO_ERROR;
}

/* Implements tree_conflict_get_description_func_t. */
static svn_error_t *
conflict_tree_get_incoming_description_generic(
  const char **incoming_change_description,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  const char *action;
  svn_node_kind_t incoming_kind;
  svn_wc_conflict_action_t conflict_action;
  svn_wc_operation_t conflict_operation;

  conflict_action = svn_client_conflict_get_incoming_change(conflict);
  conflict_operation = svn_client_conflict_get_operation(conflict);

  /* Determine the node kind of the incoming change. */
  incoming_kind = svn_node_unknown;
  if (conflict_action == svn_wc_conflict_action_edit ||
      conflict_action == svn_wc_conflict_action_delete)
    {
      /* Change is acting on 'src_left' version of the node. */
      SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
                NULL, NULL, &incoming_kind, conflict, scratch_pool,
                scratch_pool));
    }
  else if (conflict_action == svn_wc_conflict_action_add ||
           conflict_action == svn_wc_conflict_action_replace)
    {
      /* Change is acting on 'src_right' version of the node.
       *
       * ### For 'replace', the node kind is ambiguous. However, src_left
       * ### is NULL for replace, so we must use src_right. */
      SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
                NULL, NULL, &incoming_kind, conflict, scratch_pool,
                scratch_pool));
    }

  action = describe_incoming_change(incoming_kind, conflict_action,
                                    conflict_operation);
  if (action)
    {
      *incoming_change_description = apr_pstrdup(result_pool, action);
    }
  else
    {
      /* A catch-all message for very rare or nominally impossible cases.
         It will not be pretty, but is closer to an internal error than
         an ordinary user-facing string. */
      *incoming_change_description = apr_psprintf(result_pool,
                                       _("incoming %s %s"),
                                       svn_node_kind_to_word(incoming_kind),
                                       svn_token__to_word(map_conflict_action,
                                                          conflict_action));
    }
  return SVN_NO_ERROR;
}

/* Details for tree conflicts involving incoming deletions and replacements. */
struct conflict_tree_incoming_delete_details
{
  /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. */
  svn_revnum_t deleted_rev;

  /* If not SVN_INVALID_REVNUM, the node was added in ADDED_REV. The incoming
   * delete is the result of a reverse application of this addition. */
  svn_revnum_t added_rev;

  /* The path which was deleted/added relative to the repository root. */
  const char *repos_relpath;

  /* Author who committed DELETED_REV/ADDED_REV. */
  const char *rev_author;

  /* New node kind for a replaced node. This is svn_node_none for deletions. */
  svn_node_kind_t replacing_node_kind;

  /* Move information. If not NULL, this is an array of repos_move_info *
   * elements. Each element is the head of a move chain which starts in
   * DELETED_REV or in ADDED_REV (in which case moves should be interpreted
   * in reverse). */
  apr_array_header_t *moves;

  /* A map of repos_relpaths and working copy nodes for an incoming move.
   *
   * Each key is a "const char *" repository relpath corresponding to a
   * possible repository-side move destination node in the revision which
   * is the target revision in case of update and switch, or the merge-right
   * revision in case of a merge.
   *
   * Each value is an apr_array_header_t *.
   * Each array consists of "const char *" absolute paths to working copy
   * nodes which correspond to the repository node selected by the map key.
   * Each such working copy node is a potential local move target which can
   * be chosen to "follow" the incoming move when resolving a tree conflict.
   *
   * This may be an empty hash map in case if there is no move target path
   * in the working copy. */
  apr_hash_t *wc_move_targets;

  /* The preferred move target repository relpath. This is our key into
   * the WC_MOVE_TARGETS map above (can be overridden by the user). */
  const char *move_target_repos_relpath;

  /* The current index into the list of working copy nodes corresponding to
   * MOVE_TARGET_REPOS_REPLATH (can be overridden by the user). */
  int wc_move_target_idx;
};

/* Get the currently selected repository-side move target path.
 * If none was selected yet, determine and return a default one. */
static const char *
get_moved_to_repos_relpath(
  struct conflict_tree_incoming_delete_details *details,
  apr_pool_t *scratch_pool)
{
  struct repos_move_info *move;

  if (details->move_target_repos_relpath)
    return details->move_target_repos_relpath;

  if (details->wc_move_targets && apr_hash_count(details->wc_move_targets) > 0)
    {
      svn_sort__item_t item;
      apr_array_header_t *repos_relpaths;

      repos_relpaths = svn_sort__hash(details->wc_move_targets,
                                      svn_sort_compare_items_as_paths,
                                      scratch_pool);
      item = APR_ARRAY_IDX(repos_relpaths, 0, svn_sort__item_t);
      return (const char *)item.key;
    }

  move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
  return move->moved_to_repos_relpath;
}

static const char *
describe_incoming_deletion_upon_update(
  struct conflict_tree_incoming_delete_details *details,
  svn_node_kind_t victim_node_kind,
  svn_revnum_t old_rev,
  svn_revnum_t new_rev,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  if (details->replacing_node_kind == svn_node_file ||
      details->replacing_node_kind == svn_node_symlink)
    {
      if (victim_node_kind == svn_node_dir)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("Directory updated from r%ld to r%ld was "
                           "replaced with a file by %s in r%ld."),
                         old_rev, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced directory was moved to "
                               "'^/%s'."), description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("File updated from r%ld to r%ld was replaced "
                           "with a file from another line of history by "
                           "%s in r%ld."),
                         old_rev, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced file was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("Item updated from r%ld to r%ld was replaced "
                           "with a file by %s in r%ld."), old_rev, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced item was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
    }
  else if (details->replacing_node_kind == svn_node_dir)
    {
      if (victim_node_kind == svn_node_dir)
        {
          const char *description =
            apr_psprintf(result_pool,
                          _("Directory updated from r%ld to r%ld was "
                            "replaced with a directory from another line "
                            "of history by %s in r%ld."),
                          old_rev, new_rev,
                          details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced directory was moved to "
                               "'^/%s'."), description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("File updated from r%ld to r%ld was "
                           "replaced with a directory by %s in r%ld."),
                         old_rev, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced file was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("Item updated from r%ld to r%ld was replaced "
                           "by %s in r%ld."), old_rev, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced item was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
    }
  else
    {
      if (victim_node_kind == svn_node_dir)
        {
          if (details->moves)
            {
              const char *description;
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("Directory updated from r%ld to r%ld was "
                               "moved to '^/%s' by %s in r%ld."),
                             old_rev, new_rev,
                             get_moved_to_repos_relpath(details, scratch_pool),
                             details->rev_author, details->deleted_rev);
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          else
            return apr_psprintf(result_pool,
                                _("Directory updated from r%ld to r%ld was "
                                  "deleted by %s in r%ld."),
                                old_rev, new_rev,
                                details->rev_author, details->deleted_rev);
        }
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        {
          if (details->moves)
            {
              struct repos_move_info *move;
              const char *description;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("File updated from r%ld to r%ld was moved "
                               "to '^/%s' by %s in r%ld."), old_rev, new_rev,
                             get_moved_to_repos_relpath(details, scratch_pool),
                             details->rev_author, details->deleted_rev);
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          else
            return apr_psprintf(result_pool,
                                _("File updated from r%ld to r%ld was "
                                  "deleted by %s in r%ld."), old_rev, new_rev,
                                details->rev_author, details->deleted_rev);
        }
      else
        {
          if (details->moves)
            {
              const char *description;
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("Item updated from r%ld to r%ld was moved "
                               "to '^/%s' by %s in r%ld."), old_rev, new_rev,
                             get_moved_to_repos_relpath(details, scratch_pool),
                             details->rev_author, details->deleted_rev);
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          else
            return apr_psprintf(result_pool,
                                _("Item updated from r%ld to r%ld was "
                                  "deleted by %s in r%ld."), old_rev, new_rev,
                                details->rev_author, details->deleted_rev);
        }
    }
}

static const char *
describe_incoming_reverse_addition_upon_update(
  struct conflict_tree_incoming_delete_details *details,
  svn_node_kind_t victim_node_kind,
  svn_revnum_t old_rev,
  svn_revnum_t new_rev,
  apr_pool_t *result_pool)
{
  if (details->replacing_node_kind == svn_node_file ||
      details->replacing_node_kind == svn_node_symlink)
    {
      if (victim_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Directory updated backwards from r%ld to r%ld "
                              "was a file before the replacement made by %s "
                              "in r%ld."), old_rev, new_rev,
                            details->rev_author, details->added_rev);
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("File updated backwards from r%ld to r%ld was a "
                              "file from another line of history before the "
                              "replacement made by %s in r%ld."),
                            old_rev, new_rev,
                            details->rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("Item updated backwards from r%ld to r%ld was "
                              "replaced with a file by %s in r%ld."),
                            old_rev, new_rev,
                            details->rev_author, details->added_rev);
    }
  else if (details->replacing_node_kind == svn_node_dir)
    {
      if (victim_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Directory updated backwards from r%ld to r%ld "
                              "was a directory from another line of history "
                              "before the replacement made by %s in "
                              "r%ld."), old_rev, new_rev,
                            details->rev_author, details->added_rev);
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("File updated backwards from r%ld to r%ld was a "
                              "directory before the replacement made by %s "
                              "in r%ld."), old_rev, new_rev,
                            details->rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("Item updated backwards from r%ld to r%ld was "
                              "replaced with a directory by %s in r%ld."),
                            old_rev, new_rev,
                            details->rev_author, details->added_rev);
    }
  else
    {
      if (victim_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Directory updated backwards from r%ld to r%ld "
                              "did not exist before it was added by %s in "
                              "r%ld."), old_rev, new_rev,
                            details->rev_author, details->added_rev);
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("File updated backwards from r%ld to r%ld did "
                              "not exist before it was added by %s in r%ld."),
                            old_rev, new_rev,
                            details->rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("Item updated backwards from r%ld to r%ld did "
                              "not exist before it was added by %s in r%ld."),
                            old_rev, new_rev,
                            details->rev_author, details->added_rev);
    }
}

static const char *
describe_incoming_deletion_upon_switch(
  struct conflict_tree_incoming_delete_details *details,
  svn_node_kind_t victim_node_kind,
  const char *old_repos_relpath,
  svn_revnum_t old_rev,
  const char *new_repos_relpath,
  svn_revnum_t new_rev,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  if (details->replacing_node_kind == svn_node_file ||
      details->replacing_node_kind == svn_node_symlink)
    {
      if (victim_node_kind == svn_node_dir)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("Directory switched from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                           "was replaced with a file by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced directory was moved "
                               "to '^/%s'."), description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("File switched from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                           "replaced with a file from another line of "
                           "history by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced file was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("Item switched from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                           "replaced with a file by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced item was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
    }
  else if (details->replacing_node_kind == svn_node_dir)
    {
      if (victim_node_kind == svn_node_dir)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("Directory switched from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                           "was replaced with a directory from another "
                           "line of history by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced directory was moved to "
                               "'^/%s'."), description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("File switched from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                           "was replaced with a directory by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced file was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("Item switched from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                           "replaced with a directory by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced item was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
    }
  else
    {
      if (victim_node_kind == svn_node_dir)
        {
          if (details->moves)
            {
              struct repos_move_info *move;
              const char *description;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("Directory switched from\n"
                               "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                               "was moved to '^/%s' by %s in r%ld."),
                             old_repos_relpath, old_rev,
                             new_repos_relpath, new_rev,
                             get_moved_to_repos_relpath(details, scratch_pool),
                             details->rev_author, details->deleted_rev);
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          else
            return apr_psprintf(result_pool,
                                _("Directory switched from\n"
                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                                  "was deleted by %s in r%ld."),
                                old_repos_relpath, old_rev,
                                new_repos_relpath, new_rev,
                                details->rev_author, details->deleted_rev);
        }
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        {
          if (details->moves)
            {
              struct repos_move_info *move;
              const char *description;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("File switched from\n"
                               "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                               "moved to '^/%s' by %s in r%ld."),
                             old_repos_relpath, old_rev,
                             new_repos_relpath, new_rev,
                             get_moved_to_repos_relpath(details, scratch_pool),
                             details->rev_author, details->deleted_rev);
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          else
            return apr_psprintf(result_pool,
                                _("File switched from\n"
                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                                  "deleted by %s in r%ld."),
                                old_repos_relpath, old_rev,
                                new_repos_relpath, new_rev,
                                details->rev_author, details->deleted_rev);
        }
      else
        {
          if (details->moves)
            {
              struct repos_move_info *move;
              const char *description;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("Item switched from\n"
                               "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                               "moved to '^/%s' by %s in r%ld."),
                             old_repos_relpath, old_rev,
                             new_repos_relpath, new_rev,
                             get_moved_to_repos_relpath(details, scratch_pool),
                             details->rev_author, details->deleted_rev);
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          else
            return apr_psprintf(result_pool,
                                _("Item switched from\n"
                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                                  "deleted by %s in r%ld."),
                                old_repos_relpath, old_rev,
                                new_repos_relpath, new_rev,
                                details->rev_author, details->deleted_rev);
        }
    }
}

static const char *
describe_incoming_reverse_addition_upon_switch(
  struct conflict_tree_incoming_delete_details *details,
  svn_node_kind_t victim_node_kind,
  const char *old_repos_relpath,
  svn_revnum_t old_rev,
  const char *new_repos_relpath,
  svn_revnum_t new_rev,
  apr_pool_t *result_pool)
{
  if (details->replacing_node_kind == svn_node_file ||
      details->replacing_node_kind == svn_node_symlink)
    {
      if (victim_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Directory switched from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                              "was a file before the replacement made by %s "
                              "in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("File switched from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas a "
                              "file from another line of history before the "
                              "replacement made by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("Item switched from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                              "replaced with a file by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
    }
  else if (details->replacing_node_kind == svn_node_dir)
    {
      if (victim_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Directory switched from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                              "was a directory from another line of history "
                              "before the replacement made by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("Directory switched from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                              "was a file before the replacement made by %s "
                              "in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("Item switched from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                              "replaced with a directory by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
    }
  else
    {
      if (victim_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Directory switched from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                              "did not exist before it was added by %s in "
                              "r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("File switched from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\ndid "
                              "not exist before it was added by %s in "
                              "r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("Item switched from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\ndid "
                              "not exist before it was added by %s in "
                              "r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
    }
}

static const char *
describe_incoming_deletion_upon_merge(
  struct conflict_tree_incoming_delete_details *details,
  svn_node_kind_t victim_node_kind,
  const char *old_repos_relpath,
  svn_revnum_t old_rev,
  const char *new_repos_relpath,
  svn_revnum_t new_rev,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  if (details->replacing_node_kind == svn_node_file ||
      details->replacing_node_kind == svn_node_symlink)
    {
      if (victim_node_kind == svn_node_dir)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("Directory merged from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                           "was replaced with a file by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced directory was moved to "
                               "'^/%s'."), description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("File merged from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                           "replaced with a file from another line of "
                           "history by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced file was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else
        return apr_psprintf(result_pool,
                            _("Item merged from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                              "replaced with a file by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->deleted_rev);
    }
  else if (details->replacing_node_kind == svn_node_dir)
    {
      if (victim_node_kind == svn_node_dir)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("Directory merged from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                           "was replaced with a directory from another "
                           "line of history by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced directory was moved to "
                               "'^/%s'."), description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("File merged from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                           "was replaced with a directory by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced file was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
      else
        {
          const char *description =
            apr_psprintf(result_pool,
                         _("Item merged from\n"
                           "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                           "replaced with a directory by %s in r%ld."),
                         old_repos_relpath, old_rev,
                         new_repos_relpath, new_rev,
                         details->rev_author, details->deleted_rev);
          if (details->moves)
            {
              struct repos_move_info *move;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("%s\nThe replaced item was moved to '^/%s'."),
                             description,
                             get_moved_to_repos_relpath(details, scratch_pool));
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          return description;
        }
    }
  else
    {
      if (victim_node_kind == svn_node_dir)
        {
          if (details->moves)
            {
              struct repos_move_info *move;
              const char *description;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("Directory merged from\n"
                               "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                               "moved to '^/%s' by %s in r%ld."),
                             old_repos_relpath, old_rev,
                             new_repos_relpath, new_rev,
                             get_moved_to_repos_relpath(details, scratch_pool),
                             details->rev_author, details->deleted_rev);
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          else
            return apr_psprintf(result_pool,
                                _("Directory merged from\n"
                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                                  "deleted by %s in r%ld."),
                                old_repos_relpath, old_rev,
                                new_repos_relpath, new_rev,
                                details->rev_author, details->deleted_rev);
        }
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        {
          if (details->moves)
            {
              struct repos_move_info *move;
              const char *description;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("File merged from\n"
                               "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                               "moved to '^/%s' by %s in r%ld."),
                             old_repos_relpath, old_rev,
                             new_repos_relpath, new_rev,
                             get_moved_to_repos_relpath(details, scratch_pool),
                             details->rev_author, details->deleted_rev);
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          else
            return apr_psprintf(result_pool,
                                _("File merged from\n"
                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                                  "deleted by %s in r%ld."),
                                old_repos_relpath, old_rev,
                                new_repos_relpath, new_rev,
                                details->rev_author, details->deleted_rev);
        }
      else
        {
          if (details->moves)
            {
              struct repos_move_info *move;
              const char *description;

              move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *);
              description =
                apr_psprintf(result_pool,
                             _("Item merged from\n"
                               "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                               "moved to '^/%s' by %s in r%ld."),
                             old_repos_relpath, old_rev,
                             new_repos_relpath, new_rev,
                             get_moved_to_repos_relpath(details, scratch_pool),
                             details->rev_author, details->deleted_rev);
              return append_moved_to_chain_description(description,
                                                       move->next,
                                                       result_pool,
                                                       scratch_pool);
            }
          else
            return apr_psprintf(result_pool,
                                _("Item merged from\n"
                                  "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas "
                                  "deleted by %s in r%ld."),
                                old_repos_relpath, old_rev,
                                new_repos_relpath, new_rev,
                                details->rev_author, details->deleted_rev);
        }
    }
}

static const char *
describe_incoming_reverse_addition_upon_merge(
  struct conflict_tree_incoming_delete_details *details,
  svn_node_kind_t victim_node_kind,
  const char *old_repos_relpath,
  svn_revnum_t old_rev,
  const char *new_repos_relpath,
  svn_revnum_t new_rev,
  apr_pool_t *result_pool)
{
  if (details->replacing_node_kind == svn_node_file ||
      details->replacing_node_kind == svn_node_symlink)
    {
      if (victim_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Directory reverse-merged from\n'^/%s@%ld'\nto "
                              "^/%s@%ld was a file before the replacement "
                              "made by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("File reverse-merged from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                              "was a file from another line of history before "
                              "the replacement made by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("Item reverse-merged from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                              "was replaced with a file by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
    }
  else if (details->replacing_node_kind == svn_node_dir)
    {
      if (victim_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Directory reverse-merged from\n'^/%s@%ld'\nto "
                              "^/%s@%ld was a directory from another line "
                              "of history before the replacement made by %s "
                              "in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("Directory reverse-merged from\n'^/%s@%ld'\nto "
                              "^/%s@%ld was a file before the replacement "
                              "made by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("Item reverse-merged from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                              "was replaced with a directory by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
    }
  else
    {
      if (victim_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Directory reverse-merged from\n'^/%s@%ld'\nto "
                              "^/%s@%ld did not exist before it was added "
                              "by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else if (victim_node_kind == svn_node_file ||
               victim_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("File reverse-merged from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                              "did not exist before it was added by %s in "
                              "r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("Item reverse-merged from\n"
                              "'^/%s@%ld'\nto\n'^/%s@%ld'\n"
                              "did not exist before it was added by %s in "
                              "r%ld."),
                            old_repos_relpath, old_rev,
                            new_repos_relpath, new_rev,
                            details->rev_author, details->added_rev);
    }
}

/* Implements tree_conflict_get_description_func_t. */
static svn_error_t *
conflict_tree_get_description_incoming_delete(
  const char **incoming_change_description,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  const char *action;
  svn_node_kind_t victim_node_kind;
  svn_wc_operation_t conflict_operation;
  const char *old_repos_relpath;
  svn_revnum_t old_rev;
  const char *new_repos_relpath;
  svn_revnum_t new_rev;
  struct conflict_tree_incoming_delete_details *details;

  if (conflict->tree_conflict_incoming_details == NULL)
    return svn_error_trace(conflict_tree_get_incoming_description_generic(
                             incoming_change_description,
                             conflict, ctx, result_pool, scratch_pool));

  conflict_operation = svn_client_conflict_get_operation(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &old_repos_relpath, &old_rev, NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &new_repos_relpath, &new_rev, NULL, conflict, scratch_pool,
            scratch_pool));

  details = conflict->tree_conflict_incoming_details;

  if (conflict_operation == svn_wc_operation_update)
    {
      if (details->deleted_rev != SVN_INVALID_REVNUM)
        {
          action = describe_incoming_deletion_upon_update(details,
                                                          victim_node_kind,
                                                          old_rev,
                                                          new_rev,
                                                          result_pool,
                                                          scratch_pool);
        }
      else /* details->added_rev != SVN_INVALID_REVNUM */
        {
          /* This deletion is really the reverse change of an addition. */
          action = describe_incoming_reverse_addition_upon_update(
                     details, victim_node_kind, old_rev, new_rev, result_pool);
        }
    }
  else if (conflict_operation == svn_wc_operation_switch)
    {
      if (details->deleted_rev != SVN_INVALID_REVNUM)
        {
          action = describe_incoming_deletion_upon_switch(details,
                                                          victim_node_kind,
                                                          old_repos_relpath,
                                                          old_rev,
                                                          new_repos_relpath,
                                                          new_rev,
                                                          result_pool,
                                                          scratch_pool);
        }
      else /* details->added_rev != SVN_INVALID_REVNUM */
        {
          /* This deletion is really the reverse change of an addition. */
          action = describe_incoming_reverse_addition_upon_switch(
                     details, victim_node_kind, old_repos_relpath, old_rev,
                     new_repos_relpath, new_rev, result_pool);

        }
      }
  else if (conflict_operation == svn_wc_operation_merge)
    {
      if (details->deleted_rev != SVN_INVALID_REVNUM)
        {
          action = describe_incoming_deletion_upon_merge(details,
                                                         victim_node_kind,
                                                         old_repos_relpath,
                                                         old_rev,
                                                         new_repos_relpath,
                                                         new_rev,
                                                         result_pool,
                                                         scratch_pool);
        }
      else /* details->added_rev != SVN_INVALID_REVNUM */
        {
          /* This deletion is really the reverse change of an addition. */
          action = describe_incoming_reverse_addition_upon_merge(
                     details, victim_node_kind, old_repos_relpath, old_rev,
                     new_repos_relpath, new_rev, result_pool);
        }
      }

  *incoming_change_description = apr_pstrdup(result_pool, action);

  return SVN_NO_ERROR;
}

/* Baton for find_added_rev(). */
struct find_added_rev_baton
{
  const char *victim_abspath;
  svn_client_ctx_t *ctx;
  svn_revnum_t added_rev;
  const char *repos_relpath;
  const char *parent_repos_relpath;
  apr_pool_t *pool;
};

/* Implements svn_location_segment_receiver_t.
 * Finds the revision in which a node was added by tracing 'start'
 * revisions in location segments reported for the node.
 * If the PARENT_REPOS_RELPATH in the baton is not NULL, only consider
 * segments in which the node existed somwhere beneath this path. */
static svn_error_t *
find_added_rev(svn_location_segment_t *segment,
               void *baton,
               apr_pool_t *scratch_pool)
{
  struct find_added_rev_baton *b = baton;

  if (b->ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(
                 b->victim_abspath,
                 svn_wc_notify_tree_conflict_details_progress,
                 scratch_pool),
      notify->revision = segment->range_start;
      b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool);
    }

  if (segment->path) /* not interested in gaps */
    {
      if (b->parent_repos_relpath == NULL ||
          svn_relpath_skip_ancestor(b->parent_repos_relpath,
                                    segment->path) != NULL)
        {
          b->added_rev = segment->range_start;
          b->repos_relpath = apr_pstrdup(b->pool, segment->path);
        }
    }

  return SVN_NO_ERROR;
}

/* Find conflict details in the case where a revision which added a node was
 * applied in reverse, resulting in an incoming deletion. */
static svn_error_t *
get_incoming_delete_details_for_reverse_addition(
  struct conflict_tree_incoming_delete_details **details,
  const char *repos_root_url,
  const char *old_repos_relpath,
  svn_revnum_t old_rev,
  svn_revnum_t new_rev,
  svn_client_ctx_t *ctx,
  const char *victim_abspath,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  svn_ra_session_t *ra_session;
  const char *url;
  const char *corrected_url;
  svn_string_t *author_revprop;
  struct find_added_rev_baton b = { 0 };

  url = svn_path_url_add_component2(repos_root_url, old_repos_relpath,
                                    scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session,
                                               &corrected_url,
                                               url, NULL, NULL,
                                               FALSE,
                                               FALSE,
                                               ctx,
                                               scratch_pool,
                                               scratch_pool));

  *details = apr_pcalloc(result_pool, sizeof(**details));
  b.ctx = ctx;
  b.victim_abspath = victim_abspath;
  b.added_rev = SVN_INVALID_REVNUM;
  b.repos_relpath = NULL;
  b.parent_repos_relpath = NULL;
  b.pool = scratch_pool;

  /* Figure out when this node was added. */
  SVN_ERR(svn_ra_get_location_segments(ra_session, "", old_rev,
                                       old_rev, new_rev,
                                       find_added_rev, &b,
                                       scratch_pool));

  SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev,
                          SVN_PROP_REVISION_AUTHOR,
                          &author_revprop, scratch_pool));
  (*details)->deleted_rev = SVN_INVALID_REVNUM;
  (*details)->added_rev = b.added_rev;
  (*details)->repos_relpath = apr_pstrdup(result_pool, b.repos_relpath);
  if (author_revprop)
    (*details)->rev_author = apr_pstrdup(result_pool, author_revprop->data);
  else
    (*details)->rev_author = _("unknown author");

  /* Check for replacement. */
  (*details)->replacing_node_kind = svn_node_none;
  if ((*details)->added_rev > 0)
    {
      svn_node_kind_t replaced_node_kind;

      SVN_ERR(svn_ra_check_path(ra_session, "",
                                rev_below((*details)->added_rev),
                                &replaced_node_kind, scratch_pool));
      if (replaced_node_kind != svn_node_none)
        SVN_ERR(svn_ra_check_path(ra_session, "", (*details)->added_rev,
                                  &(*details)->replacing_node_kind,
                                  scratch_pool));
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
init_wc_move_targets(struct conflict_tree_incoming_delete_details *details,
                     svn_client_conflict_t *conflict,
                     svn_client_ctx_t *ctx,
                     apr_pool_t *scratch_pool)
{
  int i;
  const char *victim_abspath;
  svn_node_kind_t victim_node_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  /* ### Should we get the old location in case of reverse-merges? */
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict,
            scratch_pool, scratch_pool));
  details->wc_move_targets = apr_hash_make(conflict->pool);
  for (i = 0; i < details->moves->nelts; i++)
    {
      struct repos_move_info *move;

      move = APR_ARRAY_IDX(details->moves, i, struct repos_move_info *);
      SVN_ERR(follow_move_chains(details->wc_move_targets, move,
                                 ctx, victim_abspath,
                                 victim_node_kind,
                                 incoming_new_repos_relpath,
                                 incoming_new_pegrev,
                                 conflict->pool, scratch_pool));
    }

  /* Initialize to the first possible move target. Hopefully,
   * in most cases there will only be one candidate anyway. */
  details->move_target_repos_relpath =
    get_moved_to_repos_relpath(details, scratch_pool);
  details->wc_move_target_idx = 0;

  /* If only one move target exists recommend a resolution option. */
  if (apr_hash_count(details->wc_move_targets) == 1)
    {
      apr_array_header_t *wc_abspaths;

      wc_abspaths = svn_hash_gets(details->wc_move_targets,
                                  details->move_target_repos_relpath);
      if (wc_abspaths->nelts == 1)
        {
          svn_client_conflict_option_id_t recommended[] =
            {
              /* Only one of these will be present for any given conflict. */
              svn_client_conflict_option_incoming_move_file_text_merge,
              svn_client_conflict_option_incoming_move_dir_merge,
              svn_client_conflict_option_local_move_file_text_merge,
              svn_client_conflict_option_local_move_dir_merge,
              svn_client_conflict_option_sibling_move_file_text_merge,
              svn_client_conflict_option_sibling_move_dir_merge,
            };
          apr_array_header_t *options;

          SVN_ERR(svn_client_conflict_tree_get_resolution_options(
                    &options, conflict, ctx, scratch_pool, scratch_pool));
          for (i = 0; i < (sizeof(recommended) / sizeof(recommended[0])); i++)
            {
              svn_client_conflict_option_id_t option_id = recommended[i];

              if (svn_client_conflict_option_find_by_id(options, option_id))
                {
                  conflict->recommended_option_id = option_id;
                  break;
                }
            }
        }
    }

  return SVN_NO_ERROR;
}

/* Implements tree_conflict_get_details_func_t.
 * Find the revision in which the victim was deleted in the repository. */
static svn_error_t *
conflict_tree_get_details_incoming_delete(svn_client_conflict_t *conflict,
                                          svn_client_ctx_t *ctx,
                                          apr_pool_t *scratch_pool)
{
  const char *old_repos_relpath;
  const char *new_repos_relpath;
  const char *repos_root_url;
  svn_revnum_t old_rev;
  svn_revnum_t new_rev;
  svn_node_kind_t old_kind;
  svn_node_kind_t new_kind;
  struct conflict_tree_incoming_delete_details *details;
  svn_wc_operation_t operation;

  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &old_repos_relpath, &old_rev, &old_kind, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &new_repos_relpath, &new_rev, &new_kind, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict,
                                             scratch_pool, scratch_pool));
  operation = svn_client_conflict_get_operation(conflict);

  if (operation == svn_wc_operation_update)
    {
      if (old_rev < new_rev)
        {
          const char *parent_repos_relpath;
          svn_revnum_t parent_peg_rev;
          svn_revnum_t deleted_rev;
          svn_revnum_t end_rev;
          const char *deleted_rev_author;
          svn_node_kind_t replacing_node_kind;
          apr_array_header_t *moves;
          const char *related_repos_relpath;
          svn_revnum_t related_peg_rev;

          /* The update operation went forward in history. */
          SVN_ERR(svn_wc__node_get_repos_info(&parent_peg_rev,
                                              &parent_repos_relpath,
                                              NULL, NULL,
                                              ctx->wc_ctx,
                                              svn_dirent_dirname(
                                                conflict->local_abspath,
                                                scratch_pool),
                                              scratch_pool,
                                              scratch_pool));
          if (new_kind == svn_node_none)
            {
              SVN_ERR(find_related_node(&related_repos_relpath,
                                        &related_peg_rev,
                                        new_repos_relpath, new_rev,
                                        old_repos_relpath, old_rev,
                                        conflict, ctx,
                                        scratch_pool, scratch_pool));
            }
          else
            {
              /* related to self */
              related_repos_relpath = NULL;
              related_peg_rev = SVN_INVALID_REVNUM;
            }

          end_rev = (new_kind == svn_node_none ? 0 : old_rev);
          if (end_rev >= parent_peg_rev)
            end_rev = (parent_peg_rev > 0 ? parent_peg_rev - 1 : 0);

          SVN_ERR(find_revision_for_suspected_deletion(
                    &deleted_rev, &deleted_rev_author, &replacing_node_kind,
                    &moves, conflict,
                    svn_dirent_basename(conflict->local_abspath, scratch_pool),
                    parent_repos_relpath, parent_peg_rev, end_rev,
                    related_repos_relpath, related_peg_rev,
                    ctx, conflict->pool, scratch_pool));
          if (deleted_rev == SVN_INVALID_REVNUM)
            {
              /* We could not determine the revision in which the node was
               * deleted. We cannot provide the required details so the best
               * we can do is fall back to the default description. */
              return SVN_NO_ERROR;
            }

          details = apr_pcalloc(conflict->pool, sizeof(*details));
          details->deleted_rev = deleted_rev;
          details->added_rev = SVN_INVALID_REVNUM;
          details->repos_relpath = apr_pstrdup(conflict->pool,
                                               new_repos_relpath);
          details->rev_author = deleted_rev_author;
          details->replacing_node_kind = replacing_node_kind;
          details->moves = moves;
        }
      else /* new_rev < old_rev */
        {
          /* The update operation went backwards in history.
           * Figure out when this node was added. */
          SVN_ERR(get_incoming_delete_details_for_reverse_addition(
                    &details, repos_root_url, old_repos_relpath,
                    old_rev, new_rev, ctx,
                    svn_client_conflict_get_local_abspath(conflict),
                    conflict->pool, scratch_pool));
        }
    }
  else if (operation == svn_wc_operation_switch ||
           operation == svn_wc_operation_merge)
    {
      if (old_rev < new_rev)
        {
          svn_revnum_t deleted_rev;
          const char *deleted_rev_author;
          svn_node_kind_t replacing_node_kind;
          apr_array_header_t *moves;

          /* The switch/merge operation went forward in history.
           *
           * The deletion of the node happened on the branch we switched to
           * or merged from. Scan new_repos_relpath's parent's log to find
           * the revision which deleted the node. */
          SVN_ERR(find_revision_for_suspected_deletion(
                    &deleted_rev, &deleted_rev_author, &replacing_node_kind,
                    &moves, conflict,
                    svn_relpath_basename(new_repos_relpath, scratch_pool),
                    svn_relpath_dirname(new_repos_relpath, scratch_pool),
                    new_rev, old_rev, old_repos_relpath, old_rev, ctx,
                    conflict->pool, scratch_pool));
          if (deleted_rev == SVN_INVALID_REVNUM)
            {
              /* We could not determine the revision in which the node was
               * deleted. We cannot provide the required details so the best
               * we can do is fall back to the default description. */
              return SVN_NO_ERROR;
            }

          details = apr_pcalloc(conflict->pool, sizeof(*details));
          details->deleted_rev = deleted_rev;
          details->added_rev = SVN_INVALID_REVNUM;
          details->repos_relpath = apr_pstrdup(conflict->pool,
                                               new_repos_relpath);
          details->rev_author = apr_pstrdup(conflict->pool,
                                            deleted_rev_author);
          details->replacing_node_kind = replacing_node_kind;
          details->moves = moves;
        }
      else /* new_rev < old_rev */
        {
          /* The switch/merge operation went backwards in history.
           * Figure out when the node we switched away from, or merged
           * from another branch, was added. */
          SVN_ERR(get_incoming_delete_details_for_reverse_addition(
                    &details, repos_root_url, old_repos_relpath,
                    old_rev, new_rev, ctx,
                    svn_client_conflict_get_local_abspath(conflict),
                    conflict->pool, scratch_pool));
        }
    }
  else
    {
      details = NULL;
    }

  conflict->tree_conflict_incoming_details = details;

  if (details && details->moves)
    SVN_ERR(init_wc_move_targets(details, conflict, ctx, scratch_pool));

  return SVN_NO_ERROR;
}

/* Details for tree conflicts involving incoming additions. */
struct conflict_tree_incoming_add_details
{
  /* If not SVN_INVALID_REVNUM, the node was added in ADDED_REV. */
  svn_revnum_t added_rev;

  /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV.
   * Note that both ADDED_REV and DELETED_REV may be valid for update/switch.
   * See comment in conflict_tree_get_details_incoming_add() for details. */
  svn_revnum_t deleted_rev;

  /* The path which was added/deleted relative to the repository root. */
  const char *repos_relpath;

  /* Authors who committed ADDED_REV/DELETED_REV. */
  const char *added_rev_author;
  const char *deleted_rev_author;

  /* Move information. If not NULL, this is an array of repos_move_info *
   * elements. Each element is the head of a move chain which starts in
   * ADDED_REV or in DELETED_REV (in which case moves should be interpreted
   * in reverse). */
  apr_array_header_t *moves;
};

/* Implements tree_conflict_get_details_func_t.
 * Find the revision in which the victim was added in the repository. */
static svn_error_t *
conflict_tree_get_details_incoming_add(svn_client_conflict_t *conflict,
                                       svn_client_ctx_t *ctx,
                                       apr_pool_t *scratch_pool)
{
  const char *old_repos_relpath;
  const char *new_repos_relpath;
  const char *repos_root_url;
  svn_revnum_t old_rev;
  svn_revnum_t new_rev;
  struct conflict_tree_incoming_add_details *details = NULL;
  svn_wc_operation_t operation;

  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &old_repos_relpath, &old_rev, NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &new_repos_relpath, &new_rev, NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict,
                                             scratch_pool, scratch_pool));
  operation = svn_client_conflict_get_operation(conflict);

  if (operation == svn_wc_operation_update ||
      operation == svn_wc_operation_switch)
    {
      /* Only the new repository location is recorded for the node which
       * caused an incoming addition. There is no pre-update/pre-switch
       * revision to be recorded for the node since it does not exist in
       * the repository at that revision.
       * The implication is that we cannot know whether the operation went
       * forward or backwards in history. So always try to find an added
       * and a deleted revision for the node. Users must figure out by whether
       * the addition or deletion caused the conflict. */
      const char *url;
      const char *corrected_url;
      svn_string_t *author_revprop;
      struct find_added_rev_baton b = { 0 };
      svn_ra_session_t *ra_session;
      svn_revnum_t deleted_rev;
      svn_revnum_t head_rev;

      url = svn_path_url_add_component2(repos_root_url, new_repos_relpath,
                                        scratch_pool);
      SVN_ERR(svn_client__open_ra_session_internal(&ra_session,
                                                   &corrected_url,
                                                   url, NULL, NULL,
                                                   FALSE,
                                                   FALSE,
                                                   ctx,
                                                   scratch_pool,
                                                   scratch_pool));

      details = apr_pcalloc(conflict->pool, sizeof(*details));
      b.ctx = ctx,
      b.victim_abspath = svn_client_conflict_get_local_abspath(conflict),
      b.added_rev = SVN_INVALID_REVNUM;
      b.repos_relpath = NULL;
      b.parent_repos_relpath = NULL;
      b.pool = scratch_pool;

      /* Figure out when this node was added. */
      SVN_ERR(svn_ra_get_location_segments(ra_session, "", new_rev,
                                           new_rev, SVN_INVALID_REVNUM,
                                           find_added_rev, &b,
                                           scratch_pool));

      SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev,
                              SVN_PROP_REVISION_AUTHOR,
                              &author_revprop, scratch_pool));
      details->repos_relpath = apr_pstrdup(conflict->pool, b.repos_relpath);
      details->added_rev = b.added_rev;
      if (author_revprop)
        details->added_rev_author = apr_pstrdup(conflict->pool,
                                          author_revprop->data);
      else
        details->added_rev_author = _("unknown author");
      details->deleted_rev = SVN_INVALID_REVNUM;
      details->deleted_rev_author = NULL;

      /* Figure out whether this node was deleted later.
       * ### Could probably optimize by infering both addition and deletion
       * ### from svn_ra_get_location_segments() call above. */
      SVN_ERR(svn_ra_get_latest_revnum(ra_session, &head_rev, scratch_pool));
      if (new_rev < head_rev)
        {
          SVN_ERR(svn_ra_get_deleted_rev(ra_session, "", new_rev, head_rev,
                                         &deleted_rev, scratch_pool));
          if (SVN_IS_VALID_REVNUM(deleted_rev))
           {
              SVN_ERR(svn_ra_rev_prop(ra_session, deleted_rev,
                                      SVN_PROP_REVISION_AUTHOR,
                                      &author_revprop, scratch_pool));
              details->deleted_rev = deleted_rev;
              if (author_revprop)
                details->deleted_rev_author = apr_pstrdup(conflict->pool,
                                                          author_revprop->data);
              else
                details->deleted_rev_author = _("unknown author");
            }
        }
    }
  else if (operation == svn_wc_operation_merge &&
           strcmp(old_repos_relpath, new_repos_relpath) == 0)
    {
      if (old_rev < new_rev)
        {
          /* The merge operation went forwards in history.
           * The addition of the node happened on the branch we merged form.
           * Scan the nodes's history to find the revision which added it. */
          const char *url;
          const char *corrected_url;
          svn_string_t *author_revprop;
          struct find_added_rev_baton b = { 0 };
          svn_ra_session_t *ra_session;

          url = svn_path_url_add_component2(repos_root_url, new_repos_relpath,
                                            scratch_pool);
          SVN_ERR(svn_client__open_ra_session_internal(&ra_session,
                                                       &corrected_url,
                                                       url, NULL, NULL,
                                                       FALSE,
                                                       FALSE,
                                                       ctx,
                                                       scratch_pool,
                                                       scratch_pool));

          details = apr_pcalloc(conflict->pool, sizeof(*details));
          b.victim_abspath = svn_client_conflict_get_local_abspath(conflict);
          b.ctx = ctx;
          b.added_rev = SVN_INVALID_REVNUM;
          b.repos_relpath = NULL;
          b.parent_repos_relpath = NULL;
          b.pool = scratch_pool;

          /* Figure out when this node was added. */
          SVN_ERR(svn_ra_get_location_segments(ra_session, "", new_rev,
                                               new_rev, old_rev,
                                               find_added_rev, &b,
                                               scratch_pool));

          SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev,
                                  SVN_PROP_REVISION_AUTHOR,
                                  &author_revprop, scratch_pool));
          details->repos_relpath = apr_pstrdup(conflict->pool, b.repos_relpath);
          details->added_rev = b.added_rev;
          if (author_revprop)
            details->added_rev_author = apr_pstrdup(conflict->pool,
                                                    author_revprop->data);
          else
            details->added_rev_author = _("unknown author");
          details->deleted_rev = SVN_INVALID_REVNUM;
          details->deleted_rev_author = NULL;
        }
      else if (old_rev > new_rev)
        {
          /* The merge operation was a reverse-merge.
           * This addition is in fact a deletion, applied in reverse,
           * which happened on the branch we merged from.
           * Find the revision which deleted the node. */
          svn_revnum_t deleted_rev;
          const char *deleted_rev_author;
          svn_node_kind_t replacing_node_kind;
          apr_array_header_t *moves;

          SVN_ERR(find_revision_for_suspected_deletion(
                    &deleted_rev, &deleted_rev_author, &replacing_node_kind,
                    &moves, conflict,
                    svn_relpath_basename(old_repos_relpath, scratch_pool),
                    svn_relpath_dirname(old_repos_relpath, scratch_pool),
                    old_rev, new_rev,
                    NULL, SVN_INVALID_REVNUM, /* related to self */
                    ctx,
                    conflict->pool, scratch_pool));
          if (deleted_rev == SVN_INVALID_REVNUM)
            {
              /* We could not determine the revision in which the node was
               * deleted. We cannot provide the required details so the best
               * we can do is fall back to the default description. */
              return SVN_NO_ERROR;
            }

          details = apr_pcalloc(conflict->pool, sizeof(*details));
          details->repos_relpath = apr_pstrdup(conflict->pool,
                                               new_repos_relpath);
          details->deleted_rev = deleted_rev;
          details->deleted_rev_author = apr_pstrdup(conflict->pool,
                                                    deleted_rev_author);

          details->added_rev = SVN_INVALID_REVNUM;
          details->added_rev_author = NULL;
          details->moves = moves;
        }
    }

  conflict->tree_conflict_incoming_details = details;

  return SVN_NO_ERROR;
}

static const char *
describe_incoming_add_upon_update(
  struct conflict_tree_incoming_add_details *details,
  svn_node_kind_t new_node_kind,
  svn_revnum_t new_rev,
  apr_pool_t *result_pool)
{
  if (new_node_kind == svn_node_dir)
    {
      if (SVN_IS_VALID_REVNUM(details->added_rev) &&
          SVN_IS_VALID_REVNUM(details->deleted_rev))
        return apr_psprintf(result_pool,
                            _("A new directory appeared during update to r%ld; "
                              "it was added by %s in r%ld and later deleted "
                              "by %s in r%ld."), new_rev,
                            details->added_rev_author, details->added_rev,
                            details->deleted_rev_author, details->deleted_rev);
      else if (SVN_IS_VALID_REVNUM(details->added_rev))
        return apr_psprintf(result_pool,
                            _("A new directory appeared during update to r%ld; "
                              "it was added by %s in r%ld."), new_rev,
                            details->added_rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new directory appeared during update to r%ld; "
                              "it was deleted by %s in r%ld."), new_rev,
                            details->deleted_rev_author, details->deleted_rev);
    }
  else if (new_node_kind == svn_node_file ||
           new_node_kind == svn_node_symlink)
    {
      if (SVN_IS_VALID_REVNUM(details->added_rev) &&
          SVN_IS_VALID_REVNUM(details->deleted_rev))
        return apr_psprintf(result_pool,
                            _("A new file appeared during update to r%ld; "
                              "it was added by %s in r%ld and later deleted "
                              "by %s in r%ld."), new_rev,
                            details->added_rev_author, details->added_rev,
                            details->deleted_rev_author, details->deleted_rev);
      else if (SVN_IS_VALID_REVNUM(details->added_rev))
        return apr_psprintf(result_pool,
                            _("A new file appeared during update to r%ld; "
                              "it was added by %s in r%ld."), new_rev,
                            details->added_rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new file appeared during update to r%ld; "
                              "it was deleted by %s in r%ld."), new_rev,
                            details->deleted_rev_author, details->deleted_rev);
    }
  else
    {
      if (SVN_IS_VALID_REVNUM(details->added_rev) &&
          SVN_IS_VALID_REVNUM(details->deleted_rev))
        return apr_psprintf(result_pool,
                            _("A new item appeared during update to r%ld; "
                              "it was added by %s in r%ld and later deleted "
                              "by %s in r%ld."), new_rev,
                            details->added_rev_author, details->added_rev,
                            details->deleted_rev_author, details->deleted_rev);
      else if (SVN_IS_VALID_REVNUM(details->added_rev))
        return apr_psprintf(result_pool,
                            _("A new item appeared during update to r%ld; "
                              "it was added by %s in r%ld."), new_rev,
                            details->added_rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new item appeared during update to r%ld; "
                              "it was deleted by %s in r%ld."), new_rev,
                            details->deleted_rev_author, details->deleted_rev);
    }
}

static const char *
describe_incoming_add_upon_switch(
  struct conflict_tree_incoming_add_details *details,
  svn_node_kind_t victim_node_kind,
  const char *new_repos_relpath,
  svn_revnum_t new_rev,
  apr_pool_t *result_pool)
{
  if (victim_node_kind == svn_node_dir)
    {
      if (SVN_IS_VALID_REVNUM(details->added_rev) &&
          SVN_IS_VALID_REVNUM(details->deleted_rev))
        return apr_psprintf(result_pool,
                            _("A new directory appeared during switch to\n"
                              "'^/%s@%ld'.\n"
                              "It was added by %s in r%ld and later deleted "
                              "by %s in r%ld."), new_repos_relpath, new_rev,
                            details->added_rev_author, details->added_rev,
                            details->deleted_rev_author, details->deleted_rev);
      else if (SVN_IS_VALID_REVNUM(details->added_rev))
        return apr_psprintf(result_pool,
                            _("A new directory appeared during switch to\n"
                             "'^/%s@%ld'.\nIt was added by %s in r%ld."),
                            new_repos_relpath, new_rev,
                            details->added_rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new directory appeared during switch to\n"
                              "'^/%s@%ld'.\nIt was deleted by %s in r%ld."),
                            new_repos_relpath, new_rev,
                            details->deleted_rev_author, details->deleted_rev);
    }
  else if (victim_node_kind == svn_node_file ||
           victim_node_kind == svn_node_symlink)
    {
      if (SVN_IS_VALID_REVNUM(details->added_rev) &&
          SVN_IS_VALID_REVNUM(details->deleted_rev))
        return apr_psprintf(result_pool,
                            _("A new file appeared during switch to\n"
                              "'^/%s@%ld'.\n"
                              "It was added by %s in r%ld and later deleted "
                              "by %s in r%ld."), new_repos_relpath, new_rev,
                            details->added_rev_author, details->added_rev,
                            details->deleted_rev_author, details->deleted_rev);
      else if (SVN_IS_VALID_REVNUM(details->added_rev))
        return apr_psprintf(result_pool,
                            _("A new file appeared during switch to\n"
                              "'^/%s@%ld'.\n"
                              "It was added by %s in r%ld."),
                            new_repos_relpath, new_rev,
                            details->added_rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new file appeared during switch to\n"
                              "'^/%s@%ld'.\n"
                              "It was deleted by %s in r%ld."),
                            new_repos_relpath, new_rev,
                            details->deleted_rev_author, details->deleted_rev);
    }
  else
    {
      if (SVN_IS_VALID_REVNUM(details->added_rev) &&
          SVN_IS_VALID_REVNUM(details->deleted_rev))
        return apr_psprintf(result_pool,
                            _("A new item appeared during switch to\n"
                              "'^/%s@%ld'.\n"
                              "It was added by %s in r%ld and later deleted "
                              "by %s in r%ld."), new_repos_relpath, new_rev,
                            details->added_rev_author, details->added_rev,
                            details->deleted_rev_author, details->deleted_rev);
      else if (SVN_IS_VALID_REVNUM(details->added_rev))
        return apr_psprintf(result_pool,
                            _("A new item appeared during switch to\n"
                              "'^/%s@%ld'.\n"
                              "It was added by %s in r%ld."),
                            new_repos_relpath, new_rev,
                            details->added_rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new item appeared during switch to\n"
                              "'^/%s@%ld'.\n"
                              "It was deleted by %s in r%ld."),
                            new_repos_relpath, new_rev,
                            details->deleted_rev_author, details->deleted_rev);
    }
}

static const char *
describe_incoming_add_upon_merge(
  struct conflict_tree_incoming_add_details *details,
  svn_node_kind_t new_node_kind,
  svn_revnum_t old_rev,
  const char *new_repos_relpath,
  svn_revnum_t new_rev,
  apr_pool_t *result_pool)
{
  if (new_node_kind == svn_node_dir)
    {
      if (old_rev + 1 == new_rev)
        return apr_psprintf(result_pool,
                            _("A new directory appeared during merge of\n"
                              "'^/%s:%ld'.\nIt was added by %s in r%ld."),
                            new_repos_relpath, new_rev,
                            details->added_rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new directory appeared during merge of\n"
                              "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."),
                            new_repos_relpath, old_rev + 1, new_rev,
                            details->added_rev_author, details->added_rev);
    }
  else if (new_node_kind == svn_node_file ||
           new_node_kind == svn_node_symlink)
    {
      if (old_rev + 1 == new_rev)
        return apr_psprintf(result_pool,
                            _("A new file appeared during merge of\n"
                              "'^/%s:%ld'.\nIt was added by %s in r%ld."),
                            new_repos_relpath, new_rev,
                            details->added_rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new file appeared during merge of\n"
                              "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."),
                            new_repos_relpath, old_rev + 1, new_rev,
                            details->added_rev_author, details->added_rev);
    }
  else
    {
      if (old_rev + 1 == new_rev)
        return apr_psprintf(result_pool,
                            _("A new item appeared during merge of\n"
                              "'^/%s:%ld'.\nIt was added by %s in r%ld."),
                            new_repos_relpath, new_rev,
                            details->added_rev_author, details->added_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new item appeared during merge of\n"
                              "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."),
                            new_repos_relpath, old_rev + 1, new_rev,
                            details->added_rev_author, details->added_rev);
    }
}

static const char *
describe_incoming_reverse_deletion_upon_merge(
  struct conflict_tree_incoming_add_details *details,
  svn_node_kind_t new_node_kind,
  const char *old_repos_relpath,
  svn_revnum_t old_rev,
  svn_revnum_t new_rev,
  apr_pool_t *result_pool)
{
  if (new_node_kind == svn_node_dir)
    {
      if (new_rev + 1 == old_rev)
        return apr_psprintf(result_pool,
                            _("A new directory appeared during reverse-merge of"
                              "\n'^/%s:%ld'.\nIt was deleted by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            details->deleted_rev_author,
                            details->deleted_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new directory appeared during reverse-merge "
                              "of\n'^/%s:%ld-%ld'.\n"
                              "It was deleted by %s in r%ld."),
                            old_repos_relpath, new_rev, rev_below(old_rev),
                            details->deleted_rev_author,
                            details->deleted_rev);
    }
  else if (new_node_kind == svn_node_file ||
           new_node_kind == svn_node_symlink)
    {
      if (new_rev + 1 == old_rev)
        return apr_psprintf(result_pool,
                            _("A new file appeared during reverse-merge of\n"
                              "'^/%s:%ld'.\nIt was deleted by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            details->deleted_rev_author,
                            details->deleted_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new file appeared during reverse-merge of\n"
                              "'^/%s:%ld-%ld'.\nIt was deleted by %s in r%ld."),
                            old_repos_relpath, new_rev + 1, old_rev,
                            details->deleted_rev_author,
                            details->deleted_rev);
    }
  else
    {
      if (new_rev + 1 == old_rev)
        return apr_psprintf(result_pool,
                            _("A new item appeared during reverse-merge of\n"
                              "'^/%s:%ld'.\nIt was deleted by %s in r%ld."),
                            old_repos_relpath, old_rev,
                            details->deleted_rev_author,
                            details->deleted_rev);
      else
        return apr_psprintf(result_pool,
                            _("A new item appeared during reverse-merge of\n"
                              "'^/%s:%ld-%ld'.\nIt was deleted by %s in r%ld."),
                            old_repos_relpath, new_rev + 1, old_rev,
                            details->deleted_rev_author,
                            details->deleted_rev);
    }
}

/* Implements tree_conflict_get_description_func_t. */
static svn_error_t *
conflict_tree_get_description_incoming_add(
  const char **incoming_change_description,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  const char *action;
  svn_node_kind_t victim_node_kind;
  svn_wc_operation_t conflict_operation;
  const char *old_repos_relpath;
  svn_revnum_t old_rev;
  svn_node_kind_t old_node_kind;
  const char *new_repos_relpath;
  svn_revnum_t new_rev;
  svn_node_kind_t new_node_kind;
  struct conflict_tree_incoming_add_details *details;

  if (conflict->tree_conflict_incoming_details == NULL)
    return svn_error_trace(conflict_tree_get_incoming_description_generic(
                             incoming_change_description, conflict, ctx,
                             result_pool, scratch_pool));

  conflict_operation = svn_client_conflict_get_operation(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);

  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &old_repos_relpath, &old_rev, &old_node_kind, conflict,
            scratch_pool, scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &new_repos_relpath, &new_rev, &new_node_kind, conflict,
            scratch_pool, scratch_pool));

  details = conflict->tree_conflict_incoming_details;

  if (conflict_operation == svn_wc_operation_update)
    {
      action = describe_incoming_add_upon_update(details,
                                                 new_node_kind,
                                                 new_rev,
                                                 result_pool);
    }
  else if (conflict_operation == svn_wc_operation_switch)
    {
      action = describe_incoming_add_upon_switch(details,
                                                 victim_node_kind,
                                                 new_repos_relpath,
                                                 new_rev,
                                                 result_pool);
    }
  else if (conflict_operation == svn_wc_operation_merge)
    {
      if (old_rev < new_rev)
        action = describe_incoming_add_upon_merge(details,
                                                  new_node_kind,
                                                  old_rev,
                                                  new_repos_relpath,
                                                  new_rev,
                                                  result_pool);
      else
        action = describe_incoming_reverse_deletion_upon_merge(
                   details, new_node_kind, old_repos_relpath,
                   old_rev, new_rev, result_pool);
    }

  *incoming_change_description = apr_pstrdup(result_pool, action);

  return SVN_NO_ERROR;
}

/* Details for tree conflicts involving incoming edits.
 * Note that we store an array of these. Each element corresponds to a
 * revision within the old/new range in which a modification occured. */
struct conflict_tree_incoming_edit_details
{
  /* The revision in which the edit ocurred. */
  svn_revnum_t rev;

  /* The author of the revision. */
  const char *author;

  /** Is the text modified? May be svn_tristate_unknown. */
  svn_tristate_t text_modified;

  /** Are properties modified? May be svn_tristate_unknown. */
  svn_tristate_t props_modified;

  /** For directories, are children modified?
   * May be svn_tristate_unknown. */
  svn_tristate_t children_modified;

  /* The path which was edited, relative to the repository root. */
  const char *repos_relpath;
};

/* Baton for find_modified_rev(). */
struct find_modified_rev_baton {
  const char *victim_abspath;
  svn_client_ctx_t *ctx;
  apr_array_header_t *edits;
  const char *repos_relpath;
  svn_node_kind_t node_kind;
  apr_pool_t *result_pool;
  apr_pool_t *scratch_pool;
};

/* Implements svn_log_entry_receiver_t. */
static svn_error_t *
find_modified_rev(void *baton,
                  svn_log_entry_t *log_entry,
                  apr_pool_t *scratch_pool)
{
  struct find_modified_rev_baton *b = baton;
  struct conflict_tree_incoming_edit_details *details = NULL;
  svn_string_t *author;
  apr_hash_index_t *hi;
  apr_pool_t *iterpool;

  if (b->ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(
                 b->victim_abspath,
                 svn_wc_notify_tree_conflict_details_progress,
                 scratch_pool),
      notify->revision = log_entry->revision;
      b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool);
    }

  /* No paths were changed in this revision.  Nothing to do. */
  if (! log_entry->changed_paths2)
    return SVN_NO_ERROR;

  details = apr_pcalloc(b->result_pool, sizeof(*details));
  details->rev = log_entry->revision;
  author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR);
  if (author)
    details->author = apr_pstrdup(b->result_pool, author->data);
  else
    details->author = _("unknown author");

  details->text_modified = svn_tristate_unknown;
  details->props_modified = svn_tristate_unknown;
  details->children_modified = svn_tristate_unknown;

  iterpool = svn_pool_create(scratch_pool);
  for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2);
       hi != NULL;
       hi = apr_hash_next(hi))
    {
      void *val;
      const char *path;
      svn_log_changed_path2_t *log_item;

      svn_pool_clear(iterpool);

      apr_hash_this(hi, (void *) &path, NULL, &val);
      log_item = val;

      /* ### Remove leading slash from paths in log entries. */
      if (path[0] == '/')
          path = svn_relpath_canonicalize(path, iterpool);

      if (svn_path_compare_paths(b->repos_relpath, path) == 0 &&
          (log_item->action == 'M' || log_item->action == 'A'))
        {
          details->text_modified = log_item->text_modified;
          details->props_modified = log_item->props_modified;
          details->repos_relpath = apr_pstrdup(b->result_pool, path);

          if (log_item->copyfrom_path)
            b->repos_relpath = apr_pstrdup(b->scratch_pool,
                                          /* ### remove leading slash */
                                           svn_relpath_canonicalize(
                                               log_item->copyfrom_path,
                                               iterpool));
        }
      else if (b->node_kind == svn_node_dir &&
               svn_relpath_skip_ancestor(b->repos_relpath, path) != NULL)
        details->children_modified = svn_tristate_true;
    }

  if (b->node_kind == svn_node_dir &&
      details->children_modified == svn_tristate_unknown)
        details->children_modified = svn_tristate_false;

  APR_ARRAY_PUSH(b->edits, struct conflict_tree_incoming_edit_details *) =
    details;

  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

/* Implements tree_conflict_get_details_func_t.
 * Find one or more revisions in which the victim was modified in the
 * repository. */
static svn_error_t *
conflict_tree_get_details_incoming_edit(svn_client_conflict_t *conflict,
                                        svn_client_ctx_t *ctx,
                                        apr_pool_t *scratch_pool)
{
  const char *old_repos_relpath;
  const char *new_repos_relpath;
  const char *repos_root_url;
  svn_revnum_t old_rev;
  svn_revnum_t new_rev;
  svn_node_kind_t old_node_kind;
  svn_node_kind_t new_node_kind;
  svn_wc_operation_t operation;
  const char *url;
  const char *corrected_url;
  svn_ra_session_t *ra_session;
  apr_array_header_t *paths;
  apr_array_header_t *revprops;
  struct find_modified_rev_baton b = { 0 };

  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &old_repos_relpath, &old_rev, &old_node_kind, conflict,
            scratch_pool, scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &new_repos_relpath, &new_rev, &new_node_kind, conflict,
            scratch_pool, scratch_pool));
  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict,
                                             scratch_pool, scratch_pool));
  operation = svn_client_conflict_get_operation(conflict);
  if (operation == svn_wc_operation_update)
    {
      b.node_kind = old_rev < new_rev ? new_node_kind : old_node_kind;

      /* If there is no node then we cannot find any edits. */
      if (b.node_kind == svn_node_none)
        return SVN_NO_ERROR;

      url = svn_path_url_add_component2(repos_root_url,
                                        old_rev < new_rev ? new_repos_relpath
                                                          : old_repos_relpath,
                                        scratch_pool);

      b.repos_relpath = old_rev < new_rev ? new_repos_relpath
                                          : old_repos_relpath;
    }
  else if (operation == svn_wc_operation_switch ||
           operation == svn_wc_operation_merge)
    {
      url = svn_path_url_add_component2(repos_root_url, new_repos_relpath,
                                        scratch_pool);

      b.repos_relpath = new_repos_relpath;
      b.node_kind = new_node_kind;
    }

  SVN_ERR(svn_client__open_ra_session_internal(&ra_session,
                                               &corrected_url,
                                               url, NULL, NULL,
                                               FALSE,
                                               FALSE,
                                               ctx,
                                               scratch_pool,
                                               scratch_pool));

  paths = apr_array_make(scratch_pool, 1, sizeof(const char *));
  APR_ARRAY_PUSH(paths, const char *) = "";

  revprops = apr_array_make(scratch_pool, 1, sizeof(const char *));
  APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR;

  b.ctx = ctx;
  b.victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  b.result_pool = conflict->pool;
  b.scratch_pool = scratch_pool;
  b.edits = apr_array_make(
               conflict->pool, 0,
               sizeof(struct conflict_tree_incoming_edit_details *));

  SVN_ERR(svn_ra_get_log2(ra_session, paths,
                          old_rev < new_rev ? old_rev : new_rev,
                          old_rev < new_rev ? new_rev : old_rev,
                          0, /* no limit */
                          TRUE, /* need the changed paths list */
                          FALSE, /* need to traverse copies */
                          FALSE, /* no need for merged revisions */
                          revprops,
                          find_modified_rev, &b,
                          scratch_pool));

  conflict->tree_conflict_incoming_details = b.edits;

  return SVN_NO_ERROR;
}

static const char *
describe_incoming_edit_upon_update(svn_revnum_t old_rev,
                                   svn_revnum_t new_rev,
                                   svn_node_kind_t old_node_kind,
                                   svn_node_kind_t new_node_kind,
                                   apr_pool_t *result_pool)
{
  if (old_rev < new_rev)
    {
      if (new_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Changes destined for a directory arrived "
                              "via the following revisions during update "
                              "from r%ld to r%ld."), old_rev, new_rev);
      else if (new_node_kind == svn_node_file ||
               new_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("Changes destined for a file arrived "
                              "via the following revisions during update "
                              "from r%ld to r%ld"), old_rev, new_rev);
      else
        return apr_psprintf(result_pool,
                            _("Changes from the following revisions arrived "
                              "during update from r%ld to r%ld"),
                            old_rev, new_rev);
    }
  else
    {
      if (new_node_kind == svn_node_dir)
        return apr_psprintf(result_pool,
                            _("Changes destined for a directory arrived "
                              "via the following revisions during backwards "
                              "update from r%ld to r%ld"),
                            old_rev, new_rev);
      else if (new_node_kind == svn_node_file ||
               new_node_kind == svn_node_symlink)
        return apr_psprintf(result_pool,
                            _("Changes destined for a file arrived "
                              "via the following revisions during backwards "
                              "update from r%ld to r%ld"),
                            old_rev, new_rev);
      else
        return apr_psprintf(result_pool,
                            _("Changes from the following revisions arrived "
                              "during backwards update from r%ld to r%ld"),
                            old_rev, new_rev);
    }
}

static const char *
describe_incoming_edit_upon_switch(const char *new_repos_relpath,
                                   svn_revnum_t new_rev,
                                   svn_node_kind_t new_node_kind,
                                   apr_pool_t *result_pool)
{
  if (new_node_kind == svn_node_dir)
    return apr_psprintf(result_pool,
                        _("Changes destined for a directory arrived via "
                          "the following revisions during switch to\n"
                          "'^/%s@r%ld'"),
                        new_repos_relpath, new_rev);
  else if (new_node_kind == svn_node_file ||
           new_node_kind == svn_node_symlink)
    return apr_psprintf(result_pool,
                        _("Changes destined for a directory arrived via "
                          "the following revisions during switch to\n"
                          "'^/%s@r%ld'"),
                        new_repos_relpath, new_rev);
  else
    return apr_psprintf(result_pool,
                        _("Changes from the following revisions arrived "
                          "during switch to\n'^/%s@r%ld'"),
                        new_repos_relpath, new_rev);
}

/* Return a string showing the list of revisions in EDITS, ensuring
 * the string won't grow too large for display. */
static const char *
describe_incoming_edit_list_modified_revs(apr_array_header_t *edits,
                                          apr_pool_t *result_pool)
{
  int num_revs_to_skip;
  static const int min_revs_for_skipping = 5;
  static const int max_revs_to_display = 8;
  const char *s = "";
  int i;

  if (edits->nelts == 0)
    return _(" (no revisions found)");

  if (edits->nelts <= max_revs_to_display)
    num_revs_to_skip = 0;
  else
    {
      /* Check if we should insert a placeholder for some revisions because
       * the string would grow too long for display otherwise. */
      num_revs_to_skip = edits->nelts - max_revs_to_display;
      if (num_revs_to_skip < min_revs_for_skipping)
        {
          /* Don't bother with the placeholder. Just list all revisions. */
          num_revs_to_skip = 0;
        }
    }

  for (i = 0; i < edits->nelts; i++)
    {
      struct conflict_tree_incoming_edit_details *details;

      details = APR_ARRAY_IDX(edits, i,
                              struct conflict_tree_incoming_edit_details *);
      if (num_revs_to_skip > 0)
        {
          /* Insert a placeholder for revisions falling into the middle of
           * the range so we'll get something that looks like:
           * 1, 2, 3, 4, 5 [ placeholder ] 95, 96, 97, 98, 99 */
          if (i < max_revs_to_display / 2)
            s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s,
                             details->rev, details->author,
                             i < edits->nelts - 1 ? "," : "");
          else if (i >= max_revs_to_display / 2 &&
                   i < edits->nelts - (max_revs_to_display / 2))
              continue;
          else
            {
              if (i == edits->nelts - (max_revs_to_display / 2))
                  s = apr_psprintf(result_pool,
                                   Q_("%s\n [%d revision omitted for "
                                      "brevity],\n",
                                      "%s\n [%d revisions omitted for "
                                      "brevity],\n",
                                      num_revs_to_skip),
                                   s, num_revs_to_skip);

              s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s,
                               details->rev, details->author,
                               i < edits->nelts - 1 ? "," : "");
            }
        }
      else
        s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s,
                         details->rev, details->author,
                         i < edits->nelts - 1 ? "," : "");
    }

  return s;
}

/* Implements tree_conflict_get_description_func_t. */
static svn_error_t *
conflict_tree_get_description_incoming_edit(
  const char **incoming_change_description,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  const char *action;
  svn_wc_operation_t conflict_operation;
  const char *old_repos_relpath;
  svn_revnum_t old_rev;
  svn_node_kind_t old_node_kind;
  const char *new_repos_relpath;
  svn_revnum_t new_rev;
  svn_node_kind_t new_node_kind;
  apr_array_header_t *edits;

  if (conflict->tree_conflict_incoming_details == NULL)
    return svn_error_trace(conflict_tree_get_incoming_description_generic(
                             incoming_change_description, conflict, ctx,
                             result_pool, scratch_pool));

  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &old_repos_relpath, &old_rev, &old_node_kind, conflict,
            scratch_pool, scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &new_repos_relpath, &new_rev, &new_node_kind, conflict,
            scratch_pool, scratch_pool));

  conflict_operation = svn_client_conflict_get_operation(conflict);

  edits = conflict->tree_conflict_incoming_details;

  if (conflict_operation == svn_wc_operation_update)
    action = describe_incoming_edit_upon_update(old_rev, new_rev,
                                                old_node_kind, new_node_kind,
                                                scratch_pool);
  else if (conflict_operation == svn_wc_operation_switch)
    action = describe_incoming_edit_upon_switch(new_repos_relpath, new_rev,
                                                new_node_kind, scratch_pool);
  else if (conflict_operation == svn_wc_operation_merge)
    {
      /* Handle merge inline because it returns early sometimes. */
      if (old_rev < new_rev)
        {
          if (old_rev + 1 == new_rev)
            {
              if (new_node_kind == svn_node_dir)
                action = apr_psprintf(scratch_pool,
                                      _("Changes destined for a directory "
                                        "arrived during merge of\n"
                                        "'^/%s:%ld'."),
                                        new_repos_relpath, new_rev);
              else if (new_node_kind == svn_node_file ||
                       new_node_kind == svn_node_symlink)
                action = apr_psprintf(scratch_pool,
                                      _("Changes destined for a file "
                                        "arrived during merge of\n"
                                        "'^/%s:%ld'."),
                                      new_repos_relpath, new_rev);
              else
                action = apr_psprintf(scratch_pool,
                                      _("Changes arrived during merge of\n"
                                        "'^/%s:%ld'."),
                                      new_repos_relpath, new_rev);

              *incoming_change_description = apr_pstrdup(result_pool, action);

              return SVN_NO_ERROR;
            }
          else
            {
              if (new_node_kind == svn_node_dir)
                action = apr_psprintf(scratch_pool,
                                      _("Changes destined for a directory "
                                        "arrived via the following revisions "
                                        "during merge of\n'^/%s:%ld-%ld'"),
                                      new_repos_relpath, old_rev + 1, new_rev);
              else if (new_node_kind == svn_node_file ||
                       new_node_kind == svn_node_symlink)
                action = apr_psprintf(scratch_pool,
                                      _("Changes destined for a file "
                                        "arrived via the following revisions "
                                        "during merge of\n'^/%s:%ld-%ld'"),
                                      new_repos_relpath, old_rev + 1, new_rev);
              else
                action = apr_psprintf(scratch_pool,
                                      _("Changes from the following revisions "
                                        "arrived during merge of\n"
                                        "'^/%s:%ld-%ld'"),
                                      new_repos_relpath, old_rev + 1, new_rev);
            }
        }
      else
        {
          if (new_rev + 1 == old_rev)
            {
              if (new_node_kind == svn_node_dir)
                action = apr_psprintf(scratch_pool,
                                      _("Changes destined for a directory "
                                        "arrived during reverse-merge of\n"
                                        "'^/%s:%ld'."),
                                      new_repos_relpath, old_rev);
              else if (new_node_kind == svn_node_file ||
                       new_node_kind == svn_node_symlink)
                action = apr_psprintf(scratch_pool,
                                      _("Changes destined for a file "
                                        "arrived during reverse-merge of\n"
                                        "'^/%s:%ld'."),
                                      new_repos_relpath, old_rev);
              else
                action = apr_psprintf(scratch_pool,
                                      _("Changes arrived during reverse-merge "
                                        "of\n'^/%s:%ld'."),
                                      new_repos_relpath, old_rev);

              *incoming_change_description = apr_pstrdup(result_pool, action);

              return SVN_NO_ERROR;
            }
          else
            {
              if (new_node_kind == svn_node_dir)
                action = apr_psprintf(scratch_pool,
                                      _("Changes destined for a directory "
                                        "arrived via the following revisions "
                                        "during reverse-merge of\n"
                                        "'^/%s:%ld-%ld'"),
                                      new_repos_relpath, new_rev + 1, old_rev);
              else if (new_node_kind == svn_node_file ||
                       new_node_kind == svn_node_symlink)
                action = apr_psprintf(scratch_pool,
                                      _("Changes destined for a file "
                                        "arrived via the following revisions "
                                        "during reverse-merge of\n"
                                        "'^/%s:%ld-%ld'"),
                                      new_repos_relpath, new_rev + 1, old_rev);

              else
                action = apr_psprintf(scratch_pool,
                                      _("Changes from the following revisions "
                                        "arrived during reverse-merge of\n"
                                        "'^/%s:%ld-%ld'"),
                                      new_repos_relpath, new_rev + 1, old_rev);
            }
        }
    }

  action = apr_psprintf(scratch_pool, "%s:\n%s", action,
                        describe_incoming_edit_list_modified_revs(
                          edits, scratch_pool));
  *incoming_change_description = apr_pstrdup(result_pool, action);

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_tree_get_description(
  const char **incoming_change_description,
  const char **local_change_description,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  SVN_ERR(conflict->tree_conflict_get_incoming_description_func(
            incoming_change_description,
            conflict, ctx, result_pool, scratch_pool));

  SVN_ERR(conflict->tree_conflict_get_local_description_func(
            local_change_description,
            conflict, ctx, result_pool, scratch_pool));

  return SVN_NO_ERROR;
}

void
svn_client_conflict_option_set_merged_propval(
  svn_client_conflict_option_t *option,
  const svn_string_t *merged_propval)
{
  option->type_data.prop.merged_propval = svn_string_dup(merged_propval,
                                                         option->pool);
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_postpone(svn_client_conflict_option_t *option,
                 svn_client_conflict_t *conflict,
                 svn_client_ctx_t *ctx,
                 apr_pool_t *scratch_pool)
{
  return SVN_NO_ERROR; /* Nothing to do. */
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_text_conflict(svn_client_conflict_option_t *option,
                      svn_client_conflict_t *conflict,
                      svn_client_ctx_t *ctx,
                      apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *local_abspath;
  const char *lock_abspath;
  svn_wc_conflict_choice_t conflict_choice;
  svn_error_t *err;

  option_id = svn_client_conflict_option_get_id(option);
  conflict_choice = conflict_option_id_to_wc_conflict_choice(option_id);
  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));
  err = svn_wc__conflict_text_mark_resolved(ctx->wc_ctx,
                                            local_abspath,
                                            conflict_choice,
                                            ctx->cancel_func,
                                            ctx->cancel_baton,
                                            ctx->notify_func2,
                                            ctx->notify_baton2,
                                            scratch_pool);
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  svn_io_sleep_for_timestamps(local_abspath, scratch_pool);
  SVN_ERR(err);

  conflict->resolution_text = option_id;

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_prop_conflict(svn_client_conflict_option_t *option,
                      svn_client_conflict_t *conflict,
                      svn_client_ctx_t *ctx,
                      apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  svn_wc_conflict_choice_t conflict_choice;
  const char *local_abspath;
  const char *lock_abspath;
  const char *propname = option->type_data.prop.propname;
  svn_error_t *err;
  const svn_string_t *merged_value;

  option_id = svn_client_conflict_option_get_id(option);
  conflict_choice = conflict_option_id_to_wc_conflict_choice(option_id);
  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  if (option_id == svn_client_conflict_option_merged_text)
    merged_value = option->type_data.prop.merged_propval;
  else
    merged_value = NULL;

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));
  err = svn_wc__conflict_prop_mark_resolved(ctx->wc_ctx, local_abspath,
                                            propname, conflict_choice,
                                            merged_value,
                                            ctx->notify_func2,
                                            ctx->notify_baton2,
                                            scratch_pool);
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  svn_io_sleep_for_timestamps(local_abspath, scratch_pool);
  SVN_ERR(err);

  if (propname[0] == '\0')
    {
      apr_hash_index_t *hi;

      /* All properties have been resolved to the same option. */
      for (hi = apr_hash_first(scratch_pool, conflict->prop_conflicts);
           hi;
           hi = apr_hash_next(hi))
        {
          const char *this_propname = apr_hash_this_key(hi);

          svn_hash_sets(conflict->resolved_props,
                        apr_pstrdup(apr_hash_pool_get(conflict->resolved_props),
                                    this_propname),
                        option);
          svn_hash_sets(conflict->prop_conflicts, this_propname, NULL);
        }

      conflict->legacy_prop_conflict_propname = NULL;
    }
  else
    {
      svn_hash_sets(conflict->resolved_props,
                    apr_pstrdup(apr_hash_pool_get(conflict->resolved_props),
                                propname),
                   option);
      svn_hash_sets(conflict->prop_conflicts, propname, NULL);

      if (apr_hash_count(conflict->prop_conflicts) > 0)
        conflict->legacy_prop_conflict_propname =
            apr_hash_this_key(apr_hash_first(scratch_pool,
                                             conflict->prop_conflicts));
      else
        conflict->legacy_prop_conflict_propname = NULL;
    }

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_accept_current_wc_state(svn_client_conflict_option_t *option,
                                svn_client_conflict_t *conflict,
                                svn_client_ctx_t *ctx,
                                apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *local_abspath;
  const char *lock_abspath;
  svn_error_t *err;

  option_id = svn_client_conflict_option_get_id(option);
  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  if (option_id != svn_client_conflict_option_accept_current_wc_state)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("Tree conflict on '%s' can only be resolved "
                               "to the current working copy state"),
                             svn_dirent_local_style(local_abspath,
                                                    scratch_pool));

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));

  /* Resolve to current working copy state. */
  err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool);

  /* svn_wc__del_tree_conflict doesn't handle notification for us */
  if (ctx->notify_func2)
    ctx->notify_func2(ctx->notify_baton2,
                      svn_wc_create_notify(local_abspath,
                                           svn_wc_notify_resolved_tree,
                                           scratch_pool),
                      scratch_pool);

  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  conflict->resolution_tree = option_id;

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_update_break_moved_away(svn_client_conflict_option_t *option,
                                svn_client_conflict_t *conflict,
                                svn_client_ctx_t *ctx,
                                apr_pool_t *scratch_pool)
{
  const char *local_abspath;
  const char *lock_abspath;
  svn_error_t *err;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));
  err = svn_wc__conflict_tree_update_break_moved_away(ctx->wc_ctx,
                                                      local_abspath,
                                                      ctx->cancel_func,
                                                      ctx->cancel_baton,
                                                      ctx->notify_func2,
                                                      ctx->notify_baton2,
                                                      scratch_pool);
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_update_raise_moved_away(svn_client_conflict_option_t *option,
                                svn_client_conflict_t *conflict,
                                svn_client_ctx_t *ctx,
                                apr_pool_t *scratch_pool)
{
  const char *local_abspath;
  const char *lock_abspath;
  svn_error_t *err;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));
  err = svn_wc__conflict_tree_update_raise_moved_away(ctx->wc_ctx,
                                                      local_abspath,
                                                      ctx->cancel_func,
                                                      ctx->cancel_baton,
                                                      ctx->notify_func2,
                                                      ctx->notify_baton2,
                                                      scratch_pool);
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_update_moved_away_node(svn_client_conflict_option_t *option,
                               svn_client_conflict_t *conflict,
                               svn_client_ctx_t *ctx,
                               apr_pool_t *scratch_pool)
{
  const char *local_abspath;
  const char *lock_abspath;
  svn_error_t *err;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));
  err = svn_wc__conflict_tree_update_moved_away_node(ctx->wc_ctx,
                                                     local_abspath,
                                                     ctx->cancel_func,
                                                     ctx->cancel_baton,
                                                     ctx->notify_func2,
                                                     ctx->notify_baton2,
                                                     scratch_pool);
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  svn_io_sleep_for_timestamps(local_abspath, scratch_pool);
  SVN_ERR(err);

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  return SVN_NO_ERROR;
}

/* Verify the local working copy state matches what we expect when an
 * incoming add vs add tree conflict exists after an update operation.
 * We assume the update operation leaves the working copy in a state which
 * prefers the local change and cancels the incoming addition.
 * Run a quick sanity check and error out if it looks as if the
 * working copy was modified since, even though it's not easy to make
 * such modifications without also clearing the conflict marker. */
static svn_error_t *
verify_local_state_for_incoming_add_upon_update(
  svn_client_conflict_t *conflict,
  svn_client_conflict_option_t *option,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  const char *local_abspath;
  svn_client_conflict_option_id_t option_id;
  const char *wcroot_abspath;
  svn_wc_operation_t operation;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;
  const char *base_repos_relpath;
  svn_revnum_t base_rev;
  svn_node_kind_t base_kind;
  const char *local_style_relpath;
  svn_boolean_t is_added;
  svn_error_t *err;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);
  option_id = svn_client_conflict_option_get_id(option);
  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                             local_abspath, scratch_pool,
                             scratch_pool));
  operation = svn_client_conflict_get_operation(conflict);
  SVN_ERR_ASSERT(operation == svn_wc_operation_update);

  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  local_style_relpath = svn_dirent_local_style(
                          svn_dirent_skip_ancestor(wcroot_abspath,
                                                   local_abspath),
                          scratch_pool);

  /* Check if a local addition addition replaces the incoming new node. */
  err = svn_wc__node_get_base(&base_kind, &base_rev, &base_repos_relpath,
                              NULL, NULL, NULL, ctx->wc_ctx, local_abspath,
                              FALSE, scratch_pool, scratch_pool);
  if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
    {
      if (option_id == svn_client_conflict_option_incoming_add_ignore)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected a base node but found none)"),
                                 local_style_relpath);
      else if (option_id ==
               svn_client_conflict_option_incoming_added_dir_replace)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected a base node but found none)"),
                                 local_style_relpath);
      else
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err,
                                 _("Unexpected option id '%d'"), option_id);
    }
  else if (err)
    return svn_error_trace(err);

  if (base_kind != incoming_new_kind)
    {
      if (option_id == svn_client_conflict_option_incoming_add_ignore)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected base node kind '%s', "
                                   "but found '%s')"),
                                 local_style_relpath,
                                 svn_node_kind_to_word(incoming_new_kind),
                                 svn_node_kind_to_word(base_kind));
      else if (option_id ==
               svn_client_conflict_option_incoming_added_dir_replace)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected base node kind '%s', "
                                   "but found '%s')"),
                                  local_style_relpath,
                                 svn_node_kind_to_word(incoming_new_kind),
                                 svn_node_kind_to_word(base_kind));
      else
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Unexpected option id '%d'"), option_id);
    }

  if (strcmp(base_repos_relpath, incoming_new_repos_relpath) != 0 ||
      base_rev != incoming_new_pegrev)
    {
      if (option_id == svn_client_conflict_option_incoming_add_ignore)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected base node from '^/%s@%ld', "
                                   "but found '^/%s@%ld')"),
                                 local_style_relpath,
                                 incoming_new_repos_relpath,
                                 incoming_new_pegrev,
                                 base_repos_relpath, base_rev);
      else if (option_id ==
               svn_client_conflict_option_incoming_added_dir_replace)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected base node from '^/%s@%ld', "
                                   "but found '^/%s@%ld')"),
                                 local_style_relpath,
                                 incoming_new_repos_relpath,
                                 incoming_new_pegrev,
                                 base_repos_relpath, base_rev);
      else
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Unexpected option id '%d'"), option_id);
    }

  SVN_ERR(svn_wc__node_is_added(&is_added, ctx->wc_ctx, local_abspath,
                                scratch_pool));
  if (!is_added)
    {
      if (option_id == svn_client_conflict_option_incoming_add_ignore)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected an added item, but the item "
                                   "is not added)"),
                                 local_style_relpath);

      else if (option_id ==
               svn_client_conflict_option_incoming_added_dir_replace)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected an added item, but the item "
                                   "is not added)"),
                                 local_style_relpath);
      else
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Unexpected option id '%d'"), option_id);
    }

  return SVN_NO_ERROR;
}


/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_incoming_add_ignore(svn_client_conflict_option_t *option,
                            svn_client_conflict_t *conflict,
                            svn_client_ctx_t *ctx,
                            apr_pool_t *scratch_pool)
{
  const char *local_abspath;
  const char *lock_abspath;
  svn_wc_operation_t operation;
  svn_error_t *err;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));

  if (operation == svn_wc_operation_update)
    {
      err = verify_local_state_for_incoming_add_upon_update(conflict, option,
                                                            ctx, scratch_pool);
      if (err)
        goto unlock_wc;
    }

  /* All other options for this conflict actively fetch the incoming
   * new node. We can ignore the incoming new node by doing nothing. */

  /* Resolve to current working copy state. */
  err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool);

  /* svn_wc__del_tree_conflict doesn't handle notification for us */
  if (ctx->notify_func2)
    ctx->notify_func2(ctx->notify_baton2,
                      svn_wc_create_notify(local_abspath,
                                           svn_wc_notify_resolved_tree,
                                           scratch_pool),
                      scratch_pool);

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  return SVN_NO_ERROR;
}

/* Delete entry and wc props from a set of properties. */
static void
filter_props(apr_hash_t *props, apr_pool_t *scratch_pool)
{
  apr_hash_index_t *hi;

  for (hi = apr_hash_first(scratch_pool, props);
       hi != NULL;
       hi = apr_hash_next(hi))
    {
      const char *propname = apr_hash_this_key(hi);

      if (!svn_wc_is_normal_prop(propname))
        svn_hash_sets(props, propname, NULL);
    }
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_merge_incoming_added_file_text_update(
  svn_client_conflict_option_t *option,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  const char *wc_tmpdir;
  const char *local_abspath;
  const char *lock_abspath;
  svn_wc_merge_outcome_t merge_content_outcome;
  svn_wc_notify_state_t merge_props_outcome;
  const char *empty_file_abspath;
  const char *working_file_tmp_abspath;
  svn_stream_t *working_file_stream;
  svn_stream_t *working_file_tmp_stream;
  apr_hash_t *working_props;
  apr_array_header_t *propdiffs;
  svn_error_t *err;
  svn_wc_conflict_reason_t local_change;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);

  /* Set up tempory storage for the working version of file. */
  SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath,
                             scratch_pool, scratch_pool));
  SVN_ERR(svn_stream_open_unique(&working_file_tmp_stream,
                                 &working_file_tmp_abspath, wc_tmpdir,
                                 /* Don't delete automatically! */
                                 svn_io_file_del_none,
                                 scratch_pool, scratch_pool));

  if (local_change == svn_wc_conflict_reason_unversioned)
    {
      /* Copy the unversioned file to temporary storage. */
      SVN_ERR(svn_stream_open_readonly(&working_file_stream, local_abspath,
                                       scratch_pool, scratch_pool));
      /* Unversioned files have no properties. */
      working_props = apr_hash_make(scratch_pool);
    }
  else
    {
      /* Copy the detranslated working file to temporary storage. */
      SVN_ERR(svn_wc__translated_stream(&working_file_stream, ctx->wc_ctx,
                                        local_abspath, local_abspath,
                                        SVN_WC_TRANSLATE_TO_NF,
                                        scratch_pool, scratch_pool));
      /* Get a copy of the working file's properties. */
      SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath,
                                scratch_pool, scratch_pool));
      filter_props(working_props, scratch_pool);
    }

  SVN_ERR(svn_stream_copy3(working_file_stream, working_file_tmp_stream,
                           ctx->cancel_func, ctx->cancel_baton,
                           scratch_pool));

  /* Create an empty file as fake "merge-base" for the two added files.
   * The files are not ancestrally related so this is the best we can do. */
  SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file_abspath, NULL,
                                   svn_io_file_del_on_pool_cleanup,
                                   scratch_pool, scratch_pool));

  /* Create a property diff which shows all props as added. */
  SVN_ERR(svn_prop_diffs(&propdiffs, working_props,
                         apr_hash_make(scratch_pool), scratch_pool));

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));

  /* Revert the path in order to restore the repository's line of
   * history, which is part of the BASE tree. This revert operation
   * is why are being careful about not losing the temporary copy. */
  err = svn_wc_revert6(ctx->wc_ctx, local_abspath, svn_depth_empty,
                       FALSE, NULL, TRUE, FALSE,
                       TRUE /*added_keep_local*/,
                       NULL, NULL, /* no cancellation */
                       ctx->notify_func2, ctx->notify_baton2,
                       scratch_pool);
  if (err)
    goto unlock_wc;

  /* Perform the file merge. ### Merge into tempfile and then rename on top? */
  err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome,
                      ctx->wc_ctx, empty_file_abspath,
                      working_file_tmp_abspath, local_abspath,
                      NULL, NULL, NULL, /* labels */
                      NULL, NULL, /* conflict versions */
                      FALSE, /* dry run */
                      NULL, NULL, /* diff3_cmd, merge_options */
                      NULL, propdiffs,
                      NULL, NULL, /* conflict func/baton */
                      NULL, NULL, /* don't allow user to cancel here */
                      scratch_pool);

unlock_wc:
  if (err)
      err = svn_error_quick_wrapf(
              err, _("If needed, a backup copy of '%s' can be found at '%s'"),
              svn_dirent_local_style(local_abspath, scratch_pool),
              svn_dirent_local_style(working_file_tmp_abspath, scratch_pool));
  err = svn_error_compose_create(err,
                                 svn_wc__release_write_lock(ctx->wc_ctx,
                                                            lock_abspath,
                                                            scratch_pool));
  svn_io_sleep_for_timestamps(local_abspath, scratch_pool);
  SVN_ERR(err);

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      /* Tell the world about the file merge that just happened. */
      notify = svn_wc_create_notify(local_abspath,
                                    svn_wc_notify_update_update,
                                    scratch_pool);
      if (merge_content_outcome == svn_wc_merge_conflict)
        notify->content_state = svn_wc_notify_state_conflicted;
      else
        notify->content_state = svn_wc_notify_state_merged;
      notify->prop_state = merge_props_outcome;
      notify->kind = svn_node_file;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);

      /* And also about the successfully resolved tree conflict. */
      notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  /* All is good -- remove temporary copy of the working file. */
  SVN_ERR(svn_io_remove_file2(working_file_tmp_abspath, TRUE, scratch_pool));

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_merge_incoming_added_file_text_merge(
  svn_client_conflict_option_t *option,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  svn_ra_session_t *ra_session;
  const char *url;
  const char *corrected_url;
  const char *repos_root_url;
  const char *wc_tmpdir;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  const char *local_abspath;
  const char *lock_abspath;
  svn_wc_merge_outcome_t merge_content_outcome;
  svn_wc_notify_state_t merge_props_outcome;
  apr_file_t *incoming_new_file;
  const char *incoming_new_tmp_abspath;
  const char *empty_file_abspath;
  svn_stream_t *incoming_new_stream;
  apr_hash_t *incoming_new_props;
  apr_array_header_t *propdiffs;
  svn_error_t *err;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  /* Set up temporary storage for the repository version of file. */
  SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath,
                             scratch_pool, scratch_pool));
  SVN_ERR(svn_io_open_unique_file3(&incoming_new_file,
                                   &incoming_new_tmp_abspath, wc_tmpdir,
                                   svn_io_file_del_on_pool_cleanup,
                                   scratch_pool, scratch_pool));
  incoming_new_stream = svn_stream_from_aprfile2(incoming_new_file, TRUE,
                                                 scratch_pool);

  /* Fetch the incoming added file from the repository. */
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath,
                                    scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               url, NULL, NULL, FALSE, FALSE,
                                               ctx, scratch_pool,
                                               scratch_pool));
  SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev,
                          incoming_new_stream, NULL, /* fetched_rev */
                          &incoming_new_props, scratch_pool));

  /* Flush file to disk. */
  SVN_ERR(svn_stream_close(incoming_new_stream));
  SVN_ERR(svn_io_file_flush(incoming_new_file, scratch_pool));

  filter_props(incoming_new_props, scratch_pool);

  /* Create an empty file as fake "merge-base" for the two added files.
   * The files are not ancestrally related so this is the best we can do. */
  SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file_abspath, NULL,
                                   svn_io_file_del_on_pool_cleanup,
                                   scratch_pool, scratch_pool));

  /* Create a property diff which shows all props as added. */
  SVN_ERR(svn_prop_diffs(&propdiffs, incoming_new_props,
                         apr_hash_make(scratch_pool), scratch_pool));

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));
  /* Resolve to current working copy state. svn_wc_merge5() requires this. */
  err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool);
  if (err)
    return svn_error_compose_create(err,
                                    svn_wc__release_write_lock(ctx->wc_ctx,
                                                               lock_abspath,
                                                               scratch_pool));
  /* Perform the file merge. ### Merge into tempfile and then rename on top? */
  err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome,
                      ctx->wc_ctx, empty_file_abspath,
                      incoming_new_tmp_abspath, local_abspath,
                      NULL, NULL, NULL, /* labels */
                      NULL, NULL, /* conflict versions */
                      FALSE, /* dry run */
                      NULL, NULL, /* diff3_cmd, merge_options */
                      NULL, propdiffs,
                      NULL, NULL, /* conflict func/baton */
                      NULL, NULL, /* don't allow user to cancel here */
                      scratch_pool);
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  svn_io_sleep_for_timestamps(local_abspath, scratch_pool);
  SVN_ERR(err);

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      /* Tell the world about the file merge that just happened. */
      notify = svn_wc_create_notify(local_abspath,
                                    svn_wc_notify_update_update,
                                    scratch_pool);
      if (merge_content_outcome == svn_wc_merge_conflict)
        notify->content_state = svn_wc_notify_state_conflicted;
      else
        notify->content_state = svn_wc_notify_state_merged;
      notify->prop_state = merge_props_outcome;
      notify->kind = svn_node_file;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);

      /* And also about the successfully resolved tree conflict. */
      notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_merge_incoming_added_file_replace_and_merge(
  svn_client_conflict_option_t *option,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  svn_ra_session_t *ra_session;
  const char *url;
  const char *corrected_url;
  const char *repos_root_url;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  apr_file_t *incoming_new_file;
  svn_stream_t *incoming_new_stream;
  apr_hash_t *incoming_new_props;
  const char *local_abspath;
  const char *lock_abspath;
  const char *wc_tmpdir;
  svn_stream_t *working_file_tmp_stream;
  const char *working_file_tmp_abspath;
  svn_stream_t *working_file_stream;
  apr_hash_t *working_props;
  svn_error_t *err;
  svn_wc_merge_outcome_t merge_content_outcome;
  svn_wc_notify_state_t merge_props_outcome;
  apr_file_t *empty_file;
  const char *empty_file_abspath;
  apr_array_header_t *propdiffs;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  /* Set up tempory storage for the working version of file. */
  SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath,
                             scratch_pool, scratch_pool));
  SVN_ERR(svn_stream_open_unique(&working_file_tmp_stream,
                                 &working_file_tmp_abspath, wc_tmpdir,
                                 svn_io_file_del_on_pool_cleanup,
                                 scratch_pool, scratch_pool));

  /* Copy the detranslated working file to temporary storage. */
  SVN_ERR(svn_wc__translated_stream(&working_file_stream, ctx->wc_ctx,
                                    local_abspath, local_abspath,
                                    SVN_WC_TRANSLATE_TO_NF,
                                    scratch_pool, scratch_pool));
  SVN_ERR(svn_stream_copy3(working_file_stream, working_file_tmp_stream,
                           ctx->cancel_func, ctx->cancel_baton,
                           scratch_pool));

  /* Get a copy of the working file's properties. */
  SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath,
                            scratch_pool, scratch_pool));

  /* Fetch the incoming added file from the repository. */
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath,
                                    scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               url, NULL, NULL, FALSE, FALSE,
                                               ctx, scratch_pool,
                                               scratch_pool));
  if (corrected_url)
    url = corrected_url;
  SVN_ERR(svn_io_open_unique_file3(&incoming_new_file, NULL, wc_tmpdir,
                                   svn_io_file_del_on_pool_cleanup,
                                   scratch_pool, scratch_pool));
  incoming_new_stream = svn_stream_from_aprfile2(incoming_new_file, TRUE,
                                                 scratch_pool);
  SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev,
                          incoming_new_stream, NULL, /* fetched_rev */
                          &incoming_new_props, scratch_pool));
  /* Flush file to disk. */
  SVN_ERR(svn_io_file_flush(incoming_new_file, scratch_pool));

  /* Reset the stream in preparation for adding its content to WC. */
  SVN_ERR(svn_stream_reset(incoming_new_stream));

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));

  /* ### The following WC modifications should be atomic. */

  /* Replace the working file with the file from the repository. */
  err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE,
                       NULL, NULL, /* don't allow user to cancel here */
                       ctx->notify_func2, ctx->notify_baton2,
                       scratch_pool);
  if (err)
    goto unlock_wc;
  err = svn_wc_add_repos_file4(ctx->wc_ctx, local_abspath,
                               incoming_new_stream,
                               NULL, /* ### could we merge first, then set
                                        ### the merged content here? */
                               incoming_new_props,
                               NULL, /* ### merge props first, set here? */
                               url, incoming_new_pegrev,
                               NULL, NULL, /* don't allow user to cancel here */
                               scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify = svn_wc_create_notify(local_abspath,
                                                     svn_wc_notify_add,
                                                     scratch_pool);
      notify->kind = svn_node_file;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  /* Resolve to current working copy state. svn_wc_merge5() requires this. */
  err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Create an empty file as fake "merge-base" for the two added files.
   * The files are not ancestrally related so this is the best we can do. */
  err = svn_io_open_unique_file3(&empty_file, &empty_file_abspath, NULL,
                                 svn_io_file_del_on_pool_cleanup,
                                 scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  filter_props(incoming_new_props, scratch_pool);

  /* Create a property diff for the files. */
  err = svn_prop_diffs(&propdiffs, incoming_new_props,
                       working_props, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Perform the file merge. */
  err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome,
                      ctx->wc_ctx, empty_file_abspath,
                      working_file_tmp_abspath, local_abspath,
                      NULL, NULL, NULL, /* labels */
                      NULL, NULL, /* conflict versions */
                      FALSE, /* dry run */
                      NULL, NULL, /* diff3_cmd, merge_options */
                      NULL, propdiffs,
                      NULL, NULL, /* conflict func/baton */
                      NULL, NULL, /* don't allow user to cancel here */
                      scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify = svn_wc_create_notify(
                                   local_abspath,
                                   svn_wc_notify_update_update,
                                   scratch_pool);

      if (merge_content_outcome == svn_wc_merge_conflict)
        notify->content_state = svn_wc_notify_state_conflicted;
      else
        notify->content_state = svn_wc_notify_state_merged;
      notify->prop_state = merge_props_outcome;
      notify->kind = svn_node_file;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  svn_io_sleep_for_timestamps(local_abspath, scratch_pool);
  SVN_ERR(err);

  SVN_ERR(svn_stream_close(incoming_new_stream));

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify = svn_wc_create_notify(
                                  local_abspath,
                                  svn_wc_notify_resolved_tree,
                                  scratch_pool);

      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  return SVN_NO_ERROR;
}

static svn_error_t *
raise_tree_conflict(const char *local_abspath,
                    svn_wc_conflict_action_t incoming_change,
                    svn_wc_conflict_reason_t local_change,
                    svn_node_kind_t local_node_kind,
                    svn_node_kind_t merge_left_kind,
                    svn_node_kind_t merge_right_kind,
                    const char *repos_root_url,
                    const char *repos_uuid,
                    const char *repos_relpath,
                    svn_revnum_t merge_left_rev,
                    svn_revnum_t merge_right_rev,
                    svn_wc_context_t *wc_ctx,
                    svn_wc_notify_func2_t notify_func2,
                    void *notify_baton2,
                    apr_pool_t *scratch_pool)
{
  svn_wc_conflict_description2_t *conflict;
  const svn_wc_conflict_version_t *left_version;
  const svn_wc_conflict_version_t *right_version;

  left_version = svn_wc_conflict_version_create2(repos_root_url,
                                                 repos_uuid,
                                                 repos_relpath,
                                                 merge_left_rev,
                                                 merge_left_kind,
                                                 scratch_pool);
  right_version = svn_wc_conflict_version_create2(repos_root_url,
                                                  repos_uuid,
                                                  repos_relpath,
                                                  merge_right_rev,
                                                  merge_right_kind,
                                                  scratch_pool);
  conflict = svn_wc_conflict_description_create_tree2(local_abspath,
                                                      local_node_kind,
                                                      svn_wc_operation_merge,
                                                      left_version,
                                                      right_version,
                                                      scratch_pool);
  conflict->action = incoming_change;
  conflict->reason = local_change;

  SVN_ERR(svn_wc__add_tree_conflict(wc_ctx, conflict, scratch_pool));

  if (notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(local_abspath, svn_wc_notify_tree_conflict,
                                    scratch_pool);
      notify->kind = local_node_kind;
      notify_func2(notify_baton2, notify, scratch_pool);
    }

  return SVN_NO_ERROR;
}

struct merge_newly_added_dir_baton {
  const char *target_abspath;
  svn_client_ctx_t *ctx;
  const char *repos_root_url;
  const char *repos_uuid;
  const char *added_repos_relpath;
  svn_revnum_t merge_left_rev;
  svn_revnum_t merge_right_rev;
};

static svn_error_t *
merge_added_dir_props(const char *target_abspath,
                      const char *added_repos_relpath,
                      apr_hash_t *added_props,
                      const char *repos_root_url,
                      const char *repos_uuid,
                      svn_revnum_t merge_left_rev,
                      svn_revnum_t merge_right_rev,
                      svn_client_ctx_t *ctx,
                      apr_pool_t *scratch_pool)
{
  svn_wc_notify_state_t property_state;
  apr_array_header_t *propchanges;
  const svn_wc_conflict_version_t *left_version;
  const svn_wc_conflict_version_t *right_version;
  apr_hash_index_t *hi;

  left_version = svn_wc_conflict_version_create2(
                   repos_root_url, repos_uuid, added_repos_relpath,
                   merge_left_rev, svn_node_none, scratch_pool);

  right_version = svn_wc_conflict_version_create2(
                    repos_root_url, repos_uuid, added_repos_relpath,
                    merge_right_rev, svn_node_dir, scratch_pool);

  propchanges = apr_array_make(scratch_pool, apr_hash_count(added_props),
                               sizeof(svn_prop_t));
  for (hi = apr_hash_first(scratch_pool, added_props);
       hi;
       hi = apr_hash_next(hi))
    {
      svn_prop_t prop;

      prop.name = apr_hash_this_key(hi);
      prop.value = apr_hash_this_val(hi);

      if (svn_wc_is_normal_prop(prop.name))
        APR_ARRAY_PUSH(propchanges, svn_prop_t) = prop;
    }

  SVN_ERR(svn_wc_merge_props3(&property_state, ctx->wc_ctx,
                              target_abspath,
                              left_version, right_version,
                              apr_hash_make(scratch_pool),
                              propchanges,
                              FALSE, /* not a dry-run */
                              NULL, NULL, NULL, NULL,
                              scratch_pool));

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(target_abspath,
                                    svn_wc_notify_update_update,
                                    scratch_pool);
      notify->kind = svn_node_dir;
      notify->content_state = svn_wc_notify_state_unchanged;;
      notify->prop_state = property_state;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  return SVN_NO_ERROR;
}

/* An svn_diff_tree_processor_t callback. */
static svn_error_t *
diff_dir_added(const char *relpath,
               const svn_diff_source_t *copyfrom_source,
               const svn_diff_source_t *right_source,
               apr_hash_t *copyfrom_props,
               apr_hash_t *right_props,
               void *dir_baton,
               const struct svn_diff_tree_processor_t *processor,
               apr_pool_t *scratch_pool)
{
  struct merge_newly_added_dir_baton *b = processor->baton;
  const char *local_abspath;
  const char *copyfrom_url;
  svn_node_kind_t db_kind;
  svn_node_kind_t on_disk_kind;
  apr_hash_index_t *hi;

  /* Handle the root of the added directory tree. */
  if (relpath[0] == '\0')
    {
      /* ### svn_wc_merge_props3() requires this... */
      SVN_ERR(svn_wc__del_tree_conflict(b->ctx->wc_ctx, b->target_abspath,
                                        scratch_pool));
      SVN_ERR(merge_added_dir_props(b->target_abspath,
                                    b->added_repos_relpath, right_props,
                                    b->repos_root_url, b->repos_uuid,
                                    b->merge_left_rev, b->merge_right_rev,
                                    b->ctx, scratch_pool));
      return SVN_NO_ERROR;

    }

  local_abspath = svn_dirent_join(b->target_abspath, relpath, scratch_pool);

  SVN_ERR(svn_wc_read_kind2(&db_kind, b->ctx->wc_ctx, local_abspath,
                            FALSE, FALSE, scratch_pool));
  SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool));

  if (db_kind == svn_node_dir && on_disk_kind == svn_node_dir)
    {
      SVN_ERR(merge_added_dir_props(svn_dirent_join(b->target_abspath, relpath,
                                                    scratch_pool),
                                    b->added_repos_relpath, right_props,
                                    b->repos_root_url, b->repos_uuid,
                                    b->merge_left_rev, b->merge_right_rev,
                                    b->ctx, scratch_pool));
      return SVN_NO_ERROR;
    }

  if (db_kind != svn_node_none && db_kind != svn_node_unknown)
    {
      SVN_ERR(raise_tree_conflict(
                local_abspath, svn_wc_conflict_action_add,
                svn_wc_conflict_reason_obstructed,
                db_kind, svn_node_none, svn_node_dir,
                b->repos_root_url, b->repos_uuid,
                svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool),
                b->merge_left_rev, b->merge_right_rev,
                b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2,
                scratch_pool));
      return SVN_NO_ERROR;
    }

  if (on_disk_kind != svn_node_none)
    {
      SVN_ERR(raise_tree_conflict(
                local_abspath, svn_wc_conflict_action_add,
                svn_wc_conflict_reason_obstructed, db_kind,
                svn_node_none, svn_node_dir, b->repos_root_url, b->repos_uuid,
                svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool),
                b->merge_left_rev, b->merge_right_rev,
                b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2,
                scratch_pool));
      return SVN_NO_ERROR;
    }

  SVN_ERR(svn_io_dir_make(local_abspath, APR_OS_DEFAULT, scratch_pool));
  copyfrom_url = apr_pstrcat(scratch_pool, b->repos_root_url, "/",
                             right_source->repos_relpath, SVN_VA_NULL);
  SVN_ERR(svn_wc_add4(b->ctx->wc_ctx, local_abspath, svn_depth_infinity,
                      copyfrom_url, right_source->revision,
                      NULL, NULL, /* cancel func/baton */
                      b->ctx->notify_func2, b->ctx->notify_baton2,
                      scratch_pool));

  for (hi = apr_hash_first(scratch_pool, right_props);
       hi;
       hi = apr_hash_next(hi))
    {
      const char *propname = apr_hash_this_key(hi);
      const svn_string_t *propval = apr_hash_this_val(hi);

      SVN_ERR(svn_wc_prop_set4(b->ctx->wc_ctx, local_abspath,
                               propname, propval, svn_depth_empty,
                               FALSE, NULL /* do not skip checks */,
                               NULL, NULL, /* cancel func/baton */
                               b->ctx->notify_func2, b->ctx->notify_baton2,
                               scratch_pool));
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
merge_added_files(const char *local_abspath,
                  const char *incoming_added_file_abspath,
                  apr_hash_t *incoming_added_file_props,
                  svn_client_ctx_t *ctx,
                  apr_pool_t *scratch_pool)
{
  svn_wc_merge_outcome_t merge_content_outcome;
  svn_wc_notify_state_t merge_props_outcome;
  apr_file_t *empty_file;
  const char *empty_file_abspath;
  apr_array_header_t *propdiffs;
  apr_hash_t *working_props;

  /* Create an empty file as fake "merge-base" for the two added files.
   * The files are not ancestrally related so this is the best we can do. */
  SVN_ERR(svn_io_open_unique_file3(&empty_file, &empty_file_abspath, NULL,
                                   svn_io_file_del_on_pool_cleanup,
                                   scratch_pool, scratch_pool));

  /* Get a copy of the working file's properties. */
  SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath,
                            scratch_pool, scratch_pool));

  /* Create a property diff for the files. */
  SVN_ERR(svn_prop_diffs(&propdiffs, incoming_added_file_props,
                         working_props, scratch_pool));

  /* Perform the file merge. */
  SVN_ERR(svn_wc_merge5(&merge_content_outcome, &merge_props_outcome,
                        ctx->wc_ctx, empty_file_abspath,
                        incoming_added_file_abspath, local_abspath,
                        NULL, NULL, NULL, /* labels */
                        NULL, NULL, /* conflict versions */
                        FALSE, /* dry run */
                        NULL, NULL, /* diff3_cmd, merge_options */
                        NULL, propdiffs,
                        NULL, NULL, /* conflict func/baton */
                        NULL, NULL, /* don't allow user to cancel here */
                        scratch_pool));

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify = svn_wc_create_notify(
                                   local_abspath,
                                   svn_wc_notify_update_update,
                                   scratch_pool);

      if (merge_content_outcome == svn_wc_merge_conflict)
        notify->content_state = svn_wc_notify_state_conflicted;
      else
        notify->content_state = svn_wc_notify_state_merged;
      notify->prop_state = merge_props_outcome;
      notify->kind = svn_node_file;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  return SVN_NO_ERROR;
}

/* An svn_diff_tree_processor_t callback. */
static svn_error_t *
diff_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,
                apr_hash_t *copyfrom_props,
                apr_hash_t *right_props,
                void *file_baton,
                const struct svn_diff_tree_processor_t *processor,
                apr_pool_t *scratch_pool)
{
  struct merge_newly_added_dir_baton *b = processor->baton;
  const char *local_abspath;
  svn_node_kind_t db_kind;
  svn_node_kind_t on_disk_kind;
  apr_array_header_t *propsarray;
  apr_array_header_t *regular_props;

  local_abspath = svn_dirent_join(b->target_abspath, relpath, scratch_pool);

  SVN_ERR(svn_wc_read_kind2(&db_kind, b->ctx->wc_ctx, local_abspath,
                            FALSE, FALSE, scratch_pool));
  SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool));

  if (db_kind == svn_node_file && on_disk_kind == svn_node_file)
    {
      propsarray = svn_prop_hash_to_array(right_props, scratch_pool);
      SVN_ERR(svn_categorize_props(propsarray, NULL, NULL, &regular_props,
                                   scratch_pool));
      SVN_ERR(merge_added_files(local_abspath, right_file,
                                svn_prop_array_to_hash(regular_props,
                                                       scratch_pool),
                                b->ctx, scratch_pool));
      return SVN_NO_ERROR;
    }

  if (db_kind != svn_node_none && db_kind != svn_node_unknown)
    {
      SVN_ERR(raise_tree_conflict(
                local_abspath, svn_wc_conflict_action_add,
                svn_wc_conflict_reason_obstructed,
                db_kind, svn_node_none, svn_node_file,
                b->repos_root_url, b->repos_uuid,
                svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool),
                b->merge_left_rev, b->merge_right_rev,
                b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2,
                scratch_pool));
      return SVN_NO_ERROR;
    }

  if (on_disk_kind != svn_node_none)
    {
      SVN_ERR(raise_tree_conflict(
                local_abspath, svn_wc_conflict_action_add,
                svn_wc_conflict_reason_obstructed, db_kind,
                svn_node_none, svn_node_file, b->repos_root_url, b->repos_uuid,
                svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool),
                b->merge_left_rev, b->merge_right_rev,
                b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2,
                scratch_pool));
      return SVN_NO_ERROR;
    }

  propsarray = svn_prop_hash_to_array(right_props, scratch_pool);
  SVN_ERR(svn_categorize_props(propsarray, NULL, NULL, &regular_props,
                               scratch_pool));
  SVN_ERR(svn_io_copy_file(right_file, local_abspath, FALSE, scratch_pool));
  SVN_ERR(svn_wc_add_from_disk3(b->ctx->wc_ctx, local_abspath,
                                svn_prop_array_to_hash(regular_props,
                                                       scratch_pool),
                                FALSE, b->ctx->notify_func2,
                                b->ctx->notify_baton2, scratch_pool));

  return SVN_NO_ERROR;
}

/* Merge a newly added directory into TARGET_ABSPATH in the working copy.
 *
 * This uses a diff-tree processor because our standard merge operation
 * is not set up for merges where the merge-source anchor is itself an
 * added directory (i.e. does not exist on one side of the diff).
 * The standard merge will only merge additions of children of a path
 * that exists across the entire revision range being merged.
 * But in our case, SOURCE1 does not yet exist in REV1, but SOURCE2
 * does exist in REV2. Thus we use a diff processor.
 */
static svn_error_t *
merge_newly_added_dir(const char *added_repos_relpath,
                      const char *source1,
                      svn_revnum_t rev1,
                      const char *source2,
                      svn_revnum_t rev2,
                      const char *target_abspath,
                      svn_boolean_t reverse_merge,
                      svn_client_ctx_t *ctx,
                      apr_pool_t *result_pool,
                      apr_pool_t *scratch_pool)
{
  svn_diff_tree_processor_t *processor;
  struct merge_newly_added_dir_baton baton = { 0 };
  const svn_diff_tree_processor_t *diff_processor;
  svn_ra_session_t *ra_session;
  const char *corrected_url;
  svn_ra_session_t *extra_ra_session;
  const svn_ra_reporter3_t *reporter;
  void *reporter_baton;
  const svn_delta_editor_t *diff_editor;
  void *diff_edit_baton;
  const char *anchor1;
  const char *anchor2;
  const char *target1;
  const char *target2;

  svn_uri_split(&anchor1, &target1, source1, scratch_pool);
  svn_uri_split(&anchor2, &target2, source2, scratch_pool);

  baton.target_abspath = target_abspath;
  baton.ctx = ctx;
  baton.added_repos_relpath = added_repos_relpath;
  SVN_ERR(svn_wc__node_get_repos_info(NULL, NULL,
                                      &baton.repos_root_url, &baton.repos_uuid,
                                      ctx->wc_ctx, target_abspath,
                                      scratch_pool, scratch_pool));
  baton.merge_left_rev = rev1;
  baton.merge_right_rev = rev2;

  processor = svn_diff__tree_processor_create(&baton, scratch_pool);
  processor->dir_added = diff_dir_added;
  processor->file_added = diff_file_added;

  diff_processor = processor;
  if (reverse_merge)
    diff_processor = svn_diff__tree_processor_reverse_create(diff_processor,
                                                             scratch_pool);

  /* Filter the first path component using a filter processor, until we fixed
     the diff processing to handle this directly */
  diff_processor = svn_diff__tree_processor_filter_create(
                     diff_processor, target1, scratch_pool);

  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               anchor2, NULL, NULL, FALSE,
                                               FALSE, ctx,
                                               scratch_pool, scratch_pool));
  if (corrected_url)
    anchor2 = corrected_url;

  /* Extra RA session is used during the editor calls to fetch file contents. */
  SVN_ERR(svn_ra__dup_session(&extra_ra_session, ra_session, anchor2,
                              scratch_pool, scratch_pool));

  /* Create a repos-repos diff editor. */
  SVN_ERR(svn_client__get_diff_editor2(
                &diff_editor, &diff_edit_baton,
                extra_ra_session, svn_depth_infinity, rev1, TRUE,
                diff_processor, ctx->cancel_func, ctx->cancel_baton,
                scratch_pool));

  /* We want to switch our txn into URL2 */
  SVN_ERR(svn_ra_do_diff3(ra_session, &reporter, &reporter_baton,
                          rev2, target1, svn_depth_infinity, TRUE, TRUE,
                          source2, diff_editor, diff_edit_baton, scratch_pool));

  /* Drive the reporter; do the diff. */
  SVN_ERR(reporter->set_path(reporter_baton, "", rev1,
                             svn_depth_infinity,
                             FALSE, NULL,
                             scratch_pool));

  SVN_ERR(reporter->finish_report(reporter_baton, scratch_pool));

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_merge_incoming_added_dir_merge(svn_client_conflict_option_t *option,
                                       svn_client_conflict_t *conflict,
                                       svn_client_ctx_t *ctx,
                                       apr_pool_t *scratch_pool)
{
  const char *repos_root_url;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  const char *local_abspath;
  const char *lock_abspath;
  struct conflict_tree_incoming_add_details *details;
  const char *added_repos_relpath;
  const char *source1;
  svn_revnum_t rev1;
  const char *source2;
  svn_revnum_t rev2;
  svn_error_t *err;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  details = conflict->tree_conflict_incoming_details;
  if (details == NULL)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("Conflict resolution option '%d' requires "
                               "details for tree conflict at '%s' to be "
                               "fetched from the repository"),
                            option->id,
                            svn_dirent_local_style(local_abspath,
                                                   scratch_pool));

  /* Set up merge sources to merge the entire incoming added directory tree. */
  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  source1 = svn_path_url_add_component2(repos_root_url,
                                        details->repos_relpath,
                                        scratch_pool);
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            NULL, conflict, scratch_pool, scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool, scratch_pool));
  if (incoming_old_pegrev < incoming_new_pegrev) /* forward merge */
    {
      if (details->added_rev == SVN_INVALID_REVNUM)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Could not determine when '%s' was "
                                   "added the repository"),
                                 svn_dirent_local_style(local_abspath,
                                                        scratch_pool));
      rev1 = rev_below(details->added_rev);
      source2 = svn_path_url_add_component2(repos_root_url,
                                            incoming_new_repos_relpath,
                                            scratch_pool);
      rev2 = incoming_new_pegrev;
      added_repos_relpath = incoming_new_repos_relpath;
    }
  else /* reverse-merge */
    {
      if (details->deleted_rev == SVN_INVALID_REVNUM)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Could not determine when '%s' was "
                                   "deleted from the repository"),
                                 svn_dirent_local_style(local_abspath,
                                                        scratch_pool));
      rev1 = details->deleted_rev;
      source2 = svn_path_url_add_component2(repos_root_url,
                                            incoming_old_repos_relpath,
                                            scratch_pool);
      rev2 = incoming_old_pegrev;
      added_repos_relpath = incoming_new_repos_relpath;
    }

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));

  /* ### wrap in a transaction */
  err = merge_newly_added_dir(added_repos_relpath,
                              source1, rev1, source2, rev2,
                              local_abspath,
                              (incoming_old_pegrev > incoming_new_pegrev),
                              ctx, scratch_pool, scratch_pool);
  if (!err)
    err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool);

  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  svn_io_sleep_for_timestamps(local_abspath, scratch_pool);
  SVN_ERR(err);

  if (ctx->notify_func2)
    ctx->notify_func2(ctx->notify_baton2,
                      svn_wc_create_notify(local_abspath,
                                           svn_wc_notify_resolved_tree,
                                           scratch_pool),
                      scratch_pool);

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_update_incoming_added_dir_merge(svn_client_conflict_option_t *option,
                                       svn_client_conflict_t *conflict,
                                       svn_client_ctx_t *ctx,
                                       apr_pool_t *scratch_pool)
{
  const char *local_abspath;
  const char *lock_abspath;
  svn_error_t *err;
  svn_wc_conflict_reason_t local_change;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);

  if (local_change == svn_wc_conflict_reason_unversioned)
    {
      char *parent_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
      SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
                &lock_abspath, ctx->wc_ctx, parent_abspath,
                scratch_pool, scratch_pool));

      /* The update/switch operation has added the incoming versioned
       * directory as a deleted op-depth layer. We can revert this layer
       * to make the incoming tree appear in the working copy.
       * This meta-data-only revert operation effecively merges the
       * versioned and unversioned trees but leaves all unversioned files as
       * they were. This is the best we can do; 3-way merging of unversioned
       * files with files from the repository is impossible because there is
       * no known merge base. No unversioned data will be lost, and any
       * differences to files in the repository will show up in 'svn diff'. */
      err = svn_wc_revert6(ctx->wc_ctx, local_abspath, svn_depth_infinity,
                           FALSE, NULL, TRUE, TRUE /* metadata_only */,
                           TRUE /*added_keep_local*/,
                           NULL, NULL, /* no cancellation */
                           ctx->notify_func2, ctx->notify_baton2,
                           scratch_pool);
    }
  else
    {
      SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
                &lock_abspath, ctx->wc_ctx, local_abspath,
                scratch_pool, scratch_pool));
      err = svn_wc__conflict_tree_update_local_add(ctx->wc_ctx,
                                                   local_abspath,
                                                   ctx->cancel_func,
                                                   ctx->cancel_baton,
                                                   ctx->notify_func2,
                                                   ctx->notify_baton2,
                                                   scratch_pool);
    }

  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  return SVN_NO_ERROR;
}

/* Resolve a dir/dir "incoming add vs local obstruction" tree conflict by
 * replacing the local directory with the incoming directory.
 * If MERGE_DIRS is set, also merge the directories after replacing. */
static svn_error_t *
merge_incoming_added_dir_replace(svn_client_conflict_option_t *option,
                                 svn_client_conflict_t *conflict,
                                 svn_client_ctx_t *ctx,
                                 svn_boolean_t merge_dirs,
                                 apr_pool_t *scratch_pool)
{
  svn_ra_session_t *ra_session;
  const char *url;
  const char *corrected_url;
  const char *repos_root_url;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  const char *local_abspath;
  const char *lock_abspath;
  svn_error_t *err;
  svn_boolean_t timestamp_sleep;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  /* Find the URL of the incoming added directory in the repository. */
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath,
                                    scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               url, NULL, NULL, FALSE, FALSE,
                                               ctx, scratch_pool,
                                               scratch_pool));
  if (corrected_url)
    url = corrected_url;

  /* ### The following WC modifications should be atomic. */

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 svn_dirent_dirname(
                                                   local_abspath,
                                                   scratch_pool),
                                                 scratch_pool, scratch_pool));

  /* Remove the working directory. */
  err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE,
                       NULL, NULL, /* don't allow user to cancel here */
                       ctx->notify_func2, ctx->notify_baton2,
                       scratch_pool);
  if (err)
    goto unlock_wc;

  err = svn_client__repos_to_wc_copy_by_editor(&timestamp_sleep,
                                               svn_node_dir,
                                               url, incoming_new_pegrev,
                                               local_abspath,
                                               ra_session, ctx, scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify = svn_wc_create_notify(local_abspath,
                                                     svn_wc_notify_add,
                                                     scratch_pool);
      notify->kind = svn_node_dir;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  /* Resolve to current working copy state.
   * svn_client__merge_locked() requires this. */
  err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool);
  if (err)
    goto unlock_wc;

  if (merge_dirs)
    {
      svn_revnum_t base_revision;
      const char *base_repos_relpath;
      struct find_added_rev_baton b = { 0 };

      /* Find the URL and revision of the directory we have just replaced. */
      err = svn_wc__node_get_base(NULL, &base_revision, &base_repos_relpath,
                                  NULL, NULL, NULL, ctx->wc_ctx, local_abspath,
                                  FALSE, scratch_pool, scratch_pool);
      if (err)
        goto unlock_wc;

      url = svn_path_url_add_component2(repos_root_url, base_repos_relpath,
                                        scratch_pool);

      /* Trace the replaced directory's history to its origin. */
      err = svn_ra_reparent(ra_session, url, scratch_pool);
      if (err)
        goto unlock_wc;
      b.victim_abspath = local_abspath;
      b.ctx = ctx;
      b.added_rev = SVN_INVALID_REVNUM;
      b.repos_relpath = NULL;
      b.parent_repos_relpath = svn_relpath_dirname(base_repos_relpath,
                                                   scratch_pool);
      b.pool = scratch_pool;

      err = svn_ra_get_location_segments(ra_session, "", base_revision,
                                         base_revision, SVN_INVALID_REVNUM,
                                         find_added_rev, &b,
                                         scratch_pool);
      if (err)
        goto unlock_wc;

      if (b.added_rev == SVN_INVALID_REVNUM)
        {
          err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                  _("Could not determine the revision in "
                                    "which '^/%s' was added to the "
                                    "repository.\n"),
                                  base_repos_relpath);
          goto unlock_wc;
        }

      /* Merge the replaced directory into the directory which replaced it.
       * We do not need to consider a reverse-merge here since the source of
       * this merge was part of the merge target working copy, not a branch
       * in the repository. */
      err = merge_newly_added_dir(base_repos_relpath,
                                  url, rev_below(b.added_rev), url,
                                  base_revision, local_abspath, FALSE,
                                  ctx, scratch_pool, scratch_pool);
      if (err)
        goto unlock_wc;
    }

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  svn_io_sleep_for_timestamps(local_abspath, scratch_pool);
  SVN_ERR(err);

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify = svn_wc_create_notify(
                                  local_abspath,
                                  svn_wc_notify_resolved_tree,
                                  scratch_pool);

      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_merge_incoming_added_dir_replace(svn_client_conflict_option_t *option,
                                         svn_client_conflict_t *conflict,
                                         svn_client_ctx_t *ctx,
                                         apr_pool_t *scratch_pool)
{
  return svn_error_trace(merge_incoming_added_dir_replace(option,
                                                          conflict,
                                                          ctx,
                                                          FALSE,
                                                          scratch_pool));
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_merge_incoming_added_dir_replace_and_merge(
  svn_client_conflict_option_t *option,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  return svn_error_trace(merge_incoming_added_dir_replace(option,
                                                          conflict,
                                                          ctx,
                                                          TRUE,
                                                          scratch_pool));
}

/* Ensure the conflict victim is a copy of itself from before it was deleted.
 * Update and switch are supposed to set this up when flagging the conflict. */
static svn_error_t *
ensure_local_edit_vs_incoming_deletion_copied_state(
  struct conflict_tree_incoming_delete_details *details,
  svn_wc_operation_t operation,
  const char *wcroot_abspath,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{

  svn_boolean_t is_copy;
  svn_revnum_t copyfrom_rev;
  const char *copyfrom_repos_relpath;

  SVN_ERR_ASSERT(operation == svn_wc_operation_update ||
                 operation == svn_wc_operation_switch);

  SVN_ERR(svn_wc__node_get_origin(&is_copy, &copyfrom_rev,
                                  &copyfrom_repos_relpath,
                                  NULL, NULL, NULL, NULL,
                                  ctx->wc_ctx, conflict->local_abspath,
                                  FALSE, scratch_pool, scratch_pool));
  if (!is_copy)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("Cannot resolve tree conflict on '%s' "
                               "(expected a copied item, but the item "
                               "is not a copy)"),
                             svn_dirent_local_style(
                               svn_dirent_skip_ancestor(
                                 wcroot_abspath,
                                 conflict->local_abspath),
                             scratch_pool));
  else if (details->deleted_rev != SVN_INVALID_REVNUM &&
           copyfrom_rev >= details->deleted_rev)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("Cannot resolve tree conflict on '%s' "
                               "(expected an item copied from a revision "
                               "smaller than r%ld, but the item was "
                               "copied from r%ld)"),
                             svn_dirent_local_style(
                               svn_dirent_skip_ancestor(
                                 wcroot_abspath, conflict->local_abspath),
                               scratch_pool),
                             details->deleted_rev, copyfrom_rev);
  else if (details->added_rev != SVN_INVALID_REVNUM &&
           copyfrom_rev < details->added_rev)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("Cannot resolve tree conflict on '%s' "
                               "(expected an item copied from a revision "
                               "larger than r%ld, but the item was "
                               "copied from r%ld)"),
                             svn_dirent_local_style(
                               svn_dirent_skip_ancestor(
                                 wcroot_abspath, conflict->local_abspath),
                               scratch_pool),
                              details->added_rev, copyfrom_rev);
  else if (operation == svn_wc_operation_update)
    {
      const char *old_repos_relpath;

      SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
                &old_repos_relpath, NULL, NULL, conflict,
                scratch_pool, scratch_pool));
      if (strcmp(copyfrom_repos_relpath, details->repos_relpath) != 0 &&
          strcmp(copyfrom_repos_relpath, old_repos_relpath) != 0)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected an item copied from '^/%s' "
                                   "or from '^/%s' but the item was "
                                   "copied from '^/%s@%ld')"),
                                 svn_dirent_local_style(
                                   svn_dirent_skip_ancestor(
                                     wcroot_abspath, conflict->local_abspath),
                                   scratch_pool),
                                 details->repos_relpath,
                                 old_repos_relpath,
                                 copyfrom_repos_relpath, copyfrom_rev);
    }
  else if (operation == svn_wc_operation_switch)
    {
      const char *old_repos_relpath;

      SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
                &old_repos_relpath, NULL, NULL, conflict,
                scratch_pool, scratch_pool));

      if (strcmp(copyfrom_repos_relpath, old_repos_relpath) != 0)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected an item copied from '^/%s', "
                                   "but the item was copied from "
                                    "'^/%s@%ld')"),
                                 svn_dirent_local_style(
                                   svn_dirent_skip_ancestor(
                                     wcroot_abspath,
                                     conflict->local_abspath),
                                   scratch_pool),
                                 old_repos_relpath,
                                 copyfrom_repos_relpath, copyfrom_rev);
    }

  return SVN_NO_ERROR;
}

/* Verify the local working copy state matches what we expect when an
 * incoming deletion tree conflict exists.
 * We assume update/merge/switch operations leave the working copy in a
 * state which prefers the local change and cancels the deletion.
 * Run a quick sanity check and error out if it looks as if the
 * working copy was modified since, even though it's not easy to make
 * such modifications without also clearing the conflict marker. */
static svn_error_t *
verify_local_state_for_incoming_delete(svn_client_conflict_t *conflict,
                                       svn_client_conflict_option_t *option,
                                       svn_client_ctx_t *ctx,
                                       apr_pool_t *scratch_pool)
{
  const char *local_abspath;
  const char *wcroot_abspath;
  svn_wc_operation_t operation;
  svn_wc_conflict_reason_t local_change;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);
  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                             local_abspath, scratch_pool,
                             scratch_pool));
  operation = svn_client_conflict_get_operation(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);

  if (operation == svn_wc_operation_update ||
      operation == svn_wc_operation_switch)
    {
      struct conflict_tree_incoming_delete_details *details;

      details = conflict->tree_conflict_incoming_details;
      if (details == NULL)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Conflict resolution option '%d' requires "
                                   "details for tree conflict at '%s' to be "
                                   "fetched from the repository."),
                                option->id,
                                svn_dirent_local_style(local_abspath,
                                                       scratch_pool));

      if (details->deleted_rev == SVN_INVALID_REVNUM &&
          details->added_rev == SVN_INVALID_REVNUM)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Could not find the revision in which '%s' "
                                   "was deleted from the repository"),
                                 svn_dirent_local_style(
                                   svn_dirent_skip_ancestor(
                                     wcroot_abspath,
                                     conflict->local_abspath),
                                   scratch_pool));

      if (local_change == svn_wc_conflict_reason_edited)
        SVN_ERR(ensure_local_edit_vs_incoming_deletion_copied_state(
                  details, operation, wcroot_abspath, conflict, ctx,
                  scratch_pool));
    }
  else if (operation == svn_wc_operation_merge)
    {
      svn_node_kind_t victim_node_kind;
      svn_node_kind_t on_disk_kind;

      /* For merge, all we can do is ensure that the item still exists. */
      victim_node_kind =
        svn_client_conflict_tree_get_victim_node_kind(conflict);
      SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool));

      if (victim_node_kind != on_disk_kind)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Cannot resolve tree conflict on '%s' "
                                   "(expected node kind '%s' but found '%s')"),
                                 svn_dirent_local_style(
                                   svn_dirent_skip_ancestor(
                                     wcroot_abspath, conflict->local_abspath),
                                   scratch_pool),
                                 svn_node_kind_to_word(victim_node_kind),
                                 svn_node_kind_to_word(on_disk_kind));
    }

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_incoming_delete_ignore(svn_client_conflict_option_t *option,
                               svn_client_conflict_t *conflict,
                               svn_client_ctx_t *ctx,
                               apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *local_abspath;
  const char *lock_abspath;
  svn_error_t *err;

  option_id = svn_client_conflict_option_get_id(option);
  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 local_abspath,
                                                 scratch_pool, scratch_pool));

  err = verify_local_state_for_incoming_delete(conflict, option, ctx,
                                               scratch_pool);
  if (err)
    goto unlock_wc;

  /* Resolve to the current working copy state. */
  err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool);

  /* svn_wc__del_tree_conflict doesn't handle notification for us */
  if (ctx->notify_func2)
    ctx->notify_func2(ctx->notify_baton2,
                      svn_wc_create_notify(local_abspath,
                                           svn_wc_notify_resolved_tree,
                                           scratch_pool),
                      scratch_pool);

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  conflict->resolution_tree = option_id;

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_incoming_delete_accept(svn_client_conflict_option_t *option,
                               svn_client_conflict_t *conflict,
                               svn_client_ctx_t *ctx,
                               apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *local_abspath;
  const char *parent_abspath;
  const char *lock_abspath;
  svn_error_t *err;

  option_id = svn_client_conflict_option_get_id(option);
  local_abspath = svn_client_conflict_get_local_abspath(conflict);

  /* Deleting a node requires a lock on the node's parent. */
  parent_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
                                                 parent_abspath,
                                                 scratch_pool, scratch_pool));

  err = verify_local_state_for_incoming_delete(conflict, option, ctx,
                                               scratch_pool);
  if (err)
    goto unlock_wc;

  /* Delete the tree conflict victim. Marks the conflict resolved. */
  err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE,
                       NULL, NULL, /* don't allow user to cancel here */
                       ctx->notify_func2, ctx->notify_baton2,
                       scratch_pool);
  if (err)
    {
      if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
        {
          /* Not a versioned path. This can happen if the victim has already
           * been deleted in our branche's history, for example. Either way,
           * the item is gone, which is what we want, so don't treat this as
           * a fatal error. */
          svn_error_clear(err);

          /* Resolve to current working copy state. */
          err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath,
                                          scratch_pool);
        }

      if (err)
        goto unlock_wc;
    }

  if (ctx->notify_func2)
    ctx->notify_func2(ctx->notify_baton2,
                      svn_wc_create_notify(local_abspath,
                                           svn_wc_notify_resolved_tree,
                                           scratch_pool),
                      scratch_pool);

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  conflict->resolution_tree = option_id;

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option,
                                      svn_client_conflict_t *conflict,
                                      svn_client_ctx_t *ctx,
                                      apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *victim_abspath;
  const char *merge_source_abspath;
  svn_wc_conflict_reason_t local_change;
  svn_wc_operation_t operation;
  const char *lock_abspath;
  svn_error_t *err;
  const char *repos_root_url;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  const char *wc_tmpdir;
  const char *ancestor_abspath;
  svn_stream_t *ancestor_stream;
  apr_hash_t *ancestor_props;
  apr_hash_t *victim_props;
  apr_hash_t *move_target_props;
  const char *ancestor_url;
  const char *corrected_url;
  svn_ra_session_t *ra_session;
  svn_wc_merge_outcome_t merge_content_outcome;
  svn_wc_notify_state_t merge_props_outcome;
  apr_array_header_t *propdiffs;
  struct conflict_tree_incoming_delete_details *details;
  apr_array_header_t *possible_moved_to_abspaths;
  const char *moved_to_abspath;
  const char *incoming_abspath = NULL;

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  details = conflict->tree_conflict_incoming_details;
  if (details == NULL || details->moves == NULL)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("The specified conflict resolution option "
                               "requires details for tree conflict at '%s' "
                               "to be fetched from the repository first."),
                            svn_dirent_local_style(victim_abspath,
                                                   scratch_pool));
  if (operation == svn_wc_operation_none)
    return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL,
                             _("Invalid operation code '%d' recorded for "
                               "conflict at '%s'"), operation,
                             svn_dirent_local_style(victim_abspath,
                                                    scratch_pool));

  option_id = svn_client_conflict_option_get_id(option);
  SVN_ERR_ASSERT(option_id ==
                 svn_client_conflict_option_incoming_move_file_text_merge ||
                 option_id ==
                 svn_client_conflict_option_both_moved_file_move_merge);

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  /* Set up temporary storage for the common ancestor version of the file. */
  SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, victim_abspath,
                             scratch_pool, scratch_pool));
  SVN_ERR(svn_stream_open_unique(&ancestor_stream,
                                 &ancestor_abspath, wc_tmpdir,
                                 svn_io_file_del_on_pool_cleanup,
                                 scratch_pool, scratch_pool));

  /* Fetch the ancestor file's content. */
  ancestor_url = svn_path_url_add_component2(repos_root_url,
                                             incoming_old_repos_relpath,
                                             scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               ancestor_url, NULL, NULL,
                                               FALSE, FALSE, ctx,
                                               scratch_pool, scratch_pool));
  SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev,
                          ancestor_stream, NULL, /* fetched_rev */
                          &ancestor_props, scratch_pool));
  filter_props(ancestor_props, scratch_pool);

  /* Close stream to flush ancestor file to disk. */
  SVN_ERR(svn_stream_close(ancestor_stream));

  possible_moved_to_abspaths =
    svn_hash_gets(details->wc_move_targets,
                  get_moved_to_repos_relpath(details, scratch_pool));
  moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths,
                                   details->wc_move_target_idx,
                                   const char *);

  if (local_change == svn_wc_conflict_reason_missing)
    {
      /* This is an incoming move vs local move conflict.
       * Merge from the local move's target location to the
       * incoming move's target location. */
      struct conflict_tree_local_missing_details *local_details;

      local_details = conflict->tree_conflict_local_details;
      if (local_details->wc_move_targets &&
          local_details->move_target_repos_relpath)
        {
          apr_array_header_t *moves;
          moves = svn_hash_gets(local_details->wc_move_targets,
                                local_details->move_target_repos_relpath);
          merge_source_abspath =
            APR_ARRAY_IDX(moves, local_details->wc_move_target_idx,
            const char *);
        }
      else
        merge_source_abspath = victim_abspath;
    }
  else
    merge_source_abspath = victim_abspath;

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
            &lock_abspath, ctx->wc_ctx,
            svn_dirent_get_longest_ancestor(victim_abspath,
                                            moved_to_abspath,
                                            scratch_pool),
            scratch_pool, scratch_pool));

  if (local_change != svn_wc_conflict_reason_missing)
    {
      err = verify_local_state_for_incoming_delete(conflict, option, ctx,
                                                   scratch_pool);
      if (err)
        goto unlock_wc;
    }

   /* Get a copy of the conflict victim's properties. */
  err = svn_wc_prop_list2(&victim_props, ctx->wc_ctx, merge_source_abspath,
                          scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Get a copy of the move target's properties. */
  err = svn_wc_prop_list2(&move_target_props, ctx->wc_ctx,
                          moved_to_abspath,
                          scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Create a property diff for the files. */
  err = svn_prop_diffs(&propdiffs, move_target_props, victim_props,
                       scratch_pool);
  if (err)
    goto unlock_wc;

  if (operation == svn_wc_operation_update ||
      operation == svn_wc_operation_switch)
    {
      svn_stream_t *moved_to_stream;
      svn_stream_t *incoming_stream;

      /* Create a temporary copy of the moved file in repository-normal form.
       * Set up this temporary file to be automatically removed. */
      err = svn_stream_open_unique(&incoming_stream,
                                   &incoming_abspath, wc_tmpdir,
                                   svn_io_file_del_on_pool_cleanup,
                                   scratch_pool, scratch_pool);
      if (err)
        goto unlock_wc;

      err = svn_wc__translated_stream(&moved_to_stream, ctx->wc_ctx,
                                      moved_to_abspath,
                                      moved_to_abspath,
                                      SVN_WC_TRANSLATE_TO_NF,
                                      scratch_pool, scratch_pool);
      if (err)
        goto unlock_wc;

      err = svn_stream_copy3(moved_to_stream, incoming_stream,
                             NULL, NULL, /* no cancellation */
                             scratch_pool);
      if (err)
        goto unlock_wc;

      /* Overwrite the moved file with the conflict victim's content.
       * Incoming changes will be merged in from the temporary file created
       * above. This is required to correctly make local changes show up as
       * 'mine' during the three-way text merge between the ancestor file,
       * the conflict victim ('mine'), and the moved file ('theirs') which
       * was brought in by the update/switch operation and occupies the path
       * of the merge target. */
      err = svn_io_copy_file(merge_source_abspath, moved_to_abspath, FALSE,
                             scratch_pool);
      if (err)
        goto unlock_wc;
    }
  else if (operation == svn_wc_operation_merge)
    {
      svn_stream_t *incoming_stream;
      svn_stream_t *move_target_stream;

      /* Set aside the current move target file. This is required to apply
       * the move, and only then perform a three-way text merge between
       * the ancestor's file, our working file (which we would move to
       * the destination), and the file that we have set aside, which
       * contains the incoming fulltext.
       * Set up this temporary file to NOT be automatically removed. */
      err = svn_stream_open_unique(&incoming_stream,
                                   &incoming_abspath, wc_tmpdir,
                                   svn_io_file_del_none,
                                   scratch_pool, scratch_pool);
      if (err)
        goto unlock_wc;

      err = svn_wc__translated_stream(&move_target_stream, ctx->wc_ctx,
                                      moved_to_abspath, moved_to_abspath,
                                      SVN_WC_TRANSLATE_TO_NF,
                                      scratch_pool, scratch_pool);
      if (err)
        goto unlock_wc;

      err = svn_stream_copy3(move_target_stream, incoming_stream,
                             NULL, NULL, /* no cancellation */
                             scratch_pool);
      if (err)
        goto unlock_wc;

      /* Apply the incoming move. */
      err = svn_io_remove_file2(moved_to_abspath, FALSE, scratch_pool);
      if (err)
        goto unlock_wc;
      err = svn_wc__move2(ctx->wc_ctx, merge_source_abspath, moved_to_abspath,
                          FALSE, /* ordinary (not meta-data only) move */
                          FALSE, /* mixed-revisions don't apply to files */
                          NULL, NULL, /* don't allow user to cancel here */
                          NULL, NULL, /* no extra notification */
                          scratch_pool);
      if (err)
        goto unlock_wc;
    }
  else
    SVN_ERR_MALFUNCTION();

  /* Perform the file merge. */
  err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome,
                      ctx->wc_ctx, ancestor_abspath,
                      incoming_abspath, moved_to_abspath,
                      NULL, NULL, NULL, /* labels */
                      NULL, NULL, /* conflict versions */
                      FALSE, /* dry run */
                      NULL, NULL, /* diff3_cmd, merge_options */
                      apr_hash_count(ancestor_props) ? ancestor_props : NULL,
                      propdiffs,
                      NULL, NULL, /* conflict func/baton */
                      NULL, NULL, /* don't allow user to cancel here */
                      scratch_pool);
  svn_io_sleep_for_timestamps(moved_to_abspath, scratch_pool);
  if (err)
    goto unlock_wc;

  if (operation == svn_wc_operation_merge && incoming_abspath)
    {
      err = svn_io_remove_file2(incoming_abspath, TRUE, scratch_pool);
      if (err)
        goto unlock_wc;
      incoming_abspath = NULL;
    }

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      /* Tell the world about the file merge that just happened. */
      notify = svn_wc_create_notify(moved_to_abspath,
                                    svn_wc_notify_update_update,
                                    scratch_pool);
      if (merge_content_outcome == svn_wc_merge_conflict)
        notify->content_state = svn_wc_notify_state_conflicted;
      else
        notify->content_state = svn_wc_notify_state_merged;
      notify->prop_state = merge_props_outcome;
      notify->kind = svn_node_file;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  if (operation == svn_wc_operation_update ||
      operation == svn_wc_operation_switch)
    {
      /* Delete the tree conflict victim (clears the tree conflict marker). */
      err = svn_wc_delete4(ctx->wc_ctx, victim_abspath, FALSE, FALSE,
                           NULL, NULL, /* don't allow user to cancel here */
                           NULL, NULL, /* no extra notification */
                           scratch_pool);
      if (err)
        goto unlock_wc;
    }
  else if (local_change == svn_wc_conflict_reason_missing)
    {
      /* Clear tree conflict marker. */
      err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath,
                                      scratch_pool);
      if (err)
        goto unlock_wc;
    }

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  conflict->resolution_tree = option_id;

unlock_wc:
  if (err && operation == svn_wc_operation_merge && incoming_abspath)
      err = svn_error_quick_wrapf(
              err, _("If needed, a backup copy of '%s' can be found at '%s'"),
              svn_dirent_local_style(moved_to_abspath, scratch_pool),
              svn_dirent_local_style(incoming_abspath, scratch_pool));
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t.
 * Resolve an incoming move vs local move conflict by merging from the
 * incoming move's target location to the local move's target location,
 * overriding the incoming move. */
static svn_error_t *
resolve_both_moved_file_text_merge(svn_client_conflict_option_t *option,
                                   svn_client_conflict_t *conflict,
                                   svn_client_ctx_t *ctx,
                                   apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *victim_abspath;
  const char *local_moved_to_abspath;
  svn_wc_operation_t operation;
  const char *lock_abspath;
  svn_error_t *err;
  const char *repos_root_url;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  const char *wc_tmpdir;
  const char *ancestor_abspath;
  svn_stream_t *ancestor_stream;
  apr_hash_t *ancestor_props;
  apr_hash_t *incoming_props;
  apr_hash_t *local_props;
  const char *ancestor_url;
  const char *corrected_url;
  svn_ra_session_t *ra_session;
  svn_wc_merge_outcome_t merge_content_outcome;
  svn_wc_notify_state_t merge_props_outcome;
  apr_array_header_t *propdiffs;
  struct conflict_tree_incoming_delete_details *incoming_details;
  apr_array_header_t *possible_moved_to_abspaths;
  const char *incoming_moved_to_abspath;
  struct conflict_tree_local_missing_details *local_details;
  apr_array_header_t *local_moves;

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  incoming_details = conflict->tree_conflict_incoming_details;
  if (incoming_details == NULL || incoming_details->moves == NULL)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("The specified conflict resolution option "
                               "requires details for tree conflict at '%s' "
                               "to be fetched from the repository first."),
                            svn_dirent_local_style(victim_abspath,
                                                   scratch_pool));
  if (operation == svn_wc_operation_none)
    return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL,
                             _("Invalid operation code '%d' recorded for "
                               "conflict at '%s'"), operation,
                             svn_dirent_local_style(victim_abspath,
                                                    scratch_pool));

  option_id = svn_client_conflict_option_get_id(option);
  SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_file_merge);

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  /* Set up temporary storage for the common ancestor version of the file. */
  SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, victim_abspath,
                             scratch_pool, scratch_pool));
  SVN_ERR(svn_stream_open_unique(&ancestor_stream,
                                 &ancestor_abspath, wc_tmpdir,
                                 svn_io_file_del_on_pool_cleanup,
                                 scratch_pool, scratch_pool));

  /* Fetch the ancestor file's content. */
  ancestor_url = svn_path_url_add_component2(repos_root_url,
                                             incoming_old_repos_relpath,
                                             scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               ancestor_url, NULL, NULL,
                                               FALSE, FALSE, ctx,
                                               scratch_pool, scratch_pool));
  SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev,
                          ancestor_stream, NULL, /* fetched_rev */
                          &ancestor_props, scratch_pool));
  filter_props(ancestor_props, scratch_pool);

  /* Close stream to flush ancestor file to disk. */
  SVN_ERR(svn_stream_close(ancestor_stream));

  possible_moved_to_abspaths =
    svn_hash_gets(incoming_details->wc_move_targets,
                  get_moved_to_repos_relpath(incoming_details, scratch_pool));
  incoming_moved_to_abspath =
    APR_ARRAY_IDX(possible_moved_to_abspaths,
                  incoming_details->wc_move_target_idx, const char *);

  local_details = conflict->tree_conflict_local_details;
  local_moves = svn_hash_gets(local_details->wc_move_targets,
                        local_details->move_target_repos_relpath);
  local_moved_to_abspath =
    APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx, const char *);

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
            &lock_abspath, ctx->wc_ctx,
            svn_dirent_get_longest_ancestor(victim_abspath,
                                            local_moved_to_abspath,
                                            scratch_pool),
            scratch_pool, scratch_pool));

   /* Get a copy of the incoming moved item's properties. */
  err = svn_wc_prop_list2(&incoming_props, ctx->wc_ctx,
                          incoming_moved_to_abspath,
                          scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Get a copy of the local move target's properties. */
  err = svn_wc_prop_list2(&local_props, ctx->wc_ctx,
                          local_moved_to_abspath,
                          scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Create a property diff for the files. */
  err = svn_prop_diffs(&propdiffs, incoming_props, local_props,
                       scratch_pool);
  if (err)
    goto unlock_wc;

  /* Perform the file merge. */
  err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome,
                      ctx->wc_ctx, ancestor_abspath,
                      incoming_moved_to_abspath, local_moved_to_abspath,
                      NULL, NULL, NULL, /* labels */
                      NULL, NULL, /* conflict versions */
                      FALSE, /* dry run */
                      NULL, NULL, /* diff3_cmd, merge_options */
                      apr_hash_count(ancestor_props) ? ancestor_props : NULL,
                      propdiffs,
                      NULL, NULL, /* conflict func/baton */
                      NULL, NULL, /* don't allow user to cancel here */
                      scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      /* Tell the world about the file merge that just happened. */
      notify = svn_wc_create_notify(local_moved_to_abspath,
                                    svn_wc_notify_update_update,
                                    scratch_pool);
      if (merge_content_outcome == svn_wc_merge_conflict)
        notify->content_state = svn_wc_notify_state_conflicted;
      else
        notify->content_state = svn_wc_notify_state_merged;
      notify->prop_state = merge_props_outcome;
      notify->kind = svn_node_file;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  /* Revert local addition of the incoming move's target. */
  err = svn_wc_revert6(ctx->wc_ctx, incoming_moved_to_abspath,
                       svn_depth_infinity, FALSE, NULL, TRUE, FALSE,
                       FALSE /*added_keep_local*/,
                       NULL, NULL, /* no cancellation */
                       ctx->notify_func2, ctx->notify_baton2,
                       scratch_pool);
  if (err)
    goto unlock_wc;

  err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  svn_io_sleep_for_timestamps(local_moved_to_abspath, scratch_pool);

  conflict->resolution_tree = option_id;

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t.
 * Resolve an incoming move vs local move conflict by moving the locally moved
 * directory to the incoming move target location, and then merging changes. */
static svn_error_t *
resolve_both_moved_dir_merge(svn_client_conflict_option_t *option,
                             svn_client_conflict_t *conflict,
                             svn_client_ctx_t *ctx,
                             apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *victim_abspath;
  const char *local_moved_to_abspath;
  svn_wc_operation_t operation;
  const char *lock_abspath;
  svn_error_t *err;
  const char *repos_root_url;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  const char *incoming_moved_repos_relpath;
  struct conflict_tree_incoming_delete_details *incoming_details;
  apr_array_header_t *possible_moved_to_abspaths;
  const char *incoming_moved_to_abspath;
  struct conflict_tree_local_missing_details *local_details;
  apr_array_header_t *local_moves;
  svn_client__conflict_report_t *conflict_report;
  const char *incoming_old_url;
  const char *incoming_moved_url;
  svn_opt_revision_t incoming_old_opt_rev;
  svn_opt_revision_t incoming_moved_opt_rev;

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  incoming_details = conflict->tree_conflict_incoming_details;
  if (incoming_details == NULL || incoming_details->moves == NULL)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("The specified conflict resolution option "
                               "requires details for tree conflict at '%s' "

                               "to be fetched from the repository first."),
                            svn_dirent_local_style(victim_abspath,
                                                   scratch_pool));
  if (operation == svn_wc_operation_none)
    return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL,
                             _("Invalid operation code '%d' recorded for "
                               "conflict at '%s'"), operation,
                             svn_dirent_local_style(victim_abspath,
                                                    scratch_pool));

  option_id = svn_client_conflict_option_get_id(option);
  SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_dir_merge);

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  possible_moved_to_abspaths =
    svn_hash_gets(incoming_details->wc_move_targets,
                  get_moved_to_repos_relpath(incoming_details, scratch_pool));
  incoming_moved_to_abspath =
    APR_ARRAY_IDX(possible_moved_to_abspaths,
                  incoming_details->wc_move_target_idx, const char *);

  local_details = conflict->tree_conflict_local_details;
  local_moves = svn_hash_gets(local_details->wc_move_targets,
                        local_details->move_target_repos_relpath);
  local_moved_to_abspath =
    APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx, const char *);

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
            &lock_abspath, ctx->wc_ctx,
            svn_dirent_get_longest_ancestor(victim_abspath,
                                            local_moved_to_abspath,
                                            scratch_pool),
            scratch_pool, scratch_pool));

  /* Perform the merge. */
  incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/",
                                 incoming_old_repos_relpath, SVN_VA_NULL);
  incoming_old_opt_rev.kind = svn_opt_revision_number;
  incoming_old_opt_rev.value.number = incoming_old_pegrev;

  incoming_moved_repos_relpath =
      get_moved_to_repos_relpath(incoming_details, scratch_pool);
  incoming_moved_url = apr_pstrcat(scratch_pool, repos_root_url, "/",
                                   incoming_moved_repos_relpath, SVN_VA_NULL);
  incoming_moved_opt_rev.kind = svn_opt_revision_number;
  incoming_moved_opt_rev.value.number = incoming_new_pegrev;
  err = svn_client__merge_locked(&conflict_report,
                                 incoming_old_url, &incoming_old_opt_rev,
                                 incoming_moved_url, &incoming_moved_opt_rev,
                                 local_moved_to_abspath, svn_depth_infinity,
                                 TRUE, TRUE, /* do a no-ancestry merge */
                                 FALSE, FALSE, FALSE,
                                 TRUE, /* Allow mixed-rev just in case,
                                        * since conflict victims can't be
                                        * updated to straighten out
                                        * mixed-rev trees. */
                                 NULL, ctx, scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Revert local addition of the incoming move's target. */
  err = svn_wc_revert6(ctx->wc_ctx, incoming_moved_to_abspath,
                       svn_depth_infinity, FALSE, NULL, TRUE, FALSE,
                       FALSE /*added_keep_local*/,
                       NULL, NULL, /* no cancellation */
                       ctx->notify_func2, ctx->notify_baton2,
                       scratch_pool);
  if (err)
    goto unlock_wc;

  err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  svn_io_sleep_for_timestamps(local_moved_to_abspath, scratch_pool);

  conflict->resolution_tree = option_id;

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t.
 * Resolve an incoming move vs local move conflict by merging from the
 * incoming move's target location to the local move's target location,
 * overriding the incoming move. */
static svn_error_t *
resolve_both_moved_dir_move_merge(svn_client_conflict_option_t *option,
                                  svn_client_conflict_t *conflict,
                                  svn_client_ctx_t *ctx,
                                  apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *victim_abspath;
  const char *local_moved_to_abspath;
  svn_wc_operation_t operation;
  const char *lock_abspath;
  svn_error_t *err;
  const char *repos_root_url;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  struct conflict_tree_incoming_delete_details *incoming_details;
  apr_array_header_t *possible_moved_to_abspaths;
  const char *incoming_moved_to_abspath;
  struct conflict_tree_local_missing_details *local_details;
  apr_array_header_t *local_moves;
  svn_client__conflict_report_t *conflict_report;
  const char *incoming_old_url;
  const char *incoming_moved_url;
  svn_opt_revision_t incoming_old_opt_rev;
  svn_opt_revision_t incoming_moved_opt_rev;

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  incoming_details = conflict->tree_conflict_incoming_details;
  if (incoming_details == NULL || incoming_details->moves == NULL)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("The specified conflict resolution option "
                               "requires details for tree conflict at '%s' "

                               "to be fetched from the repository first."),
                            svn_dirent_local_style(victim_abspath,
                                                   scratch_pool));
  if (operation == svn_wc_operation_none)
    return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL,
                             _("Invalid operation code '%d' recorded for "
                               "conflict at '%s'"), operation,
                             svn_dirent_local_style(victim_abspath,
                                                    scratch_pool));

  option_id = svn_client_conflict_option_get_id(option);
  SVN_ERR_ASSERT(option_id ==
                 svn_client_conflict_option_both_moved_dir_move_merge);

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  possible_moved_to_abspaths =
    svn_hash_gets(incoming_details->wc_move_targets,
                  get_moved_to_repos_relpath(incoming_details, scratch_pool));
  incoming_moved_to_abspath =
    APR_ARRAY_IDX(possible_moved_to_abspaths,
                  incoming_details->wc_move_target_idx, const char *);

  local_details = conflict->tree_conflict_local_details;
  local_moves = svn_hash_gets(local_details->wc_move_targets,
                        local_details->move_target_repos_relpath);
  local_moved_to_abspath =
    APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx, const char *);

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
            &lock_abspath, ctx->wc_ctx,
            svn_dirent_get_longest_ancestor(victim_abspath,
                                            local_moved_to_abspath,
                                            scratch_pool),
            scratch_pool, scratch_pool));

  /* Revert the incoming move target directory. */
  err = svn_wc_revert6(ctx->wc_ctx, incoming_moved_to_abspath,
                       svn_depth_infinity,
                       FALSE, NULL, TRUE, FALSE,
                       TRUE /*added_keep_local*/,
                       NULL, NULL, /* no cancellation */
                       ctx->notify_func2, ctx->notify_baton2,
                       scratch_pool);
  if (err)
    goto unlock_wc;

  /* The move operation is not part of natural history. We must replicate
   * this move in our history. Record a move in the working copy. */
  err = svn_wc__move2(ctx->wc_ctx, local_moved_to_abspath,
                      incoming_moved_to_abspath,
                      FALSE, /* this is not a meta-data only move */
                      TRUE, /* allow mixed-revisions just in case */
                      NULL, NULL, /* don't allow user to cancel here */
                      ctx->notify_func2, ctx->notify_baton2,
                      scratch_pool);
  if (err)
    goto unlock_wc;

  /* Merge INCOMING_OLD_URL@MERGE_LEFT->INCOMING_MOVED_URL@MERGE_RIGHT
   * into the locally moved merge target. */
  incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/",
                                 incoming_old_repos_relpath, SVN_VA_NULL);
  incoming_old_opt_rev.kind = svn_opt_revision_number;
  incoming_old_opt_rev.value.number = incoming_old_pegrev;

  incoming_moved_url = apr_pstrcat(scratch_pool, repos_root_url, "/",
                                   incoming_details->move_target_repos_relpath,
                                   SVN_VA_NULL);
  incoming_moved_opt_rev.kind = svn_opt_revision_number;
  incoming_moved_opt_rev.value.number = incoming_new_pegrev;
  err = svn_client__merge_locked(&conflict_report,
                                 incoming_old_url, &incoming_old_opt_rev,
                                 incoming_moved_url, &incoming_moved_opt_rev,
                                 incoming_moved_to_abspath, svn_depth_infinity,
                                 TRUE, TRUE, /* do a no-ancestry merge */
                                 FALSE, FALSE, FALSE,
                                 TRUE, /* Allow mixed-rev just in case,
                                        * since conflict victims can't be
                                        * updated to straighten out
                                        * mixed-rev trees. */
                                 NULL, ctx, scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  svn_io_sleep_for_timestamps(local_moved_to_abspath, scratch_pool);

  conflict->resolution_tree = option_id;

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_incoming_move_dir_merge(svn_client_conflict_option_t *option,
                                svn_client_conflict_t *conflict,
                                svn_client_ctx_t *ctx,
                                apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *local_abspath;
  svn_wc_operation_t operation;
  const char *lock_abspath;
  svn_error_t *err;
  const char *repos_root_url;
  const char *repos_uuid;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  const char *victim_repos_relpath;
  svn_revnum_t victim_peg_rev;
  const char *moved_to_repos_relpath;
  svn_revnum_t moved_to_peg_rev;
  struct conflict_tree_incoming_delete_details *details;
  apr_array_header_t *possible_moved_to_abspaths;
  const char *moved_to_abspath;
  const char *incoming_old_url;
  svn_opt_revision_t incoming_old_opt_rev;
  svn_client__conflict_report_t *conflict_report;
  svn_boolean_t is_copy;
  svn_boolean_t is_modified;

  local_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  details = conflict->tree_conflict_incoming_details;
  if (details == NULL || details->moves == NULL)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("The specified conflict resolution option "
                               "requires details for tree conflict at '%s' "
                               "to be fetched from the repository first."),
                            svn_dirent_local_style(local_abspath,
                                                   scratch_pool));

  option_id = svn_client_conflict_option_get_id(option);
  SVN_ERR_ASSERT(option_id ==
                 svn_client_conflict_option_incoming_move_dir_merge);

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, &repos_uuid,
                                             conflict, scratch_pool,
                                             scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  /* Get repository location of the moved-away node (the conflict victim). */
  if (operation == svn_wc_operation_update ||
      operation == svn_wc_operation_switch)
    {
      victim_repos_relpath = incoming_old_repos_relpath;
      victim_peg_rev = incoming_old_pegrev;
    }
  else if (operation == svn_wc_operation_merge)
    SVN_ERR(svn_wc__node_get_repos_info(&victim_peg_rev, &victim_repos_relpath,
                                        NULL, NULL, ctx->wc_ctx, local_abspath,
                                        scratch_pool, scratch_pool));

  /* Get repository location of the moved-here node (incoming move). */
  possible_moved_to_abspaths =
    svn_hash_gets(details->wc_move_targets,
                  get_moved_to_repos_relpath(details, scratch_pool));
  moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths,
                                   details->wc_move_target_idx,
                                   const char *);

  /* ### The following WC modifications should be atomic. */

  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
            &lock_abspath, ctx->wc_ctx,
            svn_dirent_get_longest_ancestor(local_abspath,
                                            moved_to_abspath,
                                            scratch_pool),
            scratch_pool, scratch_pool));

  err = svn_wc__node_get_origin(&is_copy, &moved_to_peg_rev,
                                &moved_to_repos_relpath,
                                NULL, NULL, NULL, NULL,
                                ctx->wc_ctx, moved_to_abspath, FALSE,
                                scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;
  if (!is_copy && operation == svn_wc_operation_merge)
    {
      err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                              _("Cannot resolve tree conflict on '%s' "
                                "(expected a copied item at '%s', but the "
                                "item is not a copy)"),
                              svn_dirent_local_style(local_abspath,
                                                     scratch_pool),
                              svn_dirent_local_style(moved_to_abspath,
                                                     scratch_pool));
      goto unlock_wc;
    }

  if (moved_to_repos_relpath == NULL || moved_to_peg_rev == SVN_INVALID_REVNUM)
    {
      err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                              _("Cannot resolve tree conflict on '%s' "
                                "(could not determine origin of '%s')"),
                              svn_dirent_local_style(local_abspath,
                                                     scratch_pool),
                              svn_dirent_local_style(moved_to_abspath,
                                                     scratch_pool));
      goto unlock_wc;
    }

  err = verify_local_state_for_incoming_delete(conflict, option, ctx,
                                               scratch_pool);
  if (err)
    goto unlock_wc;

  if (operation == svn_wc_operation_merge)
    {
      const char *move_target_url;
      svn_opt_revision_t incoming_new_opt_rev;

      /* Revert the incoming move target directory. */
      err = svn_wc_revert6(ctx->wc_ctx, moved_to_abspath, svn_depth_infinity,
                           FALSE, NULL, TRUE, FALSE,
                           TRUE /*added_keep_local*/,
                           NULL, NULL, /* no cancellation */
                           ctx->notify_func2, ctx->notify_baton2,
                           scratch_pool);
      if (err)
        goto unlock_wc;

      /* The move operation is not part of natural history. We must replicate
       * this move in our history. Record a move in the working copy. */
      err = svn_wc__move2(ctx->wc_ctx, local_abspath, moved_to_abspath,
                          FALSE, /* this is not a meta-data only move */
                          TRUE, /* allow mixed-revisions just in case */
                          NULL, NULL, /* don't allow user to cancel here */
                          ctx->notify_func2, ctx->notify_baton2,
                          scratch_pool);
      if (err)
        goto unlock_wc;

      /* Merge INCOMING_OLD_URL@MERGE_LEFT->MOVE_TARGET_URL@MERGE_RIGHT
       * into move target. */
      incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/",
                                     incoming_old_repos_relpath, SVN_VA_NULL);
      incoming_old_opt_rev.kind = svn_opt_revision_number;
      incoming_old_opt_rev.value.number = incoming_old_pegrev;
      move_target_url = apr_pstrcat(scratch_pool, repos_root_url, "/",
                                    get_moved_to_repos_relpath(details,
                                                               scratch_pool),
                                    SVN_VA_NULL);
      incoming_new_opt_rev.kind = svn_opt_revision_number;
      incoming_new_opt_rev.value.number = incoming_new_pegrev;
      err = svn_client__merge_locked(&conflict_report,
                                     incoming_old_url, &incoming_old_opt_rev,
                                     move_target_url, &incoming_new_opt_rev,
                                     moved_to_abspath, svn_depth_infinity,
                                     TRUE, TRUE, /* do a no-ancestry merge */
                                     FALSE, FALSE, FALSE,
                                     TRUE, /* Allow mixed-rev just in case,
                                            * since conflict victims can't be
                                            * updated to straighten out
                                            * mixed-rev trees. */
                                     NULL, ctx, scratch_pool, scratch_pool);
      if (err)
        goto unlock_wc;
    }
  else
    {
      SVN_ERR_ASSERT(operation == svn_wc_operation_update ||
                     operation == svn_wc_operation_switch);

      /* Merge local modifications into the incoming move target dir. */
      err = svn_wc__has_local_mods(&is_modified, ctx->wc_ctx, local_abspath,
                                   TRUE, ctx->cancel_func, ctx->cancel_baton,
                                   scratch_pool);
      if (err)
        goto unlock_wc;

      if (is_modified)
        {
          err = svn_wc__conflict_tree_update_incoming_move(ctx->wc_ctx,
                                                           local_abspath,
                                                           moved_to_abspath,
                                                           ctx->cancel_func,
                                                           ctx->cancel_baton,
                                                           ctx->notify_func2,
                                                           ctx->notify_baton2,
                                                           scratch_pool);
          if (err)
            goto unlock_wc;
        }

      /* The move operation is part of our natural history.
       * Delete the tree conflict victim (clears the tree conflict marker). */
      err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE,
                           NULL, NULL, /* don't allow user to cancel here */
                           NULL, NULL, /* no extra notification */
                           scratch_pool);
      if (err)
        goto unlock_wc;
    }

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  conflict->resolution_tree = option_id;

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t.
 * Handles svn_client_conflict_option_local_move_file_text_merge
 * and svn_client_conflict_option_sibling_move_file_text_merge. */
static svn_error_t *
resolve_local_move_file_merge(svn_client_conflict_option_t *option,
                              svn_client_conflict_t *conflict,
                              svn_client_ctx_t *ctx,
                              apr_pool_t *scratch_pool)
{
  const char *lock_abspath;
  svn_error_t *err;
  const char *repos_root_url;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  const char *wc_tmpdir;
  const char *ancestor_tmp_abspath;
  const char *incoming_tmp_abspath;
  apr_hash_t *ancestor_props;
  apr_hash_t *incoming_props;
  svn_stream_t *stream;
  const char *url;
  const char *corrected_url;
  const char *old_session_url;
  svn_ra_session_t *ra_session;
  svn_wc_merge_outcome_t merge_content_outcome;
  svn_wc_notify_state_t merge_props_outcome;
  apr_array_header_t *propdiffs;
  struct conflict_tree_local_missing_details *details;
  const char *merge_target_abspath;
  const char *wcroot_abspath;

  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                             conflict->local_abspath, scratch_pool,
                             scratch_pool));

  details = conflict->tree_conflict_local_details;

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  if (details->wc_siblings)
    {
      merge_target_abspath = APR_ARRAY_IDX(details->wc_siblings,
                                           details->preferred_sibling_idx,
                                           const char *);
    }
  else if (details->wc_move_targets && details->move_target_repos_relpath)
    {
      apr_array_header_t *moves;
      moves = svn_hash_gets(details->wc_move_targets,
                            details->move_target_repos_relpath);
      merge_target_abspath = APR_ARRAY_IDX(moves, details->wc_move_target_idx,
                                           const char *);
    }
  else
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("Corresponding working copy node not found "
                               "for '%s'"),
                             svn_dirent_local_style(
                               svn_dirent_skip_ancestor(
                                 wcroot_abspath, conflict->local_abspath),
                             scratch_pool));

  SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx,
                             merge_target_abspath,
                             scratch_pool, scratch_pool));

  /* Fetch the common ancestor file's content. */
  SVN_ERR(svn_stream_open_unique(&stream, &ancestor_tmp_abspath, wc_tmpdir,
                                 svn_io_file_del_on_pool_cleanup,
                                 scratch_pool, scratch_pool));
  url = svn_path_url_add_component2(repos_root_url,
                                    incoming_old_repos_relpath,
                                    scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               url, NULL, NULL,
                                               FALSE, FALSE, ctx,
                                               scratch_pool, scratch_pool));
  SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev, stream, NULL,
                          &ancestor_props, scratch_pool));
  filter_props(ancestor_props, scratch_pool);

  /* Close stream to flush the file to disk. */
  SVN_ERR(svn_stream_close(stream));

  /* Do the same for the incoming file's content. */
  SVN_ERR(svn_stream_open_unique(&stream, &incoming_tmp_abspath, wc_tmpdir,
                                 svn_io_file_del_on_pool_cleanup,
                                 scratch_pool, scratch_pool));
  url = svn_path_url_add_component2(repos_root_url,
                                    incoming_new_repos_relpath,
                                    scratch_pool);
  SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session,
                                            url, scratch_pool));
  SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev, stream, NULL,
                          &incoming_props, scratch_pool));
  /* Close stream to flush the file to disk. */
  SVN_ERR(svn_stream_close(stream));

  filter_props(incoming_props, scratch_pool);

  /* Create a property diff for the files. */
  SVN_ERR(svn_prop_diffs(&propdiffs, incoming_props, ancestor_props,
                         scratch_pool));

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
            &lock_abspath, ctx->wc_ctx,
            svn_dirent_get_longest_ancestor(conflict->local_abspath,
                                            merge_target_abspath,
                                            scratch_pool),
            scratch_pool, scratch_pool));

  /* Perform the file merge. */
  err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome,
                      ctx->wc_ctx,
                      ancestor_tmp_abspath, incoming_tmp_abspath,
                      merge_target_abspath,
                      NULL, NULL, NULL, /* labels */
                      NULL, NULL, /* conflict versions */
                      FALSE, /* dry run */
                      NULL, NULL, /* diff3_cmd, merge_options */
                      apr_hash_count(ancestor_props) ? ancestor_props : NULL,
                      propdiffs,
                      NULL, NULL, /* conflict func/baton */
                      NULL, NULL, /* don't allow user to cancel here */
                      scratch_pool);
  svn_io_sleep_for_timestamps(merge_target_abspath, scratch_pool);
  if (err)
    return svn_error_compose_create(err,
                                    svn_wc__release_write_lock(ctx->wc_ctx,
                                                               lock_abspath,
                                                               scratch_pool));

  err = svn_wc__del_tree_conflict(ctx->wc_ctx, conflict->local_abspath,
                                  scratch_pool);
  err = svn_error_compose_create(err,
                                 svn_wc__release_write_lock(ctx->wc_ctx,
                                                            lock_abspath,
                                                            scratch_pool));
  if (err)
    return svn_error_trace(err);

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      /* Tell the world about the file merge that just happened. */
      notify = svn_wc_create_notify(merge_target_abspath,
                                    svn_wc_notify_update_update,
                                    scratch_pool);
      if (merge_content_outcome == svn_wc_merge_conflict)
        notify->content_state = svn_wc_notify_state_conflicted;
      else
        notify->content_state = svn_wc_notify_state_merged;
      notify->prop_state = merge_props_outcome;
      notify->kind = svn_node_file;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);

      /* And also about the successfully resolved tree conflict. */
      notify = svn_wc_create_notify(conflict->local_abspath,
                                    svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t. */
static svn_error_t *
resolve_local_move_dir_merge(svn_client_conflict_option_t *option,
                             svn_client_conflict_t *conflict,
                             svn_client_ctx_t *ctx,
                             apr_pool_t *scratch_pool)
{
  const char *lock_abspath;
  svn_error_t *err;
  const char *repos_root_url;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  struct conflict_tree_local_missing_details *details;
  const char *merge_target_abspath;
  const char *incoming_old_url;
  const char *incoming_new_url;
  svn_opt_revision_t incoming_old_opt_rev;
  svn_opt_revision_t incoming_new_opt_rev;
  svn_client__conflict_report_t *conflict_report;

  details = conflict->tree_conflict_local_details;

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  if (details->wc_move_targets && details->move_target_repos_relpath)
    {
      apr_array_header_t *moves;

      moves = svn_hash_gets(details->wc_move_targets,
                            details->move_target_repos_relpath);
      merge_target_abspath =
        APR_ARRAY_IDX(moves, details->wc_move_target_idx, const char *);
    }
  else
    merge_target_abspath = APR_ARRAY_IDX(details->wc_siblings,
                                         details->preferred_sibling_idx,
                                         const char *);

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
            &lock_abspath, ctx->wc_ctx,
            svn_dirent_get_longest_ancestor(conflict->local_abspath,
                                            merge_target_abspath,
                                            scratch_pool),
            scratch_pool, scratch_pool));

  /* Resolve to current working copy state.
   * svn_client__merge_locked() requires this. */
  err = svn_wc__del_tree_conflict(ctx->wc_ctx, conflict->local_abspath,
                                  scratch_pool);
  if (err)
    goto unlock_wc;

  /* Merge outstanding changes to the merge target. */
  incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/",
                                 incoming_old_repos_relpath, SVN_VA_NULL);
  incoming_old_opt_rev.kind = svn_opt_revision_number;
  incoming_old_opt_rev.value.number = incoming_old_pegrev;
  incoming_new_url = apr_pstrcat(scratch_pool, repos_root_url, "/",
                                 incoming_new_repos_relpath, SVN_VA_NULL);
  incoming_new_opt_rev.kind = svn_opt_revision_number;
  incoming_new_opt_rev.value.number = incoming_new_pegrev;
  err = svn_client__merge_locked(&conflict_report,
                                 incoming_old_url, &incoming_old_opt_rev,
                                 incoming_new_url, &incoming_new_opt_rev,
                                 merge_target_abspath, svn_depth_infinity,
                                 TRUE, TRUE, /* do a no-ancestry merge */
                                 FALSE, FALSE, FALSE,
                                 TRUE, /* Allow mixed-rev just in case,
                                        * since conflict victims can't be
                                        * updated to straighten out
                                        * mixed-rev trees. */
                                 NULL, ctx, scratch_pool, scratch_pool);
unlock_wc:
  svn_io_sleep_for_timestamps(merge_target_abspath, scratch_pool);
  err = svn_error_compose_create(err,
                                 svn_wc__release_write_lock(ctx->wc_ctx,
                                                            lock_abspath,
                                                            scratch_pool));
  if (err)
    return svn_error_trace(err);

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      /* Tell the world about the file merge that just happened. */
      notify = svn_wc_create_notify(merge_target_abspath,
                                    svn_wc_notify_update_update,
                                    scratch_pool);
      if (conflict_report)
        notify->content_state = svn_wc_notify_state_conflicted;
      else
        notify->content_state = svn_wc_notify_state_merged;
      notify->kind = svn_node_dir;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);

      /* And also about the successfully resolved tree conflict. */
      notify = svn_wc_create_notify(conflict->local_abspath,
                                    svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  conflict->resolution_tree = svn_client_conflict_option_get_id(option);

  return SVN_NO_ERROR;
}

static svn_error_t *
assert_text_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool)
{
  svn_boolean_t text_conflicted;

  SVN_ERR(svn_client_conflict_get_conflicted(&text_conflicted, NULL, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));

  SVN_ERR_ASSERT(text_conflicted); /* ### return proper error? */

  return SVN_NO_ERROR;
}

static svn_error_t *
assert_prop_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool)
{
  apr_array_header_t *props_conflicted;

  SVN_ERR(svn_client_conflict_get_conflicted(NULL, &props_conflicted, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));

  /* ### return proper error? */
  SVN_ERR_ASSERT(props_conflicted && props_conflicted->nelts > 0);

  return SVN_NO_ERROR;
}

static svn_error_t *
assert_tree_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool)
{
  svn_boolean_t tree_conflicted;

  SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted,
                                             conflict, scratch_pool,
                                             scratch_pool));

  SVN_ERR_ASSERT(tree_conflicted); /* ### return proper error? */

  return SVN_NO_ERROR;
}

/* Helper to add to conflict resolution option to array of OPTIONS.
 * Resolution option object will be allocated from OPTIONS->POOL
 * and DESCRIPTION will be copied to this pool.
 * Returns pointer to the created conflict resolution option. */
static svn_client_conflict_option_t *
add_resolution_option(apr_array_header_t *options,
                      svn_client_conflict_t *conflict,
                      svn_client_conflict_option_id_t id,
                      const char *label,
                      const char *description,
                      conflict_option_resolve_func_t resolve_func)
{
    svn_client_conflict_option_t *option;

    option = apr_pcalloc(options->pool, sizeof(*option));
    option->pool = options->pool;
    option->id = id;
    option->label = apr_pstrdup(option->pool, label);
    option->description = apr_pstrdup(option->pool, description);
    option->conflict = conflict;
    option->do_resolve_func = resolve_func;

    APR_ARRAY_PUSH(options, const svn_client_conflict_option_t *) = option;

    return option;
}

svn_error_t *
svn_client_conflict_text_get_resolution_options(apr_array_header_t **options,
                                                svn_client_conflict_t *conflict,
                                                svn_client_ctx_t *ctx,
                                                apr_pool_t *result_pool,
                                                apr_pool_t *scratch_pool)
{
  const char *mime_type;

  SVN_ERR(assert_text_conflict(conflict, scratch_pool));

  *options = apr_array_make(result_pool, 7,
                            sizeof(svn_client_conflict_option_t *));

  add_resolution_option(*options, conflict,
      svn_client_conflict_option_postpone,
      _("Postpone"),
      _("skip this conflict and leave it unresolved"),
      resolve_postpone);

  mime_type = svn_client_conflict_text_get_mime_type(conflict);
  if (mime_type && svn_mime_type_is_binary(mime_type))
    {
      /* Resolver options for a binary file conflict. */
      add_resolution_option(*options, conflict,
        svn_client_conflict_option_base_text,
        _("Accept base"),
        _("discard local and incoming changes for this binary file"),
        resolve_text_conflict);

      add_resolution_option(*options, conflict,
        svn_client_conflict_option_incoming_text,
        _("Accept incoming"),
        _("accept incoming version of binary file"),
        resolve_text_conflict);

      add_resolution_option(*options, conflict,
        svn_client_conflict_option_working_text,
        _("Mark as resolved"),
        _("accept binary file as it appears in the working copy"),
        resolve_text_conflict);
  }
  else
    {
      /* Resolver options for a text file conflict. */
      add_resolution_option(*options, conflict,
        svn_client_conflict_option_base_text,
        _("Accept base"),
        _("discard local and incoming changes for this file"),
        resolve_text_conflict);

      add_resolution_option(*options, conflict,
        svn_client_conflict_option_incoming_text,
        _("Accept incoming"),
        _("accept incoming version of entire file"),
        resolve_text_conflict);

      add_resolution_option(*options, conflict,
        svn_client_conflict_option_working_text,
        _("Reject incoming"),
        _("reject all incoming changes for this file"),
        resolve_text_conflict);

      add_resolution_option(*options, conflict,
        svn_client_conflict_option_incoming_text_where_conflicted,
        _("Accept incoming for conflicts"),
        _("accept incoming changes only where they conflict"),
        resolve_text_conflict);

      add_resolution_option(*options, conflict,
        svn_client_conflict_option_working_text_where_conflicted,
        _("Reject conflicts"),
        _("reject incoming changes which conflict and accept the rest"),
        resolve_text_conflict);

      add_resolution_option(*options, conflict,
        svn_client_conflict_option_merged_text,
        _("Mark as resolved"),
        _("accept the file as it appears in the working copy"),
        resolve_text_conflict);
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_prop_get_resolution_options(apr_array_header_t **options,
                                                svn_client_conflict_t *conflict,
                                                svn_client_ctx_t *ctx,
                                                apr_pool_t *result_pool,
                                                apr_pool_t *scratch_pool)
{
  SVN_ERR(assert_prop_conflict(conflict, scratch_pool));

  *options = apr_array_make(result_pool, 7,
                            sizeof(svn_client_conflict_option_t *));

  add_resolution_option(*options, conflict,
    svn_client_conflict_option_postpone,
    _("Postpone"),
    _("skip this conflict and leave it unresolved"),
    resolve_postpone);

  add_resolution_option(*options, conflict,
    svn_client_conflict_option_base_text,
    _("Accept base"),
    _("discard local and incoming changes for this property"),
    resolve_prop_conflict);

  add_resolution_option(*options, conflict,
    svn_client_conflict_option_incoming_text,
    _("Accept incoming"),
    _("accept incoming version of entire property value"),
    resolve_prop_conflict);

  add_resolution_option(*options, conflict,
    svn_client_conflict_option_working_text,
    _("Mark as resolved"),
    _("accept working copy version of entire property value"),
    resolve_prop_conflict);

  add_resolution_option(*options, conflict,
    svn_client_conflict_option_incoming_text_where_conflicted,
    _("Accept incoming for conflicts"),
    _("accept incoming changes only where they conflict"),
    resolve_prop_conflict);

  add_resolution_option(*options, conflict,
    svn_client_conflict_option_working_text_where_conflicted,
    _("Reject conflicts"),
    _("reject changes which conflict and accept the rest"),
    resolve_prop_conflict);

  add_resolution_option(*options, conflict,
    svn_client_conflict_option_merged_text,
    _("Accept merged"),
    _("accept merged version of property value"),
    resolve_prop_conflict);

  return SVN_NO_ERROR;
}

/* Configure 'accept current wc state' resolution option for a tree conflict. */
static svn_error_t *
configure_option_accept_current_wc_state(svn_client_conflict_t *conflict,
                                         apr_array_header_t *options)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  conflict_option_resolve_func_t do_resolve_func;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);

  if ((operation == svn_wc_operation_update ||
       operation == svn_wc_operation_switch) &&
      (local_change == svn_wc_conflict_reason_moved_away ||
       local_change == svn_wc_conflict_reason_deleted ||
       local_change == svn_wc_conflict_reason_replaced) &&
      incoming_change == svn_wc_conflict_action_edit)
    {
      /* We must break moves if the user accepts the current working copy
       * state instead of updating a moved-away node or updating children
       * moved outside of deleted or replaced directory nodes.
       * Else such moves would be left in an invalid state. */
      do_resolve_func = resolve_update_break_moved_away;
    }
  else
    do_resolve_func = resolve_accept_current_wc_state;

  add_resolution_option(options, conflict,
                        svn_client_conflict_option_accept_current_wc_state,
                        _("Mark as resolved"),
                        _("accept current working copy state"),
                        do_resolve_func);

  return SVN_NO_ERROR;
}

/* Configure 'update move destination' resolution option for a tree conflict. */
static svn_error_t *
configure_option_update_move_destination(svn_client_conflict_t *conflict,
                                         apr_array_header_t *options)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);

  if ((operation == svn_wc_operation_update ||
       operation == svn_wc_operation_switch) &&
      incoming_change == svn_wc_conflict_action_edit &&
      local_change == svn_wc_conflict_reason_moved_away)
    {
      add_resolution_option(
        options, conflict,
        svn_client_conflict_option_update_move_destination,
        _("Update move destination"),
        _("apply incoming changes to move destination"),
        resolve_update_moved_away_node);
    }

  return SVN_NO_ERROR;
}

/* Configure 'update raise moved away children' resolution option for a tree
 * conflict. */
static svn_error_t *
configure_option_update_raise_moved_away_children(
  svn_client_conflict_t *conflict,
  apr_array_header_t *options)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  svn_node_kind_t victim_node_kind;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);

  if ((operation == svn_wc_operation_update ||
       operation == svn_wc_operation_switch) &&
      incoming_change == svn_wc_conflict_action_edit &&
      (local_change == svn_wc_conflict_reason_deleted ||
       local_change == svn_wc_conflict_reason_replaced) &&
      victim_node_kind == svn_node_dir)
    {
      add_resolution_option(
        options, conflict,
        svn_client_conflict_option_update_any_moved_away_children,
        _("Update any moved-away children"),
        _("prepare for updating moved-away children, if any"),
        resolve_update_raise_moved_away);
    }

  return SVN_NO_ERROR;
}

/* Configure 'incoming add ignore' resolution option for a tree conflict. */
static svn_error_t *
configure_option_incoming_add_ignore(svn_client_conflict_t *conflict,
                                     svn_client_ctx_t *ctx,
                                     apr_array_header_t *options,
                                     apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t victim_node_kind;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  /* This option is only available for directories. */
  if (victim_node_kind == svn_node_dir &&
      incoming_change == svn_wc_conflict_action_add &&
      (local_change == svn_wc_conflict_reason_obstructed ||
       local_change == svn_wc_conflict_reason_added))
    {
      const char *description;
      const char *wcroot_abspath;

      SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                                 conflict->local_abspath, scratch_pool,
                                 scratch_pool));
      if (operation == svn_wc_operation_merge)
        description =
          apr_psprintf(scratch_pool,
                       _("ignore and do not add '^/%s@%ld' here"),
                       incoming_new_repos_relpath, incoming_new_pegrev);
      else if (operation == svn_wc_operation_update ||
               operation == svn_wc_operation_switch)
        {
          if (victim_node_kind == svn_node_file)
            description =
              apr_psprintf(scratch_pool,
                           _("replace '^/%s@%ld' with the locally added file"),
                           incoming_new_repos_relpath, incoming_new_pegrev);
          else if (victim_node_kind == svn_node_dir)
            description =
              apr_psprintf(scratch_pool,
                           _("replace '^/%s@%ld' with the locally added "
                             "directory"),
                           incoming_new_repos_relpath, incoming_new_pegrev);
          else
            description =
              apr_psprintf(scratch_pool,
                           _("replace '^/%s@%ld' with the locally added item"),
                           incoming_new_repos_relpath, incoming_new_pegrev);
        }
      else
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("unexpected operation code '%d'"),
                                 operation);
      add_resolution_option(
        options, conflict, svn_client_conflict_option_incoming_add_ignore,
        _("Ignore incoming addition"), description, resolve_incoming_add_ignore);
    }

  return SVN_NO_ERROR;
}

/* Configure 'incoming added file text merge' resolution option for a tree
 * conflict. */
static svn_error_t *
configure_option_incoming_added_file_text_merge(svn_client_conflict_t *conflict,
                                                svn_client_ctx_t *ctx,
                                                apr_array_header_t *options,
                                                apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  svn_node_kind_t victim_node_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  if (victim_node_kind == svn_node_file &&
      incoming_new_kind == svn_node_file &&
      incoming_change == svn_wc_conflict_action_add &&
      (local_change == svn_wc_conflict_reason_obstructed ||
       local_change == svn_wc_conflict_reason_unversioned ||
       local_change == svn_wc_conflict_reason_added))
    {
      const char *description;
      const char *wcroot_abspath;

      SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                                 conflict->local_abspath, scratch_pool,
                                 scratch_pool));

      if (operation == svn_wc_operation_merge)
        description =
          apr_psprintf(scratch_pool, _("merge '^/%s@%ld' into '%s'"),
            incoming_new_repos_relpath, incoming_new_pegrev,
            svn_dirent_local_style(
              svn_dirent_skip_ancestor(wcroot_abspath,
                                       conflict->local_abspath),
              scratch_pool));
      else
        description =
          apr_psprintf(scratch_pool, _("merge local '%s' and '^/%s@%ld'"),
            svn_dirent_local_style(
              svn_dirent_skip_ancestor(wcroot_abspath,
                                       conflict->local_abspath),
              scratch_pool),
            incoming_new_repos_relpath, incoming_new_pegrev);

      add_resolution_option(
        options, conflict,
        svn_client_conflict_option_incoming_added_file_text_merge,
        _("Merge the files"), description,
        operation == svn_wc_operation_merge
          ? resolve_merge_incoming_added_file_text_merge
          : resolve_merge_incoming_added_file_text_update);
    }

  return SVN_NO_ERROR;
}

/* Configure 'incoming added file replace and merge' resolution option for a
 * tree conflict. */
static svn_error_t *
configure_option_incoming_added_file_replace_and_merge(
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_array_header_t *options,
  apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  svn_node_kind_t victim_node_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  if (operation == svn_wc_operation_merge &&
      victim_node_kind == svn_node_file &&
      incoming_new_kind == svn_node_file &&
      incoming_change == svn_wc_conflict_action_add &&
      local_change == svn_wc_conflict_reason_obstructed)
    {
      const char *wcroot_abspath;
      const char *description;

      SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                                 conflict->local_abspath, scratch_pool,
                                 scratch_pool));
      description =
        apr_psprintf(scratch_pool,
          _("delete '%s', copy '^/%s@%ld' here, and merge the files"),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath,
                                     conflict->local_abspath),
            scratch_pool),
          incoming_new_repos_relpath, incoming_new_pegrev);

      add_resolution_option(
        options, conflict,
        svn_client_conflict_option_incoming_added_file_replace_and_merge,
        _("Replace and merge"),
        description, resolve_merge_incoming_added_file_replace_and_merge);
    }

  return SVN_NO_ERROR;
}

/* Configure 'incoming added dir merge' resolution option for a tree
 * conflict. */
static svn_error_t *
configure_option_incoming_added_dir_merge(svn_client_conflict_t *conflict,
                                          svn_client_ctx_t *ctx,
                                          apr_array_header_t *options,
                                          apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  svn_node_kind_t victim_node_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  if (victim_node_kind == svn_node_dir &&
      incoming_new_kind == svn_node_dir &&
      incoming_change == svn_wc_conflict_action_add &&
      (local_change == svn_wc_conflict_reason_added ||
       (operation == svn_wc_operation_merge &&
        local_change == svn_wc_conflict_reason_obstructed) ||
       (operation != svn_wc_operation_merge &&
        local_change == svn_wc_conflict_reason_unversioned)))
    {
      const char *description;
      const char *wcroot_abspath;

      SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                                 conflict->local_abspath, scratch_pool,
                                 scratch_pool));
      if (operation == svn_wc_operation_merge)
        {
          if (conflict->tree_conflict_incoming_details == NULL)
            return SVN_NO_ERROR;

          description =
            apr_psprintf(scratch_pool, _("merge '^/%s@%ld' into '%s'"),
              incoming_new_repos_relpath, incoming_new_pegrev,
              svn_dirent_local_style(
                svn_dirent_skip_ancestor(wcroot_abspath,
                                         conflict->local_abspath),
                scratch_pool));
        }
      else
        description =
          apr_psprintf(scratch_pool, _("merge local '%s' and '^/%s@%ld'"),
            svn_dirent_local_style(
              svn_dirent_skip_ancestor(wcroot_abspath,
                                       conflict->local_abspath),
              scratch_pool),
            incoming_new_repos_relpath, incoming_new_pegrev);

      add_resolution_option(options, conflict,
                            svn_client_conflict_option_incoming_added_dir_merge,
                            _("Merge the directories"), description,
                            operation == svn_wc_operation_merge
                              ? resolve_merge_incoming_added_dir_merge
                              : resolve_update_incoming_added_dir_merge);
    }

  return SVN_NO_ERROR;
}

/* Configure 'incoming added dir replace' resolution option for a tree
 * conflict. */
static svn_error_t *
configure_option_incoming_added_dir_replace(svn_client_conflict_t *conflict,
                                            svn_client_ctx_t *ctx,
                                            apr_array_header_t *options,
                                            apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  svn_node_kind_t victim_node_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  if (operation == svn_wc_operation_merge &&
      victim_node_kind == svn_node_dir &&
      incoming_new_kind == svn_node_dir &&
      incoming_change == svn_wc_conflict_action_add &&
      local_change == svn_wc_conflict_reason_obstructed)
    {
      const char *description;
      const char *wcroot_abspath;

      SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                                 conflict->local_abspath, scratch_pool,
                                 scratch_pool));
      description =
        apr_psprintf(scratch_pool, _("delete '%s' and copy '^/%s@%ld' here"),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath,
                                     conflict->local_abspath),
            scratch_pool),
          incoming_new_repos_relpath, incoming_new_pegrev);
      add_resolution_option(
        options, conflict,
        svn_client_conflict_option_incoming_added_dir_replace,
        _("Delete my directory and replace it with incoming directory"),
        description, resolve_merge_incoming_added_dir_replace);
    }

  return SVN_NO_ERROR;
}

/* Configure 'incoming added dir replace and merge' resolution option
 * for a tree conflict. */
static svn_error_t *
configure_option_incoming_added_dir_replace_and_merge(
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_array_header_t *options,
  apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  svn_node_kind_t victim_node_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  if (operation == svn_wc_operation_merge &&
      victim_node_kind == svn_node_dir &&
      incoming_new_kind == svn_node_dir &&
      incoming_change == svn_wc_conflict_action_add &&
      local_change == svn_wc_conflict_reason_obstructed)
    {
      const char *description;
      const char *wcroot_abspath;

      SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                                 conflict->local_abspath, scratch_pool,
                                 scratch_pool));
      description =
        apr_psprintf(scratch_pool,
          _("delete '%s', copy '^/%s@%ld' here, and merge the directories"),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath,
                                     conflict->local_abspath),
            scratch_pool),
          incoming_new_repos_relpath, incoming_new_pegrev);

      add_resolution_option(
        options, conflict,
        svn_client_conflict_option_incoming_added_dir_replace_and_merge,
        _("Replace and merge"),
        description, resolve_merge_incoming_added_dir_replace_and_merge);
    }

  return SVN_NO_ERROR;
}

/* Configure 'incoming delete ignore' resolution option for a tree conflict. */
static svn_error_t *
configure_option_incoming_delete_ignore(svn_client_conflict_t *conflict,
                                        svn_client_ctx_t *ctx,
                                        apr_array_header_t *options,
                                        apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  if (incoming_change == svn_wc_conflict_action_delete)
    {
      const char *description;
      struct conflict_tree_incoming_delete_details *incoming_details;
      svn_boolean_t is_incoming_move;

      incoming_details = conflict->tree_conflict_incoming_details;
      is_incoming_move = (incoming_details != NULL &&
                          incoming_details->moves != NULL);
      if (local_change == svn_wc_conflict_reason_moved_away ||
          local_change == svn_wc_conflict_reason_edited)
        {
          /* An option which ignores the incoming deletion makes no sense
           * if we know there was a local move and/or an incoming move. */
          if (is_incoming_move)
            return SVN_NO_ERROR;
        }
      else if (local_change == svn_wc_conflict_reason_deleted)
        {
          /* If the local item was deleted and conflict details were fetched
           * and indicate that there was no move, then this is an actual
           * 'delete vs delete' situation. An option which ignores the incoming
           * deletion makes no sense in that case because there is no local
           * node to preserve. */
          if (!is_incoming_move)
            return SVN_NO_ERROR;
        }
      else if (local_change == svn_wc_conflict_reason_missing &&
               operation == svn_wc_operation_merge)
        {
          struct conflict_tree_local_missing_details *local_details;
          svn_boolean_t is_local_move; /* "local" to branch history */

          local_details = conflict->tree_conflict_local_details;
          is_local_move = (local_details != NULL &&
                           local_details->moves != NULL);

          if (is_incoming_move || is_local_move)
            return SVN_NO_ERROR;
        }

      description =
        apr_psprintf(scratch_pool, _("ignore the deletion of '^/%s@%ld'"),
          incoming_new_repos_relpath, incoming_new_pegrev);

      add_resolution_option(options, conflict,
                            svn_client_conflict_option_incoming_delete_ignore,
                            _("Ignore incoming deletion"), description,
                            resolve_incoming_delete_ignore);
    }

  return SVN_NO_ERROR;
}

/* Configure 'incoming delete accept' resolution option for a tree conflict. */
static svn_error_t *
configure_option_incoming_delete_accept(svn_client_conflict_t *conflict,
                                        svn_client_ctx_t *ctx,
                                        apr_array_header_t *options,
                                        apr_pool_t *scratch_pool)
{
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;

  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  if (incoming_change == svn_wc_conflict_action_delete)
    {
      struct conflict_tree_incoming_delete_details *incoming_details;
      svn_boolean_t is_incoming_move;

      incoming_details = conflict->tree_conflict_incoming_details;
      is_incoming_move = (incoming_details != NULL &&
                          incoming_details->moves != NULL);
      if (is_incoming_move &&
          (local_change == svn_wc_conflict_reason_edited ||
          local_change == svn_wc_conflict_reason_moved_away ||
          local_change == svn_wc_conflict_reason_missing))
        {
          /* An option which accepts the incoming deletion makes no sense
           * if we know there was a local move and/or an incoming move. */
          return SVN_NO_ERROR;
        }
      else
        {
          const char *description;
          const char *wcroot_abspath;
          const char *local_abspath;

          SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                                     conflict->local_abspath, scratch_pool,
                                     scratch_pool));
          local_abspath = svn_client_conflict_get_local_abspath(conflict);
          description =
            apr_psprintf(scratch_pool, _("accept the deletion of '%s'"),
              svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath,
                                                              local_abspath),
                                     scratch_pool));
          add_resolution_option(
            options, conflict,
            svn_client_conflict_option_incoming_delete_accept,
            _("Accept incoming deletion"), description,
            resolve_incoming_delete_accept);
        }
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
describe_incoming_move_merge_conflict_option(
  const char **description,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  const char *moved_to_abspath,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  const char *victim_abspath;
  svn_node_kind_t victim_node_kind;
  const char *wcroot_abspath;

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                             victim_abspath, scratch_pool,
                             scratch_pool));

  operation = svn_client_conflict_get_operation(conflict);
  if (operation == svn_wc_operation_merge)
    {
      const char *incoming_moved_abspath = NULL;

      if (victim_node_kind == svn_node_none)
        {
          /* This is an incoming move vs local move conflict. */
          struct conflict_tree_incoming_delete_details *details;

          details = conflict->tree_conflict_incoming_details;
          if (details->wc_move_targets &&
              details->move_target_repos_relpath)
            {
              apr_array_header_t *moves;

              moves = svn_hash_gets(details->wc_move_targets,
                                    details->move_target_repos_relpath);
              incoming_moved_abspath =
                APR_ARRAY_IDX(moves, details->wc_move_target_idx,
                              const char *);
            }
        }

      if (incoming_moved_abspath)
        {
          /* The 'move and merge' option follows the incoming move; note that
           * moved_to_abspath points to the current location of an item which
           * was moved in the history of our merge target branch. If the user
           * chooses 'move and merge', that item will be moved again (i.e. it
           * will be moved to and merged with incoming_moved_abspath's item). */
          *description =
            apr_psprintf(
              result_pool, _("move '%s' to '%s' and merge"),
              svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath,
                                                              moved_to_abspath),
                                     scratch_pool),
              svn_dirent_local_style(svn_dirent_skip_ancestor(
                                       wcroot_abspath,
                                       incoming_moved_abspath),
                                     scratch_pool));
        }
      else
        {
          *description =
            apr_psprintf(
              result_pool, _("move '%s' to '%s' and merge"),
              svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath,
                                                              victim_abspath),
                                     scratch_pool),
              svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath,
                                                              moved_to_abspath),
                                     scratch_pool));
        }
    }
  else
    *description =
      apr_psprintf(
        result_pool, _("move and merge local changes from '%s' into '%s'"),
        svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath,
                                                        victim_abspath),
                               scratch_pool),
        svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath,
                                                        moved_to_abspath),
                               scratch_pool));

  return SVN_NO_ERROR;
}

/* Configure 'incoming move file merge' resolution option for
 * a tree conflict. */
static svn_error_t *
configure_option_incoming_move_file_merge(svn_client_conflict_t *conflict,
                                          svn_client_ctx_t *ctx,
                                          apr_array_header_t *options,
                                          apr_pool_t *scratch_pool)
{
  svn_node_kind_t victim_node_kind;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  svn_node_kind_t incoming_old_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;

  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            &incoming_old_kind, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  if (victim_node_kind == svn_node_file &&
      incoming_old_kind == svn_node_file &&
      incoming_new_kind == svn_node_none &&
      incoming_change == svn_wc_conflict_action_delete &&
      local_change == svn_wc_conflict_reason_edited)
    {
      struct conflict_tree_incoming_delete_details *details;
      const char *description;
      apr_array_header_t *move_target_wc_abspaths;
      const char *moved_to_abspath;

      details = conflict->tree_conflict_incoming_details;
      if (details == NULL || details->moves == NULL)
        return SVN_NO_ERROR;

      if (apr_hash_count(details->wc_move_targets) == 0)
        return SVN_NO_ERROR;

      move_target_wc_abspaths =
        svn_hash_gets(details->wc_move_targets,
                      get_moved_to_repos_relpath(details, scratch_pool));
      moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths,
                                       details->wc_move_target_idx,
                                       const char *);
      SVN_ERR(describe_incoming_move_merge_conflict_option(&description,
                                                           conflict, ctx,
                                                           moved_to_abspath,
                                                           scratch_pool,
                                                           scratch_pool));
      add_resolution_option(
        options, conflict,
        svn_client_conflict_option_incoming_move_file_text_merge,
        _("Move and merge"), description,
        resolve_incoming_move_file_text_merge);
    }

  return SVN_NO_ERROR;
}

/* Configure 'incoming move dir merge' resolution option for
 * a tree conflict. */
static svn_error_t *
configure_option_incoming_dir_merge(svn_client_conflict_t *conflict,
                                    svn_client_ctx_t *ctx,
                                    apr_array_header_t *options,
                                    apr_pool_t *scratch_pool)
{
  svn_node_kind_t victim_node_kind;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  svn_node_kind_t incoming_old_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;

  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            &incoming_old_kind, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  if (victim_node_kind == svn_node_dir &&
      incoming_old_kind == svn_node_dir &&
      incoming_new_kind == svn_node_none &&
      incoming_change == svn_wc_conflict_action_delete &&
      local_change == svn_wc_conflict_reason_edited)
    {
      struct conflict_tree_incoming_delete_details *details;
      const char *description;
      apr_array_header_t *move_target_wc_abspaths;
      const char *moved_to_abspath;

      details = conflict->tree_conflict_incoming_details;
      if (details == NULL || details->moves == NULL)
        return SVN_NO_ERROR;

      if (apr_hash_count(details->wc_move_targets) == 0)
        return SVN_NO_ERROR;

      move_target_wc_abspaths =
        svn_hash_gets(details->wc_move_targets,
                      get_moved_to_repos_relpath(details, scratch_pool));
      moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths,
                                       details->wc_move_target_idx,
                                       const char *);
      SVN_ERR(describe_incoming_move_merge_conflict_option(&description,
                                                           conflict, ctx,
                                                           moved_to_abspath,
                                                           scratch_pool,
                                                           scratch_pool));
      add_resolution_option(options, conflict,
                            svn_client_conflict_option_incoming_move_dir_merge,
                            _("Move and merge"), description,
                            resolve_incoming_move_dir_merge);
    }

  return SVN_NO_ERROR;
}

/* Configure 'local move file merge' resolution option for
 * a tree conflict. */
static svn_error_t *
configure_option_local_move_file_or_dir_merge(
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_array_header_t *options,
  apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  svn_node_kind_t incoming_old_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            &incoming_old_kind, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  if (operation == svn_wc_operation_merge &&
      incoming_change == svn_wc_conflict_action_edit &&
      local_change == svn_wc_conflict_reason_missing)
    {
      struct conflict_tree_local_missing_details *details;
      const char *wcroot_abspath;

      SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                                 conflict->local_abspath,
                                 scratch_pool, scratch_pool));

      details = conflict->tree_conflict_local_details;
      if (details != NULL && details->moves != NULL &&
          details->move_target_repos_relpath != NULL)
        {
          apr_array_header_t *moves;
          const char *moved_to_abspath;
          const char *description;

          moves = svn_hash_gets(details->wc_move_targets,
                                details->move_target_repos_relpath);
          moved_to_abspath =
            APR_ARRAY_IDX(moves, details->wc_move_target_idx, const char *);

          description =
            apr_psprintf(
              scratch_pool, _("apply changes to move destination '%s'"),
              svn_dirent_local_style(
                svn_dirent_skip_ancestor(wcroot_abspath, moved_to_abspath),
                scratch_pool));

          if ((incoming_old_kind == svn_node_file ||
               incoming_old_kind == svn_node_none) &&
              (incoming_new_kind == svn_node_file ||
               incoming_new_kind == svn_node_none))
            {
              add_resolution_option(
                options, conflict,
                svn_client_conflict_option_local_move_file_text_merge,
                _("Apply to move destination"),
                description, resolve_local_move_file_merge);
            }
          else
            {
              add_resolution_option(
                options, conflict,
                svn_client_conflict_option_local_move_dir_merge,
                _("Apply to move destination"),
                description, resolve_local_move_dir_merge);
            }
        }
    }

  return SVN_NO_ERROR;
}

/* Configure 'sibling move file/dir merge' resolution option for
 * a tree conflict. */
static svn_error_t *
configure_option_sibling_move_merge(svn_client_conflict_t *conflict,
                                    svn_client_ctx_t *ctx,
                                    apr_array_header_t *options,
                                    apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  svn_node_kind_t incoming_old_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            &incoming_old_kind, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  if (operation == svn_wc_operation_merge &&
      incoming_change == svn_wc_conflict_action_edit &&
      local_change == svn_wc_conflict_reason_missing)
    {
      struct conflict_tree_local_missing_details *details;
      const char *wcroot_abspath;

      SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                                 conflict->local_abspath,
                                 scratch_pool, scratch_pool));

      details = conflict->tree_conflict_local_details;
      if (details != NULL && details->wc_siblings != NULL)
        {
          const char *description;
          const char *sibling;

          sibling =
            apr_pstrdup(conflict->pool,
                        APR_ARRAY_IDX(details->wc_siblings,
                                      details->preferred_sibling_idx,
                                      const char *));
          description =
            apr_psprintf(
              scratch_pool, _("apply changes to '%s'"),
              svn_dirent_local_style(
                svn_dirent_skip_ancestor(wcroot_abspath, sibling),
                scratch_pool));

          if ((incoming_old_kind == svn_node_file ||
               incoming_old_kind == svn_node_none) &&
              (incoming_new_kind == svn_node_file ||
               incoming_new_kind == svn_node_none))
            {
              add_resolution_option(
                options, conflict,
                svn_client_conflict_option_sibling_move_file_text_merge,
                _("Apply to corresponding local location"),
                description, resolve_local_move_file_merge);
            }
          else
            {
              add_resolution_option(
                options, conflict,
                svn_client_conflict_option_sibling_move_dir_merge,
                _("Apply to corresponding local location"),
                description, resolve_local_move_dir_merge);
            }
        }
    }

  return SVN_NO_ERROR;
}

struct conflict_tree_update_local_moved_away_details {
  /*
   * This array consists of "const char *" absolute paths to working copy
   * nodes which are uncomitted copies and correspond to the repository path
   * of the conflict victim.
   * Each such working copy node is a potential local move target which can
   * be chosen to find a suitable merge target when resolving a tree conflict.
   *
   * This may be an empty array in case if there is no move target path in
   * the working copy. */
  apr_array_header_t *wc_move_targets;

  /* Current index into the list of working copy paths in WC_MOVE_TARGETS. */
  int preferred_move_target_idx;
};

/* Implements conflict_option_resolve_func_t.
 * Resolve an incoming move vs local move conflict by merging from the
 * incoming move's target location to the local move's target location,
 * overriding the incoming move. The original local move was broken during
 * update/switch, so overriding the incoming move involves recording a new
 * move from the incoming move's target location to the local move's target
 * location. */
static svn_error_t *
resolve_both_moved_file_update_keep_local_move(
  svn_client_conflict_option_t *option,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *victim_abspath;
  const char *local_moved_to_abspath;
  svn_wc_operation_t operation;
  const char *lock_abspath;
  svn_error_t *err;
  const char *repos_root_url;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  const char *wc_tmpdir;
  const char *ancestor_abspath;
  svn_stream_t *ancestor_stream;
  apr_hash_t *ancestor_props;
  apr_hash_t *incoming_props;
  apr_hash_t *local_props;
  const char *ancestor_url;
  const char *corrected_url;
  svn_ra_session_t *ra_session;
  svn_wc_merge_outcome_t merge_content_outcome;
  svn_wc_notify_state_t merge_props_outcome;
  apr_array_header_t *propdiffs;
  struct conflict_tree_incoming_delete_details *incoming_details;
  apr_array_header_t *possible_moved_to_abspaths;
  const char *incoming_moved_to_abspath;
  struct conflict_tree_update_local_moved_away_details *local_details;

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  incoming_details = conflict->tree_conflict_incoming_details;
  if (incoming_details == NULL || incoming_details->moves == NULL)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("The specified conflict resolution option "
                               "requires details for tree conflict at '%s' "
                               "to be fetched from the repository first."),
                            svn_dirent_local_style(victim_abspath,
                                                   scratch_pool));
  if (operation == svn_wc_operation_none)
    return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL,
                             _("Invalid operation code '%d' recorded for "
                               "conflict at '%s'"), operation,
                             svn_dirent_local_style(victim_abspath,
                                                    scratch_pool));

  option_id = svn_client_conflict_option_get_id(option);
  SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_file_merge);

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  /* Set up temporary storage for the common ancestor version of the file. */
  SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, victim_abspath,
                             scratch_pool, scratch_pool));
  SVN_ERR(svn_stream_open_unique(&ancestor_stream,
                                 &ancestor_abspath, wc_tmpdir,
                                 svn_io_file_del_on_pool_cleanup,
                                 scratch_pool, scratch_pool));

  /* Fetch the ancestor file's content. */
  ancestor_url = svn_path_url_add_component2(repos_root_url,
                                             incoming_old_repos_relpath,
                                             scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               ancestor_url, NULL, NULL,
                                               FALSE, FALSE, ctx,
                                               scratch_pool, scratch_pool));
  SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev,
                          ancestor_stream, NULL, /* fetched_rev */
                          &ancestor_props, scratch_pool));
  filter_props(ancestor_props, scratch_pool);

  /* Close stream to flush ancestor file to disk. */
  SVN_ERR(svn_stream_close(ancestor_stream));

  possible_moved_to_abspaths =
    svn_hash_gets(incoming_details->wc_move_targets,
                  get_moved_to_repos_relpath(incoming_details, scratch_pool));
  incoming_moved_to_abspath =
    APR_ARRAY_IDX(possible_moved_to_abspaths,
                  incoming_details->wc_move_target_idx, const char *);

  local_details = conflict->tree_conflict_local_details;
  local_moved_to_abspath =
    APR_ARRAY_IDX(local_details->wc_move_targets,
                  local_details->preferred_move_target_idx, const char *);

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
            &lock_abspath, ctx->wc_ctx,
            svn_dirent_get_longest_ancestor(victim_abspath,
                                            local_moved_to_abspath,
                                            scratch_pool),
            scratch_pool, scratch_pool));

   /* Get a copy of the incoming moved item's properties. */
  err = svn_wc_prop_list2(&incoming_props, ctx->wc_ctx,
                          incoming_moved_to_abspath,
                          scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Get a copy of the local move target's properties. */
  err = svn_wc_prop_list2(&local_props, ctx->wc_ctx,
                          local_moved_to_abspath,
                          scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Create a property diff for the files. */
  err = svn_prop_diffs(&propdiffs, incoming_props, local_props,
                       scratch_pool);
  if (err)
    goto unlock_wc;

  /* Perform the file merge. */
  err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome,
                      ctx->wc_ctx, ancestor_abspath,
                      incoming_moved_to_abspath, local_moved_to_abspath,
                      NULL, NULL, NULL, /* labels */
                      NULL, NULL, /* conflict versions */
                      FALSE, /* dry run */
                      NULL, NULL, /* diff3_cmd, merge_options */
                      apr_hash_count(ancestor_props) ? ancestor_props : NULL,
                      propdiffs,
                      NULL, NULL, /* conflict func/baton */
                      NULL, NULL, /* don't allow user to cancel here */
                      scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      /* Tell the world about the file merge that just happened. */
      notify = svn_wc_create_notify(local_moved_to_abspath,
                                    svn_wc_notify_update_update,
                                    scratch_pool);
      if (merge_content_outcome == svn_wc_merge_conflict)
        notify->content_state = svn_wc_notify_state_conflicted;
      else
        notify->content_state = svn_wc_notify_state_merged;
      notify->prop_state = merge_props_outcome;
      notify->kind = svn_node_file;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  /* Record a new move which overrides the incoming move. */
  err = svn_wc__move2(ctx->wc_ctx, incoming_moved_to_abspath,
                      local_moved_to_abspath,
                      TRUE, /* meta-data only move */
                      FALSE, /* mixed-revisions don't apply to files */
                      NULL, NULL, /* don't allow user to cancel here */
                      NULL, NULL, /* no extra notification */
                      scratch_pool);
  if (err)
    goto unlock_wc;

  /* Remove moved-away file from disk. */
  err = svn_io_remove_file2(incoming_moved_to_abspath, TRUE, scratch_pool);
  if (err)
    goto unlock_wc;

  err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  svn_io_sleep_for_timestamps(local_moved_to_abspath, scratch_pool);

  conflict->resolution_tree = option_id;

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  return SVN_NO_ERROR;
}

/* Implements conflict_option_resolve_func_t.
 * Resolve an incoming move vs local move conflict by merging from the
 * local move's target location to the incoming move's target location,
 * and reverting the local move. */
static svn_error_t *
resolve_both_moved_file_update_keep_incoming_move(
  svn_client_conflict_option_t *option,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  svn_client_conflict_option_id_t option_id;
  const char *victim_abspath;
  const char *local_moved_to_abspath;
  svn_wc_operation_t operation;
  const char *lock_abspath;
  svn_error_t *err;
  const char *repos_root_url;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  const char *wc_tmpdir;
  const char *ancestor_abspath;
  svn_stream_t *ancestor_stream;
  apr_hash_t *ancestor_props;
  apr_hash_t *incoming_props;
  apr_hash_t *local_props;
  const char *ancestor_url;
  const char *corrected_url;
  svn_ra_session_t *ra_session;
  svn_wc_merge_outcome_t merge_content_outcome;
  svn_wc_notify_state_t merge_props_outcome;
  apr_array_header_t *propdiffs;
  struct conflict_tree_incoming_delete_details *incoming_details;
  apr_array_header_t *possible_moved_to_abspaths;
  const char *incoming_moved_to_abspath;
  struct conflict_tree_update_local_moved_away_details *local_details;

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  incoming_details = conflict->tree_conflict_incoming_details;
  if (incoming_details == NULL || incoming_details->moves == NULL)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("The specified conflict resolution option "
                               "requires details for tree conflict at '%s' "
                               "to be fetched from the repository first."),
                            svn_dirent_local_style(victim_abspath,
                                                   scratch_pool));
  if (operation == svn_wc_operation_none)
    return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL,
                             _("Invalid operation code '%d' recorded for "
                               "conflict at '%s'"), operation,
                             svn_dirent_local_style(victim_abspath,
                                                    scratch_pool));

  option_id = svn_client_conflict_option_get_id(option);
  SVN_ERR_ASSERT(option_id ==
                 svn_client_conflict_option_both_moved_file_move_merge);

  SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL,
                                             conflict, scratch_pool,
                                             scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            NULL, conflict, scratch_pool,
            scratch_pool));

  /* Set up temporary storage for the common ancestor version of the file. */
  SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, victim_abspath,
                             scratch_pool, scratch_pool));
  SVN_ERR(svn_stream_open_unique(&ancestor_stream,
                                 &ancestor_abspath, wc_tmpdir,
                                 svn_io_file_del_on_pool_cleanup,
                                 scratch_pool, scratch_pool));

  /* Fetch the ancestor file's content. */
  ancestor_url = svn_path_url_add_component2(repos_root_url,
                                             incoming_old_repos_relpath,
                                             scratch_pool);
  SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
                                               ancestor_url, NULL, NULL,
                                               FALSE, FALSE, ctx,
                                               scratch_pool, scratch_pool));
  SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev,
                          ancestor_stream, NULL, /* fetched_rev */
                          &ancestor_props, scratch_pool));
  filter_props(ancestor_props, scratch_pool);

  /* Close stream to flush ancestor file to disk. */
  SVN_ERR(svn_stream_close(ancestor_stream));

  possible_moved_to_abspaths =
    svn_hash_gets(incoming_details->wc_move_targets,
                  get_moved_to_repos_relpath(incoming_details, scratch_pool));
  incoming_moved_to_abspath =
    APR_ARRAY_IDX(possible_moved_to_abspaths,
                  incoming_details->wc_move_target_idx, const char *);

  local_details = conflict->tree_conflict_local_details;
  local_moved_to_abspath =
    APR_ARRAY_IDX(local_details->wc_move_targets,
                  local_details->preferred_move_target_idx, const char *);

  /* ### The following WC modifications should be atomic. */
  SVN_ERR(svn_wc__acquire_write_lock_for_resolve(
            &lock_abspath, ctx->wc_ctx,
            svn_dirent_get_longest_ancestor(victim_abspath,
                                            local_moved_to_abspath,
                                            scratch_pool),
            scratch_pool, scratch_pool));

   /* Get a copy of the incoming moved item's properties. */
  err = svn_wc_prop_list2(&incoming_props, ctx->wc_ctx,
                          incoming_moved_to_abspath,
                          scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Get a copy of the local move target's properties. */
  err = svn_wc_prop_list2(&local_props, ctx->wc_ctx,
                          local_moved_to_abspath,
                          scratch_pool, scratch_pool);
  if (err)
    goto unlock_wc;

  /* Create a property diff for the files. */
  err = svn_prop_diffs(&propdiffs, incoming_props, local_props,
                       scratch_pool);
  if (err)
    goto unlock_wc;

  /* Perform the file merge. */
  err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome,
                      ctx->wc_ctx, ancestor_abspath,
                      local_moved_to_abspath, incoming_moved_to_abspath,
                      NULL, NULL, NULL, /* labels */
                      NULL, NULL, /* conflict versions */
                      FALSE, /* dry run */
                      NULL, NULL, /* diff3_cmd, merge_options */
                      apr_hash_count(ancestor_props) ? ancestor_props : NULL,
                      propdiffs,
                      NULL, NULL, /* conflict func/baton */
                      NULL, NULL, /* don't allow user to cancel here */
                      scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      /* Tell the world about the file merge that just happened. */
      notify = svn_wc_create_notify(local_moved_to_abspath,
                                    svn_wc_notify_update_update,
                                    scratch_pool);
      if (merge_content_outcome == svn_wc_merge_conflict)
        notify->content_state = svn_wc_notify_state_conflicted;
      else
        notify->content_state = svn_wc_notify_state_merged;
      notify->prop_state = merge_props_outcome;
      notify->kind = svn_node_file;
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  /* Revert the copy-half of the local move. The delete-half of this move
   * has already been deleted during the update/switch operation. */
  err = svn_wc_revert6(ctx->wc_ctx, local_moved_to_abspath, svn_depth_empty,
                       FALSE, NULL, TRUE, FALSE,
                       TRUE /*added_keep_local*/,
                       NULL, NULL, /* no cancellation */
                       ctx->notify_func2, ctx->notify_baton2,
                       scratch_pool);
  if (err)
    goto unlock_wc;

  err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool);
  if (err)
    goto unlock_wc;

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree,
                                    scratch_pool);
      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  svn_io_sleep_for_timestamps(local_moved_to_abspath, scratch_pool);

  conflict->resolution_tree = option_id;

unlock_wc:
  err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
                                                                 lock_abspath,
                                                                 scratch_pool));
  SVN_ERR(err);

  return SVN_NO_ERROR;
}

/* Implements tree_conflict_get_details_func_t. */
static svn_error_t *
conflict_tree_get_details_update_local_moved_away(
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  struct conflict_tree_update_local_moved_away_details *details;
  const char *incoming_old_repos_relpath;
  svn_node_kind_t incoming_old_kind;

  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, NULL, &incoming_old_kind,
            conflict, scratch_pool, scratch_pool));

  details = apr_pcalloc(conflict->pool, sizeof(*details));

  details->wc_move_targets = apr_array_make(conflict->pool, 1,
                                            sizeof(const char *));

  /* Search the WC for copies of the conflict victim. */
  SVN_ERR(svn_wc__find_copies_of_repos_path(&details->wc_move_targets,
                                            conflict->local_abspath,
                                            incoming_old_repos_relpath,
                                            incoming_old_kind,
                                            ctx->wc_ctx,
                                            conflict->pool,
                                            scratch_pool));

  conflict->tree_conflict_local_details = details;

  return SVN_NO_ERROR;
}

static svn_error_t *
get_both_moved_file_paths(const char **incoming_moved_to_abspath,
                          const char **local_moved_to_abspath,
                          svn_client_conflict_t *conflict,
                          apr_pool_t *scratch_pool)
{
  struct conflict_tree_incoming_delete_details *incoming_details;
  apr_array_header_t *incoming_move_target_wc_abspaths;
  svn_wc_operation_t operation;

  operation = svn_client_conflict_get_operation(conflict);

  *incoming_moved_to_abspath = NULL;
  *local_moved_to_abspath = NULL;

  incoming_details = conflict->tree_conflict_incoming_details;
  if (incoming_details == NULL || incoming_details->moves == NULL ||
      apr_hash_count(incoming_details->wc_move_targets) == 0)
          return SVN_NO_ERROR;

  incoming_move_target_wc_abspaths =
    svn_hash_gets(incoming_details->wc_move_targets,
                  get_moved_to_repos_relpath(incoming_details,
                                             scratch_pool));
  *incoming_moved_to_abspath =
    APR_ARRAY_IDX(incoming_move_target_wc_abspaths,
                  incoming_details->wc_move_target_idx, const char *);

  if (operation == svn_wc_operation_merge)
    {
      struct conflict_tree_local_missing_details *local_details;
      apr_array_header_t *local_moves;

      local_details = conflict->tree_conflict_local_details;
      if (local_details == NULL ||
          apr_hash_count(local_details->wc_move_targets) == 0)
          return SVN_NO_ERROR;

      local_moves = svn_hash_gets(local_details->wc_move_targets,
                                  local_details->move_target_repos_relpath);
      *local_moved_to_abspath =
        APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx,
                      const char *);
    }
  else
    {
      struct conflict_tree_update_local_moved_away_details *local_details;

      local_details = conflict->tree_conflict_local_details;
      if (local_details == NULL ||
          local_details->wc_move_targets->nelts == 0)
          return SVN_NO_ERROR;

      *local_moved_to_abspath =
        APR_ARRAY_IDX(local_details->wc_move_targets,
                      local_details->preferred_move_target_idx,
                      const char *);
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
conflict_tree_get_description_update_both_moved_file_merge(
  const char **description,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  const char *incoming_moved_to_abspath;
  const char *local_moved_to_abspath;
  svn_wc_operation_t operation;
  const char *wcroot_abspath;

  *description = NULL;

  SVN_ERR(get_both_moved_file_paths(&incoming_moved_to_abspath,
                                    &local_moved_to_abspath,
                                    conflict, scratch_pool));
  if (incoming_moved_to_abspath == NULL || local_moved_to_abspath == NULL)
    return SVN_NO_ERROR;

  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                             conflict->local_abspath, scratch_pool,
                             scratch_pool));

  operation = svn_client_conflict_get_operation(conflict);

  if (operation == svn_wc_operation_merge)
    {
      /* In case of a merge, the incoming move has A+ (copied) status... */
      *description =
        apr_psprintf(
          scratch_pool,
            _("apply changes to '%s' and revert addition of '%s'"),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath),
            scratch_pool),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath),
            scratch_pool));
    }
  else
    {
      /* ...but in case of update/switch the local move has "A+" status. */
      *description =
        apr_psprintf(
          scratch_pool,
          _("override incoming move and merge incoming changes from '%s' "
            "to '%s'"),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath),
            scratch_pool),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath),
            scratch_pool));
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
conflict_tree_get_description_update_both_moved_file_move_merge(
  const char **description,
  svn_client_conflict_t *conflict,
  svn_client_ctx_t *ctx,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  const char *incoming_moved_to_abspath;
  const char *local_moved_to_abspath;
  svn_wc_operation_t operation;
  const char *wcroot_abspath;

  *description = NULL;

  SVN_ERR(get_both_moved_file_paths(&incoming_moved_to_abspath,
                                    &local_moved_to_abspath,
                                    conflict, scratch_pool));
  if (incoming_moved_to_abspath == NULL || local_moved_to_abspath == NULL)
    return SVN_NO_ERROR;

  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                             conflict->local_abspath, scratch_pool,
                             scratch_pool));

  operation = svn_client_conflict_get_operation(conflict);

  if (operation == svn_wc_operation_merge)
    {
      SVN_ERR(describe_incoming_move_merge_conflict_option(
                description, conflict, ctx, local_moved_to_abspath,
                scratch_pool, scratch_pool));
    }
  else
    {
      *description =
        apr_psprintf(
          scratch_pool,
          _("accept incoming move and merge local changes from "
            "'%s' to '%s'"),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath),
            scratch_pool),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath),
            scratch_pool));
    }

  return SVN_NO_ERROR;
}

/* Configure 'both moved file merge' resolution options for a tree conflict. */
static svn_error_t *
configure_option_both_moved_file_merge(svn_client_conflict_t *conflict,
                                       svn_client_ctx_t *ctx,
                                       apr_array_header_t *options,
                                       apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_node_kind_t victim_node_kind;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  svn_node_kind_t incoming_old_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;
  const char *wcroot_abspath;

  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                             conflict->local_abspath, scratch_pool,
                             scratch_pool));

  operation = svn_client_conflict_get_operation(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            &incoming_old_kind, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  /* ### what about the switch operation? */
  if (((operation == svn_wc_operation_merge &&
        victim_node_kind == svn_node_none) ||
       (operation == svn_wc_operation_update &&
         victim_node_kind == svn_node_file)) &&
      incoming_old_kind == svn_node_file &&
      incoming_new_kind == svn_node_none &&
      ((operation == svn_wc_operation_merge &&
       local_change == svn_wc_conflict_reason_missing) ||
       (operation == svn_wc_operation_update &&
       local_change == svn_wc_conflict_reason_moved_away)) &&
      incoming_change == svn_wc_conflict_action_delete)
    {
      const char *description;

      SVN_ERR(conflict_tree_get_description_update_both_moved_file_merge(
                &description, conflict, ctx, conflict->pool, scratch_pool));

      if (description == NULL) /* details not fetched yet */
        return SVN_NO_ERROR;

      add_resolution_option(
        options, conflict, svn_client_conflict_option_both_moved_file_merge,
        _("Merge to corresponding local location"),
        description,
        operation == svn_wc_operation_merge ?
          resolve_both_moved_file_text_merge :
          resolve_both_moved_file_update_keep_local_move);

      SVN_ERR(conflict_tree_get_description_update_both_moved_file_move_merge(
                &description, conflict, ctx, conflict->pool, scratch_pool));

      add_resolution_option(options, conflict,
         svn_client_conflict_option_both_moved_file_move_merge,
        _("Move and merge"), description,
        operation == svn_wc_operation_merge ?
          resolve_incoming_move_file_text_merge :
          resolve_both_moved_file_update_keep_incoming_move);
    }

  return SVN_NO_ERROR;
}

/* Configure 'both moved dir merge' resolution options for a tree conflict. */
static svn_error_t *
configure_option_both_moved_dir_merge(svn_client_conflict_t *conflict,
                                       svn_client_ctx_t *ctx,
                                       apr_array_header_t *options,
                                       apr_pool_t *scratch_pool)
{
  svn_wc_operation_t operation;
  svn_node_kind_t victim_node_kind;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  const char *incoming_old_repos_relpath;
  svn_revnum_t incoming_old_pegrev;
  svn_node_kind_t incoming_old_kind;
  const char *incoming_new_repos_relpath;
  svn_revnum_t incoming_new_pegrev;
  svn_node_kind_t incoming_new_kind;
  const char *wcroot_abspath;

  SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
                             conflict->local_abspath, scratch_pool,
                             scratch_pool));

  operation = svn_client_conflict_get_operation(conflict);
  victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);
  SVN_ERR(svn_client_conflict_get_incoming_old_repos_location(
            &incoming_old_repos_relpath, &incoming_old_pegrev,
            &incoming_old_kind, conflict, scratch_pool,
            scratch_pool));
  SVN_ERR(svn_client_conflict_get_incoming_new_repos_location(
            &incoming_new_repos_relpath, &incoming_new_pegrev,
            &incoming_new_kind, conflict, scratch_pool,
            scratch_pool));

  if (operation == svn_wc_operation_merge &&
      victim_node_kind == svn_node_none &&
      incoming_old_kind == svn_node_dir &&
      incoming_new_kind == svn_node_none &&
      local_change == svn_wc_conflict_reason_missing &&
      incoming_change == svn_wc_conflict_action_delete)
    {
      struct conflict_tree_incoming_delete_details *incoming_details;
      struct conflict_tree_local_missing_details *local_details;
      const char *description;
      apr_array_header_t *local_moves;
      const char *local_moved_to_abspath;
      const char *incoming_moved_to_abspath;
      apr_array_header_t *incoming_move_target_wc_abspaths;

      incoming_details = conflict->tree_conflict_incoming_details;
      if (incoming_details == NULL || incoming_details->moves == NULL ||
          apr_hash_count(incoming_details->wc_move_targets) == 0)
              return SVN_NO_ERROR;

      local_details = conflict->tree_conflict_local_details;
      if (local_details == NULL ||
          apr_hash_count(local_details->wc_move_targets) == 0)
          return SVN_NO_ERROR;

      local_moves = svn_hash_gets(local_details->wc_move_targets,
                                  local_details->move_target_repos_relpath);
      local_moved_to_abspath =
        APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx,
                      const char *);

      incoming_move_target_wc_abspaths =
        svn_hash_gets(incoming_details->wc_move_targets,
                      get_moved_to_repos_relpath(incoming_details,
                                                 scratch_pool));
      incoming_moved_to_abspath =
        APR_ARRAY_IDX(incoming_move_target_wc_abspaths,
                      incoming_details->wc_move_target_idx, const char *);

      description =
        apr_psprintf(
          scratch_pool, _("apply changes to '%s' and revert addition of '%s'"),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath),
            scratch_pool),
          svn_dirent_local_style(
            svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath),
            scratch_pool));
      add_resolution_option(
        options, conflict, svn_client_conflict_option_both_moved_dir_merge,
        _("Merge to corresponding local location"),
        description, resolve_both_moved_dir_merge);

      SVN_ERR(describe_incoming_move_merge_conflict_option(
                &description, conflict, ctx, local_moved_to_abspath,
                scratch_pool, scratch_pool));
      add_resolution_option(options, conflict,
        svn_client_conflict_option_both_moved_dir_move_merge,
        _("Move and merge"), description,
        resolve_both_moved_dir_move_merge);
    }

  return SVN_NO_ERROR;
}

/* Return a copy of the repos replath candidate list. */
static svn_error_t *
get_repos_relpath_candidates(
  apr_array_header_t **possible_moved_to_repos_relpaths,
  apr_hash_t *wc_move_targets,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  apr_array_header_t *sorted_repos_relpaths;
  int i;

  sorted_repos_relpaths = svn_sort__hash(wc_move_targets,
                                         svn_sort_compare_items_as_paths,
                                         scratch_pool);

  *possible_moved_to_repos_relpaths =
    apr_array_make(result_pool, sorted_repos_relpaths->nelts,
                   sizeof (const char *));
  for (i = 0; i < sorted_repos_relpaths->nelts; i++)
    {
      svn_sort__item_t item;
      const char *repos_relpath;

      item = APR_ARRAY_IDX(sorted_repos_relpaths, i, svn_sort__item_t);
      repos_relpath = item.key;
      APR_ARRAY_PUSH(*possible_moved_to_repos_relpaths, const char *) =
        apr_pstrdup(result_pool, repos_relpath);
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_option_get_moved_to_repos_relpath_candidates2(
  apr_array_header_t **possible_moved_to_repos_relpaths,
  svn_client_conflict_option_t *option,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  svn_client_conflict_t *conflict = option->conflict;
  const char *victim_abspath;
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  svn_client_conflict_option_id_t id;

  id = svn_client_conflict_option_get_id(option);
  if (id != svn_client_conflict_option_incoming_move_file_text_merge &&
      id != svn_client_conflict_option_incoming_move_dir_merge &&
      id != svn_client_conflict_option_local_move_file_text_merge &&
      id != svn_client_conflict_option_local_move_dir_merge &&
      id != svn_client_conflict_option_sibling_move_file_text_merge &&
      id != svn_client_conflict_option_sibling_move_dir_merge &&
      id != svn_client_conflict_option_both_moved_file_merge &&
      id != svn_client_conflict_option_both_moved_file_move_merge &&
      id != svn_client_conflict_option_both_moved_dir_merge &&
      id != svn_client_conflict_option_both_moved_dir_move_merge)
    {
      /* We cannot operate on this option. */
      *possible_moved_to_repos_relpaths = NULL;
      return SVN_NO_ERROR;
    }

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);

  if (operation == svn_wc_operation_merge &&
      incoming_change == svn_wc_conflict_action_edit &&
      local_change == svn_wc_conflict_reason_missing)
    {
      struct conflict_tree_local_missing_details *details;

      details = conflict->tree_conflict_local_details;
      if (details == NULL ||
          (details->wc_move_targets == NULL && details->wc_siblings == NULL))
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Getting a list of possible move targets "
                                   "requires details for tree conflict at '%s' "
                                   "to be fetched from the repository first"),
                                svn_dirent_local_style(victim_abspath,
                                                       scratch_pool));

      if (details->wc_move_targets)
        SVN_ERR(get_repos_relpath_candidates(possible_moved_to_repos_relpaths,
                                             details->wc_move_targets,
                                             result_pool, scratch_pool));
      else
        *possible_moved_to_repos_relpaths = NULL;
    }
  else
    {
      struct conflict_tree_incoming_delete_details *details;

      details = conflict->tree_conflict_incoming_details;
      if (details == NULL || details->wc_move_targets == NULL)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Getting a list of possible move targets "
                                   "requires details for tree conflict at '%s' "
                                   "to be fetched from the repository first"),
                                svn_dirent_local_style(victim_abspath,
                                                       scratch_pool));

      SVN_ERR(get_repos_relpath_candidates(possible_moved_to_repos_relpaths,
                                           details->wc_move_targets,
                                           result_pool, scratch_pool));
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_option_get_moved_to_repos_relpath_candidates(
  apr_array_header_t **possible_moved_to_repos_relpaths,
  svn_client_conflict_option_t *option,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  /* The only difference to API version 2 is an assertion failure if
   * an unexpected option is passed.
   * We do not emulate this old behaviour since clients written against
   * the previous API will just keep working. */
  return svn_error_trace(
    svn_client_conflict_option_get_moved_to_repos_relpath_candidates2(
      possible_moved_to_repos_relpaths, option, result_pool, scratch_pool));
}

static svn_error_t *
set_wc_move_target(const char **new_hash_key,
                   apr_hash_t *wc_move_targets,
                   int preferred_move_target_idx,
                   const char *victim_abspath,
                   apr_pool_t *scratch_pool)
{
  apr_array_header_t *move_target_repos_relpaths;
  svn_sort__item_t item;
  const char *move_target_repos_relpath;
  apr_hash_index_t *hi;

  if (preferred_move_target_idx < 0 ||
      preferred_move_target_idx >= apr_hash_count(wc_move_targets))
    return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                             _("Index '%d' is out of bounds of the possible "
                               "move target list for '%s'"),
                            preferred_move_target_idx,
                            svn_dirent_local_style(victim_abspath,
                                                   scratch_pool));

  /* Translate the index back into a hash table key. */
  move_target_repos_relpaths = svn_sort__hash(wc_move_targets,
                                              svn_sort_compare_items_as_paths,
                                              scratch_pool);
  item = APR_ARRAY_IDX(move_target_repos_relpaths, preferred_move_target_idx,
                       svn_sort__item_t);
  move_target_repos_relpath = item.key;
  /* Find our copy of the hash key and remember the user's preference. */
  for (hi = apr_hash_first(scratch_pool, wc_move_targets);
       hi != NULL;
       hi = apr_hash_next(hi))
    {
      const char *repos_relpath = apr_hash_this_key(hi);

      if (strcmp(move_target_repos_relpath, repos_relpath) == 0)
        {
          *new_hash_key = repos_relpath;
          return SVN_NO_ERROR;
        }
    }

  return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                           _("Repository path '%s' not found in list of "
                             "possible move targets for '%s'"),
                           move_target_repos_relpath,
                           svn_dirent_local_style(victim_abspath,
                                                  scratch_pool));
}

svn_error_t *
svn_client_conflict_option_set_moved_to_repos_relpath2(
  svn_client_conflict_option_t *option,
  int preferred_move_target_idx,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  svn_client_conflict_t *conflict = option->conflict;
  const char *victim_abspath;
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  svn_client_conflict_option_id_t id;

  id = svn_client_conflict_option_get_id(option);
  if (id != svn_client_conflict_option_incoming_move_file_text_merge &&
      id != svn_client_conflict_option_incoming_move_dir_merge &&
      id != svn_client_conflict_option_local_move_file_text_merge &&
      id != svn_client_conflict_option_local_move_dir_merge &&
      id != svn_client_conflict_option_sibling_move_file_text_merge &&
      id != svn_client_conflict_option_sibling_move_dir_merge &&
      id != svn_client_conflict_option_both_moved_file_merge &&
      id != svn_client_conflict_option_both_moved_file_move_merge &&
      id != svn_client_conflict_option_both_moved_dir_merge &&
      id != svn_client_conflict_option_both_moved_dir_move_merge)
    return SVN_NO_ERROR; /* We cannot operate on this option. Nothing to do. */

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);

  if (operation == svn_wc_operation_merge &&
      incoming_change == svn_wc_conflict_action_edit &&
      local_change == svn_wc_conflict_reason_missing)
    {
      struct conflict_tree_local_missing_details *details;

      details = conflict->tree_conflict_local_details;
      if (details == NULL || details->wc_move_targets == NULL)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Setting a move target requires details "
                                   "for tree conflict at '%s' to be fetched "
                                   "from the repository first"),
                                svn_dirent_local_style(victim_abspath,
                                                       scratch_pool));

      SVN_ERR(set_wc_move_target(&details->move_target_repos_relpath,
                                 details->wc_move_targets,
                                 preferred_move_target_idx,
                                 victim_abspath, scratch_pool));
      details->wc_move_target_idx = 0;

      /* Update option description. */
      SVN_ERR(conflict_tree_get_description_local_missing(
                &option->description, conflict, ctx,
                conflict->pool, scratch_pool));
    }
  else
    {
      struct conflict_tree_incoming_delete_details *details;
      apr_array_header_t *move_target_wc_abspaths;
      const char *moved_to_abspath;

      details = conflict->tree_conflict_incoming_details;
      if (details == NULL || details->wc_move_targets == NULL)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Setting a move target requires details "
                                   "for tree conflict at '%s' to be fetched "
                                   "from the repository first"),
                                svn_dirent_local_style(victim_abspath,
                                                       scratch_pool));

      SVN_ERR(set_wc_move_target(&details->move_target_repos_relpath,
                                 details->wc_move_targets,
                                 preferred_move_target_idx,
                                 victim_abspath, scratch_pool));
      details->wc_move_target_idx = 0;

      /* Update option description. */
      move_target_wc_abspaths =
        svn_hash_gets(details->wc_move_targets,
                      get_moved_to_repos_relpath(details, scratch_pool));
      moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths,
                                       details->wc_move_target_idx,
                                       const char *);
      SVN_ERR(describe_incoming_move_merge_conflict_option(
                &option->description,
                conflict, ctx,
                moved_to_abspath,
                conflict->pool,
                scratch_pool));
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_option_set_moved_to_repos_relpath(
  svn_client_conflict_option_t *option,
  int preferred_move_target_idx,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  /* The only difference to API version 2 is an assertion failure if
   * an unexpected option is passed.
   * We do not emulate this old behaviour since clients written against
   * the previous API will just keep working. */
  return svn_error_trace(
    svn_client_conflict_option_set_moved_to_repos_relpath2(option,
      preferred_move_target_idx, ctx, scratch_pool));
}

svn_error_t *
svn_client_conflict_option_get_moved_to_abspath_candidates2(
  apr_array_header_t **possible_moved_to_abspaths,
  svn_client_conflict_option_t *option,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  svn_client_conflict_t *conflict = option->conflict;
  const char *victim_abspath;
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  int i;
  svn_client_conflict_option_id_t id;

  id = svn_client_conflict_option_get_id(option);
  if (id != svn_client_conflict_option_incoming_move_file_text_merge &&
      id != svn_client_conflict_option_incoming_move_dir_merge &&
      id != svn_client_conflict_option_local_move_file_text_merge &&
      id != svn_client_conflict_option_local_move_dir_merge &&
      id != svn_client_conflict_option_sibling_move_file_text_merge &&
      id != svn_client_conflict_option_sibling_move_dir_merge &&
      id != svn_client_conflict_option_both_moved_file_merge &&
      id != svn_client_conflict_option_both_moved_file_move_merge &&
      id != svn_client_conflict_option_both_moved_dir_merge &&
      id != svn_client_conflict_option_both_moved_dir_move_merge)
    {
      /* We cannot operate on this option. */
      *possible_moved_to_abspaths = NULL;
      return NULL;
    }

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);

  if (operation == svn_wc_operation_merge &&
      incoming_change == svn_wc_conflict_action_edit &&
      local_change == svn_wc_conflict_reason_missing)
    {
      struct conflict_tree_local_missing_details *details;

      details = conflict->tree_conflict_local_details;
      if (details == NULL ||
          (details->wc_move_targets == NULL && details->wc_siblings == NULL))
       return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                _("Getting a list of possible move siblings "
                                  "requires details for tree conflict at '%s' "
                                  "to be fetched from the repository first"),
                               svn_dirent_local_style(victim_abspath,
                                                      scratch_pool));

      *possible_moved_to_abspaths = apr_array_make(result_pool, 1,
                                                   sizeof (const char *));
      if (details->wc_move_targets && details->move_target_repos_relpath)
        {
          apr_array_header_t *move_target_wc_abspaths;
          move_target_wc_abspaths =
            svn_hash_gets(details->wc_move_targets,
                          details->move_target_repos_relpath);
          for (i = 0; i < move_target_wc_abspaths->nelts; i++)
            {
              const char *moved_to_abspath;

              moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, i,
                                               const char *);
              APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) =
                apr_pstrdup(result_pool, moved_to_abspath);
            }
        }

      /* ### Siblings are actually 'corresponding nodes', not 'move targets'.
         ### But we provide them here to avoid another API function. */
      if (details->wc_siblings)
        {
          for (i = 0; i < details->wc_siblings->nelts; i++)
            {
               const char *sibling_abspath;

               sibling_abspath = APR_ARRAY_IDX(details->wc_siblings, i,
                                               const char *);
               APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) =
                 apr_pstrdup(result_pool, sibling_abspath);
            }
        }
    }
  else if ((operation == svn_wc_operation_update ||
            operation == svn_wc_operation_switch) &&
           incoming_change == svn_wc_conflict_action_delete &&
           local_change == svn_wc_conflict_reason_moved_away)
    {
      struct conflict_tree_update_local_moved_away_details *details;

      details = conflict->tree_conflict_local_details;
      if (details == NULL || details->wc_move_targets == NULL)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Getting a list of possible move targets "
                                   "requires details for tree conflict at '%s' "
                                   "to be fetched from the repository first"),
                                 svn_dirent_local_style(victim_abspath,
                                                        scratch_pool));

      /* Return a copy of the option's move target candidate list. */
      *possible_moved_to_abspaths =
         apr_array_make(result_pool, details->wc_move_targets->nelts,
                        sizeof (const char *));
      for (i = 0; i < details->wc_move_targets->nelts; i++)
        {
          const char *moved_to_abspath;

          moved_to_abspath = APR_ARRAY_IDX(details->wc_move_targets, i,
                                           const char *);
          APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) =
             apr_pstrdup(result_pool, moved_to_abspath);
        }
    }
  else
    {
      struct conflict_tree_incoming_delete_details *details;
      apr_array_header_t *move_target_wc_abspaths;

      details = conflict->tree_conflict_incoming_details;
      if (details == NULL || details->wc_move_targets == NULL)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Getting a list of possible move targets "
                                   "requires details for tree conflict at '%s' "
                                   "to be fetched from the repository first"),
                                 svn_dirent_local_style(victim_abspath,
                                                        scratch_pool));

      move_target_wc_abspaths =
         svn_hash_gets(details->wc_move_targets,
                       get_moved_to_repos_relpath(details, scratch_pool));

      /* Return a copy of the option's move target candidate list. */
      *possible_moved_to_abspaths =
         apr_array_make(result_pool, move_target_wc_abspaths->nelts,
                        sizeof (const char *));
      for (i = 0; i < move_target_wc_abspaths->nelts; i++)
        {
          const char *moved_to_abspath;

          moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, i,
                                           const char *);
          APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) =
             apr_pstrdup(result_pool, moved_to_abspath);
        }
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_option_get_moved_to_abspath_candidates(
  apr_array_header_t **possible_moved_to_abspaths,
  svn_client_conflict_option_t *option,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  /* The only difference to API version 2 is an assertion failure if
   * an unexpected option is passed.
   * We do not emulate this old behaviour since clients written against
   * the previous API will just keep working. */
  return svn_error_trace(
    svn_client_conflict_option_get_moved_to_abspath_candidates2(
      possible_moved_to_abspaths, option, result_pool, scratch_pool));
}

svn_error_t *
svn_client_conflict_option_set_moved_to_abspath2(
  svn_client_conflict_option_t *option,
  int preferred_move_target_idx,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  svn_client_conflict_t *conflict = option->conflict;
  const char *victim_abspath;
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;
  svn_client_conflict_option_id_t id;

  id = svn_client_conflict_option_get_id(option);
  if (id != svn_client_conflict_option_incoming_move_file_text_merge &&
      id != svn_client_conflict_option_incoming_move_dir_merge &&
      id != svn_client_conflict_option_local_move_file_text_merge &&
      id != svn_client_conflict_option_local_move_dir_merge &&
      id != svn_client_conflict_option_sibling_move_file_text_merge &&
      id != svn_client_conflict_option_sibling_move_dir_merge &&
      id != svn_client_conflict_option_both_moved_file_merge &&
      id != svn_client_conflict_option_both_moved_file_move_merge &&
      id != svn_client_conflict_option_both_moved_dir_merge &&
      id != svn_client_conflict_option_both_moved_dir_move_merge)
    return NULL; /* We cannot operate on this option. Nothing to do. */

  victim_abspath = svn_client_conflict_get_local_abspath(conflict);
  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);

  if (operation == svn_wc_operation_merge &&
      incoming_change == svn_wc_conflict_action_edit &&
      local_change == svn_wc_conflict_reason_missing)
    {
      struct conflict_tree_local_missing_details *details;
      const char *wcroot_abspath;
      const char *preferred_sibling;

      SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath,
                                 ctx->wc_ctx,
                                 conflict->local_abspath,
                                 scratch_pool,
                                 scratch_pool));

      details = conflict->tree_conflict_local_details;
      if (details == NULL || (details->wc_siblings == NULL &&
          details->wc_move_targets == NULL))
       return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                _("Setting a move target requires details "
                                  "for tree conflict at '%s' to be fetched "
                                  "from the repository first"),
                                svn_dirent_local_style(victim_abspath,
                                                       scratch_pool));

      if (details->wc_siblings)
        {
          if (preferred_move_target_idx < 0 ||
              preferred_move_target_idx > details->wc_siblings->nelts)
            return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                                     _("Index '%d' is out of bounds of the "
                                       "possible move sibling list for '%s'"),
                                    preferred_move_target_idx,
                                    svn_dirent_local_style(victim_abspath,
                                                           scratch_pool));
          /* Record the user's preference. */
          details->preferred_sibling_idx = preferred_move_target_idx;

          /* Update option description. */
          preferred_sibling = APR_ARRAY_IDX(details->wc_siblings,
                                            details->preferred_sibling_idx,
                                            const char *);
          option->description =
            apr_psprintf(
              conflict->pool, _("apply changes to '%s'"),
              svn_dirent_local_style(
                svn_dirent_skip_ancestor(wcroot_abspath, preferred_sibling),
                scratch_pool));
        }
      else if (details->wc_move_targets && details->move_target_repos_relpath)
       {
          apr_array_header_t *move_target_wc_abspaths;
          move_target_wc_abspaths =
            svn_hash_gets(details->wc_move_targets,
                          details->move_target_repos_relpath);

          if (preferred_move_target_idx < 0 ||
              preferred_move_target_idx > move_target_wc_abspaths->nelts)
            return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                                     _("Index '%d' is out of bounds of the possible "
                                       "move target list for '%s'"),
                                    preferred_move_target_idx,
                                    svn_dirent_local_style(victim_abspath,
                                                           scratch_pool));

          /* Record the user's preference. */
          details->wc_move_target_idx = preferred_move_target_idx;

          /* Update option description. */
          SVN_ERR(conflict_tree_get_description_local_missing(
                    &option->description, conflict, ctx,
                    conflict->pool, scratch_pool));
       }
    }
  else if ((operation == svn_wc_operation_update ||
            operation == svn_wc_operation_switch) &&
           incoming_change == svn_wc_conflict_action_delete &&
           local_change == svn_wc_conflict_reason_moved_away)
    {
      struct conflict_tree_update_local_moved_away_details *details;

      details = conflict->tree_conflict_local_details;
      if (details == NULL || details->wc_move_targets == NULL)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Setting a move target requires details "
                                   "for tree conflict at '%s' to be fetched "
                                   "from the repository first"),
                                svn_dirent_local_style(victim_abspath,
                                                       scratch_pool));

      if (preferred_move_target_idx < 0 ||
          preferred_move_target_idx > details->wc_move_targets->nelts)
        return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                                 _("Index '%d' is out of bounds of the "
                                   "possible move target list for '%s'"),
                                preferred_move_target_idx,
                                svn_dirent_local_style(victim_abspath,
                                                       scratch_pool));

      /* Record the user's preference. */
      details->preferred_move_target_idx = preferred_move_target_idx;

      /* Update option description. */
      if (id == svn_client_conflict_option_both_moved_file_merge)
        SVN_ERR(conflict_tree_get_description_update_both_moved_file_merge(
                  &option->description, conflict, ctx, conflict->pool,
                  scratch_pool));
      else if (id == svn_client_conflict_option_both_moved_file_move_merge)
        SVN_ERR(conflict_tree_get_description_update_both_moved_file_move_merge(
           &option->description, conflict, ctx, conflict->pool, scratch_pool));
#if 0  /* ### TODO: Also handle options for directories! */
      else if (id == svn_client_conflict_option_both_moved_dir_merge)
        {
        }
      else if (id == svn_client_conflict_option_both_moved_dir_move_merge)
        {
        }
#endif
      else
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Unexpected option id '%d'"), id);
    }
  else
    {
      struct conflict_tree_incoming_delete_details *details;
      apr_array_header_t *move_target_wc_abspaths;
      const char *moved_to_abspath;

      details = conflict->tree_conflict_incoming_details;
      if (details == NULL || details->wc_move_targets == NULL)
        return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                                 _("Setting a move target requires details "
                                   "for tree conflict at '%s' to be fetched "
                                   "from the repository first"),
                                svn_dirent_local_style(victim_abspath,
                                                       scratch_pool));

      move_target_wc_abspaths =
        svn_hash_gets(details->wc_move_targets,
                      get_moved_to_repos_relpath(details, scratch_pool));

      if (preferred_move_target_idx < 0 ||
          preferred_move_target_idx > move_target_wc_abspaths->nelts)
        return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                                 _("Index '%d' is out of bounds of the possible "
                                   "move target list for '%s'"),
                                preferred_move_target_idx,
                                svn_dirent_local_style(victim_abspath,
                                                       scratch_pool));

      /* Record the user's preference. */
      details->wc_move_target_idx = preferred_move_target_idx;

      /* Update option description. */
      moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths,
                                       details->wc_move_target_idx,
                                       const char *);
      SVN_ERR(describe_incoming_move_merge_conflict_option(&option->description,
                                                           conflict, ctx,
                                                           moved_to_abspath,
                                                           conflict->pool,
                                                           scratch_pool));
    }
  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_option_set_moved_to_abspath(
  svn_client_conflict_option_t *option,
  int preferred_move_target_idx,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  /* The only difference to API version 2 is an assertion failure if
   * an unexpected option is passed.
   * We do not emulate this old behaviour since clients written against
   * the previous API will just keep working. */
  return svn_error_trace(
    svn_client_conflict_option_set_moved_to_abspath2(option,
      preferred_move_target_idx, ctx, scratch_pool));
}

svn_error_t *
svn_client_conflict_tree_get_resolution_options(apr_array_header_t **options,
                                                svn_client_conflict_t *conflict,
                                                svn_client_ctx_t *ctx,
                                                apr_pool_t *result_pool,
                                                apr_pool_t *scratch_pool)
{
  SVN_ERR(assert_tree_conflict(conflict, scratch_pool));

  *options = apr_array_make(result_pool, 2,
                            sizeof(svn_client_conflict_option_t *));

  /* Add postpone option. */
  add_resolution_option(*options, conflict,
                        svn_client_conflict_option_postpone,
                        _("Postpone"),
                        _("skip this conflict and leave it unresolved"),
                        resolve_postpone);

  /* Add an option which marks the conflict resolved. */
  SVN_ERR(configure_option_accept_current_wc_state(conflict, *options));

  /* Configure options which offer automatic resolution. */
  SVN_ERR(configure_option_update_move_destination(conflict, *options));
  SVN_ERR(configure_option_update_raise_moved_away_children(conflict,
                                                            *options));
  SVN_ERR(configure_option_incoming_add_ignore(conflict, ctx, *options,
                                               scratch_pool));
  SVN_ERR(configure_option_incoming_added_file_text_merge(conflict, ctx,
                                                          *options,
                                                          scratch_pool));
  SVN_ERR(configure_option_incoming_added_file_replace_and_merge(conflict,
                                                                 ctx,
                                                                 *options,
                                                                 scratch_pool));
  SVN_ERR(configure_option_incoming_added_dir_merge(conflict, ctx,
                                                    *options,
                                                    scratch_pool));
  SVN_ERR(configure_option_incoming_added_dir_replace(conflict, ctx,
                                                      *options,
                                                      scratch_pool));
  SVN_ERR(configure_option_incoming_added_dir_replace_and_merge(conflict,
                                                                ctx,
                                                                *options,
                                                                scratch_pool));
  SVN_ERR(configure_option_incoming_delete_ignore(conflict, ctx, *options,
                                                  scratch_pool));
  SVN_ERR(configure_option_incoming_delete_accept(conflict, ctx, *options,
                                                  scratch_pool));
  SVN_ERR(configure_option_incoming_move_file_merge(conflict, ctx, *options,
                                                    scratch_pool));
  SVN_ERR(configure_option_incoming_dir_merge(conflict, ctx, *options,
                                              scratch_pool));
  SVN_ERR(configure_option_local_move_file_or_dir_merge(conflict, ctx,
                                                        *options,
                                                        scratch_pool));
  SVN_ERR(configure_option_sibling_move_merge(conflict, ctx, *options,
                                              scratch_pool));
  SVN_ERR(configure_option_both_moved_file_merge(conflict, ctx, *options,
                                                 scratch_pool));
  SVN_ERR(configure_option_both_moved_dir_merge(conflict, ctx, *options,
                                                scratch_pool));

  return SVN_NO_ERROR;
}

/* Swallow authz failures and return SVN_NO_ERROR in that case.
 * Otherwise, return ERR unchanged. */
static svn_error_t *
ignore_authz_failures(svn_error_t *err)
{
  if (err && (   svn_error_find_cause(err, SVN_ERR_AUTHZ_UNREADABLE)
              || svn_error_find_cause(err, SVN_ERR_RA_NOT_AUTHORIZED)
              || svn_error_find_cause(err, SVN_ERR_RA_DAV_FORBIDDEN)))
    {
      svn_error_clear(err);
      err = SVN_NO_ERROR;
    }

  return err;
}

svn_error_t *
svn_client_conflict_tree_get_details(svn_client_conflict_t *conflict,
                                     svn_client_ctx_t *ctx,
                                     apr_pool_t *scratch_pool)
{
  SVN_ERR(assert_tree_conflict(conflict, scratch_pool));

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(
                 svn_client_conflict_get_local_abspath(conflict),
                 svn_wc_notify_begin_search_tree_conflict_details,
                 scratch_pool),
      ctx->notify_func2(ctx->notify_baton2, notify,
                                  scratch_pool);
    }

  /* Collecting conflict details may fail due to insufficient access rights.
   * This is not a failure but simply restricts our future options. */
  if (conflict->tree_conflict_get_incoming_details_func)
    SVN_ERR(ignore_authz_failures(
      conflict->tree_conflict_get_incoming_details_func(conflict, ctx,
                                                        scratch_pool)));


  if (conflict->tree_conflict_get_local_details_func)
    SVN_ERR(ignore_authz_failures(
      conflict->tree_conflict_get_local_details_func(conflict, ctx,
                                                    scratch_pool)));

  if (ctx->notify_func2)
    {
      svn_wc_notify_t *notify;

      notify = svn_wc_create_notify(
                 svn_client_conflict_get_local_abspath(conflict),
                 svn_wc_notify_end_search_tree_conflict_details,
                 scratch_pool),
      ctx->notify_func2(ctx->notify_baton2, notify,
                                  scratch_pool);
    }

  return SVN_NO_ERROR;
}

svn_client_conflict_option_id_t
svn_client_conflict_option_get_id(svn_client_conflict_option_t *option)
{
  return option->id;
}

const char *
svn_client_conflict_option_get_label(svn_client_conflict_option_t *option,
                                     apr_pool_t *result_pool)
{
  return apr_pstrdup(result_pool, option->label);
}

const char *
svn_client_conflict_option_get_description(svn_client_conflict_option_t *option,
                                           apr_pool_t *result_pool)
{
  return apr_pstrdup(result_pool, option->description);
}

svn_client_conflict_option_id_t
svn_client_conflict_get_recommended_option_id(svn_client_conflict_t *conflict)
{
  return conflict->recommended_option_id;
}

svn_error_t *
svn_client_conflict_text_resolve(svn_client_conflict_t *conflict,
                                 svn_client_conflict_option_t *option,
                                 svn_client_ctx_t *ctx,
                                 apr_pool_t *scratch_pool)
{
  SVN_ERR(assert_text_conflict(conflict, scratch_pool));
  SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool));

  return SVN_NO_ERROR;
}

svn_client_conflict_option_t *
svn_client_conflict_option_find_by_id(apr_array_header_t *options,
                                      svn_client_conflict_option_id_t option_id)
{
  int i;

  for (i = 0; i < options->nelts; i++)
    {
      svn_client_conflict_option_t *this_option;
      svn_client_conflict_option_id_t this_option_id;

      this_option = APR_ARRAY_IDX(options, i, svn_client_conflict_option_t *);
      this_option_id = svn_client_conflict_option_get_id(this_option);

      if (this_option_id == option_id)
        return this_option;
    }

  return NULL;
}

svn_error_t *
svn_client_conflict_text_resolve_by_id(
  svn_client_conflict_t *conflict,
  svn_client_conflict_option_id_t option_id,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  apr_array_header_t *resolution_options;
  svn_client_conflict_option_t *option;

  SVN_ERR(svn_client_conflict_text_get_resolution_options(
            &resolution_options, conflict, ctx,
            scratch_pool, scratch_pool));
  option = svn_client_conflict_option_find_by_id(resolution_options,
                                                 option_id);
  if (option == NULL)
    return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE,
                             NULL,
                             _("Inapplicable conflict resolution option "
                               "given for conflicted path '%s'"),
                             svn_dirent_local_style(conflict->local_abspath,
                                                    scratch_pool));

  SVN_ERR(svn_client_conflict_text_resolve(conflict, option, ctx, scratch_pool));

  return SVN_NO_ERROR;
}

svn_client_conflict_option_id_t
svn_client_conflict_text_get_resolution(svn_client_conflict_t *conflict)
{
  return conflict->resolution_text;
}

svn_error_t *
svn_client_conflict_prop_resolve(svn_client_conflict_t *conflict,
                                 const char *propname,
                                 svn_client_conflict_option_t *option,
                                 svn_client_ctx_t *ctx,
                                 apr_pool_t *scratch_pool)
{
  SVN_ERR(assert_prop_conflict(conflict, scratch_pool));
  option->type_data.prop.propname = propname;
  SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool));

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_prop_resolve_by_id(
  svn_client_conflict_t *conflict,
  const char *propname,
  svn_client_conflict_option_id_t option_id,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  apr_array_header_t *resolution_options;
  svn_client_conflict_option_t *option;

  SVN_ERR(svn_client_conflict_prop_get_resolution_options(
            &resolution_options, conflict, ctx,
            scratch_pool, scratch_pool));
  option = svn_client_conflict_option_find_by_id(resolution_options,
                                                 option_id);
  if (option == NULL)
    return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE,
                             NULL,
                             _("Inapplicable conflict resolution option "
                               "given for conflicted path '%s'"),
                             svn_dirent_local_style(conflict->local_abspath,
                                                    scratch_pool));
  SVN_ERR(svn_client_conflict_prop_resolve(conflict, propname, option, ctx,
                                           scratch_pool));

  return SVN_NO_ERROR;
}

svn_client_conflict_option_id_t
svn_client_conflict_prop_get_resolution(svn_client_conflict_t *conflict,
                                        const char *propname)
{
  svn_client_conflict_option_t *option;

  option = svn_hash_gets(conflict->resolved_props, propname);
  if (option == NULL)
    return svn_client_conflict_option_unspecified;

  return svn_client_conflict_option_get_id(option);
}

svn_error_t *
svn_client_conflict_tree_resolve(svn_client_conflict_t *conflict,
                                 svn_client_conflict_option_t *option,
                                 svn_client_ctx_t *ctx,
                                 apr_pool_t *scratch_pool)
{
  SVN_ERR(assert_tree_conflict(conflict, scratch_pool));
  SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool));

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_tree_resolve_by_id(
  svn_client_conflict_t *conflict,
  svn_client_conflict_option_id_t option_id,
  svn_client_ctx_t *ctx,
  apr_pool_t *scratch_pool)
{
  apr_array_header_t *resolution_options;
  svn_client_conflict_option_t *option;

  SVN_ERR(svn_client_conflict_tree_get_resolution_options(
            &resolution_options, conflict, ctx,
            scratch_pool, scratch_pool));
  option = svn_client_conflict_option_find_by_id(resolution_options,
                                                 option_id);
  if (option == NULL)
    return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE,
                             NULL,
                             _("Inapplicable conflict resolution option "
                               "given for conflicted path '%s'"),
                             svn_dirent_local_style(conflict->local_abspath,
                                                    scratch_pool));
  SVN_ERR(svn_client_conflict_tree_resolve(conflict, option, ctx, scratch_pool));

  return SVN_NO_ERROR;
}

svn_client_conflict_option_id_t
svn_client_conflict_tree_get_resolution(svn_client_conflict_t *conflict)
{
  return conflict->resolution_tree;
}

/* Return the legacy conflict descriptor which is wrapped by CONFLICT. */
static const svn_wc_conflict_description2_t *
get_conflict_desc2_t(svn_client_conflict_t *conflict)
{
  if (conflict->legacy_text_conflict)
    return conflict->legacy_text_conflict;

  if (conflict->legacy_tree_conflict)
    return conflict->legacy_tree_conflict;

  if (conflict->prop_conflicts && conflict->legacy_prop_conflict_propname)
    return svn_hash_gets(conflict->prop_conflicts,
                         conflict->legacy_prop_conflict_propname);

  return NULL;
}

svn_error_t *
svn_client_conflict_get_conflicted(svn_boolean_t *text_conflicted,
                                   apr_array_header_t **props_conflicted,
                                   svn_boolean_t *tree_conflicted,
                                   svn_client_conflict_t *conflict,
                                   apr_pool_t *result_pool,
                                   apr_pool_t *scratch_pool)
{
  if (text_conflicted)
    *text_conflicted = (conflict->legacy_text_conflict != NULL);

  if (props_conflicted)
    {
      if (conflict->prop_conflicts)
        SVN_ERR(svn_hash_keys(props_conflicted, conflict->prop_conflicts,
                              result_pool));
      else
        *props_conflicted = apr_array_make(result_pool, 0,
                                           sizeof(const char*));
    }

  if (tree_conflicted)
    *tree_conflicted = (conflict->legacy_tree_conflict != NULL);

  return SVN_NO_ERROR;
}

const char *
svn_client_conflict_get_local_abspath(svn_client_conflict_t *conflict)
{
  return conflict->local_abspath;
}

svn_wc_operation_t
svn_client_conflict_get_operation(svn_client_conflict_t *conflict)
{
  return get_conflict_desc2_t(conflict)->operation;
}

svn_wc_conflict_action_t
svn_client_conflict_get_incoming_change(svn_client_conflict_t *conflict)
{
  return get_conflict_desc2_t(conflict)->action;
}

svn_wc_conflict_reason_t
svn_client_conflict_get_local_change(svn_client_conflict_t *conflict)
{
  return get_conflict_desc2_t(conflict)->reason;
}

svn_error_t *
svn_client_conflict_get_repos_info(const char **repos_root_url,
                                   const char **repos_uuid,
                                   svn_client_conflict_t *conflict,
                                   apr_pool_t *result_pool,
                                   apr_pool_t *scratch_pool)
{
  if (repos_root_url)
    {
      if (get_conflict_desc2_t(conflict)->src_left_version)
        *repos_root_url =
          get_conflict_desc2_t(conflict)->src_left_version->repos_url;
      else if (get_conflict_desc2_t(conflict)->src_right_version)
        *repos_root_url =
          get_conflict_desc2_t(conflict)->src_right_version->repos_url;
      else
        *repos_root_url = NULL;
    }

  if (repos_uuid)
    {
      if (get_conflict_desc2_t(conflict)->src_left_version)
        *repos_uuid =
          get_conflict_desc2_t(conflict)->src_left_version->repos_uuid;
      else if (get_conflict_desc2_t(conflict)->src_right_version)
        *repos_uuid =
          get_conflict_desc2_t(conflict)->src_right_version->repos_uuid;
      else
        *repos_uuid = NULL;
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_get_incoming_old_repos_location(
  const char **incoming_old_repos_relpath,
  svn_revnum_t *incoming_old_pegrev,
  svn_node_kind_t *incoming_old_node_kind,
  svn_client_conflict_t *conflict,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  if (incoming_old_repos_relpath)
    {
      if (get_conflict_desc2_t(conflict)->src_left_version)
        *incoming_old_repos_relpath =
          get_conflict_desc2_t(conflict)->src_left_version->path_in_repos;
      else
        *incoming_old_repos_relpath = NULL;
    }

  if (incoming_old_pegrev)
    {
      if (get_conflict_desc2_t(conflict)->src_left_version)
        *incoming_old_pegrev =
          get_conflict_desc2_t(conflict)->src_left_version->peg_rev;
      else
        *incoming_old_pegrev = SVN_INVALID_REVNUM;
    }

  if (incoming_old_node_kind)
    {
      if (get_conflict_desc2_t(conflict)->src_left_version)
        *incoming_old_node_kind =
          get_conflict_desc2_t(conflict)->src_left_version->node_kind;
      else
        *incoming_old_node_kind = svn_node_none;
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_get_incoming_new_repos_location(
  const char **incoming_new_repos_relpath,
  svn_revnum_t *incoming_new_pegrev,
  svn_node_kind_t *incoming_new_node_kind,
  svn_client_conflict_t *conflict,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  if (incoming_new_repos_relpath)
    {
      if (get_conflict_desc2_t(conflict)->src_right_version)
        *incoming_new_repos_relpath =
          get_conflict_desc2_t(conflict)->src_right_version->path_in_repos;
      else
        *incoming_new_repos_relpath = NULL;
    }

  if (incoming_new_pegrev)
    {
      if (get_conflict_desc2_t(conflict)->src_right_version)
        *incoming_new_pegrev =
          get_conflict_desc2_t(conflict)->src_right_version->peg_rev;
      else
        *incoming_new_pegrev = SVN_INVALID_REVNUM;
    }

  if (incoming_new_node_kind)
    {
      if (get_conflict_desc2_t(conflict)->src_right_version)
        *incoming_new_node_kind =
          get_conflict_desc2_t(conflict)->src_right_version->node_kind;
      else
        *incoming_new_node_kind = svn_node_none;
    }

  return SVN_NO_ERROR;
}

svn_node_kind_t
svn_client_conflict_tree_get_victim_node_kind(svn_client_conflict_t *conflict)
{
  SVN_ERR_ASSERT_NO_RETURN(assert_tree_conflict(conflict, conflict->pool)
                           == SVN_NO_ERROR);

  return get_conflict_desc2_t(conflict)->node_kind;
}

svn_error_t *
svn_client_conflict_prop_get_propvals(const svn_string_t **base_propval,
                                      const svn_string_t **working_propval,
                                      const svn_string_t **incoming_old_propval,
                                      const svn_string_t **incoming_new_propval,
                                      svn_client_conflict_t *conflict,
                                      const char *propname,
                                      apr_pool_t *result_pool)
{
  const svn_wc_conflict_description2_t *desc;

  SVN_ERR(assert_prop_conflict(conflict, conflict->pool));

  desc = svn_hash_gets(conflict->prop_conflicts, propname);
  if (desc == NULL)
    return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL,
                             _("Property '%s' is not in conflict."), propname);

  if (base_propval)
    *base_propval =
      svn_string_dup(desc->prop_value_base, result_pool);

  if (working_propval)
    *working_propval =
      svn_string_dup(desc->prop_value_working, result_pool);

  if (incoming_old_propval)
    *incoming_old_propval =
      svn_string_dup(desc->prop_value_incoming_old, result_pool);

  if (incoming_new_propval)
    *incoming_new_propval =
      svn_string_dup(desc->prop_value_incoming_new, result_pool);

  return SVN_NO_ERROR;
}

const char *
svn_client_conflict_prop_get_reject_abspath(svn_client_conflict_t *conflict)
{
  SVN_ERR_ASSERT_NO_RETURN(assert_prop_conflict(conflict, conflict->pool)
                           == SVN_NO_ERROR);

  /* svn_wc_conflict_description2_t stores this path in 'their_abspath' */
  return get_conflict_desc2_t(conflict)->their_abspath;
}

const char *
svn_client_conflict_text_get_mime_type(svn_client_conflict_t *conflict)
{
  SVN_ERR_ASSERT_NO_RETURN(assert_text_conflict(conflict, conflict->pool)
                           == SVN_NO_ERROR);

  return get_conflict_desc2_t(conflict)->mime_type;
}

svn_error_t *
svn_client_conflict_text_get_contents(const char **base_abspath,
                                      const char **working_abspath,
                                      const char **incoming_old_abspath,
                                      const char **incoming_new_abspath,
                                      svn_client_conflict_t *conflict,
                                      apr_pool_t *result_pool,
                                      apr_pool_t *scratch_pool)
{
  SVN_ERR(assert_text_conflict(conflict, scratch_pool));

  if (base_abspath)
    {
      if (svn_client_conflict_get_operation(conflict) ==
          svn_wc_operation_merge)
        *base_abspath = NULL; /* ### WC base contents not available yet */
      else /* update/switch */
        *base_abspath = get_conflict_desc2_t(conflict)->base_abspath;
    }

  if (working_abspath)
    *working_abspath = get_conflict_desc2_t(conflict)->my_abspath;

  if (incoming_old_abspath)
    *incoming_old_abspath = get_conflict_desc2_t(conflict)->base_abspath;

  if (incoming_new_abspath)
    *incoming_new_abspath = get_conflict_desc2_t(conflict)->their_abspath;

  return SVN_NO_ERROR;
}

/* Set up type-specific data for a new conflict object. */
static svn_error_t *
conflict_type_specific_setup(svn_client_conflict_t *conflict,
                             apr_pool_t *scratch_pool)
{
  svn_boolean_t tree_conflicted;
  svn_wc_operation_t operation;
  svn_wc_conflict_action_t incoming_change;
  svn_wc_conflict_reason_t local_change;

  /* For now, we only deal with tree conflicts here. */
  SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted,
                                             conflict, scratch_pool,
                                             scratch_pool));
  if (!tree_conflicted)
    return SVN_NO_ERROR;

  /* Set a default description function. */
  conflict->tree_conflict_get_incoming_description_func =
    conflict_tree_get_incoming_description_generic;
  conflict->tree_conflict_get_local_description_func =
    conflict_tree_get_local_description_generic;

  operation = svn_client_conflict_get_operation(conflict);
  incoming_change = svn_client_conflict_get_incoming_change(conflict);
  local_change = svn_client_conflict_get_local_change(conflict);

  /* Set type-specific description and details functions. */
  if (incoming_change == svn_wc_conflict_action_delete ||
      incoming_change == svn_wc_conflict_action_replace)
    {
      conflict->tree_conflict_get_incoming_description_func =
        conflict_tree_get_description_incoming_delete;
      conflict->tree_conflict_get_incoming_details_func =
        conflict_tree_get_details_incoming_delete;
    }
  else if (incoming_change == svn_wc_conflict_action_add)
    {
      conflict->tree_conflict_get_incoming_description_func =
        conflict_tree_get_description_incoming_add;
      conflict->tree_conflict_get_incoming_details_func =
        conflict_tree_get_details_incoming_add;
    }
  else if (incoming_change == svn_wc_conflict_action_edit)
    {
      conflict->tree_conflict_get_incoming_description_func =
        conflict_tree_get_description_incoming_edit;
      conflict->tree_conflict_get_incoming_details_func =
        conflict_tree_get_details_incoming_edit;
    }

  if (local_change == svn_wc_conflict_reason_missing)
    {
      conflict->tree_conflict_get_local_description_func =
        conflict_tree_get_description_local_missing;
      conflict->tree_conflict_get_local_details_func =
        conflict_tree_get_details_local_missing;
    }
  else if (local_change == svn_wc_conflict_reason_moved_away &&
           operation == svn_wc_operation_update /* ### what about switch? */)
    {
      conflict->tree_conflict_get_local_details_func =
        conflict_tree_get_details_update_local_moved_away;
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_get(svn_client_conflict_t **conflict,
                        const char *local_abspath,
                        svn_client_ctx_t *ctx,
                        apr_pool_t *result_pool,
                        apr_pool_t *scratch_pool)
{
  const apr_array_header_t *descs;
  int i;

  *conflict = apr_pcalloc(result_pool, sizeof(**conflict));

  (*conflict)->local_abspath = apr_pstrdup(result_pool, local_abspath);
  (*conflict)->resolution_text = svn_client_conflict_option_unspecified;
  (*conflict)->resolution_tree = svn_client_conflict_option_unspecified;
  (*conflict)->resolved_props = apr_hash_make(result_pool);
  (*conflict)->recommended_option_id = svn_client_conflict_option_unspecified;
  (*conflict)->pool = result_pool;

  /* Add all legacy conflict descriptors we can find. Eventually, this code
   * path should stop relying on svn_wc_conflict_description2_t entirely. */
  SVN_ERR(svn_wc__read_conflict_descriptions2_t(&descs, ctx->wc_ctx,
                                                local_abspath,
                                                result_pool, scratch_pool));
  for (i = 0; i < descs->nelts; i++)
    {
      const svn_wc_conflict_description2_t *desc;

      desc = APR_ARRAY_IDX(descs, i, const svn_wc_conflict_description2_t *);
      add_legacy_desc_to_conflict(desc, *conflict, result_pool);
    }

  SVN_ERR(conflict_type_specific_setup(*conflict, scratch_pool));

  return SVN_NO_ERROR;
}

/* Baton for conflict_status_walker */
struct conflict_status_walker_baton
{
  svn_client_conflict_walk_func_t conflict_walk_func;
  void *conflict_walk_func_baton;
  svn_client_ctx_t *ctx;
  svn_wc_notify_func2_t notify_func;
  void *notify_baton;
  svn_boolean_t resolved_a_tree_conflict;
  apr_hash_t *unresolved_tree_conflicts;
};

/* Implements svn_wc_notify_func2_t to collect new conflicts caused by
   resolving a tree conflict. */
static void
tree_conflict_collector(void *baton,
                        const svn_wc_notify_t *notify,
                        apr_pool_t *pool)
{
  struct conflict_status_walker_baton *cswb = baton;

  if (cswb->notify_func)
    cswb->notify_func(cswb->notify_baton, notify, pool);

  if (cswb->unresolved_tree_conflicts
      && (notify->action == svn_wc_notify_tree_conflict
          || notify->prop_state == svn_wc_notify_state_conflicted
          || notify->content_state == svn_wc_notify_state_conflicted))
    {
      if (!svn_hash_gets(cswb->unresolved_tree_conflicts, notify->path))
        {
          const char *tc_abspath;
          apr_pool_t *hash_pool;

          hash_pool = apr_hash_pool_get(cswb->unresolved_tree_conflicts);
          tc_abspath = apr_pstrdup(hash_pool, notify->path);
          svn_hash_sets(cswb->unresolved_tree_conflicts, tc_abspath, "");
        }
    }
}

/*
 * Record a tree conflict resolution failure due to error condition ERR
 * in the RESOLVE_LATER hash table. If the hash table is not available
 * (meaning the caller does not wish to retry resolution later), or if
 * the error condition does not indicate circumstances where another
 * existing tree conflict is blocking the resolution attempt, then
 * return the error ERR itself.
 */
static svn_error_t *
handle_tree_conflict_resolution_failure(const char *local_abspath,
                                        svn_error_t *err,
                                        apr_hash_t *unresolved_tree_conflicts)
{
  const char *tc_abspath;

  if (!unresolved_tree_conflicts
      || (err->apr_err != SVN_ERR_WC_OBSTRUCTED_UPDATE
          && err->apr_err != SVN_ERR_WC_FOUND_CONFLICT))
    return svn_error_trace(err); /* Give up. Do not retry resolution later. */

  svn_error_clear(err);
  tc_abspath = apr_pstrdup(apr_hash_pool_get(unresolved_tree_conflicts),
                           local_abspath);

  svn_hash_sets(unresolved_tree_conflicts, tc_abspath, "");

  return SVN_NO_ERROR; /* Caller may retry after resolving other conflicts. */
}

/* Implements svn_wc_status4_t to walk all conflicts to resolve.
 */
static svn_error_t *
conflict_status_walker(void *baton,
                       const char *local_abspath,
                       const svn_wc_status3_t *status,
                       apr_pool_t *scratch_pool)
{
  struct conflict_status_walker_baton *cswb = baton;
  svn_client_conflict_t *conflict;
  svn_error_t *err;
  svn_boolean_t tree_conflicted;

  if (!status->conflicted)
    return SVN_NO_ERROR;

  SVN_ERR(svn_client_conflict_get(&conflict, local_abspath, cswb->ctx,
                                  scratch_pool, scratch_pool));
  SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted,
                                             conflict, scratch_pool,
                                             scratch_pool));
  err = cswb->conflict_walk_func(cswb->conflict_walk_func_baton,
                                 conflict, scratch_pool);
  if (err)
    {
      if (tree_conflicted)
        SVN_ERR(handle_tree_conflict_resolution_failure(
                  local_abspath, err, cswb->unresolved_tree_conflicts));

      else
        return svn_error_trace(err);
    }

  if (tree_conflicted)
    {
      svn_client_conflict_option_id_t resolution;

      resolution = svn_client_conflict_tree_get_resolution(conflict);
      if (resolution != svn_client_conflict_option_unspecified &&
          resolution != svn_client_conflict_option_postpone)
        cswb->resolved_a_tree_conflict = TRUE;
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client_conflict_walk(const char *local_abspath,
                         svn_depth_t depth,
                         svn_client_conflict_walk_func_t conflict_walk_func,
                         void *conflict_walk_func_baton,
                         svn_client_ctx_t *ctx,
                         apr_pool_t *scratch_pool)
{
  struct conflict_status_walker_baton cswb;
  apr_pool_t *iterpool = NULL;
  svn_error_t *err = SVN_NO_ERROR;

  if (depth == svn_depth_unknown)
    depth = svn_depth_infinity;

  cswb.conflict_walk_func = conflict_walk_func;
  cswb.conflict_walk_func_baton = conflict_walk_func_baton;
  cswb.ctx = ctx;
  cswb.resolved_a_tree_conflict = FALSE;
  cswb.unresolved_tree_conflicts = apr_hash_make(scratch_pool);

  if (ctx->notify_func2)
    ctx->notify_func2(ctx->notify_baton2,
                      svn_wc_create_notify(
                        local_abspath,
                        svn_wc_notify_conflict_resolver_starting,
                        scratch_pool),
                      scratch_pool);

  /* Swap in our notify_func wrapper. We must revert this before returning! */
  cswb.notify_func = ctx->notify_func2;
  cswb.notify_baton = ctx->notify_baton2;
  ctx->notify_func2 = tree_conflict_collector;
  ctx->notify_baton2 = &cswb;

  err = svn_wc_walk_status(ctx->wc_ctx,
                           local_abspath,
                           depth,
                           FALSE /* get_all */,
                           FALSE /* no_ignore */,
                           TRUE /* ignore_text_mods */,
                           NULL /* ignore_patterns */,
                           conflict_status_walker, &cswb,
                           ctx->cancel_func, ctx->cancel_baton,
                           scratch_pool);

  /* If we got new tree conflicts (or delayed conflicts) during the initial
     walk, we now walk them one by one as closure. */
  while (!err && cswb.unresolved_tree_conflicts &&
         apr_hash_count(cswb.unresolved_tree_conflicts))
    {
      apr_hash_index_t *hi;
      svn_wc_status3_t *status = NULL;
      const char *tc_abspath = NULL;

      if (iterpool)
        svn_pool_clear(iterpool);
      else
        iterpool = svn_pool_create(scratch_pool);

      hi = apr_hash_first(scratch_pool, cswb.unresolved_tree_conflicts);
      cswb.unresolved_tree_conflicts = apr_hash_make(scratch_pool);
      cswb.resolved_a_tree_conflict = FALSE;

      for (; hi && !err; hi = apr_hash_next(hi))
        {
          svn_pool_clear(iterpool);

          tc_abspath = apr_hash_this_key(hi);

          if (ctx->cancel_func)
            {
              err = ctx->cancel_func(ctx->cancel_baton);
              if (err)
                break;
            }

          err = svn_error_trace(svn_wc_status3(&status, ctx->wc_ctx,
                                               tc_abspath,
                                               iterpool, iterpool));
          if (err)
            break;

          err = svn_error_trace(conflict_status_walker(&cswb, tc_abspath,
                                                       status, scratch_pool));
          if (err)
            break;
        }

      if (!err && !cswb.resolved_a_tree_conflict && tc_abspath &&
          apr_hash_count(cswb.unresolved_tree_conflicts))
        {
          /* None of the remaining conflicts got resolved, without any error.
           * Disable the 'unresolved_tree_conflicts' cache and try again. */
          cswb.unresolved_tree_conflicts = NULL;

          /* Run the most recent resolve operation again.
           * We still have status and tc_abspath for that one.
           * This should uncover the error which prevents resolution. */
          err = svn_error_trace(conflict_status_walker(&cswb, tc_abspath,
                                                       status, scratch_pool));
          SVN_ERR_ASSERT(err != NULL);

          err = svn_error_createf(
                    SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err,
                    _("Unable to resolve pending conflict on '%s'"),
                    svn_dirent_local_style(tc_abspath, scratch_pool));
          break;
        }
    }

  if (iterpool)
    svn_pool_destroy(iterpool);

  ctx->notify_func2 = cswb.notify_func;
  ctx->notify_baton2 = cswb.notify_baton;

  if (!err && ctx->notify_func2)
    ctx->notify_func2(ctx->notify_baton2,
                      svn_wc_create_notify(local_abspath,
                                          svn_wc_notify_conflict_resolver_done,
                                          scratch_pool),
                      scratch_pool);

  return svn_error_trace(err);
}
