/*
 * svnmover.c: Concept Demo for Move Tracking and Branching
 *
 * ====================================================================
 *    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.
 * ====================================================================
 */

#include <stdio.h>
#include <string.h>
#include <assert.h>

#include <apr_lib.h>

#include "svn_private_config.h"
#include "svn_hash.h"
#include "svn_iter.h"
#include "svn_client.h"
#include "svn_cmdline.h"
#include "svn_config.h"
#include "svn_error.h"
#include "svn_path.h"
#include "svn_pools.h"
#include "svn_props.h"
#include "svn_string.h"
#include "svn_subst.h"
#include "svn_utf.h"
#include "svn_version.h"
#include "svnmover.h"

#include "private/svn_cmdline_private.h"
#include "private/svn_subr_private.h"
#include "private/svn_branch_repos.h"
#include "private/svn_branch_nested.h"
#include "private/svn_branch_compat.h"
#include "private/svn_ra_private.h"
#include "private/svn_string_private.h"
#include "private/svn_sorts_private.h"
#include "private/svn_token.h"
#include "private/svn_client_private.h"
#include "private/svn_delta_private.h"

#ifdef HAVE_LINENOISE
#include "linenoise/linenoise.h"
#endif

/* Version compatibility check */
static svn_error_t *
check_lib_versions(void)
{
  static const svn_version_checklist_t checklist[] =
    {
      { "svn_client", svn_client_version },
      { "svn_subr",   svn_subr_version },
      { "svn_ra",     svn_ra_version },
      { NULL, NULL }
    };
  SVN_VERSION_DEFINE(my_version);

  return svn_ver_check_list2(&my_version, checklist, svn_ver_equal);
}

static svn_boolean_t quiet = FALSE;

/* UI mode: whether to display output in terms of paths or elements */
int the_ui_mode = UI_MODE_EIDS;
static const svn_token_map_t ui_mode_map[]
  = { {"eids", UI_MODE_EIDS},
      {"e", UI_MODE_EIDS},
      {"paths", UI_MODE_PATHS},
      {"p", UI_MODE_PATHS},
      {"serial", UI_MODE_SERIAL},
      {"s", UI_MODE_SERIAL},
      {NULL, SVN_TOKEN_UNKNOWN} };

#define is_branch_root_element(branch, eid) \
  (svn_branch__root_eid(branch) == (eid))

/* Is BRANCH1 the same branch as BRANCH2? Compare by full branch-ids; don't
   require identical branch objects. */
#define BRANCH_IS_SAME_BRANCH(branch1, branch2, scratch_pool) \
  (strcmp(svn_branch__get_id(branch1, scratch_pool), \
          svn_branch__get_id(branch2, scratch_pool)) == 0)

static svn_boolean_t use_coloured_output = FALSE;

#ifndef WIN32

/* Some ANSI escape codes for controlling text colour in terminal output. */
#define TEXT_RESET      "\x1b[0m"
#define TEXT_FG_BLACK   "\x1b[30m"
#define TEXT_FG_RED     "\x1b[31m"
#define TEXT_FG_GREEN   "\x1b[32m"
#define TEXT_FG_YELLOW  "\x1b[33m"
#define TEXT_FG_BLUE    "\x1b[34m"
#define TEXT_FG_MAGENTA "\x1b[35m"
#define TEXT_FG_CYAN    "\x1b[36m"
#define TEXT_FG_WHITE   "\x1b[37m"
#define TEXT_BG_BLACK   "\x1b[40m"
#define TEXT_BG_RED     "\x1b[41m"
#define TEXT_BG_GREEN   "\x1b[42m"
#define TEXT_BG_YELLOW  "\x1b[43m"
#define TEXT_BG_BLUE    "\x1b[44m"
#define TEXT_BG_MAGENTA "\x1b[45m"
#define TEXT_BG_CYAN    "\x1b[46m"
#define TEXT_BG_WHITE   "\x1b[47m"

#define settext(text_attr) \
  do { \
    if (use_coloured_output) \
      { fputs(text_attr, stdout); fflush(stdout); } \
  } while (0)
#define settext_stderr(text_attr) \
  do { \
    if (use_coloured_output) \
      { fputs(text_attr, stderr); fflush(stderr); } \
  } while (0)

#else

/* To support colour on Windows, we could try:
 *
 * https://github.com/mattn/ansicolor-w32.c
 *
 * (I notice some obvious bugs in its puts/fputs implementations: the #defines
 * point to _fprintf_w32 instead of _fputs_w32, and puts() fails to append a
 * newline).
 */

#define settext(code)
#define settext_stderr(code)

#endif

__attribute__((format(printf, 1, 2)))
void
svnmover_notify(const char *fmt,
                ...)
{
  va_list ap;

  settext(TEXT_FG_GREEN);
  va_start(ap, fmt);
  vprintf(fmt, ap);
  va_end(ap);
  settext(TEXT_RESET);
  printf("\n");
}

__attribute__((format(printf, 1, 2)))
void
svnmover_notify_v(const char *fmt,
                  ...)
{
  va_list ap;

  if (! quiet)
    {
      settext(TEXT_FG_BLUE);
      va_start(ap, fmt);
      vprintf(fmt, ap);
      va_end(ap);
      settext(TEXT_RESET);
      printf("\n");
    }
}

#define SVN_CL__LOG_SEP_STRING \
  "------------------------------------------------------------------------\n"

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

/* Set the WC base revision of element EID to BASE_REV.
 */
static void
svnmover_wc_set_base_rev(svnmover_wc_t *wc,
                         svn_branch__state_t *branch,
                         int eid,
                         svn_revnum_t base_rev)
{
  apr_hash_t *branch_base_revs = svn_hash_gets(wc->base_revs, branch->bid);
  void *val = apr_pmemdup(wc->pool, &base_rev, sizeof(base_rev));

  if (!branch_base_revs)
    {
      branch_base_revs = apr_hash_make(wc->pool);
      svn_hash_sets(wc->base_revs, apr_pstrdup(wc->pool, branch->bid),
                    branch_base_revs);
    }
  svn_eid__hash_set(branch_base_revs, eid, val);
}

/* Get the WC base revision of element EID, or SVN_INVALID_REVNUM if
 * element EID is not present in the WC base.
 */
static svn_revnum_t
svnmover_wc_get_base_rev(svnmover_wc_t *wc,
                         svn_branch__state_t *branch,
                         int eid,
                         apr_pool_t *scratch_pool)
{
  apr_hash_t *branch_base_revs = svn_hash_gets(wc->base_revs, branch->bid);
  svn_error_t *err;
  svn_element__content_t *element;
  svn_revnum_t *base_rev_p;

  if (!branch_base_revs)
    {
      return SVN_INVALID_REVNUM;
    }
  err = svn_branch__state_get_element(branch, &element, eid, scratch_pool);
  if (err || !element)
    {
      svn_error_clear(err);
      return SVN_INVALID_REVNUM;
    }

  base_rev_p = svn_eid__hash_get(branch_base_revs, eid);
  if (! base_rev_p)
    return SVN_INVALID_REVNUM;
  return *base_rev_p;
}

/* Set the WC base revision to BASE_REV for each element in WC base branch
 * BRANCH, including nested branches.
 */
static svn_error_t *
svnmover_wc_set_base_revs_r(svnmover_wc_t *wc,
                            svn_branch__state_t *branch,
                            svn_revnum_t base_rev,
                            apr_pool_t *scratch_pool)
{
  svn_element__tree_t *elements;
  apr_hash_index_t *hi;

  SVN_ERR(svn_branch__state_get_elements(branch, &elements, scratch_pool));
  for (hi = apr_hash_first(scratch_pool, elements->e_map);
       hi; hi = apr_hash_next(hi))
    {
      int eid = svn_eid__hash_this_key(hi);
      svn_element__content_t *element;

      svnmover_wc_set_base_rev(wc, branch, eid, base_rev);

      /* recurse into nested branches */
      SVN_ERR(svn_branch__state_get_element(branch, &element, eid,
                                            scratch_pool));
      if (element->payload->is_subbranch_root)
        {
          const char *subbranch_id
            = svn_branch__id_nest(branch->bid, eid, scratch_pool);
          svn_branch__state_t *subbranch
            = svn_branch__txn_get_branch_by_id(branch->txn, subbranch_id,
                                               scratch_pool);

          SVN_ERR(svnmover_wc_set_base_revs_r(wc, subbranch,
                                              base_rev, scratch_pool));
        }
    }

  return SVN_NO_ERROR;
}

/* Set the WC base revision to BASE_REV for each element in WC base branch
 * BRANCH, including nested branches.
 */
static svn_error_t *
svnmover_wc_set_base_revs(svnmover_wc_t *wc,
                          svn_branch__state_t *branch,
                          svn_revnum_t base_rev,
                          apr_pool_t *scratch_pool)
{
  wc->base_revs = apr_hash_make(wc->pool);
  SVN_ERR(svnmover_wc_set_base_revs_r(wc, branch, base_rev, scratch_pool));
  return SVN_NO_ERROR;
}

/* Get the lowest and highest base revision numbers in WC base branch
 * BRANCH, including nested branches.
 */
static svn_error_t *
svnmover_wc_get_base_revs_r(svnmover_wc_t *wc,
                            svn_revnum_t *base_rev_min,
                            svn_revnum_t *base_rev_max,
                            svn_branch__state_t *branch,
                            apr_pool_t *scratch_pool)
{
  svn_element__tree_t *base_elements;
  apr_hash_index_t *hi;

  SVN_ERR(svn_branch__state_get_elements(branch, &base_elements,
                                         scratch_pool));

  for (hi = apr_hash_first(scratch_pool, base_elements->e_map);
       hi; hi = apr_hash_next(hi))
    {
      int eid = svn_eid__hash_this_key(hi);
      svn_revnum_t rev = svnmover_wc_get_base_rev(wc, branch, eid,
                                                  scratch_pool);
      svn_element__content_t *element;

      if (*base_rev_min == SVN_INVALID_REVNUM
          || rev < *base_rev_min)
        *base_rev_min = rev;
      if (*base_rev_max == SVN_INVALID_REVNUM
          || rev > *base_rev_max)
        *base_rev_max = rev;

      /* recurse into nested branches */
      SVN_ERR(svn_branch__state_get_element(branch, &element, eid,
                                            scratch_pool));
      if (element->payload->is_subbranch_root)
        {
          const char *subbranch_id
            = svn_branch__id_nest(branch->bid, eid, scratch_pool);
          svn_branch__state_t *subbranch
            = svn_branch__txn_get_branch_by_id(branch->txn, subbranch_id,
                                               scratch_pool);

          SVN_ERR(svnmover_wc_get_base_revs_r(wc, base_rev_min, base_rev_max,
                                              subbranch, scratch_pool));
        }
    }

  return SVN_NO_ERROR;
}

/* Get the lowest and highest base revision numbers in WC.
 */
static svn_error_t *
svnmover_wc_get_base_revs(svnmover_wc_t *wc,
                          svn_revnum_t *base_rev_min,
                          svn_revnum_t *base_rev_max,
                          apr_pool_t *scratch_pool)
{
  *base_rev_min = SVN_INVALID_REVNUM;
  *base_rev_max = SVN_INVALID_REVNUM;
  SVN_ERR(svnmover_wc_get_base_revs_r(wc, base_rev_min, base_rev_max,
                                      wc->base->branch, scratch_pool));
  return SVN_NO_ERROR;
}

/* Update the WC to revision BASE_REVISION (SVN_INVALID_REVNUM means HEAD).
 *
 * Requires these fields in WC:
 *   head_revision
 *   repos_root_url
 *   ra_session
 *   pool
 *
 * Initializes these fields in WC:
 *   base_revision
 *   base_branch_id
 *   base_branch
 *   working_branch_id
 *   working_branch
 *   editor
 *
 * Assumes there are no changes in the WC: throws away the existing txn
 * and starts a new one.
 */
static svn_error_t *
wc_checkout(svnmover_wc_t *wc,
            svn_revnum_t base_revision,
            const char *base_branch_id,
            apr_pool_t *scratch_pool)
{
  const char *branch_info_dir = NULL;
  svn_branch__compat_fetch_func_t fetch_func;
  void *fetch_baton;
  svn_branch__txn_t *base_txn;

  /* Validate and store the new base revision number */
  if (! SVN_IS_VALID_REVNUM(base_revision))
    base_revision = wc->head_revision;
  else if (base_revision > wc->head_revision)
    return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL,
                             _("No such revision %ld (HEAD is %ld)"),
                             base_revision, wc->head_revision);

  /* Choose whether to store branching info in a local dir or in revprops.
     (For now, just to exercise the options, we choose local files for
     RA-local and revprops for a remote repo.) */
  if (strncmp(wc->repos_root_url, "file://", 7) == 0)
    {
      const char *repos_dir;

      SVN_ERR(svn_uri_get_dirent_from_file_url(&repos_dir, wc->repos_root_url,
                                               scratch_pool));
      branch_info_dir = svn_dirent_join(repos_dir, "branch-info", scratch_pool);
    }

  /* Get a mutable transaction based on that rev. (This implementation
     re-reads all the move-tracking data from the repository.) */
  SVN_ERR(svn_ra_load_branching_state(&wc->edit_txn,
                                      &fetch_func, &fetch_baton,
                                      wc->ra_session, branch_info_dir,
                                      base_revision,
                                      wc->pool, scratch_pool));

  wc->edit_txn = svn_branch__nested_txn_create(wc->edit_txn, wc->pool);

  /* Store the WC base state */
  base_txn = svn_branch__repos_get_base_revision_root(wc->edit_txn);
  wc->base = apr_pcalloc(wc->pool, sizeof(*wc->base));
  wc->base->revision = base_revision;
  wc->base->branch
    = svn_branch__txn_get_branch_by_id(base_txn, base_branch_id, scratch_pool);
  if (! wc->base->branch)
    return svn_error_createf(SVN_BRANCH__ERR, NULL,
                             "Cannot check out WC: branch %s not found in r%ld",
                             base_branch_id, base_revision);
  SVN_ERR(svnmover_wc_set_base_revs(wc, wc->base->branch,
                                    base_revision, scratch_pool));

  wc->working = apr_pcalloc(wc->pool, sizeof(*wc->working));
  wc->working->revision = SVN_INVALID_REVNUM;
  wc->working->branch
    = svn_branch__txn_get_branch_by_id(wc->edit_txn, base_branch_id,
                                       scratch_pool);
  SVN_ERR_ASSERT(wc->working->branch);

  return SVN_NO_ERROR;
}

/* Create a simulated WC, in memory.
 *
 * Initializes these fields in WC:
 *   head_revision
 *   repos_root_url
 *   ra_session
 *   made_changes
 *   ctx
 *   pool
 *
 * BASE_REVISION is the revision to work on, or SVN_INVALID_REVNUM for HEAD.
 */
static svn_error_t *
wc_create(svnmover_wc_t **wc_p,
          const char *anchor_url,
          svn_revnum_t base_revision,
          const char *base_branch_id,
          svn_client_ctx_t *ctx,
          apr_pool_t *result_pool,
          apr_pool_t *scratch_pool)
{
  apr_pool_t *wc_pool = svn_pool_create(result_pool);
  svnmover_wc_t *wc = apr_pcalloc(wc_pool, sizeof(*wc));

  wc->pool = wc_pool;
  wc->ctx = ctx;

  SVN_ERR(svn_client_open_ra_session2(&wc->ra_session, anchor_url,
                                      NULL /* wri_abspath */, ctx,
                                      wc_pool, scratch_pool));

  SVN_ERR(svn_ra_get_repos_root2(wc->ra_session, &wc->repos_root_url,
                                 result_pool));
  SVN_ERR(svn_ra_get_latest_revnum(wc->ra_session, &wc->head_revision,
                                   scratch_pool));
  SVN_ERR(svn_ra_reparent(wc->ra_session, wc->repos_root_url, scratch_pool));

  SVN_ERR(wc_checkout(wc, base_revision, base_branch_id, scratch_pool));
  *wc_p = wc;
  return SVN_NO_ERROR;
}

svn_error_t *
svnmover_element_differences(apr_hash_t **diff_p,
                             const svn_element__tree_t *left,
                             const svn_element__tree_t *right,
                             apr_hash_t *elements,
                             apr_pool_t *result_pool,
                             apr_pool_t *scratch_pool)
{
  apr_hash_t *diff = apr_hash_make(result_pool);
  apr_hash_index_t *hi;

  if (! left)
    left = svn_element__tree_create(NULL, 0 /*root_eid*/, scratch_pool);
  if (! right)
    right = svn_element__tree_create(NULL, 0 /*root_eid*/, scratch_pool);

  /*SVN_DBG(("element_differences(b%s r%ld, b%s r%ld, e%d)",
           svn_branch__get_id(left->branch, scratch_pool), left->rev,
           svn_branch__get_id(right->branch, scratch_pool), right->rev,
           right->eid));*/

  if (!elements)
    elements = hash_overlay(left->e_map, right->e_map);

  for (hi = apr_hash_first(scratch_pool, elements);
       hi; hi = apr_hash_next(hi))
    {
      int e = svn_eid__hash_this_key(hi);
      svn_element__content_t *element_left
        = svn_element__tree_get(left, e);
      svn_element__content_t *element_right
        = svn_element__tree_get(right, e);

      if (! svn_element__content_equal(element_left, element_right,
                                       scratch_pool))
        {
          svn_element__content_t **contents
            = apr_palloc(result_pool, 2 * sizeof(void *));

          contents[0] = element_left;
          contents[1] = element_right;
          svn_eid__hash_set(diff, e, contents);
        }
    }

  *diff_p = diff;
  return SVN_NO_ERROR;
}

/*  */
static const char *
rev_bid_str(const svn_branch__rev_bid_t *rev_bid,
            apr_pool_t *result_pool)
{
  if (!rev_bid)
    return "<nil>";
  return apr_psprintf(result_pool, "r%ld.%s", rev_bid->rev, rev_bid->bid);
}

/*  */
static const char *
list_parents(svn_branch__history_t *history,
             apr_pool_t *result_pool)
{
  const char *result = "";
  apr_hash_index_t *hi;

  for (hi = apr_hash_first(result_pool, history->parents);
       hi; hi = apr_hash_next(hi))
    {
      svn_branch__rev_bid_t *parent = apr_hash_this_val(hi);
      const char *parent_str = rev_bid_str(parent, result_pool);

      result = apr_psprintf(result_pool, "%s%s%s",
                            result, result[0] ? ", " : "", parent_str);
    }
  return result;
}

/* Return a string representation of HISTORY.
 */
static const char *
history_str(svn_branch__history_t *history,
            apr_pool_t *result_pool)
{
  const char *result
    = list_parents(history, result_pool);

  return apr_psprintf(result_pool, "parents={%s}", result);
}

/*
 */
static svn_error_t *
svn_branch__history_add_parent(svn_branch__history_t *history,
                               svn_revnum_t rev,
                               const char *branch_id,
                               apr_pool_t *scratch_pool)
{
  apr_pool_t *pool = apr_hash_pool_get(history->parents);
  svn_branch__rev_bid_t *new_parent;

  new_parent = svn_branch__rev_bid_create(rev, branch_id, pool);
  svn_hash_sets(history->parents, apr_pstrdup(pool, branch_id), new_parent);
  return SVN_NO_ERROR;
}

/* Set *DIFFERENCE_P to some sort of indication of the difference between
 * HISTORY1 and HISTORY2, or to null if there is no difference.
 *
 * Inputs may be null.
 */
static svn_error_t *
history_diff(const char **difference_p,
             svn_branch__history_t *history1,
             svn_branch__history_t *history2,
             apr_pool_t *result_pool,
             apr_pool_t *scratch_pool)
{
  apr_hash_t *combined;
  apr_hash_index_t *hi;
  svn_boolean_t different = FALSE;

  if (! history1)
    history1 = svn_branch__history_create_empty(scratch_pool);
  if (! history2)
    history2 = svn_branch__history_create_empty(scratch_pool);
  combined = hash_overlay(history1->parents,
                          history2->parents);

  for (hi = apr_hash_first(scratch_pool, combined);
       hi; hi = apr_hash_next(hi))
    {
      const char *bid = apr_hash_this_key(hi);
      svn_branch__rev_bid_t *parent1 = svn_hash_gets(history1->parents, bid);
      svn_branch__rev_bid_t *parent2 = svn_hash_gets(history2->parents, bid);

      if (!(parent1 && parent2
            && svn_branch__rev_bid_equal(parent1, parent2)))
        {
          different = TRUE;
          break;
        }
    }
  if (different)
    {
      *difference_p = apr_psprintf(result_pool, "%s -> %s",
                                   history_str(history1, scratch_pool),
                                   history_str(history2, scratch_pool));
    }
  else
    {
      *difference_p = NULL;
    }
  return SVN_NO_ERROR;
}

/* Set *IS_CHANGED to true if EDIT_TXN differs from its base txn, else to
 * false.
 *
 * Notice only a difference in content: branches deleted or added, or branch
 * contents different. Ignore any differences in branch history metadata.
 *
 * ### At least we must ignore the "this branch" parent changing from
 *     old-revision to new-revision. However we should probably notice
 *     if a merge parent is added (which means we want to make a commit
 *     recording this merge, even if no content changed), and perhaps
 *     other cases.
 */
static svn_error_t *
txn_is_changed(svn_branch__txn_t *edit_txn,
               svn_boolean_t *is_changed,
               apr_pool_t *scratch_pool)
{
  int i;
  svn_branch__txn_t *base_txn
    = svn_branch__repos_get_base_revision_root(edit_txn);
  apr_array_header_t *edit_branches
    = svn_branch__txn_get_branches(edit_txn, scratch_pool);
  apr_array_header_t *base_branches
    = svn_branch__txn_get_branches(base_txn, scratch_pool);

  *is_changed = FALSE;

  /* If any previous branch is now missing, that's a change. */
  for (i = 0; i < base_branches->nelts; i++)
    {
      svn_branch__state_t *base_branch = APR_ARRAY_IDX(base_branches, i, void *);
      svn_branch__state_t *edit_branch
        = svn_branch__txn_get_branch_by_id(edit_txn, base_branch->bid,
                                           scratch_pool);

      if (! edit_branch)
        {
          *is_changed = TRUE;
          return SVN_NO_ERROR;
        }
    }

  /* If any current branch is new or changed, that's a change. */
  for (i = 0; i < edit_branches->nelts; i++)
    {
      svn_branch__state_t *edit_branch = APR_ARRAY_IDX(edit_branches, i, void *);
      svn_branch__state_t *base_branch
        = svn_branch__txn_get_branch_by_id(base_txn, edit_branch->bid,
                                           scratch_pool);
      svn_element__tree_t *edit_branch_elements, *base_branch_elements;
      apr_hash_t *diff;

      if (! base_branch)
        {
          *is_changed = TRUE;
          return SVN_NO_ERROR;
        }

#if 0
      /* Compare histories */
      /* ### No, don't. Ignore any differences in branch history metadata. */
      {
      svn_branch__history_t *edit_branch_history;
      svn_branch__history_t *base_branch_history;
      const char *history_difference;

      SVN_ERR(svn_branch__state_get_history(edit_branch, &edit_branch_history,
                                            scratch_pool));
      SVN_ERR(svn_branch__state_get_history(base_branch, &base_branch_history,
                                            scratch_pool));
      SVN_ERR(history_diff(&history_difference,
                           edit_branch_history,
                           base_branch_history,
                           scratch_pool, scratch_pool));
      if (history_difference)
        {
          *is_changed = TRUE;
          return SVN_NO_ERROR;
        }
      }
#endif

      /* Compare elements */
      SVN_ERR(svn_branch__state_get_elements(edit_branch, &edit_branch_elements,
                                             scratch_pool));
      SVN_ERR(svn_branch__state_get_elements(base_branch, &base_branch_elements,
                                             scratch_pool));
      SVN_ERR(svnmover_element_differences(&diff,
                                           edit_branch_elements,
                                           base_branch_elements,
                                           NULL /*all elements*/,
                                           scratch_pool, scratch_pool));
      if (apr_hash_count(diff))
        {
          *is_changed = TRUE;
          return SVN_NO_ERROR;
        }
    }

  return SVN_NO_ERROR;
}

/* Replay the whole-element changes between LEFT_BRANCH and RIGHT_BRANCH
 * into EDIT_BRANCH.
 *
 * Replaying means, for each element E that is changed (added, modified
 * or deleted) between left and right branches, we set element E in
 * EDIT_BRANCH to whole value of E in RIGHT_BRANCH. This is not like
 * merging: each change resets an element's whole value.
 *
 * ELEMENTS_TO_DIFF (eid -> [anything]) says which elements to diff; if
 * null, diff all elements in the union of left & right branches.
 *
 * LEFT_BRANCH and/or RIGHT_BRANCH may be null which means the equivalent
 * of an empty branch.
 *
 * Non-recursive: single branch only.
 */
static svn_error_t *
branch_elements_replay(svn_branch__state_t *edit_branch,
                       const svn_branch__state_t *left_branch,
                       const svn_branch__state_t *right_branch,
                       apr_hash_t *elements_to_diff,
                       apr_pool_t *scratch_pool)
{
  svn_element__tree_t *s_left = NULL, *s_right = NULL;
  apr_hash_t *diff_left_right;
  apr_hash_index_t *hi;

  if (left_branch)
    SVN_ERR(svn_branch__state_get_elements(left_branch, &s_left,
                                           scratch_pool));
  if (right_branch)
    SVN_ERR(svn_branch__state_get_elements(right_branch, &s_right,
                                           scratch_pool));
  SVN_ERR(svnmover_element_differences(&diff_left_right,
                                       s_left, s_right,
                                       elements_to_diff,
                                       scratch_pool, scratch_pool));

  /* Go through the per-element differences. */
  for (hi = apr_hash_first(scratch_pool, diff_left_right);
       hi; hi = apr_hash_next(hi))
    {
      int eid = svn_eid__hash_this_key(hi);
      svn_element__content_t **e_pair = apr_hash_this_val(hi);
      svn_element__content_t *e0 = e_pair[0], *e1 = e_pair[1];

      SVN_ERR_ASSERT(!e0
                     || svn_element__payload_invariants(e0->payload));
      SVN_ERR_ASSERT(!e1
                     || svn_element__payload_invariants(e1->payload));
      SVN_ERR(svn_branch__state_set_element(edit_branch, eid,
                                            e1, scratch_pool));
    }

  return SVN_NO_ERROR;
}

/*  */
static svn_error_t *
get_union_of_subbranches(apr_hash_t **all_subbranches_p,
                         svn_branch__state_t *left_branch,
                         svn_branch__state_t *right_branch,
                         apr_pool_t *result_pool)
{
  apr_hash_t *all_subbranches;
  svn_branch__subtree_t *s_left = NULL;
  svn_branch__subtree_t *s_right = NULL;

  if (left_branch)
    SVN_ERR(svn_branch__get_subtree(left_branch, &s_left,
                                    svn_branch__root_eid(left_branch),
                                    result_pool));
  if (right_branch)
    SVN_ERR(svn_branch__get_subtree(right_branch, &s_right,
                                    svn_branch__root_eid(right_branch),
                                    result_pool));
  all_subbranches
    = (s_left && s_right) ? hash_overlay(s_left->subbranches,
                                         s_right->subbranches)
        : s_left ? s_left->subbranches
        : s_right ? s_right->subbranches
        : apr_hash_make(result_pool);

  *all_subbranches_p = all_subbranches;
  return SVN_NO_ERROR;
}

/* Replay differences between S_LEFT and S_RIGHT into EDITOR:EDIT_BRANCH.
 *
 * S_LEFT or S_RIGHT (but not both) may be null meaning an empty set.
 *
 * Recurse into subbranches.
 */
static svn_error_t *
svn_branch__replay(svn_branch__txn_t *edit_txn,
                   svn_branch__state_t *edit_branch,
                   svn_branch__state_t *left_branch,
                   svn_branch__state_t *right_branch,
                   apr_pool_t *scratch_pool)
{
  assert((left_branch && right_branch)
         ? (svn_branch__root_eid(left_branch) == svn_branch__root_eid(right_branch))
         : (left_branch || right_branch));

  if (right_branch)
    {
      /* Replay this branch */
      apr_hash_t *elements_to_diff = NULL;  /*means the union of left & right*/

      SVN_ERR(branch_elements_replay(edit_branch, left_branch, right_branch,
                                     elements_to_diff, scratch_pool));
    }
  else
    {
      /* deleted branch LEFT */
      /* nothing to do -- it will go away because we deleted the outer-branch
         element where it was attached */
    }

  /* Replay any change in history */
  /* ### Actually, here we just set the output history to the right-hand-side
     history if that differs from left-hand-side.
     This doesn't seem right, in general. It's OK if we're just copying
     a txn into a fresh txn, as for example we do during commit. */
  {
    svn_branch__history_t *left_history = NULL;
    svn_branch__history_t *right_history = NULL;
    const char *history_difference;

    if (left_branch)
      SVN_ERR(svn_branch__state_get_history(left_branch, &left_history,
                                            scratch_pool));
    if (right_branch)
      SVN_ERR(svn_branch__state_get_history(right_branch, &right_history,
                                            scratch_pool));
    SVN_ERR(history_diff(&history_difference, left_history, right_history,
                         scratch_pool, scratch_pool));
    if (history_difference)
      {
        SVN_ERR(svn_branch__state_set_history(edit_branch, right_history,
                                              scratch_pool));
      }
  }

  /* Replay its subbranches, recursively.
     (If we're deleting the current branch, we don't also need to
     explicitly delete its subbranches... do we?) */
  if (right_branch)
    {
      apr_hash_t *all_subbranches;
      apr_hash_index_t *hi;

      SVN_ERR(get_union_of_subbranches(&all_subbranches,
                                       left_branch, right_branch, scratch_pool));
      for (hi = apr_hash_first(scratch_pool, all_subbranches);
           hi; hi = apr_hash_next(hi))
        {
          int this_eid = svn_eid__hash_this_key(hi);
          svn_branch__state_t *left_subbranch = NULL;
          svn_branch__state_t *right_subbranch = NULL;
          svn_branch__state_t *edit_subbranch = NULL;

          if (left_branch)
            SVN_ERR(svn_branch__get_subbranch_at_eid(
                      left_branch, &left_subbranch, this_eid, scratch_pool));
          if (right_branch)
            SVN_ERR(svn_branch__get_subbranch_at_eid(
                      right_branch, &right_subbranch, this_eid, scratch_pool));
          /* If the subbranch is to be edited or added, first look up the
             corresponding edit subbranch, or, if not found, create one. */
          if (right_subbranch)
            {
              const char *new_branch_id
                = svn_branch__id_nest(edit_branch->bid, this_eid, scratch_pool);

              SVN_ERR(svn_branch__txn_open_branch(edit_txn, &edit_subbranch,
                                                  new_branch_id,
                                                  svn_branch__root_eid(right_subbranch),
                                                  NULL /*tree_ref*/,
                                                  scratch_pool, scratch_pool));
            }

          /* recurse */
          if (edit_subbranch)
            {
              SVN_ERR(svn_branch__replay(edit_txn, edit_subbranch,
                                        left_subbranch, right_subbranch,
                                        scratch_pool));
            }
        }
    }

  return SVN_NO_ERROR;
}

/* Replay differences between LEFT_BRANCH and RIGHT_BRANCH into
 * EDIT_ROOT_BRANCH.
 * (Recurse into subbranches.)
 */
static svn_error_t *
replay(svn_branch__txn_t *edit_txn,
       svn_branch__state_t *edit_root_branch,
       svn_branch__state_t *left_branch,
       svn_branch__state_t *right_branch,
       apr_pool_t *scratch_pool)
{
  SVN_ERR_ASSERT(left_branch || right_branch);

  SVN_ERR(svn_branch__replay(edit_txn, edit_root_branch,
                            left_branch, right_branch, scratch_pool));
  return SVN_NO_ERROR;
}

static svn_error_t *
commit_callback(const svn_commit_info_t *commit_info,
                void *baton,
                apr_pool_t *pool);

/* Baton for commit_callback(). */
typedef struct commit_callback_baton_t
{
  svn_branch__txn_t *edit_txn;
  const char *wc_base_branch_id;
  const char *wc_commit_branch_id;

  /* just-committed revision */
  svn_revnum_t revision;
} commit_callback_baton_t;

static svn_error_t *
display_diff_of_commit(const commit_callback_baton_t *ccbb,
                       apr_pool_t *scratch_pool);

static svn_error_t *
do_topbranch(svn_branch__state_t **new_branch_p,
             svn_branch__txn_t *txn,
             svn_branch__rev_bid_eid_t *from,
             apr_pool_t *result_pool,
             apr_pool_t *scratch_pool);

/* Allocate the same number of new EIDs in NEW_TXN as are already
 * allocated in OLD_TXN.
 */
static svn_error_t *
allocate_eids(svn_branch__txn_t *new_txn,
              const svn_branch__txn_t *old_txn,
              apr_pool_t *scratch_pool)
{
  int num_new_eids;
  int i;

  SVN_ERR(svn_branch__txn_get_num_new_eids(old_txn, &num_new_eids,
                                           scratch_pool));
  for (i = 0; i < num_new_eids; i++)
    {
      SVN_ERR(svn_branch__txn_new_eid(new_txn, NULL, scratch_pool));
    }

  return SVN_NO_ERROR;
}

/* Update the EIDs, given that a commit has translated all new EIDs
 * (negative numbers) to regular EIDs (positive numbers).
 *
 * ### TODO: This will need to take and use a new-EID-translation rule
 *     that must be returned by the commit, as we must not guess (as we
 *     presently do) what translation the server performed. This guess
 *     will fail once the server does rebasing on commit.
 */
static svn_error_t *
update_wc_eids(svnmover_wc_t *wc,
               apr_pool_t *scratch_pool)
{
  SVN_ERR(allocate_eids(wc->base->branch->txn, wc->working->branch->txn,
                        scratch_pool));
  SVN_ERR(svn_branch__txn_finalize_eids(wc->base->branch->txn, scratch_pool));
  SVN_ERR(svn_branch__txn_finalize_eids(wc->working->branch->txn, scratch_pool));
  return SVN_NO_ERROR;
}

/* Update the WC base value of each committed element to match the
 * corresponding WC working element value.
 * Update the WC base revision for each committed element to NEW_REV.
 *
 * The committed elements are determined by diffing base against working.
 * ### TODO: When we allow committing a subset of the WC, we'll need to
 *     pass in a list of the committed elements.
 *
 * BASE_BRANCH and/or WORK_BRANCH may be null.
 */
static svn_error_t *
update_wc_base_r(svnmover_wc_t *wc,
                 svn_branch__state_t *base_branch,
                 svn_branch__state_t *work_branch,
                 svn_revnum_t new_rev,
                 apr_pool_t *scratch_pool)
{
  svn_element__tree_t *base_elements = NULL, *working_elements = NULL;
  apr_hash_t *committed_elements;
  apr_hash_index_t *hi;

  if (base_branch)
    SVN_ERR(svn_branch__state_get_elements(base_branch, &base_elements,
                                           scratch_pool));
  if (work_branch)
    SVN_ERR(svn_branch__state_get_elements(work_branch, &working_elements,
                                           scratch_pool));
  SVN_ERR(svnmover_element_differences(&committed_elements,
                                       base_elements, working_elements,
                                       NULL /*all elements*/,
                                       scratch_pool, scratch_pool));

  for (hi = apr_hash_first(scratch_pool, committed_elements);
       hi; hi = apr_hash_next(hi))
    {
      int eid = svn_eid__hash_this_key(hi);
      svn_element__content_t *content = NULL;

      if (work_branch)
        SVN_ERR(svn_branch__state_get_element(work_branch, &content,
                                              eid, scratch_pool));
      SVN_ERR(svn_branch__state_set_element(base_branch, eid,
                                            content, scratch_pool));
      svnmover_wc_set_base_rev(wc, base_branch, eid, new_rev);

      /* recurse into nested branches that exist in working */
      if (content && content->payload->is_subbranch_root)
        {
          svn_branch__state_t *base_subbranch = NULL;
          svn_branch__state_t *work_subbranch = NULL;

          if (base_branch)
            {
              base_subbranch
                = svn_branch__txn_get_branch_by_id(
                    base_branch->txn,
                    svn_branch__id_nest(base_branch->bid, eid, scratch_pool),
                    scratch_pool);
            }
          if (work_branch)
            {
              work_subbranch
                = svn_branch__txn_get_branch_by_id(
                    work_branch->txn,
                    svn_branch__id_nest(work_branch->bid, eid, scratch_pool),
                    scratch_pool);
            }
          if (work_subbranch && !base_subbranch)
            {
              const char *new_branch_id
                = svn_branch__id_nest(base_branch->bid, eid, scratch_pool);
              svn_branch__history_t *history;

              SVN_ERR(svn_branch__txn_open_branch(base_branch->txn,
                                                  &base_subbranch,
                                                  new_branch_id,
                                                  svn_branch__root_eid(work_subbranch),
                                                  NULL /*tree_ref*/,
                                                  scratch_pool, scratch_pool));
              SVN_ERR(svn_branch__state_get_history(
                        work_subbranch, &history, scratch_pool));
              SVN_ERR(svn_branch__state_set_history(
                        base_subbranch, history, scratch_pool));
            }
          SVN_ERR(update_wc_base_r(wc, base_subbranch, work_subbranch,
                                   new_rev, scratch_pool));
        }
    }

  return SVN_NO_ERROR;
}

/* Update the WC base value of each committed element to match the
 * corresponding WC working element value.
 * Update the WC base revision for each committed element to NEW_REV.
 *
 * The committed elements are determined by diffing base against working.
 * ### TODO: When we allow committing a subset of the WC, we'll need to
 *     pass in a list of the committed elements.
 *
 * ### This should be equivalent to 'replay(base, base, working)'. Use that
 *     instead.
 */
static svn_error_t *
update_wc_base(svnmover_wc_t *wc,
               svn_revnum_t new_rev,
               apr_pool_t *scratch_pool)
{
  svn_branch__state_t *base_branch = wc->base->branch;
  svn_branch__state_t *work_branch = wc->working->branch;
  SVN_ERR(update_wc_base_r(wc, base_branch, work_branch,
                           new_rev, scratch_pool));
  return SVN_NO_ERROR;
}

/* Commit the changes from WC into the repository.
 *
 * Open a new commit txn to the repo. Replay the changes from WC into it.
 * Update the WC base for the committed elements.
 *
 * Set WC->head_revision and *NEW_REV_P to the committed revision number.
 *
 * If there are no changes to commit, set *NEW_REV_P to SVN_INVALID_REVNUM
 * and do not make a commit and do not change WC->head_revision.
 *
 * NEW_REV_P may be null if not wanted.
 */
static svn_error_t *
wc_commit(svn_revnum_t *new_rev_p,
          svnmover_wc_t *wc,
          apr_hash_t *revprops,
          apr_pool_t *scratch_pool)
{
  const char *branch_info_dir = NULL;
  svn_branch__txn_t *commit_txn;
  commit_callback_baton_t ccbb;
  svn_boolean_t change_detected;
  const char *edit_root_branch_id;
  svn_branch__state_t *edit_root_branch;

  SVN_ERR(txn_is_changed(wc->working->branch->txn, &change_detected,
                         scratch_pool));
  if (! change_detected)
    {
      wc->list_of_commands = NULL;
      if (new_rev_p)
        *new_rev_p = SVN_INVALID_REVNUM;
      return SVN_NO_ERROR;
    }

  /* If no log msg provided, use the list of commands */
  if (! svn_hash_gets(revprops, SVN_PROP_REVISION_LOG) && wc->list_of_commands)
    {
      /* Avoid modifying the passed-in revprops hash */
      revprops = apr_hash_copy(scratch_pool, revprops);

      svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
                    svn_string_create(wc->list_of_commands, scratch_pool));
    }

  /* Choose whether to store branching info in a local dir or in revprops.
     (For now, just to exercise the options, we choose local files for
     RA-local and revprops for a remote repo.) */
  if (strncmp(wc->repos_root_url, "file://", 7) == 0)
    {
      const char *repos_dir;

      SVN_ERR(svn_uri_get_dirent_from_file_url(&repos_dir, wc->repos_root_url,
                                               scratch_pool));
      branch_info_dir = svn_dirent_join(repos_dir, "branch-info", scratch_pool);
    }

  /* Start a new editor for the commit. */
  SVN_ERR(svn_ra_get_commit_txn(wc->ra_session,
                                &commit_txn,
                                revprops,
                                commit_callback, &ccbb,
                                NULL /*lock_tokens*/, FALSE /*keep_locks*/,
                                branch_info_dir,
                                scratch_pool));
  /*SVN_ERR(svn_branch__txn_get_debug(&wc->edit_txn, wc->edit_txn, scratch_pool));*/

  edit_root_branch_id = wc->working->branch->bid;
  edit_root_branch = svn_branch__txn_get_branch_by_id(
                       commit_txn, wc->working->branch->bid, scratch_pool);

  /* We might be creating a new top-level branch in this commit. That is the
     only case in which the working branch will not be found in EDIT_TXN.
     (Creating any other branch can only be done inside a checkout of a
     parent branch.) So, maybe create a new top-level branch. */
  if (! edit_root_branch)
    {
      /* Create a new top-level branch in the edited state. (It will have
         an independent new top-level branch number.) */
      svn_branch__rev_bid_eid_t *from
        = svn_branch__rev_bid_eid_create(wc->base->revision,
                                         wc->base->branch->bid,
                                         svn_branch__root_eid(wc->base->branch),
                                         scratch_pool);

      SVN_ERR(do_topbranch(&edit_root_branch, commit_txn,
                           from, scratch_pool, scratch_pool));
      edit_root_branch_id = edit_root_branch->bid;
    }
  /* Allocate all the new eids we'll need in this new txn */
  SVN_ERR(allocate_eids(commit_txn, wc->working->branch->txn, scratch_pool));
  SVN_ERR(replay(commit_txn, edit_root_branch,
                 wc->base->branch,
                 wc->working->branch,
                 scratch_pool));

  ccbb.edit_txn = commit_txn;
  ccbb.wc_base_branch_id = wc->base->branch->bid;
  ccbb.wc_commit_branch_id = edit_root_branch_id;

  SVN_ERR(svn_branch__txn_complete(commit_txn, scratch_pool));
  SVN_ERR(update_wc_eids(wc, scratch_pool));
  SVN_ERR(update_wc_base(wc, ccbb.revision, scratch_pool));
  SVN_ERR(display_diff_of_commit(&ccbb, scratch_pool));

  wc->head_revision = ccbb.revision;
  if (new_rev_p)
    *new_rev_p = ccbb.revision;

  wc->list_of_commands = NULL;

  return SVN_NO_ERROR;
}

typedef enum action_code_t {
  ACTION_INFO_WC,
  ACTION_INFO,
  ACTION_LIST_CONFLICTS,
  ACTION_RESOLVED_CONFLICT,
  ACTION_DIFF,
  ACTION_LOG,
  ACTION_LIST_BRANCHES,
  ACTION_LIST_BRANCHES_R,
  ACTION_LS,
  ACTION_TBRANCH,
  ACTION_BRANCH,
  ACTION_BRANCH_INTO,
  ACTION_MKBRANCH,
  ACTION_MERGE3,
  ACTION_AUTO_MERGE,
  ACTION_MV,
  ACTION_MKDIR,
  ACTION_PUT_FILE,
  ACTION_CAT,
  ACTION_CP,
  ACTION_RM,
  ACTION_CP_RM,
  ACTION_BR_RM,
  ACTION_BR_INTO_RM,
  ACTION_COMMIT,
  ACTION_UPDATE,
  ACTION_SWITCH,
  ACTION_STATUS,
  ACTION_REVERT,
  ACTION_MIGRATE
} action_code_t;

typedef struct action_defn_t {
  enum action_code_t code;
  const char *name;
  int num_args;
  const char *args_help;
  const char *help;
} action_defn_t;

#define NL "\n                           "
static const action_defn_t action_defn[] =
{
  {ACTION_INFO_WC,          "info-wc", 0, "",
    "print information about the WC"},
  {ACTION_INFO,             "info", 1, "PATH",
    "show info about the element at PATH"},
  {ACTION_LIST_CONFLICTS,   "conflicts", 0, "",
    "list unresolved conflicts"},
  {ACTION_RESOLVED_CONFLICT,"resolved", 1, "CONFLICT_ID",
    "mark conflict as resolved"},
  {ACTION_LIST_BRANCHES,    "branches", 1, "PATH",
    "list all branches rooted at the same element as PATH"},
  {ACTION_LIST_BRANCHES_R,  "ls-br-r", 0, "",
    "list all branches, recursively"},
  {ACTION_LS,               "ls", 1, "PATH",
    "list elements in the branch found at PATH"},
  {ACTION_LOG,              "log", 2, "FROM@REV TO@REV",
    "show per-revision diffs between FROM and TO"},
  {ACTION_TBRANCH,          "tbranch", 1, "SRC",
    "branch the branch-root or branch-subtree at SRC" NL
    "to make a new top-level branch"},
  {ACTION_BRANCH,           "branch", 2, "SRC DST",
    "branch the branch-root or branch-subtree at SRC" NL
    "to make a new branch at DST"},
  {ACTION_BRANCH_INTO,      "branch-into", 2, "SRC DST",
    "make a branch of the existing subtree SRC appear at" NL
    "DST as part of the existing branch that contains DST" NL
    "(like merging the creation of SRC to DST)"},
  {ACTION_MKBRANCH,         "mkbranch", 1, "ROOT",
    "make a directory that's the root of a new subbranch"},
  {ACTION_DIFF,             "diff", 2, "LEFT@REV RIGHT@REV",
    "show differences from subtree LEFT to subtree RIGHT"},
  {ACTION_MERGE3,           "merge", 3, "FROM TO YCA@REV",
    "3-way merge YCA->FROM into TO"},
  {ACTION_AUTO_MERGE,       "automerge", 2, "FROM TO",
    "automatic merge FROM into TO"},
  {ACTION_CP,               "cp", 2, "REV SRC DST",
    "copy SRC@REV to DST"},
  {ACTION_MV,               "mv", 2, "SRC DST",
    "move SRC to DST"},
  {ACTION_RM,               "rm", 1, "PATH",
    "delete PATH"},
  {ACTION_CP_RM,            "copy-and-delete", 2, "SRC DST",
    "copy-and-delete SRC to DST"},
  {ACTION_BR_RM,            "branch-and-delete", 2, "SRC DST",
    "branch-and-delete SRC to DST"},
  {ACTION_BR_INTO_RM,       "branch-into-and-delete", 2, "SRC DST",
    "merge-and-delete SRC to DST"},
  {ACTION_MKDIR,            "mkdir", 1, "PATH",
    "create new directory PATH"},
  {ACTION_PUT_FILE,         "put", 2, "LOCAL_FILE PATH",
    "add or modify file PATH with text copied from" NL
    "LOCAL_FILE (use \"-\" to read from standard input)"},
  {ACTION_CAT,              "cat", 1, "PATH",
    "display text (for a file) and props (if any) of PATH"},
  {ACTION_COMMIT,           "commit", 0, "",
    "commit the changes"},
  {ACTION_UPDATE,           "update", 1, ".@REV",
    "update to revision REV, keeping local changes"},
  {ACTION_SWITCH,           "switch", 1, "TARGET[@REV]",
    "switch to another branch and/or revision, keeping local changes"},
  {ACTION_STATUS,           "status", 0, "",
    "same as 'diff .@base .'"},
  {ACTION_REVERT,           "revert", 0, "",
    "revert all uncommitted changes"},
  {ACTION_MIGRATE,          "migrate", 1, ".@REV",
    "migrate changes from non-move-tracking revision"},
};

typedef struct action_t {
  /* The original command words (const char *) by which the action was
     specified */
  apr_array_header_t *action_args;

  action_code_t action;

  /* argument revisions */
  svn_opt_revision_t rev_spec[3];

  const char *branch_id[3];

  /* argument paths */
  const char *relpath[3];
} action_t;

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

/* Find the deepest branch in the repository of which REVNUM:BRANCH_ID:RELPATH
 * is either the root element or a normal, non-sub-branch element.
 *
 * RELPATH is a repository-relative path. REVNUM is a revision number, or
 * SVN_INVALID_REVNUM meaning the current txn.
 *
 * Return the location of the element in that branch, or with
 * EID=-1 if no element exists there.
 *
 * If BRANCH_ID is null, the default is the WC base branch when REVNUM is
 * specified, and the WC working branch when REVNUM is SVN_INVALID_REVNUM.
 *
 * Return an error if branch BRANCH_ID does not exist in r<REVNUM>; otherwise,
 * the result will never be NULL, as every path is within at least the root
 * branch.
 */
static svn_error_t *
find_el_rev_by_rrpath_rev(svn_branch__el_rev_id_t **el_rev_p,
                          svnmover_wc_t *wc,
                          const svn_opt_revision_t *rev_spec,
                          const char *branch_id,
                          const char *relpath,
                          apr_pool_t *result_pool,
                          apr_pool_t *scratch_pool)
{
  if (rev_spec->kind == svn_opt_revision_number
      || rev_spec->kind == svn_opt_revision_head)
    {
      svn_revnum_t revnum
        = (rev_spec->kind == svn_opt_revision_number)
           ? rev_spec->value.number : wc->head_revision;
      const svn_branch__repos_t *repos = wc->working->branch->txn->repos;

      if (! branch_id)
        branch_id = wc->base->branch->bid;
      SVN_ERR(svn_branch__repos_find_el_rev_by_path_rev(el_rev_p, repos,
                                                        revnum,
                                                        branch_id,
                                                        relpath,
                                                        result_pool,
                                                        scratch_pool));
    }
  else if (rev_spec->kind == svn_opt_revision_unspecified
           || rev_spec->kind == svn_opt_revision_working
           || rev_spec->kind == svn_opt_revision_base
           || rev_spec->kind == svn_opt_revision_committed)
    {
      svn_branch__state_t *branch
        = branch_id ? svn_branch__txn_get_branch_by_id(
                        wc->working->branch->txn, branch_id, scratch_pool)
                    : wc->working->branch;
      svn_branch__el_rev_id_t *el_rev = apr_palloc(result_pool, sizeof(*el_rev));

      if (! branch)
        return svn_error_createf(SVN_BRANCH__ERR, NULL,
                                 _("Branch %s not found in working state"),
                                 branch_id);
      SVN_ERR(svn_branch__find_nested_branch_element_by_relpath(
        &el_rev->branch, &el_rev->eid,
        branch, relpath, scratch_pool));
      if (rev_spec->kind == svn_opt_revision_unspecified
          || rev_spec->kind == svn_opt_revision_working)
        {
          el_rev->rev = SVN_INVALID_REVNUM;
        }
      else
        {
          el_rev->rev = svnmover_wc_get_base_rev(wc, el_rev->branch,
                                                 el_rev->eid, scratch_pool);
        }
      *el_rev_p = el_rev;
    }
  else
    {
      return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                               "'%s@...': revision specifier "
                               "must be a number or 'head', 'base' "
                               "or 'committed'",
                               relpath);
    }
  SVN_ERR_ASSERT(*el_rev_p);
  return SVN_NO_ERROR;
}

/* Return a string suitable for appending to a displayed element name or
 * element id to indicate that it is a subbranch root element for SUBBRANCH.
 * Return "" if SUBBRANCH is null.
 */
static const char *
branch_str(svn_branch__state_t *subbranch,
           apr_pool_t *result_pool)
{
  if (subbranch)
    return apr_psprintf(result_pool,
                      " (branch %s)",
                      svn_branch__get_id(subbranch, result_pool));
  return "";
}

/* Return a string suitable for appending to a displayed element name or
 * element id to indicate that BRANCH:EID is a subbranch root element.
 * Return "" if the element is not a subbranch root element.
 */
static const char *
subbranch_str(svn_branch__state_t *branch,
              int eid,
              apr_pool_t *result_pool)
{
  svn_branch__state_t *subbranch;

  svn_error_clear(svn_branch__get_subbranch_at_eid(branch, &subbranch,
                                                   eid, result_pool));
  return branch_str(subbranch, result_pool);
}

/*  */
static const char *
subtree_subbranch_str(svn_branch__subtree_t *subtree,
                      const char *bid,
                      int eid,
                      apr_pool_t *result_pool)
{
  svn_branch__subtree_t *subbranch
    = svn_branch__subtree_get_subbranch_at_eid(subtree, eid, result_pool);

  if (subbranch)
    return apr_psprintf(result_pool,
                        " (branch %s)",
                        svn_branch__id_nest(bid, eid, result_pool));
  return "";
}

/*  */
static const char *
el_rev_id_to_path(svn_branch__el_rev_id_t *el_rev,
                  apr_pool_t *result_pool)
{
  const char *path
    = svn_branch__get_rrpath_by_eid(el_rev->branch, el_rev->eid, result_pool);

  return path;
}

/*  */
static const char *
branch_peid_name_to_path(svn_branch__state_t *to_branch,
                         int to_parent_eid,
                         const char *to_name,
                         apr_pool_t *result_pool)
{
  const char *path
    = svn_relpath_join(svn_branch__get_rrpath_by_eid(to_branch, to_parent_eid,
                                                     result_pool),
                       to_name, result_pool);

  return path;
}

/* */
static int
sort_compare_eid_mappings_by_path(const svn_sort__item_t *a,
                                  const svn_sort__item_t *b)
{
  const char *astr = a->value, *bstr = b->value;

  return svn_path_compare_paths(astr, bstr);
}

/* List the elements in BRANCH, in path notation.
 *
 * List only the elements for which a relpath is known -- that is, elements
 * whose parents exist all the way up to the branch root.
 */
static svn_error_t *
list_branch_elements(svn_branch__state_t *branch,
                     apr_pool_t *scratch_pool)
{
  apr_hash_t *eid_to_path = apr_hash_make(scratch_pool);
  svn_element__tree_t *elements;
  apr_hash_index_t *hi;
  svn_eid__hash_iter_t *ei;

  SVN_ERR(svn_branch__state_get_elements(branch, &elements, scratch_pool));
  for (hi = apr_hash_first(scratch_pool, elements->e_map);
       hi; hi = apr_hash_next(hi))
    {
      int eid = svn_eid__hash_this_key(hi);
      const char *relpath = svn_branch__get_path_by_eid(branch, eid,
                                                        scratch_pool);

      svn_eid__hash_set(eid_to_path, eid, relpath);
    }
  for (SVN_EID__HASH_ITER_SORTED(ei, eid_to_path,
                                 sort_compare_eid_mappings_by_path,
                                 scratch_pool))
    {
      int eid = ei->eid;
      const char *relpath = ei->val;

      svnmover_notify("    %-20s%s",
                      relpath[0] ? relpath : ".",
                      subbranch_str(branch, eid, scratch_pool));
    }

  return SVN_NO_ERROR;
}

/*  */
static int
sort_compare_items_by_eid(const svn_sort__item_t *a,
                          const svn_sort__item_t *b)
{
  int eid_a = *(const int *)a->key;
  int eid_b = *(const int *)b->key;

  return eid_a - eid_b;
}

static const char *
peid_name(const svn_element__content_t *element,
          apr_pool_t *scratch_pool)
{
  if (element->parent_eid == -1)
    return apr_psprintf(scratch_pool, "%3s %-10s", "", ".");

  return apr_psprintf(scratch_pool, "%3d/%-10s",
                      element->parent_eid, element->name);
}

static const char elements_by_eid_header[]
  = "    eid  parent-eid/name\n"
    "    ---  ----------/----";

/* List all elements in branch BRANCH, in element notation.
 */
static svn_error_t *
list_branch_elements_by_eid(svn_branch__state_t *branch,
                            apr_pool_t *scratch_pool)
{
  svn_element__tree_t *elements;
  svn_eid__hash_iter_t *ei;

  svnmover_notify_v("%s", elements_by_eid_header);
  SVN_ERR(svn_branch__state_get_elements(branch, &elements, scratch_pool));
  for (SVN_EID__HASH_ITER_SORTED_BY_EID(ei, elements->e_map, scratch_pool))
    {
      int eid = ei->eid;
      svn_element__content_t *element = ei->val;

      if (element)
        {
          svnmover_notify("    e%-3d %21s%s",
                          eid,
                          peid_name(element, scratch_pool),
                          subbranch_str(branch, eid, scratch_pool));
        }
    }

  return SVN_NO_ERROR;
}

/*  */
static const char *
branch_id_header_str(const char *prefix,
                     apr_pool_t *result_pool)
{
  if (the_ui_mode == UI_MODE_PATHS)
    {
      return apr_psprintf(result_pool,
                          "%sbranch-id  root-path\n"
                          "%s---------  ---------",
                          prefix, prefix);
    }
  else
    {
      return apr_psprintf(result_pool,
                          "%sbranch-id  branch-name  root-eid\n"
                          "%s---------  -----------  --------",
                          prefix, prefix);
    }
}

/* Show the id and path or root-eid of BRANCH.
 */
static const char *
branch_id_str(svn_branch__state_t *branch,
              apr_pool_t *result_pool)
{
  apr_pool_t *scratch_pool = result_pool;

  if (the_ui_mode == UI_MODE_PATHS)
    {
      return apr_psprintf(result_pool, "%-10s /%s",
                          svn_branch__get_id(branch, scratch_pool),
                          svn_branch__get_root_rrpath(branch, scratch_pool));
    }
  else
    {
      svn_element__content_t *outer_el = NULL;
      svn_branch__state_t *outer_branch;
      int outer_eid;

      svn_branch__get_outer_branch_and_eid(&outer_branch, &outer_eid,
                                           branch, scratch_pool);

      if (outer_branch)
        svn_error_clear(svn_branch__state_get_element(outer_branch, &outer_el,
                                                      outer_eid, scratch_pool));

      return apr_psprintf(result_pool, "%-10s %-12s root=e%d",
                          svn_branch__get_id(branch, scratch_pool),
                          outer_el ? outer_el->name : "/",
                          svn_branch__root_eid(branch));
    }
}

/* List the branch BRANCH.
 *
 * If WITH_ELEMENTS is true, also list the elements in it.
 */
static svn_error_t *
list_branch(svn_branch__state_t *branch,
            svn_boolean_t with_elements,
            apr_pool_t *scratch_pool)
{
  svnmover_notify("  %s", branch_id_str(branch, scratch_pool));

  if (with_elements)
    {
      if (the_ui_mode == UI_MODE_PATHS)
        {
          SVN_ERR(list_branch_elements(branch, scratch_pool));
        }
      else
        {
          SVN_ERR(list_branch_elements_by_eid(branch, scratch_pool));
        }
    }
  return SVN_NO_ERROR;
}

/* List all branches rooted at EID.
 *
 * If WITH_ELEMENTS is true, also list the elements in each branch.
 */
static svn_error_t *
list_branches(svn_branch__txn_t *txn,
              int eid,
              svn_boolean_t with_elements,
              apr_pool_t *scratch_pool)
{
  const apr_array_header_t *branches;
  int i;
  svn_boolean_t printed_header = FALSE;

  svnmover_notify_v("%s", branch_id_header_str("  ", scratch_pool));

  branches = svn_branch__txn_get_branches(txn, scratch_pool);

  for (i = 0; i < branches->nelts; i++)
    {
      svn_branch__state_t *branch = APR_ARRAY_IDX(branches, i, void *);

      if (svn_branch__root_eid(branch) != eid)
        continue;

      SVN_ERR(list_branch(branch, with_elements, scratch_pool));
      if (with_elements) /* separate branches by a blank line */
        svnmover_notify("%s", "");
    }

  for (i = 0; i < branches->nelts; i++)
    {
      svn_branch__state_t *branch = APR_ARRAY_IDX(branches, i, void *);
      svn_element__content_t *element;

      SVN_ERR(svn_branch__state_get_element(branch, &element,
                                            eid, scratch_pool));
      if (! element
          || svn_branch__root_eid(branch) == eid)
        continue;

      if (! printed_header)
        {
          if (the_ui_mode == UI_MODE_PATHS)
            svnmover_notify_v("branches containing but not rooted at that element:");
          else
            svnmover_notify_v("branches containing but not rooted at e%d:", eid);
          printed_header = TRUE;
        }
      SVN_ERR(list_branch(branch, with_elements, scratch_pool));
      if (with_elements) /* separate branches by a blank line */
        svnmover_notify("%s", "");
    }

  return SVN_NO_ERROR;
}

/* List all branches. If WITH_ELEMENTS is true, also list the elements
 * in each branch.
 */
static svn_error_t *
list_all_branches(svn_branch__txn_t *txn,
                  svn_boolean_t with_elements,
                  apr_pool_t *scratch_pool)
{
  const apr_array_header_t *branches;
  int i;

  branches = svn_branch__txn_get_branches(txn, scratch_pool);

  svnmover_notify_v("branches:");

  for (i = 0; i < branches->nelts; i++)
    {
      svn_branch__state_t *branch = APR_ARRAY_IDX(branches, i, void *);

      SVN_ERR(list_branch(branch, with_elements, scratch_pool));
      if (with_elements) /* separate branches by a blank line */
        svnmover_notify("%s", "");
    }

  return SVN_NO_ERROR;
}

/* Switch the WC to revision REVISION (SVN_INVALID_REVNUM means HEAD)
 * and branch TARGET_BRANCH.
 *
 * Merge any changes in the existing txn into the new txn.
 */
static svn_error_t *
do_switch(svnmover_wc_t *wc,
          svn_revnum_t revision,
          svn_branch__state_t *target_branch,
          apr_pool_t *scratch_pool)
{
  const char *target_branch_id
    = svn_branch__get_id(target_branch, scratch_pool);
  /* Keep hold of the previous WC txn */
  svn_branch__state_t *previous_base_br = wc->base->branch;
  svn_branch__state_t *previous_working_br = wc->working->branch;
  svn_boolean_t has_local_changes;

  SVN_ERR(txn_is_changed(previous_working_br->txn,
                         &has_local_changes, scratch_pool));

  /* Usually one would switch the WC to another branch (or just another
     revision) rooted at the same element. Switching to a branch rooted
     at a different element is well defined, but give a warning. */
  if (has_local_changes
      && svn_branch__root_eid(target_branch)
         != svn_branch__root_eid(previous_base_br))
    {
      svnmover_notify(_("Warning: you are switching from %s rooted at e%d "
                        "to %s rooted at e%d, a different root element, "
                        "while there are local changes. "),
             svn_branch__get_id(previous_base_br, scratch_pool),
             svn_branch__root_eid(previous_base_br),
             target_branch_id,
             svn_branch__root_eid(target_branch));
    }

  /* Complete the old edit drive into the 'WC' txn */
  SVN_ERR(svn_branch__txn_sequence_point(wc->edit_txn, scratch_pool));

  /* Check out a new WC, re-using the same data object */
  SVN_ERR(wc_checkout(wc, revision, target_branch_id, scratch_pool));

  if (has_local_changes)
    {
      svn_branch__el_rev_id_t *yca, *src, *tgt;

      /* Merge changes from the old into the new WC */
      yca = svn_branch__el_rev_id_create(previous_base_br,
                                         svn_branch__root_eid(previous_base_br),
                                         previous_base_br->txn->rev,
                                         scratch_pool);
      src = svn_branch__el_rev_id_create(previous_working_br,
                                         svn_branch__root_eid(previous_working_br),
                                         SVN_INVALID_REVNUM, scratch_pool);
      tgt = svn_branch__el_rev_id_create(wc->working->branch,
                                         svn_branch__root_eid(wc->working->branch),
                                         SVN_INVALID_REVNUM, scratch_pool);
      SVN_ERR(svnmover_branch_merge(wc->edit_txn, tgt->branch,
                                    &wc->conflicts,
                                    src, tgt, yca, wc->pool, scratch_pool));

      if (svnmover_any_conflicts(wc->conflicts))
        {
          SVN_ERR(svnmover_display_conflicts(wc->conflicts, scratch_pool));
        }

      /* ### TODO: If the merge raises conflicts, allow the user to revert
             to the pre-update state or resolve the conflicts. Currently
             this leaves the merge partially done and the pre-update state
             is lost. */
    }

  return SVN_NO_ERROR;
}

/*
 */
static svn_error_t *
do_merge(svnmover_wc_t *wc,
         svn_branch__el_rev_id_t *src,
         svn_branch__el_rev_id_t *tgt,
         svn_branch__el_rev_id_t *yca,
         apr_pool_t *scratch_pool)
{
  svn_branch__history_t *history;

  if (src->eid != tgt->eid || src->eid != yca->eid)
    {
      svnmover_notify(_("Warning: root elements differ in the requested merge "
                        "(from: e%d, to: e%d, yca: e%d)"),
                      src->eid, tgt->eid, yca->eid);
    }

  SVN_ERR(svnmover_branch_merge(wc->edit_txn, tgt->branch,
                                &wc->conflicts,
                                src, tgt, yca,
                                wc->pool, scratch_pool));

  /* Update the history */
  SVN_ERR(svn_branch__state_get_history(tgt->branch, &history, scratch_pool));
  /* ### Assume this was a complete merge -- i.e. all changes up to YCA were
     previously merged, so now SRC is a new parent. */
  SVN_ERR(svn_branch__history_add_parent(history, src->rev, src->branch->bid,
                                         scratch_pool));
  SVN_ERR(svn_branch__state_set_history(tgt->branch, history, scratch_pool));
  svnmover_notify_v(_("--- recorded merge parent as: %ld.%s"),
                    src->rev, src->branch->bid);

  if (svnmover_any_conflicts(wc->conflicts))
    {
      SVN_ERR(svnmover_display_conflicts(wc->conflicts, scratch_pool));
    }

  return SVN_NO_ERROR;
}

/*
 */
static svn_error_t *
do_auto_merge(svnmover_wc_t *wc,
              svn_branch__el_rev_id_t *src,
              svn_branch__el_rev_id_t *tgt,
              apr_pool_t *scratch_pool)
{
  svn_branch__rev_bid_t *yca;

  /* Find the Youngest Common Ancestor.
     ### TODO */
  yca = NULL;

  if (yca)
    {
      svn_branch__repos_t *repos = wc->working->branch->txn->repos;
      svn_branch__state_t *yca_branch;
      svn_branch__el_rev_id_t *_yca;

      SVN_ERR(svn_branch__repos_get_branch_by_id(&yca_branch, repos,
                                                 yca->rev, yca->bid,
                                                 scratch_pool));
      _yca = svn_branch__el_rev_id_create(yca_branch,
                                          svn_branch__root_eid(yca_branch),
                                          yca->rev, scratch_pool);

      SVN_ERR(do_merge(wc, src, tgt, _yca, scratch_pool));
    }
  else
    {
      return svn_error_create(SVN_BRANCH__ERR, NULL,
                              _("Cannot perform automatic merge: "
                                "no YCA found"));
    }

  return SVN_NO_ERROR;
}

/* Show the difference in history metadata between BRANCH1 and BRANCH2.
 *
 * If HEADER is non-null, print *HEADER and then set *HEADER to null.
 *
 * BRANCH1 and/or BRANCH2 may be null.
 */
static svn_error_t *
show_history_r(svn_branch__state_t *branch,
               const char *prefix,
               apr_pool_t *scratch_pool)
{
  svn_branch__history_t *history = NULL;
  svn_branch__subtree_t *subtree = NULL;
  apr_hash_index_t *hi;

  if (! branch)
    return SVN_NO_ERROR;

  SVN_ERR(svn_branch__state_get_history(branch, &history, scratch_pool));
  svnmover_notify("%s%s: %s", prefix,
                  branch->bid, history_str(history, scratch_pool));

  /* recurse into each subbranch */
  SVN_ERR(svn_branch__get_subtree(branch, &subtree,
                                  svn_branch__root_eid(branch),
                                  scratch_pool));
   for (hi = apr_hash_first(scratch_pool, subtree->subbranches);
       hi; hi = apr_hash_next(hi))
    {
      int e = svn_eid__hash_this_key(hi);
      svn_branch__state_t *subbranch = NULL;

      SVN_ERR(svn_branch__get_subbranch_at_eid(branch, &subbranch, e,
                                               scratch_pool));
      if (subbranch)
        {
          SVN_ERR(show_history_r(subbranch, prefix, scratch_pool));
        }
    }
  return SVN_NO_ERROR;
}

/*  */
typedef struct diff_item_t
{
  int eid;
  svn_element__content_t *e0, *e1;
  const char *relpath0, *relpath1;
  svn_boolean_t modified, reparented, renamed;
} diff_item_t;

/* Return differences between branch subtrees S_LEFT and S_RIGHT.
 * Diff the union of S_LEFT's and S_RIGHT's elements.
 *
 * Set *DIFF_CHANGES to a hash of (eid -> diff_item_t).
 *
 * ### This requires 'subtrees' only in order to produce the 'relpath'
 *     fields in the output. Other than that, it would work with arbitrary
 *     sets of elements.
 */
static svn_error_t *
subtree_diff(apr_hash_t **diff_changes,
             svn_branch__subtree_t *s_left,
             svn_branch__subtree_t *s_right,
             apr_pool_t *result_pool,
             apr_pool_t *scratch_pool)
{
  apr_hash_t *diff_left_right;
  apr_hash_index_t *hi;

  *diff_changes = apr_hash_make(result_pool);

  SVN_ERR(svnmover_element_differences(&diff_left_right,
                                       s_left->tree, s_right->tree,
                                       NULL /*union of s_left & s_right*/,
                                       result_pool, scratch_pool));

  for (hi = apr_hash_first(scratch_pool, diff_left_right);
       hi; hi = apr_hash_next(hi))
    {
      int eid = svn_eid__hash_this_key(hi);
      svn_element__content_t **e_pair = apr_hash_this_val(hi);
      svn_element__content_t *e0 = e_pair[0], *e1 = e_pair[1];

      if (e0 || e1)
        {
          diff_item_t *item = apr_palloc(result_pool, sizeof(*item));

          item->eid = eid;
          item->e0 = e0;
          item->e1 = e1;
          item->relpath0 = e0 ? svn_element__tree_get_path_by_eid(
                                  s_left->tree, eid, result_pool) : NULL;
          item->relpath1 = e1 ? svn_element__tree_get_path_by_eid(
                                  s_right->tree, eid, result_pool) : NULL;
          item->reparented = (e0 && e1 && e0->parent_eid != e1->parent_eid);
          item->renamed = (e0 && e1 && strcmp(e0->name, e1->name) != 0);

          svn_eid__hash_set(*diff_changes, eid, item);
        }
    }

  return SVN_NO_ERROR;
}

/* Find the relative order of diff items A and B, according to the
 * "major path" of each. The major path means its right-hand relpath, if
 * it exists on the right-hand side of the diff, else its left-hand relpath.
 *
 * Return negative/zero/positive when A sorts before/equal-to/after B.
 */
static int
diff_ordering_major_paths(const struct svn_sort__item_t *a,
                          const struct svn_sort__item_t *b)
{
  const diff_item_t *item_a = a->value, *item_b = b->value;
  int deleted_a = (item_a->e0 && ! item_a->e1);
  int deleted_b = (item_b->e0 && ! item_b->e1);
  const char *major_path_a = (item_a->e1 ? item_a->relpath1 : item_a->relpath0);
  const char *major_path_b = (item_b->e1 ? item_b->relpath1 : item_b->relpath0);

  /* Sort deleted items before all others */
  if (deleted_a != deleted_b)
    return deleted_b - deleted_a;

  /* Sort by path */
  return svn_path_compare_paths(major_path_a, major_path_b);
}

/* Display differences between subtrees LEFT and RIGHT, which are subtrees
 * of branches LEFT_BID and RIGHT_BID respectively.
 *
 * Diff the union of LEFT's and RIGHT's elements.
 *
 * Use EDITOR to fetch content when needed.
 *
 * Write a line containing HEADER before any other output, if it is not
 * null. Write PREFIX at the start of each line of output, including any
 * header line. PREFIX and HEADER should contain no end-of-line characters.
 *
 * The output refers to paths or to elements according to THE_UI_MODE.
 */
static svn_error_t *
show_subtree_diff(svn_branch__subtree_t *left,
                  const char *left_bid,
                  svn_branch__subtree_t *right,
                  const char *right_bid,
                  const char *prefix,
                  const char *header,
                  apr_pool_t *scratch_pool)
{
  apr_hash_t *diff_changes;
  svn_eid__hash_iter_t *ei;

  SVN_ERR_ASSERT(left && left->tree->root_eid != -1
                 && right && right->tree->root_eid != -1);

  SVN_ERR(subtree_diff(&diff_changes, left, right,
                       scratch_pool, scratch_pool));

  if (header && apr_hash_count(diff_changes))
    svnmover_notify("%s%s", prefix, header);

  for (SVN_EID__HASH_ITER_SORTED(ei, diff_changes,
                                 (the_ui_mode == UI_MODE_EIDS)
                                   ? sort_compare_items_by_eid
                                   : diff_ordering_major_paths,
                                 scratch_pool))
    {
      diff_item_t *item = ei->val;
      svn_element__content_t *e0 = item->e0, *e1 = item->e1;
      char status_mod = (e0 && e1) ? 'M' : e0 ? 'D' : 'A';

      /* For a deleted element whose parent was also deleted, mark it is
         less interesting, somehow. (Or we could omit it entirely.) */
      if (status_mod == 'D')
        {
          diff_item_t *parent_item
            = svn_eid__hash_get(diff_changes, e0->parent_eid);

          if (parent_item && ! parent_item->e1)
            status_mod = 'd';
        }

      if (the_ui_mode == UI_MODE_PATHS)
        {
          const char *major_path = (e1 ? item->relpath1 : item->relpath0);
          const char *from = "";

          if (item->reparented || item->renamed)
            {
              if (! item->reparented)
                from = apr_psprintf(scratch_pool,
                                    " (renamed from .../%s)",
                                    e0->name);
              else if (! item->renamed)
                from = apr_psprintf(scratch_pool,
                                    " (moved from %s/...)",
                                    svn_relpath_dirname(item->relpath0,
                                                        scratch_pool));
              else
                from = apr_psprintf(scratch_pool,
                                    " (moved+renamed from %s)",
                                    item->relpath0);
            }
          svnmover_notify("%s%c%c%c %s%s%s",
                          prefix,
                          status_mod,
                          item->reparented ? 'v' : ' ',
                          item->renamed ? 'r' : ' ',
                          major_path,
                          subtree_subbranch_str(e0 ? left : right,
                                                e0 ? left_bid : right_bid,
                                                item->eid, scratch_pool),
                          from);
        }
      else
        {
          svnmover_notify("%s%c%c%c e%-3d  %s%s%s%s%s",
                          prefix,
                          status_mod,
                          item->reparented ? 'v' : ' ',
                          item->renamed ? 'r' : ' ',
                          item->eid,
                          e1 ? peid_name(e1, scratch_pool) : "",
                          subtree_subbranch_str(e0 ? left : right,
                                                e0 ? left_bid : right_bid,
                                                item->eid, scratch_pool),
                          e0 && e1 ? " (from " : "",
                          e0 ? peid_name(e0, scratch_pool) : "",
                          e0 && e1 ? ")" : "");
        }
    }

  return SVN_NO_ERROR;
}

typedef svn_error_t *
svn_branch__diff_func_t(svn_branch__subtree_t *left,
                        const char *left_bid,
                        svn_branch__subtree_t *right,
                        const char *right_bid,
                        const char *prefix,
                        const char *header,
                        apr_pool_t *scratch_pool);

/* Display differences between subtrees LEFT and RIGHT.
 *
 * Recurse into sub-branches.
 */
static svn_error_t *
subtree_diff_r(svn_branch__state_t *left_branch,
               int left_root_eid,
               svn_branch__state_t *right_branch,
               int right_root_eid,
               svn_branch__diff_func_t diff_func,
               const char *prefix,
               apr_pool_t *scratch_pool)
{
  svn_branch__subtree_t *left = NULL;
  svn_branch__subtree_t *right = NULL;
  const char *left_str
    = left_branch
        ? apr_psprintf(scratch_pool, "%s:e%d at /%s",
                       left_branch->bid, left_root_eid,
                       svn_branch__get_root_rrpath(left_branch, scratch_pool))
        : NULL;
  const char *right_str
    = right_branch
        ? apr_psprintf(scratch_pool, "%s:e%d at /%s",
                       right_branch->bid, right_root_eid,
                       svn_branch__get_root_rrpath(right_branch, scratch_pool))
            : NULL;
  const char *header;
  apr_hash_t *subbranches_l, *subbranches_r, *subbranches_all;
  apr_hash_index_t *hi;

  if (left_branch)
    {
      SVN_ERR(svn_branch__get_subtree(left_branch, &left, left_root_eid,
                                      scratch_pool));
    }
  if (right_branch)
    {
      SVN_ERR(svn_branch__get_subtree(right_branch, &right, right_root_eid,
                                      scratch_pool));
    }

  if (!left)
    {
      header = apr_psprintf(scratch_pool,
                 "--- added branch %s",
                 right_str);
      svnmover_notify("%s%s", prefix, header);
    }
  else if (!right)
    {
      header = apr_psprintf(scratch_pool,
                 "--- deleted branch %s",
                 left_str);
      svnmover_notify("%s%s", prefix, header);
    }
  else
    {
      if (strcmp(left_str, right_str) == 0)
        {
          header = apr_psprintf(
                     scratch_pool, "--- diff branch %s",
                     left_str);
        }
      else
        {
          header = apr_psprintf(
                     scratch_pool, "--- diff branch %s : %s",
                     left_str, right_str);
        }
      SVN_ERR(diff_func(left, left_branch->bid, right, right_branch->bid,
                        prefix, header,
                        scratch_pool));
    }

  /* recurse into each subbranch that exists in LEFT and/or in RIGHT */
  subbranches_l = left ? left->subbranches : apr_hash_make(scratch_pool);
  subbranches_r = right ? right->subbranches : apr_hash_make(scratch_pool);
  subbranches_all = hash_overlay(subbranches_l, subbranches_r);

  for (hi = apr_hash_first(scratch_pool, subbranches_all);
       hi; hi = apr_hash_next(hi))
    {
      int e = svn_eid__hash_this_key(hi);
      svn_branch__state_t *left_subbranch = NULL, *right_subbranch = NULL;
      int left_subbranch_eid = -1, right_subbranch_eid = -1;

      /* recurse */
      if (left_branch)
        {
          SVN_ERR(svn_branch__get_subbranch_at_eid(left_branch, &left_subbranch, e,
                                                   scratch_pool));
          if (left_subbranch)
            {
              left_subbranch_eid = svn_branch__root_eid(left_subbranch);
            }
        }
      if (right_branch)
        {
          SVN_ERR(svn_branch__get_subbranch_at_eid(right_branch, &right_subbranch, e,
                                                   scratch_pool));
          if (right_subbranch)
            {
              right_subbranch_eid = svn_branch__root_eid(right_subbranch);
            }
        }
      SVN_ERR(subtree_diff_r(left_subbranch, left_subbranch_eid,
                             right_subbranch, right_subbranch_eid,
                             diff_func, prefix, scratch_pool));
    }
  return SVN_NO_ERROR;
}

/* Display differences between branch subtrees LEFT and RIGHT.
 *
 * Recurse into sub-branches.
 */
static svn_error_t *
branch_diff_r(svn_branch__el_rev_id_t *left,
              svn_branch__el_rev_id_t *right,
              svn_branch__diff_func_t diff_func,
              const char *prefix,
              apr_pool_t *scratch_pool)
{
  SVN_ERR(subtree_diff_r(left->branch, left->eid,
                         right->branch, right->eid,
                         diff_func, prefix, scratch_pool));
  return SVN_NO_ERROR;
}

/*  */
static svn_error_t *
do_copy(svn_branch__el_rev_id_t *from_el_rev,
        svn_branch__state_t *to_branch,
        svn_branch__eid_t to_parent_eid,
        const char *new_name,
        apr_pool_t *scratch_pool)
{
  const char *from_branch_id = svn_branch__get_id(from_el_rev->branch,
                                                  scratch_pool);
  svn_branch__rev_bid_eid_t *src_el_rev
    = svn_branch__rev_bid_eid_create(from_el_rev->rev, from_branch_id,
                                     from_el_rev->eid, scratch_pool);
  const char *from_path = el_rev_id_to_path(from_el_rev, scratch_pool);
  const char *to_path = branch_peid_name_to_path(to_branch, to_parent_eid,
                                                 new_name, scratch_pool);

  SVN_ERR(svn_branch__state_copy_tree(to_branch,
                                      src_el_rev, to_parent_eid, new_name,
                                      scratch_pool));
  svnmover_notify_v("A+   %s (from %s)",
                    to_path, from_path);

  return SVN_NO_ERROR;
}

/*  */
static svn_error_t *
do_delete(svn_branch__state_t *branch,
          svn_branch__eid_t eid,
          apr_pool_t *scratch_pool)
{
  const char *path = svn_branch__get_rrpath_by_eid(branch, eid, scratch_pool);

  SVN_ERR(svn_branch__state_delete_one(branch, eid, scratch_pool));
  svnmover_notify_v("D    %s", path);
  return SVN_NO_ERROR;
}

/*  */
static svn_error_t *
do_mkdir(svn_branch__txn_t *txn,
         svn_branch__state_t *to_branch,
         svn_branch__eid_t to_parent_eid,
         const char *new_name,
         apr_pool_t *scratch_pool)
{
  apr_hash_t *props = apr_hash_make(scratch_pool);
  svn_element__payload_t *payload
    = svn_element__payload_create_dir(props, scratch_pool);
  int new_eid;
  const char *path = branch_peid_name_to_path(to_branch, to_parent_eid,
                                              new_name, scratch_pool);

  SVN_ERR(svn_branch__txn_new_eid(txn, &new_eid, scratch_pool));
  SVN_ERR(svn_branch__state_alter_one(to_branch, new_eid,
                                      to_parent_eid, new_name, payload,
                                      scratch_pool));
  svnmover_notify_v("A    %s",
                    path);
  return SVN_NO_ERROR;
}

/*  */
static svn_error_t *
do_put_file(svn_branch__txn_t *txn,
            const char *local_file_path,
            svn_branch__el_rev_id_t *file_el_rev,
            svn_branch__el_rev_id_t *parent_el_rev,
            const char *file_name,
            apr_pool_t *scratch_pool)
{
  apr_hash_t *props;
  svn_stringbuf_t *text;
  int parent_eid;
  const char *name;
  svn_element__payload_t *payload;

  if (file_el_rev->eid != -1)
    {
      /* get existing props */
      svn_element__content_t *existing_element;

      SVN_ERR(svn_branch__state_get_element(file_el_rev->branch,
                                            &existing_element,
                                            file_el_rev->eid, scratch_pool));
      props = existing_element->payload->props;
    }
  else
    {
      props = apr_hash_make(scratch_pool);
    }
  /* read new text from file */
  {
    svn_stream_t *src;

    if (strcmp(local_file_path, "-") != 0)
      SVN_ERR(svn_stream_open_readonly(&src, local_file_path,
                                       scratch_pool, scratch_pool));
    else
      SVN_ERR(svn_stream_for_stdin2(&src, FALSE, scratch_pool));

    SVN_ERR(svn_stringbuf_from_stream(&text, src, 0, scratch_pool));
  }
  payload = svn_element__payload_create_file(props, text, scratch_pool);

  if (is_branch_root_element(file_el_rev->branch,
                             file_el_rev->eid))
    {
      parent_eid = -1;
      name = "";
    }
  else
    {
      parent_eid = parent_el_rev->eid;
      name = file_name;
    }

  if (file_el_rev->eid != -1)
    {
      const char *path = el_rev_id_to_path(file_el_rev, scratch_pool);

      SVN_ERR(svn_branch__state_alter_one(file_el_rev->branch, file_el_rev->eid,
                                          parent_eid, name, payload,
                                          scratch_pool));
      svnmover_notify_v("M    %s",
                        path);
    }
  else
    {
      int new_eid;
      const char *path
        = branch_peid_name_to_path(parent_el_rev->branch, parent_eid, name,
                                   scratch_pool);

      SVN_ERR(svn_branch__txn_new_eid(txn, &new_eid, scratch_pool));
      SVN_ERR(svn_branch__state_alter_one(parent_el_rev->branch, new_eid,
                                          parent_eid, name, payload,
                                          scratch_pool));
      file_el_rev->eid = new_eid;
      svnmover_notify_v("A    %s",
                        path);
    }
  return SVN_NO_ERROR;
}

/*  */
static svn_error_t *
do_cat(svn_branch__el_rev_id_t *file_el_rev,
       apr_pool_t *scratch_pool)
{
  apr_hash_t *props;
  svn_stringbuf_t *text;
  svn_element__content_t *existing_element;
  apr_hash_index_t *hi;

  /* get existing props */
  SVN_ERR(svn_branch__state_get_element(file_el_rev->branch, &existing_element,
                                        file_el_rev->eid, scratch_pool));

  props = existing_element->payload->props;
  text = existing_element->payload->text;

  for (hi = apr_hash_first(scratch_pool, props); hi; hi = apr_hash_next(hi))
    {
      const char *pname = apr_hash_this_key(hi);
      svn_string_t *pval = apr_hash_this_val(hi);

      svnmover_notify("property '%s': '%s'", pname, pval->data);
    }
  if (text)
    {
      svnmover_notify("%s", text->data);
    }
  return SVN_NO_ERROR;
}

/* Find the main parent of branch-state BRANCH. That means:
 *   - the only parent (in the case of straight history or branching), else
 *   - the parent with the same branch id (in the case of normal merging), else
 *   - none (in the case of a new unrelated branch, or a new branch formed
 *     by merging two or more other branches).
 */
static svn_error_t *
find_branch_main_parent(svn_branch__state_t *branch,
                        svn_branch__rev_bid_t **predecessor_p,
                        apr_pool_t *result_pool)
{
  svn_branch__history_t *history;
  svn_branch__rev_bid_t *our_own_history;
  svn_branch__rev_bid_t *predecessor = NULL;

  SVN_ERR(svn_branch__state_get_history(branch, &history, result_pool));
  if (apr_hash_count(history->parents) == 1)
    {
      apr_hash_index_t *hi = apr_hash_first(result_pool, history->parents);

      predecessor = apr_hash_this_val(hi);
    }
  else if ((our_own_history = svn_hash_gets(history->parents, branch->bid)))
    {
      predecessor = our_own_history;
    }

  if (predecessor_p)
    *predecessor_p = predecessor;
  return SVN_NO_ERROR;
}

/* Set *NEW_EL_REV_P to the location where OLD_EL_REV was in the previous
 * revision. Follow the "main line" of any branching in its history.
 *
 * If the same EID...
 */
static svn_error_t *
svn_branch__find_predecessor_el_rev(svn_branch__el_rev_id_t **new_el_rev_p,
                                    svn_branch__el_rev_id_t *old_el_rev,
                                    apr_pool_t *result_pool)
{
  const svn_branch__repos_t *repos = old_el_rev->branch->txn->repos;
  svn_branch__rev_bid_t *predecessor;
  svn_branch__state_t *branch;

  SVN_ERR(find_branch_main_parent(old_el_rev->branch,
                                  &predecessor, result_pool));
  if (! predecessor)
    {
      *new_el_rev_p = NULL;
      return SVN_NO_ERROR;
    }

  SVN_ERR(svn_branch__repos_get_branch_by_id(&branch,
                                             repos, predecessor->rev,
                                             predecessor->bid, result_pool));
  *new_el_rev_p = svn_branch__el_rev_id_create(branch, old_el_rev->eid,
                                               predecessor->rev, result_pool);

  return SVN_NO_ERROR;
}

/* Similar to 'svn log -v', this iterates over the revisions between
 * LEFT and RIGHT (currently excluding LEFT), printing a single-rev diff
 * for each.
 */
static svn_error_t *
do_log(svn_branch__el_rev_id_t *left,
       svn_branch__el_rev_id_t *right,
       apr_pool_t *scratch_pool)
{
  svn_revnum_t first_rev = left->rev;

  while (right->rev > first_rev)
    {
      svn_branch__el_rev_id_t *el_rev_left;

      SVN_ERR(svn_branch__find_predecessor_el_rev(&el_rev_left, right, scratch_pool));

      svnmover_notify(SVN_CL__LOG_SEP_STRING "r%ld | ...",
                      right->rev);
      svnmover_notify("History:");
      SVN_ERR(show_history_r(right->branch, "   ", scratch_pool));
      svnmover_notify("Changed elements:");
      SVN_ERR(branch_diff_r(el_rev_left, right,
                            show_subtree_diff, "   ",
                            scratch_pool));
      right = el_rev_left;
    }

  return SVN_NO_ERROR;
}

/* Make a subbranch at OUTER_BRANCH : OUTER_PARENT_EID : OUTER_NAME.
 *
 * The subbranch will consist of a single element given by PAYLOAD.
 */
static svn_error_t *
do_mkbranch(const char **new_branch_id_p,
            svn_branch__txn_t *txn,
            svn_branch__state_t *outer_branch,
            int outer_parent_eid,
            const char *outer_name,
            svn_element__payload_t *payload,
            apr_pool_t *scratch_pool)
{
  const char *outer_branch_id = svn_branch__get_id(outer_branch, scratch_pool);
  int new_outer_eid, new_inner_eid;
  const char *new_branch_id;
  svn_branch__state_t *new_branch;
  const char *path = branch_peid_name_to_path(outer_branch, outer_parent_eid,
                                              outer_name, scratch_pool);

  SVN_ERR(svn_branch__txn_new_eid(txn, &new_outer_eid, scratch_pool));
  SVN_ERR(svn_branch__state_alter_one(outer_branch, new_outer_eid,
                                      outer_parent_eid, outer_name,
                                      svn_element__payload_create_subbranch(
                                        scratch_pool), scratch_pool));

  SVN_ERR(svn_branch__txn_new_eid(txn, &new_inner_eid, scratch_pool));
  new_branch_id = svn_branch__id_nest(outer_branch_id, new_outer_eid,
                                      scratch_pool);
  SVN_ERR(svn_branch__txn_open_branch(txn, &new_branch,
                                      new_branch_id, new_inner_eid,
                                      NULL /*tree_ref*/,
                                      scratch_pool, scratch_pool));
  SVN_ERR(svn_branch__state_alter_one(new_branch, new_inner_eid,
                                      -1, "", payload, scratch_pool));

  svnmover_notify_v("A    %s (branch %s)",
                    path,
                    new_branch->bid);
  if (new_branch_id_p)
    *new_branch_id_p = new_branch->bid;
  return SVN_NO_ERROR;
}

/* Branch all or part of an existing branch, making a new branch.
 *
 * Branch the subtree of FROM_BRANCH found at FROM_EID, to create
 * a new branch at TO_OUTER_BRANCH:TO_OUTER_PARENT_EID:NEW_NAME.
 *
 * FROM_BRANCH:FROM_EID must be an existing element. It may be the
 * root of FROM_BRANCH. It must not be the root of a subbranch of
 * FROM_BRANCH.
 *
 * TO_OUTER_BRANCH:TO_OUTER_PARENT_EID must be an existing directory
 * and NEW_NAME must be nonexistent in that directory.
 */
static svn_error_t *
do_branch(svn_branch__state_t **new_branch_p,
          svn_branch__txn_t *txn,
          svn_branch__rev_bid_eid_t *from,
          svn_branch__state_t *to_outer_branch,
          svn_branch__eid_t to_outer_parent_eid,
          const char *new_name,
          apr_pool_t *result_pool,
          apr_pool_t *scratch_pool)
{
  const char *to_outer_branch_id
    = to_outer_branch ? svn_branch__get_id(to_outer_branch, scratch_pool) : NULL;
  int to_outer_eid;
  const char *new_branch_id;
  svn_branch__state_t *new_branch;
  svn_branch__history_t *history;
  const char *to_path
    = branch_peid_name_to_path(to_outer_branch,
                               to_outer_parent_eid, new_name, scratch_pool);

  /* assign new eid to root element (outer branch) */
  SVN_ERR(svn_branch__txn_new_eid(txn, &to_outer_eid, scratch_pool));

  new_branch_id = svn_branch__id_nest(to_outer_branch_id, to_outer_eid,
                                      scratch_pool);
  SVN_ERR(svn_branch__txn_open_branch(txn, &new_branch,
                                      new_branch_id, from->eid, from,
                                      result_pool, scratch_pool));
  history = svn_branch__history_create_empty(scratch_pool);
  SVN_ERR(svn_branch__history_add_parent(history, from->rev, from->bid,
                                         scratch_pool));
  SVN_ERR(svn_branch__state_set_history(new_branch, history, scratch_pool));
  SVN_ERR(svn_branch__state_alter_one(to_outer_branch, to_outer_eid,
                                      to_outer_parent_eid, new_name,
                                      svn_element__payload_create_subbranch(
                                        scratch_pool), scratch_pool));

  svnmover_notify_v("A+   %s (branch %s)",
                    to_path,
                    new_branch->bid);

  if (new_branch_p)
    *new_branch_p = new_branch;
  return SVN_NO_ERROR;
}

static svn_error_t *
do_topbranch(svn_branch__state_t **new_branch_p,
             svn_branch__txn_t *txn,
             svn_branch__rev_bid_eid_t *from,
             apr_pool_t *result_pool,
             apr_pool_t *scratch_pool)
{
  int outer_eid;
  const char *new_branch_id;
  svn_branch__state_t *new_branch;

  SVN_ERR(svn_branch__txn_new_eid(txn, &outer_eid, scratch_pool));
  new_branch_id = svn_branch__id_nest(NULL /*outer_branch*/, outer_eid,
                                      scratch_pool);
  SVN_ERR(svn_branch__txn_open_branch(txn, &new_branch,
                                      new_branch_id, from->eid, from,
                                      result_pool, scratch_pool));

  svnmover_notify_v("A+   (branch %s)",
                    new_branch->bid);

  if (new_branch_p)
    *new_branch_p = new_branch;
  return SVN_NO_ERROR;
}

/* Branch the subtree of FROM_BRANCH found at FROM_EID, to appear
 * in the existing branch TO_BRANCH at TO_PARENT_EID:NEW_NAME.
 *
 * This is like merging the creation of the source subtree into TO_BRANCH.
 *
 * Any elements of the source subtree that already exist in TO_BRANCH
 * are altered. This is like resolving any merge conflicts as 'theirs'.
 *
 * (### Sometimes the user might prefer that we throw an error if any
 * element of the source subtree already exists in TO_BRANCH.)
 */
static svn_error_t *
do_branch_into(svn_branch__state_t *from_branch,
               int from_eid,
               svn_branch__state_t *to_branch,
               svn_branch__eid_t to_parent_eid,
               const char *new_name,
               apr_pool_t *scratch_pool)
{
  svn_branch__subtree_t *from_subtree;
  svn_element__content_t *new_root_content;
  const char *to_path = branch_peid_name_to_path(to_branch, to_parent_eid,
                                                 new_name, scratch_pool);

  /* Source element must exist */
  if (! svn_branch__get_path_by_eid(from_branch, from_eid, scratch_pool))
    {
      return svn_error_createf(SVN_BRANCH__ERR, NULL,
                               _("Cannot branch from %s e%d: "
                                 "does not exist"),
                               svn_branch__get_id(
                                 from_branch, scratch_pool), from_eid);
    }

  SVN_ERR(svn_branch__get_subtree(from_branch, &from_subtree, from_eid,
                                  scratch_pool));

  /* Change this subtree's root element to TO_PARENT_EID/NEW_NAME. */
  new_root_content
    = svn_element__tree_get(from_subtree->tree, from_subtree->tree->root_eid);
  new_root_content
    = svn_element__content_create(to_parent_eid, new_name,
                                  new_root_content->payload, scratch_pool);
  svn_element__tree_set(from_subtree->tree, from_subtree->tree->root_eid,
                        new_root_content);

  /* Populate the new branch mapping */
  SVN_ERR(svn_branch__instantiate_elements_r(to_branch, *from_subtree,
                                             scratch_pool));
  svnmover_notify_v("A+   %s (subtree)",
                    to_path);

  return SVN_NO_ERROR;
}

/* Copy-and-delete.
 *
 *      copy the subtree at EL_REV to TO_BRANCH:TO_PARENT_EID:TO_NAME
 *      delete the subtree at EL_REV
 */
static svn_error_t *
do_copy_and_delete(svn_branch__el_rev_id_t *el_rev,
                   svn_branch__state_t *to_branch,
                   int to_parent_eid,
                   const char *to_name,
                   apr_pool_t *scratch_pool)
{
  const char *from_path
    = svn_branch__get_rrpath_by_eid(el_rev->branch, el_rev->eid, scratch_pool);

  SVN_ERR_ASSERT(! is_branch_root_element(el_rev->branch, el_rev->eid));

  SVN_ERR(do_copy(el_rev, to_branch, to_parent_eid, to_name,
                  scratch_pool));

  SVN_ERR(svn_branch__state_delete_one(el_rev->branch, el_rev->eid,
                                       scratch_pool));
  svnmover_notify_v("D    %s", from_path);

  return SVN_NO_ERROR;
}

/* Branch-and-delete.
 *
 *      branch the subtree at EL_REV creating a new nested branch at
 *        TO_BRANCH:TO_PARENT_EID:TO_NAME,
 *        or creating a new top-level branch if TO_BRANCH is null;
 *      delete the subtree at EL_REV
 */
static svn_error_t *
do_branch_and_delete(svn_branch__txn_t *edit_txn,
                     svn_branch__el_rev_id_t *el_rev,
                     svn_branch__state_t *to_outer_branch,
                     int to_outer_parent_eid,
                     const char *to_name,
                     apr_pool_t *scratch_pool)
{
  const char *from_branch_id = svn_branch__get_id(el_rev->branch,
                                                  scratch_pool);
  svn_branch__rev_bid_eid_t *from
    = svn_branch__rev_bid_eid_create(el_rev->rev, from_branch_id,
                                     el_rev->eid, scratch_pool);
  svn_branch__state_t *new_branch;
  const char *from_path
    = svn_branch__get_rrpath_by_eid(el_rev->branch, el_rev->eid, scratch_pool);

  SVN_ERR_ASSERT(! is_branch_root_element(el_rev->branch, el_rev->eid));

  SVN_ERR(do_branch(&new_branch, edit_txn, from,
                    to_outer_branch, to_outer_parent_eid, to_name,
                    scratch_pool, scratch_pool));

  SVN_ERR(svn_branch__state_delete_one(el_rev->branch, el_rev->eid,
                                       scratch_pool));
  svnmover_notify_v("D    %s", from_path);

  return SVN_NO_ERROR;
}

/* Branch-into-and-delete.
 *
 * (Previously, confusingly, called 'branch-and-delete'.)
 *
 * The target branch is different from the source branch.
 *
 *      delete elements from source branch
 *      instantiate (or update) same elements in target branch
 *
 * For each element being moved, if the element already exists in TO_BRANCH,
 * the effect is as if the existing element in TO_BRANCH was first deleted.
 */
static svn_error_t *
do_branch_into_and_delete(svn_branch__el_rev_id_t *el_rev,
                          svn_branch__state_t *to_branch,
                          int to_parent_eid,
                          const char *to_name,
                          apr_pool_t *scratch_pool)
{
  const char *from_path
    = svn_branch__get_rrpath_by_eid(el_rev->branch, el_rev->eid, scratch_pool);

  SVN_ERR_ASSERT(! is_branch_root_element(el_rev->branch, el_rev->eid));

  /* This is supposed to be used for moving to a *different* branch.
     In fact, this method would also work for moving within one
     branch, but we don't currently want to use it for that purpose. */
  SVN_ERR_ASSERT(! BRANCH_IS_SAME_BRANCH(el_rev->branch, to_branch,
                                         scratch_pool));

  /* Merge the "creation of the source" to the target (aka branch-into) */
  SVN_ERR(do_branch_into(el_rev->branch, el_rev->eid,
                         to_branch, to_parent_eid, to_name,
                         scratch_pool));

  SVN_ERR(svn_branch__state_delete_one(el_rev->branch, el_rev->eid,
                                       scratch_pool));
  svnmover_notify_v("D    %s", from_path);

  return SVN_NO_ERROR;
}

/* Interactive options for moving to another branch.
 */
static svn_error_t *
do_interactive_cross_branch_move(svn_branch__txn_t *txn,
                                 svn_branch__el_rev_id_t *el_rev,
                                 svn_branch__el_rev_id_t *to_parent_el_rev,
                                 const char *to_name,
                                 apr_pool_t *scratch_pool)
{
  svn_error_t *err;
  const char *input;

  if (0 /*### if non-interactive*/)
    {
      return svn_error_createf(SVN_BRANCH__ERR, NULL,
        _("mv: The source and target are in different branches. "
          "Some ways to move content to a different branch are, "
          "depending on the effect you want to achieve: "
          "copy-and-delete, branch-and-delete, branch-into-and-delete"));
    }

  svnmover_notify_v(
    _("mv: The source and target are in different branches. "
      "Some ways to move content to a different branch are, "
      "depending on the effect you want to achieve:\n"
      "  c: copy-and-delete: cp SOURCE TARGET; rm SOURCE\n"
      "  b: branch-and-delete: branch SOURCE TARGET; rm SOURCE\n"
      "  i: branch-into-and-delete: branch-into SOURCE TARGET; rm SOURCE\n"
      "We can do one of these for you now if you wish.\n"
    ));

  settext_stderr(TEXT_FG_YELLOW);
  err = svn_cmdline_prompt_user2(
          &input,
          "Your choice (c, b, i, or just <enter> to do nothing): ",
          NULL, scratch_pool);
  settext(TEXT_RESET);
  if (err && (err->apr_err == SVN_ERR_CANCELLED || err->apr_err == APR_EOF))
    {
      svn_error_clear(err);
      return SVN_NO_ERROR;
    }
  SVN_ERR(err);

  if (input[0] == 'c' || input[0] == 'C')
    {
      svnmover_notify_v("Performing 'copy-and-delete SOURCE TARGET'");

      SVN_ERR(do_copy_and_delete(el_rev,
                                 to_parent_el_rev->branch,
                                 to_parent_el_rev->eid, to_name,
                                 scratch_pool));
    }
  else if (input[0] == 'b' || input[0] == 'B')
    {
      svnmover_notify_v("Performing 'branch-and-delete SOURCE TARGET'");

      SVN_ERR(do_branch_and_delete(txn, el_rev,
                                   to_parent_el_rev->branch,
                                   to_parent_el_rev->eid, to_name,
                                   scratch_pool));
    }
  else if (input[0] == 'i' || input[0] == 'I')
    {
      svnmover_notify_v("Performing 'branch-into-and-delete SOURCE TARGET'");
      svnmover_notify_v(
        "In the current implementation of this experimental UI, each element "
        "instance from the source branch subtree will overwrite any instance "
        "of the same element that already exists in the target branch."
        );
      /* We could instead either throw an error or fall back to copy-and-delete
         if any moved element already exists in target branch. */

      SVN_ERR(do_branch_into_and_delete(el_rev,
                                        to_parent_el_rev->branch,
                                        to_parent_el_rev->eid, to_name,
                                        scratch_pool));
    }

  return SVN_NO_ERROR;
}

/* Move.
 */
static svn_error_t *
do_move(svn_branch__el_rev_id_t *el_rev,
        svn_branch__el_rev_id_t *to_parent_el_rev,
        const char *to_name,
        apr_pool_t *scratch_pool)
{
  const char *from_path = el_rev_id_to_path(el_rev, scratch_pool);
  const char *to_path
    = branch_peid_name_to_path(to_parent_el_rev->branch,
                               to_parent_el_rev->eid, to_name, scratch_pool);
  /* New payload shall be the same as before */
  svn_element__content_t *existing_element;

  SVN_ERR(svn_branch__state_get_element(el_rev->branch, &existing_element,
                                        el_rev->eid, scratch_pool));
  SVN_ERR(svn_branch__state_alter_one(el_rev->branch, el_rev->eid,
                            to_parent_el_rev->eid, to_name,
                            existing_element->payload, scratch_pool));
  svnmover_notify_v("V    %s (from %s)",
                    to_path, from_path);
  return SVN_NO_ERROR;
}

/* This commit callback prints not only a commit summary line but also
 * a log-style summary of the changes.
 */
static svn_error_t *
commit_callback(const svn_commit_info_t *commit_info,
                void *baton,
                apr_pool_t *pool)
{
  commit_callback_baton_t *b = baton;

  svnmover_notify("Committed r%ld:", commit_info->revision);

  b->revision = commit_info->revision;
  return SVN_NO_ERROR;
}

/* Display a diff of the commit */
static svn_error_t *
display_diff_of_commit(const commit_callback_baton_t *ccbb,
                       apr_pool_t *scratch_pool)
{
  svn_branch__txn_t *previous_head_txn
    = svn_branch__repos_get_base_revision_root(ccbb->edit_txn);
  svn_branch__state_t *base_branch
    = svn_branch__txn_get_branch_by_id(previous_head_txn,
                                       ccbb->wc_base_branch_id,
                                       scratch_pool);
  svn_branch__state_t *committed_branch
    = svn_branch__txn_get_branch_by_id(ccbb->edit_txn,
                                       ccbb->wc_commit_branch_id,
                                       scratch_pool);
  svn_branch__el_rev_id_t *el_rev_left
    = svn_branch__el_rev_id_create(base_branch, svn_branch__root_eid(base_branch),
                                   base_branch->txn->rev,
                                   scratch_pool);
  svn_branch__el_rev_id_t *el_rev_right
    = svn_branch__el_rev_id_create(committed_branch,
                                   svn_branch__root_eid(committed_branch),
                                   committed_branch->txn->rev,
                                   scratch_pool);

  SVN_ERR(branch_diff_r(el_rev_left, el_rev_right,
                        show_subtree_diff, "   ",
                        scratch_pool));
  return SVN_NO_ERROR;
}

static svn_error_t *
commit(svn_revnum_t *new_rev_p,
       svnmover_wc_t *wc,
       apr_hash_t *revprops,
       apr_pool_t *scratch_pool)
{
  if (svnmover_any_conflicts(wc->conflicts))
    {
      return svn_error_createf(SVN_BRANCH__ERR, NULL,
                               _("Cannot commit because there are "
                                 "unresolved conflicts"));
    }

  /* Complete the old edit drive (editing the WC working state) */
  SVN_ERR(svn_branch__txn_sequence_point(wc->edit_txn, scratch_pool));

  /* Just as in execute() the pool must be a subpool of wc->pool. */
  SVN_ERR(wc_commit(new_rev_p, wc, revprops, wc->pool));

  return SVN_NO_ERROR;
}

/* Commit.
 *
 * Set *NEW_REV_P to the committed revision number. Update the WC base of
 * each committed element to that revision.
 *
 * If there are no changes to commit, set *NEW_REV_P to SVN_INVALID_REVNUM
 * and do not make a commit.
 *
 * NEW_REV_P may be null if not wanted.
 */
static svn_error_t *
do_commit(svn_revnum_t *new_rev_p,
          svnmover_wc_t *wc,
          apr_hash_t *revprops,
          apr_pool_t *scratch_pool)
{
  svn_revnum_t new_rev;

  SVN_ERR(commit(&new_rev, wc, revprops, scratch_pool));

  if (new_rev_p)
    *new_rev_p = new_rev;
  return SVN_NO_ERROR;
}

/* Revert all uncommitted changes in WC.
 */
static svn_error_t *
do_revert(svnmover_wc_t *wc,
          apr_pool_t *scratch_pool)
{
  /* Replay the inverse of the current edit txn, into the current edit txn */
  SVN_ERR(replay(wc->edit_txn, wc->working->branch,
                 wc->working->branch,
                 wc->base->branch,
                 scratch_pool));
  wc->conflicts = NULL;

  return SVN_NO_ERROR;
}

/* Migration replay baton */
typedef struct migrate_replay_baton_t {
  svn_branch__txn_t *edit_txn;
  svn_ra_session_t *from_session;
  /* Hash (by revnum) of array of svn_repos_move_info_t. */
  apr_hash_t *moves;
} migrate_replay_baton_t;

/* Callback function for svn_ra_replay_range, invoked when starting to parse
 * a replay report.
 */
static svn_error_t *
migrate_replay_rev_started(svn_revnum_t revision,
                           void *replay_baton,
                           const svn_delta_editor_t **editor,
                           void **edit_baton,
                           apr_hash_t *rev_props,
                           apr_pool_t *pool)
{
  migrate_replay_baton_t *rb = replay_baton;
  const svn_delta_editor_t *old_editor;
  void *old_edit_baton;

  svnmover_notify("migrate: start r%ld", revision);

  SVN_ERR(svn_branch__compat_get_migration_editor(&old_editor, &old_edit_baton,
                                                  rb->edit_txn,
                                                  rb->from_session, revision,
                                                  pool));
  SVN_ERR(svn_delta__get_debug_editor(&old_editor, &old_edit_baton,
                                      old_editor, old_edit_baton,
                                      "migrate: ", pool));

  *editor = old_editor;
  *edit_baton = old_edit_baton;

  return SVN_NO_ERROR;
}

/* Callback function for svn_ra_replay_range, invoked when finishing parsing
 * a replay report.
 */
static svn_error_t *
migrate_replay_rev_finished(svn_revnum_t revision,
                            void *replay_baton,
                            const svn_delta_editor_t *editor,
                            void *edit_baton,
                            apr_hash_t *rev_props,
                            apr_pool_t *pool)
{
  migrate_replay_baton_t *rb = replay_baton;
  apr_array_header_t *moves_in_revision
    = apr_hash_get(rb->moves, &revision, sizeof(revision));

  SVN_ERR(editor->close_edit(edit_baton, pool));

  svnmover_notify("migrate: moves in revision r%ld:", revision);

  if (moves_in_revision)
    {
      int i;

      for (i = 0; i < moves_in_revision->nelts; i++)
        {
          svn_repos_move_info_t *this_move
            = APR_ARRAY_IDX(moves_in_revision, i, void *);

          if (this_move)
            {
              svnmover_notify("%s",
                     svn_client__format_move_chain_for_display(this_move,
                                                               "", pool));
            }
        }
    }

  return SVN_NO_ERROR;
}

/* Migrate changes from non-move-tracking revisions.
 */
static svn_error_t *
do_migrate(svnmover_wc_t *wc,
           svn_revnum_t start_revision,
           svn_revnum_t end_revision,
           apr_pool_t *scratch_pool)
{
  migrate_replay_baton_t *rb = apr_pcalloc(scratch_pool, sizeof(*rb));

  if (start_revision < 1 || end_revision < 1
      || start_revision > end_revision
      || end_revision > wc->head_revision)
    {
      return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                               _("migrate: Bad revision range (%ld to %ld); "
                                 "minimum is 1 and maximum (head) is %ld"),
                               start_revision, end_revision,
                               wc->head_revision);
    }

  /* Scan the repository log for move info */
  SVN_ERR(svn_client__get_repos_moves(&rb->moves,
                                      "" /*(unused)*/,
                                      wc->ra_session,
                                      start_revision, end_revision,
                                      wc->ctx, scratch_pool, scratch_pool));

  rb->edit_txn = wc->edit_txn;
  rb->from_session = wc->ra_session;
  SVN_ERR(svn_ra_replay_range(rb->from_session,
                              start_revision, end_revision,
                              0, TRUE,
                              migrate_replay_rev_started,
                              migrate_replay_rev_finished,
                              rb, scratch_pool));
  return SVN_NO_ERROR;
}

static svn_error_t *
show_branch_history(svn_branch__state_t *branch,
                    apr_pool_t *scratch_pool)
{
  svn_branch__history_t *history;
  svn_branch__rev_bid_t *main_parent;
  apr_hash_index_t *hi;

  SVN_ERR(svn_branch__state_get_history(branch, &history, scratch_pool));

  SVN_ERR(find_branch_main_parent(branch, &main_parent, scratch_pool));
  if (main_parent)
    {
      if (strcmp(main_parent->bid, branch->bid) == 0)
        {
          svnmover_notify("  main parent: r%ld.%s",
                          main_parent->rev, main_parent->bid);
        }
      else
        {
          svnmover_notify("  main parent (branched from): r%ld.%s",
                          main_parent->rev, main_parent->bid);
        }
    }
  for (hi = apr_hash_first(scratch_pool, history->parents);
       hi; hi = apr_hash_next(hi))
    {
      svn_branch__rev_bid_t *parent = apr_hash_this_val(hi);

      if (! svn_branch__rev_bid_equal(parent, main_parent))
        {
          svnmover_notify("  other parent (complete merge): r%ld.%s",
                          parent->rev, parent->bid);
        }
    }

  return SVN_NO_ERROR;
}

/* Show info about element E.
 *
 * TODO: Show different info for a repo element versus a WC element.
 */
static svn_error_t *
do_info(svnmover_wc_t *wc,
        svn_branch__el_rev_id_t *e,
        apr_pool_t *scratch_pool)
{
  svnmover_notify("Element Id: %d%s",
                  e->eid,
                  is_branch_root_element(e->branch, e->eid)
                    ? " (branch root)" : "");

  /* Show WC info for a WC working element, or repo info for a repo element */
  if (e->rev == SVN_INVALID_REVNUM)
    {
      svn_branch__state_t *base_branch, *work_branch;
      svn_revnum_t base_rev;
      svn_element__content_t *e_base, *e_work;
      svn_boolean_t is_modified;

      base_branch = svn_branch__txn_get_branch_by_id(
                      wc->base->branch->txn, e->branch->bid, scratch_pool);
      work_branch = svn_branch__txn_get_branch_by_id(
                      wc->working->branch->txn, e->branch->bid, scratch_pool);
      base_rev = svnmover_wc_get_base_rev(wc, base_branch, e->eid, scratch_pool);
      SVN_ERR(svn_branch__state_get_element(base_branch, &e_base,
                                            e->eid, scratch_pool));
      SVN_ERR(svn_branch__state_get_element(work_branch, &e_work,
                                            e->eid, scratch_pool));
      is_modified = !svn_element__content_equal(e_base, e_work,
                                                scratch_pool);

      svnmover_notify("Base Revision: %ld", base_rev);
      svnmover_notify("Base Branch:    %s", base_branch->bid);
      svnmover_notify("Working Branch: %s", work_branch->bid);
      svnmover_notify("Modified:       %s", is_modified ? "yes" : "no");
    }
  else
    {
      svnmover_notify("Revision: %ld", e->rev);
      svnmover_notify("Branch:    %s", e->branch->bid);
    }

  return SVN_NO_ERROR;
}


typedef struct arg_t
{
  const char *path_name;
  svn_branch__el_rev_id_t *el_rev, *parent_el_rev;
} arg_t;

#define VERIFY_REV_SPECIFIED(op, i)                                     \
  if (arg[i]->el_rev->rev == SVN_INVALID_REVNUM)                        \
    return svn_error_createf(SVN_BRANCH__ERR, NULL,                   \
                             _("%s: '%s': revision number required"),   \
                             op, action->relpath[i]);

#define VERIFY_REV_UNSPECIFIED(op, i)                                   \
  if (arg[i]->el_rev->rev != SVN_INVALID_REVNUM)                        \
    return svn_error_createf(SVN_BRANCH__ERR, NULL,                   \
                             _("%s: '%s@...': revision number not allowed"), \
                             op, action->relpath[i]);

#define VERIFY_EID_NONEXISTENT(op, i)                                   \
  if (arg[i]->el_rev->eid != -1)                                        \
    return svn_error_createf(SVN_BRANCH__ERR, NULL,                   \
                             _("%s: Element already exists at path '%s'"), \
                             op, action->relpath[i]);

#define VERIFY_EID_EXISTS(op, i)                                        \
  if (arg[i]->el_rev->eid == -1)                                        \
    return svn_error_createf(SVN_BRANCH__ERR, NULL,                   \
                             _("%s: Element not found at path '%s%s'"), \
                             op, action->relpath[i],                    \
                             action->rev_spec[i].kind == svn_opt_revision_unspecified \
                               ? "" : "@...");

#define VERIFY_PARENT_EID_EXISTS(op, i)                                 \
  if (arg[i]->parent_el_rev->eid == -1)                                 \
    return svn_error_createf(SVN_BRANCH__ERR, NULL,                   \
                             _("%s: Element not found at path '%s'"),   \
                             op, svn_relpath_dirname(action->relpath[i], pool));

#define VERIFY_NOT_CHILD_OF_SELF(op, i, j, pool)                        \
  if (svn_relpath_skip_ancestor(                                        \
        svn_branch__get_rrpath_by_eid(arg[i]->el_rev->branch,           \
                                      arg[i]->el_rev->eid, pool),       \
        svn_branch__get_rrpath_by_eid(arg[j]->parent_el_rev->branch,    \
                                      arg[j]->parent_el_rev->eid, pool))) \
    return svn_error_createf(SVN_BRANCH__ERR, NULL,                   \
                             _("%s: The specified target is nested "    \
                               "inside the source"), op);

/* If EL_REV specifies the root element of a nested branch, change EL_REV
 * to specify the corresponding subbranch-root element of its outer branch.
 *
 * If EL_REV specifies the root element of a top-level branch, return an
 * error.
 */
static svn_error_t *
point_to_outer_element_instead(svn_branch__el_rev_id_t *el_rev,
                               const char *op,
                               apr_pool_t *scratch_pool)
{
  if (is_branch_root_element(el_rev->branch, el_rev->eid))
    {
      svn_branch__state_t *outer_branch;
      int outer_eid;

      svn_branch__get_outer_branch_and_eid(&outer_branch, &outer_eid,
                                           el_rev->branch, scratch_pool);

      if (! outer_branch)
        return svn_error_createf(SVN_BRANCH__ERR, NULL, "%s: %s", op,
                                 _("svnmover cannot delete or move a "
                                   "top-level branch"));

      el_rev->eid = outer_eid;
      el_rev->branch = outer_branch;
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
execute(svnmover_wc_t *wc,
        const apr_array_header_t *actions,
        const char *anchor_url,
        apr_hash_t *revprops,
        svn_client_ctx_t *ctx,
        apr_pool_t *pool)
{
  const char *base_relpath;
  apr_pool_t *iterpool = svn_pool_create(pool);
  int i;

  base_relpath = svn_uri_skip_ancestor(wc->repos_root_url, anchor_url, pool);

  for (i = 0; i < actions->nelts; ++i)
    {
      action_t *action = APR_ARRAY_IDX(actions, i, action_t *);
      int j;
      arg_t *arg[3] = { NULL, NULL, NULL };

      svn_pool_clear(iterpool);

      /* Before translating paths to/from elements, need a sequence point */
      SVN_ERR(svn_branch__txn_sequence_point(wc->edit_txn, iterpool));

      /* Convert each ACTION[j].{relpath, rev_spec} to
         (EL_REV[j], PARENT_EL_REV[j], PATH_NAME[j], REVNUM[j]),
         except for the local-path argument of a 'put' command. */
      for (j = 0; j < 3; j++)
        {
          if (action->relpath[j]
              && ! (action->action == ACTION_PUT_FILE && j == 0))
            {
              const char *rrpath, *parent_rrpath;

              arg[j] = apr_palloc(iterpool, sizeof(*arg[j]));

              rrpath = svn_relpath_join(base_relpath, action->relpath[j], iterpool);
              parent_rrpath = svn_relpath_dirname(rrpath, iterpool);

              arg[j]->path_name = svn_relpath_basename(rrpath, NULL);
              SVN_ERR(find_el_rev_by_rrpath_rev(&arg[j]->el_rev, wc,
                                                &action->rev_spec[j],
                                                action->branch_id[j],
                                                rrpath,
                                                iterpool, iterpool));
              SVN_ERR(find_el_rev_by_rrpath_rev(&arg[j]->parent_el_rev, wc,
                                                &action->rev_spec[j],
                                                action->branch_id[j],
                                                parent_rrpath,
                                                iterpool, iterpool));
            }
        }

      switch (action->action)
        {
        case ACTION_INFO_WC:
          {
            svn_boolean_t is_modified;
            svn_revnum_t base_rev_min, base_rev_max;

            SVN_ERR(txn_is_changed(wc->working->branch->txn, &is_modified,
                                   iterpool));
            SVN_ERR(svnmover_wc_get_base_revs(wc, &base_rev_min, &base_rev_max,
                                              iterpool));

            svnmover_notify("Repository Root: %s", wc->repos_root_url);
            if (base_rev_min == base_rev_max)
              svnmover_notify("Base Revision: %ld", base_rev_min);
            else
              svnmover_notify("Base Revisions: %ld to %ld",
                              base_rev_min, base_rev_max);
            svnmover_notify("Base Branch:    %s", wc->base->branch->bid);
            svnmover_notify("Working Branch: %s", wc->working->branch->bid);
            SVN_ERR(show_branch_history(wc->working->branch, iterpool));
            svnmover_notify("Modified:       %s", is_modified ? "yes" : "no");
          }
          break;

        case ACTION_INFO:
          VERIFY_EID_EXISTS("info", 0);
          {
            /* If it's a nested branch root, show info for the outer element
               first, and then for the inner element. */
            if (is_branch_root_element(arg[0]->el_rev->branch,
                                       arg[0]->el_rev->eid))
              {
                svn_branch__state_t *outer_branch;
                int outer_eid;

                svn_branch__get_outer_branch_and_eid(&outer_branch, &outer_eid,
                                                     arg[0]->el_rev->branch,
                                                     iterpool);
                if (outer_branch)
                  {
                    svn_branch__el_rev_id_t *outer_e
                      = svn_branch__el_rev_id_create(outer_branch, outer_eid,
                                                     arg[0]->el_rev->rev,
                                                     iterpool);
                    SVN_ERR(do_info(wc, outer_e, iterpool));
                  }
              }
            SVN_ERR(do_info(wc, arg[0]->el_rev, iterpool));
          }
          break;

        case ACTION_LIST_CONFLICTS:
          {
            if (svnmover_any_conflicts(wc->conflicts))
              {
                SVN_ERR(svnmover_display_conflicts(wc->conflicts, iterpool));
              }
          }
          break;

        case ACTION_RESOLVED_CONFLICT:
          {
            if (svnmover_any_conflicts(wc->conflicts))
              {
                SVN_ERR(svnmover_conflict_resolved(wc->conflicts,
                                                   action->relpath[0],
                                                   iterpool));
              }
            else
              {
                return svn_error_create(SVN_BRANCH__ERR, NULL,
                                        _("No conflicts are currently flagged"));
              }
          }
          break;

        case ACTION_DIFF:
          VERIFY_EID_EXISTS("diff", 0);
          VERIFY_EID_EXISTS("diff", 1);
          {
            SVN_ERR(branch_diff_r(arg[0]->el_rev /*from*/,
                                  arg[1]->el_rev /*to*/,
                                  show_subtree_diff, "",
                                  iterpool));
          }
          break;

        case ACTION_STATUS:
          {
            svn_branch__el_rev_id_t *from, *to;

            from = svn_branch__el_rev_id_create(wc->base->branch,
                                                svn_branch__root_eid(wc->base->branch),
                                                SVN_INVALID_REVNUM, iterpool);
            to = svn_branch__el_rev_id_create(wc->working->branch,
                                              svn_branch__root_eid(wc->working->branch),
                                              SVN_INVALID_REVNUM, iterpool);
            SVN_ERR(branch_diff_r(from, to,
                                  show_subtree_diff, "",
                                  iterpool));
          }
          break;

        case ACTION_LOG:
          VERIFY_EID_EXISTS("log", 0);
          VERIFY_EID_EXISTS("log", 1);
          {
            SVN_ERR(do_log(arg[0]->el_rev /*from*/,
                           arg[1]->el_rev /*to*/,
                           iterpool));
            }
          break;

        case ACTION_LIST_BRANCHES:
          {
            VERIFY_EID_EXISTS("branches", 0);
            if (the_ui_mode == UI_MODE_PATHS)
              {
                svnmover_notify_v("branches rooted at same element as '%s':",
                                  action->relpath[0]);
              }
            else
              {
                svnmover_notify_v("branches rooted at e%d:",
                                  arg[0]->el_rev->eid);
              }
            SVN_ERR(list_branches(
                      arg[0]->el_rev->branch->txn,
                      arg[0]->el_rev->eid,
                      FALSE, iterpool));
          }
          break;

        case ACTION_LIST_BRANCHES_R:
          {
            if (the_ui_mode == UI_MODE_SERIAL)
              {
                svn_stream_t *stream;
                SVN_ERR(svn_stream_for_stdout(&stream, iterpool));
                SVN_ERR(svn_branch__txn_serialize(wc->working->branch->txn,
                          stream,
                          iterpool));
              }
            else
              {
                /* Note: BASE_REVISION is always a real revision number, here */
                SVN_ERR(list_all_branches(wc->working->branch->txn, TRUE,
                                          iterpool));
              }
          }
          break;

        case ACTION_LS:
          {
            VERIFY_EID_EXISTS("ls", 0);
            if (the_ui_mode == UI_MODE_PATHS)
              {
                SVN_ERR(list_branch_elements(arg[0]->el_rev->branch, iterpool));
              }
            else if (the_ui_mode == UI_MODE_EIDS)
              {
                SVN_ERR(list_branch_elements_by_eid(arg[0]->el_rev->branch,
                                                    iterpool));
              }
            else
              {
                svn_stream_t *stream;
                SVN_ERR(svn_stream_for_stdout(&stream, iterpool));
                SVN_ERR(svn_branch__state_serialize(stream,
                                                    arg[0]->el_rev->branch,
                                                    iterpool));
              }
          }
          break;

        case ACTION_TBRANCH:
          VERIFY_EID_EXISTS("tbranch", 0);
          {
            const char *from_branch_id = svn_branch__get_id(arg[0]->el_rev->branch,
                                                            iterpool);
            svn_branch__rev_bid_eid_t *from
              = svn_branch__rev_bid_eid_create(arg[0]->el_rev->rev, from_branch_id,
                                               arg[0]->el_rev->eid, iterpool);
            svn_branch__state_t *new_branch;

            SVN_ERR(do_topbranch(&new_branch, wc->edit_txn,
                                 from,
                                 iterpool, iterpool));
            /* Switch the WC working state to this new branch */
            wc->working->branch = new_branch;
          }
          break;

        case ACTION_BRANCH:
          VERIFY_EID_EXISTS("branch", 0);
          VERIFY_REV_UNSPECIFIED("branch", 1);
          VERIFY_EID_NONEXISTENT("branch", 1);
          VERIFY_PARENT_EID_EXISTS("branch", 1);
          {
            const char *from_branch_id = svn_branch__get_id(arg[0]->el_rev->branch,
                                                            iterpool);
            svn_branch__rev_bid_eid_t *from
              = svn_branch__rev_bid_eid_create(arg[0]->el_rev->rev, from_branch_id,
                                               arg[0]->el_rev->eid, iterpool);
            svn_branch__state_t *new_branch;

            SVN_ERR(do_branch(&new_branch, wc->edit_txn,
                              from,
                              arg[1]->el_rev->branch, arg[1]->parent_el_rev->eid,
                              arg[1]->path_name,
                              iterpool, iterpool));
          }
          break;

        case ACTION_BRANCH_INTO:
          VERIFY_EID_EXISTS("branch-into", 0);
          VERIFY_REV_UNSPECIFIED("branch-into", 1);
          VERIFY_EID_NONEXISTENT("branch-into", 1);
          VERIFY_PARENT_EID_EXISTS("branch-into", 1);
          {
            SVN_ERR(do_branch_into(arg[0]->el_rev->branch, arg[0]->el_rev->eid,
                                   arg[1]->el_rev->branch,
                                   arg[1]->parent_el_rev->eid, arg[1]->path_name,
                                   iterpool));
          }
          break;

        case ACTION_MKBRANCH:
          VERIFY_REV_UNSPECIFIED("mkbranch", 0);
          VERIFY_EID_NONEXISTENT("mkbranch", 0);
          VERIFY_PARENT_EID_EXISTS("mkbranch", 0);
          {
            apr_hash_t *props = apr_hash_make(iterpool);
            svn_element__payload_t *payload
              = svn_element__payload_create_dir(props, iterpool);

            SVN_ERR(do_mkbranch(NULL, wc->edit_txn,
                                arg[0]->parent_el_rev->branch,
                                arg[0]->parent_el_rev->eid, arg[0]->path_name,
                                payload, iterpool));
          }
          break;

        case ACTION_MERGE3:
          {
            VERIFY_EID_EXISTS("merge", 0);
            VERIFY_EID_EXISTS("merge", 1);
            VERIFY_REV_UNSPECIFIED("merge", 1);
            VERIFY_EID_EXISTS("merge", 2);

            SVN_ERR(do_merge(wc,
                             arg[0]->el_rev /*from*/,
                             arg[1]->el_rev /*to*/,
                             arg[2]->el_rev /*yca*/,
                             iterpool));
          }
          break;

        case ACTION_AUTO_MERGE:
          {
            VERIFY_EID_EXISTS("merge", 0);
            VERIFY_EID_EXISTS("merge", 1);
            VERIFY_REV_UNSPECIFIED("merge", 1);

            SVN_ERR(do_auto_merge(wc,
                                  arg[0]->el_rev /*from*/,
                                  arg[1]->el_rev /*to*/,
                                  iterpool));
          }
          break;

        case ACTION_MV:
          SVN_ERR(point_to_outer_element_instead(arg[0]->el_rev, "mv",
                                                 iterpool));

          VERIFY_REV_UNSPECIFIED("mv", 0);
          VERIFY_EID_EXISTS("mv", 0);
          VERIFY_REV_UNSPECIFIED("mv", 1);
          VERIFY_EID_NONEXISTENT("mv", 1);
          VERIFY_PARENT_EID_EXISTS("mv", 1);
          VERIFY_NOT_CHILD_OF_SELF("mv", 0, 1, iterpool);

          /* Simple move/rename within same branch, if possible */
          if (BRANCH_IS_SAME_BRANCH(arg[1]->parent_el_rev->branch,
                                    arg[0]->el_rev->branch,
                                    iterpool))
            {
              SVN_ERR(do_move(arg[0]->el_rev,
                              arg[1]->parent_el_rev, arg[1]->path_name,
                              iterpool));
            }
          else
            {
              SVN_ERR(do_interactive_cross_branch_move(wc->edit_txn,
                                                       arg[0]->el_rev,
                                                       arg[1]->parent_el_rev,
                                                       arg[1]->path_name,
                                                       iterpool));
            }
          break;

        case ACTION_CP:
          VERIFY_REV_SPECIFIED("cp", 0);
            /* (Or do we want to support copying from "this txn" too?) */
          VERIFY_EID_EXISTS("cp", 0);
          VERIFY_REV_UNSPECIFIED("cp", 1);
          VERIFY_EID_NONEXISTENT("cp", 1);
          VERIFY_PARENT_EID_EXISTS("cp", 1);
          SVN_ERR(do_copy(arg[0]->el_rev,
                          arg[1]->parent_el_rev->branch,
                          arg[1]->parent_el_rev->eid, arg[1]->path_name,
                          iterpool));
          break;

        case ACTION_RM:
          SVN_ERR(point_to_outer_element_instead(arg[0]->el_rev, "rm",
                                                 iterpool));

          VERIFY_REV_UNSPECIFIED("rm", 0);
          VERIFY_EID_EXISTS("rm", 0);
          SVN_ERR(do_delete(arg[0]->el_rev->branch, arg[0]->el_rev->eid,
                            iterpool));
          break;

        case ACTION_CP_RM:
          SVN_ERR(point_to_outer_element_instead(arg[0]->el_rev,
                                                 "copy-and-delete", iterpool));

          VERIFY_REV_UNSPECIFIED("copy-and-delete", 0);
          VERIFY_EID_EXISTS("copy-and-delete", 0);
          VERIFY_REV_UNSPECIFIED("copy-and-delete", 1);
          VERIFY_EID_NONEXISTENT("copy-and-delete", 1);
          VERIFY_PARENT_EID_EXISTS("copy-and-delete", 1);
          VERIFY_NOT_CHILD_OF_SELF("copy-and-delete", 0, 1, iterpool);

          SVN_ERR(do_copy_and_delete(arg[0]->el_rev,
                                     arg[1]->parent_el_rev->branch,
                                     arg[1]->parent_el_rev->eid,
                                     arg[1]->path_name,
                                     iterpool));
          break;

        case ACTION_BR_RM:
          SVN_ERR(point_to_outer_element_instead(arg[0]->el_rev,
                                                 "branch-and-delete",
                                                 iterpool));

          VERIFY_REV_UNSPECIFIED("branch-and-delete", 0);
          VERIFY_EID_EXISTS("branch-and-delete", 0);
          VERIFY_REV_UNSPECIFIED("branch-and-delete", 1);
          VERIFY_EID_NONEXISTENT("branch-and-delete", 1);
          VERIFY_PARENT_EID_EXISTS("branch-and-delete", 1);
          VERIFY_NOT_CHILD_OF_SELF("branch-and-delete", 0, 1, iterpool);

          SVN_ERR(do_branch_and_delete(wc->edit_txn,
                                       arg[0]->el_rev,
                                       arg[1]->parent_el_rev->branch,
                                       arg[1]->parent_el_rev->eid,
                                       arg[1]->path_name,
                                       iterpool));
          break;

        case ACTION_BR_INTO_RM:
          SVN_ERR(point_to_outer_element_instead(arg[0]->el_rev,
                                                 "branch-into-and-delete",
                                                 iterpool));

          VERIFY_REV_UNSPECIFIED("branch-into-and-delete", 0);
          VERIFY_EID_EXISTS("branch-into-and-delete", 0);
          VERIFY_REV_UNSPECIFIED("branch-into-and-delete", 1);
          VERIFY_EID_NONEXISTENT("branch-into-and-delete", 1);
          VERIFY_PARENT_EID_EXISTS("branch-into-and-delete", 1);
          VERIFY_NOT_CHILD_OF_SELF("branch-into-and-delete", 0, 1, iterpool);

          SVN_ERR(do_branch_into_and_delete(arg[0]->el_rev,
                                            arg[1]->parent_el_rev->branch,
                                            arg[1]->parent_el_rev->eid,
                                            arg[1]->path_name,
                                            iterpool));
          break;

        case ACTION_MKDIR:
          VERIFY_REV_UNSPECIFIED("mkdir", 0);
          VERIFY_EID_NONEXISTENT("mkdir", 0);
          VERIFY_PARENT_EID_EXISTS("mkdir", 0);
          SVN_ERR(do_mkdir(wc->edit_txn,
                           arg[0]->parent_el_rev->branch,
                           arg[0]->parent_el_rev->eid, arg[0]->path_name,
                           iterpool));
          break;

        case ACTION_PUT_FILE:
          VERIFY_REV_UNSPECIFIED("put", 1);
          VERIFY_PARENT_EID_EXISTS("put", 1);
          SVN_ERR(do_put_file(wc->edit_txn,
                              action->relpath[0],
                              arg[1]->el_rev,
                              arg[1]->parent_el_rev,
                              arg[1]->path_name,
                              iterpool));
          break;

        case ACTION_CAT:
          VERIFY_EID_EXISTS("rm", 0);
          SVN_ERR(do_cat(arg[0]->el_rev,
                         iterpool));
          break;

        case ACTION_COMMIT:
          {
            svn_revnum_t new_rev;

            SVN_ERR(do_commit(&new_rev, wc, revprops, iterpool));
            if (! SVN_IS_VALID_REVNUM(new_rev))
              {
                svnmover_notify_v("There are no changes to commit.");
              }
          }
          break;

        case ACTION_UPDATE:
          /* ### If current WC branch doesn't exist in target rev, should
             'update' follow to a different branch? By following merge graph?
             Presently it would try to update to a state of nonexistence. */
          /* path (or eid) is currently required for syntax, but ignored */
          VERIFY_EID_EXISTS("update", 0);
          /* We require a rev to be specified because an unspecified rev
             currently always means 'working version', whereas we would
             want it to mean 'head' for this subcommand. */
          VERIFY_REV_SPECIFIED("update", 0);
          {
            SVN_ERR(do_switch(wc, arg[0]->el_rev->rev, wc->base->branch,
                              iterpool));
          }
          break;

        case ACTION_SWITCH:
          VERIFY_EID_EXISTS("switch", 0);
          {
            SVN_ERR(do_switch(wc, arg[0]->el_rev->rev, arg[0]->el_rev->branch,
                              iterpool));
          }
          break;

        case ACTION_REVERT:
          {
            SVN_ERR(do_revert(wc, iterpool));
          }
          break;

        case ACTION_MIGRATE:
          /* path (or eid) is currently required for syntax, but ignored */
          VERIFY_EID_EXISTS("migrate", 0);
          VERIFY_REV_SPECIFIED("migrate", 0);
          {
            SVN_ERR(do_migrate(wc,
                               arg[0]->el_rev->rev, arg[0]->el_rev->rev,
                               iterpool));
          }
          break;

        default:
          SVN_ERR_MALFUNCTION();
        }

      if (action->action != ACTION_COMMIT)
        {
          wc->list_of_commands
            = apr_psprintf(pool, "%s%s\n",
                           wc->list_of_commands ? wc->list_of_commands : "",
                           svn_cstring_join2(action->action_args, " ",
                                             TRUE, pool));
        }
    }
  svn_pool_destroy(iterpool);
  return SVN_NO_ERROR;
}

/* Perform the typical suite of manipulations for user-provided URLs
   on URL, returning the result (allocated from POOL): IRI-to-URI
   conversion, auto-escaping, and canonicalization. */
static const char *
sanitize_url(const char *url,
             apr_pool_t *pool)
{
  url = svn_path_uri_from_iri(url, pool);
  url = svn_path_uri_autoescape(url, pool);
  return svn_uri_canonicalize(url, pool);
}

static const char *
help_for_subcommand(const action_defn_t *action, apr_pool_t *pool)
{
  const char *cmd = apr_psprintf(pool, "%s %s",
                                 action->name, action->args_help);

  return apr_psprintf(pool, "  %-22s : %s\n", cmd, action->help);
}

/* Print a usage message on STREAM, listing only the actions. */
static void
usage_actions_only(FILE *stream, apr_pool_t *pool)
{
  int i;

  for (i = 0; i < sizeof (action_defn) / sizeof (action_defn[0]); i++)
    svn_error_clear(svn_cmdline_fputs(
                      help_for_subcommand(&action_defn[i], pool),
                      stream, pool));
}

/* Print a usage message on STREAM. */
static void
usage(FILE *stream, apr_pool_t *pool)
{
  svn_error_clear(svn_cmdline_fputs(
    _("usage: svnmover -U REPO_URL [ACTION...]\n"
      "A client for experimenting with move tracking.\n"
      "\n"
      "  Commit a batch of ACTIONs to a Subversion repository, as a single\n"
      "  new revision.  With no ACTIONs specified, read actions interactively\n"
      "  from standard input, until EOF or ^C, and then commit the result.\n"
      "\n"
      "  Action arguments are of the form\n"
      "    [^B<branch-id>/]<path>[@<revnum>]\n"
      "  where\n"
      "    <branch-id> defaults to the working branch or, when <revnum> is\n"
      "                given, to the base branch\n"
      "    <path>      is a path relative to the branch\n"
      "    <revnum>    is the revision number, when making a historic reference\n"
      "\n"
      "  Move tracking metadata is stored in the repository, in on-disk files\n"
      "  for RA-local or in revprops otherwise.\n"
      "\n"
      "Actions:\n"),
                  stream, pool));
  usage_actions_only(stream, pool);
  svn_error_clear(svn_cmdline_fputs(
    _("\n"
      "Valid options:\n"
      "  --ui={eids|e|paths|p}  : display information as elements or as paths\n"
      "  --colo[u]r={always|never|auto}\n"
      "                         : use coloured output; 'auto' means when standard\n"
      "                           output goes to a terminal; default: never\n"
      "  -h, -? [--help]        : display this text\n"
      "  -v [--verbose]         : display debugging messages\n"
      "  -q [--quiet]           : suppress notifications\n"
      "  -m [--message] ARG     : use ARG as a log message\n"
      "  -F [--file] ARG        : read log message from file ARG\n"
      "  -u [--username] ARG    : commit the changes as username ARG\n"
      "  -p [--password] ARG    : use ARG as the password\n"
      "  -U [--root-url] ARG    : interpret all action URLs relative to ARG\n"
      "  -r [--revision] ARG    : use revision ARG as baseline for changes\n"
      "  -B [--branch-id] ARG   : work on the branch identified by ARG\n"
      "  --with-revprop ARG     : set revision property in the following format:\n"
      "                               NAME[=VALUE]\n"
      "  --non-interactive      : do no interactive prompting (default is to\n"
      "                           prompt only if standard input is a terminal)\n"
      "  --force-interactive    : do interactive prompting even if standard\n"
      "                           input is not a terminal\n"
      "  --trust-server-cert    : accept SSL server certificates from unknown\n"
      "                           certificate authorities without prompting (but\n"
      "                           only with '--non-interactive')\n"
      "  -X [--extra-args] ARG  : append arguments from file ARG (one per line;\n"
      "                           use \"-\" to read from standard input)\n"
      "  --config-dir ARG       : use ARG to override the config directory\n"
      "  --config-option ARG    : use ARG to override a configuration option\n"
      "  --no-auth-cache        : do not cache authentication tokens\n"
      "  --version              : print version information\n"),
                  stream, pool));
}

static svn_error_t *
insufficient(int i, apr_pool_t *pool)
{
  return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                           "insufficient arguments:\n"
                           "%s",
                           help_for_subcommand(&action_defn[i], pool));
}

static svn_error_t *
display_version(apr_getopt_t *os, svn_boolean_t _quiet, apr_pool_t *pool)
{
  const char *ra_desc_start
    = "The following repository access (RA) modules are available:\n\n";
  svn_stringbuf_t *version_footer;

  version_footer = svn_stringbuf_create(ra_desc_start, pool);
  SVN_ERR(svn_ra_print_modules(version_footer, pool));

  SVN_ERR(svn_opt_print_help5(NULL, "svnmover", TRUE, _quiet, FALSE,
                              version_footer->data,
                              NULL, NULL, NULL, NULL, NULL, pool));

  return SVN_NO_ERROR;
}

/* Return an error about the mutual exclusivity of the -m, -F, and
   --with-revprop=svn:log command-line options. */
static svn_error_t *
mutually_exclusive_logs_error(void)
{
  return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
                          _("--message (-m), --file (-F), and "
                            "--with-revprop=svn:log are mutually "
                            "exclusive"));
}

/* Obtain the log message from multiple sources, producing an error
   if there are multiple sources. Store the result in *FINAL_MESSAGE.  */
static svn_error_t *
get_log_message(const char **final_message,
                const char *message,
                apr_hash_t *revprops,
                svn_stringbuf_t *filedata,
                apr_pool_t *result_pool,
                apr_pool_t *scratch_pool)
{
  svn_string_t *msg;

  *final_message = NULL;
  /* If we already have a log message in the revprop hash, then just
     make sure the user didn't try to also use -m or -F.  Otherwise,
     we need to consult -m or -F to find a log message, if any. */
  msg = svn_hash_gets(revprops, SVN_PROP_REVISION_LOG);
  if (msg)
    {
      if (filedata || message)
        return mutually_exclusive_logs_error();

      /* Remove it from the revprops; it will be re-added later */
      svn_hash_sets(revprops, SVN_PROP_REVISION_LOG, NULL);
    }
  else if (filedata)
    {
      if (message)
        return mutually_exclusive_logs_error();

      msg = svn_string_create(filedata->data, scratch_pool);
    }
  else if (message)
    {
      msg = svn_string_create(message, scratch_pool);
    }

  if (msg)
    {
      SVN_ERR_W(svn_subst_translate_string2(&msg, NULL, NULL,
                                            msg, NULL, FALSE,
                                            result_pool, scratch_pool),
                _("Error normalizing log message to internal format"));

      *final_message = msg->data;
    }

  return SVN_NO_ERROR;
}

static const char *const special_commands[] =
{
  "help",
  "--verbose",
  "--ui=paths", "--ui=eids", "--ui=serial",
};

/* Parse the action arguments into action structures. */
static svn_error_t *
parse_actions(apr_array_header_t **actions,
              apr_array_header_t *action_args,
              apr_pool_t *pool)
{
  int i;

  *actions = apr_array_make(pool, 1, sizeof(action_t *));

  for (i = 0; i < action_args->nelts; ++i)
    {
      int j, k, num_url_args;
      const char *action_string = APR_ARRAY_IDX(action_args, i, const char *);
      action_t *action = apr_pcalloc(pool, sizeof(*action));
      const char *cp_from_rev = NULL;

      /* First, parse the action. Handle some special actions immediately;
         handle normal subcommands by looking them up in the table. */
      if (! strcmp(action_string, "?") || ! strcmp(action_string, "h")
          || ! strcmp(action_string, "help"))
        {
          usage_actions_only(stdout, pool);
          return SVN_NO_ERROR;
        }
      if (! strncmp(action_string, "--ui=", 5))
        {
          SVN_ERR(svn_token__from_word_err(&the_ui_mode, ui_mode_map,
                                           action_string + 5));
          continue;
        }
      if (! strcmp(action_string, "--verbose")
          || ! strcmp(action_string, "-v"))
        {
          quiet = !quiet;
          svnmover_notify("verbose mode %s", quiet ? "off" : "on");
          continue;
        }
      for (j = 0; j < sizeof(action_defn) / sizeof(action_defn[0]); j++)
        {
          if (strcmp(action_string, action_defn[j].name) == 0)
            {
              action->action = action_defn[j].code;
              num_url_args = action_defn[j].num_args;
              break;
            }
        }
      if (j == sizeof(action_defn) / sizeof(action_defn[0]))
        return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                                 "'%s' is not an action; try 'help'.",
                                 action_string);

      action->action_args = apr_array_make(pool, 0, sizeof(const char *));
      APR_ARRAY_PUSH(action->action_args, const char *) = action_string;

      if (action->action == ACTION_CP)
        {
          /* next argument is the copy source revision */
          if (++i == action_args->nelts)
            return svn_error_trace(insufficient(j, pool));
          cp_from_rev = APR_ARRAY_IDX(action_args, i, const char *);
          APR_ARRAY_PUSH(action->action_args, const char *) = cp_from_rev;
        }

      /* Parse the required number of URLs. */
      for (k = 0; k < num_url_args; ++k)
        {
          const char *path;

          if (++i == action_args->nelts)
            return svn_error_trace(insufficient(j, pool));
          path = APR_ARRAY_IDX(action_args, i, const char *);
          APR_ARRAY_PUSH(action->action_args, const char *) = path;

          if (cp_from_rev && k == 0)
            {
              path = apr_psprintf(pool, "%s@%s", path, cp_from_rev);
            }

          SVN_ERR(svn_opt_parse_path(&action->rev_spec[k], &path, path, pool));

          /* If there's an ANCHOR_URL, we expect URL to be a path
             relative to ANCHOR_URL (and we build a full url from the
             combination of the two).  Otherwise, it should be a full
             url. */
          if (svn_path_is_url(path))
            {
              return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                                       "Argument '%s' is a URL; use "
                                       "--root-url (-U) instead", path);
            }
          /* Parse "^B<branch-id>/path" syntax. */
          if (strncmp("^B", path, 2) == 0)
            {
              const char *slash = strchr(path, '/');

              action->branch_id[k]
                = slash ? apr_pstrndup(pool, path + 1, slash - (path + 1))
                        : path + 1;
              path = slash ? slash + 1 : "";
            }
          /* These args must be relpaths, except for the 'local file' arg
             of a 'put' command. */
          if (! svn_relpath_is_canonical(path)
              && ! (action->action == ACTION_PUT_FILE && k == 0))
            {
              return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                                       "Argument '%s' is not a relative path "
                                       "or a URL", path);
            }
          action->relpath[k] = path;
        }

      APR_ARRAY_PUSH(*actions, action_t *) = action;
    }

  return SVN_NO_ERROR;
}

#ifdef HAVE_LINENOISE
/* A command-line completion callback for the 'Line Noise' interactive
 * prompting.
 *
 * This is called when the user presses the Tab key. It calculates the
 * possible completions for the partial line BUF.
 *
 * ### So far, this only works on a single command keyword at the start
 *     of the line.
 */
static void
linenoise_completion(const char *buf, linenoiseCompletions *lc)
{
  int i;

  for (i = 0; i < sizeof(special_commands) / sizeof(special_commands[0]); i++)
    {
      /* Suggest each command that matches (and is longer than) what the
         user has already typed. Add a space. */
      if (strncmp(buf, special_commands[i], strlen(buf)) == 0
          && strlen(special_commands[i]) > strlen(buf))
        {
          static char completion[100];

          apr_cpystrn(completion, special_commands[i], 99);
          strcat(completion, " ");
          linenoiseAddCompletion(lc, completion);
        }
    }

  for (i = 0; i < sizeof(action_defn) / sizeof(action_defn[0]); i++)
    {
      /* Suggest each command that matches (and is longer than) what the
         user has already typed. Add a space. */
      if (strncmp(buf, action_defn[i].name, strlen(buf)) == 0
          && strlen(action_defn[i].name) > strlen(buf))
        {
          static char completion[100];

          apr_cpystrn(completion, action_defn[i].name, 99);
          strcat(completion, " ");
          linenoiseAddCompletion(lc, completion);
        }
    }
}
#endif

/* Display a prompt, read a line of input and split it into words.
 *
 * Set *WORDS to null if input is cancelled (by ctrl-C for example).
 */
static svn_error_t *
read_words(apr_array_header_t **words,
           const char *prompt,
           apr_pool_t *result_pool)
{
  svn_error_t *err;
  const char *input;

  settext(TEXT_FG_YELLOW);
  err = svnmover_prompt_user(&input, prompt, result_pool);
  settext(TEXT_RESET);
  if (err && (err->apr_err == SVN_ERR_CANCELLED || err->apr_err == APR_EOF))
    {
      *words = NULL;
      svn_error_clear(err);
      return SVN_NO_ERROR;
    }
  SVN_ERR(err);
  *words = svn_cstring_split(input, " ", TRUE /*chop_whitespace*/, result_pool);

  return SVN_NO_ERROR;
}

/*
 * On success, leave *EXIT_CODE untouched and return SVN_NO_ERROR. On error,
 * either return an error to be displayed, or set *EXIT_CODE to non-zero and
 * return SVN_NO_ERROR.
 */
static svn_error_t *
sub_main(int *exit_code, int argc, const char *argv[], apr_pool_t *pool)
{
  apr_array_header_t *actions;
  svn_error_t *err = SVN_NO_ERROR;
  apr_getopt_t *opts;
  enum {
    config_dir_opt = SVN_OPT_FIRST_LONGOPT_ID,
    config_inline_opt,
    no_auth_cache_opt,
    version_opt,
    with_revprop_opt,
    non_interactive_opt,
    force_interactive_opt,
    trust_server_cert_opt,
    trust_server_cert_failures_opt,
    ui_opt,
    colour_opt,
    auth_password_from_stdin_opt
  };
  static const apr_getopt_option_t options[] = {
    {"verbose", 'v', 0, ""},
    {"quiet", 'q', 0, ""},
    {"message", 'm', 1, ""},
    {"file", 'F', 1, ""},
    {"username", 'u', 1, ""},
    {"password", 'p', 1, ""},
    {"password-from-stdin", auth_password_from_stdin_opt, 1, ""},
    {"root-url", 'U', 1, ""},
    {"revision", 'r', 1, ""},
    {"branch-id", 'B', 1, ""},
    {"with-revprop",  with_revprop_opt, 1, ""},
    {"extra-args", 'X', 1, ""},
    {"help", 'h', 0, ""},
    {NULL, '?', 0, ""},
    {"non-interactive", non_interactive_opt, 0, ""},
    {"force-interactive", force_interactive_opt, 0, ""},
    {"trust-server-cert", trust_server_cert_opt, 0, ""},
    {"trust-server-cert-failures", trust_server_cert_failures_opt, 1, ""},
    {"config-dir", config_dir_opt, 1, ""},
    {"config-option",  config_inline_opt, 1, ""},
    {"no-auth-cache",  no_auth_cache_opt, 0, ""},
    {"version", version_opt, 0, ""},
    {"ui", ui_opt, 1, ""},
    {"colour", colour_opt, 1, ""},
    {"color", colour_opt, 1, ""},
    {NULL, 0, 0, NULL}
  };
  const char *message = NULL;
  svn_stringbuf_t *filedata = NULL;
  const char *username = NULL, *password = NULL;
  const char *anchor_url = NULL, *extra_args_file = NULL;
  const char *config_dir = NULL;
  apr_array_header_t *config_options;
  svn_boolean_t show_version = FALSE;
  svn_boolean_t non_interactive = FALSE;
  svn_boolean_t force_interactive = FALSE;
  svn_boolean_t interactive_actions;
  svn_boolean_t trust_unknown_ca = FALSE;
  svn_boolean_t trust_cn_mismatch = FALSE;
  svn_boolean_t trust_expired = FALSE;
  svn_boolean_t trust_not_yet_valid = FALSE;
  svn_boolean_t trust_other_failure = FALSE;
  svn_boolean_t no_auth_cache = FALSE;
  svn_revnum_t base_revision = SVN_INVALID_REVNUM;
  const char *branch_id = "B0";  /* default branch */
  apr_array_header_t *action_args;
  apr_hash_t *revprops = apr_hash_make(pool);
  apr_hash_t *cfg_hash;
  svn_config_t *cfg_config;
  svn_client_ctx_t *ctx;
  const char *log_msg;
  svn_tristate_t coloured_output = svn_tristate_false;
  svnmover_wc_t *wc;
  svn_boolean_t read_pass_from_stdin = FALSE;

  /* Check library versions */
  SVN_ERR(check_lib_versions());

  config_options = apr_array_make(pool, 0,
                                  sizeof(svn_cmdline__config_argument_t*));

  apr_getopt_init(&opts, pool, argc, argv);
  opts->interleave = 1;
  while (1)
    {
      int opt;
      const char *arg;
      const char *opt_arg;

      apr_status_t status = apr_getopt_long(opts, options, &opt, &arg);
      if (APR_STATUS_IS_EOF(status))
        break;
      if (status != APR_SUCCESS)
        return svn_error_wrap_apr(status, "getopt failure");
      switch(opt)
        {
        case 'v':
          quiet = FALSE;
          break;
        case 'q':
          quiet = TRUE;
          break;
        case 'm':
          SVN_ERR(svn_utf_cstring_to_utf8(&message, arg, pool));
          break;
        case 'F':
          {
            const char *filename;
            SVN_ERR(svn_utf_cstring_to_utf8(&filename, arg, pool));
            SVN_ERR(svn_stringbuf_from_file2(&filedata, filename, pool));
          }
          break;
        case 'u':
          username = apr_pstrdup(pool, arg);
          break;
        case 'p':
          password = apr_pstrdup(pool, arg);
          break;
        case auth_password_from_stdin_opt:
          read_pass_from_stdin = TRUE;
          break;
        case 'U':
          SVN_ERR(svn_utf_cstring_to_utf8(&anchor_url, arg, pool));
          if (! svn_path_is_url(anchor_url))
            return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                                     "'%s' is not a URL", anchor_url);
          anchor_url = sanitize_url(anchor_url, pool);
          break;
        case 'r':
          {
            const char *saved_arg = arg;
            char *digits_end = NULL;
            while (*arg == 'r')
              arg++;
            base_revision = strtol(arg, &digits_end, 10);
            if ((! SVN_IS_VALID_REVNUM(base_revision))
                || (! digits_end)
                || *digits_end)
              return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
                                       _("Invalid revision number '%s'"),
                                       saved_arg);
          }
          break;
        case 'B':
          branch_id = (arg[0] == 'B') ? apr_pstrdup(pool, arg)
                                      : apr_psprintf(pool, "B%s", arg);
          break;
        case with_revprop_opt:
          SVN_ERR(svn_opt_parse_revprop(&revprops, arg, pool));
          break;
        case 'X':
          SVN_ERR(svn_utf_cstring_to_utf8(&extra_args_file, arg, pool));
          break;
        case non_interactive_opt:
          non_interactive = TRUE;
          break;
        case force_interactive_opt:
          force_interactive = TRUE;
          break;
        case trust_server_cert_opt:
          trust_unknown_ca = TRUE;
          break;
        case trust_server_cert_failures_opt:
          SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, arg, pool));
          SVN_ERR(svn_cmdline__parse_trust_options(
                      &trust_unknown_ca,
                      &trust_cn_mismatch,
                      &trust_expired,
                      &trust_not_yet_valid,
                      &trust_other_failure,
                      opt_arg, pool));
          break;
        case config_dir_opt:
          SVN_ERR(svn_utf_cstring_to_utf8(&config_dir, arg, pool));
          break;
        case config_inline_opt:
          SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, arg, pool));
          SVN_ERR(svn_cmdline__parse_config_option(config_options, opt_arg,
                                                   "svnmover: ", pool));
          break;
        case no_auth_cache_opt:
          no_auth_cache = TRUE;
          break;
        case version_opt:
          show_version = TRUE;
          break;
        case ui_opt:
          SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, arg, pool));
          SVN_ERR(svn_token__from_word_err(&the_ui_mode, ui_mode_map, opt_arg));
          break;
        case colour_opt:
          if (strcmp(arg, "always") == 0)
            coloured_output = svn_tristate_true;
          else if (strcmp(arg, "never") == 0)
            coloured_output = svn_tristate_false;
          else if (strcmp(arg, "auto") == 0)
            coloured_output = svn_tristate_unknown;
          else
            return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
                                     _("Bad argument in '--colour=%s': "
                                       "use one of 'always', 'never', 'auto'"),
                                     arg);
          break;
        case 'h':
        case '?':
          usage(stdout, pool);
          return SVN_NO_ERROR;
        }
    }

  if (show_version)
    {
      SVN_ERR(display_version(opts, quiet, pool));
      return SVN_NO_ERROR;
    }

  if (coloured_output == svn_tristate_true)
    use_coloured_output = TRUE;
  else if (coloured_output == svn_tristate_false)
    use_coloured_output = FALSE;
  else
    use_coloured_output = (svn_cmdline__stdout_is_a_terminal()
                           && svn_cmdline__stderr_is_a_terminal());

  if (non_interactive && force_interactive)
    {
      return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
                              _("--non-interactive and --force-interactive "
                                "are mutually exclusive"));
    }
  else
    non_interactive = !svn_cmdline__be_interactive(non_interactive,
                                                   force_interactive);

  if (!non_interactive)
    {
      if (trust_unknown_ca || trust_cn_mismatch || trust_expired
          || trust_not_yet_valid || trust_other_failure)
        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
                                _("--trust-server-cert-failures requires "
                                  "--non-interactive"));
    }

  /* --password-from-stdin can only be used with --non-interactive */
  if (read_pass_from_stdin && !non_interactive)
    {
      return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
                              _("--password-from-stdin requires "
                                "--non-interactive"));
    }

  /* Now initialize the client context */

  err = svn_config_get_config(&cfg_hash, config_dir, pool);
  if (err)
    {
      /* Fallback to default config if the config directory isn't readable
         or is not a directory. */
      if (APR_STATUS_IS_EACCES(err->apr_err)
          || SVN__APR_STATUS_IS_ENOTDIR(err->apr_err))
        {
          svn_handle_warning2(stderr, err, "svnmover: ");
          svn_error_clear(err);

          SVN_ERR(svn_config__get_default_config(&cfg_hash, pool));
        }
      else
        return err;
    }

  if (config_options)
    {
      svn_error_clear(
          svn_cmdline__apply_config_options(cfg_hash, config_options,
                                            "svnmover: ", "--config-option"));
    }

  /* Get password from stdin if necessary */
  if (read_pass_from_stdin)
    {
      SVN_ERR(svn_cmdline__stdin_readline(&password, pool, pool));
    }

  SVN_ERR(svn_client_create_context2(&ctx, cfg_hash, pool));

  cfg_config = svn_hash_gets(cfg_hash, SVN_CONFIG_CATEGORY_CONFIG);
  SVN_ERR(svn_cmdline_create_auth_baton2(&ctx->auth_baton,
                                         non_interactive,
                                         username,
                                         password,
                                         config_dir,
                                         no_auth_cache,
                                         trust_unknown_ca,
                                         trust_cn_mismatch,
                                         trust_expired,
                                         trust_not_yet_valid,
                                         trust_other_failure,
                                         cfg_config,
                                         ctx->cancel_func,
                                         ctx->cancel_baton,
                                         pool));

  /* Get the commit log message */
  SVN_ERR(get_log_message(&log_msg, message, revprops, filedata,
                          pool, pool));

  /* Put the log message in the list of revprops, and check that the user
     did not try to supply any other "svn:*" revprops. */
  if (svn_prop_has_svn_prop(revprops, pool))
    return svn_error_create(SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
                            _("Standard properties can't be set "
                              "explicitly as revision properties"));
  if (log_msg)
    {
      svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
                    svn_string_create(log_msg, pool));
    }

  /* Help command: if given before any actions, then display full help
     (and ANCHOR_URL need not have been provided). */
  if (opts->ind < opts->argc && strcmp(opts->argv[opts->ind], "help") == 0)
    {
      usage(stdout, pool);
      return SVN_NO_ERROR;
    }

  if (!anchor_url)
    return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                             "--root-url (-U) not provided");

  /* Copy the rest of our command-line arguments to an array,
     UTF-8-ing them along the way. */
  /* If there are extra arguments in a supplementary file, tack those
     on, too (again, in UTF8 form). */
  action_args = apr_array_make(pool, opts->argc, sizeof(const char *));
  if (extra_args_file)
    {
      svn_stringbuf_t *contents, *contents_utf8;

      SVN_ERR(svn_stringbuf_from_file2(&contents, extra_args_file, pool));
      SVN_ERR(svn_utf_stringbuf_to_utf8(&contents_utf8, contents, pool));
      svn_cstring_split_append(action_args, contents_utf8->data, "\n\r",
                               FALSE, pool);
    }

  interactive_actions = !(opts->ind < opts->argc
                          || extra_args_file
                          || non_interactive);

  if (interactive_actions)
    {
#ifdef HAVE_LINENOISE
      linenoiseSetCompletionCallback(linenoise_completion);
#endif
    }

  SVN_ERR(wc_create(&wc,
                    anchor_url, base_revision,
                    branch_id,
                    ctx, pool, pool));

  do
    {
      /* Parse arguments -- converting local style to internal style,
       * repos-relative URLs to regular URLs, etc. */
      err = svn_client_args_to_target_array2(&action_args, opts, action_args,
                                             ctx, FALSE, pool);
      if (! err)
        err = parse_actions(&actions, action_args, pool);
      if (! err)
        err = execute(wc, actions, anchor_url, revprops, ctx, pool);
      if (err)
        {
          if (err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive)
            err = svn_error_quick_wrap(err,
                                       _("Authentication failed and interactive"
                                         " prompting is disabled; see the"
                                         " --force-interactive option"));
          if (interactive_actions)
            {
              /* Display the error, but don't quit */
              settext_stderr(TEXT_FG_RED);
              svn_handle_error2(err, stderr, FALSE, "svnmover: ");
              settext_stderr(TEXT_RESET);
              svn_error_clear(err);
            }
          else
            SVN_ERR(err);
        }

      /* Possibly read more actions from the command line */
      if (interactive_actions)
        {
          SVN_ERR(read_words(&action_args, "svnmover> ", pool));
        }
    }
  while (interactive_actions && action_args);

  /* Final commit */
  err = commit(NULL, wc, revprops, pool);
  svn_pool_destroy(wc->pool);
  SVN_ERR(err);

  return SVN_NO_ERROR;
}

int
main(int argc, const char *argv[])
{
  apr_pool_t *pool;
  int exit_code = EXIT_SUCCESS;
  svn_error_t *err;

  /* Initialize the app. */
  if (svn_cmdline_init("svnmover", stderr) != EXIT_SUCCESS)
    return EXIT_FAILURE;

  /* Create our top-level pool.  Use a separate mutexless allocator,
   * given this application is single threaded.
   */
  pool = apr_allocator_owner_get(svn_pool_create_allocator(FALSE));

  svn_error_set_malfunction_handler(svn_error_raise_on_malfunction);

  err = sub_main(&exit_code, argc, argv, pool);

  /* Flush stdout and report if it fails. It would be flushed on exit anyway
     but this makes sure that output is not silently lost if it fails. */
  err = svn_error_compose_create(err, svn_cmdline_fflush(stdout));

  if (err)
    {
      exit_code = EXIT_FAILURE;
      settext_stderr(TEXT_FG_RED);
      svn_cmdline_handle_exit_error(err, NULL, "svnmover: ");
      settext_stderr(TEXT_RESET);
    }

  svn_pool_destroy(pool);
  return exit_code;
}
