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