In the command-line client, forbid
[svn.git] / subversion / svnsync / main.c
blob5a5f73e141c3d5f1f64bc3b3936ff92c1ef4260b
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 typedef svn_error_t *(*with_locked_func_t)(svn_ra_session_t *session,
266 void *baton,
267 apr_pool_t *pool);
270 /* Lock the repository associated with RA SESSION, then execute the
271 * given FUNC/BATON pair while holding the lock. Finally, drop the
272 * lock once it finishes.
274 static svn_error_t *
275 with_locked(svn_ra_session_t *session,
276 with_locked_func_t func,
277 void *baton,
278 apr_pool_t *pool)
280 svn_error_t *err, *err2;
282 SVN_ERR(get_lock(session, pool));
284 err = func(session, baton, pool);
286 err2 = svn_ra_change_rev_prop(session, 0, SVNSYNC_PROP_LOCK, NULL, pool);
287 if (err2 && err)
289 svn_error_clear(err2); /* XXX what to do here? */
291 return err;
293 else if (err2)
295 return err2;
297 else
299 return err;
304 /* Callback function for the RA session's open_tmp_file()
305 * requirements.
307 static svn_error_t *
308 open_tmp_file(apr_file_t **fp, void *callback_baton, apr_pool_t *pool)
310 const char *path;
312 SVN_ERR(svn_io_temp_dir(&path, pool));
314 path = svn_path_join(path, "tempfile", pool);
316 SVN_ERR(svn_io_open_unique_file2(fp, NULL, path, ".tmp",
317 svn_io_file_del_on_close, pool));
319 return SVN_NO_ERROR;
323 /* Return SVN_NO_ERROR iff URL identifies the root directory of the
324 * repository associated with RA session SESS.
326 static svn_error_t *
327 check_if_session_is_at_repos_root(svn_ra_session_t *sess,
328 const char *url,
329 apr_pool_t *pool)
331 const char *sess_root;
333 SVN_ERR(svn_ra_get_repos_root(sess, &sess_root, pool));
335 if (strcmp(url, sess_root) == 0)
336 return SVN_NO_ERROR;
337 else
338 return svn_error_createf
339 (APR_EINVAL, NULL,
340 _("Session is rooted at '%s' but the repos root is '%s'"),
341 url, sess_root);
345 /* Remove the properties in TARGET_PROPS but not in SOURCE_PROPS from
346 * revision REV of the repository associated with RA session SESSION.
348 * All allocations will be done in a subpool of POOL.
350 static svn_error_t *
351 remove_props_not_in_source(svn_ra_session_t *session,
352 svn_revnum_t rev,
353 apr_hash_t *source_props,
354 apr_hash_t *target_props,
355 apr_pool_t *pool)
357 apr_pool_t *subpool = svn_pool_create(pool);
358 apr_hash_index_t *hi;
360 for (hi = apr_hash_first(pool, target_props);
362 hi = apr_hash_next(hi))
364 const void *key;
366 svn_pool_clear(subpool);
368 apr_hash_this(hi, &key, NULL, NULL);
370 /* Delete property if the key can't be found in SOURCE_PROPS. */
371 if (! apr_hash_get(source_props, key, APR_HASH_KEY_STRING))
372 SVN_ERR(svn_ra_change_rev_prop(session, rev, key, NULL,
373 subpool));
376 svn_pool_destroy(subpool);
378 return SVN_NO_ERROR;
381 /* Filter callback function.
382 * Takes a property name KEY, and is expected to return TRUE if the property
383 * should be filtered out (ie. not be copied to the target list), or FALSE if
384 * not.
386 typedef svn_boolean_t (*filter_func_t)(const char *key);
388 /* Make a new set of properties, by copying those properties in PROPS for which
389 * the filter FILTER returns FALSE.
391 * The number of filtered properties will be stored in FILTERED_COUNT.
393 * The returned set of properties is allocated from POOL.
395 static apr_hash_t *
396 filter_props(int *filtered_count, apr_hash_t *props,
397 filter_func_t filter,
398 apr_pool_t *pool)
400 apr_hash_index_t *hi;
401 apr_hash_t *filtered = apr_hash_make(pool);
402 *filtered_count = 0;
404 for (hi = apr_hash_first(pool, props); hi ; hi = apr_hash_next(hi))
406 void *val;
407 const void *key;
408 apr_ssize_t len;
410 apr_hash_this(hi, &key, &len, &val);
412 /* Copy all properties:
413 - not matching the exclude pattern if provided OR
414 - matching the include pattern if provided */
415 if (!filter || filter(key) == FALSE)
417 apr_hash_set(filtered, key, APR_HASH_KEY_STRING, val);
419 else
421 *filtered_count += 1;
425 return filtered;
429 /* Write the set of revision properties REV_PROPS to revision REV to the
430 * repository associated with RA session SESSION.
432 * All allocations will be done in a subpool of POOL.
434 static svn_error_t *
435 write_revprops(int *filtered_count,
436 svn_ra_session_t *session,
437 svn_revnum_t rev,
438 apr_hash_t *rev_props,
439 apr_pool_t *pool)
441 apr_pool_t *subpool = svn_pool_create(pool);
442 apr_hash_index_t *hi;
444 *filtered_count = 0;
446 for (hi = apr_hash_first(pool, rev_props); hi; hi = apr_hash_next(hi))
448 const void *key;
449 void *val;
451 svn_pool_clear(subpool);
452 apr_hash_this(hi, &key, NULL, &val);
454 if (strncmp(key, SVNSYNC_PROP_PREFIX,
455 sizeof(SVNSYNC_PROP_PREFIX) - 1) != 0)
457 SVN_ERR(svn_ra_change_rev_prop(session, rev, key, val, subpool));
459 else
461 *filtered_count += 1;
465 svn_pool_destroy(subpool);
467 return SVN_NO_ERROR;
471 static svn_error_t *
472 log_properties_copied(svn_boolean_t syncprops_found,
473 svn_revnum_t rev,
474 apr_pool_t *pool)
476 if (syncprops_found)
477 SVN_ERR(svn_cmdline_printf(pool,
478 _("Copied properties for revision %ld "
479 "(%s* properties skipped).\n"),
480 rev, SVNSYNC_PROP_PREFIX));
481 else
482 SVN_ERR(svn_cmdline_printf(pool,
483 _("Copied properties for revision %ld.\n"),
484 rev));
486 return SVN_NO_ERROR;
489 /* Copy all the revision properties, except for those that have the
490 * "svn:sync-" prefix, from revision REV of the repository associated
491 * with RA session FROM_SESSION, to the repository associated with RA
492 * session TO_SESSION.
494 * If SYNC is TRUE, then properties on the destination revision that
495 * do not exist on the source revision will be removed.
497 static svn_error_t *
498 copy_revprops(svn_ra_session_t *from_session,
499 svn_ra_session_t *to_session,
500 svn_revnum_t rev,
501 svn_boolean_t sync,
502 svn_boolean_t quiet,
503 apr_pool_t *pool)
505 apr_pool_t *subpool = svn_pool_create(pool);
506 apr_hash_t *existing_props, *rev_props;
507 int filtered_count = 0;
509 /* Get the list of revision properties on REV of TARGET. We're only interested
510 in the property names, but we'll get the values 'for free'. */
511 if (sync)
512 SVN_ERR(svn_ra_rev_proplist(to_session, rev, &existing_props, subpool));
514 /* Get the list of revision properties on REV of SOURCE. */
515 SVN_ERR(svn_ra_rev_proplist(from_session, rev, &rev_props, subpool));
517 /* Copy all but the svn:svnsync properties. */
518 SVN_ERR(write_revprops(&filtered_count, to_session, rev, rev_props, pool));
520 /* Delete those properties that were in TARGET but not in SOURCE */
521 if (sync)
522 SVN_ERR(remove_props_not_in_source(to_session, rev,
523 rev_props, existing_props, pool));
525 if (! quiet)
526 SVN_ERR(log_properties_copied(filtered_count > 0, rev, pool));
528 svn_pool_destroy(subpool);
530 return SVN_NO_ERROR;
534 /* Baton for the various subcommands to share. */
535 typedef struct {
536 /* common to all subcommands */
537 apr_hash_t *config;
538 svn_ra_callbacks2_t source_callbacks;
539 svn_ra_callbacks2_t sync_callbacks;
540 svn_boolean_t quiet;
541 const char *to_url;
543 /* initialize only */
544 const char *from_url;
546 /* syncronize only */
547 svn_revnum_t committed_rev;
549 /* copy-revprops only */
550 svn_revnum_t start_rev;
551 svn_revnum_t end_rev;
553 } subcommand_baton_t;
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 void *b,
592 apr_pool_t *pool)
594 svn_ra_session_t *from_session;
595 subcommand_baton_t *baton = b;
596 svn_string_t *from_url;
597 svn_revnum_t latest;
598 const char *uuid;
600 /* First, sanity check to see that we're copying into a brand new repos. */
602 SVN_ERR(svn_ra_get_latest_revnum(to_session, &latest, pool));
604 if (latest != 0)
605 return svn_error_create
606 (APR_EINVAL, NULL,
607 _("Cannot initialize a repository with content in it"));
609 /* And check to see if anyone's run initialize on it before... We
610 may want a --force option to override this check. */
612 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL,
613 &from_url, pool));
615 if (from_url)
616 return svn_error_createf
617 (APR_EINVAL, NULL,
618 _("Destination repository is already synchronizing from '%s'"),
619 from_url->data);
621 /* Now fill in our bookkeeping info in the dest repository. */
623 SVN_ERR(svn_ra_open2(&from_session, baton->from_url,
624 &(baton->source_callbacks), baton,
625 baton->config, pool));
627 SVN_ERR(check_if_session_is_at_repos_root(from_session, baton->from_url,
628 pool));
630 SVN_ERR(svn_ra_change_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL,
631 svn_string_create(baton->from_url, pool),
632 pool));
634 SVN_ERR(svn_ra_get_uuid(from_session, &uuid, pool));
636 SVN_ERR(svn_ra_change_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_UUID,
637 svn_string_create(uuid, pool), pool));
639 SVN_ERR(svn_ra_change_rev_prop(to_session, 0, SVNSYNC_PROP_LAST_MERGED_REV,
640 svn_string_create("0", pool), pool));
642 /* Finally, copy all non-svnsync revprops from rev 0 of the source
643 repos into the dest repos. */
645 SVN_ERR(copy_revprops(from_session, to_session, 0, FALSE,
646 baton->quiet, pool));
648 /* TODO: It would be nice if we could set the dest repos UUID to be
649 equal to the UUID of the source repos, at least optionally. That
650 way people could check out/log/diff using a local fast mirror,
651 but switch --relocate to the actual final repository in order to
652 make changes... But at this time, the RA layer doesn't have a
653 way to set a UUID. */
655 return SVN_NO_ERROR;
659 /* SUBCOMMAND: init */
660 static svn_error_t *
661 initialize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
663 const char *to_url, *from_url;
664 svn_ra_session_t *to_session;
665 opt_baton_t *opt_baton = b;
666 apr_array_header_t *targets;
667 subcommand_baton_t *baton;
669 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
670 apr_array_make(pool, 0,
671 sizeof(const char *)),
672 pool));
673 if (targets->nelts < 2)
674 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
675 if (targets->nelts > 2)
676 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
678 to_url = APR_ARRAY_IDX(targets, 0, const char *);
679 from_url = APR_ARRAY_IDX(targets, 1, const char *);
681 if (! svn_path_is_url(to_url))
682 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
683 _("Path '%s' is not a URL"), to_url);
684 if (! svn_path_is_url(from_url))
685 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
686 _("Path '%s' is not a URL"), from_url);
688 baton = make_subcommand_baton(opt_baton, to_url, from_url, 0, 0, pool);
689 SVN_ERR(svn_ra_open2(&to_session, baton->to_url, &(baton->sync_callbacks),
690 baton, baton->config, pool));
691 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
692 SVN_ERR(with_locked(to_session, do_initialize, baton, pool));
694 return SVN_NO_ERROR;
699 /*** Synchronization Editor ***/
701 /* This editor has a couple of jobs.
703 * First, it needs to filter out the propchanges that can't be passed over
704 * libsvn_ra.
706 * Second, it needs to adjust for the fact that we might not actually have
707 * permission to see all of the data from the remote repository, which means
708 * we could get revisions that are totally empty from our point of view.
710 * Third, it needs to adjust copyfrom paths, adding the root url for the
711 * destination repository to the beginning of them.
715 /* Edit baton */
716 typedef struct {
717 const svn_delta_editor_t *wrapped_editor;
718 void *wrapped_edit_baton;
719 const char *to_url; /* URL we're copying into, for correct copyfrom URLs */
720 svn_boolean_t called_open_root;
721 svn_revnum_t base_revision;
722 svn_boolean_t quiet;
723 } edit_baton_t;
726 /* A dual-purpose baton for files and directories. */
727 typedef struct {
728 void *edit_baton;
729 void *wrapped_node_baton;
730 } node_baton_t;
733 /*** Editor vtable functions ***/
735 static svn_error_t *
736 set_target_revision(void *edit_baton,
737 svn_revnum_t target_revision,
738 apr_pool_t *pool)
740 edit_baton_t *eb = edit_baton;
741 return eb->wrapped_editor->set_target_revision(eb->wrapped_edit_baton,
742 target_revision, pool);
745 static svn_error_t *
746 open_root(void *edit_baton,
747 svn_revnum_t base_revision,
748 apr_pool_t *pool,
749 void **root_baton)
751 edit_baton_t *eb = edit_baton;
752 node_baton_t *dir_baton = apr_palloc(pool, sizeof(*dir_baton));
754 SVN_ERR(eb->wrapped_editor->open_root(eb->wrapped_edit_baton,
755 base_revision, pool,
756 &dir_baton->wrapped_node_baton));
758 eb->called_open_root = TRUE;
759 dir_baton->edit_baton = edit_baton;
760 *root_baton = dir_baton;
762 if (! eb->quiet)
764 SVN_ERR(svn_cmdline_printf(pool, _("Transmitting file data ")));
765 SVN_ERR(svn_cmdline_fflush(stdout));
768 return SVN_NO_ERROR;
771 static svn_error_t *
772 delete_entry(const char *path,
773 svn_revnum_t base_revision,
774 void *parent_baton,
775 apr_pool_t *pool)
777 node_baton_t *pb = parent_baton;
778 edit_baton_t *eb = pb->edit_baton;
780 return eb->wrapped_editor->delete_entry(path, base_revision,
781 pb->wrapped_node_baton, pool);
784 static svn_error_t *
785 add_directory(const char *path,
786 void *parent_baton,
787 const char *copyfrom_path,
788 svn_revnum_t copyfrom_rev,
789 apr_pool_t *pool,
790 void **child_baton)
792 node_baton_t *pb = parent_baton;
793 edit_baton_t *eb = pb->edit_baton;
794 node_baton_t *b = apr_palloc(pool, sizeof(*b));
796 if (copyfrom_path)
797 copyfrom_path = apr_psprintf(pool, "%s%s", eb->to_url,
798 svn_path_uri_encode(copyfrom_path, pool));
800 SVN_ERR(eb->wrapped_editor->add_directory(path, pb->wrapped_node_baton,
801 copyfrom_path,
802 copyfrom_rev, pool,
803 &b->wrapped_node_baton));
805 b->edit_baton = eb;
806 *child_baton = b;
808 return SVN_NO_ERROR;
811 static svn_error_t *
812 open_directory(const char *path,
813 void *parent_baton,
814 svn_revnum_t base_revision,
815 apr_pool_t *pool,
816 void **child_baton)
818 node_baton_t *pb = parent_baton;
819 edit_baton_t *eb = pb->edit_baton;
820 node_baton_t *db = apr_palloc(pool, sizeof(*db));
822 SVN_ERR(eb->wrapped_editor->open_directory(path, pb->wrapped_node_baton,
823 base_revision, pool,
824 &db->wrapped_node_baton));
826 db->edit_baton = eb;
827 *child_baton = db;
829 return SVN_NO_ERROR;
832 static svn_error_t *
833 add_file(const char *path,
834 void *parent_baton,
835 const char *copyfrom_path,
836 svn_revnum_t copyfrom_rev,
837 apr_pool_t *pool,
838 void **file_baton)
840 node_baton_t *pb = parent_baton;
841 edit_baton_t *eb = pb->edit_baton;
842 node_baton_t *fb = apr_palloc(pool, sizeof(*fb));
844 if (copyfrom_path)
845 copyfrom_path = apr_psprintf(pool, "%s%s", eb->to_url,
846 svn_path_uri_encode(copyfrom_path, pool));
848 SVN_ERR(eb->wrapped_editor->add_file(path, pb->wrapped_node_baton,
849 copyfrom_path, copyfrom_rev,
850 pool, &fb->wrapped_node_baton));
852 fb->edit_baton = eb;
853 *file_baton = fb;
855 return SVN_NO_ERROR;
858 static svn_error_t *
859 open_file(const char *path,
860 void *parent_baton,
861 svn_revnum_t base_revision,
862 apr_pool_t *pool,
863 void **file_baton)
865 node_baton_t *pb = parent_baton;
866 edit_baton_t *eb = pb->edit_baton;
867 node_baton_t *fb = apr_palloc(pool, sizeof(*fb));
869 SVN_ERR(eb->wrapped_editor->open_file(path, pb->wrapped_node_baton,
870 base_revision, pool,
871 &fb->wrapped_node_baton));
873 fb->edit_baton = eb;
874 *file_baton = fb;
876 return SVN_NO_ERROR;
879 static svn_error_t *
880 apply_textdelta(void *file_baton,
881 const char *base_checksum,
882 apr_pool_t *pool,
883 svn_txdelta_window_handler_t *handler,
884 void **handler_baton)
886 node_baton_t *fb = file_baton;
887 edit_baton_t *eb = fb->edit_baton;
889 if (! eb->quiet)
891 SVN_ERR(svn_cmdline_printf(pool, "."));
892 SVN_ERR(svn_cmdline_fflush(stdout));
895 return eb->wrapped_editor->apply_textdelta(fb->wrapped_node_baton,
896 base_checksum, pool,
897 handler, handler_baton);
900 static svn_error_t *
901 close_file(void *file_baton,
902 const char *text_checksum,
903 apr_pool_t *pool)
905 node_baton_t *fb = file_baton;
906 edit_baton_t *eb = fb->edit_baton;
907 return eb->wrapped_editor->close_file(fb->wrapped_node_baton,
908 text_checksum, pool);
911 static svn_error_t *
912 absent_file(const char *path,
913 void *file_baton,
914 apr_pool_t *pool)
916 node_baton_t *fb = file_baton;
917 edit_baton_t *eb = fb->edit_baton;
918 return eb->wrapped_editor->absent_file(path, fb->wrapped_node_baton, pool);
921 static svn_error_t *
922 close_directory(void *dir_baton,
923 apr_pool_t *pool)
925 node_baton_t *db = dir_baton;
926 edit_baton_t *eb = db->edit_baton;
927 return eb->wrapped_editor->close_directory(db->wrapped_node_baton, pool);
930 static svn_error_t *
931 absent_directory(const char *path,
932 void *dir_baton,
933 apr_pool_t *pool)
935 node_baton_t *db = dir_baton;
936 edit_baton_t *eb = db->edit_baton;
937 return eb->wrapped_editor->absent_directory(path, db->wrapped_node_baton,
938 pool);
941 static svn_error_t *
942 change_file_prop(void *file_baton,
943 const char *name,
944 const svn_string_t *value,
945 apr_pool_t *pool)
947 node_baton_t *fb = file_baton;
948 edit_baton_t *eb = fb->edit_baton;
950 /* only regular properties can pass over libsvn_ra */
951 if (svn_property_kind(NULL, name) != svn_prop_regular_kind)
952 return SVN_NO_ERROR;
954 return eb->wrapped_editor->change_file_prop(fb->wrapped_node_baton,
955 name, value, pool);
958 static svn_error_t *
959 change_dir_prop(void *dir_baton,
960 const char *name,
961 const svn_string_t *value,
962 apr_pool_t *pool)
964 node_baton_t *db = dir_baton;
965 edit_baton_t *eb = db->edit_baton;
967 /* only regular properties can pass over libsvn_ra */
968 if (svn_property_kind(NULL, name) != svn_prop_regular_kind)
969 return SVN_NO_ERROR;
971 return eb->wrapped_editor->change_dir_prop(db->wrapped_node_baton,
972 name, value, pool);
975 static svn_error_t *
976 close_edit(void *edit_baton,
977 apr_pool_t *pool)
979 edit_baton_t *eb = edit_baton;
981 /* If we haven't opened the root yet, that means we're transfering
982 an empty revision, probably because we aren't allowed to see the
983 contents for some reason. In any event, we need to open the root
984 and close it again, before we can close out the edit, or the
985 commit will fail. */
987 if (! eb->called_open_root)
989 void *baton;
990 SVN_ERR(eb->wrapped_editor->open_root(eb->wrapped_edit_baton,
991 eb->base_revision, pool,
992 &baton));
993 SVN_ERR(eb->wrapped_editor->close_directory(baton, pool));
996 if (! eb->quiet)
998 SVN_ERR(svn_cmdline_printf(pool, "\n"));
1001 return eb->wrapped_editor->close_edit(eb->wrapped_edit_baton, pool);
1004 /*** Editor factory function ***/
1006 /* Set WRAPPED_EDITOR and WRAPPED_EDIT_BATON to an editor/baton pair
1007 * that wraps our own commit EDITOR/EDIT_BATON. BASE_REVISION is the
1008 * revision on which the driver of this returned editor will be basing
1009 * the commit. TO_URL is the URL of the root of the repository into
1010 * which the commit is being made.
1012 static svn_error_t *
1013 get_sync_editor(const svn_delta_editor_t *wrapped_editor,
1014 void *wrapped_edit_baton,
1015 svn_revnum_t base_revision,
1016 const char *to_url,
1017 svn_boolean_t quiet,
1018 const svn_delta_editor_t **editor,
1019 void **edit_baton,
1020 apr_pool_t *pool)
1022 svn_delta_editor_t *tree_editor = svn_delta_default_editor(pool);
1023 edit_baton_t *eb = apr_palloc(pool, sizeof(*eb));
1025 tree_editor->set_target_revision = set_target_revision;
1026 tree_editor->open_root = open_root;
1027 tree_editor->delete_entry = delete_entry;
1028 tree_editor->add_directory = add_directory;
1029 tree_editor->open_directory = open_directory;
1030 tree_editor->change_dir_prop = change_dir_prop;
1031 tree_editor->close_directory = close_directory;
1032 tree_editor->absent_directory = absent_directory;
1033 tree_editor->add_file = add_file;
1034 tree_editor->open_file = open_file;
1035 tree_editor->apply_textdelta = apply_textdelta;
1036 tree_editor->change_file_prop = change_file_prop;
1037 tree_editor->close_file = close_file;
1038 tree_editor->absent_file = absent_file;
1039 tree_editor->close_edit = close_edit;
1041 eb->wrapped_editor = wrapped_editor;
1042 eb->wrapped_edit_baton = wrapped_edit_baton;
1043 eb->called_open_root = FALSE;
1044 eb->base_revision = base_revision;
1045 eb->to_url = to_url;
1046 eb->quiet = quiet;
1048 *editor = tree_editor;
1049 *edit_baton = eb;
1051 return SVN_NO_ERROR;
1056 /*** `svnsync sync' ***/
1058 /* Implements `svn_commit_callback2_t' interface. */
1059 static svn_error_t *
1060 commit_callback(const svn_commit_info_t *commit_info,
1061 void *baton,
1062 apr_pool_t *pool)
1064 subcommand_baton_t *sb = baton;
1066 if (! sb->quiet)
1068 SVN_ERR(svn_cmdline_printf(pool, _("Committed revision %ld.\n"),
1069 commit_info->revision));
1072 sb->committed_rev = commit_info->revision;
1074 return SVN_NO_ERROR;
1078 /* Set *FROM_SESSION to an RA session associated with the source
1079 * repository of the synchronization, as determined by reading
1080 * svn:sync- properties from the destination repository (associated
1081 * with TO_SESSION). Set LAST_MERGED_REV to the value of the property
1082 * which records the most recently synchronized revision.
1084 * CALLBACKS is a vtable of RA callbacks to provide when creating
1085 * *FROM_SESSION. CONFIG is a configuration hash.
1087 static svn_error_t *
1088 open_source_session(svn_ra_session_t **from_session,
1089 svn_string_t **last_merged_rev,
1090 svn_ra_session_t *to_session,
1091 svn_ra_callbacks2_t *callbacks,
1092 apr_hash_t *config,
1093 void *baton,
1094 apr_pool_t *pool)
1096 svn_string_t *from_url, *from_uuid;
1097 const char *uuid;
1099 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL,
1100 &from_url, pool));
1101 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_UUID,
1102 &from_uuid, pool));
1103 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_LAST_MERGED_REV,
1104 last_merged_rev, pool));
1106 if (! from_url || ! from_uuid || ! *last_merged_rev)
1107 return svn_error_create
1108 (APR_EINVAL, NULL,
1109 _("Destination repository has not been initialized"));
1111 /* Open the session to copy the revision data. */
1112 SVN_ERR(svn_ra_open2(from_session, from_url->data, callbacks, baton,
1113 config, pool));
1114 SVN_ERR(check_if_session_is_at_repos_root(*from_session, from_url->data,
1115 pool));
1117 /* Ok, now sanity check the UUID of the source repository, it
1118 wouldn't be a good thing to sync from a different repository. */
1120 SVN_ERR(svn_ra_get_uuid(*from_session, &uuid, pool));
1122 if (strcmp(uuid, from_uuid->data) != 0)
1123 return svn_error_createf(APR_EINVAL, NULL,
1124 _("UUID of source repository (%s) does not "
1125 "match expected UUID (%s)"),
1126 uuid, from_uuid->data);
1128 return SVN_NO_ERROR;
1131 /* Replay baton, used during sychnronization. */
1132 typedef struct {
1133 svn_ra_session_t *from_session;
1134 svn_ra_session_t *to_session;
1135 subcommand_baton_t *sb;
1136 } replay_baton_t;
1138 /* Return a replay baton allocated from POOL and populated with
1139 data from the provided parameters. */
1140 static replay_baton_t *
1141 make_replay_baton(svn_ra_session_t *from_session, svn_ra_session_t *to_session,
1142 subcommand_baton_t *sb, apr_pool_t *pool)
1144 replay_baton_t *rb = apr_pcalloc(pool, sizeof(*rb));
1145 rb->from_session = from_session;
1146 rb->to_session = to_session;
1147 rb->sb = sb;
1148 return rb;
1151 /* Filter out svn:date and svn:author properties. */
1152 static svn_boolean_t filter_exclude_date_author_log_sync(const char *key)
1154 if (strncmp(key, SVN_PROP_REVISION_AUTHOR,
1155 sizeof(SVN_PROP_REVISION_AUTHOR) - 1) == 0)
1156 return TRUE;
1157 else if (strncmp(key, SVN_PROP_REVISION_DATE,
1158 sizeof(SVN_PROP_REVISION_DATE) - 1) == 0)
1159 return TRUE;
1160 else if (strncmp(key, SVN_PROP_REVISION_LOG,
1161 sizeof(SVN_PROP_REVISION_LOG) - 1) == 0)
1162 return TRUE;
1163 else if (strncmp(key, SVNSYNC_PROP_PREFIX,
1164 sizeof(SVNSYNC_PROP_PREFIX) - 1) == 0)
1165 return TRUE;
1167 return FALSE;
1170 /* Filter out all properties except svn:date and svn:author */
1171 static svn_boolean_t filter_include_date_author_log_sync(const char *key)
1173 return ! filter_exclude_date_author_log_sync(key);
1176 /* Callback function for svn_ra_replay_range, invoked when starting to parse
1177 * a replay report.
1179 static svn_error_t *
1180 replay_rev_started(svn_revnum_t revision,
1181 void *replay_baton,
1182 const svn_delta_editor_t **editor,
1183 void **edit_baton,
1184 apr_hash_t *rev_props,
1185 apr_pool_t *pool)
1187 const svn_delta_editor_t *commit_editor;
1188 const svn_delta_editor_t *cancel_editor;
1189 const svn_delta_editor_t *sync_editor;
1190 void *commit_baton;
1191 void *cancel_baton;
1192 void *sync_baton;
1193 replay_baton_t *rb = replay_baton;
1194 apr_hash_t *filtered;
1195 int filtered_count;
1197 /* We set this property so that if we error out for some reason
1198 we can later determine where we were in the process of
1199 merging a revision. If we had committed the change, but we
1200 hadn't finished copying the revprops we need to know that, so
1201 we can go back and finish the job before we move on.
1203 NOTE: We have to set this before we start the commit editor,
1204 because ra_svn doesn't let you change rev props during a
1205 commit. */
1206 SVN_ERR(svn_ra_change_rev_prop(rb->to_session, 0,
1207 SVNSYNC_PROP_CURRENTLY_COPYING,
1208 svn_string_createf(pool, "%ld",
1209 revision),
1210 pool));
1212 /* The actual copy is just a replay hooked up to a commit.
1213 Include all the revision properties from the source repositories, except
1214 svn:author and svn:date, those are not guaranteed to get through the
1215 editor anyway.
1217 filtered = filter_props(&filtered_count, rev_props,
1218 filter_exclude_date_author_log_sync,
1219 pool);
1220 /* svn_ra_get_commit_editor3 requires the log message to be set. It's possible
1221 that we didn't receive 'svn:log' here, so we have to set it to at least
1222 the empty string. If there's a svn:log property on this revision, we will
1223 write the actual value in the replay_rev_finished callback. */
1224 apr_hash_set(filtered, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING,
1225 svn_string_create("", pool));
1227 SVN_ERR(svn_ra_get_commit_editor3(rb->to_session, &commit_editor,
1228 &commit_baton,
1229 filtered,
1230 commit_callback, rb->sb,
1231 NULL, FALSE, pool));
1233 /* There's one catch though, the diff shows us props we can't
1234 send over the RA interface, so we need an editor that's smart
1235 enough to filter those out for us. */
1237 SVN_ERR(get_sync_editor(commit_editor, commit_baton, revision - 1,
1238 rb->sb->to_url, rb->sb->quiet,
1239 &sync_editor, &sync_baton, pool));
1241 SVN_ERR(svn_delta_get_cancellation_editor(check_cancel, NULL,
1242 sync_editor, sync_baton,
1243 &cancel_editor,
1244 &cancel_baton,
1245 pool));
1246 *editor = cancel_editor;
1247 *edit_baton = cancel_baton;
1249 return SVN_NO_ERROR;
1252 /* Callback function for svn_ra_replay_range, invoked when finishing parsing
1253 * a replay report.
1255 static svn_error_t *
1256 replay_rev_finished(svn_revnum_t revision,
1257 void *replay_baton,
1258 const svn_delta_editor_t *editor,
1259 void *edit_baton,
1260 apr_hash_t *rev_props,
1261 apr_pool_t *pool)
1263 apr_pool_t *subpool = svn_pool_create(pool);
1264 replay_baton_t *rb = replay_baton;
1265 apr_hash_t *filtered, *existing_props;
1266 int filtered_count;
1268 SVN_ERR(editor->close_edit(edit_baton, pool));
1270 /* Sanity check that we actually committed the revision we meant to. */
1271 if (rb->sb->committed_rev != revision)
1272 return svn_error_createf
1273 (APR_EINVAL, NULL,
1274 _("Commit created rev %ld but should have created %ld"),
1275 rb->sb->committed_rev, revision);
1277 SVN_ERR(svn_ra_rev_proplist(rb->to_session, revision, &existing_props,
1278 subpool));
1280 /* Ok, we're done with the data, now we just need to copy the remaining
1281 'svn:date' and 'svn:author' revprops and we're all set. */
1282 filtered = filter_props(&filtered_count, rev_props,
1283 filter_include_date_author_log_sync,
1284 pool);
1285 SVN_ERR(write_revprops(&filtered_count, rb->to_session, revision, filtered,
1286 pool));
1288 svn_pool_clear(subpool);
1290 /* Remove all extra properties in TARGET. */
1292 SVN_ERR(remove_props_not_in_source(rb->to_session, revision,
1293 rev_props, existing_props, pool));
1295 /* Ok, we're done, bring the last-merged-rev property up to date. */
1297 SVN_ERR(svn_ra_change_rev_prop
1298 (rb->to_session,
1300 SVNSYNC_PROP_LAST_MERGED_REV,
1301 svn_string_create(apr_psprintf(pool, "%ld", revision),
1302 subpool),
1303 subpool));
1305 /* And finally drop the currently copying prop, since we're done
1306 with this revision. */
1308 SVN_ERR(svn_ra_change_rev_prop(rb->to_session, 0,
1309 SVNSYNC_PROP_CURRENTLY_COPYING,
1310 NULL, subpool));
1312 /* Notify the user that we copied revision properties. */
1314 if (! rb->sb->quiet)
1315 SVN_ERR(log_properties_copied(filtered_count > 0, revision, subpool));
1317 svn_pool_destroy(subpool);
1319 return SVN_NO_ERROR;
1322 /* Synchronize the repository associated with RA session TO_SESSION,
1323 * using information found in baton B, while the repository is
1324 * locked. Implements `with_locked_func_t' interface.
1326 static svn_error_t *
1327 do_synchronize(svn_ra_session_t *to_session, void *b, apr_pool_t *pool)
1329 svn_string_t *last_merged_rev;
1330 svn_revnum_t from_latest;
1331 svn_ra_session_t *from_session;
1332 subcommand_baton_t *baton = b;
1333 svn_string_t *currently_copying;
1334 svn_revnum_t to_latest, copying, last_merged;
1335 svn_revnum_t start_revision, end_revision;
1336 replay_baton_t *rb;
1338 SVN_ERR(open_source_session(&from_session,
1339 &last_merged_rev, to_session,
1340 &(baton->source_callbacks), baton->config,
1341 baton, pool));
1343 /* Check to see if we have revprops that still need to be copied for
1344 a prior revision we didn't finish copying. But first, check for
1345 state sanity. Remember, mirroring is not an atomic action,
1346 because revision properties are copied separately from the
1347 revision's contents.
1349 So, any time that currently-copying is not set, then
1350 last-merged-rev should be the HEAD revision of the destination
1351 repository. That is, if we didn't fall over in the middle of a
1352 previous synchronization, then our destination repository should
1353 have exactly as many revisions in it as we've synchronized.
1355 Alternately, if currently-copying *is* set, it must
1356 be either last-merged-rev or last-merged-rev + 1, and the HEAD
1357 revision must be equal to either last-merged-rev or
1358 currently-copying. If this is not the case, somebody has meddled
1359 with the destination without using svnsync.
1362 SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_CURRENTLY_COPYING,
1363 &currently_copying, pool));
1365 SVN_ERR(svn_ra_get_latest_revnum(to_session, &to_latest, pool));
1367 last_merged = SVN_STR_TO_REV(last_merged_rev->data);
1369 if (currently_copying)
1371 copying = SVN_STR_TO_REV(currently_copying->data);
1373 if ((copying < last_merged)
1374 || (copying > (last_merged + 1))
1375 || ((to_latest != last_merged) && (to_latest != copying)))
1377 return svn_error_createf
1378 (APR_EINVAL, NULL,
1379 _("Revision being currently copied (%ld), last merged revision "
1380 "(%ld), and destination HEAD (%ld) are inconsistent; have you "
1381 "committed to the destination without using svnsync?"),
1382 copying, last_merged, to_latest);
1384 else if (copying == to_latest)
1386 if (copying > last_merged)
1388 SVN_ERR(copy_revprops(from_session, to_session,
1389 to_latest, TRUE, baton->quiet,
1390 pool));
1391 last_merged = copying;
1392 last_merged_rev = svn_string_create
1393 (apr_psprintf(pool, "%ld", last_merged), pool);
1396 /* Now update last merged rev and drop currently changing.
1397 Note that the order here is significant, if we do them
1398 in the wrong order there are race conditions where we
1399 end up not being able to tell if there have been bogus
1400 (i.e. non-svnsync) commits to the dest repository. */
1402 SVN_ERR(svn_ra_change_rev_prop(to_session, 0,
1403 SVNSYNC_PROP_LAST_MERGED_REV,
1404 last_merged_rev, pool));
1405 SVN_ERR(svn_ra_change_rev_prop(to_session, 0,
1406 SVNSYNC_PROP_CURRENTLY_COPYING,
1407 NULL, pool));
1409 /* If copying > to_latest, then we just fall through to
1410 attempting to copy the revision again. */
1412 else
1414 if (to_latest != last_merged)
1416 return svn_error_createf
1417 (APR_EINVAL, NULL,
1418 _("Destination HEAD (%ld) is not the last merged revision (%ld); "
1419 "have you committed to the destination without using svnsync?"),
1420 to_latest, last_merged);
1424 /* Now check to see if there are any revisions to copy. */
1426 SVN_ERR(svn_ra_get_latest_revnum(from_session, &from_latest, pool));
1428 if (from_latest < atol(last_merged_rev->data))
1429 return SVN_NO_ERROR;
1431 /* Ok, so there are new revisions, iterate over them copying them
1432 into the destination repository. */
1434 rb = make_replay_baton(from_session, to_session,
1435 baton, pool);
1437 start_revision = atol(last_merged_rev->data) + 1;
1438 end_revision = from_latest;
1440 SVN_ERR(check_cancel(NULL));
1442 SVN_ERR(svn_ra_replay_range(from_session, start_revision, end_revision,
1443 0, TRUE,
1444 replay_rev_started, replay_rev_finished,
1445 rb,
1446 pool));
1448 return SVN_NO_ERROR;
1452 /* SUBCOMMAND: sync */
1453 static svn_error_t *
1454 synchronize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
1456 svn_ra_session_t *to_session;
1457 opt_baton_t *opt_baton = b;
1458 apr_array_header_t *targets;
1459 subcommand_baton_t *baton;
1460 const char *to_url;
1462 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
1463 apr_array_make(pool, 0,
1464 sizeof(const char *)),
1465 pool));
1466 if (targets->nelts < 1)
1467 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1468 if (targets->nelts > 1)
1469 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1471 to_url = APR_ARRAY_IDX(targets, 0, const char *);
1473 if (! svn_path_is_url(to_url))
1474 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1475 _("Path '%s' is not a URL"), to_url);
1477 baton = make_subcommand_baton(opt_baton, to_url, NULL, 0, 0, pool);
1478 SVN_ERR(svn_ra_open2(&to_session, baton->to_url, &(baton->sync_callbacks),
1479 baton, baton->config, pool));
1480 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
1481 SVN_ERR(with_locked(to_session, do_synchronize, baton, pool));
1483 return SVN_NO_ERROR;
1488 /*** `svnsync copy-revprops' ***/
1490 /* Copy revision properties to the repository associated with RA
1491 * session TO_SESSION, using information found in baton B, while the
1492 * repository is locked. Implements `with_locked_func_t' interface.
1494 static svn_error_t *
1495 do_copy_revprops(svn_ra_session_t *to_session, void *b, apr_pool_t *pool)
1497 subcommand_baton_t *baton = b;
1498 svn_ra_session_t *from_session;
1499 svn_string_t *last_merged_rev;
1500 svn_revnum_t i;
1501 svn_revnum_t step = 1;
1503 SVN_ERR(open_source_session(&from_session, &last_merged_rev,
1504 to_session,
1505 &(baton->source_callbacks), baton->config,
1506 baton, pool));
1508 /* An invalid revision means "last-synced" */
1509 if (! SVN_IS_VALID_REVNUM(baton->start_rev))
1510 baton->start_rev = SVN_STR_TO_REV(last_merged_rev->data);
1511 if (! SVN_IS_VALID_REVNUM(baton->end_rev))
1512 baton->end_rev = SVN_STR_TO_REV(last_merged_rev->data);
1514 /* Make sure we have revisions within the valid range. */
1515 if (baton->start_rev > SVN_STR_TO_REV(last_merged_rev->data))
1516 return svn_error_createf
1517 (APR_EINVAL, NULL,
1518 _("Cannot copy revprops for a revision (%ld) that has not "
1519 "been synchronized yet"), baton->start_rev);
1520 if (baton->end_rev > SVN_STR_TO_REV(last_merged_rev->data))
1521 return svn_error_createf
1522 (APR_EINVAL, NULL,
1523 _("Cannot copy revprops for a revision (%ld) that has not "
1524 "been synchronized yet"), baton->end_rev);
1526 /* Now, copy all the requested revisions, in the requested order. */
1527 step = (baton->start_rev > baton->end_rev) ? -1 : 1;
1528 for (i = baton->start_rev; i != baton->end_rev + step; i = i + step)
1530 SVN_ERR(check_cancel(NULL));
1531 SVN_ERR(copy_revprops(from_session, to_session, i, FALSE,
1532 baton->quiet, pool));
1535 return SVN_NO_ERROR;
1539 /* SUBCOMMAND: copy-revprops */
1540 static svn_error_t *
1541 copy_revprops_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
1543 svn_ra_session_t *to_session;
1544 opt_baton_t *opt_baton = b;
1545 apr_array_header_t *targets;
1546 subcommand_baton_t *baton;
1547 const char *to_url;
1548 svn_opt_revision_t start_revision, end_revision;
1549 svn_revnum_t start_rev = 0, end_rev = SVN_INVALID_REVNUM;
1551 /* There should be either one or two arguments left to parse. */
1552 if (os->argc - os->ind > 2)
1553 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1554 if (os->argc - os->ind < 1)
1555 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1557 /* If there are two args, the last one is a revision range. We'll
1558 effectively pop it from the end of the list. Why? Because
1559 svn_opt_args_to_target_array2() does waaaaay too many useful
1560 things for us not to use it. */
1561 if (os->argc - os->ind == 2)
1563 const char *rev_str = os->argv[--(os->argc)];
1565 start_revision.kind = svn_opt_revision_unspecified;
1566 end_revision.kind = svn_opt_revision_unspecified;
1567 if ((svn_opt_parse_revision(&start_revision, &end_revision,
1568 rev_str, pool) != 0)
1569 || ((start_revision.kind != svn_opt_revision_number)
1570 && (start_revision.kind != svn_opt_revision_head))
1571 || ((end_revision.kind != svn_opt_revision_number)
1572 && (end_revision.kind != svn_opt_revision_head)
1573 && (end_revision.kind != svn_opt_revision_unspecified)))
1574 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1575 _("'%s' is not a valid revision range"),
1576 rev_str);
1578 /* Get the start revision, which must be either HEAD or a number
1579 (which is required to be a valid one). */
1580 if (start_revision.kind == svn_opt_revision_head)
1582 start_rev = SVN_INVALID_REVNUM;
1584 else
1586 start_rev = start_revision.value.number;
1587 if (! SVN_IS_VALID_REVNUM(start_rev))
1588 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1589 _("Invalid revision number (%ld)"),
1590 start_rev);
1593 /* Get the end revision, which must be unspecified (meaning,
1594 "same as the start_rev"), HEAD, or a number (which is
1595 required to be a valid one). */
1596 if (end_revision.kind == svn_opt_revision_unspecified)
1598 end_rev = start_rev;
1600 else if (end_revision.kind == svn_opt_revision_head)
1602 end_rev = SVN_INVALID_REVNUM;
1604 else
1606 end_rev = end_revision.value.number;
1607 if (! SVN_IS_VALID_REVNUM(end_rev))
1608 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1609 _("Invalid revision number (%ld)"),
1610 end_rev);
1614 SVN_ERR(svn_opt_args_to_target_array2(&targets, os,
1615 apr_array_make(pool, 1,
1616 sizeof(const char *)),
1617 pool));
1618 if (targets->nelts != 1)
1619 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1621 to_url = APR_ARRAY_IDX(targets, 0, const char *);
1623 if (! svn_path_is_url(to_url))
1624 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1625 _("Path '%s' is not a URL"), to_url);
1627 baton = make_subcommand_baton(opt_baton, to_url, NULL,
1628 start_rev, end_rev, pool);
1629 SVN_ERR(svn_ra_open2(&to_session, baton->to_url, &(baton->sync_callbacks),
1630 baton, baton->config, pool));
1631 SVN_ERR(check_if_session_is_at_repos_root(to_session, baton->to_url, pool));
1632 SVN_ERR(with_locked(to_session, do_copy_revprops, &baton, pool));
1634 return SVN_NO_ERROR;
1639 /*** `svnsync help' ***/
1642 /* SUBCOMMAND: help */
1643 static svn_error_t *
1644 help_cmd(apr_getopt_t *os, void *baton, apr_pool_t *pool)
1646 opt_baton_t *opt_baton = baton;
1648 const char *header =
1649 _("general usage: svnsync SUBCOMMAND DEST_URL [ARGS & OPTIONS ...]\n"
1650 "Type 'svnsync help <subcommand>' for help on a specific subcommand.\n"
1651 "Type 'svnsync --version' to see the program version and RA modules.\n"
1652 "\n"
1653 "Available subcommands:\n");
1655 const char *ra_desc_start
1656 = _("The following repository access (RA) modules are available:\n\n");
1658 svn_stringbuf_t *version_footer = svn_stringbuf_create(ra_desc_start,
1659 pool);
1661 SVN_ERR(svn_ra_print_modules(version_footer, pool));
1663 SVN_ERR(svn_opt_print_help(os, "svnsync",
1664 opt_baton ? opt_baton->version : FALSE,
1665 FALSE, version_footer->data, header,
1666 svnsync_cmd_table, svnsync_options, NULL,
1667 pool));
1669 return SVN_NO_ERROR;
1674 /*** Main ***/
1677 main(int argc, const char *argv[])
1679 const svn_opt_subcommand_desc_t *subcommand = NULL;
1680 apr_array_header_t *received_opts;
1681 opt_baton_t opt_baton;
1682 svn_config_t *config;
1683 apr_status_t apr_err;
1684 apr_getopt_t *os;
1685 apr_pool_t *pool;
1686 svn_error_t *err;
1687 int opt_id, i;
1688 const char *username = NULL, *source_username = NULL, *sync_username = NULL;
1689 const char *password = NULL, *source_password = NULL, *sync_password = NULL;
1691 if (svn_cmdline_init("svnsync", stderr) != EXIT_SUCCESS)
1693 return EXIT_FAILURE;
1696 err = check_lib_versions();
1697 if (err)
1698 return svn_cmdline_handle_exit_error(err, NULL, "svnsync: ");
1700 pool = svn_pool_create(NULL);
1702 err = svn_ra_initialize(pool);
1703 if (err)
1704 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1706 memset(&opt_baton, 0, sizeof(opt_baton));
1708 received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int));
1710 if (argc <= 1)
1712 help_cmd(NULL, NULL, pool);
1713 svn_pool_destroy(pool);
1714 return EXIT_FAILURE;
1717 err = svn_cmdline__getopt_init(&os, argc, argv, pool);
1718 if (err)
1719 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1721 os->interleave = 1;
1723 for (;;)
1725 const char *opt_arg;
1727 apr_err = apr_getopt_long(os, svnsync_options, &opt_id, &opt_arg);
1728 if (APR_STATUS_IS_EOF(apr_err))
1729 break;
1730 else if (apr_err)
1732 help_cmd(NULL, NULL, pool);
1733 svn_pool_destroy(pool);
1734 return EXIT_FAILURE;
1737 APR_ARRAY_PUSH(received_opts, int) = opt_id;
1739 switch (opt_id)
1741 case svnsync_opt_non_interactive:
1742 opt_baton.non_interactive = TRUE;
1743 break;
1745 case svnsync_opt_no_auth_cache:
1746 opt_baton.no_auth_cache = TRUE;
1747 break;
1749 case svnsync_opt_auth_username:
1750 username = opt_arg;
1751 break;
1753 case svnsync_opt_auth_password:
1754 password = opt_arg;
1755 break;
1757 case svnsync_opt_source_username:
1758 source_username = opt_arg;
1759 break;
1761 case svnsync_opt_source_password:
1762 source_password = opt_arg;
1763 break;
1765 case svnsync_opt_sync_username:
1766 sync_username = opt_arg;
1767 break;
1769 case svnsync_opt_sync_password:
1770 sync_password = opt_arg;
1771 break;
1773 case svnsync_opt_config_dir:
1774 opt_baton.config_dir = opt_arg;
1775 break;
1777 case svnsync_opt_version:
1778 opt_baton.version = TRUE;
1779 break;
1781 case 'q':
1782 opt_baton.quiet = TRUE;
1783 break;
1785 case '?':
1786 case 'h':
1787 opt_baton.help = TRUE;
1788 break;
1790 default:
1792 help_cmd(NULL, NULL, pool);
1793 svn_pool_destroy(pool);
1794 return EXIT_FAILURE;
1799 if (opt_baton.help)
1800 subcommand = svn_opt_get_canonical_subcommand(svnsync_cmd_table, "help");
1802 /* Disallow the mixing --username/password with their --source- and
1803 --sync- variants. Treat "--username FOO" as "--source-username
1804 FOO --sync-username FOO"; ditto for "--password FOO". */
1805 if ((username || password)
1806 && (source_username || sync_username
1807 || source_password || sync_password))
1809 err = svn_error_create
1810 (SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1811 _("Cannot use --username or --password with any of "
1812 "--source-username, --source-password, --sync-username, "
1813 "or --sync-password.\n"));
1814 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1816 if (username)
1818 source_username = username;
1819 sync_username = username;
1821 if (password)
1823 source_password = password;
1824 sync_password = password;
1826 opt_baton.source_username = source_username;
1827 opt_baton.source_password = source_password;
1828 opt_baton.sync_username = sync_username;
1829 opt_baton.sync_password = sync_password;
1831 err = svn_config_ensure(opt_baton.config_dir, pool);
1832 if (err)
1833 return svn_cmdline_handle_exit_error(err, pool, "synsync: ");
1835 if (subcommand == NULL)
1837 if (os->ind >= os->argc)
1839 if (opt_baton.version)
1841 /* Use the "help" subcommand to handle "--version". */
1842 static const svn_opt_subcommand_desc_t pseudo_cmd =
1843 { "--version", help_cmd, {0}, "",
1844 {svnsync_opt_version, /* must accept its own option */
1845 } };
1847 subcommand = &pseudo_cmd;
1849 else
1851 help_cmd(NULL, NULL, pool);
1852 svn_pool_destroy(pool);
1853 return EXIT_FAILURE;
1856 else
1858 const char *first_arg = os->argv[os->ind++];
1859 subcommand = svn_opt_get_canonical_subcommand(svnsync_cmd_table,
1860 first_arg);
1861 if (subcommand == NULL)
1863 help_cmd(NULL, NULL, pool);
1864 svn_pool_destroy(pool);
1865 return EXIT_FAILURE;
1870 for (i = 0; i < received_opts->nelts; ++i)
1872 opt_id = APR_ARRAY_IDX(received_opts, i, int);
1874 if (opt_id == 'h' || opt_id == '?')
1875 continue;
1877 if (! svn_opt_subcommand_takes_option(subcommand, opt_id))
1879 const char *optstr;
1880 const apr_getopt_option_t *badopt =
1881 svn_opt_get_option_from_code(opt_id, svnsync_options);
1882 svn_opt_format_option(&optstr, badopt, FALSE, pool);
1883 if (subcommand->name[0] == '-')
1885 help_cmd(NULL, NULL, pool);
1887 else
1889 err = svn_error_createf
1890 (SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1891 _("Subcommand '%s' doesn't accept option '%s'\n"
1892 "Type 'svnsync help %s' for usage.\n"),
1893 subcommand->name, optstr, subcommand->name);
1894 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1899 err = svn_config_get_config(&opt_baton.config, opt_baton.config_dir, pool);
1900 if (err)
1901 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1903 config = apr_hash_get(opt_baton.config, SVN_CONFIG_CATEGORY_CONFIG,
1904 APR_HASH_KEY_STRING);
1906 apr_signal(SIGINT, signal_handler);
1908 #ifdef SIGBREAK
1909 /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */
1910 apr_signal(SIGBREAK, signal_handler);
1911 #endif
1913 #ifdef SIGHUP
1914 apr_signal(SIGHUP, signal_handler);
1915 #endif
1917 #ifdef SIGTERM
1918 apr_signal(SIGTERM, signal_handler);
1919 #endif
1921 #ifdef SIGPIPE
1922 /* Disable SIGPIPE generation for the platforms that have it. */
1923 apr_signal(SIGPIPE, SIG_IGN);
1924 #endif
1926 #ifdef SIGXFSZ
1927 /* Disable SIGXFSZ generation for the platforms that have it,
1928 otherwise working with large files when compiled against an APR
1929 that doesn't have large file support will crash the program,
1930 which is uncool. */
1931 apr_signal(SIGXFSZ, SIG_IGN);
1932 #endif
1934 err = svn_cmdline_setup_auth_baton(&opt_baton.source_auth_baton,
1935 opt_baton.non_interactive,
1936 opt_baton.source_username,
1937 opt_baton.source_password,
1938 opt_baton.config_dir,
1939 opt_baton.no_auth_cache,
1940 config,
1941 check_cancel, NULL,
1942 pool);
1943 if (! err)
1944 err = svn_cmdline_setup_auth_baton(&opt_baton.sync_auth_baton,
1945 opt_baton.non_interactive,
1946 opt_baton.sync_username,
1947 opt_baton.sync_password,
1948 opt_baton.config_dir,
1949 opt_baton.no_auth_cache,
1950 config,
1951 check_cancel, NULL,
1952 pool);
1953 if (! err)
1954 err = (*subcommand->cmd_func)(os, &opt_baton, pool);
1955 if (err)
1957 /* For argument-related problems, suggest using the 'help'
1958 subcommand. */
1959 if (err->apr_err == SVN_ERR_CL_INSUFFICIENT_ARGS
1960 || err->apr_err == SVN_ERR_CL_ARG_PARSING_ERROR)
1962 err = svn_error_quick_wrap(err,
1963 _("Try 'svnsync help' for more info"));
1966 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1969 svn_pool_destroy(pool);
1971 return EXIT_SUCCESS;