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