Follow-up to r29036: Now that the "mergeinfo" transaction file is no
[svn.git] / contrib / client-side / svnmucc / svnmucc.c
blob49ec2687c8202a95c658c494d280c70b631543d0
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 <apr_lib.h>
26 #include <stdio.h>
27 #include <string.h>
29 static void handle_error(svn_error_t *err, apr_pool_t *pool)
31 if (err)
32 svn_handle_error2(err, stderr, FALSE, "svnmucc: ");
33 svn_error_clear(err);
34 if (pool)
35 svn_pool_destroy(pool);
36 exit(EXIT_FAILURE);
39 static apr_pool_t *
40 init(const char *application)
42 apr_allocator_t *allocator;
43 apr_pool_t *pool;
44 svn_error_t *err;
45 const svn_version_checklist_t checklist[] = {
46 {"svn_client", svn_client_version},
47 {"svn_subr", svn_subr_version},
48 {"svn_ra", svn_ra_version},
49 {NULL, NULL}
52 SVN_VERSION_DEFINE(my_version);
54 if (svn_cmdline_init(application, stderr)
55 || apr_allocator_create(&allocator))
56 exit(EXIT_FAILURE);
58 err = svn_ver_check_list(&my_version, checklist);
59 if (err)
60 handle_error(err, NULL);
62 apr_allocator_max_free_set(allocator, SVN_ALLOCATOR_RECOMMENDED_MAX_FREE);
63 pool = svn_pool_create_ex(NULL, allocator);
64 apr_allocator_owner_set(allocator, pool);
66 return pool;
69 static svn_error_t *
70 open_tmp_file(apr_file_t **fp,
71 void *callback_baton,
72 apr_pool_t *pool)
74 const char *temp_dir;
76 /* "Say, Subversion. Seen any good tempdirs lately?" */
77 SVN_ERR(svn_io_temp_dir(&temp_dir, pool));
79 /* Open a unique file; use APR_DELONCLOSE. */
80 return svn_io_open_unique_file2(fp, NULL,
81 svn_path_join(temp_dir, "svnmucc", pool),
82 ".tmp", svn_io_file_del_on_close, pool);
85 static svn_ra_callbacks_t *
86 ra_callbacks(const char *username,
87 const char *password,
88 svn_boolean_t non_interactive,
89 apr_pool_t *pool)
91 svn_ra_callbacks_t *callbacks = apr_palloc(pool, sizeof(*callbacks));
92 svn_cmdline_setup_auth_baton(&callbacks->auth_baton, non_interactive,
93 username, password,
94 NULL, FALSE, NULL, NULL, NULL, pool);
95 callbacks->open_tmp_file = open_tmp_file;
96 callbacks->get_wc_prop = NULL;
97 callbacks->set_wc_prop = NULL;
98 callbacks->push_wc_prop = NULL;
99 callbacks->invalidate_wc_props = NULL;
101 return callbacks;
104 static svn_error_t *
105 commit_callback(svn_revnum_t revision,
106 const char *date,
107 const char *author,
108 void *baton)
110 apr_pool_t *pool = baton;
112 SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n",
113 revision, author ? author : "(no author)",
114 date));
115 return SVN_NO_ERROR;
118 typedef enum {
119 ACTION_MV,
120 ACTION_MKDIR,
121 ACTION_CP,
122 ACTION_PROPSET,
123 ACTION_PROPDEL,
124 ACTION_PUT,
125 ACTION_RM
126 } action_code_t;
128 struct operation {
129 enum {
130 OP_OPEN,
131 OP_DELETE,
132 OP_ADD,
133 OP_REPLACE,
134 OP_PROPSET /* only for files for which no other operation is
135 occuring; directories are OP_OPEN with non-empty
136 props */
137 } operation;
138 svn_node_kind_t kind; /* to copy, mkdir, put or set revprops */
139 svn_revnum_t rev; /* to copy, valid for add and replace */
140 const char *url; /* to copy, valid for add and replace */
141 const char *src_file; /* for put, the source file for contents */
142 apr_hash_t *children; /* const char *path -> struct operation * */
143 apr_table_t *props; /* const char *prop_name -> const char *prop_value */
144 void *baton; /* as returned by the commit editor */
148 /* State to be passed to set_props iterator */
149 struct driver_state {
150 const svn_delta_editor_t *editor;
151 svn_node_kind_t kind;
152 apr_pool_t *pool;
153 void *baton;
154 svn_error_t* err;
158 /* An iterator (for use via apr_table_do) which sets node properties.
159 REC is a pointer to a struct driver_state. */
160 static int
161 set_props(void *rec, const char *key, const char *value)
163 struct driver_state *d_state = (struct driver_state*)rec;
164 svn_string_t *value_svnstring
165 = value ? svn_string_create(value, d_state->pool) : NULL;
167 if (d_state->kind == svn_node_dir)
168 d_state->err = d_state->editor->change_dir_prop(d_state->baton, key,
169 value_svnstring,
170 d_state->pool);
171 else
172 d_state->err = d_state->editor->change_file_prop(d_state->baton, key,
173 value_svnstring,
174 d_state->pool);
175 if (d_state->err)
176 return 0;
177 return 1;
181 /* Drive EDITOR to affect the change represented by OPERATION. HEAD
182 is the last-known youngest revision in the repository. */
183 static svn_error_t *
184 drive(struct operation *operation,
185 svn_revnum_t head,
186 const svn_delta_editor_t *editor,
187 apr_pool_t *pool)
189 apr_pool_t *subpool = svn_pool_create(pool);
190 apr_hash_index_t *hi;
191 struct driver_state state;
192 for (hi = apr_hash_first(pool, operation->children);
193 hi; hi = apr_hash_next(hi))
195 const void *key;
196 void *val;
197 struct operation *child;
198 void *file_baton = NULL;
200 svn_pool_clear(subpool);
201 apr_hash_this(hi, &key, NULL, &val);
202 child = val;
204 /* Deletes and replacements are simple -- delete something. */
205 if (child->operation == OP_DELETE || child->operation == OP_REPLACE)
207 SVN_ERR(editor->delete_entry(key, head, operation->baton, subpool));
209 /* Opens could be for directories or files. */
210 if (child->operation == OP_OPEN)
212 if (child->kind == svn_node_dir)
214 SVN_ERR(editor->open_directory(key, operation->baton, head,
215 subpool, &child->baton));
217 else
219 SVN_ERR(editor->open_file(key, operation->baton, head,
220 subpool, &file_baton));
223 /* Adds and replacements could also be for directories or files. */
224 if (child->operation == OP_ADD || child->operation == OP_REPLACE
225 || child->operation == OP_PROPSET)
227 if (child->kind == svn_node_dir)
229 SVN_ERR(editor->add_directory(key, operation->baton,
230 child->url, child->rev,
231 subpool, &child->baton));
233 else
235 SVN_ERR(editor->add_file(key, operation->baton, child->url,
236 child->rev, subpool, &file_baton));
239 /* If there's a source file and an open file baton, we get to
240 change textual contents. */
241 if ((child->src_file) && (file_baton))
243 svn_txdelta_window_handler_t handler;
244 void *handler_baton;
245 svn_stream_t *contents;
246 apr_file_t *f = NULL;
248 SVN_ERR(editor->apply_textdelta(file_baton, NULL, subpool,
249 &handler, &handler_baton));
250 SVN_ERR(svn_io_file_open(&f, child->src_file, APR_READ,
251 APR_OS_DEFAULT, pool));
252 contents = svn_stream_from_aprfile(f, pool);
253 SVN_ERR(svn_txdelta_send_stream(contents, handler,
254 handler_baton, NULL, pool));
255 SVN_ERR(svn_io_file_close(f, pool));
257 /* If we opened a file, we need to apply outstanding propmods,
258 then close it. */
259 if (file_baton)
261 if ((child->kind == svn_node_file)
262 && (! apr_is_empty_table(child->props)))
264 state.baton = file_baton;
265 state.pool = subpool;
266 state.editor = editor;
267 state.kind = child->kind;
268 if (! apr_table_do(set_props, &state, child->props, NULL))
269 SVN_ERR(state.err);
271 SVN_ERR(editor->close_file(file_baton, NULL, subpool));
273 /* If we opened, added, or replaced a directory, we need to
274 recurse, apply outstanding propmods, and then close it. */
275 if ((child->kind == svn_node_dir)
276 && (child->operation == OP_OPEN
277 || child->operation == OP_ADD
278 || child->operation == OP_REPLACE))
280 SVN_ERR(drive(child, head, editor, subpool));
281 if ((child->kind == svn_node_dir)
282 && (! apr_is_empty_table(child->props)))
284 state.baton = child->baton;
285 state.pool = subpool;
286 state.editor = editor;
287 state.kind = child->kind;
288 if (! apr_table_do(set_props, &state, child->props, NULL))
289 SVN_ERR(state.err);
291 SVN_ERR(editor->close_directory(child->baton, subpool));
294 svn_pool_destroy(subpool);
295 return SVN_NO_ERROR;
299 /* Find the operation associated with PATH, which is a single-path
300 component representing a child of the path represented by
301 OPERATION. If no such child operation exists, create a new one of
302 type OP_OPEN. */
303 static struct operation *
304 get_operation(const char *path,
305 struct operation *operation,
306 apr_pool_t *pool)
308 struct operation *child = apr_hash_get(operation->children, path,
309 APR_HASH_KEY_STRING);
310 if (! child)
312 child = apr_pcalloc(pool, sizeof(*child));
313 child->children = apr_hash_make(pool);
314 child->operation = OP_OPEN;
315 child->rev = SVN_INVALID_REVNUM;
316 child->kind = svn_node_dir;
317 child->props = apr_table_make(pool, 0);
318 apr_hash_set(operation->children, path, APR_HASH_KEY_STRING, child);
320 return child;
323 /* Return the portion of URL that is relative to ANCHOR. */
324 static const char *
325 subtract_anchor(const char *anchor, const char *url, apr_pool_t *pool)
327 if (! strcmp(url, anchor))
328 return "";
329 else
330 return svn_path_uri_decode(svn_path_is_child(anchor, url, pool), pool);
333 /* Add PATH to the operations tree rooted at OPERATION, creating any
334 intermediate nodes that are required. Here's what's expected for
335 each action type:
337 ACTION URL REV SRC-FILE PROPNAME
338 ------------ ----- ------- -------- --------
339 ACTION_MKDIR NULL invalid NULL NULL
340 ACTION_CP valid valid NULL NULL
341 ACTION_PUT NULL invalid valid NULL
342 ACTION_RM NULL invalid NULL NULL
343 ACTION_PROPSET valid invalid NULL valid
344 ACTION_PROPDEL valid invalid NULL valid
346 Node type information is obtained for any copy source (to determine
347 whether to create a file or directory) and for any deleted path (to
348 ensure it exists since svn_delta_editor_t->delete_entry doesn't
349 return an error on non-existent nodes). */
350 static svn_error_t *
351 build(action_code_t action,
352 const char *path,
353 const char *url,
354 svn_revnum_t rev,
355 const char *prop_name,
356 const char *prop_value,
357 const char *src_file,
358 svn_revnum_t head,
359 const char *anchor,
360 svn_ra_session_t *session,
361 struct operation *operation,
362 apr_pool_t *pool)
364 apr_array_header_t *path_bits = svn_path_decompose(path, pool);
365 const char *path_so_far = "";
366 const char *copy_src = NULL;
367 svn_revnum_t copy_rev = SVN_INVALID_REVNUM;
368 int i;
370 /* Look for any previous operations we've recognized for PATH. If
371 any of PATH's ancestors have not yet been traversed, we'll be
372 creating OP_OPEN operations for them as we walk down PATH's path
373 components. */
374 for (i = 0; i < path_bits->nelts; ++i)
376 const char *path_bit = APR_ARRAY_IDX(path_bits, i, const char *);
377 path_so_far = svn_path_join(path_so_far, path_bit, pool);
378 operation = get_operation(path_so_far, operation, pool);
380 /* If we cross a replace- or add-with-history, remember the
381 source of those things in case we need to lookup the node kind
382 of one of their children. And if this isn't such a copy,
383 but we've already seen one in of our parent paths, we just need
384 to extend that copy source path by our current path
385 component. */
386 if (operation->url
387 && SVN_IS_VALID_REVNUM(operation->rev)
388 && (operation->operation == OP_REPLACE
389 || operation->operation == OP_ADD))
391 copy_src = subtract_anchor(anchor, operation->url, pool);
392 copy_rev = operation->rev;
394 else if (copy_src)
396 copy_src = svn_path_join(copy_src, path_bit, pool);
400 /* Handle property changes. */
401 if (prop_name)
403 if (operation->operation == OP_DELETE)
404 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
405 "cannot set properties on a location being"
406 " deleted ('%s')", path);
407 SVN_ERR(svn_ra_check_path(session,
408 copy_src ? copy_src : path,
409 copy_src ? copy_rev : head,
410 &operation->kind, pool));
411 if (operation->kind == svn_node_none)
412 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
413 "propset: '%s' not found", path);
414 else if ((operation->kind == svn_node_file)
415 && (operation->operation == OP_OPEN))
416 operation->operation = OP_PROPSET;
417 apr_table_set(operation->props, prop_name, prop_value);
418 if (!operation->rev)
419 operation->rev = rev;
420 return SVN_NO_ERROR;
423 /* We won't fuss about multiple operations on the same path in the
424 following cases:
426 - the prior operation was, in fact, a no-op (open)
427 - the prior operation was a propset placeholder
428 - the prior operation was a deletion
430 Note: while the operation structure certainly supports the
431 ability to do a copy of a file followed by a put of new contents
432 for the file, we don't let that happen (yet).
434 if (operation->operation != OP_OPEN
435 && operation->operation != OP_PROPSET
436 && operation->operation != OP_DELETE)
437 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
438 "unsupported multiple operations on '%s'", path);
440 /* For deletions, we validate that there's actually something to
441 delete. If this is a deletion of the child of a copied
442 directory, we need to remember to look in the copy source tree to
443 verify that this thing actually exists. */
444 if (action == ACTION_RM)
446 operation->operation = OP_DELETE;
447 SVN_ERR(svn_ra_check_path(session,
448 copy_src ? copy_src : path,
449 copy_src ? copy_rev : head,
450 &operation->kind, pool));
451 if (operation->kind == svn_node_none)
453 if (copy_src && strcmp(path, copy_src))
454 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
455 "'%s' (from '%s:%ld') not found",
456 path, copy_src, copy_rev);
457 else
458 return svn_error_createf(SVN_ERR_BAD_URL, NULL, "'%s' not found",
459 path);
462 /* Handle copy operations (which can be adds or replacements). */
463 else if (action == ACTION_CP)
465 if (rev > head)
466 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
467 "Copy source revision cannot be younger "
468 "than base revision");
469 operation->operation =
470 operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
471 SVN_ERR(svn_ra_check_path(session, subtract_anchor(anchor, url, pool),
472 rev, &operation->kind, pool));
473 if (operation->kind == svn_node_none)
474 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
475 "'%s' not found", url);
476 operation->url = url;
477 operation->rev = rev;
479 /* Handle mkdir operations (which can be adds or replacements). */
480 else if (action == ACTION_MKDIR)
482 operation->operation =
483 operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
484 operation->kind = svn_node_dir;
486 /* Handle put operations (which can be adds, replacements, or opens). */
487 else if (action == ACTION_PUT)
489 if (operation->operation == OP_DELETE)
491 operation->operation = OP_REPLACE;
493 else
495 SVN_ERR(svn_ra_check_path(session,
496 copy_src ? copy_src : path,
497 copy_src ? copy_rev : head,
498 &operation->kind, pool));
499 if (operation->kind == svn_node_file)
500 operation->operation = OP_OPEN;
501 else if (operation->kind == svn_node_none)
502 operation->operation = OP_ADD;
503 else
504 return svn_error_createf(SVN_ERR_BAD_URL, NULL,
505 "'%s' is not a file", path);
507 operation->kind = svn_node_file;
508 operation->src_file = src_file;
510 else
512 /* We shouldn't get here. */
513 abort();
516 return SVN_NO_ERROR;
519 struct action {
520 action_code_t action;
522 /* revision (copy-from-rev of path[0] for cp; base-rev for put) */
523 svn_revnum_t rev;
525 /* action path[0] path[1]
526 * ------ ------- -------
527 * mv source target
528 * mkdir target (null)
529 * cp source target
530 * put target source
531 * rm target (null)
532 * propset target (null)
534 const char *path[2];
536 /* property name/value */
537 const char *prop_name;
538 const char *prop_value;
541 static svn_error_t *
542 execute(const apr_array_header_t *actions,
543 const char *anchor,
544 const char *message,
545 const char *username,
546 const char *password,
547 const char *config_dir,
548 svn_boolean_t non_interactive,
549 svn_revnum_t base_revision,
550 apr_pool_t *pool)
552 svn_ra_session_t *session;
553 svn_revnum_t head;
554 const svn_delta_editor_t *editor;
555 void *editor_baton;
556 struct operation root;
557 svn_error_t *err;
558 apr_hash_t *config;
559 int i;
561 SVN_ERR(svn_config_get_config(&config, config_dir, pool));
562 SVN_ERR(svn_ra_open(&session, anchor,
563 ra_callbacks(username, password, non_interactive, pool),
564 NULL, config, pool));
566 SVN_ERR(svn_ra_get_latest_revnum(session, &head, pool));
567 if (SVN_IS_VALID_REVNUM(base_revision))
569 if (base_revision > head)
570 return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL,
571 "No such revision %ld (youngest is %ld)",
572 base_revision, head);
573 head = base_revision;
576 root.children = apr_hash_make(pool);
577 root.operation = OP_OPEN;
578 for (i = 0; i < actions->nelts; ++i)
580 struct action *action = APR_ARRAY_IDX(actions, i, struct action *);
581 switch (action->action)
583 const char *path1, *path2;
584 case ACTION_MV:
585 path1 = subtract_anchor(anchor, action->path[0], pool);
586 path2 = subtract_anchor(anchor, action->path[1], pool);
587 SVN_ERR(build(ACTION_RM, path1, NULL,
588 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
589 session, &root, pool));
590 SVN_ERR(build(ACTION_CP, path2, action->path[0],
591 head, NULL, NULL, NULL, head, anchor,
592 session, &root, pool));
593 break;
594 case ACTION_CP:
595 path1 = subtract_anchor(anchor, action->path[0], pool);
596 path2 = subtract_anchor(anchor, action->path[1], pool);
597 if (action->rev == SVN_INVALID_REVNUM)
598 action->rev = head;
599 SVN_ERR(build(ACTION_CP, path2, action->path[0],
600 action->rev, NULL, NULL, NULL, head, anchor,
601 session, &root, pool));
602 break;
603 case ACTION_RM:
604 path1 = subtract_anchor(anchor, action->path[0], pool);
605 SVN_ERR(build(ACTION_RM, path1, NULL,
606 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
607 session, &root, pool));
608 break;
609 case ACTION_MKDIR:
610 path1 = subtract_anchor(anchor, action->path[0], pool);
611 SVN_ERR(build(ACTION_MKDIR, path1, action->path[0],
612 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
613 session, &root, pool));
614 break;
615 case ACTION_PUT:
616 path1 = subtract_anchor(anchor, action->path[0], pool);
617 SVN_ERR(build(ACTION_PUT, path1, action->path[0],
618 SVN_INVALID_REVNUM, NULL, NULL, action->path[1],
619 head, anchor, session, &root, pool));
620 break;
621 case ACTION_PROPSET:
622 case ACTION_PROPDEL:
623 path1 = subtract_anchor(anchor, action->path[0], pool);
624 SVN_ERR(build(action->action, path1, action->path[0],
625 SVN_INVALID_REVNUM,
626 action->prop_name, action->prop_value,
627 NULL, head, anchor, session, &root, pool));
628 break;
632 SVN_ERR(svn_ra_get_commit_editor(session, &editor, &editor_baton, message,
633 commit_callback, pool, NULL, FALSE, pool));
635 SVN_ERR(editor->open_root(editor_baton, head, pool, &root.baton));
636 err = drive(&root, head, editor, pool);
637 if (!err)
638 err = editor->close_edit(editor_baton, pool);
639 if (err)
640 svn_error_clear(editor->abort_edit(editor_baton, pool));
642 return err;
645 static void
646 usage(apr_pool_t *pool, int exit_val)
648 FILE *stream = exit_val == EXIT_SUCCESS ? stdout : stderr;
649 const char msg[] =
650 "Multiple URL Command Client (for Subversion)\n"
651 "\nUsage: svnmucc [OPTION]... [ACTION]...\n"
652 "\nActions:\n"
653 " cp REV URL1 URL2 copy URL1@REV to URL2\n"
654 " mkdir URL create new directory URL\n"
655 " mv URL1 URL2 move URL1 to URL2\n"
656 " rm URL delete URL\n"
657 " put SRC-FILE URL add or modify file URL with contents copied\n"
658 " from SRC-FILE\n"
659 " propset NAME VAL URL Set property NAME on URL to value VAL\n"
660 " propdel NAME URL Delete property NAME from URL\n"
661 "\nOptions:\n"
662 " -h, --help display this text\n"
663 " -m, --message ARG use ARG as a log message\n"
664 " -F, --file ARG read log message from file ARG\n"
665 " -u, --username ARG commit the changes as username ARG\n"
666 " -p, --password ARG use ARG as the password\n"
667 " -U, --root-url ARG interpret all action URLs are relative to ARG\n"
668 " -r, --revision ARG use revision ARG as baseline for changes\n"
669 " -n, --non-interactive don't prompt the user about anything\n"
670 " -X, --extra-args ARG append arguments from file ARG (one per line;\n"
671 " use \"-\" to read from standard input)\n"
672 " --config-dir ARG use ARG to override the config directory\n";
673 svn_error_clear(svn_cmdline_fputs(msg, stream, pool));
674 apr_pool_destroy(pool);
675 exit(exit_val);
678 static void
679 insufficient(apr_pool_t *pool)
681 handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
682 "insufficient arguments"),
683 pool);
687 main(int argc, const char **argv)
689 apr_pool_t *pool = init("svnmucc");
690 apr_array_header_t *actions = apr_array_make(pool, 1,
691 sizeof(struct action *));
692 const char *anchor = NULL;
693 svn_error_t *err = SVN_NO_ERROR;
694 apr_getopt_t *getopt;
695 enum {
696 config_dir_opt = SVN_OPT_FIRST_LONGOPT_ID
698 const apr_getopt_option_t options[] = {
699 {"message", 'm', 1, ""},
700 {"file", 'F', 1, ""},
701 {"username", 'u', 1, ""},
702 {"password", 'p', 1, ""},
703 {"root-url", 'U', 1, ""},
704 {"revision", 'r', 1, ""},
705 {"extra-args", 'X', 1, ""},
706 {"help", 'h', 0, ""},
707 {"non-interactive", 'n', 0, ""},
708 {"config-dir", config_dir_opt, 1, ""},
709 {NULL, 0, 0, NULL}
711 const char *message = "committed using svnmucc";
712 const char *username = NULL, *password = NULL;
713 const char *root_url = NULL, *extra_args_file = NULL;
714 const char *config_dir = NULL;
715 svn_boolean_t non_interactive = FALSE;
716 svn_revnum_t base_revision = SVN_INVALID_REVNUM;
717 apr_array_header_t *action_args;
718 int i;
720 apr_getopt_init(&getopt, pool, argc, argv);
721 getopt->interleave = 1;
722 while (1)
724 int opt;
725 const char *arg;
726 apr_status_t status = apr_getopt_long(getopt, options, &opt, &arg);
727 if (APR_STATUS_IS_EOF(status))
728 break;
729 if (status != APR_SUCCESS)
730 handle_error(svn_error_wrap_apr(status, "getopt failure"), pool);
731 switch(opt)
733 case 'm':
734 err = svn_utf_cstring_to_utf8(&message, arg, pool);
735 if (err)
736 handle_error(err, pool);
737 break;
738 case 'F':
740 const char *arg_utf8;
741 svn_stringbuf_t *contents;
742 err = svn_utf_cstring_to_utf8(&arg_utf8, arg, pool);
743 if (! err)
744 err = svn_stringbuf_from_file(&contents, arg, pool);
745 if (! err)
746 err = svn_utf_cstring_to_utf8(&message, contents->data, pool);
747 if (err)
748 handle_error(err, pool);
750 break;
751 case 'u':
752 username = apr_pstrdup(pool, arg);
753 break;
754 case 'p':
755 password = apr_pstrdup(pool, arg);
756 break;
757 case 'U':
758 err = svn_utf_cstring_to_utf8(&root_url, arg, pool);
759 if (err)
760 handle_error(err, pool);
761 root_url = svn_path_canonicalize(root_url, pool);
762 if (! svn_path_is_url(root_url))
763 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
764 "'%s' is not a URL\n", root_url),
765 pool);
766 break;
767 case 'r':
769 char *digits_end = NULL;
770 base_revision = strtol(arg, &digits_end, 10);
771 if ((! SVN_IS_VALID_REVNUM(base_revision))
772 || (! digits_end)
773 || *digits_end)
774 handle_error(svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR,
775 NULL, "Invalid revision number"),
776 pool);
778 break;
779 case 'X':
780 extra_args_file = apr_pstrdup(pool, arg);
781 break;
782 case 'n':
783 non_interactive = TRUE;
784 break;
785 case config_dir_opt:
786 err = svn_utf_cstring_to_utf8(&config_dir, arg, pool);
787 if (err)
788 handle_error(err, pool);
789 break;
790 case 'h':
791 usage(pool, EXIT_SUCCESS);
795 /* Copy the rest of our command-line arguments to an array,
796 UTF-8-ing them along the way. */
797 action_args = apr_array_make(pool, getopt->argc, sizeof(const char *));
798 while (getopt->ind < getopt->argc)
800 const char *arg = getopt->argv[getopt->ind++];
801 if ((err = svn_utf_cstring_to_utf8(&(APR_ARRAY_PUSH(action_args,
802 const char *)),
803 arg, pool)))
804 handle_error(err, pool);
807 /* If there are extra arguments in a supplementary file, tack those
808 on, too (again, in UTF8 form). */
809 if (extra_args_file)
811 const char *extra_args_file_utf8;
812 svn_stringbuf_t *contents, *contents_utf8;
814 err = svn_utf_cstring_to_utf8(&extra_args_file_utf8,
815 extra_args_file, pool);
816 if (! err)
817 err = svn_stringbuf_from_file2(&contents, extra_args_file_utf8, pool);
818 if (! err)
819 err = svn_utf_stringbuf_to_utf8(&contents_utf8, contents, pool);
820 if (err)
821 handle_error(err, pool);
822 svn_cstring_split_append(action_args, contents_utf8->data, "\n\r",
823 FALSE, pool);
826 /* Now, we iterate over the combined set of arguments -- our actions. */
827 for (i = 0; i < action_args->nelts; )
829 int j, num_url_args;
830 const char *action_string = APR_ARRAY_IDX(action_args, i, const char *);
831 struct action *action = apr_palloc(pool, sizeof(*action));
833 /* First, parse the action. */
834 if (! strcmp(action_string, "mv"))
835 action->action = ACTION_MV;
836 else if (! strcmp(action_string, "cp"))
837 action->action = ACTION_CP;
838 else if (! strcmp(action_string, "mkdir"))
839 action->action = ACTION_MKDIR;
840 else if (! strcmp(action_string, "rm"))
841 action->action = ACTION_RM;
842 else if (! strcmp(action_string, "put"))
843 action->action = ACTION_PUT;
844 else if (! strcmp(action_string, "propset"))
845 action->action = ACTION_PROPSET;
846 else if (! strcmp(action_string, "propdel"))
847 action->action = ACTION_PROPDEL;
848 else
849 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
850 "'%s' is not an action\n",
851 action_string), pool);
852 if (++i == action_args->nelts)
853 insufficient(pool);
855 /* For copies, there should be a revision number next. */
856 if (action->action == ACTION_CP)
858 const char *rev_str = APR_ARRAY_IDX(action_args, i, const char *);
859 if (strcmp(rev_str, "head") == 0)
860 action->rev = SVN_INVALID_REVNUM;
861 else if (strcmp(rev_str, "HEAD") == 0)
862 action->rev = SVN_INVALID_REVNUM;
863 else
865 char *end;
866 action->rev = strtol(rev_str, &end, 0);
867 if (*end)
868 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
869 "'%s' is not a revision\n",
870 rev_str), pool);
872 if (++i == action_args->nelts)
873 insufficient(pool);
875 else
877 action->rev = SVN_INVALID_REVNUM;
880 /* For puts, there should be a local file next. */
881 if (action->action == ACTION_PUT)
883 action->path[1] = svn_path_canonicalize(APR_ARRAY_IDX(action_args,
885 const char *),
886 pool);
887 if (++i == action_args->nelts)
888 insufficient(pool);
891 /* For propset and propdel, a property name (and maybe value)
892 comes next. */
893 if ((action->action == ACTION_PROPSET)
894 || (action->action == ACTION_PROPDEL))
896 action->prop_name = APR_ARRAY_IDX(action_args, i, const char *);
897 if (++i == action_args->nelts)
898 insufficient(pool);
900 if (action->action == ACTION_PROPDEL)
902 action->prop_value = NULL;
904 else
906 action->prop_value = APR_ARRAY_IDX(action_args, i, const char *);
907 if (++i == action_args->nelts)
908 insufficient(pool);
912 /* How many URLs does this action expect? */
913 if (action->action == ACTION_RM
914 || action->action == ACTION_MKDIR
915 || action->action == ACTION_PUT
916 || action->action == ACTION_PROPSET
917 || action->action == ACTION_PROPDEL)
918 num_url_args = 1;
919 else
920 num_url_args = 2;
922 /* Parse the required number of URLs. */
923 for (j = 0; j < num_url_args; ++j)
925 const char *url = APR_ARRAY_IDX(action_args, i, const char *);
927 /* If there's a root URL, we expect this to be a path
928 relative to that URL. Otherwise, it should be a full URL. */
929 if (root_url)
930 url = svn_path_join(root_url, url, pool);
931 else if (! svn_path_is_url(url))
932 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
933 "'%s' is not a URL\n", url), pool);
934 url = svn_path_uri_from_iri(url, pool);
935 url = svn_path_uri_autoescape(url, pool);
936 url = svn_path_canonicalize(url, pool);
937 action->path[j] = url;
939 /* The cp source could be the anchor, but the other URLs should be
940 children of the anchor. */
941 if (! (action->action == ACTION_CP && j == 0))
942 url = svn_path_dirname(url, pool);
943 if (! anchor)
944 anchor = url;
945 else
946 anchor = svn_path_get_longest_ancestor(anchor, url, pool);
948 if ((++i == action_args->nelts) && (j >= num_url_args))
949 insufficient(pool);
951 APR_ARRAY_PUSH(actions, struct action *) = action;
954 if (! actions->nelts)
955 usage(pool, EXIT_FAILURE);
957 if ((err = execute(actions, anchor, message, username, password,
958 config_dir, non_interactive, base_revision, pool)))
959 handle_error(err, pool);
961 svn_pool_destroy(pool);
962 return EXIT_SUCCESS;