| /* |
| * mergeinfo.c : merge history functions for the libsvn_client library |
| * |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| */ |
| |
| #include <apr_pools.h> |
| #include <apr_strings.h> |
| |
| #include "svn_pools.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_path.h" |
| #include "svn_string.h" |
| #include "svn_opt.h" |
| #include "svn_error.h" |
| #include "svn_error_codes.h" |
| #include "svn_props.h" |
| #include "svn_mergeinfo.h" |
| #include "svn_sorts.h" |
| #include "svn_ra.h" |
| #include "svn_client.h" |
| #include "svn_hash.h" |
| |
| #include "private/svn_client_private.h" |
| #include "private/svn_opt_private.h" |
| #include "private/svn_mergeinfo_private.h" |
| #include "private/svn_ra_private.h" |
| #include "private/svn_sorts_private.h" |
| #include "private/svn_wc_private.h" |
| #include "private/svn_fspath.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 */, |
| 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 /* ignore_enoent */, |
| 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, scratch_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 = apr_hash_this_key(hi); |
| svn_string_t *propval = apr_hash_this_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 = |
| apr_hash_this_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 = |
| apr_hash_this_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, 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_error_t *err; |
| |
| /* Get the TARGET_WCPATH's explicit mergeinfo. */ |
| err = svn_client__get_wc_mergeinfo(&target_mergeinfo, NULL, |
| svn_mergeinfo_explicit, |
| target_abspath, |
| NULL, NULL, 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 (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, |
| NULL, 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 = apr_hash_this_key(hi); |
| svn_log_changed_path2_t *change = apr_hash_this_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 = apr_hash_this_key(hi2); |
| svn_rangelist_t *rangelist = apr_hash_this_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 = apr_hash_this_key(hi); |
| void *val = apr_hash_this_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 = apr_hash_this_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 = apr_hash_this_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, iterpool)); |
| |
| if (!finding_merged) |
| SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo( |
| &subtree_history, target_history, |
| subtree_rel_path, scratch_pool, iterpool)); |
| } |
| 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, iterpool)); |
| } |
| |
| 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, |
| iterpool); |
| |
| /* 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 = apr_hash_this_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 = apr_hash_this_key(hi); |
| svn_rangelist_t *subtree_merged_rangelist = apr_hash_this_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; |
| apr_pool_t *session_pool = svn_pool_create(pool); |
| svn_ra_session_t *ra_session; |
| |
| 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). |
| */ |
| SVN_ERR(svn_client__ra_session_from_path2(&ra_session, NULL, path_or_url, |
| NULL, peg_revision, peg_revision, |
| ctx, session_pool)); |
| |
| SVN_ERR(get_mergeinfo(&mergeinfo_cat, &repos_root, path_or_url, |
| peg_revision, FALSE, FALSE, |
| ctx, ra_session, session_pool, session_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 = apr_hash_this_val(apr_hash_first(session_pool, |
| mergeinfo_cat)); |
| } |
| else |
| { |
| mergeinfo = NULL; |
| } |
| |
| /* ### Should we only add the last source or all copy sources back to |
| the origin? */ |
| SVN_ERR(svn_client__get_copy_source(©from_path, ©from_rev, |
| path_or_url, peg_revision, ra_session, |
| ctx, session_pool, session_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(session_pool, mergeinfo); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| const char *rel_path = apr_hash_this_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); |
| } |
| } |
| |
| svn_pool_destroy(session_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; |
| } |