svnmucc.c revision 251886
198184Sgordon/*
278344Sobrien * svnmucc.c: Subversion Multiple URL Client
3265420Simp *
4156813Sru * ====================================================================
5228541Spjd *    Licensed to the Apache Software Foundation (ASF) under one
6228541Spjd *    or more contributor license agreements.  See the NOTICE file
7228541Spjd *    distributed with this work for additional information
8228541Spjd *    regarding copyright ownership.  The ASF licenses this file
9228541Spjd *    to you under the Apache License, Version 2.0 (the
10228541Spjd *    "License"); you may not use this file except in compliance
11228541Spjd *    with the License.  You may obtain a copy of the License at
12228541Spjd *
13228541Spjd *      http://www.apache.org/licenses/LICENSE-2.0
14228541Spjd *
15228541Spjd *    Unless required by applicable law or agreed to in writing,
16228541Spjd *    software distributed under the License is distributed on an
17228541Spjd *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18228541Spjd *    KIND, either express or implied.  See the License for the
19243752Srwatson *    specific language governing permissions and limitations
20228541Spjd *    under the License.
21256022Sgjb * ====================================================================
22228541Spjd *
23256022Sgjb */
24259682Sgjb
25228541Spjd/*  Multiple URL Command Client
26228541Spjd
27228541Spjd    Combine a list of mv, cp and rm commands on URLs into a single commit.
28255570Strasz
29228541Spjd    How it works: the command line arguments are parsed into an array of
30228541Spjd    action structures.  The action structures are interpreted to build a
31228541Spjd    tree of operation structures.  The tree of operation structures is
32228541Spjd    used to drive an RA commit editor to produce a single commit.
33228541Spjd
34228541Spjd    To build this client, type 'make svnmucc' from the root of your
35228541Spjd    Subversion source directory.
36228541Spjd*/
37228541Spjd
38228541Spjd#include <stdio.h>
39228541Spjd#include <string.h>
40228541Spjd
41273955Sjmg#include <apr_lib.h>
42228541Spjd
43256022Sgjb#include "svn_hash.h"
44228541Spjd#include "svn_client.h"
45228541Spjd#include "svn_cmdline.h"
46228541Spjd#include "svn_config.h"
47279463Srstone#include "svn_error.h"
48228541Spjd#include "svn_path.h"
49228541Spjd#include "svn_pools.h"
50228541Spjd#include "svn_props.h"
51228541Spjd#include "svn_ra.h"
52228541Spjd#include "svn_string.h"
53228541Spjd#include "svn_subst.h"
54228541Spjd#include "svn_utf.h"
55273285Shrs#include "svn_version.h"
56273285Shrs
57273285Shrs#include "private/svn_cmdline_private.h"
58228541Spjd#include "private/svn_ra_private.h"
59228541Spjd#include "private/svn_string_private.h"
60273285Shrs
61228541Spjd#include "svn_private_config.h"
62228541Spjd
63228541Spjdstatic void handle_error(svn_error_t *err, apr_pool_t *pool)
64228541Spjd{
65228541Spjd  if (err)
66228541Spjd    svn_handle_error2(err, stderr, FALSE, "svnmucc: ");
67228541Spjd  svn_error_clear(err);
68228541Spjd  if (pool)
69228541Spjd    svn_pool_destroy(pool);
70228541Spjd  exit(EXIT_FAILURE);
71228541Spjd}
72228541Spjd
73228541Spjdstatic apr_pool_t *
74228541Spjdinit(const char *application)
75228541Spjd{
76228541Spjd  svn_error_t *err;
77228541Spjd  const svn_version_checklist_t checklist[] = {
78228541Spjd    {"svn_client", svn_client_version},
79228541Spjd    {"svn_subr", svn_subr_version},
80228541Spjd    {"svn_ra", svn_ra_version},
81228541Spjd    {NULL, NULL}
82228541Spjd  };
83228541Spjd  SVN_VERSION_DEFINE(my_version);
84228541Spjd
85228541Spjd  if (svn_cmdline_init(application, stderr))
86228541Spjd    exit(EXIT_FAILURE);
87228541Spjd
88228541Spjd  err = svn_ver_check_list(&my_version, checklist);
89228541Spjd  if (err)
90228541Spjd    handle_error(err, NULL);
91228541Spjd
92228541Spjd  return apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
93228541Spjd}
94228541Spjd
95118224Smtmstatic svn_error_t *
96228541Spjdopen_tmp_file(apr_file_t **fp,
97228541Spjd              void *callback_baton,
98228541Spjd              apr_pool_t *pool)
99228541Spjd{
100228541Spjd  /* Open a unique file;  use APR_DELONCLOSE. */
101228541Spjd  return svn_io_open_unique_file3(fp, NULL, NULL, svn_io_file_del_on_close,
102228541Spjd                                  pool, pool);
103228541Spjd}
104228541Spjd
105228541Spjdstatic svn_error_t *
106228541Spjdcreate_ra_callbacks(svn_ra_callbacks2_t **callbacks,
107228541Spjd                    const char *username,
108228541Spjd                    const char *password,
109228541Spjd                    const char *config_dir,
110228541Spjd                    svn_config_t *cfg_config,
111228541Spjd                    svn_boolean_t non_interactive,
112228541Spjd                    svn_boolean_t trust_server_cert,
113228541Spjd                    svn_boolean_t no_auth_cache,
114228541Spjd                    apr_pool_t *pool)
115228541Spjd{
116228541Spjd  SVN_ERR(svn_ra_create_callbacks(callbacks, pool));
117252310Shrs
118252310Shrs  SVN_ERR(svn_cmdline_create_auth_baton(&(*callbacks)->auth_baton,
119228541Spjd                                        non_interactive,
120228541Spjd                                        username, password, config_dir,
121228541Spjd                                        no_auth_cache,
122228541Spjd                                        trust_server_cert,
123153430Siedowse                                        cfg_config, NULL, NULL, pool));
124255809Sdes
125231534Sed  (*callbacks)->open_tmp_file = open_tmp_file;
126228541Spjd
127228541Spjd  return SVN_NO_ERROR;
128228541Spjd}
129228541Spjd
130228541Spjd
131228541Spjd
132228541Spjdstatic svn_error_t *
133228541Spjdcommit_callback(const svn_commit_info_t *commit_info,
134228541Spjd                void *baton,
135228541Spjd                apr_pool_t *pool)
136150490Swollman{
137277736Sngie  SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n",
138277736Sngie                             commit_info->revision,
139277736Sngie                             (commit_info->author
140277736Sngie                              ? commit_info->author : "(no author)"),
141277730Sngie                             commit_info->date));
142277730Sngie  return SVN_NO_ERROR;
143277730Sngie}
144277730Sngie
145278249Sngietypedef enum action_code_t {
146278249Sngie  ACTION_MV,
147278249Sngie  ACTION_MKDIR,
148278249Sngie  ACTION_CP,
149277733Sngie  ACTION_PROPSET,
150277733Sngie  ACTION_PROPSETF,
151277733Sngie  ACTION_PROPDEL,
152277733Sngie  ACTION_PUT,
153277732Sngie  ACTION_RM
154277732Sngie} action_code_t;
155277732Sngie
156277732Sngiestruct operation {
157277732Sngie  enum {
158277728Sngie    OP_OPEN,
159277728Sngie    OP_DELETE,
160277728Sngie    OP_ADD,
161277728Sngie    OP_REPLACE,
162277728Sngie    OP_PROPSET           /* only for files for which no other operation is
163277728Sngie                            occuring; directories are OP_OPEN with non-empty
164271892Sngie                            props */
165271892Sngie  } operation;
166271892Sngie  svn_node_kind_t kind;  /* to copy, mkdir, put or set revprops */
167271892Sngie  svn_revnum_t rev;      /* to copy, valid for add and replace */
168271892Sngie  const char *url;       /* to copy, valid for add and replace */
169219820Sjeff  const char *src_file;  /* for put, the source file for contents */
170219820Sjeff  apr_hash_t *children;  /* const char *path -> struct operation * */
171278249Sngie  apr_hash_t *prop_mods; /* const char *prop_name ->
172278249Sngie                            const svn_string_t *prop_value */
173278249Sngie  apr_array_header_t *prop_dels; /* const char *prop_name deletions */
174278249Sngie  void *baton;           /* as returned by the commit editor */
175277686Sngie};
176277686Sngie
177277686Sngie
178277686Sngie/* An iterator (for use via apr_table_do) which sets node properties.
179271892Sngie   REC is a pointer to a struct driver_state. */
180271892Sngiestatic svn_error_t *
181150490Swollmanchange_props(const svn_delta_editor_t *editor,
182150490Swollman             void *baton,
183277678Sngie             struct operation *child,
184277678Sngie             apr_pool_t *pool)
185277678Sngie{
186277678Sngie  apr_pool_t *iterpool = svn_pool_create(pool);
187278249Sngie
188278249Sngie  if (child->prop_dels)
189278249Sngie    {
190278249Sngie      int i;
191277725Sngie      for (i = 0; i < child->prop_dels->nelts; i++)
192277725Sngie        {
193277725Sngie          const char *prop_name;
194277725Sngie
195278249Sngie          svn_pool_clear(iterpool);
196278282Sngie          prop_name = APR_ARRAY_IDX(child->prop_dels, i, const char *);
197278249Sngie          if (child->kind == svn_node_dir)
198278249Sngie            SVN_ERR(editor->change_dir_prop(baton, prop_name,
199277675Sngie                                            NULL, iterpool));
200277675Sngie          else
201277675Sngie            SVN_ERR(editor->change_file_prop(baton, prop_name,
202277675Sngie                                             NULL, iterpool));
203277675Sngie        }
204277739Sngie    }
205277739Sngie  if (apr_hash_count(child->prop_mods))
206277739Sngie    {
207277739Sngie      apr_hash_index_t *hi;
208278249Sngie      for (hi = apr_hash_first(pool, child->prop_mods);
209278249Sngie           hi; hi = apr_hash_next(hi))
210278249Sngie        {
211278249Sngie          const char *propname = svn__apr_hash_index_key(hi);
212278249Sngie          const svn_string_t *val = svn__apr_hash_index_val(hi);
213277731Sngie
214277731Sngie          svn_pool_clear(iterpool);
215277731Sngie          if (child->kind == svn_node_dir)
216277731Sngie            SVN_ERR(editor->change_dir_prop(baton, propname, val, iterpool));
217273285Shrs          else
218277741Sngie            SVN_ERR(editor->change_file_prop(baton, propname, val, iterpool));
219277741Sngie        }
220273285Shrs    }
221273285Shrs
222273285Shrs  svn_pool_destroy(iterpool);
223273285Shrs  return SVN_NO_ERROR;
224273285Shrs}
225273285Shrs
226278249Sngie
227278249Sngie/* Drive EDITOR to affect the change represented by OPERATION.  HEAD
228278249Sngie   is the last-known youngest revision in the repository. */
229278249Sngiestatic svn_error_t *
230278249Sngiedrive(struct operation *operation,
231278249Sngie      svn_revnum_t head,
232278249Sngie      const svn_delta_editor_t *editor,
233278249Sngie      apr_pool_t *pool)
234278249Sngie{
235278249Sngie  apr_pool_t *subpool = svn_pool_create(pool);
236278249Sngie  apr_hash_index_t *hi;
237278249Sngie
238271892Sngie  for (hi = apr_hash_first(pool, operation->children);
239271892Sngie       hi; hi = apr_hash_next(hi))
240206706Srpaulo    {
241206706Srpaulo      const char *key = svn__apr_hash_index_key(hi);
242272043Sngie      struct operation *child = svn__apr_hash_index_val(hi);
243272043Sngie      void *file_baton = NULL;
244272043Sngie
245272043Sngie      svn_pool_clear(subpool);
246271892Sngie
247271892Sngie      /* Deletes and replacements are simple -- delete something. */
248259682Sgjb      if (child->operation == OP_DELETE || child->operation == OP_REPLACE)
249259682Sgjb        {
250271895Sngie          SVN_ERR(editor->delete_entry(key, head, operation->baton, subpool));
251271895Sngie        }
252271895Sngie      /* Opens could be for directories or files. */
253271895Sngie      if (child->operation == OP_OPEN || child->operation == OP_PROPSET)
254271893Sngie        {
255271893Sngie          if (child->kind == svn_node_dir)
256271893Sngie            {
257271893Sngie              SVN_ERR(editor->open_directory(key, operation->baton, head,
258278249Sngie                                             subpool, &child->baton));
259278249Sngie            }
260278249Sngie          else
261278249Sngie            {
262278249Sngie              SVN_ERR(editor->open_file(key, operation->baton, head,
263278249Sngie                                        subpool, &file_baton));
264278249Sngie            }
265278249Sngie        }
266278249Sngie      /* Adds and replacements could also be for directories or files. */
267278249Sngie      if (child->operation == OP_ADD || child->operation == OP_REPLACE)
268278249Sngie        {
269278249Sngie          if (child->kind == svn_node_dir)
270255809Sdes            {
271255809Sdes              SVN_ERR(editor->add_directory(key, operation->baton,
272255809Sdes                                            child->url, child->rev,
273255809Sdes                                            subpool, &child->baton));
274231534Sed            }
275231534Sed          else
276231534Sed            {
277231534Sed              SVN_ERR(editor->add_file(key, operation->baton, child->url,
278278249Sngie                                       child->rev, subpool, &file_baton));
279278249Sngie            }
280278249Sngie        }
281278249Sngie      /* If there's a source file and an open file baton, we get to
282277740Sngie         change textual contents. */
283277740Sngie      if ((child->src_file) && (file_baton))
284277740Sngie        {
285277740Sngie          svn_txdelta_window_handler_t handler;
286277740Sngie          void *handler_baton;
28778344Sobrien          svn_stream_t *contents;
28878344Sobrien
28978344Sobrien          SVN_ERR(editor->apply_textdelta(file_baton, NULL, subpool,
29078344Sobrien                                          &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. */
334static struct operation *
335get_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). */
355static const char *
356subtract_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). */
378static svn_error_t *
379build(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
577struct 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
599struct fetch_baton
600{
601  svn_ra_session_t *session;
602  svn_revnum_t head;
603};
604
605static svn_error_t *
606fetch_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
642static svn_error_t *
643fetch_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
684static svn_error_t *
685fetch_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
702static svn_delta_shim_callbacks_t *
703get_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
722static svn_error_t *
723execute(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
895static svn_error_t *
896read_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. */
912static const char *
913sanitize_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
921static void
922usage(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
971static void
972insufficient(apr_pool_t *pool)
973{
974  handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
975                                "insufficient arguments"),
976               pool);
977}
978
979static svn_error_t *
980display_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. */
998static svn_error_t *
999mutually_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.  */
1010static svn_error_t *
1011sanitize_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
1043int
1044main(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}
1461