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