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"
61 #include "svn_private_config.h"
63 static void handle_error(svn_error_t *err, apr_pool_t *pool)
66 svn_handle_error2(err, stderr, FALSE, "svnmucc: ");
69 svn_pool_destroy(pool);
74 init(const char *application)
77 const svn_version_checklist_t checklist[] = {
78 {"svn_client", svn_client_version},
79 {"svn_subr", svn_subr_version},
80 {"svn_ra", svn_ra_version},
83 SVN_VERSION_DEFINE(my_version);
85 if (svn_cmdline_init(application, stderr))
88 err = svn_ver_check_list(&my_version, checklist);
90 handle_error(err, NULL);
92 return apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
96 open_tmp_file(apr_file_t **fp,
100 /* Open a unique file; use APR_DELONCLOSE. */
101 return svn_io_open_unique_file3(fp, NULL, NULL, svn_io_file_del_on_close,
106 create_ra_callbacks(svn_ra_callbacks2_t **callbacks,
107 const char *username,
108 const char *password,
109 const char *config_dir,
110 svn_config_t *cfg_config,
111 svn_boolean_t non_interactive,
112 svn_boolean_t trust_server_cert,
113 svn_boolean_t no_auth_cache,
116 SVN_ERR(svn_ra_create_callbacks(callbacks, pool));
118 SVN_ERR(svn_cmdline_create_auth_baton(&(*callbacks)->auth_baton,
120 username, password, config_dir,
123 cfg_config, NULL, NULL, pool));
125 (*callbacks)->open_tmp_file = open_tmp_file;
133 commit_callback(const svn_commit_info_t *commit_info,
137 SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n",
138 commit_info->revision,
140 ? commit_info->author : "(no author)"),
145 typedef enum action_code_t {
162 OP_PROPSET /* only for files for which no other operation is
163 occuring; directories are OP_OPEN with non-empty
166 svn_node_kind_t kind; /* to copy, mkdir, put or set revprops */
167 svn_revnum_t rev; /* to copy, valid for add and replace */
168 const char *url; /* to copy, valid for add and replace */
169 const char *src_file; /* for put, the source file for contents */
170 apr_hash_t *children; /* const char *path -> struct operation * */
171 apr_hash_t *prop_mods; /* const char *prop_name ->
172 const svn_string_t *prop_value */
173 apr_array_header_t *prop_dels; /* const char *prop_name deletions */
174 void *baton; /* as returned by the commit editor */
178 /* An iterator (for use via apr_table_do) which sets node properties.
179 REC is a pointer to a struct driver_state. */
181 change_props(const svn_delta_editor_t *editor,
183 struct operation *child,
186 apr_pool_t *iterpool = svn_pool_create(pool);
188 if (child->prop_dels)
191 for (i = 0; i < child->prop_dels->nelts; i++)
193 const char *prop_name;
195 svn_pool_clear(iterpool);
196 prop_name = APR_ARRAY_IDX(child->prop_dels, i, const char *);
197 if (child->kind == svn_node_dir)
198 SVN_ERR(editor->change_dir_prop(baton, prop_name,
201 SVN_ERR(editor->change_file_prop(baton, prop_name,
205 if (apr_hash_count(child->prop_mods))
207 apr_hash_index_t *hi;
208 for (hi = apr_hash_first(pool, child->prop_mods);
209 hi; hi = apr_hash_next(hi))
211 const char *propname = svn__apr_hash_index_key(hi);
212 const svn_string_t *val = svn__apr_hash_index_val(hi);
214 svn_pool_clear(iterpool);
215 if (child->kind == svn_node_dir)
216 SVN_ERR(editor->change_dir_prop(baton, propname, val, iterpool));
218 SVN_ERR(editor->change_file_prop(baton, propname, val, iterpool));
222 svn_pool_destroy(iterpool);
227 /* Drive EDITOR to affect the change represented by OPERATION. HEAD
228 is the last-known youngest revision in the repository. */
230 drive(struct operation *operation,
232 const svn_delta_editor_t *editor,
235 apr_pool_t *subpool = svn_pool_create(pool);
236 apr_hash_index_t *hi;
238 for (hi = apr_hash_first(pool, operation->children);
239 hi; hi = apr_hash_next(hi))
241 const char *key = svn__apr_hash_index_key(hi);
242 struct operation *child = svn__apr_hash_index_val(hi);
243 void *file_baton = NULL;
245 svn_pool_clear(subpool);
247 /* Deletes and replacements are simple -- delete something. */
248 if (child->operation == OP_DELETE || child->operation == OP_REPLACE)
250 SVN_ERR(editor->delete_entry(key, head, operation->baton, subpool));
252 /* Opens could be for directories or files. */
253 if (child->operation == OP_OPEN || child->operation == OP_PROPSET)
255 if (child->kind == svn_node_dir)
257 SVN_ERR(editor->open_directory(key, operation->baton, head,
258 subpool, &child->baton));
262 SVN_ERR(editor->open_file(key, operation->baton, head,
263 subpool, &file_baton));
266 /* Adds and replacements could also be for directories or files. */
267 if (child->operation == OP_ADD || child->operation == OP_REPLACE)
269 if (child->kind == svn_node_dir)
271 SVN_ERR(editor->add_directory(key, operation->baton,
272 child->url, child->rev,
273 subpool, &child->baton));
277 SVN_ERR(editor->add_file(key, operation->baton, child->url,
278 child->rev, subpool, &file_baton));
281 /* If there's a source file and an open file baton, we get to
282 change textual contents. */
283 if ((child->src_file) && (file_baton))
285 svn_txdelta_window_handler_t handler;
287 svn_stream_t *contents;
289 SVN_ERR(editor->apply_textdelta(file_baton, NULL, subpool,
290 &handler, &handler_baton));
291 if (strcmp(child->src_file, "-") != 0)
293 SVN_ERR(svn_stream_open_readonly(&contents, child->src_file,
298 SVN_ERR(svn_stream_for_stdin(&contents, pool));
300 SVN_ERR(svn_txdelta_send_stream(contents, handler,
301 handler_baton, NULL, pool));
303 /* If we opened a file, we need to apply outstanding propmods,
307 if (child->kind == svn_node_file)
309 SVN_ERR(change_props(editor, file_baton, child, subpool));
311 SVN_ERR(editor->close_file(file_baton, NULL, subpool));
313 /* If we opened, added, or replaced a directory, we need to
314 recurse, apply outstanding propmods, and then close it. */
315 if ((child->kind == svn_node_dir)
316 && child->operation != OP_DELETE)
318 SVN_ERR(change_props(editor, child->baton, child, subpool));
320 SVN_ERR(drive(child, head, editor, subpool));
322 SVN_ERR(editor->close_directory(child->baton, subpool));
325 svn_pool_destroy(subpool);
330 /* Find the operation associated with PATH, which is a single-path
331 component representing a child of the path represented by
332 OPERATION. If no such child operation exists, create a new one of
334 static struct operation *
335 get_operation(const char *path,
336 struct operation *operation,
339 struct operation *child = svn_hash_gets(operation->children, path);
342 child = apr_pcalloc(pool, sizeof(*child));
343 child->children = apr_hash_make(pool);
344 child->operation = OP_OPEN;
345 child->rev = SVN_INVALID_REVNUM;
346 child->kind = svn_node_dir;
347 child->prop_mods = apr_hash_make(pool);
348 child->prop_dels = apr_array_make(pool, 1, sizeof(const char *));
349 svn_hash_sets(operation->children, path, child);
354 /* Return the portion of URL that is relative to ANCHOR (URI-decoded). */
356 subtract_anchor(const char *anchor, const char *url, apr_pool_t *pool)
358 return svn_uri_skip_ancestor(anchor, url, pool);
361 /* Add PATH to the operations tree rooted at OPERATION, creating any
362 intermediate nodes that are required. Here's what's expected for
365 ACTION URL REV SRC-FILE PROPNAME
366 ------------ ----- ------- -------- --------
367 ACTION_MKDIR NULL invalid NULL NULL
368 ACTION_CP valid valid NULL NULL
369 ACTION_PUT NULL invalid valid NULL
370 ACTION_RM NULL invalid NULL NULL
371 ACTION_PROPSET valid invalid NULL valid
372 ACTION_PROPDEL valid invalid NULL valid
374 Node type information is obtained for any copy source (to determine
375 whether to create a file or directory) and for any deleted path (to
376 ensure it exists since svn_delta_editor_t->delete_entry doesn't
377 return an error on non-existent nodes). */
379 build(action_code_t action,
383 const char *prop_name,
384 const svn_string_t *prop_value,
385 const char *src_file,
388 svn_ra_session_t *session,
389 struct operation *operation,
392 apr_array_header_t *path_bits = svn_path_decompose(path, pool);
393 const char *path_so_far = "";
394 const char *copy_src = NULL;
395 svn_revnum_t copy_rev = SVN_INVALID_REVNUM;
398 /* Look for any previous operations we've recognized for PATH. If
399 any of PATH's ancestors have not yet been traversed, we'll be
400 creating OP_OPEN operations for them as we walk down PATH's path
402 for (i = 0; i < path_bits->nelts; ++i)
404 const char *path_bit = APR_ARRAY_IDX(path_bits, i, const char *);
405 path_so_far = svn_relpath_join(path_so_far, path_bit, pool);
406 operation = get_operation(path_so_far, operation, pool);
408 /* If we cross a replace- or add-with-history, remember the
409 source of those things in case we need to lookup the node kind
410 of one of their children. And if this isn't such a copy,
411 but we've already seen one in of our parent paths, we just need
412 to extend that copy source path by our current path
415 && SVN_IS_VALID_REVNUM(operation->rev)
416 && (operation->operation == OP_REPLACE
417 || operation->operation == OP_ADD))
419 copy_src = subtract_anchor(anchor, operation->url, pool);
420 copy_rev = operation->rev;
424 copy_src = svn_relpath_join(copy_src, path_bit, pool);
428 /* Handle property changes. */
431 if (operation->operation == OP_DELETE)
432 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
433 "cannot set properties on a location being"
434 " deleted ('%s')", path);
435 /* If we're not adding this thing ourselves, check for existence. */
436 if (! ((operation->operation == OP_ADD) ||
437 (operation->operation == OP_REPLACE)))
439 SVN_ERR(svn_ra_check_path(session,
440 copy_src ? copy_src : path,
441 copy_src ? copy_rev : head,
442 &operation->kind, pool));
443 if (operation->kind == svn_node_none)
444 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
445 "propset: '%s' not found", path);
446 else if ((operation->kind == svn_node_file)
447 && (operation->operation == OP_OPEN))
448 operation->operation = OP_PROPSET;
451 APR_ARRAY_PUSH(operation->prop_dels, const char *) = prop_name;
453 svn_hash_sets(operation->prop_mods, prop_name, prop_value);
455 operation->rev = rev;
459 /* We won't fuss about multiple operations on the same path in the
462 - the prior operation was, in fact, a no-op (open)
463 - the prior operation was a propset placeholder
464 - the prior operation was a deletion
466 Note: while the operation structure certainly supports the
467 ability to do a copy of a file followed by a put of new contents
468 for the file, we don't let that happen (yet).
470 if (operation->operation != OP_OPEN
471 && operation->operation != OP_PROPSET
472 && operation->operation != OP_DELETE)
473 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
474 "unsupported multiple operations on '%s'", path);
476 /* For deletions, we validate that there's actually something to
477 delete. If this is a deletion of the child of a copied
478 directory, we need to remember to look in the copy source tree to
479 verify that this thing actually exists. */
480 if (action == ACTION_RM)
482 operation->operation = OP_DELETE;
483 SVN_ERR(svn_ra_check_path(session,
484 copy_src ? copy_src : path,
485 copy_src ? copy_rev : head,
486 &operation->kind, pool));
487 if (operation->kind == svn_node_none)
489 if (copy_src && strcmp(path, copy_src))
490 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
491 "'%s' (from '%s:%ld') not found",
492 path, copy_src, copy_rev);
494 return svn_error_createf(SVN_ERR_BAD_URL, NULL, "'%s' not found",
498 /* Handle copy operations (which can be adds or replacements). */
499 else if (action == ACTION_CP)
502 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
503 "Copy source revision cannot be younger "
504 "than base revision");
505 operation->operation =
506 operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
507 if (operation->operation == OP_ADD)
509 /* There is a bug in the current version of mod_dav_svn
510 which incorrectly replaces existing directories.
511 Therefore we need to check if the target exists
512 and raise an error here. */
513 SVN_ERR(svn_ra_check_path(session,
514 copy_src ? copy_src : path,
515 copy_src ? copy_rev : head,
516 &operation->kind, pool));
517 if (operation->kind != svn_node_none)
519 if (copy_src && strcmp(path, copy_src))
520 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
521 "'%s' (from '%s:%ld') already exists",
522 path, copy_src, copy_rev);
524 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
525 "'%s' already exists", path);
528 SVN_ERR(svn_ra_check_path(session, subtract_anchor(anchor, url, pool),
529 rev, &operation->kind, pool));
530 if (operation->kind == svn_node_none)
531 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
533 subtract_anchor(anchor, url, pool));
534 operation->url = url;
535 operation->rev = rev;
537 /* Handle mkdir operations (which can be adds or replacements). */
538 else if (action == ACTION_MKDIR)
540 operation->operation =
541 operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
542 operation->kind = svn_node_dir;
544 /* Handle put operations (which can be adds, replacements, or opens). */
545 else if (action == ACTION_PUT)
547 if (operation->operation == OP_DELETE)
549 operation->operation = OP_REPLACE;
553 SVN_ERR(svn_ra_check_path(session,
554 copy_src ? copy_src : path,
555 copy_src ? copy_rev : head,
556 &operation->kind, pool));
557 if (operation->kind == svn_node_file)
558 operation->operation = OP_OPEN;
559 else if (operation->kind == svn_node_none)
560 operation->operation = OP_ADD;
562 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
563 "'%s' is not a file", path);
565 operation->kind = svn_node_file;
566 operation->src_file = src_file;
570 /* We shouldn't get here. */
571 SVN_ERR_MALFUNCTION();
578 action_code_t action;
580 /* revision (copy-from-rev of path[0] for cp; base-rev for put) */
583 /* action path[0] path[1]
584 * ------ ------- -------
586 * mkdir target (null)
590 * propset target (null)
594 /* property name/value */
595 const char *prop_name;
596 const svn_string_t *prop_value;
601 svn_ra_session_t *session;
606 fetch_base_func(const char **filename,
609 svn_revnum_t base_revision,
610 apr_pool_t *result_pool,
611 apr_pool_t *scratch_pool)
613 struct fetch_baton *fb = baton;
614 svn_stream_t *fstream;
617 if (! SVN_IS_VALID_REVNUM(base_revision))
618 base_revision = fb->head;
620 SVN_ERR(svn_stream_open_unique(&fstream, filename, NULL,
621 svn_io_file_del_on_pool_cleanup,
622 result_pool, scratch_pool));
624 err = svn_ra_get_file(fb->session, path, base_revision, fstream, NULL, NULL,
626 if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
628 svn_error_clear(err);
629 SVN_ERR(svn_stream_close(fstream));
635 return svn_error_trace(err);
637 SVN_ERR(svn_stream_close(fstream));
643 fetch_props_func(apr_hash_t **props,
646 svn_revnum_t base_revision,
647 apr_pool_t *result_pool,
648 apr_pool_t *scratch_pool)
650 struct fetch_baton *fb = baton;
651 svn_node_kind_t node_kind;
653 if (! SVN_IS_VALID_REVNUM(base_revision))
654 base_revision = fb->head;
656 SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, &node_kind,
659 if (node_kind == svn_node_file)
661 SVN_ERR(svn_ra_get_file(fb->session, path, base_revision, NULL, NULL,
662 props, result_pool));
664 else if (node_kind == svn_node_dir)
666 apr_array_header_t *tmp_props;
668 SVN_ERR(svn_ra_get_dir2(fb->session, NULL, NULL, props, path,
669 base_revision, 0 /* Dirent fields */,
671 tmp_props = svn_prop_hash_to_array(*props, result_pool);
672 SVN_ERR(svn_categorize_props(tmp_props, NULL, NULL, &tmp_props,
674 *props = svn_prop_array_to_hash(tmp_props, result_pool);
678 *props = apr_hash_make(result_pool);
685 fetch_kind_func(svn_node_kind_t *kind,
688 svn_revnum_t base_revision,
689 apr_pool_t *scratch_pool)
691 struct fetch_baton *fb = baton;
693 if (! SVN_IS_VALID_REVNUM(base_revision))
694 base_revision = fb->head;
696 SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, kind,
702 static svn_delta_shim_callbacks_t *
703 get_shim_callbacks(svn_ra_session_t *session,
705 apr_pool_t *result_pool)
707 svn_delta_shim_callbacks_t *callbacks =
708 svn_delta_shim_callbacks_default(result_pool);
709 struct fetch_baton *fb = apr_pcalloc(result_pool, sizeof(*fb));
711 fb->session = session;
714 callbacks->fetch_props_func = fetch_props_func;
715 callbacks->fetch_kind_func = fetch_kind_func;
716 callbacks->fetch_base_func = fetch_base_func;
717 callbacks->fetch_baton = fb;
723 execute(const apr_array_header_t *actions,
725 apr_hash_t *revprops,
726 const char *username,
727 const char *password,
728 const char *config_dir,
729 const apr_array_header_t *config_options,
730 svn_boolean_t non_interactive,
731 svn_boolean_t trust_server_cert,
732 svn_boolean_t no_auth_cache,
733 svn_revnum_t base_revision,
736 svn_ra_session_t *session;
737 svn_ra_session_t *aux_session;
738 const char *repos_root;
740 const svn_delta_editor_t *editor;
741 svn_ra_callbacks2_t *ra_callbacks;
743 struct operation root;
746 svn_config_t *cfg_config;
749 SVN_ERR(svn_config_get_config(&config, config_dir, pool));
750 SVN_ERR(svn_cmdline__apply_config_options(config, config_options,
751 "svnmucc: ", "--config-option"));
752 cfg_config = svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG);
754 if (! svn_hash_gets(revprops, SVN_PROP_REVISION_LOG))
756 svn_string_t *msg = svn_string_create("", pool);
758 /* If we can do so, try to pop up $EDITOR to fetch a log message. */
761 return svn_error_create
762 (SVN_ERR_CL_INSUFFICIENT_ARGS, NULL,
763 _("Cannot invoke editor to get log message "
764 "when non-interactive"));
768 SVN_ERR(svn_cmdline__edit_string_externally(
769 &msg, NULL, NULL, "", msg, "svnmucc-commit", config,
770 TRUE, NULL, apr_hash_pool_get(revprops)));
773 svn_hash_sets(revprops, SVN_PROP_REVISION_LOG, msg);
776 SVN_ERR(create_ra_callbacks(&ra_callbacks, username, password, config_dir,
777 cfg_config, non_interactive, trust_server_cert,
778 no_auth_cache, pool));
779 SVN_ERR(svn_ra_open4(&session, NULL, anchor, NULL, ra_callbacks,
780 NULL, config, pool));
781 /* Open, then reparent to avoid AUTHZ errors when opening the reposroot */
782 SVN_ERR(svn_ra_open4(&aux_session, NULL, anchor, NULL, ra_callbacks,
783 NULL, config, pool));
784 SVN_ERR(svn_ra_get_repos_root2(aux_session, &repos_root, pool));
785 SVN_ERR(svn_ra_reparent(aux_session, repos_root, pool));
786 SVN_ERR(svn_ra_get_latest_revnum(session, &head, pool));
788 /* Reparent to ANCHOR's dir, if ANCHOR is not a directory. */
790 svn_node_kind_t kind;
792 SVN_ERR(svn_ra_check_path(aux_session,
793 svn_uri_skip_ancestor(repos_root, anchor, pool),
795 if (kind != svn_node_dir)
797 anchor = svn_uri_dirname(anchor, pool);
798 SVN_ERR(svn_ra_reparent(session, anchor, pool));
802 if (SVN_IS_VALID_REVNUM(base_revision))
804 if (base_revision > head)
805 return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL,
806 "No such revision %ld (youngest is %ld)",
807 base_revision, head);
808 head = base_revision;
811 memset(&root, 0, sizeof(root));
812 root.children = apr_hash_make(pool);
813 root.operation = OP_OPEN;
814 root.kind = svn_node_dir; /* For setting properties */
815 root.prop_mods = apr_hash_make(pool);
816 root.prop_dels = apr_array_make(pool, 1, sizeof(const char *));
818 for (i = 0; i < actions->nelts; ++i)
820 struct action *action = APR_ARRAY_IDX(actions, i, struct action *);
821 const char *path1, *path2;
822 switch (action->action)
825 path1 = subtract_anchor(anchor, action->path[0], pool);
826 path2 = subtract_anchor(anchor, action->path[1], pool);
827 SVN_ERR(build(ACTION_RM, path1, NULL,
828 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
829 session, &root, pool));
830 SVN_ERR(build(ACTION_CP, path2, action->path[0],
831 head, NULL, NULL, NULL, head, anchor,
832 session, &root, pool));
835 path2 = subtract_anchor(anchor, action->path[1], pool);
836 if (action->rev == SVN_INVALID_REVNUM)
838 SVN_ERR(build(ACTION_CP, path2, action->path[0],
839 action->rev, NULL, NULL, NULL, head, anchor,
840 session, &root, pool));
843 path1 = subtract_anchor(anchor, action->path[0], pool);
844 SVN_ERR(build(ACTION_RM, path1, NULL,
845 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
846 session, &root, pool));
849 path1 = subtract_anchor(anchor, action->path[0], pool);
850 SVN_ERR(build(ACTION_MKDIR, path1, action->path[0],
851 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
852 session, &root, pool));
855 path1 = subtract_anchor(anchor, action->path[0], pool);
856 SVN_ERR(build(ACTION_PUT, path1, action->path[0],
857 SVN_INVALID_REVNUM, NULL, NULL, action->path[1],
858 head, anchor, session, &root, pool));
862 path1 = subtract_anchor(anchor, action->path[0], pool);
863 SVN_ERR(build(action->action, path1, action->path[0],
865 action->prop_name, action->prop_value,
866 NULL, head, anchor, session, &root, pool));
868 case ACTION_PROPSETF:
870 SVN_ERR_MALFUNCTION_NO_RETURN();
874 SVN_ERR(svn_ra__register_editor_shim_callbacks(session,
875 get_shim_callbacks(aux_session, head, pool)));
876 SVN_ERR(svn_ra_get_commit_editor3(session, &editor, &editor_baton, revprops,
877 commit_callback, NULL, NULL, FALSE, pool));
879 SVN_ERR(editor->open_root(editor_baton, head, pool, &root.baton));
880 err = change_props(editor, root.baton, &root, pool);
882 err = drive(&root, head, editor, pool);
884 err = editor->close_directory(root.baton, pool);
886 err = editor->close_edit(editor_baton, pool);
889 err = svn_error_compose_create(err,
890 editor->abort_edit(editor_baton, pool));
896 read_propvalue_file(const svn_string_t **value_p,
897 const char *filename,
900 svn_stringbuf_t *value;
901 apr_pool_t *scratch_pool = svn_pool_create(pool);
903 SVN_ERR(svn_stringbuf_from_file2(&value, filename, scratch_pool));
904 *value_p = svn_string_create_from_buf(value, pool);
905 svn_pool_destroy(scratch_pool);
909 /* Perform the typical suite of manipulations for user-provided URLs
910 on URL, returning the result (allocated from POOL): IRI-to-URI
911 conversion, auto-escaping, and canonicalization. */
913 sanitize_url(const char *url,
916 url = svn_path_uri_from_iri(url, pool);
917 url = svn_path_uri_autoescape(url, pool);
918 return svn_uri_canonicalize(url, pool);
922 usage(apr_pool_t *pool, int exit_val)
924 FILE *stream = exit_val == EXIT_SUCCESS ? stdout : stderr;
925 svn_error_clear(svn_cmdline_fputs(
926 _("Subversion multiple URL command client\n"
927 "usage: svnmucc ACTION...\n"
929 " Perform one or more Subversion repository URL-based ACTIONs, committing\n"
930 " the result as a (single) new revision.\n"
933 " cp REV SRC-URL DST-URL : copy SRC-URL@REV to DST-URL\n"
934 " mkdir URL : create new directory URL\n"
935 " mv SRC-URL DST-URL : move SRC-URL to DST-URL\n"
936 " rm URL : delete URL\n"
937 " put SRC-FILE URL : add or modify file URL with contents copied from\n"
938 " SRC-FILE (use \"-\" to read from standard input)\n"
939 " propset NAME VALUE URL : set property NAME on URL to VALUE\n"
940 " propsetf NAME FILE URL : set property NAME on URL to value read from FILE\n"
941 " propdel NAME URL : delete property NAME from URL\n"
944 " -h, -? [--help] : display this text\n"
945 " -m [--message] ARG : use ARG as a log message\n"
946 " -F [--file] ARG : read log message from file ARG\n"
947 " -u [--username] ARG : commit the changes as username ARG\n"
948 " -p [--password] ARG : use ARG as the password\n"
949 " -U [--root-url] ARG : interpret all action URLs relative to ARG\n"
950 " -r [--revision] ARG : use revision ARG as baseline for changes\n"
951 " --with-revprop ARG : set revision property in the following format:\n"
953 " --non-interactive : do no interactive prompting (default is to\n"
954 " prompt only if standard input is a terminal)\n"
955 " --force-interactive : do interactive prompting even if standard\n"
956 " input is not a terminal\n"
957 " --trust-server-cert : accept SSL server certificates from unknown\n"
958 " certificate authorities without prompting (but\n"
959 " only with '--non-interactive')\n"
960 " -X [--extra-args] ARG : append arguments from file ARG (one per line;\n"
961 " use \"-\" to read from standard input)\n"
962 " --config-dir ARG : use ARG to override the config directory\n"
963 " --config-option ARG : use ARG to override a configuration option\n"
964 " --no-auth-cache : do not cache authentication tokens\n"
965 " --version : print version information\n"),
967 svn_pool_destroy(pool);
972 insufficient(apr_pool_t *pool)
974 handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
975 "insufficient arguments"),
980 display_version(apr_getopt_t *os, apr_pool_t *pool)
982 const char *ra_desc_start
983 = "The following repository access (RA) modules are available:\n\n";
984 svn_stringbuf_t *version_footer;
986 version_footer = svn_stringbuf_create(ra_desc_start, pool);
987 SVN_ERR(svn_ra_print_modules(version_footer, pool));
989 SVN_ERR(svn_opt_print_help4(os, "svnmucc", TRUE, FALSE, FALSE,
990 version_footer->data,
991 NULL, NULL, NULL, NULL, NULL, pool));
996 /* Return an error about the mutual exclusivity of the -m, -F, and
997 --with-revprop=svn:log command-line options. */
999 mutually_exclusive_logs_error(void)
1001 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1002 _("--message (-m), --file (-F), and "
1003 "--with-revprop=svn:log are mutually "
1007 /* Ensure that the REVPROPS hash contains a command-line-provided log
1008 message, if any, and that there was but one source of such a thing
1009 provided on that command-line. */
1010 static svn_error_t *
1011 sanitize_log_sources(apr_hash_t *revprops,
1012 const char *message,
1013 svn_stringbuf_t *filedata)
1015 apr_pool_t *hash_pool = apr_hash_pool_get(revprops);
1017 /* If we already have a log message in the revprop hash, then just
1018 make sure the user didn't try to also use -m or -F. Otherwise,
1019 we need to consult -m or -F to find a log message, if any. */
1020 if (svn_hash_gets(revprops, SVN_PROP_REVISION_LOG))
1022 if (filedata || message)
1023 return mutually_exclusive_logs_error();
1028 return mutually_exclusive_logs_error();
1030 SVN_ERR(svn_utf_cstring_to_utf8(&message, filedata->data, hash_pool));
1031 svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
1032 svn_stringbuf__morph_into_string(filedata));
1036 svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
1037 svn_string_create(message, hash_pool));
1040 return SVN_NO_ERROR;
1044 main(int argc, const char **argv)
1046 apr_pool_t *pool = init("svnmucc");
1047 apr_array_header_t *actions = apr_array_make(pool, 1,
1048 sizeof(struct action *));
1049 const char *anchor = NULL;
1050 svn_error_t *err = SVN_NO_ERROR;
1053 config_dir_opt = SVN_OPT_FIRST_LONGOPT_ID,
1058 non_interactive_opt,
1059 force_interactive_opt,
1060 trust_server_cert_opt
1062 static const apr_getopt_option_t options[] = {
1063 {"message", 'm', 1, ""},
1064 {"file", 'F', 1, ""},
1065 {"username", 'u', 1, ""},
1066 {"password", 'p', 1, ""},
1067 {"root-url", 'U', 1, ""},
1068 {"revision", 'r', 1, ""},
1069 {"with-revprop", with_revprop_opt, 1, ""},
1070 {"extra-args", 'X', 1, ""},
1071 {"help", 'h', 0, ""},
1073 {"non-interactive", non_interactive_opt, 0, ""},
1074 {"force-interactive", force_interactive_opt, 0, ""},
1075 {"trust-server-cert", trust_server_cert_opt, 0, ""},
1076 {"config-dir", config_dir_opt, 1, ""},
1077 {"config-option", config_inline_opt, 1, ""},
1078 {"no-auth-cache", no_auth_cache_opt, 0, ""},
1079 {"version", version_opt, 0, ""},
1082 const char *message = NULL;
1083 svn_stringbuf_t *filedata = NULL;
1084 const char *username = NULL, *password = NULL;
1085 const char *root_url = NULL, *extra_args_file = NULL;
1086 const char *config_dir = NULL;
1087 apr_array_header_t *config_options;
1088 svn_boolean_t non_interactive = FALSE;
1089 svn_boolean_t force_interactive = FALSE;
1090 svn_boolean_t trust_server_cert = FALSE;
1091 svn_boolean_t no_auth_cache = FALSE;
1092 svn_revnum_t base_revision = SVN_INVALID_REVNUM;
1093 apr_array_header_t *action_args;
1094 apr_hash_t *revprops = apr_hash_make(pool);
1097 config_options = apr_array_make(pool, 0,
1098 sizeof(svn_cmdline__config_argument_t*));
1100 apr_getopt_init(&opts, pool, argc, argv);
1101 opts->interleave = 1;
1106 const char *opt_arg;
1108 apr_status_t status = apr_getopt_long(opts, options, &opt, &arg);
1109 if (APR_STATUS_IS_EOF(status))
1111 if (status != APR_SUCCESS)
1112 handle_error(svn_error_wrap_apr(status, "getopt failure"), pool);
1116 err = svn_utf_cstring_to_utf8(&message, arg, pool);
1118 handle_error(err, pool);
1122 const char *arg_utf8;
1123 err = svn_utf_cstring_to_utf8(&arg_utf8, arg, pool);
1125 err = svn_stringbuf_from_file2(&filedata, arg, pool);
1127 handle_error(err, pool);
1131 username = apr_pstrdup(pool, arg);
1134 password = apr_pstrdup(pool, arg);
1137 err = svn_utf_cstring_to_utf8(&root_url, arg, pool);
1139 handle_error(err, pool);
1140 if (! svn_path_is_url(root_url))
1141 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1142 "'%s' is not a URL\n", root_url),
1144 root_url = sanitize_url(root_url, pool);
1148 char *digits_end = NULL;
1149 base_revision = strtol(arg, &digits_end, 10);
1150 if ((! SVN_IS_VALID_REVNUM(base_revision))
1153 handle_error(svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR,
1154 NULL, "Invalid revision number"),
1158 case with_revprop_opt:
1159 err = svn_opt_parse_revprop(&revprops, arg, pool);
1160 if (err != SVN_NO_ERROR)
1161 handle_error(err, pool);
1164 extra_args_file = apr_pstrdup(pool, arg);
1166 case non_interactive_opt:
1167 non_interactive = TRUE;
1169 case force_interactive_opt:
1170 force_interactive = TRUE;
1172 case trust_server_cert_opt:
1173 trust_server_cert = TRUE;
1175 case config_dir_opt:
1176 err = svn_utf_cstring_to_utf8(&config_dir, arg, pool);
1178 handle_error(err, pool);
1180 case config_inline_opt:
1181 err = svn_utf_cstring_to_utf8(&opt_arg, arg, pool);
1183 handle_error(err, pool);
1185 err = svn_cmdline__parse_config_option(config_options, opt_arg,
1188 handle_error(err, pool);
1190 case no_auth_cache_opt:
1191 no_auth_cache = TRUE;
1194 SVN_INT_ERR(display_version(opts, pool));
1199 usage(pool, EXIT_SUCCESS);
1204 if (non_interactive && force_interactive)
1206 err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1207 _("--non-interactive and --force-interactive "
1208 "are mutually exclusive"));
1209 return svn_cmdline_handle_exit_error(err, pool, "svnmucc: ");
1212 non_interactive = !svn_cmdline__be_interactive(non_interactive,
1215 if (trust_server_cert && !non_interactive)
1217 err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
1218 _("--trust-server-cert requires "
1219 "--non-interactive"));
1220 return svn_cmdline_handle_exit_error(err, pool, "svnmucc: ");
1223 /* Make sure we have a log message to use. */
1224 err = sanitize_log_sources(revprops, message, filedata);
1226 handle_error(err, pool);
1228 /* Copy the rest of our command-line arguments to an array,
1229 UTF-8-ing them along the way. */
1230 action_args = apr_array_make(pool, opts->argc, sizeof(const char *));
1231 while (opts->ind < opts->argc)
1233 const char *arg = opts->argv[opts->ind++];
1234 if ((err = svn_utf_cstring_to_utf8(&(APR_ARRAY_PUSH(action_args,
1237 handle_error(err, pool);
1240 /* If there are extra arguments in a supplementary file, tack those
1241 on, too (again, in UTF8 form). */
1242 if (extra_args_file)
1244 const char *extra_args_file_utf8;
1245 svn_stringbuf_t *contents, *contents_utf8;
1247 err = svn_utf_cstring_to_utf8(&extra_args_file_utf8,
1248 extra_args_file, pool);
1250 err = svn_stringbuf_from_file2(&contents, extra_args_file_utf8, pool);
1252 err = svn_utf_stringbuf_to_utf8(&contents_utf8, contents, pool);
1254 handle_error(err, pool);
1255 svn_cstring_split_append(action_args, contents_utf8->data, "\n\r",
1259 /* Now, we iterate over the combined set of arguments -- our actions. */
1260 for (i = 0; i < action_args->nelts; )
1262 int j, num_url_args;
1263 const char *action_string = APR_ARRAY_IDX(action_args, i, const char *);
1264 struct action *action = apr_pcalloc(pool, sizeof(*action));
1266 /* First, parse the action. */
1267 if (! strcmp(action_string, "mv"))
1268 action->action = ACTION_MV;
1269 else if (! strcmp(action_string, "cp"))
1270 action->action = ACTION_CP;
1271 else if (! strcmp(action_string, "mkdir"))
1272 action->action = ACTION_MKDIR;
1273 else if (! strcmp(action_string, "rm"))
1274 action->action = ACTION_RM;
1275 else if (! strcmp(action_string, "put"))
1276 action->action = ACTION_PUT;
1277 else if (! strcmp(action_string, "propset"))
1278 action->action = ACTION_PROPSET;
1279 else if (! strcmp(action_string, "propsetf"))
1280 action->action = ACTION_PROPSETF;
1281 else if (! strcmp(action_string, "propdel"))
1282 action->action = ACTION_PROPDEL;
1283 else if (! strcmp(action_string, "?") || ! strcmp(action_string, "h")
1284 || ! strcmp(action_string, "help"))
1285 usage(pool, EXIT_SUCCESS);
1287 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1288 "'%s' is not an action\n",
1289 action_string), pool);
1290 if (++i == action_args->nelts)
1293 /* For copies, there should be a revision number next. */
1294 if (action->action == ACTION_CP)
1296 const char *rev_str = APR_ARRAY_IDX(action_args, i, const char *);
1297 if (strcmp(rev_str, "head") == 0)
1298 action->rev = SVN_INVALID_REVNUM;
1299 else if (strcmp(rev_str, "HEAD") == 0)
1300 action->rev = SVN_INVALID_REVNUM;
1305 while (*rev_str == 'r')
1308 action->rev = strtol(rev_str, &end, 0);
1310 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1311 "'%s' is not a revision\n",
1314 if (++i == action_args->nelts)
1319 action->rev = SVN_INVALID_REVNUM;
1322 /* For puts, there should be a local file next. */
1323 if (action->action == ACTION_PUT)
1326 svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
1327 const char *), pool);
1328 if (++i == action_args->nelts)
1332 /* For propset, propsetf, and propdel, a property name (and
1333 maybe a property value or file which contains one) comes next. */
1334 if ((action->action == ACTION_PROPSET)
1335 || (action->action == ACTION_PROPSETF)
1336 || (action->action == ACTION_PROPDEL))
1338 action->prop_name = APR_ARRAY_IDX(action_args, i, const char *);
1339 if (++i == action_args->nelts)
1342 if (action->action == ACTION_PROPDEL)
1344 action->prop_value = NULL;
1346 else if (action->action == ACTION_PROPSET)
1348 action->prop_value =
1349 svn_string_create(APR_ARRAY_IDX(action_args, i,
1350 const char *), pool);
1351 if (++i == action_args->nelts)
1356 const char *propval_file =
1357 svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
1358 const char *), pool);
1360 if (++i == action_args->nelts)
1363 err = read_propvalue_file(&(action->prop_value),
1364 propval_file, pool);
1366 handle_error(err, pool);
1368 action->action = ACTION_PROPSET;
1371 if (action->prop_value
1372 && svn_prop_needs_translation(action->prop_name))
1374 svn_string_t *translated_value;
1375 err = svn_subst_translate_string2(&translated_value, NULL,
1376 NULL, action->prop_value, NULL,
1380 svn_error_quick_wrap(err,
1381 "Error normalizing property value"),
1383 action->prop_value = translated_value;
1387 /* How many URLs does this action expect? */
1388 if (action->action == ACTION_RM
1389 || action->action == ACTION_MKDIR
1390 || action->action == ACTION_PUT
1391 || action->action == ACTION_PROPSET
1392 || action->action == ACTION_PROPSETF /* shouldn't see this one */
1393 || action->action == ACTION_PROPDEL)
1398 /* Parse the required number of URLs. */
1399 for (j = 0; j < num_url_args; ++j)
1401 const char *url = APR_ARRAY_IDX(action_args, i, const char *);
1403 /* If there's a ROOT_URL, we expect URL to be a path
1404 relative to ROOT_URL (and we build a full url from the
1405 combination of the two). Otherwise, it should be a full
1407 if (! svn_path_is_url(url))
1410 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
1411 "'%s' is not a URL, and "
1412 "--root-url (-U) not provided\n",
1414 /* ### These relpaths are already URI-encoded. */
1415 url = apr_pstrcat(pool, root_url, "/",
1416 svn_relpath_canonicalize(url, pool),
1419 url = sanitize_url(url, pool);
1420 action->path[j] = url;
1422 /* The first URL arguments to 'cp', 'pd', 'ps' could be the anchor,
1423 but the other URLs should be children of the anchor. */
1424 if (! (action->action == ACTION_CP && j == 0)
1425 && action->action != ACTION_PROPDEL
1426 && action->action != ACTION_PROPSET
1427 && action->action != ACTION_PROPSETF)
1428 url = svn_uri_dirname(url, pool);
1432 anchor = svn_uri_get_longest_ancestor(anchor, url, pool);
1434 if ((++i == action_args->nelts) && (j + 1 < num_url_args))
1437 APR_ARRAY_PUSH(actions, struct action *) = action;
1440 if (! actions->nelts)
1441 usage(pool, EXIT_FAILURE);
1443 if ((err = execute(actions, anchor, revprops, username, password,
1444 config_dir, config_options, non_interactive,
1445 trust_server_cert, no_auth_cache, base_revision, pool)))
1447 if (err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive)
1448 err = svn_error_quick_wrap(err,
1449 _("Authentication failed and interactive"
1450 " prompting is disabled; see the"
1451 " --force-interactive option"));
1452 handle_error(err, pool);
1455 /* Ensure that stdout is flushed, so the user will see all results. */
1456 svn_error_clear(svn_cmdline_fflush(stdout));
1458 svn_pool_destroy(pool);
1459 return EXIT_SUCCESS;