| /* log.c --- retrieving log messages |
| * |
| * ==================================================================== |
| * 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 <stdlib.h> |
| #define APR_WANT_STRFUNC |
| #include <apr_want.h> |
| |
| #include "svn_compat.h" |
| #include "svn_private_config.h" |
| #include "svn_hash.h" |
| #include "svn_pools.h" |
| #include "svn_error.h" |
| #include "svn_path.h" |
| #include "svn_fs.h" |
| #include "svn_repos.h" |
| #include "svn_string.h" |
| #include "svn_sorts.h" |
| #include "svn_props.h" |
| #include "svn_mergeinfo.h" |
| #include "repos.h" |
| #include "private/svn_fspath.h" |
| #include "private/svn_fs_private.h" |
| #include "private/svn_mergeinfo_private.h" |
| #include "private/svn_subr_private.h" |
| #include "private/svn_sorts_private.h" |
| #include "private/svn_string_private.h" |
| |
| |
| /* This is a mere convenience struct such that we don't need to pass that |
| many parameters around individually. */ |
| typedef struct log_callbacks_t |
| { |
| svn_repos_path_change_receiver_t path_change_receiver; |
| void *path_change_receiver_baton; |
| svn_repos_log_entry_receiver_t revision_receiver; |
| void *revision_receiver_baton; |
| svn_repos_authz_func_t authz_read_func; |
| void *authz_read_baton; |
| } log_callbacks_t; |
| |
| |
| svn_repos_path_change_t * |
| svn_repos_path_change_create(apr_pool_t *result_pool) |
| { |
| svn_repos_path_change_t *change = apr_pcalloc(result_pool, sizeof(*change)); |
| |
| change->path.data = ""; |
| change->change_kind = svn_fs_path_change_reset; |
| change->mergeinfo_mod = svn_tristate_unknown; |
| change->copyfrom_rev = SVN_INVALID_REVNUM; |
| |
| return change; |
| } |
| |
| svn_repos_path_change_t * |
| svn_repos_path_change_dup(svn_repos_path_change_t *change, |
| apr_pool_t *result_pool) |
| { |
| svn_repos_path_change_t *new_change = apr_pmemdup(result_pool, change, |
| sizeof(*new_change)); |
| |
| new_change->path.data = apr_pstrmemdup(result_pool, change->path.data, |
| change->path.len); |
| if (change->copyfrom_path) |
| new_change->copyfrom_path = apr_pstrdup(result_pool, |
| change->copyfrom_path); |
| |
| return new_change; |
| } |
| |
| svn_repos_log_entry_t * |
| svn_repos_log_entry_create(apr_pool_t *result_pool) |
| { |
| svn_repos_log_entry_t *log_entry = apr_pcalloc(result_pool, |
| sizeof(*log_entry)); |
| |
| return log_entry; |
| } |
| |
| svn_repos_log_entry_t * |
| svn_repos_log_entry_dup(const svn_repos_log_entry_t *log_entry, |
| apr_pool_t *result_pool) |
| { |
| svn_repos_log_entry_t *new_entry = apr_pmemdup(result_pool, log_entry, |
| sizeof(*new_entry)); |
| |
| if (log_entry->revprops) |
| new_entry->revprops = svn_prop_hash_dup(log_entry->revprops, result_pool); |
| |
| return new_entry; |
| } |
| |
| svn_error_t * |
| svn_repos_check_revision_access(svn_repos_revision_access_level_t *access_level, |
| svn_repos_t *repos, |
| svn_revnum_t revision, |
| svn_repos_authz_func_t authz_read_func, |
| void *authz_read_baton, |
| apr_pool_t *pool) |
| { |
| svn_fs_t *fs = svn_repos_fs(repos); |
| svn_fs_root_t *rev_root; |
| svn_fs_path_change_iterator_t *iterator; |
| svn_fs_path_change3_t *change; |
| svn_boolean_t found_readable = FALSE; |
| svn_boolean_t found_unreadable = FALSE; |
| apr_pool_t *iterpool; |
| |
| /* By default, we'll grant full read access to REVISION. */ |
| *access_level = svn_repos_revision_access_full; |
| |
| /* No auth-checking function? We're done. */ |
| if (! authz_read_func) |
| return SVN_NO_ERROR; |
| |
| /* Fetch the changes associated with REVISION. */ |
| SVN_ERR(svn_fs_revision_root(&rev_root, fs, revision, pool)); |
| SVN_ERR(svn_fs_paths_changed3(&iterator, rev_root, pool, pool)); |
| SVN_ERR(svn_fs_path_change_get(&change, iterator)); |
| |
| /* No changed paths? We're done. |
| |
| Note that the check at "decision:" assumes that at least one |
| path has been processed. So, this actually affects functionality. */ |
| if (!change) |
| return SVN_NO_ERROR; |
| |
| /* Otherwise, we have to check the readability of each changed |
| path, or at least enough to answer the question asked. */ |
| iterpool = svn_pool_create(pool); |
| while (change) |
| { |
| svn_boolean_t readable; |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR(authz_read_func(&readable, rev_root, change->path.data, |
| authz_read_baton, iterpool)); |
| if (! readable) |
| found_unreadable = TRUE; |
| else |
| found_readable = TRUE; |
| |
| /* If we have at least one of each (readable/unreadable), we |
| have our answer. */ |
| if (found_readable && found_unreadable) |
| goto decision; |
| |
| switch (change->change_kind) |
| { |
| case svn_fs_path_change_add: |
| case svn_fs_path_change_replace: |
| { |
| const char *copyfrom_path; |
| svn_revnum_t copyfrom_rev; |
| |
| SVN_ERR(svn_fs_copied_from(©from_rev, ©from_path, |
| rev_root, change->path.data, |
| iterpool)); |
| if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_rev)) |
| { |
| svn_fs_root_t *copyfrom_root; |
| SVN_ERR(svn_fs_revision_root(©from_root, fs, |
| copyfrom_rev, iterpool)); |
| SVN_ERR(authz_read_func(&readable, |
| copyfrom_root, copyfrom_path, |
| authz_read_baton, iterpool)); |
| if (! readable) |
| found_unreadable = TRUE; |
| |
| /* If we have at least one of each (readable/unreadable), we |
| have our answer. */ |
| if (found_readable && found_unreadable) |
| goto decision; |
| } |
| } |
| break; |
| |
| case svn_fs_path_change_delete: |
| case svn_fs_path_change_modify: |
| default: |
| break; |
| } |
| |
| SVN_ERR(svn_fs_path_change_get(&change, iterator)); |
| } |
| |
| decision: |
| svn_pool_destroy(iterpool); |
| |
| /* Either every changed path was unreadable... */ |
| if (! found_readable) |
| *access_level = svn_repos_revision_access_none; |
| |
| /* ... or some changed path was unreadable... */ |
| else if (found_unreadable) |
| *access_level = svn_repos_revision_access_partial; |
| |
| /* ... or every changed path was readable (the default). */ |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Find all significant changes under ROOT and, if not NULL, report them |
| * to the CALLBACKS->PATH_CHANGE_RECEIVER. "Significant" means that the |
| * text or properties of the node were changed, or that the node was added |
| * or deleted. |
| * |
| * If optional CALLBACKS->AUTHZ_READ_FUNC is non-NULL, then use it (with |
| * CALLBACKS->AUTHZ_READ_BATON and FS) to check whether each changed-path |
| * (and copyfrom_path) is readable: |
| * |
| * - If absolutely every changed-path (and copyfrom_path) is |
| * readable, then return the full CHANGED hash, and set |
| * *ACCESS_LEVEL to svn_repos_revision_access_full. |
| * |
| * - If some paths are readable and some are not, then silently |
| * omit the unreadable paths from the CHANGED hash, and set |
| * *ACCESS_LEVEL to svn_repos_revision_access_partial. |
| * |
| * - If absolutely every changed-path (and copyfrom_path) is |
| * unreadable, then return an empty CHANGED hash, and set |
| * *ACCESS_LEVEL to svn_repos_revision_access_none. (This is |
| * to distinguish a revision which truly has no changed paths |
| * from a revision in which all paths are unreadable.) |
| */ |
| static svn_error_t * |
| detect_changed(svn_repos_revision_access_level_t *access_level, |
| svn_fs_root_t *root, |
| svn_fs_t *fs, |
| const log_callbacks_t *callbacks, |
| apr_pool_t *scratch_pool) |
| { |
| svn_fs_path_change_iterator_t *iterator; |
| svn_fs_path_change3_t *change; |
| apr_pool_t *iterpool; |
| svn_boolean_t found_readable = FALSE; |
| svn_boolean_t found_unreadable = FALSE; |
| |
| /* Retrieve the first change in the list. */ |
| SVN_ERR(svn_fs_paths_changed3(&iterator, root, scratch_pool, scratch_pool)); |
| SVN_ERR(svn_fs_path_change_get(&change, iterator)); |
| |
| if (!change) |
| { |
| /* No paths changed in this revision? Uh, sure, I guess the |
| revision is readable, then. */ |
| *access_level = svn_repos_revision_access_full; |
| return SVN_NO_ERROR; |
| } |
| |
| iterpool = svn_pool_create(scratch_pool); |
| while (change) |
| { |
| /* NOTE: Much of this loop is going to look quite similar to |
| svn_repos_check_revision_access(), but we have to do more things |
| here, so we'll live with the duplication. */ |
| const char *path = change->path.data; |
| svn_pool_clear(iterpool); |
| |
| /* Skip path if unreadable. */ |
| if (callbacks->authz_read_func) |
| { |
| svn_boolean_t readable; |
| SVN_ERR(callbacks->authz_read_func(&readable, root, path, |
| callbacks->authz_read_baton, |
| iterpool)); |
| if (! readable) |
| { |
| found_unreadable = TRUE; |
| SVN_ERR(svn_fs_path_change_get(&change, iterator)); |
| continue; |
| } |
| } |
| |
| /* At least one changed-path was readable. */ |
| found_readable = TRUE; |
| |
| /* Pre-1.6 revision files don't store the change path kind, so fetch |
| it manually. */ |
| if (change->node_kind == svn_node_unknown) |
| { |
| svn_fs_root_t *check_root = root; |
| const char *check_path = path; |
| |
| /* Deleted items don't exist so check earlier revision. We |
| know the parent must exist and could be a copy */ |
| if (change->change_kind == svn_fs_path_change_delete) |
| { |
| svn_fs_history_t *history; |
| svn_revnum_t prev_rev; |
| const char *parent_path, *name; |
| |
| svn_fspath__split(&parent_path, &name, path, iterpool); |
| |
| SVN_ERR(svn_fs_node_history2(&history, root, parent_path, |
| iterpool, iterpool)); |
| |
| /* Two calls because the first call returns the original |
| revision as the deleted child means it is 'interesting' */ |
| SVN_ERR(svn_fs_history_prev2(&history, history, TRUE, iterpool, |
| iterpool)); |
| SVN_ERR(svn_fs_history_prev2(&history, history, TRUE, iterpool, |
| iterpool)); |
| |
| SVN_ERR(svn_fs_history_location(&parent_path, &prev_rev, |
| history, iterpool)); |
| SVN_ERR(svn_fs_revision_root(&check_root, fs, prev_rev, |
| iterpool)); |
| check_path = svn_fspath__join(parent_path, name, iterpool); |
| } |
| |
| SVN_ERR(svn_fs_check_path(&change->node_kind, check_root, check_path, |
| iterpool)); |
| } |
| |
| if ( (change->change_kind == svn_fs_path_change_add) |
| || (change->change_kind == svn_fs_path_change_replace)) |
| { |
| const char *copyfrom_path = change->copyfrom_path; |
| svn_revnum_t copyfrom_rev = change->copyfrom_rev; |
| |
| /* the following is a potentially expensive operation since on FSFS |
| we will follow the DAG from ROOT to PATH and that requires |
| actually reading the directories along the way. */ |
| if (!change->copyfrom_known) |
| { |
| SVN_ERR(svn_fs_copied_from(©from_rev, ©from_path, |
| root, path, iterpool)); |
| change->copyfrom_known = TRUE; |
| } |
| |
| if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_rev)) |
| { |
| svn_boolean_t readable = TRUE; |
| |
| if (callbacks->authz_read_func) |
| { |
| svn_fs_root_t *copyfrom_root; |
| |
| SVN_ERR(svn_fs_revision_root(©from_root, fs, |
| copyfrom_rev, iterpool)); |
| SVN_ERR(callbacks->authz_read_func(&readable, |
| copyfrom_root, |
| copyfrom_path, |
| callbacks->authz_read_baton, |
| iterpool)); |
| if (! readable) |
| found_unreadable = TRUE; |
| } |
| |
| if (readable) |
| { |
| change->copyfrom_path = copyfrom_path; |
| change->copyfrom_rev = copyfrom_rev; |
| } |
| } |
| } |
| |
| if (callbacks->path_change_receiver) |
| SVN_ERR(callbacks->path_change_receiver( |
| callbacks->path_change_receiver_baton, |
| change, |
| iterpool)); |
| |
| /* Next changed path. */ |
| SVN_ERR(svn_fs_path_change_get(&change, iterator)); |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| if (! found_readable) |
| { |
| /* Every changed-path was unreadable. */ |
| *access_level = svn_repos_revision_access_none; |
| } |
| else if (found_unreadable) |
| { |
| /* At least one changed-path was unreadable. */ |
| *access_level = svn_repos_revision_access_partial; |
| } |
| else |
| { |
| /* Every changed-path was readable. */ |
| *access_level = svn_repos_revision_access_full; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* This is used by svn_repos_get_logs to keep track of multiple |
| * path history information while working through history. |
| * |
| * The two pools are swapped after each iteration through history because |
| * to get the next history requires the previous one. |
| */ |
| struct path_info |
| { |
| svn_stringbuf_t *path; |
| svn_revnum_t history_rev; |
| svn_boolean_t done; |
| svn_boolean_t first_time; |
| |
| /* If possible, we like to keep open the history object for each path, |
| since it avoids needed to open and close it many times as we walk |
| backwards in time. To do so we need two pools, so that we can clear |
| one each time through. If we're not holding the history open for |
| this path then these three pointers will be NULL. */ |
| svn_fs_history_t *hist; |
| apr_pool_t *newpool; |
| apr_pool_t *oldpool; |
| }; |
| |
| /* Advance to the next history for the path. |
| * |
| * If INFO->HIST is not NULL we do this using that existing history object, |
| * otherwise we open a new one. |
| * |
| * If no more history is available or the history revision is less |
| * (earlier) than START, or the history is not available due |
| * to authorization, then INFO->DONE is set to TRUE. |
| * |
| * A STRICT value of FALSE will indicate to follow history across copied |
| * paths. |
| * |
| * If optional AUTHZ_READ_FUNC is non-NULL, then use it (with |
| * AUTHZ_READ_BATON and FS) to check whether INFO->PATH is still readable if |
| * we do indeed find more history for the path. |
| */ |
| static svn_error_t * |
| get_history(struct path_info *info, |
| svn_fs_t *fs, |
| svn_boolean_t strict, |
| svn_repos_authz_func_t authz_read_func, |
| void *authz_read_baton, |
| svn_revnum_t start, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_fs_root_t *history_root = NULL; |
| svn_fs_history_t *hist; |
| apr_pool_t *subpool; |
| const char *path; |
| |
| if (info->hist) |
| { |
| subpool = info->newpool; |
| |
| SVN_ERR(svn_fs_history_prev2(&info->hist, info->hist, ! strict, |
| subpool, scratch_pool)); |
| |
| hist = info->hist; |
| } |
| else |
| { |
| subpool = svn_pool_create(result_pool); |
| |
| /* Open the history located at the last rev we were at. */ |
| SVN_ERR(svn_fs_revision_root(&history_root, fs, info->history_rev, |
| subpool)); |
| |
| SVN_ERR(svn_fs_node_history2(&hist, history_root, info->path->data, |
| subpool, scratch_pool)); |
| |
| SVN_ERR(svn_fs_history_prev2(&hist, hist, ! strict, subpool, |
| scratch_pool)); |
| |
| if (info->first_time) |
| info->first_time = FALSE; |
| else |
| SVN_ERR(svn_fs_history_prev2(&hist, hist, ! strict, subpool, |
| scratch_pool)); |
| } |
| |
| if (! hist) |
| { |
| svn_pool_destroy(subpool); |
| if (info->oldpool) |
| svn_pool_destroy(info->oldpool); |
| info->done = TRUE; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Fetch the location information for this history step. */ |
| SVN_ERR(svn_fs_history_location(&path, &info->history_rev, |
| hist, subpool)); |
| |
| svn_stringbuf_set(info->path, path); |
| |
| /* If this history item predates our START revision then |
| don't fetch any more for this path. */ |
| if (info->history_rev < start) |
| { |
| svn_pool_destroy(subpool); |
| if (info->oldpool) |
| svn_pool_destroy(info->oldpool); |
| info->done = TRUE; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Is the history item readable? If not, done with path. */ |
| if (authz_read_func) |
| { |
| svn_boolean_t readable; |
| SVN_ERR(svn_fs_revision_root(&history_root, fs, |
| info->history_rev, |
| scratch_pool)); |
| SVN_ERR(authz_read_func(&readable, history_root, |
| info->path->data, |
| authz_read_baton, |
| scratch_pool)); |
| if (! readable) |
| info->done = TRUE; |
| } |
| |
| if (! info->hist) |
| { |
| svn_pool_destroy(subpool); |
| } |
| else |
| { |
| apr_pool_t *temppool = info->oldpool; |
| info->oldpool = info->newpool; |
| svn_pool_clear(temppool); |
| info->newpool = temppool; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Set INFO->HIST to the next history for the path *if* there is history |
| * available and INFO->HISTORY_REV is equal to or greater than CURRENT. |
| * |
| * *CHANGED is set to TRUE if the path has history in the CURRENT revision, |
| * otherwise it is not touched. |
| * |
| * If we do need to get the next history revision for the path, call |
| * get_history to do it -- see it for details. |
| */ |
| static svn_error_t * |
| check_history(svn_boolean_t *changed, |
| struct path_info *info, |
| svn_fs_t *fs, |
| svn_revnum_t current, |
| svn_boolean_t strict, |
| svn_repos_authz_func_t authz_read_func, |
| void *authz_read_baton, |
| svn_revnum_t start, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| /* If we're already done with histories for this path, |
| don't try to fetch any more. */ |
| if (info->done) |
| return SVN_NO_ERROR; |
| |
| /* If the last rev we got for this path is less than CURRENT, |
| then just return and don't fetch history for this path. |
| The caller will get to this rev eventually or else reach |
| the limit. */ |
| if (info->history_rev < current) |
| return SVN_NO_ERROR; |
| |
| /* If the last rev we got for this path is equal to CURRENT |
| then set *CHANGED to true and get the next history |
| rev where this path was changed. */ |
| *changed = TRUE; |
| return get_history(info, fs, strict, authz_read_func, |
| authz_read_baton, start, result_pool, scratch_pool); |
| } |
| |
| /* Return the next interesting revision in our list of HISTORIES. */ |
| static svn_revnum_t |
| next_history_rev(const apr_array_header_t *histories) |
| { |
| svn_revnum_t next_rev = SVN_INVALID_REVNUM; |
| int i; |
| |
| for (i = 0; i < histories->nelts; ++i) |
| { |
| struct path_info *info = APR_ARRAY_IDX(histories, i, |
| struct path_info *); |
| if (info->done) |
| continue; |
| if (info->history_rev > next_rev) |
| next_rev = info->history_rev; |
| } |
| |
| return next_rev; |
| } |
| |
| /* Set *DELETED_MERGEINFO_CATALOG and *ADDED_MERGEINFO_CATALOG to |
| catalogs describing how mergeinfo values on paths (which are the |
| keys of those catalogs) were changed in REV. */ |
| /* ### TODO: This would make a *great*, useful public function, |
| ### svn_repos_fs_mergeinfo_changed()! -- cmpilato */ |
| static svn_error_t * |
| fs_mergeinfo_changed(svn_mergeinfo_catalog_t *deleted_mergeinfo_catalog, |
| svn_mergeinfo_catalog_t *added_mergeinfo_catalog, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_fs_root_t *root; |
| apr_pool_t *iterpool, *iterator_pool; |
| svn_fs_path_change_iterator_t *iterator; |
| svn_fs_path_change3_t *change; |
| svn_boolean_t any_mergeinfo = FALSE; |
| svn_boolean_t any_copy = FALSE; |
| |
| /* Initialize return variables. */ |
| *deleted_mergeinfo_catalog = svn_hash__make(result_pool); |
| *added_mergeinfo_catalog = svn_hash__make(result_pool); |
| |
| /* Revision 0 has no mergeinfo and no mergeinfo changes. */ |
| if (rev == 0) |
| return SVN_NO_ERROR; |
| |
| /* FS iterators are potentially heavy objects. |
| * Hold them in a separate pool to clean them up asap. */ |
| iterator_pool = svn_pool_create(scratch_pool); |
| |
| /* We're going to use the changed-paths information for REV to |
| narrow down our search. */ |
| SVN_ERR(svn_fs_revision_root(&root, fs, rev, scratch_pool)); |
| SVN_ERR(svn_fs_paths_changed3(&iterator, root, iterator_pool, |
| iterator_pool)); |
| SVN_ERR(svn_fs_path_change_get(&change, iterator)); |
| |
| /* Look for copies and (potential) mergeinfo changes. |
| We will use both flags to take shortcuts further down the road. |
| |
| The critical information here is whether there are any copies |
| because that greatly influences the costs for log processing. |
| So, it is faster to iterate over the changes twice - in the worst |
| case b/c most times there is no m/i at all and we exit out early |
| without any overhead. |
| */ |
| while (change && (!any_mergeinfo || !any_copy)) |
| { |
| /* If there was a prop change and we are not positive that _no_ |
| mergeinfo change happened, we must assume that it might have. */ |
| if (change->mergeinfo_mod != svn_tristate_false && change->prop_mod) |
| any_mergeinfo = TRUE; |
| |
| if ( (change->change_kind == svn_fs_path_change_add) |
| || (change->change_kind == svn_fs_path_change_replace)) |
| any_copy = TRUE; |
| |
| SVN_ERR(svn_fs_path_change_get(&change, iterator)); |
| } |
| |
| /* No potential mergeinfo changes? We're done. */ |
| if (! any_mergeinfo) |
| { |
| svn_pool_destroy(iterator_pool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* There is or may be some m/i change. Look closely now. */ |
| svn_pool_clear(iterator_pool); |
| SVN_ERR(svn_fs_paths_changed3(&iterator, root, iterator_pool, |
| iterator_pool)); |
| |
| /* Loop over changes, looking for anything that might carry an |
| svn:mergeinfo change and is one of our paths of interest, or a |
| child or [grand]parent directory thereof. */ |
| iterpool = svn_pool_create(scratch_pool); |
| while (TRUE) |
| { |
| const char *changed_path; |
| const char *base_path = NULL; |
| svn_revnum_t base_rev = SVN_INVALID_REVNUM; |
| svn_fs_root_t *base_root = NULL; |
| svn_string_t *prev_mergeinfo_value = NULL, *mergeinfo_value; |
| |
| /* Next change. */ |
| SVN_ERR(svn_fs_path_change_get(&change, iterator)); |
| if (!change) |
| break; |
| |
| /* Cheap pre-checks that don't require memory allocation etc. */ |
| |
| /* No mergeinfo change? -> nothing to do here. */ |
| if (change->mergeinfo_mod == svn_tristate_false) |
| continue; |
| |
| /* If there was no property change on this item, ignore it. */ |
| if (! change->prop_mod) |
| continue; |
| |
| /* Begin actual processing */ |
| changed_path = change->path.data; |
| svn_pool_clear(iterpool); |
| |
| switch (change->change_kind) |
| { |
| |
| /* ### TODO: Can the add, replace, and modify cases be joined |
| ### together to all use svn_repos__prev_location()? The |
| ### difference would be the fallback case (path/rev-1 for |
| ### modifies, NULL otherwise). -- cmpilato */ |
| |
| /* If the path was merely modified, see if its previous |
| location was affected by a copy which happened in this |
| revision before assuming it holds the same path it did the |
| previous revision. */ |
| case svn_fs_path_change_modify: |
| { |
| svn_revnum_t appeared_rev; |
| |
| /* If there were no copies in this revision, the path will have |
| existed in the previous rev. Otherwise, we might just got |
| copied here and need to check for that eventuality. */ |
| if (any_copy) |
| { |
| SVN_ERR(svn_repos__prev_location(&appeared_rev, &base_path, |
| &base_rev, fs, rev, |
| changed_path, iterpool)); |
| |
| /* If this path isn't the result of a copy that occurred |
| in this revision, we can find the previous version of |
| it in REV - 1 at the same path. */ |
| if (! (base_path && SVN_IS_VALID_REVNUM(base_rev) |
| && (appeared_rev == rev))) |
| { |
| base_path = changed_path; |
| base_rev = rev - 1; |
| } |
| } |
| else |
| { |
| base_path = changed_path; |
| base_rev = rev - 1; |
| } |
| break; |
| } |
| |
| /* If the path was added or replaced, see if it was created via |
| copy. If so, set BASE_REV/BASE_PATH to its previous location. |
| If not, there's no previous location to examine -- leave |
| BASE_REV/BASE_PATH = -1/NULL. */ |
| case svn_fs_path_change_add: |
| case svn_fs_path_change_replace: |
| { |
| if (change->copyfrom_known) |
| { |
| base_rev = change->copyfrom_rev; |
| base_path = change->copyfrom_path; |
| } |
| else |
| { |
| SVN_ERR(svn_fs_copied_from(&base_rev, &base_path, |
| root, changed_path, iterpool)); |
| } |
| break; |
| } |
| |
| /* We don't care about any of the other cases. */ |
| case svn_fs_path_change_delete: |
| case svn_fs_path_change_reset: |
| default: |
| continue; |
| } |
| |
| /* If there was a base location, fetch its mergeinfo property value. */ |
| if (base_path && SVN_IS_VALID_REVNUM(base_rev)) |
| { |
| SVN_ERR(svn_fs_revision_root(&base_root, fs, base_rev, iterpool)); |
| SVN_ERR(svn_fs_node_prop(&prev_mergeinfo_value, base_root, base_path, |
| SVN_PROP_MERGEINFO, iterpool)); |
| } |
| |
| /* Now fetch the current (as of REV) mergeinfo property value. */ |
| SVN_ERR(svn_fs_node_prop(&mergeinfo_value, root, changed_path, |
| SVN_PROP_MERGEINFO, iterpool)); |
| |
| /* No mergeinfo on either the new or previous location? Just |
| skip it. (If there *was* a change, it would have been in |
| inherited mergeinfo only, which should be picked up by the |
| iteration of this loop that finds the parent paths that |
| really got changed.) */ |
| if (! (mergeinfo_value || prev_mergeinfo_value)) |
| continue; |
| |
| /* Mergeinfo on both sides but it did not change? Skip that too. */ |
| if ( mergeinfo_value && prev_mergeinfo_value |
| && svn_string_compare(mergeinfo_value, prev_mergeinfo_value)) |
| continue; |
| |
| /* If mergeinfo was explicitly added or removed on this path, we |
| need to check to see if that was a real semantic change of |
| meaning. So, fill in the "missing" mergeinfo value with the |
| inherited mergeinfo for that path/revision. */ |
| if (prev_mergeinfo_value && (! mergeinfo_value)) |
| { |
| svn_mergeinfo_t tmp_mergeinfo; |
| |
| SVN_ERR(svn_fs__get_mergeinfo_for_path(&tmp_mergeinfo, |
| root, changed_path, |
| svn_mergeinfo_inherited, TRUE, |
| iterpool, iterpool)); |
| if (tmp_mergeinfo) |
| SVN_ERR(svn_mergeinfo_to_string(&mergeinfo_value, |
| tmp_mergeinfo, |
| iterpool)); |
| } |
| else if (mergeinfo_value && (! prev_mergeinfo_value) |
| && base_path && SVN_IS_VALID_REVNUM(base_rev)) |
| { |
| svn_mergeinfo_t tmp_mergeinfo; |
| |
| SVN_ERR(svn_fs__get_mergeinfo_for_path(&tmp_mergeinfo, |
| base_root, base_path, |
| svn_mergeinfo_inherited, TRUE, |
| iterpool, iterpool)); |
| if (tmp_mergeinfo) |
| SVN_ERR(svn_mergeinfo_to_string(&prev_mergeinfo_value, |
| tmp_mergeinfo, |
| iterpool)); |
| } |
| |
| /* Old and new mergeinfo probably differ in some way (we already |
| checked for textual equality further up). Store the before and |
| after mergeinfo values in our return hashes. They may still be |
| equal as manual intervention may have only changed the formatting |
| but not the relevant contents. */ |
| { |
| svn_mergeinfo_t prev_mergeinfo = NULL, mergeinfo = NULL; |
| svn_mergeinfo_t deleted, added; |
| const char *hash_path; |
| |
| if (mergeinfo_value) |
| SVN_ERR(svn_mergeinfo_parse(&mergeinfo, |
| mergeinfo_value->data, iterpool)); |
| if (prev_mergeinfo_value) |
| SVN_ERR(svn_mergeinfo_parse(&prev_mergeinfo, |
| prev_mergeinfo_value->data, iterpool)); |
| SVN_ERR(svn_mergeinfo_diff2(&deleted, &added, prev_mergeinfo, |
| mergeinfo, FALSE, result_pool, |
| iterpool)); |
| |
| /* Toss interesting stuff into our return catalogs. */ |
| hash_path = apr_pstrdup(result_pool, changed_path); |
| svn_hash_sets(*deleted_mergeinfo_catalog, hash_path, deleted); |
| svn_hash_sets(*added_mergeinfo_catalog, hash_path, added); |
| } |
| } |
| |
| svn_pool_destroy(iterpool); |
| svn_pool_destroy(iterator_pool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Determine what (if any) mergeinfo for PATHS was modified in |
| revision REV, returning the differences for added mergeinfo in |
| *ADDED_MERGEINFO and deleted mergeinfo in *DELETED_MERGEINFO. */ |
| static svn_error_t * |
| get_combined_mergeinfo_changes(svn_mergeinfo_t *added_mergeinfo, |
| svn_mergeinfo_t *deleted_mergeinfo, |
| svn_fs_t *fs, |
| const apr_array_header_t *paths, |
| svn_revnum_t rev, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_mergeinfo_catalog_t added_mergeinfo_catalog, deleted_mergeinfo_catalog; |
| apr_hash_index_t *hi; |
| svn_fs_root_t *root; |
| apr_pool_t *iterpool; |
| int i; |
| svn_error_t *err; |
| |
| /* Initialize return value. */ |
| *added_mergeinfo = svn_hash__make(result_pool); |
| *deleted_mergeinfo = svn_hash__make(result_pool); |
| |
| /* If we're asking about revision 0, there's no mergeinfo to be found. */ |
| if (rev == 0) |
| return SVN_NO_ERROR; |
| |
| /* No paths? No mergeinfo. */ |
| if (! paths->nelts) |
| return SVN_NO_ERROR; |
| |
| /* Fetch the mergeinfo changes for REV. */ |
| err = fs_mergeinfo_changed(&deleted_mergeinfo_catalog, |
| &added_mergeinfo_catalog, |
| fs, rev, |
| scratch_pool, scratch_pool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR) |
| { |
| /* Issue #3896: If invalid mergeinfo is encountered the |
| best we can do is ignore it and act as if there were |
| no mergeinfo modifications. */ |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| else |
| { |
| return svn_error_trace(err); |
| } |
| } |
| |
| /* In most revisions, there will be no mergeinfo change at all. */ |
| if ( apr_hash_count(deleted_mergeinfo_catalog) == 0 |
| && apr_hash_count(added_mergeinfo_catalog) == 0) |
| return SVN_NO_ERROR; |
| |
| /* Create a work subpool and get a root for REV. */ |
| SVN_ERR(svn_fs_revision_root(&root, fs, rev, scratch_pool)); |
| |
| /* Check our PATHS for any changes to their inherited mergeinfo. |
| (We deal with changes to mergeinfo directly *on* the paths in the |
| following loop.) */ |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < paths->nelts; i++) |
| { |
| const char *path = APR_ARRAY_IDX(paths, i, const char *); |
| const char *prev_path; |
| svn_revnum_t appeared_rev, prev_rev; |
| svn_fs_root_t *prev_root; |
| svn_mergeinfo_t prev_mergeinfo, mergeinfo, deleted, added, |
| prev_inherited_mergeinfo, inherited_mergeinfo; |
| |
| svn_pool_clear(iterpool); |
| |
| /* If this path is represented in the changed-mergeinfo hashes, |
| we'll deal with it in the loop below. */ |
| if (svn_hash_gets(deleted_mergeinfo_catalog, path)) |
| continue; |
| |
| /* Figure out what path/rev to compare against. Ignore |
| not-found errors returned by the filesystem. */ |
| err = svn_repos__prev_location(&appeared_rev, &prev_path, &prev_rev, |
| fs, rev, path, iterpool); |
| if (err && (err->apr_err == SVN_ERR_FS_NOT_FOUND || |
| err->apr_err == SVN_ERR_FS_NOT_DIRECTORY)) |
| { |
| svn_error_clear(err); |
| err = SVN_NO_ERROR; |
| continue; |
| } |
| SVN_ERR(err); |
| |
| /* If this path isn't the result of a copy that occurred in this |
| revision, we can find the previous version of it in REV - 1 |
| at the same path. */ |
| if (! (prev_path && SVN_IS_VALID_REVNUM(prev_rev) |
| && (appeared_rev == rev))) |
| { |
| prev_path = path; |
| prev_rev = rev - 1; |
| } |
| |
| /* Fetch the previous mergeinfo (including inherited stuff) for |
| this path. Ignore not-found errors returned by the |
| filesystem or invalid mergeinfo (Issue #3896).*/ |
| SVN_ERR(svn_fs_revision_root(&prev_root, fs, prev_rev, iterpool)); |
| err = svn_fs__get_mergeinfo_for_path(&prev_mergeinfo, |
| prev_root, prev_path, |
| svn_mergeinfo_inherited, TRUE, |
| iterpool, iterpool); |
| if (err && (err->apr_err == SVN_ERR_FS_NOT_FOUND || |
| err->apr_err == SVN_ERR_FS_NOT_DIRECTORY || |
| err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)) |
| { |
| svn_error_clear(err); |
| err = SVN_NO_ERROR; |
| continue; |
| } |
| SVN_ERR(err); |
| |
| /* Issue #4022 'svn log -g interprets change in inherited mergeinfo due |
| to move as a merge': A copy where the source and destination inherit |
| mergeinfo from the same parent means the inherited mergeinfo of the |
| source and destination will differ, but this diffrence is not |
| indicative of a merge unless the mergeinfo on the inherited parent |
| has actually changed. |
| |
| To check for this we must fetch the "raw" previous inherited |
| mergeinfo and the "raw" mergeinfo @REV then compare these. */ |
| SVN_ERR(svn_fs__get_mergeinfo_for_path(&prev_inherited_mergeinfo, |
| prev_root, prev_path, |
| svn_mergeinfo_nearest_ancestor, |
| FALSE, /* adjust_inherited_mergeinfo */ |
| iterpool, iterpool)); |
| |
| /* Fetch the current mergeinfo (as of REV, and including |
| inherited stuff) for this path. */ |
| SVN_ERR(svn_fs__get_mergeinfo_for_path(&mergeinfo, |
| root, path, |
| svn_mergeinfo_inherited, TRUE, |
| iterpool, iterpool)); |
| |
| /* Issue #4022 again, fetch the raw inherited mergeinfo. */ |
| SVN_ERR(svn_fs__get_mergeinfo_for_path(&inherited_mergeinfo, |
| root, path, |
| svn_mergeinfo_nearest_ancestor, |
| FALSE, /* adjust_inherited_mergeinfo */ |
| iterpool, iterpool)); |
| |
| if (!prev_mergeinfo && !mergeinfo) |
| continue; |
| |
| /* Last bit of issue #4022 checking. */ |
| if (prev_inherited_mergeinfo && inherited_mergeinfo) |
| { |
| svn_boolean_t inherits_same_mergeinfo; |
| |
| SVN_ERR(svn_mergeinfo__equals(&inherits_same_mergeinfo, |
| prev_inherited_mergeinfo, |
| inherited_mergeinfo, |
| TRUE, iterpool)); |
| /* If a copy rather than an actual merge brought about an |
| inherited mergeinfo change then we are finished. */ |
| if (inherits_same_mergeinfo) |
| continue; |
| } |
| else |
| { |
| svn_boolean_t same_mergeinfo; |
| SVN_ERR(svn_mergeinfo__equals(&same_mergeinfo, |
| prev_inherited_mergeinfo, |
| NULL, |
| TRUE, iterpool)); |
| if (same_mergeinfo) |
| continue; |
| } |
| |
| /* Compare, constrast, and combine the results. */ |
| SVN_ERR(svn_mergeinfo_diff2(&deleted, &added, prev_mergeinfo, |
| mergeinfo, FALSE, result_pool, iterpool)); |
| SVN_ERR(svn_mergeinfo_merge2(*deleted_mergeinfo, deleted, |
| result_pool, iterpool)); |
| SVN_ERR(svn_mergeinfo_merge2(*added_mergeinfo, added, |
| result_pool, iterpool)); |
| } |
| |
| /* Merge all the mergeinfos which are, or are children of, one of |
| our paths of interest into one giant delta mergeinfo. */ |
| for (hi = apr_hash_first(scratch_pool, added_mergeinfo_catalog); |
| hi; hi = apr_hash_next(hi)) |
| { |
| const char *changed_path = apr_hash_this_key(hi); |
| apr_ssize_t klen = apr_hash_this_key_len(hi); |
| svn_mergeinfo_t added = apr_hash_this_val(hi); |
| svn_mergeinfo_t deleted; |
| |
| for (i = 0; i < paths->nelts; i++) |
| { |
| const char *path = APR_ARRAY_IDX(paths, i, const char *); |
| if (! svn_fspath__skip_ancestor(path, changed_path)) |
| continue; |
| svn_pool_clear(iterpool); |
| deleted = apr_hash_get(deleted_mergeinfo_catalog, changed_path, klen); |
| SVN_ERR(svn_mergeinfo_merge2(*deleted_mergeinfo, |
| svn_mergeinfo_dup(deleted, result_pool), |
| result_pool, iterpool)); |
| SVN_ERR(svn_mergeinfo_merge2(*added_mergeinfo, |
| svn_mergeinfo_dup(added, result_pool), |
| result_pool, iterpool)); |
| |
| break; |
| } |
| } |
| |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Fill LOG_ENTRY with history information in FS at REV. */ |
| static svn_error_t * |
| fill_log_entry(svn_repos_log_entry_t *log_entry, |
| svn_revnum_t rev, |
| svn_fs_t *fs, |
| const apr_array_header_t *revprops, |
| const log_callbacks_t *callbacks, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *r_props; |
| svn_boolean_t get_revprops = TRUE, censor_revprops = FALSE; |
| svn_boolean_t want_revprops = !revprops || revprops->nelts; |
| |
| /* Discover changed paths if the user requested them |
| or if we need to check that they are readable. */ |
| if ((rev > 0) |
| && (callbacks->authz_read_func || callbacks->path_change_receiver)) |
| { |
| svn_fs_root_t *newroot; |
| svn_repos_revision_access_level_t access_level; |
| |
| SVN_ERR(svn_fs_revision_root(&newroot, fs, rev, pool)); |
| SVN_ERR(detect_changed(&access_level, newroot, fs, callbacks, pool)); |
| |
| if (access_level == svn_repos_revision_access_none) |
| { |
| /* All changed-paths are unreadable, so clear all fields. */ |
| get_revprops = FALSE; |
| } |
| else if (access_level == svn_repos_revision_access_partial) |
| { |
| /* At least one changed-path was unreadable, so censor all |
| but author and date. (The unreadable paths are already |
| missing from the hash.) */ |
| censor_revprops = TRUE; |
| } |
| } |
| |
| if (get_revprops && want_revprops) |
| { |
| /* User is allowed to see at least some revprops. */ |
| SVN_ERR(svn_fs_revision_proplist2(&r_props, fs, rev, FALSE, pool, |
| pool)); |
| if (revprops == NULL) |
| { |
| /* Requested all revprops... */ |
| if (censor_revprops) |
| { |
| /* ... but we can only return author/date. */ |
| log_entry->revprops = svn_hash__make(pool); |
| svn_hash_sets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR, |
| svn_hash_gets(r_props, SVN_PROP_REVISION_AUTHOR)); |
| svn_hash_sets(log_entry->revprops, SVN_PROP_REVISION_DATE, |
| svn_hash_gets(r_props, SVN_PROP_REVISION_DATE)); |
| } |
| else |
| /* ... so return all we got. */ |
| log_entry->revprops = r_props; |
| } |
| else |
| { |
| int i; |
| |
| /* Requested only some revprops... */ |
| |
| /* Make "svn:author" and "svn:date" available as svn_string_t |
| for efficient comparison via svn_string_compare(). Note that |
| we want static initialization here and must therefore emulate |
| strlen(x) by sizeof(x)-1. */ |
| static const svn_string_t svn_prop_revision_author |
| = SVN__STATIC_STRING(SVN_PROP_REVISION_AUTHOR); |
| static const svn_string_t svn_prop_revision_date |
| = SVN__STATIC_STRING(SVN_PROP_REVISION_DATE); |
| |
| /* often only the standard revprops got requested and delivered. |
| In that case, we can simply pass the hash on. */ |
| if (revprops->nelts == apr_hash_count(r_props) && !censor_revprops) |
| { |
| log_entry->revprops = r_props; |
| for (i = 0; i < revprops->nelts; i++) |
| { |
| const svn_string_t *name |
| = APR_ARRAY_IDX(revprops, i, const svn_string_t *); |
| if (!apr_hash_get(r_props, name->data, name->len)) |
| { |
| /* hash does not match list of revprops we want */ |
| log_entry->revprops = NULL; |
| break; |
| } |
| } |
| } |
| |
| /* slow, revprop-by-revprop filtering */ |
| if (log_entry->revprops == NULL) |
| for (i = 0; i < revprops->nelts; i++) |
| { |
| const svn_string_t *name |
| = APR_ARRAY_IDX(revprops, i, const svn_string_t *); |
| svn_string_t *value |
| = apr_hash_get(r_props, name->data, name->len); |
| if (censor_revprops |
| && !svn_string_compare(name, &svn_prop_revision_author) |
| && !svn_string_compare(name, &svn_prop_revision_date)) |
| /* ... but we can only return author/date. */ |
| continue; |
| if (log_entry->revprops == NULL) |
| log_entry->revprops = svn_hash__make(pool); |
| apr_hash_set(log_entry->revprops, name->data, name->len, value); |
| } |
| } |
| } |
| |
| log_entry->revision = rev; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Baton type to be used with the interesting_merge callback. */ |
| typedef struct interesting_merge_baton_t |
| { |
| /* What we are looking for. */ |
| svn_revnum_t rev; |
| svn_mergeinfo_t log_target_history_as_mergeinfo; |
| |
| /* Set to TRUE if we found it. */ |
| svn_boolean_t found_rev_of_interest; |
| |
| /* We need to invoke this user-provided callback if not NULL. */ |
| svn_repos_path_change_receiver_t inner; |
| void *inner_baton; |
| } interesting_merge_baton_t; |
| |
| /* Implements svn_repos_path_change_receiver_t. |
| * *BATON is a interesting_merge_baton_t. |
| * |
| * If BATON->REV a merged revision that is not already part of |
| * BATON->LOG_TARGET_HISTORY_AS_MERGEINFO, set BATON->FOUND_REV_OF_INTEREST. |
| */ |
| static svn_error_t * |
| interesting_merge(void *baton, |
| svn_repos_path_change_t *change, |
| apr_pool_t *scratch_pool) |
| { |
| interesting_merge_baton_t *b = baton; |
| apr_hash_index_t *hi; |
| |
| if (b->inner) |
| SVN_ERR(b->inner(b->inner_baton, change, scratch_pool)); |
| |
| if (b->found_rev_of_interest) |
| return SVN_NO_ERROR; |
| |
| /* Look at each path on the log target's mergeinfo. */ |
| for (hi = apr_hash_first(scratch_pool, b->log_target_history_as_mergeinfo); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| const char *mergeinfo_path = apr_hash_this_key(hi); |
| svn_rangelist_t *rangelist = apr_hash_this_val(hi); |
| |
| /* Check whether CHANGED_PATH at revision REV is a child of |
| a (path, revision) tuple in LOG_TARGET_HISTORY_AS_MERGEINFO. */ |
| if (svn_fspath__skip_ancestor(mergeinfo_path, change->path.data)) |
| { |
| int i; |
| |
| for (i = 0; i < rangelist->nelts; i++) |
| { |
| svn_merge_range_t *range |
| = APR_ARRAY_IDX(rangelist, i, svn_merge_range_t *); |
| if (b->rev > range->start && b->rev <= range->end) |
| return SVN_NO_ERROR; |
| } |
| } |
| } |
| |
| b->found_rev_of_interest = TRUE; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Send a log message for REV to the CALLBACKS. |
| |
| FS is used with REV to fetch the interesting history information, |
| such as changed paths, revprops, etc. |
| |
| The detect_changed function is used if either CALLBACKS->AUTHZ_READ_FUNC |
| is not NULL, or if CALLBACKS->PATH_CHANGE_RECEIVER is not NULL. |
| See it for details. |
| |
| If DESCENDING_ORDER is true, send child messages in descending order. |
| |
| If REVPROPS is NULL, retrieve all revision properties; else, retrieve |
| only the revision properties named by the (const char *) array elements |
| (i.e. retrieve none if the array is empty). |
| |
| LOG_TARGET_HISTORY_AS_MERGEINFO, HANDLING_MERGED_REVISION, and |
| NESTED_MERGES are as per the arguments of the same name to DO_LOGS. |
| If HANDLING_MERGED_REVISION is true and *all* changed paths within REV are |
| already represented in LOG_TARGET_HISTORY_AS_MERGEINFO, then don't send |
| the log message for REV. If SUBTRACTIVE_MERGE is true, then REV was |
| reverse merged. |
| |
| If HANDLING_MERGED_REVISIONS is FALSE then ignore NESTED_MERGES. Otherwise |
| if NESTED_MERGES is not NULL and REV is contained in it, then don't send |
| the log for REV, otherwise send it normally and add REV to |
| NESTED_MERGES. */ |
| static svn_error_t * |
| send_log(svn_revnum_t rev, |
| svn_fs_t *fs, |
| svn_mergeinfo_t log_target_history_as_mergeinfo, |
| svn_bit_array__t *nested_merges, |
| svn_boolean_t subtractive_merge, |
| svn_boolean_t handling_merged_revision, |
| const apr_array_header_t *revprops, |
| svn_boolean_t has_children, |
| const log_callbacks_t *callbacks, |
| apr_pool_t *pool) |
| { |
| svn_repos_log_entry_t log_entry = { 0 }; |
| log_callbacks_t my_callbacks = *callbacks; |
| |
| interesting_merge_baton_t baton; |
| |
| /* Is REV a merged revision that is already part of |
| LOG_TARGET_HISTORY_AS_MERGEINFO? If so then there is no |
| need to send it, since it already was (or will be) sent. |
| |
| Use our callback to snoop through the changes. */ |
| if (handling_merged_revision |
| && log_target_history_as_mergeinfo |
| && apr_hash_count(log_target_history_as_mergeinfo)) |
| { |
| baton.found_rev_of_interest = FALSE; |
| baton.rev = rev; |
| baton.log_target_history_as_mergeinfo = log_target_history_as_mergeinfo; |
| baton.inner = callbacks->path_change_receiver; |
| baton.inner_baton = callbacks->path_change_receiver_baton; |
| |
| my_callbacks.path_change_receiver = interesting_merge; |
| my_callbacks.path_change_receiver_baton = &baton; |
| callbacks = &my_callbacks; |
| } |
| else |
| { |
| baton.found_rev_of_interest = TRUE; |
| } |
| |
| SVN_ERR(fill_log_entry(&log_entry, rev, fs, revprops, callbacks, pool)); |
| log_entry.has_children = has_children; |
| log_entry.subtractive_merge = subtractive_merge; |
| |
| /* Send the entry to the receiver, unless it is a redundant merged |
| revision. */ |
| if (baton.found_rev_of_interest) |
| { |
| apr_pool_t *scratch_pool; |
| |
| /* Is REV a merged revision we've already sent? */ |
| if (nested_merges && handling_merged_revision) |
| { |
| if (svn_bit_array__get(nested_merges, rev)) |
| { |
| /* We already sent REV. */ |
| return SVN_NO_ERROR; |
| } |
| else |
| { |
| /* NESTED_REVS needs to last across all the send_log, do_logs, |
| handle_merged_revisions() recursions, so use the pool it |
| was created in at the top of the recursion. */ |
| svn_bit_array__set(nested_merges, rev, TRUE); |
| } |
| } |
| |
| /* Pass a scratch pool to ensure no temporary state stored |
| by the receiver callback persists. */ |
| scratch_pool = svn_pool_create(pool); |
| SVN_ERR(callbacks->revision_receiver(callbacks->revision_receiver_baton, |
| &log_entry, scratch_pool)); |
| svn_pool_destroy(scratch_pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* This controls how many history objects we keep open. For any targets |
| over this number we have to open and close their histories as needed, |
| which is CPU intensive, but keeps us from using an unbounded amount of |
| memory. */ |
| #define MAX_OPEN_HISTORIES 32 |
| |
| /* Get the histories for PATHS, and store them in *HISTORIES. |
| |
| If IGNORE_MISSING_LOCATIONS is set, don't treat requests for bogus |
| repository locations as fatal -- just ignore them. */ |
| static svn_error_t * |
| get_path_histories(apr_array_header_t **histories, |
| svn_fs_t *fs, |
| const apr_array_header_t *paths, |
| svn_revnum_t hist_start, |
| svn_revnum_t hist_end, |
| svn_boolean_t strict_node_history, |
| svn_boolean_t ignore_missing_locations, |
| svn_repos_authz_func_t authz_read_func, |
| void *authz_read_baton, |
| apr_pool_t *pool) |
| { |
| svn_fs_root_t *root; |
| apr_pool_t *iterpool; |
| svn_error_t *err; |
| int i; |
| |
| /* Create a history object for each path so we can walk through |
| them all at the same time until we have all changes or LIMIT |
| is reached. |
| |
| There is some pool fun going on due to the fact that we have |
| to hold on to the old pool with the history before we can |
| get the next history. |
| */ |
| *histories = apr_array_make(pool, paths->nelts, |
| sizeof(struct path_info *)); |
| |
| SVN_ERR(svn_fs_revision_root(&root, fs, hist_end, pool)); |
| |
| iterpool = svn_pool_create(pool); |
| for (i = 0; i < paths->nelts; i++) |
| { |
| const char *this_path = APR_ARRAY_IDX(paths, i, const char *); |
| struct path_info *info = apr_palloc(pool, |
| sizeof(struct path_info)); |
| svn_pool_clear(iterpool); |
| |
| if (authz_read_func) |
| { |
| svn_boolean_t readable; |
| SVN_ERR(authz_read_func(&readable, root, this_path, |
| authz_read_baton, iterpool)); |
| if (! readable) |
| return svn_error_create(SVN_ERR_AUTHZ_UNREADABLE, NULL, NULL); |
| } |
| |
| info->path = svn_stringbuf_create(this_path, pool); |
| info->done = FALSE; |
| info->history_rev = hist_end; |
| info->first_time = TRUE; |
| |
| if (i < MAX_OPEN_HISTORIES) |
| { |
| err = svn_fs_node_history2(&info->hist, root, this_path, pool, |
| iterpool); |
| if (err |
| && ignore_missing_locations |
| && (err->apr_err == SVN_ERR_FS_NOT_FOUND || |
| err->apr_err == SVN_ERR_FS_NOT_DIRECTORY || |
| err->apr_err == SVN_ERR_FS_NO_SUCH_REVISION)) |
| { |
| svn_error_clear(err); |
| continue; |
| } |
| SVN_ERR(err); |
| info->newpool = svn_pool_create(pool); |
| info->oldpool = svn_pool_create(pool); |
| } |
| else |
| { |
| info->hist = NULL; |
| info->oldpool = NULL; |
| info->newpool = NULL; |
| } |
| |
| err = get_history(info, fs, |
| strict_node_history, |
| authz_read_func, authz_read_baton, |
| hist_start, pool, iterpool); |
| if (err |
| && ignore_missing_locations |
| && (err->apr_err == SVN_ERR_FS_NOT_FOUND || |
| err->apr_err == SVN_ERR_FS_NOT_DIRECTORY || |
| err->apr_err == SVN_ERR_FS_NO_SUCH_REVISION)) |
| { |
| svn_error_clear(err); |
| continue; |
| } |
| SVN_ERR(err); |
| APR_ARRAY_PUSH(*histories, struct path_info *) = info; |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Remove and return the first item from ARR. */ |
| static void * |
| array_pop_front(apr_array_header_t *arr) |
| { |
| void *item = arr->elts; |
| |
| if (apr_is_empty_array(arr)) |
| return NULL; |
| |
| arr->elts += arr->elt_size; |
| arr->nelts -= 1; |
| arr->nalloc -= 1; |
| return item; |
| } |
| |
| /* A struct which represents a single revision range, and the paths which |
| have mergeinfo in that range. */ |
| struct path_list_range |
| { |
| apr_array_header_t *paths; |
| svn_merge_range_t range; |
| |
| /* Is RANGE the result of a reverse merge? */ |
| svn_boolean_t reverse_merge; |
| }; |
| |
| /* A struct which represents "inverse mergeinfo", that is, instead of having |
| a path->revision_range_list mapping, which is the way mergeinfo is commonly |
| represented, this struct enables a revision_range_list,path tuple, where |
| the paths can be accessed by revision. */ |
| struct rangelist_path |
| { |
| svn_rangelist_t *rangelist; |
| const char *path; |
| }; |
| |
| /* Comparator function for combine_mergeinfo_path_lists(). Sorts |
| rangelist_path structs in increasing order based upon starting revision, |
| then ending revision of the first element in the rangelist. |
| |
| This does not sort rangelists based upon subsequent elements, only the |
| first range. We'll sort any subsequent ranges in the correct order |
| when they get bumped up to the front by removal of earlier ones, so we |
| don't really have to sort them here. See combine_mergeinfo_path_lists() |
| for details. */ |
| static int |
| compare_rangelist_paths(const void *a, const void *b) |
| { |
| struct rangelist_path *rpa = *((struct rangelist_path *const *) a); |
| struct rangelist_path *rpb = *((struct rangelist_path *const *) b); |
| svn_merge_range_t *mra = APR_ARRAY_IDX(rpa->rangelist, 0, |
| svn_merge_range_t *); |
| svn_merge_range_t *mrb = APR_ARRAY_IDX(rpb->rangelist, 0, |
| svn_merge_range_t *); |
| |
| if (mra->start < mrb->start) |
| return -1; |
| if (mra->start > mrb->start) |
| return 1; |
| if (mra->end < mrb->end) |
| return -1; |
| if (mra->end > mrb->end) |
| return 1; |
| |
| return 0; |
| } |
| |
| /* From MERGEINFO, return in *COMBINED_LIST, allocated in POOL, a list of |
| 'struct path_list_range's. This list represents the rangelists in |
| MERGEINFO and each path which has mergeinfo in that range. |
| If REVERSE_MERGE is true, then MERGEINFO represents mergeinfo removed |
| as the result of a reverse merge. */ |
| static svn_error_t * |
| combine_mergeinfo_path_lists(apr_array_header_t **combined_list, |
| svn_mergeinfo_t mergeinfo, |
| svn_boolean_t reverse_merge, |
| apr_pool_t *pool) |
| { |
| apr_hash_index_t *hi; |
| apr_array_header_t *rangelist_paths; |
| apr_pool_t *subpool = svn_pool_create(pool); |
| |
| /* Create a list of (revision range, path) tuples from MERGEINFO. */ |
| rangelist_paths = apr_array_make(subpool, apr_hash_count(mergeinfo), |
| sizeof(struct rangelist_path *)); |
| for (hi = apr_hash_first(subpool, mergeinfo); hi; |
| hi = apr_hash_next(hi)) |
| { |
| int i; |
| struct rangelist_path *rp = apr_palloc(subpool, sizeof(*rp)); |
| |
| rp->path = apr_hash_this_key(hi); |
| rp->rangelist = apr_hash_this_val(hi); |
| APR_ARRAY_PUSH(rangelist_paths, struct rangelist_path *) = rp; |
| |
| /* We need to make local copies of the rangelist, since we will be |
| modifying it, below. */ |
| rp->rangelist = svn_rangelist_dup(rp->rangelist, subpool); |
| |
| /* Make all of the rangelists inclusive, both start and end. */ |
| for (i = 0; i < rp->rangelist->nelts; i++) |
| APR_ARRAY_IDX(rp->rangelist, i, svn_merge_range_t *)->start += 1; |
| } |
| |
| /* Loop over the (revision range, path) tuples, chopping them into |
| (revision range, paths) tuples, and appending those to the output |
| list. */ |
| if (! *combined_list) |
| *combined_list = apr_array_make(pool, 0, sizeof(struct path_list_range *)); |
| |
| while (rangelist_paths->nelts > 1) |
| { |
| svn_revnum_t youngest, next_youngest, tail, youngest_end; |
| struct path_list_range *plr; |
| struct rangelist_path *rp; |
| int num_revs; |
| int i; |
| |
| /* First, sort the list such that the start revision of the first |
| revision arrays are sorted. */ |
| svn_sort__array(rangelist_paths, compare_rangelist_paths); |
| |
| /* Next, find the number of revision ranges which start with the same |
| revision. */ |
| rp = APR_ARRAY_IDX(rangelist_paths, 0, struct rangelist_path *); |
| youngest = |
| APR_ARRAY_IDX(rp->rangelist, 0, struct svn_merge_range_t *)->start; |
| next_youngest = youngest; |
| for (num_revs = 1; next_youngest == youngest; num_revs++) |
| { |
| if (num_revs == rangelist_paths->nelts) |
| { |
| num_revs += 1; |
| break; |
| } |
| rp = APR_ARRAY_IDX(rangelist_paths, num_revs, |
| struct rangelist_path *); |
| next_youngest = APR_ARRAY_IDX(rp->rangelist, 0, |
| struct svn_merge_range_t *)->start; |
| } |
| num_revs -= 1; |
| |
| /* The start of the new range will be YOUNGEST, and we now find the end |
| of the new range, which should be either one less than the next |
| earliest start of a rangelist, or the end of the first rangelist. */ |
| youngest_end = |
| APR_ARRAY_IDX(APR_ARRAY_IDX(rangelist_paths, 0, |
| struct rangelist_path *)->rangelist, |
| 0, svn_merge_range_t *)->end; |
| if ( (next_youngest == youngest) || (youngest_end < next_youngest) ) |
| tail = youngest_end; |
| else |
| tail = next_youngest - 1; |
| |
| /* Insert the (earliest, tail) tuple into the output list, along with |
| a list of paths which match it. */ |
| plr = apr_palloc(pool, sizeof(*plr)); |
| plr->reverse_merge = reverse_merge; |
| plr->range.start = youngest; |
| plr->range.end = tail; |
| plr->paths = apr_array_make(pool, num_revs, sizeof(const char *)); |
| for (i = 0; i < num_revs; i++) |
| APR_ARRAY_PUSH(plr->paths, const char *) = |
| APR_ARRAY_IDX(rangelist_paths, i, struct rangelist_path *)->path; |
| APR_ARRAY_PUSH(*combined_list, struct path_list_range *) = plr; |
| |
| /* Now, check to see which (rangelist path) combinations we can remove, |
| and do so. */ |
| for (i = 0; i < num_revs; i++) |
| { |
| svn_merge_range_t *range; |
| rp = APR_ARRAY_IDX(rangelist_paths, i, struct rangelist_path *); |
| range = APR_ARRAY_IDX(rp->rangelist, 0, svn_merge_range_t *); |
| |
| /* Set the start of the range to beyond the end of the range we |
| just built. If the range is now "inverted", we can get pop it |
| off the list. */ |
| range->start = tail + 1; |
| if (range->start > range->end) |
| { |
| if (rp->rangelist->nelts == 1) |
| { |
| /* The range is the only on its list, so we should remove |
| the entire rangelist_path, adjusting our loop control |
| variables appropriately. */ |
| array_pop_front(rangelist_paths); |
| i--; |
| num_revs--; |
| } |
| else |
| { |
| /* We have more than one range on the list, so just remove |
| the first one. */ |
| array_pop_front(rp->rangelist); |
| } |
| } |
| } |
| } |
| |
| /* Finally, add the last remaining (revision range, path) to the output |
| list. */ |
| if (rangelist_paths->nelts > 0) |
| { |
| struct rangelist_path *first_rp = |
| APR_ARRAY_IDX(rangelist_paths, 0, struct rangelist_path *); |
| while (first_rp->rangelist->nelts > 0) |
| { |
| struct path_list_range *plr = apr_palloc(pool, sizeof(*plr)); |
| |
| plr->reverse_merge = reverse_merge; |
| plr->paths = apr_array_make(pool, 1, sizeof(const char *)); |
| APR_ARRAY_PUSH(plr->paths, const char *) = first_rp->path; |
| plr->range = *APR_ARRAY_IDX(first_rp->rangelist, 0, |
| svn_merge_range_t *); |
| array_pop_front(first_rp->rangelist); |
| APR_ARRAY_PUSH(*combined_list, struct path_list_range *) = plr; |
| } |
| } |
| |
| svn_pool_destroy(subpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Pity that C is so ... linear. */ |
| static svn_error_t * |
| do_logs(svn_fs_t *fs, |
| const apr_array_header_t *paths, |
| svn_mergeinfo_t log_target_history_as_mergeinfo, |
| svn_mergeinfo_t processed, |
| svn_bit_array__t *nested_merges, |
| svn_revnum_t hist_start, |
| svn_revnum_t hist_end, |
| int limit, |
| svn_boolean_t strict_node_history, |
| svn_boolean_t include_merged_revisions, |
| svn_boolean_t handling_merged_revisions, |
| svn_boolean_t subtractive_merge, |
| svn_boolean_t ignore_missing_locations, |
| const apr_array_header_t *revprops, |
| svn_boolean_t descending_order, |
| log_callbacks_t *callbacks, |
| apr_pool_t *pool); |
| |
| /* Comparator function for handle_merged_revisions(). Sorts path_list_range |
| structs in increasing order based on the struct's RANGE.START revision, |
| then RANGE.END revision. */ |
| static int |
| compare_path_list_range(const void *a, const void *b) |
| { |
| struct path_list_range *plr_a = *((struct path_list_range *const *) a); |
| struct path_list_range *plr_b = *((struct path_list_range *const *) b); |
| |
| if (plr_a->range.start < plr_b->range.start) |
| return -1; |
| if (plr_a->range.start > plr_b->range.start) |
| return 1; |
| if (plr_a->range.end < plr_b->range.end) |
| return -1; |
| if (plr_a->range.end > plr_b->range.end) |
| return 1; |
| |
| return 0; |
| } |
| |
| /* Examine the ADDED_MERGEINFO and DELETED_MERGEINFO for revision REV in FS |
| (as collected by examining paths of interest to a log operation), and |
| determine which revisions to report as having been merged or reverse-merged |
| via the commit resulting in REV. |
| |
| Silently ignore some failures to find the revisions mentioned in the |
| added/deleted mergeinfos, as might happen if there is invalid mergeinfo. |
| |
| Other parameters are as described by do_logs(), around which this |
| is a recursion wrapper. */ |
| static svn_error_t * |
| handle_merged_revisions(svn_revnum_t rev, |
| svn_fs_t *fs, |
| svn_mergeinfo_t log_target_history_as_mergeinfo, |
| svn_bit_array__t *nested_merges, |
| svn_mergeinfo_t processed, |
| svn_mergeinfo_t added_mergeinfo, |
| svn_mergeinfo_t deleted_mergeinfo, |
| svn_boolean_t strict_node_history, |
| const apr_array_header_t *revprops, |
| log_callbacks_t *callbacks, |
| apr_pool_t *pool) |
| { |
| apr_array_header_t *combined_list = NULL; |
| svn_repos_log_entry_t empty_log_entry = { 0 }; |
| apr_pool_t *iterpool; |
| int i; |
| |
| if (apr_hash_count(added_mergeinfo) == 0 |
| && apr_hash_count(deleted_mergeinfo) == 0) |
| return SVN_NO_ERROR; |
| |
| if (apr_hash_count(added_mergeinfo)) |
| SVN_ERR(combine_mergeinfo_path_lists(&combined_list, added_mergeinfo, |
| FALSE, pool)); |
| |
| if (apr_hash_count(deleted_mergeinfo)) |
| SVN_ERR(combine_mergeinfo_path_lists(&combined_list, deleted_mergeinfo, |
| TRUE, pool)); |
| |
| SVN_ERR_ASSERT(combined_list != NULL); |
| svn_sort__array(combined_list, compare_path_list_range); |
| |
| /* Because the combined_lists are ordered youngest to oldest, |
| iterate over them in reverse. */ |
| iterpool = svn_pool_create(pool); |
| for (i = combined_list->nelts - 1; i >= 0; i--) |
| { |
| struct path_list_range *pl_range |
| = APR_ARRAY_IDX(combined_list, i, struct path_list_range *); |
| |
| svn_pool_clear(iterpool); |
| SVN_ERR(do_logs(fs, pl_range->paths, log_target_history_as_mergeinfo, |
| processed, nested_merges, |
| pl_range->range.start, pl_range->range.end, 0, |
| strict_node_history, |
| TRUE, pl_range->reverse_merge, TRUE, TRUE, |
| revprops, TRUE, callbacks, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| |
| /* Send the empty revision. */ |
| empty_log_entry.revision = SVN_INVALID_REVNUM; |
| return (callbacks->revision_receiver)(callbacks->revision_receiver_baton, |
| &empty_log_entry, pool); |
| } |
| |
| /* This is used by do_logs to differentiate between forward and |
| reverse merges. */ |
| struct added_deleted_mergeinfo |
| { |
| svn_mergeinfo_t added_mergeinfo; |
| svn_mergeinfo_t deleted_mergeinfo; |
| }; |
| |
| /* Reduce the search range PATHS, HIST_START, HIST_END by removing |
| parts already covered by PROCESSED. If reduction is possible |
| elements may be removed from PATHS and *START_REDUCED and |
| *END_REDUCED may be set to a narrower range. */ |
| static svn_error_t * |
| reduce_search(apr_array_header_t *paths, |
| svn_revnum_t *hist_start, |
| svn_revnum_t *hist_end, |
| svn_mergeinfo_t processed, |
| apr_pool_t *scratch_pool) |
| { |
| /* We add 1 to end to compensate for store_search */ |
| svn_revnum_t start = *hist_start <= *hist_end ? *hist_start : *hist_end; |
| svn_revnum_t end = *hist_start <= *hist_end ? *hist_end + 1 : *hist_start + 1; |
| int i; |
| |
| for (i = 0; i < paths->nelts; ++i) |
| { |
| const char *path = APR_ARRAY_IDX(paths, i, const char *); |
| svn_rangelist_t *ranges = svn_hash_gets(processed, path); |
| int j; |
| |
| if (!ranges) |
| continue; |
| |
| /* ranges is ordered, could we use some sort of binary search |
| rather than iterating? */ |
| for (j = 0; j < ranges->nelts; ++j) |
| { |
| svn_merge_range_t *range = APR_ARRAY_IDX(ranges, j, |
| svn_merge_range_t *); |
| if (range->start <= start && range->end >= end) |
| { |
| for (j = i; j < paths->nelts - 1; ++j) |
| APR_ARRAY_IDX(paths, j, const char *) |
| = APR_ARRAY_IDX(paths, j + 1, const char *); |
| |
| --paths->nelts; |
| --i; |
| break; |
| } |
| |
| /* If there is only one path then we also check for a |
| partial overlap rather than the full overlap above, and |
| reduce the [hist_start, hist_end] range rather than |
| dropping the path. */ |
| if (paths->nelts == 1) |
| { |
| if (range->start <= start && range->end > start) |
| { |
| if (start == *hist_start) |
| *hist_start = range->end - 1; |
| else |
| *hist_end = range->end - 1; |
| break; |
| } |
| if (range->start < end && range->end >= end) |
| { |
| if (start == *hist_start) |
| *hist_end = range->start; |
| else |
| *hist_start = range->start; |
| break; |
| } |
| } |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Extend PROCESSED to cover PATHS from HIST_START to HIST_END */ |
| static svn_error_t * |
| store_search(svn_mergeinfo_t processed, |
| const apr_array_header_t *paths, |
| svn_revnum_t hist_start, |
| svn_revnum_t hist_end, |
| apr_pool_t *scratch_pool) |
| { |
| /* We add 1 to end so that we can use the mergeinfo API to handle |
| singe revisions where HIST_START is equal to HIST_END. */ |
| svn_revnum_t start = hist_start <= hist_end ? hist_start : hist_end; |
| svn_revnum_t end = hist_start <= hist_end ? hist_end + 1 : hist_start + 1; |
| svn_mergeinfo_t mergeinfo = svn_hash__make(scratch_pool); |
| apr_pool_t *processed_pool = apr_hash_pool_get(processed); |
| int i; |
| |
| for (i = 0; i < paths->nelts; ++i) |
| { |
| const char *path = APR_ARRAY_IDX(paths, i, const char *); |
| svn_rangelist_t *ranges = apr_array_make(processed_pool, 1, |
| sizeof(svn_merge_range_t*)); |
| svn_merge_range_t *range = apr_palloc(processed_pool, sizeof(*range)); |
| |
| range->start = start; |
| range->end = end; |
| range->inheritable = TRUE; |
| APR_ARRAY_PUSH(ranges, svn_merge_range_t *) = range; |
| svn_hash_sets(mergeinfo, apr_pstrdup(processed_pool, path), ranges); |
| } |
| SVN_ERR(svn_mergeinfo_merge2(processed, mergeinfo, |
| apr_hash_pool_get(processed), scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Find logs for PATHS from HIST_START to HIST_END in FS, and invoke the |
| CALLBACKS on them. If DESCENDING_ORDER is TRUE, send the logs back as |
| we find them, else buffer the logs and send them back in youngest->oldest |
| order. |
| |
| If IGNORE_MISSING_LOCATIONS is set, don't treat requests for bogus |
| repository locations as fatal -- just ignore them. |
| |
| If LOG_TARGET_HISTORY_AS_MERGEINFO is not NULL then it contains mergeinfo |
| representing the history of PATHS between HIST_START and HIST_END. |
| |
| If HANDLING_MERGED_REVISIONS is TRUE then this is a recursive call for |
| merged revisions, see INCLUDE_MERGED_REVISIONS argument to |
| svn_repos_get_logs5(). If SUBTRACTIVE_MERGE is true, then this is a |
| recursive call for reverse merged revisions. |
| |
| If NESTED_MERGES is not NULL then it is a hash of revisions (svn_revnum_t * |
| mapped to svn_revnum_t *) for logs that were previously sent. On the first |
| call to do_logs it should always be NULL. If INCLUDE_MERGED_REVISIONS is |
| TRUE, then NESTED_MERGES will be created on the first call to do_logs, |
| allocated in POOL. It is then shared across |
| do_logs()/send_logs()/handle_merge_revisions() recursions, see also the |
| argument of the same name in send_logs(). |
| |
| PROCESSED is a mergeinfo hash that represents the paths and |
| revisions that have already been searched. Allocated like |
| NESTED_MERGES above. |
| |
| All other parameters are the same as svn_repos_get_logs5(). |
| */ |
| static svn_error_t * |
| do_logs(svn_fs_t *fs, |
| const apr_array_header_t *paths, |
| svn_mergeinfo_t log_target_history_as_mergeinfo, |
| svn_mergeinfo_t processed, |
| svn_bit_array__t *nested_merges, |
| svn_revnum_t hist_start, |
| svn_revnum_t hist_end, |
| int limit, |
| svn_boolean_t strict_node_history, |
| svn_boolean_t include_merged_revisions, |
| svn_boolean_t subtractive_merge, |
| svn_boolean_t handling_merged_revisions, |
| svn_boolean_t ignore_missing_locations, |
| const apr_array_header_t *revprops, |
| svn_boolean_t descending_order, |
| log_callbacks_t *callbacks, |
| apr_pool_t *pool) |
| { |
| apr_pool_t *iterpool, *iterpool2; |
| apr_pool_t *subpool = NULL; |
| apr_array_header_t *revs = NULL; |
| apr_hash_t *rev_mergeinfo = NULL; |
| svn_revnum_t current; |
| apr_array_header_t *histories; |
| svn_boolean_t any_histories_left = TRUE; |
| int send_count = 0; |
| int i; |
| |
| if (processed) |
| { |
| /* Casting away const. This only happens on recursive calls when |
| it is known to be safe because we allocated paths. */ |
| SVN_ERR(reduce_search((apr_array_header_t *)paths, &hist_start, &hist_end, |
| processed, pool)); |
| } |
| |
| if (!paths->nelts) |
| return SVN_NO_ERROR; |
| |
| if (processed) |
| SVN_ERR(store_search(processed, paths, hist_start, hist_end, pool)); |
| |
| /* We have a list of paths and a revision range. But we don't care |
| about all the revisions in the range -- only the ones in which |
| one of our paths was changed. So let's go figure out which |
| revisions contain real changes to at least one of our paths. */ |
| SVN_ERR(get_path_histories(&histories, fs, paths, hist_start, hist_end, |
| strict_node_history, ignore_missing_locations, |
| callbacks->authz_read_func, |
| callbacks->authz_read_baton, pool)); |
| |
| /* Loop through all the revisions in the range and add any |
| where a path was changed to the array, or if they wanted |
| history in reverse order just send it to them right away. */ |
| iterpool = svn_pool_create(pool); |
| iterpool2 = svn_pool_create(pool); |
| for (current = hist_end; |
| any_histories_left; |
| current = next_history_rev(histories)) |
| { |
| svn_boolean_t changed = FALSE; |
| any_histories_left = FALSE; |
| svn_pool_clear(iterpool); |
| |
| for (i = 0; i < histories->nelts; i++) |
| { |
| struct path_info *info = APR_ARRAY_IDX(histories, i, |
| struct path_info *); |
| |
| svn_pool_clear(iterpool2); |
| |
| /* Check history for this path in current rev. */ |
| SVN_ERR(check_history(&changed, info, fs, current, |
| strict_node_history, |
| callbacks->authz_read_func, |
| callbacks->authz_read_baton, |
| hist_start, pool, iterpool2)); |
| if (! info->done) |
| any_histories_left = TRUE; |
| } |
| |
| svn_pool_clear(iterpool2); |
| |
| /* If any of the paths changed in this rev then add or send it. */ |
| if (changed) |
| { |
| svn_mergeinfo_t added_mergeinfo = NULL; |
| svn_mergeinfo_t deleted_mergeinfo = NULL; |
| svn_boolean_t has_children = FALSE; |
| |
| /* If we're including merged revisions, we need to calculate |
| the mergeinfo deltas committed in this revision to our |
| various paths. */ |
| if (include_merged_revisions) |
| { |
| apr_array_header_t *cur_paths = |
| apr_array_make(iterpool, paths->nelts, sizeof(const char *)); |
| |
| /* Get the current paths of our history objects so we can |
| query mergeinfo. */ |
| /* ### TODO: Should this be ignoring depleted history items? */ |
| for (i = 0; i < histories->nelts; i++) |
| { |
| struct path_info *info = APR_ARRAY_IDX(histories, i, |
| struct path_info *); |
| APR_ARRAY_PUSH(cur_paths, const char *) = info->path->data; |
| } |
| SVN_ERR(get_combined_mergeinfo_changes(&added_mergeinfo, |
| &deleted_mergeinfo, |
| fs, cur_paths, |
| current, |
| iterpool, iterpool)); |
| has_children = (apr_hash_count(added_mergeinfo) > 0 |
| || apr_hash_count(deleted_mergeinfo) > 0); |
| } |
| |
| /* If our caller wants logs in descending order, we can send |
| 'em now (because that's the order we're crawling history |
| in anyway). */ |
| if (descending_order) |
| { |
| SVN_ERR(send_log(current, fs, |
| log_target_history_as_mergeinfo, nested_merges, |
| subtractive_merge, handling_merged_revisions, |
| revprops, has_children, callbacks, iterpool)); |
| |
| if (has_children) /* Implies include_merged_revisions == TRUE */ |
| { |
| if (!nested_merges) |
| { |
| /* We're at the start of the recursion stack, create a |
| single hash to be shared across all of the merged |
| recursions so we can track and squelch duplicates. */ |
| subpool = svn_pool_create(pool); |
| nested_merges = svn_bit_array__create(hist_end, subpool); |
| processed = svn_hash__make(subpool); |
| } |
| |
| SVN_ERR(handle_merged_revisions( |
| current, fs, |
| log_target_history_as_mergeinfo, nested_merges, |
| processed, |
| added_mergeinfo, deleted_mergeinfo, |
| strict_node_history, |
| revprops, |
| callbacks, |
| iterpool)); |
| } |
| if (limit && ++send_count >= limit) |
| break; |
| } |
| /* Otherwise, the caller wanted logs in ascending order, so |
| we have to buffer up a list of revs and (if doing |
| mergeinfo) a hash of related mergeinfo deltas, and |
| process them later. */ |
| else |
| { |
| if (! revs) |
| revs = apr_array_make(pool, 64, sizeof(svn_revnum_t)); |
| APR_ARRAY_PUSH(revs, svn_revnum_t) = current; |
| |
| if (added_mergeinfo || deleted_mergeinfo) |
| { |
| svn_revnum_t *cur_rev = |
| apr_pmemdup(pool, ¤t, sizeof(*cur_rev)); |
| struct added_deleted_mergeinfo *add_and_del_mergeinfo = |
| apr_palloc(pool, sizeof(*add_and_del_mergeinfo)); |
| |
| /* If we have added or deleted mergeinfo, both are non-null */ |
| SVN_ERR_ASSERT(added_mergeinfo && deleted_mergeinfo); |
| add_and_del_mergeinfo->added_mergeinfo = |
| svn_mergeinfo_dup(added_mergeinfo, pool); |
| add_and_del_mergeinfo->deleted_mergeinfo = |
| svn_mergeinfo_dup(deleted_mergeinfo, pool); |
| |
| if (! rev_mergeinfo) |
| rev_mergeinfo = svn_hash__make(pool); |
| apr_hash_set(rev_mergeinfo, cur_rev, sizeof(*cur_rev), |
| add_and_del_mergeinfo); |
| } |
| } |
| } |
| } |
| svn_pool_destroy(iterpool2); |
| svn_pool_destroy(iterpool); |
| |
| if (subpool) |
| { |
| nested_merges = NULL; |
| svn_pool_destroy(subpool); |
| } |
| |
| if (revs) |
| { |
| /* Work loop for processing the revisions we found since they wanted |
| history in forward order. */ |
| iterpool = svn_pool_create(pool); |
| for (i = 0; i < revs->nelts; ++i) |
| { |
| svn_mergeinfo_t added_mergeinfo; |
| svn_mergeinfo_t deleted_mergeinfo; |
| svn_boolean_t has_children = FALSE; |
| |
| svn_pool_clear(iterpool); |
| current = APR_ARRAY_IDX(revs, revs->nelts - i - 1, svn_revnum_t); |
| |
| /* If we've got a hash of revision mergeinfo (which can only |
| happen if INCLUDE_MERGED_REVISIONS was set), we check to |
| see if this revision is one which merged in other |
| revisions we need to handle recursively. */ |
| if (rev_mergeinfo) |
| { |
| struct added_deleted_mergeinfo *add_and_del_mergeinfo = |
| apr_hash_get(rev_mergeinfo, ¤t, sizeof(current)); |
| added_mergeinfo = add_and_del_mergeinfo->added_mergeinfo; |
| deleted_mergeinfo = add_and_del_mergeinfo->deleted_mergeinfo; |
| has_children = (apr_hash_count(added_mergeinfo) > 0 |
| || apr_hash_count(deleted_mergeinfo) > 0); |
| } |
| |
| SVN_ERR(send_log(current, fs, |
| log_target_history_as_mergeinfo, nested_merges, |
| subtractive_merge, handling_merged_revisions, |
| revprops, has_children, callbacks, iterpool)); |
| if (has_children) |
| { |
| if (!nested_merges) |
| { |
| subpool = svn_pool_create(pool); |
| nested_merges = svn_bit_array__create(current, subpool); |
| } |
| |
| SVN_ERR(handle_merged_revisions(current, fs, |
| log_target_history_as_mergeinfo, |
| nested_merges, |
| processed, |
| added_mergeinfo, |
| deleted_mergeinfo, |
| strict_node_history, |
| revprops, callbacks, |
| iterpool)); |
| } |
| if (limit && i + 1 >= limit) |
| break; |
| } |
| svn_pool_destroy(iterpool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| struct location_segment_baton |
| { |
| apr_array_header_t *history_segments; |
| apr_pool_t *pool; |
| }; |
| |
| /* svn_location_segment_receiver_t implementation for svn_repos_get_logs5. */ |
| static svn_error_t * |
| location_segment_receiver(svn_location_segment_t *segment, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| struct location_segment_baton *b = baton; |
| |
| APR_ARRAY_PUSH(b->history_segments, svn_location_segment_t *) = |
| svn_location_segment_dup(segment, b->pool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Populate *PATHS_HISTORY_MERGEINFO with mergeinfo representing the combined |
| history of each path in PATHS between START_REV and END_REV in REPOS's |
| filesystem. START_REV and END_REV must be valid revisions. RESULT_POOL |
| is used to allocate *PATHS_HISTORY_MERGEINFO, SCRATCH_POOL is used for all |
| other (temporary) allocations. Other parameters are the same as |
| svn_repos_get_logs5(). */ |
| static svn_error_t * |
| get_paths_history_as_mergeinfo(svn_mergeinfo_t *paths_history_mergeinfo, |
| svn_repos_t *repos, |
| const apr_array_header_t *paths, |
| svn_revnum_t start_rev, |
| svn_revnum_t end_rev, |
| svn_repos_authz_func_t authz_read_func, |
| void *authz_read_baton, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| int i; |
| svn_mergeinfo_t path_history_mergeinfo; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| |
| SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(start_rev)); |
| SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(end_rev)); |
| |
| /* Ensure START_REV is the youngest revision, as required by |
| svn_repos_node_location_segments, for which this is an iterative |
| wrapper. */ |
| if (start_rev < end_rev) |
| { |
| svn_revnum_t tmp_rev = start_rev; |
| start_rev = end_rev; |
| end_rev = tmp_rev; |
| } |
| |
| *paths_history_mergeinfo = svn_hash__make(result_pool); |
| |
| for (i = 0; i < paths->nelts; i++) |
| { |
| const char *this_path = APR_ARRAY_IDX(paths, i, const char *); |
| struct location_segment_baton loc_seg_baton; |
| |
| svn_pool_clear(iterpool); |
| loc_seg_baton.pool = scratch_pool; |
| loc_seg_baton.history_segments = |
| apr_array_make(iterpool, 4, sizeof(svn_location_segment_t *)); |
| |
| SVN_ERR(svn_repos_node_location_segments(repos, this_path, start_rev, |
| start_rev, end_rev, |
| location_segment_receiver, |
| &loc_seg_baton, |
| authz_read_func, |
| authz_read_baton, |
| iterpool)); |
| |
| SVN_ERR(svn_mergeinfo__mergeinfo_from_segments( |
| &path_history_mergeinfo, loc_seg_baton.history_segments, iterpool)); |
| SVN_ERR(svn_mergeinfo_merge2(*paths_history_mergeinfo, |
| svn_mergeinfo_dup(path_history_mergeinfo, |
| result_pool), |
| result_pool, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_repos_get_logs5(svn_repos_t *repos, |
| const apr_array_header_t *paths, |
| svn_revnum_t start, |
| svn_revnum_t end, |
| int limit, |
| svn_boolean_t strict_node_history, |
| svn_boolean_t include_merged_revisions, |
| const apr_array_header_t *revprops, |
| svn_repos_authz_func_t authz_read_func, |
| void *authz_read_baton, |
| svn_repos_path_change_receiver_t path_change_receiver, |
| void *path_change_receiver_baton, |
| svn_repos_log_entry_receiver_t revision_receiver, |
| void *revision_receiver_baton, |
| apr_pool_t *scratch_pool) |
| { |
| svn_revnum_t head = SVN_INVALID_REVNUM; |
| svn_fs_t *fs = repos->fs; |
| svn_boolean_t descending_order; |
| svn_mergeinfo_t paths_history_mergeinfo = NULL; |
| log_callbacks_t callbacks; |
| |
| callbacks.path_change_receiver = path_change_receiver; |
| callbacks.path_change_receiver_baton = path_change_receiver_baton; |
| callbacks.revision_receiver = revision_receiver; |
| callbacks.revision_receiver_baton = revision_receiver_baton; |
| callbacks.authz_read_func = authz_read_func; |
| callbacks.authz_read_baton = authz_read_baton; |
| |
| if (revprops) |
| { |
| int i; |
| apr_array_header_t *new_revprops |
| = apr_array_make(scratch_pool, revprops->nelts, |
| sizeof(svn_string_t *)); |
| |
| for (i = 0; i < revprops->nelts; ++i) |
| APR_ARRAY_PUSH(new_revprops, svn_string_t *) |
| = svn_string_create(APR_ARRAY_IDX(revprops, i, const char *), |
| scratch_pool); |
| |
| revprops = new_revprops; |
| } |
| |
| /* Make sure we catch up on the latest revprop changes. This is the only |
| * time we will refresh the revprop data in this query. */ |
| SVN_ERR(svn_fs_refresh_revision_props(fs, scratch_pool)); |
| |
| /* Setup log range. */ |
| SVN_ERR(svn_fs_youngest_rev(&head, fs, scratch_pool)); |
| |
| if (! SVN_IS_VALID_REVNUM(start)) |
| start = head; |
| |
| if (! SVN_IS_VALID_REVNUM(end)) |
| end = head; |
| |
| /* Check that revisions are sane before ever invoking receiver. */ |
| if (start > head) |
| return svn_error_createf |
| (SVN_ERR_FS_NO_SUCH_REVISION, 0, |
| _("No such revision %ld"), start); |
| if (end > head) |
| return svn_error_createf |
| (SVN_ERR_FS_NO_SUCH_REVISION, 0, |
| _("No such revision %ld"), end); |
| |
| /* Ensure a youngest-to-oldest revision crawl ordering using our |
| (possibly sanitized) range values. */ |
| descending_order = start >= end; |
| if (descending_order) |
| { |
| svn_revnum_t tmp_rev = start; |
| start = end; |
| end = tmp_rev; |
| } |
| |
| if (! paths) |
| paths = apr_array_make(scratch_pool, 0, sizeof(const char *)); |
| |
| /* If we're not including merged revisions, and we were given no |
| paths or a single empty (or "/") path, then we can bypass a bunch |
| of complexity because we already know in which revisions the root |
| directory was changed -- all of them. */ |
| if ((! include_merged_revisions) |
| && ((! paths->nelts) |
| || ((paths->nelts == 1) |
| && (svn_path_is_empty(APR_ARRAY_IDX(paths, 0, const char *)) |
| || (strcmp(APR_ARRAY_IDX(paths, 0, const char *), |
| "/") == 0))))) |
| { |
| apr_uint64_t send_count = 0; |
| int i; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| |
| /* If we are provided an authz callback function, use it to |
| verify that the user has read access to the root path in the |
| first of our revisions. |
| |
| ### FIXME: Strictly speaking, we should be checking this |
| ### access in every revision along the line. But currently, |
| ### there are no known authz implementations which concern |
| ### themselves with per-revision access. */ |
| if (authz_read_func) |
| { |
| svn_boolean_t readable; |
| svn_fs_root_t *rev_root; |
| |
| SVN_ERR(svn_fs_revision_root(&rev_root, fs, |
| descending_order ? end : start, |
| scratch_pool)); |
| SVN_ERR(authz_read_func(&readable, rev_root, "", |
| authz_read_baton, scratch_pool)); |
| if (! readable) |
| return svn_error_create(SVN_ERR_AUTHZ_UNREADABLE, NULL, NULL); |
| } |
| |
| send_count = end - start + 1; |
| if (limit > 0 && send_count > limit) |
| send_count = limit; |
| for (i = 0; i < send_count; ++i) |
| { |
| svn_revnum_t rev; |
| |
| svn_pool_clear(iterpool); |
| |
| if (descending_order) |
| rev = end - i; |
| else |
| rev = start + i; |
| SVN_ERR(send_log(rev, fs, NULL, NULL, |
| FALSE, FALSE, revprops, FALSE, |
| &callbacks, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* If we are including merged revisions, then create mergeinfo that |
| represents all of PATHS' history between START and END. We will use |
| this later to squelch duplicate log revisions that might exist in |
| both natural history and merged-in history. See |
| https://issues.apache.org/jira/browse/SVN-3650#desc5 */ |
| if (include_merged_revisions) |
| { |
| apr_pool_t *subpool = svn_pool_create(scratch_pool); |
| |
| SVN_ERR(get_paths_history_as_mergeinfo(&paths_history_mergeinfo, |
| repos, paths, start, end, |
| authz_read_func, |
| authz_read_baton, |
| scratch_pool, subpool)); |
| svn_pool_destroy(subpool); |
| } |
| |
| return do_logs(repos->fs, paths, paths_history_mergeinfo, NULL, NULL, |
| start, end, limit, strict_node_history, |
| include_merged_revisions, FALSE, FALSE, FALSE, |
| revprops, descending_order, &callbacks, scratch_pool); |
| } |