summaryrefslogtreecommitdiff
path: root/subversion/svnmucc/svnmucc.c
diff options
context:
space:
mode:
Diffstat (limited to 'subversion/svnmucc/svnmucc.c')
-rw-r--r--subversion/svnmucc/svnmucc.c1468
1 files changed, 1468 insertions, 0 deletions
diff --git a/subversion/svnmucc/svnmucc.c b/subversion/svnmucc/svnmucc.c
new file mode 100644
index 0000000..d53f18f
--- /dev/null
+++ b/subversion/svnmucc/svnmucc.c
@@ -0,0 +1,1468 @@
+/*
+ * svnmucc.c: Subversion Multiple URL Client
+ *
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ */
+
+/* Multiple URL Command Client
+
+ Combine a list of mv, cp and rm commands on URLs into a single commit.
+
+ 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.
+
+ To build this client, type 'make svnmucc' from the root of your
+ Subversion source directory.
+*/
+
+#include <stdio.h>
+#include <string.h>
+
+#include <apr_lib.h>
+
+#include "svn_hash.h"
+#include "svn_client.h"
+#include "svn_cmdline.h"
+#include "svn_config.h"
+#include "svn_error.h"
+#include "svn_path.h"
+#include "svn_pools.h"
+#include "svn_props.h"
+#include "svn_ra.h"
+#include "svn_string.h"
+#include "svn_subst.h"
+#include "svn_utf.h"
+#include "svn_version.h"
+
+#include "private/svn_cmdline_private.h"
+#include "private/svn_ra_private.h"
+#include "private/svn_string_private.h"
+#include "private/svn_subr_private.h"
+
+#include "svn_private_config.h"
+
+static void handle_error(svn_error_t *err, apr_pool_t *pool)
+{
+ if (err)
+ svn_handle_error2(err, stderr, FALSE, "svnmucc: ");
+ svn_error_clear(err);
+ if (pool)
+ svn_pool_destroy(pool);
+ exit(EXIT_FAILURE);
+}
+
+static apr_pool_t *
+init(const char *application)
+{
+ 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))
+ exit(EXIT_FAILURE);
+
+ err = svn_ver_check_list2(&my_version, checklist, svn_ver_equal);
+ if (err)
+ handle_error(err, NULL);
+
+ return apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
+}
+
+static svn_error_t *
+open_tmp_file(apr_file_t **fp,
+ void *callback_baton,
+ apr_pool_t *pool)
+{
+ /* Open a unique file; use APR_DELONCLOSE. */
+ return svn_io_open_unique_file3(fp, NULL, NULL, svn_io_file_del_on_close,
+ pool, pool);
+}
+
+static svn_error_t *
+create_ra_callbacks(svn_ra_callbacks2_t **callbacks,
+ const char *username,
+ const char *password,
+ const char *config_dir,
+ svn_config_t *cfg_config,
+ svn_boolean_t non_interactive,
+ svn_boolean_t trust_server_cert,
+ svn_boolean_t no_auth_cache,
+ apr_pool_t *pool)
+{
+ SVN_ERR(svn_ra_create_callbacks(callbacks, pool));
+
+ SVN_ERR(svn_cmdline_create_auth_baton(&(*callbacks)->auth_baton,
+ non_interactive,
+ username, password, config_dir,
+ no_auth_cache,
+ trust_server_cert,
+ cfg_config, NULL, NULL, pool));
+
+ (*callbacks)->open_tmp_file = open_tmp_file;
+
+ return SVN_NO_ERROR;
+}
+
+
+
+static svn_error_t *
+commit_callback(const svn_commit_info_t *commit_info,
+ void *baton,
+ apr_pool_t *pool)
+{
+ SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n",
+ commit_info->revision,
+ (commit_info->author
+ ? commit_info->author : "(no author)"),
+ commit_info->date));
+ return SVN_NO_ERROR;
+}
+
+typedef enum action_code_t {
+ ACTION_MV,
+ ACTION_MKDIR,
+ ACTION_CP,
+ ACTION_PROPSET,
+ ACTION_PROPSETF,
+ ACTION_PROPDEL,
+ ACTION_PUT,
+ ACTION_RM
+} action_code_t;
+
+struct operation {
+ enum {
+ OP_OPEN,
+ OP_DELETE,
+ OP_ADD,
+ OP_REPLACE,
+ OP_PROPSET /* only for files for which no other operation is
+ occuring; directories are OP_OPEN with non-empty
+ props */
+ } operation;
+ svn_node_kind_t kind; /* to copy, mkdir, put or set revprops */
+ svn_revnum_t rev; /* to copy, valid for add and replace */
+ const char *url; /* to copy, valid for add and replace */
+ const char *src_file; /* for put, the source file for contents */
+ apr_hash_t *children; /* const char *path -> struct operation * */
+ apr_hash_t *prop_mods; /* const char *prop_name ->
+ const svn_string_t *prop_value */
+ apr_array_header_t *prop_dels; /* const char *prop_name deletions */
+ void *baton; /* as returned by the commit editor */
+};
+
+
+/* An iterator (for use via apr_table_do) which sets node properties.
+ REC is a pointer to a struct driver_state. */
+static svn_error_t *
+change_props(const svn_delta_editor_t *editor,
+ void *baton,
+ struct operation *child,
+ apr_pool_t *pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ if (child->prop_dels)
+ {
+ int i;
+ for (i = 0; i < child->prop_dels->nelts; i++)
+ {
+ const char *prop_name;
+
+ svn_pool_clear(iterpool);
+ prop_name = APR_ARRAY_IDX(child->prop_dels, i, const char *);
+ if (child->kind == svn_node_dir)
+ SVN_ERR(editor->change_dir_prop(baton, prop_name,
+ NULL, iterpool));
+ else
+ SVN_ERR(editor->change_file_prop(baton, prop_name,
+ NULL, iterpool));
+ }
+ }
+ if (apr_hash_count(child->prop_mods))
+ {
+ apr_hash_index_t *hi;
+ for (hi = apr_hash_first(pool, child->prop_mods);
+ hi; hi = apr_hash_next(hi))
+ {
+ const char *propname = svn__apr_hash_index_key(hi);
+ const svn_string_t *val = svn__apr_hash_index_val(hi);
+
+ svn_pool_clear(iterpool);
+ if (child->kind == svn_node_dir)
+ SVN_ERR(editor->change_dir_prop(baton, propname, val, iterpool));
+ else
+ SVN_ERR(editor->change_file_prop(baton, propname, val, iterpool));
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+
+/* Drive EDITOR to affect the change represented by OPERATION. HEAD
+ is the last-known youngest revision in the repository. */
+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 char *key = svn__apr_hash_index_key(hi);
+ struct operation *child = svn__apr_hash_index_val(hi);
+ void *file_baton = NULL;
+
+ svn_pool_clear(subpool);
+
+ /* Deletes and replacements are simple -- delete something. */
+ if (child->operation == OP_DELETE || child->operation == OP_REPLACE)
+ {
+ SVN_ERR(editor->delete_entry(key, head, operation->baton, subpool));
+ }
+ /* Opens could be for directories or files. */
+ if (child->operation == OP_OPEN || child->operation == OP_PROPSET)
+ {
+ if (child->kind == svn_node_dir)
+ {
+ SVN_ERR(editor->open_directory(key, operation->baton, head,
+ subpool, &child->baton));
+ }
+ else
+ {
+ SVN_ERR(editor->open_file(key, operation->baton, head,
+ subpool, &file_baton));
+ }
+ }
+ /* Adds and replacements could also be for directories or files. */
+ 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
+ {
+ SVN_ERR(editor->add_file(key, operation->baton, child->url,
+ child->rev, subpool, &file_baton));
+ }
+ }
+ /* If there's a source file and an open file baton, we get to
+ change textual contents. */
+ if ((child->src_file) && (file_baton))
+ {
+ svn_txdelta_window_handler_t handler;
+ void *handler_baton;
+ svn_stream_t *contents;
+
+ SVN_ERR(editor->apply_textdelta(file_baton, NULL, subpool,
+ &handler, &handler_baton));
+ if (strcmp(child->src_file, "-") != 0)
+ {
+ SVN_ERR(svn_stream_open_readonly(&contents, child->src_file,
+ pool, pool));
+ }
+ else
+ {
+ SVN_ERR(svn_stream_for_stdin(&contents, pool));
+ }
+ SVN_ERR(svn_txdelta_send_stream(contents, handler,
+ handler_baton, NULL, pool));
+ }
+ /* If we opened a file, we need to apply outstanding propmods,
+ then close it. */
+ if (file_baton)
+ {
+ if (child->kind == svn_node_file)
+ {
+ SVN_ERR(change_props(editor, file_baton, child, subpool));
+ }
+ SVN_ERR(editor->close_file(file_baton, NULL, subpool));
+ }
+ /* If we opened, added, or replaced a directory, we need to
+ recurse, apply outstanding propmods, and then close it. */
+ if ((child->kind == svn_node_dir)
+ && child->operation != OP_DELETE)
+ {
+ SVN_ERR(change_props(editor, child->baton, child, subpool));
+
+ SVN_ERR(drive(child, head, editor, subpool));
+
+ SVN_ERR(editor->close_directory(child->baton, subpool));
+ }
+ }
+ svn_pool_destroy(subpool);
+ return SVN_NO_ERROR;
+}
+
+
+/* Find the operation associated with PATH, which is a single-path
+ component representing a child of the path represented by
+ OPERATION. If no such child operation exists, create a new one of
+ type OP_OPEN. */
+static struct operation *
+get_operation(const char *path,
+ struct operation *operation,
+ apr_pool_t *pool)
+{
+ struct operation *child = svn_hash_gets(operation->children, path);
+ if (! child)
+ {
+ child = apr_pcalloc(pool, sizeof(*child));
+ child->children = apr_hash_make(pool);
+ child->operation = OP_OPEN;
+ child->rev = SVN_INVALID_REVNUM;
+ child->kind = svn_node_dir;
+ child->prop_mods = apr_hash_make(pool);
+ child->prop_dels = apr_array_make(pool, 1, sizeof(const char *));
+ svn_hash_sets(operation->children, path, child);
+ }
+ return child;
+}
+
+/* Return the portion of URL that is relative to ANCHOR (URI-decoded). */
+static const char *
+subtract_anchor(const char *anchor, const char *url, apr_pool_t *pool)
+{
+ return svn_uri_skip_ancestor(anchor, url, pool);
+}
+
+/* Add PATH to the operations tree rooted at OPERATION, creating any
+ intermediate nodes that are required. Here's what's expected for
+ each action type:
+
+ ACTION URL REV SRC-FILE PROPNAME
+ ------------ ----- ------- -------- --------
+ ACTION_MKDIR NULL invalid NULL NULL
+ ACTION_CP valid valid NULL NULL
+ ACTION_PUT NULL invalid valid NULL
+ ACTION_RM NULL invalid NULL NULL
+ ACTION_PROPSET valid invalid NULL valid
+ ACTION_PROPDEL valid invalid NULL valid
+
+ 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(action_code_t action,
+ const char *path,
+ const char *url,
+ svn_revnum_t rev,
+ const char *prop_name,
+ const svn_string_t *prop_value,
+ const char *src_file,
+ 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;
+
+ /* Look for any previous operations we've recognized for PATH. If
+ any of PATH's ancestors have not yet been traversed, we'll be
+ creating OP_OPEN operations for them as we walk down PATH's path
+ components. */
+ for (i = 0; i < path_bits->nelts; ++i)
+ {
+ const char *path_bit = APR_ARRAY_IDX(path_bits, i, const char *);
+ path_so_far = svn_relpath_join(path_so_far, path_bit, pool);
+ operation = get_operation(path_so_far, operation, pool);
+
+ /* If we cross a replace- or add-with-history, remember the
+ source of those things in case we need to lookup the node kind
+ of one of their children. And if this isn't such a copy,
+ but we've already seen one in of our parent paths, we just need
+ to extend that copy source path by our current path
+ component. */
+ if (operation->url
+ && SVN_IS_VALID_REVNUM(operation->rev)
+ && (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_relpath_join(copy_src, path_bit, pool);
+ }
+ }
+
+ /* Handle property changes. */
+ if (prop_name)
+ {
+ if (operation->operation == OP_DELETE)
+ return svn_error_createf(SVN_ERR_BAD_URL, NULL,
+ "cannot set properties on a location being"
+ " deleted ('%s')", path);
+ /* If we're not adding this thing ourselves, check for existence. */
+ if (! ((operation->operation == OP_ADD) ||
+ (operation->operation == OP_REPLACE)))
+ {
+ 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)
+ return svn_error_createf(SVN_ERR_BAD_URL, NULL,
+ "propset: '%s' not found", path);
+ else if ((operation->kind == svn_node_file)
+ && (operation->operation == OP_OPEN))
+ operation->operation = OP_PROPSET;
+ }
+ if (! prop_value)
+ APR_ARRAY_PUSH(operation->prop_dels, const char *) = prop_name;
+ else
+ svn_hash_sets(operation->prop_mods, prop_name, prop_value);
+ if (!operation->rev)
+ operation->rev = rev;
+ return SVN_NO_ERROR;
+ }
+
+ /* We won't fuss about multiple operations on the same path in the
+ following cases:
+
+ - the prior operation was, in fact, a no-op (open)
+ - the prior operation was a propset placeholder
+ - the prior operation was a deletion
+
+ Note: while the operation structure certainly supports the
+ ability to do a copy of a file followed by a put of new contents
+ for the file, we don't let that happen (yet).
+ */
+ if (operation->operation != OP_OPEN
+ && operation->operation != OP_PROPSET
+ && operation->operation != OP_DELETE)
+ return svn_error_createf(SVN_ERR_BAD_URL, NULL,
+ "unsupported multiple operations on '%s'", path);
+
+ /* For deletions, we validate that there's actually something to
+ delete. If this is a deletion of the child of a copied
+ directory, we need to remember to look in the copy source tree to
+ verify that this thing actually exists. */
+ if (action == ACTION_RM)
+ {
+ 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);
+ }
+ }
+ /* Handle copy operations (which can be adds or replacements). */
+ else if (action == ACTION_CP)
+ {
+ if (rev > head)
+ return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
+ "Copy source revision cannot be younger "
+ "than base revision");
+ operation->operation =
+ operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
+ if (operation->operation == OP_ADD)
+ {
+ /* There is a bug in the current version of mod_dav_svn
+ which incorrectly replaces existing directories.
+ Therefore we need to check if the target exists
+ and raise an error here. */
+ 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') already exists",
+ path, copy_src, copy_rev);
+ else
+ return svn_error_createf(SVN_ERR_BAD_URL, NULL,
+ "'%s' already exists", path);
+ }
+ }
+ 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",
+ subtract_anchor(anchor, url, pool));
+ operation->url = url;
+ operation->rev = rev;
+ }
+ /* Handle mkdir operations (which can be adds or replacements). */
+ else if (action == ACTION_MKDIR)
+ {
+ operation->operation =
+ operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD;
+ operation->kind = svn_node_dir;
+ }
+ /* Handle put operations (which can be adds, replacements, or opens). */
+ else if (action == ACTION_PUT)
+ {
+ if (operation->operation == OP_DELETE)
+ {
+ operation->operation = OP_REPLACE;
+ }
+ else
+ {
+ 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_file)
+ operation->operation = OP_OPEN;
+ else if (operation->kind == svn_node_none)
+ operation->operation = OP_ADD;
+ else
+ return svn_error_createf(SVN_ERR_BAD_URL, NULL,
+ "'%s' is not a file", path);
+ }
+ operation->kind = svn_node_file;
+ operation->src_file = src_file;
+ }
+ else
+ {
+ /* We shouldn't get here. */
+ SVN_ERR_MALFUNCTION();
+ }
+
+ return SVN_NO_ERROR;
+}
+
+struct action {
+ action_code_t action;
+
+ /* revision (copy-from-rev of path[0] for cp; base-rev for put) */
+ svn_revnum_t rev;
+
+ /* action path[0] path[1]
+ * ------ ------- -------
+ * mv source target
+ * mkdir target (null)
+ * cp source target
+ * put target source
+ * rm target (null)
+ * propset target (null)
+ */
+ const char *path[2];
+
+ /* property name/value */
+ const char *prop_name;
+ const svn_string_t *prop_value;
+};
+
+struct fetch_baton
+{
+ svn_ra_session_t *session;
+ svn_revnum_t head;
+};
+
+static svn_error_t *
+fetch_base_func(const char **filename,
+ void *baton,
+ const char *path,
+ svn_revnum_t base_revision,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct fetch_baton *fb = baton;
+ svn_stream_t *fstream;
+ svn_error_t *err;
+
+ if (! SVN_IS_VALID_REVNUM(base_revision))
+ base_revision = fb->head;
+
+ SVN_ERR(svn_stream_open_unique(&fstream, filename, NULL,
+ svn_io_file_del_on_pool_cleanup,
+ result_pool, scratch_pool));
+
+ err = svn_ra_get_file(fb->session, path, base_revision, fstream, NULL, NULL,
+ scratch_pool);
+ if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ SVN_ERR(svn_stream_close(fstream));
+
+ *filename = NULL;
+ return SVN_NO_ERROR;
+ }
+ else if (err)
+ return svn_error_trace(err);
+
+ SVN_ERR(svn_stream_close(fstream));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+fetch_props_func(apr_hash_t **props,
+ void *baton,
+ const char *path,
+ svn_revnum_t base_revision,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct fetch_baton *fb = baton;
+ svn_node_kind_t node_kind;
+
+ if (! SVN_IS_VALID_REVNUM(base_revision))
+ base_revision = fb->head;
+
+ SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, &node_kind,
+ scratch_pool));
+
+ if (node_kind == svn_node_file)
+ {
+ SVN_ERR(svn_ra_get_file(fb->session, path, base_revision, NULL, NULL,
+ props, result_pool));
+ }
+ else if (node_kind == svn_node_dir)
+ {
+ apr_array_header_t *tmp_props;
+
+ SVN_ERR(svn_ra_get_dir2(fb->session, NULL, NULL, props, path,
+ base_revision, 0 /* Dirent fields */,
+ result_pool));
+ tmp_props = svn_prop_hash_to_array(*props, result_pool);
+ SVN_ERR(svn_categorize_props(tmp_props, NULL, NULL, &tmp_props,
+ result_pool));
+ *props = svn_prop_array_to_hash(tmp_props, result_pool);
+ }
+ else
+ {
+ *props = apr_hash_make(result_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+fetch_kind_func(svn_node_kind_t *kind,
+ void *baton,
+ const char *path,
+ svn_revnum_t base_revision,
+ apr_pool_t *scratch_pool)
+{
+ struct fetch_baton *fb = baton;
+
+ if (! SVN_IS_VALID_REVNUM(base_revision))
+ base_revision = fb->head;
+
+ SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, kind,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_delta_shim_callbacks_t *
+get_shim_callbacks(svn_ra_session_t *session,
+ svn_revnum_t head,
+ apr_pool_t *result_pool)
+{
+ svn_delta_shim_callbacks_t *callbacks =
+ svn_delta_shim_callbacks_default(result_pool);
+ struct fetch_baton *fb = apr_pcalloc(result_pool, sizeof(*fb));
+
+ fb->session = session;
+ fb->head = head;
+
+ callbacks->fetch_props_func = fetch_props_func;
+ callbacks->fetch_kind_func = fetch_kind_func;
+ callbacks->fetch_base_func = fetch_base_func;
+ callbacks->fetch_baton = fb;
+
+ return callbacks;
+}
+
+static svn_error_t *
+execute(const apr_array_header_t *actions,
+ const char *anchor,
+ apr_hash_t *revprops,
+ const char *username,
+ const char *password,
+ const char *config_dir,
+ const apr_array_header_t *config_options,
+ svn_boolean_t non_interactive,
+ svn_boolean_t trust_server_cert,
+ svn_boolean_t no_auth_cache,
+ svn_revnum_t base_revision,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *session;
+ svn_ra_session_t *aux_session;
+ const char *repos_root;
+ svn_revnum_t head;
+ const svn_delta_editor_t *editor;
+ svn_ra_callbacks2_t *ra_callbacks;
+ void *editor_baton;
+ struct operation root;
+ svn_error_t *err;
+ apr_hash_t *config;
+ svn_config_t *cfg_config;
+ int i;
+
+ SVN_ERR(svn_config_get_config(&config, config_dir, pool));
+ SVN_ERR(svn_cmdline__apply_config_options(config, config_options,
+ "svnmucc: ", "--config-option"));
+ cfg_config = svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG);
+
+ if (! svn_hash_gets(revprops, SVN_PROP_REVISION_LOG))
+ {
+ svn_string_t *msg = svn_string_create("", pool);
+
+ /* If we can do so, try to pop up $EDITOR to fetch a log message. */
+ if (non_interactive)
+ {
+ return svn_error_create
+ (SVN_ERR_CL_INSUFFICIENT_ARGS, NULL,
+ _("Cannot invoke editor to get log message "
+ "when non-interactive"));
+ }
+ else
+ {
+ SVN_ERR(svn_cmdline__edit_string_externally(
+ &msg, NULL, NULL, "", msg, "svnmucc-commit", config,
+ TRUE, NULL, apr_hash_pool_get(revprops)));
+ }
+
+ svn_hash_sets(revprops, SVN_PROP_REVISION_LOG, msg);
+ }
+
+ SVN_ERR(create_ra_callbacks(&ra_callbacks, username, password, config_dir,
+ cfg_config, non_interactive, trust_server_cert,
+ no_auth_cache, pool));
+ SVN_ERR(svn_ra_open4(&session, NULL, anchor, NULL, ra_callbacks,
+ NULL, config, pool));
+ /* Open, then reparent to avoid AUTHZ errors when opening the reposroot */
+ SVN_ERR(svn_ra_open4(&aux_session, NULL, anchor, NULL, ra_callbacks,
+ NULL, config, pool));
+ SVN_ERR(svn_ra_get_repos_root2(aux_session, &repos_root, pool));
+ SVN_ERR(svn_ra_reparent(aux_session, repos_root, pool));
+ SVN_ERR(svn_ra_get_latest_revnum(session, &head, pool));
+
+ /* Reparent to ANCHOR's dir, if ANCHOR is not a directory. */
+ {
+ svn_node_kind_t kind;
+
+ SVN_ERR(svn_ra_check_path(aux_session,
+ svn_uri_skip_ancestor(repos_root, anchor, pool),
+ head, &kind, pool));
+ if (kind != svn_node_dir)
+ {
+ anchor = svn_uri_dirname(anchor, pool);
+ SVN_ERR(svn_ra_reparent(session, anchor, pool));
+ }
+ }
+
+ if (SVN_IS_VALID_REVNUM(base_revision))
+ {
+ if (base_revision > head)
+ return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL,
+ "No such revision %ld (youngest is %ld)",
+ base_revision, head);
+ head = base_revision;
+ }
+
+ memset(&root, 0, sizeof(root));
+ root.children = apr_hash_make(pool);
+ root.operation = OP_OPEN;
+ root.kind = svn_node_dir; /* For setting properties */
+ root.prop_mods = apr_hash_make(pool);
+ root.prop_dels = apr_array_make(pool, 1, sizeof(const char *));
+
+ for (i = 0; i < actions->nelts; ++i)
+ {
+ struct action *action = APR_ARRAY_IDX(actions, i, struct action *);
+ const char *path1, *path2;
+ switch (action->action)
+ {
+ case ACTION_MV:
+ path1 = subtract_anchor(anchor, action->path[0], pool);
+ path2 = subtract_anchor(anchor, action->path[1], pool);
+ SVN_ERR(build(ACTION_RM, path1, NULL,
+ SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
+ session, &root, pool));
+ SVN_ERR(build(ACTION_CP, path2, action->path[0],
+ head, NULL, NULL, NULL, head, anchor,
+ session, &root, pool));
+ break;
+ case ACTION_CP:
+ path2 = subtract_anchor(anchor, action->path[1], pool);
+ if (action->rev == SVN_INVALID_REVNUM)
+ action->rev = head;
+ SVN_ERR(build(ACTION_CP, path2, action->path[0],
+ action->rev, NULL, NULL, NULL, head, anchor,
+ session, &root, pool));
+ break;
+ case ACTION_RM:
+ path1 = subtract_anchor(anchor, action->path[0], pool);
+ SVN_ERR(build(ACTION_RM, path1, NULL,
+ SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
+ session, &root, pool));
+ break;
+ case ACTION_MKDIR:
+ path1 = subtract_anchor(anchor, action->path[0], pool);
+ SVN_ERR(build(ACTION_MKDIR, path1, action->path[0],
+ SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor,
+ session, &root, pool));
+ break;
+ case ACTION_PUT:
+ path1 = subtract_anchor(anchor, action->path[0], pool);
+ SVN_ERR(build(ACTION_PUT, path1, action->path[0],
+ SVN_INVALID_REVNUM, NULL, NULL, action->path[1],
+ head, anchor, session, &root, pool));
+ break;
+ case ACTION_PROPSET:
+ case ACTION_PROPDEL:
+ path1 = subtract_anchor(anchor, action->path[0], pool);
+ SVN_ERR(build(action->action, path1, action->path[0],
+ SVN_INVALID_REVNUM,
+ action->prop_name, action->prop_value,
+ NULL, head, anchor, session, &root, pool));
+ break;
+ case ACTION_PROPSETF:
+ default:
+ SVN_ERR_MALFUNCTION_NO_RETURN();
+ }
+ }
+
+ SVN_ERR(svn_ra__register_editor_shim_callbacks(session,
+ get_shim_callbacks(aux_session, head, pool)));
+ SVN_ERR(svn_ra_get_commit_editor3(session, &editor, &editor_baton, revprops,
+ commit_callback, NULL, NULL, FALSE, pool));
+
+ SVN_ERR(editor->open_root(editor_baton, head, pool, &root.baton));
+ err = change_props(editor, root.baton, &root, pool);
+ if (!err)
+ err = drive(&root, head, editor, pool);
+ if (!err)
+ err = editor->close_directory(root.baton, pool);
+ if (!err)
+ err = editor->close_edit(editor_baton, pool);
+
+ if (err)
+ err = svn_error_compose_create(err,
+ editor->abort_edit(editor_baton, pool));
+
+ return err;
+}
+
+static svn_error_t *
+read_propvalue_file(const svn_string_t **value_p,
+ const char *filename,
+ apr_pool_t *pool)
+{
+ svn_stringbuf_t *value;
+ apr_pool_t *scratch_pool = svn_pool_create(pool);
+
+ SVN_ERR(svn_stringbuf_from_file2(&value, filename, scratch_pool));
+ *value_p = svn_string_create_from_buf(value, pool);
+ svn_pool_destroy(scratch_pool);
+ return SVN_NO_ERROR;
+}
+
+/* Perform the typical suite of manipulations for user-provided URLs
+ on URL, returning the result (allocated from POOL): IRI-to-URI
+ conversion, auto-escaping, and canonicalization. */
+static const char *
+sanitize_url(const char *url,
+ apr_pool_t *pool)
+{
+ url = svn_path_uri_from_iri(url, pool);
+ url = svn_path_uri_autoescape(url, pool);
+ return svn_uri_canonicalize(url, pool);
+}
+
+static void
+usage(apr_pool_t *pool, int exit_val)
+{
+ FILE *stream = exit_val == EXIT_SUCCESS ? stdout : stderr;
+ svn_error_clear(svn_cmdline_fputs(
+ _("Subversion multiple URL command client\n"
+ "usage: svnmucc ACTION...\n"
+ "\n"
+ " Perform one or more Subversion repository URL-based ACTIONs, committing\n"
+ " the result as a (single) new revision.\n"
+ "\n"
+ "Actions:\n"
+ " cp REV SRC-URL DST-URL : copy SRC-URL@REV to DST-URL\n"
+ " mkdir URL : create new directory URL\n"
+ " mv SRC-URL DST-URL : move SRC-URL to DST-URL\n"
+ " rm URL : delete URL\n"
+ " put SRC-FILE URL : add or modify file URL with contents copied from\n"
+ " SRC-FILE (use \"-\" to read from standard input)\n"
+ " propset NAME VALUE URL : set property NAME on URL to VALUE\n"
+ " propsetf NAME FILE URL : set property NAME on URL to value read from FILE\n"
+ " propdel NAME URL : delete property NAME from URL\n"
+ "\n"
+ "Valid options:\n"
+ " -h, -? [--help] : display this text\n"
+ " -m [--message] ARG : use ARG as a log message\n"
+ " -F [--file] ARG : read log message from file ARG\n"
+ " -u [--username] ARG : commit the changes as username ARG\n"
+ " -p [--password] ARG : use ARG as the password\n"
+ " -U [--root-url] ARG : interpret all action URLs relative to ARG\n"
+ " -r [--revision] ARG : use revision ARG as baseline for changes\n"
+ " --with-revprop ARG : set revision property in the following format:\n"
+ " NAME[=VALUE]\n"
+ " --non-interactive : do no interactive prompting (default is to\n"
+ " prompt only if standard input is a terminal)\n"
+ " --force-interactive : do interactive prompting even if standard\n"
+ " input is not a terminal\n"
+ " --trust-server-cert : accept SSL server certificates from unknown\n"
+ " certificate authorities without prompting (but\n"
+ " only with '--non-interactive')\n"
+ " -X [--extra-args] ARG : append arguments from file ARG (one per line;\n"
+ " use \"-\" to read from standard input)\n"
+ " --config-dir ARG : use ARG to override the config directory\n"
+ " --config-option ARG : use ARG to override a configuration option\n"
+ " --no-auth-cache : do not cache authentication tokens\n"
+ " --version : print version information\n"),
+ stream, pool));
+ svn_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);
+}
+
+static svn_error_t *
+display_version(apr_getopt_t *os, apr_pool_t *pool)
+{
+ const char *ra_desc_start
+ = "The following repository access (RA) modules are available:\n\n";
+ svn_stringbuf_t *version_footer;
+
+ version_footer = svn_stringbuf_create(ra_desc_start, pool);
+ SVN_ERR(svn_ra_print_modules(version_footer, pool));
+
+ SVN_ERR(svn_opt_print_help4(os, "svnmucc", TRUE, FALSE, FALSE,
+ version_footer->data,
+ NULL, NULL, NULL, NULL, NULL, pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Return an error about the mutual exclusivity of the -m, -F, and
+ --with-revprop=svn:log command-line options. */
+static svn_error_t *
+mutually_exclusive_logs_error(void)
+{
+ return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
+ _("--message (-m), --file (-F), and "
+ "--with-revprop=svn:log are mutually "
+ "exclusive"));
+}
+
+/* Ensure that the REVPROPS hash contains a command-line-provided log
+ message, if any, and that there was but one source of such a thing
+ provided on that command-line. */
+static svn_error_t *
+sanitize_log_sources(apr_hash_t *revprops,
+ const char *message,
+ svn_stringbuf_t *filedata)
+{
+ apr_pool_t *hash_pool = apr_hash_pool_get(revprops);
+
+ /* If we already have a log message in the revprop hash, then just
+ make sure the user didn't try to also use -m or -F. Otherwise,
+ we need to consult -m or -F to find a log message, if any. */
+ if (svn_hash_gets(revprops, SVN_PROP_REVISION_LOG))
+ {
+ if (filedata || message)
+ return mutually_exclusive_logs_error();
+ }
+ else if (filedata)
+ {
+ if (message)
+ return mutually_exclusive_logs_error();
+
+ SVN_ERR(svn_utf_cstring_to_utf8(&message, filedata->data, hash_pool));
+ svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
+ svn_stringbuf__morph_into_string(filedata));
+ }
+ else if (message)
+ {
+ svn_hash_sets(revprops, SVN_PROP_REVISION_LOG,
+ svn_string_create(message, hash_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+int
+main(int argc, const char **argv)
+{
+ apr_pool_t *pool = init("svnmucc");
+ apr_array_header_t *actions = apr_array_make(pool, 1,
+ sizeof(struct action *));
+ const char *anchor = NULL;
+ svn_error_t *err = SVN_NO_ERROR;
+ apr_getopt_t *opts;
+ enum {
+ config_dir_opt = SVN_OPT_FIRST_LONGOPT_ID,
+ config_inline_opt,
+ no_auth_cache_opt,
+ version_opt,
+ with_revprop_opt,
+ non_interactive_opt,
+ force_interactive_opt,
+ trust_server_cert_opt
+ };
+ static const apr_getopt_option_t options[] = {
+ {"message", 'm', 1, ""},
+ {"file", 'F', 1, ""},
+ {"username", 'u', 1, ""},
+ {"password", 'p', 1, ""},
+ {"root-url", 'U', 1, ""},
+ {"revision", 'r', 1, ""},
+ {"with-revprop", with_revprop_opt, 1, ""},
+ {"extra-args", 'X', 1, ""},
+ {"help", 'h', 0, ""},
+ {NULL, '?', 0, ""},
+ {"non-interactive", non_interactive_opt, 0, ""},
+ {"force-interactive", force_interactive_opt, 0, ""},
+ {"trust-server-cert", trust_server_cert_opt, 0, ""},
+ {"config-dir", config_dir_opt, 1, ""},
+ {"config-option", config_inline_opt, 1, ""},
+ {"no-auth-cache", no_auth_cache_opt, 0, ""},
+ {"version", version_opt, 0, ""},
+ {NULL, 0, 0, NULL}
+ };
+ const char *message = NULL;
+ svn_stringbuf_t *filedata = NULL;
+ const char *username = NULL, *password = NULL;
+ const char *root_url = NULL, *extra_args_file = NULL;
+ const char *config_dir = NULL;
+ apr_array_header_t *config_options;
+ svn_boolean_t non_interactive = FALSE;
+ svn_boolean_t force_interactive = FALSE;
+ svn_boolean_t trust_server_cert = FALSE;
+ svn_boolean_t no_auth_cache = FALSE;
+ svn_revnum_t base_revision = SVN_INVALID_REVNUM;
+ apr_array_header_t *action_args;
+ apr_hash_t *revprops = apr_hash_make(pool);
+ int i;
+
+ config_options = apr_array_make(pool, 0,
+ sizeof(svn_cmdline__config_argument_t*));
+
+ apr_getopt_init(&opts, pool, argc, argv);
+ opts->interleave = 1;
+ while (1)
+ {
+ int opt;
+ const char *arg;
+ const char *opt_arg;
+
+ apr_status_t status = apr_getopt_long(opts, 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;
+ err = svn_utf_cstring_to_utf8(&arg_utf8, arg, pool);
+ if (! err)
+ err = svn_stringbuf_from_file2(&filedata, arg, pool);
+ if (err)
+ handle_error(err, pool);
+ }
+ break;
+ case 'u':
+ username = apr_pstrdup(pool, arg);
+ break;
+ case 'p':
+ password = apr_pstrdup(pool, arg);
+ break;
+ case 'U':
+ err = svn_utf_cstring_to_utf8(&root_url, arg, pool);
+ if (err)
+ handle_error(err, pool);
+ if (! svn_path_is_url(root_url))
+ handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
+ "'%s' is not a URL\n", root_url),
+ pool);
+ root_url = sanitize_url(root_url, pool);
+ break;
+ case 'r':
+ {
+ char *digits_end = NULL;
+ base_revision = strtol(arg, &digits_end, 10);
+ if ((! SVN_IS_VALID_REVNUM(base_revision))
+ || (! digits_end)
+ || *digits_end)
+ handle_error(svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR,
+ NULL, "Invalid revision number"),
+ pool);
+ }
+ break;
+ case with_revprop_opt:
+ err = svn_opt_parse_revprop(&revprops, arg, pool);
+ if (err != SVN_NO_ERROR)
+ handle_error(err, pool);
+ break;
+ case 'X':
+ extra_args_file = apr_pstrdup(pool, arg);
+ break;
+ case non_interactive_opt:
+ non_interactive = TRUE;
+ break;
+ case force_interactive_opt:
+ force_interactive = TRUE;
+ break;
+ case trust_server_cert_opt:
+ trust_server_cert = TRUE;
+ break;
+ case config_dir_opt:
+ err = svn_utf_cstring_to_utf8(&config_dir, arg, pool);
+ if (err)
+ handle_error(err, pool);
+ break;
+ case config_inline_opt:
+ err = svn_utf_cstring_to_utf8(&opt_arg, arg, pool);
+ if (err)
+ handle_error(err, pool);
+
+ err = svn_cmdline__parse_config_option(config_options, opt_arg,
+ pool);
+ if (err)
+ handle_error(err, pool);
+ break;
+ case no_auth_cache_opt:
+ no_auth_cache = TRUE;
+ break;
+ case version_opt:
+ SVN_INT_ERR(display_version(opts, pool));
+ exit(EXIT_SUCCESS);
+ break;
+ case 'h':
+ case '?':
+ usage(pool, EXIT_SUCCESS);
+ break;
+ }
+ }
+
+ if (non_interactive && force_interactive)
+ {
+ err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
+ _("--non-interactive and --force-interactive "
+ "are mutually exclusive"));
+ return svn_cmdline_handle_exit_error(err, pool, "svnmucc: ");
+ }
+ else
+ non_interactive = !svn_cmdline__be_interactive(non_interactive,
+ force_interactive);
+
+ if (trust_server_cert && !non_interactive)
+ {
+ err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
+ _("--trust-server-cert requires "
+ "--non-interactive"));
+ return svn_cmdline_handle_exit_error(err, pool, "svnmucc: ");
+ }
+
+ /* Make sure we have a log message to use. */
+ err = sanitize_log_sources(revprops, message, filedata);
+ if (err)
+ handle_error(err, pool);
+
+ /* Copy the rest of our command-line arguments to an array,
+ UTF-8-ing them along the way. */
+ action_args = apr_array_make(pool, opts->argc, sizeof(const char *));
+ while (opts->ind < opts->argc)
+ {
+ const char *arg = opts->argv[opts->ind++];
+ if ((err = svn_utf_cstring_to_utf8(&(APR_ARRAY_PUSH(action_args,
+ const char *)),
+ arg, pool)))
+ handle_error(err, pool);
+ }
+
+ /* If there are extra arguments in a supplementary file, tack those
+ on, too (again, in UTF8 form). */
+ if (extra_args_file)
+ {
+ const char *extra_args_file_utf8;
+ svn_stringbuf_t *contents, *contents_utf8;
+
+ err = svn_utf_cstring_to_utf8(&extra_args_file_utf8,
+ extra_args_file, pool);
+ if (! err)
+ err = svn_stringbuf_from_file2(&contents, extra_args_file_utf8, pool);
+ if (! err)
+ err = svn_utf_stringbuf_to_utf8(&contents_utf8, contents, pool);
+ if (err)
+ handle_error(err, pool);
+ svn_cstring_split_append(action_args, contents_utf8->data, "\n\r",
+ FALSE, pool);
+ }
+
+ /* Now, we iterate over the combined set of arguments -- our actions. */
+ for (i = 0; i < action_args->nelts; )
+ {
+ int j, num_url_args;
+ const char *action_string = APR_ARRAY_IDX(action_args, i, const char *);
+ struct action *action = apr_pcalloc(pool, sizeof(*action));
+
+ /* First, parse the action. */
+ if (! strcmp(action_string, "mv"))
+ action->action = ACTION_MV;
+ else if (! strcmp(action_string, "cp"))
+ action->action = ACTION_CP;
+ else if (! strcmp(action_string, "mkdir"))
+ action->action = ACTION_MKDIR;
+ else if (! strcmp(action_string, "rm"))
+ action->action = ACTION_RM;
+ else if (! strcmp(action_string, "put"))
+ action->action = ACTION_PUT;
+ else if (! strcmp(action_string, "propset"))
+ action->action = ACTION_PROPSET;
+ else if (! strcmp(action_string, "propsetf"))
+ action->action = ACTION_PROPSETF;
+ else if (! strcmp(action_string, "propdel"))
+ action->action = ACTION_PROPDEL;
+ else if (! strcmp(action_string, "?") || ! strcmp(action_string, "h")
+ || ! strcmp(action_string, "help"))
+ usage(pool, EXIT_SUCCESS);
+ else
+ handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
+ "'%s' is not an action\n",
+ action_string), pool);
+ if (++i == action_args->nelts)
+ insufficient(pool);
+
+ /* For copies, there should be a revision number next. */
+ if (action->action == ACTION_CP)
+ {
+ const char *rev_str = APR_ARRAY_IDX(action_args, i, const char *);
+ if (strcmp(rev_str, "head") == 0)
+ action->rev = SVN_INVALID_REVNUM;
+ else if (strcmp(rev_str, "HEAD") == 0)
+ action->rev = SVN_INVALID_REVNUM;
+ else
+ {
+ char *end;
+
+ while (*rev_str == 'r')
+ ++rev_str;
+
+ action->rev = strtol(rev_str, &end, 0);
+ if (*end)
+ handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
+ "'%s' is not a revision\n",
+ rev_str), pool);
+ }
+ if (++i == action_args->nelts)
+ insufficient(pool);
+ }
+ else
+ {
+ action->rev = SVN_INVALID_REVNUM;
+ }
+
+ /* For puts, there should be a local file next. */
+ if (action->action == ACTION_PUT)
+ {
+ action->path[1] =
+ svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
+ const char *), pool);
+ if (++i == action_args->nelts)
+ insufficient(pool);
+ }
+
+ /* For propset, propsetf, and propdel, a property name (and
+ maybe a property value or file which contains one) comes next. */
+ if ((action->action == ACTION_PROPSET)
+ || (action->action == ACTION_PROPSETF)
+ || (action->action == ACTION_PROPDEL))
+ {
+ action->prop_name = APR_ARRAY_IDX(action_args, i, const char *);
+ if (++i == action_args->nelts)
+ insufficient(pool);
+
+ if (action->action == ACTION_PROPDEL)
+ {
+ action->prop_value = NULL;
+ }
+ else if (action->action == ACTION_PROPSET)
+ {
+ action->prop_value =
+ svn_string_create(APR_ARRAY_IDX(action_args, i,
+ const char *), pool);
+ if (++i == action_args->nelts)
+ insufficient(pool);
+ }
+ else
+ {
+ const char *propval_file =
+ svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i,
+ const char *), pool);
+
+ if (++i == action_args->nelts)
+ insufficient(pool);
+
+ err = read_propvalue_file(&(action->prop_value),
+ propval_file, pool);
+ if (err)
+ handle_error(err, pool);
+
+ action->action = ACTION_PROPSET;
+ }
+
+ if (action->prop_value
+ && svn_prop_needs_translation(action->prop_name))
+ {
+ svn_string_t *translated_value;
+ err = svn_subst_translate_string2(&translated_value, NULL,
+ NULL, action->prop_value, NULL,
+ FALSE, pool, pool);
+ if (err)
+ handle_error(
+ svn_error_quick_wrap(err,
+ "Error normalizing property value"),
+ pool);
+ action->prop_value = translated_value;
+ }
+ }
+
+ /* How many URLs does this action expect? */
+ if (action->action == ACTION_RM
+ || action->action == ACTION_MKDIR
+ || action->action == ACTION_PUT
+ || action->action == ACTION_PROPSET
+ || action->action == ACTION_PROPSETF /* shouldn't see this one */
+ || action->action == ACTION_PROPDEL)
+ num_url_args = 1;
+ else
+ num_url_args = 2;
+
+ /* Parse the required number of URLs. */
+ for (j = 0; j < num_url_args; ++j)
+ {
+ const char *url = APR_ARRAY_IDX(action_args, i, const char *);
+
+ /* If there's a ROOT_URL, we expect URL to be a path
+ relative to ROOT_URL (and we build a full url from the
+ combination of the two). Otherwise, it should be a full
+ url. */
+ if (! svn_path_is_url(url))
+ {
+ if (! root_url)
+ handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
+ "'%s' is not a URL, and "
+ "--root-url (-U) not provided\n",
+ url), pool);
+ /* ### These relpaths are already URI-encoded. */
+ url = apr_pstrcat(pool, root_url, "/",
+ svn_relpath_canonicalize(url, pool),
+ (char *)NULL);
+ }
+ url = sanitize_url(url, pool);
+ action->path[j] = url;
+
+ /* The first URL arguments to 'cp', 'pd', 'ps' could be the anchor,
+ but the other URLs should be children of the anchor. */
+ if (! (action->action == ACTION_CP && j == 0)
+ && action->action != ACTION_PROPDEL
+ && action->action != ACTION_PROPSET
+ && action->action != ACTION_PROPSETF)
+ url = svn_uri_dirname(url, pool);
+ if (! anchor)
+ anchor = url;
+ else
+ {
+ anchor = svn_uri_get_longest_ancestor(anchor, url, pool);
+ if (!anchor || !anchor[0])
+ handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
+ "URLs in the action list do not "
+ "share a common ancestor"),
+ pool);
+ }
+
+ if ((++i == action_args->nelts) && (j + 1 < num_url_args))
+ insufficient(pool);
+ }
+ APR_ARRAY_PUSH(actions, struct action *) = action;
+ }
+
+ if (! actions->nelts)
+ usage(pool, EXIT_FAILURE);
+
+ if ((err = execute(actions, anchor, revprops, username, password,
+ config_dir, config_options, non_interactive,
+ trust_server_cert, no_auth_cache, base_revision, pool)))
+ {
+ if (err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive)
+ err = svn_error_quick_wrap(err,
+ _("Authentication failed and interactive"
+ " prompting is disabled; see the"
+ " --force-interactive option"));
+ handle_error(err, pool);
+ }
+
+ /* Ensure that stdout is flushed, so the user will see all results. */
+ svn_error_clear(svn_cmdline_fflush(stdout));
+
+ svn_pool_destroy(pool);
+ return EXIT_SUCCESS;
+}