| /* |
| * 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_join(action->action_args, " ", 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 |
| }; |
| 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, ""}, |
| {"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; |
| |
| /* 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 *arg_utf8; |
| SVN_ERR(svn_utf_cstring_to_utf8(&arg_utf8, arg, pool)); |
| SVN_ERR(svn_stringbuf_from_file2(&filedata, arg, pool)); |
| } |
| break; |
| case 'u': |
| username = apr_pstrdup(pool, arg); |
| break; |
| case 'p': |
| password = apr_pstrdup(pool, arg); |
| 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': |
| extra_args_file = apr_pstrdup(pool, arg); |
| 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")); |
| } |
| |
| |
| /* 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")); |
| } |
| |
| 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) |
| { |
| const char *extra_args_file_utf8; |
| svn_stringbuf_t *contents, *contents_utf8; |
| |
| SVN_ERR(svn_utf_cstring_to_utf8(&extra_args_file_utf8, |
| extra_args_file, pool)); |
| SVN_ERR(svn_stringbuf_from_file2(&contents, extra_args_file_utf8, 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; |
| } |