diff options
Diffstat (limited to 'subversion/svnsync/svnsync.c')
-rw-r--r-- | subversion/svnsync/svnsync.c | 2305 |
1 files changed, 2305 insertions, 0 deletions
diff --git a/subversion/svnsync/svnsync.c b/subversion/svnsync/svnsync.c new file mode 100644 index 0000000..0bdc976 --- /dev/null +++ b/subversion/svnsync/svnsync.c @@ -0,0 +1,2305 @@ +/* + * ==================================================================== + * 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 "svn_hash.h" +#include "svn_cmdline.h" +#include "svn_config.h" +#include "svn_pools.h" +#include "svn_delta.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_props.h" +#include "svn_auth.h" +#include "svn_opt.h" +#include "svn_ra.h" +#include "svn_utf.h" +#include "svn_subst.h" +#include "svn_string.h" +#include "svn_version.h" + +#include "private/svn_opt_private.h" +#include "private/svn_ra_private.h" +#include "private/svn_cmdline_private.h" + +#include "sync.h" + +#include "svn_private_config.h" + +#include <apr_signal.h> +#include <apr_uuid.h> + +static svn_opt_subcommand_t initialize_cmd, + synchronize_cmd, + copy_revprops_cmd, + info_cmd, + help_cmd; + +enum svnsync__opt { + svnsync_opt_non_interactive = SVN_OPT_FIRST_LONGOPT_ID, + svnsync_opt_force_interactive, + svnsync_opt_no_auth_cache, + svnsync_opt_auth_username, + svnsync_opt_auth_password, + svnsync_opt_source_username, + svnsync_opt_source_password, + svnsync_opt_sync_username, + svnsync_opt_sync_password, + svnsync_opt_config_dir, + svnsync_opt_config_options, + svnsync_opt_source_prop_encoding, + svnsync_opt_disable_locking, + svnsync_opt_version, + svnsync_opt_trust_server_cert, + svnsync_opt_allow_non_empty, + svnsync_opt_steal_lock +}; + +#define SVNSYNC_OPTS_DEFAULT svnsync_opt_non_interactive, \ + svnsync_opt_force_interactive, \ + svnsync_opt_no_auth_cache, \ + svnsync_opt_auth_username, \ + svnsync_opt_auth_password, \ + svnsync_opt_trust_server_cert, \ + svnsync_opt_source_username, \ + svnsync_opt_source_password, \ + svnsync_opt_sync_username, \ + svnsync_opt_sync_password, \ + svnsync_opt_config_dir, \ + svnsync_opt_config_options + +static const svn_opt_subcommand_desc2_t svnsync_cmd_table[] = + { + { "initialize", initialize_cmd, { "init" }, + N_("usage: svnsync initialize DEST_URL SOURCE_URL\n" + "\n" + "Initialize a destination repository for synchronization from\n" + "another repository.\n" + "\n" + "If the source URL is not the root of a repository, only the\n" + "specified part of the repository will be synchronized.\n" + "\n" + "The destination URL must point to the root of a repository which\n" + "has been configured to allow revision property changes. In\n" + "the general case, the destination repository must contain no\n" + "committed revisions. Use --allow-non-empty to override this\n" + "restriction, which will cause svnsync to assume that any revisions\n" + "already present in the destination repository perfectly mirror\n" + "their counterparts in the source repository. (This is useful\n" + "when initializing a copy of a repository as a mirror of that same\n" + "repository, for example.)\n" + "\n" + "You should not commit to, or make revision property changes in,\n" + "the destination repository by any method other than 'svnsync'.\n" + "In other words, the destination repository should be a read-only\n" + "mirror of the source repository.\n"), + { SVNSYNC_OPTS_DEFAULT, svnsync_opt_source_prop_encoding, 'q', + svnsync_opt_allow_non_empty, svnsync_opt_disable_locking, + svnsync_opt_steal_lock } }, + { "synchronize", synchronize_cmd, { "sync" }, + N_("usage: svnsync synchronize DEST_URL [SOURCE_URL]\n" + "\n" + "Transfer all pending revisions to the destination from the source\n" + "with which it was initialized.\n" + "\n" + "If SOURCE_URL is provided, use that as the source repository URL,\n" + "ignoring what is recorded in the destination repository as the\n" + "source URL. Specifying SOURCE_URL is recommended in particular\n" + "if untrusted users/administrators may have write access to the\n" + "DEST_URL repository.\n"), + { SVNSYNC_OPTS_DEFAULT, svnsync_opt_source_prop_encoding, 'q', + svnsync_opt_disable_locking, svnsync_opt_steal_lock } }, + { "copy-revprops", copy_revprops_cmd, { 0 }, + N_("usage:\n" + "\n" + " 1. svnsync copy-revprops DEST_URL [SOURCE_URL]\n" + " 2. svnsync copy-revprops DEST_URL REV[:REV2]\n" + "\n" + "Copy the revision properties in a given range of revisions to the\n" + "destination from the source with which it was initialized. If the\n" + "revision range is not specified, it defaults to all revisions in\n" + "the DEST_URL repository. Note also that the 'HEAD' revision is the\n" + "latest in DEST_URL, not necessarily the latest in SOURCE_URL.\n" + "\n" + "If SOURCE_URL is provided, use that as the source repository URL,\n" + "ignoring what is recorded in the destination repository as the\n" + "source URL. Specifying SOURCE_URL is recommended in particular\n" + "if untrusted users/administrators may have write access to the\n" + "DEST_URL repository.\n" + "\n" + "Form 2 is deprecated syntax, equivalent to specifying \"-rREV[:REV2]\".\n"), + { SVNSYNC_OPTS_DEFAULT, svnsync_opt_source_prop_encoding, 'q', 'r', + svnsync_opt_disable_locking, svnsync_opt_steal_lock } }, + { "info", info_cmd, { 0 }, + N_("usage: svnsync info DEST_URL\n" + "\n" + "Print information about the synchronization destination repository\n" + "located at DEST_URL.\n"), + { SVNSYNC_OPTS_DEFAULT } }, + { "help", help_cmd, { "?", "h" }, + N_("usage: svnsync help [SUBCOMMAND...]\n" + "\n" + "Describe the usage of this program or its subcommands.\n"), + { 0 } }, + { NULL, NULL, { 0 }, NULL, { 0 } } + }; + +static const apr_getopt_option_t svnsync_options[] = + { + {"quiet", 'q', 0, + N_("print as little as possible") }, + {"revision", 'r', 1, + N_("operate on revision ARG (or range ARG1:ARG2)\n" + " " + "A revision argument can be one of:\n" + " " + " NUMBER revision number\n" + " " + " 'HEAD' latest in repository") }, + {"allow-non-empty", svnsync_opt_allow_non_empty, 0, + N_("allow a non-empty destination repository") }, + {"non-interactive", svnsync_opt_non_interactive, 0, + N_("do no interactive prompting (default is to prompt\n" + " " + "only if standard input is a terminal device)")}, + {"force-interactive", svnsync_opt_force_interactive, 0, + N_("do interactive prompting even if standard input\n" + " " + "is not a terminal device")}, + {"no-auth-cache", svnsync_opt_no_auth_cache, 0, + N_("do not cache authentication tokens") }, + {"username", svnsync_opt_auth_username, 1, + N_("specify a username ARG (deprecated;\n" + " " + "see --source-username and --sync-username)") }, + {"password", svnsync_opt_auth_password, 1, + N_("specify a password ARG (deprecated;\n" + " " + "see --source-password and --sync-password)") }, + {"trust-server-cert", svnsync_opt_trust_server_cert, 0, + N_("accept SSL server certificates from unknown\n" + " " + "certificate authorities without prompting (but only\n" + " " + "with '--non-interactive')") }, + {"source-username", svnsync_opt_source_username, 1, + N_("connect to source repository with username ARG") }, + {"source-password", svnsync_opt_source_password, 1, + N_("connect to source repository with password ARG") }, + {"sync-username", svnsync_opt_sync_username, 1, + N_("connect to sync repository with username ARG") }, + {"sync-password", svnsync_opt_sync_password, 1, + N_("connect to sync repository with password ARG") }, + {"config-dir", svnsync_opt_config_dir, 1, + N_("read user configuration files from directory ARG")}, + {"config-option", svnsync_opt_config_options, 1, + N_("set user configuration option in the format:\n" + " " + " FILE:SECTION:OPTION=[VALUE]\n" + " " + "For example:\n" + " " + " servers:global:http-library=serf")}, + {"source-prop-encoding", svnsync_opt_source_prop_encoding, 1, + N_("convert translatable properties from encoding ARG\n" + " " + "to UTF-8. If not specified, then properties are\n" + " " + "presumed to be encoded in UTF-8.")}, + {"disable-locking", svnsync_opt_disable_locking, 0, + N_("Disable built-in locking. Use of this option can\n" + " " + "corrupt the mirror unless you ensure that no other\n" + " " + "instance of svnsync is running concurrently.")}, + {"steal-lock", svnsync_opt_steal_lock, 0, + N_("Steal locks as necessary. Use, with caution,\n" + " " + "if your mirror repository contains stale locks\n" + " " + "and is not being concurrently accessed by another\n" + " " + "svnsync instance.")}, + {"version", svnsync_opt_version, 0, + N_("show program version information")}, + {"help", 'h', 0, + N_("show help on a subcommand")}, + {NULL, '?', 0, + N_("show help on a subcommand")}, + { 0, 0, 0, 0 } + }; + +typedef struct opt_baton_t { + svn_boolean_t non_interactive; + svn_boolean_t trust_server_cert; + svn_boolean_t no_auth_cache; + svn_auth_baton_t *source_auth_baton; + svn_auth_baton_t *sync_auth_baton; + const char *source_username; + const char *source_password; + const char *sync_username; + const char *sync_password; + const char *config_dir; + apr_hash_t *config; + const char *source_prop_encoding; + svn_boolean_t disable_locking; + svn_boolean_t steal_lock; + svn_boolean_t quiet; + svn_boolean_t allow_non_empty; + svn_boolean_t version; + svn_boolean_t help; + svn_opt_revision_t start_rev; + svn_opt_revision_t end_rev; +} opt_baton_t; + + + + +/*** Helper functions ***/ + + +/* Global record of whether the user has requested cancellation. */ +static volatile sig_atomic_t cancelled = FALSE; + + +/* Callback function for apr_signal(). */ +static void +signal_handler(int signum) +{ + apr_signal(signum, SIG_IGN); + cancelled = TRUE; +} + + +/* Cancellation callback function. */ +static svn_error_t * +check_cancel(void *baton) +{ + if (cancelled) + return svn_error_create(SVN_ERR_CANCELLED, NULL, _("Caught signal")); + else + return SVN_NO_ERROR; +} + + +/* Check that the version of libraries in use match what we expect. */ +static svn_error_t * +check_lib_versions(void) +{ + static const svn_version_checklist_t checklist[] = + { + { "svn_subr", svn_subr_version }, + { "svn_delta", svn_delta_version }, + { "svn_ra", svn_ra_version }, + { NULL, NULL } + }; + SVN_VERSION_DEFINE(my_version); + + return svn_ver_check_list(&my_version, checklist); +} + + +/* Implements `svn_ra__lock_retry_func_t'. */ +static svn_error_t * +lock_retry_func(void *baton, + const svn_string_t *reposlocktoken, + apr_pool_t *pool) +{ + return svn_cmdline_printf(pool, + _("Failed to get lock on destination " + "repos, currently held by '%s'\n"), + reposlocktoken->data); +} + +/* Acquire a lock (of sorts) on the repository associated with the + * given RA SESSION. This lock is just a revprop change attempt in a + * time-delay loop. This function is duplicated by svnrdump in + * svnrdump/load_editor.c + */ +static svn_error_t * +get_lock(const svn_string_t **lock_string_p, + svn_ra_session_t *session, + svn_boolean_t steal_lock, + apr_pool_t *pool) +{ + svn_error_t *err; + svn_boolean_t be_atomic; + const svn_string_t *stolen_lock; + + SVN_ERR(svn_ra_has_capability(session, &be_atomic, + SVN_RA_CAPABILITY_ATOMIC_REVPROPS, + pool)); + if (! be_atomic) + { + /* Pre-1.7 server. Can't lock without a race condition. + See issue #3546. + */ + err = svn_error_create( + SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("Target server does not support atomic revision property " + "edits; consider upgrading it to 1.7 or using an external " + "locking program")); + svn_handle_warning2(stderr, err, "svnsync: "); + svn_error_clear(err); + } + + err = svn_ra__get_operational_lock(lock_string_p, &stolen_lock, session, + SVNSYNC_PROP_LOCK, steal_lock, + 10 /* retries */, lock_retry_func, NULL, + check_cancel, NULL, pool); + if (!err && stolen_lock) + { + return svn_cmdline_printf(pool, + _("Stole lock previously held by '%s'\n"), + stolen_lock->data); + } + return err; +} + + +/* Baton for the various subcommands to share. */ +typedef struct subcommand_baton_t { + /* common to all subcommands */ + apr_hash_t *config; + svn_ra_callbacks2_t source_callbacks; + svn_ra_callbacks2_t sync_callbacks; + svn_boolean_t quiet; + svn_boolean_t allow_non_empty; + const char *to_url; + + /* initialize, synchronize, and copy-revprops only */ + const char *source_prop_encoding; + + /* initialize only */ + const char *from_url; + + /* synchronize only */ + svn_revnum_t committed_rev; + + /* copy-revprops only */ + svn_revnum_t start_rev; + svn_revnum_t end_rev; + +} subcommand_baton_t; + +typedef svn_error_t *(*with_locked_func_t)(svn_ra_session_t *session, + subcommand_baton_t *baton, + apr_pool_t *pool); + + +/* Lock the repository associated with RA SESSION, then execute the + * given FUNC/BATON pair while holding the lock. Finally, drop the + * lock once it finishes. + */ +static svn_error_t * +with_locked(svn_ra_session_t *session, + with_locked_func_t func, + subcommand_baton_t *baton, + svn_boolean_t steal_lock, + apr_pool_t *pool) +{ + const svn_string_t *lock_string; + svn_error_t *err; + + SVN_ERR(get_lock(&lock_string, session, steal_lock, pool)); + + err = func(session, baton, pool); + return svn_error_compose_create(err, + svn_ra__release_operational_lock(session, SVNSYNC_PROP_LOCK, + lock_string, pool)); +} + + +/* Callback function for the RA session's open_tmp_file() + * requirements. + */ +static svn_error_t * +open_tmp_file(apr_file_t **fp, void *callback_baton, apr_pool_t *pool) +{ + return svn_io_open_unique_file3(fp, NULL, NULL, + svn_io_file_del_on_pool_cleanup, + pool, pool); +} + + +/* Return SVN_NO_ERROR iff URL identifies the root directory of the + * repository associated with RA session SESS. + */ +static svn_error_t * +check_if_session_is_at_repos_root(svn_ra_session_t *sess, + const char *url, + apr_pool_t *pool) +{ + const char *sess_root; + + SVN_ERR(svn_ra_get_repos_root2(sess, &sess_root, pool)); + + if (strcmp(url, sess_root) == 0) + return SVN_NO_ERROR; + else + return svn_error_createf + (APR_EINVAL, NULL, + _("Session is rooted at '%s' but the repos root is '%s'"), + url, sess_root); +} + + +/* Remove the properties in TARGET_PROPS but not in SOURCE_PROPS from + * revision REV of the repository associated with RA session SESSION. + * + * For REV zero, don't remove properties with the "svn:sync-" prefix. + * + * All allocations will be done in a subpool of POOL. + */ +static svn_error_t * +remove_props_not_in_source(svn_ra_session_t *session, + svn_revnum_t rev, + apr_hash_t *source_props, + apr_hash_t *target_props, + apr_pool_t *pool) +{ + apr_pool_t *subpool = svn_pool_create(pool); + apr_hash_index_t *hi; + + for (hi = apr_hash_first(pool, target_props); + hi; + hi = apr_hash_next(hi)) + { + const char *propname = svn__apr_hash_index_key(hi); + + svn_pool_clear(subpool); + + if (rev == 0 && !strncmp(propname, SVNSYNC_PROP_PREFIX, + sizeof(SVNSYNC_PROP_PREFIX) - 1)) + continue; + + /* Delete property if the name can't be found in SOURCE_PROPS. */ + if (! svn_hash_gets(source_props, propname)) + SVN_ERR(svn_ra_change_rev_prop2(session, rev, propname, NULL, + NULL, subpool)); + } + + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + +/* Filter callback function. + * Takes a property name KEY, and is expected to return TRUE if the property + * should be filtered out (ie. not be copied to the target list), or FALSE if + * not. + */ +typedef svn_boolean_t (*filter_func_t)(const char *key); + +/* Make a new set of properties, by copying those properties in PROPS for which + * the filter FILTER returns FALSE. + * + * The number of properties not copied will be stored in FILTERED_COUNT. + * + * The returned set of properties is allocated from POOL. + */ +static apr_hash_t * +filter_props(int *filtered_count, apr_hash_t *props, + filter_func_t filter, + apr_pool_t *pool) +{ + apr_hash_index_t *hi; + apr_hash_t *filtered = apr_hash_make(pool); + *filtered_count = 0; + + for (hi = apr_hash_first(pool, props); hi ; hi = apr_hash_next(hi)) + { + const char *propname = svn__apr_hash_index_key(hi); + void *propval = svn__apr_hash_index_val(hi); + + /* Copy all properties: + - not matching the exclude pattern if provided OR + - matching the include pattern if provided */ + if (!filter || !filter(propname)) + { + svn_hash_sets(filtered, propname, propval); + } + else + { + *filtered_count += 1; + } + } + + return filtered; +} + + +/* Write the set of revision properties REV_PROPS to revision REV to the + * repository associated with RA session SESSION. + * Omit any properties whose names are in the svnsync property name space, + * and set *FILTERED_COUNT to the number of properties thus omitted. + * REV_PROPS is a hash mapping (char *)propname to (svn_string_t *)propval. + * + * All allocations will be done in a subpool of POOL. + */ +static svn_error_t * +write_revprops(int *filtered_count, + svn_ra_session_t *session, + svn_revnum_t rev, + apr_hash_t *rev_props, + apr_pool_t *pool) +{ + apr_pool_t *subpool = svn_pool_create(pool); + apr_hash_index_t *hi; + + *filtered_count = 0; + + for (hi = apr_hash_first(pool, rev_props); hi; hi = apr_hash_next(hi)) + { + const char *propname = svn__apr_hash_index_key(hi); + const svn_string_t *propval = svn__apr_hash_index_val(hi); + + svn_pool_clear(subpool); + + if (strncmp(propname, SVNSYNC_PROP_PREFIX, + sizeof(SVNSYNC_PROP_PREFIX) - 1) != 0) + { + SVN_ERR(svn_ra_change_rev_prop2(session, rev, propname, NULL, + propval, subpool)); + } + else + { + *filtered_count += 1; + } + } + + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + + +static svn_error_t * +log_properties_copied(svn_boolean_t syncprops_found, + svn_revnum_t rev, + apr_pool_t *pool) +{ + if (syncprops_found) + SVN_ERR(svn_cmdline_printf(pool, + _("Copied properties for revision %ld " + "(%s* properties skipped).\n"), + rev, SVNSYNC_PROP_PREFIX)); + else + SVN_ERR(svn_cmdline_printf(pool, + _("Copied properties for revision %ld.\n"), + rev)); + + return SVN_NO_ERROR; +} + +/* Print a notification that NORMALIZED_REV_PROPS_COUNT rev-props and + * NORMALIZED_NODE_PROPS_COUNT node-props were normalized to LF line + * endings, if either of those numbers is non-zero. */ +static svn_error_t * +log_properties_normalized(int normalized_rev_props_count, + int normalized_node_props_count, + apr_pool_t *pool) +{ + if (normalized_rev_props_count > 0 || normalized_node_props_count > 0) + SVN_ERR(svn_cmdline_printf(pool, + _("NOTE: Normalized %s* properties " + "to LF line endings (%d rev-props, " + "%d node-props).\n"), + SVN_PROP_PREFIX, + normalized_rev_props_count, + normalized_node_props_count)); + return SVN_NO_ERROR; +} + + +/* Copy all the revision properties, except for those that have the + * "svn:sync-" prefix, from revision REV of the repository associated + * with RA session FROM_SESSION, to the repository associated with RA + * session TO_SESSION. + * + * If SYNC is TRUE, then properties on the destination revision that + * do not exist on the source revision will be removed. + * + * If QUIET is FALSE, then log_properties_copied() is called to log that + * properties were copied for revision REV. + * + * Make sure the values of svn:* revision properties use only LF (\n) + * line ending style, correcting their values as necessary. The number + * of properties that were normalized is returned in *NORMALIZED_COUNT. + */ +static svn_error_t * +copy_revprops(svn_ra_session_t *from_session, + svn_ra_session_t *to_session, + svn_revnum_t rev, + svn_boolean_t sync, + svn_boolean_t quiet, + const char *source_prop_encoding, + int *normalized_count, + apr_pool_t *pool) +{ + apr_pool_t *subpool = svn_pool_create(pool); + apr_hash_t *existing_props, *rev_props; + int filtered_count = 0; + + /* Get the list of revision properties on REV of TARGET. We're only interested + in the property names, but we'll get the values 'for free'. */ + if (sync) + SVN_ERR(svn_ra_rev_proplist(to_session, rev, &existing_props, subpool)); + else + existing_props = NULL; + + /* Get the list of revision properties on REV of SOURCE. */ + SVN_ERR(svn_ra_rev_proplist(from_session, rev, &rev_props, subpool)); + + /* If necessary, normalize encoding and line ending style and return the count + of EOL-normalized properties in int *NORMALIZED_COUNT. */ + SVN_ERR(svnsync_normalize_revprops(rev_props, normalized_count, + source_prop_encoding, pool)); + + /* Copy all but the svn:svnsync properties. */ + SVN_ERR(write_revprops(&filtered_count, to_session, rev, rev_props, pool)); + + /* Delete those properties that were in TARGET but not in SOURCE */ + if (sync) + SVN_ERR(remove_props_not_in_source(to_session, rev, + rev_props, existing_props, pool)); + + if (! quiet) + SVN_ERR(log_properties_copied(filtered_count > 0, rev, pool)); + + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + + +/* Return a subcommand baton allocated from POOL and populated with + data from the provided parameters, which include the global + OPT_BATON options structure and a handful of other options. Not + all parameters are used in all subcommands -- see + subcommand_baton_t's definition for details. */ +static subcommand_baton_t * +make_subcommand_baton(opt_baton_t *opt_baton, + const char *to_url, + const char *from_url, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + apr_pool_t *pool) +{ + subcommand_baton_t *b = apr_pcalloc(pool, sizeof(*b)); + b->config = opt_baton->config; + b->source_callbacks.open_tmp_file = open_tmp_file; + b->source_callbacks.auth_baton = opt_baton->source_auth_baton; + b->sync_callbacks.open_tmp_file = open_tmp_file; + b->sync_callbacks.auth_baton = opt_baton->sync_auth_baton; + b->quiet = opt_baton->quiet; + b->allow_non_empty = opt_baton->allow_non_empty; + b->to_url = to_url; + b->source_prop_encoding = opt_baton->source_prop_encoding; + b->from_url = from_url; + b->start_rev = start_rev; + b->end_rev = end_rev; + return b; +} + +static svn_error_t * +open_target_session(svn_ra_session_t **to_session_p, + subcommand_baton_t *baton, + apr_pool_t *pool); + + +/*** `svnsync init' ***/ + +/* Initialize the repository associated with RA session TO_SESSION, + * using information found in BATON, while the repository is + * locked. Implements `with_locked_func_t' interface. + */ +static svn_error_t * +do_initialize(svn_ra_session_t *to_session, + subcommand_baton_t *baton, + apr_pool_t *pool) +{ + svn_ra_session_t *from_session; + svn_string_t *from_url; + svn_revnum_t latest, from_latest; + const char *uuid, *root_url; + int normalized_rev_props_count; + + /* First, sanity check to see that we're copying into a brand new + repos. If we aren't, and we aren't being asked to forcibly + complete this initialization, that's a bad news. */ + SVN_ERR(svn_ra_get_latest_revnum(to_session, &latest, pool)); + if ((latest != 0) && (! baton->allow_non_empty)) + return svn_error_create + (APR_EINVAL, NULL, + _("Destination repository already contains revision history; consider " + "using --allow-non-empty if the repository's revisions are known " + "to mirror their respective revisions in the source repository")); + + SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL, + &from_url, pool)); + if (from_url && (! baton->allow_non_empty)) + return svn_error_createf + (APR_EINVAL, NULL, + _("Destination repository is already synchronizing from '%s'"), + from_url->data); + + /* Now fill in our bookkeeping info in the dest repository. */ + + SVN_ERR(svn_ra_open4(&from_session, NULL, baton->from_url, NULL, + &(baton->source_callbacks), baton, + baton->config, pool)); + SVN_ERR(svn_ra_get_repos_root2(from_session, &root_url, pool)); + + /* If we're doing a partial replay, we have to check first if the server + supports this. */ + if (strcmp(root_url, baton->from_url) != 0) + { + svn_boolean_t server_supports_partial_replay; + svn_error_t *err = svn_ra_has_capability(from_session, + &server_supports_partial_replay, + SVN_RA_CAPABILITY_PARTIAL_REPLAY, + pool); + if (err && err->apr_err != SVN_ERR_UNKNOWN_CAPABILITY) + return svn_error_trace(err); + + if (err || !server_supports_partial_replay) + return svn_error_create(SVN_ERR_RA_PARTIAL_REPLAY_NOT_SUPPORTED, err, + NULL); + } + + /* If we're initializing a non-empty destination, we'll make sure + that it at least doesn't have more revisions than the source. */ + if (latest != 0) + { + SVN_ERR(svn_ra_get_latest_revnum(from_session, &from_latest, pool)); + if (from_latest < latest) + return svn_error_create + (APR_EINVAL, NULL, + _("Destination repository has more revisions than source " + "repository")); + } + + SVN_ERR(svn_ra_change_rev_prop2(to_session, 0, SVNSYNC_PROP_FROM_URL, NULL, + svn_string_create(baton->from_url, pool), + pool)); + + SVN_ERR(svn_ra_get_uuid2(from_session, &uuid, pool)); + SVN_ERR(svn_ra_change_rev_prop2(to_session, 0, SVNSYNC_PROP_FROM_UUID, NULL, + svn_string_create(uuid, pool), pool)); + + SVN_ERR(svn_ra_change_rev_prop2(to_session, 0, SVNSYNC_PROP_LAST_MERGED_REV, + NULL, svn_string_createf(pool, "%ld", latest), + pool)); + + /* Copy all non-svnsync revprops from the LATEST rev in the source + repository into the destination, notifying about normalized + props, if any. When LATEST is 0, this serves the practical + purpose of initializing data that would otherwise be overlooked + by the sync process (which is going to begin with r1). When + LATEST is not 0, this really serves merely aesthetic and + informational purposes, keeping the output of this command + consistent while allowing folks to see what the latest revision is. */ + SVN_ERR(copy_revprops(from_session, to_session, latest, FALSE, baton->quiet, + baton->source_prop_encoding, &normalized_rev_props_count, + pool)); + + SVN_ERR(log_properties_normalized(normalized_rev_props_count, 0, pool)); + + /* TODO: It would be nice if we could set the dest repos UUID to be + equal to the UUID of the source repos, at least optionally. That + way people could check out/log/diff using a local fast mirror, + but switch --relocate to the actual final repository in order to + make changes... But at this time, the RA layer doesn't have a + way to set a UUID. */ + + return SVN_NO_ERROR; +} + + +/* SUBCOMMAND: init */ +static svn_error_t * +initialize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool) +{ + const char *to_url, *from_url; + svn_ra_session_t *to_session; + opt_baton_t *opt_baton = b; + apr_array_header_t *targets; + subcommand_baton_t *baton; + + SVN_ERR(svn_opt__args_to_target_array(&targets, os, + apr_array_make(pool, 0, + sizeof(const char *)), + pool)); + if (targets->nelts < 2) + return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL); + if (targets->nelts > 2) + return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL); + + to_url = APR_ARRAY_IDX(targets, 0, const char *); + from_url = APR_ARRAY_IDX(targets, 1, const char *); + + if (! svn_path_is_url(to_url)) + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Path '%s' is not a URL"), to_url); + if (! svn_path_is_url(from_url)) + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Path '%s' is not a URL"), from_url); + + baton = make_subcommand_baton(opt_baton, to_url, from_url, 0, 0, pool); + SVN_ERR(open_target_session(&to_session, baton, pool)); + if (opt_baton->disable_locking) + SVN_ERR(do_initialize(to_session, baton, pool)); + else + SVN_ERR(with_locked(to_session, do_initialize, baton, + opt_baton->steal_lock, pool)); + + return SVN_NO_ERROR; +} + + + +/*** `svnsync sync' ***/ + +/* Implements `svn_commit_callback2_t' interface. */ +static svn_error_t * +commit_callback(const svn_commit_info_t *commit_info, + void *baton, + apr_pool_t *pool) +{ + subcommand_baton_t *sb = baton; + + if (! sb->quiet) + { + SVN_ERR(svn_cmdline_printf(pool, _("Committed revision %ld.\n"), + commit_info->revision)); + } + + sb->committed_rev = commit_info->revision; + + return SVN_NO_ERROR; +} + + +/* Set *FROM_SESSION to an RA session associated with the source + * repository of the synchronization. If FROM_URL is non-NULL, use it + * as the source repository URL; otherwise, determine the source + * repository URL by reading svn:sync- properties from the destination + * repository (associated with TO_SESSION). Set LAST_MERGED_REV to + * the value of the property which records the most recently + * synchronized revision. + * + * CALLBACKS is a vtable of RA callbacks to provide when creating + * *FROM_SESSION. CONFIG is a configuration hash. + */ +static svn_error_t * +open_source_session(svn_ra_session_t **from_session, + svn_string_t **last_merged_rev, + const char *from_url, + svn_ra_session_t *to_session, + svn_ra_callbacks2_t *callbacks, + apr_hash_t *config, + void *baton, + apr_pool_t *pool) +{ + apr_hash_t *props; + svn_string_t *from_url_str, *from_uuid_str; + + SVN_ERR(svn_ra_rev_proplist(to_session, 0, &props, pool)); + + from_url_str = svn_hash_gets(props, SVNSYNC_PROP_FROM_URL); + from_uuid_str = svn_hash_gets(props, SVNSYNC_PROP_FROM_UUID); + *last_merged_rev = svn_hash_gets(props, SVNSYNC_PROP_LAST_MERGED_REV); + + if (! from_url_str || ! from_uuid_str || ! *last_merged_rev) + return svn_error_create + (APR_EINVAL, NULL, + _("Destination repository has not been initialized")); + + /* ### TODO: Should we validate that FROM_URL_STR->data matches any + provided FROM_URL here? */ + if (! from_url) + from_url = from_url_str->data; + + /* Open the session to copy the revision data. */ + SVN_ERR(svn_ra_open4(from_session, NULL, from_url, from_uuid_str->data, + callbacks, baton, config, pool)); + + return SVN_NO_ERROR; +} + +/* Set *TARGET_SESSION_P to an RA session associated with the target + * repository of the synchronization. + */ +static svn_error_t * +open_target_session(svn_ra_session_t **target_session_p, + subcommand_baton_t *baton, + apr_pool_t *pool) +{ + svn_ra_session_t *target_session; + SVN_ERR(svn_ra_open4(&target_session, NULL, baton->to_url, NULL, + &(baton->sync_callbacks), baton, baton->config, pool)); + SVN_ERR(check_if_session_is_at_repos_root(target_session, baton->to_url, pool)); + + *target_session_p = target_session; + return SVN_NO_ERROR; +} + +/* Replay baton, used during synchronization. */ +typedef struct replay_baton_t { + svn_ra_session_t *from_session; + svn_ra_session_t *to_session; + /* Extra 'backdoor' session for fetching data *from* the target repo. */ + svn_ra_session_t *extra_to_session; + svn_revnum_t current_revision; + subcommand_baton_t *sb; + svn_boolean_t has_commit_revprops_capability; + int normalized_rev_props_count; + int normalized_node_props_count; + const char *to_root; +} replay_baton_t; + +/* Return a replay baton allocated from POOL and populated with + data from the provided parameters. */ +static svn_error_t * +make_replay_baton(replay_baton_t **baton_p, + svn_ra_session_t *from_session, + svn_ra_session_t *to_session, + subcommand_baton_t *sb, apr_pool_t *pool) +{ + replay_baton_t *rb = apr_pcalloc(pool, sizeof(*rb)); + rb->from_session = from_session; + rb->to_session = to_session; + rb->sb = sb; + + SVN_ERR(svn_ra_get_repos_root2(to_session, &rb->to_root, pool)); + +#ifdef ENABLE_EV2_SHIMS + /* Open up the extra baton. Only needed for Ev2 shims. */ + SVN_ERR(open_target_session(&rb->extra_to_session, sb, pool)); +#endif + + *baton_p = rb; + return SVN_NO_ERROR; +} + +/* Return TRUE iff KEY is the name of an svn:date or svn:author or any svnsync + * property. Implements filter_func_t. Use with filter_props() to filter out + * svn:date and svn:author and svnsync properties. + */ +static svn_boolean_t +filter_exclude_date_author_sync(const char *key) +{ + if (strcmp(key, SVN_PROP_REVISION_AUTHOR) == 0) + return TRUE; + else if (strcmp(key, SVN_PROP_REVISION_DATE) == 0) + return TRUE; + else if (strncmp(key, SVNSYNC_PROP_PREFIX, + sizeof(SVNSYNC_PROP_PREFIX) - 1) == 0) + return TRUE; + + return FALSE; +} + +/* Return FALSE iff KEY is the name of an svn:date or svn:author or any svnsync + * property. Implements filter_func_t. Use with filter_props() to filter out + * all properties except svn:date and svn:author and svnsync properties. + */ +static svn_boolean_t +filter_include_date_author_sync(const char *key) +{ + return ! filter_exclude_date_author_sync(key); +} + + +/* Return TRUE iff KEY is the name of the svn:log property. + * Implements filter_func_t. Use with filter_props() to only exclude svn:log. + */ +static svn_boolean_t +filter_exclude_log(const char *key) +{ + if (strcmp(key, SVN_PROP_REVISION_LOG) == 0) + return TRUE; + else + return FALSE; +} + +/* Return FALSE iff KEY is the name of the svn:log property. + * Implements filter_func_t. Use with filter_props() to only include svn:log. + */ +static svn_boolean_t +filter_include_log(const char *key) +{ + return ! filter_exclude_log(key); +} + + +static svn_error_t * +fetch_base_func(const char **filename, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct replay_baton_t *rb = baton; + svn_stream_t *fstream; + svn_error_t *err; + + if (svn_path_is_url(path)) + path = svn_uri_skip_ancestor(rb->to_root, path, scratch_pool); + else if (path[0] == '/') + path += 1; + + if (! SVN_IS_VALID_REVNUM(base_revision)) + base_revision = rb->current_revision - 1; + + SVN_ERR(svn_stream_open_unique(&fstream, filename, NULL, + svn_io_file_del_on_pool_cleanup, + result_pool, scratch_pool)); + + err = svn_ra_get_file(rb->extra_to_session, path, base_revision, + fstream, NULL, NULL, scratch_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + SVN_ERR(svn_stream_close(fstream)); + + *filename = NULL; + return SVN_NO_ERROR; + } + else if (err) + return svn_error_trace(err); + + SVN_ERR(svn_stream_close(fstream)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_props_func(apr_hash_t **props, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct replay_baton_t *rb = baton; + svn_node_kind_t node_kind; + + if (svn_path_is_url(path)) + path = svn_uri_skip_ancestor(rb->to_root, path, scratch_pool); + else if (path[0] == '/') + path += 1; + + if (! SVN_IS_VALID_REVNUM(base_revision)) + base_revision = rb->current_revision - 1; + + SVN_ERR(svn_ra_check_path(rb->extra_to_session, path, base_revision, + &node_kind, scratch_pool)); + + if (node_kind == svn_node_file) + { + SVN_ERR(svn_ra_get_file(rb->extra_to_session, path, base_revision, + NULL, NULL, props, result_pool)); + } + else if (node_kind == svn_node_dir) + { + apr_array_header_t *tmp_props; + + SVN_ERR(svn_ra_get_dir2(rb->extra_to_session, NULL, NULL, props, path, + base_revision, 0 /* Dirent fields */, + result_pool)); + tmp_props = svn_prop_hash_to_array(*props, result_pool); + SVN_ERR(svn_categorize_props(tmp_props, NULL, NULL, &tmp_props, + result_pool)); + *props = svn_prop_array_to_hash(tmp_props, result_pool); + } + else + { + *props = apr_hash_make(result_pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_kind_func(svn_node_kind_t *kind, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *scratch_pool) +{ + struct replay_baton_t *rb = baton; + + if (svn_path_is_url(path)) + path = svn_uri_skip_ancestor(rb->to_root, path, scratch_pool); + else if (path[0] == '/') + path += 1; + + if (! SVN_IS_VALID_REVNUM(base_revision)) + base_revision = rb->current_revision - 1; + + SVN_ERR(svn_ra_check_path(rb->extra_to_session, path, base_revision, + kind, scratch_pool)); + + return SVN_NO_ERROR; +} + + +static svn_delta_shim_callbacks_t * +get_shim_callbacks(replay_baton_t *rb, + apr_pool_t *result_pool) +{ + svn_delta_shim_callbacks_t *callbacks = + svn_delta_shim_callbacks_default(result_pool); + + callbacks->fetch_props_func = fetch_props_func; + callbacks->fetch_kind_func = fetch_kind_func; + callbacks->fetch_base_func = fetch_base_func; + callbacks->fetch_baton = rb; + + return callbacks; +} + + +/* Callback function for svn_ra_replay_range, invoked when starting to parse + * a replay report. + */ +static svn_error_t * +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) +{ + const svn_delta_editor_t *commit_editor; + const svn_delta_editor_t *cancel_editor; + const svn_delta_editor_t *sync_editor; + void *commit_baton; + void *cancel_baton; + void *sync_baton; + replay_baton_t *rb = replay_baton; + apr_hash_t *filtered; + int filtered_count; + int normalized_count; + + /* We set this property so that if we error out for some reason + we can later determine where we were in the process of + merging a revision. If we had committed the change, but we + hadn't finished copying the revprops we need to know that, so + we can go back and finish the job before we move on. + + NOTE: We have to set this before we start the commit editor, + because ra_svn doesn't let you change rev props during a + commit. */ + SVN_ERR(svn_ra_change_rev_prop2(rb->to_session, 0, + SVNSYNC_PROP_CURRENTLY_COPYING, + NULL, + svn_string_createf(pool, "%ld", revision), + pool)); + + /* The actual copy is just a replay hooked up to a commit. Include + all the revision properties from the source repositories, except + 'svn:author' and 'svn:date', those are not guaranteed to get + through the editor anyway. + If we're syncing to an non-commit-revprops capable server, filter + out all revprops except svn:log and add them later in + revplay_rev_finished. */ + filtered = filter_props(&filtered_count, rev_props, + (rb->has_commit_revprops_capability + ? filter_exclude_date_author_sync + : filter_include_log), + pool); + + /* svn_ra_get_commit_editor3 requires the log message to be + set. It's possible that we didn't receive 'svn:log' here, so we + have to set it to at least the empty string. If there's a svn:log + property on this revision, we will write the actual value in the + replay_rev_finished callback. */ + if (! svn_hash_gets(filtered, SVN_PROP_REVISION_LOG)) + svn_hash_sets(filtered, SVN_PROP_REVISION_LOG, + svn_string_create_empty(pool)); + + /* If necessary, normalize encoding and line ending style. Add the number + of properties that required EOL normalization to the overall count + in the replay baton. */ + SVN_ERR(svnsync_normalize_revprops(filtered, &normalized_count, + rb->sb->source_prop_encoding, pool)); + rb->normalized_rev_props_count += normalized_count; + + SVN_ERR(svn_ra__register_editor_shim_callbacks(rb->to_session, + get_shim_callbacks(rb, pool))); + SVN_ERR(svn_ra_get_commit_editor3(rb->to_session, &commit_editor, + &commit_baton, + filtered, + commit_callback, rb->sb, + NULL, FALSE, pool)); + + /* There's one catch though, the diff shows us props we can't send + over the RA interface, so we need an editor that's smart enough + to filter those out for us. */ + SVN_ERR(svnsync_get_sync_editor(commit_editor, commit_baton, revision - 1, + rb->sb->to_url, rb->sb->source_prop_encoding, + rb->sb->quiet, &sync_editor, &sync_baton, + &(rb->normalized_node_props_count), pool)); + + SVN_ERR(svn_delta_get_cancellation_editor(check_cancel, NULL, + sync_editor, sync_baton, + &cancel_editor, + &cancel_baton, + pool)); + *editor = cancel_editor; + *edit_baton = cancel_baton; + + rb->current_revision = revision; + return SVN_NO_ERROR; +} + +/* Callback function for svn_ra_replay_range, invoked when finishing parsing + * a replay report. + */ +static svn_error_t * +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) +{ + apr_pool_t *subpool = svn_pool_create(pool); + replay_baton_t *rb = replay_baton; + apr_hash_t *filtered, *existing_props; + int filtered_count; + int normalized_count; + + SVN_ERR(editor->close_edit(edit_baton, pool)); + + /* Sanity check that we actually committed the revision we meant to. */ + if (rb->sb->committed_rev != revision) + return svn_error_createf + (APR_EINVAL, NULL, + _("Commit created rev %ld but should have created %ld"), + rb->sb->committed_rev, revision); + + SVN_ERR(svn_ra_rev_proplist(rb->to_session, revision, &existing_props, + subpool)); + + + /* Ok, we're done with the data, now we just need to copy the remaining + 'svn:date' and 'svn:author' revprops and we're all set. + If the server doesn't support revprops-in-a-commit, we still have to + set all revision properties except svn:log. */ + filtered = filter_props(&filtered_count, rev_props, + (rb->has_commit_revprops_capability + ? filter_include_date_author_sync + : filter_exclude_log), + subpool); + + /* If necessary, normalize encoding and line ending style, and add the number + of EOL-normalized properties to the overall count in the replay baton. */ + SVN_ERR(svnsync_normalize_revprops(filtered, &normalized_count, + rb->sb->source_prop_encoding, pool)); + rb->normalized_rev_props_count += normalized_count; + + SVN_ERR(write_revprops(&filtered_count, rb->to_session, revision, filtered, + subpool)); + + /* Remove all extra properties in TARGET. */ + SVN_ERR(remove_props_not_in_source(rb->to_session, revision, + rev_props, existing_props, subpool)); + + svn_pool_clear(subpool); + + /* Ok, we're done, bring the last-merged-rev property up to date. */ + SVN_ERR(svn_ra_change_rev_prop2( + rb->to_session, + 0, + SVNSYNC_PROP_LAST_MERGED_REV, + NULL, + svn_string_create(apr_psprintf(pool, "%ld", revision), + subpool), + subpool)); + + /* And finally drop the currently copying prop, since we're done + with this revision. */ + SVN_ERR(svn_ra_change_rev_prop2(rb->to_session, 0, + SVNSYNC_PROP_CURRENTLY_COPYING, + NULL, NULL, subpool)); + + /* Notify the user that we copied revision properties. */ + if (! rb->sb->quiet) + SVN_ERR(log_properties_copied(filtered_count > 0, revision, subpool)); + + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + +/* Synchronize the repository associated with RA session TO_SESSION, + * using information found in BATON, while the repository is + * locked. Implements `with_locked_func_t' interface. + */ +static svn_error_t * +do_synchronize(svn_ra_session_t *to_session, + subcommand_baton_t *baton, apr_pool_t *pool) +{ + svn_string_t *last_merged_rev; + svn_revnum_t from_latest; + svn_ra_session_t *from_session; + svn_string_t *currently_copying; + svn_revnum_t to_latest, copying, last_merged; + svn_revnum_t start_revision, end_revision; + replay_baton_t *rb; + int normalized_rev_props_count = 0; + + SVN_ERR(open_source_session(&from_session, &last_merged_rev, + baton->from_url, to_session, + &(baton->source_callbacks), baton->config, + baton, pool)); + + /* Check to see if we have revprops that still need to be copied for + a prior revision we didn't finish copying. But first, check for + state sanity. Remember, mirroring is not an atomic action, + because revision properties are copied separately from the + revision's contents. + + So, any time that currently-copying is not set, then + last-merged-rev should be the HEAD revision of the destination + repository. That is, if we didn't fall over in the middle of a + previous synchronization, then our destination repository should + have exactly as many revisions in it as we've synchronized. + + Alternately, if currently-copying *is* set, it must + be either last-merged-rev or last-merged-rev + 1, and the HEAD + revision must be equal to either last-merged-rev or + currently-copying. If this is not the case, somebody has meddled + with the destination without using svnsync. + */ + + SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_CURRENTLY_COPYING, + ¤tly_copying, pool)); + + SVN_ERR(svn_ra_get_latest_revnum(to_session, &to_latest, pool)); + + last_merged = SVN_STR_TO_REV(last_merged_rev->data); + + if (currently_copying) + { + copying = SVN_STR_TO_REV(currently_copying->data); + + if ((copying < last_merged) + || (copying > (last_merged + 1)) + || ((to_latest != last_merged) && (to_latest != copying))) + { + return svn_error_createf + (APR_EINVAL, NULL, + _("Revision being currently copied (%ld), last merged revision " + "(%ld), and destination HEAD (%ld) are inconsistent; have you " + "committed to the destination without using svnsync?"), + copying, last_merged, to_latest); + } + else if (copying == to_latest) + { + if (copying > last_merged) + { + SVN_ERR(copy_revprops(from_session, to_session, to_latest, TRUE, + baton->quiet, baton->source_prop_encoding, + &normalized_rev_props_count, pool)); + last_merged = copying; + last_merged_rev = svn_string_create + (apr_psprintf(pool, "%ld", last_merged), pool); + } + + /* Now update last merged rev and drop currently changing. + Note that the order here is significant, if we do them + in the wrong order there are race conditions where we + end up not being able to tell if there have been bogus + (i.e. non-svnsync) commits to the dest repository. */ + + SVN_ERR(svn_ra_change_rev_prop2(to_session, 0, + SVNSYNC_PROP_LAST_MERGED_REV, + NULL, last_merged_rev, pool)); + SVN_ERR(svn_ra_change_rev_prop2(to_session, 0, + SVNSYNC_PROP_CURRENTLY_COPYING, + NULL, NULL, pool)); + } + /* If copying > to_latest, then we just fall through to + attempting to copy the revision again. */ + } + else + { + if (to_latest != last_merged) + return svn_error_createf(APR_EINVAL, NULL, + _("Destination HEAD (%ld) is not the last " + "merged revision (%ld); have you " + "committed to the destination without " + "using svnsync?"), + to_latest, last_merged); + } + + /* Now check to see if there are any revisions to copy. */ + SVN_ERR(svn_ra_get_latest_revnum(from_session, &from_latest, pool)); + + if (from_latest < last_merged) + return SVN_NO_ERROR; + + /* Ok, so there are new revisions, iterate over them copying them + into the destination repository. */ + SVN_ERR(make_replay_baton(&rb, from_session, to_session, baton, pool)); + + /* For compatibility with older svnserve versions, check first if we + support adding revprops to the commit. */ + SVN_ERR(svn_ra_has_capability(rb->to_session, + &rb->has_commit_revprops_capability, + SVN_RA_CAPABILITY_COMMIT_REVPROPS, + pool)); + + start_revision = last_merged + 1; + end_revision = from_latest; + + SVN_ERR(check_cancel(NULL)); + + SVN_ERR(svn_ra_replay_range(from_session, start_revision, end_revision, + 0, TRUE, replay_rev_started, + replay_rev_finished, rb, pool)); + + SVN_ERR(log_properties_normalized(rb->normalized_rev_props_count + + normalized_rev_props_count, + rb->normalized_node_props_count, + pool)); + + + return SVN_NO_ERROR; +} + + +/* SUBCOMMAND: sync */ +static svn_error_t * +synchronize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool) +{ + svn_ra_session_t *to_session; + opt_baton_t *opt_baton = b; + apr_array_header_t *targets; + subcommand_baton_t *baton; + const char *to_url, *from_url; + + SVN_ERR(svn_opt__args_to_target_array(&targets, os, + apr_array_make(pool, 0, + sizeof(const char *)), + pool)); + if (targets->nelts < 1) + return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL); + if (targets->nelts > 2) + return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL); + + to_url = APR_ARRAY_IDX(targets, 0, const char *); + if (! svn_path_is_url(to_url)) + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Path '%s' is not a URL"), to_url); + + if (targets->nelts == 2) + { + from_url = APR_ARRAY_IDX(targets, 1, const char *); + if (! svn_path_is_url(from_url)) + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Path '%s' is not a URL"), from_url); + } + else + { + from_url = NULL; /* we'll read it from the destination repos */ + } + + baton = make_subcommand_baton(opt_baton, to_url, from_url, 0, 0, pool); + SVN_ERR(open_target_session(&to_session, baton, pool)); + if (opt_baton->disable_locking) + SVN_ERR(do_synchronize(to_session, baton, pool)); + else + SVN_ERR(with_locked(to_session, do_synchronize, baton, + opt_baton->steal_lock, pool)); + + return SVN_NO_ERROR; +} + + + +/*** `svnsync copy-revprops' ***/ + +/* Copy revision properties to the repository associated with RA + * session TO_SESSION, using information found in BATON, while the + * repository is locked. Implements `with_locked_func_t' interface. + */ +static svn_error_t * +do_copy_revprops(svn_ra_session_t *to_session, + subcommand_baton_t *baton, apr_pool_t *pool) +{ + svn_ra_session_t *from_session; + svn_string_t *last_merged_rev; + svn_revnum_t i; + svn_revnum_t step = 1; + int normalized_rev_props_count = 0; + + SVN_ERR(open_source_session(&from_session, &last_merged_rev, + baton->from_url, to_session, + &(baton->source_callbacks), baton->config, + baton, pool)); + + /* An invalid revision means "last-synced" */ + if (! SVN_IS_VALID_REVNUM(baton->start_rev)) + baton->start_rev = SVN_STR_TO_REV(last_merged_rev->data); + if (! SVN_IS_VALID_REVNUM(baton->end_rev)) + baton->end_rev = SVN_STR_TO_REV(last_merged_rev->data); + + /* Make sure we have revisions within the valid range. */ + if (baton->start_rev > SVN_STR_TO_REV(last_merged_rev->data)) + return svn_error_createf + (APR_EINVAL, NULL, + _("Cannot copy revprops for a revision (%ld) that has not " + "been synchronized yet"), baton->start_rev); + if (baton->end_rev > SVN_STR_TO_REV(last_merged_rev->data)) + return svn_error_createf + (APR_EINVAL, NULL, + _("Cannot copy revprops for a revision (%ld) that has not " + "been synchronized yet"), baton->end_rev); + + /* Now, copy all the requested revisions, in the requested order. */ + step = (baton->start_rev > baton->end_rev) ? -1 : 1; + for (i = baton->start_rev; i != baton->end_rev + step; i = i + step) + { + int normalized_count; + SVN_ERR(check_cancel(NULL)); + SVN_ERR(copy_revprops(from_session, to_session, i, TRUE, baton->quiet, + baton->source_prop_encoding, &normalized_count, + pool)); + normalized_rev_props_count += normalized_count; + } + + /* Notify about normalized props, if any. */ + SVN_ERR(log_properties_normalized(normalized_rev_props_count, 0, pool)); + + return SVN_NO_ERROR; +} + + +/* Set *START_REVNUM to the revision number associated with + START_REVISION, or to SVN_INVALID_REVNUM if START_REVISION + represents "HEAD"; if END_REVISION is specified, set END_REVNUM to + the revision number associated with END_REVISION or to + SVN_INVALID_REVNUM if END_REVISION represents "HEAD"; otherwise set + END_REVNUM to the same value as START_REVNUM. + + As a special case, if neither START_REVISION nor END_REVISION is + specified, set *START_REVNUM to 0 and set *END_REVNUM to + SVN_INVALID_REVNUM. + + Freak out if either START_REVISION or END_REVISION represents an + explicit but invalid revision number. */ +static svn_error_t * +resolve_revnums(svn_revnum_t *start_revnum, + svn_revnum_t *end_revnum, + svn_opt_revision_t start_revision, + svn_opt_revision_t end_revision) +{ + svn_revnum_t start_rev, end_rev; + + /* Special case: neither revision is specified? This is like + -r0:HEAD. */ + if ((start_revision.kind == svn_opt_revision_unspecified) && + (end_revision.kind == svn_opt_revision_unspecified)) + { + *start_revnum = 0; + *end_revnum = SVN_INVALID_REVNUM; + return SVN_NO_ERROR; + } + + /* Get the start revision, which must be either HEAD or a number + (which is required to be a valid one). */ + if (start_revision.kind == svn_opt_revision_head) + { + start_rev = SVN_INVALID_REVNUM; + } + else + { + start_rev = start_revision.value.number; + if (! SVN_IS_VALID_REVNUM(start_rev)) + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Invalid revision number (%ld)"), + start_rev); + } + + /* Get the end revision, which must be unspecified (meaning, + "same as the start_rev"), HEAD, or a number (which is + required to be a valid one). */ + if (end_revision.kind == svn_opt_revision_unspecified) + { + end_rev = start_rev; + } + else if (end_revision.kind == svn_opt_revision_head) + { + end_rev = SVN_INVALID_REVNUM; + } + else + { + end_rev = end_revision.value.number; + if (! SVN_IS_VALID_REVNUM(end_rev)) + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Invalid revision number (%ld)"), + end_rev); + } + + *start_revnum = start_rev; + *end_revnum = end_rev; + return SVN_NO_ERROR; +} + + +/* SUBCOMMAND: copy-revprops */ +static svn_error_t * +copy_revprops_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool) +{ + svn_ra_session_t *to_session; + opt_baton_t *opt_baton = b; + apr_array_header_t *targets; + subcommand_baton_t *baton; + const char *to_url = NULL; + const char *from_url = NULL; + svn_opt_revision_t start_revision, end_revision; + svn_revnum_t start_rev = 0, end_rev = SVN_INVALID_REVNUM; + + /* There should be either one or two arguments left to parse. */ + if (os->argc - os->ind > 2) + return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL); + if (os->argc - os->ind < 1) + return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL); + + /* If there are two args, the last one is either a revision range or + the source URL. */ + if (os->argc - os->ind == 2) + { + const char *arg_str = os->argv[os->argc - 1]; + const char *utf_arg_str; + + SVN_ERR(svn_utf_cstring_to_utf8(&utf_arg_str, arg_str, pool)); + + if (! svn_path_is_url(utf_arg_str)) + { + /* This is the old "... TO_URL REV[:REV2]" syntax. + Revisions come only from this argument. (We effectively + pop that last argument from the end of the argument list + so svn_opt__args_to_target_array() can do its thang.) */ + os->argc--; + + if ((opt_baton->start_rev.kind != svn_opt_revision_unspecified) + || (opt_baton->end_rev.kind != svn_opt_revision_unspecified)) + return svn_error_create( + SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Cannot specify revisions via both command-line arguments " + "and the --revision (-r) option")); + + start_revision.kind = svn_opt_revision_unspecified; + end_revision.kind = svn_opt_revision_unspecified; + if (svn_opt_parse_revision(&start_revision, &end_revision, + arg_str, pool) != 0) + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Invalid revision range '%s' provided"), + arg_str); + + SVN_ERR(resolve_revnums(&start_rev, &end_rev, + start_revision, end_revision)); + + SVN_ERR(svn_opt__args_to_target_array( + &targets, os, + apr_array_make(pool, 1, sizeof(const char *)), pool)); + if (targets->nelts != 1) + return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL); + to_url = APR_ARRAY_IDX(targets, 0, const char *); + from_url = NULL; + } + } + + if (! to_url) + { + /* This is the "... TO_URL SOURCE_URL" syntax. Revisions + come only from the --revision parameter. */ + SVN_ERR(resolve_revnums(&start_rev, &end_rev, + opt_baton->start_rev, opt_baton->end_rev)); + + SVN_ERR(svn_opt__args_to_target_array( + &targets, os, + apr_array_make(pool, 2, sizeof(const char *)), pool)); + if (targets->nelts < 1) + return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL); + if (targets->nelts > 2) + return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL); + to_url = APR_ARRAY_IDX(targets, 0, const char *); + if (targets->nelts == 2) + from_url = APR_ARRAY_IDX(targets, 1, const char *); + else + from_url = NULL; + } + + if (! svn_path_is_url(to_url)) + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Path '%s' is not a URL"), to_url); + if (from_url && (! svn_path_is_url(from_url))) + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Path '%s' is not a URL"), from_url); + + baton = make_subcommand_baton(opt_baton, to_url, from_url, + start_rev, end_rev, pool); + SVN_ERR(open_target_session(&to_session, baton, pool)); + if (opt_baton->disable_locking) + SVN_ERR(do_copy_revprops(to_session, baton, pool)); + else + SVN_ERR(with_locked(to_session, do_copy_revprops, baton, + opt_baton->steal_lock, pool)); + + return SVN_NO_ERROR; +} + + + +/*** `svnsync info' ***/ + + +/* SUBCOMMAND: info */ +static svn_error_t * +info_cmd(apr_getopt_t *os, void *b, apr_pool_t * pool) +{ + svn_ra_session_t *to_session; + opt_baton_t *opt_baton = b; + apr_array_header_t *targets; + subcommand_baton_t *baton; + const char *to_url; + apr_hash_t *props; + svn_string_t *from_url, *from_uuid, *last_merged_rev; + + SVN_ERR(svn_opt__args_to_target_array(&targets, os, + apr_array_make(pool, 0, + sizeof(const char *)), + pool)); + if (targets->nelts < 1) + return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL); + if (targets->nelts > 1) + return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL); + + /* Get the mirror repository URL, and verify that it is URL-ish. */ + to_url = APR_ARRAY_IDX(targets, 0, const char *); + if (! svn_path_is_url(to_url)) + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Path '%s' is not a URL"), to_url); + + /* Open an RA session to the mirror repository URL. */ + baton = make_subcommand_baton(opt_baton, to_url, NULL, 0, 0, pool); + SVN_ERR(open_target_session(&to_session, baton, pool)); + + SVN_ERR(svn_ra_rev_proplist(to_session, 0, &props, pool)); + + from_url = svn_hash_gets(props, SVNSYNC_PROP_FROM_URL); + + if (! from_url) + return svn_error_createf + (SVN_ERR_BAD_URL, NULL, + _("Repository '%s' is not initialized for synchronization"), to_url); + + from_uuid = svn_hash_gets(props, SVNSYNC_PROP_FROM_UUID); + last_merged_rev = svn_hash_gets(props, SVNSYNC_PROP_LAST_MERGED_REV); + + /* Print the info. */ + SVN_ERR(svn_cmdline_printf(pool, _("Source URL: %s\n"), from_url->data)); + if (from_uuid) + SVN_ERR(svn_cmdline_printf(pool, _("Source Repository UUID: %s\n"), + from_uuid->data)); + if (last_merged_rev) + SVN_ERR(svn_cmdline_printf(pool, _("Last Merged Revision: %s\n"), + last_merged_rev->data)); + return SVN_NO_ERROR; +} + + + +/*** `svnsync help' ***/ + + +/* SUBCOMMAND: help */ +static svn_error_t * +help_cmd(apr_getopt_t *os, void *baton, apr_pool_t *pool) +{ + opt_baton_t *opt_baton = baton; + + const char *header = + _("general usage: svnsync SUBCOMMAND DEST_URL [ARGS & OPTIONS ...]\n" + "Type 'svnsync help <subcommand>' for help on a specific subcommand.\n" + "Type 'svnsync --version' to see the program version and RA modules.\n" + "\n" + "Available subcommands:\n"); + + const char *ra_desc_start + = _("The following repository access (RA) modules are available:\n\n"); + + svn_stringbuf_t *version_footer = svn_stringbuf_create(ra_desc_start, + pool); + + SVN_ERR(svn_ra_print_modules(version_footer, pool)); + + SVN_ERR(svn_opt_print_help4(os, "svnsync", + opt_baton ? opt_baton->version : FALSE, + opt_baton ? opt_baton->quiet : FALSE, + /*###opt_state ? opt_state->verbose :*/ FALSE, + version_footer->data, header, + svnsync_cmd_table, svnsync_options, NULL, + NULL, pool)); + + return SVN_NO_ERROR; +} + + + +/*** Main ***/ + +int +main(int argc, const char *argv[]) +{ + const svn_opt_subcommand_desc2_t *subcommand = NULL; + apr_array_header_t *received_opts; + opt_baton_t opt_baton; + svn_config_t *config; + apr_status_t apr_err; + apr_getopt_t *os; + apr_pool_t *pool; + svn_error_t *err; + int opt_id, i; + const char *username = NULL, *source_username = NULL, *sync_username = NULL; + const char *password = NULL, *source_password = NULL, *sync_password = NULL; + apr_array_header_t *config_options = NULL; + const char *source_prop_encoding = NULL; + svn_boolean_t force_interactive = FALSE; + + if (svn_cmdline_init("svnsync", stderr) != EXIT_SUCCESS) + { + return EXIT_FAILURE; + } + + err = check_lib_versions(); + if (err) + return svn_cmdline_handle_exit_error(err, NULL, "svnsync: "); + + /* 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 = svn_ra_initialize(pool); + if (err) + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + + /* Initialize the option baton. */ + memset(&opt_baton, 0, sizeof(opt_baton)); + opt_baton.start_rev.kind = svn_opt_revision_unspecified; + opt_baton.end_rev.kind = svn_opt_revision_unspecified; + + received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int)); + + if (argc <= 1) + { + SVN_INT_ERR(help_cmd(NULL, NULL, pool)); + svn_pool_destroy(pool); + return EXIT_FAILURE; + } + + err = svn_cmdline__getopt_init(&os, argc, argv, pool); + if (err) + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + + os->interleave = 1; + + for (;;) + { + const char *opt_arg; + svn_error_t* opt_err = NULL; + + apr_err = apr_getopt_long(os, svnsync_options, &opt_id, &opt_arg); + if (APR_STATUS_IS_EOF(apr_err)) + break; + else if (apr_err) + { + SVN_INT_ERR(help_cmd(NULL, NULL, pool)); + svn_pool_destroy(pool); + return EXIT_FAILURE; + } + + APR_ARRAY_PUSH(received_opts, int) = opt_id; + + switch (opt_id) + { + case svnsync_opt_non_interactive: + opt_baton.non_interactive = TRUE; + break; + + case svnsync_opt_force_interactive: + force_interactive = TRUE; + break; + + case svnsync_opt_trust_server_cert: + opt_baton.trust_server_cert = TRUE; + break; + + case svnsync_opt_no_auth_cache: + opt_baton.no_auth_cache = TRUE; + break; + + case svnsync_opt_auth_username: + opt_err = svn_utf_cstring_to_utf8(&username, opt_arg, pool); + break; + + case svnsync_opt_auth_password: + opt_err = svn_utf_cstring_to_utf8(&password, opt_arg, pool); + break; + + case svnsync_opt_source_username: + opt_err = svn_utf_cstring_to_utf8(&source_username, opt_arg, pool); + break; + + case svnsync_opt_source_password: + opt_err = svn_utf_cstring_to_utf8(&source_password, opt_arg, pool); + break; + + case svnsync_opt_sync_username: + opt_err = svn_utf_cstring_to_utf8(&sync_username, opt_arg, pool); + break; + + case svnsync_opt_sync_password: + opt_err = svn_utf_cstring_to_utf8(&sync_password, opt_arg, pool); + break; + + case svnsync_opt_config_dir: + { + const char *path_utf8; + opt_err = svn_utf_cstring_to_utf8(&path_utf8, opt_arg, pool); + + if (!opt_err) + opt_baton.config_dir = svn_dirent_internal_style(path_utf8, pool); + } + break; + case svnsync_opt_config_options: + if (!config_options) + config_options = + apr_array_make(pool, 1, + sizeof(svn_cmdline__config_argument_t*)); + + err = svn_utf_cstring_to_utf8(&opt_arg, opt_arg, pool); + if (!err) + err = svn_cmdline__parse_config_option(config_options, + opt_arg, pool); + if (err) + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + break; + + case svnsync_opt_source_prop_encoding: + opt_err = svn_utf_cstring_to_utf8(&source_prop_encoding, opt_arg, + pool); + break; + + case svnsync_opt_disable_locking: + opt_baton.disable_locking = TRUE; + break; + + case svnsync_opt_steal_lock: + opt_baton.steal_lock = TRUE; + break; + + case svnsync_opt_version: + opt_baton.version = TRUE; + break; + + case svnsync_opt_allow_non_empty: + opt_baton.allow_non_empty = TRUE; + break; + + case 'q': + opt_baton.quiet = TRUE; + break; + + case 'r': + if (svn_opt_parse_revision(&opt_baton.start_rev, + &opt_baton.end_rev, + opt_arg, pool) != 0) + { + const char *utf8_opt_arg; + err = svn_utf_cstring_to_utf8(&utf8_opt_arg, opt_arg, pool); + if (! err) + err = svn_error_createf( + SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Syntax error in revision argument '%s'"), + utf8_opt_arg); + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + } + + /* We only allow numbers and 'HEAD'. */ + if (((opt_baton.start_rev.kind != svn_opt_revision_number) && + (opt_baton.start_rev.kind != svn_opt_revision_head)) + || ((opt_baton.end_rev.kind != svn_opt_revision_number) && + (opt_baton.end_rev.kind != svn_opt_revision_head) && + (opt_baton.end_rev.kind != svn_opt_revision_unspecified))) + { + err = svn_error_createf( + SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Invalid revision range '%s' provided"), opt_arg); + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + } + break; + + case '?': + case 'h': + opt_baton.help = TRUE; + break; + + default: + { + SVN_INT_ERR(help_cmd(NULL, NULL, pool)); + svn_pool_destroy(pool); + return EXIT_FAILURE; + } + } + + if(opt_err) + return svn_cmdline_handle_exit_error(opt_err, pool, "svnsync: "); + } + + if (opt_baton.help) + subcommand = svn_opt_get_canonical_subcommand2(svnsync_cmd_table, "help"); + + /* The --non-interactive and --force-interactive options are mutually + * exclusive. */ + if (opt_baton.non_interactive && force_interactive) + { + err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("--non-interactive and --force-interactive " + "are mutually exclusive")); + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + } + else + opt_baton.non_interactive = !svn_cmdline__be_interactive( + opt_baton.non_interactive, + force_interactive); + + /* Disallow the mixing --username/password with their --source- and + --sync- variants. Treat "--username FOO" as "--source-username + FOO --sync-username FOO"; ditto for "--password FOO". */ + if ((username || password) + && (source_username || sync_username + || source_password || sync_password)) + { + err = svn_error_create + (SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Cannot use --username or --password with any of " + "--source-username, --source-password, --sync-username, " + "or --sync-password.\n")); + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + } + if (username) + { + source_username = username; + sync_username = username; + } + if (password) + { + source_password = password; + sync_password = password; + } + opt_baton.source_username = source_username; + opt_baton.source_password = source_password; + opt_baton.sync_username = sync_username; + opt_baton.sync_password = sync_password; + + /* Disallow mixing of --steal-lock and --disable-locking. */ + if (opt_baton.steal_lock && opt_baton.disable_locking) + { + err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("--disable-locking and --steal-lock are " + "mutually exclusive")); + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + } + + /* --trust-server-cert can only be used with --non-interactive */ + if (opt_baton.trust_server_cert && !opt_baton.non_interactive) + { + err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("--trust-server-cert requires " + "--non-interactive")); + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + } + + err = svn_config_ensure(opt_baton.config_dir, pool); + if (err) + return svn_cmdline_handle_exit_error(err, pool, "synsync: "); + + if (subcommand == NULL) + { + if (os->ind >= os->argc) + { + if (opt_baton.version) + { + /* Use the "help" subcommand to handle "--version". */ + static const svn_opt_subcommand_desc2_t pseudo_cmd = + { "--version", help_cmd, {0}, "", + {svnsync_opt_version, /* must accept its own option */ + 'q', /* --quiet */ + } }; + + subcommand = &pseudo_cmd; + } + else + { + SVN_INT_ERR(help_cmd(NULL, NULL, pool)); + svn_pool_destroy(pool); + return EXIT_FAILURE; + } + } + else + { + const char *first_arg = os->argv[os->ind++]; + subcommand = svn_opt_get_canonical_subcommand2(svnsync_cmd_table, + first_arg); + if (subcommand == NULL) + { + SVN_INT_ERR(help_cmd(NULL, NULL, pool)); + svn_pool_destroy(pool); + return EXIT_FAILURE; + } + } + } + + for (i = 0; i < received_opts->nelts; ++i) + { + opt_id = APR_ARRAY_IDX(received_opts, i, int); + + if (opt_id == 'h' || opt_id == '?') + continue; + + if (! svn_opt_subcommand_takes_option3(subcommand, opt_id, NULL)) + { + const char *optstr; + const apr_getopt_option_t *badopt = + svn_opt_get_option_from_code2(opt_id, svnsync_options, subcommand, + pool); + svn_opt_format_option(&optstr, badopt, FALSE, pool); + if (subcommand->name[0] == '-') + { + SVN_INT_ERR(help_cmd(NULL, NULL, pool)); + } + else + { + err = svn_error_createf + (SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Subcommand '%s' doesn't accept option '%s'\n" + "Type 'svnsync help %s' for usage.\n"), + subcommand->name, optstr, subcommand->name); + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + } + } + } + + err = svn_config_get_config(&opt_baton.config, opt_baton.config_dir, pool); + if (err) + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + + /* Update the options in the config */ + if (config_options) + { + svn_error_clear( + svn_cmdline__apply_config_options(opt_baton.config, config_options, + "svnsync: ", "--config-option")); + } + + config = svn_hash_gets(opt_baton.config, SVN_CONFIG_CATEGORY_CONFIG); + + opt_baton.source_prop_encoding = source_prop_encoding; + + apr_signal(SIGINT, signal_handler); + +#ifdef SIGBREAK + /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */ + apr_signal(SIGBREAK, signal_handler); +#endif + +#ifdef SIGHUP + apr_signal(SIGHUP, signal_handler); +#endif + +#ifdef SIGTERM + apr_signal(SIGTERM, signal_handler); +#endif + +#ifdef SIGPIPE + /* Disable SIGPIPE generation for the platforms that have it. */ + apr_signal(SIGPIPE, SIG_IGN); +#endif + +#ifdef SIGXFSZ + /* Disable SIGXFSZ generation for the platforms that have it, + otherwise working with large files when compiled against an APR + that doesn't have large file support will crash the program, + which is uncool. */ + apr_signal(SIGXFSZ, SIG_IGN); +#endif + + err = svn_cmdline_create_auth_baton(&opt_baton.source_auth_baton, + opt_baton.non_interactive, + opt_baton.source_username, + opt_baton.source_password, + opt_baton.config_dir, + opt_baton.no_auth_cache, + opt_baton.trust_server_cert, + config, + check_cancel, NULL, + pool); + if (! err) + err = svn_cmdline_create_auth_baton(&opt_baton.sync_auth_baton, + opt_baton.non_interactive, + opt_baton.sync_username, + opt_baton.sync_password, + opt_baton.config_dir, + opt_baton.no_auth_cache, + opt_baton.trust_server_cert, + config, + check_cancel, NULL, + pool); + if (! err) + err = (*subcommand->cmd_func)(os, &opt_baton, pool); + if (err) + { + /* For argument-related problems, suggest using the 'help' + subcommand. */ + if (err->apr_err == SVN_ERR_CL_INSUFFICIENT_ARGS + || err->apr_err == SVN_ERR_CL_ARG_PARSING_ERROR) + { + err = svn_error_quick_wrap(err, + _("Try 'svnsync help' for more info")); + } + + return svn_cmdline_handle_exit_error(err, pool, "svnsync: "); + } + + svn_pool_destroy(pool); + + return EXIT_SUCCESS; +} |