1/*
2 *  svnrdump.c: Produce a dumpfile of a local or remote repository
3 *              without touching the filesystem, but for temporary files.
4 *
5 * ====================================================================
6 *    Licensed to the Apache Software Foundation (ASF) under one
7 *    or more contributor license agreements.  See the NOTICE file
8 *    distributed with this work for additional information
9 *    regarding copyright ownership.  The ASF licenses this file
10 *    to you under the Apache License, Version 2.0 (the
11 *    "License"); you may not use this file except in compliance
12 *    with the License.  You may obtain a copy of the License at
13 *
14 *      http://www.apache.org/licenses/LICENSE-2.0
15 *
16 *    Unless required by applicable law or agreed to in writing,
17 *    software distributed under the License is distributed on an
18 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
19 *    KIND, either express or implied.  See the License for the
20 *    specific language governing permissions and limitations
21 *    under the License.
22 * ====================================================================
23 */
24
25#include <apr_signal.h>
26#include <apr_uri.h>
27
28#include "svn_pools.h"
29#include "svn_cmdline.h"
30#include "svn_client.h"
31#include "svn_hash.h"
32#include "svn_ra.h"
33#include "svn_repos.h"
34#include "svn_path.h"
35#include "svn_utf.h"
36#include "svn_private_config.h"
37#include "svn_string.h"
38#include "svn_props.h"
39
40#include "svnrdump.h"
41
42#include "private/svn_repos_private.h"
43#include "private/svn_cmdline_private.h"
44#include "private/svn_ra_private.h"
45
46
47
48/*** Cancellation ***/
49
50/* A flag to see if we've been cancelled by the client or not. */
51static volatile sig_atomic_t cancelled = FALSE;
52
53/* A signal handler to support cancellation. */
54static void
55signal_handler(int signum)
56{
57  apr_signal(signum, SIG_IGN);
58  cancelled = TRUE;
59}
60
61/* Our cancellation callback. */
62static svn_error_t *
63check_cancel(void *baton)
64{
65  if (cancelled)
66    return svn_error_create(SVN_ERR_CANCELLED, NULL, _("Caught signal"));
67  else
68    return SVN_NO_ERROR;
69}
70
71
72
73
74static svn_opt_subcommand_t dump_cmd, load_cmd;
75
76enum svn_svnrdump__longopt_t
77  {
78    opt_config_dir = SVN_OPT_FIRST_LONGOPT_ID,
79    opt_config_option,
80    opt_auth_username,
81    opt_auth_password,
82    opt_auth_nocache,
83    opt_non_interactive,
84    opt_skip_revprop,
85    opt_force_interactive,
86    opt_incremental,
87    opt_trust_server_cert,
88    opt_trust_server_cert_failures,
89    opt_version
90  };
91
92#define SVN_SVNRDUMP__BASE_OPTIONS opt_config_dir, \
93                                   opt_config_option, \
94                                   opt_auth_username, \
95                                   opt_auth_password, \
96                                   opt_auth_nocache, \
97                                   opt_trust_server_cert, \
98                                   opt_trust_server_cert_failures, \
99                                   opt_non_interactive, \
100                                   opt_force_interactive
101
102static const svn_opt_subcommand_desc2_t svnrdump__cmd_table[] =
103{
104  { "dump", dump_cmd, { 0 },
105    N_("usage: svnrdump dump URL [-r LOWER[:UPPER]]\n\n"
106       "Dump revisions LOWER to UPPER of repository at remote URL to stdout\n"
107       "in a 'dumpfile' portable format.  If only LOWER is given, dump that\n"
108       "one revision.\n"),
109    { 'r', 'q', opt_incremental, SVN_SVNRDUMP__BASE_OPTIONS } },
110  { "load", load_cmd, { 0 },
111    N_("usage: svnrdump load URL\n\n"
112       "Load a 'dumpfile' given on stdin to a repository at remote URL.\n"),
113    { 'q', opt_skip_revprop, SVN_SVNRDUMP__BASE_OPTIONS } },
114  { "help", 0, { "?", "h" },
115    N_("usage: svnrdump help [SUBCOMMAND...]\n\n"
116       "Describe the usage of this program or its subcommands.\n"),
117    { 0 } },
118  { NULL, NULL, { 0 }, NULL, { 0 } }
119};
120
121static const apr_getopt_option_t svnrdump__options[] =
122  {
123    {"revision",     'r', 1,
124                      N_("specify revision number ARG (or X:Y range)")},
125    {"quiet",         'q', 0,
126                      N_("no progress (only errors) to stderr")},
127    {"incremental",   opt_incremental, 0,
128                      N_("dump incrementally")},
129    {"skip-revprop",  opt_skip_revprop, 1,
130                      N_("skip revision property ARG (e.g., \"svn:author\")")},
131    {"config-dir",    opt_config_dir, 1,
132                      N_("read user configuration files from directory ARG")},
133    {"username",      opt_auth_username, 1,
134                      N_("specify a username ARG")},
135    {"password",      opt_auth_password, 1,
136                      N_("specify a password ARG")},
137    {"non-interactive", opt_non_interactive, 0,
138                      N_("do no interactive prompting (default is to prompt\n"
139                         "                             "
140                         "only if standard input is a terminal device)")},
141    {"force-interactive", opt_force_interactive, 0,
142                      N_("do interactive prompting even if standard input\n"
143                         "                             "
144                         "is not a terminal device")},
145    {"no-auth-cache", opt_auth_nocache, 0,
146                      N_("do not cache authentication tokens")},
147    {"help",          'h', 0,
148                      N_("display this help")},
149    {"version",       opt_version, 0,
150                      N_("show program version information")},
151    {"config-option", opt_config_option, 1,
152                      N_("set user configuration option in the format:\n"
153                         "                             "
154                         "    FILE:SECTION:OPTION=[VALUE]\n"
155                         "                             "
156                         "For example:\n"
157                         "                             "
158                         "    servers:global:http-library=serf")},
159  {"trust-server-cert", opt_trust_server_cert, 0,
160                    N_("deprecated; same as\n"
161                       "                             "
162                       "--trust-server-cert-failures=unknown-ca")},
163  {"trust-server-cert-failures", opt_trust_server_cert_failures, 1,
164                    N_("with --non-interactive, accept SSL server\n"
165                       "                             "
166                       "certificates with failures; ARG is comma-separated\n"
167                       "                             "
168                       "list of 'unknown-ca' (Unknown Authority),\n"
169                       "                             "
170                       "'cn-mismatch' (Hostname mismatch), 'expired'\n"
171                       "                             "
172                       "(Expired certificate), 'not-yet-valid' (Not yet\n"
173                       "                             "
174                       "valid certificate) and 'other' (all other not\n"
175                       "                             "
176                       "separately classified certificate errors).")},
177    {0, 0, 0, 0}
178  };
179
180/* Baton for the RA replay session. */
181struct replay_baton {
182  /* A backdoor ra session for fetching information. */
183  svn_ra_session_t *extra_ra_session;
184
185  /* The output stream */
186  svn_stream_t *stdout_stream;
187
188  /* Whether to be quiet. */
189  svn_boolean_t quiet;
190};
191
192/* Option set */
193typedef struct opt_baton_t {
194  svn_client_ctx_t *ctx;
195  svn_ra_session_t *session;
196  const char *url;
197  svn_boolean_t help;
198  svn_boolean_t version;
199  svn_opt_revision_t start_revision;
200  svn_opt_revision_t end_revision;
201  svn_boolean_t quiet;
202  svn_boolean_t incremental;
203  apr_hash_t *skip_revprops;
204} opt_baton_t;
205
206/* Print dumpstream-formatted information about REVISION.
207 * Implements the `svn_ra_replay_revstart_callback_t' interface.
208 */
209static svn_error_t *
210replay_revstart(svn_revnum_t revision,
211                void *replay_baton,
212                const svn_delta_editor_t **editor,
213                void **edit_baton,
214                apr_hash_t *rev_props,
215                apr_pool_t *pool)
216{
217  struct replay_baton *rb = replay_baton;
218  apr_hash_t *normal_props;
219
220  /* Normalize and dump the revprops */
221  SVN_ERR(svn_rdump__normalize_props(&normal_props, rev_props, pool));
222  SVN_ERR(svn_repos__dump_revision_record(rb->stdout_stream, revision, NULL,
223                                          normal_props,
224                                          TRUE /*props_section_always*/,
225                                          pool));
226
227  SVN_ERR(svn_rdump__get_dump_editor(editor, edit_baton, revision,
228                                     rb->stdout_stream, rb->extra_ra_session,
229                                     NULL, check_cancel, NULL, pool));
230
231  return SVN_NO_ERROR;
232}
233
234/* Print progress information about the dump of REVISION.
235   Implements the `svn_ra_replay_revfinish_callback_t' interface. */
236static svn_error_t *
237replay_revend(svn_revnum_t revision,
238              void *replay_baton,
239              const svn_delta_editor_t *editor,
240              void *edit_baton,
241              apr_hash_t *rev_props,
242              apr_pool_t *pool)
243{
244  /* No resources left to free. */
245  struct replay_baton *rb = replay_baton;
246
247  SVN_ERR(editor->close_edit(edit_baton, pool));
248
249  if (! rb->quiet)
250    SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n",
251                                revision));
252  return SVN_NO_ERROR;
253}
254
255#ifdef USE_EV2_IMPL
256/* Print dumpstream-formatted information about REVISION.
257 * Implements the `svn_ra_replay_revstart_callback_t' interface.
258 */
259static svn_error_t *
260replay_revstart_v2(svn_revnum_t revision,
261                   void *replay_baton,
262                   svn_editor_t **editor,
263                   apr_hash_t *rev_props,
264                   apr_pool_t *pool)
265{
266  struct replay_baton *rb = replay_baton;
267  apr_hash_t *normal_props;
268
269  /* Normalize and dump the revprops */
270  SVN_ERR(svn_rdump__normalize_props(&normal_props, rev_props, pool));
271  SVN_ERR(svn_repos__dump_revision_record(rb->stdout_stream, revision,
272                                          normal_props,
273                                          TRUE /*props_section_always*/,
274                                          pool));
275
276  SVN_ERR(svn_rdump__get_dump_editor_v2(editor, revision,
277                                        rb->stdout_stream,
278                                        rb->extra_ra_session,
279                                        NULL, check_cancel, NULL, pool, pool));
280
281  return SVN_NO_ERROR;
282}
283
284/* Print progress information about the dump of REVISION.
285   Implements the `svn_ra_replay_revfinish_callback_t' interface. */
286static svn_error_t *
287replay_revend_v2(svn_revnum_t revision,
288                 void *replay_baton,
289                 svn_editor_t *editor,
290                 apr_hash_t *rev_props,
291                 apr_pool_t *pool)
292{
293  /* No resources left to free. */
294  struct replay_baton *rb = replay_baton;
295
296  SVN_ERR(svn_editor_complete(editor));
297
298  if (! rb->quiet)
299    SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n",
300                                revision));
301  return SVN_NO_ERROR;
302}
303#endif
304
305/* Initialize the RA layer, and set *CTX to a new client context baton
306 * allocated from POOL.  Use CONFIG_DIR and pass USERNAME, PASSWORD,
307 * CONFIG_DIR and NO_AUTH_CACHE to initialize the authorization baton.
308 * CONFIG_OPTIONS (if not NULL) is a list of configuration overrides.
309 * REPOS_URL is used to fiddle with server-specific configuration
310 * options.
311 */
312static svn_error_t *
313init_client_context(svn_client_ctx_t **ctx_p,
314                    svn_boolean_t non_interactive,
315                    const char *username,
316                    const char *password,
317                    const char *config_dir,
318                    const char *repos_url,
319                    svn_boolean_t no_auth_cache,
320                    svn_boolean_t trust_unknown_ca,
321                    svn_boolean_t trust_cn_mismatch,
322                    svn_boolean_t trust_expired,
323                    svn_boolean_t trust_not_yet_valid,
324                    svn_boolean_t trust_other_failure,
325                    apr_array_header_t *config_options,
326                    apr_pool_t *pool)
327{
328  svn_client_ctx_t *ctx = NULL;
329  svn_config_t *cfg_config, *cfg_servers;
330
331  SVN_ERR(svn_ra_initialize(pool));
332
333  SVN_ERR(svn_config_ensure(config_dir, pool));
334  SVN_ERR(svn_client_create_context2(&ctx, NULL, pool));
335
336  SVN_ERR(svn_config_get_config(&(ctx->config), config_dir, pool));
337
338  if (config_options)
339    SVN_ERR(svn_cmdline__apply_config_options(ctx->config, config_options,
340                                              "svnrdump: ", "--config-option"));
341
342  cfg_config = svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG);
343
344  /* ### FIXME: This is a hack to work around the fact that our dump
345     ### editor simply can't handle the way ra_serf violates the
346     ### editor v1 drive ordering requirements.
347     ###
348     ### We'll override both the global value and server-specific one
349     ### for the 'http-bulk-updates' and 'http-max-connections'
350     ### options in order to get ra_serf to try a bulk-update if the
351     ### server will allow it, or at least try to limit all its
352     ### auxiliary GETs/PROPFINDs to happening (well-ordered) on a
353     ### single server connection.
354     ###
355     ### See http://subversion.tigris.org/issues/show_bug.cgi?id=4116.
356  */
357  cfg_servers = svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_SERVERS);
358  svn_config_set_bool(cfg_servers, SVN_CONFIG_SECTION_GLOBAL,
359                      SVN_CONFIG_OPTION_HTTP_BULK_UPDATES, TRUE);
360  svn_config_set_int64(cfg_servers, SVN_CONFIG_SECTION_GLOBAL,
361                       SVN_CONFIG_OPTION_HTTP_MAX_CONNECTIONS, 2);
362  if (cfg_servers)
363    {
364      apr_status_t status;
365      apr_uri_t parsed_url;
366
367      status = apr_uri_parse(pool, repos_url, &parsed_url);
368      if (! status)
369        {
370          const char *server_group;
371
372          server_group = svn_config_find_group(cfg_servers, parsed_url.hostname,
373                                               SVN_CONFIG_SECTION_GROUPS, pool);
374          if (server_group)
375            {
376              svn_config_set_bool(cfg_servers, server_group,
377                                  SVN_CONFIG_OPTION_HTTP_BULK_UPDATES, TRUE);
378              svn_config_set_int64(cfg_servers, server_group,
379                                   SVN_CONFIG_OPTION_HTTP_MAX_CONNECTIONS, 2);
380            }
381        }
382    }
383
384  /* Set up our cancellation support. */
385  ctx->cancel_func = check_cancel;
386
387  /* Default authentication providers for non-interactive use */
388  SVN_ERR(svn_cmdline_create_auth_baton2(&(ctx->auth_baton), non_interactive,
389                                         username, password, config_dir,
390                                         no_auth_cache, trust_unknown_ca,
391                                         trust_cn_mismatch, trust_expired,
392                                         trust_not_yet_valid,
393                                         trust_other_failure,
394                                         cfg_config, ctx->cancel_func,
395                                         ctx->cancel_baton, pool));
396  *ctx_p = ctx;
397  return SVN_NO_ERROR;
398}
399
400/* Print a revision record header for REVISION to STDOUT_STREAM.  Use
401 * SESSION to contact the repository for revision properties and
402 * such.
403 */
404static svn_error_t *
405dump_revision_header(svn_ra_session_t *session,
406                     svn_stream_t *stdout_stream,
407                     svn_revnum_t revision,
408                     apr_pool_t *pool)
409{
410  apr_hash_t *prophash;
411
412  SVN_ERR(svn_ra_rev_proplist(session, revision, &prophash, pool));
413  SVN_ERR(svn_repos__dump_revision_record(stdout_stream, revision, NULL,
414                                          prophash,
415                                          TRUE /*props_section_always*/,
416                                          pool));
417  return SVN_NO_ERROR;
418}
419
420static svn_error_t *
421dump_initial_full_revision(svn_ra_session_t *session,
422                           svn_ra_session_t *extra_ra_session,
423                           svn_stream_t *stdout_stream,
424                           svn_revnum_t revision,
425                           svn_boolean_t quiet,
426                           apr_pool_t *pool)
427{
428  const svn_ra_reporter3_t *reporter;
429  void *report_baton;
430  const svn_delta_editor_t *dump_editor;
431  void *dump_baton;
432  const char *session_url, *source_relpath;
433
434  /* Determine whether we're dumping the repository root URL or some
435     child thereof.  If we're dumping a subtree of the repository
436     rather than the root, we have to jump through some hoops to make
437     our update-driven dump generation work the way a replay-driven
438     one would.
439
440     See http://subversion.tigris.org/issues/show_bug.cgi?id=4101
441  */
442  SVN_ERR(svn_ra_get_session_url(session, &session_url, pool));
443  SVN_ERR(svn_ra_get_path_relative_to_root(session, &source_relpath,
444                                           session_url, pool));
445
446  /* Start with a revision record header. */
447  SVN_ERR(dump_revision_header(session, stdout_stream, revision, pool));
448
449  /* Then, we'll drive the dump editor with what would look like a
450     full checkout of the repository as it looked in START_REVISION.
451     We do this by manufacturing a basic 'report' to the update
452     reporter, telling it that we have nothing to start with.  The
453     delta between nothing and everything-at-REV is, effectively, a
454     full dump of REV. */
455  SVN_ERR(svn_rdump__get_dump_editor(&dump_editor, &dump_baton, revision,
456                                     stdout_stream, extra_ra_session,
457                                     source_relpath, check_cancel, NULL, pool));
458  SVN_ERR(svn_ra_do_update3(session, &reporter, &report_baton, revision,
459                            "", svn_depth_infinity, FALSE, FALSE,
460                            dump_editor, dump_baton, pool, pool));
461  SVN_ERR(reporter->set_path(report_baton, "", revision,
462                             svn_depth_infinity, TRUE, NULL, pool));
463  SVN_ERR(reporter->finish_report(report_baton, pool));
464
465  /* All finished with START_REVISION! */
466  if (! quiet)
467    SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n",
468                                revision));
469
470  return SVN_NO_ERROR;
471}
472
473/* Replay revisions START_REVISION thru END_REVISION (inclusive) of
474 * the repository URL at which SESSION is rooted, using callbacks
475 * which generate Subversion repository dumpstreams describing the
476 * changes made in those revisions.  If QUIET is set, don't generate
477 * progress messages.
478 */
479static svn_error_t *
480replay_revisions(svn_ra_session_t *session,
481                 svn_ra_session_t *extra_ra_session,
482                 svn_revnum_t start_revision,
483                 svn_revnum_t end_revision,
484                 svn_boolean_t quiet,
485                 svn_boolean_t incremental,
486                 apr_pool_t *pool)
487{
488  struct replay_baton *replay_baton;
489  const char *uuid;
490  svn_stream_t *stdout_stream;
491
492  SVN_ERR(svn_stream_for_stdout(&stdout_stream, pool));
493
494  replay_baton = apr_pcalloc(pool, sizeof(*replay_baton));
495  replay_baton->stdout_stream = stdout_stream;
496  replay_baton->extra_ra_session = extra_ra_session;
497  replay_baton->quiet = quiet;
498
499  /* Write the magic header and UUID */
500  SVN_ERR(svn_stream_printf(stdout_stream, pool,
501                            SVN_REPOS_DUMPFILE_MAGIC_HEADER ": %d\n\n",
502                            SVN_REPOS_DUMPFILE_FORMAT_VERSION));
503  SVN_ERR(svn_ra_get_uuid2(session, &uuid, pool));
504  SVN_ERR(svn_stream_printf(stdout_stream, pool,
505                            SVN_REPOS_DUMPFILE_UUID ": %s\n\n", uuid));
506
507  /* Fake revision 0 if necessary */
508  if (start_revision == 0)
509    {
510      SVN_ERR(dump_revision_header(session, stdout_stream,
511                                   start_revision, pool));
512
513      /* Revision 0 has no tree changes, so we're done. */
514      if (! quiet)
515        SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n",
516                                    start_revision));
517      start_revision++;
518
519      /* If our first revision is 0, we can treat this as an
520         incremental dump. */
521      incremental = TRUE;
522    }
523
524  /* If what remains to be dumped is not going to be dumped
525     incrementally, then dump the first revision in full. */
526  if (!incremental)
527    {
528      SVN_ERR(dump_initial_full_revision(session, extra_ra_session,
529                                         stdout_stream, start_revision,
530                                         quiet, pool));
531      start_revision++;
532    }
533
534  /* If there are still revisions left to be dumped, do so. */
535  if (start_revision <= end_revision)
536    {
537#ifndef USE_EV2_IMPL
538      SVN_ERR(svn_ra_replay_range(session, start_revision, end_revision,
539                                  0, TRUE, replay_revstart, replay_revend,
540                                  replay_baton, pool));
541#else
542      SVN_ERR(svn_ra__replay_range_ev2(session, start_revision, end_revision,
543                                       0, TRUE, replay_revstart_v2,
544                                       replay_revend_v2, replay_baton,
545                                       NULL, NULL, NULL, NULL, pool));
546#endif
547    }
548
549  SVN_ERR(svn_stream_close(stdout_stream));
550  return SVN_NO_ERROR;
551}
552
553/* Read a dumpstream from stdin, and use it to feed a loader capable
554 * of transmitting that information to the repository located at URL
555 * (to which SESSION has been opened).  AUX_SESSION is a second RA
556 * session opened to the same URL for performing auxiliary out-of-band
557 * operations.
558 */
559static svn_error_t *
560load_revisions(svn_ra_session_t *session,
561               svn_ra_session_t *aux_session,
562               const char *url,
563               svn_boolean_t quiet,
564               apr_hash_t *skip_revprops,
565               apr_pool_t *pool)
566{
567  apr_file_t *stdin_file;
568  svn_stream_t *stdin_stream;
569
570  apr_file_open_stdin(&stdin_file, pool);
571  stdin_stream = svn_stream_from_aprfile2(stdin_file, FALSE, pool);
572
573  SVN_ERR(svn_rdump__load_dumpstream(stdin_stream, session, aux_session,
574                                     quiet, skip_revprops,
575                                     check_cancel, NULL, pool));
576
577  SVN_ERR(svn_stream_close(stdin_stream));
578
579  return SVN_NO_ERROR;
580}
581
582/* Return a program name for this program, the basename of the path
583 * represented by PROGNAME if not NULL; use "svnrdump" otherwise.
584 */
585static const char *
586ensure_appname(const char *progname,
587               apr_pool_t *pool)
588{
589  if (!progname)
590    return "svnrdump";
591
592  return svn_dirent_basename(svn_dirent_internal_style(progname, pool), NULL);
593}
594
595/* Print a simple usage string. */
596static svn_error_t *
597usage(const char *progname,
598      apr_pool_t *pool)
599{
600  return svn_cmdline_fprintf(stderr, pool,
601                             _("Type '%s help' for usage.\n"),
602                             ensure_appname(progname, pool));
603}
604
605/* Print information about the version of this program and dependent
606 * modules.
607 */
608static svn_error_t *
609version(const char *progname,
610        svn_boolean_t quiet,
611        apr_pool_t *pool)
612{
613  svn_stringbuf_t *version_footer =
614    svn_stringbuf_create(_("The following repository access (RA) modules "
615                           "are available:\n\n"),
616                         pool);
617
618  SVN_ERR(svn_ra_print_modules(version_footer, pool));
619  return svn_opt_print_help4(NULL, ensure_appname(progname, pool),
620                             TRUE, quiet, FALSE, version_footer->data,
621                             NULL, NULL, NULL, NULL, NULL, pool);
622}
623
624
625/* Handle the "dump" subcommand.  Implements `svn_opt_subcommand_t'.  */
626static svn_error_t *
627dump_cmd(apr_getopt_t *os,
628         void *baton,
629         apr_pool_t *pool)
630{
631  opt_baton_t *opt_baton = baton;
632  svn_ra_session_t *extra_ra_session;
633  const char *repos_root;
634
635  SVN_ERR(svn_client_open_ra_session2(&extra_ra_session,
636                                      opt_baton->url, NULL,
637                                      opt_baton->ctx, pool, pool));
638  SVN_ERR(svn_ra_get_repos_root2(extra_ra_session, &repos_root, pool));
639  SVN_ERR(svn_ra_reparent(extra_ra_session, repos_root, pool));
640
641  return replay_revisions(opt_baton->session, extra_ra_session,
642                          opt_baton->start_revision.value.number,
643                          opt_baton->end_revision.value.number,
644                          opt_baton->quiet, opt_baton->incremental, pool);
645}
646
647/* Handle the "load" subcommand.  Implements `svn_opt_subcommand_t'.  */
648static svn_error_t *
649load_cmd(apr_getopt_t *os,
650         void *baton,
651         apr_pool_t *pool)
652{
653  opt_baton_t *opt_baton = baton;
654  svn_ra_session_t *aux_session;
655
656  SVN_ERR(svn_client_open_ra_session2(&aux_session, opt_baton->url, NULL,
657                                      opt_baton->ctx, pool, pool));
658  return load_revisions(opt_baton->session, aux_session, opt_baton->url,
659                        opt_baton->quiet, opt_baton->skip_revprops, pool);
660}
661
662/* Handle the "help" subcommand.  Implements `svn_opt_subcommand_t'.  */
663static svn_error_t *
664help_cmd(apr_getopt_t *os,
665         void *baton,
666         apr_pool_t *pool)
667{
668  const char *header =
669    _("general usage: svnrdump SUBCOMMAND URL [-r LOWER[:UPPER]]\n"
670      "Subversion remote repository dump and load tool.\n"
671      "Type 'svnrdump help <subcommand>' for help on a specific subcommand.\n"
672      "Type 'svnrdump --version' to see the program version and RA modules.\n"
673      "\n"
674      "Available subcommands:\n");
675
676  return svn_opt_print_help4(os, "svnrdump", FALSE, FALSE, FALSE, NULL,
677                             header, svnrdump__cmd_table, svnrdump__options,
678                             NULL, NULL, pool);
679}
680
681/* Examine the OPT_BATON's 'start_revision' and 'end_revision'
682 * members, making sure that they make sense (in general, and as
683 * applied to a repository whose current youngest revision is
684 * LATEST_REVISION).
685 */
686static svn_error_t *
687validate_and_resolve_revisions(opt_baton_t *opt_baton,
688                               svn_revnum_t latest_revision,
689                               apr_pool_t *pool)
690{
691  svn_revnum_t provided_start_rev = SVN_INVALID_REVNUM;
692
693  /* Ensure that the start revision is something we can handle.  We
694     want a number >= 0.  If unspecified, make it a number (r0) --
695     anything else is bogus.  */
696  if (opt_baton->start_revision.kind == svn_opt_revision_number)
697    {
698      provided_start_rev = opt_baton->start_revision.value.number;
699    }
700  else if (opt_baton->start_revision.kind == svn_opt_revision_head)
701    {
702      opt_baton->start_revision.kind = svn_opt_revision_number;
703      opt_baton->start_revision.value.number = latest_revision;
704    }
705  else if (opt_baton->start_revision.kind == svn_opt_revision_unspecified)
706    {
707      opt_baton->start_revision.kind = svn_opt_revision_number;
708      opt_baton->start_revision.value.number = 0;
709    }
710
711  if (opt_baton->start_revision.kind != svn_opt_revision_number)
712    {
713      return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
714                              _("Unsupported revision specifier used; use "
715                                "only integer values or 'HEAD'"));
716    }
717
718  if ((opt_baton->start_revision.value.number < 0) ||
719      (opt_baton->start_revision.value.number > latest_revision))
720    {
721      return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
722                               _("Revision '%ld' does not exist"),
723                               opt_baton->start_revision.value.number);
724    }
725
726  /* Ensure that the end revision is something we can handle.  We want
727     a number <= the youngest, and > the start revision.  If
728     unspecified, make it a number (start_revision + 1 if that was
729     specified, the youngest revision in the repository otherwise) --
730     anything else is bogus.  */
731  if (opt_baton->end_revision.kind == svn_opt_revision_unspecified)
732    {
733      opt_baton->end_revision.kind = svn_opt_revision_number;
734      if (SVN_IS_VALID_REVNUM(provided_start_rev))
735        opt_baton->end_revision.value.number = provided_start_rev;
736      else
737        opt_baton->end_revision.value.number = latest_revision;
738    }
739  else if (opt_baton->end_revision.kind == svn_opt_revision_head)
740    {
741      opt_baton->end_revision.kind = svn_opt_revision_number;
742      opt_baton->end_revision.value.number = latest_revision;
743    }
744
745  if (opt_baton->end_revision.kind != svn_opt_revision_number)
746    {
747      return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
748                              _("Unsupported revision specifier used; use "
749                                "only integer values or 'HEAD'"));
750    }
751
752  if ((opt_baton->end_revision.value.number < 0) ||
753      (opt_baton->end_revision.value.number > latest_revision))
754    {
755      return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
756                               _("Revision '%ld' does not exist"),
757                               opt_baton->end_revision.value.number);
758    }
759
760  /* Finally, make sure that the end revision is younger than the
761     start revision.  We don't do "backwards" 'round here.  */
762  if (opt_baton->end_revision.value.number <
763      opt_baton->start_revision.value.number)
764    {
765      return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
766                              _("LOWER revision cannot be greater than "
767                                "UPPER revision; consider reversing your "
768                                "revision range"));
769    }
770  return SVN_NO_ERROR;
771}
772
773/*
774 * On success, leave *EXIT_CODE untouched and return SVN_NO_ERROR. On error,
775 * either return an error to be displayed, or set *EXIT_CODE to non-zero and
776 * return SVN_NO_ERROR.
777 */
778static svn_error_t *
779sub_main(int *exit_code, int argc, const char *argv[], apr_pool_t *pool)
780{
781  svn_error_t *err = SVN_NO_ERROR;
782  const svn_opt_subcommand_desc2_t *subcommand = NULL;
783  opt_baton_t *opt_baton;
784  svn_revnum_t latest_revision = SVN_INVALID_REVNUM;
785  const char *config_dir = NULL;
786  const char *username = NULL;
787  const char *password = NULL;
788  svn_boolean_t no_auth_cache = FALSE;
789  svn_boolean_t trust_unknown_ca = FALSE;
790  svn_boolean_t trust_cn_mismatch = FALSE;
791  svn_boolean_t trust_expired = FALSE;
792  svn_boolean_t trust_not_yet_valid = FALSE;
793  svn_boolean_t trust_other_failure = FALSE;
794  svn_boolean_t non_interactive = FALSE;
795  svn_boolean_t force_interactive = FALSE;
796  apr_array_header_t *config_options = NULL;
797  apr_getopt_t *os;
798  const char *first_arg;
799  apr_array_header_t *received_opts;
800  int i;
801
802  opt_baton = apr_pcalloc(pool, sizeof(*opt_baton));
803  opt_baton->start_revision.kind = svn_opt_revision_unspecified;
804  opt_baton->end_revision.kind = svn_opt_revision_unspecified;
805  opt_baton->url = NULL;
806  opt_baton->skip_revprops = apr_hash_make(pool);
807
808  SVN_ERR(svn_cmdline__getopt_init(&os, argc, argv, pool));
809
810  os->interleave = TRUE; /* Options and arguments can be interleaved */
811
812  /* Set up our cancellation support. */
813  apr_signal(SIGINT, signal_handler);
814#ifdef SIGBREAK
815  /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */
816  apr_signal(SIGBREAK, signal_handler);
817#endif
818#ifdef SIGHUP
819  apr_signal(SIGHUP, signal_handler);
820#endif
821#ifdef SIGTERM
822  apr_signal(SIGTERM, signal_handler);
823#endif
824#ifdef SIGPIPE
825  /* Disable SIGPIPE generation for the platforms that have it. */
826  apr_signal(SIGPIPE, SIG_IGN);
827#endif
828#ifdef SIGXFSZ
829  /* Disable SIGXFSZ generation for the platforms that have it, otherwise
830   * working with large files when compiled against an APR that doesn't have
831   * large file support will crash the program, which is uncool. */
832  apr_signal(SIGXFSZ, SIG_IGN);
833#endif
834
835  received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int));
836
837  while (1)
838    {
839      int opt;
840      const char *opt_arg;
841      apr_status_t status = apr_getopt_long(os, svnrdump__options, &opt,
842                                            &opt_arg);
843
844      if (APR_STATUS_IS_EOF(status))
845        break;
846      if (status != APR_SUCCESS)
847        {
848          SVN_ERR(usage(argv[0], pool));
849          *exit_code = EXIT_FAILURE;
850          return SVN_NO_ERROR;
851        }
852
853      /* Stash the option code in an array before parsing it. */
854      APR_ARRAY_PUSH(received_opts, int) = opt;
855
856      switch(opt)
857        {
858        case 'r':
859          {
860            /* Make sure we've not seen -r already. */
861            if (opt_baton->start_revision.kind != svn_opt_revision_unspecified)
862              {
863                return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
864                                        _("Multiple revision arguments "
865                                          "encountered; try '-r N:M' instead "
866                                          "of '-r N -r M'"));
867              }
868            /* Parse the -r argument. */
869            if (svn_opt_parse_revision(&(opt_baton->start_revision),
870                                       &(opt_baton->end_revision),
871                                       opt_arg, pool) != 0)
872              {
873                const char *utf8_opt_arg;
874                SVN_ERR(svn_utf_cstring_to_utf8(&utf8_opt_arg, opt_arg, pool));
875                return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
876                                         _("Syntax error in revision "
877                                           "argument '%s'"), utf8_opt_arg);
878              }
879          }
880          break;
881        case 'q':
882          opt_baton->quiet = TRUE;
883          break;
884        case opt_config_dir:
885          config_dir = opt_arg;
886          break;
887        case opt_version:
888          opt_baton->version = TRUE;
889          break;
890        case 'h':
891          opt_baton->help = TRUE;
892          break;
893        case opt_auth_username:
894          SVN_ERR(svn_utf_cstring_to_utf8(&username, opt_arg, pool));
895          break;
896        case opt_auth_password:
897          SVN_ERR(svn_utf_cstring_to_utf8(&password, opt_arg, pool));
898          break;
899        case opt_auth_nocache:
900          no_auth_cache = TRUE;
901          break;
902        case opt_non_interactive:
903          non_interactive = TRUE;
904          break;
905        case opt_force_interactive:
906          force_interactive = TRUE;
907          break;
908        case opt_incremental:
909          opt_baton->incremental = TRUE;
910          break;
911        case opt_skip_revprop:
912          SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, opt_arg, pool));
913          svn_hash_sets(opt_baton->skip_revprops, opt_arg, opt_arg);
914          break;
915        case opt_trust_server_cert: /* backward compat */
916          trust_unknown_ca = TRUE;
917          break;
918        case opt_trust_server_cert_failures:
919          SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, opt_arg, pool));
920          SVN_ERR(svn_cmdline__parse_trust_options(
921                      &trust_unknown_ca,
922                      &trust_cn_mismatch,
923                      &trust_expired,
924                      &trust_not_yet_valid,
925                      &trust_other_failure,
926                      opt_arg, pool));
927          break;
928        case opt_config_option:
929          if (!config_options)
930              config_options =
931                    apr_array_make(pool, 1,
932                                   sizeof(svn_cmdline__config_argument_t*));
933
934            SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, opt_arg, pool));
935            SVN_ERR(svn_cmdline__parse_config_option(config_options,
936                                                     opt_arg,
937                                                     "svnrdump: ",
938                                                     pool));
939        }
940    }
941
942  /* The --non-interactive and --force-interactive options are mutually
943   * exclusive. */
944  if (non_interactive && force_interactive)
945    {
946      return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
947                              _("--non-interactive and --force-interactive "
948                                "are mutually exclusive"));
949    }
950
951  if (opt_baton->help)
952    {
953      subcommand = svn_opt_get_canonical_subcommand2(svnrdump__cmd_table,
954                                                     "help");
955    }
956  if (subcommand == NULL)
957    {
958      if (os->ind >= os->argc)
959        {
960          if (opt_baton->version)
961            {
962              /* Use the "help" subcommand to handle the "--version" option. */
963              static const svn_opt_subcommand_desc2_t pseudo_cmd =
964                { "--version", help_cmd, {0}, "",
965                  {opt_version,  /* must accept its own option */
966                   'q',  /* --quiet */
967                  } };
968              subcommand = &pseudo_cmd;
969            }
970
971          else
972            {
973              SVN_ERR(help_cmd(NULL, NULL, pool));
974              *exit_code = EXIT_FAILURE;
975              return SVN_NO_ERROR;
976            }
977        }
978      else
979        {
980          first_arg = os->argv[os->ind++];
981          subcommand = svn_opt_get_canonical_subcommand2(svnrdump__cmd_table,
982                                                         first_arg);
983
984          if (subcommand == NULL)
985            {
986              const char *first_arg_utf8;
987              SVN_ERR(svn_utf_cstring_to_utf8(&first_arg_utf8, first_arg,
988                                              pool));
989              svn_error_clear(
990                svn_cmdline_fprintf(stderr, pool,
991                                    _("Unknown subcommand: '%s'\n"),
992                                    first_arg_utf8));
993              SVN_ERR(help_cmd(NULL, NULL, pool));
994              *exit_code = EXIT_FAILURE;
995              return SVN_NO_ERROR;
996            }
997        }
998    }
999
1000  /* Check that the subcommand wasn't passed any inappropriate options. */
1001  for (i = 0; i < received_opts->nelts; i++)
1002    {
1003      int opt_id = APR_ARRAY_IDX(received_opts, i, int);
1004
1005      /* All commands implicitly accept --help, so just skip over this
1006         when we see it. Note that we don't want to include this option
1007         in their "accepted options" list because it would be awfully
1008         redundant to display it in every commands' help text. */
1009      if (opt_id == 'h' || opt_id == '?')
1010        continue;
1011
1012      if (! svn_opt_subcommand_takes_option3(subcommand, opt_id, NULL))
1013        {
1014          const char *optstr;
1015          const apr_getopt_option_t *badopt =
1016            svn_opt_get_option_from_code2(opt_id, svnrdump__options,
1017                                          subcommand, pool);
1018          svn_opt_format_option(&optstr, badopt, FALSE, pool);
1019          if (subcommand->name[0] == '-')
1020            SVN_ERR(help_cmd(NULL, NULL, pool));
1021          else
1022            svn_error_clear(svn_cmdline_fprintf(
1023                                stderr, pool,
1024                                _("Subcommand '%s' doesn't accept option '%s'\n"
1025                                  "Type 'svnrdump help %s' for usage.\n"),
1026                                subcommand->name, optstr, subcommand->name));
1027          *exit_code = EXIT_FAILURE;
1028          return SVN_NO_ERROR;
1029        }
1030    }
1031
1032  if (strcmp(subcommand->name, "--version") == 0)
1033    {
1034      SVN_ERR(version(argv[0], opt_baton->quiet, pool));
1035      return SVN_NO_ERROR;
1036    }
1037
1038  if (strcmp(subcommand->name, "help") == 0)
1039    {
1040      SVN_ERR(help_cmd(os, opt_baton, pool));
1041      return SVN_NO_ERROR;
1042    }
1043
1044  /* --trust-* can only be used with --non-interactive */
1045  if (!non_interactive)
1046    {
1047      if (trust_unknown_ca || trust_cn_mismatch || trust_expired
1048          || trust_not_yet_valid || trust_other_failure)
1049        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1050                                _("--trust-server-cert-failures requires "
1051                                  "--non-interactive"));
1052    }
1053
1054  /* Expect one more non-option argument:  the repository URL. */
1055  if (os->ind != os->argc - 1)
1056    {
1057      SVN_ERR(usage(argv[0], pool));
1058      *exit_code = EXIT_FAILURE;
1059      return SVN_NO_ERROR;
1060    }
1061  else
1062    {
1063      const char *repos_url;
1064
1065      SVN_ERR(svn_utf_cstring_to_utf8(&repos_url, os->argv[os->ind], pool));
1066      if (! svn_path_is_url(repos_url))
1067        {
1068          return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, 0,
1069                                   "Target '%s' is not a URL",
1070                                   repos_url);
1071        }
1072      opt_baton->url = svn_uri_canonicalize(repos_url, pool);
1073    }
1074
1075  if (strcmp(subcommand->name, "load") == 0)
1076    {
1077      /*
1078       * By default (no --*-interactive options given), the 'load' subcommand
1079       * is interactive unless username and password were provided on the
1080       * command line. This allows prompting for auth creds to work without
1081       * requiring users to remember to use --force-interactive.
1082       * See issue #3913, "svnrdump load is not working in interactive mode".
1083       */
1084      if (!non_interactive && !force_interactive)
1085        force_interactive = (username == NULL || password == NULL);
1086    }
1087
1088  non_interactive = !svn_cmdline__be_interactive(non_interactive,
1089                                                 force_interactive);
1090
1091  SVN_ERR(init_client_context(&(opt_baton->ctx),
1092                              non_interactive,
1093                              username,
1094                              password,
1095                              config_dir,
1096                              opt_baton->url,
1097                              no_auth_cache,
1098                              trust_unknown_ca,
1099                              trust_cn_mismatch,
1100                              trust_expired,
1101                              trust_not_yet_valid,
1102                              trust_other_failure,
1103                              config_options,
1104                              pool));
1105
1106  err = svn_client_open_ra_session2(&(opt_baton->session),
1107                                    opt_baton->url, NULL,
1108                                    opt_baton->ctx, pool, pool);
1109
1110  /* Have sane opt_baton->start_revision and end_revision defaults if
1111     unspecified.  */
1112  if (!err)
1113    err = svn_ra_get_latest_revnum(opt_baton->session, &latest_revision, pool);
1114
1115  /* Make sure any provided revisions make sense. */
1116  if (!err)
1117    err = validate_and_resolve_revisions(opt_baton, latest_revision, pool);
1118
1119  /* Dispatch the subcommand */
1120  if (!err)
1121    err = (*subcommand->cmd_func)(os, opt_baton, pool);
1122
1123  if (err && err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive)
1124    {
1125      return svn_error_quick_wrap(err,
1126                                  _("Authentication failed and interactive"
1127                                    " prompting is disabled; see the"
1128                                    " --force-interactive option"));
1129    }
1130  else if (err)
1131    return err;
1132  else
1133    return SVN_NO_ERROR;
1134}
1135
1136int
1137main(int argc, const char *argv[])
1138{
1139  apr_pool_t *pool;
1140  int exit_code = EXIT_SUCCESS;
1141  svn_error_t *err;
1142
1143  /* Initialize the app. */
1144  if (svn_cmdline_init("svnrdump", stderr) != EXIT_SUCCESS)
1145    return EXIT_FAILURE;
1146
1147  /* Create our top-level pool.  Use a separate mutexless allocator,
1148   * given this application is single threaded.
1149   */
1150  pool = apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
1151
1152  err = sub_main(&exit_code, argc, argv, pool);
1153
1154  /* Flush stdout and report if it fails. It would be flushed on exit anyway
1155     but this makes sure that output is not silently lost if it fails. */
1156  err = svn_error_compose_create(err, svn_cmdline_fflush(stdout));
1157
1158  if (err)
1159    {
1160      exit_code = EXIT_FAILURE;
1161      svn_cmdline_handle_exit_error(err, NULL, "svnrdump: ");
1162    }
1163
1164  svn_pool_destroy(pool);
1165  return exit_code;
1166}
1167