| /* Multiple URL Command Client |
| |
| Combine a list of mv, cp and rm commands on URLs into a single commit. |
| |
| Copyright 2005 Philip Martin <philip@codematters.co.uk> |
| |
| Licenced under the same terms as Subversion. |
| |
| How it works: the command line arguments are parsed into an array of |
| action structures. The action structures are interpreted to build a |
| tree of operation structures. The tree of operation structures is |
| used to drive an RA commit editor to produce a single commit. */ |
| |
| #include "svn_cmdline.h" |
| #include "svn_client.h" |
| #include "svn_pools.h" |
| #include "svn_error.h" |
| #include "svn_path.h" |
| #include "svn_ra.h" |
| #include <apr_lib.h> |
| #include <stdio.h> |
| #include <string.h> |
| |
| static void handle_error(svn_error_t *err, apr_pool_t *pool) |
| { |
| if (err) |
| svn_handle_error2(err, stderr, FALSE, "mucc: "); |
| svn_error_clear(err); |
| if (pool) |
| svn_pool_destroy(pool); |
| exit(EXIT_FAILURE); |
| } |
| |
| static apr_pool_t * |
| init(const char *application) |
| { |
| apr_allocator_t *allocator; |
| apr_pool_t *pool; |
| svn_error_t *err; |
| const svn_version_checklist_t checklist[] = { |
| {"svn_client", svn_client_version}, |
| {"svn_subr", svn_subr_version}, |
| {"svn_ra", svn_ra_version}, |
| {NULL, NULL} |
| }; |
| |
| SVN_VERSION_DEFINE(my_version); |
| |
| if (svn_cmdline_init(application, stderr) || apr_allocator_create(&allocator)) |
| exit(EXIT_FAILURE); |
| |
| err = svn_ver_check_list(&my_version, checklist); |
| if (err) |
| handle_error(err, NULL); |
| |
| apr_allocator_max_free_set(allocator, SVN_ALLOCATOR_RECOMMENDED_MAX_FREE); |
| pool = svn_pool_create_ex(NULL, allocator); |
| apr_allocator_owner_set(allocator, pool); |
| |
| return pool; |
| } |
| |
| static svn_error_t * |
| prompt_for_creds(const char **username, |
| const char **password, |
| const char *realm, |
| apr_pool_t *pool) |
| { |
| char buffer[100]; |
| svn_boolean_t prompt_with_username; |
| |
| if (realm) |
| SVN_ERR(svn_cmdline_printf(pool, "Authentication realm: %s\n", realm)); |
| |
| if (! *username) |
| { |
| SVN_ERR(svn_cmdline_printf(pool, "Username: ")); |
| if (! fgets(buffer, sizeof(buffer), stdin)) |
| return svn_error_createf(0, NULL, "failed to get username"); |
| if (strlen(buffer) > 0 && buffer[strlen(buffer)-1] == '\n') |
| buffer[strlen(buffer)-1] = '\0'; |
| *username = buffer; |
| prompt_with_username = FALSE; |
| } |
| else |
| prompt_with_username = TRUE; |
| *username = apr_pstrdup(pool, *username); |
| |
| if (password) |
| { |
| apr_size_t sz = sizeof(buffer); |
| const char *prompt = (prompt_with_username |
| ? apr_psprintf(pool, "Password for %s: ", *username) |
| : "Password: "); |
| apr_status_t status = apr_password_get(prompt, buffer, &sz); |
| if (status) |
| return svn_error_wrap_apr(status, "failed to get password"); |
| *password = apr_pstrdup(pool, buffer); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| simple_prompt(svn_auth_cred_simple_t **cred, |
| void *baton, |
| const char *realm, |
| const char *username, |
| svn_boolean_t may_save, |
| apr_pool_t *pool) |
| { |
| const char *password; |
| SVN_ERR(prompt_for_creds(&username, &password, realm, pool)); |
| *cred = apr_palloc(pool, sizeof(**cred)); |
| (*cred)->username = username; |
| (*cred)->password = password; |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| username_prompt(svn_auth_cred_username_t **cred, |
| void *baton, |
| const char *realm, |
| svn_boolean_t may_save, |
| apr_pool_t *pool) |
| { |
| const char *username = NULL; |
| SVN_ERR(prompt_for_creds(&username, NULL, realm, pool)); |
| *cred = apr_palloc(pool, sizeof(**cred)); |
| (*cred)->username = username; |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_ra_callbacks_t * |
| ra_callbacks(apr_pool_t *pool) |
| { |
| apr_array_header_t *providers; |
| svn_ra_callbacks_t *callbacks; |
| svn_auth_provider_object_t *provider; |
| |
| providers = apr_array_make(pool, 2, sizeof(svn_auth_provider_object_t *)); |
| svn_client_get_simple_prompt_provider(&provider, simple_prompt, NULL, 2, |
| pool); |
| APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider; |
| svn_client_get_username_prompt_provider(&provider, username_prompt, NULL, 2, |
| pool); |
| APR_ARRAY_PUSH(providers, svn_auth_provider_object_t *) = provider; |
| |
| callbacks = apr_palloc(pool, sizeof(*callbacks)); |
| svn_auth_open(&callbacks->auth_baton, providers, pool); |
| callbacks->open_tmp_file = NULL; |
| callbacks->get_wc_prop = NULL; |
| callbacks->set_wc_prop = NULL; |
| callbacks->push_wc_prop = NULL; |
| callbacks->invalidate_wc_props = NULL; |
| |
| return callbacks; |
| } |
| |
| static svn_error_t * |
| commit_callback(svn_revnum_t revision, |
| const char *date, |
| const char *author, |
| void *baton) |
| { |
| apr_pool_t *pool = baton; |
| |
| SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n", |
| revision, author ? author : "(no author)", date)); |
| return SVN_NO_ERROR; |
| } |
| |
| struct operation { |
| enum { |
| OP_OPEN, |
| OP_DELETE, |
| OP_ADD, |
| OP_REPLACE |
| } operation; |
| svn_node_kind_t kind; /* to copy, valid for add and replace */ |
| svn_revnum_t rev; /* to copy, valid for add and replace */ |
| const char *url; /* to copy, valid for add and replace */ |
| apr_hash_t *children; /* key: const char *path, value: struct operation * */ |
| void *baton; /* as returned by the commit editor */ |
| }; |
| |
| static svn_error_t * |
| drive(struct operation *operation, |
| svn_revnum_t head, |
| const svn_delta_editor_t *editor, |
| apr_pool_t *pool) |
| { |
| apr_pool_t *subpool = svn_pool_create(pool); |
| apr_hash_index_t *hi; |
| for (hi = apr_hash_first(pool, operation->children); |
| hi; hi = apr_hash_next(hi)) |
| { |
| const void *key; |
| void *val; |
| struct operation *child; |
| |
| svn_pool_clear(subpool); |
| apr_hash_this(hi, &key, NULL, &val); |
| child = val; |
| |
| if (child->operation == OP_DELETE || child->operation == OP_REPLACE) |
| { |
| SVN_ERR(editor->delete_entry(key, head, operation->baton, subpool)); |
| } |
| if (child->operation == OP_OPEN) |
| { |
| SVN_ERR(editor->open_directory(key, operation->baton, head, subpool, |
| &child->baton)); |
| } |
| if (child->operation == OP_ADD || child->operation == OP_REPLACE) |
| { |
| if (child->kind == svn_node_dir) |
| { |
| SVN_ERR(editor->add_directory(key, operation->baton, |
| child->url, child->rev, |
| subpool, &child->baton)); |
| } |
| else |
| { |
| void *file_baton; |
| SVN_ERR(editor->add_file(key, operation->baton, |
| child->url, child->rev, |
| subpool, &file_baton)); |
| SVN_ERR(editor->close_file(file_baton, NULL, subpool)); |
| } |
| } |
| if (child->operation == OP_OPEN |
| || ((child->operation == OP_ADD || child->operation == OP_REPLACE) |
| && child->kind == svn_node_dir)) |
| { |
| SVN_ERR(drive(child, head, editor, subpool)); |
| SVN_ERR(editor->close_directory(child->baton, subpool)); |
| } |
| } |
| svn_pool_destroy(subpool); |
| return SVN_NO_ERROR; |
| } |
| |
| static struct operation * |
| get_operation(const char *path, |
| struct operation *operation, |
| apr_pool_t *pool) |
| { |
| struct operation *child = apr_hash_get(operation->children, path, |
| APR_HASH_KEY_STRING); |
| if (! child) |
| { |
| child = apr_palloc(pool, sizeof(*child)); |
| child->children = apr_hash_make(pool); |
| child->operation = OP_OPEN; |
| apr_hash_set(operation->children, path, APR_HASH_KEY_STRING, child); |
| } |
| return child; |
| } |
| |
| static const char * |
| subtract_anchor(const char *anchor, const char *url, apr_pool_t *pool) |
| { |
| if (! strcmp(url, anchor)) |
| return ""; |
| else |
| return svn_path_uri_decode(svn_path_is_child(anchor, url, pool), pool); |
| } |
| |
| /* Add PATH to the operations tree rooted at OPERATION, creating any |
| intermediate nodes that are required. If URL is null then PATH will be |
| deleted, otherwise URL@REV is the source to be copied to create PATH. |
| Node type information is obtained for any copy source (to determine |
| whether to create a file or directory) and for any deleted path (to |
| ensure it exists since svn_delta_editor_t->delete_entry doesn't return |
| an error on non-existent nodes). */ |
| static svn_error_t * |
| build(const char *path, |
| const char *url, |
| svn_revnum_t rev, |
| svn_revnum_t head, |
| const char *anchor, |
| svn_ra_session_t *session, |
| struct operation *operation, |
| apr_pool_t *pool) |
| { |
| apr_array_header_t *path_bits = svn_path_decompose(path, pool); |
| const char *path_so_far = ""; |
| const char *copy_src = NULL; |
| svn_revnum_t copy_rev = SVN_INVALID_REVNUM; |
| int i; |
| |
| for (i = 0; i < path_bits->nelts; ++i) |
| { |
| const char *path_bit = APR_ARRAY_IDX(path_bits, i, const char *); |
| path_so_far = svn_path_join(path_so_far, path_bit, pool); |
| operation = get_operation(path_so_far, operation, pool); |
| if (! url) |
| { |
| /* Delete can operate on a copy, track it back to the source */ |
| if (operation->operation == OP_REPLACE |
| || operation->operation == OP_ADD) |
| { |
| copy_src = subtract_anchor(anchor, operation->url, pool); |
| copy_rev = operation->rev; |
| } |
| else if (copy_src) |
| copy_src = svn_path_join(copy_src, path_bit, pool); |
| } |
| } |
| |
| if (operation->operation != OP_OPEN && operation->operation != OP_DELETE) |
| return svn_error_createf(SVN_ERR_BAD_URL, NULL, |
| "unsupported multiple operations on '%s'", path); |
| |
| if (! url) |
| { |
| operation->operation = OP_DELETE; |
| SVN_ERR(svn_ra_check_path(session, |
| copy_src ? copy_src : path, |
| copy_src ? copy_rev : head, |
| &operation->kind, pool)); |
| if (operation->kind == svn_node_none) |
| { |
| if (copy_src && strcmp(path, copy_src)) |
| return svn_error_createf(SVN_ERR_BAD_URL, NULL, |
| "'%s' (from '%s:%ld') not found", |
| path, copy_src, copy_rev); |
| else |
| return svn_error_createf(SVN_ERR_BAD_URL, NULL, "'%s' not found", |
| path); |
| } |
| } |
| else |
| { |
| operation->operation |
| = operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD; |
| SVN_ERR(svn_ra_check_path(session, subtract_anchor(anchor, url, pool), |
| rev, &operation->kind, pool)); |
| if (operation->kind == svn_node_none) |
| return svn_error_createf(SVN_ERR_BAD_URL, NULL, "'%s' not found", url); |
| operation->url = url; |
| operation->rev = rev; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| struct action { |
| enum { |
| ACTION_MV, |
| ACTION_CP, |
| ACTION_RM |
| } action; |
| svn_revnum_t rev; /* of url[0] for cp */ |
| const char *url[2]; |
| }; |
| |
| static svn_error_t * |
| execute(const apr_array_header_t *actions, |
| const char *anchor, |
| const char *message, |
| apr_pool_t *pool) |
| { |
| svn_ra_session_t *session; |
| svn_revnum_t head; |
| const svn_delta_editor_t *editor; |
| void *editor_baton; |
| struct operation root; |
| svn_error_t *err; |
| int i; |
| |
| SVN_ERR(svn_ra_open(&session, anchor, ra_callbacks(pool), NULL, NULL, pool)); |
| |
| SVN_ERR(svn_ra_get_latest_revnum(session, &head, pool)); |
| |
| root.children = apr_hash_make(pool); |
| root.operation = OP_OPEN; |
| for (i = 0; i < actions->nelts; ++i) |
| { |
| struct action *action = APR_ARRAY_IDX(actions, i, struct action *); |
| switch (action->action) |
| { |
| const char *path1, *path2; |
| case ACTION_MV: |
| path1 = subtract_anchor(anchor, action->url[0], pool); |
| path2 = subtract_anchor(anchor, action->url[1], pool); |
| SVN_ERR(build(path2, action->url[0], head, |
| head, anchor, session, &root, pool)); |
| SVN_ERR(build(path1, NULL, SVN_INVALID_REVNUM, |
| head, anchor, session, &root, pool)); |
| break; |
| case ACTION_CP: |
| path1 = subtract_anchor(anchor, action->url[0], pool); |
| path2 = subtract_anchor(anchor, action->url[1], pool); |
| if (action->rev == SVN_INVALID_REVNUM) |
| action->rev = head; |
| SVN_ERR(build(path2, action->url[0], action->rev, |
| head, anchor, session, &root, pool)); |
| break; |
| case ACTION_RM: |
| path1 = subtract_anchor(anchor, action->url[0], pool); |
| SVN_ERR(build(path1, NULL, SVN_INVALID_REVNUM, |
| head, anchor, session, &root, pool)); |
| break; |
| } |
| } |
| |
| SVN_ERR(svn_ra_get_commit_editor(session, &editor, &editor_baton, message, |
| commit_callback, pool, NULL, FALSE, pool)); |
| |
| SVN_ERR(editor->open_root(editor_baton, head, pool, &root.baton)); |
| err = drive(&root, head, editor, pool); |
| if (!err) |
| err = editor->close_edit(editor_baton, pool); |
| if (err) |
| svn_error_clear(editor->abort_edit(editor_baton, pool)); |
| |
| return err; |
| } |
| |
| static void |
| usage(apr_pool_t *pool, int exit_val) |
| { |
| FILE *stream = exit_val == EXIT_SUCCESS ? stdout : stderr; |
| const char msg[] |
| = |
| "usage: mucc [OPTION]... [ mv URL1 URL2 | cp REV URL1 URL2 | rm URL ]...\n" |
| "options:\n" |
| " -m, --message ARG use ARG as a log message\n" |
| " -F, --file ARG read log message from file ARG\n" |
| " -h, --help display this text\n"; |
| svn_error_clear(svn_cmdline_fputs(msg, stream, pool)); |
| apr_pool_destroy(pool); |
| exit(exit_val); |
| } |
| |
| static void |
| insufficient(apr_pool_t *pool) |
| { |
| handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, |
| "insufficient arguments"), |
| pool); |
| } |
| |
| int |
| main(int argc, const char **argv) |
| { |
| apr_pool_t *pool = init("mucc"); |
| apr_array_header_t *actions = apr_array_make(pool, 1, sizeof(struct action*)); |
| const char *anchor = NULL; |
| svn_error_t *err; |
| apr_getopt_t *getopt; |
| const apr_getopt_option_t options[] = { |
| {"message", 'm', 1, ""}, |
| {"file", 'F', 1, ""}, |
| {"help", 'h', 0, ""}, |
| {NULL, 0, 0, NULL} |
| }; |
| const char *message = "committed using mucc"; |
| |
| apr_getopt_init(&getopt, pool, argc, argv); |
| getopt->interleave = 1; |
| while (1) |
| { |
| int opt; |
| const char *arg; |
| apr_status_t status = apr_getopt_long(getopt, options, &opt, &arg); |
| if (APR_STATUS_IS_EOF(status)) |
| break; |
| if (status != APR_SUCCESS) |
| handle_error(svn_error_wrap_apr(status, "getopt failure"), pool); |
| switch(opt) |
| { |
| case 'm': |
| err = svn_utf_cstring_to_utf8(&message, arg, pool); |
| if (err) |
| handle_error(err, pool); |
| break; |
| case 'F': |
| { |
| const char *arg_utf8; |
| svn_stringbuf_t *contents; |
| err = svn_utf_cstring_to_utf8(&arg_utf8, arg, pool); |
| if (! err) |
| err = svn_stringbuf_from_file(&contents, arg, pool); |
| if (! err) |
| err = svn_utf_cstring_to_utf8(&message, contents->data, pool); |
| if (err) |
| handle_error(err, pool); |
| } |
| break; |
| case 'h': |
| usage(pool, EXIT_SUCCESS); |
| } |
| } |
| while (getopt->ind < getopt->argc) |
| { |
| int j; |
| struct action *action = apr_palloc(pool, sizeof(*action)); |
| |
| if (! strcmp(getopt->argv[getopt->ind], "mv")) |
| action->action = ACTION_MV; |
| else if (! strcmp(getopt->argv[getopt->ind], "cp")) |
| action->action = ACTION_CP; |
| else if (! strcmp(getopt->argv[getopt->ind], "rm")) |
| action->action = ACTION_RM; |
| else |
| handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, |
| "'%s' is not an action\n", |
| getopt->argv[getopt->ind]), |
| pool); |
| if (++getopt->ind == getopt->argc) |
| insufficient(pool); |
| |
| if (action->action == ACTION_CP) |
| { |
| if (! strcmp(getopt->argv[getopt->ind], "head")) |
| action->rev = SVN_INVALID_REVNUM; |
| else |
| { |
| char *end; |
| action->rev = strtol(getopt->argv[getopt->ind], &end, 0); |
| if (*end) |
| handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, |
| NULL, "'%s' is not a revision\n", |
| getopt->argv[getopt->ind]), |
| pool); |
| } |
| if (++getopt->ind == getopt->argc) |
| insufficient(pool); |
| } |
| else |
| action->rev = SVN_INVALID_REVNUM; |
| |
| for (j = 0; j < (action->action == ACTION_RM ? 1 : 2); ++j) |
| { |
| const char *url = getopt->argv[getopt->ind]; |
| err = svn_utf_cstring_to_utf8(&url, url, pool); |
| if (err) |
| handle_error(err, pool); |
| if (! svn_path_is_url(url)) |
| handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, |
| "'%s' is not an URL\n", |
| getopt->argv[getopt->ind]), |
| pool); |
| url = svn_path_uri_from_iri(url, pool); |
| url = svn_path_uri_autoescape(url, pool); |
| url = svn_path_canonicalize(url, pool); |
| action->url[j] = url; |
| |
| /* The cp source could be the anchor, but the other URLs should be |
| children of the anchor. */ |
| if (! (action->action == ACTION_CP && j == 0)) |
| url = svn_path_dirname(url, pool); |
| if (! anchor) |
| anchor = url; |
| else |
| anchor = svn_path_get_longest_ancestor(anchor, url, pool); |
| |
| if (++getopt->ind == getopt->argc |
| && ! (j == 1 || action->action == ACTION_RM)) |
| insufficient(pool); |
| } |
| APR_ARRAY_PUSH(actions, struct action *) = action; |
| } |
| |
| if (! actions->nelts) |
| usage(pool, EXIT_FAILURE); |
| |
| err = execute(actions, anchor, message, pool); |
| if (err) |
| handle_error(err, pool); |
| |
| svn_pool_destroy(pool); |
| return EXIT_SUCCESS; |
| } |