When svnsync checks to see if the server supports partial replay,
[svn.git] / subversion / svnsync / main.c
blob825ec36d97397bec9f4c2407971c353264ab465a
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_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 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_open2(&to_session, baton->to_url, &(baton->sync_callbacks),
707 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;
1240 const char *uuid;
1242 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL,
1243 &from_url, pool));
1244 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_UUID,
1245 &from_uuid, pool));
1246 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_LAST_MERGED_REV,
1247 last_merged_rev, pool));
1249 if (! from_url || ! from_uuid || ! *last_merged_rev)
1250 return svn_error_create
1251 (APR_EINVAL, NULL,
1252 _("Destination repository has not been initialized"));
1254 /* Open the session to copy the revision data. */
1255 SVN_ERR(svn_ra_open2(from_session, from_url->data, callbacks, baton,
1256 config, pool));
1258 /* Ok, now sanity check the UUID of the source repository, it
1259 wouldn't be a good thing to sync from a different repository. */
1261 SVN_ERR(svn_ra_get_uuid2(*from_session, &uuid, pool));
1263 if (strcmp(uuid, from_uuid->data) != 0)
1264 return svn_error_createf(APR_EINVAL, NULL,
1265 _("UUID of source repository (%s) does not "
1266 "match expected UUID (%s)"),
1267 uuid, from_uuid->data);
1269 return SVN_NO_ERROR;
1272 /* Replay baton, used during sychnronization. */
1273 typedef struct {
1274 svn_ra_session_t *from_session;
1275 svn_ra_session_t *to_session;
1276 subcommand_baton_t *sb;
1277 } replay_baton_t;
1279 /* Return a replay baton allocated from POOL and populated with
1280 data from the provided parameters. */
1281 static replay_baton_t *
1282 make_replay_baton(svn_ra_session_t *from_session,
1283 svn_ra_session_t *to_session,
1284 subcommand_baton_t *sb, apr_pool_t *pool)
1286 replay_baton_t *rb = apr_pcalloc(pool, sizeof(*rb));
1287 rb->from_session = from_session;
1288 rb->to_session = to_session;
1289 rb->sb = sb;
1290 return rb;
1293 /* Filter out svn:date and svn:author properties. */
1294 static svn_boolean_t
1295 filter_exclude_date_author_sync(const char *key)
1297 if (strncmp(key, SVN_PROP_REVISION_AUTHOR,
1298 sizeof(SVN_PROP_REVISION_AUTHOR) - 1) == 0)
1299 return TRUE;
1300 else if (strncmp(key, SVN_PROP_REVISION_DATE,
1301 sizeof(SVN_PROP_REVISION_DATE) - 1) == 0)
1302 return TRUE;
1303 else if (strncmp(key, SVNSYNC_PROP_PREFIX,
1304 sizeof(SVNSYNC_PROP_PREFIX) - 1) == 0)
1305 return TRUE;
1307 return FALSE;
1310 /* Filter out all properties except svn:date and svn:author */
1311 static svn_boolean_t
1312 filter_include_date_author_sync(const char *key)
1314 return ! filter_exclude_date_author_sync(key);
1317 /* Callback function for svn_ra_replay_range, invoked when starting to parse
1318 * a replay report.
1320 static svn_error_t *
1321 replay_rev_started(svn_revnum_t revision,
1322 void *replay_baton,
1323 const svn_delta_editor_t **editor,
1324 void **edit_baton,
1325 apr_hash_t *rev_props,
1326 apr_pool_t *pool)
1328 const svn_delta_editor_t *commit_editor;
1329 const svn_delta_editor_t *cancel_editor;
1330 const svn_delta_editor_t *sync_editor;
1331 void *commit_baton;
1332 void *cancel_baton;
1333 void *sync_baton;
1334 replay_baton_t *rb = replay_baton;
1335 apr_hash_t *filtered;
1336 int filtered_count;
1338 /* We set this property so that if we error out for some reason
1339 we can later determine where we were in the process of
1340 merging a revision. If we had committed the change, but we
1341 hadn't finished copying the revprops we need to know that, so
1342 we can go back and finish the job before we move on.
1344 NOTE: We have to set this before we start the commit editor,
1345 because ra_svn doesn't let you change rev props during a
1346 commit. */
1347 SVN_ERR(svn_ra_change_rev_prop(rb->to_session, 0,
1348 SVNSYNC_PROP_CURRENTLY_COPYING,
1349 svn_string_createf(pool, "%ld",
1350 revision),
1351 pool));
1353 /* The actual copy is just a replay hooked up to a commit. Include
1354 all the revision properties from the source repositories, except
1355 'svn:author' and 'svn:date', those are not guaranteed to get
1356 through the editor anyway. */
1357 filtered = filter_props(&filtered_count, rev_props,
1358 filter_exclude_date_author_sync,
1359 pool);
1361 /* svn_ra_get_commit_editor3 requires the log message to be
1362 set. It's possible that we didn't receive 'svn:log' here, so we
1363 have to set it to at least the empty string. If there's a svn:log
1364 property on this revision, we will write the actual value in the
1365 replay_rev_finished callback. */
1366 if (! apr_hash_get(filtered, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING))
1367 apr_hash_set(filtered, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING,
1368 svn_string_create("", pool));
1370 SVN_ERR(svn_ra_get_commit_editor3(rb->to_session, &commit_editor,
1371 &commit_baton,
1372 filtered,
1373 commit_callback, rb->sb,
1374 NULL, FALSE, pool));
1376 /* There's one catch though, the diff shows us props we can't send
1377 over the RA interface, so we need an editor that's smart enough
1378 to filter those out for us. */
1379 SVN_ERR(get_sync_editor(commit_editor, commit_baton, revision - 1,
1380 rb->sb->to_url, rb->sb->quiet,
1381 &sync_editor, &sync_baton, pool));
1383 SVN_ERR(svn_delta_get_cancellation_editor(check_cancel, NULL,
1384 sync_editor, sync_baton,
1385 &cancel_editor,
1386 &cancel_baton,
1387 pool));
1388 *editor = cancel_editor;
1389 *edit_baton = cancel_baton;
1391 return SVN_NO_ERROR;
1394 /* Callback function for svn_ra_replay_range, invoked when finishing parsing
1395 * a replay report.
1397 static svn_error_t *
1398 replay_rev_finished(svn_revnum_t revision,
1399 void *replay_baton,
1400 const svn_delta_editor_t *editor,
1401 void *edit_baton,
1402 apr_hash_t *rev_props,
1403 apr_pool_t *pool)
1405 apr_pool_t *subpool = svn_pool_create(pool);
1406 replay_baton_t *rb = replay_baton;
1407 apr_hash_t *filtered, *existing_props;
1408 int filtered_count;
1410 SVN_ERR(editor->close_edit(edit_baton, pool));
1412 /* Sanity check that we actually committed the revision we meant to. */
1413 if (rb->sb->committed_rev != revision)
1414 return svn_error_createf
1415 (APR_EINVAL, NULL,
1416 _("Commit created rev %ld but should have created %ld"),
1417 rb->sb->committed_rev, revision);
1419 SVN_ERR(svn_ra_rev_proplist(rb->to_session, revision, &existing_props,
1420 subpool));
1423 /* Ok, we're done with the data, now we just need to copy the remaining
1424 'svn:date' and 'svn:author' revprops and we're all set. */
1425 filtered = filter_props(&filtered_count, rev_props,
1426 filter_include_date_author_sync,
1427 subpool);
1428 SVN_ERR(write_revprops(&filtered_count, rb->to_session, revision, filtered,
1429 subpool));
1431 /* Remove all extra properties in TARGET. */
1432 SVN_ERR(remove_props_not_in_source(rb->to_session, revision,
1433 rev_props, existing_props, subpool));
1435 svn_pool_clear(subpool);
1437 /* Ok, we're done, bring the last-merged-rev property up to date. */
1438 SVN_ERR(svn_ra_change_rev_prop
1439 (rb->to_session,
1441 SVNSYNC_PROP_LAST_MERGED_REV,
1442 svn_string_create(apr_psprintf(pool, "%ld", revision),
1443 subpool),
1444 subpool));
1446 /* And finally drop the currently copying prop, since we're done
1447 with this revision. */
1448 SVN_ERR(svn_ra_change_rev_prop(rb->to_session, 0,
1449 SVNSYNC_PROP_CURRENTLY_COPYING,
1450 NULL, subpool));
1452 /* Notify the user that we copied revision properties. */
1453 if (! rb->sb->quiet)
1454 SVN_ERR(log_properties_copied(filtered_count > 0, revision, subpool));
1456 svn_pool_destroy(subpool);
1458 return SVN_NO_ERROR;
1461 /* Synchronize the repository associated with RA session TO_SESSION,
1462 * using information found in baton B, while the repository is
1463 * locked. Implements `with_locked_func_t' interface.
1465 static svn_error_t *
1466 do_synchronize(svn_ra_session_t *to_session,
1467 subcommand_baton_t *baton, apr_pool_t *pool)
1469 svn_string_t *last_merged_rev;
1470 svn_revnum_t from_latest;
1471 svn_ra_session_t *from_session;
1472 svn_string_t *currently_copying;
1473 svn_revnum_t to_latest, copying, last_merged;
1474 svn_revnum_t start_revision, end_revision;
1475 replay_baton_t *rb;
1477 SVN_ERR(open_source_session(&from_session,
1478 &last_merged_rev, to_session,
1479 &(baton->source_callbacks), baton->config,
1480 baton, pool));
1482 /* Check to see if we have revprops that still need to be copied for
1483 a prior revision we didn't finish copying. But first, check for
1484 state sanity. Remember, mirroring is not an atomic action,
1485 because revision properties are copied separately from the
1486 revision's contents.
1488 So, any time that currently-copying is not set, then
1489 last-merged-rev should be the HEAD revision of the destination
1490 repository. That is, if we didn't fall over in the middle of a
1491 previous synchronization, then our destination repository should
1492 have exactly as many revisions in it as we've synchronized.
1494 Alternately, if currently-copying *is* set, it must
1495 be either last-merged-rev or last-merged-rev + 1, and the HEAD
1496 revision must be equal to either last-merged-rev or
1497 currently-copying. If this is not the case, somebody has meddled
1498 with the destination without using svnsync.
1501 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_CURRENTLY_COPYING,
1502 &currently_copying, pool));
1504 SVN_ERR(svn_ra_get_latest_revnum(to_session, &to_latest, pool));
1506 last_merged = SVN_STR_TO_REV(last_merged_rev->data);
1508 if (currently_copying)
1510 copying = SVN_STR_TO_REV(currently_copying->data);
1512 if ((copying < last_merged)
1513 || (copying > (last_merged + 1))
1514 || ((to_latest != last_merged) && (to_latest != copying)))
1516 return svn_error_createf
1517 (APR_EINVAL, NULL,
1518 _("Revision being currently copied (%ld), last merged revision "
1519 "(%ld), and destination HEAD (%ld) are inconsistent; have you "
1520 "committed to the destination without using svnsync?"),
1521 copying, last_merged, to_latest);
1523 else if (copying == to_latest)
1525 if (copying > last_merged)
1527 SVN_ERR(copy_revprops(from_session, to_session,
1528 to_latest, TRUE, baton->quiet,
1529 pool));
1530 last_merged = copying;
1531 last_merged_rev = svn_string_create
1532 (apr_psprintf(pool, "%ld", last_merged), pool);
1535 /* Now update last merged rev and drop currently changing.
1536 Note that the order here is significant, if we do them
1537 in the wrong order there are race conditions where we
1538 end up not being able to tell if there have been bogus
1539 (i.e. non-svnsync) commits to the dest repository. */
1541 SVN_ERR(svn_ra_change_rev_prop(to_session, 0,
1542 SVNSYNC_PROP_LAST_MERGED_REV,
1543 last_merged_rev, pool));
1544 SVN_ERR(svn_ra_change_rev_prop(to_session, 0,
1545 SVNSYNC_PROP_CURRENTLY_COPYING,
1546 NULL, pool));
1548 /* If copying > to_latest, then we just fall through to
1549 attempting to copy the revision again. */
1551 else
1553 if (to_latest != last_merged)
1554 return svn_error_createf(APR_EINVAL, NULL,
1555 _("Destination HEAD (%ld) is not the last "
1556 "merged revision (%ld); have you "
1557 "committed to the destination without "
1558 "using svnsync?"),
1559 to_latest, last_merged);
1562 /* Now check to see if there are any revisions to copy. */
1563 SVN_ERR(svn_ra_get_latest_revnum(from_session, &from_latest, pool));
1565 if (from_latest < atol(last_merged_rev->data))
1566 return SVN_NO_ERROR;
1568 /* Ok, so there are new revisions, iterate over them copying them
1569 into the destination repository. */
1570 rb = make_replay_baton(from_session, to_session, baton, pool);
1572 start_revision = atol(last_merged_rev->data) + 1;
1573 end_revision = from_latest;
1575 SVN_ERR(check_cancel(NULL));
1577 SVN_ERR(svn_ra_replay_range(from_session, start_revision, end_revision,
1578 0, TRUE, replay_rev_started,
1579 replay_rev_finished, rb, pool));
1581 return SVN_NO_ERROR;
1585 /* SUBCOMMAND: sync */
1586 static svn_error_t *
1587 synchronize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
1589 svn_ra_session_t *to_session;
1590 opt_baton_t *opt_baton = b;
1591 apr_array_header_t *targets;
1592 subcommand_baton_t *baton;
1593 const char *to_url;
1595 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
1596 apr_array_make(pool, 0,
1597 sizeof(const char *)),
1598 pool));
1599 if (targets->nelts < 1)
1600 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1601 if (targets->nelts > 1)
1602 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1604 to_url = APR_ARRAY_IDX(targets, 0, const char *);
1606 if (! svn_path_is_url(to_url))
1607 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1608 _("Path '%s' is not a URL"), to_url);
1610 baton = make_subcommand_baton(opt_baton, to_url, NULL, 0, 0, pool);
1611 SVN_ERR(svn_ra_open2(&to_session, baton->to_url, &(baton->sync_callbacks),
1612 baton, baton->config, pool));
1613 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
1614 SVN_ERR(with_locked(to_session, do_synchronize, baton, pool));
1616 return SVN_NO_ERROR;
1621 /*** `svnsync copy-revprops' ***/
1623 /* Copy revision properties to the repository associated with RA
1624 * session TO_SESSION, using information found in baton B, while the
1625 * repository is locked. Implements `with_locked_func_t' interface.
1627 static svn_error_t *
1628 do_copy_revprops(svn_ra_session_t *to_session,
1629 subcommand_baton_t *baton, apr_pool_t *pool)
1631 svn_ra_session_t *from_session;
1632 svn_string_t *last_merged_rev;
1633 svn_revnum_t i;
1634 svn_revnum_t step = 1;
1636 SVN_ERR(open_source_session(&from_session, &last_merged_rev,
1637 to_session,
1638 &(baton->source_callbacks), baton->config,
1639 baton, pool));
1641 /* An invalid revision means "last-synced" */
1642 if (! SVN_IS_VALID_REVNUM(baton->start_rev))
1643 baton->start_rev = SVN_STR_TO_REV(last_merged_rev->data);
1644 if (! SVN_IS_VALID_REVNUM(baton->end_rev))
1645 baton->end_rev = SVN_STR_TO_REV(last_merged_rev->data);
1647 /* Make sure we have revisions within the valid range. */
1648 if (baton->start_rev > SVN_STR_TO_REV(last_merged_rev->data))
1649 return svn_error_createf
1650 (APR_EINVAL, NULL,
1651 _("Cannot copy revprops for a revision (%ld) that has not "
1652 "been synchronized yet"), baton->start_rev);
1653 if (baton->end_rev > SVN_STR_TO_REV(last_merged_rev->data))
1654 return svn_error_createf
1655 (APR_EINVAL, NULL,
1656 _("Cannot copy revprops for a revision (%ld) that has not "
1657 "been synchronized yet"), baton->end_rev);
1659 /* Now, copy all the requested revisions, in the requested order. */
1660 step = (baton->start_rev > baton->end_rev) ? -1 : 1;
1661 for (i = baton->start_rev; i != baton->end_rev + step; i = i + step)
1663 SVN_ERR(check_cancel(NULL));
1664 SVN_ERR(copy_revprops(from_session, to_session, i, FALSE,
1665 baton->quiet, pool));
1668 return SVN_NO_ERROR;
1672 /* SUBCOMMAND: copy-revprops */
1673 static svn_error_t *
1674 copy_revprops_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
1676 svn_ra_session_t *to_session;
1677 opt_baton_t *opt_baton = b;
1678 apr_array_header_t *targets;
1679 subcommand_baton_t *baton;
1680 const char *to_url;
1681 svn_opt_revision_t start_revision, end_revision;
1682 svn_revnum_t start_rev = 0, end_rev = SVN_INVALID_REVNUM;
1684 /* There should be either one or two arguments left to parse. */
1685 if (os->argc - os->ind > 2)
1686 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1687 if (os->argc - os->ind < 1)
1688 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1690 /* If there are two args, the last one is a revision range. We'll
1691 effectively pop it from the end of the list. Why? Because
1692 svn_opt_args_to_target_array2() does waaaaay too many useful
1693 things for us not to use it. */
1694 if (os->argc - os->ind == 2)
1696 const char *rev_str = os->argv[--(os->argc)];
1698 start_revision.kind = svn_opt_revision_unspecified;
1699 end_revision.kind = svn_opt_revision_unspecified;
1700 if ((svn_opt_parse_revision(&start_revision, &end_revision,
1701 rev_str, pool) != 0)
1702 || ((start_revision.kind != svn_opt_revision_number)
1703 && (start_revision.kind != svn_opt_revision_head))
1704 || ((end_revision.kind != svn_opt_revision_number)
1705 && (end_revision.kind != svn_opt_revision_head)
1706 && (end_revision.kind != svn_opt_revision_unspecified)))
1707 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1708 _("'%s' is not a valid revision range"),
1709 rev_str);
1711 /* Get the start revision, which must be either HEAD or a number
1712 (which is required to be a valid one). */
1713 if (start_revision.kind == svn_opt_revision_head)
1715 start_rev = SVN_INVALID_REVNUM;
1717 else
1719 start_rev = start_revision.value.number;
1720 if (! SVN_IS_VALID_REVNUM(start_rev))
1721 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1722 _("Invalid revision number (%ld)"),
1723 start_rev);
1726 /* Get the end revision, which must be unspecified (meaning,
1727 "same as the start_rev"), HEAD, or a number (which is
1728 required to be a valid one). */
1729 if (end_revision.kind == svn_opt_revision_unspecified)
1731 end_rev = start_rev;
1733 else if (end_revision.kind == svn_opt_revision_head)
1735 end_rev = SVN_INVALID_REVNUM;
1737 else
1739 end_rev = end_revision.value.number;
1740 if (! SVN_IS_VALID_REVNUM(end_rev))
1741 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1742 _("Invalid revision number (%ld)"),
1743 end_rev);
1747 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
1748 apr_array_make(pool, 1,
1749 sizeof(const char *)),
1750 pool));
1751 if (targets->nelts != 1)
1752 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1754 to_url = APR_ARRAY_IDX(targets, 0, const char *);
1756 if (! svn_path_is_url(to_url))
1757 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1758 _("Path '%s' is not a URL"), to_url);
1760 baton = make_subcommand_baton(opt_baton, to_url, NULL,
1761 start_rev, end_rev, pool);
1762 SVN_ERR(svn_ra_open2(&to_session, baton->to_url, &(baton->sync_callbacks),
1763 baton, baton->config, pool));
1764 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
1765 SVN_ERR(with_locked(to_session, do_copy_revprops, baton, pool));
1767 return SVN_NO_ERROR;
1772 /*** `svnsync help' ***/
1775 /* SUBCOMMAND: help */
1776 static svn_error_t *
1777 help_cmd(apr_getopt_t *os, void *baton, apr_pool_t *pool)
1779 opt_baton_t *opt_baton = baton;
1781 const char *header =
1782 _("general usage: svnsync SUBCOMMAND DEST_URL [ARGS & OPTIONS ...]\n"
1783 "Type 'svnsync help <subcommand>' for help on a specific subcommand.\n"
1784 "Type 'svnsync --version' to see the program version and RA modules.\n"
1785 "\n"
1786 "Available subcommands:\n");
1788 const char *ra_desc_start
1789 = _("The following repository access (RA) modules are available:\n\n");
1791 svn_stringbuf_t *version_footer = svn_stringbuf_create(ra_desc_start,
1792 pool);
1794 SVN_ERR(svn_ra_print_modules(version_footer, pool));
1796 SVN_ERR(svn_opt_print_help(os, "svnsync",
1797 opt_baton ? opt_baton->version : FALSE,
1798 FALSE, version_footer->data, header,
1799 svnsync_cmd_table, svnsync_options, NULL,
1800 pool));
1802 return SVN_NO_ERROR;
1807 /*** Main ***/
1810 main(int argc, const char *argv[])
1812 const svn_opt_subcommand_desc_t *subcommand = NULL;
1813 apr_array_header_t *received_opts;
1814 opt_baton_t opt_baton;
1815 svn_config_t *config;
1816 apr_status_t apr_err;
1817 apr_getopt_t *os;
1818 apr_pool_t *pool;
1819 svn_error_t *err;
1820 int opt_id, i;
1821 const char *username = NULL, *source_username = NULL, *sync_username = NULL;
1822 const char *password = NULL, *source_password = NULL, *sync_password = NULL;
1824 if (svn_cmdline_init("svnsync", stderr) != EXIT_SUCCESS)
1826 return EXIT_FAILURE;
1829 err = check_lib_versions();
1830 if (err)
1831 return svn_cmdline_handle_exit_error(err, NULL, "svnsync: ");
1833 pool = svn_pool_create(NULL);
1835 err = svn_ra_initialize(pool);
1836 if (err)
1837 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1839 memset(&opt_baton, 0, sizeof(opt_baton));
1841 received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int));
1843 if (argc <= 1)
1845 help_cmd(NULL, NULL, pool);
1846 svn_pool_destroy(pool);
1847 return EXIT_FAILURE;
1850 err = svn_cmdline__getopt_init(&os, argc, argv, pool);
1851 if (err)
1852 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1854 os->interleave = 1;
1856 for (;;)
1858 const char *opt_arg;
1860 apr_err = apr_getopt_long(os, svnsync_options, &opt_id, &opt_arg);
1861 if (APR_STATUS_IS_EOF(apr_err))
1862 break;
1863 else if (apr_err)
1865 help_cmd(NULL, NULL, pool);
1866 svn_pool_destroy(pool);
1867 return EXIT_FAILURE;
1870 APR_ARRAY_PUSH(received_opts, int) = opt_id;
1872 switch (opt_id)
1874 case svnsync_opt_non_interactive:
1875 opt_baton.non_interactive = TRUE;
1876 break;
1878 case svnsync_opt_no_auth_cache:
1879 opt_baton.no_auth_cache = TRUE;
1880 break;
1882 case svnsync_opt_auth_username:
1883 username = opt_arg;
1884 break;
1886 case svnsync_opt_auth_password:
1887 password = opt_arg;
1888 break;
1890 case svnsync_opt_source_username:
1891 source_username = opt_arg;
1892 break;
1894 case svnsync_opt_source_password:
1895 source_password = opt_arg;
1896 break;
1898 case svnsync_opt_sync_username:
1899 sync_username = opt_arg;
1900 break;
1902 case svnsync_opt_sync_password:
1903 sync_password = opt_arg;
1904 break;
1906 case svnsync_opt_config_dir:
1907 opt_baton.config_dir = opt_arg;
1908 break;
1910 case svnsync_opt_version:
1911 opt_baton.version = TRUE;
1912 break;
1914 case 'q':
1915 opt_baton.quiet = TRUE;
1916 break;
1918 case '?':
1919 case 'h':
1920 opt_baton.help = TRUE;
1921 break;
1923 default:
1925 help_cmd(NULL, NULL, pool);
1926 svn_pool_destroy(pool);
1927 return EXIT_FAILURE;
1932 if (opt_baton.help)
1933 subcommand = svn_opt_get_canonical_subcommand(svnsync_cmd_table, "help");
1935 /* Disallow the mixing --username/password with their --source- and
1936 --sync- variants. Treat "--username FOO" as "--source-username
1937 FOO --sync-username FOO"; ditto for "--password FOO". */
1938 if ((username || password)
1939 && (source_username || sync_username
1940 || source_password || sync_password))
1942 err = svn_error_create
1943 (SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1944 _("Cannot use --username or --password with any of "
1945 "--source-username, --source-password, --sync-username, "
1946 "or --sync-password.\n"));
1947 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1949 if (username)
1951 source_username = username;
1952 sync_username = username;
1954 if (password)
1956 source_password = password;
1957 sync_password = password;
1959 opt_baton.source_username = source_username;
1960 opt_baton.source_password = source_password;
1961 opt_baton.sync_username = sync_username;
1962 opt_baton.sync_password = sync_password;
1964 err = svn_config_ensure(opt_baton.config_dir, pool);
1965 if (err)
1966 return svn_cmdline_handle_exit_error(err, pool, "synsync: ");
1968 if (subcommand == NULL)
1970 if (os->ind >= os->argc)
1972 if (opt_baton.version)
1974 /* Use the "help" subcommand to handle "--version". */
1975 static const svn_opt_subcommand_desc_t pseudo_cmd =
1976 { "--version", help_cmd, {0}, "",
1977 {svnsync_opt_version, /* must accept its own option */
1978 } };
1980 subcommand = &pseudo_cmd;
1982 else
1984 help_cmd(NULL, NULL, pool);
1985 svn_pool_destroy(pool);
1986 return EXIT_FAILURE;
1989 else
1991 const char *first_arg = os->argv[os->ind++];
1992 subcommand = svn_opt_get_canonical_subcommand(svnsync_cmd_table,
1993 first_arg);
1994 if (subcommand == NULL)
1996 help_cmd(NULL, NULL, pool);
1997 svn_pool_destroy(pool);
1998 return EXIT_FAILURE;
2003 for (i = 0; i < received_opts->nelts; ++i)
2005 opt_id = APR_ARRAY_IDX(received_opts, i, int);
2007 if (opt_id == 'h' || opt_id == '?')
2008 continue;
2010 if (! svn_opt_subcommand_takes_option(subcommand, opt_id))
2012 const char *optstr;
2013 const apr_getopt_option_t *badopt =
2014 svn_opt_get_option_from_code(opt_id, svnsync_options);
2015 svn_opt_format_option(&optstr, badopt, FALSE, pool);
2016 if (subcommand->name[0] == '-')
2018 help_cmd(NULL, NULL, pool);
2020 else
2022 err = svn_error_createf
2023 (SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
2024 _("Subcommand '%s' doesn't accept option '%s'\n"
2025 "Type 'svnsync help %s' for usage.\n"),
2026 subcommand->name, optstr, subcommand->name);
2027 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2032 err = svn_config_get_config(&opt_baton.config, opt_baton.config_dir, pool);
2033 if (err)
2034 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2036 config = apr_hash_get(opt_baton.config, SVN_CONFIG_CATEGORY_CONFIG,
2037 APR_HASH_KEY_STRING);
2039 apr_signal(SIGINT, signal_handler);
2041 #ifdef SIGBREAK
2042 /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */
2043 apr_signal(SIGBREAK, signal_handler);
2044 #endif
2046 #ifdef SIGHUP
2047 apr_signal(SIGHUP, signal_handler);
2048 #endif
2050 #ifdef SIGTERM
2051 apr_signal(SIGTERM, signal_handler);
2052 #endif
2054 #ifdef SIGPIPE
2055 /* Disable SIGPIPE generation for the platforms that have it. */
2056 apr_signal(SIGPIPE, SIG_IGN);
2057 #endif
2059 #ifdef SIGXFSZ
2060 /* Disable SIGXFSZ generation for the platforms that have it,
2061 otherwise working with large files when compiled against an APR
2062 that doesn't have large file support will crash the program,
2063 which is uncool. */
2064 apr_signal(SIGXFSZ, SIG_IGN);
2065 #endif
2067 err = svn_cmdline_setup_auth_baton(&opt_baton.source_auth_baton,
2068 opt_baton.non_interactive,
2069 opt_baton.source_username,
2070 opt_baton.source_password,
2071 opt_baton.config_dir,
2072 opt_baton.no_auth_cache,
2073 config,
2074 check_cancel, NULL,
2075 pool);
2076 if (! err)
2077 err = svn_cmdline_setup_auth_baton(&opt_baton.sync_auth_baton,
2078 opt_baton.non_interactive,
2079 opt_baton.sync_username,
2080 opt_baton.sync_password,
2081 opt_baton.config_dir,
2082 opt_baton.no_auth_cache,
2083 config,
2084 check_cancel, NULL,
2085 pool);
2086 if (! err)
2087 err = (*subcommand->cmd_func)(os, &opt_baton, pool);
2088 if (err)
2090 /* For argument-related problems, suggest using the 'help'
2091 subcommand. */
2092 if (err->apr_err == SVN_ERR_CL_INSUFFICIENT_ARGS
2093 || err->apr_err == SVN_ERR_CL_ARG_PARSING_ERROR)
2095 err = svn_error_quick_wrap(err,
2096 _("Try 'svnsync help' for more info"));
2099 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2102 svn_pool_destroy(pool);
2104 return EXIT_SUCCESS;