diff options
author | Lorry <lorry@roadtrain.codethink.co.uk> | 2012-08-22 14:29:52 +0100 |
---|---|---|
committer | Lorry <lorry@roadtrain.codethink.co.uk> | 2012-08-22 14:29:52 +0100 |
commit | f1bdf13786f0752c0846cf36f0d91e4fc6747929 (patch) | |
tree | 4223b2035bf2240d681a53822808b3c7f687b905 /subversion/svnsync | |
download | subversion-tarball-f1bdf13786f0752c0846cf36f0d91e4fc6747929.tar.gz |
Tarball conversion
Diffstat (limited to 'subversion/svnsync')
-rw-r--r-- | subversion/svnsync/main.c | 2143 | ||||
-rw-r--r-- | subversion/svnsync/svnsync.1 | 47 | ||||
-rw-r--r-- | subversion/svnsync/sync.c | 646 | ||||
-rw-r--r-- | subversion/svnsync/sync.h | 85 |
4 files changed, 2921 insertions, 0 deletions
diff --git a/subversion/svnsync/main.c b/subversion/svnsync/main.c new file mode 100644 index 0000000..36603d4 --- /dev/null +++ b/subversion/svnsync/main.c @@ -0,0 +1,2143 @@ +/* + * ==================================================================== + * 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_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_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_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") }, + {"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 + * 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 (! apr_hash_get(source_props, propname, APR_HASH_KEY_STRING)) + 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) == FALSE) + { + apr_hash_set(filtered, propname, APR_HASH_KEY_STRING, 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)); + + /* 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 (svn_uri__is_child(root_url, baton->from_url, pool)) + { + 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 = apr_hash_get(props, SVNSYNC_PROP_FROM_URL, + APR_HASH_KEY_STRING); + from_uuid_str = apr_hash_get(props, SVNSYNC_PROP_FROM_UUID, + APR_HASH_KEY_STRING); + *last_merged_rev = apr_hash_get(props, SVNSYNC_PROP_LAST_MERGED_REV, + APR_HASH_KEY_STRING); + + 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; + subcommand_baton_t *sb; + svn_boolean_t has_commit_revprops_capability; + int normalized_rev_props_count; + int normalized_node_props_count; +} replay_baton_t; + +/* Return a replay baton allocated from POOL and populated with + data from the provided parameters. */ +static replay_baton_t * +make_replay_baton(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; + return rb; +} + +/* 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); +} + + +/* 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 (! apr_hash_get(filtered, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING)) + apr_hash_set(filtered, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING, + svn_string_create("", 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_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; + + 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. */ + rb = make_replay_baton(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; + 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 = apr_hash_get(props, SVNSYNC_PROP_FROM_URL, + APR_HASH_KEY_STRING); + + if (! from_url) + return svn_error_createf + (SVN_ERR_BAD_URL, NULL, + _("Repository '%s' is not initialized for synchronization"), to_url); + + from_uuid = apr_hash_get(props, SVNSYNC_PROP_FROM_UUID, + APR_HASH_KEY_STRING); + last_merged_rev = apr_hash_get(props, SVNSYNC_PROP_LAST_MERGED_REV, + APR_HASH_KEY_STRING); + + /* 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_help3(os, "svnsync", + opt_baton ? opt_baton->version : FALSE, + opt_baton ? opt_baton->quiet : 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; + apr_allocator_t *allocator; + + 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. + */ + if (apr_allocator_create(&allocator)) + return EXIT_FAILURE; + + apr_allocator_max_free_set(allocator, SVN_ALLOCATOR_RECOMMENDED_MAX_FREE); + + pool = svn_pool_create_ex(NULL, allocator); + apr_allocator_owner_set(allocator, pool); + + 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_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"); + + /* 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 = apr_hash_get(opt_baton.config, SVN_CONFIG_CATEGORY_CONFIG, + APR_HASH_KEY_STRING); + + 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; +} diff --git a/subversion/svnsync/svnsync.1 b/subversion/svnsync/svnsync.1 new file mode 100644 index 0000000..d9da6be --- /dev/null +++ b/subversion/svnsync/svnsync.1 @@ -0,0 +1,47 @@ +.\" +.\" +.\" 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. +.\" +.\" +.\" You can view this file with: +.\" nroff -man [filename] +.\" +.TH svnsync 1 +.SH NAME +svnsync \- Subversion repository synchronization tool +.SH SYNOPSIS +.TP +\fBsvnsync\fP \fIcommand\fP \fIdest-url\fP [\fIoptions\fP] [\fIargs\fP] +.SH OVERVIEW +Subversion is a version control system, which allows you to keep old +versions of files and directories (usually source code), keep a log of +who, when, and why changes occurred, etc., like CVS, RCS or SCCS. +\fBSubversion\fP keeps a single copy of the master sources. This copy +is called the source ``repository''; it contains all the information +to permit extracting previous versions of those files at any time. + +For more information about the Subversion project, visit +http://subversion.apache.org. + +Documentation for Subversion and its tools, including detailed usage +explanations of the \fBsvn\fP, \fBsvnadmin\fP, \fBsvnserve\fP and +\fBsvnlook\fP programs, historical background, philosophical +approaches and reasonings, etc., can be found at +http://svnbook.red-bean.com/. + +Run `svnsync help' to access the built-in tool documentation. diff --git a/subversion/svnsync/sync.c b/subversion/svnsync/sync.c new file mode 100644 index 0000000..525a57c --- /dev/null +++ b/subversion/svnsync/sync.c @@ -0,0 +1,646 @@ +/* + * ==================================================================== + * 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_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 "private/svn_opt_private.h" +#include "private/svn_cmdline_private.h" + +#include "sync.h" + +#include "svn_private_config.h" + +#include <apr_network_io.h> +#include <apr_signal.h> +#include <apr_uuid.h> + + +/* Normalize the encoding and line ending style of *STR, so that it contains + * only LF (\n) line endings and is encoded in UTF-8. After return, *STR may + * point at a new svn_string_t* allocated in RESULT_POOL. + * + * If SOURCE_PROP_ENCODING is NULL, then *STR is presumed to be encoded in + * UTF-8. + * + * *WAS_NORMALIZED is set to TRUE when *STR needed line ending normalization. + * Otherwise it is set to FALSE. + * + * SCRATCH_POOL is used for temporary allocations. + */ +static svn_error_t * +normalize_string(const svn_string_t **str, + svn_boolean_t *was_normalized, + const char *source_prop_encoding, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_string_t *new_str; + + *was_normalized = FALSE; + + if (*str == NULL) + return SVN_NO_ERROR; + + SVN_ERR_ASSERT((*str)->data != NULL); + + if (source_prop_encoding == NULL) + source_prop_encoding = "UTF-8"; + + new_str = NULL; + SVN_ERR(svn_subst_translate_string2(&new_str, NULL, was_normalized, + *str, source_prop_encoding, TRUE, + result_pool, scratch_pool)); + *str = new_str; + + return SVN_NO_ERROR; +} + + +/* Normalize the encoding and line ending style of the values of properties + * in REV_PROPS that "need translation" (according to + * svn_prop_needs_translation(), which is currently all svn:* props) so that + * they are encoded in UTF-8 and contain only LF (\n) line endings. + * + * The number of properties that needed line ending normalization is returned in + * *NORMALIZED_COUNT. + * + * No re-encoding is performed if SOURCE_PROP_ENCODING is NULL. + */ +svn_error_t * +svnsync_normalize_revprops(apr_hash_t *rev_props, + int *normalized_count, + const char *source_prop_encoding, + apr_pool_t *pool) +{ + apr_hash_index_t *hi; + *normalized_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); + + if (svn_prop_needs_translation(propname)) + { + svn_boolean_t was_normalized; + SVN_ERR(normalize_string(&propval, &was_normalized, + source_prop_encoding, pool, pool)); + + /* Replace the existing prop value. */ + apr_hash_set(rev_props, propname, APR_HASH_KEY_STRING, propval); + + if (was_normalized) + (*normalized_count)++; /* Count it. */ + } + } + return SVN_NO_ERROR; +} + + +/*** Synchronization Editor ***/ + +/* This editor has a couple of jobs. + * + * First, it needs to filter out the propchanges that can't be passed over + * libsvn_ra. + * + * Second, it needs to adjust for the fact that we might not actually have + * permission to see all of the data from the remote repository, which means + * we could get revisions that are totally empty from our point of view. + * + * Third, it needs to adjust copyfrom paths, adding the root url for the + * destination repository to the beginning of them. + */ + + +/* Edit baton */ +typedef struct edit_baton_t { + const svn_delta_editor_t *wrapped_editor; + void *wrapped_edit_baton; + const char *to_url; /* URL we're copying into, for correct copyfrom URLs */ + const char *source_prop_encoding; + svn_boolean_t called_open_root; + svn_boolean_t got_textdeltas; + svn_revnum_t base_revision; + svn_boolean_t quiet; + svn_boolean_t strip_mergeinfo; /* Are we stripping svn:mergeinfo? */ + svn_boolean_t migrate_svnmerge; /* Are we converting svnmerge.py data? */ + svn_boolean_t mergeinfo_stripped; /* Did we strip svn:mergeinfo? */ + svn_boolean_t svnmerge_migrated; /* Did we convert svnmerge.py data? */ + svn_boolean_t svnmerge_blocked; /* Was there any blocked svnmerge data? */ + int *normalized_node_props_counter; /* Where to count normalizations? */ +} edit_baton_t; + + +/* A dual-purpose baton for files and directories. */ +typedef struct node_baton_t { + void *edit_baton; + void *wrapped_node_baton; +} node_baton_t; + + +/*** Editor vtable functions ***/ + +static svn_error_t * +set_target_revision(void *edit_baton, + svn_revnum_t target_revision, + apr_pool_t *pool) +{ + edit_baton_t *eb = edit_baton; + return eb->wrapped_editor->set_target_revision(eb->wrapped_edit_baton, + target_revision, pool); +} + +static svn_error_t * +open_root(void *edit_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **root_baton) +{ + edit_baton_t *eb = edit_baton; + node_baton_t *dir_baton = apr_palloc(pool, sizeof(*dir_baton)); + + SVN_ERR(eb->wrapped_editor->open_root(eb->wrapped_edit_baton, + base_revision, pool, + &dir_baton->wrapped_node_baton)); + + eb->called_open_root = TRUE; + dir_baton->edit_baton = edit_baton; + *root_baton = dir_baton; + + return SVN_NO_ERROR; +} + +static svn_error_t * +delete_entry(const char *path, + svn_revnum_t base_revision, + void *parent_baton, + apr_pool_t *pool) +{ + node_baton_t *pb = parent_baton; + edit_baton_t *eb = pb->edit_baton; + + return eb->wrapped_editor->delete_entry(path, base_revision, + pb->wrapped_node_baton, pool); +} + +static svn_error_t * +add_directory(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + apr_pool_t *pool, + void **child_baton) +{ + node_baton_t *pb = parent_baton; + edit_baton_t *eb = pb->edit_baton; + node_baton_t *b = apr_palloc(pool, sizeof(*b)); + + /* if copyfrom_path starts with '/' join rest of copyfrom_path leaving + * leading '/' with canonicalized url eb->to_url. + */ + if (copyfrom_path && copyfrom_path[0] == '/') + copyfrom_path = svn_path_url_add_component2(eb->to_url, + copyfrom_path + 1, pool); + + SVN_ERR(eb->wrapped_editor->add_directory(path, pb->wrapped_node_baton, + copyfrom_path, + copyfrom_rev, pool, + &b->wrapped_node_baton)); + + b->edit_baton = eb; + *child_baton = b; + + return SVN_NO_ERROR; +} + +static svn_error_t * +open_directory(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **child_baton) +{ + node_baton_t *pb = parent_baton; + edit_baton_t *eb = pb->edit_baton; + node_baton_t *db = apr_palloc(pool, sizeof(*db)); + + SVN_ERR(eb->wrapped_editor->open_directory(path, pb->wrapped_node_baton, + base_revision, pool, + &db->wrapped_node_baton)); + + db->edit_baton = eb; + *child_baton = db; + + return SVN_NO_ERROR; +} + +static svn_error_t * +add_file(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + apr_pool_t *pool, + void **file_baton) +{ + node_baton_t *pb = parent_baton; + edit_baton_t *eb = pb->edit_baton; + node_baton_t *fb = apr_palloc(pool, sizeof(*fb)); + + if (copyfrom_path) + copyfrom_path = apr_psprintf(pool, "%s%s", eb->to_url, + svn_path_uri_encode(copyfrom_path, pool)); + + SVN_ERR(eb->wrapped_editor->add_file(path, pb->wrapped_node_baton, + copyfrom_path, copyfrom_rev, + pool, &fb->wrapped_node_baton)); + + fb->edit_baton = eb; + *file_baton = fb; + + return SVN_NO_ERROR; +} + +static svn_error_t * +open_file(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **file_baton) +{ + node_baton_t *pb = parent_baton; + edit_baton_t *eb = pb->edit_baton; + node_baton_t *fb = apr_palloc(pool, sizeof(*fb)); + + SVN_ERR(eb->wrapped_editor->open_file(path, pb->wrapped_node_baton, + base_revision, pool, + &fb->wrapped_node_baton)); + + fb->edit_baton = eb; + *file_baton = fb; + + return SVN_NO_ERROR; +} + +static svn_error_t * +apply_textdelta(void *file_baton, + const char *base_checksum, + apr_pool_t *pool, + svn_txdelta_window_handler_t *handler, + void **handler_baton) +{ + node_baton_t *fb = file_baton; + edit_baton_t *eb = fb->edit_baton; + + if (! eb->quiet) + { + if (! eb->got_textdeltas) + SVN_ERR(svn_cmdline_printf(pool, _("Transmitting file data "))); + SVN_ERR(svn_cmdline_printf(pool, ".")); + SVN_ERR(svn_cmdline_fflush(stdout)); + } + + eb->got_textdeltas = TRUE; + return eb->wrapped_editor->apply_textdelta(fb->wrapped_node_baton, + base_checksum, pool, + handler, handler_baton); +} + +static svn_error_t * +close_file(void *file_baton, + const char *text_checksum, + apr_pool_t *pool) +{ + node_baton_t *fb = file_baton; + edit_baton_t *eb = fb->edit_baton; + return eb->wrapped_editor->close_file(fb->wrapped_node_baton, + text_checksum, pool); +} + +static svn_error_t * +absent_file(const char *path, + void *file_baton, + apr_pool_t *pool) +{ + node_baton_t *fb = file_baton; + edit_baton_t *eb = fb->edit_baton; + return eb->wrapped_editor->absent_file(path, fb->wrapped_node_baton, pool); +} + +static svn_error_t * +close_directory(void *dir_baton, + apr_pool_t *pool) +{ + node_baton_t *db = dir_baton; + edit_baton_t *eb = db->edit_baton; + return eb->wrapped_editor->close_directory(db->wrapped_node_baton, pool); +} + +static svn_error_t * +absent_directory(const char *path, + void *dir_baton, + apr_pool_t *pool) +{ + node_baton_t *db = dir_baton; + edit_baton_t *eb = db->edit_baton; + return eb->wrapped_editor->absent_directory(path, db->wrapped_node_baton, + pool); +} + +static svn_error_t * +change_file_prop(void *file_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + node_baton_t *fb = file_baton; + edit_baton_t *eb = fb->edit_baton; + + /* only regular properties can pass over libsvn_ra */ + if (svn_property_kind(NULL, name) != svn_prop_regular_kind) + return SVN_NO_ERROR; + + /* Maybe drop svn:mergeinfo. */ + if (eb->strip_mergeinfo && (strcmp(name, SVN_PROP_MERGEINFO) == 0)) + { + eb->mergeinfo_stripped = TRUE; + return SVN_NO_ERROR; + } + + /* Maybe drop (errantly set, as this is a file) svnmerge.py properties. */ + if (eb->migrate_svnmerge && (strcmp(name, "svnmerge-integrated") == 0)) + { + eb->svnmerge_migrated = TRUE; + return SVN_NO_ERROR; + } + + /* Remember if we see any svnmerge-blocked properties. (They really + shouldn't be here, as this is a file, but whatever...) */ + if (eb->migrate_svnmerge && (strcmp(name, "svnmerge-blocked") == 0)) + { + eb->svnmerge_blocked = TRUE; + } + + /* Normalize svn:* properties as necessary. */ + if (svn_prop_needs_translation(name)) + { + svn_boolean_t was_normalized; + SVN_ERR(normalize_string(&value, &was_normalized, + eb->source_prop_encoding, pool, pool)); + if (was_normalized) + (*(eb->normalized_node_props_counter))++; + } + + return eb->wrapped_editor->change_file_prop(fb->wrapped_node_baton, + name, value, pool); +} + +static svn_error_t * +change_dir_prop(void *dir_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + node_baton_t *db = dir_baton; + edit_baton_t *eb = db->edit_baton; + + /* Only regular properties can pass over libsvn_ra */ + if (svn_property_kind(NULL, name) != svn_prop_regular_kind) + return SVN_NO_ERROR; + + /* Maybe drop svn:mergeinfo. */ + if (eb->strip_mergeinfo && (strcmp(name, SVN_PROP_MERGEINFO) == 0)) + { + eb->mergeinfo_stripped = TRUE; + return SVN_NO_ERROR; + } + + /* Maybe convert svnmerge-integrated data into svn:mergeinfo. (We + ignore svnmerge-blocked for now.) */ + /* ### FIXME: Consult the mirror repository's HEAD prop values and + ### merge svn:mergeinfo, svnmerge-integrated, and svnmerge-blocked. */ + if (eb->migrate_svnmerge && (strcmp(name, "svnmerge-integrated") == 0)) + { + if (value) + { + /* svnmerge-integrated differs from svn:mergeinfo in a pair + of ways. First, it can use tabs, newlines, or spaces to + delimit source information. Secondly, the source paths + are relative URLs, whereas svn:mergeinfo uses relative + paths (not URI-encoded). */ + svn_error_t *err; + svn_stringbuf_t *mergeinfo_buf = svn_stringbuf_create("", pool); + svn_mergeinfo_t mergeinfo; + int i; + apr_array_header_t *sources = + svn_cstring_split(value->data, " \t\n", TRUE, pool); + svn_string_t *new_value; + + for (i = 0; i < sources->nelts; i++) + { + const char *rel_path; + apr_array_header_t *path_revs = + svn_cstring_split(APR_ARRAY_IDX(sources, i, const char *), + ":", TRUE, pool); + + /* ### TODO: Warn? */ + if (path_revs->nelts != 2) + continue; + + /* Append this source's mergeinfo data. */ + rel_path = APR_ARRAY_IDX(path_revs, 0, const char *); + rel_path = svn_path_uri_decode(rel_path, pool); + svn_stringbuf_appendcstr(mergeinfo_buf, rel_path); + svn_stringbuf_appendcstr(mergeinfo_buf, ":"); + svn_stringbuf_appendcstr(mergeinfo_buf, + APR_ARRAY_IDX(path_revs, 1, + const char *)); + svn_stringbuf_appendcstr(mergeinfo_buf, "\n"); + } + + /* Try to parse the mergeinfo string we've created, just to + check for bogosity. If all goes well, we'll unparse it + again and use that as our property value. */ + err = svn_mergeinfo_parse(&mergeinfo, mergeinfo_buf->data, pool); + if (err) + { + svn_error_clear(err); + return SVN_NO_ERROR; + } + SVN_ERR(svn_mergeinfo_to_string(&new_value, mergeinfo, pool)); + value = new_value; + } + name = SVN_PROP_MERGEINFO; + eb->svnmerge_migrated = TRUE; + } + + /* Remember if we see any svnmerge-blocked properties. */ + if (eb->migrate_svnmerge && (strcmp(name, "svnmerge-blocked") == 0)) + { + eb->svnmerge_blocked = TRUE; + } + + /* Normalize svn:* properties as necessary. */ + if (svn_prop_needs_translation(name)) + { + svn_boolean_t was_normalized; + SVN_ERR(normalize_string(&value, &was_normalized, eb->source_prop_encoding, + pool, pool)); + if (was_normalized) + (*(eb->normalized_node_props_counter))++; + } + + return eb->wrapped_editor->change_dir_prop(db->wrapped_node_baton, + name, value, pool); +} + +static svn_error_t * +close_edit(void *edit_baton, + apr_pool_t *pool) +{ + edit_baton_t *eb = edit_baton; + + /* If we haven't opened the root yet, that means we're transfering + an empty revision, probably because we aren't allowed to see the + contents for some reason. In any event, we need to open the root + and close it again, before we can close out the edit, or the + commit will fail. */ + + if (! eb->called_open_root) + { + void *baton; + SVN_ERR(eb->wrapped_editor->open_root(eb->wrapped_edit_baton, + eb->base_revision, pool, + &baton)); + SVN_ERR(eb->wrapped_editor->close_directory(baton, pool)); + } + + if (! eb->quiet) + { + if (eb->got_textdeltas) + SVN_ERR(svn_cmdline_printf(pool, "\n")); + if (eb->mergeinfo_stripped) + SVN_ERR(svn_cmdline_printf(pool, + "NOTE: Dropped Subversion mergeinfo " + "from this revision.\n")); + if (eb->svnmerge_migrated) + SVN_ERR(svn_cmdline_printf(pool, + "NOTE: Migrated 'svnmerge-integrated' in " + "this revision.\n")); + if (eb->svnmerge_blocked) + SVN_ERR(svn_cmdline_printf(pool, + "NOTE: Saw 'svnmerge-blocked' in this " + "revision (but didn't migrate it).\n")); + } + + return eb->wrapped_editor->close_edit(eb->wrapped_edit_baton, pool); +} + +static svn_error_t * +abort_edit(void *edit_baton, + apr_pool_t *pool) +{ + edit_baton_t *eb = edit_baton; + return eb->wrapped_editor->abort_edit(eb->wrapped_edit_baton, pool); +} + + +/*** Editor factory function ***/ + +svn_error_t * +svnsync_get_sync_editor(const svn_delta_editor_t *wrapped_editor, + void *wrapped_edit_baton, + svn_revnum_t base_revision, + const char *to_url, + const char *source_prop_encoding, + svn_boolean_t quiet, + const svn_delta_editor_t **editor, + void **edit_baton, + int *normalized_node_props_counter, + apr_pool_t *pool) +{ + svn_delta_editor_t *tree_editor = svn_delta_default_editor(pool); + edit_baton_t *eb = apr_pcalloc(pool, sizeof(*eb)); + + tree_editor->set_target_revision = set_target_revision; + tree_editor->open_root = open_root; + tree_editor->delete_entry = delete_entry; + tree_editor->add_directory = add_directory; + tree_editor->open_directory = open_directory; + tree_editor->change_dir_prop = change_dir_prop; + tree_editor->close_directory = close_directory; + tree_editor->absent_directory = absent_directory; + tree_editor->add_file = add_file; + tree_editor->open_file = open_file; + tree_editor->apply_textdelta = apply_textdelta; + tree_editor->change_file_prop = change_file_prop; + tree_editor->close_file = close_file; + tree_editor->absent_file = absent_file; + tree_editor->close_edit = close_edit; + tree_editor->abort_edit = abort_edit; + + eb->wrapped_editor = wrapped_editor; + eb->wrapped_edit_baton = wrapped_edit_baton; + eb->base_revision = base_revision; + eb->to_url = to_url; + eb->source_prop_encoding = source_prop_encoding; + eb->quiet = quiet; + eb->normalized_node_props_counter = normalized_node_props_counter; + + if (getenv("SVNSYNC_UNSUPPORTED_STRIP_MERGEINFO")) + { + eb->strip_mergeinfo = TRUE; + } + if (getenv("SVNSYNC_UNSUPPORTED_MIGRATE_SVNMERGE")) + { + /* Current we can't merge property values. That's only possible + if all the properties to be merged were always modified in + exactly the same revisions, or if we allow ourselves to + lookup the current state of properties in the sync + destination. So for now, migrating svnmerge.py data implies + stripping pre-existing svn:mergeinfo. */ + /* ### FIXME: Do a real migration by consulting the mirror + ### repository's HEAD propvalues and merging svn:mergeinfo, + ### svnmerge-integrated, and svnmerge-blocked together. */ + eb->migrate_svnmerge = TRUE; + eb->strip_mergeinfo = TRUE; + } + + *editor = tree_editor; + *edit_baton = eb; + + return SVN_NO_ERROR; +} + diff --git a/subversion/svnsync/sync.h b/subversion/svnsync/sync.h new file mode 100644 index 0000000..8afc263 --- /dev/null +++ b/subversion/svnsync/sync.h @@ -0,0 +1,85 @@ +/* + * sync.h : The synchronization editor for svnsync. + * + * ==================================================================== + * 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. + * ==================================================================== + */ + +#ifndef SYNC_H +#define SYNC_H + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + +#include "svn_types.h" +#include "svn_delta.h" + + +/* Normalize the encoding and line ending style of the values of properties + * in REV_PROPS that "need translation" (according to + * svn_prop_needs_translation(), which is currently all svn:* props) so that + * they are encoded in UTF-8 and contain only LF (\n) line endings. + * + * The number of properties that needed line ending normalization is returned in + * *NORMALIZED_COUNT. + * + * No re-encoding is performed if SOURCE_PROP_ENCODING is NULL. + */ +svn_error_t * +svnsync_normalize_revprops(apr_hash_t *rev_props, + int *normalized_count, + const char *source_prop_encoding, + apr_pool_t *pool); + + +/* Set WRAPPED_EDITOR and WRAPPED_EDIT_BATON to an editor/baton pair + * that wraps our own commit EDITOR/EDIT_BATON. BASE_REVISION is the + * revision on which the driver of this returned editor will be basing + * the commit. TO_URL is the URL of the root of the repository into + * which the commit is being made. + * + * If SOURCE_PROP_ENCODING is NULL, then property values are presumed to be + * encoded in UTF-8 and are not re-encoded. Otherwise, the property values are + * presumed to be encoded in SOURCE_PROP_ENCODING, and are normalized to UTF-8. + * + * As the sync editor encounters property values, it might see the need to + * normalize them (re-encode and/or change to LF line endings). Each carried-out + * line ending normalization adds 1 to the *NORMALIZED_NODE_PROPS_COUNTER + * (for notification). + */ +svn_error_t * +svnsync_get_sync_editor(const svn_delta_editor_t *wrapped_editor, + void *wrapped_edit_baton, + svn_revnum_t base_revision, + const char *to_url, + const char *source_prop_encoding, + svn_boolean_t quiet, + const svn_delta_editor_t **editor, + void **edit_baton, + int *normalized_node_props_counter, + apr_pool_t *pool); + + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* SYNC_H */ |