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