diff options
Diffstat (limited to 'subversion/libsvn_client/merge.c')
-rw-r--r-- | subversion/libsvn_client/merge.c | 12674 |
1 files changed, 12674 insertions, 0 deletions
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; +} |