Followup to r29625: fix getopt tests.
[svn.git] / subversion / svnsync / main.c
blobd29572d95ce372d263651164fa0811a26f12cd71
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 /* syncronize 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_open2(&from_session, baton->from_url,
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 (strcmp(root_url, baton->from_url) < 0)
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 return svn_error_create(SVN_ERR_RA_PARTIAL_REPLAY_NOT_SUPPORTED, NULL,
640 NULL);
644 SVN_ERR(svn_ra_change_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL,
645 svn_string_create(baton->from_url, pool),
646 pool));
648 SVN_ERR(svn_ra_get_uuid2(from_session, &uuid, pool));
650 SVN_ERR(svn_ra_change_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_UUID,
651 svn_string_create(uuid, pool), pool));
653 SVN_ERR(svn_ra_change_rev_prop(to_session, 0, SVNSYNC_PROP_LAST_MERGED_REV,
654 svn_string_create("0", pool), pool));
656 /* Finally, copy all non-svnsync revprops from rev 0 of the source
657 repos into the dest repos. */
659 SVN_ERR(copy_revprops(from_session, to_session, 0, FALSE,
660 baton->quiet, pool));
662 /* TODO: It would be nice if we could set the dest repos UUID to be
663 equal to the UUID of the source repos, at least optionally. That
664 way people could check out/log/diff using a local fast mirror,
665 but switch --relocate to the actual final repository in order to
666 make changes... But at this time, the RA layer doesn't have a
667 way to set a UUID. */
669 return SVN_NO_ERROR;
673 /* SUBCOMMAND: init */
674 static svn_error_t *
675 initialize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
677 const char *to_url, *from_url;
678 svn_ra_session_t *to_session;
679 opt_baton_t *opt_baton = b;
680 apr_array_header_t *targets;
681 subcommand_baton_t *baton;
683 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
684 apr_array_make(pool, 0,
685 sizeof(const char *)),
686 pool));
687 if (targets->nelts < 2)
688 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
689 if (targets->nelts > 2)
690 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
692 to_url = APR_ARRAY_IDX(targets, 0, const char *);
693 from_url = APR_ARRAY_IDX(targets, 1, const char *);
695 if (! svn_path_is_url(to_url))
696 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
697 _("Path '%s' is not a URL"), to_url);
698 if (! svn_path_is_url(from_url))
699 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
700 _("Path '%s' is not a URL"), from_url);
702 baton = make_subcommand_baton(opt_baton, to_url, from_url, 0, 0, pool);
703 SVN_ERR(svn_ra_open2(&to_session, baton->to_url, &(baton->sync_callbacks),
704 baton, baton->config, pool));
705 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
706 SVN_ERR(with_locked(to_session, do_initialize, baton, pool));
708 return SVN_NO_ERROR;
713 /*** Synchronization Editor ***/
715 /* This editor has a couple of jobs.
717 * First, it needs to filter out the propchanges that can't be passed over
718 * libsvn_ra.
720 * Second, it needs to adjust for the fact that we might not actually have
721 * permission to see all of the data from the remote repository, which means
722 * we could get revisions that are totally empty from our point of view.
724 * Third, it needs to adjust copyfrom paths, adding the root url for the
725 * destination repository to the beginning of them.
729 /* Edit baton */
730 typedef struct {
731 const svn_delta_editor_t *wrapped_editor;
732 void *wrapped_edit_baton;
733 const char *to_url; /* URL we're copying into, for correct copyfrom URLs */
734 svn_boolean_t called_open_root;
735 svn_boolean_t got_textdeltas;
736 svn_revnum_t base_revision;
737 svn_boolean_t quiet;
738 } edit_baton_t;
741 /* A dual-purpose baton for files and directories. */
742 typedef struct {
743 void *edit_baton;
744 void *wrapped_node_baton;
745 } node_baton_t;
748 /*** Editor vtable functions ***/
750 static svn_error_t *
751 set_target_revision(void *edit_baton,
752 svn_revnum_t target_revision,
753 apr_pool_t *pool)
755 edit_baton_t *eb = edit_baton;
756 return eb->wrapped_editor->set_target_revision(eb->wrapped_edit_baton,
757 target_revision, pool);
760 static svn_error_t *
761 open_root(void *edit_baton,
762 svn_revnum_t base_revision,
763 apr_pool_t *pool,
764 void **root_baton)
766 edit_baton_t *eb = edit_baton;
767 node_baton_t *dir_baton = apr_palloc(pool, sizeof(*dir_baton));
769 SVN_ERR(eb->wrapped_editor->open_root(eb->wrapped_edit_baton,
770 base_revision, pool,
771 &dir_baton->wrapped_node_baton));
773 eb->called_open_root = TRUE;
774 dir_baton->edit_baton = edit_baton;
775 *root_baton = dir_baton;
777 return SVN_NO_ERROR;
780 static svn_error_t *
781 delete_entry(const char *path,
782 svn_revnum_t base_revision,
783 void *parent_baton,
784 apr_pool_t *pool)
786 node_baton_t *pb = parent_baton;
787 edit_baton_t *eb = pb->edit_baton;
789 return eb->wrapped_editor->delete_entry(path, base_revision,
790 pb->wrapped_node_baton, pool);
793 static svn_error_t *
794 add_directory(const char *path,
795 void *parent_baton,
796 const char *copyfrom_path,
797 svn_revnum_t copyfrom_rev,
798 apr_pool_t *pool,
799 void **child_baton)
801 node_baton_t *pb = parent_baton;
802 edit_baton_t *eb = pb->edit_baton;
803 node_baton_t *b = apr_palloc(pool, sizeof(*b));
805 if (copyfrom_path)
806 copyfrom_path = apr_psprintf(pool, "%s%s", eb->to_url,
807 svn_path_uri_encode(copyfrom_path, pool));
809 SVN_ERR(eb->wrapped_editor->add_directory(path, pb->wrapped_node_baton,
810 copyfrom_path,
811 copyfrom_rev, pool,
812 &b->wrapped_node_baton));
814 b->edit_baton = eb;
815 *child_baton = b;
817 return SVN_NO_ERROR;
820 static svn_error_t *
821 open_directory(const char *path,
822 void *parent_baton,
823 svn_revnum_t base_revision,
824 apr_pool_t *pool,
825 void **child_baton)
827 node_baton_t *pb = parent_baton;
828 edit_baton_t *eb = pb->edit_baton;
829 node_baton_t *db = apr_palloc(pool, sizeof(*db));
831 SVN_ERR(eb->wrapped_editor->open_directory(path, pb->wrapped_node_baton,
832 base_revision, pool,
833 &db->wrapped_node_baton));
835 db->edit_baton = eb;
836 *child_baton = db;
838 return SVN_NO_ERROR;
841 static svn_error_t *
842 add_file(const char *path,
843 void *parent_baton,
844 const char *copyfrom_path,
845 svn_revnum_t copyfrom_rev,
846 apr_pool_t *pool,
847 void **file_baton)
849 node_baton_t *pb = parent_baton;
850 edit_baton_t *eb = pb->edit_baton;
851 node_baton_t *fb = apr_palloc(pool, sizeof(*fb));
853 if (copyfrom_path)
854 copyfrom_path = apr_psprintf(pool, "%s%s", eb->to_url,
855 svn_path_uri_encode(copyfrom_path, pool));
857 SVN_ERR(eb->wrapped_editor->add_file(path, pb->wrapped_node_baton,
858 copyfrom_path, copyfrom_rev,
859 pool, &fb->wrapped_node_baton));
861 fb->edit_baton = eb;
862 *file_baton = fb;
864 return SVN_NO_ERROR;
867 static svn_error_t *
868 open_file(const char *path,
869 void *parent_baton,
870 svn_revnum_t base_revision,
871 apr_pool_t *pool,
872 void **file_baton)
874 node_baton_t *pb = parent_baton;
875 edit_baton_t *eb = pb->edit_baton;
876 node_baton_t *fb = apr_palloc(pool, sizeof(*fb));
878 SVN_ERR(eb->wrapped_editor->open_file(path, pb->wrapped_node_baton,
879 base_revision, pool,
880 &fb->wrapped_node_baton));
882 fb->edit_baton = eb;
883 *file_baton = fb;
885 return SVN_NO_ERROR;
888 static svn_error_t *
889 apply_textdelta(void *file_baton,
890 const char *base_checksum,
891 apr_pool_t *pool,
892 svn_txdelta_window_handler_t *handler,
893 void **handler_baton)
895 node_baton_t *fb = file_baton;
896 edit_baton_t *eb = fb->edit_baton;
898 if (! eb->quiet)
900 if (! eb->got_textdeltas)
901 SVN_ERR(svn_cmdline_printf(pool, _("Transmitting file data ")));
902 SVN_ERR(svn_cmdline_printf(pool, "."));
903 SVN_ERR(svn_cmdline_fflush(stdout));
906 eb->got_textdeltas = TRUE;
907 return eb->wrapped_editor->apply_textdelta(fb->wrapped_node_baton,
908 base_checksum, pool,
909 handler, handler_baton);
912 static svn_error_t *
913 close_file(void *file_baton,
914 const char *text_checksum,
915 apr_pool_t *pool)
917 node_baton_t *fb = file_baton;
918 edit_baton_t *eb = fb->edit_baton;
919 return eb->wrapped_editor->close_file(fb->wrapped_node_baton,
920 text_checksum, pool);
923 static svn_error_t *
924 absent_file(const char *path,
925 void *file_baton,
926 apr_pool_t *pool)
928 node_baton_t *fb = file_baton;
929 edit_baton_t *eb = fb->edit_baton;
930 return eb->wrapped_editor->absent_file(path, fb->wrapped_node_baton, pool);
933 static svn_error_t *
934 close_directory(void *dir_baton,
935 apr_pool_t *pool)
937 node_baton_t *db = dir_baton;
938 edit_baton_t *eb = db->edit_baton;
939 return eb->wrapped_editor->close_directory(db->wrapped_node_baton, pool);
942 static svn_error_t *
943 absent_directory(const char *path,
944 void *dir_baton,
945 apr_pool_t *pool)
947 node_baton_t *db = dir_baton;
948 edit_baton_t *eb = db->edit_baton;
949 return eb->wrapped_editor->absent_directory(path, db->wrapped_node_baton,
950 pool);
953 static svn_error_t *
954 change_file_prop(void *file_baton,
955 const char *name,
956 const svn_string_t *value,
957 apr_pool_t *pool)
959 node_baton_t *fb = file_baton;
960 edit_baton_t *eb = fb->edit_baton;
962 /* only regular properties can pass over libsvn_ra */
963 if (svn_property_kind(NULL, name) != svn_prop_regular_kind)
964 return SVN_NO_ERROR;
966 #ifdef SVN_SYNC__REPAIR_MERGEINFO
967 /* Drop svn:mergeinfo and (errantly set, as this is a file)
968 svnmerge.py properties. */
969 if ((strcmp(name, SVN_PROP_MERGEINFO) == 0)
970 || (strcmp(name, "svnmerge-integrated") == 0)
971 || (strcmp(name, "svnmerge-blocked") == 0))
973 return svn_cmdline_fprintf(stderr, pool,
974 "Filtering '%s' property.\n", name);
976 #endif
978 return eb->wrapped_editor->change_file_prop(fb->wrapped_node_baton,
979 name, value, pool);
982 static svn_error_t *
983 change_dir_prop(void *dir_baton,
984 const char *name,
985 const svn_string_t *value,
986 apr_pool_t *pool)
988 node_baton_t *db = dir_baton;
989 edit_baton_t *eb = db->edit_baton;
990 svn_string_t *real_value = (svn_string_t *)value;
992 /* only regular properties can pass over libsvn_ra */
993 if (svn_property_kind(NULL, name) != svn_prop_regular_kind)
994 return SVN_NO_ERROR;
996 #ifdef SVN_SYNC__REPAIR_MERGEINFO
997 /* Drop svn:mergeinfo properties. */
998 if (strcmp(name, SVN_PROP_MERGEINFO) == 0)
1000 return svn_cmdline_fprintf(stderr, pool,
1001 "Filtering '%s' property.\n", name);
1004 /* Convert svnmerge-integrated data into svn:mergeinfo. */
1005 if (strcmp(name, "svnmerge-integrated") == 0)
1007 if (value)
1009 /* svnmerge-integrated differs from svn:mergeinfo in a pair
1010 of ways. First, it can use tabs, newlines, or spaces to
1011 delimit source information. Secondly, the source paths
1012 are relative URLs, whereas svn:mergeinfo uses relative
1013 paths (not URI-encoded). */
1014 svn_error_t *err;
1015 svn_stringbuf_t *mergeinfo_buf = svn_stringbuf_create("", pool);
1016 svn_mergeinfo_t mergeinfo;
1017 int i;
1018 apr_array_header_t *sources =
1019 svn_cstring_split(value->data, " \t\n", TRUE, pool);
1021 for (i = 0; i < sources->nelts; i++)
1023 const char *rel_path;
1024 apr_array_header_t *path_revs =
1025 svn_cstring_split(APR_ARRAY_IDX(sources, i, const char *),
1026 ":", TRUE, pool);
1028 /* ### TODO: Warn? */
1029 if (path_revs->nelts != 2)
1030 continue;
1032 /* Append this source's mergeinfo data. */
1033 rel_path = APR_ARRAY_IDX(path_revs, 0, const char *);
1034 rel_path = svn_path_uri_decode(rel_path, pool);
1035 svn_stringbuf_appendcstr(mergeinfo_buf, rel_path);
1036 svn_stringbuf_appendcstr(mergeinfo_buf, ":");
1037 svn_stringbuf_appendcstr(mergeinfo_buf,
1038 APR_ARRAY_IDX(path_revs, 1,
1039 const char *));
1040 svn_stringbuf_appendcstr(mergeinfo_buf, "\n");
1043 /* Try to parse the mergeinfo string we've created, just to
1044 check for bogosity. If all goes well, we'll unparse it
1045 again and use that as our property value. */
1046 err = svn_mergeinfo_parse(&mergeinfo, mergeinfo_buf->data, pool);
1047 if (err)
1049 SVN_ERR(svn_cmdline_fprintf(stderr, pool,
1050 "Skipping bogus svnmerge-integrated "
1051 "value: %s\n", value->data));
1052 svn_error_clear(err);
1053 return SVN_NO_ERROR;
1055 SVN_ERR(svn_mergeinfo_to_string(&real_value, mergeinfo, pool));
1057 SVN_ERR(svn_cmdline_fprintf(stderr, pool,
1058 "Migrating '%s' property as '%s'.\n",
1059 name, SVN_PROP_MERGEINFO));
1060 name = SVN_PROP_MERGEINFO;
1063 /* Ignore valid svnmerge-blocked properties (but warn so folks know
1064 about them and can run the svnmerge-migrate-history.py script). */
1065 if (strcmp(name, "svnmerge-blocked") == 0)
1067 SVN_ERR(svn_cmdline_fprintf(stderr, pool,
1068 "Ignoring '%s' property.\n", name));
1070 #endif
1072 return eb->wrapped_editor->change_dir_prop(db->wrapped_node_baton,
1073 name, real_value, pool);
1076 static svn_error_t *
1077 close_edit(void *edit_baton,
1078 apr_pool_t *pool)
1080 edit_baton_t *eb = edit_baton;
1082 /* If we haven't opened the root yet, that means we're transfering
1083 an empty revision, probably because we aren't allowed to see the
1084 contents for some reason. In any event, we need to open the root
1085 and close it again, before we can close out the edit, or the
1086 commit will fail. */
1088 if (! eb->called_open_root)
1090 void *baton;
1091 SVN_ERR(eb->wrapped_editor->open_root(eb->wrapped_edit_baton,
1092 eb->base_revision, pool,
1093 &baton));
1094 SVN_ERR(eb->wrapped_editor->close_directory(baton, pool));
1097 if (! eb->quiet)
1099 if (eb->got_textdeltas)
1100 SVN_ERR(svn_cmdline_printf(pool, "\n"));
1103 return eb->wrapped_editor->close_edit(eb->wrapped_edit_baton, pool);
1106 /*** Editor factory function ***/
1108 /* Set WRAPPED_EDITOR and WRAPPED_EDIT_BATON to an editor/baton pair
1109 * that wraps our own commit EDITOR/EDIT_BATON. BASE_REVISION is the
1110 * revision on which the driver of this returned editor will be basing
1111 * the commit. TO_URL is the URL of the root of the repository into
1112 * which the commit is being made.
1114 static svn_error_t *
1115 get_sync_editor(const svn_delta_editor_t *wrapped_editor,
1116 void *wrapped_edit_baton,
1117 svn_revnum_t base_revision,
1118 const char *to_url,
1119 svn_boolean_t quiet,
1120 const svn_delta_editor_t **editor,
1121 void **edit_baton,
1122 apr_pool_t *pool)
1124 svn_delta_editor_t *tree_editor = svn_delta_default_editor(pool);
1125 edit_baton_t *eb = apr_pcalloc(pool, sizeof(*eb));
1127 tree_editor->set_target_revision = set_target_revision;
1128 tree_editor->open_root = open_root;
1129 tree_editor->delete_entry = delete_entry;
1130 tree_editor->add_directory = add_directory;
1131 tree_editor->open_directory = open_directory;
1132 tree_editor->change_dir_prop = change_dir_prop;
1133 tree_editor->close_directory = close_directory;
1134 tree_editor->absent_directory = absent_directory;
1135 tree_editor->add_file = add_file;
1136 tree_editor->open_file = open_file;
1137 tree_editor->apply_textdelta = apply_textdelta;
1138 tree_editor->change_file_prop = change_file_prop;
1139 tree_editor->close_file = close_file;
1140 tree_editor->absent_file = absent_file;
1141 tree_editor->close_edit = close_edit;
1143 eb->wrapped_editor = wrapped_editor;
1144 eb->wrapped_edit_baton = wrapped_edit_baton;
1145 eb->base_revision = base_revision;
1146 eb->to_url = to_url;
1147 eb->quiet = quiet;
1149 *editor = tree_editor;
1150 *edit_baton = eb;
1152 return SVN_NO_ERROR;
1157 /*** `svnsync sync' ***/
1159 /* Implements `svn_commit_callback2_t' interface. */
1160 static svn_error_t *
1161 commit_callback(const svn_commit_info_t *commit_info,
1162 void *baton,
1163 apr_pool_t *pool)
1165 subcommand_baton_t *sb = baton;
1167 if (! sb->quiet)
1169 SVN_ERR(svn_cmdline_printf(pool, _("Committed revision %ld.\n"),
1170 commit_info->revision));
1173 sb->committed_rev = commit_info->revision;
1175 return SVN_NO_ERROR;
1179 /* Set *FROM_SESSION to an RA session associated with the source
1180 * repository of the synchronization, as determined by reading
1181 * svn:sync- properties from the destination repository (associated
1182 * with TO_SESSION). Set LAST_MERGED_REV to the value of the property
1183 * which records the most recently synchronized revision.
1185 * CALLBACKS is a vtable of RA callbacks to provide when creating
1186 * *FROM_SESSION. CONFIG is a configuration hash.
1188 static svn_error_t *
1189 open_source_session(svn_ra_session_t **from_session,
1190 svn_string_t **last_merged_rev,
1191 svn_ra_session_t *to_session,
1192 svn_ra_callbacks2_t *callbacks,
1193 apr_hash_t *config,
1194 void *baton,
1195 apr_pool_t *pool)
1197 svn_string_t *from_url, *from_uuid;
1198 const char *uuid;
1200 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL,
1201 &from_url, pool));
1202 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_UUID,
1203 &from_uuid, pool));
1204 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_LAST_MERGED_REV,
1205 last_merged_rev, pool));
1207 if (! from_url || ! from_uuid || ! *last_merged_rev)
1208 return svn_error_create
1209 (APR_EINVAL, NULL,
1210 _("Destination repository has not been initialized"));
1212 /* Open the session to copy the revision data. */
1213 SVN_ERR(svn_ra_open2(from_session, from_url->data, callbacks, baton,
1214 config, pool));
1216 /* Ok, now sanity check the UUID of the source repository, it
1217 wouldn't be a good thing to sync from a different repository. */
1219 SVN_ERR(svn_ra_get_uuid2(*from_session, &uuid, pool));
1221 if (strcmp(uuid, from_uuid->data) != 0)
1222 return svn_error_createf(APR_EINVAL, NULL,
1223 _("UUID of source repository (%s) does not "
1224 "match expected UUID (%s)"),
1225 uuid, from_uuid->data);
1227 return SVN_NO_ERROR;
1230 /* Replay baton, used during sychnronization. */
1231 typedef struct {
1232 svn_ra_session_t *from_session;
1233 svn_ra_session_t *to_session;
1234 subcommand_baton_t *sb;
1235 } replay_baton_t;
1237 /* Return a replay baton allocated from POOL and populated with
1238 data from the provided parameters. */
1239 static replay_baton_t *
1240 make_replay_baton(svn_ra_session_t *from_session,
1241 svn_ra_session_t *to_session,
1242 subcommand_baton_t *sb, apr_pool_t *pool)
1244 replay_baton_t *rb = apr_pcalloc(pool, sizeof(*rb));
1245 rb->from_session = from_session;
1246 rb->to_session = to_session;
1247 rb->sb = sb;
1248 return rb;
1251 /* Filter out svn:date and svn:author properties. */
1252 static svn_boolean_t
1253 filter_exclude_date_author_sync(const char *key)
1255 if (strncmp(key, SVN_PROP_REVISION_AUTHOR,
1256 sizeof(SVN_PROP_REVISION_AUTHOR) - 1) == 0)
1257 return TRUE;
1258 else if (strncmp(key, SVN_PROP_REVISION_DATE,
1259 sizeof(SVN_PROP_REVISION_DATE) - 1) == 0)
1260 return TRUE;
1261 else if (strncmp(key, SVNSYNC_PROP_PREFIX,
1262 sizeof(SVNSYNC_PROP_PREFIX) - 1) == 0)
1263 return TRUE;
1265 return FALSE;
1268 /* Filter out all properties except svn:date and svn:author */
1269 static svn_boolean_t
1270 filter_include_date_author_sync(const char *key)
1272 return ! filter_exclude_date_author_sync(key);
1275 /* Callback function for svn_ra_replay_range, invoked when starting to parse
1276 * a replay report.
1278 static svn_error_t *
1279 replay_rev_started(svn_revnum_t revision,
1280 void *replay_baton,
1281 const svn_delta_editor_t **editor,
1282 void **edit_baton,
1283 apr_hash_t *rev_props,
1284 apr_pool_t *pool)
1286 const svn_delta_editor_t *commit_editor;
1287 const svn_delta_editor_t *cancel_editor;
1288 const svn_delta_editor_t *sync_editor;
1289 void *commit_baton;
1290 void *cancel_baton;
1291 void *sync_baton;
1292 replay_baton_t *rb = replay_baton;
1293 apr_hash_t *filtered;
1294 int filtered_count;
1296 /* We set this property so that if we error out for some reason
1297 we can later determine where we were in the process of
1298 merging a revision. If we had committed the change, but we
1299 hadn't finished copying the revprops we need to know that, so
1300 we can go back and finish the job before we move on.
1302 NOTE: We have to set this before we start the commit editor,
1303 because ra_svn doesn't let you change rev props during a
1304 commit. */
1305 SVN_ERR(svn_ra_change_rev_prop(rb->to_session, 0,
1306 SVNSYNC_PROP_CURRENTLY_COPYING,
1307 svn_string_createf(pool, "%ld",
1308 revision),
1309 pool));
1311 /* The actual copy is just a replay hooked up to a commit. Include
1312 all the revision properties from the source repositories, except
1313 'svn:author' and 'svn:date', those are not guaranteed to get
1314 through the editor anyway. */
1315 filtered = filter_props(&filtered_count, rev_props,
1316 filter_exclude_date_author_sync,
1317 pool);
1319 /* svn_ra_get_commit_editor3 requires the log message to be
1320 set. It's possible that we didn't receive 'svn:log' here, so we
1321 have to set it to at least the empty string. If there's a svn:log
1322 property on this revision, we will write the actual value in the
1323 replay_rev_finished callback. */
1324 if (! apr_hash_get(filtered, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING))
1325 apr_hash_set(filtered, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING,
1326 svn_string_create("", pool));
1328 SVN_ERR(svn_ra_get_commit_editor3(rb->to_session, &commit_editor,
1329 &commit_baton,
1330 filtered,
1331 commit_callback, rb->sb,
1332 NULL, FALSE, pool));
1334 /* There's one catch though, the diff shows us props we can't send
1335 over the RA interface, so we need an editor that's smart enough
1336 to filter those out for us. */
1337 SVN_ERR(get_sync_editor(commit_editor, commit_baton, revision - 1,
1338 rb->sb->to_url, rb->sb->quiet,
1339 &sync_editor, &sync_baton, pool));
1341 SVN_ERR(svn_delta_get_cancellation_editor(check_cancel, NULL,
1342 sync_editor, sync_baton,
1343 &cancel_editor,
1344 &cancel_baton,
1345 pool));
1346 *editor = cancel_editor;
1347 *edit_baton = cancel_baton;
1349 return SVN_NO_ERROR;
1352 /* Callback function for svn_ra_replay_range, invoked when finishing parsing
1353 * a replay report.
1355 static svn_error_t *
1356 replay_rev_finished(svn_revnum_t revision,
1357 void *replay_baton,
1358 const svn_delta_editor_t *editor,
1359 void *edit_baton,
1360 apr_hash_t *rev_props,
1361 apr_pool_t *pool)
1363 apr_pool_t *subpool = svn_pool_create(pool);
1364 replay_baton_t *rb = replay_baton;
1365 apr_hash_t *filtered, *existing_props;
1366 int filtered_count;
1368 SVN_ERR(editor->close_edit(edit_baton, pool));
1370 /* Sanity check that we actually committed the revision we meant to. */
1371 if (rb->sb->committed_rev != revision)
1372 return svn_error_createf
1373 (APR_EINVAL, NULL,
1374 _("Commit created rev %ld but should have created %ld"),
1375 rb->sb->committed_rev, revision);
1377 SVN_ERR(svn_ra_rev_proplist(rb->to_session, revision, &existing_props,
1378 subpool));
1381 /* Ok, we're done with the data, now we just need to copy the remaining
1382 'svn:date' and 'svn:author' revprops and we're all set. */
1383 filtered = filter_props(&filtered_count, rev_props,
1384 filter_include_date_author_sync,
1385 subpool);
1386 SVN_ERR(write_revprops(&filtered_count, rb->to_session, revision, filtered,
1387 subpool));
1389 /* Remove all extra properties in TARGET. */
1390 SVN_ERR(remove_props_not_in_source(rb->to_session, revision,
1391 rev_props, existing_props, subpool));
1393 svn_pool_clear(subpool);
1395 /* Ok, we're done, bring the last-merged-rev property up to date. */
1396 SVN_ERR(svn_ra_change_rev_prop
1397 (rb->to_session,
1399 SVNSYNC_PROP_LAST_MERGED_REV,
1400 svn_string_create(apr_psprintf(pool, "%ld", revision),
1401 subpool),
1402 subpool));
1404 /* And finally drop the currently copying prop, since we're done
1405 with this revision. */
1406 SVN_ERR(svn_ra_change_rev_prop(rb->to_session, 0,
1407 SVNSYNC_PROP_CURRENTLY_COPYING,
1408 NULL, subpool));
1410 /* Notify the user that we copied revision properties. */
1411 if (! rb->sb->quiet)
1412 SVN_ERR(log_properties_copied(filtered_count > 0, revision, subpool));
1414 svn_pool_destroy(subpool);
1416 return SVN_NO_ERROR;
1419 /* Synchronize the repository associated with RA session TO_SESSION,
1420 * using information found in baton B, while the repository is
1421 * locked. Implements `with_locked_func_t' interface.
1423 static svn_error_t *
1424 do_synchronize(svn_ra_session_t *to_session,
1425 subcommand_baton_t *baton, apr_pool_t *pool)
1427 svn_string_t *last_merged_rev;
1428 svn_revnum_t from_latest;
1429 svn_ra_session_t *from_session;
1430 svn_string_t *currently_copying;
1431 svn_revnum_t to_latest, copying, last_merged;
1432 svn_revnum_t start_revision, end_revision;
1433 replay_baton_t *rb;
1435 SVN_ERR(open_source_session(&from_session,
1436 &last_merged_rev, to_session,
1437 &(baton->source_callbacks), baton->config,
1438 baton, pool));
1440 /* Check to see if we have revprops that still need to be copied for
1441 a prior revision we didn't finish copying. But first, check for
1442 state sanity. Remember, mirroring is not an atomic action,
1443 because revision properties are copied separately from the
1444 revision's contents.
1446 So, any time that currently-copying is not set, then
1447 last-merged-rev should be the HEAD revision of the destination
1448 repository. That is, if we didn't fall over in the middle of a
1449 previous synchronization, then our destination repository should
1450 have exactly as many revisions in it as we've synchronized.
1452 Alternately, if currently-copying *is* set, it must
1453 be either last-merged-rev or last-merged-rev + 1, and the HEAD
1454 revision must be equal to either last-merged-rev or
1455 currently-copying. If this is not the case, somebody has meddled
1456 with the destination without using svnsync.
1459 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_CURRENTLY_COPYING,
1460 &currently_copying, pool));
1462 SVN_ERR(svn_ra_get_latest_revnum(to_session, &to_latest, pool));
1464 last_merged = SVN_STR_TO_REV(last_merged_rev->data);
1466 if (currently_copying)
1468 copying = SVN_STR_TO_REV(currently_copying->data);
1470 if ((copying < last_merged)
1471 || (copying > (last_merged + 1))
1472 || ((to_latest != last_merged) && (to_latest != copying)))
1474 return svn_error_createf
1475 (APR_EINVAL, NULL,
1476 _("Revision being currently copied (%ld), last merged revision "
1477 "(%ld), and destination HEAD (%ld) are inconsistent; have you "
1478 "committed to the destination without using svnsync?"),
1479 copying, last_merged, to_latest);
1481 else if (copying == to_latest)
1483 if (copying > last_merged)
1485 SVN_ERR(copy_revprops(from_session, to_session,
1486 to_latest, TRUE, baton->quiet,
1487 pool));
1488 last_merged = copying;
1489 last_merged_rev = svn_string_create
1490 (apr_psprintf(pool, "%ld", last_merged), pool);
1493 /* Now update last merged rev and drop currently changing.
1494 Note that the order here is significant, if we do them
1495 in the wrong order there are race conditions where we
1496 end up not being able to tell if there have been bogus
1497 (i.e. non-svnsync) commits to the dest repository. */
1499 SVN_ERR(svn_ra_change_rev_prop(to_session, 0,
1500 SVNSYNC_PROP_LAST_MERGED_REV,
1501 last_merged_rev, pool));
1502 SVN_ERR(svn_ra_change_rev_prop(to_session, 0,
1503 SVNSYNC_PROP_CURRENTLY_COPYING,
1504 NULL, pool));
1506 /* If copying > to_latest, then we just fall through to
1507 attempting to copy the revision again. */
1509 else
1511 if (to_latest != last_merged)
1512 return svn_error_createf(APR_EINVAL, NULL,
1513 _("Destination HEAD (%ld) is not the last "
1514 "merged revision (%ld); have you "
1515 "committed to the destination without "
1516 "using svnsync?"),
1517 to_latest, last_merged);
1520 /* Now check to see if there are any revisions to copy. */
1521 SVN_ERR(svn_ra_get_latest_revnum(from_session, &from_latest, pool));
1523 if (from_latest < atol(last_merged_rev->data))
1524 return SVN_NO_ERROR;
1526 /* Ok, so there are new revisions, iterate over them copying them
1527 into the destination repository. */
1528 rb = make_replay_baton(from_session, to_session, baton, pool);
1530 start_revision = atol(last_merged_rev->data) + 1;
1531 end_revision = from_latest;
1533 SVN_ERR(check_cancel(NULL));
1535 SVN_ERR(svn_ra_replay_range(from_session, start_revision, end_revision,
1536 0, TRUE, replay_rev_started,
1537 replay_rev_finished, rb, pool));
1539 return SVN_NO_ERROR;
1543 /* SUBCOMMAND: sync */
1544 static svn_error_t *
1545 synchronize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
1547 svn_ra_session_t *to_session;
1548 opt_baton_t *opt_baton = b;
1549 apr_array_header_t *targets;
1550 subcommand_baton_t *baton;
1551 const char *to_url;
1553 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
1554 apr_array_make(pool, 0,
1555 sizeof(const char *)),
1556 pool));
1557 if (targets->nelts < 1)
1558 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1559 if (targets->nelts > 1)
1560 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1562 to_url = APR_ARRAY_IDX(targets, 0, const char *);
1564 if (! svn_path_is_url(to_url))
1565 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1566 _("Path '%s' is not a URL"), to_url);
1568 baton = make_subcommand_baton(opt_baton, to_url, NULL, 0, 0, pool);
1569 SVN_ERR(svn_ra_open2(&to_session, baton->to_url, &(baton->sync_callbacks),
1570 baton, baton->config, pool));
1571 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
1572 SVN_ERR(with_locked(to_session, do_synchronize, baton, pool));
1574 return SVN_NO_ERROR;
1579 /*** `svnsync copy-revprops' ***/
1581 /* Copy revision properties to the repository associated with RA
1582 * session TO_SESSION, using information found in baton B, while the
1583 * repository is locked. Implements `with_locked_func_t' interface.
1585 static svn_error_t *
1586 do_copy_revprops(svn_ra_session_t *to_session,
1587 subcommand_baton_t *baton, apr_pool_t *pool)
1589 svn_ra_session_t *from_session;
1590 svn_string_t *last_merged_rev;
1591 svn_revnum_t i;
1592 svn_revnum_t step = 1;
1594 SVN_ERR(open_source_session(&from_session, &last_merged_rev,
1595 to_session,
1596 &(baton->source_callbacks), baton->config,
1597 baton, pool));
1599 /* An invalid revision means "last-synced" */
1600 if (! SVN_IS_VALID_REVNUM(baton->start_rev))
1601 baton->start_rev = SVN_STR_TO_REV(last_merged_rev->data);
1602 if (! SVN_IS_VALID_REVNUM(baton->end_rev))
1603 baton->end_rev = SVN_STR_TO_REV(last_merged_rev->data);
1605 /* Make sure we have revisions within the valid range. */
1606 if (baton->start_rev > SVN_STR_TO_REV(last_merged_rev->data))
1607 return svn_error_createf
1608 (APR_EINVAL, NULL,
1609 _("Cannot copy revprops for a revision (%ld) that has not "
1610 "been synchronized yet"), baton->start_rev);
1611 if (baton->end_rev > SVN_STR_TO_REV(last_merged_rev->data))
1612 return svn_error_createf
1613 (APR_EINVAL, NULL,
1614 _("Cannot copy revprops for a revision (%ld) that has not "
1615 "been synchronized yet"), baton->end_rev);
1617 /* Now, copy all the requested revisions, in the requested order. */
1618 step = (baton->start_rev > baton->end_rev) ? -1 : 1;
1619 for (i = baton->start_rev; i != baton->end_rev + step; i = i + step)
1621 SVN_ERR(check_cancel(NULL));
1622 SVN_ERR(copy_revprops(from_session, to_session, i, FALSE,
1623 baton->quiet, pool));
1626 return SVN_NO_ERROR;
1630 /* SUBCOMMAND: copy-revprops */
1631 static svn_error_t *
1632 copy_revprops_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
1634 svn_ra_session_t *to_session;
1635 opt_baton_t *opt_baton = b;
1636 apr_array_header_t *targets;
1637 subcommand_baton_t *baton;
1638 const char *to_url;
1639 svn_opt_revision_t start_revision, end_revision;
1640 svn_revnum_t start_rev = 0, end_rev = SVN_INVALID_REVNUM;
1642 /* There should be either one or two arguments left to parse. */
1643 if (os->argc - os->ind > 2)
1644 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1645 if (os->argc - os->ind < 1)
1646 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1648 /* If there are two args, the last one is a revision range. We'll
1649 effectively pop it from the end of the list. Why? Because
1650 svn_opt_args_to_target_array2() does waaaaay too many useful
1651 things for us not to use it. */
1652 if (os->argc - os->ind == 2)
1654 const char *rev_str = os->argv[--(os->argc)];
1656 start_revision.kind = svn_opt_revision_unspecified;
1657 end_revision.kind = svn_opt_revision_unspecified;
1658 if ((svn_opt_parse_revision(&start_revision, &end_revision,
1659 rev_str, pool) != 0)
1660 || ((start_revision.kind != svn_opt_revision_number)
1661 && (start_revision.kind != svn_opt_revision_head))
1662 || ((end_revision.kind != svn_opt_revision_number)
1663 && (end_revision.kind != svn_opt_revision_head)
1664 && (end_revision.kind != svn_opt_revision_unspecified)))
1665 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1666 _("'%s' is not a valid revision range"),
1667 rev_str);
1669 /* Get the start revision, which must be either HEAD or a number
1670 (which is required to be a valid one). */
1671 if (start_revision.kind == svn_opt_revision_head)
1673 start_rev = SVN_INVALID_REVNUM;
1675 else
1677 start_rev = start_revision.value.number;
1678 if (! SVN_IS_VALID_REVNUM(start_rev))
1679 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1680 _("Invalid revision number (%ld)"),
1681 start_rev);
1684 /* Get the end revision, which must be unspecified (meaning,
1685 "same as the start_rev"), HEAD, or a number (which is
1686 required to be a valid one). */
1687 if (end_revision.kind == svn_opt_revision_unspecified)
1689 end_rev = start_rev;
1691 else if (end_revision.kind == svn_opt_revision_head)
1693 end_rev = SVN_INVALID_REVNUM;
1695 else
1697 end_rev = end_revision.value.number;
1698 if (! SVN_IS_VALID_REVNUM(end_rev))
1699 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1700 _("Invalid revision number (%ld)"),
1701 end_rev);
1705 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
1706 apr_array_make(pool, 1,
1707 sizeof(const char *)),
1708 pool));
1709 if (targets->nelts != 1)
1710 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1712 to_url = APR_ARRAY_IDX(targets, 0, const char *);
1714 if (! svn_path_is_url(to_url))
1715 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1716 _("Path '%s' is not a URL"), to_url);
1718 baton = make_subcommand_baton(opt_baton, to_url, NULL,
1719 start_rev, end_rev, pool);
1720 SVN_ERR(svn_ra_open2(&to_session, baton->to_url, &(baton->sync_callbacks),
1721 baton, baton->config, pool));
1722 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
1723 SVN_ERR(with_locked(to_session, do_copy_revprops, baton, pool));
1725 return SVN_NO_ERROR;
1730 /*** `svnsync help' ***/
1733 /* SUBCOMMAND: help */
1734 static svn_error_t *
1735 help_cmd(apr_getopt_t *os, void *baton, apr_pool_t *pool)
1737 opt_baton_t *opt_baton = baton;
1739 const char *header =
1740 _("general usage: svnsync SUBCOMMAND DEST_URL [ARGS & OPTIONS ...]\n"
1741 "Type 'svnsync help <subcommand>' for help on a specific subcommand.\n"
1742 "Type 'svnsync --version' to see the program version and RA modules.\n"
1743 "\n"
1744 "Available subcommands:\n");
1746 const char *ra_desc_start
1747 = _("The following repository access (RA) modules are available:\n\n");
1749 svn_stringbuf_t *version_footer = svn_stringbuf_create(ra_desc_start,
1750 pool);
1752 SVN_ERR(svn_ra_print_modules(version_footer, pool));
1754 SVN_ERR(svn_opt_print_help(os, "svnsync",
1755 opt_baton ? opt_baton->version : FALSE,
1756 FALSE, version_footer->data, header,
1757 svnsync_cmd_table, svnsync_options, NULL,
1758 pool));
1760 return SVN_NO_ERROR;
1765 /*** Main ***/
1768 main(int argc, const char *argv[])
1770 const svn_opt_subcommand_desc_t *subcommand = NULL;
1771 apr_array_header_t *received_opts;
1772 opt_baton_t opt_baton;
1773 svn_config_t *config;
1774 apr_status_t apr_err;
1775 apr_getopt_t *os;
1776 apr_pool_t *pool;
1777 svn_error_t *err;
1778 int opt_id, i;
1779 const char *username = NULL, *source_username = NULL, *sync_username = NULL;
1780 const char *password = NULL, *source_password = NULL, *sync_password = NULL;
1782 if (svn_cmdline_init("svnsync", stderr) != EXIT_SUCCESS)
1784 return EXIT_FAILURE;
1787 err = check_lib_versions();
1788 if (err)
1789 return svn_cmdline_handle_exit_error(err, NULL, "svnsync: ");
1791 pool = svn_pool_create(NULL);
1793 err = svn_ra_initialize(pool);
1794 if (err)
1795 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1797 memset(&opt_baton, 0, sizeof(opt_baton));
1799 received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int));
1801 if (argc <= 1)
1803 help_cmd(NULL, NULL, pool);
1804 svn_pool_destroy(pool);
1805 return EXIT_FAILURE;
1808 err = svn_cmdline__getopt_init(&os, argc, argv, pool);
1809 if (err)
1810 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1812 os->interleave = 1;
1814 for (;;)
1816 const char *opt_arg;
1818 apr_err = apr_getopt_long(os, svnsync_options, &opt_id, &opt_arg);
1819 if (APR_STATUS_IS_EOF(apr_err))
1820 break;
1821 else if (apr_err)
1823 help_cmd(NULL, NULL, pool);
1824 svn_pool_destroy(pool);
1825 return EXIT_FAILURE;
1828 APR_ARRAY_PUSH(received_opts, int) = opt_id;
1830 switch (opt_id)
1832 case svnsync_opt_non_interactive:
1833 opt_baton.non_interactive = TRUE;
1834 break;
1836 case svnsync_opt_no_auth_cache:
1837 opt_baton.no_auth_cache = TRUE;
1838 break;
1840 case svnsync_opt_auth_username:
1841 username = opt_arg;
1842 break;
1844 case svnsync_opt_auth_password:
1845 password = opt_arg;
1846 break;
1848 case svnsync_opt_source_username:
1849 source_username = opt_arg;
1850 break;
1852 case svnsync_opt_source_password:
1853 source_password = opt_arg;
1854 break;
1856 case svnsync_opt_sync_username:
1857 sync_username = opt_arg;
1858 break;
1860 case svnsync_opt_sync_password:
1861 sync_password = opt_arg;
1862 break;
1864 case svnsync_opt_config_dir:
1865 opt_baton.config_dir = opt_arg;
1866 break;
1868 case svnsync_opt_version:
1869 opt_baton.version = TRUE;
1870 break;
1872 case 'q':
1873 opt_baton.quiet = TRUE;
1874 break;
1876 case '?':
1877 case 'h':
1878 opt_baton.help = TRUE;
1879 break;
1881 default:
1883 help_cmd(NULL, NULL, pool);
1884 svn_pool_destroy(pool);
1885 return EXIT_FAILURE;
1890 if (opt_baton.help)
1891 subcommand = svn_opt_get_canonical_subcommand(svnsync_cmd_table, "help");
1893 /* Disallow the mixing --username/password with their --source- and
1894 --sync- variants. Treat "--username FOO" as "--source-username
1895 FOO --sync-username FOO"; ditto for "--password FOO". */
1896 if ((username || password)
1897 && (source_username || sync_username
1898 || source_password || sync_password))
1900 err = svn_error_create
1901 (SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1902 _("Cannot use --username or --password with any of "
1903 "--source-username, --source-password, --sync-username, "
1904 "or --sync-password.\n"));
1905 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1907 if (username)
1909 source_username = username;
1910 sync_username = username;
1912 if (password)
1914 source_password = password;
1915 sync_password = password;
1917 opt_baton.source_username = source_username;
1918 opt_baton.source_password = source_password;
1919 opt_baton.sync_username = sync_username;
1920 opt_baton.sync_password = sync_password;
1922 err = svn_config_ensure(opt_baton.config_dir, pool);
1923 if (err)
1924 return svn_cmdline_handle_exit_error(err, pool, "synsync: ");
1926 if (subcommand == NULL)
1928 if (os->ind >= os->argc)
1930 if (opt_baton.version)
1932 /* Use the "help" subcommand to handle "--version". */
1933 static const svn_opt_subcommand_desc_t pseudo_cmd =
1934 { "--version", help_cmd, {0}, "",
1935 {svnsync_opt_version, /* must accept its own option */
1936 } };
1938 subcommand = &pseudo_cmd;
1940 else
1942 help_cmd(NULL, NULL, pool);
1943 svn_pool_destroy(pool);
1944 return EXIT_FAILURE;
1947 else
1949 const char *first_arg = os->argv[os->ind++];
1950 subcommand = svn_opt_get_canonical_subcommand(svnsync_cmd_table,
1951 first_arg);
1952 if (subcommand == NULL)
1954 help_cmd(NULL, NULL, pool);
1955 svn_pool_destroy(pool);
1956 return EXIT_FAILURE;
1961 for (i = 0; i < received_opts->nelts; ++i)
1963 opt_id = APR_ARRAY_IDX(received_opts, i, int);
1965 if (opt_id == 'h' || opt_id == '?')
1966 continue;
1968 if (! svn_opt_subcommand_takes_option(subcommand, opt_id))
1970 const char *optstr;
1971 const apr_getopt_option_t *badopt =
1972 svn_opt_get_option_from_code(opt_id, svnsync_options);
1973 svn_opt_format_option(&optstr, badopt, FALSE, pool);
1974 if (subcommand->name[0] == '-')
1976 help_cmd(NULL, NULL, pool);
1978 else
1980 err = svn_error_createf
1981 (SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1982 _("Subcommand '%s' doesn't accept option '%s'\n"
1983 "Type 'svnsync help %s' for usage.\n"),
1984 subcommand->name, optstr, subcommand->name);
1985 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1990 err = svn_config_get_config(&opt_baton.config, opt_baton.config_dir, pool);
1991 if (err)
1992 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1994 config = apr_hash_get(opt_baton.config, SVN_CONFIG_CATEGORY_CONFIG,
1995 APR_HASH_KEY_STRING);
1997 apr_signal(SIGINT, signal_handler);
1999 #ifdef SIGBREAK
2000 /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */
2001 apr_signal(SIGBREAK, signal_handler);
2002 #endif
2004 #ifdef SIGHUP
2005 apr_signal(SIGHUP, signal_handler);
2006 #endif
2008 #ifdef SIGTERM
2009 apr_signal(SIGTERM, signal_handler);
2010 #endif
2012 #ifdef SIGPIPE
2013 /* Disable SIGPIPE generation for the platforms that have it. */
2014 apr_signal(SIGPIPE, SIG_IGN);
2015 #endif
2017 #ifdef SIGXFSZ
2018 /* Disable SIGXFSZ generation for the platforms that have it,
2019 otherwise working with large files when compiled against an APR
2020 that doesn't have large file support will crash the program,
2021 which is uncool. */
2022 apr_signal(SIGXFSZ, SIG_IGN);
2023 #endif
2025 err = svn_cmdline_setup_auth_baton(&opt_baton.source_auth_baton,
2026 opt_baton.non_interactive,
2027 opt_baton.source_username,
2028 opt_baton.source_password,
2029 opt_baton.config_dir,
2030 opt_baton.no_auth_cache,
2031 config,
2032 check_cancel, NULL,
2033 pool);
2034 if (! err)
2035 err = svn_cmdline_setup_auth_baton(&opt_baton.sync_auth_baton,
2036 opt_baton.non_interactive,
2037 opt_baton.sync_username,
2038 opt_baton.sync_password,
2039 opt_baton.config_dir,
2040 opt_baton.no_auth_cache,
2041 config,
2042 check_cancel, NULL,
2043 pool);
2044 if (! err)
2045 err = (*subcommand->cmd_func)(os, &opt_baton, pool);
2046 if (err)
2048 /* For argument-related problems, suggest using the 'help'
2049 subcommand. */
2050 if (err->apr_err == SVN_ERR_CL_INSUFFICIENT_ARGS
2051 || err->apr_err == SVN_ERR_CL_ARG_PARSING_ERROR)
2053 err = svn_error_quick_wrap(err,
2054 _("Try 'svnsync help' for more info"));
2057 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2060 svn_pool_destroy(pool);
2062 return EXIT_SUCCESS;