/* * merge.c: merging * * ==================================================================== * Copyright (c) 2000-2007 CollabNet. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at http://subversion.tigris.org/license-1.html. * If newer versions of this license are posted there, you may use a * newer version instead, at your option. * * This software consists of voluntary contributions made by many * individuals. For exact contribution history, see the revision * history and logs, available at http://subversion.tigris.org/. * ==================================================================== */ /* ==================================================================== */ /*** Includes ***/ #include #include #include #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_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_ra.h" #include "client.h" #include "mergeinfo.h" #include #include "private/svn_wc_private.h" #include "private/svn_mergeinfo_private.h" #include "svn_private_config.h" /*-----------------------------------------------------------------------*/ /* MERGEINFO MERGE SOURCE NORMALIZATION * * Nearly any helper function herein that accepts two URL/revision * pairs 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, a merge between r19500 and r27567 of * Subversion's own /tags/1.4.5 directory gets split into sequential * merges of the following location pairs: * * [/trunk:19549, /trunk:19523] * (recorded in svn:mergeinfo as /trunk:19500-19523) * * [/trunk:19523, /branches/1.4.x:25188] * (recorded in svn:mergeinfo as /branches/1.4.x:19524-25188) * * [/branches/1.4.x:25188, /tags/1.4.4@26345] * (recorded in svn:mergeinfo as /tags/1.4.4:25189-26345) * * [/tags/1.4.4@26345, /branches/1.4.5@26350] * (recorded in svn:mergeinfo as /branches/1.4.5:26346-26350) * * [/branches/1.4.5@26350, /tags/1.4.5@27567] * (recorded in svn:mergeinfo as /tags/1.4.5:26351-27567) * * 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 | unsupported | * -r | (svn_client_merge_peg) | | * +--------------------------------+---------------------+ * no | mergeinfo-driven | mergeinfo-oblivious| * -c | whole-branch | diff-and-apply | * or | heuristic merge | (svn_client_merge) | * -r | (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. * * CHILDREN_WITH_MERGEINFO is intially 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. */ /*-----------------------------------------------------------------------*/ /*** Utilities ***/ /* Sanity check -- ensure that we have valid revisions to look at. */ #define ENSURE_VALID_REVISION_KINDS(rev1_kind, rev2_kind) \ /* Return SVN_ERR_UNSUPPORTED_FEATURE if URL's scheme does not match the scheme of the url for ADM_ACCESS's path; return SVN_ERR_BAD_URL if no scheme can be found for one or both urls; otherwise return SVN_NO_ERROR. Use ADM_ACCESS's pool for temporary allocation. */ static svn_error_t * check_scheme_match(svn_wc_adm_access_t *adm_access, const char *url) { const char *path = svn_wc_adm_access_path(adm_access); apr_pool_t *pool = svn_wc_adm_access_pool(adm_access); const svn_wc_entry_t *ent; const char *idx1, *idx2; SVN_ERR(svn_wc_entry(&ent, path, adm_access, TRUE, pool)); idx1 = strchr(url, ':'); idx2 = strchr(ent->url, ':'); if ((idx1 == NULL) && (idx2 == NULL)) { return svn_error_createf (SVN_ERR_BAD_URL, NULL, _("URLs have no scheme ('%s' and '%s')"), url, ent->url); } else if (idx1 == NULL) { return svn_error_createf (SVN_ERR_BAD_URL, NULL, _("URL has no scheme: '%s'"), url); } else if (idx2 == NULL) { return svn_error_createf (SVN_ERR_BAD_URL, NULL, _("URL has no scheme: '%s'"), ent->url); } else if (((idx1 - url) != (idx2 - ent->url)) || (strncmp(url, ent->url, idx1 - url) != 0)) { return svn_error_createf (SVN_ERR_UNSUPPORTED_FEATURE, NULL, _("Access scheme mixtures not yet supported ('%s' and '%s')"), url, ent->url); } /* else */ return SVN_NO_ERROR; } /*-----------------------------------------------------------------------*/ /*** Repos-Diff Editor Callbacks ***/ /* Wrapper around svn_string_t, see merge_cmd_baton_t's working_mergeinfo member. */ typedef struct { /* Working mergeinfo for a path prior to a merge. May be NULL if the path has no working mergeinfo. */ svn_string_t *working_mergeinfo_propval; } working_mergeinfo_t; typedef struct merge_cmd_baton_t { svn_boolean_t force; svn_boolean_t dry_run; svn_boolean_t record_only; /* Whether to only record mergeinfo. */ svn_boolean_t sources_ancestral; /* Whether the left-side merge source is an ancestor of the right-side, or vice-versa (history-wise). */ 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_ancestry; /* Are we ignoring ancestry (and by extension, mergeinfo)? FALSE if SOURCES_ANCESTRAL is FALSE. */ svn_boolean_t target_missing_child; /* Whether working copy target of the merge is missing any immediate children. */ const char *added_path; /* Set to the dir path whenever the dir is added as a child of a versioned dir (dry-run only) */ const char *target; /* Working copy target of merge */ const char *url; /* The second URL in the merge */ svn_client_ctx_t *ctx; /* Client context for callbacks, etc. */ /* Whether invocation of the merge_file_added() callback required delegation to the merge_file_changed() function for the file currently being merged. This info is used to detect whether a file on the left side of a 3-way merge actually exists (important because it's created as an empty temp file on disk regardless).*/ svn_boolean_t add_necessitated_merge; /* The list of paths for entries we've deleted, used only when in dry_run mode. */ apr_hash_t *dry_run_deletions; /* 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. */ apr_hash_t *conflicted_paths; /* A list of 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. */ apr_hash_t *paths_with_new_mergeinfo; /* 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; /* Flag indicating the fact target has everything merged already, for the sake of children's merge to work it sets itself a dummy merge range of requested_end_rev:requested_end_rev. */ svn_boolean_t target_has_dummy_merge_range; /* 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; } merge_cmd_baton_t; apr_hash_t * svn_client__dry_run_deletions(void *merge_cmd_baton) { merge_cmd_baton_t *merge_b = merge_cmd_baton; return merge_b->dry_run_deletions; } /* Used to avoid spurious notifications (e.g. conflicts) from a merge attempt into an existing target which would have been deleted if we weren't in dry_run mode (issue #2584). Assumes that WCPATH is still versioned (e.g. has an associated entry). */ static APR_INLINE svn_boolean_t dry_run_deleted_p(merge_cmd_baton_t *merge_b, const char *wcpath) { return (merge_b->dry_run && apr_hash_get(merge_b->dry_run_deletions, wcpath, APR_HASH_KEY_STRING) != NULL); } /* 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); } /* Set *HONOR_MERGEINFO and *RECORD_MERGEINFO (if non-NULL) appropriately for MERGE_B. One rule is that we shan't record mergeinfo if we're not honoring it. */ static APR_INLINE void mergeinfo_behavior(svn_boolean_t *honor_mergeinfo, svn_boolean_t *record_mergeinfo, merge_cmd_baton_t *merge_b) { if (honor_mergeinfo) *honor_mergeinfo = (merge_b->mergeinfo_capable && merge_b->sources_ancestral && merge_b->same_repos && (! merge_b->ignore_ancestry)); if (record_mergeinfo) *record_mergeinfo = (merge_b->mergeinfo_capable && merge_b->sources_ancestral && merge_b->same_repos && (! merge_b->ignore_ancestry) && (! merge_b->dry_run)); } /* 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; *younger_mergeinfo = NULL; for (hi = apr_hash_first(NULL, *mergeinfo); hi; hi = apr_hash_next(hi)) { int i; const void *key; void *value; apr_array_header_t *rangelist; const char *merge_source_path; apr_hash_this(hi, &key, NULL, &value); rangelist = value; merge_source_path = key; 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; apr_array_header_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); apr_hash_set(*younger_mergeinfo, (const char *)merge_source_path, APR_HASH_KEY_STRING, younger_rangelist); SVN_ERR(svn_mergeinfo_remove(mergeinfo, *younger_mergeinfo, *mergeinfo, pool)); break; /* ...out of for (i = 0; i < rangelist->nelts; i++) */ } } } return SVN_NO_ERROR; } /* Helper for merge_props_changed(). Filter out mergeinfo property additions to PATH when those additions refer to the same line of history as PATH. *PROPS is an array of svn_prop_t structures representing regular properties to be added to the working copy PATH. ADM_ACCESS and MERGE_B are cascaded from the arguments of the same name in merge_props_changed(). If mergeinfo is not being honored, do nothing. Otherwise 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 PATH (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. Set outgoing *PROPS to a shallow copy (allocated in POOL) of incoming *PROPS minus the filtered self-referential mergeinfo. */ static svn_error_t* filter_self_referential_mergeinfo(apr_array_header_t **props, const char *path, merge_cmd_baton_t *merge_b, svn_wc_adm_access_t *adm_access, apr_pool_t *pool) { svn_boolean_t honor_mergeinfo; apr_array_header_t *adjusted_props; int i; const svn_wc_entry_t *target_entry; /* If we aren't honoring mergeinfo, get outta here. */ mergeinfo_behavior(&honor_mergeinfo, NULL, merge_b); if (! honor_mergeinfo) return SVN_NO_ERROR; /* If PATH itself is newly added there is no need to filter. */ SVN_ERR(svn_wc__entry_versioned(&target_entry, path, adm_access, FALSE, pool)); if (target_entry->schedule == svn_wc_schedule_add || target_entry->schedule == svn_wc_schedule_replace) return SVN_NO_ERROR; adjusted_props = apr_array_make(pool, (*props)->nelts, sizeof(svn_prop_t)); for (i = 0; i < (*props)->nelts; ++i) { svn_prop_t *prop = &APR_ARRAY_IDX((*props), i, svn_prop_t); /* 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; } else /* Non-empty mergeinfo; filter self-referential mergeinfo out. */ { svn_mergeinfo_t mergeinfo, younger_mergeinfo; svn_mergeinfo_t filtered_mergeinfo = NULL; svn_mergeinfo_t filtered_younger_mergeinfo = NULL; const char *target_url; const char *old_url = NULL; /* Temporarily reparent our RA session to the merge target's URL. */ SVN_ERR(svn_client_url_from_path(&target_url, path, pool)); SVN_ERR(svn_client__ensure_ra_session_url(&old_url, merge_b->ra_session2, target_url, pool)); /* Parse the incoming mergeinfo to allow easier manipulation. */ SVN_ERR(svn_mergeinfo_parse(&mergeinfo, prop->value->data, pool)); /* The working copy target PATH is at base revision target_entry->revision. Divide the incoming mergeinfo into two groups. One where all revision ranges are as old or older than target_entry->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_entry->revision, pool)); /* 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(merge_b->ra_session2, &merge_source_root_url, pool)); for (hi = apr_hash_first(NULL, younger_mergeinfo); hi; hi = apr_hash_next(hi)) { int j; const void *key; void *value; const char *source_path; apr_array_header_t *rangelist; const char *merge_source_url; apr_array_header_t *adjusted_rangelist = apr_array_make(pool, 0, sizeof(svn_merge_range_t *)); apr_hash_this(hi, &key, NULL, &value); source_path = key; rangelist = value; merge_source_url = svn_path_url_add_component(merge_source_root_url, source_path + 1, pool); for (j = 0; j < rangelist->nelts; j++) { svn_error_t *err; svn_opt_revision_t *start_revision; const char *start_url; svn_opt_revision_t peg_rev, rev1_opt, rev2_opt; svn_merge_range_t *range = APR_ARRAY_IDX(rangelist, j, svn_merge_range_t *); peg_rev.kind = svn_opt_revision_number; peg_rev.value.number = target_entry->revision; rev1_opt.kind = svn_opt_revision_number; /* SVN_PROP_MERGEINFO only stores forward merges, so the start range of svn_merge_range_t RANGE is not inclusive. */ rev1_opt.value.number = range->start + 1; /* 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. */ rev2_opt.kind = svn_opt_revision_unspecified; /* Check if PATH@TARGET_ENTRY->REVISION exists at RANGE->START on the same line of history. */ err = svn_client__repos_locations(&start_url, &start_revision, NULL, NULL, merge_b->ra_session2, target_url, &peg_rev, &rev1_opt, &rev2_opt, merge_b->ctx, pool); if (err) { if (err->apr_err == SVN_ERR_CLIENT_UNRELATED_RESOURCES || err->apr_err == SVN_ERR_FS_NOT_FOUND || err->apr_err == SVN_ERR_FS_NO_SUCH_REVISION) { /* PATH@TARGET_ENTRY->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(err); err = NULL; APR_ARRAY_PUSH(adjusted_rangelist, svn_merge_range_t *) = range; } else { return err; } } else { /* PATH@TARGET_ENTRY->REVISION exists on the same line of history at RANGE->START and RANGE->END. Now check that PATH@TARGET_ENTRY->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_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(pool); apr_hash_set(filtered_younger_mergeinfo, source_path, APR_HASH_KEY_STRING, adjusted_rangelist); } } /* Iteration over each merge source in younger_mergeinfo. */ } /* if (apr_hash_count(younger_mergeinfo)) */ /* Filter self-referential mergeinfo from "older" mergeinfo. */ if (mergeinfo) { svn_mergeinfo_t implicit_mergeinfo; svn_opt_revision_t peg_rev; peg_rev.kind = svn_opt_revision_number; peg_rev.value.number = target_entry->revision; SVN_ERR(svn_client__get_history_as_mergeinfo( &implicit_mergeinfo, path, &peg_rev, target_entry->revision, SVN_INVALID_REVNUM, merge_b->ra_session2, adm_access, merge_b->ctx, pool)); /* Remove PATH's implicit mergeinfo from the incoming mergeinfo. */ SVN_ERR(svn_mergeinfo_remove(&filtered_mergeinfo, implicit_mergeinfo, mergeinfo, pool)); } /* If we reparented MERGE_B->RA_SESSION2 above, put it back to the original URL. */ if (old_url) SVN_ERR(svn_ra_reparent(merge_b->ra_session2, old_url, pool)); /* Combine whatever older and younger filtered mergeinfo exists into filtered_mergeinfo. */ if (filtered_mergeinfo && filtered_younger_mergeinfo) SVN_ERR(svn_mergeinfo_merge(filtered_mergeinfo, filtered_younger_mergeinfo, pool)); 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; } } } *props = adjusted_props; return SVN_NO_ERROR; } /* A svn_wc_diff_callbacks2_t function. Used for both file and directory property merges. */ static svn_error_t * merge_props_changed(svn_wc_adm_access_t *adm_access, svn_wc_notify_state_t *state, const char *path, const apr_array_header_t *propchanges, apr_hash_t *original_props, void *baton) { apr_array_header_t *props; merge_cmd_baton_t *merge_b = baton; svn_client_ctx_t *ctx = merge_b->ctx; apr_pool_t *subpool = svn_pool_create(merge_b->pool); svn_error_t *err; SVN_ERR(svn_categorize_props(propchanges, NULL, NULL, &props, subpool)); /* We only want to merge "regular" version properties: by definition, 'svn merge' shouldn't touch any data within .svn/ */ if (props->nelts) { int i; /* svn_wc_merge_props() requires ADM_ACCESS to be the access for the parent of PATH. Since the advent of merge tracking, do_directory_merge() may call this (indirectly) with the access for the merge_b->target instead (issue #2781). So, if we have the wrong access, get the right one. */ if (svn_path_compare_paths(svn_wc_adm_access_path(adm_access), path) != 0) SVN_ERR(svn_wc_adm_probe_try3(&adm_access, adm_access, path, TRUE, -1, ctx->cancel_func, ctx->cancel_baton, subpool)); /* Don't add mergeinfo from PATH's own history. */ SVN_ERR(filter_self_referential_mergeinfo(&props, path, merge_b, adm_access, subpool)); err = svn_wc_merge_props2(state, path, adm_access, original_props, props, FALSE, merge_b->dry_run, ctx->conflict_func, ctx->conflict_baton, subpool); /* Make a record in BATON if we find a PATH where mergeinfo is added where none existed previously. */ for (i = 0; i < props->nelts; ++i) { svn_prop_t *prop = &APR_ARRAY_IDX(props, i, svn_prop_t); /* Is this prop change the addition of mergeinfo to PATH? */ if ((strcmp(prop->name, SVN_PROP_MERGEINFO) == 0) && prop->value) /* No value if a prop delete. */ { /* Does PATH have any working mergeinfo? */ svn_prop_t *mergeinfo_prop = apr_hash_get(original_props, SVN_PROP_MERGEINFO, APR_HASH_KEY_STRING); if (!mergeinfo_prop) { /* If BATON->PATHS_WITH_NEW_MERGEINFO needs to be allocated do so in BATON->POOL so it has a sufficient lifetime. */ if (!merge_b->paths_with_new_mergeinfo) merge_b->paths_with_new_mergeinfo = apr_hash_make(merge_b->pool); apr_hash_set(merge_b->paths_with_new_mergeinfo, apr_pstrdup(merge_b->pool, path), APR_HASH_KEY_STRING, path); } } } if (err && (err->apr_err == SVN_ERR_ENTRY_NOT_FOUND || err->apr_err == SVN_ERR_UNVERSIONED_RESOURCE)) { /* if the entry doesn't exist in the wc, just 'skip' over this part of the tree-delta. */ if (state) *state = svn_wc_notify_state_missing; svn_error_clear(err); svn_pool_destroy(subpool); return SVN_NO_ERROR; } else if (err) return err; } svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* Contains any state collected while resolving conflicts. */ typedef struct { /* The wrapped callback and baton. */ svn_wc_conflict_resolver_func_t wrapped_func; void *wrapped_baton; /* The list of any paths which remained in conflict after a resolution attempt was made. */ apr_hash_t **conflicted_paths; /* Pool used in notification_receiver() to avoid the iteration sub-pool which is passed in, then subsequently destroyed. */ apr_pool_t *pool; } conflict_resolver_baton_t; /* An implementation of the svn_wc_conflict_resolver_func_t interface. We keep a record of paths which remain in conflict after any resolution attempt from BATON->wrapped_func. */ static svn_error_t * conflict_resolver(svn_wc_conflict_result_t **result, const svn_wc_conflict_description_t *description, void *baton, apr_pool_t *pool) { svn_error_t *err; conflict_resolver_baton_t *conflict_b = baton; if (conflict_b->wrapped_func) err = (*conflict_b->wrapped_func)(result, description, conflict_b->wrapped_baton, pool); else { /* If we have no wrapped callback to invoke, then we still need to behave like a proper conflict-callback ourselves. */ *result = svn_wc_create_conflict_result(svn_wc_conflict_choose_postpone, NULL, pool); err = SVN_NO_ERROR; } /* Keep a record of paths still in conflict after the resolution attempt. */ if ((! conflict_b->wrapped_func) || (*result && ((*result)->choice == svn_wc_conflict_choose_postpone))) { const char *conflicted_path = apr_pstrdup(conflict_b->pool, description->path); if (*conflict_b->conflicted_paths == NULL) *conflict_b->conflicted_paths = apr_hash_make(conflict_b->pool); apr_hash_set(*conflict_b->conflicted_paths, conflicted_path, APR_HASH_KEY_STRING, conflicted_path); } return err; } /* A svn_wc_diff_callbacks2_t function. */ static svn_error_t * merge_file_changed(svn_wc_adm_access_t *adm_access, svn_wc_notify_state_t *content_state, svn_wc_notify_state_t *prop_state, const char *mine, const char *older, const char *yours, svn_revnum_t older_rev, svn_revnum_t yours_rev, const char *mimetype1, const char *mimetype2, const apr_array_header_t *prop_changes, apr_hash_t *original_props, void *baton) { merge_cmd_baton_t *merge_b = baton; apr_pool_t *subpool = svn_pool_create(merge_b->pool); svn_boolean_t merge_required = TRUE; enum svn_wc_merge_outcome_t merge_outcome; /* Easy out: no access baton means there ain't no merge target */ if (adm_access == NULL) { if (content_state) *content_state = svn_wc_notify_state_missing; if (prop_state) *prop_state = svn_wc_notify_state_missing; svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* Other easy outs: if the merge target isn't under version control, or is just missing from disk, fogettaboutit. There's no way svn_wc_merge3() can do the merge. */ { const svn_wc_entry_t *entry; svn_node_kind_t kind; SVN_ERR(svn_wc_entry(&entry, mine, adm_access, FALSE, subpool)); SVN_ERR(svn_io_check_path(mine, &kind, subpool)); /* ### a future thought: if the file is under version control, but the working file is missing, maybe we can 'restore' the working file from the text-base, and then allow the merge to run? */ if ((! entry) || (kind != svn_node_file)) { if (content_state) *content_state = svn_wc_notify_state_missing; if (prop_state) *prop_state = svn_wc_notify_state_missing; svn_pool_destroy(subpool); return SVN_NO_ERROR; } } /* ### TODO: Thwart attempts to merge into a path that has ### unresolved conflicts. This needs to be smart enough to deal ### with tree conflicts! if (is_path_conflicted_by_merge(merge_b, mine)) { *content_state = svn_wc_notify_state_conflicted; return svn_error_createf(SVN_ERR_WC_FOUND_CONFLICT, NULL, _("Path '%s' is in conflict, and must be " "resolved before the remainder of the " "requested merge can be applied"), mine); } */ /* This callback is essentially no more than a wrapper around svn_wc_merge3(). Thank goodness that all the diff-editor-mechanisms are doing the hard work of getting the fulltexts! */ /* Do property merge before text merge so that keyword expansion takes into account the new property values. */ if (prop_changes->nelts > 0) SVN_ERR(merge_props_changed(adm_access, prop_state, mine, prop_changes, original_props, baton)); else if (prop_state) *prop_state = svn_wc_notify_state_unchanged; if (older) { svn_boolean_t has_local_mods; SVN_ERR(svn_wc_text_modified_p(&has_local_mods, mine, FALSE, adm_access, subpool)); /* Special case: if a binary file's working file is exactly identical to the 'left' side of the merge, then don't allow svn_wc_merge to produce a conflict. Instead, just overwrite the working file with the 'right' side of the merge. Why'd we check for local mods above? Because we want to do a different notification depending on whether or not the file was locally modified. Alternately, if the 'left' side of the merge doesn't exist in the repository, and the 'right' side of the merge is identical to the WC, pretend we did the merge (a no-op). */ if ((mimetype1 && svn_mime_type_is_binary(mimetype1)) || (mimetype2 && svn_mime_type_is_binary(mimetype2))) { /* For adds, the 'left' side of the merge doesn't exist. */ svn_boolean_t older_revision_exists = !merge_b->add_necessitated_merge; svn_boolean_t same_contents; SVN_ERR(svn_io_files_contents_same_p(&same_contents, (older_revision_exists ? older : yours), mine, subpool)); if (same_contents) { if (older_revision_exists && !merge_b->dry_run) SVN_ERR(svn_io_file_rename(yours, mine, subpool)); merge_outcome = svn_wc_merge_merged; merge_required = FALSE; } } if (merge_required) { /* 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(subpool, _(".merge-left.r%ld"), older_rev); const char *right_label = apr_psprintf(subpool, _(".merge-right.r%ld"), yours_rev); conflict_resolver_baton_t conflict_baton = { merge_b->ctx->conflict_func, merge_b->ctx->conflict_baton, &merge_b->conflicted_paths, merge_b->pool }; SVN_ERR(svn_wc_merge3(&merge_outcome, older, yours, mine, adm_access, left_label, right_label, target_label, merge_b->dry_run, merge_b->diff3_cmd, merge_b->merge_options, prop_changes, conflict_resolver, &conflict_baton, subpool)); } if (content_state) { if (merge_outcome == svn_wc_merge_conflict) *content_state = svn_wc_notify_state_conflicted; else if (has_local_mods && merge_outcome != svn_wc_merge_unchanged) *content_state = svn_wc_notify_state_merged; else if (merge_outcome == svn_wc_merge_merged) *content_state = svn_wc_notify_state_changed; else if (merge_outcome == svn_wc_merge_no_merge) *content_state = svn_wc_notify_state_missing; else /* merge_outcome == svn_wc_merge_unchanged */ *content_state = svn_wc_notify_state_unchanged; } } svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* A svn_wc_diff_callbacks2_t function. */ static svn_error_t * merge_file_added(svn_wc_adm_access_t *adm_access, svn_wc_notify_state_t *content_state, svn_wc_notify_state_t *prop_state, const char *mine, const char *older, const char *yours, svn_revnum_t rev1, svn_revnum_t rev2, const char *mimetype1, const char *mimetype2, const apr_array_header_t *prop_changes, apr_hash_t *original_props, void *baton) { merge_cmd_baton_t *merge_b = baton; apr_pool_t *subpool = svn_pool_create(merge_b->pool); svn_node_kind_t kind; int i; apr_hash_t *new_props; /* In most cases, we just leave prop_state as unknown, and let the content_state what happened, so we set prop_state here to avoid that below. */ if (prop_state) *prop_state = svn_wc_notify_state_unknown; /* Apply the prop changes to a new hash table. */ new_props = apr_hash_copy(subpool, original_props); for (i = 0; i < prop_changes->nelts; ++i) { const svn_prop_t *prop = &APR_ARRAY_IDX(prop_changes, i, svn_prop_t); /* We don't want any DAV wcprops related to this file because they'll point to the wrong repository (in the merge-from-foreign-repository scenario) or wrong place in the right repository (in the same-repos scenario). So we'll strip them. (Is this a layering violation?) */ if (svn_property_kind(NULL, prop->name) == svn_prop_wc_kind) continue; /* And in the foreign repository merge case, we only want regular properties. */ if ((! merge_b->same_repos) && (svn_property_kind(NULL, prop->name) != svn_prop_regular_kind)) continue; apr_hash_set(new_props, prop->name, APR_HASH_KEY_STRING, prop->value); } /* Easy out: if we have no adm_access for the parent directory, then this portion of the tree-delta "patch" must be inapplicable. Send a 'missing' state back; the repos-diff editor should then send a 'skip' notification. */ if (! adm_access) { if (merge_b->dry_run && merge_b->added_path && svn_path_is_child(merge_b->added_path, mine, subpool)) { if (content_state) *content_state = svn_wc_notify_state_changed; if (prop_state && apr_hash_count(new_props)) *prop_state = svn_wc_notify_state_changed; } else *content_state = svn_wc_notify_state_missing; svn_pool_destroy(subpool); return SVN_NO_ERROR; } SVN_ERR(svn_io_check_path(mine, &kind, subpool)); switch (kind) { case svn_node_none: { const svn_wc_entry_t *entry; SVN_ERR(svn_wc_entry(&entry, mine, adm_access, FALSE, subpool)); if (entry && entry->schedule != svn_wc_schedule_delete) { /* It's versioned but missing. */ if (content_state) *content_state = svn_wc_notify_state_obstructed; svn_pool_destroy(subpool); return SVN_NO_ERROR; } if (! merge_b->dry_run) { const char *copyfrom_url = NULL; svn_revnum_t copyfrom_rev = SVN_INVALID_REVNUM; /* 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_path_is_child(merge_b->target, mine, subpool); if (child != NULL) copyfrom_url = svn_path_url_add_component(merge_b->url, child, subpool); else copyfrom_url = merge_b->url; copyfrom_rev = rev2; SVN_ERR(check_scheme_match(adm_access, copyfrom_url)); } /* Since 'mine' doesn't exist, and this is 'merge_file_added', I hope it's safe to assume that 'older' is empty, and 'yours' is the full file. Merely copying 'yours' to 'mine', isn't enough; we need to get the whole text-base and props installed too, just as if we had called 'svn cp wc wc'. */ SVN_ERR(svn_wc_add_repos_file2(mine, adm_access, yours, NULL, new_props, NULL, copyfrom_url, copyfrom_rev, subpool)); } if (content_state) *content_state = svn_wc_notify_state_changed; if (prop_state && apr_hash_count(new_props)) *prop_state = svn_wc_notify_state_changed; } break; case svn_node_dir: if (content_state) { /* directory already exists, is it under version control? */ const svn_wc_entry_t *entry; SVN_ERR(svn_wc_entry(&entry, mine, adm_access, FALSE, subpool)); if (entry && dry_run_deleted_p(merge_b, mine)) *content_state = svn_wc_notify_state_changed; else /* this will make the repos_editor send a 'skipped' message */ *content_state = svn_wc_notify_state_obstructed; } break; case svn_node_file: { /* file already exists, is it under version control? */ const svn_wc_entry_t *entry; SVN_ERR(svn_wc_entry(&entry, mine, adm_access, FALSE, subpool)); /* If it's an unversioned file, don't touch it. If it's scheduled for deletion, then rm removed it from the working copy and the user must have recreated it, don't touch it */ if (!entry || entry->schedule == svn_wc_schedule_delete) { /* this will make the repos_editor send a 'skipped' message */ if (content_state) *content_state = svn_wc_notify_state_obstructed; } else { if (dry_run_deleted_p(merge_b, mine)) { if (content_state) *content_state = svn_wc_notify_state_changed; } else { /* Indicate that we merge because of an add to handle a special case for binary files with no local mods. */ merge_b->add_necessitated_merge = TRUE; SVN_ERR(merge_file_changed(adm_access, content_state, prop_state, mine, older, yours, rev1, rev2, mimetype1, mimetype2, prop_changes, original_props, baton)); /* Reset the state so that the baton can safely be reused in subsequent ops occurring during this merge. */ merge_b->add_necessitated_merge = FALSE; } } break; } default: if (content_state) *content_state = svn_wc_notify_state_unknown; break; } svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* A svn_wc_diff_callbacks2_t function. */ static svn_error_t * merge_file_deleted(svn_wc_adm_access_t *adm_access, svn_wc_notify_state_t *state, const char *mine, const char *older, const char *yours, const char *mimetype1, const char *mimetype2, apr_hash_t *original_props, void *baton) { merge_cmd_baton_t *merge_b = baton; apr_pool_t *subpool = svn_pool_create(merge_b->pool); svn_node_kind_t kind; svn_wc_adm_access_t *parent_access; const char *parent_path; svn_error_t *err; /* Easy out: if we have no adm_access for the parent directory, then this portion of the tree-delta "patch" must be inapplicable. Send a 'missing' state back; the repos-diff editor should then send a 'skip' notification. */ if (! adm_access) { if (state) *state = svn_wc_notify_state_missing; svn_pool_destroy(subpool); return SVN_NO_ERROR; } SVN_ERR(svn_io_check_path(mine, &kind, subpool)); switch (kind) { case svn_node_file: svn_path_split(mine, &parent_path, NULL, subpool); SVN_ERR(svn_wc_adm_retrieve(&parent_access, adm_access, parent_path, subpool)); /* Passing NULL for the notify_func and notify_baton because repos_diff.c:delete_entry() will do it for us. */ err = svn_client__wc_delete(mine, parent_access, merge_b->force, merge_b->dry_run, FALSE, NULL, NULL, merge_b->ctx, subpool); if (err && state) { *state = svn_wc_notify_state_obstructed; svn_error_clear(err); } else if (state) { *state = svn_wc_notify_state_changed; } break; case svn_node_dir: if (state) *state = svn_wc_notify_state_obstructed; break; case svn_node_none: /* file is already non-existent, this is a no-op. */ if (state) *state = svn_wc_notify_state_missing; break; default: if (state) *state = svn_wc_notify_state_unknown; break; } svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* A svn_wc_diff_callbacks2_t function. */ static svn_error_t * merge_dir_added(svn_wc_adm_access_t *adm_access, svn_wc_notify_state_t *state, const char *path, svn_revnum_t rev, void *baton) { merge_cmd_baton_t *merge_b = baton; apr_pool_t *subpool = svn_pool_create(merge_b->pool); svn_node_kind_t kind; const svn_wc_entry_t *entry; const char *copyfrom_url = NULL, *child; svn_revnum_t copyfrom_rev = SVN_INVALID_REVNUM; /* Easy out: if we have no adm_access for the parent directory, then this portion of the tree-delta "patch" must be inapplicable. Send a 'missing' state back; the repos-diff editor should then send a 'skip' notification. */ if (! adm_access) { if (state) { if (merge_b->dry_run && merge_b->added_path && svn_path_is_child(merge_b->added_path, path, subpool)) *state = svn_wc_notify_state_changed; else *state = svn_wc_notify_state_missing; } svn_pool_destroy(subpool); return SVN_NO_ERROR; } child = svn_path_is_child(merge_b->target, path, subpool); assert(child != NULL); /* 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) { copyfrom_url = svn_path_url_add_component(merge_b->url, child, subpool); copyfrom_rev = rev; SVN_ERR(check_scheme_match(adm_access, copyfrom_url)); } SVN_ERR(svn_io_check_path(path, &kind, subpool)); switch (kind) { case svn_node_none: SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, subpool)); if (entry && entry->schedule != svn_wc_schedule_delete) { /* Versioned but missing */ if (state) *state = svn_wc_notify_state_obstructed; svn_pool_destroy(subpool); return SVN_NO_ERROR; } if (merge_b->dry_run) merge_b->added_path = apr_pstrdup(merge_b->pool, path); else { SVN_ERR(svn_io_make_dir_recursively(path, subpool)); SVN_ERR(svn_wc_add2(path, adm_access, copyfrom_url, copyfrom_rev, merge_b->ctx->cancel_func, merge_b->ctx->cancel_baton, NULL, NULL, /* don't pass notification func! */ subpool)); } if (state) *state = svn_wc_notify_state_changed; break; case svn_node_dir: /* Adding an unversioned directory doesn't destroy data */ SVN_ERR(svn_wc_entry(&entry, path, adm_access, TRUE, subpool)); if (! entry || entry->schedule == svn_wc_schedule_delete) { if (!merge_b->dry_run) SVN_ERR(svn_wc_add2(path, adm_access, copyfrom_url, copyfrom_rev, merge_b->ctx->cancel_func, merge_b->ctx->cancel_baton, NULL, NULL, /* no notification func! */ subpool)); else merge_b->added_path = apr_pstrdup(merge_b->pool, path); if (state) *state = svn_wc_notify_state_changed; } else if (state) { if (dry_run_deleted_p(merge_b, path)) *state = svn_wc_notify_state_changed; else *state = svn_wc_notify_state_obstructed; } break; case svn_node_file: if (merge_b->dry_run) merge_b->added_path = NULL; if (state) { SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, subpool)); if (entry && dry_run_deleted_p(merge_b, path)) /* ### TODO: Retain record of this dir being added to ### avoid problems from subsequent edits which try to ### add children. */ *state = svn_wc_notify_state_changed; else *state = svn_wc_notify_state_obstructed; } break; default: if (merge_b->dry_run) merge_b->added_path = NULL; if (state) *state = svn_wc_notify_state_unknown; break; } svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* A svn_wc_diff_callbacks2_t function. */ static svn_error_t * merge_dir_deleted(svn_wc_adm_access_t *adm_access, svn_wc_notify_state_t *state, const char *path, void *baton) { merge_cmd_baton_t *merge_b = baton; apr_pool_t *subpool = svn_pool_create(merge_b->pool); svn_node_kind_t kind; svn_wc_adm_access_t *parent_access; const char *parent_path; svn_error_t *err; /* Easy out: if we have no adm_access for the parent directory, then this portion of the tree-delta "patch" must be inapplicable. Send a 'missing' state back; the repos-diff editor should then send a 'skip' notification. */ if (! adm_access) { if (state) *state = svn_wc_notify_state_missing; svn_pool_destroy(subpool); return SVN_NO_ERROR; } SVN_ERR(svn_io_check_path(path, &kind, subpool)); switch (kind) { case svn_node_dir: { svn_path_split(path, &parent_path, NULL, subpool); SVN_ERR(svn_wc_adm_retrieve(&parent_access, adm_access, parent_path, subpool)); /* Passing NULL for the notify_func and notify_baton because repos_diff.c:delete_entry() will do it for us. */ err = svn_client__wc_delete(path, parent_access, merge_b->force, merge_b->dry_run, FALSE, NULL, NULL, merge_b->ctx, subpool); if (err && state) { *state = svn_wc_notify_state_obstructed; svn_error_clear(err); } else if (state) { *state = svn_wc_notify_state_changed; } } break; case svn_node_file: if (state) *state = svn_wc_notify_state_obstructed; break; case svn_node_none: /* dir is already non-existent, this is a no-op. */ if (state) *state = svn_wc_notify_state_missing; break; default: if (state) *state = svn_wc_notify_state_unknown; break; } svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* The main callback table for 'svn merge'. */ static const svn_wc_diff_callbacks2_t merge_callbacks = { merge_file_changed, merge_file_added, merge_file_deleted, merge_dir_added, merge_dir_deleted, merge_props_changed }; /*-----------------------------------------------------------------------*/ /*** Merge Notification ***/ /* Contains any state collected while receiving path notifications. */ typedef struct { /* The wrapped callback and baton. */ svn_wc_notify_func2_t wrapped_func; void *wrapped_baton; /* The number of notifications received. */ apr_uint32_t nbr_notifications; /* The number of operative notifications received. */ apr_uint32_t nbr_operative_notifications; /* The list of merged paths. */ apr_hash_t *merged_paths; /* The list of any skipped paths, which should be examined and cleared after each invocation of the callback. */ apr_hash_t *skipped_paths; /* A list of the root paths of any added subtrees which might require their own explicit mergeinfo. */ apr_hash_t *added_paths; /* Flag indicating whether it is a single file merge or not. */ svn_boolean_t is_single_file_merge; /* Depth first ordered list of paths that needs special care while merging. This defaults to NULL. For 'same_url' merge alone we set it to proper array. This is used by notification_receiver to put a merge notification begin lines. */ apr_array_header_t *children_with_mergeinfo; /* The index in CHILDREN_WITH_MERGEINFO where we found the nearest ancestor for merged path. Default value is '-1'.*/ int cur_ancestor_index; /* We use this to make a decision on merge begin line notifications. */ merge_cmd_baton_t *merge_b; /* Pool used in notification_receiver() to avoid the iteration sub-pool which is passed in, then subsequently destroyed. */ apr_pool_t *pool; } notification_receiver_baton_t; /* Finds a nearest ancestor in CHILDREN_WITH_MERGEINFO for PATH. If PATH_IS_OWN_ANCESTOR is TRUE then a child in CHILDREN_WITH_MERGEINFO where child->path == PATH is considered PATH's ancestor. If FALSE, then child->path must be a proper ancestor of PATH. CHILDREN_WITH_MERGEINFO is expected to be sorted in Depth first order of path. Nearest ancestor's index from CHILDREN_WITH_MERGEINFO is returned. */ static int find_nearest_ancestor(apr_array_header_t *children_with_mergeinfo, svn_boolean_t path_is_own_ancestor, const char *path) { int i; int ancestor_index = 0; /* This if condition is not needed as this function should be used from the context of same_url merge where CHILDREN_WITH_MERGEINFO will not be NULL and of size atleast 1. We have this if condition just to protect the wrong caller. */ if (!children_with_mergeinfo) return 0; 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 (svn_path_is_ancestor(child->path, path) && (svn_path_compare_paths(child->path, path) != 0 || path_is_own_ancestor)) ancestor_index = i; } return ancestor_index; } #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) /* Our svn_wc_notify_func2_t wrapper.*/ static void notification_receiver(void *baton, const svn_wc_notify_t *notify, apr_pool_t *pool) { notification_receiver_baton_t *notify_b = baton; svn_boolean_t is_operative_notification = FALSE; /* Is the notification the result of a real operative merge? */ if (IS_OPERATIVE_NOTIFICATION(notify)) { notify_b->nbr_operative_notifications++; is_operative_notification = TRUE; } /* If our merge sources are ancestors of one another... */ if (notify_b->merge_b->sources_ancestral) { notify_b->nbr_notifications++; /* See if this is an operative directory merge. */ if (!(notify_b->is_single_file_merge) && is_operative_notification) { /* 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 */ int new_nearest_ancestor_index = find_nearest_ancestor( notify_b->children_with_mergeinfo, notify->action == svn_wc_notify_update_delete ? FALSE : TRUE, notify->path); if (new_nearest_ancestor_index != notify_b->cur_ancestor_index) { svn_client__merge_path_t *child = APR_ARRAY_IDX(notify_b->children_with_mergeinfo, new_nearest_ancestor_index, svn_client__merge_path_t *); notify_b->cur_ancestor_index = new_nearest_ancestor_index; if (!child->absent && child->remaining_ranges->nelts > 0 && !(new_nearest_ancestor_index == 0 && notify_b->merge_b->target_has_dummy_merge_range)) { svn_wc_notify_t *notify_merge_begin; notify_merge_begin = svn_wc_create_notify(child->path, notify_b->merge_b->same_repos ? svn_wc_notify_merge_begin : svn_wc_notify_foreign_merge_begin, pool); notify_merge_begin->merge_range = APR_ARRAY_IDX(child->remaining_ranges, 0, svn_merge_range_t *); if (notify_b->wrapped_func) (*notify_b->wrapped_func)(notify_b->wrapped_baton, notify_merge_begin, pool); } } } if (notify->content_state == svn_wc_notify_state_merged || notify->content_state == svn_wc_notify_state_changed || notify->prop_state == svn_wc_notify_state_merged || notify->prop_state == svn_wc_notify_state_changed || notify->action == svn_wc_notify_update_add) { const char *merged_path = apr_pstrdup(notify_b->pool, notify->path); if (notify_b->merged_paths == NULL) notify_b->merged_paths = apr_hash_make(notify_b->pool); apr_hash_set(notify_b->merged_paths, merged_path, APR_HASH_KEY_STRING, merged_path); } if (notify->action == svn_wc_notify_skip) { const char *skipped_path = apr_pstrdup(notify_b->pool, notify->path); if (notify_b->skipped_paths == NULL) notify_b->skipped_paths = apr_hash_make(notify_b->pool); apr_hash_set(notify_b->skipped_paths, skipped_path, APR_HASH_KEY_STRING, skipped_path); } if (notify->action == svn_wc_notify_update_add) { svn_boolean_t is_root_of_added_subtree = FALSE; const char *added_path = apr_pstrdup(notify_b->pool, notify->path); const char *added_path_parent = NULL; /* Stash the root path of any added subtrees. */ if (notify_b->added_paths == NULL) { notify_b->added_paths = apr_hash_make(notify_b->pool); is_root_of_added_subtree = TRUE; } else { added_path_parent = svn_path_dirname(added_path, pool); if (!apr_hash_get(notify_b->added_paths, added_path_parent, APR_HASH_KEY_STRING)) is_root_of_added_subtree = TRUE; } if (is_root_of_added_subtree) apr_hash_set(notify_b->added_paths, added_path, APR_HASH_KEY_STRING, added_path); } } /* Otherwise, our merge sources aren't ancestors of one another. */ else if (!(notify_b->is_single_file_merge) && notify_b->nbr_operative_notifications == 1 && is_operative_notification) { svn_wc_notify_t *notify_merge_begin; notify_merge_begin = svn_wc_create_notify(notify_b->merge_b->target, notify_b->merge_b->same_repos ? svn_wc_notify_merge_begin : svn_wc_notify_foreign_merge_begin, pool); if (notify_b->wrapped_func) (*notify_b->wrapped_func)(notify_b->wrapped_baton, notify_merge_begin, pool); } if (notify_b->wrapped_func) (*notify_b->wrapped_func)(notify_b->wrapped_baton, notify, pool); } /* Helper for the numerous times we need to allocate and initialize a rangelist with one element. Return a rangelist allocated in POOL with one svn_merge_range_t element defined by START, END, and INHERITABLE. */ static apr_array_header_t * init_rangelist(svn_revnum_t start, svn_revnum_t end, svn_boolean_t inheritable, apr_pool_t *pool) { apr_array_header_t *rangelist = apr_array_make(pool, 1, sizeof(svn_merge_range_t *)); svn_merge_range_t *range = apr_pcalloc(pool, sizeof(*range)); range->start = start; range->end = end; range->inheritable = inheritable; APR_ARRAY_PUSH(rangelist, svn_merge_range_t *) = range; return rangelist; } /* Helper for the numerous times we need to allocate a svn_merge_range_t and push it onto a rangelist. Allocated a svn_merge_range_t element defined by START, END, and INHERITABLE and push it onto RANGELIST. */ static void push_range(apr_array_header_t *rangelist, svn_revnum_t start, svn_revnum_t end, svn_boolean_t inheritable, apr_pool_t *pool) { svn_merge_range_t *range = apr_pcalloc(pool, sizeof(*range)); range->start = start; range->end = end; range->inheritable = inheritable; APR_ARRAY_PUSH(rangelist, svn_merge_range_t *) = range; } /* Helper for filter_merged_revisions() when operating on a subtree. Like filter_merged_revisions(), this should only be called when honoring mergeinfo. Filter the requested ranges being merged to a subtree so that we don't try to describe invalid subtrees to the merge report editor. Note in *CHILD_DELETED_OR_NONEXISTANT if the subtree doesn't exist, is deleted, or is renamed in the requested range. PARENT, MERGEINFO_PATH, REVISION1, REVISION2, PRIMARY_URL, RA_SESSION, and CTX are all cascaded from filter_merged_revisions(). Since this function is only invoked for subtrees of the merge target, the guarantees afforded by normalize_merge_sources() don't apply. Therefore it is possible that PRIMARY_URL@REVISION1 and PRIMARY_URL@REVISION2 don't describe an unbroken line of history. Specifically we can end up with one of these eight cases: Note1: Unless noted otherwise, every case sets *REQUESTED_RANGELIST to a rangelist with one element defined by REVISION1 and REVISION2. Note2: The inheritability of all svn_merge_range_t in *REQUESTED_RANGELIST is always TRUE. Forward Merges, i.e. REVISION1 < REVISION2 (PEG_REV) A) Requested range deletes subtree. PRIMARY_URL@REVISION1 exists, but PRIMARY_URL@REVISION2 doesn't exist because PRIMARY_URL was deleted prior to REVISION2. Set *CHILD_DELETED_OR_NONEXISTANT to TRUE. B) Part of requested range predates subtree's existance. PRIMARY_URL@REVISION2 exists, but PRIMARY_URL@REVISION1 doesn't exist because PRIMARY_URL didn't come into existence until some revision 'N' where N > REVISION1. Set *CHILD_DELETED_OR_NONEXISTANT to FALSE. Populate *REQUESTED_RANGELIST with the ranges between N and REVISION2 (inclusive) at which PRIMARY_URL exists. Then take the intersection of REVISION1:N (i.e. the range which predates the existance of PRIMARY_URL) and PARENT->REQUESTED_RANGELIST and add it to *REQUESTED_RANGELIST. This prevents us from later trying to describe any non-existant path/revs for this subtree in drive_merge_report_editor(). A good thing as that would break the editor. C) Subtree doesn't exist in requested range or exists inside the requested range but is ultimately deleted. Neither PRIMARY_URL@REVISION1 or PRIMARY_URL@REVISION2 exist. Set *CHILD_DELETED_OR_NONEXISTANT to TRUE D) Subtree exists at start and end of requested range and was not replaced within that range. PRIMARY_URL@REVISION1 and PRIMARY_URL@REVISION2 both exist. Set *CHILD_DELETED_OR_NONEXISTANT to FALSE. Reverse Merges, i.e. REVISION1 (PEG_REV) > REVISION2 E) Part of requested range postdates subtree's existance. PRIMARY_URL@REVISION2 exists, but PRIMARY_URL@REVISION1 doesn't exist because PRIMARY_URL was deleted prior to REVISION1. Set *CHILD_DELETED_OR_NONEXISTANT to FALSE. ### This is tricky, sort of the inverse of B; we want to reverse ### merge some range M:N, let's say 14:4, into the subtree, but the ### subtree was deleted at r10. *BUT* we only allow reverse merges ### of ranges that exist in implicit or explicit mergeinfo. Can't we ### simply set *REQUESTED_RANGELIST to REVISION1:REVISION2 and let the ### existing code in filter_merged_revisions() do its thing? Because ### if the subtree has any explicit mergeinfo (via inheritance) ### describing ranges that postdate the subtree's existance, the ### subtree's nearest parent must also have that mergeinfo right? ### Put another way, how can all of the following ever be true? ### ### i) The subtree merge source doesn't exist anymore at ### revsion X. ### ii) Mergeinfo for X is explicitly set on the subtree. ### iii) The subtree's parent has no explicit mergeinfo for X. ### ### This is where Kamesh utilized his recursive guess_live_ranges ### function...But do we ever need to do this in practice? F) Requested range deletes (or replaces) a subtree. PRIMARY_URL@REVISION1 exists, but PRIMARY_URL@REVISION2 doesn't exist because PRIMARY_URL didn't come into existence until *after* REVISION2. Or PRIMARY_URL@REVISION1 and PRIMARY_URL@REVISION2 both exist, but they don't describe an unbroken line of history. Set *CHILD_DELETED_OR_NONEXISTANT to TRUE. G) Subtree doesn't exist in requested range or exists inside the requested range but is ultimately deleted. Neither PRIMARY_URL@REVISION1 or PRIMARY_URL@REVISION2 exist. Set *CHILD_DELETED_OR_NONEXISTANT to TRUE. H) Subtree exists at start and end of requested range and was not replaced within that range. PRIMARY_URL@REVISION1 and PRIMARY_URL@REVISION2 both exist and describe the start and end of an unbroken line of history. Set *CHILD_DELETED_OR_NONEXISTANT to FALSE. All the allocations are made from POOL. */ static svn_error_t * prepare_subtree_ranges(apr_array_header_t **requested_rangelist, svn_boolean_t *child_deleted_or_nonexistant, const char *mergeinfo_path, 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 *pool) { svn_boolean_t is_rollback = revision2 < revision1; svn_revnum_t peg_rev = is_rollback ? revision1 : revision2; svn_revnum_t start_rev = is_rollback ? revision1 : revision2; svn_revnum_t end_rev = is_rollback ? revision2 : revision1; apr_array_header_t *segments; const char *rel_source_path; const char *session_url; svn_error_t *err; SVN_ERR(svn_ra_get_session_url(ra_session, &session_url, pool)); SVN_ERR(svn_client__path_relative_to_root(&rel_source_path, primary_url, session_url, FALSE, ra_session, NULL, pool)); err = svn_client__repos_location_segments(&segments, ra_session, rel_source_path, peg_rev, start_rev, end_rev, ctx, pool); /* If REL_SOURCE_PATH@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. */ if (err) { if (err->apr_err == SVN_ERR_FS_NOT_FOUND || err->apr_err == SVN_ERR_RA_DAV_REQUEST_FAILED) { svn_error_clear(err); if (is_rollback) { svn_dirent_t *dirent; SVN_ERR(svn_ra_stat(ra_session, rel_source_path, revision2, &dirent, pool)); if (dirent) *child_deleted_or_nonexistant = FALSE; /* Case E. */ else *child_deleted_or_nonexistant = TRUE; /* Case G. */ } else { *child_deleted_or_nonexistant = TRUE; /* Case A & C. */ } *requested_rangelist = init_rangelist(revision1, revision2, TRUE, pool); } else return err; } else { if (segments->nelts) { svn_location_segment_t *segment = APR_ARRAY_IDX(segments, (segments->nelts - 1), svn_location_segment_t *); if (is_rollback) { if (segment->range_start == revision2 && segment->range_end == revision1) { /* Case H. */ *requested_rangelist = init_rangelist(revision1, revision2, TRUE, pool); *child_deleted_or_nonexistant = FALSE; } else /* Multiple location segements found. */ { /* Case F. */ *requested_rangelist = init_rangelist(revision1, revision2, TRUE, pool); *child_deleted_or_nonexistant = TRUE; } } else /* Forward merge */ { /* Again, because REVISION2 is the peg revision for the call to svn_client__repos_location_segments, we know that the range_end of the last segment in segments is equal to REVISION2. */ if (segment->range_start == revision1 && segment->range_end == revision2) { /* Case D. */ *requested_rangelist = init_rangelist(revision1, revision2, TRUE, pool); *child_deleted_or_nonexistant = FALSE; } else /* segment->range_start != revision1, since segment->range_start can't be less than REVISION1, this implies revision1 < segment->range_start. */ { /* Case B. */ int i; apr_array_header_t *predate_intersection_rangelist; apr_array_header_t *different_name_rangelist = apr_array_make(pool, 1, sizeof(svn_merge_range_t *)); /* Make a ranglist that describes the range which predates PRIMARY_URL's existance... */ apr_array_header_t *predate_rangelist = init_rangelist(revision1, segment->range_start, TRUE, pool); /* ...Find the intersection of that rangelist and the subtree's parent's remaining ranges. */ SVN_ERR(svn_rangelist_intersect( &predate_intersection_rangelist, predate_rangelist, parent->remaining_ranges, FALSE, pool)); *requested_rangelist = init_rangelist(segment->range_start, revision2, TRUE, pool); /* Merge *REQUESTED_RANGELIST with its parent's remaining ranges the intersect with the subtree's prehistory. */ SVN_ERR(svn_rangelist_merge( requested_rangelist, predate_intersection_rangelist, pool)); /* Remove ranges that predate PRIMARY_URL's existance because the source exists under a different URL due to a rename between REVISION1:REVISION2 - see 'MERGE FAILS' in http://subversion.tigris.org/issues/show_bug.cgi?id=3067#desc34. */ for (i = 0; i < segments->nelts; i++) { segment = APR_ARRAY_IDX(segments, i, svn_location_segment_t *); if (segment->path && strcmp(segment->path, mergeinfo_path + 1) != 0) push_range(different_name_rangelist, segment->range_start, segment->range_end, TRUE, pool); } if (different_name_rangelist->nelts) SVN_ERR(svn_rangelist_remove(requested_rangelist, different_name_rangelist, *requested_rangelist, FALSE, pool)); *child_deleted_or_nonexistant = FALSE; } } } } /* ! err */ return SVN_NO_ERROR; } /*-----------------------------------------------------------------------*/ /*** Determining What Remains To Be Merged ***/ /* Helper for calculate_remaining_ranges(). Calculate the ranges that remain to be merged from the merge source MERGEINFO_PATH (relative to the repository root) to the working copy path represented by CHILD -- for use by drive_merge_report_editor()'s application of the editor to the WC. Set CHILD->remaining_ranges to the set of revisions to merge. PARENT, PRIMARY_URL, IS_SUBTREE, RA_SESSION, CTX are cascaded from calculate_remaining_ranges(). If IS_SUBTREE is FALSE then PARENT is ignored. REVISION1 and REVISION2 describe the merge range requested from MERGEINFO_PATH. TARGET_MERGEINFO is the path's explicit or inherited mergeinfo. May be NULL if there is not mergeinfo or an empty hash for empty mergeinfo. IMPLICIT_MERGEINFO is the path's natural history described as mergeinfo - see svn_client__get_history_as_mergeinfo(). NOTE: This should only be called when honoring mergeinfo. */ static svn_error_t * filter_merged_revisions(svn_client__merge_path_t *parent, svn_client__merge_path_t *child, const char *mergeinfo_path, svn_mergeinfo_t target_mergeinfo, svn_mergeinfo_t implicit_mergeinfo, svn_revnum_t revision1, svn_revnum_t revision2, const char *primary_url, svn_ra_session_t *ra_session, svn_boolean_t is_subtree, svn_client_ctx_t *ctx, apr_pool_t *pool) { apr_array_header_t *target_rangelist = NULL; svn_mergeinfo_t mergeinfo = implicit_mergeinfo; apr_array_header_t *requested_merge; if (is_subtree) { /* If CHILD is the merge target we then know that PRIMARY_URL, REVISION1, and REVISION2 are provided by normalize_merge_sources() -- see 'MERGEINFO MERGE SOURCE NORMALIZATION'. Due to this normalization we know that PRIMARY_URL@REVISION1 and PRIMARY_URL@REVISION2 describe an unbroken line of history such that the entire range described by REVISION1:REVISION2 can potentially be merged to CHILD. So we simply convert REVISION1 and REVISION2 to a rangelist and proceed to the filtering of merged revisions. But if CHILD is a subtree we don't have the same guarantees about PRIMARY_URL, REVISION1, and REVISION2 as we do for the merge target. PRIMARY_URL@REVSION1 and/or PRIMARY_URL@REVSION2 might not exist. If one or both doesn't exist, we need to know so we don't later try to describe these invalid subtrees in drive_merge_report_editor(), as that will break the merge. */ svn_boolean_t child_deleted_or_nonexistant; SVN_ERR(prepare_subtree_ranges(&requested_merge, &child_deleted_or_nonexistant, mergeinfo_path, parent, revision1, revision2, primary_url, ra_session, ctx, pool)); if (child_deleted_or_nonexistant && parent) { /* A little trick: If CHILD is a subtree which will be deleted by the requested merge or simply doesn't exist along the line of history described by PRIMARY_URL@REVSION1 -> PRIMARY_URL@REVSION2, then don't bother dealing with CHILD in a separate editor drive. Just make child's remaining ranges exactly the same as its nearest parent. For deletions this will cause the editor drive to be rooted at the subtree CHILD's nearest parent in CHILDREN_WITH_MERGEINFO This will simply delete the subtree. For the case where neither PRIMARY_URL@REVSION1 or PRIMARY_URL@REVSION2 exist, there is nothing to merge to the subtree, so ignoring it completely is safe. See http://subversion.tigris.org/issues/show_bug.cgi?id=3067#desc5. */ child->remaining_ranges = svn_rangelist_dup(parent->remaining_ranges, pool); return SVN_NO_ERROR; } } else { /* 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_merge = init_rangelist(revision1, revision2, TRUE, pool); } /* Now filter out revisions that have already been merged to CHILD. */ if (revision1 > revision2) /* This is a reverse merge. */ { if (target_mergeinfo) { mergeinfo = svn_mergeinfo_dup(implicit_mergeinfo, pool); SVN_ERR(svn_mergeinfo_merge(mergeinfo, target_mergeinfo, pool)); } target_rangelist = apr_hash_get(mergeinfo, mergeinfo_path, APR_HASH_KEY_STRING); if (target_rangelist) { /* Return the intersection of the revs which are both already represented by the WC and are requested for revert. The revert range and will need to be reversed for our APIs to work properly, as will the output for the revert to work properly. */ SVN_ERR(svn_rangelist_reverse(requested_merge, pool)); /* We don't consider inheritance we determining intersecting ranges. If we *did* consider inheritance, then our calculation would be wrong. For example, if the REQUESTED_MERGE 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 ENTRY'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(&(child->remaining_ranges), target_rangelist, requested_merge, FALSE, pool)); SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, pool)); } else { child->remaining_ranges = apr_array_make(pool, 1, sizeof(svn_merge_range_t *)); } } else /* This is a forward merge */ { child->remaining_ranges = requested_merge; /* ### 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 if (target_mergeinfo) target_rangelist = apr_hash_get(target_mergeinfo, mergeinfo_path, APR_HASH_KEY_STRING); #else if (target_mergeinfo) { mergeinfo = svn_mergeinfo_dup(implicit_mergeinfo, pool); SVN_ERR(svn_mergeinfo_merge(mergeinfo, target_mergeinfo, pool)); } target_rangelist = apr_hash_get(mergeinfo, mergeinfo_path, APR_HASH_KEY_STRING); #endif /* See earlier comment preceeding svn_rangelist_intersect() for why we don't consider inheritance here. */ if (target_rangelist) SVN_ERR(svn_rangelist_remove(&(child->remaining_ranges), target_rangelist, requested_merge, FALSE, pool)); } 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 URL1@REVISION1 -> URL2@REVISION2 have already been merged to CHILD->PATH and populate CHILD->REMAINING_RANGES with the ranges that still need merging. SOURCE_ROOT_URL, URL1, REVISION1, URL2, REVISION2, TARGET_MERGEINFO, IMPLICIT_MERGEINFO, RA_SESSION, and CTX are all cascaded from the caller's arguments of the same names. If IS_SUBTREE is FALSE then CHILD describes the merge target and the requirements around the values of URL1, REVISION1, URL2, and REVISION2 described in 'MERGEINFO MERGE SOURCE NORMALIZATION' hold. If IS_SUBTREE is TRUE then CHILD describes some subtree of a merge target and these normalization conditions do not necessarily hold. IS_SUBTREE should always be FALSE when calling from do_file_merge(). If IS_SUBTREE is FALSE then PARENT is ignored, otherwise PARENT must represent the nearest working copy ancestor of CHILD. NOTE: This should only be called when honoring mergeinfo. NOTE: When performing reverse merges, return SVN_ERR_CLIENT_NOT_READY_TO_MERGE if URL1@REVISION1, URL2@REVISION2, and ENTRY are all on the same line of history but ENTRY-REVISION is older than the REVISION1-REVISION2 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 char *source_root_url, const char *url1, svn_revnum_t revision1, const char *url2, svn_revnum_t revision2, svn_mergeinfo_t target_mergeinfo, svn_mergeinfo_t implicit_mergeinfo, svn_boolean_t is_subtree, svn_ra_session_t *ra_session, const svn_wc_entry_t *entry, svn_client_ctx_t *ctx, apr_pool_t *pool) { const char *mergeinfo_path; const char *primary_url = (revision1 < revision2) ? url2 : url1; /* Determine which of the requested ranges to consider merging... */ SVN_ERR(svn_client__path_relative_to_root(&mergeinfo_path, primary_url, source_root_url, TRUE, ra_session, NULL, pool)); SVN_ERR(filter_merged_revisions(parent, child, mergeinfo_path, target_mergeinfo, implicit_mergeinfo, revision1, revision2, primary_url, ra_session, is_subtree, ctx, 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 ENTRY, 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 working 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 working 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. */ if (((child->remaining_ranges)->nelts == 0) && (revision2 < revision1) && (entry->revision <= revision2)) { /* Hmmm, an inoperative reverse merge from the "future". If it is from our own future return a helpful error. */ svn_error_t *err; const char *start_url; svn_opt_revision_t requested, unspec, pegrev, *start_revision; unspec.kind = svn_opt_revision_unspecified; requested.kind = svn_opt_revision_number; requested.value.number = entry->revision; pegrev.kind = svn_opt_revision_number; pegrev.value.number = revision1; err = svn_client__repos_locations(&start_url, &start_revision, NULL, NULL, ra_session, url1, &pegrev, &requested, &unspec, ctx, 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 err; } else if (strcmp(start_url, entry->url) == 0) { return svn_error_create(SVN_ERR_CLIENT_NOT_READY_TO_MERGE, NULL, _("Cannot reverse-merge a range from a " "path's own future history; try " "updating first")); } } return SVN_NO_ERROR; } static svn_error_t * get_full_mergeinfo(svn_mergeinfo_t *recorded_mergeinfo, svn_mergeinfo_t *implicit_mergeinfo, const svn_wc_entry_t *entry, svn_boolean_t *indirect, svn_mergeinfo_inheritance_t inherit, svn_ra_session_t *ra_session, const char *target_wcpath, svn_revnum_t start, svn_revnum_t end, svn_wc_adm_access_t *adm_access, svn_client_ctx_t *ctx, apr_pool_t *pool) { const char *session_url = NULL, *url; svn_revnum_t target_rev; svn_opt_revision_t peg_revision; apr_pool_t *sesspool = NULL; /* Assert that we have sane input. */ assert(SVN_IS_VALID_REVNUM(start) && SVN_IS_VALID_REVNUM(end) && (start > end)); /* First, we get the real mergeinfo. */ SVN_ERR(svn_client__get_wc_or_repos_mergeinfo(recorded_mergeinfo, entry, indirect, FALSE, inherit, ra_session, target_wcpath, adm_access, ctx, pool)); peg_revision.kind = svn_opt_revision_working; SVN_ERR(svn_client__derive_location(&url, &target_rev, target_wcpath, &peg_revision, ra_session, adm_access, ctx, pool)); 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(pool); return SVN_NO_ERROR; } /* Temporarily point our RA_SESSION at our target URL so we can fetch so-called "implicit mergeinfo" (that is, natural history). */ if (ra_session) { SVN_ERR(svn_client__ensure_ra_session_url(&session_url, ra_session, url, pool)); } else { sesspool = svn_pool_create(pool); SVN_ERR(svn_client__open_ra_session_internal(&ra_session, url, NULL, NULL, NULL, FALSE, TRUE, ctx, sesspool)); } /* 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 the source in the youngest requested revision (which is all the underlying APIs would do in this case right now anyway). */ if (target_rev < start) { const char *start_url; svn_opt_revision_t requested, unspec, pegrev, *start_revision; unspec.kind = svn_opt_revision_unspecified; requested.kind = svn_opt_revision_number; requested.value.number = start; pegrev.kind = svn_opt_revision_number; pegrev.value.number = target_rev; SVN_ERR(svn_client__repos_locations(&start_url, &start_revision, NULL, NULL, ra_session, url, &pegrev, &requested, &unspec, ctx, pool)); /* ### FIXME: Having a low-brain moment. Shouldn't we check that START_URL matches our session URL at this point? */ target_rev = start; } /* Fetch the implicit mergeinfo. */ peg_revision.kind = svn_opt_revision_number; peg_revision.value.number = target_rev; SVN_ERR(svn_client__get_history_as_mergeinfo(implicit_mergeinfo, url, &peg_revision, start, end, ra_session, NULL, ctx, pool)); /* If we created an RA_SESSION above, destroy it. Otherwise, if reparented an existing session, point it back where it was when we were called. */ if (sesspool) { svn_pool_destroy(sesspool); } else if (session_url) { SVN_ERR(svn_ra_reparent(ra_session, session_url, pool)); } return SVN_NO_ERROR; } /* Helper for do_directory_merge(). For each child in CHILDREN_WITH_MERGEINFO, populates that child's remaining_ranges list. CHILDREN_WITH_MERGEINFO is expected to be sorted in depth first order. All persistent allocations are from CHILDREN_WITH_MERGEINFO->pool. If HONOR_MERGEINFO is set, 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 revision1, to revision2. See `MERGEINFO MERGE SOURCE NORMALIZATION' for more requirements around the values of URL1, REVISION1, URL2, and REVISION2. */ static svn_error_t * populate_remaining_ranges(apr_array_header_t *children_with_mergeinfo, const char *source_root_url, const char *url1, svn_revnum_t revision1, const char *url2, svn_revnum_t revision2, svn_boolean_t inheritable, svn_boolean_t honor_mergeinfo, svn_ra_session_t *ra_session, const char *parent_merge_src_canon_path, svn_wc_adm_access_t *adm_access, merge_cmd_baton_t *merge_b) { apr_pool_t *iterpool, *pool; int merge_target_len = strlen(merge_b->target); int i; pool = children_with_mergeinfo->pool; iterpool = svn_pool_create(pool); /* If we aren't honoring mergeinfo or this is a --record-only merge, we'll make quick work of this by simply adding dummy REVISION1:REVISION2 ranges for all children. */ if (! honor_mergeinfo || 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_merge_range_t *range = apr_pcalloc(pool, sizeof(*range)); range->start = revision1; range->end = revision2; range->inheritable = inheritable; child->remaining_ranges = apr_array_make(pool, 1, sizeof(svn_merge_range_t *)); APR_ARRAY_PUSH(child->remaining_ranges, svn_merge_range_t *) = range; } return SVN_NO_ERROR; } for (i = 0; i < children_with_mergeinfo->nelts; i++) { const char *child_repos_path; const svn_wc_entry_t *child_entry; const char *child_url1, *child_url2; svn_client__merge_path_t *child = APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *); svn_client__merge_path_t *parent = NULL; /* If the path is absent don't do subtree merge either. */ if (!child || child->absent) continue; svn_pool_clear(iterpool); if (strlen(child->path) == merge_target_len) child_repos_path = ""; else child_repos_path = child->path + (merge_target_len ? merge_target_len + 1 : 0); child_url1 = svn_path_url_add_component(url1, child_repos_path, iterpool); child_url2 = svn_path_url_add_component(url2, child_repos_path, iterpool); SVN_ERR(svn_wc__entry_versioned(&child_entry, child->path, adm_access, FALSE, iterpool)); SVN_ERR(get_full_mergeinfo(&(child->pre_merge_mergeinfo), &(child->implicit_mergeinfo), child_entry, &(child->indirect_mergeinfo), svn_mergeinfo_inherited, ra_session, child->path, MAX(revision1, revision2), MIN(revision1, revision2), adm_access, merge_b->ctx, pool)); /* If CHILD isn't the merge target find its parent. */ if (i > 0) { int parent_index = find_nearest_ancestor(children_with_mergeinfo, FALSE, child->path); parent = APR_ARRAY_IDX(children_with_mergeinfo, parent_index, svn_client__merge_path_t *); if (!parent) { /* If CHILD is a subtree then its parent must be in CHILDREN_WITH_MERGEINFO, see the global comment 'THE CHILDREN_WITH_MERGEINFO ARRAY'. */ abort(); } } SVN_ERR(calculate_remaining_ranges(parent, child, source_root_url, child_url1, revision1, child_url2, revision2, child->pre_merge_mergeinfo, child->implicit_mergeinfo, i > 0 ? TRUE : FALSE, /* is subtree */ ra_session, child_entry, merge_b->ctx, pool)); } /* Take advantage of the depth first ordering, i.e first(0th) item is target.*/ if (children_with_mergeinfo->nelts > 1) { svn_client__merge_path_t *child = APR_ARRAY_IDX(children_with_mergeinfo, 0, svn_client__merge_path_t *); if (child->remaining_ranges->nelts == 0) { svn_merge_range_t *dummy_range = apr_pcalloc(pool, sizeof(*dummy_range)); dummy_range->start = revision2; dummy_range->end = revision2; dummy_range->inheritable = inheritable; child->remaining_ranges = apr_array_make(pool, 1, sizeof(dummy_range)); APR_ARRAY_PUSH(child->remaining_ranges, svn_merge_range_t *) = dummy_range; merge_b->target_has_dummy_merge_range = TRUE; } } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } /*-----------------------------------------------------------------------*/ /*** Other Helper Functions ***/ /* Create mergeinfo describing the merge of RANGELIST into TARGET_WCPATH, accounting for paths unaffected by the merge due to skips or conflicts from NOTIFY_B. For 'immediates' merge it sets an inheritable mergeinfo corresponding to current merge on merge target. For 'files' merge it sets an inheritable mergeinfo corrsponding to current merge on merged files. If TARGET_WCPATH is a directory and it is missing an immediate child then TARGET_MISSING_CHILD should be true, otherwise it is false.*/ static svn_error_t * determine_merges_performed(apr_hash_t **merges, const char *target_wcpath, apr_array_header_t *rangelist, svn_depth_t depth, svn_wc_adm_access_t *adm_access, notification_receiver_baton_t *notify_b, merge_cmd_baton_t *merge_b, apr_pool_t *pool) { apr_size_t nbr_skips = (notify_b->skipped_paths != NULL ? apr_hash_count(notify_b->skipped_paths) : 0); *merges = apr_hash_make(pool); apr_hash_set(*merges, target_wcpath, APR_HASH_KEY_STRING, rangelist); if (nbr_skips > 0) { apr_hash_index_t *hi; /* Override the mergeinfo for child paths which weren't actually merged. */ for (hi = apr_hash_first(NULL, notify_b->skipped_paths); hi; hi = apr_hash_next(hi)) { const void *skipped_path; svn_wc_status2_t *status; apr_hash_this(hi, &skipped_path, NULL, NULL); /* Before we override, make sure this is a versioned path, it might be an unversioned obstruction. */ SVN_ERR(svn_wc_status2(&status, (const char *) skipped_path, adm_access, pool)); if (status->text_status == svn_wc_status_none || status->text_status == svn_wc_status_unversioned) 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. */ apr_hash_set(*merges, (const char *) skipped_path, APR_HASH_KEY_STRING, apr_array_make(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? */ ; } } if ((depth != svn_depth_infinity) && notify_b->merged_paths) { apr_hash_index_t *hi; const void *merged_path; for (hi = apr_hash_first(NULL, notify_b->merged_paths); hi; hi = apr_hash_next(hi)) { const svn_wc_entry_t *child_entry; apr_array_header_t *rangelist_of_child = NULL; apr_hash_this(hi, &merged_path, NULL, NULL); SVN_ERR(svn_wc__entry_versioned(&child_entry, merged_path, adm_access, FALSE, pool)); if (((child_entry->kind == svn_node_dir) && (strcmp(merge_b->target, merged_path) == 0) && (depth == svn_depth_immediates)) || ((child_entry->kind == svn_node_file) && (depth == svn_depth_files))) { /* Set the explicit inheritable mergeinfo for, 1. Merge target directory if depth is immediates. 2. If merge is on a file and requested depth is 'files'. */ int i; rangelist_of_child = svn_rangelist_dup(rangelist, pool); for (i = 0; i < rangelist_of_child->nelts; i++) { svn_merge_range_t *rng = APR_ARRAY_IDX(rangelist_of_child, i, svn_merge_range_t *); rng->inheritable = TRUE; } } if (rangelist_of_child) { apr_hash_set(*merges, (const char *)merged_path, APR_HASH_KEY_STRING, rangelist_of_child); } } } return SVN_NO_ERROR; } /* Calculate the new mergeinfo for the target tree based on the merge info for TARGET_WCPATH and MERGES (a mapping of WC paths to range lists), and record it in the WC (at, and possibly below, TARGET_WCPATH). */ static svn_error_t * update_wc_mergeinfo(const char *target_wcpath, const svn_wc_entry_t *entry, const char *repos_rel_path, apr_hash_t *merges, svn_boolean_t is_rollback, svn_wc_adm_access_t *adm_access, svn_client_ctx_t *ctx, apr_pool_t *pool) { apr_pool_t *subpool = svn_pool_create(pool); const char *rel_path; svn_mergeinfo_catalog_t mergeinfo; 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(pool, merges); hi; hi = apr_hash_next(hi)) { const void *key; void *value; const char *path; apr_array_header_t *ranges, *rangelist; int len; svn_error_t *err; svn_pool_clear(subpool); apr_hash_this(hi, &key, NULL, &value); path = key; ranges = value; /* 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, entry, path, FALSE, adm_access, ctx, subpool); /* 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) { svn_error_clear(err); continue; } else { return err; } } /* If we are attempting to set empty revision range override mergeinfo on a path with no explicit mergeinfo, we first need the pristine mergeinfo that path inherits. */ if (mergeinfo == NULL && ranges->nelts == 0) { svn_boolean_t inherited; SVN_ERR(svn_client__get_wc_mergeinfo(&mergeinfo, &inherited, TRUE, svn_mergeinfo_nearest_ancestor, entry, path, NULL, NULL, adm_access, ctx, subpool)); } if (mergeinfo == NULL) mergeinfo = apr_hash_make(subpool); /* ASSUMPTION: "target_wcpath" is always both a parent and prefix of "path". */ len = strlen(target_wcpath); if (len < strlen(path)) { const char *path_relative_to_target = len?(path + len + 1):(path); rel_path = apr_pstrcat(subpool, repos_rel_path, "/", path_relative_to_target, NULL); } else rel_path = repos_rel_path; rangelist = apr_hash_get(mergeinfo, rel_path, APR_HASH_KEY_STRING); if (rangelist == NULL) rangelist = apr_array_make(subpool, 0, sizeof(svn_merge_range_t *)); if (is_rollback) { ranges = svn_rangelist_dup(ranges, subpool); SVN_ERR(svn_rangelist_reverse(ranges, subpool)); SVN_ERR(svn_rangelist_remove(&rangelist, ranges, rangelist, FALSE, subpool)); } else { SVN_ERR(svn_rangelist_merge(&rangelist, ranges, subpool)); } /* Update the mergeinfo by adjusting the path's rangelist. */ apr_hash_set(mergeinfo, rel_path, APR_HASH_KEY_STRING, rangelist); if (is_rollback && apr_hash_count(mergeinfo) == 0) mergeinfo = NULL; svn_mergeinfo__remove_empty_rangelists(mergeinfo, pool); err = svn_client__record_wc_mergeinfo(path, mergeinfo, adm_access, subpool); 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(subpool); return SVN_NO_ERROR; } /* Create and return an error structure appropriate for the unmerged revisions range(s). */ static APR_INLINE svn_error_t * make_merge_conflict_error(const char *target_wcpath, svn_merge_range_t *r, apr_pool_t *pool) { return 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"), r->start, r->end, svn_path_local_style(target_wcpath, pool)); } /* 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. Also remove the path from the NOTIFY_B->SKIPPED_PATHS hash. */ static void remove_absent_children(const char *target_wcpath, apr_array_header_t *children_with_mergeinfo, notification_receiver_baton_t *notify_b) { /* 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 && (child->absent || child->scheduled_for_deletion) && svn_path_is_ancestor(target_wcpath, child->path)) { if (notify_b->skipped_paths) apr_hash_set(notify_b->skipped_paths, child->path, APR_HASH_KEY_STRING, NULL); APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *) = NULL; } } } /* Helper for do_directory_merge(). Set up the diff editor report to merge URL1@REVISION1 to URL2@REVISION2 into TARGET_WCPATH and drive it. Properly describe any subtrees of TARGET_WCPATH that require only a subset of REVISION1:REVISION2 to be merged -- These 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 that TARGET_WCPATH needs only a subset of REVISION1:REVISION2 while its subtrees need the entire range. REVISION1 and REVISION2 must be bound by the set of remaining_ranges fields in CHILDREN_WITH_MERGEINFO's elements, specifically: 1) For forward merges the oldest revision in all the remaining_ranges must be equal to REVISION1 and the youngest revision in the *first* range of all the remaining ranges must be equal to REVISION2. 2) For reverse merges the youngest revision in all the remaining_ranges must be equal to REVISION1 and the oldest revision in the *first* range of all the remaining ranges must be equal to REVISION2. If IS_ROLLBACK is true this is a reverse merge, otherwise it is a forward merge. DEPTH, NOTIFY_B, ADM_ACCESS, and MERGE_B are cascasded from do_directory_merge(), see that function for more info. CALLBACKS are the svn merge versions of the svn_wc_diff_callbacks3_t callbacks invoked by the editor. If MERGE_B->sources_ancestral is set, then URL1@REVISION1 must be a historical ancestor of URL2@REVISION2, or vice-versa (see `MERGEINFO MERGE SOURCE NORMALIZATION' for more requirements around the values of URL1, REVISION1, URL2, and REVISION2 in this case). */ static svn_error_t * drive_merge_report_editor(const char *target_wcpath, const char *url1, svn_revnum_t revision1, const char *url2, svn_revnum_t revision2, apr_array_header_t *children_with_mergeinfo, svn_boolean_t is_rollback, svn_depth_t depth, notification_receiver_baton_t *notify_b, svn_wc_adm_access_t *adm_access, const svn_wc_diff_callbacks2_t *callbacks, merge_cmd_baton_t *merge_b, apr_pool_t *pool) { const svn_ra_reporter3_t *reporter; const svn_delta_editor_t *diff_editor; void *diff_edit_baton; void *report_baton; svn_revnum_t default_start, target_start; svn_boolean_t honor_mergeinfo; const char *old_sess2_url; mergeinfo_behavior(&honor_mergeinfo, NULL, merge_b); /* Start with a safe default starting revision for the editor and the merge target. */ default_start = target_start = revision1; /* If we are honoring mergeinfo the starting revision for the merge target might not be REVISION1, in fact the merge target might not need *any* part of REVISION1:REVISION2 merged -- Instead some subtree of the target needs REVISION1:REVISION2 -- So get the right starting revision for the target. */ if (honor_mergeinfo) { if (merge_b->target_has_dummy_merge_range) { /* The merge target doesn't need anything merged. */ target_start = revision2; } else if (children_with_mergeinfo && 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'. */ svn_client__merge_path_t *child = APR_ARRAY_IDX(children_with_mergeinfo, 0, svn_client__merge_path_t *); if (child->remaining_ranges->nelts) { /* The merge target has remaining revisions to merge. These ranges may fully or partially overlap the range described by REVISION1:REVISION2 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 > revision2) || (is_rollback && range->start < revision2)) { /* Merge target's first remaining range doesn't intersect. */ target_start = revision2; } else { /* Merge target's first remaining range partially or fully overlaps. */ target_start = range->start; } } } } /* Temporarily point our second RA session to URL1, too. We use this to request individual file contents. */ SVN_ERR(svn_client__ensure_ra_session_url(&old_sess2_url, merge_b->ra_session2, url1, pool)); /* Get the diff editor and a reporter with which to, ultimately, drive it. */ SVN_ERR(svn_client__get_diff_editor(target_wcpath, adm_access, callbacks, merge_b, depth, merge_b->dry_run, merge_b->ra_session2, default_start, notification_receiver, notify_b, merge_b->ctx->cancel_func, merge_b->ctx->cancel_baton, &diff_editor, &diff_edit_baton, pool)); SVN_ERR(svn_ra_do_diff3(merge_b->ra_session1, &reporter, &report_baton, revision2, "", depth, merge_b->ignore_ancestry, TRUE, /* text_deltas */ url2, diff_editor, diff_edit_baton, pool)); /* Drive the reporter. */ SVN_ERR(reporter->set_path(report_baton, "", target_start, depth, FALSE, NULL, 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. */ apr_size_t target_wcpath_len = strlen(target_wcpath); int i; /* Start with CHILDREN_WITH_MERGEINFO[1], CHILDREN_WITH_MERGEINFO[0] is always the merge target (TARGET_WCPATH). */ for (i = 1; i < children_with_mergeinfo->nelts; i++) { svn_merge_range_t *range; const char *child_repos_path; svn_client__merge_path_t *parent; svn_client__merge_path_t *child = APR_ARRAY_IDX(children_with_mergeinfo, i, svn_client__merge_path_t *); int parent_index; svn_boolean_t nearest_parent_is_target; if (!child || child->absent) continue; /* Find this child's nearest wc ancestor with mergeinfo. */ parent_index = find_nearest_ancestor(children_with_mergeinfo, FALSE, child->path); parent = APR_ARRAY_IDX(children_with_mergeinfo, parent_index, svn_client__merge_path_t *); /* Note if the child's parent is the merge target. */ nearest_parent_is_target = (strcmp(parent->path, target_wcpath) == 0) ? TRUE : FALSE; /* If a subtree needs the same range applied as it's nearest parent with mergeinfo or neither the subtree nor its nearest parent need REVISION1:REVISION2 merged, then we don't need to describe the subtree separately. In the latter case this could break the editor if child->path didn't exist at REVISION2 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 > revision2) || (is_rollback && range->start < revision2)) { /* Neither subtree nor parent need any part of REVISION1:REVISION2. */ 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 the subtree's parent is the merge target, then the parent can have a dummy range; this is still the same as no remaining ranges. */ if (parent->remaining_ranges->nelts == 0 || (nearest_parent_is_target && merge_b->target_has_dummy_merge_range)) continue; /* Same as parent. */ } /* Ok, we really need to describe this subtree as it needs different ranges applied than its nearest working copy parent. */ child_repos_path = child->path + (target_wcpath_len ? target_wcpath_len + 1 : 0); if ((child->remaining_ranges->nelts == 0) || (is_rollback && (range->start < revis