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