Fix compiler warning due to missing function prototype.
[svn.git] / subversion / svnsync / main.c
blobea99279c5918f266659bc22f68818c0a86c1d548
1 /*
2 * ====================================================================
3 * Copyright (c) 2005-2007 CollabNet. All rights reserved.
5 * This software is licensed as described in the file COPYING, which
6 * you should have received as part of this distribution. The terms
7 * are also available at http://subversion.tigris.org/license-1.html.
8 * If newer versions of this license are posted there, you may use a
9 * newer version instead, at your option.
11 * This software consists of voluntary contributions made by many
12 * individuals. For exact contribution history, see the revision
13 * history and logs, available at http://subversion.tigris.org/.
14 * ====================================================================
17 #include "svn_cmdline.h"
18 #include "svn_config.h"
19 #include "svn_pools.h"
20 #include "svn_delta.h"
21 #include "svn_path.h"
22 #include "svn_props.h"
23 #include "svn_auth.h"
24 #include "svn_opt.h"
25 #include "svn_ra.h"
27 #include "svn_private_config.h"
29 #include <apr_network_io.h>
30 #include <apr_signal.h>
31 #include <apr_uuid.h>
33 static svn_opt_subcommand_t initialize_cmd,
34 synchronize_cmd,
35 copy_revprops_cmd,
36 help_cmd;
38 enum {
39 svnsync_opt_non_interactive = SVN_OPT_FIRST_LONGOPT_ID,
40 svnsync_opt_no_auth_cache,
41 svnsync_opt_auth_username,
42 svnsync_opt_auth_password,
43 svnsync_opt_source_username,
44 svnsync_opt_source_password,
45 svnsync_opt_sync_username,
46 svnsync_opt_sync_password,
47 svnsync_opt_config_dir,
48 svnsync_opt_version
51 #define SVNSYNC_OPTS_DEFAULT svnsync_opt_non_interactive, \
52 svnsync_opt_no_auth_cache, \
53 svnsync_opt_auth_username, \
54 svnsync_opt_auth_password, \
55 svnsync_opt_source_username, \
56 svnsync_opt_source_password, \
57 svnsync_opt_sync_username, \
58 svnsync_opt_sync_password, \
59 svnsync_opt_config_dir, \
60 'q'
62 static const svn_opt_subcommand_desc_t svnsync_cmd_table[] =
64 { "initialize", initialize_cmd, { "init" },
65 N_("usage: svnsync initialize DEST_URL SOURCE_URL\n"
66 "\n"
67 "Initialize a destination repository for synchronization from\n"
68 "another repository.\n"
69 "\n"
70 "The destination URL must point to the root of a repository with\n"
71 "no committed revisions. The destination repository must allow\n"
72 "revision property changes.\n"
73 "\n"
74 "You should not commit to, or make revision property changes in,\n"
75 "the destination repository by any method other than 'svnsync'.\n"
76 "In other words, the destination repository should be a read-only\n"
77 "mirror of the source repository.\n"),
78 { SVNSYNC_OPTS_DEFAULT } },
79 { "synchronize", synchronize_cmd, { "sync" },
80 N_("usage: svnsync synchronize DEST_URL\n"
81 "\n"
82 "Transfer all pending revisions to the destination from the source\n"
83 "with which it was initialized.\n"),
84 { SVNSYNC_OPTS_DEFAULT } },
85 { "copy-revprops", copy_revprops_cmd, { 0 },
86 N_("usage: svnsync copy-revprops DEST_URL [REV[:REV2]]\n"
87 "\n"
88 "Copy the revision properties in a given range of revisions to the\n"
89 "destination from the source with which it was initialized.\n"
90 "\n"
91 "If REV and REV2 are provided, copy properties for the revisions\n"
92 "specified by that range, inclusively. If only REV is provided,\n"
93 "copy properties for that revision alone. If REV is not provided,\n"
94 "copy properties for all revisions previously transferred to the\n"
95 "destination.\n"
96 "\n"
97 "REV and REV2 must be revisions which were previously transferred\n"
98 "to the destination. You may use \"HEAD\" for either revision to\n"
99 "mean \"the last revision transferred\".\n"),
100 { SVNSYNC_OPTS_DEFAULT } },
101 { "help", help_cmd, { "?", "h" },
102 N_("usage: svnsync help [SUBCOMMAND...]\n"
103 "\n"
104 "Describe the usage of this program or its subcommands.\n"),
105 { 0 } },
106 { NULL, NULL, { 0 }, NULL, { 0 } }
109 static const apr_getopt_option_t svnsync_options[] =
111 {"quiet", 'q', 0,
112 N_("print as little as possible") },
113 {"non-interactive", svnsync_opt_non_interactive, 0,
114 N_("do no interactive prompting") },
115 {"no-auth-cache", svnsync_opt_no_auth_cache, 0,
116 N_("do not cache authentication tokens") },
117 {"username", svnsync_opt_auth_username, 1,
118 N_("specify a username ARG (deprecated;\n"
120 "see --source-username and --sync-username)") },
121 {"password", svnsync_opt_auth_password, 1,
122 N_("specify a password ARG (deprecated;\n"
124 "see --source-password and --sync-password)") },
125 {"source-username", svnsync_opt_source_username, 1,
126 N_("connect to source repository with username ARG") },
127 {"source-password", svnsync_opt_source_password, 1,
128 N_("connect to source repository with password ARG") },
129 {"sync-username", svnsync_opt_sync_username, 1,
130 N_("connect to sync repository with username ARG") },
131 {"sync-password", svnsync_opt_sync_password, 1,
132 N_("connect to sync repository with password ARG") },
133 {"config-dir", svnsync_opt_config_dir, 1,
134 N_("read user configuration files from directory ARG")},
135 {"version", svnsync_opt_version, 0,
136 N_("show program version information")},
137 {"help", 'h', 0,
138 N_("show help on a subcommand")},
139 {NULL, '?', 0,
140 N_("show help on a subcommand")},
141 { 0, 0, 0, 0 }
144 typedef struct {
145 svn_boolean_t non_interactive;
146 svn_boolean_t no_auth_cache;
147 svn_auth_baton_t *source_auth_baton;
148 svn_auth_baton_t *sync_auth_baton;
149 const char *source_username;
150 const char *source_password;
151 const char *sync_username;
152 const char *sync_password;
153 const char *config_dir;
154 apr_hash_t *config;
155 svn_boolean_t quiet;
156 svn_boolean_t version;
157 svn_boolean_t help;
158 } opt_baton_t;
163 /*** Helper functions ***/
166 /* Global record of whether the user has requested cancellation. */
167 static volatile sig_atomic_t cancelled = FALSE;
170 /* Callback function for apr_signal(). */
171 static void
172 signal_handler(int signum)
174 apr_signal(signum, SIG_IGN);
175 cancelled = TRUE;
179 /* Cancellation callback function. */
180 static svn_error_t *
181 check_cancel(void *baton)
183 if (cancelled)
184 return svn_error_create(SVN_ERR_CANCELLED, NULL, _("Caught signal"));
185 else
186 return SVN_NO_ERROR;
190 /* Check that the version of libraries in use match what we expect. */
191 static svn_error_t *
192 check_lib_versions(void)
194 static const svn_version_checklist_t checklist[] =
196 { "svn_subr", svn_subr_version },
197 { "svn_delta", svn_delta_version },
198 { "svn_ra", svn_ra_version },
199 { NULL, NULL }
202 SVN_VERSION_DEFINE(my_version);
204 return svn_ver_check_list(&my_version, checklist);
208 /* Acquire a lock (of sorts) on the repository associated with the
209 * given RA SESSION.
211 static svn_error_t *
212 get_lock(svn_ra_session_t *session, apr_pool_t *pool)
214 char hostname_str[APRMAXHOSTLEN + 1] = { 0 };
215 svn_string_t *mylocktoken, *reposlocktoken;
216 apr_status_t apr_err;
217 apr_pool_t *subpool;
218 int i;
220 apr_err = apr_gethostname(hostname_str, sizeof(hostname_str), pool);
221 if (apr_err)
222 return svn_error_wrap_apr(apr_err, _("Can't get local hostname"));
224 mylocktoken = svn_string_createf(pool, "%s:%s", hostname_str,
225 svn_uuid_generate(pool));
227 subpool = svn_pool_create(pool);
229 for (i = 0; i < 10; ++i)
231 svn_pool_clear(subpool);
232 SVN_ERR(check_cancel(NULL));
234 SVN_ERR(svn_ra_rev_prop(session, 0, SVNSYNC_PROP_LOCK, &reposlocktoken,
235 subpool));
237 if (reposlocktoken)
239 /* Did we get it? If so, we're done, otherwise we sleep. */
240 if (strcmp(reposlocktoken->data, mylocktoken->data) == 0)
241 return SVN_NO_ERROR;
242 else
244 SVN_ERR(svn_cmdline_printf
245 (pool, _("Failed to get lock on destination "
246 "repos, currently held by '%s'\n"),
247 reposlocktoken->data));
249 apr_sleep(apr_time_from_sec(1));
252 else
254 SVN_ERR(svn_ra_change_rev_prop(session, 0, SVNSYNC_PROP_LOCK,
255 mylocktoken, subpool));
259 return svn_error_createf(APR_EINVAL, NULL,
260 "Couldn't get lock on destination repos "
261 "after %d attempts\n", i);
265 /* Baton for the various subcommands to share. */
266 typedef struct {
267 /* common to all subcommands */
268 apr_hash_t *config;
269 svn_ra_callbacks2_t source_callbacks;
270 svn_ra_callbacks2_t sync_callbacks;
271 svn_boolean_t quiet;
272 const char *to_url;
274 /* initialize only */
275 const char *from_url;
277 /* synchronize only */
278 svn_revnum_t committed_rev;
280 /* copy-revprops only */
281 svn_revnum_t start_rev;
282 svn_revnum_t end_rev;
284 } subcommand_baton_t;
286 typedef svn_error_t *(*with_locked_func_t)(svn_ra_session_t *session,
287 subcommand_baton_t *baton,
288 apr_pool_t *pool);
291 /* Lock the repository associated with RA SESSION, then execute the
292 * given FUNC/BATON pair while holding the lock. Finally, drop the
293 * lock once it finishes.
295 static svn_error_t *
296 with_locked(svn_ra_session_t *session,
297 with_locked_func_t func,
298 subcommand_baton_t *baton,
299 apr_pool_t *pool)
301 svn_error_t *err, *err2;
303 SVN_ERR(get_lock(session, pool));
305 err = func(session, baton, pool);
307 err2 = svn_ra_change_rev_prop(session, 0, SVNSYNC_PROP_LOCK, NULL, pool);
308 if (err2 && err)
310 svn_error_clear(err2); /* XXX what to do here? */
312 return err;
314 else if (err2)
316 return err2;
318 else
320 return err;
325 /* Callback function for the RA session's open_tmp_file()
326 * requirements.
328 static svn_error_t *
329 open_tmp_file(apr_file_t **fp, void *callback_baton, apr_pool_t *pool)
331 const char *path;
333 SVN_ERR(svn_io_temp_dir(&path, pool));
335 path = svn_path_join(path, "tempfile", pool);
337 SVN_ERR(svn_io_open_unique_file2(fp, NULL, path, ".tmp",
338 svn_io_file_del_on_close, pool));
340 return SVN_NO_ERROR;
344 /* Return SVN_NO_ERROR iff URL identifies the root directory of the
345 * repository associated with RA session SESS.
347 static svn_error_t *
348 check_if_session_is_at_repos_root(svn_ra_session_t *sess,
349 const char *url,
350 apr_pool_t *pool)
352 const char *sess_root;
354 SVN_ERR(svn_ra_get_repos_root2(sess, &sess_root, pool));
356 if (strcmp(url, sess_root) == 0)
357 return SVN_NO_ERROR;
358 else
359 return svn_error_createf
360 (APR_EINVAL, NULL,
361 _("Session is rooted at '%s' but the repos root is '%s'"),
362 url, sess_root);
366 /* Remove the properties in TARGET_PROPS but not in SOURCE_PROPS from
367 * revision REV of the repository associated with RA session SESSION.
369 * All allocations will be done in a subpool of POOL.
371 static svn_error_t *
372 remove_props_not_in_source(svn_ra_session_t *session,
373 svn_revnum_t rev,
374 apr_hash_t *source_props,
375 apr_hash_t *target_props,
376 apr_pool_t *pool)
378 apr_pool_t *subpool = svn_pool_create(pool);
379 apr_hash_index_t *hi;
381 for (hi = apr_hash_first(pool, target_props);
383 hi = apr_hash_next(hi))
385 const void *key;
387 svn_pool_clear(subpool);
389 apr_hash_this(hi, &key, NULL, NULL);
391 /* Delete property if the key can't be found in SOURCE_PROPS. */
392 if (! apr_hash_get(source_props, key, APR_HASH_KEY_STRING))
393 SVN_ERR(svn_ra_change_rev_prop(session, rev, key, NULL,
394 subpool));
397 svn_pool_destroy(subpool);
399 return SVN_NO_ERROR;
402 /* Filter callback function.
403 * Takes a property name KEY, and is expected to return TRUE if the property
404 * should be filtered out (ie. not be copied to the target list), or FALSE if
405 * not.
407 typedef svn_boolean_t (*filter_func_t)(const char *key);
409 /* Make a new set of properties, by copying those properties in PROPS for which
410 * the filter FILTER returns FALSE.
412 * The number of filtered properties will be stored in FILTERED_COUNT.
414 * The returned set of properties is allocated from POOL.
416 static apr_hash_t *
417 filter_props(int *filtered_count, apr_hash_t *props,
418 filter_func_t filter,
419 apr_pool_t *pool)
421 apr_hash_index_t *hi;
422 apr_hash_t *filtered = apr_hash_make(pool);
423 *filtered_count = 0;
425 for (hi = apr_hash_first(pool, props); hi ; hi = apr_hash_next(hi))
427 void *val;
428 const void *key;
429 apr_ssize_t len;
431 apr_hash_this(hi, &key, &len, &val);
433 /* Copy all properties:
434 - not matching the exclude pattern if provided OR
435 - matching the include pattern if provided */
436 if (!filter || filter(key) == FALSE)
438 apr_hash_set(filtered, key, APR_HASH_KEY_STRING, val);
440 else
442 *filtered_count += 1;
446 return filtered;
450 /* Write the set of revision properties REV_PROPS to revision REV to the
451 * repository associated with RA session SESSION.
453 * All allocations will be done in a subpool of POOL.
455 static svn_error_t *
456 write_revprops(int *filtered_count,
457 svn_ra_session_t *session,
458 svn_revnum_t rev,
459 apr_hash_t *rev_props,
460 apr_pool_t *pool)
462 apr_pool_t *subpool = svn_pool_create(pool);
463 apr_hash_index_t *hi;
465 *filtered_count = 0;
467 for (hi = apr_hash_first(pool, rev_props); hi; hi = apr_hash_next(hi))
469 const void *key;
470 void *val;
472 svn_pool_clear(subpool);
473 apr_hash_this(hi, &key, NULL, &val);
475 if (strncmp(key, SVNSYNC_PROP_PREFIX,
476 sizeof(SVNSYNC_PROP_PREFIX) - 1) != 0)
478 SVN_ERR(svn_ra_change_rev_prop(session, rev, key, val, subpool));
480 else
482 *filtered_count += 1;
486 svn_pool_destroy(subpool);
488 return SVN_NO_ERROR;
492 static svn_error_t *
493 log_properties_copied(svn_boolean_t syncprops_found,
494 svn_revnum_t rev,
495 apr_pool_t *pool)
497 if (syncprops_found)
498 SVN_ERR(svn_cmdline_printf(pool,
499 _("Copied properties for revision %ld "
500 "(%s* properties skipped).\n"),
501 rev, SVNSYNC_PROP_PREFIX));
502 else
503 SVN_ERR(svn_cmdline_printf(pool,
504 _("Copied properties for revision %ld.\n"),
505 rev));
507 return SVN_NO_ERROR;
510 /* Copy all the revision properties, except for those that have the
511 * "svn:sync-" prefix, from revision REV of the repository associated
512 * with RA session FROM_SESSION, to the repository associated with RA
513 * session TO_SESSION.
515 * If SYNC is TRUE, then properties on the destination revision that
516 * do not exist on the source revision will be removed.
518 static svn_error_t *
519 copy_revprops(svn_ra_session_t *from_session,
520 svn_ra_session_t *to_session,
521 svn_revnum_t rev,
522 svn_boolean_t sync,
523 svn_boolean_t quiet,
524 apr_pool_t *pool)
526 apr_pool_t *subpool = svn_pool_create(pool);
527 apr_hash_t *existing_props, *rev_props;
528 int filtered_count = 0;
530 /* Get the list of revision properties on REV of TARGET. We're only interested
531 in the property names, but we'll get the values 'for free'. */
532 if (sync)
533 SVN_ERR(svn_ra_rev_proplist(to_session, rev, &existing_props, subpool));
535 /* Get the list of revision properties on REV of SOURCE. */
536 SVN_ERR(svn_ra_rev_proplist(from_session, rev, &rev_props, subpool));
538 /* Copy all but the svn:svnsync properties. */
539 SVN_ERR(write_revprops(&filtered_count, to_session, rev, rev_props, pool));
541 /* Delete those properties that were in TARGET but not in SOURCE */
542 if (sync)
543 SVN_ERR(remove_props_not_in_source(to_session, rev,
544 rev_props, existing_props, pool));
546 if (! quiet)
547 SVN_ERR(log_properties_copied(filtered_count > 0, rev, pool));
549 svn_pool_destroy(subpool);
551 return SVN_NO_ERROR;
555 /* Return a subcommand baton allocated from POOL and populated with
556 data from the provided parameters, which include the global
557 OPT_BATON options structure and a handful of other options. Not
558 all parameters are used in all subcommands -- see
559 subcommand_baton_t's definition for details. */
560 static subcommand_baton_t *
561 make_subcommand_baton(opt_baton_t *opt_baton,
562 const char *to_url,
563 const char *from_url,
564 svn_revnum_t start_rev,
565 svn_revnum_t end_rev,
566 apr_pool_t *pool)
568 subcommand_baton_t *b = apr_pcalloc(pool, sizeof(*b));
569 b->config = opt_baton->config;
570 b->source_callbacks.open_tmp_file = open_tmp_file;
571 b->source_callbacks.auth_baton = opt_baton->source_auth_baton;
572 b->sync_callbacks.open_tmp_file = open_tmp_file;
573 b->sync_callbacks.auth_baton = opt_baton->sync_auth_baton;
574 b->quiet = opt_baton->quiet;
575 b->to_url = to_url;
576 b->from_url = from_url;
577 b->start_rev = start_rev;
578 b->end_rev = end_rev;
579 return b;
583 /*** `svnsync init' ***/
585 /* Initialize the repository associated with RA session TO_SESSION,
586 * using information found in baton B, while the repository is
587 * locked. Implements `with_locked_func_t' interface.
589 static svn_error_t *
590 do_initialize(svn_ra_session_t *to_session,
591 subcommand_baton_t *baton,
592 apr_pool_t *pool)
594 svn_ra_session_t *from_session;
595 svn_string_t *from_url;
596 svn_revnum_t latest;
597 const char *uuid, *root_url;
599 /* First, sanity check to see that we're copying into a brand new repos. */
601 SVN_ERR(svn_ra_get_latest_revnum(to_session, &latest, pool));
603 if (latest != 0)
604 return svn_error_create
605 (APR_EINVAL, NULL,
606 _("Cannot initialize a repository with content in it"));
608 /* And check to see if anyone's run initialize on it before... We
609 may want a --force option to override this check. */
611 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL,
612 &from_url, pool));
614 if (from_url)
615 return svn_error_createf
616 (APR_EINVAL, NULL,
617 _("Destination repository is already synchronizing from '%s'"),
618 from_url->data);
620 /* Now fill in our bookkeeping info in the dest repository. */
622 SVN_ERR(svn_ra_open3(&from_session, baton->from_url, NULL,
623 &(baton->source_callbacks), baton,
624 baton->config, pool));
625 SVN_ERR(svn_ra_get_repos_root2(from_session, &root_url, pool));
627 /* If we're doing a partial replay, we have to check first if the server
628 supports this. */
629 if (svn_path_is_child(root_url, baton->from_url, pool))
631 svn_boolean_t server_supports_partial_replay;
632 svn_error_t *err = svn_ra_has_capability(from_session,
633 &server_supports_partial_replay,
634 SVN_RA_CAPABILITY_PARTIAL_REPLAY,
635 pool);
636 if (err && err->apr_err == SVN_ERR_UNKNOWN_CAPABILITY)
638 svn_error_clear(err);
639 server_supports_partial_replay = FALSE;
642 if (!server_supports_partial_replay)
643 return svn_error_create(SVN_ERR_RA_PARTIAL_REPLAY_NOT_SUPPORTED, NULL,
644 NULL);
647 SVN_ERR(svn_ra_change_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL,
648 svn_string_create(baton->from_url, pool),
649 pool));
651 SVN_ERR(svn_ra_get_uuid2(from_session, &uuid, pool));
653 SVN_ERR(svn_ra_change_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_UUID,
654 svn_string_create(uuid, pool), pool));
656 SVN_ERR(svn_ra_change_rev_prop(to_session, 0, SVNSYNC_PROP_LAST_MERGED_REV,
657 svn_string_create("0", pool), pool));
659 /* Finally, copy all non-svnsync revprops from rev 0 of the source
660 repos into the dest repos. */
662 SVN_ERR(copy_revprops(from_session, to_session, 0, FALSE,
663 baton->quiet, pool));
665 /* TODO: It would be nice if we could set the dest repos UUID to be
666 equal to the UUID of the source repos, at least optionally. That
667 way people could check out/log/diff using a local fast mirror,
668 but switch --relocate to the actual final repository in order to
669 make changes... But at this time, the RA layer doesn't have a
670 way to set a UUID. */
672 return SVN_NO_ERROR;
676 /* SUBCOMMAND: init */
677 static svn_error_t *
678 initialize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
680 const char *to_url, *from_url;
681 svn_ra_session_t *to_session;
682 opt_baton_t *opt_baton = b;
683 apr_array_header_t *targets;
684 subcommand_baton_t *baton;
686 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
687 apr_array_make(pool, 0,
688 sizeof(const char *)),
689 pool));
690 if (targets->nelts < 2)
691 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
692 if (targets->nelts > 2)
693 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
695 to_url = APR_ARRAY_IDX(targets, 0, const char *);
696 from_url = APR_ARRAY_IDX(targets, 1, const char *);
698 if (! svn_path_is_url(to_url))
699 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
700 _("Path '%s' is not a URL"), to_url);
701 if (! svn_path_is_url(from_url))
702 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
703 _("Path '%s' is not a URL"), from_url);
705 baton = make_subcommand_baton(opt_baton, to_url, from_url, 0, 0, pool);
706 SVN_ERR(svn_ra_open3(&to_session, baton->to_url, NULL,
707 &(baton->sync_callbacks), baton, baton->config, pool));
708 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
709 SVN_ERR(with_locked(to_session, do_initialize, baton, pool));
711 return SVN_NO_ERROR;
716 /*** Synchronization Editor ***/
718 /* This editor has a couple of jobs.
720 * First, it needs to filter out the propchanges that can't be passed over
721 * libsvn_ra.
723 * Second, it needs to adjust for the fact that we might not actually have
724 * permission to see all of the data from the remote repository, which means
725 * we could get revisions that are totally empty from our point of view.
727 * Third, it needs to adjust copyfrom paths, adding the root url for the
728 * destination repository to the beginning of them.
732 /* Edit baton */
733 typedef struct {
734 const svn_delta_editor_t *wrapped_editor;
735 void *wrapped_edit_baton;
736 const char *to_url; /* URL we're copying into, for correct copyfrom URLs */
737 svn_boolean_t called_open_root;
738 svn_boolean_t got_textdeltas;
739 svn_revnum_t base_revision;
740 svn_boolean_t quiet;
741 svn_boolean_t strip_mergeinfo; /* Are we stripping svn:mergeinfo? */
742 svn_boolean_t migrate_svnmerge; /* Are we converting svnmerge.py data? */
743 svn_boolean_t mergeinfo_stripped; /* Did we strip svn:mergeinfo? */
744 svn_boolean_t svnmerge_migrated; /* Did we convert svnmerge.py data? */
745 svn_boolean_t svnmerge_blocked; /* Was there any blocked svnmerge data? */
746 } edit_baton_t;
749 /* A dual-purpose baton for files and directories. */
750 typedef struct {
751 void *edit_baton;
752 void *wrapped_node_baton;
753 } node_baton_t;
756 /*** Editor vtable functions ***/
758 static svn_error_t *
759 set_target_revision(void *edit_baton,
760 svn_revnum_t target_revision,
761 apr_pool_t *pool)
763 edit_baton_t *eb = edit_baton;
764 return eb->wrapped_editor->set_target_revision(eb->wrapped_edit_baton,
765 target_revision, pool);
768 static svn_error_t *
769 open_root(void *edit_baton,
770 svn_revnum_t base_revision,
771 apr_pool_t *pool,
772 void **root_baton)
774 edit_baton_t *eb = edit_baton;
775 node_baton_t *dir_baton = apr_palloc(pool, sizeof(*dir_baton));
777 SVN_ERR(eb->wrapped_editor->open_root(eb->wrapped_edit_baton,
778 base_revision, pool,
779 &dir_baton->wrapped_node_baton));
781 eb->called_open_root = TRUE;
782 dir_baton->edit_baton = edit_baton;
783 *root_baton = dir_baton;
785 return SVN_NO_ERROR;
788 static svn_error_t *
789 delete_entry(const char *path,
790 svn_revnum_t base_revision,
791 void *parent_baton,
792 apr_pool_t *pool)
794 node_baton_t *pb = parent_baton;
795 edit_baton_t *eb = pb->edit_baton;
797 return eb->wrapped_editor->delete_entry(path, base_revision,
798 pb->wrapped_node_baton, pool);
801 static svn_error_t *
802 add_directory(const char *path,
803 void *parent_baton,
804 const char *copyfrom_path,
805 svn_revnum_t copyfrom_rev,
806 apr_pool_t *pool,
807 void **child_baton)
809 node_baton_t *pb = parent_baton;
810 edit_baton_t *eb = pb->edit_baton;
811 node_baton_t *b = apr_palloc(pool, sizeof(*b));
813 if (copyfrom_path)
814 copyfrom_path = apr_psprintf(pool, "%s%s", eb->to_url,
815 svn_path_uri_encode(copyfrom_path, pool));
817 SVN_ERR(eb->wrapped_editor->add_directory(path, pb->wrapped_node_baton,
818 copyfrom_path,
819 copyfrom_rev, pool,
820 &b->wrapped_node_baton));
822 b->edit_baton = eb;
823 *child_baton = b;
825 return SVN_NO_ERROR;
828 static svn_error_t *
829 open_directory(const char *path,
830 void *parent_baton,
831 svn_revnum_t base_revision,
832 apr_pool_t *pool,
833 void **child_baton)
835 node_baton_t *pb = parent_baton;
836 edit_baton_t *eb = pb->edit_baton;
837 node_baton_t *db = apr_palloc(pool, sizeof(*db));
839 SVN_ERR(eb->wrapped_editor->open_directory(path, pb->wrapped_node_baton,
840 base_revision, pool,
841 &db->wrapped_node_baton));
843 db->edit_baton = eb;
844 *child_baton = db;
846 return SVN_NO_ERROR;
849 static svn_error_t *
850 add_file(const char *path,
851 void *parent_baton,
852 const char *copyfrom_path,
853 svn_revnum_t copyfrom_rev,
854 apr_pool_t *pool,
855 void **file_baton)
857 node_baton_t *pb = parent_baton;
858 edit_baton_t *eb = pb->edit_baton;
859 node_baton_t *fb = apr_palloc(pool, sizeof(*fb));
861 if (copyfrom_path)
862 copyfrom_path = apr_psprintf(pool, "%s%s", eb->to_url,
863 svn_path_uri_encode(copyfrom_path, pool));
865 SVN_ERR(eb->wrapped_editor->add_file(path, pb->wrapped_node_baton,
866 copyfrom_path, copyfrom_rev,
867 pool, &fb->wrapped_node_baton));
869 fb->edit_baton = eb;
870 *file_baton = fb;
872 return SVN_NO_ERROR;
875 static svn_error_t *
876 open_file(const char *path,
877 void *parent_baton,
878 svn_revnum_t base_revision,
879 apr_pool_t *pool,
880 void **file_baton)
882 node_baton_t *pb = parent_baton;
883 edit_baton_t *eb = pb->edit_baton;
884 node_baton_t *fb = apr_palloc(pool, sizeof(*fb));
886 SVN_ERR(eb->wrapped_editor->open_file(path, pb->wrapped_node_baton,
887 base_revision, pool,
888 &fb->wrapped_node_baton));
890 fb->edit_baton = eb;
891 *file_baton = fb;
893 return SVN_NO_ERROR;
896 static svn_error_t *
897 apply_textdelta(void *file_baton,
898 const char *base_checksum,
899 apr_pool_t *pool,
900 svn_txdelta_window_handler_t *handler,
901 void **handler_baton)
903 node_baton_t *fb = file_baton;
904 edit_baton_t *eb = fb->edit_baton;
906 if (! eb->quiet)
908 if (! eb->got_textdeltas)
909 SVN_ERR(svn_cmdline_printf(pool, _("Transmitting file data ")));
910 SVN_ERR(svn_cmdline_printf(pool, "."));
911 SVN_ERR(svn_cmdline_fflush(stdout));
914 eb->got_textdeltas = TRUE;
915 return eb->wrapped_editor->apply_textdelta(fb->wrapped_node_baton,
916 base_checksum, pool,
917 handler, handler_baton);
920 static svn_error_t *
921 close_file(void *file_baton,
922 const char *text_checksum,
923 apr_pool_t *pool)
925 node_baton_t *fb = file_baton;
926 edit_baton_t *eb = fb->edit_baton;
927 return eb->wrapped_editor->close_file(fb->wrapped_node_baton,
928 text_checksum, pool);
931 static svn_error_t *
932 absent_file(const char *path,
933 void *file_baton,
934 apr_pool_t *pool)
936 node_baton_t *fb = file_baton;
937 edit_baton_t *eb = fb->edit_baton;
938 return eb->wrapped_editor->absent_file(path, fb->wrapped_node_baton, pool);
941 static svn_error_t *
942 close_directory(void *dir_baton,
943 apr_pool_t *pool)
945 node_baton_t *db = dir_baton;
946 edit_baton_t *eb = db->edit_baton;
947 return eb->wrapped_editor->close_directory(db->wrapped_node_baton, pool);
950 static svn_error_t *
951 absent_directory(const char *path,
952 void *dir_baton,
953 apr_pool_t *pool)
955 node_baton_t *db = dir_baton;
956 edit_baton_t *eb = db->edit_baton;
957 return eb->wrapped_editor->absent_directory(path, db->wrapped_node_baton,
958 pool);
961 static svn_error_t *
962 change_file_prop(void *file_baton,
963 const char *name,
964 const svn_string_t *value,
965 apr_pool_t *pool)
967 node_baton_t *fb = file_baton;
968 edit_baton_t *eb = fb->edit_baton;
970 /* only regular properties can pass over libsvn_ra */
971 if (svn_property_kind(NULL, name) != svn_prop_regular_kind)
972 return SVN_NO_ERROR;
974 /* Maybe drop svn:mergeinfo. */
975 if (eb->strip_mergeinfo && (strcmp(name, SVN_PROP_MERGEINFO) == 0))
977 eb->mergeinfo_stripped = TRUE;
978 return SVN_NO_ERROR;
981 /* Maybe drop (errantly set, as this is a file) svnmerge.py properties. */
982 if (eb->migrate_svnmerge && (strcmp(name, "svnmerge-integrated") == 0))
984 eb->svnmerge_migrated = TRUE;
985 return SVN_NO_ERROR;
988 /* Remember if we see any svnmerge-blocked properties. (They really
989 shouldn't be here, as this is a file, but whatever...) */
990 if (eb->migrate_svnmerge && (strcmp(name, "svnmerge-blocked") == 0))
992 eb->svnmerge_blocked = TRUE;
995 return eb->wrapped_editor->change_file_prop(fb->wrapped_node_baton,
996 name, value, pool);
999 static svn_error_t *
1000 change_dir_prop(void *dir_baton,
1001 const char *name,
1002 const svn_string_t *value,
1003 apr_pool_t *pool)
1005 node_baton_t *db = dir_baton;
1006 edit_baton_t *eb = db->edit_baton;
1007 svn_string_t *real_value = (svn_string_t *)value;
1009 /* Only regular properties can pass over libsvn_ra */
1010 if (svn_property_kind(NULL, name) != svn_prop_regular_kind)
1011 return SVN_NO_ERROR;
1013 /* Maybe drop svn:mergeinfo. */
1014 if (eb->strip_mergeinfo && (strcmp(name, SVN_PROP_MERGEINFO) == 0))
1016 eb->mergeinfo_stripped = TRUE;
1017 return SVN_NO_ERROR;
1020 /* Maybe convert svnmerge-integrated data into svn:mergeinfo. (We
1021 ignore svnmerge-blocked for now.) */
1022 /* ### FIXME: Consult the mirror repository's HEAD prop values and
1023 ### merge svn:mergeinfo, svnmerge-integrated, and svnmerge-blocked. */
1024 if (eb->migrate_svnmerge && (strcmp(name, "svnmerge-integrated") == 0))
1026 if (value)
1028 /* svnmerge-integrated differs from svn:mergeinfo in a pair
1029 of ways. First, it can use tabs, newlines, or spaces to
1030 delimit source information. Secondly, the source paths
1031 are relative URLs, whereas svn:mergeinfo uses relative
1032 paths (not URI-encoded). */
1033 svn_error_t *err;
1034 svn_stringbuf_t *mergeinfo_buf = svn_stringbuf_create("", pool);
1035 svn_mergeinfo_t mergeinfo;
1036 int i;
1037 apr_array_header_t *sources =
1038 svn_cstring_split(value->data, " \t\n", TRUE, pool);
1040 for (i = 0; i < sources->nelts; i++)
1042 const char *rel_path;
1043 apr_array_header_t *path_revs =
1044 svn_cstring_split(APR_ARRAY_IDX(sources, i, const char *),
1045 ":", TRUE, pool);
1047 /* ### TODO: Warn? */
1048 if (path_revs->nelts != 2)
1049 continue;
1051 /* Append this source's mergeinfo data. */
1052 rel_path = APR_ARRAY_IDX(path_revs, 0, const char *);
1053 rel_path = svn_path_uri_decode(rel_path, pool);
1054 svn_stringbuf_appendcstr(mergeinfo_buf, rel_path);
1055 svn_stringbuf_appendcstr(mergeinfo_buf, ":");
1056 svn_stringbuf_appendcstr(mergeinfo_buf,
1057 APR_ARRAY_IDX(path_revs, 1,
1058 const char *));
1059 svn_stringbuf_appendcstr(mergeinfo_buf, "\n");
1062 /* Try to parse the mergeinfo string we've created, just to
1063 check for bogosity. If all goes well, we'll unparse it
1064 again and use that as our property value. */
1065 err = svn_mergeinfo_parse(&mergeinfo, mergeinfo_buf->data, pool);
1066 if (err)
1068 svn_error_clear(err);
1069 return SVN_NO_ERROR;
1071 SVN_ERR(svn_mergeinfo_to_string(&real_value, mergeinfo, pool));
1073 name = SVN_PROP_MERGEINFO;
1074 eb->svnmerge_migrated = TRUE;
1077 /* Remember if we see any svnmerge-blocked properties. */
1078 if (eb->migrate_svnmerge && (strcmp(name, "svnmerge-blocked") == 0))
1080 eb->svnmerge_blocked = TRUE;
1083 return eb->wrapped_editor->change_dir_prop(db->wrapped_node_baton,
1084 name, real_value, pool);
1087 static svn_error_t *
1088 close_edit(void *edit_baton,
1089 apr_pool_t *pool)
1091 edit_baton_t *eb = edit_baton;
1093 /* If we haven't opened the root yet, that means we're transfering
1094 an empty revision, probably because we aren't allowed to see the
1095 contents for some reason. In any event, we need to open the root
1096 and close it again, before we can close out the edit, or the
1097 commit will fail. */
1099 if (! eb->called_open_root)
1101 void *baton;
1102 SVN_ERR(eb->wrapped_editor->open_root(eb->wrapped_edit_baton,
1103 eb->base_revision, pool,
1104 &baton));
1105 SVN_ERR(eb->wrapped_editor->close_directory(baton, pool));
1108 if (! eb->quiet)
1110 if (eb->got_textdeltas)
1111 SVN_ERR(svn_cmdline_printf(pool, "\n"));
1112 if (eb->mergeinfo_stripped)
1113 SVN_ERR(svn_cmdline_printf(pool,
1114 "NOTE: Dropped Subversion mergeinfo "
1115 "from this revision.\n"));
1116 if (eb->svnmerge_migrated)
1117 SVN_ERR(svn_cmdline_printf(pool,
1118 "NOTE: Migrated 'svnmerge-integrated' in "
1119 "this revision.\n"));
1120 if (eb->svnmerge_blocked)
1121 SVN_ERR(svn_cmdline_printf(pool,
1122 "NOTE: Saw 'svnmerge-blocked' in this "
1123 "revision (but didn't migrate it).\n"));
1126 return eb->wrapped_editor->close_edit(eb->wrapped_edit_baton, pool);
1129 /*** Editor factory function ***/
1131 /* Set WRAPPED_EDITOR and WRAPPED_EDIT_BATON to an editor/baton pair
1132 * that wraps our own commit EDITOR/EDIT_BATON. BASE_REVISION is the
1133 * revision on which the driver of this returned editor will be basing
1134 * the commit. TO_URL is the URL of the root of the repository into
1135 * which the commit is being made.
1137 static svn_error_t *
1138 get_sync_editor(const svn_delta_editor_t *wrapped_editor,
1139 void *wrapped_edit_baton,
1140 svn_revnum_t base_revision,
1141 const char *to_url,
1142 svn_boolean_t quiet,
1143 const svn_delta_editor_t **editor,
1144 void **edit_baton,
1145 apr_pool_t *pool)
1147 svn_delta_editor_t *tree_editor = svn_delta_default_editor(pool);
1148 edit_baton_t *eb = apr_pcalloc(pool, sizeof(*eb));
1150 tree_editor->set_target_revision = set_target_revision;
1151 tree_editor->open_root = open_root;
1152 tree_editor->delete_entry = delete_entry;
1153 tree_editor->add_directory = add_directory;
1154 tree_editor->open_directory = open_directory;
1155 tree_editor->change_dir_prop = change_dir_prop;
1156 tree_editor->close_directory = close_directory;
1157 tree_editor->absent_directory = absent_directory;
1158 tree_editor->add_file = add_file;
1159 tree_editor->open_file = open_file;
1160 tree_editor->apply_textdelta = apply_textdelta;
1161 tree_editor->change_file_prop = change_file_prop;
1162 tree_editor->close_file = close_file;
1163 tree_editor->absent_file = absent_file;
1164 tree_editor->close_edit = close_edit;
1166 eb->wrapped_editor = wrapped_editor;
1167 eb->wrapped_edit_baton = wrapped_edit_baton;
1168 eb->base_revision = base_revision;
1169 eb->to_url = to_url;
1170 eb->quiet = quiet;
1172 if (getenv("SVNSYNC_UNSUPPORTED_STRIP_MERGEINFO"))
1174 eb->strip_mergeinfo = TRUE;
1176 if (getenv("SVNSYNC_UNSUPPORTED_MIGRATE_SVNMERGE"))
1178 /* Current we can't merge property values. That's only possible
1179 if all the properties to be merged were always modified in
1180 exactly the same revisions, or if we allow ourselves to
1181 lookup the current state of properties in the sync
1182 destination. So for now, migrating svnmerge.py data implies
1183 stripping pre-existing svn:mergeinfo. */
1184 /* ### FIXME: Do a real migration by consulting the mirror
1185 ### repository's HEAD propvalues and merging svn:mergeinfo,
1186 ### svnmerge-integrated, and svnmerge-blocked together. */
1187 eb->migrate_svnmerge = TRUE;
1188 eb->strip_mergeinfo = TRUE;
1191 *editor = tree_editor;
1192 *edit_baton = eb;
1194 return SVN_NO_ERROR;
1199 /*** `svnsync sync' ***/
1201 /* Implements `svn_commit_callback2_t' interface. */
1202 static svn_error_t *
1203 commit_callback(const svn_commit_info_t *commit_info,
1204 void *baton,
1205 apr_pool_t *pool)
1207 subcommand_baton_t *sb = baton;
1209 if (! sb->quiet)
1211 SVN_ERR(svn_cmdline_printf(pool, _("Committed revision %ld.\n"),
1212 commit_info->revision));
1215 sb->committed_rev = commit_info->revision;
1217 return SVN_NO_ERROR;
1221 /* Set *FROM_SESSION to an RA session associated with the source
1222 * repository of the synchronization, as determined by reading
1223 * svn:sync- properties from the destination repository (associated
1224 * with TO_SESSION). Set LAST_MERGED_REV to the value of the property
1225 * which records the most recently synchronized revision.
1227 * CALLBACKS is a vtable of RA callbacks to provide when creating
1228 * *FROM_SESSION. CONFIG is a configuration hash.
1230 static svn_error_t *
1231 open_source_session(svn_ra_session_t **from_session,
1232 svn_string_t **last_merged_rev,
1233 svn_ra_session_t *to_session,
1234 svn_ra_callbacks2_t *callbacks,
1235 apr_hash_t *config,
1236 void *baton,
1237 apr_pool_t *pool)
1239 svn_string_t *from_url, *from_uuid;
1241 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL,
1242 &from_url, pool));
1243 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_UUID,
1244 &from_uuid, pool));
1245 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_LAST_MERGED_REV,
1246 last_merged_rev, pool));
1248 if (! from_url || ! from_uuid || ! *last_merged_rev)
1249 return svn_error_create
1250 (APR_EINVAL, NULL,
1251 _("Destination repository has not been initialized"));
1253 /* Open the session to copy the revision data. */
1254 SVN_ERR(svn_ra_open3(from_session, from_url->data, from_uuid->data,
1255 callbacks, baton, config, pool));
1257 return SVN_NO_ERROR;
1260 /* Replay baton, used during sychnronization. */
1261 typedef struct {
1262 svn_ra_session_t *from_session;
1263 svn_ra_session_t *to_session;
1264 subcommand_baton_t *sb;
1265 svn_boolean_t has_commit_revprops_capability;
1266 } replay_baton_t;
1268 /* Return a replay baton allocated from POOL and populated with
1269 data from the provided parameters. */
1270 static replay_baton_t *
1271 make_replay_baton(svn_ra_session_t *from_session,
1272 svn_ra_session_t *to_session,
1273 subcommand_baton_t *sb, apr_pool_t *pool)
1275 replay_baton_t *rb = apr_pcalloc(pool, sizeof(*rb));
1276 rb->from_session = from_session;
1277 rb->to_session = to_session;
1278 rb->sb = sb;
1279 return rb;
1282 /* Filter out svn:date and svn:author properties. */
1283 static svn_boolean_t
1284 filter_exclude_date_author_sync(const char *key)
1286 if (strcmp(key, SVN_PROP_REVISION_AUTHOR) == 0)
1287 return TRUE;
1288 else if (strcmp(key, SVN_PROP_REVISION_DATE) == 0)
1289 return TRUE;
1290 else if (strncmp(key, SVNSYNC_PROP_PREFIX,
1291 sizeof(SVNSYNC_PROP_PREFIX) - 1) == 0)
1292 return TRUE;
1294 return FALSE;
1297 /* Filter out all properties except svn:date and svn:author */
1298 static svn_boolean_t
1299 filter_include_date_author_sync(const char *key)
1301 return ! filter_exclude_date_author_sync(key);
1305 /* Only exclude svn:log .*/
1306 static svn_boolean_t
1307 filter_exclude_log(const char *key)
1309 if (strcmp(key, SVN_PROP_REVISION_LOG) == 0)
1310 return TRUE;
1311 else
1312 return FALSE;
1315 /* Only include svn:log. */
1316 static svn_boolean_t
1317 filter_include_log(const char *key)
1319 return ! filter_exclude_log(key);
1323 /* Callback function for svn_ra_replay_range, invoked when starting to parse
1324 * a replay report.
1326 static svn_error_t *
1327 replay_rev_started(svn_revnum_t revision,
1328 void *replay_baton,
1329 const svn_delta_editor_t **editor,
1330 void **edit_baton,
1331 apr_hash_t *rev_props,
1332 apr_pool_t *pool)
1334 const svn_delta_editor_t *commit_editor;
1335 const svn_delta_editor_t *cancel_editor;
1336 const svn_delta_editor_t *sync_editor;
1337 void *commit_baton;
1338 void *cancel_baton;
1339 void *sync_baton;
1340 replay_baton_t *rb = replay_baton;
1341 apr_hash_t *filtered;
1342 int filtered_count;
1344 /* We set this property so that if we error out for some reason
1345 we can later determine where we were in the process of
1346 merging a revision. If we had committed the change, but we
1347 hadn't finished copying the revprops we need to know that, so
1348 we can go back and finish the job before we move on.
1350 NOTE: We have to set this before we start the commit editor,
1351 because ra_svn doesn't let you change rev props during a
1352 commit. */
1353 SVN_ERR(svn_ra_change_rev_prop(rb->to_session, 0,
1354 SVNSYNC_PROP_CURRENTLY_COPYING,
1355 svn_string_createf(pool, "%ld",
1356 revision),
1357 pool));
1359 /* The actual copy is just a replay hooked up to a commit. Include
1360 all the revision properties from the source repositories, except
1361 'svn:author' and 'svn:date', those are not guaranteed to get
1362 through the editor anyway.
1363 If we're syncing to an non-commit-revprops capable server, filter
1364 out all revprops except svn:log and add them later in
1365 revplay_rev_finished. */
1366 filtered = filter_props(&filtered_count, rev_props,
1367 (rb->has_commit_revprops_capability
1368 ? filter_exclude_date_author_sync
1369 : filter_include_log),
1370 pool);
1372 /* svn_ra_get_commit_editor3 requires the log message to be
1373 set. It's possible that we didn't receive 'svn:log' here, so we
1374 have to set it to at least the empty string. If there's a svn:log
1375 property on this revision, we will write the actual value in the
1376 replay_rev_finished callback. */
1377 if (! apr_hash_get(filtered, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING))
1378 apr_hash_set(filtered, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING,
1379 svn_string_create("", pool));
1381 SVN_ERR(svn_ra_get_commit_editor3(rb->to_session, &commit_editor,
1382 &commit_baton,
1383 filtered,
1384 commit_callback, rb->sb,
1385 NULL, FALSE, pool));
1387 /* There's one catch though, the diff shows us props we can't send
1388 over the RA interface, so we need an editor that's smart enough
1389 to filter those out for us. */
1390 SVN_ERR(get_sync_editor(commit_editor, commit_baton, revision - 1,
1391 rb->sb->to_url, rb->sb->quiet,
1392 &sync_editor, &sync_baton, pool));
1394 SVN_ERR(svn_delta_get_cancellation_editor(check_cancel, NULL,
1395 sync_editor, sync_baton,
1396 &cancel_editor,
1397 &cancel_baton,
1398 pool));
1399 *editor = cancel_editor;
1400 *edit_baton = cancel_baton;
1402 return SVN_NO_ERROR;
1405 /* Callback function for svn_ra_replay_range, invoked when finishing parsing
1406 * a replay report.
1408 static svn_error_t *
1409 replay_rev_finished(svn_revnum_t revision,
1410 void *replay_baton,
1411 const svn_delta_editor_t *editor,
1412 void *edit_baton,
1413 apr_hash_t *rev_props,
1414 apr_pool_t *pool)
1416 apr_pool_t *subpool = svn_pool_create(pool);
1417 replay_baton_t *rb = replay_baton;
1418 apr_hash_t *filtered, *existing_props;
1419 int filtered_count;
1421 SVN_ERR(editor->close_edit(edit_baton, pool));
1423 /* Sanity check that we actually committed the revision we meant to. */
1424 if (rb->sb->committed_rev != revision)
1425 return svn_error_createf
1426 (APR_EINVAL, NULL,
1427 _("Commit created rev %ld but should have created %ld"),
1428 rb->sb->committed_rev, revision);
1430 SVN_ERR(svn_ra_rev_proplist(rb->to_session, revision, &existing_props,
1431 subpool));
1434 /* Ok, we're done with the data, now we just need to copy the remaining
1435 'svn:date' and 'svn:author' revprops and we're all set.
1436 If the server doesn't support revprops-in-a-commit, we still have to
1437 set all revision properties except svn:log. */
1438 filtered = filter_props(&filtered_count, rev_props,
1439 (rb->has_commit_revprops_capability
1440 ? filter_include_date_author_sync
1441 : filter_exclude_log),
1442 subpool);
1443 SVN_ERR(write_revprops(&filtered_count, rb->to_session, revision, filtered,
1444 subpool));
1446 /* Remove all extra properties in TARGET. */
1447 SVN_ERR(remove_props_not_in_source(rb->to_session, revision,
1448 rev_props, existing_props, subpool));
1450 svn_pool_clear(subpool);
1452 /* Ok, we're done, bring the last-merged-rev property up to date. */
1453 SVN_ERR(svn_ra_change_rev_prop
1454 (rb->to_session,
1456 SVNSYNC_PROP_LAST_MERGED_REV,
1457 svn_string_create(apr_psprintf(pool, "%ld", revision),
1458 subpool),
1459 subpool));
1461 /* And finally drop the currently copying prop, since we're done
1462 with this revision. */
1463 SVN_ERR(svn_ra_change_rev_prop(rb->to_session, 0,
1464 SVNSYNC_PROP_CURRENTLY_COPYING,
1465 NULL, subpool));
1467 /* Notify the user that we copied revision properties. */
1468 if (! rb->sb->quiet)
1469 SVN_ERR(log_properties_copied(filtered_count > 0, revision, subpool));
1471 svn_pool_destroy(subpool);
1473 return SVN_NO_ERROR;
1476 /* Synchronize the repository associated with RA session TO_SESSION,
1477 * using information found in baton B, while the repository is
1478 * locked. Implements `with_locked_func_t' interface.
1480 static svn_error_t *
1481 do_synchronize(svn_ra_session_t *to_session,
1482 subcommand_baton_t *baton, apr_pool_t *pool)
1484 svn_string_t *last_merged_rev;
1485 svn_revnum_t from_latest;
1486 svn_ra_session_t *from_session;
1487 svn_string_t *currently_copying;
1488 svn_revnum_t to_latest, copying, last_merged;
1489 svn_revnum_t start_revision, end_revision;
1490 replay_baton_t *rb;
1492 SVN_ERR(open_source_session(&from_session,
1493 &last_merged_rev, to_session,
1494 &(baton->source_callbacks), baton->config,
1495 baton, pool));
1497 /* Check to see if we have revprops that still need to be copied for
1498 a prior revision we didn't finish copying. But first, check for
1499 state sanity. Remember, mirroring is not an atomic action,
1500 because revision properties are copied separately from the
1501 revision's contents.
1503 So, any time that currently-copying is not set, then
1504 last-merged-rev should be the HEAD revision of the destination
1505 repository. That is, if we didn't fall over in the middle of a
1506 previous synchronization, then our destination repository should
1507 have exactly as many revisions in it as we've synchronized.
1509 Alternately, if currently-copying *is* set, it must
1510 be either last-merged-rev or last-merged-rev + 1, and the HEAD
1511 revision must be equal to either last-merged-rev or
1512 currently-copying. If this is not the case, somebody has meddled
1513 with the destination without using svnsync.
1516 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_CURRENTLY_COPYING,
1517 &currently_copying, pool));
1519 SVN_ERR(svn_ra_get_latest_revnum(to_session, &to_latest, pool));
1521 last_merged = SVN_STR_TO_REV(last_merged_rev->data);
1523 if (currently_copying)
1525 copying = SVN_STR_TO_REV(currently_copying->data);
1527 if ((copying < last_merged)
1528 || (copying > (last_merged + 1))
1529 || ((to_latest != last_merged) && (to_latest != copying)))
1531 return svn_error_createf
1532 (APR_EINVAL, NULL,
1533 _("Revision being currently copied (%ld), last merged revision "
1534 "(%ld), and destination HEAD (%ld) are inconsistent; have you "
1535 "committed to the destination without using svnsync?"),
1536 copying, last_merged, to_latest);
1538 else if (copying == to_latest)
1540 if (copying > last_merged)
1542 SVN_ERR(copy_revprops(from_session, to_session,
1543 to_latest, TRUE, baton->quiet,
1544 pool));
1545 last_merged = copying;
1546 last_merged_rev = svn_string_create
1547 (apr_psprintf(pool, "%ld", last_merged), pool);
1550 /* Now update last merged rev and drop currently changing.
1551 Note that the order here is significant, if we do them
1552 in the wrong order there are race conditions where we
1553 end up not being able to tell if there have been bogus
1554 (i.e. non-svnsync) commits to the dest repository. */
1556 SVN_ERR(svn_ra_change_rev_prop(to_session, 0,
1557 SVNSYNC_PROP_LAST_MERGED_REV,
1558 last_merged_rev, pool));
1559 SVN_ERR(svn_ra_change_rev_prop(to_session, 0,
1560 SVNSYNC_PROP_CURRENTLY_COPYING,
1561 NULL, pool));
1563 /* If copying > to_latest, then we just fall through to
1564 attempting to copy the revision again. */
1566 else
1568 if (to_latest != last_merged)
1569 return svn_error_createf(APR_EINVAL, NULL,
1570 _("Destination HEAD (%ld) is not the last "
1571 "merged revision (%ld); have you "
1572 "committed to the destination without "
1573 "using svnsync?"),
1574 to_latest, last_merged);
1577 /* Now check to see if there are any revisions to copy. */
1578 SVN_ERR(svn_ra_get_latest_revnum(from_session, &from_latest, pool));
1580 if (from_latest < atol(last_merged_rev->data))
1581 return SVN_NO_ERROR;
1583 /* Ok, so there are new revisions, iterate over them copying them
1584 into the destination repository. */
1585 rb = make_replay_baton(from_session, to_session, baton, pool);
1587 /* For compatibility with older svnserve versions, check first if we
1588 support adding revprops to the commit. */
1589 SVN_ERR(svn_ra_has_capability(rb->to_session,
1590 &rb->has_commit_revprops_capability,
1591 SVN_RA_CAPABILITY_COMMIT_REVPROPS,
1592 pool));
1594 start_revision = atol(last_merged_rev->data) + 1;
1595 end_revision = from_latest;
1597 SVN_ERR(check_cancel(NULL));
1599 SVN_ERR(svn_ra_replay_range(from_session, start_revision, end_revision,
1600 0, TRUE, replay_rev_started,
1601 replay_rev_finished, rb, pool));
1603 return SVN_NO_ERROR;
1607 /* SUBCOMMAND: sync */
1608 static svn_error_t *
1609 synchronize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
1611 svn_ra_session_t *to_session;
1612 opt_baton_t *opt_baton = b;
1613 apr_array_header_t *targets;
1614 subcommand_baton_t *baton;
1615 const char *to_url;
1617 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
1618 apr_array_make(pool, 0,
1619 sizeof(const char *)),
1620 pool));
1621 if (targets->nelts < 1)
1622 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1623 if (targets->nelts > 1)
1624 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1626 to_url = APR_ARRAY_IDX(targets, 0, const char *);
1628 if (! svn_path_is_url(to_url))
1629 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1630 _("Path '%s' is not a URL"), to_url);
1632 baton = make_subcommand_baton(opt_baton, to_url, NULL, 0, 0, pool);
1633 SVN_ERR(svn_ra_open3(&to_session, baton->to_url, NULL,
1634 &(baton->sync_callbacks), baton, baton->config, pool));
1635 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
1636 SVN_ERR(with_locked(to_session, do_synchronize, baton, pool));
1638 return SVN_NO_ERROR;
1643 /*** `svnsync copy-revprops' ***/
1645 /* Copy revision properties to the repository associated with RA
1646 * session TO_SESSION, using information found in baton B, while the
1647 * repository is locked. Implements `with_locked_func_t' interface.
1649 static svn_error_t *
1650 do_copy_revprops(svn_ra_session_t *to_session,
1651 subcommand_baton_t *baton, apr_pool_t *pool)
1653 svn_ra_session_t *from_session;
1654 svn_string_t *last_merged_rev;
1655 svn_revnum_t i;
1656 svn_revnum_t step = 1;
1658 SVN_ERR(open_source_session(&from_session, &last_merged_rev,
1659 to_session,
1660 &(baton->source_callbacks), baton->config,
1661 baton, pool));
1663 /* An invalid revision means "last-synced" */
1664 if (! SVN_IS_VALID_REVNUM(baton->start_rev))
1665 baton->start_rev = SVN_STR_TO_REV(last_merged_rev->data);
1666 if (! SVN_IS_VALID_REVNUM(baton->end_rev))
1667 baton->end_rev = SVN_STR_TO_REV(last_merged_rev->data);
1669 /* Make sure we have revisions within the valid range. */
1670 if (baton->start_rev > SVN_STR_TO_REV(last_merged_rev->data))
1671 return svn_error_createf
1672 (APR_EINVAL, NULL,
1673 _("Cannot copy revprops for a revision (%ld) that has not "
1674 "been synchronized yet"), baton->start_rev);
1675 if (baton->end_rev > SVN_STR_TO_REV(last_merged_rev->data))
1676 return svn_error_createf
1677 (APR_EINVAL, NULL,
1678 _("Cannot copy revprops for a revision (%ld) that has not "
1679 "been synchronized yet"), baton->end_rev);
1681 /* Now, copy all the requested revisions, in the requested order. */
1682 step = (baton->start_rev > baton->end_rev) ? -1 : 1;
1683 for (i = baton->start_rev; i != baton->end_rev + step; i = i + step)
1685 SVN_ERR(check_cancel(NULL));
1686 SVN_ERR(copy_revprops(from_session, to_session, i, FALSE,
1687 baton->quiet, pool));
1690 return SVN_NO_ERROR;
1694 /* SUBCOMMAND: copy-revprops */
1695 static svn_error_t *
1696 copy_revprops_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
1698 svn_ra_session_t *to_session;
1699 opt_baton_t *opt_baton = b;
1700 apr_array_header_t *targets;
1701 subcommand_baton_t *baton;
1702 const char *to_url;
1703 svn_opt_revision_t start_revision, end_revision;
1704 svn_revnum_t start_rev = 0, end_rev = SVN_INVALID_REVNUM;
1706 /* There should be either one or two arguments left to parse. */
1707 if (os->argc - os->ind > 2)
1708 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1709 if (os->argc - os->ind < 1)
1710 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1712 /* If there are two args, the last one is a revision range. We'll
1713 effectively pop it from the end of the list. Why? Because
1714 svn_opt_args_to_target_array2() does waaaaay too many useful
1715 things for us not to use it. */
1716 if (os->argc - os->ind == 2)
1718 const char *rev_str = os->argv[--(os->argc)];
1720 start_revision.kind = svn_opt_revision_unspecified;
1721 end_revision.kind = svn_opt_revision_unspecified;
1722 if ((svn_opt_parse_revision(&start_revision, &end_revision,
1723 rev_str, pool) != 0)
1724 || ((start_revision.kind != svn_opt_revision_number)
1725 && (start_revision.kind != svn_opt_revision_head))
1726 || ((end_revision.kind != svn_opt_revision_number)
1727 && (end_revision.kind != svn_opt_revision_head)
1728 && (end_revision.kind != svn_opt_revision_unspecified)))
1729 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1730 _("'%s' is not a valid revision range"),
1731 rev_str);
1733 /* Get the start revision, which must be either HEAD or a number
1734 (which is required to be a valid one). */
1735 if (start_revision.kind == svn_opt_revision_head)
1737 start_rev = SVN_INVALID_REVNUM;
1739 else
1741 start_rev = start_revision.value.number;
1742 if (! SVN_IS_VALID_REVNUM(start_rev))
1743 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1744 _("Invalid revision number (%ld)"),
1745 start_rev);
1748 /* Get the end revision, which must be unspecified (meaning,
1749 "same as the start_rev"), HEAD, or a number (which is
1750 required to be a valid one). */
1751 if (end_revision.kind == svn_opt_revision_unspecified)
1753 end_rev = start_rev;
1755 else if (end_revision.kind == svn_opt_revision_head)
1757 end_rev = SVN_INVALID_REVNUM;
1759 else
1761 end_rev = end_revision.value.number;
1762 if (! SVN_IS_VALID_REVNUM(end_rev))
1763 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1764 _("Invalid revision number (%ld)"),
1765 end_rev);
1769 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
1770 apr_array_make(pool, 1,
1771 sizeof(const char *)),
1772 pool));
1773 if (targets->nelts != 1)
1774 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1776 to_url = APR_ARRAY_IDX(targets, 0, const char *);
1778 if (! svn_path_is_url(to_url))
1779 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1780 _("Path '%s' is not a URL"), to_url);
1782 baton = make_subcommand_baton(opt_baton, to_url, NULL,
1783 start_rev, end_rev, pool);
1784 SVN_ERR(svn_ra_open3(&to_session, baton->to_url, NULL,
1785 &(baton->sync_callbacks), baton, baton->config, pool));
1786 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
1787 SVN_ERR(with_locked(to_session, do_copy_revprops, baton, pool));
1789 return SVN_NO_ERROR;
1794 /*** `svnsync help' ***/
1797 /* SUBCOMMAND: help */
1798 static svn_error_t *
1799 help_cmd(apr_getopt_t *os, void *baton, apr_pool_t *pool)
1801 opt_baton_t *opt_baton = baton;
1803 const char *header =
1804 _("general usage: svnsync SUBCOMMAND DEST_URL [ARGS & OPTIONS ...]\n"
1805 "Type 'svnsync help <subcommand>' for help on a specific subcommand.\n"
1806 "Type 'svnsync --version' to see the program version and RA modules.\n"
1807 "\n"
1808 "Available subcommands:\n");
1810 const char *ra_desc_start
1811 = _("The following repository access (RA) modules are available:\n\n");
1813 svn_stringbuf_t *version_footer = svn_stringbuf_create(ra_desc_start,
1814 pool);
1816 SVN_ERR(svn_ra_print_modules(version_footer, pool));
1818 SVN_ERR(svn_opt_print_help(os, "svnsync",
1819 opt_baton ? opt_baton->version : FALSE,
1820 FALSE, version_footer->data, header,
1821 svnsync_cmd_table, svnsync_options, NULL,
1822 pool));
1824 return SVN_NO_ERROR;
1829 /*** Main ***/
1832 main(int argc, const char *argv[])
1834 const svn_opt_subcommand_desc_t *subcommand = NULL;
1835 apr_array_header_t *received_opts;
1836 opt_baton_t opt_baton;
1837 svn_config_t *config;
1838 apr_status_t apr_err;
1839 apr_getopt_t *os;
1840 apr_pool_t *pool;
1841 svn_error_t *err;
1842 int opt_id, i;
1843 const char *username = NULL, *source_username = NULL, *sync_username = NULL;
1844 const char *password = NULL, *source_password = NULL, *sync_password = NULL;
1846 if (svn_cmdline_init("svnsync", stderr) != EXIT_SUCCESS)
1848 return EXIT_FAILURE;
1851 err = check_lib_versions();
1852 if (err)
1853 return svn_cmdline_handle_exit_error(err, NULL, "svnsync: ");
1855 pool = svn_pool_create(NULL);
1857 err = svn_ra_initialize(pool);
1858 if (err)
1859 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1861 memset(&opt_baton, 0, sizeof(opt_baton));
1863 received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int));
1865 if (argc <= 1)
1867 help_cmd(NULL, NULL, pool);
1868 svn_pool_destroy(pool);
1869 return EXIT_FAILURE;
1872 err = svn_cmdline__getopt_init(&os, argc, argv, pool);
1873 if (err)
1874 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1876 os->interleave = 1;
1878 for (;;)
1880 const char *opt_arg;
1882 apr_err = apr_getopt_long(os, svnsync_options, &opt_id, &opt_arg);
1883 if (APR_STATUS_IS_EOF(apr_err))
1884 break;
1885 else if (apr_err)
1887 help_cmd(NULL, NULL, pool);
1888 svn_pool_destroy(pool);
1889 return EXIT_FAILURE;
1892 APR_ARRAY_PUSH(received_opts, int) = opt_id;
1894 switch (opt_id)
1896 case svnsync_opt_non_interactive:
1897 opt_baton.non_interactive = TRUE;
1898 break;
1900 case svnsync_opt_no_auth_cache:
1901 opt_baton.no_auth_cache = TRUE;
1902 break;
1904 case svnsync_opt_auth_username:
1905 username = opt_arg;
1906 break;
1908 case svnsync_opt_auth_password:
1909 password = opt_arg;
1910 break;
1912 case svnsync_opt_source_username:
1913 source_username = opt_arg;
1914 break;
1916 case svnsync_opt_source_password:
1917 source_password = opt_arg;
1918 break;
1920 case svnsync_opt_sync_username:
1921 sync_username = opt_arg;
1922 break;
1924 case svnsync_opt_sync_password:
1925 sync_password = opt_arg;
1926 break;
1928 case svnsync_opt_config_dir:
1929 opt_baton.config_dir = opt_arg;
1930 break;
1932 case svnsync_opt_version:
1933 opt_baton.version = TRUE;
1934 break;
1936 case 'q':
1937 opt_baton.quiet = TRUE;
1938 break;
1940 case '?':
1941 case 'h':
1942 opt_baton.help = TRUE;
1943 break;
1945 default:
1947 help_cmd(NULL, NULL, pool);
1948 svn_pool_destroy(pool);
1949 return EXIT_FAILURE;
1954 if (opt_baton.help)
1955 subcommand = svn_opt_get_canonical_subcommand(svnsync_cmd_table, "help");
1957 /* Disallow the mixing --username/password with their --source- and
1958 --sync- variants. Treat "--username FOO" as "--source-username
1959 FOO --sync-username FOO"; ditto for "--password FOO". */
1960 if ((username || password)
1961 && (source_username || sync_username
1962 || source_password || sync_password))
1964 err = svn_error_create
1965 (SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1966 _("Cannot use --username or --password with any of "
1967 "--source-username, --source-password, --sync-username, "
1968 "or --sync-password.\n"));
1969 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1971 if (username)
1973 source_username = username;
1974 sync_username = username;
1976 if (password)
1978 source_password = password;
1979 sync_password = password;
1981 opt_baton.source_username = source_username;
1982 opt_baton.source_password = source_password;
1983 opt_baton.sync_username = sync_username;
1984 opt_baton.sync_password = sync_password;
1986 err = svn_config_ensure(opt_baton.config_dir, pool);
1987 if (err)
1988 return svn_cmdline_handle_exit_error(err, pool, "synsync: ");
1990 if (subcommand == NULL)
1992 if (os->ind >= os->argc)
1994 if (opt_baton.version)
1996 /* Use the "help" subcommand to handle "--version". */
1997 static const svn_opt_subcommand_desc_t pseudo_cmd =
1998 { "--version", help_cmd, {0}, "",
1999 {svnsync_opt_version, /* must accept its own option */
2000 } };
2002 subcommand = &pseudo_cmd;
2004 else
2006 help_cmd(NULL, NULL, pool);
2007 svn_pool_destroy(pool);
2008 return EXIT_FAILURE;
2011 else
2013 const char *first_arg = os->argv[os->ind++];
2014 subcommand = svn_opt_get_canonical_subcommand(svnsync_cmd_table,
2015 first_arg);
2016 if (subcommand == NULL)
2018 help_cmd(NULL, NULL, pool);
2019 svn_pool_destroy(pool);
2020 return EXIT_FAILURE;
2025 for (i = 0; i < received_opts->nelts; ++i)
2027 opt_id = APR_ARRAY_IDX(received_opts, i, int);
2029 if (opt_id == 'h' || opt_id == '?')
2030 continue;
2032 if (! svn_opt_subcommand_takes_option(subcommand, opt_id))
2034 const char *optstr;
2035 const apr_getopt_option_t *badopt =
2036 svn_opt_get_option_from_code(opt_id, svnsync_options);
2037 svn_opt_format_option(&optstr, badopt, FALSE, pool);
2038 if (subcommand->name[0] == '-')
2040 help_cmd(NULL, NULL, pool);
2042 else
2044 err = svn_error_createf
2045 (SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
2046 _("Subcommand '%s' doesn't accept option '%s'\n"
2047 "Type 'svnsync help %s' for usage.\n"),
2048 subcommand->name, optstr, subcommand->name);
2049 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2054 err = svn_config_get_config(&opt_baton.config, opt_baton.config_dir, pool);
2055 if (err)
2056 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2058 config = apr_hash_get(opt_baton.config, SVN_CONFIG_CATEGORY_CONFIG,
2059 APR_HASH_KEY_STRING);
2061 apr_signal(SIGINT, signal_handler);
2063 #ifdef SIGBREAK
2064 /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */
2065 apr_signal(SIGBREAK, signal_handler);
2066 #endif
2068 #ifdef SIGHUP
2069 apr_signal(SIGHUP, signal_handler);
2070 #endif
2072 #ifdef SIGTERM
2073 apr_signal(SIGTERM, signal_handler);
2074 #endif
2076 #ifdef SIGPIPE
2077 /* Disable SIGPIPE generation for the platforms that have it. */
2078 apr_signal(SIGPIPE, SIG_IGN);
2079 #endif
2081 #ifdef SIGXFSZ
2082 /* Disable SIGXFSZ generation for the platforms that have it,
2083 otherwise working with large files when compiled against an APR
2084 that doesn't have large file support will crash the program,
2085 which is uncool. */
2086 apr_signal(SIGXFSZ, SIG_IGN);
2087 #endif
2089 err = svn_cmdline_setup_auth_baton(&opt_baton.source_auth_baton,
2090 opt_baton.non_interactive,
2091 opt_baton.source_username,
2092 opt_baton.source_password,
2093 opt_baton.config_dir,
2094 opt_baton.no_auth_cache,
2095 config,
2096 check_cancel, NULL,
2097 pool);
2098 if (! err)
2099 err = svn_cmdline_setup_auth_baton(&opt_baton.sync_auth_baton,
2100 opt_baton.non_interactive,
2101 opt_baton.sync_username,
2102 opt_baton.sync_password,
2103 opt_baton.config_dir,
2104 opt_baton.no_auth_cache,
2105 config,
2106 check_cancel, NULL,
2107 pool);
2108 if (! err)
2109 err = (*subcommand->cmd_func)(os, &opt_baton, pool);
2110 if (err)
2112 /* For argument-related problems, suggest using the 'help'
2113 subcommand. */
2114 if (err->apr_err == SVN_ERR_CL_INSUFFICIENT_ARGS
2115 || err->apr_err == SVN_ERR_CL_ARG_PARSING_ERROR)
2117 err = svn_error_quick_wrap(err,
2118 _("Try 'svnsync help' for more info"));
2121 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2124 svn_pool_destroy(pool);
2126 return EXIT_SUCCESS;