summaryrefslogtreecommitdiff
path: root/tools/dev/svnmover/svnmover.c
diff options
context:
space:
mode:
Diffstat (limited to 'tools/dev/svnmover/svnmover.c')
-rw-r--r--tools/dev/svnmover/svnmover.c4759
1 files changed, 4759 insertions, 0 deletions
diff --git a/tools/dev/svnmover/svnmover.c b/tools/dev/svnmover/svnmover.c
new file mode 100644
index 0000000..8bc8b65
--- /dev/null
+++ b/tools/dev/svnmover/svnmover.c
@@ -0,0 +1,4759 @@
+/*
+ * 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_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_help4(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;
+}