/* * 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 "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 | | * | -r | (svn_client_merge_peg) | | * |----+--------------------------------+ | * | | mergeinfo-driven | unsupported | * | | 'cherry harvest', i.e. merge | | * | | all revisions from URL that | | * | no | have not already been merged | | * | -c | (svn_client_merge_peg) | | * | or +--------------------------------+---------------------+ * | -r | mergeinfo-driven | mergeinfo-writing | * | | whole-branch | diff-and-apply | * | | heuristic merge | (svn_client_merge) | * | | (svn_client_merge_reintegrate) | | * +----+--------------------------------+---------------------+ * * */ /* THE CHILDREN_WITH_MERGEINFO ARRAY * * Many of the helper functions in this file pass around an * apr_array_header_t *CHILDREN_WITH_MERGEINFO. This is a depth first * sorted array filled with svn_client__merge_path_t * describing the * merge target and any of its subtrees which have explicit mergeinfo * or otherwise need special attention during a merge. * * CHILDREN_WITH_MERGEINFO is initially 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, FALSE, 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 ***/ typedef struct merge_source_t { /* "left" side URL and revision (inclusive iff youngest) */ const char *url1; svn_revnum_t rev1; /* "right" side URL and revision (inclusive iff youngest) */ const char *url2; svn_revnum_t rev2; } merge_source_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 */ /* The left and right URLs and revs. The value of this field changes to reflect the merge_source_t *currently* being merged by do_merge(). */ merge_source_t merge_source; 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 or DRY_RUN is true. */ 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; /* During the merge, *USE_SLEEP is set to TRUE if a sleep will be required afterwards to ensure timestamp integrity, or unchanged if not. */ svn_boolean_t *use_sleep; /* Pool which has a lifetime limited to one iteration over a given merge source, i.e. it is cleared on every call to do_directory_merge() or do_file_merge() in do_merge(). */ apr_pool_t *pool; } 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; } /* Return true iff we're in dry-run mode and WCPATH would have been deleted by now if we weren't in dry-run mode. 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); } /* Record a tree conflict in the WC, unless this is a dry run or a record- * only merge. * * The tree conflict, with its victim specified by VICTIM_PATH, is * assumed to have happened during a merge using merge baton MERGE_B. * * ADM_ACCESS must correspond to the victim's parent directory (even if * the victim is a directory). * * NODE_KIND must be the node kind of "old" and "theirs" and "mine"; * this function cannot cope with node kind clashes. * ACTION and REASON correspond to the fields * of the same names in svn_wc_tree_conflict_description_t. */ static svn_error_t* tree_conflict(merge_cmd_baton_t *merge_b, svn_wc_adm_access_t *adm_access, const char *victim_path, svn_node_kind_t node_kind, svn_wc_conflict_action_t action, svn_wc_conflict_reason_t reason) { svn_wc_conflict_description_t *conflict; const char *src_repos_url; /* root URL of source repository */ const char *left_url; const char *right_url; svn_wc_conflict_version_t *left; svn_wc_conflict_version_t *right; if (merge_b->record_only || merge_b->dry_run) return SVN_NO_ERROR; SVN_ERR(svn_ra_get_repos_root2(merge_b->ra_session1, &src_repos_url, merge_b->pool)); /* Construct the source URLs of the victim. */ { const char *child = svn_path_is_child(merge_b->target, victim_path, merge_b->pool); if (child != NULL) { left_url = svn_path_url_add_component(merge_b->merge_source.url1, child, merge_b->pool); right_url = svn_path_url_add_component(merge_b->merge_source.url2, child, merge_b->pool); } else { left_url = merge_b->merge_source.url1; right_url = merge_b->merge_source.url2; } } left = svn_wc_conflict_version_create( src_repos_url, svn_path_is_child(src_repos_url, left_url, merge_b->pool), merge_b->merge_source.rev1, node_kind, merge_b->pool); right = svn_wc_conflict_version_create( src_repos_url, svn_path_is_child(src_repos_url, right_url, merge_b->pool), merge_b->merge_source.rev2, node_kind, merge_b->pool); conflict = svn_wc_conflict_description_create_tree( victim_path, adm_access, node_kind, svn_wc_operation_merge, left, right, merge_b->pool); conflict->action = action; conflict->reason = reason; SVN_ERR(svn_wc__add_tree_conflict(conflict, adm_access, merge_b->pool)); return SVN_NO_ERROR; } /* Set *HONOR_MERGEINFO and *RECORD_MERGEINFO (if non-NULL) based on the merge being performed as described in MERGE_B. If the merge source server is is capable of merge tracking, the left-side merge source is an ancestor of the right-side (or vice-versa), the merge source repository is the same repository as the MERGE_B->target, and ancestry is being considered then set *HONOR_MERGEINFO to true, otherwise set it to false. If *HONOR_MERGEINFO is set to TRUE and the merge is not a dry run then set *RECORD_MERGEINFO to true, otherwise set it to false. **/ static APR_INLINE void mergeinfo_behavior(svn_boolean_t *honor_mergeinfo_p, svn_boolean_t *record_mergeinfo_p, merge_cmd_baton_t *merge_b) { svn_boolean_t honor_mergeinfo = (merge_b->mergeinfo_capable && merge_b->sources_ancestral && merge_b->same_repos && (! merge_b->ignore_ancestry)); if (honor_mergeinfo_p) *honor_mergeinfo_p = honor_mergeinfo; if (record_mergeinfo_p) *record_mergeinfo_p = (honor_mergeinfo && (! 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 or replaced 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; } /* An svn_wc_diff_callbacks3_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, svn_boolean_t *tree_conflicted, 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; if (tree_conflicted) *tree_conflicted = FALSE; /* ### TODO check tree-conflicts! */ 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_props2() 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)); /* If this is a forward merge then don't add new mergeinfo to PATH that is already part of PATH's own history. */ if (merge_b->merge_source.rev1 < merge_b->merge_source.rev2) 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); /* If this is not a dry run then make a record in BATON if we find a PATH where mergeinfo is added where none existed previously. */ if (!merge_b->dry_run) { 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, this is a tree-conflict. */ if (state) *state = svn_wc_notify_state_missing; if (tree_conflicted) *tree_conflicted = TRUE; 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; } /* An svn_wc_diff_callbacks3_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, svn_boolean_t *tree_conflicted, 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; if (tree_conflicted) *tree_conflicted = FALSE; /* 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; /* Trying to change a file at a non-existing path. * Although this is a tree-conflict, it will already have been * raised by the merge_dir_opened() callback. Not raising additional tree * conflicts for the child nodes inside. */ 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)) { /* This is use case 4 described in the paper attached to issue * #2282. See also notes/tree-conflicts/detection.txt */ SVN_ERR(tree_conflict(merge_b, adm_access, mine, svn_node_file, svn_wc_conflict_action_edit, svn_wc_conflict_reason_missing)); if (tree_conflicted) *tree_conflicted = TRUE; 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_boolean_t tree_conflicted2; SVN_ERR(merge_props_changed(adm_access, prop_state, &tree_conflicted2, mine, prop_changes, original_props, baton)); /* If the prop change caused a tree-conflict, just bail. */ if (tree_conflicted2) { if (tree_conflicted != NULL) *tree_conflicted = TRUE; svn_pool_destroy(subpool); return SVN_NO_ERROR; } } 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; } /* An svn_wc_diff_callbacks3_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, svn_boolean_t *tree_conflicted, 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; if (tree_conflicted) *tree_conflicted = FALSE; /* 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; /* Trying to add a file at a non-existing path. * Although this is a tree-conflict, it will already have been * raised by the merge_dir_opened() callback. Not raising additional tree * conflicts for the child nodes inside. */ 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. * * The file add the merge wants to carry out is obstructed by * something which isn't present on disk but which is recorded * in meta data. So the file the merge wants to add is a tree * conflict victim. * See notes about obstructions in * notes/tree-conflicts/detection.txt. */ SVN_ERR(tree_conflict(merge_b, adm_access, mine, svn_node_file, svn_wc_conflict_action_add, svn_wc_conflict_reason_obstructed)); if (tree_conflicted) *tree_conflicted = TRUE; 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; svn_stream_t *new_base_contents; /* If this is a merge from the same repository as our working copy, we handle adds as add-with-history. Otherwise, we'll use a pure add. */ if (merge_b->same_repos) { const char *child = svn_path_is_child(merge_b->target, mine, subpool); if (child != NULL) copyfrom_url = svn_path_url_add_component(merge_b->merge_source.url2, child, subpool); else copyfrom_url = merge_b->merge_source.url2; copyfrom_rev = rev2; SVN_ERR(check_scheme_match(adm_access, copyfrom_url)); } SVN_ERR(svn_stream_open_readonly(&new_base_contents, yours, subpool, subpool)); /* 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'. */ /* ### would be nice to have cancel/notify funcs to pass */ SVN_ERR(svn_wc_add_repos_file3( mine, adm_access, new_base_contents, NULL, new_props, NULL, copyfrom_url, copyfrom_rev, NULL, NULL, NULL, NULL, subpool)); /* ### delete 'yours' ? */ } 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: /* The file add the merge wants to carry out is obstructed by * a directory, so the file the merge wants to add is a tree * conflict victim. * See notes about obstructions in notes/tree-conflicts/detection.txt. */ SVN_ERR(tree_conflict(merge_b, adm_access, mine, svn_node_file, svn_wc_conflict_action_add, svn_wc_conflict_reason_obstructed)); if (tree_conflicted) *tree_conflicted = TRUE; 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) { /* The file add the merge wants to carry out is obstructed by * an unversioned file, so this path should be skipped. */ /* 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 { /* The file add the merge wants to carry out is obstructed by * a versioned file, so the file the merge wants to add is a * tree conflict victim. See notes about obstructions in * notes/tree-conflicts/detection.txt. */ SVN_ERR(tree_conflict(merge_b, adm_access, mine, svn_node_file, svn_wc_conflict_action_add, svn_wc_conflict_reason_obstructed)); if (tree_conflicted) *tree_conflicted = TRUE; } } break; } default: if (content_state) *content_state = svn_wc_notify_state_unknown; break; } svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* Compare the two sets of properties PROPS1 and PROPS2, ignoring the * "svn:mergeinfo" property, and noticing only "normal" props. Set *SAME to * true if the rest of the properties are identical or false if they differ. */ static svn_error_t * properties_same_p(svn_boolean_t *same, apr_hash_t *props1, apr_hash_t *props2, apr_pool_t *pool) { apr_array_header_t *prop_changes; int i, diffs; /* Examine the properties that differ */ SVN_ERR(svn_prop_diffs(&prop_changes, props1, props2, pool)); diffs = 0; for (i = 0; i < prop_changes->nelts; i++) { const char *pname = APR_ARRAY_IDX(prop_changes, i, svn_prop_t).name; /* Count the properties we're interested in; ignore the rest */ if (svn_wc_is_normal_prop(pname) && strcmp(pname, SVN_PROP_MERGEINFO) != 0) diffs++; } *same = (diffs == 0); return SVN_NO_ERROR; } /* Compare the file OLDER (together with its normal properties in * ORIGINAL_PROPS which may also contain WC props and entry props) and MINE * (with its properties obtained from its WC admin area ADM_ACCESS). Set * *SAME to true if they are the same or false if they differ, ignoring * the "svn:mergeinfo" property, and ignoring differences in keyword * expansion and end-of-line style. */ static svn_error_t * files_same_p(svn_boolean_t *same, const char *older, apr_hash_t *original_props, const char *mine, svn_wc_adm_access_t *adm_access, apr_pool_t *pool) { apr_hash_t *working_props; SVN_ERR(svn_wc_prop_list(&working_props, mine, adm_access, pool)); /* Compare the properties */ SVN_ERR(properties_same_p(same, original_props, working_props, pool)); if (*same) { svn_boolean_t modified; /* Compare the file content, translating 'mine' to 'normal' form. */ SVN_ERR(svn_wc__versioned_file_modcheck(&modified, mine, adm_access, older, TRUE, pool)); *same = !modified; } return SVN_NO_ERROR; } /* An svn_wc_diff_callbacks3_t function. */ static svn_error_t * merge_file_deleted(svn_wc_adm_access_t *adm_access, svn_wc_notify_state_t *state, svn_boolean_t *tree_conflicted, 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; if (*tree_conflicted) tree_conflicted = FALSE; /* 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_boolean_t same; /* If the files are identical, attempt deletion */ SVN_ERR(files_same_p(&same, older, original_props, mine, adm_access, subpool)); if (same || merge_b->force || merge_b->record_only /* ### why? */) { /* Passing NULL for the notify_func and notify_baton because repos_diff.c:delete_entry() will do it for us. */ SVN_ERR(svn_client__wc_delete(mine, adm_access, TRUE, merge_b->dry_run, FALSE, NULL, NULL, merge_b->ctx, subpool)); if (state) *state = svn_wc_notify_state_changed; } else { /* The files differ, so raise a conflict instead of deleting */ /* This might be use case 5 described in the paper attached to issue * #2282. See also notes/tree-conflicts/detection.txt */ SVN_ERR(tree_conflict(merge_b, adm_access, mine, svn_node_file, svn_wc_conflict_action_delete, svn_wc_conflict_reason_edited)); if (tree_conflicted) *tree_conflicted = TRUE; if (state) *state = svn_wc_notify_state_obstructed; } } break; case svn_node_dir: /* The file deletion the merge wants to carry out is obstructed by * a directory, so the file the merge wants to delete is a tree * conflict victim. * See notes about obstructions in notes/tree-conflicts/detection.txt. */ SVN_ERR(tree_conflict(merge_b, adm_access, mine, svn_node_file, svn_wc_conflict_action_delete, svn_wc_conflict_reason_obstructed)); if (tree_conflicted) *tree_conflicted = TRUE; if (state) *state = svn_wc_notify_state_obstructed; break; case svn_node_none: /* The file deleted in the diff does not exist at the current URL. * * This is use case 6 described in the paper attached to issue * #2282. See also notes/tree-conflicts/detection.txt */ SVN_ERR(tree_conflict(merge_b, adm_access, mine, svn_node_file, svn_wc_conflict_action_delete, svn_wc_conflict_reason_deleted)); if (tree_conflicted) *tree_conflicted = TRUE; 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; } /* An svn_wc_diff_callbacks3_t function. */ static svn_error_t * merge_dir_added(svn_wc_adm_access_t *adm_access, svn_wc_notify_state_t *state, svn_boolean_t *tree_conflicted, 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; if (tree_conflicted) *tree_conflicted = FALSE; /* 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; } /* Trying to add a directory at a non-existing path. * Although this is a tree-conflict, it will already have been * raised by the merge_dir_opened() callback. Not raising additional tree * conflicts for the child nodes inside. */ svn_pool_destroy(subpool); return SVN_NO_ERROR; } child = svn_path_is_child(merge_b->target, path, subpool); SVN_ERR_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->merge_source.url2, child, subpool); copyfrom_rev = rev; SVN_ERR(check_scheme_match(adm_access, copyfrom_url)); } /* Find the version-control state of this path */ SVN_ERR(svn_wc_entry(&entry, path, adm_access, TRUE, subpool)); /* Switch on the on-disk state of this path */ SVN_ERR(svn_io_check_path(path, &kind, subpool)); switch (kind) { case svn_node_none: 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; } /* Unversioned or schedule-delete */ 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_add3(path, adm_access, svn_depth_infinity, 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 */ if (! entry || entry->schedule == svn_wc_schedule_delete) { /* The dir is not known to Subversion, or is schedule-delete. * We will make it schedule-add. */ if (!merge_b->dry_run) SVN_ERR(svn_wc_add3(path, adm_access, svn_depth_infinity, 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 { /* The dir is known to Subversion as already existing. */ if (dry_run_deleted_p(merge_b, path)) { if (state) *state = svn_wc_notify_state_changed; } else { /* This is a tree conflict. */ SVN_ERR(tree_conflict(merge_b, adm_access, path, svn_node_dir, svn_wc_conflict_action_add, svn_wc_conflict_reason_added)); if (tree_conflicted) *tree_conflicted = TRUE; if (state) *state = svn_wc_notify_state_obstructed; } } break; case svn_node_file: if (merge_b->dry_run) merge_b->added_path = NULL; 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. */ if (state) *state = svn_wc_notify_state_changed; } else { /* Obstructed: we can't add a dir because there's a file here * (whatever the entry says should be here). */ SVN_ERR(tree_conflict(merge_b, adm_access, path, svn_node_dir, svn_wc_conflict_action_add, svn_wc_conflict_reason_obstructed)); if (tree_conflicted) *tree_conflicted = TRUE; if (state) *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; } /* An svn_wc_diff_callbacks3_t function. */ static svn_error_t * merge_dir_deleted(svn_wc_adm_access_t *adm_access, svn_wc_notify_state_t *state, svn_boolean_t *tree_conflicted, 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; const svn_wc_entry_t *entry; svn_wc_adm_access_t *parent_access; const char *parent_path; svn_error_t *err; if (tree_conflicted) *tree_conflicted = FALSE; /* 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; /* Trying to delete a directory at a non-existing path. * Although this is a tree-conflict, it will already have been * raised by the merge_dir_opened() callback. Not raising additional tree * conflicts for the child nodes inside. */ svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* Find the version-control state of this path */ SVN_ERR(svn_wc_entry(&entry, path, adm_access, TRUE, subpool)); /* Switch on the on-disk state of this path */ SVN_ERR(svn_io_check_path(path, &kind, subpool)); switch (kind) { case svn_node_dir: { if (entry && (entry->schedule != svn_wc_schedule_delete)) { /* ### TODO: Before deleting, we should ensure that this dir tree is equal to the one we're being asked to delete. If not, mark this directory as a tree conflict victim, because this could be use case 5 as described in notes/tree-conflicts/detection.txt. */ 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) { if (state) *state = svn_wc_notify_state_obstructed; svn_error_clear(err); } else { if (state) *state = svn_wc_notify_state_changed; } } else { /* Dir is already not under version control at this path. */ /* Raise a tree conflict. */ SVN_ERR(tree_conflict(merge_b, adm_access, path, svn_node_dir, svn_wc_conflict_action_delete, svn_wc_conflict_reason_deleted)); if (tree_conflicted) *tree_conflicted = TRUE; } } 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 use case 6 as described in * notes/tree-conflicts/detection.txt. * This case was formerly treated as no-op. */ SVN_ERR(tree_conflict(merge_b, adm_access, path, svn_node_dir, svn_wc_conflict_action_delete, svn_wc_conflict_reason_deleted)); if (tree_conflicted) *tree_conflicted = TRUE; 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; } /* An svn_wc_diff_callbacks3_t function. */ static svn_error_t * merge_dir_opened(svn_wc_adm_access_t *adm_access, svn_boolean_t *tree_conflicted, const char *path, svn_revnum_t rev, void *baton) { if (tree_conflicted) *tree_conflicted = FALSE; if (adm_access == NULL) { /* Trying to open a directory at a non-existing path. * Although this is a tree-conflict, it will already have been * raised by the merge_dir_opened() callback on the topmost nonexisting * ancestor, where an adm_access was still present. Not raising * additional tree conflicts for the child nodes inside. */ /* ### TODO: Verify that this holds true for explicit targets that * # point deep into a nonexisting subtree. */ return SVN_NO_ERROR; } /* Detect a tree-conflict, if any. */ { 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; /* Find the version-control and on-disk states of this path */ SVN_ERR(svn_wc_entry(&entry, path, adm_access, TRUE, subpool)); SVN_ERR(svn_io_check_path(path, &kind, subpool)); /* If we're trying to open a directory that's not a directory, * raise a tree conflict. */ if (!entry || entry->schedule == svn_wc_schedule_delete || kind != svn_node_dir) { SVN_ERR(tree_conflict(merge_b, adm_access, path, svn_node_dir, svn_wc_conflict_action_edit, svn_wc_conflict_reason_deleted)); if (tree_conflicted) *tree_conflicted = TRUE; } svn_pool_destroy(subpool); } return SVN_NO_ERROR; } /* An svn_wc_diff_callbacks3_t function. */ static svn_error_t * merge_dir_closed(svn_wc_adm_access_t *adm_access, svn_wc_notify_state_t *contentstate, svn_wc_notify_state_t *propstate, svn_boolean_t *tree_conflicted, const char *path, void *baton) { if (contentstate) *contentstate = svn_wc_notify_state_unknown; if (propstate) *propstate = svn_wc_notify_state_unknown; if (tree_conflicted) *tree_conflicted = FALSE; /* Nothing to be done. */ return SVN_NO_ERROR; } /* The main callback table for 'svn merge'. */ static const svn_wc_diff_callbacks3_t merge_callbacks = { merge_file_changed, merge_file_added, merge_file_deleted, merge_dir_added, merge_dir_deleted, merge_props_changed, merge_dir_opened, merge_dir_closed }; /*-----------------------------------------------------------------------*/ /*** 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) && (path_is_own_ancestor || svn_path_compare_paths(child->path, path) != 0)) 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 \ || notify->action == svn_wc_notify_tree_conflict) /* 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 && child->remaining_ranges == 0)) { 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, also allocated in POOL and 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 calculate_remaining_ranges() when that function is operating on CHILD, a subtree of the merge target. Like calculate_remaining_ranges() this function should only be called when honoring mergeinfo. CHILD, PARENT, MERGEINFO_PATH, REVISION1, REVISION2, and CTX are all cascaded from filter_merged_revisions() - see that function for more information on each. In particular, note that PARENT must have been processed already by this function. More specifically, this means that PARENT->REMAINING_RANGES must already be populated -- it can be an empty rangelist but cannot be NULL. PRIMARY_URL is the younger of the url1@revision1 and url2@revision2 arguments to calculate_remaining_ranges(). RA_SESSION is the session for PRIMARY_URL. Since this function is only invoked for subtrees of the merge target, the guarantees afforded by normalize_merge_sources() don't apply - see the 'MERGEINFO MERGE SOURCE NORMALIZATION' comment at the top of this file. Therefore it is possible that PRIMARY_URL@REVISION1 and PRIMARY_URL@REVISION2 don't describe the endpoints of an unbroken line of history. The purpose of this helper is to identify these cases of broken history and adjust CHILD->REMAINING_RANGES in such a way we don't later try to describe nonexistent path/revisions to the merge report editor -- see drive_merge_report_editor(). If PRIMARY_URL@REVISION1 and PRIMARY_URL@REVISION2 describe an unbroken line of history then do nothing and leave CHILD->REMAINING_RANGES as-is. If neither PRIMARY_URL@REVISION1 nor PRIMARY_URL@REVISION2 exist then there is nothing to merge to CHILD->PATH so set CHILD->REMAINING_RANGES equal to PARENT->REMAINING_RANGES. This will cause the subtree to effectively ignore CHILD -- see 'Note: If the first svn_merge_range_t...' in drive_merge_report_editor()'s doc string. If PRIMARY_URL@REVISION1 *xor* PRIMARY_URL@REVISION2 exist then we take the subset of REVISION1:REVISION2 in CHILD->REMAINING_RANGES at which PRIMARY_URL doesn't exist and set that subset equal to PARENT->REMAINING_RANGES' intersection with that non-existent range. Why? Because this causes CHILD->REMAINING_RANGES to be identical to PARENT->REMAINING_RANGES for revisions between REVISION1 and REVISION2 at which PRIMARY_URL doesn't exist. As mentioned above this means that drive_merge_report_editor() won't attempt to describe these non-existent subtree path/ranges to the reporter (which would break the merge). If the preceeding paragraph wasn't terribly clear then what follows spells out this function's behavior a bit more explicitly: For forward merges (REVISION1 < REVISION2) If PRIMARY_URL@REVISION1 exists but PRIMARY_URL@REVISION2 doesn't, then find the revision 'N' in which PRIMARY_URL@REVISION1 was deleted. Leave the subset of CHILD->REMAINING_RANGES that intersects with REVISION1:(N - 1) as-is and set the subset of CHILD->REMAINING_RANGES that intersects with (N - 1):REVISION2 equal to PARENT->REMAINING_RANGES' intersection with (N - 1):REVISION2. If PRIMARY_URL@REVISION1 doesn't exist but PRIMARY_URL@REVISION2 does, then find the revision 'M' in which PRIMARY_URL@REVISION2 came into existence. Leave the subset of CHILD->REMAINING_RANGES that intersects with (M - 1):REVISION2 as-is and set the subset of CHILD->REMAINING_RANGES that intersects with REVISION1:(M - 1) equal to PARENT->REMAINING_RANGES' intersection with REVISION1:(M - 1). For reverse merges (REVISION1 > REVISION2) If PRIMARY_URL@REVISION1 exists but PRIMARY_URL@REVISION2 doesn't, then find the revision 'N' in which PRIMARY_URL@REVISION1 came into existence. Leave the subset of CHILD->REMAINING_RANGES that intersects with REVISION2:(N - 1) as-is and set the subset of CHILD->REMAINING_RANGES that intersects with (N - 1):REVISION1 equal to PARENT->REMAINING_RANGES' intersection with (N - 1):REVISION1. If PRIMARY_URL@REVISION1 doesn't exist but PRIMARY_URL@REVISION2 does, then find the revision 'M' in which PRIMARY_URL@REVISION2 came into existence. Leave the subset of CHILD->REMAINING_RANGES that intersects with REVISION2:(M - 1) as-is and set the subset of CHILD->REMAINING_RANGES that intersects with (M - 1):REVISION1 equal to PARENT->REMAINING_RANGES' intersection with REVISION1:(M - 1). All the allocations are made from POOL. */ static svn_error_t * adjust_deleted_subtree_ranges(svn_client__merge_path_t *child, svn_client__merge_path_t *parent, const char *mergeinfo_path, 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 younger_rev = is_rollback ? revision1 : revision2; svn_revnum_t peg_rev = younger_rev; svn_revnum_t older_rev = is_rollback ? revision2 : revision1; svn_revnum_t revision_primary_url_deleted = SVN_INVALID_REVNUM; apr_array_header_t *segments; const char *rel_source_path; const char *session_url; svn_error_t *err; apr_pool_t *subpool = svn_pool_create(pool); SVN_ERR_ASSERT(parent->remaining_ranges); /* We want to know about PRIMARY_URL@peg_rev, but we need PRIMARY_URL's path relative to RA_SESSION's URL. */ SVN_ERR(svn_ra_get_session_url(ra_session, &session_url, subpool)); SVN_ERR(svn_client__path_relative_to_root(&rel_source_path, primary_url, session_url, FALSE, ra_session, NULL, subpool)); err = svn_client__repos_location_segments(&segments, ra_session, rel_source_path, peg_rev, younger_rev, older_rev, ctx, subpool); /* If PRIMARY_URL@peg_rev doesn't exist then svn_client__repos_location_segments() typically returns an SVN_ERR_FS_NOT_FOUND error, but if it doesn't exist for a forward merge over ra_neon then we get SVN_ERR_RA_DAV_REQUEST_FAILED. http://subversion.tigris.org/issues/show_bug.cgi?id=3137 fixed some of the cases where different RA layers returned different error codes to signal the "path not found"...but it looks like there is more to do. */ if (err) { if (err->apr_err == SVN_ERR_FS_NOT_FOUND || err->apr_err == SVN_ERR_RA_DAV_REQUEST_FAILED) { /* PRIMARY_URL@peg_rev doesn't exist. Check if PRIMARY_URL@older_rev exists, if neither exist then the editor can simply ignore this subtree. */ svn_node_kind_t kind; svn_error_clear(err); err = NULL; SVN_ERR(svn_ra_check_path(ra_session, rel_source_path, older_rev, &kind, subpool)); if (kind == svn_node_none) { /* Neither PRIMARY_URL@peg_rev nor PRIMARY_URL@older_rev exist, so there is nothing to merge. Set CHILD->REMAINING_RANGES identical to PARENT's. */ child->remaining_ranges = svn_rangelist_dup(parent->remaining_ranges, subpool); } else { apr_array_header_t *exists_rangelist, *deleted_rangelist; /* PRIMARY_URL@older_rev exists, so it was deleted at some revision prior to peg_rev, find that revision. */ SVN_ERR(svn_ra_get_deleted_rev(ra_session, rel_source_path, older_rev, younger_rev, &revision_primary_url_deleted, subpool)); /* PRIMARY_URL@older_rev exists and PRIMARY_URL@peg_rev doesn't, so svn_ra_get_deleted_rev() should always find the revision PRIMARY_URL@older_rev was deleted. */ SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM( revision_primary_url_deleted)); /* If this is a reverse merge reorder CHILD->REMAINING_RANGES and PARENT->REMAINING_RANGES so both will work with the svn_rangelist_* APIs below. */ if (is_rollback) { /* svn_rangelist_reverse operates in place so it's safe to use our subpool. */ SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, subpool)); SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges, subpool)); } /* Create a rangelist describing the range PRIMARY_URL@older_rev exists and find the intersection of that and CHILD->REMAINING_RANGES. */ exists_rangelist = init_rangelist(older_rev, revision_primary_url_deleted - 1, TRUE, subpool); SVN_ERR(svn_rangelist_intersect(&(child->remaining_ranges), exists_rangelist, child->remaining_ranges, FALSE, subpool)); /* Create a second rangelist describing the range beginning when PRIMARY_URL@older_rev was deleted until younger_rev. Then find the intersection of that and PARENT->REMAINING_RANGES. Finally merge this rangelist with the rangelist above and store the result in CHILD->REMANING_RANGES. */ deleted_rangelist = init_rangelist(revision_primary_url_deleted - 1, peg_rev, TRUE, subpool); SVN_ERR(svn_rangelist_intersect(&deleted_rangelist, deleted_rangelist, parent->remaining_ranges, FALSE, subpool)); SVN_ERR(svn_rangelist_merge(&(child->remaining_ranges), deleted_rangelist, subpool)); /* Return CHILD->REMAINING_RANGES and PARENT->REMAINING_RANGES to reverse order if necessary. */ if (is_rollback) { SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, subpool)); SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges, subpool)); } } } else { return err; } } else /* PRIMARY_URL@peg_rev exists. */ { apr_array_header_t *exists_rangelist, *non_existent_rangelist; svn_location_segment_t *segment = APR_ARRAY_IDX(segments, (segments->nelts - 1), svn_location_segment_t *); /* We know PRIMARY_URL@peg_rev exists as the call to svn_client__repos_location_segments() succeeded. If there is only one segment that starts at oldest_rev then we know that PRIMARY_URL@oldest_rev:PRIMARY_URL@peg_rev describes an unbroken line of history, so there is nothing more to adjust in CHILD->REMAINING_RANGES. */ if (segment->range_start == older_rev) return SVN_NO_ERROR; /* If this is a reverse merge reorder CHILD->REMAINING_RANGES and PARENT->REMAINING_RANGES so both will work with the svn_rangelist_* APIs below. */ if (is_rollback) { SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, subpool)); SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges, subpool)); } /* Since segment doesn't span older_rev:peg_rev we know PRIMARY_URL@peg_rev didn't come into existence until segment->range_start + 1. Create a rangelist describing range where PRIMARY_URL exists and find the intersection of that range and CHILD->REMAINING_RANGELIST. */ exists_rangelist = init_rangelist(segment->range_start, peg_rev, TRUE, subpool); SVN_ERR(svn_rangelist_intersect(&(child->remaining_ranges), exists_rangelist, child->remaining_ranges, FALSE, subpool)); /* Create a second rangelist describing the range before PRIMARY_URL@peg_rev came into existence and find the intersection of that range and PARENT->REMAINING_RANGES. Then merge that rangelist with exists_rangelist and store the result in CHILD->REMANING_RANGES. */ non_existent_rangelist = init_rangelist(older_rev, segment->range_start, TRUE, subpool); SVN_ERR(svn_rangelist_intersect(&non_existent_rangelist, non_existent_rangelist, parent->remaining_ranges, FALSE, subpool)); SVN_ERR(svn_rangelist_merge(&(child->remaining_ranges), non_existent_rangelist, subpool)); /* Return CHILD->REMAINING_RANGES and PARENT->REMAINING_RANGES to reverse order if necessary. */ if (is_rollback) { SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, subpool)); SVN_ERR(svn_rangelist_reverse(parent->remaining_ranges, subpool)); } } /* Make a lasting copy of CHILD->REMAINING_RANGES using POOL. */ child->remaining_ranges = svn_rangelist_dup(child->remaining_ranges, pool); svn_pool_destroy(subpool); return SVN_NO_ERROR; } /*-----------------------------------------------------------------------*/ /*** Determining What Remains To Be Merged ***/ /* Helper for calculate_remaining_ranges(). Initialize CHILD->REMAINING_RANGES to a rangelist representing the requested merge of REVISION1:REVISION2 from MERGEINFO_PATH to CHILD->PATH. For forward merges remove any ranges from CHILD->REMAINING_RANGES that have already been merged to CHILD->PATH per TARGET_MERGEINFO or IMPLICIT_MERGEINFO. For reverse merges remove any ranges from CHILD->REMAINING_RANGES that have not alreay been merged to CHILD->PATH. CHILD represents a working copy path which is the merge target or one of target's subtrees - see 'THE CHILDREN_WITH_MERGEINFO ARRAY'. MERGEINFO_PATH is the merge source relative to the repository root. REVISION1 and REVISION2 describe the merge range requested from MERGEINFO_PATH. TARGET_MERGEINFO is the CHILD->PATHS's explicit or inherited mergeinfo. TARGET_MERGEINFO should be NULL if there is no explicit or inherited mergeinfo on CHILD->PATH or an empty hash if CHILD->PATH has empty mergeinfo. IMPLICIT_MERGEINFO is CHILD->PATH's natural history described as mergeinfo - see svn_client__get_history_as_mergeinfo(). NOTE: This should only be called when honoring mergeinfo. NOTE: Like calculate_remaining_ranges() if PARENT is present then this function must have previously been called for PARENT. */ static svn_error_t * filter_merged_revisions(svn_client__merge_path_t *child, const char *mergeinfo_path, svn_mergeinfo_t target_mergeinfo, svn_mergeinfo_t implicit_mergeinfo, svn_revnum_t revision1, svn_revnum_t revision2, apr_pool_t *pool) { apr_array_header_t *target_rangelist = NULL; svn_mergeinfo_t mergeinfo = implicit_mergeinfo; apr_pool_t *subpool = svn_pool_create(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, subpool); SVN_ERR(svn_mergeinfo_merge(mergeinfo, target_mergeinfo, subpool)); } target_rangelist = apr_hash_get(mergeinfo, mergeinfo_path, APR_HASH_KEY_STRING); if (target_rangelist) { /* Convert REVISION1 and REVISION2 to a rangelist. Note: Talking about a requested merge range's inheritability doesn't make much sense, but as we are using svn_merge_range_t to describe it we need to pick *something*. Since all the rangelist manipulations in this function either don't consider inheritance by default or we are requesting that they don't (i.e. svn_rangelist_remove and svn_rangelist_intersect) then we could set the inheritability as FALSE, it won't matter either way. */ apr_array_header_t *requested_rangelist = init_rangelist(revision1, revision2, TRUE, subpool); /* 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_rangelist, subpool)); /* We don't consider inheritance when determining intersecting ranges. If we *did* consider inheritance, then our calculation would be wrong. For example, if the CHILD->REMAINING_RANGES is 5:3 and TARGET_RANGELIST is r5* (non-inheritable) then the intersection would be r4. And that would be wrong as we clearly want to reverse merge both r4 and r5 in this case. Ignoring the ranges' inheritance results in an intersection of r4-5. You might be wondering about 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_rangelist, FALSE, pool)); SVN_ERR(svn_rangelist_reverse(child->remaining_ranges, pool)); } else { /* No part of REVISION1:REVISION2 has been merged from MERGEINFO_PATH to CHILD so just set CHILD->REMAINING_RANGES to an empty array. */ child->remaining_ranges = apr_array_make(pool, 1, sizeof(svn_merge_range_t *)); } } else /* This is a forward 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, subpool); SVN_ERR(svn_mergeinfo_merge(mergeinfo, target_mergeinfo, subpool)); } 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) { apr_array_header_t *requested_rangelist = init_rangelist(revision1, revision2, TRUE, subpool); SVN_ERR(svn_rangelist_remove(&(child->remaining_ranges), target_rangelist, requested_rangelist, FALSE, pool)); } else { child->remaining_ranges = init_rangelist(revision1, revision2, TRUE, pool); } } svn_pool_destroy(subpool); 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. URL1, REVISION1, URL2, REVISION2, TARGET_MERGEINFO, IMPLICIT_MERGEINFO, and CTX are all cascaded from the caller's arguments of the same names. RA_SESSION is the session for, and SOURCE_ROOT_URL is the repository root for, the younger of URL1@REVISION1 and URL2@REVISION2. 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: If PARENT is present then this function must have previously been called for PARENT, i.e. if populate_remaining_ranges() is calling this function for a set of svn_client__merge_path_t* the calls must be made in depth-first order. NOTE: When performing reverse merges, return SVN_ERR_CLIENT_NOT_READY_TO_MERGE if 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)); /* Initialize CHILD->REMAINING_RANGES and filter out revisions already merged (or, in the case of reverse merges, ranges not yet merged). */ SVN_ERR(filter_merged_revisions(child, mergeinfo_path, target_mergeinfo, implicit_mergeinfo, revision1, revision2, pool)); 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_ERR(adjust_deleted_subtree_ranges(child, parent, mergeinfo_path, revision1, revision2, primary_url, ra_session, 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. */ SVN_ERR_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, populate that child's remaining_ranges list. CHILDREN_WITH_MERGEINFO is expected to be sorted in depth first order and each child must be processed in that 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. Note that if REVISION1 > REVISION2, then each child's remaining_ranges member does not adhere to the API rules for rangelists described in svn_mergeinfo.h -- See svn_client__merge_path_t. See `MERGEINFO MERGE SOURCE NORMALIZATION' for more requirements around 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, in the merge target's history, there was a copy from a older source which didn't exist at the time of the copy, then URL2 won't exist at some range M:N, where REVISION1 < M < N < REVISION2. If there were multiple instances of such copies then there will be multiple 'gaps' like this. The rules of 'MERGEINFO MERGE SOURCE NORMALIZATION' allow this, but we must ignore these gaps when calculating what ranges remain to be merged from URL1@REVISION1:URL2@REVISION2. In other words, we can't try to merge any part of URL2@M:URL2@N since no part of that actually exists and would break the editor. We can "ignore" these gaps by adjusting the target's implicit mergeinfo to make it look like they are already part of the target's natural history. */ if (i == 0) /* CHILDREN_WITH_MERGEINFO[0] is always the target. */ { apr_hash_index_t *hi; svn_revnum_t youngest_rev, oldest_rev; SVN_ERR(svn_mergeinfo__get_range_endpoints( &youngest_rev, &oldest_rev, child->implicit_mergeinfo, iterpool)); /* Adjust the target's implicit mergeinfo so there are no gaps */ for (hi = apr_hash_first(iterpool, child->implicit_mergeinfo); hi; hi = apr_hash_next(hi)) { const void *key; void *value; const char *source_path; apr_array_header_t *source_rangelist, *rev1_rev2_rangelist; apr_hash_this(hi, &key, NULL, &value); source_path = key; source_rangelist = value; youngest_rev = (APR_ARRAY_IDX(source_rangelist, source_rangelist->nelts - 1, svn_merge_range_t *))->end; rev1_rev2_rangelist = init_rangelist(oldest_rev, youngest_rev, TRUE, iterpool); svn_rangelist_merge(&source_rangelist, rev1_rev2_rangelist, pool); apr_hash_set(child->implicit_mergeinfo, source_path, APR_HASH_KEY_STRING, source_rangelist); } } /* 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 CHILD is a subtree then its parent must be in CHILDREN_WITH_MERGEINFO, see the global comment 'THE CHILDREN_WITH_MERGEINFO ARRAY'. */ SVN_ERR_ASSERT(parent); } 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)); } 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 handl