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_hash.h"
44#include "svn_client.h"
45#include "svn_cmdline.h"
46#include "svn_config.h"
47#include "svn_error.h"
48#include "svn_path.h"
49#include "svn_pools.h"
50#include "svn_props.h"
51#include "svn_ra.h"
52#include "svn_string.h"
53#include "svn_subst.h"
54#include "svn_utf.h"
55#include "svn_version.h"
56
57#include "private/svn_cmdline_private.h"
58#include "private/svn_ra_private.h"
59#include "private/svn_string_private.h"
60#include "private/svn_subr_private.h"
61
62#include "svn_private_config.h"
63
64static void handle_error(svn_error_t *err, apr_pool_t *pool)
65{
66  if (err)
67    svn_handle_error2(err, stderr, FALSE, "svnmucc: ");
68  svn_error_clear(err);
69  if (pool)
70    svn_pool_destroy(pool);
71  exit(EXIT_FAILURE);
72}
73
74static apr_pool_t *
75init(const char *application)
76{
77  svn_error_t *err;
78  const svn_version_checklist_t checklist[] = {
79    {"svn_client", svn_client_version},
80    {"svn_subr", svn_subr_version},
81    {"svn_ra", svn_ra_version},
82    {NULL, NULL}
83  };
84  SVN_VERSION_DEFINE(my_version);
85
86  if (svn_cmdline_init(application, stderr))
87    exit(EXIT_FAILURE);
88
89  err = svn_ver_check_list2(&my_version, checklist, svn_ver_equal);
90  if (err)
91    handle_error(err, NULL);
92
93  return apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
94}
95
96static svn_error_t *
97open_tmp_file(apr_file_t **fp,
98              void *callback_baton,
99              apr_pool_t *pool)
100{
101  /* Open a unique file;  use APR_DELONCLOSE. */
102  return svn_io_open_unique_file3(fp, NULL, NULL, svn_io_file_del_on_close,
103                                  pool, pool);
104}
105
106static svn_error_t *
107create_ra_callbacks(svn_ra_callbacks2_t **callbacks,
108                    const char *username,
109                    const char *password,
110                    const char *config_dir,
111                    svn_config_t *cfg_config,
112                    svn_boolean_t non_interactive,
113                    svn_boolean_t trust_server_cert,
114                    svn_boolean_t no_auth_cache,
115                    apr_pool_t *pool)
116{
117  SVN_ERR(svn_ra_create_callbacks(callbacks, pool));
118
119  SVN_ERR(svn_cmdline_create_auth_baton(&(*callbacks)->auth_baton,
120                                        non_interactive,
121                                        username, password, config_dir,
122                                        no_auth_cache,
123                                        trust_server_cert,
124                                        cfg_config, NULL, NULL, pool));
125
126  (*callbacks)->open_tmp_file = open_tmp_file;
127
128  return SVN_NO_ERROR;
129}
130
131
132
133static svn_error_t *
134commit_callback(const svn_commit_info_t *commit_info,
135                void *baton,
136                apr_pool_t *pool)
137{
138  SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n",
139                             commit_info->revision,
140                             (commit_info->author
141                              ? commit_info->author : "(no author)"),
142                             commit_info->date));
143  return SVN_NO_ERROR;
144}
145
146typedef enum action_code_t {
147  ACTION_MV,
148  ACTION_MKDIR,
149  ACTION_CP,
150  ACTION_PROPSET,
151  ACTION_PROPSETF,
152  ACTION_PROPDEL,
153  ACTION_PUT,
154  ACTION_RM
155} action_code_t;
156
157struct operation {
158  enum {
159    OP_OPEN,
160    OP_DELETE,
161    OP_ADD,
162    OP_REPLACE,
163    OP_PROPSET           /* only for files for which no other operation is
164                            occuring; directories are OP_OPEN with non-empty
165                            props */
166  } operation;
167  svn_node_kind_t kind;  /* to copy, mkdir, put or set revprops */
168  svn_revnum_t rev;      /* to copy, valid for add and replace */
169  const char *url;       /* to copy, valid for add and replace */
170  const char *src_file;  /* for put, the source file for contents */
171  apr_hash_t *children;  /* const char *path -> struct operation * */
172  apr_hash_t *prop_mods; /* const char *prop_name ->
173                            const svn_string_t *prop_value */
174  apr_array_header_t *prop_dels; /* const char *prop_name deletions */
175  void *baton;           /* as returned by the commit editor */
176};
177
178
179/* An iterator (for use via apr_table_do) which sets node properties.
180   REC is a pointer to a struct driver_state. */
181static svn_error_t *
182change_props(const svn_delta_editor_t *editor,
183             void *baton,
184             struct operation *child,
185             apr_pool_t *pool)
186{
187  apr_pool_t *iterpool = svn_pool_create(pool);
188
189  if (child->prop_dels)
190    {
191      int i;
192      for (i = 0; i < child->prop_dels->nelts; i++)
193        {
194          const char *prop_name;
195
196          svn_pool_clear(iterpool);
197          prop_name = APR_ARRAY_IDX(child->prop_dels, i, const char *);
198          if (child->kind == svn_node_dir)
199            SVN_ERR(editor->change_dir_prop(baton, prop_name,
200                                            NULL, iterpool));
201          else
202            SVN_ERR(editor->change_file_prop(baton, prop_name,
203                                             NULL, iterpool));
204        }
205    }
206  if (apr_hash_count(child->prop_mods))
207    {
208      apr_hash_index_t *hi;
209      for (hi = apr_hash_first(pool, child->prop_mods);
210           hi; hi = apr_hash_next(hi))
211        {
212          const char *propname = svn__apr_hash_index_key(hi);
213          const svn_string_t *val = svn__apr_hash_index_val(hi);
214
215          svn_pool_clear(iterpool);
216          if (child->kind == svn_node_dir)
217            SVN_ERR(editor->change_dir_prop(baton, propname, val, iterpool));
218          else
219            SVN_ERR(editor->change_file_prop(baton, propname, val, iterpool));
220        }
221    }
222
223  svn_pool_destroy(iterpool);
224  return SVN_NO_ERROR;
225}
226
227
228/* Drive EDITOR to affect the change represented by OPERATION.  HEAD
229   is the last-known youngest revision in the repository. */
230static svn_error_t *
231drive(struct operation *operation,
232      svn_revnum_t head,
233      const svn_delta_editor_t *editor,
234      apr_pool_t *pool)
235{
236  apr_pool_t *subpool = svn_pool_create(pool);
237  apr_hash_index_t *hi;
238
239  for (hi = apr_hash_first(pool, operation->children);
240       hi; hi = apr_hash_next(hi))
241    {
242      const char *key = svn__apr_hash_index_key(hi);
243      struct operation *child = svn__apr_hash_index_val(hi);
244      void *file_baton = NULL;
245
246      svn_pool_clear(subpool);
247
248      /* Deletes and replacements are simple -- delete something. */
249      if (child->operation == OP_DELETE || child->operation == OP_REPLACE)
250        {
251          SVN_ERR(editor->delete_entry(key, head, operation->baton, subpool));
252        }
253      /* Opens could be for directories or files. */
254      if (child->operation == OP_OPEN || child->operation == OP_PROPSET)
255        {
256          if (child->kind == svn_node_dir)
257            {
258              SVN_ERR(editor->open_directory(key, operation->baton, head,
259                                             subpool, &child->baton));
260            }
261          else
262            {
263              SVN_ERR(editor->open_file(key, operation->baton, head,
264                                        subpool, &file_baton));
265            }
266        }
267      /* Adds and replacements could also be for directories or files. */
268      if (child->operation == OP_ADD || child->operation == OP_REPLACE)
269        {
270          if (child->kind == svn_node_dir)
271            {
272              SVN_ERR(editor->add_directory(key, operation->baton,
273                                            child->url, child->rev,
274                                            subpool, &child->baton));
275            }
276          else
277            {
278              SVN_ERR(editor->add_file(key, operation->baton, child->url,
279                                       child->rev, subpool, &file_baton));
280            }
281        }
282      /* If there's a source file and an open file baton, we get to
283         change textual contents. */
284      if ((child->src_file) && (file_baton))
285        {
286          svn_txdelta_window_handler_t handler;
287          void *handler_baton;
288          svn_stream_t *contents;
289
290          SVN_ERR(editor->apply_textdelta(file_baton, NULL, subpool,
291                                          &handler, &handler_baton));
292          if (strcmp(child->src_file, "-") != 0)
293            {
294              SVN_ERR(svn_stream_open_readonly(&contents, child->src_file,
295                                               pool, pool));
296            }
297          else
298            {
299              SVN_ERR(svn_stream_for_stdin(&contents, pool));
300            }
301          SVN_ERR(svn_txdelta_send_stream(contents, handler,
302                                          handler_baton, NULL, pool));
303        }
304      /* If we opened a file, we need to apply outstanding propmods,
305         then close it. */
306      if (file_baton)
307        {
308          if (child->kind == svn_node_file)
309            {
310              SVN_ERR(change_props(editor, file_baton, child, subpool));
311            }
312          SVN_ERR(editor->close_file(file_baton, NULL, subpool));
313        }
314      /* If we opened, added, or replaced a directory, we need to
315         recurse, apply outstanding propmods, and then close it. */
316      if ((child->kind == svn_node_dir)
317          && child->operation != OP_DELETE)
318        {
319          SVN_ERR(change_props(editor, child->baton, child, subpool));
320
321          SVN_ERR(drive(child, head, editor, subpool));
322
323          SVN_ERR(editor->close_directory(child->baton, subpool));
324        }
325    }
326  svn_pool_destroy(subpool);
327  return SVN_NO_ERROR;
328}
329
330
331/* Find the operation associated with PATH, which is a single-path
332   component representing a child of the path represented by
333   OPERATION.  If no such child operation exists, create a new one of
334   type OP_OPEN. */
335static struct operation *
336get_operation(const char *path,
337              struct operation *operation,
338              apr_pool_t *pool)
339{
340  struct operation *child = svn_hash_gets(operation->children, path);
341  if (! child)
342    {
343      child = apr_pcalloc(pool, sizeof(*child));
344      child->children = apr_hash_make(pool);
345      child->operation = OP_OPEN;
346      child->rev = SVN_INVALID_REVNUM;
347      child->kind = svn_node_dir;
348      child->prop_mods = apr_hash_make(pool);
349      child->prop_dels = apr_array_make(pool, 1, sizeof(const char *));
350      svn_hash_sets(operation->children, path, child);
351    }
352  return child;
353}
354
355/* Return the portion of URL that is relative to ANCHOR (URI-decoded). */
356static const char *
357subtract_anchor(const char *anchor, const char *url, apr_pool_t *pool)
358{
359  return svn_uri_skip_ancestor(anchor, url, pool);
360}
361
362/* Add PATH to the operations tree rooted at OPERATION, creating any
363   intermediate nodes that are required.  Here's what's expected for
364   each action type:
365
366      ACTION          URL    REV      SRC-FILE  PROPNAME
367      ------------    -----  -------  --------  --------
368      ACTION_MKDIR    NULL   invalid  NULL      NULL
369      ACTION_CP       valid  valid    NULL      NULL
370      ACTION_PUT      NULL   invalid  valid     NULL
371      ACTION_RM       NULL   invalid  NULL      NULL
372      ACTION_PROPSET  valid  invalid  NULL      valid
373      ACTION_PROPDEL  valid  invalid  NULL      valid
374
375   Node type information is obtained for any copy source (to determine
376   whether to create a file or directory) and for any deleted path (to
377   ensure it exists since svn_delta_editor_t->delete_entry doesn't
378   return an error on non-existent nodes). */
379static svn_error_t *
380build(action_code_t action,
381      const char *path,
382      const char *url,
383      svn_revnum_t rev,
384      const char *prop_name,
385      const svn_string_t *prop_value,
386      const char *src_file,
387      svn_revnum_t head,
388      const char *anchor,
389      svn_ra_session_t *session,
390      struct operation *operation,
391      apr_pool_t *pool)
392{
393  apr_array_header_t *path_bits = svn_path_decompose(path, pool);
394  const char *path_so_far = "";
395  const char *copy_src = NULL;
396  svn_revnum_t copy_rev = SVN_INVALID_REVNUM;
397  int i;
398
399  /* Look for any previous operations we've recognized for PATH.  If
400     any of PATH's ancestors have not yet been traversed, we'll be
401     creating OP_OPEN operations for them as we walk down PATH's path
402     components. */
403  for (i = 0; i < path_bits->nelts; ++i)
404    {
405      const char *path_bit = APR_ARRAY_IDX(path_bits, i, const char *);
406      path_so_far = svn_relpath_join(path_so_far, path_bit, pool);
407      operation = get_operation(path_so_far, operation, pool);
408
409      /* If we cross a replace- or add-with-history, remember the
410      source of those things in case we need to lookup the node kind
411      of one of their children.  And if this isn't such a copy,
412      but we've already seen one in of our parent paths, we just need
413      to extend that copy source path by our current path
414      component. */
415      if (operation->url
416          && SVN_IS_VALID_REVNUM(operation->rev)
417          && (operation->operation == OP_REPLACE
418              || operation->operation == OP_ADD))
419        {
420          copy_src = subtract_anchor(anchor, operation->url, pool);
421          copy_rev = operation->rev;
422        }
423      else if (copy_src)
424        {
425          copy_src = svn_relpath_join(copy_src, path_bit, pool);
426        }
427    }
428
429  /* Handle property changes. */
430  if (prop_name)
431    {
432      if (operation->operation == OP_DELETE)
433        return svn_error_createf(SVN_ERR_BAD_URL, NULL,
434                                 "cannot set properties on a location being"
435                                 " deleted ('%s')", path);
436      /* If we're not adding this thing ourselves, check for existence.  */
437      if (! ((operation->operation == OP_ADD) ||
438             (operation->operation == OP_REPLACE)))
439        {
440          SVN_ERR(svn_ra_check_path(session,
441                                    copy_src ? copy_src : path,
442                                    copy_src ? copy_rev : head,
443                                    &operation->kind, pool));
444          if (operation->kind == svn_node_none)
445            return svn_error_createf(SVN_ERR_BAD_URL, NULL,
446                                     "propset: '%s' not found", path);
447          else if ((operation->kind == svn_node_file)
448                   && (operation->operation == OP_OPEN))
449            operation->operation = OP_PROPSET;
450        }
451      if (! prop_value)
452        APR_ARRAY_PUSH(operation->prop_dels, const char *) = prop_name;
453      else
454        svn_hash_sets(operation->prop_mods, prop_name, prop_value);
455      if (!operation->rev)
456        operation->rev = rev;
457      return SVN_NO_ERROR;
458    }
459
460  /* We won't fuss about multiple operations on the same path in the
461     following cases:
462
463       - the prior operation was, in fact, a no-op (open)
464       - the prior operation was a propset placeholder
465       - the prior operation was a deletion
466
467     Note: while the operation structure certainly supports the
468     ability to do a copy of a file followed by a put of new contents
469     for the file, we don't let that happen (yet).
470  */
471  if (operation->operation != OP_OPEN
472      && operation->operation != OP_PROPSET
473      && operation->operation != OP_DELETE)
474    return svn_error_createf(SVN_ERR_BAD_URL, NULL,
475                             "unsupported multiple operations on '%s'", path);
476
477  /* For deletions, we validate that there's actually something to
478     delete.  If this is a deletion of the child of a copied
479     directory, we need to remember to look in the copy source tree to
480     verify that this thing actually exists. */
481  if (action == ACTION_RM)
482    {
483      operation->operation = OP_DELETE;
484      SVN_ERR(svn_ra_check_path(session,
485                                copy_src ? copy_src : path,
486                                copy_src ? copy_rev : head,
487                                &operation->kind, pool));
488      if (operation->kind == svn_node_none)
489        {
490          if (copy_src && strcmp(path, copy_src))
491            return svn_error_createf(SVN_ERR_BAD_URL, NULL,
492                                     "'%s' (from '%s:%ld') not found",
493                                     path, copy_src, copy_rev);
494          else
495            return svn_error_createf(SVN_ERR_BAD_URL, NULL, "'%s' not found",
496                                     path);
497        }
498    }
499  /* Handle copy operations (which can be adds or replacements). */
500  else if (action == ACTION_CP)
501    {
502      if (rev > head)
503        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
504                                "Copy source revision cannot be younger "
505                                "than base revision");
506      operation->operation =
507        operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
508      if (operation->operation == OP_ADD)
509        {
510          /* There is a bug in the current version of mod_dav_svn
511             which incorrectly replaces existing directories.
512             Therefore we need to check if the target exists
513             and raise an error here. */
514          SVN_ERR(svn_ra_check_path(session,
515                                    copy_src ? copy_src : path,
516                                    copy_src ? copy_rev : head,
517                                    &operation->kind, pool));
518          if (operation->kind != svn_node_none)
519            {
520              if (copy_src && strcmp(path, copy_src))
521                return svn_error_createf(SVN_ERR_BAD_URL, NULL,
522                                         "'%s' (from '%s:%ld') already exists",
523                                         path, copy_src, copy_rev);
524              else
525                return svn_error_createf(SVN_ERR_BAD_URL, NULL,
526                                         "'%s' already exists", path);
527            }
528        }
529      SVN_ERR(svn_ra_check_path(session, subtract_anchor(anchor, url, pool),
530                                rev, &operation->kind, pool));
531      if (operation->kind == svn_node_none)
532        return svn_error_createf(SVN_ERR_BAD_URL, NULL,
533                                 "'%s' not found",
534                                  subtract_anchor(anchor, url, pool));
535      operation->url = url;
536      operation->rev = rev;
537    }
538  /* Handle mkdir operations (which can be adds or replacements). */
539  else if (action == ACTION_MKDIR)
540    {
541      operation->operation =
542        operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
543      operation->kind = svn_node_dir;
544    }
545  /* Handle put operations (which can be adds, replacements, or opens). */
546  else if (action == ACTION_PUT)
547    {
548      if (operation->operation == OP_DELETE)
549        {
550          operation->operation = OP_REPLACE;
551        }
552      else
553        {
554          SVN_ERR(svn_ra_check_path(session,
555                                    copy_src ? copy_src : path,
556                                    copy_src ? copy_rev : head,
557                                    &operation->kind, pool));
558          if (operation->kind == svn_node_file)
559            operation->operation = OP_OPEN;
560          else if (operation->kind == svn_node_none)
561            operation->operation = OP_ADD;
562          else
563            return svn_error_createf(SVN_ERR_BAD_URL, NULL,
564                                     "'%s' is not a file", path);
565        }
566      operation->kind = svn_node_file;
567      operation->src_file = src_file;
568    }
569  else
570    {
571      /* We shouldn't get here. */
572      SVN_ERR_MALFUNCTION();
573    }
574
575  return SVN_NO_ERROR;
576}
577
578struct action {
579  action_code_t action;
580
581  /* revision (copy-from-rev of path[0] for cp; base-rev for put) */
582  svn_revnum_t rev;
583
584  /* action  path[0]  path[1]
585   * ------  -------  -------
586   * mv      source   target
587   * mkdir   target   (null)
588   * cp      source   target
589   * put     target   source
590   * rm      target   (null)
591   * propset target   (null)
592   */
593  const char *path[2];
594
595  /* property name/value */
596  const char *prop_name;
597  const svn_string_t *prop_value;
598};
599
600struct fetch_baton
601{
602  svn_ra_session_t *session;
603  svn_revnum_t head;
604};
605
606static svn_error_t *
607fetch_base_func(const char **filename,
608                void *baton,
609                const char *path,
610                svn_revnum_t base_revision,
611                apr_pool_t *result_pool,
612                apr_pool_t *scratch_pool)
613{
614  struct fetch_baton *fb = baton;
615  svn_stream_t *fstream;
616  svn_error_t *err;
617
618  if (! SVN_IS_VALID_REVNUM(base_revision))
619    base_revision = fb->head;
620
621  SVN_ERR(svn_stream_open_unique(&fstream, filename, NULL,
622                                 svn_io_file_del_on_pool_cleanup,
623                                 result_pool, scratch_pool));
624
625  err = svn_ra_get_file(fb->session, path, base_revision, fstream, NULL, NULL,
626                         scratch_pool);
627  if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
628    {
629      svn_error_clear(err);
630      SVN_ERR(svn_stream_close(fstream));
631
632      *filename = NULL;
633      return SVN_NO_ERROR;
634    }
635  else if (err)
636    return svn_error_trace(err);
637
638  SVN_ERR(svn_stream_close(fstream));
639
640  return SVN_NO_ERROR;
641}
642
643static svn_error_t *
644fetch_props_func(apr_hash_t **props,
645                 void *baton,
646                 const char *path,
647                 svn_revnum_t base_revision,
648                 apr_pool_t *result_pool,
649                 apr_pool_t *scratch_pool)
650{
651  struct fetch_baton *fb = baton;
652  svn_node_kind_t node_kind;
653
654  if (! SVN_IS_VALID_REVNUM(base_revision))
655    base_revision = fb->head;
656
657  SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, &node_kind,
658                            scratch_pool));
659
660  if (node_kind == svn_node_file)
661    {
662      SVN_ERR(svn_ra_get_file(fb->session, path, base_revision, NULL, NULL,
663                              props, result_pool));
664    }
665  else if (node_kind == svn_node_dir)
666    {
667      apr_array_header_t *tmp_props;
668
669      SVN_ERR(svn_ra_get_dir2(fb->session, NULL, NULL, props, path,
670                              base_revision, 0 /* Dirent fields */,
671                              result_pool));
672      tmp_props = svn_prop_hash_to_array(*props, result_pool);
673      SVN_ERR(svn_categorize_props(tmp_props, NULL, NULL, &tmp_props,
674                                   result_pool));
675      *props = svn_prop_array_to_hash(tmp_props, result_pool);
676    }
677  else
678    {
679      *props = apr_hash_make(result_pool);
680    }
681
682  return SVN_NO_ERROR;
683}
684
685static svn_error_t *
686fetch_kind_func(svn_node_kind_t *kind,
687                void *baton,
688                const char *path,
689                svn_revnum_t base_revision,
690                apr_pool_t *scratch_pool)
691{
692  struct fetch_baton *fb = baton;
693
694  if (! SVN_IS_VALID_REVNUM(base_revision))
695    base_revision = fb->head;
696
697  SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, kind,
698                             scratch_pool));
699
700  return SVN_NO_ERROR;
701}
702
703static svn_delta_shim_callbacks_t *
704get_shim_callbacks(svn_ra_session_t *session,
705                   svn_revnum_t head,
706                   apr_pool_t *result_pool)
707{
708  svn_delta_shim_callbacks_t *callbacks =
709                            svn_delta_shim_callbacks_default(result_pool);
710  struct fetch_baton *fb = apr_pcalloc(result_pool, sizeof(*fb));
711
712  fb->session = session;
713  fb->head = head;
714
715  callbacks->fetch_props_func = fetch_props_func;
716  callbacks->fetch_kind_func = fetch_kind_func;
717  callbacks->fetch_base_func = fetch_base_func;
718  callbacks->fetch_baton = fb;
719
720  return callbacks;
721}
722
723static svn_error_t *
724execute(const apr_array_header_t *actions,
725        const char *anchor,
726        apr_hash_t *revprops,
727        const char *username,
728        const char *password,
729        const char *config_dir,
730        const apr_array_header_t *config_options,
731        svn_boolean_t non_interactive,
732        svn_boolean_t trust_server_cert,
733        svn_boolean_t no_auth_cache,
734        svn_revnum_t base_revision,
735        apr_pool_t *pool)
736{
737  svn_ra_session_t *session;
738  svn_ra_session_t *aux_session;
739  const char *repos_root;
740  svn_revnum_t head;
741  const svn_delta_editor_t *editor;
742  svn_ra_callbacks2_t *ra_callbacks;
743  void *editor_baton;
744  struct operation root;
745  svn_error_t *err;
746  apr_hash_t *config;
747  svn_config_t *cfg_config;
748  int i;
749
750  SVN_ERR(svn_config_get_config(&config, config_dir, pool));
751  SVN_ERR(svn_cmdline__apply_config_options(config, config_options,
752                                            "svnmucc: ", "--config-option"));
753  cfg_config = svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG);
754
755  if (! svn_hash_gets(revprops, SVN_PROP_REVISION_LOG))
756    {
757      svn_string_t *msg = svn_string_create("", pool);
758
759      /* If we can do so, try to pop up $EDITOR to fetch a log message. */
760      if (non_interactive)
761        {
762          return svn_error_create
763            (SVN_ERR_CL_INSUFFICIENT_ARGS, NULL,
764             _("Cannot invoke editor to get log message "
765               "when non-interactive"));
766        }
767      else
768        {
769          SVN_ERR(svn_cmdline__edit_string_externally(
770                      &msg, NULL, NULL, "", msg, "svnmucc-commit", config,
771                      TRUE, NULL, apr_hash_pool_get(revprops)));
772        }
773
774      svn_hash_sets(revprops, SVN_PROP_REVISION_LOG, msg);
775    }
776
777  SVN_ERR(create_ra_callbacks(&ra_callbacks, username, password, config_dir,
778                              cfg_config, non_interactive, trust_server_cert,
779                              no_auth_cache, pool));
780  SVN_ERR(svn_ra_open4(&session, NULL, anchor, NULL, ra_callbacks,
781                       NULL, config, pool));
782  /* Open, then reparent to avoid AUTHZ errors when opening the reposroot */
783  SVN_ERR(svn_ra_open4(&aux_session, NULL, anchor, NULL, ra_callbacks,
784                       NULL, config, pool));
785  SVN_ERR(svn_ra_get_repos_root2(aux_session, &repos_root, pool));
786  SVN_ERR(svn_ra_reparent(aux_session, repos_root, pool));
787  SVN_ERR(svn_ra_get_latest_revnum(session, &head, pool));
788
789  /* Reparent to ANCHOR's dir, if ANCHOR is not a directory. */
790  {
791    svn_node_kind_t kind;
792
793    SVN_ERR(svn_ra_check_path(aux_session,
794                              svn_uri_skip_ancestor(repos_root, anchor, pool),
795                              head, &kind, pool));
796    if (kind != svn_node_dir)
797      {
798        anchor = svn_uri_dirname(anchor, pool);
799        SVN_ERR(svn_ra_reparent(session, anchor, pool));
800      }
801  }
802
803  if (SVN_IS_VALID_REVNUM(base_revision))
804    {
805      if (base_revision > head)
806        return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL,
807                                 "No such revision %ld (youngest is %ld)",
808                                 base_revision, head);
809      head = base_revision;
810    }
811
812  memset(&root, 0, sizeof(root));
813  root.children = apr_hash_make(pool);
814  root.operation = OP_OPEN;
815  root.kind = svn_node_dir; /* For setting properties */
816  root.prop_mods = apr_hash_make(pool);
817  root.prop_dels = apr_array_make(pool, 1, sizeof(const char *));
818
819  for (i = 0; i < actions->nelts; ++i)
820    {
821      struct action *action = APR_ARRAY_IDX(actions, i, struct action *);
822      const char *path1, *path2;
823      switch (action->action)
824        {
825        case ACTION_MV:
826          path1 = subtract_anchor(anchor, action->path[0], pool);
827          path2 = subtract_anchor(anchor, action->path[1], pool);
828          SVN_ERR(build(ACTION_RM, path1, NULL,
829                        SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
830                        session, &root, pool));
831          SVN_ERR(build(ACTION_CP, path2, action->path[0],
832                        head, NULL, NULL, NULL, head, anchor,
833                        session, &root, pool));
834          break;
835        case ACTION_CP:
836          path2 = subtract_anchor(anchor, action->path[1], pool);
837          if (action->rev == SVN_INVALID_REVNUM)
838            action->rev = head;
839          SVN_ERR(build(ACTION_CP, path2, action->path[0],
840                        action->rev, NULL, NULL, NULL, head, anchor,
841                        session, &root, pool));
842          break;
843        case ACTION_RM:
844          path1 = subtract_anchor(anchor, action->path[0], pool);
845          SVN_ERR(build(ACTION_RM, path1, NULL,
846                        SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
847                        session, &root, pool));
848          break;
849        case ACTION_MKDIR:
850          path1 = subtract_anchor(anchor, action->path[0], pool);
851          SVN_ERR(build(ACTION_MKDIR, path1, action->path[0],
852                        SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
853                        session, &root, pool));
854          break;
855        case ACTION_PUT:
856          path1 = subtract_anchor(anchor, action->path[0], pool);
857          SVN_ERR(build(ACTION_PUT, path1, action->path[0],
858                        SVN_INVALID_REVNUM, NULL, NULL, action->path[1],
859                        head, anchor, session, &root, pool));
860          break;
861        case ACTION_PROPSET:
862        case ACTION_PROPDEL:
863          path1 = subtract_anchor(anchor, action->path[0], pool);
864          SVN_ERR(build(action->action, path1, action->path[0],
865                        SVN_INVALID_REVNUM,
866                        action->prop_name, action->prop_value,
867                        NULL, head, anchor, session, &root, pool));
868          break;
869        case ACTION_PROPSETF:
870        default:
871          SVN_ERR_MALFUNCTION_NO_RETURN();
872        }
873    }
874
875  SVN_ERR(svn_ra__register_editor_shim_callbacks(session,
876                            get_shim_callbacks(aux_session, head, pool)));
877  SVN_ERR(svn_ra_get_commit_editor3(session, &editor, &editor_baton, revprops,
878                                    commit_callback, NULL, NULL, FALSE, pool));
879
880  SVN_ERR(editor->open_root(editor_baton, head, pool, &root.baton));
881  err = change_props(editor, root.baton, &root, pool);
882  if (!err)
883    err = drive(&root, head, editor, pool);
884  if (!err)
885    err = editor->close_directory(root.baton, pool);
886  if (!err)
887    err = editor->close_edit(editor_baton, pool);
888
889  if (err)
890    err = svn_error_compose_create(err,
891                                   editor->abort_edit(editor_baton, pool));
892
893  return err;
894}
895
896static svn_error_t *
897read_propvalue_file(const svn_string_t **value_p,
898                    const char *filename,
899                    apr_pool_t *pool)
900{
901  svn_stringbuf_t *value;
902  apr_pool_t *scratch_pool = svn_pool_create(pool);
903
904  SVN_ERR(svn_stringbuf_from_file2(&value, filename, scratch_pool));
905  *value_p = svn_string_create_from_buf(value, pool);
906  svn_pool_destroy(scratch_pool);
907  return SVN_NO_ERROR;
908}
909
910/* Perform the typical suite of manipulations for user-provided URLs
911   on URL, returning the result (allocated from POOL): IRI-to-URI
912   conversion, auto-escaping, and canonicalization. */
913static const char *
914sanitize_url(const char *url,
915             apr_pool_t *pool)
916{
917  url = svn_path_uri_from_iri(url, pool);
918  url = svn_path_uri_autoescape(url, pool);
919  return svn_uri_canonicalize(url, pool);
920}
921
922static void
923usage(apr_pool_t *pool, int exit_val)
924{
925  FILE *stream = exit_val == EXIT_SUCCESS ? stdout : stderr;
926  svn_error_clear(svn_cmdline_fputs(
927    _("Subversion multiple URL command client\n"
928      "usage: svnmucc ACTION...\n"
929      "\n"
930      "  Perform one or more Subversion repository URL-based ACTIONs, committing\n"
931      "  the result as a (single) new revision.\n"
932      "\n"
933      "Actions:\n"
934      "  cp REV SRC-URL DST-URL : copy SRC-URL@REV to DST-URL\n"
935      "  mkdir URL              : create new directory URL\n"
936      "  mv SRC-URL DST-URL     : move SRC-URL to DST-URL\n"
937      "  rm URL                 : delete URL\n"
938      "  put SRC-FILE URL       : add or modify file URL with contents copied from\n"
939      "                           SRC-FILE (use \"-\" to read from standard input)\n"
940      "  propset NAME VALUE URL : set property NAME on URL to VALUE\n"
941      "  propsetf NAME FILE URL : set property NAME on URL to value read from FILE\n"
942      "  propdel NAME URL       : delete property NAME from URL\n"
943      "\n"
944      "Valid options:\n"
945      "  -h, -? [--help]        : display this text\n"
946      "  -m [--message] ARG     : use ARG as a log message\n"
947      "  -F [--file] ARG        : read log message from file ARG\n"
948      "  -u [--username] ARG    : commit the changes as username ARG\n"
949      "  -p [--password] ARG    : use ARG as the password\n"
950      "  -U [--root-url] ARG    : interpret all action URLs relative to ARG\n"
951      "  -r [--revision] ARG    : use revision ARG as baseline for changes\n"
952      "  --with-revprop ARG     : set revision property in the following format:\n"
953      "                               NAME[=VALUE]\n"
954      "  --non-interactive      : do no interactive prompting (default is to\n"
955      "                           prompt only if standard input is a terminal)\n"
956      "  --force-interactive    : do interactive prompting even if standard\n"
957      "                           input is not a terminal\n"
958      "  --trust-server-cert    : accept SSL server certificates from unknown\n"
959      "                           certificate authorities without prompting (but\n"
960      "                           only with '--non-interactive')\n"
961      "  -X [--extra-args] ARG  : append arguments from file ARG (one per line;\n"
962      "                           use \"-\" to read from standard input)\n"
963      "  --config-dir ARG       : use ARG to override the config directory\n"
964      "  --config-option ARG    : use ARG to override a configuration option\n"
965      "  --no-auth-cache        : do not cache authentication tokens\n"
966      "  --version              : print version information\n"),
967                  stream, pool));
968  svn_pool_destroy(pool);
969  exit(exit_val);
970}
971
972static void
973insufficient(apr_pool_t *pool)
974{
975  handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
976                                "insufficient arguments"),
977               pool);
978}
979
980static svn_error_t *
981display_version(apr_getopt_t *os, apr_pool_t *pool)
982{
983  const char *ra_desc_start
984    = "The following repository access (RA) modules are available:\n\n";
985  svn_stringbuf_t *version_footer;
986
987  version_footer = svn_stringbuf_create(ra_desc_start, pool);
988  SVN_ERR(svn_ra_print_modules(version_footer, pool));
989
990  SVN_ERR(svn_opt_print_help4(os, "svnmucc", TRUE, FALSE, FALSE,
991                              version_footer->data,
992                              NULL, NULL, NULL, NULL, NULL, pool));
993
994  return SVN_NO_ERROR;
995}
996
997/* Return an error about the mutual exclusivity of the -m, -F, and
998   --with-revprop=svn:log command-line options. */
999static svn_error_t *
1000mutually_exclusive_logs_error(void)
1001{
1002  return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1003                          _("--message (-m), --file (-F), and "
1004                            "--with-revprop=svn:log are mutually "
1005                            "exclusive"));
1006}
1007
1008/* Ensure that the REVPROPS hash contains a command-line-provided log
1009   message, if any, and that there was but one source of such a thing
1010   provided on that command-line.  */
1011static svn_error_t *
1012sanitize_log_sources(apr_hash_t *revprops,
1013                     const char *message,
1014                     svn_stringbuf_t *filedata)
1015{
1016  apr_pool_t *hash_pool = apr_hash_pool_get(revprops);
1017
1018  /* If we already have a log message in the revprop hash, then just
1019     make sure the user didn't try to also use -m or -F.  Otherwise,
1020     we need to consult -m or -F to find a log message, if any. */
1021  if (svn_hash_gets(revprops, SVN_PROP_REVISION_LOG))
1022    {
1023      if (filedata || message)
1024        return mutually_exclusive_logs_error();
1025    }
1026  else if (filedata)
1027    {
1028      if (message)
1029        return mutually_exclusive_logs_error();
1030
1031      SVN_ERR(svn_utf_cstring_to_utf8(&message, filedata->data, hash_pool));
1032      svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
1033                    svn_stringbuf__morph_into_string(filedata));
1034    }
1035  else if (message)
1036    {
1037      svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
1038                    svn_string_create(message, hash_pool));
1039    }
1040
1041  return SVN_NO_ERROR;
1042}
1043
1044int
1045main(int argc, const char **argv)
1046{
1047  apr_pool_t *pool = init("svnmucc");
1048  apr_array_header_t *actions = apr_array_make(pool, 1,
1049                                               sizeof(struct action *));
1050  const char *anchor = NULL;
1051  svn_error_t *err = SVN_NO_ERROR;
1052  apr_getopt_t *opts;
1053  enum {
1054    config_dir_opt = SVN_OPT_FIRST_LONGOPT_ID,
1055    config_inline_opt,
1056    no_auth_cache_opt,
1057    version_opt,
1058    with_revprop_opt,
1059    non_interactive_opt,
1060    force_interactive_opt,
1061    trust_server_cert_opt
1062  };
1063  static const apr_getopt_option_t options[] = {
1064    {"message", 'm', 1, ""},
1065    {"file", 'F', 1, ""},
1066    {"username", 'u', 1, ""},
1067    {"password", 'p', 1, ""},
1068    {"root-url", 'U', 1, ""},
1069    {"revision", 'r', 1, ""},
1070    {"with-revprop",  with_revprop_opt, 1, ""},
1071    {"extra-args", 'X', 1, ""},
1072    {"help", 'h', 0, ""},
1073    {NULL, '?', 0, ""},
1074    {"non-interactive", non_interactive_opt, 0, ""},
1075    {"force-interactive", force_interactive_opt, 0, ""},
1076    {"trust-server-cert", trust_server_cert_opt, 0, ""},
1077    {"config-dir", config_dir_opt, 1, ""},
1078    {"config-option",  config_inline_opt, 1, ""},
1079    {"no-auth-cache",  no_auth_cache_opt, 0, ""},
1080    {"version", version_opt, 0, ""},
1081    {NULL, 0, 0, NULL}
1082  };
1083  const char *message = NULL;
1084  svn_stringbuf_t *filedata = NULL;
1085  const char *username = NULL, *password = NULL;
1086  const char *root_url = NULL, *extra_args_file = NULL;
1087  const char *config_dir = NULL;
1088  apr_array_header_t *config_options;
1089  svn_boolean_t non_interactive = FALSE;
1090  svn_boolean_t force_interactive = FALSE;
1091  svn_boolean_t trust_server_cert = FALSE;
1092  svn_boolean_t no_auth_cache = FALSE;
1093  svn_revnum_t base_revision = SVN_INVALID_REVNUM;
1094  apr_array_header_t *action_args;
1095  apr_hash_t *revprops = apr_hash_make(pool);
1096  int i;
1097
1098  config_options = apr_array_make(pool, 0,
1099                                  sizeof(svn_cmdline__config_argument_t*));
1100
1101  apr_getopt_init(&opts, pool, argc, argv);
1102  opts->interleave = 1;
1103  while (1)
1104    {
1105      int opt;
1106      const char *arg;
1107      const char *opt_arg;
1108
1109      apr_status_t status = apr_getopt_long(opts, options, &opt, &arg);
1110      if (APR_STATUS_IS_EOF(status))
1111        break;
1112      if (status != APR_SUCCESS)
1113        handle_error(svn_error_wrap_apr(status, "getopt failure"), pool);
1114      switch(opt)
1115        {
1116        case 'm':
1117          err = svn_utf_cstring_to_utf8(&message, arg, pool);
1118          if (err)
1119            handle_error(err, pool);
1120          break;
1121        case 'F':
1122          {
1123            const char *arg_utf8;
1124            err = svn_utf_cstring_to_utf8(&arg_utf8, arg, pool);
1125            if (! err)
1126              err = svn_stringbuf_from_file2(&filedata, arg, pool);
1127            if (err)
1128              handle_error(err, pool);
1129          }
1130          break;
1131        case 'u':
1132          username = apr_pstrdup(pool, arg);
1133          break;
1134        case 'p':
1135          password = apr_pstrdup(pool, arg);
1136          break;
1137        case 'U':
1138          err = svn_utf_cstring_to_utf8(&root_url, arg, pool);
1139          if (err)
1140            handle_error(err, pool);
1141          if (! svn_path_is_url(root_url))
1142            handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1143                                           "'%s' is not a URL\n", root_url),
1144                         pool);
1145          root_url = sanitize_url(root_url, pool);
1146          break;
1147        case 'r':
1148          {
1149            char *digits_end = NULL;
1150            base_revision = strtol(arg, &digits_end, 10);
1151            if ((! SVN_IS_VALID_REVNUM(base_revision))
1152                || (! digits_end)
1153                || *digits_end)
1154              handle_error(svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR,
1155                                            NULL, "Invalid revision number"),
1156                           pool);
1157          }
1158          break;
1159        case with_revprop_opt:
1160          err = svn_opt_parse_revprop(&revprops, arg, pool);
1161          if (err != SVN_NO_ERROR)
1162            handle_error(err, pool);
1163          break;
1164        case 'X':
1165          extra_args_file = apr_pstrdup(pool, arg);
1166          break;
1167        case non_interactive_opt:
1168          non_interactive = TRUE;
1169          break;
1170        case force_interactive_opt:
1171          force_interactive = TRUE;
1172          break;
1173        case trust_server_cert_opt:
1174          trust_server_cert = TRUE;
1175          break;
1176        case config_dir_opt:
1177          err = svn_utf_cstring_to_utf8(&config_dir, arg, pool);
1178          if (err)
1179            handle_error(err, pool);
1180          break;
1181        case config_inline_opt:
1182          err = svn_utf_cstring_to_utf8(&opt_arg, arg, pool);
1183          if (err)
1184            handle_error(err, pool);
1185
1186          err = svn_cmdline__parse_config_option(config_options, opt_arg,
1187                                                 pool);
1188          if (err)
1189            handle_error(err, pool);
1190          break;
1191        case no_auth_cache_opt:
1192          no_auth_cache = TRUE;
1193          break;
1194        case version_opt:
1195          SVN_INT_ERR(display_version(opts, pool));
1196          exit(EXIT_SUCCESS);
1197          break;
1198        case 'h':
1199        case '?':
1200          usage(pool, EXIT_SUCCESS);
1201          break;
1202        }
1203    }
1204
1205  if (non_interactive && force_interactive)
1206    {
1207      err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1208                             _("--non-interactive and --force-interactive "
1209                               "are mutually exclusive"));
1210      return svn_cmdline_handle_exit_error(err, pool, "svnmucc: ");
1211    }
1212  else
1213    non_interactive = !svn_cmdline__be_interactive(non_interactive,
1214                                                   force_interactive);
1215
1216  if (trust_server_cert && !non_interactive)
1217    {
1218      err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1219                             _("--trust-server-cert requires "
1220                               "--non-interactive"));
1221      return svn_cmdline_handle_exit_error(err, pool, "svnmucc: ");
1222    }
1223
1224  /* Make sure we have a log message to use. */
1225  err = sanitize_log_sources(revprops, message, filedata);
1226  if (err)
1227    handle_error(err, pool);
1228
1229  /* Copy the rest of our command-line arguments to an array,
1230     UTF-8-ing them along the way. */
1231  action_args = apr_array_make(pool, opts->argc, sizeof(const char *));
1232  while (opts->ind < opts->argc)
1233    {
1234      const char *arg = opts->argv[opts->ind++];
1235      if ((err = svn_utf_cstring_to_utf8(&(APR_ARRAY_PUSH(action_args,
1236                                                          const char *)),
1237                                         arg, pool)))
1238        handle_error(err, pool);
1239    }
1240
1241  /* If there are extra arguments in a supplementary file, tack those
1242     on, too (again, in UTF8 form). */
1243  if (extra_args_file)
1244    {
1245      const char *extra_args_file_utf8;
1246      svn_stringbuf_t *contents, *contents_utf8;
1247
1248      err = svn_utf_cstring_to_utf8(&extra_args_file_utf8,
1249                                    extra_args_file, pool);
1250      if (! err)
1251        err = svn_stringbuf_from_file2(&contents, extra_args_file_utf8, pool);
1252      if (! err)
1253        err = svn_utf_stringbuf_to_utf8(&contents_utf8, contents, pool);
1254      if (err)
1255        handle_error(err, pool);
1256      svn_cstring_split_append(action_args, contents_utf8->data, "\n\r",
1257                               FALSE, pool);
1258    }
1259
1260  /* Now, we iterate over the combined set of arguments -- our actions. */
1261  for (i = 0; i < action_args->nelts; )
1262    {
1263      int j, num_url_args;
1264      const char *action_string = APR_ARRAY_IDX(action_args, i, const char *);
1265      struct action *action = apr_pcalloc(pool, sizeof(*action));
1266
1267      /* First, parse the action. */
1268      if (! strcmp(action_string, "mv"))
1269        action->action = ACTION_MV;
1270      else if (! strcmp(action_string, "cp"))
1271        action->action = ACTION_CP;
1272      else if (! strcmp(action_string, "mkdir"))
1273        action->action = ACTION_MKDIR;
1274      else if (! strcmp(action_string, "rm"))
1275        action->action = ACTION_RM;
1276      else if (! strcmp(action_string, "put"))
1277        action->action = ACTION_PUT;
1278      else if (! strcmp(action_string, "propset"))
1279        action->action = ACTION_PROPSET;
1280      else if (! strcmp(action_string, "propsetf"))
1281        action->action = ACTION_PROPSETF;
1282      else if (! strcmp(action_string, "propdel"))
1283        action->action = ACTION_PROPDEL;
1284      else if (! strcmp(action_string, "?") || ! strcmp(action_string, "h")
1285               || ! strcmp(action_string, "help"))
1286        usage(pool, EXIT_SUCCESS);
1287      else
1288        handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1289                                       "'%s' is not an action\n",
1290                                       action_string), pool);
1291      if (++i == action_args->nelts)
1292        insufficient(pool);
1293
1294      /* For copies, there should be a revision number next. */
1295      if (action->action == ACTION_CP)
1296        {
1297          const char *rev_str = APR_ARRAY_IDX(action_args, i, const char *);
1298          if (strcmp(rev_str, "head") == 0)
1299            action->rev = SVN_INVALID_REVNUM;
1300          else if (strcmp(rev_str, "HEAD") == 0)
1301            action->rev = SVN_INVALID_REVNUM;
1302          else
1303            {
1304              char *end;
1305
1306              while (*rev_str == 'r')
1307                ++rev_str;
1308
1309              action->rev = strtol(rev_str, &end, 0);
1310              if (*end)
1311                handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1312                                               "'%s' is not a revision\n",
1313                                               rev_str), pool);
1314            }
1315          if (++i == action_args->nelts)
1316            insufficient(pool);
1317        }
1318      else
1319        {
1320          action->rev = SVN_INVALID_REVNUM;
1321        }
1322
1323      /* For puts, there should be a local file next. */
1324      if (action->action == ACTION_PUT)
1325        {
1326          action->path[1] =
1327            svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
1328                                                    const char *), pool);
1329          if (++i == action_args->nelts)
1330            insufficient(pool);
1331        }
1332
1333      /* For propset, propsetf, and propdel, a property name (and
1334         maybe a property value or file which contains one) comes next. */
1335      if ((action->action == ACTION_PROPSET)
1336          || (action->action == ACTION_PROPSETF)
1337          || (action->action == ACTION_PROPDEL))
1338        {
1339          action->prop_name = APR_ARRAY_IDX(action_args, i, const char *);
1340          if (++i == action_args->nelts)
1341            insufficient(pool);
1342
1343          if (action->action == ACTION_PROPDEL)
1344            {
1345              action->prop_value = NULL;
1346            }
1347          else if (action->action == ACTION_PROPSET)
1348            {
1349              action->prop_value =
1350                svn_string_create(APR_ARRAY_IDX(action_args, i,
1351                                                const char *), pool);
1352              if (++i == action_args->nelts)
1353                insufficient(pool);
1354            }
1355          else
1356            {
1357              const char *propval_file =
1358                svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
1359                                                        const char *), pool);
1360
1361              if (++i == action_args->nelts)
1362                insufficient(pool);
1363
1364              err = read_propvalue_file(&(action->prop_value),
1365                                        propval_file, pool);
1366              if (err)
1367                handle_error(err, pool);
1368
1369              action->action = ACTION_PROPSET;
1370            }
1371
1372          if (action->prop_value
1373              && svn_prop_needs_translation(action->prop_name))
1374            {
1375              svn_string_t *translated_value;
1376              err = svn_subst_translate_string2(&translated_value, NULL,
1377                                                NULL, action->prop_value, NULL,
1378                                                FALSE, pool, pool);
1379              if (err)
1380                handle_error(
1381                    svn_error_quick_wrap(err,
1382                                         "Error normalizing property value"),
1383                    pool);
1384              action->prop_value = translated_value;
1385            }
1386        }
1387
1388      /* How many URLs does this action expect? */
1389      if (action->action == ACTION_RM
1390          || action->action == ACTION_MKDIR
1391          || action->action == ACTION_PUT
1392          || action->action == ACTION_PROPSET
1393          || action->action == ACTION_PROPSETF /* shouldn't see this one */
1394          || action->action == ACTION_PROPDEL)
1395        num_url_args = 1;
1396      else
1397        num_url_args = 2;
1398
1399      /* Parse the required number of URLs. */
1400      for (j = 0; j < num_url_args; ++j)
1401        {
1402          const char *url = APR_ARRAY_IDX(action_args, i, const char *);
1403
1404          /* If there's a ROOT_URL, we expect URL to be a path
1405             relative to ROOT_URL (and we build a full url from the
1406             combination of the two).  Otherwise, it should be a full
1407             url. */
1408          if (! svn_path_is_url(url))
1409            {
1410              if (! root_url)
1411                handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1412                                               "'%s' is not a URL, and "
1413                                               "--root-url (-U) not provided\n",
1414                                               url), pool);
1415              /* ### These relpaths are already URI-encoded. */
1416              url = apr_pstrcat(pool, root_url, "/",
1417                                svn_relpath_canonicalize(url, pool),
1418                                (char *)NULL);
1419            }
1420          url = sanitize_url(url, pool);
1421          action->path[j] = url;
1422
1423          /* The first URL arguments to 'cp', 'pd', 'ps' could be the anchor,
1424             but the other URLs should be children of the anchor. */
1425          if (! (action->action == ACTION_CP && j == 0)
1426              && action->action != ACTION_PROPDEL
1427              && action->action != ACTION_PROPSET
1428              && action->action != ACTION_PROPSETF)
1429            url = svn_uri_dirname(url, pool);
1430          if (! anchor)
1431            anchor = url;
1432          else
1433            {
1434              anchor = svn_uri_get_longest_ancestor(anchor, url, pool);
1435              if (!anchor || !anchor[0])
1436                handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1437                                               "URLs in the action list do not "
1438                                               "share a common ancestor"),
1439                             pool);
1440            }
1441
1442          if ((++i == action_args->nelts) && (j + 1 < num_url_args))
1443            insufficient(pool);
1444        }
1445      APR_ARRAY_PUSH(actions, struct action *) = action;
1446    }
1447
1448  if (! actions->nelts)
1449    usage(pool, EXIT_FAILURE);
1450
1451  if ((err = execute(actions, anchor, revprops, username, password,
1452                     config_dir, config_options, non_interactive,
1453                     trust_server_cert, no_auth_cache, base_revision, pool)))
1454    {
1455      if (err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive)
1456        err = svn_error_quick_wrap(err,
1457                                   _("Authentication failed and interactive"
1458                                     " prompting is disabled; see the"
1459                                     " --force-interactive option"));
1460      handle_error(err, pool);
1461    }
1462
1463  /* Ensure that stdout is flushed, so the user will see all results. */
1464  svn_error_clear(svn_cmdline_fflush(stdout));
1465
1466  svn_pool_destroy(pool);
1467  return EXIT_SUCCESS;
1468}
1469