2 * svnmucc.c: Subversion Multiple URL Client
4 * ====================================================================
5 * Licensed to the Apache Software Foundation (ASF) under one
6 * or more contributor license agreements. See the NOTICE file
7 * distributed with this work for additional information
8 * regarding copyright ownership. The ASF licenses this file
9 * to you under the Apache License, Version 2.0 (the
10 * "License"); you may not use this file except in compliance
11 * with the License. You may obtain a copy of the License at
13 * http://www.apache.org/licenses/LICENSE-2.0
15 * Unless required by applicable law or agreed to in writing,
16 * software distributed under the License is distributed on an
17 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18 * KIND, either express or implied. See the License for the
19 * specific language governing permissions and limitations
21 * ====================================================================
25 /* Multiple URL Command Client
27 Combine a list of mv, cp and rm commands on URLs into a single commit.
29 How it works: the command line arguments are parsed into an array of
30 action structures. The action structures are interpreted to build a
31 tree of operation structures. The tree of operation structures is
32 used to drive an RA commit editor to produce a single commit.
34 To build this client, type 'make svnmucc' from the root of your
35 Subversion source directory.
44 #include "svn_client.h"
45 #include "svn_cmdline.h"
46 #include "svn_config.h"
47 #include "svn_error.h"
49 #include "svn_pools.h"
50 #include "svn_props.h"
52 #include "svn_string.h"
53 #include "svn_subst.h"
55 #include "svn_version.h"
57 #include "private/svn_cmdline_private.h"
58 #include "private/svn_ra_private.h"
59 #include "private/svn_string_private.h"
60 #include "private/svn_subr_private.h"
62 #include "svn_private_config.h"
64 static void handle_error(svn_error_t *err, apr_pool_t *pool)
67 svn_handle_error2(err, stderr, FALSE, "svnmucc: ");
70 svn_pool_destroy(pool);
75 init(const char *application)
78 const svn_version_checklist_t checklist[] = {
79 {"svn_client", svn_client_version},
80 {"svn_subr", svn_subr_version},
81 {"svn_ra", svn_ra_version},
84 SVN_VERSION_DEFINE(my_version);
86 if (svn_cmdline_init(application, stderr))
89 err = svn_ver_check_list2(&my_version, checklist, svn_ver_equal);
91 handle_error(err, NULL);
93 return apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
97 open_tmp_file(apr_file_t **fp,
101 /* Open a unique file; use APR_DELONCLOSE. */
102 return svn_io_open_unique_file3(fp, NULL, NULL, svn_io_file_del_on_close,
107 create_ra_callbacks(svn_ra_callbacks2_t **callbacks,
108 const char *username,
109 const char *password,
110 const char *config_dir,
111 svn_config_t *cfg_config,
112 svn_boolean_t non_interactive,
113 svn_boolean_t trust_server_cert,
114 svn_boolean_t no_auth_cache,
117 SVN_ERR(svn_ra_create_callbacks(callbacks, pool));
119 SVN_ERR(svn_cmdline_create_auth_baton(&(*callbacks)->auth_baton,
121 username, password, config_dir,
124 cfg_config, NULL, NULL, pool));
126 (*callbacks)->open_tmp_file = open_tmp_file;
134 commit_callback(const svn_commit_info_t *commit_info,
138 SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n",
139 commit_info->revision,
141 ? commit_info->author : "(no author)"),
146 typedef enum action_code_t {
163 OP_PROPSET /* only for files for which no other operation is
164 occuring; directories are OP_OPEN with non-empty
167 svn_node_kind_t kind; /* to copy, mkdir, put or set revprops */
168 svn_revnum_t rev; /* to copy, valid for add and replace */
169 const char *url; /* to copy, valid for add and replace */
170 const char *src_file; /* for put, the source file for contents */
171 apr_hash_t *children; /* const char *path -> struct operation * */
172 apr_hash_t *prop_mods; /* const char *prop_name ->
173 const svn_string_t *prop_value */
174 apr_array_header_t *prop_dels; /* const char *prop_name deletions */
175 void *baton; /* as returned by the commit editor */
179 /* An iterator (for use via apr_table_do) which sets node properties.
180 REC is a pointer to a struct driver_state. */
182 change_props(const svn_delta_editor_t *editor,
184 struct operation *child,
187 apr_pool_t *iterpool = svn_pool_create(pool);
189 if (child->prop_dels)
192 for (i = 0; i < child->prop_dels->nelts; i++)
194 const char *prop_name;
196 svn_pool_clear(iterpool);
197 prop_name = APR_ARRAY_IDX(child->prop_dels, i, const char *);
198 if (child->kind == svn_node_dir)
199 SVN_ERR(editor->change_dir_prop(baton, prop_name,
202 SVN_ERR(editor->change_file_prop(baton, prop_name,
206 if (apr_hash_count(child->prop_mods))
208 apr_hash_index_t *hi;
209 for (hi = apr_hash_first(pool, child->prop_mods);
210 hi; hi = apr_hash_next(hi))
212 const char *propname = svn__apr_hash_index_key(hi);
213 const svn_string_t *val = svn__apr_hash_index_val(hi);
215 svn_pool_clear(iterpool);
216 if (child->kind == svn_node_dir)
217 SVN_ERR(editor->change_dir_prop(baton, propname, val, iterpool));
219 SVN_ERR(editor->change_file_prop(baton, propname, val, iterpool));
223 svn_pool_destroy(iterpool);
228 /* Drive EDITOR to affect the change represented by OPERATION. HEAD
229 is the last-known youngest revision in the repository. */
231 drive(struct operation *operation,
233 const svn_delta_editor_t *editor,
236 apr_pool_t *subpool = svn_pool_create(pool);
237 apr_hash_index_t *hi;
239 for (hi = apr_hash_first(pool, operation->children);
240 hi; hi = apr_hash_next(hi))
242 const char *key = svn__apr_hash_index_key(hi);
243 struct operation *child = svn__apr_hash_index_val(hi);
244 void *file_baton = NULL;
246 svn_pool_clear(subpool);
248 /* Deletes and replacements are simple -- delete something. */
249 if (child->operation == OP_DELETE || child->operation == OP_REPLACE)
251 SVN_ERR(editor->delete_entry(key, head, operation->baton, subpool));
253 /* Opens could be for directories or files. */
254 if (child->operation == OP_OPEN || child->operation == OP_PROPSET)
256 if (child->kind == svn_node_dir)
258 SVN_ERR(editor->open_directory(key, operation->baton, head,
259 subpool, &child->baton));
263 SVN_ERR(editor->open_file(key, operation->baton, head,
264 subpool, &file_baton));
267 /* Adds and replacements could also be for directories or files. */
268 if (child->operation == OP_ADD || child->operation == OP_REPLACE)
270 if (child->kind == svn_node_dir)
272 SVN_ERR(editor->add_directory(key, operation->baton,
273 child->url, child->rev,
274 subpool, &child->baton));
278 SVN_ERR(editor->add_file(key, operation->baton, child->url,
279 child->rev, subpool, &file_baton));
282 /* If there's a source file and an open file baton, we get to
283 change textual contents. */
284 if ((child->src_file) && (file_baton))
286 svn_txdelta_window_handler_t handler;
288 svn_stream_t *contents;
290 SVN_ERR(editor->apply_textdelta(file_baton, NULL, subpool,
291 &handler, &handler_baton));
292 if (strcmp(child->src_file, "-") != 0)
294 SVN_ERR(svn_stream_open_readonly(&contents, child->src_file,
299 SVN_ERR(svn_stream_for_stdin(&contents, pool));
301 SVN_ERR(svn_txdelta_send_stream(contents, handler,
302 handler_baton, NULL, pool));
304 /* If we opened a file, we need to apply outstanding propmods,
308 if (child->kind == svn_node_file)
310 SVN_ERR(change_props(editor, file_baton, child, subpool));
312 SVN_ERR(editor->close_file(file_baton, NULL, subpool));
314 /* If we opened, added, or replaced a directory, we need to
315 recurse, apply outstanding propmods, and then close it. */
316 if ((child->kind == svn_node_dir)
317 && child->operation != OP_DELETE)
319 SVN_ERR(change_props(editor, child->baton, child, subpool));
321 SVN_ERR(drive(child, head, editor, subpool));
323 SVN_ERR(editor->close_directory(child->baton, subpool));
326 svn_pool_destroy(subpool);
331 /* Find the operation associated with PATH, which is a single-path
332 component representing a child of the path represented by
333 OPERATION. If no such child operation exists, create a new one of
335 static struct operation *
336 get_operation(const char *path,
337 struct operation *operation,
340 struct operation *child = svn_hash_gets(operation->children, path);
343 child = apr_pcalloc(pool, sizeof(*child));
344 child->children = apr_hash_make(pool);
345 child->operation = OP_OPEN;
346 child->rev = SVN_INVALID_REVNUM;
347 child->kind = svn_node_dir;
348 child->prop_mods = apr_hash_make(pool);
349 child->prop_dels = apr_array_make(pool, 1, sizeof(const char *));
350 svn_hash_sets(operation->children, path, child);
355 /* Return the portion of URL that is relative to ANCHOR (URI-decoded). */
357 subtract_anchor(const char *anchor, const char *url, apr_pool_t *pool)
359 return svn_uri_skip_ancestor(anchor, url, pool);
362 /* Add PATH to the operations tree rooted at OPERATION, creating any
363 intermediate nodes that are required. Here's what's expected for
366 ACTION URL REV SRC-FILE PROPNAME
367 ------------ ----- ------- -------- --------
368 ACTION_MKDIR NULL invalid NULL NULL
369 ACTION_CP valid valid NULL NULL
370 ACTION_PUT NULL invalid valid NULL
371 ACTION_RM NULL invalid NULL NULL
372 ACTION_PROPSET valid invalid NULL valid
373 ACTION_PROPDEL valid invalid NULL valid
375 Node type information is obtained for any copy source (to determine
376 whether to create a file or directory) and for any deleted path (to
377 ensure it exists since svn_delta_editor_t->delete_entry doesn't
378 return an error on non-existent nodes). */
380 build(action_code_t action,
384 const char *prop_name,
385 const svn_string_t *prop_value,
386 const char *src_file,
389 svn_ra_session_t *session,
390 struct operation *operation,
393 apr_array_header_t *path_bits = svn_path_decompose(path, pool);
394 const char *path_so_far = "";
395 const char *copy_src = NULL;
396 svn_revnum_t copy_rev = SVN_INVALID_REVNUM;
399 /* Look for any previous operations we've recognized for PATH. If
400 any of PATH's ancestors have not yet been traversed, we'll be
401 creating OP_OPEN operations for them as we walk down PATH's path
403 for (i = 0; i < path_bits->nelts; ++i)
405 const char *path_bit = APR_ARRAY_IDX(path_bits, i, const char *);
406 path_so_far = svn_relpath_join(path_so_far, path_bit, pool);
407 operation = get_operation(path_so_far, operation, pool);
409 /* If we cross a replace- or add-with-history, remember the
410 source of those things in case we need to lookup the node kind
411 of one of their children. And if this isn't such a copy,
412 but we've already seen one in of our parent paths, we just need
413 to extend that copy source path by our current path
416 && SVN_IS_VALID_REVNUM(operation->rev)
417 && (operation->operation == OP_REPLACE
418 || operation->operation == OP_ADD))
420 copy_src = subtract_anchor(anchor, operation->url, pool);
421 copy_rev = operation->rev;
425 copy_src = svn_relpath_join(copy_src, path_bit, pool);
429 /* Handle property changes. */
432 if (operation->operation == OP_DELETE)
433 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
434 "cannot set properties on a location being"
435 " deleted ('%s')", path);
436 /* If we're not adding this thing ourselves, check for existence. */
437 if (! ((operation->operation == OP_ADD) ||
438 (operation->operation == OP_REPLACE)))
440 SVN_ERR(svn_ra_check_path(session,
441 copy_src ? copy_src : path,
442 copy_src ? copy_rev : head,
443 &operation->kind, pool));
444 if (operation->kind == svn_node_none)
445 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
446 "propset: '%s' not found", path);
447 else if ((operation->kind == svn_node_file)
448 && (operation->operation == OP_OPEN))
449 operation->operation = OP_PROPSET;
452 APR_ARRAY_PUSH(operation->prop_dels, const char *) = prop_name;
454 svn_hash_sets(operation->prop_mods, prop_name, prop_value);
456 operation->rev = rev;
460 /* We won't fuss about multiple operations on the same path in the
463 - the prior operation was, in fact, a no-op (open)
464 - the prior operation was a propset placeholder
465 - the prior operation was a deletion
467 Note: while the operation structure certainly supports the
468 ability to do a copy of a file followed by a put of new contents
469 for the file, we don't let that happen (yet).
471 if (operation->operation != OP_OPEN
472 && operation->operation != OP_PROPSET
473 && operation->operation != OP_DELETE)
474 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
475 "unsupported multiple operations on '%s'", path);
477 /* For deletions, we validate that there's actually something to
478 delete. If this is a deletion of the child of a copied
479 directory, we need to remember to look in the copy source tree to
480 verify that this thing actually exists. */
481 if (action == ACTION_RM)
483 operation->operation = OP_DELETE;
484 SVN_ERR(svn_ra_check_path(session,
485 copy_src ? copy_src : path,
486 copy_src ? copy_rev : head,
487 &operation->kind, pool));
488 if (operation->kind == svn_node_none)
490 if (copy_src && strcmp(path, copy_src))
491 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
492 "'%s' (from '%s:%ld') not found",
493 path, copy_src, copy_rev);
495 return svn_error_createf(SVN_ERR_BAD_URL, NULL, "'%s' not found",
499 /* Handle copy operations (which can be adds or replacements). */
500 else if (action == ACTION_CP)
503 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
504 "Copy source revision cannot be younger "
505 "than base revision");
506 operation->operation =
507 operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
508 if (operation->operation == OP_ADD)
510 /* There is a bug in the current version of mod_dav_svn
511 which incorrectly replaces existing directories.
512 Therefore we need to check if the target exists
513 and raise an error here. */
514 SVN_ERR(svn_ra_check_path(session,
515 copy_src ? copy_src : path,
516 copy_src ? copy_rev : head,
517 &operation->kind, pool));
518 if (operation->kind != svn_node_none)
520 if (copy_src && strcmp(path, copy_src))
521 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
522 "'%s' (from '%s:%ld') already exists",
523 path, copy_src, copy_rev);
525 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
526 "'%s' already exists", path);
529 SVN_ERR(svn_ra_check_path(session, subtract_anchor(anchor, url, pool),
530 rev, &operation->kind, pool));
531 if (operation->kind == svn_node_none)
532 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
534 subtract_anchor(anchor, url, pool));
535 operation->url = url;
536 operation->rev = rev;
538 /* Handle mkdir operations (which can be adds or replacements). */
539 else if (action == ACTION_MKDIR)
541 operation->operation =
542 operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
543 operation->kind = svn_node_dir;
545 /* Handle put operations (which can be adds, replacements, or opens). */
546 else if (action == ACTION_PUT)
548 if (operation->operation == OP_DELETE)
550 operation->operation = OP_REPLACE;
554 SVN_ERR(svn_ra_check_path(session,
555 copy_src ? copy_src : path,
556 copy_src ? copy_rev : head,
557 &operation->kind, pool));
558 if (operation->kind == svn_node_file)
559 operation->operation = OP_OPEN;
560 else if (operation->kind == svn_node_none)
561 operation->operation = OP_ADD;
563 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
564 "'%s' is not a file", path);
566 operation->kind = svn_node_file;
567 operation->src_file = src_file;
571 /* We shouldn't get here. */
572 SVN_ERR_MALFUNCTION();
579 action_code_t action;
581 /* revision (copy-from-rev of path[0] for cp; base-rev for put) */
584 /* action path[0] path[1]
585 * ------ ------- -------
587 * mkdir target (null)
591 * propset target (null)
595 /* property name/value */
596 const char *prop_name;
597 const svn_string_t *prop_value;
602 svn_ra_session_t *session;
607 fetch_base_func(const char **filename,
610 svn_revnum_t base_revision,
611 apr_pool_t *result_pool,
612 apr_pool_t *scratch_pool)
614 struct fetch_baton *fb = baton;
615 svn_stream_t *fstream;
618 if (! SVN_IS_VALID_REVNUM(base_revision))
619 base_revision = fb->head;
621 SVN_ERR(svn_stream_open_unique(&fstream, filename, NULL,
622 svn_io_file_del_on_pool_cleanup,
623 result_pool, scratch_pool));
625 err = svn_ra_get_file(fb->session, path, base_revision, fstream, NULL, NULL,
627 if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
629 svn_error_clear(err);
630 SVN_ERR(svn_stream_close(fstream));
636 return svn_error_trace(err);
638 SVN_ERR(svn_stream_close(fstream));
644 fetch_props_func(apr_hash_t **props,
647 svn_revnum_t base_revision,
648 apr_pool_t *result_pool,
649 apr_pool_t *scratch_pool)
651 struct fetch_baton *fb = baton;
652 svn_node_kind_t node_kind;
654 if (! SVN_IS_VALID_REVNUM(base_revision))
655 base_revision = fb->head;
657 SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, &node_kind,
660 if (node_kind == svn_node_file)
662 SVN_ERR(svn_ra_get_file(fb->session, path, base_revision, NULL, NULL,
663 props, result_pool));
665 else if (node_kind == svn_node_dir)
667 apr_array_header_t *tmp_props;
669 SVN_ERR(svn_ra_get_dir2(fb->session, NULL, NULL, props, path,
670 base_revision, 0 /* Dirent fields */,
672 tmp_props = svn_prop_hash_to_array(*props, result_pool);
673 SVN_ERR(svn_categorize_props(tmp_props, NULL, NULL, &tmp_props,
675 *props = svn_prop_array_to_hash(tmp_props, result_pool);
679 *props = apr_hash_make(result_pool);
686 fetch_kind_func(svn_node_kind_t *kind,
689 svn_revnum_t base_revision,
690 apr_pool_t *scratch_pool)
692 struct fetch_baton *fb = baton;
694 if (! SVN_IS_VALID_REVNUM(base_revision))
695 base_revision = fb->head;
697 SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, kind,
703 static svn_delta_shim_callbacks_t *
704 get_shim_callbacks(svn_ra_session_t *session,
706 apr_pool_t *result_pool)
708 svn_delta_shim_callbacks_t *callbacks =
709 svn_delta_shim_callbacks_default(result_pool);
710 struct fetch_baton *fb = apr_pcalloc(result_pool, sizeof(*fb));
712 fb->session = session;
715 callbacks->fetch_props_func = fetch_props_func;
716 callbacks->fetch_kind_func = fetch_kind_func;
717 callbacks->fetch_base_func = fetch_base_func;
718 callbacks->fetch_baton = fb;
724 execute(const apr_array_header_t *actions,
726 apr_hash_t *revprops,
727 const char *username,
728 const char *password,
729 const char *config_dir,
730 const apr_array_header_t *config_options,
731 svn_boolean_t non_interactive,
732 svn_boolean_t trust_server_cert,
733 svn_boolean_t no_auth_cache,
734 svn_revnum_t base_revision,
737 svn_ra_session_t *session;
738 svn_ra_session_t *aux_session;
739 const char *repos_root;
741 const svn_delta_editor_t *editor;
742 svn_ra_callbacks2_t *ra_callbacks;
744 struct operation root;
747 svn_config_t *cfg_config;
750 SVN_ERR(svn_config_get_config(&config, config_dir, pool));
751 SVN_ERR(svn_cmdline__apply_config_options(config, config_options,
752 "svnmucc: ", "--config-option"));
753 cfg_config = svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG);
755 if (! svn_hash_gets(revprops, SVN_PROP_REVISION_LOG))
757 svn_string_t *msg = svn_string_create("", pool);
759 /* If we can do so, try to pop up $EDITOR to fetch a log message. */
762 return svn_error_create
763 (SVN_ERR_CL_INSUFFICIENT_ARGS, NULL,
764 _("Cannot invoke editor to get log message "
765 "when non-interactive"));
769 SVN_ERR(svn_cmdline__edit_string_externally(
770 &msg, NULL, NULL, "", msg, "svnmucc-commit", config,
771 TRUE, NULL, apr_hash_pool_get(revprops)));
774 svn_hash_sets(revprops, SVN_PROP_REVISION_LOG, msg);
777 SVN_ERR(create_ra_callbacks(&ra_callbacks, username, password, config_dir,
778 cfg_config, non_interactive, trust_server_cert,
779 no_auth_cache, pool));
780 SVN_ERR(svn_ra_open4(&session, NULL, anchor, NULL, ra_callbacks,
781 NULL, config, pool));
782 /* Open, then reparent to avoid AUTHZ errors when opening the reposroot */
783 SVN_ERR(svn_ra_open4(&aux_session, NULL, anchor, NULL, ra_callbacks,
784 NULL, config, pool));
785 SVN_ERR(svn_ra_get_repos_root2(aux_session, &repos_root, pool));
786 SVN_ERR(svn_ra_reparent(aux_session, repos_root, pool));
787 SVN_ERR(svn_ra_get_latest_revnum(session, &head, pool));
789 /* Reparent to ANCHOR's dir, if ANCHOR is not a directory. */
791 svn_node_kind_t kind;
793 SVN_ERR(svn_ra_check_path(aux_session,
794 svn_uri_skip_ancestor(repos_root, anchor, pool),
796 if (kind != svn_node_dir)
798 anchor = svn_uri_dirname(anchor, pool);
799 SVN_ERR(svn_ra_reparent(session, anchor, pool));
803 if (SVN_IS_VALID_REVNUM(base_revision))
805 if (base_revision > head)
806 return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL,
807 "No such revision %ld (youngest is %ld)",
808 base_revision, head);
809 head = base_revision;
812 memset(&root, 0, sizeof(root));
813 root.children = apr_hash_make(pool);
814 root.operation = OP_OPEN;
815 root.kind = svn_node_dir; /* For setting properties */
816 root.prop_mods = apr_hash_make(pool);
817 root.prop_dels = apr_array_make(pool, 1, sizeof(const char *));
819 for (i = 0; i < actions->nelts; ++i)
821 struct action *action = APR_ARRAY_IDX(actions, i, struct action *);
822 const char *path1, *path2;
823 switch (action->action)
826 path1 = subtract_anchor(anchor, action->path[0], pool);
827 path2 = subtract_anchor(anchor, action->path[1], pool);
828 SVN_ERR(build(ACTION_RM, path1, NULL,
829 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
830 session, &root, pool));
831 SVN_ERR(build(ACTION_CP, path2, action->path[0],
832 head, NULL, NULL, NULL, head, anchor,
833 session, &root, pool));
836 path2 = subtract_anchor(anchor, action->path[1], pool);
837 if (action->rev == SVN_INVALID_REVNUM)
839 SVN_ERR(build(ACTION_CP, path2, action->path[0],
840 action->rev, NULL, NULL, NULL, head, anchor,
841 session, &root, pool));
844 path1 = subtract_anchor(anchor, action->path[0], pool);
845 SVN_ERR(build(ACTION_RM, path1, NULL,
846 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
847 session, &root, pool));
850 path1 = subtract_anchor(anchor, action->path[0], pool);
851 SVN_ERR(build(ACTION_MKDIR, path1, action->path[0],
852 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
853 session, &root, pool));
856 path1 = subtract_anchor(anchor, action->path[0], pool);
857 SVN_ERR(build(ACTION_PUT, path1, action->path[0],
858 SVN_INVALID_REVNUM, NULL, NULL, action->path[1],
859 head, anchor, session, &root, pool));
863 path1 = subtract_anchor(anchor, action->path[0], pool);
864 SVN_ERR(build(action->action, path1, action->path[0],
866 action->prop_name, action->prop_value,
867 NULL, head, anchor, session, &root, pool));
869 case ACTION_PROPSETF:
871 SVN_ERR_MALFUNCTION_NO_RETURN();
875 SVN_ERR(svn_ra__register_editor_shim_callbacks(session,
876 get_shim_callbacks(aux_session, head, pool)));
877 SVN_ERR(svn_ra_get_commit_editor3(session, &editor, &editor_baton, revprops,
878 commit_callback, NULL, NULL, FALSE, pool));
880 SVN_ERR(editor->open_root(editor_baton, head, pool, &root.baton));
881 err = change_props(editor, root.baton, &root, pool);
883 err = drive(&root, head, editor, pool);
885 err = editor->close_directory(root.baton, pool);
887 err = editor->close_edit(editor_baton, pool);
890 err = svn_error_compose_create(err,
891 editor->abort_edit(editor_baton, pool));
897 read_propvalue_file(const svn_string_t **value_p,
898 const char *filename,
901 svn_stringbuf_t *value;
902 apr_pool_t *scratch_pool = svn_pool_create(pool);
904 SVN_ERR(svn_stringbuf_from_file2(&value, filename, scratch_pool));
905 *value_p = svn_string_create_from_buf(value, pool);
906 svn_pool_destroy(scratch_pool);
910 /* Perform the typical suite of manipulations for user-provided URLs
911 on URL, returning the result (allocated from POOL): IRI-to-URI
912 conversion, auto-escaping, and canonicalization. */
914 sanitize_url(const char *url,
917 url = svn_path_uri_from_iri(url, pool);
918 url = svn_path_uri_autoescape(url, pool);
919 return svn_uri_canonicalize(url, pool);
923 usage(apr_pool_t *pool, int exit_val)
925 FILE *stream = exit_val == EXIT_SUCCESS ? stdout : stderr;
926 svn_error_clear(svn_cmdline_fputs(
927 _("Subversion multiple URL command client\n"
928 "usage: svnmucc ACTION...\n"
930 " Perform one or more Subversion repository URL-based ACTIONs, committing\n"
931 " the result as a (single) new revision.\n"
934 " cp REV SRC-URL DST-URL : copy SRC-URL@REV to DST-URL\n"
935 " mkdir URL : create new directory URL\n"
936 " mv SRC-URL DST-URL : move SRC-URL to DST-URL\n"
937 " rm URL : delete URL\n"
938 " put SRC-FILE URL : add or modify file URL with contents copied from\n"
939 " SRC-FILE (use \"-\" to read from standard input)\n"
940 " propset NAME VALUE URL : set property NAME on URL to VALUE\n"
941 " propsetf NAME FILE URL : set property NAME on URL to value read from FILE\n"
942 " propdel NAME URL : delete property NAME from URL\n"
945 " -h, -? [--help] : display this text\n"
946 " -m [--message] ARG : use ARG as a log message\n"
947 " -F [--file] ARG : read log message from file ARG\n"
948 " -u [--username] ARG : commit the changes as username ARG\n"
949 " -p [--password] ARG : use ARG as the password\n"
950 " -U [--root-url] ARG : interpret all action URLs relative to ARG\n"
951 " -r [--revision] ARG : use revision ARG as baseline for changes\n"
952 " --with-revprop ARG : set revision property in the following format:\n"
954 " --non-interactive : do no interactive prompting (default is to\n"
955 " prompt only if standard input is a terminal)\n"
956 " --force-interactive : do interactive prompting even if standard\n"
957 " input is not a terminal\n"
958 " --trust-server-cert : accept SSL server certificates from unknown\n"
959 " certificate authorities without prompting (but\n"
960 " only with '--non-interactive')\n"
961 " -X [--extra-args] ARG : append arguments from file ARG (one per line;\n"
962 " use \"-\" to read from standard input)\n"
963 " --config-dir ARG : use ARG to override the config directory\n"
964 " --config-option ARG : use ARG to override a configuration option\n"
965 " --no-auth-cache : do not cache authentication tokens\n"
966 " --version : print version information\n"),
968 svn_pool_destroy(pool);
973 insufficient(apr_pool_t *pool)
975 handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
976 "insufficient arguments"),
981 display_version(apr_getopt_t *os, apr_pool_t *pool)
983 const char *ra_desc_start
984 = "The following repository access (RA) modules are available:\n\n";
985 svn_stringbuf_t *version_footer;
987 version_footer = svn_stringbuf_create(ra_desc_start, pool);
988 SVN_ERR(svn_ra_print_modules(version_footer, pool));
990 SVN_ERR(svn_opt_print_help4(os, "svnmucc", TRUE, FALSE, FALSE,
991 version_footer->data,
992 NULL, NULL, NULL, NULL, NULL, pool));
997 /* Return an error about the mutual exclusivity of the -m, -F, and
998 --with-revprop=svn:log command-line options. */
1000 mutually_exclusive_logs_error(void)
1002 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1003 _("--message (-m), --file (-F), and "
1004 "--with-revprop=svn:log are mutually "
1008 /* Ensure that the REVPROPS hash contains a command-line-provided log
1009 message, if any, and that there was but one source of such a thing
1010 provided on that command-line. */
1011 static svn_error_t *
1012 sanitize_log_sources(apr_hash_t *revprops,
1013 const char *message,
1014 svn_stringbuf_t *filedata)
1016 apr_pool_t *hash_pool = apr_hash_pool_get(revprops);
1018 /* If we already have a log message in the revprop hash, then just
1019 make sure the user didn't try to also use -m or -F. Otherwise,
1020 we need to consult -m or -F to find a log message, if any. */
1021 if (svn_hash_gets(revprops, SVN_PROP_REVISION_LOG))
1023 if (filedata || message)
1024 return mutually_exclusive_logs_error();
1029 return mutually_exclusive_logs_error();
1031 SVN_ERR(svn_utf_cstring_to_utf8(&message, filedata->data, hash_pool));
1032 svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
1033 svn_stringbuf__morph_into_string(filedata));
1037 svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
1038 svn_string_create(message, hash_pool));
1041 return SVN_NO_ERROR;
1045 main(int argc, const char **argv)
1047 apr_pool_t *pool = init("svnmucc");
1048 apr_array_header_t *actions = apr_array_make(pool, 1,
1049 sizeof(struct action *));
1050 const char *anchor = NULL;
1051 svn_error_t *err = SVN_NO_ERROR;
1054 config_dir_opt = SVN_OPT_FIRST_LONGOPT_ID,
1059 non_interactive_opt,
1060 force_interactive_opt,
1061 trust_server_cert_opt
1063 static const apr_getopt_option_t options[] = {
1064 {"message", 'm', 1, ""},
1065 {"file", 'F', 1, ""},
1066 {"username", 'u', 1, ""},
1067 {"password", 'p', 1, ""},
1068 {"root-url", 'U', 1, ""},
1069 {"revision", 'r', 1, ""},
1070 {"with-revprop", with_revprop_opt, 1, ""},
1071 {"extra-args", 'X', 1, ""},
1072 {"help", 'h', 0, ""},
1074 {"non-interactive", non_interactive_opt, 0, ""},
1075 {"force-interactive", force_interactive_opt, 0, ""},
1076 {"trust-server-cert", trust_server_cert_opt, 0, ""},
1077 {"config-dir", config_dir_opt, 1, ""},
1078 {"config-option", config_inline_opt, 1, ""},
1079 {"no-auth-cache", no_auth_cache_opt, 0, ""},
1080 {"version", version_opt, 0, ""},
1083 const char *message = NULL;
1084 svn_stringbuf_t *filedata = NULL;
1085 const char *username = NULL, *password = NULL;
1086 const char *root_url = NULL, *extra_args_file = NULL;
1087 const char *config_dir = NULL;
1088 apr_array_header_t *config_options;
1089 svn_boolean_t non_interactive = FALSE;
1090 svn_boolean_t force_interactive = FALSE;
1091 svn_boolean_t trust_server_cert = FALSE;
1092 svn_boolean_t no_auth_cache = FALSE;
1093 svn_revnum_t base_revision = SVN_INVALID_REVNUM;
1094 apr_array_header_t *action_args;
1095 apr_hash_t *revprops = apr_hash_make(pool);
1098 config_options = apr_array_make(pool, 0,
1099 sizeof(svn_cmdline__config_argument_t*));
1101 apr_getopt_init(&opts, pool, argc, argv);
1102 opts->interleave = 1;
1107 const char *opt_arg;
1109 apr_status_t status = apr_getopt_long(opts, options, &opt, &arg);
1110 if (APR_STATUS_IS_EOF(status))
1112 if (status != APR_SUCCESS)
1113 handle_error(svn_error_wrap_apr(status, "getopt failure"), pool);
1117 err = svn_utf_cstring_to_utf8(&message, arg, pool);
1119 handle_error(err, pool);
1123 const char *arg_utf8;
1124 err = svn_utf_cstring_to_utf8(&arg_utf8, arg, pool);
1126 err = svn_stringbuf_from_file2(&filedata, arg, pool);
1128 handle_error(err, pool);
1132 username = apr_pstrdup(pool, arg);
1135 password = apr_pstrdup(pool, arg);
1138 err = svn_utf_cstring_to_utf8(&root_url, arg, pool);
1140 handle_error(err, pool);
1141 if (! svn_path_is_url(root_url))
1142 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1143 "'%s' is not a URL\n", root_url),
1145 root_url = sanitize_url(root_url, pool);
1149 char *digits_end = NULL;
1150 base_revision = strtol(arg, &digits_end, 10);
1151 if ((! SVN_IS_VALID_REVNUM(base_revision))
1154 handle_error(svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR,
1155 NULL, "Invalid revision number"),
1159 case with_revprop_opt:
1160 err = svn_opt_parse_revprop(&revprops, arg, pool);
1161 if (err != SVN_NO_ERROR)
1162 handle_error(err, pool);
1165 extra_args_file = apr_pstrdup(pool, arg);
1167 case non_interactive_opt:
1168 non_interactive = TRUE;
1170 case force_interactive_opt:
1171 force_interactive = TRUE;
1173 case trust_server_cert_opt:
1174 trust_server_cert = TRUE;
1176 case config_dir_opt:
1177 err = svn_utf_cstring_to_utf8(&config_dir, arg, pool);
1179 handle_error(err, pool);
1181 case config_inline_opt:
1182 err = svn_utf_cstring_to_utf8(&opt_arg, arg, pool);
1184 handle_error(err, pool);
1186 err = svn_cmdline__parse_config_option(config_options, opt_arg,
1189 handle_error(err, pool);
1191 case no_auth_cache_opt:
1192 no_auth_cache = TRUE;
1195 SVN_INT_ERR(display_version(opts, pool));
1200 usage(pool, EXIT_SUCCESS);
1205 if (non_interactive && force_interactive)
1207 err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1208 _("--non-interactive and --force-interactive "
1209 "are mutually exclusive"));
1210 return svn_cmdline_handle_exit_error(err, pool, "svnmucc: ");
1213 non_interactive = !svn_cmdline__be_interactive(non_interactive,
1216 if (trust_server_cert && !non_interactive)
1218 err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1219 _("--trust-server-cert requires "
1220 "--non-interactive"));
1221 return svn_cmdline_handle_exit_error(err, pool, "svnmucc: ");
1224 /* Make sure we have a log message to use. */
1225 err = sanitize_log_sources(revprops, message, filedata);
1227 handle_error(err, pool);
1229 /* Copy the rest of our command-line arguments to an array,
1230 UTF-8-ing them along the way. */
1231 action_args = apr_array_make(pool, opts->argc, sizeof(const char *));
1232 while (opts->ind < opts->argc)
1234 const char *arg = opts->argv[opts->ind++];
1235 if ((err = svn_utf_cstring_to_utf8(&(APR_ARRAY_PUSH(action_args,
1238 handle_error(err, pool);
1241 /* If there are extra arguments in a supplementary file, tack those
1242 on, too (again, in UTF8 form). */
1243 if (extra_args_file)
1245 const char *extra_args_file_utf8;
1246 svn_stringbuf_t *contents, *contents_utf8;
1248 err = svn_utf_cstring_to_utf8(&extra_args_file_utf8,
1249 extra_args_file, pool);
1251 err = svn_stringbuf_from_file2(&contents, extra_args_file_utf8, pool);
1253 err = svn_utf_stringbuf_to_utf8(&contents_utf8, contents, pool);
1255 handle_error(err, pool);
1256 svn_cstring_split_append(action_args, contents_utf8->data, "\n\r",
1260 /* Now, we iterate over the combined set of arguments -- our actions. */
1261 for (i = 0; i < action_args->nelts; )
1263 int j, num_url_args;
1264 const char *action_string = APR_ARRAY_IDX(action_args, i, const char *);
1265 struct action *action = apr_pcalloc(pool, sizeof(*action));
1267 /* First, parse the action. */
1268 if (! strcmp(action_string, "mv"))
1269 action->action = ACTION_MV;
1270 else if (! strcmp(action_string, "cp"))
1271 action->action = ACTION_CP;
1272 else if (! strcmp(action_string, "mkdir"))
1273 action->action = ACTION_MKDIR;
1274 else if (! strcmp(action_string, "rm"))
1275 action->action = ACTION_RM;
1276 else if (! strcmp(action_string, "put"))
1277 action->action = ACTION_PUT;
1278 else if (! strcmp(action_string, "propset"))
1279 action->action = ACTION_PROPSET;
1280 else if (! strcmp(action_string, "propsetf"))
1281 action->action = ACTION_PROPSETF;
1282 else if (! strcmp(action_string, "propdel"))
1283 action->action = ACTION_PROPDEL;
1284 else if (! strcmp(action_string, "?") || ! strcmp(action_string, "h")
1285 || ! strcmp(action_string, "help"))
1286 usage(pool, EXIT_SUCCESS);
1288 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1289 "'%s' is not an action\n",
1290 action_string), pool);
1291 if (++i == action_args->nelts)
1294 /* For copies, there should be a revision number next. */
1295 if (action->action == ACTION_CP)
1297 const char *rev_str = APR_ARRAY_IDX(action_args, i, const char *);
1298 if (strcmp(rev_str, "head") == 0)
1299 action->rev = SVN_INVALID_REVNUM;
1300 else if (strcmp(rev_str, "HEAD") == 0)
1301 action->rev = SVN_INVALID_REVNUM;
1306 while (*rev_str == 'r')
1309 action->rev = strtol(rev_str, &end, 0);
1311 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1312 "'%s' is not a revision\n",
1315 if (++i == action_args->nelts)
1320 action->rev = SVN_INVALID_REVNUM;
1323 /* For puts, there should be a local file next. */
1324 if (action->action == ACTION_PUT)
1327 svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
1328 const char *), pool);
1329 if (++i == action_args->nelts)
1333 /* For propset, propsetf, and propdel, a property name (and
1334 maybe a property value or file which contains one) comes next. */
1335 if ((action->action == ACTION_PROPSET)
1336 || (action->action == ACTION_PROPSETF)
1337 || (action->action == ACTION_PROPDEL))
1339 action->prop_name = APR_ARRAY_IDX(action_args, i, const char *);
1340 if (++i == action_args->nelts)
1343 if (action->action == ACTION_PROPDEL)
1345 action->prop_value = NULL;
1347 else if (action->action == ACTION_PROPSET)
1349 action->prop_value =
1350 svn_string_create(APR_ARRAY_IDX(action_args, i,
1351 const char *), pool);
1352 if (++i == action_args->nelts)
1357 const char *propval_file =
1358 svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
1359 const char *), pool);
1361 if (++i == action_args->nelts)
1364 err = read_propvalue_file(&(action->prop_value),
1365 propval_file, pool);
1367 handle_error(err, pool);
1369 action->action = ACTION_PROPSET;
1372 if (action->prop_value
1373 && svn_prop_needs_translation(action->prop_name))
1375 svn_string_t *translated_value;
1376 err = svn_subst_translate_string2(&translated_value, NULL,
1377 NULL, action->prop_value, NULL,
1381 svn_error_quick_wrap(err,
1382 "Error normalizing property value"),
1384 action->prop_value = translated_value;
1388 /* How many URLs does this action expect? */
1389 if (action->action == ACTION_RM
1390 || action->action == ACTION_MKDIR
1391 || action->action == ACTION_PUT
1392 || action->action == ACTION_PROPSET
1393 || action->action == ACTION_PROPSETF /* shouldn't see this one */
1394 || action->action == ACTION_PROPDEL)
1399 /* Parse the required number of URLs. */
1400 for (j = 0; j < num_url_args; ++j)
1402 const char *url = APR_ARRAY_IDX(action_args, i, const char *);
1404 /* If there's a ROOT_URL, we expect URL to be a path
1405 relative to ROOT_URL (and we build a full url from the
1406 combination of the two). Otherwise, it should be a full
1408 if (! svn_path_is_url(url))
1411 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1412 "'%s' is not a URL, and "
1413 "--root-url (-U) not provided\n",
1415 /* ### These relpaths are already URI-encoded. */
1416 url = apr_pstrcat(pool, root_url, "/",
1417 svn_relpath_canonicalize(url, pool),
1420 url = sanitize_url(url, pool);
1421 action->path[j] = url;
1423 /* The first URL arguments to 'cp', 'pd', 'ps' could be the anchor,
1424 but the other URLs should be children of the anchor. */
1425 if (! (action->action == ACTION_CP && j == 0)
1426 && action->action != ACTION_PROPDEL
1427 && action->action != ACTION_PROPSET
1428 && action->action != ACTION_PROPSETF)
1429 url = svn_uri_dirname(url, pool);
1434 anchor = svn_uri_get_longest_ancestor(anchor, url, pool);
1435 if (!anchor || !anchor[0])
1436 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1437 "URLs in the action list do not "
1438 "share a common ancestor"),
1442 if ((++i == action_args->nelts) && (j + 1 < num_url_args))
1445 APR_ARRAY_PUSH(actions, struct action *) = action;
1448 if (! actions->nelts)
1449 usage(pool, EXIT_FAILURE);
1451 if ((err = execute(actions, anchor, revprops, username, password,
1452 config_dir, config_options, non_interactive,
1453 trust_server_cert, no_auth_cache, base_revision, pool)))
1455 if (err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive)
1456 err = svn_error_quick_wrap(err,
1457 _("Authentication failed and interactive"
1458 " prompting is disabled; see the"
1459 " --force-interactive option"));
1460 handle_error(err, pool);
1463 /* Ensure that stdout is flushed, so the user will see all results. */
1464 svn_error_clear(svn_cmdline_fflush(stdout));
1466 svn_pool_destroy(pool);
1467 return EXIT_SUCCESS;