1/*
2 * svnmucc.c: Subversion Multiple URL Client
3 *
4 * ====================================================================
5 *    Licensed to the Apache Software Foundation (ASF) under one
6 *    or more contributor license agreements.  See the NOTICE file
7 *    distributed with this work for additional information
8 *    regarding copyright ownership.  The ASF licenses this file
9 *    to you under the Apache License, Version 2.0 (the
10 *    "License"); you may not use this file except in compliance
11 *    with the License.  You may obtain a copy of the License at
12 *
13 *      http://www.apache.org/licenses/LICENSE-2.0
14 *
15 *    Unless required by applicable law or agreed to in writing,
16 *    software distributed under the License is distributed on an
17 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18 *    KIND, either express or implied.  See the License for the
19 *    specific language governing permissions and limitations
20 *    under the License.
21 * ====================================================================
22 *
23 */
24
25/*  Multiple URL Command Client
26
27    Combine a list of mv, cp and rm commands on URLs into a single commit.
28
29    How it works: the command line arguments are parsed into an array of
30    action structures.  The action structures are interpreted to build a
31    tree of operation structures.  The tree of operation structures is
32    used to drive an RA commit editor to produce a single commit.
33
34    To build this client, type 'make svnmucc' from the root of your
35    Subversion source directory.
36*/
37
38#include <stdio.h>
39#include <string.h>
40
41#include <apr_lib.h>
42
43#include "svn_private_config.h"
44#include "svn_hash.h"
45#include "svn_client.h"
46#include "private/svn_client_mtcc.h"
47#include "svn_cmdline.h"
48#include "svn_config.h"
49#include "svn_error.h"
50#include "svn_path.h"
51#include "svn_pools.h"
52#include "svn_props.h"
53#include "svn_string.h"
54#include "svn_subst.h"
55#include "svn_utf.h"
56#include "svn_version.h"
57
58#include "private/svn_cmdline_private.h"
59#include "private/svn_subr_private.h"
60
61/* Version compatibility check */
62static svn_error_t *
63check_lib_versions(void)
64{
65  static const svn_version_checklist_t checklist[] =
66    {
67      { "svn_client", svn_client_version },
68      { "svn_subr",   svn_subr_version },
69      { "svn_ra",     svn_ra_version },
70      { NULL, NULL }
71    };
72  SVN_VERSION_DEFINE(my_version);
73
74  return svn_ver_check_list2(&my_version, checklist, svn_ver_equal);
75}
76
77static svn_error_t *
78commit_callback(const svn_commit_info_t *commit_info,
79                void *baton,
80                apr_pool_t *pool)
81{
82  SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n",
83                             commit_info->revision,
84                             (commit_info->author
85                              ? commit_info->author : "(no author)"),
86                             commit_info->date));
87  return SVN_NO_ERROR;
88}
89
90typedef enum action_code_t {
91  ACTION_MV,
92  ACTION_MKDIR,
93  ACTION_CP,
94  ACTION_PROPSET,
95  ACTION_PROPSETF,
96  ACTION_PROPDEL,
97  ACTION_PUT,
98  ACTION_RM
99} action_code_t;
100
101/* Return the portion of URL that is relative to ANCHOR (URI-decoded). */
102static const char *
103subtract_anchor(const char *anchor, const char *url, apr_pool_t *pool)
104{
105  return svn_uri_skip_ancestor(anchor, url, pool);
106}
107
108
109struct action {
110  action_code_t action;
111
112  /* revision (copy-from-rev of path[0] for cp; base-rev for put) */
113  svn_revnum_t rev;
114
115  /* action  path[0]  path[1]
116   * ------  -------  -------
117   * mv      source   target
118   * mkdir   target   (null)
119   * cp      source   target
120   * put     target   source
121   * rm      target   (null)
122   * propset target   (null)
123   */
124  const char *path[2];
125
126  /* property name/value */
127  const char *prop_name;
128  const svn_string_t *prop_value;
129};
130
131static svn_error_t *
132execute(const apr_array_header_t *actions,
133        const char *anchor,
134        apr_hash_t *revprops,
135        svn_revnum_t base_revision,
136        svn_client_ctx_t *ctx,
137        apr_pool_t *pool)
138{
139  svn_client__mtcc_t *mtcc;
140  apr_pool_t *iterpool = svn_pool_create(pool);
141  svn_error_t *err;
142  int i;
143
144  SVN_ERR(svn_client__mtcc_create(&mtcc, anchor,
145                                  SVN_IS_VALID_REVNUM(base_revision)
146                                     ? base_revision
147                                     : SVN_INVALID_REVNUM,
148                                  ctx, pool, iterpool));
149
150  for (i = 0; i < actions->nelts; ++i)
151    {
152      struct action *action = APR_ARRAY_IDX(actions, i, struct action *);
153      const char *path1, *path2;
154      svn_node_kind_t kind;
155
156      svn_pool_clear(iterpool);
157
158      switch (action->action)
159        {
160        case ACTION_MV:
161          path1 = subtract_anchor(anchor, action->path[0], pool);
162          path2 = subtract_anchor(anchor, action->path[1], pool);
163          SVN_ERR(svn_client__mtcc_add_move(path1, path2, mtcc, iterpool));
164          break;
165        case ACTION_CP:
166          path1 = subtract_anchor(anchor, action->path[0], pool);
167          path2 = subtract_anchor(anchor, action->path[1], pool);
168          SVN_ERR(svn_client__mtcc_add_copy(path1, action->rev, path2,
169                                            mtcc, iterpool));
170          break;
171        case ACTION_RM:
172          path1 = subtract_anchor(anchor, action->path[0], pool);
173          SVN_ERR(svn_client__mtcc_add_delete(path1, mtcc, iterpool));
174          break;
175        case ACTION_MKDIR:
176          path1 = subtract_anchor(anchor, action->path[0], pool);
177          SVN_ERR(svn_client__mtcc_add_mkdir(path1, mtcc, iterpool));
178          break;
179        case ACTION_PUT:
180          path1 = subtract_anchor(anchor, action->path[0], pool);
181          SVN_ERR(svn_client__mtcc_check_path(&kind, path1, TRUE, mtcc, pool));
182
183          if (kind == svn_node_dir)
184            {
185              SVN_ERR(svn_client__mtcc_add_delete(path1, mtcc, pool));
186              kind = svn_node_none;
187            }
188
189          {
190            svn_stream_t *src;
191
192            if (strcmp(action->path[1], "-") != 0)
193              SVN_ERR(svn_stream_open_readonly(&src, action->path[1],
194                                               pool, iterpool));
195            else
196              SVN_ERR(svn_stream_for_stdin(&src, pool));
197
198
199            if (kind == svn_node_file)
200              SVN_ERR(svn_client__mtcc_add_update_file(path1, src, NULL,
201                                                       NULL, NULL,
202                                                       mtcc, iterpool));
203            else if (kind == svn_node_none)
204              SVN_ERR(svn_client__mtcc_add_add_file(path1, src, NULL,
205                                                    mtcc, iterpool));
206          }
207          break;
208        case ACTION_PROPSET:
209        case ACTION_PROPDEL:
210          path1 = subtract_anchor(anchor, action->path[0], pool);
211          SVN_ERR(svn_client__mtcc_add_propset(path1, action->prop_name,
212                                               action->prop_value, FALSE,
213                                               mtcc, iterpool));
214          break;
215        case ACTION_PROPSETF:
216        default:
217          SVN_ERR_MALFUNCTION_NO_RETURN();
218        }
219    }
220
221  err = svn_client__mtcc_commit(revprops, commit_callback, NULL,
222                                mtcc, iterpool);
223
224  svn_pool_destroy(iterpool);
225  return svn_error_trace(err);
226}
227
228static svn_error_t *
229read_propvalue_file(const svn_string_t **value_p,
230                    const char *filename,
231                    apr_pool_t *pool)
232{
233  svn_stringbuf_t *value;
234  apr_pool_t *scratch_pool = svn_pool_create(pool);
235
236  SVN_ERR(svn_stringbuf_from_file2(&value, filename, scratch_pool));
237  *value_p = svn_string_create_from_buf(value, pool);
238  svn_pool_destroy(scratch_pool);
239  return SVN_NO_ERROR;
240}
241
242/* Perform the typical suite of manipulations for user-provided URLs
243   on URL, returning the result (allocated from POOL): IRI-to-URI
244   conversion, auto-escaping, and canonicalization. */
245static const char *
246sanitize_url(const char *url,
247             apr_pool_t *pool)
248{
249  url = svn_path_uri_from_iri(url, pool);
250  url = svn_path_uri_autoescape(url, pool);
251  return svn_uri_canonicalize(url, pool);
252}
253
254static void
255usage(apr_pool_t *pool)
256{
257  svn_error_clear(svn_cmdline_fprintf
258                  (stderr, pool, _("Type 'svnmucc --help' for usage.\n")));
259}
260
261/* Print a usage message on STREAM. */
262static void
263help(FILE *stream, apr_pool_t *pool)
264{
265  svn_error_clear(svn_cmdline_fputs(
266    _("usage: svnmucc ACTION...\n"
267      "Subversion multiple URL command client.\n"
268      "Type 'svnmucc --version' to see the program version and RA modules.\n"
269      "\n"
270      "  Perform one or more Subversion repository URL-based ACTIONs, committing\n"
271      "  the result as a (single) new revision.\n"
272      "\n"
273      "Actions:\n"
274      "  cp REV SRC-URL DST-URL : copy SRC-URL@REV to DST-URL\n"
275      "  mkdir URL              : create new directory URL\n"
276      "  mv SRC-URL DST-URL     : move SRC-URL to DST-URL\n"
277      "  rm URL                 : delete URL\n"
278      "  put SRC-FILE URL       : add or modify file URL with contents copied from\n"
279      "                           SRC-FILE (use \"-\" to read from standard input)\n"
280      "  propset NAME VALUE URL : set property NAME on URL to VALUE\n"
281      "  propsetf NAME FILE URL : set property NAME on URL to value read from FILE\n"
282      "  propdel NAME URL       : delete property NAME from URL\n"
283      "\n"
284      "Valid options:\n"
285      "  -h, -? [--help]        : display this text\n"
286      "  -m [--message] ARG     : use ARG as a log message\n"
287      "  -F [--file] ARG        : read log message from file ARG\n"
288      "  -u [--username] ARG    : commit the changes as username ARG\n"
289      "  -p [--password] ARG    : use ARG as the password\n"
290      "  -U [--root-url] ARG    : interpret all action URLs relative to ARG\n"
291      "  -r [--revision] ARG    : use revision ARG as baseline for changes\n"
292      "  --with-revprop ARG     : set revision property in the following format:\n"
293      "                               NAME[=VALUE]\n"
294      "  --non-interactive      : do no interactive prompting (default is to\n"
295      "                           prompt only if standard input is a terminal)\n"
296      "  --force-interactive    : do interactive prompting even if standard\n"
297      "                           input is not a terminal\n"
298      "  --trust-server-cert    : deprecated;\n"
299      "                           same as --trust-server-cert-failures=unknown-ca\n"
300      "  --trust-server-cert-failures ARG\n"
301      "                           with --non-interactive, accept SSL server\n"
302      "                           certificates with failures; ARG is comma-separated\n"
303      "                           list of 'unknown-ca' (Unknown Authority),\n"
304      "                           'cn-mismatch' (Hostname mismatch), 'expired'\n"
305      "                           (Expired certificate),'not-yet-valid' (Not yet\n"
306      "                           valid certificate) and 'other' (all other not\n"
307      "                           separately classified certificate errors).\n"
308      "  -X [--extra-args] ARG  : append arguments from file ARG (one per line;\n"
309      "                           use \"-\" to read from standard input)\n"
310      "  --config-dir ARG       : use ARG to override the config directory\n"
311      "  --config-option ARG    : use ARG to override a configuration option\n"
312      "  --no-auth-cache        : do not cache authentication tokens\n"
313      "  --version              : print version information\n"),
314                  stream, pool));
315}
316
317static svn_error_t *
318insufficient(void)
319{
320  return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
321                          "insufficient arguments");
322}
323
324static svn_error_t *
325display_version(apr_pool_t *pool)
326{
327  const char *ra_desc_start
328    = "The following repository access (RA) modules are available:\n\n";
329  svn_stringbuf_t *version_footer;
330
331  version_footer = svn_stringbuf_create(ra_desc_start, pool);
332  SVN_ERR(svn_ra_print_modules(version_footer, pool));
333
334  SVN_ERR(svn_opt_print_help4(NULL, "svnmucc", TRUE, FALSE, FALSE,
335                              version_footer->data,
336                              NULL, NULL, NULL, NULL, NULL, pool));
337
338  return SVN_NO_ERROR;
339}
340
341/* Return an error about the mutual exclusivity of the -m, -F, and
342   --with-revprop=svn:log command-line options. */
343static svn_error_t *
344mutually_exclusive_logs_error(void)
345{
346  return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
347                          _("--message (-m), --file (-F), and "
348                            "--with-revprop=svn:log are mutually "
349                            "exclusive"));
350}
351
352/* Obtain the log message from multiple sources, producing an error
353   if there are multiple sources. Store the result in *FINAL_MESSAGE.  */
354static svn_error_t *
355sanitize_log_sources(const char **final_message,
356                     const char *message,
357                     apr_hash_t *revprops,
358                     svn_stringbuf_t *filedata,
359                     apr_pool_t *result_pool,
360                     apr_pool_t *scratch_pool)
361{
362  svn_string_t *msg;
363
364  *final_message = NULL;
365  /* If we already have a log message in the revprop hash, then just
366     make sure the user didn't try to also use -m or -F.  Otherwise,
367     we need to consult -m or -F to find a log message, if any. */
368  msg = svn_hash_gets(revprops, SVN_PROP_REVISION_LOG);
369  if (msg)
370    {
371      if (filedata || message)
372        return mutually_exclusive_logs_error();
373
374      *final_message = apr_pstrdup(result_pool, msg->data);
375
376      /* Will be re-added by libsvn_client */
377      svn_hash_sets(revprops, SVN_PROP_REVISION_LOG, NULL);
378    }
379  else if (filedata)
380    {
381      if (message)
382        return mutually_exclusive_logs_error();
383
384      *final_message = apr_pstrdup(result_pool, filedata->data);
385    }
386  else if (message)
387    {
388      *final_message = apr_pstrdup(result_pool, message);
389    }
390
391  return SVN_NO_ERROR;
392}
393
394/* Baton for log_message_func */
395struct log_message_baton
396{
397  svn_boolean_t non_interactive;
398  const char *log_message;
399  svn_client_ctx_t *ctx;
400};
401
402/* Implements svn_client_get_commit_log3_t */
403static svn_error_t *
404log_message_func(const char **log_msg,
405                 const char **tmp_file,
406                 const apr_array_header_t *commit_items,
407                 void *baton,
408                 apr_pool_t *pool)
409{
410  struct log_message_baton *lmb = baton;
411
412  *tmp_file = NULL;
413
414  if (lmb->log_message)
415    {
416      svn_string_t *message = svn_string_create(lmb->log_message, pool);
417
418      SVN_ERR_W(svn_subst_translate_string2(&message, NULL, NULL,
419                                            message, NULL, FALSE,
420                                            pool, pool),
421                _("Error normalizing log message to internal format"));
422
423      *log_msg = message->data;
424
425      return SVN_NO_ERROR;
426    }
427
428  if (lmb->non_interactive)
429    {
430      return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, NULL,
431                              _("Cannot invoke editor to get log message "
432                                "when non-interactive"));
433    }
434  else
435    {
436      svn_string_t *msg = svn_string_create("", pool);
437
438      SVN_ERR(svn_cmdline__edit_string_externally(
439                      &msg, NULL, NULL, "", msg, "svnmucc-commit",
440                      lmb->ctx->config, TRUE, NULL, pool));
441
442      if (msg && msg->data)
443        *log_msg = msg->data;
444      else
445        *log_msg = NULL;
446
447      return SVN_NO_ERROR;
448    }
449}
450
451/*
452 * On success, leave *EXIT_CODE untouched and return SVN_NO_ERROR. On error,
453 * either return an error to be displayed, or set *EXIT_CODE to non-zero and
454 * return SVN_NO_ERROR.
455 */
456static svn_error_t *
457sub_main(int *exit_code, int argc, const char *argv[], apr_pool_t *pool)
458{
459  apr_array_header_t *actions = apr_array_make(pool, 1,
460                                               sizeof(struct action *));
461  const char *anchor = NULL;
462  svn_error_t *err = SVN_NO_ERROR;
463  apr_getopt_t *opts;
464  enum {
465    config_dir_opt = SVN_OPT_FIRST_LONGOPT_ID,
466    config_inline_opt,
467    no_auth_cache_opt,
468    version_opt,
469    with_revprop_opt,
470    non_interactive_opt,
471    force_interactive_opt,
472    trust_server_cert_opt,
473    trust_server_cert_failures_opt,
474  };
475  static const apr_getopt_option_t options[] = {
476    {"message", 'm', 1, ""},
477    {"file", 'F', 1, ""},
478    {"username", 'u', 1, ""},
479    {"password", 'p', 1, ""},
480    {"root-url", 'U', 1, ""},
481    {"revision", 'r', 1, ""},
482    {"with-revprop",  with_revprop_opt, 1, ""},
483    {"extra-args", 'X', 1, ""},
484    {"help", 'h', 0, ""},
485    {NULL, '?', 0, ""},
486    {"non-interactive", non_interactive_opt, 0, ""},
487    {"force-interactive", force_interactive_opt, 0, ""},
488    {"trust-server-cert", trust_server_cert_opt, 0, ""},
489    {"trust-server-cert-failures", trust_server_cert_failures_opt, 1, ""},
490    {"config-dir", config_dir_opt, 1, ""},
491    {"config-option",  config_inline_opt, 1, ""},
492    {"no-auth-cache",  no_auth_cache_opt, 0, ""},
493    {"version", version_opt, 0, ""},
494    {NULL, 0, 0, NULL}
495  };
496  const char *message = NULL;
497  svn_stringbuf_t *filedata = NULL;
498  const char *username = NULL, *password = NULL;
499  const char *root_url = NULL, *extra_args_file = NULL;
500  const char *config_dir = NULL;
501  apr_array_header_t *config_options;
502  svn_boolean_t non_interactive = FALSE;
503  svn_boolean_t force_interactive = FALSE;
504  svn_boolean_t trust_unknown_ca = FALSE;
505  svn_boolean_t trust_cn_mismatch = FALSE;
506  svn_boolean_t trust_expired = FALSE;
507  svn_boolean_t trust_not_yet_valid = FALSE;
508  svn_boolean_t trust_other_failure = FALSE;
509  svn_boolean_t no_auth_cache = FALSE;
510  svn_boolean_t show_version = FALSE;
511  svn_boolean_t show_help = FALSE;
512  svn_revnum_t base_revision = SVN_INVALID_REVNUM;
513  apr_array_header_t *action_args;
514  apr_hash_t *revprops = apr_hash_make(pool);
515  apr_hash_t *cfg_hash;
516  svn_config_t *cfg_config;
517  svn_client_ctx_t *ctx;
518  struct log_message_baton lmb;
519  int i;
520
521  /* Check library versions */
522  SVN_ERR(check_lib_versions());
523
524  config_options = apr_array_make(pool, 0,
525                                  sizeof(svn_cmdline__config_argument_t*));
526
527  apr_getopt_init(&opts, pool, argc, argv);
528  opts->interleave = 1;
529  while (1)
530    {
531      int opt;
532      const char *arg;
533      const char *opt_arg;
534
535      apr_status_t status = apr_getopt_long(opts, options, &opt, &arg);
536      if (APR_STATUS_IS_EOF(status))
537        break;
538      if (status != APR_SUCCESS)
539        {
540          usage(pool);
541          *exit_code = EXIT_FAILURE;
542          return SVN_NO_ERROR;
543        }
544      switch(opt)
545        {
546        case 'm':
547          SVN_ERR(svn_utf_cstring_to_utf8(&message, arg, pool));
548          break;
549        case 'F':
550          {
551            const char *arg_utf8;
552            SVN_ERR(svn_utf_cstring_to_utf8(&arg_utf8, arg, pool));
553            SVN_ERR(svn_stringbuf_from_file2(&filedata, arg, pool));
554          }
555          break;
556        case 'u':
557          username = apr_pstrdup(pool, arg);
558          break;
559        case 'p':
560          password = apr_pstrdup(pool, arg);
561          break;
562        case 'U':
563          SVN_ERR(svn_utf_cstring_to_utf8(&root_url, arg, pool));
564          if (! svn_path_is_url(root_url))
565            return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
566                                     "'%s' is not a URL\n", root_url);
567          root_url = sanitize_url(root_url, pool);
568          break;
569        case 'r':
570          {
571            const char *saved_arg = arg;
572            char *digits_end = NULL;
573            while (*arg == 'r')
574              arg++;
575            base_revision = strtol(arg, &digits_end, 10);
576            if ((! SVN_IS_VALID_REVNUM(base_revision))
577                || (! digits_end)
578                || *digits_end)
579              return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
580                                       _("Invalid revision number '%s'"),
581                                       saved_arg);
582          }
583          break;
584        case with_revprop_opt:
585          SVN_ERR(svn_opt_parse_revprop(&revprops, arg, pool));
586          break;
587        case 'X':
588          extra_args_file = apr_pstrdup(pool, arg);
589          break;
590        case non_interactive_opt:
591          non_interactive = TRUE;
592          break;
593        case force_interactive_opt:
594          force_interactive = TRUE;
595          break;
596        case trust_server_cert_opt: /* backward compat */
597          trust_unknown_ca = TRUE;
598          break;
599        case trust_server_cert_failures_opt:
600          SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, arg, pool));
601          SVN_ERR(svn_cmdline__parse_trust_options(
602                      &trust_unknown_ca,
603                      &trust_cn_mismatch,
604                      &trust_expired,
605                      &trust_not_yet_valid,
606                      &trust_other_failure,
607                      opt_arg, pool));
608          break;
609        case config_dir_opt:
610          SVN_ERR(svn_utf_cstring_to_utf8(&config_dir, arg, pool));
611          break;
612        case config_inline_opt:
613          SVN_ERR(svn_utf_cstring_to_utf8(&opt_arg, arg, pool));
614          SVN_ERR(svn_cmdline__parse_config_option(config_options, opt_arg,
615                                                   "svnmucc: ",
616                                                   pool));
617          break;
618        case no_auth_cache_opt:
619          no_auth_cache = TRUE;
620          break;
621        case version_opt:
622          show_version = TRUE;
623          break;
624        case 'h':
625        case '?':
626          show_help = TRUE;
627          break;
628        }
629    }
630
631  if (show_help)
632    {
633      help(stdout, pool);
634      return SVN_NO_ERROR;
635    }
636
637  if (show_version)
638    {
639      SVN_ERR(display_version(pool));
640      return SVN_NO_ERROR;
641    }
642
643  if (non_interactive && force_interactive)
644    {
645      return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
646                              _("--non-interactive and --force-interactive "
647                                "are mutually exclusive"));
648    }
649  else
650    non_interactive = !svn_cmdline__be_interactive(non_interactive,
651                                                   force_interactive);
652
653  if (!non_interactive)
654    {
655      if (trust_unknown_ca || trust_cn_mismatch || trust_expired
656          || trust_not_yet_valid || trust_other_failure)
657        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
658                                _("--trust-server-cert-failures requires "
659                                  "--non-interactive"));
660    }
661
662  /* Copy the rest of our command-line arguments to an array,
663     UTF-8-ing them along the way. */
664  action_args = apr_array_make(pool, opts->argc, sizeof(const char *));
665  while (opts->ind < opts->argc)
666    {
667      const char *arg = opts->argv[opts->ind++];
668      SVN_ERR(svn_utf_cstring_to_utf8(&APR_ARRAY_PUSH(action_args,
669                                                      const char *),
670                                      arg, pool));
671    }
672
673  /* If there are extra arguments in a supplementary file, tack those
674     on, too (again, in UTF8 form). */
675  if (extra_args_file)
676    {
677      const char *extra_args_file_utf8;
678      svn_stringbuf_t *contents, *contents_utf8;
679
680      SVN_ERR(svn_utf_cstring_to_utf8(&extra_args_file_utf8,
681                                      extra_args_file, pool));
682      SVN_ERR(svn_stringbuf_from_file2(&contents, extra_args_file_utf8, pool));
683      SVN_ERR(svn_utf_stringbuf_to_utf8(&contents_utf8, contents, pool));
684      svn_cstring_split_append(action_args, contents_utf8->data, "\n\r",
685                               FALSE, pool);
686    }
687
688  /* Now initialize the client context */
689
690  err = svn_config_get_config(&cfg_hash, config_dir, pool);
691  if (err)
692    {
693      /* Fallback to default config if the config directory isn't readable
694         or is not a directory. */
695      if (APR_STATUS_IS_EACCES(err->apr_err)
696          || SVN__APR_STATUS_IS_ENOTDIR(err->apr_err))
697        {
698          svn_handle_warning2(stderr, err, "svnmucc: ");
699          svn_error_clear(err);
700
701          SVN_ERR(svn_config__get_default_config(&cfg_hash, pool));
702        }
703      else
704        return err;
705    }
706
707  if (config_options)
708    {
709      svn_error_clear(
710          svn_cmdline__apply_config_options(cfg_hash, config_options,
711                                            "svnmucc: ", "--config-option"));
712    }
713
714  SVN_ERR(svn_client_create_context2(&ctx, cfg_hash, pool));
715
716  cfg_config = svn_hash_gets(cfg_hash, SVN_CONFIG_CATEGORY_CONFIG);
717  SVN_ERR(svn_cmdline_create_auth_baton2(
718            &ctx->auth_baton,
719            non_interactive,
720            username,
721            password,
722            config_dir,
723            no_auth_cache,
724            trust_unknown_ca,
725            trust_cn_mismatch,
726            trust_expired,
727            trust_not_yet_valid,
728            trust_other_failure,
729            cfg_config,
730            ctx->cancel_func,
731            ctx->cancel_baton,
732            pool));
733
734  lmb.non_interactive = non_interactive;
735  lmb.ctx = ctx;
736    /* Make sure we have a log message to use. */
737  SVN_ERR(sanitize_log_sources(&lmb.log_message, message, revprops, filedata,
738                               pool, pool));
739
740  ctx->log_msg_func3 = log_message_func;
741  ctx->log_msg_baton3 = &lmb;
742
743  /* Now, we iterate over the combined set of arguments -- our actions. */
744  for (i = 0; i < action_args->nelts; )
745    {
746      int j, num_url_args;
747      const char *action_string = APR_ARRAY_IDX(action_args, i, const char *);
748      struct action *action = apr_pcalloc(pool, sizeof(*action));
749
750      /* First, parse the action. */
751      if (! strcmp(action_string, "mv"))
752        action->action = ACTION_MV;
753      else if (! strcmp(action_string, "cp"))
754        action->action = ACTION_CP;
755      else if (! strcmp(action_string, "mkdir"))
756        action->action = ACTION_MKDIR;
757      else if (! strcmp(action_string, "rm"))
758        action->action = ACTION_RM;
759      else if (! strcmp(action_string, "put"))
760        action->action = ACTION_PUT;
761      else if (! strcmp(action_string, "propset"))
762        action->action = ACTION_PROPSET;
763      else if (! strcmp(action_string, "propsetf"))
764        action->action = ACTION_PROPSETF;
765      else if (! strcmp(action_string, "propdel"))
766        action->action = ACTION_PROPDEL;
767      else if (! strcmp(action_string, "?") || ! strcmp(action_string, "h")
768               || ! strcmp(action_string, "help"))
769        {
770          help(stdout, pool);
771          return SVN_NO_ERROR;
772        }
773      else
774        return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
775                                 "'%s' is not an action\n",
776                                 action_string);
777      if (++i == action_args->nelts)
778        return insufficient();
779
780      /* For copies, there should be a revision number next. */
781      if (action->action == ACTION_CP)
782        {
783          const char *rev_str = APR_ARRAY_IDX(action_args, i, const char *);
784          if (strcmp(rev_str, "head") == 0)
785            action->rev = SVN_INVALID_REVNUM;
786          else if (strcmp(rev_str, "HEAD") == 0)
787            action->rev = SVN_INVALID_REVNUM;
788          else
789            {
790              char *end;
791
792              while (*rev_str == 'r')
793                ++rev_str;
794
795              action->rev = strtol(rev_str, &end, 0);
796              if (*end)
797                return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
798                                         "'%s' is not a revision\n",
799                                         rev_str);
800            }
801          if (++i == action_args->nelts)
802            return insufficient();
803        }
804      else
805        {
806          action->rev = SVN_INVALID_REVNUM;
807        }
808
809      /* For puts, there should be a local file next. */
810      if (action->action == ACTION_PUT)
811        {
812          action->path[1] =
813            svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
814                                                    const char *), pool);
815          if (++i == action_args->nelts)
816            return insufficient();
817        }
818
819      /* For propset, propsetf, and propdel, a property name (and
820         maybe a property value or file which contains one) comes next. */
821      if ((action->action == ACTION_PROPSET)
822          || (action->action == ACTION_PROPSETF)
823          || (action->action == ACTION_PROPDEL))
824        {
825          action->prop_name = APR_ARRAY_IDX(action_args, i, const char *);
826          if (++i == action_args->nelts)
827            return insufficient();
828
829          if (action->action == ACTION_PROPDEL)
830            {
831              action->prop_value = NULL;
832            }
833          else if (action->action == ACTION_PROPSET)
834            {
835              action->prop_value =
836                svn_string_create(APR_ARRAY_IDX(action_args, i,
837                                                const char *), pool);
838              if (++i == action_args->nelts)
839                return insufficient();
840            }
841          else
842            {
843              const char *propval_file =
844                svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
845                                                        const char *), pool);
846
847              if (++i == action_args->nelts)
848                return insufficient();
849
850              SVN_ERR(read_propvalue_file(&(action->prop_value),
851                                          propval_file, pool));
852
853              action->action = ACTION_PROPSET;
854            }
855
856          if (action->prop_value
857              && svn_prop_needs_translation(action->prop_name))
858            {
859              svn_string_t *translated_value;
860              SVN_ERR_W(svn_subst_translate_string2(&translated_value, NULL,
861                                                    NULL, action->prop_value,
862                                                    NULL, FALSE, pool, pool),
863                        "Error normalizing property value");
864              action->prop_value = translated_value;
865            }
866        }
867
868      /* How many URLs does this action expect? */
869      if (action->action == ACTION_RM
870          || action->action == ACTION_MKDIR
871          || action->action == ACTION_PUT
872          || action->action == ACTION_PROPSET
873          || action->action == ACTION_PROPSETF /* shouldn't see this one */
874          || action->action == ACTION_PROPDEL)
875        num_url_args = 1;
876      else
877        num_url_args = 2;
878
879      /* Parse the required number of URLs. */
880      for (j = 0; j < num_url_args; ++j)
881        {
882          const char *url = APR_ARRAY_IDX(action_args, i, const char *);
883
884          /* If there's a ROOT_URL, we expect URL to be a path
885             relative to ROOT_URL (and we build a full url from the
886             combination of the two).  Otherwise, it should be a full
887             url. */
888          if (! svn_path_is_url(url))
889            {
890              if (! root_url)
891                return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
892                                         "'%s' is not a URL, and "
893                                         "--root-url (-U) not provided\n",
894                                         url);
895              /* ### These relpaths are already URI-encoded. */
896              url = apr_pstrcat(pool, root_url, "/",
897                                svn_relpath_canonicalize(url, pool),
898                                SVN_VA_NULL);
899            }
900          url = sanitize_url(url, pool);
901          action->path[j] = url;
902
903          /* The first URL arguments to 'cp', 'pd', 'ps' could be the anchor,
904             but the other URLs should be children of the anchor. */
905          if (! (action->action == ACTION_CP && j == 0)
906              && action->action != ACTION_PROPDEL
907              && action->action != ACTION_PROPSET
908              && action->action != ACTION_PROPSETF)
909            url = svn_uri_dirname(url, pool);
910          if (! anchor)
911            anchor = url;
912          else
913            {
914              anchor = svn_uri_get_longest_ancestor(anchor, url, pool);
915              if (!anchor || !anchor[0])
916                return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
917                                         "URLs in the action list do not "
918                                         "share a common ancestor");
919            }
920
921          if ((++i == action_args->nelts) && (j + 1 < num_url_args))
922            return insufficient();
923        }
924
925      APR_ARRAY_PUSH(actions, struct action *) = action;
926    }
927
928  if (! actions->nelts)
929    {
930      *exit_code = EXIT_FAILURE;
931      help(stderr, pool);
932      return SVN_NO_ERROR;
933    }
934
935  if ((err = execute(actions, anchor, revprops, base_revision, ctx, pool)))
936    {
937      if (err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive)
938        err = svn_error_quick_wrap(err,
939                                   _("Authentication failed and interactive"
940                                     " prompting is disabled; see the"
941                                     " --force-interactive option"));
942      return err;
943    }
944
945  return SVN_NO_ERROR;
946}
947
948int
949main(int argc, const char *argv[])
950{
951  apr_pool_t *pool;
952  int exit_code = EXIT_SUCCESS;
953  svn_error_t *err;
954
955  /* Initialize the app. */
956  if (svn_cmdline_init("svnmucc", stderr) != EXIT_SUCCESS)
957    return EXIT_FAILURE;
958
959  /* Create our top-level pool.  Use a separate mutexless allocator,
960   * given this application is single threaded.
961   */
962  pool = apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
963
964  err = sub_main(&exit_code, argc, argv, pool);
965
966  /* Flush stdout and report if it fails. It would be flushed on exit anyway
967     but this makes sure that output is not silently lost if it fails. */
968  err = svn_error_compose_create(err, svn_cmdline_fflush(stdout));
969
970  if (err)
971    {
972      exit_code = EXIT_FAILURE;
973      svn_cmdline_handle_exit_error(err, NULL, "svnmucc: ");
974    }
975
976  svn_pool_destroy(pool);
977  return exit_code;
978}
979