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