diff options
Diffstat (limited to 'tools/dev/svnmover/svnmover.c')
-rw-r--r-- | tools/dev/svnmover/svnmover.c | 4759 |
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; +} |