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