1 /* Multiple URL Command Client
3 Combine a list of mv, cp and rm commands on URLs into a single commit.
5 Copyright 2005 Philip Martin <philip@codematters.co.uk>
7 Licenced under the same terms as Subversion.
9 How it works: the command line arguments are parsed into an array of
10 action structures. The action structures are interpreted to build a
11 tree of operation structures. The tree of operation structures is
12 used to drive an RA commit editor to produce a single commit.
14 To build this client, type 'make svnmucc' from the root of your
15 Subversion source directory.
18 #include "svn_cmdline.h"
19 #include "svn_client.h"
20 #include "svn_pools.h"
21 #include "svn_error.h"
24 #include "svn_config.h"
25 #include "svn_props.h"
26 #include "svn_string.h"
31 static void handle_error(svn_error_t
*err
, apr_pool_t
*pool
)
34 svn_handle_error2(err
, stderr
, FALSE
, "svnmucc: ");
37 svn_pool_destroy(pool
);
42 init(const char *application
)
44 apr_allocator_t
*allocator
;
47 const svn_version_checklist_t checklist
[] = {
48 {"svn_client", svn_client_version
},
49 {"svn_subr", svn_subr_version
},
50 {"svn_ra", svn_ra_version
},
54 SVN_VERSION_DEFINE(my_version
);
56 if (svn_cmdline_init(application
, stderr
)
57 || apr_allocator_create(&allocator
))
60 err
= svn_ver_check_list(&my_version
, checklist
);
62 handle_error(err
, NULL
);
64 apr_allocator_max_free_set(allocator
, SVN_ALLOCATOR_RECOMMENDED_MAX_FREE
);
65 pool
= svn_pool_create_ex(NULL
, allocator
);
66 apr_allocator_owner_set(allocator
, pool
);
72 open_tmp_file(apr_file_t
**fp
,
78 /* "Say, Subversion. Seen any good tempdirs lately?" */
79 SVN_ERR(svn_io_temp_dir(&temp_dir
, pool
));
81 /* Open a unique file; use APR_DELONCLOSE. */
82 return svn_io_open_unique_file2(fp
, NULL
,
83 svn_path_join(temp_dir
, "svnmucc", pool
),
84 ".tmp", svn_io_file_del_on_close
, pool
);
87 static svn_ra_callbacks_t
*
88 ra_callbacks(const char *username
,
90 svn_boolean_t non_interactive
,
93 svn_ra_callbacks_t
*callbacks
= apr_palloc(pool
, sizeof(*callbacks
));
94 svn_cmdline_setup_auth_baton(&callbacks
->auth_baton
, non_interactive
,
96 NULL
, FALSE
, NULL
, NULL
, NULL
, pool
);
97 callbacks
->open_tmp_file
= open_tmp_file
;
98 callbacks
->get_wc_prop
= NULL
;
99 callbacks
->set_wc_prop
= NULL
;
100 callbacks
->push_wc_prop
= NULL
;
101 callbacks
->invalidate_wc_props
= NULL
;
109 commit_callback(const svn_commit_info_t
*commit_info
,
113 SVN_ERR(svn_cmdline_printf(pool
, "r%ld committed by %s at %s\n",
114 commit_info
->revision
,
116 ? commit_info
->author
: "(no author)"),
137 OP_PROPSET
/* only for files for which no other operation is
138 occuring; directories are OP_OPEN with non-empty
141 svn_node_kind_t kind
; /* to copy, mkdir, put or set revprops */
142 svn_revnum_t rev
; /* to copy, valid for add and replace */
143 const char *url
; /* to copy, valid for add and replace */
144 const char *src_file
; /* for put, the source file for contents */
145 apr_hash_t
*children
; /* const char *path -> struct operation * */
146 apr_table_t
*props
; /* const char *prop_name -> const char *prop_value */
147 void *baton
; /* as returned by the commit editor */
151 /* State to be passed to set_props iterator */
152 struct driver_state
{
153 const svn_delta_editor_t
*editor
;
154 svn_node_kind_t kind
;
161 /* An iterator (for use via apr_table_do) which sets node properties.
162 REC is a pointer to a struct driver_state. */
164 set_props(void *rec
, const char *key
, const char *value
)
166 struct driver_state
*d_state
= (struct driver_state
*)rec
;
167 svn_string_t
*value_svnstring
168 = value
? svn_string_create(value
, d_state
->pool
) : NULL
;
170 if (d_state
->kind
== svn_node_dir
)
171 d_state
->err
= d_state
->editor
->change_dir_prop(d_state
->baton
, key
,
175 d_state
->err
= d_state
->editor
->change_file_prop(d_state
->baton
, key
,
184 /* Drive EDITOR to affect the change represented by OPERATION. HEAD
185 is the last-known youngest revision in the repository. */
187 drive(struct operation
*operation
,
189 const svn_delta_editor_t
*editor
,
192 apr_pool_t
*subpool
= svn_pool_create(pool
);
193 apr_hash_index_t
*hi
;
194 struct driver_state state
;
195 for (hi
= apr_hash_first(pool
, operation
->children
);
196 hi
; hi
= apr_hash_next(hi
))
200 struct operation
*child
;
201 void *file_baton
= NULL
;
203 svn_pool_clear(subpool
);
204 apr_hash_this(hi
, &key
, NULL
, &val
);
207 /* Deletes and replacements are simple -- delete something. */
208 if (child
->operation
== OP_DELETE
|| child
->operation
== OP_REPLACE
)
210 SVN_ERR(editor
->delete_entry(key
, head
, operation
->baton
, subpool
));
212 /* Opens could be for directories or files. */
213 if (child
->operation
== OP_OPEN
)
215 if (child
->kind
== svn_node_dir
)
217 SVN_ERR(editor
->open_directory(key
, operation
->baton
, head
,
218 subpool
, &child
->baton
));
222 SVN_ERR(editor
->open_file(key
, operation
->baton
, head
,
223 subpool
, &file_baton
));
226 /* Adds and replacements could also be for directories or files. */
227 if (child
->operation
== OP_ADD
|| child
->operation
== OP_REPLACE
228 || child
->operation
== OP_PROPSET
)
230 if (child
->kind
== svn_node_dir
)
232 SVN_ERR(editor
->add_directory(key
, operation
->baton
,
233 child
->url
, child
->rev
,
234 subpool
, &child
->baton
));
238 SVN_ERR(editor
->add_file(key
, operation
->baton
, child
->url
,
239 child
->rev
, subpool
, &file_baton
));
242 /* If there's a source file and an open file baton, we get to
243 change textual contents. */
244 if ((child
->src_file
) && (file_baton
))
246 svn_txdelta_window_handler_t handler
;
248 svn_stream_t
*contents
;
249 apr_file_t
*f
= NULL
;
251 SVN_ERR(editor
->apply_textdelta(file_baton
, NULL
, subpool
,
252 &handler
, &handler_baton
));
253 SVN_ERR(svn_io_file_open(&f
, child
->src_file
, APR_READ
,
254 APR_OS_DEFAULT
, pool
));
255 contents
= svn_stream_from_aprfile(f
, pool
);
256 SVN_ERR(svn_txdelta_send_stream(contents
, handler
,
257 handler_baton
, NULL
, pool
));
258 SVN_ERR(svn_io_file_close(f
, pool
));
260 /* If we opened a file, we need to apply outstanding propmods,
264 if ((child
->kind
== svn_node_file
)
265 && (! apr_is_empty_table(child
->props
)))
267 state
.baton
= file_baton
;
268 state
.pool
= subpool
;
269 state
.editor
= editor
;
270 state
.kind
= child
->kind
;
271 if (! apr_table_do(set_props
, &state
, child
->props
, NULL
))
274 SVN_ERR(editor
->close_file(file_baton
, NULL
, subpool
));
276 /* If we opened, added, or replaced a directory, we need to
277 recurse, apply outstanding propmods, and then close it. */
278 if ((child
->kind
== svn_node_dir
)
279 && (child
->operation
== OP_OPEN
280 || child
->operation
== OP_ADD
281 || child
->operation
== OP_REPLACE
))
283 SVN_ERR(drive(child
, head
, editor
, subpool
));
284 if ((child
->kind
== svn_node_dir
)
285 && (! apr_is_empty_table(child
->props
)))
287 state
.baton
= child
->baton
;
288 state
.pool
= subpool
;
289 state
.editor
= editor
;
290 state
.kind
= child
->kind
;
291 if (! apr_table_do(set_props
, &state
, child
->props
, NULL
))
294 SVN_ERR(editor
->close_directory(child
->baton
, subpool
));
297 svn_pool_destroy(subpool
);
302 /* Find the operation associated with PATH, which is a single-path
303 component representing a child of the path represented by
304 OPERATION. If no such child operation exists, create a new one of
306 static struct operation
*
307 get_operation(const char *path
,
308 struct operation
*operation
,
311 struct operation
*child
= apr_hash_get(operation
->children
, path
,
312 APR_HASH_KEY_STRING
);
315 child
= apr_pcalloc(pool
, sizeof(*child
));
316 child
->children
= apr_hash_make(pool
);
317 child
->operation
= OP_OPEN
;
318 child
->rev
= SVN_INVALID_REVNUM
;
319 child
->kind
= svn_node_dir
;
320 child
->props
= apr_table_make(pool
, 0);
321 apr_hash_set(operation
->children
, path
, APR_HASH_KEY_STRING
, child
);
326 /* Return the portion of URL that is relative to ANCHOR. */
328 subtract_anchor(const char *anchor
, const char *url
, apr_pool_t
*pool
)
330 if (! strcmp(url
, anchor
))
333 return svn_path_uri_decode(svn_path_is_child(anchor
, url
, pool
), pool
);
336 /* Add PATH to the operations tree rooted at OPERATION, creating any
337 intermediate nodes that are required. Here's what's expected for
340 ACTION URL REV SRC-FILE PROPNAME
341 ------------ ----- ------- -------- --------
342 ACTION_MKDIR NULL invalid NULL NULL
343 ACTION_CP valid valid NULL NULL
344 ACTION_PUT NULL invalid valid NULL
345 ACTION_RM NULL invalid NULL NULL
346 ACTION_PROPSET valid invalid NULL valid
347 ACTION_PROPDEL valid invalid NULL valid
349 Node type information is obtained for any copy source (to determine
350 whether to create a file or directory) and for any deleted path (to
351 ensure it exists since svn_delta_editor_t->delete_entry doesn't
352 return an error on non-existent nodes). */
354 build(action_code_t action
,
358 const char *prop_name
,
359 const char *prop_value
,
360 const char *src_file
,
363 svn_ra_session_t
*session
,
364 struct operation
*operation
,
367 apr_array_header_t
*path_bits
= svn_path_decompose(path
, pool
);
368 const char *path_so_far
= "";
369 const char *copy_src
= NULL
;
370 svn_revnum_t copy_rev
= SVN_INVALID_REVNUM
;
373 /* Look for any previous operations we've recognized for PATH. If
374 any of PATH's ancestors have not yet been traversed, we'll be
375 creating OP_OPEN operations for them as we walk down PATH's path
377 for (i
= 0; i
< path_bits
->nelts
; ++i
)
379 const char *path_bit
= APR_ARRAY_IDX(path_bits
, i
, const char *);
380 path_so_far
= svn_path_join(path_so_far
, path_bit
, pool
);
381 operation
= get_operation(path_so_far
, operation
, pool
);
383 /* If we cross a replace- or add-with-history, remember the
384 source of those things in case we need to lookup the node kind
385 of one of their children. And if this isn't such a copy,
386 but we've already seen one in of our parent paths, we just need
387 to extend that copy source path by our current path
390 && SVN_IS_VALID_REVNUM(operation
->rev
)
391 && (operation
->operation
== OP_REPLACE
392 || operation
->operation
== OP_ADD
))
394 copy_src
= subtract_anchor(anchor
, operation
->url
, pool
);
395 copy_rev
= operation
->rev
;
399 copy_src
= svn_path_join(copy_src
, path_bit
, pool
);
403 /* Handle property changes. */
406 if (operation
->operation
== OP_DELETE
)
407 return svn_error_createf(SVN_ERR_BAD_URL
, NULL
,
408 "cannot set properties on a location being"
409 " deleted ('%s')", path
);
410 SVN_ERR(svn_ra_check_path(session
,
411 copy_src
? copy_src
: path
,
412 copy_src
? copy_rev
: head
,
413 &operation
->kind
, pool
));
414 if (operation
->kind
== svn_node_none
)
415 return svn_error_createf(SVN_ERR_BAD_URL
, NULL
,
416 "propset: '%s' not found", path
);
417 else if ((operation
->kind
== svn_node_file
)
418 && (operation
->operation
== OP_OPEN
))
419 operation
->operation
= OP_PROPSET
;
420 apr_table_set(operation
->props
, prop_name
, prop_value
);
422 operation
->rev
= rev
;
426 /* We won't fuss about multiple operations on the same path in the
429 - the prior operation was, in fact, a no-op (open)
430 - the prior operation was a propset placeholder
431 - the prior operation was a deletion
433 Note: while the operation structure certainly supports the
434 ability to do a copy of a file followed by a put of new contents
435 for the file, we don't let that happen (yet).
437 if (operation
->operation
!= OP_OPEN
438 && operation
->operation
!= OP_PROPSET
439 && operation
->operation
!= OP_DELETE
)
440 return svn_error_createf(SVN_ERR_BAD_URL
, NULL
,
441 "unsupported multiple operations on '%s'", path
);
443 /* For deletions, we validate that there's actually something to
444 delete. If this is a deletion of the child of a copied
445 directory, we need to remember to look in the copy source tree to
446 verify that this thing actually exists. */
447 if (action
== ACTION_RM
)
449 operation
->operation
= OP_DELETE
;
450 SVN_ERR(svn_ra_check_path(session
,
451 copy_src
? copy_src
: path
,
452 copy_src
? copy_rev
: head
,
453 &operation
->kind
, pool
));
454 if (operation
->kind
== svn_node_none
)
456 if (copy_src
&& strcmp(path
, copy_src
))
457 return svn_error_createf(SVN_ERR_BAD_URL
, NULL
,
458 "'%s' (from '%s:%ld') not found",
459 path
, copy_src
, copy_rev
);
461 return svn_error_createf(SVN_ERR_BAD_URL
, NULL
, "'%s' not found",
465 /* Handle copy operations (which can be adds or replacements). */
466 else if (action
== ACTION_CP
)
469 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR
, NULL
,
470 "Copy source revision cannot be younger "
471 "than base revision");
472 operation
->operation
=
473 operation
->operation
== OP_DELETE
? OP_REPLACE
: OP_ADD
;
474 SVN_ERR(svn_ra_check_path(session
, subtract_anchor(anchor
, url
, pool
),
475 rev
, &operation
->kind
, pool
));
476 if (operation
->kind
== svn_node_none
)
477 return svn_error_createf(SVN_ERR_BAD_URL
, NULL
,
478 "'%s' not found", url
);
479 operation
->url
= url
;
480 operation
->rev
= rev
;
482 /* Handle mkdir operations (which can be adds or replacements). */
483 else if (action
== ACTION_MKDIR
)
485 operation
->operation
=
486 operation
->operation
== OP_DELETE
? OP_REPLACE
: OP_ADD
;
487 operation
->kind
= svn_node_dir
;
489 /* Handle put operations (which can be adds, replacements, or opens). */
490 else if (action
== ACTION_PUT
)
492 if (operation
->operation
== OP_DELETE
)
494 operation
->operation
= OP_REPLACE
;
498 SVN_ERR(svn_ra_check_path(session
,
499 copy_src
? copy_src
: path
,
500 copy_src
? copy_rev
: head
,
501 &operation
->kind
, pool
));
502 if (operation
->kind
== svn_node_file
)
503 operation
->operation
= OP_OPEN
;
504 else if (operation
->kind
== svn_node_none
)
505 operation
->operation
= OP_ADD
;
507 return svn_error_createf(SVN_ERR_BAD_URL
, NULL
,
508 "'%s' is not a file", path
);
510 operation
->kind
= svn_node_file
;
511 operation
->src_file
= src_file
;
515 /* We shouldn't get here. */
523 action_code_t action
;
525 /* revision (copy-from-rev of path[0] for cp; base-rev for put) */
528 /* action path[0] path[1]
529 * ------ ------- -------
531 * mkdir target (null)
535 * propset target (null)
539 /* property name/value */
540 const char *prop_name
;
541 const char *prop_value
;
545 execute(const apr_array_header_t
*actions
,
547 apr_hash_t
*revprops
,
548 const char *username
,
549 const char *password
,
550 const char *config_dir
,
551 svn_boolean_t non_interactive
,
552 svn_revnum_t base_revision
,
555 svn_ra_session_t
*session
;
557 const svn_delta_editor_t
*editor
;
559 struct operation root
;
564 SVN_ERR(svn_config_get_config(&config
, config_dir
, pool
));
565 SVN_ERR(svn_ra_open(&session
, anchor
,
566 ra_callbacks(username
, password
, non_interactive
, pool
),
567 NULL
, config
, pool
));
569 SVN_ERR(svn_ra_get_latest_revnum(session
, &head
, pool
));
570 if (SVN_IS_VALID_REVNUM(base_revision
))
572 if (base_revision
> head
)
573 return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION
, NULL
,
574 "No such revision %ld (youngest is %ld)",
575 base_revision
, head
);
576 head
= base_revision
;
579 root
.children
= apr_hash_make(pool
);
580 root
.operation
= OP_OPEN
;
581 for (i
= 0; i
< actions
->nelts
; ++i
)
583 struct action
*action
= APR_ARRAY_IDX(actions
, i
, struct action
*);
584 switch (action
->action
)
586 const char *path1
, *path2
;
588 path1
= subtract_anchor(anchor
, action
->path
[0], pool
);
589 path2
= subtract_anchor(anchor
, action
->path
[1], pool
);
590 SVN_ERR(build(ACTION_RM
, path1
, NULL
,
591 SVN_INVALID_REVNUM
, NULL
, NULL
, NULL
, head
, anchor
,
592 session
, &root
, pool
));
593 SVN_ERR(build(ACTION_CP
, path2
, action
->path
[0],
594 head
, NULL
, NULL
, NULL
, head
, anchor
,
595 session
, &root
, pool
));
598 path1
= subtract_anchor(anchor
, action
->path
[0], pool
);
599 path2
= subtract_anchor(anchor
, action
->path
[1], pool
);
600 if (action
->rev
== SVN_INVALID_REVNUM
)
602 SVN_ERR(build(ACTION_CP
, path2
, action
->path
[0],
603 action
->rev
, NULL
, NULL
, NULL
, head
, anchor
,
604 session
, &root
, pool
));
607 path1
= subtract_anchor(anchor
, action
->path
[0], pool
);
608 SVN_ERR(build(ACTION_RM
, path1
, NULL
,
609 SVN_INVALID_REVNUM
, NULL
, NULL
, NULL
, head
, anchor
,
610 session
, &root
, pool
));
613 path1
= subtract_anchor(anchor
, action
->path
[0], pool
);
614 SVN_ERR(build(ACTION_MKDIR
, path1
, action
->path
[0],
615 SVN_INVALID_REVNUM
, NULL
, NULL
, NULL
, head
, anchor
,
616 session
, &root
, pool
));
619 path1
= subtract_anchor(anchor
, action
->path
[0], pool
);
620 SVN_ERR(build(ACTION_PUT
, path1
, action
->path
[0],
621 SVN_INVALID_REVNUM
, NULL
, NULL
, action
->path
[1],
622 head
, anchor
, session
, &root
, pool
));
626 path1
= subtract_anchor(anchor
, action
->path
[0], pool
);
627 SVN_ERR(build(action
->action
, path1
, action
->path
[0],
629 action
->prop_name
, action
->prop_value
,
630 NULL
, head
, anchor
, session
, &root
, pool
));
635 SVN_ERR(svn_ra_get_commit_editor3(session
, &editor
, &editor_baton
, revprops
,
636 commit_callback
, NULL
, NULL
, FALSE
, pool
));
638 SVN_ERR(editor
->open_root(editor_baton
, head
, pool
, &root
.baton
));
639 err
= drive(&root
, head
, editor
, pool
);
641 err
= editor
->close_edit(editor_baton
, pool
);
643 svn_error_clear(editor
->abort_edit(editor_baton
, pool
));
649 usage(apr_pool_t
*pool
, int exit_val
)
651 FILE *stream
= exit_val
== EXIT_SUCCESS
? stdout
: stderr
;
653 "Multiple URL Command Client (for Subversion)\n"
654 "\nUsage: svnmucc [OPTION]... [ACTION]...\n"
656 " cp REV URL1 URL2 copy URL1@REV to URL2\n"
657 " mkdir URL create new directory URL\n"
658 " mv URL1 URL2 move URL1 to URL2\n"
659 " rm URL delete URL\n"
660 " put SRC-FILE URL add or modify file URL with contents copied\n"
662 " propset NAME VAL URL Set property NAME on URL to value VAL\n"
663 " propdel NAME URL Delete property NAME from URL\n"
665 " -h, --help display this text\n"
666 " -m, --message ARG use ARG as a log message\n"
667 " -F, --file ARG read log message from file ARG\n"
668 " -u, --username ARG commit the changes as username ARG\n"
669 " -p, --password ARG use ARG as the password\n"
670 " -U, --root-url ARG interpret all action URLs are relative to ARG\n"
671 " -r, --revision ARG use revision ARG as baseline for changes\n"
672 " --with-revprop A[=B] set revision property A in new revision to B\n"
673 " if specified, else to the empty string\n"
674 " -n, --non-interactive don't prompt the user about anything\n"
675 " -X, --extra-args ARG append arguments from file ARG (one per line;\n"
676 " use \"-\" to read from standard input)\n"
677 " --config-dir ARG use ARG to override the config directory\n";
678 svn_error_clear(svn_cmdline_fputs(msg
, stream
, pool
));
679 apr_pool_destroy(pool
);
684 insufficient(apr_pool_t
*pool
)
686 handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS
, NULL
,
687 "insufficient arguments"),
692 main(int argc
, const char **argv
)
694 apr_pool_t
*pool
= init("svnmucc");
695 apr_array_header_t
*actions
= apr_array_make(pool
, 1,
696 sizeof(struct action
*));
697 const char *anchor
= NULL
;
698 svn_error_t
*err
= SVN_NO_ERROR
;
699 apr_getopt_t
*getopt
;
701 config_dir_opt
= SVN_OPT_FIRST_LONGOPT_ID
,
704 const apr_getopt_option_t options
[] = {
705 {"message", 'm', 1, ""},
706 {"file", 'F', 1, ""},
707 {"username", 'u', 1, ""},
708 {"password", 'p', 1, ""},
709 {"root-url", 'U', 1, ""},
710 {"revision", 'r', 1, ""},
711 {"with-revprop", with_revprop_opt
, 1, ""},
712 {"extra-args", 'X', 1, ""},
713 {"help", 'h', 0, ""},
714 {"non-interactive", 'n', 0, ""},
715 {"config-dir", config_dir_opt
, 1, ""},
718 const char *message
= NULL
;
719 const char *username
= NULL
, *password
= NULL
;
720 const char *root_url
= NULL
, *extra_args_file
= NULL
;
721 const char *config_dir
= NULL
;
722 svn_boolean_t non_interactive
= FALSE
;
723 svn_revnum_t base_revision
= SVN_INVALID_REVNUM
;
724 apr_array_header_t
*action_args
;
725 apr_hash_t
*revprops
= apr_hash_make(pool
);
728 apr_getopt_init(&getopt
, pool
, argc
, argv
);
729 getopt
->interleave
= 1;
734 apr_status_t status
= apr_getopt_long(getopt
, options
, &opt
, &arg
);
735 if (APR_STATUS_IS_EOF(status
))
737 if (status
!= APR_SUCCESS
)
738 handle_error(svn_error_wrap_apr(status
, "getopt failure"), pool
);
742 err
= svn_utf_cstring_to_utf8(&message
, arg
, pool
);
744 handle_error(err
, pool
);
748 const char *arg_utf8
;
749 svn_stringbuf_t
*contents
;
750 err
= svn_utf_cstring_to_utf8(&arg_utf8
, arg
, pool
);
752 err
= svn_stringbuf_from_file(&contents
, arg
, pool
);
754 err
= svn_utf_cstring_to_utf8(&message
, contents
->data
, pool
);
756 handle_error(err
, pool
);
760 username
= apr_pstrdup(pool
, arg
);
763 password
= apr_pstrdup(pool
, arg
);
766 err
= svn_utf_cstring_to_utf8(&root_url
, arg
, pool
);
768 handle_error(err
, pool
);
769 root_url
= svn_path_canonicalize(root_url
, pool
);
770 if (! svn_path_is_url(root_url
))
771 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS
, NULL
,
772 "'%s' is not a URL\n", root_url
),
777 char *digits_end
= NULL
;
778 base_revision
= strtol(arg
, &digits_end
, 10);
779 if ((! SVN_IS_VALID_REVNUM(base_revision
))
782 handle_error(svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR
,
783 NULL
, "Invalid revision number"),
787 case with_revprop_opt
:
788 err
= svn_opt_parse_revprop(&revprops
, arg
, pool
);
789 if (err
!= SVN_NO_ERROR
)
790 handle_error(err
, pool
);
793 extra_args_file
= apr_pstrdup(pool
, arg
);
796 non_interactive
= TRUE
;
799 err
= svn_utf_cstring_to_utf8(&config_dir
, arg
, pool
);
801 handle_error(err
, pool
);
804 usage(pool
, EXIT_SUCCESS
);
808 /* Copy the rest of our command-line arguments to an array,
809 UTF-8-ing them along the way. */
810 action_args
= apr_array_make(pool
, getopt
->argc
, sizeof(const char *));
811 while (getopt
->ind
< getopt
->argc
)
813 const char *arg
= getopt
->argv
[getopt
->ind
++];
814 if ((err
= svn_utf_cstring_to_utf8(&(APR_ARRAY_PUSH(action_args
,
817 handle_error(err
, pool
);
820 /* If there are extra arguments in a supplementary file, tack those
821 on, too (again, in UTF8 form). */
824 const char *extra_args_file_utf8
;
825 svn_stringbuf_t
*contents
, *contents_utf8
;
827 err
= svn_utf_cstring_to_utf8(&extra_args_file_utf8
,
828 extra_args_file
, pool
);
830 err
= svn_stringbuf_from_file2(&contents
, extra_args_file_utf8
, pool
);
832 err
= svn_utf_stringbuf_to_utf8(&contents_utf8
, contents
, pool
);
834 handle_error(err
, pool
);
835 svn_cstring_split_append(action_args
, contents_utf8
->data
, "\n\r",
839 /* Now, we iterate over the combined set of arguments -- our actions. */
840 for (i
= 0; i
< action_args
->nelts
; )
843 const char *action_string
= APR_ARRAY_IDX(action_args
, i
, const char *);
844 struct action
*action
= apr_palloc(pool
, sizeof(*action
));
846 /* First, parse the action. */
847 if (! strcmp(action_string
, "mv"))
848 action
->action
= ACTION_MV
;
849 else if (! strcmp(action_string
, "cp"))
850 action
->action
= ACTION_CP
;
851 else if (! strcmp(action_string
, "mkdir"))
852 action
->action
= ACTION_MKDIR
;
853 else if (! strcmp(action_string
, "rm"))
854 action
->action
= ACTION_RM
;
855 else if (! strcmp(action_string
, "put"))
856 action
->action
= ACTION_PUT
;
857 else if (! strcmp(action_string
, "propset"))
858 action
->action
= ACTION_PROPSET
;
859 else if (! strcmp(action_string
, "propdel"))
860 action
->action
= ACTION_PROPDEL
;
862 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS
, NULL
,
863 "'%s' is not an action\n",
864 action_string
), pool
);
865 if (++i
== action_args
->nelts
)
868 /* For copies, there should be a revision number next. */
869 if (action
->action
== ACTION_CP
)
871 const char *rev_str
= APR_ARRAY_IDX(action_args
, i
, const char *);
872 if (strcmp(rev_str
, "head") == 0)
873 action
->rev
= SVN_INVALID_REVNUM
;
874 else if (strcmp(rev_str
, "HEAD") == 0)
875 action
->rev
= SVN_INVALID_REVNUM
;
879 action
->rev
= strtol(rev_str
, &end
, 0);
881 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS
, NULL
,
882 "'%s' is not a revision\n",
885 if (++i
== action_args
->nelts
)
890 action
->rev
= SVN_INVALID_REVNUM
;
893 /* For puts, there should be a local file next. */
894 if (action
->action
== ACTION_PUT
)
896 action
->path
[1] = svn_path_canonicalize(APR_ARRAY_IDX(action_args
,
900 if (++i
== action_args
->nelts
)
904 /* For propset and propdel, a property name (and maybe value)
906 if ((action
->action
== ACTION_PROPSET
)
907 || (action
->action
== ACTION_PROPDEL
))
909 action
->prop_name
= APR_ARRAY_IDX(action_args
, i
, const char *);
910 if (++i
== action_args
->nelts
)
913 if (action
->action
== ACTION_PROPDEL
)
915 action
->prop_value
= NULL
;
919 action
->prop_value
= APR_ARRAY_IDX(action_args
, i
, const char *);
920 if (++i
== action_args
->nelts
)
925 /* How many URLs does this action expect? */
926 if (action
->action
== ACTION_RM
927 || action
->action
== ACTION_MKDIR
928 || action
->action
== ACTION_PUT
929 || action
->action
== ACTION_PROPSET
930 || action
->action
== ACTION_PROPDEL
)
935 /* Parse the required number of URLs. */
936 for (j
= 0; j
< num_url_args
; ++j
)
938 const char *url
= APR_ARRAY_IDX(action_args
, i
, const char *);
940 /* If there's a root URL, we expect this to be a path
941 relative to that URL. Otherwise, it should be a full URL. */
943 url
= svn_path_join(root_url
, url
, pool
);
944 else if (! svn_path_is_url(url
))
945 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS
, NULL
,
946 "'%s' is not a URL\n", url
), pool
);
947 url
= svn_path_uri_from_iri(url
, pool
);
948 url
= svn_path_uri_autoescape(url
, pool
);
949 url
= svn_path_canonicalize(url
, pool
);
950 action
->path
[j
] = url
;
952 /* The cp source could be the anchor, but the other URLs should be
953 children of the anchor. */
954 if (! (action
->action
== ACTION_CP
&& j
== 0))
955 url
= svn_path_dirname(url
, pool
);
959 anchor
= svn_path_get_longest_ancestor(anchor
, url
, pool
);
961 if ((++i
== action_args
->nelts
) && (j
>= num_url_args
))
964 APR_ARRAY_PUSH(actions
, struct action
*) = action
;
967 if (! actions
->nelts
)
968 usage(pool
, EXIT_FAILURE
);
972 if (apr_hash_get(revprops
, SVN_PROP_REVISION_LOG
,
973 APR_HASH_KEY_STRING
) == NULL
)
974 /* None of -F, -m, or --with-revprop=svn:log specified; default. */
975 apr_hash_set(revprops
, SVN_PROP_REVISION_LOG
, APR_HASH_KEY_STRING
,
976 svn_string_create("committed using svnmucc", pool
));
979 /* -F or -m specified; use that even if --with-revprop=svn:log. */
980 apr_hash_set(revprops
, SVN_PROP_REVISION_LOG
, APR_HASH_KEY_STRING
,
981 svn_string_create(message
, pool
));
983 if ((err
= execute(actions
, anchor
, revprops
, username
, password
,
984 config_dir
, non_interactive
, base_revision
, pool
)))
985 handle_error(err
, pool
);
987 svn_pool_destroy(pool
);