/* * mergeinfo.c : merge history functions for the libsvn_client library * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== */ #include #include #include "svn_pools.h" #include "svn_dirent_uri.h" #include "svn_path.h" #include "svn_string.h" #include "svn_opt.h" #include "svn_error.h" #include "svn_error_codes.h" #include "svn_props.h" #include "svn_mergeinfo.h" #include "svn_sorts.h" #include "svn_ra.h" #include "svn_client.h" #include "svn_hash.h" #include "private/svn_opt_private.h" #include "private/svn_mergeinfo_private.h" #include "private/svn_wc_private.h" #include "private/svn_ra_private.h" #include "private/svn_fspath.h" #include "private/svn_client_private.h" #include "client.h" #include "mergeinfo.h" #include "svn_private_config.h" svn_client__merge_path_t * svn_client__merge_path_dup(const svn_client__merge_path_t *old, apr_pool_t *pool) { svn_client__merge_path_t *new = apr_pmemdup(pool, old, sizeof(*old)); new->abspath = apr_pstrdup(pool, old->abspath); if (new->remaining_ranges) new->remaining_ranges = svn_rangelist_dup(old->remaining_ranges, pool); if (new->pre_merge_mergeinfo) new->pre_merge_mergeinfo = svn_mergeinfo_dup(old->pre_merge_mergeinfo, pool); if (new->implicit_mergeinfo) new->implicit_mergeinfo = svn_mergeinfo_dup(old->implicit_mergeinfo, pool); return new; } svn_client__merge_path_t * svn_client__merge_path_create(const char *abspath, apr_pool_t *pool) { svn_client__merge_path_t *result = apr_pcalloc(pool, sizeof(*result)); result->abspath = apr_pstrdup(pool, abspath); return result; } svn_error_t * svn_client__parse_mergeinfo(svn_mergeinfo_t *mergeinfo, svn_wc_context_t *wc_ctx, const char *local_abspath, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { const svn_string_t *propval; *mergeinfo = NULL; /* ### Use svn_wc_prop_get() would actually be sufficient for now. ### DannyB thinks that later we'll need behavior more like ### svn_client__get_prop_from_wc(). */ SVN_ERR(svn_wc_prop_get2(&propval, wc_ctx, local_abspath, SVN_PROP_MERGEINFO, scratch_pool, scratch_pool)); if (propval) SVN_ERR(svn_mergeinfo_parse(mergeinfo, propval->data, result_pool)); return SVN_NO_ERROR; } svn_error_t * svn_client__record_wc_mergeinfo(const char *local_abspath, svn_mergeinfo_t mergeinfo, svn_boolean_t do_notification, svn_client_ctx_t *ctx, apr_pool_t *scratch_pool) { svn_string_t *mergeinfo_str = NULL; svn_boolean_t mergeinfo_changes = FALSE; SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); /* Convert MERGEINFO (if any) into text for storage as a property value. */ if (mergeinfo) SVN_ERR(svn_mergeinfo_to_string(&mergeinfo_str, mergeinfo, scratch_pool)); if (do_notification && ctx->notify_func2) SVN_ERR(svn_client__mergeinfo_status(&mergeinfo_changes, ctx->wc_ctx, local_abspath, scratch_pool)); /* Record the new mergeinfo in the WC. */ /* ### Later, we'll want behavior more analogous to ### svn_client__get_prop_from_wc(). */ SVN_ERR(svn_wc_prop_set4(ctx->wc_ctx, local_abspath, SVN_PROP_MERGEINFO, mergeinfo_str, svn_depth_empty, TRUE /* skip checks */, NULL, NULL, NULL /* cancellation */, NULL, NULL /* notification */, scratch_pool)); if (do_notification && ctx->notify_func2) { svn_wc_notify_t *notify = svn_wc_create_notify(local_abspath, svn_wc_notify_merge_record_info, scratch_pool); if (mergeinfo_changes) notify->prop_state = svn_wc_notify_state_merged; else notify->prop_state = svn_wc_notify_state_changed; ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); } return SVN_NO_ERROR; } svn_error_t * svn_client__record_wc_mergeinfo_catalog(apr_hash_t *result_catalog, svn_client_ctx_t *ctx, apr_pool_t *scratch_pool) { apr_pool_t *iterpool = svn_pool_create(scratch_pool); if (apr_hash_count(result_catalog)) { int i; apr_array_header_t *sorted_cat = svn_sort__hash(result_catalog, svn_sort_compare_items_as_paths, scratch_pool); /* Write the mergeinfo out in sorted order of the paths (presumably just * so that the notifications are in a predictable, convenient order). */ for (i = 0; i < sorted_cat->nelts; i++) { svn_sort__item_t elt = APR_ARRAY_IDX(sorted_cat, i, svn_sort__item_t); svn_error_t *err; svn_pool_clear(iterpool); err = svn_client__record_wc_mergeinfo(elt.key, elt.value, TRUE, ctx, iterpool); if (err && err->apr_err == SVN_ERR_ENTRY_NOT_FOUND) { /* PATH isn't just missing, it's not even versioned as far as this working copy knows. But it was included in MERGES, which means that the server knows about it. Likely we don't have access to the source due to authz restrictions. For now just clear the error and continue... */ svn_error_clear(err); } else { SVN_ERR(err); } } } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } /*-----------------------------------------------------------------------*/ /*** Retrieving mergeinfo. ***/ svn_error_t * svn_client__get_wc_mergeinfo(svn_mergeinfo_t *mergeinfo, svn_boolean_t *inherited_p, svn_mergeinfo_inheritance_t inherit, const char *local_abspath, const char *limit_abspath, const char **walked_path, svn_boolean_t ignore_invalid_mergeinfo, svn_client_ctx_t *ctx, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { const char *walk_relpath = ""; svn_mergeinfo_t wc_mergeinfo; svn_revnum_t base_revision; apr_pool_t *iterpool; svn_boolean_t inherited; SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); if (limit_abspath) SVN_ERR_ASSERT(svn_dirent_is_absolute(limit_abspath)); SVN_ERR(svn_wc__node_get_base(NULL, &base_revision, NULL, NULL, NULL, NULL, ctx->wc_ctx, local_abspath, TRUE /* ignore_enoent */, FALSE /* show_hidden */, scratch_pool, scratch_pool)); iterpool = svn_pool_create(scratch_pool); while (TRUE) { svn_pool_clear(iterpool); /* Don't look for explicit mergeinfo on LOCAL_ABSPATH if we are only interested in inherited mergeinfo. */ if (inherit == svn_mergeinfo_nearest_ancestor) { wc_mergeinfo = NULL; inherit = svn_mergeinfo_inherited; } else { /* Look for mergeinfo on LOCAL_ABSPATH. If there isn't any and we want inherited mergeinfo, walk towards the root of the WC until we encounter either (a) an unversioned directory, or (b) mergeinfo. If we encounter (b), use that inherited mergeinfo as our baseline. */ svn_error_t *err = svn_client__parse_mergeinfo(&wc_mergeinfo, ctx->wc_ctx, local_abspath, result_pool, iterpool); if ((ignore_invalid_mergeinfo || walk_relpath [0] != '\0') && err && err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR) { svn_error_clear(err); wc_mergeinfo = apr_hash_make(result_pool); break; } else { SVN_ERR(err); } } if (wc_mergeinfo == NULL && inherit != svn_mergeinfo_explicit && !svn_dirent_is_root(local_abspath, strlen(local_abspath))) { svn_boolean_t is_wc_root; svn_boolean_t is_switched; svn_revnum_t parent_base_rev; svn_revnum_t parent_changed_rev; /* Don't look any higher than the limit path. */ if (limit_abspath && strcmp(limit_abspath, local_abspath) == 0) break; /* If we've reached the root of the working copy don't look any higher. */ SVN_ERR(svn_wc_check_root(&is_wc_root, &is_switched, NULL, ctx->wc_ctx, local_abspath, iterpool)); if (is_wc_root || is_switched) break; /* No explicit mergeinfo on this path. Look higher up the directory tree while keeping track of what we've walked. */ walk_relpath = svn_relpath_join(svn_dirent_basename(local_abspath, iterpool), walk_relpath, result_pool); local_abspath = svn_dirent_dirname(local_abspath, scratch_pool); SVN_ERR(svn_wc__node_get_base(NULL, &parent_base_rev, NULL, NULL, NULL, NULL, ctx->wc_ctx, local_abspath, TRUE, FALSE, scratch_pool, scratch_pool)); /* ### This checks the WORKING changed_rev, so invalid on replacement ### not even reliable in case an ancestor was copied from a ### different location */ SVN_ERR(svn_wc__node_get_changed_info(&parent_changed_rev, NULL, NULL, ctx->wc_ctx, local_abspath, scratch_pool, scratch_pool)); /* Look in LOCAL_ABSPATH's parent for inherited mergeinfo if LOCAL_ABSPATH has no base revision because it is an uncommitted addition, or if its base revision falls within the inclusive range of its parent's last changed revision to the parent's base revision; otherwise stop looking for inherited mergeinfo. */ if (SVN_IS_VALID_REVNUM(base_revision) && (base_revision < parent_changed_rev || parent_base_rev < base_revision)) break; /* We haven't yet risen above the root of the WC. */ continue; } break; } svn_pool_destroy(iterpool); if (svn_path_is_empty(walk_relpath)) { /* Mergeinfo is explicit. */ inherited = FALSE; *mergeinfo = wc_mergeinfo; } else { /* Mergeinfo may be inherited. */ if (wc_mergeinfo) { inherited = TRUE; SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(mergeinfo, wc_mergeinfo, walk_relpath, result_pool, scratch_pool)); } else { inherited = FALSE; *mergeinfo = NULL; } } if (walked_path) *walked_path = walk_relpath; /* Remove non-inheritable mergeinfo and paths mapped to empty ranges which may occur if WCPATH's mergeinfo is not explicit. */ if (inherited && apr_hash_count(*mergeinfo)) /* Nothing to do for empty mergeinfo. */ { SVN_ERR(svn_mergeinfo_inheritable2(mergeinfo, *mergeinfo, NULL, SVN_INVALID_REVNUM, SVN_INVALID_REVNUM, TRUE, result_pool, scratch_pool)); svn_mergeinfo__remove_empty_rangelists(*mergeinfo, result_pool); } if (inherited_p) *inherited_p = inherited; return SVN_NO_ERROR; } svn_error_t * svn_client__get_wc_mergeinfo_catalog(svn_mergeinfo_catalog_t *mergeinfo_cat, svn_boolean_t *inherited, svn_boolean_t include_descendants, svn_mergeinfo_inheritance_t inherit, const char *local_abspath, const char *limit_path, const char **walked_path, svn_boolean_t ignore_invalid_mergeinfo, svn_client_ctx_t *ctx, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { const char *target_repos_relpath; svn_mergeinfo_t mergeinfo; const char *repos_root; SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); *mergeinfo_cat = NULL; SVN_ERR(svn_wc__node_get_repos_info(NULL, &target_repos_relpath, &repos_root, NULL, ctx->wc_ctx, local_abspath, scratch_pool, scratch_pool)); /* Get the mergeinfo for the LOCAL_ABSPATH target and set *INHERITED and *WALKED_PATH. */ SVN_ERR(svn_client__get_wc_mergeinfo(&mergeinfo, inherited, inherit, local_abspath, limit_path, walked_path, ignore_invalid_mergeinfo, ctx, result_pool, scratch_pool)); /* Add any explicit/inherited mergeinfo for LOCAL_ABSPATH to *MERGEINFO_CAT. */ if (mergeinfo) { *mergeinfo_cat = apr_hash_make(result_pool); svn_hash_sets(*mergeinfo_cat, apr_pstrdup(result_pool, target_repos_relpath), mergeinfo); } /* If LOCAL_ABSPATH is a directory and we want the subtree mergeinfo too, then get it. With WC-NG it is cheaper to do a single db transaction, than first looking if we really have a directory. */ if (include_descendants) { apr_hash_t *mergeinfo_props; apr_hash_index_t *hi; SVN_ERR(svn_wc__prop_retrieve_recursive(&mergeinfo_props, ctx->wc_ctx, local_abspath, SVN_PROP_MERGEINFO, scratch_pool, scratch_pool)); /* Convert *mergeinfo_props into a proper svn_mergeinfo_catalog_t */ for (hi = apr_hash_first(scratch_pool, mergeinfo_props); hi; hi = apr_hash_next(hi)) { const char *node_abspath = svn__apr_hash_index_key(hi); svn_string_t *propval = svn__apr_hash_index_val(hi); svn_mergeinfo_t subtree_mergeinfo; const char *repos_relpath; if (strcmp(node_abspath, local_abspath) == 0) continue; /* Already parsed in svn_client__get_wc_mergeinfo */ SVN_ERR(svn_wc__node_get_repos_info(NULL, &repos_relpath, NULL, NULL, ctx->wc_ctx, node_abspath, result_pool, scratch_pool)); SVN_ERR(svn_mergeinfo_parse(&subtree_mergeinfo, propval->data, result_pool)); /* If the target had no explicit/inherited mergeinfo and this is the first subtree with mergeinfo found, then the catalog will still be NULL. */ if (*mergeinfo_cat == NULL) *mergeinfo_cat = apr_hash_make(result_pool); svn_hash_sets(*mergeinfo_cat, repos_relpath, subtree_mergeinfo); } } return SVN_NO_ERROR; } svn_error_t * svn_client__get_repos_mergeinfo(svn_mergeinfo_t *target_mergeinfo, svn_ra_session_t *ra_session, const char *url, svn_revnum_t rev, svn_mergeinfo_inheritance_t inherit, svn_boolean_t squelch_incapable, apr_pool_t *pool) { svn_mergeinfo_catalog_t tgt_mergeinfo_cat; *target_mergeinfo = NULL; SVN_ERR(svn_client__get_repos_mergeinfo_catalog(&tgt_mergeinfo_cat, ra_session, url, rev, inherit, squelch_incapable, FALSE, pool, pool)); if (tgt_mergeinfo_cat && apr_hash_count(tgt_mergeinfo_cat)) { /* We asked only for the REL_PATH's mergeinfo, not any of its descendants. So if there is anything in the catalog it is the mergeinfo for REL_PATH. */ *target_mergeinfo = svn__apr_hash_index_val(apr_hash_first(pool, tgt_mergeinfo_cat)); } return SVN_NO_ERROR; } svn_error_t * svn_client__get_repos_mergeinfo_catalog(svn_mergeinfo_catalog_t *mergeinfo_cat, svn_ra_session_t *ra_session, const char *url, svn_revnum_t rev, svn_mergeinfo_inheritance_t inherit, svn_boolean_t squelch_incapable, svn_boolean_t include_descendants, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { svn_error_t *err; svn_mergeinfo_catalog_t repos_mergeinfo_cat; apr_array_header_t *rel_paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); const char *old_session_url; APR_ARRAY_PUSH(rel_paths, const char *) = ""; /* Fetch the mergeinfo. */ SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session, url, scratch_pool)); err = svn_ra_get_mergeinfo(ra_session, &repos_mergeinfo_cat, rel_paths, rev, inherit, include_descendants, result_pool); err = svn_error_compose_create( err, svn_ra_reparent(ra_session, old_session_url, scratch_pool)); if (err) { if (squelch_incapable && err->apr_err == SVN_ERR_UNSUPPORTED_FEATURE) { svn_error_clear(err); *mergeinfo_cat = NULL; return SVN_NO_ERROR; } else return svn_error_trace(err); } if (repos_mergeinfo_cat == NULL) { *mergeinfo_cat = NULL; } else { const char *session_relpath; SVN_ERR(svn_ra_get_path_relative_to_root(ra_session, &session_relpath, url, scratch_pool)); if (session_relpath[0] == '\0') *mergeinfo_cat = repos_mergeinfo_cat; else SVN_ERR(svn_mergeinfo__add_prefix_to_catalog(mergeinfo_cat, repos_mergeinfo_cat, session_relpath, result_pool, scratch_pool)); } return SVN_NO_ERROR; } svn_error_t * svn_client__get_wc_or_repos_mergeinfo(svn_mergeinfo_t *target_mergeinfo, svn_boolean_t *inherited, svn_boolean_t *from_repos, svn_boolean_t repos_only, svn_mergeinfo_inheritance_t inherit, svn_ra_session_t *ra_session, const char *target_wcpath, svn_client_ctx_t *ctx, apr_pool_t *pool) { svn_mergeinfo_catalog_t tgt_mergeinfo_cat; *target_mergeinfo = NULL; SVN_ERR(svn_client__get_wc_or_repos_mergeinfo_catalog(&tgt_mergeinfo_cat, inherited, from_repos, FALSE, repos_only, FALSE, inherit, ra_session, target_wcpath, ctx, pool, pool)); if (tgt_mergeinfo_cat && apr_hash_count(tgt_mergeinfo_cat)) { /* We asked only for the TARGET_WCPATH's mergeinfo, not any of its descendants. It this mergeinfo is in the catalog, it's keyed on TARGET_WCPATH's root-relative path. We could dig that up so we can peek into our catalog, but it ought to be the only thing in the catalog, so we'll just fetch the first hash item. */ *target_mergeinfo = svn__apr_hash_index_val(apr_hash_first(pool, tgt_mergeinfo_cat)); } return SVN_NO_ERROR; } svn_error_t * svn_client__get_wc_or_repos_mergeinfo_catalog( svn_mergeinfo_catalog_t *target_mergeinfo_catalog, svn_boolean_t *inherited_p, svn_boolean_t *from_repos, svn_boolean_t include_descendants, svn_boolean_t repos_only, svn_boolean_t ignore_invalid_mergeinfo, svn_mergeinfo_inheritance_t inherit, svn_ra_session_t *ra_session, const char *target_wcpath, svn_client_ctx_t *ctx, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { const char *url; svn_revnum_t target_rev; const char *local_abspath; const char *repos_root; const char *repos_relpath; svn_mergeinfo_catalog_t target_mergeinfo_cat_wc = NULL; svn_mergeinfo_catalog_t target_mergeinfo_cat_repos = NULL; SVN_ERR(svn_dirent_get_absolute(&local_abspath, target_wcpath, scratch_pool)); if (from_repos) *from_repos = FALSE; /* We may get an entry with abbreviated information from TARGET_WCPATH's parent if TARGET_WCPATH is missing. These limited entries do not have a URL and without that we cannot get accurate mergeinfo for TARGET_WCPATH. */ SVN_ERR(svn_wc__node_get_origin(NULL, &target_rev, &repos_relpath, &repos_root, NULL, NULL, ctx->wc_ctx, local_abspath, FALSE, scratch_pool, scratch_pool)); if (repos_relpath) url = svn_path_url_add_component2(repos_root, repos_relpath, scratch_pool); else url = NULL; if (!repos_only) { svn_boolean_t inherited; SVN_ERR(svn_client__get_wc_mergeinfo_catalog(&target_mergeinfo_cat_wc, &inherited, include_descendants, inherit, local_abspath, NULL, NULL, ignore_invalid_mergeinfo, ctx, result_pool, scratch_pool)); if (inherited_p) *inherited_p = inherited; /* If we want LOCAL_ABSPATH's inherited mergeinfo, were we able to get it from the working copy? If not, then we must ask the repository. */ if (! (inherited || (inherit == svn_mergeinfo_explicit) || (repos_relpath && target_mergeinfo_cat_wc && svn_hash_gets(target_mergeinfo_cat_wc, repos_relpath)))) { repos_only = TRUE; /* We already have any subtree mergeinfo from the working copy, no need to ask the server for it again. */ include_descendants = FALSE; } } if (repos_only) { /* No need to check the repos if this is a local addition. */ if (url != NULL) { apr_hash_t *original_props; /* Check to see if we have local modifications which removed all of TARGET_WCPATH's pristine mergeinfo. If that is the case then TARGET_WCPATH effectively has no mergeinfo. */ SVN_ERR(svn_wc_get_pristine_props(&original_props, ctx->wc_ctx, local_abspath, result_pool, scratch_pool)); if (!svn_hash_gets(original_props, SVN_PROP_MERGEINFO)) { apr_pool_t *sesspool = NULL; if (! ra_session) { sesspool = svn_pool_create(scratch_pool); SVN_ERR(svn_client_open_ra_session2(&ra_session, url, NULL, ctx, sesspool, sesspool)); } SVN_ERR(svn_client__get_repos_mergeinfo_catalog( &target_mergeinfo_cat_repos, ra_session, url, target_rev, inherit, TRUE, include_descendants, result_pool, scratch_pool)); if (target_mergeinfo_cat_repos && svn_hash_gets(target_mergeinfo_cat_repos, repos_relpath)) { if (inherited_p) *inherited_p = TRUE; if (from_repos) *from_repos = TRUE; } /* If we created an RA_SESSION above, destroy it. Otherwise, if reparented an existing session, point it back where it was when we were called. */ if (sesspool) { svn_pool_destroy(sesspool); } } } } /* Combine the mergeinfo from the working copy and repository as needed. */ if (target_mergeinfo_cat_wc) { *target_mergeinfo_catalog = target_mergeinfo_cat_wc; if (target_mergeinfo_cat_repos) SVN_ERR(svn_mergeinfo_catalog_merge(*target_mergeinfo_catalog, target_mergeinfo_cat_repos, result_pool, scratch_pool)); } else if (target_mergeinfo_cat_repos) { *target_mergeinfo_catalog = target_mergeinfo_cat_repos; } else { *target_mergeinfo_catalog = NULL; } return SVN_NO_ERROR; } svn_error_t * svn_client__get_history_as_mergeinfo(svn_mergeinfo_t *mergeinfo_p, svn_boolean_t *has_rev_zero_history, const svn_client__pathrev_t *pathrev, svn_revnum_t range_youngest, svn_revnum_t range_oldest, svn_ra_session_t *ra_session, svn_client_ctx_t *ctx, apr_pool_t *pool) { apr_array_header_t *segments; /* Fetch the location segments for our URL@PEG_REVNUM. */ if (! SVN_IS_VALID_REVNUM(range_youngest)) range_youngest = pathrev->rev; if (! SVN_IS_VALID_REVNUM(range_oldest)) range_oldest = 0; SVN_ERR(svn_client__repos_location_segments(&segments, ra_session, pathrev->url, pathrev->rev, range_youngest, range_oldest, ctx, pool)); if (has_rev_zero_history) { *has_rev_zero_history = FALSE; if (segments->nelts) { svn_location_segment_t *oldest_segment = APR_ARRAY_IDX(segments, 0, svn_location_segment_t *); if (oldest_segment->range_start == 0) *has_rev_zero_history = TRUE; } } SVN_ERR(svn_mergeinfo__mergeinfo_from_segments(mergeinfo_p, segments, pool)); return SVN_NO_ERROR; } /*-----------------------------------------------------------------------*/ /*** Eliding mergeinfo. ***/ /* Given the mergeinfo (CHILD_MERGEINFO) for a path, and the mergeinfo of its nearest ancestor with mergeinfo (PARENT_MERGEINFO), compare CHILD_MERGEINFO to PARENT_MERGEINFO to see if the former elides to the latter, following the elision rules described in svn_client__elide_mergeinfo()'s docstring. Set *ELIDES to whether or not CHILD_MERGEINFO is redundant. Note: This function assumes that PARENT_MERGEINFO is definitive; i.e. if it is NULL then the caller not only walked the entire WC looking for inherited mergeinfo, but queried the repository if none was found in the WC. This is rather important since this function says empty mergeinfo should be elided if PARENT_MERGEINFO is NULL, and we don't want to do that unless we are *certain* that the empty mergeinfo on PATH isn't overriding anything. If PATH_SUFFIX and PARENT_MERGEINFO are not NULL append PATH_SUFFIX to each path in PARENT_MERGEINFO before performing the comparison. */ static svn_error_t * should_elide_mergeinfo(svn_boolean_t *elides, svn_mergeinfo_t parent_mergeinfo, svn_mergeinfo_t child_mergeinfo, const char *path_suffix, apr_pool_t *scratch_pool) { /* Easy out: No child mergeinfo to elide. */ if (child_mergeinfo == NULL) { *elides = FALSE; } else if (apr_hash_count(child_mergeinfo) == 0) { /* Empty mergeinfo elides to empty mergeinfo or to "nothing", i.e. it isn't overriding any parent. Otherwise it doesn't elide. */ *elides = (!parent_mergeinfo || apr_hash_count(parent_mergeinfo) == 0); } else if (!parent_mergeinfo || apr_hash_count(parent_mergeinfo) == 0) { /* Non-empty mergeinfo never elides to empty mergeinfo or no mergeinfo. */ *elides = FALSE; } else { /* Both CHILD_MERGEINFO and PARENT_MERGEINFO are non-NULL and non-empty. */ svn_mergeinfo_t path_tweaked_parent_mergeinfo; /* If we need to adjust the paths in PARENT_MERGEINFO do it now. */ if (path_suffix) SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo( &path_tweaked_parent_mergeinfo, parent_mergeinfo, path_suffix, scratch_pool, scratch_pool)); else path_tweaked_parent_mergeinfo = parent_mergeinfo; SVN_ERR(svn_mergeinfo__equals(elides, path_tweaked_parent_mergeinfo, child_mergeinfo, TRUE, scratch_pool)); } return SVN_NO_ERROR; } /* Helper for svn_client__elide_mergeinfo(). Given a working copy LOCAL_ABSPATH, its mergeinfo hash CHILD_MERGEINFO, and the mergeinfo of LOCAL_ABSPATH's nearest ancestor PARENT_MERGEINFO, use should_elide_mergeinfo() to decide whether or not CHILD_MERGEINFO elides to PARENT_MERGEINFO; PATH_SUFFIX means the same as in that function. If elision does occur, then remove the mergeinfo for LOCAL_ABSPATH. If CHILD_MERGEINFO is NULL, do nothing. Use SCRATCH_POOL for temporary allocations. */ static svn_error_t * elide_mergeinfo(svn_mergeinfo_t parent_mergeinfo, svn_mergeinfo_t child_mergeinfo, const char *local_abspath, svn_client_ctx_t *ctx, apr_pool_t *scratch_pool) { svn_boolean_t elides; SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); SVN_ERR(should_elide_mergeinfo(&elides, parent_mergeinfo, child_mergeinfo, NULL, scratch_pool)); if (elides) { SVN_ERR(svn_wc_prop_set4(ctx->wc_ctx, local_abspath, SVN_PROP_MERGEINFO, NULL, svn_depth_empty, TRUE, NULL, NULL, NULL /* cancellation */, NULL, NULL /* notification */, scratch_pool)); if (ctx->notify_func2) { svn_wc_notify_t *notify; notify = svn_wc_create_notify(local_abspath, svn_wc_notify_merge_elide_info, scratch_pool); ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); notify = svn_wc_create_notify(local_abspath, svn_wc_notify_update_update, scratch_pool); notify->prop_state = svn_wc_notify_state_changed; ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); } } return SVN_NO_ERROR; } svn_error_t * svn_client__elide_mergeinfo(const char *target_abspath, const char *wc_elision_limit_abspath, svn_client_ctx_t *ctx, apr_pool_t *pool) { const char *limit_abspath = wc_elision_limit_abspath; SVN_ERR_ASSERT(svn_dirent_is_absolute(target_abspath)); SVN_ERR_ASSERT(!wc_elision_limit_abspath || svn_dirent_is_absolute(wc_elision_limit_abspath)); /* Check for first easy out: We are already at the limit path. */ if (!limit_abspath || strcmp(target_abspath, limit_abspath) != 0) { svn_mergeinfo_t target_mergeinfo; svn_mergeinfo_t mergeinfo = NULL; svn_boolean_t inherited; const char *walk_path; svn_error_t *err; /* Get the TARGET_WCPATH's explicit mergeinfo. */ err = svn_client__get_wc_mergeinfo(&target_mergeinfo, &inherited, svn_mergeinfo_inherited, target_abspath, limit_abspath, &walk_path, FALSE, ctx, pool, pool); if (err) { if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR) { /* Issue #3896: If we attempt elision because invalid mergeinfo is present on TARGET_WCPATH, then don't let the merge fail, just skip the elision attempt. */ svn_error_clear(err); return SVN_NO_ERROR; } else { return svn_error_trace(err); } } /* If TARGET_WCPATH has no explicit mergeinfo, there's nothing to elide, we're done. */ if (inherited || target_mergeinfo == NULL) return SVN_NO_ERROR; /* Get TARGET_WCPATH's inherited mergeinfo from the WC. */ err = svn_client__get_wc_mergeinfo(&mergeinfo, NULL, svn_mergeinfo_nearest_ancestor, target_abspath, limit_abspath, &walk_path, FALSE, ctx, pool, pool); if (err) { if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR) { /* Issue #3896 again, but invalid mergeinfo is inherited. */ svn_error_clear(err); return SVN_NO_ERROR; } else { return svn_error_trace(err); } } /* If TARGET_WCPATH inherited no mergeinfo from the WC and we are not limiting our search to the working copy then check if it inherits any from the repos. */ if (!mergeinfo && !wc_elision_limit_abspath) { err = svn_client__get_wc_or_repos_mergeinfo( &mergeinfo, NULL, NULL, TRUE, svn_mergeinfo_nearest_ancestor, NULL, target_abspath, ctx, pool); if (err) { if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR) { /* Issue #3896 again, but invalid mergeinfo is inherited from the repository. */ svn_error_clear(err); return SVN_NO_ERROR; } else { return svn_error_trace(err); } } } /* If there is nowhere to elide TARGET_WCPATH's mergeinfo to and the elision is limited, then we are done.*/ if (!mergeinfo && wc_elision_limit_abspath) return SVN_NO_ERROR; SVN_ERR(elide_mergeinfo(mergeinfo, target_mergeinfo, target_abspath, ctx, pool)); } return SVN_NO_ERROR; } /* Set *MERGEINFO_CATALOG to the explicit or inherited mergeinfo for PATH_OR_URL@PEG_REVISION. If INCLUDE_DESCENDANTS is true, also store in *MERGEINFO_CATALOG the explicit mergeinfo on any subtrees under PATH_OR_URL. Key all mergeinfo in *MERGEINFO_CATALOG on repository relpaths. If no mergeinfo is found then set *MERGEINFO_CATALOG to NULL. Set *REPOS_ROOT to the root URL of the repository associated with PATH_OR_URL. If RA_SESSION is NOT NULL and PATH_OR_URL refers to a URL, RA_SESSION (which must be of the repository containing PATH_OR_URL) will be used instead of a temporary RA session. Caller is responsible for reparenting the session if it wants to use it after the call. Allocate *MERGEINFO_CATALOG and all its contents in RESULT_POOL. Use SCRATCH_POOL for all temporary allocations. Return SVN_ERR_UNSUPPORTED_FEATURE if the server does not support Merge Tracking. */ static svn_error_t * get_mergeinfo(svn_mergeinfo_catalog_t *mergeinfo_catalog, const char **repos_root, const char *path_or_url, const svn_opt_revision_t *peg_revision, svn_boolean_t include_descendants, svn_boolean_t ignore_invalid_mergeinfo, svn_client_ctx_t *ctx, svn_ra_session_t *ra_session, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { const char *local_abspath; svn_boolean_t use_url = svn_path_is_url(path_or_url); svn_client__pathrev_t *peg_loc; if (ra_session && svn_path_is_url(path_or_url)) { SVN_ERR(svn_ra_reparent(ra_session, path_or_url, scratch_pool)); SVN_ERR(svn_client__resolve_rev_and_url(&peg_loc, ra_session, path_or_url, peg_revision, peg_revision, ctx, scratch_pool)); } else { SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &peg_loc, path_or_url, NULL, peg_revision, peg_revision, ctx, scratch_pool)); } /* If PATH_OR_URL is as working copy path determine if we will need to contact the repository for the requested PEG_REVISION. */ if (!use_url) { svn_client__pathrev_t *origin; SVN_ERR(svn_dirent_get_absolute(&local_abspath, path_or_url, scratch_pool)); SVN_ERR(svn_client__wc_node_get_origin(&origin, local_abspath, ctx, scratch_pool, scratch_pool)); if (!origin || strcmp(origin->url, peg_loc->url) != 0 || peg_loc->rev != origin->rev) { use_url = TRUE; /* Don't rely on local mergeinfo */ } } SVN_ERR(svn_ra_get_repos_root2(ra_session, repos_root, result_pool)); if (use_url) { SVN_ERR(svn_client__get_repos_mergeinfo_catalog( mergeinfo_catalog, ra_session, peg_loc->url, peg_loc->rev, svn_mergeinfo_inherited, FALSE, include_descendants, result_pool, scratch_pool)); } else /* ! svn_path_is_url() */ { SVN_ERR(svn_client__get_wc_or_repos_mergeinfo_catalog( mergeinfo_catalog, NULL, NULL, include_descendants, FALSE, ignore_invalid_mergeinfo, svn_mergeinfo_inherited, ra_session, path_or_url, ctx, result_pool, scratch_pool)); } return SVN_NO_ERROR; } /*** In-memory mergeinfo elision ***/ svn_error_t * svn_client__elide_mergeinfo_catalog(svn_mergeinfo_catalog_t mergeinfo_catalog, apr_pool_t *scratch_pool) { apr_array_header_t *sorted_hash; apr_array_header_t *elidable_paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); apr_array_header_t *dir_stack = apr_array_make(scratch_pool, 1, sizeof(const char *)); apr_pool_t *iterpool; int i; /* Here's the general algorithm: Walk through the paths sorted in tree order. For each path, pop the dir_stack until it is either empty or the top item contains a parent of the current path. Check to see if that mergeinfo is then elidable, and build the list of elidable mergeinfo based upon that determination. Finally, push the path of interest onto the stack, and continue. */ sorted_hash = svn_sort__hash(mergeinfo_catalog, svn_sort_compare_items_as_paths, scratch_pool); iterpool = svn_pool_create(scratch_pool); for (i = 0; i < sorted_hash->nelts; i++) { svn_sort__item_t *item = &APR_ARRAY_IDX(sorted_hash, i, svn_sort__item_t); const char *path = item->key; if (dir_stack->nelts > 0) { const char *top; const char *path_suffix; svn_boolean_t elides = FALSE; svn_pool_clear(iterpool); /* Pop off any paths which are not ancestors of PATH. */ do { top = APR_ARRAY_IDX(dir_stack, dir_stack->nelts - 1, const char *); path_suffix = svn_dirent_is_child(top, path, NULL); if (!path_suffix) apr_array_pop(dir_stack); } while (dir_stack->nelts > 0 && !path_suffix); /* If we have a path suffix, it means we haven't popped the stack clean. */ if (path_suffix) { SVN_ERR(should_elide_mergeinfo(&elides, svn_hash_gets(mergeinfo_catalog, top), svn_hash_gets(mergeinfo_catalog, path), path_suffix, iterpool)); if (elides) APR_ARRAY_PUSH(elidable_paths, const char *) = path; } } APR_ARRAY_PUSH(dir_stack, const char *) = path; } svn_pool_destroy(iterpool); /* Now remove the elidable paths from the catalog. */ for (i = 0; i < elidable_paths->nelts; i++) { const char *path = APR_ARRAY_IDX(elidable_paths, i, const char *); svn_hash_sets(mergeinfo_catalog, path, NULL); } return SVN_NO_ERROR; } /* Helper for filter_log_entry_with_rangelist(). DEPTH_FIRST_CATALOG_INDEX is an array of svn_sort__item_t's. The keys are repository-absolute const char *paths, the values are svn_mergeinfo_t for each path. Return a pointer to the mergeinfo value of the nearest path-wise ancestor of FSPATH in DEPTH_FIRST_CATALOG_INDEX. A path is considered its own ancestor, so if a key exactly matches FSPATH, return that key's mergeinfo and set *ANCESTOR_IS_SELF to true (set it to false in all other cases). If DEPTH_FIRST_CATALOG_INDEX is NULL, empty, or no ancestor is found, then return NULL. */ static svn_mergeinfo_t find_nearest_ancestor(const apr_array_header_t *depth_first_catalog_index, svn_boolean_t *ancestor_is_self, const char *fspath) { int ancestor_index = -1; *ancestor_is_self = FALSE; if (depth_first_catalog_index) { int i; for (i = 0; i < depth_first_catalog_index->nelts; i++) { svn_sort__item_t item = APR_ARRAY_IDX(depth_first_catalog_index, i, svn_sort__item_t); if (svn_fspath__skip_ancestor(item.key, fspath)) { ancestor_index = i; /* There's no nearer ancestor than FSPATH itself. */ if (strcmp(item.key, fspath) == 0) { *ancestor_is_self = TRUE; break; } } } } if (ancestor_index == -1) return NULL; else return (APR_ARRAY_IDX(depth_first_catalog_index, ancestor_index, svn_sort__item_t)).value; } /* Baton for use with the filter_log_entry_with_rangelist() svn_log_entry_receiver_t callback. */ struct filter_log_entry_baton_t { /* Is TRUE if RANGELIST describes potentially merged revisions, is FALSE if RANGELIST describes potentially eligible revisions. */ svn_boolean_t filtering_merged; /* Unsorted array of repository relative paths representing the merge sources. There will be more than one source */ const apr_array_header_t *merge_source_fspaths; /* The repository-absolute path we are calling svn_client_log5() on. */ const char *target_fspath; /* Mergeinfo catalog for the tree rooted at TARGET_FSPATH. The path keys must be repository-absolute. */ svn_mergeinfo_catalog_t target_mergeinfo_catalog; /* Depth first sorted array of svn_sort__item_t's for TARGET_MERGEINFO_CATALOG. */ apr_array_header_t *depth_first_catalog_index; /* A rangelist describing all the revisions potentially merged or potentially eligible for merging (see FILTERING_MERGED) based on the target's explicit or inherited mergeinfo. */ const svn_rangelist_t *rangelist; /* The wrapped svn_log_entry_receiver_t callback and baton which filter_log_entry_with_rangelist() is acting as a filter for. */ svn_log_entry_receiver_t log_receiver; void *log_receiver_baton; svn_client_ctx_t *ctx; }; /* Implements the svn_log_entry_receiver_t interface. BATON is a `struct filter_log_entry_baton_t *'. Call the wrapped log receiver BATON->log_receiver (with BATON->log_receiver_baton) if: BATON->FILTERING_MERGED is FALSE and the changes represented by LOG_ENTRY have been fully merged from BATON->merge_source_fspaths to the WC target based on the mergeinfo for the WC contained in BATON->TARGET_MERGEINFO_CATALOG. Or BATON->FILTERING_MERGED is TRUE and the changes represented by LOG_ENTRY have not been merged, or only partially merged, from BATON->merge_source_fspaths to the WC target based on the mergeinfo for the WC contained in BATON->TARGET_MERGEINFO_CATALOG. */ static svn_error_t * filter_log_entry_with_rangelist(void *baton, svn_log_entry_t *log_entry, apr_pool_t *pool) { struct filter_log_entry_baton_t *fleb = baton; svn_rangelist_t *intersection, *this_rangelist; if (fleb->ctx->cancel_func) SVN_ERR(fleb->ctx->cancel_func(fleb->ctx->cancel_baton)); /* Ignore r0 because there can be no "change 0" in a merge range. */ if (log_entry->revision == 0) return SVN_NO_ERROR; this_rangelist = svn_rangelist__initialize(log_entry->revision - 1, log_entry->revision, TRUE, pool); /* Don't consider inheritance yet, see if LOG_ENTRY->REVISION is fully or partially represented in BATON->RANGELIST. */ SVN_ERR(svn_rangelist_intersect(&intersection, fleb->rangelist, this_rangelist, FALSE, pool)); if (! (intersection && intersection->nelts)) return SVN_NO_ERROR; SVN_ERR_ASSERT(intersection->nelts == 1); /* Ok, we know LOG_ENTRY->REVISION is represented in BATON->RANGELIST, but is it only partially represented, i.e. is the corresponding range in BATON->RANGELIST non-inheritable? Ask for the same intersection as above but consider inheritance this time, if the intersection is empty we know the range in BATON->RANGELIST is non-inheritable. */ SVN_ERR(svn_rangelist_intersect(&intersection, fleb->rangelist, this_rangelist, TRUE, pool)); log_entry->non_inheritable = !intersection->nelts; /* If the paths changed by LOG_ENTRY->REVISION are provided we can determine if LOG_ENTRY->REVISION, while only partially represented in BATON->RANGELIST, is in fact completely applied to all affected paths. ### And ... what if it is, or if it isn't? What do we do with the answer? And how do we cope if the changed paths are not provided? */ if ((log_entry->non_inheritable || !fleb->filtering_merged) && log_entry->changed_paths2) { apr_hash_index_t *hi; svn_boolean_t all_subtrees_have_this_rev = TRUE; svn_rangelist_t *this_rev_rangelist = svn_rangelist__initialize(log_entry->revision - 1, log_entry->revision, TRUE, pool); apr_pool_t *iterpool = svn_pool_create(pool); for (hi = apr_hash_first(pool, log_entry->changed_paths2); hi; hi = apr_hash_next(hi)) { int i; const char *path = svn__apr_hash_index_key(hi); svn_log_changed_path2_t *change = svn__apr_hash_index_val(hi); const char *target_fspath_affected; svn_mergeinfo_t nearest_ancestor_mergeinfo; svn_boolean_t found_this_revision = FALSE; const char *merge_source_rel_target; const char *merge_source_fspath; svn_boolean_t ancestor_is_self; svn_pool_clear(iterpool); /* Check that PATH is a subtree of at least one of the merge sources. If not then ignore this path. */ for (i = 0; i < fleb->merge_source_fspaths->nelts; i++) { merge_source_fspath = APR_ARRAY_IDX(fleb->merge_source_fspaths, i, const char *); merge_source_rel_target = svn_fspath__skip_ancestor(merge_source_fspath, path); if (merge_source_rel_target) { /* If MERGE_SOURCE was itself deleted, replaced, or added in LOG_ENTRY->REVISION then ignore this PATH since you can't merge a addition or deletion of yourself. */ if (merge_source_rel_target[0] == '\0' && (change->action != 'M')) i = fleb->merge_source_fspaths->nelts; break; } } /* If we examined every merge source path and PATH is a child of none of them then we can ignore this PATH. */ if (i == fleb->merge_source_fspaths->nelts) continue; /* Calculate the target path which PATH would affect if merged. */ target_fspath_affected = svn_fspath__join(fleb->target_fspath, merge_source_rel_target, iterpool); nearest_ancestor_mergeinfo = find_nearest_ancestor(fleb->depth_first_catalog_index, &ancestor_is_self, target_fspath_affected); /* Issue #3791: A path should never have explicit mergeinfo describing its own addition (that's self-referential). Nor will it have explicit mergeinfo describing its own deletion (we obviously can't add new mergeinfo to a path we are deleting). This lack of explicit mergeinfo should not cause such revisions to show up as eligible however. If PATH was deleted, replaced, or added in LOG_ENTRY->REVISION, but the corresponding TARGET_PATH_AFFECTED already exists and has explicit mergeinfo describing merges from PATH *after* LOG_ENTRY->REVISION, then ignore this PATH. If it was deleted in LOG_ENTRY->REVISION it's obviously back. If it was added or replaced it's still around possibly it was replaced one or more times, but it's back now. Regardless, LOG_ENTRY->REVISION is *not* an eligible revision! */ if (nearest_ancestor_mergeinfo && ancestor_is_self /* Explicit mergeinfo on TARGET_PATH_AFFECTED */ && (change->action != 'M')) { svn_rangelist_t *rangelist = svn_hash_gets(nearest_ancestor_mergeinfo, path); if (rangelist) { svn_merge_range_t *youngest_range = APR_ARRAY_IDX( rangelist, rangelist->nelts - 1, svn_merge_range_t *); if (youngest_range && (youngest_range->end > log_entry->revision)) continue; } } if (nearest_ancestor_mergeinfo) { apr_hash_index_t *hi2; for (hi2 = apr_hash_first(iterpool, nearest_ancestor_mergeinfo); hi2; hi2 = apr_hash_next(hi2)) { const char *mergeinfo_path = svn__apr_hash_index_key(hi2); svn_rangelist_t *rangelist = svn__apr_hash_index_val(hi2); /* Does the mergeinfo for PATH reflect if LOG_ENTRY->REVISION was previously merged from MERGE_SOURCE_FSPATH? */ if (svn_fspath__skip_ancestor(merge_source_fspath, mergeinfo_path)) { /* Something was merged from MERGE_SOURCE_FSPATH, does it include LOG_ENTRY->REVISION? */ SVN_ERR(svn_rangelist_intersect(&intersection, rangelist, this_rev_rangelist, FALSE, iterpool)); if (intersection->nelts) { if (ancestor_is_self) { /* TARGET_PATH_AFFECTED has explicit mergeinfo, so we don't need to worry if that mergeinfo is inheritable or not. */ found_this_revision = TRUE; break; } else { /* TARGET_PATH_AFFECTED inherited its mergeinfo, so we have to ignore non-inheritable ranges. */ SVN_ERR(svn_rangelist_intersect( &intersection, rangelist, this_rev_rangelist, TRUE, iterpool)); if (intersection->nelts) { found_this_revision = TRUE; break; } } } } } } if (!found_this_revision) { /* As soon as any PATH is found that is not fully merged for LOG_ENTRY->REVISION then we can stop. */ all_subtrees_have_this_rev = FALSE; break; } } svn_pool_destroy(iterpool); if (all_subtrees_have_this_rev) { if (fleb->filtering_merged) log_entry->non_inheritable = FALSE; else return SVN_NO_ERROR; } } /* Call the wrapped log receiver which this function is filtering for. */ return fleb->log_receiver(fleb->log_receiver_baton, log_entry, pool); } static svn_error_t * logs_for_mergeinfo_rangelist(const char *source_url, const apr_array_header_t *merge_source_fspaths, svn_boolean_t filtering_merged, const svn_rangelist_t *rangelist, svn_boolean_t oldest_revs_first, svn_mergeinfo_catalog_t target_mergeinfo_catalog, const char *target_fspath, svn_boolean_t discover_changed_paths, const apr_array_header_t *revprops, svn_log_entry_receiver_t log_receiver, void *log_receiver_baton, svn_client_ctx_t *ctx, svn_ra_session_t *ra_session, apr_pool_t *scratch_pool) { svn_merge_range_t *oldest_range, *youngest_range; svn_revnum_t oldest_rev, youngest_rev; struct filter_log_entry_baton_t fleb; if (! rangelist->nelts) return SVN_NO_ERROR; /* Calculate and construct the bounds of our log request. */ youngest_range = APR_ARRAY_IDX(rangelist, rangelist->nelts - 1, svn_merge_range_t *); youngest_rev = youngest_range->end; oldest_range = APR_ARRAY_IDX(rangelist, 0, svn_merge_range_t *); oldest_rev = oldest_range->start; if (! target_mergeinfo_catalog) target_mergeinfo_catalog = apr_hash_make(scratch_pool); /* FILTER_LOG_ENTRY_BATON_T->TARGET_MERGEINFO_CATALOG's keys are required to be repository-absolute. */ SVN_ERR(svn_mergeinfo__add_prefix_to_catalog(&target_mergeinfo_catalog, target_mergeinfo_catalog, "/", scratch_pool, scratch_pool)); /* Build the log filtering callback baton. */ fleb.filtering_merged = filtering_merged; fleb.merge_source_fspaths = merge_source_fspaths; fleb.target_mergeinfo_catalog = target_mergeinfo_catalog; fleb.depth_first_catalog_index = svn_sort__hash(target_mergeinfo_catalog, svn_sort_compare_items_as_paths, scratch_pool); fleb.target_fspath = target_fspath; fleb.rangelist = rangelist; fleb.log_receiver = log_receiver; fleb.log_receiver_baton = log_receiver_baton; fleb.ctx = ctx; if (!ra_session) SVN_ERR(svn_client__open_ra_session_internal(&ra_session, NULL, source_url, NULL, NULL, FALSE, FALSE, ctx, scratch_pool, scratch_pool)); else SVN_ERR(svn_ra_reparent(ra_session, source_url, scratch_pool)); { apr_array_header_t *target; target = apr_array_make(scratch_pool, 1, sizeof(const char *)); APR_ARRAY_PUSH(target, const char *) = ""; SVN_ERR(svn_ra_get_log2(ra_session, target, oldest_revs_first ? oldest_rev : youngest_rev, oldest_revs_first ? youngest_rev : oldest_rev, 0 /* limit */, discover_changed_paths, FALSE /* strict_node_history */, FALSE /* include_merged_revisions */, revprops, filter_log_entry_with_rangelist, &fleb, scratch_pool)); } /* Check for cancellation. */ if (ctx->cancel_func) SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); return SVN_NO_ERROR; } /* Set *OUT_MERGEINFO to a shallow copy of MERGEINFO with each source path converted to a (URI-encoded) URL based on REPOS_ROOT_URL. *OUT_MERGEINFO is declared as 'apr_hash_t *' because its key do not obey the rules of 'svn_mergeinfo_t'. Allocate *OUT_MERGEINFO and the new keys in RESULT_POOL. Use SCRATCH_POOL for any temporary allocations. */ static svn_error_t * mergeinfo_relpaths_to_urls(apr_hash_t **out_mergeinfo, svn_mergeinfo_t mergeinfo, const char *repos_root_url, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { *out_mergeinfo = NULL; if (mergeinfo) { apr_hash_index_t *hi; apr_hash_t *full_path_mergeinfo = apr_hash_make(result_pool); for (hi = apr_hash_first(scratch_pool, mergeinfo); hi; hi = apr_hash_next(hi)) { const char *key = svn__apr_hash_index_key(hi); void *val = svn__apr_hash_index_val(hi); svn_hash_sets(full_path_mergeinfo, svn_path_url_add_component2(repos_root_url, key + 1, result_pool), val); } *out_mergeinfo = full_path_mergeinfo; } return SVN_NO_ERROR; } /*** Public APIs ***/ svn_error_t * svn_client_mergeinfo_get_merged(apr_hash_t **mergeinfo_p, const char *path_or_url, const svn_opt_revision_t *peg_revision, svn_client_ctx_t *ctx, apr_pool_t *pool) { const char *repos_root; svn_mergeinfo_catalog_t mergeinfo_cat; svn_mergeinfo_t mergeinfo; SVN_ERR(get_mergeinfo(&mergeinfo_cat, &repos_root, path_or_url, peg_revision, FALSE, FALSE, ctx, NULL, pool, pool)); if (mergeinfo_cat) { const char *repos_relpath; if (! svn_path_is_url(path_or_url)) { SVN_ERR(svn_dirent_get_absolute(&path_or_url, path_or_url, pool)); SVN_ERR(svn_wc__node_get_repos_info(NULL, &repos_relpath, NULL, NULL, ctx->wc_ctx, path_or_url, pool, pool)); } else { repos_relpath = svn_uri_skip_ancestor(repos_root, path_or_url, pool); SVN_ERR_ASSERT(repos_relpath != NULL); /* Or get_mergeinfo failed */ } mergeinfo = svn_hash_gets(mergeinfo_cat, repos_relpath); } else { mergeinfo = NULL; } SVN_ERR(mergeinfo_relpaths_to_urls(mergeinfo_p, mergeinfo, repos_root, pool, pool)); return SVN_NO_ERROR; } svn_error_t * svn_client__mergeinfo_log(svn_boolean_t finding_merged, const char *target_path_or_url, const svn_opt_revision_t *target_peg_revision, svn_mergeinfo_catalog_t *target_mergeinfo_catalog, const char *source_path_or_url, const svn_opt_revision_t *source_peg_revision, const svn_opt_revision_t *source_start_revision, const svn_opt_revision_t *source_end_revision, svn_log_entry_receiver_t log_receiver, void *log_receiver_baton, svn_boolean_t discover_changed_paths, svn_depth_t depth, const apr_array_header_t *revprops, svn_client_ctx_t *ctx, svn_ra_session_t *ra_session, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { const char *log_target = NULL; const char *repos_root; const char *target_repos_relpath; svn_mergeinfo_catalog_t target_mergeinfo_cat; svn_ra_session_t *target_session = NULL; svn_client__pathrev_t *pathrev; /* A hash of paths, at or under TARGET_PATH_OR_URL, mapped to rangelists. Not technically mergeinfo, so not using the svn_mergeinfo_t type. */ apr_hash_t *inheritable_subtree_merges; svn_mergeinfo_t source_history; svn_mergeinfo_t target_history; svn_rangelist_t *master_noninheritable_rangelist; svn_rangelist_t *master_inheritable_rangelist; apr_array_header_t *merge_source_fspaths = apr_array_make(scratch_pool, 1, sizeof(const char *)); apr_hash_index_t *hi_catalog; apr_hash_index_t *hi; apr_pool_t *iterpool; svn_boolean_t oldest_revs_first = TRUE; apr_pool_t *subpool; /* We currently only support depth = empty | infinity. */ if (depth != svn_depth_infinity && depth != svn_depth_empty) return svn_error_create( SVN_ERR_UNSUPPORTED_FEATURE, NULL, _("Only depths 'infinity' and 'empty' are currently supported")); /* Validate and sanitize the incoming source operative revision range. */ if (!((source_start_revision->kind == svn_opt_revision_unspecified) || (source_start_revision->kind == svn_opt_revision_number) || (source_start_revision->kind == svn_opt_revision_date) || (source_start_revision->kind == svn_opt_revision_head))) return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL); if (!((source_end_revision->kind == svn_opt_revision_unspecified) || (source_end_revision->kind == svn_opt_revision_number) || (source_end_revision->kind == svn_opt_revision_date) || (source_end_revision->kind == svn_opt_revision_head))) return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL); if ((source_end_revision->kind != svn_opt_revision_unspecified) && (source_start_revision->kind == svn_opt_revision_unspecified)) return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL); if ((source_end_revision->kind == svn_opt_revision_unspecified) && (source_start_revision->kind != svn_opt_revision_unspecified)) return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL); subpool = svn_pool_create(scratch_pool); if (ra_session) target_session = ra_session; /* We need the union of TARGET_PATH_OR_URL@TARGET_PEG_REVISION's mergeinfo and MERGE_SOURCE_URL's history. It's not enough to do path matching, because renames in the history of MERGE_SOURCE_URL throw that all in a tizzy. Of course, if there's no mergeinfo on the target, that vastly simplifies matters (we'll have nothing to do). */ /* This get_mergeinfo() call doubles as a mergeinfo capabilities check. */ if (target_mergeinfo_catalog) { if (*target_mergeinfo_catalog) { /* The caller provided the mergeinfo catalog for TARGET_PATH_OR_URL, so we don't need to accquire it ourselves. We do need to get the repos_root though, because get_mergeinfo() won't do it for us. */ target_mergeinfo_cat = *target_mergeinfo_catalog; if (ra_session && svn_path_is_url(target_path_or_url)) { SVN_ERR(svn_ra_reparent(ra_session, target_path_or_url, subpool)); SVN_ERR(svn_client__resolve_rev_and_url(&pathrev, ra_session, target_path_or_url, target_peg_revision, target_peg_revision, ctx, subpool)); target_session = ra_session; } else { SVN_ERR(svn_client__ra_session_from_path2(&target_session, &pathrev, target_path_or_url, NULL, target_peg_revision, target_peg_revision, ctx, subpool)); } SVN_ERR(svn_ra_get_repos_root2(target_session, &repos_root, scratch_pool)); } else { /* The caller didn't provide the mergeinfo catalog for TARGET_PATH_OR_URL, but wants us to pass a copy back when we get it, so use RESULT_POOL. */ SVN_ERR(get_mergeinfo(target_mergeinfo_catalog, &repos_root, target_path_or_url, target_peg_revision, depth == svn_depth_infinity, TRUE, ctx, ra_session, result_pool, scratch_pool)); target_mergeinfo_cat = *target_mergeinfo_catalog; } } else { /* The caller didn't provide the mergeinfo catalog for TARGET_PATH_OR_URL, nor does it want a copy, so we can use nothing but SCRATCH_POOL. */ SVN_ERR(get_mergeinfo(&target_mergeinfo_cat, &repos_root, target_path_or_url, target_peg_revision, depth == svn_depth_infinity, TRUE, ctx, ra_session, scratch_pool, scratch_pool)); } if (!svn_path_is_url(target_path_or_url)) { SVN_ERR(svn_dirent_get_absolute(&target_path_or_url, target_path_or_url, scratch_pool)); SVN_ERR(svn_wc__node_get_repos_info(NULL, &target_repos_relpath, NULL, NULL, ctx->wc_ctx, target_path_or_url, scratch_pool, scratch_pool)); } else { target_repos_relpath = svn_uri_skip_ancestor(repos_root, target_path_or_url, scratch_pool); /* TARGET_REPOS_REL should be non-NULL, else get_mergeinfo should have failed. */ SVN_ERR_ASSERT(target_repos_relpath != NULL); } if (!target_mergeinfo_cat) { /* If we are looking for what has been merged and there is no mergeinfo then we already know the answer. If we are looking for eligible revisions then create a catalog with empty mergeinfo on the target. This is semantically equivalent to no mergeinfo and gives us something to combine with MERGE_SOURCE_URL's history. */ if (finding_merged) { svn_pool_destroy(subpool); return SVN_NO_ERROR; } else { target_mergeinfo_cat = apr_hash_make(scratch_pool); svn_hash_sets(target_mergeinfo_cat, target_repos_relpath, apr_hash_make(scratch_pool)); } } /* Fetch the location history as mergeinfo, for the source branch * (between the given start and end revisions), and, if we're finding * merged revisions, then also for the entire target branch. * * ### TODO: As the source and target must be in the same repository, we * should share a single session, tracking the two URLs separately. */ { svn_ra_session_t *source_session; svn_revnum_t start_rev, end_rev, youngest_rev = SVN_INVALID_REVNUM; if (! finding_merged) { if (!target_session) SVN_ERR(svn_client__ra_session_from_path2(&target_session, &pathrev, target_path_or_url, NULL, target_peg_revision, target_peg_revision, ctx, subpool)); SVN_ERR(svn_client__get_history_as_mergeinfo(&target_history, NULL, pathrev, SVN_INVALID_REVNUM, SVN_INVALID_REVNUM, target_session, ctx, scratch_pool)); } if (target_session && svn_path_is_url(source_path_or_url) && repos_root && svn_uri_skip_ancestor(repos_root, source_path_or_url, subpool)) { /* We can re-use the existing session */ source_session = target_session; SVN_ERR(svn_ra_reparent(source_session, source_path_or_url, subpool)); SVN_ERR(svn_client__resolve_rev_and_url(&pathrev, source_session, source_path_or_url, source_peg_revision, source_peg_revision, ctx, subpool)); } else { SVN_ERR(svn_client__ra_session_from_path2(&source_session, &pathrev, source_path_or_url, NULL, source_peg_revision, source_peg_revision, ctx, subpool)); } SVN_ERR(svn_client__get_revision_number(&start_rev, &youngest_rev, ctx->wc_ctx, source_path_or_url, source_session, source_start_revision, subpool)); SVN_ERR(svn_client__get_revision_number(&end_rev, &youngest_rev, ctx->wc_ctx, source_path_or_url, source_session, source_end_revision, subpool)); SVN_ERR(svn_client__get_history_as_mergeinfo(&source_history, NULL, pathrev, MAX(end_rev, start_rev), MIN(end_rev, start_rev), source_session, ctx, scratch_pool)); if (start_rev > end_rev) oldest_revs_first = FALSE; } /* Separate the explicit or inherited mergeinfo on TARGET_PATH_OR_URL, and possibly its explicit subtree mergeinfo, into their inheritable and non-inheritable parts. */ master_noninheritable_rangelist = apr_array_make(scratch_pool, 64, sizeof(svn_merge_range_t *)); master_inheritable_rangelist = apr_array_make(scratch_pool, 64, sizeof(svn_merge_range_t *)); inheritable_subtree_merges = apr_hash_make(scratch_pool); iterpool = svn_pool_create(scratch_pool); for (hi_catalog = apr_hash_first(scratch_pool, target_mergeinfo_cat); hi_catalog; hi_catalog = apr_hash_next(hi_catalog)) { svn_mergeinfo_t subtree_mergeinfo = svn__apr_hash_index_val(hi_catalog); svn_mergeinfo_t subtree_history; svn_mergeinfo_t subtree_source_history; svn_mergeinfo_t subtree_inheritable_mergeinfo; svn_mergeinfo_t subtree_noninheritable_mergeinfo; svn_mergeinfo_t merged_noninheritable; svn_mergeinfo_t merged; const char *subtree_path = svn__apr_hash_index_key(hi_catalog); svn_boolean_t is_subtree = strcmp(subtree_path, target_repos_relpath) != 0; svn_pool_clear(iterpool); if (is_subtree) { /* If SUBTREE_PATH is a proper subtree of TARGET_PATH_OR_URL then make a copy of SOURCE_HISTORY that is path adjusted for the subtree. */ const char *subtree_rel_path = subtree_path + strlen(target_repos_relpath) + 1; SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo( &subtree_source_history, source_history, subtree_rel_path, scratch_pool, scratch_pool)); if (!finding_merged) SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo( &subtree_history, target_history, subtree_rel_path, scratch_pool, scratch_pool)); } else { subtree_source_history = source_history; if (!finding_merged) subtree_history = target_history; } if (!finding_merged) { svn_mergeinfo_t merged_via_history; SVN_ERR(svn_mergeinfo_intersect2(&merged_via_history, subtree_history, subtree_source_history, TRUE, scratch_pool, iterpool)); SVN_ERR(svn_mergeinfo_merge2(subtree_mergeinfo, merged_via_history, scratch_pool, scratch_pool)); } SVN_ERR(svn_mergeinfo_inheritable2(&subtree_inheritable_mergeinfo, subtree_mergeinfo, NULL, SVN_INVALID_REVNUM, SVN_INVALID_REVNUM, TRUE, scratch_pool, iterpool)); SVN_ERR(svn_mergeinfo_inheritable2(&subtree_noninheritable_mergeinfo, subtree_mergeinfo, NULL, SVN_INVALID_REVNUM, SVN_INVALID_REVNUM, FALSE, scratch_pool, iterpool)); /* Find the intersection of the non-inheritable part of SUBTREE_MERGEINFO and SOURCE_HISTORY. svn_mergeinfo_intersect2() won't consider non-inheritable and inheritable ranges intersecting unless we ignore inheritance, but in doing so the resulting intersections have all inheritable ranges. To get around this we set the inheritance on the result to all non-inheritable. */ SVN_ERR(svn_mergeinfo_intersect2(&merged_noninheritable, subtree_noninheritable_mergeinfo, subtree_source_history, FALSE, scratch_pool, iterpool)); svn_mergeinfo__set_inheritance(merged_noninheritable, FALSE, scratch_pool); /* Keep track of all ranges partially merged to any and all subtrees. */ SVN_ERR(svn_rangelist__merge_many(master_noninheritable_rangelist, merged_noninheritable, scratch_pool, iterpool)); /* Find the intersection of the inheritable part of TGT_MERGEINFO and SOURCE_HISTORY. */ SVN_ERR(svn_mergeinfo_intersect2(&merged, subtree_inheritable_mergeinfo, subtree_source_history, FALSE, scratch_pool, iterpool)); /* Keep track of all ranges fully merged to any and all subtrees. */ if (apr_hash_count(merged)) { /* The inheritable rangelist merged from SUBTREE_SOURCE_HISTORY to SUBTREE_PATH. */ svn_rangelist_t *subtree_merged_rangelist = apr_array_make(scratch_pool, 1, sizeof(svn_merge_range_t *)); SVN_ERR(svn_rangelist__merge_many(master_inheritable_rangelist, merged, scratch_pool, iterpool)); SVN_ERR(svn_rangelist__merge_many(subtree_merged_rangelist, merged, scratch_pool, iterpool)); svn_hash_sets(inheritable_subtree_merges, subtree_path, subtree_merged_rangelist); } else { /* Map SUBTREE_PATH to an empty rangelist if there was nothing fully merged. e.g. Only empty or non-inheritable mergeinfo on the subtree or mergeinfo unrelated to the source. */ svn_hash_sets(inheritable_subtree_merges, subtree_path, apr_array_make(scratch_pool, 0, sizeof(svn_merge_range_t *))); } } /* Make sure every range in MASTER_INHERITABLE_RANGELIST is fully merged to each subtree (including the target itself). Any revisions which don't exist in *every* subtree are *potentially* only partially merged to the tree rooted at TARGET_PATH_OR_URL, so move those revisions to MASTER_NONINHERITABLE_RANGELIST. It may turn out that that a revision was merged to the only subtree it affects, but we need to examine the logs to make this determination (which will be done by logs_for_mergeinfo_rangelist). */ if (master_inheritable_rangelist->nelts) { for (hi = apr_hash_first(scratch_pool, inheritable_subtree_merges); hi; hi = apr_hash_next(hi)) { svn_rangelist_t *deleted_rangelist; svn_rangelist_t *added_rangelist; svn_rangelist_t *subtree_merged_rangelist = svn__apr_hash_index_val(hi); svn_pool_clear(iterpool); SVN_ERR(svn_rangelist_diff(&deleted_rangelist, &added_rangelist, master_inheritable_rangelist, subtree_merged_rangelist, TRUE, iterpool)); if (deleted_rangelist->nelts) { svn_rangelist__set_inheritance(deleted_rangelist, FALSE); SVN_ERR(svn_rangelist_merge2(master_noninheritable_rangelist, deleted_rangelist, scratch_pool, iterpool)); SVN_ERR(svn_rangelist_remove(&master_inheritable_rangelist, deleted_rangelist, master_inheritable_rangelist, FALSE, scratch_pool)); } } } if (finding_merged) { /* Roll all the merged revisions into one rangelist. */ SVN_ERR(svn_rangelist_merge2(master_inheritable_rangelist, master_noninheritable_rangelist, scratch_pool, scratch_pool)); } else { /* Create the starting rangelist for what might be eligible. */ svn_rangelist_t *source_master_rangelist = apr_array_make(scratch_pool, 1, sizeof(svn_merge_range_t *)); SVN_ERR(svn_rangelist__merge_many(source_master_rangelist, source_history, scratch_pool, scratch_pool)); /* From what might be eligible subtract what we know is partially merged and then merge that back. */ SVN_ERR(svn_rangelist_remove(&source_master_rangelist, master_noninheritable_rangelist, source_master_rangelist, FALSE, scratch_pool)); SVN_ERR(svn_rangelist_merge2(source_master_rangelist, master_noninheritable_rangelist, scratch_pool, scratch_pool)); SVN_ERR(svn_rangelist_remove(&master_inheritable_rangelist, master_inheritable_rangelist, source_master_rangelist, TRUE, scratch_pool)); } /* Nothing merged? Not even when considering shared history if looking for eligible revisions (i.e. !FINDING_MERGED)? Then there is nothing more to do. */ if (! master_inheritable_rangelist->nelts) { svn_pool_destroy(iterpool); return SVN_NO_ERROR; } else { /* Determine the correct (youngest) target for 'svn log'. */ svn_merge_range_t *youngest_range = APR_ARRAY_IDX(master_inheritable_rangelist, master_inheritable_rangelist->nelts - 1, svn_merge_range_t *); svn_rangelist_t *youngest_rangelist = svn_rangelist__initialize(youngest_range->end - 1, youngest_range->end, youngest_range->inheritable, scratch_pool);; for (hi = apr_hash_first(scratch_pool, source_history); hi; hi = apr_hash_next(hi)) { const char *key = svn__apr_hash_index_key(hi); svn_rangelist_t *subtree_merged_rangelist = svn__apr_hash_index_val(hi); svn_rangelist_t *intersecting_rangelist; svn_pool_clear(iterpool); SVN_ERR(svn_rangelist_intersect(&intersecting_rangelist, youngest_rangelist, subtree_merged_rangelist, FALSE, iterpool)); APR_ARRAY_PUSH(merge_source_fspaths, const char *) = key; if (intersecting_rangelist->nelts) log_target = key; } } svn_pool_destroy(iterpool); /* Step 4: Finally, we run 'svn log' to drive our log receiver, but using a receiver filter to only allow revisions to pass through that are in our rangelist. */ log_target = svn_path_url_add_component2(repos_root, log_target + 1, scratch_pool); { svn_error_t *err; err = logs_for_mergeinfo_rangelist(log_target, merge_source_fspaths, finding_merged, master_inheritable_rangelist, oldest_revs_first, target_mergeinfo_cat, svn_fspath__join("/", target_repos_relpath, scratch_pool), discover_changed_paths, revprops, log_receiver, log_receiver_baton, ctx, target_session, scratch_pool); /* Close the source and target sessions. */ svn_pool_destroy(subpool); /* For SVN_ERR_CEASE_INVOCATION */ return svn_error_trace(err); } } svn_error_t * svn_client_mergeinfo_log2(svn_boolean_t finding_merged, const char *target_path_or_url, const svn_opt_revision_t *target_peg_revision, const char *source_path_or_url, const svn_opt_revision_t *source_peg_revision, const svn_opt_revision_t *source_start_revision, const svn_opt_revision_t *source_end_revision, svn_log_entry_receiver_t log_receiver, void *log_receiver_baton, svn_boolean_t discover_changed_paths, svn_depth_t depth, const apr_array_header_t *revprops, svn_client_ctx_t *ctx, apr_pool_t *scratch_pool) { return svn_error_trace( svn_client__mergeinfo_log(finding_merged, target_path_or_url, target_peg_revision, NULL, source_path_or_url, source_peg_revision, source_start_revision, source_end_revision, log_receiver, log_receiver_baton, discover_changed_paths, depth, revprops, ctx, NULL, scratch_pool, scratch_pool)); } svn_error_t * svn_client_suggest_merge_sources(apr_array_header_t **suggestions, const char *path_or_url, const svn_opt_revision_t *peg_revision, svn_client_ctx_t *ctx, apr_pool_t *pool) { const char *repos_root; const char *copyfrom_path; apr_array_header_t *list; svn_revnum_t copyfrom_rev; svn_mergeinfo_catalog_t mergeinfo_cat; svn_mergeinfo_t mergeinfo; apr_hash_index_t *hi; list = apr_array_make(pool, 1, sizeof(const char *)); /* In our ideal algorithm, the list of recommendations should be ordered by: 1. The most recent existing merge source. 2. The copyfrom source (which will also be listed as a merge source if the copy was made with a 1.5+ client and server). 3. All other merge sources, most recent to least recent. However, determining the order of application of merge sources requires a new RA API. Until such an API is available, our algorithm will be: 1. The copyfrom source. 2. All remaining merge sources (unordered). */ /* ### TODO: Share ra_session batons to improve efficiency? */ SVN_ERR(get_mergeinfo(&mergeinfo_cat, &repos_root, path_or_url, peg_revision, FALSE, FALSE, ctx, NULL, pool, pool)); if (mergeinfo_cat && apr_hash_count(mergeinfo_cat)) { /* We asked only for the PATH_OR_URL's mergeinfo, not any of its descendants. So if there is anything in the catalog it is the mergeinfo for PATH_OR_URL. */ mergeinfo = svn__apr_hash_index_val(apr_hash_first(pool, mergeinfo_cat)); } else { mergeinfo = NULL; } SVN_ERR(svn_client__get_copy_source(©from_path, ©from_rev, path_or_url, peg_revision, ctx, pool, pool)); if (copyfrom_path) { APR_ARRAY_PUSH(list, const char *) = svn_path_url_add_component2(repos_root, copyfrom_path, pool); } if (mergeinfo) { for (hi = apr_hash_first(pool, mergeinfo); hi; hi = apr_hash_next(hi)) { const char *rel_path = svn__apr_hash_index_key(hi); if (copyfrom_path == NULL || strcmp(rel_path, copyfrom_path) != 0) APR_ARRAY_PUSH(list, const char *) = \ svn_path_url_add_component2(repos_root, rel_path + 1, pool); } } *suggestions = list; return SVN_NO_ERROR; } svn_error_t * svn_client__mergeinfo_status(svn_boolean_t *mergeinfo_changes, svn_wc_context_t *wc_ctx, const char *local_abspath, apr_pool_t *scratch_pool) { apr_array_header_t *propchanges; int i; *mergeinfo_changes = FALSE; SVN_ERR(svn_wc_get_prop_diffs2(&propchanges, NULL, wc_ctx, local_abspath, scratch_pool, scratch_pool)); for (i = 0; i < propchanges->nelts; i++) { svn_prop_t prop = APR_ARRAY_IDX(propchanges, i, svn_prop_t); if (strcmp(prop.name, SVN_PROP_MERGEINFO) == 0) { *mergeinfo_changes = TRUE; break; } } return SVN_NO_ERROR; }