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"
22 #include "svn_props.h"
27 #include "svn_private_config.h"
29 #include <apr_network_io.h>
30 #include <apr_signal.h>
33 static svn_opt_subcommand_t initialize_cmd
,
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
,
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, \
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"
67 "Initialize a destination repository for synchronization from\n"
68 "another repository.\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"
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"
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"
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"
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"
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"
104 "Describe the usage of this program or its subcommands.\n"),
106 { NULL
, NULL
, { 0 }, NULL
, { 0 } }
109 static const apr_getopt_option_t svnsync_options
[] =
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")},
138 N_("show help on a subcommand")},
140 N_("show help on a subcommand")},
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
;
156 svn_boolean_t version
;
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(). */
172 signal_handler(int signum
)
174 apr_signal(signum
, SIG_IGN
);
179 /* Cancellation callback function. */
181 check_cancel(void *baton
)
184 return svn_error_create(SVN_ERR_CANCELLED
, NULL
, _("Caught signal"));
190 /* Check that the version of libraries in use match what we expect. */
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
},
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
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
;
220 apr_err
= apr_gethostname(hostname_str
, sizeof(hostname_str
), pool
);
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
,
239 /* Did we get it? If so, we're done, otherwise we sleep. */
240 if (strcmp(reposlocktoken
->data
, mylocktoken
->data
) == 0)
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));
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. */
267 /* common to all subcommands */
269 svn_ra_callbacks2_t source_callbacks
;
270 svn_ra_callbacks2_t sync_callbacks
;
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
,
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.
296 with_locked(svn_ra_session_t
*session
,
297 with_locked_func_t func
,
298 subcommand_baton_t
*baton
,
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
);
310 svn_error_clear(err2
); /* XXX what to do here? */
325 /* Callback function for the RA session's open_tmp_file()
329 open_tmp_file(apr_file_t
**fp
, void *callback_baton
, apr_pool_t
*pool
)
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
));
344 /* Return SVN_NO_ERROR iff URL identifies the root directory of the
345 * repository associated with RA session SESS.
348 check_if_session_is_at_repos_root(svn_ra_session_t
*sess
,
352 const char *sess_root
;
354 SVN_ERR(svn_ra_get_repos_root2(sess
, &sess_root
, pool
));
356 if (strcmp(url
, sess_root
) == 0)
359 return svn_error_createf
361 _("Session is rooted at '%s' but the repos root is '%s'"),
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.
372 remove_props_not_in_source(svn_ra_session_t
*session
,
374 apr_hash_t
*source_props
,
375 apr_hash_t
*target_props
,
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
))
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
,
397 svn_pool_destroy(subpool
);
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
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.
417 filter_props(int *filtered_count
, apr_hash_t
*props
,
418 filter_func_t filter
,
421 apr_hash_index_t
*hi
;
422 apr_hash_t
*filtered
= apr_hash_make(pool
);
425 for (hi
= apr_hash_first(pool
, props
); hi
; hi
= apr_hash_next(hi
))
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
);
442 *filtered_count
+= 1;
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.
456 write_revprops(int *filtered_count
,
457 svn_ra_session_t
*session
,
459 apr_hash_t
*rev_props
,
462 apr_pool_t
*subpool
= svn_pool_create(pool
);
463 apr_hash_index_t
*hi
;
467 for (hi
= apr_hash_first(pool
, rev_props
); hi
; hi
= apr_hash_next(hi
))
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
));
482 *filtered_count
+= 1;
486 svn_pool_destroy(subpool
);
493 log_properties_copied(svn_boolean_t 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
));
503 SVN_ERR(svn_cmdline_printf(pool
,
504 _("Copied properties for revision %ld.\n"),
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.
519 copy_revprops(svn_ra_session_t
*from_session
,
520 svn_ra_session_t
*to_session
,
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'. */
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 */
543 SVN_ERR(remove_props_not_in_source(to_session
, rev
,
544 rev_props
, existing_props
, pool
));
547 SVN_ERR(log_properties_copied(filtered_count
> 0, rev
, pool
));
549 svn_pool_destroy(subpool
);
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
,
563 const char *from_url
,
564 svn_revnum_t start_rev
,
565 svn_revnum_t end_rev
,
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
;
576 b
->from_url
= from_url
;
577 b
->start_rev
= start_rev
;
578 b
->end_rev
= end_rev
;
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.
590 do_initialize(svn_ra_session_t
*to_session
,
591 subcommand_baton_t
*baton
,
594 svn_ra_session_t
*from_session
;
595 svn_string_t
*from_url
;
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
));
604 return svn_error_create
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
,
615 return svn_error_createf
617 _("Destination repository is already synchronizing from '%s'"),
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
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
,
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
,
647 SVN_ERR(svn_ra_change_rev_prop(to_session
, 0, SVNSYNC_PROP_FROM_URL
,
648 svn_string_create(baton
->from_url
, 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. */
676 /* SUBCOMMAND: init */
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 *)),
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
));
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
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.
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
;
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? */
749 /* A dual-purpose baton for files and directories. */
752 void *wrapped_node_baton
;
756 /*** Editor vtable functions ***/
759 set_target_revision(void *edit_baton
,
760 svn_revnum_t target_revision
,
763 edit_baton_t
*eb
= edit_baton
;
764 return eb
->wrapped_editor
->set_target_revision(eb
->wrapped_edit_baton
,
765 target_revision
, pool
);
769 open_root(void *edit_baton
,
770 svn_revnum_t base_revision
,
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
,
779 &dir_baton
->wrapped_node_baton
));
781 eb
->called_open_root
= TRUE
;
782 dir_baton
->edit_baton
= edit_baton
;
783 *root_baton
= dir_baton
;
789 delete_entry(const char *path
,
790 svn_revnum_t base_revision
,
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
);
802 add_directory(const char *path
,
804 const char *copyfrom_path
,
805 svn_revnum_t copyfrom_rev
,
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
));
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
,
820 &b
->wrapped_node_baton
));
829 open_directory(const char *path
,
831 svn_revnum_t base_revision
,
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
,
841 &db
->wrapped_node_baton
));
850 add_file(const char *path
,
852 const char *copyfrom_path
,
853 svn_revnum_t copyfrom_rev
,
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
));
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
));
876 open_file(const char *path
,
878 svn_revnum_t base_revision
,
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
,
888 &fb
->wrapped_node_baton
));
897 apply_textdelta(void *file_baton
,
898 const char *base_checksum
,
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
;
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
,
917 handler
, handler_baton
);
921 close_file(void *file_baton
,
922 const char *text_checksum
,
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
);
932 absent_file(const char *path
,
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
);
942 close_directory(void *dir_baton
,
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
);
951 absent_directory(const char *path
,
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
,
962 change_file_prop(void *file_baton
,
964 const svn_string_t
*value
,
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
)
974 /* Maybe drop svn:mergeinfo. */
975 if (eb
->strip_mergeinfo
&& (strcmp(name
, SVN_PROP_MERGEINFO
) == 0))
977 eb
->mergeinfo_stripped
= TRUE
;
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
;
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
,
1000 change_dir_prop(void *dir_baton
,
1002 const svn_string_t
*value
,
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))
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). */
1034 svn_stringbuf_t
*mergeinfo_buf
= svn_stringbuf_create("", pool
);
1035 svn_mergeinfo_t mergeinfo
;
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 *),
1047 /* ### TODO: Warn? */
1048 if (path_revs
->nelts
!= 2)
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,
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
);
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
,
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
)
1102 SVN_ERR(eb
->wrapped_editor
->open_root(eb
->wrapped_edit_baton
,
1103 eb
->base_revision
, pool
,
1105 SVN_ERR(eb
->wrapped_editor
->close_directory(baton
, pool
));
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
,
1142 svn_boolean_t quiet
,
1143 const svn_delta_editor_t
**editor
,
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
;
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
;
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
,
1207 subcommand_baton_t
*sb
= baton
;
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
,
1239 svn_string_t
*from_url
, *from_uuid
;
1242 SVN_ERR(svn_ra_rev_prop(to_session
, 0, SVNSYNC_PROP_FROM_URL
,
1244 SVN_ERR(svn_ra_rev_prop(to_session
, 0, SVNSYNC_PROP_FROM_UUID
,
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
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
,
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. */
1274 svn_ra_session_t
*from_session
;
1275 svn_ra_session_t
*to_session
;
1276 subcommand_baton_t
*sb
;
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
;
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)
1300 else if (strncmp(key
, SVN_PROP_REVISION_DATE
,
1301 sizeof(SVN_PROP_REVISION_DATE
) - 1) == 0)
1303 else if (strncmp(key
, SVNSYNC_PROP_PREFIX
,
1304 sizeof(SVNSYNC_PROP_PREFIX
) - 1) == 0)
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
1320 static svn_error_t
*
1321 replay_rev_started(svn_revnum_t revision
,
1323 const svn_delta_editor_t
**editor
,
1325 apr_hash_t
*rev_props
,
1328 const svn_delta_editor_t
*commit_editor
;
1329 const svn_delta_editor_t
*cancel_editor
;
1330 const svn_delta_editor_t
*sync_editor
;
1334 replay_baton_t
*rb
= replay_baton
;
1335 apr_hash_t
*filtered
;
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
1347 SVN_ERR(svn_ra_change_rev_prop(rb
->to_session
, 0,
1348 SVNSYNC_PROP_CURRENTLY_COPYING
,
1349 svn_string_createf(pool
, "%ld",
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
,
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
,
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
,
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
1397 static svn_error_t
*
1398 replay_rev_finished(svn_revnum_t revision
,
1400 const svn_delta_editor_t
*editor
,
1402 apr_hash_t
*rev_props
,
1405 apr_pool_t
*subpool
= svn_pool_create(pool
);
1406 replay_baton_t
*rb
= replay_baton
;
1407 apr_hash_t
*filtered
, *existing_props
;
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
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
,
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
,
1428 SVN_ERR(write_revprops(&filtered_count
, rb
->to_session
, revision
, filtered
,
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
1441 SVNSYNC_PROP_LAST_MERGED_REV
,
1442 svn_string_create(apr_psprintf(pool
, "%ld", revision
),
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
,
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
;
1477 SVN_ERR(open_source_session(&from_session
,
1478 &last_merged_rev
, to_session
,
1479 &(baton
->source_callbacks
), baton
->config
,
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 ¤tly_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
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
,
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
,
1548 /* If copying > to_latest, then we just fall through to
1549 attempting to copy the revision again. */
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 "
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
;
1595 SVN_ERR(svn_opt_args_to_target_array2(&targets
, os
,
1596 apr_array_make(pool
, 0,
1597 sizeof(const char *)),
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
;
1634 svn_revnum_t step
= 1;
1636 SVN_ERR(open_source_session(&from_session
, &last_merged_rev
,
1638 &(baton
->source_callbacks
), baton
->config
,
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
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
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
;
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"),
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
;
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)"),
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
;
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)"),
1747 SVN_ERR(svn_opt_args_to_target_array2(&targets
, os
,
1748 apr_array_make(pool
, 1,
1749 sizeof(const char *)),
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"
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
,
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
,
1802 return SVN_NO_ERROR
;
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
;
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();
1831 return svn_cmdline_handle_exit_error(err
, NULL
, "svnsync: ");
1833 pool
= svn_pool_create(NULL
);
1835 err
= svn_ra_initialize(pool
);
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));
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
);
1852 return svn_cmdline_handle_exit_error(err
, pool
, "svnsync: ");
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
))
1865 help_cmd(NULL
, NULL
, pool
);
1866 svn_pool_destroy(pool
);
1867 return EXIT_FAILURE
;
1870 APR_ARRAY_PUSH(received_opts
, int) = opt_id
;
1874 case svnsync_opt_non_interactive
:
1875 opt_baton
.non_interactive
= TRUE
;
1878 case svnsync_opt_no_auth_cache
:
1879 opt_baton
.no_auth_cache
= TRUE
;
1882 case svnsync_opt_auth_username
:
1886 case svnsync_opt_auth_password
:
1890 case svnsync_opt_source_username
:
1891 source_username
= opt_arg
;
1894 case svnsync_opt_source_password
:
1895 source_password
= opt_arg
;
1898 case svnsync_opt_sync_username
:
1899 sync_username
= opt_arg
;
1902 case svnsync_opt_sync_password
:
1903 sync_password
= opt_arg
;
1906 case svnsync_opt_config_dir
:
1907 opt_baton
.config_dir
= opt_arg
;
1910 case svnsync_opt_version
:
1911 opt_baton
.version
= TRUE
;
1915 opt_baton
.quiet
= TRUE
;
1920 opt_baton
.help
= TRUE
;
1925 help_cmd(NULL
, NULL
, pool
);
1926 svn_pool_destroy(pool
);
1927 return EXIT_FAILURE
;
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: ");
1951 source_username
= username
;
1952 sync_username
= username
;
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
);
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 */
1980 subcommand
= &pseudo_cmd
;
1984 help_cmd(NULL
, NULL
, pool
);
1985 svn_pool_destroy(pool
);
1986 return EXIT_FAILURE
;
1991 const char *first_arg
= os
->argv
[os
->ind
++];
1992 subcommand
= svn_opt_get_canonical_subcommand(svnsync_cmd_table
,
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
== '?')
2010 if (! svn_opt_subcommand_takes_option(subcommand
, opt_id
))
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
);
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
);
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
);
2042 /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */
2043 apr_signal(SIGBREAK
, signal_handler
);
2047 apr_signal(SIGHUP
, signal_handler
);
2051 apr_signal(SIGTERM
, signal_handler
);
2055 /* Disable SIGPIPE generation for the platforms that have it. */
2056 apr_signal(SIGPIPE
, SIG_IGN
);
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,
2064 apr_signal(SIGXFSZ
, SIG_IGN
);
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
,
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
,
2087 err
= (*subcommand
->cmd_func
)(os
, &opt_baton
, pool
);
2090 /* For argument-related problems, suggest using the 'help'
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
;