]> CyberLeo.Net >> Repos - FreeBSD/releng/10.0.git/blob - contrib/subversion/subversion/svnsync/svnsync.c
- Copy stable/10 (r259064) to releng/10.0 as part of the
[FreeBSD/releng/10.0.git] / contrib / subversion / subversion / svnsync / svnsync.c
1 /*
2  * ====================================================================
3  *    Licensed to the Apache Software Foundation (ASF) under one
4  *    or more contributor license agreements.  See the NOTICE file
5  *    distributed with this work for additional information
6  *    regarding copyright ownership.  The ASF licenses this file
7  *    to you under the Apache License, Version 2.0 (the
8  *    "License"); you may not use this file except in compliance
9  *    with the License.  You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  *    Unless required by applicable law or agreed to in writing,
14  *    software distributed under the License is distributed on an
15  *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  *    KIND, either express or implied.  See the License for the
17  *    specific language governing permissions and limitations
18  *    under the License.
19  * ====================================================================
20  */
21
22 #include "svn_hash.h"
23 #include "svn_cmdline.h"
24 #include "svn_config.h"
25 #include "svn_pools.h"
26 #include "svn_delta.h"
27 #include "svn_dirent_uri.h"
28 #include "svn_path.h"
29 #include "svn_props.h"
30 #include "svn_auth.h"
31 #include "svn_opt.h"
32 #include "svn_ra.h"
33 #include "svn_utf.h"
34 #include "svn_subst.h"
35 #include "svn_string.h"
36 #include "svn_version.h"
37
38 #include "private/svn_opt_private.h"
39 #include "private/svn_ra_private.h"
40 #include "private/svn_cmdline_private.h"
41
42 #include "sync.h"
43
44 #include "svn_private_config.h"
45
46 #include <apr_signal.h>
47 #include <apr_uuid.h>
48
49 static svn_opt_subcommand_t initialize_cmd,
50                             synchronize_cmd,
51                             copy_revprops_cmd,
52                             info_cmd,
53                             help_cmd;
54
55 enum svnsync__opt {
56   svnsync_opt_non_interactive = SVN_OPT_FIRST_LONGOPT_ID,
57   svnsync_opt_force_interactive,
58   svnsync_opt_no_auth_cache,
59   svnsync_opt_auth_username,
60   svnsync_opt_auth_password,
61   svnsync_opt_source_username,
62   svnsync_opt_source_password,
63   svnsync_opt_sync_username,
64   svnsync_opt_sync_password,
65   svnsync_opt_config_dir,
66   svnsync_opt_config_options,
67   svnsync_opt_source_prop_encoding,
68   svnsync_opt_disable_locking,
69   svnsync_opt_version,
70   svnsync_opt_trust_server_cert,
71   svnsync_opt_allow_non_empty,
72   svnsync_opt_steal_lock
73 };
74
75 #define SVNSYNC_OPTS_DEFAULT svnsync_opt_non_interactive, \
76                              svnsync_opt_force_interactive, \
77                              svnsync_opt_no_auth_cache, \
78                              svnsync_opt_auth_username, \
79                              svnsync_opt_auth_password, \
80                              svnsync_opt_trust_server_cert, \
81                              svnsync_opt_source_username, \
82                              svnsync_opt_source_password, \
83                              svnsync_opt_sync_username, \
84                              svnsync_opt_sync_password, \
85                              svnsync_opt_config_dir, \
86                              svnsync_opt_config_options
87
88 static const svn_opt_subcommand_desc2_t svnsync_cmd_table[] =
89   {
90     { "initialize", initialize_cmd, { "init" },
91       N_("usage: svnsync initialize DEST_URL SOURCE_URL\n"
92          "\n"
93          "Initialize a destination repository for synchronization from\n"
94          "another repository.\n"
95          "\n"
96          "If the source URL is not the root of a repository, only the\n"
97          "specified part of the repository will be synchronized.\n"
98          "\n"
99          "The destination URL must point to the root of a repository which\n"
100          "has been configured to allow revision property changes.  In\n"
101          "the general case, the destination repository must contain no\n"
102          "committed revisions.  Use --allow-non-empty to override this\n"
103          "restriction, which will cause svnsync to assume that any revisions\n"
104          "already present in the destination repository perfectly mirror\n"
105          "their counterparts in the source repository.  (This is useful\n"
106          "when initializing a copy of a repository as a mirror of that same\n"
107          "repository, for example.)\n"
108          "\n"
109          "You should not commit to, or make revision property changes in,\n"
110          "the destination repository by any method other than 'svnsync'.\n"
111          "In other words, the destination repository should be a read-only\n"
112          "mirror of the source repository.\n"),
113       { SVNSYNC_OPTS_DEFAULT, svnsync_opt_source_prop_encoding, 'q',
114         svnsync_opt_allow_non_empty, svnsync_opt_disable_locking,
115         svnsync_opt_steal_lock } },
116     { "synchronize", synchronize_cmd, { "sync" },
117       N_("usage: svnsync synchronize DEST_URL [SOURCE_URL]\n"
118          "\n"
119          "Transfer all pending revisions to the destination from the source\n"
120          "with which it was initialized.\n"
121          "\n"
122          "If SOURCE_URL is provided, use that as the source repository URL,\n"
123          "ignoring what is recorded in the destination repository as the\n"
124          "source URL.  Specifying SOURCE_URL is recommended in particular\n"
125          "if untrusted users/administrators may have write access to the\n"
126          "DEST_URL repository.\n"),
127       { SVNSYNC_OPTS_DEFAULT, svnsync_opt_source_prop_encoding, 'q',
128         svnsync_opt_disable_locking, svnsync_opt_steal_lock } },
129     { "copy-revprops", copy_revprops_cmd, { 0 },
130       N_("usage:\n"
131          "\n"
132          "    1. svnsync copy-revprops DEST_URL [SOURCE_URL]\n"
133          "    2. svnsync copy-revprops DEST_URL REV[:REV2]\n"
134          "\n"
135          "Copy the revision properties in a given range of revisions to the\n"
136          "destination from the source with which it was initialized.  If the\n"
137          "revision range is not specified, it defaults to all revisions in\n"
138          "the DEST_URL repository.  Note also that the 'HEAD' revision is the\n"
139          "latest in DEST_URL, not necessarily the latest in SOURCE_URL.\n"
140          "\n"
141          "If SOURCE_URL is provided, use that as the source repository URL,\n"
142          "ignoring what is recorded in the destination repository as the\n"
143          "source URL.  Specifying SOURCE_URL is recommended in particular\n"
144          "if untrusted users/administrators may have write access to the\n"
145          "DEST_URL repository.\n"
146          "\n"
147          "Form 2 is deprecated syntax, equivalent to specifying \"-rREV[:REV2]\".\n"),
148       { SVNSYNC_OPTS_DEFAULT, svnsync_opt_source_prop_encoding, 'q', 'r',
149         svnsync_opt_disable_locking, svnsync_opt_steal_lock } },
150     { "info", info_cmd, { 0 },
151       N_("usage: svnsync info DEST_URL\n"
152          "\n"
153          "Print information about the synchronization destination repository\n"
154          "located at DEST_URL.\n"),
155       { SVNSYNC_OPTS_DEFAULT } },
156     { "help", help_cmd, { "?", "h" },
157       N_("usage: svnsync help [SUBCOMMAND...]\n"
158          "\n"
159          "Describe the usage of this program or its subcommands.\n"),
160       { 0 } },
161     { NULL, NULL, { 0 }, NULL, { 0 } }
162   };
163
164 static const apr_getopt_option_t svnsync_options[] =
165   {
166     {"quiet",          'q', 0,
167                        N_("print as little as possible") },
168     {"revision",       'r', 1,
169                        N_("operate on revision ARG (or range ARG1:ARG2)\n"
170                           "                             "
171                           "A revision argument can be one of:\n"
172                           "                             "
173                           "    NUMBER       revision number\n"
174                           "                             "
175                           "    'HEAD'       latest in repository") },
176     {"allow-non-empty", svnsync_opt_allow_non_empty, 0,
177                        N_("allow a non-empty destination repository") },
178     {"non-interactive", svnsync_opt_non_interactive, 0,
179                        N_("do no interactive prompting (default is to prompt\n"
180                           "                             "
181                           "only if standard input is a terminal device)")},
182     {"force-interactive", svnsync_opt_force_interactive, 0,
183                       N_("do interactive prompting even if standard input\n"
184                          "                             "
185                          "is not a terminal device")},
186     {"no-auth-cache",  svnsync_opt_no_auth_cache, 0,
187                        N_("do not cache authentication tokens") },
188     {"username",       svnsync_opt_auth_username, 1,
189                        N_("specify a username ARG (deprecated;\n"
190                           "                             "
191                           "see --source-username and --sync-username)") },
192     {"password",       svnsync_opt_auth_password, 1,
193                        N_("specify a password ARG (deprecated;\n"
194                           "                             "
195                           "see --source-password and --sync-password)") },
196     {"trust-server-cert", svnsync_opt_trust_server_cert, 0,
197                        N_("accept SSL server certificates from unknown\n"
198                           "                             "
199                           "certificate authorities without prompting (but only\n"
200                           "                             "
201                           "with '--non-interactive')") },
202     {"source-username", svnsync_opt_source_username, 1,
203                        N_("connect to source repository with username ARG") },
204     {"source-password", svnsync_opt_source_password, 1,
205                        N_("connect to source repository with password ARG") },
206     {"sync-username",  svnsync_opt_sync_username, 1,
207                        N_("connect to sync repository with username ARG") },
208     {"sync-password",  svnsync_opt_sync_password, 1,
209                        N_("connect to sync repository with password ARG") },
210     {"config-dir",     svnsync_opt_config_dir, 1,
211                        N_("read user configuration files from directory ARG")},
212     {"config-option",  svnsync_opt_config_options, 1,
213                        N_("set user configuration option in the format:\n"
214                           "                             "
215                           "    FILE:SECTION:OPTION=[VALUE]\n"
216                           "                             "
217                           "For example:\n"
218                           "                             "
219                           "    servers:global:http-library=serf")},
220     {"source-prop-encoding", svnsync_opt_source_prop_encoding, 1,
221                        N_("convert translatable properties from encoding ARG\n"
222                           "                             "
223                           "to UTF-8. If not specified, then properties are\n"
224                           "                             "
225                           "presumed to be encoded in UTF-8.")},
226     {"disable-locking",  svnsync_opt_disable_locking, 0,
227                        N_("Disable built-in locking.  Use of this option can\n"
228                           "                             "
229                           "corrupt the mirror unless you ensure that no other\n"
230                           "                             "
231                           "instance of svnsync is running concurrently.")},
232     {"steal-lock",     svnsync_opt_steal_lock, 0,
233                        N_("Steal locks as necessary.  Use, with caution,\n"
234                           "                             "
235                           "if your mirror repository contains stale locks\n"
236                           "                             "
237                           "and is not being concurrently accessed by another\n"
238                           "                             "
239                           "svnsync instance.")},
240     {"version",        svnsync_opt_version, 0,
241                        N_("show program version information")},
242     {"help",           'h', 0,
243                        N_("show help on a subcommand")},
244     {NULL,             '?', 0,
245                        N_("show help on a subcommand")},
246     { 0, 0, 0, 0 }
247   };
248
249 typedef struct opt_baton_t {
250   svn_boolean_t non_interactive;
251   svn_boolean_t trust_server_cert;
252   svn_boolean_t no_auth_cache;
253   svn_auth_baton_t *source_auth_baton;
254   svn_auth_baton_t *sync_auth_baton;
255   const char *source_username;
256   const char *source_password;
257   const char *sync_username;
258   const char *sync_password;
259   const char *config_dir;
260   apr_hash_t *config;
261   const char *source_prop_encoding;
262   svn_boolean_t disable_locking;
263   svn_boolean_t steal_lock;
264   svn_boolean_t quiet;
265   svn_boolean_t allow_non_empty;
266   svn_boolean_t version;
267   svn_boolean_t help;
268   svn_opt_revision_t start_rev;
269   svn_opt_revision_t end_rev;
270 } opt_baton_t;
271
272
273
274 \f
275 /*** Helper functions ***/
276
277
278 /* Global record of whether the user has requested cancellation. */
279 static volatile sig_atomic_t cancelled = FALSE;
280
281
282 /* Callback function for apr_signal(). */
283 static void
284 signal_handler(int signum)
285 {
286   apr_signal(signum, SIG_IGN);
287   cancelled = TRUE;
288 }
289
290
291 /* Cancellation callback function. */
292 static svn_error_t *
293 check_cancel(void *baton)
294 {
295   if (cancelled)
296     return svn_error_create(SVN_ERR_CANCELLED, NULL, _("Caught signal"));
297   else
298     return SVN_NO_ERROR;
299 }
300
301
302 /* Check that the version of libraries in use match what we expect. */
303 static svn_error_t *
304 check_lib_versions(void)
305 {
306   static const svn_version_checklist_t checklist[] =
307     {
308       { "svn_subr",  svn_subr_version },
309       { "svn_delta", svn_delta_version },
310       { "svn_ra",    svn_ra_version },
311       { NULL, NULL }
312     };
313   SVN_VERSION_DEFINE(my_version);
314
315   return svn_ver_check_list(&my_version, checklist);
316 }
317
318
319 /* Implements `svn_ra__lock_retry_func_t'. */
320 static svn_error_t *
321 lock_retry_func(void *baton,
322                 const svn_string_t *reposlocktoken,
323                 apr_pool_t *pool)
324 {
325   return svn_cmdline_printf(pool,
326                             _("Failed to get lock on destination "
327                               "repos, currently held by '%s'\n"),
328                             reposlocktoken->data);
329 }
330
331 /* Acquire a lock (of sorts) on the repository associated with the
332  * given RA SESSION. This lock is just a revprop change attempt in a
333  * time-delay loop. This function is duplicated by svnrdump in
334  * svnrdump/load_editor.c
335  */
336 static svn_error_t *
337 get_lock(const svn_string_t **lock_string_p,
338          svn_ra_session_t *session,
339          svn_boolean_t steal_lock,
340          apr_pool_t *pool)
341 {
342   svn_error_t *err;
343   svn_boolean_t be_atomic;
344   const svn_string_t *stolen_lock;
345
346   SVN_ERR(svn_ra_has_capability(session, &be_atomic,
347                                 SVN_RA_CAPABILITY_ATOMIC_REVPROPS,
348                                 pool));
349   if (! be_atomic)
350     {
351       /* Pre-1.7 server.  Can't lock without a race condition.
352          See issue #3546.
353        */
354       err = svn_error_create(
355               SVN_ERR_UNSUPPORTED_FEATURE, NULL,
356               _("Target server does not support atomic revision property "
357                 "edits; consider upgrading it to 1.7 or using an external "
358                 "locking program"));
359       svn_handle_warning2(stderr, err, "svnsync: ");
360       svn_error_clear(err);
361     }
362
363   err = svn_ra__get_operational_lock(lock_string_p, &stolen_lock, session,
364                                      SVNSYNC_PROP_LOCK, steal_lock,
365                                      10 /* retries */, lock_retry_func, NULL,
366                                      check_cancel, NULL, pool);
367   if (!err && stolen_lock)
368     {
369       return svn_cmdline_printf(pool,
370                                 _("Stole lock previously held by '%s'\n"),
371                                 stolen_lock->data);
372     }
373   return err;
374 }
375
376
377 /* Baton for the various subcommands to share. */
378 typedef struct subcommand_baton_t {
379   /* common to all subcommands */
380   apr_hash_t *config;
381   svn_ra_callbacks2_t source_callbacks;
382   svn_ra_callbacks2_t sync_callbacks;
383   svn_boolean_t quiet;
384   svn_boolean_t allow_non_empty;
385   const char *to_url;
386
387   /* initialize, synchronize, and copy-revprops only */
388   const char *source_prop_encoding;
389
390   /* initialize only */
391   const char *from_url;
392
393   /* synchronize only */
394   svn_revnum_t committed_rev;
395
396   /* copy-revprops only */
397   svn_revnum_t start_rev;
398   svn_revnum_t end_rev;
399
400 } subcommand_baton_t;
401
402 typedef svn_error_t *(*with_locked_func_t)(svn_ra_session_t *session,
403                                            subcommand_baton_t *baton,
404                                            apr_pool_t *pool);
405
406
407 /* Lock the repository associated with RA SESSION, then execute the
408  * given FUNC/BATON pair while holding the lock.  Finally, drop the
409  * lock once it finishes.
410  */
411 static svn_error_t *
412 with_locked(svn_ra_session_t *session,
413             with_locked_func_t func,
414             subcommand_baton_t *baton,
415             svn_boolean_t steal_lock,
416             apr_pool_t *pool)
417 {
418   const svn_string_t *lock_string;
419   svn_error_t *err;
420
421   SVN_ERR(get_lock(&lock_string, session, steal_lock, pool));
422
423   err = func(session, baton, pool);
424   return svn_error_compose_create(err,
425              svn_ra__release_operational_lock(session, SVNSYNC_PROP_LOCK,
426                                               lock_string, pool));
427 }
428
429
430 /* Callback function for the RA session's open_tmp_file()
431  * requirements.
432  */
433 static svn_error_t *
434 open_tmp_file(apr_file_t **fp, void *callback_baton, apr_pool_t *pool)
435 {
436   return svn_io_open_unique_file3(fp, NULL, NULL,
437                                   svn_io_file_del_on_pool_cleanup,
438                                   pool, pool);
439 }
440
441
442 /* Return SVN_NO_ERROR iff URL identifies the root directory of the
443  * repository associated with RA session SESS.
444  */
445 static svn_error_t *
446 check_if_session_is_at_repos_root(svn_ra_session_t *sess,
447                                   const char *url,
448                                   apr_pool_t *pool)
449 {
450   const char *sess_root;
451
452   SVN_ERR(svn_ra_get_repos_root2(sess, &sess_root, pool));
453
454   if (strcmp(url, sess_root) == 0)
455     return SVN_NO_ERROR;
456   else
457     return svn_error_createf
458       (APR_EINVAL, NULL,
459        _("Session is rooted at '%s' but the repos root is '%s'"),
460        url, sess_root);
461 }
462
463
464 /* Remove the properties in TARGET_PROPS but not in SOURCE_PROPS from
465  * revision REV of the repository associated with RA session SESSION.
466  *
467  * For REV zero, don't remove properties with the "svn:sync-" prefix.
468  *
469  * All allocations will be done in a subpool of POOL.
470  */
471 static svn_error_t *
472 remove_props_not_in_source(svn_ra_session_t *session,
473                            svn_revnum_t rev,
474                            apr_hash_t *source_props,
475                            apr_hash_t *target_props,
476                            apr_pool_t *pool)
477 {
478   apr_pool_t *subpool = svn_pool_create(pool);
479   apr_hash_index_t *hi;
480
481   for (hi = apr_hash_first(pool, target_props);
482        hi;
483        hi = apr_hash_next(hi))
484     {
485       const char *propname = svn__apr_hash_index_key(hi);
486
487       svn_pool_clear(subpool);
488
489       if (rev == 0 && !strncmp(propname, SVNSYNC_PROP_PREFIX,
490                                sizeof(SVNSYNC_PROP_PREFIX) - 1))
491         continue;
492
493       /* Delete property if the name can't be found in SOURCE_PROPS. */
494       if (! svn_hash_gets(source_props, propname))
495         SVN_ERR(svn_ra_change_rev_prop2(session, rev, propname, NULL,
496                                         NULL, subpool));
497     }
498
499   svn_pool_destroy(subpool);
500
501   return SVN_NO_ERROR;
502 }
503
504 /* Filter callback function.
505  * Takes a property name KEY, and is expected to return TRUE if the property
506  * should be filtered out (ie. not be copied to the target list), or FALSE if
507  * not.
508  */
509 typedef svn_boolean_t (*filter_func_t)(const char *key);
510
511 /* Make a new set of properties, by copying those properties in PROPS for which
512  * the filter FILTER returns FALSE.
513  *
514  * The number of properties not copied will be stored in FILTERED_COUNT.
515  *
516  * The returned set of properties is allocated from POOL.
517  */
518 static apr_hash_t *
519 filter_props(int *filtered_count, apr_hash_t *props,
520              filter_func_t filter,
521              apr_pool_t *pool)
522 {
523   apr_hash_index_t *hi;
524   apr_hash_t *filtered = apr_hash_make(pool);
525   *filtered_count = 0;
526
527   for (hi = apr_hash_first(pool, props); hi ; hi = apr_hash_next(hi))
528     {
529       const char *propname = svn__apr_hash_index_key(hi);
530       void *propval = svn__apr_hash_index_val(hi);
531
532       /* Copy all properties:
533           - not matching the exclude pattern if provided OR
534           - matching the include pattern if provided */
535       if (!filter || !filter(propname))
536         {
537           svn_hash_sets(filtered, propname, propval);
538         }
539       else
540         {
541           *filtered_count += 1;
542         }
543     }
544
545   return filtered;
546 }
547
548
549 /* Write the set of revision properties REV_PROPS to revision REV to the
550  * repository associated with RA session SESSION.
551  * Omit any properties whose names are in the svnsync property name space,
552  * and set *FILTERED_COUNT to the number of properties thus omitted.
553  * REV_PROPS is a hash mapping (char *)propname to (svn_string_t *)propval.
554  *
555  * All allocations will be done in a subpool of POOL.
556  */
557 static svn_error_t *
558 write_revprops(int *filtered_count,
559                svn_ra_session_t *session,
560                svn_revnum_t rev,
561                apr_hash_t *rev_props,
562                apr_pool_t *pool)
563 {
564   apr_pool_t *subpool = svn_pool_create(pool);
565   apr_hash_index_t *hi;
566
567   *filtered_count = 0;
568
569   for (hi = apr_hash_first(pool, rev_props); hi; hi = apr_hash_next(hi))
570     {
571       const char *propname = svn__apr_hash_index_key(hi);
572       const svn_string_t *propval = svn__apr_hash_index_val(hi);
573
574       svn_pool_clear(subpool);
575
576       if (strncmp(propname, SVNSYNC_PROP_PREFIX,
577                   sizeof(SVNSYNC_PROP_PREFIX) - 1) != 0)
578         {
579           SVN_ERR(svn_ra_change_rev_prop2(session, rev, propname, NULL,
580                                           propval, subpool));
581         }
582       else
583         {
584           *filtered_count += 1;
585         }
586     }
587
588   svn_pool_destroy(subpool);
589
590   return SVN_NO_ERROR;
591 }
592
593
594 static svn_error_t *
595 log_properties_copied(svn_boolean_t syncprops_found,
596                       svn_revnum_t rev,
597                       apr_pool_t *pool)
598 {
599   if (syncprops_found)
600     SVN_ERR(svn_cmdline_printf(pool,
601                                _("Copied properties for revision %ld "
602                                  "(%s* properties skipped).\n"),
603                                rev, SVNSYNC_PROP_PREFIX));
604   else
605     SVN_ERR(svn_cmdline_printf(pool,
606                                _("Copied properties for revision %ld.\n"),
607                                rev));
608
609   return SVN_NO_ERROR;
610 }
611
612 /* Print a notification that NORMALIZED_REV_PROPS_COUNT rev-props and
613  * NORMALIZED_NODE_PROPS_COUNT node-props were normalized to LF line
614  * endings, if either of those numbers is non-zero. */
615 static svn_error_t *
616 log_properties_normalized(int normalized_rev_props_count,
617                           int normalized_node_props_count,
618                           apr_pool_t *pool)
619 {
620   if (normalized_rev_props_count > 0 || normalized_node_props_count > 0)
621     SVN_ERR(svn_cmdline_printf(pool,
622                                _("NOTE: Normalized %s* properties "
623                                  "to LF line endings (%d rev-props, "
624                                  "%d node-props).\n"),
625                                SVN_PROP_PREFIX,
626                                normalized_rev_props_count,
627                                normalized_node_props_count));
628   return SVN_NO_ERROR;
629 }
630
631
632 /* Copy all the revision properties, except for those that have the
633  * "svn:sync-" prefix, from revision REV of the repository associated
634  * with RA session FROM_SESSION, to the repository associated with RA
635  * session TO_SESSION.
636  *
637  * If SYNC is TRUE, then properties on the destination revision that
638  * do not exist on the source revision will be removed.
639  *
640  * If QUIET is FALSE, then log_properties_copied() is called to log that
641  * properties were copied for revision REV.
642  *
643  * Make sure the values of svn:* revision properties use only LF (\n)
644  * line ending style, correcting their values as necessary. The number
645  * of properties that were normalized is returned in *NORMALIZED_COUNT.
646  */
647 static svn_error_t *
648 copy_revprops(svn_ra_session_t *from_session,
649               svn_ra_session_t *to_session,
650               svn_revnum_t rev,
651               svn_boolean_t sync,
652               svn_boolean_t quiet,
653               const char *source_prop_encoding,
654               int *normalized_count,
655               apr_pool_t *pool)
656 {
657   apr_pool_t *subpool = svn_pool_create(pool);
658   apr_hash_t *existing_props, *rev_props;
659   int filtered_count = 0;
660
661   /* Get the list of revision properties on REV of TARGET. We're only interested
662      in the property names, but we'll get the values 'for free'. */
663   if (sync)
664     SVN_ERR(svn_ra_rev_proplist(to_session, rev, &existing_props, subpool));
665   else
666     existing_props = NULL;
667
668   /* Get the list of revision properties on REV of SOURCE. */
669   SVN_ERR(svn_ra_rev_proplist(from_session, rev, &rev_props, subpool));
670
671   /* If necessary, normalize encoding and line ending style and return the count
672      of EOL-normalized properties in int *NORMALIZED_COUNT. */
673   SVN_ERR(svnsync_normalize_revprops(rev_props, normalized_count,
674                                      source_prop_encoding, pool));
675
676   /* Copy all but the svn:svnsync properties. */
677   SVN_ERR(write_revprops(&filtered_count, to_session, rev, rev_props, pool));
678
679   /* Delete those properties that were in TARGET but not in SOURCE */
680   if (sync)
681     SVN_ERR(remove_props_not_in_source(to_session, rev,
682                                        rev_props, existing_props, pool));
683
684   if (! quiet)
685     SVN_ERR(log_properties_copied(filtered_count > 0, rev, pool));
686
687   svn_pool_destroy(subpool);
688
689   return SVN_NO_ERROR;
690 }
691
692
693 /* Return a subcommand baton allocated from POOL and populated with
694    data from the provided parameters, which include the global
695    OPT_BATON options structure and a handful of other options.  Not
696    all parameters are used in all subcommands -- see
697    subcommand_baton_t's definition for details. */
698 static subcommand_baton_t *
699 make_subcommand_baton(opt_baton_t *opt_baton,
700                       const char *to_url,
701                       const char *from_url,
702                       svn_revnum_t start_rev,
703                       svn_revnum_t end_rev,
704                       apr_pool_t *pool)
705 {
706   subcommand_baton_t *b = apr_pcalloc(pool, sizeof(*b));
707   b->config = opt_baton->config;
708   b->source_callbacks.open_tmp_file = open_tmp_file;
709   b->source_callbacks.auth_baton = opt_baton->source_auth_baton;
710   b->sync_callbacks.open_tmp_file = open_tmp_file;
711   b->sync_callbacks.auth_baton = opt_baton->sync_auth_baton;
712   b->quiet = opt_baton->quiet;
713   b->allow_non_empty = opt_baton->allow_non_empty;
714   b->to_url = to_url;
715   b->source_prop_encoding = opt_baton->source_prop_encoding;
716   b->from_url = from_url;
717   b->start_rev = start_rev;
718   b->end_rev = end_rev;
719   return b;
720 }
721
722 static svn_error_t *
723 open_target_session(svn_ra_session_t **to_session_p,
724                     subcommand_baton_t *baton,
725                     apr_pool_t *pool);
726
727 \f
728 /*** `svnsync init' ***/
729
730 /* Initialize the repository associated with RA session TO_SESSION,
731  * using information found in BATON, while the repository is
732  * locked.  Implements `with_locked_func_t' interface.
733  */
734 static svn_error_t *
735 do_initialize(svn_ra_session_t *to_session,
736               subcommand_baton_t *baton,
737               apr_pool_t *pool)
738 {
739   svn_ra_session_t *from_session;
740   svn_string_t *from_url;
741   svn_revnum_t latest, from_latest;
742   const char *uuid, *root_url;
743   int normalized_rev_props_count;
744
745   /* First, sanity check to see that we're copying into a brand new
746      repos.  If we aren't, and we aren't being asked to forcibly
747      complete this initialization, that's a bad news.  */
748   SVN_ERR(svn_ra_get_latest_revnum(to_session, &latest, pool));
749   if ((latest != 0) && (! baton->allow_non_empty))
750     return svn_error_create
751       (APR_EINVAL, NULL,
752        _("Destination repository already contains revision history; consider "
753          "using --allow-non-empty if the repository's revisions are known "
754          "to mirror their respective revisions in the source repository"));
755
756   SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_FROM_URL,
757                           &from_url, pool));
758   if (from_url && (! baton->allow_non_empty))
759     return svn_error_createf
760       (APR_EINVAL, NULL,
761        _("Destination repository is already synchronizing from '%s'"),
762        from_url->data);
763
764   /* Now fill in our bookkeeping info in the dest repository. */
765
766   SVN_ERR(svn_ra_open4(&from_session, NULL, baton->from_url, NULL,
767                        &(baton->source_callbacks), baton,
768                        baton->config, pool));
769   SVN_ERR(svn_ra_get_repos_root2(from_session, &root_url, pool));
770
771   /* If we're doing a partial replay, we have to check first if the server
772      supports this. */
773   if (strcmp(root_url, baton->from_url) != 0)
774     {
775       svn_boolean_t server_supports_partial_replay;
776       svn_error_t *err = svn_ra_has_capability(from_session,
777                                                &server_supports_partial_replay,
778                                                SVN_RA_CAPABILITY_PARTIAL_REPLAY,
779                                                pool);
780       if (err && err->apr_err != SVN_ERR_UNKNOWN_CAPABILITY)
781         return svn_error_trace(err);
782
783       if (err || !server_supports_partial_replay)
784         return svn_error_create(SVN_ERR_RA_PARTIAL_REPLAY_NOT_SUPPORTED, err,
785                                 NULL);
786     }
787
788   /* If we're initializing a non-empty destination, we'll make sure
789      that it at least doesn't have more revisions than the source. */
790   if (latest != 0)
791     {
792       SVN_ERR(svn_ra_get_latest_revnum(from_session, &from_latest, pool));
793       if (from_latest < latest)
794         return svn_error_create
795           (APR_EINVAL, NULL,
796            _("Destination repository has more revisions than source "
797              "repository"));
798     }
799
800   SVN_ERR(svn_ra_change_rev_prop2(to_session, 0, SVNSYNC_PROP_FROM_URL, NULL,
801                                   svn_string_create(baton->from_url, pool),
802                                   pool));
803
804   SVN_ERR(svn_ra_get_uuid2(from_session, &uuid, pool));
805   SVN_ERR(svn_ra_change_rev_prop2(to_session, 0, SVNSYNC_PROP_FROM_UUID, NULL,
806                                   svn_string_create(uuid, pool), pool));
807
808   SVN_ERR(svn_ra_change_rev_prop2(to_session, 0, SVNSYNC_PROP_LAST_MERGED_REV,
809                                   NULL, svn_string_createf(pool, "%ld", latest),
810                                   pool));
811
812   /* Copy all non-svnsync revprops from the LATEST rev in the source
813      repository into the destination, notifying about normalized
814      props, if any.  When LATEST is 0, this serves the practical
815      purpose of initializing data that would otherwise be overlooked
816      by the sync process (which is going to begin with r1).  When
817      LATEST is not 0, this really serves merely aesthetic and
818      informational purposes, keeping the output of this command
819      consistent while allowing folks to see what the latest revision is.  */
820   SVN_ERR(copy_revprops(from_session, to_session, latest, FALSE, baton->quiet,
821                         baton->source_prop_encoding, &normalized_rev_props_count,
822                         pool));
823
824   SVN_ERR(log_properties_normalized(normalized_rev_props_count, 0, pool));
825
826   /* TODO: It would be nice if we could set the dest repos UUID to be
827      equal to the UUID of the source repos, at least optionally.  That
828      way people could check out/log/diff using a local fast mirror,
829      but switch --relocate to the actual final repository in order to
830      make changes...  But at this time, the RA layer doesn't have a
831      way to set a UUID. */
832
833   return SVN_NO_ERROR;
834 }
835
836
837 /* SUBCOMMAND: init */
838 static svn_error_t *
839 initialize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
840 {
841   const char *to_url, *from_url;
842   svn_ra_session_t *to_session;
843   opt_baton_t *opt_baton = b;
844   apr_array_header_t *targets;
845   subcommand_baton_t *baton;
846
847   SVN_ERR(svn_opt__args_to_target_array(&targets, os,
848                                         apr_array_make(pool, 0,
849                                                        sizeof(const char *)),
850                                         pool));
851   if (targets->nelts < 2)
852     return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
853   if (targets->nelts > 2)
854     return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
855
856   to_url = APR_ARRAY_IDX(targets, 0, const char *);
857   from_url = APR_ARRAY_IDX(targets, 1, const char *);
858
859   if (! svn_path_is_url(to_url))
860     return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
861                              _("Path '%s' is not a URL"), to_url);
862   if (! svn_path_is_url(from_url))
863     return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
864                              _("Path '%s' is not a URL"), from_url);
865
866   baton = make_subcommand_baton(opt_baton, to_url, from_url, 0, 0, pool);
867   SVN_ERR(open_target_session(&to_session, baton, pool));
868   if (opt_baton->disable_locking)
869     SVN_ERR(do_initialize(to_session, baton, pool));
870   else
871     SVN_ERR(with_locked(to_session, do_initialize, baton,
872                         opt_baton->steal_lock, pool));
873
874   return SVN_NO_ERROR;
875 }
876
877
878 \f
879 /*** `svnsync sync' ***/
880
881 /* Implements `svn_commit_callback2_t' interface. */
882 static svn_error_t *
883 commit_callback(const svn_commit_info_t *commit_info,
884                 void *baton,
885                 apr_pool_t *pool)
886 {
887   subcommand_baton_t *sb = baton;
888
889   if (! sb->quiet)
890     {
891       SVN_ERR(svn_cmdline_printf(pool, _("Committed revision %ld.\n"),
892                                  commit_info->revision));
893     }
894
895   sb->committed_rev = commit_info->revision;
896
897   return SVN_NO_ERROR;
898 }
899
900
901 /* Set *FROM_SESSION to an RA session associated with the source
902  * repository of the synchronization.  If FROM_URL is non-NULL, use it
903  * as the source repository URL; otherwise, determine the source
904  * repository URL by reading svn:sync- properties from the destination
905  * repository (associated with TO_SESSION).  Set LAST_MERGED_REV to
906  * the value of the property which records the most recently
907  * synchronized revision.
908  *
909  * CALLBACKS is a vtable of RA callbacks to provide when creating
910  * *FROM_SESSION.  CONFIG is a configuration hash.
911  */
912 static svn_error_t *
913 open_source_session(svn_ra_session_t **from_session,
914                     svn_string_t **last_merged_rev,
915                     const char *from_url,
916                     svn_ra_session_t *to_session,
917                     svn_ra_callbacks2_t *callbacks,
918                     apr_hash_t *config,
919                     void *baton,
920                     apr_pool_t *pool)
921 {
922   apr_hash_t *props;
923   svn_string_t *from_url_str, *from_uuid_str;
924
925   SVN_ERR(svn_ra_rev_proplist(to_session, 0, &props, pool));
926
927   from_url_str = svn_hash_gets(props, SVNSYNC_PROP_FROM_URL);
928   from_uuid_str = svn_hash_gets(props, SVNSYNC_PROP_FROM_UUID);
929   *last_merged_rev = svn_hash_gets(props, SVNSYNC_PROP_LAST_MERGED_REV);
930
931   if (! from_url_str || ! from_uuid_str || ! *last_merged_rev)
932     return svn_error_create
933       (APR_EINVAL, NULL,
934        _("Destination repository has not been initialized"));
935
936   /* ### TODO: Should we validate that FROM_URL_STR->data matches any
937      provided FROM_URL here?  */
938   if (! from_url)
939     from_url = from_url_str->data;
940
941   /* Open the session to copy the revision data. */
942   SVN_ERR(svn_ra_open4(from_session, NULL, from_url, from_uuid_str->data,
943                        callbacks, baton, config, pool));
944
945   return SVN_NO_ERROR;
946 }
947
948 /* Set *TARGET_SESSION_P to an RA session associated with the target
949  * repository of the synchronization.
950  */
951 static svn_error_t *
952 open_target_session(svn_ra_session_t **target_session_p,
953                     subcommand_baton_t *baton,
954                     apr_pool_t *pool)
955 {
956   svn_ra_session_t *target_session;
957   SVN_ERR(svn_ra_open4(&target_session, NULL, baton->to_url, NULL,
958                        &(baton->sync_callbacks), baton, baton->config, pool));
959   SVN_ERR(check_if_session_is_at_repos_root(target_session, baton->to_url, pool));
960
961   *target_session_p = target_session;
962   return SVN_NO_ERROR;
963 }
964
965 /* Replay baton, used during synchronization. */
966 typedef struct replay_baton_t {
967   svn_ra_session_t *from_session;
968   svn_ra_session_t *to_session;
969   /* Extra 'backdoor' session for fetching data *from* the target repo. */
970   svn_ra_session_t *extra_to_session;
971   svn_revnum_t current_revision;
972   subcommand_baton_t *sb;
973   svn_boolean_t has_commit_revprops_capability;
974   int normalized_rev_props_count;
975   int normalized_node_props_count;
976   const char *to_root;
977 } replay_baton_t;
978
979 /* Return a replay baton allocated from POOL and populated with
980    data from the provided parameters. */
981 static svn_error_t *
982 make_replay_baton(replay_baton_t **baton_p,
983                   svn_ra_session_t *from_session,
984                   svn_ra_session_t *to_session,
985                   subcommand_baton_t *sb, apr_pool_t *pool)
986 {
987   replay_baton_t *rb = apr_pcalloc(pool, sizeof(*rb));
988   rb->from_session = from_session;
989   rb->to_session = to_session;
990   rb->sb = sb;
991
992   SVN_ERR(svn_ra_get_repos_root2(to_session, &rb->to_root, pool));
993
994 #ifdef ENABLE_EV2_SHIMS
995   /* Open up the extra baton.  Only needed for Ev2 shims. */
996   SVN_ERR(open_target_session(&rb->extra_to_session, sb, pool));
997 #endif
998
999   *baton_p = rb;
1000   return SVN_NO_ERROR;
1001 }
1002
1003 /* Return TRUE iff KEY is the name of an svn:date or svn:author or any svnsync
1004  * property. Implements filter_func_t. Use with filter_props() to filter out
1005  * svn:date and svn:author and svnsync properties.
1006  */
1007 static svn_boolean_t
1008 filter_exclude_date_author_sync(const char *key)
1009 {
1010   if (strcmp(key, SVN_PROP_REVISION_AUTHOR) == 0)
1011     return TRUE;
1012   else if (strcmp(key, SVN_PROP_REVISION_DATE) == 0)
1013     return TRUE;
1014   else if (strncmp(key, SVNSYNC_PROP_PREFIX,
1015                    sizeof(SVNSYNC_PROP_PREFIX) - 1) == 0)
1016     return TRUE;
1017
1018   return FALSE;
1019 }
1020
1021 /* Return FALSE iff KEY is the name of an svn:date or svn:author or any svnsync
1022  * property. Implements filter_func_t. Use with filter_props() to filter out
1023  * all properties except svn:date and svn:author and svnsync properties.
1024  */
1025 static svn_boolean_t
1026 filter_include_date_author_sync(const char *key)
1027 {
1028   return ! filter_exclude_date_author_sync(key);
1029 }
1030
1031
1032 /* Return TRUE iff KEY is the name of the svn:log property.
1033  * Implements filter_func_t. Use with filter_props() to only exclude svn:log.
1034  */
1035 static svn_boolean_t
1036 filter_exclude_log(const char *key)
1037 {
1038   if (strcmp(key, SVN_PROP_REVISION_LOG) == 0)
1039     return TRUE;
1040   else
1041     return FALSE;
1042 }
1043
1044 /* Return FALSE iff KEY is the name of the svn:log property.
1045  * Implements filter_func_t. Use with filter_props() to only include svn:log.
1046  */
1047 static svn_boolean_t
1048 filter_include_log(const char *key)
1049 {
1050   return ! filter_exclude_log(key);
1051 }
1052
1053
1054 static svn_error_t *
1055 fetch_base_func(const char **filename,
1056                 void *baton,
1057                 const char *path,
1058                 svn_revnum_t base_revision,
1059                 apr_pool_t *result_pool,
1060                 apr_pool_t *scratch_pool)
1061 {
1062   struct replay_baton_t *rb = baton;
1063   svn_stream_t *fstream;
1064   svn_error_t *err;
1065
1066   if (svn_path_is_url(path))
1067     path = svn_uri_skip_ancestor(rb->to_root, path, scratch_pool);
1068   else if (path[0] == '/')
1069     path += 1;
1070
1071   if (! SVN_IS_VALID_REVNUM(base_revision))
1072     base_revision = rb->current_revision - 1;
1073
1074   SVN_ERR(svn_stream_open_unique(&fstream, filename, NULL,
1075                                  svn_io_file_del_on_pool_cleanup,
1076                                  result_pool, scratch_pool));
1077
1078   err = svn_ra_get_file(rb->extra_to_session, path, base_revision,
1079                         fstream, NULL, NULL, scratch_pool);
1080   if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
1081     {
1082       svn_error_clear(err);
1083       SVN_ERR(svn_stream_close(fstream));
1084
1085       *filename = NULL;
1086       return SVN_NO_ERROR;
1087     }
1088   else if (err)
1089     return svn_error_trace(err);
1090
1091   SVN_ERR(svn_stream_close(fstream));
1092
1093   return SVN_NO_ERROR;
1094 }
1095
1096 static svn_error_t *
1097 fetch_props_func(apr_hash_t **props,
1098                  void *baton,
1099                  const char *path,
1100                  svn_revnum_t base_revision,
1101                  apr_pool_t *result_pool,
1102                  apr_pool_t *scratch_pool)
1103 {
1104   struct replay_baton_t *rb = baton;
1105   svn_node_kind_t node_kind;
1106
1107   if (svn_path_is_url(path))
1108     path = svn_uri_skip_ancestor(rb->to_root, path, scratch_pool);
1109   else if (path[0] == '/')
1110     path += 1;
1111
1112   if (! SVN_IS_VALID_REVNUM(base_revision))
1113     base_revision = rb->current_revision - 1;
1114
1115   SVN_ERR(svn_ra_check_path(rb->extra_to_session, path, base_revision,
1116                             &node_kind, scratch_pool));
1117
1118   if (node_kind == svn_node_file)
1119     {
1120       SVN_ERR(svn_ra_get_file(rb->extra_to_session, path, base_revision,
1121                               NULL, NULL, props, result_pool));
1122     }
1123   else if (node_kind == svn_node_dir)
1124     {
1125       apr_array_header_t *tmp_props;
1126
1127       SVN_ERR(svn_ra_get_dir2(rb->extra_to_session, NULL, NULL, props, path,
1128                               base_revision, 0 /* Dirent fields */,
1129                               result_pool));
1130       tmp_props = svn_prop_hash_to_array(*props, result_pool);
1131       SVN_ERR(svn_categorize_props(tmp_props, NULL, NULL, &tmp_props,
1132                                    result_pool));
1133       *props = svn_prop_array_to_hash(tmp_props, result_pool);
1134     }
1135   else
1136     {
1137       *props = apr_hash_make(result_pool);
1138     }
1139
1140   return SVN_NO_ERROR;
1141 }
1142
1143 static svn_error_t *
1144 fetch_kind_func(svn_node_kind_t *kind,
1145                 void *baton,
1146                 const char *path,
1147                 svn_revnum_t base_revision,
1148                 apr_pool_t *scratch_pool)
1149 {
1150   struct replay_baton_t *rb = baton;
1151
1152   if (svn_path_is_url(path))
1153     path = svn_uri_skip_ancestor(rb->to_root, path, scratch_pool);
1154   else if (path[0] == '/')
1155     path += 1;
1156
1157   if (! SVN_IS_VALID_REVNUM(base_revision))
1158     base_revision = rb->current_revision - 1;
1159
1160   SVN_ERR(svn_ra_check_path(rb->extra_to_session, path, base_revision,
1161                             kind, scratch_pool));
1162
1163   return SVN_NO_ERROR;
1164 }
1165
1166
1167 static svn_delta_shim_callbacks_t *
1168 get_shim_callbacks(replay_baton_t *rb,
1169                    apr_pool_t *result_pool)
1170 {
1171   svn_delta_shim_callbacks_t *callbacks =
1172                             svn_delta_shim_callbacks_default(result_pool);
1173
1174   callbacks->fetch_props_func = fetch_props_func;
1175   callbacks->fetch_kind_func = fetch_kind_func;
1176   callbacks->fetch_base_func = fetch_base_func;
1177   callbacks->fetch_baton = rb;
1178
1179   return callbacks;
1180 }
1181
1182
1183 /* Callback function for svn_ra_replay_range, invoked when starting to parse
1184  * a replay report.
1185  */
1186 static svn_error_t *
1187 replay_rev_started(svn_revnum_t revision,
1188                    void *replay_baton,
1189                    const svn_delta_editor_t **editor,
1190                    void **edit_baton,
1191                    apr_hash_t *rev_props,
1192                    apr_pool_t *pool)
1193 {
1194   const svn_delta_editor_t *commit_editor;
1195   const svn_delta_editor_t *cancel_editor;
1196   const svn_delta_editor_t *sync_editor;
1197   void *commit_baton;
1198   void *cancel_baton;
1199   void *sync_baton;
1200   replay_baton_t *rb = replay_baton;
1201   apr_hash_t *filtered;
1202   int filtered_count;
1203   int normalized_count;
1204
1205   /* We set this property so that if we error out for some reason
1206      we can later determine where we were in the process of
1207      merging a revision.  If we had committed the change, but we
1208      hadn't finished copying the revprops we need to know that, so
1209      we can go back and finish the job before we move on.
1210
1211      NOTE: We have to set this before we start the commit editor,
1212      because ra_svn doesn't let you change rev props during a
1213      commit. */
1214   SVN_ERR(svn_ra_change_rev_prop2(rb->to_session, 0,
1215                                   SVNSYNC_PROP_CURRENTLY_COPYING,
1216                                   NULL,
1217                                   svn_string_createf(pool, "%ld", revision),
1218                                   pool));
1219
1220   /* The actual copy is just a replay hooked up to a commit.  Include
1221      all the revision properties from the source repositories, except
1222      'svn:author' and 'svn:date', those are not guaranteed to get
1223      through the editor anyway.
1224      If we're syncing to an non-commit-revprops capable server, filter
1225      out all revprops except svn:log and add them later in
1226      revplay_rev_finished. */
1227   filtered = filter_props(&filtered_count, rev_props,
1228                           (rb->has_commit_revprops_capability
1229                             ? filter_exclude_date_author_sync
1230                             : filter_include_log),
1231                           pool);
1232
1233   /* svn_ra_get_commit_editor3 requires the log message to be
1234      set. It's possible that we didn't receive 'svn:log' here, so we
1235      have to set it to at least the empty string. If there's a svn:log
1236      property on this revision, we will write the actual value in the
1237      replay_rev_finished callback. */
1238   if (! svn_hash_gets(filtered, SVN_PROP_REVISION_LOG))
1239     svn_hash_sets(filtered, SVN_PROP_REVISION_LOG,
1240                   svn_string_create_empty(pool));
1241
1242   /* If necessary, normalize encoding and line ending style. Add the number
1243      of properties that required EOL normalization to the overall count
1244      in the replay baton. */
1245   SVN_ERR(svnsync_normalize_revprops(filtered, &normalized_count,
1246                                      rb->sb->source_prop_encoding, pool));
1247   rb->normalized_rev_props_count += normalized_count;
1248
1249   SVN_ERR(svn_ra__register_editor_shim_callbacks(rb->to_session,
1250                                 get_shim_callbacks(rb, pool)));
1251   SVN_ERR(svn_ra_get_commit_editor3(rb->to_session, &commit_editor,
1252                                     &commit_baton,
1253                                     filtered,
1254                                     commit_callback, rb->sb,
1255                                     NULL, FALSE, pool));
1256
1257   /* There's one catch though, the diff shows us props we can't send
1258      over the RA interface, so we need an editor that's smart enough
1259      to filter those out for us.  */
1260   SVN_ERR(svnsync_get_sync_editor(commit_editor, commit_baton, revision - 1,
1261                                   rb->sb->to_url, rb->sb->source_prop_encoding,
1262                                   rb->sb->quiet, &sync_editor, &sync_baton,
1263                                   &(rb->normalized_node_props_count), pool));
1264
1265   SVN_ERR(svn_delta_get_cancellation_editor(check_cancel, NULL,
1266                                             sync_editor, sync_baton,
1267                                             &cancel_editor,
1268                                             &cancel_baton,
1269                                             pool));
1270   *editor = cancel_editor;
1271   *edit_baton = cancel_baton;
1272
1273   rb->current_revision = revision;
1274   return SVN_NO_ERROR;
1275 }
1276
1277 /* Callback function for svn_ra_replay_range, invoked when finishing parsing
1278  * a replay report.
1279  */
1280 static svn_error_t *
1281 replay_rev_finished(svn_revnum_t revision,
1282                     void *replay_baton,
1283                     const svn_delta_editor_t *editor,
1284                     void *edit_baton,
1285                     apr_hash_t *rev_props,
1286                     apr_pool_t *pool)
1287 {
1288   apr_pool_t *subpool = svn_pool_create(pool);
1289   replay_baton_t *rb = replay_baton;
1290   apr_hash_t *filtered, *existing_props;
1291   int filtered_count;
1292   int normalized_count;
1293
1294   SVN_ERR(editor->close_edit(edit_baton, pool));
1295
1296   /* Sanity check that we actually committed the revision we meant to. */
1297   if (rb->sb->committed_rev != revision)
1298     return svn_error_createf
1299              (APR_EINVAL, NULL,
1300               _("Commit created rev %ld but should have created %ld"),
1301               rb->sb->committed_rev, revision);
1302
1303   SVN_ERR(svn_ra_rev_proplist(rb->to_session, revision, &existing_props,
1304                               subpool));
1305
1306
1307   /* Ok, we're done with the data, now we just need to copy the remaining
1308      'svn:date' and 'svn:author' revprops and we're all set.
1309      If the server doesn't support revprops-in-a-commit, we still have to
1310      set all revision properties except svn:log. */
1311   filtered = filter_props(&filtered_count, rev_props,
1312                           (rb->has_commit_revprops_capability
1313                             ? filter_include_date_author_sync
1314                             : filter_exclude_log),
1315                           subpool);
1316
1317   /* If necessary, normalize encoding and line ending style, and add the number
1318      of EOL-normalized properties to the overall count in the replay baton. */
1319   SVN_ERR(svnsync_normalize_revprops(filtered, &normalized_count,
1320                                      rb->sb->source_prop_encoding, pool));
1321   rb->normalized_rev_props_count += normalized_count;
1322
1323   SVN_ERR(write_revprops(&filtered_count, rb->to_session, revision, filtered,
1324                          subpool));
1325
1326   /* Remove all extra properties in TARGET. */
1327   SVN_ERR(remove_props_not_in_source(rb->to_session, revision,
1328                                      rev_props, existing_props, subpool));
1329
1330   svn_pool_clear(subpool);
1331
1332   /* Ok, we're done, bring the last-merged-rev property up to date. */
1333   SVN_ERR(svn_ra_change_rev_prop2(
1334            rb->to_session,
1335            0,
1336            SVNSYNC_PROP_LAST_MERGED_REV,
1337            NULL,
1338            svn_string_create(apr_psprintf(pool, "%ld", revision),
1339                              subpool),
1340            subpool));
1341
1342   /* And finally drop the currently copying prop, since we're done
1343      with this revision. */
1344   SVN_ERR(svn_ra_change_rev_prop2(rb->to_session, 0,
1345                                   SVNSYNC_PROP_CURRENTLY_COPYING,
1346                                   NULL, NULL, subpool));
1347
1348   /* Notify the user that we copied revision properties. */
1349   if (! rb->sb->quiet)
1350     SVN_ERR(log_properties_copied(filtered_count > 0, revision, subpool));
1351
1352   svn_pool_destroy(subpool);
1353
1354   return SVN_NO_ERROR;
1355 }
1356
1357 /* Synchronize the repository associated with RA session TO_SESSION,
1358  * using information found in BATON, while the repository is
1359  * locked.  Implements `with_locked_func_t' interface.
1360  */
1361 static svn_error_t *
1362 do_synchronize(svn_ra_session_t *to_session,
1363                subcommand_baton_t *baton, apr_pool_t *pool)
1364 {
1365   svn_string_t *last_merged_rev;
1366   svn_revnum_t from_latest;
1367   svn_ra_session_t *from_session;
1368   svn_string_t *currently_copying;
1369   svn_revnum_t to_latest, copying, last_merged;
1370   svn_revnum_t start_revision, end_revision;
1371   replay_baton_t *rb;
1372   int normalized_rev_props_count = 0;
1373
1374   SVN_ERR(open_source_session(&from_session, &last_merged_rev,
1375                               baton->from_url, to_session,
1376                               &(baton->source_callbacks), baton->config,
1377                               baton, pool));
1378
1379   /* Check to see if we have revprops that still need to be copied for
1380      a prior revision we didn't finish copying.  But first, check for
1381      state sanity.  Remember, mirroring is not an atomic action,
1382      because revision properties are copied separately from the
1383      revision's contents.
1384
1385      So, any time that currently-copying is not set, then
1386      last-merged-rev should be the HEAD revision of the destination
1387      repository.  That is, if we didn't fall over in the middle of a
1388      previous synchronization, then our destination repository should
1389      have exactly as many revisions in it as we've synchronized.
1390
1391      Alternately, if currently-copying *is* set, it must
1392      be either last-merged-rev or last-merged-rev + 1, and the HEAD
1393      revision must be equal to either last-merged-rev or
1394      currently-copying. If this is not the case, somebody has meddled
1395      with the destination without using svnsync.
1396   */
1397
1398   SVN_ERR(svn_ra_rev_prop(to_session, 0, SVNSYNC_PROP_CURRENTLY_COPYING,
1399                           &currently_copying, pool));
1400
1401   SVN_ERR(svn_ra_get_latest_revnum(to_session, &to_latest, pool));
1402
1403   last_merged = SVN_STR_TO_REV(last_merged_rev->data);
1404
1405   if (currently_copying)
1406     {
1407       copying = SVN_STR_TO_REV(currently_copying->data);
1408
1409       if ((copying < last_merged)
1410           || (copying > (last_merged + 1))
1411           || ((to_latest != last_merged) && (to_latest != copying)))
1412         {
1413           return svn_error_createf
1414             (APR_EINVAL, NULL,
1415              _("Revision being currently copied (%ld), last merged revision "
1416                "(%ld), and destination HEAD (%ld) are inconsistent; have you "
1417                "committed to the destination without using svnsync?"),
1418              copying, last_merged, to_latest);
1419         }
1420       else if (copying == to_latest)
1421         {
1422           if (copying > last_merged)
1423             {
1424               SVN_ERR(copy_revprops(from_session, to_session, to_latest, TRUE,
1425                                     baton->quiet, baton->source_prop_encoding,
1426                                     &normalized_rev_props_count, pool));
1427               last_merged = copying;
1428               last_merged_rev = svn_string_create
1429                 (apr_psprintf(pool, "%ld", last_merged), pool);
1430             }
1431
1432           /* Now update last merged rev and drop currently changing.
1433              Note that the order here is significant, if we do them
1434              in the wrong order there are race conditions where we
1435              end up not being able to tell if there have been bogus
1436              (i.e. non-svnsync) commits to the dest repository. */
1437
1438           SVN_ERR(svn_ra_change_rev_prop2(to_session, 0,
1439                                           SVNSYNC_PROP_LAST_MERGED_REV,
1440                                           NULL, last_merged_rev, pool));
1441           SVN_ERR(svn_ra_change_rev_prop2(to_session, 0,
1442                                           SVNSYNC_PROP_CURRENTLY_COPYING,
1443                                           NULL, NULL, pool));
1444         }
1445       /* If copying > to_latest, then we just fall through to
1446          attempting to copy the revision again. */
1447     }
1448   else
1449     {
1450       if (to_latest != last_merged)
1451         return svn_error_createf(APR_EINVAL, NULL,
1452                                  _("Destination HEAD (%ld) is not the last "
1453                                    "merged revision (%ld); have you "
1454                                    "committed to the destination without "
1455                                    "using svnsync?"),
1456                                  to_latest, last_merged);
1457     }
1458
1459   /* Now check to see if there are any revisions to copy. */
1460   SVN_ERR(svn_ra_get_latest_revnum(from_session, &from_latest, pool));
1461
1462   if (from_latest < last_merged)
1463     return SVN_NO_ERROR;
1464
1465   /* Ok, so there are new revisions, iterate over them copying them
1466      into the destination repository. */
1467   SVN_ERR(make_replay_baton(&rb, from_session, to_session, baton, pool));
1468
1469   /* For compatibility with older svnserve versions, check first if we
1470      support adding revprops to the commit. */
1471   SVN_ERR(svn_ra_has_capability(rb->to_session,
1472                                 &rb->has_commit_revprops_capability,
1473                                 SVN_RA_CAPABILITY_COMMIT_REVPROPS,
1474                                 pool));
1475
1476   start_revision = last_merged + 1;
1477   end_revision = from_latest;
1478
1479   SVN_ERR(check_cancel(NULL));
1480
1481   SVN_ERR(svn_ra_replay_range(from_session, start_revision, end_revision,
1482                               0, TRUE, replay_rev_started,
1483                               replay_rev_finished, rb, pool));
1484
1485   SVN_ERR(log_properties_normalized(rb->normalized_rev_props_count
1486                                       + normalized_rev_props_count,
1487                                     rb->normalized_node_props_count,
1488                                     pool));
1489
1490
1491   return SVN_NO_ERROR;
1492 }
1493
1494
1495 /* SUBCOMMAND: sync */
1496 static svn_error_t *
1497 synchronize_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
1498 {
1499   svn_ra_session_t *to_session;
1500   opt_baton_t *opt_baton = b;
1501   apr_array_header_t *targets;
1502   subcommand_baton_t *baton;
1503   const char *to_url, *from_url;
1504
1505   SVN_ERR(svn_opt__args_to_target_array(&targets, os,
1506                                         apr_array_make(pool, 0,
1507                                                        sizeof(const char *)),
1508                                         pool));
1509   if (targets->nelts < 1)
1510     return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1511   if (targets->nelts > 2)
1512     return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1513
1514   to_url = APR_ARRAY_IDX(targets, 0, const char *);
1515   if (! svn_path_is_url(to_url))
1516     return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1517                              _("Path '%s' is not a URL"), to_url);
1518
1519   if (targets->nelts == 2)
1520     {
1521       from_url = APR_ARRAY_IDX(targets, 1, const char *);
1522       if (! svn_path_is_url(from_url))
1523         return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1524                                  _("Path '%s' is not a URL"), from_url);
1525     }
1526   else
1527     {
1528       from_url = NULL; /* we'll read it from the destination repos */
1529     }
1530
1531   baton = make_subcommand_baton(opt_baton, to_url, from_url, 0, 0, pool);
1532   SVN_ERR(open_target_session(&to_session, baton, pool));
1533   if (opt_baton->disable_locking)
1534     SVN_ERR(do_synchronize(to_session, baton, pool));
1535   else
1536     SVN_ERR(with_locked(to_session, do_synchronize, baton,
1537                         opt_baton->steal_lock, pool));
1538
1539   return SVN_NO_ERROR;
1540 }
1541
1542
1543 \f
1544 /*** `svnsync copy-revprops' ***/
1545
1546 /* Copy revision properties to the repository associated with RA
1547  * session TO_SESSION, using information found in BATON, while the
1548  * repository is locked.  Implements `with_locked_func_t' interface.
1549  */
1550 static svn_error_t *
1551 do_copy_revprops(svn_ra_session_t *to_session,
1552                  subcommand_baton_t *baton, apr_pool_t *pool)
1553 {
1554   svn_ra_session_t *from_session;
1555   svn_string_t *last_merged_rev;
1556   svn_revnum_t i;
1557   svn_revnum_t step = 1;
1558   int normalized_rev_props_count = 0;
1559
1560   SVN_ERR(open_source_session(&from_session, &last_merged_rev,
1561                               baton->from_url, to_session,
1562                               &(baton->source_callbacks), baton->config,
1563                               baton, pool));
1564
1565   /* An invalid revision means "last-synced" */
1566   if (! SVN_IS_VALID_REVNUM(baton->start_rev))
1567     baton->start_rev = SVN_STR_TO_REV(last_merged_rev->data);
1568   if (! SVN_IS_VALID_REVNUM(baton->end_rev))
1569     baton->end_rev = SVN_STR_TO_REV(last_merged_rev->data);
1570
1571   /* Make sure we have revisions within the valid range. */
1572   if (baton->start_rev > SVN_STR_TO_REV(last_merged_rev->data))
1573     return svn_error_createf
1574       (APR_EINVAL, NULL,
1575        _("Cannot copy revprops for a revision (%ld) that has not "
1576          "been synchronized yet"), baton->start_rev);
1577   if (baton->end_rev > SVN_STR_TO_REV(last_merged_rev->data))
1578     return svn_error_createf
1579       (APR_EINVAL, NULL,
1580        _("Cannot copy revprops for a revision (%ld) that has not "
1581          "been synchronized yet"), baton->end_rev);
1582
1583   /* Now, copy all the requested revisions, in the requested order. */
1584   step = (baton->start_rev > baton->end_rev) ? -1 : 1;
1585   for (i = baton->start_rev; i != baton->end_rev + step; i = i + step)
1586     {
1587       int normalized_count;
1588       SVN_ERR(check_cancel(NULL));
1589       SVN_ERR(copy_revprops(from_session, to_session, i, TRUE, baton->quiet,
1590                             baton->source_prop_encoding, &normalized_count,
1591                             pool));
1592       normalized_rev_props_count += normalized_count;
1593     }
1594
1595   /* Notify about normalized props, if any. */
1596   SVN_ERR(log_properties_normalized(normalized_rev_props_count, 0, pool));
1597
1598   return SVN_NO_ERROR;
1599 }
1600
1601
1602 /* Set *START_REVNUM to the revision number associated with
1603    START_REVISION, or to SVN_INVALID_REVNUM if START_REVISION
1604    represents "HEAD"; if END_REVISION is specified, set END_REVNUM to
1605    the revision number associated with END_REVISION or to
1606    SVN_INVALID_REVNUM if END_REVISION represents "HEAD"; otherwise set
1607    END_REVNUM to the same value as START_REVNUM.
1608
1609    As a special case, if neither START_REVISION nor END_REVISION is
1610    specified, set *START_REVNUM to 0 and set *END_REVNUM to
1611    SVN_INVALID_REVNUM.
1612
1613    Freak out if either START_REVISION or END_REVISION represents an
1614    explicit but invalid revision number. */
1615 static svn_error_t *
1616 resolve_revnums(svn_revnum_t *start_revnum,
1617                 svn_revnum_t *end_revnum,
1618                 svn_opt_revision_t start_revision,
1619                 svn_opt_revision_t end_revision)
1620 {
1621   svn_revnum_t start_rev, end_rev;
1622
1623   /* Special case: neither revision is specified?  This is like
1624      -r0:HEAD. */
1625   if ((start_revision.kind == svn_opt_revision_unspecified) &&
1626       (end_revision.kind == svn_opt_revision_unspecified))
1627     {
1628       *start_revnum = 0;
1629       *end_revnum = SVN_INVALID_REVNUM;
1630       return SVN_NO_ERROR;
1631     }
1632
1633   /* Get the start revision, which must be either HEAD or a number
1634      (which is required to be a valid one). */
1635   if (start_revision.kind == svn_opt_revision_head)
1636     {
1637       start_rev = SVN_INVALID_REVNUM;
1638     }
1639   else
1640     {
1641       start_rev = start_revision.value.number;
1642       if (! SVN_IS_VALID_REVNUM(start_rev))
1643         return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1644                                  _("Invalid revision number (%ld)"),
1645                                  start_rev);
1646     }
1647
1648   /* Get the end revision, which must be unspecified (meaning,
1649      "same as the start_rev"), HEAD, or a number (which is
1650      required to be a valid one). */
1651   if (end_revision.kind == svn_opt_revision_unspecified)
1652     {
1653       end_rev = start_rev;
1654     }
1655   else if (end_revision.kind == svn_opt_revision_head)
1656     {
1657       end_rev = SVN_INVALID_REVNUM;
1658     }
1659   else
1660     {
1661       end_rev = end_revision.value.number;
1662       if (! SVN_IS_VALID_REVNUM(end_rev))
1663         return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1664                                  _("Invalid revision number (%ld)"),
1665                                  end_rev);
1666     }
1667
1668   *start_revnum = start_rev;
1669   *end_revnum = end_rev;
1670   return SVN_NO_ERROR;
1671 }
1672
1673
1674 /* SUBCOMMAND: copy-revprops */
1675 static svn_error_t *
1676 copy_revprops_cmd(apr_getopt_t *os, void *b, apr_pool_t *pool)
1677 {
1678   svn_ra_session_t *to_session;
1679   opt_baton_t *opt_baton = b;
1680   apr_array_header_t *targets;
1681   subcommand_baton_t *baton;
1682   const char *to_url = NULL;
1683   const char *from_url = NULL;
1684   svn_opt_revision_t start_revision, end_revision;
1685   svn_revnum_t start_rev = 0, end_rev = SVN_INVALID_REVNUM;
1686
1687   /* There should be either one or two arguments left to parse. */
1688   if (os->argc - os->ind > 2)
1689     return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1690   if (os->argc - os->ind < 1)
1691     return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1692
1693   /* If there are two args, the last one is either a revision range or
1694      the source URL.  */
1695   if (os->argc - os->ind == 2)
1696     {
1697       const char *arg_str = os->argv[os->argc - 1];
1698       const char *utf_arg_str;
1699
1700       SVN_ERR(svn_utf_cstring_to_utf8(&utf_arg_str, arg_str, pool));
1701
1702       if (! svn_path_is_url(utf_arg_str))
1703         {
1704           /* This is the old "... TO_URL REV[:REV2]" syntax.
1705              Revisions come only from this argument.  (We effectively
1706              pop that last argument from the end of the argument list
1707              so svn_opt__args_to_target_array() can do its thang.) */
1708           os->argc--;
1709
1710           if ((opt_baton->start_rev.kind != svn_opt_revision_unspecified)
1711               || (opt_baton->end_rev.kind != svn_opt_revision_unspecified))
1712             return svn_error_create(
1713                 SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1714                 _("Cannot specify revisions via both command-line arguments "
1715                   "and the --revision (-r) option"));
1716
1717           start_revision.kind = svn_opt_revision_unspecified;
1718           end_revision.kind = svn_opt_revision_unspecified;
1719           if (svn_opt_parse_revision(&start_revision, &end_revision,
1720                                      arg_str, pool) != 0)
1721             return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1722                                      _("Invalid revision range '%s' provided"),
1723                                      arg_str);
1724
1725           SVN_ERR(resolve_revnums(&start_rev, &end_rev,
1726                                   start_revision, end_revision));
1727
1728           SVN_ERR(svn_opt__args_to_target_array(
1729                       &targets, os,
1730                       apr_array_make(pool, 1, sizeof(const char *)), pool));
1731           if (targets->nelts != 1)
1732             return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1733           to_url = APR_ARRAY_IDX(targets, 0, const char *);
1734           from_url = NULL;
1735         }
1736     }
1737
1738   if (! to_url)
1739     {
1740       /* This is the "... TO_URL SOURCE_URL" syntax.  Revisions
1741          come only from the --revision parameter.  */
1742       SVN_ERR(resolve_revnums(&start_rev, &end_rev,
1743                               opt_baton->start_rev, opt_baton->end_rev));
1744
1745       SVN_ERR(svn_opt__args_to_target_array(
1746                   &targets, os,
1747                   apr_array_make(pool, 2, sizeof(const char *)), pool));
1748       if (targets->nelts < 1)
1749         return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1750       if (targets->nelts > 2)
1751         return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1752       to_url = APR_ARRAY_IDX(targets, 0, const char *);
1753       if (targets->nelts == 2)
1754         from_url = APR_ARRAY_IDX(targets, 1, const char *);
1755       else
1756         from_url = NULL;
1757     }
1758
1759   if (! svn_path_is_url(to_url))
1760     return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1761                              _("Path '%s' is not a URL"), to_url);
1762   if (from_url && (! svn_path_is_url(from_url)))
1763     return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1764                              _("Path '%s' is not a URL"), from_url);
1765
1766   baton = make_subcommand_baton(opt_baton, to_url, from_url,
1767                                 start_rev, end_rev, pool);
1768   SVN_ERR(open_target_session(&to_session, baton, pool));
1769   if (opt_baton->disable_locking)
1770     SVN_ERR(do_copy_revprops(to_session, baton, pool));
1771   else
1772     SVN_ERR(with_locked(to_session, do_copy_revprops, baton,
1773                         opt_baton->steal_lock, pool));
1774
1775   return SVN_NO_ERROR;
1776 }
1777
1778
1779 \f
1780 /*** `svnsync info' ***/
1781
1782
1783 /* SUBCOMMAND: info */
1784 static svn_error_t *
1785 info_cmd(apr_getopt_t *os, void *b, apr_pool_t * pool)
1786 {
1787   svn_ra_session_t *to_session;
1788   opt_baton_t *opt_baton = b;
1789   apr_array_header_t *targets;
1790   subcommand_baton_t *baton;
1791   const char *to_url;
1792   apr_hash_t *props;
1793   svn_string_t *from_url, *from_uuid, *last_merged_rev;
1794
1795   SVN_ERR(svn_opt__args_to_target_array(&targets, os,
1796                                         apr_array_make(pool, 0,
1797                                                        sizeof(const char *)),
1798                                         pool));
1799   if (targets->nelts < 1)
1800     return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
1801   if (targets->nelts > 1)
1802     return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 0, NULL);
1803
1804   /* Get the mirror repository URL, and verify that it is URL-ish. */
1805   to_url = APR_ARRAY_IDX(targets, 0, const char *);
1806   if (! svn_path_is_url(to_url))
1807     return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1808                              _("Path '%s' is not a URL"), to_url);
1809
1810   /* Open an RA session to the mirror repository URL. */
1811   baton = make_subcommand_baton(opt_baton, to_url, NULL, 0, 0, pool);
1812   SVN_ERR(open_target_session(&to_session, baton, pool));
1813
1814   SVN_ERR(svn_ra_rev_proplist(to_session, 0, &props, pool));
1815
1816   from_url = svn_hash_gets(props, SVNSYNC_PROP_FROM_URL);
1817
1818   if (! from_url)
1819     return svn_error_createf
1820       (SVN_ERR_BAD_URL, NULL,
1821        _("Repository '%s' is not initialized for synchronization"), to_url);
1822
1823   from_uuid = svn_hash_gets(props, SVNSYNC_PROP_FROM_UUID);
1824   last_merged_rev = svn_hash_gets(props, SVNSYNC_PROP_LAST_MERGED_REV);
1825
1826   /* Print the info. */
1827   SVN_ERR(svn_cmdline_printf(pool, _("Source URL: %s\n"), from_url->data));
1828   if (from_uuid)
1829     SVN_ERR(svn_cmdline_printf(pool, _("Source Repository UUID: %s\n"),
1830                                from_uuid->data));
1831   if (last_merged_rev)
1832     SVN_ERR(svn_cmdline_printf(pool, _("Last Merged Revision: %s\n"),
1833                                last_merged_rev->data));
1834   return SVN_NO_ERROR;
1835 }
1836
1837
1838 \f
1839 /*** `svnsync help' ***/
1840
1841
1842 /* SUBCOMMAND: help */
1843 static svn_error_t *
1844 help_cmd(apr_getopt_t *os, void *baton, apr_pool_t *pool)
1845 {
1846   opt_baton_t *opt_baton = baton;
1847
1848   const char *header =
1849     _("general usage: svnsync SUBCOMMAND DEST_URL  [ARGS & OPTIONS ...]\n"
1850       "Type 'svnsync help <subcommand>' for help on a specific subcommand.\n"
1851       "Type 'svnsync --version' to see the program version and RA modules.\n"
1852       "\n"
1853       "Available subcommands:\n");
1854
1855   const char *ra_desc_start
1856     = _("The following repository access (RA) modules are available:\n\n");
1857
1858   svn_stringbuf_t *version_footer = svn_stringbuf_create(ra_desc_start,
1859                                                          pool);
1860
1861   SVN_ERR(svn_ra_print_modules(version_footer, pool));
1862
1863   SVN_ERR(svn_opt_print_help4(os, "svnsync",
1864                               opt_baton ? opt_baton->version : FALSE,
1865                               opt_baton ? opt_baton->quiet : FALSE,
1866                               /*###opt_state ? opt_state->verbose :*/ FALSE,
1867                               version_footer->data, header,
1868                               svnsync_cmd_table, svnsync_options, NULL,
1869                               NULL, pool));
1870
1871   return SVN_NO_ERROR;
1872 }
1873
1874
1875 \f
1876 /*** Main ***/
1877
1878 int
1879 main(int argc, const char *argv[])
1880 {
1881   const svn_opt_subcommand_desc2_t *subcommand = NULL;
1882   apr_array_header_t *received_opts;
1883   opt_baton_t opt_baton;
1884   svn_config_t *config;
1885   apr_status_t apr_err;
1886   apr_getopt_t *os;
1887   apr_pool_t *pool;
1888   svn_error_t *err;
1889   int opt_id, i;
1890   const char *username = NULL, *source_username = NULL, *sync_username = NULL;
1891   const char *password = NULL, *source_password = NULL, *sync_password = NULL;
1892   apr_array_header_t *config_options = NULL;
1893   const char *source_prop_encoding = NULL;
1894   svn_boolean_t force_interactive = FALSE;
1895
1896   if (svn_cmdline_init("svnsync", stderr) != EXIT_SUCCESS)
1897     {
1898       return EXIT_FAILURE;
1899     }
1900
1901   err = check_lib_versions();
1902   if (err)
1903     return svn_cmdline_handle_exit_error(err, NULL, "svnsync: ");
1904
1905   /* Create our top-level pool.  Use a separate mutexless allocator,
1906    * given this application is single threaded.
1907    */
1908   pool = apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
1909
1910   err = svn_ra_initialize(pool);
1911   if (err)
1912     return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1913
1914   /* Initialize the option baton. */
1915   memset(&opt_baton, 0, sizeof(opt_baton));
1916   opt_baton.start_rev.kind = svn_opt_revision_unspecified;
1917   opt_baton.end_rev.kind = svn_opt_revision_unspecified;
1918
1919   received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int));
1920
1921   if (argc <= 1)
1922     {
1923       SVN_INT_ERR(help_cmd(NULL, NULL, pool));
1924       svn_pool_destroy(pool);
1925       return EXIT_FAILURE;
1926     }
1927
1928   err = svn_cmdline__getopt_init(&os, argc, argv, pool);
1929   if (err)
1930     return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
1931
1932   os->interleave = 1;
1933
1934   for (;;)
1935     {
1936       const char *opt_arg;
1937       svn_error_t* opt_err = NULL;
1938
1939       apr_err = apr_getopt_long(os, svnsync_options, &opt_id, &opt_arg);
1940       if (APR_STATUS_IS_EOF(apr_err))
1941         break;
1942       else if (apr_err)
1943         {
1944           SVN_INT_ERR(help_cmd(NULL, NULL, pool));
1945           svn_pool_destroy(pool);
1946           return EXIT_FAILURE;
1947         }
1948
1949       APR_ARRAY_PUSH(received_opts, int) = opt_id;
1950
1951       switch (opt_id)
1952         {
1953           case svnsync_opt_non_interactive:
1954             opt_baton.non_interactive = TRUE;
1955             break;
1956
1957           case svnsync_opt_force_interactive:
1958             force_interactive = TRUE;
1959             break;
1960
1961           case svnsync_opt_trust_server_cert:
1962             opt_baton.trust_server_cert = TRUE;
1963             break;
1964
1965           case svnsync_opt_no_auth_cache:
1966             opt_baton.no_auth_cache = TRUE;
1967             break;
1968
1969           case svnsync_opt_auth_username:
1970             opt_err = svn_utf_cstring_to_utf8(&username, opt_arg, pool);
1971             break;
1972
1973           case svnsync_opt_auth_password:
1974             opt_err = svn_utf_cstring_to_utf8(&password, opt_arg, pool);
1975             break;
1976
1977           case svnsync_opt_source_username:
1978             opt_err = svn_utf_cstring_to_utf8(&source_username, opt_arg, pool);
1979             break;
1980
1981           case svnsync_opt_source_password:
1982             opt_err = svn_utf_cstring_to_utf8(&source_password, opt_arg, pool);
1983             break;
1984
1985           case svnsync_opt_sync_username:
1986             opt_err = svn_utf_cstring_to_utf8(&sync_username, opt_arg, pool);
1987             break;
1988
1989           case svnsync_opt_sync_password:
1990             opt_err = svn_utf_cstring_to_utf8(&sync_password, opt_arg, pool);
1991             break;
1992
1993           case svnsync_opt_config_dir:
1994             {
1995               const char *path_utf8;
1996               opt_err = svn_utf_cstring_to_utf8(&path_utf8, opt_arg, pool);
1997
1998               if (!opt_err)
1999                 opt_baton.config_dir = svn_dirent_internal_style(path_utf8, pool);
2000             }
2001             break;
2002           case svnsync_opt_config_options:
2003             if (!config_options)
2004               config_options =
2005                     apr_array_make(pool, 1,
2006                                    sizeof(svn_cmdline__config_argument_t*));
2007
2008             err = svn_utf_cstring_to_utf8(&opt_arg, opt_arg, pool);
2009             if (!err)
2010               err = svn_cmdline__parse_config_option(config_options,
2011                                                      opt_arg, pool);
2012             if (err)
2013               return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2014             break;
2015
2016           case svnsync_opt_source_prop_encoding:
2017             opt_err = svn_utf_cstring_to_utf8(&source_prop_encoding, opt_arg,
2018                                               pool);
2019             break;
2020
2021           case svnsync_opt_disable_locking:
2022             opt_baton.disable_locking = TRUE;
2023             break;
2024
2025           case svnsync_opt_steal_lock:
2026             opt_baton.steal_lock = TRUE;
2027             break;
2028
2029           case svnsync_opt_version:
2030             opt_baton.version = TRUE;
2031             break;
2032
2033           case svnsync_opt_allow_non_empty:
2034             opt_baton.allow_non_empty = TRUE;
2035             break;
2036
2037           case 'q':
2038             opt_baton.quiet = TRUE;
2039             break;
2040
2041           case 'r':
2042             if (svn_opt_parse_revision(&opt_baton.start_rev,
2043                                        &opt_baton.end_rev,
2044                                        opt_arg, pool) != 0)
2045               {
2046                 const char *utf8_opt_arg;
2047                 err = svn_utf_cstring_to_utf8(&utf8_opt_arg, opt_arg, pool);
2048                 if (! err)
2049                   err = svn_error_createf(
2050                             SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
2051                             _("Syntax error in revision argument '%s'"),
2052                             utf8_opt_arg);
2053                 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2054               }
2055
2056             /* We only allow numbers and 'HEAD'. */
2057             if (((opt_baton.start_rev.kind != svn_opt_revision_number) &&
2058                  (opt_baton.start_rev.kind != svn_opt_revision_head))
2059                 || ((opt_baton.end_rev.kind != svn_opt_revision_number) &&
2060                     (opt_baton.end_rev.kind != svn_opt_revision_head) &&
2061                     (opt_baton.end_rev.kind != svn_opt_revision_unspecified)))
2062               {
2063                 err = svn_error_createf(
2064                           SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
2065                           _("Invalid revision range '%s' provided"), opt_arg);
2066                 return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2067               }
2068             break;
2069
2070           case '?':
2071           case 'h':
2072             opt_baton.help = TRUE;
2073             break;
2074
2075           default:
2076             {
2077               SVN_INT_ERR(help_cmd(NULL, NULL, pool));
2078               svn_pool_destroy(pool);
2079               return EXIT_FAILURE;
2080             }
2081         }
2082
2083       if(opt_err)
2084         return svn_cmdline_handle_exit_error(opt_err, pool, "svnsync: ");
2085     }
2086
2087   if (opt_baton.help)
2088     subcommand = svn_opt_get_canonical_subcommand2(svnsync_cmd_table, "help");
2089
2090   /* The --non-interactive and --force-interactive options are mutually
2091    * exclusive. */
2092   if (opt_baton.non_interactive && force_interactive)
2093     {
2094       err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
2095                              _("--non-interactive and --force-interactive "
2096                                "are mutually exclusive"));
2097       return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2098     }
2099   else
2100     opt_baton.non_interactive = !svn_cmdline__be_interactive(
2101                                   opt_baton.non_interactive,
2102                                   force_interactive);
2103
2104   /* Disallow the mixing --username/password with their --source- and
2105      --sync- variants.  Treat "--username FOO" as "--source-username
2106      FOO --sync-username FOO"; ditto for "--password FOO". */
2107   if ((username || password)
2108       && (source_username || sync_username
2109           || source_password || sync_password))
2110     {
2111       err = svn_error_create
2112         (SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
2113          _("Cannot use --username or --password with any of "
2114            "--source-username, --source-password, --sync-username, "
2115            "or --sync-password.\n"));
2116       return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2117     }
2118   if (username)
2119     {
2120       source_username = username;
2121       sync_username = username;
2122     }
2123   if (password)
2124     {
2125       source_password = password;
2126       sync_password = password;
2127     }
2128   opt_baton.source_username = source_username;
2129   opt_baton.source_password = source_password;
2130   opt_baton.sync_username = sync_username;
2131   opt_baton.sync_password = sync_password;
2132
2133   /* Disallow mixing of --steal-lock and --disable-locking. */
2134   if (opt_baton.steal_lock && opt_baton.disable_locking)
2135     {
2136       err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
2137                              _("--disable-locking and --steal-lock are "
2138                                "mutually exclusive"));
2139       return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2140     }
2141
2142   /* --trust-server-cert can only be used with --non-interactive */
2143   if (opt_baton.trust_server_cert && !opt_baton.non_interactive)
2144     {
2145       err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
2146                              _("--trust-server-cert requires "
2147                                "--non-interactive"));
2148       return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2149     }
2150
2151   err = svn_config_ensure(opt_baton.config_dir, pool);
2152   if (err)
2153     return svn_cmdline_handle_exit_error(err, pool, "synsync: ");
2154
2155   if (subcommand == NULL)
2156     {
2157       if (os->ind >= os->argc)
2158         {
2159           if (opt_baton.version)
2160             {
2161               /* Use the "help" subcommand to handle "--version". */
2162               static const svn_opt_subcommand_desc2_t pseudo_cmd =
2163                 { "--version", help_cmd, {0}, "",
2164                   {svnsync_opt_version,  /* must accept its own option */
2165                    'q',  /* --quiet */
2166                   } };
2167
2168               subcommand = &pseudo_cmd;
2169             }
2170           else
2171             {
2172               SVN_INT_ERR(help_cmd(NULL, NULL, pool));
2173               svn_pool_destroy(pool);
2174               return EXIT_FAILURE;
2175             }
2176         }
2177       else
2178         {
2179           const char *first_arg = os->argv[os->ind++];
2180           subcommand = svn_opt_get_canonical_subcommand2(svnsync_cmd_table,
2181                                                          first_arg);
2182           if (subcommand == NULL)
2183             {
2184               SVN_INT_ERR(help_cmd(NULL, NULL, pool));
2185               svn_pool_destroy(pool);
2186               return EXIT_FAILURE;
2187             }
2188         }
2189     }
2190
2191   for (i = 0; i < received_opts->nelts; ++i)
2192     {
2193       opt_id = APR_ARRAY_IDX(received_opts, i, int);
2194
2195       if (opt_id == 'h' || opt_id == '?')
2196         continue;
2197
2198       if (! svn_opt_subcommand_takes_option3(subcommand, opt_id, NULL))
2199         {
2200           const char *optstr;
2201           const apr_getopt_option_t *badopt =
2202             svn_opt_get_option_from_code2(opt_id, svnsync_options, subcommand,
2203                                           pool);
2204           svn_opt_format_option(&optstr, badopt, FALSE, pool);
2205           if (subcommand->name[0] == '-')
2206             {
2207               SVN_INT_ERR(help_cmd(NULL, NULL, pool));
2208             }
2209           else
2210             {
2211               err = svn_error_createf
2212                 (SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
2213                  _("Subcommand '%s' doesn't accept option '%s'\n"
2214                    "Type 'svnsync help %s' for usage.\n"),
2215                  subcommand->name, optstr, subcommand->name);
2216               return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2217             }
2218         }
2219     }
2220
2221   err = svn_config_get_config(&opt_baton.config, opt_baton.config_dir, pool);
2222   if (err)
2223     return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2224
2225   /* Update the options in the config */
2226   if (config_options)
2227     {
2228       svn_error_clear(
2229           svn_cmdline__apply_config_options(opt_baton.config, config_options,
2230                                             "svnsync: ", "--config-option"));
2231     }
2232
2233   config = svn_hash_gets(opt_baton.config, SVN_CONFIG_CATEGORY_CONFIG);
2234
2235   opt_baton.source_prop_encoding = source_prop_encoding;
2236
2237   apr_signal(SIGINT, signal_handler);
2238
2239 #ifdef SIGBREAK
2240   /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */
2241   apr_signal(SIGBREAK, signal_handler);
2242 #endif
2243
2244 #ifdef SIGHUP
2245   apr_signal(SIGHUP, signal_handler);
2246 #endif
2247
2248 #ifdef SIGTERM
2249   apr_signal(SIGTERM, signal_handler);
2250 #endif
2251
2252 #ifdef SIGPIPE
2253   /* Disable SIGPIPE generation for the platforms that have it. */
2254   apr_signal(SIGPIPE, SIG_IGN);
2255 #endif
2256
2257 #ifdef SIGXFSZ
2258   /* Disable SIGXFSZ generation for the platforms that have it,
2259      otherwise working with large files when compiled against an APR
2260      that doesn't have large file support will crash the program,
2261      which is uncool. */
2262   apr_signal(SIGXFSZ, SIG_IGN);
2263 #endif
2264
2265   err = svn_cmdline_create_auth_baton(&opt_baton.source_auth_baton,
2266                                       opt_baton.non_interactive,
2267                                       opt_baton.source_username,
2268                                       opt_baton.source_password,
2269                                       opt_baton.config_dir,
2270                                       opt_baton.no_auth_cache,
2271                                       opt_baton.trust_server_cert,
2272                                       config,
2273                                       check_cancel, NULL,
2274                                       pool);
2275   if (! err)
2276     err = svn_cmdline_create_auth_baton(&opt_baton.sync_auth_baton,
2277                                         opt_baton.non_interactive,
2278                                         opt_baton.sync_username,
2279                                         opt_baton.sync_password,
2280                                         opt_baton.config_dir,
2281                                         opt_baton.no_auth_cache,
2282                                         opt_baton.trust_server_cert,
2283                                         config,
2284                                         check_cancel, NULL,
2285                                         pool);
2286   if (! err)
2287     err = (*subcommand->cmd_func)(os, &opt_baton, pool);
2288   if (err)
2289     {
2290       /* For argument-related problems, suggest using the 'help'
2291          subcommand. */
2292       if (err->apr_err == SVN_ERR_CL_INSUFFICIENT_ARGS
2293           || err->apr_err == SVN_ERR_CL_ARG_PARSING_ERROR)
2294         {
2295           err = svn_error_quick_wrap(err,
2296                                      _("Try 'svnsync help' for more info"));
2297         }
2298
2299       return svn_cmdline_handle_exit_error(err, pool, "svnsync: ");
2300     }
2301
2302   svn_pool_destroy(pool);
2303
2304   return EXIT_SUCCESS;
2305 }