1251881Speter/*
2251881Speter * svnmucc.c: Subversion Multiple URL Client
3251881Speter *
4251881Speter * ====================================================================
5251881Speter *    Licensed to the Apache Software Foundation (ASF) under one
6251881Speter *    or more contributor license agreements.  See the NOTICE file
7251881Speter *    distributed with this work for additional information
8251881Speter *    regarding copyright ownership.  The ASF licenses this file
9251881Speter *    to you under the Apache License, Version 2.0 (the
10251881Speter *    "License"); you may not use this file except in compliance
11251881Speter *    with the License.  You may obtain a copy of the License at
12251881Speter *
13251881Speter *      http://www.apache.org/licenses/LICENSE-2.0
14251881Speter *
15251881Speter *    Unless required by applicable law or agreed to in writing,
16251881Speter *    software distributed under the License is distributed on an
17251881Speter *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18251881Speter *    KIND, either express or implied.  See the License for the
19251881Speter *    specific language governing permissions and limitations
20251881Speter *    under the License.
21251881Speter * ====================================================================
22251881Speter *
23251881Speter */
24251881Speter
25251881Speter/*  Multiple URL Command Client
26251881Speter
27251881Speter    Combine a list of mv, cp and rm commands on URLs into a single commit.
28251881Speter
29251881Speter    How it works: the command line arguments are parsed into an array of
30251881Speter    action structures.  The action structures are interpreted to build a
31251881Speter    tree of operation structures.  The tree of operation structures is
32251881Speter    used to drive an RA commit editor to produce a single commit.
33251881Speter
34251881Speter    To build this client, type 'make svnmucc' from the root of your
35251881Speter    Subversion source directory.
36251881Speter*/
37251881Speter
38251881Speter#include <stdio.h>
39251881Speter#include <string.h>
40251881Speter
41251881Speter#include <apr_lib.h>
42251881Speter
43251881Speter#include "svn_hash.h"
44251881Speter#include "svn_client.h"
45251881Speter#include "svn_cmdline.h"
46251881Speter#include "svn_config.h"
47251881Speter#include "svn_error.h"
48251881Speter#include "svn_path.h"
49251881Speter#include "svn_pools.h"
50251881Speter#include "svn_props.h"
51251881Speter#include "svn_ra.h"
52251881Speter#include "svn_string.h"
53251881Speter#include "svn_subst.h"
54251881Speter#include "svn_utf.h"
55251881Speter#include "svn_version.h"
56251881Speter
57251881Speter#include "private/svn_cmdline_private.h"
58251881Speter#include "private/svn_ra_private.h"
59251881Speter#include "private/svn_string_private.h"
60262253Speter#include "private/svn_subr_private.h"
61251881Speter
62251881Speter#include "svn_private_config.h"
63251881Speter
64251881Speterstatic void handle_error(svn_error_t *err, apr_pool_t *pool)
65251881Speter{
66251881Speter  if (err)
67251881Speter    svn_handle_error2(err, stderr, FALSE, "svnmucc: ");
68251881Speter  svn_error_clear(err);
69251881Speter  if (pool)
70251881Speter    svn_pool_destroy(pool);
71251881Speter  exit(EXIT_FAILURE);
72251881Speter}
73251881Speter
74251881Speterstatic apr_pool_t *
75251881Speterinit(const char *application)
76251881Speter{
77251881Speter  svn_error_t *err;
78251881Speter  const svn_version_checklist_t checklist[] = {
79251881Speter    {"svn_client", svn_client_version},
80251881Speter    {"svn_subr", svn_subr_version},
81251881Speter    {"svn_ra", svn_ra_version},
82251881Speter    {NULL, NULL}
83251881Speter  };
84251881Speter  SVN_VERSION_DEFINE(my_version);
85251881Speter
86251881Speter  if (svn_cmdline_init(application, stderr))
87251881Speter    exit(EXIT_FAILURE);
88251881Speter
89262253Speter  err = svn_ver_check_list2(&my_version, checklist, svn_ver_equal);
90251881Speter  if (err)
91251881Speter    handle_error(err, NULL);
92251881Speter
93251881Speter  return apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
94251881Speter}
95251881Speter
96251881Speterstatic svn_error_t *
97251881Speteropen_tmp_file(apr_file_t **fp,
98251881Speter              void *callback_baton,
99251881Speter              apr_pool_t *pool)
100251881Speter{
101251881Speter  /* Open a unique file;  use APR_DELONCLOSE. */
102251881Speter  return svn_io_open_unique_file3(fp, NULL, NULL, svn_io_file_del_on_close,
103251881Speter                                  pool, pool);
104251881Speter}
105251881Speter
106251881Speterstatic svn_error_t *
107251881Spetercreate_ra_callbacks(svn_ra_callbacks2_t **callbacks,
108251881Speter                    const char *username,
109251881Speter                    const char *password,
110251881Speter                    const char *config_dir,
111251881Speter                    svn_config_t *cfg_config,
112251881Speter                    svn_boolean_t non_interactive,
113251881Speter                    svn_boolean_t trust_server_cert,
114251881Speter                    svn_boolean_t no_auth_cache,
115251881Speter                    apr_pool_t *pool)
116251881Speter{
117251881Speter  SVN_ERR(svn_ra_create_callbacks(callbacks, pool));
118251881Speter
119251881Speter  SVN_ERR(svn_cmdline_create_auth_baton(&(*callbacks)->auth_baton,
120251881Speter                                        non_interactive,
121251881Speter                                        username, password, config_dir,
122251881Speter                                        no_auth_cache,
123251881Speter                                        trust_server_cert,
124251881Speter                                        cfg_config, NULL, NULL, pool));
125251881Speter
126251881Speter  (*callbacks)->open_tmp_file = open_tmp_file;
127251881Speter
128251881Speter  return SVN_NO_ERROR;
129251881Speter}
130251881Speter
131251881Speter
132251881Speter
133251881Speterstatic svn_error_t *
134251881Spetercommit_callback(const svn_commit_info_t *commit_info,
135251881Speter                void *baton,
136251881Speter                apr_pool_t *pool)
137251881Speter{
138251881Speter  SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n",
139251881Speter                             commit_info->revision,
140251881Speter                             (commit_info->author
141251881Speter                              ? commit_info->author : "(no author)"),
142251881Speter                             commit_info->date));
143251881Speter  return SVN_NO_ERROR;
144251881Speter}
145251881Speter
146251881Spetertypedef enum action_code_t {
147251881Speter  ACTION_MV,
148251881Speter  ACTION_MKDIR,
149251881Speter  ACTION_CP,
150251881Speter  ACTION_PROPSET,
151251881Speter  ACTION_PROPSETF,
152251881Speter  ACTION_PROPDEL,
153251881Speter  ACTION_PUT,
154251881Speter  ACTION_RM
155251881Speter} action_code_t;
156251881Speter
157251881Speterstruct operation {
158251881Speter  enum {
159251881Speter    OP_OPEN,
160251881Speter    OP_DELETE,
161251881Speter    OP_ADD,
162251881Speter    OP_REPLACE,
163251881Speter    OP_PROPSET           /* only for files for which no other operation is
164251881Speter                            occuring; directories are OP_OPEN with non-empty
165251881Speter                            props */
166251881Speter  } operation;
167251881Speter  svn_node_kind_t kind;  /* to copy, mkdir, put or set revprops */
168251881Speter  svn_revnum_t rev;      /* to copy, valid for add and replace */
169251881Speter  const char *url;       /* to copy, valid for add and replace */
170251881Speter  const char *src_file;  /* for put, the source file for contents */
171251881Speter  apr_hash_t *children;  /* const char *path -> struct operation * */
172251881Speter  apr_hash_t *prop_mods; /* const char *prop_name ->
173251881Speter                            const svn_string_t *prop_value */
174251881Speter  apr_array_header_t *prop_dels; /* const char *prop_name deletions */
175251881Speter  void *baton;           /* as returned by the commit editor */
176251881Speter};
177251881Speter
178251881Speter
179251881Speter/* An iterator (for use via apr_table_do) which sets node properties.
180251881Speter   REC is a pointer to a struct driver_state. */
181251881Speterstatic svn_error_t *
182251881Speterchange_props(const svn_delta_editor_t *editor,
183251881Speter             void *baton,
184251881Speter             struct operation *child,
185251881Speter             apr_pool_t *pool)
186251881Speter{
187251881Speter  apr_pool_t *iterpool = svn_pool_create(pool);
188251881Speter
189251881Speter  if (child->prop_dels)
190251881Speter    {
191251881Speter      int i;
192251881Speter      for (i = 0; i < child->prop_dels->nelts; i++)
193251881Speter        {
194251881Speter          const char *prop_name;
195251881Speter
196251881Speter          svn_pool_clear(iterpool);
197251881Speter          prop_name = APR_ARRAY_IDX(child->prop_dels, i, const char *);
198251881Speter          if (child->kind == svn_node_dir)
199251881Speter            SVN_ERR(editor->change_dir_prop(baton, prop_name,
200251881Speter                                            NULL, iterpool));
201251881Speter          else
202251881Speter            SVN_ERR(editor->change_file_prop(baton, prop_name,
203251881Speter                                             NULL, iterpool));
204251881Speter        }
205251881Speter    }
206251881Speter  if (apr_hash_count(child->prop_mods))
207251881Speter    {
208251881Speter      apr_hash_index_t *hi;
209251881Speter      for (hi = apr_hash_first(pool, child->prop_mods);
210251881Speter           hi; hi = apr_hash_next(hi))
211251881Speter        {
212251881Speter          const char *propname = svn__apr_hash_index_key(hi);
213251881Speter          const svn_string_t *val = svn__apr_hash_index_val(hi);
214251881Speter
215251881Speter          svn_pool_clear(iterpool);
216251881Speter          if (child->kind == svn_node_dir)
217251881Speter            SVN_ERR(editor->change_dir_prop(baton, propname, val, iterpool));
218251881Speter          else
219251881Speter            SVN_ERR(editor->change_file_prop(baton, propname, val, iterpool));
220251881Speter        }
221251881Speter    }
222251881Speter
223251881Speter  svn_pool_destroy(iterpool);
224251881Speter  return SVN_NO_ERROR;
225251881Speter}
226251881Speter
227251881Speter
228251881Speter/* Drive EDITOR to affect the change represented by OPERATION.  HEAD
229251881Speter   is the last-known youngest revision in the repository. */
230251881Speterstatic svn_error_t *
231251881Speterdrive(struct operation *operation,
232251881Speter      svn_revnum_t head,
233251881Speter      const svn_delta_editor_t *editor,
234251881Speter      apr_pool_t *pool)
235251881Speter{
236251881Speter  apr_pool_t *subpool = svn_pool_create(pool);
237251881Speter  apr_hash_index_t *hi;
238251881Speter
239251881Speter  for (hi = apr_hash_first(pool, operation->children);
240251881Speter       hi; hi = apr_hash_next(hi))
241251881Speter    {
242251881Speter      const char *key = svn__apr_hash_index_key(hi);
243251881Speter      struct operation *child = svn__apr_hash_index_val(hi);
244251881Speter      void *file_baton = NULL;
245251881Speter
246251881Speter      svn_pool_clear(subpool);
247251881Speter
248251881Speter      /* Deletes and replacements are simple -- delete something. */
249251881Speter      if (child->operation == OP_DELETE || child->operation == OP_REPLACE)
250251881Speter        {
251251881Speter          SVN_ERR(editor->delete_entry(key, head, operation->baton, subpool));
252251881Speter        }
253251881Speter      /* Opens could be for directories or files. */
254251881Speter      if (child->operation == OP_OPEN || child->operation == OP_PROPSET)
255251881Speter        {
256251881Speter          if (child->kind == svn_node_dir)
257251881Speter            {
258251881Speter              SVN_ERR(editor->open_directory(key, operation->baton, head,
259251881Speter                                             subpool, &child->baton));
260251881Speter            }
261251881Speter          else
262251881Speter            {
263251881Speter              SVN_ERR(editor->open_file(key, operation->baton, head,
264251881Speter                                        subpool, &file_baton));
265251881Speter            }
266251881Speter        }
267251881Speter      /* Adds and replacements could also be for directories or files. */
268251881Speter      if (child->operation == OP_ADD || child->operation == OP_REPLACE)
269251881Speter        {
270251881Speter          if (child->kind == svn_node_dir)
271251881Speter            {
272251881Speter              SVN_ERR(editor->add_directory(key, operation->baton,
273251881Speter                                            child->url, child->rev,
274251881Speter                                            subpool, &child->baton));
275251881Speter            }
276251881Speter          else
277251881Speter            {
278251881Speter              SVN_ERR(editor->add_file(key, operation->baton, child->url,
279251881Speter                                       child->rev, subpool, &file_baton));
280251881Speter            }
281251881Speter        }
282251881Speter      /* If there's a source file and an open file baton, we get to
283251881Speter         change textual contents. */
284251881Speter      if ((child->src_file) && (file_baton))
285251881Speter        {
286251881Speter          svn_txdelta_window_handler_t handler;
287251881Speter          void *handler_baton;
288251881Speter          svn_stream_t *contents;
289251881Speter
290251881Speter          SVN_ERR(editor->apply_textdelta(file_baton, NULL, subpool,
291251881Speter                                          &handler, &handler_baton));
292251881Speter          if (strcmp(child->src_file, "-") != 0)
293251881Speter            {
294251881Speter              SVN_ERR(svn_stream_open_readonly(&contents, child->src_file,
295251881Speter                                               pool, pool));
296251881Speter            }
297251881Speter          else
298251881Speter            {
299251881Speter              SVN_ERR(svn_stream_for_stdin(&contents, pool));
300251881Speter            }
301251881Speter          SVN_ERR(svn_txdelta_send_stream(contents, handler,
302251881Speter                                          handler_baton, NULL, pool));
303251881Speter        }
304251881Speter      /* If we opened a file, we need to apply outstanding propmods,
305251881Speter         then close it. */
306251881Speter      if (file_baton)
307251881Speter        {
308251881Speter          if (child->kind == svn_node_file)
309251881Speter            {
310251881Speter              SVN_ERR(change_props(editor, file_baton, child, subpool));
311251881Speter            }
312251881Speter          SVN_ERR(editor->close_file(file_baton, NULL, subpool));
313251881Speter        }
314251881Speter      /* If we opened, added, or replaced a directory, we need to
315251881Speter         recurse, apply outstanding propmods, and then close it. */
316251881Speter      if ((child->kind == svn_node_dir)
317251881Speter          && child->operation != OP_DELETE)
318251881Speter        {
319251881Speter          SVN_ERR(change_props(editor, child->baton, child, subpool));
320251881Speter
321251881Speter          SVN_ERR(drive(child, head, editor, subpool));
322251881Speter
323251881Speter          SVN_ERR(editor->close_directory(child->baton, subpool));
324251881Speter        }
325251881Speter    }
326251881Speter  svn_pool_destroy(subpool);
327251881Speter  return SVN_NO_ERROR;
328251881Speter}
329251881Speter
330251881Speter
331251881Speter/* Find the operation associated with PATH, which is a single-path
332251881Speter   component representing a child of the path represented by
333251881Speter   OPERATION.  If no such child operation exists, create a new one of
334251881Speter   type OP_OPEN. */
335251881Speterstatic struct operation *
336251881Speterget_operation(const char *path,
337251881Speter              struct operation *operation,
338251881Speter              apr_pool_t *pool)
339251881Speter{
340251881Speter  struct operation *child = svn_hash_gets(operation->children, path);
341251881Speter  if (! child)
342251881Speter    {
343251881Speter      child = apr_pcalloc(pool, sizeof(*child));
344251881Speter      child->children = apr_hash_make(pool);
345251881Speter      child->operation = OP_OPEN;
346251881Speter      child->rev = SVN_INVALID_REVNUM;
347251881Speter      child->kind = svn_node_dir;
348251881Speter      child->prop_mods = apr_hash_make(pool);
349251881Speter      child->prop_dels = apr_array_make(pool, 1, sizeof(const char *));
350251881Speter      svn_hash_sets(operation->children, path, child);
351251881Speter    }
352251881Speter  return child;
353251881Speter}
354251881Speter
355251881Speter/* Return the portion of URL that is relative to ANCHOR (URI-decoded). */
356251881Speterstatic const char *
357251881Spetersubtract_anchor(const char *anchor, const char *url, apr_pool_t *pool)
358251881Speter{
359251881Speter  return svn_uri_skip_ancestor(anchor, url, pool);
360251881Speter}
361251881Speter
362251881Speter/* Add PATH to the operations tree rooted at OPERATION, creating any
363251881Speter   intermediate nodes that are required.  Here's what's expected for
364251881Speter   each action type:
365251881Speter
366251881Speter      ACTION          URL    REV      SRC-FILE  PROPNAME
367251881Speter      ------------    -----  -------  --------  --------
368251881Speter      ACTION_MKDIR    NULL   invalid  NULL      NULL
369251881Speter      ACTION_CP       valid  valid    NULL      NULL
370251881Speter      ACTION_PUT      NULL   invalid  valid     NULL
371251881Speter      ACTION_RM       NULL   invalid  NULL      NULL
372251881Speter      ACTION_PROPSET  valid  invalid  NULL      valid
373251881Speter      ACTION_PROPDEL  valid  invalid  NULL      valid
374251881Speter
375251881Speter   Node type information is obtained for any copy source (to determine
376251881Speter   whether to create a file or directory) and for any deleted path (to
377251881Speter   ensure it exists since svn_delta_editor_t->delete_entry doesn't
378251881Speter   return an error on non-existent nodes). */
379251881Speterstatic svn_error_t *
380251881Speterbuild(action_code_t action,
381251881Speter      const char *path,
382251881Speter      const char *url,
383251881Speter      svn_revnum_t rev,
384251881Speter      const char *prop_name,
385251881Speter      const svn_string_t *prop_value,
386251881Speter      const char *src_file,
387251881Speter      svn_revnum_t head,
388251881Speter      const char *anchor,
389251881Speter      svn_ra_session_t *session,
390251881Speter      struct operation *operation,
391251881Speter      apr_pool_t *pool)
392251881Speter{
393251881Speter  apr_array_header_t *path_bits = svn_path_decompose(path, pool);
394251881Speter  const char *path_so_far = "";
395251881Speter  const char *copy_src = NULL;
396251881Speter  svn_revnum_t copy_rev = SVN_INVALID_REVNUM;
397251881Speter  int i;
398251881Speter
399251881Speter  /* Look for any previous operations we've recognized for PATH.  If
400251881Speter     any of PATH's ancestors have not yet been traversed, we'll be
401251881Speter     creating OP_OPEN operations for them as we walk down PATH's path
402251881Speter     components. */
403251881Speter  for (i = 0; i < path_bits->nelts; ++i)
404251881Speter    {
405251881Speter      const char *path_bit = APR_ARRAY_IDX(path_bits, i, const char *);
406251881Speter      path_so_far = svn_relpath_join(path_so_far, path_bit, pool);
407251881Speter      operation = get_operation(path_so_far, operation, pool);
408251881Speter
409251881Speter      /* If we cross a replace- or add-with-history, remember the
410251881Speter      source of those things in case we need to lookup the node kind
411251881Speter      of one of their children.  And if this isn't such a copy,
412251881Speter      but we've already seen one in of our parent paths, we just need
413251881Speter      to extend that copy source path by our current path
414251881Speter      component. */
415251881Speter      if (operation->url
416251881Speter          && SVN_IS_VALID_REVNUM(operation->rev)
417251881Speter          && (operation->operation == OP_REPLACE
418251881Speter              || operation->operation == OP_ADD))
419251881Speter        {
420251881Speter          copy_src = subtract_anchor(anchor, operation->url, pool);
421251881Speter          copy_rev = operation->rev;
422251881Speter        }
423251881Speter      else if (copy_src)
424251881Speter        {
425251881Speter          copy_src = svn_relpath_join(copy_src, path_bit, pool);
426251881Speter        }
427251881Speter    }
428251881Speter
429251881Speter  /* Handle property changes. */
430251881Speter  if (prop_name)
431251881Speter    {
432251881Speter      if (operation->operation == OP_DELETE)
433251881Speter        return svn_error_createf(SVN_ERR_BAD_URL, NULL,
434251881Speter                                 "cannot set properties on a location being"
435251881Speter                                 " deleted ('%s')", path);
436251881Speter      /* If we're not adding this thing ourselves, check for existence.  */
437251881Speter      if (! ((operation->operation == OP_ADD) ||
438251881Speter             (operation->operation == OP_REPLACE)))
439251881Speter        {
440251881Speter          SVN_ERR(svn_ra_check_path(session,
441251881Speter                                    copy_src ? copy_src : path,
442251881Speter                                    copy_src ? copy_rev : head,
443251881Speter                                    &operation->kind, pool));
444251881Speter          if (operation->kind == svn_node_none)
445251881Speter            return svn_error_createf(SVN_ERR_BAD_URL, NULL,
446251881Speter                                     "propset: '%s' not found", path);
447251881Speter          else if ((operation->kind == svn_node_file)
448251881Speter                   && (operation->operation == OP_OPEN))
449251881Speter            operation->operation = OP_PROPSET;
450251881Speter        }
451251881Speter      if (! prop_value)
452251881Speter        APR_ARRAY_PUSH(operation->prop_dels, const char *) = prop_name;
453251881Speter      else
454251881Speter        svn_hash_sets(operation->prop_mods, prop_name, prop_value);
455251881Speter      if (!operation->rev)
456251881Speter        operation->rev = rev;
457251881Speter      return SVN_NO_ERROR;
458251881Speter    }
459251881Speter
460251881Speter  /* We won't fuss about multiple operations on the same path in the
461251881Speter     following cases:
462251881Speter
463251881Speter       - the prior operation was, in fact, a no-op (open)
464251881Speter       - the prior operation was a propset placeholder
465251881Speter       - the prior operation was a deletion
466251881Speter
467251881Speter     Note: while the operation structure certainly supports the
468251881Speter     ability to do a copy of a file followed by a put of new contents
469251881Speter     for the file, we don't let that happen (yet).
470251881Speter  */
471251881Speter  if (operation->operation != OP_OPEN
472251881Speter      && operation->operation != OP_PROPSET
473251881Speter      && operation->operation != OP_DELETE)
474251881Speter    return svn_error_createf(SVN_ERR_BAD_URL, NULL,
475251881Speter                             "unsupported multiple operations on '%s'", path);
476251881Speter
477251881Speter  /* For deletions, we validate that there's actually something to
478251881Speter     delete.  If this is a deletion of the child of a copied
479251881Speter     directory, we need to remember to look in the copy source tree to
480251881Speter     verify that this thing actually exists. */
481251881Speter  if (action == ACTION_RM)
482251881Speter    {
483251881Speter      operation->operation = OP_DELETE;
484251881Speter      SVN_ERR(svn_ra_check_path(session,
485251881Speter                                copy_src ? copy_src : path,
486251881Speter                                copy_src ? copy_rev : head,
487251881Speter                                &operation->kind, pool));
488251881Speter      if (operation->kind == svn_node_none)
489251881Speter        {
490251881Speter          if (copy_src && strcmp(path, copy_src))
491251881Speter            return svn_error_createf(SVN_ERR_BAD_URL, NULL,
492251881Speter                                     "'%s' (from '%s:%ld') not found",
493251881Speter                                     path, copy_src, copy_rev);
494251881Speter          else
495251881Speter            return svn_error_createf(SVN_ERR_BAD_URL, NULL, "'%s' not found",
496251881Speter                                     path);
497251881Speter        }
498251881Speter    }
499251881Speter  /* Handle copy operations (which can be adds or replacements). */
500251881Speter  else if (action == ACTION_CP)
501251881Speter    {
502251881Speter      if (rev > head)
503251881Speter        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
504251881Speter                                "Copy source revision cannot be younger "
505251881Speter                                "than base revision");
506251881Speter      operation->operation =
507251881Speter        operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
508251881Speter      if (operation->operation == OP_ADD)
509251881Speter        {
510251881Speter          /* There is a bug in the current version of mod_dav_svn
511251881Speter             which incorrectly replaces existing directories.
512251881Speter             Therefore we need to check if the target exists
513251881Speter             and raise an error here. */
514251881Speter          SVN_ERR(svn_ra_check_path(session,
515251881Speter                                    copy_src ? copy_src : path,
516251881Speter                                    copy_src ? copy_rev : head,
517251881Speter                                    &operation->kind, pool));
518251881Speter          if (operation->kind != svn_node_none)
519251881Speter            {
520251881Speter              if (copy_src && strcmp(path, copy_src))
521251881Speter                return svn_error_createf(SVN_ERR_BAD_URL, NULL,
522251881Speter                                         "'%s' (from '%s:%ld') already exists",
523251881Speter                                         path, copy_src, copy_rev);
524251881Speter              else
525251881Speter                return svn_error_createf(SVN_ERR_BAD_URL, NULL,
526251881Speter                                         "'%s' already exists", path);
527251881Speter            }
528251881Speter        }
529251881Speter      SVN_ERR(svn_ra_check_path(session, subtract_anchor(anchor, url, pool),
530251881Speter                                rev, &operation->kind, pool));
531251881Speter      if (operation->kind == svn_node_none)
532251881Speter        return svn_error_createf(SVN_ERR_BAD_URL, NULL,
533251881Speter                                 "'%s' not found",
534251881Speter                                  subtract_anchor(anchor, url, pool));
535251881Speter      operation->url = url;
536251881Speter      operation->rev = rev;
537251881Speter    }
538251881Speter  /* Handle mkdir operations (which can be adds or replacements). */
539251881Speter  else if (action == ACTION_MKDIR)
540251881Speter    {
541251881Speter      operation->operation =
542251881Speter        operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
543251881Speter      operation->kind = svn_node_dir;
544251881Speter    }
545251881Speter  /* Handle put operations (which can be adds, replacements, or opens). */
546251881Speter  else if (action == ACTION_PUT)
547251881Speter    {
548251881Speter      if (operation->operation == OP_DELETE)
549251881Speter        {
550251881Speter          operation->operation = OP_REPLACE;
551251881Speter        }
552251881Speter      else
553251881Speter        {
554251881Speter          SVN_ERR(svn_ra_check_path(session,
555251881Speter                                    copy_src ? copy_src : path,
556251881Speter                                    copy_src ? copy_rev : head,
557251881Speter                                    &operation->kind, pool));
558251881Speter          if (operation->kind == svn_node_file)
559251881Speter            operation->operation = OP_OPEN;
560251881Speter          else if (operation->kind == svn_node_none)
561251881Speter            operation->operation = OP_ADD;
562251881Speter          else
563251881Speter            return svn_error_createf(SVN_ERR_BAD_URL, NULL,
564251881Speter                                     "'%s' is not a file", path);
565251881Speter        }
566251881Speter      operation->kind = svn_node_file;
567251881Speter      operation->src_file = src_file;
568251881Speter    }
569251881Speter  else
570251881Speter    {
571251881Speter      /* We shouldn't get here. */
572251881Speter      SVN_ERR_MALFUNCTION();
573251881Speter    }
574251881Speter
575251881Speter  return SVN_NO_ERROR;
576251881Speter}
577251881Speter
578251881Speterstruct action {
579251881Speter  action_code_t action;
580251881Speter
581251881Speter  /* revision (copy-from-rev of path[0] for cp; base-rev for put) */
582251881Speter  svn_revnum_t rev;
583251881Speter
584251881Speter  /* action  path[0]  path[1]
585251881Speter   * ------  -------  -------
586251881Speter   * mv      source   target
587251881Speter   * mkdir   target   (null)
588251881Speter   * cp      source   target
589251881Speter   * put     target   source
590251881Speter   * rm      target   (null)
591251881Speter   * propset target   (null)
592251881Speter   */
593251881Speter  const char *path[2];
594251881Speter
595251881Speter  /* property name/value */
596251881Speter  const char *prop_name;
597251881Speter  const svn_string_t *prop_value;
598251881Speter};
599251881Speter
600251881Speterstruct fetch_baton
601251881Speter{
602251881Speter  svn_ra_session_t *session;
603251881Speter  svn_revnum_t head;
604251881Speter};
605251881Speter
606251881Speterstatic svn_error_t *
607251881Speterfetch_base_func(const char **filename,
608251881Speter                void *baton,
609251881Speter                const char *path,
610251881Speter                svn_revnum_t base_revision,
611251881Speter                apr_pool_t *result_pool,
612251881Speter                apr_pool_t *scratch_pool)
613251881Speter{
614251881Speter  struct fetch_baton *fb = baton;
615251881Speter  svn_stream_t *fstream;
616251881Speter  svn_error_t *err;
617251881Speter
618251881Speter  if (! SVN_IS_VALID_REVNUM(base_revision))
619251881Speter    base_revision = fb->head;
620251881Speter
621251881Speter  SVN_ERR(svn_stream_open_unique(&fstream, filename, NULL,
622251881Speter                                 svn_io_file_del_on_pool_cleanup,
623251881Speter                                 result_pool, scratch_pool));
624251881Speter
625251881Speter  err = svn_ra_get_file(fb->session, path, base_revision, fstream, NULL, NULL,
626251881Speter                         scratch_pool);
627251881Speter  if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
628251881Speter    {
629251881Speter      svn_error_clear(err);
630251881Speter      SVN_ERR(svn_stream_close(fstream));
631251881Speter
632251881Speter      *filename = NULL;
633251881Speter      return SVN_NO_ERROR;
634251881Speter    }
635251881Speter  else if (err)
636251881Speter    return svn_error_trace(err);
637251881Speter
638251881Speter  SVN_ERR(svn_stream_close(fstream));
639251881Speter
640251881Speter  return SVN_NO_ERROR;
641251881Speter}
642251881Speter
643251881Speterstatic svn_error_t *
644251881Speterfetch_props_func(apr_hash_t **props,
645251881Speter                 void *baton,
646251881Speter                 const char *path,
647251881Speter                 svn_revnum_t base_revision,
648251881Speter                 apr_pool_t *result_pool,
649251881Speter                 apr_pool_t *scratch_pool)
650251881Speter{
651251881Speter  struct fetch_baton *fb = baton;
652251881Speter  svn_node_kind_t node_kind;
653251881Speter
654251881Speter  if (! SVN_IS_VALID_REVNUM(base_revision))
655251881Speter    base_revision = fb->head;
656251881Speter
657251881Speter  SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, &node_kind,
658251881Speter                            scratch_pool));
659251881Speter
660251881Speter  if (node_kind == svn_node_file)
661251881Speter    {
662251881Speter      SVN_ERR(svn_ra_get_file(fb->session, path, base_revision, NULL, NULL,
663251881Speter                              props, result_pool));
664251881Speter    }
665251881Speter  else if (node_kind == svn_node_dir)
666251881Speter    {
667251881Speter      apr_array_header_t *tmp_props;
668251881Speter
669251881Speter      SVN_ERR(svn_ra_get_dir2(fb->session, NULL, NULL, props, path,
670251881Speter                              base_revision, 0 /* Dirent fields */,
671251881Speter                              result_pool));
672251881Speter      tmp_props = svn_prop_hash_to_array(*props, result_pool);
673251881Speter      SVN_ERR(svn_categorize_props(tmp_props, NULL, NULL, &tmp_props,
674251881Speter                                   result_pool));
675251881Speter      *props = svn_prop_array_to_hash(tmp_props, result_pool);
676251881Speter    }
677251881Speter  else
678251881Speter    {
679251881Speter      *props = apr_hash_make(result_pool);
680251881Speter    }
681251881Speter
682251881Speter  return SVN_NO_ERROR;
683251881Speter}
684251881Speter
685251881Speterstatic svn_error_t *
686251881Speterfetch_kind_func(svn_node_kind_t *kind,
687251881Speter                void *baton,
688251881Speter                const char *path,
689251881Speter                svn_revnum_t base_revision,
690251881Speter                apr_pool_t *scratch_pool)
691251881Speter{
692251881Speter  struct fetch_baton *fb = baton;
693251881Speter
694251881Speter  if (! SVN_IS_VALID_REVNUM(base_revision))
695251881Speter    base_revision = fb->head;
696251881Speter
697251881Speter  SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, kind,
698251881Speter                             scratch_pool));
699251881Speter
700251881Speter  return SVN_NO_ERROR;
701251881Speter}
702251881Speter
703251881Speterstatic svn_delta_shim_callbacks_t *
704251881Speterget_shim_callbacks(svn_ra_session_t *session,
705251881Speter                   svn_revnum_t head,
706251881Speter                   apr_pool_t *result_pool)
707251881Speter{
708251881Speter  svn_delta_shim_callbacks_t *callbacks =
709251881Speter                            svn_delta_shim_callbacks_default(result_pool);
710251881Speter  struct fetch_baton *fb = apr_pcalloc(result_pool, sizeof(*fb));
711251881Speter
712251881Speter  fb->session = session;
713251881Speter  fb->head = head;
714251881Speter
715251881Speter  callbacks->fetch_props_func = fetch_props_func;
716251881Speter  callbacks->fetch_kind_func = fetch_kind_func;
717251881Speter  callbacks->fetch_base_func = fetch_base_func;
718251881Speter  callbacks->fetch_baton = fb;
719251881Speter
720251881Speter  return callbacks;
721251881Speter}
722251881Speter
723251881Speterstatic svn_error_t *
724251881Speterexecute(const apr_array_header_t *actions,
725251881Speter        const char *anchor,
726251881Speter        apr_hash_t *revprops,
727251881Speter        const char *username,
728251881Speter        const char *password,
729251881Speter        const char *config_dir,
730251881Speter        const apr_array_header_t *config_options,
731251881Speter        svn_boolean_t non_interactive,
732251881Speter        svn_boolean_t trust_server_cert,
733251881Speter        svn_boolean_t no_auth_cache,
734251881Speter        svn_revnum_t base_revision,
735251881Speter        apr_pool_t *pool)
736251881Speter{
737251881Speter  svn_ra_session_t *session;
738251881Speter  svn_ra_session_t *aux_session;
739251881Speter  const char *repos_root;
740251881Speter  svn_revnum_t head;
741251881Speter  const svn_delta_editor_t *editor;
742251881Speter  svn_ra_callbacks2_t *ra_callbacks;
743251881Speter  void *editor_baton;
744251881Speter  struct operation root;
745251881Speter  svn_error_t *err;
746251881Speter  apr_hash_t *config;
747251881Speter  svn_config_t *cfg_config;
748251881Speter  int i;
749251881Speter
750251881Speter  SVN_ERR(svn_config_get_config(&config, config_dir, pool));
751251881Speter  SVN_ERR(svn_cmdline__apply_config_options(config, config_options,
752251881Speter                                            "svnmucc: ", "--config-option"));
753251881Speter  cfg_config = svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG);
754251881Speter
755251881Speter  if (! svn_hash_gets(revprops, SVN_PROP_REVISION_LOG))
756251881Speter    {
757251881Speter      svn_string_t *msg = svn_string_create("", pool);
758251881Speter
759251881Speter      /* If we can do so, try to pop up $EDITOR to fetch a log message. */
760251881Speter      if (non_interactive)
761251881Speter        {
762251881Speter          return svn_error_create
763251881Speter            (SVN_ERR_CL_INSUFFICIENT_ARGS, NULL,
764251881Speter             _("Cannot invoke editor to get log message "
765251881Speter               "when non-interactive"));
766251881Speter        }
767251881Speter      else
768251881Speter        {
769251881Speter          SVN_ERR(svn_cmdline__edit_string_externally(
770251881Speter                      &msg, NULL, NULL, "", msg, "svnmucc-commit", config,
771251881Speter                      TRUE, NULL, apr_hash_pool_get(revprops)));
772251881Speter        }
773251881Speter
774251881Speter      svn_hash_sets(revprops, SVN_PROP_REVISION_LOG, msg);
775251881Speter    }
776251881Speter
777251881Speter  SVN_ERR(create_ra_callbacks(&ra_callbacks, username, password, config_dir,
778251881Speter                              cfg_config, non_interactive, trust_server_cert,
779251881Speter                              no_auth_cache, pool));
780251881Speter  SVN_ERR(svn_ra_open4(&session, NULL, anchor, NULL, ra_callbacks,
781251881Speter                       NULL, config, pool));
782251881Speter  /* Open, then reparent to avoid AUTHZ errors when opening the reposroot */
783251881Speter  SVN_ERR(svn_ra_open4(&aux_session, NULL, anchor, NULL, ra_callbacks,
784251881Speter                       NULL, config, pool));
785251881Speter  SVN_ERR(svn_ra_get_repos_root2(aux_session, &repos_root, pool));
786251881Speter  SVN_ERR(svn_ra_reparent(aux_session, repos_root, pool));
787251881Speter  SVN_ERR(svn_ra_get_latest_revnum(session, &head, pool));
788251881Speter
789251881Speter  /* Reparent to ANCHOR's dir, if ANCHOR is not a directory. */
790251881Speter  {
791251881Speter    svn_node_kind_t kind;
792251881Speter
793251881Speter    SVN_ERR(svn_ra_check_path(aux_session,
794251881Speter                              svn_uri_skip_ancestor(repos_root, anchor, pool),
795251881Speter                              head, &kind, pool));
796251881Speter    if (kind != svn_node_dir)
797251881Speter      {
798251881Speter        anchor = svn_uri_dirname(anchor, pool);
799251881Speter        SVN_ERR(svn_ra_reparent(session, anchor, pool));
800251881Speter      }
801251881Speter  }
802251881Speter
803251881Speter  if (SVN_IS_VALID_REVNUM(base_revision))
804251881Speter    {
805251881Speter      if (base_revision > head)
806251881Speter        return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL,
807251881Speter                                 "No such revision %ld (youngest is %ld)",
808251881Speter                                 base_revision, head);
809251881Speter      head = base_revision;
810251881Speter    }
811251881Speter
812251881Speter  memset(&root, 0, sizeof(root));
813251881Speter  root.children = apr_hash_make(pool);
814251881Speter  root.operation = OP_OPEN;
815251881Speter  root.kind = svn_node_dir; /* For setting properties */
816251881Speter  root.prop_mods = apr_hash_make(pool);
817251881Speter  root.prop_dels = apr_array_make(pool, 1, sizeof(const char *));
818251881Speter
819251881Speter  for (i = 0; i < actions->nelts; ++i)
820251881Speter    {
821251881Speter      struct action *action = APR_ARRAY_IDX(actions, i, struct action *);
822251881Speter      const char *path1, *path2;
823251881Speter      switch (action->action)
824251881Speter        {
825251881Speter        case ACTION_MV:
826251881Speter          path1 = subtract_anchor(anchor, action->path[0], pool);
827251881Speter          path2 = subtract_anchor(anchor, action->path[1], pool);
828251881Speter          SVN_ERR(build(ACTION_RM, path1, NULL,
829251881Speter                        SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
830251881Speter                        session, &root, pool));
831251881Speter          SVN_ERR(build(ACTION_CP, path2, action->path[0],
832251881Speter                        head, NULL, NULL, NULL, head, anchor,
833251881Speter                        session, &root, pool));
834251881Speter          break;
835251881Speter        case ACTION_CP:
836251881Speter          path2 = subtract_anchor(anchor, action->path[1], pool);
837251881Speter          if (action->rev == SVN_INVALID_REVNUM)
838251881Speter            action->rev = head;
839251881Speter          SVN_ERR(build(ACTION_CP, path2, action->path[0],
840251881Speter                        action->rev, NULL, NULL, NULL, head, anchor,
841251881Speter                        session, &root, pool));
842251881Speter          break;
843251881Speter        case ACTION_RM:
844251881Speter          path1 = subtract_anchor(anchor, action->path[0], pool);
845251881Speter          SVN_ERR(build(ACTION_RM, path1, NULL,
846251881Speter                        SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
847251881Speter                        session, &root, pool));
848251881Speter          break;
849251881Speter        case ACTION_MKDIR:
850251881Speter          path1 = subtract_anchor(anchor, action->path[0], pool);
851251881Speter          SVN_ERR(build(ACTION_MKDIR, path1, action->path[0],
852251881Speter                        SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
853251881Speter                        session, &root, pool));
854251881Speter          break;
855251881Speter        case ACTION_PUT:
856251881Speter          path1 = subtract_anchor(anchor, action->path[0], pool);
857251881Speter          SVN_ERR(build(ACTION_PUT, path1, action->path[0],
858251881Speter                        SVN_INVALID_REVNUM, NULL, NULL, action->path[1],
859251881Speter                        head, anchor, session, &root, pool));
860251881Speter          break;
861251881Speter        case ACTION_PROPSET:
862251881Speter        case ACTION_PROPDEL:
863251881Speter          path1 = subtract_anchor(anchor, action->path[0], pool);
864251881Speter          SVN_ERR(build(action->action, path1, action->path[0],
865251881Speter                        SVN_INVALID_REVNUM,
866251881Speter                        action->prop_name, action->prop_value,
867251881Speter                        NULL, head, anchor, session, &root, pool));
868251881Speter          break;
869251881Speter        case ACTION_PROPSETF:
870251881Speter        default:
871251881Speter          SVN_ERR_MALFUNCTION_NO_RETURN();
872251881Speter        }
873251881Speter    }
874251881Speter
875251881Speter  SVN_ERR(svn_ra__register_editor_shim_callbacks(session,
876251881Speter                            get_shim_callbacks(aux_session, head, pool)));
877251881Speter  SVN_ERR(svn_ra_get_commit_editor3(session, &editor, &editor_baton, revprops,
878251881Speter                                    commit_callback, NULL, NULL, FALSE, pool));
879251881Speter
880251881Speter  SVN_ERR(editor->open_root(editor_baton, head, pool, &root.baton));
881251881Speter  err = change_props(editor, root.baton, &root, pool);
882251881Speter  if (!err)
883251881Speter    err = drive(&root, head, editor, pool);
884251881Speter  if (!err)
885251881Speter    err = editor->close_directory(root.baton, pool);
886251881Speter  if (!err)
887251881Speter    err = editor->close_edit(editor_baton, pool);
888251881Speter
889251881Speter  if (err)
890251881Speter    err = svn_error_compose_create(err,
891251881Speter                                   editor->abort_edit(editor_baton, pool));
892251881Speter
893251881Speter  return err;
894251881Speter}
895251881Speter
896251881Speterstatic svn_error_t *
897251881Speterread_propvalue_file(const svn_string_t **value_p,
898251881Speter                    const char *filename,
899251881Speter                    apr_pool_t *pool)
900251881Speter{
901251881Speter  svn_stringbuf_t *value;
902251881Speter  apr_pool_t *scratch_pool = svn_pool_create(pool);
903251881Speter
904251881Speter  SVN_ERR(svn_stringbuf_from_file2(&value, filename, scratch_pool));
905251881Speter  *value_p = svn_string_create_from_buf(value, pool);
906251881Speter  svn_pool_destroy(scratch_pool);
907251881Speter  return SVN_NO_ERROR;
908251881Speter}
909251881Speter
910251881Speter/* Perform the typical suite of manipulations for user-provided URLs
911251881Speter   on URL, returning the result (allocated from POOL): IRI-to-URI
912251881Speter   conversion, auto-escaping, and canonicalization. */
913251881Speterstatic const char *
914251881Spetersanitize_url(const char *url,
915251881Speter             apr_pool_t *pool)
916251881Speter{
917251881Speter  url = svn_path_uri_from_iri(url, pool);
918251881Speter  url = svn_path_uri_autoescape(url, pool);
919251881Speter  return svn_uri_canonicalize(url, pool);
920251881Speter}
921251881Speter
922251881Speterstatic void
923251881Speterusage(apr_pool_t *pool, int exit_val)
924251881Speter{
925251881Speter  FILE *stream = exit_val == EXIT_SUCCESS ? stdout : stderr;
926251881Speter  svn_error_clear(svn_cmdline_fputs(
927251881Speter    _("Subversion multiple URL command client\n"
928251881Speter      "usage: svnmucc ACTION...\n"
929251881Speter      "\n"
930251881Speter      "  Perform one or more Subversion repository URL-based ACTIONs, committing\n"
931251881Speter      "  the result as a (single) new revision.\n"
932251881Speter      "\n"
933251881Speter      "Actions:\n"
934251881Speter      "  cp REV SRC-URL DST-URL : copy SRC-URL@REV to DST-URL\n"
935251881Speter      "  mkdir URL              : create new directory URL\n"
936251881Speter      "  mv SRC-URL DST-URL     : move SRC-URL to DST-URL\n"
937251881Speter      "  rm URL                 : delete URL\n"
938251881Speter      "  put SRC-FILE URL       : add or modify file URL with contents copied from\n"
939251881Speter      "                           SRC-FILE (use \"-\" to read from standard input)\n"
940251881Speter      "  propset NAME VALUE URL : set property NAME on URL to VALUE\n"
941251881Speter      "  propsetf NAME FILE URL : set property NAME on URL to value read from FILE\n"
942251881Speter      "  propdel NAME URL       : delete property NAME from URL\n"
943251881Speter      "\n"
944251881Speter      "Valid options:\n"
945251881Speter      "  -h, -? [--help]        : display this text\n"
946251881Speter      "  -m [--message] ARG     : use ARG as a log message\n"
947251881Speter      "  -F [--file] ARG        : read log message from file ARG\n"
948251881Speter      "  -u [--username] ARG    : commit the changes as username ARG\n"
949251881Speter      "  -p [--password] ARG    : use ARG as the password\n"
950251881Speter      "  -U [--root-url] ARG    : interpret all action URLs relative to ARG\n"
951251881Speter      "  -r [--revision] ARG    : use revision ARG as baseline for changes\n"
952251881Speter      "  --with-revprop ARG     : set revision property in the following format:\n"
953251881Speter      "                               NAME[=VALUE]\n"
954251881Speter      "  --non-interactive      : do no interactive prompting (default is to\n"
955251881Speter      "                           prompt only if standard input is a terminal)\n"
956251881Speter      "  --force-interactive    : do interactive prompting even if standard\n"
957251881Speter      "                           input is not a terminal\n"
958251881Speter      "  --trust-server-cert    : accept SSL server certificates from unknown\n"
959251881Speter      "                           certificate authorities without prompting (but\n"
960251881Speter      "                           only with '--non-interactive')\n"
961251881Speter      "  -X [--extra-args] ARG  : append arguments from file ARG (one per line;\n"
962251881Speter      "                           use \"-\" to read from standard input)\n"
963251881Speter      "  --config-dir ARG       : use ARG to override the config directory\n"
964251881Speter      "  --config-option ARG    : use ARG to override a configuration option\n"
965251881Speter      "  --no-auth-cache        : do not cache authentication tokens\n"
966251881Speter      "  --version              : print version information\n"),
967251881Speter                  stream, pool));
968251881Speter  svn_pool_destroy(pool);
969251881Speter  exit(exit_val);
970251881Speter}
971251881Speter
972251881Speterstatic void
973251881Speterinsufficient(apr_pool_t *pool)
974251881Speter{
975251881Speter  handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
976251881Speter                                "insufficient arguments"),
977251881Speter               pool);
978251881Speter}
979251881Speter
980251881Speterstatic svn_error_t *
981251881Speterdisplay_version(apr_getopt_t *os, apr_pool_t *pool)
982251881Speter{
983251881Speter  const char *ra_desc_start
984251881Speter    = "The following repository access (RA) modules are available:\n\n";
985251881Speter  svn_stringbuf_t *version_footer;
986251881Speter
987251881Speter  version_footer = svn_stringbuf_create(ra_desc_start, pool);
988251881Speter  SVN_ERR(svn_ra_print_modules(version_footer, pool));
989251881Speter
990251881Speter  SVN_ERR(svn_opt_print_help4(os, "svnmucc", TRUE, FALSE, FALSE,
991251881Speter                              version_footer->data,
992251881Speter                              NULL, NULL, NULL, NULL, NULL, pool));
993251881Speter
994251881Speter  return SVN_NO_ERROR;
995251881Speter}
996251881Speter
997251881Speter/* Return an error about the mutual exclusivity of the -m, -F, and
998251881Speter   --with-revprop=svn:log command-line options. */
999251881Speterstatic svn_error_t *
1000251881Spetermutually_exclusive_logs_error(void)
1001251881Speter{
1002251881Speter  return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1003251881Speter                          _("--message (-m), --file (-F), and "
1004251881Speter                            "--with-revprop=svn:log are mutually "
1005251881Speter                            "exclusive"));
1006251881Speter}
1007251881Speter
1008251881Speter/* Ensure that the REVPROPS hash contains a command-line-provided log
1009251881Speter   message, if any, and that there was but one source of such a thing
1010251881Speter   provided on that command-line.  */
1011251881Speterstatic svn_error_t *
1012251881Spetersanitize_log_sources(apr_hash_t *revprops,
1013251881Speter                     const char *message,
1014251881Speter                     svn_stringbuf_t *filedata)
1015251881Speter{
1016251881Speter  apr_pool_t *hash_pool = apr_hash_pool_get(revprops);
1017251881Speter
1018251881Speter  /* If we already have a log message in the revprop hash, then just
1019251881Speter     make sure the user didn't try to also use -m or -F.  Otherwise,
1020251881Speter     we need to consult -m or -F to find a log message, if any. */
1021251881Speter  if (svn_hash_gets(revprops, SVN_PROP_REVISION_LOG))
1022251881Speter    {
1023251881Speter      if (filedata || message)
1024251881Speter        return mutually_exclusive_logs_error();
1025251881Speter    }
1026251881Speter  else if (filedata)
1027251881Speter    {
1028251881Speter      if (message)
1029251881Speter        return mutually_exclusive_logs_error();
1030251881Speter
1031251881Speter      SVN_ERR(svn_utf_cstring_to_utf8(&message, filedata->data, hash_pool));
1032251881Speter      svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
1033251881Speter                    svn_stringbuf__morph_into_string(filedata));
1034251881Speter    }
1035251881Speter  else if (message)
1036251881Speter    {
1037251881Speter      svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
1038251881Speter                    svn_string_create(message, hash_pool));
1039251881Speter    }
1040251881Speter
1041251881Speter  return SVN_NO_ERROR;
1042251881Speter}
1043251881Speter
1044251881Speterint
1045251881Spetermain(int argc, const char **argv)
1046251881Speter{
1047251881Speter  apr_pool_t *pool = init("svnmucc");
1048251881Speter  apr_array_header_t *actions = apr_array_make(pool, 1,
1049251881Speter                                               sizeof(struct action *));
1050251881Speter  const char *anchor = NULL;
1051251881Speter  svn_error_t *err = SVN_NO_ERROR;
1052251881Speter  apr_getopt_t *opts;
1053251881Speter  enum {
1054251881Speter    config_dir_opt = SVN_OPT_FIRST_LONGOPT_ID,
1055251881Speter    config_inline_opt,
1056251881Speter    no_auth_cache_opt,
1057251881Speter    version_opt,
1058251881Speter    with_revprop_opt,
1059251881Speter    non_interactive_opt,
1060251881Speter    force_interactive_opt,
1061251881Speter    trust_server_cert_opt
1062251881Speter  };
1063251881Speter  static const apr_getopt_option_t options[] = {
1064251881Speter    {"message", 'm', 1, ""},
1065251881Speter    {"file", 'F', 1, ""},
1066251881Speter    {"username", 'u', 1, ""},
1067251881Speter    {"password", 'p', 1, ""},
1068251881Speter    {"root-url", 'U', 1, ""},
1069251881Speter    {"revision", 'r', 1, ""},
1070251881Speter    {"with-revprop",  with_revprop_opt, 1, ""},
1071251881Speter    {"extra-args", 'X', 1, ""},
1072251881Speter    {"help", 'h', 0, ""},
1073251881Speter    {NULL, '?', 0, ""},
1074251881Speter    {"non-interactive", non_interactive_opt, 0, ""},
1075251881Speter    {"force-interactive", force_interactive_opt, 0, ""},
1076251881Speter    {"trust-server-cert", trust_server_cert_opt, 0, ""},
1077251881Speter    {"config-dir", config_dir_opt, 1, ""},
1078251881Speter    {"config-option",  config_inline_opt, 1, ""},
1079251881Speter    {"no-auth-cache",  no_auth_cache_opt, 0, ""},
1080251881Speter    {"version", version_opt, 0, ""},
1081251881Speter    {NULL, 0, 0, NULL}
1082251881Speter  };
1083251881Speter  const char *message = NULL;
1084251881Speter  svn_stringbuf_t *filedata = NULL;
1085251881Speter  const char *username = NULL, *password = NULL;
1086251881Speter  const char *root_url = NULL, *extra_args_file = NULL;
1087251881Speter  const char *config_dir = NULL;
1088251881Speter  apr_array_header_t *config_options;
1089251881Speter  svn_boolean_t non_interactive = FALSE;
1090251881Speter  svn_boolean_t force_interactive = FALSE;
1091251881Speter  svn_boolean_t trust_server_cert = FALSE;
1092251881Speter  svn_boolean_t no_auth_cache = FALSE;
1093251881Speter  svn_revnum_t base_revision = SVN_INVALID_REVNUM;
1094251881Speter  apr_array_header_t *action_args;
1095251881Speter  apr_hash_t *revprops = apr_hash_make(pool);
1096251881Speter  int i;
1097251881Speter
1098251881Speter  config_options = apr_array_make(pool, 0,
1099251881Speter                                  sizeof(svn_cmdline__config_argument_t*));
1100251881Speter
1101251881Speter  apr_getopt_init(&opts, pool, argc, argv);
1102251881Speter  opts->interleave = 1;
1103251881Speter  while (1)
1104251881Speter    {
1105251881Speter      int opt;
1106251881Speter      const char *arg;
1107251881Speter      const char *opt_arg;
1108251881Speter
1109251881Speter      apr_status_t status = apr_getopt_long(opts, options, &opt, &arg);
1110251881Speter      if (APR_STATUS_IS_EOF(status))
1111251881Speter        break;
1112251881Speter      if (status != APR_SUCCESS)
1113251881Speter        handle_error(svn_error_wrap_apr(status, "getopt failure"), pool);
1114251881Speter      switch(opt)
1115251881Speter        {
1116251881Speter        case 'm':
1117251881Speter          err = svn_utf_cstring_to_utf8(&message, arg, pool);
1118251881Speter          if (err)
1119251881Speter            handle_error(err, pool);
1120251881Speter          break;
1121251881Speter        case 'F':
1122251881Speter          {
1123251881Speter            const char *arg_utf8;
1124251881Speter            err = svn_utf_cstring_to_utf8(&arg_utf8, arg, pool);
1125251881Speter            if (! err)
1126251881Speter              err = svn_stringbuf_from_file2(&filedata, arg, pool);
1127251881Speter            if (err)
1128251881Speter              handle_error(err, pool);
1129251881Speter          }
1130251881Speter          break;
1131251881Speter        case 'u':
1132251881Speter          username = apr_pstrdup(pool, arg);
1133251881Speter          break;
1134251881Speter        case 'p':
1135251881Speter          password = apr_pstrdup(pool, arg);
1136251881Speter          break;
1137251881Speter        case 'U':
1138251881Speter          err = svn_utf_cstring_to_utf8(&root_url, arg, pool);
1139251881Speter          if (err)
1140251881Speter            handle_error(err, pool);
1141251881Speter          if (! svn_path_is_url(root_url))
1142251881Speter            handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1143251881Speter                                           "'%s' is not a URL\n", root_url),
1144251881Speter                         pool);
1145251881Speter          root_url = sanitize_url(root_url, pool);
1146251881Speter          break;
1147251881Speter        case 'r':
1148251881Speter          {
1149251881Speter            char *digits_end = NULL;
1150251881Speter            base_revision = strtol(arg, &digits_end, 10);
1151251881Speter            if ((! SVN_IS_VALID_REVNUM(base_revision))
1152251881Speter                || (! digits_end)
1153251881Speter                || *digits_end)
1154251881Speter              handle_error(svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR,
1155251881Speter                                            NULL, "Invalid revision number"),
1156251881Speter                           pool);
1157251881Speter          }
1158251881Speter          break;
1159251881Speter        case with_revprop_opt:
1160251881Speter          err = svn_opt_parse_revprop(&revprops, arg, pool);
1161251881Speter          if (err != SVN_NO_ERROR)
1162251881Speter            handle_error(err, pool);
1163251881Speter          break;
1164251881Speter        case 'X':
1165251881Speter          extra_args_file = apr_pstrdup(pool, arg);
1166251881Speter          break;
1167251881Speter        case non_interactive_opt:
1168251881Speter          non_interactive = TRUE;
1169251881Speter          break;
1170251881Speter        case force_interactive_opt:
1171251881Speter          force_interactive = TRUE;
1172251881Speter          break;
1173251881Speter        case trust_server_cert_opt:
1174251881Speter          trust_server_cert = TRUE;
1175251881Speter          break;
1176251881Speter        case config_dir_opt:
1177251881Speter          err = svn_utf_cstring_to_utf8(&config_dir, arg, pool);
1178251881Speter          if (err)
1179251881Speter            handle_error(err, pool);
1180251881Speter          break;
1181251881Speter        case config_inline_opt:
1182251881Speter          err = svn_utf_cstring_to_utf8(&opt_arg, arg, pool);
1183251881Speter          if (err)
1184251881Speter            handle_error(err, pool);
1185251881Speter
1186251881Speter          err = svn_cmdline__parse_config_option(config_options, opt_arg,
1187251881Speter                                                 pool);
1188251881Speter          if (err)
1189251881Speter            handle_error(err, pool);
1190251881Speter          break;
1191251881Speter        case no_auth_cache_opt:
1192251881Speter          no_auth_cache = TRUE;
1193251881Speter          break;
1194251881Speter        case version_opt:
1195251881Speter          SVN_INT_ERR(display_version(opts, pool));
1196251881Speter          exit(EXIT_SUCCESS);
1197251881Speter          break;
1198251881Speter        case 'h':
1199251881Speter        case '?':
1200251881Speter          usage(pool, EXIT_SUCCESS);
1201251881Speter          break;
1202251881Speter        }
1203251881Speter    }
1204251881Speter
1205251881Speter  if (non_interactive && force_interactive)
1206251881Speter    {
1207251881Speter      err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1208251881Speter                             _("--non-interactive and --force-interactive "
1209251881Speter                               "are mutually exclusive"));
1210251881Speter      return svn_cmdline_handle_exit_error(err, pool, "svnmucc: ");
1211251881Speter    }
1212251881Speter  else
1213251881Speter    non_interactive = !svn_cmdline__be_interactive(non_interactive,
1214251881Speter                                                   force_interactive);
1215251881Speter
1216251881Speter  if (trust_server_cert && !non_interactive)
1217251881Speter    {
1218251881Speter      err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1219251881Speter                             _("--trust-server-cert requires "
1220251881Speter                               "--non-interactive"));
1221251881Speter      return svn_cmdline_handle_exit_error(err, pool, "svnmucc: ");
1222251881Speter    }
1223251881Speter
1224251881Speter  /* Make sure we have a log message to use. */
1225251881Speter  err = sanitize_log_sources(revprops, message, filedata);
1226251881Speter  if (err)
1227251881Speter    handle_error(err, pool);
1228251881Speter
1229251881Speter  /* Copy the rest of our command-line arguments to an array,
1230251881Speter     UTF-8-ing them along the way. */
1231251881Speter  action_args = apr_array_make(pool, opts->argc, sizeof(const char *));
1232251881Speter  while (opts->ind < opts->argc)
1233251881Speter    {
1234251881Speter      const char *arg = opts->argv[opts->ind++];
1235251881Speter      if ((err = svn_utf_cstring_to_utf8(&(APR_ARRAY_PUSH(action_args,
1236251881Speter                                                          const char *)),
1237251881Speter                                         arg, pool)))
1238251881Speter        handle_error(err, pool);
1239251881Speter    }
1240251881Speter
1241251881Speter  /* If there are extra arguments in a supplementary file, tack those
1242251881Speter     on, too (again, in UTF8 form). */
1243251881Speter  if (extra_args_file)
1244251881Speter    {
1245251881Speter      const char *extra_args_file_utf8;
1246251881Speter      svn_stringbuf_t *contents, *contents_utf8;
1247251881Speter
1248251881Speter      err = svn_utf_cstring_to_utf8(&extra_args_file_utf8,
1249251881Speter                                    extra_args_file, pool);
1250251881Speter      if (! err)
1251251881Speter        err = svn_stringbuf_from_file2(&contents, extra_args_file_utf8, pool);
1252251881Speter      if (! err)
1253251881Speter        err = svn_utf_stringbuf_to_utf8(&contents_utf8, contents, pool);
1254251881Speter      if (err)
1255251881Speter        handle_error(err, pool);
1256251881Speter      svn_cstring_split_append(action_args, contents_utf8->data, "\n\r",
1257251881Speter                               FALSE, pool);
1258251881Speter    }
1259251881Speter
1260251881Speter  /* Now, we iterate over the combined set of arguments -- our actions. */
1261251881Speter  for (i = 0; i < action_args->nelts; )
1262251881Speter    {
1263251881Speter      int j, num_url_args;
1264251881Speter      const char *action_string = APR_ARRAY_IDX(action_args, i, const char *);
1265251881Speter      struct action *action = apr_pcalloc(pool, sizeof(*action));
1266251881Speter
1267251881Speter      /* First, parse the action. */
1268251881Speter      if (! strcmp(action_string, "mv"))
1269251881Speter        action->action = ACTION_MV;
1270251881Speter      else if (! strcmp(action_string, "cp"))
1271251881Speter        action->action = ACTION_CP;
1272251881Speter      else if (! strcmp(action_string, "mkdir"))
1273251881Speter        action->action = ACTION_MKDIR;
1274251881Speter      else if (! strcmp(action_string, "rm"))
1275251881Speter        action->action = ACTION_RM;
1276251881Speter      else if (! strcmp(action_string, "put"))
1277251881Speter        action->action = ACTION_PUT;
1278251881Speter      else if (! strcmp(action_string, "propset"))
1279251881Speter        action->action = ACTION_PROPSET;
1280251881Speter      else if (! strcmp(action_string, "propsetf"))
1281251881Speter        action->action = ACTION_PROPSETF;
1282251881Speter      else if (! strcmp(action_string, "propdel"))
1283251881Speter        action->action = ACTION_PROPDEL;
1284251881Speter      else if (! strcmp(action_string, "?") || ! strcmp(action_string, "h")
1285251881Speter               || ! strcmp(action_string, "help"))
1286251881Speter        usage(pool, EXIT_SUCCESS);
1287251881Speter      else
1288251881Speter        handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1289251881Speter                                       "'%s' is not an action\n",
1290251881Speter                                       action_string), pool);
1291251881Speter      if (++i == action_args->nelts)
1292251881Speter        insufficient(pool);
1293251881Speter
1294251881Speter      /* For copies, there should be a revision number next. */
1295251881Speter      if (action->action == ACTION_CP)
1296251881Speter        {
1297251881Speter          const char *rev_str = APR_ARRAY_IDX(action_args, i, const char *);
1298251881Speter          if (strcmp(rev_str, "head") == 0)
1299251881Speter            action->rev = SVN_INVALID_REVNUM;
1300251881Speter          else if (strcmp(rev_str, "HEAD") == 0)
1301251881Speter            action->rev = SVN_INVALID_REVNUM;
1302251881Speter          else
1303251881Speter            {
1304251881Speter              char *end;
1305251881Speter
1306251881Speter              while (*rev_str == 'r')
1307251881Speter                ++rev_str;
1308251881Speter
1309251881Speter              action->rev = strtol(rev_str, &end, 0);
1310251881Speter              if (*end)
1311251881Speter                handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1312251881Speter                                               "'%s' is not a revision\n",
1313251881Speter                                               rev_str), pool);
1314251881Speter            }
1315251881Speter          if (++i == action_args->nelts)
1316251881Speter            insufficient(pool);
1317251881Speter        }
1318251881Speter      else
1319251881Speter        {
1320251881Speter          action->rev = SVN_INVALID_REVNUM;
1321251881Speter        }
1322251881Speter
1323251881Speter      /* For puts, there should be a local file next. */
1324251881Speter      if (action->action == ACTION_PUT)
1325251881Speter        {
1326251881Speter          action->path[1] =
1327251881Speter            svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
1328251881Speter                                                    const char *), pool);
1329251881Speter          if (++i == action_args->nelts)
1330251881Speter            insufficient(pool);
1331251881Speter        }
1332251881Speter
1333251881Speter      /* For propset, propsetf, and propdel, a property name (and
1334251881Speter         maybe a property value or file which contains one) comes next. */
1335251881Speter      if ((action->action == ACTION_PROPSET)
1336251881Speter          || (action->action == ACTION_PROPSETF)
1337251881Speter          || (action->action == ACTION_PROPDEL))
1338251881Speter        {
1339251881Speter          action->prop_name = APR_ARRAY_IDX(action_args, i, const char *);
1340251881Speter          if (++i == action_args->nelts)
1341251881Speter            insufficient(pool);
1342251881Speter
1343251881Speter          if (action->action == ACTION_PROPDEL)
1344251881Speter            {
1345251881Speter              action->prop_value = NULL;
1346251881Speter            }
1347251881Speter          else if (action->action == ACTION_PROPSET)
1348251881Speter            {
1349251881Speter              action->prop_value =
1350251881Speter                svn_string_create(APR_ARRAY_IDX(action_args, i,
1351251881Speter                                                const char *), pool);
1352251881Speter              if (++i == action_args->nelts)
1353251881Speter                insufficient(pool);
1354251881Speter            }
1355251881Speter          else
1356251881Speter            {
1357251881Speter              const char *propval_file =
1358251881Speter                svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
1359251881Speter                                                        const char *), pool);
1360251881Speter
1361251881Speter              if (++i == action_args->nelts)
1362251881Speter                insufficient(pool);
1363251881Speter
1364251881Speter              err = read_propvalue_file(&(action->prop_value),
1365251881Speter                                        propval_file, pool);
1366251881Speter              if (err)
1367251881Speter                handle_error(err, pool);
1368251881Speter
1369251881Speter              action->action = ACTION_PROPSET;
1370251881Speter            }
1371251881Speter
1372251881Speter          if (action->prop_value
1373251881Speter              && svn_prop_needs_translation(action->prop_name))
1374251881Speter            {
1375251881Speter              svn_string_t *translated_value;
1376251881Speter              err = svn_subst_translate_string2(&translated_value, NULL,
1377251881Speter                                                NULL, action->prop_value, NULL,
1378251881Speter                                                FALSE, pool, pool);
1379251881Speter              if (err)
1380251881Speter                handle_error(
1381251881Speter                    svn_error_quick_wrap(err,
1382251881Speter                                         "Error normalizing property value"),
1383251881Speter                    pool);
1384251881Speter              action->prop_value = translated_value;
1385251881Speter            }
1386251881Speter        }
1387251881Speter
1388251881Speter      /* How many URLs does this action expect? */
1389251881Speter      if (action->action == ACTION_RM
1390251881Speter          || action->action == ACTION_MKDIR
1391251881Speter          || action->action == ACTION_PUT
1392251881Speter          || action->action == ACTION_PROPSET
1393251881Speter          || action->action == ACTION_PROPSETF /* shouldn't see this one */
1394251881Speter          || action->action == ACTION_PROPDEL)
1395251881Speter        num_url_args = 1;
1396251881Speter      else
1397251881Speter        num_url_args = 2;
1398251881Speter
1399251881Speter      /* Parse the required number of URLs. */
1400251881Speter      for (j = 0; j < num_url_args; ++j)
1401251881Speter        {
1402251881Speter          const char *url = APR_ARRAY_IDX(action_args, i, const char *);
1403251881Speter
1404251881Speter          /* If there's a ROOT_URL, we expect URL to be a path
1405251881Speter             relative to ROOT_URL (and we build a full url from the
1406251881Speter             combination of the two).  Otherwise, it should be a full
1407251881Speter             url. */
1408251881Speter          if (! svn_path_is_url(url))
1409251881Speter            {
1410251881Speter              if (! root_url)
1411251881Speter                handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1412251881Speter                                               "'%s' is not a URL, and "
1413251881Speter                                               "--root-url (-U) not provided\n",
1414251881Speter                                               url), pool);
1415251881Speter              /* ### These relpaths are already URI-encoded. */
1416251881Speter              url = apr_pstrcat(pool, root_url, "/",
1417251881Speter                                svn_relpath_canonicalize(url, pool),
1418251881Speter                                (char *)NULL);
1419251881Speter            }
1420251881Speter          url = sanitize_url(url, pool);
1421251881Speter          action->path[j] = url;
1422251881Speter
1423251881Speter          /* The first URL arguments to 'cp', 'pd', 'ps' could be the anchor,
1424251881Speter             but the other URLs should be children of the anchor. */
1425251881Speter          if (! (action->action == ACTION_CP && j == 0)
1426251881Speter              && action->action != ACTION_PROPDEL
1427251881Speter              && action->action != ACTION_PROPSET
1428251881Speter              && action->action != ACTION_PROPSETF)
1429251881Speter            url = svn_uri_dirname(url, pool);
1430251881Speter          if (! anchor)
1431251881Speter            anchor = url;
1432251881Speter          else
1433262253Speter            {
1434262253Speter              anchor = svn_uri_get_longest_ancestor(anchor, url, pool);
1435262253Speter              if (!anchor || !anchor[0])
1436262253Speter                handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1437262253Speter                                               "URLs in the action list do not "
1438262253Speter                                               "share a common ancestor"),
1439262253Speter                             pool);
1440262253Speter            }
1441251881Speter
1442251881Speter          if ((++i == action_args->nelts) && (j + 1 < num_url_args))
1443251881Speter            insufficient(pool);
1444251881Speter        }
1445251881Speter      APR_ARRAY_PUSH(actions, struct action *) = action;
1446251881Speter    }
1447251881Speter
1448251881Speter  if (! actions->nelts)
1449251881Speter    usage(pool, EXIT_FAILURE);
1450251881Speter
1451251881Speter  if ((err = execute(actions, anchor, revprops, username, password,
1452251881Speter                     config_dir, config_options, non_interactive,
1453251881Speter                     trust_server_cert, no_auth_cache, base_revision, pool)))
1454251881Speter    {
1455251881Speter      if (err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive)
1456251881Speter        err = svn_error_quick_wrap(err,
1457251881Speter                                   _("Authentication failed and interactive"
1458251881Speter                                     " prompting is disabled; see the"
1459251881Speter                                     " --force-interactive option"));
1460251881Speter      handle_error(err, pool);
1461251881Speter    }
1462251881Speter
1463251881Speter  /* Ensure that stdout is flushed, so the user will see all results. */
1464251881Speter  svn_error_clear(svn_cmdline_fflush(stdout));
1465251881Speter
1466251881Speter  svn_pool_destroy(pool);
1467251881Speter  return EXIT_SUCCESS;
1468251881Speter}
1469