Fix compiler warning due to missing function prototype.
[svn.git] / contrib / client-side / svnmucc / svnmucc.c
blob83157edeee05cd5ea4edba65141894c7a2783ac1
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"
22 #include "svn_path.h"
23 #include "svn_ra.h"
24 #include "svn_config.h"
25 #include "svn_props.h"
26 #include "svn_string.h"
27 #include <apr_lib.h>
28 #include <stdio.h>
29 #include <string.h>
31 static void handle_error(svn_error_t *err, apr_pool_t *pool)
33 if (err)
34 svn_handle_error2(err, stderr, FALSE, "svnmucc: ");
35 svn_error_clear(err);
36 if (pool)
37 svn_pool_destroy(pool);
38 exit(EXIT_FAILURE);
41 static apr_pool_t *
42 init(const char *application)
44 apr_allocator_t *allocator;
45 apr_pool_t *pool;
46 svn_error_t *err;
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},
51 {NULL, NULL}
54 SVN_VERSION_DEFINE(my_version);
56 if (svn_cmdline_init(application, stderr)
57 || apr_allocator_create(&allocator))
58 exit(EXIT_FAILURE);
60 err = svn_ver_check_list(&my_version, checklist);
61 if (err)
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);
68 return pool;
71 static svn_error_t *
72 open_tmp_file(apr_file_t **fp,
73 void *callback_baton,
74 apr_pool_t *pool)
76 const char *temp_dir;
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,
89 const char *password,
90 svn_boolean_t non_interactive,
91 apr_pool_t *pool)
93 svn_ra_callbacks_t *callbacks = apr_palloc(pool, sizeof(*callbacks));
94 svn_cmdline_setup_auth_baton(&callbacks->auth_baton, non_interactive,
95 username, password,
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;
103 return callbacks;
108 static svn_error_t *
109 commit_callback(const svn_commit_info_t *commit_info,
110 void *baton,
111 apr_pool_t *pool)
113 SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n",
114 commit_info->revision,
115 (commit_info->author
116 ? commit_info->author : "(no author)"),
117 commit_info->date));
118 return SVN_NO_ERROR;
121 typedef enum {
122 ACTION_MV,
123 ACTION_MKDIR,
124 ACTION_CP,
125 ACTION_PROPSET,
126 ACTION_PROPDEL,
127 ACTION_PUT,
128 ACTION_RM
129 } action_code_t;
131 struct operation {
132 enum {
133 OP_OPEN,
134 OP_DELETE,
135 OP_ADD,
136 OP_REPLACE,
137 OP_PROPSET /* only for files for which no other operation is
138 occuring; directories are OP_OPEN with non-empty
139 props */
140 } operation;
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;
155 apr_pool_t *pool;
156 void *baton;
157 svn_error_t* err;
161 /* An iterator (for use via apr_table_do) which sets node properties.
162 REC is a pointer to a struct driver_state. */
163 static int
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,
172 value_svnstring,
173 d_state->pool);
174 else
175 d_state->err = d_state->editor->change_file_prop(d_state->baton, key,
176 value_svnstring,
177 d_state->pool);
178 if (d_state->err)
179 return 0;
180 return 1;
184 /* Drive EDITOR to affect the change represented by OPERATION. HEAD
185 is the last-known youngest revision in the repository. */
186 static svn_error_t *
187 drive(struct operation *operation,
188 svn_revnum_t head,
189 const svn_delta_editor_t *editor,
190 apr_pool_t *pool)
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))
198 const void *key;
199 void *val;
200 struct operation *child;
201 void *file_baton = NULL;
203 svn_pool_clear(subpool);
204 apr_hash_this(hi, &key, NULL, &val);
205 child = 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));
220 else
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));
236 else
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;
247 void *handler_baton;
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,
261 then close it. */
262 if (file_baton)
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))
272 SVN_ERR(state.err);
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))
292 SVN_ERR(state.err);
294 SVN_ERR(editor->close_directory(child->baton, subpool));
297 svn_pool_destroy(subpool);
298 return SVN_NO_ERROR;
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
305 type OP_OPEN. */
306 static struct operation *
307 get_operation(const char *path,
308 struct operation *operation,
309 apr_pool_t *pool)
311 struct operation *child = apr_hash_get(operation->children, path,
312 APR_HASH_KEY_STRING);
313 if (! child)
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);
323 return child;
326 /* Return the portion of URL that is relative to ANCHOR. */
327 static const char *
328 subtract_anchor(const char *anchor, const char *url, apr_pool_t *pool)
330 if (! strcmp(url, anchor))
331 return "";
332 else
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
338 each action type:
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). */
353 static svn_error_t *
354 build(action_code_t action,
355 const char *path,
356 const char *url,
357 svn_revnum_t rev,
358 const char *prop_name,
359 const char *prop_value,
360 const char *src_file,
361 svn_revnum_t head,
362 const char *anchor,
363 svn_ra_session_t *session,
364 struct operation *operation,
365 apr_pool_t *pool)
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;
371 int i;
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
376 components. */
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
388 component. */
389 if (operation->url
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;
397 else if (copy_src)
399 copy_src = svn_path_join(copy_src, path_bit, pool);
403 /* Handle property changes. */
404 if (prop_name)
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);
421 if (!operation->rev)
422 operation->rev = rev;
423 return SVN_NO_ERROR;
426 /* We won't fuss about multiple operations on the same path in the
427 following cases:
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);
460 else
461 return svn_error_createf(SVN_ERR_BAD_URL, NULL, "'%s' not found",
462 path);
465 /* Handle copy operations (which can be adds or replacements). */
466 else if (action == ACTION_CP)
468 if (rev > head)
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;
496 else
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;
506 else
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;
513 else
515 /* We shouldn't get here. */
516 abort();
519 return SVN_NO_ERROR;
522 struct action {
523 action_code_t action;
525 /* revision (copy-from-rev of path[0] for cp; base-rev for put) */
526 svn_revnum_t rev;
528 /* action path[0] path[1]
529 * ------ ------- -------
530 * mv source target
531 * mkdir target (null)
532 * cp source target
533 * put target source
534 * rm target (null)
535 * propset target (null)
537 const char *path[2];
539 /* property name/value */
540 const char *prop_name;
541 const char *prop_value;
544 static svn_error_t *
545 execute(const apr_array_header_t *actions,
546 const char *anchor,
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,
553 apr_pool_t *pool)
555 svn_ra_session_t *session;
556 svn_revnum_t head;
557 const svn_delta_editor_t *editor;
558 void *editor_baton;
559 struct operation root;
560 svn_error_t *err;
561 apr_hash_t *config;
562 int i;
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;
587 case ACTION_MV:
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));
596 break;
597 case ACTION_CP:
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)
601 action->rev = head;
602 SVN_ERR(build(ACTION_CP, path2, action->path[0],
603 action->rev, NULL, NULL, NULL, head, anchor,
604 session, &root, pool));
605 break;
606 case ACTION_RM:
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));
611 break;
612 case ACTION_MKDIR:
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));
617 break;
618 case ACTION_PUT:
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));
623 break;
624 case ACTION_PROPSET:
625 case ACTION_PROPDEL:
626 path1 = subtract_anchor(anchor, action->path[0], pool);
627 SVN_ERR(build(action->action, path1, action->path[0],
628 SVN_INVALID_REVNUM,
629 action->prop_name, action->prop_value,
630 NULL, head, anchor, session, &root, pool));
631 break;
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);
640 if (!err)
641 err = editor->close_edit(editor_baton, pool);
642 if (err)
643 svn_error_clear(editor->abort_edit(editor_baton, pool));
645 return err;
648 static void
649 usage(apr_pool_t *pool, int exit_val)
651 FILE *stream = exit_val == EXIT_SUCCESS ? stdout : stderr;
652 const char msg[] =
653 "Multiple URL Command Client (for Subversion)\n"
654 "\nUsage: svnmucc [OPTION]... [ACTION]...\n"
655 "\nActions:\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"
661 " from SRC-FILE\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"
664 "\nOptions:\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);
680 exit(exit_val);
683 static void
684 insufficient(apr_pool_t *pool)
686 handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
687 "insufficient arguments"),
688 pool);
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;
700 enum {
701 config_dir_opt = SVN_OPT_FIRST_LONGOPT_ID,
702 with_revprop_opt
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, ""},
716 {NULL, 0, 0, NULL}
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);
726 int i;
728 apr_getopt_init(&getopt, pool, argc, argv);
729 getopt->interleave = 1;
730 while (1)
732 int opt;
733 const char *arg;
734 apr_status_t status = apr_getopt_long(getopt, options, &opt, &arg);
735 if (APR_STATUS_IS_EOF(status))
736 break;
737 if (status != APR_SUCCESS)
738 handle_error(svn_error_wrap_apr(status, "getopt failure"), pool);
739 switch(opt)
741 case 'm':
742 err = svn_utf_cstring_to_utf8(&message, arg, pool);
743 if (err)
744 handle_error(err, pool);
745 break;
746 case 'F':
748 const char *arg_utf8;
749 svn_stringbuf_t *contents;
750 err = svn_utf_cstring_to_utf8(&arg_utf8, arg, pool);
751 if (! err)
752 err = svn_stringbuf_from_file(&contents, arg, pool);
753 if (! err)
754 err = svn_utf_cstring_to_utf8(&message, contents->data, pool);
755 if (err)
756 handle_error(err, pool);
758 break;
759 case 'u':
760 username = apr_pstrdup(pool, arg);
761 break;
762 case 'p':
763 password = apr_pstrdup(pool, arg);
764 break;
765 case 'U':
766 err = svn_utf_cstring_to_utf8(&root_url, arg, pool);
767 if (err)
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),
773 pool);
774 break;
775 case 'r':
777 char *digits_end = NULL;
778 base_revision = strtol(arg, &digits_end, 10);
779 if ((! SVN_IS_VALID_REVNUM(base_revision))
780 || (! digits_end)
781 || *digits_end)
782 handle_error(svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR,
783 NULL, "Invalid revision number"),
784 pool);
786 break;
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);
791 break;
792 case 'X':
793 extra_args_file = apr_pstrdup(pool, arg);
794 break;
795 case 'n':
796 non_interactive = TRUE;
797 break;
798 case config_dir_opt:
799 err = svn_utf_cstring_to_utf8(&config_dir, arg, pool);
800 if (err)
801 handle_error(err, pool);
802 break;
803 case 'h':
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,
815 const char *)),
816 arg, pool)))
817 handle_error(err, pool);
820 /* If there are extra arguments in a supplementary file, tack those
821 on, too (again, in UTF8 form). */
822 if (extra_args_file)
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);
829 if (! err)
830 err = svn_stringbuf_from_file2(&contents, extra_args_file_utf8, pool);
831 if (! err)
832 err = svn_utf_stringbuf_to_utf8(&contents_utf8, contents, pool);
833 if (err)
834 handle_error(err, pool);
835 svn_cstring_split_append(action_args, contents_utf8->data, "\n\r",
836 FALSE, pool);
839 /* Now, we iterate over the combined set of arguments -- our actions. */
840 for (i = 0; i < action_args->nelts; )
842 int j, num_url_args;
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;
861 else
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)
866 insufficient(pool);
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;
876 else
878 char *end;
879 action->rev = strtol(rev_str, &end, 0);
880 if (*end)
881 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
882 "'%s' is not a revision\n",
883 rev_str), pool);
885 if (++i == action_args->nelts)
886 insufficient(pool);
888 else
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,
898 const char *),
899 pool);
900 if (++i == action_args->nelts)
901 insufficient(pool);
904 /* For propset and propdel, a property name (and maybe value)
905 comes next. */
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)
911 insufficient(pool);
913 if (action->action == ACTION_PROPDEL)
915 action->prop_value = NULL;
917 else
919 action->prop_value = APR_ARRAY_IDX(action_args, i, const char *);
920 if (++i == action_args->nelts)
921 insufficient(pool);
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)
931 num_url_args = 1;
932 else
933 num_url_args = 2;
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. */
942 if (root_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);
956 if (! anchor)
957 anchor = url;
958 else
959 anchor = svn_path_get_longest_ancestor(anchor, url, pool);
961 if ((++i == action_args->nelts) && (j >= num_url_args))
962 insufficient(pool);
964 APR_ARRAY_PUSH(actions, struct action *) = action;
967 if (! actions->nelts)
968 usage(pool, EXIT_FAILURE);
970 if (message == NULL)
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));
978 else
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);
988 return EXIT_SUCCESS;