/* 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;
}
syntax highlighted by Code2HTML, v. 0.9.1