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