diff options
author | peter <peter@FreeBSD.org> | 2013-06-18 02:07:41 +0000 |
---|---|---|
committer | peter <peter@FreeBSD.org> | 2013-06-18 02:07:41 +0000 |
commit | d25dac7fcc6acc838b71bbda8916fd9665c709ab (patch) | |
tree | 135691142dc0e75a5e5d97b5074d03436435b8e0 /subversion/libsvn_client | |
download | FreeBSD-src-d25dac7fcc6acc838b71bbda8916fd9665c709ab.zip FreeBSD-src-d25dac7fcc6acc838b71bbda8916fd9665c709ab.tar.gz |
Import trimmed svn-1.8.0-rc3
Diffstat (limited to 'subversion/libsvn_client')
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(¤t_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(¤t_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, + ©_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(¬_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, ©_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, ©from_rev, + ©from_repos_relpath, + ©from_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(×tamp_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(×tamp_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(×tamp_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(×tamp_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 = ¬ify_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 *) = ©_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, ©from_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(©_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, ©from_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, ®ular_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, ®ular_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 *) = ⦥ + } + + 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(©from_path, ©from_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, ¤t, 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, + ©_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, + ©_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; +} |