summaryrefslogtreecommitdiffstats
path: root/subversion/libsvn_client
diff options
context:
space:
mode:
authorpeter <peter@FreeBSD.org>2013-06-18 02:07:41 +0000
committerpeter <peter@FreeBSD.org>2013-06-18 02:07:41 +0000
commitd25dac7fcc6acc838b71bbda8916fd9665c709ab (patch)
tree135691142dc0e75a5e5d97b5074d03436435b8e0 /subversion/libsvn_client
downloadFreeBSD-src-d25dac7fcc6acc838b71bbda8916fd9665c709ab.zip
FreeBSD-src-d25dac7fcc6acc838b71bbda8916fd9665c709ab.tar.gz
Import trimmed svn-1.8.0-rc3
Diffstat (limited to 'subversion/libsvn_client')
-rw-r--r--subversion/libsvn_client/add.c1326
-rw-r--r--subversion/libsvn_client/blame.c837
-rw-r--r--subversion/libsvn_client/cat.c308
-rw-r--r--subversion/libsvn_client/changelist.c144
-rw-r--r--subversion/libsvn_client/checkout.c198
-rw-r--r--subversion/libsvn_client/cleanup.c63
-rw-r--r--subversion/libsvn_client/client.h1124
-rw-r--r--subversion/libsvn_client/cmdline.c363
-rw-r--r--subversion/libsvn_client/commit.c1031
-rw-r--r--subversion/libsvn_client/commit_util.c1981
-rw-r--r--subversion/libsvn_client/compat_providers.c136
-rw-r--r--subversion/libsvn_client/copy.c2422
-rw-r--r--subversion/libsvn_client/copy_foreign.c571
-rw-r--r--subversion/libsvn_client/ctx.c112
-rw-r--r--subversion/libsvn_client/delete.c595
-rw-r--r--subversion/libsvn_client/deprecated.c2966
-rw-r--r--subversion/libsvn_client/diff.c2723
-rw-r--r--subversion/libsvn_client/diff_local.c633
-rw-r--r--subversion/libsvn_client/diff_summarize.c317
-rw-r--r--subversion/libsvn_client/export.c1589
-rw-r--r--subversion/libsvn_client/externals.c1139
-rw-r--r--subversion/libsvn_client/import.c964
-rw-r--r--subversion/libsvn_client/info.c402
-rw-r--r--subversion/libsvn_client/iprops.c270
-rw-r--r--subversion/libsvn_client/list.c579
-rw-r--r--subversion/libsvn_client/locking_commands.c552
-rw-r--r--subversion/libsvn_client/log.c868
-rw-r--r--subversion/libsvn_client/merge.c12674
-rw-r--r--subversion/libsvn_client/mergeinfo.c2191
-rw-r--r--subversion/libsvn_client/mergeinfo.h414
-rw-r--r--subversion/libsvn_client/patch.c3043
-rw-r--r--subversion/libsvn_client/prop_commands.c1559
-rw-r--r--subversion/libsvn_client/ra.c1147
-rw-r--r--subversion/libsvn_client/relocate.c289
-rw-r--r--subversion/libsvn_client/repos_diff.c1405
-rw-r--r--subversion/libsvn_client/resolved.c148
-rw-r--r--subversion/libsvn_client/revert.c201
-rw-r--r--subversion/libsvn_client/revisions.c191
-rw-r--r--subversion/libsvn_client/status.c767
-rw-r--r--subversion/libsvn_client/switch.c487
-rw-r--r--subversion/libsvn_client/update.c707
-rw-r--r--subversion/libsvn_client/upgrade.c327
-rw-r--r--subversion/libsvn_client/url.c63
-rw-r--r--subversion/libsvn_client/util.c457
-rw-r--r--subversion/libsvn_client/version.c33
45 files changed, 50316 insertions, 0 deletions
diff --git a/subversion/libsvn_client/add.c b/subversion/libsvn_client/add.c
new file mode 100644
index 0000000..f121bc8
--- /dev/null
+++ b/subversion/libsvn_client/add.c
@@ -0,0 +1,1326 @@
+/*
+ * add.c: wrappers around wc add/mkdir functionality.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <string.h>
+#include <apr_lib.h>
+#include <apr_fnmatch.h>
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "svn_string.h"
+#include "svn_pools.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_io.h"
+#include "svn_config.h"
+#include "svn_props.h"
+#include "svn_hash.h"
+#include "svn_sorts.h"
+#include "client.h"
+#include "svn_ctype.h"
+
+#include "private/svn_client_private.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_ra_private.h"
+#include "private/svn_magic.h"
+
+#include "svn_private_config.h"
+
+
+
+/*** Code. ***/
+
+/* Remove leading and trailing white space from a C string, in place. */
+static void
+trim_string(char **pstr)
+{
+ char *str = *pstr;
+ size_t i;
+
+ while (svn_ctype_isspace(*str))
+ str++;
+ *pstr = str;
+ i = strlen(str);
+ while ((i > 0) && svn_ctype_isspace(str[i-1]))
+ i--;
+ str[i] = '\0';
+}
+
+/* Remove leading and trailing single- or double quotes from a C string,
+ * in place. */
+static void
+unquote_string(char **pstr)
+{
+ char *str = *pstr;
+ size_t i = strlen(str);
+
+ if (i > 0 && ((*str == '"' && str[i - 1] == '"') ||
+ (*str == '\'' && str[i - 1] == '\'')))
+ {
+ str[i - 1] = '\0';
+ str++;
+ }
+ *pstr = str;
+}
+
+/* Split PROPERTY and store each individual value in PROPS.
+ Allocates from POOL. */
+static void
+split_props(apr_array_header_t **props,
+ const char *property,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *temp_props;
+ char *new_prop;
+ int i = 0;
+ int j = 0;
+
+ temp_props = apr_array_make(pool, 4, sizeof(char *));
+ new_prop = apr_palloc(pool, strlen(property)+1);
+
+ for (i = 0; property[i] != '\0'; i++)
+ {
+ if (property[i] != ';')
+ {
+ new_prop[j] = property[i];
+ j++;
+ }
+ else if (property[i] == ';')
+ {
+ /* ";;" becomes ";" */
+ if (property[i+1] == ';')
+ {
+ new_prop[j] = ';';
+ j++;
+ i++;
+ }
+ else
+ {
+ new_prop[j] = '\0';
+ APR_ARRAY_PUSH(temp_props, char *) = new_prop;
+ new_prop += j + 1;
+ j = 0;
+ }
+ }
+ }
+ new_prop[j] = '\0';
+ APR_ARRAY_PUSH(temp_props, char *) = new_prop;
+ *props = temp_props;
+}
+
+/* PROPVALS is a hash mapping char * property names to const char * property
+ values. PROPERTIES can be empty but not NULL.
+
+ If FILENAME doesn't match the filename pattern PATTERN case insensitively,
+ the do nothing. Otherwise for each 'name':'value' pair in PROPVALS, add
+ a new entry mappying 'name' to a svn_string_t * wrapping the 'value' in
+ PROPERTIES. The svn_string_t is allocated in the pool used to allocate
+ PROPERTIES, but the char *'s from PROPVALS are re-used in PROPERTIES.
+ If PROPVALS contains a 'svn:mime-type' mapping, then set *MIMETYPE to
+ the mapped value. Likewise if PROPVALS contains a mapping for
+ svn:executable, then set *HAVE_EXECUTABLE to TRUE.
+
+ Use SCRATCH_POOL for temporary allocations.
+*/
+static void
+get_auto_props_for_pattern(apr_hash_t *properties,
+ const char **mimetype,
+ svn_boolean_t *have_executable,
+ const char *filename,
+ const char *pattern,
+ apr_hash_t *propvals,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_index_t *hi;
+
+ /* check if filename matches and return if it doesn't */
+ if (apr_fnmatch(pattern, filename,
+ APR_FNM_CASE_BLIND) == APR_FNM_NOMATCH)
+ return;
+
+ for (hi = apr_hash_first(scratch_pool, propvals);
+ hi != NULL;
+ hi = apr_hash_next(hi))
+ {
+ const char *propname = svn__apr_hash_index_key(hi);
+ const char *propval = svn__apr_hash_index_val(hi);
+ svn_string_t *propval_str =
+ svn_string_create_empty(apr_hash_pool_get(properties));
+
+ propval_str->data = propval;
+ propval_str->len = strlen(propval);
+
+ svn_hash_sets(properties, propname, propval_str);
+ if (strcmp(propname, SVN_PROP_MIME_TYPE) == 0)
+ *mimetype = propval;
+ else if (strcmp(propname, SVN_PROP_EXECUTABLE) == 0)
+ *have_executable = TRUE;
+ }
+}
+
+svn_error_t *
+svn_client__get_paths_auto_props(apr_hash_t **properties,
+ const char **mimetype,
+ const char *path,
+ svn_magic__cookie_t *magic_cookie,
+ apr_hash_t *autoprops,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_index_t *hi;
+ svn_boolean_t have_executable = FALSE;
+
+ *properties = apr_hash_make(result_pool);
+ *mimetype = NULL;
+
+ if (autoprops)
+ {
+ for (hi = apr_hash_first(scratch_pool, autoprops);
+ hi != NULL;
+ hi = apr_hash_next(hi))
+ {
+ const char *pattern = svn__apr_hash_index_key(hi);
+ apr_hash_t *propvals = svn__apr_hash_index_val(hi);
+
+ get_auto_props_for_pattern(*properties, mimetype, &have_executable,
+ svn_dirent_basename(path, scratch_pool),
+ pattern, propvals, scratch_pool);
+ }
+ }
+
+ /* if mimetype has not been set check the file */
+ if (! *mimetype)
+ {
+ SVN_ERR(svn_io_detect_mimetype2(mimetype, path, ctx->mimetypes_map,
+ result_pool));
+
+ /* If we got no mime-type, or if it is "application/octet-stream",
+ * try to get the mime-type from libmagic. */
+ if (magic_cookie &&
+ (!*mimetype ||
+ strcmp(*mimetype, "application/octet-stream") == 0))
+ {
+ const char *magic_mimetype;
+
+ /* Since libmagic usually treats UTF-16 files as "text/plain",
+ * svn_magic__detect_binary_mimetype() will return NULL for such
+ * files. This is fine for now since we currently don't support
+ * UTF-16-encoded text files (issue #2194).
+ * Once we do support UTF-16 this code path will fail to detect
+ * them as text unless the svn_io_detect_mimetype2() call above
+ * returns "text/plain" for them. */
+ SVN_ERR(svn_magic__detect_binary_mimetype(&magic_mimetype,
+ path, magic_cookie,
+ result_pool,
+ scratch_pool));
+ if (magic_mimetype)
+ *mimetype = magic_mimetype;
+ }
+
+ if (*mimetype)
+ apr_hash_set(*properties, SVN_PROP_MIME_TYPE,
+ strlen(SVN_PROP_MIME_TYPE),
+ svn_string_create(*mimetype, result_pool));
+ }
+
+ /* if executable has not been set check the file */
+ if (! have_executable)
+ {
+ svn_boolean_t executable = FALSE;
+ SVN_ERR(svn_io_is_file_executable(&executable, path, scratch_pool));
+ if (executable)
+ apr_hash_set(*properties, SVN_PROP_EXECUTABLE,
+ strlen(SVN_PROP_EXECUTABLE),
+ svn_string_create_empty(result_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Only call this if the on-disk node kind is a file. */
+static svn_error_t *
+add_file(const char *local_abspath,
+ svn_magic__cookie_t *magic_cookie,
+ apr_hash_t *autoprops,
+ svn_boolean_t no_autoprops,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_hash_t *properties;
+ const char *mimetype;
+ svn_node_kind_t kind;
+ svn_boolean_t is_special;
+
+ /* Check to see if this is a special file. */
+ SVN_ERR(svn_io_check_special_path(local_abspath, &kind, &is_special, pool));
+
+ /* Determine the properties that the file should have */
+ if (is_special)
+ {
+ mimetype = NULL;
+ properties = apr_hash_make(pool);
+ svn_hash_sets(properties, SVN_PROP_SPECIAL,
+ svn_string_create(SVN_PROP_BOOLEAN_TRUE, pool));
+ }
+ else
+ {
+ apr_hash_t *file_autoprops = NULL;
+
+ /* Get automatic properties */
+ /* If we are setting autoprops grab the inherited svn:auto-props and
+ config file auto-props for this file if we haven't already got them
+ when iterating over the file's unversioned parents. */
+ if (!no_autoprops)
+ {
+ if (autoprops == NULL)
+ SVN_ERR(svn_client__get_all_auto_props(
+ &file_autoprops, svn_dirent_dirname(local_abspath,pool),
+ ctx, pool, pool));
+ else
+ file_autoprops = autoprops;
+ }
+
+ /* This may fail on write-only files:
+ we open them to estimate file type. */
+ SVN_ERR(svn_client__get_paths_auto_props(&properties, &mimetype,
+ local_abspath, magic_cookie,
+ file_autoprops, ctx, pool,
+ pool));
+ }
+
+ /* Add the file */
+ SVN_ERR(svn_wc_add_from_disk2(ctx->wc_ctx, local_abspath, properties,
+ ctx->notify_func2, ctx->notify_baton2, pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Schedule directory DIR_ABSPATH, and some of the tree under it, for
+ * addition. DEPTH is the depth at this point in the descent (it may
+ * be changed for recursive calls).
+ *
+ * If DIR_ABSPATH (or any item below DIR_ABSPATH) is already scheduled for
+ * addition, add will fail and return an error unless FORCE is TRUE.
+ *
+ * Use MAGIC_COOKIE (which may be NULL) to detect the mime-type of files
+ * if necessary.
+ *
+ * If not NULL, CONFIG_AUTOPROPS is a hash representing the config file and
+ * svn:auto-props autoprops which apply to DIR_ABSPATH. It maps
+ * const char * file patterns to another hash which maps const char *
+ * property names to const char *property values. If CONFIG_AUTOPROPS is
+ * NULL and the config file and svn:auto-props autoprops are required by this
+ * function, then such will be obtained.
+ *
+ * If IGNORES is not NULL, then it is an array of const char * ignore patterns
+ * that apply to any children of DIR_ABSPATH. If REFRESH_IGNORES is TRUE, then
+ * the passed in value of IGNORES (if any) is itself ignored and this function
+ * will gather all ignore patterns applicable to DIR_ABSPATH itself (allocated in
+ * RESULT_POOL). Any recursive calls to this function get the refreshed ignore
+ * patterns. If IGNORES is NULL and REFRESH_IGNORES is FALSE, then all children of DIR_ABSPATH
+ * are unconditionally added.
+ *
+ * If CTX->CANCEL_FUNC is non-null, call it with CTX->CANCEL_BATON to allow
+ * the user to cancel the operation.
+ *
+ * Use SCRATCH_POOL for temporary allocations.
+ */
+static svn_error_t *
+add_dir_recursive(const char *dir_abspath,
+ svn_depth_t depth,
+ svn_boolean_t force,
+ svn_boolean_t no_autoprops,
+ svn_magic__cookie_t *magic_cookie,
+ apr_hash_t *config_autoprops,
+ svn_boolean_t refresh_ignores,
+ apr_array_header_t *ignores,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err;
+ apr_pool_t *iterpool;
+ apr_hash_t *dirents;
+ apr_hash_index_t *hi;
+ svn_boolean_t entry_exists = FALSE;
+
+ /* Check cancellation; note that this catches recursive calls too. */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ iterpool = svn_pool_create(scratch_pool);
+
+ /* Add this directory to revision control. */
+ err = svn_wc_add_from_disk2(ctx->wc_ctx, dir_abspath, NULL /*props*/,
+ ctx->notify_func2, ctx->notify_baton2,
+ iterpool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_ENTRY_EXISTS && force)
+ {
+ svn_error_clear(err);
+ entry_exists = TRUE;
+ }
+ else if (err)
+ {
+ return svn_error_trace(err);
+ }
+ }
+
+ /* Fetch ignores after adding to handle ignores on the directory itself
+ and ancestors via the single db optimization in libsvn_wc */
+ if (refresh_ignores)
+ SVN_ERR(svn_wc_get_ignores2(&ignores, ctx->wc_ctx, dir_abspath,
+ ctx->config, result_pool, iterpool));
+
+ /* If DIR_ABSPATH is the root of an unversioned subtree then get the
+ following "autoprops":
+
+ 1) Explicit and inherited svn:auto-props properties on
+ DIR_ABSPATH
+ 2) auto-props from the CTX->CONFIG hash
+
+ Since this set of autoprops applies to all unversioned children of
+ DIR_ABSPATH, we will pass these along to any recursive calls to
+ add_dir_recursive() and calls to add_file() below. Thus sparing
+ these callees from looking up the same information. */
+ if (!entry_exists && config_autoprops == NULL)
+ {
+ SVN_ERR(svn_client__get_all_auto_props(&config_autoprops, dir_abspath,
+ ctx, scratch_pool, iterpool));
+ }
+
+ SVN_ERR(svn_io_get_dirents3(&dirents, dir_abspath, TRUE, scratch_pool,
+ iterpool));
+
+ /* Read the directory entries one by one and add those things to
+ version control. */
+ for (hi = apr_hash_first(scratch_pool, dirents); hi; hi = apr_hash_next(hi))
+ {
+ const char *name = svn__apr_hash_index_key(hi);
+ svn_io_dirent2_t *dirent = svn__apr_hash_index_val(hi);
+ const char *abspath;
+
+ svn_pool_clear(iterpool);
+
+ /* Check cancellation so you can cancel during an
+ * add of a directory with lots of files. */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ /* Skip over SVN admin directories. */
+ if (svn_wc_is_adm_dir(name, iterpool))
+ continue;
+
+ if (ignores
+ && svn_wc_match_ignore_list(name, ignores, iterpool))
+ continue;
+
+ /* Construct the full path of the entry. */
+ abspath = svn_dirent_join(dir_abspath, name, iterpool);
+
+ /* Recurse on directories; add files; ignore the rest. */
+ if (dirent->kind == svn_node_dir && depth >= svn_depth_immediates)
+ {
+ svn_depth_t depth_below_here = depth;
+ if (depth == svn_depth_immediates)
+ depth_below_here = svn_depth_empty;
+
+ /* When DIR_ABSPATH is the root of an unversioned subtree then
+ it and all of its children have the same set of ignores. So
+ save any recursive calls the extra work of finding the same
+ set of ignores. */
+ if (refresh_ignores && !entry_exists)
+ refresh_ignores = FALSE;
+
+ SVN_ERR(add_dir_recursive(abspath, depth_below_here,
+ force, no_autoprops,
+ magic_cookie, config_autoprops,
+ refresh_ignores, ignores, ctx,
+ result_pool, iterpool));
+ }
+ else if ((dirent->kind == svn_node_file || dirent->special)
+ && depth >= svn_depth_files)
+ {
+ err = add_file(abspath, magic_cookie, config_autoprops,
+ no_autoprops, ctx, iterpool);
+ if (err && err->apr_err == SVN_ERR_ENTRY_EXISTS && force)
+ svn_error_clear(err);
+ else
+ SVN_ERR(err);
+ }
+ }
+
+ /* Destroy the per-iteration pool. */
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* This structure is used as baton for collecting the config entries
+ in the auto-props section and any inherited svn:auto-props
+ properties.
+*/
+typedef struct collect_auto_props_baton_t
+{
+ /* the hash table for storing the property name/value pairs */
+ apr_hash_t *autoprops;
+
+ /* a pool used for allocating memory */
+ apr_pool_t *result_pool;
+} collect_auto_props_baton_t;
+
+/* Implements svn_config_enumerator2_t callback.
+
+ For one auto-props config entry (NAME, VALUE), stash a copy of
+ NAME and VALUE, allocated in BATON->POOL, in BATON->AUTOPROP.
+ BATON must point to an collect_auto_props_baton_t.
+*/
+static svn_boolean_t
+all_auto_props_collector(const char *name,
+ const char *value,
+ void *baton,
+ apr_pool_t *pool)
+{
+ collect_auto_props_baton_t *autoprops_baton = baton;
+ apr_array_header_t *autoprops;
+ int i;
+
+ /* nothing to do here without a value */
+ if (*value == 0)
+ return TRUE;
+
+ split_props(&autoprops, value, pool);
+
+ for (i = 0; i < autoprops->nelts; i ++)
+ {
+ size_t len;
+ const char *this_value;
+ char *property = APR_ARRAY_IDX(autoprops, i, char *);
+ char *equal_sign = strchr(property, '=');
+
+ if (equal_sign)
+ {
+ *equal_sign = '\0';
+ equal_sign++;
+ trim_string(&equal_sign);
+ unquote_string(&equal_sign);
+ this_value = equal_sign;
+ }
+ else
+ {
+ this_value = "";
+ }
+ trim_string(&property);
+ len = strlen(property);
+
+ if (len > 0)
+ {
+ apr_hash_t *pattern_hash = svn_hash_gets(autoprops_baton->autoprops,
+ name);
+ svn_string_t *propval;
+
+ /* Force reserved boolean property values to '*'. */
+ if (svn_prop_is_boolean(property))
+ {
+ /* SVN_PROP_EXECUTABLE, SVN_PROP_NEEDS_LOCK, SVN_PROP_SPECIAL */
+ propval = svn_string_create("*", autoprops_baton->result_pool);
+ }
+ else
+ {
+ propval = svn_string_create(this_value,
+ autoprops_baton->result_pool);
+ }
+
+ if (!pattern_hash)
+ {
+ pattern_hash = apr_hash_make(autoprops_baton->result_pool);
+ svn_hash_sets(autoprops_baton->autoprops,
+ apr_pstrdup(autoprops_baton->result_pool, name),
+ pattern_hash);
+ }
+ svn_hash_sets(pattern_hash,
+ apr_pstrdup(autoprops_baton->result_pool, property),
+ propval->data);
+ }
+ }
+ return TRUE;
+}
+
+/* Go up the directory tree from LOCAL_ABSPATH, looking for a versioned
+ * directory. If found, return its path in *EXISTING_PARENT_ABSPATH.
+ * Otherwise, return SVN_ERR_CLIENT_NO_VERSIONED_PARENT. */
+static svn_error_t *
+find_existing_parent(const char **existing_parent_abspath,
+ svn_client_ctx_t *ctx,
+ const char *local_abspath,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_node_kind_t kind;
+ const char *parent_abspath;
+ svn_wc_context_t *wc_ctx = ctx->wc_ctx;
+
+ SVN_ERR(svn_wc_read_kind2(&kind, wc_ctx, local_abspath,
+ FALSE, FALSE, scratch_pool));
+
+ if (kind == svn_node_dir)
+ {
+ *existing_parent_abspath = apr_pstrdup(result_pool, local_abspath);
+ return SVN_NO_ERROR;
+ }
+
+ if (svn_dirent_is_root(local_abspath, strlen(local_abspath)))
+ return svn_error_create(SVN_ERR_CLIENT_NO_VERSIONED_PARENT, NULL, NULL);
+
+ if (svn_wc_is_adm_dir(svn_dirent_basename(local_abspath, scratch_pool),
+ scratch_pool))
+ return svn_error_createf(SVN_ERR_RESERVED_FILENAME_SPECIFIED, NULL,
+ _("'%s' ends in a reserved name"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+
+ parent_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
+
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ SVN_ERR(find_existing_parent(existing_parent_abspath, ctx, parent_abspath,
+ result_pool, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__get_all_auto_props(apr_hash_t **autoprops,
+ const char *path_or_url,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ apr_array_header_t *inherited_config_auto_props;
+ apr_hash_t *props;
+ svn_opt_revision_t rev;
+ svn_string_t *config_auto_prop;
+ svn_boolean_t use_autoprops;
+ collect_auto_props_baton_t autoprops_baton;
+ svn_error_t *err = NULL;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ svn_boolean_t target_is_url = svn_path_is_url(path_or_url);
+ svn_config_t *cfg = ctx->config ? svn_hash_gets(ctx->config,
+ SVN_CONFIG_CATEGORY_CONFIG)
+ : NULL;
+ *autoprops = apr_hash_make(result_pool);
+ autoprops_baton.result_pool = result_pool;
+ autoprops_baton.autoprops = *autoprops;
+
+
+ /* Are "traditional" auto-props enabled? If so grab them from the
+ config. This is our starting set auto-props, which may be overriden
+ by svn:auto-props. */
+ SVN_ERR(svn_config_get_bool(cfg, &use_autoprops,
+ SVN_CONFIG_SECTION_MISCELLANY,
+ SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS, FALSE));
+ if (use_autoprops)
+ svn_config_enumerate2(cfg, SVN_CONFIG_SECTION_AUTO_PROPS,
+ all_auto_props_collector, &autoprops_baton,
+ scratch_pool);
+
+ /* Convert the config file setting (if any) into a hash mapping file
+ patterns to as hash of prop-->val mappings. */
+ if (svn_path_is_url(path_or_url))
+ rev.kind = svn_opt_revision_head;
+ else
+ rev.kind = svn_opt_revision_working;
+
+ /* If PATH_OR_URL is a WC path, then it might be unversioned, in which case
+ we find it's nearest versioned parent. */
+ do
+ {
+ err = svn_client_propget5(&props, &inherited_config_auto_props,
+ SVN_PROP_INHERITABLE_AUTO_PROPS, path_or_url,
+ &rev, &rev, NULL, svn_depth_empty, NULL, ctx,
+ scratch_pool, iterpool);
+ if (err)
+ {
+ if (target_is_url || err->apr_err != SVN_ERR_UNVERSIONED_RESOURCE)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ err = NULL;
+ SVN_ERR(find_existing_parent(&path_or_url, ctx, path_or_url,
+ scratch_pool, iterpool));
+ }
+ else
+ {
+ break;
+ }
+ }
+ while (err == NULL);
+
+ /* Stash any explicit PROPS for PARENT_PATH into the inherited props array,
+ since these are actually inherited props for LOCAL_ABSPATH. */
+ config_auto_prop = svn_hash_gets(props, path_or_url);
+
+ if (config_auto_prop)
+ {
+ svn_prop_inherited_item_t *new_iprop =
+ apr_palloc(scratch_pool, sizeof(*new_iprop));
+ new_iprop->path_or_url = path_or_url;
+ new_iprop->prop_hash = apr_hash_make(scratch_pool);
+ svn_hash_sets(new_iprop->prop_hash, SVN_PROP_INHERITABLE_AUTO_PROPS,
+ config_auto_prop);
+ APR_ARRAY_PUSH(inherited_config_auto_props,
+ svn_prop_inherited_item_t *) = new_iprop;
+ }
+
+ for (i = 0; i < inherited_config_auto_props->nelts; i++)
+ {
+ apr_hash_index_t *hi;
+ svn_prop_inherited_item_t *elt = APR_ARRAY_IDX(
+ inherited_config_auto_props, i, svn_prop_inherited_item_t *);
+
+ for (hi = apr_hash_first(scratch_pool, elt->prop_hash);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const svn_string_t *propval = svn__apr_hash_index_val(hi);
+ const char *ch = propval->data;
+ svn_stringbuf_t *config_auto_prop_pattern;
+ svn_stringbuf_t *config_auto_prop_val;
+
+ svn_pool_clear(iterpool);
+
+ config_auto_prop_pattern = svn_stringbuf_create_empty(iterpool);
+ config_auto_prop_val = svn_stringbuf_create_empty(iterpool);
+
+ /* Parse svn:auto-props value. */
+ while (*ch != '\0')
+ {
+ svn_stringbuf_setempty(config_auto_prop_pattern);
+ svn_stringbuf_setempty(config_auto_prop_val);
+
+ /* Parse the file pattern. */
+ while (*ch != '\0' && *ch != '=' && *ch != '\n')
+ {
+ svn_stringbuf_appendbyte(config_auto_prop_pattern, *ch);
+ ch++;
+ }
+
+ svn_stringbuf_strip_whitespace(config_auto_prop_pattern);
+
+ /* Parse the auto-prop group. */
+ while (*ch != '\0' && *ch != '\n')
+ {
+ svn_stringbuf_appendbyte(config_auto_prop_val, *ch);
+ ch++;
+ }
+
+ /* Strip leading '=' and whitespace from auto-prop group. */
+ if (config_auto_prop_val->data[0] == '=')
+ svn_stringbuf_remove(config_auto_prop_val, 0, 1);
+ svn_stringbuf_strip_whitespace(config_auto_prop_val);
+
+ all_auto_props_collector(config_auto_prop_pattern->data,
+ config_auto_prop_val->data,
+ &autoprops_baton,
+ scratch_pool);
+
+ /* Skip to next line if any. */
+ while (*ch != '\0' && *ch != '\n')
+ ch++;
+ if (*ch == '\n')
+ ch++;
+ }
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *svn_client__get_inherited_ignores(apr_array_header_t **ignores,
+ const char *path_or_url,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_opt_revision_t rev;
+ apr_hash_t *explicit_ignores;
+ apr_array_header_t *inherited_ignores;
+ svn_boolean_t target_is_url = svn_path_is_url(path_or_url);
+ svn_string_t *explicit_prop;
+ int i;
+
+ if (target_is_url)
+ rev.kind = svn_opt_revision_head;
+ else
+ rev.kind = svn_opt_revision_working;
+
+ SVN_ERR(svn_client_propget5(&explicit_ignores, &inherited_ignores,
+ SVN_PROP_INHERITABLE_IGNORES, path_or_url,
+ &rev, &rev, NULL, svn_depth_empty, NULL, ctx,
+ scratch_pool, scratch_pool));
+
+ explicit_prop = svn_hash_gets(explicit_ignores, path_or_url);
+
+ if (explicit_prop)
+ {
+ svn_prop_inherited_item_t *new_iprop =
+ apr_palloc(scratch_pool, sizeof(*new_iprop));
+ new_iprop->path_or_url = path_or_url;
+ new_iprop->prop_hash = apr_hash_make(scratch_pool);
+ svn_hash_sets(new_iprop->prop_hash, SVN_PROP_INHERITABLE_IGNORES,
+ explicit_prop);
+ APR_ARRAY_PUSH(inherited_ignores,
+ svn_prop_inherited_item_t *) = new_iprop;
+ }
+
+ *ignores = apr_array_make(result_pool, 16, sizeof(const char *));
+
+ for (i = 0; i < inherited_ignores->nelts; i++)
+ {
+ svn_prop_inherited_item_t *elt = APR_ARRAY_IDX(
+ inherited_ignores, i, svn_prop_inherited_item_t *);
+ svn_string_t *ignore_val = svn_hash_gets(elt->prop_hash,
+ SVN_PROP_INHERITABLE_IGNORES);
+ if (ignore_val)
+ svn_cstring_split_append(*ignores, ignore_val->data, "\n\r\t\v ",
+ FALSE, result_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* The main logic of the public svn_client_add5.
+ *
+ * EXISTING_PARENT_ABSPATH is the absolute path to the first existing
+ * parent directory of local_abspath. If not NULL, all missing parents
+ * of LOCAL_ABSPATH must be created before LOCAL_ABSPATH can be added. */
+static svn_error_t *
+add(const char *local_abspath,
+ svn_depth_t depth,
+ svn_boolean_t force,
+ svn_boolean_t no_ignore,
+ svn_boolean_t no_autoprops,
+ const char *existing_parent_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_node_kind_t kind;
+ svn_error_t *err;
+ svn_magic__cookie_t *magic_cookie;
+ apr_array_header_t *ignores = NULL;
+
+ svn_magic__init(&magic_cookie, scratch_pool);
+
+ if (existing_parent_abspath)
+ {
+ const char *parent_abspath;
+ const char *child_relpath;
+ apr_array_header_t *components;
+ int i;
+ apr_pool_t *iterpool;
+
+ parent_abspath = existing_parent_abspath;
+ child_relpath = svn_dirent_is_child(existing_parent_abspath,
+ local_abspath, NULL);
+ components = svn_path_decompose(child_relpath, scratch_pool);
+ iterpool = svn_pool_create(scratch_pool);
+ for (i = 0; i < components->nelts - 1; i++)
+ {
+ const char *component;
+ svn_node_kind_t disk_kind;
+
+ svn_pool_clear(iterpool);
+
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ component = APR_ARRAY_IDX(components, i, const char *);
+ parent_abspath = svn_dirent_join(parent_abspath, component,
+ scratch_pool);
+ SVN_ERR(svn_io_check_path(parent_abspath, &disk_kind, iterpool));
+ if (disk_kind != svn_node_none && disk_kind != svn_node_dir)
+ return svn_error_createf(SVN_ERR_CLIENT_NO_VERSIONED_PARENT, NULL,
+ _("'%s' prevents creating parent of '%s'"),
+ parent_abspath, local_abspath);
+
+ SVN_ERR(svn_io_make_dir_recursively(parent_abspath, scratch_pool));
+ SVN_ERR(svn_wc_add_from_disk2(ctx->wc_ctx, parent_abspath,
+ NULL /*props*/,
+ ctx->notify_func2, ctx->notify_baton2,
+ scratch_pool));
+ }
+ svn_pool_destroy(iterpool);
+ }
+
+ SVN_ERR(svn_io_check_path(local_abspath, &kind, scratch_pool));
+ if (kind == svn_node_dir)
+ {
+ /* We use add_dir_recursive for all directory targets
+ and pass depth along no matter what it is, so that the
+ target's depth will be set correctly. */
+ err = add_dir_recursive(local_abspath, depth, force,
+ no_autoprops, magic_cookie, NULL,
+ !no_ignore, ignores, ctx,
+ scratch_pool, scratch_pool);
+ }
+ else if (kind == svn_node_file)
+ err = add_file(local_abspath, magic_cookie, NULL,
+ no_autoprops, ctx, scratch_pool);
+ else if (kind == svn_node_none)
+ {
+ svn_boolean_t tree_conflicted;
+
+ /* Provide a meaningful error message if the node does not exist
+ * on disk but is a tree conflict victim. */
+ err = svn_wc_conflicted_p3(NULL, NULL, &tree_conflicted,
+ ctx->wc_ctx, local_abspath,
+ scratch_pool);
+ if (err)
+ svn_error_clear(err);
+ else if (tree_conflicted)
+ return svn_error_createf(SVN_ERR_WC_FOUND_CONFLICT, NULL,
+ _("'%s' is an existing item in conflict; "
+ "please mark the conflict as resolved "
+ "before adding a new item here"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+
+ return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL,
+ _("'%s' not found"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+ }
+ else
+ return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Unsupported node kind for path '%s'"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+
+ /* Ignore SVN_ERR_ENTRY_EXISTS when FORCE is set. */
+ if (err && err->apr_err == SVN_ERR_ENTRY_EXISTS && force)
+ {
+ svn_error_clear(err);
+ err = SVN_NO_ERROR;
+ }
+ return svn_error_trace(err);
+}
+
+
+
+svn_error_t *
+svn_client_add5(const char *path,
+ svn_depth_t depth,
+ svn_boolean_t force,
+ svn_boolean_t no_ignore,
+ svn_boolean_t no_autoprops,
+ svn_boolean_t add_parents,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ const char *parent_abspath;
+ const char *local_abspath;
+ const char *existing_parent_abspath;
+ svn_boolean_t is_wc_root;
+ svn_error_t *err;
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, scratch_pool));
+
+ /* See if we're being asked to add a wc-root. That's typically not
+ okay, unless we're in "force" mode. svn_wc__is_wcroot()
+ will return TRUE even if LOCAL_ABSPATH is a *symlink* to a working
+ copy root, which is a scenario we want to treat differently. */
+ err = svn_wc__is_wcroot(&is_wc_root, ctx->wc_ctx, local_abspath,
+ scratch_pool);
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
+ && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
+ {
+ return svn_error_trace(err);
+ }
+
+ svn_error_clear(err);
+ err = NULL; /* SVN_NO_ERROR */
+ is_wc_root = FALSE;
+ }
+ if (is_wc_root)
+ {
+#ifdef HAVE_SYMLINK
+ svn_node_kind_t disk_kind;
+ svn_boolean_t is_special;
+
+ SVN_ERR(svn_io_check_special_path(local_abspath, &disk_kind, &is_special,
+ scratch_pool));
+
+ /* A symlink can be an unversioned target and a wcroot. Lets try to add
+ the symlink, which can't be a wcroot. */
+ if (is_special)
+ is_wc_root = FALSE;
+ else
+#endif
+ {
+ if (! force)
+ return svn_error_createf(
+ SVN_ERR_ENTRY_EXISTS, NULL,
+ _("'%s' is already under version control"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+ }
+ }
+
+ if (is_wc_root)
+ parent_abspath = local_abspath; /* We will only add children */
+ else
+ parent_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
+
+ existing_parent_abspath = NULL;
+ if (add_parents && !is_wc_root)
+ {
+ apr_pool_t *subpool;
+ const char *existing_parent_abspath2;
+
+ subpool = svn_pool_create(scratch_pool);
+ SVN_ERR(find_existing_parent(&existing_parent_abspath2, ctx,
+ parent_abspath, scratch_pool, subpool));
+ if (strcmp(existing_parent_abspath2, parent_abspath) != 0)
+ existing_parent_abspath = existing_parent_abspath2;
+ svn_pool_destroy(subpool);
+ }
+
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ add(local_abspath, depth, force, no_ignore, no_autoprops,
+ existing_parent_abspath, ctx, scratch_pool),
+ ctx->wc_ctx, (existing_parent_abspath ? existing_parent_abspath
+ : parent_abspath),
+ FALSE /* lock_anchor */, scratch_pool);
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+path_driver_cb_func(void **dir_baton,
+ void *parent_baton,
+ void *callback_baton,
+ const char *path,
+ apr_pool_t *pool)
+{
+ const svn_delta_editor_t *editor = callback_baton;
+ SVN_ERR(svn_path_check_valid(path, pool));
+ return editor->add_directory(path, parent_baton, NULL,
+ SVN_INVALID_REVNUM, pool, dir_baton);
+}
+
+/* Append URL, and all it's non-existent parent directories, to TARGETS.
+ Use TEMPPOOL for temporary allocations and POOL for any additions to
+ TARGETS. */
+static svn_error_t *
+add_url_parents(svn_ra_session_t *ra_session,
+ const char *url,
+ apr_array_header_t *targets,
+ apr_pool_t *temppool,
+ apr_pool_t *pool)
+{
+ svn_node_kind_t kind;
+ const char *parent_url = svn_uri_dirname(url, pool);
+
+ SVN_ERR(svn_ra_reparent(ra_session, parent_url, temppool));
+ SVN_ERR(svn_ra_check_path(ra_session, "", SVN_INVALID_REVNUM, &kind,
+ temppool));
+
+ if (kind == svn_node_none)
+ SVN_ERR(add_url_parents(ra_session, parent_url, targets, temppool, pool));
+
+ APR_ARRAY_PUSH(targets, const char *) = url;
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+mkdir_urls(const apr_array_header_t *urls,
+ svn_boolean_t make_parents,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *ra_session = NULL;
+ const svn_delta_editor_t *editor;
+ void *edit_baton;
+ const char *log_msg;
+ apr_array_header_t *targets;
+ apr_hash_t *targets_hash;
+ apr_hash_t *commit_revprops;
+ svn_error_t *err;
+ const char *common;
+ int i;
+
+ /* Find any non-existent parent directories */
+ if (make_parents)
+ {
+ apr_array_header_t *all_urls = apr_array_make(pool, urls->nelts,
+ sizeof(const char *));
+ const char *first_url = APR_ARRAY_IDX(urls, 0, const char *);
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, first_url, NULL,
+ ctx, pool, iterpool));
+
+ for (i = 0; i < urls->nelts; i++)
+ {
+ const char *url = APR_ARRAY_IDX(urls, i, const char *);
+
+ svn_pool_clear(iterpool);
+ SVN_ERR(add_url_parents(ra_session, url, all_urls, iterpool, pool));
+ }
+
+ svn_pool_destroy(iterpool);
+
+ urls = all_urls;
+ }
+
+ /* Condense our list of mkdir targets. */
+ SVN_ERR(svn_uri_condense_targets(&common, &targets, urls, FALSE,
+ pool, pool));
+
+ /*Remove duplicate targets introduced by make_parents with more targets. */
+ SVN_ERR(svn_hash_from_cstring_keys(&targets_hash, targets, pool));
+ SVN_ERR(svn_hash_keys(&targets, targets_hash, pool));
+
+ if (! targets->nelts)
+ {
+ const char *bname;
+ svn_uri_split(&common, &bname, common, pool);
+ APR_ARRAY_PUSH(targets, const char *) = bname;
+
+ if (*bname == '\0')
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("There is no valid URI above '%s'"),
+ common);
+ }
+ else
+ {
+ svn_boolean_t resplit = FALSE;
+
+ /* We can't "mkdir" the root of an editor drive, so if one of
+ our targets is the empty string, we need to back everything
+ up by a path component. */
+ for (i = 0; i < targets->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(targets, i, const char *);
+ if (! *path)
+ {
+ resplit = TRUE;
+ break;
+ }
+ }
+ if (resplit)
+ {
+ const char *bname;
+
+ svn_uri_split(&common, &bname, common, pool);
+
+ if (*bname == '\0')
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("There is no valid URI above '%s'"),
+ common);
+
+ for (i = 0; i < targets->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(targets, i, const char *);
+ path = svn_relpath_join(bname, path, pool);
+ APR_ARRAY_IDX(targets, i, const char *) = path;
+ }
+ }
+ }
+ qsort(targets->elts, targets->nelts, targets->elt_size,
+ svn_sort_compare_paths);
+
+ /* ### This reparent may be problematic in limited-authz-to-common-parent
+ ### scenarios (compare issue #3242). See also issue #3649. */
+ if (ra_session)
+ SVN_ERR(svn_ra_reparent(ra_session, common, pool));
+
+ /* Create new commit items and add them to the array. */
+ if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx))
+ {
+ svn_client_commit_item3_t *item;
+ const char *tmp_file;
+ apr_array_header_t *commit_items
+ = apr_array_make(pool, targets->nelts, sizeof(item));
+
+ for (i = 0; i < targets->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(targets, i, const char *);
+
+ item = svn_client_commit_item3_create(pool);
+ item->url = svn_path_url_add_component2(common, path, pool);
+ item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD;
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
+ }
+
+ SVN_ERR(svn_client__get_log_msg(&log_msg, &tmp_file, commit_items,
+ ctx, pool));
+
+ if (! log_msg)
+ return SVN_NO_ERROR;
+ }
+ else
+ log_msg = "";
+
+ SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table,
+ log_msg, ctx, pool));
+
+ /* Open an RA session for the URL. Note that we don't have a local
+ directory, nor a place to put temp files. */
+ if (!ra_session)
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, common, NULL, ctx,
+ pool, pool));
+ else
+ SVN_ERR(svn_ra_reparent(ra_session, common, pool));
+
+
+ /* Fetch RA commit editor */
+ SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session,
+ svn_client__get_shim_callbacks(ctx->wc_ctx, NULL,
+ pool)));
+ SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton,
+ commit_revprops,
+ commit_callback,
+ commit_baton,
+ NULL, TRUE, /* No lock tokens */
+ pool));
+
+ /* Call the path-based editor driver. */
+ err = svn_delta_path_driver2(editor, edit_baton, targets, TRUE,
+ path_driver_cb_func, (void *)editor, pool);
+
+ if (err)
+ {
+ /* At least try to abort the edit (and fs txn) before throwing err. */
+ return svn_error_compose_create(
+ err,
+ editor->abort_edit(edit_baton, pool));
+ }
+
+ /* Close the edit. */
+ return editor->close_edit(edit_baton, pool);
+}
+
+
+
+svn_error_t *
+svn_client__make_local_parents(const char *path,
+ svn_boolean_t make_parents,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+ svn_node_kind_t orig_kind;
+ SVN_ERR(svn_io_check_path(path, &orig_kind, pool));
+ if (make_parents)
+ SVN_ERR(svn_io_make_dir_recursively(path, pool));
+ else
+ SVN_ERR(svn_io_dir_make(path, APR_OS_DEFAULT, pool));
+
+ /* Should no longer use svn_depth_empty to indicate that only the directory
+ itself is added, since it not only constraints the operation depth, but
+ also defines the depth of the target directory now. Moreover, the new
+ directory will have no children at all.*/
+ err = svn_client_add5(path, svn_depth_infinity, FALSE, FALSE, FALSE,
+ make_parents, ctx, pool);
+
+ /* If we created a new directory, but couldn't add it to version
+ control, then delete it. */
+ if (err && (orig_kind == svn_node_none))
+ {
+ /* ### If this returns an error, should we link it onto
+ err instead, so that the user is warned that we just
+ created an unversioned directory? */
+
+ svn_error_clear(svn_io_remove_dir2(path, FALSE, NULL, NULL, pool));
+ }
+
+ return svn_error_trace(err);
+}
+
+
+svn_error_t *
+svn_client_mkdir4(const apr_array_header_t *paths,
+ svn_boolean_t make_parents,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ if (! paths->nelts)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(svn_client__assert_homogeneous_target_type(paths));
+
+ if (svn_path_is_url(APR_ARRAY_IDX(paths, 0, const char *)))
+ {
+ SVN_ERR(mkdir_urls(paths, make_parents, revprop_table, commit_callback,
+ commit_baton, ctx, pool));
+ }
+ else
+ {
+ /* This is a regular "mkdir" + "svn add" */
+ apr_pool_t *subpool = svn_pool_create(pool);
+ int i;
+
+ for (i = 0; i < paths->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(paths, i, const char *);
+
+ svn_pool_clear(subpool);
+
+ /* See if the user wants us to stop. */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ SVN_ERR(svn_client__make_local_parents(path, make_parents, ctx,
+ subpool));
+ }
+ svn_pool_destroy(subpool);
+ }
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/blame.c b/subversion/libsvn_client/blame.c
new file mode 100644
index 0000000..188fdd2
--- /dev/null
+++ b/subversion/libsvn_client/blame.c
@@ -0,0 +1,837 @@
+/*
+ * blame.c: return blame messages
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+#include <apr_pools.h>
+
+#include "client.h"
+
+#include "svn_client.h"
+#include "svn_subst.h"
+#include "svn_string.h"
+#include "svn_error.h"
+#include "svn_diff.h"
+#include "svn_pools.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_props.h"
+#include "svn_sorts.h"
+
+#include "private/svn_wc_private.h"
+
+#include "svn_private_config.h"
+
+#include <assert.h>
+
+/* The metadata associated with a particular revision. */
+struct rev
+{
+ svn_revnum_t revision; /* the revision number */
+ apr_hash_t *rev_props; /* the revision properties */
+ /* Used for merge reporting. */
+ const char *path; /* the absolute repository path */
+};
+
+/* One chunk of blame */
+struct blame
+{
+ const struct rev *rev; /* the responsible revision */
+ apr_off_t start; /* the starting diff-token (line) */
+ struct blame *next; /* the next chunk */
+};
+
+/* A chain of blame chunks */
+struct blame_chain
+{
+ struct blame *blame; /* linked list of blame chunks */
+ struct blame *avail; /* linked list of free blame chunks */
+ struct apr_pool_t *pool; /* Allocate members from this pool. */
+};
+
+/* The baton use for the diff output routine. */
+struct diff_baton {
+ struct blame_chain *chain;
+ const struct rev *rev;
+};
+
+/* The baton used for a file revision. */
+struct file_rev_baton {
+ svn_revnum_t start_rev, end_rev;
+ const char *target;
+ svn_client_ctx_t *ctx;
+ const svn_diff_file_options_t *diff_options;
+ /* name of file containing the previous revision of the file */
+ const char *last_filename;
+ struct rev *rev; /* the rev for which blame is being assigned
+ during a diff */
+ struct blame_chain *chain; /* the original blame chain. */
+ const char *repos_root_url; /* To construct a url */
+ apr_pool_t *mainpool; /* lives during the whole sequence of calls */
+ apr_pool_t *lastpool; /* pool used during previous call */
+ apr_pool_t *currpool; /* pool used during this call */
+
+ /* These are used for tracking merged revisions. */
+ svn_boolean_t include_merged_revisions;
+ svn_boolean_t merged_revision;
+ struct blame_chain *merged_chain; /* the merged blame chain. */
+ /* name of file containing the previous merged revision of the file */
+ const char *last_original_filename;
+ /* pools for files which may need to persist for more than one rev. */
+ apr_pool_t *filepool;
+ apr_pool_t *prevfilepool;
+};
+
+/* The baton used by the txdelta window handler. */
+struct delta_baton {
+ /* Our underlying handler/baton that we wrap */
+ svn_txdelta_window_handler_t wrapped_handler;
+ void *wrapped_baton;
+ struct file_rev_baton *file_rev_baton;
+ const char *filename;
+};
+
+
+
+
+/* Return a blame chunk associated with REV for a change starting
+ at token START, and allocated in CHAIN->mainpool. */
+static struct blame *
+blame_create(struct blame_chain *chain,
+ const struct rev *rev,
+ apr_off_t start)
+{
+ struct blame *blame;
+ if (chain->avail)
+ {
+ blame = chain->avail;
+ chain->avail = blame->next;
+ }
+ else
+ blame = apr_palloc(chain->pool, sizeof(*blame));
+ blame->rev = rev;
+ blame->start = start;
+ blame->next = NULL;
+ return blame;
+}
+
+/* Destroy a blame chunk. */
+static void
+blame_destroy(struct blame_chain *chain,
+ struct blame *blame)
+{
+ blame->next = chain->avail;
+ chain->avail = blame;
+}
+
+/* Return the blame chunk that contains token OFF, starting the search at
+ BLAME. */
+static struct blame *
+blame_find(struct blame *blame, apr_off_t off)
+{
+ struct blame *prev = NULL;
+ while (blame)
+ {
+ if (blame->start > off) break;
+ prev = blame;
+ blame = blame->next;
+ }
+ return prev;
+}
+
+/* Shift the start-point of BLAME and all subsequence blame-chunks
+ by ADJUST tokens */
+static void
+blame_adjust(struct blame *blame, apr_off_t adjust)
+{
+ while (blame)
+ {
+ blame->start += adjust;
+ blame = blame->next;
+ }
+}
+
+/* Delete the blame associated with the region from token START to
+ START + LENGTH */
+static svn_error_t *
+blame_delete_range(struct blame_chain *chain,
+ apr_off_t start,
+ apr_off_t length)
+{
+ struct blame *first = blame_find(chain->blame, start);
+ struct blame *last = blame_find(chain->blame, start + length);
+ struct blame *tail = last->next;
+
+ if (first != last)
+ {
+ struct blame *walk = first->next;
+ while (walk != last)
+ {
+ struct blame *next = walk->next;
+ blame_destroy(chain, walk);
+ walk = next;
+ }
+ first->next = last;
+ last->start = start;
+ if (first->start == start)
+ {
+ *first = *last;
+ blame_destroy(chain, last);
+ last = first;
+ }
+ }
+
+ if (tail && tail->start == last->start + length)
+ {
+ *last = *tail;
+ blame_destroy(chain, tail);
+ tail = last->next;
+ }
+
+ blame_adjust(tail, -length);
+
+ return SVN_NO_ERROR;
+}
+
+/* Insert a chunk of blame associated with REV starting
+ at token START and continuing for LENGTH tokens */
+static svn_error_t *
+blame_insert_range(struct blame_chain *chain,
+ const struct rev *rev,
+ apr_off_t start,
+ apr_off_t length)
+{
+ struct blame *head = chain->blame;
+ struct blame *point = blame_find(head, start);
+ struct blame *insert;
+
+ if (point->start == start)
+ {
+ insert = blame_create(chain, point->rev, point->start + length);
+ point->rev = rev;
+ insert->next = point->next;
+ point->next = insert;
+ }
+ else
+ {
+ struct blame *middle;
+ middle = blame_create(chain, rev, start);
+ insert = blame_create(chain, point->rev, start + length);
+ middle->next = insert;
+ insert->next = point->next;
+ point->next = middle;
+ }
+ blame_adjust(insert->next, length);
+
+ return SVN_NO_ERROR;
+}
+
+/* Callback for diff between subsequent revisions */
+static svn_error_t *
+output_diff_modified(void *baton,
+ apr_off_t original_start,
+ apr_off_t original_length,
+ apr_off_t modified_start,
+ apr_off_t modified_length,
+ apr_off_t latest_start,
+ apr_off_t latest_length)
+{
+ struct diff_baton *db = baton;
+
+ if (original_length)
+ SVN_ERR(blame_delete_range(db->chain, modified_start, original_length));
+
+ if (modified_length)
+ SVN_ERR(blame_insert_range(db->chain, db->rev, modified_start,
+ modified_length));
+
+ return SVN_NO_ERROR;
+}
+
+static const svn_diff_output_fns_t output_fns = {
+ NULL,
+ output_diff_modified
+};
+
+/* Add the blame for the diffs between LAST_FILE and CUR_FILE to CHAIN,
+ for revision REV. LAST_FILE may be NULL in which
+ case blame is added for every line of CUR_FILE. */
+static svn_error_t *
+add_file_blame(const char *last_file,
+ const char *cur_file,
+ struct blame_chain *chain,
+ struct rev *rev,
+ const svn_diff_file_options_t *diff_options,
+ apr_pool_t *pool)
+{
+ if (!last_file)
+ {
+ SVN_ERR_ASSERT(chain->blame == NULL);
+ chain->blame = blame_create(chain, rev, 0);
+ }
+ else
+ {
+ svn_diff_t *diff;
+ struct diff_baton diff_baton;
+
+ diff_baton.chain = chain;
+ diff_baton.rev = rev;
+
+ /* We have a previous file. Get the diff and adjust blame info. */
+ SVN_ERR(svn_diff_file_diff_2(&diff, last_file, cur_file,
+ diff_options, pool));
+ SVN_ERR(svn_diff_output(diff, &diff_baton, &output_fns));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* The delta window handler for the text delta between the previously seen
+ * revision and the revision currently being handled.
+ *
+ * Record the blame information for this revision in BATON->file_rev_baton.
+ *
+ * Implements svn_txdelta_window_handler_t.
+ */
+static svn_error_t *
+window_handler(svn_txdelta_window_t *window, void *baton)
+{
+ struct delta_baton *dbaton = baton;
+ struct file_rev_baton *frb = dbaton->file_rev_baton;
+ struct blame_chain *chain;
+
+ /* Call the wrapped handler first. */
+ SVN_ERR(dbaton->wrapped_handler(window, dbaton->wrapped_baton));
+
+ /* We patiently wait for the NULL window marking the end. */
+ if (window)
+ return SVN_NO_ERROR;
+
+ /* If we are including merged revisions, we need to add each rev to the
+ merged chain. */
+ if (frb->include_merged_revisions)
+ chain = frb->merged_chain;
+ else
+ chain = frb->chain;
+
+ /* Process this file. */
+ SVN_ERR(add_file_blame(frb->last_filename,
+ dbaton->filename, chain, frb->rev,
+ frb->diff_options, frb->currpool));
+
+ /* If we are including merged revisions, and the current revision is not a
+ merged one, we need to add its blame info to the chain for the original
+ line of history. */
+ if (frb->include_merged_revisions && ! frb->merged_revision)
+ {
+ apr_pool_t *tmppool;
+
+ SVN_ERR(add_file_blame(frb->last_original_filename,
+ dbaton->filename, frb->chain, frb->rev,
+ frb->diff_options, frb->currpool));
+
+ /* This filename could be around for a while, potentially, so
+ use the longer lifetime pool, and switch it with the previous one*/
+ svn_pool_clear(frb->prevfilepool);
+ tmppool = frb->filepool;
+ frb->filepool = frb->prevfilepool;
+ frb->prevfilepool = tmppool;
+
+ frb->last_original_filename = apr_pstrdup(frb->filepool,
+ dbaton->filename);
+ }
+
+ /* Prepare for next revision. */
+
+ /* Remember the file name so we can diff it with the next revision. */
+ frb->last_filename = dbaton->filename;
+
+ /* Switch pools. */
+ {
+ apr_pool_t *tmp_pool = frb->lastpool;
+ frb->lastpool = frb->currpool;
+ frb->currpool = tmp_pool;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Calculate and record blame information for one revision of the file,
+ * by comparing the file content against the previously seen revision.
+ *
+ * This handler is called once for each interesting revision of the file.
+ *
+ * Record the blame information for this revision in (file_rev_baton) BATON.
+ *
+ * Implements svn_file_rev_handler_t.
+ */
+static svn_error_t *
+file_rev_handler(void *baton, const char *path, svn_revnum_t revnum,
+ apr_hash_t *rev_props,
+ svn_boolean_t merged_revision,
+ svn_txdelta_window_handler_t *content_delta_handler,
+ void **content_delta_baton,
+ apr_array_header_t *prop_diffs,
+ apr_pool_t *pool)
+{
+ struct file_rev_baton *frb = baton;
+ svn_stream_t *last_stream;
+ svn_stream_t *cur_stream;
+ struct delta_baton *delta_baton;
+ apr_pool_t *filepool;
+
+ /* Clear the current pool. */
+ svn_pool_clear(frb->currpool);
+
+ if (frb->ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify_url(
+ svn_path_url_add_component2(frb->repos_root_url,
+ path+1, pool),
+ svn_wc_notify_blame_revision, pool);
+ notify->path = path;
+ notify->kind = svn_node_none;
+ notify->content_state = notify->prop_state
+ = svn_wc_notify_state_inapplicable;
+ notify->lock_state = svn_wc_notify_lock_state_inapplicable;
+ notify->revision = revnum;
+ notify->rev_props = rev_props;
+ frb->ctx->notify_func2(frb->ctx->notify_baton2, notify, pool);
+ }
+
+ if (frb->ctx->cancel_func)
+ SVN_ERR(frb->ctx->cancel_func(frb->ctx->cancel_baton));
+
+ /* If there were no content changes, we couldn't care less about this
+ revision now. Note that we checked the mime type above, so things
+ work if the user just changes the mime type in a commit.
+ Also note that we don't switch the pools in this case. This is important,
+ since the tempfile will be removed by the pool and we need the tempfile
+ from the last revision with content changes. */
+ if (!content_delta_handler)
+ return SVN_NO_ERROR;
+
+ frb->merged_revision = merged_revision;
+
+ /* Create delta baton. */
+ delta_baton = apr_palloc(frb->currpool, sizeof(*delta_baton));
+
+ /* Prepare the text delta window handler. */
+ if (frb->last_filename)
+ SVN_ERR(svn_stream_open_readonly(&last_stream, frb->last_filename,
+ frb->currpool, pool));
+ else
+ last_stream = svn_stream_empty(frb->currpool);
+
+ if (frb->include_merged_revisions && !frb->merged_revision)
+ filepool = frb->filepool;
+ else
+ filepool = frb->currpool;
+
+ SVN_ERR(svn_stream_open_unique(&cur_stream, &delta_baton->filename, NULL,
+ svn_io_file_del_on_pool_cleanup,
+ filepool, pool));
+
+ /* Get window handler for applying delta. */
+ svn_txdelta_apply(last_stream, cur_stream, NULL, NULL,
+ frb->currpool,
+ &delta_baton->wrapped_handler,
+ &delta_baton->wrapped_baton);
+
+ /* Wrap the window handler with our own. */
+ delta_baton->file_rev_baton = frb;
+ *content_delta_handler = window_handler;
+ *content_delta_baton = delta_baton;
+
+ /* Create the rev structure. */
+ frb->rev = apr_pcalloc(frb->mainpool, sizeof(struct rev));
+
+ if (revnum < frb->start_rev)
+ {
+ /* We shouldn't get more than one revision before the starting
+ revision (unless of including merged revisions). */
+ SVN_ERR_ASSERT((frb->last_filename == NULL)
+ || frb->include_merged_revisions);
+
+ /* The file existed before start_rev; generate no blame info for
+ lines from this revision (or before). */
+ frb->rev->revision = SVN_INVALID_REVNUM;
+ }
+ else
+ {
+ SVN_ERR_ASSERT(revnum <= frb->end_rev);
+
+ /* Set values from revision props. */
+ frb->rev->revision = revnum;
+ frb->rev->rev_props = svn_prop_hash_dup(rev_props, frb->mainpool);
+ }
+
+ if (frb->include_merged_revisions)
+ frb->rev->path = apr_pstrdup(frb->mainpool, path);
+
+ return SVN_NO_ERROR;
+}
+
+/* Ensure that CHAIN_ORIG and CHAIN_MERGED have the same number of chunks,
+ and that for every chunk C, CHAIN_ORIG[C] and CHAIN_MERGED[C] have the
+ same starting value. Both CHAIN_ORIG and CHAIN_MERGED should not be
+ NULL. */
+static void
+normalize_blames(struct blame_chain *chain,
+ struct blame_chain *chain_merged,
+ apr_pool_t *pool)
+{
+ struct blame *walk, *walk_merged;
+
+ /* Walk over the CHAIN's blame chunks and CHAIN_MERGED's blame chunks,
+ creating new chunks as needed. */
+ for (walk = chain->blame, walk_merged = chain_merged->blame;
+ walk->next && walk_merged->next;
+ walk = walk->next, walk_merged = walk_merged->next)
+ {
+ /* The current chunks should always be starting at the same offset. */
+ assert(walk->start == walk_merged->start);
+
+ if (walk->next->start < walk_merged->next->start)
+ {
+ /* insert a new chunk in CHAIN_MERGED. */
+ struct blame *tmp = blame_create(chain_merged, walk_merged->rev,
+ walk->next->start);
+ tmp->next = walk_merged->next;
+ walk_merged->next = tmp;
+ }
+
+ if (walk->next->start > walk_merged->next->start)
+ {
+ /* insert a new chunk in CHAIN. */
+ struct blame *tmp = blame_create(chain, walk->rev,
+ walk_merged->next->start);
+ tmp->next = walk->next;
+ walk->next = tmp;
+ }
+ }
+
+ /* If both NEXT pointers are null, the lists are equally long, otherwise
+ we need to extend one of them. If CHAIN is longer, append new chunks
+ to CHAIN_MERGED until its length matches that of CHAIN. */
+ while (walk->next != NULL)
+ {
+ struct blame *tmp = blame_create(chain_merged, walk_merged->rev,
+ walk->next->start);
+ walk_merged->next = tmp;
+
+ walk_merged = walk_merged->next;
+ walk = walk->next;
+ }
+
+ /* Same as above, only extend CHAIN to match CHAIN_MERGED. */
+ while (walk_merged->next != NULL)
+ {
+ struct blame *tmp = blame_create(chain, walk->rev,
+ walk_merged->next->start);
+ walk->next = tmp;
+
+ walk = walk->next;
+ walk_merged = walk_merged->next;
+ }
+}
+
+svn_error_t *
+svn_client_blame5(const char *target,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ const svn_diff_file_options_t *diff_options,
+ svn_boolean_t ignore_mime_type,
+ svn_boolean_t include_merged_revisions,
+ svn_client_blame_receiver3_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct file_rev_baton frb;
+ svn_ra_session_t *ra_session;
+ svn_revnum_t start_revnum, end_revnum;
+ svn_client__pathrev_t *end_loc;
+ struct blame *walk, *walk_merged = NULL;
+ apr_pool_t *iterpool;
+ svn_stream_t *last_stream;
+ svn_stream_t *stream;
+ const char *target_abspath_or_url;
+
+ if (start->kind == svn_opt_revision_unspecified
+ || end->kind == svn_opt_revision_unspecified)
+ return svn_error_create
+ (SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
+
+ if (svn_path_is_url(target))
+ target_abspath_or_url = target;
+ else
+ SVN_ERR(svn_dirent_get_absolute(&target_abspath_or_url, target, pool));
+
+ /* Get an RA plugin for this filesystem object. */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &end_loc,
+ target, NULL, peg_revision, end,
+ ctx, pool));
+ end_revnum = end_loc->rev;
+
+ SVN_ERR(svn_client__get_revision_number(&start_revnum, NULL, ctx->wc_ctx,
+ target_abspath_or_url, ra_session,
+ start, pool));
+
+ if (end_revnum < start_revnum)
+ return svn_error_create
+ (SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Start revision must precede end revision"));
+
+ /* We check the mime-type of the yougest revision before getting all
+ the older revisions. */
+ if (!ignore_mime_type)
+ {
+ apr_hash_t *props;
+ apr_hash_index_t *hi;
+
+ SVN_ERR(svn_client_propget5(&props, NULL, SVN_PROP_MIME_TYPE,
+ target_abspath_or_url, peg_revision,
+ end, NULL, svn_depth_empty, NULL, ctx,
+ pool, pool));
+
+ /* props could be keyed on URLs or paths depending on the
+ peg_revision and end values so avoid using the key. */
+ hi = apr_hash_first(pool, props);
+ if (hi)
+ {
+ svn_string_t *value;
+
+ /* Should only be one value */
+ SVN_ERR_ASSERT(apr_hash_count(props) == 1);
+
+ value = svn__apr_hash_index_val(hi);
+ if (value && svn_mime_type_is_binary(value->data))
+ return svn_error_createf
+ (SVN_ERR_CLIENT_IS_BINARY_FILE, 0,
+ _("Cannot calculate blame information for binary file '%s'"),
+ (svn_path_is_url(target)
+ ? target : svn_dirent_local_style(target, pool)));
+ }
+ }
+
+ frb.start_rev = start_revnum;
+ frb.end_rev = end_revnum;
+ frb.target = target;
+ frb.ctx = ctx;
+ frb.diff_options = diff_options;
+ frb.include_merged_revisions = include_merged_revisions;
+ frb.last_filename = NULL;
+ frb.last_original_filename = NULL;
+ frb.chain = apr_palloc(pool, sizeof(*frb.chain));
+ frb.chain->blame = NULL;
+ frb.chain->avail = NULL;
+ frb.chain->pool = pool;
+ if (include_merged_revisions)
+ {
+ frb.merged_chain = apr_palloc(pool, sizeof(*frb.merged_chain));
+ frb.merged_chain->blame = NULL;
+ frb.merged_chain->avail = NULL;
+ frb.merged_chain->pool = pool;
+ }
+
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &frb.repos_root_url, pool));
+
+ frb.mainpool = pool;
+ /* The callback will flip the following two pools, because it needs
+ information from the previous call. Obviously, it can't rely on
+ the lifetime of the pool provided by get_file_revs. */
+ frb.lastpool = svn_pool_create(pool);
+ frb.currpool = svn_pool_create(pool);
+ if (include_merged_revisions)
+ {
+ frb.filepool = svn_pool_create(pool);
+ frb.prevfilepool = svn_pool_create(pool);
+ }
+
+ /* Collect all blame information.
+ We need to ensure that we get one revision before the start_rev,
+ if available so that we can know what was actually changed in the start
+ revision. */
+ SVN_ERR(svn_ra_get_file_revs2(ra_session, "",
+ start_revnum - (start_revnum > 0 ? 1 : 0),
+ end_revnum, include_merged_revisions,
+ file_rev_handler, &frb, pool));
+
+ if (end->kind == svn_opt_revision_working)
+ {
+ /* If the local file is modified we have to call the handler on the
+ working copy file with keywords unexpanded */
+ svn_wc_status3_t *status;
+
+ SVN_ERR(svn_wc_status3(&status, ctx->wc_ctx, target_abspath_or_url, pool,
+ pool));
+
+ if (status->text_status != svn_wc_status_normal
+ || (status->prop_status != svn_wc_status_normal
+ && status->prop_status != svn_wc_status_none))
+ {
+ svn_stream_t *wcfile;
+ svn_stream_t *tempfile;
+ svn_opt_revision_t rev;
+ svn_boolean_t normalize_eols = FALSE;
+ const char *temppath;
+
+ if (status->prop_status != svn_wc_status_none)
+ {
+ const svn_string_t *eol_style;
+ SVN_ERR(svn_wc_prop_get2(&eol_style, ctx->wc_ctx,
+ target_abspath_or_url,
+ SVN_PROP_EOL_STYLE,
+ pool, pool));
+
+ if (eol_style)
+ {
+ svn_subst_eol_style_t style;
+ const char *eol;
+ svn_subst_eol_style_from_value(&style, &eol, eol_style->data);
+
+ normalize_eols = (style == svn_subst_eol_style_native);
+ }
+ }
+
+ rev.kind = svn_opt_revision_working;
+ SVN_ERR(svn_client__get_normalized_stream(&wcfile, ctx->wc_ctx,
+ target_abspath_or_url, &rev,
+ FALSE, normalize_eols,
+ ctx->cancel_func,
+ ctx->cancel_baton,
+ pool, pool));
+
+ SVN_ERR(svn_stream_open_unique(&tempfile, &temppath, NULL,
+ svn_io_file_del_on_pool_cleanup,
+ pool, pool));
+
+ SVN_ERR(svn_stream_copy3(wcfile, tempfile, ctx->cancel_func,
+ ctx->cancel_baton, pool));
+
+ SVN_ERR(add_file_blame(frb.last_filename, temppath, frb.chain, NULL,
+ frb.diff_options, pool));
+
+ frb.last_filename = temppath;
+ }
+ }
+
+ /* Report the blame to the caller. */
+
+ /* The callback has to have been called at least once. */
+ SVN_ERR_ASSERT(frb.last_filename != NULL);
+
+ /* Create a pool for the iteration below. */
+ iterpool = svn_pool_create(pool);
+
+ /* Open the last file and get a stream. */
+ SVN_ERR(svn_stream_open_readonly(&last_stream, frb.last_filename,
+ pool, pool));
+ stream = svn_subst_stream_translated(last_stream,
+ "\n", TRUE, NULL, FALSE, pool);
+
+ /* Perform optional merged chain normalization. */
+ if (include_merged_revisions)
+ {
+ /* If we never created any blame for the original chain, create it now,
+ with the most recent changed revision. This could occur if a file
+ was created on a branch and them merged to another branch. This is
+ semanticly a copy, and we want to use the revision on the branch as
+ the most recently changed revision. ### Is this really what we want
+ to do here? Do the sematics of copy change? */
+ if (!frb.chain->blame)
+ frb.chain->blame = blame_create(frb.chain, frb.rev, 0);
+
+ normalize_blames(frb.chain, frb.merged_chain, pool);
+ walk_merged = frb.merged_chain->blame;
+ }
+
+ /* Process each blame item. */
+ for (walk = frb.chain->blame; walk; walk = walk->next)
+ {
+ apr_off_t line_no;
+ svn_revnum_t merged_rev;
+ const char *merged_path;
+ apr_hash_t *merged_rev_props;
+
+ if (walk_merged)
+ {
+ merged_rev = walk_merged->rev->revision;
+ merged_rev_props = walk_merged->rev->rev_props;
+ merged_path = walk_merged->rev->path;
+ }
+ else
+ {
+ merged_rev = SVN_INVALID_REVNUM;
+ merged_rev_props = NULL;
+ merged_path = NULL;
+ }
+
+ for (line_no = walk->start;
+ !walk->next || line_no < walk->next->start;
+ ++line_no)
+ {
+ svn_boolean_t eof;
+ svn_stringbuf_t *sb;
+
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_stream_readline(stream, &sb, "\n", &eof, iterpool));
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+ if (!eof || sb->len)
+ {
+ if (walk->rev)
+ SVN_ERR(receiver(receiver_baton, start_revnum, end_revnum,
+ line_no, walk->rev->revision,
+ walk->rev->rev_props, merged_rev,
+ merged_rev_props, merged_path,
+ sb->data, FALSE, iterpool));
+ else
+ SVN_ERR(receiver(receiver_baton, start_revnum, end_revnum,
+ line_no, SVN_INVALID_REVNUM,
+ NULL, SVN_INVALID_REVNUM,
+ NULL, NULL,
+ sb->data, TRUE, iterpool));
+ }
+ if (eof) break;
+ }
+
+ if (walk_merged)
+ walk_merged = walk_merged->next;
+ }
+
+ SVN_ERR(svn_stream_close(stream));
+
+ svn_pool_destroy(frb.lastpool);
+ svn_pool_destroy(frb.currpool);
+ if (include_merged_revisions)
+ {
+ svn_pool_destroy(frb.filepool);
+ svn_pool_destroy(frb.prevfilepool);
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/cat.c b/subversion/libsvn_client/cat.c
new file mode 100644
index 0000000..7c58f88
--- /dev/null
+++ b/subversion/libsvn_client/cat.c
@@ -0,0 +1,308 @@
+/*
+ * cat.c: implementation of the 'cat' command
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_hash.h"
+#include "svn_client.h"
+#include "svn_string.h"
+#include "svn_error.h"
+#include "svn_subst.h"
+#include "svn_io.h"
+#include "svn_time.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_props.h"
+#include "client.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+
+
+/*** Code. ***/
+
+svn_error_t *
+svn_client__get_normalized_stream(svn_stream_t **normal_stream,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t expand_keywords,
+ svn_boolean_t normalize_eols,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *kw = NULL;
+ svn_subst_eol_style_t style;
+ apr_hash_t *props;
+ svn_string_t *eol_style, *keywords, *special;
+ const char *eol = NULL;
+ svn_boolean_t local_mod = FALSE;
+ svn_stream_t *input;
+ svn_node_kind_t kind;
+
+ SVN_ERR_ASSERT(SVN_CLIENT__REVKIND_IS_LOCAL_TO_WC(revision->kind));
+
+ SVN_ERR(svn_wc_read_kind2(&kind, wc_ctx, local_abspath,
+ (revision->kind != svn_opt_revision_working),
+ FALSE, scratch_pool));
+
+ if (kind == svn_node_unknown || kind == svn_node_none)
+ return svn_error_createf(SVN_ERR_UNVERSIONED_RESOURCE, NULL,
+ _("'%s' is not under version control"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+ if (kind != svn_node_file)
+ return svn_error_createf(SVN_ERR_CLIENT_IS_DIRECTORY, NULL,
+ _("'%s' refers to a directory"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+
+ if (revision->kind != svn_opt_revision_working)
+ {
+ SVN_ERR(svn_wc_get_pristine_contents2(&input, wc_ctx, local_abspath,
+ result_pool, scratch_pool));
+ if (input == NULL)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' has no pristine version until it is committed"),
+ svn_dirent_local_style(local_abspath, scratch_pool));
+
+ SVN_ERR(svn_wc_get_pristine_props(&props, wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+ }
+ else
+ {
+ svn_wc_status3_t *status;
+
+ SVN_ERR(svn_stream_open_readonly(&input, local_abspath, scratch_pool,
+ result_pool));
+
+ SVN_ERR(svn_wc_prop_list2(&props, wc_ctx, local_abspath, scratch_pool,
+ scratch_pool));
+ SVN_ERR(svn_wc_status3(&status, wc_ctx, local_abspath, scratch_pool,
+ scratch_pool));
+ if (status->node_status != svn_wc_status_normal)
+ local_mod = TRUE;
+ }
+
+ eol_style = svn_hash_gets(props, SVN_PROP_EOL_STYLE);
+ keywords = svn_hash_gets(props, SVN_PROP_KEYWORDS);
+ special = svn_hash_gets(props, SVN_PROP_SPECIAL);
+
+ if (eol_style)
+ svn_subst_eol_style_from_value(&style, &eol, eol_style->data);
+
+ if (keywords)
+ {
+ svn_revnum_t changed_rev;
+ const char *rev_str;
+ const char *author;
+ const char *url;
+ apr_time_t tm;
+ const char *repos_root_url;
+ const char *repos_relpath;
+
+ SVN_ERR(svn_wc__node_get_changed_info(&changed_rev, &tm, &author, wc_ctx,
+ local_abspath, scratch_pool,
+ scratch_pool));
+ SVN_ERR(svn_wc__node_get_repos_info(NULL, &repos_relpath, &repos_root_url,
+ NULL,
+ wc_ctx, local_abspath, scratch_pool,
+ scratch_pool));
+ url = svn_path_url_add_component2(repos_root_url, repos_relpath,
+ scratch_pool);
+
+ if (local_mod)
+ {
+ /* For locally modified files, we'll append an 'M'
+ to the revision number, and set the author to
+ "(local)" since we can't always determine the
+ current user's username */
+ rev_str = apr_psprintf(scratch_pool, "%ldM", changed_rev);
+ author = _("(local)");
+
+ if (! special)
+ {
+ /* Use the modified time from the working copy for files */
+ SVN_ERR(svn_io_file_affected_time(&tm, local_abspath,
+ scratch_pool));
+ }
+ }
+ else
+ {
+ rev_str = apr_psprintf(scratch_pool, "%ld", changed_rev);
+ }
+
+ SVN_ERR(svn_subst_build_keywords3(&kw, keywords->data, rev_str, url,
+ repos_root_url, tm, author,
+ scratch_pool));
+ }
+
+ /* Wrap the output stream if translation is needed. */
+ if (eol != NULL || kw != NULL)
+ input = svn_subst_stream_translated(
+ input,
+ (eol_style && normalize_eols) ? SVN_SUBST_NATIVE_EOL_STR : eol,
+ FALSE, kw, expand_keywords, result_pool);
+
+ *normal_stream = input;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_cat2(svn_stream_t *out,
+ const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *ra_session;
+ svn_client__pathrev_t *loc;
+ svn_string_t *eol_style;
+ svn_string_t *keywords;
+ apr_hash_t *props;
+ const char *repos_root_url;
+ svn_stream_t *output = out;
+ svn_error_t *err;
+
+ /* ### Inconsistent default revision logic in this command. */
+ if (peg_revision->kind == svn_opt_revision_unspecified)
+ {
+ peg_revision = svn_cl__rev_default_to_head_or_working(peg_revision,
+ path_or_url);
+ revision = svn_cl__rev_default_to_head_or_base(revision, path_or_url);
+ }
+ else
+ {
+ peg_revision = svn_cl__rev_default_to_head_or_working(peg_revision,
+ path_or_url);
+ revision = svn_cl__rev_default_to_peg(revision, peg_revision);
+ }
+
+ if (! svn_path_is_url(path_or_url)
+ && SVN_CLIENT__REVKIND_IS_LOCAL_TO_WC(peg_revision->kind)
+ && SVN_CLIENT__REVKIND_IS_LOCAL_TO_WC(revision->kind))
+ {
+ const char *local_abspath;
+ svn_stream_t *normal_stream;
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path_or_url, pool));
+ SVN_ERR(svn_client__get_normalized_stream(&normal_stream, ctx->wc_ctx,
+ local_abspath, revision, TRUE, FALSE,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool, pool));
+
+ /* We don't promise to close output, so disown it to ensure we don't. */
+ output = svn_stream_disown(output, pool);
+
+ return svn_error_trace(svn_stream_copy3(normal_stream, output,
+ ctx->cancel_func,
+ ctx->cancel_baton, pool));
+ }
+
+ /* Get an RA plugin for this filesystem object. */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &loc,
+ path_or_url, NULL,
+ peg_revision,
+ revision, ctx, pool));
+
+ /* Find the repos root URL */
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root_url, pool));
+
+ /* Grab some properties we need to know in order to figure out if anything
+ special needs to be done with this file. */
+ err = svn_ra_get_file(ra_session, "", loc->rev, NULL, NULL, &props, pool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_FS_NOT_FILE)
+ {
+ return svn_error_createf(SVN_ERR_CLIENT_IS_DIRECTORY, err,
+ _("URL '%s' refers to a directory"),
+ loc->url);
+ }
+ else
+ {
+ return svn_error_trace(err);
+ }
+ }
+
+ eol_style = svn_hash_gets(props, SVN_PROP_EOL_STYLE);
+ keywords = svn_hash_gets(props, SVN_PROP_KEYWORDS);
+
+ if (eol_style || keywords)
+ {
+ /* It's a file with no special eol style or keywords. */
+ svn_subst_eol_style_t eol;
+ const char *eol_str;
+ apr_hash_t *kw;
+
+ if (eol_style)
+ svn_subst_eol_style_from_value(&eol, &eol_str, eol_style->data);
+ else
+ {
+ eol = svn_subst_eol_style_none;
+ eol_str = NULL;
+ }
+
+
+ if (keywords)
+ {
+ svn_string_t *cmt_rev, *cmt_date, *cmt_author;
+ apr_time_t when = 0;
+
+ cmt_rev = svn_hash_gets(props, SVN_PROP_ENTRY_COMMITTED_REV);
+ cmt_date = svn_hash_gets(props, SVN_PROP_ENTRY_COMMITTED_DATE);
+ cmt_author = svn_hash_gets(props, SVN_PROP_ENTRY_LAST_AUTHOR);
+ if (cmt_date)
+ SVN_ERR(svn_time_from_cstring(&when, cmt_date->data, pool));
+
+ SVN_ERR(svn_subst_build_keywords3(&kw, keywords->data,
+ cmt_rev->data, loc->url,
+ repos_root_url, when,
+ cmt_author ?
+ cmt_author->data : NULL,
+ pool));
+ }
+ else
+ kw = NULL;
+
+ /* Interject a translating stream */
+ output = svn_subst_stream_translated(svn_stream_disown(out, pool),
+ eol_str, FALSE, kw, TRUE, pool);
+ }
+
+ SVN_ERR(svn_ra_get_file(ra_session, "", loc->rev, output, NULL, NULL, pool));
+
+ if (out != output)
+ /* Close the interjected stream */
+ SVN_ERR(svn_stream_close(output));
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/changelist.c b/subversion/libsvn_client/changelist.c
new file mode 100644
index 0000000..fc4d987
--- /dev/null
+++ b/subversion/libsvn_client/changelist.c
@@ -0,0 +1,144 @@
+/*
+ * changelist.c: implementation of the 'changelist' command
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_client.h"
+#include "svn_wc.h"
+#include "svn_pools.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_hash.h"
+
+#include "client.h"
+#include "private/svn_wc_private.h"
+#include "svn_private_config.h"
+
+
+
+
+svn_error_t *
+svn_client_add_to_changelist(const apr_array_header_t *paths,
+ const char *changelist,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(pool);
+ int i;
+
+ if (changelist[0] == '\0')
+ return svn_error_create(SVN_ERR_BAD_CHANGELIST_NAME, NULL,
+ _("Target changelist name must not be empty"));
+
+ for (i = 0; i < paths->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(paths, i, const char *);
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+ }
+
+ for (i = 0; i < paths->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(paths, i, const char *);
+ const char *local_abspath;
+
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, iterpool));
+
+ SVN_ERR(svn_wc_set_changelist2(ctx->wc_ctx, local_abspath, changelist,
+ depth, changelists,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ iterpool));
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client_remove_from_changelists(const apr_array_header_t *paths,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(pool);
+ int i;
+
+ for (i = 0; i < paths->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(paths, i, const char *);
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+ }
+
+ for (i = 0; i < paths->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(paths, i, const char *);
+ const char *local_abspath;
+
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, iterpool));
+
+ SVN_ERR(svn_wc_set_changelist2(ctx->wc_ctx, local_abspath, NULL,
+ depth, changelists,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ iterpool));
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client_get_changelists(const char *path,
+ const apr_array_header_t *changelists,
+ svn_depth_t depth,
+ svn_changelist_receiver_t callback_func,
+ void *callback_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *local_abspath;
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, pool));
+
+ SVN_ERR(svn_wc_get_changelists(ctx->wc_ctx, local_abspath, depth, changelists,
+ callback_func, callback_baton,
+ ctx->cancel_func, ctx->cancel_baton, pool));
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/checkout.c b/subversion/libsvn_client/checkout.c
new file mode 100644
index 0000000..41be776
--- /dev/null
+++ b/subversion/libsvn_client/checkout.c
@@ -0,0 +1,198 @@
+/*
+ * checkout.c: wrappers around wc checkout functionality
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_pools.h"
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "svn_ra.h"
+#include "svn_types.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_io.h"
+#include "svn_opt.h"
+#include "svn_time.h"
+#include "client.h"
+
+#include "private/svn_wc_private.h"
+
+#include "svn_private_config.h"
+
+
+/*** Public Interfaces. ***/
+
+static svn_error_t *
+initialize_area(const char *local_abspath,
+ const svn_client__pathrev_t *pathrev,
+ svn_depth_t depth,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ if (depth == svn_depth_unknown)
+ depth = svn_depth_infinity;
+
+ /* Make the unversioned directory into a versioned one. */
+ SVN_ERR(svn_wc_ensure_adm4(ctx->wc_ctx, local_abspath, pathrev->url,
+ pathrev->repos_root_url, pathrev->repos_uuid,
+ pathrev->rev, depth, pool));
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__checkout_internal(svn_revnum_t *result_rev,
+ const char *url,
+ const char *local_abspath,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_boolean_t *timestamp_sleep,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_node_kind_t kind;
+ apr_pool_t *session_pool = svn_pool_create(pool);
+ svn_ra_session_t *ra_session;
+ svn_client__pathrev_t *pathrev;
+
+ /* Sanity check. Without these, the checkout is meaningless. */
+ SVN_ERR_ASSERT(local_abspath != NULL);
+ SVN_ERR_ASSERT(svn_uri_is_canonical(url, pool));
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ /* Fulfill the docstring promise of svn_client_checkout: */
+ if ((revision->kind != svn_opt_revision_number)
+ && (revision->kind != svn_opt_revision_date)
+ && (revision->kind != svn_opt_revision_head))
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
+
+ /* Get the RA connection. */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &pathrev,
+ url, NULL, peg_revision, revision,
+ ctx, session_pool));
+
+ pathrev = svn_client__pathrev_dup(pathrev, pool);
+ SVN_ERR(svn_ra_check_path(ra_session, "", pathrev->rev, &kind, pool));
+
+ svn_pool_destroy(session_pool);
+
+ if (kind == svn_node_none)
+ return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
+ _("URL '%s' doesn't exist"), pathrev->url);
+ else if (kind == svn_node_file)
+ return svn_error_createf
+ (SVN_ERR_UNSUPPORTED_FEATURE , NULL,
+ _("URL '%s' refers to a file, not a directory"), pathrev->url);
+
+ SVN_ERR(svn_io_check_path(local_abspath, &kind, pool));
+
+ if (kind == svn_node_none)
+ {
+ /* Bootstrap: create an incomplete working-copy root dir. Its
+ entries file should only have an entry for THIS_DIR with a
+ URL, revnum, and an 'incomplete' flag. */
+ SVN_ERR(svn_io_make_dir_recursively(local_abspath, pool));
+ SVN_ERR(initialize_area(local_abspath, pathrev, depth, ctx, pool));
+ }
+ else if (kind == svn_node_dir)
+ {
+ int wc_format;
+ const char *entry_url;
+
+ SVN_ERR(svn_wc_check_wc2(&wc_format, ctx->wc_ctx, local_abspath, pool));
+ if (! wc_format)
+ {
+ SVN_ERR(initialize_area(local_abspath, pathrev, depth, ctx, pool));
+ }
+ else
+ {
+ /* Get PATH's URL. */
+ SVN_ERR(svn_wc__node_get_url(&entry_url, ctx->wc_ctx, local_abspath,
+ pool, pool));
+
+ /* If PATH's existing URL matches the incoming one, then
+ just update. This allows 'svn co' to restart an
+ interrupted checkout. Otherwise bail out. */
+ if (strcmp(entry_url, pathrev->url) != 0)
+ return svn_error_createf(
+ SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
+ _("'%s' is already a working copy for a"
+ " different URL"),
+ svn_dirent_local_style(local_abspath, pool));
+ }
+ }
+ else
+ {
+ return svn_error_createf(SVN_ERR_WC_NODE_KIND_CHANGE, NULL,
+ _("'%s' already exists and is not a directory"),
+ svn_dirent_local_style(local_abspath, pool));
+ }
+
+ /* Have update fix the incompleteness. */
+ SVN_ERR(svn_client__update_internal(result_rev, local_abspath,
+ revision, depth, TRUE,
+ ignore_externals,
+ allow_unver_obstructions,
+ TRUE /* adds_as_modification */,
+ FALSE, FALSE,
+ timestamp_sleep, ctx, pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_checkout3(svn_revnum_t *result_rev,
+ const char *URL,
+ const char *path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *local_abspath;
+ svn_error_t *err;
+ svn_boolean_t sleep_here = FALSE;
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, pool));
+
+ err = svn_client__checkout_internal(result_rev, URL, local_abspath,
+ peg_revision, revision, depth,
+ ignore_externals,
+ allow_unver_obstructions, &sleep_here,
+ ctx, pool);
+ if (sleep_here)
+ svn_io_sleep_for_timestamps(local_abspath, pool);
+
+ return svn_error_trace(err);
+}
diff --git a/subversion/libsvn_client/cleanup.c b/subversion/libsvn_client/cleanup.c
new file mode 100644
index 0000000..b15e824
--- /dev/null
+++ b/subversion/libsvn_client/cleanup.c
@@ -0,0 +1,63 @@
+/*
+ * cleanup.c: wrapper around wc cleanup functionality.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_time.h"
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "svn_config.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_pools.h"
+#include "client.h"
+#include "svn_props.h"
+
+#include "svn_private_config.h"
+
+
+/*** Code. ***/
+
+svn_error_t *
+svn_client_cleanup(const char *path,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ const char *local_abspath;
+ svn_error_t *err;
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, scratch_pool));
+
+ err = svn_wc_cleanup3(ctx->wc_ctx, local_abspath, ctx->cancel_func,
+ ctx->cancel_baton, scratch_pool);
+ svn_io_sleep_for_timestamps(path, scratch_pool);
+ return svn_error_trace(err);
+}
diff --git a/subversion/libsvn_client/client.h b/subversion/libsvn_client/client.h
new file mode 100644
index 0000000..9ea25f2
--- /dev/null
+++ b/subversion/libsvn_client/client.h
@@ -0,0 +1,1124 @@
+/*
+ * client.h : shared stuff internal to the client library.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+
+#ifndef SVN_LIBSVN_CLIENT_H
+#define SVN_LIBSVN_CLIENT_H
+
+
+#include <apr_pools.h>
+
+#include "svn_types.h"
+#include "svn_opt.h"
+#include "svn_string.h"
+#include "svn_error.h"
+#include "svn_ra.h"
+#include "svn_client.h"
+
+#include "private/svn_magic.h"
+#include "private/svn_client_private.h"
+#include "private/svn_diff_tree.h"
+#include "private/svn_editor.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+/* Set *REVNUM to the revision number identified by REVISION.
+
+ If REVISION->kind is svn_opt_revision_number, just use
+ REVISION->value.number, ignoring LOCAL_ABSPATH and RA_SESSION.
+
+ Else if REVISION->kind is svn_opt_revision_committed,
+ svn_opt_revision_previous, or svn_opt_revision_base, or
+ svn_opt_revision_working, then the revision can be identified
+ purely based on the working copy's administrative information for
+ LOCAL_ABSPATH, so RA_SESSION is ignored. If LOCAL_ABSPATH is not
+ under revision control, return SVN_ERR_UNVERSIONED_RESOURCE, or if
+ LOCAL_ABSPATH is null, return SVN_ERR_CLIENT_VERSIONED_PATH_REQUIRED.
+
+ Else if REVISION->kind is svn_opt_revision_date or
+ svn_opt_revision_head, then RA_SESSION is used to retrieve the
+ revision from the repository (using REVISION->value.date in the
+ former case), and LOCAL_ABSPATH is ignored. If RA_SESSION is null,
+ return SVN_ERR_CLIENT_RA_ACCESS_REQUIRED.
+
+ Else if REVISION->kind is svn_opt_revision_unspecified, set
+ *REVNUM to SVN_INVALID_REVNUM.
+
+ If YOUNGEST_REV is non-NULL, it is an in/out parameter. If
+ *YOUNGEST_REV is valid, use it as the youngest revision in the
+ repository (regardless of reality) -- don't bother to lookup the
+ true value for HEAD, and don't return any value in *REVNUM greater
+ than *YOUNGEST_REV. If *YOUNGEST_REV is not valid, and a HEAD
+ lookup is required to populate *REVNUM, then also populate
+ *YOUNGEST_REV with the result. This is useful for making multiple
+ serialized calls to this function with a basically static view of
+ the repository, avoiding race conditions which could occur between
+ multiple invocations with HEAD lookup requests.
+
+ Else return SVN_ERR_CLIENT_BAD_REVISION.
+
+ Use SCRATCH_POOL for any temporary allocation. */
+svn_error_t *
+svn_client__get_revision_number(svn_revnum_t *revnum,
+ svn_revnum_t *youngest_rev,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ svn_ra_session_t *ra_session,
+ const svn_opt_revision_t *revision,
+ apr_pool_t *scratch_pool);
+
+/* Set *ORIGINAL_REPOS_RELPATH and *ORIGINAL_REVISION to the original location
+ that served as the source of the copy from which PATH_OR_URL at REVISION was
+ created, or NULL and SVN_INVALID_REVNUM (respectively) if PATH_OR_URL at
+ REVISION was not the result of a copy operation. */
+svn_error_t *
+svn_client__get_copy_source(const char **original_repos_relpath,
+ svn_revnum_t *original_revision,
+ const char *path_or_url,
+ const svn_opt_revision_t *revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Set *START_URL and *START_REVISION (and maybe *END_URL
+ and *END_REVISION) to the revisions and repository URLs of one
+ (or two) points of interest along a particular versioned resource's
+ line of history. PATH as it exists in "peg revision"
+ REVISION identifies that line of history, and START and END
+ specify the point(s) of interest (typically the revisions referred
+ to as the "operative range" for a given operation) along that history.
+
+ START_REVISION and/or END_REVISION may be NULL if not wanted.
+ END may be NULL or of kind svn_opt_revision_unspecified (in either case
+ END_URL and END_REVISION are not touched by the function);
+ START and REVISION may not.
+
+ If PATH is a WC path and REVISION is of kind svn_opt_revision_working,
+ then look at the PATH's copy-from URL instead of its base URL.
+
+ RA_SESSION should be an open RA session pointing at the URL of PATH,
+ or NULL, in which case this function will open its own temporary session.
+
+ A NOTE ABOUT FUTURE REPORTING:
+
+ If either START or END are greater than REVISION, then do a
+ sanity check (since we cannot search future history yet): verify
+ that PATH in the future revision(s) is the "same object" as the
+ one pegged by REVISION. In other words, all three objects must
+ be connected by a single line of history which exactly passes
+ through PATH at REVISION. If this sanity check fails, return
+ SVN_ERR_CLIENT_UNRELATED_RESOURCES. If PATH doesn't exist in the future
+ revision, SVN_ERR_FS_NOT_FOUND may also be returned.
+
+ CTX is the client context baton.
+
+ Use POOL for all allocations. */
+svn_error_t *
+svn_client__repos_locations(const char **start_url,
+ svn_revnum_t *start_revision,
+ const char **end_url,
+ svn_revnum_t *end_revision,
+ svn_ra_session_t *ra_session,
+ const char *path,
+ const svn_opt_revision_t *revision,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* Trace a line of history of a particular versioned resource back to a
+ * specific revision.
+ *
+ * Set *OP_LOC_P to the location that the object PEG_LOC had in
+ * revision OP_REVNUM.
+ *
+ * RA_SESSION is an open RA session to the correct repository; it may be
+ * temporarily reparented inside this function. */
+svn_error_t *
+svn_client__repos_location(svn_client__pathrev_t **op_loc_p,
+ svn_ra_session_t *ra_session,
+ const svn_client__pathrev_t *peg_loc,
+ svn_revnum_t op_revnum,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+
+/* Set *SEGMENTS to an array of svn_location_segment_t * objects, each
+ representing a reposition location segment for the history of URL
+ in PEG_REVISION
+ between END_REVISION and START_REVISION, ordered from oldest
+ segment to youngest. *SEGMENTS may be empty but it will never
+ be NULL.
+
+ This is basically a thin de-stream-ifying wrapper around the
+ svn_ra_get_location_segments() interface, which see for the rules
+ governing PEG_REVISION, START_REVISION, and END_REVISION.
+
+ RA_SESSION is an RA session open to the repository of URL; it may be
+ temporarily reparented within this function.
+
+ CTX is the client context baton.
+
+ Use POOL for all allocations. */
+svn_error_t *
+svn_client__repos_location_segments(apr_array_header_t **segments,
+ svn_ra_session_t *ra_session,
+ const char *url,
+ svn_revnum_t peg_revision,
+ svn_revnum_t start_revision,
+ svn_revnum_t end_revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+
+/* Find the common ancestor of two locations in a repository.
+ Ancestry is determined by the 'copy-from' relationship and the normal
+ successor relationship.
+
+ Set *ANCESTOR_P to the location of the youngest common ancestor of
+ LOC1 and LOC2. If the locations have no common ancestor (including if
+ they don't have the same repository root URL), set *ANCESTOR_P to NULL.
+
+ If SESSION is not NULL, use it for retrieving the common ancestor instead
+ of creating a new session.
+
+ Use the authentication baton cached in CTX to authenticate against
+ the repository. Use POOL for all allocations.
+
+ See also svn_client__youngest_common_ancestor().
+*/
+svn_error_t *
+svn_client__get_youngest_common_ancestor(svn_client__pathrev_t **ancestor_p,
+ const svn_client__pathrev_t *loc1,
+ const svn_client__pathrev_t *loc2,
+ svn_ra_session_t *session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Ensure that RA_SESSION's session URL matches SESSION_URL,
+ reparenting that session if necessary.
+ Store the previous session URL in *OLD_SESSION_URL (so that if the
+ reparenting is meant to be temporary, the caller can reparent the
+ session back to where it was).
+
+ If SESSION_URL is NULL, treat this as a magic value meaning "point
+ the RA session to the root of the repository".
+
+ NOTE: The typical usage pattern for this functions is:
+
+ const char *old_session_url;
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url,
+ ra_session,
+ new_session_url,
+ pool);
+
+ [...]
+
+ SVN_ERR(svn_ra_reparent(ra_session, old_session_url, pool));
+*/
+svn_error_t *
+svn_client__ensure_ra_session_url(const char **old_session_url,
+ svn_ra_session_t *ra_session,
+ const char *session_url,
+ apr_pool_t *pool);
+
+/* ---------------------------------------------------------------- */
+
+/*** RA callbacks ***/
+
+
+/* CTX is of type "svn_client_ctx_t *". */
+#define SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx) \
+ ((ctx)->log_msg_func3 || (ctx)->log_msg_func2 || (ctx)->log_msg_func)
+
+/* Open an RA session, returning it in *RA_SESSION or a corrected URL
+ in *CORRECTED_URL. (This function mirrors svn_ra_open4(), which
+ see, regarding the interpretation and handling of these two parameters.)
+
+ The root of the session is specified by BASE_URL and BASE_DIR_ABSPATH.
+
+ Additional control parameters:
+
+ - COMMIT_ITEMS is an array of svn_client_commit_item_t *
+ structures, present only for working copy commits, NULL otherwise.
+
+ - WRITE_DAV_PROPS indicates that the RA layer can clear and write
+ the DAV properties in the working copy of BASE_DIR_ABSPATH.
+
+ - READ_DAV_PROPS indicates that the RA layer should not attempt to
+ modify the WC props directly, but is still allowed to read them.
+
+ BASE_DIR_ABSPATH may be NULL if the RA operation does not correspond to a
+ working copy (in which case, WRITE_DAV_PROPS and READ_DAV_PROPS must be
+ FALSE.
+
+ If WRITE_DAV_PROPS and READ_DAV_PROPS are both FALSE the working copy may
+ still be used for locating pristine files via their SHA1.
+
+ The calling application's authentication baton is provided in CTX,
+ and allocations related to this session are performed in POOL.
+
+ NOTE: The reason for the _internal suffix of this function's name is to
+ avoid confusion with the public API svn_client_open_ra_session(). */
+svn_error_t *
+svn_client__open_ra_session_internal(svn_ra_session_t **ra_session,
+ const char **corrected_url,
+ const char *base_url,
+ const char *base_dir_abspath,
+ const apr_array_header_t *commit_items,
+ svn_boolean_t write_dav_props,
+ svn_boolean_t read_dav_props,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+
+svn_error_t *
+svn_client__ra_provide_base(svn_stream_t **contents,
+ svn_revnum_t *revision,
+ void *baton,
+ const char *repos_relpath,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+
+svn_error_t *
+svn_client__ra_provide_props(apr_hash_t **props,
+ svn_revnum_t *revision,
+ void *baton,
+ const char *repos_relpath,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+
+svn_error_t *
+svn_client__ra_get_copysrc_kind(svn_node_kind_t *kind,
+ void *baton,
+ const char *repos_relpath,
+ svn_revnum_t src_revision,
+ apr_pool_t *scratch_pool);
+
+
+void *
+svn_client__ra_make_cb_baton(svn_wc_context_t *wc_ctx,
+ apr_hash_t *relpath_map,
+ apr_pool_t *result_pool);
+
+/* ---------------------------------------------------------------- */
+
+/*** Add/delete ***/
+
+/* If AUTOPROPS is not null: Then read automatic properties matching PATH
+ from AUTOPROPS. AUTOPROPS is is a hash as per
+ svn_client__get_all_auto_props. Set *PROPERTIES to a hash containing
+ propname/value pairs (const char * keys mapping to svn_string_t * values).
+
+ If AUTOPROPS is null then set *PROPERTIES to an empty hash.
+
+ If *MIMETYPE is null or "application/octet-stream" then check AUTOPROPS
+ for a matching svn:mime-type. If AUTOPROPS is null or no match is found
+ and MAGIC_COOKIE is not NULL, then then try to detect the mime-type with
+ libmagic. If a mimetype is found then add it to *PROPERTIES and set
+ *MIMETYPE to the mimetype value or NULL otherwise.
+
+ Allocate the *PROPERTIES and its contents as well as *MIMETYPE, in
+ RESULT_POOL. Use SCRATCH_POOL for temporary allocations. */
+svn_error_t *svn_client__get_paths_auto_props(
+ apr_hash_t **properties,
+ const char **mimetype,
+ const char *path,
+ svn_magic__cookie_t *magic_cookie,
+ apr_hash_t *autoprops,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Gather all auto-props from CTX->config (or none if auto-props are
+ disabled) and all svn:auto-props explicitly set on or inherited
+ by PATH_OR_URL.
+
+ If PATH_OR_URL is an unversioned WC path then gather the
+ svn:auto-props inherited by PATH_OR_URL's nearest versioned
+ parent.
+
+ If PATH_OR_URL is a URL ask for the properties @HEAD, if it is a WC
+ path as sfor the working properties.
+
+ Store both types of auto-props in *AUTOPROPS, a hash mapping const
+ char * file patterns to another hash which maps const char * property
+ names to const char *property values.
+
+ If a given property name exists for the same pattern in both the config
+ file and in an a svn:auto-props property, the latter overrides the
+ former. If a given property name exists for the same pattern in two
+ different inherited svn:auto-props, then the closer path-wise
+ property overrides the more distant. svn:auto-props explicitly set
+ on PATH_OR_URL have the highest precedence and override inherited props
+ and config file settings.
+
+ Allocate *AUTOPROPS in RESULT_POOL. Use SCRATCH_POOL for temporary
+ allocations. */
+svn_error_t *svn_client__get_all_auto_props(apr_hash_t **autoprops,
+ const char *path_or_url,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Get a list of ignore patterns defined by the svn:global-ignores
+ properties set on, or inherited by, PATH_OR_URL. Store the collected
+ patterns as const char * elements in the array *IGNORES. Allocate
+ *IGNORES and its contents in RESULT_POOL. Use SCRATCH_POOL for
+ temporary allocations. */
+svn_error_t *svn_client__get_inherited_ignores(apr_array_header_t **ignores,
+ const char *path_or_url,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* The main logic for client deletion from a working copy. Deletes PATH
+ from CTX->WC_CTX. If PATH (or any item below a directory PATH) is
+ modified the delete will fail and return an error unless FORCE or KEEP_LOCAL
+ is TRUE.
+
+ If KEEP_LOCAL is TRUE then PATH is only scheduled from deletion from the
+ repository and a local copy of PATH will be kept in the working copy.
+
+ If DRY_RUN is TRUE all the checks are made to ensure that the delete can
+ occur, but the working copy is not modified. If NOTIFY_FUNC is not
+ null, it is called with NOTIFY_BATON for each file or directory deleted. */
+svn_error_t *
+svn_client__wc_delete(const char *local_abspath,
+ svn_boolean_t force,
+ svn_boolean_t dry_run,
+ svn_boolean_t keep_local,
+ svn_wc_notify_func2_t notify_func,
+ void *notify_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+
+/* Like svn_client__wc_delete(), but deletes multiple TARGETS efficiently. */
+svn_error_t *
+svn_client__wc_delete_many(const apr_array_header_t *targets,
+ svn_boolean_t force,
+ svn_boolean_t dry_run,
+ svn_boolean_t keep_local,
+ svn_wc_notify_func2_t notify_func,
+ void *notify_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+
+/* Make PATH and add it to the working copy, optionally making all the
+ intermediate parent directories if MAKE_PARENTS is TRUE. */
+svn_error_t *
+svn_client__make_local_parents(const char *path,
+ svn_boolean_t make_parents,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* ---------------------------------------------------------------- */
+
+/*** Checkout, update and switch ***/
+
+/* Update a working copy LOCAL_ABSPATH to REVISION, and (if not NULL) set
+ RESULT_REV to the update revision.
+
+ If DEPTH is svn_depth_unknown, then use whatever depth is already
+ set for LOCAL_ABSPATH, or @c svn_depth_infinity if LOCAL_ABSPATH does
+ not exist.
+
+ Else if DEPTH is svn_depth_infinity, then update fully recursively
+ (resetting the existing depth of the working copy if necessary).
+ Else if DEPTH is svn_depth_files, update all files under LOCAL_ABSPATH (if
+ any), but exclude any subdirectories. Else if DEPTH is
+ svn_depth_immediates, update all files and include immediate
+ subdirectories (at svn_depth_empty). Else if DEPTH is
+ svn_depth_empty, just update LOCAL_ABSPATH; if LOCAL_ABSPATH is a
+ directory, that means touching only its properties not its entries.
+
+ If DEPTH_IS_STICKY is set and DEPTH is not svn_depth_unknown, then
+ in addition to updating LOCAL_ABSPATH, also set its sticky ambient depth
+ value to DEPTH.
+
+ If IGNORE_EXTERNALS is true, do no externals processing.
+
+ Set *TIMESTAMP_SLEEP to TRUE if a sleep is required; otherwise do not
+ change *TIMESTAMP_SLEEP. The output will be valid even if the function
+ returns an error.
+
+ If ALLOW_UNVER_OBSTRUCTIONS is TRUE, unversioned children of LOCAL_ABSPATH
+ that obstruct items added from the repos are tolerated; if FALSE,
+ these obstructions cause the update to fail.
+
+ If ADDS_AS_MODIFICATION is TRUE, local additions are handled as
+ modifications on added nodes.
+
+ If INNERUPDATE is true, no anchor check is performed on the update target.
+
+ If MAKE_PARENTS is true, allow the update to calculate and checkout
+ (with depth=empty) any parent directories of the requested update
+ target which are missing from the working copy.
+
+ NOTE: You may not specify both INNERUPDATE and MAKE_PARENTS as true.
+*/
+svn_error_t *
+svn_client__update_internal(svn_revnum_t *result_rev,
+ const char *local_abspath,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t depth_is_sticky,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_boolean_t adds_as_modification,
+ svn_boolean_t make_parents,
+ svn_boolean_t innerupdate,
+ svn_boolean_t *timestamp_sleep,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* Checkout into LOCAL_ABSPATH a working copy of URL at REVISION, and (if not
+ NULL) set RESULT_REV to the checked out revision.
+
+ If DEPTH is svn_depth_infinity, then check out fully recursively.
+ Else if DEPTH is svn_depth_files, checkout all files under LOCAL_ABSPATH (if
+ any), but not subdirectories. Else if DEPTH is
+ svn_depth_immediates, check out all files and include immediate
+ subdirectories (at svn_depth_empty). Else if DEPTH is
+ svn_depth_empty, just check out LOCAL_ABSPATH, with none of its entries.
+
+ DEPTH must be a definite depth, not (e.g.) svn_depth_unknown.
+
+ RA_CACHE is a pointer to a cache of information for the URL at
+ REVISION based on the PEG_REVISION. Any information not in
+ *RA_CACHE is retrieved by a round-trip to the repository. RA_CACHE
+ may be NULL which indicates that no cache information is available.
+
+ If IGNORE_EXTERNALS is true, do no externals processing.
+
+ Set *TIMESTAMP_SLEEP to TRUE if a sleep is required; otherwise do not
+ change *TIMESTAMP_SLEEP. The output will be valid even if the function
+ returns an error.
+
+ If ALLOW_UNVER_OBSTRUCTIONS is TRUE,
+ unversioned children of LOCAL_ABSPATH that obstruct items added from
+ the repos are tolerated; if FALSE, these obstructions cause the checkout
+ to fail.
+
+ If INNERCHECKOUT is true, no anchor check is performed on the target.
+ */
+svn_error_t *
+svn_client__checkout_internal(svn_revnum_t *result_rev,
+ const char *URL,
+ const char *local_abspath,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_boolean_t *timestamp_sleep,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* Switch a working copy PATH to URL@PEG_REVISION at REVISION, and (if not
+ NULL) set RESULT_REV to the switch revision. A write lock will be
+ acquired and released if not held. Only switch as deeply as DEPTH
+ indicates.
+
+ Set *TIMESTAMP_SLEEP to TRUE if a sleep is required; otherwise do not
+ change *TIMESTAMP_SLEEP. The output will be valid even if the function
+ returns an error.
+
+ If IGNORE_EXTERNALS is true, don't process externals.
+
+ If ALLOW_UNVER_OBSTRUCTIONS is TRUE, unversioned children of PATH
+ that obstruct items added from the repos are tolerated; if FALSE,
+ these obstructions cause the switch to fail.
+
+ DEPTH and DEPTH_IS_STICKY behave as for svn_client__update_internal().
+
+ If IGNORE_ANCESTRY is true, don't perform a common ancestry check
+ between the PATH and URL; otherwise, do, and return
+ SVN_ERR_CLIENT_UNRELATED_RESOURCES if they aren't related.
+*/
+svn_error_t *
+svn_client__switch_internal(svn_revnum_t *result_rev,
+ const char *path,
+ const char *url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t depth_is_sticky,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t *timestamp_sleep,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* ---------------------------------------------------------------- */
+
+/*** Inheritable Properties ***/
+
+/* Convert any svn_prop_inherited_item_t elements in INHERITED_PROPS which
+ have repository root relative path PATH_OR_URL structure members to URLs
+ using REPOS_ROOT_URL. Changes to the contents of INHERITED_PROPS are
+ allocated in RESULT_POOL. SCRATCH_POOL is used for temporary
+ allocations. */
+svn_error_t *
+svn_client__iprop_relpaths_to_urls(apr_array_header_t *inherited_props,
+ const char *repos_root_url,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Fetch the inherited properties for the base of LOCAL_ABSPATH as well
+ as any WC roots under LOCAL_ABSPATH (as limited by DEPTH) using
+ RA_SESSION. Store the results in *WCROOT_IPROPS, a hash mapping
+ const char * absolute working copy paths to depth-first ordered arrays
+ of svn_prop_inherited_item_t * structures.
+
+ Any svn_prop_inherited_item_t->path_or_url members returned in
+ *WCROOT_IPROPS are repository relative paths.
+
+ If LOCAL_ABSPATH has no base then do nothing.
+
+ RA_SESSION should be an open RA session pointing at the URL of PATH,
+ or NULL, in which case this function will use its own temporary session.
+
+ Allocate *WCROOT_IPROPS in RESULT_POOL, use SCRATCH_POOL for temporary
+ allocations.
+
+ If one or more of the paths are not available in the repository at the
+ specified revision, these paths will not be added to the hashtable.
+*/
+svn_error_t *
+svn_client__get_inheritable_props(apr_hash_t **wcroot_iprops,
+ const char *local_abspath,
+ svn_revnum_t revision,
+ svn_depth_t depth,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* ---------------------------------------------------------------- */
+
+/*** Editor for repository diff ***/
+
+/* Create an editor for a pure repository comparison, i.e. comparing one
+ repository version against the other.
+
+ DIFF_CALLBACKS/DIFF_CMD_BATON represent the callback that implements
+ the comparison.
+
+ DEPTH is the depth to recurse.
+
+ RA_SESSION is an RA session through which this editor may fetch
+ properties, file contents and directory listings of the 'old' side of the
+ diff. It is a separate RA session from the one through which this editor
+ is being driven. REVISION is the revision number of the 'old' side of
+ the diff.
+
+ If TEXT_DELTAS is FALSE, then do not expect text deltas from the edit
+ drive, nor send the 'before' and 'after' texts to the diff callbacks;
+ instead, send empty files to the diff callbacks if there was a change.
+ This must be FALSE if the edit producer is not sending text deltas,
+ otherwise the file content checksum comparisons will fail.
+
+ EDITOR/EDIT_BATON return the newly created editor and baton.
+
+ @since New in 1.8.
+ */
+svn_error_t *
+svn_client__get_diff_editor2(const svn_delta_editor_t **editor,
+ void **edit_baton,
+ svn_ra_session_t *ra_session,
+ svn_depth_t depth,
+ svn_revnum_t revision,
+ svn_boolean_t text_deltas,
+ const svn_diff_tree_processor_t *processor,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ apr_pool_t *result_pool);
+
+/* ---------------------------------------------------------------- */
+
+/*** Editor for diff summary ***/
+
+/* Set *CALLBACKS and *CALLBACK_BATON to a set of diff callbacks that will
+ report a diff summary, i.e. only providing information about the changed
+ items without the text deltas.
+
+ TARGET is the target path, relative to the anchor, of the diff.
+
+ SUMMARIZE_FUNC is called with SUMMARIZE_BATON as parameter by the
+ created callbacks for each changed item.
+*/
+svn_error_t *
+svn_client__get_diff_summarize_callbacks(
+ svn_wc_diff_callbacks4_t **callbacks,
+ void **callback_baton,
+ const char *target,
+ svn_boolean_t reversed,
+ svn_client_diff_summarize_func_t summarize_func,
+ void *summarize_baton,
+ apr_pool_t *pool);
+
+/* ---------------------------------------------------------------- */
+
+/*** Copy Stuff ***/
+
+/* This structure is used to associate a specific copy or move SRC with a
+ specific copy or move destination. It also contains information which
+ various helper functions may need. Not every copy function uses every
+ field.
+*/
+typedef struct svn_client__copy_pair_t
+{
+ /* The absolute source path or url. */
+ const char *src_abspath_or_url;
+
+ /* The base name of the object. It should be the same for both src
+ and dst. */
+ const char *base_name;
+
+ /* The node kind of the source */
+ svn_node_kind_t src_kind;
+
+ /* The original source name. (Used when the source gets overwritten by a
+ peg revision lookup.) */
+ const char *src_original;
+
+ /* The source operational revision. */
+ svn_opt_revision_t src_op_revision;
+
+ /* The source peg revision. */
+ svn_opt_revision_t src_peg_revision;
+
+ /* The source revision number. */
+ svn_revnum_t src_revnum;
+
+ /* The absolute destination path or url */
+ const char *dst_abspath_or_url;
+
+ /* The absolute source path or url of the destination's parent. */
+ const char *dst_parent_abspath;
+} svn_client__copy_pair_t;
+
+/* ---------------------------------------------------------------- */
+
+/*** Commit Stuff ***/
+
+/* WARNING: This is all new, untested, un-peer-reviewed conceptual
+ stuff.
+
+ The day that 'svn switch' came into existence, our old commit
+ crawler (svn_wc_crawl_local_mods) became obsolete. It relied far
+ too heavily on the on-disk hierarchy of files and directories, and
+ simply had no way to support disjoint working copy trees or nest
+ working copies. The primary reason for this is that commit
+ process, in order to guarantee atomicity, is a single drive of a
+ commit editor which is based not on working copy paths, but on
+ URLs. With the completion of 'svn switch', it became all too
+ likely that the on-disk working copy hierarchy would no longer be
+ guaranteed to map to a similar in-repository hierarchy.
+
+ Aside from this new brokenness of the old system, an unrelated
+ feature request had cropped up -- the ability to know in advance of
+ your commit, exactly what would be committed (so that log messages
+ could be initially populated with this information). Since the old
+ crawler discovered commit candidates while in the process of
+ committing, it was impossible to harvest this information upfront.
+ As a workaround, svn_wc_statuses() was used to stat the whole
+ working copy for changes before the commit started...and then the
+ commit would again stat the whole tree for changes.
+
+ Enter the new system.
+
+ The primary goal of this system is very straightforward: harvest
+ all commit candidate information up front, and cache enough info in
+ the process to use this to drive a URL-sorted commit.
+
+ *** END-OF-KNOWLEDGE ***
+
+ The prototypes below are still in development. In general, the
+ idea is that commit-y processes ('svn mkdir URL', 'svn delete URL',
+ 'svn commit', 'svn copy WC_PATH URL', 'svn copy URL1 URL2', 'svn
+ move URL1 URL2', others?) generate the cached commit candidate
+ information, and hand this information off to a consumer which is
+ responsible for driving the RA layer's commit editor in a
+ URL-depth-first fashion and reporting back the post-commit
+ information.
+
+*/
+
+/* Structure that contains an apr_hash_t * hash of apr_array_header_t *
+ arrays of svn_client_commit_item3_t * structures; keyed by the
+ canonical repository URLs. For faster lookup, it also provides
+ an hash index keyed by the local absolute path. */
+typedef struct svn_client__committables_t
+{
+ /* apr_array_header_t array of svn_client_commit_item3_t structures
+ keyed by canonical repository URL */
+ apr_hash_t *by_repository;
+
+ /* svn_client_commit_item3_t structures keyed by local absolute path
+ (path member in the respective structures).
+
+ This member is for fast lookup only, i.e. whether there is an
+ entry for the given path or not, but it will only allow for one
+ entry per absolute path (in case of duplicate entries in the
+ above arrays). The "canonical" data storage containing all item
+ is by_repository. */
+ apr_hash_t *by_path;
+
+} svn_client__committables_t;
+
+/* Callback for the commit harvester to check if a node exists at the specified
+ url */
+typedef svn_error_t *(*svn_client__check_url_kind_t)(void *baton,
+ svn_node_kind_t *kind,
+ const char *url,
+ svn_revnum_t revision,
+ apr_pool_t *scratch_pool);
+
+/* Recursively crawl a set of working copy paths (BASE_DIR_ABSPATH + each
+ item in the TARGETS array) looking for commit candidates, locking
+ working copy directories as the crawl progresses. For each
+ candidate found:
+
+ - create svn_client_commit_item3_t for the candidate.
+
+ - add the structure to an apr_array_header_t array of commit
+ items that are in the same repository, creating a new array if
+ necessary.
+
+ - add (or update) a reference to this array to the by_repository
+ hash within COMMITTABLES and update the by_path member as well-
+
+ - if the candidate has a lock token, add it to the LOCK_TOKENS hash.
+
+ - if the candidate is a directory scheduled for deletion, crawl
+ the directories children recursively for any lock tokens and
+ add them to the LOCK_TOKENS array.
+
+ At the successful return of this function, COMMITTABLES will point
+ a new svn_client__committables_t*. LOCK_TOKENS will point to a hash
+ table with const char * lock tokens, keyed on const char * URLs.
+
+ If DEPTH is specified, descend (or not) into each target in TARGETS
+ as specified by DEPTH; the behavior is the same as that described
+ for svn_client_commit4().
+
+ If DEPTH_EMPTY_START is >= 0, all targets after index DEPTH_EMPTY_START
+ in TARGETS are handled as having svn_depth_empty.
+
+ If JUST_LOCKED is TRUE, treat unmodified items with lock tokens as
+ commit candidates.
+
+ If CHANGELISTS is non-NULL, it is an array of const char *
+ changelist names used as a restrictive filter
+ when harvesting committables; that is, don't add a path to
+ COMMITTABLES unless it's a member of one of those changelists.
+
+ If CTX->CANCEL_FUNC is non-null, it will be called with
+ CTX->CANCEL_BATON while harvesting to determine if the client has
+ cancelled the operation. */
+svn_error_t *
+svn_client__harvest_committables(svn_client__committables_t **committables,
+ apr_hash_t **lock_tokens,
+ const char *base_dir_abspath,
+ const apr_array_header_t *targets,
+ int depth_empty_start,
+ svn_depth_t depth,
+ svn_boolean_t just_locked,
+ const apr_array_header_t *changelists,
+ svn_client__check_url_kind_t check_url_func,
+ void *check_url_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+
+/* Recursively crawl each absolute working copy path SRC in COPY_PAIRS,
+ harvesting commit_items into a COMMITABLES structure as if every entry
+ at or below the SRC was to be committed as a set of adds (mostly with
+ history) to a new repository URL (DST in COPY_PAIRS).
+
+ If CTX->CANCEL_FUNC is non-null, it will be called with
+ CTX->CANCEL_BATON while harvesting to determine if the client has
+ cancelled the operation. */
+svn_error_t *
+svn_client__get_copy_committables(svn_client__committables_t **committables,
+ const apr_array_header_t *copy_pairs,
+ svn_client__check_url_kind_t check_url_func,
+ void *check_url_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* A qsort()-compatible sort routine for sorting an array of
+ svn_client_commit_item_t *'s by their URL member. */
+int svn_client__sort_commit_item_urls(const void *a, const void *b);
+
+
+/* Rewrite the COMMIT_ITEMS array to be sorted by URL. Also, discover
+ a common *BASE_URL for the items in the array, and rewrite those
+ items' URLs to be relative to that *BASE_URL.
+
+ COMMIT_ITEMS is an array of (svn_client_commit_item3_t *) items.
+
+ Afterwards, some of the items in COMMIT_ITEMS may contain data
+ allocated in POOL. */
+svn_error_t *
+svn_client__condense_commit_items(const char **base_url,
+ apr_array_header_t *commit_items,
+ apr_pool_t *pool);
+
+
+/* Like svn_ra_stat() on the ra session root, but with a compatibility
+ hack for pre-1.2 svnserve that don't support this api. */
+svn_error_t *
+svn_client__ra_stat_compatible(svn_ra_session_t *ra_session,
+ svn_revnum_t rev,
+ svn_dirent_t **dirent_p,
+ apr_uint32_t dirent_fields,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool);
+
+
+/* Commit the items in the COMMIT_ITEMS array using EDITOR/EDIT_BATON
+ to describe the committed local mods. Prior to this call,
+ COMMIT_ITEMS should have been run through (and BASE_URL generated
+ by) svn_client__condense_commit_items().
+
+ COMMIT_ITEMS is an array of (svn_client_commit_item3_t *) items.
+
+ CTX->NOTIFY_FUNC/CTX->BATON will be called as the commit progresses, as
+ a way of describing actions to the application layer (if non NULL).
+
+ NOTIFY_PATH_PREFIX will be passed to CTX->notify_func2() as the
+ common absolute path prefix of the committed paths. It can be NULL.
+
+ If SHA1_CHECKSUMS is not NULL, set *SHA1_CHECKSUMS to a hash containing,
+ for each file transmitted, a mapping from the commit-item's (const
+ char *) path to the (const svn_checksum_t *) SHA1 checksum of its new text
+ base.
+
+ Use RESULT_POOL for all allocating the resulting hashes and SCRATCH_POOL
+ for temporary allocations.
+ */
+svn_error_t *
+svn_client__do_commit(const char *base_url,
+ const apr_array_header_t *commit_items,
+ const svn_delta_editor_t *editor,
+ void *edit_baton,
+ const char *notify_path_prefix,
+ apr_hash_t **sha1_checksums,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+
+
+/*** Externals (Modules) ***/
+
+/* Handle changes to the svn:externals property described by EXTERNALS_NEW,
+ and AMBIENT_DEPTHS. The tree's top level directory
+ is at TARGET_ABSPATH which has a root URL of REPOS_ROOT_URL.
+ A write lock should be held.
+
+ For each changed value of the property, discover the nature of the
+ change and behave appropriately -- either check a new "external"
+ subdir, or call svn_wc_remove_from_revision_control() on an
+ existing one, or both.
+
+ TARGET_ABSPATH is the root of the driving operation and
+ REQUESTED_DEPTH is the requested depth of the driving operation
+ (e.g., update, switch, etc). If it is neither svn_depth_infinity
+ nor svn_depth_unknown, then changes to svn:externals will have no
+ effect. If REQUESTED_DEPTH is svn_depth_unknown, then the ambient
+ depth of each working copy directory holding an svn:externals value
+ will determine whether that value is interpreted there (the ambient
+ depth must be svn_depth_infinity). If REQUESTED_DEPTH is
+ svn_depth_infinity, then it is presumed to be expanding any
+ shallower ambient depth, so changes to svn:externals values will be
+ interpreted.
+
+ Pass NOTIFY_FUNC with NOTIFY_BATON along to svn_client_checkout().
+
+ Set *TIMESTAMP_SLEEP to TRUE if a sleep is required; otherwise do not
+ change *TIMESTAMP_SLEEP. The output will be valid even if the function
+ returns an error.
+
+ Use POOL for temporary allocation. */
+svn_error_t *
+svn_client__handle_externals(apr_hash_t *externals_new,
+ apr_hash_t *ambient_depths,
+ const char *repos_root_url,
+ const char *target_abspath,
+ svn_depth_t requested_depth,
+ svn_boolean_t *timestamp_sleep,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+
+/* Export externals definitions described by EXTERNALS, a hash of the
+ form returned by svn_wc_edited_externals() (which see). The external
+ items will be exported instead of checked out -- they will have no
+ administrative subdirectories.
+
+ The checked out or exported tree's top level directory is at
+ TO_ABSPATH and corresponds to FROM_URL URL in the repository, which
+ has a root URL of REPOS_ROOT_URL.
+
+ REQUESTED_DEPTH is the requested_depth of the driving operation; it
+ behaves as for svn_client__handle_externals(), except that ambient
+ depths are presumed to be svn_depth_infinity.
+
+ NATIVE_EOL is the value passed as NATIVE_EOL when exporting.
+
+ Use POOL for temporary allocation. */
+svn_error_t *
+svn_client__export_externals(apr_hash_t *externals,
+ const char *from_url,
+ const char *to_abspath,
+ const char *repos_root_url,
+ svn_depth_t requested_depth,
+ const char *native_eol,
+ svn_boolean_t ignore_keywords,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* Baton for svn_client__dirent_fetcher */
+struct svn_client__dirent_fetcher_baton_t
+{
+ svn_ra_session_t *ra_session;
+ svn_revnum_t target_revision;
+ const char *anchor_url;
+};
+
+/* Implements svn_wc_dirents_func_t for update and switch handling. Assumes
+ a struct svn_client__dirent_fetcher_baton_t * baton */
+svn_error_t *
+svn_client__dirent_fetcher(void *baton,
+ apr_hash_t **dirents,
+ const char *repos_root_url,
+ const char *repos_relpath,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Retrieve log messages using the first provided (non-NULL) callback
+ in the set of *CTX->log_msg_func3, CTX->log_msg_func2, or
+ CTX->log_msg_func. Other arguments same as
+ svn_client_get_commit_log3_t. */
+svn_error_t *
+svn_client__get_log_msg(const char **log_msg,
+ const char **tmp_file,
+ const apr_array_header_t *commit_items,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* Return the revision properties stored in REVPROP_TABLE_IN, adding
+ LOG_MSG as SVN_PROP_REVISION_LOG in *REVPROP_TABLE_OUT, allocated in
+ POOL. *REVPROP_TABLE_OUT will map const char * property names to
+ svn_string_t values. If REVPROP_TABLE_IN is non-NULL, check that
+ it doesn't contain any of the standard Subversion properties. In
+ that case, return SVN_ERR_CLIENT_PROPERTY_NAME. */
+svn_error_t *
+svn_client__ensure_revprop_table(apr_hash_t **revprop_table_out,
+ const apr_hash_t *revprop_table_in,
+ const char *log_msg,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* Return a potentially translated version of local file LOCAL_ABSPATH
+ in NORMAL_STREAM. REVISION must be one of the following: BASE, COMMITTED,
+ WORKING.
+
+ EXPAND_KEYWORDS operates as per the EXPAND argument to
+ svn_subst_stream_translated, which see. If NORMALIZE_EOLS is TRUE and
+ LOCAL_ABSPATH requires translation, then normalize the line endings in
+ *NORMAL_STREAM.
+
+ Uses SCRATCH_POOL for temporary allocations. */
+svn_error_t *
+svn_client__get_normalized_stream(svn_stream_t **normal_stream,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t expand_keywords,
+ svn_boolean_t normalize_eols,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Return a set of callbacks to use with the Ev2 shims. */
+svn_delta_shim_callbacks_t *
+svn_client__get_shim_callbacks(svn_wc_context_t *wc_ctx,
+ apr_hash_t *relpath_map,
+ apr_pool_t *result_pool);
+
+/* Return REVISION unless its kind is 'unspecified' in which case return
+ * a pointer to a statically allocated revision structure of kind 'head'
+ * if PATH_OR_URL is a URL or 'base' if it is a WC path. */
+const svn_opt_revision_t *
+svn_cl__rev_default_to_head_or_base(const svn_opt_revision_t *revision,
+ const char *path_or_url);
+
+/* Return REVISION unless its kind is 'unspecified' in which case return
+ * a pointer to a statically allocated revision structure of kind 'head'
+ * if PATH_OR_URL is a URL or 'working' if it is a WC path. */
+const svn_opt_revision_t *
+svn_cl__rev_default_to_head_or_working(const svn_opt_revision_t *revision,
+ const char *path_or_url);
+
+/* Return REVISION unless its kind is 'unspecified' in which case return
+ * PEG_REVISION. */
+const svn_opt_revision_t *
+svn_cl__rev_default_to_peg(const svn_opt_revision_t *revision,
+ const svn_opt_revision_t *peg_revision);
+
+/* Call the conflict resolver callback in CTX for each conflict recorded
+ * in CONFLICTED_PATHS (const char *abspath keys; ignored values). If
+ * CONFLICTS_REMAIN is not NULL, then set *CONFLICTS_REMAIN to true if
+ * there are any conflicts among CONFLICTED_PATHS remaining unresolved
+ * at the end of this operation, else set it to false.
+ */
+svn_error_t *
+svn_client__resolve_conflicts(svn_boolean_t *conflicts_remain,
+ apr_hash_t *conflicted_paths,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool);
+
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* SVN_LIBSVN_CLIENT_H */
diff --git a/subversion/libsvn_client/cmdline.c b/subversion/libsvn_client/cmdline.c
new file mode 100644
index 0000000..a17f8c4
--- /dev/null
+++ b/subversion/libsvn_client/cmdline.c
@@ -0,0 +1,363 @@
+/*
+ * cmdline.c: command-line processing
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+/*** Includes. ***/
+#include "svn_client.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_opt.h"
+#include "svn_utf.h"
+
+#include "client.h"
+
+#include "private/svn_opt_private.h"
+
+#include "svn_private_config.h"
+
+
+/*** Code. ***/
+
+#define DEFAULT_ARRAY_SIZE 5
+
+
+/* Attempt to find the repository root url for TARGET, possibly using CTX for
+ * authentication. If one is found and *ROOT_URL is not NULL, then just check
+ * that the root url for TARGET matches the value given in *ROOT_URL and
+ * return an error if it does not. If one is found and *ROOT_URL is NULL then
+ * set *ROOT_URL to the root url for TARGET, allocated from POOL.
+ * If a root url is not found for TARGET because it does not exist in the
+ * repository, then return with no error.
+ *
+ * TARGET is a UTF-8 encoded string that is fully canonicalized and escaped.
+ */
+static svn_error_t *
+check_root_url_of_target(const char **root_url,
+ const char *target,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+ const char *tmp_root_url;
+ const char *truepath;
+ svn_opt_revision_t opt_rev;
+
+ SVN_ERR(svn_opt_parse_path(&opt_rev, &truepath, target, pool));
+ if (!svn_path_is_url(truepath))
+ SVN_ERR(svn_dirent_get_absolute(&truepath, truepath, pool));
+
+ err = svn_client_get_repos_root(&tmp_root_url, NULL, truepath,
+ ctx, pool, pool);
+
+ if (err)
+ {
+ /* It is OK if the given target does not exist, it just means
+ * we will not be able to determine the root url from this particular
+ * argument.
+ *
+ * If the target itself is a URL to a repository that does not exist,
+ * that's fine, too. The callers will deal with this argument in an
+ * appropriate manner if it does not make any sense.
+ *
+ * Also tolerate locally added targets ("bad revision" error).
+ */
+ if ((err->apr_err == SVN_ERR_ENTRY_NOT_FOUND)
+ || (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ || (err->apr_err == SVN_ERR_WC_NOT_WORKING_COPY)
+ || (err->apr_err == SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED)
+ || (err->apr_err == SVN_ERR_CLIENT_BAD_REVISION))
+ {
+ svn_error_clear(err);
+ return SVN_NO_ERROR;
+ }
+ else
+ return svn_error_trace(err);
+ }
+
+ if (*root_url && tmp_root_url)
+ {
+ if (strcmp(*root_url, tmp_root_url) != 0)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("All non-relative targets must have "
+ "the same root URL"));
+ }
+ else
+ *root_url = tmp_root_url;
+
+ return SVN_NO_ERROR;
+}
+
+/* Note: This is substantially copied from svn_opt__args_to_target_array() in
+ * order to move to libsvn_client while maintaining backward compatibility. */
+svn_error_t *
+svn_client_args_to_target_array2(apr_array_header_t **targets_p,
+ apr_getopt_t *os,
+ const apr_array_header_t *known_targets,
+ svn_client_ctx_t *ctx,
+ svn_boolean_t keep_last_origpath_on_truepath_collision,
+ apr_pool_t *pool)
+{
+ int i;
+ svn_boolean_t rel_url_found = FALSE;
+ const char *root_url = NULL;
+ apr_array_header_t *input_targets =
+ apr_array_make(pool, DEFAULT_ARRAY_SIZE, sizeof(const char *));
+ apr_array_header_t *output_targets =
+ apr_array_make(pool, DEFAULT_ARRAY_SIZE, sizeof(const char *));
+ apr_array_header_t *reserved_names = NULL;
+
+ /* Step 1: create a master array of targets that are in UTF-8
+ encoding, and come from concatenating the targets left by apr_getopt,
+ plus any extra targets (e.g., from the --targets switch.)
+ If any of the targets are relative urls, then set the rel_url_found
+ flag.*/
+
+ for (; os->ind < os->argc; os->ind++)
+ {
+ /* The apr_getopt targets are still in native encoding. */
+ const char *raw_target = os->argv[os->ind];
+ const char *utf8_target;
+
+ SVN_ERR(svn_utf_cstring_to_utf8(&utf8_target,
+ raw_target, pool));
+
+ if (svn_path_is_repos_relative_url(utf8_target))
+ rel_url_found = TRUE;
+
+ APR_ARRAY_PUSH(input_targets, const char *) = utf8_target;
+ }
+
+ if (known_targets)
+ {
+ for (i = 0; i < known_targets->nelts; i++)
+ {
+ /* The --targets array have already been converted to UTF-8,
+ because we needed to split up the list with svn_cstring_split. */
+ const char *utf8_target = APR_ARRAY_IDX(known_targets,
+ i, const char *);
+
+ if (svn_path_is_repos_relative_url(utf8_target))
+ rel_url_found = TRUE;
+
+ APR_ARRAY_PUSH(input_targets, const char *) = utf8_target;
+ }
+ }
+
+ /* Step 2: process each target. */
+
+ for (i = 0; i < input_targets->nelts; i++)
+ {
+ const char *utf8_target = APR_ARRAY_IDX(input_targets, i, const char *);
+
+ /* Relative urls will be canonicalized when they are resolved later in
+ * the function
+ */
+ if (svn_path_is_repos_relative_url(utf8_target))
+ {
+ APR_ARRAY_PUSH(output_targets, const char *) = utf8_target;
+ }
+ else
+ {
+ const char *true_target;
+ const char *peg_rev;
+ const char *target;
+
+ /*
+ * This is needed so that the target can be properly canonicalized,
+ * otherwise the canonicalization does not treat a ".@BASE" as a "."
+ * with a BASE peg revision, and it is not canonicalized to "@BASE".
+ * If any peg revision exists, it is appended to the final
+ * canonicalized path or URL. Do not use svn_opt_parse_path()
+ * because the resulting peg revision is a structure that would have
+ * to be converted back into a string. Converting from a string date
+ * to the apr_time_t field in the svn_opt_revision_value_t and back to
+ * a string would not necessarily preserve the exact bytes of the
+ * input date, so its easier just to keep it in string form.
+ */
+ SVN_ERR(svn_opt__split_arg_at_peg_revision(&true_target, &peg_rev,
+ utf8_target, pool));
+
+ /* URLs and wc-paths get treated differently. */
+ if (svn_path_is_url(true_target))
+ {
+ SVN_ERR(svn_opt__arg_canonicalize_url(&true_target,
+ true_target, pool));
+ }
+ else /* not a url, so treat as a path */
+ {
+ const char *base_name;
+ const char *original_target;
+
+ original_target = svn_dirent_internal_style(true_target, pool);
+ SVN_ERR(svn_opt__arg_canonicalize_path(&true_target,
+ true_target, pool));
+
+ /* There are two situations in which a 'truepath-conversion'
+ (case-canonicalization to on-disk path on case-insensitive
+ filesystem) needs to be undone:
+
+ 1. If KEEP_LAST_ORIGPATH_ON_TRUEPATH_COLLISION is TRUE, and
+ this is the last target of a 2-element target list, and
+ both targets have the same truepath. */
+ if (keep_last_origpath_on_truepath_collision
+ && input_targets->nelts == 2 && i == 1
+ && strcmp(original_target, true_target) != 0)
+ {
+ const char *src_truepath = APR_ARRAY_IDX(output_targets,
+ 0,
+ const char *);
+ if (strcmp(src_truepath, true_target) == 0)
+ true_target = original_target;
+ }
+
+ /* 2. If there is an exact match in the wc-db without a
+ corresponding on-disk path (e.g. a scheduled-for-delete
+ file only differing in case from an on-disk file). */
+ if (strcmp(original_target, true_target) != 0)
+ {
+ const char *target_abspath;
+ svn_node_kind_t kind;
+ svn_error_t *err2;
+
+ SVN_ERR(svn_dirent_get_absolute(&target_abspath,
+ original_target, pool));
+ err2 = svn_wc_read_kind2(&kind, ctx->wc_ctx, target_abspath,
+ TRUE, FALSE, pool);
+ if (err2
+ && (err2->apr_err == SVN_ERR_WC_NOT_WORKING_COPY
+ || err2->apr_err == SVN_ERR_WC_UPGRADE_REQUIRED))
+ {
+ svn_error_clear(err2);
+ }
+ else
+ {
+ SVN_ERR(err2);
+ /* We successfully did a lookup in the wc-db. Now see
+ if it's something interesting. */
+ if (kind == svn_node_file || kind == svn_node_dir)
+ true_target = original_target;
+ }
+ }
+
+ /* If the target has the same name as a Subversion
+ working copy administrative dir, skip it. */
+ base_name = svn_dirent_basename(true_target, pool);
+
+ if (svn_wc_is_adm_dir(base_name, pool))
+ {
+ if (!reserved_names)
+ reserved_names = apr_array_make(pool, DEFAULT_ARRAY_SIZE,
+ sizeof(const char *));
+
+ APR_ARRAY_PUSH(reserved_names, const char *) = utf8_target;
+
+ continue;
+ }
+ }
+
+ target = apr_pstrcat(pool, true_target, peg_rev, (char *)NULL);
+
+ if (rel_url_found)
+ {
+ /* Later targets have priority over earlier target, I
+ don't know why, see basic_relative_url_multi_repo. */
+ SVN_ERR(check_root_url_of_target(&root_url, target,
+ ctx, pool));
+ }
+
+ APR_ARRAY_PUSH(output_targets, const char *) = target;
+ }
+ }
+
+ /* Only resolve relative urls if there were some actually found earlier. */
+ if (rel_url_found)
+ {
+ /*
+ * Use the current directory's root url if one wasn't found using the
+ * arguments.
+ */
+ if (root_url == NULL)
+ {
+ const char *current_abspath;
+ svn_error_t *err;
+
+ SVN_ERR(svn_dirent_get_absolute(&current_abspath, "", pool));
+ err = svn_client_get_repos_root(&root_url, NULL /* uuid */,
+ current_abspath, ctx, pool, pool);
+ if (err || root_url == NULL)
+ return svn_error_create(SVN_ERR_WC_NOT_WORKING_COPY, err,
+ _("Resolving '^/': no repository root "
+ "found in the target arguments or "
+ "in the current directory"));
+ }
+
+ *targets_p = apr_array_make(pool, output_targets->nelts,
+ sizeof(const char *));
+
+ for (i = 0; i < output_targets->nelts; i++)
+ {
+ const char *target = APR_ARRAY_IDX(output_targets, i,
+ const char *);
+
+ if (svn_path_is_repos_relative_url(target))
+ {
+ const char *abs_target;
+ const char *true_target;
+ const char *peg_rev;
+
+ SVN_ERR(svn_opt__split_arg_at_peg_revision(&true_target, &peg_rev,
+ target, pool));
+
+ SVN_ERR(svn_path_resolve_repos_relative_url(&abs_target,
+ true_target,
+ root_url, pool));
+
+ SVN_ERR(svn_opt__arg_canonicalize_url(&true_target, abs_target,
+ pool));
+
+ target = apr_pstrcat(pool, true_target, peg_rev, (char *)NULL);
+ }
+
+ APR_ARRAY_PUSH(*targets_p, const char *) = target;
+ }
+ }
+ else
+ *targets_p = output_targets;
+
+ if (reserved_names)
+ {
+ svn_error_t *err = SVN_NO_ERROR;
+
+ for (i = 0; i < reserved_names->nelts; ++i)
+ err = svn_error_createf(SVN_ERR_RESERVED_FILENAME_SPECIFIED, err,
+ _("'%s' ends in a reserved name"),
+ APR_ARRAY_IDX(reserved_names, i,
+ const char *));
+ return svn_error_trace(err);
+ }
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/commit.c b/subversion/libsvn_client/commit.c
new file mode 100644
index 0000000..3f6bfef
--- /dev/null
+++ b/subversion/libsvn_client/commit.c
@@ -0,0 +1,1031 @@
+/*
+ * commit.c: wrappers around wc commit functionality.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <string.h>
+#include <apr_strings.h>
+#include <apr_hash.h>
+#include "svn_hash.h"
+#include "svn_wc.h"
+#include "svn_ra.h"
+#include "svn_client.h"
+#include "svn_string.h"
+#include "svn_pools.h"
+#include "svn_error.h"
+#include "svn_error_codes.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_sorts.h"
+
+#include "client.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_ra_private.h"
+
+#include "svn_private_config.h"
+
+struct capture_baton_t {
+ svn_commit_callback2_t original_callback;
+ void *original_baton;
+
+ svn_commit_info_t **info;
+ apr_pool_t *pool;
+};
+
+
+static svn_error_t *
+capture_commit_info(const svn_commit_info_t *commit_info,
+ void *baton,
+ apr_pool_t *pool)
+{
+ struct capture_baton_t *cb = baton;
+
+ *(cb->info) = svn_commit_info_dup(commit_info, cb->pool);
+
+ if (cb->original_callback)
+ SVN_ERR((cb->original_callback)(commit_info, cb->original_baton, pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+get_ra_editor(const svn_delta_editor_t **editor,
+ void **edit_baton,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ const char *log_msg,
+ const apr_array_header_t *commit_items,
+ const apr_hash_t *revprop_table,
+ apr_hash_t *lock_tokens,
+ svn_boolean_t keep_locks,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ apr_pool_t *pool)
+{
+ apr_hash_t *commit_revprops;
+ apr_hash_t *relpath_map = NULL;
+
+ SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table,
+ log_msg, ctx, pool));
+
+#ifdef ENABLE_EV2_SHIMS
+ if (commit_items)
+ {
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ relpath_map = apr_hash_make(pool);
+ for (i = 0; i < commit_items->nelts; i++)
+ {
+ svn_client_commit_item3_t *item = APR_ARRAY_IDX(commit_items, i,
+ svn_client_commit_item3_t *);
+ const char *relpath;
+
+ if (!item->path)
+ continue;
+
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_wc__node_get_origin(NULL, NULL, &relpath, NULL, NULL, NULL,
+ ctx->wc_ctx, item->path, FALSE, pool,
+ iterpool));
+ if (relpath)
+ svn_hash_sets(relpath_map, relpath, item->path);
+ }
+ svn_pool_destroy(iterpool);
+ }
+#endif
+
+ /* Fetch RA commit editor. */
+ SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session,
+ svn_client__get_shim_callbacks(ctx->wc_ctx,
+ relpath_map, pool)));
+ SVN_ERR(svn_ra_get_commit_editor3(ra_session, editor, edit_baton,
+ commit_revprops, commit_callback,
+ commit_baton, lock_tokens, keep_locks,
+ pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/*** Public Interfaces. ***/
+
+static svn_error_t *
+reconcile_errors(svn_error_t *commit_err,
+ svn_error_t *unlock_err,
+ svn_error_t *bump_err,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+
+ /* Early release (for good behavior). */
+ if (! (commit_err || unlock_err || bump_err))
+ return SVN_NO_ERROR;
+
+ /* If there was a commit error, start off our error chain with
+ that. */
+ if (commit_err)
+ {
+ commit_err = svn_error_quick_wrap
+ (commit_err, _("Commit failed (details follow):"));
+ err = commit_err;
+ }
+
+ /* Else, create a new "general" error that will lead off the errors
+ that follow. */
+ else
+ err = svn_error_create(SVN_ERR_BASE, NULL,
+ _("Commit succeeded, but other errors follow:"));
+
+ /* If there was an unlock error... */
+ if (unlock_err)
+ {
+ /* Wrap the error with some headers. */
+ unlock_err = svn_error_quick_wrap
+ (unlock_err, _("Error unlocking locked dirs (details follow):"));
+
+ /* Append this error to the chain. */
+ svn_error_compose(err, unlock_err);
+ }
+
+ /* If there was a bumping error... */
+ if (bump_err)
+ {
+ /* Wrap the error with some headers. */
+ bump_err = svn_error_quick_wrap
+ (bump_err, _("Error bumping revisions post-commit (details follow):"));
+
+ /* Append this error to the chain. */
+ svn_error_compose(err, bump_err);
+ }
+
+ return err;
+}
+
+/* For all lock tokens in ALL_TOKENS for URLs under BASE_URL, add them
+ to a new hashtable allocated in POOL. *RESULT is set to point to this
+ new hash table. *RESULT will be keyed on const char * URI-decoded paths
+ relative to BASE_URL. The lock tokens will not be duplicated. */
+static svn_error_t *
+collect_lock_tokens(apr_hash_t **result,
+ apr_hash_t *all_tokens,
+ const char *base_url,
+ apr_pool_t *pool)
+{
+ apr_hash_index_t *hi;
+
+ *result = apr_hash_make(pool);
+
+ for (hi = apr_hash_first(pool, all_tokens); hi; hi = apr_hash_next(hi))
+ {
+ const char *url = svn__apr_hash_index_key(hi);
+ const char *token = svn__apr_hash_index_val(hi);
+ const char *relpath = svn_uri_skip_ancestor(base_url, url, pool);
+
+ if (relpath)
+ {
+ svn_hash_sets(*result, relpath, token);
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Put ITEM onto QUEUE, allocating it in QUEUE's pool...
+ * If a checksum is provided, it can be the MD5 and/or the SHA1. */
+static svn_error_t *
+post_process_commit_item(svn_wc_committed_queue_t *queue,
+ const svn_client_commit_item3_t *item,
+ svn_wc_context_t *wc_ctx,
+ svn_boolean_t keep_changelists,
+ svn_boolean_t keep_locks,
+ svn_boolean_t commit_as_operations,
+ const svn_checksum_t *sha1_checksum,
+ apr_pool_t *scratch_pool)
+{
+ svn_boolean_t loop_recurse = FALSE;
+ svn_boolean_t remove_lock;
+
+ if (! commit_as_operations
+ && (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
+ && (item->kind == svn_node_dir)
+ && (item->copyfrom_url))
+ loop_recurse = TRUE;
+
+ remove_lock = (! keep_locks && (item->state_flags
+ & SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN));
+
+ return svn_wc_queue_committed3(queue, wc_ctx, item->path,
+ loop_recurse, item->incoming_prop_changes,
+ remove_lock, !keep_changelists,
+ sha1_checksum, scratch_pool);
+}
+
+
+static svn_error_t *
+check_nonrecursive_dir_delete(svn_wc_context_t *wc_ctx,
+ const char *target_abspath,
+ svn_depth_t depth,
+ apr_pool_t *scratch_pool)
+{
+ svn_node_kind_t kind;
+
+ SVN_ERR_ASSERT(depth != svn_depth_infinity);
+
+ SVN_ERR(svn_wc_read_kind2(&kind, wc_ctx, target_abspath,
+ TRUE, FALSE, scratch_pool));
+
+
+ /* ### TODO(sd): This check is slightly too strict. It should be
+ ### possible to:
+ ###
+ ### * delete a directory containing only files when
+ ### depth==svn_depth_files;
+ ###
+ ### * delete a directory containing only files and empty
+ ### subdirs when depth==svn_depth_immediates.
+ ###
+ ### But for now, we insist on svn_depth_infinity if you're
+ ### going to delete a directory, because we're lazy and
+ ### trying to get depthy commits working in the first place.
+ ###
+ ### This would be fairly easy to fix, though: just, well,
+ ### check the above conditions!
+ ###
+ ### GJS: I think there may be some confusion here. there is
+ ### the depth of the commit, and the depth of a checked-out
+ ### directory in the working copy. Delete, by its nature, will
+ ### always delete all of its children, so it seems a bit
+ ### strange to worry about what is in the working copy.
+ */
+ if (kind == svn_node_dir)
+ {
+ svn_wc_schedule_t schedule;
+
+ /* ### Looking at schedule is probably enough, no need for
+ pristine compare etc. */
+ SVN_ERR(svn_wc__node_get_schedule(&schedule, NULL,
+ wc_ctx, target_abspath,
+ scratch_pool));
+
+ if (schedule == svn_wc_schedule_delete
+ || schedule == svn_wc_schedule_replace)
+ {
+ const apr_array_header_t *children;
+
+ SVN_ERR(svn_wc__node_get_children(&children, wc_ctx,
+ target_abspath, TRUE,
+ scratch_pool, scratch_pool));
+
+ if (children->nelts > 0)
+ return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Cannot delete the directory '%s' "
+ "in a non-recursive commit "
+ "because it has children"),
+ svn_dirent_local_style(target_abspath,
+ scratch_pool));
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Given a list of committables described by their common base abspath
+ BASE_ABSPATH and a list of relative dirents TARGET_RELPATHS determine
+ which absolute paths must be locked to commit all these targets and
+ return this as a const char * array in LOCK_TARGETS
+
+ Allocate the result in RESULT_POOL and use SCRATCH_POOL for temporary
+ storage */
+static svn_error_t *
+determine_lock_targets(apr_array_header_t **lock_targets,
+ svn_wc_context_t *wc_ctx,
+ const char *base_abspath,
+ const apr_array_header_t *target_relpaths,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_hash_t *wc_items; /* const char *wcroot -> apr_array_header_t */
+ apr_hash_index_t *hi;
+ int i;
+
+ wc_items = apr_hash_make(scratch_pool);
+
+ /* Create an array of targets for each working copy used */
+ for (i = 0; i < target_relpaths->nelts; i++)
+ {
+ const char *target_abspath;
+ const char *wcroot_abspath;
+ apr_array_header_t *wc_targets;
+ svn_error_t *err;
+ const char *target_relpath = APR_ARRAY_IDX(target_relpaths, i,
+ const char *);
+
+ svn_pool_clear(iterpool);
+ target_abspath = svn_dirent_join(base_abspath, target_relpath,
+ scratch_pool);
+
+ err = svn_wc__get_wcroot(&wcroot_abspath, wc_ctx, target_abspath,
+ iterpool, iterpool);
+
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ continue;
+ }
+ return svn_error_trace(err);
+ }
+
+ wc_targets = svn_hash_gets(wc_items, wcroot_abspath);
+
+ if (! wc_targets)
+ {
+ wc_targets = apr_array_make(scratch_pool, 4, sizeof(const char *));
+ svn_hash_sets(wc_items, apr_pstrdup(scratch_pool, wcroot_abspath),
+ wc_targets);
+ }
+
+ APR_ARRAY_PUSH(wc_targets, const char *) = target_abspath;
+ }
+
+ *lock_targets = apr_array_make(result_pool, apr_hash_count(wc_items),
+ sizeof(const char *));
+
+ /* For each working copy determine where to lock */
+ for (hi = apr_hash_first(scratch_pool, wc_items);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *common;
+ const char *wcroot_abspath = svn__apr_hash_index_key(hi);
+ apr_array_header_t *wc_targets = svn__apr_hash_index_val(hi);
+
+ svn_pool_clear(iterpool);
+
+ if (wc_targets->nelts == 1)
+ {
+ const char *target_abspath;
+ target_abspath = APR_ARRAY_IDX(wc_targets, 0, const char *);
+
+ if (! strcmp(wcroot_abspath, target_abspath))
+ {
+ APR_ARRAY_PUSH(*lock_targets, const char *)
+ = apr_pstrdup(result_pool, target_abspath);
+ }
+ else
+ {
+ /* Lock the parent to allow deleting the target */
+ APR_ARRAY_PUSH(*lock_targets, const char *)
+ = svn_dirent_dirname(target_abspath, result_pool);
+ }
+ }
+ else if (wc_targets->nelts > 1)
+ {
+ SVN_ERR(svn_dirent_condense_targets(&common, &wc_targets, wc_targets,
+ FALSE, iterpool, iterpool));
+
+ qsort(wc_targets->elts, wc_targets->nelts, wc_targets->elt_size,
+ svn_sort_compare_paths);
+
+ if (wc_targets->nelts == 0
+ || !svn_path_is_empty(APR_ARRAY_IDX(wc_targets, 0, const char*))
+ || !strcmp(common, wcroot_abspath))
+ {
+ APR_ARRAY_PUSH(*lock_targets, const char *)
+ = apr_pstrdup(result_pool, common);
+ }
+ else
+ {
+ /* Lock the parent to allow deleting the target */
+ APR_ARRAY_PUSH(*lock_targets, const char *)
+ = svn_dirent_dirname(common, result_pool);
+ }
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Baton for check_url_kind */
+struct check_url_kind_baton
+{
+ apr_pool_t *pool;
+ svn_ra_session_t *session;
+ const char *repos_root_url;
+ svn_client_ctx_t *ctx;
+};
+
+/* Implements svn_client__check_url_kind_t for svn_client_commit5 */
+static svn_error_t *
+check_url_kind(void *baton,
+ svn_node_kind_t *kind,
+ const char *url,
+ svn_revnum_t revision,
+ apr_pool_t *scratch_pool)
+{
+ struct check_url_kind_baton *cukb = baton;
+
+ /* If we don't have a session or can't use the session, get one */
+ if (!cukb->session || !svn_uri__is_ancestor(cukb->repos_root_url, url))
+ {
+ SVN_ERR(svn_client_open_ra_session2(&cukb->session, url, NULL, cukb->ctx,
+ cukb->pool, scratch_pool));
+ SVN_ERR(svn_ra_get_repos_root2(cukb->session, &cukb->repos_root_url,
+ cukb->pool));
+ }
+ else
+ SVN_ERR(svn_ra_reparent(cukb->session, url, scratch_pool));
+
+ return svn_error_trace(
+ svn_ra_check_path(cukb->session, "", revision,
+ kind, scratch_pool));
+}
+
+/* Recurse into every target in REL_TARGETS, finding committable externals
+ * nested within. Append these to REL_TARGETS itself. The paths in REL_TARGETS
+ * are assumed to be / will be created relative to BASE_ABSPATH. The remaining
+ * arguments correspond to those of svn_client_commit6(). */
+static svn_error_t*
+append_externals_as_explicit_targets(apr_array_header_t *rel_targets,
+ const char *base_abspath,
+ svn_boolean_t include_file_externals,
+ svn_boolean_t include_dir_externals,
+ svn_depth_t depth,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ int rel_targets_nelts_fixed;
+ int i;
+ apr_pool_t *iterpool;
+
+ if (! (include_file_externals || include_dir_externals))
+ return SVN_NO_ERROR;
+
+ /* Easy part of applying DEPTH to externals. */
+ if (depth == svn_depth_empty)
+ {
+ /* Don't recurse. */
+ return SVN_NO_ERROR;
+ }
+
+ /* Iterate *and* grow REL_TARGETS at the same time. */
+ rel_targets_nelts_fixed = rel_targets->nelts;
+
+ iterpool = svn_pool_create(scratch_pool);
+
+ for (i = 0; i < rel_targets_nelts_fixed; i++)
+ {
+ int j;
+ const char *target;
+ apr_array_header_t *externals = NULL;
+
+ svn_pool_clear(iterpool);
+
+ target = svn_dirent_join(base_abspath,
+ APR_ARRAY_IDX(rel_targets, i, const char *),
+ iterpool);
+
+ /* ### TODO: Possible optimization: No need to do this for file targets.
+ * ### But what's cheaper, stat'ing the file system or querying the db?
+ * ### --> future. */
+
+ SVN_ERR(svn_wc__committable_externals_below(&externals, ctx->wc_ctx,
+ target, depth,
+ iterpool, iterpool));
+
+ if (externals != NULL)
+ {
+ const char *rel_target;
+
+ for (j = 0; j < externals->nelts; j++)
+ {
+ svn_wc__committable_external_info_t *xinfo =
+ APR_ARRAY_IDX(externals, j,
+ svn_wc__committable_external_info_t *);
+
+ if ((xinfo->kind == svn_node_file && ! include_file_externals)
+ || (xinfo->kind == svn_node_dir && ! include_dir_externals))
+ continue;
+
+ rel_target = svn_dirent_skip_ancestor(base_abspath,
+ xinfo->local_abspath);
+
+ SVN_ERR_ASSERT(rel_target != NULL && *rel_target != '\0');
+
+ APR_ARRAY_PUSH(rel_targets, const char *) =
+ apr_pstrdup(result_pool, rel_target);
+ }
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_commit6(const apr_array_header_t *targets,
+ svn_depth_t depth,
+ svn_boolean_t keep_locks,
+ svn_boolean_t keep_changelists,
+ svn_boolean_t commit_as_operations,
+ svn_boolean_t include_file_externals,
+ svn_boolean_t include_dir_externals,
+ const apr_array_header_t *changelists,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const svn_delta_editor_t *editor;
+ void *edit_baton;
+ struct capture_baton_t cb;
+ svn_ra_session_t *ra_session;
+ const char *log_msg;
+ const char *base_abspath;
+ const char *base_url;
+ apr_array_header_t *rel_targets;
+ apr_array_header_t *lock_targets;
+ apr_array_header_t *locks_obtained;
+ svn_client__committables_t *committables;
+ apr_hash_t *lock_tokens;
+ apr_hash_t *sha1_checksums;
+ apr_array_header_t *commit_items;
+ svn_error_t *cmt_err = SVN_NO_ERROR;
+ svn_error_t *bump_err = SVN_NO_ERROR;
+ svn_error_t *unlock_err = SVN_NO_ERROR;
+ svn_boolean_t commit_in_progress = FALSE;
+ svn_boolean_t timestamp_sleep = FALSE;
+ svn_commit_info_t *commit_info = NULL;
+ apr_pool_t *iterpool = svn_pool_create(pool);
+ const char *current_abspath;
+ const char *notify_prefix;
+ int depth_empty_after = -1;
+ int i;
+
+ SVN_ERR_ASSERT(depth != svn_depth_unknown && depth != svn_depth_exclude);
+
+ /* Committing URLs doesn't make sense, so error if it's tried. */
+ for (i = 0; i < targets->nelts; i++)
+ {
+ const char *target = APR_ARRAY_IDX(targets, i, const char *);
+ if (svn_path_is_url(target))
+ return svn_error_createf
+ (SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is a URL, but URLs cannot be commit targets"), target);
+ }
+
+ /* Condense the target list. This makes all targets absolute. */
+ SVN_ERR(svn_dirent_condense_targets(&base_abspath, &rel_targets, targets,
+ FALSE, pool, iterpool));
+
+ /* No targets means nothing to commit, so just return. */
+ if (base_abspath == NULL)
+ return SVN_NO_ERROR;
+
+ SVN_ERR_ASSERT(rel_targets != NULL);
+
+ /* If we calculated only a base and no relative targets, this
+ must mean that we are being asked to commit (effectively) a
+ single path. */
+ if (rel_targets->nelts == 0)
+ APR_ARRAY_PUSH(rel_targets, const char *) = "";
+
+ if (include_file_externals || include_dir_externals)
+ {
+ if (depth != svn_depth_unknown && depth != svn_depth_infinity)
+ {
+ /* All targets after this will be handled as depth empty */
+ depth_empty_after = rel_targets->nelts;
+ }
+
+ SVN_ERR(append_externals_as_explicit_targets(rel_targets, base_abspath,
+ include_file_externals,
+ include_dir_externals,
+ depth, ctx,
+ pool, pool));
+ }
+
+ SVN_ERR(determine_lock_targets(&lock_targets, ctx->wc_ctx, base_abspath,
+ rel_targets, pool, iterpool));
+
+ locks_obtained = apr_array_make(pool, lock_targets->nelts,
+ sizeof(const char *));
+
+ for (i = 0; i < lock_targets->nelts; i++)
+ {
+ const char *lock_root;
+ const char *target = APR_ARRAY_IDX(lock_targets, i, const char *);
+
+ svn_pool_clear(iterpool);
+
+ cmt_err = svn_error_trace(
+ svn_wc__acquire_write_lock(&lock_root, ctx->wc_ctx, target,
+ FALSE, pool, iterpool));
+
+ if (cmt_err)
+ goto cleanup;
+
+ APR_ARRAY_PUSH(locks_obtained, const char *) = lock_root;
+ }
+
+ /* Determine prefix to strip from the commit notify messages */
+ SVN_ERR(svn_dirent_get_absolute(&current_abspath, "", pool));
+ notify_prefix = svn_dirent_get_longest_ancestor(current_abspath,
+ base_abspath,
+ pool);
+
+ /* If a non-recursive commit is desired, do not allow a deleted directory
+ as one of the targets. */
+ if (depth != svn_depth_infinity && ! commit_as_operations)
+ for (i = 0; i < rel_targets->nelts; i++)
+ {
+ const char *relpath = APR_ARRAY_IDX(rel_targets, i, const char *);
+ const char *target_abspath;
+
+ svn_pool_clear(iterpool);
+
+ target_abspath = svn_dirent_join(base_abspath, relpath, iterpool);
+
+ cmt_err = svn_error_trace(
+ check_nonrecursive_dir_delete(ctx->wc_ctx, target_abspath,
+ depth, iterpool));
+
+ if (cmt_err)
+ goto cleanup;
+ }
+
+ /* Crawl the working copy for commit items. */
+ {
+ struct check_url_kind_baton cukb;
+
+ /* Prepare for when we have a copy containing not-present nodes. */
+ cukb.pool = iterpool;
+ cukb.session = NULL; /* ### Can we somehow reuse session? */
+ cukb.repos_root_url = NULL;
+ cukb.ctx = ctx;
+
+ cmt_err = svn_error_trace(
+ svn_client__harvest_committables(&committables,
+ &lock_tokens,
+ base_abspath,
+ rel_targets,
+ depth_empty_after,
+ depth,
+ ! keep_locks,
+ changelists,
+ check_url_kind,
+ &cukb,
+ ctx,
+ pool,
+ iterpool));
+
+ svn_pool_clear(iterpool);
+ }
+
+ if (cmt_err)
+ goto cleanup;
+
+ if (apr_hash_count(committables->by_repository) == 0)
+ {
+ goto cleanup; /* Nothing to do */
+ }
+ else if (apr_hash_count(committables->by_repository) > 1)
+ {
+ cmt_err = svn_error_create(
+ SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Commit can only commit to a single repository at a time.\n"
+ "Are all targets part of the same working copy?"));
+ goto cleanup;
+ }
+
+ {
+ apr_hash_index_t *hi = apr_hash_first(iterpool,
+ committables->by_repository);
+
+ commit_items = svn__apr_hash_index_val(hi);
+ }
+
+ /* If our array of targets contains only locks (and no actual file
+ or prop modifications), then we return here to avoid committing a
+ revision with no changes. */
+ {
+ svn_boolean_t found_changed_path = FALSE;
+
+ for (i = 0; i < commit_items->nelts; ++i)
+ {
+ svn_client_commit_item3_t *item =
+ APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
+
+ if (item->state_flags != SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN)
+ {
+ found_changed_path = TRUE;
+ break;
+ }
+ }
+
+ if (!found_changed_path)
+ goto cleanup;
+ }
+
+ /* For every target that was moved verify that both halves of the
+ * move are part of the commit. */
+ for (i = 0; i < commit_items->nelts; i++)
+ {
+ svn_client_commit_item3_t *item =
+ APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
+
+ svn_pool_clear(iterpool);
+
+ if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_MOVED_HERE)
+ {
+ /* ### item->moved_from_abspath contains the move origin */
+ const char *moved_from_abspath;
+ const char *delete_op_root_abspath;
+
+ cmt_err = svn_error_trace(svn_wc__node_was_moved_here(
+ &moved_from_abspath,
+ &delete_op_root_abspath,
+ ctx->wc_ctx, item->path,
+ iterpool, iterpool));
+ if (cmt_err)
+ goto cleanup;
+
+ if (moved_from_abspath && delete_op_root_abspath &&
+ strcmp(moved_from_abspath, delete_op_root_abspath) == 0)
+
+ {
+ svn_boolean_t found_delete_half =
+ (svn_hash_gets(committables->by_path, delete_op_root_abspath)
+ != NULL);
+
+ if (!found_delete_half)
+ {
+ const char *delete_half_parent_abspath;
+
+ /* The delete-half isn't in the commit target list.
+ * However, it might itself be the child of a deleted node,
+ * either because of another move or a deletion.
+ *
+ * For example, consider: mv A/B B; mv B/C C; commit;
+ * C's moved-from A/B/C is a child of the deleted A/B.
+ * A/B/C does not appear in the commit target list, but
+ * A/B does appear.
+ * (Note that moved-from information is always stored
+ * relative to the BASE tree, so we have 'C moved-from
+ * A/B/C', not 'C moved-from B/C'.)
+ *
+ * An example involving a move and a delete would be:
+ * mv A/B C; rm A; commit;
+ * Now C is moved-from A/B which does not appear in the
+ * commit target list, but A does appear.
+ */
+
+ /* Scan upwards for a deletion op-root from the
+ * delete-half's parent directory. */
+ delete_half_parent_abspath =
+ svn_dirent_dirname(delete_op_root_abspath, iterpool);
+ if (strcmp(delete_op_root_abspath,
+ delete_half_parent_abspath) != 0)
+ {
+ const char *parent_delete_op_root_abspath;
+
+ cmt_err = svn_error_trace(
+ svn_wc__node_get_deleted_ancestor(
+ &parent_delete_op_root_abspath,
+ ctx->wc_ctx, delete_half_parent_abspath,
+ iterpool, iterpool));
+ if (cmt_err)
+ goto cleanup;
+
+ if (parent_delete_op_root_abspath)
+ found_delete_half =
+ (svn_hash_gets(committables->by_path,
+ parent_delete_op_root_abspath)
+ != NULL);
+ }
+ }
+
+ if (!found_delete_half)
+ {
+ cmt_err = svn_error_createf(
+ SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Cannot commit '%s' because it was moved from "
+ "'%s' which is not part of the commit; both "
+ "sides of the move must be committed together"),
+ svn_dirent_local_style(item->path, iterpool),
+ svn_dirent_local_style(delete_op_root_abspath,
+ iterpool));
+ goto cleanup;
+ }
+ }
+ }
+
+ if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
+ {
+ const char *moved_to_abspath;
+ const char *copy_op_root_abspath;
+
+ cmt_err = svn_error_trace(svn_wc__node_was_moved_away(
+ &moved_to_abspath,
+ &copy_op_root_abspath,
+ ctx->wc_ctx, item->path,
+ iterpool, iterpool));
+ if (cmt_err)
+ goto cleanup;
+
+ if (moved_to_abspath && copy_op_root_abspath &&
+ strcmp(moved_to_abspath, copy_op_root_abspath) == 0 &&
+ svn_hash_gets(committables->by_path, copy_op_root_abspath)
+ == NULL)
+ {
+ cmt_err = svn_error_createf(
+ SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Cannot commit '%s' because it was moved to '%s' "
+ "which is not part of the commit; both sides of "
+ "the move must be committed together"),
+ svn_dirent_local_style(item->path, iterpool),
+ svn_dirent_local_style(copy_op_root_abspath,
+ iterpool));
+ goto cleanup;
+ }
+ }
+ }
+
+ /* Go get a log message. If an error occurs, or no log message is
+ specified, abort the operation. */
+ if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx))
+ {
+ const char *tmp_file;
+ cmt_err = svn_error_trace(
+ svn_client__get_log_msg(&log_msg, &tmp_file, commit_items,
+ ctx, pool));
+
+ if (cmt_err || (! log_msg))
+ goto cleanup;
+ }
+ else
+ log_msg = "";
+
+ /* Sort and condense our COMMIT_ITEMS. */
+ cmt_err = svn_error_trace(svn_client__condense_commit_items(&base_url,
+ commit_items,
+ pool));
+
+ if (cmt_err)
+ goto cleanup;
+
+ /* Collect our lock tokens with paths relative to base_url. */
+ cmt_err = svn_error_trace(collect_lock_tokens(&lock_tokens, lock_tokens,
+ base_url, pool));
+
+ if (cmt_err)
+ goto cleanup;
+
+ cb.original_callback = commit_callback;
+ cb.original_baton = commit_baton;
+ cb.info = &commit_info;
+ cb.pool = pool;
+
+ /* Get the RA editor from the first lock target, rather than BASE_ABSPATH.
+ * When committing from multiple WCs, BASE_ABSPATH might be an unrelated
+ * parent of nested working copies. We don't support commits to multiple
+ * repositories so using the first WC to get the RA session is safe. */
+ cmt_err = svn_error_trace(
+ svn_client__open_ra_session_internal(&ra_session, NULL, base_url,
+ APR_ARRAY_IDX(lock_targets,
+ 0,
+ const char *),
+ commit_items,
+ TRUE, TRUE, ctx,
+ pool, pool));
+
+ if (cmt_err)
+ goto cleanup;
+
+ cmt_err = svn_error_trace(
+ get_ra_editor(&editor, &edit_baton, ra_session, ctx,
+ log_msg, commit_items, revprop_table,
+ lock_tokens, keep_locks, capture_commit_info,
+ &cb, pool));
+
+ if (cmt_err)
+ goto cleanup;
+
+ /* Make a note that we have a commit-in-progress. */
+ commit_in_progress = TRUE;
+
+ /* We'll assume that, once we pass this point, we are going to need to
+ * sleep for timestamps. Really, we may not need to do unless and until
+ * we reach the point where we post-commit 'bump' the WC metadata. */
+ timestamp_sleep = TRUE;
+
+ /* Perform the commit. */
+ cmt_err = svn_error_trace(
+ svn_client__do_commit(base_url, commit_items, editor, edit_baton,
+ notify_prefix, &sha1_checksums, ctx, pool,
+ iterpool));
+
+ /* Handle a successful commit. */
+ if ((! cmt_err)
+ || (cmt_err->apr_err == SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED))
+ {
+ svn_wc_committed_queue_t *queue = svn_wc_committed_queue_create(pool);
+
+ /* Make a note that our commit is finished. */
+ commit_in_progress = FALSE;
+
+ for (i = 0; i < commit_items->nelts; i++)
+ {
+ svn_client_commit_item3_t *item
+ = APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
+
+ svn_pool_clear(iterpool);
+ bump_err = post_process_commit_item(
+ queue, item, ctx->wc_ctx,
+ keep_changelists, keep_locks, commit_as_operations,
+ svn_hash_gets(sha1_checksums, item->path),
+ iterpool);
+ if (bump_err)
+ goto cleanup;
+ }
+
+ SVN_ERR_ASSERT(commit_info);
+ bump_err = svn_wc_process_committed_queue2(
+ queue, ctx->wc_ctx,
+ commit_info->revision,
+ commit_info->date,
+ commit_info->author,
+ ctx->cancel_func, ctx->cancel_baton,
+ iterpool);
+ }
+
+ cleanup:
+ /* Sleep to ensure timestamp integrity. */
+ if (timestamp_sleep)
+ svn_io_sleep_for_timestamps(base_abspath, pool);
+
+ /* Abort the commit if it is still in progress. */
+ svn_pool_clear(iterpool); /* Close open handles before aborting */
+ if (commit_in_progress)
+ cmt_err = svn_error_compose_create(cmt_err,
+ editor->abort_edit(edit_baton, pool));
+
+ /* A bump error is likely to occur while running a working copy log file,
+ explicitly unlocking and removing temporary files would be wrong in
+ that case. A commit error (cmt_err) should only occur before any
+ attempt to modify the working copy, so it doesn't prevent explicit
+ clean-up. */
+ if (! bump_err)
+ {
+ /* Release all locks we obtained */
+ for (i = 0; i < locks_obtained->nelts; i++)
+ {
+ const char *lock_root = APR_ARRAY_IDX(locks_obtained, i,
+ const char *);
+
+ svn_pool_clear(iterpool);
+
+ unlock_err = svn_error_compose_create(
+ svn_wc__release_write_lock(ctx->wc_ctx, lock_root,
+ iterpool),
+ unlock_err);
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return svn_error_trace(reconcile_errors(cmt_err, unlock_err, bump_err,
+ pool));
+}
diff --git a/subversion/libsvn_client/commit_util.c b/subversion/libsvn_client/commit_util.c
new file mode 100644
index 0000000..1e2c50c
--- /dev/null
+++ b/subversion/libsvn_client/commit_util.c
@@ -0,0 +1,1981 @@
+/*
+ * commit_util.c: Driver for the WC commit process.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+#include <string.h>
+
+#include <apr_pools.h>
+#include <apr_hash.h>
+#include <apr_md5.h>
+
+#include "client.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_types.h"
+#include "svn_pools.h"
+#include "svn_props.h"
+#include "svn_iter.h"
+#include "svn_hash.h"
+
+#include <assert.h>
+#include <stdlib.h> /* for qsort() */
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_client_private.h"
+
+/*** Uncomment this to turn on commit driver debugging. ***/
+/*
+#define SVN_CLIENT_COMMIT_DEBUG
+*/
+
+/* Wrap an RA error in a nicer error if one is available. */
+static svn_error_t *
+fixup_commit_error(const char *local_abspath,
+ const char *base_url,
+ const char *path,
+ svn_node_kind_t kind,
+ svn_error_t *err,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ if (err->apr_err == SVN_ERR_FS_NOT_FOUND
+ || err->apr_err == SVN_ERR_FS_ALREADY_EXISTS
+ || err->apr_err == SVN_ERR_FS_TXN_OUT_OF_DATE
+ || err->apr_err == SVN_ERR_RA_DAV_PATH_NOT_FOUND
+ || err->apr_err == SVN_ERR_RA_DAV_ALREADY_EXISTS
+ || svn_error_find_cause(err, SVN_ERR_RA_OUT_OF_DATE))
+ {
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+
+ if (local_abspath)
+ notify = svn_wc_create_notify(local_abspath,
+ svn_wc_notify_failed_out_of_date,
+ scratch_pool);
+ else
+ notify = svn_wc_create_notify_url(
+ svn_path_url_add_component2(base_url, path,
+ scratch_pool),
+ svn_wc_notify_failed_out_of_date,
+ scratch_pool);
+
+ notify->kind = kind;
+ notify->err = err;
+
+ ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
+ }
+
+ return svn_error_createf(SVN_ERR_WC_NOT_UP_TO_DATE, err,
+ (kind == svn_node_dir
+ ? _("Directory '%s' is out of date")
+ : _("File '%s' is out of date")),
+ local_abspath
+ ? svn_dirent_local_style(local_abspath,
+ scratch_pool)
+ : svn_path_url_add_component2(base_url,
+ path,
+ scratch_pool));
+ }
+ else if (svn_error_find_cause(err, SVN_ERR_FS_NO_LOCK_TOKEN)
+ || err->apr_err == SVN_ERR_FS_LOCK_OWNER_MISMATCH
+ || err->apr_err == SVN_ERR_RA_NOT_LOCKED)
+ {
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+
+ if (local_abspath)
+ notify = svn_wc_create_notify(local_abspath,
+ svn_wc_notify_failed_locked,
+ scratch_pool);
+ else
+ notify = svn_wc_create_notify_url(
+ svn_path_url_add_component2(base_url, path,
+ scratch_pool),
+ svn_wc_notify_failed_locked,
+ scratch_pool);
+
+ notify->kind = kind;
+ notify->err = err;
+
+ ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
+ }
+
+ return svn_error_createf(SVN_ERR_CLIENT_NO_LOCK_TOKEN, err,
+ (kind == svn_node_dir
+ ? _("Directory '%s' is locked in another working copy")
+ : _("File '%s' is locked in another working copy")),
+ local_abspath
+ ? svn_dirent_local_style(local_abspath,
+ scratch_pool)
+ : svn_path_url_add_component2(base_url,
+ path,
+ scratch_pool));
+ }
+ else if (svn_error_find_cause(err, SVN_ERR_RA_DAV_FORBIDDEN)
+ || err->apr_err == SVN_ERR_AUTHZ_UNWRITABLE)
+ {
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+
+ if (local_abspath)
+ notify = svn_wc_create_notify(
+ local_abspath,
+ svn_wc_notify_failed_forbidden_by_server,
+ scratch_pool);
+ else
+ notify = svn_wc_create_notify_url(
+ svn_path_url_add_component2(base_url, path,
+ scratch_pool),
+ svn_wc_notify_failed_forbidden_by_server,
+ scratch_pool);
+
+ notify->kind = kind;
+ notify->err = err;
+
+ ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
+ }
+
+ return svn_error_createf(SVN_ERR_CLIENT_FORBIDDEN_BY_SERVER, err,
+ (kind == svn_node_dir
+ ? _("Changing directory '%s' is forbidden by the server")
+ : _("Changing file '%s' is forbidden by the server")),
+ local_abspath
+ ? svn_dirent_local_style(local_abspath,
+ scratch_pool)
+ : svn_path_url_add_component2(base_url,
+ path,
+ scratch_pool));
+ }
+ else
+ return err;
+}
+
+
+/*** Harvesting Commit Candidates ***/
+
+
+/* Add a new commit candidate (described by all parameters except
+ `COMMITTABLES') to the COMMITTABLES hash. All of the commit item's
+ members are allocated out of RESULT_POOL.
+
+ If the state flag specifies that a lock must be used, store the token in LOCK
+ in lock_tokens.
+ */
+static svn_error_t *
+add_committable(svn_client__committables_t *committables,
+ const char *local_abspath,
+ svn_node_kind_t kind,
+ const char *repos_root_url,
+ const char *repos_relpath,
+ svn_revnum_t revision,
+ const char *copyfrom_relpath,
+ svn_revnum_t copyfrom_rev,
+ const char *moved_from_abspath,
+ apr_byte_t state_flags,
+ apr_hash_t *lock_tokens,
+ const svn_lock_t *lock,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *array;
+ svn_client_commit_item3_t *new_item;
+
+ /* Sanity checks. */
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+ SVN_ERR_ASSERT(repos_root_url && repos_relpath);
+
+ /* ### todo: Get the canonical repository for this item, which will
+ be the real key for the COMMITTABLES hash, instead of the above
+ bogosity. */
+ array = svn_hash_gets(committables->by_repository, repos_root_url);
+
+ /* E-gads! There is no array for this repository yet! Oh, no
+ problem, we'll just create (and add to the hash) one. */
+ if (array == NULL)
+ {
+ array = apr_array_make(result_pool, 1, sizeof(new_item));
+ svn_hash_sets(committables->by_repository,
+ apr_pstrdup(result_pool, repos_root_url), array);
+ }
+
+ /* Now update pointer values, ensuring that their allocations live
+ in POOL. */
+ new_item = svn_client_commit_item3_create(result_pool);
+ new_item->path = apr_pstrdup(result_pool, local_abspath);
+ new_item->kind = kind;
+ new_item->url = svn_path_url_add_component2(repos_root_url,
+ repos_relpath,
+ result_pool);
+ new_item->revision = revision;
+ new_item->copyfrom_url = copyfrom_relpath
+ ? svn_path_url_add_component2(repos_root_url,
+ copyfrom_relpath,
+ result_pool)
+ : NULL;
+ new_item->copyfrom_rev = copyfrom_rev;
+ new_item->state_flags = state_flags;
+ new_item->incoming_prop_changes = apr_array_make(result_pool, 1,
+ sizeof(svn_prop_t *));
+
+ if (moved_from_abspath)
+ new_item->moved_from_abspath = apr_pstrdup(result_pool,
+ moved_from_abspath);
+
+ /* Now, add the commit item to the array. */
+ APR_ARRAY_PUSH(array, svn_client_commit_item3_t *) = new_item;
+
+ /* ... and to the hash. */
+ svn_hash_sets(committables->by_path, new_item->path, new_item);
+
+ if (lock
+ && lock_tokens
+ && (state_flags & SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN))
+ {
+ svn_hash_sets(lock_tokens, new_item->url,
+ apr_pstrdup(result_pool, lock->token));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* If there is a commit item for PATH in COMMITTABLES, return it, else
+ return NULL. Use POOL for temporary allocation only. */
+static svn_client_commit_item3_t *
+look_up_committable(svn_client__committables_t *committables,
+ const char *path,
+ apr_pool_t *pool)
+{
+ return (svn_client_commit_item3_t *)
+ svn_hash_gets(committables->by_path, path);
+}
+
+/* Helper function for svn_client__harvest_committables().
+ * Determine whether we are within a tree-conflicted subtree of the
+ * working copy and return an SVN_ERR_WC_FOUND_CONFLICT error if so. */
+static svn_error_t *
+bail_on_tree_conflicted_ancestor(svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ svn_wc_notify_func2_t notify_func,
+ void *notify_baton,
+ apr_pool_t *scratch_pool)
+{
+ const char *wcroot_abspath;
+
+ SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+
+ local_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
+
+ while(svn_dirent_is_ancestor(wcroot_abspath, local_abspath))
+ {
+ svn_boolean_t tree_conflicted;
+
+ /* Check if the parent has tree conflicts */
+ SVN_ERR(svn_wc_conflicted_p3(NULL, NULL, &tree_conflicted,
+ wc_ctx, local_abspath, scratch_pool));
+ if (tree_conflicted)
+ {
+ if (notify_func != NULL)
+ {
+ notify_func(notify_baton,
+ svn_wc_create_notify(local_abspath,
+ svn_wc_notify_failed_conflict,
+ scratch_pool),
+ scratch_pool);
+ }
+
+ return svn_error_createf(
+ SVN_ERR_WC_FOUND_CONFLICT, NULL,
+ _("Aborting commit: '%s' remains in tree-conflict"),
+ svn_dirent_local_style(local_abspath, scratch_pool));
+ }
+
+ /* Step outwards */
+ if (svn_dirent_is_root(local_abspath, strlen(local_abspath)))
+ break;
+ else
+ local_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Recursively search for commit candidates in (and under) LOCAL_ABSPATH using
+ WC_CTX and add those candidates to COMMITTABLES. If in ADDS_ONLY modes,
+ only new additions are recognized.
+
+ DEPTH indicates how to treat files and subdirectories of LOCAL_ABSPATH
+ when LOCAL_ABSPATH is itself a directory; see
+ svn_client__harvest_committables() for its behavior.
+
+ Lock tokens of candidates will be added to LOCK_TOKENS, if
+ non-NULL. JUST_LOCKED indicates whether to treat non-modified items with
+ lock tokens as commit candidates.
+
+ If COMMIT_RELPATH is not NULL, treat not-added nodes as if it is destined to
+ be added as COMMIT_RELPATH, and add 'deleted' entries to COMMITTABLES as
+ items to delete in the copy destination. COPY_MODE_ROOT should be set TRUE
+ for the first call for which COPY_MODE is TRUE, i.e. not for the
+ recursive calls, and FALSE otherwise.
+
+ If CHANGELISTS is non-NULL, it is a hash whose keys are const char *
+ changelist names used as a restrictive filter
+ when harvesting committables; that is, don't add a path to
+ COMMITTABLES unless it's a member of one of those changelists.
+
+ IS_EXPLICIT_TARGET should always be passed as TRUE, except when
+ harvest_committables() calls itself in recursion. This provides a way to
+ tell whether LOCAL_ABSPATH was an original target or whether it was reached
+ by recursing deeper into a dir target. (This is used to skip all file
+ externals that aren't explicit commit targets.)
+
+ DANGLERS is a hash table mapping const char* absolute paths of a parent
+ to a const char * absolute path of a child. See the comment about
+ danglers at the top of svn_client__harvest_committables().
+
+ If CANCEL_FUNC is non-null, call it with CANCEL_BATON to see
+ if the user has cancelled the operation.
+
+ Any items added to COMMITTABLES are allocated from the COMITTABLES
+ hash pool, not POOL. SCRATCH_POOL is used for temporary allocations. */
+
+struct harvest_baton
+{
+ /* Static data */
+ const char *root_abspath;
+ svn_client__committables_t *committables;
+ apr_hash_t *lock_tokens;
+ const char *commit_relpath; /* Valid for the harvest root */
+ svn_depth_t depth;
+ svn_boolean_t just_locked;
+ apr_hash_t *changelists;
+ apr_hash_t *danglers;
+ svn_client__check_url_kind_t check_url_func;
+ void *check_url_baton;
+ svn_wc_notify_func2_t notify_func;
+ void *notify_baton;
+ svn_wc_context_t *wc_ctx;
+ apr_pool_t *result_pool;
+
+ /* Harvester state */
+ const char *skip_below_abspath; /* If non-NULL, skip everything below */
+};
+
+static svn_error_t *
+harvest_status_callback(void *status_baton,
+ const char *local_abspath,
+ const svn_wc_status3_t *status,
+ apr_pool_t *scratch_pool);
+
+static svn_error_t *
+harvest_committables(const char *local_abspath,
+ svn_client__committables_t *committables,
+ apr_hash_t *lock_tokens,
+ const char *copy_mode_relpath,
+ svn_depth_t depth,
+ svn_boolean_t just_locked,
+ apr_hash_t *changelists,
+ apr_hash_t *danglers,
+ svn_client__check_url_kind_t check_url_func,
+ void *check_url_baton,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ svn_wc_notify_func2_t notify_func,
+ void *notify_baton,
+ svn_wc_context_t *wc_ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct harvest_baton baton;
+
+ SVN_ERR_ASSERT((just_locked && lock_tokens) || !just_locked);
+
+ baton.root_abspath = local_abspath;
+ baton.committables = committables;
+ baton.lock_tokens = lock_tokens;
+ baton.commit_relpath = copy_mode_relpath;
+ baton.depth = depth;
+ baton.just_locked = just_locked;
+ baton.changelists = changelists;
+ baton.danglers = danglers;
+ baton.check_url_func = check_url_func;
+ baton.check_url_baton = check_url_baton;
+ baton.notify_func = notify_func;
+ baton.notify_baton = notify_baton;
+ baton.wc_ctx = wc_ctx;
+ baton.result_pool = result_pool;
+
+ baton.skip_below_abspath = NULL;
+
+ SVN_ERR(svn_wc_walk_status(wc_ctx,
+ local_abspath,
+ depth,
+ (copy_mode_relpath != NULL) /* get_all */,
+ FALSE /* no_ignore */,
+ FALSE /* ignore_text_mods */,
+ NULL /* ignore_patterns */,
+ harvest_status_callback,
+ &baton,
+ cancel_func, cancel_baton,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+harvest_not_present_for_copy(svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ svn_client__committables_t *committables,
+ const char *repos_root_url,
+ const char *commit_relpath,
+ svn_client__check_url_kind_t check_url_func,
+ void *check_url_baton,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const apr_array_header_t *children;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ int i;
+
+ /* A function to retrieve not present children would be nice to have */
+ SVN_ERR(svn_wc__node_get_children_of_working_node(
+ &children, wc_ctx, local_abspath, TRUE,
+ scratch_pool, iterpool));
+
+ for (i = 0; i < children->nelts; i++)
+ {
+ const char *this_abspath = APR_ARRAY_IDX(children, i, const char *);
+ const char *name = svn_dirent_basename(this_abspath, NULL);
+ const char *this_commit_relpath;
+ svn_boolean_t not_present;
+ svn_node_kind_t kind;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_wc__node_is_not_present(&not_present, NULL, NULL, wc_ctx,
+ this_abspath, FALSE, scratch_pool));
+
+ if (!not_present)
+ continue;
+
+ if (commit_relpath == NULL)
+ this_commit_relpath = NULL;
+ else
+ this_commit_relpath = svn_relpath_join(commit_relpath, name,
+ iterpool);
+
+ /* We should check if we should really add a delete operation */
+ if (check_url_func)
+ {
+ svn_revnum_t parent_rev;
+ const char *parent_repos_relpath;
+ const char *parent_repos_root_url;
+ const char *node_url;
+
+ /* Determine from what parent we would be the deleted child */
+ SVN_ERR(svn_wc__node_get_origin(
+ NULL, &parent_rev, &parent_repos_relpath,
+ &parent_repos_root_url, NULL, NULL,
+ wc_ctx,
+ svn_dirent_dirname(this_abspath,
+ scratch_pool),
+ FALSE, scratch_pool, scratch_pool));
+
+ node_url = svn_path_url_add_component2(
+ svn_path_url_add_component2(parent_repos_root_url,
+ parent_repos_relpath,
+ scratch_pool),
+ svn_dirent_basename(this_abspath, NULL),
+ iterpool);
+
+ SVN_ERR(check_url_func(check_url_baton, &kind,
+ node_url, parent_rev, iterpool));
+
+ if (kind == svn_node_none)
+ continue; /* This node can't be deleted */
+ }
+ else
+ SVN_ERR(svn_wc_read_kind2(&kind, wc_ctx, this_abspath,
+ TRUE, TRUE, scratch_pool));
+
+ SVN_ERR(add_committable(committables, this_abspath, kind,
+ repos_root_url,
+ this_commit_relpath,
+ SVN_INVALID_REVNUM,
+ NULL /* copyfrom_relpath */,
+ SVN_INVALID_REVNUM /* copyfrom_rev */,
+ NULL /* moved_from_abspath */,
+ SVN_CLIENT_COMMIT_ITEM_DELETE,
+ NULL, NULL,
+ result_pool, scratch_pool));
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Implements svn_wc_status_func4_t */
+static svn_error_t *
+harvest_status_callback(void *status_baton,
+ const char *local_abspath,
+ const svn_wc_status3_t *status,
+ apr_pool_t *scratch_pool)
+{
+ apr_byte_t state_flags = 0;
+ svn_revnum_t node_rev;
+ const char *cf_relpath = NULL;
+ svn_revnum_t cf_rev = SVN_INVALID_REVNUM;
+ svn_boolean_t matches_changelists;
+ svn_boolean_t is_added;
+ svn_boolean_t is_deleted;
+ svn_boolean_t is_replaced;
+ svn_boolean_t is_op_root;
+ svn_revnum_t original_rev;
+ const char *original_relpath;
+ svn_boolean_t copy_mode;
+
+ struct harvest_baton *baton = status_baton;
+ svn_boolean_t is_harvest_root =
+ (strcmp(baton->root_abspath, local_abspath) == 0);
+ svn_client__committables_t *committables = baton->committables;
+ const char *repos_root_url = status->repos_root_url;
+ const char *commit_relpath = NULL;
+ svn_boolean_t copy_mode_root = (baton->commit_relpath && is_harvest_root);
+ svn_boolean_t just_locked = baton->just_locked;
+ apr_hash_t *changelists = baton->changelists;
+ svn_wc_notify_func2_t notify_func = baton->notify_func;
+ void *notify_baton = baton->notify_baton;
+ svn_wc_context_t *wc_ctx = baton->wc_ctx;
+ apr_pool_t *result_pool = baton->result_pool;
+ const char *moved_from_abspath = NULL;
+
+ if (baton->commit_relpath)
+ commit_relpath = svn_relpath_join(
+ baton->commit_relpath,
+ svn_dirent_skip_ancestor(baton->root_abspath,
+ local_abspath),
+ scratch_pool);
+
+ copy_mode = (commit_relpath != NULL);
+
+ if (baton->skip_below_abspath
+ && svn_dirent_is_ancestor(baton->skip_below_abspath, local_abspath))
+ {
+ return SVN_NO_ERROR;
+ }
+ else
+ baton->skip_below_abspath = NULL; /* We have left the skip tree */
+
+ /* Return early for nodes that don't have a committable status */
+ switch (status->node_status)
+ {
+ case svn_wc_status_unversioned:
+ case svn_wc_status_ignored:
+ case svn_wc_status_external:
+ case svn_wc_status_none:
+ /* Unversioned nodes aren't committable, but are reported by the status
+ walker.
+ But if the unversioned node is the root of the walk, we have a user
+ error */
+ if (is_harvest_root)
+ return svn_error_createf(
+ SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not under version control"),
+ svn_dirent_local_style(local_abspath, scratch_pool));
+ return SVN_NO_ERROR;
+ case svn_wc_status_normal:
+ /* Status normal nodes aren't modified, so we don't have to commit them
+ when we perform a normal commit. But if a node is conflicted we want
+ to stop the commit and if we are collecting lock tokens we want to
+ look further anyway.
+
+ When in copy mode we need to compare the revision of the node against
+ the parent node to copy mixed-revision base nodes properly */
+ if (!copy_mode && !status->conflicted
+ && !(just_locked && status->lock))
+ return SVN_NO_ERROR;
+ break;
+ default:
+ /* Fall through */
+ break;
+ }
+
+ /* Early out if the item is already marked as committable. */
+ if (look_up_committable(committables, local_abspath, scratch_pool))
+ return SVN_NO_ERROR;
+
+ SVN_ERR_ASSERT((copy_mode && commit_relpath)
+ || (! copy_mode && ! commit_relpath));
+ SVN_ERR_ASSERT((copy_mode_root && copy_mode) || ! copy_mode_root);
+
+ /* Save the result for reuse. */
+ matches_changelists = ((changelists == NULL)
+ || (status->changelist != NULL
+ && svn_hash_gets(changelists, status->changelist)
+ != NULL));
+
+ /* Early exit. */
+ if (status->kind != svn_node_dir && ! matches_changelists)
+ {
+ return SVN_NO_ERROR;
+ }
+
+ /* If NODE is in our changelist, then examine it for conflicts. We
+ need to bail out if any conflicts exist.
+ The status walker checked for conflict marker removal. */
+ if (status->conflicted && matches_changelists)
+ {
+ if (notify_func != NULL)
+ {
+ notify_func(notify_baton,
+ svn_wc_create_notify(local_abspath,
+ svn_wc_notify_failed_conflict,
+ scratch_pool),
+ scratch_pool);
+ }
+
+ return svn_error_createf(
+ SVN_ERR_WC_FOUND_CONFLICT, NULL,
+ _("Aborting commit: '%s' remains in conflict"),
+ svn_dirent_local_style(local_abspath, scratch_pool));
+ }
+ else if (status->node_status == svn_wc_status_obstructed)
+ {
+ /* A node's type has changed before attempting to commit.
+ This also catches symlink vs non symlink changes */
+
+ if (notify_func != NULL)
+ {
+ notify_func(notify_baton,
+ svn_wc_create_notify(local_abspath,
+ svn_wc_notify_failed_obstruction,
+ scratch_pool),
+ scratch_pool);
+ }
+
+ return svn_error_createf(
+ SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
+ _("Node '%s' has unexpectedly changed kind"),
+ svn_dirent_local_style(local_abspath, scratch_pool));
+ }
+
+ if (status->conflicted && status->kind == svn_node_unknown)
+ return SVN_NO_ERROR; /* Ignore delete-delete conflict */
+
+ /* Return error on unknown path kinds. We check both the entry and
+ the node itself, since a path might have changed kind since its
+ entry was written. */
+ SVN_ERR(svn_wc__node_get_commit_status(&is_added, &is_deleted,
+ &is_replaced,
+ &is_op_root,
+ &node_rev,
+ &original_rev, &original_relpath,
+ wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+
+ /* Hande file externals only when passed as explicit target. Note that
+ * svn_client_commit6() passes all committable externals in as explicit
+ * targets iff they count. */
+ if (status->file_external && !is_harvest_root)
+ {
+ return SVN_NO_ERROR;
+ }
+
+ if (status->node_status == svn_wc_status_missing && matches_changelists)
+ {
+ /* Added files and directories must exist. See issue #3198. */
+ if (is_added && is_op_root)
+ {
+ if (notify_func != NULL)
+ {
+ notify_func(notify_baton,
+ svn_wc_create_notify(local_abspath,
+ svn_wc_notify_failed_missing,
+ scratch_pool),
+ scratch_pool);
+ }
+ return svn_error_createf(
+ SVN_ERR_WC_PATH_NOT_FOUND, NULL,
+ _("'%s' is scheduled for addition, but is missing"),
+ svn_dirent_local_style(local_abspath, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+ }
+
+ if (is_deleted && !is_op_root /* && !is_added */)
+ return SVN_NO_ERROR; /* Not an operational delete and not an add. */
+
+ /* Check for the deletion case.
+ * We delete explicitly deleted nodes (duh!)
+ * We delete not-present children of copies
+ * We delete nodes that directly replace a node in its ancestor
+ */
+
+ if (is_deleted || is_replaced)
+ state_flags |= SVN_CLIENT_COMMIT_ITEM_DELETE;
+
+ /* Check for adds and copies */
+ if (is_added && is_op_root)
+ {
+ /* Root of local add or copy */
+ state_flags |= SVN_CLIENT_COMMIT_ITEM_ADD;
+
+ if (original_relpath)
+ {
+ /* Root of copy */
+ state_flags |= SVN_CLIENT_COMMIT_ITEM_IS_COPY;
+ cf_relpath = original_relpath;
+ cf_rev = original_rev;
+
+ if (status->moved_from_abspath && !copy_mode)
+ {
+ state_flags |= SVN_CLIENT_COMMIT_ITEM_MOVED_HERE;
+ moved_from_abspath = status->moved_from_abspath;
+ }
+ }
+ }
+
+ /* Further copies may occur in copy mode. */
+ else if (copy_mode
+ && !(state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
+ {
+ svn_revnum_t dir_rev = SVN_INVALID_REVNUM;
+
+ if (!copy_mode_root && !status->switched && !is_added)
+ SVN_ERR(svn_wc__node_get_base(NULL, &dir_rev, NULL, NULL, NULL, NULL,
+ wc_ctx, svn_dirent_dirname(local_abspath,
+ scratch_pool),
+ FALSE /* ignore_enoent */,
+ FALSE /* show_hidden */,
+ scratch_pool, scratch_pool));
+
+ if (copy_mode_root || status->switched || node_rev != dir_rev)
+ {
+ state_flags |= (SVN_CLIENT_COMMIT_ITEM_ADD
+ | SVN_CLIENT_COMMIT_ITEM_IS_COPY);
+
+ if (status->copied)
+ {
+ /* Copy from original location */
+ cf_rev = original_rev;
+ cf_relpath = original_relpath;
+ }
+ else
+ {
+ /* Copy BASE location, to represent a mixed-rev or switch copy */
+ cf_rev = status->revision;
+ cf_relpath = status->repos_relpath;
+ }
+ }
+ }
+
+ if (!(state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
+ || (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD))
+ {
+ svn_boolean_t text_mod = FALSE;
+ svn_boolean_t prop_mod = FALSE;
+
+ if (status->kind == svn_node_file)
+ {
+ /* Check for text modifications on files */
+ if ((state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
+ && ! (state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY))
+ {
+ text_mod = TRUE; /* Local added files are always modified */
+ }
+ else
+ text_mod = (status->text_status != svn_wc_status_normal);
+ }
+
+ prop_mod = (status->prop_status != svn_wc_status_normal
+ && status->prop_status != svn_wc_status_none);
+
+ /* Set text/prop modification flags accordingly. */
+ if (text_mod)
+ state_flags |= SVN_CLIENT_COMMIT_ITEM_TEXT_MODS;
+ if (prop_mod)
+ state_flags |= SVN_CLIENT_COMMIT_ITEM_PROP_MODS;
+ }
+
+ /* If the entry has a lock token and it is already a commit candidate,
+ or the caller wants unmodified locked items to be treated as
+ such, note this fact. */
+ if (status->lock && baton->lock_tokens && (state_flags || just_locked))
+ {
+ state_flags |= SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN;
+ }
+
+ /* Now, if this is something to commit, add it to our list. */
+ if (matches_changelists
+ && state_flags)
+ {
+ /* Finally, add the committable item. */
+ SVN_ERR(add_committable(committables, local_abspath,
+ status->kind,
+ repos_root_url,
+ copy_mode
+ ? commit_relpath
+ : status->repos_relpath,
+ copy_mode
+ ? SVN_INVALID_REVNUM
+ : node_rev,
+ cf_relpath,
+ cf_rev,
+ moved_from_abspath,
+ state_flags,
+ baton->lock_tokens, status->lock,
+ result_pool, scratch_pool));
+ }
+
+ /* Fetch lock tokens for descendants of deleted BASE nodes. */
+ if (matches_changelists
+ && (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
+ && !copy_mode
+ && SVN_IS_VALID_REVNUM(node_rev) /* && BASE-kind = dir */
+ && baton->lock_tokens)
+ {
+ apr_hash_t *local_relpath_tokens;
+ apr_hash_index_t *hi;
+
+ SVN_ERR(svn_wc__node_get_lock_tokens_recursive(
+ &local_relpath_tokens, wc_ctx, local_abspath,
+ result_pool, scratch_pool));
+
+ /* Add tokens to existing hash. */
+ for (hi = apr_hash_first(scratch_pool, local_relpath_tokens);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const void *key;
+ apr_ssize_t klen;
+ void * val;
+
+ apr_hash_this(hi, &key, &klen, &val);
+
+ apr_hash_set(baton->lock_tokens, key, klen, val);
+ }
+ }
+
+ /* Make sure we check for dangling children on additions
+
+ We perform this operation on the harvest root, and on roots caused by
+ changelist filtering.
+ */
+ if (matches_changelists
+ && (is_harvest_root || baton->changelists)
+ && state_flags
+ && is_added
+ && baton->danglers)
+ {
+ /* If a node is added, its parent must exist in the repository at the
+ time of committing */
+ apr_hash_t *danglers = baton->danglers;
+ svn_boolean_t parent_added;
+ const char *parent_abspath = svn_dirent_dirname(local_abspath,
+ scratch_pool);
+
+ /* First check if parent is already in the list of commits
+ (Common case for GUI clients that provide a list of commit targets) */
+ if (look_up_committable(committables, parent_abspath, scratch_pool))
+ parent_added = FALSE; /* Skip all expensive checks */
+ else
+ SVN_ERR(svn_wc__node_is_added(&parent_added, wc_ctx, parent_abspath,
+ scratch_pool));
+
+ if (parent_added)
+ {
+ const char *copy_root_abspath;
+ svn_boolean_t parent_is_copy;
+
+ /* The parent is added, so either it is a copy, or a locally added
+ * directory. In either case, we require the op-root of the parent
+ * to be part of the commit. See issue #4059. */
+ SVN_ERR(svn_wc__node_get_origin(&parent_is_copy, NULL, NULL, NULL,
+ NULL, &copy_root_abspath,
+ wc_ctx, parent_abspath,
+ FALSE, scratch_pool, scratch_pool));
+
+ if (parent_is_copy)
+ parent_abspath = copy_root_abspath;
+
+ if (!svn_hash_gets(danglers, parent_abspath))
+ {
+ svn_hash_sets(danglers, apr_pstrdup(result_pool, parent_abspath),
+ apr_pstrdup(result_pool, local_abspath));
+ }
+ }
+ }
+
+ if (is_deleted && !is_added)
+ {
+ /* Skip all descendants */
+ if (status->kind == svn_node_dir)
+ baton->skip_below_abspath = apr_pstrdup(baton->result_pool,
+ local_abspath);
+ return SVN_NO_ERROR;
+ }
+
+ /* Recursively handle each node according to depth, except when the
+ node is only being deleted, or is in an added tree (as added trees
+ use the normal commit handling). */
+ if (copy_mode && !is_added && !is_deleted && status->kind == svn_node_dir)
+ {
+ SVN_ERR(harvest_not_present_for_copy(wc_ctx, local_abspath, committables,
+ repos_root_url, commit_relpath,
+ baton->check_url_func,
+ baton->check_url_baton,
+ result_pool, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Baton for handle_descendants */
+struct handle_descendants_baton
+{
+ svn_wc_context_t *wc_ctx;
+ svn_cancel_func_t cancel_func;
+ void *cancel_baton;
+ svn_client__check_url_kind_t check_url_func;
+ void *check_url_baton;
+};
+
+/* Helper for the commit harvesters */
+static svn_error_t *
+handle_descendants(void *baton,
+ const void *key, apr_ssize_t klen, void *val,
+ apr_pool_t *pool)
+{
+ struct handle_descendants_baton *hdb = baton;
+ apr_array_header_t *commit_items = val;
+ apr_pool_t *iterpool = svn_pool_create(pool);
+ int i;
+
+ for (i = 0; i < commit_items->nelts; i++)
+ {
+ svn_client_commit_item3_t *item =
+ APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
+ const apr_array_header_t *absent_descendants;
+ int j;
+
+ /* Is this a copy operation? */
+ if (!(item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
+ || ! item->copyfrom_url)
+ continue;
+
+ if (hdb->cancel_func)
+ SVN_ERR(hdb->cancel_func(hdb->cancel_baton));
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_wc__get_not_present_descendants(&absent_descendants,
+ hdb->wc_ctx, item->path,
+ iterpool, iterpool));
+
+ for (j = 0; j < absent_descendants->nelts; j++)
+ {
+ int k;
+ svn_boolean_t found_item = FALSE;
+ svn_node_kind_t kind;
+ const char *relpath = APR_ARRAY_IDX(absent_descendants, j,
+ const char *);
+ const char *local_abspath = svn_dirent_join(item->path, relpath,
+ iterpool);
+
+ /* If the path has a commit operation, we do nothing.
+ (It will be deleted by the operation) */
+ for (k = 0; k < commit_items->nelts; k++)
+ {
+ svn_client_commit_item3_t *cmt_item =
+ APR_ARRAY_IDX(commit_items, k, svn_client_commit_item3_t *);
+
+ if (! strcmp(cmt_item->path, local_abspath))
+ {
+ found_item = TRUE;
+ break;
+ }
+ }
+
+ if (found_item)
+ continue; /* We have an explicit delete or replace for this path */
+
+ /* ### Need a sub-iterpool? */
+
+ if (hdb->check_url_func)
+ {
+ const char *from_url = svn_path_url_add_component2(
+ item->copyfrom_url, relpath,
+ iterpool);
+
+ SVN_ERR(hdb->check_url_func(hdb->check_url_baton,
+ &kind, from_url, item->copyfrom_rev,
+ iterpool));
+
+ if (kind == svn_node_none)
+ continue; /* This node is already deleted */
+ }
+ else
+ kind = svn_node_unknown; /* 'Ok' for a delete of something */
+
+ {
+ /* Add a new commit item that describes the delete */
+ apr_pool_t *result_pool = commit_items->pool;
+ svn_client_commit_item3_t *new_item
+ = svn_client_commit_item3_create(result_pool);
+
+ new_item->path = svn_dirent_join(item->path, relpath,
+ result_pool);
+ new_item->kind = kind;
+ new_item->url = svn_path_url_add_component2(item->url, relpath,
+ result_pool);
+ new_item->revision = SVN_INVALID_REVNUM;
+ new_item->state_flags = SVN_CLIENT_COMMIT_ITEM_DELETE;
+ new_item->incoming_prop_changes = apr_array_make(result_pool, 1,
+ sizeof(svn_prop_t *));
+
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *)
+ = new_item;
+ }
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Allocate and initialize the COMMITTABLES structure from POOL.
+ */
+static void
+create_committables(svn_client__committables_t **committables,
+ apr_pool_t *pool)
+{
+ *committables = apr_palloc(pool, sizeof(**committables));
+
+ (*committables)->by_repository = apr_hash_make(pool);
+ (*committables)->by_path = apr_hash_make(pool);
+}
+
+svn_error_t *
+svn_client__harvest_committables(svn_client__committables_t **committables,
+ apr_hash_t **lock_tokens,
+ const char *base_dir_abspath,
+ const apr_array_header_t *targets,
+ int depth_empty_start,
+ svn_depth_t depth,
+ svn_boolean_t just_locked,
+ const apr_array_header_t *changelists,
+ svn_client__check_url_kind_t check_url_func,
+ void *check_url_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_hash_t *changelist_hash = NULL;
+ struct handle_descendants_baton hdb;
+ apr_hash_index_t *hi;
+
+ /* It's possible that one of the named targets has a parent that is
+ * itself scheduled for addition or replacement -- that is, the
+ * parent is not yet versioned in the repository. This is okay, as
+ * long as the parent itself is part of this same commit, either
+ * directly, or by virtue of a grandparent, great-grandparent, etc,
+ * being part of the commit.
+ *
+ * Since we don't know what's included in the commit until we've
+ * harvested all the targets, we can't reliably check this as we
+ * go. So in `danglers', we record named targets whose parents
+ * do not yet exist in the repository. Then after harvesting the total
+ * commit group, we check to make sure those parents are included.
+ *
+ * Each key of danglers is a parent which does not exist in the
+ * repository. The (const char *) value is one of that parent's
+ * children which is named as part of the commit; the child is
+ * included only to make a better error message.
+ *
+ * (The reason we don't bother to check unnamed -- i.e, implicit --
+ * targets is that they can only join the commit if their parents
+ * did too, so this situation can't arise for them.)
+ */
+ apr_hash_t *danglers = apr_hash_make(scratch_pool);
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(base_dir_abspath));
+
+ /* Create the COMMITTABLES structure. */
+ create_committables(committables, result_pool);
+
+ /* And the LOCK_TOKENS dito. */
+ *lock_tokens = apr_hash_make(result_pool);
+
+ /* If we have a list of changelists, convert that into a hash with
+ changelist keys. */
+ if (changelists && changelists->nelts)
+ SVN_ERR(svn_hash_from_cstring_keys(&changelist_hash, changelists,
+ scratch_pool));
+
+ for (i = 0; i < targets->nelts; ++i)
+ {
+ const char *target_abspath;
+
+ svn_pool_clear(iterpool);
+
+ /* Add the relative portion to the base abspath. */
+ target_abspath = svn_dirent_join(base_dir_abspath,
+ APR_ARRAY_IDX(targets, i, const char *),
+ iterpool);
+
+ /* Handle our TARGET. */
+ /* Make sure this isn't inside a working copy subtree that is
+ * marked as tree-conflicted. */
+ SVN_ERR(bail_on_tree_conflicted_ancestor(ctx->wc_ctx, target_abspath,
+ ctx->notify_func2,
+ ctx->notify_baton2,
+ iterpool));
+
+ /* Are the remaining items externals with depth empty? */
+ if (i == depth_empty_start)
+ depth = svn_depth_empty;
+
+ SVN_ERR(harvest_committables(target_abspath,
+ *committables, *lock_tokens,
+ NULL /* COPY_MODE_RELPATH */,
+ depth, just_locked, changelist_hash,
+ danglers,
+ check_url_func, check_url_baton,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ ctx->wc_ctx, result_pool, iterpool));
+ }
+
+ hdb.wc_ctx = ctx->wc_ctx;
+ hdb.cancel_func = ctx->cancel_func;
+ hdb.cancel_baton = ctx->cancel_baton;
+ hdb.check_url_func = check_url_func;
+ hdb.check_url_baton = check_url_baton;
+
+ SVN_ERR(svn_iter_apr_hash(NULL, (*committables)->by_repository,
+ handle_descendants, &hdb, iterpool));
+
+ /* Make sure that every path in danglers is part of the commit. */
+ for (hi = apr_hash_first(scratch_pool, danglers); hi; hi = apr_hash_next(hi))
+ {
+ const char *dangling_parent = svn__apr_hash_index_key(hi);
+
+ svn_pool_clear(iterpool);
+
+ if (! look_up_committable(*committables, dangling_parent, iterpool))
+ {
+ const char *dangling_child = svn__apr_hash_index_val(hi);
+
+ if (ctx->notify_func2 != NULL)
+ {
+ svn_wc_notify_t *notify;
+
+ notify = svn_wc_create_notify(dangling_child,
+ svn_wc_notify_failed_no_parent,
+ scratch_pool);
+
+ ctx->notify_func2(ctx->notify_baton2, notify, iterpool);
+ }
+
+ return svn_error_createf(
+ SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not known to exist in the repository "
+ "and is not part of the commit, "
+ "yet its child '%s' is part of the commit"),
+ /* Probably one or both of these is an entry, but
+ safest to local_stylize just in case. */
+ svn_dirent_local_style(dangling_parent, iterpool),
+ svn_dirent_local_style(dangling_child, iterpool));
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+struct copy_committables_baton
+{
+ svn_client_ctx_t *ctx;
+ svn_client__committables_t *committables;
+ apr_pool_t *result_pool;
+ svn_client__check_url_kind_t check_url_func;
+ void *check_url_baton;
+};
+
+static svn_error_t *
+harvest_copy_committables(void *baton, void *item, apr_pool_t *pool)
+{
+ struct copy_committables_baton *btn = baton;
+ svn_client__copy_pair_t *pair = *(svn_client__copy_pair_t **)item;
+ const char *repos_root_url;
+ const char *commit_relpath;
+ struct handle_descendants_baton hdb;
+
+ /* Read the entry for this SRC. */
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(pair->src_abspath_or_url));
+
+ SVN_ERR(svn_wc__node_get_repos_info(NULL, NULL, &repos_root_url, NULL,
+ btn->ctx->wc_ctx,
+ pair->src_abspath_or_url,
+ pool, pool));
+
+ commit_relpath = svn_uri_skip_ancestor(repos_root_url,
+ pair->dst_abspath_or_url, pool);
+
+ /* Handle this SRC. */
+ SVN_ERR(harvest_committables(pair->src_abspath_or_url,
+ btn->committables, NULL,
+ commit_relpath,
+ svn_depth_infinity,
+ FALSE, /* JUST_LOCKED */
+ NULL /* changelists */,
+ NULL,
+ btn->check_url_func,
+ btn->check_url_baton,
+ btn->ctx->cancel_func,
+ btn->ctx->cancel_baton,
+ btn->ctx->notify_func2,
+ btn->ctx->notify_baton2,
+ btn->ctx->wc_ctx, btn->result_pool, pool));
+
+ hdb.wc_ctx = btn->ctx->wc_ctx;
+ hdb.cancel_func = btn->ctx->cancel_func;
+ hdb.cancel_baton = btn->ctx->cancel_baton;
+ hdb.check_url_func = btn->check_url_func;
+ hdb.check_url_baton = btn->check_url_baton;
+
+ SVN_ERR(svn_iter_apr_hash(NULL, btn->committables->by_repository,
+ handle_descendants, &hdb, pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+
+svn_error_t *
+svn_client__get_copy_committables(svn_client__committables_t **committables,
+ const apr_array_header_t *copy_pairs,
+ svn_client__check_url_kind_t check_url_func,
+ void *check_url_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct copy_committables_baton btn;
+
+ /* Create the COMMITTABLES structure. */
+ create_committables(committables, result_pool);
+
+ btn.ctx = ctx;
+ btn.committables = *committables;
+ btn.result_pool = result_pool;
+
+ btn.check_url_func = check_url_func;
+ btn.check_url_baton = check_url_baton;
+
+ /* For each copy pair, harvest the committables for that pair into the
+ committables hash. */
+ return svn_iter_apr_array(NULL, copy_pairs,
+ harvest_copy_committables, &btn, scratch_pool);
+}
+
+
+int svn_client__sort_commit_item_urls(const void *a, const void *b)
+{
+ const svn_client_commit_item3_t *item1
+ = *((const svn_client_commit_item3_t * const *) a);
+ const svn_client_commit_item3_t *item2
+ = *((const svn_client_commit_item3_t * const *) b);
+ return svn_path_compare_paths(item1->url, item2->url);
+}
+
+
+
+svn_error_t *
+svn_client__condense_commit_items(const char **base_url,
+ apr_array_header_t *commit_items,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *ci = commit_items; /* convenience */
+ const char *url;
+ svn_client_commit_item3_t *item, *last_item = NULL;
+ int i;
+
+ SVN_ERR_ASSERT(ci && ci->nelts);
+
+ /* Sort our commit items by their URLs. */
+ qsort(ci->elts, ci->nelts,
+ ci->elt_size, svn_client__sort_commit_item_urls);
+
+ /* Loop through the URLs, finding the longest usable ancestor common
+ to all of them, and making sure there are no duplicate URLs. */
+ for (i = 0; i < ci->nelts; i++)
+ {
+ item = APR_ARRAY_IDX(ci, i, svn_client_commit_item3_t *);
+ url = item->url;
+
+ if ((last_item) && (strcmp(last_item->url, url) == 0))
+ return svn_error_createf
+ (SVN_ERR_CLIENT_DUPLICATE_COMMIT_URL, NULL,
+ _("Cannot commit both '%s' and '%s' as they refer to the same URL"),
+ svn_dirent_local_style(item->path, pool),
+ svn_dirent_local_style(last_item->path, pool));
+
+ /* In the first iteration, our BASE_URL is just our only
+ encountered commit URL to date. After that, we find the
+ longest ancestor between the current BASE_URL and the current
+ commit URL. */
+ if (i == 0)
+ *base_url = apr_pstrdup(pool, url);
+ else
+ *base_url = svn_uri_get_longest_ancestor(*base_url, url, pool);
+
+ /* If our BASE_URL is itself a to-be-committed item, and it is
+ anything other than an already-versioned directory with
+ property mods, we'll call its parent directory URL the
+ BASE_URL. Why? Because we can't have a file URL as our base
+ -- period -- and all other directory operations (removal,
+ addition, etc.) require that we open that directory's parent
+ dir first. */
+ /* ### I don't understand the strlen()s here, hmmm. -kff */
+ if ((strlen(*base_url) == strlen(url))
+ && (! ((item->kind == svn_node_dir)
+ && item->state_flags == SVN_CLIENT_COMMIT_ITEM_PROP_MODS)))
+ *base_url = svn_uri_dirname(*base_url, pool);
+
+ /* Stash our item here for the next iteration. */
+ last_item = item;
+ }
+
+ /* Now that we've settled on a *BASE_URL, go hack that base off
+ of all of our URLs and store it as session_relpath. */
+ for (i = 0; i < ci->nelts; i++)
+ {
+ svn_client_commit_item3_t *this_item
+ = APR_ARRAY_IDX(ci, i, svn_client_commit_item3_t *);
+
+ this_item->session_relpath = svn_uri_skip_ancestor(*base_url,
+ this_item->url, pool);
+ }
+#ifdef SVN_CLIENT_COMMIT_DEBUG
+ /* ### TEMPORARY CODE ### */
+ SVN_DBG(("COMMITTABLES: (base URL=%s)\n", *base_url));
+ SVN_DBG((" FLAGS REV REL-URL (COPY-URL)\n"));
+ for (i = 0; i < ci->nelts; i++)
+ {
+ svn_client_commit_item3_t *this_item
+ = APR_ARRAY_IDX(ci, i, svn_client_commit_item3_t *);
+ char flags[6];
+ flags[0] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
+ ? 'a' : '-';
+ flags[1] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
+ ? 'd' : '-';
+ flags[2] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS)
+ ? 't' : '-';
+ flags[3] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
+ ? 'p' : '-';
+ flags[4] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)
+ ? 'c' : '-';
+ flags[5] = '\0';
+ SVN_DBG((" %s %6ld '%s' (%s)\n",
+ flags,
+ this_item->revision,
+ this_item->url ? this_item->url : "",
+ this_item->copyfrom_url ? this_item->copyfrom_url : "none"));
+ }
+#endif /* SVN_CLIENT_COMMIT_DEBUG */
+
+ return SVN_NO_ERROR;
+}
+
+
+struct file_mod_t
+{
+ const svn_client_commit_item3_t *item;
+ void *file_baton;
+};
+
+
+/* A baton for use while driving a path-based editor driver for commit */
+struct item_commit_baton
+{
+ const svn_delta_editor_t *editor; /* commit editor */
+ void *edit_baton; /* commit editor's baton */
+ apr_hash_t *file_mods; /* hash: path->file_mod_t */
+ const char *notify_path_prefix; /* notification path prefix
+ (NULL is okay, else abs path) */
+ svn_client_ctx_t *ctx; /* client context baton */
+ apr_hash_t *commit_items; /* the committables */
+ const char *base_url; /* The session url for the commit */
+};
+
+
+/* Drive CALLBACK_BATON->editor with the change described by the item in
+ * CALLBACK_BATON->commit_items that is keyed by PATH. If the change
+ * includes a text mod, however, call the editor's file_open() function
+ * but do not send the text mod to the editor; instead, add a mapping of
+ * "item-url => (commit-item, file-baton)" into CALLBACK_BATON->file_mods.
+ *
+ * Before driving the editor, call the cancellation and notification
+ * callbacks in CALLBACK_BATON->ctx, if present.
+ *
+ * This implements svn_delta_path_driver_cb_func_t. */
+static svn_error_t *
+do_item_commit(void **dir_baton,
+ void *parent_baton,
+ void *callback_baton,
+ const char *path,
+ apr_pool_t *pool)
+{
+ struct item_commit_baton *icb = callback_baton;
+ const svn_client_commit_item3_t *item = svn_hash_gets(icb->commit_items,
+ path);
+ svn_node_kind_t kind = item->kind;
+ void *file_baton = NULL;
+ apr_pool_t *file_pool = NULL;
+ const svn_delta_editor_t *editor = icb->editor;
+ apr_hash_t *file_mods = icb->file_mods;
+ svn_client_ctx_t *ctx = icb->ctx;
+ svn_error_t *err;
+ const char *local_abspath = NULL;
+
+ /* Do some initializations. */
+ *dir_baton = NULL;
+ if (item->kind != svn_node_none && item->path)
+ {
+ /* We always get an absolute path, see svn_client_commit_item3_t. */
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(item->path));
+ local_abspath = item->path;
+ }
+
+ /* If this is a file with textual mods, we'll be keeping its baton
+ around until the end of the commit. So just lump its memory into
+ a single, big, all-the-file-batons-in-here pool. Otherwise, we
+ can just use POOL, and trust our caller to clean that mess up. */
+ if ((kind == svn_node_file)
+ && (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS))
+ file_pool = apr_hash_pool_get(file_mods);
+ else
+ file_pool = pool;
+
+ /* Call the cancellation function. */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ /* Validation. */
+ if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)
+ {
+ if (! item->copyfrom_url)
+ return svn_error_createf
+ (SVN_ERR_BAD_URL, NULL,
+ _("Commit item '%s' has copy flag but no copyfrom URL"),
+ svn_dirent_local_style(path, pool));
+ if (! SVN_IS_VALID_REVNUM(item->copyfrom_rev))
+ return svn_error_createf
+ (SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Commit item '%s' has copy flag but an invalid revision"),
+ svn_dirent_local_style(path, pool));
+ }
+
+ /* If a feedback table was supplied by the application layer,
+ describe what we're about to do to this item. */
+ if (ctx->notify_func2 && item->path)
+ {
+ const char *npath = item->path;
+ svn_wc_notify_t *notify;
+
+ if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
+ && (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD))
+ {
+ /* We don't print the "(bin)" notice for binary files when
+ replacing, only when adding. So we don't bother to get
+ the mime-type here. */
+ if (item->copyfrom_url)
+ notify = svn_wc_create_notify(npath,
+ svn_wc_notify_commit_copied_replaced,
+ pool);
+ else
+ notify = svn_wc_create_notify(npath, svn_wc_notify_commit_replaced,
+ pool);
+
+ }
+ else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
+ {
+ notify = svn_wc_create_notify(npath, svn_wc_notify_commit_deleted,
+ pool);
+ }
+ else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
+ {
+ if (item->copyfrom_url)
+ notify = svn_wc_create_notify(npath, svn_wc_notify_commit_copied,
+ pool);
+ else
+ notify = svn_wc_create_notify(npath, svn_wc_notify_commit_added,
+ pool);
+
+ if (item->kind == svn_node_file)
+ {
+ const svn_string_t *propval;
+
+ SVN_ERR(svn_wc_prop_get2(&propval, ctx->wc_ctx, local_abspath,
+ SVN_PROP_MIME_TYPE, pool, pool));
+
+ if (propval)
+ notify->mime_type = propval->data;
+ }
+ }
+ else if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS)
+ || (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS))
+ {
+ notify = svn_wc_create_notify(npath, svn_wc_notify_commit_modified,
+ pool);
+ if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS)
+ notify->content_state = svn_wc_notify_state_changed;
+ else
+ notify->content_state = svn_wc_notify_state_unchanged;
+ if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
+ notify->prop_state = svn_wc_notify_state_changed;
+ else
+ notify->prop_state = svn_wc_notify_state_unchanged;
+ }
+ else
+ notify = NULL;
+
+ if (notify)
+ {
+ notify->kind = item->kind;
+ notify->path_prefix = icb->notify_path_prefix;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+ }
+ }
+
+ /* If this item is supposed to be deleted, do so. */
+ if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
+ {
+ SVN_ERR_ASSERT(parent_baton);
+ err = editor->delete_entry(path, item->revision,
+ parent_baton, pool);
+
+ if (err)
+ goto fixup_error;
+ }
+
+ /* If this item is supposed to be added, do so. */
+ if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
+ {
+ if (kind == svn_node_file)
+ {
+ SVN_ERR_ASSERT(parent_baton);
+ err = editor->add_file(
+ path, parent_baton, item->copyfrom_url,
+ item->copyfrom_url ? item->copyfrom_rev : SVN_INVALID_REVNUM,
+ file_pool, &file_baton);
+ }
+ else /* May be svn_node_none when adding parent dirs for a copy. */
+ {
+ SVN_ERR_ASSERT(parent_baton);
+ err = editor->add_directory(
+ path, parent_baton, item->copyfrom_url,
+ item->copyfrom_url ? item->copyfrom_rev : SVN_INVALID_REVNUM,
+ pool, dir_baton);
+ }
+
+ if (err)
+ goto fixup_error;
+
+ /* Set other prop-changes, if available in the baton */
+ if (item->outgoing_prop_changes)
+ {
+ svn_prop_t *prop;
+ apr_array_header_t *prop_changes = item->outgoing_prop_changes;
+ int ctr;
+ for (ctr = 0; ctr < prop_changes->nelts; ctr++)
+ {
+ prop = APR_ARRAY_IDX(prop_changes, ctr, svn_prop_t *);
+ if (kind == svn_node_file)
+ {
+ err = editor->change_file_prop(file_baton, prop->name,
+ prop->value, pool);
+ }
+ else
+ {
+ err = editor->change_dir_prop(*dir_baton, prop->name,
+ prop->value, pool);
+ }
+
+ if (err)
+ goto fixup_error;
+ }
+ }
+ }
+
+ /* Now handle property mods. */
+ if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
+ {
+ if (kind == svn_node_file)
+ {
+ if (! file_baton)
+ {
+ SVN_ERR_ASSERT(parent_baton);
+ err = editor->open_file(path, parent_baton,
+ item->revision,
+ file_pool, &file_baton);
+
+ if (err)
+ goto fixup_error;
+ }
+ }
+ else
+ {
+ if (! *dir_baton)
+ {
+ if (! parent_baton)
+ {
+ err = editor->open_root(icb->edit_baton, item->revision,
+ pool, dir_baton);
+ }
+ else
+ {
+ err = editor->open_directory(path, parent_baton,
+ item->revision,
+ pool, dir_baton);
+ }
+
+ if (err)
+ goto fixup_error;
+ }
+ }
+
+ /* When committing a directory that no longer exists in the
+ repository, a "not found" error does not occur immediately
+ upon opening the directory. It appears here during the delta
+ transmisssion. */
+ err = svn_wc_transmit_prop_deltas2(
+ ctx->wc_ctx, local_abspath, editor,
+ (kind == svn_node_dir) ? *dir_baton : file_baton, pool);
+
+ if (err)
+ goto fixup_error;
+
+ /* Make any additional client -> repository prop changes. */
+ if (item->outgoing_prop_changes)
+ {
+ svn_prop_t *prop;
+ int i;
+
+ for (i = 0; i < item->outgoing_prop_changes->nelts; i++)
+ {
+ prop = APR_ARRAY_IDX(item->outgoing_prop_changes, i,
+ svn_prop_t *);
+ if (kind == svn_node_file)
+ {
+ err = editor->change_file_prop(file_baton, prop->name,
+ prop->value, pool);
+ }
+ else
+ {
+ err = editor->change_dir_prop(*dir_baton, prop->name,
+ prop->value, pool);
+ }
+
+ if (err)
+ goto fixup_error;
+ }
+ }
+ }
+
+ /* Finally, handle text mods (in that we need to open a file if it
+ hasn't already been opened, and we need to put the file baton in
+ our FILES hash). */
+ if ((kind == svn_node_file)
+ && (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS))
+ {
+ struct file_mod_t *mod = apr_palloc(file_pool, sizeof(*mod));
+
+ if (! file_baton)
+ {
+ SVN_ERR_ASSERT(parent_baton);
+ err = editor->open_file(path, parent_baton,
+ item->revision,
+ file_pool, &file_baton);
+
+ if (err)
+ goto fixup_error;
+ }
+
+ /* Add this file mod to the FILE_MODS hash. */
+ mod->item = item;
+ mod->file_baton = file_baton;
+ svn_hash_sets(file_mods, item->session_relpath, mod);
+ }
+ else if (file_baton)
+ {
+ /* Close any outstanding file batons that didn't get caught by
+ the "has local mods" conditional above. */
+ err = editor->close_file(file_baton, NULL, file_pool);
+
+ if (err)
+ goto fixup_error;
+ }
+
+ return SVN_NO_ERROR;
+
+fixup_error:
+ return svn_error_trace(fixup_commit_error(local_abspath,
+ icb->base_url,
+ path, kind,
+ err, ctx, pool));
+}
+
+svn_error_t *
+svn_client__do_commit(const char *base_url,
+ const apr_array_header_t *commit_items,
+ const svn_delta_editor_t *editor,
+ void *edit_baton,
+ const char *notify_path_prefix,
+ apr_hash_t **sha1_checksums,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *file_mods = apr_hash_make(scratch_pool);
+ apr_hash_t *items_hash = apr_hash_make(scratch_pool);
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_hash_index_t *hi;
+ int i;
+ struct item_commit_baton cb_baton;
+ apr_array_header_t *paths =
+ apr_array_make(scratch_pool, commit_items->nelts, sizeof(const char *));
+
+ /* Ditto for the checksums. */
+ if (sha1_checksums)
+ *sha1_checksums = apr_hash_make(result_pool);
+
+ /* Build a hash from our COMMIT_ITEMS array, keyed on the
+ relative paths (which come from the item URLs). And
+ keep an array of those decoded paths, too. */
+ for (i = 0; i < commit_items->nelts; i++)
+ {
+ svn_client_commit_item3_t *item =
+ APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
+ const char *path = item->session_relpath;
+ svn_hash_sets(items_hash, path, item);
+ APR_ARRAY_PUSH(paths, const char *) = path;
+ }
+
+ /* Setup the callback baton. */
+ cb_baton.editor = editor;
+ cb_baton.edit_baton = edit_baton;
+ cb_baton.file_mods = file_mods;
+ cb_baton.notify_path_prefix = notify_path_prefix;
+ cb_baton.ctx = ctx;
+ cb_baton.commit_items = items_hash;
+ cb_baton.base_url = base_url;
+
+ /* Drive the commit editor! */
+ SVN_ERR(svn_delta_path_driver2(editor, edit_baton, paths, TRUE,
+ do_item_commit, &cb_baton, scratch_pool));
+
+ /* Transmit outstanding text deltas. */
+ for (hi = apr_hash_first(scratch_pool, file_mods);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ struct file_mod_t *mod = svn__apr_hash_index_val(hi);
+ const svn_client_commit_item3_t *item = mod->item;
+ const svn_checksum_t *new_text_base_md5_checksum;
+ const svn_checksum_t *new_text_base_sha1_checksum;
+ svn_boolean_t fulltext = FALSE;
+ svn_error_t *err;
+
+ svn_pool_clear(iterpool);
+
+ /* Transmit the entry. */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+ notify = svn_wc_create_notify(item->path,
+ svn_wc_notify_commit_postfix_txdelta,
+ iterpool);
+ notify->kind = svn_node_file;
+ notify->path_prefix = notify_path_prefix;
+ ctx->notify_func2(ctx->notify_baton2, notify, iterpool);
+ }
+
+ /* If the node has no history, transmit full text */
+ if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
+ && ! (item->state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY))
+ fulltext = TRUE;
+
+ err = svn_wc_transmit_text_deltas3(&new_text_base_md5_checksum,
+ &new_text_base_sha1_checksum,
+ ctx->wc_ctx, item->path,
+ fulltext, editor, mod->file_baton,
+ result_pool, iterpool);
+
+ if (err)
+ {
+ svn_pool_destroy(iterpool); /* Close tempfiles */
+ return svn_error_trace(fixup_commit_error(item->path,
+ base_url,
+ item->session_relpath,
+ svn_node_file,
+ err, ctx, scratch_pool));
+ }
+
+ if (sha1_checksums)
+ svn_hash_sets(*sha1_checksums, item->path, new_text_base_sha1_checksum);
+ }
+
+ svn_pool_destroy(iterpool);
+
+ /* Close the edit. */
+ return svn_error_trace(editor->close_edit(edit_baton, scratch_pool));
+}
+
+
+svn_error_t *
+svn_client__get_log_msg(const char **log_msg,
+ const char **tmp_file,
+ const apr_array_header_t *commit_items,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ if (ctx->log_msg_func3)
+ {
+ /* The client provided a callback function for the current API.
+ Forward the call to it directly. */
+ return (*ctx->log_msg_func3)(log_msg, tmp_file, commit_items,
+ ctx->log_msg_baton3, pool);
+ }
+ else if (ctx->log_msg_func2 || ctx->log_msg_func)
+ {
+ /* The client provided a pre-1.5 (or pre-1.3) API callback
+ function. Convert the commit_items list to the appropriate
+ type, and forward call to it. */
+ svn_error_t *err;
+ apr_pool_t *scratch_pool = svn_pool_create(pool);
+ apr_array_header_t *old_commit_items =
+ apr_array_make(scratch_pool, commit_items->nelts, sizeof(void*));
+
+ int i;
+ for (i = 0; i < commit_items->nelts; i++)
+ {
+ svn_client_commit_item3_t *item =
+ APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
+
+ if (ctx->log_msg_func2)
+ {
+ svn_client_commit_item2_t *old_item =
+ apr_pcalloc(scratch_pool, sizeof(*old_item));
+
+ old_item->path = item->path;
+ old_item->kind = item->kind;
+ old_item->url = item->url;
+ old_item->revision = item->revision;
+ old_item->copyfrom_url = item->copyfrom_url;
+ old_item->copyfrom_rev = item->copyfrom_rev;
+ old_item->state_flags = item->state_flags;
+ old_item->wcprop_changes = item->incoming_prop_changes;
+
+ APR_ARRAY_PUSH(old_commit_items, svn_client_commit_item2_t *) =
+ old_item;
+ }
+ else /* ctx->log_msg_func */
+ {
+ svn_client_commit_item_t *old_item =
+ apr_pcalloc(scratch_pool, sizeof(*old_item));
+
+ old_item->path = item->path;
+ old_item->kind = item->kind;
+ old_item->url = item->url;
+ /* The pre-1.3 API used the revision field for copyfrom_rev
+ and revision depeding of copyfrom_url. */
+ old_item->revision = item->copyfrom_url ?
+ item->copyfrom_rev : item->revision;
+ old_item->copyfrom_url = item->copyfrom_url;
+ old_item->state_flags = item->state_flags;
+ old_item->wcprop_changes = item->incoming_prop_changes;
+
+ APR_ARRAY_PUSH(old_commit_items, svn_client_commit_item_t *) =
+ old_item;
+ }
+ }
+
+ if (ctx->log_msg_func2)
+ err = (*ctx->log_msg_func2)(log_msg, tmp_file, old_commit_items,
+ ctx->log_msg_baton2, pool);
+ else
+ err = (*ctx->log_msg_func)(log_msg, tmp_file, old_commit_items,
+ ctx->log_msg_baton, pool);
+ svn_pool_destroy(scratch_pool);
+ return err;
+ }
+ else
+ {
+ /* No log message callback was provided by the client. */
+ *log_msg = "";
+ *tmp_file = NULL;
+ return SVN_NO_ERROR;
+ }
+}
+
+svn_error_t *
+svn_client__ensure_revprop_table(apr_hash_t **revprop_table_out,
+ const apr_hash_t *revprop_table_in,
+ const char *log_msg,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_hash_t *new_revprop_table;
+ if (revprop_table_in)
+ {
+ if (svn_prop_has_svn_prop(revprop_table_in, pool))
+ return svn_error_create(SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
+ _("Standard properties can't be set "
+ "explicitly as revision properties"));
+ new_revprop_table = apr_hash_copy(pool, revprop_table_in);
+ }
+ else
+ {
+ new_revprop_table = apr_hash_make(pool);
+ }
+ svn_hash_sets(new_revprop_table, SVN_PROP_REVISION_LOG,
+ svn_string_create(log_msg, pool));
+ *revprop_table_out = new_revprop_table;
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/compat_providers.c b/subversion/libsvn_client/compat_providers.c
new file mode 100644
index 0000000..ae53a15
--- /dev/null
+++ b/subversion/libsvn_client/compat_providers.c
@@ -0,0 +1,136 @@
+/*
+ * compat_providers.c: wrapper providers backwards compatibility
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_auth.h"
+#include "svn_client.h"
+
+void
+svn_client_get_simple_prompt_provider
+ (svn_auth_provider_object_t **provider,
+ svn_auth_simple_prompt_func_t prompt_func,
+ void *prompt_baton,
+ int retry_limit,
+ apr_pool_t *pool)
+{
+ svn_auth_get_simple_prompt_provider(provider, prompt_func, prompt_baton,
+ retry_limit, pool);
+}
+
+void
+svn_client_get_username_prompt_provider
+ (svn_auth_provider_object_t **provider,
+ svn_auth_username_prompt_func_t prompt_func,
+ void *prompt_baton,
+ int retry_limit,
+ apr_pool_t *pool)
+{
+ svn_auth_get_username_prompt_provider(provider, prompt_func, prompt_baton,
+ retry_limit, pool);
+}
+
+
+
+void svn_client_get_simple_provider(svn_auth_provider_object_t **provider,
+ apr_pool_t *pool)
+{
+ svn_auth_get_simple_provider2(provider, NULL, NULL, pool);
+}
+
+#if defined(WIN32) && !defined(__MINGW32__)
+void
+svn_client_get_windows_simple_provider(svn_auth_provider_object_t **provider,
+ apr_pool_t *pool)
+{
+ svn_auth_get_windows_simple_provider(provider, pool);
+}
+#endif /* WIN32 */
+
+void svn_client_get_username_provider(svn_auth_provider_object_t **provider,
+ apr_pool_t *pool)
+{
+ svn_auth_get_username_provider(provider, pool);
+}
+
+void
+svn_client_get_ssl_server_trust_file_provider
+ (svn_auth_provider_object_t **provider, apr_pool_t *pool)
+{
+ svn_auth_get_ssl_server_trust_file_provider(provider, pool);
+}
+
+void
+svn_client_get_ssl_client_cert_file_provider
+ (svn_auth_provider_object_t **provider, apr_pool_t *pool)
+{
+ svn_auth_get_ssl_client_cert_file_provider(provider, pool);
+}
+
+void
+svn_client_get_ssl_client_cert_pw_file_provider
+ (svn_auth_provider_object_t **provider, apr_pool_t *pool)
+{
+ svn_auth_get_ssl_client_cert_pw_file_provider2(provider, NULL, NULL, pool);
+}
+
+void
+svn_client_get_ssl_server_trust_prompt_provider
+ (svn_auth_provider_object_t **provider,
+ svn_auth_ssl_server_trust_prompt_func_t prompt_func,
+ void *prompt_baton,
+ apr_pool_t *pool)
+{
+ svn_auth_get_ssl_server_trust_prompt_provider(provider, prompt_func,
+ prompt_baton, pool);
+}
+
+void
+svn_client_get_ssl_client_cert_prompt_provider
+ (svn_auth_provider_object_t **provider,
+ svn_auth_ssl_client_cert_prompt_func_t prompt_func,
+ void *prompt_baton,
+ int retry_limit,
+ apr_pool_t *pool)
+{
+ svn_auth_get_ssl_client_cert_prompt_provider(provider, prompt_func,
+ prompt_baton, retry_limit,
+ pool);
+}
+
+void
+svn_client_get_ssl_client_cert_pw_prompt_provider
+ (svn_auth_provider_object_t **provider,
+ svn_auth_ssl_client_cert_pw_prompt_func_t prompt_func,
+ void *prompt_baton,
+ int retry_limit,
+ apr_pool_t *pool)
+{
+ svn_auth_get_ssl_client_cert_pw_prompt_provider(provider, prompt_func,
+ prompt_baton, retry_limit,
+ pool);
+}
diff --git a/subversion/libsvn_client/copy.c b/subversion/libsvn_client/copy.c
new file mode 100644
index 0000000..c0501b9
--- /dev/null
+++ b/subversion/libsvn_client/copy.c
@@ -0,0 +1,2422 @@
+/*
+ * copy.c: copy/move wrappers around wc 'copy' functionality.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <string.h>
+#include "svn_hash.h"
+#include "svn_client.h"
+#include "svn_error.h"
+#include "svn_error_codes.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_opt.h"
+#include "svn_time.h"
+#include "svn_props.h"
+#include "svn_mergeinfo.h"
+#include "svn_pools.h"
+
+#include "client.h"
+#include "mergeinfo.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_ra_private.h"
+#include "private/svn_mergeinfo_private.h"
+#include "private/svn_client_private.h"
+
+
+/*
+ * OUR BASIC APPROACH TO COPIES
+ * ============================
+ *
+ * for each source/destination pair
+ * if (not exist src_path)
+ * return ERR_BAD_SRC error
+ *
+ * if (exist dst_path)
+ * return ERR_OBSTRUCTION error
+ * else
+ * copy src_path into parent_of_dst_path as basename (dst_path)
+ *
+ * if (this is a move)
+ * delete src_path
+ */
+
+
+
+/*** Code. ***/
+
+/* Extend the mergeinfo for the single WC path TARGET_WCPATH, adding
+ MERGEINFO to any mergeinfo pre-existing in the WC. */
+static svn_error_t *
+extend_wc_mergeinfo(const char *target_abspath,
+ apr_hash_t *mergeinfo,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_hash_t *wc_mergeinfo;
+
+ /* Get a fresh copy of the pre-existing state of the WC's mergeinfo
+ updating it. */
+ SVN_ERR(svn_client__parse_mergeinfo(&wc_mergeinfo, ctx->wc_ctx,
+ target_abspath, pool, pool));
+
+ /* Combine the provided mergeinfo with any mergeinfo from the WC. */
+ if (wc_mergeinfo && mergeinfo)
+ SVN_ERR(svn_mergeinfo_merge2(wc_mergeinfo, mergeinfo, pool, pool));
+ else if (! wc_mergeinfo)
+ wc_mergeinfo = mergeinfo;
+
+ return svn_error_trace(
+ svn_client__record_wc_mergeinfo(target_abspath, wc_mergeinfo,
+ FALSE, ctx, pool));
+}
+
+/* Find the longest common ancestor of paths in COPY_PAIRS. If
+ SRC_ANCESTOR is NULL, ignore source paths in this calculation. If
+ DST_ANCESTOR is NULL, ignore destination paths in this calculation.
+ COMMON_ANCESTOR will be the common ancestor of both the
+ SRC_ANCESTOR and DST_ANCESTOR, and will only be set if it is not
+ NULL.
+ */
+static svn_error_t *
+get_copy_pair_ancestors(const apr_array_header_t *copy_pairs,
+ const char **src_ancestor,
+ const char **dst_ancestor,
+ const char **common_ancestor,
+ apr_pool_t *pool)
+{
+ apr_pool_t *subpool = svn_pool_create(pool);
+ svn_client__copy_pair_t *first;
+ const char *first_dst;
+ const char *first_src;
+ const char *top_dst;
+ svn_boolean_t src_is_url;
+ svn_boolean_t dst_is_url;
+ char *top_src;
+ int i;
+
+ first = APR_ARRAY_IDX(copy_pairs, 0, svn_client__copy_pair_t *);
+
+ /* Because all the destinations are in the same directory, we can easily
+ determine their common ancestor. */
+ first_dst = first->dst_abspath_or_url;
+ dst_is_url = svn_path_is_url(first_dst);
+
+ if (copy_pairs->nelts == 1)
+ top_dst = apr_pstrdup(subpool, first_dst);
+ else
+ top_dst = dst_is_url ? svn_uri_dirname(first_dst, subpool)
+ : svn_dirent_dirname(first_dst, subpool);
+
+ /* Sources can came from anywhere, so we have to actually do some
+ work for them. */
+ first_src = first->src_abspath_or_url;
+ src_is_url = svn_path_is_url(first_src);
+ top_src = apr_pstrdup(subpool, first_src);
+ for (i = 1; i < copy_pairs->nelts; i++)
+ {
+ /* We don't need to clear the subpool here for several reasons:
+ 1) If we do, we can't use it to allocate the initial versions of
+ top_src and top_dst (above).
+ 2) We don't return any errors in the following loop, so we
+ are guanteed to destroy the subpool at the end of this function.
+ 3) The number of iterations is likely to be few, and the loop will
+ be through quickly, so memory leakage will not be significant,
+ in time or space.
+ */
+ const svn_client__copy_pair_t *pair =
+ APR_ARRAY_IDX(copy_pairs, i, svn_client__copy_pair_t *);
+
+ top_src = src_is_url
+ ? svn_uri_get_longest_ancestor(top_src, pair->src_abspath_or_url,
+ subpool)
+ : svn_dirent_get_longest_ancestor(top_src, pair->src_abspath_or_url,
+ subpool);
+ }
+
+ if (src_ancestor)
+ *src_ancestor = apr_pstrdup(pool, top_src);
+
+ if (dst_ancestor)
+ *dst_ancestor = apr_pstrdup(pool, top_dst);
+
+ if (common_ancestor)
+ *common_ancestor =
+ src_is_url
+ ? svn_uri_get_longest_ancestor(top_src, top_dst, pool)
+ : svn_dirent_get_longest_ancestor(top_src, top_dst, pool);
+
+ svn_pool_destroy(subpool);
+
+ return SVN_NO_ERROR;
+}
+
+
+/* The guts of do_wc_to_wc_copies */
+static svn_error_t *
+do_wc_to_wc_copies_with_write_lock(svn_boolean_t *timestamp_sleep,
+ const apr_array_header_t *copy_pairs,
+ const char *dst_parent,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ svn_error_t *err = SVN_NO_ERROR;
+
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ const char *dst_abspath;
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ svn_pool_clear(iterpool);
+
+ /* Check for cancellation */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ /* Perform the copy */
+ dst_abspath = svn_dirent_join(pair->dst_parent_abspath, pair->base_name,
+ iterpool);
+ *timestamp_sleep = TRUE;
+ err = svn_wc_copy3(ctx->wc_ctx, pair->src_abspath_or_url, dst_abspath,
+ FALSE /* metadata_only */,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2, iterpool);
+ if (err)
+ break;
+ }
+ svn_pool_destroy(iterpool);
+
+ SVN_ERR(err);
+ return SVN_NO_ERROR;
+}
+
+/* Copy each COPY_PAIR->SRC into COPY_PAIR->DST. Use POOL for temporary
+ allocations. */
+static svn_error_t *
+do_wc_to_wc_copies(svn_boolean_t *timestamp_sleep,
+ const apr_array_header_t *copy_pairs,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *dst_parent, *dst_parent_abspath;
+
+ SVN_ERR(get_copy_pair_ancestors(copy_pairs, NULL, &dst_parent, NULL, pool));
+ if (copy_pairs->nelts == 1)
+ dst_parent = svn_dirent_dirname(dst_parent, pool);
+
+ SVN_ERR(svn_dirent_get_absolute(&dst_parent_abspath, dst_parent, pool));
+
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ do_wc_to_wc_copies_with_write_lock(timestamp_sleep, copy_pairs, dst_parent,
+ ctx, pool),
+ ctx->wc_ctx, dst_parent_abspath, FALSE, pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* The locked bit of do_wc_to_wc_moves. */
+static svn_error_t *
+do_wc_to_wc_moves_with_locks2(svn_client__copy_pair_t *pair,
+ const char *dst_parent_abspath,
+ svn_boolean_t lock_src,
+ svn_boolean_t lock_dst,
+ svn_boolean_t allow_mixed_revisions,
+ svn_boolean_t metadata_only,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ const char *dst_abspath;
+
+ dst_abspath = svn_dirent_join(dst_parent_abspath, pair->base_name,
+ scratch_pool);
+
+ SVN_ERR(svn_wc__move2(ctx->wc_ctx, pair->src_abspath_or_url,
+ dst_abspath, metadata_only,
+ allow_mixed_revisions,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Wrapper to add an optional second lock */
+static svn_error_t *
+do_wc_to_wc_moves_with_locks1(svn_client__copy_pair_t *pair,
+ const char *dst_parent_abspath,
+ svn_boolean_t lock_src,
+ svn_boolean_t lock_dst,
+ svn_boolean_t allow_mixed_revisions,
+ svn_boolean_t metadata_only,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ if (lock_dst)
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ do_wc_to_wc_moves_with_locks2(pair, dst_parent_abspath, lock_src,
+ lock_dst, allow_mixed_revisions,
+ metadata_only,
+ ctx, scratch_pool),
+ ctx->wc_ctx, dst_parent_abspath, FALSE, scratch_pool);
+ else
+ SVN_ERR(do_wc_to_wc_moves_with_locks2(pair, dst_parent_abspath, lock_src,
+ lock_dst, allow_mixed_revisions,
+ metadata_only,
+ ctx, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Move each COPY_PAIR->SRC into COPY_PAIR->DST, deleting COPY_PAIR->SRC
+ afterwards. Use POOL for temporary allocations. */
+static svn_error_t *
+do_wc_to_wc_moves(svn_boolean_t *timestamp_sleep,
+ const apr_array_header_t *copy_pairs,
+ const char *dst_path,
+ svn_boolean_t allow_mixed_revisions,
+ svn_boolean_t metadata_only,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(pool);
+ svn_error_t *err = SVN_NO_ERROR;
+
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ const char *src_parent_abspath;
+ svn_boolean_t lock_src, lock_dst;
+
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ svn_pool_clear(iterpool);
+
+ /* Check for cancellation */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ src_parent_abspath = svn_dirent_dirname(pair->src_abspath_or_url,
+ iterpool);
+
+ /* We now need to lock the right combination of batons.
+ Four cases:
+ 1) src_parent == dst_parent
+ 2) src_parent is parent of dst_parent
+ 3) dst_parent is parent of src_parent
+ 4) src_parent and dst_parent are disjoint
+ We can handle 1) as either 2) or 3) */
+ if (strcmp(src_parent_abspath, pair->dst_parent_abspath) == 0
+ || svn_dirent_is_child(src_parent_abspath, pair->dst_parent_abspath,
+ iterpool))
+ {
+ lock_src = TRUE;
+ lock_dst = FALSE;
+ }
+ else if (svn_dirent_is_child(pair->dst_parent_abspath,
+ src_parent_abspath,
+ iterpool))
+ {
+ lock_src = FALSE;
+ lock_dst = TRUE;
+ }
+ else
+ {
+ lock_src = TRUE;
+ lock_dst = TRUE;
+ }
+
+ *timestamp_sleep = TRUE;
+
+ /* Perform the copy and then the delete. */
+ if (lock_src)
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ do_wc_to_wc_moves_with_locks1(pair, pair->dst_parent_abspath,
+ lock_src, lock_dst,
+ allow_mixed_revisions,
+ metadata_only,
+ ctx, iterpool),
+ ctx->wc_ctx, src_parent_abspath,
+ FALSE, iterpool);
+ else
+ SVN_ERR(do_wc_to_wc_moves_with_locks1(pair, pair->dst_parent_abspath,
+ lock_src, lock_dst,
+ allow_mixed_revisions,
+ metadata_only,
+ ctx, iterpool));
+
+ }
+ svn_pool_destroy(iterpool);
+
+ return svn_error_trace(err);
+}
+
+/* Verify that the destinations stored in COPY_PAIRS are valid working copy
+ destinations and set pair->dst_parent_abspath and pair->base_name for each
+ item to the resulting location if they do */
+static svn_error_t *
+verify_wc_dsts(const apr_array_header_t *copy_pairs,
+ svn_boolean_t make_parents,
+ svn_boolean_t is_move,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ /* Check that DST does not exist, but its parent does */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ svn_node_kind_t dst_kind, dst_parent_kind;
+
+ svn_pool_clear(iterpool);
+
+ /* If DST_PATH does not exist, then its basename will become a new
+ file or dir added to its parent (possibly an implicit '.').
+ Else, just error out. */
+ SVN_ERR(svn_wc_read_kind2(&dst_kind, ctx->wc_ctx,
+ pair->dst_abspath_or_url,
+ FALSE /* show_deleted */,
+ TRUE /* show_hidden */,
+ iterpool));
+ if (dst_kind != svn_node_none)
+ {
+ svn_boolean_t is_excluded;
+ svn_boolean_t is_server_excluded;
+
+ SVN_ERR(svn_wc__node_is_not_present(NULL, &is_excluded,
+ &is_server_excluded, ctx->wc_ctx,
+ pair->dst_abspath_or_url, FALSE,
+ iterpool));
+
+ if (is_excluded || is_server_excluded)
+ {
+ return svn_error_createf(
+ SVN_ERR_WC_OBSTRUCTED_UPDATE,
+ NULL, _("Path '%s' exists, but is excluded"),
+ svn_dirent_local_style(pair->dst_abspath_or_url, iterpool));
+ }
+ else
+ return svn_error_createf(
+ SVN_ERR_ENTRY_EXISTS, NULL,
+ _("Path '%s' already exists"),
+ svn_dirent_local_style(pair->dst_abspath_or_url,
+ scratch_pool));
+ }
+
+ /* Check that there is no unversioned obstruction */
+ SVN_ERR(svn_io_check_path(pair->dst_abspath_or_url, &dst_kind,
+ iterpool));
+
+ if (dst_kind != svn_node_none)
+ {
+ if (is_move
+ && copy_pairs->nelts == 1
+ && strcmp(svn_dirent_dirname(pair->src_abspath_or_url, iterpool),
+ svn_dirent_dirname(pair->dst_abspath_or_url,
+ iterpool)) == 0)
+ {
+ const char *dst;
+ char *dst_apr;
+ apr_status_t apr_err;
+ /* We have a rename inside a directory, which might collide
+ just because the case insensivity of the filesystem makes
+ the source match the destination. */
+
+ SVN_ERR(svn_path_cstring_from_utf8(&dst,
+ pair->dst_abspath_or_url,
+ scratch_pool));
+
+ apr_err = apr_filepath_merge(&dst_apr, NULL, dst,
+ APR_FILEPATH_TRUENAME, iterpool);
+
+ if (!apr_err)
+ {
+ /* And now bring it back to our canonical format */
+ SVN_ERR(svn_path_cstring_to_utf8(&dst, dst_apr, iterpool));
+ dst = svn_dirent_canonicalize(dst, iterpool);
+ }
+ /* else: Don't report this error; just report the normal error */
+
+ if (!apr_err && strcmp(dst, pair->src_abspath_or_url) == 0)
+ {
+ /* Ok, we have a single case only rename. Get out of here */
+ svn_dirent_split(&pair->dst_parent_abspath, &pair->base_name,
+ pair->dst_abspath_or_url, result_pool);
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+ }
+ }
+
+ return svn_error_createf(
+ SVN_ERR_ENTRY_EXISTS, NULL,
+ _("Path '%s' already exists as unversioned node"),
+ svn_dirent_local_style(pair->dst_abspath_or_url,
+ scratch_pool));
+ }
+
+ svn_dirent_split(&pair->dst_parent_abspath, &pair->base_name,
+ pair->dst_abspath_or_url, result_pool);
+
+ /* Make sure the destination parent is a directory and produce a clear
+ error message if it is not. */
+ SVN_ERR(svn_wc_read_kind2(&dst_parent_kind,
+ ctx->wc_ctx, pair->dst_parent_abspath,
+ FALSE, TRUE,
+ iterpool));
+ if (make_parents && dst_parent_kind == svn_node_none)
+ {
+ SVN_ERR(svn_client__make_local_parents(pair->dst_parent_abspath,
+ TRUE, ctx, iterpool));
+ }
+ else if (dst_parent_kind != svn_node_dir)
+ {
+ return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL,
+ _("Path '%s' is not a directory"),
+ svn_dirent_local_style(
+ pair->dst_parent_abspath, scratch_pool));
+ }
+
+ SVN_ERR(svn_io_check_path(pair->dst_parent_abspath,
+ &dst_parent_kind, scratch_pool));
+
+ if (dst_parent_kind != svn_node_dir)
+ return svn_error_createf(SVN_ERR_WC_MISSING, NULL,
+ _("Path '%s' is not a directory"),
+ svn_dirent_local_style(
+ pair->dst_parent_abspath, scratch_pool));
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+verify_wc_srcs_and_dsts(const apr_array_header_t *copy_pairs,
+ svn_boolean_t make_parents,
+ svn_boolean_t is_move,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ /* Check that all of our SRCs exist. */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_boolean_t deleted_ok;
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ svn_pool_clear(iterpool);
+
+ deleted_ok = (pair->src_peg_revision.kind == svn_opt_revision_base
+ || pair->src_op_revision.kind == svn_opt_revision_base);
+
+ /* Verify that SRC_PATH exists. */
+ SVN_ERR(svn_wc_read_kind2(&pair->src_kind, ctx->wc_ctx,
+ pair->src_abspath_or_url,
+ deleted_ok, FALSE, iterpool));
+ if (pair->src_kind == svn_node_none)
+ return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL,
+ _("Path '%s' does not exist"),
+ svn_dirent_local_style(
+ pair->src_abspath_or_url,
+ scratch_pool));
+ }
+
+ SVN_ERR(verify_wc_dsts(copy_pairs, make_parents, is_move, ctx,
+ result_pool, iterpool));
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Path-specific state used as part of path_driver_cb_baton. */
+typedef struct path_driver_info_t
+{
+ const char *src_url;
+ const char *src_path;
+ const char *dst_path;
+ svn_node_kind_t src_kind;
+ svn_revnum_t src_revnum;
+ svn_boolean_t resurrection;
+ svn_boolean_t dir_add;
+ svn_string_t *mergeinfo; /* the new mergeinfo for the target */
+} path_driver_info_t;
+
+
+/* The baton used with the path_driver_cb_func() callback for a copy
+ or move operation. */
+struct path_driver_cb_baton
+{
+ /* The editor (and its state) used to perform the operation. */
+ const svn_delta_editor_t *editor;
+ void *edit_baton;
+
+ /* A hash of path -> path_driver_info_t *'s. */
+ apr_hash_t *action_hash;
+
+ /* Whether the operation is a move or copy. */
+ svn_boolean_t is_move;
+};
+
+static svn_error_t *
+path_driver_cb_func(void **dir_baton,
+ void *parent_baton,
+ void *callback_baton,
+ const char *path,
+ apr_pool_t *pool)
+{
+ struct path_driver_cb_baton *cb_baton = callback_baton;
+ svn_boolean_t do_delete = FALSE, do_add = FALSE;
+ path_driver_info_t *path_info = svn_hash_gets(cb_baton->action_hash, path);
+
+ /* Initialize return value. */
+ *dir_baton = NULL;
+
+ /* This function should never get an empty PATH. We can neither
+ create nor delete the empty PATH, so if someone is calling us
+ with such, the code is just plain wrong. */
+ SVN_ERR_ASSERT(! svn_path_is_empty(path));
+
+ /* Check to see if we need to add the path as a directory. */
+ if (path_info->dir_add)
+ {
+ return cb_baton->editor->add_directory(path, parent_baton, NULL,
+ SVN_INVALID_REVNUM, pool,
+ dir_baton);
+ }
+
+ /* If this is a resurrection, we know the source and dest paths are
+ the same, and that our driver will only be calling us once. */
+ if (path_info->resurrection)
+ {
+ /* If this is a move, we do nothing. Otherwise, we do the copy. */
+ if (! cb_baton->is_move)
+ do_add = TRUE;
+ }
+ /* Not a resurrection. */
+ else
+ {
+ /* If this is a move, we check PATH to see if it is the source
+ or the destination of the move. */
+ if (cb_baton->is_move)
+ {
+ if (strcmp(path_info->src_path, path) == 0)
+ do_delete = TRUE;
+ else
+ do_add = TRUE;
+ }
+ /* Not a move? This must just be the copy addition. */
+ else
+ {
+ do_add = TRUE;
+ }
+ }
+
+ if (do_delete)
+ {
+ SVN_ERR(cb_baton->editor->delete_entry(path, SVN_INVALID_REVNUM,
+ parent_baton, pool));
+ }
+ if (do_add)
+ {
+ SVN_ERR(svn_path_check_valid(path, pool));
+
+ if (path_info->src_kind == svn_node_file)
+ {
+ void *file_baton;
+ SVN_ERR(cb_baton->editor->add_file(path, parent_baton,
+ path_info->src_url,
+ path_info->src_revnum,
+ pool, &file_baton));
+ if (path_info->mergeinfo)
+ SVN_ERR(cb_baton->editor->change_file_prop(file_baton,
+ SVN_PROP_MERGEINFO,
+ path_info->mergeinfo,
+ pool));
+ SVN_ERR(cb_baton->editor->close_file(file_baton, NULL, pool));
+ }
+ else
+ {
+ SVN_ERR(cb_baton->editor->add_directory(path, parent_baton,
+ path_info->src_url,
+ path_info->src_revnum,
+ pool, dir_baton));
+ if (path_info->mergeinfo)
+ SVN_ERR(cb_baton->editor->change_dir_prop(*dir_baton,
+ SVN_PROP_MERGEINFO,
+ path_info->mergeinfo,
+ pool));
+ }
+ }
+ return SVN_NO_ERROR;
+}
+
+
+/* Starting with the path DIR relative to the RA_SESSION's session
+ URL, work up through DIR's parents until an existing node is found.
+ Push each nonexistent path onto the array NEW_DIRS, allocating in
+ POOL. Raise an error if the existing node is not a directory.
+
+ ### Multiple requests for HEAD (SVN_INVALID_REVNUM) make this
+ ### implementation susceptible to race conditions. */
+static svn_error_t *
+find_absent_parents1(svn_ra_session_t *ra_session,
+ const char *dir,
+ apr_array_header_t *new_dirs,
+ apr_pool_t *pool)
+{
+ svn_node_kind_t kind;
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ SVN_ERR(svn_ra_check_path(ra_session, dir, SVN_INVALID_REVNUM, &kind,
+ iterpool));
+
+ while (kind == svn_node_none)
+ {
+ svn_pool_clear(iterpool);
+
+ APR_ARRAY_PUSH(new_dirs, const char *) = dir;
+ dir = svn_dirent_dirname(dir, pool);
+
+ SVN_ERR(svn_ra_check_path(ra_session, dir, SVN_INVALID_REVNUM,
+ &kind, iterpool));
+ }
+
+ if (kind != svn_node_dir)
+ return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL,
+ _("Path '%s' already exists, but is not a "
+ "directory"), dir);
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Starting with the URL *TOP_DST_URL which is also the root of
+ RA_SESSION, work up through its parents until an existing node is
+ found. Push each nonexistent URL onto the array NEW_DIRS,
+ allocating in POOL. Raise an error if the existing node is not a
+ directory.
+
+ Set *TOP_DST_URL and the RA session's root to the existing node's URL.
+
+ ### Multiple requests for HEAD (SVN_INVALID_REVNUM) make this
+ ### implementation susceptible to race conditions. */
+static svn_error_t *
+find_absent_parents2(svn_ra_session_t *ra_session,
+ const char **top_dst_url,
+ apr_array_header_t *new_dirs,
+ apr_pool_t *pool)
+{
+ const char *root_url = *top_dst_url;
+ svn_node_kind_t kind;
+
+ SVN_ERR(svn_ra_check_path(ra_session, "", SVN_INVALID_REVNUM, &kind,
+ pool));
+
+ while (kind == svn_node_none)
+ {
+ APR_ARRAY_PUSH(new_dirs, const char *) = root_url;
+ root_url = svn_uri_dirname(root_url, pool);
+
+ SVN_ERR(svn_ra_reparent(ra_session, root_url, pool));
+ SVN_ERR(svn_ra_check_path(ra_session, "", SVN_INVALID_REVNUM, &kind,
+ pool));
+ }
+
+ if (kind != svn_node_dir)
+ return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL,
+ _("Path '%s' already exists, but is not a directory"),
+ root_url);
+
+ *top_dst_url = root_url;
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+repos_to_repos_copy(const apr_array_header_t *copy_pairs,
+ svn_boolean_t make_parents,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ svn_boolean_t is_move,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+ apr_array_header_t *paths = apr_array_make(pool, 2 * copy_pairs->nelts,
+ sizeof(const char *));
+ apr_hash_t *action_hash = apr_hash_make(pool);
+ apr_array_header_t *path_infos;
+ const char *top_url, *top_url_all, *top_url_dst;
+ const char *message, *repos_root;
+ svn_ra_session_t *ra_session = NULL;
+ const svn_delta_editor_t *editor;
+ void *edit_baton;
+ struct path_driver_cb_baton cb_baton;
+ apr_array_header_t *new_dirs = NULL;
+ apr_hash_t *commit_revprops;
+ int i;
+ svn_client__copy_pair_t *first_pair =
+ APR_ARRAY_IDX(copy_pairs, 0, svn_client__copy_pair_t *);
+
+ /* Open an RA session to the first copy pair's destination. We'll
+ be verifying that every one of our copy source and destination
+ URLs is or is beneath this sucker's repository root URL as a form
+ of a cheap(ish) sanity check. */
+ SVN_ERR(svn_client_open_ra_session2(&ra_session,
+ first_pair->src_abspath_or_url, NULL,
+ ctx, pool, pool));
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root, pool));
+
+ /* Verify that sources and destinations are all at or under
+ REPOS_ROOT. While here, create a path_info struct for each
+ src/dst pair and initialize portions of it with normalized source
+ location information. */
+ path_infos = apr_array_make(pool, copy_pairs->nelts,
+ sizeof(path_driver_info_t *));
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ path_driver_info_t *info = apr_pcalloc(pool, sizeof(*info));
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ apr_hash_t *mergeinfo;
+
+ /* Are the source and destination URLs at or under REPOS_ROOT? */
+ if (! (svn_uri__is_ancestor(repos_root, pair->src_abspath_or_url)
+ && svn_uri__is_ancestor(repos_root, pair->dst_abspath_or_url)))
+ return svn_error_create
+ (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Source and destination URLs appear not to point to the "
+ "same repository."));
+
+ /* Run the history function to get the source's URL and revnum in the
+ operational revision. */
+ SVN_ERR(svn_ra_reparent(ra_session, pair->src_abspath_or_url, pool));
+ SVN_ERR(svn_client__repos_locations(&pair->src_abspath_or_url,
+ &pair->src_revnum,
+ NULL, NULL,
+ ra_session,
+ pair->src_abspath_or_url,
+ &pair->src_peg_revision,
+ &pair->src_op_revision, NULL,
+ ctx, pool));
+
+ /* Go ahead and grab mergeinfo from the source, too. */
+ SVN_ERR(svn_ra_reparent(ra_session, pair->src_abspath_or_url, pool));
+ SVN_ERR(svn_client__get_repos_mergeinfo(
+ &mergeinfo, ra_session,
+ pair->src_abspath_or_url, pair->src_revnum,
+ svn_mergeinfo_inherited, TRUE /*squelch_incapable*/, pool));
+ if (mergeinfo)
+ SVN_ERR(svn_mergeinfo_to_string(&info->mergeinfo, mergeinfo, pool));
+
+ /* Plop an INFO structure onto our array thereof. */
+ info->src_url = pair->src_abspath_or_url;
+ info->src_revnum = pair->src_revnum;
+ info->resurrection = FALSE;
+ APR_ARRAY_PUSH(path_infos, path_driver_info_t *) = info;
+ }
+
+ /* If this is a move, we have to open our session to the longest
+ path common to all SRC_URLS and DST_URLS in the repository so we
+ can do existence checks on all paths, and so we can operate on
+ all paths in the case of a move. But if this is *not* a move,
+ then opening our session at the longest path common to sources
+ *and* destinations might be an optimization when the user is
+ authorized to access all that stuff, but could cause the
+ operation to fail altogether otherwise. See issue #3242. */
+ SVN_ERR(get_copy_pair_ancestors(copy_pairs, NULL, &top_url_dst, &top_url_all,
+ pool));
+ top_url = is_move ? top_url_all : top_url_dst;
+
+ /* Check each src/dst pair for resurrection, and verify that TOP_URL
+ is anchored high enough to cover all the editor_t activities
+ required for this operation. */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ path_driver_info_t *info = APR_ARRAY_IDX(path_infos, i,
+ path_driver_info_t *);
+
+ /* Source and destination are the same? It's a resurrection. */
+ if (strcmp(pair->src_abspath_or_url, pair->dst_abspath_or_url) == 0)
+ info->resurrection = TRUE;
+
+ /* We need to add each dst_URL, and (in a move) we'll need to
+ delete each src_URL. Our selection of TOP_URL so far ensures
+ that all our destination URLs (and source URLs, for moves)
+ are at least as deep as TOP_URL, but we need to make sure
+ that TOP_URL is an *ancestor* of all our to-be-edited paths.
+
+ Issue #683 is demonstrates this scenario. If you're
+ resurrecting a deleted item like this: 'svn cp -rN src_URL
+ dst_URL', then src_URL == dst_URL == top_url. In this
+ situation, we want to open an RA session to be at least the
+ *parent* of all three. */
+ if ((strcmp(top_url, pair->dst_abspath_or_url) == 0)
+ && (strcmp(top_url, repos_root) != 0))
+ {
+ top_url = svn_uri_dirname(top_url, pool);
+ }
+ if (is_move
+ && (strcmp(top_url, pair->src_abspath_or_url) == 0)
+ && (strcmp(top_url, repos_root) != 0))
+ {
+ top_url = svn_uri_dirname(top_url, pool);
+ }
+ }
+
+ /* Point the RA session to our current TOP_URL. */
+ SVN_ERR(svn_ra_reparent(ra_session, top_url, pool));
+
+ /* If we're allowed to create nonexistent parent directories of our
+ destinations, then make a list in NEW_DIRS of the parent
+ directories of the destination that don't yet exist. */
+ if (make_parents)
+ {
+ new_dirs = apr_array_make(pool, 0, sizeof(const char *));
+
+ /* If this is a move, TOP_URL is at least the common ancestor of
+ all the paths (sources and destinations) involved. Assuming
+ the sources exist (which is fair, because if they don't, this
+ whole operation will fail anyway), TOP_URL must also exist.
+ So it's the paths between TOP_URL and the destinations which
+ we have to check for existence. But here, we take advantage
+ of the knowledge of our caller. We know that if there are
+ multiple copy/move operations being requested, then the
+ destinations of the copies/moves will all be siblings of one
+ another. Therefore, we need only to check for the
+ nonexistent paths between TOP_URL and *one* of our
+ destinations to find nonexistent parents of all of them. */
+ if (is_move)
+ {
+ /* Imagine a situation where the user tries to copy an
+ existing source directory to nonexistent directory with
+ --parents options specified:
+
+ svn copy --parents URL/src URL/dst
+
+ where src exists and dst does not. If the dirname of the
+ destination path is equal to TOP_URL,
+ do not try to add dst to the NEW_DIRS list since it
+ will be added to the commit items array later in this
+ function. */
+ const char *dir = svn_uri_skip_ancestor(
+ top_url,
+ svn_uri_dirname(first_pair->dst_abspath_or_url,
+ pool),
+ pool);
+ if (dir && *dir)
+ SVN_ERR(find_absent_parents1(ra_session, dir, new_dirs, pool));
+ }
+ /* If, however, this is *not* a move, TOP_URL only points to the
+ common ancestor of our destination path(s), or possibly one
+ level higher. We'll need to do an existence crawl toward the
+ root of the repository, starting with one of our destinations
+ (see "... take advantage of the knowledge of our caller ..."
+ above), and possibly adjusting TOP_URL as we go. */
+ else
+ {
+ apr_array_header_t *new_urls =
+ apr_array_make(pool, 0, sizeof(const char *));
+ SVN_ERR(find_absent_parents2(ra_session, &top_url, new_urls, pool));
+
+ /* Convert absolute URLs into relpaths relative to TOP_URL. */
+ for (i = 0; i < new_urls->nelts; i++)
+ {
+ const char *new_url = APR_ARRAY_IDX(new_urls, i, const char *);
+ const char *dir = svn_uri_skip_ancestor(top_url, new_url, pool);
+
+ APR_ARRAY_PUSH(new_dirs, const char *) = dir;
+ }
+ }
+ }
+
+ /* For each src/dst pair, check to see if that SRC_URL is a child of
+ the DST_URL (excepting the case where DST_URL is the repo root).
+ If it is, and the parent of DST_URL is the current TOP_URL, then we
+ need to reparent the session one directory higher, the parent of
+ the DST_URL. */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ path_driver_info_t *info = APR_ARRAY_IDX(path_infos, i,
+ path_driver_info_t *);
+ const char *relpath = svn_uri_skip_ancestor(pair->dst_abspath_or_url,
+ pair->src_abspath_or_url,
+ pool);
+
+ if ((strcmp(pair->dst_abspath_or_url, repos_root) != 0)
+ && (relpath != NULL && *relpath != '\0'))
+ {
+ info->resurrection = TRUE;
+ top_url = svn_uri_dirname(top_url, pool);
+ SVN_ERR(svn_ra_reparent(ra_session, top_url, pool));
+ }
+ }
+
+ /* Get the portions of the SRC and DST URLs that are relative to
+ TOP_URL (URI-decoding them while we're at it), verify that the
+ source exists and the proposed destination does not, and toss
+ what we've learned into the INFO array. (For copies -- that is,
+ non-moves -- the relative source URL NULL because it isn't a
+ child of the TOP_URL at all. That's okay, we'll deal with
+ it.) */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair =
+ APR_ARRAY_IDX(copy_pairs, i, svn_client__copy_pair_t *);
+ path_driver_info_t *info =
+ APR_ARRAY_IDX(path_infos, i, path_driver_info_t *);
+ svn_node_kind_t dst_kind;
+ const char *src_rel, *dst_rel;
+
+ src_rel = svn_uri_skip_ancestor(top_url, pair->src_abspath_or_url, pool);
+ if (src_rel)
+ {
+ SVN_ERR(svn_ra_check_path(ra_session, src_rel, pair->src_revnum,
+ &info->src_kind, pool));
+ }
+ else
+ {
+ const char *old_url;
+
+ src_rel = NULL;
+ SVN_ERR_ASSERT(! is_move);
+
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_url, ra_session,
+ pair->src_abspath_or_url,
+ pool));
+ SVN_ERR(svn_ra_check_path(ra_session, "", pair->src_revnum,
+ &info->src_kind, pool));
+ SVN_ERR(svn_ra_reparent(ra_session, old_url, pool));
+ }
+ if (info->src_kind == svn_node_none)
+ return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
+ _("Path '%s' does not exist in revision %ld"),
+ pair->src_abspath_or_url, pair->src_revnum);
+
+ /* Figure out the basename that will result from this operation,
+ and ensure that we aren't trying to overwrite existing paths. */
+ dst_rel = svn_uri_skip_ancestor(top_url, pair->dst_abspath_or_url, pool);
+ SVN_ERR(svn_ra_check_path(ra_session, dst_rel, SVN_INVALID_REVNUM,
+ &dst_kind, pool));
+ if (dst_kind != svn_node_none)
+ return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL,
+ _("Path '%s' already exists"), dst_rel);
+
+ /* More info for our INFO structure. */
+ info->src_path = src_rel;
+ info->dst_path = dst_rel;
+
+ svn_hash_sets(action_hash, info->dst_path, info);
+ if (is_move && (! info->resurrection))
+ svn_hash_sets(action_hash, info->src_path, info);
+ }
+
+ if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx))
+ {
+ /* Produce a list of new paths to add, and provide it to the
+ mechanism used to acquire a log message. */
+ svn_client_commit_item3_t *item;
+ const char *tmp_file;
+ apr_array_header_t *commit_items
+ = apr_array_make(pool, 2 * copy_pairs->nelts, sizeof(item));
+
+ /* Add any intermediate directories to the message */
+ if (make_parents)
+ {
+ for (i = 0; i < new_dirs->nelts; i++)
+ {
+ const char *relpath = APR_ARRAY_IDX(new_dirs, i, const char *);
+
+ item = svn_client_commit_item3_create(pool);
+ item->url = svn_path_url_add_component2(top_url, relpath, pool);
+ item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD;
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
+ }
+ }
+
+ for (i = 0; i < path_infos->nelts; i++)
+ {
+ path_driver_info_t *info = APR_ARRAY_IDX(path_infos, i,
+ path_driver_info_t *);
+
+ item = svn_client_commit_item3_create(pool);
+ item->url = svn_path_url_add_component2(top_url, info->dst_path,
+ pool);
+ item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD;
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
+
+ if (is_move && (! info->resurrection))
+ {
+ item = apr_pcalloc(pool, sizeof(*item));
+ item->url = svn_path_url_add_component2(top_url, info->src_path,
+ pool);
+ item->state_flags = SVN_CLIENT_COMMIT_ITEM_DELETE;
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
+ }
+ }
+
+ SVN_ERR(svn_client__get_log_msg(&message, &tmp_file, commit_items,
+ ctx, pool));
+ if (! message)
+ return SVN_NO_ERROR;
+ }
+ else
+ message = "";
+
+ /* Setup our PATHS for the path-based editor drive. */
+ /* First any intermediate directories. */
+ if (make_parents)
+ {
+ for (i = 0; i < new_dirs->nelts; i++)
+ {
+ const char *relpath = APR_ARRAY_IDX(new_dirs, i, const char *);
+ path_driver_info_t *info = apr_pcalloc(pool, sizeof(*info));
+
+ info->dst_path = relpath;
+ info->dir_add = TRUE;
+
+ APR_ARRAY_PUSH(paths, const char *) = relpath;
+ svn_hash_sets(action_hash, relpath, info);
+ }
+ }
+
+ /* Then our copy destinations and move sources (if any). */
+ for (i = 0; i < path_infos->nelts; i++)
+ {
+ path_driver_info_t *info = APR_ARRAY_IDX(path_infos, i,
+ path_driver_info_t *);
+
+ APR_ARRAY_PUSH(paths, const char *) = info->dst_path;
+ if (is_move && (! info->resurrection))
+ APR_ARRAY_PUSH(paths, const char *) = info->src_path;
+ }
+
+ SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table,
+ message, ctx, pool));
+
+ /* Fetch RA commit editor. */
+ SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session,
+ svn_client__get_shim_callbacks(ctx->wc_ctx,
+ NULL, pool)));
+ SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton,
+ commit_revprops,
+ commit_callback,
+ commit_baton,
+ NULL, TRUE, /* No lock tokens */
+ pool));
+
+ /* Setup the callback baton. */
+ cb_baton.editor = editor;
+ cb_baton.edit_baton = edit_baton;
+ cb_baton.action_hash = action_hash;
+ cb_baton.is_move = is_move;
+
+ /* Call the path-based editor driver. */
+ err = svn_delta_path_driver2(editor, edit_baton, paths, TRUE,
+ path_driver_cb_func, &cb_baton, pool);
+ if (err)
+ {
+ /* At least try to abort the edit (and fs txn) before throwing err. */
+ return svn_error_compose_create(
+ err,
+ editor->abort_edit(edit_baton, pool));
+ }
+
+ /* Close the edit. */
+ return svn_error_trace(editor->close_edit(edit_baton, pool));
+}
+
+/* Baton for check_url_kind */
+struct check_url_kind_baton
+{
+ svn_ra_session_t *session;
+ const char *repos_root_url;
+ svn_boolean_t should_reparent;
+};
+
+/* Implements svn_client__check_url_kind_t for wc_to_repos_copy */
+static svn_error_t *
+check_url_kind(void *baton,
+ svn_node_kind_t *kind,
+ const char *url,
+ svn_revnum_t revision,
+ apr_pool_t *scratch_pool)
+{
+ struct check_url_kind_baton *cukb = baton;
+
+ /* If we don't have a session or can't use the session, get one */
+ if (!svn_uri__is_ancestor(cukb->repos_root_url, url))
+ *kind = svn_node_none;
+ else
+ {
+ cukb->should_reparent = TRUE;
+
+ SVN_ERR(svn_ra_reparent(cukb->session, url, scratch_pool));
+
+ SVN_ERR(svn_ra_check_path(cukb->session, "", revision,
+ kind, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* ### Copy ...
+ * COMMIT_INFO_P is ...
+ * COPY_PAIRS is ... such that each 'src_abspath_or_url' is a local abspath
+ * and each 'dst_abspath_or_url' is a URL.
+ * MAKE_PARENTS is ...
+ * REVPROP_TABLE is ...
+ * CTX is ... */
+static svn_error_t *
+wc_to_repos_copy(const apr_array_header_t *copy_pairs,
+ svn_boolean_t make_parents,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ const char *message;
+ const char *top_src_path, *top_dst_url;
+ struct check_url_kind_baton cukb;
+ const char *top_src_abspath;
+ svn_ra_session_t *ra_session;
+ const svn_delta_editor_t *editor;
+ apr_hash_t *relpath_map = NULL;
+ void *edit_baton;
+ svn_client__committables_t *committables;
+ apr_array_header_t *commit_items;
+ apr_pool_t *iterpool;
+ apr_array_header_t *new_dirs = NULL;
+ apr_hash_t *commit_revprops;
+ svn_client__copy_pair_t *first_pair;
+ apr_pool_t *session_pool = svn_pool_create(scratch_pool);
+ int i;
+
+ /* Find the common root of all the source paths */
+ SVN_ERR(get_copy_pair_ancestors(copy_pairs, &top_src_path, NULL, NULL,
+ scratch_pool));
+
+ /* Do we need to lock the working copy? 1.6 didn't take a write
+ lock, but what happens if the working copy changes during the copy
+ operation? */
+
+ iterpool = svn_pool_create(scratch_pool);
+
+ /* Determine the longest common ancestor for the destinations, and open an RA
+ session to that location. */
+ /* ### But why start by getting the _parent_ of the first one? */
+ /* --- That works because multiple destinations always point to the same
+ * directory. I'm rather wondering why we need to find a common
+ * destination parent here at all, instead of simply getting
+ * top_dst_url from get_copy_pair_ancestors() above?
+ * It looks like the entire block of code hanging off this comment
+ * is redundant. */
+ first_pair = APR_ARRAY_IDX(copy_pairs, 0, svn_client__copy_pair_t *);
+ top_dst_url = svn_uri_dirname(first_pair->dst_abspath_or_url, scratch_pool);
+ for (i = 1; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ top_dst_url = svn_uri_get_longest_ancestor(top_dst_url,
+ pair->dst_abspath_or_url,
+ scratch_pool);
+ }
+
+ SVN_ERR(svn_dirent_get_absolute(&top_src_abspath, top_src_path, scratch_pool));
+
+ /* Open a session to help while determining the exact targets */
+ SVN_ERR(svn_client__open_ra_session_internal(&ra_session, NULL, top_dst_url,
+ top_src_abspath, NULL,
+ FALSE /* write_dav_props */,
+ TRUE /* read_dav_props */,
+ ctx,
+ session_pool, session_pool));
+
+ /* If requested, determine the nearest existing parent of the destination,
+ and reparent the ra session there. */
+ if (make_parents)
+ {
+ new_dirs = apr_array_make(scratch_pool, 0, sizeof(const char *));
+ SVN_ERR(find_absent_parents2(ra_session, &top_dst_url, new_dirs,
+ scratch_pool));
+ }
+
+ /* Figure out the basename that will result from each copy and check to make
+ sure it doesn't exist already. */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_node_kind_t dst_kind;
+ const char *dst_rel;
+ svn_client__copy_pair_t *pair =
+ APR_ARRAY_IDX(copy_pairs, i, svn_client__copy_pair_t *);
+
+ svn_pool_clear(iterpool);
+ dst_rel = svn_uri_skip_ancestor(top_dst_url, pair->dst_abspath_or_url,
+ iterpool);
+ SVN_ERR(svn_ra_check_path(ra_session, dst_rel, SVN_INVALID_REVNUM,
+ &dst_kind, iterpool));
+ if (dst_kind != svn_node_none)
+ {
+ return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL,
+ _("Path '%s' already exists"),
+ pair->dst_abspath_or_url);
+ }
+ }
+
+ if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx))
+ {
+ /* Produce a list of new paths to add, and provide it to the
+ mechanism used to acquire a log message. */
+ svn_client_commit_item3_t *item;
+ const char *tmp_file;
+ commit_items = apr_array_make(scratch_pool, copy_pairs->nelts,
+ sizeof(item));
+
+ /* Add any intermediate directories to the message */
+ if (make_parents)
+ {
+ for (i = 0; i < new_dirs->nelts; i++)
+ {
+ const char *url = APR_ARRAY_IDX(new_dirs, i, const char *);
+
+ item = svn_client_commit_item3_create(scratch_pool);
+ item->url = url;
+ item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD;
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
+ }
+ }
+
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+
+ item = svn_client_commit_item3_create(scratch_pool);
+ item->url = pair->dst_abspath_or_url;
+ item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD;
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
+ }
+
+ SVN_ERR(svn_client__get_log_msg(&message, &tmp_file, commit_items,
+ ctx, scratch_pool));
+ if (! message)
+ {
+ svn_pool_destroy(iterpool);
+ svn_pool_destroy(session_pool);
+ return SVN_NO_ERROR;
+ }
+ }
+ else
+ message = "";
+
+ cukb.session = ra_session;
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &cukb.repos_root_url, session_pool));
+ cukb.should_reparent = FALSE;
+
+ /* Crawl the working copy for commit items. */
+ /* ### TODO: Pass check_url_func for issue #3314 handling */
+ SVN_ERR(svn_client__get_copy_committables(&committables,
+ copy_pairs,
+ check_url_kind, &cukb,
+ ctx, scratch_pool, iterpool));
+
+ /* The committables are keyed by the repository root */
+ commit_items = svn_hash_gets(committables->by_repository,
+ cukb.repos_root_url);
+ SVN_ERR_ASSERT(commit_items != NULL);
+
+ if (cukb.should_reparent)
+ SVN_ERR(svn_ra_reparent(ra_session, top_dst_url, session_pool));
+
+ /* If we are creating intermediate directories, tack them onto the list
+ of committables. */
+ if (make_parents)
+ {
+ for (i = 0; i < new_dirs->nelts; i++)
+ {
+ const char *url = APR_ARRAY_IDX(new_dirs, i, const char *);
+ svn_client_commit_item3_t *item;
+
+ item = svn_client_commit_item3_create(scratch_pool);
+ item->url = url;
+ item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD;
+ item->incoming_prop_changes = apr_array_make(scratch_pool, 1,
+ sizeof(svn_prop_t *));
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
+ }
+ }
+
+ /* ### TODO: This extra loop would be unnecessary if this code lived
+ ### in svn_client__get_copy_committables(), which is incidentally
+ ### only used above (so should really be in this source file). */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ apr_hash_t *mergeinfo, *wc_mergeinfo;
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ svn_client_commit_item3_t *item =
+ APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
+ svn_client__pathrev_t *src_origin;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_client__wc_node_get_origin(&src_origin,
+ pair->src_abspath_or_url,
+ ctx, iterpool, iterpool));
+
+ /* Set the mergeinfo for the destination to the combined merge
+ info known to the WC and the repository. */
+ item->outgoing_prop_changes = apr_array_make(scratch_pool, 1,
+ sizeof(svn_prop_t *));
+ /* Repository mergeinfo (or NULL if it's locally added)... */
+ if (src_origin)
+ SVN_ERR(svn_client__get_repos_mergeinfo(
+ &mergeinfo, ra_session, src_origin->url, src_origin->rev,
+ svn_mergeinfo_inherited, TRUE /*sqelch_inc.*/, iterpool));
+ else
+ mergeinfo = NULL;
+ /* ... and WC mergeinfo. */
+ SVN_ERR(svn_client__parse_mergeinfo(&wc_mergeinfo, ctx->wc_ctx,
+ pair->src_abspath_or_url,
+ iterpool, iterpool));
+ if (wc_mergeinfo && mergeinfo)
+ SVN_ERR(svn_mergeinfo_merge2(mergeinfo, wc_mergeinfo, iterpool,
+ iterpool));
+ else if (! mergeinfo)
+ mergeinfo = wc_mergeinfo;
+ if (mergeinfo)
+ {
+ /* Push a mergeinfo prop representing MERGEINFO onto the
+ * OUTGOING_PROP_CHANGES array. */
+
+ svn_prop_t *mergeinfo_prop
+ = apr_palloc(item->outgoing_prop_changes->pool,
+ sizeof(svn_prop_t));
+ svn_string_t *prop_value;
+
+ SVN_ERR(svn_mergeinfo_to_string(&prop_value, mergeinfo,
+ item->outgoing_prop_changes->pool));
+
+ mergeinfo_prop->name = SVN_PROP_MERGEINFO;
+ mergeinfo_prop->value = prop_value;
+ APR_ARRAY_PUSH(item->outgoing_prop_changes, svn_prop_t *)
+ = mergeinfo_prop;
+ }
+ }
+
+ /* Sort and condense our COMMIT_ITEMS. */
+ SVN_ERR(svn_client__condense_commit_items(&top_dst_url,
+ commit_items, scratch_pool));
+
+#ifdef ENABLE_EV2_SHIMS
+ if (commit_items)
+ {
+ relpath_map = apr_hash_make(pool);
+ for (i = 0; i < commit_items->nelts; i++)
+ {
+ svn_client_commit_item3_t *item = APR_ARRAY_IDX(commit_items, i,
+ svn_client_commit_item3_t *);
+ const char *relpath;
+
+ if (!item->path)
+ continue;
+
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_wc__node_get_origin(NULL, NULL, &relpath, NULL, NULL, NULL,
+ ctx->wc_ctx, item->path, FALSE,
+ scratch_pool, iterpool));
+ if (relpath)
+ svn_hash_sets(relpath_map, relpath, item->path);
+ }
+ }
+#endif
+
+ /* Close the initial session, to reopen a new session with commit handling */
+ svn_pool_clear(session_pool);
+
+ /* Open a new RA session to DST_URL. */
+ SVN_ERR(svn_client__open_ra_session_internal(&ra_session, NULL, top_dst_url,
+ NULL, commit_items,
+ FALSE, FALSE, ctx,
+ session_pool, session_pool));
+
+ SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table,
+ message, ctx, session_pool));
+
+ /* Fetch RA commit editor. */
+ SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session,
+ svn_client__get_shim_callbacks(ctx->wc_ctx, relpath_map,
+ session_pool)));
+ SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton,
+ commit_revprops,
+ commit_callback,
+ commit_baton, NULL,
+ TRUE, /* No lock tokens */
+ session_pool));
+
+ /* Perform the commit. */
+ SVN_ERR_W(svn_client__do_commit(top_dst_url, commit_items,
+ editor, edit_baton,
+ 0, /* ### any notify_path_offset needed? */
+ NULL, ctx, session_pool, session_pool),
+ _("Commit failed (details follow):"));
+
+ svn_pool_destroy(iterpool);
+ svn_pool_destroy(session_pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* A baton for notification_adjust_func(). */
+struct notification_adjust_baton
+{
+ svn_wc_notify_func2_t inner_func;
+ void *inner_baton;
+ const char *checkout_abspath;
+ const char *final_abspath;
+};
+
+/* A svn_wc_notify_func2_t function that wraps BATON->inner_func (whose
+ * baton is BATON->inner_baton) and adjusts the notification paths that
+ * start with BATON->checkout_abspath to start instead with
+ * BATON->final_abspath. */
+static void
+notification_adjust_func(void *baton,
+ const svn_wc_notify_t *notify,
+ apr_pool_t *pool)
+{
+ struct notification_adjust_baton *nb = baton;
+ svn_wc_notify_t *inner_notify = svn_wc_dup_notify(notify, pool);
+ const char *relpath;
+
+ relpath = svn_dirent_skip_ancestor(nb->checkout_abspath, notify->path);
+ inner_notify->path = svn_dirent_join(nb->final_abspath, relpath, pool);
+
+ if (nb->inner_func)
+ nb->inner_func(nb->inner_baton, inner_notify, pool);
+}
+
+/* Peform each individual copy operation for a repos -> wc copy. A
+ helper for repos_to_wc_copy().
+
+ Resolve PAIR->src_revnum to a real revision number if it isn't already. */
+static svn_error_t *
+repos_to_wc_copy_single(svn_boolean_t *timestamp_sleep,
+ svn_client__copy_pair_t *pair,
+ svn_boolean_t same_repositories,
+ svn_boolean_t ignore_externals,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_hash_t *src_mergeinfo;
+ const char *dst_abspath = pair->dst_abspath_or_url;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(dst_abspath));
+
+ if (!same_repositories && ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+ notify = svn_wc_create_notify_url(
+ pair->src_abspath_or_url,
+ svn_wc_notify_foreign_copy_begin,
+ pool);
+ notify->kind = pair->src_kind;
+ ctx->notify_func2(ctx->notify_baton2, notify, pool);
+
+ /* Allow a theoretical cancel to get through. */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+ }
+
+ if (pair->src_kind == svn_node_dir)
+ {
+ if (same_repositories)
+ {
+ svn_boolean_t sleep_needed = FALSE;
+ const char *tmpdir_abspath, *tmp_abspath;
+
+ /* Find a temporary location in which to check out the copy source. */
+ SVN_ERR(svn_wc__get_tmpdir(&tmpdir_abspath, ctx->wc_ctx, dst_abspath,
+ pool, pool));
+
+ SVN_ERR(svn_io_open_unique_file3(NULL, &tmp_abspath, tmpdir_abspath,
+ svn_io_file_del_on_close, pool, pool));
+
+ /* Make a new checkout of the requested source. While doing so,
+ * resolve pair->src_revnum to an actual revision number in case it
+ * was until now 'invalid' meaning 'head'. Ask this function not to
+ * sleep for timestamps, by passing a sleep_needed output param.
+ * Send notifications for all nodes except the root node, and adjust
+ * them to refer to the destination rather than this temporary path. */
+ {
+ svn_wc_notify_func2_t old_notify_func2 = ctx->notify_func2;
+ void *old_notify_baton2 = ctx->notify_baton2;
+ struct notification_adjust_baton nb;
+ svn_error_t *err;
+
+ nb.inner_func = ctx->notify_func2;
+ nb.inner_baton = ctx->notify_baton2;
+ nb.checkout_abspath = tmp_abspath;
+ nb.final_abspath = dst_abspath;
+ ctx->notify_func2 = notification_adjust_func;
+ ctx->notify_baton2 = &nb;
+
+ err = svn_client__checkout_internal(&pair->src_revnum,
+ pair->src_original,
+ tmp_abspath,
+ &pair->src_peg_revision,
+ &pair->src_op_revision,
+ svn_depth_infinity,
+ ignore_externals, FALSE,
+ &sleep_needed, ctx, pool);
+
+ ctx->notify_func2 = old_notify_func2;
+ ctx->notify_baton2 = old_notify_baton2;
+
+ SVN_ERR(err);
+ }
+
+ *timestamp_sleep = TRUE;
+
+ /* Schedule dst_path for addition in parent, with copy history.
+ Don't send any notification here.
+ Then remove the temporary checkout's .svn dir in preparation for
+ moving the rest of it into the final destination. */
+ SVN_ERR(svn_wc_copy3(ctx->wc_ctx, tmp_abspath, dst_abspath,
+ TRUE /* metadata_only */,
+ ctx->cancel_func, ctx->cancel_baton,
+ NULL, NULL, pool));
+ SVN_ERR(svn_wc__acquire_write_lock(NULL, ctx->wc_ctx, tmp_abspath,
+ FALSE, pool, pool));
+ SVN_ERR(svn_wc_remove_from_revision_control2(ctx->wc_ctx,
+ tmp_abspath,
+ FALSE, FALSE,
+ ctx->cancel_func,
+ ctx->cancel_baton,
+ pool));
+
+ /* Move the temporary disk tree into place. */
+ SVN_ERR(svn_io_file_rename(tmp_abspath, dst_abspath, pool));
+ }
+ else
+ {
+ *timestamp_sleep = TRUE;
+
+ SVN_ERR(svn_client__copy_foreign(pair->src_abspath_or_url,
+ dst_abspath,
+ &pair->src_peg_revision,
+ &pair->src_op_revision,
+ svn_depth_infinity,
+ FALSE /* make_parents */,
+ TRUE /* already_locked */,
+ ctx, pool));
+
+ return SVN_NO_ERROR;
+ }
+ } /* end directory case */
+
+ else if (pair->src_kind == svn_node_file)
+ {
+ apr_hash_t *new_props;
+ const char *src_rel;
+ svn_stream_t *new_base_contents = svn_stream_buffered(pool);
+
+ SVN_ERR(svn_ra_get_path_relative_to_session(ra_session, &src_rel,
+ pair->src_abspath_or_url,
+ pool));
+ /* Fetch the file content. While doing so, resolve pair->src_revnum
+ * to an actual revision number if it's 'invalid' meaning 'head'. */
+ SVN_ERR(svn_ra_get_file(ra_session, src_rel, pair->src_revnum,
+ new_base_contents,
+ &pair->src_revnum, &new_props, pool));
+
+ if (new_props && ! same_repositories)
+ svn_hash_sets(new_props, SVN_PROP_MERGEINFO, NULL);
+
+ *timestamp_sleep = TRUE;
+
+ SVN_ERR(svn_wc_add_repos_file4(
+ ctx->wc_ctx, dst_abspath,
+ new_base_contents, NULL, new_props, NULL,
+ same_repositories ? pair->src_abspath_or_url : NULL,
+ same_repositories ? pair->src_revnum : SVN_INVALID_REVNUM,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool));
+ }
+
+ /* Record the implied mergeinfo (before the notification callback
+ is invoked for the root node). */
+ SVN_ERR(svn_client__get_repos_mergeinfo(
+ &src_mergeinfo, ra_session,
+ pair->src_abspath_or_url, pair->src_revnum,
+ svn_mergeinfo_inherited, TRUE /*squelch_incapable*/, pool));
+ SVN_ERR(extend_wc_mergeinfo(dst_abspath, src_mergeinfo, ctx, pool));
+
+ /* Do our own notification for the root node, even if we could possibly
+ have delegated it. See also issue #1552.
+
+ ### Maybe this notification should mention the mergeinfo change. */
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify = svn_wc_create_notify(
+ dst_abspath, svn_wc_notify_add, pool);
+ notify->kind = pair->src_kind;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+repos_to_wc_copy_locked(svn_boolean_t *timestamp_sleep,
+ const apr_array_header_t *copy_pairs,
+ const char *top_dst_path,
+ svn_boolean_t ignore_externals,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ svn_boolean_t same_repositories;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ /* We've already checked for physical obstruction by a working file.
+ But there could also be logical obstruction by an entry whose
+ working file happens to be missing.*/
+ SVN_ERR(verify_wc_dsts(copy_pairs, FALSE, FALSE, ctx,
+ scratch_pool, iterpool));
+
+ /* Decide whether the two repositories are the same or not. */
+ {
+ svn_error_t *src_err, *dst_err;
+ const char *parent;
+ const char *parent_abspath;
+ const char *src_uuid, *dst_uuid;
+
+ /* Get the repository uuid of SRC_URL */
+ src_err = svn_ra_get_uuid2(ra_session, &src_uuid, iterpool);
+ if (src_err && src_err->apr_err != SVN_ERR_RA_NO_REPOS_UUID)
+ return svn_error_trace(src_err);
+
+ /* Get repository uuid of dst's parent directory, since dst may
+ not exist. ### TODO: we should probably walk up the wc here,
+ in case the parent dir has an imaginary URL. */
+ if (copy_pairs->nelts == 1)
+ parent = svn_dirent_dirname(top_dst_path, scratch_pool);
+ else
+ parent = top_dst_path;
+
+ SVN_ERR(svn_dirent_get_absolute(&parent_abspath, parent, scratch_pool));
+ dst_err = svn_client_get_repos_root(NULL /* root_url */, &dst_uuid,
+ parent_abspath, ctx,
+ iterpool, iterpool);
+ if (dst_err && dst_err->apr_err != SVN_ERR_RA_NO_REPOS_UUID)
+ return dst_err;
+
+ /* If either of the UUIDs are nonexistent, then at least one of
+ the repositories must be very old. Rather than punish the
+ user, just assume the repositories are different, so no
+ copy-history is attempted. */
+ if (src_err || dst_err || (! src_uuid) || (! dst_uuid))
+ same_repositories = FALSE;
+ else
+ same_repositories = (strcmp(src_uuid, dst_uuid) == 0);
+ }
+
+ /* Perform the move for each of the copy_pairs. */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ /* Check for cancellation */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(repos_to_wc_copy_single(timestamp_sleep,
+ APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *),
+ same_repositories,
+ ignore_externals,
+ ra_session, ctx, iterpool));
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+repos_to_wc_copy(svn_boolean_t *timestamp_sleep,
+ const apr_array_header_t *copy_pairs,
+ svn_boolean_t make_parents,
+ svn_boolean_t ignore_externals,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *ra_session;
+ const char *top_src_url, *top_dst_path;
+ apr_pool_t *iterpool = svn_pool_create(pool);
+ const char *lock_abspath;
+ int i;
+
+ /* Get the real path for the source, based upon its peg revision. */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ const char *src;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_client__repos_locations(&src, &pair->src_revnum, NULL, NULL,
+ NULL,
+ pair->src_abspath_or_url,
+ &pair->src_peg_revision,
+ &pair->src_op_revision, NULL,
+ ctx, iterpool));
+
+ pair->src_original = pair->src_abspath_or_url;
+ pair->src_abspath_or_url = apr_pstrdup(pool, src);
+ }
+
+ SVN_ERR(get_copy_pair_ancestors(copy_pairs, &top_src_url, &top_dst_path,
+ NULL, pool));
+ lock_abspath = top_dst_path;
+ if (copy_pairs->nelts == 1)
+ {
+ top_src_url = svn_uri_dirname(top_src_url, pool);
+ lock_abspath = svn_dirent_dirname(top_dst_path, pool);
+ }
+
+ /* Open a repository session to the longest common src ancestor. We do not
+ (yet) have a working copy, so we don't have a corresponding path and
+ tempfiles cannot go into the admin area. */
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, top_src_url, lock_abspath,
+ ctx, pool, pool));
+
+ /* Get the correct src path for the peg revision used, and verify that we
+ aren't overwriting an existing path. */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+ svn_node_kind_t dst_parent_kind, dst_kind;
+ const char *dst_parent;
+ const char *src_rel;
+
+ svn_pool_clear(iterpool);
+
+ /* Next, make sure that the path exists in the repository. */
+ SVN_ERR(svn_ra_get_path_relative_to_session(ra_session, &src_rel,
+ pair->src_abspath_or_url,
+ iterpool));
+ SVN_ERR(svn_ra_check_path(ra_session, src_rel, pair->src_revnum,
+ &pair->src_kind, pool));
+ if (pair->src_kind == svn_node_none)
+ {
+ if (SVN_IS_VALID_REVNUM(pair->src_revnum))
+ return svn_error_createf
+ (SVN_ERR_FS_NOT_FOUND, NULL,
+ _("Path '%s' not found in revision %ld"),
+ pair->src_abspath_or_url, pair->src_revnum);
+ else
+ return svn_error_createf
+ (SVN_ERR_FS_NOT_FOUND, NULL,
+ _("Path '%s' not found in head revision"),
+ pair->src_abspath_or_url);
+ }
+
+ /* Figure out about dst. */
+ SVN_ERR(svn_io_check_path(pair->dst_abspath_or_url, &dst_kind,
+ iterpool));
+ if (dst_kind != svn_node_none)
+ {
+ return svn_error_createf(
+ SVN_ERR_ENTRY_EXISTS, NULL,
+ _("Path '%s' already exists"),
+ svn_dirent_local_style(pair->dst_abspath_or_url, pool));
+ }
+
+ /* Make sure the destination parent is a directory and produce a clear
+ error message if it is not. */
+ dst_parent = svn_dirent_dirname(pair->dst_abspath_or_url, iterpool);
+ SVN_ERR(svn_io_check_path(dst_parent, &dst_parent_kind, iterpool));
+ if (make_parents && dst_parent_kind == svn_node_none)
+ {
+ SVN_ERR(svn_client__make_local_parents(dst_parent, TRUE, ctx,
+ iterpool));
+ }
+ else if (dst_parent_kind != svn_node_dir)
+ {
+ return svn_error_createf(SVN_ERR_WC_NOT_WORKING_COPY, NULL,
+ _("Path '%s' is not a directory"),
+ svn_dirent_local_style(dst_parent, pool));
+ }
+ }
+ svn_pool_destroy(iterpool);
+
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ repos_to_wc_copy_locked(timestamp_sleep,
+ copy_pairs, top_dst_path, ignore_externals,
+ ra_session, ctx, pool),
+ ctx->wc_ctx, lock_abspath, FALSE, pool);
+ return SVN_NO_ERROR;
+}
+
+#define NEED_REPOS_REVNUM(revision) \
+ ((revision.kind != svn_opt_revision_unspecified) \
+ && (revision.kind != svn_opt_revision_working))
+
+/* ...
+ *
+ * Set *TIMESTAMP_SLEEP to TRUE if a sleep is required; otherwise do not
+ * change *TIMESTAMP_SLEEP. This output will be valid even if the
+ * function returns an error.
+ *
+ * Perform all allocations in POOL.
+ */
+static svn_error_t *
+try_copy(svn_boolean_t *timestamp_sleep,
+ const apr_array_header_t *sources,
+ const char *dst_path_in,
+ svn_boolean_t is_move,
+ svn_boolean_t allow_mixed_revisions,
+ svn_boolean_t metadata_only,
+ svn_boolean_t make_parents,
+ svn_boolean_t ignore_externals,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *copy_pairs =
+ apr_array_make(pool, sources->nelts,
+ sizeof(svn_client__copy_pair_t *));
+ svn_boolean_t srcs_are_urls, dst_is_url;
+ int i;
+
+ /* Are either of our paths URLs? Just check the first src_path. If
+ there are more than one, we'll check for homogeneity among them
+ down below. */
+ srcs_are_urls = svn_path_is_url(APR_ARRAY_IDX(sources, 0,
+ svn_client_copy_source_t *)->path);
+ dst_is_url = svn_path_is_url(dst_path_in);
+ if (!dst_is_url)
+ SVN_ERR(svn_dirent_get_absolute(&dst_path_in, dst_path_in, pool));
+
+ /* If we have multiple source paths, it implies the dst_path is a
+ directory we are moving or copying into. Populate the COPY_PAIRS
+ array to contain a destination path for each of the source paths. */
+ if (sources->nelts > 1)
+ {
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ for (i = 0; i < sources->nelts; i++)
+ {
+ svn_client_copy_source_t *source = APR_ARRAY_IDX(sources, i,
+ svn_client_copy_source_t *);
+ svn_client__copy_pair_t *pair = apr_palloc(pool, sizeof(*pair));
+ const char *src_basename;
+ svn_boolean_t src_is_url = svn_path_is_url(source->path);
+
+ svn_pool_clear(iterpool);
+
+ if (src_is_url)
+ {
+ pair->src_abspath_or_url = apr_pstrdup(pool, source->path);
+ src_basename = svn_uri_basename(pair->src_abspath_or_url,
+ iterpool);
+ }
+ else
+ {
+ SVN_ERR(svn_dirent_get_absolute(&pair->src_abspath_or_url,
+ source->path, pool));
+ src_basename = svn_dirent_basename(pair->src_abspath_or_url,
+ iterpool);
+ }
+
+ pair->src_op_revision = *source->revision;
+ pair->src_peg_revision = *source->peg_revision;
+
+ SVN_ERR(svn_opt_resolve_revisions(&pair->src_peg_revision,
+ &pair->src_op_revision,
+ src_is_url,
+ TRUE,
+ iterpool));
+
+ /* Check to see if all the sources are urls or all working copy
+ * paths. */
+ if (src_is_url != srcs_are_urls)
+ return svn_error_create
+ (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Cannot mix repository and working copy sources"));
+
+ if (dst_is_url)
+ pair->dst_abspath_or_url =
+ svn_path_url_add_component2(dst_path_in, src_basename, pool);
+ else
+ pair->dst_abspath_or_url = svn_dirent_join(dst_path_in,
+ src_basename, pool);
+ APR_ARRAY_PUSH(copy_pairs, svn_client__copy_pair_t *) = pair;
+ }
+
+ svn_pool_destroy(iterpool);
+ }
+ else
+ {
+ /* Only one source path. */
+ svn_client__copy_pair_t *pair = apr_palloc(pool, sizeof(*pair));
+ svn_client_copy_source_t *source =
+ APR_ARRAY_IDX(sources, 0, svn_client_copy_source_t *);
+ svn_boolean_t src_is_url = svn_path_is_url(source->path);
+
+ if (src_is_url)
+ pair->src_abspath_or_url = apr_pstrdup(pool, source->path);
+ else
+ SVN_ERR(svn_dirent_get_absolute(&pair->src_abspath_or_url,
+ source->path, pool));
+ pair->src_op_revision = *source->revision;
+ pair->src_peg_revision = *source->peg_revision;
+
+ SVN_ERR(svn_opt_resolve_revisions(&pair->src_peg_revision,
+ &pair->src_op_revision,
+ src_is_url, TRUE, pool));
+
+ pair->dst_abspath_or_url = dst_path_in;
+ APR_ARRAY_PUSH(copy_pairs, svn_client__copy_pair_t *) = pair;
+ }
+
+ if (!srcs_are_urls && !dst_is_url)
+ {
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+
+ svn_pool_clear(iterpool);
+
+ if (svn_dirent_is_child(pair->src_abspath_or_url,
+ pair->dst_abspath_or_url, iterpool))
+ return svn_error_createf
+ (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Cannot copy path '%s' into its own child '%s'"),
+ svn_dirent_local_style(pair->src_abspath_or_url, pool),
+ svn_dirent_local_style(pair->dst_abspath_or_url, pool));
+ }
+
+ svn_pool_destroy(iterpool);
+ }
+
+ /* A file external should not be moved since the file external is
+ implemented as a switched file and it would delete the file the
+ file external is switched to, which is not the behavior the user
+ would probably want. */
+ if (is_move && !srcs_are_urls)
+ {
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair =
+ APR_ARRAY_IDX(copy_pairs, i, svn_client__copy_pair_t *);
+ svn_node_kind_t external_kind;
+ const char *defining_abspath;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(pair->src_abspath_or_url));
+ SVN_ERR(svn_wc__read_external_info(&external_kind, &defining_abspath,
+ NULL, NULL, NULL, ctx->wc_ctx,
+ pair->src_abspath_or_url,
+ pair->src_abspath_or_url, TRUE,
+ iterpool, iterpool));
+
+ if (external_kind != svn_node_none)
+ return svn_error_createf(
+ SVN_ERR_WC_CANNOT_MOVE_FILE_EXTERNAL,
+ NULL,
+ _("Cannot move the external at '%s'; please "
+ "edit the svn:externals property on '%s'."),
+ svn_dirent_local_style(pair->src_abspath_or_url, pool),
+ svn_dirent_local_style(defining_abspath, pool));
+ }
+ svn_pool_destroy(iterpool);
+ }
+
+ if (is_move)
+ {
+ /* Disallow moves between the working copy and the repository. */
+ if (srcs_are_urls != dst_is_url)
+ {
+ return svn_error_create
+ (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Moves between the working copy and the repository are not "
+ "supported"));
+ }
+
+ /* Disallow moving any path/URL onto or into itself. */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+
+ if (strcmp(pair->src_abspath_or_url,
+ pair->dst_abspath_or_url) == 0)
+ return svn_error_createf(
+ SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ srcs_are_urls ?
+ _("Cannot move URL '%s' into itself") :
+ _("Cannot move path '%s' into itself"),
+ srcs_are_urls ?
+ pair->src_abspath_or_url :
+ svn_dirent_local_style(pair->src_abspath_or_url, pool));
+ }
+ }
+ else
+ {
+ if (!srcs_are_urls)
+ {
+ /* If we are doing a wc->* copy, but with an operational revision
+ other than the working copy revision, we are really doing a
+ repo->* copy, because we're going to need to get the rev from the
+ repo. */
+
+ svn_boolean_t need_repos_op_rev = FALSE;
+ svn_boolean_t need_repos_peg_rev = FALSE;
+
+ /* Check to see if any revision is something other than
+ svn_opt_revision_unspecified or svn_opt_revision_working. */
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+
+ if (NEED_REPOS_REVNUM(pair->src_op_revision))
+ need_repos_op_rev = TRUE;
+
+ if (NEED_REPOS_REVNUM(pair->src_peg_revision))
+ need_repos_peg_rev = TRUE;
+
+ if (need_repos_op_rev || need_repos_peg_rev)
+ break;
+ }
+
+ if (need_repos_op_rev || need_repos_peg_rev)
+ {
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ for (i = 0; i < copy_pairs->nelts; i++)
+ {
+ const char *copyfrom_repos_root_url;
+ const char *copyfrom_repos_relpath;
+ const char *url;
+ svn_revnum_t copyfrom_rev;
+ svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i,
+ svn_client__copy_pair_t *);
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(pair->src_abspath_or_url));
+
+ SVN_ERR(svn_wc__node_get_origin(NULL, &copyfrom_rev,
+ &copyfrom_repos_relpath,
+ &copyfrom_repos_root_url,
+ NULL, NULL,
+ ctx->wc_ctx,
+ pair->src_abspath_or_url,
+ TRUE, iterpool, iterpool));
+
+ if (copyfrom_repos_relpath)
+ url = svn_path_url_add_component2(copyfrom_repos_root_url,
+ copyfrom_repos_relpath,
+ pool);
+ else
+ return svn_error_createf
+ (SVN_ERR_ENTRY_MISSING_URL, NULL,
+ _("'%s' does not have a URL associated with it"),
+ svn_dirent_local_style(pair->src_abspath_or_url, pool));
+
+ pair->src_abspath_or_url = url;
+
+ if (!need_repos_peg_rev
+ || pair->src_peg_revision.kind == svn_opt_revision_base)
+ {
+ /* Default the peg revision to that of the WC entry. */
+ pair->src_peg_revision.kind = svn_opt_revision_number;
+ pair->src_peg_revision.value.number = copyfrom_rev;
+ }
+
+ if (pair->src_op_revision.kind == svn_opt_revision_base)
+ {
+ /* Use the entry's revision as the operational rev. */
+ pair->src_op_revision.kind = svn_opt_revision_number;
+ pair->src_op_revision.value.number = copyfrom_rev;
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ srcs_are_urls = TRUE;
+ }
+ }
+ }
+
+ /* Now, call the right handler for the operation. */
+ if ((! srcs_are_urls) && (! dst_is_url))
+ {
+ SVN_ERR(verify_wc_srcs_and_dsts(copy_pairs, make_parents, is_move,
+ ctx, pool, pool));
+
+ /* Copy or move all targets. */
+ if (is_move)
+ return svn_error_trace(do_wc_to_wc_moves(timestamp_sleep,
+ copy_pairs, dst_path_in,
+ allow_mixed_revisions,
+ metadata_only,
+ ctx, pool));
+ else
+ {
+ /* We ignore these values, so assert the default value */
+ SVN_ERR_ASSERT(allow_mixed_revisions && !metadata_only);
+ return svn_error_trace(do_wc_to_wc_copies(timestamp_sleep,
+ copy_pairs, ctx, pool));
+ }
+ }
+ else if ((! srcs_are_urls) && (dst_is_url))
+ {
+ return svn_error_trace(
+ wc_to_repos_copy(copy_pairs, make_parents, revprop_table,
+ commit_callback, commit_baton, ctx, pool));
+ }
+ else if ((srcs_are_urls) && (! dst_is_url))
+ {
+ return svn_error_trace(
+ repos_to_wc_copy(timestamp_sleep,
+ copy_pairs, make_parents, ignore_externals,
+ ctx, pool));
+ }
+ else
+ {
+ return svn_error_trace(
+ repos_to_repos_copy(copy_pairs, make_parents, revprop_table,
+ commit_callback, commit_baton, ctx, is_move,
+ pool));
+ }
+}
+
+
+
+/* Public Interfaces */
+svn_error_t *
+svn_client_copy6(const apr_array_header_t *sources,
+ const char *dst_path,
+ svn_boolean_t copy_as_child,
+ svn_boolean_t make_parents,
+ svn_boolean_t ignore_externals,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+ svn_boolean_t timestamp_sleep = FALSE;
+ apr_pool_t *subpool = svn_pool_create(pool);
+
+ if (sources->nelts > 1 && !copy_as_child)
+ return svn_error_create(SVN_ERR_CLIENT_MULTIPLE_SOURCES_DISALLOWED,
+ NULL, NULL);
+
+ err = try_copy(&timestamp_sleep,
+ sources, dst_path,
+ FALSE /* is_move */,
+ TRUE /* allow_mixed_revisions */,
+ FALSE /* metadata_only */,
+ make_parents,
+ ignore_externals,
+ revprop_table,
+ commit_callback, commit_baton,
+ ctx,
+ subpool);
+
+ /* If the destination exists, try to copy the sources as children of the
+ destination. */
+ if (copy_as_child && err && (sources->nelts == 1)
+ && (err->apr_err == SVN_ERR_ENTRY_EXISTS
+ || err->apr_err == SVN_ERR_FS_ALREADY_EXISTS))
+ {
+ const char *src_path = APR_ARRAY_IDX(sources, 0,
+ svn_client_copy_source_t *)->path;
+ const char *src_basename;
+ svn_boolean_t src_is_url = svn_path_is_url(src_path);
+ svn_boolean_t dst_is_url = svn_path_is_url(dst_path);
+
+ svn_error_clear(err);
+ svn_pool_clear(subpool);
+
+ src_basename = src_is_url ? svn_uri_basename(src_path, subpool)
+ : svn_dirent_basename(src_path, subpool);
+ dst_path
+ = dst_is_url ? svn_path_url_add_component2(dst_path, src_basename,
+ subpool)
+ : svn_dirent_join(dst_path, src_basename, subpool);
+
+ err = try_copy(&timestamp_sleep,
+ sources, dst_path,
+ FALSE /* is_move */,
+ TRUE /* allow_mixed_revisions */,
+ FALSE /* metadata_only */,
+ make_parents,
+ ignore_externals,
+ revprop_table,
+ commit_callback, commit_baton,
+ ctx,
+ subpool);
+ }
+
+ /* Sleep if required. DST_PATH is not a URL in these cases. */
+ if (timestamp_sleep)
+ svn_io_sleep_for_timestamps(dst_path, subpool);
+
+ svn_pool_destroy(subpool);
+ return svn_error_trace(err);
+}
+
+
+svn_error_t *
+svn_client_move7(const apr_array_header_t *src_paths,
+ const char *dst_path,
+ svn_boolean_t move_as_child,
+ svn_boolean_t make_parents,
+ svn_boolean_t allow_mixed_revisions,
+ svn_boolean_t metadata_only,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const svn_opt_revision_t head_revision
+ = { svn_opt_revision_head, { 0 } };
+ svn_error_t *err;
+ svn_boolean_t timestamp_sleep = FALSE;
+ int i;
+ apr_pool_t *subpool = svn_pool_create(pool);
+ apr_array_header_t *sources = apr_array_make(pool, src_paths->nelts,
+ sizeof(const svn_client_copy_source_t *));
+
+ if (src_paths->nelts > 1 && !move_as_child)
+ return svn_error_create(SVN_ERR_CLIENT_MULTIPLE_SOURCES_DISALLOWED,
+ NULL, NULL);
+
+ for (i = 0; i < src_paths->nelts; i++)
+ {
+ const char *src_path = APR_ARRAY_IDX(src_paths, i, const char *);
+ svn_client_copy_source_t *copy_source = apr_palloc(pool,
+ sizeof(*copy_source));
+
+ copy_source->path = src_path;
+ copy_source->revision = &head_revision;
+ copy_source->peg_revision = &head_revision;
+
+ APR_ARRAY_PUSH(sources, svn_client_copy_source_t *) = copy_source;
+ }
+
+ err = try_copy(&timestamp_sleep,
+ sources, dst_path,
+ TRUE /* is_move */,
+ allow_mixed_revisions,
+ metadata_only,
+ make_parents,
+ FALSE /* ignore_externals */,
+ revprop_table,
+ commit_callback, commit_baton,
+ ctx,
+ subpool);
+
+ /* If the destination exists, try to move the sources as children of the
+ destination. */
+ if (move_as_child && err && (src_paths->nelts == 1)
+ && (err->apr_err == SVN_ERR_ENTRY_EXISTS
+ || err->apr_err == SVN_ERR_FS_ALREADY_EXISTS))
+ {
+ const char *src_path = APR_ARRAY_IDX(src_paths, 0, const char *);
+ const char *src_basename;
+ svn_boolean_t src_is_url = svn_path_is_url(src_path);
+ svn_boolean_t dst_is_url = svn_path_is_url(dst_path);
+
+ svn_error_clear(err);
+ svn_pool_clear(subpool);
+
+ src_basename = src_is_url ? svn_uri_basename(src_path, pool)
+ : svn_dirent_basename(src_path, pool);
+ dst_path
+ = dst_is_url ? svn_path_url_add_component2(dst_path, src_basename,
+ subpool)
+ : svn_dirent_join(dst_path, src_basename, subpool);
+
+ err = try_copy(&timestamp_sleep,
+ sources, dst_path,
+ TRUE /* is_move */,
+ allow_mixed_revisions,
+ metadata_only,
+ make_parents,
+ FALSE /* ignore_externals */,
+ revprop_table,
+ commit_callback, commit_baton,
+ ctx,
+ subpool);
+ }
+
+ /* Sleep if required. DST_PATH is not a URL in these cases. */
+ if (timestamp_sleep)
+ svn_io_sleep_for_timestamps(dst_path, subpool);
+
+ svn_pool_destroy(subpool);
+ return svn_error_trace(err);
+}
diff --git a/subversion/libsvn_client/copy_foreign.c b/subversion/libsvn_client/copy_foreign.c
new file mode 100644
index 0000000..8de8a5d
--- /dev/null
+++ b/subversion/libsvn_client/copy_foreign.c
@@ -0,0 +1,571 @@
+/*
+ * copy_foreign.c: copy from other repository support.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+/*** Includes. ***/
+
+#include <string.h>
+#include "svn_hash.h"
+#include "svn_client.h"
+#include "svn_delta.h"
+#include "svn_dirent_uri.h"
+#include "svn_error.h"
+#include "svn_error_codes.h"
+#include "svn_path.h"
+#include "svn_pools.h"
+#include "svn_props.h"
+#include "svn_ra.h"
+#include "svn_wc.h"
+
+#include <apr_md5.h>
+
+#include "client.h"
+#include "private/svn_subr_private.h"
+#include "private/svn_wc_private.h"
+#include "svn_private_config.h"
+
+struct edit_baton_t
+{
+ apr_pool_t *pool;
+ const char *anchor_abspath;
+
+ svn_wc_context_t *wc_ctx;
+ svn_wc_notify_func2_t notify_func;
+ void *notify_baton;
+};
+
+struct dir_baton_t
+{
+ apr_pool_t *pool;
+
+ struct dir_baton_t *pb;
+ struct edit_baton_t *eb;
+
+ const char *local_abspath;
+
+ svn_boolean_t created;
+ apr_hash_t *properties;
+
+ int users;
+};
+
+/* svn_delta_editor_t function */
+static svn_error_t *
+edit_open(void *edit_baton,
+ svn_revnum_t base_revision,
+ apr_pool_t *result_pool,
+ void **root_baton)
+{
+ struct edit_baton_t *eb = edit_baton;
+ apr_pool_t *dir_pool = svn_pool_create(eb->pool);
+ struct dir_baton_t *db = apr_pcalloc(dir_pool, sizeof(*db));
+
+ db->pool = dir_pool;
+ db->eb = eb;
+ db->users = 1;
+ db->local_abspath = eb->anchor_abspath;
+
+ SVN_ERR(svn_io_make_dir_recursively(eb->anchor_abspath, dir_pool));
+
+ *root_baton = db;
+
+ return SVN_NO_ERROR;
+}
+
+/* svn_delta_editor_t function */
+static svn_error_t *
+edit_close(void *edit_baton,
+ apr_pool_t *scratch_pool)
+{
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+dir_add(const char *path,
+ void *parent_baton,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_revision,
+ apr_pool_t *result_pool,
+ void **child_baton)
+{
+ struct dir_baton_t *pb = parent_baton;
+ struct edit_baton_t *eb = pb->eb;
+ apr_pool_t *dir_pool = svn_pool_create(pb->pool);
+ struct dir_baton_t *db = apr_pcalloc(dir_pool, sizeof(*db));
+ svn_boolean_t under_root;
+
+ pb->users++;
+
+ db->pb = pb;
+ db->eb = pb->eb;
+ db->pool = dir_pool;
+ db->users = 1;
+
+ SVN_ERR(svn_dirent_is_under_root(&under_root, &db->local_abspath,
+ eb->anchor_abspath, path, db->pool));
+ if (! under_root)
+ {
+ return svn_error_createf(
+ SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
+ _("Path '%s' is not in the working copy"),
+ svn_dirent_local_style(path, db->pool));
+ }
+
+ SVN_ERR(svn_io_make_dir_recursively(db->local_abspath, db->pool));
+
+ *child_baton = db;
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+dir_change_prop(void *dir_baton,
+ const char *name,
+ const svn_string_t *value,
+ apr_pool_t *scratch_pool)
+{
+ struct dir_baton_t *db = dir_baton;
+ struct edit_baton_t *eb = db->eb;
+ svn_prop_kind_t prop_kind;
+
+ prop_kind = svn_property_kind2(name);
+
+ if (prop_kind != svn_prop_regular_kind
+ || ! strcmp(name, SVN_PROP_MERGEINFO))
+ {
+ /* We can't handle DAV, ENTRY and merge specific props here */
+ return SVN_NO_ERROR;
+ }
+
+ if (! db->created)
+ {
+ /* We can still store them in the hash for immediate addition
+ with the svn_wc_add_from_disk2() call */
+ if (! db->properties)
+ db->properties = apr_hash_make(db->pool);
+
+ if (value != NULL)
+ svn_hash_sets(db->properties, apr_pstrdup(db->pool, name),
+ svn_string_dup(value, db->pool));
+ }
+ else
+ {
+ /* We have already notified for this directory, so don't do that again */
+ SVN_ERR(svn_wc_prop_set4(eb->wc_ctx, db->local_abspath, name, value,
+ svn_depth_empty, FALSE, NULL,
+ NULL, NULL, /* Cancelation */
+ NULL, NULL, /* Notification */
+ scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Releases the directory baton if there are no more users */
+static svn_error_t *
+maybe_done(struct dir_baton_t *db)
+{
+ db->users--;
+
+ if (db->users == 0)
+ {
+ struct dir_baton_t *pb = db->pb;
+
+ svn_pool_clear(db->pool);
+
+ if (pb)
+ SVN_ERR(maybe_done(pb));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+ensure_added(struct dir_baton_t *db,
+ apr_pool_t *scratch_pool)
+{
+ if (db->created)
+ return SVN_NO_ERROR;
+
+ if (db->pb)
+ SVN_ERR(ensure_added(db->pb, scratch_pool));
+
+ db->created = TRUE;
+
+ /* Add the directory with all the already collected properties */
+ SVN_ERR(svn_wc_add_from_disk2(db->eb->wc_ctx,
+ db->local_abspath,
+ db->properties,
+ db->eb->notify_func,
+ db->eb->notify_baton,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+dir_close(void *dir_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct dir_baton_t *db = dir_baton;
+ /*struct edit_baton_t *eb = db->eb;*/
+
+ SVN_ERR(ensure_added(db, scratch_pool));
+
+ SVN_ERR(maybe_done(db));
+
+ return SVN_NO_ERROR;
+}
+
+struct file_baton_t
+{
+ apr_pool_t *pool;
+
+ struct dir_baton_t *pb;
+ struct edit_baton_t *eb;
+
+ const char *local_abspath;
+ apr_hash_t *properties;
+
+ svn_boolean_t writing;
+ unsigned char digest[APR_MD5_DIGESTSIZE];
+
+ const char *tmp_path;
+};
+
+static svn_error_t *
+file_add(const char *path,
+ void *parent_baton,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_revision,
+ apr_pool_t *result_pool,
+ void **file_baton)
+{
+ struct dir_baton_t *pb = parent_baton;
+ struct edit_baton_t *eb = pb->eb;
+ apr_pool_t *file_pool = svn_pool_create(pb->pool);
+ struct file_baton_t *fb = apr_pcalloc(file_pool, sizeof(*fb));
+ svn_boolean_t under_root;
+
+ pb->users++;
+
+ fb->pool = file_pool;
+ fb->eb = eb;
+ fb->pb = pb;
+
+ SVN_ERR(svn_dirent_is_under_root(&under_root, &fb->local_abspath,
+ eb->anchor_abspath, path, fb->pool));
+ if (! under_root)
+ {
+ return svn_error_createf(
+ SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
+ _("Path '%s' is not in the working copy"),
+ svn_dirent_local_style(path, fb->pool));
+ }
+
+ *file_baton = fb;
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+file_change_prop(void *file_baton,
+ const char *name,
+ const svn_string_t *value,
+ apr_pool_t *scratch_pool)
+{
+ struct file_baton_t *fb = file_baton;
+ svn_prop_kind_t prop_kind;
+
+ prop_kind = svn_property_kind2(name);
+
+ if (prop_kind != svn_prop_regular_kind
+ || ! strcmp(name, SVN_PROP_MERGEINFO))
+ {
+ /* We can't handle DAV, ENTRY and merge specific props here */
+ return SVN_NO_ERROR;
+ }
+
+ /* We store all properties in the hash for immediate addition
+ with the svn_wc_add_from_disk2() call */
+ if (! fb->properties)
+ fb->properties = apr_hash_make(fb->pool);
+
+ if (value != NULL)
+ svn_hash_sets(fb->properties, apr_pstrdup(fb->pool, name),
+ svn_string_dup(value, fb->pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+file_textdelta(void *file_baton,
+ const char *base_checksum,
+ apr_pool_t *result_pool,
+ svn_txdelta_window_handler_t *handler,
+ void **handler_baton)
+{
+ struct file_baton_t *fb = file_baton;
+ svn_stream_t *target;
+
+ SVN_ERR_ASSERT(! fb->writing);
+
+ SVN_ERR(svn_stream_open_writable(&target, fb->local_abspath, fb->pool,
+ fb->pool));
+
+ fb->writing = TRUE;
+ svn_txdelta_apply(svn_stream_empty(fb->pool) /* source */,
+ target,
+ fb->digest,
+ fb->local_abspath,
+ fb->pool,
+ /* Provide the handler directly */
+ handler, handler_baton);
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+file_close(void *file_baton,
+ const char *text_checksum,
+ apr_pool_t *scratch_pool)
+{
+ struct file_baton_t *fb = file_baton;
+ struct edit_baton_t *eb = fb->eb;
+ struct dir_baton_t *pb = fb->pb;
+
+ SVN_ERR(ensure_added(pb, fb->pool));
+
+ if (text_checksum)
+ {
+ svn_checksum_t *expected_checksum;
+ svn_checksum_t *actual_checksum;
+
+ SVN_ERR(svn_checksum_parse_hex(&expected_checksum, svn_checksum_md5,
+ text_checksum, fb->pool));
+ actual_checksum = svn_checksum__from_digest_md5(fb->digest, fb->pool);
+
+ if (! svn_checksum_match(expected_checksum, actual_checksum))
+ return svn_error_trace(
+ svn_checksum_mismatch_err(expected_checksum,
+ actual_checksum,
+ fb->pool,
+ _("Checksum mismatch for '%s'"),
+ svn_dirent_local_style(
+ fb->local_abspath,
+ fb->pool)));
+ }
+
+ SVN_ERR(svn_wc_add_from_disk2(eb->wc_ctx, fb->local_abspath, fb->properties,
+ eb->notify_func, eb->notify_baton,
+ fb->pool));
+
+ svn_pool_destroy(fb->pool);
+ SVN_ERR(maybe_done(pb));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+copy_foreign_dir(svn_ra_session_t *ra_session,
+ svn_client__pathrev_t *location,
+ svn_wc_context_t *wc_ctx,
+ const char *dst_abspath,
+ svn_depth_t depth,
+ svn_wc_notify_func2_t notify_func,
+ void *notify_baton,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct edit_baton_t eb;
+ svn_delta_editor_t *editor = svn_delta_default_editor(scratch_pool);
+ const svn_delta_editor_t *wrapped_editor;
+ void *wrapped_baton;
+ const svn_ra_reporter3_t *reporter;
+ void *reporter_baton;
+
+ eb.pool = scratch_pool;
+ eb.anchor_abspath = dst_abspath;
+
+ eb.wc_ctx = wc_ctx;
+ eb.notify_func = notify_func;
+ eb.notify_baton = notify_baton;
+
+ editor->open_root = edit_open;
+ editor->close_edit = edit_close;
+
+ editor->add_directory = dir_add;
+ editor->change_dir_prop = dir_change_prop;
+ editor->close_directory = dir_close;
+
+ editor->add_file = file_add;
+ editor->change_file_prop = file_change_prop;
+ editor->apply_textdelta = file_textdelta;
+ editor->close_file = file_close;
+
+ SVN_ERR(svn_delta_get_cancellation_editor(cancel_func, cancel_baton,
+ editor, &eb,
+ &wrapped_editor, &wrapped_baton,
+ scratch_pool));
+
+ SVN_ERR(svn_ra_do_update3(ra_session, &reporter, &reporter_baton,
+ location->rev, "", svn_depth_infinity,
+ FALSE, FALSE, wrapped_editor, wrapped_baton,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(reporter->set_path(reporter_baton, "", location->rev, depth,
+ TRUE /* incomplete */,
+ NULL, scratch_pool));
+
+ SVN_ERR(reporter->finish_report(reporter_baton, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__copy_foreign(const char *url,
+ const char *dst_abspath,
+ svn_opt_revision_t *peg_revision,
+ svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t make_parents,
+ svn_boolean_t already_locked,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_ra_session_t *ra_session;
+ svn_client__pathrev_t *loc;
+ svn_node_kind_t kind;
+ svn_node_kind_t wc_kind;
+ const char *dir_abspath;
+
+ SVN_ERR_ASSERT(svn_path_is_url(url));
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(dst_abspath));
+
+ /* Do we need to validate/update revisions? */
+
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &loc,
+ url, NULL,
+ peg_revision,
+ revision, ctx,
+ scratch_pool));
+
+ SVN_ERR(svn_ra_check_path(ra_session, "", loc->rev, &kind, scratch_pool));
+
+ if (kind != svn_node_file && kind != svn_node_dir)
+ return svn_error_createf(
+ SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a valid location inside a repository"),
+ url);
+
+ SVN_ERR(svn_wc_read_kind2(&wc_kind, ctx->wc_ctx, dst_abspath, FALSE, TRUE,
+ scratch_pool));
+
+ if (wc_kind != svn_node_none)
+ {
+ return svn_error_createf(
+ SVN_ERR_ENTRY_EXISTS, NULL,
+ _("'%s' is already under version control"),
+ svn_dirent_local_style(dst_abspath, scratch_pool));
+ }
+
+ dir_abspath = svn_dirent_dirname(dst_abspath, scratch_pool);
+ SVN_ERR(svn_wc_read_kind2(&wc_kind, ctx->wc_ctx, dir_abspath,
+ FALSE, FALSE, scratch_pool));
+
+ if (wc_kind == svn_node_none)
+ {
+ if (make_parents)
+ SVN_ERR(svn_client__make_local_parents(dir_abspath, make_parents, ctx,
+ scratch_pool));
+
+ SVN_ERR(svn_wc_read_kind2(&wc_kind, ctx->wc_ctx, dir_abspath,
+ FALSE, FALSE, scratch_pool));
+ }
+
+ if (wc_kind != svn_node_dir)
+ return svn_error_createf(
+ SVN_ERR_ENTRY_NOT_FOUND, NULL,
+ _("Can't add '%s', because no parent directory is found"),
+ svn_dirent_local_style(dst_abspath, scratch_pool));
+
+
+ if (kind == svn_node_file)
+ {
+ svn_stream_t *target;
+ apr_hash_t *props;
+ apr_hash_index_t *hi;
+ SVN_ERR(svn_stream_open_writable(&target, dst_abspath, scratch_pool,
+ scratch_pool));
+
+ SVN_ERR(svn_ra_get_file(ra_session, "", loc->rev, target, NULL, &props,
+ scratch_pool));
+
+ if (props != NULL)
+ for (hi = apr_hash_first(scratch_pool, props); hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *name = svn__apr_hash_index_key(hi);
+
+ if (svn_property_kind2(name) != svn_prop_regular_kind
+ || ! strcmp(name, SVN_PROP_MERGEINFO))
+ {
+ /* We can't handle DAV, ENTRY and merge specific props here */
+ svn_hash_sets(props, name, NULL);
+ }
+ }
+
+ if (!already_locked)
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ svn_wc_add_from_disk2(ctx->wc_ctx, dst_abspath, props,
+ ctx->notify_func2, ctx->notify_baton2,
+ scratch_pool),
+ ctx->wc_ctx, dir_abspath, FALSE, scratch_pool);
+ else
+ SVN_ERR(svn_wc_add_from_disk2(ctx->wc_ctx, dst_abspath, props,
+ ctx->notify_func2, ctx->notify_baton2,
+ scratch_pool));
+ }
+ else
+ {
+ if (!already_locked)
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ copy_foreign_dir(ra_session, loc,
+ ctx->wc_ctx, dst_abspath,
+ depth,
+ ctx->notify_func2, ctx->notify_baton2,
+ ctx->cancel_func, ctx->cancel_baton,
+ scratch_pool),
+ ctx->wc_ctx, dir_abspath, FALSE, scratch_pool);
+ else
+ SVN_ERR(copy_foreign_dir(ra_session, loc,
+ ctx->wc_ctx, dst_abspath,
+ depth,
+ ctx->notify_func2, ctx->notify_baton2,
+ ctx->cancel_func, ctx->cancel_baton,
+ scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/ctx.c b/subversion/libsvn_client/ctx.c
new file mode 100644
index 0000000..185b91e
--- /dev/null
+++ b/subversion/libsvn_client/ctx.c
@@ -0,0 +1,112 @@
+/*
+ * ctx.c: initialization function for client context
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <apr_pools.h>
+#include "svn_hash.h"
+#include "svn_client.h"
+#include "svn_error.h"
+
+#include "private/svn_wc_private.h"
+
+
+/*** Code. ***/
+
+/* Call the notify_func of the context provided by BATON, if non-NULL. */
+static void
+call_notify_func(void *baton, const svn_wc_notify_t *n, apr_pool_t *pool)
+{
+ const svn_client_ctx_t *ctx = baton;
+
+ if (ctx->notify_func)
+ ctx->notify_func(ctx->notify_baton, n->path, n->action, n->kind,
+ n->mime_type, n->content_state, n->prop_state,
+ n->revision);
+}
+
+static svn_error_t *
+call_conflict_func(svn_wc_conflict_result_t **result,
+ const svn_wc_conflict_description2_t *conflict,
+ void *baton,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_client_ctx_t *ctx = baton;
+
+ if (ctx->conflict_func)
+ {
+ const svn_wc_conflict_description_t *cd;
+
+ cd = svn_wc__cd2_to_cd(conflict, scratch_pool);
+ SVN_ERR(ctx->conflict_func(result, cd, ctx->conflict_baton,
+ result_pool));
+ }
+ else
+ {
+ /* We have to set a result; so we postpone */
+ *result = svn_wc_create_conflict_result(svn_wc_conflict_choose_postpone,
+ NULL, result_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_create_context2(svn_client_ctx_t **ctx,
+ apr_hash_t *cfg_hash,
+ apr_pool_t *pool)
+{
+ svn_config_t *cfg_config;
+
+ *ctx = apr_pcalloc(pool, sizeof(svn_client_ctx_t));
+
+ (*ctx)->notify_func2 = call_notify_func;
+ (*ctx)->notify_baton2 = *ctx;
+
+ (*ctx)->conflict_func2 = call_conflict_func;
+ (*ctx)->conflict_baton2 = *ctx;
+
+ (*ctx)->config = cfg_hash;
+
+ if (cfg_hash)
+ cfg_config = svn_hash_gets(cfg_hash, SVN_CONFIG_CATEGORY_CONFIG);
+ else
+ cfg_config = NULL;
+
+ SVN_ERR(svn_wc_context_create(&(*ctx)->wc_ctx, cfg_config, pool,
+ pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_create_context(svn_client_ctx_t **ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_create_context2(ctx, NULL, pool);
+}
diff --git a/subversion/libsvn_client/delete.c b/subversion/libsvn_client/delete.c
new file mode 100644
index 0000000..2f4ee66
--- /dev/null
+++ b/subversion/libsvn_client/delete.c
@@ -0,0 +1,595 @@
+/*
+ * delete.c: wrappers around wc delete functionality.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <apr_file_io.h>
+#include "svn_hash.h"
+#include "svn_types.h"
+#include "svn_pools.h"
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "client.h"
+
+#include "private/svn_client_private.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_ra_private.h"
+
+#include "svn_private_config.h"
+
+
+/*** Code. ***/
+
+/* Baton for find_undeletables */
+struct can_delete_baton_t
+{
+ const char *root_abspath;
+ svn_boolean_t target_missing;
+};
+
+/* An svn_wc_status_func4_t callback function for finding
+ status structures which are not safely deletable. */
+static svn_error_t *
+find_undeletables(void *baton,
+ const char *local_abspath,
+ const svn_wc_status3_t *status,
+ apr_pool_t *pool)
+{
+ if (status->node_status == svn_wc_status_missing)
+ {
+ struct can_delete_baton_t *cdt = baton;
+
+ if (strcmp(cdt->root_abspath, local_abspath) == 0)
+ cdt->target_missing = TRUE;
+ }
+
+ /* Check for error-ful states. */
+ if (status->node_status == svn_wc_status_obstructed)
+ return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
+ _("'%s' is in the way of the resource "
+ "actually under version control"),
+ svn_dirent_local_style(local_abspath, pool));
+ else if (! status->versioned)
+ return svn_error_createf(SVN_ERR_UNVERSIONED_RESOURCE, NULL,
+ _("'%s' is not under version control"),
+ svn_dirent_local_style(local_abspath, pool));
+ else if ((status->node_status == svn_wc_status_added
+ || status->node_status == svn_wc_status_replaced)
+ && status->text_status == svn_wc_status_normal
+ && (status->prop_status == svn_wc_status_normal
+ || status->prop_status == svn_wc_status_none))
+ {
+ /* Unmodified copy. Go ahead, remove it */
+ }
+ else if (status->node_status != svn_wc_status_normal
+ && status->node_status != svn_wc_status_deleted
+ && status->node_status != svn_wc_status_missing)
+ return svn_error_createf(SVN_ERR_CLIENT_MODIFIED, NULL,
+ _("'%s' has local modifications -- commit or "
+ "revert them first"),
+ svn_dirent_local_style(local_abspath, pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Check whether LOCAL_ABSPATH is an external and raise an error if it is.
+
+ A file external should not be deleted since the file external is
+ implemented as a switched file and it would delete the file the
+ file external is switched to, which is not the behavior the user
+ would probably want.
+
+ A directory external should not be deleted since it is the root
+ of a different working copy. */
+static svn_error_t *
+check_external(const char *local_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_node_kind_t external_kind;
+ const char *defining_abspath;
+
+ SVN_ERR(svn_wc__read_external_info(&external_kind, &defining_abspath, NULL,
+ NULL, NULL,
+ ctx->wc_ctx, local_abspath,
+ local_abspath, TRUE,
+ scratch_pool, scratch_pool));
+
+ if (external_kind != svn_node_none)
+ return svn_error_createf(SVN_ERR_WC_CANNOT_DELETE_FILE_EXTERNAL, NULL,
+ _("Cannot remove the external at '%s'; "
+ "please edit or delete the svn:externals "
+ "property on '%s'"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool),
+ svn_dirent_local_style(defining_abspath,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Verify that the path can be deleted without losing stuff,
+ i.e. ensure that there are no modified or unversioned resources
+ under PATH. This is similar to checking the output of the status
+ command. CTX is used for the client's config options. POOL is
+ used for all temporary allocations. */
+static svn_error_t *
+can_delete_node(svn_boolean_t *target_missing,
+ const char *local_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *ignores;
+ struct can_delete_baton_t cdt;
+
+ /* Use an infinite-depth status check to see if there's anything in
+ or under PATH which would make it unsafe for deletion. The
+ status callback function find_undeletables() makes the
+ determination, returning an error if it finds anything that shouldn't
+ be deleted. */
+
+ SVN_ERR(svn_wc_get_default_ignores(&ignores, ctx->config, scratch_pool));
+
+ cdt.root_abspath = local_abspath;
+ cdt.target_missing = FALSE;
+
+ SVN_ERR(svn_wc_walk_status(ctx->wc_ctx,
+ local_abspath,
+ svn_depth_infinity,
+ FALSE /* get_all */,
+ FALSE /* no_ignore */,
+ FALSE /* ignore_text_mod */,
+ ignores,
+ find_undeletables, &cdt,
+ ctx->cancel_func,
+ ctx->cancel_baton,
+ scratch_pool));
+
+ if (target_missing)
+ *target_missing = cdt.target_missing;
+
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+path_driver_cb_func(void **dir_baton,
+ void *parent_baton,
+ void *callback_baton,
+ const char *path,
+ apr_pool_t *pool)
+{
+ const svn_delta_editor_t *editor = callback_baton;
+ *dir_baton = NULL;
+ return editor->delete_entry(path, SVN_INVALID_REVNUM, parent_baton, pool);
+}
+
+static svn_error_t *
+single_repos_delete(svn_ra_session_t *ra_session,
+ const char *repos_root,
+ const apr_array_header_t *relpaths,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const svn_delta_editor_t *editor;
+ apr_hash_t *commit_revprops;
+ void *edit_baton;
+ const char *log_msg;
+ int i;
+ svn_error_t *err;
+
+ /* Create new commit items and add them to the array. */
+ if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx))
+ {
+ svn_client_commit_item3_t *item;
+ const char *tmp_file;
+ apr_array_header_t *commit_items
+ = apr_array_make(pool, relpaths->nelts, sizeof(item));
+
+ for (i = 0; i < relpaths->nelts; i++)
+ {
+ const char *relpath = APR_ARRAY_IDX(relpaths, i, const char *);
+
+ item = svn_client_commit_item3_create(pool);
+ item->url = svn_path_url_add_component2(repos_root, relpath, pool);
+ item->state_flags = SVN_CLIENT_COMMIT_ITEM_DELETE;
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
+ }
+ SVN_ERR(svn_client__get_log_msg(&log_msg, &tmp_file, commit_items,
+ ctx, pool));
+ if (! log_msg)
+ return SVN_NO_ERROR;
+ }
+ else
+ log_msg = "";
+
+ SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table,
+ log_msg, ctx, pool));
+
+ /* Fetch RA commit editor */
+ SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session,
+ svn_client__get_shim_callbacks(ctx->wc_ctx,
+ NULL, pool)));
+ SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton,
+ commit_revprops,
+ commit_callback,
+ commit_baton,
+ NULL, TRUE, /* No lock tokens */
+ pool));
+
+ /* Call the path-based editor driver. */
+ err = svn_delta_path_driver2(editor, edit_baton, relpaths, TRUE,
+ path_driver_cb_func, (void *)editor, pool);
+
+ if (err)
+ {
+ return svn_error_trace(
+ svn_error_compose_create(err,
+ editor->abort_edit(edit_baton, pool)));
+ }
+
+ /* Close the edit. */
+ return svn_error_trace(editor->close_edit(edit_baton, pool));
+}
+
+
+/* Structure for tracking remote delete targets associated with a
+ specific repository. */
+struct repos_deletables_t
+{
+ svn_ra_session_t *ra_session;
+ apr_array_header_t *target_uris;
+};
+
+
+static svn_error_t *
+delete_urls_multi_repos(const apr_array_header_t *uris,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_hash_t *deletables = apr_hash_make(pool);
+ apr_pool_t *iterpool;
+ apr_hash_index_t *hi;
+ int i;
+
+ /* Create a hash mapping repository root URLs -> repos_deletables_t *
+ structures. */
+ for (i = 0; i < uris->nelts; i++)
+ {
+ const char *uri = APR_ARRAY_IDX(uris, i, const char *);
+ struct repos_deletables_t *repos_deletables = NULL;
+ const char *repos_relpath;
+ svn_node_kind_t kind;
+
+ for (hi = apr_hash_first(pool, deletables); hi; hi = apr_hash_next(hi))
+ {
+ const char *repos_root = svn__apr_hash_index_key(hi);
+
+ repos_relpath = svn_uri_skip_ancestor(repos_root, uri, pool);
+ if (repos_relpath)
+ {
+ /* Great! We've found another URI underneath this
+ session. We'll pick out the related RA session for
+ use later, store the new target, and move on. */
+ repos_deletables = svn__apr_hash_index_val(hi);
+ APR_ARRAY_PUSH(repos_deletables->target_uris, const char *) =
+ apr_pstrdup(pool, uri);
+ break;
+ }
+ }
+
+ /* If we haven't created a repos_deletable structure for this
+ delete target, we need to do. That means opening up an RA
+ session and initializing its targets list. */
+ if (!repos_deletables)
+ {
+ svn_ra_session_t *ra_session = NULL;
+ const char *repos_root;
+ apr_array_header_t *target_uris;
+
+ /* Open an RA session to (ultimately) the root of the
+ repository in which URI is found. */
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, uri, NULL,
+ ctx, pool, pool));
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root, pool));
+ SVN_ERR(svn_ra_reparent(ra_session, repos_root, pool));
+ repos_relpath = svn_uri_skip_ancestor(repos_root, uri, pool);
+
+ /* Make a new relpaths list for this repository, and add
+ this URI's relpath to it. */
+ target_uris = apr_array_make(pool, 1, sizeof(const char *));
+ APR_ARRAY_PUSH(target_uris, const char *) = apr_pstrdup(pool, uri);
+
+ /* Build our repos_deletables_t item and stash it in the
+ hash. */
+ repos_deletables = apr_pcalloc(pool, sizeof(*repos_deletables));
+ repos_deletables->ra_session = ra_session;
+ repos_deletables->target_uris = target_uris;
+ svn_hash_sets(deletables, repos_root, repos_deletables);
+ }
+
+ /* If we get here, we should have been able to calculate a
+ repos_relpath for this URI. Let's make sure. (We return an
+ RA error code otherwise for 1.6 compatibility.) */
+ if (!repos_relpath || !*repos_relpath)
+ return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
+ "URL '%s' not within a repository", uri);
+
+ /* Now, test to see if the thing actually exists in HEAD. */
+ SVN_ERR(svn_ra_check_path(repos_deletables->ra_session, repos_relpath,
+ SVN_INVALID_REVNUM, &kind, pool));
+ if (kind == svn_node_none)
+ return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
+ "URL '%s' does not exist", uri);
+ }
+
+ /* Now we iterate over the DELETABLES hash, issuing a commit for
+ each repository with its associated collected targets. */
+ iterpool = svn_pool_create(pool);
+ for (hi = apr_hash_first(pool, deletables); hi; hi = apr_hash_next(hi))
+ {
+ const char *repos_root = svn__apr_hash_index_key(hi);
+ struct repos_deletables_t *repos_deletables = svn__apr_hash_index_val(hi);
+ const char *base_uri;
+ apr_array_header_t *target_relpaths;
+
+ svn_pool_clear(iterpool);
+
+ /* We want to anchor the commit on the longest common path
+ across the targets for this one repository. If, however, one
+ of our targets is that longest common path, we need instead
+ anchor the commit on that path's immediate parent. Because
+ we're asking svn_uri_condense_targets() to remove
+ redundancies, this situation should be detectable by their
+ being returned either a) only a single, empty-path, target
+ relpath, or b) no target relpaths at all. */
+ SVN_ERR(svn_uri_condense_targets(&base_uri, &target_relpaths,
+ repos_deletables->target_uris,
+ TRUE, iterpool, iterpool));
+ SVN_ERR_ASSERT(!svn_path_is_empty(base_uri));
+ if (target_relpaths->nelts == 0)
+ {
+ const char *target_relpath;
+
+ svn_uri_split(&base_uri, &target_relpath, base_uri, iterpool);
+ APR_ARRAY_PUSH(target_relpaths, const char *) = target_relpath;
+ }
+ else if ((target_relpaths->nelts == 1)
+ && (svn_path_is_empty(APR_ARRAY_IDX(target_relpaths, 0,
+ const char *))))
+ {
+ const char *target_relpath;
+
+ svn_uri_split(&base_uri, &target_relpath, base_uri, iterpool);
+ APR_ARRAY_IDX(target_relpaths, 0, const char *) = target_relpath;
+ }
+
+ SVN_ERR(svn_ra_reparent(repos_deletables->ra_session, base_uri, pool));
+ SVN_ERR(single_repos_delete(repos_deletables->ra_session, repos_root,
+ target_relpaths,
+ revprop_table, commit_callback,
+ commit_baton, ctx, iterpool));
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__wc_delete(const char *local_abspath,
+ svn_boolean_t force,
+ svn_boolean_t dry_run,
+ svn_boolean_t keep_local,
+ svn_wc_notify_func2_t notify_func,
+ void *notify_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_boolean_t target_missing = FALSE;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ SVN_ERR(check_external(local_abspath, ctx, pool));
+
+ if (!force && !keep_local)
+ /* Verify that there are no "awkward" files */
+ SVN_ERR(can_delete_node(&target_missing, local_abspath, ctx, pool));
+
+ if (!dry_run)
+ /* Mark the entry for commit deletion and perform wc deletion */
+ return svn_error_trace(svn_wc_delete4(ctx->wc_ctx, local_abspath,
+ keep_local || target_missing
+ /*keep_local */,
+ TRUE /* delete_unversioned */,
+ ctx->cancel_func, ctx->cancel_baton,
+ notify_func, notify_baton, pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__wc_delete_many(const apr_array_header_t *targets,
+ svn_boolean_t force,
+ svn_boolean_t dry_run,
+ svn_boolean_t keep_local,
+ svn_wc_notify_func2_t notify_func,
+ void *notify_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ int i;
+ svn_boolean_t has_non_missing = FALSE;
+
+ for (i = 0; i < targets->nelts; i++)
+ {
+ const char *local_abspath = APR_ARRAY_IDX(targets, i, const char *);
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ SVN_ERR(check_external(local_abspath, ctx, pool));
+
+ if (!force && !keep_local)
+ {
+ svn_boolean_t missing;
+ /* Verify that there are no "awkward" files */
+
+ SVN_ERR(can_delete_node(&missing, local_abspath, ctx, pool));
+
+ if (! missing)
+ has_non_missing = TRUE;
+ }
+ else
+ has_non_missing = TRUE;
+ }
+
+ if (!dry_run)
+ {
+ /* Mark the entry for commit deletion and perform wc deletion */
+
+ /* If none of the targets exists, pass keep local TRUE, to avoid
+ deleting case-different files. Detecting this in the generic case
+ from the delete code is expensive */
+ return svn_error_trace(svn_wc__delete_many(ctx->wc_ctx, targets,
+ keep_local || !has_non_missing,
+ TRUE /* delete_unversioned_target */,
+ ctx->cancel_func,
+ ctx->cancel_baton,
+ notify_func, notify_baton,
+ pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_delete4(const apr_array_header_t *paths,
+ svn_boolean_t force,
+ svn_boolean_t keep_local,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_boolean_t is_url;
+
+ if (! paths->nelts)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(svn_client__assert_homogeneous_target_type(paths));
+ is_url = svn_path_is_url(APR_ARRAY_IDX(paths, 0, const char *));
+
+ if (is_url)
+ {
+ SVN_ERR(delete_urls_multi_repos(paths, revprop_table, commit_callback,
+ commit_baton, ctx, pool));
+ }
+ else
+ {
+ const char *local_abspath;
+ apr_hash_t *wcroots;
+ apr_hash_index_t *hi;
+ int i;
+ int j;
+ apr_pool_t *iterpool;
+ svn_boolean_t is_new_target;
+
+ /* Build a map of wcroots and targets within them. */
+ wcroots = apr_hash_make(pool);
+ iterpool = svn_pool_create(pool);
+ for (i = 0; i < paths->nelts; i++)
+ {
+ const char *wcroot_abspath;
+ apr_array_header_t *targets;
+
+ svn_pool_clear(iterpool);
+
+ /* See if the user wants us to stop. */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath,
+ APR_ARRAY_IDX(paths, i,
+ const char *),
+ pool));
+ SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx,
+ local_abspath, pool, iterpool));
+ targets = svn_hash_gets(wcroots, wcroot_abspath);
+ if (targets == NULL)
+ {
+ targets = apr_array_make(pool, 1, sizeof(const char *));
+ svn_hash_sets(wcroots, wcroot_abspath, targets);
+ }
+
+ /* Make sure targets are unique. */
+ is_new_target = TRUE;
+ for (j = 0; j < targets->nelts; j++)
+ {
+ if (strcmp(APR_ARRAY_IDX(targets, j, const char *),
+ local_abspath) == 0)
+ {
+ is_new_target = FALSE;
+ break;
+ }
+ }
+
+ if (is_new_target)
+ APR_ARRAY_PUSH(targets, const char *) = local_abspath;
+ }
+
+ /* Delete the targets from each working copy in turn. */
+ for (hi = apr_hash_first(pool, wcroots); hi; hi = apr_hash_next(hi))
+ {
+ const char *root_abspath;
+ const apr_array_header_t *targets = svn__apr_hash_index_val(hi);
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_dirent_condense_targets(&root_abspath, NULL, targets,
+ FALSE, iterpool, iterpool));
+
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ svn_client__wc_delete_many(targets, force, FALSE, keep_local,
+ ctx->notify_func2, ctx->notify_baton2,
+ ctx, iterpool),
+ ctx->wc_ctx, root_abspath, TRUE /* lock_anchor */,
+ iterpool);
+ }
+ svn_pool_destroy(iterpool);
+ }
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/deprecated.c b/subversion/libsvn_client/deprecated.c
new file mode 100644
index 0000000..a67a69b
--- /dev/null
+++ b/subversion/libsvn_client/deprecated.c
@@ -0,0 +1,2966 @@
+/*
+ * deprecated.c: holding file for all deprecated APIs.
+ * "we can't lose 'em, but we can shun 'em!"
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+/* We define this here to remove any further warnings about the usage of
+ deprecated functions in this file. */
+#define SVN_DEPRECATED
+
+#include <string.h>
+#include "svn_client.h"
+#include "svn_path.h"
+#include "svn_compat.h"
+#include "svn_hash.h"
+#include "svn_props.h"
+#include "svn_utf.h"
+#include "svn_string.h"
+#include "svn_pools.h"
+
+#include "client.h"
+#include "mergeinfo.h"
+
+#include "private/svn_opt_private.h"
+#include "private/svn_wc_private.h"
+#include "svn_private_config.h"
+
+
+
+
+/*** Code. ***/
+
+
+/* Baton for capture_commit_info() */
+struct capture_baton_t {
+ svn_commit_info_t **info;
+ apr_pool_t *pool;
+};
+
+
+/* Callback which implements svn_commit_callback2_t for use with some
+ backward compat functions. */
+static svn_error_t *
+capture_commit_info(const svn_commit_info_t *commit_info,
+ void *baton,
+ apr_pool_t *pool)
+{
+ struct capture_baton_t *cb = baton;
+
+ *(cb->info) = svn_commit_info_dup(commit_info, cb->pool);
+
+ return SVN_NO_ERROR;
+}
+
+
+/*** From add.c ***/
+svn_error_t *
+svn_client_add4(const char *path,
+ svn_depth_t depth,
+ svn_boolean_t force,
+ svn_boolean_t no_ignore,
+ svn_boolean_t add_parents,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_add5(path, depth, force, no_ignore, FALSE, add_parents,
+ ctx, pool);
+}
+
+svn_error_t *
+svn_client_add3(const char *path,
+ svn_boolean_t recursive,
+ svn_boolean_t force,
+ svn_boolean_t no_ignore,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_add4(path, SVN_DEPTH_INFINITY_OR_EMPTY(recursive),
+ force, no_ignore, FALSE, ctx,
+ pool);
+}
+
+svn_error_t *
+svn_client_add2(const char *path,
+ svn_boolean_t recursive,
+ svn_boolean_t force,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_add3(path, recursive, force, FALSE, ctx, pool);
+}
+
+svn_error_t *
+svn_client_add(const char *path,
+ svn_boolean_t recursive,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_add3(path, recursive, FALSE, FALSE, ctx, pool);
+}
+
+svn_error_t *
+svn_client_mkdir3(svn_commit_info_t **commit_info_p,
+ const apr_array_header_t *paths,
+ svn_boolean_t make_parents,
+ const apr_hash_t *revprop_table,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct capture_baton_t cb;
+
+ cb.info = commit_info_p;
+ cb.pool = pool;
+
+ return svn_client_mkdir4(paths, make_parents, revprop_table,
+ capture_commit_info, &cb, ctx, pool);
+}
+
+svn_error_t *
+svn_client_mkdir2(svn_commit_info_t **commit_info_p,
+ const apr_array_header_t *paths,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_mkdir3(commit_info_p, paths, FALSE, NULL, ctx, pool);
+}
+
+
+svn_error_t *
+svn_client_mkdir(svn_client_commit_info_t **commit_info_p,
+ const apr_array_header_t *paths,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_commit_info_t *commit_info = NULL;
+ svn_error_t *err;
+
+ err = svn_client_mkdir2(&commit_info, paths, ctx, pool);
+ /* These structs have the same layout for the common fields. */
+ *commit_info_p = (svn_client_commit_info_t *) commit_info;
+ return svn_error_trace(err);
+}
+
+/*** From blame.c ***/
+
+struct blame_receiver_wrapper_baton2 {
+ void *baton;
+ svn_client_blame_receiver2_t receiver;
+};
+
+static svn_error_t *
+blame_wrapper_receiver2(void *baton,
+ svn_revnum_t start_revnum,
+ svn_revnum_t end_revnum,
+ apr_int64_t line_no,
+ svn_revnum_t revision,
+ apr_hash_t *rev_props,
+ svn_revnum_t merged_revision,
+ apr_hash_t *merged_rev_props,
+ const char *merged_path,
+ const char *line,
+ svn_boolean_t local_change,
+ apr_pool_t *pool)
+{
+ struct blame_receiver_wrapper_baton2 *brwb = baton;
+ const char *author = NULL;
+ const char *date = NULL;
+ const char *merged_author = NULL;
+ const char *merged_date = NULL;
+
+ if (rev_props != NULL)
+ {
+ author = svn_prop_get_value(rev_props, SVN_PROP_REVISION_AUTHOR);
+ date = svn_prop_get_value(rev_props, SVN_PROP_REVISION_DATE);
+ }
+ if (merged_rev_props != NULL)
+ {
+ merged_author = svn_prop_get_value(merged_rev_props,
+ SVN_PROP_REVISION_AUTHOR);
+ merged_date = svn_prop_get_value(merged_rev_props,
+ SVN_PROP_REVISION_DATE);
+ }
+
+ if (brwb->receiver)
+ return brwb->receiver(brwb->baton, line_no, revision, author, date,
+ merged_revision, merged_author, merged_date,
+ merged_path, line, pool);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_blame4(const char *target,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ const svn_diff_file_options_t *diff_options,
+ svn_boolean_t ignore_mime_type,
+ svn_boolean_t include_merged_revisions,
+ svn_client_blame_receiver2_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct blame_receiver_wrapper_baton2 baton;
+
+ baton.receiver = receiver;
+ baton.baton = receiver_baton;
+
+ return svn_client_blame5(target, peg_revision, start, end, diff_options,
+ ignore_mime_type, include_merged_revisions,
+ blame_wrapper_receiver2, &baton, ctx, pool);
+}
+
+
+/* Baton for use with wrap_blame_receiver */
+struct blame_receiver_wrapper_baton {
+ void *baton;
+ svn_client_blame_receiver_t receiver;
+};
+
+/* This implements svn_client_blame_receiver2_t */
+static svn_error_t *
+blame_wrapper_receiver(void *baton,
+ apr_int64_t line_no,
+ svn_revnum_t revision,
+ const char *author,
+ const char *date,
+ svn_revnum_t merged_revision,
+ const char *merged_author,
+ const char *merged_date,
+ const char *merged_path,
+ const char *line,
+ apr_pool_t *pool)
+{
+ struct blame_receiver_wrapper_baton *brwb = baton;
+
+ if (brwb->receiver)
+ return brwb->receiver(brwb->baton,
+ line_no, revision, author, date, line, pool);
+
+ return SVN_NO_ERROR;
+}
+
+static void
+wrap_blame_receiver(svn_client_blame_receiver2_t *receiver2,
+ void **receiver2_baton,
+ svn_client_blame_receiver_t receiver,
+ void *receiver_baton,
+ apr_pool_t *pool)
+{
+ struct blame_receiver_wrapper_baton *brwb = apr_palloc(pool, sizeof(*brwb));
+
+ /* Set the user provided old format callback in the baton. */
+ brwb->baton = receiver_baton;
+ brwb->receiver = receiver;
+
+ *receiver2_baton = brwb;
+ *receiver2 = blame_wrapper_receiver;
+}
+
+svn_error_t *
+svn_client_blame3(const char *target,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ const svn_diff_file_options_t *diff_options,
+ svn_boolean_t ignore_mime_type,
+ svn_client_blame_receiver_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_client_blame_receiver2_t receiver2;
+ void *receiver2_baton;
+
+ wrap_blame_receiver(&receiver2, &receiver2_baton, receiver, receiver_baton,
+ pool);
+
+ return svn_client_blame4(target, peg_revision, start, end, diff_options,
+ ignore_mime_type, FALSE, receiver2, receiver2_baton,
+ ctx, pool);
+}
+
+/* svn_client_blame3 guarantees 'no EOL chars' as part of the receiver
+ LINE argument. Older versions depend on the fact that if a CR is
+ required, that CR is already part of the LINE data.
+
+ Because of this difference, we need to trap old receivers and append
+ a CR to LINE before passing it on to the actual receiver on platforms
+ which want CRLF line termination.
+
+*/
+
+struct wrapped_receiver_baton_s
+{
+ svn_client_blame_receiver_t orig_receiver;
+ void *orig_baton;
+};
+
+static svn_error_t *
+wrapped_receiver(void *baton,
+ apr_int64_t line_no,
+ svn_revnum_t revision,
+ const char *author,
+ const char *date,
+ const char *line,
+ apr_pool_t *pool)
+{
+ struct wrapped_receiver_baton_s *b = baton;
+ svn_stringbuf_t *expanded_line = svn_stringbuf_create(line, pool);
+
+ svn_stringbuf_appendbyte(expanded_line, '\r');
+
+ return b->orig_receiver(b->orig_baton, line_no, revision, author,
+ date, expanded_line->data, pool);
+}
+
+static void
+wrap_pre_blame3_receiver(svn_client_blame_receiver_t *receiver,
+ void **receiver_baton,
+ apr_pool_t *pool)
+{
+ if (sizeof(APR_EOL_STR) == 3)
+ {
+ struct wrapped_receiver_baton_s *b = apr_palloc(pool,sizeof(*b));
+
+ b->orig_receiver = *receiver;
+ b->orig_baton = *receiver_baton;
+
+ *receiver_baton = b;
+ *receiver = wrapped_receiver;
+ }
+}
+
+svn_error_t *
+svn_client_blame2(const char *target,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ svn_client_blame_receiver_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ wrap_pre_blame3_receiver(&receiver, &receiver_baton, pool);
+ return svn_client_blame3(target, peg_revision, start, end,
+ svn_diff_file_options_create(pool), FALSE,
+ receiver, receiver_baton, ctx, pool);
+}
+svn_error_t *
+svn_client_blame(const char *target,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ svn_client_blame_receiver_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ wrap_pre_blame3_receiver(&receiver, &receiver_baton, pool);
+ return svn_client_blame2(target, end, start, end,
+ receiver, receiver_baton, ctx, pool);
+}
+
+/*** From cmdline.c ***/
+svn_error_t *
+svn_client_args_to_target_array(apr_array_header_t **targets_p,
+ apr_getopt_t *os,
+ const apr_array_header_t *known_targets,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_args_to_target_array2(targets_p, os, known_targets, ctx,
+ FALSE, pool);
+}
+
+/*** From commit.c ***/
+svn_error_t *
+svn_client_import4(const char *path,
+ const char *url,
+ svn_depth_t depth,
+ svn_boolean_t no_ignore,
+ svn_boolean_t ignore_unknown_node_types,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_error_trace(svn_client_import5(path, url, depth, no_ignore,
+ FALSE, ignore_unknown_node_types,
+ revprop_table,
+ NULL, NULL,
+ commit_callback, commit_baton,
+ ctx, pool));
+}
+
+
+svn_error_t *
+svn_client_import3(svn_commit_info_t **commit_info_p,
+ const char *path,
+ const char *url,
+ svn_depth_t depth,
+ svn_boolean_t no_ignore,
+ svn_boolean_t ignore_unknown_node_types,
+ const apr_hash_t *revprop_table,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct capture_baton_t cb;
+
+ cb.info = commit_info_p;
+ cb.pool = pool;
+
+ return svn_client_import4(path, url, depth, no_ignore,
+ ignore_unknown_node_types, revprop_table,
+ capture_commit_info, &cb, ctx, pool);
+}
+
+svn_error_t *
+svn_client_import2(svn_commit_info_t **commit_info_p,
+ const char *path,
+ const char *url,
+ svn_boolean_t nonrecursive,
+ svn_boolean_t no_ignore,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_import3(commit_info_p,
+ path, url,
+ SVN_DEPTH_INFINITY_OR_FILES(! nonrecursive),
+ no_ignore, FALSE, NULL, ctx, pool);
+}
+
+svn_error_t *
+svn_client_import(svn_client_commit_info_t **commit_info_p,
+ const char *path,
+ const char *url,
+ svn_boolean_t nonrecursive,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_commit_info_t *commit_info = NULL;
+ svn_error_t *err;
+
+ err = svn_client_import2(&commit_info,
+ path, url, nonrecursive,
+ FALSE, ctx, pool);
+ /* These structs have the same layout for the common fields. */
+ *commit_info_p = (svn_client_commit_info_t *) commit_info;
+ return svn_error_trace(err);
+}
+
+
+/* Wrapper notify_func2 function and baton for downgrading
+ svn_wc_notify_commit_copied and svn_wc_notify_commit_copied_replaced
+ to svn_wc_notify_commit_added and svn_wc_notify_commit_replaced,
+ respectively. */
+struct downgrade_commit_copied_notify_baton
+{
+ svn_wc_notify_func2_t orig_notify_func2;
+ void *orig_notify_baton2;
+};
+
+static void
+downgrade_commit_copied_notify_func(void *baton,
+ const svn_wc_notify_t *notify,
+ apr_pool_t *pool)
+{
+ struct downgrade_commit_copied_notify_baton *b = baton;
+
+ if (notify->action == svn_wc_notify_commit_copied)
+ {
+ svn_wc_notify_t *my_notify = svn_wc_dup_notify(notify, pool);
+ my_notify->action = svn_wc_notify_commit_added;
+ notify = my_notify;
+ }
+ else if (notify->action == svn_wc_notify_commit_copied_replaced)
+ {
+ svn_wc_notify_t *my_notify = svn_wc_dup_notify(notify, pool);
+ my_notify->action = svn_wc_notify_commit_replaced;
+ notify = my_notify;
+ }
+
+ /* Call the wrapped notification system (if any) with MY_NOTIFY,
+ which is either the original NOTIFY object, or a tweaked deep
+ copy thereof. */
+ if (b->orig_notify_func2)
+ b->orig_notify_func2(b->orig_notify_baton2, notify, pool);
+}
+
+svn_error_t *
+svn_client_commit5(const apr_array_header_t *targets,
+ svn_depth_t depth,
+ svn_boolean_t keep_locks,
+ svn_boolean_t keep_changelists,
+ svn_boolean_t commit_as_operations,
+ const apr_array_header_t *changelists,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_commit6(targets, depth, keep_locks, keep_changelists,
+ commit_as_operations,
+ FALSE, /* include_file_externals */
+ FALSE, /* include_dir_externals */
+ changelists, revprop_table, commit_callback,
+ commit_baton, ctx, pool);
+}
+
+svn_error_t *
+svn_client_commit4(svn_commit_info_t **commit_info_p,
+ const apr_array_header_t *targets,
+ svn_depth_t depth,
+ svn_boolean_t keep_locks,
+ svn_boolean_t keep_changelists,
+ const apr_array_header_t *changelists,
+ const apr_hash_t *revprop_table,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct capture_baton_t cb;
+ struct downgrade_commit_copied_notify_baton notify_baton;
+ svn_error_t *err;
+
+ notify_baton.orig_notify_func2 = ctx->notify_func2;
+ notify_baton.orig_notify_baton2 = ctx->notify_baton2;
+
+ *commit_info_p = NULL;
+ cb.info = commit_info_p;
+ cb.pool = pool;
+
+ /* Swap out the notification system (if any) with a thin filtering
+ wrapper. */
+ if (ctx->notify_func2)
+ {
+ ctx->notify_func2 = downgrade_commit_copied_notify_func;
+ ctx->notify_baton2 = &notify_baton;
+ }
+
+ err = svn_client_commit5(targets, depth, keep_locks, keep_changelists, FALSE,
+ changelists, revprop_table,
+ capture_commit_info, &cb, ctx, pool);
+
+ /* Ensure that the original notification system is in place. */
+ ctx->notify_func2 = notify_baton.orig_notify_func2;
+ ctx->notify_baton2 = notify_baton.orig_notify_baton2;
+
+ SVN_ERR(err);
+
+ if (! *commit_info_p)
+ *commit_info_p = svn_create_commit_info(pool);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_commit3(svn_commit_info_t **commit_info_p,
+ const apr_array_header_t *targets,
+ svn_boolean_t recurse,
+ svn_boolean_t keep_locks,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_depth_t depth = SVN_DEPTH_INFINITY_OR_EMPTY(recurse);
+
+ return svn_client_commit4(commit_info_p, targets, depth, keep_locks,
+ FALSE, NULL, NULL, ctx, pool);
+}
+
+svn_error_t *
+svn_client_commit2(svn_client_commit_info_t **commit_info_p,
+ const apr_array_header_t *targets,
+ svn_boolean_t recurse,
+ svn_boolean_t keep_locks,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_commit_info_t *commit_info = NULL;
+ svn_error_t *err;
+
+ err = svn_client_commit3(&commit_info, targets, recurse, keep_locks,
+ ctx, pool);
+ /* These structs have the same layout for the common fields. */
+ *commit_info_p = (svn_client_commit_info_t *) commit_info;
+ return svn_error_trace(err);
+}
+
+svn_error_t *
+svn_client_commit(svn_client_commit_info_t **commit_info_p,
+ const apr_array_header_t *targets,
+ svn_boolean_t nonrecursive,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_commit2(commit_info_p, targets,
+ ! nonrecursive,
+ TRUE,
+ ctx, pool);
+}
+
+/*** From copy.c ***/
+svn_error_t *
+svn_client_copy5(svn_commit_info_t **commit_info_p,
+ const apr_array_header_t *sources,
+ const char *dst_path,
+ svn_boolean_t copy_as_child,
+ svn_boolean_t make_parents,
+ svn_boolean_t ignore_externals,
+ const apr_hash_t *revprop_table,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct capture_baton_t cb;
+
+ cb.info = commit_info_p;
+ cb.pool = pool;
+
+ return svn_client_copy6(sources, dst_path, copy_as_child, make_parents,
+ ignore_externals, revprop_table,
+ capture_commit_info, &cb, ctx, pool);
+}
+
+svn_error_t *
+svn_client_copy4(svn_commit_info_t **commit_info_p,
+ const apr_array_header_t *sources,
+ const char *dst_path,
+ svn_boolean_t copy_as_child,
+ svn_boolean_t make_parents,
+ const apr_hash_t *revprop_table,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_copy5(commit_info_p, sources, dst_path, copy_as_child,
+ make_parents, FALSE, revprop_table, ctx, pool);
+}
+
+svn_error_t *
+svn_client_copy3(svn_commit_info_t **commit_info_p,
+ const char *src_path,
+ const svn_opt_revision_t *src_revision,
+ const char *dst_path,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *sources = apr_array_make(pool, 1,
+ sizeof(const svn_client_copy_source_t *));
+ svn_client_copy_source_t copy_source;
+
+ copy_source.path = src_path;
+ copy_source.revision = src_revision;
+ copy_source.peg_revision = src_revision;
+
+ APR_ARRAY_PUSH(sources, const svn_client_copy_source_t *) = &copy_source;
+
+ return svn_client_copy4(commit_info_p, sources, dst_path, FALSE, FALSE,
+ NULL, ctx, pool);
+}
+
+svn_error_t *
+svn_client_copy2(svn_commit_info_t **commit_info_p,
+ const char *src_path,
+ const svn_opt_revision_t *src_revision,
+ const char *dst_path,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+
+ err = svn_client_copy3(commit_info_p, src_path, src_revision,
+ dst_path, ctx, pool);
+
+ /* If the target exists, try to copy the source as a child of the target.
+ This will obviously fail if target is not a directory, but that's exactly
+ what we want. */
+ if (err && (err->apr_err == SVN_ERR_ENTRY_EXISTS
+ || err->apr_err == SVN_ERR_FS_ALREADY_EXISTS))
+ {
+ const char *src_basename = svn_path_basename(src_path, pool);
+
+ svn_error_clear(err);
+
+ return svn_client_copy3(commit_info_p, src_path, src_revision,
+ svn_path_join(dst_path, src_basename, pool),
+ ctx, pool);
+ }
+
+ return svn_error_trace(err);
+}
+
+svn_error_t *
+svn_client_copy(svn_client_commit_info_t **commit_info_p,
+ const char *src_path,
+ const svn_opt_revision_t *src_revision,
+ const char *dst_path,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_commit_info_t *commit_info = NULL;
+ svn_error_t *err;
+
+ err = svn_client_copy2(&commit_info, src_path, src_revision, dst_path,
+ ctx, pool);
+ /* These structs have the same layout for the common fields. */
+ *commit_info_p = (svn_client_commit_info_t *) commit_info;
+ return svn_error_trace(err);
+}
+
+svn_error_t *
+svn_client_move6(const apr_array_header_t *src_paths,
+ const char *dst_path,
+ svn_boolean_t move_as_child,
+ svn_boolean_t make_parents,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_error_trace(svn_client_move7(src_paths, dst_path,
+ move_as_child, make_parents,
+ TRUE /* allow_mixed_revisions */,
+ FALSE /* metadata_only */,
+ revprop_table,
+ commit_callback, commit_baton,
+ ctx, pool));
+}
+
+svn_error_t *
+svn_client_move5(svn_commit_info_t **commit_info_p,
+ const apr_array_header_t *src_paths,
+ const char *dst_path,
+ svn_boolean_t force,
+ svn_boolean_t move_as_child,
+ svn_boolean_t make_parents,
+ const apr_hash_t *revprop_table,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct capture_baton_t cb;
+
+ cb.info = commit_info_p;
+ cb.pool = pool;
+
+ return svn_client_move6(src_paths, dst_path, move_as_child,
+ make_parents, revprop_table,
+ capture_commit_info, &cb, ctx, pool);
+}
+
+svn_error_t *
+svn_client_move4(svn_commit_info_t **commit_info_p,
+ const char *src_path,
+ const char *dst_path,
+ svn_boolean_t force,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *src_paths =
+ apr_array_make(pool, 1, sizeof(const char *));
+ APR_ARRAY_PUSH(src_paths, const char *) = src_path;
+
+
+ return svn_client_move5(commit_info_p, src_paths, dst_path, force, FALSE,
+ FALSE, NULL, ctx, pool);
+}
+
+svn_error_t *
+svn_client_move3(svn_commit_info_t **commit_info_p,
+ const char *src_path,
+ const char *dst_path,
+ svn_boolean_t force,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+
+ err = svn_client_move4(commit_info_p, src_path, dst_path, force, ctx, pool);
+
+ /* If the target exists, try to move the source as a child of the target.
+ This will obviously fail if target is not a directory, but that's exactly
+ what we want. */
+ if (err && (err->apr_err == SVN_ERR_ENTRY_EXISTS
+ || err->apr_err == SVN_ERR_FS_ALREADY_EXISTS))
+ {
+ const char *src_basename = svn_path_basename(src_path, pool);
+
+ svn_error_clear(err);
+
+ return svn_client_move4(commit_info_p, src_path,
+ svn_path_join(dst_path, src_basename, pool),
+ force, ctx, pool);
+ }
+
+ return svn_error_trace(err);
+}
+
+svn_error_t *
+svn_client_move2(svn_client_commit_info_t **commit_info_p,
+ const char *src_path,
+ const char *dst_path,
+ svn_boolean_t force,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_commit_info_t *commit_info = NULL;
+ svn_error_t *err;
+
+ err = svn_client_move3(&commit_info, src_path, dst_path, force, ctx, pool);
+ /* These structs have the same layout for the common fields. */
+ *commit_info_p = (svn_client_commit_info_t *) commit_info;
+ return svn_error_trace(err);
+}
+
+
+svn_error_t *
+svn_client_move(svn_client_commit_info_t **commit_info_p,
+ const char *src_path,
+ const svn_opt_revision_t *src_revision,
+ const char *dst_path,
+ svn_boolean_t force,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ /* It doesn't make sense to specify revisions in a move. */
+
+ /* ### todo: this check could fail wrongly. For example,
+ someone could pass in an svn_opt_revision_number that just
+ happens to be the HEAD. It's fair enough to punt then, IMHO,
+ and just demand that the user not specify a revision at all;
+ beats mucking up this function with RA calls and such. */
+ if (src_revision->kind != svn_opt_revision_unspecified
+ && src_revision->kind != svn_opt_revision_head)
+ {
+ return svn_error_create
+ (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Cannot specify revisions (except HEAD) with move operations"));
+ }
+
+ return svn_client_move2(commit_info_p, src_path, dst_path, force, ctx, pool);
+}
+
+/*** From delete.c ***/
+svn_error_t *
+svn_client_delete3(svn_commit_info_t **commit_info_p,
+ const apr_array_header_t *paths,
+ svn_boolean_t force,
+ svn_boolean_t keep_local,
+ const apr_hash_t *revprop_table,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct capture_baton_t cb;
+
+ cb.info = commit_info_p;
+ cb.pool = pool;
+
+ return svn_client_delete4(paths, force, keep_local, revprop_table,
+ capture_commit_info, &cb, ctx, pool);
+}
+
+svn_error_t *
+svn_client_delete2(svn_commit_info_t **commit_info_p,
+ const apr_array_header_t *paths,
+ svn_boolean_t force,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_delete3(commit_info_p, paths, force, FALSE, NULL,
+ ctx, pool);
+}
+
+svn_error_t *
+svn_client_delete(svn_client_commit_info_t **commit_info_p,
+ const apr_array_header_t *paths,
+ svn_boolean_t force,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_commit_info_t *commit_info = NULL;
+ svn_error_t *err = NULL;
+
+ err = svn_client_delete2(&commit_info, paths, force, ctx, pool);
+ /* These structs have the same layout for the common fields. */
+ *commit_info_p = (svn_client_commit_info_t *) commit_info;
+ return svn_error_trace(err);
+}
+
+/*** From diff.c ***/
+
+svn_error_t *
+svn_client_diff5(const apr_array_header_t *diff_options,
+ const char *path1,
+ const svn_opt_revision_t *revision1,
+ const char *path2,
+ const svn_opt_revision_t *revision2,
+ const char *relative_to_dir,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_deleted,
+ svn_boolean_t show_copies_as_adds,
+ svn_boolean_t ignore_content_type,
+ svn_boolean_t use_git_diff_format,
+ const char *header_encoding,
+ apr_file_t *outfile,
+ apr_file_t *errfile,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_stream_t *outstream = svn_stream_from_aprfile2(outfile, TRUE, pool);
+ svn_stream_t *errstream = svn_stream_from_aprfile2(errfile, TRUE, pool);
+
+ return svn_client_diff6(diff_options, path1, revision1, path2,
+ revision2, relative_to_dir, depth,
+ ignore_ancestry, FALSE /* no_diff_added */,
+ no_diff_deleted, show_copies_as_adds,
+ ignore_content_type, FALSE /* ignore_properties */,
+ FALSE /* properties_only */, use_git_diff_format,
+ header_encoding,
+ outstream, errstream, changelists, ctx, pool);
+}
+
+svn_error_t *
+svn_client_diff4(const apr_array_header_t *options,
+ const char *path1,
+ const svn_opt_revision_t *revision1,
+ const char *path2,
+ const svn_opt_revision_t *revision2,
+ const char *relative_to_dir,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_deleted,
+ svn_boolean_t ignore_content_type,
+ const char *header_encoding,
+ apr_file_t *outfile,
+ apr_file_t *errfile,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_diff5(options, path1, revision1, path2,
+ revision2, relative_to_dir, depth,
+ ignore_ancestry, no_diff_deleted, FALSE,
+ ignore_content_type, FALSE, header_encoding,
+ outfile, errfile, changelists, ctx, pool);
+}
+
+svn_error_t *
+svn_client_diff3(const apr_array_header_t *options,
+ const char *path1,
+ const svn_opt_revision_t *revision1,
+ const char *path2,
+ const svn_opt_revision_t *revision2,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_deleted,
+ svn_boolean_t ignore_content_type,
+ const char *header_encoding,
+ apr_file_t *outfile,
+ apr_file_t *errfile,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_diff4(options, path1, revision1, path2,
+ revision2, NULL,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse),
+ ignore_ancestry, no_diff_deleted,
+ ignore_content_type, header_encoding,
+ outfile, errfile, NULL, ctx, pool);
+}
+
+svn_error_t *
+svn_client_diff2(const apr_array_header_t *options,
+ const char *path1,
+ const svn_opt_revision_t *revision1,
+ const char *path2,
+ const svn_opt_revision_t *revision2,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_deleted,
+ svn_boolean_t ignore_content_type,
+ apr_file_t *outfile,
+ apr_file_t *errfile,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_diff3(options, path1, revision1, path2, revision2,
+ recurse, ignore_ancestry, no_diff_deleted,
+ ignore_content_type, SVN_APR_LOCALE_CHARSET,
+ outfile, errfile, ctx, pool);
+}
+
+svn_error_t *
+svn_client_diff(const apr_array_header_t *options,
+ const char *path1,
+ const svn_opt_revision_t *revision1,
+ const char *path2,
+ const svn_opt_revision_t *revision2,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_deleted,
+ apr_file_t *outfile,
+ apr_file_t *errfile,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_diff2(options, path1, revision1, path2, revision2,
+ recurse, ignore_ancestry, no_diff_deleted, FALSE,
+ outfile, errfile, ctx, pool);
+}
+
+svn_error_t *
+svn_client_diff_peg5(const apr_array_header_t *diff_options,
+ const char *path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start_revision,
+ const svn_opt_revision_t *end_revision,
+ const char *relative_to_dir,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_deleted,
+ svn_boolean_t show_copies_as_adds,
+ svn_boolean_t ignore_content_type,
+ svn_boolean_t use_git_diff_format,
+ const char *header_encoding,
+ apr_file_t *outfile,
+ apr_file_t *errfile,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_stream_t *outstream = svn_stream_from_aprfile2(outfile, TRUE, pool);
+ svn_stream_t *errstream = svn_stream_from_aprfile2(errfile, TRUE, pool);
+
+ return svn_client_diff_peg6(diff_options,
+ path,
+ peg_revision,
+ start_revision,
+ end_revision,
+ relative_to_dir,
+ depth,
+ ignore_ancestry,
+ FALSE /* no_diff_added */,
+ no_diff_deleted,
+ show_copies_as_adds,
+ ignore_content_type,
+ FALSE /* ignore_properties */,
+ FALSE /* properties_only */,
+ use_git_diff_format,
+ header_encoding,
+ outstream,
+ errstream,
+ changelists,
+ ctx,
+ pool);
+}
+
+svn_error_t *
+svn_client_diff_peg4(const apr_array_header_t *options,
+ const char *path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start_revision,
+ const svn_opt_revision_t *end_revision,
+ const char *relative_to_dir,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_deleted,
+ svn_boolean_t ignore_content_type,
+ const char *header_encoding,
+ apr_file_t *outfile,
+ apr_file_t *errfile,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_diff_peg5(options,
+ path,
+ peg_revision,
+ start_revision,
+ end_revision,
+ relative_to_dir,
+ depth,
+ ignore_ancestry,
+ no_diff_deleted,
+ FALSE,
+ ignore_content_type,
+ FALSE,
+ header_encoding,
+ outfile,
+ errfile,
+ changelists,
+ ctx,
+ pool);
+}
+
+svn_error_t *
+svn_client_diff_peg3(const apr_array_header_t *options,
+ const char *path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start_revision,
+ const svn_opt_revision_t *end_revision,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_deleted,
+ svn_boolean_t ignore_content_type,
+ const char *header_encoding,
+ apr_file_t *outfile,
+ apr_file_t *errfile,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_diff_peg4(options,
+ path,
+ peg_revision,
+ start_revision,
+ end_revision,
+ NULL,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse),
+ ignore_ancestry,
+ no_diff_deleted,
+ ignore_content_type,
+ header_encoding,
+ outfile,
+ errfile,
+ NULL,
+ ctx,
+ pool);
+}
+
+svn_error_t *
+svn_client_diff_peg2(const apr_array_header_t *options,
+ const char *path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start_revision,
+ const svn_opt_revision_t *end_revision,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_deleted,
+ svn_boolean_t ignore_content_type,
+ apr_file_t *outfile,
+ apr_file_t *errfile,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_diff_peg3(options, path, peg_revision, start_revision,
+ end_revision,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse),
+ ignore_ancestry, no_diff_deleted,
+ ignore_content_type, SVN_APR_LOCALE_CHARSET,
+ outfile, errfile, ctx, pool);
+}
+
+svn_error_t *
+svn_client_diff_peg(const apr_array_header_t *options,
+ const char *path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start_revision,
+ const svn_opt_revision_t *end_revision,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_deleted,
+ apr_file_t *outfile,
+ apr_file_t *errfile,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_diff_peg2(options, path, peg_revision,
+ start_revision, end_revision, recurse,
+ ignore_ancestry, no_diff_deleted, FALSE,
+ outfile, errfile, ctx, pool);
+}
+
+svn_error_t *
+svn_client_diff_summarize(const char *path1,
+ const svn_opt_revision_t *revision1,
+ const char *path2,
+ const svn_opt_revision_t *revision2,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_client_diff_summarize_func_t summarize_func,
+ void *summarize_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_diff_summarize2(path1, revision1, path2,
+ revision2,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse),
+ ignore_ancestry, NULL, summarize_func,
+ summarize_baton, ctx, pool);
+}
+
+svn_error_t *
+svn_client_diff_summarize_peg(const char *path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start_revision,
+ const svn_opt_revision_t *end_revision,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_client_diff_summarize_func_t summarize_func,
+ void *summarize_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_diff_summarize_peg2(path, peg_revision,
+ start_revision, end_revision,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse),
+ ignore_ancestry, NULL,
+ summarize_func, summarize_baton,
+ ctx, pool);
+}
+
+/*** From export.c ***/
+svn_error_t *
+svn_client_export4(svn_revnum_t *result_rev,
+ const char *from_path_or_url,
+ const char *to_path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t overwrite,
+ svn_boolean_t ignore_externals,
+ svn_depth_t depth,
+ const char *native_eol,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_export5(result_rev, from_path_or_url, to_path,
+ peg_revision, revision, overwrite, ignore_externals,
+ FALSE, depth, native_eol, ctx, pool);
+}
+
+svn_error_t *
+svn_client_export3(svn_revnum_t *result_rev,
+ const char *from_path_or_url,
+ const char *to_path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t overwrite,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t recurse,
+ const char *native_eol,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_export4(result_rev, from_path_or_url, to_path,
+ peg_revision, revision, overwrite, ignore_externals,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse),
+ native_eol, ctx, pool);
+}
+
+svn_error_t *
+svn_client_export2(svn_revnum_t *result_rev,
+ const char *from_path_or_url,
+ const char *to_path,
+ svn_opt_revision_t *revision,
+ svn_boolean_t force,
+ const char *native_eol,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_opt_revision_t peg_revision;
+
+ peg_revision.kind = svn_opt_revision_unspecified;
+
+ return svn_client_export3(result_rev, from_path_or_url, to_path,
+ &peg_revision, revision, force, FALSE, TRUE,
+ native_eol, ctx, pool);
+}
+
+
+svn_error_t *
+svn_client_export(svn_revnum_t *result_rev,
+ const char *from_path_or_url,
+ const char *to_path,
+ svn_opt_revision_t *revision,
+ svn_boolean_t force,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_export2(result_rev, from_path_or_url, to_path, revision,
+ force, NULL, ctx, pool);
+}
+
+/*** From list.c ***/
+
+/* Baton for use with wrap_list_func */
+struct list_func_wrapper_baton {
+ void *list_func1_baton;
+ svn_client_list_func_t list_func1;
+};
+
+/* This implements svn_client_list_func2_t */
+static svn_error_t *
+list_func_wrapper(void *baton,
+ const char *path,
+ const svn_dirent_t *dirent,
+ const svn_lock_t *lock,
+ const char *abs_path,
+ const char *external_parent_url,
+ const char *external_target,
+ apr_pool_t *scratch_pool)
+{
+ struct list_func_wrapper_baton *lfwb = baton;
+
+ if (lfwb->list_func1)
+ return lfwb->list_func1(lfwb->list_func1_baton, path, dirent,
+ lock, abs_path, scratch_pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper function for svn_client_list2(). It wraps old format baton
+ and callback function in list_func_wrapper_baton and
+ returns new baton and callback to use with svn_client_list3(). */
+static void
+wrap_list_func(svn_client_list_func2_t *list_func2,
+ void **list_func2_baton,
+ svn_client_list_func_t list_func,
+ void *baton,
+ apr_pool_t *result_pool)
+{
+ struct list_func_wrapper_baton *lfwb = apr_palloc(result_pool,
+ sizeof(*lfwb));
+
+ /* Set the user provided old format callback in the baton. */
+ lfwb->list_func1_baton = baton;
+ lfwb->list_func1 = list_func;
+
+ *list_func2_baton = lfwb;
+ *list_func2 = list_func_wrapper;
+}
+
+svn_error_t *
+svn_client_list2(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ apr_uint32_t dirent_fields,
+ svn_boolean_t fetch_locks,
+ svn_client_list_func_t list_func,
+ void *baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_client_list_func2_t list_func2;
+ void *list_func2_baton;
+
+ wrap_list_func(&list_func2, &list_func2_baton, list_func, baton, pool);
+
+ return svn_client_list3(path_or_url, peg_revision, revision, depth,
+ dirent_fields, fetch_locks,
+ FALSE /* include externals */,
+ list_func2, list_func2_baton, ctx, pool);
+}
+
+svn_error_t *
+svn_client_list(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ apr_uint32_t dirent_fields,
+ svn_boolean_t fetch_locks,
+ svn_client_list_func_t list_func,
+ void *baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_list2(path_or_url,
+ peg_revision,
+ revision,
+ SVN_DEPTH_INFINITY_OR_IMMEDIATES(recurse),
+ dirent_fields,
+ fetch_locks,
+ list_func,
+ baton,
+ ctx,
+ pool);
+}
+
+/* Baton used by compatibility wrapper svn_client_ls3. */
+struct ls_baton {
+ apr_hash_t *dirents;
+ apr_hash_t *locks;
+ apr_pool_t *pool;
+};
+
+/* This implements svn_client_list_func_t. */
+static svn_error_t *
+store_dirent(void *baton, const char *path, const svn_dirent_t *dirent,
+ const svn_lock_t *lock, const char *abs_path, apr_pool_t *pool)
+{
+ struct ls_baton *lb = baton;
+
+ /* The dirent is allocated in a temporary pool, so duplicate it into the
+ correct pool. Note, however, that the locks are stored in the correct
+ pool already. */
+ dirent = svn_dirent_dup(dirent, lb->pool);
+
+ /* An empty path means we are called for the target of the operation.
+ For compatibility, we only store the target if it is a file, and we
+ store it under the basename of the URL. Note that this makes it
+ impossible to differentiate between the target being a directory with a
+ child with the same basename as the target and the target being a file,
+ but that's how it was implemented. */
+ if (path[0] == '\0')
+ {
+ if (dirent->kind == svn_node_file)
+ {
+ const char *base_name = svn_path_basename(abs_path, lb->pool);
+ svn_hash_sets(lb->dirents, base_name, dirent);
+ if (lock)
+ svn_hash_sets(lb->locks, base_name, lock);
+ }
+ }
+ else
+ {
+ path = apr_pstrdup(lb->pool, path);
+ svn_hash_sets(lb->dirents, path, dirent);
+ if (lock)
+ svn_hash_sets(lb->locks, path, lock);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_ls3(apr_hash_t **dirents,
+ apr_hash_t **locks,
+ const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct ls_baton lb;
+
+ *dirents = lb.dirents = apr_hash_make(pool);
+ if (locks)
+ *locks = lb.locks = apr_hash_make(pool);
+ lb.pool = pool;
+
+ return svn_client_list(path_or_url, peg_revision, revision, recurse,
+ SVN_DIRENT_ALL, locks != NULL,
+ store_dirent, &lb, ctx, pool);
+}
+
+svn_error_t *
+svn_client_ls2(apr_hash_t **dirents,
+ const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+
+ return svn_client_ls3(dirents, NULL, path_or_url, peg_revision,
+ revision, recurse, ctx, pool);
+}
+
+
+svn_error_t *
+svn_client_ls(apr_hash_t **dirents,
+ const char *path_or_url,
+ svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_ls2(dirents, path_or_url, revision,
+ revision, recurse, ctx, pool);
+}
+
+/*** From log.c ***/
+svn_error_t *
+svn_client_log4(const apr_array_header_t *targets,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ int limit,
+ svn_boolean_t discover_changed_paths,
+ svn_boolean_t strict_node_history,
+ svn_boolean_t include_merged_revisions,
+ const apr_array_header_t *revprops,
+ svn_log_entry_receiver_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *revision_ranges;
+
+ revision_ranges = apr_array_make(pool, 1,
+ sizeof(svn_opt_revision_range_t *));
+ APR_ARRAY_PUSH(revision_ranges, svn_opt_revision_range_t *)
+ = svn_opt__revision_range_create(start, end, pool);
+
+ return svn_client_log5(targets, peg_revision, revision_ranges, limit,
+ discover_changed_paths, strict_node_history,
+ include_merged_revisions, revprops, receiver,
+ receiver_baton, ctx, pool);
+}
+
+
+svn_error_t *
+svn_client_log3(const apr_array_header_t *targets,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ int limit,
+ svn_boolean_t discover_changed_paths,
+ svn_boolean_t strict_node_history,
+ svn_log_message_receiver_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_log_entry_receiver_t receiver2;
+ void *receiver2_baton;
+
+ svn_compat_wrap_log_receiver(&receiver2, &receiver2_baton,
+ receiver, receiver_baton,
+ pool);
+
+ return svn_client_log4(targets, peg_revision, start, end, limit,
+ discover_changed_paths, strict_node_history, FALSE,
+ svn_compat_log_revprops_in(pool),
+ receiver2, receiver2_baton, ctx, pool);
+}
+
+svn_error_t *
+svn_client_log2(const apr_array_header_t *targets,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ int limit,
+ svn_boolean_t discover_changed_paths,
+ svn_boolean_t strict_node_history,
+ svn_log_message_receiver_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_opt_revision_t peg_revision;
+ peg_revision.kind = svn_opt_revision_unspecified;
+ return svn_client_log3(targets, &peg_revision, start, end, limit,
+ discover_changed_paths, strict_node_history,
+ receiver, receiver_baton, ctx, pool);
+}
+
+svn_error_t *
+svn_client_log(const apr_array_header_t *targets,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ svn_boolean_t discover_changed_paths,
+ svn_boolean_t strict_node_history,
+ svn_log_message_receiver_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err = SVN_NO_ERROR;
+
+ err = svn_client_log2(targets, start, end, 0, discover_changed_paths,
+ strict_node_history, receiver, receiver_baton, ctx,
+ pool);
+
+ /* Special case: If there have been no commits, we'll get an error
+ * for requesting log of a revision higher than 0. But the
+ * default behavior of "svn log" is to give revisions HEAD through
+ * 1, on the assumption that HEAD >= 1.
+ *
+ * So if we got that error for that reason, and it looks like the
+ * user was just depending on the defaults (rather than explicitly
+ * requesting the log for revision 1), then we don't error. Instead
+ * we just invoke the receiver manually on a hand-constructed log
+ * message for revision 0.
+ *
+ * See also http://subversion.tigris.org/issues/show_bug.cgi?id=692.
+ */
+ if (err && (err->apr_err == SVN_ERR_FS_NO_SUCH_REVISION)
+ && (start->kind == svn_opt_revision_head)
+ && ((end->kind == svn_opt_revision_number)
+ && (end->value.number == 1)))
+ {
+
+ /* We don't need to check if HEAD is 0, because that must be the case,
+ * by logical deduction: The revision range specified is HEAD:1.
+ * HEAD cannot not exist, so the revision to which "no such revision"
+ * applies is 1. If revision 1 does not exist, then HEAD is 0.
+ * Hence, we deduce the repository is empty without needing access
+ * to further information. */
+
+ svn_error_clear(err);
+ err = SVN_NO_ERROR;
+
+ /* Log receivers are free to handle revision 0 specially... But
+ just in case some don't, we make up a message here. */
+ SVN_ERR(receiver(receiver_baton,
+ NULL, 0, "", "", _("No commits in repository"),
+ pool));
+ }
+
+ return svn_error_trace(err);
+}
+
+/*** From merge.c ***/
+
+svn_error_t *
+svn_client_merge4(const char *source1,
+ const svn_opt_revision_t *revision1,
+ const char *source2,
+ const svn_opt_revision_t *revision2,
+ const char *target_wcpath,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t force_delete,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ svn_boolean_t allow_mixed_rev,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ SVN_ERR(svn_client_merge5(source1, revision1,
+ source2, revision2,
+ target_wcpath,
+ depth,
+ ignore_ancestry /*ignore_mergeinfo*/,
+ ignore_ancestry /*diff_ignore_ancestry*/,
+ force_delete, record_only,
+ dry_run, allow_mixed_rev,
+ merge_options, ctx, pool));
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_merge3(const char *source1,
+ const svn_opt_revision_t *revision1,
+ const char *source2,
+ const svn_opt_revision_t *revision2,
+ const char *target_wcpath,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t force,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_merge4(source1, revision1, source2, revision2,
+ target_wcpath, depth, ignore_ancestry, force,
+ record_only, dry_run, TRUE, merge_options,
+ ctx, pool);
+}
+
+svn_error_t *
+svn_client_merge2(const char *source1,
+ const svn_opt_revision_t *revision1,
+ const char *source2,
+ const svn_opt_revision_t *revision2,
+ const char *target_wcpath,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t force,
+ svn_boolean_t dry_run,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_merge3(source1, revision1, source2, revision2,
+ target_wcpath,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse),
+ ignore_ancestry, force, FALSE, dry_run,
+ merge_options, ctx, pool);
+}
+
+svn_error_t *
+svn_client_merge(const char *source1,
+ const svn_opt_revision_t *revision1,
+ const char *source2,
+ const svn_opt_revision_t *revision2,
+ const char *target_wcpath,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t force,
+ svn_boolean_t dry_run,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_merge2(source1, revision1, source2, revision2,
+ target_wcpath, recurse, ignore_ancestry, force,
+ dry_run, NULL, ctx, pool);
+}
+
+svn_error_t *
+svn_client_merge_peg4(const char *source_path_or_url,
+ const apr_array_header_t *ranges_to_merge,
+ const svn_opt_revision_t *source_peg_revision,
+ const char *target_wcpath,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t force_delete,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ svn_boolean_t allow_mixed_rev,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ SVN_ERR(svn_client_merge_peg5(source_path_or_url,
+ ranges_to_merge,
+ source_peg_revision,
+ target_wcpath,
+ depth,
+ ignore_ancestry /*ignore_mergeinfo*/,
+ ignore_ancestry /*diff_ignore_ancestry*/,
+ force_delete, record_only,
+ dry_run, allow_mixed_rev,
+ merge_options, ctx, pool));
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_merge_peg3(const char *source,
+ const apr_array_header_t *ranges_to_merge,
+ const svn_opt_revision_t *peg_revision,
+ const char *target_wcpath,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t force,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_merge_peg4(source, ranges_to_merge, peg_revision,
+ target_wcpath, depth, ignore_ancestry, force,
+ record_only, dry_run, TRUE, merge_options,
+ ctx, pool);
+}
+
+svn_error_t *
+svn_client_merge_peg2(const char *source,
+ const svn_opt_revision_t *revision1,
+ const svn_opt_revision_t *revision2,
+ const svn_opt_revision_t *peg_revision,
+ const char *target_wcpath,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t force,
+ svn_boolean_t dry_run,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *ranges_to_merge =
+ apr_array_make(pool, 1, sizeof(svn_opt_revision_range_t *));
+
+ APR_ARRAY_PUSH(ranges_to_merge, svn_opt_revision_range_t *)
+ = svn_opt__revision_range_create(revision1, revision2, pool);
+ return svn_client_merge_peg3(source, ranges_to_merge,
+ peg_revision,
+ target_wcpath,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse),
+ ignore_ancestry, force, FALSE, dry_run,
+ merge_options, ctx, pool);
+}
+
+svn_error_t *
+svn_client_merge_peg(const char *source,
+ const svn_opt_revision_t *revision1,
+ const svn_opt_revision_t *revision2,
+ const svn_opt_revision_t *peg_revision,
+ const char *target_wcpath,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t force,
+ svn_boolean_t dry_run,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_merge_peg2(source, revision1, revision2, peg_revision,
+ target_wcpath, recurse, ignore_ancestry, force,
+ dry_run, NULL, ctx, pool);
+}
+
+/*** From prop_commands.c ***/
+svn_error_t *
+svn_client_propset3(svn_commit_info_t **commit_info_p,
+ const char *propname,
+ const svn_string_t *propval,
+ const char *target,
+ svn_depth_t depth,
+ svn_boolean_t skip_checks,
+ svn_revnum_t base_revision_for_url,
+ const apr_array_header_t *changelists,
+ const apr_hash_t *revprop_table,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ if (svn_path_is_url(target))
+ {
+ struct capture_baton_t cb;
+
+ cb.info = commit_info_p;
+ cb.pool = pool;
+
+ SVN_ERR(svn_client_propset_remote(propname, propval, target, skip_checks,
+ base_revision_for_url, revprop_table,
+ capture_commit_info, &cb, ctx, pool));
+ }
+ else
+ {
+ apr_array_header_t *targets = apr_array_make(pool, 1,
+ sizeof(const char *));
+
+ APR_ARRAY_PUSH(targets, const char *) = target;
+ SVN_ERR(svn_client_propset_local(propname, propval, targets, depth,
+ skip_checks, changelists, ctx, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_propset2(const char *propname,
+ const svn_string_t *propval,
+ const char *target,
+ svn_boolean_t recurse,
+ svn_boolean_t skip_checks,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_propset3(NULL, propname, propval, target,
+ SVN_DEPTH_INFINITY_OR_EMPTY(recurse),
+ skip_checks, SVN_INVALID_REVNUM,
+ NULL, NULL, ctx, pool);
+}
+
+
+svn_error_t *
+svn_client_propset(const char *propname,
+ const svn_string_t *propval,
+ const char *target,
+ svn_boolean_t recurse,
+ apr_pool_t *pool)
+{
+ svn_client_ctx_t *ctx;
+
+ SVN_ERR(svn_client_create_context(&ctx, pool));
+
+ return svn_client_propset2(propname, propval, target, recurse, FALSE,
+ ctx, pool);
+}
+
+svn_error_t *
+svn_client_revprop_set(const char *propname,
+ const svn_string_t *propval,
+ const char *URL,
+ const svn_opt_revision_t *revision,
+ svn_revnum_t *set_rev,
+ svn_boolean_t force,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_revprop_set2(propname, propval, NULL, URL,
+ revision, set_rev, force, ctx, pool);
+
+}
+
+svn_error_t *
+svn_client_propget4(apr_hash_t **props,
+ const char *propname,
+ const char *target,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_revnum_t *actual_revnum,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ return svn_error_trace(svn_client_propget5(props, NULL, propname, target,
+ peg_revision, revision,
+ actual_revnum, depth,
+ changelists, ctx,
+ result_pool, scratch_pool));
+}
+
+svn_error_t *
+svn_client_propget3(apr_hash_t **props,
+ const char *propname,
+ const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_revnum_t *actual_revnum,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *target;
+ apr_hash_t *temp_props;
+ svn_error_t *err;
+
+ if (svn_path_is_url(path_or_url))
+ target = path_or_url;
+ else
+ SVN_ERR(svn_dirent_get_absolute(&target, path_or_url, pool));
+
+ err = svn_client_propget4(&temp_props, propname, target,
+ peg_revision, revision, actual_revnum,
+ depth, changelists, ctx, pool, pool);
+
+ if (err && err->apr_err == SVN_ERR_UNVERSIONED_RESOURCE)
+ {
+ err->apr_err = SVN_ERR_ENTRY_NOT_FOUND;
+ return svn_error_trace(err);
+ }
+ else
+ SVN_ERR(err);
+
+ if (actual_revnum
+ && !svn_path_is_url(path_or_url)
+ && !SVN_IS_VALID_REVNUM(*actual_revnum))
+ {
+ /* Get the actual_revnum; added nodes have no revision yet, and old
+ * callers expected the mock-up revision of 0. */
+ svn_boolean_t added;
+
+ SVN_ERR(svn_wc__node_is_added(&added, ctx->wc_ctx, target, pool));
+ if (added)
+ *actual_revnum = 0;
+ }
+
+ /* We may need to fix up our hash keys for legacy callers. */
+ if (!svn_path_is_url(path_or_url) && strcmp(target, path_or_url) != 0)
+ {
+ apr_hash_index_t *hi;
+
+ *props = apr_hash_make(pool);
+ for (hi = apr_hash_first(pool, temp_props); hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *abspath = svn__apr_hash_index_key(hi);
+ svn_string_t *value = svn__apr_hash_index_val(hi);
+ const char *relpath = svn_dirent_join(path_or_url,
+ svn_dirent_skip_ancestor(target, abspath),
+ pool);
+
+ svn_hash_sets(*props, relpath, value);
+ }
+ }
+ else
+ *props = temp_props;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_propget2(apr_hash_t **props,
+ const char *propname,
+ const char *target,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_propget3(props,
+ propname,
+ target,
+ peg_revision,
+ revision,
+ NULL,
+ SVN_DEPTH_INFINITY_OR_EMPTY(recurse),
+ NULL,
+ ctx,
+ pool);
+}
+
+svn_error_t *
+svn_client_propget(apr_hash_t **props,
+ const char *propname,
+ const char *target,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_propget2(props, propname, target, revision, revision,
+ recurse, ctx, pool);
+}
+
+
+/* Duplicate a HASH containing (char * -> svn_string_t *) key/value
+ pairs using POOL. */
+static apr_hash_t *
+string_hash_dup(apr_hash_t *hash, apr_pool_t *pool)
+{
+ apr_hash_index_t *hi;
+ apr_hash_t *new_hash = apr_hash_make(pool);
+
+ for (hi = apr_hash_first(pool, hash); hi; hi = apr_hash_next(hi))
+ {
+ const char *key = apr_pstrdup(pool, svn__apr_hash_index_key(hi));
+ apr_ssize_t klen = svn__apr_hash_index_klen(hi);
+ svn_string_t *val = svn_string_dup(svn__apr_hash_index_val(hi), pool);
+
+ apr_hash_set(new_hash, key, klen, val);
+ }
+ return new_hash;
+}
+
+svn_client_proplist_item_t *
+svn_client_proplist_item_dup(const svn_client_proplist_item_t *item,
+ apr_pool_t * pool)
+{
+ svn_client_proplist_item_t *new_item = apr_pcalloc(pool, sizeof(*new_item));
+
+ if (item->node_name)
+ new_item->node_name = svn_stringbuf_dup(item->node_name, pool);
+
+ if (item->prop_hash)
+ new_item->prop_hash = string_hash_dup(item->prop_hash, pool);
+
+ return new_item;
+}
+
+/* Baton for use with wrap_proplist_receiver */
+struct proplist_receiver_wrapper_baton {
+ void *baton;
+ svn_proplist_receiver_t receiver;
+};
+
+/* This implements svn_client_proplist_receiver2_t */
+static svn_error_t *
+proplist_wrapper_receiver(void *baton,
+ const char *path,
+ apr_hash_t *prop_hash,
+ apr_array_header_t *inherited_props,
+ apr_pool_t *pool)
+{
+ struct proplist_receiver_wrapper_baton *plrwb = baton;
+
+ if (plrwb->receiver)
+ return plrwb->receiver(plrwb->baton, path, prop_hash, pool);
+
+ return SVN_NO_ERROR;
+}
+
+static void
+wrap_proplist_receiver(svn_proplist_receiver2_t *receiver2,
+ void **receiver2_baton,
+ svn_proplist_receiver_t receiver,
+ void *receiver_baton,
+ apr_pool_t *pool)
+{
+ struct proplist_receiver_wrapper_baton *plrwb = apr_palloc(pool,
+ sizeof(*plrwb));
+
+ /* Set the user provided old format callback in the baton. */
+ plrwb->baton = receiver_baton;
+ plrwb->receiver = receiver;
+
+ *receiver2_baton = plrwb;
+ *receiver2 = proplist_wrapper_receiver;
+}
+
+svn_error_t *
+svn_client_proplist3(const char *target,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_proplist_receiver_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+
+ svn_proplist_receiver2_t receiver2;
+ void *receiver2_baton;
+
+ wrap_proplist_receiver(&receiver2, &receiver2_baton, receiver, receiver_baton,
+ pool);
+
+ return svn_error_trace(svn_client_proplist4(target, peg_revision, revision,
+ depth, changelists, FALSE,
+ receiver2, receiver2_baton,
+ ctx, pool));
+}
+
+/* Receiver baton used by proplist2() */
+struct proplist_receiver_baton {
+ apr_array_header_t *props;
+ apr_pool_t *pool;
+};
+
+/* Receiver function used by proplist2(). */
+static svn_error_t *
+proplist_receiver_cb(void *baton,
+ const char *path,
+ apr_hash_t *prop_hash,
+ apr_pool_t *pool)
+{
+ struct proplist_receiver_baton *pl_baton =
+ (struct proplist_receiver_baton *) baton;
+ svn_client_proplist_item_t *tmp_item = apr_palloc(pool, sizeof(*tmp_item));
+ svn_client_proplist_item_t *item;
+
+ /* Because the pool passed to the receiver function is likely to be a
+ temporary pool of some kind, we need to make copies of *path and
+ *prop_hash in the pool provided by the baton. */
+ tmp_item->node_name = svn_stringbuf_create(path, pl_baton->pool);
+ tmp_item->prop_hash = prop_hash;
+
+ item = svn_client_proplist_item_dup(tmp_item, pl_baton->pool);
+
+ APR_ARRAY_PUSH(pl_baton->props, const svn_client_proplist_item_t *) = item;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_proplist2(apr_array_header_t **props,
+ const char *target,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct proplist_receiver_baton pl_baton;
+
+ *props = apr_array_make(pool, 5, sizeof(svn_client_proplist_item_t *));
+ pl_baton.props = *props;
+ pl_baton.pool = pool;
+
+ return svn_client_proplist3(target, peg_revision, revision,
+ SVN_DEPTH_INFINITY_OR_EMPTY(recurse), NULL,
+ proplist_receiver_cb, &pl_baton, ctx, pool);
+}
+
+
+svn_error_t *
+svn_client_proplist(apr_array_header_t **props,
+ const char *target,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_proplist2(props, target, revision, revision,
+ recurse, ctx, pool);
+}
+
+/*** From status.c ***/
+
+struct status4_wrapper_baton
+{
+ svn_wc_context_t *wc_ctx;
+ svn_wc_status_func3_t old_func;
+ void *old_baton;
+};
+
+/* Implements svn_client_status_func_t */
+static svn_error_t *
+status4_wrapper_func(void *baton,
+ const char *path,
+ const svn_client_status_t *status,
+ apr_pool_t *scratch_pool)
+{
+ struct status4_wrapper_baton *swb = baton;
+ svn_wc_status2_t *dup;
+ const char *local_abspath;
+ const svn_wc_status3_t *wc_status = status->backwards_compatibility_baton;
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, scratch_pool));
+ SVN_ERR(svn_wc__status2_from_3(&dup, wc_status, swb->wc_ctx,
+ local_abspath, scratch_pool,
+ scratch_pool));
+
+ return (*swb->old_func)(swb->old_baton, path, dup, scratch_pool);
+}
+
+svn_error_t *
+svn_client_status4(svn_revnum_t *result_rev,
+ const char *path,
+ const svn_opt_revision_t *revision,
+ svn_wc_status_func3_t status_func,
+ void *status_baton,
+ svn_depth_t depth,
+ svn_boolean_t get_all,
+ svn_boolean_t update,
+ svn_boolean_t no_ignore,
+ svn_boolean_t ignore_externals,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct status4_wrapper_baton swb;
+
+ swb.wc_ctx = ctx->wc_ctx;
+ swb.old_func = status_func;
+ swb.old_baton = status_baton;
+
+ return svn_client_status5(result_rev, ctx, path, revision, depth, get_all,
+ update, no_ignore, ignore_externals, TRUE,
+ changelists, status4_wrapper_func, &swb, pool);
+}
+
+struct status3_wrapper_baton
+{
+ svn_wc_status_func2_t old_func;
+ void *old_baton;
+};
+
+static svn_error_t *
+status3_wrapper_func(void *baton,
+ const char *path,
+ svn_wc_status2_t *status,
+ apr_pool_t *pool)
+{
+ struct status3_wrapper_baton *swb = baton;
+
+ swb->old_func(swb->old_baton, path, status);
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_status3(svn_revnum_t *result_rev,
+ const char *path,
+ const svn_opt_revision_t *revision,
+ svn_wc_status_func2_t status_func,
+ void *status_baton,
+ svn_depth_t depth,
+ svn_boolean_t get_all,
+ svn_boolean_t update,
+ svn_boolean_t no_ignore,
+ svn_boolean_t ignore_externals,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct status3_wrapper_baton swb = { 0 };
+ swb.old_func = status_func;
+ swb.old_baton = status_baton;
+ return svn_client_status4(result_rev, path, revision, status3_wrapper_func,
+ &swb, depth, get_all, update, no_ignore,
+ ignore_externals, changelists, ctx, pool);
+}
+
+svn_error_t *
+svn_client_status2(svn_revnum_t *result_rev,
+ const char *path,
+ const svn_opt_revision_t *revision,
+ svn_wc_status_func2_t status_func,
+ void *status_baton,
+ svn_boolean_t recurse,
+ svn_boolean_t get_all,
+ svn_boolean_t update,
+ svn_boolean_t no_ignore,
+ svn_boolean_t ignore_externals,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_status3(result_rev, path, revision,
+ status_func, status_baton,
+ SVN_DEPTH_INFINITY_OR_IMMEDIATES(recurse),
+ get_all, update, no_ignore, ignore_externals, NULL,
+ ctx, pool);
+}
+
+
+/* Baton for old_status_func_cb; does what you think it does. */
+struct old_status_func_cb_baton
+{
+ svn_wc_status_func_t original_func;
+ void *original_baton;
+};
+
+/* Help svn_client_status() accept an old-style status func and baton,
+ by wrapping them before passing along to svn_client_status2().
+
+ This implements the 'svn_wc_status_func2_t' function type. */
+static void old_status_func_cb(void *baton,
+ const char *path,
+ svn_wc_status2_t *status)
+{
+ struct old_status_func_cb_baton *b = baton;
+ svn_wc_status_t *stat = (svn_wc_status_t *) status;
+
+ b->original_func(b->original_baton, path, stat);
+}
+
+
+svn_error_t *
+svn_client_status(svn_revnum_t *result_rev,
+ const char *path,
+ svn_opt_revision_t *revision,
+ svn_wc_status_func_t status_func,
+ void *status_baton,
+ svn_boolean_t recurse,
+ svn_boolean_t get_all,
+ svn_boolean_t update,
+ svn_boolean_t no_ignore,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct old_status_func_cb_baton *b = apr_pcalloc(pool, sizeof(*b));
+ b->original_func = status_func;
+ b->original_baton = status_baton;
+
+ return svn_client_status2(result_rev, path, revision,
+ old_status_func_cb, b,
+ recurse, get_all, update, no_ignore, FALSE,
+ ctx, pool);
+}
+
+/*** From update.c ***/
+svn_error_t *
+svn_client_update3(apr_array_header_t **result_revs,
+ const apr_array_header_t *paths,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t depth_is_sticky,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_update4(result_revs, paths, revision,
+ depth, depth_is_sticky, ignore_externals,
+ allow_unver_obstructions, TRUE, FALSE,
+ ctx, pool);
+}
+
+svn_error_t *
+svn_client_update2(apr_array_header_t **result_revs,
+ const apr_array_header_t *paths,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_externals,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_update3(result_revs, paths, revision,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse), FALSE,
+ ignore_externals, FALSE, ctx, pool);
+}
+
+svn_error_t *
+svn_client_update(svn_revnum_t *result_rev,
+ const char *path,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *paths = apr_array_make(pool, 1, sizeof(const char *));
+ apr_array_header_t *result_revs;
+
+ APR_ARRAY_PUSH(paths, const char *) = path;
+
+ SVN_ERR(svn_client_update2(&result_revs, paths, revision, recurse, FALSE,
+ ctx, pool));
+
+ *result_rev = APR_ARRAY_IDX(result_revs, 0, svn_revnum_t);
+
+ return SVN_NO_ERROR;
+}
+
+/*** From switch.c ***/
+svn_error_t *
+svn_client_switch2(svn_revnum_t *result_rev,
+ const char *path,
+ const char *switch_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t depth_is_sticky,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_switch3(result_rev, path, switch_url, peg_revision,
+ revision, depth, depth_is_sticky, ignore_externals,
+ allow_unver_obstructions,
+ TRUE /* ignore_ancestry */,
+ ctx, pool);
+}
+
+svn_error_t *
+svn_client_switch(svn_revnum_t *result_rev,
+ const char *path,
+ const char *switch_url,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_opt_revision_t peg_revision;
+ peg_revision.kind = svn_opt_revision_unspecified;
+ return svn_client_switch2(result_rev, path, switch_url,
+ &peg_revision, revision,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse),
+ FALSE, FALSE, FALSE, ctx, pool);
+}
+
+/*** From cat.c ***/
+svn_error_t *
+svn_client_cat(svn_stream_t *out,
+ const char *path_or_url,
+ const svn_opt_revision_t *revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_cat2(out, path_or_url, revision, revision,
+ ctx, pool);
+}
+
+/*** From checkout.c ***/
+svn_error_t *
+svn_client_checkout2(svn_revnum_t *result_rev,
+ const char *URL,
+ const char *path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_boolean_t ignore_externals,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_error_trace(svn_client_checkout3(result_rev, URL, path,
+ peg_revision, revision,
+ SVN_DEPTH_INFINITY_OR_FILES(recurse),
+ ignore_externals, FALSE, ctx, pool));
+}
+
+svn_error_t *
+svn_client_checkout(svn_revnum_t *result_rev,
+ const char *URL,
+ const char *path,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_opt_revision_t peg_revision;
+
+ peg_revision.kind = svn_opt_revision_unspecified;
+
+ return svn_error_trace(svn_client_checkout2(result_rev, URL, path,
+ &peg_revision, revision, recurse,
+ FALSE, ctx, pool));
+}
+
+/*** From info.c ***/
+
+svn_info_t *
+svn_info_dup(const svn_info_t *info, apr_pool_t *pool)
+{
+ svn_info_t *dupinfo = apr_palloc(pool, sizeof(*dupinfo));
+
+ /* Perform a trivial copy ... */
+ *dupinfo = *info;
+
+ /* ...and then re-copy stuff that needs to be duped into our pool. */
+ if (info->URL)
+ dupinfo->URL = apr_pstrdup(pool, info->URL);
+ if (info->repos_root_URL)
+ dupinfo->repos_root_URL = apr_pstrdup(pool, info->repos_root_URL);
+ if (info->repos_UUID)
+ dupinfo->repos_UUID = apr_pstrdup(pool, info->repos_UUID);
+ if (info->last_changed_author)
+ dupinfo->last_changed_author = apr_pstrdup(pool,
+ info->last_changed_author);
+ if (info->lock)
+ dupinfo->lock = svn_lock_dup(info->lock, pool);
+ if (info->copyfrom_url)
+ dupinfo->copyfrom_url = apr_pstrdup(pool, info->copyfrom_url);
+ if (info->checksum)
+ dupinfo->checksum = apr_pstrdup(pool, info->checksum);
+ if (info->conflict_old)
+ dupinfo->conflict_old = apr_pstrdup(pool, info->conflict_old);
+ if (info->conflict_new)
+ dupinfo->conflict_new = apr_pstrdup(pool, info->conflict_new);
+ if (info->conflict_wrk)
+ dupinfo->conflict_wrk = apr_pstrdup(pool, info->conflict_wrk);
+ if (info->prejfile)
+ dupinfo->prejfile = apr_pstrdup(pool, info->prejfile);
+
+ return dupinfo;
+}
+
+/* Convert an svn_client_info2_t to an svn_info_t, doing shallow copies. */
+static svn_error_t *
+info_from_info2(svn_info_t **new_info,
+ svn_wc_context_t *wc_ctx,
+ const svn_client_info2_t *info2,
+ apr_pool_t *pool)
+{
+ svn_info_t *info = apr_pcalloc(pool, sizeof(*info));
+
+ info->URL = info2->URL;
+ /* Goofy backward compat handling for added nodes. */
+ if (SVN_IS_VALID_REVNUM(info2->rev))
+ info->rev = info2->rev;
+ else
+ info->rev = 0;
+
+ info->kind = info2->kind;
+ info->repos_root_URL = info2->repos_root_URL;
+ info->repos_UUID = info2->repos_UUID;
+ info->last_changed_rev = info2->last_changed_rev;
+ info->last_changed_date = info2->last_changed_date;
+ info->last_changed_author = info2->last_changed_author;
+
+ /* Stupid old structure has a non-const LOCK member. Sigh. */
+ info->lock = (svn_lock_t *)info2->lock;
+
+ info->size64 = info2->size;
+ if (info2->size == SVN_INVALID_FILESIZE)
+ info->size = SVN_INFO_SIZE_UNKNOWN;
+ else if (((svn_filesize_t)(apr_size_t)info->size64) == info->size64)
+ info->size = (apr_size_t)info->size64;
+ else /* >= 4GB */
+ info->size = SVN_INFO_SIZE_UNKNOWN;
+
+ if (info2->wc_info)
+ {
+ info->has_wc_info = TRUE;
+ info->schedule = info2->wc_info->schedule;
+ info->copyfrom_url = info2->wc_info->copyfrom_url;
+ info->copyfrom_rev = info2->wc_info->copyfrom_rev;
+ info->text_time = info2->wc_info->recorded_time;
+ info->prop_time = 0;
+ if (info2->wc_info->checksum
+ && info2->wc_info->checksum->kind == svn_checksum_md5)
+ info->checksum = svn_checksum_to_cstring(
+ info2->wc_info->checksum, pool);
+ else
+ info->checksum = NULL;
+ info->changelist = info2->wc_info->changelist;
+ info->depth = info2->wc_info->depth;
+
+ if (info->depth == svn_depth_unknown && info->kind == svn_node_file)
+ info->depth = svn_depth_infinity;
+
+ info->working_size64 = info2->wc_info->recorded_size;
+ if (((svn_filesize_t)(apr_size_t)info->working_size64) == info->working_size64)
+ info->working_size = (apr_size_t)info->working_size64;
+ else /* >= 4GB */
+ info->working_size = SVN_INFO_SIZE_UNKNOWN;
+ }
+ else
+ {
+ info->has_wc_info = FALSE;
+ info->working_size = SVN_INFO_SIZE_UNKNOWN;
+ info->working_size64 = SVN_INVALID_FILESIZE;
+ info->depth = svn_depth_unknown;
+ }
+
+ /* Populate conflict fields. */
+ if (info2->wc_info && info2->wc_info->conflicts)
+ {
+ int i;
+
+ for (i = 0; i < info2->wc_info->conflicts->nelts; i++)
+ {
+ const svn_wc_conflict_description2_t *conflict
+ = APR_ARRAY_IDX(info2->wc_info->conflicts, i,
+ const svn_wc_conflict_description2_t *);
+
+ /* ### Not really sure what we should do if we get multiple
+ ### conflicts of the same type. */
+ switch (conflict->kind)
+ {
+ case svn_wc_conflict_kind_tree:
+ info->tree_conflict = svn_wc__cd2_to_cd(conflict, pool);
+ break;
+
+ case svn_wc_conflict_kind_text:
+ info->conflict_old = conflict->base_abspath;
+ info->conflict_new = conflict->my_abspath;
+ info->conflict_wrk = conflict->their_abspath;
+ break;
+
+ case svn_wc_conflict_kind_property:
+ info->prejfile = conflict->their_abspath;
+ break;
+ }
+ }
+ }
+
+ if (info2->wc_info && info2->wc_info->checksum)
+ {
+ const svn_checksum_t *md5_checksum;
+
+ SVN_ERR(svn_wc__node_get_md5_from_sha1(&md5_checksum,
+ wc_ctx,
+ info2->wc_info->wcroot_abspath,
+ info2->wc_info->checksum,
+ pool, pool));
+
+ info->checksum = svn_checksum_to_cstring(md5_checksum, pool);
+ }
+
+ *new_info = info;
+ return SVN_NO_ERROR;
+}
+
+struct info_to_relpath_baton
+{
+ const char *anchor_abspath;
+ const char *anchor_relpath;
+ svn_info_receiver_t info_receiver;
+ void *info_baton;
+ svn_wc_context_t *wc_ctx;
+};
+
+static svn_error_t *
+info_receiver_relpath_wrapper(void *baton,
+ const char *abspath_or_url,
+ const svn_client_info2_t *info2,
+ apr_pool_t *scratch_pool)
+{
+ struct info_to_relpath_baton *rb = baton;
+ const char *path = abspath_or_url;
+ svn_info_t *info;
+
+ if (rb->anchor_relpath &&
+ svn_dirent_is_ancestor(rb->anchor_abspath, abspath_or_url))
+ {
+ path = svn_dirent_join(rb->anchor_relpath,
+ svn_dirent_skip_ancestor(rb->anchor_abspath,
+ abspath_or_url),
+ scratch_pool);
+ }
+
+ SVN_ERR(info_from_info2(&info, rb->wc_ctx, info2, scratch_pool));
+
+ SVN_ERR(rb->info_receiver(rb->info_baton,
+ path,
+ info,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_info2(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_info_receiver_t receiver,
+ void *receiver_baton,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct info_to_relpath_baton rb;
+ const char *abspath_or_url = path_or_url;
+
+ rb.anchor_relpath = NULL;
+ rb.info_receiver = receiver;
+ rb.info_baton = receiver_baton;
+ rb.wc_ctx = ctx->wc_ctx;
+
+ if (!svn_path_is_url(path_or_url))
+ {
+ SVN_ERR(svn_dirent_get_absolute(&abspath_or_url, path_or_url, pool));
+ rb.anchor_abspath = abspath_or_url;
+ rb.anchor_relpath = path_or_url;
+ }
+
+ SVN_ERR(svn_client_info3(abspath_or_url,
+ peg_revision,
+ revision,
+ depth,
+ FALSE, TRUE,
+ changelists,
+ info_receiver_relpath_wrapper,
+ &rb,
+ ctx,
+ pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_info(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_info_receiver_t receiver,
+ void *receiver_baton,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_info2(path_or_url, peg_revision, revision,
+ receiver, receiver_baton,
+ SVN_DEPTH_INFINITY_OR_EMPTY(recurse),
+ NULL, ctx, pool);
+}
+
+/*** From resolved.c ***/
+svn_error_t *
+svn_client_resolved(const char *path,
+ svn_boolean_t recursive,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_depth_t depth = SVN_DEPTH_INFINITY_OR_EMPTY(recursive);
+ return svn_client_resolve(path, depth,
+ svn_wc_conflict_choose_merged, ctx, pool);
+}
+/*** From revert.c ***/
+svn_error_t *
+svn_client_revert(const apr_array_header_t *paths,
+ svn_boolean_t recursive,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_revert2(paths, SVN_DEPTH_INFINITY_OR_EMPTY(recursive),
+ NULL, ctx, pool);
+}
+
+/*** From ra.c ***/
+svn_error_t *
+svn_client_open_ra_session(svn_ra_session_t **session,
+ const char *url,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_error_trace(
+ svn_client_open_ra_session2(session, url,
+ NULL, ctx,
+ pool, pool));
+}
+
+svn_error_t *
+svn_client_uuid_from_url(const char **uuid,
+ const char *url,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+ apr_pool_t *subpool = svn_pool_create(pool);
+
+ err = svn_client_get_repos_root(NULL, uuid, url,
+ ctx, pool, subpool);
+ /* destroy the RA session */
+ svn_pool_destroy(subpool);
+
+ return svn_error_trace(err);;
+}
+
+svn_error_t *
+svn_client_uuid_from_path2(const char **uuid,
+ const char *local_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ return svn_error_trace(
+ svn_client_get_repos_root(NULL, uuid,
+ local_abspath, ctx,
+ result_pool, scratch_pool));
+}
+
+svn_error_t *
+svn_client_uuid_from_path(const char **uuid,
+ const char *path,
+ svn_wc_adm_access_t *adm_access,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *local_abspath;
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, pool));
+ return svn_error_trace(
+ svn_client_uuid_from_path2(uuid, local_abspath, ctx, pool, pool));
+}
+
+/*** From url.c ***/
+svn_error_t *
+svn_client_root_url_from_path(const char **url,
+ const char *path_or_url,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_pool_t *subpool = svn_pool_create(pool);
+ svn_error_t *err;
+ if (!svn_path_is_url(path_or_url))
+ SVN_ERR(svn_dirent_get_absolute(&path_or_url, path_or_url, pool));
+
+ err = svn_client_get_repos_root(url, NULL, path_or_url,
+ ctx, pool, subpool);
+
+ /* close ra session */
+ svn_pool_destroy(subpool);
+ return svn_error_trace(err);
+}
+
+svn_error_t *
+svn_client_url_from_path(const char **url,
+ const char *path_or_url,
+ apr_pool_t *pool)
+{
+ svn_client_ctx_t *ctx;
+
+ SVN_ERR(svn_client_create_context(&ctx, pool));
+
+ return svn_client_url_from_path2(url, path_or_url, ctx, pool, pool);
+}
+
+/*** From mergeinfo.c ***/
+svn_error_t *
+svn_client_mergeinfo_log(svn_boolean_t finding_merged,
+ const char *target_path_or_url,
+ const svn_opt_revision_t *target_peg_revision,
+ const char *source_path_or_url,
+ const svn_opt_revision_t *source_peg_revision,
+ svn_log_entry_receiver_t receiver,
+ void *receiver_baton,
+ svn_boolean_t discover_changed_paths,
+ svn_depth_t depth,
+ const apr_array_header_t *revprops,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_opt_revision_t start_revision, end_revision;
+
+ start_revision.kind = svn_opt_revision_unspecified;
+ end_revision.kind = svn_opt_revision_unspecified;
+
+ return svn_client_mergeinfo_log2(finding_merged,
+ target_path_or_url, target_peg_revision,
+ source_path_or_url, source_peg_revision,
+ &start_revision, &end_revision,
+ receiver, receiver_baton,
+ discover_changed_paths, depth, revprops,
+ ctx, scratch_pool);
+}
+
+svn_error_t *
+svn_client_mergeinfo_log_merged(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const char *merge_source_path_or_url,
+ const svn_opt_revision_t *src_peg_revision,
+ svn_log_entry_receiver_t log_receiver,
+ void *log_receiver_baton,
+ svn_boolean_t discover_changed_paths,
+ const apr_array_header_t *revprops,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_mergeinfo_log(TRUE, path_or_url, peg_revision,
+ merge_source_path_or_url,
+ src_peg_revision,
+ log_receiver, log_receiver_baton,
+ discover_changed_paths,
+ svn_depth_empty, revprops, ctx,
+ pool);
+}
+
+svn_error_t *
+svn_client_mergeinfo_log_eligible(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const char *merge_source_path_or_url,
+ const svn_opt_revision_t *src_peg_revision,
+ svn_log_entry_receiver_t log_receiver,
+ void *log_receiver_baton,
+ svn_boolean_t discover_changed_paths,
+ const apr_array_header_t *revprops,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return svn_client_mergeinfo_log(FALSE, path_or_url, peg_revision,
+ merge_source_path_or_url,
+ src_peg_revision,
+ log_receiver, log_receiver_baton,
+ discover_changed_paths,
+ svn_depth_empty, revprops, ctx,
+ pool);
+}
+
+/*** From relocate.c ***/
+svn_error_t *
+svn_client_relocate(const char *path,
+ const char *from_prefix,
+ const char *to_prefix,
+ svn_boolean_t recurse,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ if (! recurse)
+ SVN_ERR(svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Non-recursive relocation not supported")));
+ return svn_client_relocate2(path, from_prefix, to_prefix, TRUE, ctx, pool);
+}
+
+/*** From util.c ***/
+svn_error_t *
+svn_client_commit_item_create(const svn_client_commit_item3_t **item,
+ apr_pool_t *pool)
+{
+ *item = svn_client_commit_item3_create(pool);
+ return SVN_NO_ERROR;
+}
+
+svn_client_commit_item2_t *
+svn_client_commit_item2_dup(const svn_client_commit_item2_t *item,
+ apr_pool_t *pool)
+{
+ svn_client_commit_item2_t *new_item = apr_palloc(pool, sizeof(*new_item));
+
+ *new_item = *item;
+
+ if (new_item->path)
+ new_item->path = apr_pstrdup(pool, new_item->path);
+
+ if (new_item->url)
+ new_item->url = apr_pstrdup(pool, new_item->url);
+
+ if (new_item->copyfrom_url)
+ new_item->copyfrom_url = apr_pstrdup(pool, new_item->copyfrom_url);
+
+ if (new_item->wcprop_changes)
+ new_item->wcprop_changes = svn_prop_array_dup(new_item->wcprop_changes,
+ pool);
+
+ return new_item;
+}
+
diff --git a/subversion/libsvn_client/diff.c b/subversion/libsvn_client/diff.c
new file mode 100644
index 0000000..a5a36bd
--- /dev/null
+++ b/subversion/libsvn_client/diff.c
@@ -0,0 +1,2723 @@
+/*
+ * diff.c: comparing
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <apr_strings.h>
+#include <apr_pools.h>
+#include <apr_hash.h>
+#include "svn_types.h"
+#include "svn_hash.h"
+#include "svn_wc.h"
+#include "svn_diff.h"
+#include "svn_mergeinfo.h"
+#include "svn_client.h"
+#include "svn_string.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_io.h"
+#include "svn_utf.h"
+#include "svn_pools.h"
+#include "svn_config.h"
+#include "svn_props.h"
+#include "svn_subst.h"
+#include "client.h"
+
+#include "private/svn_wc_private.h"
+#include "private/svn_diff_private.h"
+#include "private/svn_subr_private.h"
+
+#include "svn_private_config.h"
+
+
+/* Utilities */
+
+
+#define MAKE_ERR_BAD_RELATIVE_PATH(path, relative_to_dir) \
+ svn_error_createf(SVN_ERR_BAD_RELATIVE_PATH, NULL, \
+ _("Path '%s' must be an immediate child of " \
+ "the directory '%s'"), path, relative_to_dir)
+
+/* Calculate the repository relative path of DIFF_RELPATH, using RA_SESSION
+ * and WC_CTX, and return the result in *REPOS_RELPATH.
+ * ORIG_TARGET is the related original target passed to the diff command,
+ * and may be used to derive leading path components missing from PATH.
+ * ANCHOR is the local path where the diff editor is anchored.
+ * Do all allocations in POOL. */
+static svn_error_t *
+make_repos_relpath(const char **repos_relpath,
+ const char *diff_relpath,
+ const char *orig_target,
+ svn_ra_session_t *ra_session,
+ svn_wc_context_t *wc_ctx,
+ const char *anchor,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *local_abspath;
+ const char *orig_repos_relpath = NULL;
+
+ if (! ra_session
+ || (anchor && !svn_path_is_url(orig_target)))
+ {
+ svn_error_t *err;
+ /* We're doing a WC-WC diff, so we can retrieve all information we
+ * need from the working copy. */
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath,
+ svn_dirent_join(anchor, diff_relpath,
+ scratch_pool),
+ scratch_pool));
+
+ err = svn_wc__node_get_repos_info(NULL, repos_relpath, NULL, NULL,
+ wc_ctx, local_abspath,
+ result_pool, scratch_pool);
+
+ if (!ra_session
+ || ! err
+ || (err && err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND))
+ {
+ return svn_error_trace(err);
+ }
+
+ /* The path represents a local working copy path, but does not
+ exist. Fall through to calculate an in-repository location
+ based on the ra session */
+
+ /* ### Maybe we should use the nearest existing ancestor instead? */
+ svn_error_clear(err);
+ }
+
+ {
+ const char *url;
+ const char *repos_root_url;
+
+ /* Would be nice if the RA layer could just provide the parent
+ repos_relpath of the ra session */
+ SVN_ERR(svn_ra_get_session_url(ra_session, &url, scratch_pool));
+
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root_url,
+ scratch_pool));
+
+ orig_repos_relpath = svn_uri_skip_ancestor(repos_root_url, url,
+ scratch_pool);
+
+ *repos_relpath = svn_relpath_join(orig_repos_relpath, diff_relpath,
+ result_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Adjust *INDEX_PATH, *ORIG_PATH_1 and *ORIG_PATH_2, representing the changed
+ * node and the two original targets passed to the diff command, to handle the
+ * case when we're dealing with different anchors. RELATIVE_TO_DIR is the
+ * directory the diff target should be considered relative to.
+ * ANCHOR is the local path where the diff editor is anchored. The resulting
+ * values are allocated in RESULT_POOL and temporary allocations are performed
+ * in SCRATCH_POOL. */
+static svn_error_t *
+adjust_paths_for_diff_labels(const char **index_path,
+ const char **orig_path_1,
+ const char **orig_path_2,
+ const char *relative_to_dir,
+ const char *anchor,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *new_path = *index_path;
+ const char *new_path1 = *orig_path_1;
+ const char *new_path2 = *orig_path_2;
+
+ if (anchor)
+ new_path = svn_dirent_join(anchor, new_path, result_pool);
+
+ if (relative_to_dir)
+ {
+ /* Possibly adjust the paths shown in the output (see issue #2723). */
+ const char *child_path = svn_dirent_is_child(relative_to_dir, new_path,
+ result_pool);
+
+ if (child_path)
+ new_path = child_path;
+ else if (! strcmp(relative_to_dir, new_path))
+ new_path = ".";
+ else
+ return MAKE_ERR_BAD_RELATIVE_PATH(new_path, relative_to_dir);
+
+ child_path = svn_dirent_is_child(relative_to_dir, new_path1,
+ result_pool);
+ }
+
+ {
+ apr_size_t len;
+ svn_boolean_t is_url1;
+ svn_boolean_t is_url2;
+ /* ### Holy cow. Due to anchor/target weirdness, we can't
+ simply join diff_cmd_baton->orig_path_1 with path, ditto for
+ orig_path_2. That will work when they're directory URLs, but
+ not for file URLs. Nor can we just use anchor1 and anchor2
+ from do_diff(), at least not without some more logic here.
+ What a nightmare.
+
+ For now, to distinguish the two paths, we'll just put the
+ unique portions of the original targets in parentheses after
+ the received path, with ellipses for handwaving. This makes
+ the labels a bit clumsy, but at least distinctive. Better
+ solutions are possible, they'll just take more thought. */
+
+ /* ### BH: We can now just construct the repos_relpath, etc. as the
+ anchor is available. See also make_repos_relpath() */
+
+ is_url1 = svn_path_is_url(new_path1);
+ is_url2 = svn_path_is_url(new_path2);
+
+ if (is_url1 && is_url2)
+ len = strlen(svn_uri_get_longest_ancestor(new_path1, new_path2,
+ scratch_pool));
+ else if (!is_url1 && !is_url2)
+ len = strlen(svn_dirent_get_longest_ancestor(new_path1, new_path2,
+ scratch_pool));
+ else
+ len = 0; /* Path and URL */
+
+ new_path1 += len;
+ new_path2 += len;
+ }
+
+ /* ### Should diff labels print paths in local style? Is there
+ already a standard for this? In any case, this code depends on
+ a particular style, so not calling svn_dirent_local_style() on the
+ paths below.*/
+
+ if (new_path[0] == '\0')
+ new_path = ".";
+
+ if (new_path1[0] == '\0')
+ new_path1 = new_path;
+ else if (svn_path_is_url(new_path1))
+ new_path1 = apr_psprintf(result_pool, "%s\t(%s)", new_path, new_path1);
+ else if (new_path1[0] == '/')
+ new_path1 = apr_psprintf(result_pool, "%s\t(...%s)", new_path, new_path1);
+ else
+ new_path1 = apr_psprintf(result_pool, "%s\t(.../%s)", new_path, new_path1);
+
+ if (new_path2[0] == '\0')
+ new_path2 = new_path;
+ else if (svn_path_is_url(new_path2))
+ new_path1 = apr_psprintf(result_pool, "%s\t(%s)", new_path, new_path2);
+ else if (new_path2[0] == '/')
+ new_path2 = apr_psprintf(result_pool, "%s\t(...%s)", new_path, new_path2);
+ else
+ new_path2 = apr_psprintf(result_pool, "%s\t(.../%s)", new_path, new_path2);
+
+ *index_path = new_path;
+ *orig_path_1 = new_path1;
+ *orig_path_2 = new_path2;
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Generate a label for the diff output for file PATH at revision REVNUM.
+ If REVNUM is invalid then it is assumed to be the current working
+ copy. Assumes the paths are already in the desired style (local
+ vs internal). Allocate the label in POOL. */
+static const char *
+diff_label(const char *path,
+ svn_revnum_t revnum,
+ apr_pool_t *pool)
+{
+ const char *label;
+ if (revnum != SVN_INVALID_REVNUM)
+ label = apr_psprintf(pool, _("%s\t(revision %ld)"), path, revnum);
+ else
+ label = apr_psprintf(pool, _("%s\t(working copy)"), path);
+
+ return label;
+}
+
+/* Print a git diff header for an addition within a diff between PATH1 and
+ * PATH2 to the stream OS using HEADER_ENCODING.
+ * All allocations are done in RESULT_POOL. */
+static svn_error_t *
+print_git_diff_header_added(svn_stream_t *os, const char *header_encoding,
+ const char *path1, const char *path2,
+ apr_pool_t *result_pool)
+{
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "diff --git a/%s b/%s%s",
+ path1, path2, APR_EOL_STR));
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "new file mode 10644" APR_EOL_STR));
+ return SVN_NO_ERROR;
+}
+
+/* Print a git diff header for a deletion within a diff between PATH1 and
+ * PATH2 to the stream OS using HEADER_ENCODING.
+ * All allocations are done in RESULT_POOL. */
+static svn_error_t *
+print_git_diff_header_deleted(svn_stream_t *os, const char *header_encoding,
+ const char *path1, const char *path2,
+ apr_pool_t *result_pool)
+{
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "diff --git a/%s b/%s%s",
+ path1, path2, APR_EOL_STR));
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "deleted file mode 10644"
+ APR_EOL_STR));
+ return SVN_NO_ERROR;
+}
+
+/* Print a git diff header for a copy from COPYFROM_PATH to PATH to the stream
+ * OS using HEADER_ENCODING. All allocations are done in RESULT_POOL. */
+static svn_error_t *
+print_git_diff_header_copied(svn_stream_t *os, const char *header_encoding,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_rev,
+ const char *path,
+ apr_pool_t *result_pool)
+{
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "diff --git a/%s b/%s%s",
+ copyfrom_path, path, APR_EOL_STR));
+ if (copyfrom_rev != SVN_INVALID_REVNUM)
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "copy from %s@%ld%s", copyfrom_path,
+ copyfrom_rev, APR_EOL_STR));
+ else
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "copy from %s%s", copyfrom_path,
+ APR_EOL_STR));
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "copy to %s%s", path, APR_EOL_STR));
+ return SVN_NO_ERROR;
+}
+
+/* Print a git diff header for a rename from COPYFROM_PATH to PATH to the
+ * stream OS using HEADER_ENCODING. All allocations are done in RESULT_POOL. */
+static svn_error_t *
+print_git_diff_header_renamed(svn_stream_t *os, const char *header_encoding,
+ const char *copyfrom_path, const char *path,
+ apr_pool_t *result_pool)
+{
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "diff --git a/%s b/%s%s",
+ copyfrom_path, path, APR_EOL_STR));
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "rename from %s%s", copyfrom_path,
+ APR_EOL_STR));
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "rename to %s%s", path, APR_EOL_STR));
+ return SVN_NO_ERROR;
+}
+
+/* Print a git diff header for a modification within a diff between PATH1 and
+ * PATH2 to the stream OS using HEADER_ENCODING.
+ * All allocations are done in RESULT_POOL. */
+static svn_error_t *
+print_git_diff_header_modified(svn_stream_t *os, const char *header_encoding,
+ const char *path1, const char *path2,
+ apr_pool_t *result_pool)
+{
+ SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool,
+ "diff --git a/%s b/%s%s",
+ path1, path2, APR_EOL_STR));
+ return SVN_NO_ERROR;
+}
+
+/* Print a git diff header showing the OPERATION to the stream OS using
+ * HEADER_ENCODING. Return suitable diff labels for the git diff in *LABEL1
+ * and *LABEL2. REPOS_RELPATH1 and REPOS_RELPATH2 are relative to reposroot.
+ * are the paths passed to the original diff command. REV1 and REV2 are
+ * revisions being diffed. COPYFROM_PATH and COPYFROM_REV indicate where the
+ * diffed item was copied from.
+ * Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+print_git_diff_header(svn_stream_t *os,
+ const char **label1, const char **label2,
+ svn_diff_operation_kind_t operation,
+ const char *repos_relpath1,
+ const char *repos_relpath2,
+ svn_revnum_t rev1,
+ svn_revnum_t rev2,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_rev,
+ const char *header_encoding,
+ apr_pool_t *scratch_pool)
+{
+ if (operation == svn_diff_op_deleted)
+ {
+ SVN_ERR(print_git_diff_header_deleted(os, header_encoding,
+ repos_relpath1, repos_relpath2,
+ scratch_pool));
+ *label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", repos_relpath1),
+ rev1, scratch_pool);
+ *label2 = diff_label("/dev/null", rev2, scratch_pool);
+
+ }
+ else if (operation == svn_diff_op_copied)
+ {
+ SVN_ERR(print_git_diff_header_copied(os, header_encoding,
+ copyfrom_path, copyfrom_rev,
+ repos_relpath2,
+ scratch_pool));
+ *label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", copyfrom_path),
+ rev1, scratch_pool);
+ *label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2),
+ rev2, scratch_pool);
+ }
+ else if (operation == svn_diff_op_added)
+ {
+ SVN_ERR(print_git_diff_header_added(os, header_encoding,
+ repos_relpath1, repos_relpath2,
+ scratch_pool));
+ *label1 = diff_label("/dev/null", rev1, scratch_pool);
+ *label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2),
+ rev2, scratch_pool);
+ }
+ else if (operation == svn_diff_op_modified)
+ {
+ SVN_ERR(print_git_diff_header_modified(os, header_encoding,
+ repos_relpath1, repos_relpath2,
+ scratch_pool));
+ *label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", repos_relpath1),
+ rev1, scratch_pool);
+ *label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2),
+ rev2, scratch_pool);
+ }
+ else if (operation == svn_diff_op_moved)
+ {
+ SVN_ERR(print_git_diff_header_renamed(os, header_encoding,
+ copyfrom_path, repos_relpath2,
+ scratch_pool));
+ *label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", copyfrom_path),
+ rev1, scratch_pool);
+ *label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2),
+ rev2, scratch_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* A helper func that writes out verbal descriptions of property diffs
+ to OUTSTREAM. Of course, OUTSTREAM will probably be whatever was
+ passed to svn_client_diff6(), which is probably stdout.
+
+ ### FIXME needs proper docstring
+
+ If USE_GIT_DIFF_FORMAT is TRUE, pring git diff headers, which always
+ show paths relative to the repository root. RA_SESSION and WC_CTX are
+ needed to normalize paths relative the repository root, and are ignored
+ if USE_GIT_DIFF_FORMAT is FALSE.
+
+ ANCHOR is the local path where the diff editor is anchored. */
+static svn_error_t *
+display_prop_diffs(const apr_array_header_t *propchanges,
+ apr_hash_t *original_props,
+ const char *diff_relpath,
+ const char *anchor,
+ const char *orig_path1,
+ const char *orig_path2,
+ svn_revnum_t rev1,
+ svn_revnum_t rev2,
+ const char *encoding,
+ svn_stream_t *outstream,
+ const char *relative_to_dir,
+ svn_boolean_t show_diff_header,
+ svn_boolean_t use_git_diff_format,
+ svn_ra_session_t *ra_session,
+ svn_wc_context_t *wc_ctx,
+ apr_pool_t *scratch_pool)
+{
+ const char *repos_relpath1 = NULL;
+ const char *repos_relpath2 = NULL;
+ const char *index_path = diff_relpath;
+ const char *adjusted_path1 = orig_path1;
+ const char *adjusted_path2 = orig_path2;
+
+ if (use_git_diff_format)
+ {
+ SVN_ERR(make_repos_relpath(&repos_relpath1, diff_relpath, orig_path1,
+ ra_session, wc_ctx, anchor,
+ scratch_pool, scratch_pool));
+ SVN_ERR(make_repos_relpath(&repos_relpath2, diff_relpath, orig_path2,
+ ra_session, wc_ctx, anchor,
+ scratch_pool, scratch_pool));
+ }
+
+ /* If we're creating a diff on the wc root, path would be empty. */
+ SVN_ERR(adjust_paths_for_diff_labels(&index_path, &adjusted_path1,
+ &adjusted_path2,
+ relative_to_dir, anchor,
+ scratch_pool, scratch_pool));
+
+ if (show_diff_header)
+ {
+ const char *label1;
+ const char *label2;
+
+ label1 = diff_label(adjusted_path1, rev1, scratch_pool);
+ label2 = diff_label(adjusted_path2, rev2, scratch_pool);
+
+ /* ### Should we show the paths in platform specific format,
+ * ### diff_content_changed() does not! */
+
+ SVN_ERR(svn_stream_printf_from_utf8(outstream, encoding, scratch_pool,
+ "Index: %s" APR_EOL_STR
+ SVN_DIFF__EQUAL_STRING APR_EOL_STR,
+ index_path));
+
+ if (use_git_diff_format)
+ SVN_ERR(print_git_diff_header(outstream, &label1, &label2,
+ svn_diff_op_modified,
+ repos_relpath1, repos_relpath2,
+ rev1, rev2, NULL,
+ SVN_INVALID_REVNUM,
+ encoding, scratch_pool));
+
+ /* --- label1
+ * +++ label2 */
+ SVN_ERR(svn_diff__unidiff_write_header(
+ outstream, encoding, label1, label2, scratch_pool));
+ }
+
+ SVN_ERR(svn_stream_printf_from_utf8(outstream, encoding, scratch_pool,
+ _("%sProperty changes on: %s%s"),
+ APR_EOL_STR,
+ use_git_diff_format
+ ? repos_relpath1
+ : index_path,
+ APR_EOL_STR));
+
+ SVN_ERR(svn_stream_printf_from_utf8(outstream, encoding, scratch_pool,
+ SVN_DIFF__UNDER_STRING APR_EOL_STR));
+
+ SVN_ERR(svn_diff__display_prop_diffs(
+ outstream, encoding, propchanges, original_props,
+ TRUE /* pretty_print_mergeinfo */, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/*-----------------------------------------------------------------*/
+
+/*** Callbacks for 'svn diff', invoked by the repos-diff editor. ***/
+
+
+struct diff_cmd_baton {
+
+ /* If non-null, the external diff command to invoke. */
+ const char *diff_cmd;
+
+ /* This is allocated in this struct's pool or a higher-up pool. */
+ union {
+ /* If 'diff_cmd' is null, then this is the parsed options to
+ pass to the internal libsvn_diff implementation. */
+ svn_diff_file_options_t *for_internal;
+ /* Else if 'diff_cmd' is non-null, then... */
+ struct {
+ /* ...this is an argument array for the external command, and */
+ const char **argv;
+ /* ...this is the length of argv. */
+ int argc;
+ } for_external;
+ } options;
+
+ apr_pool_t *pool;
+ svn_stream_t *outstream;
+ svn_stream_t *errstream;
+
+ const char *header_encoding;
+
+ /* The original targets passed to the diff command. We may need
+ these to construct distinctive diff labels when comparing the
+ same relative path in the same revision, under different anchors
+ (for example, when comparing a trunk against a branch). */
+ const char *orig_path_1;
+ const char *orig_path_2;
+
+ /* These are the numeric representations of the revisions passed to
+ svn_client_diff6(), either may be SVN_INVALID_REVNUM. We need these
+ because some of the svn_wc_diff_callbacks4_t don't get revision
+ arguments.
+
+ ### Perhaps we should change the callback signatures and eliminate
+ ### these?
+ */
+ svn_revnum_t revnum1;
+ svn_revnum_t revnum2;
+
+ /* Set this if you want diff output even for binary files. */
+ svn_boolean_t force_binary;
+
+ /* The directory that diff target paths should be considered as
+ relative to for output generation (see issue #2723). */
+ const char *relative_to_dir;
+
+ /* Whether property differences are ignored. */
+ svn_boolean_t ignore_properties;
+
+ /* Whether to show only property changes. */
+ svn_boolean_t properties_only;
+
+ /* Whether we're producing a git-style diff. */
+ svn_boolean_t use_git_diff_format;
+
+ /* Whether addition of a file is summarized versus showing a full diff. */
+ svn_boolean_t no_diff_added;
+
+ /* Whether deletion of a file is summarized versus showing a full diff. */
+ svn_boolean_t no_diff_deleted;
+
+ /* Whether to ignore copyfrom information when showing adds */
+ svn_boolean_t no_copyfrom_on_add;
+
+ /* Empty files for creating diffs or NULL if not used yet */
+ const char *empty_file;
+
+ svn_wc_context_t *wc_ctx;
+
+ /* The RA session used during diffs involving the repository. */
+ svn_ra_session_t *ra_session;
+
+ /* The anchor to prefix before wc paths */
+ const char *anchor;
+
+ /* Whether the local diff target of a repos->wc diff is a copy. */
+ svn_boolean_t repos_wc_diff_target_is_copy;
+};
+
+/* An helper for diff_dir_props_changed, diff_file_changed and diff_file_added
+ */
+static svn_error_t *
+diff_props_changed(const char *diff_relpath,
+ svn_revnum_t rev1,
+ svn_revnum_t rev2,
+ svn_boolean_t dir_was_added,
+ const apr_array_header_t *propchanges,
+ apr_hash_t *original_props,
+ svn_boolean_t show_diff_header,
+ struct diff_cmd_baton *diff_cmd_baton,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *props;
+
+ /* If property differences are ignored, there's nothing to do. */
+ if (diff_cmd_baton->ignore_properties)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(svn_categorize_props(propchanges, NULL, NULL, &props,
+ scratch_pool));
+
+ if (props->nelts > 0)
+ {
+ /* We're using the revnums from the diff_cmd_baton since there's
+ * no revision argument to the svn_wc_diff_callback_t
+ * dir_props_changed(). */
+ SVN_ERR(display_prop_diffs(props, original_props,
+ diff_relpath,
+ diff_cmd_baton->anchor,
+ diff_cmd_baton->orig_path_1,
+ diff_cmd_baton->orig_path_2,
+ rev1,
+ rev2,
+ diff_cmd_baton->header_encoding,
+ diff_cmd_baton->outstream,
+ diff_cmd_baton->relative_to_dir,
+ show_diff_header,
+ diff_cmd_baton->use_git_diff_format,
+ diff_cmd_baton->ra_session,
+ diff_cmd_baton->wc_ctx,
+ scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_wc_diff_callbacks4_t function. */
+static svn_error_t *
+diff_dir_props_changed(svn_wc_notify_state_t *state,
+ svn_boolean_t *tree_conflicted,
+ const char *diff_relpath,
+ svn_boolean_t dir_was_added,
+ const apr_array_header_t *propchanges,
+ apr_hash_t *original_props,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct diff_cmd_baton *diff_cmd_baton = diff_baton;
+
+ return svn_error_trace(diff_props_changed(diff_relpath,
+ /* ### These revs be filled
+ * ### with per node info */
+ dir_was_added
+ ? 0 /* Magic legacy value */
+ : diff_cmd_baton->revnum1,
+ diff_cmd_baton->revnum2,
+ dir_was_added,
+ propchanges,
+ original_props,
+ TRUE /* show_diff_header */,
+ diff_cmd_baton,
+ scratch_pool));
+}
+
+
+/* Show differences between TMPFILE1 and TMPFILE2. DIFF_RELPATH, REV1, and
+ REV2 are used in the headers to indicate the file and revisions. If either
+ MIMETYPE1 or MIMETYPE2 indicate binary content, don't show a diff,
+ but instead print a warning message.
+
+ If FORCE_DIFF is TRUE, always write a diff, even for empty diffs.
+
+ Set *WROTE_HEADER to TRUE if a diff header was written */
+static svn_error_t *
+diff_content_changed(svn_boolean_t *wrote_header,
+ const char *diff_relpath,
+ const char *tmpfile1,
+ const char *tmpfile2,
+ svn_revnum_t rev1,
+ svn_revnum_t rev2,
+ const char *mimetype1,
+ const char *mimetype2,
+ svn_diff_operation_kind_t operation,
+ svn_boolean_t force_diff,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_rev,
+ struct diff_cmd_baton *diff_cmd_baton,
+ apr_pool_t *scratch_pool)
+{
+ int exitcode;
+ const char *rel_to_dir = diff_cmd_baton->relative_to_dir;
+ svn_stream_t *errstream = diff_cmd_baton->errstream;
+ svn_stream_t *outstream = diff_cmd_baton->outstream;
+ const char *label1, *label2;
+ svn_boolean_t mt1_binary = FALSE, mt2_binary = FALSE;
+ const char *index_path = diff_relpath;
+ const char *path1 = diff_cmd_baton->orig_path_1;
+ const char *path2 = diff_cmd_baton->orig_path_2;
+
+ /* If only property differences are shown, there's nothing to do. */
+ if (diff_cmd_baton->properties_only)
+ return SVN_NO_ERROR;
+
+ /* Generate the diff headers. */
+ SVN_ERR(adjust_paths_for_diff_labels(&index_path, &path1, &path2,
+ rel_to_dir, diff_cmd_baton->anchor,
+ scratch_pool, scratch_pool));
+
+ label1 = diff_label(path1, rev1, scratch_pool);
+ label2 = diff_label(path2, rev2, scratch_pool);
+
+ /* Possible easy-out: if either mime-type is binary and force was not
+ specified, don't attempt to generate a viewable diff at all.
+ Print a warning and exit. */
+ if (mimetype1)
+ mt1_binary = svn_mime_type_is_binary(mimetype1);
+ if (mimetype2)
+ mt2_binary = svn_mime_type_is_binary(mimetype2);
+
+ if (! diff_cmd_baton->force_binary && (mt1_binary || mt2_binary))
+ {
+ /* Print out the diff header. */
+ SVN_ERR(svn_stream_printf_from_utf8(outstream,
+ diff_cmd_baton->header_encoding, scratch_pool,
+ "Index: %s" APR_EOL_STR
+ SVN_DIFF__EQUAL_STRING APR_EOL_STR,
+ index_path));
+
+ /* ### Print git diff headers. */
+
+ SVN_ERR(svn_stream_printf_from_utf8(outstream,
+ diff_cmd_baton->header_encoding, scratch_pool,
+ _("Cannot display: file marked as a binary type.%s"),
+ APR_EOL_STR));
+
+ if (mt1_binary && !mt2_binary)
+ SVN_ERR(svn_stream_printf_from_utf8(outstream,
+ diff_cmd_baton->header_encoding, scratch_pool,
+ "svn:mime-type = %s" APR_EOL_STR, mimetype1));
+ else if (mt2_binary && !mt1_binary)
+ SVN_ERR(svn_stream_printf_from_utf8(outstream,
+ diff_cmd_baton->header_encoding, scratch_pool,
+ "svn:mime-type = %s" APR_EOL_STR, mimetype2));
+ else if (mt1_binary && mt2_binary)
+ {
+ if (strcmp(mimetype1, mimetype2) == 0)
+ SVN_ERR(svn_stream_printf_from_utf8(outstream,
+ diff_cmd_baton->header_encoding, scratch_pool,
+ "svn:mime-type = %s" APR_EOL_STR,
+ mimetype1));
+ else
+ SVN_ERR(svn_stream_printf_from_utf8(outstream,
+ diff_cmd_baton->header_encoding, scratch_pool,
+ "svn:mime-type = (%s, %s)" APR_EOL_STR,
+ mimetype1, mimetype2));
+ }
+
+ /* Exit early. */
+ return SVN_NO_ERROR;
+ }
+
+
+ if (diff_cmd_baton->diff_cmd)
+ {
+ apr_file_t *outfile;
+ apr_file_t *errfile;
+ const char *outfilename;
+ const char *errfilename;
+ svn_stream_t *stream;
+
+ /* Print out the diff header. */
+ SVN_ERR(svn_stream_printf_from_utf8(outstream,
+ diff_cmd_baton->header_encoding, scratch_pool,
+ "Index: %s" APR_EOL_STR
+ SVN_DIFF__EQUAL_STRING APR_EOL_STR,
+ index_path));
+
+ /* ### Do we want to add git diff headers here too? I'd say no. The
+ * ### 'Index' and '===' line is something subversion has added. The rest
+ * ### is up to the external diff application. We may be dealing with
+ * ### a non-git compatible diff application.*/
+
+ /* We deal in streams, but svn_io_run_diff2() deals in file handles,
+ unfortunately, so we need to make these temporary files, and then
+ copy the contents to our stream. */
+ SVN_ERR(svn_io_open_unique_file3(&outfile, &outfilename, NULL,
+ svn_io_file_del_on_pool_cleanup,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_io_open_unique_file3(&errfile, &errfilename, NULL,
+ svn_io_file_del_on_pool_cleanup,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(svn_io_run_diff2(".",
+ diff_cmd_baton->options.for_external.argv,
+ diff_cmd_baton->options.for_external.argc,
+ label1, label2,
+ tmpfile1, tmpfile2,
+ &exitcode, outfile, errfile,
+ diff_cmd_baton->diff_cmd, scratch_pool));
+
+ SVN_ERR(svn_io_file_close(outfile, scratch_pool));
+ SVN_ERR(svn_io_file_close(errfile, scratch_pool));
+
+ /* Now, open and copy our files to our output streams. */
+ SVN_ERR(svn_stream_open_readonly(&stream, outfilename,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_stream_copy3(stream, svn_stream_disown(outstream,
+ scratch_pool),
+ NULL, NULL, scratch_pool));
+ SVN_ERR(svn_stream_open_readonly(&stream, errfilename,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_stream_copy3(stream, svn_stream_disown(errstream,
+ scratch_pool),
+ NULL, NULL, scratch_pool));
+
+ /* We have a printed a diff for this path, mark it as visited. */
+ *wrote_header = TRUE;
+ }
+ else /* use libsvn_diff to generate the diff */
+ {
+ svn_diff_t *diff;
+
+ SVN_ERR(svn_diff_file_diff_2(&diff, tmpfile1, tmpfile2,
+ diff_cmd_baton->options.for_internal,
+ scratch_pool));
+
+ if (force_diff
+ || diff_cmd_baton->use_git_diff_format
+ || svn_diff_contains_diffs(diff))
+ {
+ /* Print out the diff header. */
+ SVN_ERR(svn_stream_printf_from_utf8(outstream,
+ diff_cmd_baton->header_encoding, scratch_pool,
+ "Index: %s" APR_EOL_STR
+ SVN_DIFF__EQUAL_STRING APR_EOL_STR,
+ index_path));
+
+ if (diff_cmd_baton->use_git_diff_format)
+ {
+ const char *repos_relpath1;
+ const char *repos_relpath2;
+ SVN_ERR(make_repos_relpath(&repos_relpath1, diff_relpath,
+ diff_cmd_baton->orig_path_1,
+ diff_cmd_baton->ra_session,
+ diff_cmd_baton->wc_ctx,
+ diff_cmd_baton->anchor,
+ scratch_pool, scratch_pool));
+ SVN_ERR(make_repos_relpath(&repos_relpath2, diff_relpath,
+ diff_cmd_baton->orig_path_2,
+ diff_cmd_baton->ra_session,
+ diff_cmd_baton->wc_ctx,
+ diff_cmd_baton->anchor,
+ scratch_pool, scratch_pool));
+ SVN_ERR(print_git_diff_header(outstream, &label1, &label2,
+ operation,
+ repos_relpath1, repos_relpath2,
+ rev1, rev2,
+ copyfrom_path,
+ copyfrom_rev,
+ diff_cmd_baton->header_encoding,
+ scratch_pool));
+ }
+
+ /* Output the actual diff */
+ if (force_diff || svn_diff_contains_diffs(diff))
+ SVN_ERR(svn_diff_file_output_unified3(outstream, diff,
+ tmpfile1, tmpfile2, label1, label2,
+ diff_cmd_baton->header_encoding, rel_to_dir,
+ diff_cmd_baton->options.for_internal->show_c_function,
+ scratch_pool));
+
+ /* We have a printed a diff for this path, mark it as visited. */
+ *wrote_header = TRUE;
+ }
+ }
+
+ /* ### todo: someday we'll need to worry about whether we're going
+ to need to write a diff plug-in mechanism that makes use of the
+ two paths, instead of just blindly running SVN_CLIENT_DIFF. */
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+diff_file_opened(svn_boolean_t *tree_conflicted,
+ svn_boolean_t *skip,
+ const char *diff_relpath,
+ svn_revnum_t rev,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ return SVN_NO_ERROR;
+}
+
+/* An svn_wc_diff_callbacks4_t function. */
+static svn_error_t *
+diff_file_changed(svn_wc_notify_state_t *content_state,
+ svn_wc_notify_state_t *prop_state,
+ svn_boolean_t *tree_conflicted,
+ const char *diff_relpath,
+ const char *tmpfile1,
+ const char *tmpfile2,
+ svn_revnum_t rev1,
+ svn_revnum_t rev2,
+ const char *mimetype1,
+ const char *mimetype2,
+ const apr_array_header_t *prop_changes,
+ apr_hash_t *original_props,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct diff_cmd_baton *diff_cmd_baton = diff_baton;
+ svn_boolean_t wrote_header = FALSE;
+
+ /* During repos->wc diff of a copy revision numbers obtained
+ * from the working copy are always SVN_INVALID_REVNUM. */
+ if (diff_cmd_baton->repos_wc_diff_target_is_copy)
+ {
+ if (rev1 == SVN_INVALID_REVNUM &&
+ diff_cmd_baton->revnum1 != SVN_INVALID_REVNUM)
+ rev1 = diff_cmd_baton->revnum1;
+
+ if (rev2 == SVN_INVALID_REVNUM &&
+ diff_cmd_baton->revnum2 != SVN_INVALID_REVNUM)
+ rev2 = diff_cmd_baton->revnum2;
+ }
+
+ if (tmpfile1)
+ SVN_ERR(diff_content_changed(&wrote_header, diff_relpath,
+ tmpfile1, tmpfile2, rev1, rev2,
+ mimetype1, mimetype2,
+ svn_diff_op_modified, FALSE,
+ NULL,
+ SVN_INVALID_REVNUM, diff_cmd_baton,
+ scratch_pool));
+ if (prop_changes->nelts > 0)
+ SVN_ERR(diff_props_changed(diff_relpath, rev1, rev2, FALSE, prop_changes,
+ original_props, !wrote_header,
+ diff_cmd_baton, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* Because the repos-diff editor passes at least one empty file to
+ each of these next two functions, they can be dumb wrappers around
+ the main workhorse routine. */
+
+/* An svn_wc_diff_callbacks4_t function. */
+static svn_error_t *
+diff_file_added(svn_wc_notify_state_t *content_state,
+ svn_wc_notify_state_t *prop_state,
+ svn_boolean_t *tree_conflicted,
+ const char *diff_relpath,
+ const char *tmpfile1,
+ const char *tmpfile2,
+ svn_revnum_t rev1,
+ svn_revnum_t rev2,
+ const char *mimetype1,
+ const char *mimetype2,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_revision,
+ const apr_array_header_t *prop_changes,
+ apr_hash_t *original_props,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct diff_cmd_baton *diff_cmd_baton = diff_baton;
+ svn_boolean_t wrote_header = FALSE;
+
+ /* During repos->wc diff of a copy revision numbers obtained
+ * from the working copy are always SVN_INVALID_REVNUM. */
+ if (diff_cmd_baton->repos_wc_diff_target_is_copy)
+ {
+ if (rev1 == SVN_INVALID_REVNUM &&
+ diff_cmd_baton->revnum1 != SVN_INVALID_REVNUM)
+ rev1 = diff_cmd_baton->revnum1;
+
+ if (rev2 == SVN_INVALID_REVNUM &&
+ diff_cmd_baton->revnum2 != SVN_INVALID_REVNUM)
+ rev2 = diff_cmd_baton->revnum2;
+ }
+
+ if (diff_cmd_baton->no_copyfrom_on_add
+ && (copyfrom_path || SVN_IS_VALID_REVNUM(copyfrom_revision)))
+ {
+ apr_hash_t *empty_hash = apr_hash_make(scratch_pool);
+ apr_array_header_t *new_changes;
+
+ /* Rebase changes on having no left source. */
+ if (!diff_cmd_baton->empty_file)
+ SVN_ERR(svn_io_open_unique_file3(NULL, &diff_cmd_baton->empty_file,
+ NULL, svn_io_file_del_on_pool_cleanup,
+ diff_cmd_baton->pool, scratch_pool));
+
+ SVN_ERR(svn_prop_diffs(&new_changes,
+ svn_prop__patch(original_props, prop_changes,
+ scratch_pool),
+ empty_hash,
+ scratch_pool));
+
+ tmpfile1 = diff_cmd_baton->empty_file;
+ prop_changes = new_changes;
+ original_props = empty_hash;
+ copyfrom_revision = SVN_INVALID_REVNUM;
+ }
+
+ if (diff_cmd_baton->no_diff_added)
+ {
+ const char *index_path = diff_relpath;
+
+ if (diff_cmd_baton->anchor)
+ index_path = svn_dirent_join(diff_cmd_baton->anchor, diff_relpath,
+ scratch_pool);
+
+ SVN_ERR(svn_stream_printf_from_utf8(diff_cmd_baton->outstream,
+ diff_cmd_baton->header_encoding, scratch_pool,
+ "Index: %s (added)" APR_EOL_STR
+ SVN_DIFF__EQUAL_STRING APR_EOL_STR,
+ index_path));
+ wrote_header = TRUE;
+ }
+ else if (tmpfile1 && copyfrom_path)
+ SVN_ERR(diff_content_changed(&wrote_header, diff_relpath,
+ tmpfile1, tmpfile2, rev1, rev2,
+ mimetype1, mimetype2,
+ svn_diff_op_copied,
+ TRUE /* force diff output */,
+ copyfrom_path,
+ copyfrom_revision, diff_cmd_baton,
+ scratch_pool));
+ else if (tmpfile1)
+ SVN_ERR(diff_content_changed(&wrote_header, diff_relpath,
+ tmpfile1, tmpfile2, rev1, rev2,
+ mimetype1, mimetype2,
+ svn_diff_op_added,
+ TRUE /* force diff output */,
+ NULL, SVN_INVALID_REVNUM,
+ diff_cmd_baton, scratch_pool));
+
+ if (prop_changes->nelts > 0)
+ SVN_ERR(diff_props_changed(diff_relpath, rev1, rev2,
+ FALSE, prop_changes,
+ original_props, ! wrote_header,
+ diff_cmd_baton, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_wc_diff_callbacks4_t function. */
+static svn_error_t *
+diff_file_deleted(svn_wc_notify_state_t *state,
+ svn_boolean_t *tree_conflicted,
+ const char *diff_relpath,
+ const char *tmpfile1,
+ const char *tmpfile2,
+ const char *mimetype1,
+ const char *mimetype2,
+ apr_hash_t *original_props,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct diff_cmd_baton *diff_cmd_baton = diff_baton;
+
+ if (diff_cmd_baton->no_diff_deleted)
+ {
+ const char *index_path = diff_relpath;
+
+ if (diff_cmd_baton->anchor)
+ index_path = svn_dirent_join(diff_cmd_baton->anchor, diff_relpath,
+ scratch_pool);
+
+ SVN_ERR(svn_stream_printf_from_utf8(diff_cmd_baton->outstream,
+ diff_cmd_baton->header_encoding, scratch_pool,
+ "Index: %s (deleted)" APR_EOL_STR
+ SVN_DIFF__EQUAL_STRING APR_EOL_STR,
+ index_path));
+ }
+ else
+ {
+ svn_boolean_t wrote_header = FALSE;
+ if (tmpfile1)
+ SVN_ERR(diff_content_changed(&wrote_header, diff_relpath,
+ tmpfile1, tmpfile2,
+ diff_cmd_baton->revnum1,
+ diff_cmd_baton->revnum2,
+ mimetype1, mimetype2,
+ svn_diff_op_deleted, FALSE,
+ NULL, SVN_INVALID_REVNUM,
+ diff_cmd_baton,
+ scratch_pool));
+
+ /* Should we also report the properties as deleted? */
+ }
+
+ /* We don't list all the deleted properties. */
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_wc_diff_callbacks4_t function. */
+static svn_error_t *
+diff_dir_added(svn_wc_notify_state_t *state,
+ svn_boolean_t *tree_conflicted,
+ svn_boolean_t *skip,
+ svn_boolean_t *skip_children,
+ const char *diff_relpath,
+ svn_revnum_t rev,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_revision,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ /* Do nothing. */
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_wc_diff_callbacks4_t function. */
+static svn_error_t *
+diff_dir_deleted(svn_wc_notify_state_t *state,
+ svn_boolean_t *tree_conflicted,
+ const char *diff_relpath,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ /* Do nothing. */
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_wc_diff_callbacks4_t function. */
+static svn_error_t *
+diff_dir_opened(svn_boolean_t *tree_conflicted,
+ svn_boolean_t *skip,
+ svn_boolean_t *skip_children,
+ const char *diff_relpath,
+ svn_revnum_t rev,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ /* Do nothing. */
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_wc_diff_callbacks4_t function. */
+static svn_error_t *
+diff_dir_closed(svn_wc_notify_state_t *contentstate,
+ svn_wc_notify_state_t *propstate,
+ svn_boolean_t *tree_conflicted,
+ const char *diff_relpath,
+ svn_boolean_t dir_was_added,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ /* Do nothing. */
+
+ return SVN_NO_ERROR;
+}
+
+static const svn_wc_diff_callbacks4_t diff_callbacks =
+{
+ diff_file_opened,
+ diff_file_changed,
+ diff_file_added,
+ diff_file_deleted,
+ diff_dir_deleted,
+ diff_dir_opened,
+ diff_dir_added,
+ diff_dir_props_changed,
+ diff_dir_closed
+};
+
+/*-----------------------------------------------------------------*/
+
+/** The logic behind 'svn diff' and 'svn merge'. */
+
+
+/* Hi! This is a comment left behind by Karl, and Ben is too afraid
+ to erase it at this time, because he's not fully confident that all
+ this knowledge has been grokked yet.
+
+ There are five cases:
+ 1. path is not a URL and start_revision != end_revision
+ 2. path is not a URL and start_revision == end_revision
+ 3. path is a URL and start_revision != end_revision
+ 4. path is a URL and start_revision == end_revision
+ 5. path is not a URL and no revisions given
+
+ With only one distinct revision the working copy provides the
+ other. When path is a URL there is no working copy. Thus
+
+ 1: compare repository versions for URL coresponding to working copy
+ 2: compare working copy against repository version
+ 3: compare repository versions for URL
+ 4: nothing to do.
+ 5: compare working copy against text-base
+
+ Case 4 is not as stupid as it looks, for example it may occur if
+ the user specifies two dates that resolve to the same revision. */
+
+
+/** Check if paths PATH_OR_URL1 and PATH_OR_URL2 are urls and if the
+ * revisions REVISION1 and REVISION2 are local. If PEG_REVISION is not
+ * unspecified, ensure that at least one of the two revisions is not
+ * BASE or WORKING.
+ * If PATH_OR_URL1 can only be found in the repository, set *IS_REPOS1
+ * to TRUE. If PATH_OR_URL2 can only be found in the repository, set
+ * *IS_REPOS2 to TRUE. */
+static svn_error_t *
+check_paths(svn_boolean_t *is_repos1,
+ svn_boolean_t *is_repos2,
+ const char *path_or_url1,
+ const char *path_or_url2,
+ const svn_opt_revision_t *revision1,
+ const svn_opt_revision_t *revision2,
+ const svn_opt_revision_t *peg_revision)
+{
+ svn_boolean_t is_local_rev1, is_local_rev2;
+
+ /* Verify our revision arguments in light of the paths. */
+ if ((revision1->kind == svn_opt_revision_unspecified)
+ || (revision2->kind == svn_opt_revision_unspecified))
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Not all required revisions are specified"));
+
+ /* Revisions can be said to be local or remote.
+ * BASE and WORKING are local revisions. */
+ is_local_rev1 =
+ ((revision1->kind == svn_opt_revision_base)
+ || (revision1->kind == svn_opt_revision_working));
+ is_local_rev2 =
+ ((revision2->kind == svn_opt_revision_base)
+ || (revision2->kind == svn_opt_revision_working));
+
+ if (peg_revision->kind != svn_opt_revision_unspecified &&
+ is_local_rev1 && is_local_rev2)
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("At least one revision must be something other "
+ "than BASE or WORKING when diffing a URL"));
+
+ /* Working copy paths with non-local revisions get turned into
+ URLs. We don't do that here, though. We simply record that it
+ needs to be done, which is information that helps us choose our
+ diff helper function. */
+ *is_repos1 = ! is_local_rev1 || svn_path_is_url(path_or_url1);
+ *is_repos2 = ! is_local_rev2 || svn_path_is_url(path_or_url2);
+
+ return SVN_NO_ERROR;
+}
+
+/* Raise an error if the diff target URL does not exist at REVISION.
+ * If REVISION does not equal OTHER_REVISION, mention both revisions in
+ * the error message. Use RA_SESSION to contact the repository.
+ * Use POOL for temporary allocations. */
+static svn_error_t *
+check_diff_target_exists(const char *url,
+ svn_revnum_t revision,
+ svn_revnum_t other_revision,
+ svn_ra_session_t *ra_session,
+ apr_pool_t *pool)
+{
+ svn_node_kind_t kind;
+ const char *session_url;
+
+ SVN_ERR(svn_ra_get_session_url(ra_session, &session_url, pool));
+
+ if (strcmp(url, session_url) != 0)
+ SVN_ERR(svn_ra_reparent(ra_session, url, pool));
+
+ SVN_ERR(svn_ra_check_path(ra_session, "", revision, &kind, pool));
+ if (kind == svn_node_none)
+ {
+ if (revision == other_revision)
+ return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
+ _("Diff target '%s' was not found in the "
+ "repository at revision '%ld'"),
+ url, revision);
+ else
+ return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
+ _("Diff target '%s' was not found in the "
+ "repository at revision '%ld' or '%ld'"),
+ url, revision, other_revision);
+ }
+
+ if (strcmp(url, session_url) != 0)
+ SVN_ERR(svn_ra_reparent(ra_session, session_url, pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Return in *RESOLVED_URL the URL which PATH_OR_URL@PEG_REVISION has in
+ * REVISION. If the object has no location in REVISION, set *RESOLVED_URL
+ * to NULL. */
+static svn_error_t *
+resolve_pegged_diff_target_url(const char **resolved_url,
+ svn_ra_session_t *ra_session,
+ const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err;
+
+ /* Check if the PATH_OR_URL exists at REVISION. */
+ err = svn_client__repos_locations(resolved_url, NULL,
+ NULL, NULL,
+ ra_session,
+ path_or_url,
+ peg_revision,
+ revision,
+ NULL,
+ ctx, scratch_pool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_CLIENT_UNRELATED_RESOURCES ||
+ err->apr_err == SVN_ERR_FS_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ *resolved_url = NULL;
+ }
+ else
+ return svn_error_trace(err);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/** Prepare a repos repos diff between PATH_OR_URL1 and
+ * PATH_OR_URL2@PEG_REVISION, in the revision range REVISION1:REVISION2.
+ * Return URLs and peg revisions in *URL1, *REV1 and in *URL2, *REV2.
+ * Return suitable anchors in *ANCHOR1 and *ANCHOR2, and targets in
+ * *TARGET1 and *TARGET2, based on *URL1 and *URL2.
+ * Indicate the corresponding node kinds in *KIND1 and *KIND2, and verify
+ * that at least one of the diff targets exists.
+ * Set *BASE_PATH corresponding to the URL opened in the new *RA_SESSION
+ * which is pointing at *ANCHOR1.
+ * Use client context CTX. Do all allocations in POOL. */
+static svn_error_t *
+diff_prepare_repos_repos(const char **url1,
+ const char **url2,
+ const char **base_path,
+ svn_revnum_t *rev1,
+ svn_revnum_t *rev2,
+ const char **anchor1,
+ const char **anchor2,
+ const char **target1,
+ const char **target2,
+ svn_node_kind_t *kind1,
+ svn_node_kind_t *kind2,
+ svn_ra_session_t **ra_session,
+ svn_client_ctx_t *ctx,
+ const char *path_or_url1,
+ const char *path_or_url2,
+ const svn_opt_revision_t *revision1,
+ const svn_opt_revision_t *revision2,
+ const svn_opt_revision_t *peg_revision,
+ apr_pool_t *pool)
+{
+ const char *abspath_or_url2;
+ const char *abspath_or_url1;
+ const char *repos_root_url;
+ const char *wri_abspath = NULL;
+
+ if (!svn_path_is_url(path_or_url2))
+ {
+ SVN_ERR(svn_dirent_get_absolute(&abspath_or_url2, path_or_url2, pool));
+ SVN_ERR(svn_wc__node_get_url(url2, ctx->wc_ctx, abspath_or_url2,
+ pool, pool));
+ wri_abspath = abspath_or_url2;
+ }
+ else
+ *url2 = abspath_or_url2 = apr_pstrdup(pool, path_or_url2);
+
+ if (!svn_path_is_url(path_or_url1))
+ {
+ SVN_ERR(svn_dirent_get_absolute(&abspath_or_url1, path_or_url1, pool));
+ SVN_ERR(svn_wc__node_get_url(url1, ctx->wc_ctx, abspath_or_url1,
+ pool, pool));
+ wri_abspath = abspath_or_url1;
+ }
+ else
+ *url1 = abspath_or_url1 = apr_pstrdup(pool, path_or_url1);
+
+ /* We need exactly one BASE_PATH, so we'll let the BASE_PATH
+ calculated for PATH_OR_URL2 override the one for PATH_OR_URL1
+ (since the diff will be "applied" to URL2 anyway). */
+ *base_path = NULL;
+ if (strcmp(*url1, path_or_url1) != 0)
+ *base_path = path_or_url1;
+ if (strcmp(*url2, path_or_url2) != 0)
+ *base_path = path_or_url2;
+
+ SVN_ERR(svn_client_open_ra_session2(ra_session, *url2, wri_abspath,
+ ctx, pool, pool));
+
+ /* If we are performing a pegged diff, we need to find out what our
+ actual URLs will be. */
+ if (peg_revision->kind != svn_opt_revision_unspecified)
+ {
+ const char *resolved_url1;
+ const char *resolved_url2;
+
+ SVN_ERR(resolve_pegged_diff_target_url(&resolved_url2, *ra_session,
+ path_or_url2, peg_revision,
+ revision2, ctx, pool));
+
+ SVN_ERR(svn_ra_reparent(*ra_session, *url1, pool));
+ SVN_ERR(resolve_pegged_diff_target_url(&resolved_url1, *ra_session,
+ path_or_url1, peg_revision,
+ revision1, ctx, pool));
+
+ /* Either or both URLs might have changed as a result of resolving
+ * the PATH_OR_URL@PEG_REVISION's history. If only one of the URLs
+ * could be resolved, use the same URL for URL1 and URL2, so we can
+ * show a diff that adds or removes the object (see issue #4153). */
+ if (resolved_url2)
+ {
+ *url2 = resolved_url2;
+ if (!resolved_url1)
+ *url1 = resolved_url2;
+ }
+ if (resolved_url1)
+ {
+ *url1 = resolved_url1;
+ if (!resolved_url2)
+ *url2 = resolved_url1;
+ }
+
+ /* Reparent the session, since *URL2 might have changed as a result
+ the above call. */
+ SVN_ERR(svn_ra_reparent(*ra_session, *url2, pool));
+ }
+
+ /* Resolve revision and get path kind for the second target. */
+ SVN_ERR(svn_client__get_revision_number(rev2, NULL, ctx->wc_ctx,
+ (path_or_url2 == *url2) ? NULL : abspath_or_url2,
+ *ra_session, revision2, pool));
+ SVN_ERR(svn_ra_check_path(*ra_session, "", *rev2, kind2, pool));
+
+ /* Do the same for the first target. */
+ SVN_ERR(svn_ra_reparent(*ra_session, *url1, pool));
+ SVN_ERR(svn_client__get_revision_number(rev1, NULL, ctx->wc_ctx,
+ (strcmp(path_or_url1, *url1) == 0) ? NULL : abspath_or_url1,
+ *ra_session, revision1, pool));
+ SVN_ERR(svn_ra_check_path(*ra_session, "", *rev1, kind1, pool));
+
+ /* Either both URLs must exist at their respective revisions,
+ * or one of them may be missing from one side of the diff. */
+ if (*kind1 == svn_node_none && *kind2 == svn_node_none)
+ {
+ if (strcmp(*url1, *url2) == 0)
+ return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
+ _("Diff target '%s' was not found in the "
+ "repository at revisions '%ld' and '%ld'"),
+ *url1, *rev1, *rev2);
+ else
+ return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
+ _("Diff targets '%s' and '%s' were not found "
+ "in the repository at revisions '%ld' and "
+ "'%ld'"),
+ *url1, *url2, *rev1, *rev2);
+ }
+ else if (*kind1 == svn_node_none)
+ SVN_ERR(check_diff_target_exists(*url1, *rev2, *rev1, *ra_session, pool));
+ else if (*kind2 == svn_node_none)
+ SVN_ERR(check_diff_target_exists(*url2, *rev1, *rev2, *ra_session, pool));
+
+ SVN_ERR(svn_ra_get_repos_root2(*ra_session, &repos_root_url, pool));
+
+ /* Choose useful anchors and targets for our two URLs. */
+ *anchor1 = *url1;
+ *anchor2 = *url2;
+ *target1 = "";
+ *target2 = "";
+
+ /* If none of the targets is the repository root open the parent directory
+ to allow describing replacement of the target itself */
+ if (strcmp(*url1, repos_root_url) != 0
+ && strcmp(*url2, repos_root_url) != 0)
+ {
+ svn_uri_split(anchor1, target1, *url1, pool);
+ svn_uri_split(anchor2, target2, *url2, pool);
+ if (*base_path
+ && (*kind1 == svn_node_file || *kind2 == svn_node_file))
+ *base_path = svn_dirent_dirname(*base_path, pool);
+ SVN_ERR(svn_ra_reparent(*ra_session, *anchor1, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* A Theoretical Note From Ben, regarding do_diff().
+
+ This function is really svn_client_diff6(). If you read the public
+ API description for svn_client_diff6(), it sounds quite Grand. It
+ sounds really generalized and abstract and beautiful: that it will
+ diff any two paths, be they working-copy paths or URLs, at any two
+ revisions.
+
+ Now, the *reality* is that we have exactly three 'tools' for doing
+ diffing, and thus this routine is built around the use of the three
+ tools. Here they are, for clarity:
+
+ - svn_wc_diff: assumes both paths are the same wcpath.
+ compares wcpath@BASE vs. wcpath@WORKING
+
+ - svn_wc_get_diff_editor: compares some URL@REV vs. wcpath@WORKING
+
+ - svn_client__get_diff_editor: compares some URL1@REV1 vs. URL2@REV2
+
+ Since Subversion 1.8 we also have a variant of svn_wc_diff called
+ svn_client__arbitrary_nodes_diff, that allows handling WORKING-WORKING
+ comparisions between nodes in the working copy.
+
+ So the truth of the matter is, if the caller's arguments can't be
+ pigeonholed into one of these use-cases, we currently bail with a
+ friendly apology.
+
+ Perhaps someday a brave soul will truly make svn_client_diff6()
+ perfectly general. For now, we live with the 90% case. Certainly,
+ the commandline client only calls this function in legal ways.
+ When there are other users of svn_client.h, maybe this will become
+ a more pressing issue.
+ */
+
+/* Return a "you can't do that" error, optionally wrapping another
+ error CHILD_ERR. */
+static svn_error_t *
+unsupported_diff_error(svn_error_t *child_err)
+{
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, child_err,
+ _("Sorry, svn_client_diff6 was called in a way "
+ "that is not yet supported"));
+}
+
+/* Perform a diff between two working-copy paths.
+
+ PATH1 and PATH2 are both working copy paths. REVISION1 and
+ REVISION2 are their respective revisions.
+
+ All other options are the same as those passed to svn_client_diff6(). */
+static svn_error_t *
+diff_wc_wc(const char *path1,
+ const svn_opt_revision_t *revision1,
+ const char *path2,
+ const svn_opt_revision_t *revision2,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t show_copies_as_adds,
+ svn_boolean_t use_git_diff_format,
+ const apr_array_header_t *changelists,
+ const svn_wc_diff_callbacks4_t *callbacks,
+ struct diff_cmd_baton *callback_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *abspath1;
+ svn_error_t *err;
+ svn_node_kind_t kind;
+
+ SVN_ERR_ASSERT(! svn_path_is_url(path1));
+ SVN_ERR_ASSERT(! svn_path_is_url(path2));
+
+ SVN_ERR(svn_dirent_get_absolute(&abspath1, path1, pool));
+
+ /* Currently we support only the case where path1 and path2 are the
+ same path. */
+ if ((strcmp(path1, path2) != 0)
+ || (! ((revision1->kind == svn_opt_revision_base)
+ && (revision2->kind == svn_opt_revision_working))))
+ return unsupported_diff_error(
+ svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("Only diffs between a path's text-base "
+ "and its working files are supported at this time"
+ )));
+
+
+ /* Resolve named revisions to real numbers. */
+ err = svn_client__get_revision_number(&callback_baton->revnum1, NULL,
+ ctx->wc_ctx, abspath1, NULL,
+ revision1, pool);
+
+ /* In case of an added node, we have no base rev, and we show a revision
+ * number of 0. Note that this code is currently always asking for
+ * svn_opt_revision_base.
+ * ### TODO: get rid of this 0 for added nodes. */
+ if (err && (err->apr_err == SVN_ERR_CLIENT_BAD_REVISION))
+ {
+ svn_error_clear(err);
+ callback_baton->revnum1 = 0;
+ }
+ else
+ SVN_ERR(err);
+
+ callback_baton->revnum2 = SVN_INVALID_REVNUM; /* WC */
+
+ SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, abspath1,
+ TRUE, FALSE, pool));
+
+ if (kind != svn_node_dir)
+ callback_baton->anchor = svn_dirent_dirname(path1, pool);
+ else
+ callback_baton->anchor = path1;
+
+ SVN_ERR(svn_wc_diff6(ctx->wc_ctx,
+ abspath1,
+ callbacks, callback_baton,
+ depth,
+ ignore_ancestry, show_copies_as_adds,
+ use_git_diff_format, changelists,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool));
+ return SVN_NO_ERROR;
+}
+
+/* Perform a diff between two repository paths.
+
+ PATH_OR_URL1 and PATH_OR_URL2 may be either URLs or the working copy paths.
+ REVISION1 and REVISION2 are their respective revisions.
+ If PEG_REVISION is specified, PATH_OR_URL2 is the path at the peg revision,
+ and the actual two paths compared are determined by following copy
+ history from PATH_OR_URL2.
+
+ All other options are the same as those passed to svn_client_diff6(). */
+static svn_error_t *
+diff_repos_repos(const svn_wc_diff_callbacks4_t *callbacks,
+ struct diff_cmd_baton *callback_baton,
+ svn_client_ctx_t *ctx,
+ const char *path_or_url1,
+ const char *path_or_url2,
+ const svn_opt_revision_t *revision1,
+ const svn_opt_revision_t *revision2,
+ const svn_opt_revision_t *peg_revision,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *extra_ra_session;
+
+ const svn_ra_reporter3_t *reporter;
+ void *reporter_baton;
+
+ const svn_delta_editor_t *diff_editor;
+ void *diff_edit_baton;
+
+ const svn_diff_tree_processor_t *diff_processor;
+
+ const char *url1;
+ const char *url2;
+ const char *base_path;
+ svn_revnum_t rev1;
+ svn_revnum_t rev2;
+ svn_node_kind_t kind1;
+ svn_node_kind_t kind2;
+ const char *anchor1;
+ const char *anchor2;
+ const char *target1;
+ const char *target2;
+ svn_ra_session_t *ra_session;
+ const char *wri_abspath = NULL;
+
+ /* Prepare info for the repos repos diff. */
+ SVN_ERR(diff_prepare_repos_repos(&url1, &url2, &base_path, &rev1, &rev2,
+ &anchor1, &anchor2, &target1, &target2,
+ &kind1, &kind2, &ra_session,
+ ctx, path_or_url1, path_or_url2,
+ revision1, revision2, peg_revision,
+ pool));
+
+ /* Find a WC path for the ra session */
+ if (!svn_path_is_url(path_or_url1))
+ SVN_ERR(svn_dirent_get_absolute(&wri_abspath, path_or_url1, pool));
+ else if (!svn_path_is_url(path_or_url2))
+ SVN_ERR(svn_dirent_get_absolute(&wri_abspath, path_or_url2, pool));
+
+ /* Set up the repos_diff editor on BASE_PATH, if available.
+ Otherwise, we just use "". */
+
+ SVN_ERR(svn_wc__wrap_diff_callbacks(&diff_processor,
+ callbacks, callback_baton,
+ TRUE /* walk_deleted_dirs */,
+ pool, pool));
+
+ /* Get actual URLs. */
+ callback_baton->orig_path_1 = url1;
+ callback_baton->orig_path_2 = url2;
+
+ /* Get numeric revisions. */
+ callback_baton->revnum1 = rev1;
+ callback_baton->revnum2 = rev2;
+
+ callback_baton->ra_session = ra_session;
+ callback_baton->anchor = base_path;
+
+ /* The repository can bring in a new working copy, but not delete
+ everything. Luckily our new diff handler can just be reversed. */
+ if (kind2 == svn_node_none)
+ {
+ const char *str_tmp;
+ svn_revnum_t rev_tmp;
+
+ str_tmp = url2;
+ url2 = url1;
+ url1 = str_tmp;
+
+ rev_tmp = rev2;
+ rev2 = rev1;
+ rev1 = rev_tmp;
+
+ str_tmp = anchor2;
+ anchor2 = anchor1;
+ anchor1 = str_tmp;
+
+ str_tmp = target2;
+ target2 = target1;
+ target1 = str_tmp;
+
+ diff_processor = svn_diff__tree_processor_reverse_create(diff_processor,
+ NULL, pool);
+ }
+
+ /* Filter the first path component using a filter processor, until we fixed
+ the diff processing to handle this directly */
+ if ((kind1 != svn_node_file && kind2 != svn_node_file) && target1[0] != '\0')
+ {
+ diff_processor = svn_diff__tree_processor_filter_create(diff_processor,
+ target1, pool);
+ }
+
+ /* Now, we open an extra RA session to the correct anchor
+ location for URL1. This is used during the editor calls to fetch file
+ contents. */
+ SVN_ERR(svn_client_open_ra_session2(&extra_ra_session, anchor1, wri_abspath,
+ ctx, pool, pool));
+
+ SVN_ERR(svn_client__get_diff_editor2(
+ &diff_editor, &diff_edit_baton,
+ extra_ra_session, depth,
+ rev1,
+ TRUE /* text_deltas */,
+ diff_processor,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool));
+
+ /* We want to switch our txn into URL2 */
+ SVN_ERR(svn_ra_do_diff3(ra_session, &reporter, &reporter_baton,
+ rev2, target1,
+ depth, ignore_ancestry, TRUE /* text_deltas */,
+ url2, diff_editor, diff_edit_baton, pool));
+
+ /* Drive the reporter; do the diff. */
+ SVN_ERR(reporter->set_path(reporter_baton, "", rev1,
+ svn_depth_infinity,
+ FALSE, NULL,
+ pool));
+
+ return svn_error_trace(reporter->finish_report(reporter_baton, pool));
+}
+
+/* Perform a diff between a repository path and a working-copy path.
+
+ PATH_OR_URL1 may be either a URL or a working copy path. PATH2 is a
+ working copy path. REVISION1 and REVISION2 are their respective
+ revisions. If REVERSE is TRUE, the diff will be done in reverse.
+ If PEG_REVISION is specified, then PATH_OR_URL1 is the path in the peg
+ revision, and the actual repository path to be compared is
+ determined by following copy history.
+
+ All other options are the same as those passed to svn_client_diff6(). */
+static svn_error_t *
+diff_repos_wc(const char *path_or_url1,
+ const svn_opt_revision_t *revision1,
+ const svn_opt_revision_t *peg_revision,
+ const char *path2,
+ const svn_opt_revision_t *revision2,
+ svn_boolean_t reverse,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t show_copies_as_adds,
+ svn_boolean_t use_git_diff_format,
+ const apr_array_header_t *changelists,
+ const svn_wc_diff_callbacks4_t *callbacks,
+ void *callback_baton,
+ struct diff_cmd_baton *cmd_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *pool = scratch_pool;
+ const char *url1, *anchor, *anchor_url, *target;
+ svn_revnum_t rev;
+ svn_ra_session_t *ra_session;
+ svn_depth_t diff_depth;
+ const svn_ra_reporter3_t *reporter;
+ void *reporter_baton;
+ const svn_delta_editor_t *diff_editor;
+ void *diff_edit_baton;
+ svn_boolean_t rev2_is_base = (revision2->kind == svn_opt_revision_base);
+ svn_boolean_t server_supports_depth;
+ const char *abspath_or_url1;
+ const char *abspath2;
+ const char *anchor_abspath;
+ svn_node_kind_t kind1;
+ svn_node_kind_t kind2;
+ svn_boolean_t is_copy;
+ svn_revnum_t cf_revision;
+ const char *cf_repos_relpath;
+ const char *cf_repos_root_url;
+
+ SVN_ERR_ASSERT(! svn_path_is_url(path2));
+
+ if (!svn_path_is_url(path_or_url1))
+ {
+ SVN_ERR(svn_dirent_get_absolute(&abspath_or_url1, path_or_url1, pool));
+ SVN_ERR(svn_wc__node_get_url(&url1, ctx->wc_ctx, abspath_or_url1,
+ pool, pool));
+ }
+ else
+ {
+ url1 = path_or_url1;
+ abspath_or_url1 = path_or_url1;
+ }
+
+ SVN_ERR(svn_dirent_get_absolute(&abspath2, path2, pool));
+
+ /* Convert path_or_url1 to a URL to feed to do_diff. */
+ SVN_ERR(svn_wc_get_actual_target2(&anchor, &target,
+ ctx->wc_ctx, path2,
+ pool, pool));
+
+ /* Fetch the URL of the anchor directory. */
+ SVN_ERR(svn_dirent_get_absolute(&anchor_abspath, anchor, pool));
+ SVN_ERR(svn_wc__node_get_url(&anchor_url, ctx->wc_ctx, anchor_abspath,
+ pool, pool));
+ SVN_ERR_ASSERT(anchor_url != NULL);
+
+ /* If we are performing a pegged diff, we need to find out what our
+ actual URLs will be. */
+ if (peg_revision->kind != svn_opt_revision_unspecified)
+ {
+ SVN_ERR(svn_client__repos_locations(&url1, NULL, NULL, NULL,
+ NULL,
+ path_or_url1,
+ peg_revision,
+ revision1, NULL,
+ ctx, pool));
+ if (!reverse)
+ {
+ cmd_baton->orig_path_1 = url1;
+ cmd_baton->orig_path_2 =
+ svn_path_url_add_component2(anchor_url, target, pool);
+ }
+ else
+ {
+ cmd_baton->orig_path_1 =
+ svn_path_url_add_component2(anchor_url, target, pool);
+ cmd_baton->orig_path_2 = url1;
+ }
+ }
+
+ /* Open an RA session to URL1 to figure out its node kind. */
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, url1, abspath2,
+ ctx, pool, pool));
+ /* Resolve the revision to use for URL1. */
+ SVN_ERR(svn_client__get_revision_number(&rev, NULL, ctx->wc_ctx,
+ (strcmp(path_or_url1, url1) == 0)
+ ? NULL : abspath_or_url1,
+ ra_session, revision1, pool));
+ SVN_ERR(svn_ra_check_path(ra_session, "", rev, &kind1, pool));
+
+ /* Figure out the node kind of the local target. */
+ SVN_ERR(svn_wc_read_kind2(&kind2, ctx->wc_ctx, abspath2,
+ TRUE, FALSE, pool));
+
+ cmd_baton->ra_session = ra_session;
+ cmd_baton->anchor = anchor;
+
+ if (!reverse)
+ cmd_baton->revnum1 = rev;
+ else
+ cmd_baton->revnum2 = rev;
+
+ /* Check if our diff target is a copied node. */
+ SVN_ERR(svn_wc__node_get_origin(&is_copy,
+ &cf_revision,
+ &cf_repos_relpath,
+ &cf_repos_root_url,
+ NULL, NULL,
+ ctx->wc_ctx, abspath2,
+ FALSE, pool, pool));
+
+ /* Use the diff editor to generate the diff. */
+ SVN_ERR(svn_ra_has_capability(ra_session, &server_supports_depth,
+ SVN_RA_CAPABILITY_DEPTH, pool));
+ SVN_ERR(svn_wc__get_diff_editor(&diff_editor, &diff_edit_baton,
+ ctx->wc_ctx,
+ anchor_abspath,
+ target,
+ depth,
+ ignore_ancestry || is_copy,
+ show_copies_as_adds,
+ use_git_diff_format,
+ rev2_is_base,
+ reverse,
+ server_supports_depth,
+ changelists,
+ callbacks, callback_baton,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool, pool));
+ SVN_ERR(svn_ra_reparent(ra_session, anchor_url, pool));
+
+ if (depth != svn_depth_infinity)
+ diff_depth = depth;
+ else
+ diff_depth = svn_depth_unknown;
+
+ if (is_copy)
+ {
+ const char *copyfrom_parent_url;
+ const char *copyfrom_basename;
+ svn_depth_t copy_depth;
+
+ cmd_baton->repos_wc_diff_target_is_copy = TRUE;
+
+ /* We're diffing a locally copied/moved node.
+ * Describe the copy source to the reporter instead of the copy itself.
+ * Doing the latter would generate a single add_directory() call to the
+ * diff editor which results in an unexpected diff (the copy would
+ * be shown as deleted). */
+
+ if (cf_repos_relpath[0] == '\0')
+ {
+ copyfrom_parent_url = cf_repos_root_url;
+ copyfrom_basename = "";
+ }
+ else
+ {
+ const char *parent_relpath;
+ svn_relpath_split(&parent_relpath, &copyfrom_basename,
+ cf_repos_relpath, scratch_pool);
+
+ copyfrom_parent_url = svn_path_url_add_component2(cf_repos_root_url,
+ parent_relpath,
+ scratch_pool);
+ }
+ SVN_ERR(svn_ra_reparent(ra_session, copyfrom_parent_url, pool));
+
+ /* Tell the RA layer we want a delta to change our txn to URL1 */
+ SVN_ERR(svn_ra_do_diff3(ra_session,
+ &reporter, &reporter_baton,
+ rev,
+ target,
+ diff_depth,
+ ignore_ancestry,
+ TRUE, /* text_deltas */
+ url1,
+ diff_editor, diff_edit_baton, pool));
+
+ /* Report the copy source. */
+ SVN_ERR(svn_wc__node_get_depth(&copy_depth, ctx->wc_ctx, abspath2,
+ pool));
+
+ if (copy_depth == svn_depth_unknown)
+ copy_depth = svn_depth_infinity;
+
+ SVN_ERR(reporter->set_path(reporter_baton, "",
+ cf_revision,
+ copy_depth, FALSE, NULL, scratch_pool));
+
+ if (strcmp(target, copyfrom_basename) != 0)
+ SVN_ERR(reporter->link_path(reporter_baton, target,
+ svn_path_url_add_component2(
+ cf_repos_root_url,
+ cf_repos_relpath,
+ scratch_pool),
+ cf_revision,
+ copy_depth, FALSE, NULL, scratch_pool));
+
+ /* Finish the report to generate the diff. */
+ SVN_ERR(reporter->finish_report(reporter_baton, pool));
+ }
+ else
+ {
+ /* Tell the RA layer we want a delta to change our txn to URL1 */
+ SVN_ERR(svn_ra_do_diff3(ra_session,
+ &reporter, &reporter_baton,
+ rev,
+ target,
+ diff_depth,
+ ignore_ancestry,
+ TRUE, /* text_deltas */
+ url1,
+ diff_editor, diff_edit_baton, pool));
+
+ /* Create a txn mirror of path2; the diff editor will print
+ diffs in reverse. :-) */
+ SVN_ERR(svn_wc_crawl_revisions5(ctx->wc_ctx, abspath2,
+ reporter, reporter_baton,
+ FALSE, depth, TRUE,
+ (! server_supports_depth),
+ FALSE,
+ ctx->cancel_func, ctx->cancel_baton,
+ NULL, NULL, /* notification is N/A */
+ pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* This is basically just the guts of svn_client_diff[_peg]6(). */
+static svn_error_t *
+do_diff(const svn_wc_diff_callbacks4_t *callbacks,
+ struct diff_cmd_baton *callback_baton,
+ svn_client_ctx_t *ctx,
+ const char *path_or_url1,
+ const char *path_or_url2,
+ const svn_opt_revision_t *revision1,
+ const svn_opt_revision_t *revision2,
+ const svn_opt_revision_t *peg_revision,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t show_copies_as_adds,
+ svn_boolean_t use_git_diff_format,
+ const apr_array_header_t *changelists,
+ apr_pool_t *pool)
+{
+ svn_boolean_t is_repos1;
+ svn_boolean_t is_repos2;
+
+ /* Check if paths/revisions are urls/local. */
+ SVN_ERR(check_paths(&is_repos1, &is_repos2, path_or_url1, path_or_url2,
+ revision1, revision2, peg_revision));
+
+ if (is_repos1)
+ {
+ if (is_repos2)
+ {
+ /* ### Ignores 'show_copies_as_adds'. */
+ SVN_ERR(diff_repos_repos(callbacks, callback_baton, ctx,
+ path_or_url1, path_or_url2,
+ revision1, revision2,
+ peg_revision, depth, ignore_ancestry,
+ pool));
+ }
+ else /* path_or_url2 is a working copy path */
+ {
+ SVN_ERR(diff_repos_wc(path_or_url1, revision1, peg_revision,
+ path_or_url2, revision2, FALSE, depth,
+ ignore_ancestry, show_copies_as_adds,
+ use_git_diff_format, changelists,
+ callbacks, callback_baton, callback_baton,
+ ctx, pool));
+ }
+ }
+ else /* path_or_url1 is a working copy path */
+ {
+ if (is_repos2)
+ {
+ SVN_ERR(diff_repos_wc(path_or_url2, revision2, peg_revision,
+ path_or_url1, revision1, TRUE, depth,
+ ignore_ancestry, show_copies_as_adds,
+ use_git_diff_format, changelists,
+ callbacks, callback_baton, callback_baton,
+ ctx, pool));
+ }
+ else /* path_or_url2 is a working copy path */
+ {
+ if (revision1->kind == svn_opt_revision_working
+ && revision2->kind == svn_opt_revision_working)
+ {
+ const char *abspath1;
+ const char *abspath2;
+
+ SVN_ERR(svn_dirent_get_absolute(&abspath1, path_or_url1, pool));
+ SVN_ERR(svn_dirent_get_absolute(&abspath2, path_or_url2, pool));
+
+ SVN_ERR(svn_client__arbitrary_nodes_diff(abspath1, abspath2,
+ depth,
+ callbacks,
+ callback_baton,
+ ctx, pool));
+ }
+ else
+ SVN_ERR(diff_wc_wc(path_or_url1, revision1,
+ path_or_url2, revision2,
+ depth, ignore_ancestry, show_copies_as_adds,
+ use_git_diff_format, changelists,
+ callbacks, callback_baton, ctx, pool));
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Perform a diff between a repository path and a working-copy path.
+
+ PATH_OR_URL1 may be either a URL or a working copy path. PATH2 is a
+ working copy path. REVISION1 and REVISION2 are their respective
+ revisions. If REVERSE is TRUE, the diff will be done in reverse.
+ If PEG_REVISION is specified, then PATH_OR_URL1 is the path in the peg
+ revision, and the actual repository path to be compared is
+ determined by following copy history.
+
+ All other options are the same as those passed to svn_client_diff6(). */
+static svn_error_t *
+diff_summarize_repos_wc(svn_client_diff_summarize_func_t summarize_func,
+ void *summarize_baton,
+ const char *path_or_url1,
+ const svn_opt_revision_t *revision1,
+ const svn_opt_revision_t *peg_revision,
+ const char *path2,
+ const svn_opt_revision_t *revision2,
+ svn_boolean_t reverse,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *anchor, *target;
+ svn_wc_diff_callbacks4_t *callbacks;
+ void *callback_baton;
+ struct diff_cmd_baton cmd_baton;
+
+ SVN_ERR_ASSERT(! svn_path_is_url(path2));
+
+ SVN_ERR(svn_wc_get_actual_target2(&anchor, &target,
+ ctx->wc_ctx, path2,
+ pool, pool));
+
+ SVN_ERR(svn_client__get_diff_summarize_callbacks(
+ &callbacks, &callback_baton, target, reverse,
+ summarize_func, summarize_baton, pool));
+
+ SVN_ERR(diff_repos_wc(path_or_url1, revision1, peg_revision,
+ path2, revision2, reverse,
+ depth, FALSE, TRUE, FALSE, changelists,
+ callbacks, callback_baton, &cmd_baton,
+ ctx, pool));
+ return SVN_NO_ERROR;
+}
+
+/* Perform a summary diff between two working-copy paths.
+
+ PATH1 and PATH2 are both working copy paths. REVISION1 and
+ REVISION2 are their respective revisions.
+
+ All other options are the same as those passed to svn_client_diff6(). */
+static svn_error_t *
+diff_summarize_wc_wc(svn_client_diff_summarize_func_t summarize_func,
+ void *summarize_baton,
+ const char *path1,
+ const svn_opt_revision_t *revision1,
+ const char *path2,
+ const svn_opt_revision_t *revision2,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_wc_diff_callbacks4_t *callbacks;
+ void *callback_baton;
+ const char *abspath1, *target1;
+ svn_node_kind_t kind;
+
+ SVN_ERR_ASSERT(! svn_path_is_url(path1));
+ SVN_ERR_ASSERT(! svn_path_is_url(path2));
+
+ /* Currently we support only the case where path1 and path2 are the
+ same path. */
+ if ((strcmp(path1, path2) != 0)
+ || (! ((revision1->kind == svn_opt_revision_base)
+ && (revision2->kind == svn_opt_revision_working))))
+ return unsupported_diff_error
+ (svn_error_create
+ (SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("Summarized diffs are only supported between a path's text-base "
+ "and its working files at this time")));
+
+ /* Find the node kind of PATH1 so that we know whether the diff drive will
+ be anchored at PATH1 or its parent dir. */
+ SVN_ERR(svn_dirent_get_absolute(&abspath1, path1, pool));
+ SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, abspath1,
+ TRUE, FALSE, pool));
+ target1 = (kind == svn_node_dir) ? "" : svn_dirent_basename(path1, pool);
+ SVN_ERR(svn_client__get_diff_summarize_callbacks(
+ &callbacks, &callback_baton, target1, FALSE,
+ summarize_func, summarize_baton, pool));
+
+ SVN_ERR(svn_wc_diff6(ctx->wc_ctx,
+ abspath1,
+ callbacks, callback_baton,
+ depth,
+ ignore_ancestry, FALSE /* show_copies_as_adds */,
+ FALSE /* use_git_diff_format */, changelists,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool));
+ return SVN_NO_ERROR;
+}
+
+/* Perform a diff summary between two repository paths. */
+static svn_error_t *
+diff_summarize_repos_repos(svn_client_diff_summarize_func_t summarize_func,
+ void *summarize_baton,
+ svn_client_ctx_t *ctx,
+ const char *path_or_url1,
+ const char *path_or_url2,
+ const svn_opt_revision_t *revision1,
+ const svn_opt_revision_t *revision2,
+ const svn_opt_revision_t *peg_revision,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *extra_ra_session;
+
+ const svn_ra_reporter3_t *reporter;
+ void *reporter_baton;
+
+ const svn_delta_editor_t *diff_editor;
+ void *diff_edit_baton;
+
+ const svn_diff_tree_processor_t *diff_processor;
+
+ const char *url1;
+ const char *url2;
+ const char *base_path;
+ svn_revnum_t rev1;
+ svn_revnum_t rev2;
+ svn_node_kind_t kind1;
+ svn_node_kind_t kind2;
+ const char *anchor1;
+ const char *anchor2;
+ const char *target1;
+ const char *target2;
+ svn_ra_session_t *ra_session;
+ svn_wc_diff_callbacks4_t *callbacks;
+ void *callback_baton;
+
+ /* Prepare info for the repos repos diff. */
+ SVN_ERR(diff_prepare_repos_repos(&url1, &url2, &base_path, &rev1, &rev2,
+ &anchor1, &anchor2, &target1, &target2,
+ &kind1, &kind2, &ra_session,
+ ctx, path_or_url1, path_or_url2,
+ revision1, revision2,
+ peg_revision, pool));
+
+ /* Set up the repos_diff editor. */
+ SVN_ERR(svn_client__get_diff_summarize_callbacks(
+ &callbacks, &callback_baton,
+ target1, FALSE, summarize_func, summarize_baton, pool));
+
+ SVN_ERR(svn_wc__wrap_diff_callbacks(&diff_processor,
+ callbacks, callback_baton,
+ TRUE /* walk_deleted_dirs */,
+ pool, pool));
+
+
+ /* The repository can bring in a new working copy, but not delete
+ everything. Luckily our new diff handler can just be reversed. */
+ if (kind2 == svn_node_none)
+ {
+ const char *str_tmp;
+ svn_revnum_t rev_tmp;
+
+ str_tmp = url2;
+ url2 = url1;
+ url1 = str_tmp;
+
+ rev_tmp = rev2;
+ rev2 = rev1;
+ rev1 = rev_tmp;
+
+ str_tmp = anchor2;
+ anchor2 = anchor1;
+ anchor1 = str_tmp;
+
+ str_tmp = target2;
+ target2 = target1;
+ target1 = str_tmp;
+
+ diff_processor = svn_diff__tree_processor_reverse_create(diff_processor,
+ NULL, pool);
+ }
+
+ /* Now, we open an extra RA session to the correct anchor
+ location for URL1. This is used to get deleted path information. */
+ SVN_ERR(svn_client_open_ra_session2(&extra_ra_session, anchor1, NULL,
+ ctx, pool, pool));
+
+ SVN_ERR(svn_client__get_diff_editor2(&diff_editor, &diff_edit_baton,
+ extra_ra_session,
+ depth,
+ rev1,
+ FALSE /* text_deltas */,
+ diff_processor,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool));
+
+ /* We want to switch our txn into URL2 */
+ SVN_ERR(svn_ra_do_diff3
+ (ra_session, &reporter, &reporter_baton, rev2, target1,
+ depth, ignore_ancestry,
+ FALSE /* do not create text delta */, url2, diff_editor,
+ diff_edit_baton, pool));
+
+ /* Drive the reporter; do the diff. */
+ SVN_ERR(reporter->set_path(reporter_baton, "", rev1,
+ svn_depth_infinity,
+ FALSE, NULL, pool));
+ return svn_error_trace(reporter->finish_report(reporter_baton, pool));
+}
+
+/* This is basically just the guts of svn_client_diff_summarize[_peg]2(). */
+static svn_error_t *
+do_diff_summarize(svn_client_diff_summarize_func_t summarize_func,
+ void *summarize_baton,
+ svn_client_ctx_t *ctx,
+ const char *path_or_url1,
+ const char *path_or_url2,
+ const svn_opt_revision_t *revision1,
+ const svn_opt_revision_t *revision2,
+ const svn_opt_revision_t *peg_revision,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ const apr_array_header_t *changelists,
+ apr_pool_t *pool)
+{
+ svn_boolean_t is_repos1;
+ svn_boolean_t is_repos2;
+
+ /* Check if paths/revisions are urls/local. */
+ SVN_ERR(check_paths(&is_repos1, &is_repos2, path_or_url1, path_or_url2,
+ revision1, revision2, peg_revision));
+
+ if (is_repos1)
+ {
+ if (is_repos2)
+ SVN_ERR(diff_summarize_repos_repos(summarize_func, summarize_baton, ctx,
+ path_or_url1, path_or_url2,
+ revision1, revision2,
+ peg_revision, depth, ignore_ancestry,
+ pool));
+ else
+ SVN_ERR(diff_summarize_repos_wc(summarize_func, summarize_baton,
+ path_or_url1, revision1,
+ peg_revision,
+ path_or_url2, revision2,
+ FALSE, depth,
+ ignore_ancestry,
+ changelists,
+ ctx, pool));
+ }
+ else /* ! is_repos1 */
+ {
+ if (is_repos2)
+ SVN_ERR(diff_summarize_repos_wc(summarize_func, summarize_baton,
+ path_or_url2, revision2,
+ peg_revision,
+ path_or_url1, revision1,
+ TRUE, depth,
+ ignore_ancestry,
+ changelists,
+ ctx, pool));
+ else
+ {
+ if (revision1->kind == svn_opt_revision_working
+ && revision2->kind == svn_opt_revision_working)
+ {
+ const char *abspath1;
+ const char *abspath2;
+ svn_wc_diff_callbacks4_t *callbacks;
+ void *callback_baton;
+ const char *target;
+ svn_node_kind_t kind;
+
+ SVN_ERR(svn_dirent_get_absolute(&abspath1, path_or_url1, pool));
+ SVN_ERR(svn_dirent_get_absolute(&abspath2, path_or_url2, pool));
+
+ SVN_ERR(svn_io_check_resolved_path(abspath1, &kind, pool));
+
+ if (kind == svn_node_dir)
+ target = "";
+ else
+ target = svn_dirent_basename(path_or_url1, NULL);
+
+ SVN_ERR(svn_client__get_diff_summarize_callbacks(
+ &callbacks, &callback_baton, target, FALSE,
+ summarize_func, summarize_baton, pool));
+
+ SVN_ERR(svn_client__arbitrary_nodes_diff(abspath1, abspath2,
+ depth,
+ callbacks,
+ callback_baton,
+ ctx, pool));
+ }
+ else
+ SVN_ERR(diff_summarize_wc_wc(summarize_func, summarize_baton,
+ path_or_url1, revision1,
+ path_or_url2, revision2,
+ depth, ignore_ancestry,
+ changelists, ctx, pool));
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Initialize DIFF_CMD_BATON.diff_cmd and DIFF_CMD_BATON.options,
+ * according to OPTIONS and CONFIG. CONFIG and OPTIONS may be null.
+ * Allocate the fields in POOL, which should be at least as long-lived
+ * as the pool DIFF_CMD_BATON itself is allocated in.
+ */
+static svn_error_t *
+set_up_diff_cmd_and_options(struct diff_cmd_baton *diff_cmd_baton,
+ const apr_array_header_t *options,
+ apr_hash_t *config, apr_pool_t *pool)
+{
+ const char *diff_cmd = NULL;
+
+ /* See if there is a diff command and/or diff arguments. */
+ if (config)
+ {
+ svn_config_t *cfg = svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG);
+ svn_config_get(cfg, &diff_cmd, SVN_CONFIG_SECTION_HELPERS,
+ SVN_CONFIG_OPTION_DIFF_CMD, NULL);
+ if (options == NULL)
+ {
+ const char *diff_extensions;
+ svn_config_get(cfg, &diff_extensions, SVN_CONFIG_SECTION_HELPERS,
+ SVN_CONFIG_OPTION_DIFF_EXTENSIONS, NULL);
+ if (diff_extensions)
+ options = svn_cstring_split(diff_extensions, " \t\n\r", TRUE, pool);
+ }
+ }
+
+ if (options == NULL)
+ options = apr_array_make(pool, 0, sizeof(const char *));
+
+ if (diff_cmd)
+ SVN_ERR(svn_path_cstring_to_utf8(&diff_cmd_baton->diff_cmd, diff_cmd,
+ pool));
+ else
+ diff_cmd_baton->diff_cmd = NULL;
+
+ /* If there was a command, arrange options to pass to it. */
+ if (diff_cmd_baton->diff_cmd)
+ {
+ const char **argv = NULL;
+ int argc = options->nelts;
+ if (argc)
+ {
+ int i;
+ argv = apr_palloc(pool, argc * sizeof(char *));
+ for (i = 0; i < argc; i++)
+ SVN_ERR(svn_utf_cstring_to_utf8(&argv[i],
+ APR_ARRAY_IDX(options, i, const char *), pool));
+ }
+ diff_cmd_baton->options.for_external.argv = argv;
+ diff_cmd_baton->options.for_external.argc = argc;
+ }
+ else /* No command, so arrange options for internal invocation instead. */
+ {
+ diff_cmd_baton->options.for_internal
+ = svn_diff_file_options_create(pool);
+ SVN_ERR(svn_diff_file_options_parse
+ (diff_cmd_baton->options.for_internal, options, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/*----------------------------------------------------------------------- */
+
+/*** Public Interfaces. ***/
+
+/* Display context diffs between two PATH/REVISION pairs. Each of
+ these inputs will be one of the following:
+
+ - a repository URL at a given revision.
+ - a working copy path, ignoring local mods.
+ - a working copy path, including local mods.
+
+ We can establish a matrix that shows the nine possible types of
+ diffs we expect to support.
+
+
+ ` . DST || URL:rev | WC:base | WC:working |
+ ` . || | | |
+ SRC ` . || | | |
+ ============++============+============+============+
+ URL:rev || (*) | (*) | (*) |
+ || | | |
+ || | | |
+ || | | |
+ ------------++------------+------------+------------+
+ WC:base || (*) | |
+ || | New svn_wc_diff which |
+ || | is smart enough to |
+ || | handle two WC paths |
+ ------------++------------+ and their related +
+ WC:working || (*) | text-bases and working |
+ || | files. This operation |
+ || | is entirely local. |
+ || | |
+ ------------++------------+------------+------------+
+ * These cases require server communication.
+*/
+svn_error_t *
+svn_client_diff6(const apr_array_header_t *options,
+ const char *path_or_url1,
+ const svn_opt_revision_t *revision1,
+ const char *path_or_url2,
+ const svn_opt_revision_t *revision2,
+ const char *relative_to_dir,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_added,
+ svn_boolean_t no_diff_deleted,
+ svn_boolean_t show_copies_as_adds,
+ svn_boolean_t ignore_content_type,
+ svn_boolean_t ignore_properties,
+ svn_boolean_t properties_only,
+ svn_boolean_t use_git_diff_format,
+ const char *header_encoding,
+ svn_stream_t *outstream,
+ svn_stream_t *errstream,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct diff_cmd_baton diff_cmd_baton = { 0 };
+ svn_opt_revision_t peg_revision;
+
+ if (ignore_properties && properties_only)
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("Cannot ignore properties and show only "
+ "properties at the same time"));
+
+ /* We will never do a pegged diff from here. */
+ peg_revision.kind = svn_opt_revision_unspecified;
+
+ /* setup callback and baton */
+ diff_cmd_baton.orig_path_1 = path_or_url1;
+ diff_cmd_baton.orig_path_2 = path_or_url2;
+
+ SVN_ERR(set_up_diff_cmd_and_options(&diff_cmd_baton, options,
+ ctx->config, pool));
+ diff_cmd_baton.pool = pool;
+ diff_cmd_baton.outstream = outstream;
+ diff_cmd_baton.errstream = errstream;
+ diff_cmd_baton.header_encoding = header_encoding;
+ diff_cmd_baton.revnum1 = SVN_INVALID_REVNUM;
+ diff_cmd_baton.revnum2 = SVN_INVALID_REVNUM;
+
+ diff_cmd_baton.force_binary = ignore_content_type;
+ diff_cmd_baton.ignore_properties = ignore_properties;
+ diff_cmd_baton.properties_only = properties_only;
+ diff_cmd_baton.relative_to_dir = relative_to_dir;
+ diff_cmd_baton.use_git_diff_format = use_git_diff_format;
+ diff_cmd_baton.no_diff_added = no_diff_added;
+ diff_cmd_baton.no_diff_deleted = no_diff_deleted;
+ diff_cmd_baton.no_copyfrom_on_add = show_copies_as_adds;
+
+ diff_cmd_baton.wc_ctx = ctx->wc_ctx;
+ diff_cmd_baton.ra_session = NULL;
+ diff_cmd_baton.anchor = NULL;
+
+ return do_diff(&diff_callbacks, &diff_cmd_baton, ctx,
+ path_or_url1, path_or_url2, revision1, revision2,
+ &peg_revision,
+ depth, ignore_ancestry, show_copies_as_adds,
+ use_git_diff_format, changelists, pool);
+}
+
+svn_error_t *
+svn_client_diff_peg6(const apr_array_header_t *options,
+ const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start_revision,
+ const svn_opt_revision_t *end_revision,
+ const char *relative_to_dir,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t no_diff_added,
+ svn_boolean_t no_diff_deleted,
+ svn_boolean_t show_copies_as_adds,
+ svn_boolean_t ignore_content_type,
+ svn_boolean_t ignore_properties,
+ svn_boolean_t properties_only,
+ svn_boolean_t use_git_diff_format,
+ const char *header_encoding,
+ svn_stream_t *outstream,
+ svn_stream_t *errstream,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct diff_cmd_baton diff_cmd_baton = { 0 };
+
+ if (ignore_properties && properties_only)
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("Cannot ignore properties and show only "
+ "properties at the same time"));
+
+ /* setup callback and baton */
+ diff_cmd_baton.orig_path_1 = path_or_url;
+ diff_cmd_baton.orig_path_2 = path_or_url;
+
+ SVN_ERR(set_up_diff_cmd_and_options(&diff_cmd_baton, options,
+ ctx->config, pool));
+ diff_cmd_baton.pool = pool;
+ diff_cmd_baton.outstream = outstream;
+ diff_cmd_baton.errstream = errstream;
+ diff_cmd_baton.header_encoding = header_encoding;
+ diff_cmd_baton.revnum1 = SVN_INVALID_REVNUM;
+ diff_cmd_baton.revnum2 = SVN_INVALID_REVNUM;
+
+ diff_cmd_baton.force_binary = ignore_content_type;
+ diff_cmd_baton.ignore_properties = ignore_properties;
+ diff_cmd_baton.properties_only = properties_only;
+ diff_cmd_baton.relative_to_dir = relative_to_dir;
+ diff_cmd_baton.use_git_diff_format = use_git_diff_format;
+ diff_cmd_baton.no_diff_added = no_diff_added;
+ diff_cmd_baton.no_diff_deleted = no_diff_deleted;
+ diff_cmd_baton.no_copyfrom_on_add = show_copies_as_adds;
+
+ diff_cmd_baton.wc_ctx = ctx->wc_ctx;
+ diff_cmd_baton.ra_session = NULL;
+ diff_cmd_baton.anchor = NULL;
+
+ return do_diff(&diff_callbacks, &diff_cmd_baton, ctx,
+ path_or_url, path_or_url, start_revision, end_revision,
+ peg_revision,
+ depth, ignore_ancestry, show_copies_as_adds,
+ use_git_diff_format, changelists, pool);
+}
+
+svn_error_t *
+svn_client_diff_summarize2(const char *path_or_url1,
+ const svn_opt_revision_t *revision1,
+ const char *path_or_url2,
+ const svn_opt_revision_t *revision2,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ const apr_array_header_t *changelists,
+ svn_client_diff_summarize_func_t summarize_func,
+ void *summarize_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ /* We will never do a pegged diff from here. */
+ svn_opt_revision_t peg_revision;
+ peg_revision.kind = svn_opt_revision_unspecified;
+
+ return do_diff_summarize(summarize_func, summarize_baton, ctx,
+ path_or_url1, path_or_url2, revision1, revision2,
+ &peg_revision,
+ depth, ignore_ancestry, changelists, pool);
+}
+
+svn_error_t *
+svn_client_diff_summarize_peg2(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *start_revision,
+ const svn_opt_revision_t *end_revision,
+ svn_depth_t depth,
+ svn_boolean_t ignore_ancestry,
+ const apr_array_header_t *changelists,
+ svn_client_diff_summarize_func_t summarize_func,
+ void *summarize_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ return do_diff_summarize(summarize_func, summarize_baton, ctx,
+ path_or_url, path_or_url,
+ start_revision, end_revision, peg_revision,
+ depth, ignore_ancestry, changelists, pool);
+}
+
+svn_client_diff_summarize_t *
+svn_client_diff_summarize_dup(const svn_client_diff_summarize_t *diff,
+ apr_pool_t *pool)
+{
+ svn_client_diff_summarize_t *dup_diff = apr_palloc(pool, sizeof(*dup_diff));
+
+ *dup_diff = *diff;
+
+ if (diff->path)
+ dup_diff->path = apr_pstrdup(pool, diff->path);
+
+ return dup_diff;
+}
diff --git a/subversion/libsvn_client/diff_local.c b/subversion/libsvn_client/diff_local.c
new file mode 100644
index 0000000..cc7184f
--- /dev/null
+++ b/subversion/libsvn_client/diff_local.c
@@ -0,0 +1,633 @@
+/*
+ * diff_local.c: comparing local trees with each other
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <apr_strings.h>
+#include <apr_pools.h>
+#include <apr_hash.h>
+#include "svn_hash.h"
+#include "svn_types.h"
+#include "svn_wc.h"
+#include "svn_diff.h"
+#include "svn_client.h"
+#include "svn_string.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_io.h"
+#include "svn_pools.h"
+#include "svn_props.h"
+#include "svn_sorts.h"
+#include "svn_subst.h"
+#include "client.h"
+
+#include "private/svn_wc_private.h"
+
+#include "svn_private_config.h"
+
+
+/* Try to get properties for LOCAL_ABSPATH and return them in the property
+ * hash *PROPS. If there are no properties because LOCAL_ABSPATH is not
+ * versioned, return an empty property hash. */
+static svn_error_t *
+get_props(apr_hash_t **props,
+ const char *local_abspath,
+ svn_wc_context_t *wc_ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err;
+
+ err = svn_wc_prop_list2(props, wc_ctx, local_abspath, result_pool,
+ scratch_pool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND ||
+ err->apr_err == SVN_ERR_WC_NOT_WORKING_COPY)
+ {
+ svn_error_clear(err);
+ *props = apr_hash_make(result_pool);
+
+ /* ### Apply autoprops, like 'svn add' would? */
+ }
+ else
+ return svn_error_trace(err);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Produce a diff between two arbitrary files at LOCAL_ABSPATH1 and
+ * LOCAL_ABSPATH2, using the diff callbacks from CALLBACKS.
+ * Use PATH as the name passed to diff callbacks.
+ * FILE1_IS_EMPTY and FILE2_IS_EMPTY are used as hints which diff callback
+ * function to use to compare the files (added/deleted/changed).
+ *
+ * If ORIGINAL_PROPS_OVERRIDE is not NULL, use it as original properties
+ * instead of reading properties from LOCAL_ABSPATH1. This is required when
+ * a file replaces a directory, where LOCAL_ABSPATH1 is an empty file that
+ * file content must be diffed against, but properties to diff against come
+ * from the replaced directory. */
+static svn_error_t *
+do_arbitrary_files_diff(const char *local_abspath1,
+ const char *local_abspath2,
+ const char *path,
+ svn_boolean_t file1_is_empty,
+ svn_boolean_t file2_is_empty,
+ apr_hash_t *original_props_override,
+ const svn_wc_diff_callbacks4_t *callbacks,
+ void *diff_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *original_props;
+ apr_hash_t *modified_props;
+ apr_array_header_t *prop_changes;
+ svn_string_t *original_mime_type = NULL;
+ svn_string_t *modified_mime_type = NULL;
+
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ /* Try to get properties from either file. It's OK if the files do not
+ * have properties, or if they are unversioned. */
+ if (original_props_override)
+ original_props = original_props_override;
+ else
+ SVN_ERR(get_props(&original_props, local_abspath1, ctx->wc_ctx,
+ scratch_pool, scratch_pool));
+ SVN_ERR(get_props(&modified_props, local_abspath2, ctx->wc_ctx,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(svn_prop_diffs(&prop_changes, modified_props, original_props,
+ scratch_pool));
+
+ /* Try to determine the mime-type of each file. */
+ original_mime_type = svn_hash_gets(original_props, SVN_PROP_MIME_TYPE);
+ if (!file1_is_empty && !original_mime_type)
+ {
+ const char *mime_type;
+ SVN_ERR(svn_io_detect_mimetype2(&mime_type, local_abspath1,
+ ctx->mimetypes_map, scratch_pool));
+
+ if (mime_type)
+ original_mime_type = svn_string_create(mime_type, scratch_pool);
+ }
+
+ modified_mime_type = svn_hash_gets(modified_props, SVN_PROP_MIME_TYPE);
+ if (!file2_is_empty && !modified_mime_type)
+ {
+ const char *mime_type;
+ SVN_ERR(svn_io_detect_mimetype2(&mime_type, local_abspath1,
+ ctx->mimetypes_map, scratch_pool));
+
+ if (mime_type)
+ modified_mime_type = svn_string_create(mime_type, scratch_pool);
+ }
+
+ /* Produce the diff. */
+ if (file1_is_empty && !file2_is_empty)
+ SVN_ERR(callbacks->file_added(NULL, NULL, NULL, path,
+ local_abspath1, local_abspath2,
+ /* ### TODO get real revision info
+ * for versioned files? */
+ SVN_INVALID_REVNUM, SVN_INVALID_REVNUM,
+ original_mime_type ?
+ original_mime_type->data : NULL,
+ modified_mime_type ?
+ modified_mime_type->data : NULL,
+ /* ### TODO get copyfrom? */
+ NULL, SVN_INVALID_REVNUM,
+ prop_changes, original_props,
+ diff_baton, scratch_pool));
+ else if (!file1_is_empty && file2_is_empty)
+ SVN_ERR(callbacks->file_deleted(NULL, NULL, path,
+ local_abspath1, local_abspath2,
+ original_mime_type ?
+ original_mime_type->data : NULL,
+ modified_mime_type ?
+ modified_mime_type->data : NULL,
+ original_props,
+ diff_baton, scratch_pool));
+ else
+ {
+ svn_stream_t *file1;
+ svn_stream_t *file2;
+ svn_boolean_t same;
+ svn_string_t *val;
+ /* We have two files, which may or may not be the same.
+
+ ### Our caller assumes that we should ignore symlinks here and
+ handle them as normal paths. Perhaps that should change?
+ */
+ SVN_ERR(svn_stream_open_readonly(&file1, local_abspath1, scratch_pool,
+ scratch_pool));
+
+ SVN_ERR(svn_stream_open_readonly(&file2, local_abspath2, scratch_pool,
+ scratch_pool));
+
+ /* Wrap with normalization, etc. if necessary */
+ if (original_props)
+ {
+ val = svn_hash_gets(original_props, SVN_PROP_EOL_STYLE);
+
+ if (val)
+ {
+ svn_subst_eol_style_t style;
+ const char *eol;
+ svn_subst_eol_style_from_value(&style, &eol, val->data);
+
+ /* ### Ignoring keywords */
+ if (eol)
+ file1 = svn_subst_stream_translated(file1, eol, TRUE,
+ NULL, FALSE,
+ scratch_pool);
+ }
+ }
+
+ if (modified_props)
+ {
+ val = svn_hash_gets(modified_props, SVN_PROP_EOL_STYLE);
+
+ if (val)
+ {
+ svn_subst_eol_style_t style;
+ const char *eol;
+ svn_subst_eol_style_from_value(&style, &eol, val->data);
+
+ /* ### Ignoring keywords */
+ if (eol)
+ file2 = svn_subst_stream_translated(file2, eol, TRUE,
+ NULL, FALSE,
+ scratch_pool);
+ }
+ }
+
+ SVN_ERR(svn_stream_contents_same2(&same, file1, file2, scratch_pool));
+
+ if (! same || prop_changes->nelts > 0)
+ {
+ /* ### We should probably pass the normalized data we created using
+ the subst streams as that is what diff users expect */
+ SVN_ERR(callbacks->file_changed(NULL, NULL, NULL, path,
+ same ? NULL : local_abspath1,
+ same ? NULL : local_abspath2,
+ /* ### TODO get real revision info
+ * for versioned files? */
+ SVN_INVALID_REVNUM /* rev1 */,
+ SVN_INVALID_REVNUM /* rev2 */,
+ original_mime_type ?
+ original_mime_type->data : NULL,
+ modified_mime_type ?
+ modified_mime_type->data : NULL,
+ prop_changes, original_props,
+ diff_baton, scratch_pool));
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+struct arbitrary_diff_walker_baton {
+ /* The root directories of the trees being compared. */
+ const char *root1_abspath;
+ const char *root2_abspath;
+
+ /* TRUE if recursing within an added subtree of root2_abspath that
+ * does not exist in root1_abspath. */
+ svn_boolean_t recursing_within_added_subtree;
+
+ /* TRUE if recursing within an administrative (.i.e. .svn) directory. */
+ svn_boolean_t recursing_within_adm_dir;
+
+ /* The absolute path of the adm dir if RECURSING_WITHIN_ADM_DIR is TRUE.
+ * Else this is NULL.*/
+ const char *adm_dir_abspath;
+
+ /* A path to an empty file used for diffs that add/delete files. */
+ const char *empty_file_abspath;
+
+ const svn_wc_diff_callbacks4_t *callbacks;
+ void *diff_baton;
+ svn_client_ctx_t *ctx;
+ apr_pool_t *pool;
+} arbitrary_diff_walker_baton;
+
+/* Forward declaration needed because this function has a cyclic
+ * dependency with do_arbitrary_dirs_diff(). */
+static svn_error_t *
+arbitrary_diff_walker(void *baton, const char *local_abspath,
+ const apr_finfo_t *finfo,
+ apr_pool_t *scratch_pool);
+
+/* Another forward declaration. */
+static svn_error_t *
+arbitrary_diff_this_dir(struct arbitrary_diff_walker_baton *b,
+ const char *local_abspath,
+ svn_depth_t depth,
+ apr_pool_t *scratch_pool);
+
+/* Produce a diff of depth DEPTH between two arbitrary directories at
+ * LOCAL_ABSPATH1 and LOCAL_ABSPATH2, using the provided diff callbacks
+ * to show file changes and, for versioned nodes, property changes.
+ *
+ * If ROOT_ABSPATH1 and ROOT_ABSPATH2 are not NULL, show paths in diffs
+ * relative to these roots, rather than relative to LOCAL_ABSPATH1 and
+ * LOCAL_ABSPATH2. This is needed when crawling a subtree that exists
+ * only within LOCAL_ABSPATH2. */
+static svn_error_t *
+do_arbitrary_dirs_diff(const char *local_abspath1,
+ const char *local_abspath2,
+ const char *root_abspath1,
+ const char *root_abspath2,
+ svn_depth_t depth,
+ const svn_wc_diff_callbacks4_t *callbacks,
+ void *diff_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_file_t *empty_file;
+ svn_node_kind_t kind1;
+
+ struct arbitrary_diff_walker_baton b;
+
+ /* If LOCAL_ABSPATH1 is not a directory, crawl LOCAL_ABSPATH2 instead
+ * and compare it to LOCAL_ABSPATH1, showing only additions.
+ * This case can only happen during recursion from arbitrary_diff_walker(),
+ * because do_arbitrary_nodes_diff() prevents this from happening at
+ * the root of the comparison. */
+ SVN_ERR(svn_io_check_resolved_path(local_abspath1, &kind1, scratch_pool));
+ b.recursing_within_added_subtree = (kind1 != svn_node_dir);
+
+ b.root1_abspath = root_abspath1 ? root_abspath1 : local_abspath1;
+ b.root2_abspath = root_abspath2 ? root_abspath2 : local_abspath2;
+ b.recursing_within_adm_dir = FALSE;
+ b.adm_dir_abspath = NULL;
+ b.callbacks = callbacks;
+ b.diff_baton = diff_baton;
+ b.ctx = ctx;
+ b.pool = scratch_pool;
+
+ SVN_ERR(svn_io_open_unique_file3(&empty_file, &b.empty_file_abspath,
+ NULL, svn_io_file_del_on_pool_cleanup,
+ scratch_pool, scratch_pool));
+
+ if (depth <= svn_depth_immediates)
+ SVN_ERR(arbitrary_diff_this_dir(&b, local_abspath1, depth, scratch_pool));
+ else if (depth == svn_depth_infinity)
+ SVN_ERR(svn_io_dir_walk2(b.recursing_within_added_subtree ? local_abspath2
+ : local_abspath1,
+ 0, arbitrary_diff_walker, &b, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* Produce a diff of depth DEPTH for the directory at LOCAL_ABSPATH,
+ * using information from the arbitrary_diff_walker_baton B.
+ * LOCAL_ABSPATH is the path being crawled and can be on either side
+ * of the diff depending on baton->recursing_within_added_subtree. */
+static svn_error_t *
+arbitrary_diff_this_dir(struct arbitrary_diff_walker_baton *b,
+ const char *local_abspath,
+ svn_depth_t depth,
+ apr_pool_t *scratch_pool)
+{
+ const char *local_abspath1;
+ const char *local_abspath2;
+ svn_node_kind_t kind1;
+ svn_node_kind_t kind2;
+ const char *child_relpath;
+ apr_hash_t *dirents1;
+ apr_hash_t *dirents2;
+ apr_hash_t *merged_dirents;
+ apr_array_header_t *sorted_dirents;
+ int i;
+ apr_pool_t *iterpool;
+
+ if (b->recursing_within_adm_dir)
+ {
+ if (svn_dirent_skip_ancestor(b->adm_dir_abspath, local_abspath))
+ return SVN_NO_ERROR;
+ else
+ {
+ b->recursing_within_adm_dir = FALSE;
+ b->adm_dir_abspath = NULL;
+ }
+ }
+ else if (svn_wc_is_adm_dir(svn_dirent_basename(local_abspath, NULL),
+ scratch_pool))
+ {
+ b->recursing_within_adm_dir = TRUE;
+ b->adm_dir_abspath = apr_pstrdup(b->pool, local_abspath);
+ return SVN_NO_ERROR;
+ }
+
+ if (b->recursing_within_added_subtree)
+ child_relpath = svn_dirent_skip_ancestor(b->root2_abspath, local_abspath);
+ else
+ child_relpath = svn_dirent_skip_ancestor(b->root1_abspath, local_abspath);
+ if (!child_relpath)
+ return SVN_NO_ERROR;
+
+ local_abspath1 = svn_dirent_join(b->root1_abspath, child_relpath,
+ scratch_pool);
+ SVN_ERR(svn_io_check_resolved_path(local_abspath1, &kind1, scratch_pool));
+
+ local_abspath2 = svn_dirent_join(b->root2_abspath, child_relpath,
+ scratch_pool);
+ SVN_ERR(svn_io_check_resolved_path(local_abspath2, &kind2, scratch_pool));
+
+ if (depth > svn_depth_empty)
+ {
+ if (kind1 == svn_node_dir)
+ SVN_ERR(svn_io_get_dirents3(&dirents1, local_abspath1,
+ TRUE, /* only_check_type */
+ scratch_pool, scratch_pool));
+ else
+ dirents1 = apr_hash_make(scratch_pool);
+ }
+
+ if (kind2 == svn_node_dir)
+ {
+ apr_hash_t *original_props;
+ apr_hash_t *modified_props;
+ apr_array_header_t *prop_changes;
+
+ /* Show any property changes for this directory. */
+ SVN_ERR(get_props(&original_props, local_abspath1, b->ctx->wc_ctx,
+ scratch_pool, scratch_pool));
+ SVN_ERR(get_props(&modified_props, local_abspath2, b->ctx->wc_ctx,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_prop_diffs(&prop_changes, modified_props, original_props,
+ scratch_pool));
+ if (prop_changes->nelts > 0)
+ SVN_ERR(b->callbacks->dir_props_changed(NULL, NULL, child_relpath,
+ FALSE /* was_added */,
+ prop_changes, original_props,
+ b->diff_baton,
+ scratch_pool));
+
+ if (depth > svn_depth_empty)
+ {
+ /* Read directory entries. */
+ SVN_ERR(svn_io_get_dirents3(&dirents2, local_abspath2,
+ TRUE, /* only_check_type */
+ scratch_pool, scratch_pool));
+ }
+ }
+ else if (depth > svn_depth_empty)
+ dirents2 = apr_hash_make(scratch_pool);
+
+ if (depth <= svn_depth_empty)
+ return SVN_NO_ERROR;
+
+ /* Compare dirents1 to dirents2 and show added/deleted/changed files. */
+ merged_dirents = apr_hash_merge(scratch_pool, dirents1, dirents2,
+ NULL, NULL);
+ sorted_dirents = svn_sort__hash(merged_dirents,
+ svn_sort_compare_items_as_paths,
+ scratch_pool);
+ iterpool = svn_pool_create(scratch_pool);
+ for (i = 0; i < sorted_dirents->nelts; i++)
+ {
+ svn_sort__item_t elt = APR_ARRAY_IDX(sorted_dirents, i, svn_sort__item_t);
+ const char *name = elt.key;
+ svn_io_dirent2_t *dirent1;
+ svn_io_dirent2_t *dirent2;
+ const char *child1_abspath;
+ const char *child2_abspath;
+
+ svn_pool_clear(iterpool);
+
+ if (b->ctx->cancel_func)
+ SVN_ERR(b->ctx->cancel_func(b->ctx->cancel_baton));
+
+ if (strcmp(name, SVN_WC_ADM_DIR_NAME) == 0)
+ continue;
+
+ dirent1 = svn_hash_gets(dirents1, name);
+ if (!dirent1)
+ {
+ dirent1 = svn_io_dirent2_create(iterpool);
+ dirent1->kind = svn_node_none;
+ }
+ dirent2 = svn_hash_gets(dirents2, name);
+ if (!dirent2)
+ {
+ dirent2 = svn_io_dirent2_create(iterpool);
+ dirent2->kind = svn_node_none;
+ }
+
+ child1_abspath = svn_dirent_join(local_abspath1, name, iterpool);
+ child2_abspath = svn_dirent_join(local_abspath2, name, iterpool);
+
+ if (dirent1->special)
+ SVN_ERR(svn_io_check_resolved_path(child1_abspath, &dirent1->kind,
+ iterpool));
+ if (dirent2->special)
+ SVN_ERR(svn_io_check_resolved_path(child1_abspath, &dirent2->kind,
+ iterpool));
+
+ if (dirent1->kind == svn_node_dir &&
+ dirent2->kind == svn_node_dir)
+ {
+ if (depth == svn_depth_immediates)
+ {
+ /* Not using the walker, so show property diffs on these dirs. */
+ SVN_ERR(do_arbitrary_dirs_diff(child1_abspath, child2_abspath,
+ b->root1_abspath, b->root2_abspath,
+ svn_depth_empty,
+ b->callbacks, b->diff_baton,
+ b->ctx, iterpool));
+ }
+ else
+ {
+ /* Either the walker will visit these directories (with
+ * depth=infinity) and they will be processed as 'this dir'
+ * later, or we're showing file children only (depth=files). */
+ continue;
+ }
+
+ }
+
+ /* Files that exist only in dirents1. */
+ if (dirent1->kind == svn_node_file &&
+ (dirent2->kind == svn_node_dir || dirent2->kind == svn_node_none))
+ SVN_ERR(do_arbitrary_files_diff(child1_abspath, b->empty_file_abspath,
+ svn_relpath_join(child_relpath, name,
+ iterpool),
+ FALSE, TRUE, NULL,
+ b->callbacks, b->diff_baton,
+ b->ctx, iterpool));
+
+ /* Files that exist only in dirents2. */
+ if (dirent2->kind == svn_node_file &&
+ (dirent1->kind == svn_node_dir || dirent1->kind == svn_node_none))
+ {
+ apr_hash_t *original_props;
+
+ SVN_ERR(get_props(&original_props, child1_abspath, b->ctx->wc_ctx,
+ scratch_pool, scratch_pool));
+ SVN_ERR(do_arbitrary_files_diff(b->empty_file_abspath, child2_abspath,
+ svn_relpath_join(child_relpath, name,
+ iterpool),
+ TRUE, FALSE, original_props,
+ b->callbacks, b->diff_baton,
+ b->ctx, iterpool));
+ }
+
+ /* Files that exist in dirents1 and dirents2. */
+ if (dirent1->kind == svn_node_file && dirent2->kind == svn_node_file)
+ SVN_ERR(do_arbitrary_files_diff(child1_abspath, child2_abspath,
+ svn_relpath_join(child_relpath, name,
+ iterpool),
+ FALSE, FALSE, NULL,
+ b->callbacks, b->diff_baton,
+ b->ctx, scratch_pool));
+
+ /* Directories that only exist in dirents2. These aren't crawled
+ * by this walker so we have to crawl them separately. */
+ if (depth > svn_depth_files &&
+ dirent2->kind == svn_node_dir &&
+ (dirent1->kind == svn_node_file || dirent1->kind == svn_node_none))
+ SVN_ERR(do_arbitrary_dirs_diff(child1_abspath, child2_abspath,
+ b->root1_abspath, b->root2_abspath,
+ depth <= svn_depth_immediates
+ ? svn_depth_empty
+ : svn_depth_infinity ,
+ b->callbacks, b->diff_baton,
+ b->ctx, iterpool));
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* An implementation of svn_io_walk_func_t.
+ * Note: LOCAL_ABSPATH is the path being crawled and can be on either side
+ * of the diff depending on baton->recursing_within_added_subtree. */
+static svn_error_t *
+arbitrary_diff_walker(void *baton, const char *local_abspath,
+ const apr_finfo_t *finfo,
+ apr_pool_t *scratch_pool)
+{
+ struct arbitrary_diff_walker_baton *b = baton;
+
+ if (b->ctx->cancel_func)
+ SVN_ERR(b->ctx->cancel_func(b->ctx->cancel_baton));
+
+ if (finfo->filetype != APR_DIR)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(arbitrary_diff_this_dir(b, local_abspath, svn_depth_infinity,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__arbitrary_nodes_diff(const char *local_abspath1,
+ const char *local_abspath2,
+ svn_depth_t depth,
+ const svn_wc_diff_callbacks4_t *callbacks,
+ void *diff_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_node_kind_t kind1;
+ svn_node_kind_t kind2;
+
+ SVN_ERR(svn_io_check_resolved_path(local_abspath1, &kind1, scratch_pool));
+ SVN_ERR(svn_io_check_resolved_path(local_abspath2, &kind2, scratch_pool));
+
+ if (kind1 != kind2)
+ return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
+ _("'%s' is not the same node kind as '%s'"),
+ local_abspath1, local_abspath2);
+
+ if (depth == svn_depth_unknown)
+ depth = svn_depth_infinity;
+
+ if (kind1 == svn_node_file)
+ SVN_ERR(do_arbitrary_files_diff(local_abspath1, local_abspath2,
+ svn_dirent_basename(local_abspath1,
+ scratch_pool),
+ FALSE, FALSE, NULL,
+ callbacks, diff_baton,
+ ctx, scratch_pool));
+ else if (kind1 == svn_node_dir)
+ SVN_ERR(do_arbitrary_dirs_diff(local_abspath1, local_abspath2,
+ NULL, NULL, depth,
+ callbacks, diff_baton,
+ ctx, scratch_pool));
+ else
+ return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
+ _("'%s' is not a file or directory"),
+ kind1 == svn_node_none ?
+ local_abspath1 : local_abspath2);
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/diff_summarize.c b/subversion/libsvn_client/diff_summarize.c
new file mode 100644
index 0000000..df0911b
--- /dev/null
+++ b/subversion/libsvn_client/diff_summarize.c
@@ -0,0 +1,317 @@
+/*
+ * repos_diff_summarize.c -- The diff callbacks for summarizing
+ * the differences of two repository versions
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+
+#include "svn_dirent_uri.h"
+#include "svn_hash.h"
+#include "svn_props.h"
+#include "svn_pools.h"
+
+#include "client.h"
+
+
+/* Diff callbacks baton. */
+struct summarize_baton_t {
+ /* The target path of the diff, relative to the anchor; "" if target == anchor. */
+ const char *target;
+
+ /* The summarize callback passed down from the API */
+ svn_client_diff_summarize_func_t summarize_func;
+
+ /* Is the diff handling reversed? (add<->delete) */
+ svn_boolean_t reversed;
+
+ /* The summarize callback baton */
+ void *summarize_func_baton;
+
+ /* Which paths have a prop change. Key is a (const char *) path; the value
+ * is any non-null pointer to indicate that this path has a prop change. */
+ apr_hash_t *prop_changes;
+};
+
+
+/* Call B->summarize_func with B->summarize_func_baton, passing it a
+ * summary object composed from PATH (but made to be relative to the target
+ * of the diff), SUMMARIZE_KIND, PROP_CHANGED (or FALSE if the action is an
+ * add or delete) and NODE_KIND. */
+static svn_error_t *
+send_summary(struct summarize_baton_t *b,
+ const char *path,
+ svn_client_diff_summarize_kind_t summarize_kind,
+ svn_boolean_t prop_changed,
+ svn_node_kind_t node_kind,
+ apr_pool_t *scratch_pool)
+{
+ svn_client_diff_summarize_t *sum = apr_pcalloc(scratch_pool, sizeof(*sum));
+
+ SVN_ERR_ASSERT(summarize_kind != svn_client_diff_summarize_kind_normal
+ || prop_changed);
+
+ if (b->reversed)
+ {
+ switch(summarize_kind)
+ {
+ case svn_client_diff_summarize_kind_added:
+ summarize_kind = svn_client_diff_summarize_kind_deleted;
+ break;
+ case svn_client_diff_summarize_kind_deleted:
+ summarize_kind = svn_client_diff_summarize_kind_added;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* PATH is relative to the anchor of the diff, but SUM->path needs to be
+ relative to the target of the diff. */
+ sum->path = svn_relpath_skip_ancestor(b->target, path);
+ sum->summarize_kind = summarize_kind;
+ if (summarize_kind == svn_client_diff_summarize_kind_modified
+ || summarize_kind == svn_client_diff_summarize_kind_normal)
+ sum->prop_changed = prop_changed;
+ sum->node_kind = node_kind;
+
+ SVN_ERR(b->summarize_func(sum, b->summarize_func_baton, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* Are there any changes to relevant (normal) props in PROPCHANGES? */
+static svn_boolean_t
+props_changed(const apr_array_header_t *propchanges,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *props;
+
+ svn_error_clear(svn_categorize_props(propchanges, NULL, NULL, &props,
+ scratch_pool));
+ return (props->nelts != 0);
+}
+
+
+static svn_error_t *
+cb_dir_deleted(svn_wc_notify_state_t *state,
+ svn_boolean_t *tree_conflicted,
+ const char *path,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct summarize_baton_t *b = diff_baton;
+
+ SVN_ERR(send_summary(b, path, svn_client_diff_summarize_kind_deleted,
+ FALSE, svn_node_dir, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+cb_file_deleted(svn_wc_notify_state_t *state,
+ svn_boolean_t *tree_conflicted,
+ const char *path,
+ const char *tmpfile1,
+ const char *tmpfile2,
+ const char *mimetype1,
+ const char *mimetype2,
+ apr_hash_t *originalprops,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct summarize_baton_t *b = diff_baton;
+
+ SVN_ERR(send_summary(b, path, svn_client_diff_summarize_kind_deleted,
+ FALSE, svn_node_file, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+cb_dir_added(svn_wc_notify_state_t *state,
+ svn_boolean_t *tree_conflicted,
+ svn_boolean_t *skip,
+ svn_boolean_t *skip_children,
+ const char *path,
+ svn_revnum_t rev,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_revision,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+cb_dir_opened(svn_boolean_t *tree_conflicted,
+ svn_boolean_t *skip,
+ svn_boolean_t *skip_children,
+ const char *path,
+ svn_revnum_t rev,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+cb_dir_closed(svn_wc_notify_state_t *contentstate,
+ svn_wc_notify_state_t *propstate,
+ svn_boolean_t *tree_conflicted,
+ const char *path,
+ svn_boolean_t dir_was_added,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct summarize_baton_t *b = diff_baton;
+ svn_boolean_t prop_change;
+
+ if (! svn_relpath_skip_ancestor(b->target, path))
+ return SVN_NO_ERROR;
+
+ prop_change = svn_hash_gets(b->prop_changes, path) != NULL;
+ if (dir_was_added || prop_change)
+ SVN_ERR(send_summary(b, path,
+ dir_was_added ? svn_client_diff_summarize_kind_added
+ : svn_client_diff_summarize_kind_normal,
+ prop_change, svn_node_dir, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+cb_file_added(svn_wc_notify_state_t *contentstate,
+ svn_wc_notify_state_t *propstate,
+ svn_boolean_t *tree_conflicted,
+ const char *path,
+ const char *tmpfile1,
+ const char *tmpfile2,
+ svn_revnum_t rev1,
+ svn_revnum_t rev2,
+ const char *mimetype1,
+ const char *mimetype2,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_revision,
+ const apr_array_header_t *propchanges,
+ apr_hash_t *originalprops,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct summarize_baton_t *b = diff_baton;
+
+ SVN_ERR(send_summary(b, path, svn_client_diff_summarize_kind_added,
+ props_changed(propchanges, scratch_pool),
+ svn_node_file, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+cb_file_opened(svn_boolean_t *tree_conflicted,
+ svn_boolean_t *skip,
+ const char *path,
+ svn_revnum_t rev,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+cb_file_changed(svn_wc_notify_state_t *contentstate,
+ svn_wc_notify_state_t *propstate,
+ svn_boolean_t *tree_conflicted,
+ const char *path,
+ const char *tmpfile1,
+ const char *tmpfile2,
+ svn_revnum_t rev1,
+ svn_revnum_t rev2,
+ const char *mimetype1,
+ const char *mimetype2,
+ const apr_array_header_t *propchanges,
+ apr_hash_t *originalprops,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct summarize_baton_t *b = diff_baton;
+ svn_boolean_t text_change = (tmpfile2 != NULL);
+ svn_boolean_t prop_change = props_changed(propchanges, scratch_pool);
+
+ if (text_change || prop_change)
+ SVN_ERR(send_summary(b, path,
+ text_change ? svn_client_diff_summarize_kind_modified
+ : svn_client_diff_summarize_kind_normal,
+ prop_change, svn_node_file, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+cb_dir_props_changed(svn_wc_notify_state_t *propstate,
+ svn_boolean_t *tree_conflicted,
+ const char *path,
+ svn_boolean_t dir_was_added,
+ const apr_array_header_t *propchanges,
+ apr_hash_t *original_props,
+ void *diff_baton,
+ apr_pool_t *scratch_pool)
+{
+ struct summarize_baton_t *b = diff_baton;
+
+ if (props_changed(propchanges, scratch_pool))
+ svn_hash_sets(b->prop_changes, path, path);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__get_diff_summarize_callbacks(
+ svn_wc_diff_callbacks4_t **callbacks,
+ void **callback_baton,
+ const char *target,
+ svn_boolean_t reversed,
+ svn_client_diff_summarize_func_t summarize_func,
+ void *summarize_baton,
+ apr_pool_t *pool)
+{
+ svn_wc_diff_callbacks4_t *cb = apr_palloc(pool, sizeof(*cb));
+ struct summarize_baton_t *b = apr_palloc(pool, sizeof(*b));
+
+ b->target = target;
+ b->summarize_func = summarize_func;
+ b->summarize_func_baton = summarize_baton;
+ b->prop_changes = apr_hash_make(pool);
+ b->reversed = reversed;
+
+ cb->file_opened = cb_file_opened;
+ cb->file_changed = cb_file_changed;
+ cb->file_added = cb_file_added;
+ cb->file_deleted = cb_file_deleted;
+ cb->dir_deleted = cb_dir_deleted;
+ cb->dir_opened = cb_dir_opened;
+ cb->dir_added = cb_dir_added;
+ cb->dir_props_changed = cb_dir_props_changed;
+ cb->dir_closed = cb_dir_closed;
+
+ *callbacks = cb;
+ *callback_baton = b;
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/export.c b/subversion/libsvn_client/export.c
new file mode 100644
index 0000000..d6022ed
--- /dev/null
+++ b/subversion/libsvn_client/export.c
@@ -0,0 +1,1589 @@
+/*
+ * export.c: export a tree.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <apr_file_io.h>
+#include <apr_md5.h>
+#include "svn_types.h"
+#include "svn_client.h"
+#include "svn_string.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_hash.h"
+#include "svn_path.h"
+#include "svn_pools.h"
+#include "svn_subst.h"
+#include "svn_time.h"
+#include "svn_props.h"
+#include "client.h"
+
+#include "svn_private_config.h"
+#include "private/svn_subr_private.h"
+#include "private/svn_delta_private.h"
+#include "private/svn_wc_private.h"
+
+#ifndef ENABLE_EV2_IMPL
+#define ENABLE_EV2_IMPL 0
+#endif
+
+
+/*** Code. ***/
+
+/* Add EXTERNALS_PROP_VAL for the export destination path PATH to
+ TRAVERSAL_INFO. */
+static svn_error_t *
+add_externals(apr_hash_t *externals,
+ const char *path,
+ const svn_string_t *externals_prop_val)
+{
+ apr_pool_t *pool = apr_hash_pool_get(externals);
+ const char *local_abspath;
+
+ if (! externals_prop_val)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, pool));
+
+ svn_hash_sets(externals, local_abspath,
+ apr_pstrmemdup(pool, externals_prop_val->data,
+ externals_prop_val->len));
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper function that gets the eol style and optionally overrides the
+ EOL marker for files marked as native with the EOL marker matching
+ the string specified in requested_value which is of the same format
+ as the svn:eol-style property values. */
+static svn_error_t *
+get_eol_style(svn_subst_eol_style_t *style,
+ const char **eol,
+ const char *value,
+ const char *requested_value)
+{
+ svn_subst_eol_style_from_value(style, eol, value);
+ if (requested_value && *style == svn_subst_eol_style_native)
+ {
+ svn_subst_eol_style_t requested_style;
+ const char *requested_eol;
+
+ svn_subst_eol_style_from_value(&requested_style, &requested_eol,
+ requested_value);
+
+ if (requested_style == svn_subst_eol_style_fixed)
+ *eol = requested_eol;
+ else
+ return svn_error_createf(SVN_ERR_IO_UNKNOWN_EOL, NULL,
+ _("'%s' is not a valid EOL value"),
+ requested_value);
+ }
+ return SVN_NO_ERROR;
+}
+
+/* If *APPENDABLE_DIRENT_P represents an existing directory, then append
+ * to it the basename of BASENAME_OF and return the result in
+ * *APPENDABLE_DIRENT_P. The kind of BASENAME_OF is either dirent or uri,
+ * as given by IS_URI.
+ */
+static svn_error_t *
+append_basename_if_dir(const char **appendable_dirent_p,
+ const char *basename_of,
+ svn_boolean_t is_uri,
+ apr_pool_t *pool)
+{
+ svn_node_kind_t local_kind;
+ SVN_ERR(svn_io_check_resolved_path(*appendable_dirent_p, &local_kind, pool));
+ if (local_kind == svn_node_dir)
+ {
+ const char *base_name;
+
+ if (is_uri)
+ base_name = svn_uri_basename(basename_of, pool);
+ else
+ base_name = svn_dirent_basename(basename_of, NULL);
+
+ *appendable_dirent_p = svn_dirent_join(*appendable_dirent_p,
+ base_name, pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Make an unversioned copy of the versioned file at FROM_ABSPATH. Copy it
+ * to the destination path TO_ABSPATH.
+ *
+ * If REVISION is svn_opt_revision_working, copy the working version,
+ * otherwise copy the base version.
+ *
+ * Expand the file's keywords according to the source file's 'svn:keywords'
+ * property, if present. If copying a locally modified working version,
+ * append 'M' to the revision number and use '(local)' for the author.
+ *
+ * Translate the file's line endings according to the source file's
+ * 'svn:eol-style' property, if present. If NATIVE_EOL is not NULL, use it
+ * in place of the native EOL style. Throw an error if the source file has
+ * inconsistent line endings and EOL translation is attempted.
+ *
+ * Set the destination file's modification time to the source file's
+ * modification time if copying the working version and the working version
+ * is locally modified; otherwise set it to the versioned file's last
+ * changed time.
+ *
+ * Set the destination file's 'executable' flag according to the source
+ * file's 'svn:executable' property.
+ */
+
+/* baton for export_node */
+struct export_info_baton
+{
+ const char *to_path;
+ const svn_opt_revision_t *revision;
+ svn_boolean_t ignore_keywords;
+ svn_boolean_t overwrite;
+ svn_wc_context_t *wc_ctx;
+ const char *native_eol;
+ svn_wc_notify_func2_t notify_func;
+ void *notify_baton;
+ const char *origin_abspath;
+ svn_boolean_t exported;
+};
+
+/* Export a file or directory. Implements svn_wc_status_func4_t */
+static svn_error_t *
+export_node(void *baton,
+ const char *local_abspath,
+ const svn_wc_status3_t *status,
+ apr_pool_t *scratch_pool)
+{
+ struct export_info_baton *eib = baton;
+ svn_wc_context_t *wc_ctx = eib->wc_ctx;
+ apr_hash_t *kw = NULL;
+ svn_subst_eol_style_t style;
+ apr_hash_t *props;
+ svn_string_t *eol_style, *keywords, *executable, *special;
+ const char *eol = NULL;
+ svn_boolean_t local_mod = FALSE;
+ apr_time_t tm;
+ svn_stream_t *source;
+ svn_stream_t *dst_stream;
+ const char *dst_tmp;
+ svn_error_t *err;
+
+ const char *to_abspath = svn_dirent_join(
+ eib->to_path,
+ svn_dirent_skip_ancestor(eib->origin_abspath,
+ local_abspath),
+ scratch_pool);
+
+ eib->exported = TRUE;
+
+ /* Don't export 'deleted' files and directories unless it's a
+ revision other than WORKING. These files and directories
+ don't really exist in WORKING. */
+ if (eib->revision->kind == svn_opt_revision_working
+ && status->node_status == svn_wc_status_deleted)
+ return SVN_NO_ERROR;
+
+ if (status->kind == svn_node_dir)
+ {
+ apr_fileperms_t perm = APR_OS_DEFAULT;
+
+ /* Try to make the new directory. If this fails because the
+ directory already exists, check our FORCE flag to see if we
+ care. */
+
+ /* Keep the source directory's permissions if applicable.
+ Skip retrieving the umask on windows. Apr does not implement setting
+ filesystem privileges on Windows.
+ Retrieving the file permissions with APR_FINFO_PROT | APR_FINFO_OWNER
+ is documented to be 'incredibly expensive' */
+#ifndef WIN32
+ if (eib->revision->kind == svn_opt_revision_working)
+ {
+ apr_finfo_t finfo;
+ SVN_ERR(svn_io_stat(&finfo, local_abspath, APR_FINFO_PROT,
+ scratch_pool));
+ perm = finfo.protection;
+ }
+#endif
+ err = svn_io_dir_make(to_abspath, perm, scratch_pool);
+ if (err)
+ {
+ if (! APR_STATUS_IS_EEXIST(err->apr_err))
+ return svn_error_trace(err);
+ if (! eib->overwrite)
+ SVN_ERR_W(err, _("Destination directory exists, and will not be "
+ "overwritten unless forced"));
+ else
+ svn_error_clear(err);
+ }
+
+ if (eib->notify_func
+ && (strcmp(eib->origin_abspath, local_abspath) != 0))
+ {
+ svn_wc_notify_t *notify =
+ svn_wc_create_notify(to_abspath,
+ svn_wc_notify_update_add, scratch_pool);
+
+ notify->kind = svn_node_dir;
+ (eib->notify_func)(eib->notify_baton, notify, scratch_pool);
+ }
+
+ return SVN_NO_ERROR;
+ }
+ else if (status->kind != svn_node_file)
+ {
+ if (strcmp(eib->origin_abspath, local_abspath) != 0)
+ return SVN_NO_ERROR;
+
+ return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL,
+ _("The node '%s' was not found."),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+ }
+
+ if (status->file_external)
+ return SVN_NO_ERROR;
+
+ /* Produce overwrite errors for the export root */
+ if (strcmp(local_abspath, eib->origin_abspath) == 0)
+ {
+ svn_node_kind_t to_kind;
+
+ SVN_ERR(svn_io_check_path(to_abspath, &to_kind, scratch_pool));
+
+ if ((to_kind == svn_node_file || to_kind == svn_node_unknown)
+ && !eib->overwrite)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Destination file '%s' exists, and "
+ "will not be overwritten unless forced"),
+ svn_dirent_local_style(to_abspath,
+ scratch_pool));
+ else if (to_kind == svn_node_dir)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Destination '%s' exists. Cannot "
+ "overwrite directory with non-directory"),
+ svn_dirent_local_style(to_abspath,
+ scratch_pool));
+ }
+
+ if (eib->revision->kind != svn_opt_revision_working)
+ {
+ /* Only export 'added' files when the revision is WORKING. This is not
+ WORKING, so skip the 'added' files, since they didn't exist
+ in the BASE revision and don't have an associated text-base.
+
+ 'replaced' files are technically the same as 'added' files.
+ ### TODO: Handle replaced nodes properly.
+ ### svn_opt_revision_base refers to the "new"
+ ### base of the node. That means, if a node is locally
+ ### replaced, export skips this node, as if it was locally
+ ### added, because svn_opt_revision_base refers to the base
+ ### of the added node, not to the node that was deleted.
+ ### In contrast, when the node is copied-here or moved-here,
+ ### the copy/move source's content will be exported.
+ ### It is currently not possible to export the revert-base
+ ### when a node is locally replaced. We need a new
+ ### svn_opt_revision_ enum value for proper distinction
+ ### between revert-base and commit-base.
+
+ Copied-/moved-here nodes have a base, so export both added and
+ replaced files when they involve a copy-/move-here.
+
+ We get all this for free from evaluating SOURCE == NULL:
+ */
+ SVN_ERR(svn_wc_get_pristine_contents2(&source, wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+ if (source == NULL)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(svn_wc_get_pristine_props(&props, wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+ }
+ else
+ {
+ /* ### hmm. this isn't always a specialfile. this will simply open
+ ### the file readonly if it is a regular file. */
+ SVN_ERR(svn_subst_read_specialfile(&source, local_abspath, scratch_pool,
+ scratch_pool));
+
+ SVN_ERR(svn_wc_prop_list2(&props, wc_ctx, local_abspath, scratch_pool,
+ scratch_pool));
+ if (status->node_status != svn_wc_status_normal)
+ local_mod = TRUE;
+ }
+
+ /* We can early-exit if we're creating a special file. */
+ special = svn_hash_gets(props, SVN_PROP_SPECIAL);
+ if (special != NULL)
+ {
+ /* Create the destination as a special file, and copy the source
+ details into the destination stream. */
+ /* ### And forget the notification */
+ SVN_ERR(svn_subst_create_specialfile(&dst_stream, to_abspath,
+ scratch_pool, scratch_pool));
+ return svn_error_trace(
+ svn_stream_copy3(source, dst_stream, NULL, NULL, scratch_pool));
+ }
+
+
+ eol_style = svn_hash_gets(props, SVN_PROP_EOL_STYLE);
+ keywords = svn_hash_gets(props, SVN_PROP_KEYWORDS);
+ executable = svn_hash_gets(props, SVN_PROP_EXECUTABLE);
+
+ if (eol_style)
+ SVN_ERR(get_eol_style(&style, &eol, eol_style->data, eib->native_eol));
+
+ if (local_mod)
+ {
+ /* Use the modified time from the working copy of
+ the file */
+ SVN_ERR(svn_io_file_affected_time(&tm, local_abspath, scratch_pool));
+ }
+ else
+ {
+ tm = status->changed_date;
+ }
+
+ if (keywords)
+ {
+ svn_revnum_t changed_rev = status->changed_rev;
+ const char *suffix;
+ const char *url = svn_path_url_add_component2(status->repos_root_url,
+ status->repos_relpath,
+ scratch_pool);
+ const char *author = status->changed_author;
+ if (local_mod)
+ {
+ /* For locally modified files, we'll append an 'M'
+ to the revision number, and set the author to
+ "(local)" since we can't always determine the
+ current user's username */
+ suffix = "M";
+ author = _("(local)");
+ }
+ else
+ {
+ suffix = "";
+ }
+
+ SVN_ERR(svn_subst_build_keywords3(&kw, keywords->data,
+ apr_psprintf(scratch_pool, "%ld%s",
+ changed_rev, suffix),
+ url, status->repos_root_url, tm,
+ author, scratch_pool));
+ }
+
+ /* For atomicity, we translate to a tmp file and then rename the tmp file
+ over the real destination. */
+ SVN_ERR(svn_stream_open_unique(&dst_stream, &dst_tmp,
+ svn_dirent_dirname(to_abspath, scratch_pool),
+ svn_io_file_del_none, scratch_pool,
+ scratch_pool));
+
+ /* If some translation is needed, then wrap the output stream (this is
+ more efficient than wrapping the input). */
+ if (eol || (kw && (apr_hash_count(kw) > 0)))
+ dst_stream = svn_subst_stream_translated(dst_stream,
+ eol,
+ FALSE /* repair */,
+ kw,
+ ! eib->ignore_keywords /* expand */,
+ scratch_pool);
+
+ /* ###: use cancel func/baton in place of NULL/NULL below. */
+ err = svn_stream_copy3(source, dst_stream, NULL, NULL, scratch_pool);
+
+ if (!err && executable)
+ err = svn_io_set_file_executable(dst_tmp, TRUE, FALSE, scratch_pool);
+
+ if (!err)
+ err = svn_io_set_file_affected_time(tm, dst_tmp, scratch_pool);
+
+ if (err)
+ return svn_error_compose_create(err, svn_io_remove_file2(dst_tmp, FALSE,
+ scratch_pool));
+
+ /* Now that dst_tmp contains the translated data, do the atomic rename. */
+ SVN_ERR(svn_io_file_rename(dst_tmp, to_abspath, scratch_pool));
+
+ if (eib->notify_func)
+ {
+ svn_wc_notify_t *notify = svn_wc_create_notify(to_abspath,
+ svn_wc_notify_update_add, scratch_pool);
+ notify->kind = svn_node_file;
+ (eib->notify_func)(eib->notify_baton, notify, scratch_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Abstraction of open_root.
+ *
+ * Create PATH if it does not exist and is not obstructed, and invoke
+ * NOTIFY_FUNC with NOTIFY_BATON on PATH.
+ *
+ * If PATH exists but is a file, then error with SVN_ERR_WC_NOT_WORKING_COPY.
+ *
+ * If PATH is a already a directory, then error with
+ * SVN_ERR_WC_OBSTRUCTED_UPDATE, unless FORCE, in which case just
+ * export into PATH with no error.
+ */
+static svn_error_t *
+open_root_internal(const char *path,
+ svn_boolean_t force,
+ svn_wc_notify_func2_t notify_func,
+ void *notify_baton,
+ apr_pool_t *pool)
+{
+ svn_node_kind_t kind;
+
+ SVN_ERR(svn_io_check_path(path, &kind, pool));
+ if (kind == svn_node_none)
+ SVN_ERR(svn_io_make_dir_recursively(path, pool));
+ else if (kind == svn_node_file)
+ return svn_error_createf(SVN_ERR_WC_NOT_WORKING_COPY, NULL,
+ _("'%s' exists and is not a directory"),
+ svn_dirent_local_style(path, pool));
+ else if ((kind != svn_node_dir) || (! force))
+ return svn_error_createf(SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
+ _("'%s' already exists"),
+ svn_dirent_local_style(path, pool));
+
+ if (notify_func)
+ {
+ svn_wc_notify_t *notify = svn_wc_create_notify(path,
+ svn_wc_notify_update_add,
+ pool);
+ notify->kind = svn_node_dir;
+ (*notify_func)(notify_baton, notify, pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* ---------------------------------------------------------------------- */
+
+
+/*** A dedicated 'export' editor, which does no .svn/ accounting. ***/
+
+
+struct edit_baton
+{
+ const char *repos_root_url;
+ const char *root_path;
+ const char *root_url;
+ svn_boolean_t force;
+ svn_revnum_t *target_revision;
+ apr_hash_t *externals;
+ const char *native_eol;
+ svn_boolean_t ignore_keywords;
+
+ svn_cancel_func_t cancel_func;
+ void *cancel_baton;
+ svn_wc_notify_func2_t notify_func;
+ void *notify_baton;
+};
+
+
+struct dir_baton
+{
+ struct edit_baton *edit_baton;
+ const char *path;
+};
+
+
+struct file_baton
+{
+ struct edit_baton *edit_baton;
+
+ const char *path;
+ const char *tmppath;
+
+ /* We need to keep this around so we can explicitly close it in close_file,
+ thus flushing its output to disk so we can copy and translate it. */
+ svn_stream_t *tmp_stream;
+
+ /* The MD5 digest of the file's fulltext. This is all zeros until
+ the last textdelta window handler call returns. */
+ unsigned char text_digest[APR_MD5_DIGESTSIZE];
+
+ /* The three svn: properties we might actually care about. */
+ const svn_string_t *eol_style_val;
+ const svn_string_t *keywords_val;
+ const svn_string_t *executable_val;
+ svn_boolean_t special;
+
+ /* Any keyword vals to be substituted */
+ const char *revision;
+ const char *url;
+ const char *repos_root_url;
+ const char *author;
+ apr_time_t date;
+
+ /* Pool associated with this baton. */
+ apr_pool_t *pool;
+};
+
+
+struct handler_baton
+{
+ svn_txdelta_window_handler_t apply_handler;
+ void *apply_baton;
+ apr_pool_t *pool;
+ const char *tmppath;
+};
+
+
+static svn_error_t *
+set_target_revision(void *edit_baton,
+ svn_revnum_t target_revision,
+ apr_pool_t *pool)
+{
+ struct edit_baton *eb = edit_baton;
+
+ /* Stashing a target_revision in the baton */
+ *(eb->target_revision) = target_revision;
+ return SVN_NO_ERROR;
+}
+
+
+
+/* Just ensure that the main export directory exists. */
+static svn_error_t *
+open_root(void *edit_baton,
+ svn_revnum_t base_revision,
+ apr_pool_t *pool,
+ void **root_baton)
+{
+ struct edit_baton *eb = edit_baton;
+ struct dir_baton *db = apr_pcalloc(pool, sizeof(*db));
+
+ SVN_ERR(open_root_internal(eb->root_path, eb->force,
+ eb->notify_func, eb->notify_baton, pool));
+
+ /* Build our dir baton. */
+ db->path = eb->root_path;
+ db->edit_baton = eb;
+ *root_baton = db;
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Ensure the directory exists, and send feedback. */
+static svn_error_t *
+add_directory(const char *path,
+ void *parent_baton,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_revision,
+ apr_pool_t *pool,
+ void **baton)
+{
+ struct dir_baton *pb = parent_baton;
+ struct dir_baton *db = apr_pcalloc(pool, sizeof(*db));
+ struct edit_baton *eb = pb->edit_baton;
+ const char *full_path = svn_dirent_join(eb->root_path, path, pool);
+ svn_node_kind_t kind;
+
+ SVN_ERR(svn_io_check_path(full_path, &kind, pool));
+ if (kind == svn_node_none)
+ SVN_ERR(svn_io_dir_make(full_path, APR_OS_DEFAULT, pool));
+ else if (kind == svn_node_file)
+ return svn_error_createf(SVN_ERR_WC_NOT_WORKING_COPY, NULL,
+ _("'%s' exists and is not a directory"),
+ svn_dirent_local_style(full_path, pool));
+ else if (! (kind == svn_node_dir && eb->force))
+ return svn_error_createf(SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
+ _("'%s' already exists"),
+ svn_dirent_local_style(full_path, pool));
+
+ if (eb->notify_func)
+ {
+ svn_wc_notify_t *notify = svn_wc_create_notify(full_path,
+ svn_wc_notify_update_add,
+ pool);
+ notify->kind = svn_node_dir;
+ (*eb->notify_func)(eb->notify_baton, notify, pool);
+ }
+
+ /* Build our dir baton. */
+ db->path = full_path;
+ db->edit_baton = eb;
+ *baton = db;
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Build a file baton. */
+static svn_error_t *
+add_file(const char *path,
+ void *parent_baton,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_revision,
+ apr_pool_t *pool,
+ void **baton)
+{
+ struct dir_baton *pb = parent_baton;
+ struct edit_baton *eb = pb->edit_baton;
+ struct file_baton *fb = apr_pcalloc(pool, sizeof(*fb));
+ const char *full_path = svn_dirent_join(eb->root_path, path, pool);
+
+ /* PATH is not canonicalized, i.e. it may still contain spaces etc.
+ * but EB->root_url is. */
+ const char *full_url = svn_path_url_add_component2(eb->root_url,
+ path,
+ pool);
+
+ fb->edit_baton = eb;
+ fb->path = full_path;
+ fb->url = full_url;
+ fb->repos_root_url = eb->repos_root_url;
+ fb->pool = pool;
+
+ *baton = fb;
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+window_handler(svn_txdelta_window_t *window, void *baton)
+{
+ struct handler_baton *hb = baton;
+ svn_error_t *err;
+
+ err = hb->apply_handler(window, hb->apply_baton);
+ if (err)
+ {
+ /* We failed to apply the patch; clean up the temporary file. */
+ err = svn_error_compose_create(
+ err,
+ svn_io_remove_file2(hb->tmppath, TRUE, hb->pool));
+ }
+
+ return svn_error_trace(err);
+}
+
+
+
+/* Write incoming data into the tmpfile stream */
+static svn_error_t *
+apply_textdelta(void *file_baton,
+ const char *base_checksum,
+ apr_pool_t *pool,
+ svn_txdelta_window_handler_t *handler,
+ void **handler_baton)
+{
+ struct file_baton *fb = file_baton;
+ struct handler_baton *hb = apr_palloc(pool, sizeof(*hb));
+
+ /* Create a temporary file in the same directory as the file. We're going
+ to rename the thing into place when we're done. */
+ SVN_ERR(svn_stream_open_unique(&fb->tmp_stream, &fb->tmppath,
+ svn_dirent_dirname(fb->path, pool),
+ svn_io_file_del_none, fb->pool, fb->pool));
+
+ hb->pool = pool;
+ hb->tmppath = fb->tmppath;
+
+ /* svn_txdelta_apply() closes the stream, but we want to close it in the
+ close_file() function, so disown it here. */
+ /* ### contrast to when we call svn_ra_get_file() which does NOT close the
+ ### tmp_stream. we *should* be much more consistent! */
+ svn_txdelta_apply(svn_stream_empty(pool),
+ svn_stream_disown(fb->tmp_stream, pool),
+ fb->text_digest, NULL, pool,
+ &hb->apply_handler, &hb->apply_baton);
+
+ *handler_baton = hb;
+ *handler = window_handler;
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+change_file_prop(void *file_baton,
+ const char *name,
+ const svn_string_t *value,
+ apr_pool_t *pool)
+{
+ struct file_baton *fb = file_baton;
+
+ if (! value)
+ return SVN_NO_ERROR;
+
+ /* Store only the magic three properties. */
+ if (strcmp(name, SVN_PROP_EOL_STYLE) == 0)
+ fb->eol_style_val = svn_string_dup(value, fb->pool);
+
+ else if (! fb->edit_baton->ignore_keywords &&
+ strcmp(name, SVN_PROP_KEYWORDS) == 0)
+ fb->keywords_val = svn_string_dup(value, fb->pool);
+
+ else if (strcmp(name, SVN_PROP_EXECUTABLE) == 0)
+ fb->executable_val = svn_string_dup(value, fb->pool);
+
+ /* Try to fill out the baton's keywords-structure too. */
+ else if (strcmp(name, SVN_PROP_ENTRY_COMMITTED_REV) == 0)
+ fb->revision = apr_pstrdup(fb->pool, value->data);
+
+ else if (strcmp(name, SVN_PROP_ENTRY_COMMITTED_DATE) == 0)
+ SVN_ERR(svn_time_from_cstring(&fb->date, value->data, fb->pool));
+
+ else if (strcmp(name, SVN_PROP_ENTRY_LAST_AUTHOR) == 0)
+ fb->author = apr_pstrdup(fb->pool, value->data);
+
+ else if (strcmp(name, SVN_PROP_SPECIAL) == 0)
+ fb->special = TRUE;
+
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+change_dir_prop(void *dir_baton,
+ const char *name,
+ const svn_string_t *value,
+ apr_pool_t *pool)
+{
+ struct dir_baton *db = dir_baton;
+ struct edit_baton *eb = db->edit_baton;
+
+ if (value && (strcmp(name, SVN_PROP_EXTERNALS) == 0))
+ SVN_ERR(add_externals(eb->externals, db->path, value));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Move the tmpfile to file, and send feedback. */
+static svn_error_t *
+close_file(void *file_baton,
+ const char *text_digest,
+ apr_pool_t *pool)
+{
+ struct file_baton *fb = file_baton;
+ struct edit_baton *eb = fb->edit_baton;
+ svn_checksum_t *text_checksum;
+ svn_checksum_t *actual_checksum;
+
+ /* Was a txdelta even sent? */
+ if (! fb->tmppath)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(svn_stream_close(fb->tmp_stream));
+
+ SVN_ERR(svn_checksum_parse_hex(&text_checksum, svn_checksum_md5, text_digest,
+ pool));
+ actual_checksum = svn_checksum__from_digest_md5(fb->text_digest, pool);
+
+ /* Note that text_digest can be NULL when talking to certain repositories.
+ In that case text_checksum will be NULL and the following match code
+ will note that the checksums match */
+ if (!svn_checksum_match(text_checksum, actual_checksum))
+ return svn_checksum_mismatch_err(text_checksum, actual_checksum, pool,
+ _("Checksum mismatch for '%s'"),
+ svn_dirent_local_style(fb->path, pool));
+
+ if ((! fb->eol_style_val) && (! fb->keywords_val) && (! fb->special))
+ {
+ SVN_ERR(svn_io_file_rename(fb->tmppath, fb->path, pool));
+ }
+ else
+ {
+ svn_subst_eol_style_t style;
+ const char *eol = NULL;
+ svn_boolean_t repair = FALSE;
+ apr_hash_t *final_kw = NULL;
+
+ if (fb->eol_style_val)
+ {
+ SVN_ERR(get_eol_style(&style, &eol, fb->eol_style_val->data,
+ eb->native_eol));
+ repair = TRUE;
+ }
+
+ if (fb->keywords_val)
+ SVN_ERR(svn_subst_build_keywords3(&final_kw, fb->keywords_val->data,
+ fb->revision, fb->url,
+ fb->repos_root_url, fb->date,
+ fb->author, pool));
+
+ SVN_ERR(svn_subst_copy_and_translate4(fb->tmppath, fb->path,
+ eol, repair, final_kw,
+ TRUE, /* expand */
+ fb->special,
+ eb->cancel_func, eb->cancel_baton,
+ pool));
+
+ SVN_ERR(svn_io_remove_file2(fb->tmppath, FALSE, pool));
+ }
+
+ if (fb->executable_val)
+ SVN_ERR(svn_io_set_file_executable(fb->path, TRUE, FALSE, pool));
+
+ if (fb->date && (! fb->special))
+ SVN_ERR(svn_io_set_file_affected_time(fb->date, fb->path, pool));
+
+ if (fb->edit_baton->notify_func)
+ {
+ svn_wc_notify_t *notify = svn_wc_create_notify(fb->path,
+ svn_wc_notify_update_add,
+ pool);
+ notify->kind = svn_node_file;
+ (*fb->edit_baton->notify_func)(fb->edit_baton->notify_baton, notify,
+ pool);
+ }
+
+ 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)
+{
+ /* Always use empty props, since the node won't have pre-existing props
+ (This is an export, remember?) */
+ *props = apr_hash_make(result_pool);
+
+ return SVN_NO_ERROR;
+}
+
+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)
+{
+ /* An export always gets text against the empty stream (i.e, full texts). */
+ *filename = NULL;
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+get_editor_ev1(const svn_delta_editor_t **export_editor,
+ void **edit_baton,
+ struct edit_baton *eb,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_delta_editor_t *editor = svn_delta_default_editor(result_pool);
+
+ editor->set_target_revision = set_target_revision;
+ editor->open_root = open_root;
+ editor->add_directory = add_directory;
+ editor->add_file = add_file;
+ editor->apply_textdelta = apply_textdelta;
+ editor->close_file = close_file;
+ editor->change_file_prop = change_file_prop;
+ editor->change_dir_prop = change_dir_prop;
+
+ SVN_ERR(svn_delta_get_cancellation_editor(ctx->cancel_func,
+ ctx->cancel_baton,
+ editor,
+ eb,
+ export_editor,
+ edit_baton,
+ result_pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/*** The Ev2 Implementation ***/
+
+static svn_error_t *
+add_file_ev2(void *baton,
+ const char *relpath,
+ const svn_checksum_t *checksum,
+ svn_stream_t *contents,
+ apr_hash_t *props,
+ svn_revnum_t replaces_rev,
+ apr_pool_t *scratch_pool)
+{
+ struct edit_baton *eb = baton;
+ const char *full_path = svn_dirent_join(eb->root_path, relpath,
+ scratch_pool);
+ /* RELPATH is not canonicalized, i.e. it may still contain spaces etc.
+ * but EB->root_url is. */
+ const char *full_url = svn_path_url_add_component2(eb->root_url,
+ relpath,
+ scratch_pool);
+ const svn_string_t *val;
+ /* The four svn: properties we might actually care about. */
+ const svn_string_t *eol_style_val = NULL;
+ const svn_string_t *keywords_val = NULL;
+ const svn_string_t *executable_val = NULL;
+ svn_boolean_t special = FALSE;
+ /* Any keyword vals to be substituted */
+ const char *revision = NULL;
+ const char *author = NULL;
+ apr_time_t date = 0;
+
+ /* Look at any properties for additional information. */
+ if ( (val = svn_hash_gets(props, SVN_PROP_EOL_STYLE)) )
+ eol_style_val = val;
+
+ if ( !eb->ignore_keywords && (val = svn_hash_gets(props, SVN_PROP_KEYWORDS)) )
+ keywords_val = val;
+
+ if ( (val = svn_hash_gets(props, SVN_PROP_EXECUTABLE)) )
+ executable_val = val;
+
+ /* Try to fill out the baton's keywords-structure too. */
+ if ( (val = svn_hash_gets(props, SVN_PROP_ENTRY_COMMITTED_REV)) )
+ revision = val->data;
+
+ if ( (val = svn_hash_gets(props, SVN_PROP_ENTRY_COMMITTED_DATE)) )
+ SVN_ERR(svn_time_from_cstring(&date, val->data, scratch_pool));
+
+ if ( (val = svn_hash_gets(props, SVN_PROP_ENTRY_LAST_AUTHOR)) )
+ author = val->data;
+
+ if ( (val = svn_hash_gets(props, SVN_PROP_SPECIAL)) )
+ special = TRUE;
+
+ if (special)
+ {
+ svn_stream_t *tmp_stream;
+
+ SVN_ERR(svn_subst_create_specialfile(&tmp_stream, full_path,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_stream_copy3(contents, tmp_stream, eb->cancel_func,
+ eb->cancel_baton, scratch_pool));
+ }
+ else
+ {
+ svn_stream_t *tmp_stream;
+ const char *tmppath;
+
+ /* Create a temporary file in the same directory as the file. We're going
+ to rename the thing into place when we're done. */
+ SVN_ERR(svn_stream_open_unique(&tmp_stream, &tmppath,
+ svn_dirent_dirname(full_path,
+ scratch_pool),
+ svn_io_file_del_none,
+ scratch_pool, scratch_pool));
+
+ /* Possibly wrap the stream to be translated, as dictated by
+ the props. */
+ if (eol_style_val || keywords_val)
+ {
+ svn_subst_eol_style_t style;
+ const char *eol = NULL;
+ svn_boolean_t repair = FALSE;
+ apr_hash_t *final_kw = NULL;
+
+ if (eol_style_val)
+ {
+ SVN_ERR(get_eol_style(&style, &eol, eol_style_val->data,
+ eb->native_eol));
+ repair = TRUE;
+ }
+
+ if (keywords_val)
+ SVN_ERR(svn_subst_build_keywords3(&final_kw, keywords_val->data,
+ revision, full_url,
+ eb->repos_root_url,
+ date, author, scratch_pool));
+
+ /* Writing through a translated stream is more efficient than
+ reading through one, so we wrap TMP_STREAM and not CONTENTS. */
+ tmp_stream = svn_subst_stream_translated(tmp_stream, eol, repair,
+ final_kw, TRUE, /* expand */
+ scratch_pool);
+ }
+
+ SVN_ERR(svn_stream_copy3(contents, tmp_stream, eb->cancel_func,
+ eb->cancel_baton, scratch_pool));
+
+ /* Move the file into place. */
+ SVN_ERR(svn_io_file_rename(tmppath, full_path, scratch_pool));
+ }
+
+ if (executable_val)
+ SVN_ERR(svn_io_set_file_executable(full_path, TRUE, FALSE, scratch_pool));
+
+ if (date && (! special))
+ SVN_ERR(svn_io_set_file_affected_time(date, full_path, scratch_pool));
+
+ if (eb->notify_func)
+ {
+ svn_wc_notify_t *notify = svn_wc_create_notify(full_path,
+ svn_wc_notify_update_add,
+ scratch_pool);
+ notify->kind = svn_node_file;
+ (*eb->notify_func)(eb->notify_baton, notify, scratch_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+add_directory_ev2(void *baton,
+ const char *relpath,
+ const apr_array_header_t *children,
+ apr_hash_t *props,
+ svn_revnum_t replaces_rev,
+ apr_pool_t *scratch_pool)
+{
+ struct edit_baton *eb = baton;
+ svn_node_kind_t kind;
+ const char *full_path = svn_dirent_join(eb->root_path, relpath,
+ scratch_pool);
+ svn_string_t *val;
+
+ SVN_ERR(svn_io_check_path(full_path, &kind, scratch_pool));
+ if (kind == svn_node_none)
+ SVN_ERR(svn_io_dir_make(full_path, APR_OS_DEFAULT, scratch_pool));
+ else if (kind == svn_node_file)
+ return svn_error_createf(SVN_ERR_WC_NOT_WORKING_COPY, NULL,
+ _("'%s' exists and is not a directory"),
+ svn_dirent_local_style(full_path, scratch_pool));
+ else if (! (kind == svn_node_dir && eb->force))
+ return svn_error_createf(SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
+ _("'%s' already exists"),
+ svn_dirent_local_style(full_path, scratch_pool));
+
+ if ( (val = svn_hash_gets(props, SVN_PROP_EXTERNALS)) )
+ SVN_ERR(add_externals(eb->externals, full_path, val));
+
+ if (eb->notify_func)
+ {
+ svn_wc_notify_t *notify = svn_wc_create_notify(full_path,
+ svn_wc_notify_update_add,
+ scratch_pool);
+ notify->kind = svn_node_dir;
+ (*eb->notify_func)(eb->notify_baton, notify, scratch_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+target_revision_func(void *baton,
+ svn_revnum_t target_revision,
+ apr_pool_t *scratch_pool)
+{
+ struct edit_baton *eb = baton;
+
+ *eb->target_revision = target_revision;
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+get_editor_ev2(const svn_delta_editor_t **export_editor,
+ void **edit_baton,
+ struct edit_baton *eb,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_editor_t *editor;
+ struct svn_delta__extra_baton *exb = apr_pcalloc(result_pool, sizeof(*exb));
+ svn_boolean_t *found_abs_paths = apr_palloc(result_pool,
+ sizeof(*found_abs_paths));
+
+ exb->baton = eb;
+ exb->target_revision = target_revision_func;
+
+ SVN_ERR(svn_editor_create(&editor, eb, ctx->cancel_func, ctx->cancel_baton,
+ result_pool, scratch_pool));
+ SVN_ERR(svn_editor_setcb_add_directory(editor, add_directory_ev2,
+ scratch_pool));
+ SVN_ERR(svn_editor_setcb_add_file(editor, add_file_ev2, scratch_pool));
+
+ *found_abs_paths = TRUE;
+
+ SVN_ERR(svn_delta__delta_from_editor(export_editor, edit_baton,
+ editor, NULL, NULL, found_abs_paths,
+ NULL, NULL,
+ fetch_props_func, eb,
+ fetch_base_func, eb,
+ exb, result_pool));
+
+ /* Create the root of the export. */
+ SVN_ERR(open_root_internal(eb->root_path, eb->force, eb->notify_func,
+ eb->notify_baton, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+export_file_ev2(const char *from_path_or_url,
+ const char *to_path,
+ struct edit_baton *eb,
+ svn_client__pathrev_t *loc,
+ svn_ra_session_t *ra_session,
+ svn_boolean_t overwrite,
+ apr_pool_t *scratch_pool)
+{
+ svn_boolean_t from_is_url = svn_path_is_url(from_path_or_url);
+ apr_hash_t *props;
+ svn_stream_t *tmp_stream;
+ svn_node_kind_t to_kind;
+
+ if (svn_path_is_empty(to_path))
+ {
+ if (from_is_url)
+ to_path = svn_uri_basename(from_path_or_url, scratch_pool);
+ else
+ to_path = svn_dirent_basename(from_path_or_url, NULL);
+ eb->root_path = to_path;
+ }
+ else
+ {
+ SVN_ERR(append_basename_if_dir(&to_path, from_path_or_url,
+ from_is_url, scratch_pool));
+ eb->root_path = to_path;
+ }
+
+ SVN_ERR(svn_io_check_path(to_path, &to_kind, scratch_pool));
+
+ if ((to_kind == svn_node_file || to_kind == svn_node_unknown) &&
+ ! overwrite)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Destination file '%s' exists, and "
+ "will not be overwritten unless forced"),
+ svn_dirent_local_style(to_path, scratch_pool));
+ else if (to_kind == svn_node_dir)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Destination '%s' exists. Cannot "
+ "overwrite directory with non-directory"),
+ svn_dirent_local_style(to_path, scratch_pool));
+
+ tmp_stream = svn_stream_buffered(scratch_pool);
+
+ SVN_ERR(svn_ra_get_file(ra_session, "", loc->rev,
+ tmp_stream, NULL, &props, scratch_pool));
+
+ /* Since you cannot actually root an editor at a file, we manually drive
+ * a function of our editor. */
+ SVN_ERR(add_file_ev2(eb, "", NULL, tmp_stream, props, SVN_INVALID_REVNUM,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+export_file(const char *from_path_or_url,
+ const char *to_path,
+ struct edit_baton *eb,
+ svn_client__pathrev_t *loc,
+ svn_ra_session_t *ra_session,
+ svn_boolean_t overwrite,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *props;
+ apr_hash_index_t *hi;
+ struct file_baton *fb = apr_pcalloc(scratch_pool, sizeof(*fb));
+ svn_node_kind_t to_kind;
+ svn_boolean_t from_is_url = svn_path_is_url(from_path_or_url);
+
+ if (svn_path_is_empty(to_path))
+ {
+ if (from_is_url)
+ to_path = svn_uri_basename(from_path_or_url, scratch_pool);
+ else
+ to_path = svn_dirent_basename(from_path_or_url, NULL);
+ eb->root_path = to_path;
+ }
+ else
+ {
+ SVN_ERR(append_basename_if_dir(&to_path, from_path_or_url,
+ from_is_url, scratch_pool));
+ eb->root_path = to_path;
+ }
+
+ SVN_ERR(svn_io_check_path(to_path, &to_kind, scratch_pool));
+
+ if ((to_kind == svn_node_file || to_kind == svn_node_unknown) &&
+ ! overwrite)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Destination file '%s' exists, and "
+ "will not be overwritten unless forced"),
+ svn_dirent_local_style(to_path, scratch_pool));
+ else if (to_kind == svn_node_dir)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Destination '%s' exists. Cannot "
+ "overwrite directory with non-directory"),
+ svn_dirent_local_style(to_path, scratch_pool));
+
+ /* Since you cannot actually root an editor at a file, we
+ * manually drive a few functions of our editor. */
+
+ /* This is the equivalent of a parentless add_file(). */
+ fb->edit_baton = eb;
+ fb->path = eb->root_path;
+ fb->url = eb->root_url;
+ fb->pool = scratch_pool;
+ fb->repos_root_url = eb->repos_root_url;
+
+ /* Copied from apply_textdelta(). */
+ SVN_ERR(svn_stream_open_unique(&fb->tmp_stream, &fb->tmppath,
+ svn_dirent_dirname(fb->path, scratch_pool),
+ svn_io_file_del_none,
+ fb->pool, fb->pool));
+
+ /* Step outside the editor-likeness for a moment, to actually talk
+ * to the repository. */
+ /* ### note: the stream will not be closed */
+ SVN_ERR(svn_ra_get_file(ra_session, "", loc->rev,
+ fb->tmp_stream,
+ NULL, &props, scratch_pool));
+
+ /* Push the props into change_file_prop(), to update the file_baton
+ * with information. */
+ for (hi = apr_hash_first(scratch_pool, props); hi; hi = apr_hash_next(hi))
+ {
+ const char *propname = svn__apr_hash_index_key(hi);
+ const svn_string_t *propval = svn__apr_hash_index_val(hi);
+
+ SVN_ERR(change_file_prop(fb, propname, propval, scratch_pool));
+ }
+
+ /* And now just use close_file() to do all the keyword and EOL
+ * work, and put the file into place. */
+ SVN_ERR(close_file(fb, NULL, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+export_directory(const char *from_path_or_url,
+ const char *to_path,
+ struct edit_baton *eb,
+ svn_client__pathrev_t *loc,
+ svn_ra_session_t *ra_session,
+ svn_boolean_t overwrite,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t ignore_keywords,
+ svn_depth_t depth,
+ const char *native_eol,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ void *edit_baton;
+ const svn_delta_editor_t *export_editor;
+ const svn_ra_reporter3_t *reporter;
+ void *report_baton;
+ svn_node_kind_t kind;
+
+ if (!ENABLE_EV2_IMPL)
+ SVN_ERR(get_editor_ev1(&export_editor, &edit_baton, eb, ctx,
+ scratch_pool, scratch_pool));
+ else
+ SVN_ERR(get_editor_ev2(&export_editor, &edit_baton, eb, ctx,
+ scratch_pool, scratch_pool));
+
+ /* Manufacture a basic 'report' to the update reporter. */
+ SVN_ERR(svn_ra_do_update3(ra_session,
+ &reporter, &report_baton,
+ loc->rev,
+ "", /* no sub-target */
+ depth,
+ FALSE, /* don't want copyfrom-args */
+ FALSE, /* don't want ignore_ancestry */
+ export_editor, edit_baton,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(reporter->set_path(report_baton, "", loc->rev,
+ /* Depth is irrelevant, as we're
+ passing start_empty=TRUE anyway. */
+ svn_depth_infinity,
+ TRUE, /* "help, my dir is empty!" */
+ NULL, scratch_pool));
+
+ SVN_ERR(reporter->finish_report(report_baton, scratch_pool));
+
+ /* Special case: Due to our sly export/checkout method of updating an
+ * empty directory, no target will have been created if the exported
+ * item is itself an empty directory (export_editor->open_root never
+ * gets called, because there are no "changes" to make to the empty
+ * dir we reported to the repository).
+ *
+ * So we just create the empty dir manually; but we do it via
+ * open_root_internal(), in order to get proper notification.
+ */
+ SVN_ERR(svn_io_check_path(to_path, &kind, scratch_pool));
+ if (kind == svn_node_none)
+ SVN_ERR(open_root_internal
+ (to_path, overwrite, ctx->notify_func2,
+ ctx->notify_baton2, scratch_pool));
+
+ if (! ignore_externals && depth == svn_depth_infinity)
+ {
+ const char *to_abspath;
+
+ SVN_ERR(svn_dirent_get_absolute(&to_abspath, to_path, scratch_pool));
+ SVN_ERR(svn_client__export_externals(eb->externals,
+ from_path_or_url,
+ to_abspath, eb->repos_root_url,
+ depth, native_eol,
+ ignore_keywords,
+ ctx, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+
+/*** Public Interfaces ***/
+
+svn_error_t *
+svn_client_export5(svn_revnum_t *result_rev,
+ const char *from_path_or_url,
+ const char *to_path,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_boolean_t overwrite,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t ignore_keywords,
+ svn_depth_t depth,
+ const char *native_eol,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_revnum_t edit_revision = SVN_INVALID_REVNUM;
+ svn_boolean_t from_is_url = svn_path_is_url(from_path_or_url);
+
+ SVN_ERR_ASSERT(peg_revision != NULL);
+ SVN_ERR_ASSERT(revision != NULL);
+
+ if (svn_path_is_url(to_path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), to_path);
+
+ peg_revision = svn_cl__rev_default_to_head_or_working(peg_revision,
+ from_path_or_url);
+ revision = svn_cl__rev_default_to_peg(revision, peg_revision);
+
+ if (from_is_url || ! SVN_CLIENT__REVKIND_IS_LOCAL_TO_WC(revision->kind))
+ {
+ svn_client__pathrev_t *loc;
+ svn_ra_session_t *ra_session;
+ svn_node_kind_t kind;
+ struct edit_baton *eb = apr_pcalloc(pool, sizeof(*eb));
+
+ /* Get the RA connection. */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &loc,
+ from_path_or_url, NULL,
+ peg_revision,
+ revision, ctx, pool));
+
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &eb->repos_root_url, pool));
+ eb->root_path = to_path;
+ eb->root_url = loc->url;
+ eb->force = overwrite;
+ eb->target_revision = &edit_revision;
+ eb->externals = apr_hash_make(pool);
+ eb->native_eol = native_eol;
+ eb->ignore_keywords = ignore_keywords;
+ eb->cancel_func = ctx->cancel_func;
+ eb->cancel_baton = ctx->cancel_baton;
+ eb->notify_func = ctx->notify_func2;
+ eb->notify_baton = ctx->notify_baton2;
+
+ SVN_ERR(svn_ra_check_path(ra_session, "", loc->rev, &kind, pool));
+
+ if (kind == svn_node_file)
+ {
+ if (!ENABLE_EV2_IMPL)
+ SVN_ERR(export_file(from_path_or_url, to_path, eb, loc, ra_session,
+ overwrite, pool));
+ else
+ SVN_ERR(export_file_ev2(from_path_or_url, to_path, eb, loc,
+ ra_session, overwrite, pool));
+ }
+ else if (kind == svn_node_dir)
+ {
+ SVN_ERR(export_directory(from_path_or_url, to_path,
+ eb, loc, ra_session, overwrite,
+ ignore_externals, ignore_keywords, depth,
+ native_eol, ctx, pool));
+ }
+ else if (kind == svn_node_none)
+ {
+ return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
+ _("URL '%s' doesn't exist"),
+ from_path_or_url);
+ }
+ /* kind == svn_node_unknown not handled */
+ }
+ else
+ {
+ struct export_info_baton eib;
+ svn_node_kind_t kind;
+ apr_hash_t *externals = NULL;
+
+ /* This is a working copy export. */
+ /* just copy the contents of the working copy into the target path. */
+ SVN_ERR(svn_dirent_get_absolute(&from_path_or_url, from_path_or_url,
+ pool));
+
+ SVN_ERR(svn_dirent_get_absolute(&to_path, to_path, pool));
+
+ SVN_ERR(svn_io_check_path(from_path_or_url, &kind, pool));
+
+ /* ### [JAF] If something already exists on disk at the destination path,
+ * the behaviour depends on the node kinds of the source and destination
+ * and on the FORCE flag. The intention (I guess) is to follow the
+ * semantics of svn_client_export5(), semantics that are not fully
+ * documented but would be something like:
+ *
+ * -----------+---------------------------------------------------------
+ * Src | DIR FILE SPECIAL
+ * Dst (disk) +---------------------------------------------------------
+ * NONE | simple copy simple copy (as src=file?)
+ * DIR | merge if forced [2] inside if root [1] (as src=file?)
+ * FILE | err overwr if forced[3] (as src=file?)
+ * SPECIAL | ??? ??? ???
+ * -----------+---------------------------------------------------------
+ *
+ * [1] FILE onto DIR case: If this file is the root of the copy and thus
+ * the only node to be copied, then copy it as a child of the
+ * directory TO, applying these same rules again except that if this
+ * case occurs again (the child path is already a directory) then
+ * error out. If this file is not the root of the copy (it is
+ * reached by recursion), then error out.
+ *
+ * [2] DIR onto DIR case. If the 'FORCE' flag is true then copy the
+ * source's children inside the target dir, else error out. When
+ * copying the children, apply the same set of rules, except in the
+ * FILE onto DIR case error out like in note [1].
+ *
+ * [3] If the 'FORCE' flag is true then overwrite the destination file
+ * else error out.
+ *
+ * The reality (apparently, looking at the code) is somewhat different.
+ * For a start, to detect the source kind, it looks at what is on disk
+ * rather than the versioned working or base node.
+ */
+ if (kind == svn_node_file)
+ SVN_ERR(append_basename_if_dir(&to_path, from_path_or_url, FALSE,
+ pool));
+
+ eib.to_path = to_path;
+ eib.revision = revision;
+ eib.overwrite = overwrite;
+ eib.ignore_keywords = ignore_keywords;
+ eib.wc_ctx = ctx->wc_ctx;
+ eib.native_eol = native_eol;
+ eib.notify_func = ctx->notify_func2;;
+ eib.notify_baton = ctx->notify_baton2;
+ eib.origin_abspath = from_path_or_url;
+ eib.exported = FALSE;
+
+ SVN_ERR(svn_wc_walk_status(ctx->wc_ctx, from_path_or_url, depth,
+ TRUE /* get_all */,
+ TRUE /* no_ignore */,
+ FALSE /* ignore_text_mods */,
+ NULL,
+ export_node, &eib,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool));
+
+ if (!eib.exported)
+ return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL,
+ _("The node '%s' was not found."),
+ svn_dirent_local_style(from_path_or_url,
+ pool));
+
+ if (!ignore_externals)
+ SVN_ERR(svn_wc__externals_defined_below(&externals, ctx->wc_ctx,
+ from_path_or_url,
+ pool, pool));
+
+ if (externals && apr_hash_count(externals))
+ {
+ apr_pool_t *iterpool = svn_pool_create(pool);
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(pool, externals);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *external_abspath = svn__apr_hash_index_key(hi);
+ const char *relpath;
+ const char *target_abspath;
+
+ svn_pool_clear(iterpool);
+
+ relpath = svn_dirent_skip_ancestor(from_path_or_url,
+ external_abspath);
+
+ target_abspath = svn_dirent_join(to_path, relpath,
+ iterpool);
+
+ /* Ensure that the parent directory exists */
+ SVN_ERR(svn_io_make_dir_recursively(
+ svn_dirent_dirname(target_abspath, iterpool),
+ iterpool));
+
+ SVN_ERR(svn_client_export5(NULL,
+ svn_dirent_join(from_path_or_url,
+ relpath,
+ iterpool),
+ target_abspath,
+ peg_revision, revision,
+ TRUE, ignore_externals,
+ ignore_keywords, depth, native_eol,
+ ctx, iterpool));
+ }
+
+ svn_pool_destroy(iterpool);
+ }
+ }
+
+
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify(to_path,
+ svn_wc_notify_update_completed, pool);
+ notify->revision = edit_revision;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+ }
+
+ if (result_rev)
+ *result_rev = edit_revision;
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/externals.c b/subversion/libsvn_client/externals.c
new file mode 100644
index 0000000..30748ea
--- /dev/null
+++ b/subversion/libsvn_client/externals.c
@@ -0,0 +1,1139 @@
+/*
+ * externals.c: handle the svn:externals property
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <apr_uri.h>
+#include "svn_hash.h"
+#include "svn_wc.h"
+#include "svn_pools.h"
+#include "svn_client.h"
+#include "svn_types.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_props.h"
+#include "svn_config.h"
+#include "client.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+
+
+/* Remove the directory at LOCAL_ABSPATH from revision control, and do the
+ * same to any revision controlled directories underneath LOCAL_ABSPATH
+ * (including directories not referred to by parent svn administrative areas);
+ * then if LOCAL_ABSPATH is empty afterwards, remove it, else rename it to a
+ * unique name in the same parent directory.
+ *
+ * Pass CANCEL_FUNC, CANCEL_BATON to svn_wc_remove_from_revision_control.
+ *
+ * Use SCRATCH_POOL for all temporary allocation.
+ */
+static svn_error_t *
+relegate_dir_external(svn_wc_context_t *wc_ctx,
+ const char *wri_abspath,
+ const char *local_abspath,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ svn_wc_notify_func2_t notify_func,
+ void *notify_baton,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err = SVN_NO_ERROR;
+
+ SVN_ERR(svn_wc__acquire_write_lock(NULL, wc_ctx, local_abspath,
+ FALSE, scratch_pool, scratch_pool));
+
+ err = svn_wc__external_remove(wc_ctx, wri_abspath, local_abspath, FALSE,
+ cancel_func, cancel_baton, scratch_pool);
+ if (err && (err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD))
+ {
+ const char *parent_dir;
+ const char *dirname;
+ const char *new_path;
+
+ svn_error_clear(err);
+ err = SVN_NO_ERROR;
+
+ svn_dirent_split(&parent_dir, &dirname, local_abspath, scratch_pool);
+
+ /* Reserve the new dir name. */
+ SVN_ERR(svn_io_open_uniquely_named(NULL, &new_path,
+ parent_dir, dirname, ".OLD",
+ svn_io_file_del_none,
+ scratch_pool, scratch_pool));
+
+ /* Sigh... We must fall ever so slightly from grace.
+
+ Ideally, there would be no window, however brief, when we
+ don't have a reservation on the new name. Unfortunately,
+ at least in the Unix (Linux?) version of apr_file_rename(),
+ you can't rename a directory over a file, because it's just
+ calling stdio rename(), which says:
+
+ ENOTDIR
+ A component used as a directory in oldpath or newpath
+ path is not, in fact, a directory. Or, oldpath is
+ a directory, and newpath exists but is not a directory
+
+ So instead, we get the name, then remove the file (ugh), then
+ rename the directory, hoping that nobody has gotten that name
+ in the meantime -- which would never happen in real life, so
+ no big deal.
+ */
+ /* Do our best, but no biggy if it fails. The rename will fail. */
+ svn_error_clear(svn_io_remove_file2(new_path, TRUE, scratch_pool));
+
+ /* Rename. If this is still a working copy we should use the working
+ copy rename function (to release open handles) */
+ err = svn_wc__rename_wc(wc_ctx, local_abspath, new_path,
+ scratch_pool);
+
+ if (err && err->apr_err == SVN_ERR_WC_PATH_UNEXPECTED_STATUS)
+ {
+ svn_error_clear(err);
+
+ /* And if it is no longer a working copy, we should just rename
+ it */
+ err = svn_io_file_rename(local_abspath, new_path, scratch_pool);
+ }
+
+ /* ### TODO: We should notify the user about the rename */
+ if (notify_func)
+ {
+ svn_wc_notify_t *notify;
+
+ notify = svn_wc_create_notify(err ? local_abspath : new_path,
+ svn_wc_notify_left_local_modifications,
+ scratch_pool);
+ notify->kind = svn_node_dir;
+ notify->err = err;
+
+ notify_func(notify_baton, notify, scratch_pool);
+ }
+ }
+
+ return svn_error_trace(err);
+}
+
+/* Try to update a directory external at PATH to URL at REVISION.
+ Use POOL for temporary allocations, and use the client context CTX. */
+static svn_error_t *
+switch_dir_external(const char *local_abspath,
+ const char *url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ const char *defining_abspath,
+ svn_boolean_t *timestamp_sleep,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_node_kind_t kind;
+ svn_error_t *err;
+ svn_revnum_t external_peg_rev = SVN_INVALID_REVNUM;
+ svn_revnum_t external_rev = SVN_INVALID_REVNUM;
+ apr_pool_t *subpool = svn_pool_create(pool);
+ const char *repos_root_url;
+ const char *repos_uuid;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ if (peg_revision->kind == svn_opt_revision_number)
+ external_peg_rev = peg_revision->value.number;
+
+ if (revision->kind == svn_opt_revision_number)
+ external_rev = revision->value.number;
+
+ /* If path is a directory, try to update/switch to the correct URL
+ and revision. */
+ SVN_ERR(svn_io_check_path(local_abspath, &kind, pool));
+ if (kind == svn_node_dir)
+ {
+ const char *node_url;
+
+ /* Doubles as an "is versioned" check. */
+ err = svn_wc__node_get_url(&node_url, ctx->wc_ctx, local_abspath,
+ pool, subpool);
+ if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ err = SVN_NO_ERROR;
+ goto relegate;
+ }
+ else if (err)
+ return svn_error_trace(err);
+
+ if (node_url)
+ {
+ /* If we have what appears to be a version controlled
+ subdir, and its top-level URL matches that of our
+ externals definition, perform an update. */
+ if (strcmp(node_url, url) == 0)
+ {
+ SVN_ERR(svn_client__update_internal(NULL, local_abspath,
+ revision, svn_depth_unknown,
+ FALSE, FALSE, FALSE, TRUE,
+ FALSE, TRUE,
+ timestamp_sleep,
+ ctx, subpool));
+ svn_pool_destroy(subpool);
+ goto cleanup;
+ }
+
+ /* We'd really prefer not to have to do a brute-force
+ relegation -- blowing away the current external working
+ copy and checking it out anew -- so we'll first see if we
+ can get away with a generally cheaper relocation (if
+ required) and switch-style update.
+
+ To do so, we need to know the repository root URL of the
+ external working copy as it currently sits. */
+ err = svn_wc__node_get_repos_info(NULL, NULL,
+ &repos_root_url, &repos_uuid,
+ ctx->wc_ctx, local_abspath,
+ pool, subpool);
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
+ && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ repos_root_url = NULL;
+ repos_uuid = NULL;
+ }
+
+ if (repos_root_url)
+ {
+ /* If the new external target URL is not obviously a
+ child of the external working copy's current
+ repository root URL... */
+ if (! svn_uri__is_ancestor(repos_root_url, url))
+ {
+ const char *repos_root;
+
+ /* ... then figure out precisely which repository
+ root URL that target URL *is* a child of ... */
+ SVN_ERR(svn_client_get_repos_root(&repos_root, NULL, url,
+ ctx, subpool, subpool));
+
+ /* ... and use that to try to relocate the external
+ working copy to the target location. */
+ err = svn_client_relocate2(local_abspath, repos_root_url,
+ repos_root, FALSE, ctx, subpool);
+
+ /* If the relocation failed because the new URL
+ points to a totally different repository, we've
+ no choice but to relegate and check out a new WC. */
+ if (err
+ && (err->apr_err == SVN_ERR_WC_INVALID_RELOCATION
+ || (err->apr_err
+ == SVN_ERR_CLIENT_INVALID_RELOCATION)))
+ {
+ svn_error_clear(err);
+ goto relegate;
+ }
+ else if (err)
+ return svn_error_trace(err);
+
+ /* If the relocation went without a hitch, we should
+ have a new repository root URL. */
+ repos_root_url = repos_root;
+ }
+
+ SVN_ERR(svn_client__switch_internal(NULL, local_abspath, url,
+ peg_revision, revision,
+ svn_depth_infinity,
+ TRUE, FALSE, FALSE,
+ TRUE /* ignore_ancestry */,
+ timestamp_sleep,
+ ctx, subpool));
+
+ SVN_ERR(svn_wc__external_register(ctx->wc_ctx,
+ defining_abspath,
+ local_abspath, svn_node_dir,
+ repos_root_url, repos_uuid,
+ svn_uri_skip_ancestor(
+ repos_root_url,
+ url, subpool),
+ external_peg_rev,
+ external_rev,
+ subpool));
+
+ svn_pool_destroy(subpool);
+ goto cleanup;
+ }
+ }
+ }
+
+ relegate:
+
+ /* Fall back on removing the WC and checking out a new one. */
+
+ /* Ensure that we don't have any RA sessions or WC locks from failed
+ operations above. */
+ svn_pool_destroy(subpool);
+
+ if (kind == svn_node_dir)
+ {
+ /* Buh-bye, old and busted ... */
+ SVN_ERR(relegate_dir_external(ctx->wc_ctx, defining_abspath,
+ local_abspath,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ pool));
+ }
+ else
+ {
+ /* The target dir might have multiple components. Guarantee
+ the path leading down to the last component. */
+ const char *parent = svn_dirent_dirname(local_abspath, pool);
+ SVN_ERR(svn_io_make_dir_recursively(parent, pool));
+ }
+
+ /* ... Hello, new hotness. */
+ SVN_ERR(svn_client__checkout_internal(NULL, url, local_abspath, peg_revision,
+ revision, svn_depth_infinity,
+ FALSE, FALSE, timestamp_sleep,
+ ctx, pool));
+
+ SVN_ERR(svn_wc__node_get_repos_info(NULL, NULL,
+ &repos_root_url,
+ &repos_uuid,
+ ctx->wc_ctx, local_abspath,
+ pool, pool));
+
+ SVN_ERR(svn_wc__external_register(ctx->wc_ctx,
+ defining_abspath,
+ local_abspath, svn_node_dir,
+ repos_root_url, repos_uuid,
+ svn_uri_skip_ancestor(repos_root_url,
+ url, pool),
+ external_peg_rev,
+ external_rev,
+ pool));
+
+ cleanup:
+ /* Issues #4123 and #4130: We don't need to keep the newly checked
+ out external's DB open. */
+ SVN_ERR(svn_wc__close_db(local_abspath, ctx->wc_ctx, pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Try to update a file external at LOCAL_ABSPATH to URL at REVISION using a
+ access baton that has a write lock. Use SCRATCH_POOL for temporary
+ allocations, and use the client context CTX. */
+static svn_error_t *
+switch_file_external(const char *local_abspath,
+ const char *url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ const char *def_dir_abspath,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_config_t *cfg = ctx->config
+ ? svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG)
+ : NULL;
+ svn_boolean_t use_commit_times;
+ const char *diff3_cmd;
+ const char *preserved_exts_str;
+ const apr_array_header_t *preserved_exts;
+ svn_node_kind_t kind, external_kind;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ /* See if the user wants last-commit timestamps instead of current ones. */
+ SVN_ERR(svn_config_get_bool(cfg, &use_commit_times,
+ SVN_CONFIG_SECTION_MISCELLANY,
+ SVN_CONFIG_OPTION_USE_COMMIT_TIMES, FALSE));
+
+ /* Get the external diff3, if any. */
+ svn_config_get(cfg, &diff3_cmd, SVN_CONFIG_SECTION_HELPERS,
+ SVN_CONFIG_OPTION_DIFF3_CMD, NULL);
+
+ if (diff3_cmd != NULL)
+ SVN_ERR(svn_path_cstring_to_utf8(&diff3_cmd, diff3_cmd, scratch_pool));
+
+ /* See which files the user wants to preserve the extension of when
+ conflict files are made. */
+ svn_config_get(cfg, &preserved_exts_str, SVN_CONFIG_SECTION_MISCELLANY,
+ SVN_CONFIG_OPTION_PRESERVED_CF_EXTS, "");
+ preserved_exts = *preserved_exts_str
+ ? svn_cstring_split(preserved_exts_str, "\n\r\t\v ", FALSE, scratch_pool)
+ : NULL;
+
+ {
+ const char *wcroot_abspath;
+
+ SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+
+ /* File externals can only be installed inside the current working copy.
+ So verify if the working copy that contains/will contain the target
+ is the defining abspath, or one of its ancestors */
+
+ if (!svn_dirent_is_ancestor(wcroot_abspath, def_dir_abspath))
+ return svn_error_createf(
+ SVN_ERR_WC_BAD_PATH, NULL,
+ _("Cannot insert a file external defined on '%s' "
+ "into the working copy '%s'."),
+ svn_dirent_local_style(def_dir_abspath,
+ scratch_pool),
+ svn_dirent_local_style(wcroot_abspath,
+ scratch_pool));
+ }
+
+ SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, local_abspath,
+ TRUE, FALSE, scratch_pool));
+
+ SVN_ERR(svn_wc__read_external_info(&external_kind, NULL, NULL, NULL, NULL,
+ ctx->wc_ctx, local_abspath, local_abspath,
+ TRUE, scratch_pool, scratch_pool));
+
+ /* If there is a versioned item with this name, ensure it's a file
+ external before working with it. If there is no entry in the
+ working copy, then create an empty file and add it to the working
+ copy. */
+ if (kind != svn_node_none && kind != svn_node_unknown)
+ {
+ if (external_kind != svn_node_file)
+ {
+ return svn_error_createf(
+ SVN_ERR_CLIENT_FILE_EXTERNAL_OVERWRITE_VERSIONED, 0,
+ _("The file external from '%s' cannot overwrite the existing "
+ "versioned item at '%s'"),
+ url, svn_dirent_local_style(local_abspath, scratch_pool));
+ }
+ }
+ else
+ {
+ svn_node_kind_t disk_kind;
+
+ SVN_ERR(svn_io_check_path(local_abspath, &disk_kind, scratch_pool));
+
+ if (kind == svn_node_file || kind == svn_node_dir)
+ return svn_error_createf(SVN_ERR_WC_PATH_FOUND, NULL,
+ _("The file external '%s' can not be "
+ "created because the node exists."),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+ }
+
+ {
+ const svn_ra_reporter3_t *reporter;
+ void *report_baton;
+ const svn_delta_editor_t *switch_editor;
+ void *switch_baton;
+ svn_client__pathrev_t *switch_loc;
+ svn_revnum_t revnum;
+ apr_array_header_t *inherited_props;
+ const char *dir_abspath;
+ const char *target;
+
+ svn_dirent_split(&dir_abspath, &target, local_abspath, scratch_pool);
+
+ /* Open an RA session to 'source' URL */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &switch_loc,
+ url, dir_abspath,
+ peg_revision, revision,
+ ctx, scratch_pool));
+ /* Get the external file's iprops. */
+ SVN_ERR(svn_ra_get_inherited_props(ra_session, &inherited_props, "",
+ switch_loc->rev,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(svn_ra_reparent(ra_session, svn_uri_dirname(url, scratch_pool),
+ scratch_pool));
+
+ SVN_ERR(svn_wc__get_file_external_editor(&switch_editor, &switch_baton,
+ &revnum, ctx->wc_ctx,
+ local_abspath,
+ def_dir_abspath,
+ switch_loc->url,
+ switch_loc->repos_root_url,
+ switch_loc->repos_uuid,
+ inherited_props,
+ use_commit_times,
+ diff3_cmd, preserved_exts,
+ def_dir_abspath,
+ url, peg_revision, revision,
+ ctx->conflict_func2,
+ ctx->conflict_baton2,
+ ctx->cancel_func,
+ ctx->cancel_baton,
+ ctx->notify_func2,
+ ctx->notify_baton2,
+ scratch_pool, scratch_pool));
+
+ /* Tell RA to do an update of URL+TARGET to REVISION; if we pass an
+ invalid revnum, that means RA will use the latest revision. */
+ SVN_ERR(svn_ra_do_switch3(ra_session, &reporter, &report_baton,
+ switch_loc->rev,
+ target, svn_depth_unknown, url,
+ FALSE /* send_copyfrom */,
+ TRUE /* ignore_ancestry */,
+ switch_editor, switch_baton,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(svn_wc__crawl_file_external(ctx->wc_ctx, local_abspath,
+ reporter, report_baton,
+ TRUE, use_commit_times,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ scratch_pool));
+
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify(local_abspath, svn_wc_notify_update_completed,
+ scratch_pool);
+ notify->kind = svn_node_none;
+ notify->content_state = notify->prop_state
+ = svn_wc_notify_state_inapplicable;
+ notify->lock_state = svn_wc_notify_lock_state_inapplicable;
+ notify->revision = revnum;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, scratch_pool);
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Wrappers around svn_wc__external_remove, obtaining and releasing a lock for
+ directory externals */
+static svn_error_t *
+remove_external2(svn_boolean_t *removed,
+ svn_wc_context_t *wc_ctx,
+ const char *wri_abspath,
+ const char *local_abspath,
+ svn_node_kind_t external_kind,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ apr_pool_t *scratch_pool)
+{
+ SVN_ERR(svn_wc__external_remove(wc_ctx, wri_abspath,
+ local_abspath,
+ (external_kind == svn_node_none),
+ cancel_func, cancel_baton,
+ scratch_pool));
+
+ *removed = TRUE;
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+remove_external(svn_boolean_t *removed,
+ svn_wc_context_t *wc_ctx,
+ const char *wri_abspath,
+ const char *local_abspath,
+ svn_node_kind_t external_kind,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ apr_pool_t *scratch_pool)
+{
+ *removed = FALSE;
+ switch (external_kind)
+ {
+ case svn_node_dir:
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ remove_external2(removed,
+ wc_ctx, wri_abspath,
+ local_abspath, external_kind,
+ cancel_func, cancel_baton,
+ scratch_pool),
+ wc_ctx, local_abspath, FALSE, scratch_pool);
+ break;
+ case svn_node_file:
+ default:
+ SVN_ERR(remove_external2(removed,
+ wc_ctx, wri_abspath,
+ local_abspath, external_kind,
+ cancel_func, cancel_baton,
+ scratch_pool));
+ break;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Called when an external that is in the EXTERNALS table is no longer
+ referenced from an svn:externals property */
+static svn_error_t *
+handle_external_item_removal(const svn_client_ctx_t *ctx,
+ const char *defining_abspath,
+ const char *local_abspath,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err;
+ svn_node_kind_t external_kind;
+ svn_node_kind_t kind;
+ svn_boolean_t removed = FALSE;
+
+ /* local_abspath should be a wcroot or a file external */
+ SVN_ERR(svn_wc__read_external_info(&external_kind, NULL, NULL, NULL, NULL,
+ ctx->wc_ctx, defining_abspath,
+ local_abspath, FALSE,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, local_abspath, TRUE, FALSE,
+ scratch_pool));
+
+ if (external_kind != kind)
+ external_kind = svn_node_none; /* Only remove the registration */
+
+ err = remove_external(&removed,
+ ctx->wc_ctx, defining_abspath, local_abspath,
+ external_kind,
+ ctx->cancel_func, ctx->cancel_baton,
+ scratch_pool);
+
+ if (err && err->apr_err == SVN_ERR_WC_NOT_LOCKED && removed)
+ {
+ svn_error_clear(err);
+ err = NULL; /* We removed the working copy, so we can't release the
+ lock that was stored inside */
+ }
+
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify =
+ svn_wc_create_notify(local_abspath,
+ svn_wc_notify_update_external_removed,
+ scratch_pool);
+
+ notify->kind = kind;
+ notify->err = err;
+
+ (ctx->notify_func2)(ctx->notify_baton2, notify, scratch_pool);
+
+ if (err && err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)
+ {
+ notify = svn_wc_create_notify(local_abspath,
+ svn_wc_notify_left_local_modifications,
+ scratch_pool);
+ notify->kind = svn_node_dir;
+ notify->err = err;
+
+ (ctx->notify_func2)(ctx->notify_baton2, notify, scratch_pool);
+ }
+ }
+
+ if (err && err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)
+ {
+ svn_error_clear(err);
+ err = NULL;
+ }
+
+ return svn_error_trace(err);
+}
+
+static svn_error_t *
+handle_external_item_change(svn_client_ctx_t *ctx,
+ const char *repos_root_url,
+ const char *parent_dir_abspath,
+ const char *parent_dir_url,
+ const char *local_abspath,
+ const char *old_defining_abspath,
+ const svn_wc_external_item2_t *new_item,
+ svn_boolean_t *timestamp_sleep,
+ apr_pool_t *scratch_pool)
+{
+ svn_ra_session_t *ra_session;
+ svn_client__pathrev_t *new_loc;
+ const char *new_url;
+ svn_node_kind_t ext_kind;
+
+ SVN_ERR_ASSERT(repos_root_url && parent_dir_url);
+ SVN_ERR_ASSERT(new_item != NULL);
+
+ /* Don't bother to check status, since we'll get that for free by
+ attempting to retrieve the hash values anyway. */
+
+ /* When creating the absolute URL, use the pool and not the
+ iterpool, since the hash table values outlive the iterpool and
+ any pointers they have should also outlive the iterpool. */
+
+ SVN_ERR(svn_wc__resolve_relative_external_url(&new_url,
+ new_item, repos_root_url,
+ parent_dir_url,
+ scratch_pool, scratch_pool));
+
+ /* Determine if the external is a file or directory. */
+ /* Get the RA connection. */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &new_loc,
+ new_url, NULL,
+ &(new_item->peg_revision),
+ &(new_item->revision), ctx,
+ scratch_pool));
+
+ SVN_ERR(svn_ra_check_path(ra_session, "", new_loc->rev, &ext_kind,
+ scratch_pool));
+
+ if (svn_node_none == ext_kind)
+ return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
+ _("URL '%s' at revision %ld doesn't exist"),
+ new_loc->url, new_loc->rev);
+
+ if (svn_node_dir != ext_kind && svn_node_file != ext_kind)
+ return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
+ _("URL '%s' at revision %ld is not a file "
+ "or a directory"),
+ new_loc->url, new_loc->rev);
+
+
+ /* Not protecting against recursive externals. Detecting them in
+ the global case is hard, and it should be pretty obvious to a
+ user when it happens. Worst case: your disk fills up :-). */
+
+ /* First notify that we're about to handle an external. */
+ if (ctx->notify_func2)
+ {
+ (*ctx->notify_func2)(
+ ctx->notify_baton2,
+ svn_wc_create_notify(local_abspath,
+ svn_wc_notify_update_external,
+ scratch_pool),
+ scratch_pool);
+ }
+
+ if (! old_defining_abspath)
+ {
+ /* The target dir might have multiple components. Guarantee the path
+ leading down to the last component. */
+ SVN_ERR(svn_io_make_dir_recursively(svn_dirent_dirname(local_abspath,
+ scratch_pool),
+ scratch_pool));
+ }
+
+ switch (ext_kind)
+ {
+ case svn_node_dir:
+ SVN_ERR(switch_dir_external(local_abspath, new_url,
+ &(new_item->peg_revision),
+ &(new_item->revision),
+ parent_dir_abspath,
+ timestamp_sleep, ctx,
+ scratch_pool));
+ break;
+ case svn_node_file:
+ if (strcmp(repos_root_url, new_loc->repos_root_url))
+ {
+ const char *local_repos_root_url;
+ const char *local_repos_uuid;
+ const char *ext_repos_relpath;
+ svn_error_t *err;
+
+ /*
+ * The working copy library currently requires that all files
+ * in the working copy have the same repository root URL.
+ * The URL from the file external's definition differs from the
+ * one used by the working copy. As a workaround, replace the
+ * root URL portion of the file external's URL, after making
+ * sure both URLs point to the same repository. See issue #4087.
+ */
+
+ err = svn_wc__node_get_repos_info(NULL, NULL,
+ &local_repos_root_url,
+ &local_repos_uuid,
+ ctx->wc_ctx, parent_dir_abspath,
+ scratch_pool, scratch_pool);
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
+ && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ local_repos_root_url = NULL;
+ local_repos_uuid = NULL;
+ }
+
+ ext_repos_relpath = svn_uri_skip_ancestor(new_loc->repos_root_url,
+ new_url, scratch_pool);
+ if (local_repos_uuid == NULL || local_repos_root_url == NULL ||
+ ext_repos_relpath == NULL ||
+ strcmp(local_repos_uuid, new_loc->repos_uuid) != 0)
+ return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Unsupported external: URL of file external '%s' "
+ "is not in repository '%s'"),
+ new_url, repos_root_url);
+
+ new_url = svn_path_url_add_component2(local_repos_root_url,
+ ext_repos_relpath,
+ scratch_pool);
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &new_loc,
+ new_url,
+ NULL,
+ &(new_item->peg_revision),
+ &(new_item->revision),
+ ctx, scratch_pool));
+ }
+
+ SVN_ERR(switch_file_external(local_abspath,
+ new_url,
+ &new_item->peg_revision,
+ &new_item->revision,
+ parent_dir_abspath,
+ ra_session,
+ ctx,
+ scratch_pool));
+ break;
+
+ default:
+ SVN_ERR_MALFUNCTION();
+ break;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+wrap_external_error(const svn_client_ctx_t *ctx,
+ const char *target_abspath,
+ svn_error_t *err,
+ apr_pool_t *scratch_pool)
+{
+ if (err && err->apr_err != SVN_ERR_CANCELLED)
+ {
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notifier = svn_wc_create_notify(
+ target_abspath,
+ svn_wc_notify_failed_external,
+ scratch_pool);
+ notifier->err = err;
+ ctx->notify_func2(ctx->notify_baton2, notifier, scratch_pool);
+ }
+ svn_error_clear(err);
+ return SVN_NO_ERROR;
+ }
+
+ return err;
+}
+
+static svn_error_t *
+handle_externals_change(svn_client_ctx_t *ctx,
+ const char *repos_root_url,
+ svn_boolean_t *timestamp_sleep,
+ const char *local_abspath,
+ const char *new_desc_text,
+ apr_hash_t *old_externals,
+ svn_depth_t ambient_depth,
+ svn_depth_t requested_depth,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *new_desc;
+ int i;
+ apr_pool_t *iterpool;
+ const char *url;
+
+ iterpool = svn_pool_create(scratch_pool);
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ /* Bag out if the depth here is too shallow for externals action. */
+ if ((requested_depth < svn_depth_infinity
+ && requested_depth != svn_depth_unknown)
+ || (ambient_depth < svn_depth_infinity
+ && requested_depth < svn_depth_infinity))
+ return SVN_NO_ERROR;
+
+ if (new_desc_text)
+ SVN_ERR(svn_wc_parse_externals_description3(&new_desc, local_abspath,
+ new_desc_text,
+ FALSE, scratch_pool));
+ else
+ new_desc = NULL;
+
+ SVN_ERR(svn_wc__node_get_url(&url, ctx->wc_ctx, local_abspath,
+ scratch_pool, iterpool));
+
+ SVN_ERR_ASSERT(url);
+
+ for (i = 0; new_desc && (i < new_desc->nelts); i++)
+ {
+ const char *old_defining_abspath;
+ svn_wc_external_item2_t *new_item;
+ const char *target_abspath;
+ svn_boolean_t under_root;
+
+ new_item = APR_ARRAY_IDX(new_desc, i, svn_wc_external_item2_t *);
+
+ svn_pool_clear(iterpool);
+
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ SVN_ERR(svn_dirent_is_under_root(&under_root, &target_abspath,
+ local_abspath, new_item->target_dir,
+ iterpool));
+
+ if (! under_root)
+ {
+ return svn_error_createf(
+ SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
+ _("Path '%s' is not in the working copy"),
+ svn_dirent_local_style(
+ svn_dirent_join(local_abspath, new_item->target_dir,
+ iterpool),
+ iterpool));
+ }
+
+ old_defining_abspath = svn_hash_gets(old_externals, target_abspath);
+
+ SVN_ERR(wrap_external_error(
+ ctx, target_abspath,
+ handle_external_item_change(ctx,
+ repos_root_url,
+ local_abspath, url,
+ target_abspath,
+ old_defining_abspath,
+ new_item,
+ timestamp_sleep,
+ iterpool),
+ iterpool));
+
+ /* And remove already processed items from the to-remove hash */
+ if (old_defining_abspath)
+ svn_hash_sets(old_externals, target_abspath, NULL);
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__handle_externals(apr_hash_t *externals_new,
+ apr_hash_t *ambient_depths,
+ const char *repos_root_url,
+ const char *target_abspath,
+ svn_depth_t requested_depth,
+ svn_boolean_t *timestamp_sleep,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *old_external_defs;
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool;
+
+ SVN_ERR_ASSERT(repos_root_url);
+
+ iterpool = svn_pool_create(scratch_pool);
+
+ SVN_ERR(svn_wc__externals_defined_below(&old_external_defs,
+ ctx->wc_ctx, target_abspath,
+ scratch_pool, iterpool));
+
+ for (hi = apr_hash_first(scratch_pool, externals_new);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *local_abspath = svn__apr_hash_index_key(hi);
+ const char *desc_text = svn__apr_hash_index_val(hi);
+ svn_depth_t ambient_depth = svn_depth_infinity;
+
+ svn_pool_clear(iterpool);
+
+ if (ambient_depths)
+ {
+ const char *ambient_depth_w;
+
+ ambient_depth_w = apr_hash_get(ambient_depths, local_abspath,
+ svn__apr_hash_index_klen(hi));
+
+ if (ambient_depth_w == NULL)
+ {
+ return svn_error_createf(
+ SVN_ERR_WC_CORRUPT, NULL,
+ _("Traversal of '%s' found no ambient depth"),
+ svn_dirent_local_style(local_abspath, scratch_pool));
+ }
+ else
+ {
+ ambient_depth = svn_depth_from_word(ambient_depth_w);
+ }
+ }
+
+ SVN_ERR(handle_externals_change(ctx, repos_root_url, timestamp_sleep,
+ local_abspath,
+ desc_text, old_external_defs,
+ ambient_depth, requested_depth,
+ iterpool));
+ }
+
+ /* Remove the remaining externals */
+ for (hi = apr_hash_first(scratch_pool, old_external_defs);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *item_abspath = svn__apr_hash_index_key(hi);
+ const char *defining_abspath = svn__apr_hash_index_val(hi);
+ const char *parent_abspath;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(wrap_external_error(
+ ctx, item_abspath,
+ handle_external_item_removal(ctx, defining_abspath,
+ item_abspath, iterpool),
+ iterpool));
+
+ /* Are there any unversioned directories between the removed
+ * external and the DEFINING_ABSPATH which we can remove? */
+ parent_abspath = item_abspath;
+ do {
+ svn_node_kind_t kind;
+
+ parent_abspath = svn_dirent_dirname(parent_abspath, iterpool);
+ SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, parent_abspath,
+ TRUE, FALSE, iterpool));
+ if (kind == svn_node_none)
+ {
+ svn_error_t *err;
+
+ err = svn_io_dir_remove_nonrecursive(parent_abspath, iterpool);
+ if (err && APR_STATUS_IS_ENOTEMPTY(err->apr_err))
+ {
+ svn_error_clear(err);
+ break;
+ }
+ else
+ SVN_ERR(err);
+ }
+ } while (strcmp(parent_abspath, defining_abspath) != 0);
+ }
+
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__export_externals(apr_hash_t *externals,
+ const char *from_url,
+ const char *to_abspath,
+ const char *repos_root_url,
+ svn_depth_t requested_depth,
+ const char *native_eol,
+ svn_boolean_t ignore_keywords,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_pool_t *sub_iterpool = svn_pool_create(scratch_pool);
+ apr_hash_index_t *hi;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(to_abspath));
+
+ for (hi = apr_hash_first(scratch_pool, externals);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *local_abspath = svn__apr_hash_index_key(hi);
+ const char *desc_text = svn__apr_hash_index_val(hi);
+ const char *local_relpath;
+ const char *dir_url;
+ apr_array_header_t *items;
+ int i;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_wc_parse_externals_description3(&items, local_abspath,
+ desc_text, FALSE,
+ iterpool));
+
+ if (! items->nelts)
+ continue;
+
+ local_relpath = svn_dirent_skip_ancestor(to_abspath, local_abspath);
+
+ dir_url = svn_path_url_add_component2(from_url, local_relpath,
+ scratch_pool);
+
+ for (i = 0; i < items->nelts; i++)
+ {
+ const char *item_abspath;
+ const char *new_url;
+ svn_boolean_t under_root;
+ svn_wc_external_item2_t *item = APR_ARRAY_IDX(items, i,
+ svn_wc_external_item2_t *);
+
+ svn_pool_clear(sub_iterpool);
+
+ SVN_ERR(svn_dirent_is_under_root(&under_root, &item_abspath,
+ local_abspath, item->target_dir,
+ sub_iterpool));
+
+ if (! under_root)
+ {
+ return svn_error_createf(
+ SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
+ _("Path '%s' is not in the working copy"),
+ svn_dirent_local_style(
+ svn_dirent_join(local_abspath, item->target_dir,
+ sub_iterpool),
+ sub_iterpool));
+ }
+
+ SVN_ERR(svn_wc__resolve_relative_external_url(&new_url, item,
+ repos_root_url,
+ dir_url, sub_iterpool,
+ sub_iterpool));
+
+ /* The target dir might have multiple components. Guarantee
+ the path leading down to the last component. */
+ SVN_ERR(svn_io_make_dir_recursively(svn_dirent_dirname(item_abspath,
+ sub_iterpool),
+ sub_iterpool));
+
+ SVN_ERR(wrap_external_error(
+ ctx, item_abspath,
+ svn_client_export5(NULL, new_url, item_abspath,
+ &item->peg_revision,
+ &item->revision,
+ TRUE, FALSE, ignore_keywords,
+ svn_depth_infinity,
+ native_eol,
+ ctx, sub_iterpool),
+ sub_iterpool));
+ }
+ }
+
+ svn_pool_destroy(sub_iterpool);
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
diff --git a/subversion/libsvn_client/import.c b/subversion/libsvn_client/import.c
new file mode 100644
index 0000000..43e0d79
--- /dev/null
+++ b/subversion/libsvn_client/import.c
@@ -0,0 +1,964 @@
+/*
+ * import.c: wrappers around import functionality.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <string.h>
+#include <apr_strings.h>
+#include <apr_hash.h>
+#include <apr_md5.h>
+
+#include "svn_hash.h"
+#include "svn_ra.h"
+#include "svn_delta.h"
+#include "svn_subst.h"
+#include "svn_client.h"
+#include "svn_string.h"
+#include "svn_pools.h"
+#include "svn_error_codes.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_io.h"
+#include "svn_sorts.h"
+#include "svn_props.h"
+
+#include "client.h"
+#include "private/svn_subr_private.h"
+#include "private/svn_ra_private.h"
+#include "private/svn_magic.h"
+
+#include "svn_private_config.h"
+
+/* Import context baton.
+
+ ### TODO: Add the following items to this baton:
+ /` import editor/baton. `/
+ const svn_delta_editor_t *editor;
+ void *edit_baton;
+
+ /` Client context baton `/
+ svn_client_ctx_t `ctx;
+
+ /` Paths (keys) excluded from the import (values ignored) `/
+ apr_hash_t *excludes;
+*/
+typedef struct import_ctx_t
+{
+ /* Whether any changes were made to the repository */
+ svn_boolean_t repos_changed;
+
+ /* A magic cookie for mime-type detection. */
+ svn_magic__cookie_t *magic_cookie;
+
+ /* Collection of all possible configuration file dictated auto-props and
+ svn:auto-props. A hash mapping const char * file patterns to a
+ second hash which maps const char * property names to const char *
+ property values. Properties which don't have a value, e.g.
+ svn:executable, simply map the property name to an empty string.
+ May be NULL if autoprops are disabled. */
+ apr_hash_t *autoprops;
+} import_ctx_t;
+
+
+/* Apply LOCAL_ABSPATH's contents (as a delta against the empty string) to
+ FILE_BATON in EDITOR. Use POOL for any temporary allocation.
+ PROPERTIES is the set of node properties set on this file.
+
+ Fill DIGEST with the md5 checksum of the sent file; DIGEST must be
+ at least APR_MD5_DIGESTSIZE bytes long. */
+
+/* ### how does this compare against svn_wc_transmit_text_deltas2() ??? */
+
+static svn_error_t *
+send_file_contents(const char *local_abspath,
+ void *file_baton,
+ const svn_delta_editor_t *editor,
+ apr_hash_t *properties,
+ unsigned char *digest,
+ apr_pool_t *pool)
+{
+ svn_stream_t *contents;
+ svn_txdelta_window_handler_t handler;
+ void *handler_baton;
+ const svn_string_t *eol_style_val = NULL, *keywords_val = NULL;
+ svn_boolean_t special = FALSE;
+ svn_subst_eol_style_t eol_style;
+ const char *eol;
+ apr_hash_t *keywords;
+
+ /* If there are properties, look for EOL-style and keywords ones. */
+ if (properties)
+ {
+ eol_style_val = apr_hash_get(properties, SVN_PROP_EOL_STYLE,
+ sizeof(SVN_PROP_EOL_STYLE) - 1);
+ keywords_val = apr_hash_get(properties, SVN_PROP_KEYWORDS,
+ sizeof(SVN_PROP_KEYWORDS) - 1);
+ if (svn_hash_gets(properties, SVN_PROP_SPECIAL))
+ special = TRUE;
+ }
+
+ /* Get an editor func that wants to consume the delta stream. */
+ SVN_ERR(editor->apply_textdelta(file_baton, NULL, pool,
+ &handler, &handler_baton));
+
+ if (eol_style_val)
+ svn_subst_eol_style_from_value(&eol_style, &eol, eol_style_val->data);
+ else
+ {
+ eol = NULL;
+ eol_style = svn_subst_eol_style_none;
+ }
+
+ if (keywords_val)
+ SVN_ERR(svn_subst_build_keywords3(&keywords, keywords_val->data,
+ APR_STRINGIFY(SVN_INVALID_REVNUM),
+ "", "", 0, "", pool));
+ else
+ keywords = NULL;
+
+ if (special)
+ {
+ SVN_ERR(svn_subst_read_specialfile(&contents, local_abspath,
+ pool, pool));
+ }
+ else
+ {
+ /* Open the working copy file. */
+ SVN_ERR(svn_stream_open_readonly(&contents, local_abspath, pool, pool));
+
+ /* If we have EOL styles or keywords, then detranslate the file. */
+ if (svn_subst_translation_required(eol_style, eol, keywords,
+ FALSE, TRUE))
+ {
+ if (eol_style == svn_subst_eol_style_unknown)
+ return svn_error_createf(SVN_ERR_IO_UNKNOWN_EOL, NULL,
+ _("%s property on '%s' contains "
+ "unrecognized EOL-style '%s'"),
+ SVN_PROP_EOL_STYLE,
+ svn_dirent_local_style(local_abspath,
+ pool),
+ eol_style_val->data);
+
+ /* We're importing, so translate files with 'native' eol-style to
+ * repository-normal form, not to this platform's native EOL. */
+ if (eol_style == svn_subst_eol_style_native)
+ eol = SVN_SUBST_NATIVE_EOL_STR;
+
+ /* Wrap the working copy stream with a filter to detranslate it. */
+ contents = svn_subst_stream_translated(contents,
+ eol,
+ TRUE /* repair */,
+ keywords,
+ FALSE /* expand */,
+ pool);
+ }
+ }
+
+ /* Send the file's contents to the delta-window handler. */
+ return svn_error_trace(svn_txdelta_send_stream(contents, handler,
+ handler_baton, digest,
+ pool));
+}
+
+
+/* Import file PATH as EDIT_PATH in the repository directory indicated
+ * by DIR_BATON in EDITOR.
+ *
+ * Accumulate file paths and their batons in FILES, which must be
+ * non-null. (These are used to send postfix textdeltas later).
+ *
+ * If CTX->NOTIFY_FUNC is non-null, invoke it with CTX->NOTIFY_BATON
+ * for each file.
+ *
+ * Use POOL for any temporary allocation.
+ */
+static svn_error_t *
+import_file(const svn_delta_editor_t *editor,
+ void *dir_baton,
+ const char *local_abspath,
+ const char *edit_path,
+ const svn_io_dirent2_t *dirent,
+ import_ctx_t *import_ctx,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ void *file_baton;
+ const char *mimetype = NULL;
+ unsigned char digest[APR_MD5_DIGESTSIZE];
+ const char *text_checksum;
+ apr_hash_t* properties;
+ apr_hash_index_t *hi;
+
+ SVN_ERR(svn_path_check_valid(local_abspath, pool));
+
+ /* Add the file, using the pool from the FILES hash. */
+ SVN_ERR(editor->add_file(edit_path, dir_baton, NULL, SVN_INVALID_REVNUM,
+ pool, &file_baton));
+
+ /* Remember that the repository was modified */
+ import_ctx->repos_changed = TRUE;
+
+ if (! dirent->special)
+ {
+ /* add automatic properties */
+ SVN_ERR(svn_client__get_paths_auto_props(&properties, &mimetype,
+ local_abspath,
+ import_ctx->magic_cookie,
+ import_ctx->autoprops,
+ ctx, pool, pool));
+ }
+ else
+ properties = apr_hash_make(pool);
+
+ if (properties)
+ {
+ for (hi = apr_hash_first(pool, properties); hi; hi = apr_hash_next(hi))
+ {
+ const char *pname = svn__apr_hash_index_key(hi);
+ const svn_string_t *pval = svn__apr_hash_index_val(hi);
+
+ SVN_ERR(editor->change_file_prop(file_baton, pname, pval, pool));
+ }
+ }
+
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify(local_abspath, svn_wc_notify_commit_added,
+ pool);
+ notify->kind = svn_node_file;
+ notify->mime_type = mimetype;
+ notify->content_state = notify->prop_state
+ = svn_wc_notify_state_inapplicable;
+ notify->lock_state = svn_wc_notify_lock_state_inapplicable;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+ }
+
+ /* If this is a special file, we need to set the svn:special
+ property and create a temporary detranslated version in order to
+ send to the server. */
+ if (dirent->special)
+ {
+ svn_hash_sets(properties, SVN_PROP_SPECIAL,
+ svn_string_create(SVN_PROP_BOOLEAN_TRUE, pool));
+ SVN_ERR(editor->change_file_prop(file_baton, SVN_PROP_SPECIAL,
+ svn_hash_gets(properties,
+ SVN_PROP_SPECIAL),
+ pool));
+ }
+
+ /* Now, transmit the file contents. */
+ SVN_ERR(send_file_contents(local_abspath, file_baton, editor,
+ properties, digest, pool));
+
+ /* Finally, close the file. */
+ text_checksum =
+ svn_checksum_to_cstring(svn_checksum__from_digest_md5(digest, pool), pool);
+
+ return editor->close_file(file_baton, text_checksum, pool);
+}
+
+
+/* Return in CHILDREN a mapping of basenames to dirents for the importable
+ * children of DIR_ABSPATH. EXCLUDES is a hash of absolute paths to filter
+ * out. IGNORES and GLOBAL_IGNORES, if non-NULL, are lists of basename
+ * patterns to filter out.
+ * FILTER_CALLBACK and FILTER_BATON will be called for each absolute path,
+ * allowing users to further filter the list of returned entries.
+ *
+ * Results are returned in RESULT_POOL; use SCRATCH_POOL for temporary data.*/
+static svn_error_t *
+get_filtered_children(apr_hash_t **children,
+ const char *dir_abspath,
+ apr_hash_t *excludes,
+ apr_array_header_t *ignores,
+ apr_array_header_t *global_ignores,
+ svn_client_import_filter_func_t filter_callback,
+ void *filter_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *dirents;
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ SVN_ERR(svn_io_get_dirents3(&dirents, dir_abspath, TRUE, result_pool,
+ scratch_pool));
+
+ for (hi = apr_hash_first(scratch_pool, dirents); hi; hi = apr_hash_next(hi))
+ {
+ const char *base_name = svn__apr_hash_index_key(hi);
+ const svn_io_dirent2_t *dirent = svn__apr_hash_index_val(hi);
+ const char *local_abspath;
+
+ svn_pool_clear(iterpool);
+
+ local_abspath = svn_dirent_join(dir_abspath, base_name, iterpool);
+
+ if (svn_wc_is_adm_dir(base_name, iterpool))
+ {
+ /* If someone's trying to import a directory named the same
+ as our administrative directories, that's probably not
+ what they wanted to do. If they are importing a file
+ with that name, something is bound to blow up when they
+ checkout what they've imported. So, just skip items with
+ that name. */
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify(svn_dirent_join(local_abspath, base_name,
+ iterpool),
+ svn_wc_notify_skip, iterpool);
+ notify->kind = svn_node_dir;
+ notify->content_state = notify->prop_state
+ = svn_wc_notify_state_inapplicable;
+ notify->lock_state = svn_wc_notify_lock_state_inapplicable;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, iterpool);
+ }
+
+ svn_hash_sets(dirents, base_name, NULL);
+ continue;
+ }
+ /* If this is an excluded path, exclude it. */
+ if (svn_hash_gets(excludes, local_abspath))
+ {
+ svn_hash_sets(dirents, base_name, NULL);
+ continue;
+ }
+
+ if (ignores && svn_wc_match_ignore_list(base_name, ignores, iterpool))
+ {
+ svn_hash_sets(dirents, base_name, NULL);
+ continue;
+ }
+
+ if (global_ignores &&
+ svn_wc_match_ignore_list(base_name, global_ignores, iterpool))
+ {
+ svn_hash_sets(dirents, base_name, NULL);
+ continue;
+ }
+
+ if (filter_callback)
+ {
+ svn_boolean_t filter = FALSE;
+
+ SVN_ERR(filter_callback(filter_baton, &filter, local_abspath,
+ dirent, iterpool));
+
+ if (filter)
+ {
+ svn_hash_sets(dirents, base_name, NULL);
+ continue;
+ }
+ }
+ }
+ svn_pool_destroy(iterpool);
+
+ *children = dirents;
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+import_dir(const svn_delta_editor_t *editor,
+ void *dir_baton,
+ const char *local_abspath,
+ const char *edit_path,
+ svn_depth_t depth,
+ apr_hash_t *excludes,
+ apr_array_header_t *global_ignores,
+ svn_boolean_t no_ignore,
+ svn_boolean_t no_autoprops,
+ svn_boolean_t ignore_unknown_node_types,
+ svn_client_import_filter_func_t filter_callback,
+ void *filter_baton,
+ import_ctx_t *import_ctx,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+
+/* Import the children of DIR_ABSPATH, with other arguments similar to
+ * import_dir(). */
+static svn_error_t *
+import_children(const char *dir_abspath,
+ const char *edit_path,
+ apr_hash_t *dirents,
+ const svn_delta_editor_t *editor,
+ void *dir_baton,
+ svn_depth_t depth,
+ apr_hash_t *excludes,
+ apr_array_header_t *global_ignores,
+ svn_boolean_t no_ignore,
+ svn_boolean_t no_autoprops,
+ svn_boolean_t ignore_unknown_node_types,
+ svn_client_import_filter_func_t filter_callback,
+ void *filter_baton,
+ import_ctx_t *import_ctx,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *sorted_dirents;
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ sorted_dirents = svn_sort__hash(dirents, svn_sort_compare_items_lexically,
+ scratch_pool);
+ for (i = 0; i < sorted_dirents->nelts; i++)
+ {
+ const char *this_abspath, *this_edit_path;
+ svn_sort__item_t item = APR_ARRAY_IDX(sorted_dirents, i,
+ svn_sort__item_t);
+ const char *filename = item.key;
+ const svn_io_dirent2_t *dirent = item.value;
+
+ svn_pool_clear(iterpool);
+
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ /* Typically, we started importing from ".", in which case
+ edit_path is "". So below, this_path might become "./blah",
+ and this_edit_path might become "blah", for example. */
+ this_abspath = svn_dirent_join(dir_abspath, filename, iterpool);
+ this_edit_path = svn_relpath_join(edit_path, filename, iterpool);
+
+ if (dirent->kind == svn_node_dir && depth >= svn_depth_immediates)
+ {
+ /* Recurse. */
+ svn_depth_t depth_below_here = depth;
+ if (depth == svn_depth_immediates)
+ depth_below_here = svn_depth_empty;
+
+ SVN_ERR(import_dir(editor, dir_baton, this_abspath,
+ this_edit_path, depth_below_here, excludes,
+ global_ignores, no_ignore, no_autoprops,
+ ignore_unknown_node_types, filter_callback,
+ filter_baton, import_ctx, ctx, iterpool));
+ }
+ else if (dirent->kind == svn_node_file && depth >= svn_depth_files)
+ {
+ SVN_ERR(import_file(editor, dir_baton, this_abspath,
+ this_edit_path, dirent,
+ import_ctx, ctx, iterpool));
+ }
+ else if (dirent->kind != svn_node_dir && dirent->kind != svn_node_file)
+ {
+ if (ignore_unknown_node_types)
+ {
+ /*## warn about it*/
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify(this_abspath,
+ svn_wc_notify_skip, iterpool);
+ notify->kind = svn_node_dir;
+ notify->content_state = notify->prop_state
+ = svn_wc_notify_state_inapplicable;
+ notify->lock_state = svn_wc_notify_lock_state_inapplicable;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, iterpool);
+ }
+ }
+ else
+ return svn_error_createf
+ (SVN_ERR_NODE_UNKNOWN_KIND, NULL,
+ _("Unknown or unversionable type for '%s'"),
+ svn_dirent_local_style(this_abspath, iterpool));
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+
+/* Import directory LOCAL_ABSPATH into the repository directory indicated by
+ * DIR_BATON in EDITOR. EDIT_PATH is the path imported as the root
+ * directory, so all edits are relative to that.
+ *
+ * DEPTH is the depth at this point in the descent (it may be changed
+ * for recursive calls).
+ *
+ * Accumulate file paths and their batons in FILES, which must be
+ * non-null. (These are used to send postfix textdeltas later).
+ *
+ * EXCLUDES is a hash whose keys are absolute paths to exclude from
+ * the import (values are unused).
+ *
+ * GLOBAL_IGNORES is an array of const char * ignore patterns. Any child
+ * of LOCAL_ABSPATH which matches one or more of the patterns is not imported.
+ *
+ * If NO_IGNORE is FALSE, don't import files or directories that match
+ * ignore patterns.
+ *
+ * If FILTER_CALLBACK is not NULL, call it with FILTER_BATON on each to be
+ * imported node below LOCAL_ABSPATH to allow filtering nodes.
+ *
+ * If CTX->NOTIFY_FUNC is non-null, invoke it with CTX->NOTIFY_BATON for each
+ * directory.
+ *
+ * Use POOL for any temporary allocation. */
+static svn_error_t *
+import_dir(const svn_delta_editor_t *editor,
+ void *dir_baton,
+ const char *local_abspath,
+ const char *edit_path,
+ svn_depth_t depth,
+ apr_hash_t *excludes,
+ apr_array_header_t *global_ignores,
+ svn_boolean_t no_ignore,
+ svn_boolean_t no_autoprops,
+ svn_boolean_t ignore_unknown_node_types,
+ svn_client_import_filter_func_t filter_callback,
+ void *filter_baton,
+ import_ctx_t *import_ctx,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_hash_t *dirents;
+ void *this_dir_baton;
+
+ SVN_ERR(svn_path_check_valid(local_abspath, pool));
+ SVN_ERR(get_filtered_children(&dirents, local_abspath, excludes, NULL,
+ global_ignores, filter_callback,
+ filter_baton, ctx, pool, pool));
+
+ /* Import this directory, but not yet its children. */
+ {
+ /* Add the new subdirectory, getting a descent baton from the editor. */
+ SVN_ERR(editor->add_directory(edit_path, dir_baton, NULL,
+ SVN_INVALID_REVNUM, pool, &this_dir_baton));
+
+ /* Remember that the repository was modified */
+ import_ctx->repos_changed = TRUE;
+
+ /* By notifying before the recursive call below, we display
+ a directory add before displaying adds underneath the
+ directory. To do it the other way around, just move this
+ after the recursive call. */
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify(local_abspath, svn_wc_notify_commit_added,
+ pool);
+ notify->kind = svn_node_dir;
+ notify->content_state = notify->prop_state
+ = svn_wc_notify_state_inapplicable;
+ notify->lock_state = svn_wc_notify_lock_state_inapplicable;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+ }
+ }
+
+ /* Now import the children recursively. */
+ SVN_ERR(import_children(local_abspath, edit_path, dirents, editor,
+ this_dir_baton, depth, excludes, global_ignores,
+ no_ignore, no_autoprops, ignore_unknown_node_types,
+ filter_callback, filter_baton,
+ import_ctx, ctx, pool));
+
+ /* Finally, close the sub-directory. */
+ SVN_ERR(editor->close_directory(this_dir_baton, pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Recursively import PATH to a repository using EDITOR and
+ * EDIT_BATON. PATH can be a file or directory.
+ *
+ * DEPTH is the depth at which to import PATH; it behaves as for
+ * svn_client_import4().
+ *
+ * NEW_ENTRIES is an ordered array of path components that must be
+ * created in the repository (where the ordering direction is
+ * parent-to-child). If PATH is a directory, NEW_ENTRIES may be empty
+ * -- the result is an import which creates as many new entries in the
+ * top repository target directory as there are importable entries in
+ * the top of PATH; but if NEW_ENTRIES is not empty, its last item is
+ * the name of a new subdirectory in the repository to hold the
+ * import. If PATH is a file, NEW_ENTRIES may not be empty, and its
+ * last item is the name used for the file in the repository. If
+ * NEW_ENTRIES contains more than one item, all but the last item are
+ * the names of intermediate directories that are created before the
+ * real import begins. NEW_ENTRIES may NOT be NULL.
+ *
+ * EXCLUDES is a hash whose keys are absolute paths to exclude from
+ * the import (values are unused).
+ *
+ * AUTOPROPS is hash of all config file autoprops and
+ * svn:auto-props inherited by the import target, see the
+ * IMPORT_CTX member of the same name.
+ *
+ * LOCAL_IGNORES is an array of const char * ignore patterns which
+ * correspond to the svn:ignore property (if any) set on the root of the
+ * repository target and thus dictates which immediate children of that
+ * target should be ignored and not imported.
+ *
+ * GLOBAL_IGNORES is an array of const char * ignore patterns which
+ * correspond to the svn:global-ignores properties (if any) set on
+ * the root of the repository target or inherited by it.
+ *
+ * If NO_IGNORE is FALSE, don't import files or directories that match
+ * ignore patterns.
+ *
+ * If CTX->NOTIFY_FUNC is non-null, invoke it with CTX->NOTIFY_BATON for
+ * each imported path, passing actions svn_wc_notify_commit_added.
+ *
+ * Use POOL for any temporary allocation.
+ *
+ * Note: the repository directory receiving the import was specified
+ * when the editor was fetched. (I.e, when EDITOR->open_root() is
+ * called, it returns a directory baton for that directory, which is
+ * not necessarily the root.)
+ */
+static svn_error_t *
+import(const char *local_abspath,
+ const apr_array_header_t *new_entries,
+ const svn_delta_editor_t *editor,
+ void *edit_baton,
+ svn_depth_t depth,
+ apr_hash_t *excludes,
+ apr_hash_t *autoprops,
+ apr_array_header_t *local_ignores,
+ apr_array_header_t *global_ignores,
+ svn_boolean_t no_ignore,
+ svn_boolean_t no_autoprops,
+ svn_boolean_t ignore_unknown_node_types,
+ svn_client_import_filter_func_t filter_callback,
+ void *filter_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ void *root_baton;
+ apr_array_header_t *batons = NULL;
+ const char *edit_path = "";
+ import_ctx_t *import_ctx = apr_pcalloc(pool, sizeof(*import_ctx));
+ const svn_io_dirent2_t *dirent;
+
+ import_ctx->autoprops = autoprops;
+ svn_magic__init(&import_ctx->magic_cookie, pool);
+
+ /* Get a root dir baton. We pass an invalid revnum to open_root
+ to mean "base this on the youngest revision". Should we have an
+ SVN_YOUNGEST_REVNUM defined for these purposes? */
+ SVN_ERR(editor->open_root(edit_baton, SVN_INVALID_REVNUM,
+ pool, &root_baton));
+
+ /* Import a file or a directory tree. */
+ SVN_ERR(svn_io_stat_dirent2(&dirent, local_abspath, FALSE, FALSE,
+ pool, pool));
+
+ /* Make the intermediate directory components necessary for properly
+ rooting our import source tree. */
+ if (new_entries->nelts)
+ {
+ int i;
+
+ batons = apr_array_make(pool, new_entries->nelts, sizeof(void *));
+ for (i = 0; i < new_entries->nelts; i++)
+ {
+ const char *component = APR_ARRAY_IDX(new_entries, i, const char *);
+ edit_path = svn_relpath_join(edit_path, component, pool);
+
+ /* If this is the last path component, and we're importing a
+ file, then this component is the name of the file, not an
+ intermediate directory. */
+ if ((i == new_entries->nelts - 1) && (dirent->kind == svn_node_file))
+ break;
+
+ APR_ARRAY_PUSH(batons, void *) = root_baton;
+ SVN_ERR(editor->add_directory(edit_path,
+ root_baton,
+ NULL, SVN_INVALID_REVNUM,
+ pool, &root_baton));
+
+ /* Remember that the repository was modified */
+ import_ctx->repos_changed = TRUE;
+ }
+ }
+ else if (dirent->kind == svn_node_file)
+ {
+ return svn_error_create
+ (SVN_ERR_NODE_UNKNOWN_KIND, NULL,
+ _("New entry name required when importing a file"));
+ }
+
+ /* Note that there is no need to check whether PATH's basename is
+ the same name that we reserve for our administrative
+ subdirectories. It would be strange -- though not illegal -- to
+ import the contents of a directory of that name, because the
+ directory's own name is not part of those contents. Of course,
+ if something underneath it also has our reserved name, then we'll
+ error. */
+
+ if (dirent->kind == svn_node_file)
+ {
+ /* This code path ignores EXCLUDES and FILTER, but they don't make
+ much sense for a single file import anyway. */
+ svn_boolean_t ignores_match = FALSE;
+
+ if (!no_ignore)
+ ignores_match =
+ (svn_wc_match_ignore_list(local_abspath, global_ignores, pool)
+ || svn_wc_match_ignore_list(local_abspath, local_ignores, pool));
+
+ if (!ignores_match)
+ SVN_ERR(import_file(editor, root_baton, local_abspath, edit_path,
+ dirent, import_ctx, ctx, pool));
+ }
+ else if (dirent->kind == svn_node_dir)
+ {
+ apr_hash_t *dirents;
+
+ /* If we are creating a new repository directory path to import to,
+ then we disregard any svn:ignore property. */
+ if (!no_ignore && new_entries->nelts)
+ local_ignores = NULL;
+
+ SVN_ERR(get_filtered_children(&dirents, local_abspath, excludes,
+ local_ignores, global_ignores,
+ filter_callback, filter_baton, ctx,
+ pool, pool));
+
+ SVN_ERR(import_children(local_abspath, edit_path, dirents, editor,
+ root_baton, depth, excludes, global_ignores,
+ no_ignore, no_autoprops,
+ ignore_unknown_node_types, filter_callback,
+ filter_baton, import_ctx, ctx, pool));
+
+ }
+ else if (dirent->kind == svn_node_none
+ || dirent->kind == svn_node_unknown)
+ {
+ return svn_error_createf(SVN_ERR_NODE_UNKNOWN_KIND, NULL,
+ _("'%s' does not exist"),
+ svn_dirent_local_style(local_abspath, pool));
+ }
+
+ /* Close up shop; it's time to go home. */
+ SVN_ERR(editor->close_directory(root_baton, pool));
+ if (batons && batons->nelts)
+ {
+ void **baton;
+ while ((baton = (void **) apr_array_pop(batons)))
+ {
+ SVN_ERR(editor->close_directory(*baton, pool));
+ }
+ }
+
+ if (import_ctx->repos_changed)
+ return editor->close_edit(edit_baton, pool);
+ else
+ return editor->abort_edit(edit_baton, pool);
+}
+
+
+/*** Public Interfaces. ***/
+
+svn_error_t *
+svn_client_import5(const char *path,
+ const char *url,
+ svn_depth_t depth,
+ svn_boolean_t no_ignore,
+ svn_boolean_t no_autoprops,
+ svn_boolean_t ignore_unknown_node_types,
+ const apr_hash_t *revprop_table,
+ svn_client_import_filter_func_t filter_callback,
+ void *filter_baton,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err = SVN_NO_ERROR;
+ const char *log_msg = "";
+ const svn_delta_editor_t *editor;
+ void *edit_baton;
+ svn_ra_session_t *ra_session;
+ apr_hash_t *excludes = apr_hash_make(scratch_pool);
+ svn_node_kind_t kind;
+ const char *local_abspath;
+ apr_array_header_t *new_entries = apr_array_make(scratch_pool, 4,
+ sizeof(const char *));
+ apr_hash_t *commit_revprops;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_hash_t *autoprops = NULL;
+ apr_array_header_t *global_ignores;
+ apr_array_header_t *local_ignores_arr;
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, scratch_pool));
+
+ /* Create a new commit item and add it to the array. */
+ if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx))
+ {
+ /* If there's a log message gatherer, create a temporary commit
+ item array solely to help generate the log message. The
+ array is not used for the import itself. */
+ svn_client_commit_item3_t *item;
+ const char *tmp_file;
+ apr_array_header_t *commit_items
+ = apr_array_make(scratch_pool, 1, sizeof(item));
+
+ item = svn_client_commit_item3_create(scratch_pool);
+ item->path = apr_pstrdup(scratch_pool, path);
+ item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD;
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
+
+ SVN_ERR(svn_client__get_log_msg(&log_msg, &tmp_file, commit_items,
+ ctx, scratch_pool));
+ if (! log_msg)
+ return SVN_NO_ERROR;
+ if (tmp_file)
+ {
+ const char *abs_path;
+ SVN_ERR(svn_dirent_get_absolute(&abs_path, tmp_file, scratch_pool));
+ svn_hash_sets(excludes, abs_path, (void *)1);
+ }
+ }
+
+ SVN_ERR(svn_io_check_path(local_abspath, &kind, scratch_pool));
+
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, url, NULL,
+ ctx, scratch_pool, iterpool));
+
+ /* Figure out all the path components we need to create just to have
+ a place to stick our imported tree. */
+ SVN_ERR(svn_ra_check_path(ra_session, "", SVN_INVALID_REVNUM, &kind,
+ iterpool));
+
+ /* We can import into directories, but if a file already exists, that's
+ an error. */
+ if (kind == svn_node_file)
+ return svn_error_createf
+ (SVN_ERR_ENTRY_EXISTS, NULL,
+ _("Path '%s' already exists"), url);
+
+ while (kind == svn_node_none)
+ {
+ const char *dir;
+
+ svn_pool_clear(iterpool);
+
+ svn_uri_split(&url, &dir, url, scratch_pool);
+ APR_ARRAY_PUSH(new_entries, const char *) = dir;
+ SVN_ERR(svn_ra_reparent(ra_session, url, iterpool));
+
+ SVN_ERR(svn_ra_check_path(ra_session, "", SVN_INVALID_REVNUM, &kind,
+ iterpool));
+ }
+
+ /* Reverse the order of the components we added to our NEW_ENTRIES array. */
+ svn_sort__array_reverse(new_entries, scratch_pool);
+
+ /* The repository doesn't know about the reserved administrative
+ directory. */
+ if (new_entries->nelts)
+ {
+ const char *last_component
+ = APR_ARRAY_IDX(new_entries, new_entries->nelts - 1, const char *);
+
+ if (svn_wc_is_adm_dir(last_component, scratch_pool))
+ return svn_error_createf
+ (SVN_ERR_CL_ADM_DIR_RESERVED, NULL,
+ _("'%s' is a reserved name and cannot be imported"),
+ svn_dirent_local_style(last_component, scratch_pool));
+ }
+
+ SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table,
+ log_msg, ctx, scratch_pool));
+
+ /* Fetch RA commit editor. */
+ SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session,
+ svn_client__get_shim_callbacks(ctx->wc_ctx,
+ NULL, scratch_pool)));
+ SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton,
+ commit_revprops, commit_callback,
+ commit_baton, NULL, TRUE,
+ scratch_pool));
+
+ /* Get inherited svn:auto-props, svn:global-ignores, and
+ svn:ignores for the location we are importing to. */
+ if (!no_autoprops)
+ SVN_ERR(svn_client__get_all_auto_props(&autoprops, url, ctx,
+ scratch_pool, iterpool));
+ if (no_ignore)
+ {
+ global_ignores = NULL;
+ local_ignores_arr = NULL;
+ }
+ else
+ {
+ svn_opt_revision_t rev;
+ apr_array_header_t *config_ignores;
+ apr_hash_t *local_ignores_hash;
+
+ SVN_ERR(svn_client__get_inherited_ignores(&global_ignores, url, ctx,
+ scratch_pool, iterpool));
+ SVN_ERR(svn_wc_get_default_ignores(&config_ignores, ctx->config,
+ scratch_pool));
+ global_ignores = apr_array_append(scratch_pool, global_ignores,
+ config_ignores);
+
+ rev.kind = svn_opt_revision_head;
+ SVN_ERR(svn_client_propget5(&local_ignores_hash, NULL, SVN_PROP_IGNORE, url,
+ &rev, &rev, NULL, svn_depth_empty, NULL, ctx,
+ scratch_pool, scratch_pool));
+ local_ignores_arr = apr_array_make(scratch_pool, 1, sizeof(const char *));
+
+ if (apr_hash_count(local_ignores_hash))
+ {
+ svn_string_t *propval = svn_hash_gets(local_ignores_hash, url);
+ if (propval)
+ {
+ svn_cstring_split_append(local_ignores_arr, propval->data,
+ "\n\r\t\v ", FALSE, scratch_pool);
+ }
+ }
+ }
+
+ /* If an error occurred during the commit, abort the edit and return
+ the error. We don't even care if the abort itself fails. */
+ if ((err = import(local_abspath, new_entries, editor, edit_baton,
+ depth, excludes, autoprops, local_ignores_arr,
+ global_ignores, no_ignore, no_autoprops,
+ ignore_unknown_node_types, filter_callback,
+ filter_baton, ctx, iterpool)))
+ {
+ return svn_error_compose_create(
+ err,
+ editor->abort_edit(edit_baton, iterpool));
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
diff --git a/subversion/libsvn_client/info.c b/subversion/libsvn_client/info.c
new file mode 100644
index 0000000..f49f22e
--- /dev/null
+++ b/subversion/libsvn_client/info.c
@@ -0,0 +1,402 @@
+/*
+ * info.c: return system-generated metadata about paths or URLs.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+#include "client.h"
+#include "svn_client.h"
+#include "svn_pools.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_hash.h"
+#include "svn_wc.h"
+
+#include "svn_private_config.h"
+#include "private/svn_fspath.h"
+#include "private/svn_wc_private.h"
+
+
+svn_client_info2_t *
+svn_client_info2_dup(const svn_client_info2_t *info,
+ apr_pool_t *pool)
+{
+ svn_client_info2_t *new_info = apr_pmemdup(pool, info, sizeof(*new_info));
+
+ if (new_info->URL)
+ new_info->URL = apr_pstrdup(pool, info->URL);
+ if (new_info->repos_root_URL)
+ new_info->repos_root_URL = apr_pstrdup(pool, info->repos_root_URL);
+ if (new_info->repos_UUID)
+ new_info->repos_UUID = apr_pstrdup(pool, info->repos_UUID);
+ if (info->last_changed_author)
+ new_info->last_changed_author = apr_pstrdup(pool, info->last_changed_author);
+ if (new_info->lock)
+ new_info->lock = svn_lock_dup(info->lock, pool);
+ if (new_info->wc_info)
+ new_info->wc_info = svn_wc_info_dup(info->wc_info, pool);
+ return new_info;
+}
+
+/* Set *INFO to a new info struct built from DIRENT
+ and (possibly NULL) svn_lock_t LOCK, all allocated in POOL.
+ Pointer fields are copied by reference, not dup'd. */
+static svn_error_t *
+build_info_from_dirent(svn_client_info2_t **info,
+ const svn_dirent_t *dirent,
+ svn_lock_t *lock,
+ const svn_client__pathrev_t *pathrev,
+ apr_pool_t *pool)
+{
+ svn_client_info2_t *tmpinfo = apr_pcalloc(pool, sizeof(*tmpinfo));
+
+ tmpinfo->URL = pathrev->url;
+ tmpinfo->rev = pathrev->rev;
+ tmpinfo->kind = dirent->kind;
+ tmpinfo->repos_UUID = pathrev->repos_uuid;
+ tmpinfo->repos_root_URL = pathrev->repos_root_url;
+ tmpinfo->last_changed_rev = dirent->created_rev;
+ tmpinfo->last_changed_date = dirent->time;
+ tmpinfo->last_changed_author = dirent->last_author;
+ tmpinfo->lock = lock;
+ tmpinfo->size = dirent->size;
+
+ tmpinfo->wc_info = NULL;
+
+ *info = tmpinfo;
+ return SVN_NO_ERROR;
+}
+
+
+/* The dirent fields we care about for our calls to svn_ra_get_dir2. */
+#define DIRENT_FIELDS (SVN_DIRENT_KIND | \
+ SVN_DIRENT_CREATED_REV | \
+ SVN_DIRENT_TIME | \
+ SVN_DIRENT_LAST_AUTHOR)
+
+
+/* Helper func for recursively fetching svn_dirent_t's from a remote
+ directory and pushing them at an info-receiver callback.
+
+ DEPTH is the depth starting at DIR, even though RECEIVER is never
+ invoked on DIR: if DEPTH is svn_depth_immediates, then invoke
+ RECEIVER on all children of DIR, but none of their children; if
+ svn_depth_files, then invoke RECEIVER on file children of DIR but
+ not on subdirectories; if svn_depth_infinity, recurse fully.
+ DIR is a relpath, relative to the root of RA_SESSION.
+*/
+static svn_error_t *
+push_dir_info(svn_ra_session_t *ra_session,
+ const svn_client__pathrev_t *pathrev,
+ const char *dir,
+ svn_client_info_receiver2_t receiver,
+ void *receiver_baton,
+ svn_depth_t depth,
+ svn_client_ctx_t *ctx,
+ apr_hash_t *locks,
+ apr_pool_t *pool)
+{
+ apr_hash_t *tmpdirents;
+ apr_hash_index_t *hi;
+ apr_pool_t *subpool = svn_pool_create(pool);
+
+ SVN_ERR(svn_ra_get_dir2(ra_session, &tmpdirents, NULL, NULL,
+ dir, pathrev->rev, DIRENT_FIELDS, pool));
+
+ for (hi = apr_hash_first(pool, tmpdirents); hi; hi = apr_hash_next(hi))
+ {
+ const char *path, *fs_path;
+ svn_lock_t *lock;
+ svn_client_info2_t *info;
+ const char *name = svn__apr_hash_index_key(hi);
+ svn_dirent_t *the_ent = svn__apr_hash_index_val(hi);
+ svn_client__pathrev_t *child_pathrev;
+
+ svn_pool_clear(subpool);
+
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ path = svn_relpath_join(dir, name, subpool);
+ child_pathrev = svn_client__pathrev_join_relpath(pathrev, name, subpool);
+ fs_path = svn_client__pathrev_fspath(child_pathrev, subpool);
+
+ lock = svn_hash_gets(locks, fs_path);
+
+ SVN_ERR(build_info_from_dirent(&info, the_ent, lock, child_pathrev,
+ subpool));
+
+ if (depth >= svn_depth_immediates
+ || (depth == svn_depth_files && the_ent->kind == svn_node_file))
+ {
+ SVN_ERR(receiver(receiver_baton, path, info, subpool));
+ }
+
+ if (depth == svn_depth_infinity && the_ent->kind == svn_node_dir)
+ {
+ SVN_ERR(push_dir_info(ra_session, child_pathrev, path,
+ receiver, receiver_baton,
+ depth, ctx, locks, subpool));
+ }
+ }
+
+ svn_pool_destroy(subpool);
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Set *SAME_P to TRUE if URL exists in the head of the repository and
+ refers to the same resource as it does in REV, using POOL for
+ temporary allocations. RA_SESSION is an open RA session for URL. */
+static svn_error_t *
+same_resource_in_head(svn_boolean_t *same_p,
+ const char *url,
+ svn_revnum_t rev,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+ svn_opt_revision_t start_rev, peg_rev;
+ const char *head_url;
+
+ start_rev.kind = svn_opt_revision_head;
+ peg_rev.kind = svn_opt_revision_number;
+ peg_rev.value.number = rev;
+
+ err = svn_client__repos_locations(&head_url, NULL, NULL, NULL,
+ ra_session,
+ url, &peg_rev,
+ &start_rev, NULL,
+ ctx, pool);
+ if (err &&
+ ((err->apr_err == SVN_ERR_CLIENT_UNRELATED_RESOURCES) ||
+ (err->apr_err == SVN_ERR_FS_NOT_FOUND)))
+ {
+ svn_error_clear(err);
+ *same_p = FALSE;
+ return SVN_NO_ERROR;
+ }
+ else
+ SVN_ERR(err);
+
+ /* ### Currently, the URLs should always be equal, since we can't
+ ### walk forwards in history. */
+ *same_p = (strcmp(url, head_url) == 0);
+
+ return SVN_NO_ERROR;
+}
+
+/* A baton for wc_info_receiver(), containing the wrapped receiver. */
+typedef struct wc_info_receiver_baton_t
+{
+ svn_client_info_receiver2_t client_receiver_func;
+ void *client_receiver_baton;
+} wc_info_receiver_baton_t;
+
+/* A receiver for WC info, implementing svn_client_info_receiver2_t.
+ * Convert the WC info to client info and pass it to the client info
+ * receiver (BATON->client_receiver_func with BATON->client_receiver_baton). */
+static svn_error_t *
+wc_info_receiver(void *baton,
+ const char *abspath_or_url,
+ const svn_wc__info2_t *wc_info,
+ apr_pool_t *scratch_pool)
+{
+ wc_info_receiver_baton_t *b = baton;
+ svn_client_info2_t client_info;
+
+ /* Make a shallow copy in CLIENT_INFO of the contents of WC_INFO. */
+ client_info.repos_root_URL = wc_info->repos_root_URL;
+ client_info.repos_UUID = wc_info->repos_UUID;
+ client_info.rev = wc_info->rev;
+ client_info.URL = wc_info->URL;
+
+ client_info.kind = wc_info->kind;
+ client_info.size = wc_info->size;
+ client_info.last_changed_rev = wc_info->last_changed_rev;
+ client_info.last_changed_date = wc_info->last_changed_date;
+ client_info.last_changed_author = wc_info->last_changed_author;
+
+ client_info.lock = wc_info->lock;
+
+ client_info.wc_info = wc_info->wc_info;
+
+ return b->client_receiver_func(b->client_receiver_baton,
+ abspath_or_url, &client_info, scratch_pool);
+}
+
+svn_error_t *
+svn_client_info3(const char *abspath_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t fetch_excluded,
+ svn_boolean_t fetch_actual_only,
+ const apr_array_header_t *changelists,
+ svn_client_info_receiver2_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *ra_session;
+ svn_client__pathrev_t *pathrev;
+ svn_lock_t *lock;
+ svn_boolean_t related;
+ const char *base_name;
+ svn_dirent_t *the_ent;
+ svn_client_info2_t *info;
+ svn_error_t *err;
+
+ if (depth == svn_depth_unknown)
+ depth = svn_depth_empty;
+
+ if ((revision == NULL
+ || revision->kind == svn_opt_revision_unspecified)
+ && (peg_revision == NULL
+ || peg_revision->kind == svn_opt_revision_unspecified))
+ {
+ /* Do all digging in the working copy. */
+ wc_info_receiver_baton_t b;
+
+ b.client_receiver_func = receiver;
+ b.client_receiver_baton = receiver_baton;
+ return svn_error_trace(
+ svn_wc__get_info(ctx->wc_ctx, abspath_or_url, depth,
+ fetch_excluded, fetch_actual_only, changelists,
+ wc_info_receiver, &b,
+ ctx->cancel_func, ctx->cancel_baton, pool));
+ }
+
+ /* Go repository digging instead. */
+
+ /* Trace rename history (starting at path_or_url@peg_revision) and
+ return RA session to the possibly-renamed URL as it exists in REVISION.
+ The ra_session returned will be anchored on this "final" URL. */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &pathrev,
+ abspath_or_url, NULL, peg_revision,
+ revision, ctx, pool));
+
+ svn_uri_split(NULL, &base_name, pathrev->url, pool);
+
+ /* Get the dirent for the URL itself. */
+ SVN_ERR(svn_client__ra_stat_compatible(ra_session, pathrev->rev, &the_ent,
+ DIRENT_FIELDS, ctx, pool));
+
+ if (! the_ent)
+ return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
+ _("URL '%s' non-existent in revision %ld"),
+ pathrev->url, pathrev->rev);
+
+ /* Check if the URL exists in HEAD and refers to the same resource.
+ In this case, we check the repository for a lock on this URL.
+
+ ### There is a possible race here, since HEAD might have changed since
+ ### we checked it. A solution to this problem could be to do the below
+ ### check in a loop which only terminates if the HEAD revision is the same
+ ### before and after this check. That could, however, lead to a
+ ### starvation situation instead. */
+ SVN_ERR(same_resource_in_head(&related, pathrev->url, pathrev->rev,
+ ra_session, ctx, pool));
+ if (related)
+ {
+ err = svn_ra_get_lock(ra_session, &lock, "", pool);
+
+ /* An old mod_dav_svn will always work; there's nothing wrong with
+ doing a PROPFIND for a property named "DAV:supportedlock". But
+ an old svnserve will error. */
+ if (err && err->apr_err == SVN_ERR_RA_NOT_IMPLEMENTED)
+ {
+ svn_error_clear(err);
+ lock = NULL;
+ }
+ else if (err)
+ return svn_error_trace(err);
+ }
+ else
+ lock = NULL;
+
+ /* Push the URL's dirent (and lock) at the callback.*/
+ SVN_ERR(build_info_from_dirent(&info, the_ent, lock, pathrev, pool));
+ SVN_ERR(receiver(receiver_baton, base_name, info, pool));
+
+ /* Possibly recurse, using the original RA session. */
+ if (depth > svn_depth_empty && (the_ent->kind == svn_node_dir))
+ {
+ apr_hash_t *locks;
+
+ if (peg_revision->kind == svn_opt_revision_head)
+ {
+ err = svn_ra_get_locks2(ra_session, &locks, "", depth,
+ pool);
+
+ /* Catch specific errors thrown by old mod_dav_svn or svnserve. */
+ if (err &&
+ (err->apr_err == SVN_ERR_RA_NOT_IMPLEMENTED
+ || err->apr_err == SVN_ERR_UNSUPPORTED_FEATURE))
+ {
+ svn_error_clear(err);
+ locks = apr_hash_make(pool); /* use an empty hash */
+ }
+ else if (err)
+ return svn_error_trace(err);
+ }
+ else
+ locks = apr_hash_make(pool); /* use an empty hash */
+
+ SVN_ERR(push_dir_info(ra_session, pathrev, "",
+ receiver, receiver_baton,
+ depth, ctx, locks, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client_get_wc_root(const char **wcroot_abspath,
+ const char *local_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ return svn_wc__get_wcroot(wcroot_abspath, ctx->wc_ctx, local_abspath,
+ result_pool, scratch_pool);
+}
+
+
+/* NOTE: This function was requested by the TortoiseSVN project. See
+ issue #3927. */
+svn_error_t *
+svn_client_min_max_revisions(svn_revnum_t *min_revision,
+ svn_revnum_t *max_revision,
+ const char *local_abspath,
+ svn_boolean_t committed,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ return svn_wc__min_max_revisions(min_revision, max_revision, ctx->wc_ctx,
+ local_abspath, committed, scratch_pool);
+}
diff --git a/subversion/libsvn_client/iprops.c b/subversion/libsvn_client/iprops.c
new file mode 100644
index 0000000..653ce8c
--- /dev/null
+++ b/subversion/libsvn_client/iprops.c
@@ -0,0 +1,270 @@
+/*
+ * iprops.c: wrappers around wc inherited property functionality
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_error.h"
+#include "svn_hash.h"
+#include "svn_pools.h"
+#include "svn_wc.h"
+#include "svn_ra.h"
+#include "svn_props.h"
+#include "svn_path.h"
+
+#include "client.h"
+#include "svn_private_config.h"
+
+#include "private/svn_wc_private.h"
+
+
+/*** Code. ***/
+
+/* Determine if LOCAL_ABSPATH needs an inherited property cache. If it does,
+ then set *NEEDS_CACHE to TRUE, set it to FALSE otherwise. All other args
+ are as per svn_client__get_inheritable_props(). */
+static svn_error_t *
+need_to_cache_iprops(svn_boolean_t *needs_cache,
+ const char *local_abspath,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_boolean_t is_wc_root;
+ svn_boolean_t is_switched;
+ svn_error_t *err;
+
+ err = svn_wc_check_root(&is_wc_root, &is_switched, NULL,
+ ctx->wc_ctx, local_abspath,
+ scratch_pool);
+
+ /* LOCAL_ABSPATH doesn't need a cache if it doesn't exist. */
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ is_wc_root = FALSE;
+ is_switched = FALSE;
+ }
+ else
+ {
+ return svn_error_trace(err);
+ }
+ }
+
+ /* Starting assumption. */
+ *needs_cache = FALSE;
+
+ if (is_wc_root || is_switched)
+ {
+ const char *session_url;
+ const char *session_root_url;
+
+ /* Looks likely that we need an inherited properties cache...Unless
+ LOCAL_ABSPATH is a WC root that points to the repos root. Then it
+ doesn't need a cache because it has nowhere to inherit from. Check
+ for that case. */
+ SVN_ERR(svn_ra_get_session_url(ra_session, &session_url, scratch_pool));
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &session_root_url,
+ scratch_pool));
+
+ if (strcmp(session_root_url, session_url) != 0)
+ *needs_cache = TRUE;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__iprop_relpaths_to_urls(apr_array_header_t *inherited_props,
+ const char *repos_root_url,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+
+ for (i = 0; i < inherited_props->nelts; i++)
+ {
+ svn_prop_inherited_item_t *elt =
+ APR_ARRAY_IDX(inherited_props, i, svn_prop_inherited_item_t *);
+
+ /* Convert repos root relpaths to full URLs. */
+ if (! (svn_path_is_url(elt->path_or_url)
+ || svn_dirent_is_absolute(elt->path_or_url)))
+ {
+ elt->path_or_url = svn_path_url_add_component2(repos_root_url,
+ elt->path_or_url,
+ result_pool);
+ }
+ }
+ return SVN_NO_ERROR;
+}
+
+/* The real implementation of svn_client__get_inheritable_props */
+static svn_error_t *
+get_inheritable_props(apr_hash_t **wcroot_iprops,
+ const char *local_abspath,
+ svn_revnum_t revision,
+ svn_depth_t depth,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *iprop_paths;
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_pool_t *session_pool = NULL;
+ *wcroot_iprops = apr_hash_make(result_pool);
+
+ SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(revision));
+
+ /* If we don't have a base revision for LOCAL_ABSPATH then it can't
+ possibly be a working copy root, nor can it contain any WC roots
+ in the form of switched subtrees. So there is nothing to cache. */
+
+ SVN_ERR(svn_wc__get_cached_iprop_children(&iprop_paths, depth,
+ ctx->wc_ctx, local_abspath,
+ scratch_pool, iterpool));
+
+ /* If we are in the midst of a checkout or an update that is bringing in
+ an external, then svn_wc__get_cached_iprop_children won't return
+ LOCAL_ABSPATH in IPROPS_PATHS because the former has no cached iprops
+ yet. So make sure LOCAL_ABSPATH is present if it's a WC root. */
+ if (!svn_hash_gets(iprop_paths, local_abspath))
+ {
+ svn_boolean_t needs_cached_iprops;
+
+ SVN_ERR(need_to_cache_iprops(&needs_cached_iprops, local_abspath,
+ ra_session, ctx, iterpool));
+ if (needs_cached_iprops)
+ {
+ const char *target_abspath = apr_pstrdup(scratch_pool,
+ local_abspath);
+
+ /* As value we set TARGET_ABSPATH, but any string besides ""
+ would do */
+ svn_hash_sets(iprop_paths, target_abspath, target_abspath);
+ }
+ }
+
+ for (hi = apr_hash_first(scratch_pool, iprop_paths);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *child_abspath = svn__apr_hash_index_key(hi);
+ const char *child_repos_relpath = svn__apr_hash_index_val(hi);
+ const char *url;
+ apr_array_header_t *inherited_props;
+ svn_error_t *err;
+
+ svn_pool_clear(iterpool);
+
+ if (*child_repos_relpath == '\0')
+ {
+ /* A repository root doesn't have inherited properties */
+ continue;
+ }
+
+ SVN_ERR(svn_wc__node_get_url(&url, ctx->wc_ctx, child_abspath,
+ iterpool, iterpool));
+ if (ra_session)
+ SVN_ERR(svn_ra_reparent(ra_session, url, scratch_pool));
+ else
+ {
+ if (! session_pool)
+ session_pool = svn_pool_create(scratch_pool);
+
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, url, NULL,
+ ctx,
+ session_pool, iterpool));
+ }
+
+ err = svn_ra_get_inherited_props(ra_session, &inherited_props,
+ "", revision,
+ result_pool, iterpool);
+
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_FS_NOT_FOUND)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ continue;
+ }
+
+ svn_hash_sets(*wcroot_iprops,
+ apr_pstrdup(result_pool, child_abspath),
+ inherited_props);
+ }
+
+
+ svn_pool_destroy(iterpool);
+ if (session_pool)
+ svn_pool_destroy(session_pool);
+
+ return SVN_NO_ERROR;
+
+}
+
+svn_error_t *
+svn_client__get_inheritable_props(apr_hash_t **wcroot_iprops,
+ const char *local_abspath,
+ svn_revnum_t revision,
+ svn_depth_t depth,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *old_session_url;
+ svn_error_t *err;
+
+ if (!SVN_IS_VALID_REVNUM(revision))
+ return SVN_NO_ERROR;
+
+ if (ra_session)
+ SVN_ERR(svn_ra_get_session_url(ra_session, &old_session_url, scratch_pool));
+
+ /* We just wrap a simple helper function, as it is to easy to leave the ra
+ session rooted at some wrong path without a wrapper like this.
+
+ During development we had problems where some now deleted switched path
+ made the update try to update to that url instead of the intended url
+ */
+
+ err = get_inheritable_props(wcroot_iprops, local_abspath, revision, depth,
+ ra_session, ctx, result_pool, scratch_pool);
+
+ if (ra_session)
+ {
+ err = svn_error_compose_create(
+ err,
+ svn_ra_reparent(ra_session, old_session_url, scratch_pool));
+ }
+ return svn_error_trace(err);
+}
diff --git a/subversion/libsvn_client/list.c b/subversion/libsvn_client/list.c
new file mode 100644
index 0000000..4093893
--- /dev/null
+++ b/subversion/libsvn_client/list.c
@@ -0,0 +1,579 @@
+/*
+ * list.c: list local and remote directory entries.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+#include "svn_client.h"
+#include "svn_dirent_uri.h"
+#include "svn_hash.h"
+#include "svn_path.h"
+#include "svn_pools.h"
+#include "svn_time.h"
+#include "svn_sorts.h"
+#include "svn_props.h"
+
+#include "client.h"
+
+#include "private/svn_fspath.h"
+#include "private/svn_ra_private.h"
+#include "private/svn_wc_private.h"
+#include "svn_private_config.h"
+
+/* Prototypes for referencing before declaration */
+static svn_error_t *
+list_externals(apr_hash_t *externals,
+ svn_depth_t depth,
+ apr_uint32_t dirent_fields,
+ svn_boolean_t fetch_locks,
+ svn_client_list_func2_t list_func,
+ void *baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool);
+
+static svn_error_t *
+list_internal(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ apr_uint32_t dirent_fields,
+ svn_boolean_t fetch_locks,
+ svn_boolean_t include_externals,
+ const char *external_parent_url,
+ const char *external_target,
+ svn_client_list_func2_t list_func,
+ void *baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+
+/* Get the directory entries of DIR at REV (relative to the root of
+ RA_SESSION), getting at least the fields specified by DIRENT_FIELDS.
+ Use the cancellation function/baton of CTX to check for cancellation.
+
+ If DEPTH is svn_depth_empty, return immediately. If DEPTH is
+ svn_depth_files, invoke LIST_FUNC on the file entries with BATON;
+ if svn_depth_immediates, invoke it on file and directory entries;
+ if svn_depth_infinity, invoke it on file and directory entries and
+ recurse into the directory entries with the same depth.
+
+ LOCKS, if non-NULL, is a hash mapping const char * paths to svn_lock_t
+ objects and FS_PATH is the absolute filesystem path of the RA session.
+ Use SCRATCH_POOL for temporary allocations.
+
+ If the caller passes EXTERNALS as non-NULL, populate the EXTERNALS
+ hash table whose keys are URLs of the directory which has externals
+ definitions, and whose values are the externals description text.
+ Allocate the hash's keys and values in RESULT_POOL.
+
+ EXTERNAL_PARENT_URL and EXTERNAL_TARGET are set when external items
+ are listed, otherwise both are set to NULL by the caller.
+*/
+static svn_error_t *
+get_dir_contents(apr_uint32_t dirent_fields,
+ const char *dir,
+ svn_revnum_t rev,
+ svn_ra_session_t *ra_session,
+ apr_hash_t *locks,
+ const char *fs_path,
+ svn_depth_t depth,
+ svn_client_ctx_t *ctx,
+ apr_hash_t *externals,
+ const char *external_parent_url,
+ const char *external_target,
+ svn_client_list_func2_t list_func,
+ void *baton,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *tmpdirents;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_array_header_t *array;
+ svn_error_t *err;
+ apr_hash_t *prop_hash = NULL;
+ const svn_string_t *prop_val = NULL;
+ int i;
+
+ if (depth == svn_depth_empty)
+ return SVN_NO_ERROR;
+
+ /* Get the directory's entries. If externals hash is non-NULL, get its
+ properties also. Ignore any not-authorized errors. */
+ err = svn_ra_get_dir2(ra_session, &tmpdirents, NULL,
+ externals ? &prop_hash : NULL,
+ dir, rev, dirent_fields, scratch_pool);
+
+ if (err && ((err->apr_err == SVN_ERR_RA_NOT_AUTHORIZED) ||
+ (err->apr_err == SVN_ERR_RA_DAV_FORBIDDEN)))
+ {
+ svn_error_clear(err);
+ return SVN_NO_ERROR;
+ }
+ SVN_ERR(err);
+
+ /* Filter out svn:externals from all properties hash. */
+ if (prop_hash)
+ prop_val = svn_hash_gets(prop_hash, SVN_PROP_EXTERNALS);
+ if (prop_val)
+ {
+ const char *url;
+
+ SVN_ERR(svn_ra_get_session_url(ra_session, &url, scratch_pool));
+
+ svn_hash_sets(externals,
+ svn_path_url_add_component2(url, dir, result_pool),
+ svn_string_dup(prop_val, result_pool));
+ }
+
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ /* Sort the hash, so we can call the callback in a "deterministic" order. */
+ array = svn_sort__hash(tmpdirents, svn_sort_compare_items_lexically,
+ scratch_pool);
+ for (i = 0; i < array->nelts; ++i)
+ {
+ svn_sort__item_t *item = &APR_ARRAY_IDX(array, i, svn_sort__item_t);
+ const char *path;
+ svn_dirent_t *the_ent = item->value;
+ svn_lock_t *lock;
+
+ svn_pool_clear(iterpool);
+
+ path = svn_relpath_join(dir, item->key, iterpool);
+
+ if (locks)
+ {
+ const char *abs_path = svn_fspath__join(fs_path, path, iterpool);
+ lock = svn_hash_gets(locks, abs_path);
+ }
+ else
+ lock = NULL;
+
+ if (the_ent->kind == svn_node_file
+ || depth == svn_depth_immediates
+ || depth == svn_depth_infinity)
+ SVN_ERR(list_func(baton, path, the_ent, lock, fs_path,
+ external_parent_url, external_target, iterpool));
+
+ /* If externals is non-NULL, populate the externals hash table
+ recursively for all directory entries. */
+ if (depth == svn_depth_infinity && the_ent->kind == svn_node_dir)
+ SVN_ERR(get_dir_contents(dirent_fields, path, rev,
+ ra_session, locks, fs_path, depth, ctx,
+ externals, external_parent_url,
+ external_target, list_func, baton,
+ result_pool, iterpool));
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Like svn_ra_stat() but with a compatibility hack for pre-1.2 svnserve. */
+/* ### Maybe we should move this behavior into the svn_ra_stat wrapper? */
+svn_error_t *
+svn_client__ra_stat_compatible(svn_ra_session_t *ra_session,
+ svn_revnum_t rev,
+ svn_dirent_t **dirent_p,
+ apr_uint32_t dirent_fields,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+
+ err = svn_ra_stat(ra_session, "", rev, dirent_p, pool);
+
+ /* svnserve before 1.2 doesn't support the above, so fall back on
+ a less efficient method. */
+ if (err && err->apr_err == SVN_ERR_RA_NOT_IMPLEMENTED)
+ {
+ const char *repos_root_url;
+ const char *session_url;
+ svn_node_kind_t kind;
+ svn_dirent_t *dirent;
+
+ svn_error_clear(err);
+
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root_url, pool));
+ SVN_ERR(svn_ra_get_session_url(ra_session, &session_url, pool));
+
+ SVN_ERR(svn_ra_check_path(ra_session, "", rev, &kind, pool));
+
+ if (kind != svn_node_none)
+ {
+ if (strcmp(session_url, repos_root_url) != 0)
+ {
+ svn_ra_session_t *parent_session;
+ apr_hash_t *parent_ents;
+ const char *parent_url, *base_name;
+ apr_pool_t *subpool = svn_pool_create(pool);
+
+ /* Open another session to the path's parent. This server
+ doesn't support svn_ra_reparent anyway, so don't try it. */
+ svn_uri_split(&parent_url, &base_name, session_url, subpool);
+
+ SVN_ERR(svn_client_open_ra_session2(&parent_session, parent_url,
+ NULL, ctx,
+ subpool, subpool));
+
+ /* Get all parent's entries, no props. */
+ SVN_ERR(svn_ra_get_dir2(parent_session, &parent_ents, NULL,
+ NULL, "", rev, dirent_fields, subpool));
+
+ /* Get the relevant entry. */
+ dirent = svn_hash_gets(parent_ents, base_name);
+
+ if (dirent)
+ *dirent_p = svn_dirent_dup(dirent, pool);
+ else
+ *dirent_p = NULL;
+
+ svn_pool_destroy(subpool); /* Close RA session */
+ }
+ else
+ {
+ /* We can't get the directory entry for the repository root,
+ but we can still get the information we want.
+ The created-rev of the repository root must, by definition,
+ be rev. */
+ dirent = apr_palloc(pool, sizeof(*dirent));
+ dirent->kind = kind;
+ dirent->size = SVN_INVALID_FILESIZE;
+ if (dirent_fields & SVN_DIRENT_HAS_PROPS)
+ {
+ apr_hash_t *props;
+ SVN_ERR(svn_ra_get_dir2(ra_session, NULL, NULL, &props,
+ "", rev, 0 /* no dirent fields */,
+ pool));
+ dirent->has_props = (apr_hash_count(props) != 0);
+ }
+ dirent->created_rev = rev;
+ if (dirent_fields & (SVN_DIRENT_TIME | SVN_DIRENT_LAST_AUTHOR))
+ {
+ apr_hash_t *props;
+ svn_string_t *val;
+
+ SVN_ERR(svn_ra_rev_proplist(ra_session, rev, &props,
+ pool));
+ val = svn_hash_gets(props, SVN_PROP_REVISION_DATE);
+ if (val)
+ SVN_ERR(svn_time_from_cstring(&dirent->time, val->data,
+ pool));
+ else
+ dirent->time = 0;
+
+ val = svn_hash_gets(props, SVN_PROP_REVISION_AUTHOR);
+ dirent->last_author = val ? val->data : NULL;
+ }
+
+ *dirent_p = dirent;
+ }
+ }
+ else
+ *dirent_p = NULL;
+ }
+ else
+ SVN_ERR(err);
+
+ return SVN_NO_ERROR;
+}
+
+/* List the file/directory entries for PATH_OR_URL at REVISION.
+ The actual node revision selected is determined by the path as
+ it exists in PEG_REVISION.
+
+ If DEPTH is svn_depth_infinity, then list all file and directory entries
+ recursively. Else if DEPTH is svn_depth_files, list all files under
+ PATH_OR_URL (if any), but not subdirectories. Else if DEPTH is
+ svn_depth_immediates, list all files and include immediate
+ subdirectories (at svn_depth_empty). Else if DEPTH is
+ svn_depth_empty, just list PATH_OR_URL with none of its entries.
+
+ DIRENT_FIELDS controls which fields in the svn_dirent_t's are
+ filled in. To have them totally filled in use SVN_DIRENT_ALL,
+ otherwise simply bitwise OR together the combination of SVN_DIRENT_*
+ fields you care about.
+
+ If FETCH_LOCKS is TRUE, include locks when reporting directory entries.
+
+ If INCLUDE_EXTERNALS is TRUE, also list all external items
+ reached by recursion. DEPTH value passed to the original list target
+ applies for the externals also. EXTERNAL_PARENT_URL is url of the
+ directory which has the externals definitions. EXTERNAL_TARGET is the
+ target subdirectory of externals definitions.
+
+ Report directory entries by invoking LIST_FUNC/BATON.
+ Pass EXTERNAL_PARENT_URL and EXTERNAL_TARGET to LIST_FUNC when external
+ items are listed, otherwise both are set to NULL.
+
+ Use authentication baton cached in CTX to authenticate against the
+ repository.
+
+ Use POOL for all allocations.
+*/
+static svn_error_t *
+list_internal(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ apr_uint32_t dirent_fields,
+ svn_boolean_t fetch_locks,
+ svn_boolean_t include_externals,
+ const char *external_parent_url,
+ const char *external_target,
+ svn_client_list_func2_t list_func,
+ void *baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *ra_session;
+ svn_client__pathrev_t *loc;
+ svn_dirent_t *dirent;
+ const char *fs_path;
+ svn_error_t *err;
+ apr_hash_t *locks;
+ apr_hash_t *externals;
+
+ if (include_externals)
+ externals = apr_hash_make(pool);
+ else
+ externals = NULL;
+
+ /* We use the kind field to determine if we should recurse, so we
+ always need it. */
+ dirent_fields |= SVN_DIRENT_KIND;
+
+ /* Get an RA plugin for this filesystem object. */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &loc,
+ path_or_url, NULL,
+ peg_revision,
+ revision, ctx, pool));
+
+ fs_path = svn_client__pathrev_fspath(loc, pool);
+
+ SVN_ERR(svn_client__ra_stat_compatible(ra_session, loc->rev, &dirent,
+ dirent_fields, ctx, pool));
+ if (! dirent)
+ return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
+ _("URL '%s' non-existent in revision %ld"),
+ loc->url, loc->rev);
+
+ /* Maybe get all locks under url. */
+ if (fetch_locks)
+ {
+ /* IMPORTANT: If locks are stored in a more temporary pool, we need
+ to fix store_dirent below to duplicate the locks. */
+ err = svn_ra_get_locks2(ra_session, &locks, "", depth, pool);
+
+ if (err && err->apr_err == SVN_ERR_RA_NOT_IMPLEMENTED)
+ {
+ svn_error_clear(err);
+ locks = NULL;
+ }
+ else if (err)
+ return svn_error_trace(err);
+ }
+ else
+ locks = NULL;
+
+ /* Report the dirent for the target. */
+ SVN_ERR(list_func(baton, "", dirent, locks
+ ? (svn_hash_gets(locks, fs_path))
+ : NULL, fs_path, external_parent_url,
+ external_target, pool));
+
+ if (dirent->kind == svn_node_dir
+ && (depth == svn_depth_files
+ || depth == svn_depth_immediates
+ || depth == svn_depth_infinity))
+ SVN_ERR(get_dir_contents(dirent_fields, "", loc->rev, ra_session, locks,
+ fs_path, depth, ctx, externals,
+ external_parent_url, external_target, list_func,
+ baton, pool, pool));
+
+ /* We handle externals after listing entries under path_or_url, so that
+ handling external items (and any errors therefrom) doesn't delay
+ the primary operation. */
+ if (include_externals && apr_hash_count(externals))
+ {
+ /* The 'externals' hash populated by get_dir_contents() is processed
+ here. */
+ SVN_ERR(list_externals(externals, depth, dirent_fields,
+ fetch_locks, list_func, baton,
+ ctx, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+wrap_list_error(const svn_client_ctx_t *ctx,
+ const char *target_abspath,
+ svn_error_t *err,
+ apr_pool_t *scratch_pool)
+{
+ if (err && err->apr_err != SVN_ERR_CANCELLED)
+ {
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notifier = svn_wc_create_notify(
+ target_abspath,
+ svn_wc_notify_failed_external,
+ scratch_pool);
+ notifier->err = err;
+ ctx->notify_func2(ctx->notify_baton2, notifier, scratch_pool);
+ }
+ svn_error_clear(err);
+ return SVN_NO_ERROR;
+ }
+
+ return err;
+}
+
+
+/* Walk through all the external items and list them. */
+static svn_error_t *
+list_external_items(apr_array_header_t *external_items,
+ const char *externals_parent_url,
+ svn_depth_t depth,
+ apr_uint32_t dirent_fields,
+ svn_boolean_t fetch_locks,
+ svn_client_list_func2_t list_func,
+ void *baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ const char *externals_parent_repos_root_url;
+ apr_pool_t *iterpool;
+ int i;
+
+ SVN_ERR(svn_client_get_repos_root(&externals_parent_repos_root_url,
+ NULL /* uuid */,
+ externals_parent_url, ctx,
+ scratch_pool, scratch_pool));
+
+ iterpool = svn_pool_create(scratch_pool);
+
+ for (i = 0; i < external_items->nelts; i++)
+ {
+ const char *resolved_url;
+
+ svn_wc_external_item2_t *item =
+ APR_ARRAY_IDX(external_items, i, svn_wc_external_item2_t *);
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_wc__resolve_relative_external_url(
+ &resolved_url,
+ item,
+ externals_parent_repos_root_url,
+ externals_parent_url,
+ iterpool, iterpool));
+
+ /* List the external */
+ SVN_ERR(wrap_list_error(ctx, item->target_dir,
+ list_internal(resolved_url,
+ &item->peg_revision,
+ &item->revision,
+ depth, dirent_fields,
+ fetch_locks,
+ TRUE,
+ externals_parent_url,
+ item->target_dir,
+ list_func, baton, ctx,
+ iterpool),
+ iterpool));
+
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* List external items defined on each external in EXTERNALS, a const char *
+ externals_parent_url(url of the directory which has the externals
+ definitions) of all externals mapping to the svn_string_t * externals_desc
+ (externals description text). All other options are the same as those
+ passed to svn_client_list(). */
+static svn_error_t *
+list_externals(apr_hash_t *externals,
+ svn_depth_t depth,
+ apr_uint32_t dirent_fields,
+ svn_boolean_t fetch_locks,
+ svn_client_list_func2_t list_func,
+ void *baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(scratch_pool, externals);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *externals_parent_url = svn__apr_hash_index_key(hi);
+ svn_string_t *externals_desc = svn__apr_hash_index_val(hi);
+ apr_array_header_t *external_items;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_wc_parse_externals_description3(&external_items,
+ externals_parent_url,
+ externals_desc->data,
+ FALSE, iterpool));
+
+ if (! external_items->nelts)
+ continue;
+
+ SVN_ERR(list_external_items(external_items, externals_parent_url, depth,
+ dirent_fields, fetch_locks, list_func,
+ baton, ctx, iterpool));
+
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client_list3(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ apr_uint32_t dirent_fields,
+ svn_boolean_t fetch_locks,
+ svn_boolean_t include_externals,
+ svn_client_list_func2_t list_func,
+ void *baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+
+ return svn_error_trace(list_internal(path_or_url, peg_revision,
+ revision,
+ depth, dirent_fields,
+ fetch_locks,
+ include_externals,
+ NULL, NULL, list_func,
+ baton, ctx, pool));
+}
diff --git a/subversion/libsvn_client/locking_commands.c b/subversion/libsvn_client/locking_commands.c
new file mode 100644
index 0000000..c768503
--- /dev/null
+++ b/subversion/libsvn_client/locking_commands.c
@@ -0,0 +1,552 @@
+/*
+ * locking_commands.c: Implementation of lock and unlock.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_client.h"
+#include "svn_hash.h"
+#include "client.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_xml.h"
+#include "svn_pools.h"
+
+#include "svn_private_config.h"
+#include "private/svn_client_private.h"
+#include "private/svn_wc_private.h"
+
+
+/*** Code. ***/
+
+/* For use with store_locks_callback, below. */
+struct lock_baton
+{
+ const char *base_dir_abspath;
+ apr_hash_t *urls_to_paths;
+ svn_client_ctx_t *ctx;
+ apr_pool_t *pool;
+};
+
+
+/* This callback is called by the ra_layer for each path locked.
+ * BATON is a 'struct lock_baton *', PATH is the path being locked,
+ * and LOCK is the lock itself.
+ *
+ * If BATON->base_dir_abspath is not null, then this function either
+ * stores the LOCK on REL_URL or removes any lock tokens from REL_URL
+ * (depending on whether DO_LOCK is true or false respectively), but
+ * only if RA_ERR is null, or (in the unlock case) is something other
+ * than SVN_ERR_FS_LOCK_OWNER_MISMATCH.
+ *
+ * Implements svn_ra_lock_callback_t.
+ */
+static svn_error_t *
+store_locks_callback(void *baton,
+ const char *rel_url,
+ svn_boolean_t do_lock,
+ const svn_lock_t *lock,
+ svn_error_t *ra_err, apr_pool_t *pool)
+{
+ struct lock_baton *lb = baton;
+ svn_wc_notify_t *notify;
+
+ /* Create the notify struct first, so we can tweak it below. */
+ notify = svn_wc_create_notify(rel_url,
+ do_lock
+ ? (ra_err
+ ? svn_wc_notify_failed_lock
+ : svn_wc_notify_locked)
+ : (ra_err
+ ? svn_wc_notify_failed_unlock
+ : svn_wc_notify_unlocked),
+ pool);
+ notify->lock = lock;
+ notify->err = ra_err;
+
+ if (lb->base_dir_abspath)
+ {
+ char *path = svn_hash_gets(lb->urls_to_paths, rel_url);
+ const char *local_abspath;
+
+ local_abspath = svn_dirent_join(lb->base_dir_abspath, path, pool);
+
+ /* Notify a valid working copy path */
+ notify->path = local_abspath;
+ notify->path_prefix = lb->base_dir_abspath;
+
+ if (do_lock)
+ {
+ if (!ra_err)
+ {
+ SVN_ERR(svn_wc_add_lock2(lb->ctx->wc_ctx, local_abspath, lock,
+ lb->pool));
+ notify->lock_state = svn_wc_notify_lock_state_locked;
+ }
+ else
+ notify->lock_state = svn_wc_notify_lock_state_unchanged;
+ }
+ else /* unlocking */
+ {
+ /* Remove our wc lock token either a) if we got no error, or b) if
+ we got any error except for owner mismatch. Note that the only
+ errors that are handed to this callback will be locking-related
+ errors. */
+
+ if (!ra_err ||
+ (ra_err && (ra_err->apr_err != SVN_ERR_FS_LOCK_OWNER_MISMATCH)))
+ {
+ SVN_ERR(svn_wc_remove_lock2(lb->ctx->wc_ctx, local_abspath,
+ lb->pool));
+ notify->lock_state = svn_wc_notify_lock_state_unlocked;
+ }
+ else
+ notify->lock_state = svn_wc_notify_lock_state_unchanged;
+ }
+ }
+ else
+ notify->url = rel_url; /* Notify that path is actually a url */
+
+ if (lb->ctx->notify_func2)
+ lb->ctx->notify_func2(lb->ctx->notify_baton2, notify, pool);
+
+ return SVN_NO_ERROR;
+}
+
+
+/* This is a wrapper around svn_uri_condense_targets() and
+ * svn_dirent_condense_targets() (the choice of which is made based on
+ * the value of TARGETS_ARE_URIS) which takes care of the
+ * single-target special case.
+ *
+ * Callers are expected to check for an empty *COMMON_PARENT (which
+ * means, "there was nothing common") for themselves.
+ */
+static svn_error_t *
+condense_targets(const char **common_parent,
+ apr_array_header_t **target_relpaths,
+ const apr_array_header_t *targets,
+ svn_boolean_t targets_are_uris,
+ svn_boolean_t remove_redundancies,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ if (targets_are_uris)
+ {
+ SVN_ERR(svn_uri_condense_targets(common_parent, target_relpaths,
+ targets, remove_redundancies,
+ result_pool, scratch_pool));
+ }
+ else
+ {
+ SVN_ERR(svn_dirent_condense_targets(common_parent, target_relpaths,
+ targets, remove_redundancies,
+ result_pool, scratch_pool));
+ }
+
+ /* svn_*_condense_targets leaves *TARGET_RELPATHS empty if TARGETS only
+ had 1 member, so we special case that. */
+ if (apr_is_empty_array(*target_relpaths))
+ {
+ const char *base_name;
+
+ if (targets_are_uris)
+ {
+ svn_uri_split(common_parent, &base_name,
+ *common_parent, result_pool);
+ }
+ else
+ {
+ svn_dirent_split(common_parent, &base_name,
+ *common_parent, result_pool);
+ }
+ APR_ARRAY_PUSH(*target_relpaths, const char *) = base_name;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Lock info. Used in organize_lock_targets.
+ ### Maybe return this instead of the ugly hashes? */
+struct wc_lock_item_t
+{
+ svn_revnum_t revision;
+ const char *lock_token;
+};
+
+/* Set *COMMON_PARENT_URL to the nearest common parent URL of all TARGETS.
+ * If TARGETS are local paths, then the entry for each path is examined
+ * and *COMMON_PARENT is set to the common parent URL for all the
+ * targets (as opposed to the common local path).
+ *
+ * If there is no common parent, either because the targets are a
+ * mixture of URLs and local paths, or because they simply do not
+ * share a common parent, then return SVN_ERR_UNSUPPORTED_FEATURE.
+ *
+ * DO_LOCK is TRUE for locking TARGETS, and FALSE for unlocking them.
+ * FORCE is TRUE for breaking or stealing locks, and FALSE otherwise.
+ *
+ * Each key stored in *REL_TARGETS_P is a path relative to
+ * *COMMON_PARENT. If TARGETS are local paths, then: if DO_LOCK is
+ * true, the value is a pointer to the corresponding base_revision
+ * (allocated in POOL) for the path, else the value is the lock token
+ * (or "" if no token found in the wc).
+ *
+ * If TARGETS is an array of urls, REL_FS_PATHS_P is set to NULL.
+ * Otherwise each key in REL_FS_PATHS_P is an repository path (relative to
+ * COMMON_PARENT) mapped to the target path for TARGET (relative to
+ * the common parent WC path). working copy targets that they "belong" to.
+ *
+ * If *COMMON_PARENT is a URL, then the values are a pointer to
+ * SVN_INVALID_REVNUM (allocated in pool) if DO_LOCK, else "".
+ *
+ * TARGETS may not be empty.
+ */
+static svn_error_t *
+organize_lock_targets(const char **common_parent_url,
+ const char **base_dir,
+ apr_hash_t **rel_targets_p,
+ apr_hash_t **rel_fs_paths_p,
+ const apr_array_header_t *targets,
+ svn_boolean_t do_lock,
+ svn_boolean_t force,
+ svn_wc_context_t *wc_ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *common_url = NULL;
+ const char *common_dirent = NULL;
+ apr_hash_t *rel_targets_ret = apr_hash_make(result_pool);
+ apr_hash_t *rel_fs_paths = NULL;
+ apr_array_header_t *rel_targets;
+ apr_hash_t *wc_info = apr_hash_make(scratch_pool);
+ svn_boolean_t url_mode;
+ int i;
+
+ SVN_ERR_ASSERT(targets->nelts);
+ SVN_ERR(svn_client__assert_homogeneous_target_type(targets));
+
+ url_mode = svn_path_is_url(APR_ARRAY_IDX(targets, 0, const char *));
+
+ if (url_mode)
+ {
+ svn_revnum_t *invalid_revnum =
+ apr_palloc(result_pool, sizeof(*invalid_revnum));
+
+ *invalid_revnum = SVN_INVALID_REVNUM;
+
+ /* Get the common parent URL and a bunch of relpaths, one per target. */
+ SVN_ERR(condense_targets(&common_url, &rel_targets, targets,
+ TRUE, TRUE, result_pool, scratch_pool));
+ if (! (common_url && *common_url))
+ return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("No common parent found, unable to operate "
+ "on disjoint arguments"));
+
+ /* Create mapping of the target relpaths to either
+ SVN_INVALID_REVNUM (if our caller is locking) or to an empty
+ lock token string (if the caller is unlocking). */
+ for (i = 0; i < rel_targets->nelts; i++)
+ {
+ svn_hash_sets(rel_targets_ret,
+ APR_ARRAY_IDX(rel_targets, i, const char *),
+ do_lock
+ ? (const void *)invalid_revnum
+ : (const void *)"");
+ }
+ }
+ else
+ {
+ apr_array_header_t *rel_urls, *target_urls;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ /* Get the common parent dirent and a bunch of relpaths, one per
+ target. */
+ SVN_ERR(condense_targets(&common_dirent, &rel_targets, targets,
+ FALSE, TRUE, result_pool, scratch_pool));
+ if (! (common_dirent && *common_dirent))
+ return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("No common parent found, unable to operate "
+ "on disjoint arguments"));
+
+ /* Get the URL for each target (which also serves to verify that
+ the dirent targets are sane). */
+ target_urls = apr_array_make(scratch_pool, rel_targets->nelts,
+ sizeof(const char *));
+ for (i = 0; i < rel_targets->nelts; i++)
+ {
+ const char *rel_target;
+ const char *repos_relpath;
+ const char *repos_root_url;
+ const char *target_url;
+ struct wc_lock_item_t *wli;
+ const char *local_abspath;
+ svn_node_kind_t kind;
+
+ svn_pool_clear(iterpool);
+
+ rel_target = APR_ARRAY_IDX(rel_targets, i, const char *);
+ local_abspath = svn_dirent_join(common_dirent, rel_target, scratch_pool);
+ wli = apr_pcalloc(scratch_pool, sizeof(*wli));
+
+ SVN_ERR(svn_wc__node_get_base(&kind, &wli->revision, &repos_relpath,
+ &repos_root_url, NULL,
+ &wli->lock_token,
+ wc_ctx, local_abspath,
+ FALSE /* ignore_enoent */,
+ FALSE /* show_hidden */,
+ result_pool, iterpool));
+
+ if (kind != svn_node_file)
+ return svn_error_createf(SVN_ERR_WC_NOT_FILE, NULL,
+ _("The node '%s' is not a file"),
+ svn_dirent_local_style(local_abspath,
+ iterpool));
+
+ svn_hash_sets(wc_info, local_abspath, wli);
+
+ target_url = svn_path_url_add_component2(repos_root_url,
+ repos_relpath,
+ scratch_pool);
+
+ APR_ARRAY_PUSH(target_urls, const char *) = target_url;
+ }
+
+ /* Now that we have a bunch of URLs for our dirent targets,
+ condense those into a single common parent URL and a bunch of
+ paths relative to that. */
+ SVN_ERR(condense_targets(&common_url, &rel_urls, target_urls,
+ TRUE, FALSE, result_pool, scratch_pool));
+ if (! (common_url && *common_url))
+ return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Unable to lock/unlock across multiple "
+ "repositories"));
+
+ /* Now we need to create a couple of different hash mappings. */
+ rel_fs_paths = apr_hash_make(result_pool);
+ for (i = 0; i < rel_targets->nelts; i++)
+ {
+ const char *rel_target, *rel_url;
+ const char *local_abspath;
+
+ svn_pool_clear(iterpool);
+
+ /* First, we need to map our REL_URL (which is relative to
+ COMMON_URL) to our REL_TARGET (which is relative to
+ COMMON_DIRENT). */
+ rel_target = APR_ARRAY_IDX(rel_targets, i, const char *);
+ rel_url = APR_ARRAY_IDX(rel_urls, i, const char *);
+ svn_hash_sets(rel_fs_paths, rel_url,
+ apr_pstrdup(result_pool, rel_target));
+
+ /* Then, we map our REL_URL (again) to either the base
+ revision of the dirent target with which it is associated
+ (if our caller is locking) or to a (possible empty) lock
+ token string (if the caller is unlocking). */
+ local_abspath = svn_dirent_join(common_dirent, rel_target, iterpool);
+
+ if (do_lock) /* Lock. */
+ {
+ svn_revnum_t *revnum;
+ struct wc_lock_item_t *wli;
+ revnum = apr_palloc(result_pool, sizeof(* revnum));
+
+ wli = svn_hash_gets(wc_info, local_abspath);
+
+ SVN_ERR_ASSERT(wli != NULL);
+
+ *revnum = wli->revision;
+
+ svn_hash_sets(rel_targets_ret, rel_url, revnum);
+ }
+ else /* Unlock. */
+ {
+ const char *lock_token;
+ struct wc_lock_item_t *wli;
+
+ /* If not forcing the unlock, get the lock token. */
+ if (! force)
+ {
+ wli = svn_hash_gets(wc_info, local_abspath);
+
+ SVN_ERR_ASSERT(wli != NULL);
+
+ if (! wli->lock_token)
+ return svn_error_createf(
+ SVN_ERR_CLIENT_MISSING_LOCK_TOKEN, NULL,
+ _("'%s' is not locked in this working copy"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+
+ lock_token = wli->lock_token
+ ? apr_pstrdup(result_pool, wli->lock_token)
+ : NULL;
+ }
+ else
+ lock_token = NULL;
+
+ /* If breaking a lock, we shouldn't pass any lock token. */
+ svn_hash_sets(rel_targets_ret, rel_url,
+ lock_token ? lock_token : "");
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ }
+
+ /* Set our return variables. */
+ *common_parent_url = common_url;
+ *base_dir = common_dirent;
+ *rel_targets_p = rel_targets_ret;
+ *rel_fs_paths_p = rel_fs_paths;
+
+ return SVN_NO_ERROR;
+}
+
+/* Fetch lock tokens from the repository for the paths in PATH_TOKENS,
+ setting the values to the fetched tokens, allocated in pool. */
+static svn_error_t *
+fetch_tokens(svn_ra_session_t *ra_session, apr_hash_t *path_tokens,
+ apr_pool_t *pool)
+{
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ for (hi = apr_hash_first(pool, path_tokens); hi; hi = apr_hash_next(hi))
+ {
+ const char *path = svn__apr_hash_index_key(hi);
+ svn_lock_t *lock;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_ra_get_lock(ra_session, &lock, path, iterpool));
+
+ if (! lock)
+ return svn_error_createf
+ (SVN_ERR_CLIENT_MISSING_LOCK_TOKEN, NULL,
+ _("'%s' is not locked"), path);
+
+ svn_hash_sets(path_tokens, path, apr_pstrdup(pool, lock->token));
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client_lock(const apr_array_header_t *targets,
+ const char *comment,
+ svn_boolean_t steal_lock,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *base_dir;
+ const char *base_dir_abspath = NULL;
+ const char *common_parent_url;
+ svn_ra_session_t *ra_session;
+ apr_hash_t *path_revs, *urls_to_paths;
+ struct lock_baton cb;
+
+ if (apr_is_empty_array(targets))
+ return SVN_NO_ERROR;
+
+ /* Enforce that the comment be xml-escapable. */
+ if (comment)
+ {
+ if (! svn_xml_is_xml_safe(comment, strlen(comment)))
+ return svn_error_create
+ (SVN_ERR_XML_UNESCAPABLE_DATA, NULL,
+ _("Lock comment contains illegal characters"));
+ }
+
+ SVN_ERR(organize_lock_targets(&common_parent_url, &base_dir, &path_revs,
+ &urls_to_paths, targets, TRUE, steal_lock,
+ ctx->wc_ctx, pool, pool));
+
+ /* Open an RA session to the common parent of TARGETS. */
+ if (base_dir)
+ SVN_ERR(svn_dirent_get_absolute(&base_dir_abspath, base_dir, pool));
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, common_parent_url,
+ base_dir_abspath, ctx, pool, pool));
+
+ cb.base_dir_abspath = base_dir_abspath;
+ cb.urls_to_paths = urls_to_paths;
+ cb.ctx = ctx;
+ cb.pool = pool;
+
+ /* Lock the paths. */
+ SVN_ERR(svn_ra_lock(ra_session, path_revs, comment,
+ steal_lock, store_locks_callback, &cb, pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_unlock(const apr_array_header_t *targets,
+ svn_boolean_t break_lock,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *base_dir;
+ const char *base_dir_abspath = NULL;
+ const char *common_parent_url;
+ svn_ra_session_t *ra_session;
+ apr_hash_t *path_tokens, *urls_to_paths;
+ struct lock_baton cb;
+
+ if (apr_is_empty_array(targets))
+ return SVN_NO_ERROR;
+
+ SVN_ERR(organize_lock_targets(&common_parent_url, &base_dir, &path_tokens,
+ &urls_to_paths, targets, FALSE, break_lock,
+ ctx->wc_ctx, pool, pool));
+
+ /* Open an RA session. */
+ if (base_dir)
+ SVN_ERR(svn_dirent_get_absolute(&base_dir_abspath, base_dir, pool));
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, common_parent_url,
+ base_dir_abspath, ctx, pool, pool));
+
+ /* If break_lock is not set, lock tokens are required by the server.
+ If the targets were all URLs, ensure that we provide lock tokens,
+ so the repository will only check that the user owns the
+ locks. */
+ if (! base_dir && !break_lock)
+ SVN_ERR(fetch_tokens(ra_session, path_tokens, pool));
+
+ cb.base_dir_abspath = base_dir_abspath;
+ cb.urls_to_paths = urls_to_paths;
+ cb.ctx = ctx;
+ cb.pool = pool;
+
+ /* Unlock the paths. */
+ SVN_ERR(svn_ra_unlock(ra_session, path_tokens, break_lock,
+ store_locks_callback, &cb, pool));
+
+ return SVN_NO_ERROR;
+}
+
diff --git a/subversion/libsvn_client/log.c b/subversion/libsvn_client/log.c
new file mode 100644
index 0000000..ca3edac
--- /dev/null
+++ b/subversion/libsvn_client/log.c
@@ -0,0 +1,868 @@
+/*
+ * log.c: return log messages
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+#define APR_WANT_STRFUNC
+#include <apr_want.h>
+
+#include <apr_strings.h>
+#include <apr_pools.h>
+
+#include "svn_pools.h"
+#include "svn_client.h"
+#include "svn_compat.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_hash.h"
+#include "svn_path.h"
+#include "svn_sorts.h"
+#include "svn_props.h"
+
+#include "client.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+
+#include <assert.h>
+
+/*** Getting misc. information ***/
+
+/* The baton for use with copyfrom_info_receiver(). */
+typedef struct copyfrom_info_t
+{
+ svn_boolean_t is_first;
+ const char *path;
+ svn_revnum_t rev;
+ apr_pool_t *pool;
+} copyfrom_info_t;
+
+/* A location segment callback for obtaining the copy source of
+ a node at a path and storing it in *BATON (a struct copyfrom_info_t *).
+ Implements svn_location_segment_receiver_t. */
+static svn_error_t *
+copyfrom_info_receiver(svn_location_segment_t *segment,
+ void *baton,
+ apr_pool_t *pool)
+{
+ copyfrom_info_t *copyfrom_info = baton;
+
+ /* If we've already identified the copy source, there's nothing more
+ to do.
+ ### FIXME: We *should* be able to send */
+ if (copyfrom_info->path)
+ return SVN_NO_ERROR;
+
+ /* If this is the first segment, it's not of interest to us. Otherwise
+ (so long as this segment doesn't represent a history gap), it holds
+ our path's previous location (from which it was last copied). */
+ if (copyfrom_info->is_first)
+ {
+ copyfrom_info->is_first = FALSE;
+ }
+ else if (segment->path)
+ {
+ /* The end of the second non-gap segment is the location copied from. */
+ copyfrom_info->path = apr_pstrdup(copyfrom_info->pool, segment->path);
+ copyfrom_info->rev = segment->range_end;
+
+ /* ### FIXME: We *should* be able to return SVN_ERR_CEASE_INVOCATION
+ ### here so we don't get called anymore. */
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__get_copy_source(const char **original_repos_relpath,
+ svn_revnum_t *original_revision,
+ const char *path_or_url,
+ const svn_opt_revision_t *revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err;
+ copyfrom_info_t copyfrom_info = { 0 };
+ apr_pool_t *sesspool = svn_pool_create(scratch_pool);
+ svn_ra_session_t *ra_session;
+ svn_client__pathrev_t *at_loc;
+
+ copyfrom_info.is_first = TRUE;
+ copyfrom_info.path = NULL;
+ copyfrom_info.rev = SVN_INVALID_REVNUM;
+ copyfrom_info.pool = result_pool;
+
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &at_loc,
+ path_or_url, NULL,
+ revision, revision,
+ ctx, sesspool));
+
+ /* Find the copy source. Walk the location segments to find the revision
+ at which this node was created (copied or added). */
+
+ err = svn_ra_get_location_segments(ra_session, "", at_loc->rev, at_loc->rev,
+ SVN_INVALID_REVNUM,
+ copyfrom_info_receiver, &copyfrom_info,
+ scratch_pool);
+
+ svn_pool_destroy(sesspool);
+
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_FS_NOT_FOUND ||
+ err->apr_err == SVN_ERR_RA_DAV_REQUEST_FAILED)
+ {
+ /* A locally-added but uncommitted versioned resource won't
+ exist in the repository. */
+ svn_error_clear(err);
+ err = SVN_NO_ERROR;
+
+ *original_repos_relpath = NULL;
+ *original_revision = SVN_INVALID_REVNUM;
+ }
+ return svn_error_trace(err);
+ }
+
+ *original_repos_relpath = copyfrom_info.path;
+ *original_revision = copyfrom_info.rev;
+ return SVN_NO_ERROR;
+}
+
+
+/* compatibility with pre-1.5 servers, which send only author/date/log
+ *revprops in log entries */
+typedef struct pre_15_receiver_baton_t
+{
+ svn_client_ctx_t *ctx;
+ /* ra session for retrieving revprops from old servers */
+ svn_ra_session_t *ra_session;
+ /* caller's list of requested revprops, receiver, and baton */
+ const char *ra_session_url;
+ apr_pool_t *ra_session_pool;
+ const apr_array_header_t *revprops;
+ svn_log_entry_receiver_t receiver;
+ void *baton;
+} pre_15_receiver_baton_t;
+
+static svn_error_t *
+pre_15_receiver(void *baton, svn_log_entry_t *log_entry, apr_pool_t *pool)
+{
+ pre_15_receiver_baton_t *rb = baton;
+
+ if (log_entry->revision == SVN_INVALID_REVNUM)
+ return rb->receiver(rb->baton, log_entry, pool);
+
+ /* If only some revprops are requested, get them one at a time on the
+ second ra connection. If all are requested, get them all with
+ svn_ra_rev_proplist. This avoids getting unrequested revprops (which
+ may be arbitrarily large), but means one round-trip per requested
+ revprop. epg isn't entirely sure which should be optimized for. */
+ if (rb->revprops)
+ {
+ int i;
+ svn_boolean_t want_author, want_date, want_log;
+ want_author = want_date = want_log = FALSE;
+ for (i = 0; i < rb->revprops->nelts; i++)
+ {
+ const char *name = APR_ARRAY_IDX(rb->revprops, i, const char *);
+ svn_string_t *value;
+
+ /* If a standard revprop is requested, we know it is already in
+ log_entry->revprops if available. */
+ if (strcmp(name, SVN_PROP_REVISION_AUTHOR) == 0)
+ {
+ want_author = TRUE;
+ continue;
+ }
+ if (strcmp(name, SVN_PROP_REVISION_DATE) == 0)
+ {
+ want_date = TRUE;
+ continue;
+ }
+ if (strcmp(name, SVN_PROP_REVISION_LOG) == 0)
+ {
+ want_log = TRUE;
+ continue;
+ }
+
+ if (rb->ra_session == NULL)
+ SVN_ERR(svn_client_open_ra_session2(&rb->ra_session,
+ rb->ra_session_url, NULL,
+ rb->ctx, rb->ra_session_pool,
+ pool));
+
+ SVN_ERR(svn_ra_rev_prop(rb->ra_session, log_entry->revision,
+ name, &value, pool));
+ if (log_entry->revprops == NULL)
+ log_entry->revprops = apr_hash_make(pool);
+ svn_hash_sets(log_entry->revprops, name, value);
+ }
+ if (log_entry->revprops)
+ {
+ /* Pre-1.5 servers send the standard revprops unconditionally;
+ clear those the caller doesn't want. */
+ if (!want_author)
+ svn_hash_sets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR, NULL);
+ if (!want_date)
+ svn_hash_sets(log_entry->revprops, SVN_PROP_REVISION_DATE, NULL);
+ if (!want_log)
+ svn_hash_sets(log_entry->revprops, SVN_PROP_REVISION_LOG, NULL);
+ }
+ }
+ else
+ {
+ if (rb->ra_session == NULL)
+ SVN_ERR(svn_client_open_ra_session2(&rb->ra_session,
+ rb->ra_session_url, NULL,
+ rb->ctx, rb->ra_session_pool,
+ pool));
+
+ SVN_ERR(svn_ra_rev_proplist(rb->ra_session, log_entry->revision,
+ &log_entry->revprops, pool));
+ }
+
+ return rb->receiver(rb->baton, log_entry, pool);
+}
+
+/* limit receiver */
+typedef struct limit_receiver_baton_t
+{
+ int limit;
+ svn_log_entry_receiver_t receiver;
+ void *baton;
+} limit_receiver_baton_t;
+
+static svn_error_t *
+limit_receiver(void *baton, svn_log_entry_t *log_entry, apr_pool_t *pool)
+{
+ limit_receiver_baton_t *rb = baton;
+
+ rb->limit--;
+
+ return rb->receiver(rb->baton, log_entry, pool);
+}
+
+/* Resolve the URLs or WC path in TARGETS as per the svn_client_log5 API.
+
+ The limitations on TARGETS specified by svn_client_log5 are enforced here.
+ So TARGETS can only contain a single WC path or a URL and zero or more
+ relative paths -- anything else will raise an error.
+
+ PEG_REVISION, TARGETS, and CTX are as per svn_client_log5.
+
+ If TARGETS contains a single WC path then set *RA_TARGET to the absolute
+ path of that single path if PEG_REVISION is dependent on the working copy
+ (e.g. PREV). Otherwise set *RA_TARGET to the corresponding URL for the
+ single WC path. Set *RELATIVE_TARGETS to an array with a single
+ element "".
+
+ If TARGETS contains only a single URL, then set *RA_TARGET to a copy of
+ that URL and *RELATIVE_TARGETS to an array with a single element "".
+
+ If TARGETS contains a single URL and one or more relative paths, then
+ set *RA_TARGET to a copy of that URL and *RELATIVE_TARGETS to a copy of
+ each relative path after the URL.
+
+ If *PEG_REVISION is svn_opt_revision_unspecified, then *PEG_REVISION is
+ set to svn_opt_revision_head for URLs or svn_opt_revision_working for a
+ WC path.
+
+ *RA_TARGET and *RELATIVE_TARGETS are allocated in RESULT_POOL. */
+static svn_error_t *
+resolve_log_targets(apr_array_header_t **relative_targets,
+ const char **ra_target,
+ svn_opt_revision_t *peg_revision,
+ const apr_array_header_t *targets,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ svn_boolean_t url_targets;
+
+ /* Per svn_client_log5, TARGETS contains either a URL followed by zero or
+ more relative paths, or one working copy path. */
+ const char *url_or_path = APR_ARRAY_IDX(targets, 0, const char *);
+
+ /* svn_client_log5 requires at least one target. */
+ if (targets->nelts == 0)
+ return svn_error_create(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("No valid target found"));
+
+ /* Initialize the output array. At a minimum, we need room for one
+ (possibly empty) relpath. Otherwise, we have to hold a relpath
+ for every item in TARGETS except the first. */
+ *relative_targets = apr_array_make(result_pool,
+ MAX(1, targets->nelts - 1),
+ sizeof(const char *));
+
+ if (svn_path_is_url(url_or_path))
+ {
+ /* An unspecified PEG_REVISION for a URL path defaults
+ to svn_opt_revision_head. */
+ if (peg_revision->kind == svn_opt_revision_unspecified)
+ peg_revision->kind = svn_opt_revision_head;
+
+ /* The logic here is this: If we get passed one argument, we assume
+ it is the full URL to a file/dir we want log info for. If we get
+ a URL plus some paths, then we assume that the URL is the base,
+ and that the paths passed are relative to it. */
+ if (targets->nelts > 1)
+ {
+ /* We have some paths, let's use them. Start after the URL. */
+ for (i = 1; i < targets->nelts; i++)
+ {
+ const char *target;
+
+ target = APR_ARRAY_IDX(targets, i, const char *);
+
+ if (svn_path_is_url(target) || svn_dirent_is_absolute(target))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a relative path"),
+ target);
+
+ APR_ARRAY_PUSH(*relative_targets, const char *) =
+ apr_pstrdup(result_pool, target);
+ }
+ }
+ else
+ {
+ /* If we have a single URL, then the session will be rooted at
+ it, so just send an empty string for the paths we are
+ interested in. */
+ APR_ARRAY_PUSH(*relative_targets, const char *) = "";
+ }
+
+ /* Remember that our targets are URLs. */
+ url_targets = TRUE;
+ }
+ else /* WC path target. */
+ {
+ const char *target;
+ const char *target_abspath;
+
+ url_targets = FALSE;
+ if (targets->nelts > 1)
+ return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("When specifying working copy paths, only "
+ "one target may be given"));
+
+ /* An unspecified PEG_REVISION for a working copy path defaults
+ to svn_opt_revision_working. */
+ if (peg_revision->kind == svn_opt_revision_unspecified)
+ peg_revision->kind = svn_opt_revision_working;
+
+ /* Get URLs for each target */
+ target = APR_ARRAY_IDX(targets, 0, const char *);
+
+ SVN_ERR(svn_dirent_get_absolute(&target_abspath, target, scratch_pool));
+ SVN_ERR(svn_wc__node_get_url(&url_or_path, ctx->wc_ctx, target_abspath,
+ scratch_pool, scratch_pool));
+ APR_ARRAY_PUSH(*relative_targets, const char *) = "";
+ }
+
+ /* If this is a revision type that requires access to the working copy,
+ * we use our initial target path to figure out where to root the RA
+ * session, otherwise we use our URL. */
+ if (SVN_CLIENT__REVKIND_NEEDS_WC(peg_revision->kind))
+ {
+ if (url_targets)
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("PREV, BASE, or COMMITTED revision "
+ "keywords are invalid for URL"));
+
+ else
+ SVN_ERR(svn_dirent_get_absolute(
+ ra_target, APR_ARRAY_IDX(targets, 0, const char *), result_pool));
+ }
+ else
+ {
+ *ra_target = apr_pstrdup(result_pool, url_or_path);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Keep track of oldest and youngest opt revs found.
+
+ If REV is younger than *YOUNGEST_REV, or *YOUNGEST_REV is
+ svn_opt_revision_unspecified, then set *YOUNGEST_REV equal to REV.
+
+ If REV is older than *OLDEST_REV, or *OLDEST_REV is
+ svn_opt_revision_unspecified, then set *OLDEST_REV equal to REV. */
+static void
+find_youngest_and_oldest_revs(svn_revnum_t *youngest_rev,
+ svn_revnum_t *oldest_rev,
+ svn_revnum_t rev)
+{
+ /* Is REV younger than YOUNGEST_REV? */
+ if (! SVN_IS_VALID_REVNUM(*youngest_rev)
+ || rev > *youngest_rev)
+ *youngest_rev = rev;
+
+ if (! SVN_IS_VALID_REVNUM(*oldest_rev)
+ || rev < *oldest_rev)
+ *oldest_rev = rev;
+}
+
+typedef struct rev_range_t
+{
+ svn_revnum_t range_start;
+ svn_revnum_t range_end;
+} rev_range_t;
+
+/* Convert array of svn_opt_revision_t ranges to an array of svn_revnum_t
+ ranges.
+
+ Given a log target URL_OR_ABSPATH@PEG_REV and an array of
+ svn_opt_revision_range_t's OPT_REV_RANGES, resolve the opt revs in
+ OPT_REV_RANGES to svn_revnum_t's and return these in *REVISION_RANGES, an
+ array of rev_range_t *.
+
+ Set *YOUNGEST_REV and *OLDEST_REV to the youngest and oldest revisions
+ found in *REVISION_RANGES.
+
+ If the repository needs to be contacted to resolve svn_opt_revision_date or
+ svn_opt_revision_head revisions, then the session used to do this is
+ RA_SESSION; it must be an open session to any URL in the right repository.
+*/
+static svn_error_t*
+convert_opt_rev_array_to_rev_range_array(
+ apr_array_header_t **revision_ranges,
+ svn_revnum_t *youngest_rev,
+ svn_revnum_t *oldest_rev,
+ svn_ra_session_t *ra_session,
+ const char *url_or_abspath,
+ const apr_array_header_t *opt_rev_ranges,
+ const svn_opt_revision_t *peg_rev,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ svn_revnum_t head_rev = SVN_INVALID_REVNUM;
+
+ /* Initialize the input/output parameters. */
+ *youngest_rev = *oldest_rev = SVN_INVALID_REVNUM;
+
+ /* Convert OPT_REV_RANGES to an array of rev_range_t and find the youngest
+ and oldest revision range that spans all of OPT_REV_RANGES. */
+ *revision_ranges = apr_array_make(result_pool, opt_rev_ranges->nelts,
+ sizeof(rev_range_t *));
+
+ for (i = 0; i < opt_rev_ranges->nelts; i++)
+ {
+ svn_opt_revision_range_t *range;
+ rev_range_t *rev_range;
+ svn_boolean_t start_same_as_end = FALSE;
+
+ range = APR_ARRAY_IDX(opt_rev_ranges, i, svn_opt_revision_range_t *);
+
+ /* Right now RANGE can be any valid pair of svn_opt_revision_t's. We
+ will now convert all RANGEs in place to the corresponding
+ svn_opt_revision_number kind. */
+ if ((range->start.kind != svn_opt_revision_unspecified)
+ && (range->end.kind == svn_opt_revision_unspecified))
+ {
+ /* If the user specified exactly one revision, then start rev is
+ * set but end is not. We show the log message for just that
+ * revision by making end equal to start.
+ *
+ * Note that if the user requested a single dated revision, then
+ * this will cause the same date to be resolved twice. The
+ * extra code complexity to get around this slight inefficiency
+ * doesn't seem worth it, however. */
+ range->end = range->start;
+ }
+ else if (range->start.kind == svn_opt_revision_unspecified)
+ {
+ /* Default to any specified peg revision. Otherwise, if the
+ * first target is a URL, then we default to HEAD:0. Lastly,
+ * the default is BASE:0 since WC@HEAD may not exist. */
+ if (peg_rev->kind == svn_opt_revision_unspecified)
+ {
+ if (svn_path_is_url(url_or_abspath))
+ range->start.kind = svn_opt_revision_head;
+ else
+ range->start.kind = svn_opt_revision_base;
+ }
+ else
+ range->start = *peg_rev;
+
+ if (range->end.kind == svn_opt_revision_unspecified)
+ {
+ range->end.kind = svn_opt_revision_number;
+ range->end.value.number = 0;
+ }
+ }
+
+ if ((range->start.kind == svn_opt_revision_unspecified)
+ || (range->end.kind == svn_opt_revision_unspecified))
+ {
+ return svn_error_create
+ (SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Missing required revision specification"));
+ }
+
+ /* Does RANGE describe a single svn_opt_revision_t? */
+ if (range->start.kind == range->end.kind)
+ {
+ if (range->start.kind == svn_opt_revision_number)
+ {
+ if (range->start.value.number == range->end.value.number)
+ start_same_as_end = TRUE;
+ }
+ else if (range->start.kind == svn_opt_revision_date)
+ {
+ if (range->start.value.date == range->end.value.date)
+ start_same_as_end = TRUE;
+ }
+ else
+ {
+ start_same_as_end = TRUE;
+ }
+ }
+
+ rev_range = apr_palloc(result_pool, sizeof(*rev_range));
+ SVN_ERR(svn_client__get_revision_number(
+ &rev_range->range_start, &head_rev,
+ ctx->wc_ctx, url_or_abspath, ra_session,
+ &range->start, scratch_pool));
+ if (start_same_as_end)
+ rev_range->range_end = rev_range->range_start;
+ else
+ SVN_ERR(svn_client__get_revision_number(
+ &rev_range->range_end, &head_rev,
+ ctx->wc_ctx, url_or_abspath, ra_session,
+ &range->end, scratch_pool));
+
+ /* Possibly update the oldest and youngest revisions requested. */
+ find_youngest_and_oldest_revs(youngest_rev,
+ oldest_rev,
+ rev_range->range_start);
+ find_youngest_and_oldest_revs(youngest_rev,
+ oldest_rev,
+ rev_range->range_end);
+ APR_ARRAY_PUSH(*revision_ranges, rev_range_t *) = rev_range;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+static int
+compare_rev_to_segment(const void *key_p,
+ const void *element_p)
+{
+ svn_revnum_t rev =
+ * (svn_revnum_t *)key_p;
+ const svn_location_segment_t *segment =
+ *((const svn_location_segment_t * const *) element_p);
+
+ if (rev < segment->range_start)
+ return -1;
+ else if (rev > segment->range_end)
+ return 1;
+ else
+ return 0;
+}
+
+/* Run svn_ra_get_log2 for PATHS, one or more paths relative to RA_SESSION's
+ common parent, for each revision in REVISION_RANGES, an array of
+ rev_range_t.
+
+ RA_SESSION is an open session pointing to ACTUAL_LOC.
+
+ LOG_SEGMENTS is an array of svn_location_segment_t * items representing the
+ history of PATHS from the oldest to youngest revisions found in
+ REVISION_RANGES.
+
+ The TARGETS, LIMIT, DISCOVER_CHANGED_PATHS, STRICT_NODE_HISTORY,
+ INCLUDE_MERGED_REVISIONS, REVPROPS, REAL_RECEIVER, and REAL_RECEIVER_BATON
+ parameters are all as per the svn_client_log5 API. */
+static svn_error_t *
+run_ra_get_log(apr_array_header_t *revision_ranges,
+ apr_array_header_t *paths,
+ apr_array_header_t *log_segments,
+ svn_client__pathrev_t *actual_loc,
+ svn_ra_session_t *ra_session,
+ /* The following are as per svn_client_log5. */
+ const apr_array_header_t *targets,
+ int limit,
+ svn_boolean_t discover_changed_paths,
+ svn_boolean_t strict_node_history,
+ svn_boolean_t include_merged_revisions,
+ const apr_array_header_t *revprops,
+ svn_log_entry_receiver_t real_receiver,
+ void *real_receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ pre_15_receiver_baton_t rb = {0};
+ apr_pool_t *iterpool;
+ svn_boolean_t has_log_revprops;
+
+ SVN_ERR(svn_ra_has_capability(ra_session, &has_log_revprops,
+ SVN_RA_CAPABILITY_LOG_REVPROPS,
+ scratch_pool));
+
+ if (!has_log_revprops)
+ {
+ /* See above pre-1.5 notes. */
+ rb.ctx = ctx;
+
+ /* Create ra session on first use */
+ rb.ra_session_pool = scratch_pool;
+ rb.ra_session_url = actual_loc->url;
+ }
+
+ /* It's a bit complex to correctly handle the special revision words
+ * such as "BASE", "COMMITTED", and "PREV". For example, if the
+ * user runs
+ *
+ * $ svn log -rCOMMITTED foo.txt bar.c
+ *
+ * which committed rev should be used? The younger of the two? The
+ * first one? Should we just error?
+ *
+ * None of the above, I think. Rather, the committed rev of each
+ * target in turn should be used. This is what most users would
+ * expect, and is the most useful interpretation. Of course, this
+ * goes for the other dynamic (i.e., local) revision words too.
+ *
+ * Note that the code to do this is a bit more complex than a simple
+ * loop, because the user might run
+ *
+ * $ svn log -rCOMMITTED:42 foo.txt bar.c
+ *
+ * in which case we want to avoid recomputing the static revision on
+ * every iteration.
+ *
+ * ### FIXME: However, we can't yet handle multiple wc targets anyway.
+ *
+ * We used to iterate over each target in turn, getting the logs for
+ * the named range. This led to revisions being printed in strange
+ * order or being printed more than once. This is issue 1550.
+ *
+ * In r851673, jpieper blocked multiple wc targets in svn/log-cmd.c,
+ * meaning this block not only doesn't work right in that case, but isn't
+ * even testable that way (svn has no unit test suite; we can only test
+ * via the svn command). So, that check is now moved into this function
+ * (see above).
+ *
+ * kfogel ponders future enhancements in r844260:
+ * I think that's okay behavior, since the sense of the command is
+ * that one wants a particular range of logs for *this* file, then
+ * another range for *that* file, and so on. But we should
+ * probably put some sort of separator header between the log
+ * groups. Of course, libsvn_client can't just print stuff out --
+ * it has to take a callback from the client to do that. So we
+ * need to define that callback interface, then have the command
+ * line client pass one down here.
+ *
+ * epg wonders if the repository could send a unified stream of log
+ * entries if the paths and revisions were passed down.
+ */
+ iterpool = svn_pool_create(scratch_pool);
+ for (i = 0; i < revision_ranges->nelts; i++)
+ {
+ const char *old_session_url;
+ const char *path = APR_ARRAY_IDX(targets, 0, const char *);
+ const char *local_abspath_or_url;
+ rev_range_t *range;
+ limit_receiver_baton_t lb;
+ svn_log_entry_receiver_t passed_receiver;
+ void *passed_receiver_baton;
+ const apr_array_header_t *passed_receiver_revprops;
+ svn_location_segment_t **matching_segment;
+ svn_revnum_t younger_rev;
+
+ svn_pool_clear(iterpool);
+
+ if (!svn_path_is_url(path))
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath_or_url, path,
+ iterpool));
+ else
+ local_abspath_or_url = path;
+
+ range = APR_ARRAY_IDX(revision_ranges, i, rev_range_t *);
+
+ /* Issue #4355: Account for renames spanning requested
+ revision ranges. */
+ younger_rev = MAX(range->range_start, range->range_end);
+ matching_segment = bsearch(&younger_rev, log_segments->elts,
+ log_segments->nelts, log_segments->elt_size,
+ compare_rev_to_segment);
+ SVN_ERR_ASSERT(*matching_segment);
+
+ /* A segment with a NULL path means there is gap in the history.
+ We'll just proceed and let svn_ra_get_log2 fail with a useful
+ error...*/
+ if ((*matching_segment)->path != NULL)
+ {
+ /* ...but if there is history, then we must account for issue
+ #4355 and make sure our RA session is pointing at the correct
+ location. */
+ const char *segment_url = svn_path_url_add_component2(
+ actual_loc->repos_root_url, (*matching_segment)->path,
+ scratch_pool);
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url,
+ ra_session,
+ segment_url,
+ scratch_pool));
+ }
+
+ if (has_log_revprops)
+ {
+ passed_receiver = real_receiver;
+ passed_receiver_baton = real_receiver_baton;
+ passed_receiver_revprops = revprops;
+ }
+ else
+ {
+ rb.revprops = revprops;
+ rb.receiver = real_receiver;
+ rb.baton = real_receiver_baton;
+
+ passed_receiver = pre_15_receiver;
+ passed_receiver_baton = &rb;
+ passed_receiver_revprops = svn_compat_log_revprops_in(iterpool);
+ }
+
+ if (limit && revision_ranges->nelts > 1)
+ {
+ lb.limit = limit;
+ lb.receiver = passed_receiver;
+ lb.baton = passed_receiver_baton;
+
+ passed_receiver = limit_receiver;
+ passed_receiver_baton = &lb;
+ }
+
+ SVN_ERR(svn_ra_get_log2(ra_session,
+ paths,
+ range->range_start,
+ range->range_end,
+ limit,
+ discover_changed_paths,
+ strict_node_history,
+ include_merged_revisions,
+ passed_receiver_revprops,
+ passed_receiver,
+ passed_receiver_baton,
+ iterpool));
+
+ if (limit && revision_ranges->nelts > 1)
+ {
+ limit = lb.limit;
+ if (limit == 0)
+ {
+ return SVN_NO_ERROR;
+ }
+ }
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/*** Public Interface. ***/
+
+svn_error_t *
+svn_client_log5(const apr_array_header_t *targets,
+ const svn_opt_revision_t *peg_revision,
+ const apr_array_header_t *opt_rev_ranges,
+ int limit,
+ svn_boolean_t discover_changed_paths,
+ svn_boolean_t strict_node_history,
+ svn_boolean_t include_merged_revisions,
+ const apr_array_header_t *revprops,
+ svn_log_entry_receiver_t real_receiver,
+ void *real_receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *ra_session;
+ const char *old_session_url;
+ const char *ra_target;
+ svn_opt_revision_t youngest_opt_rev;
+ svn_revnum_t youngest_rev;
+ svn_revnum_t oldest_rev;
+ svn_opt_revision_t peg_rev;
+ svn_client__pathrev_t *actual_loc;
+ apr_array_header_t *log_segments;
+ apr_array_header_t *revision_ranges;
+ apr_array_header_t *relative_targets;
+
+ if (opt_rev_ranges->nelts == 0)
+ {
+ return svn_error_create
+ (SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Missing required revision specification"));
+ }
+
+ /* Make a copy of PEG_REVISION, we may need to change it to a
+ default value. */
+ peg_rev = *peg_revision;
+
+ SVN_ERR(resolve_log_targets(&relative_targets, &ra_target, &peg_rev,
+ targets, ctx, pool, pool));
+
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &actual_loc,
+ ra_target, NULL, &peg_rev, &peg_rev,
+ ctx, pool));
+
+ /* Convert OPT_REV_RANGES to an array of rev_range_t and find the youngest
+ and oldest revision range that spans all of OPT_REV_RANGES. */
+ SVN_ERR(convert_opt_rev_array_to_rev_range_array(&revision_ranges,
+ &youngest_rev,
+ &oldest_rev,
+ ra_session,
+ ra_target,
+ opt_rev_ranges, &peg_rev,
+ ctx, pool, pool));
+
+ /* Make ACTUAL_LOC and RA_SESSION point to the youngest operative rev. */
+ youngest_opt_rev.kind = svn_opt_revision_number;
+ youngest_opt_rev.value.number = youngest_rev;
+ SVN_ERR(svn_client__resolve_rev_and_url(&actual_loc, ra_session,
+ ra_target, &peg_rev,
+ &youngest_opt_rev, ctx, pool));
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session,
+ actual_loc->url, pool));
+
+ /* Get the svn_location_segment_t's representing the requested log ranges. */
+ SVN_ERR(svn_client__repos_location_segments(&log_segments, ra_session,
+ actual_loc->url,
+ actual_loc->rev, /* peg */
+ actual_loc->rev, /* start */
+ oldest_rev, /* end */
+ ctx, pool));
+
+ SVN_ERR(run_ra_get_log(revision_ranges, relative_targets, log_segments,
+ actual_loc, ra_session, targets, limit,
+ discover_changed_paths, strict_node_history,
+ include_merged_revisions, revprops, real_receiver,
+ real_receiver_baton, ctx, pool));
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/merge.c b/subversion/libsvn_client/merge.c
new file mode 100644
index 0000000..884d63d
--- /dev/null
+++ b/subversion/libsvn_client/merge.c
@@ -0,0 +1,12674 @@
+/*
+ * merge.c: merging
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes ***/
+
+#include <assert.h>
+#include <apr_strings.h>
+#include <apr_tables.h>
+#include <apr_hash.h>
+#include "svn_types.h"
+#include "svn_hash.h"
+#include "svn_wc.h"
+#include "svn_delta.h"
+#include "svn_diff.h"
+#include "svn_mergeinfo.h"
+#include "svn_client.h"
+#include "svn_string.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_io.h"
+#include "svn_utf.h"
+#include "svn_pools.h"
+#include "svn_config.h"
+#include "svn_props.h"
+#include "svn_time.h"
+#include "svn_sorts.h"
+#include "svn_subst.h"
+#include "svn_ra.h"
+#include "client.h"
+#include "mergeinfo.h"
+
+#include "private/svn_opt_private.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_mergeinfo_private.h"
+#include "private/svn_fspath.h"
+#include "private/svn_ra_private.h"
+#include "private/svn_client_private.h"
+#include "private/svn_subr_private.h"
+
+#include "svn_private_config.h"
+
+
+/*-----------------------------------------------------------------------*/
+
+/* MERGEINFO MERGE SOURCE NORMALIZATION
+ *
+ * Nearly any helper function herein that accepts two URL/revision
+ * pairs (or equivalent struct merge_source_t) expects one of two things
+ * to be true:
+ *
+ * 1. that mergeinfo is not being recorded at all for this
+ * operation, or
+ *
+ * 2. that the pairs represent two locations along a single line
+ * of version history such that there are no copies in the
+ * history of the object between the locations when treating
+ * the oldest of the two locations as non-inclusive. In other
+ * words, if there is a copy at all between them, there is only
+ * one copy and its source was the oldest of the two locations.
+ *
+ * We use svn_ra_get_location_segments() to split a given range of
+ * revisions across an object's history into several which obey these
+ * rules. For example, an extract from the log of Subversion's own
+ * /subversion/tags/1.4.5 directory shows the following copies between
+ * r859500 and r866500 (omitting the '/subversion' prefix for clarity):
+ *
+ * r859598:
+ * A /branches/1.4.x (from /trunk:859597)
+ *
+ * r865417:
+ * A /tags/1.4.4 (from /branches/1.4.x:865262)
+ * # Notice that this copy leaves a gap between 865262 and 865417.
+ *
+ * r866420:
+ * A /branches/1.4.5 (from /tags/1.4.4:866419)
+ *
+ * r866425:
+ * D /branches/1.4.5
+ * A /tags/1.4.5 (from /branches/1.4.5:866424)
+ *
+ * In graphical form:
+ *
+ * 859500 859597 865262 866419 866424 866500
+ * . . . . . .
+ * trunk ------------------------------------------------
+ * \ . . .
+ * branches/1.4.x A-------------------------------------
+ * . \______ . .
+ * . \ . .
+ * tags/1.4.4 . A-----------------------
+ * . . \ .
+ * branches/1.4.5 . . A------D
+ * . . . \.
+ * tags/1.4.5 . . . A---------
+ * . . . .
+ * 859598 865417 866420 866425
+ *
+ * A merge of the difference between r859500 and r866500 of this directory
+ * gets split into sequential merges of the following location pairs.
+ *
+ * 859500 859597 865262 865416 866419 866424 866500
+ * . . . . . . .
+ * trunk (======] . . . . .
+ * . . . . .
+ * trunk ( . . . . .
+ * branches/1.4.x ======] . . . .
+ * . . . .
+ * branches/1.4.x ( . . . .
+ * tags/1.4.4 =============] . .
+ * implicit_src_gap (======] . . .
+ * . . .
+ * tags/1.4.4 ( . .
+ * branches/1.4.5 ======] .
+ * . .
+ * branches/1.4.5 ( .
+ * tags/1.4.5 ======]
+ *
+ * which are represented in merge_source_t as:
+ *
+ * [/trunk:859500, /trunk:859597]
+ * (recorded in svn:mergeinfo as /trunk:859501-859597)
+ *
+ * [/trunk:859597, /branches/1.4.x:865262]
+ * (recorded in svn:mergeinfo as /branches/1.4.x:859598-865262)
+ *
+ * [/branches/1.4.x:865262, /tags/1.4.4@866419]
+ * (recorded in svn:mergeinfo as /tags/1.4.4:865263-866419)
+ * (and there is a gap, the revision range [865262, 865416])
+ *
+ * [/tags/1.4.4@866419, /branches/1.4.5@866424]
+ * (recorded in svn:mergeinfo as /branches/1.4.5:866420-866424)
+ *
+ * [/branches/1.4.5@866424, /tags/1.4.5@866500]
+ * (recorded in svn:mergeinfo as /tags/1.4.5:866425-866500)
+ *
+ * Our helper functions would then operate on one of these location
+ * pairs at a time.
+ */
+
+/* WHICH SVN_CLIENT_MERGE* API DO I WANT?
+ *
+ * libsvn_client has three public merge APIs; they are all wrappers
+ * around the do_merge engine. Which one to use depends on the number
+ * of URLs passed as arguments and whether or not specific merge
+ * ranges (-c/-r) are specified.
+ *
+ * 1 URL 2 URLs
+ * +----+--------------------------------+---------------------+
+ * | -c | mergeinfo-driven | |
+ * | or | cherrypicking | |
+ * | -r | (svn_client_merge_peg) | |
+ * |----+--------------------------------+ |
+ * | | mergeinfo-driven | unsupported |
+ * | | 'cherry harvest', i.e. merge | |
+ * | | all revisions from URL that | |
+ * | no | have not already been merged | |
+ * | -c | (svn_client_merge_peg) | |
+ * | or +--------------------------------+---------------------+
+ * | -r | mergeinfo-driven | mergeinfo-writing |
+ * | | whole-branch | diff-and-apply |
+ * | | heuristic merge | (svn_client_merge) |
+ * | | (svn_client_merge_reintegrate) | |
+ * +----+--------------------------------+---------------------+
+ *
+ *
+ */
+
+/* THE CHILDREN_WITH_MERGEINFO ARRAY
+ *
+ * Many of the helper functions in this file pass around an
+ * apr_array_header_t *CHILDREN_WITH_MERGEINFO. This is a depth first
+ * sorted array filled with svn_client__merge_path_t * describing the
+ * merge target and any of its subtrees which have explicit mergeinfo
+ * or otherwise need special attention during a merge.
+ *
+ * During mergeinfo unaware merges, CHILDREN_WITH_MERGEINFO contains
+ * contains only one element (added by do_mergeinfo_unaware_dir_merge)
+ * describing a contiguous range to be merged to the WC merge target.
+ *
+ * During mergeinfo aware merges CHILDREN_WITH_MERGEINFO is created
+ * by get_mergeinfo_paths() and outside of that function and its helpers
+ * should always meet the criteria dictated in get_mergeinfo_paths()'s doc
+ * string. The elements of CHILDREN_WITH_MERGEINFO should never be NULL.
+ *
+ * For clarification on mergeinfo aware vs. mergeinfo unaware merges, see
+ * the doc string for HONOR_MERGEINFO().
+ */
+
+
+/*-----------------------------------------------------------------------*/
+
+/*** Repos-Diff Editor Callbacks ***/
+
+/* */
+typedef struct merge_source_t
+{
+ /* "left" side URL and revision (inclusive iff youngest) */
+ const svn_client__pathrev_t *loc1;
+
+ /* "right" side URL and revision (inclusive iff youngest) */
+ const svn_client__pathrev_t *loc2;
+
+ /* True iff LOC1 is an ancestor of LOC2 or vice-versa (history-wise). */
+ svn_boolean_t ancestral;
+} merge_source_t;
+
+/* Description of the merge target root node (a WC working node) */
+typedef struct merge_target_t
+{
+ /* Absolute path to the WC node */
+ const char *abspath;
+
+ /* The repository location of the base node of the target WC. If the node
+ * is locally added, then URL & REV are NULL & SVN_INVALID_REVNUM.
+ * REPOS_ROOT_URL and REPOS_UUID are always valid. */
+ svn_client__pathrev_t loc;
+
+} merge_target_t;
+
+typedef struct merge_cmd_baton_t {
+ svn_boolean_t force_delete; /* Delete a file/dir even if modified */
+ svn_boolean_t dry_run;
+ svn_boolean_t record_only; /* Whether to merge only mergeinfo
+ differences. */
+ svn_boolean_t same_repos; /* Whether the merge source repository
+ is the same repository as the
+ target. Defaults to FALSE if DRY_RUN
+ is TRUE.*/
+ svn_boolean_t mergeinfo_capable; /* Whether the merge source server
+ is capable of Merge Tracking. */
+ svn_boolean_t ignore_mergeinfo; /* Don't honor mergeinfo; see
+ doc string of do_merge(). FALSE if
+ MERGE_SOURCE->ancestral is FALSE. */
+ svn_boolean_t diff_ignore_ancestry; /* Diff unrelated nodes as if related; see
+ doc string of do_merge(). FALSE if
+ MERGE_SOURCE->ancestral is FALSE. */
+ svn_boolean_t reintegrate_merge; /* Whether this is a --reintegrate
+ merge or not. */
+ const merge_target_t *target; /* Description of merge target node */
+
+ /* The left and right URLs and revs. The value of this field changes to
+ reflect the merge_source_t *currently* being merged by do_merge(). */
+ merge_source_t merge_source;
+
+ /* Rangelist containing single range which describes the gap, if any,
+ in the natural history of the merge source currently being processed.
+ See http://subversion.tigris.org/issues/show_bug.cgi?id=3432.
+ Updated during each call to do_directory_merge(). May be NULL if there
+ is no gap. */
+ svn_rangelist_t *implicit_src_gap;
+
+ svn_client_ctx_t *ctx; /* Client context for callbacks, etc. */
+
+ /* The list of any paths which remained in conflict after a
+ resolution attempt was made. We track this in-memory, rather
+ than just using WC entry state, since the latter doesn't help us
+ when in dry_run mode.
+ ### And because we only want to resolve conflicts that were
+ generated by this merge, not pre-existing ones? */
+ apr_hash_t *conflicted_paths;
+
+ /* A list of absolute paths which had no explicit mergeinfo prior to the
+ merge but got explicit mergeinfo added by the merge. This is populated
+ by merge_change_props() and is allocated in POOL so it is subject to the
+ lifetime limitations of POOL. Is NULL if no paths are found which
+ meet the criteria or DRY_RUN is true. */
+ apr_hash_t *paths_with_new_mergeinfo;
+
+ /* A list of absolute paths whose mergeinfo doesn't need updating after
+ the merge. This can be caused by the removal of mergeinfo by the merge
+ or by deleting the node itself. This is populated by merge_change_props()
+ and the delete callbacks and is allocated in POOL so it is subject to the
+ lifetime limitations of POOL. Is NULL if no paths are found which
+ meet the criteria or DRY_RUN is true. */
+ apr_hash_t *paths_with_deleted_mergeinfo;
+
+ /* The list of absolute skipped paths, which should be examined and
+ cleared after each invocation of the callback. The paths
+ are absolute. Is NULL if MERGE_B->MERGE_SOURCE->ancestral and
+ MERGE_B->REINTEGRATE_MERGE are both false. */
+ apr_hash_t *skipped_abspaths;
+
+ /* The list of absolute merged paths. Unused if MERGE_B->MERGE_SOURCE->ancestral
+ and MERGE_B->REINTEGRATE_MERGE are both false. */
+ apr_hash_t *merged_abspaths;
+
+ /* A hash of (const char *) absolute WC paths mapped to the same which
+ represent the roots of subtrees added by the merge. */
+ apr_hash_t *added_abspaths;
+
+ /* A list of tree conflict victim absolute paths which may be NULL. */
+ apr_hash_t *tree_conflicted_abspaths;
+
+ /* The diff3_cmd in ctx->config, if any, else null. We could just
+ extract this as needed, but since more than one caller uses it,
+ we just set it up when this baton is created. */
+ const char *diff3_cmd;
+ const apr_array_header_t *merge_options;
+
+ /* RA sessions used throughout a merge operation. Opened/re-parented
+ as needed.
+
+ NOTE: During the actual merge editor drive, RA_SESSION1 is used
+ for the primary editing and RA_SESSION2 for fetching additional
+ information -- as necessary -- from the repository. So during
+ this phase of the merge, you *must not* reparent RA_SESSION1; use
+ (temporarily reparenting if you must) RA_SESSION2 instead. */
+ svn_ra_session_t *ra_session1;
+ svn_ra_session_t *ra_session2;
+
+ /* During the merge, *USE_SLEEP is set to TRUE if a sleep will be required
+ afterwards to ensure timestamp integrity, or unchanged if not. */
+ svn_boolean_t *use_sleep;
+
+ /* Pool which has a lifetime limited to one iteration over a given
+ merge source, i.e. it is cleared on every call to do_directory_merge()
+ or do_file_merge() in do_merge(). */
+ apr_pool_t *pool;
+
+
+ /* State for notify_merge_begin() */
+ struct notify_begin_state_t
+ {
+ /* Cache of which abspath was last notified. */
+ const char *last_abspath;
+
+ /* Reference to the one-and-only CHILDREN_WITH_MERGEINFO (see global
+ comment) or a similar list for single-file-merges */
+ const apr_array_header_t *nodes_with_mergeinfo;
+ } notify_begin;
+
+} merge_cmd_baton_t;
+
+
+/* Return TRUE iff we should be taking account of mergeinfo in deciding what
+ changes to merge, for the merge described by MERGE_B. Specifically, that
+ is if the merge source server is capable of merge tracking, the left-side
+ merge source is an ancestor of the right-side (or vice-versa), the merge
+ source is in the same repository as the merge target, and we are not
+ ignoring mergeinfo. */
+#define HONOR_MERGEINFO(merge_b) ((merge_b)->mergeinfo_capable \
+ && (merge_b)->merge_source.ancestral \
+ && (merge_b)->same_repos \
+ && (! (merge_b)->ignore_mergeinfo))
+
+
+/* Return TRUE iff we should be recording mergeinfo for the merge described
+ by MERGE_B. Specifically, that is if we are honoring mergeinfo and the
+ merge is not a dry run. */
+#define RECORD_MERGEINFO(merge_b) (HONOR_MERGEINFO(merge_b) \
+ && !(merge_b)->dry_run)
+
+
+/*-----------------------------------------------------------------------*/
+
+/*** Utilities ***/
+
+/* Return TRUE iff the session URL of RA_SESSION is equal to URL. Useful in
+ * asserting preconditions. */
+static svn_boolean_t
+session_url_is(svn_ra_session_t *ra_session,
+ const char *url,
+ apr_pool_t *scratch_pool)
+{
+ const char *session_url;
+ svn_error_t *err
+ = svn_ra_get_session_url(ra_session, &session_url, scratch_pool);
+
+ SVN_ERR_ASSERT_NO_RETURN(! err);
+ return strcmp(url, session_url) == 0;
+}
+
+/* Return a new merge_source_t structure, allocated in RESULT_POOL,
+ * initialized with deep copies of LOC1 and LOC2 and ANCESTRAL. */
+static merge_source_t *
+merge_source_create(const svn_client__pathrev_t *loc1,
+ const svn_client__pathrev_t *loc2,
+ svn_boolean_t ancestral,
+ apr_pool_t *result_pool)
+{
+ merge_source_t *s
+ = apr_palloc(result_pool, sizeof(*s));
+
+ s->loc1 = svn_client__pathrev_dup(loc1, result_pool);
+ s->loc2 = svn_client__pathrev_dup(loc2, result_pool);
+ s->ancestral = ancestral;
+ return s;
+}
+
+/* Return a deep copy of SOURCE, allocated in RESULT_POOL. */
+static merge_source_t *
+merge_source_dup(const merge_source_t *source,
+ apr_pool_t *result_pool)
+{
+ merge_source_t *s = apr_palloc(result_pool, sizeof(*s));
+
+ s->loc1 = svn_client__pathrev_dup(source->loc1, result_pool);
+ s->loc2 = svn_client__pathrev_dup(source->loc2, result_pool);
+ s->ancestral = source->ancestral;
+ return s;
+}
+
+/* Return SVN_ERR_UNSUPPORTED_FEATURE if URL is not inside the repository
+ of LOCAL_ABSPATH. Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+check_repos_match(const merge_target_t *target,
+ const char *local_abspath,
+ const char *url,
+ apr_pool_t *scratch_pool)
+{
+ if (!svn_uri__is_ancestor(target->loc.repos_root_url, url))
+ return svn_error_createf(
+ SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("URL '%s' of '%s' is not in repository '%s'"),
+ url, svn_dirent_local_style(local_abspath, scratch_pool),
+ target->loc.repos_root_url);
+
+ return SVN_NO_ERROR;
+}
+
+/* Return TRUE iff the repository of LOCATION1 is the same as
+ * that of LOCATION2. If STRICT_URLS is true, the URLs must
+ * match (and the UUIDs, just to be sure), otherwise just the UUIDs must
+ * match and the URLs can differ (a common case is http versus https). */
+static svn_boolean_t
+is_same_repos(const svn_client__pathrev_t *location1,
+ const svn_client__pathrev_t *location2,
+ svn_boolean_t strict_urls)
+{
+ if (strict_urls)
+ return (strcmp(location1->repos_root_url, location2->repos_root_url) == 0
+ && strcmp(location1->repos_uuid, location2->repos_uuid) == 0);
+ else
+ return (strcmp(location1->repos_uuid, location2->repos_uuid) == 0);
+}
+
+/* If the repository identified of LOCATION1 is not the same as that
+ * of LOCATION2, throw a SVN_ERR_CLIENT_UNRELATED_RESOURCES
+ * error mentioning PATH1 and PATH2. For STRICT_URLS, see is_same_repos().
+ */
+static svn_error_t *
+check_same_repos(const svn_client__pathrev_t *location1,
+ const char *path1,
+ const svn_client__pathrev_t *location2,
+ const char *path2,
+ svn_boolean_t strict_urls,
+ apr_pool_t *scratch_pool)
+{
+ if (! is_same_repos(location1, location2, strict_urls))
+ return svn_error_createf(SVN_ERR_CLIENT_UNRELATED_RESOURCES, NULL,
+ _("'%s' must be from the same repository as "
+ "'%s'"), path1, path2);
+ return SVN_NO_ERROR;
+}
+
+/* Store LOCAL_ABSPATH in PATH_HASH after duplicating it into the pool
+ containing PATH_HASH. */
+static APR_INLINE void
+store_path(apr_hash_t *path_hash, const char *local_abspath)
+{
+ const char *dup_path = apr_pstrdup(apr_hash_pool_get(path_hash),
+ local_abspath);
+
+ svn_hash_sets(path_hash, dup_path, dup_path);
+}
+
+/* Store LOCAL_ABSPATH in *PATH_HASH_P after duplicating it into the pool
+ containing *PATH_HASH_P. If *PATH_HASH_P is NULL, then first set
+ *PATH_HASH_P to a new hash allocated from POOL. */
+static APR_INLINE void
+alloc_and_store_path(apr_hash_t **path_hash_p,
+ const char *local_abspath,
+ apr_pool_t *pool)
+{
+ if (! *path_hash_p)
+ *path_hash_p = apr_hash_make(pool);
+ store_path(*path_hash_p, local_abspath);
+}
+
+/* Return whether any WC path was put in conflict by the merge
+ operation corresponding to MERGE_B. */
+static APR_INLINE svn_boolean_t
+is_path_conflicted_by_merge(merge_cmd_baton_t *merge_b)
+{
+ return (merge_b->conflicted_paths &&
+ apr_hash_count(merge_b->conflicted_paths) > 0);
+}
+
+/* Return a state indicating whether the WC metadata matches the
+ * node kind on disk of the local path LOCAL_ABSPATH.
+ * Use MERGE_B to determine the dry-run details; particularly, if a dry run
+ * noted that it deleted this path, assume matching node kinds (as if both
+ * kinds were svn_node_none).
+ *
+ * - Return svn_wc_notify_state_inapplicable if the node kind matches.
+ * - Return 'obstructed' if there is a node on disk where none or a
+ * different kind is expected, or if the disk node cannot be read.
+ * - Return 'missing' if there is no node on disk but one is expected.
+ * Also return 'missing' for server-excluded nodes (not here due to
+ * authz or other reasons determined by the server).
+ *
+ * Optionally return a bit more info for interested users.
+ **/
+static svn_error_t *
+perform_obstruction_check(svn_wc_notify_state_t *obstruction_state,
+ svn_boolean_t *deleted,
+ svn_boolean_t *excluded,
+ svn_node_kind_t *kind,
+ svn_depth_t *parent_depth,
+ const merge_cmd_baton_t *merge_b,
+ const char *local_abspath,
+ apr_pool_t *scratch_pool)
+{
+ svn_wc_context_t *wc_ctx = merge_b->ctx->wc_ctx;
+ svn_node_kind_t wc_kind;
+ svn_boolean_t check_root;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ *obstruction_state = svn_wc_notify_state_inapplicable;
+
+ if (deleted)
+ *deleted = FALSE;
+ if (kind)
+ *kind = svn_node_none;
+
+ if (kind == NULL)
+ kind = &wc_kind;
+
+ check_root = ! strcmp(local_abspath, merge_b->target->abspath);
+
+ SVN_ERR(svn_wc__check_for_obstructions(obstruction_state,
+ kind,
+ deleted,
+ excluded,
+ parent_depth,
+ wc_ctx, local_abspath,
+ check_root,
+ scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* Create *LEFT and *RIGHT conflict versions for conflict victim
+ * at VICTIM_ABSPATH, with kind NODE_KIND, using information obtained
+ * from MERGE_SOURCE and TARGET.
+ * Allocate returned conflict versions in RESULT_POOL. */
+static svn_error_t *
+make_conflict_versions(const svn_wc_conflict_version_t **left,
+ const svn_wc_conflict_version_t **right,
+ const char *victim_abspath,
+ svn_node_kind_t node_kind,
+ const merge_source_t *merge_source,
+ const merge_target_t *target,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *child = svn_dirent_skip_ancestor(target->abspath,
+ victim_abspath);
+ const char *left_relpath, *right_relpath;
+
+ SVN_ERR_ASSERT(child != NULL);
+ left_relpath = svn_client__pathrev_relpath(merge_source->loc1,
+ scratch_pool);
+ right_relpath = svn_client__pathrev_relpath(merge_source->loc2,
+ scratch_pool);
+
+ *left = svn_wc_conflict_version_create2(
+ merge_source->loc1->repos_root_url,
+ merge_source->loc1->repos_uuid,
+ svn_relpath_join(left_relpath, child, scratch_pool),
+ merge_source->loc1->rev, node_kind, result_pool);
+
+ *right = svn_wc_conflict_version_create2(
+ merge_source->loc2->repos_root_url,
+ merge_source->loc2->repos_uuid,
+ svn_relpath_join(right_relpath, child, scratch_pool),
+ merge_source->loc2->rev, node_kind, result_pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for filter_self_referential_mergeinfo()
+
+ *MERGEINFO is a non-empty, non-null collection of mergeinfo.
+
+ Remove all mergeinfo from *MERGEINFO that describes revision ranges
+ greater than REVISION. Put a copy of any removed mergeinfo, allocated
+ in POOL, into *YOUNGER_MERGEINFO.
+
+ If no mergeinfo is removed from *MERGEINFO then *YOUNGER_MERGEINFO is set
+ to NULL. If all mergeinfo is removed from *MERGEINFO then *MERGEINFO is
+ set to NULL.
+ */
+static svn_error_t*
+split_mergeinfo_on_revision(svn_mergeinfo_t *younger_mergeinfo,
+ svn_mergeinfo_t *mergeinfo,
+ svn_revnum_t revision,
+ apr_pool_t *pool)
+{
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ *younger_mergeinfo = NULL;
+ for (hi = apr_hash_first(pool, *mergeinfo); hi; hi = apr_hash_next(hi))
+ {
+ int i;
+ const char *merge_source_path = svn__apr_hash_index_key(hi);
+ svn_rangelist_t *rangelist = svn__apr_hash_index_val(hi);
+
+ svn_pool_clear(iterpool);
+
+ for (i = 0; i < rangelist->nelts; i++)
+ {
+ svn_merge_range_t *range =
+ APR_ARRAY_IDX(rangelist, i, svn_merge_range_t *);
+ if (range->end <= revision)
+ {
+ /* This entirely of this range is as old or older than
+ REVISION, so leave it in *MERGEINFO. */
+ continue;
+ }
+ else
+ {
+ /* Since the rangelists in svn_mergeinfo_t's are sorted in
+ increasing order we know that part or all of *this* range
+ and *all* of the remaining ranges in *RANGELIST are younger
+ than REVISION. Remove the younger rangelists from
+ *MERGEINFO and put them in *YOUNGER_MERGEINFO. */
+ int j;
+ svn_rangelist_t *younger_rangelist =
+ apr_array_make(pool, 1, sizeof(svn_merge_range_t *));
+
+ for (j = i; j < rangelist->nelts; j++)
+ {
+ svn_merge_range_t *younger_range = svn_merge_range_dup(
+ APR_ARRAY_IDX(rangelist, j, svn_merge_range_t *), pool);
+
+ /* REVISION might intersect with the first range where
+ range->end > REVISION. If that is the case then split
+ the current range into two, putting the younger half
+ into *YOUNGER_MERGEINFO and leaving the older half in
+ *MERGEINFO. */
+ if (j == i && range->start + 1 <= revision)
+ younger_range->start = range->end = revision;
+
+ APR_ARRAY_PUSH(younger_rangelist, svn_merge_range_t *) =
+ younger_range;
+ }
+
+ /* So far we've only been manipulating rangelists, now we
+ actually create *YOUNGER_MERGEINFO and then remove the older
+ ranges from *MERGEINFO */
+ if (!(*younger_mergeinfo))
+ *younger_mergeinfo = apr_hash_make(pool);
+ svn_hash_sets(*younger_mergeinfo, merge_source_path,
+ younger_rangelist);
+ SVN_ERR(svn_mergeinfo_remove2(mergeinfo, *younger_mergeinfo,
+ *mergeinfo, TRUE, pool, iterpool));
+ break; /* ...out of for (i = 0; i < rangelist->nelts; i++) */
+ }
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Make a copy of PROPCHANGES (array of svn_prop_t) into *TRIMMED_PROPCHANGES,
+ omitting any svn:mergeinfo changes. */
+static svn_error_t *
+omit_mergeinfo_changes(apr_array_header_t **trimmed_propchanges,
+ const apr_array_header_t *propchanges,
+ apr_pool_t *result_pool)
+{
+ int i;
+
+ *trimmed_propchanges = apr_array_make(result_pool,
+ propchanges->nelts,
+ sizeof(svn_prop_t));
+
+ for (i = 0; i < propchanges->nelts; ++i)
+ {
+ const svn_prop_t *change = &APR_ARRAY_IDX(propchanges, i, svn_prop_t);
+
+ /* If this property is not svn:mergeinfo, then copy it. */
+ if (strcmp(change->name, SVN_PROP_MERGEINFO) != 0)
+ APR_ARRAY_PUSH(*trimmed_propchanges, svn_prop_t) = *change;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Helper for merge_props_changed().
+
+ *PROPS is an array of svn_prop_t structures representing regular properties
+ to be added to the working copy TARGET_ABSPATH.
+
+ The merge source and target are assumed to be in the same repository.
+
+ Filter out mergeinfo property additions to TARGET_ABSPATH when
+ those additions refer to the same line of history as TARGET_ABSPATH as
+ described below.
+
+ Examine the added mergeinfo, looking at each range (or single rev)
+ of each source path. If a source_path/range refers to the same line of
+ history as TARGET_ABSPATH (pegged at its base revision), then filter out
+ that range. If the entire rangelist for a given path is filtered then
+ filter out the path as well.
+
+ RA_SESSION is an open RA session to the repository
+ in which both the source and target live, else RA_SESSION is not used. It
+ may be temporarily reparented as needed by this function.
+
+ Use CTX for any further client operations.
+
+ If any filtering occurs, set outgoing *PROPS to a shallow copy (allocated
+ in POOL) of incoming *PROPS minus the filtered mergeinfo. */
+static svn_error_t *
+filter_self_referential_mergeinfo(apr_array_header_t **props,
+ const char *target_abspath,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *adjusted_props;
+ int i;
+ apr_pool_t *iterpool;
+ svn_boolean_t is_copy;
+ const char *repos_relpath;
+ svn_client__pathrev_t target_base;
+
+ /* If PATH itself has been added there is no need to filter. */
+ SVN_ERR(svn_wc__node_get_origin(&is_copy, &target_base.rev, &repos_relpath,
+ &target_base.repos_root_url,
+ &target_base.repos_uuid, NULL,
+ ctx->wc_ctx, target_abspath, FALSE,
+ pool, pool));
+
+ if (is_copy || !repos_relpath)
+ return SVN_NO_ERROR; /* A copy or a local addition */
+
+ target_base.url = svn_path_url_add_component2(target_base.repos_root_url,
+ repos_relpath, pool);
+
+ adjusted_props = apr_array_make(pool, (*props)->nelts, sizeof(svn_prop_t));
+ iterpool = svn_pool_create(pool);
+ for (i = 0; i < (*props)->nelts; ++i)
+ {
+ svn_prop_t *prop = &APR_ARRAY_IDX((*props), i, svn_prop_t);
+
+ svn_mergeinfo_t mergeinfo, younger_mergeinfo;
+ svn_mergeinfo_t filtered_mergeinfo = NULL;
+ svn_mergeinfo_t filtered_younger_mergeinfo = NULL;
+ svn_error_t *err;
+
+ /* If this property isn't mergeinfo or is NULL valued (i.e. prop removal)
+ or empty mergeinfo it does not require any special handling. There
+ is nothing to filter out of empty mergeinfo and the concept of
+ filtering doesn't apply if we are trying to remove mergeinfo
+ entirely. */
+ if ((strcmp(prop->name, SVN_PROP_MERGEINFO) != 0)
+ || (! prop->value) /* Removal of mergeinfo */
+ || (! prop->value->len)) /* Empty mergeinfo */
+ {
+ APR_ARRAY_PUSH(adjusted_props, svn_prop_t) = *prop;
+ continue;
+ }
+
+ svn_pool_clear(iterpool);
+
+ /* Non-empty mergeinfo; filter self-referential mergeinfo out. */
+
+ /* Parse the incoming mergeinfo to allow easier manipulation. */
+ err = svn_mergeinfo_parse(&mergeinfo, prop->value->data, iterpool);
+
+ if (err)
+ {
+ /* Issue #3896: If we can't parse it, we certainly can't
+ filter it. */
+ if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
+ {
+ svn_error_clear(err);
+ APR_ARRAY_PUSH(adjusted_props, svn_prop_t) = *prop;
+ continue;
+ }
+ else
+ {
+ return svn_error_trace(err);
+ }
+ }
+
+ /* The working copy target PATH is at BASE_REVISION. Divide the
+ incoming mergeinfo into two groups. One where all revision ranges
+ are as old or older than BASE_REVISION and one where all revision
+ ranges are younger.
+
+ Note: You may be wondering why we do this.
+
+ For the incoming mergeinfo "older" than target's base revision we
+ can filter out self-referential mergeinfo efficiently using
+ svn_client__get_history_as_mergeinfo(). We simply look at PATH's
+ natural history as mergeinfo and remove that from any incoming
+ mergeinfo.
+
+ For mergeinfo "younger" than the base revision we can't use
+ svn_ra_get_location_segments() to look into PATH's future
+ history. Instead we must use svn_client__repos_locations() and
+ look at each incoming source/range individually and see if PATH
+ at its base revision and PATH at the start of the incoming range
+ exist on the same line of history. If they do then we can filter
+ out the incoming range. But since we have to do this for each
+ range there is a substantial performance penalty to pay if the
+ incoming ranges are not contiguous, i.e. we call
+ svn_client__repos_locations for each discrete range and incur
+ the cost of a roundtrip communication with the repository. */
+ SVN_ERR(split_mergeinfo_on_revision(&younger_mergeinfo,
+ &mergeinfo,
+ target_base.rev,
+ iterpool));
+
+ /* Filter self-referential mergeinfo from younger_mergeinfo. */
+ if (younger_mergeinfo)
+ {
+ apr_hash_index_t *hi;
+ const char *merge_source_root_url;
+
+ SVN_ERR(svn_ra_get_repos_root2(ra_session,
+ &merge_source_root_url, iterpool));
+
+ for (hi = apr_hash_first(iterpool, younger_mergeinfo);
+ hi; hi = apr_hash_next(hi))
+ {
+ int j;
+ const char *source_path = svn__apr_hash_index_key(hi);
+ svn_rangelist_t *rangelist = svn__apr_hash_index_val(hi);
+ const char *merge_source_url;
+ svn_rangelist_t *adjusted_rangelist =
+ apr_array_make(iterpool, 0, sizeof(svn_merge_range_t *));
+
+ merge_source_url =
+ svn_path_url_add_component2(merge_source_root_url,
+ source_path + 1, iterpool);
+
+ for (j = 0; j < rangelist->nelts; j++)
+ {
+ svn_error_t *err2;
+ svn_client__pathrev_t *start_loc;
+ svn_merge_range_t *range =
+ APR_ARRAY_IDX(rangelist, j, svn_merge_range_t *);
+
+ /* Because the merge source normalization code
+ ensures mergeinfo refers to real locations on
+ the same line of history, there's no need to
+ look at the whole range, just the start. */
+
+ /* Check if PATH@BASE_REVISION exists at
+ RANGE->START on the same line of history.
+ (start+1 because RANGE->start is not inclusive.) */
+ err2 = svn_client__repos_location(&start_loc, ra_session,
+ &target_base,
+ range->start + 1,
+ ctx, iterpool, iterpool);
+ if (err2)
+ {
+ if (err2->apr_err == SVN_ERR_CLIENT_UNRELATED_RESOURCES
+ || err2->apr_err == SVN_ERR_FS_NOT_FOUND
+ || err2->apr_err == SVN_ERR_FS_NO_SUCH_REVISION)
+ {
+ /* PATH@BASE_REVISION didn't exist at
+ RANGE->START + 1 or is unrelated to the
+ resource PATH@RANGE->START. Some of the
+ requested revisions may not even exist in
+ the repository; a real possibility since
+ mergeinfo is hand editable. In all of these
+ cases clear and ignore the error and don't
+ do any filtering.
+
+ Note: In this last case it is possible that
+ we will allow self-referential mergeinfo to
+ be applied, but fixing it here is potentially
+ very costly in terms of finding what part of
+ a range is actually valid. Simply allowing
+ the merge to proceed without filtering the
+ offending range seems the least worst
+ option. */
+ svn_error_clear(err2);
+ err2 = NULL;
+ APR_ARRAY_PUSH(adjusted_rangelist,
+ svn_merge_range_t *) = range;
+ }
+ else
+ {
+ return svn_error_trace(err2);
+ }
+ }
+ else
+ {
+ /* PATH@BASE_REVISION exists on the same
+ line of history at RANGE->START and RANGE->END.
+ Now check that PATH@BASE_REVISION's path
+ names at RANGE->START and RANGE->END are the same.
+ If the names are not the same then the mergeinfo
+ describing PATH@RANGE->START through
+ PATH@RANGE->END actually belong to some other
+ line of history and we want to record this
+ mergeinfo, not filter it. */
+ if (strcmp(start_loc->url, merge_source_url) != 0)
+ {
+ APR_ARRAY_PUSH(adjusted_rangelist,
+ svn_merge_range_t *) = range;
+ }
+ }
+ /* else no need to add, this mergeinfo is
+ all on the same line of history. */
+ } /* for (j = 0; j < rangelist->nelts; j++) */
+
+ /* Add any rangelists for source_path that are not
+ self-referential. */
+ if (adjusted_rangelist->nelts)
+ {
+ if (!filtered_younger_mergeinfo)
+ filtered_younger_mergeinfo = apr_hash_make(iterpool);
+ svn_hash_sets(filtered_younger_mergeinfo, source_path,
+ adjusted_rangelist);
+ }
+
+ } /* Iteration over each merge source in younger_mergeinfo. */
+ } /* if (younger_mergeinfo) */
+
+ /* Filter self-referential mergeinfo from "older" mergeinfo. */
+ if (mergeinfo)
+ {
+ svn_mergeinfo_t implicit_mergeinfo;
+
+ SVN_ERR(svn_client__get_history_as_mergeinfo(
+ &implicit_mergeinfo, NULL,
+ &target_base, target_base.rev, SVN_INVALID_REVNUM,
+ ra_session, ctx, iterpool));
+
+ /* Remove PATH's implicit mergeinfo from the incoming mergeinfo. */
+ SVN_ERR(svn_mergeinfo_remove2(&filtered_mergeinfo,
+ implicit_mergeinfo,
+ mergeinfo, TRUE, iterpool, iterpool));
+ }
+
+ /* Combine whatever older and younger filtered mergeinfo exists
+ into filtered_mergeinfo. */
+ if (filtered_mergeinfo && filtered_younger_mergeinfo)
+ SVN_ERR(svn_mergeinfo_merge2(filtered_mergeinfo,
+ filtered_younger_mergeinfo, iterpool,
+ iterpool));
+ else if (filtered_younger_mergeinfo)
+ filtered_mergeinfo = filtered_younger_mergeinfo;
+
+ /* If there is any incoming mergeinfo remaining after filtering
+ then put it in adjusted_props. */
+ if (filtered_mergeinfo && apr_hash_count(filtered_mergeinfo))
+ {
+ /* Convert filtered_mergeinfo to a svn_prop_t and put it
+ back in the array. */
+ svn_string_t *filtered_mergeinfo_str;
+ svn_prop_t *adjusted_prop = apr_pcalloc(pool,
+ sizeof(*adjusted_prop));
+ SVN_ERR(svn_mergeinfo_to_string(&filtered_mergeinfo_str,
+ filtered_mergeinfo,
+ pool));
+ adjusted_prop->name = SVN_PROP_MERGEINFO;
+ adjusted_prop->value = filtered_mergeinfo_str;
+ APR_ARRAY_PUSH(adjusted_props, svn_prop_t) = *adjusted_prop;
+ }
+ }
+ svn_pool_destroy(iterpool);
+
+ *props = adjusted_props;
+ return SVN_NO_ERROR;
+}
+
+/* Prepare a set of property changes PROPCHANGES to be used for a merge
+ operation on LOCAL_ABSPATH.
+
+ Remove all non-regular prop-changes (entry-props and WC-props).
+ Remove all non-mergeinfo prop-changes if it's a record-only merge.
+ Remove self-referential mergeinfo (### in some cases...)
+ Remove foreign-repository mergeinfo (### in some cases...)
+
+ Store the resulting property changes in *PROP_UPDATES.
+ Store information on where mergeinfo is updated in MERGE_B.
+
+ Used for both file and directory property merges. */
+static svn_error_t *
+prepare_merge_props_changed(const apr_array_header_t **prop_updates,
+ const char *local_abspath,
+ const apr_array_header_t *propchanges,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *props;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ /* We only want to merge "regular" version properties: by
+ definition, 'svn merge' shouldn't touch any data within .svn/ */
+ SVN_ERR(svn_categorize_props(propchanges, NULL, NULL, &props,
+ result_pool));
+
+ /* If we are only applying mergeinfo changes then we need to do
+ additional filtering of PROPS so it contains only mergeinfo changes. */
+ if (merge_b->record_only && props->nelts)
+ {
+ apr_array_header_t *mergeinfo_props =
+ apr_array_make(result_pool, 1, sizeof(svn_prop_t));
+ int i;
+
+ for (i = 0; i < props->nelts; i++)
+ {
+ svn_prop_t *prop = &APR_ARRAY_IDX(props, i, svn_prop_t);
+
+ if (strcmp(prop->name, SVN_PROP_MERGEINFO) == 0)
+ {
+ APR_ARRAY_PUSH(mergeinfo_props, svn_prop_t) = *prop;
+ break;
+ }
+ }
+ props = mergeinfo_props;
+ }
+
+ if (props->nelts)
+ {
+ /* Issue #3383: We don't want mergeinfo from a foreign repos.
+
+ If this is a merge from a foreign repository we must strip all
+ incoming mergeinfo (including mergeinfo deletions). */
+ if (! merge_b->same_repos)
+ SVN_ERR(omit_mergeinfo_changes(&props, props, result_pool));
+
+ /* If this is a forward merge then don't add new mergeinfo to
+ PATH that is already part of PATH's own history, see
+ http://svn.haxx.se/dev/archive-2008-09/0006.shtml. If the
+ merge sources are not ancestral then there is no concept of a
+ 'forward' or 'reverse' merge and we filter unconditionally. */
+ if (merge_b->merge_source.loc1->rev < merge_b->merge_source.loc2->rev
+ || !merge_b->merge_source.ancestral)
+ {
+ if (HONOR_MERGEINFO(merge_b) || merge_b->reintegrate_merge)
+ SVN_ERR(filter_self_referential_mergeinfo(&props,
+ local_abspath,
+ merge_b->ra_session2,
+ merge_b->ctx,
+ result_pool));
+ }
+ }
+ *prop_updates = props;
+
+ /* Make a record in BATON if we find a PATH where mergeinfo is added
+ where none existed previously or PATH is having its existing
+ mergeinfo deleted. */
+ if (props->nelts)
+ {
+ int i;
+
+ for (i = 0; i < props->nelts; ++i)
+ {
+ svn_prop_t *prop = &APR_ARRAY_IDX(props, i, svn_prop_t);
+
+ if (strcmp(prop->name, SVN_PROP_MERGEINFO) == 0)
+ {
+ /* Does LOCAL_ABSPATH have any pristine mergeinfo? */
+ svn_boolean_t has_pristine_mergeinfo = FALSE;
+ apr_hash_t *pristine_props;
+
+ SVN_ERR(svn_wc_get_pristine_props(&pristine_props,
+ merge_b->ctx->wc_ctx,
+ local_abspath,
+ scratch_pool,
+ scratch_pool));
+
+ if (pristine_props
+ && svn_hash_gets(pristine_props, SVN_PROP_MERGEINFO))
+ has_pristine_mergeinfo = TRUE;
+
+ if (!has_pristine_mergeinfo && prop->value)
+ {
+ alloc_and_store_path(&merge_b->paths_with_new_mergeinfo,
+ local_abspath, merge_b->pool);
+ }
+ else if (has_pristine_mergeinfo && !prop->value)
+ {
+ alloc_and_store_path(&merge_b->paths_with_deleted_mergeinfo,
+ local_abspath, merge_b->pool);
+ }
+ }
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+#define CONFLICT_REASON_NONE ((svn_wc_conflict_reason_t)-1)
+#define CONFLICT_REASON_SKIP ((svn_wc_conflict_reason_t)-2)
+#define CONFLICT_REASON_SKIP_WC ((svn_wc_conflict_reason_t)-3)
+
+/* Baton used for testing trees for being editted while performing tree
+ conflict detection for incoming deletes */
+struct dir_delete_baton_t
+{
+ /* Reference to dir baton of directory that is the root of the deletion */
+ struct merge_dir_baton_t *del_root;
+
+ /* Boolean indicating that some edit is found. Allows avoiding more work */
+ svn_boolean_t found_edit;
+
+ /* A list of paths that are compared. Kept up to date until FOUND_EDIT is
+ set to TRUE */
+ apr_hash_t *compared_abspaths;
+};
+
+/* Baton for the merge_dir_*() functions. Initialized in merge_dir_opened() */
+struct merge_dir_baton_t
+{
+ /* Reference to the parent baton, unless the parent is the anchor, in which
+ case PARENT_BATON is NULL */
+ struct merge_dir_baton_t *parent_baton;
+
+ /* The pool containing this baton. Use for RESULT_POOL for storing in this
+ baton */
+ apr_pool_t *pool;
+
+ /* This directory doesn't have a representation in the working copy, so any
+ operation on it will be skipped and possibly cause a tree conflict on the
+ shadow root */
+ svn_boolean_t shadowed;
+
+ /* This node or one of its descendants received operational changes from the
+ merge. If this node is the shadow root its tree conflict status has been
+ applied */
+ svn_boolean_t edited;
+
+ /* If a tree conflict will be installed once edited, it's reason. If a skip
+ should be produced its reason. Otherwise CONFLICT_REASON_NONE for no tree
+ conflict.
+
+ Special values:
+ CONFLICT_REASON_SKIP:
+ The node will be skipped with content and property state as stored in
+ SKIP_REASON.
+
+ CONFLICT_REASON_SKIP_WC:
+ The node will be skipped as an obstructing working copy.
+ */
+ svn_wc_conflict_reason_t tree_conflict_reason;
+ svn_wc_conflict_action_t tree_conflict_action;
+
+ /* When TREE_CONFLICT_REASON is CONFLICT_REASON_SKIP, the skip state to
+ add to the notification */
+ svn_wc_notify_state_t skip_reason;
+
+ /* TRUE if the node was added by this merge. Otherwise FALSE */
+ svn_boolean_t added;
+ svn_boolean_t add_is_replace; /* Add is second part of replace */
+
+ /* TRUE if we are taking over an existing directory as addition, otherwise
+ FALSE. */
+ svn_boolean_t add_existing;
+
+ /* NULL, or an hashtable mapping const char * local_abspaths to
+ const char *kind mapping, containing deleted nodes that still need a delete
+ notification (which may be a replaced notification if the node is not just
+ deleted) */
+ apr_hash_t *pending_deletes;
+
+ /* NULL, or an hashtable mapping const char * LOCAL_ABSPATHs to
+ a const svn_wc_conflict_description2_t * instance, describing the just
+ installed conflict */
+ apr_hash_t *new_tree_conflicts;
+
+ /* If not NULL, a reference to the information of the delete test that is
+ currently in progress. Allocated in the root-directory baton, referenced
+ from all descendants */
+ struct dir_delete_baton_t *delete_state;
+};
+
+/* Baton for the merge_dir_*() functions. Initialized in merge_file_opened() */
+struct merge_file_baton_t
+{
+ /* Reference to the parent baton, unless the parent is the anchor, in which
+ case PARENT_BATON is NULL */
+ struct merge_dir_baton_t *parent_baton;
+
+ /* This file doesn't have a representation in the working copy, so any
+ operation on it will be skipped and possibly cause a tree conflict
+ on the shadow root */
+ svn_boolean_t shadowed;
+
+ /* This node received operational changes from the merge. If this node
+ is the shadow root its tree conflict status has been applied */
+ svn_boolean_t edited;
+
+ /* If a tree conflict will be installed once edited, it's reason. If a skip
+ should be produced its reason. Some special values are defined. See the
+ merge_tree_baton_t for an explanation. */
+ svn_wc_conflict_reason_t tree_conflict_reason;
+ svn_wc_conflict_action_t tree_conflict_action;
+
+ /* When TREE_CONFLICT_REASON is CONFLICT_REASON_SKIP, the skip state to
+ add to the notification */
+ svn_wc_notify_state_t skip_reason;
+
+ /* TRUE if the node was added by this merge. Otherwise FALSE */
+ svn_boolean_t added;
+ svn_boolean_t add_is_replace; /* Add is second part of replace */
+};
+
+/* Forward declaration */
+static svn_error_t *
+notify_merge_begin(merge_cmd_baton_t *merge_b,
+ const char *local_abspath,
+ svn_boolean_t delete_action,
+ apr_pool_t *scratch_pool);
+
+/* Record the skip for future processing and (later) produce the
+ skip notification */
+static svn_error_t *
+record_skip(merge_cmd_baton_t *merge_b,
+ const char *local_abspath,
+ svn_node_kind_t kind,
+ svn_wc_notify_action_t action,
+ svn_wc_notify_state_t state,
+ apr_pool_t *scratch_pool)
+{
+ if (merge_b->record_only)
+ return SVN_NO_ERROR; /* ### Why? - Legacy compatibility */
+
+ if (merge_b->merge_source.ancestral
+ || merge_b->reintegrate_merge)
+ {
+ store_path(merge_b->skipped_abspaths, local_abspath);
+ }
+
+ if (merge_b->ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+
+ SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
+
+ notify = svn_wc_create_notify(local_abspath, action, scratch_pool);
+ notify->kind = kind;
+ notify->content_state = notify->prop_state = state;
+
+ (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
+ scratch_pool);
+ }
+ return SVN_NO_ERROR;
+}
+
+/* Record a tree conflict in the WC, unless this is a dry run or a record-
+ * only merge, or if a tree conflict is already flagged for the VICTIM_PATH.
+ * (The latter can happen if a merge-tracking-aware merge is doing multiple
+ * editor drives because of a gap in the range of eligible revisions.)
+ *
+ * The tree conflict, with its victim specified by VICTIM_PATH, is
+ * assumed to have happened during a merge using merge baton MERGE_B.
+ *
+ * NODE_KIND must be the node kind of "old" and "theirs" and "mine";
+ * this function cannot cope with node kind clashes.
+ * ACTION and REASON correspond to the fields
+ * of the same names in svn_wc_tree_conflict_description_t.
+ */
+static svn_error_t *
+record_tree_conflict(merge_cmd_baton_t *merge_b,
+ const char *local_abspath,
+ struct merge_dir_baton_t *parent_baton,
+ svn_node_kind_t node_kind,
+ svn_wc_conflict_action_t action,
+ svn_wc_conflict_reason_t reason,
+ const svn_wc_conflict_description2_t *existing_conflict,
+ svn_boolean_t notify_tc,
+ apr_pool_t *scratch_pool)
+{
+ svn_wc_context_t *wc_ctx = merge_b->ctx->wc_ctx;
+
+ if (merge_b->merge_source.ancestral
+ || merge_b->reintegrate_merge)
+ {
+ store_path(merge_b->tree_conflicted_abspaths, local_abspath);
+ }
+
+ alloc_and_store_path(&merge_b->conflicted_paths, local_abspath,
+ merge_b->pool);
+
+
+ if (!merge_b->record_only && !merge_b->dry_run)
+ {
+ svn_wc_conflict_description2_t *conflict;
+ const svn_wc_conflict_version_t *left;
+ const svn_wc_conflict_version_t *right;
+ apr_pool_t *result_pool = parent_baton ? parent_baton->pool
+ : scratch_pool;
+
+ if (reason == svn_wc_conflict_reason_deleted)
+ {
+ const char *moved_to_abspath;
+
+ SVN_ERR(svn_wc__node_was_moved_away(&moved_to_abspath, NULL,
+ wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+
+ if (moved_to_abspath)
+ {
+ /* Local abspath itself has been moved away. If only a
+ descendant is moved away, we call the node itself deleted */
+ reason = svn_wc_conflict_reason_moved_away;
+ }
+ }
+ else if (reason == svn_wc_conflict_reason_added)
+ {
+ const char *moved_from_abspath;
+ SVN_ERR(svn_wc__node_was_moved_here(&moved_from_abspath, NULL,
+ wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+ if (moved_from_abspath)
+ reason = svn_wc_conflict_reason_moved_here;
+ }
+
+ SVN_ERR(make_conflict_versions(&left, &right, local_abspath, node_kind,
+ &merge_b->merge_source, merge_b->target,
+ result_pool, scratch_pool));
+
+ /* Fix up delete of file, add of dir replacement (or other way around) */
+ if (existing_conflict != NULL && existing_conflict->src_left_version)
+ left = existing_conflict->src_left_version;
+
+ conflict = svn_wc_conflict_description_create_tree2(
+ local_abspath, node_kind, svn_wc_operation_merge,
+ left, right, result_pool);
+
+ conflict->action = action;
+ conflict->reason = reason;
+
+ /* May return SVN_ERR_WC_PATH_UNEXPECTED_STATUS */
+ if (existing_conflict)
+ SVN_ERR(svn_wc__del_tree_conflict(wc_ctx, local_abspath,
+ scratch_pool));
+
+ SVN_ERR(svn_wc__add_tree_conflict(merge_b->ctx->wc_ctx, conflict,
+ scratch_pool));
+
+ if (parent_baton)
+ {
+ if (! parent_baton->new_tree_conflicts)
+ parent_baton->new_tree_conflicts = apr_hash_make(result_pool);
+
+ svn_hash_sets(parent_baton->new_tree_conflicts,
+ apr_pstrdup(result_pool, local_abspath),
+ conflict);
+ }
+
+ /* ### TODO: Store in parent baton */
+ }
+
+ /* On a replacement we currently get two tree conflicts */
+ if (merge_b->ctx->notify_func2 && notify_tc)
+ {
+ svn_wc_notify_t *notify;
+
+ SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
+
+ notify = svn_wc_create_notify(local_abspath, svn_wc_notify_tree_conflict,
+ scratch_pool);
+ notify->kind = node_kind;
+
+ (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
+ scratch_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Record the add for future processing and produce the
+ update_add notification
+ */
+static svn_error_t *
+record_update_add(merge_cmd_baton_t *merge_b,
+ const char *local_abspath,
+ svn_node_kind_t kind,
+ svn_boolean_t notify_replaced,
+ apr_pool_t *scratch_pool)
+{
+ if (merge_b->merge_source.ancestral || merge_b->reintegrate_merge)
+ {
+ store_path(merge_b->merged_abspaths, local_abspath);
+ }
+
+ if (merge_b->ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+ svn_wc_notify_action_t action = svn_wc_notify_update_add;
+
+ SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
+
+ if (notify_replaced)
+ action = svn_wc_notify_update_replace;
+
+ notify = svn_wc_create_notify(local_abspath, action, scratch_pool);
+ notify->kind = kind;
+
+ (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
+ scratch_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Record the update for future processing and produce the
+ update_update notification */
+static svn_error_t *
+record_update_update(merge_cmd_baton_t *merge_b,
+ const char *local_abspath,
+ svn_node_kind_t kind,
+ svn_wc_notify_state_t content_state,
+ svn_wc_notify_state_t prop_state,
+ apr_pool_t *scratch_pool)
+{
+ if (merge_b->merge_source.ancestral || merge_b->reintegrate_merge)
+ {
+ store_path(merge_b->merged_abspaths, local_abspath);
+ }
+
+ if (merge_b->ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+
+ SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE, scratch_pool));
+
+ notify = svn_wc_create_notify(local_abspath, svn_wc_notify_update_update,
+ scratch_pool);
+ notify->kind = kind;
+ notify->content_state = content_state;
+ notify->prop_state = prop_state;
+
+ (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
+ scratch_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Record the delete for future processing and for (later) producing the
+ update_delete notification */
+static svn_error_t *
+record_update_delete(merge_cmd_baton_t *merge_b,
+ struct merge_dir_baton_t *parent_db,
+ const char *local_abspath,
+ svn_node_kind_t kind,
+ apr_pool_t *scratch_pool)
+{
+ /* Update the lists of merged, skipped, tree-conflicted and added paths. */
+ if (merge_b->merge_source.ancestral
+ || merge_b->reintegrate_merge)
+ {
+ /* Issue #4166: If a previous merge added NOTIFY_ABSPATH, but we
+ are now deleting it, then remove it from the list of added
+ paths. */
+ svn_hash_sets(merge_b->added_abspaths, local_abspath, NULL);
+ store_path(merge_b->merged_abspaths, local_abspath);
+ }
+
+ SVN_ERR(notify_merge_begin(merge_b, local_abspath, TRUE, scratch_pool));
+
+ if (parent_db)
+ {
+ const char *dup_abspath = apr_pstrdup(parent_db->pool, local_abspath);
+
+ if (!parent_db->pending_deletes)
+ parent_db->pending_deletes = apr_hash_make(parent_db->pool);
+
+ svn_hash_sets(parent_db->pending_deletes, dup_abspath,
+ svn_node_kind_to_word(kind));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Notify the pending 'D'eletes, that were waiting to see if a matching 'A'dd
+ might make them a 'R'eplace. */
+static svn_error_t *
+handle_pending_notifications(merge_cmd_baton_t *merge_b,
+ struct merge_dir_baton_t *db,
+ apr_pool_t *scratch_pool)
+{
+ if (merge_b->ctx->notify_func2 && db->pending_deletes)
+ {
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(scratch_pool, db->pending_deletes);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *del_abspath = svn__apr_hash_index_key(hi);
+ svn_wc_notify_t *notify;
+
+ notify = svn_wc_create_notify(del_abspath,
+ svn_wc_notify_update_delete,
+ scratch_pool);
+ notify->kind = svn_node_kind_from_word(
+ svn__apr_hash_index_val(hi));
+
+ (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2,
+ notify, scratch_pool);
+ }
+
+ db->pending_deletes = NULL;
+ }
+ return SVN_NO_ERROR;
+}
+
+/* Helper function for the merge_dir_*() and merge_file_*() functions.
+
+ Installs and notifies pre-recorded tree conflicts and skips for
+ ancestors of operational merges
+ */
+static svn_error_t *
+mark_dir_edited(merge_cmd_baton_t *merge_b,
+ struct merge_dir_baton_t *db,
+ const char *local_abspath,
+ apr_pool_t *scratch_pool)
+{
+ /* ### Too much common code with mark_file_edited */
+ if (db->edited)
+ return SVN_NO_ERROR;
+
+ if (db->parent_baton && !db->parent_baton->edited)
+ {
+ const char *dir_abspath = svn_dirent_dirname(local_abspath,
+ scratch_pool);
+
+ SVN_ERR(mark_dir_edited(merge_b, db->parent_baton, dir_abspath,
+ scratch_pool));
+ }
+
+ db->edited = TRUE;
+
+ if (! db->shadowed)
+ return SVN_NO_ERROR; /* Easy out */
+
+ if (db->parent_baton
+ && db->parent_baton->delete_state
+ && db->tree_conflict_reason != CONFLICT_REASON_NONE)
+ {
+ db->parent_baton->delete_state->found_edit = TRUE;
+ }
+ else if (db->tree_conflict_reason == CONFLICT_REASON_SKIP
+ || db->tree_conflict_reason == CONFLICT_REASON_SKIP_WC)
+ {
+ /* open_directory() decided not to flag a tree conflict, but
+ for clarity we produce a skip for this node that
+ most likely isn't touched by the merge itself */
+
+ if (merge_b->ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+
+ SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE,
+ scratch_pool));
+
+ notify = svn_wc_create_notify(
+ local_abspath,
+ (db->tree_conflict_reason == CONFLICT_REASON_SKIP)
+ ? svn_wc_notify_skip
+ : svn_wc_notify_update_skip_obstruction,
+ scratch_pool);
+ notify->kind = svn_node_dir;
+ notify->content_state = notify->prop_state = db->skip_reason;
+
+ (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2,
+ notify,
+ scratch_pool);
+ }
+
+ if (merge_b->merge_source.ancestral
+ || merge_b->reintegrate_merge)
+ {
+ store_path(merge_b->skipped_abspaths, local_abspath);
+ }
+ }
+ else if (db->tree_conflict_reason != CONFLICT_REASON_NONE)
+ {
+ /* open_directory() decided that a tree conflict should be raised */
+
+ SVN_ERR(record_tree_conflict(merge_b, local_abspath, db->parent_baton,
+ svn_node_dir, db->tree_conflict_action,
+ db->tree_conflict_reason,
+ NULL, TRUE,
+ scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper function for the merge_file_*() functions.
+
+ Installs and notifies pre-recorded tree conflicts and skips for
+ ancestors of operational merges
+ */
+static svn_error_t *
+mark_file_edited(merge_cmd_baton_t *merge_b,
+ struct merge_file_baton_t *fb,
+ const char *local_abspath,
+ apr_pool_t *scratch_pool)
+{
+ /* ### Too much common code with mark_dir_edited */
+ if (fb->edited)
+ return SVN_NO_ERROR;
+
+ if (fb->parent_baton && !fb->parent_baton->edited)
+ {
+ const char *dir_abspath = svn_dirent_dirname(local_abspath,
+ scratch_pool);
+
+ SVN_ERR(mark_dir_edited(merge_b, fb->parent_baton, dir_abspath,
+ scratch_pool));
+ }
+
+ fb->edited = TRUE;
+
+ if (! fb->shadowed)
+ return SVN_NO_ERROR; /* Easy out */
+
+ if (fb->parent_baton
+ && fb->parent_baton->delete_state
+ && fb->tree_conflict_reason != CONFLICT_REASON_NONE)
+ {
+ fb->parent_baton->delete_state->found_edit = TRUE;
+ }
+ else if (fb->tree_conflict_reason == CONFLICT_REASON_SKIP
+ || fb->tree_conflict_reason == CONFLICT_REASON_SKIP_WC)
+ {
+ /* open_directory() decided not to flag a tree conflict, but
+ for clarity we produce a skip for this node that
+ most likely isn't touched by the merge itself */
+
+ if (merge_b->ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+
+ SVN_ERR(notify_merge_begin(merge_b, local_abspath, FALSE,
+ scratch_pool));
+
+ notify = svn_wc_create_notify(local_abspath, svn_wc_notify_skip,
+ scratch_pool);
+ notify->kind = svn_node_file;
+ notify->content_state = notify->prop_state = fb->skip_reason;
+
+ (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2,
+ notify,
+ scratch_pool);
+ }
+
+ if (merge_b->merge_source.ancestral
+ || merge_b->reintegrate_merge)
+ {
+ store_path(merge_b->skipped_abspaths, local_abspath);
+ }
+ }
+ else if (fb->tree_conflict_reason != CONFLICT_REASON_NONE)
+ {
+ /* open_file() decided that a tree conflict should be raised */
+
+ SVN_ERR(record_tree_conflict(merge_b, local_abspath, fb->parent_baton,
+ svn_node_file, fb->tree_conflict_action,
+ fb->tree_conflict_reason,
+ NULL, TRUE,
+ scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_diff_tree_processor_t function.
+
+ Called before either merge_file_changed(), merge_file_added(),
+ merge_file_deleted() or merge_file_closed(), unless it sets *SKIP to TRUE.
+
+ When *SKIP is TRUE, the diff driver avoids work on getting the details
+ for the closing callbacks.
+ */
+static svn_error_t *
+merge_file_opened(void **new_file_baton,
+ svn_boolean_t *skip,
+ const char *relpath,
+ const svn_diff_source_t *left_source,
+ const svn_diff_source_t *right_source,
+ const svn_diff_source_t *copyfrom_source,
+ void *dir_baton,
+ const struct svn_diff_tree_processor_t *processor,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t *merge_b = processor->baton;
+ struct merge_dir_baton_t *pdb = dir_baton;
+ struct merge_file_baton_t *fb;
+ const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
+ relpath, scratch_pool);
+
+ fb = apr_pcalloc(result_pool, sizeof(*fb));
+ fb->tree_conflict_reason = CONFLICT_REASON_NONE;
+ fb->tree_conflict_action = svn_wc_conflict_action_edit;
+ fb->skip_reason = svn_wc_notify_state_unknown;
+
+ *new_file_baton = fb;
+
+ if (pdb)
+ {
+ fb->parent_baton = pdb;
+ fb->shadowed = pdb->shadowed;
+ fb->skip_reason = pdb->skip_reason;
+ }
+
+ if (fb->shadowed)
+ {
+ /* An ancestor is tree conflicted. Nothing to do here. */
+ }
+ else if (left_source != NULL)
+ {
+ /* Node is expected to be a file, which will be changed or deleted. */
+ svn_node_kind_t kind;
+ svn_boolean_t is_deleted;
+ svn_boolean_t excluded;
+ svn_depth_t parent_depth;
+
+ if (! right_source)
+ fb->tree_conflict_action = svn_wc_conflict_action_delete;
+
+ {
+ svn_wc_notify_state_t obstr_state;
+
+ SVN_ERR(perform_obstruction_check(&obstr_state, &is_deleted, &excluded,
+ &kind, &parent_depth,
+ merge_b, local_abspath,
+ scratch_pool));
+
+ if (obstr_state != svn_wc_notify_state_inapplicable)
+ {
+ fb->shadowed = TRUE;
+ fb->tree_conflict_reason = CONFLICT_REASON_SKIP;
+ fb->skip_reason = obstr_state;
+ return SVN_NO_ERROR;
+ }
+
+ if (is_deleted)
+ kind = svn_node_none;
+ }
+
+ if (kind == svn_node_none)
+ {
+ fb->shadowed = TRUE;
+
+ /* If this is not the merge target and the parent is too shallow to
+ contain this directory, and the directory is not present
+ via exclusion or depth filtering, skip it instead of recording
+ a tree conflict.
+
+ Non-inheritable mergeinfo will be recorded, allowing
+ future merges into non-shallow working copies to merge
+ changes we missed this time around. */
+ if (pdb && (excluded
+ || (parent_depth != svn_depth_unknown &&
+ parent_depth < svn_depth_files)))
+ {
+ fb->shadowed = TRUE;
+
+ fb->tree_conflict_reason = CONFLICT_REASON_SKIP;
+ fb->skip_reason = svn_wc_notify_state_missing;
+ return SVN_NO_ERROR;
+ }
+
+ if (is_deleted)
+ fb->tree_conflict_reason = svn_wc_conflict_reason_deleted;
+ else
+ fb->tree_conflict_reason = svn_wc_conflict_reason_missing;
+
+ /* ### Similar to directory */
+ *skip = TRUE;
+ SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+ return SVN_NO_ERROR;
+ /* ### /Similar */
+ }
+ else if (kind != svn_node_file)
+ {
+ fb->shadowed = TRUE;
+
+ fb->tree_conflict_reason = svn_wc_conflict_reason_obstructed;
+
+ /* ### Similar to directory */
+ *skip = TRUE;
+ SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+ return SVN_NO_ERROR;
+ /* ### /Similar */
+ }
+
+ if (! right_source)
+ {
+ /* We want to delete the directory */
+ fb->tree_conflict_action = svn_wc_conflict_action_delete;
+ SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+
+ if (fb->shadowed)
+ {
+ return SVN_NO_ERROR; /* Already set a tree conflict */
+ }
+
+ /* Comparison mode to verify for delete tree conflicts? */
+ if (pdb && pdb->delete_state
+ && pdb->delete_state->found_edit)
+ {
+ /* Earlier nodes found a conflict. Done. */
+ *skip = TRUE;
+ }
+ }
+ }
+ else
+ {
+ const svn_wc_conflict_description2_t *old_tc = NULL;
+
+ /* The node doesn't exist pre-merge: We have an addition */
+ fb->added = TRUE;
+ fb->tree_conflict_action = svn_wc_conflict_action_add;
+
+ if (pdb && pdb->pending_deletes
+ && svn_hash_gets(pdb->pending_deletes, local_abspath))
+ {
+ fb->add_is_replace = TRUE;
+ fb->tree_conflict_action = svn_wc_conflict_action_replace;
+
+ svn_hash_sets(pdb->pending_deletes, local_abspath, NULL);
+ }
+
+ if (pdb
+ && pdb->new_tree_conflicts
+ && (old_tc = svn_hash_gets(pdb->new_tree_conflicts, local_abspath)))
+ {
+ fb->tree_conflict_action = svn_wc_conflict_action_replace;
+ fb->tree_conflict_reason = old_tc->reason;
+
+ /* Update the tree conflict to store that this is a replace */
+ SVN_ERR(record_tree_conflict(merge_b, local_abspath, pdb,
+ svn_node_file,
+ fb->tree_conflict_action,
+ fb->tree_conflict_reason,
+ old_tc, FALSE,
+ scratch_pool));
+
+ if (old_tc->reason == svn_wc_conflict_reason_deleted
+ || old_tc->reason == svn_wc_conflict_reason_moved_away)
+ {
+ /* Issue #3806: Incoming replacements on local deletes produce
+ inconsistent result.
+
+ In this specific case we can continue applying the add part
+ of the replacement. */
+ }
+ else
+ {
+ *skip = TRUE;
+
+ return SVN_NO_ERROR;
+ }
+ }
+ else if (! (merge_b->dry_run
+ && ((pdb && pdb->added) || fb->add_is_replace)))
+ {
+ svn_wc_notify_state_t obstr_state;
+ svn_node_kind_t kind;
+ svn_boolean_t is_deleted;
+
+ SVN_ERR(perform_obstruction_check(&obstr_state, &is_deleted, NULL,
+ &kind, NULL,
+ merge_b, local_abspath,
+ scratch_pool));
+
+ if (obstr_state != svn_wc_notify_state_inapplicable)
+ {
+ /* Skip the obstruction */
+ fb->shadowed = TRUE;
+ fb->tree_conflict_reason = CONFLICT_REASON_SKIP;
+ fb->skip_reason = obstr_state;
+ }
+ else if (kind != svn_node_none && !is_deleted)
+ {
+ /* Set a tree conflict */
+ fb->shadowed = TRUE;
+ fb->tree_conflict_reason = svn_wc_conflict_reason_obstructed;
+ }
+ }
+
+ /* Handle pending conflicts */
+ SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_diff_tree_processor_t function.
+ *
+ * Called after merge_file_opened() when a node receives only text and/or
+ * property changes between LEFT_SOURCE and RIGHT_SOURCE.
+ *
+ * left_file and right_file can be NULL when the file is not modified.
+ * left_props and right_props are always available.
+ */
+static svn_error_t *
+merge_file_changed(const char *relpath,
+ const svn_diff_source_t *left_source,
+ const svn_diff_source_t *right_source,
+ const char *left_file,
+ const char *right_file,
+ /*const*/ apr_hash_t *left_props,
+ /*const*/ apr_hash_t *right_props,
+ svn_boolean_t file_modified,
+ const apr_array_header_t *prop_changes,
+ void *file_baton,
+ const struct svn_diff_tree_processor_t *processor,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t *merge_b = processor->baton;
+ struct merge_file_baton_t *fb = file_baton;
+ svn_client_ctx_t *ctx = merge_b->ctx;
+ const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
+ relpath, scratch_pool);
+ const svn_wc_conflict_version_t *left;
+ const svn_wc_conflict_version_t *right;
+ svn_wc_notify_state_t text_state;
+ svn_wc_notify_state_t property_state;
+
+ SVN_ERR_ASSERT(local_abspath && svn_dirent_is_absolute(local_abspath));
+ SVN_ERR_ASSERT(!left_file || svn_dirent_is_absolute(left_file));
+ SVN_ERR_ASSERT(!right_file || svn_dirent_is_absolute(right_file));
+
+ SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+
+ if (fb->shadowed)
+ {
+ if (fb->tree_conflict_reason == CONFLICT_REASON_NONE)
+ {
+ /* We haven't notified for this node yet: report a skip */
+ SVN_ERR(record_skip(merge_b, local_abspath, svn_node_file,
+ svn_wc_notify_update_shadowed_update,
+ fb->skip_reason, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+ }
+
+ /* This callback is essentially no more than a wrapper around
+ svn_wc_merge5(). Thank goodness that all the
+ diff-editor-mechanisms are doing the hard work of getting the
+ fulltexts! */
+
+ property_state = svn_wc_notify_state_unchanged;
+ text_state = svn_wc_notify_state_unchanged;
+
+ SVN_ERR(prepare_merge_props_changed(&prop_changes, local_abspath,
+ prop_changes, merge_b,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(make_conflict_versions(&left, &right, local_abspath,
+ svn_node_file, &merge_b->merge_source, merge_b->target,
+ scratch_pool, scratch_pool));
+
+ /* Do property merge now, if we are not going to perform a text merge */
+ if ((merge_b->record_only || !left_file) && prop_changes->nelts)
+ {
+ SVN_ERR(svn_wc_merge_props3(&property_state, ctx->wc_ctx, local_abspath,
+ left, right,
+ left_props, prop_changes,
+ merge_b->dry_run,
+ NULL, NULL,
+ ctx->cancel_func, ctx->cancel_baton,
+ scratch_pool));
+ if (property_state == svn_wc_notify_state_conflicted)
+ {
+ alloc_and_store_path(&merge_b->conflicted_paths, local_abspath,
+ merge_b->pool);
+ }
+ }
+
+ /* Easy out: We are only applying mergeinfo differences. */
+ if (merge_b->record_only)
+ {
+ /* NO-OP */
+ }
+ else if (left_file)
+ {
+ svn_boolean_t has_local_mods;
+ enum svn_wc_merge_outcome_t content_outcome;
+
+ /* xgettext: the '.working', '.merge-left.r%ld' and
+ '.merge-right.r%ld' strings are used to tag onto a file
+ name in case of a merge conflict */
+ const char *target_label = _(".working");
+ const char *left_label = apr_psprintf(scratch_pool,
+ _(".merge-left.r%ld"),
+ left_source->revision);
+ const char *right_label = apr_psprintf(scratch_pool,
+ _(".merge-right.r%ld"),
+ right_source->revision);
+
+ SVN_ERR(svn_wc_text_modified_p2(&has_local_mods, ctx->wc_ctx,
+ local_abspath, FALSE, scratch_pool));
+
+ /* Do property merge and text merge in one step so that keyword expansion
+ takes into account the new property values. */
+ SVN_ERR(svn_wc_merge5(&content_outcome, &property_state, ctx->wc_ctx,
+ left_file, right_file, local_abspath,
+ left_label, right_label, target_label,
+ left, right,
+ merge_b->dry_run, merge_b->diff3_cmd,
+ merge_b->merge_options,
+ left_props, prop_changes,
+ NULL, NULL,
+ ctx->cancel_func,
+ ctx->cancel_baton,
+ scratch_pool));
+
+ if (content_outcome == svn_wc_merge_conflict
+ || property_state == svn_wc_notify_state_conflicted)
+ {
+ alloc_and_store_path(&merge_b->conflicted_paths, local_abspath,
+ merge_b->pool);
+ }
+
+ if (content_outcome == svn_wc_merge_conflict)
+ text_state = svn_wc_notify_state_conflicted;
+ else if (has_local_mods
+ && content_outcome != svn_wc_merge_unchanged)
+ text_state = svn_wc_notify_state_merged;
+ else if (content_outcome == svn_wc_merge_merged)
+ text_state = svn_wc_notify_state_changed;
+ else if (content_outcome == svn_wc_merge_no_merge)
+ text_state = svn_wc_notify_state_missing;
+ else /* merge_outcome == svn_wc_merge_unchanged */
+ text_state = svn_wc_notify_state_unchanged;
+ }
+
+ if (text_state == svn_wc_notify_state_conflicted
+ || text_state == svn_wc_notify_state_merged
+ || text_state == svn_wc_notify_state_changed
+ || property_state == svn_wc_notify_state_conflicted
+ || property_state == svn_wc_notify_state_merged
+ || property_state == svn_wc_notify_state_changed)
+ {
+ SVN_ERR(record_update_update(merge_b, local_abspath, svn_node_file,
+ text_state, property_state,
+ scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_diff_tree_processor_t function.
+ *
+ * Called after merge_file_opened() when a node doesn't exist in LEFT_SOURCE,
+ * but does in RIGHT_SOURCE.
+ *
+ * When a node is replaced instead of just added a separate opened+deleted will
+ * be invoked before the current open+added.
+ */
+static svn_error_t *
+merge_file_added(const char *relpath,
+ const svn_diff_source_t *copyfrom_source,
+ const svn_diff_source_t *right_source,
+ const char *copyfrom_file,
+ const char *right_file,
+ /*const*/ apr_hash_t *copyfrom_props,
+ /*const*/ apr_hash_t *right_props,
+ void *file_baton,
+ const struct svn_diff_tree_processor_t *processor,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t *merge_b = processor->baton;
+ struct merge_file_baton_t *fb = file_baton;
+ const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
+ relpath, scratch_pool);
+ apr_hash_t *pristine_props;
+ apr_hash_t *new_props;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+
+ if (fb->shadowed)
+ {
+ if (fb->tree_conflict_reason == CONFLICT_REASON_NONE)
+ {
+ /* We haven't notified for this node yet: report a skip */
+ SVN_ERR(record_skip(merge_b, local_abspath, svn_node_file,
+ svn_wc_notify_update_shadowed_add,
+ fb->skip_reason, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+ }
+
+ /* Easy out: We are only applying mergeinfo differences. */
+ if (merge_b->record_only)
+ {
+ return SVN_NO_ERROR;
+ }
+
+ if ((merge_b->merge_source.ancestral || merge_b->reintegrate_merge)
+ && ( !fb->parent_baton || !fb->parent_baton->added))
+ {
+ /* Store the roots of added subtrees */
+ store_path(merge_b->added_abspaths, local_abspath);
+ }
+
+ if (!merge_b->dry_run)
+ {
+ const char *copyfrom_url;
+ svn_revnum_t copyfrom_rev;
+ svn_stream_t *new_contents, *pristine_contents;
+
+ /* If this is a merge from the same repository as our
+ working copy, we handle adds as add-with-history.
+ Otherwise, we'll use a pure add. */
+ if (merge_b->same_repos)
+ {
+ const char *child =
+ svn_dirent_skip_ancestor(merge_b->target->abspath,
+ local_abspath);
+ SVN_ERR_ASSERT(child != NULL);
+ copyfrom_url = svn_path_url_add_component2(
+ merge_b->merge_source.loc2->url,
+ child, scratch_pool);
+ copyfrom_rev = right_source->revision;
+ SVN_ERR(check_repos_match(merge_b->target, local_abspath,
+ copyfrom_url, scratch_pool));
+ SVN_ERR(svn_stream_open_readonly(&pristine_contents,
+ right_file,
+ scratch_pool,
+ scratch_pool));
+ new_contents = NULL; /* inherit from new_base_contents */
+
+ pristine_props = right_props; /* Includes last_* information */
+ new_props = NULL; /* No local changes */
+
+ if (svn_hash_gets(pristine_props, SVN_PROP_MERGEINFO))
+ {
+ alloc_and_store_path(&merge_b->paths_with_new_mergeinfo,
+ local_abspath, merge_b->pool);
+ }
+ }
+ else
+ {
+ apr_array_header_t *regular_props;
+
+ copyfrom_url = NULL;
+ copyfrom_rev = SVN_INVALID_REVNUM;
+
+ pristine_contents = svn_stream_empty(scratch_pool);
+ SVN_ERR(svn_stream_open_readonly(&new_contents, right_file,
+ scratch_pool, scratch_pool));
+
+ pristine_props = apr_hash_make(scratch_pool); /* Local addition */
+
+ /* We don't want any foreign properties */
+ SVN_ERR(svn_categorize_props(svn_prop_hash_to_array(right_props,
+ scratch_pool),
+ NULL, NULL, &regular_props,
+ scratch_pool));
+
+ new_props = svn_prop_array_to_hash(regular_props, scratch_pool);
+
+ /* Issue #3383: We don't want mergeinfo from a foreign repository. */
+ svn_hash_sets(new_props, SVN_PROP_MERGEINFO, NULL);
+ }
+
+ /* Do everything like if we had called 'svn cp PATH1 PATH2'. */
+ SVN_ERR(svn_wc_add_repos_file4(merge_b->ctx->wc_ctx,
+ local_abspath,
+ pristine_contents,
+ new_contents,
+ pristine_props, new_props,
+ copyfrom_url, copyfrom_rev,
+ merge_b->ctx->cancel_func,
+ merge_b->ctx->cancel_baton,
+ scratch_pool));
+
+ /* Caller must call svn_sleep_for_timestamps() */
+ *merge_b->use_sleep = TRUE;
+ }
+
+ SVN_ERR(record_update_add(merge_b, local_abspath, svn_node_file,
+ fb->add_is_replace, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Compare the two sets of properties PROPS1 and PROPS2, ignoring the
+ * "svn:mergeinfo" property, and noticing only "normal" props. Set *SAME to
+ * true if the rest of the properties are identical or false if they differ.
+ */
+static svn_error_t *
+properties_same_p(svn_boolean_t *same,
+ apr_hash_t *props1,
+ apr_hash_t *props2,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *prop_changes;
+ int i, diffs;
+
+ /* Examine the properties that differ */
+ SVN_ERR(svn_prop_diffs(&prop_changes, props1, props2, scratch_pool));
+ diffs = 0;
+ for (i = 0; i < prop_changes->nelts; i++)
+ {
+ const char *pname = APR_ARRAY_IDX(prop_changes, i, svn_prop_t).name;
+
+ /* Count the properties we're interested in; ignore the rest */
+ if (svn_wc_is_normal_prop(pname)
+ && strcmp(pname, SVN_PROP_MERGEINFO) != 0)
+ diffs++;
+ }
+ *same = (diffs == 0);
+ return SVN_NO_ERROR;
+}
+
+/* Compare the file OLDER_ABSPATH (together with its normal properties in
+ * ORIGINAL_PROPS which may also contain WC props and entry props) with the
+ * versioned file MINE_ABSPATH (together with its versioned properties).
+ * Set *SAME to true if they are the same or false if they differ, ignoring
+ * the "svn:mergeinfo" property, and ignoring differences in keyword
+ * expansion and end-of-line style. */
+static svn_error_t *
+files_same_p(svn_boolean_t *same,
+ const char *older_abspath,
+ apr_hash_t *original_props,
+ const char *mine_abspath,
+ svn_wc_context_t *wc_ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *working_props;
+
+ SVN_ERR(svn_wc_prop_list2(&working_props, wc_ctx, mine_abspath,
+ scratch_pool, scratch_pool));
+
+ /* Compare the properties */
+ SVN_ERR(properties_same_p(same, original_props, working_props,
+ scratch_pool));
+ if (*same)
+ {
+ svn_stream_t *mine_stream;
+ svn_stream_t *older_stream;
+ svn_opt_revision_t working_rev = { svn_opt_revision_working, { 0 } };
+
+ /* Compare the file content, translating 'mine' to 'normal' form. */
+ if (svn_prop_get_value(working_props, SVN_PROP_SPECIAL) != NULL)
+ SVN_ERR(svn_subst_read_specialfile(&mine_stream, mine_abspath,
+ scratch_pool, scratch_pool));
+ else
+ SVN_ERR(svn_client__get_normalized_stream(&mine_stream, wc_ctx,
+ mine_abspath, &working_rev,
+ FALSE, TRUE, NULL, NULL,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(svn_stream_open_readonly(&older_stream, older_abspath,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(svn_stream_contents_same2(same, mine_stream, older_stream,
+ scratch_pool));
+
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_diff_tree_processor_t function.
+ *
+ * Called after merge_file_opened() when a node does exist in LEFT_SOURCE, but
+ * no longer exists (or is replaced) in RIGHT_SOURCE.
+ *
+ * When a node is replaced instead of just added a separate opened+added will
+ * be invoked after the current open+deleted.
+ */
+static svn_error_t *
+merge_file_deleted(const char *relpath,
+ const svn_diff_source_t *left_source,
+ const char *left_file,
+ /*const*/ apr_hash_t *left_props,
+ void *file_baton,
+ const struct svn_diff_tree_processor_t *processor,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t *merge_b = processor->baton;
+ struct merge_file_baton_t *fb = file_baton;
+ const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
+ relpath, scratch_pool);
+ svn_boolean_t same;
+
+ SVN_ERR(mark_file_edited(merge_b, fb, local_abspath, scratch_pool));
+
+ if (fb->shadowed)
+ {
+ if (fb->tree_conflict_reason == CONFLICT_REASON_NONE)
+ {
+ /* We haven't notified for this node yet: report a skip */
+ SVN_ERR(record_skip(merge_b, local_abspath, svn_node_file,
+ svn_wc_notify_update_shadowed_delete,
+ fb->skip_reason, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+ }
+
+ /* Easy out: We are only applying mergeinfo differences. */
+ if (merge_b->record_only)
+ {
+ return SVN_NO_ERROR;
+ }
+
+ /* If the files are identical, attempt deletion */
+ if (merge_b->force_delete)
+ same = TRUE;
+ else
+ SVN_ERR(files_same_p(&same, left_file, left_props,
+ local_abspath, merge_b->ctx->wc_ctx,
+ scratch_pool));
+
+ if (fb->parent_baton
+ && fb->parent_baton->delete_state)
+ {
+ if (same)
+ {
+ /* Note that we checked this file */
+ store_path(fb->parent_baton->delete_state->compared_abspaths,
+ local_abspath);
+ }
+ else
+ {
+ /* We found some modification. Parent should raise a tree conflict */
+ fb->parent_baton->delete_state->found_edit = TRUE;
+ }
+
+ return SVN_NO_ERROR;
+ }
+ else if (same)
+ {
+ if (!merge_b->dry_run)
+ SVN_ERR(svn_wc_delete4(merge_b->ctx->wc_ctx, local_abspath,
+ FALSE /* keep_local */, FALSE /* unversioned */,
+ merge_b->ctx->cancel_func,
+ merge_b->ctx->cancel_baton,
+ NULL, NULL /* no notify */,
+ scratch_pool));
+
+ /* Record that we might have deleted mergeinfo */
+ alloc_and_store_path(&merge_b->paths_with_deleted_mergeinfo,
+ local_abspath, merge_b->pool);
+
+ /* And notify the deletion */
+ SVN_ERR(record_update_delete(merge_b, fb->parent_baton, local_abspath,
+ svn_node_file, scratch_pool));
+ }
+ else
+ {
+ /* The files differ, so raise a conflict instead of deleting */
+
+ /* This is use case 5 described in the paper attached to issue
+ * #2282. See also notes/tree-conflicts/detection.txt
+ */
+ SVN_ERR(record_tree_conflict(merge_b, local_abspath, fb->parent_baton,
+ svn_node_file,
+ svn_wc_conflict_action_delete,
+ svn_wc_conflict_reason_edited,
+ NULL, TRUE,
+ scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_diff_tree_processor_t function.
+
+ Called before either merge_dir_changed(), merge_dir_added(),
+ merge_dir_deleted() or merge_dir_closed(), unless it sets *SKIP to TRUE.
+
+ After this call and before the close call, all descendants will receive
+ their changes, unless *SKIP_CHILDREN is set to TRUE.
+
+ When *SKIP is TRUE, the diff driver avoids work on getting the details
+ for the closing callbacks.
+
+ The SKIP and SKIP_DESCENDANTS work independantly.
+ */
+static svn_error_t *
+merge_dir_opened(void **new_dir_baton,
+ svn_boolean_t *skip,
+ svn_boolean_t *skip_children,
+ const char *relpath,
+ const svn_diff_source_t *left_source,
+ const svn_diff_source_t *right_source,
+ const svn_diff_source_t *copyfrom_source,
+ void *parent_dir_baton,
+ const struct svn_diff_tree_processor_t *processor,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t *merge_b = processor->baton;
+ struct merge_dir_baton_t *db;
+ struct merge_dir_baton_t *pdb = parent_dir_baton;
+
+ const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
+ relpath, scratch_pool);
+
+ db = apr_pcalloc(result_pool, sizeof(*db));
+ db->pool = result_pool;
+ db->tree_conflict_reason = CONFLICT_REASON_NONE;
+ db->tree_conflict_action = svn_wc_conflict_action_edit;
+ db->skip_reason = svn_wc_notify_state_unknown;
+
+ *new_dir_baton = db;
+
+ if (pdb)
+ {
+ db->parent_baton = pdb;
+ db->shadowed = pdb->shadowed;
+ db->skip_reason = pdb->skip_reason;
+ }
+
+ if (db->shadowed)
+ {
+ /* An ancestor is tree conflicted. Nothing to do here. */
+ if (! left_source)
+ db->added = TRUE;
+ }
+ else if (left_source != NULL)
+ {
+ /* Node is expected to be a directory. */
+ svn_node_kind_t kind;
+ svn_boolean_t is_deleted;
+ svn_boolean_t excluded;
+ svn_depth_t parent_depth;
+
+ if (! right_source)
+ db->tree_conflict_action = svn_wc_conflict_action_delete;
+
+ /* Check for an obstructed or missing node on disk. */
+ {
+ svn_wc_notify_state_t obstr_state;
+ SVN_ERR(perform_obstruction_check(&obstr_state, &is_deleted, &excluded,
+ &kind, &parent_depth,
+ merge_b, local_abspath,
+ scratch_pool));
+
+ if (obstr_state != svn_wc_notify_state_inapplicable)
+ {
+ db->shadowed = TRUE;
+
+ if (obstr_state == svn_wc_notify_state_obstructed)
+ {
+ svn_boolean_t is_wcroot;
+
+ SVN_ERR(svn_wc_check_root(&is_wcroot, NULL, NULL,
+ merge_b->ctx->wc_ctx,
+ local_abspath, scratch_pool));
+
+ if (is_wcroot)
+ {
+ db->tree_conflict_reason = CONFLICT_REASON_SKIP_WC;
+ return SVN_NO_ERROR;
+ }
+ }
+
+ db->tree_conflict_reason = CONFLICT_REASON_SKIP;
+ db->skip_reason = obstr_state;
+
+ if (! right_source)
+ {
+ *skip = *skip_children = TRUE;
+ SVN_ERR(mark_dir_edited(merge_b, db, local_abspath,
+ scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+ }
+
+ if (is_deleted)
+ kind = svn_node_none;
+ }
+
+ if (kind == svn_node_none)
+ {
+ db->shadowed = TRUE;
+
+ /* If this is not the merge target and the parent is too shallow to
+ contain this directory, and the directory is not presen
+ via exclusion or depth filtering, skip it instead of recording
+ a tree conflict.
+
+ Non-inheritable mergeinfo will be recorded, allowing
+ future merges into non-shallow working copies to merge
+ changes we missed this time around. */
+ if (pdb && (excluded
+ || (parent_depth != svn_depth_unknown &&
+ parent_depth < svn_depth_immediates)))
+ {
+ db->shadowed = TRUE;
+
+ db->tree_conflict_reason = CONFLICT_REASON_SKIP;
+ db->skip_reason = svn_wc_notify_state_missing;
+
+ return SVN_NO_ERROR;
+ }
+
+ if (is_deleted)
+ db->tree_conflict_reason = svn_wc_conflict_reason_deleted;
+ else
+ db->tree_conflict_reason = svn_wc_conflict_reason_missing;
+
+ /* ### To avoid breaking tests */
+ *skip = TRUE;
+ *skip_children = TRUE;
+ SVN_ERR(mark_dir_edited(merge_b, db, local_abspath, scratch_pool));
+ return SVN_NO_ERROR;
+ /* ### /avoid breaking tests */
+ }
+ else if (kind != svn_node_dir)
+ {
+ db->shadowed = TRUE;
+
+ db->tree_conflict_reason = svn_wc_conflict_reason_obstructed;
+
+ /* ### To avoid breaking tests */
+ *skip = TRUE;
+ *skip_children = TRUE;
+ SVN_ERR(mark_dir_edited(merge_b, db, local_abspath, scratch_pool));
+ return SVN_NO_ERROR;
+ /* ### /avoid breaking tests */
+ }
+
+ if (! right_source)
+ {
+ /* We want to delete the directory */
+ /* Mark PB edited now? */
+ db->tree_conflict_action = svn_wc_conflict_action_delete;
+ SVN_ERR(mark_dir_edited(merge_b, db, local_abspath, scratch_pool));
+
+ if (db->shadowed)
+ {
+ *skip_children = TRUE;
+ return SVN_NO_ERROR; /* Already set a tree conflict */
+ }
+
+ db->delete_state = (pdb != NULL) ? pdb->delete_state : NULL;
+
+ if (db->delete_state && db->delete_state->found_edit)
+ {
+ /* A sibling found a conflict. Done. */
+ *skip = TRUE;
+ *skip_children = TRUE;
+ }
+ else if (merge_b->force_delete)
+ {
+ /* No comparison necessary */
+ *skip_children = TRUE;
+ }
+ else if (! db->delete_state)
+ {
+ /* Start descendant comparison */
+ db->delete_state = apr_pcalloc(db->pool,
+ sizeof(*db->delete_state));
+
+ db->delete_state->del_root = db;
+ db->delete_state->compared_abspaths = apr_hash_make(db->pool);
+ }
+ }
+ }
+ else
+ {
+ const svn_wc_conflict_description2_t *old_tc = NULL;
+
+ /* The node doesn't exist pre-merge: We have an addition */
+ db->added = TRUE;
+ db->tree_conflict_action = svn_wc_conflict_action_add;
+
+ if (pdb && pdb->pending_deletes
+ && svn_hash_gets(pdb->pending_deletes, local_abspath))
+ {
+ db->add_is_replace = TRUE;
+ db->tree_conflict_action = svn_wc_conflict_action_replace;
+
+ svn_hash_sets(pdb->pending_deletes, local_abspath, NULL);
+ }
+
+ if (pdb
+ && pdb->new_tree_conflicts
+ && (old_tc = svn_hash_gets(pdb->new_tree_conflicts, local_abspath)))
+ {
+ db->tree_conflict_action = svn_wc_conflict_action_replace;
+ db->tree_conflict_reason = old_tc->reason;
+
+ if (old_tc->reason == svn_wc_conflict_reason_deleted
+ || old_tc->reason == svn_wc_conflict_reason_moved_away)
+ {
+ /* Issue #3806: Incoming replacements on local deletes produce
+ inconsistent result.
+
+ In this specific case we can continue applying the add part
+ of the replacement. */
+ }
+ else
+ {
+ *skip = TRUE;
+ *skip_children = TRUE;
+
+ /* Update the tree conflict to store that this is a replace */
+ SVN_ERR(record_tree_conflict(merge_b, local_abspath, pdb,
+ svn_node_dir,
+ db->tree_conflict_action,
+ db->tree_conflict_reason,
+ old_tc, FALSE,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+ }
+ }
+
+ if (! (merge_b->dry_run
+ && ((pdb && pdb->added) || db->add_is_replace)))
+ {
+ svn_wc_notify_state_t obstr_state;
+ svn_node_kind_t kind;
+ svn_boolean_t is_deleted;
+
+ SVN_ERR(perform_obstruction_check(&obstr_state, &is_deleted, NULL,
+ &kind, NULL,
+ merge_b, local_abspath,
+ scratch_pool));
+
+ /* In this case of adding a directory, we have an exception to the
+ * usual "skip if it's inconsistent" rule. If the directory exists
+ * on disk unexpectedly, we simply make it versioned, because we can
+ * do so without risk of destroying data. Only skip if it is
+ * versioned but unexpectedly missing from disk, or is unversioned
+ * but obstructed by a node of the wrong kind. */
+ if (obstr_state == svn_wc_notify_state_obstructed
+ && (is_deleted || kind == svn_node_none))
+ {
+ svn_node_kind_t disk_kind;
+
+ SVN_ERR(svn_io_check_path(local_abspath, &disk_kind,
+ scratch_pool));
+
+ if (disk_kind == svn_node_dir)
+ {
+ obstr_state = svn_wc_notify_state_inapplicable;
+ db->add_existing = TRUE; /* Take over existing directory */
+ }
+ }
+
+ if (obstr_state != svn_wc_notify_state_inapplicable)
+ {
+ /* Skip the obstruction */
+ db->shadowed = TRUE;
+ db->tree_conflict_reason = CONFLICT_REASON_SKIP;
+ db->skip_reason = obstr_state;
+ }
+ else if (kind != svn_node_none && !is_deleted)
+ {
+ /* Set a tree conflict */
+ db->shadowed = TRUE;
+ db->tree_conflict_reason = svn_wc_conflict_reason_obstructed;
+ }
+ }
+
+ /* Handle pending conflicts */
+ SVN_ERR(mark_dir_edited(merge_b, db, local_abspath, scratch_pool));
+
+ if (db->shadowed)
+ {
+ /* Notified and done. Skip children? */
+ }
+ else if (merge_b->record_only)
+ {
+ /* Ok, we are done for this node and its descendants */
+ *skip = TRUE;
+ *skip_children = TRUE;
+ }
+ else if (! merge_b->dry_run)
+ {
+ /* Create the directory on disk, to allow descendants to be added */
+ if (! db->add_existing)
+ SVN_ERR(svn_io_dir_make(local_abspath, APR_OS_DEFAULT,
+ scratch_pool));
+
+ if (old_tc)
+ {
+ /* svn_wc_add4 and svn_wc_add_from_disk2 can't add a node
+ over an existing tree conflict */
+
+ /* ### These functions should take some tree conflict argument
+ and allow overwriting the tc when one is passed */
+
+ SVN_ERR(svn_wc__del_tree_conflict(merge_b->ctx->wc_ctx,
+ local_abspath,
+ scratch_pool));
+ }
+
+ if (merge_b->same_repos)
+ {
+ const char *original_url;
+
+ original_url = svn_path_url_add_component2(
+ merge_b->merge_source.loc2->url,
+ relpath, scratch_pool);
+
+ /* Limitation (aka HACK):
+ We create a newly added directory with an original URL and
+ revision as that in the repository, but without its properties
+ and children.
+
+ When the merge is cancelled before the final dir_added(), the
+ copy won't really represent the in-repository state of the node.
+ */
+ SVN_ERR(svn_wc_add4(merge_b->ctx->wc_ctx, local_abspath,
+ svn_depth_infinity,
+ original_url,
+ right_source->revision,
+ merge_b->ctx->cancel_func,
+ merge_b->ctx->cancel_baton,
+ NULL, NULL /* no notify! */,
+ scratch_pool));
+ }
+ else
+ {
+ SVN_ERR(svn_wc_add_from_disk2(merge_b->ctx->wc_ctx, local_abspath,
+ apr_hash_make(scratch_pool),
+ NULL, NULL /* no notify! */,
+ scratch_pool));
+ }
+
+ if (old_tc != NULL)
+ {
+ /* ### Should be atomic with svn_wc_add(4|_from_disk2)() */
+ SVN_ERR(record_tree_conflict(merge_b, local_abspath, pdb,
+ svn_node_dir,
+ db->tree_conflict_action,
+ db->tree_conflict_reason,
+ old_tc, FALSE,
+ scratch_pool));
+ }
+ }
+
+ if (! db->shadowed && !merge_b->record_only)
+ SVN_ERR(record_update_add(merge_b, local_abspath, svn_node_dir,
+ db->add_is_replace, scratch_pool));
+ }
+ return SVN_NO_ERROR;
+}
+
+/* An svn_diff_tree_processor_t function.
+ *
+ * Called after merge_dir_opened() when a node exists in both the left and
+ * right source, but has its properties changed inbetween.
+ *
+ * After the merge_dir_opened() but before the call to this merge_dir_changed()
+ * function all descendants will have been updated.
+ */
+static svn_error_t *
+merge_dir_changed(const char *relpath,
+ const svn_diff_source_t *left_source,
+ const svn_diff_source_t *right_source,
+ /*const*/ apr_hash_t *left_props,
+ /*const*/ apr_hash_t *right_props,
+ const apr_array_header_t *prop_changes,
+ void *dir_baton,
+ const struct svn_diff_tree_processor_t *processor,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t *merge_b = processor->baton;
+ struct merge_dir_baton_t *db = dir_baton;
+ const apr_array_header_t *props;
+ const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
+ relpath, scratch_pool);
+
+ SVN_ERR(handle_pending_notifications(merge_b, db, scratch_pool));
+
+ SVN_ERR(mark_dir_edited(merge_b, db, local_abspath, scratch_pool));
+
+ if (db->shadowed)
+ {
+ if (db->tree_conflict_reason == CONFLICT_REASON_NONE)
+ {
+ /* We haven't notified for this node yet: report a skip */
+ SVN_ERR(record_skip(merge_b, local_abspath, svn_node_dir,
+ svn_wc_notify_update_shadowed_update,
+ db->skip_reason, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR(prepare_merge_props_changed(&props, local_abspath, prop_changes,
+ merge_b, scratch_pool, scratch_pool));
+
+ if (props->nelts)
+ {
+ const svn_wc_conflict_version_t *left;
+ const svn_wc_conflict_version_t *right;
+ svn_client_ctx_t *ctx = merge_b->ctx;
+ svn_wc_notify_state_t prop_state;
+
+ SVN_ERR(make_conflict_versions(&left, &right, local_abspath,
+ svn_node_dir, &merge_b->merge_source,
+ merge_b->target,
+ scratch_pool, scratch_pool));
+
+ SVN_ERR(svn_wc_merge_props3(&prop_state, ctx->wc_ctx, local_abspath,
+ left, right,
+ left_props, props,
+ merge_b->dry_run,
+ NULL, NULL,
+ ctx->cancel_func, ctx->cancel_baton,
+ scratch_pool));
+
+ if (prop_state == svn_wc_notify_state_conflicted)
+ {
+ alloc_and_store_path(&merge_b->conflicted_paths, local_abspath,
+ merge_b->pool);
+ }
+
+ if (prop_state == svn_wc_notify_state_conflicted
+ || prop_state == svn_wc_notify_state_merged
+ || prop_state == svn_wc_notify_state_changed)
+ {
+ SVN_ERR(record_update_update(merge_b, local_abspath, svn_node_file,
+ svn_wc_notify_state_inapplicable,
+ prop_state, scratch_pool));
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* An svn_diff_tree_processor_t function.
+ *
+ * Called after merge_dir_opened() when a node doesn't exist in LEFT_SOURCE,
+ * but does in RIGHT_SOURCE. After the merge_dir_opened() but before the call
+ * to this merge_dir_added() function all descendants will have been added.
+ *
+ * When a node is replaced instead of just added a separate opened+deleted will
+ * be invoked before the current open+added.
+ */
+static svn_error_t *
+merge_dir_added(const char *relpath,
+ const svn_diff_source_t *copyfrom_source,
+ const svn_diff_source_t *right_source,
+ /*const*/ apr_hash_t *copyfrom_props,
+ /*const*/ apr_hash_t *right_props,
+ void *dir_baton,
+ const struct svn_diff_tree_processor_t *processor,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t *merge_b = processor->baton;
+ struct merge_dir_baton_t *db = dir_baton;
+ const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
+ relpath, scratch_pool);
+
+ /* For consistency; usually a no-op from _dir_added() */
+ SVN_ERR(handle_pending_notifications(merge_b, db, scratch_pool));
+ SVN_ERR(mark_dir_edited(merge_b, db, local_abspath, scratch_pool));
+
+ if (db->shadowed)
+ {
+ if (db->tree_conflict_reason == CONFLICT_REASON_NONE)
+ {
+ /* We haven't notified for this node yet: report a skip */
+ SVN_ERR(record_skip(merge_b, local_abspath, svn_node_dir,
+ svn_wc_notify_update_shadowed_add,
+ db->skip_reason, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR_ASSERT(
+ db->edited /* Marked edited from merge_open_dir() */
+ && ! merge_b->record_only /* Skip details from merge_open_dir() */
+ );
+
+ if ((merge_b->merge_source.ancestral || merge_b->reintegrate_merge)
+ && ( !db->parent_baton || !db->parent_baton->added))
+ {
+ /* Store the roots of added subtrees */
+ store_path(merge_b->added_abspaths, local_abspath);
+ }
+
+ if (merge_b->same_repos)
+ {
+ /* When the directory was added in merge_dir_added() we didn't update its
+ pristine properties. Instead we receive the property changes later and
+ apply them in this function.
+
+ If we would apply them as changes (such as before fixing issue #3405),
+ we would see the unmodified properties as local changes, and the
+ pristine properties would be out of sync with what the repository
+ expects for this directory.
+
+ Instead of doing that we now simply set the properties as the pristine
+ properties via a private libsvn_wc api.
+ */
+
+ const char *copyfrom_url;
+ svn_revnum_t copyfrom_rev;
+ const char *parent_abspath;
+ const char *child;
+
+ /* Creating a hash containing regular and entry props */
+ apr_hash_t *new_pristine_props = right_props;
+
+ parent_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
+ child = svn_dirent_is_child(merge_b->target->abspath, local_abspath, NULL);
+ SVN_ERR_ASSERT(child != NULL);
+
+ copyfrom_url = svn_path_url_add_component2(merge_b->merge_source.loc2->url,
+ child, scratch_pool);
+ copyfrom_rev = right_source->revision;
+
+ SVN_ERR(check_repos_match(merge_b->target, parent_abspath, copyfrom_url,
+ scratch_pool));
+
+ if (!merge_b->dry_run)
+ {
+ SVN_ERR(svn_wc__complete_directory_add(merge_b->ctx->wc_ctx,
+ local_abspath,
+ new_pristine_props,
+ copyfrom_url, copyfrom_rev,
+ scratch_pool));
+ }
+
+ if (svn_hash_gets(new_pristine_props, SVN_PROP_MERGEINFO))
+ {
+ alloc_and_store_path(&merge_b->paths_with_new_mergeinfo,
+ local_abspath, merge_b->pool);
+ }
+ }
+ else
+ {
+ apr_array_header_t *regular_props;
+ apr_hash_t *new_props;
+ svn_wc_notify_state_t prop_state;
+
+ SVN_ERR(svn_categorize_props(svn_prop_hash_to_array(right_props,
+ scratch_pool),
+ NULL, NULL, &regular_props, scratch_pool));
+
+ new_props = svn_prop_array_to_hash(regular_props, scratch_pool);
+
+ svn_hash_sets(new_props, SVN_PROP_MERGEINFO, NULL);
+
+ /* ### What is the easiest way to set new_props on LOCAL_ABSPATH?
+
+ ### This doesn't need a merge as we just added the node
+ ### (or installed a tree conflict and skipped this node)*/
+
+ SVN_ERR(svn_wc_merge_props3(&prop_state, merge_b->ctx->wc_ctx,
+ local_abspath,
+ NULL, NULL,
+ apr_hash_make(scratch_pool),
+ svn_prop_hash_to_array(new_props,
+ scratch_pool),
+ merge_b->dry_run,
+ NULL, NULL,
+ merge_b->ctx->cancel_func,
+ merge_b->ctx->cancel_baton,
+ scratch_pool));
+ if (prop_state == svn_wc_notify_state_conflicted)
+ {
+ alloc_and_store_path(&merge_b->conflicted_paths, local_abspath,
+ merge_b->pool);
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for merge_dir_deleted. Implement svn_wc_status_func4_t */
+static svn_error_t *
+verify_touched_by_del_check(void *baton,
+ const char *local_abspath,
+ const svn_wc_status3_t *status,
+ apr_pool_t *scratch_pool)
+{
+ struct dir_delete_baton_t *delb = baton;
+
+ if (svn_hash_gets(delb->compared_abspaths, local_abspath))
+ return SVN_NO_ERROR;
+
+ switch (status->node_status)
+ {
+ case svn_wc_status_deleted:
+ case svn_wc_status_ignored:
+ case svn_wc_status_none:
+ return SVN_NO_ERROR;
+
+ default:
+ delb->found_edit = TRUE;
+ return svn_error_create(SVN_ERR_CEASE_INVOCATION, NULL, NULL);
+ }
+}
+
+/* An svn_diff_tree_processor_t function.
+ *
+ * Called after merge_dir_opened() when a node existed only in the left source.
+ *
+ * After the merge_dir_opened() but before the call to this merge_dir_deleted()
+ * function all descendants that existed in left_source will have been deleted.
+ *
+ * If this node is replaced, an _opened() followed by a matching _add() will
+ * be invoked after this function.
+ */
+static svn_error_t *
+merge_dir_deleted(const char *relpath,
+ const svn_diff_source_t *left_source,
+ /*const*/ apr_hash_t *left_props,
+ void *dir_baton,
+ const struct svn_diff_tree_processor_t *processor,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t *merge_b = processor->baton;
+ struct merge_dir_baton_t *db = dir_baton;
+ const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
+ relpath, scratch_pool);
+ struct dir_delete_baton_t *delb;
+ svn_boolean_t same;
+ apr_hash_t *working_props;
+
+ SVN_ERR(handle_pending_notifications(merge_b, db, scratch_pool));
+ SVN_ERR(mark_dir_edited(merge_b, db, local_abspath, scratch_pool));
+
+ if (db->shadowed)
+ {
+ if (db->tree_conflict_reason == CONFLICT_REASON_NONE)
+ {
+ /* We haven't notified for this node yet: report a skip */
+ SVN_ERR(record_skip(merge_b, local_abspath, svn_node_dir,
+ svn_wc_notify_update_shadowed_delete,
+ db->skip_reason, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+ }
+
+ /* Easy out: We are only applying mergeinfo differences. */
+ if (merge_b->record_only)
+ {
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR(svn_wc_prop_list2(&working_props,
+ merge_b->ctx->wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+
+ if (merge_b->force_delete)
+ same = TRUE;
+ else
+ {
+ /* Compare the properties */
+ SVN_ERR(properties_same_p(&same, left_props, working_props,
+ scratch_pool));
+ }
+
+ delb = db->delete_state;
+ assert(delb != NULL);
+
+ if (! same)
+ {
+ delb->found_edit = TRUE;
+ }
+ else
+ {
+ store_path(delb->compared_abspaths, local_abspath);
+ }
+
+ if (delb->del_root != db)
+ return SVN_NO_ERROR;
+
+ if (delb->found_edit)
+ same = FALSE;
+ else if (merge_b->force_delete)
+ same = TRUE;
+ else
+ {
+ apr_array_header_t *ignores;
+ svn_error_t *err;
+ same = TRUE;
+
+ SVN_ERR(svn_wc_get_default_ignores(&ignores, merge_b->ctx->config,
+ scratch_pool));
+
+ /* None of the descendants was modified, but maybe there are
+ descendants we haven't walked?
+
+ Note that we aren't interested in changes, as we already verified
+ changes in the paths touched by the merge. And the existance of
+ other paths is enough to mark the directory edited */
+ err = svn_wc_walk_status(merge_b->ctx->wc_ctx, local_abspath,
+ svn_depth_infinity, TRUE /* get-all */,
+ FALSE /* no-ignore */,
+ TRUE /* ignore-text-mods */, ignores,
+ verify_touched_by_del_check, delb,
+ merge_b->ctx->cancel_func,
+ merge_b->ctx->cancel_baton,
+ scratch_pool);
+
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_CEASE_INVOCATION)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ }
+
+ same = ! delb->found_edit;
+ }
+
+ if (same && !merge_b->dry_run)
+ {
+ svn_error_t *err;
+
+ err = svn_wc_delete4(merge_b->ctx->wc_ctx, local_abspath,
+ FALSE /* keep_local */, FALSE /* unversioned */,
+ merge_b->ctx->cancel_func,
+ merge_b->ctx->cancel_baton,
+ NULL, NULL /* no notify */,
+ scratch_pool);
+
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_LEFT_LOCAL_MOD)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ same = FALSE;
+ }
+ }
+
+ if (! same)
+ {
+ /* If the attempt to delete an existing directory failed,
+ * the directory has local modifications (e.g. locally added
+ * files, or property changes). Flag a tree conflict. */
+
+ /* This handles use case 5 described in the paper attached to issue
+ * #2282. See also notes/tree-conflicts/detection.txt
+ */
+ SVN_ERR(record_tree_conflict(merge_b, local_abspath, db->parent_baton,
+ svn_node_dir,
+ svn_wc_conflict_action_delete,
+ svn_wc_conflict_reason_edited,
+ NULL, TRUE,
+ scratch_pool));
+ }
+ else
+ {
+ /* Record that we might have deleted mergeinfo */
+ if (working_props
+ && svn_hash_gets(working_props, SVN_PROP_MERGEINFO))
+ {
+ alloc_and_store_path(&merge_b->paths_with_deleted_mergeinfo,
+ local_abspath, merge_b->pool);
+ }
+
+ SVN_ERR(record_update_delete(merge_b, db->parent_baton, local_abspath,
+ svn_node_dir, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_diff_tree_processor_t function.
+ *
+ * Called after merge_dir_opened() when a node itself didn't change between
+ * the left and right source.
+ *
+ * After the merge_dir_opened() but before the call to this merge_dir_closed()
+ * function all descendants will have been processed.
+ */
+static svn_error_t *
+merge_dir_closed(const char *relpath,
+ const svn_diff_source_t *left_source,
+ const svn_diff_source_t *right_source,
+ void *dir_baton,
+ const struct svn_diff_tree_processor_t *processor,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t *merge_b = processor->baton;
+ struct merge_dir_baton_t *db = dir_baton;
+
+ SVN_ERR(handle_pending_notifications(merge_b, db, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_diff_tree_processor_t function.
+
+ Called when the diff driver wants to report an absent path.
+
+ In case of merges this happens when the diff encounters a server-excluded
+ path.
+
+ We register a skipped path, which will make parent mergeinfo non-
+ inheritable. This ensures that a future merge might see these skipped
+ changes as eligable for merging.
+
+ For legacy reasons we also notify the path as skipped.
+ */
+static svn_error_t *
+merge_node_absent(const char *relpath,
+ void *dir_baton,
+ const svn_diff_tree_processor_t *processor,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t *merge_b = processor->baton;
+
+ const char *local_abspath = svn_dirent_join(merge_b->target->abspath,
+ relpath, scratch_pool);
+
+ SVN_ERR(record_skip(merge_b, local_abspath, svn_node_unknown,
+ svn_wc_notify_skip, svn_wc_notify_state_missing,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/*-----------------------------------------------------------------------*/
+
+/*** Merge Notification ***/
+
+
+/* Finds a nearest ancestor in CHILDREN_WITH_MERGEINFO for LOCAL_ABSPATH. If
+ PATH_IS_OWN_ANCESTOR is TRUE then a child in CHILDREN_WITH_MERGEINFO
+ where child->abspath == PATH is considered PATH's ancestor. If FALSE,
+ then child->abspath must be a proper ancestor of PATH.
+
+ CHILDREN_WITH_MERGEINFO is expected to be sorted in Depth first
+ order of path. */
+static svn_client__merge_path_t *
+find_nearest_ancestor(const apr_array_header_t *children_with_mergeinfo,
+ svn_boolean_t path_is_own_ancestor,
+ const char *local_abspath)
+{
+ int i;
+
+ SVN_ERR_ASSERT_NO_RETURN(children_with_mergeinfo != NULL);
+
+ for (i = children_with_mergeinfo->nelts - 1; i >= 0 ; i--)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *);
+
+ if (svn_dirent_is_ancestor(child->abspath, local_abspath)
+ && (path_is_own_ancestor
+ || strcmp(child->abspath, local_abspath) != 0))
+ return child;
+ }
+ return NULL;
+}
+
+/* Find the highest level path in a merge target (possibly the merge target
+ itself) to use in a merge notification header.
+
+ Return the svn_client__merge_path_t * representing the most distant
+ ancestor in CHILDREN_WITH_MERGEINFO of LOCAL_ABSPATH where said
+ ancestor's first remaining ranges element (per the REMAINING_RANGES
+ member of the ancestor) intersect with the first remaining ranges element
+ for every intermediate ancestor svn_client__merge_path_t * of
+ LOCAL_ABSPATH. If no such ancestor is found return NULL.
+
+ If the remaining ranges of the elements in CHILDREN_WITH_MERGEINFO
+ represent a forward merge, then set *START to the oldest revision found
+ in any of the intersecting ancestors and *END to the youngest revision
+ found. If the remaining ranges of the elements in CHILDREN_WITH_MERGEINFO
+ represent a reverse merge, then set *START to the youngest revision
+ found and *END to the oldest revision found. If no ancestors are found
+ then set *START and *END to SVN_INVALID_REVNUM.
+
+ If PATH_IS_OWN_ANCESTOR is TRUE then a child in CHILDREN_WITH_MERGEINFO
+ where child->abspath == PATH is considered PATH's ancestor. If FALSE,
+ then child->abspath must be a proper ancestor of PATH.
+
+ See the CHILDREN_WITH_MERGEINFO ARRAY global comment for more
+ information. */
+static svn_client__merge_path_t *
+find_nearest_ancestor_with_intersecting_ranges(
+ svn_revnum_t *start,
+ svn_revnum_t *end,
+ const apr_array_header_t *children_with_mergeinfo,
+ svn_boolean_t path_is_own_ancestor,
+ const char *local_abspath)
+{
+ int i;
+ svn_client__merge_path_t *nearest_ancestor = NULL;
+
+ *start = SVN_INVALID_REVNUM;
+ *end = SVN_INVALID_REVNUM;
+
+ SVN_ERR_ASSERT_NO_RETURN(children_with_mergeinfo != NULL);
+
+ for (i = children_with_mergeinfo->nelts - 1; i >= 0 ; i--)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *);
+
+ if (svn_dirent_is_ancestor(child->abspath, local_abspath)
+ && (path_is_own_ancestor
+ || strcmp(child->abspath, local_abspath) != 0))
+ {
+ if (nearest_ancestor == NULL)
+ {
+ /* Found an ancestor. */
+ nearest_ancestor = child;
+
+ if (child->remaining_ranges)
+ {
+ svn_merge_range_t *r1 = APR_ARRAY_IDX(
+ child->remaining_ranges, 0, svn_merge_range_t *);
+ *start = r1->start;
+ *end = r1->end;
+ }
+ else
+ {
+ /* If CHILD->REMAINING_RANGES is null then LOCAL_ABSPATH
+ is inside an absent subtree in the merge target. */
+ *start = SVN_INVALID_REVNUM;
+ *end = SVN_INVALID_REVNUM;
+ break;
+ }
+ }
+ else
+ {
+ /* We'e found another ancestor for LOCAL_ABSPATH. Do its
+ first remaining range intersect with the previously
+ found ancestor? */
+ svn_merge_range_t *r1 =
+ APR_ARRAY_IDX(nearest_ancestor->remaining_ranges, 0,
+ svn_merge_range_t *);
+ svn_merge_range_t *r2 =
+ APR_ARRAY_IDX(child->remaining_ranges, 0,
+ svn_merge_range_t *);
+
+ if (r1 && r2)
+ {
+ svn_merge_range_t range1;
+ svn_merge_range_t range2;
+ svn_boolean_t reverse_merge = r1->start > r2->end;
+
+ /* Flip endpoints if this is a reverse merge. */
+ if (reverse_merge)
+ {
+ range1.start = r1->end;
+ range1.end = r1->start;
+ range2.start = r2->end;
+ range2.end = r2->start;
+ }
+ else
+ {
+ range1.start = r1->start;
+ range1.end = r1->end;
+ range2.start = r2->start;
+ range2.end = r2->end;
+ }
+
+ if (range1.start < range2.end && range2.start < range1.end)
+ {
+ *start = reverse_merge ?
+ MAX(r1->start, r2->start) : MIN(r1->start, r2->start);
+ *end = reverse_merge ?
+ MIN(r1->end, r2->end) : MAX(r1->end, r2->end);
+ nearest_ancestor = child;
+ }
+ }
+ }
+ }
+ }
+ return nearest_ancestor;
+}
+
+/* Notify that we're starting to record mergeinfo for the merge of the
+ * revision range RANGE into TARGET_ABSPATH. RANGE should be null if the
+ * merge sources are not from the same URL.
+ *
+ * This calls the client's notification receiver (as found in the client
+ * context), with a WC abspath.
+ */
+static void
+notify_mergeinfo_recording(const char *target_abspath,
+ const svn_merge_range_t *range,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *n = svn_wc_create_notify(
+ target_abspath, svn_wc_notify_merge_record_info_begin, pool);
+
+ n->merge_range = range ? svn_merge_range_dup(range, pool) : NULL;
+ ctx->notify_func2(ctx->notify_baton2, n, pool);
+ }
+}
+
+/* Notify that we're completing the merge into TARGET_ABSPATH.
+ *
+ * This calls the client's notification receiver (as found in the client
+ * context), with a WC abspath.
+ */
+static void
+notify_merge_completed(const char *target_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *n
+ = svn_wc_create_notify(target_abspath, svn_wc_notify_merge_completed,
+ pool);
+ ctx->notify_func2(ctx->notify_baton2, n, pool);
+ }
+}
+
+/* Is the notification the result of a real operative merge? */
+#define IS_OPERATIVE_NOTIFICATION(notify) \
+ (notify->content_state == svn_wc_notify_state_conflicted \
+ || notify->content_state == svn_wc_notify_state_merged \
+ || notify->content_state == svn_wc_notify_state_changed \
+ || notify->prop_state == svn_wc_notify_state_conflicted \
+ || notify->prop_state == svn_wc_notify_state_merged \
+ || notify->prop_state == svn_wc_notify_state_changed \
+ || notify->action == svn_wc_notify_update_add \
+ || notify->action == svn_wc_notify_tree_conflict)
+
+
+/* Remove merge source gaps from range used for merge notifications.
+ See http://subversion.tigris.org/issues/show_bug.cgi?id=4138
+
+ If IMPLICIT_SRC_GAP is not NULL then it is a rangelist containing a
+ single range (see the implicit_src_gap member of merge_cmd_baton_t).
+ RANGE describes a (possibly reverse) merge.
+
+ If IMPLICIT_SRC_GAP is not NULL and it's sole range intersects with
+ the older revision in *RANGE, then remove IMPLICIT_SRC_GAP's range
+ from *RANGE. */
+static void
+remove_source_gap(svn_merge_range_t *range,
+ apr_array_header_t *implicit_src_gap)
+{
+ if (implicit_src_gap)
+ {
+ svn_merge_range_t *gap_range =
+ APR_ARRAY_IDX(implicit_src_gap, 0, svn_merge_range_t *);
+ if (range->start < range->end)
+ {
+ if (gap_range->start == range->start)
+ range->start = gap_range->end;
+ }
+ else /* Reverse merge */
+ {
+ if (gap_range->start == range->end)
+ range->end = gap_range->end;
+ }
+ }
+}
+
+/* Notify that we're starting a merge
+ *
+ * This calls the client's notification receiver (as found in the client
+ * context), with a WC abspath.
+ */
+static svn_error_t *
+notify_merge_begin(merge_cmd_baton_t *merge_b,
+ const char *local_abspath,
+ svn_boolean_t delete_action,
+ apr_pool_t *scratch_pool)
+{
+ svn_wc_notify_t *notify;
+ svn_merge_range_t n_range =
+ {SVN_INVALID_REVNUM, SVN_INVALID_REVNUM, TRUE};
+ const char *notify_abspath;
+
+ if (! merge_b->ctx->notify_func2)
+ return SVN_NO_ERROR;
+
+ /* If our merge sources are ancestors of one another... */
+ if (merge_b->merge_source.ancestral)
+ {
+ const svn_client__merge_path_t *child;
+ /* Find NOTIFY->PATH's nearest ancestor in
+ NOTIFY->CHILDREN_WITH_MERGEINFO. Normally we consider a child in
+ NOTIFY->CHILDREN_WITH_MERGEINFO representing PATH to be an
+ ancestor of PATH, but if this is a deletion of PATH then the
+ notification must be for a proper ancestor of PATH. This ensures
+ we don't get notifications like:
+
+ --- Merging rX into 'PARENT/CHILD'
+ D PARENT/CHILD
+
+ But rather:
+
+ --- Merging rX into 'PARENT'
+ D PARENT/CHILD
+ */
+
+ child = find_nearest_ancestor_with_intersecting_ranges(
+ &(n_range.start), &(n_range.end),
+ merge_b->notify_begin.nodes_with_mergeinfo,
+ ! delete_action, local_abspath);
+
+ if (!child && delete_action)
+ {
+ /* Triggered by file replace in single-file-merge */
+ child = find_nearest_ancestor(merge_b->notify_begin.nodes_with_mergeinfo,
+ TRUE, local_abspath);
+ }
+
+ assert(child != NULL); /* Should always find the merge anchor */
+
+ if (! child)
+ return SVN_NO_ERROR;
+
+ if (merge_b->notify_begin.last_abspath != NULL
+ && strcmp(child->abspath, merge_b->notify_begin.last_abspath) == 0)
+ {
+ /* Don't notify the same merge again */
+ return SVN_NO_ERROR;
+ }
+
+ merge_b->notify_begin.last_abspath = child->abspath;
+
+ if (child->absent || child->remaining_ranges->nelts == 0
+ || !SVN_IS_VALID_REVNUM(n_range.start))
+ {
+ /* No valid information for an header */
+ return SVN_NO_ERROR;
+ }
+
+ notify_abspath = child->abspath;
+ }
+ else
+ {
+ if (merge_b->notify_begin.last_abspath)
+ return SVN_NO_ERROR; /* already notified */
+
+ notify_abspath = merge_b->target->abspath;
+ /* Store something in last_abspath. Any value would do */
+ merge_b->notify_begin.last_abspath = merge_b->target->abspath;
+ }
+
+ notify = svn_wc_create_notify(notify_abspath,
+ merge_b->same_repos
+ ? svn_wc_notify_merge_begin
+ : svn_wc_notify_foreign_merge_begin,
+ scratch_pool);
+
+ if (SVN_IS_VALID_REVNUM(n_range.start))
+ {
+ /* If the merge source has a gap, then don't mention
+ those gap revisions in the notification. */
+ remove_source_gap(&n_range, merge_b->implicit_src_gap);
+ notify->merge_range = &n_range;
+ }
+ else
+ {
+ notify->merge_range = NULL;
+ }
+
+ (*merge_b->ctx->notify_func2)(merge_b->ctx->notify_baton2, notify,
+ scratch_pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Set *OUT_RANGELIST to the intersection of IN_RANGELIST with the simple
+ * (inheritable) revision range REV1:REV2, according to CONSIDER_INHERITANCE.
+ * If REV1 is equal to REV2, the result is an empty rangelist, otherwise
+ * REV1 must be less than REV2.
+ *
+ * Note: If CONSIDER_INHERITANCE is FALSE, the effect is to treat any non-
+ * inheritable input ranges as if they were inheritable. If it is TRUE, the
+ * effect is to discard any non-inheritable input ranges. Therefore the
+ * ranges in *OUT_RANGELIST will always be inheritable. */
+static svn_error_t *
+rangelist_intersect_range(svn_rangelist_t **out_rangelist,
+ const svn_rangelist_t *in_rangelist,
+ svn_revnum_t rev1,
+ svn_revnum_t rev2,
+ svn_boolean_t consider_inheritance,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ SVN_ERR_ASSERT(rev1 <= rev2);
+
+ if (rev1 < rev2)
+ {
+ svn_rangelist_t *simple_rangelist =
+ svn_rangelist__initialize(rev1, rev2, TRUE, scratch_pool);
+
+ SVN_ERR(svn_rangelist_intersect(out_rangelist,
+ simple_rangelist, in_rangelist,
+ consider_inheritance, result_pool));
+ }
+ else
+ {
+ *out_rangelist = apr_array_make(result_pool, 0,
+ sizeof(svn_merge_range_t *));
+ }
+ return SVN_NO_ERROR;
+}
+
+/* Helper for fix_deleted_subtree_ranges(). Like fix_deleted_subtree_ranges()
+ this function should only be called when honoring mergeinfo.
+
+ CHILD, PARENT, REVISION1, REVISION2, and RA_SESSION are all cascaded from
+ fix_deleted_subtree_ranges() -- see that function for more information on
+ each.
+
+ If PARENT is not the merge target then PARENT must have already have been
+ processed by this function as a child. Specifically, this means that
+ PARENT->REMAINING_RANGES must already be populated -- it can be an empty
+ rangelist but cannot be NULL.
+
+ PRIMARY_URL is the merge source url of CHILD at the younger of REVISION1
+ and REVISION2.
+
+ Since this function is only invoked for subtrees of the merge target, the
+ guarantees afforded by normalize_merge_sources() don't apply - see the
+ 'MERGEINFO MERGE SOURCE NORMALIZATION' comment at the top of this file.
+ Therefore it is possible that PRIMARY_URL@REVISION1 and
+ PRIMARY_URL@REVISION2 don't describe the endpoints of an unbroken line of
+ history. The purpose of this helper is to identify these cases of broken
+ history and adjust CHILD->REMAINING_RANGES in such a way we don't later try
+ to describe nonexistent path/revisions to the merge report editor -- see
+ drive_merge_report_editor().
+
+ If PRIMARY_URL@REVISION1 and PRIMARY_URL@REVISION2 describe an unbroken
+ line of history then do nothing and leave CHILD->REMAINING_RANGES as-is.
+
+ If neither PRIMARY_URL@REVISION1 nor PRIMARY_URL@REVISION2 exist then
+ there is nothing to merge to CHILD->ABSPATH so set CHILD->REMAINING_RANGES
+ equal to PARENT->REMAINING_RANGES. This will cause the subtree to
+ effectively ignore CHILD -- see 'Note: If the first svn_merge_range_t...'
+ in drive_merge_report_editor()'s doc string.
+
+ If PRIMARY_URL@REVISION1 *xor* PRIMARY_URL@REVISION2 exist then we take the
+ subset of REVISION1:REVISION2 in CHILD->REMAINING_RANGES at which
+ PRIMARY_URL doesn't exist and set that subset equal to
+ PARENT->REMAINING_RANGES' intersection with that non-existent range. Why?
+ Because this causes CHILD->REMAINING_RANGES to be identical to
+ PARENT->REMAINING_RANGES for revisions between REVISION1 and REVISION2 at
+ which PRIMARY_URL doesn't exist. As mentioned above this means that
+ drive_merge_report_editor() won't attempt to describe these non-existent
+ subtree path/ranges to the reporter (which would break the merge).
+
+ If the preceding paragraph wasn't terribly clear then what follows spells
+ out this function's behavior a bit more explicitly:
+
+ For forward merges (REVISION1 < REVISION2)
+
+ If PRIMARY_URL@REVISION1 exists but PRIMARY_URL@REVISION2 doesn't, then
+ find the revision 'N' in which PRIMARY_URL@REVISION1 was deleted. Leave
+ the subset of CHILD->REMAINING_RANGES that intersects with
+ REVISION1:(N - 1) as-is and set the subset of CHILD->REMAINING_RANGES
+ that intersects with (N - 1):REVISION2 equal to PARENT->REMAINING_RANGES'
+ intersection with (N - 1):REVISION2.
+
+ If PRIMARY_URL@REVISION1 doesn't exist but PRIMARY_URL@REVISION2 does,
+ then find the revision 'M' in which PRIMARY_URL@REVISION2 came into
+ existence. Leave the subset of CHILD->REMAINING_RANGES that intersects with
+ (M - 1):REVISION2 as-is and set the subset of CHILD->REMAINING_RANGES
+ that intersects with REVISION1:(M - 1) equal to PARENT->REMAINING_RANGES'
+ intersection with REVISION1:(M - 1).
+
+ For reverse merges (REVISION1 > REVISION2)
+
+ If PRIMARY_URL@REVISION1 exists but PRIMARY_URL@REVISION2 doesn't, then
+ find the revision 'N' in which PRIMARY_URL@REVISION1 came into existence.
+ Leave the subset of CHILD->REMAINING_RANGES that intersects with
+ REVISION2:(N - 1) as-is and set the subset of CHILD->REMAINING_RANGES
+ that intersects with (N - 1):REVISION1 equal to PARENT->REMAINING_RANGES'
+ intersection with (N - 1):REVISION1.
+
+ If PRIMARY_URL@REVISION1 doesn't exist but PRIMARY_URL@REVISION2 does,
+ then find the revision 'M' in which PRIMARY_URL@REVISION2 came into
+ existence. Leave the subset of CHILD->REMAINING_RANGES that intersects with
+ REVISION2:(M - 1) as-is and set the subset of CHILD->REMAINING_RANGES
+ that intersects with (M - 1):REVISION1 equal to PARENT->REMAINING_RANGES'
+ intersection with REVISION1:(M - 1).
+
+ SCRATCH_POOL is used for all temporary allocations. Changes to CHILD are
+ allocated in RESULT_POOL. */
+static svn_error_t *
+adjust_deleted_subtree_ranges(svn_client__merge_path_t *child,
+ svn_client__merge_path_t *parent,
+ svn_revnum_t revision1,
+ svn_revnum_t revision2,
+ const char *primary_url,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_boolean_t is_rollback = revision2 < revision1;
+ svn_revnum_t younger_rev = is_rollback ? revision1 : revision2;
+ svn_revnum_t peg_rev = younger_rev;
+ svn_revnum_t older_rev = is_rollback ? revision2 : revision1;
+ apr_array_header_t *segments;
+ svn_error_t *err;
+
+ SVN_ERR_ASSERT(parent->remaining_ranges);
+
+ err = svn_client__repos_location_segments(&segments, ra_session,
+ primary_url, peg_rev,
+ younger_rev, older_rev, ctx,
+ scratch_pool);
+
+ /* If PRIMARY_URL@peg_rev doesn't exist then
+ svn_client__repos_location_segments() typically returns an
+ SVN_ERR_FS_NOT_FOUND error, but if it doesn't exist for a
+ forward merge over ra_neon then we get SVN_ERR_RA_DAV_REQUEST_FAILED.
+ http://subversion.tigris.org/issues/show_bug.cgi?id=3137 fixed some of
+ the cases where different RA layers returned different error codes to
+ signal the "path not found"...but it looks like there is more to do.
+
+ ### Do we still need to special case for ra_neon (since it no longer
+ exists)? */
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_FS_NOT_FOUND
+ || err->apr_err == SVN_ERR_RA_DAV_REQUEST_FAILED)
+ {
+ /* PRIMARY_URL@peg_rev doesn't exist. Check if PRIMARY_URL@older_rev
+ exists, if neither exist then the editor can simply ignore this
+ subtree. */
+ const char *rel_source_path; /* PRIMARY_URL relative to RA_SESSION */
+ svn_node_kind_t kind;
+
+ svn_error_clear(err);
+ err = NULL;
+
+ SVN_ERR(svn_ra_get_path_relative_to_session(
+ ra_session, &rel_source_path, primary_url, scratch_pool));
+
+ SVN_ERR(svn_ra_check_path(ra_session, rel_source_path,
+ older_rev, &kind, scratch_pool));
+ if (kind == svn_node_none)
+ {
+ /* Neither PRIMARY_URL@peg_rev nor PRIMARY_URL@older_rev exist,
+ so there is nothing to merge. Set CHILD->REMAINING_RANGES
+ identical to PARENT's. */
+ child->remaining_ranges =
+ svn_rangelist_dup(parent->remaining_ranges, scratch_pool);
+ }
+ else
+ {
+ svn_rangelist_t *deleted_rangelist;
+ svn_revnum_t rev_primary_url_deleted;
+
+ /* PRIMARY_URL@older_rev exists, so it was deleted at some
+ revision prior to peg_rev, find that revision. */
+ SVN_ERR(svn_ra_get_deleted_rev(ra_session, rel_source_path,
+ older_rev, younger_rev,
+ &rev_primary_url_deleted,
+ scratch_pool));
+
+ /* PRIMARY_URL@older_rev exists and PRIMARY_URL@peg_rev doesn't,
+ so svn_ra_get_deleted_rev() should always find the revision
+ PRIMARY_URL@older_rev was deleted. */
+ SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(rev_primary_url_deleted));
+
+ /* If this is a reverse merge reorder CHILD->REMAINING_RANGES and
+ PARENT->REMAINING_RANGES so both will work with the
+ svn_rangelist_* APIs below. */
+ if (is_rollback)
+ {
+ /* svn_rangelist_reverse operates in place so it's safe
+ to use our scratch_pool. */
+ SVN_ERR(svn_rangelist_reverse(child->remaining_ranges,
+ scratch_pool));
+ SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges,
+ scratch_pool));
+ }
+
+ /* Find the intersection of CHILD->REMAINING_RANGES with the
+ range over which PRIMARY_URL@older_rev exists (ending at
+ the youngest revision at which it still exists). */
+ SVN_ERR(rangelist_intersect_range(&child->remaining_ranges,
+ child->remaining_ranges,
+ older_rev,
+ rev_primary_url_deleted - 1,
+ FALSE,
+ scratch_pool, scratch_pool));
+
+ /* Merge into CHILD->REMAINING_RANGES the intersection of
+ PARENT->REMAINING_RANGES with the range beginning when
+ PRIMARY_URL@older_rev was deleted until younger_rev. */
+ SVN_ERR(rangelist_intersect_range(&deleted_rangelist,
+ parent->remaining_ranges,
+ rev_primary_url_deleted - 1,
+ peg_rev,
+ FALSE,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_rangelist_merge2(child->remaining_ranges,
+ deleted_rangelist, scratch_pool,
+ scratch_pool));
+
+ /* Return CHILD->REMAINING_RANGES and PARENT->REMAINING_RANGES
+ to reverse order if necessary. */
+ if (is_rollback)
+ {
+ SVN_ERR(svn_rangelist_reverse(child->remaining_ranges,
+ scratch_pool));
+ SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges,
+ scratch_pool));
+ }
+ }
+ }
+ else
+ {
+ return svn_error_trace(err);
+ }
+ }
+ else /* PRIMARY_URL@peg_rev exists. */
+ {
+ svn_rangelist_t *non_existent_rangelist;
+ svn_location_segment_t *segment =
+ APR_ARRAY_IDX(segments, (segments->nelts - 1),
+ svn_location_segment_t *);
+
+ /* We know PRIMARY_URL@peg_rev exists as the call to
+ svn_client__repos_location_segments() succeeded. If there is only
+ one segment that starts at oldest_rev then we know that
+ PRIMARY_URL@oldest_rev:PRIMARY_URL@peg_rev describes an unbroken
+ line of history, so there is nothing more to adjust in
+ CHILD->REMAINING_RANGES. */
+ if (segment->range_start == older_rev)
+ {
+ return SVN_NO_ERROR;
+ }
+
+ /* If this is a reverse merge reorder CHILD->REMAINING_RANGES and
+ PARENT->REMAINING_RANGES so both will work with the
+ svn_rangelist_* APIs below. */
+ if (is_rollback)
+ {
+ SVN_ERR(svn_rangelist_reverse(child->remaining_ranges,
+ scratch_pool));
+ SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges,
+ scratch_pool));
+ }
+
+ /* Intersect CHILD->REMAINING_RANGES with the range where PRIMARY_URL
+ exists. Since segment doesn't span older_rev:peg_rev we know
+ PRIMARY_URL@peg_rev didn't come into existence until
+ segment->range_start + 1. */
+ SVN_ERR(rangelist_intersect_range(&child->remaining_ranges,
+ child->remaining_ranges,
+ segment->range_start, peg_rev,
+ FALSE, scratch_pool, scratch_pool));
+
+ /* Merge into CHILD->REMAINING_RANGES the intersection of
+ PARENT->REMAINING_RANGES with the range before PRIMARY_URL@peg_rev
+ came into existence. */
+ SVN_ERR(rangelist_intersect_range(&non_existent_rangelist,
+ parent->remaining_ranges,
+ older_rev, segment->range_start,
+ FALSE, scratch_pool, scratch_pool));
+ SVN_ERR(svn_rangelist_merge2(child->remaining_ranges,
+ non_existent_rangelist, scratch_pool,
+ scratch_pool));
+
+ /* Return CHILD->REMAINING_RANGES and PARENT->REMAINING_RANGES
+ to reverse order if necessary. */
+ if (is_rollback)
+ {
+ SVN_ERR(svn_rangelist_reverse(child->remaining_ranges,
+ scratch_pool));
+ SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges,
+ scratch_pool));
+ }
+ }
+
+ /* Make a lasting copy of CHILD->REMAINING_RANGES using POOL. */
+ child->remaining_ranges = svn_rangelist_dup(child->remaining_ranges,
+ result_pool);
+ return SVN_NO_ERROR;
+}
+
+/* Helper for do_directory_merge().
+
+ SOURCE is cascaded from the argument of the same name in
+ do_directory_merge(). TARGET is the merge target. RA_SESSION is the
+ session for the younger of SOURCE->loc1 and SOURCE->loc2.
+
+ Adjust the subtrees in CHILDREN_WITH_MERGEINFO so that we don't
+ later try to describe invalid paths in drive_merge_report_editor().
+ This function is just a thin wrapper around
+ adjust_deleted_subtree_ranges(), which see for further details.
+
+ SCRATCH_POOL is used for all temporary allocations. Changes to
+ CHILDREN_WITH_MERGEINFO are allocated in RESULT_POOL.
+*/
+static svn_error_t *
+fix_deleted_subtree_ranges(const merge_source_t *source,
+ const merge_target_t *target,
+ svn_ra_session_t *ra_session,
+ apr_array_header_t *children_with_mergeinfo,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ svn_boolean_t is_rollback = source->loc2->rev < source->loc1->rev;
+
+ assert(session_url_is(ra_session,
+ (is_rollback ? source->loc1 : source->loc2)->url,
+ scratch_pool));
+
+ /* CHILDREN_WITH_MERGEINFO is sorted in depth-first order, so
+ start at index 1 to examine only subtrees. */
+ for (i = 1; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *);
+ svn_client__merge_path_t *parent;
+ svn_rangelist_t *deleted_rangelist, *added_rangelist;
+
+ SVN_ERR_ASSERT(child);
+ if (child->absent)
+ continue;
+
+ svn_pool_clear(iterpool);
+
+ /* Find CHILD's parent. */
+ parent = find_nearest_ancestor(children_with_mergeinfo,
+ FALSE, child->abspath);
+
+ /* Since CHILD is a subtree then its parent must be in
+ CHILDREN_WITH_MERGEINFO, see the global comment
+ 'THE CHILDREN_WITH_MERGEINFO ARRAY'. */
+ SVN_ERR_ASSERT(parent);
+
+ /* If this is a reverse merge reorder CHILD->REMAINING_RANGES
+ so it will work with the svn_rangelist_diff API. */
+ if (is_rollback)
+ {
+ SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, iterpool));
+ SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges, iterpool));
+ }
+
+ SVN_ERR(svn_rangelist_diff(&deleted_rangelist, &added_rangelist,
+ child->remaining_ranges,
+ parent->remaining_ranges,
+ TRUE, iterpool));
+
+ if (is_rollback)
+ {
+ SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, iterpool));
+ SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges, iterpool));
+ }
+
+ /* If CHILD is the merge target we then know that SOURCE is provided
+ by normalize_merge_sources() -- see 'MERGEINFO MERGE SOURCE
+ NORMALIZATION'. Due to this normalization we know that SOURCE
+ describes an unbroken line of history such that the entire range
+ described by SOURCE can potentially be merged to CHILD.
+
+ But if CHILD is a subtree we don't have the same guarantees about
+ SOURCE as we do for the merge target. SOURCE->loc1 and/or
+ SOURCE->loc2 might not exist.
+
+ If one or both doesn't exist, then adjust CHILD->REMAINING_RANGES
+ such that we don't later try to describe invalid subtrees in
+ drive_merge_report_editor(), as that will break the merge.
+ If CHILD has the same remaining ranges as PARENT however, then
+ there is no need to make these adjustments, since
+ drive_merge_report_editor() won't attempt to describe CHILD in this
+ case, see the 'Note' in drive_merge_report_editor's docstring. */
+ if (deleted_rangelist->nelts || added_rangelist->nelts)
+ {
+ const char *child_primary_source_url;
+ const char *child_repos_src_path =
+ svn_dirent_is_child(target->abspath, child->abspath, iterpool);
+
+ /* This loop is only processing subtrees, so CHILD->ABSPATH
+ better be a proper child of the merge target. */
+ SVN_ERR_ASSERT(child_repos_src_path);
+
+ child_primary_source_url =
+ svn_path_url_add_component2((source->loc1->rev < source->loc2->rev)
+ ? source->loc2->url : source->loc1->url,
+ child_repos_src_path, iterpool);
+
+ SVN_ERR(adjust_deleted_subtree_ranges(child, parent,
+ source->loc1->rev,
+ source->loc2->rev,
+ child_primary_source_url,
+ ra_session,
+ ctx, result_pool, iterpool));
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/*-----------------------------------------------------------------------*/
+
+/*** Determining What Remains To Be Merged ***/
+
+/* Get explicit and/or implicit mergeinfo for the working copy path
+ TARGET_ABSPATH.
+
+ If RECORDED_MERGEINFO is not NULL then set *RECORDED_MERGEINFO
+ to TARGET_ABSPATH's explicit or inherited mergeinfo as dictated by
+ INHERIT.
+
+ If IMPLICIT_MERGEINFO is not NULL then set *IMPLICIT_MERGEINFO
+ to TARGET_ABSPATH's implicit mergeinfo (a.k.a. natural history).
+
+ If both RECORDED_MERGEINFO and IMPLICIT_MERGEINFO are not NULL and
+ *RECORDED_MERGEINFO is inherited, then *IMPLICIT_MERGEINFO will be
+ removed from *RECORDED_MERGEINFO.
+
+ If INHERITED is not NULL set *INHERITED to TRUE if *RECORDED_MERGEINFO
+ is inherited rather than explicit. If RECORDED_MERGEINFO is NULL then
+ INHERITED is ignored.
+
+
+ If IMPLICIT_MERGEINFO is not NULL then START and END are limits on
+ the natural history sought, must both be valid revision numbers, and
+ START must be greater than END. If TARGET_ABSPATH's base revision
+ is older than START, then the base revision is used as the younger
+ bound in place of START.
+
+ RA_SESSION is an RA session open to the repository in which TARGET_ABSPATH
+ lives. It may be temporarily reparented as needed by this function.
+
+ Allocate *RECORDED_MERGEINFO and *IMPLICIT_MERGEINFO in RESULT_POOL.
+ Use SCRATCH_POOL for any temporary allocations. */
+static svn_error_t *
+get_full_mergeinfo(svn_mergeinfo_t *recorded_mergeinfo,
+ svn_mergeinfo_t *implicit_mergeinfo,
+ svn_boolean_t *inherited,
+ svn_mergeinfo_inheritance_t inherit,
+ svn_ra_session_t *ra_session,
+ const char *target_abspath,
+ svn_revnum_t start,
+ svn_revnum_t end,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ /* First, we get the real mergeinfo. */
+ if (recorded_mergeinfo)
+ {
+ SVN_ERR(svn_client__get_wc_or_repos_mergeinfo(recorded_mergeinfo,
+ inherited,
+ NULL /* from_repos */,
+ FALSE,
+ inherit, ra_session,
+ target_abspath,
+ ctx, result_pool));
+ }
+
+ if (implicit_mergeinfo)
+ {
+ svn_client__pathrev_t *target;
+
+ /* Assert that we have sane input. */
+ SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(start) && SVN_IS_VALID_REVNUM(end)
+ && (start > end));
+
+ /* Retrieve the origin (original_*) of the node, or just the
+ url if the node was not copied. */
+ SVN_ERR(svn_client__wc_node_get_origin(&target, target_abspath, ctx,
+ scratch_pool, scratch_pool));
+
+ if (! target)
+ {
+ /* We've been asked to operate on a locally added target, so its
+ * implicit mergeinfo is empty. */
+ *implicit_mergeinfo = apr_hash_make(result_pool);
+ }
+ else if (target->rev <= end)
+ {
+ /* We're asking about a range outside our natural history
+ altogether. That means our implicit mergeinfo is empty. */
+ *implicit_mergeinfo = apr_hash_make(result_pool);
+ }
+ else
+ {
+ /* Fetch so-called "implicit mergeinfo" (that is, natural
+ history). */
+
+ /* Do not ask for implicit mergeinfo from TARGET_ABSPATH's future.
+ TARGET_ABSPATH might not even exist, and even if it does the
+ working copy is *at* TARGET_REV so its implicit history ends
+ at TARGET_REV! */
+ if (target->rev < start)
+ start = target->rev;
+
+ /* Fetch the implicit mergeinfo. */
+ SVN_ERR(svn_client__get_history_as_mergeinfo(implicit_mergeinfo,
+ NULL,
+ target, start, end,
+ ra_session, ctx,
+ result_pool));
+ }
+ } /*if (implicit_mergeinfo) */
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for ensure_implicit_mergeinfo().
+
+ PARENT, CHILD, REVISION1, REVISION2 and CTX
+ are all cascaded from the arguments of the same names in
+ ensure_implicit_mergeinfo(). PARENT and CHILD must both exist, i.e.
+ this function should never be called where CHILD is the merge target.
+
+ If PARENT->IMPLICIT_MERGEINFO is NULL, obtain it from the server.
+
+ Set CHILD->IMPLICIT_MERGEINFO to the mergeinfo inherited from
+ PARENT->IMPLICIT_MERGEINFO. CHILD->IMPLICIT_MERGEINFO is allocated
+ in RESULT_POOL.
+
+ RA_SESSION is an RA session open to the repository that contains CHILD.
+ It may be temporarily reparented by this function.
+ */
+static svn_error_t *
+inherit_implicit_mergeinfo_from_parent(svn_client__merge_path_t *parent,
+ svn_client__merge_path_t *child,
+ svn_revnum_t revision1,
+ svn_revnum_t revision2,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *path_diff;
+
+ /* This only works on subtrees! */
+ SVN_ERR_ASSERT(parent);
+ SVN_ERR_ASSERT(child);
+
+ /* While PARENT must exist, it is possible we've deferred
+ getting its implicit mergeinfo. If so get it now. */
+ if (!parent->implicit_mergeinfo)
+ SVN_ERR(get_full_mergeinfo(NULL, &(parent->implicit_mergeinfo),
+ NULL, svn_mergeinfo_inherited,
+ ra_session, child->abspath,
+ MAX(revision1, revision2),
+ MIN(revision1, revision2),
+ ctx, result_pool, scratch_pool));
+
+ /* Let CHILD inherit PARENT's implicit mergeinfo. */
+
+ path_diff = svn_dirent_is_child(parent->abspath, child->abspath,
+ scratch_pool);
+ /* PARENT->PATH better be an ancestor of CHILD->ABSPATH! */
+ SVN_ERR_ASSERT(path_diff);
+
+ SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(
+ &child->implicit_mergeinfo, parent->implicit_mergeinfo,
+ path_diff, result_pool, scratch_pool));
+ child->implicit_mergeinfo = svn_mergeinfo_dup(child->implicit_mergeinfo,
+ result_pool);
+ return SVN_NO_ERROR;
+}
+
+/* Helper of filter_merged_revisions().
+
+ If we have deferred obtaining CHILD->IMPLICIT_MERGEINFO, then get
+ it now, allocating it in RESULT_POOL. If CHILD_INHERITS_PARENT is true
+ then set CHILD->IMPLICIT_MERGEINFO to the mergeinfo inherited from
+ PARENT->IMPLICIT_MERGEINFO, otherwise contact the repository. Use
+ SCRATCH_POOL for all temporary allocations.
+
+ RA_SESSION is an RA session open to the repository that contains CHILD.
+ It may be temporarily reparented by this function.
+
+ PARENT, CHILD, REVISION1, REVISION2 and
+ CTX are all cascaded from the arguments of the same name in
+ filter_merged_revisions() and the same conditions for that function
+ hold here. */
+static svn_error_t *
+ensure_implicit_mergeinfo(svn_client__merge_path_t *parent,
+ svn_client__merge_path_t *child,
+ svn_boolean_t child_inherits_parent,
+ svn_revnum_t revision1,
+ svn_revnum_t revision2,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ /* If we haven't already found CHILD->IMPLICIT_MERGEINFO then
+ contact the server to get it. */
+
+ if (child->implicit_mergeinfo)
+ return SVN_NO_ERROR;
+
+ if (child_inherits_parent)
+ SVN_ERR(inherit_implicit_mergeinfo_from_parent(parent,
+ child,
+ revision1,
+ revision2,
+ ra_session,
+ ctx,
+ result_pool,
+ scratch_pool));
+ else
+ SVN_ERR(get_full_mergeinfo(NULL,
+ &(child->implicit_mergeinfo),
+ NULL, svn_mergeinfo_inherited,
+ ra_session, child->abspath,
+ MAX(revision1, revision2),
+ MIN(revision1, revision2),
+ ctx, result_pool, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for calculate_remaining_ranges().
+
+ Initialize CHILD->REMAINING_RANGES to a rangelist representing the
+ requested merge of REVISION1:REVISION2 from MERGEINFO_PATH to
+ CHILD->ABSPATH.
+
+ For forward merges remove any ranges from CHILD->REMAINING_RANGES that
+ have already been merged to CHILD->ABSPATH per TARGET_MERGEINFO or
+ CHILD->IMPLICIT_MERGEINFO. For reverse merges remove any ranges from
+ CHILD->REMAINING_RANGES that have not already been merged to CHILD->ABSPATH
+ per TARGET_MERGEINFO or CHILD->IMPLICIT_MERGEINFO. If we have deferred
+ obtaining CHILD->IMPLICIT_MERGEINFO and it is necessary to use it for
+ these calculations, then get it from the server, allocating it in
+ RESULT_POOL.
+
+ CHILD represents a working copy path which is the merge target or one of
+ the target's subtrees. If not NULL, PARENT is CHILD's nearest path-wise
+ ancestor - see 'THE CHILDREN_WITH_MERGEINFO ARRAY'.
+
+ If the function needs to consider CHILD->IMPLICIT_MERGEINFO and
+ CHILD_INHERITS_IMPLICIT is true, then set CHILD->IMPLICIT_MERGEINFO to the
+ mergeinfo inherited from PARENT->IMPLICIT_MERGEINFO. Otherwise contact
+ the repository for CHILD->IMPLICIT_MERGEINFO.
+
+ NOTE: If PARENT is present then this function must have previously been
+ called for PARENT, i.e. if populate_remaining_ranges() is calling this
+ function for a set of svn_client__merge_path_t* the calls must be made
+ in depth-first order.
+
+ MERGEINFO_PATH is the merge source relative to the repository root.
+
+ REVISION1 and REVISION2 describe the merge range requested from
+ MERGEINFO_PATH.
+
+ TARGET_RANGELIST is the portion of CHILD->ABSPATH's explicit or inherited
+ mergeinfo that intersects with the merge history described by
+ MERGEINFO_PATH@REVISION1:MERGEINFO_PATH@REVISION2. TARGET_RANGELIST
+ should be NULL if there is no explicit or inherited mergeinfo on
+ CHILD->ABSPATH or an empty list if CHILD->ABSPATH has empty mergeinfo or
+ explicit mergeinfo that exclusively describes non-intersecting history
+ with MERGEINFO_PATH@REVISION1:MERGEINFO_PATH@REVISION2.
+
+ SCRATCH_POOL is used for all temporary allocations.
+
+ NOTE: This should only be called when honoring mergeinfo.
+
+ NOTE: Like calculate_remaining_ranges() if PARENT is present then this
+ function must have previously been called for PARENT.
+*/
+static svn_error_t *
+filter_merged_revisions(svn_client__merge_path_t *parent,
+ svn_client__merge_path_t *child,
+ const char *mergeinfo_path,
+ svn_rangelist_t *target_rangelist,
+ svn_revnum_t revision1,
+ svn_revnum_t revision2,
+ svn_boolean_t child_inherits_implicit,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_rangelist_t *requested_rangelist,
+ *target_implicit_rangelist, *explicit_rangelist;
+
+ /* Convert REVISION1 and REVISION2 to a rangelist.
+
+ Note: Talking about a requested merge range's inheritability
+ doesn't make much sense, but as we are using svn_merge_range_t
+ to describe it we need to pick *something*. Since all the
+ rangelist manipulations in this function either don't consider
+ inheritance by default or we are requesting that they don't (i.e.
+ svn_rangelist_remove and svn_rangelist_intersect) then we could
+ set the inheritability as FALSE, it won't matter either way. */
+ requested_rangelist = svn_rangelist__initialize(revision1, revision2,
+ TRUE, scratch_pool);
+
+ /* Now filter out revisions that have already been merged to CHILD. */
+
+ if (revision1 > revision2) /* This is a reverse merge. */
+ {
+ svn_rangelist_t *added_rangelist, *deleted_rangelist;
+
+ /* The revert range and will need to be reversed for
+ our svn_rangelist_* APIs to work properly. */
+ SVN_ERR(svn_rangelist_reverse(requested_rangelist, scratch_pool));
+
+ /* Set EXPLICIT_RANGELIST to the list of source-range revs that are
+ already recorded as merged to target. */
+ if (target_rangelist)
+ {
+ /* Return the intersection of the revs which are both already
+ represented by CHILD's explicit or inherited mergeinfo.
+
+ We don't consider inheritance when determining intersecting
+ ranges. If we *did* consider inheritance, then our calculation
+ would be wrong. For example, if the CHILD->REMAINING_RANGES is
+ 5:3 and TARGET_RANGELIST is r5* (non-inheritable) then the
+ intersection would be r4. And that would be wrong as we clearly
+ want to reverse merge both r4 and r5 in this case. Ignoring the
+ ranges' inheritance results in an intersection of r4-5.
+
+ You might be wondering about CHILD's children, doesn't the above
+ imply that we will reverse merge r4-5 from them? Nope, this is
+ safe to do because any path whose parent has non-inheritable
+ ranges is always considered a subtree with differing mergeinfo
+ even if that path has no explicit mergeinfo prior to the
+ merge -- See condition 3 in the doc string for
+ merge.c:get_mergeinfo_paths(). */
+ SVN_ERR(svn_rangelist_intersect(&explicit_rangelist,
+ target_rangelist,
+ requested_rangelist,
+ FALSE, scratch_pool));
+ }
+ else
+ {
+ explicit_rangelist =
+ apr_array_make(result_pool, 0, sizeof(svn_merge_range_t *));
+ }
+
+ /* Was any part of the requested reverse merge not accounted for in
+ CHILD's explicit or inherited mergeinfo? */
+ SVN_ERR(svn_rangelist_diff(&deleted_rangelist, &added_rangelist,
+ requested_rangelist, explicit_rangelist,
+ FALSE, scratch_pool));
+
+ if (deleted_rangelist->nelts == 0)
+ {
+ /* The whole of REVISION1:REVISION2 was represented in CHILD's
+ explicit/inherited mergeinfo, allocate CHILD's remaining
+ ranges in POOL and then we are done. */
+ SVN_ERR(svn_rangelist_reverse(requested_rangelist, scratch_pool));
+ child->remaining_ranges = svn_rangelist_dup(requested_rangelist,
+ result_pool);
+ }
+ else /* We need to check CHILD's implicit mergeinfo. */
+ {
+ svn_rangelist_t *implicit_rangelist;
+
+ SVN_ERR(ensure_implicit_mergeinfo(parent,
+ child,
+ child_inherits_implicit,
+ revision1,
+ revision2,
+ ra_session,
+ ctx,
+ result_pool,
+ scratch_pool));
+
+ target_implicit_rangelist = svn_hash_gets(child->implicit_mergeinfo,
+ mergeinfo_path);
+
+ if (target_implicit_rangelist)
+ SVN_ERR(svn_rangelist_intersect(&implicit_rangelist,
+ target_implicit_rangelist,
+ requested_rangelist,
+ FALSE, scratch_pool));
+ else
+ implicit_rangelist = apr_array_make(scratch_pool, 0,
+ sizeof(svn_merge_range_t *));
+
+ SVN_ERR(svn_rangelist_merge2(implicit_rangelist,
+ explicit_rangelist, scratch_pool,
+ scratch_pool));
+ SVN_ERR(svn_rangelist_reverse(implicit_rangelist, scratch_pool));
+ child->remaining_ranges = svn_rangelist_dup(implicit_rangelist,
+ result_pool);
+ }
+ }
+ else /* This is a forward merge */
+ {
+ /* Set EXPLICIT_RANGELIST to the list of source-range revs that are
+ NOT already recorded as merged to target. */
+ if (target_rangelist)
+ {
+ /* See earlier comment preceding svn_rangelist_intersect() for
+ why we don't consider inheritance here. */
+ SVN_ERR(svn_rangelist_remove(&explicit_rangelist,
+ target_rangelist,
+ requested_rangelist, FALSE,
+ scratch_pool));
+ }
+ else
+ {
+ explicit_rangelist = svn_rangelist_dup(requested_rangelist,
+ scratch_pool);
+ }
+
+ if (explicit_rangelist->nelts == 0)
+ {
+ child->remaining_ranges =
+ apr_array_make(result_pool, 0, sizeof(svn_merge_range_t *));
+ }
+ else
+/* ### TODO: Which evil shall we choose?
+ ###
+ ### If we allow all forward-merges not already found in recorded
+ ### mergeinfo, we destroy the ability to, say, merge the whole of a
+ ### branch to the trunk while automatically ignoring the revisions
+ ### common to both. That's bad.
+ ###
+ ### If we allow only forward-merges not found in either recorded
+ ### mergeinfo or implicit mergeinfo (natural history), then the
+ ### previous scenario works great, but we can't reverse-merge a
+ ### previous change made to our line of history and then remake it
+ ### (because the reverse-merge will leave no mergeinfo trace, and
+ ### the remake-it attempt will still find the original change in
+ ### natural mergeinfo. But you know, that we happen to use 'merge'
+ ### for revision undoing is somewhat unnatural anyway, so I'm
+ ### finding myself having little interest in caring too much about
+ ### this. That said, if we had a way of storing reverse merge
+ ### ranges, we'd be in good shape either way.
+*/
+#ifdef SVN_MERGE__ALLOW_ALL_FORWARD_MERGES_FROM_SELF
+ {
+ /* ### Don't consider implicit mergeinfo. */
+ child->remaining_ranges = svn_rangelist_dup(explicit_rangelist,
+ pool);
+ }
+#else
+ {
+ /* Based on CHILD's TARGET_MERGEINFO there are ranges to merge.
+ Check CHILD's implicit mergeinfo to see if these remaining
+ ranges are represented there. */
+ SVN_ERR(ensure_implicit_mergeinfo(parent,
+ child,
+ child_inherits_implicit,
+ revision1,
+ revision2,
+ ra_session,
+ ctx,
+ result_pool,
+ scratch_pool));
+
+ target_implicit_rangelist = svn_hash_gets(child->implicit_mergeinfo,
+ mergeinfo_path);
+ if (target_implicit_rangelist)
+ SVN_ERR(svn_rangelist_remove(&(child->remaining_ranges),
+ target_implicit_rangelist,
+ explicit_rangelist,
+ FALSE, result_pool));
+ else
+ child->remaining_ranges = svn_rangelist_dup(explicit_rangelist,
+ result_pool);
+ }
+#endif
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for do_file_merge and do_directory_merge (by way of
+ populate_remaining_ranges() for the latter).
+
+ Determine what portions of SOURCE have already
+ been merged to CHILD->ABSPATH and populate CHILD->REMAINING_RANGES with
+ the ranges that still need merging.
+
+ SOURCE and CTX are all cascaded from the caller's arguments of the same
+ names. Note that this means SOURCE adheres to the requirements noted in
+ `MERGEINFO MERGE SOURCE NORMALIZATION'.
+
+ CHILD represents a working copy path which is the merge target or one of
+ the target's subtrees. If not NULL, PARENT is CHILD's nearest path-wise
+ ancestor - see 'THE CHILDREN_WITH_MERGEINFO ARRAY'. TARGET_MERGEINFO is
+ the working mergeinfo on CHILD.
+
+ RA_SESSION is the session for the younger of SOURCE->loc1 and
+ SOURCE->loc2.
+
+ If the function needs to consider CHILD->IMPLICIT_MERGEINFO and
+ CHILD_INHERITS_IMPLICIT is true, then set CHILD->IMPLICIT_MERGEINFO to the
+ mergeinfo inherited from PARENT->IMPLICIT_MERGEINFO. Otherwise contact
+ the repository for CHILD->IMPLICIT_MERGEINFO.
+
+ If not null, IMPLICIT_SRC_GAP is the gap, if any, in the natural history
+ of SOURCE, see merge_cmd_baton_t.implicit_src_gap.
+
+ SCRATCH_POOL is used for all temporary allocations. Changes to CHILD and
+ PARENT are made in RESULT_POOL.
+
+ NOTE: This should only be called when honoring mergeinfo.
+
+ NOTE: If PARENT is present then this function must have previously been
+ called for PARENT, i.e. if populate_remaining_ranges() is calling this
+ function for a set of svn_client__merge_path_t* the calls must be made
+ in depth-first order.
+
+ NOTE: When performing reverse merges, return
+ SVN_ERR_CLIENT_NOT_READY_TO_MERGE if both locations in SOURCE and
+ CHILD->ABSPATH are all on the same line of history but CHILD->ABSPATH's
+ base revision is older than the SOURCE->rev1:rev2 range, see comment re
+ issue #2973 below.
+*/
+static svn_error_t *
+calculate_remaining_ranges(svn_client__merge_path_t *parent,
+ svn_client__merge_path_t *child,
+ const merge_source_t *source,
+ svn_mergeinfo_t target_mergeinfo,
+ const apr_array_header_t *implicit_src_gap,
+ svn_boolean_t child_inherits_implicit,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const svn_client__pathrev_t *primary_src
+ = (source->loc1->rev < source->loc2->rev) ? source->loc2 : source->loc1;
+ const char *mergeinfo_path = svn_client__pathrev_fspath(primary_src,
+ scratch_pool);
+ /* Intersection of TARGET_MERGEINFO and the merge history
+ described by SOURCE. */
+ svn_rangelist_t *target_rangelist;
+ svn_revnum_t child_base_revision;
+
+ /* Since this function should only be called when honoring mergeinfo and
+ * SOURCE adheres to the requirements noted in 'MERGEINFO MERGE SOURCE
+ * NORMALIZATION', SOURCE must be 'ancestral'. */
+ SVN_ERR_ASSERT(source->ancestral);
+
+ /* Determine which of the requested ranges to consider merging... */
+
+ /* Set TARGET_RANGELIST to the portion of TARGET_MERGEINFO that refers
+ to SOURCE (excluding any gap in SOURCE): first get all ranges from
+ TARGET_MERGEINFO that refer to the path of SOURCE, and then prune
+ any ranges that lie in the gap in SOURCE.
+
+ ### [JAF] In fact, that may still leave some ranges that lie entirely
+ outside the range of SOURCE; it seems we don't care about that. */
+ if (target_mergeinfo)
+ target_rangelist = svn_hash_gets(target_mergeinfo, mergeinfo_path);
+ else
+ target_rangelist = NULL;
+ if (implicit_src_gap && target_rangelist)
+ {
+ /* Remove any mergeinfo referring to the 'gap' in SOURCE, as that
+ mergeinfo doesn't really refer to SOURCE at all but instead
+ refers to locations that are non-existent or on a different
+ line of history. (Issue #3242.) */
+ SVN_ERR(svn_rangelist_remove(&target_rangelist,
+ implicit_src_gap, target_rangelist,
+ FALSE, result_pool));
+ }
+
+ /* Initialize CHILD->REMAINING_RANGES and filter out revisions already
+ merged (or, in the case of reverse merges, ranges not yet merged). */
+ SVN_ERR(filter_merged_revisions(parent, child, mergeinfo_path,
+ target_rangelist,
+ source->loc1->rev, source->loc2->rev,
+ child_inherits_implicit,
+ ra_session, ctx, result_pool,
+ scratch_pool));
+
+ /* Issue #2973 -- from the continuing series of "Why, since the advent of
+ merge tracking, allowing merges into mixed rev and locally modified
+ working copies isn't simple and could be considered downright evil".
+
+ If reverse merging a range to the WC path represented by CHILD, from
+ that path's own history, where the path inherits no locally modified
+ mergeinfo from its WC parents (i.e. there is no uncommitted merge to
+ the WC), and the path's base revision is older than the range, then
+ the merge will always be a no-op. This is because we only allow reverse
+ merges of ranges in the path's explicit or natural mergeinfo and a
+ reverse merge from the path's future history obviously isn't going to be
+ in either, hence the no-op.
+
+ The problem is two-fold. First, in a mixed rev WC, the change we
+ want to revert might actually be to some child of the target path
+ which is at a younger base revision. Sure, we can merge directly
+ to that child or update the WC or even use --ignore-ancestry and then
+ successfully run the reverse merge, but that gets to the second
+ problem: Those courses of action are not very obvious. Before 1.5 if
+ a user committed a change that didn't touch the commit target, then
+ immediately decided to revert that change via a reverse merge it would
+ just DTRT. But with the advent of merge tracking the user gets a no-op.
+
+ So in the name of user friendliness, return an error suggesting a helpful
+ course of action.
+ */
+ SVN_ERR(svn_wc__node_get_base(NULL, &child_base_revision,
+ NULL, NULL, NULL, NULL,
+ ctx->wc_ctx, child->abspath,
+ TRUE /* ignore_enoent */,
+ FALSE /* show_hidden */,
+ scratch_pool, scratch_pool));
+ /* If CHILD has no base revision then it hasn't been committed yet, so it
+ can't have any "future" history. */
+ if (SVN_IS_VALID_REVNUM(child_base_revision)
+ && ((child->remaining_ranges)->nelts == 0) /* Inoperative merge */
+ && (source->loc2->rev < source->loc1->rev) /* Reverse merge */
+ && (child_base_revision <= source->loc2->rev)) /* From CHILD's future */
+ {
+ /* Hmmm, an inoperative reverse merge from the "future". If it is
+ from our own future return a helpful error. */
+ svn_error_t *err;
+ svn_client__pathrev_t *start_loc;
+
+ err = svn_client__repos_location(&start_loc,
+ ra_session,
+ source->loc1,
+ child_base_revision,
+ ctx, scratch_pool, scratch_pool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_FS_NOT_FOUND
+ || err->apr_err == SVN_ERR_CLIENT_UNRELATED_RESOURCES)
+ svn_error_clear(err);
+ else
+ return svn_error_trace(err);
+ }
+ else
+ {
+ const char *url;
+
+ SVN_ERR(svn_wc__node_get_url(&url, ctx->wc_ctx, child->abspath,
+ scratch_pool, scratch_pool));
+ if (strcmp(start_loc->url, url) == 0)
+ return svn_error_create(SVN_ERR_CLIENT_MERGE_UPDATE_REQUIRED, NULL,
+ _("Cannot reverse-merge a range from a "
+ "path's own future history; try "
+ "updating first"));
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for populate_remaining_ranges().
+
+ SOURCE is cascaded from the arguments of the same name in
+ populate_remaining_ranges().
+
+ Note: The following comments assume a forward merge, i.e.
+ SOURCE->loc1->rev < SOURCE->loc2->rev. If this is a reverse merge then
+ all the following comments still apply, but with SOURCE->loc1 switched
+ with SOURCE->loc2.
+
+ Like populate_remaining_ranges(), SOURCE must adhere to the restrictions
+ documented in 'MERGEINFO MERGE SOURCE NORMALIZATION'. These restrictions
+ allow for a *single* gap in SOURCE, GAP_REV1:GAP_REV2 exclusive:inclusive
+ (where SOURCE->loc1->rev == GAP_REV1 <= GAP_REV2 < SOURCE->loc2->rev),
+ if SOURCE->loc2->url@(GAP_REV2+1) was copied from SOURCE->loc1. If such
+ a gap exists, set *GAP_START and *GAP_END to the starting and ending
+ revisions of the gap. Otherwise set both to SVN_INVALID_REVNUM.
+
+ For example, if the natural history of URL@2:URL@9 is 'trunk/:2,7-9' this
+ would indicate that trunk@7 was copied from trunk@2. This function would
+ return GAP_START:GAP_END of 2:6 in this case. Note that a path 'trunk'
+ might exist at r3-6, but it would not be on the same line of history as
+ trunk@9.
+
+ ### GAP_START is basically redundant, as (if there is a gap at all) it is
+ necessarily the older revision of SOURCE.
+
+ RA_SESSION is an open RA session to the repository in which SOURCE lives.
+*/
+static svn_error_t *
+find_gaps_in_merge_source_history(svn_revnum_t *gap_start,
+ svn_revnum_t *gap_end,
+ const merge_source_t *source,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_mergeinfo_t implicit_src_mergeinfo;
+ svn_revnum_t old_rev = MIN(source->loc1->rev, source->loc2->rev);
+ const svn_client__pathrev_t *primary_src
+ = (source->loc1->rev < source->loc2->rev) ? source->loc2 : source->loc1;
+ const char *merge_src_fspath = svn_client__pathrev_fspath(primary_src,
+ scratch_pool);
+ svn_rangelist_t *rangelist;
+
+ SVN_ERR_ASSERT(source->ancestral);
+
+ /* Start by assuming there is no gap. */
+ *gap_start = *gap_end = SVN_INVALID_REVNUM;
+
+ /* Easy out: There can't be a gap between adjacent revisions. */
+ if (abs(source->loc1->rev - source->loc2->rev) == 1)
+ return SVN_NO_ERROR;
+
+ /* Get SOURCE as mergeinfo. */
+ SVN_ERR(svn_client__get_history_as_mergeinfo(&implicit_src_mergeinfo, NULL,
+ primary_src,
+ primary_src->rev, old_rev,
+ ra_session,
+ ctx, scratch_pool));
+
+ rangelist = svn_hash_gets(implicit_src_mergeinfo, merge_src_fspath);
+
+ if (!rangelist) /* ### Can we ever not find a rangelist? */
+ return SVN_NO_ERROR;
+
+ /* A gap in natural history can result from either a copy or
+ a rename. If from a copy then history as mergeinfo will look
+ something like this:
+
+ '/trunk:X,Y-Z'
+
+ If from a rename it will look like this:
+
+ '/trunk_old_name:X'
+ '/trunk_new_name:Y-Z'
+
+ In both cases the gap, if it exists, is M-N, where M = X + 1 and
+ N = Y - 1.
+
+ Note that per the rules of 'MERGEINFO MERGE SOURCE NORMALIZATION' we
+ should never have multiple gaps, e.g. if we see anything like the
+ following then something is quite wrong:
+
+ '/trunk_old_name:A,B-C'
+ '/trunk_new_name:D-E'
+ */
+
+ if (rangelist->nelts > 1) /* Copy */
+ {
+ const svn_merge_range_t *gap;
+ /* As mentioned above, multiple gaps *shouldn't* be possible. */
+ SVN_ERR_ASSERT(apr_hash_count(implicit_src_mergeinfo) == 1);
+
+ gap = APR_ARRAY_IDX(rangelist, rangelist->nelts - 1,
+ const svn_merge_range_t *);
+
+ *gap_start = MIN(source->loc1->rev, source->loc2->rev);
+ *gap_end = gap->start;
+
+ /* ### Issue #4132:
+ ### This assertion triggers in merge_tests.py svnmucc_abuse_1()
+ ### when a node is replaced by an older copy of itself.
+
+ BH: I think we should review this and the 'rename' case to find
+ out which behavior we really want, and if we can really
+ determine what happened this way. */
+ SVN_ERR_ASSERT(*gap_start < *gap_end);
+ }
+ else if (apr_hash_count(implicit_src_mergeinfo) > 1) /* Rename */
+ {
+ svn_rangelist_t *requested_rangelist =
+ svn_rangelist__initialize(MIN(source->loc1->rev, source->loc2->rev),
+ MAX(source->loc1->rev, source->loc2->rev),
+ TRUE, scratch_pool);
+ svn_rangelist_t *implicit_rangelist =
+ apr_array_make(scratch_pool, 2, sizeof(svn_merge_range_t *));
+ svn_rangelist_t *gap_rangelist;
+
+ SVN_ERR(svn_rangelist__merge_many(implicit_rangelist,
+ implicit_src_mergeinfo,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_rangelist_remove(&gap_rangelist, implicit_rangelist,
+ requested_rangelist, FALSE,
+ scratch_pool));
+
+ /* If there is anything left it is the gap. */
+ if (gap_rangelist->nelts)
+ {
+ svn_merge_range_t *gap_range =
+ APR_ARRAY_IDX(gap_rangelist, 0, svn_merge_range_t *);
+
+ *gap_start = gap_range->start;
+ *gap_end = gap_range->end;
+ }
+ }
+
+ SVN_ERR_ASSERT(*gap_start == MIN(source->loc1->rev, source->loc2->rev)
+ || (*gap_start == SVN_INVALID_REVNUM
+ && *gap_end == SVN_INVALID_REVNUM));
+ return SVN_NO_ERROR;
+}
+
+/* Helper for do_directory_merge().
+
+ For each (svn_client__merge_path_t *) child in CHILDREN_WITH_MERGEINFO,
+ populate that child's 'remaining_ranges' list with (### ... what?),
+ and populate that child's 'implicit_mergeinfo' with its implicit
+ mergeinfo (natural history). CHILDREN_WITH_MERGEINFO is expected
+ to be sorted in depth first order and each child must be processed in
+ that order. The inheritability of all calculated ranges is TRUE.
+
+ If mergeinfo is being honored (based on MERGE_B -- see HONOR_MERGEINFO()
+ for how this is determined), this function will actually try to be
+ intelligent about populating remaining_ranges list. Otherwise, it
+ will claim that each child has a single remaining range, from
+ SOURCE->rev1, to SOURCE->rev2.
+ ### We also take the short-cut if doing record-only. Why?
+
+ SCRATCH_POOL is used for all temporary allocations. Changes to
+ CHILDREN_WITH_MERGEINFO are made in RESULT_POOL.
+
+ Note that if SOURCE->rev1 > SOURCE->rev2, then each child's remaining_ranges
+ member does not adhere to the API rules for rangelists described in
+ svn_mergeinfo.h -- See svn_client__merge_path_t.
+
+ See `MERGEINFO MERGE SOURCE NORMALIZATION' for more requirements
+ around SOURCE.
+*/
+static svn_error_t *
+populate_remaining_ranges(apr_array_header_t *children_with_mergeinfo,
+ const merge_source_t *source,
+ svn_ra_session_t *ra_session,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ int i;
+ svn_revnum_t gap_start, gap_end;
+
+ /* If we aren't honoring mergeinfo or this is a --record-only merge,
+ we'll make quick work of this by simply adding dummy SOURCE->rev1:rev2
+ ranges for all children. */
+ if (! HONOR_MERGEINFO(merge_b) || merge_b->record_only)
+ {
+ for (i = 0; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i,
+ svn_client__merge_path_t *);
+
+ svn_pool_clear(iterpool);
+
+ /* Issue #3646 'record-only merges create self-referential
+ mergeinfo'. Get the merge target's implicit mergeinfo (natural
+ history). We'll use it later to avoid setting self-referential
+ mergeinfo -- see filter_natural_history_from_mergeinfo(). */
+ if (i == 0) /* First item is always the merge target. */
+ {
+ SVN_ERR(get_full_mergeinfo(NULL, /* child->pre_merge_mergeinfo */
+ &(child->implicit_mergeinfo),
+ NULL, /* child->inherited_mergeinfo */
+ svn_mergeinfo_inherited, ra_session,
+ child->abspath,
+ MAX(source->loc1->rev,
+ source->loc2->rev),
+ MIN(source->loc1->rev,
+ source->loc2->rev),
+ merge_b->ctx, result_pool,
+ iterpool));
+ }
+ else
+ {
+ /* Issue #3443 - Subtrees of the merge target can inherit
+ their parent's implicit mergeinfo in most cases. */
+ svn_client__merge_path_t *parent
+ = find_nearest_ancestor(children_with_mergeinfo,
+ FALSE, child->abspath);
+ svn_boolean_t child_inherits_implicit;
+
+ /* If CHILD is a subtree then its parent must be in
+ CHILDREN_WITH_MERGEINFO, see the global comment
+ 'THE CHILDREN_WITH_MERGEINFO ARRAY'. */
+ SVN_ERR_ASSERT(parent);
+
+ child_inherits_implicit = (parent && !child->switched);
+ SVN_ERR(ensure_implicit_mergeinfo(parent, child,
+ child_inherits_implicit,
+ source->loc1->rev,
+ source->loc2->rev,
+ ra_session, merge_b->ctx,
+ result_pool, iterpool));
+ }
+
+ child->remaining_ranges = svn_rangelist__initialize(source->loc1->rev,
+ source->loc2->rev,
+ TRUE,
+ result_pool);
+ }
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+ }
+
+ /* If, in the merge source's history, there was a copy from an older
+ revision, then SOURCE->loc2->url won't exist at some range M:N, where
+ SOURCE->loc1->rev < M < N < SOURCE->loc2->rev. The rules of 'MERGEINFO
+ MERGE SOURCE NORMALIZATION' allow this, but we must ignore these gaps
+ when calculating what ranges remain to be merged from SOURCE. If we
+ don't and try to merge any part of SOURCE->loc2->url@M:N we would
+ break the editor since no part of that actually exists. See
+ http://svn.haxx.se/dev/archive-2008-11/0618.shtml.
+
+ Find the gaps in the merge target's history, if any. Eventually
+ we will adjust CHILD->REMAINING_RANGES such that we don't describe
+ non-existent paths to the editor. */
+ SVN_ERR(find_gaps_in_merge_source_history(&gap_start, &gap_end,
+ source,
+ ra_session, merge_b->ctx,
+ iterpool));
+
+ /* Stash any gap in the merge command baton, we'll need it later when
+ recording mergeinfo describing this merge. */
+ if (SVN_IS_VALID_REVNUM(gap_start) && SVN_IS_VALID_REVNUM(gap_end))
+ merge_b->implicit_src_gap = svn_rangelist__initialize(gap_start, gap_end,
+ TRUE, result_pool);
+
+ for (i = 0; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *);
+ const char *child_repos_path
+ = svn_dirent_skip_ancestor(merge_b->target->abspath, child->abspath);
+ merge_source_t child_source;
+ svn_client__merge_path_t *parent = NULL;
+ svn_boolean_t child_inherits_implicit;
+
+ svn_pool_clear(iterpool);
+
+ /* If the path is absent don't do subtree merge either. */
+ SVN_ERR_ASSERT(child);
+ if (child->absent)
+ continue;
+
+ SVN_ERR_ASSERT(child_repos_path != NULL);
+ child_source.loc1 = svn_client__pathrev_join_relpath(
+ source->loc1, child_repos_path, iterpool);
+ child_source.loc2 = svn_client__pathrev_join_relpath(
+ source->loc2, child_repos_path, iterpool);
+ /* ### Is the child 'ancestral' over the same revision range? It's
+ * not necessarily true that a child is 'ancestral' if the parent is,
+ * nor that it's not if the parent is not. However, here we claim
+ * that it is. Before we had this 'ancestral' field that we need to
+ * set explicitly, the claim was implicit. Either way, the impact is
+ * that we might pass calculate_remaining_ranges() a source that is
+ * not in fact 'ancestral' (despite its 'ancestral' field being true),
+ * contrary to its doc-string. */
+ child_source.ancestral = source->ancestral;
+
+ /* Get the explicit/inherited mergeinfo for CHILD. If CHILD is the
+ merge target then also get its implicit mergeinfo. Otherwise defer
+ this until we know it is absolutely necessary, since it requires an
+ expensive round trip communication with the server. */
+ SVN_ERR(get_full_mergeinfo(
+ child->pre_merge_mergeinfo ? NULL : &(child->pre_merge_mergeinfo),
+ /* Get implicit only for merge target. */
+ (i == 0) ? &(child->implicit_mergeinfo) : NULL,
+ &(child->inherited_mergeinfo),
+ svn_mergeinfo_inherited, ra_session,
+ child->abspath,
+ MAX(source->loc1->rev, source->loc2->rev),
+ MIN(source->loc1->rev, source->loc2->rev),
+ merge_b->ctx, result_pool, iterpool));
+
+ /* If CHILD isn't the merge target find its parent. */
+ if (i > 0)
+ {
+ parent = find_nearest_ancestor(children_with_mergeinfo,
+ FALSE, child->abspath);
+ /* If CHILD is a subtree then its parent must be in
+ CHILDREN_WITH_MERGEINFO, see the global comment
+ 'THE CHILDREN_WITH_MERGEINFO ARRAY'. */
+ SVN_ERR_ASSERT(parent);
+ }
+
+ /* Issue #3443 - Can CHILD inherit PARENT's implicit mergeinfo, saving
+ us from having to ask the repos? The only time we can't do this is if
+ CHILD is the merge target and so there is no PARENT to inherit from
+ or if CHILD is the root of a switched subtree, in which case PARENT
+ exists but is not CHILD's repository parent. */
+ child_inherits_implicit = (parent && !child->switched);
+
+ SVN_ERR(calculate_remaining_ranges(parent, child,
+ &child_source,
+ child->pre_merge_mergeinfo,
+ merge_b->implicit_src_gap,
+ child_inherits_implicit,
+ ra_session,
+ merge_b->ctx, result_pool,
+ iterpool));
+
+ /* Deal with any gap in SOURCE's natural history.
+
+ If the gap is a proper subset of CHILD->REMAINING_RANGES then we can
+ safely ignore it since we won't describe this path/rev pair.
+
+ If the gap exactly matches or is a superset of a range in
+ CHILD->REMAINING_RANGES then we must remove that range so we don't
+ attempt to describe non-existent paths via the reporter, this will
+ break the editor and our merge.
+
+ If the gap adjoins or overlaps a range in CHILD->REMAINING_RANGES
+ then we must *add* the gap so we span the missing revisions. */
+ if (child->remaining_ranges->nelts
+ && merge_b->implicit_src_gap)
+ {
+ int j;
+ svn_boolean_t proper_subset = FALSE;
+ svn_boolean_t overlaps_or_adjoins = FALSE;
+
+ /* If this is a reverse merge reorder CHILD->REMAINING_RANGES
+ so it will work with the svn_rangelist_* APIs below. */
+ if (source->loc1->rev > source->loc2->rev)
+ SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, iterpool));
+
+ for (j = 0; j < child->remaining_ranges->nelts; j++)
+ {
+ svn_merge_range_t *range
+ = APR_ARRAY_IDX(child->remaining_ranges, j, svn_merge_range_t *);
+
+ if ((range->start <= gap_start && gap_end < range->end)
+ || (range->start < gap_start && gap_end <= range->end))
+ {
+ proper_subset = TRUE;
+ break;
+ }
+ else if ((gap_start == range->start) && (range->end == gap_end))
+ {
+ break;
+ }
+ else if (gap_start <= range->end && range->start <= gap_end)
+ /* intersect */
+ {
+ overlaps_or_adjoins = TRUE;
+ break;
+ }
+ }
+
+ if (!proper_subset)
+ {
+ /* We need to make adjustments. Remove from, or add the gap
+ to, CHILD->REMAINING_RANGES as appropriate. */
+
+ if (overlaps_or_adjoins)
+ SVN_ERR(svn_rangelist_merge2(child->remaining_ranges,
+ merge_b->implicit_src_gap,
+ result_pool, iterpool));
+ else /* equals == TRUE */
+ SVN_ERR(svn_rangelist_remove(&(child->remaining_ranges),
+ merge_b->implicit_src_gap,
+ child->remaining_ranges, FALSE,
+ result_pool));
+ }
+
+ if (source->loc1->rev > source->loc2->rev) /* Reverse merge */
+ SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, iterpool));
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+
+/*-----------------------------------------------------------------------*/
+
+/*** Other Helper Functions ***/
+
+/* Calculate the new mergeinfo for the target tree rooted at TARGET_ABSPATH
+ based on MERGES (a mapping of absolute WC paths to rangelists representing
+ a merge from the source SOURCE_FSPATH).
+
+ If RESULT_CATALOG is NULL, then record the new mergeinfo in the WC (at,
+ and possibly below, TARGET_ABSPATH).
+
+ If RESULT_CATALOG is not NULL, then don't record the new mergeinfo on the
+ WC, but instead record it in RESULT_CATALOG, where the keys are absolute
+ working copy paths and the values are the new mergeinfos for each.
+ Allocate additions to RESULT_CATALOG in pool which RESULT_CATALOG was
+ created in. */
+static svn_error_t *
+update_wc_mergeinfo(svn_mergeinfo_catalog_t result_catalog,
+ const char *target_abspath,
+ const char *source_fspath,
+ apr_hash_t *merges,
+ svn_boolean_t is_rollback,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_hash_index_t *hi;
+
+ /* Combine the mergeinfo for the revision range just merged into
+ the WC with its on-disk mergeinfo. */
+ for (hi = apr_hash_first(scratch_pool, merges); hi; hi = apr_hash_next(hi))
+ {
+ const char *local_abspath = svn__apr_hash_index_key(hi);
+ svn_rangelist_t *ranges = svn__apr_hash_index_val(hi);
+ svn_rangelist_t *rangelist;
+ svn_error_t *err;
+ const char *local_abspath_rel_to_target;
+ const char *fspath;
+ svn_mergeinfo_t mergeinfo;
+
+ svn_pool_clear(iterpool);
+
+ /* As some of the merges may've changed the WC's mergeinfo, get
+ a fresh copy before using it to update the WC's mergeinfo. */
+ err = svn_client__parse_mergeinfo(&mergeinfo, ctx->wc_ctx,
+ local_abspath, iterpool, iterpool);
+
+ /* If a directory PATH was skipped because it is missing or was
+ obstructed by an unversioned item then there's nothing we can
+ do with that, so skip it. */
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_WC_NOT_LOCKED
+ || err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ continue;
+ }
+ else
+ {
+ return svn_error_trace(err);
+ }
+ }
+
+ /* If we are attempting to set empty revision range override mergeinfo
+ on a path with no explicit mergeinfo, we first need the
+ mergeinfo that path inherits. */
+ if (mergeinfo == NULL && ranges->nelts == 0)
+ {
+ SVN_ERR(svn_client__get_wc_mergeinfo(&mergeinfo, NULL,
+ svn_mergeinfo_nearest_ancestor,
+ local_abspath, NULL, NULL,
+ FALSE, ctx, iterpool, iterpool));
+ }
+
+ if (mergeinfo == NULL)
+ mergeinfo = apr_hash_make(iterpool);
+
+ local_abspath_rel_to_target = svn_dirent_skip_ancestor(target_abspath,
+ local_abspath);
+ SVN_ERR_ASSERT(local_abspath_rel_to_target != NULL);
+ fspath = svn_fspath__join(source_fspath,
+ local_abspath_rel_to_target,
+ iterpool);
+ rangelist = svn_hash_gets(mergeinfo, fspath);
+ if (rangelist == NULL)
+ rangelist = apr_array_make(iterpool, 0, sizeof(svn_merge_range_t *));
+
+ if (is_rollback)
+ {
+ ranges = svn_rangelist_dup(ranges, iterpool);
+ SVN_ERR(svn_rangelist_reverse(ranges, iterpool));
+ SVN_ERR(svn_rangelist_remove(&rangelist, ranges, rangelist,
+ FALSE,
+ iterpool));
+ }
+ else
+ {
+ SVN_ERR(svn_rangelist_merge2(rangelist, ranges, iterpool, iterpool));
+ }
+ /* Update the mergeinfo by adjusting the path's rangelist. */
+ svn_hash_sets(mergeinfo, fspath, rangelist);
+
+ if (is_rollback && apr_hash_count(mergeinfo) == 0)
+ mergeinfo = NULL;
+
+ svn_mergeinfo__remove_empty_rangelists(mergeinfo, scratch_pool);
+
+ if (result_catalog)
+ {
+ svn_mergeinfo_t existing_mergeinfo =
+ svn_hash_gets(result_catalog, local_abspath);
+ apr_pool_t *result_catalog_pool = apr_hash_pool_get(result_catalog);
+
+ if (existing_mergeinfo)
+ SVN_ERR(svn_mergeinfo_merge2(mergeinfo, existing_mergeinfo,
+ result_catalog_pool, scratch_pool));
+ svn_hash_sets(result_catalog,
+ apr_pstrdup(result_catalog_pool, local_abspath),
+ svn_mergeinfo_dup(mergeinfo, result_catalog_pool));
+ }
+ else
+ {
+ err = svn_client__record_wc_mergeinfo(local_abspath, mergeinfo,
+ TRUE, ctx, iterpool);
+
+ if (err && err->apr_err == SVN_ERR_ENTRY_NOT_FOUND)
+ {
+ /* PATH isn't just missing, it's not even versioned as far
+ as this working copy knows. But it was included in
+ MERGES, which means that the server knows about it.
+ Likely we don't have access to the source due to authz
+ restrictions. For now just clear the error and
+ continue...
+
+ ### TODO: Set non-inheritable mergeinfo on PATH's immediate
+ ### parent and normal mergeinfo on PATH's siblings which we
+ ### do have access to. */
+ svn_error_clear(err);
+ }
+ else
+ SVN_ERR(err);
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Helper for record_mergeinfo_for_dir_merge().
+
+ Record override mergeinfo on any paths skipped during a merge.
+
+ Set empty mergeinfo on each path in MERGE_B->SKIPPED_ABSPATHS so the path
+ does not incorrectly inherit mergeinfo that will later be describing
+ the merge.
+
+ MERGEINFO_PATH and MERGE_B are cascaded from
+ arguments of the same name in the caller.
+
+ IS_ROLLBACK is true if the caller is recording a reverse merge and false
+ otherwise. RANGELIST is the set of revisions being merged from
+ MERGEINFO_PATH to MERGE_B->target. */
+static svn_error_t *
+record_skips_in_mergeinfo(const char *mergeinfo_path,
+ const svn_rangelist_t *rangelist,
+ svn_boolean_t is_rollback,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_index_t *hi;
+ apr_hash_t *merges;
+ apr_size_t nbr_skips = apr_hash_count(merge_b->skipped_abspaths);
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ if (nbr_skips == 0)
+ return SVN_NO_ERROR;
+
+ merges = apr_hash_make(scratch_pool);
+
+ /* Override the mergeinfo for child paths which weren't actually merged. */
+ for (hi = apr_hash_first(scratch_pool, merge_b->skipped_abspaths); hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *skipped_abspath = svn__apr_hash_index_key(hi);
+ svn_wc_notify_state_t obstruction_state;
+
+ svn_pool_clear(iterpool);
+
+ /* Before we override, make sure this is a versioned path, it might
+ be an external or missing from disk due to authz restrictions. */
+ SVN_ERR(perform_obstruction_check(&obstruction_state, NULL, NULL,
+ NULL, NULL,
+ merge_b, skipped_abspath,
+ iterpool));
+ if (obstruction_state == svn_wc_notify_state_obstructed
+ || obstruction_state == svn_wc_notify_state_missing)
+ continue;
+
+ /* Add an empty range list for this path.
+
+ ### TODO: This works fine for a file path skipped because it is
+ ### missing as long as the file's parent directory is present.
+ ### But missing directory paths skipped are not handled yet,
+ ### see issue #2915.
+
+ ### TODO: An empty range is fine if the skipped path doesn't
+ ### inherit any mergeinfo from a parent, but if it does
+ ### we need to account for that. See issue #3440
+ ### http://subversion.tigris.org/issues/show_bug.cgi?id=3440. */
+ svn_hash_sets(merges, skipped_abspath,
+ apr_array_make(scratch_pool, 0,
+ sizeof(svn_merge_range_t *)));
+
+ /* if (nbr_skips < notify_b->nbr_notifications)
+ ### Use RANGELIST as the mergeinfo for all children of
+ ### this path which were not also explicitly
+ ### skipped? */
+ }
+ SVN_ERR(update_wc_mergeinfo(NULL, merge_b->target->abspath,
+ mergeinfo_path, merges,
+ is_rollback, merge_b->ctx, iterpool));
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Data for reporting when a merge aborted because of raising conflicts.
+ */
+typedef struct single_range_conflict_report_t
+{
+ /* What sub-range of the requested source raised conflicts?
+ * The 'inheritable' flag is ignored. */
+ merge_source_t *conflicted_range;
+ /* What sub-range of the requested source remains to be merged?
+ * NULL if no more. The 'inheritable' flag is ignored. */
+ merge_source_t *remaining_source;
+
+} single_range_conflict_report_t;
+
+/* Create a single_range_conflict_report_t, containing deep copies of
+ * CONFLICTED_RANGE and REMAINING_SOURCE, allocated in RESULT_POOL. */
+static single_range_conflict_report_t *
+single_range_conflict_report_create(const merge_source_t *conflicted_range,
+ const merge_source_t *remaining_source,
+ apr_pool_t *result_pool)
+{
+ single_range_conflict_report_t *report
+ = apr_palloc(result_pool, sizeof(*report));
+
+ assert(conflicted_range != NULL);
+
+ report->conflicted_range = merge_source_dup(conflicted_range, result_pool);
+ report->remaining_source
+ = remaining_source ? merge_source_dup(remaining_source, result_pool)
+ : NULL;
+ return report;
+}
+
+/* Data for reporting when a merge aborted because of raising conflicts.
+ *
+ * ### TODO: More info, including the ranges (or other parameters) the user
+ * needs to complete the merge.
+ */
+typedef struct conflict_report_t
+{
+ const char *target_abspath;
+ /* The revision range during which conflicts were raised */
+ const merge_source_t *conflicted_range;
+ /* Was the conflicted range the last range in the whole requested merge? */
+ svn_boolean_t was_last_range;
+} conflict_report_t;
+
+/* Return a new conflict_report_t containing deep copies of the parameters,
+ * allocated in RESULT_POOL. */
+static conflict_report_t *
+conflict_report_create(const char *target_abspath,
+ const merge_source_t *conflicted_range,
+ svn_boolean_t was_last_range,
+ apr_pool_t *result_pool)
+{
+ conflict_report_t *report = apr_palloc(result_pool, sizeof(*report));
+
+ report->target_abspath = apr_pstrdup(result_pool, target_abspath);
+ report->conflicted_range = merge_source_dup(conflicted_range, result_pool);
+ report->was_last_range = was_last_range;
+ return report;
+}
+
+/* Return a deep copy of REPORT, allocated in RESULT_POOL. */
+static conflict_report_t *
+conflict_report_dup(const conflict_report_t *report,
+ apr_pool_t *result_pool)
+{
+ conflict_report_t *new = apr_pmemdup(result_pool, report, sizeof(*new));
+
+ new->target_abspath = apr_pstrdup(result_pool, report->target_abspath);
+ new->conflicted_range = merge_source_dup(report->conflicted_range,
+ result_pool);
+ return new;
+}
+
+/* Create and return an error structure appropriate for the unmerged
+ revisions range(s). */
+static APR_INLINE svn_error_t *
+make_merge_conflict_error(conflict_report_t *report,
+ apr_pool_t *scratch_pool)
+{
+ assert(!report || svn_dirent_is_absolute(report->target_abspath));
+
+ if (report && ! report->was_last_range)
+ {
+ svn_error_t *err = svn_error_createf(SVN_ERR_WC_FOUND_CONFLICT, NULL,
+ _("One or more conflicts were produced while merging r%ld:%ld into\n"
+ "'%s' --\n"
+ "resolve all conflicts and rerun the merge to apply the remaining\n"
+ "unmerged revisions"),
+ report->conflicted_range->loc1->rev, report->conflicted_range->loc2->rev,
+ svn_dirent_local_style(report->target_abspath, scratch_pool));
+ assert(report->conflicted_range->loc1->rev != report->conflicted_range->loc2->rev); /* ### is a valid case in a 2-URL merge */
+ return err;
+ }
+ return SVN_NO_ERROR;
+}
+
+/* Helper for do_directory_merge().
+
+ TARGET_WCPATH is a directory and CHILDREN_WITH_MERGEINFO is filled
+ with paths (svn_client__merge_path_t *) arranged in depth first order,
+ which have mergeinfo set on them or meet one of the other criteria
+ defined in get_mergeinfo_paths(). Remove any paths absent from disk
+ or scheduled for deletion from CHILDREN_WITH_MERGEINFO which are equal to
+ or are descendants of TARGET_WCPATH by setting those children to NULL. */
+static void
+remove_absent_children(const char *target_wcpath,
+ apr_array_header_t *children_with_mergeinfo)
+{
+ /* Before we try to override mergeinfo for skipped paths, make sure
+ the path isn't absent due to authz restrictions, because there's
+ nothing we can do about those. */
+ int i;
+ for (i = 0; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *);
+ if ((child->absent || child->scheduled_for_deletion)
+ && svn_dirent_is_ancestor(target_wcpath, child->abspath))
+ {
+ svn_sort__array_delete(children_with_mergeinfo, i--, 1);
+ }
+ }
+}
+
+/* Helper for do_directory_merge() to handle the case where a merge editor
+ drive removes explicit mergeinfo from a subtree of the merge target.
+
+ MERGE_B is cascaded from the argument of the same name in
+ do_directory_merge(). For each path (if any) in
+ MERGE_B->PATHS_WITH_DELETED_MERGEINFO remove that path from
+ CHILDREN_WITH_MERGEINFO.
+
+ The one exception is for the merge target itself,
+ MERGE_B->target->abspath, this must always be present in
+ CHILDREN_WITH_MERGEINFO so this is never removed by this
+ function. */
+static void
+remove_children_with_deleted_mergeinfo(merge_cmd_baton_t *merge_b,
+ apr_array_header_t *children_with_mergeinfo)
+{
+ int i;
+
+ if (!merge_b->paths_with_deleted_mergeinfo)
+ return;
+
+ /* CHILDREN_WITH_MERGEINFO[0] is the always the merge target
+ so start at the first child. */
+ for (i = 1; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *);
+
+ if (svn_hash_gets(merge_b->paths_with_deleted_mergeinfo, child->abspath))
+ {
+ svn_sort__array_delete(children_with_mergeinfo, i--, 1);
+ }
+ }
+}
+
+/* Helper for do_directory_merge().
+
+ Set up the diff editor report to merge the SOURCE diff
+ into TARGET_ABSPATH and drive it.
+
+ If mergeinfo is not being honored (based on MERGE_B -- see the doc
+ string for HONOR_MERGEINFO() for how this is determined), then ignore
+ CHILDREN_WITH_MERGEINFO and merge the SOURCE diff to TARGET_ABSPATH.
+
+ If mergeinfo is being honored then perform a history-aware merge,
+ describing TARGET_ABSPATH and its subtrees to the reporter in such as way
+ as to avoid repeating merges already performed per the mergeinfo and
+ natural history of TARGET_ABSPATH and its subtrees.
+
+ The ranges that still need to be merged to the TARGET_ABSPATH and its
+ subtrees are described in CHILDREN_WITH_MERGEINFO, an array of
+ svn_client__merge_path_t * -- see 'THE CHILDREN_WITH_MERGEINFO ARRAY'
+ comment at the top of this file for more info. Note that it is possible
+ TARGET_ABSPATH and/or some of its subtrees need only a subset, or no part,
+ of SOURCE to be merged. Though there is little point to
+ calling this function if TARGET_ABSPATH and all its subtrees have already
+ had SOURCE merged, this will work but is a no-op.
+
+ SOURCE->rev1 and SOURCE->rev2 must be bound by the set of remaining_ranges
+ fields in CHILDREN_WITH_MERGEINFO's elements, specifically:
+
+ For forward merges (SOURCE->rev1 < SOURCE->rev2):
+
+ 1) The first svn_merge_range_t * element of each child's remaining_ranges
+ array must meet one of the following conditions:
+
+ a) The range's start field is greater than or equal to SOURCE->rev2.
+
+ b) The range's end field is SOURCE->rev2.
+
+ 2) Among all the ranges that meet condition 'b' the oldest start
+ revision must equal SOURCE->rev1.
+
+ For reverse merges (SOURCE->rev1 > SOURCE->rev2):
+
+ 1) The first svn_merge_range_t * element of each child's remaining_ranges
+ array must meet one of the following conditions:
+
+ a) The range's start field is less than or equal to SOURCE->rev2.
+
+ b) The range's end field is SOURCE->rev2.
+
+ 2) Among all the ranges that meet condition 'b' the youngest start
+ revision must equal SOURCE->rev1.
+
+ Note: If the first svn_merge_range_t * element of some subtree child's
+ remaining_ranges array is the same as the first range of that child's
+ nearest path-wise ancestor, then the subtree child *will not* be described
+ to the reporter.
+
+ DEPTH, NOTIFY_B, and MERGE_B are cascaded from do_directory_merge(), see
+ that function for more info.
+
+ MERGE_B->ra_session1 and MERGE_B->ra_session2 are RA sessions open to any
+ URL in the repository of SOURCE; they may be temporarily reparented within
+ this function.
+
+ If SOURCE->ancestral is set, then SOURCE->loc1 must be a
+ historical ancestor of SOURCE->loc2, or vice-versa (see
+ `MERGEINFO MERGE SOURCE NORMALIZATION' for more requirements around
+ SOURCE in this case).
+*/
+static svn_error_t *
+drive_merge_report_editor(const char *target_abspath,
+ const merge_source_t *source,
+ const apr_array_header_t *children_with_mergeinfo,
+ const svn_diff_tree_processor_t *processor,
+ svn_depth_t depth,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *scratch_pool)
+{
+ const svn_ra_reporter3_t *reporter;
+ const svn_delta_editor_t *diff_editor;
+ void *diff_edit_baton;
+ void *report_baton;
+ svn_revnum_t target_start;
+ svn_boolean_t honor_mergeinfo = HONOR_MERGEINFO(merge_b);
+ const char *old_sess1_url, *old_sess2_url;
+ svn_boolean_t is_rollback = source->loc1->rev > source->loc2->rev;
+
+ /* Start with a safe default starting revision for the editor and the
+ merge target. */
+ target_start = source->loc1->rev;
+
+ /* If we are honoring mergeinfo the starting revision for the merge target
+ might not be SOURCE->rev1, in fact the merge target might not need *any*
+ part of SOURCE merged -- Instead some subtree of the target
+ needs SOURCE -- So get the right starting revision for the
+ target. */
+ if (honor_mergeinfo)
+ {
+ svn_client__merge_path_t *child;
+
+ /* CHILDREN_WITH_MERGEINFO must always exist if we are honoring
+ mergeinfo and must have at least one element (describing the
+ merge target). */
+ SVN_ERR_ASSERT(children_with_mergeinfo);
+ SVN_ERR_ASSERT(children_with_mergeinfo->nelts);
+
+ /* Get the merge target's svn_client__merge_path_t, which is always
+ the first in the array due to depth first sorting requirement,
+ see 'THE CHILDREN_WITH_MERGEINFO ARRAY'. */
+ child = APR_ARRAY_IDX(children_with_mergeinfo, 0,
+ svn_client__merge_path_t *);
+ SVN_ERR_ASSERT(child);
+ if (child->remaining_ranges->nelts == 0)
+ {
+ /* The merge target doesn't need anything merged. */
+ target_start = source->loc2->rev;
+ }
+ else
+ {
+ /* The merge target has remaining revisions to merge. These
+ ranges may fully or partially overlap the range described
+ by SOURCE->rev1:rev2 or may not intersect that range at
+ all. */
+ svn_merge_range_t *range =
+ APR_ARRAY_IDX(child->remaining_ranges, 0,
+ svn_merge_range_t *);
+ if ((!is_rollback && range->start > source->loc2->rev)
+ || (is_rollback && range->start < source->loc2->rev))
+ {
+ /* Merge target's first remaining range doesn't intersect. */
+ target_start = source->loc2->rev;
+ }
+ else
+ {
+ /* Merge target's first remaining range partially or
+ fully overlaps. */
+ target_start = range->start;
+ }
+ }
+ }
+
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_sess1_url,
+ merge_b->ra_session1,
+ source->loc1->url, scratch_pool));
+ /* Temporarily point our second RA session to SOURCE->loc1->url, too. We use
+ this to request individual file contents. */
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_sess2_url,
+ merge_b->ra_session2,
+ source->loc1->url, scratch_pool));
+
+ /* Get the diff editor and a reporter with which to, ultimately,
+ drive it. */
+ SVN_ERR(svn_client__get_diff_editor2(&diff_editor, &diff_edit_baton,
+ merge_b->ra_session2,
+ depth,
+ source->loc1->rev,
+ TRUE /* text_deltas */,
+ processor,
+ merge_b->ctx->cancel_func,
+ merge_b->ctx->cancel_baton,
+ scratch_pool));
+ SVN_ERR(svn_ra_do_diff3(merge_b->ra_session1,
+ &reporter, &report_baton, source->loc2->rev,
+ "", depth, merge_b->diff_ignore_ancestry,
+ TRUE, /* text_deltas */
+ source->loc2->url, diff_editor, diff_edit_baton,
+ scratch_pool));
+
+ /* Drive the reporter. */
+ SVN_ERR(reporter->set_path(report_baton, "", target_start, depth,
+ FALSE, NULL, scratch_pool));
+ if (honor_mergeinfo && children_with_mergeinfo)
+ {
+ /* Describe children with mergeinfo overlapping this merge
+ operation such that no repeated diff is retrieved for them from
+ the repository. */
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ /* Start with CHILDREN_WITH_MERGEINFO[1], CHILDREN_WITH_MERGEINFO[0]
+ is always the merge target (TARGET_ABSPATH). */
+ for (i = 1; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_merge_range_t *range;
+ const char *child_repos_path;
+ const svn_client__merge_path_t *parent;
+ const svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i,
+ svn_client__merge_path_t *);
+
+ SVN_ERR_ASSERT(child);
+ if (child->absent)
+ continue;
+
+ svn_pool_clear(iterpool);
+
+ /* Find this child's nearest wc ancestor with mergeinfo. */
+ parent = find_nearest_ancestor(children_with_mergeinfo,
+ FALSE, child->abspath);
+
+ /* If a subtree needs the same range applied as its nearest parent
+ with mergeinfo or neither the subtree nor this parent need
+ SOURCE->rev1:rev2 merged, then we don't need to describe the
+ subtree separately. In the latter case this could break the
+ editor if child->abspath didn't exist at SOURCE->rev2 and we
+ attempt to describe it via a reporter set_path call. */
+ if (child->remaining_ranges->nelts)
+ {
+ range = APR_ARRAY_IDX(child->remaining_ranges, 0,
+ svn_merge_range_t *);
+ if ((!is_rollback && range->start > source->loc2->rev)
+ || (is_rollback && range->start < source->loc2->rev))
+ {
+ /* This child's first remaining range comes after the range
+ we are currently merging, so skip it. We expect to get
+ to it in a subsequent call to this function. */
+ continue;
+ }
+ else if (parent->remaining_ranges->nelts)
+ {
+ svn_merge_range_t *parent_range =
+ APR_ARRAY_IDX(parent->remaining_ranges, 0,
+ svn_merge_range_t *);
+ svn_merge_range_t *child_range =
+ APR_ARRAY_IDX(child->remaining_ranges, 0,
+ svn_merge_range_t *);
+ if (parent_range->start == child_range->start)
+ continue; /* Subtree needs same range as parent. */
+ }
+ }
+ else /* child->remaining_ranges->nelts == 0*/
+ {
+ /* If both the subtree and its parent need no ranges applied
+ consider that as the "same ranges" and don't describe
+ the subtree. */
+ if (parent->remaining_ranges->nelts == 0)
+ continue;
+ }
+
+ /* Ok, we really need to describe this subtree as it needs different
+ ranges applied than its nearest working copy parent. */
+ child_repos_path = svn_dirent_is_child(target_abspath,
+ child->abspath,
+ iterpool);
+ /* This loop is only processing subtrees, so CHILD->ABSPATH
+ better be a proper child of the merge target. */
+ SVN_ERR_ASSERT(child_repos_path);
+
+ if ((child->remaining_ranges->nelts == 0)
+ || (is_rollback && (range->start < source->loc2->rev))
+ || (!is_rollback && (range->start > source->loc2->rev)))
+ {
+ /* Nothing to merge to this child. We'll claim we have
+ it up to date so the server doesn't send us
+ anything. */
+ SVN_ERR(reporter->set_path(report_baton, child_repos_path,
+ source->loc2->rev, depth, FALSE,
+ NULL, iterpool));
+ }
+ else
+ {
+ SVN_ERR(reporter->set_path(report_baton, child_repos_path,
+ range->start, depth, FALSE,
+ NULL, iterpool));
+ }
+ }
+ svn_pool_destroy(iterpool);
+ }
+ SVN_ERR(reporter->finish_report(report_baton, scratch_pool));
+
+ /* Point the merge baton's RA sessions back where they were. */
+ SVN_ERR(svn_ra_reparent(merge_b->ra_session1, old_sess1_url, scratch_pool));
+ SVN_ERR(svn_ra_reparent(merge_b->ra_session2, old_sess2_url, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Iterate over each svn_client__merge_path_t * element in
+ CHILDREN_WITH_MERGEINFO and, if START_REV is true, find the most inclusive
+ start revision among those element's first remaining_ranges element. If
+ START_REV is false, then look for the most inclusive end revision.
+
+ If IS_ROLLBACK is true the youngest start or end (as per START_REV)
+ revision is considered the "most inclusive" otherwise the oldest revision
+ is.
+
+ If none of CHILDREN_WITH_MERGEINFO's elements have any remaining ranges
+ return SVN_INVALID_REVNUM. */
+static svn_revnum_t
+get_most_inclusive_rev(const apr_array_header_t *children_with_mergeinfo,
+ svn_boolean_t is_rollback,
+ svn_boolean_t start_rev)
+{
+ int i;
+ svn_revnum_t most_inclusive_rev = SVN_INVALID_REVNUM;
+
+ for (i = 0; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *);
+
+ if ((! child) || child->absent)
+ continue;
+ if (child->remaining_ranges->nelts > 0)
+ {
+ svn_merge_range_t *range =
+ APR_ARRAY_IDX(child->remaining_ranges, 0, svn_merge_range_t *);
+
+ /* Are we looking for the most inclusive start or end rev? */
+ svn_revnum_t rev = start_rev ? range->start : range->end;
+
+ if ((most_inclusive_rev == SVN_INVALID_REVNUM)
+ || (is_rollback && (rev > most_inclusive_rev))
+ || ((! is_rollback) && (rev < most_inclusive_rev)))
+ most_inclusive_rev = rev;
+ }
+ }
+ return most_inclusive_rev;
+}
+
+
+/* If first item in each child of CHILDREN_WITH_MERGEINFO's
+ remaining_ranges is inclusive of END_REV, Slice the first range in
+ to two at END_REV. All the allocations are persistent and allocated
+ from POOL. */
+static void
+slice_remaining_ranges(apr_array_header_t *children_with_mergeinfo,
+ svn_boolean_t is_rollback, svn_revnum_t end_rev,
+ apr_pool_t *pool)
+{
+ int i;
+ for (i = 0; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i,
+ svn_client__merge_path_t *);
+ if (!child || child->absent)
+ continue;
+ if (child->remaining_ranges->nelts > 0)
+ {
+ svn_merge_range_t *range = APR_ARRAY_IDX(child->remaining_ranges, 0,
+ svn_merge_range_t *);
+ if ((is_rollback && (range->start > end_rev)
+ && (range->end < end_rev))
+ || (!is_rollback && (range->start < end_rev)
+ && (range->end > end_rev)))
+ {
+ svn_merge_range_t *split_range1, *split_range2;
+
+ split_range1 = svn_merge_range_dup(range, pool);
+ split_range2 = svn_merge_range_dup(range, pool);
+ split_range1->end = end_rev;
+ split_range2->start = end_rev;
+ APR_ARRAY_IDX(child->remaining_ranges, 0,
+ svn_merge_range_t *) = split_range1;
+ svn_sort__array_insert(&split_range2, child->remaining_ranges, 1);
+ }
+ }
+ }
+}
+
+/* Helper for do_directory_merge().
+
+ For each child in CHILDREN_WITH_MERGEINFO remove the first remaining_ranges
+ svn_merge_range_t *element of the child if that range has an end revision
+ equal to REVISION.
+
+ If a range is removed from a child's remaining_ranges array, allocate the
+ new remaining_ranges array in POOL.
+ */
+static void
+remove_first_range_from_remaining_ranges(svn_revnum_t revision,
+ apr_array_header_t
+ *children_with_mergeinfo,
+ apr_pool_t *pool)
+{
+ int i;
+
+ for (i = 0; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i,
+ svn_client__merge_path_t *);
+ if (!child || child->absent)
+ continue;
+ if (child->remaining_ranges->nelts > 0)
+ {
+ svn_merge_range_t *first_range =
+ APR_ARRAY_IDX(child->remaining_ranges, 0, svn_merge_range_t *);
+ if (first_range->end == revision)
+ {
+ svn_sort__array_delete(child->remaining_ranges, 0, 1);
+ }
+ }
+ }
+}
+
+/* Get a file's content and properties from the repository.
+ Set *FILENAME to the local path to a new temporary file holding its text,
+ and set *PROPS to a new hash of its properties.
+
+ RA_SESSION is a session open to the correct repository, which will be
+ temporarily reparented to the URL of the file itself. LOCATION is the
+ repository location of the file.
+
+ The resulting file and the return values live as long as RESULT_POOL, all
+ other allocations occur in SCRATCH_POOL.
+*/
+static svn_error_t *
+single_file_merge_get_file(const char **filename,
+ apr_hash_t **props,
+ svn_ra_session_t *ra_session,
+ const svn_client__pathrev_t *location,
+ const char *wc_target,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_stream_t *stream;
+ const char *old_sess_url;
+ svn_error_t *err;
+
+ SVN_ERR(svn_stream_open_unique(&stream, filename, NULL,
+ svn_io_file_del_on_pool_cleanup,
+ result_pool, scratch_pool));
+
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_sess_url, ra_session, location->url,
+ scratch_pool));
+ err = svn_ra_get_file(ra_session, "", location->rev,
+ stream, NULL, props, scratch_pool);
+ SVN_ERR(svn_error_compose_create(
+ err, svn_ra_reparent(ra_session, old_sess_url, scratch_pool)));
+
+ return svn_error_trace(svn_stream_close(stream));
+}
+
+/* Compare two svn_client__merge_path_t elements **A and **B, given the
+ addresses of pointers to them. Return an integer less than, equal to, or
+ greater than zero if A sorts before, the same as, or after B, respectively.
+ This is a helper for qsort() and bsearch() on an array of such elements. */
+static int
+compare_merge_path_t_as_paths(const void *a,
+ const void *b)
+{
+ const svn_client__merge_path_t *child1
+ = *((const svn_client__merge_path_t * const *) a);
+ const svn_client__merge_path_t *child2
+ = *((const svn_client__merge_path_t * const *) b);
+
+ return svn_path_compare_paths(child1->abspath, child2->abspath);
+}
+
+/* Return a pointer to the element of CHILDREN_WITH_MERGEINFO whose path
+ * is PATH, or return NULL if there is no such element. */
+static svn_client__merge_path_t *
+get_child_with_mergeinfo(const apr_array_header_t *children_with_mergeinfo,
+ const char *abspath)
+{
+ svn_client__merge_path_t merge_path;
+ svn_client__merge_path_t *key;
+ svn_client__merge_path_t **pchild;
+
+ merge_path.abspath = abspath;
+ key = &merge_path;
+ pchild = bsearch(&key, children_with_mergeinfo->elts,
+ children_with_mergeinfo->nelts,
+ children_with_mergeinfo->elt_size,
+ compare_merge_path_t_as_paths);
+ return pchild ? *pchild : NULL;
+}
+
+/* Insert a deep copy of INSERT_ELEMENT into the CHILDREN_WITH_MERGEINFO
+ array at its correct position. Allocate the new storage in POOL.
+ CHILDREN_WITH_MERGEINFO is a depth first sorted array of
+ (svn_client__merge_path_t *).
+
+ ### Most callers don't need this to deep-copy the new element.
+ ### It may be more efficient for some callers to insert a bunch of items
+ out of order and then sort afterwards. (One caller is doing a qsort
+ after calling this anyway.)
+ */
+static void
+insert_child_to_merge(apr_array_header_t *children_with_mergeinfo,
+ const svn_client__merge_path_t *insert_element,
+ apr_pool_t *pool)
+{
+ int insert_index;
+ const svn_client__merge_path_t *new_element;
+
+ /* Find where to insert the new element */
+ insert_index =
+ svn_sort__bsearch_lower_bound(&insert_element, children_with_mergeinfo,
+ compare_merge_path_t_as_paths);
+
+ new_element = svn_client__merge_path_dup(insert_element, pool);
+ svn_sort__array_insert(&new_element, children_with_mergeinfo, insert_index);
+}
+
+/* Helper for get_mergeinfo_paths().
+
+ CHILDREN_WITH_MERGEINFO, DEPTH, and POOL are
+ all cascaded from the arguments of the same name to get_mergeinfo_paths().
+
+ TARGET is the merge target.
+
+ *CHILD is the element in in CHILDREN_WITH_MERGEINFO that
+ get_mergeinfo_paths() is iterating over and *CURR_INDEX is index for
+ *CHILD.
+
+ If CHILD->ABSPATH is equal to MERGE_CMD_BATON->target->abspath do nothing.
+ Else if CHILD->ABSPATH is switched or absent then make sure its immediate
+ (as opposed to nearest) parent in CHILDREN_WITH_MERGEINFO is marked as
+ missing a child. If the immediate parent does not exist in
+ CHILDREN_WITH_MERGEINFO then create it (and increment *CURR_INDEX so that
+ caller doesn't process the inserted element). Also ensure that
+ CHILD->ABSPATH's siblings which are not already present in
+ CHILDREN_WITH_MERGEINFO are also added to the array, limited by DEPTH
+ (e.g. don't add directory siblings of a switched file).
+ Use POOL for temporary allocations only, any new CHILDREN_WITH_MERGEINFO
+ elements are allocated in POOL. */
+static svn_error_t *
+insert_parent_and_sibs_of_sw_absent_del_subtree(
+ apr_array_header_t *children_with_mergeinfo,
+ const merge_target_t *target,
+ int *curr_index,
+ svn_client__merge_path_t *child,
+ svn_depth_t depth,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_client__merge_path_t *parent;
+ const char *parent_abspath;
+ apr_pool_t *iterpool;
+ const apr_array_header_t *children;
+ int i;
+
+ if (!(child->absent
+ || (child->switched
+ && strcmp(target->abspath,
+ child->abspath) != 0)))
+ return SVN_NO_ERROR;
+
+ parent_abspath = svn_dirent_dirname(child->abspath, pool);
+ parent = get_child_with_mergeinfo(children_with_mergeinfo, parent_abspath);
+ if (parent)
+ {
+ parent->missing_child = child->absent;
+ parent->switched_child = child->switched;
+ }
+ else
+ {
+ /* Create a new element to insert into CHILDREN_WITH_MERGEINFO. */
+ parent = svn_client__merge_path_create(parent_abspath, pool);
+ parent->missing_child = child->absent;
+ parent->switched_child = child->switched;
+ /* Insert PARENT into CHILDREN_WITH_MERGEINFO. */
+ insert_child_to_merge(children_with_mergeinfo, parent, pool);
+ /* Increment for loop index so we don't process the inserted element. */
+ (*curr_index)++;
+ } /*(parent == NULL) */
+
+ /* Add all of PARENT's non-missing children that are not already present.*/
+ SVN_ERR(svn_wc__node_get_children(&children, ctx->wc_ctx,
+ parent_abspath, FALSE, pool, pool));
+ iterpool = svn_pool_create(pool);
+ for (i = 0; i < children->nelts; i++)
+ {
+ const char *child_abspath = APR_ARRAY_IDX(children, i, const char *);
+ svn_client__merge_path_t *sibling_of_missing;
+
+ svn_pool_clear(iterpool);
+
+ /* Does this child already exist in CHILDREN_WITH_MERGEINFO? */
+ sibling_of_missing = get_child_with_mergeinfo(children_with_mergeinfo,
+ child_abspath);
+ /* Create the missing child and insert it into CHILDREN_WITH_MERGEINFO.*/
+ if (!sibling_of_missing)
+ {
+ /* Don't add directory children if DEPTH is svn_depth_files. */
+ if (depth == svn_depth_files)
+ {
+ svn_node_kind_t child_kind;
+
+ SVN_ERR(svn_wc_read_kind2(&child_kind,
+ ctx->wc_ctx, child_abspath,
+ FALSE, FALSE, iterpool));
+ if (child_kind != svn_node_file)
+ continue;
+ }
+
+ sibling_of_missing = svn_client__merge_path_create(child_abspath,
+ pool);
+ insert_child_to_merge(children_with_mergeinfo, sibling_of_missing,
+ pool);
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* pre_merge_status_cb's baton */
+struct pre_merge_status_baton_t
+{
+ svn_wc_context_t *wc_ctx;
+
+ /* const char *absolute_wc_path to svn_depth_t * mapping for depths
+ of empty, immediates, and files. */
+ apr_hash_t *shallow_subtrees;
+
+ /* const char *absolute_wc_path to the same, for all paths missing
+ from the working copy. */
+ apr_hash_t *missing_subtrees;
+
+ /* const char *absolute_wc_path const char * repos relative path, describing
+ the root of each switched subtree in the working copy and the repository
+ relative path it is switched to. */
+ apr_hash_t *switched_subtrees;
+
+ /* A pool to allocate additions to the above hashes in. */
+ apr_pool_t *pool;
+};
+
+/* A svn_wc_status_func4_t callback used by get_mergeinfo_paths to gather
+ all switched, depth filtered and missing subtrees under a merge target.
+
+ Note that this doesn't see server and user excluded trees. */
+static svn_error_t *
+pre_merge_status_cb(void *baton,
+ const char *local_abspath,
+ const svn_wc_status3_t *status,
+ apr_pool_t *scratch_pool)
+{
+ struct pre_merge_status_baton_t *pmsb = baton;
+
+ if (status->switched && !status->file_external)
+ {
+ store_path(pmsb->switched_subtrees, local_abspath);
+ }
+
+ if (status->depth == svn_depth_empty
+ || status->depth == svn_depth_files)
+ {
+ const char *dup_abspath;
+ svn_depth_t *depth = apr_pmemdup(pmsb->pool, &status->depth,
+ sizeof *depth);
+
+ dup_abspath = apr_pstrdup(pmsb->pool, local_abspath);
+
+ svn_hash_sets(pmsb->shallow_subtrees, dup_abspath, depth);
+ }
+
+ if (status->node_status == svn_wc_status_missing)
+ {
+ svn_boolean_t new_missing_root = TRUE;
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(scratch_pool, pmsb->missing_subtrees);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *missing_root_path = svn__apr_hash_index_key(hi);
+
+ if (svn_dirent_is_ancestor(missing_root_path,
+ local_abspath))
+ {
+ new_missing_root = FALSE;
+ break;
+ }
+ }
+
+ if (new_missing_root)
+ store_path(pmsb->missing_subtrees, local_abspath);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Find all the subtrees in the working copy tree rooted at TARGET_ABSPATH
+ * that have explicit mergeinfo.
+ * Set *SUBTREES_WITH_MERGEINFO to a hash mapping (const char *) absolute
+ * WC path to (svn_mergeinfo_t *) mergeinfo.
+ *
+ * ### Is this function equivalent to:
+ *
+ * svn_client__get_wc_mergeinfo_catalog(
+ * subtrees_with_mergeinfo, inherited=NULL, include_descendants=TRUE,
+ * svn_mergeinfo_explicit, target_abspath, limit_path=NULL,
+ * walked_path=NULL, ignore_invalid_mergeinfo=FALSE, ...)
+ *
+ * except for the catalog keys being abspaths instead of repo-relpaths?
+ */
+static svn_error_t *
+get_wc_explicit_mergeinfo_catalog(apr_hash_t **subtrees_with_mergeinfo,
+ const char *target_abspath,
+ svn_depth_t depth,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_opt_revision_t working_revision = { svn_opt_revision_working, { 0 } };
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_hash_index_t *hi;
+ apr_hash_t *externals;
+
+ SVN_ERR(svn_client_propget5(subtrees_with_mergeinfo, NULL,
+ SVN_PROP_MERGEINFO, target_abspath,
+ &working_revision, &working_revision, NULL,
+ depth, NULL, ctx, result_pool, scratch_pool));
+
+ SVN_ERR(svn_wc__externals_defined_below(&externals, ctx->wc_ctx,
+ target_abspath, scratch_pool,
+ scratch_pool));
+
+ /* Convert property values to svn_mergeinfo_t. */
+ for (hi = apr_hash_first(scratch_pool, *subtrees_with_mergeinfo);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *wc_path = svn__apr_hash_index_key(hi);
+ svn_string_t *mergeinfo_string = svn__apr_hash_index_val(hi);
+ svn_mergeinfo_t mergeinfo;
+ svn_error_t *err;
+
+ /* svn_client_propget5 picks up file externals with
+ mergeinfo, but we don't want those. */
+ if (svn_hash_gets(externals, wc_path))
+ {
+ svn_hash_sets(*subtrees_with_mergeinfo, wc_path, NULL);
+ continue;
+ }
+
+ svn_pool_clear(iterpool);
+
+ err = svn_mergeinfo_parse(&mergeinfo, mergeinfo_string->data,
+ result_pool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
+ {
+ err = svn_error_createf(
+ SVN_ERR_CLIENT_INVALID_MERGEINFO_NO_MERGETRACKING, err,
+ _("Invalid mergeinfo detected on '%s', "
+ "merge tracking not possible"),
+ svn_dirent_local_style(wc_path, scratch_pool));
+ }
+ return svn_error_trace(err);
+ }
+ svn_hash_sets(*subtrees_with_mergeinfo, wc_path, mergeinfo);
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for do_directory_merge() when performing merge-tracking aware
+ merges.
+
+ Walk of the working copy tree rooted at TARGET->abspath to
+ depth DEPTH. Create an svn_client__merge_path_t * for any path which meets
+ one or more of the following criteria:
+
+ 1) Path has working svn:mergeinfo.
+ 2) Path is switched.
+ 3) Path is a subtree of the merge target (i.e. is not equal to
+ TARGET->abspath) and has no mergeinfo of its own but
+ its immediate parent has mergeinfo with non-inheritable ranges. If
+ this isn't a dry-run and the merge is between differences in the same
+ repository, then this function will set working mergeinfo on the path
+ equal to the mergeinfo inheritable from its parent.
+ 4) Path has an immediate child (or children) missing from the WC because
+ the child is switched or absent from the WC, or due to a sparse
+ checkout.
+ 5) Path has a sibling (or siblings) missing from the WC because the
+ sibling is switched, absent, scheduled for deletion, or missing due to
+ a sparse checkout.
+ 6) Path is absent from disk due to an authz restriction.
+ 7) Path is equal to TARGET->abspath.
+ 8) Path is an immediate *directory* child of
+ TARGET->abspath and DEPTH is svn_depth_immediates.
+ 9) Path is an immediate *file* child of TARGET->abspath
+ and DEPTH is svn_depth_files.
+ 10) Path is at a depth of 'empty' or 'files'.
+ 11) Path is missing from disk (e.g. due to an OS-level deletion).
+
+ If subtrees within the requested DEPTH are unexpectedly missing disk,
+ then raise SVN_ERR_CLIENT_NOT_READY_TO_MERGE.
+
+ Store the svn_client__merge_path_t *'s in *CHILDREN_WITH_MERGEINFO in
+ depth-first order based on the svn_client__merge_path_t *s path member as
+ sorted by svn_path_compare_paths(). Set the remaining_ranges field of each
+ element to NULL.
+
+ Note: Since the walk is rooted at TARGET->abspath, the
+ latter is guaranteed to be in *CHILDREN_WITH_MERGEINFO and due to the
+ depth-first ordering it is guaranteed to be the first element in
+ *CHILDREN_WITH_MERGEINFO.
+
+ MERGE_CMD_BATON is cascaded from the argument of the same name in
+ do_directory_merge().
+*/
+static svn_error_t *
+get_mergeinfo_paths(apr_array_header_t *children_with_mergeinfo,
+ const merge_target_t *target,
+ svn_depth_t depth,
+ svn_boolean_t dry_run,
+ svn_boolean_t same_repos,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_hash_t *subtrees_with_mergeinfo;
+ apr_hash_t *excluded_subtrees;
+ apr_hash_t *switched_subtrees;
+ apr_hash_t *shallow_subtrees;
+ apr_hash_t *missing_subtrees;
+ struct pre_merge_status_baton_t pre_merge_status_baton;
+
+ /* Case 1: Subtrees with explicit mergeinfo. */
+ SVN_ERR(get_wc_explicit_mergeinfo_catalog(&subtrees_with_mergeinfo,
+ target->abspath,
+ depth, ctx,
+ result_pool, scratch_pool));
+ if (subtrees_with_mergeinfo)
+ {
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(scratch_pool, subtrees_with_mergeinfo);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *wc_path = svn__apr_hash_index_key(hi);
+ svn_mergeinfo_t mergeinfo = svn__apr_hash_index_val(hi);
+ svn_client__merge_path_t *mergeinfo_child =
+ svn_client__merge_path_create(wc_path, result_pool);
+
+ svn_pool_clear(iterpool);
+
+ /* Stash this child's pre-existing mergeinfo. */
+ mergeinfo_child->pre_merge_mergeinfo = mergeinfo;
+
+ /* Note if this child has non-inheritable mergeinfo */
+ mergeinfo_child->has_noninheritable
+ = svn_mergeinfo__is_noninheritable(
+ mergeinfo_child->pre_merge_mergeinfo, iterpool);
+
+ /* Append it. We'll sort below. */
+ APR_ARRAY_PUSH(children_with_mergeinfo, svn_client__merge_path_t *)
+ = svn_client__merge_path_dup(mergeinfo_child, result_pool);
+ }
+
+ /* Sort CHILDREN_WITH_MERGEINFO by each child's path (i.e. as per
+ compare_merge_path_t_as_paths). Any subsequent insertions of new
+ children with insert_child_to_merge() require this ordering. */
+ qsort(children_with_mergeinfo->elts,
+ children_with_mergeinfo->nelts,
+ children_with_mergeinfo->elt_size,
+ compare_merge_path_t_as_paths);
+ }
+
+ /* Case 2: Switched subtrees
+ Case 10: Paths at depths of 'empty' or 'files'
+ Case 11: Paths missing from disk */
+ pre_merge_status_baton.wc_ctx = ctx->wc_ctx;
+ switched_subtrees = apr_hash_make(scratch_pool);
+ pre_merge_status_baton.switched_subtrees = switched_subtrees;
+ shallow_subtrees = apr_hash_make(scratch_pool);
+ pre_merge_status_baton.shallow_subtrees = shallow_subtrees;
+ missing_subtrees = apr_hash_make(scratch_pool);
+ pre_merge_status_baton.missing_subtrees = missing_subtrees;
+ pre_merge_status_baton.pool = scratch_pool;
+ SVN_ERR(svn_wc_walk_status(ctx->wc_ctx,
+ target->abspath,
+ depth,
+ TRUE /* get_all */,
+ FALSE /* no_ignore */,
+ TRUE /* ignore_text_mods */,
+ NULL /* ingore_patterns */,
+ pre_merge_status_cb, &pre_merge_status_baton,
+ ctx->cancel_func, ctx->cancel_baton,
+ scratch_pool));
+
+ /* Issue #2915: Raise an error describing the roots of any missing
+ subtrees, i.e. those that the WC thinks are on disk but have been
+ removed outside of Subversion. */
+ if (apr_hash_count(missing_subtrees))
+ {
+ apr_hash_index_t *hi;
+ svn_stringbuf_t *missing_subtree_err_buf =
+ svn_stringbuf_create(_("Merge tracking not allowed with missing "
+ "subtrees; try restoring these items "
+ "first:\n"), scratch_pool);
+
+ for (hi = apr_hash_first(scratch_pool, missing_subtrees);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ svn_pool_clear(iterpool);
+ svn_stringbuf_appendcstr(missing_subtree_err_buf,
+ svn_dirent_local_style(
+ svn__apr_hash_index_key(hi), iterpool));
+ svn_stringbuf_appendcstr(missing_subtree_err_buf, "\n");
+ }
+
+ return svn_error_create(SVN_ERR_CLIENT_NOT_READY_TO_MERGE,
+ NULL, missing_subtree_err_buf->data);
+ }
+
+ if (apr_hash_count(switched_subtrees))
+ {
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(scratch_pool, switched_subtrees);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *wc_path = svn__apr_hash_index_key(hi);
+ svn_client__merge_path_t *child = get_child_with_mergeinfo(
+ children_with_mergeinfo, wc_path);
+
+ if (child)
+ {
+ child->switched = TRUE;
+ }
+ else
+ {
+ svn_client__merge_path_t *switched_child =
+ svn_client__merge_path_create(wc_path, result_pool);
+ switched_child->switched = TRUE;
+ insert_child_to_merge(children_with_mergeinfo, switched_child,
+ result_pool);
+ }
+ }
+ }
+
+ if (apr_hash_count(shallow_subtrees))
+ {
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(scratch_pool, shallow_subtrees);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ svn_boolean_t new_shallow_child = FALSE;
+ const char *wc_path = svn__apr_hash_index_key(hi);
+ svn_depth_t *child_depth = svn__apr_hash_index_val(hi);
+ svn_client__merge_path_t *shallow_child = get_child_with_mergeinfo(
+ children_with_mergeinfo, wc_path);
+
+ if (shallow_child)
+ {
+ if (*child_depth == svn_depth_empty
+ || *child_depth == svn_depth_files)
+ shallow_child->missing_child = TRUE;
+ }
+ else
+ {
+ shallow_child = svn_client__merge_path_create(wc_path,
+ result_pool);
+ new_shallow_child = TRUE;
+
+ if (*child_depth == svn_depth_empty
+ || *child_depth == svn_depth_files)
+ shallow_child->missing_child = TRUE;
+ }
+
+ /* A little trickery: If PATH doesn't have any mergeinfo or has
+ only inheritable mergeinfo, we still describe it as having
+ non-inheritable mergeinfo if it is missing a child due to
+ a shallow depth. Why? Because the mergeinfo we'll add to PATH
+ to describe the merge must be non-inheritable, so PATH's missing
+ children don't inherit it. Marking these PATHs as non-
+ inheritable allows the logic for case 3 to properly account
+ for PATH's children. */
+ if (!shallow_child->has_noninheritable
+ && (*child_depth == svn_depth_empty
+ || *child_depth == svn_depth_files))
+ {
+ shallow_child->has_noninheritable = TRUE;
+ }
+
+ if (new_shallow_child)
+ insert_child_to_merge(children_with_mergeinfo, shallow_child,
+ result_pool);
+ }
+ }
+
+ /* Case 6: Paths absent from disk due to server or user exclusion. */
+ SVN_ERR(svn_wc__get_excluded_subtrees(&excluded_subtrees,
+ ctx->wc_ctx, target->abspath,
+ result_pool, scratch_pool));
+ if (excluded_subtrees)
+ {
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(scratch_pool, excluded_subtrees);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *wc_path = svn__apr_hash_index_key(hi);
+ svn_client__merge_path_t *child = get_child_with_mergeinfo(
+ children_with_mergeinfo, wc_path);
+
+ if (child)
+ {
+ child->absent = TRUE;
+ }
+ else
+ {
+ svn_client__merge_path_t *absent_child =
+ svn_client__merge_path_create(wc_path, result_pool);
+ absent_child->absent = TRUE;
+ insert_child_to_merge(children_with_mergeinfo, absent_child,
+ result_pool);
+ }
+ }
+ }
+
+ /* Case 7: The merge target MERGE_CMD_BATON->target->abspath is always
+ present. */
+ if (!get_child_with_mergeinfo(children_with_mergeinfo,
+ target->abspath))
+ {
+ svn_client__merge_path_t *target_child =
+ svn_client__merge_path_create(target->abspath,
+ result_pool);
+ insert_child_to_merge(children_with_mergeinfo, target_child,
+ result_pool);
+ }
+
+ /* Case 8: Path is an immediate *directory* child of
+ MERGE_CMD_BATON->target->abspath and DEPTH is svn_depth_immediates.
+
+ Case 9: Path is an immediate *file* child of
+ MERGE_CMD_BATON->target->abspath and DEPTH is svn_depth_files. */
+ if (depth == svn_depth_immediates || depth == svn_depth_files)
+ {
+ int j;
+ const apr_array_header_t *immediate_children;
+
+ SVN_ERR(svn_wc__node_get_children_of_working_node(
+ &immediate_children, ctx->wc_ctx,
+ target->abspath, FALSE, scratch_pool, scratch_pool));
+
+ for (j = 0; j < immediate_children->nelts; j++)
+ {
+ const char *immediate_child_abspath =
+ APR_ARRAY_IDX(immediate_children, j, const char *);
+ svn_node_kind_t immediate_child_kind;
+
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_wc_read_kind2(&immediate_child_kind,
+ ctx->wc_ctx, immediate_child_abspath,
+ FALSE, FALSE, iterpool));
+ if ((immediate_child_kind == svn_node_dir
+ && depth == svn_depth_immediates)
+ || (immediate_child_kind == svn_node_file
+ && depth == svn_depth_files))
+ {
+ if (!get_child_with_mergeinfo(children_with_mergeinfo,
+ immediate_child_abspath))
+ {
+ svn_client__merge_path_t *immediate_child =
+ svn_client__merge_path_create(immediate_child_abspath,
+ result_pool);
+
+ if (immediate_child_kind == svn_node_dir
+ && depth == svn_depth_immediates)
+ immediate_child->immediate_child_dir = TRUE;
+
+ insert_child_to_merge(children_with_mergeinfo,
+ immediate_child, result_pool);
+ }
+ }
+ }
+ }
+
+ /* If DEPTH isn't empty then cover cases 3), 4), and 5), possibly adding
+ elements to CHILDREN_WITH_MERGEINFO. */
+ if (depth <= svn_depth_empty)
+ return SVN_NO_ERROR;
+
+ for (i = 0; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i,
+ svn_client__merge_path_t *);
+ svn_pool_clear(iterpool);
+
+ /* Case 3) Where merging to a path with a switched child the path
+ gets non-inheritable mergeinfo for the merge range performed and
+ the child gets its own set of mergeinfo. If the switched child
+ later "returns", e.g. a switched path is unswitched, the child
+ may not have any explicit mergeinfo. If the initial merge is
+ repeated we don't want to repeat the merge for the path, but we
+ do want to repeat it for the previously switched child. To
+ ensure this we check if all of CHILD's non-missing children have
+ explicit mergeinfo (they should already be present in
+ CHILDREN_WITH_MERGEINFO if they do). If not,
+ add the children without mergeinfo to CHILDREN_WITH_MERGEINFO so
+ do_directory_merge() will merge them independently.
+
+ But that's not enough! Since do_directory_merge() performs
+ the merges on the paths in CHILDREN_WITH_MERGEINFO in a depth
+ first manner it will merge the previously switched path's parent
+ first. As part of this merge it will update the parent's
+ previously non-inheritable mergeinfo and make it inheritable
+ (since it notices the path has no missing children), then when
+ do_directory_merge() finally merges the previously missing
+ child it needs to get mergeinfo from the child's nearest
+ ancestor, but since do_directory_merge() already tweaked that
+ mergeinfo, removing the non-inheritable flag, it appears that the
+ child already has been merged to. To prevent this we set
+ override mergeinfo on the child now, before any merging is done,
+ so it has explicit mergeinfo that reflects only CHILD's
+ inheritable mergeinfo. */
+
+ /* If depth is immediates or files then don't add new children if
+ CHILD is a subtree of the merge target; those children are below
+ the operational depth of the merge. */
+ if (child->has_noninheritable
+ && (i == 0 || depth == svn_depth_infinity))
+ {
+ const apr_array_header_t *children;
+ int j;
+
+ SVN_ERR(svn_wc__node_get_children(&children,
+ ctx->wc_ctx,
+ child->abspath, FALSE,
+ iterpool, iterpool));
+ for (j = 0; j < children->nelts; j++)
+ {
+ svn_client__merge_path_t *child_of_noninheritable;
+ const char *child_abspath = APR_ARRAY_IDX(children, j,
+ const char*);
+
+ /* Does this child already exist in CHILDREN_WITH_MERGEINFO?
+ If not, create it and insert it into
+ CHILDREN_WITH_MERGEINFO and set override mergeinfo on
+ it. */
+ child_of_noninheritable =
+ get_child_with_mergeinfo(children_with_mergeinfo,
+ child_abspath);
+ if (!child_of_noninheritable)
+ {
+ /* Don't add directory children if DEPTH
+ is svn_depth_files. */
+ if (depth == svn_depth_files)
+ {
+ svn_node_kind_t child_kind;
+ SVN_ERR(svn_wc_read_kind2(&child_kind,
+ ctx->wc_ctx, child_abspath,
+ FALSE, FALSE, iterpool));
+ if (child_kind != svn_node_file)
+ continue;
+ }
+ /* else DEPTH is infinity or immediates so we want both
+ directory and file children. */
+
+ child_of_noninheritable =
+ svn_client__merge_path_create(child_abspath, result_pool);
+ child_of_noninheritable->child_of_noninheritable = TRUE;
+ insert_child_to_merge(children_with_mergeinfo,
+ child_of_noninheritable,
+ result_pool);
+ if (!dry_run && same_repos)
+ {
+ svn_mergeinfo_t mergeinfo;
+
+ SVN_ERR(svn_client__get_wc_mergeinfo(
+ &mergeinfo, NULL,
+ svn_mergeinfo_nearest_ancestor,
+ child_of_noninheritable->abspath,
+ target->abspath, NULL, FALSE,
+ ctx, iterpool, iterpool));
+
+ SVN_ERR(svn_client__record_wc_mergeinfo(
+ child_of_noninheritable->abspath, mergeinfo,
+ FALSE, ctx, iterpool));
+ }
+ }
+ }
+ }
+ /* Case 4 and 5 are handled by the following function. */
+ SVN_ERR(insert_parent_and_sibs_of_sw_absent_del_subtree(
+ children_with_mergeinfo, target, &i, child,
+ depth, ctx, result_pool));
+ } /* i < children_with_mergeinfo->nelts */
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Implements the svn_log_entry_receiver_t interface.
+ *
+ * BATON is an 'apr_array_header_t *' array of 'svn_revnum_t'.
+ * Push a copy of LOG_ENTRY->revision onto BATON. Thus, a
+ * series of invocations of this callback accumulates the
+ * corresponding set of revisions into BATON.
+ */
+static svn_error_t *
+log_changed_revs(void *baton,
+ svn_log_entry_t *log_entry,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *revs = baton;
+
+ APR_ARRAY_PUSH(revs, svn_revnum_t) = log_entry->revision;
+ return SVN_NO_ERROR;
+}
+
+
+/* Set *MIN_REV_P to the oldest and *MAX_REV_P to the youngest start or end
+ * revision occurring in RANGELIST, or to SVN_INVALID_REVNUM if RANGELIST
+ * is empty. */
+static void
+merge_range_find_extremes(svn_revnum_t *min_rev_p,
+ svn_revnum_t *max_rev_p,
+ const svn_rangelist_t *rangelist)
+{
+ int i;
+
+ *min_rev_p = SVN_INVALID_REVNUM;
+ *max_rev_p = SVN_INVALID_REVNUM;
+ for (i = 0; i < rangelist->nelts; i++)
+ {
+ svn_merge_range_t *range
+ = APR_ARRAY_IDX(rangelist, i, svn_merge_range_t *);
+ svn_revnum_t range_min = MIN(range->start, range->end);
+ svn_revnum_t range_max = MAX(range->start, range->end);
+
+ if ((! SVN_IS_VALID_REVNUM(*min_rev_p)) || (range_min < *min_rev_p))
+ *min_rev_p = range_min;
+ if ((! SVN_IS_VALID_REVNUM(*max_rev_p)) || (range_max > *max_rev_p))
+ *max_rev_p = range_max;
+ }
+}
+
+/* Wrapper around svn_ra_get_log2(). Invoke RECEIVER with RECEIVER_BATON
+ * on each commit from YOUNGEST_REV to OLDEST_REV in which TARGET_RELPATH
+ * changed. TARGET_RELPATH is relative to RA_SESSION's URL.
+ * Important: Revision properties are not retrieved by this function for
+ * performance reasons.
+ */
+static svn_error_t *
+get_log(svn_ra_session_t *ra_session,
+ const char *target_relpath,
+ svn_revnum_t youngest_rev,
+ svn_revnum_t oldest_rev,
+ svn_boolean_t discover_changed_paths,
+ svn_log_entry_receiver_t receiver,
+ void *receiver_baton,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *log_targets;
+ apr_array_header_t *revprops;
+
+ log_targets = apr_array_make(pool, 1, sizeof(const char *));
+ APR_ARRAY_PUSH(log_targets, const char *) = target_relpath;
+
+ revprops = apr_array_make(pool, 0, sizeof(const char *));
+
+ SVN_ERR(svn_ra_get_log2(ra_session, log_targets, youngest_rev,
+ oldest_rev, 0 /* limit */, discover_changed_paths,
+ FALSE /* strict_node_history */,
+ FALSE /* include_merged_revisions */,
+ revprops, receiver, receiver_baton, pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Set *OPERATIVE_RANGES_P to an array of svn_merge_range_t * merge
+ range objects copied wholesale from RANGES which have the property
+ that in some revision within that range the object identified by
+ RA_SESSION was modified (if by "modified" we mean "'svn log' would
+ return that revision). *OPERATIVE_RANGES_P is allocated from the
+ same pool as RANGES, and the ranges within it are shared with
+ RANGES, too.
+
+ *OPERATIVE_RANGES_P may be the same as RANGES (that is, the output
+ parameter is set only after the input is no longer used).
+
+ Use POOL for temporary allocations. */
+static svn_error_t *
+remove_noop_merge_ranges(svn_rangelist_t **operative_ranges_p,
+ svn_ra_session_t *ra_session,
+ const svn_rangelist_t *ranges,
+ apr_pool_t *pool)
+{
+ int i;
+ svn_revnum_t oldest_rev, youngest_rev;
+ apr_array_header_t *changed_revs =
+ apr_array_make(pool, ranges->nelts, sizeof(svn_revnum_t));
+ svn_rangelist_t *operative_ranges =
+ apr_array_make(ranges->pool, ranges->nelts, ranges->elt_size);
+
+ /* Find the revision extremes of the RANGES we have. */
+ merge_range_find_extremes(&oldest_rev, &youngest_rev, ranges);
+ if (SVN_IS_VALID_REVNUM(oldest_rev))
+ oldest_rev++; /* make it inclusive */
+
+ /* Get logs across those ranges, recording which revisions hold
+ changes to our object's history. */
+ SVN_ERR(get_log(ra_session, "", youngest_rev, oldest_rev, FALSE,
+ log_changed_revs, changed_revs, pool));
+
+ /* Are there *any* changes? */
+ if (changed_revs->nelts)
+ {
+ /* Our list of changed revisions should be in youngest-to-oldest
+ order. */
+ svn_revnum_t youngest_changed_rev
+ = APR_ARRAY_IDX(changed_revs, 0, svn_revnum_t);
+ svn_revnum_t oldest_changed_rev
+ = APR_ARRAY_IDX(changed_revs, changed_revs->nelts - 1, svn_revnum_t);
+
+ /* Now, copy from RANGES to *OPERATIVE_RANGES, filtering out ranges
+ that aren't operative (by virtue of not having any revisions
+ represented in the CHANGED_REVS array). */
+ for (i = 0; i < ranges->nelts; i++)
+ {
+ svn_merge_range_t *range = APR_ARRAY_IDX(ranges, i,
+ svn_merge_range_t *);
+ svn_revnum_t range_min = MIN(range->start, range->end) + 1;
+ svn_revnum_t range_max = MAX(range->start, range->end);
+ int j;
+
+ /* If the merge range is entirely outside the range of changed
+ revisions, we've no use for it. */
+ if ((range_min > youngest_changed_rev)
+ || (range_max < oldest_changed_rev))
+ continue;
+
+ /* Walk through the changed_revs to see if any of them fall
+ inside our current range. */
+ for (j = 0; j < changed_revs->nelts; j++)
+ {
+ svn_revnum_t changed_rev
+ = APR_ARRAY_IDX(changed_revs, j, svn_revnum_t);
+ if ((changed_rev >= range_min) && (changed_rev <= range_max))
+ {
+ APR_ARRAY_PUSH(operative_ranges, svn_merge_range_t *) =
+ range;
+ break;
+ }
+ }
+ }
+ }
+
+ *operative_ranges_p = operative_ranges;
+ return SVN_NO_ERROR;
+}
+
+
+/*-----------------------------------------------------------------------*/
+
+/*** Merge Source Normalization ***/
+
+/* qsort-compatible sort routine, rating merge_source_t * objects to
+ be in descending (youngest-to-oldest) order based on their ->loc1->rev
+ component. */
+static int
+compare_merge_source_ts(const void *a,
+ const void *b)
+{
+ svn_revnum_t a_rev = (*(const merge_source_t *const *)a)->loc1->rev;
+ svn_revnum_t b_rev = (*(const merge_source_t *const *)b)->loc1->rev;
+ if (a_rev == b_rev)
+ return 0;
+ return a_rev < b_rev ? 1 : -1;
+}
+
+/* Set *MERGE_SOURCE_TS_P to a list of merge sources generated by
+ slicing history location SEGMENTS with a given requested merge
+ RANGE. Use SOURCE_LOC for full source URL calculation.
+
+ Order the merge sources in *MERGE_SOURCE_TS_P from oldest to
+ youngest. */
+static svn_error_t *
+combine_range_with_segments(apr_array_header_t **merge_source_ts_p,
+ const svn_merge_range_t *range,
+ const apr_array_header_t *segments,
+ const svn_client__pathrev_t *source_loc,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *merge_source_ts =
+ apr_array_make(pool, 1, sizeof(merge_source_t *));
+ svn_revnum_t minrev = MIN(range->start, range->end) + 1;
+ svn_revnum_t maxrev = MAX(range->start, range->end);
+ svn_boolean_t subtractive = (range->start > range->end);
+ int i;
+
+ for (i = 0; i < segments->nelts; i++)
+ {
+ svn_location_segment_t *segment =
+ APR_ARRAY_IDX(segments, i, svn_location_segment_t *);
+ svn_client__pathrev_t *loc1, *loc2;
+ merge_source_t *merge_source;
+ const char *path1 = NULL;
+ svn_revnum_t rev1;
+
+ /* If this segment doesn't overlap our range at all, or
+ represents a gap, ignore it. */
+ if ((segment->range_end < minrev)
+ || (segment->range_start > maxrev)
+ || (! segment->path))
+ continue;
+
+ /* If our range spans a segment boundary, we have to point our
+ merge_source_t's path1 to the path of the immediately older
+ segment, else it points to the same location as its path2. */
+ rev1 = MAX(segment->range_start, minrev) - 1;
+ if (minrev <= segment->range_start)
+ {
+ if (i > 0)
+ {
+ path1 = (APR_ARRAY_IDX(segments, i - 1,
+ svn_location_segment_t *))->path;
+ }
+ /* If we've backed PATH1 up into a segment gap, let's back
+ it up further still to the segment before the gap. We'll
+ have to adjust rev1, too. */
+ if ((! path1) && (i > 1))
+ {
+ path1 = (APR_ARRAY_IDX(segments, i - 2,
+ svn_location_segment_t *))->path;
+ rev1 = (APR_ARRAY_IDX(segments, i - 2,
+ svn_location_segment_t *))->range_end;
+ }
+ }
+ else
+ {
+ path1 = apr_pstrdup(pool, segment->path);
+ }
+
+ /* If we don't have two valid paths, we won't know what to do
+ when merging. This could happen if someone requested a merge
+ where the source didn't exist in a particular revision or
+ something. The merge code would probably bomb out anyway, so
+ we'll just *not* create a merge source in this case. */
+ if (! (path1 && segment->path))
+ continue;
+
+ /* Build our merge source structure. */
+ loc1 = svn_client__pathrev_create_with_relpath(
+ source_loc->repos_root_url, source_loc->repos_uuid,
+ rev1, path1, pool);
+ loc2 = svn_client__pathrev_create_with_relpath(
+ source_loc->repos_root_url, source_loc->repos_uuid,
+ MIN(segment->range_end, maxrev), segment->path, pool);
+ /* If this is subtractive, reverse the whole calculation. */
+ if (subtractive)
+ merge_source = merge_source_create(loc2, loc1, TRUE /* ancestral */,
+ pool);
+ else
+ merge_source = merge_source_create(loc1, loc2, TRUE /* ancestral */,
+ pool);
+
+ APR_ARRAY_PUSH(merge_source_ts, merge_source_t *) = merge_source;
+ }
+
+ /* If this was a subtractive merge, and we created more than one
+ merge source, we need to reverse the sort ordering of our sources. */
+ if (subtractive && (merge_source_ts->nelts > 1))
+ qsort(merge_source_ts->elts, merge_source_ts->nelts,
+ merge_source_ts->elt_size, compare_merge_source_ts);
+
+ *merge_source_ts_p = merge_source_ts;
+ return SVN_NO_ERROR;
+}
+
+/* Similar to normalize_merge_sources() except the input MERGE_RANGE_TS is a
+ * rangelist.
+ */
+static svn_error_t *
+normalize_merge_sources_internal(apr_array_header_t **merge_sources_p,
+ const svn_client__pathrev_t *source_loc,
+ const svn_rangelist_t *merge_range_ts,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_revnum_t source_peg_revnum = source_loc->rev;
+ svn_revnum_t oldest_requested, youngest_requested;
+ svn_revnum_t trim_revision = SVN_INVALID_REVNUM;
+ apr_array_header_t *segments;
+ int i;
+
+ /* Initialize our return variable. */
+ *merge_sources_p = apr_array_make(result_pool, 1, sizeof(merge_source_t *));
+
+ /* No ranges to merge? No problem. */
+ if (merge_range_ts->nelts == 0)
+ return SVN_NO_ERROR;
+
+ /* Find the extremes of the revisions across our set of ranges. */
+ merge_range_find_extremes(&oldest_requested, &youngest_requested,
+ merge_range_ts);
+
+ /* ### FIXME: Our underlying APIs can't yet handle the case where
+ the peg revision isn't the youngest of the three revisions. So
+ we'll just verify that the source in the peg revision is related
+ to the source in the youngest requested revision (which is
+ all the underlying APIs would do in this case right now anyway). */
+ if (source_peg_revnum < youngest_requested)
+ {
+ svn_client__pathrev_t *start_loc;
+
+ SVN_ERR(svn_client__repos_location(&start_loc,
+ ra_session, source_loc,
+ youngest_requested,
+ ctx, scratch_pool, scratch_pool));
+ source_peg_revnum = youngest_requested;
+ }
+
+ /* Fetch the locations for our merge range span. */
+ SVN_ERR(svn_client__repos_location_segments(&segments,
+ ra_session, source_loc->url,
+ source_peg_revnum,
+ youngest_requested,
+ oldest_requested,
+ ctx, result_pool));
+
+ /* See if we fetched enough history to do the job. "Surely we did,"
+ you say. "After all, we covered the entire requested merge
+ range." Yes, that's true, but if our first segment doesn't
+ extend back to the oldest request revision, we've got a special
+ case to deal with. Or if the first segment represents a gap,
+ that's another special case. */
+ trim_revision = SVN_INVALID_REVNUM;
+ if (segments->nelts)
+ {
+ svn_location_segment_t *first_segment =
+ APR_ARRAY_IDX(segments, 0, svn_location_segment_t *);
+
+ /* If the first segment doesn't start with the OLDEST_REQUESTED
+ revision, we'll need to pass a trim revision to our range
+ cruncher. */
+ if (first_segment->range_start != oldest_requested)
+ {
+ trim_revision = first_segment->range_start;
+ }
+
+ /* Else, if the first segment has no path (and therefore is a
+ gap), then we'll fetch the copy source revision from the
+ second segment (provided there is one, of course) and use it
+ to prepend an extra pathful segment to our list.
+
+ ### We could avoid this bit entirely if we'd passed
+ ### SVN_INVALID_REVNUM instead of OLDEST_REQUESTED to
+ ### svn_client__repos_location_segments(), but that would
+ ### really penalize clients hitting pre-1.5 repositories with
+ ### the typical small merge range request (because of the
+ ### lack of a node-origins cache in the repository). */
+ else if (! first_segment->path)
+ {
+ if (segments->nelts > 1)
+ {
+ svn_location_segment_t *second_segment =
+ APR_ARRAY_IDX(segments, 1, svn_location_segment_t *);
+ const char *segment_url;
+ const char *original_repos_relpath;
+ svn_revnum_t original_revision;
+ svn_opt_revision_t range_start_rev;
+ range_start_rev.kind = svn_opt_revision_number;
+ range_start_rev.value.number = second_segment->range_start;
+
+ segment_url = svn_path_url_add_component2(
+ source_loc->repos_root_url, second_segment->path,
+ scratch_pool);
+ SVN_ERR(svn_client__get_copy_source(&original_repos_relpath,
+ &original_revision,
+ segment_url,
+ &range_start_rev, ctx,
+ result_pool, scratch_pool));
+ /* Got copyfrom data? Fix up the first segment to cover
+ back to COPYFROM_REV + 1, and then prepend a new
+ segment covering just COPYFROM_REV. */
+ if (original_repos_relpath)
+ {
+ svn_location_segment_t *new_segment =
+ apr_pcalloc(result_pool, sizeof(*new_segment));
+
+ new_segment->path = original_repos_relpath;
+ new_segment->range_start = original_revision;
+ new_segment->range_end = original_revision;
+ svn_sort__array_insert(&new_segment, segments, 0);
+ }
+ }
+ }
+ }
+
+ /* For each range in our requested range set, try to determine the
+ path(s) associated with that range. */
+ for (i = 0; i < merge_range_ts->nelts; i++)
+ {
+ svn_merge_range_t *range =
+ APR_ARRAY_IDX(merge_range_ts, i, svn_merge_range_t *);
+ apr_array_header_t *merge_sources;
+
+ if (SVN_IS_VALID_REVNUM(trim_revision))
+ {
+ /* If the range predates the trim revision, discard it. */
+ if (MAX(range->start, range->end) < trim_revision)
+ continue;
+
+ /* If the range overlaps the trim revision, trim it. */
+ if (range->start < trim_revision)
+ range->start = trim_revision;
+ if (range->end < trim_revision)
+ range->end = trim_revision;
+ }
+
+ /* Copy the resulting merge sources into master list thereof. */
+ SVN_ERR(combine_range_with_segments(&merge_sources, range,
+ segments, source_loc,
+ result_pool));
+ apr_array_cat(*merge_sources_p, merge_sources);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Determine the normalized ranges to merge from a given line of history.
+
+ Calculate the result by intersecting the list of location segments at
+ which SOURCE_LOC existed along its line of history with the requested
+ revision ranges in RANGES_TO_MERGE. RANGES_TO_MERGE is an array of
+ (svn_opt_revision_range_t *) revision ranges. Use SOURCE_PATH_OR_URL to
+ resolve any WC-relative revision specifiers (such as 'base') in
+ RANGES_TO_MERGE.
+
+ Set *MERGE_SOURCES_P to an array of merge_source_t * objects, each
+ describing a normalized range of revisions to be merged from the line
+ history of SOURCE_LOC. Order the objects from oldest to youngest.
+
+ RA_SESSION is an RA session open to the repository of SOURCE_LOC; it may
+ be temporarily reparented within this function. Use RA_SESSION to find
+ the location segments along the line of history of SOURCE_LOC.
+
+ Allocate MERGE_SOURCES_P and its contents in RESULT_POOL.
+
+ See `MERGEINFO MERGE SOURCE NORMALIZATION' for more on the
+ background of this function.
+*/
+static svn_error_t *
+normalize_merge_sources(apr_array_header_t **merge_sources_p,
+ const char *source_path_or_url,
+ const svn_client__pathrev_t *source_loc,
+ const apr_array_header_t *ranges_to_merge,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *source_abspath_or_url;
+ svn_revnum_t youngest_rev = SVN_INVALID_REVNUM;
+ svn_rangelist_t *merge_range_ts;
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ if(!svn_path_is_url(source_path_or_url))
+ SVN_ERR(svn_dirent_get_absolute(&source_abspath_or_url, source_path_or_url,
+ scratch_pool));
+ else
+ source_abspath_or_url = source_path_or_url;
+
+ /* Create a list to hold svn_merge_range_t's. */
+ merge_range_ts = apr_array_make(scratch_pool, ranges_to_merge->nelts,
+ sizeof(svn_merge_range_t *));
+
+ for (i = 0; i < ranges_to_merge->nelts; i++)
+ {
+ svn_opt_revision_range_t *range
+ = APR_ARRAY_IDX(ranges_to_merge, i, svn_opt_revision_range_t *);
+ svn_merge_range_t mrange;
+
+ svn_pool_clear(iterpool);
+
+ /* Resolve revisions to real numbers, validating as we go. */
+ if ((range->start.kind == svn_opt_revision_unspecified)
+ || (range->end.kind == svn_opt_revision_unspecified))
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Not all required revisions are specified"));
+
+ SVN_ERR(svn_client__get_revision_number(&mrange.start, &youngest_rev,
+ ctx->wc_ctx,
+ source_abspath_or_url,
+ ra_session, &range->start,
+ iterpool));
+ SVN_ERR(svn_client__get_revision_number(&mrange.end, &youngest_rev,
+ ctx->wc_ctx,
+ source_abspath_or_url,
+ ra_session, &range->end,
+ iterpool));
+
+ /* If this isn't a no-op range... */
+ if (mrange.start != mrange.end)
+ {
+ /* ...then add it to the list. */
+ mrange.inheritable = TRUE;
+ APR_ARRAY_PUSH(merge_range_ts, svn_merge_range_t *)
+ = svn_merge_range_dup(&mrange, scratch_pool);
+ }
+ }
+
+ SVN_ERR(normalize_merge_sources_internal(
+ merge_sources_p, source_loc,
+ merge_range_ts, ra_session, ctx, result_pool, scratch_pool));
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+
+/*-----------------------------------------------------------------------*/
+
+/*** Merge Workhorse Functions ***/
+
+/* Helper for do_directory_merge() and do_file_merge() which filters out a
+ path's own natural history from the mergeinfo describing a merge.
+
+ Given the natural history IMPLICIT_MERGEINFO of some wc merge target path,
+ the repository-relative merge source path SOURCE_REL_PATH, and the
+ requested merge range REQUESTED_RANGE from SOURCE_REL_PATH, remove any
+ portion of REQUESTED_RANGE which is already described in
+ IMPLICIT_MERGEINFO. Store the result in *FILTERED_RANGELIST.
+
+ This function only filters natural history for mergeinfo that will be
+ *added* during a forward merge. Removing natural history from explicit
+ mergeinfo is harmless. If REQUESTED_RANGE describes a reverse merge,
+ then *FILTERED_RANGELIST is simply populated with one range described
+ by REQUESTED_RANGE. *FILTERED_RANGELIST is never NULL.
+
+ Allocate *FILTERED_RANGELIST in POOL. */
+static svn_error_t *
+filter_natural_history_from_mergeinfo(svn_rangelist_t **filtered_rangelist,
+ const char *source_rel_path,
+ svn_mergeinfo_t implicit_mergeinfo,
+ svn_merge_range_t *requested_range,
+ apr_pool_t *pool)
+{
+ /* Make the REQUESTED_RANGE into a rangelist. */
+ svn_rangelist_t *requested_rangelist =
+ svn_rangelist__initialize(requested_range->start, requested_range->end,
+ requested_range->inheritable, pool);
+
+ *filtered_rangelist = NULL;
+
+ /* For forward merges: If the IMPLICIT_MERGEINFO already describes ranges
+ associated with SOURCE_REL_PATH then filter those ranges out. */
+ if (implicit_mergeinfo
+ && (requested_range->start < requested_range->end))
+ {
+ svn_rangelist_t *implied_rangelist =
+ svn_hash_gets(implicit_mergeinfo, source_rel_path);
+
+ if (implied_rangelist)
+ SVN_ERR(svn_rangelist_remove(filtered_rangelist,
+ implied_rangelist,
+ requested_rangelist,
+ FALSE, pool));
+ }
+
+ /* If no filtering was performed the filtered rangelist is
+ simply the requested rangelist.*/
+ if (! (*filtered_rangelist))
+ *filtered_rangelist = requested_rangelist;
+
+ return SVN_NO_ERROR;
+}
+
+/* Return a merge source representing the sub-range from START_REV to
+ END_REV of SOURCE. SOURCE obeys the rules described in the
+ 'MERGEINFO MERGE SOURCE NORMALIZATION' comment at the top of this file.
+ The younger of START_REV and END_REV is inclusive while the older is
+ exclusive.
+
+ Allocate the result structure in POOL but leave the URLs in it as shallow
+ copies of the URLs in SOURCE.
+*/
+static merge_source_t *
+subrange_source(const merge_source_t *source,
+ svn_revnum_t start_rev,
+ svn_revnum_t end_rev,
+ apr_pool_t *pool)
+{
+ svn_boolean_t is_rollback = (source->loc1->rev > source->loc2->rev);
+ svn_boolean_t same_urls = (strcmp(source->loc1->url, source->loc2->url) == 0);
+ svn_client__pathrev_t loc1 = *source->loc1;
+ svn_client__pathrev_t loc2 = *source->loc2;
+
+ /* For this function we require that the input source is 'ancestral'. */
+ SVN_ERR_ASSERT_NO_RETURN(source->ancestral);
+ SVN_ERR_ASSERT_NO_RETURN(start_rev != end_rev);
+
+ loc1.rev = start_rev;
+ loc2.rev = end_rev;
+ if (! same_urls)
+ {
+ if (is_rollback && (end_rev != source->loc2->rev))
+ {
+ loc2.url = source->loc1->url;
+ }
+ if ((! is_rollback) && (start_rev != source->loc1->rev))
+ {
+ loc1.url = source->loc2->url;
+ }
+ }
+ return merge_source_create(&loc1, &loc2, source->ancestral, pool);
+}
+
+/* The single-file, simplified version of do_directory_merge(), which see for
+ parameter descriptions.
+
+ Additional parameters:
+
+ If SOURCES_RELATED is set, the "left" and "right" sides of SOURCE are
+ historically related (ancestors, uncles, second
+ cousins thrice removed, etc...). (This is used to simulate the
+ history checks that the repository logic does in the directory case.)
+
+ If mergeinfo is being recorded to describe this merge, and RESULT_CATALOG
+ is not NULL, then don't record the new mergeinfo on the TARGET_ABSPATH,
+ but instead record it in RESULT_CATALOG, where the key is TARGET_ABSPATH
+ and the value is the new mergeinfo for that path. Allocate additions
+ to RESULT_CATALOG in pool which RESULT_CATALOG was created in.
+
+ CONFLICTED_RANGE is as documented for do_directory_merge().
+
+ Note: MERGE_B->RA_SESSION1 must be associated with SOURCE->loc1->url and
+ MERGE_B->RA_SESSION2 with SOURCE->loc2->url.
+*/
+static svn_error_t *
+do_file_merge(svn_mergeinfo_catalog_t result_catalog,
+ single_range_conflict_report_t **conflict_report,
+ const merge_source_t *source,
+ const char *target_abspath,
+ const svn_diff_tree_processor_t *processor,
+ svn_boolean_t sources_related,
+ svn_boolean_t squelch_mergeinfo_notifications,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_rangelist_t *remaining_ranges;
+ svn_client_ctx_t *ctx = merge_b->ctx;
+ svn_merge_range_t range;
+ svn_mergeinfo_t target_mergeinfo;
+ svn_boolean_t inherited = FALSE;
+ svn_boolean_t is_rollback = (source->loc1->rev > source->loc2->rev);
+ const svn_client__pathrev_t *primary_src
+ = is_rollback ? source->loc1 : source->loc2;
+ svn_boolean_t honor_mergeinfo = HONOR_MERGEINFO(merge_b);
+ svn_client__merge_path_t *merge_target = NULL;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(target_abspath));
+
+ *conflict_report = NULL;
+
+ /* Note that this is a single-file merge. */
+ range.start = source->loc1->rev;
+ range.end = source->loc2->rev;
+ range.inheritable = TRUE;
+
+ merge_target = svn_client__merge_path_create(target_abspath, scratch_pool);
+
+ if (honor_mergeinfo)
+ {
+ svn_error_t *err;
+
+ /* Fetch mergeinfo. */
+ err = get_full_mergeinfo(&target_mergeinfo,
+ &(merge_target->implicit_mergeinfo),
+ &inherited, svn_mergeinfo_inherited,
+ merge_b->ra_session1, target_abspath,
+ MAX(source->loc1->rev, source->loc2->rev),
+ MIN(source->loc1->rev, source->loc2->rev),
+ ctx, scratch_pool, iterpool);
+
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
+ {
+ err = svn_error_createf(
+ SVN_ERR_CLIENT_INVALID_MERGEINFO_NO_MERGETRACKING, err,
+ _("Invalid mergeinfo detected on merge target '%s', "
+ "merge tracking not possible"),
+ svn_dirent_local_style(target_abspath, scratch_pool));
+ }
+ return svn_error_trace(err);
+ }
+
+ /* Calculate remaining merges unless this is a record only merge.
+ In that case the remaining range is the whole range described
+ by SOURCE->rev1:rev2. */
+ if (!merge_b->record_only)
+ {
+ /* ### Bug? calculate_remaining_ranges() needs 'source' to adhere
+ * to the requirements of 'MERGEINFO MERGE SOURCE NORMALIZATION'
+ * here, but it doesn't appear to be guaranteed so. */
+ SVN_ERR(calculate_remaining_ranges(NULL, merge_target,
+ source,
+ target_mergeinfo,
+ merge_b->implicit_src_gap, FALSE,
+ merge_b->ra_session1,
+ ctx, scratch_pool,
+ iterpool));
+ remaining_ranges = merge_target->remaining_ranges;
+
+ /* We are honoring mergeinfo and this is not a simple record only
+ merge which blindly records mergeinfo describing the merge of
+ SOURCE->LOC1->URL@SOURCE->LOC1->REV through
+ SOURCE->LOC2->URL@SOURCE->LOC2->REV. This means that the oldest
+ and youngest revisions merged (as determined above by
+ calculate_remaining_ranges) might differ from those described
+ in SOURCE. To keep the '--- Merging *' notifications consistent
+ with the '--- Recording mergeinfo *' notifications, we adjust
+ RANGE to account for such changes. */
+ if (remaining_ranges->nelts)
+ {
+ svn_merge_range_t *adj_start_range =
+ APR_ARRAY_IDX(remaining_ranges, 0, svn_merge_range_t *);
+ svn_merge_range_t *adj_end_range =
+ APR_ARRAY_IDX(remaining_ranges, remaining_ranges->nelts - 1,
+ svn_merge_range_t *);
+ range.start = adj_start_range->start;
+ range.end = adj_end_range->end;
+ }
+ }
+ }
+
+ /* The simple cases where our remaining range is SOURCE->rev1:rev2. */
+ if (!honor_mergeinfo || merge_b->record_only)
+ {
+ remaining_ranges = apr_array_make(scratch_pool, 1, sizeof(&range));
+ APR_ARRAY_PUSH(remaining_ranges, svn_merge_range_t *) = &range;
+ }
+
+ if (!merge_b->record_only)
+ {
+ svn_rangelist_t *ranges_to_merge = apr_array_copy(scratch_pool,
+ remaining_ranges);
+ const char *target_relpath = ""; /* relative to root of merge */
+
+ if (source->ancestral)
+ {
+ apr_array_header_t *child_with_mergeinfo;
+ svn_client__merge_path_t *target_info;
+
+ /* If we have ancestrally related sources and more than one
+ range to merge, eliminate no-op ranges before going through
+ the effort of downloading the many copies of the file
+ required to do these merges (two copies per range). */
+ if (remaining_ranges->nelts > 1)
+ {
+ const char *old_sess_url;
+ svn_error_t *err;
+
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_sess_url,
+ merge_b->ra_session1,
+ primary_src->url,
+ iterpool));
+ err = remove_noop_merge_ranges(&ranges_to_merge,
+ merge_b->ra_session1,
+ remaining_ranges, scratch_pool);
+ SVN_ERR(svn_error_compose_create(
+ err, svn_ra_reparent(merge_b->ra_session1,
+ old_sess_url, iterpool)));
+ }
+
+ /* To support notify_merge_begin() initialize our
+ CHILD_WITH_MERGEINFO. See the comment
+ 'THE CHILDREN_WITH_MERGEINFO ARRAY' at the start of this file. */
+
+ child_with_mergeinfo = apr_array_make(scratch_pool, 1,
+ sizeof(svn_client__merge_path_t *));
+
+ /* ### Create a fake copy of merge_target as we don't keep
+ remaining_ranges in sync (yet). */
+ target_info = apr_pcalloc(scratch_pool, sizeof(*target_info));
+
+ target_info->abspath = merge_target->abspath;
+ target_info->remaining_ranges = ranges_to_merge;
+
+ APR_ARRAY_PUSH(child_with_mergeinfo, svn_client__merge_path_t *)
+ = target_info;
+
+ /* And store in baton to allow using it from notify_merge_begin() */
+ merge_b->notify_begin.nodes_with_mergeinfo = child_with_mergeinfo;
+ }
+
+ while (ranges_to_merge->nelts > 0)
+ {
+ svn_merge_range_t *r = APR_ARRAY_IDX(ranges_to_merge, 0,
+ svn_merge_range_t *);
+ const merge_source_t *real_source;
+ const char *left_file, *right_file;
+ apr_hash_t *left_props, *right_props;
+ const svn_diff_source_t *left_source;
+ const svn_diff_source_t *right_source;
+
+ svn_pool_clear(iterpool);
+
+ /* Ensure any subsequent drives gets their own notification. */
+ merge_b->notify_begin.last_abspath = NULL;
+
+ /* While we currently don't allow it, in theory we could be
+ fetching two fulltexts from two different repositories here. */
+ if (source->ancestral)
+ real_source = subrange_source(source, r->start, r->end, iterpool);
+ else
+ real_source = source;
+ SVN_ERR(single_file_merge_get_file(&left_file, &left_props,
+ merge_b->ra_session1,
+ real_source->loc1,
+ target_abspath,
+ iterpool, iterpool));
+ SVN_ERR(single_file_merge_get_file(&right_file, &right_props,
+ merge_b->ra_session2,
+ real_source->loc2,
+ target_abspath,
+ iterpool, iterpool));
+ /* Calculate sources for the diff processor */
+ left_source = svn_diff__source_create(r->start, iterpool);
+ right_source = svn_diff__source_create(r->end, iterpool);
+
+
+ /* If the sources are related or we're ignoring ancestry in diffs,
+ do a text-n-props merge; otherwise, do a delete-n-add merge. */
+ if (! (merge_b->diff_ignore_ancestry || sources_related))
+ {
+ struct merge_dir_baton_t dir_baton;
+ void *file_baton;
+ svn_boolean_t skip;
+
+ /* Initialize minimal dir baton to allow calculating 'R'eplace
+ from 'D'elete + 'A'dd. */
+
+ memset(&dir_baton, 0, sizeof(dir_baton));
+ dir_baton.pool = iterpool;
+ dir_baton.tree_conflict_reason = CONFLICT_REASON_NONE;
+ dir_baton.tree_conflict_action = svn_wc_conflict_action_edit;
+ dir_baton.skip_reason = svn_wc_notify_state_unknown;
+
+ /* Delete... */
+ file_baton = NULL;
+ skip = FALSE;
+ SVN_ERR(processor->file_opened(&file_baton, &skip, target_relpath,
+ left_source,
+ NULL /* right_source */,
+ NULL /* copyfrom_source */,
+ &dir_baton,
+ processor,
+ iterpool, iterpool));
+ if (! skip)
+ SVN_ERR(processor->file_deleted(target_relpath,
+ left_source,
+ left_file,
+ left_props,
+ file_baton,
+ processor,
+ iterpool));
+
+ /* ...plus add... */
+ file_baton = NULL;
+ skip = FALSE;
+ SVN_ERR(processor->file_opened(&file_baton, &skip, target_relpath,
+ NULL /* left_source */,
+ right_source,
+ NULL /* copyfrom_source */,
+ &dir_baton,
+ processor,
+ iterpool, iterpool));
+ if (! skip)
+ SVN_ERR(processor->file_added(target_relpath,
+ NULL /* copyfrom_source */,
+ right_source,
+ NULL /* copyfrom_file */,
+ right_file,
+ NULL /* copyfrom_props */,
+ right_props,
+ file_baton,
+ processor,
+ iterpool));
+ /* ... equals replace. */
+ }
+ else
+ {
+ void *file_baton = NULL;
+ svn_boolean_t skip = FALSE;
+ apr_array_header_t *propchanges;
+
+
+ /* Deduce property diffs. */
+ SVN_ERR(svn_prop_diffs(&propchanges, right_props, left_props,
+ iterpool));
+
+ SVN_ERR(processor->file_opened(&file_baton, &skip, target_relpath,
+ left_source,
+ right_source,
+ NULL /* copyfrom_source */,
+ NULL /* dir_baton */,
+ processor,
+ iterpool, iterpool));
+ if (! skip)
+ SVN_ERR(processor->file_changed(target_relpath,
+ left_source,
+ right_source,
+ left_file,
+ right_file,
+ left_props,
+ right_props,
+ TRUE /* file changed */,
+ propchanges,
+ file_baton,
+ processor,
+ iterpool));
+ }
+
+ if (is_path_conflicted_by_merge(merge_b))
+ {
+ merge_source_t *remaining_range = NULL;
+
+ if (real_source->loc2->rev != source->loc2->rev)
+ remaining_range = subrange_source(source,
+ real_source->loc2->rev,
+ source->loc2->rev,
+ scratch_pool);
+ *conflict_report = single_range_conflict_report_create(
+ real_source, remaining_range, result_pool);
+
+ /* Only record partial mergeinfo if only a partial merge was
+ performed before a conflict was encountered. */
+ range.end = r->end;
+ break;
+ }
+
+ /* Now delete the just merged range from the hash
+ (This list is used from notify_merge_begin)
+
+ Directory merges use remove_first_range_from_remaining_ranges() */
+ svn_sort__array_delete(ranges_to_merge, 0, 1);
+ }
+ merge_b->notify_begin.last_abspath = NULL;
+ } /* !merge_b->record_only */
+
+ /* Record updated WC mergeinfo to account for our new merges, minus
+ any unresolved conflicts and skips. We use the original
+ REMAINING_RANGES here because we want to record all the requested
+ merge ranges, include the noop ones. */
+ if (RECORD_MERGEINFO(merge_b) && remaining_ranges->nelts)
+ {
+ const char *mergeinfo_path = svn_client__pathrev_fspath(primary_src,
+ scratch_pool);
+ svn_rangelist_t *filtered_rangelist;
+
+ /* Filter any ranges from TARGET_WCPATH's own history, there is no
+ need to record this explicitly in mergeinfo, it is already part
+ of TARGET_WCPATH's natural history (implicit mergeinfo). */
+ SVN_ERR(filter_natural_history_from_mergeinfo(
+ &filtered_rangelist,
+ mergeinfo_path,
+ merge_target->implicit_mergeinfo,
+ &range,
+ iterpool));
+
+ /* Only record mergeinfo if there is something other than
+ self-referential mergeinfo, but don't record mergeinfo if
+ TARGET_WCPATH was skipped. */
+ if (filtered_rangelist->nelts
+ && (apr_hash_count(merge_b->skipped_abspaths) == 0))
+ {
+ apr_hash_t *merges = apr_hash_make(iterpool);
+
+ /* If merge target has inherited mergeinfo set it before
+ recording the first merge range. */
+ if (inherited)
+ SVN_ERR(svn_client__record_wc_mergeinfo(target_abspath,
+ target_mergeinfo,
+ FALSE, ctx,
+ iterpool));
+
+ svn_hash_sets(merges, target_abspath, filtered_rangelist);
+
+ if (!squelch_mergeinfo_notifications)
+ {
+ /* Notify that we are recording mergeinfo describing a merge. */
+ svn_merge_range_t n_range;
+
+ SVN_ERR(svn_mergeinfo__get_range_endpoints(
+ &n_range.end, &n_range.start, merges, iterpool));
+ n_range.inheritable = TRUE;
+ notify_mergeinfo_recording(target_abspath, &n_range,
+ merge_b->ctx, iterpool);
+ }
+
+ SVN_ERR(update_wc_mergeinfo(result_catalog, target_abspath,
+ mergeinfo_path, merges, is_rollback,
+ ctx, iterpool));
+ }
+ }
+
+ merge_b->notify_begin.nodes_with_mergeinfo = NULL;
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for do_directory_merge() to handle the case where a merge editor
+ drive adds explicit mergeinfo to a path which didn't have any explicit
+ mergeinfo previously.
+
+ MERGE_B is cascaded from the argument of the same
+ name in do_directory_merge(). Should be called only after
+ do_directory_merge() has called populate_remaining_ranges() and populated
+ the remaining_ranges field of each child in
+ CHILDREN_WITH_MERGEINFO (i.e. the remaining_ranges fields can be
+ empty but never NULL).
+
+ If MERGE_B->DRY_RUN is true do nothing, if it is false then
+ for each path (if any) in MERGE_B->PATHS_WITH_NEW_MERGEINFO merge that
+ path's inherited mergeinfo (if any) with its working explicit mergeinfo
+ and set that as the path's new explicit mergeinfo. Then add an
+ svn_client__merge_path_t * element representing the path to
+ CHILDREN_WITH_MERGEINFO if it isn't already present. All fields
+ in any elements added to CHILDREN_WITH_MERGEINFO are initialized
+ to FALSE/NULL with the exception of 'path' and 'remaining_ranges'. The
+ latter is set to a rangelist equal to the remaining_ranges of the path's
+ nearest path-wise ancestor in CHILDREN_WITH_MERGEINFO.
+
+ Any elements added to CHILDREN_WITH_MERGEINFO are allocated
+ in POOL. */
+static svn_error_t *
+process_children_with_new_mergeinfo(merge_cmd_baton_t *merge_b,
+ apr_array_header_t *children_with_mergeinfo,
+ apr_pool_t *pool)
+{
+ apr_pool_t *iterpool;
+ apr_hash_index_t *hi;
+
+ if (!merge_b->paths_with_new_mergeinfo || merge_b->dry_run)
+ return SVN_NO_ERROR;
+
+ /* Iterate over each path with explicit mergeinfo added by the merge. */
+ iterpool = svn_pool_create(pool);
+ for (hi = apr_hash_first(pool, merge_b->paths_with_new_mergeinfo);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *abspath_with_new_mergeinfo = svn__apr_hash_index_key(hi);
+ svn_mergeinfo_t path_inherited_mergeinfo;
+ svn_mergeinfo_t path_explicit_mergeinfo;
+ svn_client__merge_path_t *new_child;
+
+ svn_pool_clear(iterpool);
+
+ /* Note: We could skip recording inherited mergeinfo here if this path
+ was added (with preexisting mergeinfo) by the merge. That's actually
+ more correct, since the inherited mergeinfo likely describes
+ non-existent or unrelated merge history, but it's not quite so simple
+ as that, see http://subversion.tigris.org/issues/show_bug.cgi?id=4309
+ */
+
+ /* Get the path's new explicit mergeinfo... */
+ SVN_ERR(svn_client__get_wc_mergeinfo(&path_explicit_mergeinfo, NULL,
+ svn_mergeinfo_explicit,
+ abspath_with_new_mergeinfo,
+ NULL, NULL, FALSE,
+ merge_b->ctx,
+ iterpool, iterpool));
+ /* ...there *should* always be explicit mergeinfo at this point
+ but you can't be too careful. */
+ if (path_explicit_mergeinfo)
+ {
+ /* Get the mergeinfo the path would have inherited before
+ the merge. */
+ SVN_ERR(svn_client__get_wc_or_repos_mergeinfo(
+ &path_inherited_mergeinfo,
+ NULL, NULL,
+ FALSE,
+ svn_mergeinfo_nearest_ancestor, /* We only want inherited MI */
+ merge_b->ra_session2,
+ abspath_with_new_mergeinfo,
+ merge_b->ctx,
+ iterpool));
+
+ /* If the path inherited any mergeinfo then merge that with the
+ explicit mergeinfo and record the result as the path's new
+ explicit mergeinfo. */
+ if (path_inherited_mergeinfo)
+ {
+ SVN_ERR(svn_mergeinfo_merge2(path_explicit_mergeinfo,
+ path_inherited_mergeinfo,
+ iterpool, iterpool));
+ SVN_ERR(svn_client__record_wc_mergeinfo(
+ abspath_with_new_mergeinfo,
+ path_explicit_mergeinfo,
+ FALSE, merge_b->ctx, iterpool));
+ }
+
+ /* If the path is not in CHILDREN_WITH_MERGEINFO then add it. */
+ new_child =
+ get_child_with_mergeinfo(children_with_mergeinfo,
+ abspath_with_new_mergeinfo);
+ if (!new_child)
+ {
+ const svn_client__merge_path_t *parent
+ = find_nearest_ancestor(children_with_mergeinfo,
+ FALSE, abspath_with_new_mergeinfo);
+ new_child
+ = svn_client__merge_path_create(abspath_with_new_mergeinfo,
+ pool);
+
+ /* If path_with_new_mergeinfo is the merge target itself
+ then it should already be in
+ CHILDREN_WITH_MERGEINFO per the criteria of
+ get_mergeinfo_paths() and we shouldn't be in this block.
+ If path_with_new_mergeinfo is a subtree then it must have
+ a parent in CHILDREN_WITH_MERGEINFO if only
+ the merge target itself...so if we don't find a parent
+ the caller has done something quite wrong. */
+ SVN_ERR_ASSERT(parent);
+ SVN_ERR_ASSERT(parent->remaining_ranges);
+
+ /* Set the path's remaining_ranges equal to its parent's. */
+ new_child->remaining_ranges = svn_rangelist_dup(
+ parent->remaining_ranges, pool);
+ insert_child_to_merge(children_with_mergeinfo, new_child, pool);
+ }
+ }
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Return true if any path in SUBTREES is equal to, or is a subtree of,
+ LOCAL_ABSPATH. Return false otherwise. The keys of SUBTREES are
+ (const char *) absolute paths and its values are irrelevant.
+ If SUBTREES is NULL return false. */
+static svn_boolean_t
+path_is_subtree(const char *local_abspath,
+ apr_hash_t *subtrees,
+ apr_pool_t *pool)
+{
+ if (subtrees)
+ {
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(pool, subtrees);
+ hi; hi = apr_hash_next(hi))
+ {
+ const char *path_touched_by_merge = svn__apr_hash_index_key(hi);
+ if (svn_dirent_is_ancestor(local_abspath, path_touched_by_merge))
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+/* Return true if any merged, skipped, added or tree-conflicted path
+ recorded in MERGE_B is equal to, or is a subtree of LOCAL_ABSPATH. Return
+ false otherwise.
+
+ ### Why not text- or prop-conflicted paths? Are such paths guaranteed
+ to be recorded as 'merged' or 'skipped' or 'added', perhaps?
+*/
+static svn_boolean_t
+subtree_touched_by_merge(const char *local_abspath,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *pool)
+{
+ return (path_is_subtree(local_abspath, merge_b->merged_abspaths, pool)
+ || path_is_subtree(local_abspath, merge_b->skipped_abspaths, pool)
+ || path_is_subtree(local_abspath, merge_b->added_abspaths, pool)
+ || path_is_subtree(local_abspath, merge_b->tree_conflicted_abspaths,
+ pool));
+}
+
+/* Helper for do_directory_merge() when performing mergeinfo unaware merges.
+
+ Merge the SOURCE diff into TARGET_DIR_WCPATH.
+
+ SOURCE, DEPTH, NOTIFY_B, and MERGE_B
+ are all cascaded from do_directory_merge's arguments of the same names.
+
+ CONFLICT_REPORT is as documented for do_directory_merge().
+
+ NOTE: This is a very thin wrapper around drive_merge_report_editor() and
+ exists only to populate CHILDREN_WITH_MERGEINFO with the single element
+ expected during mergeinfo unaware merges.
+*/
+static svn_error_t *
+do_mergeinfo_unaware_dir_merge(single_range_conflict_report_t **conflict_report,
+ const merge_source_t *source,
+ const char *target_dir_wcpath,
+ apr_array_header_t *children_with_mergeinfo,
+ const svn_diff_tree_processor_t *processor,
+ svn_depth_t depth,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ /* Initialize CHILDREN_WITH_MERGEINFO and populate it with
+ one element describing the merge of SOURCE->rev1:rev2 to
+ TARGET_DIR_WCPATH. */
+ svn_client__merge_path_t *item
+ = svn_client__merge_path_create(target_dir_wcpath, scratch_pool);
+
+ *conflict_report = NULL;
+ item->remaining_ranges = svn_rangelist__initialize(source->loc1->rev,
+ source->loc2->rev,
+ TRUE, scratch_pool);
+ APR_ARRAY_PUSH(children_with_mergeinfo,
+ svn_client__merge_path_t *) = item;
+ SVN_ERR(drive_merge_report_editor(target_dir_wcpath,
+ source,
+ NULL, processor, depth,
+ merge_b, scratch_pool));
+ if (is_path_conflicted_by_merge(merge_b))
+ {
+ *conflict_report = single_range_conflict_report_create(
+ source, NULL, result_pool);
+ }
+ return SVN_NO_ERROR;
+}
+
+/* A svn_log_entry_receiver_t baton for log_find_operative_subtree_revs(). */
+typedef struct log_find_operative_subtree_baton_t
+{
+ /* Mapping of const char * absolute working copy paths to those
+ path's const char * repos absolute paths. */
+ apr_hash_t *operative_children;
+
+ /* As per the arguments of the same name to
+ get_operative_immediate_children(). */
+ const char *merge_source_fspath;
+ const char *merge_target_abspath;
+ svn_depth_t depth;
+ svn_wc_context_t *wc_ctx;
+
+ /* A pool to allocate additions to the hashes in. */
+ apr_pool_t *result_pool;
+} log_find_operative_subtree_baton_t;
+
+/* A svn_log_entry_receiver_t callback for
+ get_inoperative_immediate_children(). */
+static svn_error_t *
+log_find_operative_subtree_revs(void *baton,
+ svn_log_entry_t *log_entry,
+ apr_pool_t *pool)
+{
+ log_find_operative_subtree_baton_t *log_baton = baton;
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool;
+
+ /* It's possible that authz restrictions on the merge source prevent us
+ from knowing about any of the changes for LOG_ENTRY->REVISION. */
+ if (!log_entry->changed_paths2)
+ return SVN_NO_ERROR;
+
+ iterpool = svn_pool_create(pool);
+
+ for (hi = apr_hash_first(pool, log_entry->changed_paths2);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *path = svn__apr_hash_index_key(hi);
+ svn_log_changed_path2_t *change = svn__apr_hash_index_val(hi);
+
+ {
+ const char *child;
+ const char *potential_child;
+ const char *rel_path =
+ svn_fspath__skip_ancestor(log_baton->merge_source_fspath, path);
+
+ /* Some affected paths might be the root of the merge source or
+ entirely outside our subtree of interest. In either case they
+ are not operative *immediate* children. */
+ if (rel_path == NULL
+ || rel_path[0] == '\0')
+ continue;
+
+ svn_pool_clear(iterpool);
+
+ child = svn_relpath_dirname(rel_path, iterpool);
+ if (child[0] == '\0')
+ {
+ /* The svn_log_changed_path2_t.node_kind members in
+ LOG_ENTRY->CHANGED_PATHS2 may be set to
+ svn_node_unknown, see svn_log_changed_path2_t and
+ svn_fs_paths_changed2. In that case we check the
+ type of the corresponding subtree in the merge
+ target. */
+ svn_node_kind_t node_kind;
+
+ if (change->node_kind == svn_node_unknown)
+ {
+ const char *wc_child_abspath =
+ svn_dirent_join(log_baton->merge_target_abspath,
+ rel_path, iterpool);
+
+ SVN_ERR(svn_wc_read_kind2(&node_kind, log_baton->wc_ctx,
+ wc_child_abspath, FALSE, FALSE,
+ iterpool));
+ }
+ else
+ {
+ node_kind = change->node_kind;
+ }
+
+ /* We only care about immediate directory children if
+ DEPTH is svn_depth_files. */
+ if (log_baton->depth == svn_depth_files
+ && node_kind != svn_node_dir)
+ continue;
+
+ /* If depth is svn_depth_immediates, then we only care
+ about changes to proper subtrees of PATH. If the change
+ is to PATH itself then PATH is within the operational
+ depth of the merge. */
+ if (log_baton->depth == svn_depth_immediates)
+ continue;
+
+ child = rel_path;
+ }
+
+ potential_child = svn_dirent_join(log_baton->merge_target_abspath,
+ child, iterpool);
+
+ if (change->action == 'A'
+ || !svn_hash_gets(log_baton->operative_children,
+ potential_child))
+ {
+ svn_hash_sets(log_baton->operative_children,
+ apr_pstrdup(log_baton->result_pool,
+ potential_child),
+ apr_pstrdup(log_baton->result_pool, path));
+ }
+ }
+ }
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Find immediate subtrees of MERGE_TARGET_ABSPATH which would have
+ additional differences applied if record_mergeinfo_for_dir_merge() were
+ recording mergeinfo describing a merge at svn_depth_infinity, rather
+ than at DEPTH (which is assumed to be shallow; if
+ DEPTH == svn_depth_infinity then this function does nothing beyond
+ setting *OPERATIVE_CHILDREN to an empty hash).
+
+ MERGE_SOURCE_FSPATH is the absolute repository path of the merge
+ source. OLDEST_REV and YOUNGEST_REV are the revisions merged from
+ MERGE_SOURCE_FSPATH to MERGE_TARGET_ABSPATH.
+
+ RA_SESSION points to MERGE_SOURCE_FSPATH.
+
+ Set *OPERATIVE_CHILDREN to a hash (mapping const char * absolute
+ working copy paths to those path's const char * repos absolute paths)
+ containing all the immediate subtrees of MERGE_TARGET_ABSPATH which would
+ have a different diff applied if MERGE_SOURCE_FSPATH
+ -r(OLDEST_REV - 1):YOUNGEST_REV were merged to MERGE_TARGET_ABSPATH at
+ svn_depth_infinity rather than DEPTH.
+
+ RESULT_POOL is used to allocate the contents of *OPERATIVE_CHILDREN.
+ SCRATCH_POOL is used for temporary allocations. */
+static svn_error_t *
+get_operative_immediate_children(apr_hash_t **operative_children,
+ const char *merge_source_fspath,
+ svn_revnum_t oldest_rev,
+ svn_revnum_t youngest_rev,
+ const char *merge_target_abspath,
+ svn_depth_t depth,
+ svn_wc_context_t *wc_ctx,
+ svn_ra_session_t *ra_session,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ log_find_operative_subtree_baton_t log_baton;
+
+ SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(oldest_rev));
+ SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(youngest_rev));
+ SVN_ERR_ASSERT(oldest_rev <= youngest_rev);
+
+ *operative_children = apr_hash_make(result_pool);
+
+ if (depth == svn_depth_infinity)
+ return SVN_NO_ERROR;
+
+ /* Now remove any paths from *OPERATIVE_CHILDREN that are inoperative when
+ merging MERGE_SOURCE_REPOS_PATH -r(OLDEST_REV - 1):YOUNGEST_REV to
+ MERGE_TARGET_ABSPATH at --depth infinity. */
+ log_baton.operative_children = *operative_children;
+ log_baton.merge_source_fspath = merge_source_fspath;
+ log_baton.merge_target_abspath = merge_target_abspath;
+ log_baton.depth = depth;
+ log_baton.wc_ctx = wc_ctx;
+ log_baton.result_pool = result_pool;
+
+ SVN_ERR(get_log(ra_session, "", youngest_rev, oldest_rev,
+ TRUE, /* discover_changed_paths */
+ log_find_operative_subtree_revs,
+ &log_baton, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for record_mergeinfo_for_dir_merge(): Identify which elements of
+ CHILDREN_WITH_MERGEINFO need new mergeinfo set to accurately
+ describe a merge, what inheritance type such new mergeinfo should have,
+ and what subtrees can be ignored altogether.
+
+ For each svn_client__merge_path_t CHILD in CHILDREN_WITH_MERGEINFO,
+ set CHILD->RECORD_MERGEINFO and CHILD->RECORD_NONINHERITABLE to true
+ if the subtree needs mergeinfo to describe the merge and if that
+ mergeinfo should be non-inheritable respectively.
+
+ If OPERATIVE_MERGE is true, then the merge being described is operative
+ as per subtree_touched_by_merge(). OPERATIVE_MERGE is false otherwise.
+
+ MERGED_RANGE, MERGEINFO_FSPATH, DEPTH, NOTIFY_B, and MERGE_B are all
+ cascaded from record_mergeinfo_for_dir_merge's arguments of the same
+ names.
+
+ SCRATCH_POOL is used for temporary allocations.
+*/
+static svn_error_t *
+flag_subtrees_needing_mergeinfo(svn_boolean_t operative_merge,
+ const svn_merge_range_t *merged_range,
+ apr_array_header_t *children_with_mergeinfo,
+ const char *mergeinfo_fspath,
+ svn_depth_t depth,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ int i;
+ apr_hash_t *operative_immediate_children = NULL;
+
+ assert(! merge_b->dry_run);
+
+ if (!merge_b->record_only
+ && merged_range->start <= merged_range->end
+ && (depth < svn_depth_infinity))
+ SVN_ERR(get_operative_immediate_children(
+ &operative_immediate_children,
+ mergeinfo_fspath, merged_range->start + 1, merged_range->end,
+ merge_b->target->abspath, depth, merge_b->ctx->wc_ctx,
+ merge_b->ra_session1, scratch_pool, iterpool));
+
+ /* Issue #4056: Walk NOTIFY_B->CHILDREN_WITH_MERGEINFO reverse depth-first
+ order. This way each child knows if it has operative missing/switched
+ children which necessitates non-inheritable mergeinfo. */
+ for (i = children_with_mergeinfo->nelts - 1; i >= 0; i--)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i,
+ svn_client__merge_path_t *);
+
+ /* Can't record mergeinfo on something that isn't here. */
+ if (child->absent)
+ continue;
+
+ /* Verify that remove_children_with_deleted_mergeinfo() did its job */
+ assert((i == 0)
+ ||! merge_b->paths_with_deleted_mergeinfo
+ || !svn_hash_gets(merge_b->paths_with_deleted_mergeinfo,
+ child->abspath));
+
+ /* Don't record mergeinfo on skipped paths. */
+ if (svn_hash_gets(merge_b->skipped_abspaths, child->abspath))
+ continue;
+
+ /* ### ptb: Yes, we could combine the following into a single
+ ### conditional, but clarity would suffer (even more than
+ ### it does now). */
+ if (i == 0)
+ {
+ /* Always record mergeinfo on the merge target. */
+ child->record_mergeinfo = TRUE;
+ }
+ else if (merge_b->record_only && !merge_b->reintegrate_merge)
+ {
+ /* Always record mergeinfo for --record-only merges. */
+ child->record_mergeinfo = TRUE;
+ }
+ else if (child->immediate_child_dir
+ && !child->pre_merge_mergeinfo
+ && operative_immediate_children
+ && svn_hash_gets(operative_immediate_children, child->abspath))
+ {
+ /* We must record mergeinfo on those issue #3642 children
+ that are operative at a greater depth. */
+ child->record_mergeinfo = TRUE;
+ }
+
+ if (operative_merge
+ && subtree_touched_by_merge(child->abspath, merge_b, iterpool))
+ {
+ svn_pool_clear(iterpool);
+
+ /* This subtree was affected by the merge. */
+ child->record_mergeinfo = TRUE;
+
+ /* Were any CHILD's missing children skipped by the merge?
+ If not, then CHILD's missing children don't need to be
+ considered when recording mergeinfo describing the merge. */
+ if (! merge_b->reintegrate_merge
+ && child->missing_child
+ && !path_is_subtree(child->abspath,
+ merge_b->skipped_abspaths,
+ iterpool))
+ {
+ child->missing_child = FALSE;
+ }
+
+ /* If CHILD has an immediate switched child or children and
+ none of these were touched by the merge, then we don't need
+ need to do any special handling of those switched subtrees
+ (e.g. record non-inheritable mergeinfo) when recording
+ mergeinfo describing the merge. */
+ if (child->switched_child)
+ {
+ int j;
+ svn_boolean_t operative_switched_child = FALSE;
+
+ for (j = i + 1;
+ j < children_with_mergeinfo->nelts;
+ j++)
+ {
+ svn_client__merge_path_t *potential_child =
+ APR_ARRAY_IDX(children_with_mergeinfo, j,
+ svn_client__merge_path_t *);
+ if (!svn_dirent_is_ancestor(child->abspath,
+ potential_child->abspath))
+ break;
+
+ /* POTENTIAL_CHILD is a subtree of CHILD, but is it
+ an immediate child? */
+ if (strcmp(child->abspath,
+ svn_dirent_dirname(potential_child->abspath,
+ iterpool)))
+ continue;
+
+ if (potential_child->switched
+ && potential_child->record_mergeinfo)
+ {
+ operative_switched_child = TRUE;
+ break;
+ }
+ }
+
+ /* Can we treat CHILD as if it has no switched children? */
+ if (! operative_switched_child)
+ child->switched_child = FALSE;
+ }
+ }
+
+ if (child->record_mergeinfo)
+ {
+ /* We need to record mergeinfo, but should that mergeinfo be
+ non-inheritable? */
+ svn_node_kind_t path_kind;
+ SVN_ERR(svn_wc_read_kind2(&path_kind, merge_b->ctx->wc_ctx,
+ child->abspath, FALSE, FALSE, iterpool));
+
+ /* Only directories can have non-inheritable mergeinfo. */
+ if (path_kind == svn_node_dir)
+ {
+ /* There are two general cases where non-inheritable mergeinfo
+ is required:
+
+ 1) There merge target has missing subtrees (due to authz
+ restrictions, switched subtrees, or a shallow working
+ copy).
+
+ 2) The operational depth of the merge itself is shallow. */
+
+ /* We've already determined the first case. */
+ child->record_noninheritable =
+ child->missing_child || child->switched_child;
+
+ /* The second case requires a bit more work. */
+ if (i == 0)
+ {
+ /* If CHILD is the root of the merge target and the
+ operational depth is empty or files, then the mere
+ existence of operative immediate children means we
+ must record non-inheritable mergeinfo.
+
+ ### What about svn_depth_immediates? In that case
+ ### the merge target needs only normal inheritable
+ ### mergeinfo and the target's immediate children will
+ ### get non-inheritable mergeinfo, assuming they
+ ### need even that. */
+ if (depth < svn_depth_immediates
+ && operative_immediate_children
+ && apr_hash_count(operative_immediate_children))
+ child->record_noninheritable = TRUE;
+ }
+ else if (depth == svn_depth_immediates)
+ {
+ /* An immediate directory child of the merge target, which
+ was affected by a --depth=immediates merge, needs
+ non-inheritable mergeinfo. */
+ if (svn_hash_gets(operative_immediate_children,
+ child->abspath))
+ child->record_noninheritable = TRUE;
+ }
+ }
+ }
+ else /* child->record_mergeinfo */
+ {
+ /* If CHILD is in NOTIFY_B->CHILDREN_WITH_MERGEINFO simply
+ because it had no explicit mergeinfo of its own at the
+ start of the merge but is the child of of some path with
+ non-inheritable mergeinfo, then the explicit mergeinfo it
+ has *now* was set by get_mergeinfo_paths() -- see criteria
+ 3 in that function's doc string. So since CHILD->ABSPATH
+ was not touched by the merge we can remove the
+ mergeinfo. */
+ if (child->child_of_noninheritable)
+ SVN_ERR(svn_client__record_wc_mergeinfo(child->abspath,
+ NULL, FALSE,
+ merge_b->ctx,
+ iterpool));
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Helper for do_directory_merge().
+
+ If RESULT_CATALOG is NULL then record mergeinfo describing a merge of
+ MERGED_RANGE->START:MERGED_RANGE->END from the repository relative path
+ MERGEINFO_FSPATH to the merge target (and possibly its subtrees) described
+ by NOTIFY_B->CHILDREN_WITH_MERGEINFO -- see the global comment
+ 'THE CHILDREN_WITH_MERGEINFO ARRAY'. Obviously this should only
+ be called if recording mergeinfo -- see doc string for RECORD_MERGEINFO().
+
+ If RESULT_CATALOG is not NULL, then don't record the new mergeinfo on the
+ WC, but instead record it in RESULT_CATALOG, where the keys are absolute
+ working copy paths and the values are the new mergeinfos for each.
+ Allocate additions to RESULT_CATALOG in pool which RESULT_CATALOG was
+ created in.
+
+ DEPTH, NOTIFY_B, MERGE_B, and SQUELCH_MERGEINFO_NOTIFICATIONS are all
+ cascaded from do_directory_merge's arguments of the same names.
+
+ SCRATCH_POOL is used for temporary allocations.
+*/
+static svn_error_t *
+record_mergeinfo_for_dir_merge(svn_mergeinfo_catalog_t result_catalog,
+ const svn_merge_range_t *merged_range,
+ const char *mergeinfo_fspath,
+ apr_array_header_t *children_with_mergeinfo,
+ svn_depth_t depth,
+ svn_boolean_t squelch_mergeinfo_notifications,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+ svn_boolean_t is_rollback = (merged_range->start > merged_range->end);
+ svn_boolean_t operative_merge;
+
+ /* Update the WC mergeinfo here to account for our new
+ merges, minus any unresolved conflicts and skips. */
+
+ /* We need a scratch pool for iterations below. */
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ svn_merge_range_t range = *merged_range;
+
+ assert(! merge_b->dry_run);
+
+ /* Regardless of what subtrees in MERGE_B->target->abspath might be missing
+ could this merge have been operative? */
+ operative_merge = subtree_touched_by_merge(merge_b->target->abspath,
+ merge_b, iterpool);
+
+ /* If this couldn't be an operative merge then don't bother with
+ the added complexity (and user confusion) of non-inheritable ranges.
+ There is no harm in subtrees inheriting inoperative mergeinfo. */
+ if (!operative_merge)
+ range.inheritable = TRUE;
+
+ /* Remove absent children at or under MERGE_B->target->abspath from
+ NOTIFY_B->CHILDREN_WITH_MERGEINFO
+ before we calculate the merges performed. */
+ remove_absent_children(merge_b->target->abspath,
+ children_with_mergeinfo);
+
+ /* Determine which subtrees of interest need mergeinfo recorded... */
+ SVN_ERR(flag_subtrees_needing_mergeinfo(operative_merge, &range,
+ children_with_mergeinfo,
+ mergeinfo_fspath, depth,
+ merge_b, iterpool));
+
+ /* ...and then record it. */
+ for (i = 0; i < children_with_mergeinfo->nelts; i++)
+ {
+ const char *child_repos_path;
+ const char *child_merge_src_fspath;
+ svn_rangelist_t *child_merge_rangelist;
+ apr_hash_t *child_merges;
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i,
+ svn_client__merge_path_t *);
+ SVN_ERR_ASSERT(child);
+
+ svn_pool_clear(iterpool);
+
+ if (child->record_mergeinfo)
+ {
+ child_repos_path = svn_dirent_skip_ancestor(merge_b->target->abspath,
+ child->abspath);
+ SVN_ERR_ASSERT(child_repos_path != NULL);
+ child_merge_src_fspath = svn_fspath__join(mergeinfo_fspath,
+ child_repos_path,
+ iterpool);
+ /* Filter any ranges from each child's natural history before
+ setting mergeinfo describing the merge. */
+ SVN_ERR(filter_natural_history_from_mergeinfo(
+ &child_merge_rangelist, child_merge_src_fspath,
+ child->implicit_mergeinfo, &range, iterpool));
+
+ if (child_merge_rangelist->nelts == 0)
+ continue;
+
+ if (!squelch_mergeinfo_notifications)
+ {
+ /* If the merge source has a gap, then don't mention
+ those gap revisions in the notification. */
+ remove_source_gap(&range, merge_b->implicit_src_gap);
+ notify_mergeinfo_recording(child->abspath, &range,
+ merge_b->ctx, iterpool);
+ }
+
+ /* If we are here we know we will be recording some mergeinfo, but
+ before we do, set override mergeinfo on skipped paths so they
+ don't incorrectly inherit the mergeinfo we are about to set. */
+ if (i == 0)
+ SVN_ERR(record_skips_in_mergeinfo(mergeinfo_fspath,
+ child_merge_rangelist,
+ is_rollback, merge_b, iterpool));
+
+ /* We may need to record non-inheritable mergeinfo that applies
+ only to CHILD->ABSPATH. */
+ if (child->record_noninheritable)
+ svn_rangelist__set_inheritance(child_merge_rangelist, FALSE);
+
+ /* If CHILD has inherited mergeinfo set it before
+ recording the first merge range. */
+ if (child->inherited_mergeinfo)
+ SVN_ERR(svn_client__record_wc_mergeinfo(
+ child->abspath,
+ child->pre_merge_mergeinfo,
+ FALSE, merge_b->ctx,
+ iterpool));
+ if (merge_b->implicit_src_gap)
+ {
+ /* If this is a reverse merge reorder CHILD->REMAINING_RANGES
+ so it will work with the svn_rangelist_remove API. */
+ if (is_rollback)
+ SVN_ERR(svn_rangelist_reverse(child_merge_rangelist,
+ iterpool));
+
+ SVN_ERR(svn_rangelist_remove(&child_merge_rangelist,
+ merge_b->implicit_src_gap,
+ child_merge_rangelist, FALSE,
+ iterpool));
+ if (is_rollback)
+ SVN_ERR(svn_rangelist_reverse(child_merge_rangelist,
+ iterpool));
+ }
+
+ child_merges = apr_hash_make(iterpool);
+
+ /* The short story:
+
+ If we are describing a forward merge, then the naive mergeinfo
+ defined by MERGE_SOURCE_PATH:MERGED_RANGE->START:
+ MERGE_SOURCE_PATH:MERGED_RANGE->END may contain non-existent
+ path-revs or may describe other lines of history. We must
+ remove these invalid portion(s) before recording mergeinfo
+ describing the merge.
+
+ The long story:
+
+ If CHILD is the merge target we know that
+ MERGE_SOURCE_PATH:MERGED_RANGE->END exists. Further, if there
+ were no copies in MERGE_SOURCE_PATH's history going back to
+ RANGE->START then we know that
+ MERGE_SOURCE_PATH:MERGED_RANGE->START exists too and the two
+ describe an unbroken line of history, and thus
+ MERGE_SOURCE_PATH:MERGED_RANGE->START:
+ MERGE_SOURCE_PATH:MERGED_RANGE->END is a valid description of
+ the merge -- see normalize_merge_sources() and the global comment
+ 'MERGEINFO MERGE SOURCE NORMALIZATION'.
+
+ However, if there *was* a copy, then
+ MERGE_SOURCE_PATH:MERGED_RANGE->START doesn't exist or is
+ unrelated to MERGE_SOURCE_PATH:MERGED_RANGE->END. Also, we
+ don't know if (MERGE_SOURCE_PATH:MERGED_RANGE->START)+1 through
+ (MERGE_SOURCE_PATH:MERGED_RANGE->END)-1 actually exist.
+
+ If CHILD is a subtree of the merge target, then nothing is
+ guaranteed beyond the fact that MERGE_SOURCE_PATH exists at
+ MERGED_RANGE->END. */
+ if ((!merge_b->record_only || merge_b->reintegrate_merge)
+ && (!is_rollback))
+ {
+ svn_error_t *err;
+ svn_mergeinfo_t subtree_history_as_mergeinfo;
+ svn_rangelist_t *child_merge_src_rangelist;
+ svn_client__pathrev_t *subtree_mergeinfo_pathrev
+ = svn_client__pathrev_create_with_relpath(
+ merge_b->target->loc.repos_root_url,
+ merge_b->target->loc.repos_uuid,
+ merged_range->end, child_merge_src_fspath + 1,
+ iterpool);
+
+ /* Confirm that the naive mergeinfo we want to set on
+ CHILD->ABSPATH both exists and is part of
+ (MERGE_SOURCE_PATH+CHILD_REPOS_PATH)@MERGED_RANGE->END's
+ history. */
+ /* We know MERGED_RANGE->END is younger than MERGE_RANGE->START
+ because we only do this for forward merges. */
+ err = svn_client__get_history_as_mergeinfo(
+ &subtree_history_as_mergeinfo, NULL,
+ subtree_mergeinfo_pathrev,
+ merged_range->end, merged_range->start,
+ merge_b->ra_session2, merge_b->ctx, iterpool);
+
+ /* If CHILD is a subtree it may have been deleted prior to
+ MERGED_RANGE->END so the above call to get its history
+ will fail. */
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_FS_NOT_FOUND)
+ return svn_error_trace(err);
+ svn_error_clear(err);
+ }
+ else
+ {
+ child_merge_src_rangelist = svn_hash_gets(
+ subtree_history_as_mergeinfo,
+ child_merge_src_fspath);
+ SVN_ERR(svn_rangelist_intersect(&child_merge_rangelist,
+ child_merge_rangelist,
+ child_merge_src_rangelist,
+ FALSE, iterpool));
+ if (child->record_noninheritable)
+ svn_rangelist__set_inheritance(child_merge_rangelist,
+ FALSE);
+ }
+ }
+
+ svn_hash_sets(child_merges, child->abspath, child_merge_rangelist);
+ SVN_ERR(update_wc_mergeinfo(result_catalog,
+ child->abspath,
+ child_merge_src_fspath,
+ child_merges, is_rollback,
+ merge_b->ctx, iterpool));
+
+ /* Once is enough: We don't need to record mergeinfo describing
+ the merge a second. If CHILD->ABSPATH is in
+ MERGE_B->ADDED_ABSPATHS, we'll do just that, so remove the
+ former from the latter. */
+ svn_hash_sets(merge_b->added_abspaths, child->abspath, NULL);
+ }
+
+ /* Elide explicit subtree mergeinfo whether or not we updated it. */
+ if (i > 0)
+ {
+ svn_boolean_t in_switched_subtree = FALSE;
+
+ if (child->switched)
+ in_switched_subtree = TRUE;
+ else if (i > 1)
+ {
+ /* Check if CHILD is part of a switched subtree */
+ svn_client__merge_path_t *parent;
+ int j = i - 1;
+ for (; j > 0; j--)
+ {
+ parent = APR_ARRAY_IDX(children_with_mergeinfo,
+ j, svn_client__merge_path_t *);
+ if (parent
+ && parent->switched
+ && svn_dirent_is_ancestor(parent->abspath,
+ child->abspath))
+ {
+ in_switched_subtree = TRUE;
+ break;
+ }
+ }
+ }
+
+ /* Allow mergeinfo on switched subtrees to elide to the
+ repository. Otherwise limit elision to the merge target
+ for now. do_directory_merge() will eventually try to
+ elide that when the merge is complete. */
+ SVN_ERR(svn_client__elide_mergeinfo(
+ child->abspath,
+ in_switched_subtree ? NULL : merge_b->target->abspath,
+ merge_b->ctx, iterpool));
+ }
+ } /* (i = 0; i < notify_b->children_with_mergeinfo->nelts; i++) */
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Helper for do_directory_merge().
+
+ Record mergeinfo describing a merge of
+ MERGED_RANGE->START:MERGED_RANGE->END from the repository relative path
+ MERGEINFO_FSPATH to each path in ADDED_ABSPATHS which has explicit
+ mergeinfo or is the immediate child of a parent with explicit
+ non-inheritable mergeinfo.
+
+ DEPTH, MERGE_B, and SQUELCH_MERGEINFO_NOTIFICATIONS, are
+ cascaded from do_directory_merge's arguments of the same names.
+
+ Note: This is intended to support forward merges only, i.e.
+ MERGED_RANGE->START must be older than MERGED_RANGE->END.
+*/
+static svn_error_t *
+record_mergeinfo_for_added_subtrees(
+ svn_merge_range_t *merged_range,
+ const char *mergeinfo_fspath,
+ svn_depth_t depth,
+ svn_boolean_t squelch_mergeinfo_notifications,
+ apr_hash_t *added_abspaths,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *pool)
+{
+ apr_pool_t *iterpool;
+ apr_hash_index_t *hi;
+
+ /* If no paths were added by the merge then we have nothing to do. */
+ if (!added_abspaths)
+ return SVN_NO_ERROR;
+
+ SVN_ERR_ASSERT(merged_range->start < merged_range->end);
+
+ iterpool = svn_pool_create(pool);
+ for (hi = apr_hash_first(pool, added_abspaths); hi; hi = apr_hash_next(hi))
+ {
+ const char *added_abspath = svn__apr_hash_index_key(hi);
+ const char *dir_abspath;
+ svn_mergeinfo_t parent_mergeinfo;
+ svn_mergeinfo_t added_path_mergeinfo;
+
+ svn_pool_clear(iterpool);
+ dir_abspath = svn_dirent_dirname(added_abspath, iterpool);
+
+ /* Grab the added path's explicit mergeinfo. */
+ SVN_ERR(svn_client__get_wc_mergeinfo(&added_path_mergeinfo, NULL,
+ svn_mergeinfo_explicit,
+ added_abspath, NULL, NULL, FALSE,
+ merge_b->ctx, iterpool, iterpool));
+
+ /* If the added path doesn't have explicit mergeinfo, does its immediate
+ parent have non-inheritable mergeinfo? */
+ if (!added_path_mergeinfo)
+ SVN_ERR(svn_client__get_wc_mergeinfo(&parent_mergeinfo, NULL,
+ svn_mergeinfo_explicit,
+ dir_abspath, NULL, NULL, FALSE,
+ merge_b->ctx,
+ iterpool, iterpool));
+
+ if (added_path_mergeinfo
+ || svn_mergeinfo__is_noninheritable(parent_mergeinfo, iterpool))
+ {
+ svn_node_kind_t added_path_kind;
+ svn_mergeinfo_t merge_mergeinfo;
+ svn_mergeinfo_t adds_history_as_mergeinfo;
+ svn_rangelist_t *rangelist;
+ const char *rel_added_path;
+ const char *added_path_mergeinfo_fspath;
+ svn_client__pathrev_t *added_path_pathrev;
+
+ SVN_ERR(svn_wc_read_kind2(&added_path_kind, merge_b->ctx->wc_ctx,
+ added_abspath, FALSE, FALSE, iterpool));
+
+ /* Calculate the naive mergeinfo describing the merge. */
+ merge_mergeinfo = apr_hash_make(iterpool);
+ rangelist = svn_rangelist__initialize(
+ merged_range->start, merged_range->end,
+ ((added_path_kind == svn_node_file)
+ || (!(depth == svn_depth_infinity
+ || depth == svn_depth_immediates))),
+ iterpool);
+
+ /* Create the new mergeinfo path for added_path's mergeinfo.
+ (added_abspath had better be a child of MERGE_B->target->abspath
+ or something is *really* wrong.) */
+ rel_added_path = svn_dirent_is_child(merge_b->target->abspath,
+ added_abspath, iterpool);
+ SVN_ERR_ASSERT(rel_added_path);
+ added_path_mergeinfo_fspath = svn_fspath__join(mergeinfo_fspath,
+ rel_added_path,
+ iterpool);
+ svn_hash_sets(merge_mergeinfo, added_path_mergeinfo_fspath,
+ rangelist);
+
+ /* Don't add new mergeinfo to describe the merge if that mergeinfo
+ contains non-existent merge sources.
+
+ We know that MERGEINFO_PATH/rel_added_path's history does not
+ span MERGED_RANGE->START:MERGED_RANGE->END but rather that it
+ was added at some revions greater than MERGED_RANGE->START
+ (assuming this is a forward merge). It may have been added,
+ deleted, and re-added many times. The point is that we cannot
+ blindly apply the naive mergeinfo calculated above because it
+ will describe non-existent merge sources. To avoid this we get
+ take the intersection of the naive mergeinfo with
+ MERGEINFO_PATH/rel_added_path's history. */
+ added_path_pathrev = svn_client__pathrev_create_with_relpath(
+ merge_b->target->loc.repos_root_url,
+ merge_b->target->loc.repos_uuid,
+ MAX(merged_range->start, merged_range->end),
+ added_path_mergeinfo_fspath + 1, iterpool);
+ SVN_ERR(svn_client__get_history_as_mergeinfo(
+ &adds_history_as_mergeinfo, NULL,
+ added_path_pathrev,
+ MAX(merged_range->start, merged_range->end),
+ MIN(merged_range->start, merged_range->end),
+ merge_b->ra_session2, merge_b->ctx, iterpool));
+
+ SVN_ERR(svn_mergeinfo_intersect2(&merge_mergeinfo,
+ merge_mergeinfo,
+ adds_history_as_mergeinfo,
+ FALSE, iterpool, iterpool));
+
+ /* Combine the explicit mergeinfo on the added path (if any)
+ with the mergeinfo describing this merge. */
+ if (added_path_mergeinfo)
+ SVN_ERR(svn_mergeinfo_merge2(merge_mergeinfo,
+ added_path_mergeinfo,
+ iterpool, iterpool));
+ SVN_ERR(svn_client__record_wc_mergeinfo(
+ added_abspath, merge_mergeinfo,
+ !squelch_mergeinfo_notifications, merge_b->ctx, iterpool));
+ }
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+/* Baton structure for log_noop_revs. */
+typedef struct log_noop_baton_t
+{
+ /* See the comment 'THE CHILDREN_WITH_MERGEINFO ARRAY' at the start
+ of this file.*/
+ apr_array_header_t *children_with_mergeinfo;
+
+ /* Absolute repository path of younger of the two merge sources
+ being diffed. */
+ const char *source_fspath;
+
+ /* The merge target. */
+ const merge_target_t *target;
+
+ /* Initially empty rangelists allocated in POOL. The rangelists are
+ * populated across multiple invocations of log_noop_revs(). */
+ svn_rangelist_t *operative_ranges;
+ svn_rangelist_t *merged_ranges;
+
+ /* Pool to store the rangelists. */
+ apr_pool_t *pool;
+} log_noop_baton_t;
+
+/* Helper for log_noop_revs: Merge a svn_merge_range_t representation of
+ REVISION into RANGELIST. New elements added to rangelist are allocated
+ in RESULT_POOL.
+
+ This is *not* a general purpose rangelist merge but a special replacement
+ for svn_rangelist_merge when REVISION is guaranteed to be younger than any
+ element in RANGELIST. svn_rangelist_merge is O(n) worst-case (i.e. when
+ all the ranges in output rangelist are older than the incoming changes).
+ This turns the special case of a single incoming younger range into O(1).
+ */
+static svn_error_t *
+rangelist_merge_revision(svn_rangelist_t *rangelist,
+ svn_revnum_t revision,
+ apr_pool_t *result_pool)
+{
+ svn_merge_range_t *new_range;
+ if (rangelist->nelts)
+ {
+ svn_merge_range_t *range = APR_ARRAY_IDX(rangelist, rangelist->nelts - 1,
+ svn_merge_range_t *);
+ if (range->end == revision - 1)
+ {
+ /* REVISION is adjacent to the youngest range in RANGELIST
+ so we can simply expand that range to encompass REVISION. */
+ range->end = revision;
+ return SVN_NO_ERROR;
+ }
+ }
+ new_range = apr_palloc(result_pool, sizeof(*new_range));
+ new_range->start = revision - 1;
+ new_range->end = revision;
+ new_range->inheritable = TRUE;
+
+ APR_ARRAY_PUSH(rangelist, svn_merge_range_t *) = new_range;
+
+ return SVN_NO_ERROR;
+}
+
+/* Implements the svn_log_entry_receiver_t interface.
+
+ BATON is an log_noop_baton_t *.
+
+ Add LOG_ENTRY->REVISION to BATON->OPERATIVE_RANGES.
+
+ If LOG_ENTRY->REVISION has already been fully merged to
+ BATON->target->abspath per the mergeinfo in BATON->CHILDREN_WITH_MERGEINFO,
+ then add LOG_ENTRY->REVISION to BATON->MERGED_RANGES.
+
+ Use SCRATCH_POOL for temporary allocations. Allocate additions to
+ BATON->MERGED_RANGES and BATON->OPERATIVE_RANGES in BATON->POOL.
+
+ Note: This callback must be invoked from oldest LOG_ENTRY->REVISION
+ to youngest LOG_ENTRY->REVISION -- see rangelist_merge_revision().
+*/
+static svn_error_t *
+log_noop_revs(void *baton,
+ svn_log_entry_t *log_entry,
+ apr_pool_t *scratch_pool)
+{
+ log_noop_baton_t *log_gap_baton = baton;
+ apr_hash_index_t *hi;
+ svn_revnum_t revision;
+ svn_boolean_t log_entry_rev_required = FALSE;
+
+ revision = log_entry->revision;
+
+ /* It's possible that authz restrictions on the merge source prevent us
+ from knowing about any of the changes for LOG_ENTRY->REVISION. */
+ if (!log_entry->changed_paths2)
+ return SVN_NO_ERROR;
+
+ /* Unconditionally add LOG_ENTRY->REVISION to BATON->OPERATIVE_MERGES. */
+ SVN_ERR(rangelist_merge_revision(log_gap_baton->operative_ranges,
+ revision,
+ log_gap_baton->pool));
+
+ /* Examine each path affected by LOG_ENTRY->REVISION. If the explicit or
+ inherited mergeinfo for *all* of the corresponding paths under
+ BATON->target->abspath reflects that LOG_ENTRY->REVISION has been
+ merged, then add LOG_ENTRY->REVISION to BATON->MERGED_RANGES. */
+ for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *fspath = svn__apr_hash_index_key(hi);
+ const char *rel_path;
+ const char *cwmi_abspath;
+ svn_rangelist_t *paths_explicit_rangelist = NULL;
+ svn_boolean_t mergeinfo_inherited = FALSE;
+
+ /* Adjust REL_PATH so it is relative to the merge source then use it to
+ calculate what path in the merge target would be affected by this
+ revision. */
+ rel_path = svn_fspath__skip_ancestor(log_gap_baton->source_fspath,
+ fspath);
+ /* Is PATH even within the merge target? If it isn't we
+ can disregard it altogether. */
+ if (rel_path == NULL)
+ continue;
+ cwmi_abspath = svn_dirent_join(log_gap_baton->target->abspath,
+ rel_path, scratch_pool);
+
+ /* Find any explicit or inherited mergeinfo for PATH. */
+ while (!log_entry_rev_required)
+ {
+ svn_client__merge_path_t *child = get_child_with_mergeinfo(
+ log_gap_baton->children_with_mergeinfo, cwmi_abspath);
+
+ if (child && child->pre_merge_mergeinfo)
+ {
+ /* Found some explicit mergeinfo, grab any ranges
+ for PATH. */
+ paths_explicit_rangelist =
+ svn_hash_gets(child->pre_merge_mergeinfo, fspath);
+ break;
+ }
+
+ if (cwmi_abspath[0] == '\0'
+ || svn_dirent_is_root(cwmi_abspath, strlen(cwmi_abspath))
+ || strcmp(log_gap_baton->target->abspath, cwmi_abspath) == 0)
+ {
+ /* Can't crawl any higher. */
+ break;
+ }
+
+ /* Didn't find anything so crawl up to the parent. */
+ cwmi_abspath = svn_dirent_dirname(cwmi_abspath, scratch_pool);
+ fspath = svn_fspath__dirname(fspath, scratch_pool);
+
+ /* At this point *if* we find mergeinfo it will be inherited. */
+ mergeinfo_inherited = TRUE;
+ }
+
+ if (paths_explicit_rangelist)
+ {
+ svn_rangelist_t *intersecting_range;
+ svn_rangelist_t *rangelist;
+
+ rangelist = svn_rangelist__initialize(revision - 1, revision, TRUE,
+ scratch_pool);
+
+ /* If PATH inherited mergeinfo we must consider inheritance in the
+ event the inherited mergeinfo is actually non-inheritable. */
+ SVN_ERR(svn_rangelist_intersect(&intersecting_range,
+ paths_explicit_rangelist,
+ rangelist,
+ mergeinfo_inherited, scratch_pool));
+
+ if (intersecting_range->nelts == 0)
+ log_entry_rev_required = TRUE;
+ }
+ else
+ {
+ log_entry_rev_required = TRUE;
+ }
+ }
+
+ if (!log_entry_rev_required)
+ SVN_ERR(rangelist_merge_revision(log_gap_baton->merged_ranges,
+ revision,
+ log_gap_baton->pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for do_directory_merge().
+
+ SOURCE is cascaded from the argument of the same name in
+ do_directory_merge(). TARGET is the merge target. RA_SESSION is the
+ session for SOURCE->loc2.
+
+ Find all the ranges required by subtrees in
+ CHILDREN_WITH_MERGEINFO that are *not* required by
+ TARGET->abspath (i.e. CHILDREN_WITH_MERGEINFO[0]). If such
+ ranges exist, then find any subset of ranges which, if merged, would be
+ inoperative. Finally, if any inoperative ranges are found then remove
+ these ranges from all of the subtree's REMAINING_RANGES.
+
+ This function should only be called when honoring mergeinfo during
+ forward merges (i.e. SOURCE->rev1 < SOURCE->rev2).
+*/
+static svn_error_t *
+remove_noop_subtree_ranges(const merge_source_t *source,
+ const merge_target_t *target,
+ svn_ra_session_t *ra_session,
+ apr_array_header_t *children_with_mergeinfo,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ /* ### Do we need to check that we are at a uniform working revision? */
+ int i;
+ svn_client__merge_path_t *root_child =
+ APR_ARRAY_IDX(children_with_mergeinfo, 0, svn_client__merge_path_t *);
+ svn_rangelist_t *requested_ranges;
+ svn_rangelist_t *subtree_gap_ranges;
+ svn_rangelist_t *subtree_remaining_ranges;
+ log_noop_baton_t log_gap_baton;
+ svn_merge_range_t *oldest_gap_rev;
+ svn_merge_range_t *youngest_gap_rev;
+ svn_rangelist_t *inoperative_ranges;
+ apr_pool_t *iterpool;
+ const char *longest_common_subtree_ancestor = NULL;
+ svn_error_t *err;
+
+ assert(session_url_is(ra_session, source->loc2->url, scratch_pool));
+
+ /* This function is only intended to work with forward merges. */
+ if (source->loc1->rev > source->loc2->rev)
+ return SVN_NO_ERROR;
+
+ /* Another easy out: There are no subtrees. */
+ if (children_with_mergeinfo->nelts < 2)
+ return SVN_NO_ERROR;
+
+ subtree_remaining_ranges = apr_array_make(scratch_pool, 1,
+ sizeof(svn_merge_range_t *));
+
+ /* Given the requested merge of SOURCE->rev1:rev2 might there be any
+ part of this range required for subtrees but not for the target? */
+ requested_ranges = svn_rangelist__initialize(MIN(source->loc1->rev,
+ source->loc2->rev),
+ MAX(source->loc1->rev,
+ source->loc2->rev),
+ TRUE, scratch_pool);
+ SVN_ERR(svn_rangelist_remove(&subtree_gap_ranges,
+ root_child->remaining_ranges,
+ requested_ranges, FALSE, scratch_pool));
+
+ /* Early out, nothing to operate on */
+ if (!subtree_gap_ranges->nelts)
+ return SVN_NO_ERROR;
+
+ /* Create a rangelist describing every range required across all subtrees. */
+ iterpool = svn_pool_create(scratch_pool);
+ for (i = 1; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *);
+
+ svn_pool_clear(iterpool);
+
+ /* Issue #4269: Keep track of the longest common ancestor of all the
+ subtrees which require merges. This may be a child of
+ TARGET->ABSPATH, which will allow us to narrow the log request
+ below. */
+ if (child->remaining_ranges && child->remaining_ranges->nelts)
+ {
+ if (longest_common_subtree_ancestor)
+ longest_common_subtree_ancestor = svn_dirent_get_longest_ancestor(
+ longest_common_subtree_ancestor, child->abspath, scratch_pool);
+ else
+ longest_common_subtree_ancestor = child->abspath;
+ }
+
+ /* CHILD->REMAINING_RANGES will be NULL if child is absent. */
+ if (child->remaining_ranges && child->remaining_ranges->nelts)
+ SVN_ERR(svn_rangelist_merge2(subtree_remaining_ranges,
+ child->remaining_ranges,
+ scratch_pool, iterpool));
+ }
+ svn_pool_destroy(iterpool);
+
+ /* It's possible that none of the subtrees had any remaining ranges. */
+ if (!subtree_remaining_ranges->nelts)
+ return SVN_NO_ERROR;
+
+ /* Ok, *finally* we can answer what part(s) of SOURCE->rev1:rev2 are
+ required for the subtrees but not the target. */
+ SVN_ERR(svn_rangelist_intersect(&subtree_gap_ranges,
+ subtree_gap_ranges,
+ subtree_remaining_ranges, FALSE,
+ scratch_pool));
+
+ /* Another early out */
+ if (!subtree_gap_ranges->nelts)
+ return SVN_NO_ERROR;
+
+ /* One or more subtrees need some revisions that the target doesn't need.
+ Use log to determine if any of these revisions are inoperative. */
+ oldest_gap_rev = APR_ARRAY_IDX(subtree_gap_ranges, 0, svn_merge_range_t *);
+ youngest_gap_rev = APR_ARRAY_IDX(subtree_gap_ranges,
+ subtree_gap_ranges->nelts - 1, svn_merge_range_t *);
+
+ /* Set up the log baton. */
+ log_gap_baton.children_with_mergeinfo = children_with_mergeinfo;
+ log_gap_baton.source_fspath
+ = svn_client__pathrev_fspath(source->loc2, result_pool);
+ log_gap_baton.target = target;
+ log_gap_baton.merged_ranges = apr_array_make(scratch_pool, 0,
+ sizeof(svn_revnum_t *));
+ log_gap_baton.operative_ranges = apr_array_make(scratch_pool, 0,
+ sizeof(svn_revnum_t *));
+ log_gap_baton.pool = svn_pool_create(scratch_pool);
+
+ /* Find the longest common ancestor of all subtrees relative to
+ RA_SESSION's URL. */
+ if (longest_common_subtree_ancestor)
+ longest_common_subtree_ancestor =
+ svn_dirent_skip_ancestor(target->abspath,
+ longest_common_subtree_ancestor);
+ else
+ longest_common_subtree_ancestor = "";
+
+ /* Invoke the svn_log_entry_receiver_t receiver log_noop_revs() from
+ oldest to youngest. The receiver is optimized to add ranges to
+ log_gap_baton.merged_ranges and log_gap_baton.operative_ranges, but
+ requires that the revs arrive oldest to youngest -- see log_noop_revs()
+ and rangelist_merge_revision(). */
+ err = get_log(ra_session, longest_common_subtree_ancestor,
+ oldest_gap_rev->start + 1, youngest_gap_rev->end, TRUE,
+ log_noop_revs, &log_gap_baton, scratch_pool);
+
+ /* It's possible that the only subtrees with mergeinfo in TARGET don't have
+ any corresponding subtree in SOURCE between SOURCE->REV1 < SOURCE->REV2.
+ So it's also possible that we may ask for the logs of non-existent paths.
+ If we do, then assume that no subtree requires any ranges that are not
+ already required by the TARGET. */
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_FS_NOT_FOUND
+ && longest_common_subtree_ancestor[0] != '\0')
+ return svn_error_trace(err);
+
+ /* Asked about a non-existent subtree in SOURCE. */
+ svn_error_clear(err);
+ log_gap_baton.merged_ranges =
+ svn_rangelist__initialize(oldest_gap_rev->start,
+ youngest_gap_rev->end,
+ TRUE, scratch_pool);
+ }
+ else
+ {
+ inoperative_ranges = svn_rangelist__initialize(oldest_gap_rev->start,
+ youngest_gap_rev->end,
+ TRUE, scratch_pool);
+ SVN_ERR(svn_rangelist_remove(&(inoperative_ranges),
+ log_gap_baton.operative_ranges,
+ inoperative_ranges, FALSE, scratch_pool));
+ SVN_ERR(svn_rangelist_merge2(log_gap_baton.merged_ranges, inoperative_ranges,
+ scratch_pool, scratch_pool));
+ }
+
+ for (i = 1; i < children_with_mergeinfo->nelts; i++)
+ {
+ svn_client__merge_path_t *child =
+ APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *);
+
+ /* CHILD->REMAINING_RANGES will be NULL if child is absent. */
+ if (child->remaining_ranges && child->remaining_ranges->nelts)
+ {
+ /* Remove inoperative ranges from all children so we don't perform
+ inoperative editor drives. */
+ SVN_ERR(svn_rangelist_remove(&(child->remaining_ranges),
+ log_gap_baton.merged_ranges,
+ child->remaining_ranges,
+ FALSE, result_pool));
+ }
+ }
+
+ svn_pool_destroy(log_gap_baton.pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Perform a merge of changes in SOURCE to the working copy path
+ TARGET_ABSPATH. Both URLs in SOURCE, and TARGET_ABSPATH all represent
+ directories -- for the single file case, the caller should use
+ do_file_merge().
+
+ CHILDREN_WITH_MERGEINFO and MERGE_B describe the merge being performed
+ As this function is for a mergeinfo-aware merge, SOURCE->ancestral
+ should be TRUE, and SOURCE->loc1 must be a historical ancestor of
+ SOURCE->loc2, or vice-versa (see `MERGEINFO MERGE SOURCE NORMALIZATION'
+ for more requirements around SOURCE).
+
+ Mergeinfo changes will be recorded unless MERGE_B->dry_run is true.
+
+ If mergeinfo is being recorded, SQUELCH_MERGEINFO_NOTIFICATIONS is FALSE,
+ and MERGE_B->CTX->NOTIFY_FUNC2 is not NULL, then call
+ MERGE_B->CTX->NOTIFY_FUNC2 with MERGE_B->CTX->NOTIFY_BATON2 and a
+ svn_wc_notify_merge_record_info_begin notification before any mergeinfo
+ changes are made to describe the merge performed.
+
+ If mergeinfo is being recorded to describe this merge, and RESULT_CATALOG
+ is not NULL, then don't record the new mergeinfo on the WC, but instead
+ record it in RESULT_CATALOG, where the keys are absolute working copy
+ paths and the values are the new mergeinfos for each. Allocate additions
+ to RESULT_CATALOG in pool which RESULT_CATALOG was created in.
+
+ Handle DEPTH as documented for svn_client_merge5().
+
+ CONFLICT_REPORT is as documented for do_directory_merge().
+
+ Perform any temporary allocations in SCRATCH_POOL.
+
+ NOTE: This is a wrapper around drive_merge_report_editor() which
+ handles the complexities inherent to situations where a given
+ directory's children may have intersecting merges (because they
+ meet one or more of the criteria described in get_mergeinfo_paths()).
+*/
+static svn_error_t *
+do_mergeinfo_aware_dir_merge(svn_mergeinfo_catalog_t result_catalog,
+ single_range_conflict_report_t **conflict_report,
+ const merge_source_t *source,
+ const char *target_abspath,
+ apr_array_header_t *children_with_mergeinfo,
+ const svn_diff_tree_processor_t *processor,
+ svn_depth_t depth,
+ svn_boolean_t squelch_mergeinfo_notifications,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ /* The range defining the mergeinfo we will record to describe the merge
+ (assuming we are recording mergeinfo
+
+ Note: This may be a subset of SOURCE->rev1:rev2 if
+ populate_remaining_ranges() determines that some part of
+ SOURCE->rev1:rev2 has already been wholly merged to TARGET_ABSPATH.
+ Also, the actual editor drive(s) may be a subset of RANGE, if
+ remove_noop_subtree_ranges() and/or fix_deleted_subtree_ranges()
+ further tweak things. */
+ svn_merge_range_t range;
+
+ svn_ra_session_t *ra_session;
+ svn_client__merge_path_t *target_merge_path;
+ svn_boolean_t is_rollback = (source->loc1->rev > source->loc2->rev);
+
+ SVN_ERR_ASSERT(source->ancestral);
+
+ /*** If we get here, we're dealing with related sources from the
+ same repository as the target -- merge tracking might be
+ happenin'! ***/
+
+ *conflict_report = NULL;
+
+ /* Point our RA_SESSION to the URL of our youngest merge source side. */
+ ra_session = is_rollback ? merge_b->ra_session1 : merge_b->ra_session2;
+
+ /* Fill NOTIFY_B->CHILDREN_WITH_MERGEINFO with child paths (const
+ svn_client__merge_path_t *) which might have intersecting merges
+ because they meet one or more of the criteria described in
+ get_mergeinfo_paths(). Here the paths are arranged in a depth
+ first order. */
+ SVN_ERR(get_mergeinfo_paths(children_with_mergeinfo,
+ merge_b->target, depth,
+ merge_b->dry_run, merge_b->same_repos,
+ merge_b->ctx, scratch_pool, scratch_pool));
+
+ /* The first item from the NOTIFY_B->CHILDREN_WITH_MERGEINFO is always
+ the target thanks to depth-first ordering. */
+ target_merge_path = APR_ARRAY_IDX(children_with_mergeinfo, 0,
+ svn_client__merge_path_t *);
+
+ /* If we are honoring mergeinfo, then for each item in
+ NOTIFY_B->CHILDREN_WITH_MERGEINFO, we need to calculate what needs to be
+ merged, and then merge it. Otherwise, we just merge what we were asked
+ to merge across the whole tree. */
+ SVN_ERR(populate_remaining_ranges(children_with_mergeinfo,
+ source, ra_session,
+ merge_b, scratch_pool, scratch_pool));
+
+ /* Always start with a range which describes the most inclusive merge
+ possible, i.e. SOURCE->rev1:rev2. */
+ range.start = source->loc1->rev;
+ range.end = source->loc2->rev;
+ range.inheritable = TRUE;
+
+ if (!merge_b->reintegrate_merge)
+ {
+ svn_revnum_t new_range_start, start_rev;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ /* The merge target TARGET_ABSPATH and/or its subtrees may not need all
+ of SOURCE->rev1:rev2 applied. So examine
+ NOTIFY_B->CHILDREN_WITH_MERGEINFO to find the oldest starting
+ revision that actually needs to be merged (for reverse merges this is
+ the youngest starting revision).
+
+ We'll do this twice, right now for the start of the mergeinfo we will
+ ultimately record to describe this merge and then later for the
+ start of the actual editor drive. */
+ new_range_start = get_most_inclusive_rev(
+ children_with_mergeinfo, is_rollback, TRUE);
+ if (SVN_IS_VALID_REVNUM(new_range_start))
+ range.start = new_range_start;
+
+ /* Remove inoperative ranges from any subtrees' remaining_ranges
+ to spare the expense of noop editor drives. */
+ if (!is_rollback)
+ SVN_ERR(remove_noop_subtree_ranges(source, merge_b->target,
+ ra_session,
+ children_with_mergeinfo,
+ scratch_pool, iterpool));
+
+ /* Adjust subtrees' remaining_ranges to deal with issue #3067:
+ * "subtrees that don't exist at the start or end of a merge range
+ * shouldn't break the merge". */
+ SVN_ERR(fix_deleted_subtree_ranges(source, merge_b->target,
+ ra_session,
+ children_with_mergeinfo,
+ merge_b->ctx, scratch_pool, iterpool));
+
+ /* remove_noop_subtree_ranges() and/or fix_deleted_subtree_range()
+ may have further refined the starting revision for our editor
+ drive. */
+ start_rev =
+ get_most_inclusive_rev(children_with_mergeinfo,
+ is_rollback, TRUE);
+
+ /* Is there anything to merge? */
+ if (SVN_IS_VALID_REVNUM(start_rev))
+ {
+ /* Now examine NOTIFY_B->CHILDREN_WITH_MERGEINFO to find the oldest
+ ending revision that actually needs to be merged (for reverse
+ merges this is the youngest ending revision). */
+ svn_revnum_t end_rev =
+ get_most_inclusive_rev(children_with_mergeinfo,
+ is_rollback, FALSE);
+
+ /* While END_REV is valid, do the following:
+
+ 1. Tweak each NOTIFY_B->CHILDREN_WITH_MERGEINFO element so that
+ the element's remaining_ranges member has as its first element
+ a range that ends with end_rev.
+
+ 2. Starting with start_rev, call drive_merge_report_editor()
+ on MERGE_B->target->abspath for start_rev:end_rev.
+
+ 3. Remove the first element from each
+ NOTIFY_B->CHILDREN_WITH_MERGEINFO element's remaining_ranges
+ member.
+
+ 4. Again examine NOTIFY_B->CHILDREN_WITH_MERGEINFO to find the most
+ inclusive starting revision that actually needs to be merged and
+ update start_rev. This prevents us from needlessly contacting the
+ repository and doing a diff where we describe the entire target
+ tree as *not* needing any of the requested range. This can happen
+ whenever we have mergeinfo with gaps in it for the merge source.
+
+ 5. Again examine NOTIFY_B->CHILDREN_WITH_MERGEINFO to find the most
+ inclusive ending revision that actually needs to be merged and
+ update end_rev.
+
+ 6. Lather, rinse, repeat.
+ */
+
+ while (end_rev != SVN_INVALID_REVNUM)
+ {
+ merge_source_t *real_source;
+ svn_merge_range_t *first_target_range
+ = (target_merge_path->remaining_ranges->nelts == 0 ? NULL
+ : APR_ARRAY_IDX(target_merge_path->remaining_ranges, 0,
+ svn_merge_range_t *));
+
+ /* Issue #3324: Stop editor abuse! Don't call
+ drive_merge_report_editor() in such a way that we request an
+ editor with svn_client__get_diff_editor() for some rev X,
+ then call svn_ra_do_diff3() for some revision Y, and then
+ call reporter->set_path(PATH=="") to set the root revision
+ for the editor drive to revision Z where
+ (X != Z && X < Z < Y). This is bogus because the server will
+ send us the diff between X:Y but the client is expecting the
+ diff between Y:Z. See issue #3324 for full details on the
+ problems this can cause. */
+ if (first_target_range
+ && start_rev != first_target_range->start)
+ {
+ if (is_rollback)
+ {
+ if (end_rev < first_target_range->start)
+ end_rev = first_target_range->start;
+ }
+ else
+ {
+ if (end_rev > first_target_range->start)
+ end_rev = first_target_range->start;
+ }
+ }
+
+ svn_pool_clear(iterpool);
+
+ slice_remaining_ranges(children_with_mergeinfo,
+ is_rollback, end_rev, scratch_pool);
+
+ /* Reset variables that must be reset for every drive */
+ merge_b->notify_begin.last_abspath = NULL;
+
+ real_source = subrange_source(source, start_rev, end_rev, iterpool);
+ SVN_ERR(drive_merge_report_editor(
+ merge_b->target->abspath,
+ real_source,
+ children_with_mergeinfo,
+ processor,
+ depth,
+ merge_b,
+ iterpool));
+
+ /* If any paths picked up explicit mergeinfo as a result of
+ the merge we need to make sure any mergeinfo those paths
+ inherited is recorded and then add these paths to
+ NOTIFY_B->CHILDREN_WITH_MERGEINFO.*/
+ SVN_ERR(process_children_with_new_mergeinfo(
+ merge_b, children_with_mergeinfo,
+ scratch_pool));
+
+ /* If any subtrees had their explicit mergeinfo deleted as a
+ result of the merge then remove these paths from
+ NOTIFY_B->CHILDREN_WITH_MERGEINFO since there is no need
+ to consider these subtrees for subsequent editor drives
+ nor do we want to record mergeinfo on them describing
+ the merge itself. */
+ remove_children_with_deleted_mergeinfo(
+ merge_b, children_with_mergeinfo);
+
+ /* Prepare for the next iteration (if any). */
+ remove_first_range_from_remaining_ranges(
+ end_rev, children_with_mergeinfo, scratch_pool);
+
+ /* If we raised any conflicts, break out and report how much
+ we have merged. */
+ if (is_path_conflicted_by_merge(merge_b))
+ {
+ merge_source_t *remaining_range = NULL;
+
+ if (real_source->loc2->rev != source->loc2->rev)
+ remaining_range = subrange_source(source,
+ real_source->loc2->rev,
+ source->loc2->rev,
+ scratch_pool);
+ *conflict_report = single_range_conflict_report_create(
+ real_source, remaining_range,
+ result_pool);
+
+ range.end = end_rev;
+ break;
+ }
+
+ start_rev =
+ get_most_inclusive_rev(children_with_mergeinfo,
+ is_rollback, TRUE);
+ end_rev =
+ get_most_inclusive_rev(children_with_mergeinfo,
+ is_rollback, FALSE);
+ }
+ }
+ svn_pool_destroy(iterpool);
+ }
+ else
+ {
+ if (!merge_b->record_only)
+ {
+ /* Reset cur_ancestor_abspath to null so that subsequent cherry
+ picked revision ranges will be notified upon subsequent
+ operative merge. */
+ merge_b->notify_begin.last_abspath = NULL;
+
+ SVN_ERR(drive_merge_report_editor(merge_b->target->abspath,
+ source,
+ NULL,
+ processor,
+ depth,
+ merge_b,
+ scratch_pool));
+ }
+ }
+
+ /* Record mergeinfo where appropriate.*/
+ if (RECORD_MERGEINFO(merge_b))
+ {
+ const svn_client__pathrev_t *primary_src
+ = is_rollback ? source->loc1 : source->loc2;
+ const char *mergeinfo_path
+ = svn_client__pathrev_fspath(primary_src, scratch_pool);
+
+ SVN_ERR(record_mergeinfo_for_dir_merge(result_catalog,
+ &range,
+ mergeinfo_path,
+ children_with_mergeinfo,
+ depth,
+ squelch_mergeinfo_notifications,
+ merge_b,
+ scratch_pool));
+
+ /* If a path has an immediate parent with non-inheritable mergeinfo at
+ this point, then it meets criteria 3 or 5 described in
+ get_mergeinfo_paths' doc string. For paths which exist prior to a
+ merge explicit mergeinfo has already been set. But for paths added
+ during the merge this is not the case. The path might have explicit
+ mergeinfo from the merge source, but no mergeinfo yet exists
+ describing *this* merge. So the added path has either incomplete
+ explicit mergeinfo or inherits incomplete mergeinfo from its
+ immediate parent (if any, the parent might have only non-inheritable
+ ranges in which case the path simply inherits empty mergeinfo).
+
+ So here we look at the root path of each subtree added during the
+ merge and set explicit mergeinfo on it if it meets the aforementioned
+ conditions. */
+ if (range.start < range.end) /* Nothing to record on added subtrees
+ resulting from reverse merges. */
+ {
+ SVN_ERR(record_mergeinfo_for_added_subtrees(
+ &range, mergeinfo_path, depth,
+ squelch_mergeinfo_notifications,
+ merge_b->added_abspaths, merge_b, scratch_pool));
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for do_merge() when the merge target is a directory.
+ *
+ * If any conflict is raised during the merge, set *CONFLICTED_RANGE to
+ * the revision sub-range that raised the conflict. In this case, the
+ * merge will have ended at revision CONFLICTED_RANGE and mergeinfo will
+ * have been recorded for all revision sub-ranges up to and including
+ * CONFLICTED_RANGE. Otherwise, set *CONFLICTED_RANGE to NULL.
+ */
+static svn_error_t *
+do_directory_merge(svn_mergeinfo_catalog_t result_catalog,
+ single_range_conflict_report_t **conflict_report,
+ const merge_source_t *source,
+ const char *target_abspath,
+ const svn_diff_tree_processor_t *processor,
+ svn_depth_t depth,
+ svn_boolean_t squelch_mergeinfo_notifications,
+ merge_cmd_baton_t *merge_b,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *children_with_mergeinfo;
+
+ /* Initialize CHILDREN_WITH_MERGEINFO. See the comment
+ 'THE CHILDREN_WITH_MERGEINFO ARRAY' at the start of this file. */
+ children_with_mergeinfo =
+ apr_array_make(scratch_pool, 16, sizeof(svn_client__merge_path_t *));
+
+ /* And make it read-only accessible from the baton */
+ merge_b->notify_begin.nodes_with_mergeinfo = children_with_mergeinfo;
+
+ /* If we are not honoring mergeinfo we can skip right to the
+ business of merging changes! */
+ if (HONOR_MERGEINFO(merge_b))
+ SVN_ERR(do_mergeinfo_aware_dir_merge(result_catalog, conflict_report,
+ source, target_abspath,
+ children_with_mergeinfo,
+ processor, depth,
+ squelch_mergeinfo_notifications,
+ merge_b, result_pool, scratch_pool));
+ else
+ SVN_ERR(do_mergeinfo_unaware_dir_merge(conflict_report,
+ source, target_abspath,
+ children_with_mergeinfo,
+ processor, depth,
+ merge_b, result_pool, scratch_pool));
+
+ merge_b->notify_begin.nodes_with_mergeinfo = NULL;
+
+ return SVN_NO_ERROR;
+}
+
+/** Ensure that *RA_SESSION is opened to URL, either by reusing
+ * *RA_SESSION if it is non-null and already opened to URL's
+ * repository, or by allocating a new *RA_SESSION in POOL.
+ * (RA_SESSION itself cannot be null, of course.)
+ *
+ * CTX is used as for svn_client_open_ra_session().
+ */
+static svn_error_t *
+ensure_ra_session_url(svn_ra_session_t **ra_session,
+ const char *url,
+ const char *wri_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err = SVN_NO_ERROR;
+
+ if (*ra_session)
+ {
+ err = svn_ra_reparent(*ra_session, url, pool);
+ }
+
+ /* SVN_ERR_RA_ILLEGAL_URL is raised when url doesn't point to the same
+ repository as ra_session. */
+ if (! *ra_session || (err && err->apr_err == SVN_ERR_RA_ILLEGAL_URL))
+ {
+ svn_error_clear(err);
+ err = svn_client_open_ra_session2(ra_session, url, wri_abspath,
+ ctx, pool, pool);
+ }
+ SVN_ERR(err);
+
+ return SVN_NO_ERROR;
+}
+
+/* Drive a merge of MERGE_SOURCES into working copy node TARGET
+ and possibly record mergeinfo describing the merge -- see
+ RECORD_MERGEINFO().
+
+ If MODIFIED_SUBTREES is not NULL and all the MERGE_SOURCES are 'ancestral'
+ or REINTEGRATE_MERGE is true, then replace *MODIFIED_SUBTREES with a new
+ hash containing all the paths that *MODIFIED_SUBTREES contained before,
+ and also every path modified, skipped, added, or tree-conflicted
+ by the merge. Keys and values of the hash are both (const char *)
+ absolute paths. The contents of the hash are allocated in RESULT_POOL.
+
+ If the merge raises any conflicts while merging a revision range, return
+ early and set *CONFLICT_REPORT to describe the details. (In this case,
+ notify that the merge is complete if and only if this was the last
+ revision range of the merge.) If there are no conflicts, set
+ *CONFLICT_REPORT to NULL. A revision range here can be one specified
+ in MERGE_SOURCES or an internally generated sub-range of one of those
+ when merge tracking is in use.
+
+ For every (const merge_source_t *) merge source in MERGE_SOURCES, if
+ SOURCE->ANCESTRAL is set, then the "left" and "right" side are
+ ancestrally related. (See 'MERGEINFO MERGE SOURCE NORMALIZATION'
+ for more on what that means and how it matters.)
+
+ If SOURCES_RELATED is set, the "left" and "right" sides of the
+ merge source are historically related (ancestors, uncles, second
+ cousins thrice removed, etc...). (This is passed through to
+ do_file_merge() to simulate the history checks that the repository
+ logic does in the directory case.)
+
+ SAME_REPOS is TRUE iff the merge sources live in the same
+ repository as the one from which the target working copy has been
+ checked out.
+
+ If mergeinfo is being recorded, SQUELCH_MERGEINFO_NOTIFICATIONS is FALSE,
+ and CTX->NOTIFY_FUNC2 is not NULL, then call CTX->NOTIFY_FUNC2 with
+ CTX->NOTIFY_BATON2 and a svn_wc_notify_merge_record_info_begin
+ notification before any mergeinfo changes are made to describe the merge
+ performed.
+
+ If mergeinfo is being recorded to describe this merge, and RESULT_CATALOG
+ is not NULL, then don't record the new mergeinfo on the WC, but instead
+ record it in RESULT_CATALOG, where the keys are absolute working copy
+ paths and the values are the new mergeinfos for each. Allocate additions
+ to RESULT_CATALOG in pool which RESULT_CATALOG was created in.
+
+ FORCE_DELETE, DRY_RUN, RECORD_ONLY, DEPTH, MERGE_OPTIONS,
+ and CTX are as described in the docstring for svn_client_merge_peg3().
+
+ If IGNORE_MERGEINFO is true, disable merge tracking, by treating the two
+ sources as unrelated even if they actually have a common ancestor. See
+ the macro HONOR_MERGEINFO().
+
+ If DIFF_IGNORE_ANCESTRY is true, diff the 'left' and 'right' versions
+ of a node (if they are the same kind) as if they were related, even if
+ they are not related. Otherwise, diff unrelated items as a deletion
+ of one thing and the addition of another.
+
+ If not NULL, RECORD_ONLY_PATHS is a hash of (const char *) paths mapped
+ to the same. If RECORD_ONLY is true and RECORD_ONLY_PATHS is not NULL,
+ then record mergeinfo describing the merge only on subtrees which contain
+ items from RECORD_ONLY_PATHS. If RECORD_ONLY is true and RECORD_ONLY_PATHS
+ is NULL, then record mergeinfo on every subtree with mergeinfo in
+ TARGET.
+
+ REINTEGRATE_MERGE is TRUE if this is a reintegrate merge.
+
+ *USE_SLEEP will be set TRUE if a sleep is required to ensure timestamp
+ integrity, *USE_SLEEP will be unchanged if no sleep is required.
+
+ SCRATCH_POOL is used for all temporary allocations.
+*/
+static svn_error_t *
+do_merge(apr_hash_t **modified_subtrees,
+ svn_mergeinfo_catalog_t result_catalog,
+ conflict_report_t **conflict_report,
+ svn_boolean_t *use_sleep,
+ const apr_array_header_t *merge_sources,
+ const merge_target_t *target,
+ svn_ra_session_t *src_session,
+ svn_boolean_t sources_related,
+ svn_boolean_t same_repos,
+ svn_boolean_t ignore_mergeinfo,
+ svn_boolean_t diff_ignore_ancestry,
+ svn_boolean_t force_delete,
+ svn_boolean_t dry_run,
+ svn_boolean_t record_only,
+ apr_hash_t *record_only_paths,
+ svn_boolean_t reintegrate_merge,
+ svn_boolean_t squelch_mergeinfo_notifications,
+ svn_depth_t depth,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ merge_cmd_baton_t merge_cmd_baton = { 0 };
+ svn_config_t *cfg;
+ const char *diff3_cmd;
+ int i;
+ svn_boolean_t checked_mergeinfo_capability = FALSE;
+ svn_ra_session_t *ra_session1 = NULL, *ra_session2 = NULL;
+ const char *old_src_session_url = NULL;
+ apr_pool_t *iterpool;
+ const svn_diff_tree_processor_t *processor;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(target->abspath));
+
+ *conflict_report = NULL;
+
+ /* Check from some special conditions when in record-only mode
+ (which is a merge-tracking thing). */
+ if (record_only)
+ {
+ svn_boolean_t sources_ancestral = TRUE;
+ int j;
+
+ /* Find out whether all of the sources are 'ancestral'. */
+ for (j = 0; j < merge_sources->nelts; j++)
+ if (! APR_ARRAY_IDX(merge_sources, j, merge_source_t *)->ancestral)
+ {
+ sources_ancestral = FALSE;
+ break;
+ }
+
+ /* We can't do a record-only merge if the sources aren't related. */
+ if (! sources_ancestral)
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("Use of two URLs is not compatible with "
+ "mergeinfo modification"));
+
+ /* We can't do a record-only merge if the sources aren't from
+ the same repository as the target. */
+ if (! same_repos)
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("Merge from foreign repository is not "
+ "compatible with mergeinfo modification"));
+
+ /* If this is a dry-run record-only merge, there's nothing to do. */
+ if (dry_run)
+ return SVN_NO_ERROR;
+ }
+
+ iterpool = svn_pool_create(scratch_pool);
+
+ /* Ensure a known depth. */
+ if (depth == svn_depth_unknown)
+ depth = svn_depth_infinity;
+
+ /* Set up the diff3 command, so various callers don't have to. */
+ cfg = ctx->config
+ ? svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG)
+ : NULL;
+ svn_config_get(cfg, &diff3_cmd, SVN_CONFIG_SECTION_HELPERS,
+ SVN_CONFIG_OPTION_DIFF3_CMD, NULL);
+
+ if (diff3_cmd != NULL)
+ SVN_ERR(svn_path_cstring_to_utf8(&diff3_cmd, diff3_cmd, scratch_pool));
+
+ /* Build the merge context baton (or at least the parts of it that
+ don't need to be reset for each merge source). */
+ merge_cmd_baton.force_delete = force_delete;
+ merge_cmd_baton.dry_run = dry_run;
+ merge_cmd_baton.record_only = record_only;
+ merge_cmd_baton.ignore_mergeinfo = ignore_mergeinfo;
+ merge_cmd_baton.diff_ignore_ancestry = diff_ignore_ancestry;
+ merge_cmd_baton.same_repos = same_repos;
+ merge_cmd_baton.mergeinfo_capable = FALSE;
+ merge_cmd_baton.ctx = ctx;
+ merge_cmd_baton.reintegrate_merge = reintegrate_merge;
+ merge_cmd_baton.target = target;
+ merge_cmd_baton.pool = iterpool;
+ merge_cmd_baton.merge_options = merge_options;
+ merge_cmd_baton.diff3_cmd = diff3_cmd;
+ merge_cmd_baton.use_sleep = use_sleep;
+
+ /* Do we already know the specific subtrees with mergeinfo we want
+ to record-only mergeinfo on? */
+ if (record_only && record_only_paths)
+ merge_cmd_baton.merged_abspaths = record_only_paths;
+ else
+ merge_cmd_baton.merged_abspaths = apr_hash_make(result_pool);
+
+ merge_cmd_baton.skipped_abspaths = apr_hash_make(result_pool);
+ merge_cmd_baton.added_abspaths = apr_hash_make(result_pool);
+ merge_cmd_baton.tree_conflicted_abspaths = apr_hash_make(result_pool);
+
+ {
+ svn_diff_tree_processor_t *merge_processor;
+
+ merge_processor = svn_diff__tree_processor_create(&merge_cmd_baton,
+ scratch_pool);
+
+ merge_processor->dir_opened = merge_dir_opened;
+ merge_processor->dir_changed = merge_dir_changed;
+ merge_processor->dir_added = merge_dir_added;
+ merge_processor->dir_deleted = merge_dir_deleted;
+ merge_processor->dir_closed = merge_dir_closed;
+
+ merge_processor->file_opened = merge_file_opened;
+ merge_processor->file_changed = merge_file_changed;
+ merge_processor->file_added = merge_file_added;
+ merge_processor->file_deleted = merge_file_deleted;
+ /* Not interested in file_closed() */
+
+ merge_processor->node_absent = merge_node_absent;
+
+ processor = merge_processor;
+ }
+
+ if (src_session)
+ {
+ SVN_ERR(svn_ra_get_session_url(src_session, &old_src_session_url,
+ scratch_pool));
+ ra_session1 = src_session;
+ }
+
+ for (i = 0; i < merge_sources->nelts; i++)
+ {
+ svn_node_kind_t src1_kind;
+ merge_source_t *source =
+ APR_ARRAY_IDX(merge_sources, i, merge_source_t *);
+ single_range_conflict_report_t *conflicted_range_report;
+
+ svn_pool_clear(iterpool);
+
+ /* Sanity check: if our left- and right-side merge sources are
+ the same, there's nothing to here. */
+ if ((strcmp(source->loc1->url, source->loc2->url) == 0)
+ && (source->loc1->rev == source->loc2->rev))
+ continue;
+
+ /* Establish RA sessions to our URLs, reuse where possible. */
+ SVN_ERR(ensure_ra_session_url(&ra_session1, source->loc1->url,
+ target->abspath, ctx, scratch_pool));
+ SVN_ERR(ensure_ra_session_url(&ra_session2, source->loc2->url,
+ target->abspath, ctx, scratch_pool));
+
+ /* Populate the portions of the merge context baton that need to
+ be reset for each merge source iteration. */
+ merge_cmd_baton.merge_source = *source;
+ merge_cmd_baton.implicit_src_gap = NULL;
+ merge_cmd_baton.conflicted_paths = NULL;
+ merge_cmd_baton.paths_with_new_mergeinfo = NULL;
+ merge_cmd_baton.paths_with_deleted_mergeinfo = NULL;
+ merge_cmd_baton.ra_session1 = ra_session1;
+ merge_cmd_baton.ra_session2 = ra_session2;
+
+ merge_cmd_baton.notify_begin.last_abspath = NULL;
+
+ /* Populate the portions of the merge context baton that require
+ an RA session to set, but shouldn't be reset for each iteration. */
+ if (! checked_mergeinfo_capability)
+ {
+ SVN_ERR(svn_ra_has_capability(ra_session1,
+ &merge_cmd_baton.mergeinfo_capable,
+ SVN_RA_CAPABILITY_MERGEINFO,
+ iterpool));
+ checked_mergeinfo_capability = TRUE;
+ }
+
+ SVN_ERR(svn_ra_check_path(ra_session1, "", source->loc1->rev,
+ &src1_kind, iterpool));
+
+ /* Run the merge; if there are conflicts, allow the callback to
+ * resolve them, and if it resolves all of them, then run the
+ * merge again with the remaining revision range, until it is all
+ * done. */
+ do
+ {
+ /* Merge as far as possible without resolving any conflicts */
+ if (src1_kind != svn_node_dir)
+ {
+ SVN_ERR(do_file_merge(result_catalog, &conflicted_range_report,
+ source, target->abspath,
+ processor,
+ sources_related,
+ squelch_mergeinfo_notifications,
+ &merge_cmd_baton, iterpool, iterpool));
+ }
+ else /* Directory */
+ {
+ SVN_ERR(do_directory_merge(result_catalog, &conflicted_range_report,
+ source, target->abspath,
+ processor,
+ depth, squelch_mergeinfo_notifications,
+ &merge_cmd_baton, iterpool, iterpool));
+ }
+
+ /* Give the conflict resolver callback the opportunity to
+ * resolve any conflicts that were raised. If it resolves all
+ * of them, go around again to merge the next sub-range (if any). */
+ if (conflicted_range_report && ctx->conflict_func2 && ! dry_run)
+ {
+ svn_boolean_t conflicts_remain;
+
+ SVN_ERR(svn_client__resolve_conflicts(
+ &conflicts_remain, merge_cmd_baton.conflicted_paths,
+ ctx, iterpool));
+ if (conflicts_remain)
+ break;
+
+ merge_cmd_baton.conflicted_paths = NULL;
+ /* Caution: this source is in iterpool */
+ source = conflicted_range_report->remaining_source;
+ conflicted_range_report = NULL;
+ }
+ else
+ break;
+ }
+ while (source);
+
+ /* The final mergeinfo on TARGET_WCPATH may itself elide. */
+ if (! dry_run)
+ SVN_ERR(svn_client__elide_mergeinfo(target->abspath, NULL,
+ ctx, iterpool));
+
+ /* If conflicts occurred while merging any but the very last
+ * range of a multi-pass merge, we raise an error that aborts
+ * the merge. The user will be asked to resolve conflicts
+ * before merging subsequent revision ranges. */
+ if (conflicted_range_report)
+ {
+ *conflict_report = conflict_report_create(
+ target->abspath, conflicted_range_report->conflicted_range,
+ (i == merge_sources->nelts - 1
+ && ! conflicted_range_report->remaining_source),
+ result_pool);
+ break;
+ }
+ }
+
+ if (! *conflict_report || (*conflict_report)->was_last_range)
+ {
+ /* Let everyone know we're finished here. */
+ notify_merge_completed(target->abspath, ctx, iterpool);
+ }
+
+ /* Does the caller want to know what the merge has done? */
+ if (modified_subtrees)
+ {
+ *modified_subtrees =
+ apr_hash_overlay(result_pool, *modified_subtrees,
+ merge_cmd_baton.merged_abspaths);
+ *modified_subtrees =
+ apr_hash_overlay(result_pool, *modified_subtrees,
+ merge_cmd_baton.added_abspaths);
+ *modified_subtrees =
+ apr_hash_overlay(result_pool, *modified_subtrees,
+ merge_cmd_baton.skipped_abspaths);
+ *modified_subtrees =
+ apr_hash_overlay(result_pool, *modified_subtrees,
+ merge_cmd_baton.tree_conflicted_abspaths);
+ }
+
+ if (src_session)
+ SVN_ERR(svn_ra_reparent(src_session, old_src_session_url, iterpool));
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Perform a two-URL merge between URLs which are related, but neither
+ is a direct ancestor of the other. This first does a real two-URL
+ merge (unless this is record-only), followed by record-only merges
+ to represent the changed mergeinfo.
+
+ Set *CONFLICT_REPORT to indicate if there were any conflicts, as in
+ do_merge().
+
+ The diff to be merged is between SOURCE->loc1 (in URL1_RA_SESSION1)
+ and SOURCE->loc2 (in URL2_RA_SESSION2); YCA is their youngest
+ common ancestor.
+
+ SAME_REPOS must be true if and only if the source URLs are in the same
+ repository as the target working copy.
+
+ DIFF_IGNORE_ANCESTRY is as in do_merge().
+
+ Other arguments are as in all of the public merge APIs.
+
+ *USE_SLEEP will be set TRUE if a sleep is required to ensure timestamp
+ integrity, *USE_SLEEP will be unchanged if no sleep is required.
+
+ SCRATCH_POOL is used for all temporary allocations.
+ */
+static svn_error_t *
+merge_cousins_and_supplement_mergeinfo(conflict_report_t **conflict_report,
+ svn_boolean_t *use_sleep,
+ const merge_target_t *target,
+ svn_ra_session_t *URL1_ra_session,
+ svn_ra_session_t *URL2_ra_session,
+ const merge_source_t *source,
+ const svn_client__pathrev_t *yca,
+ svn_boolean_t same_repos,
+ svn_depth_t depth,
+ svn_boolean_t diff_ignore_ancestry,
+ svn_boolean_t force_delete,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *remove_sources, *add_sources;
+ apr_hash_t *modified_subtrees = NULL;
+
+ /* Sure we could use SCRATCH_POOL throughout this function, but since this
+ is a wrapper around three separate merges we'll create a subpool we can
+ clear between each of the three. If the merge target has a lot of
+ subtree mergeinfo, then this will help keep memory use in check. */
+ apr_pool_t *subpool = svn_pool_create(scratch_pool);
+
+ assert(session_url_is(URL1_ra_session, source->loc1->url, scratch_pool));
+ assert(session_url_is(URL2_ra_session, source->loc2->url, scratch_pool));
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(target->abspath));
+ SVN_ERR_ASSERT(! source->ancestral);
+
+ SVN_ERR(normalize_merge_sources_internal(
+ &remove_sources, source->loc1,
+ svn_rangelist__initialize(source->loc1->rev, yca->rev, TRUE,
+ scratch_pool),
+ URL1_ra_session, ctx, scratch_pool, subpool));
+
+ SVN_ERR(normalize_merge_sources_internal(
+ &add_sources, source->loc2,
+ svn_rangelist__initialize(yca->rev, source->loc2->rev, TRUE,
+ scratch_pool),
+ URL2_ra_session, ctx, scratch_pool, subpool));
+
+ *conflict_report = NULL;
+
+ /* If this isn't a record-only merge, we'll first do a stupid
+ point-to-point merge... */
+ if (! record_only)
+ {
+ apr_array_header_t *faux_sources =
+ apr_array_make(scratch_pool, 1, sizeof(merge_source_t *));
+
+ modified_subtrees = apr_hash_make(scratch_pool);
+ APR_ARRAY_PUSH(faux_sources, const merge_source_t *) = source;
+ SVN_ERR(do_merge(&modified_subtrees, NULL, conflict_report, use_sleep,
+ faux_sources, target,
+ URL1_ra_session, TRUE, same_repos,
+ FALSE /*ignore_mergeinfo*/, diff_ignore_ancestry,
+ force_delete, dry_run, FALSE, NULL, TRUE,
+ FALSE, depth, merge_options, ctx,
+ scratch_pool, subpool));
+ if (*conflict_report)
+ {
+ *conflict_report = conflict_report_dup(*conflict_report, result_pool);
+ if (! (*conflict_report)->was_last_range)
+ return SVN_NO_ERROR;
+ }
+ }
+ else if (! same_repos)
+ {
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("Merge from foreign repository is not "
+ "compatible with mergeinfo modification"));
+ }
+
+ /* ... and now, if we're doing the mergeinfo thang, we execute a
+ pair of record-only merges using the real sources we've
+ calculated.
+
+ Issue #3648: We don't actually perform these two record-only merges
+ on the WC at first, but rather see what each would do and store that
+ in two mergeinfo catalogs. We then merge the catalogs together and
+ then record the result in the WC. This prevents the second record
+ only merge from removing legitimate mergeinfo history, from the same
+ source, that was made in prior merges. */
+ if (same_repos && !dry_run)
+ {
+ svn_mergeinfo_catalog_t add_result_catalog =
+ apr_hash_make(scratch_pool);
+ svn_mergeinfo_catalog_t remove_result_catalog =
+ apr_hash_make(scratch_pool);
+
+ notify_mergeinfo_recording(target->abspath, NULL, ctx, scratch_pool);
+ svn_pool_clear(subpool);
+ SVN_ERR(do_merge(NULL, add_result_catalog, conflict_report, use_sleep,
+ add_sources, target,
+ URL1_ra_session, TRUE, same_repos,
+ FALSE /*ignore_mergeinfo*/, diff_ignore_ancestry,
+ force_delete, dry_run, TRUE,
+ modified_subtrees, TRUE,
+ TRUE, depth, merge_options, ctx,
+ scratch_pool, subpool));
+ if (*conflict_report)
+ {
+ *conflict_report = conflict_report_dup(*conflict_report, result_pool);
+ if (! (*conflict_report)->was_last_range)
+ return SVN_NO_ERROR;
+ }
+ svn_pool_clear(subpool);
+ SVN_ERR(do_merge(NULL, remove_result_catalog, conflict_report, use_sleep,
+ remove_sources, target,
+ URL1_ra_session, TRUE, same_repos,
+ FALSE /*ignore_mergeinfo*/, diff_ignore_ancestry,
+ force_delete, dry_run, TRUE,
+ modified_subtrees, TRUE,
+ TRUE, depth, merge_options, ctx,
+ scratch_pool, subpool));
+ if (*conflict_report)
+ {
+ *conflict_report = conflict_report_dup(*conflict_report, result_pool);
+ if (! (*conflict_report)->was_last_range)
+ return SVN_NO_ERROR;
+ }
+ SVN_ERR(svn_mergeinfo_catalog_merge(add_result_catalog,
+ remove_result_catalog,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_client__record_wc_mergeinfo_catalog(add_result_catalog,
+ ctx, scratch_pool));
+ }
+
+ svn_pool_destroy(subpool);
+ return SVN_NO_ERROR;
+}
+
+/* Perform checks to determine whether the working copy at TARGET_ABSPATH
+ * can safely be used as a merge target. Checks are performed according to
+ * the ALLOW_MIXED_REV, ALLOW_LOCAL_MODS, and ALLOW_SWITCHED_SUBTREES
+ * parameters. If any checks fail, raise SVN_ERR_CLIENT_NOT_READY_TO_MERGE.
+ *
+ * E.g. if all the ALLOW_* parameters are FALSE, TARGET_ABSPATH must
+ * be a single-revision, pristine, unswitched working copy.
+ * In other words, it must reflect a subtree of the repository as found
+ * at single revision -- although sparse checkouts are permitted. */
+static svn_error_t *
+ensure_wc_is_suitable_merge_target(const char *target_abspath,
+ svn_client_ctx_t *ctx,
+ svn_boolean_t allow_mixed_rev,
+ svn_boolean_t allow_local_mods,
+ svn_boolean_t allow_switched_subtrees,
+ apr_pool_t *scratch_pool)
+{
+ svn_node_kind_t target_kind;
+
+ /* Check the target exists. */
+ SVN_ERR(svn_io_check_path(target_abspath, &target_kind, scratch_pool));
+ if (target_kind == svn_node_none)
+ return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL,
+ _("Path '%s' does not exist"),
+ svn_dirent_local_style(target_abspath,
+ scratch_pool));
+ SVN_ERR(svn_wc_read_kind2(&target_kind, ctx->wc_ctx, target_abspath,
+ FALSE, FALSE, scratch_pool));
+ if (target_kind != svn_node_dir && target_kind != svn_node_file)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Merge target '%s' does not exist in the "
+ "working copy"), target_abspath);
+
+ /* Perform the mixed-revision check first because it's the cheapest one. */
+ if (! allow_mixed_rev)
+ {
+ svn_revnum_t min_rev;
+ svn_revnum_t max_rev;
+
+ SVN_ERR(svn_client_min_max_revisions(&min_rev, &max_rev, target_abspath,
+ FALSE, ctx, scratch_pool));
+
+ if (!(SVN_IS_VALID_REVNUM(min_rev) && SVN_IS_VALID_REVNUM(max_rev)))
+ {
+ svn_boolean_t is_added;
+
+ /* Allow merge into added nodes. */
+ SVN_ERR(svn_wc__node_is_added(&is_added, ctx->wc_ctx, target_abspath,
+ scratch_pool));
+ if (is_added)
+ return SVN_NO_ERROR;
+ else
+ return svn_error_create(SVN_ERR_CLIENT_NOT_READY_TO_MERGE, NULL,
+ _("Cannot determine revision of working "
+ "copy"));
+ }
+
+ if (min_rev != max_rev)
+ return svn_error_createf(SVN_ERR_CLIENT_MERGE_UPDATE_REQUIRED, NULL,
+ _("Cannot merge into mixed-revision working "
+ "copy [%ld:%ld]; try updating first"),
+ min_rev, max_rev);
+ }
+
+ /* Next, check for switched subtrees. */
+ if (! allow_switched_subtrees)
+ {
+ svn_boolean_t is_switched;
+
+ SVN_ERR(svn_wc__has_switched_subtrees(&is_switched, ctx->wc_ctx,
+ target_abspath, NULL,
+ scratch_pool));
+ if (is_switched)
+ return svn_error_create(SVN_ERR_CLIENT_NOT_READY_TO_MERGE, NULL,
+ _("Cannot merge into a working copy "
+ "with a switched subtree"));
+ }
+
+ /* This is the most expensive check, so it is performed last.*/
+ if (! allow_local_mods)
+ {
+ svn_boolean_t is_modified;
+
+ SVN_ERR(svn_wc__has_local_mods(&is_modified, ctx->wc_ctx,
+ target_abspath,
+ ctx->cancel_func,
+ ctx->cancel_baton,
+ scratch_pool));
+ if (is_modified)
+ return svn_error_create(SVN_ERR_CLIENT_NOT_READY_TO_MERGE, NULL,
+ _("Cannot merge into a working copy "
+ "that has local modifications"));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Throw an error if PATH_OR_URL is a path and REVISION isn't a repository
+ * revision. */
+static svn_error_t *
+ensure_wc_path_has_repo_revision(const char *path_or_url,
+ const svn_opt_revision_t *revision,
+ apr_pool_t *scratch_pool)
+{
+ if (revision->kind != svn_opt_revision_number
+ && revision->kind != svn_opt_revision_date
+ && revision->kind != svn_opt_revision_head
+ && ! svn_path_is_url(path_or_url))
+ return svn_error_createf(
+ SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Invalid merge source '%s'; a working copy path can only be "
+ "used with a repository revision (a number, a date, or head)"),
+ svn_dirent_local_style(path_or_url, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* "Open" the target WC for a merge. That means:
+ * - find out its exact repository location
+ * - check the WC for suitability (throw an error if unsuitable)
+ *
+ * Set *TARGET_P to a new, fully initialized, target description structure.
+ *
+ * ALLOW_MIXED_REV, ALLOW_LOCAL_MODS, ALLOW_SWITCHED_SUBTREES determine
+ * whether the WC is deemed suitable; see ensure_wc_is_suitable_merge_target()
+ * for details.
+ *
+ * If the node is locally added, the rev and URL will be null/invalid. Some
+ * kinds of merge can use such a target; others can't.
+ */
+static svn_error_t *
+open_target_wc(merge_target_t **target_p,
+ const char *wc_abspath,
+ svn_boolean_t allow_mixed_rev,
+ svn_boolean_t allow_local_mods,
+ svn_boolean_t allow_switched_subtrees,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ merge_target_t *target = apr_palloc(result_pool, sizeof(*target));
+ svn_client__pathrev_t *origin;
+
+ target->abspath = apr_pstrdup(result_pool, wc_abspath);
+
+ SVN_ERR(svn_client__wc_node_get_origin(&origin, wc_abspath, ctx,
+ result_pool, scratch_pool));
+ if (origin)
+ {
+ target->loc = *origin;
+ }
+ else
+ {
+ svn_error_t *err;
+ /* The node has no location in the repository. It's unversioned or
+ * locally added or locally deleted.
+ *
+ * If it's locally added or deleted, find the repository root
+ * URL and UUID anyway, and leave the node URL and revision as NULL
+ * and INVALID. If it's unversioned, this will throw an error. */
+ err = svn_wc__node_get_repos_info(NULL, NULL,
+ &target->loc.repos_root_url,
+ &target->loc.repos_uuid,
+ ctx->wc_ctx, wc_abspath,
+ result_pool, scratch_pool);
+
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
+ && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY
+ && err->apr_err != SVN_ERR_WC_UPGRADE_REQUIRED)
+ return svn_error_trace(err);
+
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, err,
+ _("Merge target '%s' does not exist in the "
+ "working copy"),
+ svn_dirent_local_style(wc_abspath,
+ scratch_pool));
+ }
+
+ target->loc.rev = SVN_INVALID_REVNUM;
+ target->loc.url = NULL;
+ }
+
+ SVN_ERR(ensure_wc_is_suitable_merge_target(
+ wc_abspath, ctx,
+ allow_mixed_rev, allow_local_mods, allow_switched_subtrees,
+ scratch_pool));
+
+ *target_p = target;
+ return SVN_NO_ERROR;
+}
+
+/*-----------------------------------------------------------------------*/
+
+/*** Public APIs ***/
+
+/* The body of svn_client_merge5(), which see for details.
+ *
+ * If SOURCE1 @ REVISION1 is related to SOURCE2 @ REVISION2 then use merge
+ * tracking (subject to other constraints -- see HONOR_MERGEINFO());
+ * otherwise disable merge tracking.
+ *
+ * IGNORE_MERGEINFO and DIFF_IGNORE_ANCESTRY are as in do_merge().
+ */
+static svn_error_t *
+merge_locked(conflict_report_t **conflict_report,
+ const char *source1,
+ const svn_opt_revision_t *revision1,
+ const char *source2,
+ const svn_opt_revision_t *revision2,
+ const char *target_abspath,
+ svn_depth_t depth,
+ svn_boolean_t ignore_mergeinfo,
+ svn_boolean_t diff_ignore_ancestry,
+ svn_boolean_t force_delete,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ svn_boolean_t allow_mixed_rev,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ merge_target_t *target;
+ svn_client__pathrev_t *source1_loc, *source2_loc;
+ svn_boolean_t sources_related = FALSE;
+ svn_ra_session_t *ra_session1, *ra_session2;
+ apr_array_header_t *merge_sources;
+ svn_error_t *err;
+ svn_boolean_t use_sleep = FALSE;
+ svn_client__pathrev_t *yca = NULL;
+ apr_pool_t *sesspool;
+ svn_boolean_t same_repos;
+
+ /* ### FIXME: This function really ought to do a history check on
+ the left and right sides of the merge source, and -- if one is an
+ ancestor of the other -- just call svn_client_merge_peg3() with
+ the appropriate args. */
+
+ SVN_ERR(open_target_wc(&target, target_abspath,
+ allow_mixed_rev, TRUE, TRUE,
+ ctx, scratch_pool, scratch_pool));
+
+ /* Open RA sessions to both sides of our merge source, and resolve URLs
+ * and revisions. */
+ sesspool = svn_pool_create(scratch_pool);
+ SVN_ERR(svn_client__ra_session_from_path2(
+ &ra_session1, &source1_loc,
+ source1, NULL, revision1, revision1, ctx, sesspool));
+ SVN_ERR(svn_client__ra_session_from_path2(
+ &ra_session2, &source2_loc,
+ source2, NULL, revision2, revision2, ctx, sesspool));
+
+ /* We can't do a diff between different repositories. */
+ /* ### We should also insist that the root URLs of the two sources match,
+ * as we are only carrying around a single source-repos-root from now
+ * on, and URL calculations will go wrong if they differ.
+ * Alternatively, teach the code to cope with differing root URLs. */
+ SVN_ERR(check_same_repos(source1_loc, source1_loc->url,
+ source2_loc, source2_loc->url,
+ FALSE /* strict_urls */, scratch_pool));
+
+ /* Do our working copy and sources come from the same repository? */
+ same_repos = is_same_repos(&target->loc, source1_loc, TRUE /* strict_urls */);
+
+ /* Unless we're ignoring ancestry, see if the two sources are related. */
+ if (! ignore_mergeinfo)
+ SVN_ERR(svn_client__get_youngest_common_ancestor(
+ &yca, source1_loc, source2_loc, ra_session1, ctx,
+ scratch_pool, scratch_pool));
+
+ /* Check for a youngest common ancestor. If we have one, we'll be
+ doing merge tracking.
+
+ So, given a requested merge of the differences between A and
+ B, and a common ancestor of C, we will find ourselves in one of
+ four positions, and four different approaches:
+
+ A == B == C there's nothing to merge
+
+ A == C != B we merge the changes between A (or C) and B
+
+ B == C != A we merge the changes between B (or C) and A
+
+ A != B != C we merge the changes between A and B without
+ merge recording, then record-only two merges:
+ from A to C, and from C to B
+ */
+ if (yca)
+ {
+ /* Note that our merge sources are related. */
+ sources_related = TRUE;
+
+ /* If the common ancestor matches the right side of our merge,
+ then we only need to reverse-merge the left side. */
+ if ((strcmp(yca->url, source2_loc->url) == 0)
+ && (yca->rev == source2_loc->rev))
+ {
+ SVN_ERR(normalize_merge_sources_internal(
+ &merge_sources, source1_loc,
+ svn_rangelist__initialize(source1_loc->rev, yca->rev, TRUE,
+ scratch_pool),
+ ra_session1, ctx, scratch_pool, scratch_pool));
+ }
+ /* If the common ancestor matches the left side of our merge,
+ then we only need to merge the right side. */
+ else if ((strcmp(yca->url, source1_loc->url) == 0)
+ && (yca->rev == source1_loc->rev))
+ {
+ SVN_ERR(normalize_merge_sources_internal(
+ &merge_sources, source2_loc,
+ svn_rangelist__initialize(yca->rev, source2_loc->rev, TRUE,
+ scratch_pool),
+ ra_session2, ctx, scratch_pool, scratch_pool));
+ }
+ /* And otherwise, we need to do both: reverse merge the left
+ side, and merge the right. */
+ else
+ {
+ merge_source_t source;
+
+ source.loc1 = source1_loc;
+ source.loc2 = source2_loc;
+ source.ancestral = FALSE;
+
+ err = merge_cousins_and_supplement_mergeinfo(conflict_report,
+ &use_sleep,
+ target,
+ ra_session1,
+ ra_session2,
+ &source,
+ yca,
+ same_repos,
+ depth,
+ diff_ignore_ancestry,
+ force_delete,
+ record_only, dry_run,
+ merge_options,
+ ctx,
+ result_pool,
+ scratch_pool);
+ /* Close our temporary RA sessions (this could've happened
+ after the second call to normalize_merge_sources() inside
+ the merge_cousins_and_supplement_mergeinfo() routine). */
+ svn_pool_destroy(sesspool);
+
+ if (use_sleep)
+ svn_io_sleep_for_timestamps(target->abspath, scratch_pool);
+
+ SVN_ERR(err);
+ return SVN_NO_ERROR;
+ }
+ }
+ else
+ {
+ merge_source_t source;
+
+ source.loc1 = source1_loc;
+ source.loc2 = source2_loc;
+ source.ancestral = FALSE;
+
+ /* Build a single-item merge_source_t array. */
+ merge_sources = apr_array_make(scratch_pool, 1, sizeof(merge_source_t *));
+ APR_ARRAY_PUSH(merge_sources, merge_source_t *) = &source;
+ }
+
+ err = do_merge(NULL, NULL, conflict_report, &use_sleep,
+ merge_sources, target,
+ ra_session1, sources_related, same_repos,
+ ignore_mergeinfo, diff_ignore_ancestry, force_delete, dry_run,
+ record_only, NULL, FALSE, FALSE, depth, merge_options,
+ ctx, result_pool, scratch_pool);
+
+ /* Close our temporary RA sessions. */
+ svn_pool_destroy(sesspool);
+
+ if (use_sleep)
+ svn_io_sleep_for_timestamps(target->abspath, scratch_pool);
+
+ SVN_ERR(err);
+ return SVN_NO_ERROR;
+}
+
+/* Set *TARGET_ABSPATH to the absolute path of, and *LOCK_ABSPATH to
+ the absolute path to lock for, TARGET_WCPATH. */
+static svn_error_t *
+get_target_and_lock_abspath(const char **target_abspath,
+ const char **lock_abspath,
+ const char *target_wcpath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool)
+{
+ svn_node_kind_t kind;
+ SVN_ERR(svn_dirent_get_absolute(target_abspath, target_wcpath,
+ result_pool));
+ SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, *target_abspath,
+ FALSE, FALSE, result_pool));
+ if (kind == svn_node_dir)
+ *lock_abspath = *target_abspath;
+ else
+ *lock_abspath = svn_dirent_dirname(*target_abspath, result_pool);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_merge5(const char *source1,
+ const svn_opt_revision_t *revision1,
+ const char *source2,
+ const svn_opt_revision_t *revision2,
+ const char *target_wcpath,
+ svn_depth_t depth,
+ svn_boolean_t ignore_mergeinfo,
+ svn_boolean_t diff_ignore_ancestry,
+ svn_boolean_t force_delete,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ svn_boolean_t allow_mixed_rev,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *target_abspath, *lock_abspath;
+ conflict_report_t *conflict_report;
+
+ /* Sanity check our input -- we require specified revisions,
+ * and either 2 paths or 2 URLs. */
+ if ((revision1->kind == svn_opt_revision_unspecified)
+ || (revision2->kind == svn_opt_revision_unspecified))
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Not all required revisions are specified"));
+ if (svn_path_is_url(source1) != svn_path_is_url(source2))
+ return svn_error_create(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Merge sources must both be "
+ "either paths or URLs"));
+ /* A WC path must be used with a repository revision, as we can't
+ * (currently) use the WC itself as a source, we can only read the URL
+ * from it and use that. */
+ SVN_ERR(ensure_wc_path_has_repo_revision(source1, revision1, pool));
+ SVN_ERR(ensure_wc_path_has_repo_revision(source2, revision2, pool));
+
+ SVN_ERR(get_target_and_lock_abspath(&target_abspath, &lock_abspath,
+ target_wcpath, ctx, pool));
+
+ if (!dry_run)
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ merge_locked(&conflict_report,
+ source1, revision1, source2, revision2,
+ target_abspath, depth, ignore_mergeinfo,
+ diff_ignore_ancestry,
+ force_delete, record_only, dry_run,
+ allow_mixed_rev, merge_options, ctx, pool, pool),
+ ctx->wc_ctx, lock_abspath, FALSE /* lock_anchor */, pool);
+ else
+ SVN_ERR(merge_locked(&conflict_report,
+ source1, revision1, source2, revision2,
+ target_abspath, depth, ignore_mergeinfo,
+ diff_ignore_ancestry,
+ force_delete, record_only, dry_run,
+ allow_mixed_rev, merge_options, ctx, pool, pool));
+
+ SVN_ERR(make_merge_conflict_error(conflict_report, pool));
+ return SVN_NO_ERROR;
+}
+
+
+/* Check if mergeinfo for a given path is described explicitly or via
+ inheritance in a mergeinfo catalog.
+
+ If REPOS_REL_PATH exists in CATALOG and has mergeinfo containing
+ MERGEINFO, then set *IN_CATALOG to TRUE. If REPOS_REL_PATH does
+ not exist in CATALOG, then find its nearest parent which does exist.
+ If the mergeinfo REPOS_REL_PATH would inherit from that parent
+ contains MERGEINFO then set *IN_CATALOG to TRUE. Set *IN_CATALOG
+ to FALSE in all other cases.
+
+ Set *CAT_KEY_PATH to the key path in CATALOG for REPOS_REL_PATH's
+ explicit or inherited mergeinfo. If no explicit or inherited mergeinfo
+ is found for REPOS_REL_PATH then set *CAT_KEY_PATH to NULL.
+
+ User RESULT_POOL to allocate *CAT_KEY_PATH. Use SCRATCH_POOL for
+ temporary allocations. */
+static svn_error_t *
+mergeinfo_in_catalog(svn_boolean_t *in_catalog,
+ const char **cat_key_path,
+ const char *repos_rel_path,
+ svn_mergeinfo_t mergeinfo,
+ svn_mergeinfo_catalog_t catalog,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *walk_path = NULL;
+
+ *in_catalog = FALSE;
+ *cat_key_path = NULL;
+
+ if (mergeinfo && catalog && apr_hash_count(catalog))
+ {
+ const char *path = repos_rel_path;
+
+ /* Start with the assumption there is no explicit or inherited
+ mergeinfo for REPOS_REL_PATH in CATALOG. */
+ svn_mergeinfo_t mergeinfo_in_cat = NULL;
+
+ while (1)
+ {
+ mergeinfo_in_cat = svn_hash_gets(catalog, path);
+
+ if (mergeinfo_in_cat) /* Found it! */
+ {
+ *cat_key_path = apr_pstrdup(result_pool, path);
+ break;
+ }
+ else /* Look for inherited mergeinfo. */
+ {
+ walk_path = svn_relpath_join(svn_relpath_basename(path,
+ scratch_pool),
+ walk_path ? walk_path : "",
+ scratch_pool);
+ path = svn_relpath_dirname(path, scratch_pool);
+
+ if (path[0] == '\0') /* No mergeinfo to inherit. */
+ break;
+ }
+ }
+
+ if (mergeinfo_in_cat)
+ {
+ if (walk_path)
+ SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(&mergeinfo_in_cat,
+ mergeinfo_in_cat,
+ walk_path,
+ scratch_pool,
+ scratch_pool));
+ SVN_ERR(svn_mergeinfo_intersect2(&mergeinfo_in_cat,
+ mergeinfo_in_cat, mergeinfo,
+ TRUE,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_mergeinfo__equals(in_catalog, mergeinfo_in_cat,
+ mergeinfo, TRUE, scratch_pool));
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* A svn_log_entry_receiver_t baton for log_find_operative_revs(). */
+typedef struct log_find_operative_baton_t
+{
+ /* The catalog of explicit mergeinfo on a reintegrate source. */
+ svn_mergeinfo_catalog_t merged_catalog;
+
+ /* The catalog of unmerged history from the reintegrate target to
+ the source which we will create. Allocated in RESULT_POOL. */
+ svn_mergeinfo_catalog_t unmerged_catalog;
+
+ /* The repository absolute path of the reintegrate target. */
+ const char *target_fspath;
+
+ /* The path of the reintegrate source relative to the repository root. */
+ const char *source_repos_rel_path;
+
+ apr_pool_t *result_pool;
+} log_find_operative_baton_t;
+
+/* A svn_log_entry_receiver_t callback for find_unsynced_ranges(). */
+static svn_error_t *
+log_find_operative_revs(void *baton,
+ svn_log_entry_t *log_entry,
+ apr_pool_t *pool)
+{
+ log_find_operative_baton_t *log_baton = baton;
+ apr_hash_index_t *hi;
+ svn_revnum_t revision;
+
+ /* It's possible that authz restrictions on the merge source prevent us
+ from knowing about any of the changes for LOG_ENTRY->REVISION. */
+ if (!log_entry->changed_paths2)
+ return SVN_NO_ERROR;
+
+ revision = log_entry->revision;
+
+ for (hi = apr_hash_first(pool, log_entry->changed_paths2);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *subtree_missing_this_rev;
+ const char *path = svn__apr_hash_index_key(hi);
+ const char *rel_path;
+ const char *source_rel_path;
+ svn_boolean_t in_catalog;
+ svn_mergeinfo_t log_entry_as_mergeinfo;
+
+ rel_path = svn_fspath__skip_ancestor(log_baton->target_fspath, path);
+ /* Easy out: The path is not within the tree of interest. */
+ if (rel_path == NULL)
+ continue;
+
+ source_rel_path = svn_relpath_join(log_baton->source_repos_rel_path,
+ rel_path, pool);
+
+ SVN_ERR(svn_mergeinfo_parse(&log_entry_as_mergeinfo,
+ apr_psprintf(pool, "%s:%ld",
+ path, revision),
+ pool));
+
+ SVN_ERR(mergeinfo_in_catalog(&in_catalog, &subtree_missing_this_rev,
+ source_rel_path, log_entry_as_mergeinfo,
+ log_baton->merged_catalog,
+ pool, pool));
+
+ if (!in_catalog)
+ {
+ svn_mergeinfo_t unmerged_for_key;
+ const char *suffix, *missing_path;
+
+ /* If there is no mergeinfo on the source tree we'll say
+ the "subtree" missing this revision is the root of the
+ source. */
+ if (!subtree_missing_this_rev)
+ subtree_missing_this_rev = log_baton->source_repos_rel_path;
+
+ suffix = svn_relpath_skip_ancestor(subtree_missing_this_rev,
+ source_rel_path);
+ if (suffix)
+ {
+ missing_path = apr_pstrmemdup(pool, path,
+ strlen(path) - strlen(suffix) - 1);
+ }
+ else
+ {
+ missing_path = path;
+ }
+
+ SVN_ERR(svn_mergeinfo_parse(&log_entry_as_mergeinfo,
+ apr_psprintf(pool, "%s:%ld",
+ missing_path, revision),
+ log_baton->result_pool));
+ unmerged_for_key = svn_hash_gets(log_baton->unmerged_catalog,
+ subtree_missing_this_rev);
+
+ if (unmerged_for_key)
+ {
+ SVN_ERR(svn_mergeinfo_merge2(unmerged_for_key,
+ log_entry_as_mergeinfo,
+ log_baton->result_pool,
+ pool));
+ }
+ else
+ {
+ svn_hash_sets(log_baton->unmerged_catalog,
+ apr_pstrdup(log_baton->result_pool,
+ subtree_missing_this_rev),
+ log_entry_as_mergeinfo);
+ }
+
+ }
+ }
+ return SVN_NO_ERROR;
+}
+
+/* Determine if the mergeinfo on a reintegrate source SOURCE_LOC,
+ reflects that the source is fully synced with the reintegrate target
+ TARGET_LOC, even if a naive interpretation of the source's
+ mergeinfo says otherwise -- See issue #3577.
+
+ UNMERGED_CATALOG represents the history (as mergeinfo) from
+ TARGET_LOC that is not represented in SOURCE_LOC's
+ explicit/inherited mergeinfo as represented by MERGED_CATALOG.
+ MERGEINFO_CATALOG may be empty if the source has no explicit or inherited
+ mergeinfo.
+
+ Check that all of the unmerged revisions in UNMERGED_CATALOG's
+ mergeinfos are "phantoms", that is, one of the following conditions holds:
+
+ 1) The revision affects no corresponding paths in SOURCE_LOC.
+
+ 2) The revision affects corresponding paths in SOURCE_LOC,
+ but based on the mergeinfo in MERGED_CATALOG, the change was
+ previously merged.
+
+ Make a deep copy, allocated in RESULT_POOL, of any portions of
+ UNMERGED_CATALOG that are not phantoms, to TRUE_UNMERGED_CATALOG.
+
+ Note: The keys in all mergeinfo catalogs used here are relative to the
+ root of the repository.
+
+ RA_SESSION is an RA session open to the repository of TARGET_LOC; it may
+ be temporarily reparented within this function.
+
+ Use SCRATCH_POOL for all temporary allocations. */
+static svn_error_t *
+find_unsynced_ranges(const svn_client__pathrev_t *source_loc,
+ const svn_client__pathrev_t *target_loc,
+ svn_mergeinfo_catalog_t unmerged_catalog,
+ svn_mergeinfo_catalog_t merged_catalog,
+ svn_mergeinfo_catalog_t true_unmerged_catalog,
+ svn_ra_session_t *ra_session,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_rangelist_t *potentially_unmerged_ranges = NULL;
+
+ /* Convert all the unmerged history to a rangelist. */
+ if (apr_hash_count(unmerged_catalog))
+ {
+ apr_hash_index_t *hi_catalog;
+
+ potentially_unmerged_ranges =
+ apr_array_make(scratch_pool, 1, sizeof(svn_merge_range_t *));
+
+ for (hi_catalog = apr_hash_first(scratch_pool, unmerged_catalog);
+ hi_catalog;
+ hi_catalog = apr_hash_next(hi_catalog))
+ {
+ svn_mergeinfo_t mergeinfo = svn__apr_hash_index_val(hi_catalog);
+
+ SVN_ERR(svn_rangelist__merge_many(potentially_unmerged_ranges,
+ mergeinfo,
+ scratch_pool, scratch_pool));
+ }
+ }
+
+ /* Find any unmerged revisions which both affect the source and
+ are not yet merged to it. */
+ if (potentially_unmerged_ranges)
+ {
+ svn_revnum_t oldest_rev =
+ (APR_ARRAY_IDX(potentially_unmerged_ranges,
+ 0,
+ svn_merge_range_t *))->start + 1;
+ svn_revnum_t youngest_rev =
+ (APR_ARRAY_IDX(potentially_unmerged_ranges,
+ potentially_unmerged_ranges->nelts - 1,
+ svn_merge_range_t *))->end;
+ log_find_operative_baton_t log_baton;
+ const char *old_session_url;
+ svn_error_t *err;
+
+ log_baton.merged_catalog = merged_catalog;
+ log_baton.unmerged_catalog = true_unmerged_catalog;
+ log_baton.source_repos_rel_path
+ = svn_client__pathrev_relpath(source_loc, scratch_pool);
+ log_baton.target_fspath
+ = svn_client__pathrev_fspath(target_loc, scratch_pool);
+ log_baton.result_pool = result_pool;
+
+ SVN_ERR(svn_client__ensure_ra_session_url(
+ &old_session_url, ra_session, target_loc->url, scratch_pool));
+ err = get_log(ra_session, "", youngest_rev, oldest_rev,
+ TRUE, /* discover_changed_paths */
+ log_find_operative_revs, &log_baton,
+ scratch_pool);
+ SVN_ERR(svn_error_compose_create(
+ err, svn_ra_reparent(ra_session, old_session_url, scratch_pool)));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Find the youngest revision that has been merged from target to source.
+ *
+ * If any location in TARGET_HISTORY_AS_MERGEINFO is mentioned in
+ * SOURCE_MERGEINFO, then we know that at least one merge was done from the
+ * target to the source. In that case, set *YOUNGEST_MERGED_REV to the
+ * youngest revision of that intersection (unless *YOUNGEST_MERGED_REV is
+ * already younger than that). Otherwise, leave *YOUNGEST_MERGED_REV alone.
+ */
+static svn_error_t *
+find_youngest_merged_rev(svn_revnum_t *youngest_merged_rev,
+ svn_mergeinfo_t target_history_as_mergeinfo,
+ svn_mergeinfo_t source_mergeinfo,
+ apr_pool_t *scratch_pool)
+{
+ svn_mergeinfo_t explicit_source_target_history_intersection;
+
+ SVN_ERR(svn_mergeinfo_intersect2(
+ &explicit_source_target_history_intersection,
+ source_mergeinfo, target_history_as_mergeinfo, TRUE,
+ scratch_pool, scratch_pool));
+ if (apr_hash_count(explicit_source_target_history_intersection))
+ {
+ svn_revnum_t old_rev, young_rev;
+
+ /* Keep track of the youngest revision merged from target to source. */
+ SVN_ERR(svn_mergeinfo__get_range_endpoints(
+ &young_rev, &old_rev,
+ explicit_source_target_history_intersection, scratch_pool));
+ if (!SVN_IS_VALID_REVNUM(*youngest_merged_rev)
+ || (young_rev > *youngest_merged_rev))
+ *youngest_merged_rev = young_rev;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Set *FILTERED_MERGEINFO_P to the parts of TARGET_HISTORY_AS_MERGEINFO
+ * that are not present in the source branch.
+ *
+ * SOURCE_MERGEINFO is the explicit or inherited mergeinfo of the source
+ * branch SOURCE_PATHREV. Extend SOURCE_MERGEINFO, modifying it in
+ * place, to include the natural history (implicit mergeinfo) of
+ * SOURCE_PATHREV. ### But make these additions in SCRATCH_POOL.
+ *
+ * SOURCE_RA_SESSION is an RA session open to the repository containing
+ * SOURCE_PATHREV; it may be temporarily reparented within this function.
+ *
+ * ### [JAF] This function is named '..._subroutine' simply because I
+ * factored it out based on code similarity, without knowing what it's
+ * purpose is. We should clarify its purpose and choose a better name.
+ */
+static svn_error_t *
+find_unmerged_mergeinfo_subroutine(svn_mergeinfo_t *filtered_mergeinfo_p,
+ svn_mergeinfo_t target_history_as_mergeinfo,
+ svn_mergeinfo_t source_mergeinfo,
+ const svn_client__pathrev_t *source_pathrev,
+ svn_ra_session_t *source_ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_mergeinfo_t source_history_as_mergeinfo;
+
+ /* Get the source path's natural history and merge it into source
+ path's explicit or inherited mergeinfo. */
+ SVN_ERR(svn_client__get_history_as_mergeinfo(
+ &source_history_as_mergeinfo, NULL /* has_rev_zero_history */,
+ source_pathrev, source_pathrev->rev, SVN_INVALID_REVNUM,
+ source_ra_session, ctx, scratch_pool));
+ SVN_ERR(svn_mergeinfo_merge2(source_mergeinfo,
+ source_history_as_mergeinfo,
+ scratch_pool, scratch_pool));
+
+ /* Now source_mergeinfo represents everything we know about
+ source_path's history. Now we need to know what part, if any, of the
+ corresponding target's history is *not* part of source_path's total
+ history; because it is neither shared history nor was it ever merged
+ from the target to the source. */
+ SVN_ERR(svn_mergeinfo_remove2(filtered_mergeinfo_p,
+ source_mergeinfo,
+ target_history_as_mergeinfo, TRUE,
+ result_pool, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* Helper for calculate_left_hand_side() which produces a mergeinfo catalog
+ describing what parts of of the reintegrate target have not previously been
+ merged to the reintegrate source.
+
+ SOURCE_CATALOG is the collection of explicit mergeinfo on SOURCE_LOC and
+ all its children, i.e. the mergeinfo catalog for the reintegrate source.
+
+ TARGET_HISTORY_HASH is a hash of (const char *) paths mapped to
+ svn_mergeinfo_t representing the location history. Each of these
+ path keys represent a path in the reintegrate target, relative to the
+ repository root, which has explicit mergeinfo and/or is the reintegrate
+ target itself. The svn_mergeinfo_t's contain the natural history of each
+ path@TARGET_REV. Effectively this is the mergeinfo catalog on the
+ reintegrate target.
+
+ YC_ANCESTOR_REV is the revision of the youngest common ancestor of the
+ reintegrate source and the reintegrate target.
+
+ SOURCE_LOC is the reintegrate source.
+
+ SOURCE_RA_SESSION is a session opened to the URL of SOURCE_LOC
+ and TARGET_RA_SESSION is open to TARGET->loc.url.
+
+ For each entry in TARGET_HISTORY_HASH check that the history it
+ represents is contained in either the explicit mergeinfo for the
+ corresponding path in SOURCE_CATALOG, the corresponding path's inherited
+ mergeinfo (if no explicit mergeinfo for the path is found in
+ SOURCE_CATALOG), or the corresponding path's natural history. Populate
+ *UNMERGED_TO_SOURCE_CATALOG with the corresponding source paths mapped to
+ the mergeinfo from the target's natural history which is *not* found. Also
+ include any mergeinfo from SOURCE_CATALOG which explicitly describes the
+ target's history but for which *no* entry was found in
+ TARGET_HISTORY_HASH.
+
+ If no part of TARGET_HISTORY_HASH is found in SOURCE_CATALOG set
+ *YOUNGEST_MERGED_REV to SVN_INVALID_REVNUM; otherwise set it to the youngest
+ revision previously merged from the target to the source, and filter
+ *UNMERGED_TO_SOURCE_CATALOG so that it contains no ranges greater than
+ *YOUNGEST_MERGED_REV.
+
+ *UNMERGED_TO_SOURCE_CATALOG is (deeply) allocated in RESULT_POOL.
+ SCRATCH_POOL is used for all temporary allocations. */
+static svn_error_t *
+find_unmerged_mergeinfo(svn_mergeinfo_catalog_t *unmerged_to_source_catalog,
+ svn_revnum_t *youngest_merged_rev,
+ svn_revnum_t yc_ancestor_rev,
+ svn_mergeinfo_catalog_t source_catalog,
+ apr_hash_t *target_history_hash,
+ const svn_client__pathrev_t *source_loc,
+ const merge_target_t *target,
+ svn_ra_session_t *source_ra_session,
+ svn_ra_session_t *target_ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *source_repos_rel_path
+ = svn_client__pathrev_relpath(source_loc, scratch_pool);
+ const char *target_repos_rel_path
+ = svn_client__pathrev_relpath(&target->loc, scratch_pool);
+ apr_hash_index_t *hi;
+ svn_mergeinfo_catalog_t new_catalog = apr_hash_make(result_pool);
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ assert(session_url_is(source_ra_session, source_loc->url, scratch_pool));
+ assert(session_url_is(target_ra_session, target->loc.url, scratch_pool));
+
+ *youngest_merged_rev = SVN_INVALID_REVNUM;
+
+ /* Examine the natural history of each path in the reintegrate target
+ with explicit mergeinfo. */
+ for (hi = apr_hash_first(scratch_pool, target_history_hash);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *target_path = svn__apr_hash_index_key(hi);
+ svn_mergeinfo_t target_history_as_mergeinfo = svn__apr_hash_index_val(hi);
+ const char *path_rel_to_session
+ = svn_relpath_skip_ancestor(target_repos_rel_path, target_path);
+ const char *source_path;
+ svn_client__pathrev_t *source_pathrev;
+ svn_mergeinfo_t source_mergeinfo, filtered_mergeinfo;
+
+ svn_pool_clear(iterpool);
+
+ source_path = svn_relpath_join(source_repos_rel_path,
+ path_rel_to_session, iterpool);
+ source_pathrev = svn_client__pathrev_join_relpath(
+ source_loc, path_rel_to_session, iterpool);
+
+ /* Remove any target history that is also part of the source's history,
+ i.e. their common ancestry. By definition this has already been
+ "merged" from the target to the source. If the source has explicit
+ self referential mergeinfo it would intersect with the target's
+ history below, making it appear that some merges had been done from
+ the target to the source, when this might not actually be the case. */
+ SVN_ERR(svn_mergeinfo__filter_mergeinfo_by_ranges(
+ &target_history_as_mergeinfo, target_history_as_mergeinfo,
+ source_loc->rev, yc_ancestor_rev, TRUE, iterpool, iterpool));
+
+ /* Look for any explicit mergeinfo on the source path corresponding to
+ the target path. If we find any remove that from SOURCE_CATALOG.
+ When this iteration over TARGET_HISTORY_HASH is complete all that
+ should be left in SOURCE_CATALOG are subtrees that have explicit
+ mergeinfo on the reintegrate source where there is no corresponding
+ explicit mergeinfo on the reintegrate target. */
+ source_mergeinfo = svn_hash_gets(source_catalog, source_path);
+ if (source_mergeinfo)
+ {
+ svn_hash_sets(source_catalog, source_path, NULL);
+
+ SVN_ERR(find_youngest_merged_rev(youngest_merged_rev,
+ target_history_as_mergeinfo,
+ source_mergeinfo,
+ iterpool));
+ }
+ else
+ {
+ /* There is no mergeinfo on source_path *or* source_path doesn't
+ exist at all. If simply doesn't exist we can ignore it
+ altogether. */
+ svn_node_kind_t kind;
+
+ SVN_ERR(svn_ra_check_path(source_ra_session,
+ path_rel_to_session,
+ source_loc->rev, &kind, iterpool));
+ if (kind == svn_node_none)
+ continue;
+ /* Else source_path does exist though it has no explicit mergeinfo.
+ Find its inherited mergeinfo. If it doesn't have any then simply
+ set source_mergeinfo to an empty hash. */
+ SVN_ERR(svn_client__get_repos_mergeinfo(
+ &source_mergeinfo, source_ra_session,
+ source_pathrev->url, source_pathrev->rev,
+ svn_mergeinfo_inherited, FALSE /*squelch_incapable*/,
+ iterpool));
+ if (!source_mergeinfo)
+ source_mergeinfo = apr_hash_make(iterpool);
+ }
+
+ /* Use scratch_pool rather than iterpool because filtered_mergeinfo
+ is going into new_catalog below and needs to last to the end of
+ this function. */
+ SVN_ERR(find_unmerged_mergeinfo_subroutine(
+ &filtered_mergeinfo, target_history_as_mergeinfo,
+ source_mergeinfo, source_pathrev,
+ source_ra_session, ctx, scratch_pool, iterpool));
+ svn_hash_sets(new_catalog, apr_pstrdup(scratch_pool, source_path),
+ filtered_mergeinfo);
+ }
+
+ /* Are there any subtrees with explicit mergeinfo still left in the merge
+ source where there was no explicit mergeinfo for the corresponding path
+ in the merge target? If so, add the intersection of those path's
+ mergeinfo and the corresponding target path's mergeinfo to
+ new_catalog. */
+ for (hi = apr_hash_first(scratch_pool, source_catalog);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *source_path = svn__apr_hash_index_key(hi);
+ const char *path_rel_to_session =
+ svn_relpath_skip_ancestor(source_repos_rel_path, source_path);
+ const char *source_url;
+ svn_mergeinfo_t source_mergeinfo = svn__apr_hash_index_val(hi);
+ svn_mergeinfo_t filtered_mergeinfo;
+ svn_client__pathrev_t *target_pathrev;
+ svn_mergeinfo_t target_history_as_mergeinfo;
+ svn_error_t *err;
+
+ svn_pool_clear(iterpool);
+
+ source_url = svn_path_url_add_component2(source_loc->url,
+ path_rel_to_session, iterpool);
+ target_pathrev = svn_client__pathrev_join_relpath(
+ &target->loc, path_rel_to_session, iterpool);
+ err = svn_client__get_history_as_mergeinfo(&target_history_as_mergeinfo,
+ NULL /* has_rev_zero_history */,
+ target_pathrev,
+ target->loc.rev,
+ SVN_INVALID_REVNUM,
+ target_ra_session,
+ ctx, iterpool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_FS_NOT_FOUND
+ || err->apr_err == SVN_ERR_RA_DAV_REQUEST_FAILED)
+ {
+ /* This path with explicit mergeinfo in the source doesn't
+ exist on the target. */
+ svn_error_clear(err);
+ err = NULL;
+ }
+ else
+ {
+ return svn_error_trace(err);
+ }
+ }
+ else
+ {
+ svn_client__pathrev_t *pathrev;
+
+ SVN_ERR(find_youngest_merged_rev(youngest_merged_rev,
+ target_history_as_mergeinfo,
+ source_mergeinfo,
+ iterpool));
+
+ /* Use scratch_pool rather than iterpool because filtered_mergeinfo
+ is going into new_catalog below and needs to last to the end of
+ this function. */
+ /* ### Why looking at SOURCE_url at TARGET_rev? */
+ SVN_ERR(svn_client__pathrev_create_with_session(
+ &pathrev, source_ra_session, target->loc.rev, source_url,
+ iterpool));
+ SVN_ERR(find_unmerged_mergeinfo_subroutine(
+ &filtered_mergeinfo, target_history_as_mergeinfo,
+ source_mergeinfo, pathrev,
+ source_ra_session, ctx, scratch_pool, iterpool));
+ if (apr_hash_count(filtered_mergeinfo))
+ svn_hash_sets(new_catalog,
+ apr_pstrdup(scratch_pool, source_path),
+ filtered_mergeinfo);
+ }
+ }
+
+ /* Limit new_catalog to the youngest revisions previously merged from
+ the target to the source. */
+ if (SVN_IS_VALID_REVNUM(*youngest_merged_rev))
+ SVN_ERR(svn_mergeinfo__filter_catalog_by_ranges(&new_catalog,
+ new_catalog,
+ *youngest_merged_rev,
+ 0, /* No oldest bound. */
+ TRUE,
+ scratch_pool,
+ scratch_pool));
+
+ /* Make a shiny new copy before blowing away all the temporary pools. */
+ *unmerged_to_source_catalog = svn_mergeinfo_catalog_dup(new_catalog,
+ result_pool);
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Helper for svn_client_merge_reintegrate() which calculates the
+ 'left hand side' of the underlying two-URL merge that a --reintegrate
+ merge actually performs. If no merge should be performed, set
+ *LEFT_P to NULL.
+
+ TARGET->abspath is the absolute working copy path of the reintegrate
+ merge.
+
+ SOURCE_LOC is the reintegrate source.
+
+ SUBTREES_WITH_MERGEINFO is a hash of (const char *) absolute paths mapped
+ to (svn_mergeinfo_t *) mergeinfo values for each working copy path with
+ explicit mergeinfo in TARGET->abspath. Actually we only need to know the
+ paths, not the mergeinfo.
+
+ TARGET->loc.rev is the working revision the entire WC tree rooted at
+ TARGET is at.
+
+ Populate *UNMERGED_TO_SOURCE_CATALOG with the mergeinfo describing what
+ parts of TARGET->loc have not been merged to SOURCE_LOC, up to the
+ youngest revision ever merged from the TARGET->abspath to the source if
+ such exists, see doc string for find_unmerged_mergeinfo().
+
+ SOURCE_RA_SESSION is a session opened to the SOURCE_LOC
+ and TARGET_RA_SESSION is open to TARGET->loc.url.
+
+ *LEFT_P, *MERGED_TO_SOURCE_CATALOG , and *UNMERGED_TO_SOURCE_CATALOG are
+ allocated in RESULT_POOL. SCRATCH_POOL is used for all temporary
+ allocations. */
+static svn_error_t *
+calculate_left_hand_side(svn_client__pathrev_t **left_p,
+ svn_mergeinfo_catalog_t *merged_to_source_catalog,
+ svn_mergeinfo_catalog_t *unmerged_to_source_catalog,
+ const merge_target_t *target,
+ apr_hash_t *subtrees_with_mergeinfo,
+ const svn_client__pathrev_t *source_loc,
+ svn_ra_session_t *source_ra_session,
+ svn_ra_session_t *target_ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_mergeinfo_catalog_t mergeinfo_catalog, unmerged_catalog;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_hash_index_t *hi;
+ /* hash of paths mapped to arrays of svn_mergeinfo_t. */
+ apr_hash_t *target_history_hash = apr_hash_make(scratch_pool);
+ svn_revnum_t youngest_merged_rev;
+ svn_client__pathrev_t *yc_ancestor;
+
+ assert(session_url_is(source_ra_session, source_loc->url, scratch_pool));
+ assert(session_url_is(target_ra_session, target->loc.url, scratch_pool));
+
+ /* Initialize our return variables. */
+ *left_p = NULL;
+
+ /* TARGET->abspath may not have explicit mergeinfo and thus may not be
+ contained within SUBTREES_WITH_MERGEINFO. If this is the case then
+ add a dummy item for TARGET->abspath so we get its history (i.e. implicit
+ mergeinfo) below. */
+ if (!svn_hash_gets(subtrees_with_mergeinfo, target->abspath))
+ svn_hash_sets(subtrees_with_mergeinfo, target->abspath,
+ apr_hash_make(result_pool));
+
+ /* Get the history segments (as mergeinfo) for TARGET->abspath and any of
+ its subtrees with explicit mergeinfo. */
+ for (hi = apr_hash_first(scratch_pool, subtrees_with_mergeinfo);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *local_abspath = svn__apr_hash_index_key(hi);
+ svn_client__pathrev_t *target_child;
+ const char *repos_relpath;
+ svn_mergeinfo_t target_history_as_mergeinfo;
+
+ svn_pool_clear(iterpool);
+
+ /* Convert the absolute path with mergeinfo on it to a path relative
+ to the session root. */
+ SVN_ERR(svn_wc__node_get_repos_info(NULL, &repos_relpath, NULL, NULL,
+ ctx->wc_ctx, local_abspath,
+ scratch_pool, iterpool));
+ target_child = svn_client__pathrev_create_with_relpath(
+ target->loc.repos_root_url, target->loc.repos_uuid,
+ target->loc.rev, repos_relpath, iterpool);
+ SVN_ERR(svn_client__get_history_as_mergeinfo(&target_history_as_mergeinfo,
+ NULL /* has_rev_zero_hist */,
+ target_child,
+ target->loc.rev,
+ SVN_INVALID_REVNUM,
+ target_ra_session,
+ ctx, scratch_pool));
+
+ svn_hash_sets(target_history_hash, repos_relpath,
+ target_history_as_mergeinfo);
+ }
+
+ /* Check that SOURCE_LOC and TARGET->loc are
+ actually related, we can't reintegrate if they are not. Also
+ get an initial value for the YCA revision number. */
+ SVN_ERR(svn_client__get_youngest_common_ancestor(
+ &yc_ancestor, source_loc, &target->loc, target_ra_session, ctx,
+ iterpool, iterpool));
+ if (! yc_ancestor)
+ return svn_error_createf(SVN_ERR_CLIENT_NOT_READY_TO_MERGE, NULL,
+ _("'%s@%ld' must be ancestrally related to "
+ "'%s@%ld'"), source_loc->url, source_loc->rev,
+ target->loc.url, target->loc.rev);
+
+ /* If the source revision is the same as the youngest common
+ revision, then there can't possibly be any unmerged revisions
+ that we need to apply to target. */
+ if (source_loc->rev == yc_ancestor->rev)
+ {
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+ }
+
+ /* Get the mergeinfo from the source, including its descendants
+ with differing explicit mergeinfo. */
+ SVN_ERR(svn_client__get_repos_mergeinfo_catalog(
+ &mergeinfo_catalog, source_ra_session,
+ source_loc->url, source_loc->rev,
+ svn_mergeinfo_inherited, FALSE /* squelch_incapable */,
+ TRUE /* include_descendants */, iterpool, iterpool));
+
+ if (!mergeinfo_catalog)
+ mergeinfo_catalog = apr_hash_make(iterpool);
+
+ *merged_to_source_catalog = svn_mergeinfo_catalog_dup(mergeinfo_catalog,
+ result_pool);
+
+ /* Filter the source's mergeinfo catalog so that we are left with
+ mergeinfo that describes what has *not* previously been merged from
+ TARGET->loc to SOURCE_LOC. */
+ SVN_ERR(find_unmerged_mergeinfo(&unmerged_catalog,
+ &youngest_merged_rev,
+ yc_ancestor->rev,
+ mergeinfo_catalog,
+ target_history_hash,
+ source_loc,
+ target,
+ source_ra_session,
+ target_ra_session,
+ ctx,
+ iterpool, iterpool));
+
+ /* Simplify unmerged_catalog through elision then make a copy in POOL. */
+ SVN_ERR(svn_client__elide_mergeinfo_catalog(unmerged_catalog,
+ iterpool));
+ *unmerged_to_source_catalog = svn_mergeinfo_catalog_dup(unmerged_catalog,
+ result_pool);
+
+ if (youngest_merged_rev == SVN_INVALID_REVNUM)
+ {
+ /* We never merged to the source. Just return the branch point. */
+ *left_p = svn_client__pathrev_dup(yc_ancestor, result_pool);
+ }
+ else
+ {
+ /* We've previously merged some or all of the target, up to
+ youngest_merged_rev, to the source. Set
+ *LEFT_P to cover the youngest part of this range. */
+ SVN_ERR(svn_client__repos_location(left_p, target_ra_session,
+ &target->loc, youngest_merged_rev,
+ ctx, result_pool, iterpool));
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Determine the URLs and revisions needed to perform a reintegrate merge
+ * from SOURCE_LOC into the working copy at TARGET.
+ *
+ * SOURCE_RA_SESSION and TARGET_RA_SESSION are RA sessions opened to the
+ * URLs of SOURCE_LOC and TARGET->loc respectively.
+ *
+ * Set *SOURCE_P to
+ * the source-left and source-right locations of the required merge. Set
+ * *YC_ANCESTOR_P to the location of the youngest ancestor.
+ * Any of these output pointers may be NULL if not wanted.
+ *
+ * See svn_client_find_reintegrate_merge() for other details.
+ */
+static svn_error_t *
+find_reintegrate_merge(merge_source_t **source_p,
+ svn_client__pathrev_t **yc_ancestor_p,
+ svn_ra_session_t *source_ra_session,
+ const svn_client__pathrev_t *source_loc,
+ svn_ra_session_t *target_ra_session,
+ const merge_target_t *target,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_client__pathrev_t *yc_ancestor;
+ svn_client__pathrev_t *loc1;
+ merge_source_t source;
+ svn_mergeinfo_catalog_t unmerged_to_source_mergeinfo_catalog;
+ svn_mergeinfo_catalog_t merged_to_source_mergeinfo_catalog;
+ svn_error_t *err;
+ apr_hash_t *subtrees_with_mergeinfo;
+
+ assert(session_url_is(source_ra_session, source_loc->url, scratch_pool));
+ assert(session_url_is(target_ra_session, target->loc.url, scratch_pool));
+
+ /* As the WC tree is "pure", use its last-updated-to revision as
+ the default revision for the left side of our merge, since that's
+ what the repository sub-tree is required to be up to date with
+ (with regard to the WC). */
+ /* ### Bogus/obsolete comment? */
+
+ /* Can't reintegrate to or from the root of the repository. */
+ if (strcmp(source_loc->url, source_loc->repos_root_url) == 0
+ || strcmp(target->loc.url, target->loc.repos_root_url) == 0)
+ return svn_error_createf(SVN_ERR_CLIENT_NOT_READY_TO_MERGE, NULL,
+ _("Neither the reintegrate source nor target "
+ "can be the root of the repository"));
+
+ /* Find all the subtrees in TARGET_WCPATH that have explicit mergeinfo. */
+ err = get_wc_explicit_mergeinfo_catalog(&subtrees_with_mergeinfo,
+ target->abspath, svn_depth_infinity,
+ ctx, scratch_pool, scratch_pool);
+ /* Issue #3896: If invalid mergeinfo in the reintegrate target
+ prevents us from proceeding, then raise the best error possible. */
+ if (err && err->apr_err == SVN_ERR_CLIENT_INVALID_MERGEINFO_NO_MERGETRACKING)
+ err = svn_error_quick_wrap(err, _("Reintegrate merge not possible"));
+ SVN_ERR(err);
+
+ SVN_ERR(calculate_left_hand_side(&loc1,
+ &merged_to_source_mergeinfo_catalog,
+ &unmerged_to_source_mergeinfo_catalog,
+ target,
+ subtrees_with_mergeinfo,
+ source_loc,
+ source_ra_session,
+ target_ra_session,
+ ctx,
+ scratch_pool, scratch_pool));
+
+ /* Did calculate_left_hand_side() decide that there was no merge to
+ be performed here? */
+ if (! loc1)
+ {
+ if (source_p)
+ *source_p = NULL;
+ if (yc_ancestor_p)
+ *yc_ancestor_p = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ source.loc1 = loc1;
+ source.loc2 = source_loc;
+
+ /* If the target was moved after the source was branched from it,
+ it is possible that the left URL differs from the target's current
+ URL. If so, then adjust TARGET_RA_SESSION to point to the old URL. */
+ if (strcmp(source.loc1->url, target->loc.url))
+ SVN_ERR(svn_ra_reparent(target_ra_session, source.loc1->url, scratch_pool));
+
+ SVN_ERR(svn_client__get_youngest_common_ancestor(
+ &yc_ancestor, source.loc2, source.loc1, target_ra_session,
+ ctx, scratch_pool, scratch_pool));
+
+ if (! yc_ancestor)
+ return svn_error_createf(SVN_ERR_CLIENT_NOT_READY_TO_MERGE, NULL,
+ _("'%s@%ld' must be ancestrally related to "
+ "'%s@%ld'"),
+ source.loc1->url, source.loc1->rev,
+ source.loc2->url, source.loc2->rev);
+
+ /* The source side of a reintegrate merge is not 'ancestral', except in
+ * the degenerate case where source == YCA. */
+ source.ancestral = (loc1->rev == yc_ancestor->rev);
+
+ if (source.loc1->rev > yc_ancestor->rev)
+ {
+ /* Have we actually merged anything to the source from the
+ target? If so, make sure we've merged a contiguous
+ prefix. */
+ svn_mergeinfo_catalog_t final_unmerged_catalog = apr_hash_make(scratch_pool);
+
+ SVN_ERR(find_unsynced_ranges(source_loc, yc_ancestor,
+ unmerged_to_source_mergeinfo_catalog,
+ merged_to_source_mergeinfo_catalog,
+ final_unmerged_catalog,
+ target_ra_session, scratch_pool,
+ scratch_pool));
+
+ if (apr_hash_count(final_unmerged_catalog))
+ {
+ svn_string_t *source_mergeinfo_cat_string;
+
+ SVN_ERR(svn_mergeinfo__catalog_to_formatted_string(
+ &source_mergeinfo_cat_string,
+ final_unmerged_catalog,
+ " ", " Missing ranges: ", scratch_pool));
+ return svn_error_createf(SVN_ERR_CLIENT_NOT_READY_TO_MERGE,
+ NULL,
+ _("Reintegrate can only be used if "
+ "revisions %ld through %ld were "
+ "previously merged from %s to the "
+ "reintegrate source, but this is "
+ "not the case:\n%s"),
+ yc_ancestor->rev + 1, source.loc2->rev,
+ target->loc.url,
+ source_mergeinfo_cat_string->data);
+ }
+ }
+
+ /* Left side: trunk@youngest-trunk-rev-merged-to-branch-at-specified-peg-rev
+ * Right side: branch@specified-peg-revision */
+ if (source_p)
+ *source_p = merge_source_dup(&source, result_pool);
+
+ if (yc_ancestor_p)
+ *yc_ancestor_p = svn_client__pathrev_dup(yc_ancestor, result_pool);
+ return SVN_NO_ERROR;
+}
+
+/* Resolve the source and target locations and open RA sessions to them, and
+ * perform some checks appropriate for a reintegrate merge.
+ *
+ * Set *SOURCE_RA_SESSION_P and *SOURCE_LOC_P to a new session and the
+ * repository location of SOURCE_PATH_OR_URL at SOURCE_PEG_REVISION. Set
+ * *TARGET_RA_SESSION_P and *TARGET_P to a new session and the repository
+ * location of the WC at TARGET_ABSPATH.
+ *
+ * Throw a SVN_ERR_CLIENT_UNRELATED_RESOURCES error if the target WC node is
+ * a locally added node or if the source and target are not in the same
+ * repository. Throw a SVN_ERR_CLIENT_NOT_READY_TO_MERGE error if the
+ * target WC is not at a single revision without switched subtrees and
+ * without local mods.
+ *
+ * Allocate all the outputs in RESULT_POOL.
+ */
+static svn_error_t *
+open_reintegrate_source_and_target(svn_ra_session_t **source_ra_session_p,
+ svn_client__pathrev_t **source_loc_p,
+ svn_ra_session_t **target_ra_session_p,
+ merge_target_t **target_p,
+ const char *source_path_or_url,
+ const svn_opt_revision_t *source_peg_revision,
+ const char *target_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_client__pathrev_t *source_loc;
+ merge_target_t *target;
+
+ /* Open the target WC. A reintegrate merge requires the merge target to
+ * reflect a subtree of the repository as found at a single revision. */
+ SVN_ERR(open_target_wc(&target, target_abspath,
+ FALSE, FALSE, FALSE,
+ ctx, scratch_pool, scratch_pool));
+ SVN_ERR(svn_client_open_ra_session2(target_ra_session_p,
+ target->loc.url, target->abspath,
+ ctx, result_pool, scratch_pool));
+ if (! target->loc.url)
+ return svn_error_createf(SVN_ERR_CLIENT_UNRELATED_RESOURCES, NULL,
+ _("Can't reintegrate into '%s' because it is "
+ "locally added and therefore not related to "
+ "the merge source"),
+ svn_dirent_local_style(target->abspath,
+ scratch_pool));
+
+ SVN_ERR(svn_client__ra_session_from_path2(
+ source_ra_session_p, &source_loc,
+ source_path_or_url, NULL, source_peg_revision, source_peg_revision,
+ ctx, result_pool));
+
+ /* source_loc and target->loc are required to be in the same repository,
+ as mergeinfo doesn't come into play for cross-repository merging. */
+ SVN_ERR(check_same_repos(source_loc,
+ svn_dirent_local_style(source_path_or_url,
+ scratch_pool),
+ &target->loc,
+ svn_dirent_local_style(target->abspath,
+ scratch_pool),
+ TRUE /* strict_urls */, scratch_pool));
+
+ *source_loc_p = source_loc;
+ *target_p = target;
+ return SVN_NO_ERROR;
+}
+
+/* The body of svn_client_merge_reintegrate(), which see for details. */
+static svn_error_t *
+merge_reintegrate_locked(conflict_report_t **conflict_report,
+ const char *source_path_or_url,
+ const svn_opt_revision_t *source_peg_revision,
+ const char *target_abspath,
+ svn_boolean_t diff_ignore_ancestry,
+ svn_boolean_t dry_run,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_ra_session_t *target_ra_session, *source_ra_session;
+ merge_target_t *target;
+ svn_client__pathrev_t *source_loc;
+ merge_source_t *source;
+ svn_client__pathrev_t *yc_ancestor;
+ svn_boolean_t use_sleep = FALSE;
+ svn_error_t *err;
+
+ SVN_ERR(open_reintegrate_source_and_target(
+ &source_ra_session, &source_loc, &target_ra_session, &target,
+ source_path_or_url, source_peg_revision, target_abspath,
+ ctx, scratch_pool, scratch_pool));
+
+ SVN_ERR(find_reintegrate_merge(&source, &yc_ancestor,
+ source_ra_session, source_loc,
+ target_ra_session, target,
+ ctx, scratch_pool, scratch_pool));
+
+ if (! source)
+ {
+ return SVN_NO_ERROR;
+ }
+
+ /* Do the real merge! */
+ /* ### TODO(reint): Make sure that one isn't the same line ancestor
+ ### of the other (what's erroneously referred to as "ancestrally
+ ### related" in this source file). For now, we just say the source
+ ### isn't "ancestral" even if it is (in the degenerate case where
+ ### source-left equals YCA). */
+ source->ancestral = FALSE;
+ err = merge_cousins_and_supplement_mergeinfo(conflict_report,
+ &use_sleep,
+ target,
+ target_ra_session,
+ source_ra_session,
+ source, yc_ancestor,
+ TRUE /* same_repos */,
+ svn_depth_infinity,
+ diff_ignore_ancestry,
+ FALSE /* force_delete */,
+ FALSE /* record_only */,
+ dry_run,
+ merge_options,
+ ctx,
+ result_pool, scratch_pool);
+
+ if (use_sleep)
+ svn_io_sleep_for_timestamps(target_abspath, scratch_pool);
+
+ SVN_ERR(err);
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_merge_reintegrate(const char *source_path_or_url,
+ const svn_opt_revision_t *source_peg_revision,
+ const char *target_wcpath,
+ svn_boolean_t dry_run,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *target_abspath, *lock_abspath;
+ conflict_report_t *conflict_report;
+
+ SVN_ERR(get_target_and_lock_abspath(&target_abspath, &lock_abspath,
+ target_wcpath, ctx, pool));
+
+ if (!dry_run)
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ merge_reintegrate_locked(&conflict_report,
+ source_path_or_url, source_peg_revision,
+ target_abspath,
+ FALSE /*diff_ignore_ancestry*/,
+ dry_run, merge_options, ctx, pool, pool),
+ ctx->wc_ctx, lock_abspath, FALSE /* lock_anchor */, pool);
+ else
+ SVN_ERR(merge_reintegrate_locked(&conflict_report,
+ source_path_or_url, source_peg_revision,
+ target_abspath,
+ FALSE /*diff_ignore_ancestry*/,
+ dry_run, merge_options, ctx, pool, pool));
+
+ SVN_ERR(make_merge_conflict_error(conflict_report, pool));
+ return SVN_NO_ERROR;
+}
+
+
+/* The body of svn_client_merge_peg5(), which see for details.
+ *
+ * IGNORE_MERGEINFO and DIFF_IGNORE_ANCESTRY are as in do_merge().
+ */
+static svn_error_t *
+merge_peg_locked(conflict_report_t **conflict_report,
+ const char *source_path_or_url,
+ const svn_opt_revision_t *source_peg_revision,
+ const svn_rangelist_t *ranges_to_merge,
+ const char *target_abspath,
+ svn_depth_t depth,
+ svn_boolean_t ignore_mergeinfo,
+ svn_boolean_t diff_ignore_ancestry,
+ svn_boolean_t force_delete,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ svn_boolean_t allow_mixed_rev,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ merge_target_t *target;
+ svn_client__pathrev_t *source_loc;
+ apr_array_header_t *merge_sources;
+ svn_ra_session_t *ra_session;
+ apr_pool_t *sesspool;
+ svn_boolean_t use_sleep = FALSE;
+ svn_error_t *err;
+ svn_boolean_t same_repos;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(target_abspath));
+
+ SVN_ERR(open_target_wc(&target, target_abspath,
+ allow_mixed_rev, TRUE, TRUE,
+ ctx, scratch_pool, scratch_pool));
+
+ /* Create a short lived session pool */
+ sesspool = svn_pool_create(scratch_pool);
+
+ /* Open an RA session to our source URL, and determine its root URL. */
+ SVN_ERR(svn_client__ra_session_from_path2(
+ &ra_session, &source_loc,
+ source_path_or_url, NULL, source_peg_revision, source_peg_revision,
+ ctx, sesspool));
+
+ /* Normalize our merge sources. */
+ SVN_ERR(normalize_merge_sources(&merge_sources, source_path_or_url,
+ source_loc,
+ ranges_to_merge, ra_session, ctx,
+ scratch_pool, scratch_pool));
+
+ /* Check for same_repos. */
+ same_repos = is_same_repos(&target->loc, source_loc, TRUE /* strict_urls */);
+
+ /* Do the real merge! (We say with confidence that our merge
+ sources are both ancestral and related.) */
+ err = do_merge(NULL, NULL, conflict_report, &use_sleep,
+ merge_sources, target, ra_session,
+ TRUE /*sources_related*/, same_repos, ignore_mergeinfo,
+ diff_ignore_ancestry, force_delete, dry_run,
+ record_only, NULL, FALSE, FALSE, depth, merge_options,
+ ctx, result_pool, scratch_pool);
+
+ /* We're done with our RA session. */
+ svn_pool_destroy(sesspool);
+
+ if (use_sleep)
+ svn_io_sleep_for_timestamps(target_abspath, scratch_pool);
+
+ SVN_ERR(err);
+ return SVN_NO_ERROR;
+}
+
+/* Details of an automatic merge. */
+typedef struct automatic_merge_t
+{
+ svn_client__pathrev_t *yca, *base, *right, *target;
+ svn_boolean_t is_reintegrate_like;
+ svn_boolean_t allow_mixed_rev, allow_local_mods, allow_switched_subtrees;
+} automatic_merge_t;
+
+static svn_error_t *
+client_find_automatic_merge(automatic_merge_t **merge_p,
+ const char *source_path_or_url,
+ const svn_opt_revision_t *source_revision,
+ const char *target_abspath,
+ svn_boolean_t allow_mixed_rev,
+ svn_boolean_t allow_local_mods,
+ svn_boolean_t allow_switched_subtrees,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+static svn_error_t *
+do_automatic_merge_locked(conflict_report_t **conflict_report,
+ const automatic_merge_t *merge,
+ const char *target_abspath,
+ svn_depth_t depth,
+ svn_boolean_t diff_ignore_ancestry,
+ svn_boolean_t force_delete,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+svn_error_t *
+svn_client_merge_peg5(const char *source_path_or_url,
+ const apr_array_header_t *ranges_to_merge,
+ const svn_opt_revision_t *source_peg_revision,
+ const char *target_wcpath,
+ svn_depth_t depth,
+ svn_boolean_t ignore_mergeinfo,
+ svn_boolean_t diff_ignore_ancestry,
+ svn_boolean_t force_delete,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ svn_boolean_t allow_mixed_rev,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *target_abspath, *lock_abspath;
+ conflict_report_t *conflict_report;
+
+ /* No ranges to merge? No problem. */
+ if (ranges_to_merge != NULL && ranges_to_merge->nelts == 0)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(get_target_and_lock_abspath(&target_abspath, &lock_abspath,
+ target_wcpath, ctx, pool));
+
+ /* Do an automatic merge if no revision ranges are specified. */
+ if (ranges_to_merge == NULL)
+ {
+ automatic_merge_t *merge;
+
+ if (ignore_mergeinfo)
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("Cannot merge automatically while "
+ "ignoring mergeinfo"));
+
+ /* Find the details of the merge needed. */
+ SVN_ERR(client_find_automatic_merge(
+ &merge,
+ source_path_or_url, source_peg_revision,
+ target_abspath,
+ allow_mixed_rev,
+ TRUE /*allow_local_mods*/,
+ TRUE /*allow_switched_subtrees*/,
+ ctx, pool, pool));
+
+ if (!dry_run)
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ do_automatic_merge_locked(&conflict_report,
+ merge,
+ target_abspath, depth,
+ diff_ignore_ancestry,
+ force_delete, record_only, dry_run,
+ merge_options, ctx, pool, pool),
+ ctx->wc_ctx, lock_abspath, FALSE /* lock_anchor */, pool);
+ else
+ SVN_ERR(do_automatic_merge_locked(&conflict_report,
+ merge,
+ target_abspath, depth,
+ diff_ignore_ancestry,
+ force_delete, record_only, dry_run,
+ merge_options, ctx, pool, pool));
+ }
+ else if (!dry_run)
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ merge_peg_locked(&conflict_report,
+ source_path_or_url, source_peg_revision,
+ ranges_to_merge,
+ target_abspath, depth, ignore_mergeinfo,
+ diff_ignore_ancestry,
+ force_delete, record_only, dry_run,
+ allow_mixed_rev, merge_options, ctx, pool, pool),
+ ctx->wc_ctx, lock_abspath, FALSE /* lock_anchor */, pool);
+ else
+ SVN_ERR(merge_peg_locked(&conflict_report,
+ source_path_or_url, source_peg_revision,
+ ranges_to_merge,
+ target_abspath, depth, ignore_mergeinfo,
+ diff_ignore_ancestry,
+ force_delete, record_only, dry_run,
+ allow_mixed_rev, merge_options, ctx, pool, pool));
+
+ SVN_ERR(make_merge_conflict_error(conflict_report, pool));
+ return SVN_NO_ERROR;
+}
+
+
+/* The location-history of a branch.
+ *
+ * This structure holds the set of path-revisions occupied by a branch,
+ * from an externally chosen 'tip' location back to its origin. The
+ * 'tip' location is the youngest location that we are considering on
+ * the branch. */
+typedef struct branch_history_t
+{
+ /* The tip location of the branch. That is, the youngest location that's
+ * in the repository and that we're considering. If we're considering a
+ * target branch right up to an uncommitted WC, then this is the WC base
+ * (pristine) location. */
+ svn_client__pathrev_t *tip;
+ /* The location-segment history, as mergeinfo. */
+ svn_mergeinfo_t history;
+ /* Whether the location-segment history reached as far as (necessarily
+ the root path in) revision 0 -- a fact that can't be represented as
+ mergeinfo. */
+ svn_boolean_t has_r0_history;
+} branch_history_t;
+
+/* Return the location on BRANCH_HISTORY at revision REV, or NULL if none. */
+static svn_client__pathrev_t *
+location_on_branch_at_rev(const branch_history_t *branch_history,
+ svn_revnum_t rev,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(scratch_pool, branch_history->history); hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *fspath = svn__apr_hash_index_key(hi);
+ svn_rangelist_t *rangelist = svn__apr_hash_index_val(hi);
+ int i;
+
+ for (i = 0; i < rangelist->nelts; i++)
+ {
+ svn_merge_range_t *r = APR_ARRAY_IDX(rangelist, i, svn_merge_range_t *);
+ if (r->start < rev && rev <= r->end)
+ {
+ return svn_client__pathrev_create_with_relpath(
+ branch_history->tip->repos_root_url,
+ branch_history->tip->repos_uuid,
+ rev, fspath + 1, result_pool);
+ }
+ }
+ }
+ return NULL;
+}
+
+/* */
+typedef struct source_and_target_t
+{
+ svn_client__pathrev_t *source;
+ svn_ra_session_t *source_ra_session;
+ branch_history_t source_branch;
+
+ merge_target_t *target;
+ svn_ra_session_t *target_ra_session;
+ branch_history_t target_branch;
+
+ /* Repos location of the youngest common ancestor of SOURCE and TARGET. */
+ svn_client__pathrev_t *yca;
+} source_and_target_t;
+
+/* Set *INTERSECTION_P to the intersection of BRANCH_HISTORY with the
+ * revision range OLDEST_REV to YOUNGEST_REV (inclusive).
+ *
+ * If the intersection is empty, the result will be a branch history object
+ * containing an empty (not null) history.
+ *
+ * ### The 'tip' of the result is currently unchanged.
+ */
+static svn_error_t *
+branch_history_intersect_range(branch_history_t **intersection_p,
+ const branch_history_t *branch_history,
+ svn_revnum_t oldest_rev,
+ svn_revnum_t youngest_rev,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ branch_history_t *result = apr_palloc(result_pool, sizeof(*result));
+
+ SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(oldest_rev));
+ SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(youngest_rev));
+ SVN_ERR_ASSERT(oldest_rev >= 1);
+ /* Allow a just-empty range (oldest = youngest + 1) but not an
+ * arbitrary reverse range (such as oldest = youngest + 2). */
+ SVN_ERR_ASSERT(oldest_rev <= youngest_rev + 1);
+
+ if (oldest_rev <= youngest_rev)
+ {
+ SVN_ERR(svn_mergeinfo__filter_mergeinfo_by_ranges(
+ &result->history, branch_history->history,
+ youngest_rev, oldest_rev - 1, TRUE /* include_range */,
+ result_pool, scratch_pool));
+ result->history = svn_mergeinfo_dup(result->history, result_pool);
+ }
+ else
+ {
+ result->history = apr_hash_make(result_pool);
+ }
+ result->has_r0_history = FALSE;
+
+ /* ### TODO: Set RESULT->tip to the tip of the intersection. */
+ result->tip = svn_client__pathrev_dup(branch_history->tip, result_pool);
+
+ *intersection_p = result;
+ return SVN_NO_ERROR;
+}
+
+/* Set *OLDEST_P and *YOUNGEST_P to the oldest and youngest locations
+ * (inclusive) along BRANCH. OLDEST_P and/or YOUNGEST_P may be NULL if not
+ * wanted.
+ */
+static svn_error_t *
+branch_history_get_endpoints(svn_client__pathrev_t **oldest_p,
+ svn_client__pathrev_t **youngest_p,
+ const branch_history_t *branch,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_revnum_t youngest_rev, oldest_rev;
+
+ SVN_ERR(svn_mergeinfo__get_range_endpoints(
+ &youngest_rev, &oldest_rev,
+ branch->history, scratch_pool));
+ if (oldest_p)
+ *oldest_p = location_on_branch_at_rev(
+ branch, oldest_rev + 1, result_pool, scratch_pool);
+ if (youngest_p)
+ *youngest_p = location_on_branch_at_rev(
+ branch, youngest_rev, result_pool, scratch_pool);
+ return SVN_NO_ERROR;
+}
+
+/* Implements the svn_log_entry_receiver_t interface.
+
+ Set *BATON to LOG_ENTRY->revision and return SVN_ERR_CEASE_INVOCATION. */
+static svn_error_t *
+operative_rev_receiver(void *baton,
+ svn_log_entry_t *log_entry,
+ apr_pool_t *pool)
+{
+ svn_revnum_t *operative_rev = baton;
+
+ *operative_rev = log_entry->revision;
+
+ /* We've found the youngest merged or oldest eligible revision, so
+ we're done...
+
+ ...but wait, shouldn't we care if LOG_ENTRY->NON_INHERITABLE is
+ true? Because if it is, then LOG_ENTRY->REVISION is only
+ partially merged/elgibile! And our only caller,
+ find_last_merged_location (via short_circuit_mergeinfo_log) is
+ interested in *fully* merged revisions. That's all true, but if
+ find_last_merged_location() finds the youngest merged revision it
+ will also check for the oldest eligible revision. So in the case
+ the youngest merged rev is non-inheritable, the *same* non-inheritable
+ rev will be found as the oldest eligible rev -- and
+ find_last_merged_location() handles that situation. */
+ return svn_error_create(SVN_ERR_CEASE_INVOCATION, NULL, NULL);
+}
+
+/* Wrapper around svn_client_mergeinfo_log2. All arguments are as per
+ that API. The discover_changed_paths, depth, and revprops args to
+ svn_client_mergeinfo_log2 are always TRUE, svn_depth_infinity_t,
+ and NULL respectively.
+
+ If RECEIVER raises a SVN_ERR_CEASE_INVOCATION error, but still sets
+ *REVISION to a valid revnum, then clear the error. Otherwise return
+ any error. */
+static svn_error_t*
+short_circuit_mergeinfo_log(svn_boolean_t finding_merged,
+ const char *target_path_or_url,
+ const svn_opt_revision_t *target_peg_revision,
+ const char *source_path_or_url,
+ const svn_opt_revision_t *source_peg_revision,
+ const svn_opt_revision_t *source_start_revision,
+ const svn_opt_revision_t *source_end_revision,
+ svn_log_entry_receiver_t receiver,
+ svn_revnum_t *revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err = svn_client_mergeinfo_log2(finding_merged,
+ target_path_or_url,
+ target_peg_revision,
+ source_path_or_url,
+ source_peg_revision,
+ source_start_revision,
+ source_end_revision,
+ receiver, revision,
+ TRUE, svn_depth_infinity,
+ NULL, ctx, scratch_pool);
+
+ if (err)
+ {
+ /* We expect RECEIVER to short-circuit the (potentially expensive) log
+ by raising an SVN_ERR_CEASE_INVOCATION -- see operative_rev_receiver.
+ So we can ignore that error, but only as long as we actually found a
+ valid revision. */
+ if (SVN_IS_VALID_REVNUM(*revision)
+ && err->apr_err == SVN_ERR_CEASE_INVOCATION)
+ {
+ svn_error_clear(err);
+ err = NULL;
+ }
+ else
+ {
+ return svn_error_trace(err);
+ }
+ }
+ return SVN_NO_ERROR;
+}
+
+/* Set *BASE_P to the last location on SOURCE_BRANCH such that all changes
+ * on SOURCE_BRANCH after YCA up to and including *BASE_P have already
+ * been fully merged into TARGET.
+ *
+ * *BASE_P TIP
+ * o-------o-----------o--- SOURCE_BRANCH
+ * / \
+ * -----o prev. \
+ * YCA \ merges \
+ * o-----------o----------- TARGET branch
+ *
+ * In terms of mergeinfo:
+ *
+ * Source a--... o=change, -=no-op revision
+ * branch / \
+ * YCA --> o a---o---o---o---o--- d=delete, a=add-as-a-copy
+ *
+ * Eligible -.eee.eeeeeeeeeeeeeeeeeeee .=not a source branch location
+ *
+ * Tgt-mi -.mmm.mm-mm-------m------- m=merged to root of TARGET or
+ * subtree of TARGET with no
+ * operative changes outside of that
+ * subtree, -=not merged
+ *
+ * Eligible -.---.--e--eeeeeee-eeeeeee
+ *
+ * Next --------^----------------- BASE is just before here.
+ *
+ * / \
+ * -----o prev. \
+ * YCA \ merges \
+ * o-----------o-------------
+ *
+ * If no revisions from SOURCE_BRANCH have been completely merged to TARGET,
+ * then set *BASE_P to the YCA.
+ */
+static svn_error_t *
+find_last_merged_location(svn_client__pathrev_t **base_p,
+ svn_client__pathrev_t *yca,
+ const branch_history_t *source_branch,
+ svn_client__pathrev_t *target,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_opt_revision_t source_peg_rev, source_start_rev, source_end_rev,
+ target_opt_rev;
+ svn_revnum_t youngest_merged_rev = SVN_INVALID_REVNUM;
+
+ source_peg_rev.kind = svn_opt_revision_number;
+ source_peg_rev.value.number = source_branch->tip->rev;
+ source_start_rev.kind = svn_opt_revision_number;
+ source_start_rev.value.number = yca->rev;
+ source_end_rev.kind = svn_opt_revision_number;
+ source_end_rev.value.number = source_branch->tip->rev;
+ target_opt_rev.kind = svn_opt_revision_number;
+ target_opt_rev.value.number = target->rev;
+
+ /* Find the youngest revision fully merged from SOURCE_BRANCH to TARGET,
+ if such a revision exists. */
+ SVN_ERR(short_circuit_mergeinfo_log(TRUE, /* Find merged */
+ target->url, &target_opt_rev,
+ source_branch->tip->url,
+ &source_peg_rev,
+ &source_end_rev, &source_start_rev,
+ operative_rev_receiver,
+ &youngest_merged_rev,
+ ctx, scratch_pool));
+
+ if (!SVN_IS_VALID_REVNUM(youngest_merged_rev))
+ {
+ /* No revisions have been completely merged from SOURCE_BRANCH to
+ TARGET so the base for the next merge is the YCA. */
+ *base_p = yca;
+ }
+ else
+ {
+ /* One or more revisions have already been completely merged from
+ SOURCE_BRANCH to TARGET, now find the oldest revision, older
+ than the youngest merged revision, which is still eligible to
+ be merged, if such exists. */
+ branch_history_t *contiguous_source;
+ svn_revnum_t base_rev;
+ svn_revnum_t oldest_eligible_rev = SVN_INVALID_REVNUM;
+
+ /* If the only revisions eligible are younger than the youngest merged
+ revision we can simply assume that the youngest eligible revision
+ is the youngest merged revision. Obviously this may not be true!
+ The revisions between the youngest merged revision and the tip of
+ the branch may have several inoperative revisions -- they may *all*
+ be inoperative revisions! But for the purpose of this function
+ (i.e. finding the youngest revision after the YCA where all revs have
+ been merged) that doesn't matter. */
+ source_end_rev.value.number = youngest_merged_rev;
+ SVN_ERR(short_circuit_mergeinfo_log(FALSE, /* Find eligible */
+ target->url, &target_opt_rev,
+ source_branch->tip->url,
+ &source_peg_rev,
+ &source_start_rev, &source_end_rev,
+ operative_rev_receiver,
+ &oldest_eligible_rev,
+ ctx, scratch_pool));
+
+ /* If there are revisions eligible for merging, use the oldest one
+ to calculate the base. Otherwise there are no operative revisions
+ to merge and we can simple set the base to the youngest revision
+ already merged. */
+ if (SVN_IS_VALID_REVNUM(oldest_eligible_rev))
+ base_rev = oldest_eligible_rev - 1;
+ else
+ base_rev = youngest_merged_rev;
+
+ /* Find the branch location just before the oldest eligible rev.
+ (We can't just use the base revs calculated above because the branch
+ might have a gap there.) */
+ SVN_ERR(branch_history_intersect_range(&contiguous_source,
+ source_branch, yca->rev,
+ base_rev,
+ scratch_pool, scratch_pool));
+ SVN_ERR(branch_history_get_endpoints(NULL, base_p, contiguous_source,
+ result_pool, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Find a merge base location on the target branch, like in a sync
+ * merge.
+ *
+ * BASE S_T->source
+ * o-------o-----------o---
+ * / \ \
+ * -----o prev. \ \ this
+ * YCA \ merge \ \ merge
+ * o-----------o-----------o
+ * S_T->target
+ *
+ * Set *BASE_P to BASE, the youngest location in the history of S_T->source
+ * (at or after the YCA) at which all revisions up to BASE are effectively
+ * merged into S_T->target.
+ *
+ * If no locations on the history of S_T->source are effectively merged to
+ * S_T->target, set *BASE_P to the YCA.
+ */
+static svn_error_t *
+find_base_on_source(svn_client__pathrev_t **base_p,
+ source_and_target_t *s_t,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ SVN_ERR(find_last_merged_location(base_p,
+ s_t->yca,
+ &s_t->source_branch,
+ s_t->target_branch.tip,
+ ctx, result_pool, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* Find a merge base location on the target branch, like in a reintegrate
+ * merge.
+ *
+ * S_T->source
+ * o-----------o-------o---
+ * / prev. / \
+ * -----o merge / \ this
+ * YCA \ / \ merge
+ * o-------o---------------o
+ * BASE S_T->target
+ *
+ * Set *BASE_P to BASE, the youngest location in the history of S_T->target
+ * (at or after the YCA) at which all revisions up to BASE are effectively
+ * merged into S_T->source.
+ *
+ * If no locations on the history of S_T->target are effectively merged to
+ * S_T->source, set *BASE_P to the YCA.
+ */
+static svn_error_t *
+find_base_on_target(svn_client__pathrev_t **base_p,
+ source_and_target_t *s_t,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ SVN_ERR(find_last_merged_location(base_p,
+ s_t->yca,
+ &s_t->target_branch,
+ s_t->source,
+ ctx, result_pool, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* The body of client_find_automatic_merge(), which see.
+ */
+static svn_error_t *
+find_automatic_merge(svn_client__pathrev_t **base_p,
+ svn_boolean_t *is_reintegrate_like,
+ source_and_target_t *s_t,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_client__pathrev_t *base_on_source, *base_on_target;
+
+ /* Get the location-history of each branch. */
+ s_t->source_branch.tip = s_t->source;
+ SVN_ERR(svn_client__get_history_as_mergeinfo(
+ &s_t->source_branch.history, &s_t->source_branch.has_r0_history,
+ s_t->source, SVN_INVALID_REVNUM, SVN_INVALID_REVNUM,
+ s_t->source_ra_session, ctx, scratch_pool));
+ s_t->target_branch.tip = &s_t->target->loc;
+ SVN_ERR(svn_client__get_history_as_mergeinfo(
+ &s_t->target_branch.history, &s_t->target_branch.has_r0_history,
+ &s_t->target->loc, SVN_INVALID_REVNUM, SVN_INVALID_REVNUM,
+ s_t->target_ra_session, ctx, scratch_pool));
+
+ SVN_ERR(svn_client__get_youngest_common_ancestor(
+ &s_t->yca, s_t->source, &s_t->target->loc, s_t->source_ra_session,
+ ctx, result_pool, result_pool));
+ if (! s_t->yca)
+ return svn_error_createf(SVN_ERR_CLIENT_NOT_READY_TO_MERGE, NULL,
+ _("'%s@%ld' must be ancestrally related to "
+ "'%s@%ld'"),
+ s_t->source->url, s_t->source->rev,
+ s_t->target->loc.url, s_t->target->loc.rev);
+
+ /* Find the latest revision of A synced to B and the latest
+ * revision of B synced to A.
+ *
+ * base_on_source = youngest_complete_synced_point(source, target)
+ * base_on_target = youngest_complete_synced_point(target, source)
+ */
+ SVN_ERR(find_base_on_source(&base_on_source, s_t,
+ ctx, scratch_pool, scratch_pool));
+ SVN_ERR(find_base_on_target(&base_on_target, s_t,
+ ctx, scratch_pool, scratch_pool));
+
+ /* Choose a base. */
+ if (base_on_source->rev >= base_on_target->rev)
+ {
+ *base_p = base_on_source;
+ *is_reintegrate_like = FALSE;
+ }
+ else
+ {
+ *base_p = base_on_target;
+ *is_reintegrate_like = TRUE;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/** Find out what kind of automatic merge would be needed, when the target
+ * is only known as a repository location rather than a WC.
+ *
+ * Like find_automatic_merge() except that the target is
+ * specified by @a target_path_or_url at @a target_revision, which must
+ * refer to a repository location, instead of by a WC path argument.
+ */
+static svn_error_t *
+find_automatic_merge_no_wc(automatic_merge_t **merge_p,
+ const char *source_path_or_url,
+ const svn_opt_revision_t *source_revision,
+ const char *target_path_or_url,
+ const svn_opt_revision_t *target_revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ source_and_target_t *s_t = apr_palloc(scratch_pool, sizeof(*s_t));
+ svn_client__pathrev_t *target_loc;
+ automatic_merge_t *merge = apr_palloc(result_pool, sizeof(*merge));
+
+ /* Source */
+ SVN_ERR(svn_client__ra_session_from_path2(
+ &s_t->source_ra_session, &s_t->source,
+ source_path_or_url, NULL, source_revision, source_revision,
+ ctx, result_pool));
+
+ /* Target */
+ SVN_ERR(svn_client__ra_session_from_path2(
+ &s_t->target_ra_session, &target_loc,
+ target_path_or_url, NULL, target_revision, target_revision,
+ ctx, result_pool));
+ s_t->target = apr_palloc(scratch_pool, sizeof(*s_t->target));
+ s_t->target->abspath = NULL; /* indicate the target is not a WC */
+ s_t->target->loc = *target_loc;
+
+ SVN_ERR(find_automatic_merge(&merge->base, &merge->is_reintegrate_like, s_t,
+ ctx, result_pool, scratch_pool));
+
+ merge->right = s_t->source;
+ merge->target = &s_t->target->loc;
+ merge->yca = s_t->yca;
+ *merge_p = merge;
+
+ return SVN_NO_ERROR;
+}
+
+/* Find the information needed to merge all unmerged changes from a source
+ * branch into a target branch.
+ *
+ * Set @a *merge_p to the information needed to merge all unmerged changes
+ * (up to @a source_revision) from the source branch @a source_path_or_url
+ * at @a source_revision into the target WC at @a target_abspath.
+ *
+ * The flags @a allow_mixed_rev, @a allow_local_mods and
+ * @a allow_switched_subtrees enable merging into a WC that is in any or all
+ * of the states described by their names, but only if this function decides
+ * that the merge will be in the same direction as the last automatic merge.
+ * If, on the other hand, the last automatic merge was in the opposite
+ * direction, then such states of the WC are not allowed regardless
+ * of these flags. This function merely records these flags in the
+ * @a *merge_p structure; do_automatic_merge_locked() checks the WC
+ * state for compliance.
+ *
+ * Allocate the @a *merge_p structure in @a result_pool.
+ */
+static svn_error_t *
+client_find_automatic_merge(automatic_merge_t **merge_p,
+ const char *source_path_or_url,
+ const svn_opt_revision_t *source_revision,
+ const char *target_abspath,
+ svn_boolean_t allow_mixed_rev,
+ svn_boolean_t allow_local_mods,
+ svn_boolean_t allow_switched_subtrees,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ source_and_target_t *s_t = apr_palloc(result_pool, sizeof(*s_t));
+ automatic_merge_t *merge = apr_palloc(result_pool, sizeof(*merge));
+
+ /* "Open" the target WC. Check the target WC for mixed-rev, local mods and
+ * switched subtrees yet to faster exit and notify user before contacting
+ * with server. After we find out what kind of merge is required, then if a
+ * reintegrate-like merge is required we'll do the stricter checks, in
+ * do_automatic_merge_locked(). */
+ SVN_ERR(open_target_wc(&s_t->target, target_abspath,
+ allow_mixed_rev,
+ allow_local_mods,
+ allow_switched_subtrees,
+ ctx, result_pool, scratch_pool));
+
+ /* Open RA sessions to the source and target trees. */
+ SVN_ERR(svn_client_open_ra_session2(&s_t->target_ra_session,
+ s_t->target->loc.url,
+ s_t->target->abspath,
+ ctx, result_pool, scratch_pool));
+ /* ### check for null URL (i.e. added path) here, like in reintegrate? */
+ SVN_ERR(svn_client__ra_session_from_path2(
+ &s_t->source_ra_session, &s_t->source,
+ source_path_or_url, NULL, source_revision, source_revision,
+ ctx, result_pool));
+
+ /* Check source is in same repos as target. */
+ SVN_ERR(check_same_repos(s_t->source, source_path_or_url,
+ &s_t->target->loc, target_abspath,
+ TRUE /* strict_urls */, scratch_pool));
+
+ SVN_ERR(find_automatic_merge(&merge->base, &merge->is_reintegrate_like, s_t,
+ ctx, result_pool, scratch_pool));
+ merge->yca = s_t->yca;
+ merge->right = s_t->source;
+ merge->allow_mixed_rev = allow_mixed_rev;
+ merge->allow_local_mods = allow_local_mods;
+ merge->allow_switched_subtrees = allow_switched_subtrees;
+
+ *merge_p = merge;
+
+ /* TODO: Close the source and target sessions here? */
+
+ return SVN_NO_ERROR;
+}
+
+/* Perform an automatic merge, given the information in MERGE which
+ * must have come from calling client_find_automatic_merge().
+ *
+ * Four locations are inputs: YCA, BASE, RIGHT, TARGET, as shown
+ * depending on whether the base is on the source branch or the target
+ * branch of this merge.
+ *
+ * RIGHT (is_reintegrate_like)
+ * o-----------o-------o---
+ * / prev. / \
+ * -----o merge / \ this
+ * YCA \ / \ merge
+ * o-------o---------------o
+ * BASE TARGET
+ *
+ * or
+ *
+ * BASE RIGHT (! is_reintegrate_like)
+ * o-------o-----------o---
+ * / \ \
+ * -----o prev. \ \ this
+ * YCA \ merge \ \ merge
+ * o-----------o-----------o
+ * TARGET
+ *
+ * ### TODO: The reintegrate-like code path does not yet
+ * eliminate already-cherry-picked revisions from the source.
+ */
+static svn_error_t *
+do_automatic_merge_locked(conflict_report_t **conflict_report,
+ const automatic_merge_t *merge,
+ const char *target_abspath,
+ svn_depth_t depth,
+ svn_boolean_t diff_ignore_ancestry,
+ svn_boolean_t force_delete,
+ svn_boolean_t record_only,
+ svn_boolean_t dry_run,
+ const apr_array_header_t *merge_options,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ merge_target_t *target;
+ svn_boolean_t reintegrate_like = merge->is_reintegrate_like;
+ svn_boolean_t use_sleep = FALSE;
+ svn_error_t *err;
+
+ SVN_ERR(open_target_wc(&target, target_abspath,
+ merge->allow_mixed_rev && ! reintegrate_like,
+ merge->allow_local_mods && ! reintegrate_like,
+ merge->allow_switched_subtrees && ! reintegrate_like,
+ ctx, scratch_pool, scratch_pool));
+
+ if (reintegrate_like)
+ {
+ merge_source_t source;
+ svn_ra_session_t *base_ra_session = NULL;
+ svn_ra_session_t *right_ra_session = NULL;
+ svn_ra_session_t *target_ra_session = NULL;
+
+ if (record_only)
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("The required merge is reintegrate-like, "
+ "and the record-only option "
+ "cannot be used with this kind of merge"));
+
+ if (depth != svn_depth_unknown)
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("The required merge is reintegrate-like, "
+ "and the depth option "
+ "cannot be used with this kind of merge"));
+
+ if (force_delete)
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("The required merge is reintegrate-like, "
+ "and the force_delete option "
+ "cannot be used with this kind of merge"));
+
+ SVN_ERR(ensure_ra_session_url(&base_ra_session, merge->base->url,
+ target->abspath, ctx, scratch_pool));
+ SVN_ERR(ensure_ra_session_url(&right_ra_session, merge->right->url,
+ target->abspath, ctx, scratch_pool));
+ SVN_ERR(ensure_ra_session_url(&target_ra_session, target->loc.url,
+ target->abspath, ctx, scratch_pool));
+
+ /* Check for and reject any abnormalities -- such as revisions that
+ * have not yet been merged in the opposite direction -- that a
+ * 'reintegrate' merge would have rejected. */
+ {
+ merge_source_t *source2;
+
+ SVN_ERR(find_reintegrate_merge(&source2, NULL,
+ right_ra_session, merge->right,
+ target_ra_session, target,
+ ctx, scratch_pool, scratch_pool));
+ }
+
+ source.loc1 = merge->base;
+ source.loc2 = merge->right;
+ source.ancestral = ! merge->is_reintegrate_like;
+
+ err = merge_cousins_and_supplement_mergeinfo(conflict_report,
+ &use_sleep,
+ target,
+ base_ra_session,
+ right_ra_session,
+ &source, merge->yca,
+ TRUE /* same_repos */,
+ depth,
+ FALSE /*diff_ignore_ancestry*/,
+ force_delete, record_only,
+ dry_run,
+ merge_options,
+ ctx,
+ result_pool, scratch_pool);
+ }
+ else /* ! merge->is_reintegrate_like */
+ {
+ /* Ignoring the base that we found, we pass the YCA instead and let
+ do_merge() work out which subtrees need which revision ranges to
+ be merged. This enables do_merge() to fill in revision-range
+ gaps that are older than the base that we calculated (which is
+ for the root path of the merge).
+
+ An improvement would be to change find_automatic_merge() to
+ find the base for each sutree, and then here use the oldest base
+ among all subtrees. */
+ apr_array_header_t *merge_sources;
+ svn_ra_session_t *ra_session = NULL;
+
+ /* Normalize our merge sources, do_merge() requires this. See the
+ 'MERGEINFO MERGE SOURCE NORMALIZATION' global comment. */
+ SVN_ERR(ensure_ra_session_url(&ra_session, merge->right->url,
+ target->abspath, ctx, scratch_pool));
+ SVN_ERR(normalize_merge_sources_internal(
+ &merge_sources, merge->right,
+ svn_rangelist__initialize(merge->yca->rev, merge->right->rev, TRUE,
+ scratch_pool),
+ ra_session, ctx, scratch_pool, scratch_pool));
+
+ err = do_merge(NULL, NULL, conflict_report, &use_sleep,
+ merge_sources, target, ra_session,
+ TRUE /*related*/, TRUE /*same_repos*/,
+ FALSE /*ignore_mergeinfo*/, diff_ignore_ancestry,
+ force_delete, dry_run,
+ record_only, NULL, FALSE, FALSE, depth, merge_options,
+ ctx, result_pool, scratch_pool);
+ }
+
+ if (use_sleep)
+ svn_io_sleep_for_timestamps(target_abspath, scratch_pool);
+
+ SVN_ERR(err);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_get_merging_summary(svn_boolean_t *needs_reintegration,
+ const char **yca_url, svn_revnum_t *yca_rev,
+ const char **base_url, svn_revnum_t *base_rev,
+ const char **right_url, svn_revnum_t *right_rev,
+ const char **target_url, svn_revnum_t *target_rev,
+ const char **repos_root_url,
+ const char *source_path_or_url,
+ const svn_opt_revision_t *source_revision,
+ const char *target_path_or_url,
+ const svn_opt_revision_t *target_revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_boolean_t target_is_wc;
+ automatic_merge_t *merge;
+
+ target_is_wc = (! svn_path_is_url(target_path_or_url))
+ && (target_revision->kind == svn_opt_revision_unspecified
+ || target_revision->kind == svn_opt_revision_working);
+ if (target_is_wc)
+ SVN_ERR(client_find_automatic_merge(
+ &merge,
+ source_path_or_url, source_revision,
+ target_path_or_url,
+ TRUE, TRUE, TRUE, /* allow_* */
+ ctx, scratch_pool, scratch_pool));
+ else
+ SVN_ERR(find_automatic_merge_no_wc(
+ &merge,
+ source_path_or_url, source_revision,
+ target_path_or_url, target_revision,
+ ctx, scratch_pool, scratch_pool));
+
+ if (needs_reintegration)
+ *needs_reintegration = merge->is_reintegrate_like;
+ if (yca_url)
+ *yca_url = apr_pstrdup(result_pool, merge->yca->url);
+ if (yca_rev)
+ *yca_rev = merge->yca->rev;
+ if (base_url)
+ *base_url = apr_pstrdup(result_pool, merge->base->url);
+ if (base_rev)
+ *base_rev = merge->base->rev;
+ if (right_url)
+ *right_url = apr_pstrdup(result_pool, merge->right->url);
+ if (right_rev)
+ *right_rev = merge->right->rev;
+ if (target_url)
+ *target_url = apr_pstrdup(result_pool, merge->target->url);
+ if (target_rev)
+ *target_rev = merge->target->rev;
+ if (repos_root_url)
+ *repos_root_url = apr_pstrdup(result_pool, merge->yca->repos_root_url);
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/mergeinfo.c b/subversion/libsvn_client/mergeinfo.c
new file mode 100644
index 0000000..453cc66
--- /dev/null
+++ b/subversion/libsvn_client/mergeinfo.c
@@ -0,0 +1,2191 @@
+/*
+ * mergeinfo.c : merge history functions for the libsvn_client library
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+#include <apr_pools.h>
+#include <apr_strings.h>
+
+#include "svn_pools.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_string.h"
+#include "svn_opt.h"
+#include "svn_error.h"
+#include "svn_error_codes.h"
+#include "svn_props.h"
+#include "svn_mergeinfo.h"
+#include "svn_sorts.h"
+#include "svn_ra.h"
+#include "svn_client.h"
+#include "svn_hash.h"
+
+#include "private/svn_opt_private.h"
+#include "private/svn_mergeinfo_private.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_ra_private.h"
+#include "private/svn_fspath.h"
+#include "private/svn_client_private.h"
+#include "client.h"
+#include "mergeinfo.h"
+#include "svn_private_config.h"
+
+
+
+svn_client__merge_path_t *
+svn_client__merge_path_dup(const svn_client__merge_path_t *old,
+ apr_pool_t *pool)
+{
+ svn_client__merge_path_t *new = apr_pmemdup(pool, old, sizeof(*old));
+
+ new->abspath = apr_pstrdup(pool, old->abspath);
+ if (new->remaining_ranges)
+ new->remaining_ranges = svn_rangelist_dup(old->remaining_ranges, pool);
+ if (new->pre_merge_mergeinfo)
+ new->pre_merge_mergeinfo = svn_mergeinfo_dup(old->pre_merge_mergeinfo,
+ pool);
+ if (new->implicit_mergeinfo)
+ new->implicit_mergeinfo = svn_mergeinfo_dup(old->implicit_mergeinfo,
+ pool);
+
+ return new;
+}
+
+svn_client__merge_path_t *
+svn_client__merge_path_create(const char *abspath,
+ apr_pool_t *pool)
+{
+ svn_client__merge_path_t *result = apr_pcalloc(pool, sizeof(*result));
+
+ result->abspath = apr_pstrdup(pool, abspath);
+ return result;
+}
+
+svn_error_t *
+svn_client__parse_mergeinfo(svn_mergeinfo_t *mergeinfo,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const svn_string_t *propval;
+
+ *mergeinfo = NULL;
+
+ /* ### Use svn_wc_prop_get() would actually be sufficient for now.
+ ### DannyB thinks that later we'll need behavior more like
+ ### svn_client__get_prop_from_wc(). */
+ SVN_ERR(svn_wc_prop_get2(&propval, wc_ctx, local_abspath, SVN_PROP_MERGEINFO,
+ scratch_pool, scratch_pool));
+ if (propval)
+ SVN_ERR(svn_mergeinfo_parse(mergeinfo, propval->data, result_pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__record_wc_mergeinfo(const char *local_abspath,
+ svn_mergeinfo_t mergeinfo,
+ svn_boolean_t do_notification,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_string_t *mergeinfo_str = NULL;
+ svn_boolean_t mergeinfo_changes = FALSE;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ /* Convert MERGEINFO (if any) into text for storage as a property value. */
+ if (mergeinfo)
+ SVN_ERR(svn_mergeinfo_to_string(&mergeinfo_str, mergeinfo, scratch_pool));
+
+ if (do_notification && ctx->notify_func2)
+ SVN_ERR(svn_client__mergeinfo_status(&mergeinfo_changes, ctx->wc_ctx,
+ local_abspath, scratch_pool));
+
+ /* Record the new mergeinfo in the WC. */
+ /* ### Later, we'll want behavior more analogous to
+ ### svn_client__get_prop_from_wc(). */
+ SVN_ERR(svn_wc_prop_set4(ctx->wc_ctx, local_abspath, SVN_PROP_MERGEINFO,
+ mergeinfo_str, svn_depth_empty,
+ TRUE /* skip checks */, NULL,
+ NULL, NULL /* cancellation */,
+ NULL, NULL /* notification */,
+ scratch_pool));
+
+ if (do_notification && ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify =
+ svn_wc_create_notify(local_abspath,
+ svn_wc_notify_merge_record_info,
+ scratch_pool);
+ if (mergeinfo_changes)
+ notify->prop_state = svn_wc_notify_state_merged;
+ else
+ notify->prop_state = svn_wc_notify_state_changed;
+
+ ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__record_wc_mergeinfo_catalog(apr_hash_t *result_catalog,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ if (apr_hash_count(result_catalog))
+ {
+ int i;
+ apr_array_header_t *sorted_cat =
+ svn_sort__hash(result_catalog, svn_sort_compare_items_as_paths,
+ scratch_pool);
+
+ /* Write the mergeinfo out in sorted order of the paths (presumably just
+ * so that the notifications are in a predictable, convenient order). */
+ for (i = 0; i < sorted_cat->nelts; i++)
+ {
+ svn_sort__item_t elt = APR_ARRAY_IDX(sorted_cat, i,
+ svn_sort__item_t);
+ svn_error_t *err;
+
+ svn_pool_clear(iterpool);
+ err = svn_client__record_wc_mergeinfo(elt.key, elt.value, TRUE,
+ ctx, iterpool);
+
+ if (err && err->apr_err == SVN_ERR_ENTRY_NOT_FOUND)
+ {
+ /* PATH isn't just missing, it's not even versioned as far
+ as this working copy knows. But it was included in
+ MERGES, which means that the server knows about it.
+ Likely we don't have access to the source due to authz
+ restrictions. For now just clear the error and
+ continue... */
+ svn_error_clear(err);
+ }
+ else
+ {
+ SVN_ERR(err);
+ }
+ }
+ }
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/*-----------------------------------------------------------------------*/
+
+/*** Retrieving mergeinfo. ***/
+
+svn_error_t *
+svn_client__get_wc_mergeinfo(svn_mergeinfo_t *mergeinfo,
+ svn_boolean_t *inherited_p,
+ svn_mergeinfo_inheritance_t inherit,
+ const char *local_abspath,
+ const char *limit_abspath,
+ const char **walked_path,
+ svn_boolean_t ignore_invalid_mergeinfo,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *walk_relpath = "";
+ svn_mergeinfo_t wc_mergeinfo;
+ svn_revnum_t base_revision;
+ apr_pool_t *iterpool;
+ svn_boolean_t inherited;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+ if (limit_abspath)
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(limit_abspath));
+
+ SVN_ERR(svn_wc__node_get_base(NULL, &base_revision, NULL, NULL, NULL, NULL,
+ ctx->wc_ctx, local_abspath,
+ TRUE /* ignore_enoent */,
+ FALSE /* show_hidden */,
+ scratch_pool, scratch_pool));
+
+ iterpool = svn_pool_create(scratch_pool);
+ while (TRUE)
+ {
+ svn_pool_clear(iterpool);
+
+ /* Don't look for explicit mergeinfo on LOCAL_ABSPATH if we are only
+ interested in inherited mergeinfo. */
+ if (inherit == svn_mergeinfo_nearest_ancestor)
+ {
+ wc_mergeinfo = NULL;
+ inherit = svn_mergeinfo_inherited;
+ }
+ else
+ {
+ /* Look for mergeinfo on LOCAL_ABSPATH. If there isn't any and we
+ want inherited mergeinfo, walk towards the root of the WC until
+ we encounter either (a) an unversioned directory, or
+ (b) mergeinfo. If we encounter (b), use that inherited
+ mergeinfo as our baseline. */
+ svn_error_t *err = svn_client__parse_mergeinfo(&wc_mergeinfo,
+ ctx->wc_ctx,
+ local_abspath,
+ result_pool,
+ iterpool);
+ if ((ignore_invalid_mergeinfo || walk_relpath [0] != '\0')
+ && err
+ && err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
+ {
+ svn_error_clear(err);
+ wc_mergeinfo = apr_hash_make(result_pool);
+ break;
+ }
+ else
+ {
+ SVN_ERR(err);
+ }
+ }
+
+ if (wc_mergeinfo == NULL &&
+ inherit != svn_mergeinfo_explicit &&
+ !svn_dirent_is_root(local_abspath, strlen(local_abspath)))
+ {
+ svn_boolean_t is_wc_root;
+ svn_boolean_t is_switched;
+ svn_revnum_t parent_base_rev;
+ svn_revnum_t parent_changed_rev;
+
+ /* Don't look any higher than the limit path. */
+ if (limit_abspath && strcmp(limit_abspath, local_abspath) == 0)
+ break;
+
+ /* If we've reached the root of the working copy don't look any
+ higher. */
+ SVN_ERR(svn_wc_check_root(&is_wc_root, &is_switched, NULL,
+ ctx->wc_ctx, local_abspath, iterpool));
+ if (is_wc_root || is_switched)
+ break;
+
+ /* No explicit mergeinfo on this path. Look higher up the
+ directory tree while keeping track of what we've walked. */
+ walk_relpath = svn_relpath_join(svn_dirent_basename(local_abspath,
+ iterpool),
+ walk_relpath, result_pool);
+ local_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
+
+ SVN_ERR(svn_wc__node_get_base(NULL, &parent_base_rev, NULL, NULL,
+ NULL, NULL,
+ ctx->wc_ctx, local_abspath,
+ TRUE, FALSE,
+ scratch_pool, scratch_pool));
+
+ /* ### This checks the WORKING changed_rev, so invalid on replacement
+ ### not even reliable in case an ancestor was copied from a
+ ### different location */
+ SVN_ERR(svn_wc__node_get_changed_info(&parent_changed_rev,
+ NULL, NULL,
+ ctx->wc_ctx, local_abspath,
+ scratch_pool,
+ scratch_pool));
+
+ /* Look in LOCAL_ABSPATH's parent for inherited mergeinfo if
+ LOCAL_ABSPATH has no base revision because it is an uncommitted
+ addition, or if its base revision falls within the inclusive
+ range of its parent's last changed revision to the parent's base
+ revision; otherwise stop looking for inherited mergeinfo. */
+ if (SVN_IS_VALID_REVNUM(base_revision)
+ && (base_revision < parent_changed_rev
+ || parent_base_rev < base_revision))
+ break;
+
+ /* We haven't yet risen above the root of the WC. */
+ continue;
+ }
+ break;
+ }
+
+ svn_pool_destroy(iterpool);
+
+ if (svn_path_is_empty(walk_relpath))
+ {
+ /* Mergeinfo is explicit. */
+ inherited = FALSE;
+ *mergeinfo = wc_mergeinfo;
+ }
+ else
+ {
+ /* Mergeinfo may be inherited. */
+ if (wc_mergeinfo)
+ {
+ inherited = TRUE;
+ SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(mergeinfo,
+ wc_mergeinfo,
+ walk_relpath,
+ result_pool,
+ scratch_pool));
+ }
+ else
+ {
+ inherited = FALSE;
+ *mergeinfo = NULL;
+ }
+ }
+
+ if (walked_path)
+ *walked_path = walk_relpath;
+
+ /* Remove non-inheritable mergeinfo and paths mapped to empty ranges
+ which may occur if WCPATH's mergeinfo is not explicit. */
+ if (inherited
+ && apr_hash_count(*mergeinfo)) /* Nothing to do for empty mergeinfo. */
+ {
+ SVN_ERR(svn_mergeinfo_inheritable2(mergeinfo, *mergeinfo, NULL,
+ SVN_INVALID_REVNUM, SVN_INVALID_REVNUM,
+ TRUE, result_pool, scratch_pool));
+ svn_mergeinfo__remove_empty_rangelists(*mergeinfo, result_pool);
+ }
+
+ if (inherited_p)
+ *inherited_p = inherited;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__get_wc_mergeinfo_catalog(svn_mergeinfo_catalog_t *mergeinfo_cat,
+ svn_boolean_t *inherited,
+ svn_boolean_t include_descendants,
+ svn_mergeinfo_inheritance_t inherit,
+ const char *local_abspath,
+ const char *limit_path,
+ const char **walked_path,
+ svn_boolean_t ignore_invalid_mergeinfo,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *target_repos_relpath;
+ svn_mergeinfo_t mergeinfo;
+ const char *repos_root;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+ *mergeinfo_cat = NULL;
+ SVN_ERR(svn_wc__node_get_repos_info(NULL, &target_repos_relpath,
+ &repos_root, NULL,
+ ctx->wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+
+ /* Get the mergeinfo for the LOCAL_ABSPATH target and set *INHERITED and
+ *WALKED_PATH. */
+ SVN_ERR(svn_client__get_wc_mergeinfo(&mergeinfo, inherited, inherit,
+ local_abspath, limit_path,
+ walked_path, ignore_invalid_mergeinfo,
+ ctx, result_pool, scratch_pool));
+
+ /* Add any explicit/inherited mergeinfo for LOCAL_ABSPATH to
+ *MERGEINFO_CAT. */
+ if (mergeinfo)
+ {
+ *mergeinfo_cat = apr_hash_make(result_pool);
+ svn_hash_sets(*mergeinfo_cat,
+ apr_pstrdup(result_pool, target_repos_relpath), mergeinfo);
+ }
+
+ /* If LOCAL_ABSPATH is a directory and we want the subtree mergeinfo too,
+ then get it.
+
+ With WC-NG it is cheaper to do a single db transaction, than first
+ looking if we really have a directory. */
+ if (include_descendants)
+ {
+ apr_hash_t *mergeinfo_props;
+ apr_hash_index_t *hi;
+
+ SVN_ERR(svn_wc__prop_retrieve_recursive(&mergeinfo_props,
+ ctx->wc_ctx, local_abspath,
+ SVN_PROP_MERGEINFO,
+ scratch_pool, scratch_pool));
+
+ /* Convert *mergeinfo_props into a proper svn_mergeinfo_catalog_t */
+ for (hi = apr_hash_first(scratch_pool, mergeinfo_props);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *node_abspath = svn__apr_hash_index_key(hi);
+ svn_string_t *propval = svn__apr_hash_index_val(hi);
+ svn_mergeinfo_t subtree_mergeinfo;
+ const char *repos_relpath;
+
+ if (strcmp(node_abspath, local_abspath) == 0)
+ continue; /* Already parsed in svn_client__get_wc_mergeinfo */
+
+ SVN_ERR(svn_wc__node_get_repos_info(NULL, &repos_relpath, NULL, NULL,
+ ctx->wc_ctx, node_abspath,
+ result_pool, scratch_pool));
+
+ SVN_ERR(svn_mergeinfo_parse(&subtree_mergeinfo, propval->data,
+ result_pool));
+
+ /* If the target had no explicit/inherited mergeinfo and this is the
+ first subtree with mergeinfo found, then the catalog will still
+ be NULL. */
+ if (*mergeinfo_cat == NULL)
+ *mergeinfo_cat = apr_hash_make(result_pool);
+
+ svn_hash_sets(*mergeinfo_cat, repos_relpath, subtree_mergeinfo);
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__get_repos_mergeinfo(svn_mergeinfo_t *target_mergeinfo,
+ svn_ra_session_t *ra_session,
+ const char *url,
+ svn_revnum_t rev,
+ svn_mergeinfo_inheritance_t inherit,
+ svn_boolean_t squelch_incapable,
+ apr_pool_t *pool)
+{
+ svn_mergeinfo_catalog_t tgt_mergeinfo_cat;
+
+ *target_mergeinfo = NULL;
+
+ SVN_ERR(svn_client__get_repos_mergeinfo_catalog(&tgt_mergeinfo_cat,
+ ra_session,
+ url, rev, inherit,
+ squelch_incapable, FALSE,
+ pool, pool));
+
+ if (tgt_mergeinfo_cat && apr_hash_count(tgt_mergeinfo_cat))
+ {
+ /* We asked only for the REL_PATH's mergeinfo, not any of its
+ descendants. So if there is anything in the catalog it is the
+ mergeinfo for REL_PATH. */
+ *target_mergeinfo =
+ svn__apr_hash_index_val(apr_hash_first(pool, tgt_mergeinfo_cat));
+
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__get_repos_mergeinfo_catalog(svn_mergeinfo_catalog_t *mergeinfo_cat,
+ svn_ra_session_t *ra_session,
+ const char *url,
+ svn_revnum_t rev,
+ svn_mergeinfo_inheritance_t inherit,
+ svn_boolean_t squelch_incapable,
+ svn_boolean_t include_descendants,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err;
+ svn_mergeinfo_catalog_t repos_mergeinfo_cat;
+ apr_array_header_t *rel_paths = apr_array_make(scratch_pool, 1,
+ sizeof(const char *));
+ const char *old_session_url;
+
+ APR_ARRAY_PUSH(rel_paths, const char *) = "";
+
+ /* Fetch the mergeinfo. */
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url,
+ ra_session, url, scratch_pool));
+ err = svn_ra_get_mergeinfo(ra_session, &repos_mergeinfo_cat, rel_paths,
+ rev, inherit, include_descendants, result_pool);
+ err = svn_error_compose_create(
+ err, svn_ra_reparent(ra_session, old_session_url, scratch_pool));
+ if (err)
+ {
+ if (squelch_incapable && err->apr_err == SVN_ERR_UNSUPPORTED_FEATURE)
+ {
+ svn_error_clear(err);
+ *mergeinfo_cat = NULL;
+ return SVN_NO_ERROR;
+ }
+ else
+ return svn_error_trace(err);
+ }
+
+ if (repos_mergeinfo_cat == NULL)
+ {
+ *mergeinfo_cat = NULL;
+ }
+ else
+ {
+ const char *session_relpath;
+
+ SVN_ERR(svn_ra_get_path_relative_to_root(ra_session, &session_relpath,
+ url, scratch_pool));
+
+ if (session_relpath[0] == '\0')
+ *mergeinfo_cat = repos_mergeinfo_cat;
+ else
+ SVN_ERR(svn_mergeinfo__add_prefix_to_catalog(mergeinfo_cat,
+ repos_mergeinfo_cat,
+ session_relpath,
+ result_pool,
+ scratch_pool));
+ }
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__get_wc_or_repos_mergeinfo(svn_mergeinfo_t *target_mergeinfo,
+ svn_boolean_t *inherited,
+ svn_boolean_t *from_repos,
+ svn_boolean_t repos_only,
+ svn_mergeinfo_inheritance_t inherit,
+ svn_ra_session_t *ra_session,
+ const char *target_wcpath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_mergeinfo_catalog_t tgt_mergeinfo_cat;
+
+ *target_mergeinfo = NULL;
+
+ SVN_ERR(svn_client__get_wc_or_repos_mergeinfo_catalog(&tgt_mergeinfo_cat,
+ inherited, from_repos,
+ FALSE,
+ repos_only,
+ FALSE, inherit,
+ ra_session,
+ target_wcpath, ctx,
+ pool, pool));
+ if (tgt_mergeinfo_cat && apr_hash_count(tgt_mergeinfo_cat))
+ {
+ /* We asked only for the TARGET_WCPATH's mergeinfo, not any of its
+ descendants. It this mergeinfo is in the catalog, it's keyed
+ on TARGET_WCPATH's root-relative path. We could dig that up
+ so we can peek into our catalog, but it ought to be the only
+ thing in the catalog, so we'll just fetch the first hash item. */
+ *target_mergeinfo =
+ svn__apr_hash_index_val(apr_hash_first(pool, tgt_mergeinfo_cat));
+
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__get_wc_or_repos_mergeinfo_catalog(
+ svn_mergeinfo_catalog_t *target_mergeinfo_catalog,
+ svn_boolean_t *inherited_p,
+ svn_boolean_t *from_repos,
+ svn_boolean_t include_descendants,
+ svn_boolean_t repos_only,
+ svn_boolean_t ignore_invalid_mergeinfo,
+ svn_mergeinfo_inheritance_t inherit,
+ svn_ra_session_t *ra_session,
+ const char *target_wcpath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *url;
+ svn_revnum_t target_rev;
+ const char *local_abspath;
+ const char *repos_root;
+ const char *repos_relpath;
+ svn_mergeinfo_catalog_t target_mergeinfo_cat_wc = NULL;
+ svn_mergeinfo_catalog_t target_mergeinfo_cat_repos = NULL;
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, target_wcpath,
+ scratch_pool));
+
+ if (from_repos)
+ *from_repos = FALSE;
+
+ /* We may get an entry with abbreviated information from TARGET_WCPATH's
+ parent if TARGET_WCPATH is missing. These limited entries do not have
+ a URL and without that we cannot get accurate mergeinfo for
+ TARGET_WCPATH. */
+ SVN_ERR(svn_wc__node_get_origin(NULL, &target_rev, &repos_relpath,
+ &repos_root, NULL, NULL,
+ ctx->wc_ctx, local_abspath, FALSE,
+ scratch_pool, scratch_pool));
+
+ if (repos_relpath)
+ url = svn_path_url_add_component2(repos_root, repos_relpath, scratch_pool);
+ else
+ url = NULL;
+
+ if (!repos_only)
+ {
+ svn_boolean_t inherited;
+ SVN_ERR(svn_client__get_wc_mergeinfo_catalog(&target_mergeinfo_cat_wc,
+ &inherited,
+ include_descendants,
+ inherit,
+ local_abspath,
+ NULL, NULL,
+ ignore_invalid_mergeinfo,
+ ctx,
+ result_pool,
+ scratch_pool));
+ if (inherited_p)
+ *inherited_p = inherited;
+
+ /* If we want LOCAL_ABSPATH's inherited mergeinfo, were we able to
+ get it from the working copy? If not, then we must ask the
+ repository. */
+ if (! (inherited
+ || (inherit == svn_mergeinfo_explicit)
+ || (repos_relpath
+ && target_mergeinfo_cat_wc
+ && svn_hash_gets(target_mergeinfo_cat_wc, repos_relpath))))
+ {
+ repos_only = TRUE;
+ /* We already have any subtree mergeinfo from the working copy, no
+ need to ask the server for it again. */
+ include_descendants = FALSE;
+ }
+ }
+
+ if (repos_only)
+ {
+ /* No need to check the repos if this is a local addition. */
+ if (url != NULL)
+ {
+ apr_hash_t *original_props;
+
+ /* Check to see if we have local modifications which removed all of
+ TARGET_WCPATH's pristine mergeinfo. If that is the case then
+ TARGET_WCPATH effectively has no mergeinfo. */
+ SVN_ERR(svn_wc_get_pristine_props(&original_props,
+ ctx->wc_ctx, local_abspath,
+ result_pool, scratch_pool));
+ if (!svn_hash_gets(original_props, SVN_PROP_MERGEINFO))
+ {
+ apr_pool_t *sesspool = NULL;
+
+ if (! ra_session)
+ {
+ sesspool = svn_pool_create(scratch_pool);
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, url, NULL,
+ ctx,
+ sesspool, sesspool));
+ }
+
+ SVN_ERR(svn_client__get_repos_mergeinfo_catalog(
+ &target_mergeinfo_cat_repos, ra_session,
+ url, target_rev, inherit,
+ TRUE, include_descendants,
+ result_pool, scratch_pool));
+
+ if (target_mergeinfo_cat_repos
+ && svn_hash_gets(target_mergeinfo_cat_repos, repos_relpath))
+ {
+ if (inherited_p)
+ *inherited_p = TRUE;
+ if (from_repos)
+ *from_repos = TRUE;
+ }
+
+ /* If we created an RA_SESSION above, destroy it.
+ Otherwise, if reparented an existing session, point
+ it back where it was when we were called. */
+ if (sesspool)
+ {
+ svn_pool_destroy(sesspool);
+ }
+ }
+ }
+ }
+
+ /* Combine the mergeinfo from the working copy and repository as needed. */
+ if (target_mergeinfo_cat_wc)
+ {
+ *target_mergeinfo_catalog = target_mergeinfo_cat_wc;
+ if (target_mergeinfo_cat_repos)
+ SVN_ERR(svn_mergeinfo_catalog_merge(*target_mergeinfo_catalog,
+ target_mergeinfo_cat_repos,
+ result_pool, scratch_pool));
+ }
+ else if (target_mergeinfo_cat_repos)
+ {
+ *target_mergeinfo_catalog = target_mergeinfo_cat_repos;
+ }
+ else
+ {
+ *target_mergeinfo_catalog = NULL;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__get_history_as_mergeinfo(svn_mergeinfo_t *mergeinfo_p,
+ svn_boolean_t *has_rev_zero_history,
+ const svn_client__pathrev_t *pathrev,
+ svn_revnum_t range_youngest,
+ svn_revnum_t range_oldest,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *segments;
+
+ /* Fetch the location segments for our URL@PEG_REVNUM. */
+ if (! SVN_IS_VALID_REVNUM(range_youngest))
+ range_youngest = pathrev->rev;
+ if (! SVN_IS_VALID_REVNUM(range_oldest))
+ range_oldest = 0;
+
+ SVN_ERR(svn_client__repos_location_segments(&segments, ra_session,
+ pathrev->url, pathrev->rev,
+ range_youngest, range_oldest,
+ ctx, pool));
+
+ if (has_rev_zero_history)
+ {
+ *has_rev_zero_history = FALSE;
+ if (segments->nelts)
+ {
+ svn_location_segment_t *oldest_segment =
+ APR_ARRAY_IDX(segments, 0, svn_location_segment_t *);
+ if (oldest_segment->range_start == 0)
+ *has_rev_zero_history = TRUE;
+ }
+ }
+
+ SVN_ERR(svn_mergeinfo__mergeinfo_from_segments(mergeinfo_p, segments, pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/*-----------------------------------------------------------------------*/
+
+/*** Eliding mergeinfo. ***/
+
+/* Given the mergeinfo (CHILD_MERGEINFO) for a path, and the
+ mergeinfo of its nearest ancestor with mergeinfo (PARENT_MERGEINFO), compare
+ CHILD_MERGEINFO to PARENT_MERGEINFO to see if the former elides to
+ the latter, following the elision rules described in
+ svn_client__elide_mergeinfo()'s docstring. Set *ELIDES to whether
+ or not CHILD_MERGEINFO is redundant.
+
+ Note: This function assumes that PARENT_MERGEINFO is definitive;
+ i.e. if it is NULL then the caller not only walked the entire WC
+ looking for inherited mergeinfo, but queried the repository if none
+ was found in the WC. This is rather important since this function
+ says empty mergeinfo should be elided if PARENT_MERGEINFO is NULL,
+ and we don't want to do that unless we are *certain* that the empty
+ mergeinfo on PATH isn't overriding anything.
+
+ If PATH_SUFFIX and PARENT_MERGEINFO are not NULL append PATH_SUFFIX
+ to each path in PARENT_MERGEINFO before performing the comparison. */
+static svn_error_t *
+should_elide_mergeinfo(svn_boolean_t *elides,
+ svn_mergeinfo_t parent_mergeinfo,
+ svn_mergeinfo_t child_mergeinfo,
+ const char *path_suffix,
+ apr_pool_t *scratch_pool)
+{
+ /* Easy out: No child mergeinfo to elide. */
+ if (child_mergeinfo == NULL)
+ {
+ *elides = FALSE;
+ }
+ else if (apr_hash_count(child_mergeinfo) == 0)
+ {
+ /* Empty mergeinfo elides to empty mergeinfo or to "nothing",
+ i.e. it isn't overriding any parent. Otherwise it doesn't
+ elide. */
+ *elides = (!parent_mergeinfo || apr_hash_count(parent_mergeinfo) == 0);
+ }
+ else if (!parent_mergeinfo || apr_hash_count(parent_mergeinfo) == 0)
+ {
+ /* Non-empty mergeinfo never elides to empty mergeinfo
+ or no mergeinfo. */
+ *elides = FALSE;
+ }
+ else
+ {
+ /* Both CHILD_MERGEINFO and PARENT_MERGEINFO are non-NULL and
+ non-empty. */
+ svn_mergeinfo_t path_tweaked_parent_mergeinfo;
+
+ /* If we need to adjust the paths in PARENT_MERGEINFO do it now. */
+ if (path_suffix)
+ SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(
+ &path_tweaked_parent_mergeinfo, parent_mergeinfo,
+ path_suffix, scratch_pool, scratch_pool));
+ else
+ path_tweaked_parent_mergeinfo = parent_mergeinfo;
+
+ SVN_ERR(svn_mergeinfo__equals(elides,
+ path_tweaked_parent_mergeinfo,
+ child_mergeinfo, TRUE, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for svn_client__elide_mergeinfo().
+
+ Given a working copy LOCAL_ABSPATH, its mergeinfo hash CHILD_MERGEINFO, and
+ the mergeinfo of LOCAL_ABSPATH's nearest ancestor PARENT_MERGEINFO, use
+ should_elide_mergeinfo() to decide whether or not CHILD_MERGEINFO elides to
+ PARENT_MERGEINFO; PATH_SUFFIX means the same as in that function.
+
+ If elision does occur, then remove the mergeinfo for LOCAL_ABSPATH.
+
+ If CHILD_MERGEINFO is NULL, do nothing.
+
+ Use SCRATCH_POOL for temporary allocations.
+*/
+static svn_error_t *
+elide_mergeinfo(svn_mergeinfo_t parent_mergeinfo,
+ svn_mergeinfo_t child_mergeinfo,
+ const char *local_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_boolean_t elides;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+
+ SVN_ERR(should_elide_mergeinfo(&elides,
+ parent_mergeinfo, child_mergeinfo, NULL,
+ scratch_pool));
+
+ if (elides)
+ {
+ SVN_ERR(svn_wc_prop_set4(ctx->wc_ctx, local_abspath, SVN_PROP_MERGEINFO,
+ NULL, svn_depth_empty, TRUE, NULL,
+ NULL, NULL /* cancellation */,
+ NULL, NULL /* notification */,
+ scratch_pool));
+
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+
+ notify = svn_wc_create_notify(local_abspath,
+ svn_wc_notify_merge_elide_info,
+ scratch_pool);
+ ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
+
+ notify = svn_wc_create_notify(local_abspath,
+ svn_wc_notify_update_update,
+ scratch_pool);
+ notify->prop_state = svn_wc_notify_state_changed;
+
+ ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__elide_mergeinfo(const char *target_abspath,
+ const char *wc_elision_limit_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *limit_abspath = wc_elision_limit_abspath;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(target_abspath));
+ SVN_ERR_ASSERT(!wc_elision_limit_abspath || svn_dirent_is_absolute(wc_elision_limit_abspath));
+
+ /* Check for first easy out: We are already at the limit path. */
+ if (!limit_abspath
+ || strcmp(target_abspath, limit_abspath) != 0)
+ {
+ svn_mergeinfo_t target_mergeinfo;
+ svn_mergeinfo_t mergeinfo = NULL;
+ svn_boolean_t inherited;
+ const char *walk_path;
+ svn_error_t *err;
+
+ /* Get the TARGET_WCPATH's explicit mergeinfo. */
+ err = svn_client__get_wc_mergeinfo(&target_mergeinfo, &inherited,
+ svn_mergeinfo_inherited,
+ target_abspath,
+ limit_abspath,
+ &walk_path, FALSE,
+ ctx, pool, pool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
+ {
+ /* Issue #3896: If we attempt elision because invalid
+ mergeinfo is present on TARGET_WCPATH, then don't let
+ the merge fail, just skip the elision attempt. */
+ svn_error_clear(err);
+ return SVN_NO_ERROR;
+ }
+ else
+ {
+ return svn_error_trace(err);
+ }
+ }
+
+ /* If TARGET_WCPATH has no explicit mergeinfo, there's nothing to
+ elide, we're done. */
+ if (inherited || target_mergeinfo == NULL)
+ return SVN_NO_ERROR;
+
+ /* Get TARGET_WCPATH's inherited mergeinfo from the WC. */
+ err = svn_client__get_wc_mergeinfo(&mergeinfo, NULL,
+ svn_mergeinfo_nearest_ancestor,
+ target_abspath,
+ limit_abspath,
+ &walk_path, FALSE, ctx, pool, pool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
+ {
+ /* Issue #3896 again, but invalid mergeinfo is inherited. */
+ svn_error_clear(err);
+ return SVN_NO_ERROR;
+ }
+ else
+ {
+ return svn_error_trace(err);
+ }
+ }
+
+ /* If TARGET_WCPATH inherited no mergeinfo from the WC and we are
+ not limiting our search to the working copy then check if it
+ inherits any from the repos. */
+ if (!mergeinfo && !wc_elision_limit_abspath)
+ {
+ err = svn_client__get_wc_or_repos_mergeinfo(
+ &mergeinfo, NULL, NULL, TRUE,
+ svn_mergeinfo_nearest_ancestor,
+ NULL, target_abspath, ctx, pool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
+ {
+ /* Issue #3896 again, but invalid mergeinfo is inherited
+ from the repository. */
+ svn_error_clear(err);
+ return SVN_NO_ERROR;
+ }
+ else
+ {
+ return svn_error_trace(err);
+ }
+ }
+ }
+
+ /* If there is nowhere to elide TARGET_WCPATH's mergeinfo to and
+ the elision is limited, then we are done.*/
+ if (!mergeinfo && wc_elision_limit_abspath)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(elide_mergeinfo(mergeinfo, target_mergeinfo, target_abspath,
+ ctx, pool));
+ }
+ return SVN_NO_ERROR;
+}
+
+
+/* Set *MERGEINFO_CATALOG to the explicit or inherited mergeinfo for
+ PATH_OR_URL@PEG_REVISION. If INCLUDE_DESCENDANTS is true, also
+ store in *MERGEINFO_CATALOG the explicit mergeinfo on any subtrees
+ under PATH_OR_URL. Key all mergeinfo in *MERGEINFO_CATALOG on
+ repository relpaths.
+
+ If no mergeinfo is found then set *MERGEINFO_CATALOG to NULL.
+
+ Set *REPOS_ROOT to the root URL of the repository associated with
+ PATH_OR_URL.
+
+ Allocate *MERGEINFO_CATALOG and all its contents in RESULT_POOL. Use
+ SCRATCH_POOL for all temporary allocations.
+
+ Return SVN_ERR_UNSUPPORTED_FEATURE if the server does not support
+ Merge Tracking. */
+static svn_error_t *
+get_mergeinfo(svn_mergeinfo_catalog_t *mergeinfo_catalog,
+ const char **repos_root,
+ const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ svn_boolean_t include_descendants,
+ svn_boolean_t ignore_invalid_mergeinfo,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_ra_session_t *ra_session;
+ const char *local_abspath;
+ svn_boolean_t use_url = svn_path_is_url(path_or_url);
+ svn_client__pathrev_t *peg_loc;
+
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &peg_loc,
+ path_or_url, NULL, peg_revision,
+ peg_revision, ctx, scratch_pool));
+
+ /* If PATH_OR_URL is as working copy path determine if we will need to
+ contact the repository for the requested PEG_REVISION. */
+ if (!use_url)
+ {
+ svn_client__pathrev_t *origin;
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path_or_url,
+ scratch_pool));
+
+ SVN_ERR(svn_client__wc_node_get_origin(&origin, local_abspath, ctx,
+ scratch_pool, scratch_pool));
+ if (!origin
+ || strcmp(origin->url, peg_loc->url) != 0
+ || peg_loc->rev != origin->rev)
+ {
+ use_url = TRUE; /* Don't rely on local mergeinfo */
+ }
+ }
+
+ /* Check server Merge Tracking capability. */
+ SVN_ERR(svn_ra__assert_mergeinfo_capable_server(ra_session, path_or_url,
+ scratch_pool));
+
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, repos_root, result_pool));
+
+ if (use_url)
+ {
+ SVN_ERR(svn_client__get_repos_mergeinfo_catalog(
+ mergeinfo_catalog, ra_session, peg_loc->url, peg_loc->rev,
+ svn_mergeinfo_inherited, FALSE, include_descendants,
+ result_pool, scratch_pool));
+ }
+ else /* ! svn_path_is_url() */
+ {
+ SVN_ERR(svn_client__get_wc_or_repos_mergeinfo_catalog(
+ mergeinfo_catalog, NULL, NULL, include_descendants, FALSE,
+ ignore_invalid_mergeinfo, svn_mergeinfo_inherited,
+ ra_session, path_or_url, ctx,
+ result_pool, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/*** In-memory mergeinfo elision ***/
+svn_error_t *
+svn_client__elide_mergeinfo_catalog(svn_mergeinfo_catalog_t mergeinfo_catalog,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *sorted_hash;
+ apr_array_header_t *elidable_paths = apr_array_make(scratch_pool, 1,
+ sizeof(const char *));
+ apr_array_header_t *dir_stack = apr_array_make(scratch_pool, 1,
+ sizeof(const char *));
+ apr_pool_t *iterpool;
+ int i;
+
+ /* Here's the general algorithm:
+ Walk through the paths sorted in tree order. For each path, pop
+ the dir_stack until it is either empty or the top item contains a parent
+ of the current path. Check to see if that mergeinfo is then elidable,
+ and build the list of elidable mergeinfo based upon that determination.
+ Finally, push the path of interest onto the stack, and continue. */
+ sorted_hash = svn_sort__hash(mergeinfo_catalog,
+ svn_sort_compare_items_as_paths,
+ scratch_pool);
+ iterpool = svn_pool_create(scratch_pool);
+ for (i = 0; i < sorted_hash->nelts; i++)
+ {
+ svn_sort__item_t *item = &APR_ARRAY_IDX(sorted_hash, i,
+ svn_sort__item_t);
+ const char *path = item->key;
+
+ if (dir_stack->nelts > 0)
+ {
+ const char *top;
+ const char *path_suffix;
+ svn_boolean_t elides = FALSE;
+
+ svn_pool_clear(iterpool);
+
+ /* Pop off any paths which are not ancestors of PATH. */
+ do
+ {
+ top = APR_ARRAY_IDX(dir_stack, dir_stack->nelts - 1,
+ const char *);
+ path_suffix = svn_dirent_is_child(top, path, NULL);
+
+ if (!path_suffix)
+ apr_array_pop(dir_stack);
+ }
+ while (dir_stack->nelts > 0 && !path_suffix);
+
+ /* If we have a path suffix, it means we haven't popped the stack
+ clean. */
+ if (path_suffix)
+ {
+ SVN_ERR(should_elide_mergeinfo(&elides,
+ svn_hash_gets(mergeinfo_catalog, top),
+ svn_hash_gets(mergeinfo_catalog, path),
+ path_suffix,
+ iterpool));
+
+ if (elides)
+ APR_ARRAY_PUSH(elidable_paths, const char *) = path;
+ }
+ }
+
+ APR_ARRAY_PUSH(dir_stack, const char *) = path;
+ }
+ svn_pool_destroy(iterpool);
+
+ /* Now remove the elidable paths from the catalog. */
+ for (i = 0; i < elidable_paths->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(elidable_paths, i, const char *);
+ svn_hash_sets(mergeinfo_catalog, path, NULL);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Helper for filter_log_entry_with_rangelist().
+
+ DEPTH_FIRST_CATALOG_INDEX is an array of svn_sort__item_t's. The keys are
+ repository-absolute const char *paths, the values are svn_mergeinfo_t for
+ each path.
+
+ Return a pointer to the mergeinfo value of the nearest path-wise ancestor
+ of FSPATH in DEPTH_FIRST_CATALOG_INDEX. A path is considered its
+ own ancestor, so if a key exactly matches FSPATH, return that
+ key's mergeinfo and set *ANCESTOR_IS_SELF to true (set it to false in all
+ other cases).
+
+ If DEPTH_FIRST_CATALOG_INDEX is NULL, empty, or no ancestor is found, then
+ return NULL. */
+static svn_mergeinfo_t
+find_nearest_ancestor(const apr_array_header_t *depth_first_catalog_index,
+ svn_boolean_t *ancestor_is_self,
+ const char *fspath)
+{
+ int ancestor_index = -1;
+
+ *ancestor_is_self = FALSE;
+
+ if (depth_first_catalog_index)
+ {
+ int i;
+
+ for (i = 0; i < depth_first_catalog_index->nelts; i++)
+ {
+ svn_sort__item_t item = APR_ARRAY_IDX(depth_first_catalog_index, i,
+ svn_sort__item_t);
+ if (svn_fspath__skip_ancestor(item.key, fspath))
+ {
+ ancestor_index = i;
+
+ /* There's no nearer ancestor than FSPATH itself. */
+ if (strcmp(item.key, fspath) == 0)
+ {
+ *ancestor_is_self = TRUE;
+ break;
+ }
+ }
+
+ }
+ }
+
+ if (ancestor_index == -1)
+ return NULL;
+ else
+ return (APR_ARRAY_IDX(depth_first_catalog_index,
+ ancestor_index,
+ svn_sort__item_t)).value;
+}
+
+/* Baton for use with the filter_log_entry_with_rangelist()
+ svn_log_entry_receiver_t callback. */
+struct filter_log_entry_baton_t
+{
+ /* Is TRUE if RANGELIST describes potentially merged revisions, is FALSE
+ if RANGELIST describes potentially eligible revisions. */
+ svn_boolean_t filtering_merged;
+
+ /* Unsorted array of repository relative paths representing the merge
+ sources. There will be more than one source */
+ const apr_array_header_t *merge_source_fspaths;
+
+ /* The repository-absolute path we are calling svn_client_log5() on. */
+ const char *target_fspath;
+
+ /* Mergeinfo catalog for the tree rooted at TARGET_FSPATH.
+ The path keys must be repository-absolute. */
+ svn_mergeinfo_catalog_t target_mergeinfo_catalog;
+
+ /* Depth first sorted array of svn_sort__item_t's for
+ TARGET_MERGEINFO_CATALOG. */
+ apr_array_header_t *depth_first_catalog_index;
+
+ /* A rangelist describing all the revisions potentially merged or
+ potentially eligible for merging (see FILTERING_MERGED) based on
+ the target's explicit or inherited mergeinfo. */
+ const svn_rangelist_t *rangelist;
+
+ /* The wrapped svn_log_entry_receiver_t callback and baton which
+ filter_log_entry_with_rangelist() is acting as a filter for. */
+ svn_log_entry_receiver_t log_receiver;
+ void *log_receiver_baton;
+
+ svn_client_ctx_t *ctx;
+};
+
+/* Implements the svn_log_entry_receiver_t interface. BATON is a
+ `struct filter_log_entry_baton_t *'.
+
+ Call the wrapped log receiver BATON->log_receiver (with
+ BATON->log_receiver_baton) if:
+
+ BATON->FILTERING_MERGED is FALSE and the changes represented by LOG_ENTRY
+ have been fully merged from BATON->merge_source_fspaths to the WC target
+ based on the mergeinfo for the WC contained in BATON->TARGET_MERGEINFO_CATALOG.
+
+ Or
+
+ BATON->FILTERING_MERGED is TRUE and the changes represented by LOG_ENTRY
+ have not been merged, or only partially merged, from
+ BATON->merge_source_fspaths to the WC target based on the mergeinfo for the
+ WC contained in BATON->TARGET_MERGEINFO_CATALOG. */
+static svn_error_t *
+filter_log_entry_with_rangelist(void *baton,
+ svn_log_entry_t *log_entry,
+ apr_pool_t *pool)
+{
+ struct filter_log_entry_baton_t *fleb = baton;
+ svn_rangelist_t *intersection, *this_rangelist;
+
+ if (fleb->ctx->cancel_func)
+ SVN_ERR(fleb->ctx->cancel_func(fleb->ctx->cancel_baton));
+
+ /* Ignore r0 because there can be no "change 0" in a merge range. */
+ if (log_entry->revision == 0)
+ return SVN_NO_ERROR;
+
+ this_rangelist = svn_rangelist__initialize(log_entry->revision - 1,
+ log_entry->revision,
+ TRUE, pool);
+
+ /* Don't consider inheritance yet, see if LOG_ENTRY->REVISION is
+ fully or partially represented in BATON->RANGELIST. */
+ SVN_ERR(svn_rangelist_intersect(&intersection, fleb->rangelist,
+ this_rangelist, FALSE, pool));
+ if (! (intersection && intersection->nelts))
+ return SVN_NO_ERROR;
+
+ SVN_ERR_ASSERT(intersection->nelts == 1);
+
+ /* Ok, we know LOG_ENTRY->REVISION is represented in BATON->RANGELIST,
+ but is it only partially represented, i.e. is the corresponding range in
+ BATON->RANGELIST non-inheritable? Ask for the same intersection as
+ above but consider inheritance this time, if the intersection is empty
+ we know the range in BATON->RANGELIST is non-inheritable. */
+ SVN_ERR(svn_rangelist_intersect(&intersection, fleb->rangelist,
+ this_rangelist, TRUE, pool));
+ log_entry->non_inheritable = !intersection->nelts;
+
+ /* If the paths changed by LOG_ENTRY->REVISION are provided we can determine
+ if LOG_ENTRY->REVISION, while only partially represented in
+ BATON->RANGELIST, is in fact completely applied to all affected paths.
+ ### And ... what if it is, or if it isn't? What do we do with the answer?
+ And how do we cope if the changed paths are not provided? */
+ if ((log_entry->non_inheritable || !fleb->filtering_merged)
+ && log_entry->changed_paths2)
+ {
+ apr_hash_index_t *hi;
+ svn_boolean_t all_subtrees_have_this_rev = TRUE;
+ svn_rangelist_t *this_rev_rangelist =
+ svn_rangelist__initialize(log_entry->revision - 1,
+ log_entry->revision, TRUE, pool);
+ apr_pool_t *iterpool = svn_pool_create(pool);
+
+ for (hi = apr_hash_first(pool, log_entry->changed_paths2);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ int i;
+ const char *path = svn__apr_hash_index_key(hi);
+ svn_log_changed_path2_t *change = svn__apr_hash_index_val(hi);
+ const char *target_fspath_affected;
+ svn_mergeinfo_t nearest_ancestor_mergeinfo;
+ svn_boolean_t found_this_revision = FALSE;
+ const char *merge_source_rel_target;
+ const char *merge_source_fspath;
+ svn_boolean_t ancestor_is_self;
+
+ svn_pool_clear(iterpool);
+
+ /* Check that PATH is a subtree of at least one of the
+ merge sources. If not then ignore this path. */
+ for (i = 0; i < fleb->merge_source_fspaths->nelts; i++)
+ {
+ merge_source_fspath = APR_ARRAY_IDX(fleb->merge_source_fspaths,
+ i, const char *);
+
+ merge_source_rel_target
+ = svn_fspath__skip_ancestor(merge_source_fspath, path);
+ if (merge_source_rel_target)
+ {
+ /* If MERGE_SOURCE was itself deleted, replaced, or added
+ in LOG_ENTRY->REVISION then ignore this PATH since you
+ can't merge a addition or deletion of yourself. */
+ if (merge_source_rel_target[0] == '\0'
+ && (change->action != 'M'))
+ i = fleb->merge_source_fspaths->nelts;
+ break;
+ }
+ }
+ /* If we examined every merge source path and PATH is a child of
+ none of them then we can ignore this PATH. */
+ if (i == fleb->merge_source_fspaths->nelts)
+ continue;
+
+ /* Calculate the target path which PATH would affect if merged. */
+ target_fspath_affected = svn_fspath__join(fleb->target_fspath,
+ merge_source_rel_target,
+ iterpool);
+
+ nearest_ancestor_mergeinfo =
+ find_nearest_ancestor(fleb->depth_first_catalog_index,
+ &ancestor_is_self,
+ target_fspath_affected);
+
+ /* Issue #3791: A path should never have explicit mergeinfo
+ describing its own addition (that's self-referential). Nor will
+ it have explicit mergeinfo describing its own deletion (we
+ obviously can't add new mergeinfo to a path we are deleting).
+
+ This lack of explicit mergeinfo should not cause such revisions
+ to show up as eligible however. If PATH was deleted, replaced,
+ or added in LOG_ENTRY->REVISION, but the corresponding
+ TARGET_PATH_AFFECTED already exists and has explicit mergeinfo
+ describing merges from PATH *after* LOG_ENTRY->REVISION, then
+ ignore this PATH. If it was deleted in LOG_ENTRY->REVISION it's
+ obviously back. If it was added or replaced it's still around
+ possibly it was replaced one or more times, but it's back now.
+ Regardless, LOG_ENTRY->REVISION is *not* an eligible revision! */
+ if (ancestor_is_self /* Explicit mergeinfo on TARGET_PATH_AFFECTED */
+ && (change->action != 'M'))
+ {
+ svn_rangelist_t *rangelist =
+ svn_hash_gets(nearest_ancestor_mergeinfo, path);
+ svn_merge_range_t *youngest_range = APR_ARRAY_IDX(
+ rangelist, rangelist->nelts - 1, svn_merge_range_t *);
+
+ if (youngest_range
+ && (youngest_range->end > log_entry->revision))
+ continue;
+ }
+
+ if (nearest_ancestor_mergeinfo)
+ {
+ apr_hash_index_t *hi2;
+
+ for (hi2 = apr_hash_first(iterpool, nearest_ancestor_mergeinfo);
+ hi2;
+ hi2 = apr_hash_next(hi2))
+ {
+ const char *mergeinfo_path = svn__apr_hash_index_key(hi2);
+ svn_rangelist_t *rangelist = svn__apr_hash_index_val(hi2);
+
+ /* Does the mergeinfo for PATH reflect if
+ LOG_ENTRY->REVISION was previously merged
+ from MERGE_SOURCE_FSPATH? */
+ if (svn_fspath__skip_ancestor(merge_source_fspath,
+ mergeinfo_path))
+ {
+ /* Something was merged from MERGE_SOURCE_FSPATH, does
+ it include LOG_ENTRY->REVISION? */
+ SVN_ERR(svn_rangelist_intersect(&intersection,
+ rangelist,
+ this_rev_rangelist,
+ FALSE,
+ iterpool));
+ if (intersection->nelts)
+ {
+ if (ancestor_is_self)
+ {
+ /* TARGET_PATH_AFFECTED has explicit mergeinfo,
+ so we don't need to worry if that mergeinfo
+ is inheritable or not. */
+ found_this_revision = TRUE;
+ break;
+ }
+ else
+ {
+ /* TARGET_PATH_AFFECTED inherited its mergeinfo,
+ so we have to ignore non-inheritable
+ ranges. */
+ SVN_ERR(svn_rangelist_intersect(
+ &intersection,
+ rangelist,
+ this_rev_rangelist,
+ TRUE, iterpool));
+ if (intersection->nelts)
+ {
+ found_this_revision = TRUE;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!found_this_revision)
+ {
+ /* As soon as any PATH is found that is not fully merged for
+ LOG_ENTRY->REVISION then we can stop. */
+ all_subtrees_have_this_rev = FALSE;
+ break;
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+
+ if (all_subtrees_have_this_rev)
+ {
+ if (fleb->filtering_merged)
+ log_entry->non_inheritable = FALSE;
+ else
+ return SVN_NO_ERROR;
+ }
+ }
+
+ /* Call the wrapped log receiver which this function is filtering for. */
+ return fleb->log_receiver(fleb->log_receiver_baton, log_entry, pool);
+}
+
+static svn_error_t *
+logs_for_mergeinfo_rangelist(const char *source_url,
+ const apr_array_header_t *merge_source_fspaths,
+ svn_boolean_t filtering_merged,
+ const svn_rangelist_t *rangelist,
+ svn_boolean_t oldest_revs_first,
+ svn_mergeinfo_catalog_t target_mergeinfo_catalog,
+ const char *target_fspath,
+ svn_boolean_t discover_changed_paths,
+ const apr_array_header_t *revprops,
+ svn_log_entry_receiver_t log_receiver,
+ void *log_receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *target;
+ svn_merge_range_t *oldest_range, *youngest_range;
+ apr_array_header_t *revision_ranges;
+ svn_opt_revision_t oldest_rev, youngest_rev;
+ struct filter_log_entry_baton_t fleb;
+
+ if (! rangelist->nelts)
+ return SVN_NO_ERROR;
+
+ /* Sort the rangelist. */
+ qsort(rangelist->elts, rangelist->nelts,
+ rangelist->elt_size, svn_sort_compare_ranges);
+
+ /* Build a single-member log target list using SOURCE_URL. */
+ target = apr_array_make(scratch_pool, 1, sizeof(const char *));
+ APR_ARRAY_PUSH(target, const char *) = source_url;
+
+ /* Calculate and construct the bounds of our log request. */
+ youngest_range = APR_ARRAY_IDX(rangelist, rangelist->nelts - 1,
+ svn_merge_range_t *);
+ youngest_rev.kind = svn_opt_revision_number;
+ youngest_rev.value.number = youngest_range->end;
+ oldest_range = APR_ARRAY_IDX(rangelist, 0, svn_merge_range_t *);
+ oldest_rev.kind = svn_opt_revision_number;
+ oldest_rev.value.number = oldest_range->start;
+
+ if (! target_mergeinfo_catalog)
+ target_mergeinfo_catalog = apr_hash_make(scratch_pool);
+
+ /* FILTER_LOG_ENTRY_BATON_T->TARGET_MERGEINFO_CATALOG's keys are required
+ to be repository-absolute. */
+ SVN_ERR(svn_mergeinfo__add_prefix_to_catalog(&target_mergeinfo_catalog,
+ target_mergeinfo_catalog, "/",
+ scratch_pool, scratch_pool));
+
+ /* Build the log filtering callback baton. */
+ fleb.filtering_merged = filtering_merged;
+ fleb.merge_source_fspaths = merge_source_fspaths;
+ fleb.target_mergeinfo_catalog = target_mergeinfo_catalog;
+ fleb.depth_first_catalog_index =
+ svn_sort__hash(target_mergeinfo_catalog,
+ svn_sort_compare_items_as_paths,
+ scratch_pool);
+ fleb.target_fspath = target_fspath;
+ fleb.rangelist = rangelist;
+ fleb.log_receiver = log_receiver;
+ fleb.log_receiver_baton = log_receiver_baton;
+ fleb.ctx = ctx;
+
+ /* Drive the log. */
+ revision_ranges = apr_array_make(scratch_pool, 1,
+ sizeof(svn_opt_revision_range_t *));
+ if (oldest_revs_first)
+ APR_ARRAY_PUSH(revision_ranges, svn_opt_revision_range_t *)
+ = svn_opt__revision_range_create(&oldest_rev, &youngest_rev, scratch_pool);
+ else
+ APR_ARRAY_PUSH(revision_ranges, svn_opt_revision_range_t *)
+ = svn_opt__revision_range_create(&youngest_rev, &oldest_rev, scratch_pool);
+ SVN_ERR(svn_client_log5(target, &youngest_rev, revision_ranges,
+ 0, discover_changed_paths, FALSE, FALSE, revprops,
+ filter_log_entry_with_rangelist, &fleb, ctx,
+ scratch_pool));
+
+ /* Check for cancellation. */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ return SVN_NO_ERROR;
+}
+
+/* Set *OUT_MERGEINFO to a shallow copy of MERGEINFO with each source path
+ converted to a (URI-encoded) URL based on REPOS_ROOT_URL. *OUT_MERGEINFO
+ is declared as 'apr_hash_t *' because its key do not obey the rules of
+ 'svn_mergeinfo_t'.
+
+ Allocate *OUT_MERGEINFO and the new keys in RESULT_POOL. Use
+ SCRATCH_POOL for any temporary allocations. */
+static svn_error_t *
+mergeinfo_relpaths_to_urls(apr_hash_t **out_mergeinfo,
+ svn_mergeinfo_t mergeinfo,
+ const char *repos_root_url,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ *out_mergeinfo = NULL;
+ if (mergeinfo)
+ {
+ apr_hash_index_t *hi;
+ apr_hash_t *full_path_mergeinfo = apr_hash_make(result_pool);
+
+ for (hi = apr_hash_first(scratch_pool, mergeinfo);
+ hi; hi = apr_hash_next(hi))
+ {
+ const char *key = svn__apr_hash_index_key(hi);
+ void *val = svn__apr_hash_index_val(hi);
+
+ svn_hash_sets(full_path_mergeinfo,
+ svn_path_url_add_component2(repos_root_url, key + 1,
+ result_pool),
+ val);
+ }
+ *out_mergeinfo = full_path_mergeinfo;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/*** Public APIs ***/
+
+svn_error_t *
+svn_client_mergeinfo_get_merged(apr_hash_t **mergeinfo_p,
+ const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *repos_root;
+ svn_mergeinfo_catalog_t mergeinfo_cat;
+ svn_mergeinfo_t mergeinfo;
+
+ SVN_ERR(get_mergeinfo(&mergeinfo_cat, &repos_root, path_or_url,
+ peg_revision, FALSE, FALSE, ctx, pool, pool));
+ if (mergeinfo_cat)
+ {
+ const char *repos_relpath;
+
+ if (! svn_path_is_url(path_or_url))
+ {
+ SVN_ERR(svn_dirent_get_absolute(&path_or_url, path_or_url, pool));
+ SVN_ERR(svn_wc__node_get_repos_info(NULL, &repos_relpath, NULL, NULL,
+ ctx->wc_ctx, path_or_url,
+ pool, pool));
+ }
+ else
+ {
+ repos_relpath = svn_uri_skip_ancestor(repos_root, path_or_url, pool);
+
+ SVN_ERR_ASSERT(repos_relpath != NULL); /* Or get_mergeinfo failed */
+ }
+
+ mergeinfo = svn_hash_gets(mergeinfo_cat, repos_relpath);
+ }
+ else
+ {
+ mergeinfo = NULL;
+ }
+
+ SVN_ERR(mergeinfo_relpaths_to_urls(mergeinfo_p, mergeinfo,
+ repos_root, pool, pool));
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client_mergeinfo_log2(svn_boolean_t finding_merged,
+ const char *target_path_or_url,
+ const svn_opt_revision_t *target_peg_revision,
+ const char *source_path_or_url,
+ const svn_opt_revision_t *source_peg_revision,
+ const svn_opt_revision_t *source_start_revision,
+ const svn_opt_revision_t *source_end_revision,
+ svn_log_entry_receiver_t log_receiver,
+ void *log_receiver_baton,
+ svn_boolean_t discover_changed_paths,
+ svn_depth_t depth,
+ const apr_array_header_t *revprops,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ const char *log_target = NULL;
+ const char *repos_root;
+ const char *target_repos_relpath;
+ svn_mergeinfo_catalog_t target_mergeinfo_cat;
+
+ /* A hash of paths, at or under TARGET_PATH_OR_URL, mapped to
+ rangelists. Not technically mergeinfo, so not using the
+ svn_mergeinfo_t type. */
+ apr_hash_t *inheritable_subtree_merges;
+
+ svn_mergeinfo_t source_history;
+ svn_mergeinfo_t target_history;
+ svn_rangelist_t *master_noninheritable_rangelist;
+ svn_rangelist_t *master_inheritable_rangelist;
+ apr_array_header_t *merge_source_fspaths =
+ apr_array_make(scratch_pool, 1, sizeof(const char *));
+ apr_hash_index_t *hi_catalog;
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool;
+ svn_boolean_t oldest_revs_first = TRUE;
+
+ /* We currently only support depth = empty | infinity. */
+ if (depth != svn_depth_infinity && depth != svn_depth_empty)
+ return svn_error_create(
+ SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Only depths 'infinity' and 'empty' are currently supported"));
+
+ /* Validate and sanitize the incoming source operative revision range. */
+ if (!((source_start_revision->kind == svn_opt_revision_unspecified) ||
+ (source_start_revision->kind == svn_opt_revision_number) ||
+ (source_start_revision->kind == svn_opt_revision_date) ||
+ (source_start_revision->kind == svn_opt_revision_head)))
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
+ if (!((source_end_revision->kind == svn_opt_revision_unspecified) ||
+ (source_end_revision->kind == svn_opt_revision_number) ||
+ (source_end_revision->kind == svn_opt_revision_date) ||
+ (source_end_revision->kind == svn_opt_revision_head)))
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
+ if ((source_end_revision->kind != svn_opt_revision_unspecified)
+ && (source_start_revision->kind == svn_opt_revision_unspecified))
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
+ if ((source_end_revision->kind == svn_opt_revision_unspecified)
+ && (source_start_revision->kind != svn_opt_revision_unspecified))
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
+
+ /* We need the union of TARGET_PATH_OR_URL@TARGET_PEG_REVISION's mergeinfo
+ and MERGE_SOURCE_URL's history. It's not enough to do path
+ matching, because renames in the history of MERGE_SOURCE_URL
+ throw that all in a tizzy. Of course, if there's no mergeinfo on
+ the target, that vastly simplifies matters (we'll have nothing to
+ do). */
+ /* This get_mergeinfo() call doubles as a mergeinfo capabilities check. */
+ SVN_ERR(get_mergeinfo(&target_mergeinfo_cat, &repos_root,
+ target_path_or_url, target_peg_revision,
+ depth == svn_depth_infinity, TRUE,
+ ctx, scratch_pool, scratch_pool));
+
+ if (!svn_path_is_url(target_path_or_url))
+ {
+ SVN_ERR(svn_dirent_get_absolute(&target_path_or_url,
+ target_path_or_url, scratch_pool));
+ SVN_ERR(svn_wc__node_get_repos_info(NULL, &target_repos_relpath,
+ NULL, NULL,
+ ctx->wc_ctx, target_path_or_url,
+ scratch_pool, scratch_pool));
+ }
+ else
+ {
+ target_repos_relpath = svn_uri_skip_ancestor(repos_root,
+ target_path_or_url,
+ scratch_pool);
+
+ /* TARGET_REPOS_REL should be non-NULL, else get_mergeinfo
+ should have failed. */
+ SVN_ERR_ASSERT(target_repos_relpath != NULL);
+ }
+
+ if (!target_mergeinfo_cat)
+ {
+ /* If we are looking for what has been merged and there is no
+ mergeinfo then we already know the answer. If we are looking
+ for eligible revisions then create a catalog with empty mergeinfo
+ on the target. This is semantically equivalent to no mergeinfo
+ and gives us something to combine with MERGE_SOURCE_URL's
+ history. */
+ if (finding_merged)
+ {
+ return SVN_NO_ERROR;
+ }
+ else
+ {
+ target_mergeinfo_cat = apr_hash_make(scratch_pool);
+ svn_hash_sets(target_mergeinfo_cat, target_repos_relpath,
+ apr_hash_make(scratch_pool));
+ }
+ }
+
+ /* Fetch the location history as mergeinfo, for the source branch
+ * (between the given start and end revisions), and, if we're finding
+ * merged revisions, then also for the entire target branch.
+ *
+ * ### TODO: As the source and target must be in the same repository, we
+ * should share a single session, tracking the two URLs separately. */
+ {
+ apr_pool_t *sesspool = svn_pool_create(scratch_pool);
+ svn_ra_session_t *source_session, *target_session;
+ svn_client__pathrev_t *pathrev;
+ svn_revnum_t start_rev, end_rev, youngest_rev = SVN_INVALID_REVNUM;
+
+ if (! finding_merged)
+ {
+ SVN_ERR(svn_client__ra_session_from_path2(&target_session, &pathrev,
+ target_path_or_url, NULL,
+ target_peg_revision,
+ target_peg_revision,
+ ctx, sesspool));
+ SVN_ERR(svn_client__get_history_as_mergeinfo(&target_history, NULL,
+ pathrev,
+ SVN_INVALID_REVNUM,
+ SVN_INVALID_REVNUM,
+ target_session, ctx,
+ scratch_pool));
+ }
+
+ SVN_ERR(svn_client__ra_session_from_path2(&source_session, &pathrev,
+ source_path_or_url, NULL,
+ source_peg_revision,
+ source_peg_revision,
+ ctx, sesspool));
+ SVN_ERR(svn_client__get_revision_number(&start_rev, &youngest_rev,
+ ctx->wc_ctx, source_path_or_url,
+ source_session,
+ source_start_revision,
+ sesspool));
+ SVN_ERR(svn_client__get_revision_number(&end_rev, &youngest_rev,
+ ctx->wc_ctx, source_path_or_url,
+ source_session,
+ source_end_revision,
+ sesspool));
+ SVN_ERR(svn_client__get_history_as_mergeinfo(&source_history, NULL,
+ pathrev,
+ MAX(end_rev, start_rev),
+ MIN(end_rev, start_rev),
+ source_session, ctx,
+ scratch_pool));
+ if (start_rev > end_rev)
+ oldest_revs_first = FALSE;
+
+ /* Close the source and target sessions. */
+ svn_pool_destroy(sesspool);
+ }
+
+ /* Separate the explicit or inherited mergeinfo on TARGET_PATH_OR_URL,
+ and possibly its explicit subtree mergeinfo, into their
+ inheritable and non-inheritable parts. */
+ master_noninheritable_rangelist = apr_array_make(scratch_pool, 64,
+ sizeof(svn_merge_range_t *));
+ master_inheritable_rangelist = apr_array_make(scratch_pool, 64,
+ sizeof(svn_merge_range_t *));
+ inheritable_subtree_merges = apr_hash_make(scratch_pool);
+
+ iterpool = svn_pool_create(scratch_pool);
+
+ for (hi_catalog = apr_hash_first(scratch_pool, target_mergeinfo_cat);
+ hi_catalog;
+ hi_catalog = apr_hash_next(hi_catalog))
+ {
+ svn_mergeinfo_t subtree_mergeinfo = svn__apr_hash_index_val(hi_catalog);
+ svn_mergeinfo_t subtree_history;
+ svn_mergeinfo_t subtree_source_history;
+ svn_mergeinfo_t subtree_inheritable_mergeinfo;
+ svn_mergeinfo_t subtree_noninheritable_mergeinfo;
+ svn_mergeinfo_t merged_noninheritable;
+ svn_mergeinfo_t merged;
+ const char *subtree_path = svn__apr_hash_index_key(hi_catalog);
+ svn_boolean_t is_subtree = strcmp(subtree_path,
+ target_repos_relpath) != 0;
+ svn_pool_clear(iterpool);
+
+ if (is_subtree)
+ {
+ /* If SUBTREE_PATH is a proper subtree of TARGET_PATH_OR_URL
+ then make a copy of SOURCE_HISTORY that is path adjusted
+ for the subtree. */
+ const char *subtree_rel_path =
+ subtree_path + strlen(target_repos_relpath) + 1;
+
+ SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(
+ &subtree_source_history, source_history,
+ subtree_rel_path, scratch_pool, scratch_pool));
+
+ if (!finding_merged)
+ SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(
+ &subtree_history, target_history,
+ subtree_rel_path, scratch_pool, scratch_pool));
+ }
+ else
+ {
+ subtree_source_history = source_history;
+ if (!finding_merged)
+ subtree_history = target_history;
+ }
+
+ if (!finding_merged)
+ {
+ svn_mergeinfo_t merged_via_history;
+ SVN_ERR(svn_mergeinfo_intersect2(&merged_via_history,
+ subtree_history,
+ subtree_source_history, TRUE,
+ scratch_pool, iterpool));
+ SVN_ERR(svn_mergeinfo_merge2(subtree_mergeinfo,
+ merged_via_history,
+ scratch_pool, scratch_pool));
+ }
+
+ SVN_ERR(svn_mergeinfo_inheritable2(&subtree_inheritable_mergeinfo,
+ subtree_mergeinfo, NULL,
+ SVN_INVALID_REVNUM,
+ SVN_INVALID_REVNUM,
+ TRUE, scratch_pool, iterpool));
+ SVN_ERR(svn_mergeinfo_inheritable2(&subtree_noninheritable_mergeinfo,
+ subtree_mergeinfo, NULL,
+ SVN_INVALID_REVNUM,
+ SVN_INVALID_REVNUM,
+ FALSE, scratch_pool, iterpool));
+
+ /* Find the intersection of the non-inheritable part of
+ SUBTREE_MERGEINFO and SOURCE_HISTORY. svn_mergeinfo_intersect2()
+ won't consider non-inheritable and inheritable ranges
+ intersecting unless we ignore inheritance, but in doing so the
+ resulting intersections have all inheritable ranges. To get
+ around this we set the inheritance on the result to all
+ non-inheritable. */
+ SVN_ERR(svn_mergeinfo_intersect2(&merged_noninheritable,
+ subtree_noninheritable_mergeinfo,
+ subtree_source_history, FALSE,
+ scratch_pool, iterpool));
+ svn_mergeinfo__set_inheritance(merged_noninheritable, FALSE,
+ scratch_pool);
+
+ /* Keep track of all ranges partially merged to any and all
+ subtrees. */
+ SVN_ERR(svn_rangelist__merge_many(master_noninheritable_rangelist,
+ merged_noninheritable,
+ scratch_pool, iterpool));
+
+ /* Find the intersection of the inheritable part of TGT_MERGEINFO
+ and SOURCE_HISTORY. */
+ SVN_ERR(svn_mergeinfo_intersect2(&merged,
+ subtree_inheritable_mergeinfo,
+ subtree_source_history, FALSE,
+ scratch_pool, iterpool));
+
+ /* Keep track of all ranges fully merged to any and all
+ subtrees. */
+ if (apr_hash_count(merged))
+ {
+ /* The inheritable rangelist merged from SUBTREE_SOURCE_HISTORY
+ to SUBTREE_PATH. */
+ svn_rangelist_t *subtree_merged_rangelist =
+ apr_array_make(scratch_pool, 1, sizeof(svn_merge_range_t *));
+
+ SVN_ERR(svn_rangelist__merge_many(master_inheritable_rangelist,
+ merged, scratch_pool, iterpool));
+ SVN_ERR(svn_rangelist__merge_many(subtree_merged_rangelist,
+ merged, scratch_pool, iterpool));
+
+ svn_hash_sets(inheritable_subtree_merges, subtree_path,
+ subtree_merged_rangelist);
+ }
+ else
+ {
+ /* Map SUBTREE_PATH to an empty rangelist if there was nothing
+ fully merged. e.g. Only empty or non-inheritable mergeinfo
+ on the subtree or mergeinfo unrelated to the source. */
+ svn_hash_sets(inheritable_subtree_merges, subtree_path,
+ apr_array_make(scratch_pool, 0,
+ sizeof(svn_merge_range_t *)));
+ }
+ }
+
+ /* Make sure every range in MASTER_INHERITABLE_RANGELIST is fully merged to
+ each subtree (including the target itself). Any revisions which don't
+ exist in *every* subtree are *potentially* only partially merged to the
+ tree rooted at TARGET_PATH_OR_URL, so move those revisions to
+ MASTER_NONINHERITABLE_RANGELIST. It may turn out that that a revision
+ was merged to the only subtree it affects, but we need to examine the
+ logs to make this determination (which will be done by
+ logs_for_mergeinfo_rangelist). */
+ if (master_inheritable_rangelist->nelts)
+ {
+ for (hi = apr_hash_first(scratch_pool, inheritable_subtree_merges);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ svn_rangelist_t *deleted_rangelist;
+ svn_rangelist_t *added_rangelist;
+ svn_rangelist_t *subtree_merged_rangelist =
+ svn__apr_hash_index_val(hi);
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_rangelist_diff(&deleted_rangelist, &added_rangelist,
+ master_inheritable_rangelist,
+ subtree_merged_rangelist, TRUE,
+ iterpool));
+
+ if (deleted_rangelist->nelts)
+ {
+ svn_rangelist__set_inheritance(deleted_rangelist, FALSE);
+ SVN_ERR(svn_rangelist_merge2(master_noninheritable_rangelist,
+ deleted_rangelist,
+ scratch_pool, iterpool));
+ SVN_ERR(svn_rangelist_remove(&master_inheritable_rangelist,
+ deleted_rangelist,
+ master_inheritable_rangelist,
+ FALSE,
+ scratch_pool));
+ }
+ }
+ }
+
+ if (finding_merged)
+ {
+ /* Roll all the merged revisions into one rangelist. */
+ SVN_ERR(svn_rangelist_merge2(master_inheritable_rangelist,
+ master_noninheritable_rangelist,
+ scratch_pool, scratch_pool));
+
+ }
+ else
+ {
+ /* Create the starting rangelist for what might be eligible. */
+ svn_rangelist_t *source_master_rangelist =
+ apr_array_make(scratch_pool, 1, sizeof(svn_merge_range_t *));
+
+ SVN_ERR(svn_rangelist__merge_many(source_master_rangelist,
+ source_history,
+ scratch_pool, scratch_pool));
+
+ /* From what might be eligible subtract what we know is
+ partially merged and then merge that back. */
+ SVN_ERR(svn_rangelist_remove(&source_master_rangelist,
+ master_noninheritable_rangelist,
+ source_master_rangelist,
+ FALSE, scratch_pool));
+ SVN_ERR(svn_rangelist_merge2(source_master_rangelist,
+ master_noninheritable_rangelist,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_rangelist_remove(&master_inheritable_rangelist,
+ master_inheritable_rangelist,
+ source_master_rangelist,
+ TRUE, scratch_pool));
+ }
+
+ /* Nothing merged? Not even when considering shared history if
+ looking for eligible revisions (i.e. !FINDING_MERGED)? Then there
+ is nothing more to do. */
+ if (! master_inheritable_rangelist->nelts)
+ {
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+ }
+ else
+ {
+ /* Determine the correct (youngest) target for 'svn log'. */
+ svn_merge_range_t *youngest_range
+ = APR_ARRAY_IDX(master_inheritable_rangelist,
+ master_inheritable_rangelist->nelts - 1,
+ svn_merge_range_t *);
+ svn_rangelist_t *youngest_rangelist =
+ svn_rangelist__initialize(youngest_range->end - 1,
+ youngest_range->end,
+ youngest_range->inheritable,
+ scratch_pool);;
+
+ for (hi = apr_hash_first(scratch_pool, source_history);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *key = svn__apr_hash_index_key(hi);
+ svn_rangelist_t *subtree_merged_rangelist =
+ svn__apr_hash_index_val(hi);
+ svn_rangelist_t *intersecting_rangelist;
+
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_rangelist_intersect(&intersecting_rangelist,
+ youngest_rangelist,
+ subtree_merged_rangelist,
+ FALSE, iterpool));
+
+ APR_ARRAY_PUSH(merge_source_fspaths, const char *) = key;
+
+ if (intersecting_rangelist->nelts)
+ log_target = key;
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+
+ /* Step 4: Finally, we run 'svn log' to drive our log receiver, but
+ using a receiver filter to only allow revisions to pass through
+ that are in our rangelist. */
+ log_target = svn_path_url_add_component2(repos_root, log_target + 1,
+ scratch_pool);
+
+ SVN_ERR(logs_for_mergeinfo_rangelist(log_target, merge_source_fspaths,
+ finding_merged,
+ master_inheritable_rangelist,
+ oldest_revs_first,
+ target_mergeinfo_cat,
+ svn_fspath__join("/",
+ target_repos_relpath,
+ scratch_pool),
+ discover_changed_paths,
+ revprops,
+ log_receiver, log_receiver_baton,
+ ctx, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_suggest_merge_sources(apr_array_header_t **suggestions,
+ const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *repos_root;
+ const char *copyfrom_path;
+ apr_array_header_t *list;
+ svn_revnum_t copyfrom_rev;
+ svn_mergeinfo_catalog_t mergeinfo_cat;
+ svn_mergeinfo_t mergeinfo;
+ apr_hash_index_t *hi;
+
+ list = apr_array_make(pool, 1, sizeof(const char *));
+
+ /* In our ideal algorithm, the list of recommendations should be
+ ordered by:
+
+ 1. The most recent existing merge source.
+ 2. The copyfrom source (which will also be listed as a merge
+ source if the copy was made with a 1.5+ client and server).
+ 3. All other merge sources, most recent to least recent.
+
+ However, determining the order of application of merge sources
+ requires a new RA API. Until such an API is available, our
+ algorithm will be:
+
+ 1. The copyfrom source.
+ 2. All remaining merge sources (unordered).
+ */
+
+ /* ### TODO: Share ra_session batons to improve efficiency? */
+ SVN_ERR(get_mergeinfo(&mergeinfo_cat, &repos_root, path_or_url,
+ peg_revision, FALSE, FALSE, ctx, pool, pool));
+
+ if (mergeinfo_cat && apr_hash_count(mergeinfo_cat))
+ {
+ /* We asked only for the PATH_OR_URL's mergeinfo, not any of its
+ descendants. So if there is anything in the catalog it is the
+ mergeinfo for PATH_OR_URL. */
+ mergeinfo = svn__apr_hash_index_val(apr_hash_first(pool, mergeinfo_cat));
+ }
+ else
+ {
+ mergeinfo = NULL;
+ }
+
+ SVN_ERR(svn_client__get_copy_source(&copyfrom_path, &copyfrom_rev,
+ path_or_url, peg_revision, ctx,
+ pool, pool));
+ if (copyfrom_path)
+ {
+ APR_ARRAY_PUSH(list, const char *) =
+ svn_path_url_add_component2(repos_root, copyfrom_path, pool);
+ }
+
+ if (mergeinfo)
+ {
+ for (hi = apr_hash_first(pool, mergeinfo); hi; hi = apr_hash_next(hi))
+ {
+ const char *rel_path = svn__apr_hash_index_key(hi);
+
+ if (copyfrom_path == NULL || strcmp(rel_path, copyfrom_path) != 0)
+ APR_ARRAY_PUSH(list, const char *) = \
+ svn_path_url_add_component2(repos_root, rel_path + 1, pool);
+ }
+ }
+
+ *suggestions = list;
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__mergeinfo_status(svn_boolean_t *mergeinfo_changes,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *propchanges;
+ int i;
+
+ *mergeinfo_changes = FALSE;
+
+ SVN_ERR(svn_wc_get_prop_diffs2(&propchanges, NULL, wc_ctx,
+ local_abspath, scratch_pool, scratch_pool));
+
+ for (i = 0; i < propchanges->nelts; i++)
+ {
+ svn_prop_t prop = APR_ARRAY_IDX(propchanges, i, svn_prop_t);
+ if (strcmp(prop.name, SVN_PROP_MERGEINFO) == 0)
+ {
+ *mergeinfo_changes = TRUE;
+ break;
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/mergeinfo.h b/subversion/libsvn_client/mergeinfo.h
new file mode 100644
index 0000000..0c4cf05
--- /dev/null
+++ b/subversion/libsvn_client/mergeinfo.h
@@ -0,0 +1,414 @@
+/*
+ * mergeinfo.h : Client library-internal mergeinfo APIs.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+#ifndef SVN_LIBSVN_CLIENT_MERGEINFO_H
+#define SVN_LIBSVN_CLIENT_MERGEINFO_H
+
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "private/svn_client_private.h"
+
+
+/*** Data Structures ***/
+
+
+/* Structure to store information about working copy paths that need special
+ consideration during a mergeinfo aware merge -- See the
+ 'THE CHILDREN_WITH_MERGEINFO ARRAY' meta comment and the doc string for the
+ function get_mergeinfo_paths() in libsvn_client/merge.c.
+*/
+typedef struct svn_client__merge_path_t
+{
+ const char *abspath; /* Absolute working copy path. */
+ svn_boolean_t missing_child; /* ABSPATH has an immediate child which
+ is missing, but is not switched. */
+ svn_boolean_t switched_child; /* ABSPATH has an immediate child which
+ is switched. */
+ svn_boolean_t switched; /* ABSPATH is switched. */
+ svn_boolean_t has_noninheritable; /* ABSPATH has svn:mergeinfo set on it
+ which includes non-inheritable
+ revision ranges. */
+ svn_boolean_t absent; /* ABSPATH is absent from the WC,
+ probably due to authz
+ restrictions. */
+
+ svn_boolean_t child_of_noninheritable; /* ABSPATH has no explicit mergeinfo
+ itself but is the child of a
+ path with noniheritable
+ mergeinfo. */
+
+ /* The remaining ranges to be merged to ABSPATH. When describing a forward
+ merge this rangelist adheres to the rules for rangelists described in
+ svn_mergeinfo.h. However, when describing reverse merges this
+ rangelist can contain reverse merge ranges that are not sorted per
+ svn_sort_compare_ranges(), but rather are sorted such that the ranges
+ with the youngest start revisions come first. In both the forward and
+ reverse merge cases the ranges should never overlap. This rangelist
+ may be empty but should never be NULL unless ABSENT is true. */
+ svn_rangelist_t *remaining_ranges;
+
+ svn_mergeinfo_t pre_merge_mergeinfo; /* Explicit or inherited mergeinfo
+ on ABSPATH prior to a merge.
+ May be NULL. */
+ svn_mergeinfo_t implicit_mergeinfo; /* Implicit mergeinfo on ABSPATH
+ prior to a merge. May be NULL. */
+ svn_boolean_t inherited_mergeinfo; /* Whether PRE_MERGE_MERGEINFO was
+ explicit or inherited. */
+ svn_boolean_t scheduled_for_deletion; /* ABSPATH is scheduled for
+ deletion. */
+ svn_boolean_t immediate_child_dir; /* ABSPATH is an immediate child
+ directory of the merge target,
+ has no explicit mergeinfo prior
+ to the merge, and the operational
+ depth of the merge is
+ svn_depth_immediates. */
+ svn_boolean_t record_mergeinfo; /* Mergeinfo needs to be recorded
+ on ABSPATH to describe the
+ merge. */
+ svn_boolean_t record_noninheritable; /* Non-inheritable mergeinfo needs to
+ be recorded on ABSPATH to describe
+ the merge. Implies RECORD_MERGEINFO
+ is true. */
+} svn_client__merge_path_t;
+
+/* Return a deep copy of the merge-path structure OLD, allocated in POOL. */
+svn_client__merge_path_t *
+svn_client__merge_path_dup(const svn_client__merge_path_t *old,
+ apr_pool_t *pool);
+
+/* Create a new merge path structure, allocated in POOL. Initialize the
+ * 'abspath' member to a deep copy of ABSPATH and all other fields to zero
+ * bytes. */
+svn_client__merge_path_t *
+svn_client__merge_path_create(const char *abspath,
+ apr_pool_t *pool);
+
+
+
+/*** Functions ***/
+
+/* Find explicit or inherited WC mergeinfo for LOCAL_ABSPATH, and return it
+ in *MERGEINFO (NULL if no mergeinfo is set). Set *INHERITED to
+ whether the mergeinfo was inherited (TRUE or FALSE), if INHERITED is
+ non-null.
+
+ This function will search for inherited mergeinfo in the parents of
+ LOCAL_ABSPATH only if the base revision of LOCAL_ABSPATH falls within
+ the range of the parent's last committed revision to the parent's base
+ revision (inclusive) or is LOCAL_ABSPATH is a local addition. If asking
+ for the inherited mergeinfo of an added path (i.e. one with no base
+ revision), that path may inherit mergeinfo from its nearest parent
+ with a base revision and explicit mergeinfo.
+
+ INHERIT indicates whether explicit, explicit or inherited, or only
+ inherited mergeinfo for LOCAL_ABSPATH is retrieved.
+
+ Don't look for inherited mergeinfo any higher than LIMIT_ABSPATH
+ (ignored if NULL) or beyond any switched path.
+
+ Set *WALKED_PATH to the path climbed from LOCAL_ABSPATH to find inherited
+ mergeinfo, or "" if none was found. (ignored if NULL).
+
+ If IGNORE_INVALID_MERGEINFO is true, then syntactically invalid explicit
+ mergeinfo on found on LOCAL_ABSPATH is ignored and *MERGEINFO is set to an
+ empty hash. If IGNORE_INVALID_MERGEINFO is false, then syntactically
+ invalid explicit mergeinfo on found on LOCAL_ABSPATH results in a
+ SVN_ERR_MERGEINFO_PARSE_ERROR error. Regardless of
+ IGNORE_INVALID_MERGEINFO, if LOCAL_ABSPATH inherits invalid mergeinfo,
+ then *MERGEINFO is always set to an empty hash and no parse error is
+ raised. */
+svn_error_t *
+svn_client__get_wc_mergeinfo(svn_mergeinfo_t *mergeinfo,
+ svn_boolean_t *inherited,
+ svn_mergeinfo_inheritance_t inherit,
+ const char *local_abspath,
+ const char *limit_abspath,
+ const char **walked_path,
+ svn_boolean_t ignore_invalid_mergeinfo,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* If INCLUDE_DESCENDANTS is FALSE, behave exactly like
+ svn_client__get_wc_mergeinfo() except the mergeinfo for LOCAL_ABSPATH is
+ put in the mergeinfo catalog MERGEINFO_CAT, mapped from LOCAL_ABSPATH's
+ repository root-relative path.
+
+ If INCLUDE_DESCENDANTS is true, then any subtrees under LOCAL_ABSPATH with
+ explicit mergeinfo are also included in MERGEINFO_CAT and again the
+ keys are the repository root-relative paths of the subtrees. If no
+ mergeinfo is found, then *MERGEINFO_CAT is set to NULL. */
+svn_error_t *
+svn_client__get_wc_mergeinfo_catalog(svn_mergeinfo_catalog_t *mergeinfo_cat,
+ svn_boolean_t *inherited,
+ svn_boolean_t include_descendants,
+ svn_mergeinfo_inheritance_t inherit,
+ const char *local_abspath,
+ const char *limit_path,
+ const char **walked_path,
+ svn_boolean_t ignore_invalid_mergeinfo,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Obtain any mergeinfo for URL from the repository, and set
+ it in *TARGET_MERGEINFO.
+
+ INHERIT indicates whether explicit, explicit or inherited, or only
+ inherited mergeinfo for URL is obtained.
+
+ If URL does not exist at REV, SVN_ERR_FS_NOT_FOUND or
+ SVN_ERR_RA_DAV_REQUEST_FAILED is returned and *TARGET_MERGEINFO
+ is untouched.
+
+ If there is no mergeinfo available for URL, or if the server
+ doesn't support a mergeinfo capability and SQUELCH_INCAPABLE is
+ TRUE, set *TARGET_MERGEINFO to NULL. If the server doesn't support
+ a mergeinfo capability and SQUELCH_INCAPABLE is FALSE, return an
+ SVN_ERR_UNSUPPORTED_FEATURE error.
+
+ RA_SESSION is an open RA session to the repository in which URL lives;
+ it may be temporarily reparented by this function.
+*/
+svn_error_t *
+svn_client__get_repos_mergeinfo(svn_mergeinfo_t *target_mergeinfo,
+ svn_ra_session_t *ra_session,
+ const char *url,
+ svn_revnum_t rev,
+ svn_mergeinfo_inheritance_t inherit,
+ svn_boolean_t squelch_incapable,
+ apr_pool_t *pool);
+
+/* If INCLUDE_DESCENDANTS is FALSE, behave exactly like
+ svn_client__get_repos_mergeinfo() except the mergeinfo for URL
+ is put in the mergeinfo catalog MERGEINFO_CAT, with the key being
+ the repository root-relative path of URL.
+
+ If INCLUDE_DESCENDANTS is true, then any subtrees under URL
+ with explicit mergeinfo are also included in MERGEINFO_CAT. The
+ keys for the subtree mergeinfo are the repository root-relative
+ paths of the subtrees. If no mergeinfo is found, then
+ *TARGET_MERGEINFO_CAT is set to NULL. */
+svn_error_t *
+svn_client__get_repos_mergeinfo_catalog(svn_mergeinfo_catalog_t *mergeinfo_cat,
+ svn_ra_session_t *ra_session,
+ const char *url,
+ svn_revnum_t rev,
+ svn_mergeinfo_inheritance_t inherit,
+ svn_boolean_t squelch_incapable,
+ svn_boolean_t include_descendants,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Retrieve the direct mergeinfo for the TARGET_WCPATH from the WC's
+ mergeinfo prop, or that inherited from its nearest ancestor if the
+ target has no info of its own.
+
+ If no mergeinfo can be obtained from the WC or REPOS_ONLY is TRUE,
+ get it from the repository. If the repository is contacted for mergeinfo
+ and RA_SESSION does not point to TARGET_WCPATH's URL, then it is
+ temporarily reparented. If RA_SESSION is NULL, then a temporary session
+ is opened as needed.
+
+ Store any mergeinfo obtained for TARGET_WCPATH in
+ *TARGET_MERGEINFO, if no mergeinfo is found *TARGET_MERGEINFO is
+ NULL.
+
+ Like svn_client__get_wc_mergeinfo(), this function considers no
+ inherited mergeinfo to be found in the WC when trying to crawl into
+ a parent path with a different working revision.
+
+ INHERIT indicates whether explicit, explicit or inherited, or only
+ inherited mergeinfo for TARGET_WCPATH is retrieved.
+
+ If FROM_REPOS is not NULL, then set *FROM_REPOS to true if
+ *TARGET_MERGEINFO is inherited and the repository was contacted to
+ obtain it. Set *FROM_REPOS to false otherwise.
+
+ If TARGET_WCPATH inherited its mergeinfo from a working copy ancestor
+ or if it was obtained from the repository, set *INHERITED to TRUE, set it
+ to FALSE otherwise, if INHERITED is non-null. */
+svn_error_t *
+svn_client__get_wc_or_repos_mergeinfo(svn_mergeinfo_t *target_mergeinfo,
+ svn_boolean_t *inherited,
+ svn_boolean_t *from_repos,
+ svn_boolean_t repos_only,
+ svn_mergeinfo_inheritance_t inherit,
+ svn_ra_session_t *ra_session,
+ const char *target_wcpath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* If INCLUDE_DESCENDANTS is false then behaves exactly like
+ svn_client__get_wc_or_repos_mergeinfo() except the mergeinfo for
+ TARGET_WCPATH is put in the mergeinfo catalog
+ TARGET_MERGEINFO_CATALOG, mapped from TARGET_WCPATH's repository
+ root-relative path.
+
+ IGNORE_INVALID_MERGEINFO behaves as per the argument of the same
+ name to svn_client__get_wc_mergeinfo(). It is applicable only if
+ the mergeinfo for TARGET_WCPATH is obtained from the working copy.
+
+ If INCLUDE_DESCENDANTS is true, then any subtrees under
+ TARGET_WCPATH with explicit mergeinfo are also included in
+ TARGET_MERGEINFO_CATALOG and again the keys are the repository
+ root-relative paths of the subtrees. If no mergeinfo is found,
+ then *TARGET_MERGEINFO_CAT is set to NULL. */
+svn_error_t *
+svn_client__get_wc_or_repos_mergeinfo_catalog(
+ svn_mergeinfo_catalog_t *target_mergeinfo_catalog,
+ svn_boolean_t *inherited,
+ svn_boolean_t *from_repos,
+ svn_boolean_t include_descendants,
+ svn_boolean_t repos_only,
+ svn_boolean_t ignore_invalid_mergeinfo,
+ svn_mergeinfo_inheritance_t inherit,
+ svn_ra_session_t *ra_session,
+ const char *target_wcpath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Set *MERGEINFO_P to a mergeinfo constructed solely from the
+ natural history of PATHREV.
+
+ If RANGE_YOUNGEST and RANGE_OLDEST are valid, use them as inclusive
+ bounds on the revision ranges of returned mergeinfo. PATHREV->rev,
+ RANGE_YOUNGEST and RANGE_OLDEST are governed by the same rules as the
+ PEG_REVISION, START_REV, and END_REV parameters (respectively) of
+ svn_ra_get_location_segments().
+
+ If HAS_REV_ZERO_HISTORY is not NULL, then set *HAS_REV_ZERO_HISTORY to
+ TRUE if the natural history includes revision 0, else to FALSE.
+
+ RA_SESSION is an open RA session to the repository of PATHREV;
+ it may be temporarily reparented by this function.
+*/
+svn_error_t *
+svn_client__get_history_as_mergeinfo(svn_mergeinfo_t *mergeinfo_p,
+ svn_boolean_t *has_rev_zero_history,
+ const svn_client__pathrev_t *pathrev,
+ svn_revnum_t range_youngest,
+ svn_revnum_t range_oldest,
+ svn_ra_session_t *ra_session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* Parse any explicit mergeinfo on LOCAL_ABSPATH and store it in
+ *MERGEINFO. If no record of any mergeinfo exists, set *MERGEINFO to NULL.
+ Does not acount for inherited mergeinfo. */
+svn_error_t *
+svn_client__parse_mergeinfo(svn_mergeinfo_t *mergeinfo,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool);
+
+/* Write MERGEINFO into the WC for LOCAL_ABSPATH. If MERGEINFO is NULL,
+ remove any SVN_PROP_MERGEINFO for LOCAL_ABSPATH. If MERGEINFO is empty,
+ record an empty property value (e.g. ""). If CTX->NOTIFY_FUNC2 is
+ not null call it with notification type svn_wc_notify_merge_record_info
+ if DO_NOTIFICATION is true.
+
+ Use WC_CTX to access the working copy, and SCRATCH_POOL for any temporary
+ allocations. */
+svn_error_t *
+svn_client__record_wc_mergeinfo(const char *local_abspath,
+ svn_mergeinfo_t mergeinfo,
+ svn_boolean_t do_notification,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool);
+
+/* Write mergeinfo into the WC.
+ *
+ * For each path in RESULT_CATALOG, set the SVN_PROP_MERGEINFO
+ * property to represent the given mergeinfo, or remove the property
+ * if the given mergeinfo is null, and notify the change. Leave
+ * other paths unchanged. RESULT_CATALOG maps (const char *) WC paths
+ * to (svn_mergeinfo_t) mergeinfo. */
+svn_error_t *
+svn_client__record_wc_mergeinfo_catalog(apr_hash_t *result_catalog,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool);
+
+/* Elide any svn:mergeinfo set on TARGET_ABSPATH to its nearest working
+ copy (or possibly repository) ancestor with equivalent mergeinfo.
+
+ If WC_ELISION_LIMIT_ABSPATH is NULL check up to the root of the
+ working copy or the nearest switched parent for an elision
+ destination, if none is found check the repository, otherwise check
+ as far as WC_ELISION_LIMIT_ABSPATH within the working copy.
+ TARGET_WCPATH and WC_ELISION_LIMIT_ABSPATH, if it exists, must both be
+ absolute or relative to the working directory.
+
+ Elision occurs if:
+
+ A) TARGET_ABSPATH has empty mergeinfo and no parent path with
+ explicit mergeinfo can be found in either the WC or the
+ repository (WC_ELISION_LIMIT_PATH must be NULL for this to
+ occur).
+
+ B) TARGET_ABSPATH has empty mergeinfo and its nearest parent also
+ has empty mergeinfo.
+
+ C) TARGET_ABSPATH has the same mergeinfo as its nearest parent
+ when that parent's mergeinfo is adjusted for the path
+ difference between the two, e.g.:
+
+ TARGET_ABSPATH = A_COPY/D/H
+ TARGET_ABSPATH's mergeinfo = '/A/D/H:3'
+ TARGET_ABSPATH nearest parent = A_COPY
+ Parent's mergeinfo = '/A:3'
+ Path difference = 'D/H'
+ Parent's adjusted mergeinfo = '/A/D/H:3'
+
+ If Elision occurs remove the svn:mergeinfo property from
+ TARGET_ABSPATH. */
+svn_error_t *
+svn_client__elide_mergeinfo(const char *target_abspath,
+ const char *wc_elision_limit_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool);
+
+/* Simplify a mergeinfo catalog, if possible, via elision.
+
+ For each path in MERGEINFO_CATALOG, check if the path's mergeinfo can
+ elide to the path's nearest path-wise parent in MERGEINFO_CATALOG. If
+ so, remove that path from MERGEINFO_CATALOG. Elidability is determined
+ as per svn_client__elide_mergeinfo except that elision to the repository
+ is not considered.
+
+ SCRATCH_POOL is used for temporary allocations. */
+svn_error_t *
+svn_client__elide_mergeinfo_catalog(svn_mergeinfo_catalog_t mergeinfo_catalog,
+ apr_pool_t *scratch_pool);
+
+/* Set *MERGEINFO_CHANGES to TRUE if LOCAL_ABSPATH has locally modified
+ mergeinfo, set *MERGEINFO_CHANGES to FALSE otherwise. */
+svn_error_t *
+svn_client__mergeinfo_status(svn_boolean_t *mergeinfo_changes,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ apr_pool_t *scratch_pool);
+
+#endif /* SVN_LIBSVN_CLIENT_MERGEINFO_H */
diff --git a/subversion/libsvn_client/patch.c b/subversion/libsvn_client/patch.c
new file mode 100644
index 0000000..b965646
--- /dev/null
+++ b/subversion/libsvn_client/patch.c
@@ -0,0 +1,3043 @@
+/*
+ * patch.c: patch application support
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <apr_hash.h>
+#include <apr_fnmatch.h>
+#include "svn_client.h"
+#include "svn_dirent_uri.h"
+#include "svn_diff.h"
+#include "svn_hash.h"
+#include "svn_io.h"
+#include "svn_path.h"
+#include "svn_pools.h"
+#include "svn_props.h"
+#include "svn_sorts.h"
+#include "svn_subst.h"
+#include "svn_wc.h"
+#include "client.h"
+
+#include "svn_private_config.h"
+#include "private/svn_eol_private.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_dep_compat.h"
+#include "private/svn_string_private.h"
+#include "private/svn_subr_private.h"
+
+typedef struct hunk_info_t {
+ /* The hunk. */
+ svn_diff_hunk_t *hunk;
+
+ /* The line where the hunk matched in the target file. */
+ svn_linenum_t matched_line;
+
+ /* Whether this hunk has been rejected. */
+ svn_boolean_t rejected;
+
+ /* Whether this hunk has already been applied (either manually
+ * or by an earlier run of patch). */
+ svn_boolean_t already_applied;
+
+ /* The fuzz factor used when matching this hunk, i.e. how many
+ * lines of leading and trailing context to ignore during matching. */
+ svn_linenum_t fuzz;
+} hunk_info_t;
+
+/* A struct carrying information related to the patched and unpatched
+ * content of a target, be it a property or the text of a file. */
+typedef struct target_content_t {
+ /* Indicates whether unpatched content existed prior to patching. */
+ svn_boolean_t existed;
+
+ /* The line last read from the unpatched content. */
+ svn_linenum_t current_line;
+
+ /* The EOL-style of the unpatched content. Either 'none', 'fixed',
+ * or 'native'. See the documentation of svn_subst_eol_style_t. */
+ svn_subst_eol_style_t eol_style;
+
+ /* If the EOL_STYLE above is not 'none', this is the EOL string
+ * corresponding to the EOL-style. Else, it is the EOL string the
+ * last line read from the target file was using. */
+ const char *eol_str;
+
+ /* An array containing apr_off_t offsets marking the beginning of
+ * each line in the unpatched content. */
+ apr_array_header_t *lines;
+
+ /* An array containing hunk_info_t structures for hunks already matched. */
+ apr_array_header_t *hunks;
+
+ /* True if end-of-file was reached while reading from the unpatched
+ * content. */
+ svn_boolean_t eof;
+
+ /* The keywords of the target. They will be contracted when reading
+ * unpatched content and expanded when writing patched content.
+ * When patching properties this hash is always empty. */
+ apr_hash_t *keywords;
+
+ /* A callback, with an associated baton, to read a line of unpatched
+ * content. */
+ svn_error_t *(*readline)(void *baton, svn_stringbuf_t **line,
+ const char **eol_str, svn_boolean_t *eof,
+ apr_pool_t *result_pool, apr_pool_t *scratch_pool);
+ void *read_baton;
+
+ /* A callback to get the current byte offset within the unpatched
+ * content. Uses the read baton. */
+ svn_error_t * (*tell)(void *baton, apr_off_t *offset,
+ apr_pool_t *scratch_pool);
+
+ /* A callback to seek to an offset within the unpatched content.
+ * Uses the read baton. */
+ svn_error_t * (*seek)(void *baton, apr_off_t offset,
+ apr_pool_t *scratch_pool);
+
+ /* A callback to write data to the patched content, with an
+ * associated baton. */
+ svn_error_t * (*write)(void *baton, const char *buf, apr_size_t len,
+ apr_pool_t *scratch_pool);
+ void *write_baton;
+
+} target_content_t;
+
+typedef struct prop_patch_target_t {
+
+ /* The name of the property */
+ const char *name;
+
+ /* The property value. This is NULL in case the property did not exist
+ * prior to patch application (see also CONTENT->existed).
+ * Note that the patch implementation does not support binary properties,
+ * so this string is not expected to contain embedded NUL characters. */
+ const svn_string_t *value;
+
+ /* The patched property value.
+ * This is equivalent to the target, except that in appropriate
+ * places it contains the modified text as it appears in the patch file. */
+ svn_stringbuf_t *patched_value;
+
+ /* All information that is specific to the content of the property. */
+ target_content_t *content;
+
+ /* Represents the operation performed on the property. It can be added,
+ * deleted or modified.
+ * ### Should we use flags instead since we're not using all enum values? */
+ svn_diff_operation_kind_t operation;
+
+ /* ### Here we'll add flags telling if the prop was added, deleted,
+ * ### had_rejects, had_local_mods prior to patching and so on. */
+} prop_patch_target_t;
+
+typedef struct patch_target_t {
+ /* The target path as it appeared in the patch file,
+ * but in canonicalised form. */
+ const char *canon_path_from_patchfile;
+
+ /* The target path, relative to the working copy directory the
+ * patch is being applied to. A patch strip count applies to this
+ * and only this path. This is never NULL. */
+ const char *local_relpath;
+
+ /* The absolute path of the target on the filesystem.
+ * Any symlinks the path from the patch file may contain are resolved.
+ * Is not always known, so it may be NULL. */
+ const char *local_abspath;
+
+ /* The target file, read-only. This is NULL in case the target
+ * file did not exist prior to patch application (see also
+ * CONTENT->existed). */
+ apr_file_t *file;
+
+ /* The target file is a symlink */
+ svn_boolean_t is_symlink;
+
+ /* The patched file.
+ * This is equivalent to the target, except that in appropriate
+ * places it contains the modified text as it appears in the patch file.
+ * The data in this file is written in repository-normal form.
+ * EOL transformation and keyword contraction is performed when the
+ * patched result is installed in the working copy. */
+ apr_file_t *patched_file;
+
+ /* Path to the patched file. */
+ const char *patched_path;
+
+ /* Hunks that are rejected will be written to this file. */
+ apr_file_t *reject_file;
+
+ /* Path to the reject file. */
+ const char *reject_path;
+
+ /* The node kind of the target as found in WC-DB prior
+ * to patch application. */
+ svn_node_kind_t db_kind;
+
+ /* The target's kind on disk prior to patch application. */
+ svn_node_kind_t kind_on_disk;
+
+ /* True if the target was locally deleted prior to patching. */
+ svn_boolean_t locally_deleted;
+
+ /* True if the target had to be skipped for some reason. */
+ svn_boolean_t skipped;
+
+ /* True if the target has been filtered by the patch callback. */
+ svn_boolean_t filtered;
+
+ /* True if at least one hunk was rejected. */
+ svn_boolean_t had_rejects;
+
+ /* True if at least one property hunk was rejected. */
+ svn_boolean_t had_prop_rejects;
+
+ /* True if the target file had local modifications before the
+ * patch was applied to it. */
+ svn_boolean_t local_mods;
+
+ /* True if the target was added by the patch, which means that it did
+ * not exist on disk before patching and has content after patching. */
+ svn_boolean_t added;
+
+ /* True if the target ended up being deleted by the patch. */
+ svn_boolean_t deleted;
+
+ /* True if the target ended up being replaced by the patch
+ * (i.e. a new file was added on top locally deleted node). */
+ svn_boolean_t replaced;
+
+ /* True if the target has the executable bit set. */
+ svn_boolean_t executable;
+
+ /* True if the patch changed the text of the target. */
+ svn_boolean_t has_text_changes;
+
+ /* True if the patch changed any of the properties of the target. */
+ svn_boolean_t has_prop_changes;
+
+ /* True if the patch contained a svn:special property. */
+ svn_boolean_t is_special;
+
+ /* All the information that is specific to the content of the target. */
+ target_content_t *content;
+
+ /* A hash table of prop_patch_target_t objects keyed by property names. */
+ apr_hash_t *prop_targets;
+
+} patch_target_t;
+
+
+/* A smaller struct containing a subset of patch_target_t.
+ * Carries the minimal amount of information we still need for a
+ * target after we're done patching it so we can free other resources. */
+typedef struct patch_target_info_t {
+ const char *local_abspath;
+ svn_boolean_t deleted;
+} patch_target_info_t;
+
+
+/* Strip STRIP_COUNT components from the front of PATH, returning
+ * the result in *RESULT, allocated in RESULT_POOL.
+ * Do temporary allocations in SCRATCH_POOL. */
+static svn_error_t *
+strip_path(const char **result, const char *path, int strip_count,
+ apr_pool_t *result_pool, apr_pool_t *scratch_pool)
+{
+ int i;
+ apr_array_header_t *components;
+ apr_array_header_t *stripped;
+
+ components = svn_path_decompose(path, scratch_pool);
+ if (strip_count > components->nelts)
+ return svn_error_createf(SVN_ERR_CLIENT_PATCH_BAD_STRIP_COUNT, NULL,
+ _("Cannot strip %u components from '%s'"),
+ strip_count,
+ svn_dirent_local_style(path, scratch_pool));
+
+ stripped = apr_array_make(scratch_pool, components->nelts - strip_count,
+ sizeof(const char *));
+ for (i = strip_count; i < components->nelts; i++)
+ {
+ const char *component;
+
+ component = APR_ARRAY_IDX(components, i, const char *);
+ APR_ARRAY_PUSH(stripped, const char *) = component;
+ }
+
+ *result = svn_path_compose(stripped, result_pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Obtain KEYWORDS, EOL_STYLE and EOL_STR for LOCAL_ABSPATH.
+ * WC_CTX is a context for the working copy the patch is applied to.
+ * Use RESULT_POOL for allocations of fields in TARGET.
+ * Use SCRATCH_POOL for all other allocations. */
+static svn_error_t *
+obtain_eol_and_keywords_for_file(apr_hash_t **keywords,
+ svn_subst_eol_style_t *eol_style,
+ const char **eol_str,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *props;
+ svn_string_t *keywords_val, *eol_style_val;
+
+ SVN_ERR(svn_wc_prop_list2(&props, wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+ keywords_val = svn_hash_gets(props, SVN_PROP_KEYWORDS);
+ if (keywords_val)
+ {
+ svn_revnum_t changed_rev;
+ apr_time_t changed_date;
+ const char *rev_str;
+ const char *author;
+ const char *url;
+ const char *root_url;
+
+ SVN_ERR(svn_wc__node_get_changed_info(&changed_rev,
+ &changed_date,
+ &author, wc_ctx,
+ local_abspath,
+ scratch_pool,
+ scratch_pool));
+ rev_str = apr_psprintf(scratch_pool, "%ld", changed_rev);
+ SVN_ERR(svn_wc__node_get_url(&url, wc_ctx,
+ local_abspath,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_wc__node_get_repos_info(NULL, NULL, &root_url, NULL,
+ wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_subst_build_keywords3(keywords,
+ keywords_val->data,
+ rev_str, url, root_url, changed_date,
+ author, result_pool));
+ }
+
+ eol_style_val = svn_hash_gets(props, SVN_PROP_EOL_STYLE);
+ if (eol_style_val)
+ {
+ svn_subst_eol_style_from_value(eol_style,
+ eol_str,
+ eol_style_val->data);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Resolve the exact path for a patch TARGET at path PATH_FROM_PATCHFILE,
+ * which is the path of the target as it appeared in the patch file.
+ * Put a canonicalized version of PATH_FROM_PATCHFILE into
+ * TARGET->CANON_PATH_FROM_PATCHFILE.
+ * WC_CTX is a context for the working copy the patch is applied to.
+ * If possible, determine TARGET->WC_PATH, TARGET->ABS_PATH, TARGET->KIND,
+ * TARGET->ADDED, and TARGET->PARENT_DIR_EXISTS.
+ * Indicate in TARGET->SKIPPED whether the target should be skipped.
+ * STRIP_COUNT specifies the number of leading path components
+ * which should be stripped from target paths in the patch.
+ * PROP_CHANGES_ONLY specifies whether the target path is allowed to have
+ * only property changes, and no content changes (in which case the target
+ * must be a directory).
+ * Use RESULT_POOL for allocations of fields in TARGET.
+ * Use SCRATCH_POOL for all other allocations. */
+static svn_error_t *
+resolve_target_path(patch_target_t *target,
+ const char *path_from_patchfile,
+ const char *wcroot_abspath,
+ int strip_count,
+ svn_boolean_t prop_changes_only,
+ svn_wc_context_t *wc_ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *stripped_path;
+ svn_wc_status3_t *status;
+ svn_error_t *err;
+ svn_boolean_t under_root;
+
+ target->canon_path_from_patchfile = svn_dirent_internal_style(
+ path_from_patchfile, result_pool);
+
+ /* We allow properties to be set on the wc root dir. */
+ if (! prop_changes_only && target->canon_path_from_patchfile[0] == '\0')
+ {
+ /* An empty patch target path? What gives? Skip this. */
+ target->skipped = TRUE;
+ target->local_abspath = NULL;
+ target->local_relpath = "";
+ return SVN_NO_ERROR;
+ }
+
+ if (strip_count > 0)
+ SVN_ERR(strip_path(&stripped_path, target->canon_path_from_patchfile,
+ strip_count, result_pool, scratch_pool));
+ else
+ stripped_path = target->canon_path_from_patchfile;
+
+ if (svn_dirent_is_absolute(stripped_path))
+ {
+ target->local_relpath = svn_dirent_is_child(wcroot_abspath,
+ stripped_path,
+ result_pool);
+
+ if (! target->local_relpath)
+ {
+ /* The target path is either outside of the working copy
+ * or it is the working copy itself. Skip it. */
+ target->skipped = TRUE;
+ target->local_abspath = NULL;
+ target->local_relpath = stripped_path;
+ return SVN_NO_ERROR;
+ }
+ }
+ else
+ {
+ target->local_relpath = stripped_path;
+ }
+
+ /* Make sure the path is secure to use. We want the target to be inside
+ * of the working copy and not be fooled by symlinks it might contain. */
+ SVN_ERR(svn_dirent_is_under_root(&under_root,
+ &target->local_abspath, wcroot_abspath,
+ target->local_relpath, result_pool));
+
+ if (! under_root)
+ {
+ /* The target path is outside of the working copy. Skip it. */
+ target->skipped = TRUE;
+ target->local_abspath = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ /* Skip things we should not be messing with. */
+ err = svn_wc_status3(&status, wc_ctx, target->local_abspath,
+ result_pool, scratch_pool);
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+
+ target->locally_deleted = TRUE;
+ target->db_kind = svn_node_none;
+ status = NULL;
+ }
+ else if (status->node_status == svn_wc_status_ignored ||
+ status->node_status == svn_wc_status_unversioned ||
+ status->node_status == svn_wc_status_missing ||
+ status->node_status == svn_wc_status_obstructed ||
+ status->conflicted)
+ {
+ target->skipped = TRUE;
+ return SVN_NO_ERROR;
+ }
+ else if (status->node_status == svn_wc_status_deleted)
+ {
+ target->locally_deleted = TRUE;
+ }
+
+ if (status && (status->kind != svn_node_unknown))
+ target->db_kind = status->kind;
+ else
+ target->db_kind = svn_node_none;
+
+ SVN_ERR(svn_io_check_special_path(target->local_abspath,
+ &target->kind_on_disk, &target->is_symlink,
+ scratch_pool));
+
+ if (target->locally_deleted)
+ {
+ const char *moved_to_abspath;
+
+ SVN_ERR(svn_wc__node_was_moved_away(&moved_to_abspath, NULL,
+ wc_ctx, target->local_abspath,
+ result_pool, scratch_pool));
+ if (moved_to_abspath)
+ {
+ target->local_abspath = moved_to_abspath;
+ target->local_relpath = svn_dirent_skip_ancestor(wcroot_abspath,
+ moved_to_abspath);
+ SVN_ERR_ASSERT(target->local_relpath &&
+ target->local_relpath[0] != '\0');
+
+ /* As far as we are concerned this target is not locally deleted. */
+ target->locally_deleted = FALSE;
+
+ SVN_ERR(svn_io_check_special_path(target->local_abspath,
+ &target->kind_on_disk,
+ &target->is_symlink,
+ scratch_pool));
+ }
+ else if (target->kind_on_disk != svn_node_none)
+ {
+ target->skipped = TRUE;
+ return SVN_NO_ERROR;
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Baton for reading from properties. */
+typedef struct prop_read_baton_t {
+ const svn_string_t *value;
+ apr_off_t offset;
+} prop_read_baton_t;
+
+/* Allocate *STRINGBUF in RESULT_POOL, and read into it one line from
+ * the unpatched property value accessed via BATON.
+ * Reading stops either after a line-terminator was found, or if
+ * the property value runs out in which case *EOF is set to TRUE.
+ * The line-terminator is not stored in *STRINGBUF.
+ *
+ * If the line is empty or could not be read, *line is set to NULL.
+ *
+ * The line-terminator is detected automatically and stored in *EOL
+ * if EOL is not NULL. If the end of the property value is reached
+ * and does not end with a newline character, and EOL is not NULL,
+ * *EOL is set to NULL.
+ *
+ * SCRATCH_POOL is used for temporary allocations.
+ */
+static svn_error_t *
+readline_prop(void *baton, svn_stringbuf_t **line, const char **eol_str,
+ svn_boolean_t *eof, apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ prop_read_baton_t *b = (prop_read_baton_t *)baton;
+ svn_stringbuf_t *str = NULL;
+ const char *c;
+ svn_boolean_t found_eof;
+
+ if ((apr_uint64_t)b->offset >= (apr_uint64_t)b->value->len)
+ {
+ *eol_str = NULL;
+ *eof = TRUE;
+ *line = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ /* Read bytes into STR up to and including, but not storing,
+ * the next EOL sequence. */
+ *eol_str = NULL;
+ found_eof = FALSE;
+ do
+ {
+ c = b->value->data + b->offset;
+ b->offset++;
+
+ if (*c == '\0')
+ {
+ found_eof = TRUE;
+ break;
+ }
+ else if (*c == '\n')
+ {
+ *eol_str = "\n";
+ }
+ else if (*c == '\r')
+ {
+ *eol_str = "\r";
+ if (*(c + 1) == '\n')
+ {
+ *eol_str = "\r\n";
+ b->offset++;
+ }
+ }
+ else
+ {
+ if (str == NULL)
+ str = svn_stringbuf_create_ensure(80, result_pool);
+ svn_stringbuf_appendbyte(str, *c);
+ }
+
+ if (*eol_str)
+ break;
+ }
+ while (c < b->value->data + b->value->len);
+
+ if (eof)
+ *eof = found_eof;
+ *line = str;
+
+ return SVN_NO_ERROR;
+}
+
+/* Return in *OFFSET the current byte offset for reading from the
+ * unpatched property value accessed via BATON.
+ * Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+tell_prop(void *baton, apr_off_t *offset, apr_pool_t *scratch_pool)
+{
+ prop_read_baton_t *b = (prop_read_baton_t *)baton;
+ *offset = b->offset;
+ return SVN_NO_ERROR;
+}
+
+/* Seek to the specified by OFFSET in the unpatched property value accessed
+ * via BATON. Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+seek_prop(void *baton, apr_off_t offset, apr_pool_t *scratch_pool)
+{
+ prop_read_baton_t *b = (prop_read_baton_t *)baton;
+ b->offset = offset;
+ return SVN_NO_ERROR;
+}
+
+/* Write LEN bytes from BUF into the patched property value accessed
+ * via BATON. Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+write_prop(void *baton, const char *buf, apr_size_t len,
+ apr_pool_t *scratch_pool)
+{
+ svn_stringbuf_t *patched_value = (svn_stringbuf_t *)baton;
+ svn_stringbuf_appendbytes(patched_value, buf, len);
+ return SVN_NO_ERROR;
+}
+
+/* Initialize a PROP_TARGET structure for PROP_NAME on the patch target
+ * at LOCAL_ABSPATH. OPERATION indicates the operation performed on the
+ * property. Use working copy context WC_CTX.
+ * Allocate results in RESULT_POOL.
+ * Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+init_prop_target(prop_patch_target_t **prop_target,
+ const char *prop_name,
+ svn_diff_operation_kind_t operation,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ apr_pool_t *result_pool, apr_pool_t *scratch_pool)
+{
+ prop_patch_target_t *new_prop_target;
+ target_content_t *content;
+ const svn_string_t *value;
+ svn_error_t *err;
+ prop_read_baton_t *prop_read_baton;
+
+ content = apr_pcalloc(result_pool, sizeof(*content));
+
+ /* All other fields are FALSE or NULL due to apr_pcalloc(). */
+ content->current_line = 1;
+ content->eol_style = svn_subst_eol_style_none;
+ content->lines = apr_array_make(result_pool, 0, sizeof(apr_off_t));
+ content->hunks = apr_array_make(result_pool, 0, sizeof(hunk_info_t *));
+ content->keywords = apr_hash_make(result_pool);
+
+ new_prop_target = apr_pcalloc(result_pool, sizeof(*new_prop_target));
+ new_prop_target->name = apr_pstrdup(result_pool, prop_name);
+ new_prop_target->operation = operation;
+ new_prop_target->content = content;
+
+ err = svn_wc_prop_get2(&value, wc_ctx, local_abspath, prop_name,
+ result_pool, scratch_pool);
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ value = NULL;
+ }
+ else
+ return svn_error_trace(err);
+ }
+ content->existed = (value != NULL);
+ new_prop_target->value = value;
+ new_prop_target->patched_value = svn_stringbuf_create_empty(result_pool);
+
+
+ /* Wire up the read and write callbacks. */
+ prop_read_baton = apr_pcalloc(result_pool, sizeof(*prop_read_baton));
+ prop_read_baton->value = value;
+ prop_read_baton->offset = 0;
+ content->readline = readline_prop;
+ content->tell = tell_prop;
+ content->seek = seek_prop;
+ content->read_baton = prop_read_baton;
+ content->write = write_prop;
+ content->write_baton = new_prop_target->patched_value;
+
+ *prop_target = new_prop_target;
+
+ return SVN_NO_ERROR;
+}
+
+/* Allocate *STRINGBUF in RESULT_POOL, and read into it one line from
+ * the unpatched file content accessed via BATON.
+ * Reading stops either after a line-terminator was found,
+ * or if EOF is reached in which case *EOF is set to TRUE.
+ * The line-terminator is not stored in *STRINGBUF.
+ *
+ * If the line is empty or could not be read, *line is set to NULL.
+ *
+ * The line-terminator is detected automatically and stored in *EOL
+ * if EOL is not NULL. If EOF is reached and FILE does not end
+ * with a newline character, and EOL is not NULL, *EOL is set to NULL.
+ *
+ * SCRATCH_POOL is used for temporary allocations.
+ */
+static svn_error_t *
+readline_file(void *baton, svn_stringbuf_t **line, const char **eol_str,
+ svn_boolean_t *eof, apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_file_t *file = (apr_file_t *)baton;
+ svn_stringbuf_t *str = NULL;
+ apr_size_t numbytes;
+ char c;
+ svn_boolean_t found_eof;
+
+ /* Read bytes into STR up to and including, but not storing,
+ * the next EOL sequence. */
+ *eol_str = NULL;
+ numbytes = 1;
+ found_eof = FALSE;
+ while (!found_eof)
+ {
+ SVN_ERR(svn_io_file_read_full2(file, &c, sizeof(c), &numbytes,
+ &found_eof, scratch_pool));
+ if (numbytes != 1)
+ {
+ found_eof = TRUE;
+ break;
+ }
+
+ if (c == '\n')
+ {
+ *eol_str = "\n";
+ }
+ else if (c == '\r')
+ {
+ *eol_str = "\r";
+
+ if (!found_eof)
+ {
+ apr_off_t pos;
+
+ /* Check for "\r\n" by peeking at the next byte. */
+ pos = 0;
+ SVN_ERR(svn_io_file_seek(file, APR_CUR, &pos, scratch_pool));
+ SVN_ERR(svn_io_file_read_full2(file, &c, sizeof(c), &numbytes,
+ &found_eof, scratch_pool));
+ if (numbytes == 1 && c == '\n')
+ {
+ *eol_str = "\r\n";
+ }
+ else
+ {
+ /* Pretend we never peeked. */
+ SVN_ERR(svn_io_file_seek(file, APR_SET, &pos, scratch_pool));
+ found_eof = FALSE;
+ numbytes = 1;
+ }
+ }
+ }
+ else
+ {
+ if (str == NULL)
+ str = svn_stringbuf_create_ensure(80, result_pool);
+ svn_stringbuf_appendbyte(str, c);
+ }
+
+ if (*eol_str)
+ break;
+ }
+
+ if (eof)
+ *eof = found_eof;
+ *line = str;
+
+ return SVN_NO_ERROR;
+}
+
+/* Return in *OFFSET the current byte offset for reading from the
+ * unpatched file content accessed via BATON.
+ * Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+tell_file(void *baton, apr_off_t *offset, apr_pool_t *scratch_pool)
+{
+ apr_file_t *file = (apr_file_t *)baton;
+ *offset = 0;
+ SVN_ERR(svn_io_file_seek(file, APR_CUR, offset, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* Seek to the specified by OFFSET in the unpatched file content accessed
+ * via BATON. Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+seek_file(void *baton, apr_off_t offset, apr_pool_t *scratch_pool)
+{
+ apr_file_t *file = (apr_file_t *)baton;
+ SVN_ERR(svn_io_file_seek(file, APR_SET, &offset, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* Write LEN bytes from BUF into the patched file content accessed
+ * via BATON. Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+write_file(void *baton, const char *buf, apr_size_t len,
+ apr_pool_t *scratch_pool)
+{
+ apr_file_t *file = (apr_file_t *)baton;
+ SVN_ERR(svn_io_file_write_full(file, buf, len, &len, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* Handling symbolic links:
+ *
+ * In Subversion, symlinks can be represented on disk in two distinct ways.
+ * On systems which support symlinks, a symlink is created on disk.
+ * On systems which do not support symlink, a file is created on disk
+ * which contains the "normal form" of the symlink, which looks like:
+ * link TARGET
+ * where TARGET is the file the symlink points to.
+ *
+ * When reading symlinks (i.e. the link itself, not the file the symlink
+ * is pointing to) through the svn_subst_create_specialfile() function
+ * into a buffer, the buffer always contains the "normal form" of the symlink.
+ * Due to this representation symlinks always contain a single line of text.
+ *
+ * The functions below are needed to deal with the case where a patch
+ * wants to change the TARGET that a symlink points to.
+ */
+
+/* Baton for the (readline|tell|seek|write)_symlink functions. */
+struct symlink_baton_t
+{
+ /* The path to the symlink on disk (not the path to the target of the link) */
+ const char *local_abspath;
+
+ /* Indicates whether the "normal form" of the symlink has been read. */
+ svn_boolean_t at_eof;
+};
+
+/* Allocate *STRINGBUF in RESULT_POOL, and store into it the "normal form"
+ * of the symlink accessed via BATON.
+ *
+ * Otherwise behaves like readline_file(), which see.
+ */
+static svn_error_t *
+readline_symlink(void *baton, svn_stringbuf_t **line, const char **eol_str,
+ svn_boolean_t *eof, apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct symlink_baton_t *sb = baton;
+
+ if (eof)
+ *eof = TRUE;
+ if (eol_str)
+ *eol_str = NULL;
+
+ if (sb->at_eof)
+ {
+ *line = NULL;
+ }
+ else
+ {
+ svn_string_t *dest;
+
+ SVN_ERR(svn_io_read_link(&dest, sb->local_abspath, scratch_pool));
+ *line = svn_stringbuf_createf(result_pool, "link %s", dest->data);
+ sb->at_eof = TRUE;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Set *OFFSET to 1 or 0 depending on whether the "normal form" of
+ * the symlink has already been read. */
+static svn_error_t *
+tell_symlink(void *baton, apr_off_t *offset, apr_pool_t *scratch_pool)
+{
+ struct symlink_baton_t *sb = baton;
+
+ *offset = sb->at_eof ? 1 : 0;
+ return SVN_NO_ERROR;
+}
+
+/* If offset is non-zero, mark the symlink as having been read in its
+ * "normal form". Else, mark the symlink as not having been read yet. */
+static svn_error_t *
+seek_symlink(void *baton, apr_off_t offset, apr_pool_t *scratch_pool)
+{
+ struct symlink_baton_t *sb = baton;
+
+ sb->at_eof = (offset != 0);
+ return SVN_NO_ERROR;
+}
+
+
+/* Set the target of the symlink accessed via BATON.
+ * The contents of BUF must be a valid "normal form" of a symlink. */
+static svn_error_t *
+write_symlink(void *baton, const char *buf, apr_size_t len,
+ apr_pool_t *scratch_pool)
+{
+ const char *target_abspath = baton;
+ const char *new_name;
+ const char *link = apr_pstrndup(scratch_pool, buf, len);
+
+ if (strncmp(link, "link ", 5) != 0)
+ return svn_error_create(SVN_ERR_IO_WRITE_ERROR, NULL,
+ _("Invalid link representation"));
+
+ link += 5; /* Skip "link " */
+
+ /* We assume the entire symlink is written at once, as the patch
+ format is line based */
+
+ SVN_ERR(svn_io_create_unique_link(&new_name, target_abspath, link,
+ ".tmp", scratch_pool));
+
+ SVN_ERR(svn_io_file_rename(new_name, target_abspath, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Return a suitable filename for the target of PATCH.
+ * Examine the ``old'' and ``new'' file names, and choose the file name
+ * with the fewest path components, the shortest basename, and the shortest
+ * total file name length (in that order). In case of a tie, return the new
+ * filename. This heuristic is also used by Larry Wall's UNIX patch (except
+ * that it prompts for a filename in case of a tie).
+ * Additionally, for compatibility with git, if one of the filenames
+ * is "/dev/null", use the other filename. */
+static const char *
+choose_target_filename(const svn_patch_t *patch)
+{
+ apr_size_t old;
+ apr_size_t new;
+
+ if (strcmp(patch->old_filename, "/dev/null") == 0)
+ return patch->new_filename;
+ if (strcmp(patch->new_filename, "/dev/null") == 0)
+ return patch->old_filename;
+
+ old = svn_path_component_count(patch->old_filename);
+ new = svn_path_component_count(patch->new_filename);
+
+ if (old == new)
+ {
+ old = strlen(svn_dirent_basename(patch->old_filename, NULL));
+ new = strlen(svn_dirent_basename(patch->new_filename, NULL));
+
+ if (old == new)
+ {
+ old = strlen(patch->old_filename);
+ new = strlen(patch->new_filename);
+ }
+ }
+
+ return (old < new) ? patch->old_filename : patch->new_filename;
+}
+
+/* Attempt to initialize a *PATCH_TARGET structure for a target file
+ * described by PATCH. Use working copy context WC_CTX.
+ * STRIP_COUNT specifies the number of leading path components
+ * which should be stripped from target paths in the patch.
+ * The patch target structure is allocated in RESULT_POOL, but if the target
+ * should be skipped, PATCH_TARGET->SKIPPED is set and the target should be
+ * treated as not fully initialized, e.g. the caller should not not do any
+ * further operations on the target if it is marked to be skipped.
+ * If REMOVE_TEMPFILES is TRUE, set up temporary files to be removed as
+ * soon as they are no longer needed.
+ * Use SCRATCH_POOL for all other allocations. */
+static svn_error_t *
+init_patch_target(patch_target_t **patch_target,
+ const svn_patch_t *patch,
+ const char *wcroot_abspath,
+ svn_wc_context_t *wc_ctx, int strip_count,
+ svn_boolean_t remove_tempfiles,
+ apr_pool_t *result_pool, apr_pool_t *scratch_pool)
+{
+ patch_target_t *target;
+ target_content_t *content;
+ svn_boolean_t has_prop_changes = FALSE;
+ svn_boolean_t prop_changes_only = FALSE;
+
+ {
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(scratch_pool, patch->prop_patches);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ svn_prop_patch_t *prop_patch = svn__apr_hash_index_val(hi);
+ if (! has_prop_changes)
+ has_prop_changes = prop_patch->hunks->nelts > 0;
+ else
+ break;
+ }
+ }
+
+ prop_changes_only = has_prop_changes && patch->hunks->nelts == 0;
+
+ content = apr_pcalloc(result_pool, sizeof(*content));
+
+ /* All other fields in content are FALSE or NULL due to apr_pcalloc().*/
+ content->current_line = 1;
+ content->eol_style = svn_subst_eol_style_none;
+ content->lines = apr_array_make(result_pool, 0, sizeof(apr_off_t));
+ content->hunks = apr_array_make(result_pool, 0, sizeof(hunk_info_t *));
+ content->keywords = apr_hash_make(result_pool);
+
+ target = apr_pcalloc(result_pool, sizeof(*target));
+
+ /* All other fields in target are FALSE or NULL due to apr_pcalloc(). */
+ target->db_kind = svn_node_none;
+ target->kind_on_disk = svn_node_none;
+ target->content = content;
+ target->prop_targets = apr_hash_make(result_pool);
+
+ SVN_ERR(resolve_target_path(target, choose_target_filename(patch),
+ wcroot_abspath, strip_count, prop_changes_only,
+ wc_ctx, result_pool, scratch_pool));
+ if (! target->skipped)
+ {
+ const char *diff_header;
+ apr_size_t len;
+
+ /* Create a temporary file to write the patched result to.
+ * Also grab various bits of information about the file. */
+ if (target->is_symlink)
+ {
+ struct symlink_baton_t *sb = apr_pcalloc(result_pool, sizeof(*sb));
+ content->existed = TRUE;
+
+ sb->local_abspath = target->local_abspath;
+
+ /* Wire up the read callbacks. */
+ content->read_baton = sb;
+
+ content->readline = readline_symlink;
+ content->seek = seek_symlink;
+ content->tell = tell_symlink;
+ }
+ else if (target->kind_on_disk == svn_node_file)
+ {
+ SVN_ERR(svn_io_file_open(&target->file, target->local_abspath,
+ APR_READ | APR_BUFFERED,
+ APR_OS_DEFAULT, result_pool));
+ SVN_ERR(svn_wc_text_modified_p2(&target->local_mods, wc_ctx,
+ target->local_abspath, FALSE,
+ scratch_pool));
+ SVN_ERR(svn_io_is_file_executable(&target->executable,
+ target->local_abspath,
+ scratch_pool));
+ SVN_ERR(obtain_eol_and_keywords_for_file(&content->keywords,
+ &content->eol_style,
+ &content->eol_str,
+ wc_ctx,
+ target->local_abspath,
+ result_pool,
+ scratch_pool));
+ content->existed = TRUE;
+
+ /* Wire up the read callbacks. */
+ content->readline = readline_file;
+ content->seek = seek_file;
+ content->tell = tell_file;
+ content->read_baton = target->file;
+ }
+
+ /* ### Is it ok to set the operation of the target already here? Isn't
+ * ### the target supposed to be marked with an operation after we have
+ * ### determined that the changes will apply cleanly to the WC? Maybe
+ * ### we should have kept the patch field in patch_target_t to be
+ * ### able to distinguish between 'what the patch says we should do'
+ * ### and 'what we can do with the given state of our WC'. */
+ if (patch->operation == svn_diff_op_added)
+ target->added = TRUE;
+ else if (patch->operation == svn_diff_op_deleted)
+ target->deleted = TRUE;
+
+ if (! target->is_symlink)
+ {
+ /* Open a temporary file to write the patched result to. */
+ SVN_ERR(svn_io_open_unique_file3(&target->patched_file,
+ &target->patched_path, NULL,
+ remove_tempfiles ?
+ svn_io_file_del_on_pool_cleanup :
+ svn_io_file_del_none,
+ result_pool, scratch_pool));
+
+ /* Put the write callback in place. */
+ content->write = write_file;
+ content->write_baton = target->patched_file;
+ }
+ else
+ {
+ /* Put the write callback in place. */
+ SVN_ERR(svn_io_open_unique_file3(NULL,
+ &target->patched_path, NULL,
+ remove_tempfiles ?
+ svn_io_file_del_on_pool_cleanup :
+ svn_io_file_del_none,
+ result_pool, scratch_pool));
+
+ content->write_baton = (void*)target->patched_path;
+
+ content->write = write_symlink;
+ }
+
+ /* Open a temporary file to write rejected hunks to. */
+ SVN_ERR(svn_io_open_unique_file3(&target->reject_file,
+ &target->reject_path, NULL,
+ remove_tempfiles ?
+ svn_io_file_del_on_pool_cleanup :
+ svn_io_file_del_none,
+ result_pool, scratch_pool));
+
+ /* The reject file needs a diff header. */
+ diff_header = apr_psprintf(scratch_pool, "--- %s%s+++ %s%s",
+ target->canon_path_from_patchfile,
+ APR_EOL_STR,
+ target->canon_path_from_patchfile,
+ APR_EOL_STR);
+ len = strlen(diff_header);
+ SVN_ERR(svn_io_file_write_full(target->reject_file, diff_header, len,
+ &len, scratch_pool));
+
+ /* Handle properties. */
+ if (! target->skipped)
+ {
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(result_pool, patch->prop_patches);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *prop_name = svn__apr_hash_index_key(hi);
+ svn_prop_patch_t *prop_patch = svn__apr_hash_index_val(hi);
+ prop_patch_target_t *prop_target;
+
+ SVN_ERR(init_prop_target(&prop_target,
+ prop_name,
+ prop_patch->operation,
+ wc_ctx, target->local_abspath,
+ result_pool, scratch_pool));
+ svn_hash_sets(target->prop_targets, prop_name, prop_target);
+ }
+ }
+ }
+
+ *patch_target = target;
+ return SVN_NO_ERROR;
+}
+
+/* Read a *LINE from CONTENT. If the line has not been read before
+ * mark the line in CONTENT->LINES.
+ * If a line could be read successfully, increase CONTENT->CURRENT_LINE,
+ * and allocate *LINE in RESULT_POOL.
+ * Do temporary allocations in SCRATCH_POOL.
+ */
+static svn_error_t *
+readline(target_content_t *content,
+ const char **line,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_stringbuf_t *line_raw;
+ const char *eol_str;
+ svn_linenum_t max_line = (svn_linenum_t)content->lines->nelts + 1;
+
+ if (content->eof || content->readline == NULL)
+ {
+ *line = "";
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR_ASSERT(content->current_line <= max_line);
+ if (content->current_line == max_line)
+ {
+ apr_off_t offset;
+
+ SVN_ERR(content->tell(content->read_baton, &offset,
+ scratch_pool));
+ APR_ARRAY_PUSH(content->lines, apr_off_t) = offset;
+ }
+
+ SVN_ERR(content->readline(content->read_baton, &line_raw,
+ &eol_str, &content->eof,
+ result_pool, scratch_pool));
+ if (content->eol_style == svn_subst_eol_style_none)
+ content->eol_str = eol_str;
+
+ if (line_raw)
+ {
+ /* Contract keywords. */
+ SVN_ERR(svn_subst_translate_cstring2(line_raw->data, line,
+ NULL, FALSE,
+ content->keywords, FALSE,
+ result_pool));
+ }
+ else
+ *line = "";
+
+ if ((line_raw && line_raw->len > 0) || eol_str)
+ content->current_line++;
+
+ SVN_ERR_ASSERT(content->current_line > 0);
+
+ return SVN_NO_ERROR;
+}
+
+/* Seek to the specified LINE in CONTENT.
+ * Mark any lines not read before in CONTENT->LINES.
+ * Do temporary allocations in SCRATCH_POOL.
+ */
+static svn_error_t *
+seek_to_line(target_content_t *content, svn_linenum_t line,
+ apr_pool_t *scratch_pool)
+{
+ svn_linenum_t saved_line;
+ svn_boolean_t saved_eof;
+
+ SVN_ERR_ASSERT(line > 0);
+
+ if (line == content->current_line)
+ return SVN_NO_ERROR;
+
+ saved_line = content->current_line;
+ saved_eof = content->eof;
+
+ if (line <= (svn_linenum_t)content->lines->nelts)
+ {
+ apr_off_t offset;
+
+ offset = APR_ARRAY_IDX(content->lines, line - 1, apr_off_t);
+ SVN_ERR(content->seek(content->read_baton, offset,
+ scratch_pool));
+ content->current_line = line;
+ }
+ else
+ {
+ const char *dummy;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ while (! content->eof && content->current_line < line)
+ {
+ svn_pool_clear(iterpool);
+ SVN_ERR(readline(content, &dummy, iterpool, iterpool));
+ }
+ svn_pool_destroy(iterpool);
+ }
+
+ /* After seeking backwards from EOF position clear EOF indicator. */
+ if (saved_eof && saved_line > content->current_line)
+ content->eof = FALSE;
+
+ return SVN_NO_ERROR;
+}
+
+/* Indicate in *MATCHED whether the original text of HUNK matches the patch
+ * CONTENT at its current line. Lines within FUZZ lines of the start or
+ * end of HUNK will always match. If IGNORE_WHITESPACE is set, we ignore
+ * whitespace when doing the matching. When this function returns, neither
+ * CONTENT->CURRENT_LINE nor the file offset in the target file will
+ * have changed. If MATCH_MODIFIED is TRUE, match the modified hunk text,
+ * rather than the original hunk text.
+ * Do temporary allocations in POOL. */
+static svn_error_t *
+match_hunk(svn_boolean_t *matched, target_content_t *content,
+ svn_diff_hunk_t *hunk, svn_linenum_t fuzz,
+ svn_boolean_t ignore_whitespace,
+ svn_boolean_t match_modified, apr_pool_t *pool)
+{
+ svn_stringbuf_t *hunk_line;
+ const char *target_line;
+ svn_linenum_t lines_read;
+ svn_linenum_t saved_line;
+ svn_boolean_t hunk_eof;
+ svn_boolean_t lines_matched;
+ apr_pool_t *iterpool;
+ svn_linenum_t hunk_length;
+ svn_linenum_t leading_context;
+ svn_linenum_t trailing_context;
+
+ *matched = FALSE;
+
+ if (content->eof)
+ return SVN_NO_ERROR;
+
+ saved_line = content->current_line;
+ lines_read = 0;
+ lines_matched = FALSE;
+ leading_context = svn_diff_hunk_get_leading_context(hunk);
+ trailing_context = svn_diff_hunk_get_trailing_context(hunk);
+ if (match_modified)
+ {
+ svn_diff_hunk_reset_modified_text(hunk);
+ hunk_length = svn_diff_hunk_get_modified_length(hunk);
+ }
+ else
+ {
+ svn_diff_hunk_reset_original_text(hunk);
+ hunk_length = svn_diff_hunk_get_original_length(hunk);
+ }
+ iterpool = svn_pool_create(pool);
+ do
+ {
+ const char *hunk_line_translated;
+
+ svn_pool_clear(iterpool);
+
+ if (match_modified)
+ SVN_ERR(svn_diff_hunk_readline_modified_text(hunk, &hunk_line,
+ NULL, &hunk_eof,
+ iterpool, iterpool));
+ else
+ SVN_ERR(svn_diff_hunk_readline_original_text(hunk, &hunk_line,
+ NULL, &hunk_eof,
+ iterpool, iterpool));
+
+ /* Contract keywords, if any, before matching. */
+ SVN_ERR(svn_subst_translate_cstring2(hunk_line->data,
+ &hunk_line_translated,
+ NULL, FALSE,
+ content->keywords, FALSE,
+ iterpool));
+ SVN_ERR(readline(content, &target_line, iterpool, iterpool));
+
+ lines_read++;
+
+ /* If the last line doesn't have a newline, we get EOF but still
+ * have a non-empty line to compare. */
+ if ((hunk_eof && hunk_line->len == 0) ||
+ (content->eof && *target_line == 0))
+ break;
+
+ /* Leading/trailing fuzzy lines always match. */
+ if ((lines_read <= fuzz && leading_context > fuzz) ||
+ (lines_read > hunk_length - fuzz && trailing_context > fuzz))
+ lines_matched = TRUE;
+ else
+ {
+ if (ignore_whitespace)
+ {
+ char *hunk_line_trimmed;
+ char *target_line_trimmed;
+
+ hunk_line_trimmed = apr_pstrdup(iterpool, hunk_line_translated);
+ target_line_trimmed = apr_pstrdup(iterpool, target_line);
+ apr_collapse_spaces(hunk_line_trimmed, hunk_line_trimmed);
+ apr_collapse_spaces(target_line_trimmed, target_line_trimmed);
+ lines_matched = ! strcmp(hunk_line_trimmed, target_line_trimmed);
+ }
+ else
+ lines_matched = ! strcmp(hunk_line_translated, target_line);
+ }
+ }
+ while (lines_matched);
+
+ *matched = lines_matched && hunk_eof && hunk_line->len == 0;
+ SVN_ERR(seek_to_line(content, saved_line, iterpool));
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Scan lines of CONTENT for a match of the original text of HUNK,
+ * up to but not including the specified UPPER_LINE. Use fuzz factor FUZZ.
+ * If UPPER_LINE is zero scan until EOF occurs when reading from TARGET.
+ * Return the line at which HUNK was matched in *MATCHED_LINE.
+ * If the hunk did not match at all, set *MATCHED_LINE to zero.
+ * If the hunk matched multiple times, and MATCH_FIRST is TRUE,
+ * return the line number at which the first match occurred in *MATCHED_LINE.
+ * If the hunk matched multiple times, and MATCH_FIRST is FALSE,
+ * return the line number at which the last match occurred in *MATCHED_LINE.
+ * If IGNORE_WHITESPACE is set, ignore whitespace during the matching.
+ * If MATCH_MODIFIED is TRUE, match the modified hunk text,
+ * rather than the original hunk text.
+ * Call cancel CANCEL_FUNC with baton CANCEL_BATON to trigger cancellation.
+ * Do all allocations in POOL. */
+static svn_error_t *
+scan_for_match(svn_linenum_t *matched_line,
+ target_content_t *content,
+ svn_diff_hunk_t *hunk, svn_boolean_t match_first,
+ svn_linenum_t upper_line, svn_linenum_t fuzz,
+ svn_boolean_t ignore_whitespace,
+ svn_boolean_t match_modified,
+ svn_cancel_func_t cancel_func, void *cancel_baton,
+ apr_pool_t *pool)
+{
+ apr_pool_t *iterpool;
+
+ *matched_line = 0;
+ iterpool = svn_pool_create(pool);
+ while ((content->current_line < upper_line || upper_line == 0) &&
+ ! content->eof)
+ {
+ svn_boolean_t matched;
+
+ svn_pool_clear(iterpool);
+
+ if (cancel_func)
+ SVN_ERR(cancel_func(cancel_baton));
+
+ SVN_ERR(match_hunk(&matched, content, hunk, fuzz, ignore_whitespace,
+ match_modified, iterpool));
+ if (matched)
+ {
+ svn_boolean_t taken = FALSE;
+ int i;
+
+ /* Don't allow hunks to match at overlapping locations. */
+ for (i = 0; i < content->hunks->nelts; i++)
+ {
+ const hunk_info_t *hi;
+ svn_linenum_t length;
+
+ hi = APR_ARRAY_IDX(content->hunks, i, const hunk_info_t *);
+
+ if (match_modified)
+ length = svn_diff_hunk_get_modified_length(hi->hunk);
+ else
+ length = svn_diff_hunk_get_original_length(hi->hunk);
+
+ taken = (! hi->rejected &&
+ content->current_line >= hi->matched_line &&
+ content->current_line < (hi->matched_line + length));
+ if (taken)
+ break;
+ }
+
+ if (! taken)
+ {
+ *matched_line = content->current_line;
+ if (match_first)
+ break;
+ }
+ }
+
+ if (! content->eof)
+ SVN_ERR(seek_to_line(content, content->current_line + 1,
+ iterpool));
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Indicate in *MATCH whether the content described by CONTENT
+ * matches the modified text of HUNK.
+ * Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+match_existing_target(svn_boolean_t *match,
+ target_content_t *content,
+ svn_diff_hunk_t *hunk,
+ apr_pool_t *scratch_pool)
+{
+ svn_boolean_t lines_matched;
+ apr_pool_t *iterpool;
+ svn_boolean_t hunk_eof;
+ svn_linenum_t saved_line;
+
+ svn_diff_hunk_reset_modified_text(hunk);
+
+ saved_line = content->current_line;
+
+ iterpool = svn_pool_create(scratch_pool);
+ do
+ {
+ const char *line;
+ svn_stringbuf_t *hunk_line;
+ const char *line_translated;
+ const char *hunk_line_translated;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(readline(content, &line, iterpool, iterpool));
+ SVN_ERR(svn_diff_hunk_readline_modified_text(hunk, &hunk_line,
+ NULL, &hunk_eof,
+ iterpool, iterpool));
+ /* Contract keywords. */
+ SVN_ERR(svn_subst_translate_cstring2(line, &line_translated,
+ NULL, FALSE,
+ content->keywords,
+ FALSE, iterpool));
+ SVN_ERR(svn_subst_translate_cstring2(hunk_line->data,
+ &hunk_line_translated,
+ NULL, FALSE,
+ content->keywords,
+ FALSE, iterpool));
+ lines_matched = ! strcmp(line_translated, hunk_line_translated);
+ if (content->eof != hunk_eof)
+ {
+ svn_pool_destroy(iterpool);
+ *match = FALSE;
+ return SVN_NO_ERROR;
+ }
+ }
+ while (lines_matched && ! content->eof && ! hunk_eof);
+ svn_pool_destroy(iterpool);
+
+ *match = (lines_matched && content->eof == hunk_eof);
+ SVN_ERR(seek_to_line(content, saved_line, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Determine the line at which a HUNK applies to CONTENT of the TARGET
+ * file, and return an appropriate hunk_info object in *HI, allocated from
+ * RESULT_POOL. Use fuzz factor FUZZ. Set HI->FUZZ to FUZZ. If no correct
+ * line can be determined, set HI->REJECTED to TRUE.
+ * IGNORE_WHITESPACE tells whether whitespace should be considered when
+ * matching. IS_PROP_HUNK indicates whether the hunk patches file content
+ * or a property.
+ * When this function returns, neither CONTENT->CURRENT_LINE nor
+ * the file offset in the target file will have changed.
+ * Call cancel CANCEL_FUNC with baton CANCEL_BATON to trigger cancellation.
+ * Do temporary allocations in POOL. */
+static svn_error_t *
+get_hunk_info(hunk_info_t **hi, patch_target_t *target,
+ target_content_t *content,
+ svn_diff_hunk_t *hunk, svn_linenum_t fuzz,
+ svn_boolean_t ignore_whitespace,
+ svn_boolean_t is_prop_hunk,
+ svn_cancel_func_t cancel_func, void *cancel_baton,
+ apr_pool_t *result_pool, apr_pool_t *scratch_pool)
+{
+ svn_linenum_t matched_line;
+ svn_linenum_t original_start;
+ svn_boolean_t already_applied;
+
+ original_start = svn_diff_hunk_get_original_start(hunk);
+ already_applied = FALSE;
+
+ /* An original offset of zero means that this hunk wants to create
+ * a new file. Don't bother matching hunks in that case, since
+ * the hunk applies at line 1. If the file already exists, the hunk
+ * is rejected, unless the file is versioned and its content matches
+ * the file the patch wants to create. */
+ if (original_start == 0 && fuzz > 0)
+ {
+ matched_line = 0; /* reject any fuzz for new files */
+ }
+ else if (original_start == 0 && ! is_prop_hunk)
+ {
+ if (target->kind_on_disk == svn_node_file)
+ {
+ const svn_io_dirent2_t *dirent;
+ SVN_ERR(svn_io_stat_dirent2(&dirent, target->local_abspath, FALSE,
+ TRUE, scratch_pool, scratch_pool));
+
+ if (dirent->kind == svn_node_file
+ && !dirent->special
+ && dirent->filesize == 0)
+ {
+ matched_line = 1; /* Matched an on-disk empty file */
+ }
+ else
+ {
+ if (target->db_kind == svn_node_file)
+ {
+ svn_boolean_t file_matches;
+
+ /* ### I can't reproduce anything but a no-match here.
+ The content is already at eof, so any hunk fails */
+ SVN_ERR(match_existing_target(&file_matches, content, hunk,
+ scratch_pool));
+ if (file_matches)
+ {
+ matched_line = 1;
+ already_applied = TRUE;
+ }
+ else
+ matched_line = 0; /* reject */
+ }
+ else
+ matched_line = 0; /* reject */
+ }
+ }
+ else
+ matched_line = 1;
+ }
+ /* Same conditions apply as for the file case above.
+ *
+ * ### Since the hunk says the prop should be added we just assume so for
+ * ### now and don't bother with storing the previous lines and such. When
+ * ### we have the diff operation available we can just check for adds. */
+ else if (original_start == 0 && is_prop_hunk)
+ {
+ if (content->existed)
+ {
+ svn_boolean_t prop_matches;
+
+ SVN_ERR(match_existing_target(&prop_matches, content, hunk,
+ scratch_pool));
+
+ if (prop_matches)
+ {
+ matched_line = 1;
+ already_applied = TRUE;
+ }
+ else
+ matched_line = 0; /* reject */
+ }
+ else
+ matched_line = 1;
+ }
+ else if (original_start > 0 && content->existed)
+ {
+ svn_linenum_t saved_line = content->current_line;
+
+ /* Scan for a match at the line where the hunk thinks it
+ * should be going. */
+ SVN_ERR(seek_to_line(content, original_start, scratch_pool));
+ if (content->current_line != original_start)
+ {
+ /* Seek failed. */
+ matched_line = 0;
+ }
+ else
+ SVN_ERR(scan_for_match(&matched_line, content, hunk, TRUE,
+ original_start + 1, fuzz,
+ ignore_whitespace, FALSE,
+ cancel_func, cancel_baton,
+ scratch_pool));
+
+ if (matched_line != original_start)
+ {
+ /* Check if the hunk is already applied.
+ * We only check for an exact match here, and don't bother checking
+ * for already applied patches with offset/fuzz, because such a
+ * check would be ambiguous. */
+ if (fuzz == 0)
+ {
+ svn_linenum_t modified_start;
+
+ modified_start = svn_diff_hunk_get_modified_start(hunk);
+ if (modified_start == 0)
+ {
+ /* Patch wants to delete the file. */
+ already_applied = target->locally_deleted;
+ }
+ else
+ {
+ SVN_ERR(seek_to_line(content, modified_start,
+ scratch_pool));
+ SVN_ERR(scan_for_match(&matched_line, content,
+ hunk, TRUE,
+ modified_start + 1,
+ fuzz, ignore_whitespace, TRUE,
+ cancel_func, cancel_baton,
+ scratch_pool));
+ already_applied = (matched_line == modified_start);
+ }
+ }
+ else
+ already_applied = FALSE;
+
+ if (! already_applied)
+ {
+ /* Scan the whole file again from the start. */
+ SVN_ERR(seek_to_line(content, 1, scratch_pool));
+
+ /* Scan forward towards the hunk's line and look for a line
+ * where the hunk matches. */
+ SVN_ERR(scan_for_match(&matched_line, content, hunk, FALSE,
+ original_start, fuzz,
+ ignore_whitespace, FALSE,
+ cancel_func, cancel_baton,
+ scratch_pool));
+
+ /* In tie-break situations, we arbitrarily prefer early matches
+ * to save us from scanning the rest of the file. */
+ if (matched_line == 0)
+ {
+ /* Scan forward towards the end of the file and look
+ * for a line where the hunk matches. */
+ SVN_ERR(scan_for_match(&matched_line, content, hunk,
+ TRUE, 0, fuzz, ignore_whitespace,
+ FALSE, cancel_func, cancel_baton,
+ scratch_pool));
+ }
+ }
+ }
+
+ SVN_ERR(seek_to_line(content, saved_line, scratch_pool));
+ }
+ else
+ {
+ /* The hunk wants to modify a file which doesn't exist. */
+ matched_line = 0;
+ }
+
+ (*hi) = apr_pcalloc(result_pool, sizeof(hunk_info_t));
+ (*hi)->hunk = hunk;
+ (*hi)->matched_line = matched_line;
+ (*hi)->rejected = (matched_line == 0);
+ (*hi)->already_applied = already_applied;
+ (*hi)->fuzz = fuzz;
+
+ return SVN_NO_ERROR;
+}
+
+/* Copy lines to the patched content until the specified LINE has been
+ * reached. Indicate in *EOF whether end-of-file was encountered while
+ * reading from the target.
+ * If LINE is zero, copy lines until end-of-file has been reached.
+ * Do all allocations in POOL. */
+static svn_error_t *
+copy_lines_to_target(target_content_t *content, svn_linenum_t line,
+ apr_pool_t *pool)
+{
+ apr_pool_t *iterpool;
+
+ iterpool = svn_pool_create(pool);
+ while ((content->current_line < line || line == 0) && ! content->eof)
+ {
+ const char *target_line;
+ apr_size_t len;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(readline(content, &target_line, iterpool, iterpool));
+ if (! content->eof)
+ target_line = apr_pstrcat(iterpool, target_line, content->eol_str,
+ (char *)NULL);
+ len = strlen(target_line);
+ SVN_ERR(content->write(content->write_baton, target_line,
+ len, iterpool));
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Write the diff text of HUNK to TARGET's reject file,
+ * and mark TARGET as having had rejects.
+ * We don't expand keywords, nor normalise line-endings, in reject files.
+ * Do temporary allocations in SCRATCH_POOL. */
+static svn_error_t *
+reject_hunk(patch_target_t *target, target_content_t *content,
+ svn_diff_hunk_t *hunk, const char *prop_name,
+ apr_pool_t *pool)
+{
+ const char *hunk_header;
+ apr_size_t len;
+ svn_boolean_t eof;
+ static const char * const text_atat = "@@";
+ static const char * const prop_atat = "##";
+ const char *atat;
+ apr_pool_t *iterpool;
+
+ if (prop_name)
+ {
+ const char *prop_header;
+
+ /* ### Print 'Added', 'Deleted' or 'Modified' instead of 'Property'.
+ */
+ prop_header = apr_psprintf(pool, "Property: %s\n", prop_name);
+ len = strlen(prop_header);
+ SVN_ERR(svn_io_file_write_full(target->reject_file, prop_header,
+ len, &len, pool));
+ atat = prop_atat;
+ }
+ else
+ {
+ atat = text_atat;
+ }
+
+ hunk_header = apr_psprintf(pool, "%s -%lu,%lu +%lu,%lu %s%s",
+ atat,
+ svn_diff_hunk_get_original_start(hunk),
+ svn_diff_hunk_get_original_length(hunk),
+ svn_diff_hunk_get_modified_start(hunk),
+ svn_diff_hunk_get_modified_length(hunk),
+ atat,
+ APR_EOL_STR);
+ len = strlen(hunk_header);
+ SVN_ERR(svn_io_file_write_full(target->reject_file, hunk_header, len,
+ &len, pool));
+
+ iterpool = svn_pool_create(pool);
+ do
+ {
+ svn_stringbuf_t *hunk_line;
+ const char *eol_str;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_diff_hunk_readline_diff_text(hunk, &hunk_line, &eol_str,
+ &eof, iterpool, iterpool));
+ if (! eof)
+ {
+ if (hunk_line->len >= 1)
+ {
+ len = hunk_line->len;
+ SVN_ERR(svn_io_file_write_full(target->reject_file,
+ hunk_line->data, len, &len,
+ iterpool));
+ }
+
+ if (eol_str)
+ {
+ len = strlen(eol_str);
+ SVN_ERR(svn_io_file_write_full(target->reject_file, eol_str,
+ len, &len, iterpool));
+ }
+ }
+ }
+ while (! eof);
+ svn_pool_destroy(iterpool);
+
+ if (prop_name)
+ target->had_prop_rejects = TRUE;
+ else
+ target->had_rejects = TRUE;
+
+ return SVN_NO_ERROR;
+}
+
+/* Write the modified text of the hunk described by HI to the patched
+ * CONTENT. TARGET is the patch target.
+ * If PROP_NAME is not NULL, the hunk is assumed to be targeted for
+ * a property with the given name.
+ * Do temporary allocations in POOL. */
+static svn_error_t *
+apply_hunk(patch_target_t *target, target_content_t *content,
+ hunk_info_t *hi, const char *prop_name, apr_pool_t *pool)
+{
+ svn_linenum_t lines_read;
+ svn_boolean_t eof;
+ apr_pool_t *iterpool;
+
+ /* ### Is there a cleaner way to describe if we have an existing target?
+ */
+ if (target->kind_on_disk == svn_node_file || prop_name)
+ {
+ svn_linenum_t line;
+
+ /* Move forward to the hunk's line, copying data as we go.
+ * Also copy leading lines of context which matched with fuzz.
+ * The target has changed on the fuzzy-matched lines,
+ * so we should retain the target's version of those lines. */
+ SVN_ERR(copy_lines_to_target(content, hi->matched_line + hi->fuzz,
+ pool));
+
+ /* Skip the target's version of the hunk.
+ * Don't skip trailing lines which matched with fuzz. */
+ line = content->current_line +
+ svn_diff_hunk_get_original_length(hi->hunk) - (2 * hi->fuzz);
+ SVN_ERR(seek_to_line(content, line, pool));
+ if (content->current_line != line && ! content->eof)
+ {
+ /* Seek failed, reject this hunk. */
+ hi->rejected = TRUE;
+ SVN_ERR(reject_hunk(target, content, hi->hunk, prop_name, pool));
+ return SVN_NO_ERROR;
+ }
+ }
+
+ /* Write the hunk's version to the patched result.
+ * Don't write the lines which matched with fuzz. */
+ lines_read = 0;
+ svn_diff_hunk_reset_modified_text(hi->hunk);
+ iterpool = svn_pool_create(pool);
+ do
+ {
+ svn_stringbuf_t *hunk_line;
+ const char *eol_str;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_diff_hunk_readline_modified_text(hi->hunk, &hunk_line,
+ &eol_str, &eof,
+ iterpool, iterpool));
+ lines_read++;
+ if (lines_read > hi->fuzz &&
+ lines_read <= svn_diff_hunk_get_modified_length(hi->hunk) - hi->fuzz)
+ {
+ apr_size_t len;
+
+ if (hunk_line->len >= 1)
+ {
+ len = hunk_line->len;
+ SVN_ERR(content->write(content->write_baton,
+ hunk_line->data, len, iterpool));
+ }
+
+ if (eol_str)
+ {
+ /* Use the EOL as it was read from the patch file,
+ * unless the target's EOL style is set by svn:eol-style */
+ if (content->eol_style != svn_subst_eol_style_none)
+ eol_str = content->eol_str;
+
+ len = strlen(eol_str);
+ SVN_ERR(content->write(content->write_baton,
+ eol_str, len, iterpool));
+ }
+ }
+ }
+ while (! eof);
+ svn_pool_destroy(iterpool);
+
+ if (prop_name)
+ target->has_prop_changes = TRUE;
+ else
+ target->has_text_changes = TRUE;
+
+ return SVN_NO_ERROR;
+}
+
+/* Use client context CTX to send a suitable notification for hunk HI,
+ * using TARGET to determine the path. If the hunk is a property hunk,
+ * PROP_NAME must be the name of the property, else NULL.
+ * Use POOL for temporary allocations. */
+static svn_error_t *
+send_hunk_notification(const hunk_info_t *hi,
+ const patch_target_t *target,
+ const char *prop_name,
+ const svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_wc_notify_t *notify;
+ svn_wc_notify_action_t action;
+
+ if (hi->already_applied)
+ action = svn_wc_notify_patch_hunk_already_applied;
+ else if (hi->rejected)
+ action = svn_wc_notify_patch_rejected_hunk;
+ else
+ action = svn_wc_notify_patch_applied_hunk;
+
+ notify = svn_wc_create_notify(target->local_abspath
+ ? target->local_abspath
+ : target->local_relpath,
+ action, pool);
+ notify->hunk_original_start =
+ svn_diff_hunk_get_original_start(hi->hunk);
+ notify->hunk_original_length =
+ svn_diff_hunk_get_original_length(hi->hunk);
+ notify->hunk_modified_start =
+ svn_diff_hunk_get_modified_start(hi->hunk);
+ notify->hunk_modified_length =
+ svn_diff_hunk_get_modified_length(hi->hunk);
+ notify->hunk_matched_line = hi->matched_line;
+ notify->hunk_fuzz = hi->fuzz;
+ notify->prop_name = prop_name;
+
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Use client context CTX to send a suitable notification for a patch TARGET.
+ * Use POOL for temporary allocations. */
+static svn_error_t *
+send_patch_notification(const patch_target_t *target,
+ const svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_wc_notify_t *notify;
+ svn_wc_notify_action_t action;
+
+ if (! ctx->notify_func2)
+ return SVN_NO_ERROR;
+
+ if (target->skipped)
+ action = svn_wc_notify_skip;
+ else if (target->deleted)
+ action = svn_wc_notify_delete;
+ else if (target->added || target->replaced)
+ action = svn_wc_notify_add;
+ else
+ action = svn_wc_notify_patch;
+
+ notify = svn_wc_create_notify(target->local_abspath ? target->local_abspath
+ : target->local_relpath,
+ action, pool);
+ notify->kind = svn_node_file;
+
+ if (action == svn_wc_notify_skip)
+ {
+ if (target->db_kind == svn_node_none ||
+ target->db_kind == svn_node_unknown)
+ notify->content_state = svn_wc_notify_state_missing;
+ else if (target->db_kind == svn_node_dir)
+ notify->content_state = svn_wc_notify_state_obstructed;
+ else
+ notify->content_state = svn_wc_notify_state_unknown;
+ }
+ else
+ {
+ if (target->had_rejects)
+ notify->content_state = svn_wc_notify_state_conflicted;
+ else if (target->local_mods)
+ notify->content_state = svn_wc_notify_state_merged;
+ else if (target->has_text_changes)
+ notify->content_state = svn_wc_notify_state_changed;
+
+ if (target->had_prop_rejects)
+ notify->prop_state = svn_wc_notify_state_conflicted;
+ else if (target->has_prop_changes)
+ notify->prop_state = svn_wc_notify_state_changed;
+ }
+
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+
+ if (action == svn_wc_notify_patch)
+ {
+ int i;
+ apr_pool_t *iterpool;
+ apr_hash_index_t *hash_index;
+
+ iterpool = svn_pool_create(pool);
+ for (i = 0; i < target->content->hunks->nelts; i++)
+ {
+ const hunk_info_t *hi;
+
+ svn_pool_clear(iterpool);
+
+ hi = APR_ARRAY_IDX(target->content->hunks, i, hunk_info_t *);
+
+ SVN_ERR(send_hunk_notification(hi, target, NULL /* prop_name */,
+ ctx, iterpool));
+ }
+
+ for (hash_index = apr_hash_first(pool, target->prop_targets);
+ hash_index;
+ hash_index = apr_hash_next(hash_index))
+ {
+ prop_patch_target_t *prop_target;
+
+ prop_target = svn__apr_hash_index_val(hash_index);
+
+ for (i = 0; i < prop_target->content->hunks->nelts; i++)
+ {
+ const hunk_info_t *hi;
+
+ svn_pool_clear(iterpool);
+
+ hi = APR_ARRAY_IDX(prop_target->content->hunks, i,
+ hunk_info_t *);
+
+ /* Don't notify on the hunk level for added or deleted props. */
+ if (prop_target->operation != svn_diff_op_added &&
+ prop_target->operation != svn_diff_op_deleted)
+ SVN_ERR(send_hunk_notification(hi, target, prop_target->name,
+ ctx, iterpool));
+ }
+ }
+ svn_pool_destroy(iterpool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Apply a PATCH to a working copy at ABS_WC_PATH and put the result
+ * into temporary files, to be installed in the working copy later.
+ * Return information about the patch target in *PATCH_TARGET, allocated
+ * in RESULT_POOL. Use WC_CTX as the working copy context.
+ * STRIP_COUNT specifies the number of leading path components
+ * which should be stripped from target paths in the patch.
+ * REMOVE_TEMPFILES, PATCH_FUNC, and PATCH_BATON as in svn_client_patch().
+ * IGNORE_WHITESPACE tells whether whitespace should be considered when
+ * doing the matching.
+ * Call cancel CANCEL_FUNC with baton CANCEL_BATON to trigger cancellation.
+ * Do temporary allocations in SCRATCH_POOL. */
+static svn_error_t *
+apply_one_patch(patch_target_t **patch_target, svn_patch_t *patch,
+ const char *abs_wc_path, svn_wc_context_t *wc_ctx,
+ int strip_count,
+ svn_boolean_t ignore_whitespace,
+ svn_boolean_t remove_tempfiles,
+ svn_client_patch_func_t patch_func,
+ void *patch_baton,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ apr_pool_t *result_pool, apr_pool_t *scratch_pool)
+{
+ patch_target_t *target;
+ apr_pool_t *iterpool;
+ int i;
+ static const svn_linenum_t MAX_FUZZ = 2;
+ apr_hash_index_t *hash_index;
+
+ SVN_ERR(init_patch_target(&target, patch, abs_wc_path, wc_ctx, strip_count,
+ remove_tempfiles, result_pool, scratch_pool));
+ if (target->skipped)
+ {
+ *patch_target = target;
+ return SVN_NO_ERROR;
+ }
+
+ if (patch_func)
+ {
+ SVN_ERR(patch_func(patch_baton, &target->filtered,
+ target->canon_path_from_patchfile,
+ target->patched_path, target->reject_path,
+ scratch_pool));
+ if (target->filtered)
+ {
+ *patch_target = target;
+ return SVN_NO_ERROR;
+ }
+ }
+
+ iterpool = svn_pool_create(scratch_pool);
+ /* Match hunks. */
+ for (i = 0; i < patch->hunks->nelts; i++)
+ {
+ svn_diff_hunk_t *hunk;
+ hunk_info_t *hi;
+ svn_linenum_t fuzz = 0;
+
+ svn_pool_clear(iterpool);
+
+ if (cancel_func)
+ SVN_ERR(cancel_func(cancel_baton));
+
+ hunk = APR_ARRAY_IDX(patch->hunks, i, svn_diff_hunk_t *);
+
+ /* Determine the line the hunk should be applied at.
+ * If no match is found initially, try with fuzz. */
+ do
+ {
+ SVN_ERR(get_hunk_info(&hi, target, target->content, hunk, fuzz,
+ ignore_whitespace,
+ FALSE /* is_prop_hunk */,
+ cancel_func, cancel_baton,
+ result_pool, iterpool));
+ fuzz++;
+ }
+ while (hi->rejected && fuzz <= MAX_FUZZ && ! hi->already_applied);
+
+ APR_ARRAY_PUSH(target->content->hunks, hunk_info_t *) = hi;
+ }
+
+ /* Apply or reject hunks. */
+ for (i = 0; i < target->content->hunks->nelts; i++)
+ {
+ hunk_info_t *hi;
+
+ svn_pool_clear(iterpool);
+
+ if (cancel_func)
+ SVN_ERR(cancel_func(cancel_baton));
+
+ hi = APR_ARRAY_IDX(target->content->hunks, i, hunk_info_t *);
+ if (hi->already_applied)
+ continue;
+ else if (hi->rejected)
+ SVN_ERR(reject_hunk(target, target->content, hi->hunk,
+ NULL /* prop_name */,
+ iterpool));
+ else
+ SVN_ERR(apply_hunk(target, target->content, hi,
+ NULL /* prop_name */, iterpool));
+ }
+
+ if (target->kind_on_disk == svn_node_file)
+ {
+ /* Copy any remaining lines to target. */
+ SVN_ERR(copy_lines_to_target(target->content, 0, scratch_pool));
+ if (! target->content->eof)
+ {
+ /* We could not copy the entire target file to the temporary file,
+ * and would truncate the target if we copied the temporary file
+ * on top of it. Skip this target. */
+ target->skipped = TRUE;
+ }
+ }
+
+ /* Match property hunks. */
+ for (hash_index = apr_hash_first(scratch_pool, patch->prop_patches);
+ hash_index;
+ hash_index = apr_hash_next(hash_index))
+ {
+ svn_prop_patch_t *prop_patch;
+ const char *prop_name;
+ prop_patch_target_t *prop_target;
+
+ prop_name = svn__apr_hash_index_key(hash_index);
+ prop_patch = svn__apr_hash_index_val(hash_index);
+
+ if (! strcmp(prop_name, SVN_PROP_SPECIAL))
+ target->is_special = TRUE;
+
+ /* We'll store matched hunks in prop_content. */
+ prop_target = svn_hash_gets(target->prop_targets, prop_name);
+
+ for (i = 0; i < prop_patch->hunks->nelts; i++)
+ {
+ svn_diff_hunk_t *hunk;
+ hunk_info_t *hi;
+ svn_linenum_t fuzz = 0;
+
+ svn_pool_clear(iterpool);
+
+ if (cancel_func)
+ SVN_ERR(cancel_func(cancel_baton));
+
+ hunk = APR_ARRAY_IDX(prop_patch->hunks, i, svn_diff_hunk_t *);
+
+ /* Determine the line the hunk should be applied at.
+ * If no match is found initially, try with fuzz. */
+ do
+ {
+ SVN_ERR(get_hunk_info(&hi, target, prop_target->content,
+ hunk, fuzz,
+ ignore_whitespace,
+ TRUE /* is_prop_hunk */,
+ cancel_func, cancel_baton,
+ result_pool, iterpool));
+ fuzz++;
+ }
+ while (hi->rejected && fuzz <= MAX_FUZZ && ! hi->already_applied);
+
+ APR_ARRAY_PUSH(prop_target->content->hunks, hunk_info_t *) = hi;
+ }
+ }
+
+ /* Apply or reject property hunks. */
+ for (hash_index = apr_hash_first(scratch_pool, target->prop_targets);
+ hash_index;
+ hash_index = apr_hash_next(hash_index))
+ {
+ prop_patch_target_t *prop_target;
+
+ prop_target = svn__apr_hash_index_val(hash_index);
+
+ for (i = 0; i < prop_target->content->hunks->nelts; i++)
+ {
+ hunk_info_t *hi;
+
+ svn_pool_clear(iterpool);
+
+ hi = APR_ARRAY_IDX(prop_target->content->hunks, i,
+ hunk_info_t *);
+ if (hi->already_applied)
+ continue;
+ else if (hi->rejected)
+ SVN_ERR(reject_hunk(target, prop_target->content, hi->hunk,
+ prop_target->name,
+ iterpool));
+ else
+ SVN_ERR(apply_hunk(target, prop_target->content, hi,
+ prop_target->name,
+ iterpool));
+ }
+
+ if (prop_target->content->existed)
+ {
+ /* Copy any remaining lines to target. */
+ SVN_ERR(copy_lines_to_target(prop_target->content, 0,
+ scratch_pool));
+ if (! prop_target->content->eof)
+ {
+ /* We could not copy the entire target property to the
+ * temporary file, and would truncate the target if we
+ * copied the temporary file on top of it. Skip this target. */
+ target->skipped = TRUE;
+ }
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+
+ if (!target->is_symlink)
+ {
+ /* Now close files we don't need any longer to get their contents
+ * flushed to disk.
+ * But we're not closing the reject file -- it still needed and
+ * will be closed later in write_out_rejected_hunks(). */
+ if (target->kind_on_disk == svn_node_file)
+ SVN_ERR(svn_io_file_close(target->file, scratch_pool));
+
+ SVN_ERR(svn_io_file_close(target->patched_file, scratch_pool));
+ }
+
+ if (! target->skipped)
+ {
+ apr_finfo_t working_file;
+ apr_finfo_t patched_file;
+
+ /* Get sizes of the patched temporary file and the working file.
+ * We'll need those to figure out whether we should delete the
+ * patched file. */
+ SVN_ERR(svn_io_stat(&patched_file, target->patched_path,
+ APR_FINFO_SIZE | APR_FINFO_LINK, scratch_pool));
+ if (target->kind_on_disk == svn_node_file)
+ SVN_ERR(svn_io_stat(&working_file, target->local_abspath,
+ APR_FINFO_SIZE | APR_FINFO_LINK, scratch_pool));
+ else
+ working_file.size = 0;
+
+ if (patched_file.size == 0 && working_file.size > 0)
+ {
+ /* If a unidiff removes all lines from a file, that usually
+ * means deletion, so we can confidently schedule the target
+ * for deletion. In the rare case where the unidiff was really
+ * meant to replace a file with an empty one, this may not
+ * be desirable. But the deletion can easily be reverted and
+ * creating an empty file manually is not exactly hard either. */
+ target->deleted = (target->db_kind == svn_node_file);
+ }
+ else if (patched_file.size == 0 && working_file.size == 0)
+ {
+ /* The target was empty or non-existent to begin with
+ * and no content was changed by patching.
+ * Report this as skipped if it didn't exist, unless in the special
+ * case of adding an empty file which has properties set on it or
+ * adding an empty file with a 'git diff' */
+ if (target->kind_on_disk == svn_node_none
+ && ! target->has_prop_changes
+ && ! target->added)
+ target->skipped = TRUE;
+ }
+ else if (patched_file.size > 0 && working_file.size == 0)
+ {
+ /* The patch has created a file. */
+ if (target->locally_deleted)
+ target->replaced = TRUE;
+ else if (target->db_kind == svn_node_none)
+ target->added = TRUE;
+ }
+ }
+
+ *patch_target = target;
+
+ return SVN_NO_ERROR;
+}
+
+/* Try to create missing parent directories for TARGET in the working copy
+ * rooted at ABS_WC_PATH, and add the parents to version control.
+ * If the parents cannot be created, mark the target as skipped.
+ * Use client context CTX. If DRY_RUN is true, do not create missing
+ * parents but issue notifications only.
+ * Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+create_missing_parents(patch_target_t *target,
+ const char *abs_wc_path,
+ svn_client_ctx_t *ctx,
+ svn_boolean_t dry_run,
+ apr_pool_t *scratch_pool)
+{
+ const char *local_abspath;
+ apr_array_header_t *components;
+ int present_components;
+ int i;
+ apr_pool_t *iterpool;
+
+ /* Check if we can safely create the target's parent. */
+ local_abspath = abs_wc_path;
+ components = svn_path_decompose(target->local_relpath, scratch_pool);
+ present_components = 0;
+ iterpool = svn_pool_create(scratch_pool);
+ for (i = 0; i < components->nelts - 1; i++)
+ {
+ const char *component;
+ svn_node_kind_t wc_kind, disk_kind;
+
+ svn_pool_clear(iterpool);
+
+ component = APR_ARRAY_IDX(components, i, const char *);
+ local_abspath = svn_dirent_join(local_abspath, component, scratch_pool);
+
+ SVN_ERR(svn_wc_read_kind2(&wc_kind, ctx->wc_ctx, local_abspath,
+ FALSE, TRUE, iterpool));
+
+ SVN_ERR(svn_io_check_path(local_abspath, &disk_kind, iterpool));
+
+ if (disk_kind == svn_node_file || wc_kind == svn_node_file)
+ {
+ /* on-disk files and missing files are obstructions */
+ target->skipped = TRUE;
+ break;
+ }
+ else if (disk_kind == svn_node_dir)
+ {
+ if (wc_kind == svn_node_dir)
+ present_components++;
+ else
+ {
+ target->skipped = TRUE;
+ break;
+ }
+ }
+ else if (wc_kind != svn_node_none)
+ {
+ /* Node is missing */
+ target->skipped = TRUE;
+ break;
+ }
+ else
+ {
+ /* It's not a file, it's not a dir...
+ Let's add a dir */
+ break;
+ }
+ }
+ if (! target->skipped)
+ {
+ local_abspath = abs_wc_path;
+ for (i = 0; i < present_components; i++)
+ {
+ const char *component;
+ component = APR_ARRAY_IDX(components, i, const char *);
+ local_abspath = svn_dirent_join(local_abspath,
+ component, scratch_pool);
+ }
+
+ if (!dry_run && present_components < components->nelts - 1)
+ SVN_ERR(svn_io_make_dir_recursively(
+ svn_dirent_join(
+ abs_wc_path,
+ svn_relpath_dirname(target->local_relpath,
+ scratch_pool),
+ scratch_pool),
+ scratch_pool));
+
+ for (i = present_components; i < components->nelts - 1; i++)
+ {
+ const char *component;
+
+ svn_pool_clear(iterpool);
+
+ component = APR_ARRAY_IDX(components, i, const char *);
+ local_abspath = svn_dirent_join(local_abspath, component,
+ scratch_pool);
+ if (dry_run)
+ {
+ if (ctx->notify_func2)
+ {
+ /* Just do notification. */
+ svn_wc_notify_t *notify;
+ notify = svn_wc_create_notify(local_abspath,
+ svn_wc_notify_add,
+ iterpool);
+ notify->kind = svn_node_dir;
+ ctx->notify_func2(ctx->notify_baton2, notify,
+ iterpool);
+ }
+ }
+ else
+ {
+ /* Create the missing component and add it
+ * to version control. Allow cancellation since we
+ * have not modified the working copy yet for this
+ * target. */
+
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ SVN_ERR(svn_wc_add_from_disk2(ctx->wc_ctx, local_abspath,
+ NULL /*props*/,
+ ctx->notify_func2, ctx->notify_baton2,
+ iterpool));
+ }
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Install a patched TARGET into the working copy at ABS_WC_PATH.
+ * Use client context CTX to retrieve WC_CTX, and possibly doing
+ * notifications. If DRY_RUN is TRUE, don't modify the working copy.
+ * Do temporary allocations in POOL. */
+static svn_error_t *
+install_patched_target(patch_target_t *target, const char *abs_wc_path,
+ svn_client_ctx_t *ctx, svn_boolean_t dry_run,
+ apr_pool_t *pool)
+{
+ if (target->deleted)
+ {
+ if (! dry_run)
+ {
+ /* Schedule the target for deletion. Suppress
+ * notification, we'll do it manually in a minute
+ * because we also need to notify during dry-run.
+ * Also suppress cancellation, because we'd rather
+ * notify about what we did before aborting. */
+ SVN_ERR(svn_wc_delete4(ctx->wc_ctx, target->local_abspath,
+ FALSE /* keep_local */, FALSE,
+ NULL, NULL, NULL, NULL, pool));
+ }
+ }
+ else
+ {
+ svn_node_kind_t parent_db_kind;
+ if (target->added || target->replaced)
+ {
+ const char *parent_abspath;
+
+ parent_abspath = svn_dirent_dirname(target->local_abspath,
+ pool);
+ /* If the target's parent directory does not yet exist
+ * we need to create it before we can copy the patched
+ * result in place. */
+ SVN_ERR(svn_wc_read_kind2(&parent_db_kind, ctx->wc_ctx,
+ parent_abspath, FALSE, FALSE, pool));
+
+ /* We can't add targets under nodes scheduled for delete, so add
+ a new directory if needed. */
+ if (parent_db_kind == svn_node_dir
+ || parent_db_kind == svn_node_file)
+ {
+ if (parent_db_kind != svn_node_dir)
+ target->skipped = TRUE;
+ else
+ {
+ svn_node_kind_t disk_kind;
+
+ SVN_ERR(svn_io_check_path(parent_abspath, &disk_kind, pool));
+ if (disk_kind != svn_node_dir)
+ target->skipped = TRUE;
+ }
+ }
+ else
+ SVN_ERR(create_missing_parents(target, abs_wc_path, ctx,
+ dry_run, pool));
+
+ }
+ else
+ {
+ svn_node_kind_t wc_kind;
+
+ /* The target should exist */
+ SVN_ERR(svn_wc_read_kind2(&wc_kind, ctx->wc_ctx,
+ target->local_abspath,
+ FALSE, FALSE, pool));
+
+ if (target->kind_on_disk == svn_node_none
+ || wc_kind != target->kind_on_disk)
+ {
+ target->skipped = TRUE;
+ }
+ }
+
+ if (! dry_run && ! target->skipped)
+ {
+ if (target->is_special)
+ {
+ svn_stream_t *stream;
+ svn_stream_t *patched_stream;
+
+ SVN_ERR(svn_stream_open_readonly(&patched_stream,
+ target->patched_path,
+ pool, pool));
+ SVN_ERR(svn_subst_create_specialfile(&stream,
+ target->local_abspath,
+ pool, pool));
+ SVN_ERR(svn_stream_copy3(patched_stream, stream,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool));
+ }
+ else
+ {
+ svn_boolean_t repair_eol;
+
+ /* Copy the patched file on top of the target file.
+ * Always expand keywords in the patched file, but repair EOL
+ * only if svn:eol-style dictates a particular style. */
+ repair_eol = (target->content->eol_style ==
+ svn_subst_eol_style_fixed ||
+ target->content->eol_style ==
+ svn_subst_eol_style_native);
+
+ SVN_ERR(svn_subst_copy_and_translate4(
+ target->patched_path, target->local_abspath,
+ target->content->eol_str, repair_eol,
+ target->content->keywords,
+ TRUE /* expand */, FALSE /* special */,
+ ctx->cancel_func, ctx->cancel_baton, pool));
+ }
+
+ if (target->added || target->replaced)
+ {
+ /* The target file didn't exist previously,
+ * so add it to version control.
+ * Suppress notification, we'll do that later (and also
+ * during dry-run). Don't allow cancellation because
+ * we'd rather notify about what we did before aborting. */
+ SVN_ERR(svn_wc_add_from_disk2(ctx->wc_ctx, target->local_abspath,
+ NULL /*props*/,
+ NULL, NULL, pool));
+ }
+
+ /* Restore the target's executable bit if necessary. */
+ SVN_ERR(svn_io_set_file_executable(target->local_abspath,
+ target->executable,
+ FALSE, pool));
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Write out rejected hunks, if any, to TARGET->REJECT_PATH. If DRY_RUN is
+ * TRUE, don't modify the working copy.
+ * Do temporary allocations in POOL.
+ */
+static svn_error_t *
+write_out_rejected_hunks(patch_target_t *target,
+ svn_boolean_t dry_run,
+ apr_pool_t *pool)
+{
+ SVN_ERR(svn_io_file_close(target->reject_file, pool));
+
+ if (! dry_run && (target->had_rejects || target->had_prop_rejects))
+ {
+ /* Write out rejected hunks, if any. */
+ SVN_ERR(svn_io_copy_file(target->reject_path,
+ apr_psprintf(pool, "%s.svnpatch.rej",
+ target->local_abspath),
+ FALSE, pool));
+ /* ### TODO mark file as conflicted. */
+ }
+ return SVN_NO_ERROR;
+}
+
+/* Install the patched properties for TARGET. Use client context CTX to
+ * retrieve WC_CTX. If DRY_RUN is TRUE, don't modify the working copy.
+ * Do temporary allocations in SCRATCH_POOL. */
+static svn_error_t *
+install_patched_prop_targets(patch_target_t *target,
+ svn_client_ctx_t *ctx, svn_boolean_t dry_run,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool;
+
+ iterpool = svn_pool_create(scratch_pool);
+
+ for (hi = apr_hash_first(scratch_pool, target->prop_targets);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ prop_patch_target_t *prop_target = svn__apr_hash_index_val(hi);
+ const svn_string_t *prop_val;
+ svn_error_t *err;
+
+ svn_pool_clear(iterpool);
+
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ /* For a deleted prop we only set the value to NULL. */
+ if (prop_target->operation == svn_diff_op_deleted)
+ {
+ if (! dry_run)
+ SVN_ERR(svn_wc_prop_set4(ctx->wc_ctx, target->local_abspath,
+ prop_target->name, NULL, svn_depth_empty,
+ TRUE /* skip_checks */,
+ NULL /* changelist_filter */,
+ NULL, NULL /* cancellation */,
+ NULL, NULL /* notification */,
+ iterpool));
+ continue;
+ }
+
+ /* If the patch target doesn't exist yet, the patch wants to add an
+ * empty file with properties set on it. So create an empty file and
+ * add it to version control. But if the patch was in the 'git format'
+ * then the file has already been added.
+ *
+ * ### How can we tell whether the patch really wanted to create
+ * ### an empty directory? */
+ if (! target->has_text_changes
+ && target->kind_on_disk == svn_node_none
+ && ! target->added)
+ {
+ if (! dry_run)
+ {
+ SVN_ERR(svn_io_file_create(target->local_abspath, "",
+ scratch_pool));
+ SVN_ERR(svn_wc_add_from_disk2(ctx->wc_ctx, target->local_abspath,
+ NULL /*props*/,
+ /* suppress notification */
+ NULL, NULL,
+ iterpool));
+ }
+ target->added = TRUE;
+ }
+
+ /* Attempt to set the property, and reject all hunks if this
+ fails. If the property had a non-empty value, but now has
+ an empty one, we'll just delete the property altogether. */
+ if (prop_target->value && prop_target->value->len
+ && prop_target->patched_value && !prop_target->patched_value->len)
+ prop_val = NULL;
+ else
+ prop_val = svn_stringbuf__morph_into_string(prop_target->patched_value);
+
+ if (dry_run)
+ {
+ const svn_string_t *canon_propval;
+
+ err = svn_wc_canonicalize_svn_prop(&canon_propval,
+ prop_target->name,
+ prop_val, target->local_abspath,
+ target->db_kind,
+ TRUE, /* ### Skipping checks */
+ NULL, NULL,
+ iterpool);
+ }
+ else
+ {
+ err = svn_wc_prop_set4(ctx->wc_ctx, target->local_abspath,
+ prop_target->name, prop_val, svn_depth_empty,
+ TRUE /* skip_checks */,
+ NULL /* changelist_filter */,
+ NULL, NULL /* cancellation */,
+ NULL, NULL /* notification */,
+ iterpool);
+ }
+
+ if (err)
+ {
+ /* ### The errors which svn_wc_canonicalize_svn_prop() will
+ * ### return aren't documented. */
+ if (err->apr_err == SVN_ERR_ILLEGAL_TARGET ||
+ err->apr_err == SVN_ERR_NODE_UNEXPECTED_KIND ||
+ err->apr_err == SVN_ERR_IO_UNKNOWN_EOL ||
+ err->apr_err == SVN_ERR_BAD_MIME_TYPE ||
+ err->apr_err == SVN_ERR_CLIENT_INVALID_EXTERNALS_DESCRIPTION)
+ {
+ int i;
+
+ svn_error_clear(err);
+
+ for (i = 0; i < prop_target->content->hunks->nelts; i++)
+ {
+ hunk_info_t *hunk_info;
+
+ hunk_info = APR_ARRAY_IDX(prop_target->content->hunks,
+ i, hunk_info_t *);
+ hunk_info->rejected = TRUE;
+ SVN_ERR(reject_hunk(target, prop_target->content,
+ hunk_info->hunk, prop_target->name,
+ iterpool));
+ }
+ }
+ else
+ return svn_error_trace(err);
+ }
+
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Baton for can_delete_callback */
+struct can_delete_baton_t
+{
+ svn_boolean_t must_keep;
+ const apr_array_header_t *targets_info;
+ const char *local_abspath;
+};
+
+/* Implements svn_wc_status_func4_t. */
+static svn_error_t *
+can_delete_callback(void *baton,
+ const char *abspath,
+ const svn_wc_status3_t *status,
+ apr_pool_t *pool)
+{
+ struct can_delete_baton_t *cb = baton;
+ int i;
+
+ switch(status->node_status)
+ {
+ case svn_wc_status_none:
+ case svn_wc_status_deleted:
+ return SVN_NO_ERROR;
+
+ default:
+ if (! strcmp(cb->local_abspath, abspath))
+ return SVN_NO_ERROR; /* Only interested in descendants */
+
+ for (i = 0; i < cb->targets_info->nelts; i++)
+ {
+ const patch_target_info_t *target_info =
+ APR_ARRAY_IDX(cb->targets_info, i, const patch_target_info_t *);
+
+ if (! strcmp(target_info->local_abspath, abspath))
+ {
+ if (target_info->deleted)
+ return SVN_NO_ERROR;
+
+ break; /* Cease invocation; must keep */
+ }
+ }
+
+ cb->must_keep = TRUE;
+
+ return svn_error_create(SVN_ERR_CEASE_INVOCATION, NULL, NULL);
+ }
+}
+
+static svn_error_t *
+check_ancestor_delete(const char *deleted_target,
+ apr_array_header_t *targets_info,
+ const char *apply_root,
+ svn_boolean_t dry_run,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct can_delete_baton_t cb;
+ svn_error_t *err;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ const char *dir_abspath = svn_dirent_dirname(deleted_target, scratch_pool);
+
+ while (svn_dirent_is_child(apply_root, dir_abspath, iterpool))
+ {
+ svn_pool_clear(iterpool);
+
+ cb.local_abspath = dir_abspath;
+ cb.must_keep = FALSE;
+ cb.targets_info = targets_info;
+
+ err = svn_wc_walk_status(ctx->wc_ctx, dir_abspath, svn_depth_infinity,
+ TRUE, FALSE, FALSE, NULL,
+ can_delete_callback, &cb,
+ ctx->cancel_func, ctx->cancel_baton,
+ iterpool);
+
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_CEASE_INVOCATION)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ }
+
+ if (cb.must_keep)
+ {
+ break;
+ }
+
+ if (! dry_run)
+ {
+ SVN_ERR(svn_wc_delete4(ctx->wc_ctx, dir_abspath, FALSE, FALSE,
+ ctx->cancel_func, ctx->cancel_baton,
+ NULL, NULL,
+ scratch_pool));
+ }
+
+ {
+ patch_target_info_t *pti = apr_pcalloc(result_pool, sizeof(*pti));
+
+ pti->local_abspath = apr_pstrdup(result_pool, dir_abspath);
+ pti->deleted = TRUE;
+
+ APR_ARRAY_PUSH(targets_info, patch_target_info_t *) = pti;
+ }
+
+
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+
+ notify = svn_wc_create_notify(dir_abspath, svn_wc_notify_delete,
+ iterpool);
+ notify->kind = svn_node_dir;
+
+ ctx->notify_func2(ctx->notify_baton2, notify, iterpool);
+ }
+
+ /* And check if we must also delete the parent */
+ dir_abspath = svn_dirent_dirname(dir_abspath, scratch_pool);
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/* This function is the main entry point into the patch code. */
+static svn_error_t *
+apply_patches(/* The path to the patch file. */
+ const char *patch_abspath,
+ /* The abspath to the working copy the patch should be applied to. */
+ const char *abs_wc_path,
+ /* Indicates whether we're doing a dry run. */
+ svn_boolean_t dry_run,
+ /* Number of leading components to strip from patch target paths. */
+ int strip_count,
+ /* Whether to apply the patch in reverse. */
+ svn_boolean_t reverse,
+ /* Whether to ignore whitespace when matching context lines. */
+ svn_boolean_t ignore_whitespace,
+ /* As in svn_client_patch(). */
+ svn_boolean_t remove_tempfiles,
+ /* As in svn_client_patch(). */
+ svn_client_patch_func_t patch_func,
+ void *patch_baton,
+ /* The client context. */
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_patch_t *patch;
+ apr_pool_t *iterpool;
+ svn_patch_file_t *patch_file;
+ apr_array_header_t *targets_info;
+
+ /* Try to open the patch file. */
+ SVN_ERR(svn_diff_open_patch_file(&patch_file, patch_abspath, scratch_pool));
+
+ /* Apply patches. */
+ targets_info = apr_array_make(scratch_pool, 0,
+ sizeof(patch_target_info_t *));
+ iterpool = svn_pool_create(scratch_pool);
+ do
+ {
+ svn_pool_clear(iterpool);
+
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ SVN_ERR(svn_diff_parse_next_patch(&patch, patch_file,
+ reverse, ignore_whitespace,
+ iterpool, iterpool));
+ if (patch)
+ {
+ patch_target_t *target;
+
+ SVN_ERR(apply_one_patch(&target, patch, abs_wc_path,
+ ctx->wc_ctx, strip_count,
+ ignore_whitespace, remove_tempfiles,
+ patch_func, patch_baton,
+ ctx->cancel_func, ctx->cancel_baton,
+ iterpool, iterpool));
+ if (! target->filtered)
+ {
+ /* Save info we'll still need when we're done patching. */
+ patch_target_info_t *target_info =
+ apr_pcalloc(scratch_pool, sizeof(patch_target_info_t));
+ target_info->local_abspath = apr_pstrdup(scratch_pool,
+ target->local_abspath);
+ target_info->deleted = target->deleted;
+
+ if (! target->skipped)
+ {
+ APR_ARRAY_PUSH(targets_info,
+ patch_target_info_t *) = target_info;
+
+ if (target->has_text_changes
+ || target->added
+ || target->deleted)
+ SVN_ERR(install_patched_target(target, abs_wc_path,
+ ctx, dry_run, iterpool));
+
+ if (target->has_prop_changes && (!target->deleted))
+ SVN_ERR(install_patched_prop_targets(target, ctx,
+ dry_run, iterpool));
+
+ SVN_ERR(write_out_rejected_hunks(target, dry_run, iterpool));
+ }
+ SVN_ERR(send_patch_notification(target, ctx, iterpool));
+
+ if (target->deleted && !target->skipped)
+ {
+ SVN_ERR(check_ancestor_delete(target_info->local_abspath,
+ targets_info, abs_wc_path,
+ dry_run, ctx,
+ scratch_pool, iterpool));
+ }
+ }
+ }
+ }
+ while (patch);
+
+ SVN_ERR(svn_diff_close_patch_file(patch_file, iterpool));
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_patch(const char *patch_abspath,
+ const char *wc_dir_abspath,
+ svn_boolean_t dry_run,
+ int strip_count,
+ svn_boolean_t reverse,
+ svn_boolean_t ignore_whitespace,
+ svn_boolean_t remove_tempfiles,
+ svn_client_patch_func_t patch_func,
+ void *patch_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_node_kind_t kind;
+
+ if (strip_count < 0)
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("strip count must be positive"));
+
+ if (svn_path_is_url(wc_dir_abspath))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"),
+ svn_dirent_local_style(wc_dir_abspath,
+ scratch_pool));
+
+ SVN_ERR(svn_io_check_path(patch_abspath, &kind, scratch_pool));
+ if (kind == svn_node_none)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' does not exist"),
+ svn_dirent_local_style(patch_abspath,
+ scratch_pool));
+ if (kind != svn_node_file)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a file"),
+ svn_dirent_local_style(patch_abspath,
+ scratch_pool));
+
+ SVN_ERR(svn_io_check_path(wc_dir_abspath, &kind, scratch_pool));
+ if (kind == svn_node_none)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' does not exist"),
+ svn_dirent_local_style(wc_dir_abspath,
+ scratch_pool));
+ if (kind != svn_node_dir)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a directory"),
+ svn_dirent_local_style(wc_dir_abspath,
+ scratch_pool));
+
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ apply_patches(patch_abspath, wc_dir_abspath, dry_run, strip_count,
+ reverse, ignore_whitespace, remove_tempfiles,
+ patch_func, patch_baton, ctx, scratch_pool),
+ ctx->wc_ctx, wc_dir_abspath, FALSE /* lock_anchor */, scratch_pool);
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/prop_commands.c b/subversion/libsvn_client/prop_commands.c
new file mode 100644
index 0000000..c3c1cfa
--- /dev/null
+++ b/subversion/libsvn_client/prop_commands.c
@@ -0,0 +1,1559 @@
+/*
+ * prop_commands.c: Implementation of propset, propget, and proplist.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#define APR_WANT_STRFUNC
+#include <apr_want.h>
+
+#include "svn_error.h"
+#include "svn_client.h"
+#include "client.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_pools.h"
+#include "svn_props.h"
+#include "svn_hash.h"
+#include "svn_sorts.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_ra_private.h"
+#include "private/svn_client_private.h"
+
+
+/*** Code. ***/
+
+/* Return an SVN_ERR_CLIENT_PROPERTY_NAME error if NAME is a wcprop,
+ else return SVN_NO_ERROR. */
+static svn_error_t *
+error_if_wcprop_name(const char *name)
+{
+ if (svn_property_kind2(name) == svn_prop_wc_kind)
+ {
+ return svn_error_createf
+ (SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
+ _("'%s' is a wcprop, thus not accessible to clients"),
+ name);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+struct getter_baton
+{
+ svn_ra_session_t *ra_session;
+ svn_revnum_t base_revision_for_url;
+};
+
+
+static svn_error_t *
+get_file_for_validation(const svn_string_t **mime_type,
+ svn_stream_t *stream,
+ void *baton,
+ apr_pool_t *pool)
+{
+ struct getter_baton *gb = baton;
+ svn_ra_session_t *ra_session = gb->ra_session;
+ apr_hash_t *props;
+
+ SVN_ERR(svn_ra_get_file(ra_session, "", gb->base_revision_for_url,
+ stream, NULL,
+ (mime_type ? &props : NULL),
+ pool));
+
+ if (mime_type)
+ *mime_type = svn_hash_gets(props, SVN_PROP_MIME_TYPE);
+
+ return SVN_NO_ERROR;
+}
+
+
+static
+svn_error_t *
+do_url_propset(const char *url,
+ const char *propname,
+ const svn_string_t *propval,
+ const svn_node_kind_t kind,
+ const svn_revnum_t base_revision_for_url,
+ const svn_delta_editor_t *editor,
+ void *edit_baton,
+ apr_pool_t *pool)
+{
+ void *root_baton;
+
+ SVN_ERR(editor->open_root(edit_baton, base_revision_for_url, pool,
+ &root_baton));
+
+ if (kind == svn_node_file)
+ {
+ void *file_baton;
+ const char *uri_basename = svn_uri_basename(url, pool);
+
+ SVN_ERR(editor->open_file(uri_basename, root_baton,
+ base_revision_for_url, pool, &file_baton));
+ SVN_ERR(editor->change_file_prop(file_baton, propname, propval, pool));
+ SVN_ERR(editor->close_file(file_baton, NULL, pool));
+ }
+ else
+ {
+ SVN_ERR(editor->change_dir_prop(root_baton, propname, propval, pool));
+ }
+
+ return editor->close_directory(root_baton, pool);
+}
+
+static svn_error_t *
+propset_on_url(const char *propname,
+ const svn_string_t *propval,
+ const char *target,
+ svn_boolean_t skip_checks,
+ svn_revnum_t base_revision_for_url,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ enum svn_prop_kind prop_kind = svn_property_kind2(propname);
+ svn_ra_session_t *ra_session;
+ svn_node_kind_t node_kind;
+ const char *message;
+ const svn_delta_editor_t *editor;
+ void *edit_baton;
+ apr_hash_t *commit_revprops;
+ svn_error_t *err;
+
+ if (prop_kind != svn_prop_regular_kind)
+ return svn_error_createf
+ (SVN_ERR_BAD_PROP_KIND, NULL,
+ _("Property '%s' is not a regular property"), propname);
+
+ /* Open an RA session for the URL. Note that we don't have a local
+ directory, nor a place to put temp files. */
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, target, NULL,
+ ctx, pool, pool));
+
+ SVN_ERR(svn_ra_check_path(ra_session, "", base_revision_for_url,
+ &node_kind, pool));
+ if (node_kind == svn_node_none)
+ return svn_error_createf
+ (SVN_ERR_FS_NOT_FOUND, NULL,
+ _("Path '%s' does not exist in revision %ld"),
+ target, base_revision_for_url);
+
+ if (node_kind == svn_node_file)
+ {
+ /* We need to reparent our session one directory up, since editor
+ semantics require the root is a directory.
+
+ ### How does this interact with authz? */
+ const char *parent_url;
+ parent_url = svn_uri_dirname(target, pool);
+
+ SVN_ERR(svn_ra_reparent(ra_session, parent_url, pool));
+ }
+
+ /* Setting an inappropriate property is not allowed (unless
+ overridden by 'skip_checks', in some circumstances). Deleting an
+ inappropriate property is allowed, however, since older clients
+ allowed (and other clients possibly still allow) setting it in
+ the first place. */
+ if (propval && svn_prop_is_svn_prop(propname))
+ {
+ const svn_string_t *new_value;
+ struct getter_baton gb;
+
+ gb.ra_session = ra_session;
+ gb.base_revision_for_url = base_revision_for_url;
+ SVN_ERR(svn_wc_canonicalize_svn_prop(&new_value, propname, propval,
+ target, node_kind, skip_checks,
+ get_file_for_validation, &gb, pool));
+ propval = new_value;
+ }
+
+ /* Create a new commit item and add it to the array. */
+ if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx))
+ {
+ svn_client_commit_item3_t *item;
+ const char *tmp_file;
+ apr_array_header_t *commit_items = apr_array_make(pool, 1, sizeof(item));
+
+ item = svn_client_commit_item3_create(pool);
+ item->url = target;
+ item->state_flags = SVN_CLIENT_COMMIT_ITEM_PROP_MODS;
+ APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
+ SVN_ERR(svn_client__get_log_msg(&message, &tmp_file, commit_items,
+ ctx, pool));
+ if (! message)
+ return SVN_NO_ERROR;
+ }
+ else
+ message = "";
+
+ SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table,
+ message, ctx, pool));
+
+ /* Fetch RA commit editor. */
+ SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session,
+ svn_client__get_shim_callbacks(ctx->wc_ctx,
+ NULL, pool)));
+ SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton,
+ commit_revprops,
+ commit_callback,
+ commit_baton,
+ NULL, TRUE, /* No lock tokens */
+ pool));
+
+ err = do_url_propset(target, propname, propval, node_kind,
+ base_revision_for_url, editor, edit_baton, pool);
+
+ if (err)
+ {
+ /* At least try to abort the edit (and fs txn) before throwing err. */
+ svn_error_clear(editor->abort_edit(edit_baton, pool));
+ return svn_error_trace(err);
+ }
+
+ /* Close the edit. */
+ return editor->close_edit(edit_baton, pool);
+}
+
+/* Check that PROPNAME is a valid name for a versioned property. Return an
+ * error if it is not valid, specifically if it is:
+ * - the name of a standard Subversion rev-prop; or
+ * - in the namespace of WC-props; or
+ * - not a well-formed property name (except if PROPVAL is NULL: in other
+ * words we do allow deleting a prop with an ill-formed name).
+ *
+ * Since Subversion controls the "svn:" property namespace, we don't honor
+ * a 'skip_checks' flag here. Checks for unusual property combinations such
+ * as svn:eol-style with a non-text svn:mime-type might understandably be
+ * skipped, but things such as using a property name reserved for revprops
+ * on a local target are never allowed.
+ */
+static svn_error_t *
+check_prop_name(const char *propname,
+ const svn_string_t *propval)
+{
+ if (svn_prop_is_known_svn_rev_prop(propname))
+ return svn_error_createf(SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
+ _("Revision property '%s' not allowed "
+ "in this context"), propname);
+
+ SVN_ERR(error_if_wcprop_name(propname));
+
+ if (propval && ! svn_prop_name_is_valid(propname))
+ return svn_error_createf(SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
+ _("Bad property name: '%s'"), propname);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_propset_local(const char *propname,
+ const svn_string_t *propval,
+ const apr_array_header_t *targets,
+ svn_depth_t depth,
+ svn_boolean_t skip_checks,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ svn_boolean_t targets_are_urls;
+ int i;
+
+ if (targets->nelts == 0)
+ return SVN_NO_ERROR;
+
+ /* Check for homogeneity among our targets. */
+ targets_are_urls = svn_path_is_url(APR_ARRAY_IDX(targets, 0, const char *));
+ SVN_ERR(svn_client__assert_homogeneous_target_type(targets));
+
+ if (targets_are_urls)
+ return svn_error_create(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Targets must be working copy paths"));
+
+ SVN_ERR(check_prop_name(propname, propval));
+
+ for (i = 0; i < targets->nelts; i++)
+ {
+ svn_node_kind_t kind;
+ const char *target_abspath;
+ const char *target = APR_ARRAY_IDX(targets, i, const char *);
+
+ svn_pool_clear(iterpool);
+
+ /* Check for cancellation */
+ if (ctx->cancel_func)
+ SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
+
+ SVN_ERR(svn_dirent_get_absolute(&target_abspath, target, iterpool));
+
+ /* Call prop_set for deleted nodes to have special errors */
+ SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, target_abspath,
+ FALSE, FALSE, iterpool));
+
+ if (kind == svn_node_unknown || kind == svn_node_none)
+ {
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify = svn_wc_create_notify(
+ target_abspath,
+ svn_wc_notify_path_nonexistent,
+ iterpool);
+
+ ctx->notify_func2(ctx->notify_baton2, notify, iterpool);
+ }
+ }
+
+ SVN_WC__CALL_WITH_WRITE_LOCK(
+ svn_wc_prop_set4(ctx->wc_ctx, target_abspath, propname,
+ propval, depth, skip_checks, changelists,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2, iterpool),
+ ctx->wc_ctx, target_abspath, FALSE /* lock_anchor */, iterpool);
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_propset_remote(const char *propname,
+ const svn_string_t *propval,
+ const char *url,
+ svn_boolean_t skip_checks,
+ svn_revnum_t base_revision_for_url,
+ const apr_hash_t *revprop_table,
+ svn_commit_callback2_t commit_callback,
+ void *commit_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ if (!svn_path_is_url(url))
+ return svn_error_create(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Targets must be URLs"));
+
+ SVN_ERR(check_prop_name(propname, propval));
+
+ /* The rationale for requiring the base_revision_for_url
+ argument is that without it, it's too easy to possibly
+ overwrite someone else's change without noticing. (See also
+ tools/examples/svnput.c). */
+ if (! SVN_IS_VALID_REVNUM(base_revision_for_url))
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Setting property on non-local targets "
+ "needs a base revision"));
+
+ /* ### When you set svn:eol-style or svn:keywords on a wc file,
+ ### Subversion sends a textdelta at commit time to properly
+ ### normalize the file in the repository. If we want to
+ ### support editing these properties on URLs, then we should
+ ### generate the same textdelta; for now, we won't support
+ ### editing these properties on URLs. (Admittedly, this
+ ### means that all the machinery with get_file_for_validation
+ ### is unused.)
+ */
+ if ((strcmp(propname, SVN_PROP_EOL_STYLE) == 0) ||
+ (strcmp(propname, SVN_PROP_KEYWORDS) == 0))
+ return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Setting property '%s' on non-local "
+ "targets is not supported"), propname);
+
+ SVN_ERR(propset_on_url(propname, propval, url, skip_checks,
+ base_revision_for_url, revprop_table,
+ commit_callback, commit_baton, ctx, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+check_and_set_revprop(svn_revnum_t *set_rev,
+ svn_ra_session_t *ra_session,
+ const char *propname,
+ const svn_string_t *original_propval,
+ const svn_string_t *propval,
+ apr_pool_t *pool)
+{
+ if (original_propval)
+ {
+ /* Ensure old value hasn't changed behind our back. */
+ svn_string_t *current;
+ SVN_ERR(svn_ra_rev_prop(ra_session, *set_rev, propname, &current, pool));
+
+ if (original_propval->data && (! current))
+ {
+ return svn_error_createf(
+ SVN_ERR_RA_OUT_OF_DATE, NULL,
+ _("revprop '%s' in r%ld is unexpectedly absent "
+ "in repository (maybe someone else deleted it?)"),
+ propname, *set_rev);
+ }
+ else if (original_propval->data
+ && (! svn_string_compare(original_propval, current)))
+ {
+ return svn_error_createf(
+ SVN_ERR_RA_OUT_OF_DATE, NULL,
+ _("revprop '%s' in r%ld has unexpected value "
+ "in repository (maybe someone else changed it?)"),
+ propname, *set_rev);
+ }
+ else if ((! original_propval->data) && current)
+ {
+ return svn_error_createf(
+ SVN_ERR_RA_OUT_OF_DATE, NULL,
+ _("revprop '%s' in r%ld is unexpectedly present "
+ "in repository (maybe someone else set it?)"),
+ propname, *set_rev);
+ }
+ }
+
+ SVN_ERR(svn_ra_change_rev_prop2(ra_session, *set_rev, propname,
+ NULL, propval, pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_revprop_set2(const char *propname,
+ const svn_string_t *propval,
+ const svn_string_t *original_propval,
+ const char *URL,
+ const svn_opt_revision_t *revision,
+ svn_revnum_t *set_rev,
+ svn_boolean_t force,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *ra_session;
+ svn_boolean_t be_atomic;
+
+ if ((strcmp(propname, SVN_PROP_REVISION_AUTHOR) == 0)
+ && propval
+ && strchr(propval->data, '\n') != NULL
+ && (! force))
+ return svn_error_create(SVN_ERR_CLIENT_REVISION_AUTHOR_CONTAINS_NEWLINE,
+ NULL, _("Author name should not contain a newline;"
+ " value will not be set unless forced"));
+
+ if (propval && ! svn_prop_name_is_valid(propname))
+ return svn_error_createf(SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
+ _("Bad property name: '%s'"), propname);
+
+ /* Open an RA session for the URL. */
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, URL, NULL,
+ ctx, pool, pool));
+
+ /* Resolve the revision into something real, and return that to the
+ caller as well. */
+ SVN_ERR(svn_client__get_revision_number(set_rev, NULL, ctx->wc_ctx, NULL,
+ ra_session, revision, pool));
+
+ SVN_ERR(svn_ra_has_capability(ra_session, &be_atomic,
+ SVN_RA_CAPABILITY_ATOMIC_REVPROPS, pool));
+ if (be_atomic)
+ {
+ /* Convert ORIGINAL_PROPVAL to an OLD_VALUE_P. */
+ const svn_string_t *const *old_value_p;
+ const svn_string_t *unset = NULL;
+
+ if (original_propval == NULL)
+ old_value_p = NULL;
+ else if (original_propval->data == NULL)
+ old_value_p = &unset;
+ else
+ old_value_p = &original_propval;
+
+ /* The actual RA call. */
+ SVN_ERR(svn_ra_change_rev_prop2(ra_session, *set_rev, propname,
+ old_value_p, propval, pool));
+ }
+ else
+ {
+ /* The actual RA call. */
+ SVN_ERR(check_and_set_revprop(set_rev, ra_session, propname,
+ original_propval, propval, pool));
+ }
+
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify = svn_wc_create_notify_url(URL,
+ propval == NULL
+ ? svn_wc_notify_revprop_deleted
+ : svn_wc_notify_revprop_set,
+ pool);
+ notify->prop_name = propname;
+ notify->revision = *set_rev;
+
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Helper for the remote case of svn_client_propget.
+ *
+ * If PROPS is not null, then get the value of property PROPNAME in REVNUM,
+ using RA_LIB and SESSION. Store the value ('svn_string_t *') in PROPS,
+ under the path key "TARGET_PREFIX/TARGET_RELATIVE" ('const char *').
+ *
+ * If INHERITED_PROPS is not null, then set *INHERITED_PROPS to a
+ * depth-first ordered array of svn_prop_inherited_item_t * structures
+ * representing the PROPNAME properties inherited by the target. If
+ * INHERITABLE_PROPS in not null and no inheritable properties are found,
+ * then set *INHERITED_PROPS to an empty array.
+ *
+ * Recurse according to DEPTH, similarly to svn_client_propget3().
+ *
+ * KIND is the kind of the node at "TARGET_PREFIX/TARGET_RELATIVE".
+ * Yes, caller passes this; it makes the recursion more efficient :-).
+ *
+ * Allocate PROPS and *INHERITED_PROPS in RESULT_POOL, but do all temporary
+ * work in SCRATCH_POOL. The two pools can be the same; recursive
+ * calls may use a different SCRATCH_POOL, however.
+ */
+static svn_error_t *
+remote_propget(apr_hash_t *props,
+ apr_array_header_t **inherited_props,
+ const char *propname,
+ const char *target_prefix,
+ const char *target_relative,
+ svn_node_kind_t kind,
+ svn_revnum_t revnum,
+ svn_ra_session_t *ra_session,
+ svn_depth_t depth,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *dirents;
+ apr_hash_t *prop_hash = NULL;
+ const svn_string_t *val;
+ const char *target_full_url =
+ svn_path_url_add_component2(target_prefix, target_relative,
+ scratch_pool);
+
+ if (kind == svn_node_dir)
+ {
+ SVN_ERR(svn_ra_get_dir2(ra_session,
+ (depth >= svn_depth_files ? &dirents : NULL),
+ NULL, &prop_hash, target_relative, revnum,
+ SVN_DIRENT_KIND, scratch_pool));
+ }
+ else if (kind == svn_node_file)
+ {
+ SVN_ERR(svn_ra_get_file(ra_session, target_relative, revnum,
+ NULL, NULL, &prop_hash, scratch_pool));
+ }
+ else if (kind == svn_node_none)
+ {
+ return svn_error_createf(SVN_ERR_ENTRY_NOT_FOUND, NULL,
+ _("'%s' does not exist in revision %ld"),
+ target_full_url, revnum);
+ }
+ else
+ {
+ return svn_error_createf(SVN_ERR_NODE_UNKNOWN_KIND, NULL,
+ _("Unknown node kind for '%s'"),
+ target_full_url);
+ }
+
+ if (inherited_props)
+ {
+ const char *repos_root_url;
+
+ /* We will filter out all but PROPNAME later, making a final copy
+ in RESULT_POOL, so pass SCRATCH_POOL for all pools. */
+ SVN_ERR(svn_ra_get_inherited_props(ra_session, inherited_props,
+ target_relative, revnum,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root_url,
+ scratch_pool));
+ SVN_ERR(svn_client__iprop_relpaths_to_urls(*inherited_props,
+ repos_root_url,
+ scratch_pool,
+ scratch_pool));
+ }
+
+ /* Make a copy of any inherited PROPNAME properties in RESULT_POOL. */
+ if (inherited_props)
+ {
+ int i;
+ apr_array_header_t *final_iprops =
+ apr_array_make(result_pool, 1, sizeof(svn_prop_inherited_item_t *));
+
+ for (i = 0; i < (*inherited_props)->nelts; i++)
+ {
+ svn_prop_inherited_item_t *iprop =
+ APR_ARRAY_IDX((*inherited_props), i, svn_prop_inherited_item_t *);
+ svn_string_t *iprop_val = svn_hash_gets(iprop->prop_hash, propname);
+
+ if (iprop_val)
+ {
+ svn_prop_inherited_item_t *new_iprop =
+ apr_palloc(result_pool, sizeof(*new_iprop));
+ new_iprop->path_or_url =
+ apr_pstrdup(result_pool, iprop->path_or_url);
+ new_iprop->prop_hash = apr_hash_make(result_pool);
+ svn_hash_sets(new_iprop->prop_hash,
+ apr_pstrdup(result_pool, propname),
+ svn_string_dup(iprop_val, result_pool));
+ APR_ARRAY_PUSH(final_iprops, svn_prop_inherited_item_t *) =
+ new_iprop;
+ }
+ }
+ *inherited_props = final_iprops;
+ }
+
+ if (prop_hash
+ && (val = svn_hash_gets(prop_hash, propname)))
+ {
+ svn_hash_sets(props,
+ apr_pstrdup(result_pool, target_full_url),
+ svn_string_dup(val, result_pool));
+ }
+
+ if (depth >= svn_depth_files
+ && kind == svn_node_dir
+ && apr_hash_count(dirents) > 0)
+ {
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ for (hi = apr_hash_first(scratch_pool, dirents);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *this_name = svn__apr_hash_index_key(hi);
+ svn_dirent_t *this_ent = svn__apr_hash_index_val(hi);
+ const char *new_target_relative;
+ svn_depth_t depth_below_here = depth;
+
+ svn_pool_clear(iterpool);
+
+ if (depth == svn_depth_files && this_ent->kind == svn_node_dir)
+ continue;
+
+ if (depth == svn_depth_files || depth == svn_depth_immediates)
+ depth_below_here = svn_depth_empty;
+
+ new_target_relative = svn_relpath_join(target_relative, this_name,
+ iterpool);
+
+ SVN_ERR(remote_propget(props, NULL,
+ propname,
+ target_prefix,
+ new_target_relative,
+ this_ent->kind,
+ revnum,
+ ra_session,
+ depth_below_here,
+ result_pool, iterpool));
+ }
+
+ svn_pool_destroy(iterpool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Baton for recursive_propget_receiver(). */
+struct recursive_propget_receiver_baton
+{
+ apr_hash_t *props; /* Hash to collect props. */
+ apr_pool_t *pool; /* Pool to allocate additions to PROPS. */
+ svn_wc_context_t *wc_ctx; /* Working copy context. */
+};
+
+/* An implementation of svn_wc__proplist_receiver_t. */
+static svn_error_t *
+recursive_propget_receiver(void *baton,
+ const char *local_abspath,
+ apr_hash_t *props,
+ apr_pool_t *scratch_pool)
+{
+ struct recursive_propget_receiver_baton *b = baton;
+
+ if (apr_hash_count(props))
+ {
+ apr_hash_index_t *hi = apr_hash_first(scratch_pool, props);
+ svn_hash_sets(b->props, apr_pstrdup(b->pool, local_abspath),
+ svn_string_dup(svn__apr_hash_index_val(hi), b->pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Return the property value for any PROPNAME set on TARGET in *PROPS,
+ with WC paths of char * for keys and property values of
+ svn_string_t * for values. Assumes that PROPS is non-NULL. Additions
+ to *PROPS are allocated in RESULT_POOL, temporary allocations happen in
+ SCRATCH_POOL.
+
+ CHANGELISTS is an array of const char * changelist names, used as a
+ restrictive filter on items whose properties are set; that is,
+ don't set properties on any item unless it's a member of one of
+ those changelists. If CHANGELISTS is empty (or altogether NULL),
+ no changelist filtering occurs.
+
+ Treat DEPTH as in svn_client_propget3().
+*/
+static svn_error_t *
+get_prop_from_wc(apr_hash_t **props,
+ const char *propname,
+ const char *target_abspath,
+ svn_boolean_t pristine,
+ svn_node_kind_t kind,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct recursive_propget_receiver_baton rb;
+
+ /* Technically, svn_depth_unknown just means use whatever depth(s)
+ we find in the working copy. But this is a walk over extant
+ working copy paths: if they're there at all, then by definition
+ the local depth reaches them, so let's just use svn_depth_infinity
+ to get there. */
+ if (depth == svn_depth_unknown)
+ depth = svn_depth_infinity;
+
+ if (!pristine && depth == svn_depth_infinity
+ && (!changelists || changelists->nelts == 0))
+ {
+ /* Handle this common svn:mergeinfo case more efficient than the target
+ list handling in the recursive retrieval. */
+ SVN_ERR(svn_wc__prop_retrieve_recursive(
+ props, ctx->wc_ctx, target_abspath, propname,
+ result_pool, scratch_pool));
+ return SVN_NO_ERROR;
+ }
+
+ *props = apr_hash_make(result_pool);
+ rb.props = *props;
+ rb.pool = result_pool;
+ rb.wc_ctx = ctx->wc_ctx;
+
+ SVN_ERR(svn_wc__prop_list_recursive(ctx->wc_ctx, target_abspath,
+ propname, depth, pristine,
+ changelists,
+ recursive_propget_receiver, &rb,
+ ctx->cancel_func, ctx->cancel_baton,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Note: this implementation is very similar to svn_client_proplist. */
+svn_error_t *
+svn_client_propget5(apr_hash_t **props,
+ apr_array_header_t **inherited_props,
+ const char *propname,
+ const char *target,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_revnum_t *actual_revnum,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_revnum_t revnum;
+ svn_boolean_t local_explicit_props;
+ svn_boolean_t local_iprops;
+
+ SVN_ERR(error_if_wcprop_name(propname));
+ if (!svn_path_is_url(target))
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(target));
+
+ peg_revision = svn_cl__rev_default_to_head_or_working(peg_revision,
+ target);
+ revision = svn_cl__rev_default_to_peg(revision, peg_revision);
+
+ local_explicit_props =
+ (! svn_path_is_url(target)
+ && SVN_CLIENT__REVKIND_IS_LOCAL_TO_WC(peg_revision->kind)
+ && SVN_CLIENT__REVKIND_IS_LOCAL_TO_WC(revision->kind));
+
+ local_iprops =
+ (local_explicit_props
+ && (peg_revision->kind == svn_opt_revision_working
+ || peg_revision->kind == svn_opt_revision_unspecified )
+ && (revision->kind == svn_opt_revision_working
+ || revision->kind == svn_opt_revision_unspecified ));
+
+ if (local_explicit_props)
+ {
+ svn_node_kind_t kind;
+ svn_boolean_t pristine;
+ svn_error_t *err;
+
+ /* If FALSE, we want the working revision. */
+ pristine = (revision->kind == svn_opt_revision_committed
+ || revision->kind == svn_opt_revision_base);
+
+ SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, target,
+ pristine, FALSE,
+ scratch_pool));
+
+ if (kind == svn_node_unknown || kind == svn_node_none)
+ {
+ /* svn uses SVN_ERR_UNVERSIONED_RESOURCE as warning only
+ for this function. */
+ return svn_error_createf(SVN_ERR_UNVERSIONED_RESOURCE, NULL,
+ _("'%s' is not under version control"),
+ svn_dirent_local_style(target,
+ scratch_pool));
+ }
+
+ err = svn_client__get_revision_number(&revnum, NULL, ctx->wc_ctx,
+ target, NULL, revision,
+ scratch_pool);
+ if (err && err->apr_err == SVN_ERR_CLIENT_BAD_REVISION)
+ {
+ svn_error_clear(err);
+ revnum = SVN_INVALID_REVNUM;
+ }
+ else if (err)
+ return svn_error_trace(err);
+
+ if (inherited_props && local_iprops)
+ {
+ const char *repos_root_url;
+
+ SVN_ERR(svn_wc__get_iprops(inherited_props, ctx->wc_ctx,
+ target, propname,
+ result_pool, scratch_pool));
+ SVN_ERR(svn_client_get_repos_root(&repos_root_url, NULL,
+ target, ctx, scratch_pool,
+ scratch_pool));
+ SVN_ERR(svn_client__iprop_relpaths_to_urls(*inherited_props,
+ repos_root_url,
+ result_pool,
+ scratch_pool));
+ }
+
+ SVN_ERR(get_prop_from_wc(props, propname, target,
+ pristine, kind,
+ depth, changelists, ctx, result_pool,
+ scratch_pool));
+ }
+
+ if ((inherited_props && !local_iprops)
+ || !local_explicit_props)
+ {
+ svn_ra_session_t *ra_session;
+ svn_node_kind_t kind;
+ svn_opt_revision_t new_operative_rev;
+ svn_opt_revision_t new_peg_rev;
+
+ /* Peg or operative revisions may be WC specific for
+ TARGET's explicit props, but still require us to
+ contact the repository for the inherited properties. */
+ if (SVN_CLIENT__REVKIND_NEEDS_WC(peg_revision->kind)
+ || SVN_CLIENT__REVKIND_NEEDS_WC(revision->kind))
+ {
+ svn_revnum_t origin_rev;
+ const char *repos_relpath;
+ const char *repos_root_url;
+ const char *repos_uuid;
+ const char *local_abspath;
+ const char *copy_root_abspath;
+ svn_boolean_t is_copy;
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, target,
+ scratch_pool));
+
+ if (SVN_CLIENT__REVKIND_NEEDS_WC(peg_revision->kind))
+ {
+ SVN_ERR(svn_wc__node_get_origin(&is_copy,
+ &origin_rev,
+ &repos_relpath,
+ &repos_root_url,
+ &repos_uuid,
+ &copy_root_abspath,
+ ctx->wc_ctx,
+ local_abspath,
+ FALSE, /* scan_deleted */
+ result_pool,
+ scratch_pool));
+ if (repos_relpath)
+ {
+ target = svn_path_url_add_component2(repos_root_url,
+ repos_relpath,
+ scratch_pool);
+ if (SVN_CLIENT__REVKIND_NEEDS_WC(peg_revision->kind))
+ {
+ svn_revnum_t resolved_peg_rev;
+
+ SVN_ERR(svn_client__get_revision_number(
+ &resolved_peg_rev, NULL, ctx->wc_ctx,
+ local_abspath, NULL, peg_revision, scratch_pool));
+ new_peg_rev.kind = svn_opt_revision_number;
+ new_peg_rev.value.number = resolved_peg_rev;
+ peg_revision = &new_peg_rev;
+ }
+
+ if (SVN_CLIENT__REVKIND_NEEDS_WC(revision->kind))
+ {
+ svn_revnum_t resolved_operative_rev;
+
+ SVN_ERR(svn_client__get_revision_number(
+ &resolved_operative_rev, NULL, ctx->wc_ctx,
+ local_abspath, NULL, revision, scratch_pool));
+ new_operative_rev.kind = svn_opt_revision_number;
+ new_operative_rev.value.number = resolved_operative_rev;
+ revision = &new_operative_rev;
+ }
+ }
+ else
+ {
+ /* TARGET doesn't exist in the repository, so there are
+ obviously not inherited props to be found there. */
+ local_iprops = TRUE;
+ *inherited_props = apr_array_make(
+ result_pool, 0, sizeof(svn_prop_inherited_item_t *));
+ }
+ }
+ }
+
+ /* Do we still have anything to ask the repository about? */
+ if (!local_explicit_props || !local_iprops)
+ {
+ svn_client__pathrev_t *loc;
+
+ /* Get an RA plugin for this filesystem object. */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &loc,
+ target, NULL,
+ peg_revision,
+ revision, ctx,
+ scratch_pool));
+
+ SVN_ERR(svn_ra_check_path(ra_session, "", loc->rev, &kind,
+ scratch_pool));
+
+ if (!local_explicit_props)
+ *props = apr_hash_make(result_pool);
+
+ SVN_ERR(remote_propget(!local_explicit_props ? *props : NULL,
+ !local_iprops ? inherited_props : NULL,
+ propname, loc->url, "",
+ kind, loc->rev, ra_session,
+ depth, result_pool, scratch_pool));
+ revnum = loc->rev;
+ }
+ }
+
+ if (actual_revnum)
+ *actual_revnum = revnum;
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_revprop_get(const char *propname,
+ svn_string_t **propval,
+ const char *URL,
+ const svn_opt_revision_t *revision,
+ svn_revnum_t *set_rev,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *ra_session;
+ apr_pool_t *subpool = svn_pool_create(pool);
+ svn_error_t *err;
+
+ /* Open an RA session for the URL. Note that we don't have a local
+ directory, nor a place to put temp files. */
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, URL, NULL,
+ ctx, subpool, subpool));
+
+ /* Resolve the revision into something real, and return that to the
+ caller as well. */
+ SVN_ERR(svn_client__get_revision_number(set_rev, NULL, ctx->wc_ctx, NULL,
+ ra_session, revision, subpool));
+
+ /* The actual RA call. */
+ err = svn_ra_rev_prop(ra_session, *set_rev, propname, propval, pool);
+
+ /* Close RA session */
+ svn_pool_destroy(subpool);
+ return svn_error_trace(err);
+}
+
+
+/* Call RECEIVER for the given PATH and its PROP_HASH and/or
+ * INHERITED_PROPERTIES.
+ *
+ * If PROP_HASH is null or has zero count or INHERITED_PROPERTIES is null,
+ * then do nothing.
+ */
+static svn_error_t*
+call_receiver(const char *path,
+ apr_hash_t *prop_hash,
+ apr_array_header_t *inherited_properties,
+ svn_proplist_receiver2_t receiver,
+ void *receiver_baton,
+ apr_pool_t *scratch_pool)
+{
+ if ((prop_hash && apr_hash_count(prop_hash))
+ || inherited_properties)
+ SVN_ERR(receiver(receiver_baton, path, prop_hash, inherited_properties,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Helper for the remote case of svn_client_proplist.
+ *
+ * If GET_EXPLICIT_PROPS is true, then call RECEIVER for paths at or under
+ * "TARGET_PREFIX/TARGET_RELATIVE@REVNUM" (obtained using RA_SESSION) which
+ * have regular properties. If GET_TARGET_INHERITED_PROPS is true, then send
+ * the target's inherited properties to the callback.
+ *
+ * The 'path' and keys for 'prop_hash' and 'inherited_prop' arguments to
+ * RECEIVER are all URLs.
+ *
+ * RESULT_POOL is used to allocated the 'path', 'prop_hash', and
+ * 'inherited_prop' arguments to RECEIVER. SCRATCH_POOL is used for all
+ * other (temporary) allocations.
+ *
+ * KIND is the kind of the node at "TARGET_PREFIX/TARGET_RELATIVE".
+ *
+ * If the target is a directory, only fetch properties for the files
+ * and directories at depth DEPTH. DEPTH has not effect on inherited
+ * properties.
+ */
+static svn_error_t *
+remote_proplist(const char *target_prefix,
+ const char *target_relative,
+ svn_node_kind_t kind,
+ svn_revnum_t revnum,
+ svn_ra_session_t *ra_session,
+ svn_boolean_t get_explicit_props,
+ svn_boolean_t get_target_inherited_props,
+ svn_depth_t depth,
+ svn_proplist_receiver2_t receiver,
+ void *receiver_baton,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *dirents;
+ apr_hash_t *prop_hash = NULL;
+ apr_hash_index_t *hi;
+ const char *target_full_url =
+ svn_path_url_add_component2(target_prefix, target_relative, scratch_pool);
+ apr_array_header_t *inherited_props;
+
+ /* Note that we pass only the SCRATCH_POOL to svn_ra_get[dir*|file*] because
+ we'll be filtering out non-regular properties from PROP_HASH before we
+ return. */
+ if (kind == svn_node_dir)
+ {
+ SVN_ERR(svn_ra_get_dir2(ra_session,
+ (depth > svn_depth_empty) ? &dirents : NULL,
+ NULL, &prop_hash, target_relative, revnum,
+ SVN_DIRENT_KIND, scratch_pool));
+ }
+ else if (kind == svn_node_file)
+ {
+ SVN_ERR(svn_ra_get_file(ra_session, target_relative, revnum,
+ NULL, NULL, &prop_hash, scratch_pool));
+ }
+ else
+ {
+ return svn_error_createf(SVN_ERR_NODE_UNKNOWN_KIND, NULL,
+ _("Unknown node kind for '%s'"),
+ target_full_url);
+ }
+
+ if (get_target_inherited_props)
+ {
+ const char *repos_root_url;
+
+ SVN_ERR(svn_ra_get_inherited_props(ra_session, &inherited_props,
+ target_relative, revnum,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root_url,
+ scratch_pool));
+ SVN_ERR(svn_client__iprop_relpaths_to_urls(inherited_props,
+ repos_root_url,
+ scratch_pool,
+ scratch_pool));
+ }
+ else
+ {
+ inherited_props = NULL;
+ }
+
+ if (!get_explicit_props)
+ prop_hash = NULL;
+ else
+ {
+ /* Filter out non-regular properties, since the RA layer returns all
+ kinds. Copy regular properties keys/vals from the prop_hash
+ allocated in SCRATCH_POOL to the "final" hash allocated in
+ RESULT_POOL. */
+ for (hi = apr_hash_first(scratch_pool, prop_hash);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *name = svn__apr_hash_index_key(hi);
+ apr_ssize_t klen = svn__apr_hash_index_klen(hi);
+ svn_prop_kind_t prop_kind;
+
+ prop_kind = svn_property_kind2(name);
+
+ if (prop_kind != svn_prop_regular_kind)
+ {
+ apr_hash_set(prop_hash, name, klen, NULL);
+ }
+ }
+ }
+
+ SVN_ERR(call_receiver(target_full_url, prop_hash, inherited_props,
+ receiver, receiver_baton, scratch_pool));
+
+ if (depth > svn_depth_empty
+ && get_explicit_props
+ && (kind == svn_node_dir) && (apr_hash_count(dirents) > 0))
+ {
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ for (hi = apr_hash_first(scratch_pool, dirents);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *this_name = svn__apr_hash_index_key(hi);
+ svn_dirent_t *this_ent = svn__apr_hash_index_val(hi);
+ const char *new_target_relative;
+
+ if (cancel_func)
+ SVN_ERR(cancel_func(cancel_baton));
+
+ svn_pool_clear(iterpool);
+
+ new_target_relative = svn_relpath_join(target_relative,
+ this_name, iterpool);
+
+ if (this_ent->kind == svn_node_file
+ || depth > svn_depth_files)
+ {
+ svn_depth_t depth_below_here = depth;
+
+ if (depth == svn_depth_immediates)
+ depth_below_here = svn_depth_empty;
+
+ SVN_ERR(remote_proplist(target_prefix,
+ new_target_relative,
+ this_ent->kind,
+ revnum,
+ ra_session,
+ TRUE /* get_explicit_props */,
+ FALSE /* get_target_inherited_props */,
+ depth_below_here,
+ receiver, receiver_baton,
+ cancel_func, cancel_baton,
+ iterpool));
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Baton for recursive_proplist_receiver(). */
+struct recursive_proplist_receiver_baton
+{
+ svn_wc_context_t *wc_ctx; /* Working copy context. */
+ svn_proplist_receiver2_t wrapped_receiver; /* Proplist receiver to call. */
+ void *wrapped_receiver_baton; /* Baton for the proplist receiver. */
+
+ /* Anchor, anchor_abspath pair for converting to relative paths */
+ const char *anchor;
+ const char *anchor_abspath;
+};
+
+/* An implementation of svn_wc__proplist_receiver_t. */
+static svn_error_t *
+recursive_proplist_receiver(void *baton,
+ const char *local_abspath,
+ apr_hash_t *props,
+ apr_pool_t *scratch_pool)
+{
+ struct recursive_proplist_receiver_baton *b = baton;
+ const char *path;
+
+ /* Attempt to convert absolute paths to relative paths for
+ * presentation purposes, if needed. */
+ if (b->anchor && b->anchor_abspath)
+ {
+ path = svn_dirent_join(b->anchor,
+ svn_dirent_skip_ancestor(b->anchor_abspath,
+ local_abspath),
+ scratch_pool);
+ }
+ else
+ path = local_abspath;
+
+ return svn_error_trace(b->wrapped_receiver(b->wrapped_receiver_baton,
+ path, props, NULL,
+ scratch_pool));
+}
+
+/* Helper for svn_client_proplist4 when retrieving properties and/or
+ inherited properties from the repository. Except as noted below,
+ all arguments are as per svn_client_proplist4.
+
+ GET_EXPLICIT_PROPS controls if explicit props are retrieved. */
+static svn_error_t *
+get_remote_props(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t get_explicit_props,
+ svn_boolean_t get_target_inherited_props,
+ svn_proplist_receiver2_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_ra_session_t *ra_session;
+ svn_node_kind_t kind;
+ svn_opt_revision_t new_operative_rev;
+ svn_opt_revision_t new_peg_rev;
+ svn_client__pathrev_t *loc;
+
+ /* Peg or operative revisions may be WC specific for
+ PATH_OR_URL's explicit props, but still require us to
+ contact the repository for the inherited properties. */
+ if (SVN_CLIENT__REVKIND_NEEDS_WC(peg_revision->kind)
+ || SVN_CLIENT__REVKIND_NEEDS_WC(revision->kind))
+ {
+ svn_revnum_t origin_rev;
+ const char *repos_relpath;
+ const char *repos_root_url;
+ const char *repos_uuid;
+ const char *local_abspath;
+ const char *copy_root_abspath;
+ svn_boolean_t is_copy;
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path_or_url,
+ scratch_pool));
+
+ if (SVN_CLIENT__REVKIND_NEEDS_WC(peg_revision->kind))
+ {
+ SVN_ERR(svn_wc__node_get_origin(&is_copy,
+ &origin_rev,
+ &repos_relpath,
+ &repos_root_url,
+ &repos_uuid,
+ &copy_root_abspath,
+ ctx->wc_ctx,
+ local_abspath,
+ FALSE, /* scan_deleted */
+ scratch_pool,
+ scratch_pool));
+ if (repos_relpath)
+ {
+ path_or_url =
+ svn_path_url_add_component2(repos_root_url,
+ repos_relpath,
+ scratch_pool);
+ if (SVN_CLIENT__REVKIND_NEEDS_WC(peg_revision->kind))
+ {
+ svn_revnum_t resolved_peg_rev;
+
+ SVN_ERR(svn_client__get_revision_number(&resolved_peg_rev,
+ NULL, ctx->wc_ctx,
+ local_abspath, NULL,
+ peg_revision,
+ scratch_pool));
+ new_peg_rev.kind = svn_opt_revision_number;
+ new_peg_rev.value.number = resolved_peg_rev;
+ peg_revision = &new_peg_rev;
+ }
+
+ if (SVN_CLIENT__REVKIND_NEEDS_WC(revision->kind))
+ {
+ svn_revnum_t resolved_operative_rev;
+
+ SVN_ERR(svn_client__get_revision_number(
+ &resolved_operative_rev,
+ NULL, ctx->wc_ctx,
+ local_abspath, NULL,
+ revision,
+ scratch_pool));
+ new_operative_rev.kind = svn_opt_revision_number;
+ new_operative_rev.value.number = resolved_operative_rev;
+ revision = &new_operative_rev;
+ }
+ }
+ else
+ {
+ /* PATH_OR_URL doesn't exist in the repository, so there are
+ obviously not inherited props to be found there. If we
+ aren't looking for explicit props then we're done. */
+ if (!get_explicit_props)
+ return SVN_NO_ERROR;
+ }
+ }
+ }
+
+ /* Get an RA session for this URL. */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &loc,
+ path_or_url, NULL,
+ peg_revision,
+ revision, ctx,
+ scratch_pool));
+
+ SVN_ERR(svn_ra_check_path(ra_session, "", loc->rev, &kind,
+ scratch_pool));
+
+ SVN_ERR(remote_proplist(loc->url, "", kind, loc->rev, ra_session,
+ get_explicit_props,
+ get_target_inherited_props,
+ depth, receiver, receiver_baton,
+ ctx->cancel_func, ctx->cancel_baton,
+ scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+/* Helper for svn_client_proplist4 when retrieving properties and
+ possibly inherited properties from the WC. All arguments are as
+ per svn_client_proplist4. */
+static svn_error_t *
+get_local_props(const char *path_or_url,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_boolean_t get_target_inherited_props,
+ svn_proplist_receiver2_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_boolean_t pristine;
+ svn_node_kind_t kind;
+ apr_hash_t *changelist_hash = NULL;
+ const char *local_abspath;
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path_or_url,
+ scratch_pool));
+
+ pristine = ((revision->kind == svn_opt_revision_committed)
+ || (revision->kind == svn_opt_revision_base));
+
+ SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, local_abspath,
+ pristine, FALSE, scratch_pool));
+
+ if (kind == svn_node_unknown || kind == svn_node_none)
+ {
+ /* svn uses SVN_ERR_UNVERSIONED_RESOURCE as warning only
+ for this function. */
+ return svn_error_createf(SVN_ERR_UNVERSIONED_RESOURCE, NULL,
+ _("'%s' is not under version control"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+ }
+
+ if (get_target_inherited_props)
+ {
+ apr_array_header_t *iprops;
+ const char *repos_root_url;
+
+ SVN_ERR(svn_wc__get_iprops(&iprops, ctx->wc_ctx, local_abspath,
+ NULL, scratch_pool, scratch_pool));
+ SVN_ERR(svn_client_get_repos_root(&repos_root_url, NULL, local_abspath,
+ ctx, scratch_pool, scratch_pool));
+ SVN_ERR(svn_client__iprop_relpaths_to_urls(iprops, repos_root_url,
+ scratch_pool,
+ scratch_pool));
+ SVN_ERR(call_receiver(path_or_url, NULL, iprops, receiver,
+ receiver_baton, scratch_pool));
+ }
+
+ if (changelists && changelists->nelts)
+ SVN_ERR(svn_hash_from_cstring_keys(&changelist_hash,
+ changelists, scratch_pool));
+
+ /* Fetch, recursively or not. */
+ if (kind == svn_node_dir)
+ {
+ struct recursive_proplist_receiver_baton rb;
+
+ rb.wc_ctx = ctx->wc_ctx;
+ rb.wrapped_receiver = receiver;
+ rb.wrapped_receiver_baton = receiver_baton;
+
+ if (strcmp(path_or_url, local_abspath) != 0)
+ {
+ rb.anchor = path_or_url;
+ rb.anchor_abspath = local_abspath;
+ }
+ else
+ {
+ rb.anchor = NULL;
+ rb.anchor_abspath = NULL;
+ }
+
+ SVN_ERR(svn_wc__prop_list_recursive(ctx->wc_ctx, local_abspath, NULL,
+ depth, pristine, changelists,
+ recursive_proplist_receiver, &rb,
+ ctx->cancel_func, ctx->cancel_baton,
+ scratch_pool));
+ }
+ else if (svn_wc__changelist_match(ctx->wc_ctx, local_abspath,
+ changelist_hash, scratch_pool))
+ {
+ apr_hash_t *props;
+
+ if (pristine)
+ SVN_ERR(svn_wc_get_pristine_props(&props,
+ ctx->wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+ else
+ {
+ svn_error_t *err;
+
+ err = svn_wc_prop_list2(&props, ctx->wc_ctx, local_abspath,
+ scratch_pool, scratch_pool);
+
+
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_PATH_UNEXPECTED_STATUS)
+ return svn_error_trace(err);
+ /* As svn_wc_prop_list2() doesn't return NULL for locally-deleted
+ let's do that here. */
+ svn_error_clear(err);
+ props = apr_hash_make(scratch_pool);
+ }
+ }
+
+ SVN_ERR(call_receiver(path_or_url, props, NULL,
+ receiver, receiver_baton, scratch_pool));
+
+ }
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_proplist4(const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_boolean_t get_target_inherited_props,
+ svn_proplist_receiver2_t receiver,
+ void *receiver_baton,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_boolean_t local_explicit_props;
+ svn_boolean_t local_iprops;
+
+ peg_revision = svn_cl__rev_default_to_head_or_working(peg_revision,
+ path_or_url);
+ revision = svn_cl__rev_default_to_peg(revision, peg_revision);
+
+ if (depth == svn_depth_unknown)
+ depth = svn_depth_empty;
+
+ /* Are explicit props available locally? */
+ local_explicit_props =
+ (! svn_path_is_url(path_or_url)
+ && SVN_CLIENT__REVKIND_IS_LOCAL_TO_WC(peg_revision->kind)
+ && SVN_CLIENT__REVKIND_IS_LOCAL_TO_WC(revision->kind));
+
+ /* If we want iprops are they available locally? */
+ local_iprops =
+ (get_target_inherited_props /* We want iprops */
+ && local_explicit_props /* No local explicit props means no local iprops. */
+ && (peg_revision->kind == svn_opt_revision_working
+ || peg_revision->kind == svn_opt_revision_unspecified )
+ && (revision->kind == svn_opt_revision_working
+ || revision->kind == svn_opt_revision_unspecified ));
+
+ if ((get_target_inherited_props && !local_iprops)
+ || !local_explicit_props)
+ {
+ SVN_ERR(get_remote_props(path_or_url, peg_revision, revision, depth,
+ !local_explicit_props,
+ (get_target_inherited_props && !local_iprops),
+ receiver, receiver_baton, ctx, scratch_pool));
+ }
+
+ if (local_explicit_props)
+ {
+ SVN_ERR(get_local_props(path_or_url, revision, depth, changelists,
+ local_iprops, receiver, receiver_baton, ctx,
+ scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_revprop_list(apr_hash_t **props,
+ const char *URL,
+ const svn_opt_revision_t *revision,
+ svn_revnum_t *set_rev,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *ra_session;
+ apr_hash_t *proplist;
+ apr_pool_t *subpool = svn_pool_create(pool);
+ svn_error_t *err;
+
+ /* Open an RA session for the URL. Note that we don't have a local
+ directory, nor a place to put temp files. */
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, URL, NULL,
+ ctx, subpool, subpool));
+
+ /* Resolve the revision into something real, and return that to the
+ caller as well. */
+ SVN_ERR(svn_client__get_revision_number(set_rev, NULL, ctx->wc_ctx, NULL,
+ ra_session, revision, subpool));
+
+ /* The actual RA call. */
+ err = svn_ra_rev_proplist(ra_session, *set_rev, &proplist, pool);
+
+ *props = proplist;
+ svn_pool_destroy(subpool); /* Close RA session */
+ return svn_error_trace(err);
+}
diff --git a/subversion/libsvn_client/ra.c b/subversion/libsvn_client/ra.c
new file mode 100644
index 0000000..33d3de5
--- /dev/null
+++ b/subversion/libsvn_client/ra.c
@@ -0,0 +1,1147 @@
+/*
+ * ra.c : routines for interacting with the RA layer
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+
+
+#include <apr_pools.h>
+
+#include "svn_error.h"
+#include "svn_hash.h"
+#include "svn_pools.h"
+#include "svn_string.h"
+#include "svn_sorts.h"
+#include "svn_ra.h"
+#include "svn_client.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_props.h"
+#include "svn_mergeinfo.h"
+#include "client.h"
+#include "mergeinfo.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_client_private.h"
+
+
+/* This is the baton that we pass svn_ra_open3(), and is associated with
+ the callback table we provide to RA. */
+typedef struct callback_baton_t
+{
+ /* Holds the directory that corresponds to the REPOS_URL at svn_ra_open3()
+ time. When callbacks specify a relative path, they are joined with
+ this base directory. */
+ const char *base_dir_abspath;
+
+ /* TEMPORARY: Is 'base_dir_abspath' a versioned path? cmpilato
+ suspects that the commit-to-multiple-disjoint-working-copies
+ code is getting this all wrong, sometimes passing an unversioned
+ (or versioned in a foreign wc) path here which sorta kinda
+ happens to work most of the time but is ultimately incorrect. */
+ svn_boolean_t base_dir_isversioned;
+
+ /* Used as wri_abspath for obtaining access to the pristine store */
+ const char *wcroot_abspath;
+
+ /* An array of svn_client_commit_item3_t * structures, present only
+ during working copy commits. */
+ const apr_array_header_t *commit_items;
+
+ /* A client context. */
+ svn_client_ctx_t *ctx;
+
+} callback_baton_t;
+
+
+
+static svn_error_t *
+open_tmp_file(apr_file_t **fp,
+ void *callback_baton,
+ apr_pool_t *pool)
+{
+ return svn_error_trace(svn_io_open_unique_file3(fp, NULL, NULL,
+ svn_io_file_del_on_pool_cleanup,
+ pool, pool));
+}
+
+
+/* This implements the 'svn_ra_get_wc_prop_func_t' interface. */
+static svn_error_t *
+get_wc_prop(void *baton,
+ const char *relpath,
+ const char *name,
+ const svn_string_t **value,
+ apr_pool_t *pool)
+{
+ callback_baton_t *cb = baton;
+ const char *local_abspath = NULL;
+ svn_error_t *err;
+
+ *value = NULL;
+
+ /* If we have a list of commit_items, search through that for a
+ match for this relative URL. */
+ if (cb->commit_items)
+ {
+ int i;
+ for (i = 0; i < cb->commit_items->nelts; i++)
+ {
+ svn_client_commit_item3_t *item
+ = APR_ARRAY_IDX(cb->commit_items, i, svn_client_commit_item3_t *);
+
+ if (! strcmp(relpath, item->session_relpath))
+ {
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(item->path));
+ local_abspath = item->path;
+ break;
+ }
+ }
+
+ /* Commits can only query relpaths in the commit_items list
+ since the commit driver traverses paths as they are, or will
+ be, in the repository. Non-commits query relpaths in the
+ working copy. */
+ if (! local_abspath)
+ return SVN_NO_ERROR;
+ }
+
+ /* If we don't have a base directory, then there are no properties. */
+ else if (cb->base_dir_abspath == NULL)
+ return SVN_NO_ERROR;
+
+ else
+ local_abspath = svn_dirent_join(cb->base_dir_abspath, relpath, pool);
+
+ err = svn_wc_prop_get2(value, cb->ctx->wc_ctx, local_abspath, name,
+ pool, pool);
+ if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ err = NULL;
+ }
+ return svn_error_trace(err);
+}
+
+/* This implements the 'svn_ra_push_wc_prop_func_t' interface. */
+static svn_error_t *
+push_wc_prop(void *baton,
+ const char *relpath,
+ const char *name,
+ const svn_string_t *value,
+ apr_pool_t *pool)
+{
+ callback_baton_t *cb = baton;
+ int i;
+
+ /* If we're committing, search through the commit_items list for a
+ match for this relative URL. */
+ if (! cb->commit_items)
+ return svn_error_createf
+ (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Attempt to set wcprop '%s' on '%s' in a non-commit operation"),
+ name, svn_dirent_local_style(relpath, pool));
+
+ for (i = 0; i < cb->commit_items->nelts; i++)
+ {
+ svn_client_commit_item3_t *item
+ = APR_ARRAY_IDX(cb->commit_items, i, svn_client_commit_item3_t *);
+
+ if (strcmp(relpath, item->session_relpath) == 0)
+ {
+ apr_pool_t *changes_pool = item->incoming_prop_changes->pool;
+ svn_prop_t *prop = apr_palloc(changes_pool, sizeof(*prop));
+
+ prop->name = apr_pstrdup(changes_pool, name);
+ if (value)
+ prop->value = svn_string_dup(value, changes_pool);
+ else
+ prop->value = NULL;
+
+ /* Buffer the propchange to take effect during the
+ post-commit process. */
+ APR_ARRAY_PUSH(item->incoming_prop_changes, svn_prop_t *) = prop;
+ return SVN_NO_ERROR;
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* This implements the 'svn_ra_set_wc_prop_func_t' interface. */
+static svn_error_t *
+set_wc_prop(void *baton,
+ const char *path,
+ const char *name,
+ const svn_string_t *value,
+ apr_pool_t *pool)
+{
+ callback_baton_t *cb = baton;
+ const char *local_abspath;
+
+ local_abspath = svn_dirent_join(cb->base_dir_abspath, path, pool);
+
+ /* We pass 1 for the 'force' parameter here. Since the property is
+ coming from the repository, we definitely want to accept it.
+ Ideally, we'd raise a conflict if, say, the received property is
+ svn:eol-style yet the file has a locally added svn:mime-type
+ claiming that it's binary. Probably the repository is still
+ right, but the conflict would remind the user to make sure.
+ Unfortunately, we don't have a clean mechanism for doing that
+ here, so we just set the property and hope for the best. */
+ return svn_error_trace(svn_wc_prop_set4(cb->ctx->wc_ctx, local_abspath,
+ name,
+ value, svn_depth_empty,
+ TRUE /* skip_checks */,
+ NULL /* changelist_filter */,
+ NULL, NULL /* cancellation */,
+ NULL, NULL /* notification */,
+ pool));
+}
+
+
+/* This implements the `svn_ra_invalidate_wc_props_func_t' interface. */
+static svn_error_t *
+invalidate_wc_props(void *baton,
+ const char *path,
+ const char *prop_name,
+ apr_pool_t *pool)
+{
+ callback_baton_t *cb = baton;
+ const char *local_abspath;
+
+ local_abspath = svn_dirent_join(cb->base_dir_abspath, path, pool);
+
+ /* It's easier just to clear the whole dav_cache than to remove
+ individual items from it recursively like this. And since we
+ know that the RA providers that ship with Subversion only
+ invalidate the one property they use the most from this cache,
+ and that we're intentionally trying to get away from the use of
+ the cache altogether anyway, there's little to lose in wiping the
+ whole cache. Is it the most well-behaved approach to take? Not
+ so much. We choose not to care. */
+ return svn_error_trace(svn_wc__node_clear_dav_cache_recursive(
+ cb->ctx->wc_ctx, local_abspath, pool));
+}
+
+
+/* This implements the `svn_ra_get_wc_contents_func_t' interface. */
+static svn_error_t *
+get_wc_contents(void *baton,
+ svn_stream_t **contents,
+ const svn_checksum_t *checksum,
+ apr_pool_t *pool)
+{
+ callback_baton_t *cb = baton;
+
+ if (! cb->wcroot_abspath)
+ {
+ *contents = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ return svn_error_trace(
+ svn_wc__get_pristine_contents_by_checksum(contents,
+ cb->ctx->wc_ctx,
+ cb->wcroot_abspath,
+ checksum,
+ pool, pool));
+}
+
+
+static svn_error_t *
+cancel_callback(void *baton)
+{
+ callback_baton_t *b = baton;
+ return svn_error_trace((b->ctx->cancel_func)(b->ctx->cancel_baton));
+}
+
+
+static svn_error_t *
+get_client_string(void *baton,
+ const char **name,
+ apr_pool_t *pool)
+{
+ callback_baton_t *b = baton;
+ *name = apr_pstrdup(pool, b->ctx->client_name);
+ return SVN_NO_ERROR;
+}
+
+
+#define SVN_CLIENT__MAX_REDIRECT_ATTEMPTS 3 /* ### TODO: Make configurable. */
+
+svn_error_t *
+svn_client__open_ra_session_internal(svn_ra_session_t **ra_session,
+ const char **corrected_url,
+ const char *base_url,
+ const char *base_dir_abspath,
+ const apr_array_header_t *commit_items,
+ svn_boolean_t write_dav_props,
+ svn_boolean_t read_dav_props,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_ra_callbacks2_t *cbtable;
+ callback_baton_t *cb = apr_pcalloc(result_pool, sizeof(*cb));
+ const char *uuid = NULL;
+
+ SVN_ERR_ASSERT(!write_dav_props || read_dav_props);
+ SVN_ERR_ASSERT(!read_dav_props || base_dir_abspath != NULL);
+ SVN_ERR_ASSERT(base_dir_abspath == NULL
+ || svn_dirent_is_absolute(base_dir_abspath));
+
+ SVN_ERR(svn_ra_create_callbacks(&cbtable, result_pool));
+ cbtable->open_tmp_file = open_tmp_file;
+ cbtable->get_wc_prop = read_dav_props ? get_wc_prop : NULL;
+ cbtable->set_wc_prop = (write_dav_props && read_dav_props)
+ ? set_wc_prop : NULL;
+ cbtable->push_wc_prop = commit_items ? push_wc_prop : NULL;
+ cbtable->invalidate_wc_props = (write_dav_props && read_dav_props)
+ ? invalidate_wc_props : NULL;
+ cbtable->auth_baton = ctx->auth_baton; /* new-style */
+ cbtable->progress_func = ctx->progress_func;
+ cbtable->progress_baton = ctx->progress_baton;
+ cbtable->cancel_func = ctx->cancel_func ? cancel_callback : NULL;
+ cbtable->get_client_string = get_client_string;
+ if (base_dir_abspath)
+ cbtable->get_wc_contents = get_wc_contents;
+
+ cb->commit_items = commit_items;
+ cb->ctx = ctx;
+
+ if (base_dir_abspath && (read_dav_props || write_dav_props))
+ {
+ svn_error_t *err = svn_wc__node_get_repos_info(NULL, NULL, NULL, &uuid,
+ ctx->wc_ctx,
+ base_dir_abspath,
+ result_pool,
+ scratch_pool);
+
+ if (err && (err->apr_err == SVN_ERR_WC_NOT_WORKING_COPY
+ || err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND
+ || err->apr_err == SVN_ERR_WC_UPGRADE_REQUIRED))
+ {
+ svn_error_clear(err);
+ uuid = NULL;
+ }
+ else
+ {
+ SVN_ERR(err);
+ cb->base_dir_isversioned = TRUE;
+ }
+ cb->base_dir_abspath = apr_pstrdup(result_pool, base_dir_abspath);
+ }
+
+ if (base_dir_abspath)
+ {
+ svn_error_t *err = svn_wc__get_wcroot(&cb->wcroot_abspath,
+ ctx->wc_ctx, base_dir_abspath,
+ result_pool, scratch_pool);
+
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY
+ && err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
+ && err->apr_err != SVN_ERR_WC_UPGRADE_REQUIRED)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ cb->wcroot_abspath = NULL;
+ }
+ }
+
+ /* If the caller allows for auto-following redirections, and the
+ RA->open() call above reveals a CORRECTED_URL, try the new URL.
+ We'll do this in a loop up to some maximum number follow-and-retry
+ attempts. */
+ if (corrected_url)
+ {
+ apr_hash_t *attempted = apr_hash_make(scratch_pool);
+ int attempts_left = SVN_CLIENT__MAX_REDIRECT_ATTEMPTS;
+
+ *corrected_url = NULL;
+ while (attempts_left--)
+ {
+ const char *corrected = NULL;
+
+ /* Try to open the RA session. If this is our last attempt,
+ don't accept corrected URLs from the RA provider. */
+ SVN_ERR(svn_ra_open4(ra_session,
+ attempts_left == 0 ? NULL : &corrected,
+ base_url, uuid, cbtable, cb, ctx->config,
+ result_pool));
+
+ /* No error and no corrected URL? We're done here. */
+ if (! corrected)
+ break;
+
+ /* Notify the user that a redirect is being followed. */
+ if (ctx->notify_func2 != NULL)
+ {
+ svn_wc_notify_t *notify =
+ svn_wc_create_notify_url(corrected,
+ svn_wc_notify_url_redirect,
+ scratch_pool);
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, scratch_pool);
+ }
+
+ /* Our caller will want to know what our final corrected URL was. */
+ *corrected_url = corrected;
+
+ /* Make sure we've not attempted this URL before. */
+ if (svn_hash_gets(attempted, corrected))
+ return svn_error_createf(SVN_ERR_CLIENT_CYCLE_DETECTED, NULL,
+ _("Redirect cycle detected for URL '%s'"),
+ corrected);
+
+ /* Remember this CORRECTED_URL so we don't wind up in a loop. */
+ svn_hash_sets(attempted, corrected, (void *)1);
+ base_url = corrected;
+ }
+ }
+ else
+ {
+ SVN_ERR(svn_ra_open4(ra_session, NULL, base_url,
+ uuid, cbtable, cb, ctx->config, result_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+#undef SVN_CLIENT__MAX_REDIRECT_ATTEMPTS
+
+
+svn_error_t *
+svn_client_open_ra_session2(svn_ra_session_t **session,
+ const char *url,
+ const char *wri_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ return svn_error_trace(
+ svn_client__open_ra_session_internal(session, NULL, url,
+ wri_abspath, NULL,
+ FALSE, FALSE,
+ ctx, result_pool,
+ scratch_pool));
+}
+
+svn_error_t *
+svn_client__resolve_rev_and_url(svn_client__pathrev_t **resolved_loc_p,
+ svn_ra_session_t *ra_session,
+ const char *path_or_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_opt_revision_t peg_rev = *peg_revision;
+ svn_opt_revision_t start_rev = *revision;
+ const char *url;
+ svn_revnum_t rev;
+
+ /* Default revisions: peg -> working or head; operative -> peg. */
+ SVN_ERR(svn_opt_resolve_revisions(&peg_rev, &start_rev,
+ svn_path_is_url(path_or_url),
+ TRUE /* notice_local_mods */,
+ pool));
+
+ /* Run the history function to get the object's (possibly
+ different) url in REVISION. */
+ SVN_ERR(svn_client__repos_locations(&url, &rev, NULL, NULL,
+ ra_session, path_or_url, &peg_rev,
+ &start_rev, NULL, ctx, pool));
+
+ SVN_ERR(svn_client__pathrev_create_with_session(resolved_loc_p,
+ ra_session, rev, url, pool));
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__ra_session_from_path2(svn_ra_session_t **ra_session_p,
+ svn_client__pathrev_t **resolved_loc_p,
+ const char *path_or_url,
+ const char *base_dir_abspath,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_ra_session_t *ra_session;
+ const char *initial_url;
+ const char *corrected_url;
+ svn_client__pathrev_t *resolved_loc;
+ const char *wri_abspath;
+
+ SVN_ERR(svn_client_url_from_path2(&initial_url, path_or_url, ctx, pool,
+ pool));
+ if (! initial_url)
+ return svn_error_createf(SVN_ERR_ENTRY_MISSING_URL, NULL,
+ _("'%s' has no URL"), path_or_url);
+
+ if (base_dir_abspath)
+ wri_abspath = base_dir_abspath;
+ else if (!svn_path_is_url(path_or_url))
+ SVN_ERR(svn_dirent_get_absolute(&wri_abspath, path_or_url, pool));
+ else
+ wri_abspath = NULL;
+
+ SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
+ initial_url,
+ wri_abspath,
+ NULL /* commit_items */,
+ base_dir_abspath != NULL,
+ base_dir_abspath != NULL,
+ ctx, pool, pool));
+
+ /* If we got a CORRECTED_URL, we'll want to refer to that as the
+ URL-ized form of PATH_OR_URL from now on. */
+ if (corrected_url && svn_path_is_url(path_or_url))
+ path_or_url = corrected_url;
+
+ SVN_ERR(svn_client__resolve_rev_and_url(&resolved_loc, ra_session,
+ path_or_url, peg_revision, revision,
+ ctx, pool));
+
+ /* Make the session point to the real URL. */
+ SVN_ERR(svn_ra_reparent(ra_session, resolved_loc->url, pool));
+
+ *ra_session_p = ra_session;
+ if (resolved_loc_p)
+ *resolved_loc_p = resolved_loc;
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__ensure_ra_session_url(const char **old_session_url,
+ svn_ra_session_t *ra_session,
+ const char *session_url,
+ apr_pool_t *pool)
+{
+ SVN_ERR(svn_ra_get_session_url(ra_session, old_session_url, pool));
+ if (! session_url)
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &session_url, pool));
+ if (strcmp(*old_session_url, session_url) != 0)
+ SVN_ERR(svn_ra_reparent(ra_session, session_url, pool));
+ return SVN_NO_ERROR;
+}
+
+
+
+/*** Repository Locations ***/
+
+struct gls_receiver_baton_t
+{
+ apr_array_header_t *segments;
+ svn_client_ctx_t *ctx;
+ apr_pool_t *pool;
+};
+
+static svn_error_t *
+gls_receiver(svn_location_segment_t *segment,
+ void *baton,
+ apr_pool_t *pool)
+{
+ struct gls_receiver_baton_t *b = baton;
+ APR_ARRAY_PUSH(b->segments, svn_location_segment_t *) =
+ svn_location_segment_dup(segment, b->pool);
+ if (b->ctx->cancel_func)
+ SVN_ERR((b->ctx->cancel_func)(b->ctx->cancel_baton));
+ return SVN_NO_ERROR;
+}
+
+/* A qsort-compatible function which sorts svn_location_segment_t's
+ based on their revision range covering, resulting in ascending
+ (oldest-to-youngest) ordering. */
+static int
+compare_segments(const void *a, const void *b)
+{
+ const svn_location_segment_t *a_seg
+ = *((const svn_location_segment_t * const *) a);
+ const svn_location_segment_t *b_seg
+ = *((const svn_location_segment_t * const *) b);
+ if (a_seg->range_start == b_seg->range_start)
+ return 0;
+ return (a_seg->range_start < b_seg->range_start) ? -1 : 1;
+}
+
+svn_error_t *
+svn_client__repos_location_segments(apr_array_header_t **segments,
+ svn_ra_session_t *ra_session,
+ const char *url,
+ svn_revnum_t peg_revision,
+ svn_revnum_t start_revision,
+ svn_revnum_t end_revision,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct gls_receiver_baton_t gls_receiver_baton;
+ const char *old_session_url;
+ svn_error_t *err;
+
+ *segments = apr_array_make(pool, 8, sizeof(svn_location_segment_t *));
+ gls_receiver_baton.segments = *segments;
+ gls_receiver_baton.ctx = ctx;
+ gls_receiver_baton.pool = pool;
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session,
+ url, pool));
+ err = svn_ra_get_location_segments(ra_session, "", peg_revision,
+ start_revision, end_revision,
+ gls_receiver, &gls_receiver_baton,
+ pool);
+ SVN_ERR(svn_error_compose_create(
+ err, svn_ra_reparent(ra_session, old_session_url, pool)));
+ qsort((*segments)->elts, (*segments)->nelts,
+ (*segments)->elt_size, compare_segments);
+ return SVN_NO_ERROR;
+}
+
+/* Set *START_URL and *END_URL to the URLs that the object URL@PEG_REVNUM
+ * had in revisions START_REVNUM and END_REVNUM. Return an error if the
+ * node cannot be traced back to one of the requested revisions.
+ *
+ * START_URL and/or END_URL may be NULL if not wanted. START_REVNUM and
+ * END_REVNUM must be valid revision numbers except that END_REVNUM may
+ * be SVN_INVALID_REVNUM if END_URL is NULL.
+ *
+ * RA_SESSION is an open RA session parented at URL.
+ */
+static svn_error_t *
+repos_locations(const char **start_url,
+ const char **end_url,
+ svn_ra_session_t *ra_session,
+ const char *url,
+ svn_revnum_t peg_revnum,
+ svn_revnum_t start_revnum,
+ svn_revnum_t end_revnum,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *repos_url, *start_path, *end_path;
+ apr_array_header_t *revs;
+ apr_hash_t *rev_locs;
+
+ SVN_ERR_ASSERT(peg_revnum != SVN_INVALID_REVNUM);
+ SVN_ERR_ASSERT(start_revnum != SVN_INVALID_REVNUM);
+ SVN_ERR_ASSERT(end_revnum != SVN_INVALID_REVNUM || end_url == NULL);
+
+ /* Avoid a network request in the common easy case. */
+ if (start_revnum == peg_revnum
+ && (end_revnum == peg_revnum || end_revnum == SVN_INVALID_REVNUM))
+ {
+ if (start_url)
+ *start_url = apr_pstrdup(result_pool, url);
+ if (end_url)
+ *end_url = apr_pstrdup(result_pool, url);
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_url, scratch_pool));
+
+ revs = apr_array_make(scratch_pool, 2, sizeof(svn_revnum_t));
+ APR_ARRAY_PUSH(revs, svn_revnum_t) = start_revnum;
+ if (end_revnum != start_revnum && end_revnum != SVN_INVALID_REVNUM)
+ APR_ARRAY_PUSH(revs, svn_revnum_t) = end_revnum;
+
+ SVN_ERR(svn_ra_get_locations(ra_session, &rev_locs, "", peg_revnum,
+ revs, scratch_pool));
+
+ /* We'd better have all the paths we were looking for! */
+ if (start_url)
+ {
+ start_path = apr_hash_get(rev_locs, &start_revnum, sizeof(svn_revnum_t));
+ if (! start_path)
+ return svn_error_createf
+ (SVN_ERR_CLIENT_UNRELATED_RESOURCES, NULL,
+ _("Unable to find repository location for '%s' in revision %ld"),
+ url, start_revnum);
+ *start_url = svn_path_url_add_component2(repos_url, start_path + 1,
+ result_pool);
+ }
+
+ if (end_url)
+ {
+ end_path = apr_hash_get(rev_locs, &end_revnum, sizeof(svn_revnum_t));
+ if (! end_path)
+ return svn_error_createf
+ (SVN_ERR_CLIENT_UNRELATED_RESOURCES, NULL,
+ _("The location for '%s' for revision %ld does not exist in the "
+ "repository or refers to an unrelated object"),
+ url, end_revnum);
+
+ *end_url = svn_path_url_add_component2(repos_url, end_path + 1,
+ result_pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__repos_location(svn_client__pathrev_t **op_loc_p,
+ svn_ra_session_t *ra_session,
+ const svn_client__pathrev_t *peg_loc,
+ svn_revnum_t op_revnum,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *old_session_url;
+ const char *op_url;
+ svn_error_t *err;
+
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session,
+ peg_loc->url, scratch_pool));
+ err = repos_locations(&op_url, NULL, ra_session,
+ peg_loc->url, peg_loc->rev,
+ op_revnum, SVN_INVALID_REVNUM,
+ result_pool, scratch_pool);
+ SVN_ERR(svn_error_compose_create(
+ err, svn_ra_reparent(ra_session, old_session_url, scratch_pool)));
+
+ *op_loc_p = svn_client__pathrev_create(peg_loc->repos_root_url,
+ peg_loc->repos_uuid,
+ op_revnum, op_url, result_pool);
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__repos_locations(const char **start_url,
+ svn_revnum_t *start_revision,
+ const char **end_url,
+ svn_revnum_t *end_revision,
+ svn_ra_session_t *ra_session,
+ const char *path,
+ const svn_opt_revision_t *revision,
+ const svn_opt_revision_t *start,
+ const svn_opt_revision_t *end,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *url;
+ const char *local_abspath_or_url;
+ svn_revnum_t peg_revnum = SVN_INVALID_REVNUM;
+ svn_revnum_t start_revnum, end_revnum;
+ svn_revnum_t youngest_rev = SVN_INVALID_REVNUM;
+ apr_pool_t *subpool = svn_pool_create(pool);
+
+ /* Ensure that we are given some real revision data to work with.
+ (It's okay if the END is unspecified -- in that case, we'll just
+ set it to the same thing as START.) */
+ if (revision->kind == svn_opt_revision_unspecified
+ || start->kind == svn_opt_revision_unspecified)
+ return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
+
+ if (end == NULL)
+ {
+ static const svn_opt_revision_t unspecified_rev
+ = { svn_opt_revision_unspecified, { 0 } };
+
+ end = &unspecified_rev;
+ }
+
+ /* Determine LOCAL_ABSPATH_OR_URL, URL, and possibly PEG_REVNUM.
+ If we are looking at the working version of a WC path that is scheduled
+ as a copy, then we need to use the copy-from URL and peg revision. */
+ if (! svn_path_is_url(path))
+ {
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath_or_url, path, subpool));
+
+ if (revision->kind == svn_opt_revision_working)
+ {
+ const char *repos_root_url;
+ const char *repos_relpath;
+ svn_boolean_t is_copy;
+
+ SVN_ERR(svn_wc__node_get_origin(&is_copy, &peg_revnum, &repos_relpath,
+ &repos_root_url, NULL, NULL,
+ ctx->wc_ctx, local_abspath_or_url,
+ FALSE, subpool, subpool));
+
+ if (repos_relpath)
+ url = svn_path_url_add_component2(repos_root_url, repos_relpath,
+ pool);
+ else
+ url = NULL;
+
+ if (url && is_copy && ra_session)
+ {
+ const char *session_url;
+ SVN_ERR(svn_ra_get_session_url(ra_session, &session_url,
+ subpool));
+
+ if (strcmp(session_url, url) != 0)
+ {
+ /* We can't use the caller provided RA session now :( */
+ ra_session = NULL;
+ }
+ }
+ }
+ else
+ url = NULL;
+
+ if (! url)
+ SVN_ERR(svn_wc__node_get_url(&url, ctx->wc_ctx,
+ local_abspath_or_url, pool, subpool));
+
+ if (!url)
+ {
+ return svn_error_createf(SVN_ERR_ENTRY_MISSING_URL, NULL,
+ _("'%s' has no URL"),
+ svn_dirent_local_style(path, pool));
+ }
+ }
+ else
+ {
+ local_abspath_or_url = path;
+ url = path;
+ }
+
+ /* ### We should be smarter here. If the callers just asks for BASE and
+ WORKING revisions, we should already have the correct URLs, so we
+ don't need to do anything more here in that case. */
+
+ /* Open a RA session to this URL if we don't have one already. */
+ if (! ra_session)
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, url, NULL,
+ ctx, subpool, subpool));
+
+ /* Resolve the opt_revision_ts. */
+ if (peg_revnum == SVN_INVALID_REVNUM)
+ SVN_ERR(svn_client__get_revision_number(&peg_revnum, &youngest_rev,
+ ctx->wc_ctx, local_abspath_or_url,
+ ra_session, revision, pool));
+
+ SVN_ERR(svn_client__get_revision_number(&start_revnum, &youngest_rev,
+ ctx->wc_ctx, local_abspath_or_url,
+ ra_session, start, pool));
+ if (end->kind == svn_opt_revision_unspecified)
+ end_revnum = start_revnum;
+ else
+ SVN_ERR(svn_client__get_revision_number(&end_revnum, &youngest_rev,
+ ctx->wc_ctx, local_abspath_or_url,
+ ra_session, end, pool));
+
+ /* Set the output revision variables. */
+ if (start_revision)
+ {
+ *start_revision = start_revnum;
+ }
+ if (end_revision && end->kind != svn_opt_revision_unspecified)
+ {
+ *end_revision = end_revnum;
+ }
+
+ SVN_ERR(repos_locations(start_url, end_url,
+ ra_session, url, peg_revnum,
+ start_revnum, end_revnum,
+ pool, subpool));
+ svn_pool_destroy(subpool);
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__get_youngest_common_ancestor(svn_client__pathrev_t **ancestor_p,
+ const svn_client__pathrev_t *loc1,
+ const svn_client__pathrev_t *loc2,
+ svn_ra_session_t *session,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *sesspool = NULL;
+ apr_hash_t *history1, *history2;
+ apr_hash_index_t *hi;
+ svn_revnum_t yc_revision = SVN_INVALID_REVNUM;
+ const char *yc_relpath = NULL;
+ svn_boolean_t has_rev_zero_history1;
+ svn_boolean_t has_rev_zero_history2;
+
+ if (strcmp(loc1->repos_root_url, loc2->repos_root_url) != 0)
+ {
+ *ancestor_p = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ /* Open an RA session for the two locations. */
+ if (session == NULL)
+ {
+ sesspool = svn_pool_create(scratch_pool);
+ SVN_ERR(svn_client_open_ra_session2(&session, loc1->url, NULL, ctx,
+ sesspool, sesspool));
+ }
+
+ /* We're going to cheat and use history-as-mergeinfo because it
+ saves us a bunch of annoying custom data comparisons and such. */
+ SVN_ERR(svn_client__get_history_as_mergeinfo(&history1,
+ &has_rev_zero_history1,
+ loc1,
+ SVN_INVALID_REVNUM,
+ SVN_INVALID_REVNUM,
+ session, ctx, scratch_pool));
+ SVN_ERR(svn_client__get_history_as_mergeinfo(&history2,
+ &has_rev_zero_history2,
+ loc2,
+ SVN_INVALID_REVNUM,
+ SVN_INVALID_REVNUM,
+ session, ctx, scratch_pool));
+ /* Close the ra session if we opened one. */
+ if (sesspool)
+ svn_pool_destroy(sesspool);
+
+ /* Loop through the first location's history, check for overlapping
+ paths and ranges in the second location's history, and
+ remembering the youngest matching location. */
+ for (hi = apr_hash_first(scratch_pool, history1); hi; hi = apr_hash_next(hi))
+ {
+ const char *path = svn__apr_hash_index_key(hi);
+ apr_ssize_t path_len = svn__apr_hash_index_klen(hi);
+ svn_rangelist_t *ranges1 = svn__apr_hash_index_val(hi);
+ svn_rangelist_t *ranges2, *common;
+
+ ranges2 = apr_hash_get(history2, path, path_len);
+ if (ranges2)
+ {
+ /* We have a path match. Now, did our two histories share
+ any revisions at that path? */
+ SVN_ERR(svn_rangelist_intersect(&common, ranges1, ranges2,
+ TRUE, scratch_pool));
+ if (common->nelts)
+ {
+ svn_merge_range_t *yc_range =
+ APR_ARRAY_IDX(common, common->nelts - 1, svn_merge_range_t *);
+ if ((! SVN_IS_VALID_REVNUM(yc_revision))
+ || (yc_range->end > yc_revision))
+ {
+ yc_revision = yc_range->end;
+ yc_relpath = path + 1;
+ }
+ }
+ }
+ }
+
+ /* It's possible that PATH_OR_URL1 and PATH_OR_URL2's only common
+ history is revision 0. */
+ if (!yc_relpath && has_rev_zero_history1 && has_rev_zero_history2)
+ {
+ yc_relpath = "";
+ yc_revision = 0;
+ }
+
+ if (yc_relpath)
+ {
+ *ancestor_p = svn_client__pathrev_create_with_relpath(
+ loc1->repos_root_url, loc1->repos_uuid,
+ yc_revision, yc_relpath, result_pool);
+ }
+ else
+ {
+ *ancestor_p = NULL;
+ }
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__youngest_common_ancestor(const char **ancestor_url,
+ svn_revnum_t *ancestor_rev,
+ const char *path_or_url1,
+ const svn_opt_revision_t *revision1,
+ const char *path_or_url2,
+ const svn_opt_revision_t *revision2,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *sesspool = svn_pool_create(scratch_pool);
+ svn_ra_session_t *session;
+ svn_client__pathrev_t *loc1, *loc2, *ancestor;
+
+ /* Resolve the two locations */
+ SVN_ERR(svn_client__ra_session_from_path2(&session, &loc1,
+ path_or_url1, NULL,
+ revision1, revision1,
+ ctx, sesspool));
+ SVN_ERR(svn_client__resolve_rev_and_url(&loc2, session,
+ path_or_url2, revision2, revision2,
+ ctx, scratch_pool));
+
+ SVN_ERR(svn_client__get_youngest_common_ancestor(
+ &ancestor, loc1, loc2, session, ctx, result_pool, scratch_pool));
+
+ if (ancestor)
+ {
+ *ancestor_url = ancestor->url;
+ *ancestor_rev = ancestor->rev;
+ }
+ else
+ {
+ *ancestor_url = NULL;
+ *ancestor_rev = SVN_INVALID_REVNUM;
+ }
+ svn_pool_destroy(sesspool);
+ return SVN_NO_ERROR;
+}
+
+
+struct ra_ev2_baton {
+ /* The working copy context, from the client context. */
+ svn_wc_context_t *wc_ctx;
+
+ /* For a given REPOS_RELPATH, provide a LOCAL_ABSPATH that represents
+ that repository node. */
+ apr_hash_t *relpath_map;
+};
+
+
+svn_error_t *
+svn_client__ra_provide_base(svn_stream_t **contents,
+ svn_revnum_t *revision,
+ void *baton,
+ const char *repos_relpath,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct ra_ev2_baton *reb = baton;
+ const char *local_abspath;
+ svn_error_t *err;
+
+ local_abspath = svn_hash_gets(reb->relpath_map, repos_relpath);
+ if (!local_abspath)
+ {
+ *contents = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ err = svn_wc_get_pristine_contents2(contents, reb->wc_ctx, local_abspath,
+ result_pool, scratch_pool);
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ *contents = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ if (*contents != NULL)
+ {
+ /* The pristine contents refer to the BASE, or to the pristine of
+ a copy/move to this location. Fetch the correct revision. */
+ SVN_ERR(svn_wc__node_get_origin(NULL, revision, NULL, NULL, NULL, NULL,
+ reb->wc_ctx, local_abspath, FALSE,
+ scratch_pool, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__ra_provide_props(apr_hash_t **props,
+ svn_revnum_t *revision,
+ void *baton,
+ const char *repos_relpath,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct ra_ev2_baton *reb = baton;
+ const char *local_abspath;
+ svn_error_t *err;
+
+ local_abspath = svn_hash_gets(reb->relpath_map, repos_relpath);
+ if (!local_abspath)
+ {
+ *props = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ err = svn_wc_get_pristine_props(props, reb->wc_ctx, local_abspath,
+ result_pool, scratch_pool);
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ *props = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ if (*props != NULL)
+ {
+ /* The pristine props refer to the BASE, or to the pristine props of
+ a copy/move to this location. Fetch the correct revision. */
+ SVN_ERR(svn_wc__node_get_origin(NULL, revision, NULL, NULL, NULL, NULL,
+ reb->wc_ctx, local_abspath, FALSE,
+ scratch_pool, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client__ra_get_copysrc_kind(svn_node_kind_t *kind,
+ void *baton,
+ const char *repos_relpath,
+ svn_revnum_t src_revision,
+ apr_pool_t *scratch_pool)
+{
+ struct ra_ev2_baton *reb = baton;
+ const char *local_abspath;
+
+ local_abspath = svn_hash_gets(reb->relpath_map, repos_relpath);
+ if (!local_abspath)
+ {
+ *kind = svn_node_unknown;
+ return SVN_NO_ERROR;
+ }
+
+ /* ### what to do with SRC_REVISION? */
+
+ SVN_ERR(svn_wc_read_kind2(kind, reb->wc_ctx, local_abspath,
+ FALSE, FALSE, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+void *
+svn_client__ra_make_cb_baton(svn_wc_context_t *wc_ctx,
+ apr_hash_t *relpath_map,
+ apr_pool_t *result_pool)
+{
+ struct ra_ev2_baton *reb = apr_palloc(result_pool, sizeof(*reb));
+
+ SVN_ERR_ASSERT_NO_RETURN(wc_ctx != NULL);
+ SVN_ERR_ASSERT_NO_RETURN(relpath_map != NULL);
+
+ reb->wc_ctx = wc_ctx;
+ reb->relpath_map = relpath_map;
+
+ return reb;
+}
diff --git a/subversion/libsvn_client/relocate.c b/subversion/libsvn_client/relocate.c
new file mode 100644
index 0000000..ed8d09c
--- /dev/null
+++ b/subversion/libsvn_client/relocate.c
@@ -0,0 +1,289 @@
+/*
+ * relocate.c: wrapper around wc relocation functionality.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "svn_pools.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "client.h"
+
+#include "private/svn_wc_private.h"
+
+#include "svn_private_config.h"
+
+
+/*** Code. ***/
+
+/* Repository root and UUID for a repository. */
+struct url_uuid_t
+{
+ const char *root;
+ const char *uuid;
+};
+
+struct validator_baton_t
+{
+ svn_client_ctx_t *ctx;
+ const char *path;
+ apr_array_header_t *url_uuids;
+ apr_pool_t *pool;
+
+};
+
+
+static svn_error_t *
+validator_func(void *baton,
+ const char *uuid,
+ const char *url,
+ const char *root_url,
+ apr_pool_t *pool)
+{
+ struct validator_baton_t *b = baton;
+ struct url_uuid_t *url_uuid = NULL;
+ const char *disable_checks;
+
+ apr_array_header_t *uuids = b->url_uuids;
+ int i;
+
+ for (i = 0; i < uuids->nelts; ++i)
+ {
+ struct url_uuid_t *uu = &APR_ARRAY_IDX(uuids, i,
+ struct url_uuid_t);
+ if (svn_uri__is_ancestor(uu->root, url))
+ {
+ url_uuid = uu;
+ break;
+ }
+ }
+
+ disable_checks = getenv("SVN_I_LOVE_CORRUPTED_WORKING_COPIES_SO_DISABLE_RELOCATE_VALIDATION");
+ if (disable_checks && (strcmp(disable_checks, "yes") == 0))
+ {
+ /* Lie about URL_UUID's components, claiming they match the
+ expectations of the validation code below. */
+ url_uuid = apr_pcalloc(pool, sizeof(*url_uuid));
+ url_uuid->root = apr_pstrdup(pool, root_url);
+ url_uuid->uuid = apr_pstrdup(pool, uuid);
+ }
+
+ /* We use an RA session in a subpool to get the UUID of the
+ repository at the new URL so we can force the RA session to close
+ by destroying the subpool. */
+ if (! url_uuid)
+ {
+ apr_pool_t *sesspool = svn_pool_create(pool);
+
+ url_uuid = &APR_ARRAY_PUSH(uuids, struct url_uuid_t);
+ SVN_ERR(svn_client_get_repos_root(&url_uuid->root,
+ &url_uuid->uuid,
+ url, b->ctx,
+ pool, sesspool));
+
+ svn_pool_destroy(sesspool);
+ }
+
+ /* Make sure the url is a repository root if desired. */
+ if (root_url
+ && strcmp(root_url, url_uuid->root) != 0)
+ return svn_error_createf(SVN_ERR_CLIENT_INVALID_RELOCATION, NULL,
+ _("'%s' is not the root of the repository"),
+ url);
+
+ /* Make sure the UUIDs match. */
+ if (uuid && strcmp(uuid, url_uuid->uuid) != 0)
+ return svn_error_createf
+ (SVN_ERR_CLIENT_INVALID_RELOCATION, NULL,
+ _("The repository at '%s' has uuid '%s', but the WC has '%s'"),
+ url, url_uuid->uuid, uuid);
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Examing the array of svn_wc_external_item2_t's EXT_DESC (parsed
+ from the svn:externals property set on LOCAL_ABSPATH) and determine
+ if the external working copies described by such should be
+ relocated as a side-effect of the relocation of their parent
+ working copy (from OLD_PARENT_REPOS_ROOT_URL to
+ NEW_PARENT_REPOS_ROOT_URL). If so, attempt said relocation. */
+static svn_error_t *
+relocate_externals(const char *local_abspath,
+ apr_array_header_t *ext_desc,
+ const char *old_parent_repos_root_url,
+ const char *new_parent_repos_root_url,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool;
+ int i;
+
+ /* Parse an externals definition into an array of external items. */
+
+ iterpool = svn_pool_create(scratch_pool);
+
+ for (i = 0; i < ext_desc->nelts; i++)
+ {
+ svn_wc_external_item2_t *ext_item =
+ APR_ARRAY_IDX(ext_desc, i, svn_wc_external_item2_t *);
+ const char *target_repos_root_url;
+ const char *target_abspath;
+ svn_error_t *err;
+
+ svn_pool_clear(iterpool);
+
+ /* If this external isn't pulled in via a relative URL, ignore
+ it. There's no sense in relocating a working copy only to
+ have the next 'svn update' try to point it back to another
+ location. */
+ if (! ((strncmp("../", ext_item->url, 3) == 0) ||
+ (strncmp("^/", ext_item->url, 2) == 0)))
+ continue;
+
+ /* If the external working copy's not-yet-relocated repos root
+ URL matches the primary working copy's pre-relocated
+ repository root URL, try to relocate that external, too.
+ You might wonder why this check is needed, given that we're
+ already limiting ourselves to externals pulled via URLs
+ relative to their primary working copy. Well, it's because
+ you can use "../" to "crawl up" above one repository's URL
+ space and down into another one. */
+ SVN_ERR(svn_dirent_get_absolute(&target_abspath,
+ svn_dirent_join(local_abspath,
+ ext_item->target_dir,
+ iterpool),
+ iterpool));
+ err = svn_client_get_repos_root(&target_repos_root_url, NULL /* uuid */,
+ target_abspath, ctx, iterpool, iterpool);
+
+ /* Ignore externals that aren't present in the working copy.
+ * This can happen if an external is deleted from disk accidentally,
+ * or if an external is configured on a locally added directory. */
+ if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ continue;
+ }
+ else
+ SVN_ERR(err);
+
+ if (strcmp(target_repos_root_url, old_parent_repos_root_url) == 0)
+ SVN_ERR(svn_client_relocate2(target_abspath,
+ old_parent_repos_root_url,
+ new_parent_repos_root_url,
+ FALSE, ctx, iterpool));
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_relocate2(const char *wcroot_dir,
+ const char *from_prefix,
+ const char *to_prefix,
+ svn_boolean_t ignore_externals,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ struct validator_baton_t vb;
+ const char *local_abspath;
+ apr_hash_t *externals_hash = NULL;
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool = NULL;
+ const char *old_repos_root_url, *new_repos_root_url;
+
+ /* Populate our validator callback baton, and call the relocate code. */
+ vb.ctx = ctx;
+ vb.path = wcroot_dir;
+ vb.url_uuids = apr_array_make(pool, 1, sizeof(struct url_uuid_t));
+ vb.pool = pool;
+
+ if (svn_path_is_url(wcroot_dir))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"),
+ wcroot_dir);
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, wcroot_dir, pool));
+
+ /* If we're ignoring externals, just relocate and get outta here. */
+ if (ignore_externals)
+ {
+ return svn_error_trace(svn_wc_relocate4(ctx->wc_ctx, local_abspath,
+ from_prefix, to_prefix,
+ validator_func, &vb, pool));
+ }
+
+ /* Fetch our current root URL. */
+ SVN_ERR(svn_client_get_repos_root(&old_repos_root_url, NULL /* uuid */,
+ local_abspath, ctx, pool, pool));
+
+ /* Perform the relocation. */
+ SVN_ERR(svn_wc_relocate4(ctx->wc_ctx, local_abspath, from_prefix, to_prefix,
+ validator_func, &vb, pool));
+
+ /* Now fetch new current root URL. */
+ SVN_ERR(svn_client_get_repos_root(&new_repos_root_url, NULL /* uuid */,
+ local_abspath, ctx, pool, pool));
+
+
+ /* Relocate externals, too (if any). */
+ SVN_ERR(svn_wc__externals_gather_definitions(&externals_hash, NULL,
+ ctx->wc_ctx, local_abspath,
+ svn_depth_infinity,
+ pool, pool));
+ if (! apr_hash_count(externals_hash))
+ return SVN_NO_ERROR;
+
+ iterpool = svn_pool_create(pool);
+
+ for (hi = apr_hash_first(pool, externals_hash);
+ hi != NULL;
+ hi = apr_hash_next(hi))
+ {
+ const char *this_abspath = svn__apr_hash_index_key(hi);
+ const char *value = svn__apr_hash_index_val(hi);
+ apr_array_header_t *ext_desc;
+
+ svn_pool_clear(iterpool);
+
+ SVN_ERR(svn_wc_parse_externals_description3(&ext_desc, this_abspath,
+ value, FALSE,
+ iterpool));
+ if (ext_desc->nelts)
+ SVN_ERR(relocate_externals(this_abspath, ext_desc, old_repos_root_url,
+ new_repos_root_url, ctx, iterpool));
+ }
+
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/repos_diff.c b/subversion/libsvn_client/repos_diff.c
new file mode 100644
index 0000000..6a7725f
--- /dev/null
+++ b/subversion/libsvn_client/repos_diff.c
@@ -0,0 +1,1405 @@
+/*
+ * repos_diff.c -- The diff editor for comparing two repository versions
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* This code uses an editor driven by a tree delta between two
+ * repository revisions (REV1 and REV2). For each file encountered in
+ * the delta the editor constructs two temporary files, one for each
+ * revision. This necessitates a separate request for the REV1 version
+ * of the file when the delta shows the file being modified or
+ * deleted. Files that are added by the delta do not require a
+ * separate request, the REV1 version is empty and the delta is
+ * sufficient to construct the REV2 version. When both versions of
+ * each file have been created the diff callback is invoked to display
+ * the difference between the two files. */
+
+#include <apr_uri.h>
+#include <apr_md5.h>
+#include <assert.h>
+
+#include "svn_checksum.h"
+#include "svn_hash.h"
+#include "svn_wc.h"
+#include "svn_pools.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_io.h"
+#include "svn_props.h"
+#include "svn_private_config.h"
+
+#include "client.h"
+
+#include "private/svn_subr_private.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_editor.h"
+
+/* Overall crawler editor baton. */
+struct edit_baton {
+ /* The passed depth */
+ svn_depth_t depth;
+
+ /* The result processor */
+ const svn_diff_tree_processor_t *processor;
+
+ /* RA_SESSION is the open session for making requests to the RA layer */
+ svn_ra_session_t *ra_session;
+
+ /* The rev1 from the '-r Rev1:Rev2' command line option */
+ svn_revnum_t revision;
+
+ /* The rev2 from the '-r Rev1:Rev2' option, specifically set by
+ set_target_revision(). */
+ svn_revnum_t target_revision;
+
+ /* The path to a temporary empty file used for add/delete
+ differences. The path is cached here so that it can be reused,
+ since all empty files are the same. */
+ const char *empty_file;
+
+ /* Empty hash used for adds. */
+ apr_hash_t *empty_hash;
+
+ /* Whether to report text deltas */
+ svn_boolean_t text_deltas;
+
+ /* A callback used to see if the client wishes to cancel the running
+ operation. */
+ svn_cancel_func_t cancel_func;
+
+ /* A baton to pass to the cancellation callback. */
+ void *cancel_baton;
+
+ apr_pool_t *pool;
+};
+
+typedef struct deleted_path_notify_t
+{
+ svn_node_kind_t kind;
+ svn_wc_notify_action_t action;
+ svn_wc_notify_state_t state;
+ svn_boolean_t tree_conflicted;
+} deleted_path_notify_t;
+
+/* Directory level baton.
+ */
+struct dir_baton {
+ /* Gets set if the directory is added rather than replaced/unchanged. */
+ svn_boolean_t added;
+
+ /* Gets set if this operation caused a tree-conflict on this directory
+ * (does not show tree-conflicts persisting from before this operation). */
+ svn_boolean_t tree_conflicted;
+
+ /* If TRUE, this node is skipped entirely.
+ * This is used to skip all children of a tree-conflicted
+ * directory without setting TREE_CONFLICTED to TRUE everywhere. */
+ svn_boolean_t skip;
+
+ /* If TRUE, all children of this directory are skipped. */
+ svn_boolean_t skip_children;
+
+ /* The path of the directory within the repository */
+ const char *path;
+
+ /* The baton for the parent directory, or null if this is the root of the
+ hierarchy to be compared. */
+ struct dir_baton *parent_baton;
+
+ /* The overall crawler editor baton. */
+ struct edit_baton *edit_baton;
+
+ /* A cache of any property changes (svn_prop_t) received for this dir. */
+ apr_array_header_t *propchanges;
+
+ /* Boolean indicating whether a node property was changed */
+ svn_boolean_t has_propchange;
+
+ /* Baton for svn_diff_tree_processor_t */
+ void *pdb;
+ svn_diff_source_t *left_source;
+ svn_diff_source_t *right_source;
+
+ /* The pool passed in by add_dir, open_dir, or open_root.
+ Also, the pool this dir baton is allocated in. */
+ apr_pool_t *pool;
+
+ /* Base revision of directory. */
+ svn_revnum_t base_revision;
+
+ /* Number of users of baton. Its pool will be destroyed 0 */
+ int users;
+};
+
+/* File level baton.
+ */
+struct file_baton {
+ /* Reference to parent baton */
+ struct dir_baton *parent_baton;
+
+ /* Gets set if the file is added rather than replaced. */
+ svn_boolean_t added;
+
+ /* Gets set if this operation caused a tree-conflict on this file
+ * (does not show tree-conflicts persisting from before this operation). */
+ svn_boolean_t tree_conflicted;
+
+ /* If TRUE, this node is skipped entirely.
+ * This is currently used to skip all children of a tree-conflicted
+ * directory. */
+ svn_boolean_t skip;
+
+ /* The path of the file within the repository */
+ const char *path;
+
+ /* The path and APR file handle to the temporary file that contains the
+ first repository version. Also, the pristine-property list of
+ this file. */
+ const char *path_start_revision;
+ apr_hash_t *pristine_props;
+ svn_revnum_t base_revision;
+
+ /* The path and APR file handle to the temporary file that contains the
+ second repository version. These fields are set when processing
+ textdelta and file deletion, and will be NULL if there's no
+ textual difference between the two revisions. */
+ const char *path_end_revision;
+
+ /* APPLY_HANDLER/APPLY_BATON represent the delta application baton. */
+ svn_txdelta_window_handler_t apply_handler;
+ void *apply_baton;
+
+ /* The overall crawler editor baton. */
+ struct edit_baton *edit_baton;
+
+ /* Holds the checksum of the start revision file */
+ svn_checksum_t *start_md5_checksum;
+
+ /* Holds the resulting md5 digest of a textdelta transform */
+ unsigned char result_digest[APR_MD5_DIGESTSIZE];
+ svn_checksum_t *result_md5_checksum;
+
+ /* A cache of any property changes (svn_prop_t) received for this file. */
+ apr_array_header_t *propchanges;
+
+ /* Boolean indicating whether a node property was changed */
+ svn_boolean_t has_propchange;
+
+ /* Baton for svn_diff_tree_processor_t */
+ void *pfb;
+ svn_diff_source_t *left_source;
+ svn_diff_source_t *right_source;
+
+ /* The pool passed in by add_file or open_file.
+ Also, the pool this file_baton is allocated in. */
+ apr_pool_t *pool;
+};
+
+/* Create a new directory baton for PATH in POOL. ADDED is set if
+ * this directory is being added rather than replaced. PARENT_BATON is
+ * the baton of the parent directory (or NULL if this is the root of
+ * the comparison hierarchy). The directory and its parent may or may
+ * not exist in the working copy. EDIT_BATON is the overall crawler
+ * editor baton.
+ */
+static struct dir_baton *
+make_dir_baton(const char *path,
+ struct dir_baton *parent_baton,
+ struct edit_baton *edit_baton,
+ svn_boolean_t added,
+ svn_revnum_t base_revision,
+ apr_pool_t *result_pool)
+{
+ apr_pool_t *dir_pool = svn_pool_create(result_pool);
+ struct dir_baton *dir_baton = apr_pcalloc(dir_pool, sizeof(*dir_baton));
+
+ dir_baton->parent_baton = parent_baton;
+ dir_baton->edit_baton = edit_baton;
+ dir_baton->added = added;
+ dir_baton->tree_conflicted = FALSE;
+ dir_baton->skip = FALSE;
+ dir_baton->skip_children = FALSE;
+ dir_baton->pool = dir_pool;
+ dir_baton->path = apr_pstrdup(dir_pool, path);
+ dir_baton->propchanges = apr_array_make(dir_pool, 8, sizeof(svn_prop_t));
+ dir_baton->base_revision = base_revision;
+ dir_baton->users++;
+
+ if (parent_baton)
+ parent_baton->users++;
+
+ return dir_baton;
+}
+
+/* New function. Called by everyone who has a reference when done */
+static svn_error_t *
+release_dir(struct dir_baton *db)
+{
+ assert(db->users > 0);
+
+ db->users--;
+ if (db->users)
+ return SVN_NO_ERROR;
+
+ {
+ struct dir_baton *pb = db->parent_baton;
+
+ svn_pool_destroy(db->pool);
+
+ if (pb != NULL)
+ SVN_ERR(release_dir(pb));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Create a new file baton for PATH in POOL, which is a child of
+ * directory PARENT_PATH. ADDED is set if this file is being added
+ * rather than replaced. EDIT_BATON is a pointer to the global edit
+ * baton.
+ */
+static struct file_baton *
+make_file_baton(const char *path,
+ struct dir_baton *parent_baton,
+ svn_boolean_t added,
+ apr_pool_t *result_pool)
+{
+ apr_pool_t *file_pool = svn_pool_create(result_pool);
+ struct file_baton *file_baton = apr_pcalloc(file_pool, sizeof(*file_baton));
+
+ file_baton->parent_baton = parent_baton;
+ file_baton->edit_baton = parent_baton->edit_baton;
+ file_baton->added = added;
+ file_baton->tree_conflicted = FALSE;
+ file_baton->skip = FALSE;
+ file_baton->pool = file_pool;
+ file_baton->path = apr_pstrdup(file_pool, path);
+ file_baton->propchanges = apr_array_make(file_pool, 8, sizeof(svn_prop_t));
+ file_baton->base_revision = parent_baton->edit_baton->revision;
+
+ parent_baton->users++;
+
+ return file_baton;
+}
+
+/* Get revision FB->base_revision of the file described by FB from the
+ * repository, through FB->edit_baton->ra_session.
+ *
+ * Unless PROPS_ONLY is true:
+ * Set FB->path_start_revision to the path of a new temporary file containing
+ * the file's text.
+ * Set FB->start_md5_checksum to that file's MD-5 checksum.
+ * Install a pool cleanup handler on FB->pool to delete the file.
+ *
+ * Always:
+ * Set FB->pristine_props to a new hash containing the file's properties.
+ *
+ * Allocate all results in FB->pool.
+ */
+static svn_error_t *
+get_file_from_ra(struct file_baton *fb,
+ svn_boolean_t props_only,
+ apr_pool_t *scratch_pool)
+{
+ if (! props_only)
+ {
+ svn_stream_t *fstream;
+
+ SVN_ERR(svn_stream_open_unique(&fstream, &(fb->path_start_revision),
+ NULL, svn_io_file_del_on_pool_cleanup,
+ fb->pool, scratch_pool));
+
+ fstream = svn_stream_checksummed2(fstream, NULL, &fb->start_md5_checksum,
+ svn_checksum_md5, TRUE, scratch_pool);
+
+ /* Retrieve the file and its properties */
+ SVN_ERR(svn_ra_get_file(fb->edit_baton->ra_session,
+ fb->path,
+ fb->base_revision,
+ fstream, NULL,
+ &(fb->pristine_props),
+ fb->pool));
+ SVN_ERR(svn_stream_close(fstream));
+ }
+ else
+ {
+ SVN_ERR(svn_ra_get_file(fb->edit_baton->ra_session,
+ fb->path,
+ fb->base_revision,
+ NULL, NULL,
+ &(fb->pristine_props),
+ fb->pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Remove every no-op property change from CHANGES: that is, remove every
+ entry in which the target value is the same as the value of the
+ corresponding property in PRISTINE_PROPS.
+
+ Issue #3657 'dav update report handler in skelta mode can cause
+ spurious conflicts'. When communicating with the repository via ra_serf,
+ the change_dir_prop and change_file_prop svn_delta_editor_t
+ callbacks are called (obviously) when a directory or file property has
+ changed between the start and end of the edit. Less obvious however,
+ is that these callbacks may be made describing *all* of the properties
+ on FILE_BATON->PATH when using the DAV providers, not just the change(s).
+ (Specifically ra_serf does it for diff/merge/update/switch).
+
+ This means that the change_[file|dir]_prop svn_delta_editor_t callbacks
+ may be made where there are no property changes (i.e. a noop change of
+ NAME from VALUE to VALUE). Normally this is harmless, but during a
+ merge it can result in spurious conflicts if the WC's pristine property
+ NAME has a value other than VALUE. In an ideal world the mod_dav_svn
+ update report handler, when in 'skelta' mode and describing changes to
+ a path on which a property has changed, wouldn't ask the client to later
+ fetch all properties and figure out what has changed itself. The server
+ already knows which properties have changed!
+
+ Regardless, such a change is not yet implemented, and even when it is,
+ the client should DTRT with regard to older servers which behave this
+ way. Hence this little hack: We populate FILE_BATON->PROPCHANGES only
+ with *actual* property changes.
+
+ See http://subversion.tigris.org/issues/show_bug.cgi?id=3657#desc9 and
+ http://svn.haxx.se/dev/archive-2010-08/0351.shtml for more details.
+ */
+static void
+remove_non_prop_changes(apr_hash_t *pristine_props,
+ apr_array_header_t *changes)
+{
+ int i;
+
+ for (i = 0; i < changes->nelts; i++)
+ {
+ svn_prop_t *change = &APR_ARRAY_IDX(changes, i, svn_prop_t);
+
+ if (change->value)
+ {
+ const svn_string_t *old_val = svn_hash_gets(pristine_props,
+ change->name);
+
+ if (old_val && svn_string_compare(old_val, change->value))
+ {
+ int j;
+
+ /* Remove the matching change by shifting the rest */
+ for (j = i; j < changes->nelts - 1; j++)
+ {
+ APR_ARRAY_IDX(changes, j, svn_prop_t)
+ = APR_ARRAY_IDX(changes, j+1, svn_prop_t);
+ }
+ changes->nelts--;
+ }
+ }
+ }
+}
+
+/* Get the empty file associated with the edit baton. This is cached so
+ * that it can be reused, all empty files are the same.
+ */
+static svn_error_t *
+get_empty_file(struct edit_baton *eb,
+ const char **empty_file_path)
+{
+ /* Create the file if it does not exist */
+ /* Note that we tried to use /dev/null in r857294, but
+ that won't work on Windows: it's impossible to stat NUL */
+ if (!eb->empty_file)
+ SVN_ERR(svn_io_open_unique_file3(NULL, &(eb->empty_file), NULL,
+ svn_io_file_del_on_pool_cleanup,
+ eb->pool, eb->pool));
+
+ *empty_file_path = eb->empty_file;
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_delta_editor_t function. */
+static svn_error_t *
+set_target_revision(void *edit_baton,
+ svn_revnum_t target_revision,
+ apr_pool_t *pool)
+{
+ struct edit_baton *eb = edit_baton;
+
+ eb->target_revision = target_revision;
+ return SVN_NO_ERROR;
+}
+
+/* An svn_delta_editor_t function. The root of the comparison hierarchy */
+static svn_error_t *
+open_root(void *edit_baton,
+ svn_revnum_t base_revision,
+ apr_pool_t *pool,
+ void **root_baton)
+{
+ struct edit_baton *eb = edit_baton;
+ struct dir_baton *db = make_dir_baton("", NULL, eb, FALSE, base_revision,
+ eb->pool);
+
+ db->left_source = svn_diff__source_create(eb->revision, db->pool);
+ db->right_source = svn_diff__source_create(eb->target_revision, db->pool);
+
+ SVN_ERR(eb->processor->dir_opened(&db->pdb,
+ &db->skip,
+ &db->skip_children,
+ "",
+ db->left_source,
+ db->right_source,
+ NULL,
+ NULL,
+ eb->processor,
+ db->pool,
+ db->pool /* scratch_pool */));
+
+ *root_baton = db;
+ return SVN_NO_ERROR;
+}
+
+/* Compare a file being deleted against an empty file.
+ */
+static svn_error_t *
+diff_deleted_file(const char *path,
+ struct dir_baton *db,
+ apr_pool_t *scratch_pool)
+{
+ struct edit_baton *eb = db->edit_baton;
+ struct file_baton *fb = make_file_baton(path, db, FALSE, scratch_pool);
+ svn_boolean_t skip = FALSE;
+ svn_diff_source_t *left_source = svn_diff__source_create(eb->revision,
+ scratch_pool);
+
+ if (eb->cancel_func)
+ SVN_ERR(eb->cancel_func(eb->cancel_baton));
+
+ SVN_ERR(eb->processor->file_opened(&fb->pfb, &skip, path,
+ left_source,
+ NULL /* right_source */,
+ NULL /* copyfrom_source */,
+ db->pdb,
+ eb->processor,
+ scratch_pool, scratch_pool));
+
+ if (eb->cancel_func)
+ SVN_ERR(eb->cancel_func(eb->cancel_baton));
+
+ if (skip)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(get_file_from_ra(fb, ! eb->text_deltas, scratch_pool));
+
+ SVN_ERR(eb->processor->file_deleted(fb->path,
+ left_source,
+ fb->path_start_revision,
+ fb->pristine_props,
+ fb->pfb,
+ eb->processor,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Recursively walk tree rooted at DIR (at EB->revision) in the repository,
+ * reporting all children as deleted. Part of a workaround for issue 2333.
+ *
+ * DIR is a repository path relative to the URL in EB->ra_session. EB is
+ * the overall crawler editor baton. EB->revision must be a valid revision
+ * number, not SVN_INVALID_REVNUM. Use EB->cancel_func (if not null) with
+ * EB->cancel_baton for cancellation.
+ */
+/* ### TODO: Handle depth. */
+static svn_error_t *
+diff_deleted_dir(const char *path,
+ struct dir_baton *pb,
+ apr_pool_t *scratch_pool)
+{
+ struct edit_baton *eb = pb->edit_baton;
+ struct dir_baton *db;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ svn_boolean_t skip = FALSE;
+ svn_boolean_t skip_children = FALSE;
+ apr_hash_t *dirents = NULL;
+ apr_hash_t *left_props = NULL;
+ svn_diff_source_t *left_source = svn_diff__source_create(eb->revision,
+ scratch_pool);
+ db = make_dir_baton(path, pb, pb->edit_baton, FALSE, SVN_INVALID_REVNUM,
+ scratch_pool);
+
+ SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(eb->revision));
+
+ if (eb->cancel_func)
+ SVN_ERR(eb->cancel_func(eb->cancel_baton));
+
+ SVN_ERR(eb->processor->dir_opened(&db->pdb, &skip, &skip_children,
+ path,
+ left_source,
+ NULL /* right_source */,
+ NULL /* copyfrom_source */,
+ pb->pdb,
+ eb->processor,
+ scratch_pool, iterpool));
+
+ if (!skip || !skip_children)
+ SVN_ERR(svn_ra_get_dir2(eb->ra_session,
+ skip_children ? NULL : &dirents,
+ NULL,
+ skip ? NULL : &left_props,
+ path,
+ eb->revision,
+ SVN_DIRENT_KIND,
+ scratch_pool));
+
+ /* The "old" dir will be skipped by the repository report. If required,
+ * crawl it recursively, diffing each file against the empty file. This
+ * is a workaround for issue 2333 "'svn diff URL1 URL2' not reverse of
+ * 'svn diff URL2 URL1'". */
+ if (! skip_children)
+ {
+ apr_hash_index_t *hi;
+
+ for (hi = apr_hash_first(scratch_pool, dirents); hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *child_path;
+ const char *name = svn__apr_hash_index_key(hi);
+ svn_dirent_t *dirent = svn__apr_hash_index_val(hi);
+
+ svn_pool_clear(iterpool);
+
+ child_path = svn_relpath_join(path, name, iterpool);
+
+ if (dirent->kind == svn_node_file)
+ {
+ SVN_ERR(diff_deleted_file(child_path, db, iterpool));
+ }
+ else if (dirent->kind == svn_node_dir)
+ {
+ SVN_ERR(diff_deleted_dir(child_path, db, iterpool));
+ }
+ }
+ }
+
+ if (! skip)
+ {
+ SVN_ERR(eb->processor->dir_deleted(path,
+ left_source,
+ left_props,
+ db->pdb,
+ eb->processor,
+ scratch_pool));
+ }
+
+ SVN_ERR(release_dir(db));
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* An svn_delta_editor_t function. */
+static svn_error_t *
+delete_entry(const char *path,
+ svn_revnum_t base_revision,
+ void *parent_baton,
+ apr_pool_t *pool)
+{
+ struct dir_baton *pb = parent_baton;
+ struct edit_baton *eb = pb->edit_baton;
+ svn_node_kind_t kind;
+ apr_pool_t *scratch_pool;
+
+ /* Process skips. */
+ if (pb->skip_children)
+ return SVN_NO_ERROR;
+
+ scratch_pool = svn_pool_create(eb->pool);
+
+ /* We need to know if this is a directory or a file */
+ SVN_ERR(svn_ra_check_path(eb->ra_session, path, eb->revision, &kind,
+ scratch_pool));
+
+ switch (kind)
+ {
+ case svn_node_file:
+ {
+ SVN_ERR(diff_deleted_file(path, pb, scratch_pool));
+ break;
+ }
+ case svn_node_dir:
+ {
+ SVN_ERR(diff_deleted_dir(path, pb, scratch_pool));
+ break;
+ }
+ default:
+ break;
+ }
+
+ svn_pool_destroy(scratch_pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_delta_editor_t function. */
+static svn_error_t *
+add_directory(const char *path,
+ void *parent_baton,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_revision,
+ apr_pool_t *pool,
+ void **child_baton)
+{
+ struct dir_baton *pb = parent_baton;
+ struct edit_baton *eb = pb->edit_baton;
+ struct dir_baton *db;
+
+ /* ### TODO: support copyfrom? */
+
+ db = make_dir_baton(path, pb, eb, TRUE, SVN_INVALID_REVNUM, pb->pool);
+ *child_baton = db;
+
+ /* Skip *everything* within a newly tree-conflicted directory,
+ * and directories the children of which should be skipped. */
+ if (pb->skip_children)
+ {
+ db->skip = TRUE;
+ db->skip_children = TRUE;
+ return SVN_NO_ERROR;
+ }
+
+ db->right_source = svn_diff__source_create(eb->target_revision,
+ db->pool);
+
+ SVN_ERR(eb->processor->dir_opened(&db->pdb,
+ &db->skip,
+ &db->skip_children,
+ db->path,
+ NULL,
+ db->right_source,
+ NULL /* copyfrom_source */,
+ pb->pdb,
+ eb->processor,
+ db->pool, db->pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_delta_editor_t function. */
+static svn_error_t *
+open_directory(const char *path,
+ void *parent_baton,
+ svn_revnum_t base_revision,
+ apr_pool_t *pool,
+ void **child_baton)
+{
+ struct dir_baton *pb = parent_baton;
+ struct edit_baton *eb = pb->edit_baton;
+ struct dir_baton *db;
+
+ db = make_dir_baton(path, pb, eb, FALSE, base_revision, pb->pool);
+
+ *child_baton = db;
+
+ /* Process Skips. */
+ if (pb->skip_children)
+ {
+ db->skip = TRUE;
+ db->skip_children = TRUE;
+ return SVN_NO_ERROR;
+ }
+
+ db->left_source = svn_diff__source_create(eb->revision, db->pool);
+ db->right_source = svn_diff__source_create(eb->target_revision, db->pool);
+
+ SVN_ERR(eb->processor->dir_opened(&db->pdb,
+ &db->skip, &db->skip_children,
+ path,
+ db->left_source,
+ db->right_source,
+ NULL /* copyfrom */,
+ pb ? pb->pdb : NULL,
+ eb->processor,
+ db->pool, db->pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* An svn_delta_editor_t function. */
+static svn_error_t *
+add_file(const char *path,
+ void *parent_baton,
+ const char *copyfrom_path,
+ svn_revnum_t copyfrom_revision,
+ apr_pool_t *pool,
+ void **file_baton)
+{
+ struct dir_baton *pb = parent_baton;
+ struct edit_baton *eb = pb->edit_baton;
+ struct file_baton *fb;
+
+ /* ### TODO: support copyfrom? */
+
+ fb = make_file_baton(path, pb, TRUE, pb->pool);
+ *file_baton = fb;
+
+ /* Process Skips. */
+ if (pb->skip_children)
+ {
+ fb->skip = TRUE;
+ return SVN_NO_ERROR;
+ }
+
+ fb->pristine_props = pb->edit_baton->empty_hash;
+
+ fb->right_source = svn_diff__source_create(eb->target_revision, fb->pool);
+
+ SVN_ERR(eb->processor->file_opened(&fb->pfb,
+ &fb->skip,
+ path,
+ NULL,
+ fb->right_source,
+ NULL /* copy source */,
+ pb->pdb,
+ eb->processor,
+ fb->pool, fb->pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_delta_editor_t function. */
+static svn_error_t *
+open_file(const char *path,
+ void *parent_baton,
+ svn_revnum_t base_revision,
+ apr_pool_t *pool,
+ void **file_baton)
+{
+ struct dir_baton *pb = parent_baton;
+ struct file_baton *fb;
+ struct edit_baton *eb = pb->edit_baton;
+ fb = make_file_baton(path, pb, FALSE, pb->pool);
+ *file_baton = fb;
+
+ /* Process Skips. */
+ if (pb->skip_children)
+ {
+ fb->skip = TRUE;
+ return SVN_NO_ERROR;
+ }
+
+ fb->base_revision = base_revision;
+
+ fb->left_source = svn_diff__source_create(eb->revision, fb->pool);
+ fb->right_source = svn_diff__source_create(eb->target_revision, fb->pool);
+
+ SVN_ERR(eb->processor->file_opened(&fb->pfb,
+ &fb->skip,
+ path,
+ fb->left_source,
+ fb->right_source,
+ NULL /* copy source */,
+ pb->pdb,
+ eb->processor,
+ fb->pool, fb->pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Do the work of applying the text delta. */
+static svn_error_t *
+window_handler(svn_txdelta_window_t *window,
+ void *window_baton)
+{
+ struct file_baton *fb = window_baton;
+
+ SVN_ERR(fb->apply_handler(window, fb->apply_baton));
+
+ if (!window)
+ {
+ fb->result_md5_checksum = svn_checksum__from_digest_md5(
+ fb->result_digest,
+ fb->pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Implements svn_stream_lazyopen_func_t. */
+static svn_error_t *
+lazy_open_source(svn_stream_t **stream,
+ void *baton,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct file_baton *fb = baton;
+
+ SVN_ERR(svn_stream_open_readonly(stream, fb->path_start_revision,
+ result_pool, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Implements svn_stream_lazyopen_func_t. */
+static svn_error_t *
+lazy_open_result(svn_stream_t **stream,
+ void *baton,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct file_baton *fb = baton;
+
+ SVN_ERR(svn_stream_open_unique(stream, &fb->path_end_revision, NULL,
+ svn_io_file_del_on_pool_cleanup,
+ result_pool, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_delta_editor_t function. */
+static svn_error_t *
+apply_textdelta(void *file_baton,
+ const char *base_md5_digest,
+ apr_pool_t *pool,
+ svn_txdelta_window_handler_t *handler,
+ void **handler_baton)
+{
+ struct file_baton *fb = file_baton;
+ svn_stream_t *src_stream;
+ svn_stream_t *result_stream;
+ apr_pool_t *scratch_pool = fb->pool;
+
+ /* Skip *everything* within a newly tree-conflicted directory. */
+ if (fb->skip)
+ {
+ *handler = svn_delta_noop_window_handler;
+ *handler_baton = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ /* If we're not sending file text, then ignore any that we receive. */
+ if (! fb->edit_baton->text_deltas)
+ {
+ /* Supply valid paths to indicate there is a text change. */
+ SVN_ERR(get_empty_file(fb->edit_baton, &fb->path_start_revision));
+ SVN_ERR(get_empty_file(fb->edit_baton, &fb->path_end_revision));
+
+ *handler = svn_delta_noop_window_handler;
+ *handler_baton = NULL;
+
+ return SVN_NO_ERROR;
+ }
+
+ /* We need the expected pristine file, so go get it */
+ if (!fb->added)
+ SVN_ERR(get_file_from_ra(fb, FALSE, scratch_pool));
+ else
+ SVN_ERR(get_empty_file(fb->edit_baton, &(fb->path_start_revision)));
+
+ SVN_ERR_ASSERT(fb->path_start_revision != NULL);
+
+ if (base_md5_digest != NULL)
+ {
+ svn_checksum_t *base_md5_checksum;
+
+ SVN_ERR(svn_checksum_parse_hex(&base_md5_checksum, svn_checksum_md5,
+ base_md5_digest, scratch_pool));
+
+ if (!svn_checksum_match(base_md5_checksum, fb->start_md5_checksum))
+ return svn_error_trace(svn_checksum_mismatch_err(
+ base_md5_checksum,
+ fb->start_md5_checksum,
+ scratch_pool,
+ _("Base checksum mismatch for '%s'"),
+ fb->path));
+ }
+
+ /* Open the file to be used as the base for second revision */
+ src_stream = svn_stream_lazyopen_create(lazy_open_source, fb, TRUE,
+ scratch_pool);
+
+ /* Open the file that will become the second revision after applying the
+ text delta, it starts empty */
+ result_stream = svn_stream_lazyopen_create(lazy_open_result, fb, TRUE,
+ scratch_pool);
+
+ svn_txdelta_apply(src_stream,
+ result_stream,
+ fb->result_digest,
+ fb->path, fb->pool,
+ &(fb->apply_handler), &(fb->apply_baton));
+
+ *handler = window_handler;
+ *handler_baton = file_baton;
+
+ return SVN_NO_ERROR;
+}
+
+/* An svn_delta_editor_t function. When the file is closed we have a temporary
+ * file containing a pristine version of the repository file. This can
+ * be compared against the working copy.
+ *
+ * ### Ignore TEXT_CHECKSUM for now. Someday we can use it to verify
+ * ### the integrity of the file being diffed. Done efficiently, this
+ * ### would probably involve calculating the checksum as the data is
+ * ### received, storing the final checksum in the file_baton, and
+ * ### comparing against it here.
+ */
+static svn_error_t *
+close_file(void *file_baton,
+ const char *expected_md5_digest,
+ apr_pool_t *pool)
+{
+ struct file_baton *fb = file_baton;
+ struct dir_baton *pb = fb->parent_baton;
+ struct edit_baton *eb = fb->edit_baton;
+ apr_pool_t *scratch_pool;
+
+ /* Skip *everything* within a newly tree-conflicted directory. */
+ if (fb->skip)
+ {
+ svn_pool_destroy(fb->pool);
+ SVN_ERR(release_dir(pb));
+ return SVN_NO_ERROR;
+ }
+
+ scratch_pool = fb->pool;
+
+ if (expected_md5_digest && eb->text_deltas)
+ {
+ svn_checksum_t *expected_md5_checksum;
+
+ SVN_ERR(svn_checksum_parse_hex(&expected_md5_checksum, svn_checksum_md5,
+ expected_md5_digest, scratch_pool));
+
+ if (!svn_checksum_match(expected_md5_checksum, fb->result_md5_checksum))
+ return svn_error_trace(svn_checksum_mismatch_err(
+ expected_md5_checksum,
+ fb->result_md5_checksum,
+ pool,
+ _("Checksum mismatch for '%s'"),
+ fb->path));
+ }
+
+ if (fb->added || fb->path_end_revision || fb->has_propchange)
+ {
+ apr_hash_t *right_props;
+
+ if (!fb->added && !fb->pristine_props)
+ {
+ /* We didn't receive a text change, so we have no pristine props.
+ Retrieve just the props now. */
+ SVN_ERR(get_file_from_ra(fb, TRUE, scratch_pool));
+ }
+
+ if (fb->pristine_props)
+ remove_non_prop_changes(fb->pristine_props, fb->propchanges);
+
+ right_props = svn_prop__patch(fb->pristine_props, fb->propchanges,
+ fb->pool);
+
+ if (fb->added)
+ SVN_ERR(eb->processor->file_added(fb->path,
+ NULL /* copyfrom_src */,
+ fb->right_source,
+ NULL /* copyfrom_file */,
+ fb->path_end_revision,
+ NULL /* copyfrom_props */,
+ right_props,
+ fb->pfb,
+ eb->processor,
+ fb->pool));
+ else
+ SVN_ERR(eb->processor->file_changed(fb->path,
+ fb->left_source,
+ fb->right_source,
+ fb->path_end_revision
+ ? fb->path_start_revision
+ : NULL,
+ fb->path_end_revision,
+ fb->pristine_props,
+ right_props,
+ (fb->path_end_revision != NULL),
+ fb->propchanges,
+ fb->pfb,
+ eb->processor,
+ fb->pool));
+ }
+
+ svn_pool_destroy(fb->pool); /* Destroy file and scratch pool */
+
+ SVN_ERR(release_dir(pb));
+
+ return SVN_NO_ERROR;
+}
+
+/* Report any accumulated prop changes via the 'dir_props_changed' callback,
+ * and then call the 'dir_closed' callback. Notify about any deleted paths
+ * within this directory that have not already been notified, and then about
+ * this directory itself (unless it was added, in which case the notification
+ * was done at that time).
+ *
+ * An svn_delta_editor_t function. */
+static svn_error_t *
+close_directory(void *dir_baton,
+ apr_pool_t *pool)
+{
+ struct dir_baton *db = dir_baton;
+ struct edit_baton *eb = db->edit_baton;
+ apr_pool_t *scratch_pool;
+ apr_hash_t *pristine_props;
+ svn_boolean_t send_changed = FALSE;
+
+ scratch_pool = db->pool;
+
+ if ((db->has_propchange || db->added) && !db->skip)
+ {
+ if (db->added)
+ {
+ pristine_props = eb->empty_hash;
+ }
+ else
+ {
+ SVN_ERR(svn_ra_get_dir2(eb->ra_session, NULL, NULL, &pristine_props,
+ db->path, db->base_revision, 0, scratch_pool));
+ }
+
+ if (db->propchanges->nelts > 0)
+ {
+ remove_non_prop_changes(pristine_props, db->propchanges);
+ }
+
+ if (db->propchanges->nelts > 0 || db->added)
+ {
+ apr_hash_t *right_props;
+
+ right_props = svn_prop__patch(pristine_props, db->propchanges,
+ scratch_pool);
+
+ if (db->added)
+ {
+ SVN_ERR(eb->processor->dir_added(db->path,
+ NULL /* copyfrom */,
+ db->right_source,
+ NULL /* copyfrom props */,
+ right_props,
+ db->pdb,
+ eb->processor,
+ db->pool));
+ }
+ else
+ {
+ SVN_ERR(eb->processor->dir_changed(db->path,
+ db->left_source,
+ db->right_source,
+ pristine_props,
+ right_props,
+ db->propchanges,
+ db->pdb,
+ eb->processor,
+ db->pool));
+ }
+
+ send_changed = TRUE; /* Skip dir_closed */
+ }
+ }
+
+ if (! db->skip && !send_changed)
+ {
+ SVN_ERR(eb->processor->dir_closed(db->path,
+ db->left_source,
+ db->right_source,
+ db->pdb,
+ eb->processor,
+ db->pool));
+ }
+ SVN_ERR(release_dir(db));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Record a prop change, which we will report later in close_file().
+ *
+ * An svn_delta_editor_t function. */
+static svn_error_t *
+change_file_prop(void *file_baton,
+ const char *name,
+ const svn_string_t *value,
+ apr_pool_t *pool)
+{
+ struct file_baton *fb = file_baton;
+ svn_prop_t *propchange;
+ svn_prop_kind_t propkind;
+
+ /* Skip *everything* within a newly tree-conflicted directory. */
+ if (fb->skip)
+ return SVN_NO_ERROR;
+
+ propkind = svn_property_kind2(name);
+ if (propkind == svn_prop_wc_kind)
+ return SVN_NO_ERROR;
+ else if (propkind == svn_prop_regular_kind)
+ fb->has_propchange = TRUE;
+
+ propchange = apr_array_push(fb->propchanges);
+ propchange->name = apr_pstrdup(fb->pool, name);
+ propchange->value = value ? svn_string_dup(value, fb->pool) : NULL;
+
+ return SVN_NO_ERROR;
+}
+
+/* Make a note of this prop change, to be reported when the dir is closed.
+ *
+ * An svn_delta_editor_t function. */
+static svn_error_t *
+change_dir_prop(void *dir_baton,
+ const char *name,
+ const svn_string_t *value,
+ apr_pool_t *pool)
+{
+ struct dir_baton *db = dir_baton;
+ svn_prop_t *propchange;
+ svn_prop_kind_t propkind;
+
+ /* Skip *everything* within a newly tree-conflicted directory. */
+ if (db->skip)
+ return SVN_NO_ERROR;
+
+ propkind = svn_property_kind2(name);
+ if (propkind == svn_prop_wc_kind)
+ return SVN_NO_ERROR;
+ else if (propkind == svn_prop_regular_kind)
+ db->has_propchange = TRUE;
+
+ propchange = apr_array_push(db->propchanges);
+ propchange->name = apr_pstrdup(db->pool, name);
+ propchange->value = value ? svn_string_dup(value, db->pool) : NULL;
+
+ return SVN_NO_ERROR;
+}
+
+
+/* An svn_delta_editor_t function. */
+static svn_error_t *
+close_edit(void *edit_baton,
+ apr_pool_t *pool)
+{
+ struct edit_baton *eb = edit_baton;
+
+ svn_pool_destroy(eb->pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Notify that the node at PATH is 'missing'.
+ * An svn_delta_editor_t function. */
+static svn_error_t *
+absent_directory(const char *path,
+ void *parent_baton,
+ apr_pool_t *pool)
+{
+ struct dir_baton *pb = parent_baton;
+ struct edit_baton *eb = pb->edit_baton;
+
+ SVN_ERR(eb->processor->node_absent(path, pb->pdb, eb->processor, pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Notify that the node at PATH is 'missing'.
+ * An svn_delta_editor_t function. */
+static svn_error_t *
+absent_file(const char *path,
+ void *parent_baton,
+ apr_pool_t *pool)
+{
+ struct dir_baton *pb = parent_baton;
+ struct edit_baton *eb = pb->edit_baton;
+
+ SVN_ERR(eb->processor->node_absent(path, pb->pdb, eb->processor, 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 edit_baton *eb = baton;
+
+ if (!SVN_IS_VALID_REVNUM(base_revision))
+ base_revision = eb->revision;
+
+ SVN_ERR(svn_ra_check_path(eb->ra_session, path, base_revision, kind,
+ scratch_pool));
+
+ 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 edit_baton *eb = baton;
+ svn_node_kind_t node_kind;
+
+ if (!SVN_IS_VALID_REVNUM(base_revision))
+ base_revision = eb->revision;
+
+ SVN_ERR(svn_ra_check_path(eb->ra_session, path, base_revision, &node_kind,
+ scratch_pool));
+
+ if (node_kind == svn_node_file)
+ {
+ SVN_ERR(svn_ra_get_file(eb->ra_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(eb->ra_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_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 edit_baton *eb = baton;
+ svn_stream_t *fstream;
+ svn_error_t *err;
+
+ if (!SVN_IS_VALID_REVNUM(base_revision))
+ base_revision = eb->revision;
+
+ 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(eb->ra_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;
+}
+
+/* Create a repository diff editor and baton. */
+svn_error_t *
+svn_client__get_diff_editor2(const svn_delta_editor_t **editor,
+ void **edit_baton,
+ svn_ra_session_t *ra_session,
+ svn_depth_t depth,
+ svn_revnum_t revision,
+ svn_boolean_t text_deltas,
+ const svn_diff_tree_processor_t *processor,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ apr_pool_t *result_pool)
+{
+ apr_pool_t *editor_pool = svn_pool_create(result_pool);
+ svn_delta_editor_t *tree_editor = svn_delta_default_editor(editor_pool);
+ struct edit_baton *eb = apr_pcalloc(editor_pool, sizeof(*eb));
+ svn_delta_shim_callbacks_t *shim_callbacks =
+ svn_delta_shim_callbacks_default(editor_pool);
+
+ eb->pool = editor_pool;
+ eb->depth = depth;
+
+ eb->processor = processor;
+
+ eb->ra_session = ra_session;
+
+ eb->revision = revision;
+ eb->empty_file = NULL;
+ eb->empty_hash = apr_hash_make(eb->pool);
+ eb->text_deltas = text_deltas;
+ eb->cancel_func = cancel_func;
+ eb->cancel_baton = cancel_baton;
+
+ tree_editor->set_target_revision = set_target_revision;
+ tree_editor->open_root = open_root;
+ tree_editor->delete_entry = delete_entry;
+ tree_editor->add_directory = add_directory;
+ tree_editor->open_directory = open_directory;
+ tree_editor->add_file = add_file;
+ tree_editor->open_file = open_file;
+ tree_editor->apply_textdelta = apply_textdelta;
+ tree_editor->close_file = close_file;
+ tree_editor->close_directory = close_directory;
+ tree_editor->change_file_prop = change_file_prop;
+ tree_editor->change_dir_prop = change_dir_prop;
+ tree_editor->close_edit = close_edit;
+ tree_editor->absent_directory = absent_directory;
+ tree_editor->absent_file = absent_file;
+
+ SVN_ERR(svn_delta_get_cancellation_editor(cancel_func, cancel_baton,
+ tree_editor, eb,
+ editor, edit_baton,
+ eb->pool));
+
+ shim_callbacks->fetch_kind_func = fetch_kind_func;
+ shim_callbacks->fetch_props_func = fetch_props_func;
+ shim_callbacks->fetch_base_func = fetch_base_func;
+ shim_callbacks->fetch_baton = eb;
+
+ SVN_ERR(svn_editor__insert_shims(editor, edit_baton, *editor, *edit_baton,
+ NULL, NULL, shim_callbacks,
+ result_pool, result_pool));
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/resolved.c b/subversion/libsvn_client/resolved.c
new file mode 100644
index 0000000..0496371
--- /dev/null
+++ b/subversion/libsvn_client/resolved.c
@@ -0,0 +1,148 @@
+/*
+ * resolved.c: wrapper around wc resolved functionality.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include <stdlib.h>
+
+#include "svn_types.h"
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_pools.h"
+#include "svn_hash.h"
+#include "svn_sorts.h"
+#include "client.h"
+#include "private/svn_wc_private.h"
+
+#include "svn_private_config.h"
+
+/*** Code. ***/
+
+svn_error_t *
+svn_client__resolve_conflicts(svn_boolean_t *conflicts_remain,
+ apr_hash_t *conflicted_paths,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ apr_array_header_t *array;
+ int i;
+
+ if (conflicts_remain)
+ *conflicts_remain = FALSE;
+
+ SVN_ERR(svn_hash_keys(&array, conflicted_paths, scratch_pool));
+ qsort(array->elts, array->nelts, array->elt_size,
+ svn_sort_compare_paths);
+
+ for (i = 0; i < array->nelts; i++)
+ {
+ const char *local_abspath = APR_ARRAY_IDX(array, i, const char *);
+
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_wc__resolve_conflicts(ctx->wc_ctx, local_abspath,
+ svn_depth_empty,
+ TRUE /* resolve_text */,
+ "" /* resolve_prop (ALL props) */,
+ TRUE /* resolve_tree */,
+ svn_wc_conflict_choose_unspecified,
+ ctx->conflict_func2,
+ ctx->conflict_baton2,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ iterpool));
+
+ if (conflicts_remain)
+ {
+ svn_error_t *err;
+ svn_boolean_t text_c, prop_c, tree_c;
+
+ err = svn_wc_conflicted_p3(&text_c, &prop_c, &tree_c,
+ ctx->wc_ctx, local_abspath,
+ iterpool);
+ if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ text_c = prop_c = tree_c = FALSE;
+ }
+ else
+ {
+ SVN_ERR(err);
+ }
+ if (text_c || prop_c || tree_c)
+ *conflicts_remain = TRUE;
+ }
+ }
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_resolve(const char *path,
+ svn_depth_t depth,
+ svn_wc_conflict_choice_t conflict_choice,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *local_abspath;
+ svn_error_t *err;
+ const char *lock_abspath;
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, pool));
+
+ /* Similar to SVN_WC__CALL_WITH_WRITE_LOCK but using a custom
+ locking function. */
+
+ SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx,
+ local_abspath, pool, pool));
+ err = svn_wc__resolve_conflicts(ctx->wc_ctx, local_abspath,
+ depth,
+ TRUE /* resolve_text */,
+ "" /* resolve_prop (ALL props) */,
+ TRUE /* resolve_tree */,
+ conflict_choice,
+ ctx->conflict_func2,
+ ctx->conflict_baton2,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ pool);
+
+ err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx,
+ lock_abspath,
+ pool));
+ svn_io_sleep_for_timestamps(path, pool);
+
+ return svn_error_trace(err);
+}
diff --git a/subversion/libsvn_client/revert.c b/subversion/libsvn_client/revert.c
new file mode 100644
index 0000000..681e39c
--- /dev/null
+++ b/subversion/libsvn_client/revert.c
@@ -0,0 +1,201 @@
+/*
+ * revert.c: wrapper around wc revert functionality.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_path.h"
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "svn_dirent_uri.h"
+#include "svn_hash.h"
+#include "svn_pools.h"
+#include "svn_error.h"
+#include "svn_time.h"
+#include "svn_config.h"
+#include "client.h"
+#include "private/svn_wc_private.h"
+
+#include "svn_private_config.h"
+
+
+/*** Code. ***/
+
+struct revert_with_write_lock_baton {
+ const char *local_abspath;
+ svn_depth_t depth;
+ svn_boolean_t use_commit_times;
+ const apr_array_header_t *changelists;
+ svn_client_ctx_t *ctx;
+};
+
+/* (Note: All arguments are in the baton above.)
+
+ Attempt to revert LOCAL_ABSPATH.
+
+ If DEPTH is svn_depth_empty, revert just the properties on the
+ directory; else if svn_depth_files, revert the properties and any
+ files immediately under the directory; else if
+ svn_depth_immediates, revert all of the preceding plus properties
+ on immediate subdirectories; else if svn_depth_infinity, revert
+ path and everything under it fully recursively.
+
+ CHANGELISTS is an array of const char * changelist names, used as a
+ restrictive filter on items reverted; that is, don't revert any
+ item unless it's a member of one of those changelists. If
+ CHANGELISTS is empty (or altogether NULL), no changelist filtering occurs.
+
+ Consult CTX to determine whether or not to revert timestamp to the
+ time of last commit ('use-commit-times = yes').
+
+ If PATH is unversioned, return SVN_ERR_UNVERSIONED_RESOURCE. */
+static svn_error_t *
+revert(void *baton, apr_pool_t *result_pool, apr_pool_t *scratch_pool)
+{
+ struct revert_with_write_lock_baton *b = baton;
+ svn_error_t *err;
+
+ err = svn_wc_revert4(b->ctx->wc_ctx,
+ b->local_abspath,
+ b->depth,
+ b->use_commit_times,
+ b->changelists,
+ b->ctx->cancel_func, b->ctx->cancel_baton,
+ b->ctx->notify_func2, b->ctx->notify_baton2,
+ scratch_pool);
+
+ if (err)
+ {
+ /* If target isn't versioned, just send a 'skip'
+ notification and move on. */
+ if (err->apr_err == SVN_ERR_ENTRY_NOT_FOUND
+ || err->apr_err == SVN_ERR_UNVERSIONED_RESOURCE
+ || err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ if (b->ctx->notify_func2)
+ (*b->ctx->notify_func2)(
+ b->ctx->notify_baton2,
+ svn_wc_create_notify(b->local_abspath, svn_wc_notify_skip,
+ scratch_pool),
+ scratch_pool);
+ svn_error_clear(err);
+ }
+ else
+ return svn_error_trace(err);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_client_revert2(const apr_array_header_t *paths,
+ svn_depth_t depth,
+ const apr_array_header_t *changelists,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ apr_pool_t *subpool;
+ svn_error_t *err = SVN_NO_ERROR;
+ int i;
+ svn_config_t *cfg;
+ svn_boolean_t use_commit_times;
+ struct revert_with_write_lock_baton baton;
+
+ /* Don't even attempt to modify the working copy if any of the
+ * targets look like URLs. URLs are invalid input. */
+ for (i = 0; i < paths->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(paths, i, const char *);
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+ }
+
+ cfg = ctx->config
+ ? svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG)
+ : NULL;
+
+ SVN_ERR(svn_config_get_bool(cfg, &use_commit_times,
+ SVN_CONFIG_SECTION_MISCELLANY,
+ SVN_CONFIG_OPTION_USE_COMMIT_TIMES,
+ FALSE));
+
+ subpool = svn_pool_create(pool);
+
+ for (i = 0; i < paths->nelts; i++)
+ {
+ const char *path = APR_ARRAY_IDX(paths, i, const char *);
+ const char *local_abspath, *lock_target;
+ svn_boolean_t wc_root;
+
+ svn_pool_clear(subpool);
+
+ /* See if we've been asked to cancel this operation. */
+ if ((ctx->cancel_func)
+ && ((err = ctx->cancel_func(ctx->cancel_baton))))
+ goto errorful;
+
+ err = svn_dirent_get_absolute(&local_abspath, path, pool);
+ if (err)
+ goto errorful;
+
+ baton.local_abspath = local_abspath;
+ baton.depth = depth;
+ baton.use_commit_times = use_commit_times;
+ baton.changelists = changelists;
+ baton.ctx = ctx;
+
+ err = svn_wc__is_wcroot(&wc_root, ctx->wc_ctx, local_abspath, pool);
+ if (err)
+ goto errorful;
+ lock_target = wc_root ? local_abspath
+ : svn_dirent_dirname(local_abspath, pool);
+ err = svn_wc__call_with_write_lock(revert, &baton, ctx->wc_ctx,
+ lock_target, FALSE, pool, pool);
+ if (err)
+ goto errorful;
+ }
+
+ errorful:
+
+ {
+ /* Sleep to ensure timestamp integrity. */
+ const char *sleep_path = NULL;
+
+ /* Only specify a path if we are certain all paths are on the
+ same filesystem */
+ if (paths->nelts == 1)
+ sleep_path = APR_ARRAY_IDX(paths, 0, const char *);
+
+ svn_io_sleep_for_timestamps(sleep_path, subpool);
+ }
+
+ svn_pool_destroy(subpool);
+
+ return svn_error_trace(err);
+}
diff --git a/subversion/libsvn_client/revisions.c b/subversion/libsvn_client/revisions.c
new file mode 100644
index 0000000..ec255c1
--- /dev/null
+++ b/subversion/libsvn_client/revisions.c
@@ -0,0 +1,191 @@
+/*
+ * revisions.c: discovering revisions
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+
+
+#include <apr_pools.h>
+
+#include "svn_error.h"
+#include "svn_ra.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "client.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+
+
+
+
+svn_error_t *
+svn_client__get_revision_number(svn_revnum_t *revnum,
+ svn_revnum_t *youngest_rev,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ svn_ra_session_t *ra_session,
+ const svn_opt_revision_t *revision,
+ apr_pool_t *scratch_pool)
+{
+ switch (revision->kind)
+ {
+ case svn_opt_revision_unspecified:
+ *revnum = SVN_INVALID_REVNUM;
+ break;
+
+ case svn_opt_revision_number:
+ *revnum = revision->value.number;
+ break;
+
+ case svn_opt_revision_head:
+ /* If our caller provided a value for HEAD that he wants us to
+ use, we'll use it. Otherwise, we have to query the
+ repository (and possible return our fetched value in
+ *YOUNGEST_REV, too). */
+ if (youngest_rev && SVN_IS_VALID_REVNUM(*youngest_rev))
+ {
+ *revnum = *youngest_rev;
+ }
+ else
+ {
+ if (! ra_session)
+ return svn_error_create(SVN_ERR_CLIENT_RA_ACCESS_REQUIRED,
+ NULL, NULL);
+ SVN_ERR(svn_ra_get_latest_revnum(ra_session, revnum, scratch_pool));
+ if (youngest_rev)
+ *youngest_rev = *revnum;
+ }
+ break;
+
+ case svn_opt_revision_working:
+ case svn_opt_revision_base:
+ {
+ svn_error_t *err;
+
+ /* Sanity check. */
+ if (local_abspath == NULL)
+ return svn_error_create(SVN_ERR_CLIENT_VERSIONED_PATH_REQUIRED,
+ NULL, NULL);
+
+ /* The BASE, COMMITTED, and PREV revision keywords do not
+ apply to URLs. */
+ if (svn_path_is_url(local_abspath))
+ goto invalid_rev_arg;
+
+ err = svn_wc__node_get_origin(NULL, revnum, NULL, NULL, NULL, NULL,
+ wc_ctx, local_abspath, TRUE,
+ scratch_pool, scratch_pool);
+
+ /* Return the same error as older code did (before and at r935091).
+ At least svn_client_proplist4 promises SVN_ERR_ENTRY_NOT_FOUND. */
+ if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ return svn_error_createf(SVN_ERR_ENTRY_NOT_FOUND, NULL,
+ _("'%s' is not under version control"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+ }
+ else
+ SVN_ERR(err);
+
+ if (! SVN_IS_VALID_REVNUM(*revnum))
+ return svn_error_createf(SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Path '%s' has no committed "
+ "revision"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+ }
+ break;
+
+ case svn_opt_revision_committed:
+ case svn_opt_revision_previous:
+ {
+ /* Sanity check. */
+ if (local_abspath == NULL)
+ return svn_error_create(SVN_ERR_CLIENT_VERSIONED_PATH_REQUIRED,
+ NULL, NULL);
+
+ /* The BASE, COMMITTED, and PREV revision keywords do not
+ apply to URLs. */
+ if (svn_path_is_url(local_abspath))
+ goto invalid_rev_arg;
+
+ SVN_ERR(svn_wc__node_get_changed_info(revnum, NULL, NULL,
+ wc_ctx, local_abspath,
+ scratch_pool, scratch_pool));
+ if (! SVN_IS_VALID_REVNUM(*revnum))
+ return svn_error_createf(SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Path '%s' has no committed "
+ "revision"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+
+ if (revision->kind == svn_opt_revision_previous)
+ (*revnum)--;
+ }
+ break;
+
+ case svn_opt_revision_date:
+ /* ### When revision->kind == svn_opt_revision_date, is there an
+ ### optimization such that we can compare
+ ### revision->value->date with the committed-date in the
+ ### entries file (or rather, with some range of which
+ ### committed-date is one endpoint), and sometimes avoid a
+ ### trip over the RA layer? The only optimizations I can
+ ### think of involve examining other entries to build a
+ ### timespan across which committed-revision is known to be
+ ### the head, but it doesn't seem worth it. -kff */
+ if (! ra_session)
+ return svn_error_create(SVN_ERR_CLIENT_RA_ACCESS_REQUIRED, NULL, NULL);
+ SVN_ERR(svn_ra_get_dated_revision(ra_session, revnum,
+ revision->value.date, scratch_pool));
+ break;
+
+ default:
+ return svn_error_createf(SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("Unrecognized revision type requested for "
+ "'%s'"),
+ svn_dirent_local_style(local_abspath,
+ scratch_pool));
+ }
+
+ /* Final check -- if our caller provided a youngest revision, and
+ the number we wound up with (after talking to the server) is younger
+ than that revision, we need to stick to our caller's idea of "youngest".
+ */
+ if (youngest_rev
+ && (revision->kind == svn_opt_revision_head
+ || revision->kind == svn_opt_revision_date)
+ && SVN_IS_VALID_REVNUM(*youngest_rev)
+ && SVN_IS_VALID_REVNUM(*revnum)
+ && (*revnum > *youngest_rev))
+ *revnum = *youngest_rev;
+
+ return SVN_NO_ERROR;
+
+ invalid_rev_arg:
+ return svn_error_create(
+ SVN_ERR_CLIENT_BAD_REVISION, NULL,
+ _("PREV, BASE, or COMMITTED revision keywords are invalid for URL"));
+
+}
diff --git a/subversion/libsvn_client/status.c b/subversion/libsvn_client/status.c
new file mode 100644
index 0000000..e581d37
--- /dev/null
+++ b/subversion/libsvn_client/status.c
@@ -0,0 +1,767 @@
+/*
+ * status.c: return the status of a working copy dirent
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+#include <apr_strings.h>
+#include <apr_pools.h>
+
+#include "svn_pools.h"
+#include "client.h"
+
+#include "svn_path.h"
+#include "svn_dirent_uri.h"
+#include "svn_delta.h"
+#include "svn_client.h"
+#include "svn_error.h"
+#include "svn_hash.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_client_private.h"
+
+
+/*** Getting update information ***/
+
+/* Baton for tweak_status. It wraps a bit of extra functionality
+ around the received status func/baton, so we can remember if the
+ target was deleted in HEAD and tweak incoming status structures
+ accordingly. */
+struct status_baton
+{
+ svn_boolean_t deleted_in_repos; /* target is deleted in repos */
+ apr_hash_t *changelist_hash; /* keys are changelist names */
+ svn_client_status_func_t real_status_func; /* real status function */
+ void *real_status_baton; /* real status baton */
+ const char *anchor_abspath; /* Absolute path of anchor */
+ const char *anchor_relpath; /* Relative path of anchor */
+ svn_wc_context_t *wc_ctx; /* A working copy context. */
+};
+
+/* A status callback function which wraps the *real* status
+ function/baton. This sucker takes care of any status tweaks we
+ need to make (such as noting that the target of the status is
+ missing from HEAD in the repository).
+
+ This implements the 'svn_wc_status_func4_t' function type. */
+static svn_error_t *
+tweak_status(void *baton,
+ const char *local_abspath,
+ const svn_wc_status3_t *status,
+ apr_pool_t *scratch_pool)
+{
+ struct status_baton *sb = baton;
+ const char *path = local_abspath;
+ svn_client_status_t *cst;
+
+ if (sb->anchor_abspath)
+ path = svn_dirent_join(sb->anchor_relpath,
+ svn_dirent_skip_ancestor(sb->anchor_abspath, path),
+ scratch_pool);
+
+ /* If the status item has an entry, but doesn't belong to one of the
+ changelists our caller is interested in, we filter out this status
+ transmission. */
+ if (sb->changelist_hash
+ && (! status->changelist
+ || ! svn_hash_gets(sb->changelist_hash, status->changelist)))
+ {
+ return SVN_NO_ERROR;
+ }
+
+ /* If we know that the target was deleted in HEAD of the repository,
+ we need to note that fact in all the status structures that come
+ through here. */
+ if (sb->deleted_in_repos)
+ {
+ svn_wc_status3_t *new_status = svn_wc_dup_status3(status, scratch_pool);
+ new_status->repos_node_status = svn_wc_status_deleted;
+ status = new_status;
+ }
+
+ SVN_ERR(svn_client__create_status(&cst, sb->wc_ctx, local_abspath, status,
+ scratch_pool, scratch_pool));
+
+ /* Call the real status function/baton. */
+ return sb->real_status_func(sb->real_status_baton, path, cst,
+ scratch_pool);
+}
+
+/* A baton for our reporter that is used to collect locks. */
+typedef struct report_baton_t {
+ const svn_ra_reporter3_t* wrapped_reporter;
+ void *wrapped_report_baton;
+ /* The common ancestor URL of all paths included in the report. */
+ char *ancestor;
+ void *set_locks_baton;
+ svn_depth_t depth;
+ svn_client_ctx_t *ctx;
+ /* Pool to store locks in. */
+ apr_pool_t *pool;
+} report_baton_t;
+
+/* Implements svn_ra_reporter3_t->set_path. */
+static svn_error_t *
+reporter_set_path(void *report_baton, const char *path,
+ svn_revnum_t revision, svn_depth_t depth,
+ svn_boolean_t start_empty, const char *lock_token,
+ apr_pool_t *pool)
+{
+ report_baton_t *rb = report_baton;
+
+ return rb->wrapped_reporter->set_path(rb->wrapped_report_baton, path,
+ revision, depth, start_empty,
+ lock_token, pool);
+}
+
+/* Implements svn_ra_reporter3_t->delete_path. */
+static svn_error_t *
+reporter_delete_path(void *report_baton, const char *path, apr_pool_t *pool)
+{
+ report_baton_t *rb = report_baton;
+
+ return rb->wrapped_reporter->delete_path(rb->wrapped_report_baton, path,
+ pool);
+}
+
+/* Implements svn_ra_reporter3_t->link_path. */
+static svn_error_t *
+reporter_link_path(void *report_baton, const char *path, const char *url,
+ svn_revnum_t revision, svn_depth_t depth,
+ svn_boolean_t start_empty,
+ const char *lock_token, apr_pool_t *pool)
+{
+ report_baton_t *rb = report_baton;
+
+ if (!svn_uri__is_ancestor(rb->ancestor, url))
+ {
+ const char *ancestor;
+
+ ancestor = svn_uri_get_longest_ancestor(url, rb->ancestor, pool);
+
+ /* If we got a shorter ancestor, truncate our current ancestor.
+ Note that svn_uri_get_longest_ancestor will allocate its return
+ value even if it identical to one of its arguments. */
+
+ rb->ancestor[strlen(ancestor)] = '\0';
+ rb->depth = svn_depth_infinity;
+ }
+
+ return rb->wrapped_reporter->link_path(rb->wrapped_report_baton, path, url,
+ revision, depth, start_empty,
+ lock_token, pool);
+}
+
+/* Implements svn_ra_reporter3_t->finish_report. */
+static svn_error_t *
+reporter_finish_report(void *report_baton, apr_pool_t *pool)
+{
+ report_baton_t *rb = report_baton;
+ svn_ra_session_t *ras;
+ apr_hash_t *locks;
+ const char *repos_root;
+ apr_pool_t *subpool = svn_pool_create(pool);
+ svn_error_t *err = SVN_NO_ERROR;
+
+ /* Open an RA session to our common ancestor and grab the locks under it.
+ */
+ SVN_ERR(svn_client_open_ra_session2(&ras, rb->ancestor, NULL,
+ rb->ctx, subpool, subpool));
+
+ /* The locks need to live throughout the edit. Note that if the
+ server doesn't support lock discovery, we'll just not do locky
+ stuff. */
+ err = svn_ra_get_locks2(ras, &locks, "", rb->depth, rb->pool);
+ if (err && ((err->apr_err == SVN_ERR_RA_NOT_IMPLEMENTED)
+ || (err->apr_err == SVN_ERR_UNSUPPORTED_FEATURE)))
+ {
+ svn_error_clear(err);
+ err = SVN_NO_ERROR;
+ locks = apr_hash_make(rb->pool);
+ }
+ SVN_ERR(err);
+
+ SVN_ERR(svn_ra_get_repos_root2(ras, &repos_root, rb->pool));
+
+ /* Close the RA session. */
+ svn_pool_destroy(subpool);
+
+ SVN_ERR(svn_wc_status_set_repos_locks(rb->set_locks_baton, locks,
+ repos_root, rb->pool));
+
+ return rb->wrapped_reporter->finish_report(rb->wrapped_report_baton, pool);
+}
+
+/* Implements svn_ra_reporter3_t->abort_report. */
+static svn_error_t *
+reporter_abort_report(void *report_baton, apr_pool_t *pool)
+{
+ report_baton_t *rb = report_baton;
+
+ return rb->wrapped_reporter->abort_report(rb->wrapped_report_baton, pool);
+}
+
+/* A reporter that keeps track of the common URL ancestor of all paths in
+ the WC and fetches repository locks for all paths under this ancestor. */
+static svn_ra_reporter3_t lock_fetch_reporter = {
+ reporter_set_path,
+ reporter_delete_path,
+ reporter_link_path,
+ reporter_finish_report,
+ reporter_abort_report
+};
+
+/* Perform status operations on each external in EXTERNAL_MAP, a const char *
+ local_abspath of all externals mapping to the const char* defining_abspath.
+ All other options are the same as those passed to svn_client_status().
+
+ If ANCHOR_ABSPATH and ANCHOR-RELPATH are not null, use them to provide
+ properly formatted relative paths */
+static svn_error_t *
+do_external_status(svn_client_ctx_t *ctx,
+ apr_hash_t *external_map,
+ svn_depth_t depth,
+ svn_boolean_t get_all,
+ svn_boolean_t update,
+ svn_boolean_t no_ignore,
+ const char *anchor_abspath,
+ const char *anchor_relpath,
+ svn_client_status_func_t status_func,
+ void *status_baton,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+ /* Loop over the hash of new values (we don't care about the old
+ ones). This is a mapping of versioned directories to property
+ values. */
+ for (hi = apr_hash_first(scratch_pool, external_map);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ svn_node_kind_t external_kind;
+ const char *local_abspath = svn__apr_hash_index_key(hi);
+ const char *defining_abspath = svn__apr_hash_index_val(hi);
+ svn_node_kind_t kind;
+ svn_opt_revision_t opt_rev;
+ const char *status_path;
+
+ svn_pool_clear(iterpool);
+
+ /* Obtain information on the expected external. */
+ SVN_ERR(svn_wc__read_external_info(&external_kind, NULL, NULL, NULL,
+ &opt_rev.value.number,
+ ctx->wc_ctx, defining_abspath,
+ local_abspath, FALSE,
+ iterpool, iterpool));
+
+ if (external_kind != svn_node_dir)
+ continue;
+
+ SVN_ERR(svn_io_check_path(local_abspath, &kind, iterpool));
+ if (kind != svn_node_dir)
+ continue;
+
+ if (SVN_IS_VALID_REVNUM(opt_rev.value.number))
+ opt_rev.kind = svn_opt_revision_number;
+ else
+ opt_rev.kind = svn_opt_revision_unspecified;
+
+ /* Tell the client we're starting an external status set. */
+ if (ctx->notify_func2)
+ ctx->notify_func2(
+ ctx->notify_baton2,
+ svn_wc_create_notify(local_abspath,
+ svn_wc_notify_status_external,
+ iterpool), iterpool);
+
+ status_path = local_abspath;
+ if (anchor_abspath)
+ {
+ status_path = svn_dirent_join(anchor_relpath,
+ svn_dirent_skip_ancestor(anchor_abspath,
+ status_path),
+ iterpool);
+ }
+
+ /* And then do the status. */
+ SVN_ERR(svn_client_status5(NULL, ctx, status_path, &opt_rev, depth,
+ get_all, update, no_ignore, FALSE, FALSE,
+ NULL, status_func, status_baton,
+ iterpool));
+ }
+
+ /* Destroy SUBPOOL and (implicitly) ITERPOOL. */
+ svn_pool_destroy(iterpool);
+
+ return SVN_NO_ERROR;
+}
+
+/*** Public Interface. ***/
+
+
+svn_error_t *
+svn_client_status5(svn_revnum_t *result_rev,
+ svn_client_ctx_t *ctx,
+ const char *path,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t get_all,
+ svn_boolean_t update,
+ svn_boolean_t no_ignore,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t depth_as_sticky,
+ const apr_array_header_t *changelists,
+ svn_client_status_func_t status_func,
+ void *status_baton,
+ apr_pool_t *pool) /* ### aka scratch_pool */
+{
+ struct status_baton sb;
+ const char *dir, *dir_abspath;
+ const char *target_abspath;
+ const char *target_basename;
+ apr_array_header_t *ignores;
+ svn_error_t *err;
+ apr_hash_t *changelist_hash = NULL;
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+
+ if (changelists && changelists->nelts)
+ SVN_ERR(svn_hash_from_cstring_keys(&changelist_hash, changelists, pool));
+
+ if (result_rev)
+ *result_rev = SVN_INVALID_REVNUM;
+
+ sb.real_status_func = status_func;
+ sb.real_status_baton = status_baton;
+ sb.deleted_in_repos = FALSE;
+ sb.changelist_hash = changelist_hash;
+ sb.wc_ctx = ctx->wc_ctx;
+
+ SVN_ERR(svn_dirent_get_absolute(&target_abspath, path, pool));
+
+ if (update)
+ {
+ /* The status editor only works on directories, so get the ancestor
+ if necessary */
+
+ svn_node_kind_t kind;
+
+ SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, target_abspath,
+ TRUE, FALSE, pool));
+
+ /* Dir must be a working copy directory or the status editor fails */
+ if (kind == svn_node_dir)
+ {
+ dir_abspath = target_abspath;
+ target_basename = "";
+ dir = path;
+ }
+ else
+ {
+ dir_abspath = svn_dirent_dirname(target_abspath, pool);
+ target_basename = svn_dirent_basename(target_abspath, NULL);
+ dir = svn_dirent_dirname(path, pool);
+
+ if (kind == svn_node_file)
+ {
+ if (depth == svn_depth_empty)
+ depth = svn_depth_files;
+ }
+ else
+ {
+ err = svn_wc_read_kind2(&kind, ctx->wc_ctx, dir_abspath,
+ FALSE, FALSE, pool);
+
+ svn_error_clear(err);
+
+ if (err || kind != svn_node_dir)
+ {
+ return svn_error_createf(SVN_ERR_WC_NOT_WORKING_COPY, NULL,
+ _("'%s' is not a working copy"),
+ svn_dirent_local_style(path, pool));
+ }
+ }
+ }
+ }
+ else
+ {
+ dir = path;
+ dir_abspath = target_abspath;
+ }
+
+ if (svn_dirent_is_absolute(dir))
+ {
+ sb.anchor_abspath = NULL;
+ sb.anchor_relpath = NULL;
+ }
+ else
+ {
+ sb.anchor_abspath = dir_abspath;
+ sb.anchor_relpath = dir;
+ }
+
+ /* Get the status edit, and use our wrapping status function/baton
+ as the callback pair. */
+ SVN_ERR(svn_wc_get_default_ignores(&ignores, ctx->config, pool));
+
+ /* If we want to know about out-of-dateness, we crawl the working copy and
+ let the RA layer drive the editor for real. Otherwise, we just close the
+ edit. :-) */
+ if (update)
+ {
+ svn_ra_session_t *ra_session;
+ const char *URL;
+ svn_node_kind_t kind;
+ svn_boolean_t server_supports_depth;
+ const svn_delta_editor_t *editor;
+ void *edit_baton, *set_locks_baton;
+ svn_revnum_t edit_revision = SVN_INVALID_REVNUM;
+
+ /* Get full URL from the ANCHOR. */
+ SVN_ERR(svn_client_url_from_path2(&URL, dir_abspath, ctx,
+ pool, pool));
+
+ if (!URL)
+ return svn_error_createf
+ (SVN_ERR_ENTRY_MISSING_URL, NULL,
+ _("Entry '%s' has no URL"),
+ svn_dirent_local_style(dir, pool));
+
+ /* Open a repository session to the URL. */
+ SVN_ERR(svn_client__open_ra_session_internal(&ra_session, NULL, URL,
+ dir_abspath, NULL,
+ FALSE, TRUE,
+ ctx, pool, pool));
+
+ SVN_ERR(svn_ra_has_capability(ra_session, &server_supports_depth,
+ SVN_RA_CAPABILITY_DEPTH, pool));
+
+ SVN_ERR(svn_wc__get_status_editor(&editor, &edit_baton, &set_locks_baton,
+ &edit_revision, ctx->wc_ctx,
+ dir_abspath, target_basename,
+ depth, get_all,
+ no_ignore, depth_as_sticky,
+ server_supports_depth,
+ ignores, tweak_status, &sb,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool, pool));
+
+
+ /* Verify that URL exists in HEAD. If it doesn't, this can save
+ us a whole lot of hassle; if it does, the cost of this
+ request should be minimal compared to the size of getting
+ back the average amount of "out-of-date" information. */
+ SVN_ERR(svn_ra_check_path(ra_session, "", SVN_INVALID_REVNUM,
+ &kind, pool));
+ if (kind == svn_node_none)
+ {
+ svn_boolean_t added;
+
+ /* Our status target does not exist in HEAD. If we've got
+ it locally added, that's okay. But if it was previously
+ versioned, then it must have since been deleted from the
+ repository. (Note that "locally replaced" doesn't count
+ as "added" in this case.) */
+ SVN_ERR(svn_wc__node_is_added(&added, ctx->wc_ctx,
+ dir_abspath, pool));
+ if (! added)
+ sb.deleted_in_repos = TRUE;
+
+ /* And now close the edit. */
+ SVN_ERR(editor->close_edit(edit_baton, pool));
+ }
+ else
+ {
+ svn_revnum_t revnum;
+ report_baton_t rb;
+ svn_depth_t status_depth;
+
+ if (revision->kind == svn_opt_revision_head)
+ {
+ /* Cause the revision number to be omitted from the request,
+ which implies HEAD. */
+ revnum = SVN_INVALID_REVNUM;
+ }
+ else
+ {
+ /* Get a revision number for our status operation. */
+ SVN_ERR(svn_client__get_revision_number(&revnum, NULL,
+ ctx->wc_ctx,
+ target_abspath,
+ ra_session, revision,
+ pool));
+ }
+
+ if (depth_as_sticky || !server_supports_depth)
+ status_depth = depth;
+ else
+ status_depth = svn_depth_unknown; /* Use depth from WC */
+
+ /* Do the deed. Let the RA layer drive the status editor. */
+ SVN_ERR(svn_ra_do_status2(ra_session, &rb.wrapped_reporter,
+ &rb.wrapped_report_baton,
+ target_basename, revnum, status_depth,
+ editor, edit_baton, pool));
+
+ /* Init the report baton. */
+ rb.ancestor = apr_pstrdup(pool, URL); /* Edited later */
+ rb.set_locks_baton = set_locks_baton;
+ rb.ctx = ctx;
+ rb.pool = pool;
+
+ if (depth == svn_depth_unknown)
+ rb.depth = svn_depth_infinity;
+ else
+ rb.depth = depth;
+
+ /* Drive the reporter structure, describing the revisions
+ within PATH. When we call reporter->finish_report,
+ EDITOR will be driven to describe differences between our
+ working copy and HEAD. */
+ SVN_ERR(svn_wc_crawl_revisions5(ctx->wc_ctx,
+ target_abspath,
+ &lock_fetch_reporter, &rb,
+ FALSE /* restore_files */,
+ depth, (! depth_as_sticky),
+ (! server_supports_depth),
+ FALSE /* use_commit_times */,
+ ctx->cancel_func, ctx->cancel_baton,
+ NULL, NULL, pool));
+ }
+
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify(target_abspath,
+ svn_wc_notify_status_completed, pool);
+ notify->revision = edit_revision;
+ (ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+ }
+
+ /* If the caller wants the result revision, give it to them. */
+ if (result_rev)
+ *result_rev = edit_revision;
+ }
+ else
+ {
+ err = svn_wc_walk_status(ctx->wc_ctx, target_abspath,
+ depth, get_all, no_ignore, FALSE, ignores,
+ tweak_status, &sb,
+ ctx->cancel_func, ctx->cancel_baton,
+ pool);
+
+ if (err && err->apr_err == SVN_ERR_WC_MISSING)
+ {
+ /* This error code is checked for in svn to continue after
+ this error */
+ svn_error_clear(err);
+ return svn_error_createf(SVN_ERR_WC_NOT_WORKING_COPY, NULL,
+ _("'%s' is not a working copy"),
+ svn_dirent_local_style(path, pool));
+ }
+
+ SVN_ERR(err);
+ }
+
+ /* If there are svn:externals set, we don't want those to show up as
+ unversioned or unrecognized, so patch up the hash. If caller wants
+ all the statuses, we will change unversioned status items that
+ are interesting to an svn:externals property to
+ svn_wc_status_unversioned, otherwise we'll just remove the status
+ item altogether.
+
+ We only descend into an external if depth is svn_depth_infinity or
+ svn_depth_unknown. However, there are conceivable behaviors that
+ would involve descending under other circumstances; thus, we pass
+ depth anyway, so the code will DTRT if we change the conditional
+ in the future.
+ */
+ if (SVN_DEPTH_IS_RECURSIVE(depth) && (! ignore_externals))
+ {
+ apr_hash_t *external_map;
+ SVN_ERR(svn_wc__externals_defined_below(&external_map,
+ ctx->wc_ctx, target_abspath,
+ pool, pool));
+
+
+ SVN_ERR(do_external_status(ctx, external_map,
+ depth, get_all,
+ update, no_ignore,
+ sb.anchor_abspath, sb.anchor_relpath,
+ status_func, status_baton, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_client_status_t *
+svn_client_status_dup(const svn_client_status_t *status,
+ apr_pool_t *result_pool)
+{
+ svn_client_status_t *st = apr_palloc(result_pool, sizeof(*st));
+
+ *st = *status;
+
+ if (status->local_abspath)
+ st->local_abspath = apr_pstrdup(result_pool, status->local_abspath);
+
+ if (status->repos_root_url)
+ st->repos_root_url = apr_pstrdup(result_pool, status->repos_root_url);
+
+ if (status->repos_uuid)
+ st->repos_uuid = apr_pstrdup(result_pool, status->repos_uuid);
+
+ if (status->repos_relpath)
+ st->repos_relpath = apr_pstrdup(result_pool, status->repos_relpath);
+
+ if (status->changed_author)
+ st->changed_author = apr_pstrdup(result_pool, status->changed_author);
+
+ if (status->lock)
+ st->lock = svn_lock_dup(status->lock, result_pool);
+
+ if (status->changelist)
+ st->changelist = apr_pstrdup(result_pool, status->changelist);
+
+ if (status->ood_changed_author)
+ st->ood_changed_author = apr_pstrdup(result_pool, status->ood_changed_author);
+
+ if (status->repos_lock)
+ st->repos_lock = svn_lock_dup(status->repos_lock, result_pool);
+
+ if (status->backwards_compatibility_baton)
+ {
+ const svn_wc_status3_t *wc_st = status->backwards_compatibility_baton;
+
+ st->backwards_compatibility_baton = svn_wc_dup_status3(wc_st,
+ result_pool);
+ }
+
+ if (status->moved_from_abspath)
+ st->moved_from_abspath =
+ apr_pstrdup(result_pool, status->moved_from_abspath);
+
+ if (status->moved_to_abspath)
+ st->moved_to_abspath = apr_pstrdup(result_pool, status->moved_to_abspath);
+
+ return st;
+}
+
+svn_error_t *
+svn_client__create_status(svn_client_status_t **cst,
+ svn_wc_context_t *wc_ctx,
+ const char *local_abspath,
+ const svn_wc_status3_t *status,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ *cst = apr_pcalloc(result_pool, sizeof(**cst));
+
+ (*cst)->kind = status->kind;
+ (*cst)->local_abspath = local_abspath;
+ (*cst)->filesize = status->filesize;
+ (*cst)->versioned = status->versioned;
+
+ (*cst)->conflicted = status->conflicted;
+
+ (*cst)->node_status = status->node_status;
+ (*cst)->text_status = status->text_status;
+ (*cst)->prop_status = status->prop_status;
+
+ if (status->kind == svn_node_dir)
+ (*cst)->wc_is_locked = status->locked;
+
+ (*cst)->copied = status->copied;
+ (*cst)->revision = status->revision;
+
+ (*cst)->changed_rev = status->changed_rev;
+ (*cst)->changed_date = status->changed_date;
+ (*cst)->changed_author = status->changed_author;
+
+ (*cst)->repos_root_url = status->repos_root_url;
+ (*cst)->repos_uuid = status->repos_uuid;
+ (*cst)->repos_relpath = status->repos_relpath;
+
+ (*cst)->switched = status->switched;
+
+ (*cst)->file_external = status->file_external;
+ if (status->file_external)
+ {
+ (*cst)->switched = FALSE;
+ }
+
+ (*cst)->lock = status->lock;
+
+ (*cst)->changelist = status->changelist;
+ (*cst)->depth = status->depth;
+
+ /* Out of date information */
+ (*cst)->ood_kind = status->ood_kind;
+ (*cst)->repos_node_status = status->repos_node_status;
+ (*cst)->repos_text_status = status->repos_text_status;
+ (*cst)->repos_prop_status = status->repos_prop_status;
+ (*cst)->repos_lock = status->repos_lock;
+
+ (*cst)->ood_changed_rev = status->ood_changed_rev;
+ (*cst)->ood_changed_date = status->ood_changed_date;
+ (*cst)->ood_changed_author = status->ood_changed_author;
+
+ /* When changing the value of backwards_compatibility_baton, also
+ change its use in status4_wrapper_func in deprecated.c */
+ (*cst)->backwards_compatibility_baton = status;
+
+ if (status->versioned && status->conflicted)
+ {
+ svn_boolean_t text_conflicted, prop_conflicted, tree_conflicted;
+
+ /* Note: This checks the on disk markers to automatically hide
+ text/property conflicts that are hidden by removing their
+ markers */
+ SVN_ERR(svn_wc_conflicted_p3(&text_conflicted, &prop_conflicted,
+ &tree_conflicted, wc_ctx, local_abspath,
+ scratch_pool));
+
+ if (text_conflicted)
+ (*cst)->text_status = svn_wc_status_conflicted;
+
+ if (prop_conflicted)
+ (*cst)->prop_status = svn_wc_status_conflicted;
+
+ /* ### Also set this for tree_conflicts? */
+ if (text_conflicted || prop_conflicted)
+ (*cst)->node_status = svn_wc_status_conflicted;
+ }
+
+ (*cst)->moved_from_abspath = status->moved_from_abspath;
+ (*cst)->moved_to_abspath = status->moved_to_abspath;
+
+ return SVN_NO_ERROR;
+}
+
diff --git a/subversion/libsvn_client/switch.c b/subversion/libsvn_client/switch.c
new file mode 100644
index 0000000..fae03de
--- /dev/null
+++ b/subversion/libsvn_client/switch.c
@@ -0,0 +1,487 @@
+/*
+ * switch.c: implement 'switch' feature via WC & RA interfaces.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_client.h"
+#include "svn_error.h"
+#include "svn_hash.h"
+#include "svn_time.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_config.h"
+#include "svn_pools.h"
+#include "client.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+
+
+/*** Code. ***/
+
+/* This feature is essentially identical to 'svn update' (see
+ ./update.c), but with two differences:
+
+ - the reporter->finish_report() routine needs to make the server
+ run delta_dirs() on two *different* paths, rather than on two
+ identical paths.
+
+ - after the update runs, we need to more than just
+ ensure_uniform_revision; we need to rewrite all the entries'
+ URL attributes.
+*/
+
+
+/* A conflict callback that simply records the conflicted path in BATON.
+
+ Implements svn_wc_conflict_resolver_func2_t.
+*/
+static svn_error_t *
+record_conflict(svn_wc_conflict_result_t **result,
+ const svn_wc_conflict_description2_t *description,
+ void *baton,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *conflicted_paths = baton;
+
+ svn_hash_sets(conflicted_paths,
+ apr_pstrdup(apr_hash_pool_get(conflicted_paths),
+ description->local_abspath), "");
+ *result = svn_wc_create_conflict_result(svn_wc_conflict_choose_postpone,
+ NULL, result_pool);
+ return SVN_NO_ERROR;
+}
+
+/* ...
+
+ Add the paths of any conflict victims to CONFLICTED_PATHS, if that
+ is not null.
+*/
+static svn_error_t *
+switch_internal(svn_revnum_t *result_rev,
+ apr_hash_t *conflicted_paths,
+ const char *local_abspath,
+ const char *anchor_abspath,
+ const char *switch_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t depth_is_sticky,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t *timestamp_sleep,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const svn_ra_reporter3_t *reporter;
+ void *report_baton;
+ const char *anchor_url, *target;
+ svn_client__pathrev_t *switch_loc;
+ svn_ra_session_t *ra_session;
+ svn_revnum_t revnum;
+ const char *diff3_cmd;
+ apr_hash_t *wcroot_iprops;
+ apr_array_header_t *inherited_props;
+ svn_boolean_t use_commit_times;
+ const svn_delta_editor_t *switch_editor;
+ void *switch_edit_baton;
+ const char *preserved_exts_str;
+ apr_array_header_t *preserved_exts;
+ svn_boolean_t server_supports_depth;
+ struct svn_client__dirent_fetcher_baton_t dfb;
+ svn_config_t *cfg = ctx->config
+ ? svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG)
+ : NULL;
+
+ /* An unknown depth can't be sticky. */
+ if (depth == svn_depth_unknown)
+ depth_is_sticky = FALSE;
+
+ /* Do not support the situation of both exclude and switch a target. */
+ if (depth == svn_depth_exclude)
+ return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Cannot both exclude and switch a path"));
+
+ /* Get the external diff3, if any. */
+ svn_config_get(cfg, &diff3_cmd, SVN_CONFIG_SECTION_HELPERS,
+ SVN_CONFIG_OPTION_DIFF3_CMD, NULL);
+
+ if (diff3_cmd != NULL)
+ SVN_ERR(svn_path_cstring_to_utf8(&diff3_cmd, diff3_cmd, pool));
+
+ /* See if the user wants last-commit timestamps instead of current ones. */
+ SVN_ERR(svn_config_get_bool(cfg, &use_commit_times,
+ SVN_CONFIG_SECTION_MISCELLANY,
+ SVN_CONFIG_OPTION_USE_COMMIT_TIMES, FALSE));
+
+ {
+ svn_boolean_t has_working;
+ SVN_ERR(svn_wc__node_has_working(&has_working, ctx->wc_ctx, local_abspath,
+ pool));
+
+ if (has_working)
+ return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+ _("Cannot switch '%s' because it is not in the "
+ "repository yet"),
+ svn_dirent_local_style(local_abspath, pool));
+ }
+
+ /* See which files the user wants to preserve the extension of when
+ conflict files are made. */
+ svn_config_get(cfg, &preserved_exts_str, SVN_CONFIG_SECTION_MISCELLANY,
+ SVN_CONFIG_OPTION_PRESERVED_CF_EXTS, "");
+ preserved_exts = *preserved_exts_str
+ ? svn_cstring_split(preserved_exts_str, "\n\r\t\v ", FALSE, pool)
+ : NULL;
+
+ /* Sanity check. Without these, the switch is meaningless. */
+ SVN_ERR_ASSERT(switch_url && (switch_url[0] != '\0'));
+
+ if (strcmp(local_abspath, anchor_abspath))
+ target = svn_dirent_basename(local_abspath, pool);
+ else
+ target = "";
+
+ SVN_ERR(svn_wc__node_get_url(&anchor_url, ctx->wc_ctx, anchor_abspath,
+ pool, pool));
+ if (! anchor_url)
+ return svn_error_createf(SVN_ERR_ENTRY_MISSING_URL, NULL,
+ _("Directory '%s' has no URL"),
+ svn_dirent_local_style(anchor_abspath, pool));
+
+ /* We may need to crop the tree if the depth is sticky */
+ if (depth_is_sticky && depth < svn_depth_infinity)
+ {
+ svn_node_kind_t target_kind;
+
+ if (depth == svn_depth_exclude)
+ {
+ SVN_ERR(svn_wc_exclude(ctx->wc_ctx,
+ local_abspath,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ pool));
+
+ /* Target excluded, we are done now */
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR(svn_wc_read_kind2(&target_kind, ctx->wc_ctx, local_abspath,
+ TRUE, TRUE, pool));
+
+ if (target_kind == svn_node_dir)
+ SVN_ERR(svn_wc_crop_tree2(ctx->wc_ctx, local_abspath, depth,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ pool));
+ }
+
+ /* Open an RA session to 'source' URL */
+ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &switch_loc,
+ switch_url, anchor_abspath,
+ peg_revision, revision,
+ ctx, pool));
+
+ /* Disallow a switch operation to change the repository root of the
+ target. */
+ if (! svn_uri__is_ancestor(switch_loc->repos_root_url, anchor_url))
+ return svn_error_createf(SVN_ERR_WC_INVALID_SWITCH, NULL,
+ _("'%s'\nis not the same repository as\n'%s'"),
+ anchor_url, switch_loc->repos_root_url);
+
+ /* If we're not ignoring ancestry, then error out if the switch
+ source and target don't have a common ancestory.
+
+ ### We're acting on the anchor here, not the target. Is that
+ ### okay? */
+ if (! ignore_ancestry)
+ {
+ svn_client__pathrev_t *target_base_loc, *yca;
+
+ SVN_ERR(svn_client__wc_node_get_base(&target_base_loc, local_abspath,
+ ctx->wc_ctx, pool, pool));
+
+ if (!target_base_loc)
+ yca = NULL; /* Not versioned */
+ else
+ {
+ /* ### It would be nice if this function could reuse the existing
+ ra session instead of opening two for its own use. */
+ SVN_ERR(svn_client__get_youngest_common_ancestor(
+ &yca, switch_loc, target_base_loc, ra_session, ctx,
+ pool, pool));
+ }
+ if (! yca)
+ return svn_error_createf(SVN_ERR_CLIENT_UNRELATED_RESOURCES, NULL,
+ _("'%s' shares no common ancestry with '%s'"),
+ switch_url,
+ svn_dirent_dirname(local_abspath, pool));
+ }
+
+ wcroot_iprops = apr_hash_make(pool);
+
+ /* Will the base of LOCAL_ABSPATH require an iprop cache post-switch?
+ If we are switching LOCAL_ABSPATH to the root of the repository then
+ we don't need to cache inherited properties. In all other cases we
+ *might* need to cache iprops. */
+ if (strcmp(switch_loc->repos_root_url, switch_loc->url) != 0)
+ {
+ svn_boolean_t wc_root;
+ svn_boolean_t needs_iprop_cache = TRUE;
+
+ SVN_ERR(svn_wc__is_wcroot(&wc_root, ctx->wc_ctx, local_abspath,
+ pool));
+
+ /* Switching the WC root to anything but the repos root means
+ we need an iprop cache. */
+ if (!wc_root)
+ {
+ /* We know we are switching a subtree to something other than the
+ repos root, but if we are unswitching that subtree we don't
+ need an iprops cache. */
+ const char *target_parent_url;
+ const char *unswitched_url;
+
+ /* Calculate the URL LOCAL_ABSPATH would have if it was unswitched
+ relative to its parent. */
+ SVN_ERR(svn_wc__node_get_url(&target_parent_url, ctx->wc_ctx,
+ svn_dirent_dirname(local_abspath,
+ pool),
+ pool, pool));
+ unswitched_url = svn_path_url_add_component2(
+ target_parent_url,
+ svn_dirent_basename(local_abspath, pool),
+ pool);
+
+ /* If LOCAL_ABSPATH will be unswitched relative to its parent, then
+ it doesn't need an iprop cache. Note: It doesn't matter if
+ LOCAL_ABSPATH is withing a switched subtree, only if it's the
+ *root* of a switched subtree.*/
+ if (strcmp(unswitched_url, switch_loc->url) == 0)
+ needs_iprop_cache = FALSE;
+ }
+
+
+ if (needs_iprop_cache)
+ {
+ SVN_ERR(svn_ra_get_inherited_props(ra_session, &inherited_props,
+ "", switch_loc->rev, pool,
+ pool));
+ svn_hash_sets(wcroot_iprops, local_abspath, inherited_props);
+ }
+ }
+
+ SVN_ERR(svn_ra_reparent(ra_session, anchor_url, pool));
+
+ /* Fetch the switch (update) editor. If REVISION is invalid, that's
+ okay; the RA driver will call editor->set_target_revision() later on. */
+ SVN_ERR(svn_ra_has_capability(ra_session, &server_supports_depth,
+ SVN_RA_CAPABILITY_DEPTH, pool));
+
+ dfb.ra_session = ra_session;
+ dfb.anchor_url = anchor_url;
+ dfb.target_revision = switch_loc->rev;
+
+ SVN_ERR(svn_wc__get_switch_editor(&switch_editor, &switch_edit_baton,
+ &revnum, ctx->wc_ctx, anchor_abspath,
+ target, switch_loc->url, wcroot_iprops,
+ use_commit_times, depth,
+ depth_is_sticky, allow_unver_obstructions,
+ server_supports_depth,
+ diff3_cmd, preserved_exts,
+ svn_client__dirent_fetcher, &dfb,
+ conflicted_paths ? record_conflict : NULL,
+ conflicted_paths,
+ NULL, NULL,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ pool, pool));
+
+ /* Tell RA to do an update of URL+TARGET to REVISION; if we pass an
+ invalid revnum, that means RA will use the latest revision. */
+ SVN_ERR(svn_ra_do_switch3(ra_session, &reporter, &report_baton,
+ switch_loc->rev,
+ target,
+ depth_is_sticky ? depth : svn_depth_unknown,
+ switch_loc->url,
+ FALSE /* send_copyfrom_args */,
+ ignore_ancestry,
+ switch_editor, switch_edit_baton,
+ pool, pool));
+
+ /* Past this point, we assume the WC is going to be modified so we will
+ * need to sleep for timestamps. */
+ *timestamp_sleep = TRUE;
+
+ /* Drive the reporter structure, describing the revisions within
+ PATH. When we call reporter->finish_report, the update_editor
+ will be driven by svn_repos_dir_delta2. */
+ SVN_ERR(svn_wc_crawl_revisions5(ctx->wc_ctx, local_abspath, reporter,
+ report_baton, TRUE,
+ depth, (! depth_is_sticky),
+ (! server_supports_depth),
+ use_commit_times,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ pool));
+
+ /* We handle externals after the switch is complete, so that
+ handling external items (and any errors therefrom) doesn't delay
+ the primary operation. */
+ if (SVN_DEPTH_IS_RECURSIVE(depth) && (! ignore_externals))
+ {
+ apr_hash_t *new_externals;
+ apr_hash_t *new_depths;
+ SVN_ERR(svn_wc__externals_gather_definitions(&new_externals,
+ &new_depths,
+ ctx->wc_ctx, local_abspath,
+ depth, pool, pool));
+
+ SVN_ERR(svn_client__handle_externals(new_externals,
+ new_depths,
+ switch_loc->repos_root_url,
+ local_abspath,
+ depth, timestamp_sleep,
+ ctx, pool));
+ }
+
+ /* Let everyone know we're finished here. */
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify(anchor_abspath, svn_wc_notify_update_completed,
+ pool);
+ notify->kind = svn_node_none;
+ notify->content_state = notify->prop_state
+ = svn_wc_notify_state_inapplicable;
+ notify->lock_state = svn_wc_notify_lock_state_inapplicable;
+ notify->revision = revnum;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+ }
+
+ /* If the caller wants the result revision, give it to them. */
+ if (result_rev)
+ *result_rev = revnum;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__switch_internal(svn_revnum_t *result_rev,
+ const char *path,
+ const char *switch_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t depth_is_sticky,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_boolean_t ignore_ancestry,
+ svn_boolean_t *timestamp_sleep,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *local_abspath, *anchor_abspath;
+ svn_boolean_t acquired_lock;
+ svn_error_t *err, *err1, *err2;
+ apr_hash_t *conflicted_paths
+ = ctx->conflict_func2 ? apr_hash_make(pool) : NULL;
+
+ SVN_ERR_ASSERT(path);
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, pool));
+
+ /* Rely on svn_wc__acquire_write_lock setting ANCHOR_ABSPATH even
+ when it returns SVN_ERR_WC_LOCKED */
+ err = svn_wc__acquire_write_lock(&anchor_abspath,
+ ctx->wc_ctx, local_abspath, TRUE,
+ pool, pool);
+ if (err && err->apr_err != SVN_ERR_WC_LOCKED)
+ return svn_error_trace(err);
+
+ acquired_lock = (err == SVN_NO_ERROR);
+ svn_error_clear(err);
+
+ err1 = switch_internal(result_rev, conflicted_paths,
+ local_abspath, anchor_abspath,
+ switch_url, peg_revision, revision,
+ depth, depth_is_sticky,
+ ignore_externals,
+ allow_unver_obstructions, ignore_ancestry,
+ timestamp_sleep, ctx, pool);
+
+ /* Give the conflict resolver callback the opportunity to
+ * resolve any conflicts that were raised. */
+ if (! err1 && ctx->conflict_func2)
+ {
+ err1 = svn_client__resolve_conflicts(NULL, conflicted_paths, ctx, pool);
+ }
+
+ if (acquired_lock)
+ err2 = svn_wc__release_write_lock(ctx->wc_ctx, anchor_abspath, pool);
+ else
+ err2 = SVN_NO_ERROR;
+
+ return svn_error_compose_create(err1, err2);
+}
+
+svn_error_t *
+svn_client_switch3(svn_revnum_t *result_rev,
+ const char *path,
+ const char *switch_url,
+ const svn_opt_revision_t *peg_revision,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t depth_is_sticky,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_boolean_t ignore_ancestry,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+ svn_boolean_t sleep_here = FALSE;
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+
+ err = svn_client__switch_internal(result_rev, path, switch_url,
+ peg_revision, revision, depth,
+ depth_is_sticky, ignore_externals,
+ allow_unver_obstructions,
+ ignore_ancestry, &sleep_here, ctx, pool);
+
+ /* Sleep to ensure timestamp integrity (we do this regardless of
+ errors in the actual switch operation(s)). */
+ if (sleep_here)
+ svn_io_sleep_for_timestamps(path, pool);
+
+ return svn_error_trace(err);
+}
diff --git a/subversion/libsvn_client/update.c b/subversion/libsvn_client/update.c
new file mode 100644
index 0000000..21f33ec
--- /dev/null
+++ b/subversion/libsvn_client/update.c
@@ -0,0 +1,707 @@
+/*
+ * update.c: wrappers around wc update functionality
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_hash.h"
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "svn_error.h"
+#include "svn_config.h"
+#include "svn_time.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_pools.h"
+#include "svn_io.h"
+#include "client.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+
+/* Implements svn_wc_dirents_func_t for update and switch handling. Assumes
+ a struct svn_client__dirent_fetcher_baton_t * baton */
+svn_error_t *
+svn_client__dirent_fetcher(void *baton,
+ apr_hash_t **dirents,
+ const char *repos_root_url,
+ const char *repos_relpath,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct svn_client__dirent_fetcher_baton_t *dfb = baton;
+ const char *old_url = NULL;
+ const char *session_relpath;
+ svn_node_kind_t kind;
+ const char *url;
+
+ url = svn_path_url_add_component2(repos_root_url, repos_relpath,
+ scratch_pool);
+
+ if (!svn_uri__is_ancestor(dfb->anchor_url, url))
+ {
+ SVN_ERR(svn_client__ensure_ra_session_url(&old_url, dfb->ra_session,
+ url, scratch_pool));
+ session_relpath = "";
+ }
+ else
+ SVN_ERR(svn_ra_get_path_relative_to_session(dfb->ra_session,
+ &session_relpath, url,
+ scratch_pool));
+
+ /* Is session_relpath still a directory? */
+ SVN_ERR(svn_ra_check_path(dfb->ra_session, session_relpath,
+ dfb->target_revision, &kind, scratch_pool));
+
+ if (kind == svn_node_dir)
+ SVN_ERR(svn_ra_get_dir2(dfb->ra_session, dirents, NULL, NULL,
+ session_relpath, dfb->target_revision,
+ SVN_DIRENT_KIND, result_pool));
+ else
+ *dirents = NULL;
+
+ if (old_url)
+ SVN_ERR(svn_ra_reparent(dfb->ra_session, old_url, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/*** Code. ***/
+
+/* Set *CLEAN_CHECKOUT to FALSE only if LOCAL_ABSPATH is a non-empty
+ folder. ANCHOR_ABSPATH is the w/c root and LOCAL_ABSPATH will still
+ be considered empty, if it is equal to ANCHOR_ABSPATH and only
+ contains the admin sub-folder.
+ If the w/c folder already exists but cannot be openend, we return
+ "unclean" - just in case. Most likely, the caller will have to bail
+ out later due to the same error we got here.
+ */
+static svn_error_t *
+is_empty_wc(svn_boolean_t *clean_checkout,
+ const char *local_abspath,
+ const char *anchor_abspath,
+ apr_pool_t *pool)
+{
+ apr_dir_t *dir;
+ apr_finfo_t finfo;
+ svn_error_t *err;
+
+ /* "clean" until found dirty */
+ *clean_checkout = TRUE;
+
+ /* open directory. If it does not exist, yet, a clean one will
+ be created by the caller. */
+ err = svn_io_dir_open(&dir, local_abspath, pool);
+ if (err)
+ {
+ if (! APR_STATUS_IS_ENOENT(err->apr_err))
+ *clean_checkout = FALSE;
+
+ svn_error_clear(err);
+ return SVN_NO_ERROR;
+ }
+
+ for (err = svn_io_dir_read(&finfo, APR_FINFO_NAME, dir, pool);
+ err == SVN_NO_ERROR;
+ err = svn_io_dir_read(&finfo, APR_FINFO_NAME, dir, pool))
+ {
+ /* Ignore entries for this dir and its parent, robustly.
+ (APR promises that they'll come first, so technically
+ this guard could be moved outside the loop. But Ryan Bloom
+ says he doesn't believe it, and I believe him. */
+ if (! (finfo.name[0] == '.'
+ && (finfo.name[1] == '\0'
+ || (finfo.name[1] == '.' && finfo.name[2] == '\0'))))
+ {
+ if ( ! svn_wc_is_adm_dir(finfo.name, pool)
+ || strcmp(local_abspath, anchor_abspath) != 0)
+ {
+ *clean_checkout = FALSE;
+ break;
+ }
+ }
+ }
+
+ if (err)
+ {
+ if (! APR_STATUS_IS_ENOENT(err->apr_err))
+ {
+ /* There was some issue reading the folder content.
+ * We better disable optimizations in that case. */
+ *clean_checkout = FALSE;
+ }
+
+ svn_error_clear(err);
+ }
+
+ return svn_io_dir_close(dir);
+}
+
+/* A conflict callback that simply records the conflicted path in BATON.
+
+ Implements svn_wc_conflict_resolver_func2_t.
+*/
+static svn_error_t *
+record_conflict(svn_wc_conflict_result_t **result,
+ const svn_wc_conflict_description2_t *description,
+ void *baton,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_hash_t *conflicted_paths = baton;
+
+ svn_hash_sets(conflicted_paths,
+ apr_pstrdup(apr_hash_pool_get(conflicted_paths),
+ description->local_abspath), "");
+ *result = svn_wc_create_conflict_result(svn_wc_conflict_choose_postpone,
+ NULL, result_pool);
+ return SVN_NO_ERROR;
+}
+
+/* This is a helper for svn_client__update_internal(), which see for
+ an explanation of most of these parameters. Some stuff that's
+ unique is as follows:
+
+ ANCHOR_ABSPATH is the local absolute path of the update anchor.
+ This is typically either the same as LOCAL_ABSPATH, or the
+ immediate parent of LOCAL_ABSPATH.
+
+ If NOTIFY_SUMMARY is set (and there's a notification handler in
+ CTX), transmit the final update summary upon successful
+ completion of the update.
+
+ Add the paths of any conflict victims to CONFLICTED_PATHS, if that
+ is not null.
+*/
+static svn_error_t *
+update_internal(svn_revnum_t *result_rev,
+ apr_hash_t *conflicted_paths,
+ const char *local_abspath,
+ const char *anchor_abspath,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t depth_is_sticky,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_boolean_t adds_as_modification,
+ svn_boolean_t *timestamp_sleep,
+ svn_boolean_t notify_summary,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const svn_delta_editor_t *update_editor;
+ void *update_edit_baton;
+ const svn_ra_reporter3_t *reporter;
+ void *report_baton;
+ const char *corrected_url;
+ const char *target;
+ const char *repos_root_url;
+ const char *repos_relpath;
+ const char *repos_uuid;
+ const char *anchor_url;
+ svn_revnum_t revnum;
+ svn_boolean_t use_commit_times;
+ svn_boolean_t clean_checkout = FALSE;
+ const char *diff3_cmd;
+ apr_hash_t *wcroot_iprops;
+ svn_opt_revision_t opt_rev;
+ svn_ra_session_t *ra_session;
+ const char *preserved_exts_str;
+ apr_array_header_t *preserved_exts;
+ struct svn_client__dirent_fetcher_baton_t dfb;
+ svn_boolean_t server_supports_depth;
+ svn_boolean_t cropping_target;
+ svn_boolean_t target_conflicted = FALSE;
+ svn_config_t *cfg = ctx->config
+ ? svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG)
+ : NULL;
+
+ if (result_rev)
+ *result_rev = SVN_INVALID_REVNUM;
+
+ /* An unknown depth can't be sticky. */
+ if (depth == svn_depth_unknown)
+ depth_is_sticky = FALSE;
+
+ if (strcmp(local_abspath, anchor_abspath))
+ target = svn_dirent_basename(local_abspath, pool);
+ else
+ target = "";
+
+ /* Check if our anchor exists in BASE. If it doesn't we can't update. */
+ SVN_ERR(svn_wc__node_get_base(NULL, NULL, &repos_relpath, &repos_root_url,
+ &repos_uuid, NULL,
+ ctx->wc_ctx, anchor_abspath,
+ TRUE, FALSE,
+ pool, pool));
+
+ /* It does not make sense to update conflict victims. */
+ if (repos_relpath)
+ {
+ svn_error_t *err;
+ svn_boolean_t text_conflicted, prop_conflicted;
+
+ anchor_url = svn_path_url_add_component2(repos_root_url, repos_relpath,
+ pool);
+
+ err = svn_wc_conflicted_p3(&text_conflicted, &prop_conflicted,
+ NULL,
+ ctx->wc_ctx, local_abspath, pool);
+
+ if (err && err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND)
+ return svn_error_trace(err);
+ svn_error_clear(err);
+
+ /* tree-conflicts are handled by the update editor */
+ if (!err && (text_conflicted || prop_conflicted))
+ target_conflicted = TRUE;
+ }
+ else
+ anchor_url = NULL;
+
+ if (! anchor_url || target_conflicted)
+ {
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *nt;
+
+ nt = svn_wc_create_notify(local_abspath,
+ target_conflicted
+ ? svn_wc_notify_skip_conflicted
+ : svn_wc_notify_update_skip_working_only,
+ pool);
+
+ ctx->notify_func2(ctx->notify_baton2, nt, pool);
+ }
+ return SVN_NO_ERROR;
+ }
+
+ /* We may need to crop the tree if the depth is sticky */
+ cropping_target = (depth_is_sticky && depth < svn_depth_infinity);
+ if (cropping_target)
+ {
+ svn_node_kind_t target_kind;
+
+ if (depth == svn_depth_exclude)
+ {
+ SVN_ERR(svn_wc_exclude(ctx->wc_ctx,
+ local_abspath,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ pool));
+
+ /* Target excluded, we are done now */
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR(svn_wc_read_kind2(&target_kind, ctx->wc_ctx, local_abspath,
+ TRUE, TRUE, pool));
+ if (target_kind == svn_node_dir)
+ {
+ SVN_ERR(svn_wc_crop_tree2(ctx->wc_ctx, local_abspath, depth,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ pool));
+ }
+ }
+
+ /* check whether the "clean c/o" optimization is applicable */
+ SVN_ERR(is_empty_wc(&clean_checkout, local_abspath, anchor_abspath, pool));
+
+ /* Get the external diff3, if any. */
+ svn_config_get(cfg, &diff3_cmd, SVN_CONFIG_SECTION_HELPERS,
+ SVN_CONFIG_OPTION_DIFF3_CMD, NULL);
+
+ if (diff3_cmd != NULL)
+ SVN_ERR(svn_path_cstring_to_utf8(&diff3_cmd, diff3_cmd, pool));
+
+ /* See if the user wants last-commit timestamps instead of current ones. */
+ SVN_ERR(svn_config_get_bool(cfg, &use_commit_times,
+ SVN_CONFIG_SECTION_MISCELLANY,
+ SVN_CONFIG_OPTION_USE_COMMIT_TIMES, FALSE));
+
+ /* See which files the user wants to preserve the extension of when
+ conflict files are made. */
+ svn_config_get(cfg, &preserved_exts_str, SVN_CONFIG_SECTION_MISCELLANY,
+ SVN_CONFIG_OPTION_PRESERVED_CF_EXTS, "");
+ preserved_exts = *preserved_exts_str
+ ? svn_cstring_split(preserved_exts_str, "\n\r\t\v ", FALSE, pool)
+ : NULL;
+
+ /* Let everyone know we're starting a real update (unless we're
+ asked not to). */
+ if (ctx->notify_func2 && notify_summary)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify(local_abspath, svn_wc_notify_update_started,
+ pool);
+ notify->kind = svn_node_none;
+ notify->content_state = notify->prop_state
+ = svn_wc_notify_state_inapplicable;
+ notify->lock_state = svn_wc_notify_lock_state_inapplicable;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+ }
+
+ /* Open an RA session for the URL */
+ SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url,
+ anchor_url,
+ anchor_abspath, NULL, TRUE,
+ TRUE, ctx, pool, pool));
+
+ /* If we got a corrected URL from the RA subsystem, we'll need to
+ relocate our working copy first. */
+ if (corrected_url)
+ {
+ const char *new_repos_root_url;
+
+ /* To relocate everything inside our repository we need the old and new
+ repos root. */
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &new_repos_root_url, pool));
+
+ /* svn_client_relocate2() will check the uuid */
+ SVN_ERR(svn_client_relocate2(anchor_abspath, anchor_url,
+ new_repos_root_url, ignore_externals,
+ ctx, pool));
+
+ /* Store updated repository root for externals */
+ repos_root_url = new_repos_root_url;
+ /* ### We should update anchor_loc->repos_uuid too, although currently
+ * we don't use it. */
+ anchor_url = corrected_url;
+ }
+
+ /* Resolve unspecified REVISION now, because we need to retrieve the
+ correct inherited props prior to the editor drive and we need to
+ use the same value of HEAD for both. */
+ opt_rev.kind = revision->kind;
+ opt_rev.value = revision->value;
+ if (opt_rev.kind == svn_opt_revision_unspecified)
+ opt_rev.kind = svn_opt_revision_head;
+
+ /* ### todo: shouldn't svn_client__get_revision_number be able
+ to take a URL as easily as a local path? */
+ SVN_ERR(svn_client__get_revision_number(&revnum, NULL, ctx->wc_ctx,
+ local_abspath, ra_session, &opt_rev,
+ pool));
+
+ SVN_ERR(svn_ra_has_capability(ra_session, &server_supports_depth,
+ SVN_RA_CAPABILITY_DEPTH, pool));
+
+ dfb.ra_session = ra_session;
+ dfb.target_revision = revnum;
+ dfb.anchor_url = anchor_url;
+
+ SVN_ERR(svn_client__get_inheritable_props(&wcroot_iprops, local_abspath,
+ revnum, depth, ra_session,
+ ctx, pool, pool));
+
+ /* Fetch the update editor. If REVISION is invalid, that's okay;
+ the RA driver will call editor->set_target_revision later on. */
+ SVN_ERR(svn_wc__get_update_editor(&update_editor, &update_edit_baton,
+ &revnum, ctx->wc_ctx, anchor_abspath,
+ target, wcroot_iprops, use_commit_times,
+ depth, depth_is_sticky,
+ allow_unver_obstructions,
+ adds_as_modification,
+ server_supports_depth,
+ clean_checkout,
+ diff3_cmd, preserved_exts,
+ svn_client__dirent_fetcher, &dfb,
+ conflicted_paths ? record_conflict : NULL,
+ conflicted_paths,
+ NULL, NULL,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ pool, pool));
+
+ /* Tell RA to do an update of URL+TARGET to REVISION; if we pass an
+ invalid revnum, that means RA will use the latest revision. */
+ SVN_ERR(svn_ra_do_update3(ra_session, &reporter, &report_baton,
+ revnum, target,
+ (!server_supports_depth || depth_is_sticky
+ ? depth
+ : svn_depth_unknown),
+ FALSE /* send_copyfrom_args */,
+ FALSE /* ignore_ancestry */,
+ update_editor, update_edit_baton, pool, pool));
+
+ /* Past this point, we assume the WC is going to be modified so we will
+ * need to sleep for timestamps. */
+ *timestamp_sleep = TRUE;
+
+ /* Drive the reporter structure, describing the revisions within
+ PATH. When we call reporter->finish_report, the
+ update_editor will be driven by svn_repos_dir_delta2. */
+ SVN_ERR(svn_wc_crawl_revisions5(ctx->wc_ctx, local_abspath, reporter,
+ report_baton, TRUE,
+ depth, (! depth_is_sticky),
+ (! server_supports_depth),
+ use_commit_times,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ pool));
+
+ /* We handle externals after the update is complete, so that
+ handling external items (and any errors therefrom) doesn't delay
+ the primary operation. */
+ if ((SVN_DEPTH_IS_RECURSIVE(depth) || cropping_target)
+ && (! ignore_externals))
+ {
+ apr_hash_t *new_externals;
+ apr_hash_t *new_depths;
+ SVN_ERR(svn_wc__externals_gather_definitions(&new_externals,
+ &new_depths,
+ ctx->wc_ctx, local_abspath,
+ depth, pool, pool));
+
+ SVN_ERR(svn_client__handle_externals(new_externals,
+ new_depths,
+ repos_root_url, local_abspath,
+ depth, timestamp_sleep,
+ ctx, pool));
+ }
+
+ /* Let everyone know we're finished here (unless we're asked not to). */
+ if (ctx->notify_func2 && notify_summary)
+ {
+ svn_wc_notify_t *notify
+ = svn_wc_create_notify(local_abspath, svn_wc_notify_update_completed,
+ pool);
+ notify->kind = svn_node_none;
+ notify->content_state = notify->prop_state
+ = svn_wc_notify_state_inapplicable;
+ notify->lock_state = svn_wc_notify_lock_state_inapplicable;
+ notify->revision = revnum;
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
+ }
+
+ /* If the caller wants the result revision, give it to them. */
+ if (result_rev)
+ *result_rev = revnum;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__update_internal(svn_revnum_t *result_rev,
+ const char *local_abspath,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t depth_is_sticky,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_boolean_t adds_as_modification,
+ svn_boolean_t make_parents,
+ svn_boolean_t innerupdate,
+ svn_boolean_t *timestamp_sleep,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ const char *anchor_abspath, *lockroot_abspath;
+ svn_error_t *err;
+ svn_opt_revision_t peg_revision = *revision;
+ apr_hash_t *conflicted_paths
+ = ctx->conflict_func2 ? apr_hash_make(pool) : NULL;
+
+ SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
+ SVN_ERR_ASSERT(! (innerupdate && make_parents));
+
+ if (make_parents)
+ {
+ int i;
+ const char *parent_abspath = local_abspath;
+ apr_array_header_t *missing_parents =
+ apr_array_make(pool, 4, sizeof(const char *));
+
+ while (1)
+ {
+ /* Try to lock. If we can't lock because our target (or its
+ parent) isn't a working copy, we'll try to walk up the
+ tree to find a working copy, remembering this path's
+ parent as one we need to flesh out. */
+ err = svn_wc__acquire_write_lock(&lockroot_abspath, ctx->wc_ctx,
+ parent_abspath, !innerupdate,
+ pool, pool);
+ if (!err)
+ break;
+ if ((err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
+ || svn_dirent_is_root(parent_abspath, strlen(parent_abspath)))
+ return err;
+ svn_error_clear(err);
+
+ /* Remember the parent of our update target as a missing
+ parent. */
+ parent_abspath = svn_dirent_dirname(parent_abspath, pool);
+ APR_ARRAY_PUSH(missing_parents, const char *) = parent_abspath;
+ }
+
+ /* Run 'svn up --depth=empty' (effectively) on the missing
+ parents, if any. */
+ anchor_abspath = lockroot_abspath;
+ for (i = missing_parents->nelts - 1; i >= 0; i--)
+ {
+ const char *missing_parent =
+ APR_ARRAY_IDX(missing_parents, i, const char *);
+
+ err = update_internal(result_rev, conflicted_paths,
+ missing_parent, anchor_abspath,
+ &peg_revision, svn_depth_empty, FALSE,
+ ignore_externals, allow_unver_obstructions,
+ adds_as_modification, timestamp_sleep,
+ FALSE, ctx, pool);
+ if (err)
+ goto cleanup;
+ anchor_abspath = missing_parent;
+
+ /* If we successfully updated a missing parent, let's re-use
+ the returned revision number for future updates for the
+ sake of consistency. */
+ peg_revision.kind = svn_opt_revision_number;
+ peg_revision.value.number = *result_rev;
+ }
+ }
+ else
+ {
+ SVN_ERR(svn_wc__acquire_write_lock(&lockroot_abspath, ctx->wc_ctx,
+ local_abspath, !innerupdate,
+ pool, pool));
+ anchor_abspath = lockroot_abspath;
+ }
+
+ err = update_internal(result_rev, conflicted_paths,
+ local_abspath, anchor_abspath,
+ &peg_revision, depth, depth_is_sticky,
+ ignore_externals, allow_unver_obstructions,
+ adds_as_modification, timestamp_sleep,
+ TRUE, ctx, pool);
+
+ /* Give the conflict resolver callback the opportunity to
+ * resolve any conflicts that were raised. */
+ if (! err && ctx->conflict_func2)
+ {
+ err = svn_client__resolve_conflicts(NULL, conflicted_paths, ctx, pool);
+ }
+
+ cleanup:
+ err = svn_error_compose_create(
+ err,
+ svn_wc__release_write_lock(ctx->wc_ctx, lockroot_abspath, pool));
+
+ return svn_error_trace(err);
+}
+
+
+svn_error_t *
+svn_client_update4(apr_array_header_t **result_revs,
+ const apr_array_header_t *paths,
+ const svn_opt_revision_t *revision,
+ svn_depth_t depth,
+ svn_boolean_t depth_is_sticky,
+ svn_boolean_t ignore_externals,
+ svn_boolean_t allow_unver_obstructions,
+ svn_boolean_t adds_as_modification,
+ svn_boolean_t make_parents,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *pool)
+{
+ int i;
+ apr_pool_t *iterpool = svn_pool_create(pool);
+ const char *path = NULL;
+ svn_boolean_t sleep = FALSE;
+ svn_error_t *err = SVN_NO_ERROR;
+
+ if (result_revs)
+ *result_revs = apr_array_make(pool, paths->nelts, sizeof(svn_revnum_t));
+
+ for (i = 0; i < paths->nelts; ++i)
+ {
+ path = APR_ARRAY_IDX(paths, i, const char *);
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+ }
+
+ for (i = 0; i < paths->nelts; ++i)
+ {
+ svn_revnum_t result_rev;
+ const char *local_abspath;
+ path = APR_ARRAY_IDX(paths, i, const char *);
+
+ svn_pool_clear(iterpool);
+
+ if (ctx->cancel_func)
+ {
+ err = ctx->cancel_func(ctx->cancel_baton);
+ if (err)
+ goto cleanup;
+ }
+
+ err = svn_dirent_get_absolute(&local_abspath, path, iterpool);
+ if (err)
+ goto cleanup;
+ err = svn_client__update_internal(&result_rev, local_abspath,
+ revision, depth, depth_is_sticky,
+ ignore_externals,
+ allow_unver_obstructions,
+ adds_as_modification,
+ make_parents,
+ FALSE, &sleep,
+ ctx,
+ iterpool);
+
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
+ goto cleanup;
+
+ svn_error_clear(err);
+ err = SVN_NO_ERROR;
+
+ /* SVN_ERR_WC_NOT_WORKING_COPY: it's not versioned */
+
+ result_rev = SVN_INVALID_REVNUM;
+ if (ctx->notify_func2)
+ {
+ svn_wc_notify_t *notify;
+ notify = svn_wc_create_notify(path,
+ svn_wc_notify_skip,
+ iterpool);
+ (*ctx->notify_func2)(ctx->notify_baton2, notify, iterpool);
+ }
+ }
+ if (result_revs)
+ APR_ARRAY_PUSH(*result_revs, svn_revnum_t) = result_rev;
+ }
+ svn_pool_destroy(iterpool);
+
+ cleanup:
+ if (sleep)
+ svn_io_sleep_for_timestamps((paths->nelts == 1) ? path : NULL, pool);
+
+ return svn_error_trace(err);
+}
diff --git a/subversion/libsvn_client/upgrade.c b/subversion/libsvn_client/upgrade.c
new file mode 100644
index 0000000..b9f3235
--- /dev/null
+++ b/subversion/libsvn_client/upgrade.c
@@ -0,0 +1,327 @@
+/*
+ * upgrade.c: wrapper around wc upgrade functionality.
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+
+
+/*** Includes. ***/
+
+#include "svn_time.h"
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "svn_config.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_pools.h"
+#include "client.h"
+#include "svn_props.h"
+
+#include "svn_private_config.h"
+#include "private/svn_wc_private.h"
+
+
+/*** Code. ***/
+
+/* callback baton for fetch_repos_info */
+struct repos_info_baton
+{
+ apr_pool_t *state_pool;
+ svn_client_ctx_t *ctx;
+ const char *last_repos;
+ const char *last_uuid;
+};
+
+/* svn_wc_upgrade_get_repos_info_t implementation for calling
+ svn_wc_upgrade() from svn_client_upgrade() */
+static svn_error_t *
+fetch_repos_info(const char **repos_root,
+ const char **repos_uuid,
+ void *baton,
+ const char *url,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct repos_info_baton *ri = baton;
+
+ /* The same info is likely to retrieved multiple times (e.g. externals) */
+ if (ri->last_repos && svn_uri__is_ancestor(ri->last_repos, url))
+ {
+ *repos_root = apr_pstrdup(result_pool, ri->last_repos);
+ *repos_uuid = apr_pstrdup(result_pool, ri->last_uuid);
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR(svn_client_get_repos_root(repos_root, repos_uuid, url, ri->ctx,
+ result_pool, scratch_pool));
+
+ /* Store data for further calls */
+ ri->last_repos = apr_pstrdup(ri->state_pool, *repos_root);
+ ri->last_uuid = apr_pstrdup(ri->state_pool, *repos_uuid);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_upgrade(const char *path,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *scratch_pool)
+{
+ const char *local_abspath;
+ apr_hash_t *externals;
+ apr_hash_index_t *hi;
+ apr_pool_t *iterpool;
+ apr_pool_t *iterpool2;
+ svn_opt_revision_t rev = {svn_opt_revision_unspecified, {0}};
+ struct repos_info_baton info_baton;
+
+ info_baton.state_pool = scratch_pool;
+ info_baton.ctx = ctx;
+ info_baton.last_repos = NULL;
+ info_baton.last_uuid = NULL;
+
+ if (svn_path_is_url(path))
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("'%s' is not a local path"), path);
+
+ SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, scratch_pool));
+ SVN_ERR(svn_wc_upgrade(ctx->wc_ctx, local_abspath,
+ fetch_repos_info, &info_baton,
+ ctx->cancel_func, ctx->cancel_baton,
+ ctx->notify_func2, ctx->notify_baton2,
+ scratch_pool));
+
+ /* Now it's time to upgrade the externals too. We do it after the wc
+ upgrade to avoid that errors in the externals causes the wc upgrade to
+ fail. Thanks to caching the performance penalty of walking the wc a
+ second time shouldn't be too severe */
+ SVN_ERR(svn_client_propget5(&externals, NULL, SVN_PROP_EXTERNALS,
+ local_abspath, &rev, &rev, NULL,
+ svn_depth_infinity, NULL, ctx,
+ scratch_pool, scratch_pool));
+
+ iterpool = svn_pool_create(scratch_pool);
+ iterpool2 = svn_pool_create(scratch_pool);
+
+ for (hi = apr_hash_first(scratch_pool, externals); hi;
+ hi = apr_hash_next(hi))
+ {
+ int i;
+ const char *externals_parent_abspath;
+ const char *externals_parent_url;
+ const char *externals_parent_repos_root_url;
+ const char *externals_parent_repos_relpath;
+ const char *externals_parent = svn__apr_hash_index_key(hi);
+ svn_string_t *external_desc = svn__apr_hash_index_val(hi);
+ apr_array_header_t *externals_p;
+ svn_error_t *err;
+
+ svn_pool_clear(iterpool);
+ externals_p = apr_array_make(iterpool, 1,
+ sizeof(svn_wc_external_item2_t*));
+
+ /* In this loop, an error causes the respective externals definition, or
+ * the external (inner loop), to be skipped, so that upgrade carries on
+ * with the other externals. */
+
+ err = svn_dirent_get_absolute(&externals_parent_abspath,
+ externals_parent, iterpool);
+
+ if (!err)
+ err = svn_wc__node_get_repos_info(NULL,
+ &externals_parent_repos_relpath,
+ &externals_parent_repos_root_url,
+ NULL,
+ ctx->wc_ctx,
+ externals_parent_abspath,
+ iterpool, iterpool);
+
+ if (!err)
+ externals_parent_url = svn_path_url_add_component2(
+ externals_parent_repos_root_url,
+ externals_parent_repos_relpath,
+ iterpool);
+ if (!err)
+ err = svn_wc_parse_externals_description3(
+ &externals_p, svn_dirent_dirname(path, iterpool),
+ external_desc->data, FALSE, iterpool);
+ if (err)
+ {
+ svn_wc_notify_t *notify =
+ svn_wc_create_notify(externals_parent,
+ svn_wc_notify_failed_external,
+ scratch_pool);
+ notify->err = err;
+
+ ctx->notify_func2(ctx->notify_baton2,
+ notify, scratch_pool);
+
+ svn_error_clear(err);
+
+ /* Next externals definition, please... */
+ continue;
+ }
+
+ for (i = 0; i < externals_p->nelts; i++)
+ {
+ svn_wc_external_item2_t *item;
+ const char *resolved_url;
+ const char *external_abspath;
+ const char *repos_relpath;
+ const char *repos_root_url;
+ const char *repos_uuid;
+ svn_node_kind_t external_kind;
+ svn_revnum_t peg_revision;
+ svn_revnum_t revision;
+
+ item = APR_ARRAY_IDX(externals_p, i, svn_wc_external_item2_t*);
+
+ svn_pool_clear(iterpool2);
+ external_abspath = svn_dirent_join(externals_parent_abspath,
+ item->target_dir,
+ iterpool2);
+
+ err = svn_wc__resolve_relative_external_url(
+ &resolved_url,
+ item,
+ externals_parent_repos_root_url,
+ externals_parent_url,
+ scratch_pool, scratch_pool);
+ if (err)
+ goto handle_error;
+
+ /* This is a hack. We only need to call svn_wc_upgrade() on external
+ * dirs, as file externals are upgraded along with their defining
+ * WC. Reading the kind will throw an exception on an external dir,
+ * saying that the wc must be upgraded. If it's a file, the lookup
+ * is done in an adm_dir belonging to the defining wc (which has
+ * already been upgraded) and no error is returned. If it doesn't
+ * exist (external that isn't checked out yet), we'll just get
+ * svn_node_none. */
+ err = svn_wc_read_kind2(&external_kind, ctx->wc_ctx,
+ external_abspath, TRUE, FALSE, iterpool2);
+ if (err && err->apr_err == SVN_ERR_WC_UPGRADE_REQUIRED)
+ {
+ svn_error_clear(err);
+
+ err = svn_client_upgrade(external_abspath, ctx, iterpool2);
+ if (err)
+ goto handle_error;
+ }
+ else if (err)
+ goto handle_error;
+
+ /* The upgrade of any dir should be done now, get the now reliable
+ * kind. */
+ err = svn_wc_read_kind2(&external_kind, ctx->wc_ctx, external_abspath,
+ TRUE, FALSE, iterpool2);
+ if (err)
+ goto handle_error;
+
+ /* Update the EXTERNALS table according to the root URL,
+ * relpath and uuid known in the upgraded external WC. */
+
+ /* We should probably have a function that provides all three
+ * of root URL, repos relpath and uuid at once, but here goes... */
+
+ /* First get the relpath, as that returns SVN_ERR_WC_PATH_NOT_FOUND
+ * when the node is not present in the file system.
+ * svn_wc__node_get_repos_info() would try to derive the URL. */
+ err = svn_wc__node_get_repos_info(NULL,
+ &repos_relpath,
+ &repos_root_url,
+ &repos_uuid,
+ ctx->wc_ctx,
+ external_abspath,
+ iterpool2, iterpool2);
+ if (err)
+ goto handle_error;
+
+ /* If we haven't got any information from the checked out external,
+ * or if the URL information mismatches the external's definition,
+ * ask fetch_repos_info() to find out the repos root. */
+ if (0 != strcmp(resolved_url,
+ svn_path_url_add_component2(repos_root_url,
+ repos_relpath,
+ scratch_pool)))
+ {
+ err = fetch_repos_info(&repos_root_url,
+ &repos_uuid,
+ &info_baton,
+ resolved_url,
+ scratch_pool, scratch_pool);
+ if (err)
+ goto handle_error;
+
+ repos_relpath = svn_uri_skip_ancestor(repos_root_url,
+ resolved_url,
+ iterpool2);
+
+ /* There's just the URL, no idea what kind the external is.
+ * That's fine, as the external isn't even checked out yet.
+ * The kind will be set during the next 'update'. */
+ external_kind = svn_node_unknown;
+ }
+
+ if (err)
+ goto handle_error;
+
+ peg_revision = (item->peg_revision.kind == svn_opt_revision_number
+ ? item->peg_revision.value.number
+ : SVN_INVALID_REVNUM);
+
+ revision = (item->revision.kind == svn_opt_revision_number
+ ? item->revision.value.number
+ : SVN_INVALID_REVNUM);
+
+ err = svn_wc__upgrade_add_external_info(ctx->wc_ctx,
+ external_abspath,
+ external_kind,
+ externals_parent,
+ repos_relpath,
+ repos_root_url,
+ repos_uuid,
+ peg_revision,
+ revision,
+ iterpool2);
+handle_error:
+ if (err)
+ {
+ svn_wc_notify_t *notify =
+ svn_wc_create_notify(external_abspath,
+ svn_wc_notify_failed_external,
+ scratch_pool);
+ notify->err = err;
+ ctx->notify_func2(ctx->notify_baton2,
+ notify, scratch_pool);
+ svn_error_clear(err);
+ /* Next external node, please... */
+ }
+ }
+ }
+
+ svn_pool_destroy(iterpool);
+ svn_pool_destroy(iterpool2);
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/url.c b/subversion/libsvn_client/url.c
new file mode 100644
index 0000000..36019ad
--- /dev/null
+++ b/subversion/libsvn_client/url.c
@@ -0,0 +1,63 @@
+/*
+ * url.c: converting paths to urls
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+
+
+#include <apr_pools.h>
+
+#include "svn_pools.h"
+#include "svn_error.h"
+#include "svn_types.h"
+#include "svn_opt.h"
+#include "svn_wc.h"
+#include "svn_client.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+
+#include "private/svn_wc_private.h"
+#include "client.h"
+#include "svn_private_config.h"
+
+
+
+svn_error_t *
+svn_client_url_from_path2(const char **url,
+ const char *path_or_url,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ if (!svn_path_is_url(path_or_url))
+ {
+ SVN_ERR(svn_dirent_get_absolute(&path_or_url, path_or_url,
+ scratch_pool));
+
+ return svn_error_trace(
+ svn_wc__node_get_url(url, ctx->wc_ctx, path_or_url,
+ result_pool, scratch_pool));
+ }
+ else
+ *url = svn_uri_canonicalize(path_or_url, result_pool);
+
+ return SVN_NO_ERROR;
+}
diff --git a/subversion/libsvn_client/util.c b/subversion/libsvn_client/util.c
new file mode 100644
index 0000000..5ac0b8f
--- /dev/null
+++ b/subversion/libsvn_client/util.c
@@ -0,0 +1,457 @@
+/*
+ * util.c : utility functions for the libsvn_client library
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+#include <apr_pools.h>
+#include <apr_strings.h>
+
+#include "svn_hash.h"
+#include "svn_pools.h"
+#include "svn_error.h"
+#include "svn_types.h"
+#include "svn_opt.h"
+#include "svn_props.h"
+#include "svn_path.h"
+#include "svn_wc.h"
+#include "svn_client.h"
+
+#include "private/svn_client_private.h"
+#include "private/svn_wc_private.h"
+#include "private/svn_fspath.h"
+
+#include "client.h"
+
+#include "svn_private_config.h"
+
+svn_client__pathrev_t *
+svn_client__pathrev_create(const char *repos_root_url,
+ const char *repos_uuid,
+ svn_revnum_t rev,
+ const char *url,
+ apr_pool_t *result_pool)
+{
+ svn_client__pathrev_t *loc = apr_palloc(result_pool, sizeof(*loc));
+
+ SVN_ERR_ASSERT_NO_RETURN(svn_path_is_url(repos_root_url));
+ SVN_ERR_ASSERT_NO_RETURN(svn_path_is_url(url));
+
+ loc->repos_root_url = apr_pstrdup(result_pool, repos_root_url);
+ loc->repos_uuid = apr_pstrdup(result_pool, repos_uuid);
+ loc->rev = rev;
+ loc->url = apr_pstrdup(result_pool, url);
+ return loc;
+}
+
+svn_client__pathrev_t *
+svn_client__pathrev_create_with_relpath(const char *repos_root_url,
+ const char *repos_uuid,
+ svn_revnum_t rev,
+ const char *relpath,
+ apr_pool_t *result_pool)
+{
+ SVN_ERR_ASSERT_NO_RETURN(svn_relpath_is_canonical(relpath));
+
+ return svn_client__pathrev_create(
+ repos_root_url, repos_uuid, rev,
+ svn_path_url_add_component2(repos_root_url, relpath, result_pool),
+ result_pool);
+}
+
+svn_error_t *
+svn_client__pathrev_create_with_session(svn_client__pathrev_t **pathrev_p,
+ svn_ra_session_t *ra_session,
+ svn_revnum_t rev,
+ const char *url,
+ apr_pool_t *result_pool)
+{
+ svn_client__pathrev_t *pathrev = apr_palloc(result_pool, sizeof(*pathrev));
+
+ SVN_ERR_ASSERT(svn_path_is_url(url));
+
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, &pathrev->repos_root_url,
+ result_pool));
+ SVN_ERR(svn_ra_get_uuid2(ra_session, &pathrev->repos_uuid, result_pool));
+ pathrev->rev = rev;
+ pathrev->url = apr_pstrdup(result_pool, url);
+ *pathrev_p = pathrev;
+ return SVN_NO_ERROR;
+}
+
+svn_client__pathrev_t *
+svn_client__pathrev_dup(const svn_client__pathrev_t *pathrev,
+ apr_pool_t *result_pool)
+{
+ return svn_client__pathrev_create(
+ pathrev->repos_root_url, pathrev->repos_uuid,
+ pathrev->rev, pathrev->url, result_pool);
+}
+
+svn_client__pathrev_t *
+svn_client__pathrev_join_relpath(const svn_client__pathrev_t *pathrev,
+ const char *relpath,
+ apr_pool_t *result_pool)
+{
+ return svn_client__pathrev_create(
+ pathrev->repos_root_url, pathrev->repos_uuid, pathrev->rev,
+ svn_path_url_add_component2(pathrev->url, relpath, result_pool),
+ result_pool);
+}
+
+const char *
+svn_client__pathrev_relpath(const svn_client__pathrev_t *pathrev,
+ apr_pool_t *result_pool)
+{
+ return svn_uri_skip_ancestor(pathrev->repos_root_url, pathrev->url,
+ result_pool);
+}
+
+const char *
+svn_client__pathrev_fspath(const svn_client__pathrev_t *pathrev,
+ apr_pool_t *result_pool)
+{
+ return svn_fspath__canonicalize(svn_uri_skip_ancestor(
+ pathrev->repos_root_url, pathrev->url,
+ result_pool),
+ result_pool);
+}
+
+
+svn_client_commit_item3_t *
+svn_client_commit_item3_create(apr_pool_t *pool)
+{
+ return apr_pcalloc(pool, sizeof(svn_client_commit_item3_t));
+}
+
+svn_client_commit_item3_t *
+svn_client_commit_item3_dup(const svn_client_commit_item3_t *item,
+ apr_pool_t *pool)
+{
+ svn_client_commit_item3_t *new_item = apr_palloc(pool, sizeof(*new_item));
+
+ *new_item = *item;
+
+ if (new_item->path)
+ new_item->path = apr_pstrdup(pool, new_item->path);
+
+ if (new_item->url)
+ new_item->url = apr_pstrdup(pool, new_item->url);
+
+ if (new_item->copyfrom_url)
+ new_item->copyfrom_url = apr_pstrdup(pool, new_item->copyfrom_url);
+
+ if (new_item->incoming_prop_changes)
+ new_item->incoming_prop_changes =
+ svn_prop_array_dup(new_item->incoming_prop_changes, pool);
+
+ if (new_item->outgoing_prop_changes)
+ new_item->outgoing_prop_changes =
+ svn_prop_array_dup(new_item->outgoing_prop_changes, pool);
+
+ return new_item;
+}
+
+svn_error_t *
+svn_client__wc_node_get_base(svn_client__pathrev_t **base_p,
+ const char *wc_abspath,
+ svn_wc_context_t *wc_ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *relpath;
+
+ *base_p = apr_palloc(result_pool, sizeof(**base_p));
+
+ SVN_ERR(svn_wc__node_get_base(NULL,
+ &(*base_p)->rev,
+ &relpath,
+ &(*base_p)->repos_root_url,
+ &(*base_p)->repos_uuid,
+ NULL,
+ wc_ctx, wc_abspath,
+ TRUE /* ignore_enoent */,
+ TRUE /* show_hidden */,
+ result_pool, scratch_pool));
+ if ((*base_p)->repos_root_url && relpath)
+ {
+ (*base_p)->url = svn_path_url_add_component2(
+ (*base_p)->repos_root_url, relpath, result_pool);
+ }
+ else
+ {
+ *base_p = NULL;
+ }
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client__wc_node_get_origin(svn_client__pathrev_t **origin_p,
+ const char *wc_abspath,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *relpath;
+
+ *origin_p = apr_palloc(result_pool, sizeof(**origin_p));
+
+ SVN_ERR(svn_wc__node_get_origin(NULL /* is_copy */,
+ &(*origin_p)->rev,
+ &relpath,
+ &(*origin_p)->repos_root_url,
+ &(*origin_p)->repos_uuid,
+ NULL, ctx->wc_ctx, wc_abspath,
+ FALSE /* scan_deleted */,
+ result_pool, scratch_pool));
+ if ((*origin_p)->repos_root_url && relpath)
+ {
+ (*origin_p)->url = svn_path_url_add_component2(
+ (*origin_p)->repos_root_url, relpath, result_pool);
+ }
+ else
+ {
+ *origin_p = NULL;
+ }
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_client_get_repos_root(const char **repos_root,
+ const char **repos_uuid,
+ const char *abspath_or_url,
+ svn_client_ctx_t *ctx,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_ra_session_t *ra_session;
+
+ /* If PATH_OR_URL is a local path we can fetch the repos root locally. */
+ if (!svn_path_is_url(abspath_or_url))
+ {
+ svn_error_t *err;
+ err = svn_wc__node_get_repos_info(NULL, NULL, repos_root, repos_uuid,
+ ctx->wc_ctx, abspath_or_url,
+ result_pool, scratch_pool);
+
+ if (err)
+ {
+ if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND
+ && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY)
+ return svn_error_trace(err);
+
+ svn_error_clear(err);
+ if (repos_root)
+ *repos_root = NULL;
+ if (repos_uuid)
+ *repos_uuid = NULL;
+ }
+ return SVN_NO_ERROR;
+ }
+
+ /* If PATH_OR_URL was a URL, we use the RA layer to look it up. */
+ SVN_ERR(svn_client_open_ra_session2(&ra_session, abspath_or_url, NULL,
+ ctx, scratch_pool, scratch_pool));
+
+ if (repos_root)
+ SVN_ERR(svn_ra_get_repos_root2(ra_session, repos_root, result_pool));
+ if (repos_uuid)
+ SVN_ERR(svn_ra_get_uuid2(ra_session, repos_uuid, result_pool));
+
+ return SVN_NO_ERROR;
+}
+
+const svn_opt_revision_t *
+svn_cl__rev_default_to_head_or_base(const svn_opt_revision_t *revision,
+ const char *path_or_url)
+{
+ static svn_opt_revision_t head_rev = { svn_opt_revision_head, { 0 } };
+ static svn_opt_revision_t base_rev = { svn_opt_revision_base, { 0 } };
+
+ if (revision->kind == svn_opt_revision_unspecified)
+ return svn_path_is_url(path_or_url) ? &head_rev : &base_rev;
+ return revision;
+}
+
+const svn_opt_revision_t *
+svn_cl__rev_default_to_head_or_working(const svn_opt_revision_t *revision,
+ const char *path_or_url)
+{
+ static svn_opt_revision_t head_rev = { svn_opt_revision_head, { 0 } };
+ static svn_opt_revision_t work_rev = { svn_opt_revision_working, { 0 } };
+
+ if (revision->kind == svn_opt_revision_unspecified)
+ return svn_path_is_url(path_or_url) ? &head_rev : &work_rev;
+ return revision;
+}
+
+const svn_opt_revision_t *
+svn_cl__rev_default_to_peg(const svn_opt_revision_t *revision,
+ const svn_opt_revision_t *peg_revision)
+{
+ if (revision->kind == svn_opt_revision_unspecified)
+ return peg_revision;
+ return revision;
+}
+
+svn_error_t *
+svn_client__assert_homogeneous_target_type(const apr_array_header_t *targets)
+{
+ svn_boolean_t wc_present = FALSE, url_present = FALSE;
+ int i;
+
+ for (i = 0; i < targets->nelts; ++i)
+ {
+ const char *target = APR_ARRAY_IDX(targets, i, const char *);
+ if (! svn_path_is_url(target))
+ wc_present = TRUE;
+ else
+ url_present = TRUE;
+ if (url_present && wc_present)
+ return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
+ _("Cannot mix repository and working copy "
+ "targets"));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+struct shim_callbacks_baton
+{
+ svn_wc_context_t *wc_ctx;
+ apr_hash_t *relpath_map;
+};
+
+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 shim_callbacks_baton *scb = baton;
+ const char *local_abspath;
+
+ local_abspath = svn_hash_gets(scb->relpath_map, path);
+ if (!local_abspath)
+ {
+ *props = apr_hash_make(result_pool);
+ return SVN_NO_ERROR;
+ }
+
+ /* Reads the pristine properties of WORKING, not those of BASE */
+ SVN_ERR(svn_wc_get_pristine_props(props, scb->wc_ctx, local_abspath,
+ result_pool, scratch_pool));
+
+ if (!*props)
+ *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 shim_callbacks_baton *scb = baton;
+ const char *local_abspath;
+
+ local_abspath = svn_hash_gets(scb->relpath_map, path);
+ if (!local_abspath)
+ {
+ *kind = svn_node_unknown;
+ return SVN_NO_ERROR;
+ }
+ /* Reads the WORKING kind. Not the BASE kind */
+ SVN_ERR(svn_wc_read_kind2(kind, scb->wc_ctx, local_abspath,
+ TRUE, FALSE, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+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 shim_callbacks_baton *scb = baton;
+ const char *local_abspath;
+ svn_stream_t *pristine_stream;
+ svn_stream_t *temp_stream;
+ svn_error_t *err;
+
+ local_abspath = svn_hash_gets(scb->relpath_map, path);
+ if (!local_abspath)
+ {
+ *filename = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ /* Reads the pristine of WORKING, not of BASE */
+ err = svn_wc_get_pristine_contents2(&pristine_stream, scb->wc_ctx,
+ local_abspath, scratch_pool,
+ scratch_pool);
+ if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND)
+ {
+ svn_error_clear(err);
+ *filename = NULL;
+ return SVN_NO_ERROR;
+ }
+ else if (err)
+ return svn_error_trace(err);
+
+ SVN_ERR(svn_stream_open_unique(&temp_stream, filename, NULL,
+ svn_io_file_del_on_pool_cleanup,
+ result_pool, scratch_pool));
+ SVN_ERR(svn_stream_copy3(pristine_stream, temp_stream, NULL, NULL,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_delta_shim_callbacks_t *
+svn_client__get_shim_callbacks(svn_wc_context_t *wc_ctx,
+ apr_hash_t *relpath_map,
+ apr_pool_t *result_pool)
+{
+ svn_delta_shim_callbacks_t *callbacks =
+ svn_delta_shim_callbacks_default(result_pool);
+ struct shim_callbacks_baton *scb = apr_pcalloc(result_pool, sizeof(*scb));
+
+ scb->wc_ctx = wc_ctx;
+ if (relpath_map)
+ scb->relpath_map = relpath_map;
+ else
+ scb->relpath_map = apr_hash_make(result_pool);
+
+ callbacks->fetch_props_func = fetch_props_func;
+ callbacks->fetch_kind_func = fetch_kind_func;
+ callbacks->fetch_base_func = fetch_base_func;
+ callbacks->fetch_baton = scb;
+
+ return callbacks;
+}
diff --git a/subversion/libsvn_client/version.c b/subversion/libsvn_client/version.c
new file mode 100644
index 0000000..2ad6417
--- /dev/null
+++ b/subversion/libsvn_client/version.c
@@ -0,0 +1,33 @@
+/*
+ * version.c: library version number
+ *
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ */
+
+
+
+#include "svn_version.h"
+#include "svn_client.h"
+
+const svn_version_t *
+svn_client_version(void)
+{
+ SVN_VERSION_BODY;
+}
OpenPOWER on IntegriCloud