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