| /* |
| * svnrdump.c: Produce a dumpfile of a local or remote repository |
| * without touching the filesystem, but for temporary files. |
| * |
| * ==================================================================== |
| * 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 <apr_uri.h> |
| |
| #include "svn_pools.h" |
| #include "svn_cmdline.h" |
| #include "svn_client.h" |
| #include "svn_hash.h" |
| #include "svn_ra.h" |
| #include "svn_repos.h" |
| #include "svn_path.h" |
| #include "svn_utf.h" |
| #include "svn_private_config.h" |
| #include "svn_string.h" |
| #include "svn_props.h" |
| |
| #include "svnrdump.h" |
| |
| #include "private/svn_repos_private.h" |
| #include "private/svn_cmdline_private.h" |
| #include "private/svn_ra_private.h" |
| |
| |
| |
| /*** Cancellation ***/ |
| |
| /* Our cancellation callback. */ |
| static svn_cancel_func_t check_cancel = NULL; |
| |
| |
| |
| static svn_opt_subcommand_t dump_cmd, load_cmd; |
| |
| enum svn_svnrdump__longopt_t |
| { |
| opt_config_dir = SVN_OPT_FIRST_LONGOPT_ID, |
| opt_config_option, |
| opt_auth_username, |
| opt_auth_password, |
| opt_auth_password_from_stdin, |
| opt_auth_nocache, |
| opt_non_interactive, |
| opt_skip_revprop, |
| opt_force_interactive, |
| opt_incremental, |
| opt_trust_server_cert, |
| opt_trust_server_cert_failures, |
| opt_version |
| }; |
| |
| #define SVN_SVNRDUMP__BASE_OPTIONS opt_config_dir, \ |
| opt_config_option, \ |
| opt_auth_username, \ |
| opt_auth_password, \ |
| opt_auth_password_from_stdin, \ |
| opt_auth_nocache, \ |
| opt_trust_server_cert, \ |
| opt_trust_server_cert_failures, \ |
| opt_non_interactive, \ |
| opt_force_interactive |
| |
| static const svn_opt_subcommand_desc3_t svnrdump__cmd_table[] = |
| { |
| { "dump", dump_cmd, { 0 }, {N_( |
| "usage: svnrdump dump URL [-r LOWER[:UPPER]]\n" |
| "\n"), N_( |
| "Dump revisions LOWER to UPPER of repository at remote URL to stdout\n" |
| "in a 'dumpfile' portable format. If only LOWER is given, dump that\n" |
| "one revision.\n" |
| )}, |
| { 'r', 'q', opt_incremental, 'F', SVN_SVNRDUMP__BASE_OPTIONS }, |
| {{'F', N_("write to file ARG instead of stdout")}} }, |
| { "load", load_cmd, { 0 }, {N_( |
| "usage: svnrdump load URL\n" |
| "\n"), N_( |
| "Load a 'dumpfile' given on stdin to a repository at remote URL.\n" |
| )}, |
| { 'q', opt_skip_revprop, 'F', SVN_SVNRDUMP__BASE_OPTIONS }, |
| {{'F', N_("read from file ARG instead of stdin")}} }, |
| { "help", 0, { "?", "h" }, {N_( |
| "usage: svnrdump help [SUBCOMMAND...]\n" |
| "\n"), N_( |
| "Describe the usage of this program or its subcommands.\n" |
| )}, |
| { 0 } }, |
| { NULL, NULL, { 0 }, {NULL}, { 0 } } |
| }; |
| |
| static const apr_getopt_option_t svnrdump__options[] = |
| { |
| {"revision", 'r', 1, |
| N_("specify revision number ARG (or X:Y range)")}, |
| {"quiet", 'q', 0, |
| N_("no progress (only errors) to stderr")}, |
| {"incremental", opt_incremental, 0, |
| N_("dump incrementally")}, |
| {"skip-revprop", opt_skip_revprop, 1, |
| N_("skip revision property ARG (e.g., \"svn:author\")")}, |
| {"config-dir", opt_config_dir, 1, |
| N_("read user configuration files from directory ARG")}, |
| {"username", opt_auth_username, 1, |
| N_("specify a username ARG")}, |
| {"password", opt_auth_password, 1, |
| N_("specify a password ARG")}, |
| {"password-from-stdin", opt_auth_password_from_stdin, 0, |
| N_("read password from stdin")}, |
| {"non-interactive", opt_non_interactive, 0, |
| N_("do no interactive prompting (default is to prompt\n" |
| " " |
| "only if standard input is a terminal device)")}, |
| {"force-interactive", opt_force_interactive, 0, |
| N_("do interactive prompting even if standard input\n" |
| " " |
| "is not a terminal device")}, |
| {"no-auth-cache", opt_auth_nocache, 0, |
| N_("do not cache authentication tokens")}, |
| {"help", 'h', 0, |
| N_("display this help")}, |
| {"version", opt_version, 0, |
| N_("show program version information")}, |
| {"config-option", opt_config_option, 1, |
| N_("set user configuration option in the format:\n" |
| " " |
| " FILE:SECTION:OPTION=[VALUE]\n" |
| " " |
| "For example:\n" |
| " " |
| " servers:global:http-library=serf")}, |
| {"trust-server-cert", opt_trust_server_cert, 0, |
| N_("deprecated; same as\n" |
| " " |
| "--trust-server-cert-failures=unknown-ca")}, |
| {"trust-server-cert-failures", opt_trust_server_cert_failures, 1, |
| N_("with --non-interactive, accept SSL server\n" |
| " " |
| "certificates with failures; ARG is comma-separated\n" |
| " " |
| "list of 'unknown-ca' (Unknown Authority),\n" |
| " " |
| "'cn-mismatch' (Hostname mismatch), 'expired'\n" |
| " " |
| "(Expired certificate), 'not-yet-valid' (Not yet\n" |
| " " |
| "valid certificate) and 'other' (all other not\n" |
| " " |
| "separately classified certificate errors).")}, |
| {"file", 'F', 1, |
| N_("read/write file ARG instead of stdin/stdout")}, |
| {0, 0, 0, 0} |
| }; |
| |
| /* Baton for the RA replay session. */ |
| struct replay_baton { |
| /* A backdoor ra session for fetching information. */ |
| svn_ra_session_t *extra_ra_session; |
| |
| /* The output stream */ |
| svn_stream_t *stdout_stream; |
| |
| /* Whether to be quiet. */ |
| svn_boolean_t quiet; |
| }; |
| |
| /* Option set */ |
| typedef struct opt_baton_t { |
| svn_client_ctx_t *ctx; |
| svn_ra_session_t *session; |
| const char *url; |
| const char *dumpfile; |
| svn_boolean_t help; |
| svn_boolean_t version; |
| svn_opt_revision_t start_revision; |
| svn_opt_revision_t end_revision; |
| svn_boolean_t quiet; |
| svn_boolean_t incremental; |
| apr_hash_t *skip_revprops; |
| } opt_baton_t; |
| |
| /* Print dumpstream-formatted information about REVISION. |
| * Implements the `svn_ra_replay_revstart_callback_t' interface. |
| */ |
| static svn_error_t * |
| replay_revstart(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) |
| { |
| struct replay_baton *rb = replay_baton; |
| apr_hash_t *normal_props; |
| |
| /* Normalize and dump the revprops */ |
| SVN_ERR(svn_rdump__normalize_props(&normal_props, rev_props, pool)); |
| SVN_ERR(svn_repos__dump_revision_record(rb->stdout_stream, revision, NULL, |
| normal_props, |
| TRUE /*props_section_always*/, |
| pool)); |
| |
| SVN_ERR(svn_rdump__get_dump_editor(editor, edit_baton, revision, |
| rb->stdout_stream, rb->extra_ra_session, |
| NULL, check_cancel, NULL, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Print progress information about the dump of REVISION. |
| Implements the `svn_ra_replay_revfinish_callback_t' interface. */ |
| static svn_error_t * |
| replay_revend(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) |
| { |
| /* No resources left to free. */ |
| struct replay_baton *rb = replay_baton; |
| |
| SVN_ERR(editor->close_edit(edit_baton, pool)); |
| |
| if (! rb->quiet) |
| SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n", |
| revision)); |
| return SVN_NO_ERROR; |
| } |
| |
| #ifdef USE_EV2_IMPL |
| /* Print dumpstream-formatted information about REVISION. |
| * Implements the `svn_ra_replay_revstart_callback_t' interface. |
| */ |
| static svn_error_t * |
| replay_revstart_v2(svn_revnum_t revision, |
| void *replay_baton, |
| svn_editor_t **editor, |
| apr_hash_t *rev_props, |
| apr_pool_t *pool) |
| { |
| struct replay_baton *rb = replay_baton; |
| apr_hash_t *normal_props; |
| |
| /* Normalize and dump the revprops */ |
| SVN_ERR(svn_rdump__normalize_props(&normal_props, rev_props, pool)); |
| SVN_ERR(svn_repos__dump_revision_record(rb->stdout_stream, revision, |
| normal_props, |
| TRUE /*props_section_always*/, |
| pool)); |
| |
| SVN_ERR(svn_rdump__get_dump_editor_v2(editor, revision, |
| rb->stdout_stream, |
| rb->extra_ra_session, |
| NULL, check_cancel, NULL, pool, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Print progress information about the dump of REVISION. |
| Implements the `svn_ra_replay_revfinish_callback_t' interface. */ |
| static svn_error_t * |
| replay_revend_v2(svn_revnum_t revision, |
| void *replay_baton, |
| svn_editor_t *editor, |
| apr_hash_t *rev_props, |
| apr_pool_t *pool) |
| { |
| /* No resources left to free. */ |
| struct replay_baton *rb = replay_baton; |
| |
| SVN_ERR(svn_editor_complete(editor)); |
| |
| if (! rb->quiet) |
| SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n", |
| revision)); |
| return SVN_NO_ERROR; |
| } |
| #endif |
| |
| /* Initialize the RA layer, and set *CTX to a new client context baton |
| * allocated from POOL. Use CONFIG_DIR and pass USERNAME, PASSWORD, |
| * CONFIG_DIR and NO_AUTH_CACHE to initialize the authorization baton. |
| * CONFIG_OPTIONS (if not NULL) is a list of configuration overrides. |
| * REPOS_URL is used to fiddle with server-specific configuration |
| * options. |
| */ |
| static svn_error_t * |
| init_client_context(svn_client_ctx_t **ctx_p, |
| svn_boolean_t non_interactive, |
| const char *username, |
| const char *password, |
| const char *config_dir, |
| const char *repos_url, |
| svn_boolean_t no_auth_cache, |
| svn_boolean_t trust_unknown_ca, |
| svn_boolean_t trust_cn_mismatch, |
| svn_boolean_t trust_expired, |
| svn_boolean_t trust_not_yet_valid, |
| svn_boolean_t trust_other_failure, |
| apr_array_header_t *config_options, |
| apr_pool_t *pool) |
| { |
| svn_client_ctx_t *ctx = NULL; |
| svn_config_t *cfg_config, *cfg_servers; |
| |
| SVN_ERR(svn_ra_initialize(pool)); |
| |
| SVN_ERR(svn_config_ensure(config_dir, pool)); |
| SVN_ERR(svn_client_create_context2(&ctx, NULL, pool)); |
| |
| SVN_ERR(svn_config_get_config(&(ctx->config), config_dir, pool)); |
| |
| if (config_options) |
| SVN_ERR(svn_cmdline__apply_config_options(ctx->config, config_options, |
| "svnrdump: ", "--config-option")); |
| |
| cfg_config = svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG); |
| |
| /* ### FIXME: This is a hack to work around the fact that our dump |
| ### editor simply can't handle the way ra_serf violates the |
| ### editor v1 drive ordering requirements. |
| ### |
| ### We'll override both the global value and server-specific one |
| ### for the 'http-bulk-updates' and 'http-max-connections' |
| ### options in order to get ra_serf to try a bulk-update if the |
| ### server will allow it, or at least try to limit all its |
| ### auxiliary GETs/PROPFINDs to happening (well-ordered) on a |
| ### single server connection. |
| ### |
| ### See https://issues.apache.org/jira/browse/SVN-4116. |
| */ |
| cfg_servers = svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_SERVERS); |
| svn_config_set_bool(cfg_servers, SVN_CONFIG_SECTION_GLOBAL, |
| SVN_CONFIG_OPTION_HTTP_BULK_UPDATES, TRUE); |
| svn_config_set_int64(cfg_servers, SVN_CONFIG_SECTION_GLOBAL, |
| SVN_CONFIG_OPTION_HTTP_MAX_CONNECTIONS, 2); |
| if (cfg_servers) |
| { |
| apr_status_t status; |
| apr_uri_t parsed_url; |
| |
| status = apr_uri_parse(pool, repos_url, &parsed_url); |
| if (! status) |
| { |
| const char *server_group; |
| |
| server_group = svn_config_find_group(cfg_servers, parsed_url.hostname, |
| SVN_CONFIG_SECTION_GROUPS, pool); |
| if (server_group) |
| { |
| svn_config_set_bool(cfg_servers, server_group, |
| SVN_CONFIG_OPTION_HTTP_BULK_UPDATES, TRUE); |
| svn_config_set_int64(cfg_servers, server_group, |
| SVN_CONFIG_OPTION_HTTP_MAX_CONNECTIONS, 2); |
| } |
| } |
| } |
| |
| /* Set up our cancellation support. */ |
| ctx->cancel_func = check_cancel; |
| |
| /* Default authentication providers for non-interactive use */ |
| 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)); |
| *ctx_p = ctx; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Print a revision record header for REVISION to STDOUT_STREAM. Use |
| * SESSION to contact the repository for revision properties and |
| * such. |
| */ |
| static svn_error_t * |
| dump_revision_header(svn_ra_session_t *session, |
| svn_stream_t *stdout_stream, |
| svn_revnum_t revision, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *prophash; |
| |
| SVN_ERR(svn_ra_rev_proplist(session, revision, &prophash, pool)); |
| SVN_ERR(svn_repos__dump_revision_record(stdout_stream, revision, NULL, |
| prophash, |
| TRUE /*props_section_always*/, |
| pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| dump_initial_full_revision(svn_ra_session_t *session, |
| svn_ra_session_t *extra_ra_session, |
| svn_stream_t *stdout_stream, |
| svn_revnum_t revision, |
| svn_boolean_t quiet, |
| apr_pool_t *pool) |
| { |
| const svn_ra_reporter3_t *reporter; |
| void *report_baton; |
| const svn_delta_editor_t *dump_editor; |
| void *dump_baton; |
| const char *session_url, *source_relpath; |
| |
| /* Determine whether we're dumping the repository root URL or some |
| child thereof. If we're dumping a subtree of the repository |
| rather than the root, we have to jump through some hoops to make |
| our update-driven dump generation work the way a replay-driven |
| one would. |
| |
| See https://issues.apache.org/jira/browse/SVN-4101 |
| */ |
| SVN_ERR(svn_ra_get_session_url(session, &session_url, pool)); |
| SVN_ERR(svn_ra_get_path_relative_to_root(session, &source_relpath, |
| session_url, pool)); |
| |
| /* Start with a revision record header. */ |
| SVN_ERR(dump_revision_header(session, stdout_stream, revision, pool)); |
| |
| /* Then, we'll drive the dump editor with what would look like a |
| full checkout of the repository as it looked in START_REVISION. |
| We do this by manufacturing a basic 'report' to the update |
| reporter, telling it that we have nothing to start with. The |
| delta between nothing and everything-at-REV is, effectively, a |
| full dump of REV. */ |
| SVN_ERR(svn_rdump__get_dump_editor(&dump_editor, &dump_baton, revision, |
| stdout_stream, extra_ra_session, |
| source_relpath, check_cancel, NULL, pool)); |
| SVN_ERR(svn_ra_do_update3(session, &reporter, &report_baton, revision, |
| "", svn_depth_infinity, FALSE, FALSE, |
| dump_editor, dump_baton, pool, pool)); |
| SVN_ERR(reporter->set_path(report_baton, "", revision, |
| svn_depth_infinity, TRUE, NULL, pool)); |
| SVN_ERR(reporter->finish_report(report_baton, pool)); |
| |
| /* All finished with START_REVISION! */ |
| if (! quiet) |
| SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n", |
| revision)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Replay revisions START_REVISION thru END_REVISION (inclusive) of |
| * the repository URL at which SESSION is rooted, using callbacks |
| * which generate Subversion repository dumpstreams describing the |
| * changes made in those revisions. If QUIET is set, don't generate |
| * progress messages. |
| */ |
| static svn_error_t * |
| replay_revisions(svn_ra_session_t *session, |
| svn_ra_session_t *extra_ra_session, |
| svn_revnum_t start_revision, |
| svn_revnum_t end_revision, |
| svn_boolean_t quiet, |
| svn_boolean_t incremental, |
| const char *dumpfile, |
| apr_pool_t *pool) |
| { |
| struct replay_baton *replay_baton; |
| const char *uuid; |
| svn_stream_t *output_stream; |
| |
| if (dumpfile) |
| { |
| SVN_ERR(svn_stream_open_writable(&output_stream, dumpfile, pool, pool)); |
| } |
| else |
| { |
| SVN_ERR(svn_stream_for_stdout(&output_stream, pool)); |
| } |
| |
| replay_baton = apr_pcalloc(pool, sizeof(*replay_baton)); |
| replay_baton->stdout_stream = output_stream; |
| replay_baton->extra_ra_session = extra_ra_session; |
| replay_baton->quiet = quiet; |
| |
| /* Write the magic header and UUID */ |
| SVN_ERR(svn_repos__dump_magic_header_record(output_stream, |
| SVN_REPOS_DUMPFILE_FORMAT_VERSION, |
| pool)); |
| SVN_ERR(svn_ra_get_uuid2(session, &uuid, pool)); |
| SVN_ERR(svn_repos__dump_uuid_header_record(output_stream, uuid, pool)); |
| |
| /* Fake revision 0 if necessary */ |
| if (start_revision == 0) |
| { |
| SVN_ERR(dump_revision_header(session, output_stream, |
| start_revision, pool)); |
| |
| /* Revision 0 has no tree changes, so we're done. */ |
| if (! quiet) |
| SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n", |
| start_revision)); |
| start_revision++; |
| |
| /* If our first revision is 0, we can treat this as an |
| incremental dump. */ |
| incremental = TRUE; |
| } |
| |
| /* If what remains to be dumped is not going to be dumped |
| incrementally, then dump the first revision in full. */ |
| if (!incremental) |
| { |
| SVN_ERR(dump_initial_full_revision(session, extra_ra_session, |
| output_stream, start_revision, |
| quiet, pool)); |
| start_revision++; |
| } |
| |
| /* If there are still revisions left to be dumped, do so. */ |
| if (start_revision <= end_revision) |
| { |
| #ifndef USE_EV2_IMPL |
| SVN_ERR(svn_ra_replay_range(session, start_revision, end_revision, |
| 0, TRUE, replay_revstart, replay_revend, |
| replay_baton, pool)); |
| #else |
| SVN_ERR(svn_ra__replay_range_ev2(session, start_revision, end_revision, |
| 0, TRUE, replay_revstart_v2, |
| replay_revend_v2, replay_baton, |
| NULL, NULL, NULL, NULL, pool)); |
| #endif |
| } |
| |
| SVN_ERR(svn_stream_close(output_stream)); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Read a dumpstream from stdin, and use it to feed a loader capable |
| * of transmitting that information to the repository located at URL |
| * (to which SESSION has been opened). AUX_SESSION is a second RA |
| * session opened to the same URL for performing auxiliary out-of-band |
| * operations. |
| */ |
| static svn_error_t * |
| load_revisions(svn_ra_session_t *session, |
| svn_ra_session_t *aux_session, |
| const char *dumpfile, |
| svn_boolean_t quiet, |
| apr_hash_t *skip_revprops, |
| apr_pool_t *pool) |
| { |
| svn_stream_t *output_stream; |
| |
| if (dumpfile) |
| { |
| SVN_ERR(svn_stream_open_readonly(&output_stream, dumpfile, pool, pool)); |
| } |
| else |
| { |
| SVN_ERR(svn_stream_for_stdin2(&output_stream, TRUE, pool)); |
| } |
| |
| SVN_ERR(svn_rdump__load_dumpstream(output_stream, session, aux_session, |
| quiet, skip_revprops, |
| check_cancel, NULL, pool)); |
| |
| SVN_ERR(svn_stream_close(output_stream)); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return a program name for this program, the basename of the path |
| * represented by PROGNAME if not NULL; use "svnrdump" otherwise. |
| */ |
| static const char * |
| ensure_appname(const char *progname, |
| apr_pool_t *pool) |
| { |
| if (!progname) |
| return "svnrdump"; |
| |
| return svn_dirent_basename(svn_dirent_internal_style(progname, pool), NULL); |
| } |
| |
| /* Print a simple usage string. */ |
| static svn_error_t * |
| usage(const char *progname, |
| apr_pool_t *pool) |
| { |
| return svn_cmdline_fprintf(stderr, pool, |
| _("Type '%s help' for usage.\n"), |
| ensure_appname(progname, pool)); |
| } |
| |
| /* Print information about the version of this program and dependent |
| * modules. |
| */ |
| static svn_error_t * |
| version(const char *progname, |
| svn_boolean_t quiet, |
| apr_pool_t *pool) |
| { |
| svn_stringbuf_t *version_footer = |
| svn_stringbuf_create(_("The following repository access (RA) modules " |
| "are available:\n\n"), |
| pool); |
| |
| SVN_ERR(svn_ra_print_modules(version_footer, pool)); |
| return svn_opt_print_help5(NULL, ensure_appname(progname, pool), |
| TRUE, quiet, FALSE, version_footer->data, |
| NULL, NULL, NULL, NULL, NULL, pool); |
| } |
| |
| |
| /* Handle the "dump" subcommand. Implements `svn_opt_subcommand_t'. */ |
| static svn_error_t * |
| dump_cmd(apr_getopt_t *os, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| opt_baton_t *opt_baton = baton; |
| svn_ra_session_t *extra_ra_session; |
| const char *repos_root; |
| |
| SVN_ERR(svn_client_open_ra_session2(&extra_ra_session, |
| opt_baton->url, NULL, |
| opt_baton->ctx, pool, pool)); |
| SVN_ERR(svn_ra_get_repos_root2(extra_ra_session, &repos_root, pool)); |
| SVN_ERR(svn_ra_reparent(extra_ra_session, repos_root, pool)); |
| |
| return replay_revisions(opt_baton->session, extra_ra_session, |
| opt_baton->start_revision.value.number, |
| opt_baton->end_revision.value.number, |
| opt_baton->quiet, opt_baton->incremental, |
| opt_baton->dumpfile, pool); |
| } |
| |
| /* Handle the "load" subcommand. Implements `svn_opt_subcommand_t'. */ |
| static svn_error_t * |
| load_cmd(apr_getopt_t *os, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| opt_baton_t *opt_baton = baton; |
| svn_ra_session_t *aux_session; |
| |
| SVN_ERR(svn_client_open_ra_session2(&aux_session, opt_baton->url, NULL, |
| opt_baton->ctx, pool, pool)); |
| return load_revisions(opt_baton->session, aux_session, |
| opt_baton->dumpfile, opt_baton->quiet, |
| opt_baton->skip_revprops, pool); |
| } |
| |
| /* Handle the "help" subcommand. Implements `svn_opt_subcommand_t'. */ |
| static svn_error_t * |
| help_cmd(apr_getopt_t *os, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| const char *header = |
| _("general usage: svnrdump SUBCOMMAND URL [-r LOWER[:UPPER]]\n" |
| "Subversion remote repository dump and load tool.\n" |
| "Type 'svnrdump help <subcommand>' for help on a specific subcommand.\n" |
| "Type 'svnrdump --version' to see the program version and RA modules.\n" |
| "\n" |
| "Available subcommands:\n"); |
| |
| return svn_opt_print_help5(os, "svnrdump", FALSE, FALSE, FALSE, NULL, |
| header, svnrdump__cmd_table, svnrdump__options, |
| NULL, NULL, pool); |
| } |
| |
| /* Examine the OPT_BATON's 'start_revision' and 'end_revision' |
| * members, making sure that they make sense (in general, and as |
| * applied to a repository whose current youngest revision is |
| * LATEST_REVISION). |
| */ |
| static svn_error_t * |
| validate_and_resolve_revisions(opt_baton_t *opt_baton, |
| svn_revnum_t latest_revision, |
| apr_pool_t *pool) |
| { |
| svn_revnum_t provided_start_rev = SVN_INVALID_REVNUM; |
| |
| /* Ensure that the start revision is something we can handle. We |
| want a number >= 0. If unspecified, make it a number (r0) -- |
| anything else is bogus. */ |
| if (opt_baton->start_revision.kind == svn_opt_revision_number) |
| { |
| provided_start_rev = opt_baton->start_revision.value.number; |
| } |
| else if (opt_baton->start_revision.kind == svn_opt_revision_head) |
| { |
| opt_baton->start_revision.kind = svn_opt_revision_number; |
| opt_baton->start_revision.value.number = latest_revision; |
| } |
| else if (opt_baton->start_revision.kind == svn_opt_revision_unspecified) |
| { |
| opt_baton->start_revision.kind = svn_opt_revision_number; |
| opt_baton->start_revision.value.number = 0; |
| } |
| |
| if (opt_baton->start_revision.kind != svn_opt_revision_number) |
| { |
| return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("Unsupported revision specifier used; use " |
| "only integer values or 'HEAD'")); |
| } |
| |
| if ((opt_baton->start_revision.value.number < 0) || |
| (opt_baton->start_revision.value.number > latest_revision)) |
| { |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("Revision '%ld' does not exist"), |
| opt_baton->start_revision.value.number); |
| } |
| |
| /* Ensure that the end revision is something we can handle. We want |
| a number <= the youngest, and > the start revision. If |
| unspecified, make it a number (start_revision + 1 if that was |
| specified, the youngest revision in the repository otherwise) -- |
| anything else is bogus. */ |
| if (opt_baton->end_revision.kind == svn_opt_revision_unspecified) |
| { |
| opt_baton->end_revision.kind = svn_opt_revision_number; |
| if (SVN_IS_VALID_REVNUM(provided_start_rev)) |
| opt_baton->end_revision.value.number = provided_start_rev; |
| else |
| opt_baton->end_revision.value.number = latest_revision; |
| } |
| else if (opt_baton->end_revision.kind == svn_opt_revision_head) |
| { |
| opt_baton->end_revision.kind = svn_opt_revision_number; |
| opt_baton->end_revision.value.number = latest_revision; |
| } |
| |
| if (opt_baton->end_revision.kind != svn_opt_revision_number) |
| { |
| return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("Unsupported revision specifier used; use " |
| "only integer values or 'HEAD'")); |
| } |
| |
| if ((opt_baton->end_revision.value.number < 0) || |
| (opt_baton->end_revision.value.number > latest_revision)) |
| { |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("Revision '%ld' does not exist"), |
| opt_baton->end_revision.value.number); |
| } |
| |
| /* Finally, make sure that the end revision is younger than the |
| start revision. We don't do "backwards" 'round here. */ |
| if (opt_baton->end_revision.value.number < |
| opt_baton->start_revision.value.number) |
| { |
| return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("LOWER revision cannot be greater than " |
| "UPPER revision; consider reversing your " |
| "revision range")); |
| } |
| 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) |
| { |
| svn_error_t *err = SVN_NO_ERROR; |
| const svn_opt_subcommand_desc3_t *subcommand = NULL; |
| opt_baton_t *opt_baton; |
| svn_revnum_t latest_revision = SVN_INVALID_REVNUM; |
| const char *config_dir = NULL; |
| const char *username = NULL; |
| const char *password = NULL; |
| svn_boolean_t no_auth_cache = FALSE; |
| 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 non_interactive = FALSE; |
| svn_boolean_t force_interactive = FALSE; |
| apr_array_header_t *config_options = NULL; |
| apr_getopt_t *os; |
| apr_array_header_t *received_opts; |
| int i; |
| svn_boolean_t read_pass_from_stdin = FALSE; |
| |
| opt_baton = apr_pcalloc(pool, sizeof(*opt_baton)); |
| opt_baton->start_revision.kind = svn_opt_revision_unspecified; |
| opt_baton->end_revision.kind = svn_opt_revision_unspecified; |
| opt_baton->url = NULL; |
| opt_baton->skip_revprops = apr_hash_make(pool); |
| opt_baton->dumpfile = NULL; |
| |
| SVN_ERR(svn_cmdline__getopt_init(&os, argc, argv, pool)); |
| |
| os->interleave = TRUE; /* Options and arguments can be interleaved */ |
| |
| /* Set up our cancellation support. */ |
| check_cancel = svn_cmdline__setup_cancellation_handler(); |
| |
| received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int)); |
| |
| while (1) |
| { |
| int opt; |
| const char *opt_arg; |
| apr_status_t status = apr_getopt_long(os, svnrdump__options, &opt, |
| &opt_arg); |
| |
| if (APR_STATUS_IS_EOF(status)) |
| break; |
| if (status != APR_SUCCESS) |
| { |
| SVN_ERR(usage(argv[0], pool)); |
| *exit_code = EXIT_FAILURE; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Stash the option code in an array before parsing it. */ |
| APR_ARRAY_PUSH(received_opts, int) = opt; |
| |
| switch(opt) |
| { |
| case 'r': |
| { |
| /* Make sure we've not seen -r already. */ |
| if (opt_baton->start_revision.kind != svn_opt_revision_unspecified) |
| { |
| return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("Multiple revision arguments " |
| "encountered; try '-r N:M' instead " |
| "of '-r N -r M'")); |
| } |
| /* Parse the -r argument. */ |
| if (svn_opt_parse_revision(&(opt_baton->start_revision), |
| &(opt_baton->end_revision), |
| opt_arg, pool) != 0) |
| { |
| const char *utf8_opt_arg; |
| SVN_ERR(svn_utf_cstring_to_utf8(&utf8_opt_arg, opt_arg, pool)); |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("Syntax error in revision " |
| "argument '%s'"), utf8_opt_arg); |
| } |
| } |
| break; |
| case 'q': |
| opt_baton->quiet = TRUE; |
| break; |
| case opt_config_dir: |
| config_dir = opt_arg; |
| break; |
| case opt_version: |
| opt_baton->version = TRUE; |
| break; |
| case 'h': |
| opt_baton->help = TRUE; |
| break; |
| case opt_auth_username: |
| SVN_ERR(svn_utf_cstring_to_utf8(&username, opt_arg, pool)); |
| break; |
| case opt_auth_password: |
| SVN_ERR(svn_utf_cstring_to_utf8(&password, opt_arg, pool)); |
| break; |
| case opt_auth_password_from_stdin: |
| read_pass_from_stdin = TRUE; |
| break; |
| case opt_auth_nocache: |
| no_auth_cache = TRUE; |
| break; |
| case opt_non_interactive: |
| non_interactive = TRUE; |
| break; |
| case opt_force_interactive: |
| force_interactive = TRUE; |
| break; |
| case opt_incremental: |
| opt_baton->incremental = TRUE; |
| break; |
| case opt_skip_revprop: |
| SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, opt_arg, pool)); |
| svn_hash_sets(opt_baton->skip_revprops, opt_arg, opt_arg); |
| break; |
| case opt_trust_server_cert: /* backward compat */ |
| trust_unknown_ca = TRUE; |
| break; |
| case opt_trust_server_cert_failures: |
| SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, opt_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 opt_config_option: |
| if (!config_options) |
| config_options = |
| apr_array_make(pool, 1, |
| sizeof(svn_cmdline__config_argument_t*)); |
| |
| SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, opt_arg, pool)); |
| SVN_ERR(svn_cmdline__parse_config_option(config_options, |
| opt_arg, |
| "svnrdump: ", |
| pool)); |
| break; |
| case 'F': |
| SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, opt_arg, pool)); |
| opt_baton->dumpfile = opt_arg; |
| break; |
| } |
| } |
| |
| /* The --non-interactive and --force-interactive options are mutually |
| * exclusive. */ |
| if (non_interactive && force_interactive) |
| { |
| return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("--non-interactive and --force-interactive " |
| "are mutually exclusive")); |
| } |
| |
| if (opt_baton->help) |
| { |
| subcommand = svn_opt_get_canonical_subcommand3(svnrdump__cmd_table, |
| "help"); |
| } |
| if (subcommand == NULL) |
| { |
| if (os->ind >= os->argc) |
| { |
| if (opt_baton->version) |
| { |
| /* Use the "help" subcommand to handle the "--version" option. */ |
| static const svn_opt_subcommand_desc3_t pseudo_cmd = |
| { "--version", help_cmd, {0}, {""}, |
| {opt_version, /* must accept its own option */ |
| 'q', /* --quiet */ |
| } }; |
| subcommand = &pseudo_cmd; |
| } |
| |
| else |
| { |
| SVN_ERR(help_cmd(NULL, NULL, pool)); |
| *exit_code = EXIT_FAILURE; |
| return SVN_NO_ERROR; |
| } |
| } |
| else |
| { |
| const char *first_arg; |
| |
| SVN_ERR(svn_utf_cstring_to_utf8(&first_arg, os->argv[os->ind++], |
| pool)); |
| subcommand = svn_opt_get_canonical_subcommand3(svnrdump__cmd_table, |
| first_arg); |
| |
| if (subcommand == NULL) |
| { |
| svn_error_clear( |
| svn_cmdline_fprintf(stderr, pool, |
| _("Unknown subcommand: '%s'\n"), |
| first_arg)); |
| SVN_ERR(help_cmd(NULL, NULL, pool)); |
| *exit_code = EXIT_FAILURE; |
| return SVN_NO_ERROR; |
| } |
| } |
| } |
| |
| /* Check that the subcommand wasn't passed any inappropriate options. */ |
| for (i = 0; i < received_opts->nelts; i++) |
| { |
| int opt_id = APR_ARRAY_IDX(received_opts, i, int); |
| |
| /* All commands implicitly accept --help, so just skip over this |
| when we see it. Note that we don't want to include this option |
| in their "accepted options" list because it would be awfully |
| redundant to display it in every commands' help text. */ |
| if (opt_id == 'h' || opt_id == '?') |
| continue; |
| |
| if (! svn_opt_subcommand_takes_option4(subcommand, opt_id, NULL)) |
| { |
| const char *optstr; |
| const apr_getopt_option_t *badopt = |
| svn_opt_get_option_from_code3(opt_id, svnrdump__options, |
| subcommand, pool); |
| svn_opt_format_option(&optstr, badopt, FALSE, pool); |
| if (subcommand->name[0] == '-') |
| SVN_ERR(help_cmd(NULL, NULL, pool)); |
| else |
| svn_error_clear(svn_cmdline_fprintf( |
| stderr, pool, |
| _("Subcommand '%s' doesn't accept option '%s'\n" |
| "Type 'svnrdump help %s' for usage.\n"), |
| subcommand->name, optstr, subcommand->name)); |
| *exit_code = EXIT_FAILURE; |
| return SVN_NO_ERROR; |
| } |
| } |
| |
| if (strcmp(subcommand->name, "--version") == 0) |
| { |
| SVN_ERR(version(argv[0], opt_baton->quiet, pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| if (strcmp(subcommand->name, "help") == 0) |
| { |
| SVN_ERR(help_cmd(os, opt_baton, pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| /* --trust-* can only be used with --non-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")); |
| } |
| |
| if (read_pass_from_stdin && !non_interactive) |
| { |
| return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("--password-from-stdin requires " |
| "--non-interactive")); |
| } |
| |
| if (strcmp(subcommand->name, "load") == 0) |
| { |
| if (read_pass_from_stdin && opt_baton->dumpfile == NULL) |
| { |
| /* error here, since load cannot process a password over stdin */ |
| return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("load subcommand with " |
| "--password-from-stdin requires -F")); |
| } |
| } |
| |
| /* Expect one more non-option argument: the repository URL. */ |
| if (os->ind != os->argc - 1) |
| { |
| SVN_ERR(usage(argv[0], pool)); |
| *exit_code = EXIT_FAILURE; |
| return SVN_NO_ERROR; |
| } |
| else |
| { |
| const char *repos_url; |
| |
| SVN_ERR(svn_utf_cstring_to_utf8(&repos_url, os->argv[os->ind], pool)); |
| if (! svn_path_is_url(repos_url)) |
| { |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, 0, |
| "Target '%s' is not a URL", |
| repos_url); |
| } |
| opt_baton->url = svn_uri_canonicalize(repos_url, pool); |
| } |
| |
| if (strcmp(subcommand->name, "load") == 0) |
| { |
| /* |
| * By default (no --*-interactive options given), the 'load' subcommand |
| * is interactive unless username and password were provided on the |
| * command line. This allows prompting for auth creds to work without |
| * requiring users to remember to use --force-interactive. |
| * See issue #3913, "svnrdump load is not working in interactive mode". |
| */ |
| if (!non_interactive && !force_interactive) |
| force_interactive = (username == NULL || password == NULL); |
| } |
| |
| /* Get password from stdin if necessary */ |
| if (read_pass_from_stdin) |
| { |
| SVN_ERR(svn_cmdline__stdin_readline(&password, pool, pool)); |
| } |
| |
| non_interactive = !svn_cmdline__be_interactive(non_interactive, |
| force_interactive); |
| |
| SVN_ERR(init_client_context(&(opt_baton->ctx), |
| non_interactive, |
| username, |
| password, |
| config_dir, |
| opt_baton->url, |
| no_auth_cache, |
| trust_unknown_ca, |
| trust_cn_mismatch, |
| trust_expired, |
| trust_not_yet_valid, |
| trust_other_failure, |
| config_options, |
| pool)); |
| |
| err = svn_client_open_ra_session2(&(opt_baton->session), |
| opt_baton->url, NULL, |
| opt_baton->ctx, pool, pool); |
| |
| /* Have sane opt_baton->start_revision and end_revision defaults if |
| unspecified. */ |
| if (!err) |
| err = svn_ra_get_latest_revnum(opt_baton->session, &latest_revision, pool); |
| |
| /* Make sure any provided revisions make sense. */ |
| if (!err) |
| err = validate_and_resolve_revisions(opt_baton, latest_revision, pool); |
| |
| /* Dispatch the subcommand */ |
| if (!err) |
| err = (*subcommand->cmd_func)(os, opt_baton, pool); |
| |
| if (err && err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive) |
| { |
| return svn_error_quick_wrap(err, |
| _("Authentication failed and interactive" |
| " prompting is disabled; see the" |
| " --force-interactive option")); |
| } |
| else if (err) |
| return err; |
| else |
| 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("svnrdump", 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)); |
| |
| 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; |
| svn_cmdline_handle_exit_error(err, NULL, "svnrdump: "); |
| } |
| |
| svn_pool_destroy(pool); |
| |
| svn_cmdline__cancellation_exit(); |
| |
| return exit_code; |
| } |