]> CyberLeo.Net >> Repos - FreeBSD/releng/10.2.git/blob - contrib/subversion/subversion/svnmucc/svnmucc.c
- Copy stable/10@285827 to releng/10.2 in preparation for 10.2-RC1
[FreeBSD/releng/10.2.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 #include "private/svn_subr_private.h"
61
62 #include "svn_private_config.h"
63
64 static 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
74 static apr_pool_t *
75 init(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
96 static svn_error_t *
97 open_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
106 static svn_error_t *
107 create_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
133 static svn_error_t *
134 commit_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
146 typedef 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
157 struct 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. */
181 static svn_error_t *
182 change_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. */
230 static svn_error_t *
231 drive(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. */
335 static struct operation *
336 get_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). */
356 static const char *
357 subtract_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). */
379 static svn_error_t *
380 build(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
578 struct 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
600 struct fetch_baton
601 {
602   svn_ra_session_t *session;
603   svn_revnum_t head;
604 };
605
606 static svn_error_t *
607 fetch_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
643 static svn_error_t *
644 fetch_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
685 static svn_error_t *
686 fetch_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
703 static svn_delta_shim_callbacks_t *
704 get_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
723 static svn_error_t *
724 execute(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
896 static svn_error_t *
897 read_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. */
913 static const char *
914 sanitize_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
922 static void
923 usage(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
972 static void
973 insufficient(apr_pool_t *pool)
974 {
975   handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
976                                 "insufficient arguments"),
977                pool);
978 }
979
980 static svn_error_t *
981 display_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. */
999 static svn_error_t *
1000 mutually_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.  */
1011 static svn_error_t *
1012 sanitize_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
1044 int
1045 main(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 }