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