diff options
Diffstat (limited to 'subversion/libsvn_repos')
-rw-r--r-- | subversion/libsvn_repos/authz.c | 1075 | ||||
-rw-r--r-- | subversion/libsvn_repos/commit.c | 1381 | ||||
-rw-r--r-- | subversion/libsvn_repos/delta.c | 1074 | ||||
-rw-r--r-- | subversion/libsvn_repos/deprecated.c | 1017 | ||||
-rw-r--r-- | subversion/libsvn_repos/dump.c | 1503 | ||||
-rw-r--r-- | subversion/libsvn_repos/fs-wrap.c | 844 | ||||
-rw-r--r-- | subversion/libsvn_repos/hooks.c | 890 | ||||
-rw-r--r-- | subversion/libsvn_repos/load-fs-vtable.c | 1140 | ||||
-rw-r--r-- | subversion/libsvn_repos/load.c | 684 | ||||
-rw-r--r-- | subversion/libsvn_repos/log.c | 2369 | ||||
-rw-r--r-- | subversion/libsvn_repos/node_tree.c | 431 | ||||
-rw-r--r-- | subversion/libsvn_repos/notify.c | 44 | ||||
-rw-r--r-- | subversion/libsvn_repos/replay.c | 1591 | ||||
-rw-r--r-- | subversion/libsvn_repos/reporter.c | 1610 | ||||
-rw-r--r-- | subversion/libsvn_repos/repos.c | 2132 | ||||
-rw-r--r-- | subversion/libsvn_repos/repos.h | 425 | ||||
-rw-r--r-- | subversion/libsvn_repos/rev_hunt.c | 1699 |
17 files changed, 19909 insertions, 0 deletions
diff --git a/subversion/libsvn_repos/authz.c b/subversion/libsvn_repos/authz.c new file mode 100644 index 0000000..af4a1f2 --- /dev/null +++ b/subversion/libsvn_repos/authz.c @@ -0,0 +1,1075 @@ +/* authz.c : path-based access control + * + * ==================================================================== + * 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. + * ==================================================================== + */ + + +/*** Includes. ***/ + +#include <apr_pools.h> +#include <apr_file_io.h> + +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_repos.h" +#include "svn_config.h" +#include "svn_ctype.h" +#include "private/svn_fspath.h" +#include "repos.h" + + +/*** Structures. ***/ + +/* Information for the config enumerators called during authz + lookup. */ +struct authz_lookup_baton { + /* The authz configuration. */ + svn_config_t *config; + + /* The user to authorize. */ + const char *user; + + /* Explicitly granted rights. */ + svn_repos_authz_access_t allow; + /* Explicitly denied rights. */ + svn_repos_authz_access_t deny; + + /* The rights required by the caller of the lookup. */ + svn_repos_authz_access_t required_access; + + /* The following are used exclusively in recursive lookups. */ + + /* The path in the repository (an fspath) to authorize. */ + const char *repos_path; + /* repos_path prefixed by the repository name and a colon. */ + const char *qualified_repos_path; + + /* Whether, at the end of a recursive lookup, access is granted. */ + svn_boolean_t access; +}; + +/* Information for the config enumeration functions called during the + validation process. */ +struct authz_validate_baton { + svn_config_t *config; /* The configuration file being validated. */ + svn_error_t *err; /* The error being thrown out of the + enumerator, if any. */ +}; + +/* Currently this structure is just a wrapper around a + svn_config_t. */ +struct svn_authz_t +{ + svn_config_t *cfg; +}; + + + +/*** Checking access. ***/ + +/* Determine whether the REQUIRED access is granted given what authz + * to ALLOW or DENY. Return TRUE if the REQUIRED access is + * granted. + * + * Access is granted either when no required access is explicitly + * denied (implicit grant), or when the required access is explicitly + * granted, overriding any denials. + */ +static svn_boolean_t +authz_access_is_granted(svn_repos_authz_access_t allow, + svn_repos_authz_access_t deny, + svn_repos_authz_access_t required) +{ + svn_repos_authz_access_t stripped_req = + required & (svn_authz_read | svn_authz_write); + + if ((deny & required) == svn_authz_none) + return TRUE; + else if ((allow & required) == stripped_req) + return TRUE; + else + return FALSE; +} + + +/* Decide whether the REQUIRED access has been conclusively + * determined. Return TRUE if the given ALLOW/DENY authz are + * conclusive regarding the REQUIRED authz. + * + * Conclusive determination occurs when any of the REQUIRED authz are + * granted or denied by ALLOW/DENY. + */ +static svn_boolean_t +authz_access_is_determined(svn_repos_authz_access_t allow, + svn_repos_authz_access_t deny, + svn_repos_authz_access_t required) +{ + if ((deny & required) || (allow & required)) + return TRUE; + else + return FALSE; +} + +/* Return TRUE is USER equals ALIAS. The alias definitions are in the + "aliases" sections of CFG. Use POOL for temporary allocations during + the lookup. */ +static svn_boolean_t +authz_alias_is_user(svn_config_t *cfg, + const char *alias, + const char *user, + apr_pool_t *pool) +{ + const char *value; + + svn_config_get(cfg, &value, "aliases", alias, NULL); + if (!value) + return FALSE; + + if (strcmp(value, user) == 0) + return TRUE; + + return FALSE; +} + + +/* Return TRUE if USER is in GROUP. The group definitions are in the + "groups" section of CFG. Use POOL for temporary allocations during + the lookup. */ +static svn_boolean_t +authz_group_contains_user(svn_config_t *cfg, + const char *group, + const char *user, + apr_pool_t *pool) +{ + const char *value; + apr_array_header_t *list; + int i; + + svn_config_get(cfg, &value, "groups", group, NULL); + + list = svn_cstring_split(value, ",", TRUE, pool); + + for (i = 0; i < list->nelts; i++) + { + const char *group_user = APR_ARRAY_IDX(list, i, char *); + + /* If the 'user' is a subgroup, recurse into it. */ + if (*group_user == '@') + { + if (authz_group_contains_user(cfg, &group_user[1], + user, pool)) + return TRUE; + } + + /* If the 'user' is an alias, verify it. */ + else if (*group_user == '&') + { + if (authz_alias_is_user(cfg, &group_user[1], + user, pool)) + return TRUE; + } + + /* If the user matches, stop. */ + else if (strcmp(user, group_user) == 0) + return TRUE; + } + + return FALSE; +} + + +/* Determines whether an authz rule applies to the current + * user, given the name part of the rule's name-value pair + * in RULE_MATCH_STRING and the authz_lookup_baton object + * B with the username in question. + */ +static svn_boolean_t +authz_line_applies_to_user(const char *rule_match_string, + struct authz_lookup_baton *b, + apr_pool_t *pool) +{ + /* If the rule has an inversion, recurse and invert the result. */ + if (rule_match_string[0] == '~') + return !authz_line_applies_to_user(&rule_match_string[1], b, pool); + + /* Check for special tokens. */ + if (strcmp(rule_match_string, "$anonymous") == 0) + return (b->user == NULL); + if (strcmp(rule_match_string, "$authenticated") == 0) + return (b->user != NULL); + + /* Check for a wildcard rule. */ + if (strcmp(rule_match_string, "*") == 0) + return TRUE; + + /* If we get here, then the rule is: + * - Not an inversion rule. + * - Not an authz token rule. + * - Not a wildcard rule. + * + * All that's left over is regular user or group specifications. + */ + + /* If the session is anonymous, then a user/group + * rule definitely won't match. + */ + if (b->user == NULL) + return FALSE; + + /* Process the rule depending on whether it is + * a user, alias or group rule. + */ + if (rule_match_string[0] == '@') + return authz_group_contains_user( + b->config, &rule_match_string[1], b->user, pool); + else if (rule_match_string[0] == '&') + return authz_alias_is_user( + b->config, &rule_match_string[1], b->user, pool); + else + return (strcmp(b->user, rule_match_string) == 0); +} + + +/* Callback to parse one line of an authz file and update the + * authz_baton accordingly. + */ +static svn_boolean_t +authz_parse_line(const char *name, const char *value, + void *baton, apr_pool_t *pool) +{ + struct authz_lookup_baton *b = baton; + + /* Stop if the rule doesn't apply to this user. */ + if (!authz_line_applies_to_user(name, b, pool)) + return TRUE; + + /* Set the access grants for the rule. */ + if (strchr(value, 'r')) + b->allow |= svn_authz_read; + else + b->deny |= svn_authz_read; + + if (strchr(value, 'w')) + b->allow |= svn_authz_write; + else + b->deny |= svn_authz_write; + + return TRUE; +} + + +/* Return TRUE iff the access rules in SECTION_NAME apply to PATH_SPEC + * (which is a repository name, colon, and repository fspath, such as + * "myrepos:/trunk/foo"). + */ +static svn_boolean_t +is_applicable_section(const char *path_spec, + const char *section_name) +{ + apr_size_t path_spec_len = strlen(path_spec); + + return ((strncmp(path_spec, section_name, path_spec_len) == 0) + && (path_spec[path_spec_len - 1] == '/' + || section_name[path_spec_len] == '/' + || section_name[path_spec_len] == '\0')); +} + + +/* Callback to parse a section and update the authz_baton if the + * section denies access to the subtree the baton describes. + */ +static svn_boolean_t +authz_parse_section(const char *section_name, void *baton, apr_pool_t *pool) +{ + struct authz_lookup_baton *b = baton; + svn_boolean_t conclusive; + + /* Does the section apply to us? */ + if (!is_applicable_section(b->qualified_repos_path, section_name) + && !is_applicable_section(b->repos_path, section_name)) + return TRUE; + + /* Work out what this section grants. */ + b->allow = b->deny = 0; + svn_config_enumerate2(b->config, section_name, + authz_parse_line, b, pool); + + /* Has the section explicitly determined an access? */ + conclusive = authz_access_is_determined(b->allow, b->deny, + b->required_access); + + /* Is access granted OR inconclusive? */ + b->access = authz_access_is_granted(b->allow, b->deny, + b->required_access) + || !conclusive; + + /* As long as access isn't conclusively denied, carry on. */ + return b->access; +} + + +/* Validate access to the given user for the given path. This + * function checks rules for exactly the given path, and first tries + * to access a section specific to the given repository before falling + * back to pan-repository rules. + * + * Update *access_granted to inform the caller of the outcome of the + * lookup. Return a boolean indicating whether the access rights were + * successfully determined. + */ +static svn_boolean_t +authz_get_path_access(svn_config_t *cfg, const char *repos_name, + const char *path, const char *user, + svn_repos_authz_access_t required_access, + svn_boolean_t *access_granted, + apr_pool_t *pool) +{ + const char *qualified_path; + struct authz_lookup_baton baton = { 0 }; + + baton.config = cfg; + baton.user = user; + + /* Try to locate a repository-specific block first. */ + qualified_path = apr_pstrcat(pool, repos_name, ":", path, (char *)NULL); + svn_config_enumerate2(cfg, qualified_path, + authz_parse_line, &baton, pool); + + *access_granted = authz_access_is_granted(baton.allow, baton.deny, + required_access); + + /* If the first test has determined access, stop now. */ + if (authz_access_is_determined(baton.allow, baton.deny, + required_access)) + return TRUE; + + /* No repository specific rule, try pan-repository rules. */ + svn_config_enumerate2(cfg, path, authz_parse_line, &baton, pool); + + *access_granted = authz_access_is_granted(baton.allow, baton.deny, + required_access); + return authz_access_is_determined(baton.allow, baton.deny, + required_access); +} + + +/* Validate access to the given user for the subtree starting at the + * given path. This function walks the whole authz file in search of + * rules applying to paths in the requested subtree which deny the + * requested access. + * + * As soon as one is found, or else when the whole ACL file has been + * searched, return the updated authorization status. + */ +static svn_boolean_t +authz_get_tree_access(svn_config_t *cfg, const char *repos_name, + const char *path, const char *user, + svn_repos_authz_access_t required_access, + apr_pool_t *pool) +{ + struct authz_lookup_baton baton = { 0 }; + + baton.config = cfg; + baton.user = user; + baton.required_access = required_access; + baton.repos_path = path; + baton.qualified_repos_path = apr_pstrcat(pool, repos_name, + ":", path, (char *)NULL); + /* Default to access granted if no rules say otherwise. */ + baton.access = TRUE; + + svn_config_enumerate_sections2(cfg, authz_parse_section, + &baton, pool); + + return baton.access; +} + + +/* Callback to parse sections of the configuration file, looking for + any kind of granted access. Implements the + svn_config_section_enumerator2_t interface. */ +static svn_boolean_t +authz_get_any_access_parser_cb(const char *section_name, void *baton, + apr_pool_t *pool) +{ + struct authz_lookup_baton *b = baton; + + /* Does the section apply to the query? */ + if (section_name[0] == '/' + || strncmp(section_name, b->qualified_repos_path, + strlen(b->qualified_repos_path)) == 0) + { + b->allow = b->deny = svn_authz_none; + + svn_config_enumerate2(b->config, section_name, + authz_parse_line, baton, pool); + b->access = authz_access_is_granted(b->allow, b->deny, + b->required_access); + + /* Continue as long as we don't find a determined, granted access. */ + return !(b->access + && authz_access_is_determined(b->allow, b->deny, + b->required_access)); + } + + return TRUE; +} + + +/* Walk through the authz CFG to check if USER has the REQUIRED_ACCESS + * to any path within the REPOSITORY. Return TRUE if so. Use POOL + * for temporary allocations. */ +static svn_boolean_t +authz_get_any_access(svn_config_t *cfg, const char *repos_name, + const char *user, + svn_repos_authz_access_t required_access, + apr_pool_t *pool) +{ + struct authz_lookup_baton baton = { 0 }; + + baton.config = cfg; + baton.user = user; + baton.required_access = required_access; + baton.access = FALSE; /* Deny access by default. */ + baton.repos_path = "/"; + baton.qualified_repos_path = apr_pstrcat(pool, repos_name, + ":/", (char *)NULL); + + /* We could have used svn_config_enumerate2 for "repos_name:/". + * However, this requires access for root explicitly (which the user + * may not always have). So we end up enumerating the sections in + * the authz CFG and stop on the first match with some access for + * this user. */ + svn_config_enumerate_sections2(cfg, authz_get_any_access_parser_cb, + &baton, pool); + + /* If walking the configuration was inconclusive, deny access. */ + if (!authz_access_is_determined(baton.allow, + baton.deny, baton.required_access)) + return FALSE; + + return baton.access; +} + + + +/*** Validating the authz file. ***/ + +/* Check for errors in GROUP's definition of CFG. The errors + * detected are references to non-existent groups and circular + * dependencies between groups. If an error is found, return + * SVN_ERR_AUTHZ_INVALID_CONFIG. Use POOL for temporary + * allocations only. + * + * CHECKED_GROUPS should be an empty (it is used for recursive calls). + */ +static svn_error_t * +authz_group_walk(svn_config_t *cfg, + const char *group, + apr_hash_t *checked_groups, + apr_pool_t *pool) +{ + const char *value; + apr_array_header_t *list; + int i; + + svn_config_get(cfg, &value, "groups", group, NULL); + /* Having a non-existent group in the ACL configuration might be the + sign of a typo. Refuse to perform authz on uncertain rules. */ + if (!value) + return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "An authz rule refers to group '%s', " + "which is undefined", + group); + + list = svn_cstring_split(value, ",", TRUE, pool); + + for (i = 0; i < list->nelts; i++) + { + const char *group_user = APR_ARRAY_IDX(list, i, char *); + + /* If the 'user' is a subgroup, recurse into it. */ + if (*group_user == '@') + { + /* A circular dependency between groups is a Bad Thing. We + don't do authz with invalid ACL files. */ + if (svn_hash_gets(checked_groups, &group_user[1])) + return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, + NULL, + "Circular dependency between " + "groups '%s' and '%s'", + &group_user[1], group); + + /* Add group to hash of checked groups. */ + svn_hash_sets(checked_groups, &group_user[1], ""); + + /* Recurse on that group. */ + SVN_ERR(authz_group_walk(cfg, &group_user[1], + checked_groups, pool)); + + /* Remove group from hash of checked groups, so that we don't + incorrectly report an error if we see it again as part of + another group. */ + svn_hash_sets(checked_groups, &group_user[1], NULL); + } + else if (*group_user == '&') + { + const char *alias; + + svn_config_get(cfg, &alias, "aliases", &group_user[1], NULL); + /* Having a non-existent alias in the ACL configuration might be the + sign of a typo. Refuse to perform authz on uncertain rules. */ + if (!alias) + return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "An authz rule refers to alias '%s', " + "which is undefined", + &group_user[1]); + } + } + + return SVN_NO_ERROR; +} + + +/* Callback to perform some simple sanity checks on an authz rule. + * + * - If RULE_MATCH_STRING references a group or an alias, verify that + * the group or alias definition exists. + * - If RULE_MATCH_STRING specifies a token (starts with $), verify + * that the token name is valid. + * - If RULE_MATCH_STRING is using inversion, verify that it isn't + * doing it more than once within the one rule, and that it isn't + * "~*", as that would never match. + * - Check that VALUE part of the rule specifies only allowed rule + * flag characters ('r' and 'w'). + * + * Return TRUE if the rule has no errors. Use BATON for context and + * error reporting. + */ +static svn_boolean_t authz_validate_rule(const char *rule_match_string, + const char *value, + void *baton, + apr_pool_t *pool) +{ + const char *val; + const char *match = rule_match_string; + struct authz_validate_baton *b = baton; + + /* Make sure the user isn't using double-negatives. */ + if (match[0] == '~') + { + /* Bump the pointer past the inversion for the other checks. */ + match++; + + /* Another inversion is a double negative; we can't not stop. */ + if (match[0] == '~') + { + b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "Rule '%s' has more than one " + "inversion; double negatives are " + "not permitted.", + rule_match_string); + return FALSE; + } + + /* Make sure that the rule isn't "~*", which won't ever match. */ + if (strcmp(match, "*") == 0) + { + b->err = svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "Authz rules with match string '~*' " + "are not allowed, because they never " + "match anyone."); + return FALSE; + } + } + + /* If the rule applies to a group, check its existence. */ + if (match[0] == '@') + { + const char *group = &match[1]; + + svn_config_get(b->config, &val, "groups", group, NULL); + + /* Having a non-existent group in the ACL configuration might be + the sign of a typo. Refuse to perform authz on uncertain + rules. */ + if (!val) + { + b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "An authz rule refers to group " + "'%s', which is undefined", + rule_match_string); + return FALSE; + } + } + + /* If the rule applies to an alias, check its existence. */ + if (match[0] == '&') + { + const char *alias = &match[1]; + + svn_config_get(b->config, &val, "aliases", alias, NULL); + + if (!val) + { + b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "An authz rule refers to alias " + "'%s', which is undefined", + rule_match_string); + return FALSE; + } + } + + /* If the rule specifies a token, check its validity. */ + if (match[0] == '$') + { + const char *token_name = &match[1]; + + if ((strcmp(token_name, "anonymous") != 0) + && (strcmp(token_name, "authenticated") != 0)) + { + b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "Unrecognized authz token '%s'.", + rule_match_string); + return FALSE; + } + } + + val = value; + + while (*val) + { + if (*val != 'r' && *val != 'w' && ! svn_ctype_isspace(*val)) + { + b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "The character '%c' in rule '%s' is not " + "allowed in authz rules", *val, + rule_match_string); + return FALSE; + } + + ++val; + } + + return TRUE; +} + +/* Callback to check ALIAS's definition for validity. Use + BATON for context and error reporting. */ +static svn_boolean_t authz_validate_alias(const char *alias, + const char *value, + void *baton, + apr_pool_t *pool) +{ + /* No checking at the moment, every alias is valid */ + return TRUE; +} + + +/* Callback to check GROUP's definition for cyclic dependancies. Use + BATON for context and error reporting. */ +static svn_boolean_t authz_validate_group(const char *group, + const char *value, + void *baton, + apr_pool_t *pool) +{ + struct authz_validate_baton *b = baton; + + b->err = authz_group_walk(b->config, group, apr_hash_make(pool), pool); + if (b->err) + return FALSE; + + return TRUE; +} + + +/* Callback to check the contents of the configuration section given + by NAME. Use BATON for context and error reporting. */ +static svn_boolean_t authz_validate_section(const char *name, + void *baton, + apr_pool_t *pool) +{ + struct authz_validate_baton *b = baton; + + /* Use the group checking callback for the "groups" section... */ + if (strcmp(name, "groups") == 0) + svn_config_enumerate2(b->config, name, authz_validate_group, + baton, pool); + /* ...and the alias checking callback for "aliases"... */ + else if (strcmp(name, "aliases") == 0) + svn_config_enumerate2(b->config, name, authz_validate_alias, + baton, pool); + /* ...but for everything else use the rule checking callback. */ + else + { + /* Validate the section's name. Skip the optional REPOS_NAME. */ + const char *fspath = strchr(name, ':'); + if (fspath) + fspath++; + else + fspath = name; + if (! svn_fspath__is_canonical(fspath)) + { + b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "Section name '%s' contains non-canonical " + "fspath '%s'", + name, fspath); + return FALSE; + } + + svn_config_enumerate2(b->config, name, authz_validate_rule, + baton, pool); + } + + if (b->err) + return FALSE; + + return TRUE; +} + + +/* Walk the configuration in AUTHZ looking for any errors. */ +static svn_error_t * +authz_validate(svn_authz_t *authz, apr_pool_t *pool) +{ + struct authz_validate_baton baton = { 0 }; + + baton.err = SVN_NO_ERROR; + baton.config = authz->cfg; + + /* Step through the entire rule file stopping on error. */ + svn_config_enumerate_sections2(authz->cfg, authz_validate_section, + &baton, pool); + SVN_ERR(baton.err); + + return SVN_NO_ERROR; +} + + +/* Retrieve the file at DIRENT (contained in a repo) then parse it as a config + * file placing the result into CFG_P allocated in POOL. + * + * If DIRENT cannot be parsed as a config file then an error is returned. The + * contents of CFG_P is then undefined. If MUST_EXIST is TRUE, a missing + * authz file is also an error. + * + * SCRATCH_POOL will be used for temporary allocations. */ +static svn_error_t * +authz_retrieve_config_repo(svn_config_t **cfg_p, const char *dirent, + svn_boolean_t must_exist, + apr_pool_t *result_pool, apr_pool_t *scratch_pool) +{ + svn_error_t *err; + svn_repos_t *repos; + const char *repos_root_dirent; + const char *fs_path; + svn_fs_t *fs; + svn_fs_root_t *root; + svn_revnum_t youngest_rev; + svn_node_kind_t node_kind; + svn_stream_t *contents; + + /* Search for a repository in the full path. */ + repos_root_dirent = svn_repos_find_root_path(dirent, scratch_pool); + if (!repos_root_dirent) + return svn_error_createf(SVN_ERR_RA_LOCAL_REPOS_NOT_FOUND, NULL, + "Unable to find repository at '%s'", dirent); + + /* Attempt to open a repository at repos_root_dirent. */ + SVN_ERR(svn_repos_open2(&repos, repos_root_dirent, NULL, scratch_pool)); + + fs_path = &dirent[strlen(repos_root_dirent)]; + + /* Root path is always a directory so no reason to go any further */ + if (*fs_path == '\0') + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + "'/' is not a file in repo '%s'", + repos_root_dirent); + + /* We skip some things that are non-important for how we're going to use + * this repo connection. We do not set any capabilities since none of + * the current ones are important for what we're doing. We also do not + * setup the environment that repos hooks would run under since we won't + * be triggering any. */ + + /* Get the filesystem. */ + fs = svn_repos_fs(repos); + + /* Find HEAD and the revision root */ + SVN_ERR(svn_fs_youngest_rev(&youngest_rev, fs, scratch_pool)); + SVN_ERR(svn_fs_revision_root(&root, fs, youngest_rev, scratch_pool)); + + SVN_ERR(svn_fs_check_path(&node_kind, root, fs_path, scratch_pool)); + if (node_kind == svn_node_none) + { + if (!must_exist) + { + SVN_ERR(svn_config_create2(cfg_p, TRUE, TRUE, result_pool)); + return SVN_NO_ERROR; + } + else + { + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + "'%s' path not found in repo '%s'", fs_path, + repos_root_dirent); + } + } + else if (node_kind != svn_node_file) + { + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + "'%s' is not a file in repo '%s'", fs_path, + repos_root_dirent); + } + + SVN_ERR(svn_fs_file_contents(&contents, root, fs_path, scratch_pool)); + err = svn_config_parse(cfg_p, contents, TRUE, TRUE, result_pool); + + /* Add the URL to the error stack since the parser doesn't have it. */ + if (err != SVN_NO_ERROR) + return svn_error_createf(err->apr_err, err, + "Error while parsing config file: '%s' in repo '%s':", + fs_path, repos_root_dirent); + + return SVN_NO_ERROR; +} + +/* Given a PATH which might be a relative repo URL (^/), an absolute + * local repo URL (file://), an absolute path outside of the repo + * or a location in the Windows registry. + * + * Retrieve the configuration data that PATH points at and parse it into + * CFG_P allocated in POOL. + * + * If PATH cannot be parsed as a config file then an error is returned. The + * contents of CFG_P is then undefined. If MUST_EXIST is TRUE, a missing + * authz file is also an error. + * + * REPOS_ROOT points at the root of the repos you are + * going to apply the authz against, can be NULL if you are sure that you + * don't have a repos relative URL in PATH. */ +static svn_error_t * +authz_retrieve_config(svn_config_t **cfg_p, const char *path, + svn_boolean_t must_exist, apr_pool_t *pool) +{ + if (svn_path_is_url(path)) + { + const char *dirent; + svn_error_t *err; + apr_pool_t *scratch_pool = svn_pool_create(pool); + + err = svn_uri_get_dirent_from_file_url(&dirent, path, scratch_pool); + + if (err == SVN_NO_ERROR) + err = authz_retrieve_config_repo(cfg_p, dirent, must_exist, pool, + scratch_pool); + + /* Close the repos and streams we opened. */ + svn_pool_destroy(scratch_pool); + + return err; + } + else + { + /* Outside of repo file or Windows registry*/ + SVN_ERR(svn_config_read3(cfg_p, path, must_exist, TRUE, TRUE, pool)); + } + + return SVN_NO_ERROR; +} + + +/* Callback to copy (name, value) group into the "groups" section + of another configuration. */ +static svn_boolean_t +authz_copy_group(const char *name, const char *value, + void *baton, apr_pool_t *pool) +{ + svn_config_t *authz_cfg = baton; + + svn_config_set(authz_cfg, SVN_CONFIG_SECTION_GROUPS, name, value); + + return TRUE; +} + +/* Copy group definitions from GROUPS_CFG to the resulting AUTHZ. + * If AUTHZ already contains any group definition, report an error. + * Use POOL for temporary allocations. */ +static svn_error_t * +authz_copy_groups(svn_authz_t *authz, svn_config_t *groups_cfg, + apr_pool_t *pool) +{ + /* Easy out: we prohibit local groups in the authz file when global + groups are being used. */ + if (svn_config_has_section(authz->cfg, SVN_CONFIG_SECTION_GROUPS)) + { + return svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "Authz file cannot contain any groups " + "when global groups are being used."); + } + + svn_config_enumerate2(groups_cfg, SVN_CONFIG_SECTION_GROUPS, + authz_copy_group, authz->cfg, pool); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos__authz_read(svn_authz_t **authz_p, const char *path, + const char *groups_path, svn_boolean_t must_exist, + svn_boolean_t accept_urls, apr_pool_t *pool) +{ + svn_authz_t *authz = apr_palloc(pool, sizeof(*authz)); + + /* Load the authz file */ + if (accept_urls) + SVN_ERR(authz_retrieve_config(&authz->cfg, path, must_exist, pool)); + else + SVN_ERR(svn_config_read3(&authz->cfg, path, must_exist, TRUE, TRUE, pool)); + + if (groups_path) + { + svn_config_t *groups_cfg; + svn_error_t *err; + + /* Load the groups file */ + if (accept_urls) + SVN_ERR(authz_retrieve_config(&groups_cfg, groups_path, must_exist, + pool)); + else + SVN_ERR(svn_config_read3(&groups_cfg, groups_path, must_exist, + TRUE, TRUE, pool)); + + /* Copy the groups from groups_cfg into authz. */ + err = authz_copy_groups(authz, groups_cfg, pool); + + /* Add the paths to the error stack since the authz_copy_groups + routine knows nothing about them. */ + if (err != SVN_NO_ERROR) + return svn_error_createf(err->apr_err, err, + "Error reading authz file '%s' with " + "groups file '%s':", path, groups_path); + } + + /* Make sure there are no errors in the configuration. */ + SVN_ERR(authz_validate(authz, pool)); + + *authz_p = authz; + return SVN_NO_ERROR; +} + + + +/*** Public functions. ***/ + +svn_error_t * +svn_repos_authz_read2(svn_authz_t **authz_p, const char *path, + const char *groups_path, svn_boolean_t must_exist, + apr_pool_t *pool) +{ + return svn_repos__authz_read(authz_p, path, groups_path, must_exist, + TRUE, pool); +} + + +svn_error_t * +svn_repos_authz_parse(svn_authz_t **authz_p, svn_stream_t *stream, + svn_stream_t *groups_stream, apr_pool_t *pool) +{ + svn_authz_t *authz = apr_palloc(pool, sizeof(*authz)); + + /* Parse the authz stream */ + SVN_ERR(svn_config_parse(&authz->cfg, stream, TRUE, TRUE, pool)); + + if (groups_stream) + { + svn_config_t *groups_cfg; + + /* Parse the groups stream */ + SVN_ERR(svn_config_parse(&groups_cfg, groups_stream, TRUE, TRUE, pool)); + + SVN_ERR(authz_copy_groups(authz, groups_cfg, pool)); + } + + /* Make sure there are no errors in the configuration. */ + SVN_ERR(authz_validate(authz, pool)); + + *authz_p = authz; + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_authz_check_access(svn_authz_t *authz, const char *repos_name, + const char *path, const char *user, + svn_repos_authz_access_t required_access, + svn_boolean_t *access_granted, + apr_pool_t *pool) +{ + const char *current_path; + + if (!repos_name) + repos_name = ""; + + /* If PATH is NULL, check if the user has *any* access. */ + if (!path) + { + *access_granted = authz_get_any_access(authz->cfg, repos_name, + user, required_access, pool); + return SVN_NO_ERROR; + } + + /* Sanity check. */ + SVN_ERR_ASSERT(path[0] == '/'); + + /* Determine the granted access for the requested path. */ + path = svn_fspath__canonicalize(path, pool); + current_path = path; + + while (!authz_get_path_access(authz->cfg, repos_name, + current_path, user, + required_access, + access_granted, + pool)) + { + /* Stop if the loop hits the repository root with no + results. */ + if (current_path[0] == '/' && current_path[1] == '\0') + { + /* Deny access by default. */ + *access_granted = FALSE; + return SVN_NO_ERROR; + } + + /* Work back to the parent path. */ + current_path = svn_fspath__dirname(current_path, pool); + } + + /* If the caller requested recursive access, we need to walk through + the entire authz config to see whether any child paths are denied + to the requested user. */ + if (*access_granted && (required_access & svn_authz_recursive)) + *access_granted = authz_get_tree_access(authz->cfg, repos_name, path, + user, required_access, pool); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_repos/commit.c b/subversion/libsvn_repos/commit.c new file mode 100644 index 0000000..c4606ab --- /dev/null +++ b/subversion/libsvn_repos/commit.c @@ -0,0 +1,1381 @@ +/* commit.c --- editor for committing changes to a filesystem. + * + * ==================================================================== + * 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 <string.h> + +#include <apr_pools.h> +#include <apr_file_io.h> + +#include "svn_hash.h" +#include "svn_compat.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_delta.h" +#include "svn_fs.h" +#include "svn_repos.h" +#include "svn_checksum.h" +#include "svn_ctype.h" +#include "svn_props.h" +#include "svn_mergeinfo.h" +#include "svn_private_config.h" + +#include "repos.h" + +#include "private/svn_fspath.h" +#include "private/svn_fs_private.h" +#include "private/svn_repos_private.h" +#include "private/svn_editor.h" + + + +/*** Editor batons. ***/ + +struct edit_baton +{ + apr_pool_t *pool; + + /** Supplied when the editor is created: **/ + + /* Revision properties to set for this commit. */ + apr_hash_t *revprop_table; + + /* Callback to run when the commit is done. */ + svn_commit_callback2_t commit_callback; + void *commit_callback_baton; + + /* Callback to check authorizations on paths. */ + svn_repos_authz_callback_t authz_callback; + void *authz_baton; + + /* The already-open svn repository to commit to. */ + svn_repos_t *repos; + + /* URL to the root of the open repository. */ + const char *repos_url; + + /* The name of the repository (here for convenience). */ + const char *repos_name; + + /* The filesystem associated with the REPOS above (here for + convenience). */ + svn_fs_t *fs; + + /* Location in fs where the edit will begin. */ + const char *base_path; + + /* Does this set of interfaces 'own' the commit transaction? */ + svn_boolean_t txn_owner; + + /* svn transaction associated with this edit (created in + open_root, or supplied by the public API caller). */ + svn_fs_txn_t *txn; + + /** Filled in during open_root: **/ + + /* The name of the transaction. */ + const char *txn_name; + + /* The object representing the root directory of the svn txn. */ + svn_fs_root_t *txn_root; + + /* Avoid aborting an fs transaction more than once */ + svn_boolean_t txn_aborted; + + /** Filled in when the edit is closed: **/ + + /* The new revision created by this commit. */ + svn_revnum_t *new_rev; + + /* The date (according to the repository) of this commit. */ + const char **committed_date; + + /* The author (also according to the repository) of this commit. */ + const char **committed_author; +}; + + +struct dir_baton +{ + struct edit_baton *edit_baton; + struct dir_baton *parent; + const char *path; /* the -absolute- path to this dir in the fs */ + svn_revnum_t base_rev; /* the revision I'm based on */ + svn_boolean_t was_copied; /* was this directory added with history? */ + apr_pool_t *pool; /* my personal pool, in which I am allocated. */ +}; + + +struct file_baton +{ + struct edit_baton *edit_baton; + const char *path; /* the -absolute- path to this file in the fs */ +}; + + +struct ev2_baton +{ + /* The repository we are editing. */ + svn_repos_t *repos; + + /* The authz baton for checks; NULL to skip authz. */ + svn_authz_t *authz; + + /* The repository name and user for performing authz checks. */ + const char *authz_repos_name; + const char *authz_user; + + /* Callback to provide info about the committed revision. */ + svn_commit_callback2_t commit_cb; + void *commit_baton; + + /* The FS txn editor */ + svn_editor_t *inner; + + /* The name of the open transaction (so we know what to commit) */ + const char *txn_name; +}; + + +/* Create and return a generic out-of-dateness error. */ +static svn_error_t * +out_of_date(const char *path, svn_node_kind_t kind) +{ + return svn_error_createf(SVN_ERR_FS_TXN_OUT_OF_DATE, NULL, + (kind == svn_node_dir + ? _("Directory '%s' is out of date") + : kind == svn_node_file + ? _("File '%s' is out of date") + : _("'%s' is out of date")), + path); +} + + +static svn_error_t * +invoke_commit_cb(svn_commit_callback2_t commit_cb, + void *commit_baton, + svn_fs_t *fs, + svn_revnum_t revision, + const char *post_commit_errstr, + apr_pool_t *scratch_pool) +{ + /* FS interface returns non-const values. */ + /* const */ svn_string_t *date; + /* const */ svn_string_t *author; + svn_commit_info_t *commit_info; + + if (commit_cb == NULL) + return SVN_NO_ERROR; + + SVN_ERR(svn_fs_revision_prop(&date, fs, revision, SVN_PROP_REVISION_DATE, + scratch_pool)); + SVN_ERR(svn_fs_revision_prop(&author, fs, revision, + SVN_PROP_REVISION_AUTHOR, + scratch_pool)); + + commit_info = svn_create_commit_info(scratch_pool); + + /* fill up the svn_commit_info structure */ + commit_info->revision = revision; + commit_info->date = date ? date->data : NULL; + commit_info->author = author ? author->data : NULL; + commit_info->post_commit_err = post_commit_errstr; + + return svn_error_trace(commit_cb(commit_info, commit_baton, scratch_pool)); +} + + + +/* If EDITOR_BATON contains a valid authz callback, verify that the + REQUIRED access to PATH in ROOT is authorized. Return an error + appropriate for throwing out of the commit editor with SVN_ERR. If + no authz callback is present in EDITOR_BATON, then authorize all + paths. Use POOL for temporary allocation only. */ +static svn_error_t * +check_authz(struct edit_baton *editor_baton, const char *path, + svn_fs_root_t *root, svn_repos_authz_access_t required, + apr_pool_t *pool) +{ + if (editor_baton->authz_callback) + { + svn_boolean_t allowed; + + SVN_ERR(editor_baton->authz_callback(required, &allowed, root, path, + editor_baton->authz_baton, pool)); + if (!allowed) + return svn_error_create(required & svn_authz_write ? + SVN_ERR_AUTHZ_UNWRITABLE : + SVN_ERR_AUTHZ_UNREADABLE, + NULL, "Access denied"); + } + + return SVN_NO_ERROR; +} + + +/* Return a directory baton allocated in POOL which represents + FULL_PATH, which is the immediate directory child of the directory + represented by PARENT_BATON. EDIT_BATON is the commit editor + baton. WAS_COPIED reveals whether or not this directory is the + result of a copy operation. BASE_REVISION is the base revision of + the directory. */ +static struct dir_baton * +make_dir_baton(struct edit_baton *edit_baton, + struct dir_baton *parent_baton, + const char *full_path, + svn_boolean_t was_copied, + svn_revnum_t base_revision, + apr_pool_t *pool) +{ + struct dir_baton *db; + db = apr_pcalloc(pool, sizeof(*db)); + db->edit_baton = edit_baton; + db->parent = parent_baton; + db->pool = pool; + db->path = full_path; + db->was_copied = was_copied; + db->base_rev = base_revision; + return db; +} + +/* This function is the shared guts of add_file() and add_directory(), + which see for the meanings of the parameters. The only extra + parameter here is IS_DIR, which is TRUE when adding a directory, + and FALSE when adding a file. */ +static svn_error_t * +add_file_or_directory(const char *path, + void *parent_baton, + const char *copy_path, + svn_revnum_t copy_revision, + svn_boolean_t is_dir, + apr_pool_t *pool, + void **return_baton) +{ + struct dir_baton *pb = parent_baton; + struct edit_baton *eb = pb->edit_baton; + apr_pool_t *subpool = svn_pool_create(pool); + svn_boolean_t was_copied = FALSE; + const char *full_path; + + /* Reject paths which contain control characters (related to issue #4340). */ + SVN_ERR(svn_path_check_valid(path, pool)); + + full_path = svn_fspath__join(eb->base_path, + svn_relpath_canonicalize(path, pool), pool); + + /* Sanity check. */ + if (copy_path && (! SVN_IS_VALID_REVNUM(copy_revision))) + return svn_error_createf + (SVN_ERR_FS_GENERAL, NULL, + _("Got source path but no source revision for '%s'"), full_path); + + if (copy_path) + { + const char *fs_path; + svn_fs_root_t *copy_root; + svn_node_kind_t kind; + size_t repos_url_len; + svn_repos_authz_access_t required; + + /* Copy requires recursive write access to the destination path + and write access to the parent path. */ + required = svn_authz_write | (is_dir ? svn_authz_recursive : 0); + SVN_ERR(check_authz(eb, full_path, eb->txn_root, + required, subpool)); + SVN_ERR(check_authz(eb, pb->path, eb->txn_root, + svn_authz_write, subpool)); + + /* Check PATH in our transaction. Make sure it does not exist + unless its parent directory was copied (in which case, the + thing might have been copied in as well), else return an + out-of-dateness error. */ + SVN_ERR(svn_fs_check_path(&kind, eb->txn_root, full_path, subpool)); + if ((kind != svn_node_none) && (! pb->was_copied)) + return svn_error_trace(out_of_date(full_path, kind)); + + /* For now, require that the url come from the same repository + that this commit is operating on. */ + copy_path = svn_path_uri_decode(copy_path, subpool); + repos_url_len = strlen(eb->repos_url); + if (strncmp(copy_path, eb->repos_url, repos_url_len) != 0) + return svn_error_createf + (SVN_ERR_FS_GENERAL, NULL, + _("Source url '%s' is from different repository"), copy_path); + + fs_path = apr_pstrdup(subpool, copy_path + repos_url_len); + + /* Now use the "fs_path" as an absolute path within the + repository to make the copy from. */ + SVN_ERR(svn_fs_revision_root(©_root, eb->fs, + copy_revision, subpool)); + + /* Copy also requires (recursive) read access to the source */ + required = svn_authz_read | (is_dir ? svn_authz_recursive : 0); + SVN_ERR(check_authz(eb, fs_path, copy_root, required, subpool)); + + SVN_ERR(svn_fs_copy(copy_root, fs_path, + eb->txn_root, full_path, subpool)); + was_copied = TRUE; + } + else + { + /* No ancestry given, just make a new directory or empty file. + Note that we don't perform an existence check here like the + copy-from case does -- that's because svn_fs_make_*() + already errors out if the file already exists. Verify write + access to the full path and to the parent. */ + SVN_ERR(check_authz(eb, full_path, eb->txn_root, + svn_authz_write, subpool)); + SVN_ERR(check_authz(eb, pb->path, eb->txn_root, + svn_authz_write, subpool)); + if (is_dir) + SVN_ERR(svn_fs_make_dir(eb->txn_root, full_path, subpool)); + else + SVN_ERR(svn_fs_make_file(eb->txn_root, full_path, subpool)); + } + + /* Cleanup our temporary subpool. */ + svn_pool_destroy(subpool); + + /* Build a new child baton. */ + if (is_dir) + { + *return_baton = make_dir_baton(eb, pb, full_path, was_copied, + SVN_INVALID_REVNUM, pool); + } + else + { + struct file_baton *new_fb = apr_pcalloc(pool, sizeof(*new_fb)); + new_fb->edit_baton = eb; + new_fb->path = full_path; + *return_baton = new_fb; + } + + return SVN_NO_ERROR; +} + + + +/*** Editor functions ***/ + +static svn_error_t * +open_root(void *edit_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **root_baton) +{ + struct dir_baton *dirb; + struct edit_baton *eb = edit_baton; + svn_revnum_t youngest; + + /* Ignore BASE_REVISION. We always build our transaction against + HEAD. However, we will keep it in our dir baton for out of + dateness checks. */ + SVN_ERR(svn_fs_youngest_rev(&youngest, eb->fs, eb->pool)); + + /* Unless we've been instructed to use a specific transaction, we'll + make our own. */ + if (eb->txn_owner) + { + SVN_ERR(svn_repos_fs_begin_txn_for_commit2(&(eb->txn), + eb->repos, + youngest, + eb->revprop_table, + eb->pool)); + } + else /* Even if we aren't the owner of the transaction, we might + have been instructed to set some properties. */ + { + apr_array_header_t *props = svn_prop_hash_to_array(eb->revprop_table, + pool); + SVN_ERR(svn_repos_fs_change_txn_props(eb->txn, props, pool)); + } + SVN_ERR(svn_fs_txn_name(&(eb->txn_name), eb->txn, eb->pool)); + SVN_ERR(svn_fs_txn_root(&(eb->txn_root), eb->txn, eb->pool)); + + /* Create a root dir baton. The `base_path' field is an -absolute- + path in the filesystem, upon which all further editor paths are + based. */ + dirb = apr_pcalloc(pool, sizeof(*dirb)); + dirb->edit_baton = edit_baton; + dirb->parent = NULL; + dirb->pool = pool; + dirb->was_copied = FALSE; + dirb->path = apr_pstrdup(pool, eb->base_path); + dirb->base_rev = base_revision; + + *root_baton = dirb; + return SVN_NO_ERROR; +} + + + +static svn_error_t * +delete_entry(const char *path, + svn_revnum_t revision, + void *parent_baton, + apr_pool_t *pool) +{ + struct dir_baton *parent = parent_baton; + struct edit_baton *eb = parent->edit_baton; + svn_node_kind_t kind; + svn_revnum_t cr_rev; + svn_repos_authz_access_t required = svn_authz_write; + const char *full_path; + + full_path = svn_fspath__join(eb->base_path, + svn_relpath_canonicalize(path, pool), pool); + + /* Check PATH in our transaction. */ + SVN_ERR(svn_fs_check_path(&kind, eb->txn_root, full_path, pool)); + + /* Deletion requires a recursive write access, as well as write + access to the parent directory. */ + if (kind == svn_node_dir) + required |= svn_authz_recursive; + SVN_ERR(check_authz(eb, full_path, eb->txn_root, + required, pool)); + SVN_ERR(check_authz(eb, parent->path, eb->txn_root, + svn_authz_write, pool)); + + /* If PATH doesn't exist in the txn, the working copy is out of date. */ + if (kind == svn_node_none) + return svn_error_trace(out_of_date(full_path, kind)); + + /* Now, make sure we're deleting the node we *think* we're + deleting, else return an out-of-dateness error. */ + SVN_ERR(svn_fs_node_created_rev(&cr_rev, eb->txn_root, full_path, pool)); + if (SVN_IS_VALID_REVNUM(revision) && (revision < cr_rev)) + return svn_error_trace(out_of_date(full_path, kind)); + + /* This routine is a mindless wrapper. We call svn_fs_delete() + because that will delete files and recursively delete + directories. */ + return svn_fs_delete(eb->txn_root, full_path, pool); +} + + +static svn_error_t * +add_directory(const char *path, + void *parent_baton, + const char *copy_path, + svn_revnum_t copy_revision, + apr_pool_t *pool, + void **child_baton) +{ + return add_file_or_directory(path, parent_baton, copy_path, copy_revision, + TRUE /* is_dir */, pool, child_baton); +} + + +static svn_error_t * +open_directory(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **child_baton) +{ + struct dir_baton *pb = parent_baton; + struct edit_baton *eb = pb->edit_baton; + svn_node_kind_t kind; + const char *full_path; + + full_path = svn_fspath__join(eb->base_path, + svn_relpath_canonicalize(path, pool), pool); + + /* Check PATH in our transaction. If it does not exist, + return a 'Path not present' error. */ + SVN_ERR(svn_fs_check_path(&kind, eb->txn_root, full_path, pool)); + if (kind == svn_node_none) + return svn_error_createf(SVN_ERR_FS_NOT_DIRECTORY, NULL, + _("Path '%s' not present"), + path); + + /* Build a new dir baton for this directory. */ + *child_baton = make_dir_baton(eb, pb, full_path, pb->was_copied, + base_revision, pool); + return SVN_NO_ERROR; +} + + +static svn_error_t * +apply_textdelta(void *file_baton, + const char *base_checksum, + apr_pool_t *pool, + svn_txdelta_window_handler_t *handler, + void **handler_baton) +{ + struct file_baton *fb = file_baton; + + /* Check for write authorization. */ + SVN_ERR(check_authz(fb->edit_baton, fb->path, + fb->edit_baton->txn_root, + svn_authz_write, pool)); + + return svn_fs_apply_textdelta(handler, handler_baton, + fb->edit_baton->txn_root, + fb->path, + base_checksum, + NULL, + pool); +} + + +static svn_error_t * +add_file(const char *path, + void *parent_baton, + const char *copy_path, + svn_revnum_t copy_revision, + apr_pool_t *pool, + void **file_baton) +{ + return add_file_or_directory(path, parent_baton, copy_path, copy_revision, + FALSE /* is_dir */, pool, file_baton); +} + + +static svn_error_t * +open_file(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **file_baton) +{ + struct file_baton *new_fb; + struct dir_baton *pb = parent_baton; + struct edit_baton *eb = pb->edit_baton; + svn_revnum_t cr_rev; + apr_pool_t *subpool = svn_pool_create(pool); + const char *full_path; + + full_path = svn_fspath__join(eb->base_path, + svn_relpath_canonicalize(path, pool), pool); + + /* Check for read authorization. */ + SVN_ERR(check_authz(eb, full_path, eb->txn_root, + svn_authz_read, subpool)); + + /* Get this node's creation revision (doubles as an existence check). */ + SVN_ERR(svn_fs_node_created_rev(&cr_rev, eb->txn_root, full_path, + subpool)); + + /* If the node our caller has is an older revision number than the + one in our transaction, return an out-of-dateness error. */ + if (SVN_IS_VALID_REVNUM(base_revision) && (base_revision < cr_rev)) + return svn_error_trace(out_of_date(full_path, svn_node_file)); + + /* Build a new file baton */ + new_fb = apr_pcalloc(pool, sizeof(*new_fb)); + new_fb->edit_baton = eb; + new_fb->path = full_path; + + *file_baton = new_fb; + + /* Destory the work subpool. */ + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + + +static svn_error_t * +change_file_prop(void *file_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + struct file_baton *fb = file_baton; + struct edit_baton *eb = fb->edit_baton; + + /* Check for write authorization. */ + SVN_ERR(check_authz(eb, fb->path, eb->txn_root, + svn_authz_write, pool)); + + return svn_repos_fs_change_node_prop(eb->txn_root, fb->path, + name, value, pool); +} + + +static svn_error_t * +close_file(void *file_baton, + const char *text_digest, + apr_pool_t *pool) +{ + struct file_baton *fb = file_baton; + + if (text_digest) + { + svn_checksum_t *checksum; + svn_checksum_t *text_checksum; + + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, + fb->edit_baton->txn_root, fb->path, + TRUE, pool)); + SVN_ERR(svn_checksum_parse_hex(&text_checksum, svn_checksum_md5, + text_digest, pool)); + + if (!svn_checksum_match(text_checksum, checksum)) + return svn_checksum_mismatch_err(text_checksum, checksum, pool, + _("Checksum mismatch for resulting fulltext\n(%s)"), + fb->path); + } + + return SVN_NO_ERROR; +} + + +static svn_error_t * +change_dir_prop(void *dir_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + struct dir_baton *db = dir_baton; + struct edit_baton *eb = db->edit_baton; + + /* Check for write authorization. */ + SVN_ERR(check_authz(eb, db->path, eb->txn_root, + svn_authz_write, pool)); + + if (SVN_IS_VALID_REVNUM(db->base_rev)) + { + /* Subversion rule: propchanges can only happen on a directory + which is up-to-date. */ + svn_revnum_t created_rev; + SVN_ERR(svn_fs_node_created_rev(&created_rev, + eb->txn_root, db->path, pool)); + + if (db->base_rev < created_rev) + return svn_error_trace(out_of_date(db->path, svn_node_dir)); + } + + return svn_repos_fs_change_node_prop(eb->txn_root, db->path, + name, value, pool); +} + +const char * +svn_repos__post_commit_error_str(svn_error_t *err, + apr_pool_t *pool) +{ + svn_error_t *hook_err1, *hook_err2; + const char *msg; + + if (! err) + return _("(no error)"); + + err = svn_error_purge_tracing(err); + + /* hook_err1 is the SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED wrapped + error from the post-commit script, if any, and hook_err2 should + be the original error, but be defensive and handle a case where + SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED doesn't wrap an error. */ + hook_err1 = svn_error_find_cause(err, SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED); + if (hook_err1 && hook_err1->child) + hook_err2 = hook_err1->child; + else + hook_err2 = hook_err1; + + /* This implementation counts on svn_repos_fs_commit_txn() and + libsvn_repos/commit.c:complete_cb() returning + svn_fs_commit_txn() as the parent error with a child + SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED error. If the parent error + is SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED then there was no error + in svn_fs_commit_txn(). + + The post-commit hook error message is already self describing, so + it can be dropped into an error message without any additional + text. */ + if (hook_err1) + { + if (err == hook_err1) + { + if (hook_err2->message) + msg = apr_pstrdup(pool, hook_err2->message); + else + msg = _("post-commit hook failed with no error message."); + } + else + { + msg = hook_err2->message + ? apr_pstrdup(pool, hook_err2->message) + : _("post-commit hook failed with no error message."); + msg = apr_psprintf( + pool, + _("post commit FS processing had error:\n%s\n%s"), + err->message ? err->message : _("(no error message)"), + msg); + } + } + else + { + msg = apr_psprintf(pool, + _("post commit FS processing had error:\n%s"), + err->message ? err->message + : _("(no error message)")); + } + + return msg; +} + +static svn_error_t * +close_edit(void *edit_baton, + apr_pool_t *pool) +{ + struct edit_baton *eb = edit_baton; + svn_revnum_t new_revision = SVN_INVALID_REVNUM; + svn_error_t *err; + const char *conflict; + const char *post_commit_err = NULL; + + /* If no transaction has been created (ie. if open_root wasn't + called before close_edit), abort the operation here with an + error. */ + if (! eb->txn) + return svn_error_create(SVN_ERR_REPOS_BAD_ARGS, NULL, + "No valid transaction supplied to close_edit"); + + /* Commit. */ + err = svn_repos_fs_commit_txn(&conflict, eb->repos, + &new_revision, eb->txn, pool); + + if (SVN_IS_VALID_REVNUM(new_revision)) + { + if (err) + { + /* If the error was in post-commit, then the commit itself + succeeded. In which case, save the post-commit warning + (to be reported back to the client, who will probably + display it as a warning) and clear the error. */ + post_commit_err = svn_repos__post_commit_error_str(err, pool); + svn_error_clear(err); + } + } + else + { + /* ### todo: we should check whether it really was a conflict, + and return the conflict info if so? */ + + /* If the commit failed, it's *probably* due to a conflict -- + that is, the txn being out-of-date. The filesystem gives us + the ability to continue diddling the transaction and try + again; but let's face it: that's not how the cvs or svn works + from a user interface standpoint. Thus we don't make use of + this fs feature (for now, at least.) + + So, in a nutshell: svn commits are an all-or-nothing deal. + Each commit creates a new fs txn which either succeeds or is + aborted completely. No second chances; the user simply + needs to update and commit again :) */ + + eb->txn_aborted = TRUE; + + return svn_error_trace( + svn_error_compose_create(err, + svn_fs_abort_txn(eb->txn, pool))); + } + + /* At this point, the post-commit error has been converted to a string. + That information will be passed to a callback, if provided. If the + callback invocation fails in some way, that failure is returned here. + IOW, the post-commit error information is low priority compared to + other gunk here. */ + + /* Pass new revision information to the caller's callback. */ + return svn_error_trace(invoke_commit_cb(eb->commit_callback, + eb->commit_callback_baton, + eb->repos->fs, + new_revision, + post_commit_err, + pool)); +} + + +static svn_error_t * +abort_edit(void *edit_baton, + apr_pool_t *pool) +{ + struct edit_baton *eb = edit_baton; + if ((! eb->txn) || (! eb->txn_owner) || eb->txn_aborted) + return SVN_NO_ERROR; + + eb->txn_aborted = TRUE; + + return svn_error_trace(svn_fs_abort_txn(eb->txn, pool)); +} + + +static svn_error_t * +fetch_props_func(apr_hash_t **props, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct edit_baton *eb = baton; + svn_fs_root_t *fs_root; + svn_error_t *err; + + SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, + svn_fs_txn_base_revision(eb->txn), + scratch_pool)); + err = svn_fs_node_proplist(props, fs_root, path, result_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + *props = apr_hash_make(result_pool); + return SVN_NO_ERROR; + } + else if (err) + return svn_error_trace(err); + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_kind_func(svn_node_kind_t *kind, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *scratch_pool) +{ + struct edit_baton *eb = baton; + svn_fs_root_t *fs_root; + + if (!SVN_IS_VALID_REVNUM(base_revision)) + base_revision = svn_fs_txn_base_revision(eb->txn); + + SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, base_revision, scratch_pool)); + + SVN_ERR(svn_fs_check_path(kind, fs_root, path, scratch_pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_base_func(const char **filename, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct edit_baton *eb = baton; + svn_stream_t *contents; + svn_stream_t *file_stream; + const char *tmp_filename; + svn_fs_root_t *fs_root; + svn_error_t *err; + + if (!SVN_IS_VALID_REVNUM(base_revision)) + base_revision = svn_fs_txn_base_revision(eb->txn); + + SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, base_revision, scratch_pool)); + + err = svn_fs_file_contents(&contents, fs_root, path, scratch_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + *filename = NULL; + return SVN_NO_ERROR; + } + else if (err) + return svn_error_trace(err); + SVN_ERR(svn_stream_open_unique(&file_stream, &tmp_filename, NULL, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_copy3(contents, file_stream, NULL, NULL, scratch_pool)); + + *filename = apr_pstrdup(result_pool, tmp_filename); + + return SVN_NO_ERROR; +} + + + +/*** Public interfaces. ***/ + +svn_error_t * +svn_repos_get_commit_editor5(const svn_delta_editor_t **editor, + void **edit_baton, + svn_repos_t *repos, + svn_fs_txn_t *txn, + const char *repos_url, + const char *base_path, + apr_hash_t *revprop_table, + svn_commit_callback2_t commit_callback, + void *commit_baton, + svn_repos_authz_callback_t authz_callback, + void *authz_baton, + apr_pool_t *pool) +{ + svn_delta_editor_t *e; + apr_pool_t *subpool = svn_pool_create(pool); + struct edit_baton *eb; + svn_delta_shim_callbacks_t *shim_callbacks = + svn_delta_shim_callbacks_default(pool); + + /* Do a global authz access lookup. Users with no write access + whatsoever to the repository don't get a commit editor. */ + if (authz_callback) + { + svn_boolean_t allowed; + + SVN_ERR(authz_callback(svn_authz_write, &allowed, NULL, NULL, + authz_baton, pool)); + if (!allowed) + return svn_error_create(SVN_ERR_AUTHZ_UNWRITABLE, NULL, + "Not authorized to open a commit editor."); + } + + /* Allocate the structures. */ + e = svn_delta_default_editor(pool); + eb = apr_pcalloc(subpool, sizeof(*eb)); + + /* Set up the editor. */ + e->open_root = open_root; + e->delete_entry = delete_entry; + e->add_directory = add_directory; + e->open_directory = open_directory; + e->change_dir_prop = change_dir_prop; + e->add_file = add_file; + e->open_file = open_file; + e->close_file = close_file; + e->apply_textdelta = apply_textdelta; + e->change_file_prop = change_file_prop; + e->close_edit = close_edit; + e->abort_edit = abort_edit; + + /* Set up the edit baton. */ + eb->pool = subpool; + eb->revprop_table = svn_prop_hash_dup(revprop_table, subpool); + eb->commit_callback = commit_callback; + eb->commit_callback_baton = commit_baton; + eb->authz_callback = authz_callback; + eb->authz_baton = authz_baton; + eb->base_path = svn_fspath__canonicalize(base_path, subpool); + eb->repos = repos; + eb->repos_url = repos_url; + eb->repos_name = svn_dirent_basename(svn_repos_path(repos, subpool), + subpool); + eb->fs = svn_repos_fs(repos); + eb->txn = txn; + eb->txn_owner = txn == NULL; + + *edit_baton = eb; + *editor = e; + + shim_callbacks->fetch_props_func = fetch_props_func; + shim_callbacks->fetch_kind_func = fetch_kind_func; + shim_callbacks->fetch_base_func = fetch_base_func; + shim_callbacks->fetch_baton = eb; + + SVN_ERR(svn_editor__insert_shims(editor, edit_baton, *editor, *edit_baton, + eb->repos_url, eb->base_path, + shim_callbacks, pool, pool)); + + return SVN_NO_ERROR; +} + + +#if 0 +static svn_error_t * +ev2_check_authz(const struct ev2_baton *eb, + const char *relpath, + svn_repos_authz_access_t required, + apr_pool_t *scratch_pool) +{ + const char *fspath; + svn_boolean_t allowed; + + if (eb->authz == NULL) + return SVN_NO_ERROR; + + if (relpath) + fspath = apr_pstrcat(scratch_pool, "/", relpath, NULL); + else + fspath = NULL; + + SVN_ERR(svn_repos_authz_check_access(eb->authz, eb->authz_repos_name, fspath, + eb->authz_user, required, + &allowed, scratch_pool)); + if (!allowed) + return svn_error_create(required & svn_authz_write + ? SVN_ERR_AUTHZ_UNWRITABLE + : SVN_ERR_AUTHZ_UNREADABLE, + NULL, "Access denied"); + + return SVN_NO_ERROR; +} +#endif + + +/* This implements svn_editor_cb_add_directory_t */ +static svn_error_t * +add_directory_cb(void *baton, + const char *relpath, + const apr_array_header_t *children, + apr_hash_t *props, + svn_revnum_t replaces_rev, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_add_directory(eb->inner, relpath, children, props, + replaces_rev)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_add_file_t */ +static svn_error_t * +add_file_cb(void *baton, + const char *relpath, + const svn_checksum_t *checksum, + svn_stream_t *contents, + apr_hash_t *props, + svn_revnum_t replaces_rev, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_add_file(eb->inner, relpath, checksum, contents, props, + replaces_rev)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_add_symlink_t */ +static svn_error_t * +add_symlink_cb(void *baton, + const char *relpath, + const char *target, + apr_hash_t *props, + svn_revnum_t replaces_rev, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_add_symlink(eb->inner, relpath, target, props, + replaces_rev)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_add_absent_t */ +static svn_error_t * +add_absent_cb(void *baton, + const char *relpath, + svn_node_kind_t kind, + svn_revnum_t replaces_rev, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_add_absent(eb->inner, relpath, kind, replaces_rev)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_alter_directory_t */ +static svn_error_t * +alter_directory_cb(void *baton, + const char *relpath, + svn_revnum_t revision, + const apr_array_header_t *children, + apr_hash_t *props, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_alter_directory(eb->inner, relpath, revision, + children, props)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_alter_file_t */ +static svn_error_t * +alter_file_cb(void *baton, + const char *relpath, + svn_revnum_t revision, + apr_hash_t *props, + const svn_checksum_t *checksum, + svn_stream_t *contents, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_alter_file(eb->inner, relpath, revision, props, + checksum, contents)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_alter_symlink_t */ +static svn_error_t * +alter_symlink_cb(void *baton, + const char *relpath, + svn_revnum_t revision, + apr_hash_t *props, + const char *target, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_alter_symlink(eb->inner, relpath, revision, props, + target)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_delete_t */ +static svn_error_t * +delete_cb(void *baton, + const char *relpath, + svn_revnum_t revision, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_delete(eb->inner, relpath, revision)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_copy_t */ +static svn_error_t * +copy_cb(void *baton, + const char *src_relpath, + svn_revnum_t src_revision, + const char *dst_relpath, + svn_revnum_t replaces_rev, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_copy(eb->inner, src_relpath, src_revision, dst_relpath, + replaces_rev)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_move_t */ +static svn_error_t * +move_cb(void *baton, + const char *src_relpath, + svn_revnum_t src_revision, + const char *dst_relpath, + svn_revnum_t replaces_rev, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_move(eb->inner, src_relpath, src_revision, dst_relpath, + replaces_rev)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_rotate_t */ +static svn_error_t * +rotate_cb(void *baton, + const apr_array_header_t *relpaths, + const apr_array_header_t *revisions, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_rotate(eb->inner, relpaths, revisions)); + return SVN_NO_ERROR; +} + + +/* This implements svn_editor_cb_complete_t */ +static svn_error_t * +complete_cb(void *baton, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + svn_revnum_t revision; + svn_error_t *post_commit_err; + const char *conflict_path; + svn_error_t *err; + const char *post_commit_errstr; + apr_hash_t *hooks_env; + + /* Parse the hooks-env file (if any). */ + SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, eb->repos->hooks_env_path, + scratch_pool, scratch_pool)); + + /* The transaction has been fully edited. Let the pre-commit hook + have a look at the thing. */ + SVN_ERR(svn_repos__hooks_pre_commit(eb->repos, hooks_env, + eb->txn_name, scratch_pool)); + + /* Hook is done. Let's do the actual commit. */ + SVN_ERR(svn_fs__editor_commit(&revision, &post_commit_err, &conflict_path, + eb->inner, scratch_pool, scratch_pool)); + + /* Did a conflict occur during the commit process? */ + if (conflict_path != NULL) + return svn_error_createf(SVN_ERR_FS_CONFLICT, NULL, + _("Conflict at '%s'"), conflict_path); + + /* Since did not receive an error during the commit process, and no + conflict was specified... we committed a revision. Run the hooks. + Other errors may have occurred within the FS (specified by the + POST_COMMIT_ERR localvar), but we need to run the hooks. */ + SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(revision)); + err = svn_repos__hooks_post_commit(eb->repos, hooks_env, revision, + eb->txn_name, scratch_pool); + if (err) + err = svn_error_create(SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED, err, + _("Commit succeeded, but post-commit hook failed")); + + /* Combine the FS errors with the hook errors, and stringify. */ + err = svn_error_compose_create(post_commit_err, err); + if (err) + { + post_commit_errstr = svn_repos__post_commit_error_str(err, scratch_pool); + svn_error_clear(err); + } + else + { + post_commit_errstr = NULL; + } + + return svn_error_trace(invoke_commit_cb(eb->commit_cb, eb->commit_baton, + eb->repos->fs, revision, + post_commit_errstr, + scratch_pool)); +} + + +/* This implements svn_editor_cb_abort_t */ +static svn_error_t * +abort_cb(void *baton, + apr_pool_t *scratch_pool) +{ + struct ev2_baton *eb = baton; + + SVN_ERR(svn_editor_abort(eb->inner)); + return SVN_NO_ERROR; +} + + +static svn_error_t * +apply_revprops(svn_fs_t *fs, + const char *txn_name, + apr_hash_t *revprops, + apr_pool_t *scratch_pool) +{ + svn_fs_txn_t *txn; + const apr_array_header_t *revprops_array; + + /* The FS editor has a TXN inside it, but we can't access it. Open another + based on the TXN_NAME. */ + SVN_ERR(svn_fs_open_txn(&txn, fs, txn_name, scratch_pool)); + + /* Validate and apply the revision properties. */ + revprops_array = svn_prop_hash_to_array(revprops, scratch_pool); + SVN_ERR(svn_repos_fs_change_txn_props(txn, revprops_array, scratch_pool)); + + /* ### do we need to force the txn to close, or is it enough to wait + ### for the pool to be cleared? */ + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos__get_commit_ev2(svn_editor_t **editor, + svn_repos_t *repos, + svn_authz_t *authz, + const char *authz_repos_name, + const char *authz_user, + apr_hash_t *revprops, + svn_commit_callback2_t commit_cb, + void *commit_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + static const svn_editor_cb_many_t editor_cbs = { + add_directory_cb, + add_file_cb, + add_symlink_cb, + add_absent_cb, + alter_directory_cb, + alter_file_cb, + alter_symlink_cb, + delete_cb, + copy_cb, + move_cb, + rotate_cb, + complete_cb, + abort_cb + }; + struct ev2_baton *eb; + const svn_string_t *author; + apr_hash_t *hooks_env; + + /* Parse the hooks-env file (if any). */ + SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, repos->hooks_env_path, + scratch_pool, scratch_pool)); + + /* Can the user modify the repository at all? */ + /* ### check against AUTHZ. */ + + author = svn_hash_gets(revprops, SVN_PROP_REVISION_AUTHOR); + + eb = apr_palloc(result_pool, sizeof(*eb)); + eb->repos = repos; + eb->authz = authz; + eb->authz_repos_name = authz_repos_name; + eb->authz_user = authz_user; + eb->commit_cb = commit_cb; + eb->commit_baton = commit_baton; + + SVN_ERR(svn_fs__editor_create(&eb->inner, &eb->txn_name, + repos->fs, SVN_FS_TXN_CHECK_LOCKS, + cancel_func, cancel_baton, + result_pool, scratch_pool)); + + /* The TXN has been created. Go ahead and apply all revision properties. */ + SVN_ERR(apply_revprops(repos->fs, eb->txn_name, revprops, scratch_pool)); + + /* Okay... some access is allowed. Let's run the start-commit hook. */ + SVN_ERR(svn_repos__hooks_start_commit(repos, hooks_env, + author ? author->data : NULL, + repos->client_capabilities, + eb->txn_name, scratch_pool)); + + /* Wrap the FS editor within our editor. */ + SVN_ERR(svn_editor_create(editor, eb, cancel_func, cancel_baton, + result_pool, scratch_pool)); + SVN_ERR(svn_editor_setcb_many(*editor, &editor_cbs, scratch_pool)); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_repos/delta.c b/subversion/libsvn_repos/delta.c new file mode 100644 index 0000000..51cfda7 --- /dev/null +++ b/subversion/libsvn_repos/delta.c @@ -0,0 +1,1074 @@ +/* + * delta.c: an editor driver for expressing differences between two trees + * + * ==================================================================== + * 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_hash.h> + +#include "svn_hash.h" +#include "svn_types.h" +#include "svn_delta.h" +#include "svn_fs.h" +#include "svn_checksum.h" +#include "svn_path.h" +#include "svn_repos.h" +#include "svn_pools.h" +#include "svn_props.h" +#include "svn_private_config.h" +#include "repos.h" + + + +/* THINGS TODO: Currently the code herein gives only a slight nod to + fully supporting directory deltas that involve renames, copies, and + such. */ + + +/* Some datatypes and declarations used throughout the file. */ + + +/* Parameters which remain constant throughout a delta traversal. + At the top of the recursion, we initialize one of these structures. + Then we pass it down to every call. This way, functions invoked + deep in the recursion can get access to this traversal's global + parameters, without using global variables. */ +struct context { + const svn_delta_editor_t *editor; + const char *edit_base_path; + svn_fs_root_t *source_root; + svn_fs_root_t *target_root; + svn_repos_authz_func_t authz_read_func; + void *authz_read_baton; + svn_boolean_t text_deltas; + svn_boolean_t entry_props; + svn_boolean_t ignore_ancestry; +}; + + +/* The type of a function that accepts changes to an object's property + list. OBJECT is the object whose properties are being changed. + NAME is the name of the property to change. VALUE is the new value + for the property, or zero if the property should be deleted. */ +typedef svn_error_t *proplist_change_fn_t(struct context *c, + void *object, + const char *name, + const svn_string_t *value, + apr_pool_t *pool); + + + +/* Some prototypes for functions used throughout. See each individual + function for information about what it does. */ + + +/* Retrieving the base revision from the path/revision hash. */ +static svn_revnum_t get_path_revision(svn_fs_root_t *root, + const char *path, + apr_pool_t *pool); + + +/* proplist_change_fn_t property changing functions. */ +static svn_error_t *change_dir_prop(struct context *c, + void *object, + const char *path, + const svn_string_t *value, + apr_pool_t *pool); + +static svn_error_t *change_file_prop(struct context *c, + void *object, + const char *path, + const svn_string_t *value, + apr_pool_t *pool); + + +/* Constructing deltas for properties of files and directories. */ +static svn_error_t *delta_proplists(struct context *c, + const char *source_path, + const char *target_path, + proplist_change_fn_t *change_fn, + void *object, + apr_pool_t *pool); + + +/* Constructing deltas for file constents. */ +static svn_error_t *send_text_delta(struct context *c, + void *file_baton, + const char *base_checksum, + svn_txdelta_stream_t *delta_stream, + apr_pool_t *pool); + +static svn_error_t *delta_files(struct context *c, + void *file_baton, + const char *source_path, + const char *target_path, + apr_pool_t *pool); + + +/* Generic directory deltafication routines. */ +static svn_error_t *delete(struct context *c, + void *dir_baton, + const char *edit_path, + apr_pool_t *pool); + +static svn_error_t *add_file_or_dir(struct context *c, + void *dir_baton, + svn_depth_t depth, + const char *target_path, + const char *edit_path, + svn_node_kind_t tgt_kind, + apr_pool_t *pool); + +static svn_error_t *replace_file_or_dir(struct context *c, + void *dir_baton, + svn_depth_t depth, + const char *source_path, + const char *target_path, + const char *edit_path, + svn_node_kind_t tgt_kind, + apr_pool_t *pool); + +static svn_error_t *absent_file_or_dir(struct context *c, + void *dir_baton, + const char *edit_path, + svn_node_kind_t tgt_kind, + apr_pool_t *pool); + +static svn_error_t *delta_dirs(struct context *c, + void *dir_baton, + svn_depth_t depth, + const char *source_path, + const char *target_path, + const char *edit_path, + apr_pool_t *pool); + + + +#define MAYBE_DEMOTE_DEPTH(depth) \ + (((depth) == svn_depth_immediates || (depth) == svn_depth_files) \ + ? svn_depth_empty \ + : (depth)) + + +/* Return the error 'SVN_ERR_AUTHZ_ROOT_UNREADABLE' if PATH in ROOT is + * unreadable according to AUTHZ_READ_FUNC with AUTHZ_READ_BATON. + * + * PATH should be the implicit root path of an editor drive, that is, + * the path used by editor->open_root(). + */ +static svn_error_t * +authz_root_check(svn_fs_root_t *root, + const char *path, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + svn_boolean_t allowed; + + if (authz_read_func) + { + SVN_ERR(authz_read_func(&allowed, root, path, authz_read_baton, pool)); + + if (! allowed) + return svn_error_create(SVN_ERR_AUTHZ_ROOT_UNREADABLE, 0, + _("Unable to open root of edit")); + } + + return SVN_NO_ERROR; +} + + +static svn_error_t * +not_a_dir_error(const char *role, + const char *path) +{ + return svn_error_createf + (SVN_ERR_FS_NOT_DIRECTORY, 0, + "Invalid %s directory '%s'", + role, path ? path : "(null)"); +} + + +/* Public interface to computing directory deltas. */ +svn_error_t * +svn_repos_dir_delta2(svn_fs_root_t *src_root, + const char *src_parent_dir, + const char *src_entry, + svn_fs_root_t *tgt_root, + const char *tgt_fullpath, + const svn_delta_editor_t *editor, + void *edit_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_boolean_t text_deltas, + svn_depth_t depth, + svn_boolean_t entry_props, + svn_boolean_t ignore_ancestry, + apr_pool_t *pool) +{ + void *root_baton = NULL; + struct context c; + const char *src_fullpath; + const svn_fs_id_t *src_id, *tgt_id; + svn_node_kind_t src_kind, tgt_kind; + svn_revnum_t rootrev; + int distance; + const char *authz_root_path; + + /* SRC_PARENT_DIR must be valid. */ + if (src_parent_dir) + src_parent_dir = svn_relpath_canonicalize(src_parent_dir, pool); + else + return not_a_dir_error("source parent", src_parent_dir); + + /* TGT_FULLPATH must be valid. */ + if (tgt_fullpath) + tgt_fullpath = svn_relpath_canonicalize(tgt_fullpath, pool); + else + return svn_error_create(SVN_ERR_FS_PATH_SYNTAX, 0, + _("Invalid target path")); + + if (depth == svn_depth_exclude) + return svn_error_create(SVN_ERR_REPOS_BAD_ARGS, NULL, + _("Delta depth 'exclude' not supported")); + + /* Calculate the fs path implicitly used for editor->open_root, so + we can do an authz check on that path first. */ + if (*src_entry) + authz_root_path = svn_relpath_dirname(tgt_fullpath, pool); + else + authz_root_path = tgt_fullpath; + + /* Construct the full path of the source item. */ + src_fullpath = svn_relpath_join(src_parent_dir, src_entry, pool); + + /* Get the node kinds for the source and target paths. */ + SVN_ERR(svn_fs_check_path(&tgt_kind, tgt_root, tgt_fullpath, pool)); + SVN_ERR(svn_fs_check_path(&src_kind, src_root, src_fullpath, pool)); + + /* If neither of our paths exists, we don't really have anything to do. */ + if ((tgt_kind == svn_node_none) && (src_kind == svn_node_none)) + goto cleanup; + + /* If either the source or the target is a non-directory, we + require that a SRC_ENTRY be supplied. */ + if ((! *src_entry) && ((src_kind != svn_node_dir) + || tgt_kind != svn_node_dir)) + return svn_error_create + (SVN_ERR_FS_PATH_SYNTAX, 0, + _("Invalid editor anchoring; at least one of the " + "input paths is not a directory and there was no source entry")); + + /* Set the global target revision if one can be determined. */ + if (svn_fs_is_revision_root(tgt_root)) + { + SVN_ERR(editor->set_target_revision + (edit_baton, svn_fs_revision_root_revision(tgt_root), pool)); + } + else if (svn_fs_is_txn_root(tgt_root)) + { + SVN_ERR(editor->set_target_revision + (edit_baton, svn_fs_txn_root_base_revision(tgt_root), pool)); + } + + /* Setup our pseudo-global structure here. We need these variables + throughout the deltafication process, so pass them around by + reference to all the helper functions. */ + c.editor = editor; + c.source_root = src_root; + c.target_root = tgt_root; + c.authz_read_func = authz_read_func; + c.authz_read_baton = authz_read_baton; + c.text_deltas = text_deltas; + c.entry_props = entry_props; + c.ignore_ancestry = ignore_ancestry; + + /* Get our editor root's revision. */ + rootrev = get_path_revision(src_root, src_parent_dir, pool); + + /* If one or the other of our paths doesn't exist, we have to handle + those cases specially. */ + if (tgt_kind == svn_node_none) + { + /* Caller thinks that target still exists, but it doesn't. + So transform their source path to "nothing" by deleting it. */ + SVN_ERR(authz_root_check(tgt_root, authz_root_path, + authz_read_func, authz_read_baton, pool)); + SVN_ERR(editor->open_root(edit_baton, rootrev, pool, &root_baton)); + SVN_ERR(delete(&c, root_baton, src_entry, pool)); + goto cleanup; + } + if (src_kind == svn_node_none) + { + /* The source path no longer exists, but the target does. + So transform "nothing" into "something" by adding. */ + SVN_ERR(authz_root_check(tgt_root, authz_root_path, + authz_read_func, authz_read_baton, pool)); + SVN_ERR(editor->open_root(edit_baton, rootrev, pool, &root_baton)); + SVN_ERR(add_file_or_dir(&c, root_baton, depth, tgt_fullpath, + src_entry, tgt_kind, pool)); + goto cleanup; + } + + /* Get and compare the node IDs for the source and target. */ + SVN_ERR(svn_fs_node_id(&tgt_id, tgt_root, tgt_fullpath, pool)); + SVN_ERR(svn_fs_node_id(&src_id, src_root, src_fullpath, pool)); + distance = svn_fs_compare_ids(src_id, tgt_id); + + if (distance == 0) + { + /* They are the same node! No-op (you gotta love those). */ + goto cleanup; + } + else if (*src_entry) + { + /* If the nodes have different kinds, we must delete the one and + add the other. Also, if they are completely unrelated and + our caller is interested in relatedness, we do the same thing. */ + if ((src_kind != tgt_kind) + || ((distance == -1) && (! ignore_ancestry))) + { + SVN_ERR(authz_root_check(tgt_root, authz_root_path, + authz_read_func, authz_read_baton, pool)); + SVN_ERR(editor->open_root(edit_baton, rootrev, pool, &root_baton)); + SVN_ERR(delete(&c, root_baton, src_entry, pool)); + SVN_ERR(add_file_or_dir(&c, root_baton, depth, tgt_fullpath, + src_entry, tgt_kind, pool)); + } + /* Otherwise, we just replace the one with the other. */ + else + { + SVN_ERR(authz_root_check(tgt_root, authz_root_path, + authz_read_func, authz_read_baton, pool)); + SVN_ERR(editor->open_root(edit_baton, rootrev, pool, &root_baton)); + SVN_ERR(replace_file_or_dir(&c, root_baton, depth, src_fullpath, + tgt_fullpath, src_entry, + tgt_kind, pool)); + } + } + else + { + /* There is no entry given, so delta the whole parent directory. */ + SVN_ERR(authz_root_check(tgt_root, authz_root_path, + authz_read_func, authz_read_baton, pool)); + SVN_ERR(editor->open_root(edit_baton, rootrev, pool, &root_baton)); + SVN_ERR(delta_dirs(&c, root_baton, depth, src_fullpath, + tgt_fullpath, "", pool)); + } + + cleanup: + + /* Make sure we close the root directory if we opened one above. */ + if (root_baton) + SVN_ERR(editor->close_directory(root_baton, pool)); + + /* Close the edit. */ + return editor->close_edit(edit_baton, pool); +} + + +/* Retrieving the base revision from the path/revision hash. */ + + +static svn_revnum_t +get_path_revision(svn_fs_root_t *root, + const char *path, + apr_pool_t *pool) +{ + svn_revnum_t revision; + svn_error_t *err; + + /* Easy out -- if ROOT is a revision root, we can use the revision + that it's a root of. */ + if (svn_fs_is_revision_root(root)) + return svn_fs_revision_root_revision(root); + + /* Else, this must be a transaction root, so ask the filesystem in + what revision this path was created. */ + if ((err = svn_fs_node_created_rev(&revision, root, path, pool))) + { + revision = SVN_INVALID_REVNUM; + svn_error_clear(err); + } + + /* If we don't get back a valid revision, this path is mutable in + the transaction. We should probably examine the node on which it + is based, doable by querying for the node-id of the path, and + then examining that node-id's predecessor. ### This predecessor + determination isn't exposed via the FS public API right now, so + for now, we'll just return the SVN_INVALID_REVNUM. */ + return revision; +} + + +/* proplist_change_fn_t property changing functions. */ + + +/* Call the directory property-setting function of C->editor to set + the property NAME to given VALUE on the OBJECT passed to this + function. */ +static svn_error_t * +change_dir_prop(struct context *c, + void *object, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + return c->editor->change_dir_prop(object, name, value, pool); +} + + +/* Call the file property-setting function of C->editor to set the + property NAME to given VALUE on the OBJECT passed to this + function. */ +static svn_error_t * +change_file_prop(struct context *c, + void *object, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + return c->editor->change_file_prop(object, name, value, pool); +} + + + + +/* Constructing deltas for properties of files and directories. */ + + +/* Generate the appropriate property editing calls to turn the + properties of SOURCE_PATH into those of TARGET_PATH. If + SOURCE_PATH is NULL, this is an add, so assume the target starts + with no properties. Pass OBJECT on to the editor function wrapper + CHANGE_FN. */ +static svn_error_t * +delta_proplists(struct context *c, + const char *source_path, + const char *target_path, + proplist_change_fn_t *change_fn, + void *object, + apr_pool_t *pool) +{ + apr_hash_t *s_props = 0; + apr_hash_t *t_props = 0; + apr_pool_t *subpool; + apr_array_header_t *prop_diffs; + int i; + + SVN_ERR_ASSERT(target_path); + + /* Make a subpool for local allocations. */ + subpool = svn_pool_create(pool); + + /* If we're supposed to send entry props for all non-deleted items, + here we go! */ + if (c->entry_props) + { + svn_revnum_t committed_rev = SVN_INVALID_REVNUM; + svn_string_t *cr_str = NULL; + svn_string_t *committed_date = NULL; + svn_string_t *last_author = NULL; + + /* Get the CR and two derivative props. ### check for error returns. */ + SVN_ERR(svn_fs_node_created_rev(&committed_rev, c->target_root, + target_path, subpool)); + if (SVN_IS_VALID_REVNUM(committed_rev)) + { + svn_fs_t *fs = svn_fs_root_fs(c->target_root); + apr_hash_t *r_props; + const char *uuid; + + /* Transmit the committed-rev. */ + cr_str = svn_string_createf(subpool, "%ld", + committed_rev); + SVN_ERR(change_fn(c, object, SVN_PROP_ENTRY_COMMITTED_REV, + cr_str, subpool)); + + SVN_ERR(svn_fs_revision_proplist(&r_props, fs, committed_rev, + pool)); + + /* Transmit the committed-date. */ + committed_date = svn_hash_gets(r_props, SVN_PROP_REVISION_DATE); + if (committed_date || source_path) + { + SVN_ERR(change_fn(c, object, SVN_PROP_ENTRY_COMMITTED_DATE, + committed_date, subpool)); + } + + /* Transmit the last-author. */ + last_author = svn_hash_gets(r_props, SVN_PROP_REVISION_AUTHOR); + if (last_author || source_path) + { + SVN_ERR(change_fn(c, object, SVN_PROP_ENTRY_LAST_AUTHOR, + last_author, subpool)); + } + + /* Transmit the UUID. */ + SVN_ERR(svn_fs_get_uuid(fs, &uuid, subpool)); + SVN_ERR(change_fn(c, object, SVN_PROP_ENTRY_UUID, + svn_string_create(uuid, subpool), + subpool)); + } + } + + if (source_path) + { + svn_boolean_t changed; + + /* Is this deltification worth our time? */ + SVN_ERR(svn_fs_props_changed(&changed, c->target_root, target_path, + c->source_root, source_path, subpool)); + if (! changed) + goto cleanup; + + /* If so, go ahead and get the source path's properties. */ + SVN_ERR(svn_fs_node_proplist(&s_props, c->source_root, + source_path, subpool)); + } + else + { + s_props = apr_hash_make(subpool); + } + + /* Get the target path's properties */ + SVN_ERR(svn_fs_node_proplist(&t_props, c->target_root, + target_path, subpool)); + + /* Now transmit the differences. */ + SVN_ERR(svn_prop_diffs(&prop_diffs, t_props, s_props, subpool)); + for (i = 0; i < prop_diffs->nelts; i++) + { + const svn_prop_t *pc = &APR_ARRAY_IDX(prop_diffs, i, svn_prop_t); + SVN_ERR(change_fn(c, object, pc->name, pc->value, subpool)); + } + + cleanup: + /* Destroy local subpool. */ + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + + + + +/* Constructing deltas for file contents. */ + + +/* Change the contents of FILE_BATON in C->editor, according to the + text delta from DELTA_STREAM. Pass BASE_CHECKSUM along to + C->editor->apply_textdelta. */ +static svn_error_t * +send_text_delta(struct context *c, + void *file_baton, + const char *base_checksum, + svn_txdelta_stream_t *delta_stream, + apr_pool_t *pool) +{ + svn_txdelta_window_handler_t delta_handler; + void *delta_handler_baton; + + /* Get a handler that will apply the delta to the file. */ + SVN_ERR(c->editor->apply_textdelta + (file_baton, base_checksum, pool, + &delta_handler, &delta_handler_baton)); + + if (c->text_deltas && delta_stream) + { + /* Deliver the delta stream to the file. */ + return svn_txdelta_send_txstream(delta_stream, + delta_handler, + delta_handler_baton, + pool); + } + else + { + /* The caller doesn't want text delta data. Just send a single + NULL window. */ + return delta_handler(NULL, delta_handler_baton); + } +} + +svn_error_t * +svn_repos__compare_files(svn_boolean_t *changed_p, + svn_fs_root_t *root1, + const char *path1, + svn_fs_root_t *root2, + const char *path2, + apr_pool_t *pool) +{ + svn_filesize_t size1, size2; + svn_checksum_t *checksum1, *checksum2; + svn_stream_t *stream1, *stream2; + svn_boolean_t same; + + /* If the filesystem claims the things haven't changed, then they + haven't changed. */ + SVN_ERR(svn_fs_contents_changed(changed_p, root1, path1, + root2, path2, pool)); + if (!*changed_p) + return SVN_NO_ERROR; + + /* If the SHA1 checksums match for these things, we'll claim they + have the same contents. (We don't give quite as much weight to + MD5 checksums.) */ + SVN_ERR(svn_fs_file_checksum(&checksum1, svn_checksum_sha1, + root1, path1, FALSE, pool)); + SVN_ERR(svn_fs_file_checksum(&checksum2, svn_checksum_sha1, + root2, path2, FALSE, pool)); + if (checksum1 && checksum2) + { + *changed_p = !svn_checksum_match(checksum1, checksum2); + return SVN_NO_ERROR; + } + + /* From this point on, our default answer is "Nothing's changed". */ + *changed_p = FALSE; + + /* Different filesizes means the contents are different. */ + SVN_ERR(svn_fs_file_length(&size1, root1, path1, pool)); + SVN_ERR(svn_fs_file_length(&size2, root2, path2, pool)); + if (size1 != size2) + { + *changed_p = TRUE; + return SVN_NO_ERROR; + } + + /* Different MD5 checksums means the contents are different. */ + SVN_ERR(svn_fs_file_checksum(&checksum1, svn_checksum_md5, root1, path1, + FALSE, pool)); + SVN_ERR(svn_fs_file_checksum(&checksum2, svn_checksum_md5, root2, path2, + FALSE, pool)); + if (! svn_checksum_match(checksum1, checksum2)) + { + *changed_p = TRUE; + return SVN_NO_ERROR; + } + + /* And finally, different contents means the ... uh ... contents are + different. */ + SVN_ERR(svn_fs_file_contents(&stream1, root1, path1, pool)); + SVN_ERR(svn_fs_file_contents(&stream2, root2, path2, pool)); + SVN_ERR(svn_stream_contents_same2(&same, stream1, stream2, pool)); + *changed_p = !same; + + return SVN_NO_ERROR; +} + + +/* Make the appropriate edits on FILE_BATON to change its contents and + properties from those in SOURCE_PATH to those in TARGET_PATH. */ +static svn_error_t * +delta_files(struct context *c, + void *file_baton, + const char *source_path, + const char *target_path, + apr_pool_t *pool) +{ + apr_pool_t *subpool; + svn_boolean_t changed = TRUE; + + SVN_ERR_ASSERT(target_path); + + /* Make a subpool for local allocations. */ + subpool = svn_pool_create(pool); + + /* Compare the files' property lists. */ + SVN_ERR(delta_proplists(c, source_path, target_path, + change_file_prop, file_baton, subpool)); + + if (source_path) + { + /* Is this delta calculation worth our time? If we are ignoring + ancestry, then our editor implementor isn't concerned by the + theoretical differences between "has contents which have not + changed with respect to" and "has the same actual contents + as". We'll do everything we can to avoid transmitting even + an empty text-delta in that case. */ + if (c->ignore_ancestry) + SVN_ERR(svn_repos__compare_files(&changed, + c->target_root, target_path, + c->source_root, source_path, + subpool)); + else + SVN_ERR(svn_fs_contents_changed(&changed, + c->target_root, target_path, + c->source_root, source_path, + subpool)); + } + else + { + /* If there isn't a source path, this is an add, which + necessarily has textual mods. */ + } + + /* If there is a change, and the context indicates that we should + care about it, then hand it off to a delta stream. */ + if (changed) + { + svn_txdelta_stream_t *delta_stream = NULL; + svn_checksum_t *source_checksum; + const char *source_hex_digest = NULL; + + if (c->text_deltas) + { + /* Get a delta stream turning an empty file into one having + TARGET_PATH's contents. */ + SVN_ERR(svn_fs_get_file_delta_stream + (&delta_stream, + source_path ? c->source_root : NULL, + source_path ? source_path : NULL, + c->target_root, target_path, subpool)); + } + + if (source_path) + { + SVN_ERR(svn_fs_file_checksum(&source_checksum, svn_checksum_md5, + c->source_root, source_path, TRUE, + subpool)); + + source_hex_digest = svn_checksum_to_cstring(source_checksum, + subpool); + } + + SVN_ERR(send_text_delta(c, file_baton, source_hex_digest, + delta_stream, subpool)); + } + + /* Cleanup. */ + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + + + + +/* Generic directory deltafication routines. */ + + +/* Emit a delta to delete the entry named TARGET_ENTRY from DIR_BATON. */ +static svn_error_t * +delete(struct context *c, + void *dir_baton, + const char *edit_path, + apr_pool_t *pool) +{ + return c->editor->delete_entry(edit_path, SVN_INVALID_REVNUM, + dir_baton, pool); +} + + +/* If authorized, emit a delta to create the entry named TARGET_ENTRY + at the location EDIT_PATH. If not authorized, indicate that + EDIT_PATH is absent. Pass DIR_BATON through to editor functions + that require it. DEPTH is the depth from this point downward. */ +static svn_error_t * +add_file_or_dir(struct context *c, void *dir_baton, + svn_depth_t depth, + const char *target_path, + const char *edit_path, + svn_node_kind_t tgt_kind, + apr_pool_t *pool) +{ + struct context *context = c; + svn_boolean_t allowed; + + SVN_ERR_ASSERT(target_path && edit_path); + + if (c->authz_read_func) + { + SVN_ERR(c->authz_read_func(&allowed, c->target_root, target_path, + c->authz_read_baton, pool)); + if (!allowed) + return absent_file_or_dir(c, dir_baton, edit_path, tgt_kind, pool); + } + + if (tgt_kind == svn_node_dir) + { + void *subdir_baton; + + SVN_ERR(context->editor->add_directory(edit_path, dir_baton, NULL, + SVN_INVALID_REVNUM, pool, + &subdir_baton)); + SVN_ERR(delta_dirs(context, subdir_baton, MAYBE_DEMOTE_DEPTH(depth), + NULL, target_path, edit_path, pool)); + return context->editor->close_directory(subdir_baton, pool); + } + else + { + void *file_baton; + svn_checksum_t *checksum; + + SVN_ERR(context->editor->add_file(edit_path, dir_baton, + NULL, SVN_INVALID_REVNUM, pool, + &file_baton)); + SVN_ERR(delta_files(context, file_baton, NULL, target_path, pool)); + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, + context->target_root, target_path, + TRUE, pool)); + return context->editor->close_file + (file_baton, svn_checksum_to_cstring(checksum, pool), pool); + } +} + + +/* If authorized, emit a delta to modify EDIT_PATH with the changes + from SOURCE_PATH to TARGET_PATH. If not authorized, indicate that + EDIT_PATH is absent. Pass DIR_BATON through to editor functions + that require it. DEPTH is the depth from this point downward. */ +static svn_error_t * +replace_file_or_dir(struct context *c, + void *dir_baton, + svn_depth_t depth, + const char *source_path, + const char *target_path, + const char *edit_path, + svn_node_kind_t tgt_kind, + apr_pool_t *pool) +{ + svn_revnum_t base_revision = SVN_INVALID_REVNUM; + svn_boolean_t allowed; + + SVN_ERR_ASSERT(target_path && source_path && edit_path); + + if (c->authz_read_func) + { + SVN_ERR(c->authz_read_func(&allowed, c->target_root, target_path, + c->authz_read_baton, pool)); + if (!allowed) + return absent_file_or_dir(c, dir_baton, edit_path, tgt_kind, pool); + } + + /* Get the base revision for the entry from the hash. */ + base_revision = get_path_revision(c->source_root, source_path, pool); + + if (tgt_kind == svn_node_dir) + { + void *subdir_baton; + + SVN_ERR(c->editor->open_directory(edit_path, dir_baton, + base_revision, pool, + &subdir_baton)); + SVN_ERR(delta_dirs(c, subdir_baton, MAYBE_DEMOTE_DEPTH(depth), + source_path, target_path, edit_path, pool)); + return c->editor->close_directory(subdir_baton, pool); + } + else + { + void *file_baton; + svn_checksum_t *checksum; + + SVN_ERR(c->editor->open_file(edit_path, dir_baton, base_revision, + pool, &file_baton)); + SVN_ERR(delta_files(c, file_baton, source_path, target_path, pool)); + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, + c->target_root, target_path, TRUE, + pool)); + return c->editor->close_file + (file_baton, svn_checksum_to_cstring(checksum, pool), pool); + } +} + + +/* In directory DIR_BATON, indicate that EDIT_PATH (relative to the + edit root) is absent by invoking C->editor->absent_directory or + C->editor->absent_file (depending on TGT_KIND). */ +static svn_error_t * +absent_file_or_dir(struct context *c, + void *dir_baton, + const char *edit_path, + svn_node_kind_t tgt_kind, + apr_pool_t *pool) +{ + SVN_ERR_ASSERT(edit_path); + + if (tgt_kind == svn_node_dir) + return c->editor->absent_directory(edit_path, dir_baton, pool); + else + return c->editor->absent_file(edit_path, dir_baton, pool); +} + + +/* Emit deltas to turn SOURCE_PATH into TARGET_PATH. Assume that + DIR_BATON represents the directory we're constructing to the editor + in the context C. */ +static svn_error_t * +delta_dirs(struct context *c, + void *dir_baton, + svn_depth_t depth, + const char *source_path, + const char *target_path, + const char *edit_path, + apr_pool_t *pool) +{ + apr_hash_t *s_entries = 0, *t_entries = 0; + apr_hash_index_t *hi; + apr_pool_t *subpool; + + SVN_ERR_ASSERT(target_path); + + /* Compare the property lists. */ + SVN_ERR(delta_proplists(c, source_path, target_path, + change_dir_prop, dir_baton, pool)); + + /* Get the list of entries in each of source and target. */ + SVN_ERR(svn_fs_dir_entries(&t_entries, c->target_root, + target_path, pool)); + if (source_path) + SVN_ERR(svn_fs_dir_entries(&s_entries, c->source_root, + source_path, pool)); + + /* Make a subpool for local allocations. */ + subpool = svn_pool_create(pool); + + /* Loop over the hash of entries in the target, searching for its + partner in the source. If we find the matching partner entry, + use editor calls to replace the one in target with a new version + if necessary, then remove that entry from the source entries + hash. If we can't find a related node in the source, we use + editor calls to add the entry as a new item in the target. + Having handled all the entries that exist in target, any entries + still remaining the source entries hash represent entries that no + longer exist in target. Use editor calls to delete those entries + from the target tree. */ + for (hi = apr_hash_first(pool, t_entries); hi; hi = apr_hash_next(hi)) + { + const svn_fs_dirent_t *s_entry, *t_entry; + const void *key; + void *val; + apr_ssize_t klen; + const char *t_fullpath; + const char *e_fullpath; + const char *s_fullpath; + svn_node_kind_t tgt_kind; + + /* Clear out our subpool for the next iteration... */ + svn_pool_clear(subpool); + + /* KEY is the entry name in target, VAL the dirent */ + apr_hash_this(hi, &key, &klen, &val); + t_entry = val; + tgt_kind = t_entry->kind; + t_fullpath = svn_relpath_join(target_path, t_entry->name, subpool); + e_fullpath = svn_relpath_join(edit_path, t_entry->name, subpool); + + /* Can we find something with the same name in the source + entries hash? */ + if (s_entries && ((s_entry = apr_hash_get(s_entries, key, klen)) != 0)) + { + svn_node_kind_t src_kind; + + s_fullpath = svn_relpath_join(source_path, t_entry->name, subpool); + src_kind = s_entry->kind; + + if (depth == svn_depth_infinity + || src_kind != svn_node_dir + || (src_kind == svn_node_dir + && depth == svn_depth_immediates)) + { + /* Use svn_fs_compare_ids() to compare our current + source and target ids. + + 0: means they are the same id, and this is a noop. + -1: means they are unrelated, so we have to delete the + old one and add the new one. + 1: means the nodes are related through ancestry, so go + ahead and do the replace directly. */ + int distance = svn_fs_compare_ids(s_entry->id, t_entry->id); + if (distance == 0) + { + /* no-op */ + } + else if ((src_kind != tgt_kind) + || ((distance == -1) && (! c->ignore_ancestry))) + { + SVN_ERR(delete(c, dir_baton, e_fullpath, subpool)); + SVN_ERR(add_file_or_dir(c, dir_baton, + MAYBE_DEMOTE_DEPTH(depth), + t_fullpath, e_fullpath, tgt_kind, + subpool)); + } + else + { + SVN_ERR(replace_file_or_dir(c, dir_baton, + MAYBE_DEMOTE_DEPTH(depth), + s_fullpath, t_fullpath, + e_fullpath, tgt_kind, + subpool)); + } + } + + /* Remove the entry from the source_hash. */ + svn_hash_sets(s_entries, key, NULL); + } + else + { + if (depth == svn_depth_infinity + || tgt_kind != svn_node_dir + || (tgt_kind == svn_node_dir + && depth == svn_depth_immediates)) + { + SVN_ERR(add_file_or_dir(c, dir_baton, + MAYBE_DEMOTE_DEPTH(depth), + t_fullpath, e_fullpath, tgt_kind, + subpool)); + } + } + } + + /* All that is left in the source entries hash are things that need + to be deleted. Delete them. */ + if (s_entries) + { + for (hi = apr_hash_first(pool, s_entries); hi; hi = apr_hash_next(hi)) + { + const svn_fs_dirent_t *s_entry; + void *val; + const char *e_fullpath; + svn_node_kind_t src_kind; + + /* Clear out our subpool for the next iteration... */ + svn_pool_clear(subpool); + + /* KEY is the entry name in source, VAL the dirent */ + apr_hash_this(hi, NULL, NULL, &val); + s_entry = val; + src_kind = s_entry->kind; + e_fullpath = svn_relpath_join(edit_path, s_entry->name, subpool); + + /* Do we actually want to delete the dir if we're non-recursive? */ + if (depth == svn_depth_infinity + || src_kind != svn_node_dir + || (src_kind == svn_node_dir + && depth == svn_depth_immediates)) + { + SVN_ERR(delete(c, dir_baton, e_fullpath, subpool)); + } + } + } + + /* Destroy local allocation subpool. */ + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_repos/deprecated.c b/subversion/libsvn_repos/deprecated.c new file mode 100644 index 0000000..7208ba6 --- /dev/null +++ b/subversion/libsvn_repos/deprecated.c @@ -0,0 +1,1017 @@ +/* + * deprecated.c: holding file for all deprecated APIs. + * "we can't lose 'em, but we can shun 'em!" + * + * ==================================================================== + * 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. + * ==================================================================== + */ + +/* We define this here to remove any further warnings about the usage of + deprecated functions in this file. */ +#define SVN_DEPRECATED + +#include "svn_repos.h" +#include "svn_compat.h" +#include "svn_hash.h" +#include "svn_props.h" + +#include "svn_private_config.h" + +#include "repos.h" + + + + +/*** From commit.c ***/ + +svn_error_t * +svn_repos_get_commit_editor4(const svn_delta_editor_t **editor, + void **edit_baton, + svn_repos_t *repos, + svn_fs_txn_t *txn, + const char *repos_url, + const char *base_path, + const char *user, + const char *log_msg, + svn_commit_callback2_t commit_callback, + void *commit_baton, + svn_repos_authz_callback_t authz_callback, + void *authz_baton, + apr_pool_t *pool) +{ + apr_hash_t *revprop_table = apr_hash_make(pool); + if (user) + svn_hash_sets(revprop_table, SVN_PROP_REVISION_AUTHOR, + svn_string_create(user, pool)); + if (log_msg) + svn_hash_sets(revprop_table, SVN_PROP_REVISION_LOG, + svn_string_create(log_msg, pool)); + return svn_repos_get_commit_editor5(editor, edit_baton, repos, txn, + repos_url, base_path, revprop_table, + commit_callback, commit_baton, + authz_callback, authz_baton, pool); +} + + +svn_error_t * +svn_repos_get_commit_editor3(const svn_delta_editor_t **editor, + void **edit_baton, + svn_repos_t *repos, + svn_fs_txn_t *txn, + const char *repos_url, + const char *base_path, + const char *user, + const char *log_msg, + svn_commit_callback_t callback, + void *callback_baton, + svn_repos_authz_callback_t authz_callback, + void *authz_baton, + apr_pool_t *pool) +{ + svn_commit_callback2_t callback2; + void *callback2_baton; + + svn_compat_wrap_commit_callback(&callback2, &callback2_baton, + callback, callback_baton, + pool); + + return svn_repos_get_commit_editor4(editor, edit_baton, repos, txn, + repos_url, base_path, user, + log_msg, callback2, + callback2_baton, authz_callback, + authz_baton, pool); +} + + +svn_error_t * +svn_repos_get_commit_editor2(const svn_delta_editor_t **editor, + void **edit_baton, + svn_repos_t *repos, + svn_fs_txn_t *txn, + const char *repos_url, + const char *base_path, + const char *user, + const char *log_msg, + svn_commit_callback_t callback, + void *callback_baton, + apr_pool_t *pool) +{ + return svn_repos_get_commit_editor3(editor, edit_baton, repos, txn, + repos_url, base_path, user, + log_msg, callback, callback_baton, + NULL, NULL, pool); +} + + +svn_error_t * +svn_repos_get_commit_editor(const svn_delta_editor_t **editor, + void **edit_baton, + svn_repos_t *repos, + const char *repos_url, + const char *base_path, + const char *user, + const char *log_msg, + svn_commit_callback_t callback, + void *callback_baton, + apr_pool_t *pool) +{ + return svn_repos_get_commit_editor2(editor, edit_baton, repos, NULL, + repos_url, base_path, user, + log_msg, callback, + callback_baton, pool); +} + +svn_error_t * +svn_repos_open(svn_repos_t **repos_p, + const char *path, + apr_pool_t *pool) +{ + return svn_repos_open2(repos_p, path, NULL, pool); +} + + +/*** From repos.c ***/ +struct recover_baton +{ + svn_error_t *(*start_callback)(void *baton); + void *start_callback_baton; +}; + +static void +recovery_started(void *baton, + const svn_repos_notify_t *notify, + apr_pool_t *scratch_pool) +{ + struct recover_baton *rb = baton; + + if (notify->action == svn_repos_notify_mutex_acquired + && rb->start_callback != NULL) + svn_error_clear(rb->start_callback(rb->start_callback_baton)); +} + +svn_error_t * +svn_repos_recover3(const char *path, + svn_boolean_t nonblocking, + svn_error_t *(*start_callback)(void *baton), + void *start_callback_baton, + svn_cancel_func_t cancel_func, void *cancel_baton, + apr_pool_t *pool) +{ + struct recover_baton rb; + + rb.start_callback = start_callback; + rb.start_callback_baton = start_callback_baton; + + return svn_repos_recover4(path, nonblocking, recovery_started, &rb, + cancel_func, cancel_baton, pool); +} + +svn_error_t * +svn_repos_recover2(const char *path, + svn_boolean_t nonblocking, + svn_error_t *(*start_callback)(void *baton), + void *start_callback_baton, + apr_pool_t *pool) +{ + return svn_repos_recover3(path, nonblocking, + start_callback, start_callback_baton, + NULL, NULL, + pool); +} + +svn_error_t * +svn_repos_recover(const char *path, + apr_pool_t *pool) +{ + return svn_repos_recover2(path, FALSE, NULL, NULL, pool); +} + +svn_error_t * +svn_repos_upgrade(const char *path, + svn_boolean_t nonblocking, + svn_error_t *(*start_callback)(void *baton), + void *start_callback_baton, + apr_pool_t *pool) +{ + struct recover_baton rb; + + rb.start_callback = start_callback; + rb.start_callback_baton = start_callback_baton; + + return svn_repos_upgrade2(path, nonblocking, recovery_started, &rb, pool); +} + +/*** From reporter.c ***/ +svn_error_t * +svn_repos_begin_report(void **report_baton, + svn_revnum_t revnum, + const char *username, + svn_repos_t *repos, + const char *fs_base, + const char *s_operand, + const char *switch_path, + svn_boolean_t text_deltas, + svn_boolean_t recurse, + svn_boolean_t ignore_ancestry, + const svn_delta_editor_t *editor, + void *edit_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + return svn_repos_begin_report2(report_baton, + revnum, + repos, + fs_base, + s_operand, + switch_path, + text_deltas, + SVN_DEPTH_INFINITY_OR_FILES(recurse), + ignore_ancestry, + FALSE, /* don't send copyfrom args */ + editor, + edit_baton, + authz_read_func, + authz_read_baton, + pool); +} + +svn_error_t * +svn_repos_begin_report2(void **report_baton, + svn_revnum_t revnum, + svn_repos_t *repos, + const char *fs_base, + const char *target, + const char *tgt_path, + svn_boolean_t text_deltas, + svn_depth_t depth, + svn_boolean_t ignore_ancestry, + svn_boolean_t send_copyfrom_args, + const svn_delta_editor_t *editor, + void *edit_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + return svn_repos_begin_report3(report_baton, + revnum, + repos, + fs_base, + target, + tgt_path, + text_deltas, + depth, + ignore_ancestry, + send_copyfrom_args, + editor, + edit_baton, + authz_read_func, + authz_read_baton, + 0, /* disable zero-copy code path */ + pool); +} + +svn_error_t * +svn_repos_set_path2(void *baton, const char *path, svn_revnum_t rev, + svn_boolean_t start_empty, const char *lock_token, + apr_pool_t *pool) +{ + return svn_repos_set_path3(baton, path, rev, svn_depth_infinity, + start_empty, lock_token, pool); +} + +svn_error_t * +svn_repos_set_path(void *baton, const char *path, svn_revnum_t rev, + svn_boolean_t start_empty, apr_pool_t *pool) +{ + return svn_repos_set_path2(baton, path, rev, start_empty, NULL, pool); +} + +svn_error_t * +svn_repos_link_path2(void *baton, const char *path, const char *link_path, + svn_revnum_t rev, svn_boolean_t start_empty, + const char *lock_token, apr_pool_t *pool) +{ + return svn_repos_link_path3(baton, path, link_path, rev, svn_depth_infinity, + start_empty, lock_token, pool); +} + +svn_error_t * +svn_repos_link_path(void *baton, const char *path, const char *link_path, + svn_revnum_t rev, svn_boolean_t start_empty, + apr_pool_t *pool) +{ + return svn_repos_link_path2(baton, path, link_path, rev, start_empty, + NULL, pool); +} + +/*** From dir-delta.c ***/ +svn_error_t * +svn_repos_dir_delta(svn_fs_root_t *src_root, + const char *src_parent_dir, + const char *src_entry, + svn_fs_root_t *tgt_root, + const char *tgt_fullpath, + const svn_delta_editor_t *editor, + void *edit_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_boolean_t text_deltas, + svn_boolean_t recurse, + svn_boolean_t entry_props, + svn_boolean_t ignore_ancestry, + apr_pool_t *pool) +{ + return svn_repos_dir_delta2(src_root, + src_parent_dir, + src_entry, + tgt_root, + tgt_fullpath, + editor, + edit_baton, + authz_read_func, + authz_read_baton, + text_deltas, + SVN_DEPTH_INFINITY_OR_FILES(recurse), + entry_props, + ignore_ancestry, + pool); +} + +/*** From replay.c ***/ +svn_error_t * +svn_repos_replay(svn_fs_root_t *root, + const svn_delta_editor_t *editor, + void *edit_baton, + apr_pool_t *pool) +{ + return svn_repos_replay2(root, + "" /* the whole tree */, + SVN_INVALID_REVNUM, /* no low water mark */ + FALSE /* no text deltas */, + editor, edit_baton, + NULL /* no authz func */, + NULL /* no authz baton */, + pool); +} + +/*** From fs-wrap.c ***/ +svn_error_t * +svn_repos_fs_change_rev_prop3(svn_repos_t *repos, + svn_revnum_t rev, + const char *author, + const char *name, + const svn_string_t *new_value, + svn_boolean_t use_pre_revprop_change_hook, + svn_boolean_t use_post_revprop_change_hook, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + return svn_repos_fs_change_rev_prop4(repos, rev, author, name, NULL, + new_value, + use_pre_revprop_change_hook, + use_post_revprop_change_hook, + authz_read_func, + authz_read_baton, pool); +} + +svn_error_t * +svn_repos_fs_change_rev_prop2(svn_repos_t *repos, + svn_revnum_t rev, + const char *author, + const char *name, + const svn_string_t *new_value, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + return svn_repos_fs_change_rev_prop3(repos, rev, author, name, new_value, + TRUE, TRUE, authz_read_func, + authz_read_baton, pool); +} + + + +svn_error_t * +svn_repos_fs_change_rev_prop(svn_repos_t *repos, + svn_revnum_t rev, + const char *author, + const char *name, + const svn_string_t *new_value, + apr_pool_t *pool) +{ + return svn_repos_fs_change_rev_prop2(repos, rev, author, name, new_value, + NULL, NULL, pool); +} + +struct pack_notify_wrapper_baton +{ + svn_fs_pack_notify_t notify_func; + void *notify_baton; +}; + +static void +pack_notify_wrapper_func(void *baton, + const svn_repos_notify_t *notify, + apr_pool_t *scratch_pool) +{ + struct pack_notify_wrapper_baton *pnwb = baton; + + svn_error_clear(pnwb->notify_func(pnwb->notify_baton, notify->shard, + notify->action - 3, scratch_pool)); +} + +svn_error_t * +svn_repos_fs_pack(svn_repos_t *repos, + svn_fs_pack_notify_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + struct pack_notify_wrapper_baton pnwb; + + pnwb.notify_func = notify_func; + pnwb.notify_baton = notify_baton; + + return svn_repos_fs_pack2(repos, pack_notify_wrapper_func, &pnwb, + cancel_func, cancel_baton, pool); +} + + +svn_error_t * +svn_repos_fs_get_locks(apr_hash_t **locks, + svn_repos_t *repos, + const char *path, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + return svn_error_trace(svn_repos_fs_get_locks2(locks, repos, path, + svn_depth_infinity, + authz_read_func, + authz_read_baton, pool)); +} + + +/*** From logs.c ***/ +svn_error_t * +svn_repos_get_logs3(svn_repos_t *repos, + const apr_array_header_t *paths, + svn_revnum_t start, + svn_revnum_t end, + int limit, + svn_boolean_t discover_changed_paths, + svn_boolean_t strict_node_history, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_log_message_receiver_t receiver, + void *receiver_baton, + apr_pool_t *pool) +{ + svn_log_entry_receiver_t receiver2; + void *receiver2_baton; + + svn_compat_wrap_log_receiver(&receiver2, &receiver2_baton, + receiver, receiver_baton, + pool); + + return svn_repos_get_logs4(repos, paths, start, end, limit, + discover_changed_paths, strict_node_history, + FALSE, svn_compat_log_revprops_in(pool), + authz_read_func, authz_read_baton, + receiver2, receiver2_baton, + pool); +} + +svn_error_t * +svn_repos_get_logs2(svn_repos_t *repos, + const apr_array_header_t *paths, + svn_revnum_t start, + svn_revnum_t end, + svn_boolean_t discover_changed_paths, + svn_boolean_t strict_node_history, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_log_message_receiver_t receiver, + void *receiver_baton, + apr_pool_t *pool) +{ + return svn_repos_get_logs3(repos, paths, start, end, 0, + discover_changed_paths, strict_node_history, + authz_read_func, authz_read_baton, receiver, + receiver_baton, pool); +} + + +svn_error_t * +svn_repos_get_logs(svn_repos_t *repos, + const apr_array_header_t *paths, + svn_revnum_t start, + svn_revnum_t end, + svn_boolean_t discover_changed_paths, + svn_boolean_t strict_node_history, + svn_log_message_receiver_t receiver, + void *receiver_baton, + apr_pool_t *pool) +{ + return svn_repos_get_logs3(repos, paths, start, end, 0, + discover_changed_paths, strict_node_history, + NULL, NULL, /* no authz stuff */ + receiver, receiver_baton, pool); +} + +/*** From rev_hunt.c ***/ +svn_error_t * +svn_repos_history(svn_fs_t *fs, + const char *path, + svn_repos_history_func_t history_func, + void *history_baton, + svn_revnum_t start, + svn_revnum_t end, + svn_boolean_t cross_copies, + apr_pool_t *pool) +{ + return svn_repos_history2(fs, path, history_func, history_baton, + NULL, NULL, + start, end, cross_copies, pool); +} + +svn_error_t * +svn_repos_get_file_revs(svn_repos_t *repos, + const char *path, + svn_revnum_t start, + svn_revnum_t end, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_repos_file_rev_handler_t handler, + void *handler_baton, + apr_pool_t *pool) +{ + svn_file_rev_handler_t handler2; + void *handler2_baton; + + svn_compat_wrap_file_rev_handler(&handler2, &handler2_baton, handler, + handler_baton, pool); + + return svn_repos_get_file_revs2(repos, path, start, end, FALSE, + authz_read_func, authz_read_baton, + handler2, handler2_baton, pool); +} + +/*** From dump.c ***/ +svn_error_t * +svn_repos_dump_fs(svn_repos_t *repos, + svn_stream_t *stream, + svn_stream_t *feedback_stream, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_boolean_t incremental, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + return svn_repos_dump_fs2(repos, stream, feedback_stream, start_rev, + end_rev, incremental, FALSE, cancel_func, + cancel_baton, pool); +} + +/* Implementation of svn_repos_notify_func_t to wrap the output to a + response stream for svn_repos_dump_fs2() and svn_repos_verify_fs() */ +static void +repos_notify_handler(void *baton, + const svn_repos_notify_t *notify, + apr_pool_t *scratch_pool) +{ + svn_stream_t *feedback_stream = baton; + apr_size_t len; + + switch (notify->action) + { + case svn_repos_notify_warning: + svn_error_clear(svn_stream_puts(feedback_stream, notify->warning_str)); + return; + + case svn_repos_notify_dump_rev_end: + svn_error_clear(svn_stream_printf(feedback_stream, scratch_pool, + _("* Dumped revision %ld.\n"), + notify->revision)); + return; + + case svn_repos_notify_verify_rev_end: + svn_error_clear(svn_stream_printf(feedback_stream, scratch_pool, + _("* Verified revision %ld.\n"), + notify->revision)); + return; + + case svn_repos_notify_load_txn_committed: + if (notify->old_revision == SVN_INVALID_REVNUM) + { + svn_error_clear(svn_stream_printf(feedback_stream, scratch_pool, + _("\n------- Committed revision %ld >>>\n\n"), + notify->new_revision)); + } + else + { + svn_error_clear(svn_stream_printf(feedback_stream, scratch_pool, + _("\n------- Committed new rev %ld" + " (loaded from original rev %ld" + ") >>>\n\n"), notify->new_revision, + notify->old_revision)); + } + return; + + case svn_repos_notify_load_node_start: + { + switch (notify->node_action) + { + case svn_node_action_change: + svn_error_clear(svn_stream_printf(feedback_stream, scratch_pool, + _(" * editing path : %s ..."), + notify->path)); + break; + + case svn_node_action_delete: + svn_error_clear(svn_stream_printf(feedback_stream, scratch_pool, + _(" * deleting path : %s ..."), + notify->path)); + break; + + case svn_node_action_add: + svn_error_clear(svn_stream_printf(feedback_stream, scratch_pool, + _(" * adding path : %s ..."), + notify->path)); + break; + + case svn_node_action_replace: + svn_error_clear(svn_stream_printf(feedback_stream, scratch_pool, + _(" * replacing path : %s ..."), + notify->path)); + break; + + } + } + return; + + case svn_repos_notify_load_node_done: + len = 7; + svn_error_clear(svn_stream_write(feedback_stream, _(" done.\n"), &len)); + return; + + case svn_repos_notify_load_copied_node: + len = 9; + svn_error_clear(svn_stream_write(feedback_stream, "COPIED...", &len)); + return; + + case svn_repos_notify_load_txn_start: + svn_error_clear(svn_stream_printf(feedback_stream, scratch_pool, + _("<<< Started new transaction, based on " + "original revision %ld\n"), + notify->old_revision)); + return; + + case svn_repos_notify_load_normalized_mergeinfo: + svn_error_clear(svn_stream_printf(feedback_stream, scratch_pool, + _(" removing '\\r' from %s ..."), + SVN_PROP_MERGEINFO)); + return; + + default: + return; + } +} + + +svn_error_t * +svn_repos_dump_fs2(svn_repos_t *repos, + svn_stream_t *stream, + svn_stream_t *feedback_stream, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_boolean_t incremental, + svn_boolean_t use_deltas, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + return svn_error_trace(svn_repos_dump_fs3(repos, + stream, + start_rev, + end_rev, + incremental, + use_deltas, + feedback_stream + ? repos_notify_handler + : NULL, + feedback_stream, + cancel_func, + cancel_baton, + pool)); +} + +svn_error_t * +svn_repos_verify_fs(svn_repos_t *repos, + svn_stream_t *feedback_stream, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + return svn_error_trace(svn_repos_verify_fs2(repos, + start_rev, + end_rev, + feedback_stream + ? repos_notify_handler + : NULL, + feedback_stream, + cancel_func, + cancel_baton, + pool)); +} + +/*** From load.c ***/ + +svn_error_t * +svn_repos_load_fs3(svn_repos_t *repos, + svn_stream_t *dumpstream, + enum svn_repos_load_uuid uuid_action, + const char *parent_dir, + svn_boolean_t use_pre_commit_hook, + svn_boolean_t use_post_commit_hook, + svn_boolean_t validate_props, + svn_repos_notify_func_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + return svn_repos_load_fs4(repos, dumpstream, + SVN_INVALID_REVNUM, SVN_INVALID_REVNUM, + uuid_action, parent_dir, + use_pre_commit_hook, use_post_commit_hook, + validate_props, notify_func, notify_baton, + cancel_func, cancel_baton, pool); +} + +svn_error_t * +svn_repos_load_fs2(svn_repos_t *repos, + svn_stream_t *dumpstream, + svn_stream_t *feedback_stream, + enum svn_repos_load_uuid uuid_action, + const char *parent_dir, + svn_boolean_t use_pre_commit_hook, + svn_boolean_t use_post_commit_hook, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + return svn_repos_load_fs3(repos, dumpstream, uuid_action, parent_dir, + use_pre_commit_hook, use_post_commit_hook, FALSE, + feedback_stream ? repos_notify_handler : NULL, + feedback_stream, cancel_func, cancel_baton, pool); +} + + +static svn_repos_parser_fns_t * +fns_from_fns2(const svn_repos_parse_fns2_t *fns2, + apr_pool_t *pool) +{ + svn_repos_parser_fns_t *fns; + + fns = apr_palloc(pool, sizeof(*fns)); + fns->new_revision_record = fns2->new_revision_record; + fns->uuid_record = fns2->uuid_record; + fns->new_node_record = fns2->new_node_record; + fns->set_revision_property = fns2->set_revision_property; + fns->set_node_property = fns2->set_node_property; + fns->remove_node_props = fns2->remove_node_props; + fns->set_fulltext = fns2->set_fulltext; + fns->close_node = fns2->close_node; + fns->close_revision = fns2->close_revision; + return fns; +} + +static svn_repos_parser_fns2_t * +fns2_from_fns3(const svn_repos_parse_fns3_t *fns3, + apr_pool_t *pool) +{ + svn_repos_parser_fns2_t *fns2; + + fns2 = apr_palloc(pool, sizeof(*fns2)); + fns2->new_revision_record = fns3->new_revision_record; + fns2->uuid_record = fns3->uuid_record; + fns2->new_node_record = fns3->new_node_record; + fns2->set_revision_property = fns3->set_revision_property; + fns2->set_node_property = fns3->set_node_property; + fns2->remove_node_props = fns3->remove_node_props; + fns2->set_fulltext = fns3->set_fulltext; + fns2->close_node = fns3->close_node; + fns2->close_revision = fns3->close_revision; + fns2->delete_node_property = fns3->delete_node_property; + fns2->apply_textdelta = fns3->apply_textdelta; + return fns2; +} + +static svn_repos_parse_fns2_t * +fns2_from_fns(const svn_repos_parser_fns_t *fns, + apr_pool_t *pool) +{ + svn_repos_parse_fns2_t *fns2; + + fns2 = apr_palloc(pool, sizeof(*fns2)); + fns2->new_revision_record = fns->new_revision_record; + fns2->uuid_record = fns->uuid_record; + fns2->new_node_record = fns->new_node_record; + fns2->set_revision_property = fns->set_revision_property; + fns2->set_node_property = fns->set_node_property; + fns2->remove_node_props = fns->remove_node_props; + fns2->set_fulltext = fns->set_fulltext; + fns2->close_node = fns->close_node; + fns2->close_revision = fns->close_revision; + fns2->delete_node_property = NULL; + fns2->apply_textdelta = NULL; + return fns2; +} + +static svn_repos_parse_fns3_t * +fns3_from_fns2(const svn_repos_parser_fns2_t *fns2, + apr_pool_t *pool) +{ + svn_repos_parse_fns3_t *fns3; + + fns3 = apr_palloc(pool, sizeof(*fns3)); + fns3->magic_header_record = NULL; + fns3->uuid_record = fns2->uuid_record; + fns3->new_revision_record = fns2->new_revision_record; + fns3->new_node_record = fns2->new_node_record; + fns3->set_revision_property = fns2->set_revision_property; + fns3->set_node_property = fns2->set_node_property; + fns3->remove_node_props = fns2->remove_node_props; + fns3->set_fulltext = fns2->set_fulltext; + fns3->close_node = fns2->close_node; + fns3->close_revision = fns2->close_revision; + fns3->delete_node_property = fns2->delete_node_property; + fns3->apply_textdelta = fns2->apply_textdelta; + return fns3; +} + +svn_error_t * +svn_repos_parse_dumpstream2(svn_stream_t *stream, + const svn_repos_parser_fns2_t *parse_fns, + void *parse_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + svn_repos_parse_fns3_t *fns3 = fns3_from_fns2(parse_fns, pool); + + return svn_repos_parse_dumpstream3(stream, fns3, parse_baton, FALSE, + cancel_func, cancel_baton, pool); +} + +svn_error_t * +svn_repos_parse_dumpstream(svn_stream_t *stream, + const svn_repos_parser_fns_t *parse_fns, + void *parse_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + svn_repos_parse_fns2_t *fns2 = fns2_from_fns(parse_fns, pool); + + return svn_repos_parse_dumpstream2(stream, fns2, parse_baton, + cancel_func, cancel_baton, pool); +} + +svn_error_t * +svn_repos_load_fs(svn_repos_t *repos, + svn_stream_t *dumpstream, + svn_stream_t *feedback_stream, + enum svn_repos_load_uuid uuid_action, + const char *parent_dir, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + return svn_repos_load_fs2(repos, dumpstream, feedback_stream, + uuid_action, parent_dir, FALSE, FALSE, + cancel_func, cancel_baton, pool); +} + +svn_error_t * +svn_repos_get_fs_build_parser3(const svn_repos_parse_fns2_t **callbacks, + void **parse_baton, + svn_repos_t *repos, + svn_boolean_t use_history, + svn_boolean_t validate_props, + enum svn_repos_load_uuid uuid_action, + const char *parent_dir, + svn_repos_notify_func_t notify_func, + void *notify_baton, + apr_pool_t *pool) +{ + const svn_repos_parse_fns3_t *fns3; + + SVN_ERR(svn_repos_get_fs_build_parser4(&fns3, parse_baton, repos, + SVN_INVALID_REVNUM, + SVN_INVALID_REVNUM, + use_history, validate_props, + uuid_action, parent_dir, + notify_func, notify_baton, pool)); + + *callbacks = fns2_from_fns3(fns3, pool); + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos_get_fs_build_parser2(const svn_repos_parse_fns2_t **parser, + void **parse_baton, + svn_repos_t *repos, + svn_boolean_t use_history, + enum svn_repos_load_uuid uuid_action, + svn_stream_t *outstream, + const char *parent_dir, + apr_pool_t *pool) +{ + return svn_repos_get_fs_build_parser3(parser, parse_baton, repos, use_history, + FALSE, uuid_action, parent_dir, + outstream ? repos_notify_handler : NULL, + outstream, pool); +} + +svn_error_t * +svn_repos_get_fs_build_parser(const svn_repos_parser_fns_t **parser_callbacks, + void **parse_baton, + svn_repos_t *repos, + svn_boolean_t use_history, + enum svn_repos_load_uuid uuid_action, + svn_stream_t *outstream, + const char *parent_dir, + apr_pool_t *pool) +{ + const svn_repos_parse_fns2_t *fns2; + + SVN_ERR(svn_repos_get_fs_build_parser2(&fns2, parse_baton, repos, + use_history, uuid_action, outstream, + parent_dir, pool)); + + *parser_callbacks = fns_from_fns2(fns2, pool); + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_fs_begin_txn_for_update(svn_fs_txn_t **txn_p, + svn_repos_t *repos, + svn_revnum_t rev, + const char *author, + apr_pool_t *pool) +{ + /* ### someday, we might run a read-hook here. */ + + /* Begin the transaction. */ + SVN_ERR(svn_fs_begin_txn2(txn_p, repos->fs, rev, 0, pool)); + + /* We pass the author to the filesystem by adding it as a property + on the txn. */ + + /* User (author). */ + if (author) + { + svn_string_t val; + val.data = author; + val.len = strlen(author); + SVN_ERR(svn_fs_change_txn_prop(*txn_p, SVN_PROP_REVISION_AUTHOR, + &val, pool)); + } + + return SVN_NO_ERROR; +} + +/*** From authz.c ***/ + +svn_error_t * +svn_repos_authz_read(svn_authz_t **authz_p, const char *file, + svn_boolean_t must_exist, apr_pool_t *pool) +{ + return svn_repos__authz_read(authz_p, file, NULL, must_exist, + FALSE, pool); +} diff --git a/subversion/libsvn_repos/dump.c b/subversion/libsvn_repos/dump.c new file mode 100644 index 0000000..75843d7 --- /dev/null +++ b/subversion/libsvn_repos/dump.c @@ -0,0 +1,1503 @@ +/* dump.c --- writing filesystem contents into a portable 'dumpfile' format. + * + * ==================================================================== + * 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 "svn_private_config.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_fs.h" +#include "svn_hash.h" +#include "svn_iter.h" +#include "svn_repos.h" +#include "svn_string.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_time.h" +#include "svn_checksum.h" +#include "svn_props.h" +#include "svn_sorts.h" + +#include "private/svn_mergeinfo_private.h" +#include "private/svn_fs_private.h" + +#define ARE_VALID_COPY_ARGS(p,r) ((p) && SVN_IS_VALID_REVNUM(r)) + +/*----------------------------------------------------------------------*/ + + + +/* Compute the delta between OLDROOT/OLDPATH and NEWROOT/NEWPATH and + store it into a new temporary file *TEMPFILE. OLDROOT may be NULL, + in which case the delta will be computed against an empty file, as + per the svn_fs_get_file_delta_stream docstring. Record the length + of the temporary file in *LEN, and rewind the file before + returning. */ +static svn_error_t * +store_delta(apr_file_t **tempfile, svn_filesize_t *len, + svn_fs_root_t *oldroot, const char *oldpath, + svn_fs_root_t *newroot, const char *newpath, apr_pool_t *pool) +{ + svn_stream_t *temp_stream; + apr_off_t offset = 0; + svn_txdelta_stream_t *delta_stream; + svn_txdelta_window_handler_t wh; + void *whb; + + /* Create a temporary file and open a stream to it. Note that we need + the file handle in order to rewind it. */ + SVN_ERR(svn_io_open_unique_file3(tempfile, NULL, NULL, + svn_io_file_del_on_pool_cleanup, + pool, pool)); + temp_stream = svn_stream_from_aprfile2(*tempfile, TRUE, pool); + + /* Compute the delta and send it to the temporary file. */ + SVN_ERR(svn_fs_get_file_delta_stream(&delta_stream, oldroot, oldpath, + newroot, newpath, pool)); + svn_txdelta_to_svndiff3(&wh, &whb, temp_stream, 0, + SVN_DELTA_COMPRESSION_LEVEL_DEFAULT, pool); + SVN_ERR(svn_txdelta_send_txstream(delta_stream, wh, whb, pool)); + + /* Get the length of the temporary file and rewind it. */ + SVN_ERR(svn_io_file_seek(*tempfile, APR_CUR, &offset, pool)); + *len = offset; + offset = 0; + return svn_io_file_seek(*tempfile, APR_SET, &offset, pool); +} + + +/*----------------------------------------------------------------------*/ + +/** An editor which dumps node-data in 'dumpfile format' to a file. **/ + +/* Look, mom! No file batons! */ + +struct edit_baton +{ + /* The relpath which implicitly prepends all full paths coming into + this editor. This will almost always be "". */ + const char *path; + + /* The stream to dump to. */ + svn_stream_t *stream; + + /* Send feedback here, if non-NULL */ + svn_repos_notify_func_t notify_func; + void *notify_baton; + + /* The fs revision root, so we can read the contents of paths. */ + svn_fs_root_t *fs_root; + svn_revnum_t current_rev; + + /* The fs, so we can grab historic information if needed. */ + svn_fs_t *fs; + + /* True if dumped nodes should output deltas instead of full text. */ + svn_boolean_t use_deltas; + + /* True if this "dump" is in fact a verify. */ + svn_boolean_t verify; + + /* The first revision dumped in this dumpstream. */ + svn_revnum_t oldest_dumped_rev; + + /* If not NULL, set to true if any references to revisions older than + OLDEST_DUMPED_REV were found in the dumpstream. */ + svn_boolean_t *found_old_reference; + + /* If not NULL, set to true if any mergeinfo was dumped which contains + revisions older than OLDEST_DUMPED_REV. */ + svn_boolean_t *found_old_mergeinfo; + + /* reusable buffer for writing file contents */ + char buffer[SVN__STREAM_CHUNK_SIZE]; + apr_size_t bufsize; +}; + +struct dir_baton +{ + struct edit_baton *edit_baton; + struct dir_baton *parent_dir_baton; + + /* is this directory a new addition to this revision? */ + svn_boolean_t added; + + /* has this directory been written to the output stream? */ + svn_boolean_t written_out; + + /* the repository relpath associated with this directory */ + const char *path; + + /* The comparison repository relpath and revision of this directory. + If both of these are valid, use them as a source against which to + compare the directory instead of the default comparison source of + PATH in the previous revision. */ + const char *cmp_path; + svn_revnum_t cmp_rev; + + /* hash of paths that need to be deleted, though some -might- be + replaced. maps const char * paths to this dir_baton. (they're + full paths, because that's what the editor driver gives us. but + really, they're all within this directory.) */ + apr_hash_t *deleted_entries; + + /* pool to be used for deleting the hash items */ + apr_pool_t *pool; +}; + + +/* Make a directory baton to represent the directory was path + (relative to EDIT_BATON's path) is PATH. + + CMP_PATH/CMP_REV are the path/revision against which this directory + should be compared for changes. If either is omitted (NULL for the + path, SVN_INVALID_REVNUM for the rev), just compare this directory + PATH against itself in the previous revision. + + PARENT_DIR_BATON is the directory baton of this directory's parent, + or NULL if this is the top-level directory of the edit. ADDED + indicated if this directory is newly added in this revision. + Perform all allocations in POOL. */ +static struct dir_baton * +make_dir_baton(const char *path, + const char *cmp_path, + svn_revnum_t cmp_rev, + void *edit_baton, + void *parent_dir_baton, + svn_boolean_t added, + apr_pool_t *pool) +{ + struct edit_baton *eb = edit_baton; + struct dir_baton *pb = parent_dir_baton; + struct dir_baton *new_db = apr_pcalloc(pool, sizeof(*new_db)); + const char *full_path; + + /* A path relative to nothing? I don't think so. */ + SVN_ERR_ASSERT_NO_RETURN(!path || pb); + + /* Construct the full path of this node. */ + if (pb) + full_path = svn_relpath_join(eb->path, path, pool); + else + full_path = apr_pstrdup(pool, eb->path); + + /* Remove leading slashes from copyfrom paths. */ + if (cmp_path) + cmp_path = svn_relpath_canonicalize(cmp_path, pool); + + new_db->edit_baton = eb; + new_db->parent_dir_baton = pb; + new_db->path = full_path; + new_db->cmp_path = cmp_path; + new_db->cmp_rev = cmp_rev; + new_db->added = added; + new_db->written_out = FALSE; + new_db->deleted_entries = apr_hash_make(pool); + new_db->pool = pool; + + return new_db; +} + + +/* This helper is the main "meat" of the editor -- it does all the + work of writing a node record. + + Write out a node record for PATH of type KIND under EB->FS_ROOT. + ACTION describes what is happening to the node (see enum svn_node_action). + Write record to writable EB->STREAM, using EB->BUFFER to write in chunks. + + If the node was itself copied, IS_COPY is TRUE and the + path/revision of the copy source are in CMP_PATH/CMP_REV. If + IS_COPY is FALSE, yet CMP_PATH/CMP_REV are valid, this node is part + of a copied subtree. + */ +static svn_error_t * +dump_node(struct edit_baton *eb, + const char *path, + svn_node_kind_t kind, + enum svn_node_action action, + svn_boolean_t is_copy, + const char *cmp_path, + svn_revnum_t cmp_rev, + apr_pool_t *pool) +{ + svn_stringbuf_t *propstring; + svn_filesize_t content_length = 0; + apr_size_t len; + svn_boolean_t must_dump_text = FALSE, must_dump_props = FALSE; + const char *compare_path = path; + svn_revnum_t compare_rev = eb->current_rev - 1; + svn_fs_root_t *compare_root = NULL; + apr_file_t *delta_file = NULL; + + /* Maybe validate the path. */ + if (eb->verify || eb->notify_func) + { + svn_error_t *err = svn_fs__path_valid(path, pool); + + if (err) + { + if (eb->notify_func) + { + char errbuf[512]; /* ### svn_strerror() magic number */ + svn_repos_notify_t *notify; + notify = svn_repos_notify_create(svn_repos_notify_warning, pool); + + notify->warning = svn_repos_notify_warning_invalid_fspath; + notify->warning_str = apr_psprintf( + pool, + _("E%06d: While validating fspath '%s': %s"), + err->apr_err, path, + svn_err_best_message(err, errbuf, sizeof(errbuf))); + + eb->notify_func(eb->notify_baton, notify, pool); + } + + /* Return the error in addition to notifying about it. */ + if (eb->verify) + return svn_error_trace(err); + else + svn_error_clear(err); + } + } + + /* Write out metadata headers for this file node. */ + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_NODE_PATH ": %s\n", + path)); + if (kind == svn_node_file) + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_KIND ": file\n")); + else if (kind == svn_node_dir) + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_KIND ": dir\n")); + + /* Remove leading slashes from copyfrom paths. */ + if (cmp_path) + cmp_path = svn_relpath_canonicalize(cmp_path, pool); + + /* Validate the comparison path/rev. */ + if (ARE_VALID_COPY_ARGS(cmp_path, cmp_rev)) + { + compare_path = cmp_path; + compare_rev = cmp_rev; + } + + if (action == svn_node_action_change) + { + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_ACTION ": change\n")); + + /* either the text or props changed, or possibly both. */ + SVN_ERR(svn_fs_revision_root(&compare_root, + svn_fs_root_fs(eb->fs_root), + compare_rev, pool)); + + SVN_ERR(svn_fs_props_changed(&must_dump_props, + compare_root, compare_path, + eb->fs_root, path, pool)); + if (kind == svn_node_file) + SVN_ERR(svn_fs_contents_changed(&must_dump_text, + compare_root, compare_path, + eb->fs_root, path, pool)); + } + else if (action == svn_node_action_replace) + { + if (! is_copy) + { + /* a simple delete+add, implied by a single 'replace' action. */ + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_ACTION + ": replace\n")); + + /* definitely need to dump all content for a replace. */ + if (kind == svn_node_file) + must_dump_text = TRUE; + must_dump_props = TRUE; + } + else + { + /* more complex: delete original, then add-with-history. */ + + /* the path & kind headers have already been printed; just + add a delete action, and end the current record.*/ + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_ACTION + ": delete\n\n")); + + /* recurse: print an additional add-with-history record. */ + SVN_ERR(dump_node(eb, path, kind, svn_node_action_add, + is_copy, compare_path, compare_rev, pool)); + + /* we can leave this routine quietly now, don't need to dump + any content; that was already done in the second record. */ + must_dump_text = FALSE; + must_dump_props = FALSE; + } + } + else if (action == svn_node_action_delete) + { + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_ACTION ": delete\n")); + + /* we can leave this routine quietly now, don't need to dump + any content. */ + must_dump_text = FALSE; + must_dump_props = FALSE; + } + else if (action == svn_node_action_add) + { + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_ACTION ": add\n")); + + if (! is_copy) + { + /* Dump all contents for a simple 'add'. */ + if (kind == svn_node_file) + must_dump_text = TRUE; + must_dump_props = TRUE; + } + else + { + if (!eb->verify && cmp_rev < eb->oldest_dumped_rev + && eb->notify_func) + { + svn_repos_notify_t *notify = + svn_repos_notify_create(svn_repos_notify_warning, pool); + + notify->warning = svn_repos_notify_warning_found_old_reference; + notify->warning_str = apr_psprintf( + pool, + _("Referencing data in revision %ld," + " which is older than the oldest" + " dumped revision (r%ld). Loading this dump" + " into an empty repository" + " will fail."), + cmp_rev, eb->oldest_dumped_rev); + if (eb->found_old_reference) + *eb->found_old_reference = TRUE; + eb->notify_func(eb->notify_baton, notify, pool); + } + + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV + ": %ld\n" + SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH + ": %s\n", + cmp_rev, cmp_path)); + + SVN_ERR(svn_fs_revision_root(&compare_root, + svn_fs_root_fs(eb->fs_root), + compare_rev, pool)); + + /* Need to decide if the copied node had any extra textual or + property mods as well. */ + SVN_ERR(svn_fs_props_changed(&must_dump_props, + compare_root, compare_path, + eb->fs_root, path, pool)); + if (kind == svn_node_file) + { + svn_checksum_t *checksum; + const char *hex_digest; + SVN_ERR(svn_fs_contents_changed(&must_dump_text, + compare_root, compare_path, + eb->fs_root, path, pool)); + + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, + compare_root, compare_path, + FALSE, pool)); + hex_digest = svn_checksum_to_cstring(checksum, pool); + if (hex_digest) + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_TEXT_COPY_SOURCE_MD5 + ": %s\n", hex_digest)); + + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_sha1, + compare_root, compare_path, + FALSE, pool)); + hex_digest = svn_checksum_to_cstring(checksum, pool); + if (hex_digest) + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_TEXT_COPY_SOURCE_SHA1 + ": %s\n", hex_digest)); + } + } + } + + if ((! must_dump_text) && (! must_dump_props)) + { + /* If we're not supposed to dump text or props, so be it, we can + just go home. However, if either one needs to be dumped, + then our dumpstream format demands that at a *minimum*, we + see a lone "PROPS-END" as a divider between text and props + content within the content-block. */ + len = 2; + return svn_stream_write(eb->stream, "\n\n", &len); /* ### needed? */ + } + + /*** Start prepping content to dump... ***/ + + /* If we are supposed to dump properties, write out a property + length header and generate a stringbuf that contains those + property values here. */ + if (must_dump_props) + { + apr_hash_t *prophash, *oldhash = NULL; + apr_size_t proplen; + svn_stream_t *propstream; + + SVN_ERR(svn_fs_node_proplist(&prophash, eb->fs_root, path, pool)); + + /* If this is a partial dump, then issue a warning if we dump mergeinfo + properties that refer to revisions older than the first revision + dumped. */ + if (!eb->verify && eb->notify_func && eb->oldest_dumped_rev > 1) + { + svn_string_t *mergeinfo_str = svn_hash_gets(prophash, + SVN_PROP_MERGEINFO); + if (mergeinfo_str) + { + svn_mergeinfo_t mergeinfo, old_mergeinfo; + + SVN_ERR(svn_mergeinfo_parse(&mergeinfo, mergeinfo_str->data, + pool)); + SVN_ERR(svn_mergeinfo__filter_mergeinfo_by_ranges( + &old_mergeinfo, mergeinfo, + eb->oldest_dumped_rev - 1, 0, + TRUE, pool, pool)); + if (apr_hash_count(old_mergeinfo)) + { + svn_repos_notify_t *notify = + svn_repos_notify_create(svn_repos_notify_warning, pool); + + notify->warning = svn_repos_notify_warning_found_old_mergeinfo; + notify->warning_str = apr_psprintf( + pool, + _("Mergeinfo referencing revision(s) prior " + "to the oldest dumped revision (r%ld). " + "Loading this dump may result in invalid " + "mergeinfo."), + eb->oldest_dumped_rev); + + if (eb->found_old_mergeinfo) + *eb->found_old_mergeinfo = TRUE; + eb->notify_func(eb->notify_baton, notify, pool); + } + } + } + + if (eb->use_deltas && compare_root) + { + /* Fetch the old property hash to diff against and output a header + saying that our property contents are a delta. */ + SVN_ERR(svn_fs_node_proplist(&oldhash, compare_root, compare_path, + pool)); + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_PROP_DELTA ": true\n")); + } + else + oldhash = apr_hash_make(pool); + propstring = svn_stringbuf_create_ensure(0, pool); + propstream = svn_stream_from_stringbuf(propstring, pool); + SVN_ERR(svn_hash_write_incremental(prophash, oldhash, propstream, + "PROPS-END", pool)); + SVN_ERR(svn_stream_close(propstream)); + proplen = propstring->len; + content_length += proplen; + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n", proplen)); + } + + /* If we are supposed to dump text, write out a text length header + here, and an MD5 checksum (if available). */ + if (must_dump_text && (kind == svn_node_file)) + { + svn_checksum_t *checksum; + const char *hex_digest; + svn_filesize_t textlen; + + if (eb->use_deltas) + { + /* Compute the text delta now and write it into a temporary + file, so that we can find its length. Output a header + saying our text contents are a delta. */ + SVN_ERR(store_delta(&delta_file, &textlen, compare_root, + compare_path, eb->fs_root, path, pool)); + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_TEXT_DELTA ": true\n")); + + if (compare_root) + { + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, + compare_root, compare_path, + FALSE, pool)); + hex_digest = svn_checksum_to_cstring(checksum, pool); + if (hex_digest) + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_TEXT_DELTA_BASE_MD5 + ": %s\n", hex_digest)); + + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_sha1, + compare_root, compare_path, + FALSE, pool)); + hex_digest = svn_checksum_to_cstring(checksum, pool); + if (hex_digest) + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_TEXT_DELTA_BASE_SHA1 + ": %s\n", hex_digest)); + } + } + else + { + /* Just fetch the length of the file. */ + SVN_ERR(svn_fs_file_length(&textlen, eb->fs_root, path, pool)); + } + + content_length += textlen; + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH + ": %" SVN_FILESIZE_T_FMT "\n", textlen)); + + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, + eb->fs_root, path, FALSE, pool)); + hex_digest = svn_checksum_to_cstring(checksum, pool); + if (hex_digest) + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_TEXT_CONTENT_MD5 + ": %s\n", hex_digest)); + + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_sha1, + eb->fs_root, path, FALSE, pool)); + hex_digest = svn_checksum_to_cstring(checksum, pool); + if (hex_digest) + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_TEXT_CONTENT_SHA1 + ": %s\n", hex_digest)); + } + + /* 'Content-length:' is the last header before we dump the content, + and is the sum of the text and prop contents lengths. We write + this only for the benefit of non-Subversion RFC-822 parsers. */ + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_CONTENT_LENGTH + ": %" SVN_FILESIZE_T_FMT "\n\n", + content_length)); + + /* Dump property content if we're supposed to do so. */ + if (must_dump_props) + { + len = propstring->len; + SVN_ERR(svn_stream_write(eb->stream, propstring->data, &len)); + } + + /* Dump text content */ + if (must_dump_text && (kind == svn_node_file)) + { + svn_stream_t *contents; + + if (delta_file) + { + /* Make sure to close the underlying file when the stream is + closed. */ + contents = svn_stream_from_aprfile2(delta_file, FALSE, pool); + } + else + SVN_ERR(svn_fs_file_contents(&contents, eb->fs_root, path, pool)); + + SVN_ERR(svn_stream_copy3(contents, svn_stream_disown(eb->stream, pool), + NULL, NULL, pool)); + } + + len = 2; + return svn_stream_write(eb->stream, "\n\n", &len); /* ### needed? */ +} + + +static svn_error_t * +open_root(void *edit_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **root_baton) +{ + *root_baton = make_dir_baton(NULL, NULL, SVN_INVALID_REVNUM, + edit_baton, NULL, FALSE, pool); + return SVN_NO_ERROR; +} + + +static svn_error_t * +delete_entry(const char *path, + svn_revnum_t revision, + void *parent_baton, + apr_pool_t *pool) +{ + struct dir_baton *pb = parent_baton; + const char *mypath = apr_pstrdup(pb->pool, path); + + /* remember this path needs to be deleted. */ + svn_hash_sets(pb->deleted_entries, mypath, pb); + + return SVN_NO_ERROR; +} + + +static svn_error_t * +add_directory(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + apr_pool_t *pool, + void **child_baton) +{ + struct dir_baton *pb = parent_baton; + struct edit_baton *eb = pb->edit_baton; + void *val; + svn_boolean_t is_copy = FALSE; + struct dir_baton *new_db + = make_dir_baton(path, copyfrom_path, copyfrom_rev, eb, pb, TRUE, pool); + + /* This might be a replacement -- is the path already deleted? */ + val = svn_hash_gets(pb->deleted_entries, path); + + /* Detect an add-with-history. */ + is_copy = ARE_VALID_COPY_ARGS(copyfrom_path, copyfrom_rev); + + /* Dump the node. */ + SVN_ERR(dump_node(eb, path, + svn_node_dir, + val ? svn_node_action_replace : svn_node_action_add, + is_copy, + is_copy ? copyfrom_path : NULL, + is_copy ? copyfrom_rev : SVN_INVALID_REVNUM, + pool)); + + if (val) + /* Delete the path, it's now been dumped. */ + svn_hash_sets(pb->deleted_entries, path, NULL); + + new_db->written_out = TRUE; + + *child_baton = new_db; + return SVN_NO_ERROR; +} + + +static svn_error_t * +open_directory(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **child_baton) +{ + struct dir_baton *pb = parent_baton; + struct edit_baton *eb = pb->edit_baton; + struct dir_baton *new_db; + const char *cmp_path = NULL; + svn_revnum_t cmp_rev = SVN_INVALID_REVNUM; + + /* If the parent directory has explicit comparison path and rev, + record the same for this one. */ + if (ARE_VALID_COPY_ARGS(pb->cmp_path, pb->cmp_rev)) + { + cmp_path = svn_relpath_join(pb->cmp_path, + svn_relpath_basename(path, pool), pool); + cmp_rev = pb->cmp_rev; + } + + new_db = make_dir_baton(path, cmp_path, cmp_rev, eb, pb, FALSE, pool); + *child_baton = new_db; + return SVN_NO_ERROR; +} + + +static svn_error_t * +close_directory(void *dir_baton, + apr_pool_t *pool) +{ + struct dir_baton *db = dir_baton; + struct edit_baton *eb = db->edit_baton; + apr_pool_t *subpool = svn_pool_create(pool); + int i; + apr_array_header_t *sorted_entries; + + /* Sort entries lexically instead of as paths. Even though the entries + * are full paths they're all in the same directory (see comment in struct + * dir_baton definition). So we really want to sort by basename, in which + * case the lexical sort function is more efficient. */ + sorted_entries = svn_sort__hash(db->deleted_entries, + svn_sort_compare_items_lexically, pool); + for (i = 0; i < sorted_entries->nelts; i++) + { + const char *path = APR_ARRAY_IDX(sorted_entries, i, + svn_sort__item_t).key; + + svn_pool_clear(subpool); + + /* By sending 'svn_node_unknown', the Node-kind: header simply won't + be written out. No big deal at all, really. The loader + shouldn't care. */ + SVN_ERR(dump_node(eb, path, + svn_node_unknown, svn_node_action_delete, + FALSE, NULL, SVN_INVALID_REVNUM, subpool)); + } + + svn_pool_destroy(subpool); + return SVN_NO_ERROR; +} + + +static svn_error_t * +add_file(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + apr_pool_t *pool, + void **file_baton) +{ + struct dir_baton *pb = parent_baton; + struct edit_baton *eb = pb->edit_baton; + void *val; + svn_boolean_t is_copy = FALSE; + + /* This might be a replacement -- is the path already deleted? */ + val = svn_hash_gets(pb->deleted_entries, path); + + /* Detect add-with-history. */ + is_copy = ARE_VALID_COPY_ARGS(copyfrom_path, copyfrom_rev); + + /* Dump the node. */ + SVN_ERR(dump_node(eb, path, + svn_node_file, + val ? svn_node_action_replace : svn_node_action_add, + is_copy, + is_copy ? copyfrom_path : NULL, + is_copy ? copyfrom_rev : SVN_INVALID_REVNUM, + pool)); + + if (val) + /* delete the path, it's now been dumped. */ + svn_hash_sets(pb->deleted_entries, path, NULL); + + *file_baton = NULL; /* muhahahaha */ + return SVN_NO_ERROR; +} + + +static svn_error_t * +open_file(const char *path, + void *parent_baton, + svn_revnum_t ancestor_revision, + apr_pool_t *pool, + void **file_baton) +{ + struct dir_baton *pb = parent_baton; + struct edit_baton *eb = pb->edit_baton; + const char *cmp_path = NULL; + svn_revnum_t cmp_rev = SVN_INVALID_REVNUM; + + /* If the parent directory has explicit comparison path and rev, + record the same for this one. */ + if (ARE_VALID_COPY_ARGS(pb->cmp_path, pb->cmp_rev)) + { + cmp_path = svn_relpath_join(pb->cmp_path, + svn_relpath_basename(path, pool), pool); + cmp_rev = pb->cmp_rev; + } + + SVN_ERR(dump_node(eb, path, + svn_node_file, svn_node_action_change, + FALSE, cmp_path, cmp_rev, pool)); + + *file_baton = NULL; /* muhahahaha again */ + return SVN_NO_ERROR; +} + + +static svn_error_t * +change_dir_prop(void *parent_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + struct dir_baton *db = parent_baton; + struct edit_baton *eb = db->edit_baton; + + /* This function is what distinguishes between a directory that is + opened to merely get somewhere, vs. one that is opened because it + *actually* changed by itself. */ + if (! db->written_out) + { + SVN_ERR(dump_node(eb, db->path, + svn_node_dir, svn_node_action_change, + FALSE, db->cmp_path, db->cmp_rev, pool)); + db->written_out = TRUE; + } + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_props_func(apr_hash_t **props, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct edit_baton *eb = baton; + svn_error_t *err; + svn_fs_root_t *fs_root; + + if (!SVN_IS_VALID_REVNUM(base_revision)) + base_revision = eb->current_rev - 1; + + SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, base_revision, scratch_pool)); + + err = svn_fs_node_proplist(props, fs_root, path, result_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + *props = apr_hash_make(result_pool); + return SVN_NO_ERROR; + } + else if (err) + return svn_error_trace(err); + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_kind_func(svn_node_kind_t *kind, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *scratch_pool) +{ + struct edit_baton *eb = baton; + svn_fs_root_t *fs_root; + + if (!SVN_IS_VALID_REVNUM(base_revision)) + base_revision = eb->current_rev - 1; + + SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, base_revision, scratch_pool)); + + SVN_ERR(svn_fs_check_path(kind, fs_root, path, scratch_pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_base_func(const char **filename, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct edit_baton *eb = baton; + svn_stream_t *contents; + svn_stream_t *file_stream; + const char *tmp_filename; + svn_error_t *err; + svn_fs_root_t *fs_root; + + if (!SVN_IS_VALID_REVNUM(base_revision)) + base_revision = eb->current_rev - 1; + + SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, base_revision, scratch_pool)); + + err = svn_fs_file_contents(&contents, fs_root, path, scratch_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + *filename = NULL; + return SVN_NO_ERROR; + } + else if (err) + return svn_error_trace(err); + SVN_ERR(svn_stream_open_unique(&file_stream, &tmp_filename, NULL, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_copy3(contents, file_stream, NULL, NULL, scratch_pool)); + + *filename = apr_pstrdup(result_pool, tmp_filename); + + return SVN_NO_ERROR; +} + + +static svn_error_t * +get_dump_editor(const svn_delta_editor_t **editor, + void **edit_baton, + svn_fs_t *fs, + svn_revnum_t to_rev, + const char *root_path, + svn_stream_t *stream, + svn_boolean_t *found_old_reference, + svn_boolean_t *found_old_mergeinfo, + svn_error_t *(*custom_close_directory)(void *dir_baton, + apr_pool_t *scratch_pool), + svn_repos_notify_func_t notify_func, + void *notify_baton, + svn_revnum_t oldest_dumped_rev, + svn_boolean_t use_deltas, + svn_boolean_t verify, + apr_pool_t *pool) +{ + /* Allocate an edit baton to be stored in every directory baton. + Set it up for the directory baton we create here, which is the + root baton. */ + struct edit_baton *eb = apr_pcalloc(pool, sizeof(*eb)); + svn_delta_editor_t *dump_editor = svn_delta_default_editor(pool); + svn_delta_shim_callbacks_t *shim_callbacks = + svn_delta_shim_callbacks_default(pool); + + /* Set up the edit baton. */ + eb->stream = stream; + eb->notify_func = notify_func; + eb->notify_baton = notify_baton; + eb->oldest_dumped_rev = oldest_dumped_rev; + eb->bufsize = sizeof(eb->buffer); + eb->path = apr_pstrdup(pool, root_path); + SVN_ERR(svn_fs_revision_root(&(eb->fs_root), fs, to_rev, pool)); + eb->fs = fs; + eb->current_rev = to_rev; + eb->use_deltas = use_deltas; + eb->verify = verify; + eb->found_old_reference = found_old_reference; + eb->found_old_mergeinfo = found_old_mergeinfo; + + /* Set up the editor. */ + dump_editor->open_root = open_root; + dump_editor->delete_entry = delete_entry; + dump_editor->add_directory = add_directory; + dump_editor->open_directory = open_directory; + if (custom_close_directory) + dump_editor->close_directory = custom_close_directory; + else + dump_editor->close_directory = close_directory; + dump_editor->change_dir_prop = change_dir_prop; + dump_editor->add_file = add_file; + dump_editor->open_file = open_file; + + *edit_baton = eb; + *editor = dump_editor; + + shim_callbacks->fetch_kind_func = fetch_kind_func; + shim_callbacks->fetch_props_func = fetch_props_func; + shim_callbacks->fetch_base_func = fetch_base_func; + shim_callbacks->fetch_baton = eb; + + SVN_ERR(svn_editor__insert_shims(editor, edit_baton, *editor, *edit_baton, + NULL, NULL, shim_callbacks, pool, pool)); + + return SVN_NO_ERROR; +} + +/*----------------------------------------------------------------------*/ + +/** The main dumping routine, svn_repos_dump_fs. **/ + + +/* Helper for svn_repos_dump_fs. + + Write a revision record of REV in FS to writable STREAM, using POOL. + */ +static svn_error_t * +write_revision_record(svn_stream_t *stream, + svn_fs_t *fs, + svn_revnum_t rev, + apr_pool_t *pool) +{ + apr_size_t len; + apr_hash_t *props; + svn_stringbuf_t *encoded_prophash; + apr_time_t timetemp; + svn_string_t *datevalue; + svn_stream_t *propstream; + + /* Read the revision props even if we're aren't going to dump + them for verification purposes */ + SVN_ERR(svn_fs_revision_proplist(&props, fs, rev, pool)); + + /* Run revision date properties through the time conversion to + canonicalize them. */ + /* ### Remove this when it is no longer needed for sure. */ + datevalue = svn_hash_gets(props, SVN_PROP_REVISION_DATE); + if (datevalue) + { + SVN_ERR(svn_time_from_cstring(&timetemp, datevalue->data, pool)); + datevalue = svn_string_create(svn_time_to_cstring(timetemp, pool), + pool); + svn_hash_sets(props, SVN_PROP_REVISION_DATE, datevalue); + } + + encoded_prophash = svn_stringbuf_create_ensure(0, pool); + propstream = svn_stream_from_stringbuf(encoded_prophash, pool); + SVN_ERR(svn_hash_write2(props, propstream, "PROPS-END", pool)); + SVN_ERR(svn_stream_close(propstream)); + + /* ### someday write a revision-content-checksum */ + + SVN_ERR(svn_stream_printf(stream, pool, + SVN_REPOS_DUMPFILE_REVISION_NUMBER + ": %ld\n", rev)); + SVN_ERR(svn_stream_printf(stream, pool, + SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n", + encoded_prophash->len)); + + /* Write out a regular Content-length header for the benefit of + non-Subversion RFC-822 parsers. */ + SVN_ERR(svn_stream_printf(stream, pool, + SVN_REPOS_DUMPFILE_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n\n", + encoded_prophash->len)); + + len = encoded_prophash->len; + SVN_ERR(svn_stream_write(stream, encoded_prophash->data, &len)); + + len = 1; + return svn_stream_write(stream, "\n", &len); +} + + + +/* The main dumper. */ +svn_error_t * +svn_repos_dump_fs3(svn_repos_t *repos, + svn_stream_t *stream, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_boolean_t incremental, + svn_boolean_t use_deltas, + svn_repos_notify_func_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + const svn_delta_editor_t *dump_editor; + void *dump_edit_baton = NULL; + svn_revnum_t i; + svn_fs_t *fs = svn_repos_fs(repos); + apr_pool_t *subpool = svn_pool_create(pool); + svn_revnum_t youngest; + const char *uuid; + int version; + svn_boolean_t found_old_reference = FALSE; + svn_boolean_t found_old_mergeinfo = FALSE; + svn_repos_notify_t *notify; + + /* Determine the current youngest revision of the filesystem. */ + SVN_ERR(svn_fs_youngest_rev(&youngest, fs, pool)); + + /* Use default vals if necessary. */ + if (! SVN_IS_VALID_REVNUM(start_rev)) + start_rev = 0; + if (! SVN_IS_VALID_REVNUM(end_rev)) + end_rev = youngest; + if (! stream) + stream = svn_stream_empty(pool); + + /* Validate the revisions. */ + if (start_rev > end_rev) + return svn_error_createf(SVN_ERR_REPOS_BAD_ARGS, NULL, + _("Start revision %ld" + " is greater than end revision %ld"), + start_rev, end_rev); + if (end_rev > youngest) + return svn_error_createf(SVN_ERR_REPOS_BAD_ARGS, NULL, + _("End revision %ld is invalid " + "(youngest revision is %ld)"), + end_rev, youngest); + if ((start_rev == 0) && incremental) + incremental = FALSE; /* revision 0 looks the same regardless of + whether or not this is an incremental + dump, so just simplify things. */ + + /* Write out the UUID. */ + SVN_ERR(svn_fs_get_uuid(fs, &uuid, pool)); + + /* If we're not using deltas, use the previous version, for + compatibility with svn 1.0.x. */ + version = SVN_REPOS_DUMPFILE_FORMAT_VERSION; + if (!use_deltas) + version--; + + /* Write out "general" metadata for the dumpfile. In this case, a + magic header followed by a dumpfile format version. */ + SVN_ERR(svn_stream_printf(stream, pool, + SVN_REPOS_DUMPFILE_MAGIC_HEADER ": %d\n\n", + version)); + SVN_ERR(svn_stream_printf(stream, pool, SVN_REPOS_DUMPFILE_UUID + ": %s\n\n", uuid)); + + /* Create a notify object that we can reuse in the loop. */ + if (notify_func) + notify = svn_repos_notify_create(svn_repos_notify_dump_rev_end, + pool); + + /* Main loop: we're going to dump revision i. */ + for (i = start_rev; i <= end_rev; i++) + { + svn_revnum_t from_rev, to_rev; + svn_fs_root_t *to_root; + svn_boolean_t use_deltas_for_rev; + + svn_pool_clear(subpool); + + /* Check for cancellation. */ + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Special-case the initial revision dump: it needs to contain + *all* nodes, because it's the foundation of all future + revisions in the dumpfile. */ + if ((i == start_rev) && (! incremental)) + { + /* Special-special-case a dump of revision 0. */ + if (i == 0) + { + /* Just write out the one revision 0 record and move on. + The parser might want to use its properties. */ + SVN_ERR(write_revision_record(stream, fs, 0, subpool)); + to_rev = 0; + goto loop_end; + } + + /* Compare START_REV to revision 0, so that everything + appears to be added. */ + from_rev = 0; + to_rev = i; + } + else + { + /* In the normal case, we want to compare consecutive revs. */ + from_rev = i - 1; + to_rev = i; + } + + /* Write the revision record. */ + SVN_ERR(write_revision_record(stream, fs, to_rev, subpool)); + + /* Fetch the editor which dumps nodes to a file. Regardless of + what we've been told, don't use deltas for the first rev of a + non-incremental dump. */ + use_deltas_for_rev = use_deltas && (incremental || i != start_rev); + SVN_ERR(get_dump_editor(&dump_editor, &dump_edit_baton, fs, to_rev, + "", stream, &found_old_reference, + &found_old_mergeinfo, NULL, + notify_func, notify_baton, + start_rev, use_deltas_for_rev, FALSE, subpool)); + + /* Drive the editor in one way or another. */ + SVN_ERR(svn_fs_revision_root(&to_root, fs, to_rev, subpool)); + + /* If this is the first revision of a non-incremental dump, + we're in for a full tree dump. Otherwise, we want to simply + replay the revision. */ + if ((i == start_rev) && (! incremental)) + { + svn_fs_root_t *from_root; + SVN_ERR(svn_fs_revision_root(&from_root, fs, from_rev, subpool)); + SVN_ERR(svn_repos_dir_delta2(from_root, "", "", + to_root, "", + dump_editor, dump_edit_baton, + NULL, + NULL, + FALSE, /* don't send text-deltas */ + svn_depth_infinity, + FALSE, /* don't send entry props */ + FALSE, /* don't ignore ancestry */ + subpool)); + } + else + { + SVN_ERR(svn_repos_replay2(to_root, "", SVN_INVALID_REVNUM, FALSE, + dump_editor, dump_edit_baton, + NULL, NULL, subpool)); + + /* While our editor close_edit implementation is a no-op, we still + do this for completeness. */ + SVN_ERR(dump_editor->close_edit(dump_edit_baton, subpool)); + } + + loop_end: + if (notify_func) + { + notify->revision = to_rev; + notify_func(notify_baton, notify, subpool); + } + } + + if (notify_func) + { + /* Did we issue any warnings about references to revisions older than + the oldest dumped revision? If so, then issue a final generic + warning, since the inline warnings already issued might easily be + missed. */ + + notify = svn_repos_notify_create(svn_repos_notify_dump_end, subpool); + notify_func(notify_baton, notify, subpool); + + if (found_old_reference) + { + notify = svn_repos_notify_create(svn_repos_notify_warning, subpool); + + notify->warning = svn_repos_notify_warning_found_old_reference; + notify->warning_str = _("The range of revisions dumped " + "contained references to " + "copy sources outside that " + "range."); + notify_func(notify_baton, notify, subpool); + } + + /* Ditto if we issued any warnings about old revisions referenced + in dumped mergeinfo. */ + if (found_old_mergeinfo) + { + notify = svn_repos_notify_create(svn_repos_notify_warning, subpool); + + notify->warning = svn_repos_notify_warning_found_old_mergeinfo; + notify->warning_str = _("The range of revisions dumped " + "contained mergeinfo " + "which reference revisions outside " + "that range."); + notify_func(notify_baton, notify, subpool); + } + } + + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + + +/*----------------------------------------------------------------------*/ + +/* verify, based on dump */ + + +/* Creating a new revision that changes /A/B/E/bravo means creating new + directory listings for /, /A, /A/B, and /A/B/E in the new revision, with + each entry not changed in the new revision a link back to the entry in a + previous revision. svn_repos_replay()ing a revision does not verify that + those links are correct. + + For paths actually changed in the revision we verify, we get directory + contents or file length twice: once in the dump editor, and once here. + We could create a new verify baton, store in it the changed paths, and + skip those here, but that means building an entire wrapper editor and + managing two levels of batons. The impact from checking these entries + twice should be minimal, while the code to avoid it is not. +*/ + +static svn_error_t * +verify_directory_entry(void *baton, const void *key, apr_ssize_t klen, + void *val, apr_pool_t *pool) +{ + struct dir_baton *db = baton; + svn_fs_dirent_t *dirent = (svn_fs_dirent_t *)val; + char *path = svn_relpath_join(db->path, (const char *)key, pool); + apr_hash_t *dirents; + svn_filesize_t len; + + /* since we can't access the directory entries directly by their ID, + we need to navigate from the FS_ROOT to them (relatively expensive + because we may start at a never rev than the last change to node). */ + switch (dirent->kind) { + case svn_node_dir: + /* Getting this directory's contents is enough to ensure that our + link to it is correct. */ + SVN_ERR(svn_fs_dir_entries(&dirents, db->edit_baton->fs_root, path, pool)); + break; + case svn_node_file: + /* Getting this file's size is enough to ensure that our link to it + is correct. */ + SVN_ERR(svn_fs_file_length(&len, db->edit_baton->fs_root, path, pool)); + break; + default: + return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, + _("Unexpected node kind %d for '%s'"), + dirent->kind, path); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +verify_close_directory(void *dir_baton, + apr_pool_t *pool) +{ + struct dir_baton *db = dir_baton; + apr_hash_t *dirents; + SVN_ERR(svn_fs_dir_entries(&dirents, db->edit_baton->fs_root, + db->path, pool)); + SVN_ERR(svn_iter_apr_hash(NULL, dirents, verify_directory_entry, + dir_baton, pool)); + return close_directory(dir_baton, pool); +} + +/* Baton type used for forwarding notifications from FS API to REPOS API. */ +struct verify_fs2_notify_func_baton_t +{ + /* notification function to call (must not be NULL) */ + svn_repos_notify_func_t notify_func; + + /* baton to use for it */ + void *notify_baton; + + /* type of notification to send (we will simply plug in the revision) */ + svn_repos_notify_t *notify; +}; + +/* Forward the notification to BATON. */ +static void +verify_fs2_notify_func(svn_revnum_t revision, + void *baton, + apr_pool_t *pool) +{ + struct verify_fs2_notify_func_baton_t *notify_baton = baton; + + notify_baton->notify->revision = revision; + notify_baton->notify_func(notify_baton->notify_baton, + notify_baton->notify, pool); +} + +svn_error_t * +svn_repos_verify_fs2(svn_repos_t *repos, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_repos_notify_func_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + svn_fs_t *fs = svn_repos_fs(repos); + svn_revnum_t youngest; + svn_revnum_t rev; + apr_pool_t *iterpool = svn_pool_create(pool); + svn_repos_notify_t *notify; + svn_fs_progress_notify_func_t verify_notify = NULL; + struct verify_fs2_notify_func_baton_t *verify_notify_baton = NULL; + + /* Determine the current youngest revision of the filesystem. */ + SVN_ERR(svn_fs_youngest_rev(&youngest, fs, pool)); + + /* Use default vals if necessary. */ + if (! SVN_IS_VALID_REVNUM(start_rev)) + start_rev = 0; + if (! SVN_IS_VALID_REVNUM(end_rev)) + end_rev = youngest; + + /* Validate the revisions. */ + if (start_rev > end_rev) + return svn_error_createf(SVN_ERR_REPOS_BAD_ARGS, NULL, + _("Start revision %ld" + " is greater than end revision %ld"), + start_rev, end_rev); + if (end_rev > youngest) + return svn_error_createf(SVN_ERR_REPOS_BAD_ARGS, NULL, + _("End revision %ld is invalid " + "(youngest revision is %ld)"), + end_rev, youngest); + + /* Create a notify object that we can reuse within the loop and a + forwarding structure for notifications from inside svn_fs_verify(). */ + if (notify_func) + { + notify = svn_repos_notify_create(svn_repos_notify_verify_rev_end, + pool); + + verify_notify = verify_fs2_notify_func; + verify_notify_baton = apr_palloc(pool, sizeof(*verify_notify_baton)); + verify_notify_baton->notify_func = notify_func; + verify_notify_baton->notify_baton = notify_baton; + verify_notify_baton->notify + = svn_repos_notify_create(svn_repos_notify_verify_rev_structure, pool); + } + + /* Verify global metadata and backend-specific data first. */ + SVN_ERR(svn_fs_verify(svn_fs_path(fs, pool), svn_fs_config(fs, pool), + start_rev, end_rev, + verify_notify, verify_notify_baton, + cancel_func, cancel_baton, pool)); + + for (rev = start_rev; rev <= end_rev; rev++) + { + const svn_delta_editor_t *dump_editor; + void *dump_edit_baton; + const svn_delta_editor_t *cancel_editor; + void *cancel_edit_baton; + svn_fs_root_t *to_root; + apr_hash_t *props; + + svn_pool_clear(iterpool); + + /* Get cancellable dump editor, but with our close_directory handler. */ + SVN_ERR(get_dump_editor(&dump_editor, &dump_edit_baton, + fs, rev, "", + svn_stream_empty(iterpool), + NULL, NULL, + verify_close_directory, + notify_func, notify_baton, + start_rev, + FALSE, TRUE, /* use_deltas, verify */ + iterpool)); + SVN_ERR(svn_delta_get_cancellation_editor(cancel_func, cancel_baton, + dump_editor, dump_edit_baton, + &cancel_editor, + &cancel_edit_baton, + iterpool)); + + SVN_ERR(svn_fs_revision_root(&to_root, fs, rev, iterpool)); + SVN_ERR(svn_fs_verify_root(to_root, iterpool)); + + SVN_ERR(svn_repos_replay2(to_root, "", SVN_INVALID_REVNUM, FALSE, + cancel_editor, cancel_edit_baton, + NULL, NULL, iterpool)); + /* While our editor close_edit implementation is a no-op, we still + do this for completeness. */ + SVN_ERR(cancel_editor->close_edit(cancel_edit_baton, iterpool)); + + SVN_ERR(svn_fs_revision_proplist(&props, fs, rev, iterpool)); + + if (notify_func) + { + notify->revision = rev; + notify_func(notify_baton, notify, iterpool); + } + } + + /* We're done. */ + if (notify_func) + { + notify = svn_repos_notify_create(svn_repos_notify_verify_end, iterpool); + notify_func(notify_baton, notify, iterpool); + } + + /* Per-backend verification. */ + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_repos/fs-wrap.c b/subversion/libsvn_repos/fs-wrap.c new file mode 100644 index 0000000..b759c57 --- /dev/null +++ b/subversion/libsvn_repos/fs-wrap.c @@ -0,0 +1,844 @@ +/* fs-wrap.c --- filesystem interface wrappers. + * + * ==================================================================== + * 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 <stdio.h> +#include <string.h> +#include <ctype.h> + +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_fs.h" +#include "svn_path.h" +#include "svn_props.h" +#include "svn_repos.h" +#include "svn_time.h" +#include "svn_sorts.h" +#include "repos.h" +#include "svn_private_config.h" +#include "private/svn_repos_private.h" +#include "private/svn_utf_private.h" +#include "private/svn_fspath.h" + + +/*** Commit wrappers ***/ + +svn_error_t * +svn_repos_fs_commit_txn(const char **conflict_p, + svn_repos_t *repos, + svn_revnum_t *new_rev, + svn_fs_txn_t *txn, + apr_pool_t *pool) +{ + svn_error_t *err, *err2; + const char *txn_name; + apr_hash_t *props; + apr_pool_t *iterpool; + apr_hash_index_t *hi; + apr_hash_t *hooks_env; + + *new_rev = SVN_INVALID_REVNUM; + + /* Parse the hooks-env file (if any). */ + SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, repos->hooks_env_path, + pool, pool)); + + /* Run pre-commit hooks. */ + SVN_ERR(svn_fs_txn_name(&txn_name, txn, pool)); + SVN_ERR(svn_repos__hooks_pre_commit(repos, hooks_env, txn_name, pool)); + + /* Remove any ephemeral transaction properties. */ + SVN_ERR(svn_fs_txn_proplist(&props, txn, pool)); + iterpool = svn_pool_create(pool); + for (hi = apr_hash_first(pool, props); hi; hi = apr_hash_next(hi)) + { + const void *key; + apr_hash_this(hi, &key, NULL, NULL); + + svn_pool_clear(iterpool); + + if (strncmp(key, SVN_PROP_TXN_PREFIX, + (sizeof(SVN_PROP_TXN_PREFIX) - 1)) == 0) + { + SVN_ERR(svn_fs_change_txn_prop(txn, key, NULL, iterpool)); + } + } + svn_pool_destroy(iterpool); + + /* Commit. */ + err = svn_fs_commit_txn(conflict_p, new_rev, txn, pool); + if (! SVN_IS_VALID_REVNUM(*new_rev)) + return err; + + /* Run post-commit hooks. */ + if ((err2 = svn_repos__hooks_post_commit(repos, hooks_env, + *new_rev, txn_name, pool))) + { + err2 = svn_error_create + (SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED, err2, + _("Commit succeeded, but post-commit hook failed")); + } + + return svn_error_compose_create(err, err2); +} + + + +/*** Transaction creation wrappers. ***/ + + +svn_error_t * +svn_repos_fs_begin_txn_for_commit2(svn_fs_txn_t **txn_p, + svn_repos_t *repos, + svn_revnum_t rev, + apr_hash_t *revprop_table, + apr_pool_t *pool) +{ + apr_array_header_t *revprops; + const char *txn_name; + svn_string_t *author = svn_hash_gets(revprop_table, SVN_PROP_REVISION_AUTHOR); + apr_hash_t *hooks_env; + + /* Parse the hooks-env file (if any). */ + SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, repos->hooks_env_path, + pool, pool)); + + /* Begin the transaction, ask for the fs to do on-the-fly lock checks. + We fetch its name, too, so the start-commit hook can use it. */ + SVN_ERR(svn_fs_begin_txn2(txn_p, repos->fs, rev, + SVN_FS_TXN_CHECK_LOCKS, pool)); + SVN_ERR(svn_fs_txn_name(&txn_name, *txn_p, pool)); + + /* We pass the revision properties to the filesystem by adding them + as properties on the txn. Later, when we commit the txn, these + properties will be copied into the newly created revision. */ + revprops = svn_prop_hash_to_array(revprop_table, pool); + SVN_ERR(svn_repos_fs_change_txn_props(*txn_p, revprops, pool)); + + /* Run start-commit hooks. */ + SVN_ERR(svn_repos__hooks_start_commit(repos, hooks_env, + author ? author->data : NULL, + repos->client_capabilities, txn_name, + pool)); + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_fs_begin_txn_for_commit(svn_fs_txn_t **txn_p, + svn_repos_t *repos, + svn_revnum_t rev, + const char *author, + const char *log_msg, + apr_pool_t *pool) +{ + apr_hash_t *revprop_table = apr_hash_make(pool); + if (author) + svn_hash_sets(revprop_table, SVN_PROP_REVISION_AUTHOR, + svn_string_create(author, pool)); + if (log_msg) + svn_hash_sets(revprop_table, SVN_PROP_REVISION_LOG, + svn_string_create(log_msg, pool)); + return svn_repos_fs_begin_txn_for_commit2(txn_p, repos, rev, revprop_table, + pool); +} + + +/*** Property wrappers ***/ + +svn_error_t * +svn_repos__validate_prop(const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + svn_prop_kind_t kind = svn_property_kind2(name); + + /* Disallow setting non-regular properties. */ + if (kind != svn_prop_regular_kind) + return svn_error_createf + (SVN_ERR_REPOS_BAD_ARGS, NULL, + _("Storage of non-regular property '%s' is disallowed through the " + "repository interface, and could indicate a bug in your client"), + name); + + /* Validate "svn:" properties. */ + if (svn_prop_is_svn_prop(name) && value != NULL) + { + /* Validate that translated props (e.g., svn:log) are UTF-8 with + * LF line endings. */ + if (svn_prop_needs_translation(name)) + { + if (!svn_utf__is_valid(value->data, value->len)) + { + return svn_error_createf + (SVN_ERR_BAD_PROPERTY_VALUE, NULL, + _("Cannot accept '%s' property because it is not encoded in " + "UTF-8"), name); + } + + /* Disallow inconsistent line ending style, by simply looking for + * carriage return characters ('\r'). */ + if (strchr(value->data, '\r') != NULL) + { + return svn_error_createf + (SVN_ERR_BAD_PROPERTY_VALUE, NULL, + _("Cannot accept non-LF line endings in '%s' property"), + name); + } + } + + /* "svn:date" should be a valid date. */ + if (strcmp(name, SVN_PROP_REVISION_DATE) == 0) + { + apr_time_t temp; + svn_error_t *err; + + err = svn_time_from_cstring(&temp, value->data, pool); + if (err) + return svn_error_create(SVN_ERR_BAD_PROPERTY_VALUE, + err, NULL); + } + } + + return SVN_NO_ERROR; +} + + +/* Verify the mergeinfo property value VALUE and return an error if it + * is invalid. The PATH on which that property is set is used for error + * messages only. Use SCRATCH_POOL for temporary allocations. */ +static svn_error_t * +verify_mergeinfo(const svn_string_t *value, + const char *path, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + svn_mergeinfo_t mergeinfo; + + /* It's okay to delete svn:mergeinfo. */ + if (value == NULL) + return SVN_NO_ERROR; + + /* Mergeinfo is UTF-8 encoded so the number of bytes returned by strlen() + * should match VALUE->LEN. Prevents trailing garbage in the property. */ + if (strlen(value->data) != value->len) + return svn_error_createf(SVN_ERR_MERGEINFO_PARSE_ERROR, NULL, + _("Commit rejected because mergeinfo on '%s' " + "contains unexpected string terminator"), + path); + + err = svn_mergeinfo_parse(&mergeinfo, value->data, scratch_pool); + if (err) + return svn_error_createf(err->apr_err, err, + _("Commit rejected because mergeinfo on '%s' " + "is syntactically invalid"), + path); + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_fs_change_node_prop(svn_fs_root_t *root, + const char *path, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + if (value && strcmp(name, SVN_PROP_MERGEINFO) == 0) + SVN_ERR(verify_mergeinfo(value, path, pool)); + + /* Validate the property, then call the wrapped function. */ + SVN_ERR(svn_repos__validate_prop(name, value, pool)); + return svn_fs_change_node_prop(root, path, name, value, pool); +} + + +svn_error_t * +svn_repos_fs_change_txn_props(svn_fs_txn_t *txn, + const apr_array_header_t *txnprops, + apr_pool_t *pool) +{ + int i; + + for (i = 0; i < txnprops->nelts; i++) + { + svn_prop_t *prop = &APR_ARRAY_IDX(txnprops, i, svn_prop_t); + SVN_ERR(svn_repos__validate_prop(prop->name, prop->value, pool)); + } + + return svn_fs_change_txn_props(txn, txnprops, pool); +} + + +svn_error_t * +svn_repos_fs_change_txn_prop(svn_fs_txn_t *txn, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + apr_array_header_t *props = apr_array_make(pool, 1, sizeof(svn_prop_t)); + svn_prop_t prop; + + prop.name = name; + prop.value = value; + APR_ARRAY_PUSH(props, svn_prop_t) = prop; + + return svn_repos_fs_change_txn_props(txn, props, pool); +} + + +svn_error_t * +svn_repos_fs_change_rev_prop4(svn_repos_t *repos, + svn_revnum_t rev, + const char *author, + const char *name, + const svn_string_t *const *old_value_p, + const svn_string_t *new_value, + svn_boolean_t use_pre_revprop_change_hook, + svn_boolean_t use_post_revprop_change_hook, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + svn_repos_revision_access_level_t readability; + + SVN_ERR(svn_repos_check_revision_access(&readability, repos, rev, + authz_read_func, authz_read_baton, + pool)); + + if (readability == svn_repos_revision_access_full) + { + const svn_string_t *old_value; + char action; + apr_hash_t *hooks_env; + + SVN_ERR(svn_repos__validate_prop(name, new_value, pool)); + + /* Fetch OLD_VALUE for svn_fs_change_rev_prop2(). */ + if (old_value_p) + { + old_value = *old_value_p; + } + else + { + /* Get OLD_VALUE anyway, in order for ACTION and OLD_VALUE arguments + * to the hooks to be accurate. */ + svn_string_t *old_value2; + + SVN_ERR(svn_fs_revision_prop(&old_value2, repos->fs, rev, name, pool)); + old_value = old_value2; + } + + /* Prepare ACTION. */ + if (! new_value) + action = 'D'; + else if (! old_value) + action = 'A'; + else + action = 'M'; + + /* Parse the hooks-env file (if any, and if to be used). */ + if (use_pre_revprop_change_hook || use_post_revprop_change_hook) + SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, repos->hooks_env_path, + pool, pool)); + + /* ### currently not passing the old_value to hooks */ + if (use_pre_revprop_change_hook) + SVN_ERR(svn_repos__hooks_pre_revprop_change(repos, hooks_env, rev, + author, name, new_value, + action, pool)); + + SVN_ERR(svn_fs_change_rev_prop2(repos->fs, rev, name, + &old_value, new_value, pool)); + + if (use_post_revprop_change_hook) + SVN_ERR(svn_repos__hooks_post_revprop_change(repos, hooks_env, rev, + author, name, old_value, + action, pool)); + } + else /* rev is either unreadable or only partially readable */ + { + return svn_error_createf + (SVN_ERR_AUTHZ_UNREADABLE, NULL, + _("Write denied: not authorized to read all of revision %ld"), rev); + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_fs_revision_prop(svn_string_t **value_p, + svn_repos_t *repos, + svn_revnum_t rev, + const char *propname, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + svn_repos_revision_access_level_t readability; + + SVN_ERR(svn_repos_check_revision_access(&readability, repos, rev, + authz_read_func, authz_read_baton, + pool)); + + if (readability == svn_repos_revision_access_none) + { + /* Property? What property? */ + *value_p = NULL; + } + else if (readability == svn_repos_revision_access_partial) + { + /* Only svn:author and svn:date are fetchable. */ + if ((strcmp(propname, SVN_PROP_REVISION_AUTHOR) != 0) + && (strcmp(propname, SVN_PROP_REVISION_DATE) != 0)) + *value_p = NULL; + + else + SVN_ERR(svn_fs_revision_prop(value_p, repos->fs, + rev, propname, pool)); + } + else /* wholly readable revision */ + { + SVN_ERR(svn_fs_revision_prop(value_p, repos->fs, rev, propname, pool)); + } + + return SVN_NO_ERROR; +} + + + +svn_error_t * +svn_repos_fs_revision_proplist(apr_hash_t **table_p, + svn_repos_t *repos, + svn_revnum_t rev, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + svn_repos_revision_access_level_t readability; + + SVN_ERR(svn_repos_check_revision_access(&readability, repos, rev, + authz_read_func, authz_read_baton, + pool)); + + if (readability == svn_repos_revision_access_none) + { + /* Return an empty hash. */ + *table_p = apr_hash_make(pool); + } + else if (readability == svn_repos_revision_access_partial) + { + apr_hash_t *tmphash; + svn_string_t *value; + + /* Produce two property hashtables, both in POOL. */ + SVN_ERR(svn_fs_revision_proplist(&tmphash, repos->fs, rev, pool)); + *table_p = apr_hash_make(pool); + + /* If they exist, we only copy svn:author and svn:date into the + 'real' hashtable being returned. */ + value = svn_hash_gets(tmphash, SVN_PROP_REVISION_AUTHOR); + if (value) + svn_hash_sets(*table_p, SVN_PROP_REVISION_AUTHOR, value); + + value = svn_hash_gets(tmphash, SVN_PROP_REVISION_DATE); + if (value) + svn_hash_sets(*table_p, SVN_PROP_REVISION_DATE, value); + } + else /* wholly readable revision */ + { + SVN_ERR(svn_fs_revision_proplist(table_p, repos->fs, rev, pool)); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos_fs_lock(svn_lock_t **lock, + svn_repos_t *repos, + const char *path, + const char *token, + const char *comment, + svn_boolean_t is_dav_comment, + apr_time_t expiration_date, + svn_revnum_t current_rev, + svn_boolean_t steal_lock, + apr_pool_t *pool) +{ + svn_error_t *err; + svn_fs_access_t *access_ctx = NULL; + const char *username = NULL; + const char *new_token; + apr_array_header_t *paths; + apr_hash_t *hooks_env; + + /* Parse the hooks-env file (if any). */ + SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, repos->hooks_env_path, + pool, pool)); + + /* Setup an array of paths in anticipation of the ra layers handling + multiple locks in one request (1.3 most likely). This is only + used by svn_repos__hooks_post_lock. */ + paths = apr_array_make(pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(paths, const char *) = path; + + SVN_ERR(svn_fs_get_access(&access_ctx, repos->fs)); + if (access_ctx) + SVN_ERR(svn_fs_access_get_username(&username, access_ctx)); + + if (! username) + return svn_error_createf + (SVN_ERR_FS_NO_USER, NULL, + "Cannot lock path '%s', no authenticated username available.", path); + + /* Run pre-lock hook. This could throw error, preventing + svn_fs_lock() from happening. */ + SVN_ERR(svn_repos__hooks_pre_lock(repos, hooks_env, &new_token, path, + username, comment, steal_lock, pool)); + if (*new_token) + token = new_token; + + /* Lock. */ + SVN_ERR(svn_fs_lock(lock, repos->fs, path, token, comment, is_dav_comment, + expiration_date, current_rev, steal_lock, pool)); + + /* Run post-lock hook. */ + if ((err = svn_repos__hooks_post_lock(repos, hooks_env, + paths, username, pool))) + return svn_error_create + (SVN_ERR_REPOS_POST_LOCK_HOOK_FAILED, err, + "Lock succeeded, but post-lock hook failed"); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_fs_unlock(svn_repos_t *repos, + const char *path, + const char *token, + svn_boolean_t break_lock, + apr_pool_t *pool) +{ + svn_error_t *err; + svn_fs_access_t *access_ctx = NULL; + const char *username = NULL; + apr_array_header_t *paths; + apr_hash_t *hooks_env; + + /* Parse the hooks-env file (if any). */ + SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, repos->hooks_env_path, + pool, pool)); + + /* Setup an array of paths in anticipation of the ra layers handling + multiple locks in one request (1.3 most likely). This is only + used by svn_repos__hooks_post_lock. */ + paths = apr_array_make(pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(paths, const char *) = path; + + SVN_ERR(svn_fs_get_access(&access_ctx, repos->fs)); + if (access_ctx) + SVN_ERR(svn_fs_access_get_username(&username, access_ctx)); + + if (! break_lock && ! username) + return svn_error_createf + (SVN_ERR_FS_NO_USER, NULL, + _("Cannot unlock path '%s', no authenticated username available"), + path); + + /* Run pre-unlock hook. This could throw error, preventing + svn_fs_unlock() from happening. */ + SVN_ERR(svn_repos__hooks_pre_unlock(repos, hooks_env, path, username, token, + break_lock, pool)); + + /* Unlock. */ + SVN_ERR(svn_fs_unlock(repos->fs, path, token, break_lock, pool)); + + /* Run post-unlock hook. */ + if ((err = svn_repos__hooks_post_unlock(repos, hooks_env, paths, + username, pool))) + return svn_error_create + (SVN_ERR_REPOS_POST_UNLOCK_HOOK_FAILED, err, + _("Unlock succeeded, but post-unlock hook failed")); + + return SVN_NO_ERROR; +} + + +struct get_locks_baton_t +{ + svn_fs_t *fs; + svn_fs_root_t *head_root; + svn_repos_authz_func_t authz_read_func; + void *authz_read_baton; + apr_hash_t *locks; +}; + + +/* This implements the svn_fs_get_locks_callback_t interface. */ +static svn_error_t * +get_locks_callback(void *baton, + svn_lock_t *lock, + apr_pool_t *pool) +{ + struct get_locks_baton_t *b = baton; + svn_boolean_t readable = TRUE; + apr_pool_t *hash_pool = apr_hash_pool_get(b->locks); + + /* If there's auth to deal with, deal with it. */ + if (b->authz_read_func) + SVN_ERR(b->authz_read_func(&readable, b->head_root, lock->path, + b->authz_read_baton, pool)); + + /* If we can read this lock path, add the lock to the return hash. */ + if (readable) + svn_hash_sets(b->locks, apr_pstrdup(hash_pool, lock->path), + svn_lock_dup(lock, hash_pool)); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_fs_get_locks2(apr_hash_t **locks, + svn_repos_t *repos, + const char *path, + svn_depth_t depth, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + apr_hash_t *all_locks = apr_hash_make(pool); + svn_revnum_t head_rev; + struct get_locks_baton_t baton; + + SVN_ERR_ASSERT((depth == svn_depth_empty) || + (depth == svn_depth_files) || + (depth == svn_depth_immediates) || + (depth == svn_depth_infinity)); + + SVN_ERR(svn_fs_youngest_rev(&head_rev, repos->fs, pool)); + + /* Populate our callback baton. */ + baton.fs = repos->fs; + baton.locks = all_locks; + baton.authz_read_func = authz_read_func; + baton.authz_read_baton = authz_read_baton; + SVN_ERR(svn_fs_revision_root(&(baton.head_root), repos->fs, + head_rev, pool)); + + /* Get all the locks. */ + SVN_ERR(svn_fs_get_locks2(repos->fs, path, depth, + get_locks_callback, &baton, pool)); + + *locks = baton.locks; + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_fs_get_mergeinfo(svn_mergeinfo_catalog_t *mergeinfo, + svn_repos_t *repos, + const apr_array_header_t *paths, + svn_revnum_t rev, + svn_mergeinfo_inheritance_t inherit, + svn_boolean_t include_descendants, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + /* Here we cast away 'const', but won't try to write through this pointer + * without first allocating a new array. */ + apr_array_header_t *readable_paths = (apr_array_header_t *) paths; + svn_fs_root_t *root; + apr_pool_t *iterpool = svn_pool_create(pool); + + if (!SVN_IS_VALID_REVNUM(rev)) + SVN_ERR(svn_fs_youngest_rev(&rev, repos->fs, pool)); + SVN_ERR(svn_fs_revision_root(&root, repos->fs, rev, pool)); + + /* Filter out unreadable paths before divining merge tracking info. */ + if (authz_read_func) + { + int i; + + for (i = 0; i < paths->nelts; i++) + { + svn_boolean_t readable; + const char *path = APR_ARRAY_IDX(paths, i, char *); + svn_pool_clear(iterpool); + SVN_ERR(authz_read_func(&readable, root, path, authz_read_baton, + iterpool)); + if (readable && readable_paths != paths) + APR_ARRAY_PUSH(readable_paths, const char *) = path; + else if (!readable && readable_paths == paths) + { + /* Requested paths differ from readable paths. Fork + list of readable paths from requested paths. */ + int j; + readable_paths = apr_array_make(pool, paths->nelts - 1, + sizeof(char *)); + for (j = 0; j < i; j++) + { + path = APR_ARRAY_IDX(paths, j, char *); + APR_ARRAY_PUSH(readable_paths, const char *) = path; + } + } + } + } + + /* We consciously do not perform authz checks on the paths returned + in *MERGEINFO, avoiding massive authz overhead which would allow + us to protect the name of where a change was merged from, but not + the change itself. */ + /* ### TODO(reint): ... but how about descendant merged-to paths? */ + if (readable_paths->nelts > 0) + SVN_ERR(svn_fs_get_mergeinfo2(mergeinfo, root, readable_paths, inherit, + include_descendants, TRUE, pool, pool)); + else + *mergeinfo = apr_hash_make(pool); + + svn_pool_destroy(iterpool); + return SVN_NO_ERROR; +} + +struct pack_notify_baton +{ + svn_repos_notify_func_t notify_func; + void *notify_baton; +}; + +/* Implements svn_fs_pack_notify_t. */ +static svn_error_t * +pack_notify_func(void *baton, + apr_int64_t shard, + svn_fs_pack_notify_action_t pack_action, + apr_pool_t *pool) +{ + struct pack_notify_baton *pnb = baton; + svn_repos_notify_t *notify; + + /* Simple conversion works for these values. */ + SVN_ERR_ASSERT(pack_action >= svn_fs_pack_notify_start + && pack_action <= svn_fs_pack_notify_end_revprop); + + notify = svn_repos_notify_create(pack_action + + svn_repos_notify_pack_shard_start + - svn_fs_pack_notify_start, + pool); + notify->shard = shard; + pnb->notify_func(pnb->notify_baton, notify, pool); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos_fs_pack2(svn_repos_t *repos, + svn_repos_notify_func_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + struct pack_notify_baton pnb; + + pnb.notify_func = notify_func; + pnb.notify_baton = notify_baton; + + return svn_fs_pack(repos->db_path, + notify_func ? pack_notify_func : NULL, + notify_func ? &pnb : NULL, + cancel_func, cancel_baton, pool); +} + +svn_error_t * +svn_repos_fs_get_inherited_props(apr_array_header_t **inherited_props_p, + svn_fs_root_t *root, + const char *path, + const char *propname, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_pool_t *iterpool = svn_pool_create(scratch_pool); + apr_array_header_t *inherited_props; + const char *parent_path = path; + + inherited_props = apr_array_make(result_pool, 1, + sizeof(svn_prop_inherited_item_t *)); + while (!(parent_path[0] == '/' && parent_path[1] == '\0')) + { + svn_boolean_t allowed = TRUE; + apr_hash_t *parent_properties = NULL; + + svn_pool_clear(iterpool); + parent_path = svn_fspath__dirname(parent_path, scratch_pool); + + if (authz_read_func) + SVN_ERR(authz_read_func(&allowed, root, parent_path, + authz_read_baton, iterpool)); + if (allowed) + { + if (propname) + { + svn_string_t *propval; + + SVN_ERR(svn_fs_node_prop(&propval, root, parent_path, propname, + result_pool)); + if (propval) + { + parent_properties = apr_hash_make(result_pool); + svn_hash_sets(parent_properties, propname, propval); + } + } + else + { + SVN_ERR(svn_fs_node_proplist(&parent_properties, root, + parent_path, result_pool)); + } + + if (parent_properties && apr_hash_count(parent_properties)) + { + svn_prop_inherited_item_t *i_props = + apr_pcalloc(result_pool, sizeof(*i_props)); + i_props->path_or_url = + apr_pstrdup(result_pool, parent_path + 1); + i_props->prop_hash = parent_properties; + /* Build the output array in depth-first order. */ + svn_sort__array_insert(&i_props, inherited_props, 0); + } + } + } + + svn_pool_destroy(iterpool); + + *inherited_props_p = inherited_props; + return SVN_NO_ERROR; +} + +/* + * vim:ts=4:sw=2:expandtab:tw=80:fo=tcroq + * vim:isk=a-z,A-Z,48-57,_,.,-,> + * vim:cino=>1s,e0,n0,f0,{.5s,}0,^-.5s,=.5s,t0,+1s,c3,(0,u0,\:0 + */ diff --git a/subversion/libsvn_repos/hooks.c b/subversion/libsvn_repos/hooks.c new file mode 100644 index 0000000..9727599 --- /dev/null +++ b/subversion/libsvn_repos/hooks.c @@ -0,0 +1,890 @@ +/* hooks.c : running repository hooks + * + * ==================================================================== + * 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 <stdio.h> +#include <string.h> +#include <ctype.h> + +#include <apr_pools.h> +#include <apr_file_io.h> + +#include "svn_config.h" +#include "svn_hash.h" +#include "svn_error.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_pools.h" +#include "svn_repos.h" +#include "svn_utf.h" +#include "repos.h" +#include "svn_private_config.h" +#include "private/svn_fs_private.h" +#include "private/svn_repos_private.h" +#include "private/svn_string_private.h" + + + +/*** Hook drivers. ***/ + +/* Helper function for run_hook_cmd(). Wait for a hook to finish + executing and return either SVN_NO_ERROR if the hook script completed + without error, or an error describing the reason for failure. + + NAME and CMD are the name and path of the hook program, CMD_PROC + is a pointer to the structure representing the running process, + and READ_ERRHANDLE is an open handle to the hook's stderr. + + Hooks are considered to have failed if we are unable to wait for the + process, if we are unable to read from the hook's stderr, if the + process has failed to exit cleanly (due to a coredump, for example), + or if the process returned a non-zero return code. + + Any error output returned by the hook's stderr will be included in an + error message, though the presence of output on stderr is not itself + a reason to fail a hook. */ +static svn_error_t * +check_hook_result(const char *name, const char *cmd, apr_proc_t *cmd_proc, + apr_file_t *read_errhandle, apr_pool_t *pool) +{ + svn_error_t *err, *err2; + svn_stringbuf_t *native_stderr, *failure_message; + const char *utf8_stderr; + int exitcode; + apr_exit_why_e exitwhy; + + err2 = svn_stringbuf_from_aprfile(&native_stderr, read_errhandle, pool); + + err = svn_io_wait_for_cmd(cmd_proc, cmd, &exitcode, &exitwhy, pool); + if (err) + { + svn_error_clear(err2); + return svn_error_trace(err); + } + + if (APR_PROC_CHECK_EXIT(exitwhy) && exitcode == 0) + { + /* The hook exited cleanly. However, if we got an error reading + the hook's stderr, fail the hook anyway, because this might be + symptomatic of a more important problem. */ + if (err2) + { + return svn_error_createf + (SVN_ERR_REPOS_HOOK_FAILURE, err2, + _("'%s' hook succeeded, but error output could not be read"), + name); + } + + return SVN_NO_ERROR; + } + + /* The hook script failed. */ + + /* If we got the stderr output okay, try to translate it into UTF-8. + Ensure there is something sensible in the UTF-8 string regardless. */ + if (!err2) + { + err2 = svn_utf_cstring_to_utf8(&utf8_stderr, native_stderr->data, pool); + if (err2) + utf8_stderr = _("[Error output could not be translated from the " + "native locale to UTF-8.]"); + } + else + { + utf8_stderr = _("[Error output could not be read.]"); + } + /*### It would be nice to include the text of any translation or read + error in the messages above before we clear it here. */ + svn_error_clear(err2); + + if (!APR_PROC_CHECK_EXIT(exitwhy)) + { + failure_message = svn_stringbuf_createf(pool, + _("'%s' hook failed (did not exit cleanly: " + "apr_exit_why_e was %d, exitcode was %d). "), + name, exitwhy, exitcode); + } + else + { + const char *action; + if (strcmp(name, "start-commit") == 0 + || strcmp(name, "pre-commit") == 0) + action = _("Commit"); + else if (strcmp(name, "pre-revprop-change") == 0) + action = _("Revprop change"); + else if (strcmp(name, "pre-lock") == 0) + action = _("Lock"); + else if (strcmp(name, "pre-unlock") == 0) + action = _("Unlock"); + else + action = NULL; + if (action == NULL) + failure_message = svn_stringbuf_createf( + pool, _("%s hook failed (exit code %d)"), + name, exitcode); + else + failure_message = svn_stringbuf_createf( + pool, _("%s blocked by %s hook (exit code %d)"), + action, name, exitcode); + } + + if (utf8_stderr[0]) + { + svn_stringbuf_appendcstr(failure_message, + _(" with output:\n")); + svn_stringbuf_appendcstr(failure_message, utf8_stderr); + } + else + { + svn_stringbuf_appendcstr(failure_message, + _(" with no output.")); + } + + return svn_error_create(SVN_ERR_REPOS_HOOK_FAILURE, err, + failure_message->data); +} + +/* Copy the environment given as key/value pairs of ENV_HASH into + * an array of C strings allocated in RESULT_POOL. + * If the hook environment is empty, return NULL. + * Use SCRATCH_POOL for temporary allocations. */ +static const char ** +env_from_env_hash(apr_hash_t *env_hash, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_hash_index_t *hi; + const char **env; + const char **envp; + + if (!env_hash) + return NULL; + + env = apr_palloc(result_pool, + sizeof(const char *) * (apr_hash_count(env_hash) + 1)); + envp = env; + for (hi = apr_hash_first(scratch_pool, env_hash); hi; hi = apr_hash_next(hi)) + { + *envp = apr_psprintf(result_pool, "%s=%s", + (const char *)svn__apr_hash_index_key(hi), + (const char *)svn__apr_hash_index_val(hi)); + envp++; + } + *envp = NULL; + + return env; +} + +/* NAME, CMD and ARGS are the name, path to and arguments for the hook + program that is to be run. The hook's exit status will be checked, + and if an error occurred the hook's stderr output will be added to + the returned error. + + If STDIN_HANDLE is non-null, pass it as the hook's stdin, else pass + no stdin to the hook. + + If RESULT is non-null, set *RESULT to the stdout of the hook or to + a zero-length string if the hook generates no output on stdout. */ +static svn_error_t * +run_hook_cmd(svn_string_t **result, + const char *name, + const char *cmd, + const char **args, + apr_hash_t *hooks_env, + apr_file_t *stdin_handle, + apr_pool_t *pool) +{ + apr_file_t *null_handle; + apr_status_t apr_err; + svn_error_t *err; + apr_proc_t cmd_proc = {0}; + apr_pool_t *cmd_pool; + apr_hash_t *hook_env = NULL; + + if (result) + { + null_handle = NULL; + } + else + { + /* Redirect stdout to the null device */ + apr_err = apr_file_open(&null_handle, SVN_NULL_DEVICE_NAME, APR_WRITE, + APR_OS_DEFAULT, pool); + if (apr_err) + return svn_error_wrap_apr + (apr_err, _("Can't create null stdout for hook '%s'"), cmd); + } + + /* Tie resources allocated for the command to a special pool which we can + * destroy in order to clean up the stderr pipe opened for the process. */ + cmd_pool = svn_pool_create(pool); + + /* Check if a custom environment is defined for this hook, or else + * whether a default environment is defined. */ + if (hooks_env) + { + hook_env = svn_hash_gets(hooks_env, name); + if (hook_env == NULL) + hook_env = svn_hash_gets(hooks_env, + SVN_REPOS__HOOKS_ENV_DEFAULT_SECTION); + } + + err = svn_io_start_cmd3(&cmd_proc, ".", cmd, args, + env_from_env_hash(hook_env, pool, pool), + FALSE, FALSE, stdin_handle, result != NULL, + null_handle, TRUE, NULL, cmd_pool); + if (!err) + err = check_hook_result(name, cmd, &cmd_proc, cmd_proc.err, pool); + else + { + /* The command could not be started for some reason. */ + err = svn_error_createf(SVN_ERR_REPOS_BAD_ARGS, err, + _("Failed to start '%s' hook"), cmd); + } + + /* Hooks are fallible, and so hook failure is "expected" to occur at + times. When such a failure happens we still want to close the pipe + and null file */ + if (!err && result) + { + svn_stringbuf_t *native_stdout; + err = svn_stringbuf_from_aprfile(&native_stdout, cmd_proc.out, pool); + if (!err) + *result = svn_stringbuf__morph_into_string(native_stdout); + } + + /* Close resources allocated by svn_io_start_cmd3(), such as the pipe. */ + svn_pool_destroy(cmd_pool); + + /* Close the null handle. */ + if (null_handle) + { + apr_err = apr_file_close(null_handle); + if (!err && apr_err) + return svn_error_wrap_apr(apr_err, _("Error closing null file")); + } + + return svn_error_trace(err); +} + + +/* Create a temporary file F that will automatically be deleted when the + pool is cleaned up. Fill it with VALUE, and leave it open and rewound, + ready to be read from. */ +static svn_error_t * +create_temp_file(apr_file_t **f, const svn_string_t *value, apr_pool_t *pool) +{ + apr_off_t offset = 0; + + SVN_ERR(svn_io_open_unique_file3(f, NULL, NULL, + svn_io_file_del_on_pool_cleanup, + pool, pool)); + SVN_ERR(svn_io_file_write_full(*f, value->data, value->len, NULL, pool)); + return svn_io_file_seek(*f, APR_SET, &offset, pool); +} + + +/* Check if the HOOK program exists and is a file or a symbolic link, using + POOL for temporary allocations. + + If the hook exists but is a broken symbolic link, set *BROKEN_LINK + to TRUE, else if the hook program exists set *BROKEN_LINK to FALSE. + + Return the hook program if found, else return NULL and don't touch + *BROKEN_LINK. +*/ +static const char* +check_hook_cmd(const char *hook, svn_boolean_t *broken_link, apr_pool_t *pool) +{ + static const char* const check_extns[] = { +#ifdef WIN32 + /* For WIN32, we need to check with file name extension(s) added. + + As Windows Scripting Host (.wsf) files can accomodate (at least) + JavaScript (.js) and VB Script (.vbs) code, extensions for the + corresponding file types need not be enumerated explicitly. */ + ".exe", ".cmd", ".bat", ".wsf", /* ### Any other extensions? */ +#else + "", +#endif + NULL + }; + + const char *const *extn; + svn_error_t *err = NULL; + svn_boolean_t is_special; + for (extn = check_extns; *extn; ++extn) + { + const char *const hook_path = + (**extn ? apr_pstrcat(pool, hook, *extn, (char *)NULL) : hook); + + svn_node_kind_t kind; + if (!(err = svn_io_check_resolved_path(hook_path, &kind, pool)) + && kind == svn_node_file) + { + *broken_link = FALSE; + return hook_path; + } + svn_error_clear(err); + if (!(err = svn_io_check_special_path(hook_path, &kind, &is_special, + pool)) + && is_special) + { + *broken_link = TRUE; + return hook_path; + } + svn_error_clear(err); + } + return NULL; +} + +/* Baton for parse_hooks_env_option. */ +struct parse_hooks_env_option_baton { + /* The name of the section being parsed. If not the default section, + * the section name should match the name of a hook to which the + * options apply. */ + const char *section; + apr_hash_t *hooks_env; +} parse_hooks_env_option_baton; + +/* An implementation of svn_config_enumerator2_t. + * Set environment variable NAME to value VALUE in the environment for + * all hooks (in case the current section is the default section), + * or the hook with the name corresponding to the current section's name. */ +static svn_boolean_t +parse_hooks_env_option(const char *name, const char *value, + void *baton, apr_pool_t *pool) +{ + struct parse_hooks_env_option_baton *bo = baton; + apr_pool_t *result_pool = apr_hash_pool_get(bo->hooks_env); + apr_hash_t *hook_env; + + hook_env = svn_hash_gets(bo->hooks_env, bo->section); + if (hook_env == NULL) + { + hook_env = apr_hash_make(result_pool); + svn_hash_sets(bo->hooks_env, apr_pstrdup(result_pool, bo->section), + hook_env); + } + svn_hash_sets(hook_env, apr_pstrdup(result_pool, name), + apr_pstrdup(result_pool, value)); + + return TRUE; +} + +struct parse_hooks_env_section_baton { + svn_config_t *cfg; + apr_hash_t *hooks_env; +} parse_hooks_env_section_baton; + +/* An implementation of svn_config_section_enumerator2_t. */ +static svn_boolean_t +parse_hooks_env_section(const char *name, void *baton, apr_pool_t *pool) +{ + struct parse_hooks_env_section_baton *b = baton; + struct parse_hooks_env_option_baton bo; + + bo.section = name; + bo.hooks_env = b->hooks_env; + + (void)svn_config_enumerate2(b->cfg, name, parse_hooks_env_option, &bo, pool); + + return TRUE; +} + +svn_error_t * +svn_repos__parse_hooks_env(apr_hash_t **hooks_env_p, + const char *local_abspath, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_config_t *cfg; + struct parse_hooks_env_section_baton b; + + if (local_abspath) + { + SVN_ERR(svn_config_read3(&cfg, local_abspath, FALSE, + TRUE, TRUE, scratch_pool)); + b.cfg = cfg; + b.hooks_env = apr_hash_make(result_pool); + (void)svn_config_enumerate_sections2(cfg, parse_hooks_env_section, &b, + scratch_pool); + *hooks_env_p = b.hooks_env; + } + else + { + *hooks_env_p = NULL; + } + + return SVN_NO_ERROR; +} + +/* Return an error for the failure of HOOK due to a broken symlink. */ +static svn_error_t * +hook_symlink_error(const char *hook) +{ + return svn_error_createf + (SVN_ERR_REPOS_HOOK_FAILURE, NULL, + _("Failed to run '%s' hook; broken symlink"), hook); +} + +svn_error_t * +svn_repos__hooks_start_commit(svn_repos_t *repos, + apr_hash_t *hooks_env, + const char *user, + const apr_array_header_t *capabilities, + const char *txn_name, + apr_pool_t *pool) +{ + const char *hook = svn_repos_start_commit_hook(repos, pool); + svn_boolean_t broken_link; + + if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link) + { + return hook_symlink_error(hook); + } + else if (hook) + { + const char *args[6]; + char *capabilities_string; + + if (capabilities) + { + capabilities_string = svn_cstring_join(capabilities, ":", pool); + + /* Get rid of that annoying final colon. */ + if (capabilities_string[0]) + capabilities_string[strlen(capabilities_string) - 1] = '\0'; + } + else + { + capabilities_string = apr_pstrdup(pool, ""); + } + + args[0] = hook; + args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool); + args[2] = user ? user : ""; + args[3] = capabilities_string; + args[4] = txn_name; + args[5] = NULL; + + SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_START_COMMIT, hook, args, + hooks_env, NULL, pool)); + } + + return SVN_NO_ERROR; +} + +/* Set *HANDLE to an open filehandle for a temporary file (i.e., + automatically deleted when closed), into which the LOCK_TOKENS have + been written out in the format described in the pre-commit hook + template. + + LOCK_TOKENS is as returned by svn_fs__access_get_lock_tokens(). + + Allocate *HANDLE in POOL, and use POOL for temporary allocations. */ +static svn_error_t * +lock_token_content(apr_file_t **handle, apr_hash_t *lock_tokens, + apr_pool_t *pool) +{ + svn_stringbuf_t *lock_str = svn_stringbuf_create("LOCK-TOKENS:\n", pool); + apr_hash_index_t *hi; + + for (hi = apr_hash_first(pool, lock_tokens); hi; + hi = apr_hash_next(hi)) + { + void *val; + const char *path, *token; + + apr_hash_this(hi, (void *)&token, NULL, &val); + path = val; + svn_stringbuf_appendstr(lock_str, + svn_stringbuf_createf(pool, "%s|%s\n", + svn_path_uri_autoescape(path, pool), + token)); + } + + svn_stringbuf_appendcstr(lock_str, "\n"); + return create_temp_file(handle, + svn_stringbuf__morph_into_string(lock_str), pool); +} + + + +svn_error_t * +svn_repos__hooks_pre_commit(svn_repos_t *repos, + apr_hash_t *hooks_env, + const char *txn_name, + apr_pool_t *pool) +{ + const char *hook = svn_repos_pre_commit_hook(repos, pool); + svn_boolean_t broken_link; + + if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link) + { + return hook_symlink_error(hook); + } + else if (hook) + { + const char *args[4]; + svn_fs_access_t *access_ctx; + apr_file_t *stdin_handle = NULL; + + args[0] = hook; + args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool); + args[2] = txn_name; + args[3] = NULL; + + SVN_ERR(svn_fs_get_access(&access_ctx, repos->fs)); + if (access_ctx) + { + apr_hash_t *lock_tokens = svn_fs__access_get_lock_tokens(access_ctx); + if (apr_hash_count(lock_tokens)) { + SVN_ERR(lock_token_content(&stdin_handle, lock_tokens, pool)); + } + } + + if (!stdin_handle) + SVN_ERR(svn_io_file_open(&stdin_handle, SVN_NULL_DEVICE_NAME, + APR_READ, APR_OS_DEFAULT, pool)); + + SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_PRE_COMMIT, hook, args, + hooks_env, stdin_handle, pool)); + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos__hooks_post_commit(svn_repos_t *repos, + apr_hash_t *hooks_env, + svn_revnum_t rev, + const char *txn_name, + apr_pool_t *pool) +{ + const char *hook = svn_repos_post_commit_hook(repos, pool); + svn_boolean_t broken_link; + + if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link) + { + return hook_symlink_error(hook); + } + else if (hook) + { + const char *args[5]; + + args[0] = hook; + args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool); + args[2] = apr_psprintf(pool, "%ld", rev); + args[3] = txn_name; + args[4] = NULL; + + SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_POST_COMMIT, hook, args, + hooks_env, NULL, pool)); + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos__hooks_pre_revprop_change(svn_repos_t *repos, + apr_hash_t *hooks_env, + svn_revnum_t rev, + const char *author, + const char *name, + const svn_string_t *new_value, + char action, + apr_pool_t *pool) +{ + const char *hook = svn_repos_pre_revprop_change_hook(repos, pool); + svn_boolean_t broken_link; + + if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link) + { + return hook_symlink_error(hook); + } + else if (hook) + { + const char *args[7]; + apr_file_t *stdin_handle = NULL; + char action_string[2]; + + /* Pass the new value as stdin to hook */ + if (new_value) + SVN_ERR(create_temp_file(&stdin_handle, new_value, pool)); + else + SVN_ERR(svn_io_file_open(&stdin_handle, SVN_NULL_DEVICE_NAME, + APR_READ, APR_OS_DEFAULT, pool)); + + action_string[0] = action; + action_string[1] = '\0'; + + args[0] = hook; + args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool); + args[2] = apr_psprintf(pool, "%ld", rev); + args[3] = author ? author : ""; + args[4] = name; + args[5] = action_string; + args[6] = NULL; + + SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_PRE_REVPROP_CHANGE, hook, + args, hooks_env, stdin_handle, pool)); + + SVN_ERR(svn_io_file_close(stdin_handle, pool)); + } + else + { + /* If the pre- hook doesn't exist at all, then default to + MASSIVE PARANOIA. Changing revision properties is a lossy + operation; so unless the repository admininstrator has + *deliberately* created the pre-hook, disallow all changes. */ + return + svn_error_create + (SVN_ERR_REPOS_DISABLED_FEATURE, NULL, + _("Repository has not been enabled to accept revision propchanges;\n" + "ask the administrator to create a pre-revprop-change hook")); + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos__hooks_post_revprop_change(svn_repos_t *repos, + apr_hash_t *hooks_env, + svn_revnum_t rev, + const char *author, + const char *name, + const svn_string_t *old_value, + char action, + apr_pool_t *pool) +{ + const char *hook = svn_repos_post_revprop_change_hook(repos, pool); + svn_boolean_t broken_link; + + if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link) + { + return hook_symlink_error(hook); + } + else if (hook) + { + const char *args[7]; + apr_file_t *stdin_handle = NULL; + char action_string[2]; + + /* Pass the old value as stdin to hook */ + if (old_value) + SVN_ERR(create_temp_file(&stdin_handle, old_value, pool)); + else + SVN_ERR(svn_io_file_open(&stdin_handle, SVN_NULL_DEVICE_NAME, + APR_READ, APR_OS_DEFAULT, pool)); + + action_string[0] = action; + action_string[1] = '\0'; + + args[0] = hook; + args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool); + args[2] = apr_psprintf(pool, "%ld", rev); + args[3] = author ? author : ""; + args[4] = name; + args[5] = action_string; + args[6] = NULL; + + SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_POST_REVPROP_CHANGE, hook, + args, hooks_env, stdin_handle, pool)); + + SVN_ERR(svn_io_file_close(stdin_handle, pool)); + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos__hooks_pre_lock(svn_repos_t *repos, + apr_hash_t *hooks_env, + const char **token, + const char *path, + const char *username, + const char *comment, + svn_boolean_t steal_lock, + apr_pool_t *pool) +{ + const char *hook = svn_repos_pre_lock_hook(repos, pool); + svn_boolean_t broken_link; + + if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link) + { + return hook_symlink_error(hook); + } + else if (hook) + { + const char *args[7]; + svn_string_t *buf; + + + args[0] = hook; + args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool); + args[2] = path; + args[3] = username; + args[4] = comment ? comment : ""; + args[5] = steal_lock ? "1" : "0"; + args[6] = NULL; + + SVN_ERR(run_hook_cmd(&buf, SVN_REPOS__HOOK_PRE_LOCK, hook, args, + hooks_env, NULL, pool)); + + if (token) + /* No validation here; the FS will take care of that. */ + *token = buf->data; + + } + else if (token) + *token = ""; + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos__hooks_post_lock(svn_repos_t *repos, + apr_hash_t *hooks_env, + const apr_array_header_t *paths, + const char *username, + apr_pool_t *pool) +{ + const char *hook = svn_repos_post_lock_hook(repos, pool); + svn_boolean_t broken_link; + + if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link) + { + return hook_symlink_error(hook); + } + else if (hook) + { + const char *args[5]; + apr_file_t *stdin_handle = NULL; + svn_string_t *paths_str = svn_string_create(svn_cstring_join + (paths, "\n", pool), + pool); + + SVN_ERR(create_temp_file(&stdin_handle, paths_str, pool)); + + args[0] = hook; + args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool); + args[2] = username; + args[3] = NULL; + args[4] = NULL; + + SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_POST_LOCK, hook, args, + hooks_env, stdin_handle, pool)); + + SVN_ERR(svn_io_file_close(stdin_handle, pool)); + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos__hooks_pre_unlock(svn_repos_t *repos, + apr_hash_t *hooks_env, + const char *path, + const char *username, + const char *token, + svn_boolean_t break_lock, + apr_pool_t *pool) +{ + const char *hook = svn_repos_pre_unlock_hook(repos, pool); + svn_boolean_t broken_link; + + if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link) + { + return hook_symlink_error(hook); + } + else if (hook) + { + const char *args[7]; + + args[0] = hook; + args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool); + args[2] = path; + args[3] = username ? username : ""; + args[4] = token ? token : ""; + args[5] = break_lock ? "1" : "0"; + args[6] = NULL; + + SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_PRE_UNLOCK, hook, args, + hooks_env, NULL, pool)); + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos__hooks_post_unlock(svn_repos_t *repos, + apr_hash_t *hooks_env, + const apr_array_header_t *paths, + const char *username, + apr_pool_t *pool) +{ + const char *hook = svn_repos_post_unlock_hook(repos, pool); + svn_boolean_t broken_link; + + if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link) + { + return hook_symlink_error(hook); + } + else if (hook) + { + const char *args[5]; + apr_file_t *stdin_handle = NULL; + svn_string_t *paths_str = svn_string_create(svn_cstring_join + (paths, "\n", pool), + pool); + + SVN_ERR(create_temp_file(&stdin_handle, paths_str, pool)); + + args[0] = hook; + args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool); + args[2] = username ? username : ""; + args[3] = NULL; + args[4] = NULL; + + SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_POST_UNLOCK, hook, args, + hooks_env, stdin_handle, pool)); + + SVN_ERR(svn_io_file_close(stdin_handle, pool)); + } + + return SVN_NO_ERROR; +} + + + +/* + * vim:ts=4:sw=4:expandtab:tw=80:fo=tcroq + * vim:isk=a-z,A-Z,48-57,_,.,-,> + * vim:cino=>1s,e0,n0,f0,{.5s,}0,^-.5s,=.5s,t0,+1s,c3,(0,u0,\:0 + */ diff --git a/subversion/libsvn_repos/load-fs-vtable.c b/subversion/libsvn_repos/load-fs-vtable.c new file mode 100644 index 0000000..c8c5e95 --- /dev/null +++ b/subversion/libsvn_repos/load-fs-vtable.c @@ -0,0 +1,1140 @@ +/* load-fs-vtable.c --- dumpstream loader vtable for committing into a + * Subversion filesystem. + * + * ==================================================================== + * 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 "svn_private_config.h" +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_fs.h" +#include "svn_repos.h" +#include "svn_string.h" +#include "svn_props.h" +#include "repos.h" +#include "svn_private_config.h" +#include "svn_mergeinfo.h" +#include "svn_checksum.h" +#include "svn_subst.h" +#include "svn_ctype.h" +#include "svn_dirent_uri.h" + +#include <apr_lib.h> + +#include "private/svn_fspath.h" +#include "private/svn_dep_compat.h" +#include "private/svn_mergeinfo_private.h" + +/*----------------------------------------------------------------------*/ + +/** Batons used herein **/ + +struct parse_baton +{ + svn_repos_t *repos; + svn_fs_t *fs; + + svn_boolean_t use_history; + svn_boolean_t validate_props; + svn_boolean_t use_pre_commit_hook; + svn_boolean_t use_post_commit_hook; + enum svn_repos_load_uuid uuid_action; + const char *parent_dir; /* repository relpath, or NULL */ + svn_repos_notify_func_t notify_func; + void *notify_baton; + svn_repos_notify_t *notify; + apr_pool_t *pool; + + /* Start and end (inclusive) of revision range we'll pay attention + to, or a pair of SVN_INVALID_REVNUMs if we're not filtering by + revisions. */ + svn_revnum_t start_rev; + svn_revnum_t end_rev; + + /* A hash mapping copy-from revisions and mergeinfo range revisions + (svn_revnum_t *) in the dump stream to their corresponding revisions + (svn_revnum_t *) in the loaded repository. The hash and its + contents are allocated in POOL. */ + /* ### See http://subversion.tigris.org/issues/show_bug.cgi?id=3903 + ### for discussion about improving the memory costs of this mapping. */ + apr_hash_t *rev_map; + + /* The most recent (youngest) revision from the dump stream mapped in + REV_MAP. If no revisions have been mapped yet, this is set to + SVN_INVALID_REVNUM. */ + svn_revnum_t last_rev_mapped; + + /* The oldest old revision loaded from the dump stream. If no revisions + have been loaded yet, this is set to SVN_INVALID_REVNUM. */ + svn_revnum_t oldest_old_rev; +}; + +struct revision_baton +{ + svn_revnum_t rev; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root; + + const svn_string_t *datestamp; + + apr_int32_t rev_offset; + svn_boolean_t skipped; + + struct parse_baton *pb; + apr_pool_t *pool; +}; + +struct node_baton +{ + const char *path; + svn_node_kind_t kind; + enum svn_node_action action; + svn_checksum_t *base_checksum; /* null, if not available */ + svn_checksum_t *result_checksum; /* null, if not available */ + svn_checksum_t *copy_source_checksum; /* null, if not available */ + + svn_revnum_t copyfrom_rev; + const char *copyfrom_path; + + struct revision_baton *rb; + apr_pool_t *pool; +}; + + +/*----------------------------------------------------------------------*/ + +/* Record the mapping of FROM_REV to TO_REV in REV_MAP, ensuring that + anything added to the hash is allocated in the hash's pool. */ +static void +set_revision_mapping(apr_hash_t *rev_map, + svn_revnum_t from_rev, + svn_revnum_t to_rev) +{ + svn_revnum_t *mapped_revs = apr_palloc(apr_hash_pool_get(rev_map), + sizeof(svn_revnum_t) * 2); + mapped_revs[0] = from_rev; + mapped_revs[1] = to_rev; + apr_hash_set(rev_map, mapped_revs, + sizeof(svn_revnum_t), mapped_revs + 1); +} + +/* Return the revision to which FROM_REV maps in REV_MAP, or + SVN_INVALID_REVNUM if no such mapping exists. */ +static svn_revnum_t +get_revision_mapping(apr_hash_t *rev_map, + svn_revnum_t from_rev) +{ + svn_revnum_t *to_rev = apr_hash_get(rev_map, &from_rev, + sizeof(from_rev)); + return to_rev ? *to_rev : SVN_INVALID_REVNUM; +} + + +/* Change revision property NAME to VALUE for REVISION in REPOS. If + VALIDATE_PROPS is set, use functions which perform validation of + the property value. Otherwise, bypass those checks. */ +static svn_error_t * +change_rev_prop(svn_repos_t *repos, + svn_revnum_t revision, + const char *name, + const svn_string_t *value, + svn_boolean_t validate_props, + apr_pool_t *pool) +{ + if (validate_props) + return svn_repos_fs_change_rev_prop4(repos, revision, NULL, name, + NULL, value, FALSE, FALSE, + NULL, NULL, pool); + else + return svn_fs_change_rev_prop2(svn_repos_fs(repos), revision, name, + NULL, value, pool); +} + +/* Change property NAME to VALUE for PATH in TXN_ROOT. If + VALIDATE_PROPS is set, use functions which perform validation of + the property value. Otherwise, bypass those checks. */ +static svn_error_t * +change_node_prop(svn_fs_root_t *txn_root, + const char *path, + const char *name, + const svn_string_t *value, + svn_boolean_t validate_props, + apr_pool_t *pool) +{ + if (validate_props) + return svn_repos_fs_change_node_prop(txn_root, path, name, value, pool); + else + return svn_fs_change_node_prop(txn_root, path, name, value, pool); +} + +/* Prepend the mergeinfo source paths in MERGEINFO_ORIG with PARENT_DIR, and + return it in *MERGEINFO_VAL. */ +/* ### FIXME: Consider somehow sharing code with + ### svnrdump/load_editor.c:prefix_mergeinfo_paths() */ +static svn_error_t * +prefix_mergeinfo_paths(svn_string_t **mergeinfo_val, + const svn_string_t *mergeinfo_orig, + const char *parent_dir, + apr_pool_t *pool) +{ + apr_hash_t *prefixed_mergeinfo, *mergeinfo; + apr_hash_index_t *hi; + void *rangelist; + + SVN_ERR(svn_mergeinfo_parse(&mergeinfo, mergeinfo_orig->data, pool)); + prefixed_mergeinfo = apr_hash_make(pool); + for (hi = apr_hash_first(pool, mergeinfo); hi; hi = apr_hash_next(hi)) + { + const void *key; + const char *path, *merge_source; + + apr_hash_this(hi, &key, NULL, &rangelist); + merge_source = svn_relpath_canonicalize(key, pool); + + /* The svn:mergeinfo property syntax demands a repos abspath */ + path = svn_fspath__canonicalize(svn_relpath_join(parent_dir, + merge_source, pool), + pool); + svn_hash_sets(prefixed_mergeinfo, path, rangelist); + } + return svn_mergeinfo_to_string(mergeinfo_val, prefixed_mergeinfo, pool); +} + + +/* Examine the mergeinfo in INITIAL_VAL, renumber revisions in rangelists + as appropriate, and return the (possibly new) mergeinfo in *FINAL_VAL + (allocated from POOL). */ +/* ### FIXME: Consider somehow sharing code with + ### svnrdump/load_editor.c:renumber_mergeinfo_revs() */ +static svn_error_t * +renumber_mergeinfo_revs(svn_string_t **final_val, + const svn_string_t *initial_val, + struct revision_baton *rb, + apr_pool_t *pool) +{ + apr_pool_t *subpool = svn_pool_create(pool); + svn_mergeinfo_t mergeinfo, predates_stream_mergeinfo; + svn_mergeinfo_t final_mergeinfo = apr_hash_make(subpool); + apr_hash_index_t *hi; + + SVN_ERR(svn_mergeinfo_parse(&mergeinfo, initial_val->data, subpool)); + + /* Issue #3020 + http://subversion.tigris.org/issues/show_bug.cgi?id=3020#desc16 + Remove mergeinfo older than the oldest revision in the dump stream + and adjust its revisions by the difference between the head rev of + the target repository and the current dump stream rev. */ + if (rb->pb->oldest_old_rev > 1) + { + SVN_ERR(svn_mergeinfo__filter_mergeinfo_by_ranges( + &predates_stream_mergeinfo, mergeinfo, + rb->pb->oldest_old_rev - 1, 0, + TRUE, subpool, subpool)); + SVN_ERR(svn_mergeinfo__filter_mergeinfo_by_ranges( + &mergeinfo, mergeinfo, + rb->pb->oldest_old_rev - 1, 0, + FALSE, subpool, subpool)); + SVN_ERR(svn_mergeinfo__adjust_mergeinfo_rangelists( + &predates_stream_mergeinfo, predates_stream_mergeinfo, + -rb->rev_offset, subpool, subpool)); + } + else + { + predates_stream_mergeinfo = NULL; + } + + for (hi = apr_hash_first(subpool, mergeinfo); hi; hi = apr_hash_next(hi)) + { + const char *merge_source; + svn_rangelist_t *rangelist; + struct parse_baton *pb = rb->pb; + int i; + const void *key; + void *val; + + apr_hash_this(hi, &key, NULL, &val); + merge_source = key; + rangelist = val; + + /* Possibly renumber revisions in merge source's rangelist. */ + for (i = 0; i < rangelist->nelts; i++) + { + svn_revnum_t rev_from_map; + svn_merge_range_t *range = APR_ARRAY_IDX(rangelist, i, + svn_merge_range_t *); + rev_from_map = get_revision_mapping(pb->rev_map, range->start); + if (SVN_IS_VALID_REVNUM(rev_from_map)) + { + range->start = rev_from_map; + } + else if (range->start == pb->oldest_old_rev - 1) + { + /* Since the start revision of svn_merge_range_t are not + inclusive there is one possible valid start revision that + won't be found in the PB->REV_MAP mapping of load stream + revsions to loaded revisions: The revision immediately + preceeding the oldest revision from the load stream. + This is a valid revision for mergeinfo, but not a valid + copy from revision (which PB->REV_MAP also maps for) so it + will never be in the mapping. + + If that is what we have here, then find the mapping for the + oldest rev from the load stream and subtract 1 to get the + renumbered, non-inclusive, start revision. */ + rev_from_map = get_revision_mapping(pb->rev_map, + pb->oldest_old_rev); + if (SVN_IS_VALID_REVNUM(rev_from_map)) + range->start = rev_from_map - 1; + } + else + { + /* If we can't remap the start revision then don't even bother + trying to remap the end revision. It's possible we might + actually succeed at the latter, which can result in invalid + mergeinfo with a start rev > end rev. If that gets into the + repository then a world of bustage breaks loose anytime that + bogus mergeinfo is parsed. See + http://subversion.tigris.org/issues/show_bug.cgi?id=3020#desc16. + */ + continue; + } + + rev_from_map = get_revision_mapping(pb->rev_map, range->end); + if (SVN_IS_VALID_REVNUM(rev_from_map)) + range->end = rev_from_map; + } + svn_hash_sets(final_mergeinfo, merge_source, rangelist); + } + + if (predates_stream_mergeinfo) + SVN_ERR(svn_mergeinfo_merge2(final_mergeinfo, predates_stream_mergeinfo, + subpool, subpool)); + + SVN_ERR(svn_mergeinfo_sort(final_mergeinfo, subpool)); + + /* Mergeinfo revision sources for r0 and r1 are invalid; you can't merge r0 + or r1. However, svndumpfilter can be abused to produce r1 merge source + revs. So if we encounter any, then strip them out, no need to put them + into the load target. */ + SVN_ERR(svn_mergeinfo__filter_mergeinfo_by_ranges(&final_mergeinfo, + final_mergeinfo, + 1, 0, FALSE, + subpool, subpool)); + + SVN_ERR(svn_mergeinfo_to_string(final_val, final_mergeinfo, pool)); + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + +/*----------------------------------------------------------------------*/ + +/** vtable for doing commits to a fs **/ + + +static svn_error_t * +make_node_baton(struct node_baton **node_baton_p, + apr_hash_t *headers, + struct revision_baton *rb, + apr_pool_t *pool) +{ + struct node_baton *nb = apr_pcalloc(pool, sizeof(*nb)); + const char *val; + + /* Start with sensible defaults. */ + nb->rb = rb; + nb->pool = pool; + nb->kind = svn_node_unknown; + + /* Then add info from the headers. */ + if ((val = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_PATH))) + { + val = svn_relpath_canonicalize(val, pool); + if (rb->pb->parent_dir) + nb->path = svn_relpath_join(rb->pb->parent_dir, val, pool); + else + nb->path = val; + } + + if ((val = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_KIND))) + { + if (! strcmp(val, "file")) + nb->kind = svn_node_file; + else if (! strcmp(val, "dir")) + nb->kind = svn_node_dir; + } + + nb->action = (enum svn_node_action)(-1); /* an invalid action code */ + if ((val = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_ACTION))) + { + if (! strcmp(val, "change")) + nb->action = svn_node_action_change; + else if (! strcmp(val, "add")) + nb->action = svn_node_action_add; + else if (! strcmp(val, "delete")) + nb->action = svn_node_action_delete; + else if (! strcmp(val, "replace")) + nb->action = svn_node_action_replace; + } + + nb->copyfrom_rev = SVN_INVALID_REVNUM; + if ((val = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV))) + { + nb->copyfrom_rev = SVN_STR_TO_REV(val); + } + if ((val = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH))) + { + val = svn_relpath_canonicalize(val, pool); + if (rb->pb->parent_dir) + nb->copyfrom_path = svn_relpath_join(rb->pb->parent_dir, val, pool); + else + nb->copyfrom_path = val; + } + + if ((val = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_TEXT_CONTENT_CHECKSUM))) + { + SVN_ERR(svn_checksum_parse_hex(&nb->result_checksum, svn_checksum_md5, + val, pool)); + } + + if ((val = svn_hash_gets(headers, + SVN_REPOS_DUMPFILE_TEXT_DELTA_BASE_CHECKSUM))) + { + SVN_ERR(svn_checksum_parse_hex(&nb->base_checksum, svn_checksum_md5, val, + pool)); + } + + if ((val = svn_hash_gets(headers, + SVN_REPOS_DUMPFILE_TEXT_COPY_SOURCE_CHECKSUM))) + { + SVN_ERR(svn_checksum_parse_hex(&nb->copy_source_checksum, + svn_checksum_md5, val, pool)); + } + + /* What's cool about this dump format is that the parser just + ignores any unrecognized headers. :-) */ + + *node_baton_p = nb; + return SVN_NO_ERROR; +} + +static struct revision_baton * +make_revision_baton(apr_hash_t *headers, + struct parse_baton *pb, + apr_pool_t *pool) +{ + struct revision_baton *rb = apr_pcalloc(pool, sizeof(*rb)); + const char *val; + + rb->pb = pb; + rb->pool = pool; + rb->rev = SVN_INVALID_REVNUM; + + if ((val = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_REVISION_NUMBER))) + { + rb->rev = SVN_STR_TO_REV(val); + + /* If we're filtering revisions, is this one we'll skip? */ + rb->skipped = (SVN_IS_VALID_REVNUM(pb->start_rev) + && ((rb->rev < pb->start_rev) || + (rb->rev > pb->end_rev))); + } + + return rb; +} + + +static svn_error_t * +new_revision_record(void **revision_baton, + apr_hash_t *headers, + void *parse_baton, + apr_pool_t *pool) +{ + struct parse_baton *pb = parse_baton; + struct revision_baton *rb; + svn_revnum_t head_rev; + + rb = make_revision_baton(headers, pb, pool); + + /* ### If we're filtering revisions, and this is one we've skipped, + ### and we've skipped it because it has a revision number younger + ### than the youngest in our acceptable range, then should we + ### just bail out here? */ + /* + if (rb->skipped && (rb->rev > pb->end_rev)) + return svn_error_createf(SVN_ERR_CEASE_INVOCATION, 0, + _("Finished processing acceptable load " + "revision range")); + */ + + SVN_ERR(svn_fs_youngest_rev(&head_rev, pb->fs, pool)); + + /* FIXME: This is a lame fallback loading multiple segments of dump in + several separate operations. It is highly susceptible to race conditions. + Calculate the revision 'offset' for finding copyfrom sources. + It might be positive or negative. */ + rb->rev_offset = (apr_int32_t) ((rb->rev) - (head_rev + 1)); + + if ((rb->rev > 0) && (! rb->skipped)) + { + /* Create a new fs txn. */ + SVN_ERR(svn_fs_begin_txn2(&(rb->txn), pb->fs, head_rev, 0, pool)); + SVN_ERR(svn_fs_txn_root(&(rb->txn_root), rb->txn, pool)); + + if (pb->notify_func) + { + pb->notify->action = svn_repos_notify_load_txn_start; + pb->notify->old_revision = rb->rev; + pb->notify_func(pb->notify_baton, pb->notify, rb->pool); + } + + /* Stash the oldest "old" revision committed from the load stream. */ + if (!SVN_IS_VALID_REVNUM(pb->oldest_old_rev)) + pb->oldest_old_rev = rb->rev; + } + + /* If we're skipping this revision, try to notify someone. */ + if (rb->skipped && pb->notify_func) + { + pb->notify->action = svn_repos_notify_load_skipped_rev; + pb->notify->old_revision = rb->rev; + pb->notify_func(pb->notify_baton, pb->notify, rb->pool); + } + + /* If we're parsing revision 0, only the revision are (possibly) + interesting to us: when loading the stream into an empty + filesystem, then we want new filesystem's revision 0 to have the + same props. Otherwise, we just ignore revision 0 in the stream. */ + + *revision_baton = rb; + return SVN_NO_ERROR; +} + + + +/* Factorized helper func for new_node_record() */ +static svn_error_t * +maybe_add_with_history(struct node_baton *nb, + struct revision_baton *rb, + apr_pool_t *pool) +{ + struct parse_baton *pb = rb->pb; + + if ((nb->copyfrom_path == NULL) || (! pb->use_history)) + { + /* Add empty file or dir, without history. */ + if (nb->kind == svn_node_file) + SVN_ERR(svn_fs_make_file(rb->txn_root, nb->path, pool)); + + else if (nb->kind == svn_node_dir) + SVN_ERR(svn_fs_make_dir(rb->txn_root, nb->path, pool)); + } + else + { + /* Hunt down the source revision in this fs. */ + svn_fs_root_t *copy_root; + svn_revnum_t copyfrom_rev; + + /* Try to find the copyfrom revision in the revision map; + failing that, fall back to the revision offset approach. */ + copyfrom_rev = get_revision_mapping(rb->pb->rev_map, nb->copyfrom_rev); + if (! SVN_IS_VALID_REVNUM(copyfrom_rev)) + copyfrom_rev = nb->copyfrom_rev - rb->rev_offset; + + if (! SVN_IS_VALID_REVNUM(copyfrom_rev)) + return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, + _("Relative source revision %ld is not" + " available in current repository"), + copyfrom_rev); + + SVN_ERR(svn_fs_revision_root(©_root, pb->fs, copyfrom_rev, pool)); + + if (nb->copy_source_checksum) + { + svn_checksum_t *checksum; + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, copy_root, + nb->copyfrom_path, TRUE, pool)); + if (!svn_checksum_match(nb->copy_source_checksum, checksum)) + return svn_checksum_mismatch_err(nb->copy_source_checksum, + checksum, pool, + _("Copy source checksum mismatch on copy from '%s'@%ld\n" + "to '%s' in rev based on r%ld"), + nb->copyfrom_path, copyfrom_rev, nb->path, rb->rev); + } + + SVN_ERR(svn_fs_copy(copy_root, nb->copyfrom_path, + rb->txn_root, nb->path, pool)); + + if (pb->notify_func) + { + pb->notify->action = svn_repos_notify_load_copied_node; + pb->notify_func(pb->notify_baton, pb->notify, rb->pool); + } + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +magic_header_record(int version, + void *parse_baton, + apr_pool_t *pool) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +uuid_record(const char *uuid, + void *parse_baton, + apr_pool_t *pool) +{ + struct parse_baton *pb = parse_baton; + svn_revnum_t youngest_rev; + + if (pb->uuid_action == svn_repos_load_uuid_ignore) + return SVN_NO_ERROR; + + if (pb->uuid_action != svn_repos_load_uuid_force) + { + SVN_ERR(svn_fs_youngest_rev(&youngest_rev, pb->fs, pool)); + if (youngest_rev != 0) + return SVN_NO_ERROR; + } + + return svn_fs_set_uuid(pb->fs, uuid, pool); +} + +static svn_error_t * +new_node_record(void **node_baton, + apr_hash_t *headers, + void *revision_baton, + apr_pool_t *pool) +{ + struct revision_baton *rb = revision_baton; + struct parse_baton *pb = rb->pb; + struct node_baton *nb; + + if (rb->rev == 0) + return svn_error_create(SVN_ERR_STREAM_MALFORMED_DATA, NULL, + _("Malformed dumpstream: " + "Revision 0 must not contain node records")); + + SVN_ERR(make_node_baton(&nb, headers, rb, pool)); + + /* If we're skipping this revision, we're done here. */ + if (rb->skipped) + { + *node_baton = nb; + return SVN_NO_ERROR; + } + + /* Make sure we have an action we recognize. */ + if (nb->action < svn_node_action_change + || nb->action > svn_node_action_replace) + return svn_error_createf(SVN_ERR_STREAM_UNRECOGNIZED_DATA, NULL, + _("Unrecognized node-action on node '%s'"), + nb->path); + + if (pb->notify_func) + { + pb->notify->action = svn_repos_notify_load_node_start; + pb->notify->node_action = nb->action; + pb->notify->path = nb->path; + pb->notify_func(pb->notify_baton, pb->notify, rb->pool); + } + + switch (nb->action) + { + case svn_node_action_change: + break; + + case svn_node_action_delete: + SVN_ERR(svn_fs_delete(rb->txn_root, nb->path, pool)); + break; + + case svn_node_action_add: + SVN_ERR(maybe_add_with_history(nb, rb, pool)); + break; + + case svn_node_action_replace: + SVN_ERR(svn_fs_delete(rb->txn_root, nb->path, pool)); + SVN_ERR(maybe_add_with_history(nb, rb, pool)); + break; + } + + *node_baton = nb; + return SVN_NO_ERROR; +} + +static svn_error_t * +set_revision_property(void *baton, + const char *name, + const svn_string_t *value) +{ + struct revision_baton *rb = baton; + + /* If we're skipping this revision, we're done here. */ + if (rb->skipped) + return SVN_NO_ERROR; + + if (rb->rev > 0) + { + if (rb->pb->validate_props) + SVN_ERR(svn_repos_fs_change_txn_prop(rb->txn, name, value, rb->pool)); + else + SVN_ERR(svn_fs_change_txn_prop(rb->txn, name, value, rb->pool)); + + /* Remember any datestamp that passes through! (See comment in + close_revision() below.) */ + if (! strcmp(name, SVN_PROP_REVISION_DATE)) + rb->datestamp = svn_string_dup(value, rb->pool); + } + else if (rb->rev == 0) + { + /* Special case: set revision 0 properties when loading into an + 'empty' filesystem. */ + struct parse_baton *pb = rb->pb; + svn_revnum_t youngest_rev; + + SVN_ERR(svn_fs_youngest_rev(&youngest_rev, pb->fs, rb->pool)); + + if (youngest_rev == 0) + SVN_ERR(change_rev_prop(pb->repos, 0, name, value, + pb->validate_props, rb->pool)); + } + + return SVN_NO_ERROR; +} + + +static svn_error_t * +set_node_property(void *baton, + const char *name, + const svn_string_t *value) +{ + struct node_baton *nb = baton; + struct revision_baton *rb = nb->rb; + struct parse_baton *pb = rb->pb; + + /* If we're skipping this revision, we're done here. */ + if (rb->skipped) + return SVN_NO_ERROR; + + if (strcmp(name, SVN_PROP_MERGEINFO) == 0) + { + svn_string_t *renumbered_mergeinfo; + /* ### Need to cast away const. We cannot change the declaration of + * ### this function since it is part of svn_repos_parse_fns2_t. */ + svn_string_t *prop_val = (svn_string_t *)value; + + /* Tolerate mergeinfo with "\r\n" line endings because some + dumpstream sources might contain as much. If so normalize + the line endings to '\n' and make a notification to + PARSE_BATON->FEEDBACK_STREAM that we have made this + correction. */ + if (strstr(prop_val->data, "\r")) + { + const char *prop_eol_normalized; + + SVN_ERR(svn_subst_translate_cstring2(prop_val->data, + &prop_eol_normalized, + "\n", /* translate to LF */ + FALSE, /* no repair */ + NULL, /* no keywords */ + FALSE, /* no expansion */ + nb->pool)); + prop_val->data = prop_eol_normalized; + prop_val->len = strlen(prop_eol_normalized); + + if (pb->notify_func) + { + pb->notify->action = svn_repos_notify_load_normalized_mergeinfo; + pb->notify_func(pb->notify_baton, pb->notify, nb->pool); + } + } + + /* Renumber mergeinfo as appropriate. */ + SVN_ERR(renumber_mergeinfo_revs(&renumbered_mergeinfo, prop_val, rb, + nb->pool)); + value = renumbered_mergeinfo; + if (pb->parent_dir) + { + /* Prefix the merge source paths with PB->parent_dir. */ + /* ASSUMPTION: All source paths are included in the dump stream. */ + svn_string_t *mergeinfo_val; + SVN_ERR(prefix_mergeinfo_paths(&mergeinfo_val, value, + pb->parent_dir, nb->pool)); + value = mergeinfo_val; + } + } + + return change_node_prop(rb->txn_root, nb->path, name, value, + pb->validate_props, nb->pool); +} + + +static svn_error_t * +delete_node_property(void *baton, + const char *name) +{ + struct node_baton *nb = baton; + struct revision_baton *rb = nb->rb; + + /* If we're skipping this revision, we're done here. */ + if (rb->skipped) + return SVN_NO_ERROR; + + return change_node_prop(rb->txn_root, nb->path, name, NULL, + rb->pb->validate_props, nb->pool); +} + + +static svn_error_t * +remove_node_props(void *baton) +{ + struct node_baton *nb = baton; + struct revision_baton *rb = nb->rb; + apr_hash_t *proplist; + apr_hash_index_t *hi; + + /* If we're skipping this revision, we're done here. */ + if (rb->skipped) + return SVN_NO_ERROR; + + SVN_ERR(svn_fs_node_proplist(&proplist, + rb->txn_root, nb->path, nb->pool)); + + for (hi = apr_hash_first(nb->pool, proplist); hi; hi = apr_hash_next(hi)) + { + const void *key; + + apr_hash_this(hi, &key, NULL, NULL); + SVN_ERR(change_node_prop(rb->txn_root, nb->path, key, NULL, + rb->pb->validate_props, nb->pool)); + } + + return SVN_NO_ERROR; +} + + +static svn_error_t * +apply_textdelta(svn_txdelta_window_handler_t *handler, + void **handler_baton, + void *node_baton) +{ + struct node_baton *nb = node_baton; + struct revision_baton *rb = nb->rb; + + /* If we're skipping this revision, we're done here. */ + if (rb->skipped) + { + *handler = NULL; + return SVN_NO_ERROR; + } + + return svn_fs_apply_textdelta(handler, handler_baton, + rb->txn_root, nb->path, + svn_checksum_to_cstring(nb->base_checksum, + nb->pool), + svn_checksum_to_cstring(nb->result_checksum, + nb->pool), + nb->pool); +} + + +static svn_error_t * +set_fulltext(svn_stream_t **stream, + void *node_baton) +{ + struct node_baton *nb = node_baton; + struct revision_baton *rb = nb->rb; + + /* If we're skipping this revision, we're done here. */ + if (rb->skipped) + { + *stream = NULL; + return SVN_NO_ERROR; + } + + return svn_fs_apply_text(stream, + rb->txn_root, nb->path, + svn_checksum_to_cstring(nb->result_checksum, + nb->pool), + nb->pool); +} + + +static svn_error_t * +close_node(void *baton) +{ + struct node_baton *nb = baton; + struct revision_baton *rb = nb->rb; + struct parse_baton *pb = rb->pb; + + /* If we're skipping this revision, we're done here. */ + if (rb->skipped) + return SVN_NO_ERROR; + + if (pb->notify_func) + { + pb->notify->action = svn_repos_notify_load_node_done; + pb->notify_func(pb->notify_baton, pb->notify, rb->pool); + } + + return SVN_NO_ERROR; +} + + +static svn_error_t * +close_revision(void *baton) +{ + struct revision_baton *rb = baton; + struct parse_baton *pb = rb->pb; + const char *conflict_msg = NULL; + svn_revnum_t committed_rev; + svn_error_t *err; + const char *txn_name = NULL; + apr_hash_t *hooks_env; + + /* If we're skipping this revision or it has an invalid revision + number, we're done here. */ + if (rb->skipped || (rb->rev <= 0)) + return SVN_NO_ERROR; + + /* Get the txn name and hooks environment if they will be needed. */ + if (pb->use_pre_commit_hook || pb->use_post_commit_hook) + { + SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, pb->repos->hooks_env_path, + rb->pool, rb->pool)); + + err = svn_fs_txn_name(&txn_name, rb->txn, rb->pool); + if (err) + { + svn_error_clear(svn_fs_abort_txn(rb->txn, rb->pool)); + return svn_error_trace(err); + } + } + + /* Run the pre-commit hook, if so commanded. */ + if (pb->use_pre_commit_hook) + { + err = svn_repos__hooks_pre_commit(pb->repos, hooks_env, + txn_name, rb->pool); + if (err) + { + svn_error_clear(svn_fs_abort_txn(rb->txn, rb->pool)); + return svn_error_trace(err); + } + } + + /* Commit. */ + err = svn_fs_commit_txn(&conflict_msg, &committed_rev, rb->txn, rb->pool); + if (SVN_IS_VALID_REVNUM(committed_rev)) + { + if (err) + { + /* ### Log any error, but better yet is to rev + ### close_revision()'s API to allow both committed_rev and err + ### to be returned, see #3768. */ + svn_error_clear(err); + } + } + else + { + svn_error_clear(svn_fs_abort_txn(rb->txn, rb->pool)); + if (conflict_msg) + return svn_error_quick_wrap(err, conflict_msg); + else + return svn_error_trace(err); + } + + /* Run post-commit hook, if so commanded. */ + if (pb->use_post_commit_hook) + { + if ((err = svn_repos__hooks_post_commit(pb->repos, hooks_env, + committed_rev, txn_name, + rb->pool))) + return svn_error_create + (SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED, err, + _("Commit succeeded, but post-commit hook failed")); + } + + /* After a successful commit, must record the dump-rev -> in-repos-rev + mapping, so that copyfrom instructions in the dump file can look up the + correct repository revision to copy from. */ + set_revision_mapping(pb->rev_map, rb->rev, committed_rev); + + /* If the incoming dump stream has non-contiguous revisions (e.g. from + using svndumpfilter --drop-empty-revs without --renumber-revs) then + we must account for the missing gaps in PB->REV_MAP. Otherwise we + might not be able to map all mergeinfo source revisions to the correct + revisions in the target repos. */ + if ((pb->last_rev_mapped != SVN_INVALID_REVNUM) + && (rb->rev != pb->last_rev_mapped + 1)) + { + svn_revnum_t i; + + for (i = pb->last_rev_mapped + 1; i < rb->rev; i++) + { + set_revision_mapping(pb->rev_map, i, pb->last_rev_mapped); + } + } + + /* Update our "last revision mapped". */ + pb->last_rev_mapped = rb->rev; + + /* Deltify the predecessors of paths changed in this revision. */ + SVN_ERR(svn_fs_deltify_revision(pb->fs, committed_rev, rb->pool)); + + /* Grrr, svn_fs_commit_txn rewrites the datestamp property to the + current clock-time. We don't want that, we want to preserve + history exactly. Good thing revision props aren't versioned! + Note that if rb->datestamp is NULL, that's fine -- if the dump + data doesn't carry a datestamp, we want to preserve that fact in + the load. */ + SVN_ERR(change_rev_prop(pb->repos, committed_rev, SVN_PROP_REVISION_DATE, + rb->datestamp, pb->validate_props, rb->pool)); + + if (pb->notify_func) + { + pb->notify->action = svn_repos_notify_load_txn_committed; + pb->notify->new_revision = committed_rev; + pb->notify->old_revision = ((committed_rev == rb->rev) + ? SVN_INVALID_REVNUM + : rb->rev); + pb->notify_func(pb->notify_baton, pb->notify, rb->pool); + } + + return SVN_NO_ERROR; +} + + +/*----------------------------------------------------------------------*/ + +/** The public routines **/ + + +svn_error_t * +svn_repos_get_fs_build_parser4(const svn_repos_parse_fns3_t **callbacks, + void **parse_baton, + svn_repos_t *repos, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_boolean_t use_history, + svn_boolean_t validate_props, + enum svn_repos_load_uuid uuid_action, + const char *parent_dir, + svn_repos_notify_func_t notify_func, + void *notify_baton, + apr_pool_t *pool) +{ + svn_repos_parse_fns3_t *parser = apr_pcalloc(pool, sizeof(*parser)); + struct parse_baton *pb = apr_pcalloc(pool, sizeof(*pb)); + + if (parent_dir) + parent_dir = svn_relpath_canonicalize(parent_dir, pool); + + SVN_ERR_ASSERT((SVN_IS_VALID_REVNUM(start_rev) && + SVN_IS_VALID_REVNUM(end_rev)) + || ((! SVN_IS_VALID_REVNUM(start_rev)) && + (! SVN_IS_VALID_REVNUM(end_rev)))); + if (SVN_IS_VALID_REVNUM(start_rev)) + SVN_ERR_ASSERT(start_rev <= end_rev); + + parser->magic_header_record = magic_header_record; + parser->uuid_record = uuid_record; + parser->new_revision_record = new_revision_record; + parser->new_node_record = new_node_record; + parser->set_revision_property = set_revision_property; + parser->set_node_property = set_node_property; + parser->remove_node_props = remove_node_props; + parser->set_fulltext = set_fulltext; + parser->close_node = close_node; + parser->close_revision = close_revision; + parser->delete_node_property = delete_node_property; + parser->apply_textdelta = apply_textdelta; + + pb->repos = repos; + pb->fs = svn_repos_fs(repos); + pb->use_history = use_history; + pb->validate_props = validate_props; + pb->notify_func = notify_func; + pb->notify_baton = notify_baton; + pb->notify = svn_repos_notify_create(svn_repos_notify_load_txn_start, pool); + pb->uuid_action = uuid_action; + pb->parent_dir = parent_dir; + pb->pool = pool; + pb->rev_map = apr_hash_make(pool); + pb->oldest_old_rev = SVN_INVALID_REVNUM; + pb->last_rev_mapped = SVN_INVALID_REVNUM; + pb->start_rev = start_rev; + pb->end_rev = end_rev; + + *callbacks = parser; + *parse_baton = pb; + return SVN_NO_ERROR; +} + + + +svn_error_t * +svn_repos_load_fs4(svn_repos_t *repos, + svn_stream_t *dumpstream, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + enum svn_repos_load_uuid uuid_action, + const char *parent_dir, + svn_boolean_t use_pre_commit_hook, + svn_boolean_t use_post_commit_hook, + svn_boolean_t validate_props, + svn_repos_notify_func_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + const svn_repos_parse_fns3_t *parser; + void *parse_baton; + struct parse_baton *pb; + + /* This is really simple. */ + + SVN_ERR(svn_repos_get_fs_build_parser4(&parser, &parse_baton, + repos, + start_rev, end_rev, + TRUE, /* look for copyfrom revs */ + validate_props, + uuid_action, + parent_dir, + notify_func, + notify_baton, + pool)); + + /* Heh. We know this is a parse_baton. This file made it. So + cast away, and set our hook booleans. */ + pb = parse_baton; + pb->use_pre_commit_hook = use_pre_commit_hook; + pb->use_post_commit_hook = use_post_commit_hook; + + return svn_repos_parse_dumpstream3(dumpstream, parser, parse_baton, FALSE, + cancel_func, cancel_baton, pool); +} diff --git a/subversion/libsvn_repos/load.c b/subversion/libsvn_repos/load.c new file mode 100644 index 0000000..691ff92 --- /dev/null +++ b/subversion/libsvn_repos/load.c @@ -0,0 +1,684 @@ +/* load.c --- parsing a 'dumpfile'-formatted stream. + * + * ==================================================================== + * 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 "svn_private_config.h" +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_fs.h" +#include "svn_repos.h" +#include "svn_string.h" +#include "svn_path.h" +#include "svn_props.h" +#include "repos.h" +#include "svn_private_config.h" +#include "svn_mergeinfo.h" +#include "svn_checksum.h" +#include "svn_subst.h" +#include "svn_ctype.h" + +#include <apr_lib.h> + +#include "private/svn_dep_compat.h" +#include "private/svn_mergeinfo_private.h" + +/*----------------------------------------------------------------------*/ + +/** The parser and related helper funcs **/ + + +static svn_error_t * +stream_ran_dry(void) +{ + return svn_error_create(SVN_ERR_INCOMPLETE_DATA, NULL, + _("Premature end of content data in dumpstream")); +} + +static svn_error_t * +stream_malformed(void) +{ + return svn_error_create(SVN_ERR_STREAM_MALFORMED_DATA, NULL, + _("Dumpstream data appears to be malformed")); +} + +/* Allocate a new hash *HEADERS in POOL, and read a series of + RFC822-style headers from STREAM. Duplicate each header's name and + value into POOL and store in hash as a const char * ==> const char *. + + The headers are assumed to be terminated by a single blank line, + which will be permanently sucked from the stream and tossed. + + If the caller has already read in the first header line, it should + be passed in as FIRST_HEADER. If not, pass NULL instead. + */ +static svn_error_t * +read_header_block(svn_stream_t *stream, + svn_stringbuf_t *first_header, + apr_hash_t **headers, + apr_pool_t *pool) +{ + *headers = apr_hash_make(pool); + + while (1) + { + svn_stringbuf_t *header_str; + const char *name, *value; + svn_boolean_t eof; + apr_size_t i = 0; + + if (first_header != NULL) + { + header_str = first_header; + first_header = NULL; /* so we never visit this block again. */ + eof = FALSE; + } + + else + /* Read the next line into a stringbuf. */ + SVN_ERR(svn_stream_readline(stream, &header_str, "\n", &eof, pool)); + + if (svn_stringbuf_isempty(header_str)) + break; /* end of header block */ + else if (eof) + return stream_ran_dry(); + + /* Find the next colon in the stringbuf. */ + while (header_str->data[i] != ':') + { + if (header_str->data[i] == '\0') + return svn_error_createf(SVN_ERR_STREAM_MALFORMED_DATA, NULL, + _("Dump stream contains a malformed " + "header (with no ':') at '%.20s'"), + header_str->data); + i++; + } + /* Create a 'name' string and point to it. */ + header_str->data[i] = '\0'; + name = header_str->data; + + /* Skip over the NULL byte and the space following it. */ + i += 2; + if (i > header_str->len) + return svn_error_createf(SVN_ERR_STREAM_MALFORMED_DATA, NULL, + _("Dump stream contains a malformed " + "header (with no value) at '%.20s'"), + header_str->data); + + /* Point to the 'value' string. */ + value = header_str->data + i; + + /* Store name/value in hash. */ + svn_hash_sets(*headers, name, value); + } + + return SVN_NO_ERROR; +} + + +/* Set *PBUF to a string of length LEN, allocated in POOL, read from STREAM. + Also read a newline from STREAM and increase *ACTUAL_LEN by the total + number of bytes read from STREAM. */ +static svn_error_t * +read_key_or_val(char **pbuf, + svn_filesize_t *actual_length, + svn_stream_t *stream, + apr_size_t len, + apr_pool_t *pool) +{ + char *buf = apr_pcalloc(pool, len + 1); + apr_size_t numread; + char c; + + numread = len; + SVN_ERR(svn_stream_read(stream, buf, &numread)); + *actual_length += numread; + if (numread != len) + return svn_error_trace(stream_ran_dry()); + buf[len] = '\0'; + + /* Suck up extra newline after key data */ + numread = 1; + SVN_ERR(svn_stream_read(stream, &c, &numread)); + *actual_length += numread; + if (numread != 1) + return svn_error_trace(stream_ran_dry()); + if (c != '\n') + return svn_error_trace(stream_malformed()); + + *pbuf = buf; + return SVN_NO_ERROR; +} + + +/* Read CONTENT_LENGTH bytes from STREAM, parsing the bytes as an + encoded Subversion properties hash, and making multiple calls to + PARSE_FNS->set_*_property on RECORD_BATON (depending on the value + of IS_NODE.) + + Set *ACTUAL_LENGTH to the number of bytes consumed from STREAM. + If an error is returned, the value of *ACTUAL_LENGTH is undefined. + + Use POOL for all allocations. */ +static svn_error_t * +parse_property_block(svn_stream_t *stream, + svn_filesize_t content_length, + const svn_repos_parse_fns3_t *parse_fns, + void *record_baton, + void *parse_baton, + svn_boolean_t is_node, + svn_filesize_t *actual_length, + apr_pool_t *pool) +{ + svn_stringbuf_t *strbuf; + apr_pool_t *proppool = svn_pool_create(pool); + + *actual_length = 0; + while (content_length != *actual_length) + { + char *buf; /* a pointer into the stringbuf's data */ + svn_boolean_t eof; + + svn_pool_clear(proppool); + + /* Read a key length line. (Actually, it might be PROPS_END). */ + SVN_ERR(svn_stream_readline(stream, &strbuf, "\n", &eof, proppool)); + + if (eof) + { + /* We could just use stream_ran_dry() or stream_malformed(), + but better to give a non-generic property block error. */ + return svn_error_create + (SVN_ERR_STREAM_MALFORMED_DATA, NULL, + _("Incomplete or unterminated property block")); + } + + *actual_length += (strbuf->len + 1); /* +1 because we read a \n too. */ + buf = strbuf->data; + + if (! strcmp(buf, "PROPS-END")) + break; /* no more properties. */ + + else if ((buf[0] == 'K') && (buf[1] == ' ')) + { + char *keybuf; + apr_uint64_t len; + + SVN_ERR(svn_cstring_strtoui64(&len, buf + 2, 0, APR_SIZE_MAX, 10)); + SVN_ERR(read_key_or_val(&keybuf, actual_length, + stream, (apr_size_t)len, proppool)); + + /* Read a val length line */ + SVN_ERR(svn_stream_readline(stream, &strbuf, "\n", &eof, proppool)); + if (eof) + return stream_ran_dry(); + + *actual_length += (strbuf->len + 1); /* +1 because we read \n too */ + buf = strbuf->data; + + if ((buf[0] == 'V') && (buf[1] == ' ')) + { + svn_string_t propstring; + char *valbuf; + apr_int64_t val; + + SVN_ERR(svn_cstring_atoi64(&val, buf + 2)); + propstring.len = (apr_size_t)val; + SVN_ERR(read_key_or_val(&valbuf, actual_length, + stream, propstring.len, proppool)); + propstring.data = valbuf; + + /* Now, send the property pair to the vtable! */ + if (is_node) + { + SVN_ERR(parse_fns->set_node_property(record_baton, + keybuf, + &propstring)); + } + else + { + SVN_ERR(parse_fns->set_revision_property(record_baton, + keybuf, + &propstring)); + } + } + else + return stream_malformed(); /* didn't find expected 'V' line */ + } + else if ((buf[0] == 'D') && (buf[1] == ' ')) + { + char *keybuf; + apr_uint64_t len; + + SVN_ERR(svn_cstring_strtoui64(&len, buf + 2, 0, APR_SIZE_MAX, 10)); + SVN_ERR(read_key_or_val(&keybuf, actual_length, + stream, (apr_size_t)len, proppool)); + + /* We don't expect these in revision properties, and if we see + one when we don't have a delete_node_property callback, + then we're seeing a v3 feature in a v2 dump. */ + if (!is_node || !parse_fns->delete_node_property) + return stream_malformed(); + + SVN_ERR(parse_fns->delete_node_property(record_baton, keybuf)); + } + else + return stream_malformed(); /* didn't find expected 'K' line */ + + } /* while (1) */ + + svn_pool_destroy(proppool); + return SVN_NO_ERROR; +} + + +/* Read CONTENT_LENGTH bytes from STREAM, and use + PARSE_FNS->set_fulltext to push those bytes as replace fulltext for + a node. Use BUFFER/BUFLEN to push the fulltext in "chunks". + + Use POOL for all allocations. */ +static svn_error_t * +parse_text_block(svn_stream_t *stream, + svn_filesize_t content_length, + svn_boolean_t is_delta, + const svn_repos_parse_fns3_t *parse_fns, + void *record_baton, + char *buffer, + apr_size_t buflen, + apr_pool_t *pool) +{ + svn_stream_t *text_stream = NULL; + apr_size_t num_to_read, rlen, wlen; + + if (is_delta) + { + svn_txdelta_window_handler_t wh; + void *whb; + + SVN_ERR(parse_fns->apply_textdelta(&wh, &whb, record_baton)); + if (wh) + text_stream = svn_txdelta_parse_svndiff(wh, whb, TRUE, pool); + } + else + { + /* Get a stream to which we can push the data. */ + SVN_ERR(parse_fns->set_fulltext(&text_stream, record_baton)); + } + + /* If there are no contents to read, just write an empty buffer + through our callback. */ + if (content_length == 0) + { + wlen = 0; + if (text_stream) + SVN_ERR(svn_stream_write(text_stream, "", &wlen)); + } + + /* Regardless of whether or not we have a sink for our data, we + need to read it. */ + while (content_length) + { + if (content_length >= (svn_filesize_t)buflen) + rlen = buflen; + else + rlen = (apr_size_t) content_length; + + num_to_read = rlen; + SVN_ERR(svn_stream_read(stream, buffer, &rlen)); + content_length -= rlen; + if (rlen != num_to_read) + return stream_ran_dry(); + + if (text_stream) + { + /* write however many bytes you read. */ + wlen = rlen; + SVN_ERR(svn_stream_write(text_stream, buffer, &wlen)); + if (wlen != rlen) + { + /* Uh oh, didn't write as many bytes as we read. */ + return svn_error_create(SVN_ERR_STREAM_UNEXPECTED_EOF, NULL, + _("Unexpected EOF writing contents")); + } + } + } + + /* If we opened a stream, we must close it. */ + if (text_stream) + SVN_ERR(svn_stream_close(text_stream)); + + return SVN_NO_ERROR; +} + + + +/* Parse VERSIONSTRING and verify that we support the dumpfile format + version number, setting *VERSION appropriately. */ +static svn_error_t * +parse_format_version(int *version, + const char *versionstring) +{ + static const int magic_len = sizeof(SVN_REPOS_DUMPFILE_MAGIC_HEADER) - 1; + const char *p = strchr(versionstring, ':'); + int value; + + if (p == NULL + || p != (versionstring + magic_len) + || strncmp(versionstring, + SVN_REPOS_DUMPFILE_MAGIC_HEADER, + magic_len)) + return svn_error_createf(SVN_ERR_STREAM_MALFORMED_DATA, NULL, + _("Malformed dumpfile header '%s'"), + versionstring); + + SVN_ERR(svn_cstring_atoi(&value, p + 1)); + + if (value > SVN_REPOS_DUMPFILE_FORMAT_VERSION) + return svn_error_createf(SVN_ERR_STREAM_MALFORMED_DATA, NULL, + _("Unsupported dumpfile version: %d"), + value); + + *version = value; + return SVN_NO_ERROR; +} + + + +/*----------------------------------------------------------------------*/ + +/** The public routines **/ + +svn_error_t * +svn_repos_parse_dumpstream3(svn_stream_t *stream, + const svn_repos_parse_fns3_t *parse_fns, + void *parse_baton, + svn_boolean_t deltas_are_text, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + svn_boolean_t eof; + svn_stringbuf_t *linebuf; + void *rev_baton = NULL; + char *buffer = apr_palloc(pool, SVN__STREAM_CHUNK_SIZE); + apr_size_t buflen = SVN__STREAM_CHUNK_SIZE; + apr_pool_t *linepool = svn_pool_create(pool); + apr_pool_t *revpool = svn_pool_create(pool); + apr_pool_t *nodepool = svn_pool_create(pool); + int version; + + SVN_ERR(svn_stream_readline(stream, &linebuf, "\n", &eof, linepool)); + if (eof) + return stream_ran_dry(); + + /* The first two lines of the stream are the dumpfile-format version + number, and a blank line. To preserve backward compatibility, + don't assume the existence of newer parser-vtable functions. */ + SVN_ERR(parse_format_version(&version, linebuf->data)); + if (parse_fns->magic_header_record != NULL) + SVN_ERR(parse_fns->magic_header_record(version, parse_baton, pool)); + + /* A dumpfile "record" is defined to be a header-block of + rfc822-style headers, possibly followed by a content-block. + + - A header-block is always terminated by a single blank line (\n\n) + + - We know whether the record has a content-block by looking for + a 'Content-length:' header. The content-block will always be + of a specific length, plus an extra newline. + + Once a record is fully sucked from the stream, an indeterminate + number of blank lines (or lines that begin with whitespace) may + follow before the next record (or the end of the stream.) + */ + + while (1) + { + apr_hash_t *headers; + void *node_baton; + svn_boolean_t found_node = FALSE; + svn_boolean_t old_v1_with_cl = FALSE; + const char *content_length; + const char *prop_cl; + const char *text_cl; + const char *value; + svn_filesize_t actual_prop_length; + + /* Clear our per-line pool. */ + svn_pool_clear(linepool); + + /* Check for cancellation. */ + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Keep reading blank lines until we discover a new record, or until + the stream runs out. */ + SVN_ERR(svn_stream_readline(stream, &linebuf, "\n", &eof, linepool)); + + if (eof) + { + if (svn_stringbuf_isempty(linebuf)) + break; /* end of stream, go home. */ + else + return stream_ran_dry(); + } + + if ((linebuf->len == 0) || (svn_ctype_isspace(linebuf->data[0]))) + continue; /* empty line ... loop */ + + /*** Found the beginning of a new record. ***/ + + /* The last line we read better be a header of some sort. + Read the whole header-block into a hash. */ + SVN_ERR(read_header_block(stream, linebuf, &headers, linepool)); + + /*** Handle the various header blocks. ***/ + + /* Is this a revision record? */ + if (svn_hash_gets(headers, SVN_REPOS_DUMPFILE_REVISION_NUMBER)) + { + /* If we already have a rev_baton open, we need to close it + and clear the per-revision subpool. */ + if (rev_baton != NULL) + { + SVN_ERR(parse_fns->close_revision(rev_baton)); + svn_pool_clear(revpool); + } + + SVN_ERR(parse_fns->new_revision_record(&rev_baton, + headers, parse_baton, + revpool)); + } + /* Or is this, perhaps, a node record? */ + else if (svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_PATH)) + { + SVN_ERR(parse_fns->new_node_record(&node_baton, + headers, + rev_baton, + nodepool)); + found_node = TRUE; + } + /* Or is this the repos UUID? */ + else if ((value = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_UUID))) + { + SVN_ERR(parse_fns->uuid_record(value, parse_baton, pool)); + } + /* Or perhaps a dumpfile format? */ + /* ### TODO: use parse_format_version */ + else if ((value = svn_hash_gets(headers, + SVN_REPOS_DUMPFILE_MAGIC_HEADER))) + { + /* ### someday, switch modes of operation here. */ + SVN_ERR(svn_cstring_atoi(&version, value)); + } + /* Or is this bogosity?! */ + else + { + /* What the heck is this record?!? */ + return svn_error_create(SVN_ERR_STREAM_MALFORMED_DATA, NULL, + _("Unrecognized record type in stream")); + } + + /* Need 3 values below to determine v1 dump type + + Old (pre 0.14?) v1 dumps don't have Prop-content-length + and Text-content-length fields, but always have a properties + block in a block with Content-Length > 0 */ + + content_length = svn_hash_gets(headers, + SVN_REPOS_DUMPFILE_CONTENT_LENGTH); + prop_cl = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH); + text_cl = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH); + old_v1_with_cl = + version == 1 && content_length && ! prop_cl && ! text_cl; + + /* Is there a props content-block to parse? */ + if (prop_cl || old_v1_with_cl) + { + const char *delta = svn_hash_gets(headers, + SVN_REPOS_DUMPFILE_PROP_DELTA); + svn_boolean_t is_delta = (delta && strcmp(delta, "true") == 0); + + /* First, remove all node properties, unless this is a delta + property block. */ + if (found_node && !is_delta) + SVN_ERR(parse_fns->remove_node_props(node_baton)); + + SVN_ERR(parse_property_block + (stream, + svn__atoui64(prop_cl ? prop_cl : content_length), + parse_fns, + found_node ? node_baton : rev_baton, + parse_baton, + found_node, + &actual_prop_length, + found_node ? nodepool : revpool)); + } + + /* Is there a text content-block to parse? */ + if (text_cl) + { + const char *delta = svn_hash_gets(headers, + SVN_REPOS_DUMPFILE_TEXT_DELTA); + svn_boolean_t is_delta = FALSE; + if (! deltas_are_text) + is_delta = (delta && strcmp(delta, "true") == 0); + + SVN_ERR(parse_text_block(stream, + svn__atoui64(text_cl), + is_delta, + parse_fns, + found_node ? node_baton : rev_baton, + buffer, + buflen, + found_node ? nodepool : revpool)); + } + else if (old_v1_with_cl) + { + /* An old-v1 block with a Content-length might have a text block. + If the property block did not consume all the bytes of the + Content-length, then it clearly does have a text block. + If not, then we must deduce whether we have an *empty* text + block or an *absent* text block. The rules are: + - "Node-kind: file" blocks have an empty (i.e. present, but + zero-length) text block, since they represent a file + modification. Note that file-copied-text-unmodified blocks + have no Content-length - even if they should have contained + a modified property block, the pre-0.14 dumper forgets to + dump the modified properties. + - If it is not a file node, then it is a revision or directory, + and so has an absent text block. + */ + const char *node_kind; + svn_filesize_t cl_value = svn__atoui64(content_length) + - actual_prop_length; + + if (cl_value || + ((node_kind = svn_hash_gets(headers, + SVN_REPOS_DUMPFILE_NODE_KIND)) + && strcmp(node_kind, "file") == 0) + ) + SVN_ERR(parse_text_block(stream, + cl_value, + FALSE, + parse_fns, + found_node ? node_baton : rev_baton, + buffer, + buflen, + found_node ? nodepool : revpool)); + } + + /* if we have a content-length header, did we read all of it? + in case of an old v1, we *always* read all of it, because + text-content-length == content-length - prop-content-length + */ + if (content_length && ! old_v1_with_cl) + { + apr_size_t rlen, num_to_read; + svn_filesize_t remaining = + svn__atoui64(content_length) - + (prop_cl ? svn__atoui64(prop_cl) : 0) - + (text_cl ? svn__atoui64(text_cl) : 0); + + + if (remaining < 0) + return svn_error_create(SVN_ERR_STREAM_MALFORMED_DATA, NULL, + _("Sum of subblock sizes larger than " + "total block content length")); + + /* Consume remaining bytes in this content block */ + while (remaining > 0) + { + if (remaining >= (svn_filesize_t)buflen) + rlen = buflen; + else + rlen = (apr_size_t) remaining; + + num_to_read = rlen; + SVN_ERR(svn_stream_read(stream, buffer, &rlen)); + remaining -= rlen; + if (rlen != num_to_read) + return stream_ran_dry(); + } + } + + /* If we just finished processing a node record, we need to + close the node record and clear the per-node subpool. */ + if (found_node) + { + SVN_ERR(parse_fns->close_node(node_baton)); + svn_pool_clear(nodepool); + } + + /*** End of processing for one record. ***/ + + } /* end of stream */ + + /* Close out whatever revision we're in. */ + if (rev_baton != NULL) + SVN_ERR(parse_fns->close_revision(rev_baton)); + + svn_pool_destroy(linepool); + svn_pool_destroy(revpool); + svn_pool_destroy(nodepool); + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_repos/log.c b/subversion/libsvn_repos/log.c new file mode 100644 index 0000000..8ca870b --- /dev/null +++ b/subversion/libsvn_repos/log.c @@ -0,0 +1,2369 @@ +/* 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_mergeinfo_private.h" +#include "private/svn_subr_private.h" + + + +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; + apr_hash_t *changes; + apr_hash_index_t *hi; + svn_boolean_t found_readable = FALSE; + svn_boolean_t found_unreadable = FALSE; + apr_pool_t *subpool; + + /* 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_changed2(&changes, rev_root, pool)); + + /* No changed paths? We're done. */ + if (apr_hash_count(changes) == 0) + return SVN_NO_ERROR; + + /* Otherwise, we have to check the readability of each changed + path, or at least enough to answer the question asked. */ + subpool = svn_pool_create(pool); + for (hi = apr_hash_first(pool, changes); hi; hi = apr_hash_next(hi)) + { + const void *key; + void *val; + svn_fs_path_change2_t *change; + svn_boolean_t readable; + + svn_pool_clear(subpool); + apr_hash_this(hi, &key, NULL, &val); + change = val; + + SVN_ERR(authz_read_func(&readable, rev_root, key, + authz_read_baton, subpool)); + 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, key, subpool)); + 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, subpool)); + SVN_ERR(authz_read_func(&readable, + copyfrom_root, copyfrom_path, + authz_read_baton, subpool)); + 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; + } + } + + decision: + svn_pool_destroy(subpool); + + /* 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; +} + + +/* Store as keys in CHANGED the paths of all node in ROOT that show a + * significant change. "Significant" means that the text or + * properties of the node were changed, or that the node was added or + * deleted. + * + * The CHANGED hash set and its keys and values are allocated in POOL; + * keys are const char * paths and values are svn_log_changed_path_t. + * + * To prevent changes from being processed over and over again, the + * changed paths for ROOT may be passed in PREFETCHED_CHANGES. If the + * latter is NULL, we will request the list inside this function. + * + * If optional AUTHZ_READ_FUNC is non-NULL, then use it (with + * AUTHZ_READ_BATON and FS) to check whether each changed-path (and + * copyfrom_path) is readable: + * + * - If some paths are readable and some are not, then silently + * omit the unreadable paths from the CHANGED hash, and return + * SVN_ERR_AUTHZ_PARTIALLY_READABLE. + * + * - If absolutely every changed-path (and copyfrom_path) is + * unreadable, then return an empty CHANGED hash and + * SVN_ERR_AUTHZ_UNREADABLE. (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(apr_hash_t **changed, + svn_fs_root_t *root, + svn_fs_t *fs, + apr_hash_t *prefetched_changes, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + apr_hash_t *changes = prefetched_changes; + apr_hash_index_t *hi; + apr_pool_t *subpool; + svn_boolean_t found_readable = FALSE; + svn_boolean_t found_unreadable = FALSE; + + *changed = svn_hash__make(pool); + if (changes == NULL) + SVN_ERR(svn_fs_paths_changed2(&changes, root, pool)); + + if (apr_hash_count(changes) == 0) + /* No paths changed in this revision? Uh, sure, I guess the + revision is readable, then. */ + return SVN_NO_ERROR; + + subpool = svn_pool_create(pool); + + for (hi = apr_hash_first(pool, changes); hi; hi = apr_hash_next(hi)) + { + /* 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 void *key; + void *val; + svn_fs_path_change2_t *change; + const char *path; + char action; + svn_log_changed_path2_t *item; + + svn_pool_clear(subpool); + + /* KEY will be the path, VAL the change. */ + apr_hash_this(hi, &key, NULL, &val); + path = (const char *) key; + change = val; + + /* Skip path if unreadable. */ + if (authz_read_func) + { + svn_boolean_t readable; + SVN_ERR(authz_read_func(&readable, + root, path, + authz_read_baton, subpool)); + if (! readable) + { + found_unreadable = TRUE; + continue; + } + } + + /* At least one changed-path was readable. */ + found_readable = TRUE; + + switch (change->change_kind) + { + case svn_fs_path_change_reset: + continue; + + case svn_fs_path_change_add: + action = 'A'; + break; + + case svn_fs_path_change_replace: + action = 'R'; + break; + + case svn_fs_path_change_delete: + action = 'D'; + break; + + case svn_fs_path_change_modify: + default: + action = 'M'; + break; + } + + item = svn_log_changed_path2_create(pool); + item->action = action; + item->node_kind = change->node_kind; + item->copyfrom_rev = SVN_INVALID_REVNUM; + item->text_modified = change->text_mod ? svn_tristate_true + : svn_tristate_false; + item->props_modified = change->prop_mod ? svn_tristate_true + : svn_tristate_false; + + /* Pre-1.6 revision files don't store the change path kind, so fetch + it manually. */ + if (item->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, subpool); + + SVN_ERR(svn_fs_node_history(&history, root, parent_path, + subpool)); + + /* Two calls because the first call returns the original + revision as the deleted child means it is 'interesting' */ + SVN_ERR(svn_fs_history_prev(&history, history, TRUE, subpool)); + SVN_ERR(svn_fs_history_prev(&history, history, TRUE, subpool)); + + SVN_ERR(svn_fs_history_location(&parent_path, &prev_rev, history, + subpool)); + SVN_ERR(svn_fs_revision_root(&check_root, fs, prev_rev, subpool)); + check_path = svn_fspath__join(parent_path, name, subpool); + } + + SVN_ERR(svn_fs_check_path(&item->node_kind, check_root, check_path, + subpool)); + } + + + if ((action == 'A') || (action == 'R')) + { + 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, subpool)); + + if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_rev)) + { + svn_boolean_t readable = TRUE; + + if (authz_read_func) + { + svn_fs_root_t *copyfrom_root; + + SVN_ERR(svn_fs_revision_root(©from_root, fs, + copyfrom_rev, subpool)); + SVN_ERR(authz_read_func(&readable, + copyfrom_root, copyfrom_path, + authz_read_baton, subpool)); + if (! readable) + found_unreadable = TRUE; + } + + if (readable) + { + item->copyfrom_path = apr_pstrdup(pool, copyfrom_path); + item->copyfrom_rev = copyfrom_rev; + } + } + } + svn_hash_sets(*changed, apr_pstrdup(pool, path), item); + } + + svn_pool_destroy(subpool); + + if (! found_readable) + /* Every changed-path was unreadable. */ + return svn_error_create(SVN_ERR_AUTHZ_UNREADABLE, + NULL, NULL); + + if (found_unreadable) + /* At least one changed-path was unreadable. */ + return svn_error_create(SVN_ERR_AUTHZ_PARTIALLY_READABLE, + NULL, NULL); + + /* Every changed-path was readable. */ + 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 *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_prev(&info->hist, info->hist, ! strict, subpool)); + + hist = info->hist; + } + else + { + subpool = svn_pool_create(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_history(&hist, history_root, info->path->data, + subpool)); + + SVN_ERR(svn_fs_history_prev(&hist, hist, ! strict, subpool)); + + if (info->first_time) + info->first_time = FALSE; + else + SVN_ERR(svn_fs_history_prev(&hist, hist, ! strict, subpool)); + } + + 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, + subpool)); + SVN_ERR(authz_read_func(&readable, history_root, + info->path->data, + authz_read_baton, + subpool)); + 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 *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, 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. If *PREFETCHED_CAHNGES + already contains the changed paths for REV, use that. Otherwise, + request that data and return it in *PREFETCHED_CHANGES. */ +/* ### 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, + apr_hash_t **prefetched_changes, + 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; + apr_hash_index_t *hi; + + /* 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; + + /* 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)); + if (*prefetched_changes == NULL) + SVN_ERR(svn_fs_paths_changed2(prefetched_changes, root, scratch_pool)); + + /* No changed paths? We're done. */ + if (apr_hash_count(*prefetched_changes) == 0) + return SVN_NO_ERROR; + + /* 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); + for (hi = apr_hash_first(scratch_pool, *prefetched_changes); + hi; + hi = apr_hash_next(hi)) + { + const void *key; + void *val; + svn_fs_path_change2_t *change; + const char *changed_path, *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; + + svn_pool_clear(iterpool); + + /* KEY will be the path, VAL the change. */ + apr_hash_this(hi, &key, NULL, &val); + changed_path = key; + change = val; + + /* If there was no property change on this item, ignore it. */ + if (! change->prop_mod) + continue; + + 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 added or replaced, see if it was created via + copy. If so, that will tell us where its previous location + was. If not, there's no previous location to examine. */ + 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, + root, changed_path, iterpool)); + if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_rev)) + { + base_path = apr_pstrdup(scratch_pool, copyfrom_path); + base_rev = copyfrom_rev; + } + break; + } + + /* 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; + + 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; + } + 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; + + /* 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)) + { + apr_array_header_t *query_paths = + apr_array_make(iterpool, 1, sizeof(const char *)); + svn_mergeinfo_t tmp_mergeinfo; + svn_mergeinfo_catalog_t tmp_catalog; + + APR_ARRAY_PUSH(query_paths, const char *) = changed_path; + SVN_ERR(svn_fs_get_mergeinfo2(&tmp_catalog, root, + query_paths, svn_mergeinfo_inherited, + FALSE, TRUE, iterpool, iterpool)); + tmp_mergeinfo = svn_hash_gets(tmp_catalog, changed_path); + 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)) + { + apr_array_header_t *query_paths = + apr_array_make(iterpool, 1, sizeof(const char *)); + svn_mergeinfo_t tmp_mergeinfo; + svn_mergeinfo_catalog_t tmp_catalog; + + APR_ARRAY_PUSH(query_paths, const char *) = base_path; + SVN_ERR(svn_fs_get_mergeinfo2(&tmp_catalog, base_root, + query_paths, svn_mergeinfo_inherited, + FALSE, TRUE, iterpool, iterpool)); + tmp_mergeinfo = svn_hash_gets(tmp_catalog, base_path); + if (tmp_mergeinfo) + SVN_ERR(svn_mergeinfo_to_string(&prev_mergeinfo_value, + tmp_mergeinfo, + iterpool)); + } + + /* If the old and new mergeinfo differ in any way, store the + before and after mergeinfo values in our return hashes. */ + if ((prev_mergeinfo_value && (! mergeinfo_value)) + || ((! prev_mergeinfo_value) && mergeinfo_value) + || (prev_mergeinfo_value && mergeinfo_value + && (! svn_string_compare(mergeinfo_value, + prev_mergeinfo_value)))) + { + 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); + 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. + If *PREFETCHED_CAHNGES already contains the changed paths for + REV, use that. Otherwise, request that data and return it in + *PREFETCHED_CHANGES. + Use POOL for all allocations. */ +static svn_error_t * +get_combined_mergeinfo_changes(svn_mergeinfo_t *added_mergeinfo, + svn_mergeinfo_t *deleted_mergeinfo, + apr_hash_t **prefetched_changes, + 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; + + /* Create a work subpool and get a root for REV. */ + SVN_ERR(svn_fs_revision_root(&root, fs, rev, scratch_pool)); + + /* Fetch the mergeinfo changes for REV. */ + err = fs_mergeinfo_changed(&deleted_mergeinfo_catalog, + &added_mergeinfo_catalog, + prefetched_changes, + 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; + + /* 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; + apr_ssize_t klen; + svn_revnum_t appeared_rev, prev_rev; + svn_fs_root_t *prev_root; + svn_mergeinfo_catalog_t catalog, inherited_catalog; + svn_mergeinfo_t prev_mergeinfo, mergeinfo, deleted, added, + prev_inherited_mergeinfo, inherited_mergeinfo; + apr_array_header_t *query_paths; + + 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)); + query_paths = apr_array_make(iterpool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(query_paths, const char *) = prev_path; + err = svn_fs_get_mergeinfo2(&catalog, prev_root, query_paths, + svn_mergeinfo_inherited, FALSE, 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_mergeinfo2(&inherited_catalog, prev_root, query_paths, + svn_mergeinfo_nearest_ancestor, FALSE, + FALSE, /* adjust_inherited_mergeinfo */ + iterpool, iterpool)); + + klen = strlen(prev_path); + prev_mergeinfo = apr_hash_get(catalog, prev_path, klen); + prev_inherited_mergeinfo = apr_hash_get(inherited_catalog, prev_path, klen); + + /* Fetch the current mergeinfo (as of REV, and including + inherited stuff) for this path. */ + APR_ARRAY_IDX(query_paths, 0, const char *) = path; + SVN_ERR(svn_fs_get_mergeinfo2(&catalog, root, query_paths, + svn_mergeinfo_inherited, FALSE, TRUE, + iterpool, iterpool)); + + /* Issue #4022 again, fetch the raw inherited mergeinfo. */ + SVN_ERR(svn_fs_get_mergeinfo2(&inherited_catalog, root, query_paths, + svn_mergeinfo_nearest_ancestor, FALSE, + FALSE, /* adjust_inherited_mergeinfo */ + iterpool, iterpool)); + + klen = strlen(path); + mergeinfo = apr_hash_get(catalog, path, klen); + inherited_mergeinfo = apr_hash_get(inherited_catalog, path, klen); + + 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, + FALSE, + 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 void *key; + apr_ssize_t klen; + void *val; + const char *changed_path; + svn_mergeinfo_t added, deleted; + + /* The path is the key, the mergeinfo delta is the value. */ + apr_hash_this(hi, &key, &klen, &val); + changed_path = key; + added = val; + + 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, key, 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_log_entry_t *log_entry, + svn_revnum_t rev, + svn_fs_t *fs, + apr_hash_t *prefetched_changes, + svn_boolean_t discover_changed_paths, + const apr_array_header_t *revprops, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + apr_hash_t *r_props, *changed_paths = NULL; + svn_boolean_t get_revprops = TRUE, censor_revprops = FALSE; + + /* Discover changed paths if the user requested them + or if we need to check that they are readable. */ + if ((rev > 0) + && (authz_read_func || discover_changed_paths)) + { + svn_fs_root_t *newroot; + svn_error_t *patherr; + + SVN_ERR(svn_fs_revision_root(&newroot, fs, rev, pool)); + patherr = detect_changed(&changed_paths, + newroot, fs, prefetched_changes, + authz_read_func, authz_read_baton, + pool); + + if (patherr + && patherr->apr_err == SVN_ERR_AUTHZ_UNREADABLE) + { + /* All changed-paths are unreadable, so clear all fields. */ + svn_error_clear(patherr); + changed_paths = NULL; + get_revprops = FALSE; + } + else if (patherr + && patherr->apr_err == SVN_ERR_AUTHZ_PARTIALLY_READABLE) + { + /* At least one changed-path was unreadable, so censor all + but author and date. (The unreadable paths are already + missing from the hash.) */ + svn_error_clear(patherr); + censor_revprops = TRUE; + } + else if (patherr) + return patherr; + + /* It may be the case that an authz func was passed in, but + the user still doesn't want to see any changed-paths. */ + if (! discover_changed_paths) + changed_paths = NULL; + } + + if (get_revprops) + { + /* User is allowed to see at least some revprops. */ + SVN_ERR(svn_fs_revision_proplist(&r_props, fs, rev, 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 + { + /* Requested only some revprops... */ + int i; + for (i = 0; i < revprops->nelts; i++) + { + char *name = APR_ARRAY_IDX(revprops, i, char *); + svn_string_t *value = svn_hash_gets(r_props, name); + if (censor_revprops + && !(strcmp(name, SVN_PROP_REVISION_AUTHOR) == 0 + || strcmp(name, SVN_PROP_REVISION_DATE) == 0)) + /* ... but we can only return author/date. */ + continue; + if (log_entry->revprops == NULL) + log_entry->revprops = svn_hash__make(pool); + svn_hash_sets(log_entry->revprops, name, value); + } + } + } + + log_entry->changed_paths = changed_paths; + log_entry->changed_paths2 = changed_paths; + log_entry->revision = rev; + + return SVN_NO_ERROR; +} + +/* Send a log message for REV to RECEIVER with its RECEIVER_BATON. + + 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 AUTHZ_READ_FUNC is + not NULL, or if DISCOVER_CHANGED_PATHS is TRUE. 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, + apr_hash_t *prefetched_changes, + svn_mergeinfo_t log_target_history_as_mergeinfo, + apr_hash_t *nested_merges, + svn_boolean_t discover_changed_paths, + svn_boolean_t subtractive_merge, + svn_boolean_t handling_merged_revision, + const apr_array_header_t *revprops, + svn_boolean_t has_children, + svn_log_entry_receiver_t receiver, + void *receiver_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + svn_log_entry_t *log_entry; + /* Assume we want to send the log for REV. */ + svn_boolean_t found_rev_of_interest = TRUE; + + log_entry = svn_log_entry_create(pool); + SVN_ERR(fill_log_entry(log_entry, rev, fs, prefetched_changes, + discover_changed_paths || handling_merged_revision, + revprops, authz_read_func, authz_read_baton, + pool)); + log_entry->has_children = has_children; + log_entry->subtractive_merge = subtractive_merge; + + /* 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. */ + if (handling_merged_revision + && log_entry->changed_paths2 + && log_target_history_as_mergeinfo + && apr_hash_count(log_target_history_as_mergeinfo)) + { + apr_hash_index_t *hi; + apr_pool_t *subpool = svn_pool_create(pool); + + /* REV was merged in, but it might already be part of the log target's + natural history, so change our starting assumption. */ + found_rev_of_interest = FALSE; + + /* Look at each changed path in REV. */ + for (hi = apr_hash_first(subpool, log_entry->changed_paths2); + hi; + hi = apr_hash_next(hi)) + { + svn_boolean_t path_is_in_history = FALSE; + const char *changed_path = svn__apr_hash_index_key(hi); + apr_hash_index_t *hi2; + apr_pool_t *inner_subpool = svn_pool_create(subpool); + + /* Look at each path on the log target's mergeinfo. */ + for (hi2 = apr_hash_first(inner_subpool, + log_target_history_as_mergeinfo); + hi2; + hi2 = apr_hash_next(hi2)) + { + const char *mergeinfo_path = + svn__apr_hash_index_key(hi2); + svn_rangelist_t *rangelist = + svn__apr_hash_index_val(hi2); + + /* 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, changed_path)) + { + int i; + + for (i = 0; i < rangelist->nelts; i++) + { + svn_merge_range_t *range = + APR_ARRAY_IDX(rangelist, i, + svn_merge_range_t *); + if (rev > range->start && rev <= range->end) + { + path_is_in_history = TRUE; + break; + } + } + } + if (path_is_in_history) + break; + } + svn_pool_destroy(inner_subpool); + + if (!path_is_in_history) + { + /* If even one path in LOG_ENTRY->CHANGED_PATHS2 is not part of + LOG_TARGET_HISTORY_AS_MERGEINFO, then we want to send the + log for REV. */ + found_rev_of_interest = TRUE; + break; + } + } + svn_pool_destroy(subpool); + } + + /* If we only got changed paths the sake of detecting redundant merged + revisions, then be sure we don't send that info to the receiver. */ + if (!discover_changed_paths && handling_merged_revision) + log_entry->changed_paths = log_entry->changed_paths2 = NULL; + + /* Send the entry to the receiver, unless it is a redundant merged + revision. */ + if (found_rev_of_interest) + { + /* Is REV a merged revision we've already sent? */ + if (nested_merges && handling_merged_revision) + { + svn_revnum_t *merged_rev = apr_hash_get(nested_merges, &rev, + sizeof(svn_revnum_t *)); + + if (merged_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. */ + apr_pool_t *hash_pool = apr_hash_pool_get(nested_merges); + svn_revnum_t *long_lived_rev = apr_palloc(hash_pool, + sizeof(svn_revnum_t)); + *long_lived_rev = rev; + apr_hash_set(nested_merges, long_lived_rev, + sizeof(svn_revnum_t *), long_lived_rev); + } + } + + return (*receiver)(receiver_baton, log_entry, pool); + } + else + { + 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)); + + if (authz_read_func) + { + svn_boolean_t readable; + + svn_pool_clear(iterpool); + + 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_history(&info->hist, root, this_path, pool); + 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); + 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)); + apr_hash_this(hi, (void *) &rp->path, NULL, + (void *) &rp->rangelist); + 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. */ + qsort(rangelist_paths->elts, rangelist_paths->nelts, + rangelist_paths->elt_size, 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, + apr_hash_t *nested_merges, + svn_revnum_t hist_start, + svn_revnum_t hist_end, + int limit, + svn_boolean_t discover_changed_paths, + 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, + svn_log_entry_receiver_t receiver, + void *receiver_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + 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, + apr_hash_t *nested_merges, + svn_mergeinfo_t processed, + svn_mergeinfo_t added_mergeinfo, + svn_mergeinfo_t deleted_mergeinfo, + svn_boolean_t discover_changed_paths, + svn_boolean_t strict_node_history, + const apr_array_header_t *revprops, + svn_log_entry_receiver_t receiver, + void *receiver_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + apr_array_header_t *combined_list = NULL; + svn_log_entry_t *empty_log_entry; + 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); + qsort(combined_list->elts, combined_list->nelts, + combined_list->elt_size, 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, + discover_changed_paths, strict_node_history, + TRUE, pl_range->reverse_merge, TRUE, TRUE, + revprops, TRUE, receiver, receiver_baton, + authz_read_func, authz_read_baton, iterpool)); + } + svn_pool_destroy(iterpool); + + /* Send the empty revision. */ + empty_log_entry = svn_log_entry_create(pool); + empty_log_entry->revision = SVN_INVALID_REVNUM; + return (*receiver)(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(svn_merge_range_t)); + + 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 + RECEIVER with RECEIVER_BATON 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_logs4(). 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_logs4(). + */ +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, + apr_hash_t *nested_merges, + svn_revnum_t hist_start, + svn_revnum_t hist_end, + int limit, + svn_boolean_t discover_changed_paths, + 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, + svn_log_entry_receiver_t receiver, + void *receiver_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + apr_pool_t *iterpool; + 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, + authz_read_func, 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); + 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 *); + + /* Check history for this path in current rev. */ + SVN_ERR(check_history(&changed, info, fs, current, + strict_node_history, authz_read_func, + authz_read_baton, hist_start, pool)); + if (! info->done) + any_histories_left = TRUE; + } + + /* 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; + apr_hash_t *changes = NULL; + + /* 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, + &changes, + 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, changes, + log_target_history_as_mergeinfo, nested_merges, + discover_changed_paths, + subtractive_merge, handling_merged_revisions, + revprops, has_children, + receiver, receiver_baton, + authz_read_func, authz_read_baton, 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_hash__make(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, + discover_changed_paths, + strict_node_history, + revprops, + receiver, receiver_baton, + authz_read_func, + authz_read_baton, + 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_pcalloc(pool, sizeof(*cur_rev)); + struct added_deleted_mergeinfo *add_and_del_mergeinfo = + apr_palloc(pool, sizeof(*add_and_del_mergeinfo)); + + if (added_mergeinfo) + add_and_del_mergeinfo->added_mergeinfo = + svn_mergeinfo_dup(added_mergeinfo, pool); + + if (deleted_mergeinfo) + add_and_del_mergeinfo->deleted_mergeinfo = + svn_mergeinfo_dup(deleted_mergeinfo, pool); + + *cur_rev = current; + 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(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(svn_revnum_t)); + 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, NULL, + log_target_history_as_mergeinfo, nested_merges, + discover_changed_paths, subtractive_merge, + handling_merged_revisions, revprops, has_children, + receiver, receiver_baton, authz_read_func, + authz_read_baton, iterpool)); + if (has_children) + { + if (!nested_merges) + { + subpool = svn_pool_create(pool); + nested_merges = svn_hash__make(subpool); + } + + SVN_ERR(handle_merged_revisions(current, fs, + log_target_history_as_mergeinfo, + nested_merges, + processed, + added_mergeinfo, + deleted_mergeinfo, + discover_changed_paths, + strict_node_history, revprops, + receiver, receiver_baton, + authz_read_func, + authz_read_baton, + 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_logs4. */ +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_logs4(). */ +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_logs4(svn_repos_t *repos, + const apr_array_header_t *paths, + svn_revnum_t start, + svn_revnum_t end, + int limit, + svn_boolean_t discover_changed_paths, + 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_log_entry_receiver_t receiver, + void *receiver_baton, + apr_pool_t *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; + + /* Setup log range. */ + SVN_ERR(svn_fs_youngest_rev(&head, fs, 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(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(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, pool)); + SVN_ERR(authz_read_func(&readable, rev_root, "", + authz_read_baton, pool)); + if (! readable) + return svn_error_create(SVN_ERR_AUTHZ_UNREADABLE, NULL, NULL); + } + + send_count = end - start + 1; + if (limit && 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, NULL, + discover_changed_paths, FALSE, + FALSE, revprops, FALSE, receiver, + receiver_baton, authz_read_func, + authz_read_baton, 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 + http://subversion.tigris.org/issues/show_bug.cgi?id=3650#desc5 */ + if (include_merged_revisions) + { + apr_pool_t *subpool = svn_pool_create(pool); + + SVN_ERR(get_paths_history_as_mergeinfo(&paths_history_mergeinfo, + repos, paths, start, end, + authz_read_func, + authz_read_baton, + pool, subpool)); + svn_pool_destroy(subpool); + } + + return do_logs(repos->fs, paths, paths_history_mergeinfo, NULL, NULL, start, end, + limit, discover_changed_paths, strict_node_history, + include_merged_revisions, FALSE, FALSE, FALSE, revprops, + descending_order, receiver, receiver_baton, + authz_read_func, authz_read_baton, pool); +} diff --git a/subversion/libsvn_repos/node_tree.c b/subversion/libsvn_repos/node_tree.c new file mode 100644 index 0000000..bd947b0 --- /dev/null +++ b/subversion/libsvn_repos/node_tree.c @@ -0,0 +1,431 @@ +/* + * node_tree.c: an editor for tracking repository deltas changes + * + * ==================================================================== + * 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 <stdio.h> + +#define APR_WANT_STRFUNC +#include <apr_want.h> +#include <apr_pools.h> + +#include "svn_types.h" +#include "svn_error.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_delta.h" +#include "svn_fs.h" +#include "svn_repos.h" +#include "repos.h" +#include "svn_private_config.h" +#include "private/svn_fspath.h" + +/*** NOTE: This editor is unique in that it currently is hard-coded to + be anchored at the root directory of the filesystem. This + affords us the ability to use the same paths for filesystem + locations and editor paths. ***/ + + + +/*** Node creation and assembly structures and routines. ***/ +static svn_repos_node_t * +create_node(const char *name, + svn_repos_node_t *parent, + apr_pool_t *pool) +{ + svn_repos_node_t *node = apr_pcalloc(pool, sizeof(*node)); + node->action = 'R'; + node->kind = svn_node_unknown; + node->name = apr_pstrdup(pool, name); + node->parent = parent; + return node; +} + + +static svn_repos_node_t * +create_sibling_node(svn_repos_node_t *elder, + const char *name, + apr_pool_t *pool) +{ + svn_repos_node_t *tmp_node; + + /* No ELDER sibling? That's just not gonna work out. */ + if (! elder) + return NULL; + + /* Run to the end of the list of siblings of ELDER. */ + tmp_node = elder; + while (tmp_node->sibling) + tmp_node = tmp_node->sibling; + + /* Create a new youngest sibling and return that. */ + return (tmp_node->sibling = create_node(name, elder->parent, pool)); +} + + +static svn_repos_node_t * +create_child_node(svn_repos_node_t *parent, + const char *name, + apr_pool_t *pool) +{ + /* No PARENT node? That's just not gonna work out. */ + if (! parent) + return NULL; + + /* If PARENT has no children, create its first one and return that. */ + if (! parent->child) + return (parent->child = create_node(name, parent, pool)); + + /* If PARENT already has a child, create a new sibling for its first + child and return that. */ + return create_sibling_node(parent->child, name, pool); +} + + +static svn_repos_node_t * +find_child_by_name(svn_repos_node_t *parent, + const char *name) +{ + svn_repos_node_t *tmp_node; + + /* No PARENT node, or a barren PARENT? Nothing to find. */ + if ((! parent) || (! parent->child)) + return NULL; + + /* Look through the children for a node with a matching name. */ + tmp_node = parent->child; + while (1) + { + if (! strcmp(tmp_node->name, name)) + { + return tmp_node; + } + else + { + if (tmp_node->sibling) + tmp_node = tmp_node->sibling; + else + break; + } + } + + return NULL; +} + + +static void +find_real_base_location(const char **path_p, + svn_revnum_t *rev_p, + svn_repos_node_t *node, + apr_pool_t *pool) +{ + /* If NODE is an add-with-history, then its real base location is + the copy source. */ + if ((node->action == 'A') + && node->copyfrom_path + && SVN_IS_VALID_REVNUM(node->copyfrom_rev)) + { + *path_p = node->copyfrom_path; + *rev_p = node->copyfrom_rev; + return; + } + + /* Otherwise, if NODE has a parent, we'll recurse, and add NODE's + name to whatever the parent's real base path turns out to be (and + pass the base revision on through). */ + if (node->parent) + { + const char *path; + svn_revnum_t rev; + + find_real_base_location(&path, &rev, node->parent, pool); + *path_p = svn_fspath__join(path, node->name, pool); + *rev_p = rev; + return; + } + + /* Finally, if the node has no parent, then its name is "/", and it + has no interesting base revision. */ + *path_p = "/"; + *rev_p = SVN_INVALID_REVNUM; + return; +} + + + + +/*** Editor functions and batons. ***/ + +struct edit_baton +{ + svn_fs_t *fs; + svn_fs_root_t *root; + svn_fs_root_t *base_root; + apr_pool_t *node_pool; + svn_repos_node_t *node; +}; + + +struct node_baton +{ + struct edit_baton *edit_baton; + struct node_baton *parent_baton; + svn_repos_node_t *node; +}; + + +static svn_error_t * +delete_entry(const char *path, + svn_revnum_t revision, + void *parent_baton, + apr_pool_t *pool) +{ + struct node_baton *d = parent_baton; + struct edit_baton *eb = d->edit_baton; + svn_repos_node_t *node; + const char *name; + const char *base_path; + svn_revnum_t base_rev; + svn_fs_root_t *base_root; + svn_node_kind_t kind; + + /* Get (or create) the change node and update it. */ + name = svn_relpath_basename(path, pool); + node = find_child_by_name(d->node, name); + if (! node) + node = create_child_node(d->node, name, eb->node_pool); + node->action = 'D'; + + /* We need to look up this node's parents to see what its original + path in the filesystem was. Why? Because if this deletion + occurred underneath a copied path, the thing that was deleted + probably lived at a different location (relative to the copy + source). */ + find_real_base_location(&base_path, &base_rev, node, pool); + if (! SVN_IS_VALID_REVNUM(base_rev)) + { + /* No interesting base revision? We'll just look for the path + in our base root. */ + base_root = eb->base_root; + } + else + { + /* Oh. Perhaps some copy goodness happened somewhere? */ + SVN_ERR(svn_fs_revision_root(&base_root, eb->fs, base_rev, pool)); + } + + /* Now figure out if this thing was a file or a dir. */ + SVN_ERR(svn_fs_check_path(&kind, base_root, base_path, pool)); + if (kind == svn_node_none) + return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, + _("'%s' not found in filesystem"), path); + node->kind = kind; + + return SVN_NO_ERROR; +} + + + +static svn_error_t * +add_open_helper(const char *path, + char action, + svn_node_kind_t kind, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + apr_pool_t *pool, + void **child_baton) +{ + struct node_baton *pb = parent_baton; + struct edit_baton *eb = pb->edit_baton; + struct node_baton *nb = apr_pcalloc(pool, sizeof(*nb)); + + SVN_ERR_ASSERT(parent_baton && path); + + nb->edit_baton = eb; + nb->parent_baton = pb; + + /* Create and populate the node. */ + nb->node = create_child_node(pb->node, svn_relpath_basename(path, NULL), + eb->node_pool); + nb->node->kind = kind; + nb->node->action = action; + nb->node->copyfrom_rev = copyfrom_rev; + nb->node->copyfrom_path = + copyfrom_path ? apr_pstrdup(eb->node_pool, copyfrom_path) : NULL; + + *child_baton = nb; + return SVN_NO_ERROR; +} + + +static svn_error_t * +open_root(void *edit_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **root_baton) +{ + struct edit_baton *eb = edit_baton; + struct node_baton *d = apr_pcalloc(pool, sizeof(*d)); + + d->edit_baton = eb; + d->parent_baton = NULL; + d->node = (eb->node = create_node("", NULL, eb->node_pool)); + d->node->kind = svn_node_dir; + d->node->action = 'R'; + *root_baton = d; + + return SVN_NO_ERROR; +} + + +static svn_error_t * +open_directory(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **child_baton) +{ + return add_open_helper(path, 'R', svn_node_dir, parent_baton, + NULL, SVN_INVALID_REVNUM, + pool, child_baton); +} + + +static svn_error_t * +add_directory(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_revision, + apr_pool_t *pool, + void **child_baton) +{ + return add_open_helper(path, 'A', svn_node_dir, parent_baton, + copyfrom_path, copyfrom_revision, + pool, child_baton); +} + + +static svn_error_t * +open_file(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **file_baton) +{ + return add_open_helper(path, 'R', svn_node_file, parent_baton, + NULL, SVN_INVALID_REVNUM, + pool, file_baton); +} + + +static svn_error_t * +add_file(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_revision, + apr_pool_t *pool, + void **file_baton) +{ + return add_open_helper(path, 'A', svn_node_file, parent_baton, + copyfrom_path, copyfrom_revision, + pool, file_baton); +} + + +static svn_error_t * +apply_textdelta(void *file_baton, + const char *base_checksum, + apr_pool_t *pool, + svn_txdelta_window_handler_t *handler, + void **handler_baton) +{ + struct node_baton *fb = file_baton; + fb->node->text_mod = TRUE; + *handler = svn_delta_noop_window_handler; + *handler_baton = NULL; + return SVN_NO_ERROR; +} + + + +static svn_error_t * +change_node_prop(void *node_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + struct node_baton *nb = node_baton; + nb->node->prop_mod = TRUE; + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_node_editor(const svn_delta_editor_t **editor, + void **edit_baton, + svn_repos_t *repos, + svn_fs_root_t *base_root, + svn_fs_root_t *root, + apr_pool_t *node_pool, + apr_pool_t *pool) +{ + svn_delta_editor_t *my_editor; + struct edit_baton *my_edit_baton; + + /* Set up the editor. */ + my_editor = svn_delta_default_editor(pool); + my_editor->open_root = open_root; + my_editor->delete_entry = delete_entry; + my_editor->add_directory = add_directory; + my_editor->open_directory = open_directory; + my_editor->add_file = add_file; + my_editor->open_file = open_file; + my_editor->apply_textdelta = apply_textdelta; + my_editor->change_file_prop = change_node_prop; + my_editor->change_dir_prop = change_node_prop; + + /* Set up the edit baton. */ + my_edit_baton = apr_pcalloc(pool, sizeof(*my_edit_baton)); + my_edit_baton->node_pool = node_pool; + my_edit_baton->fs = repos->fs; + my_edit_baton->root = root; + my_edit_baton->base_root = base_root; + + *editor = my_editor; + *edit_baton = my_edit_baton; + + return SVN_NO_ERROR; +} + + + +svn_repos_node_t * +svn_repos_node_from_baton(void *edit_baton) +{ + struct edit_baton *eb = edit_baton; + return eb->node; +} diff --git a/subversion/libsvn_repos/notify.c b/subversion/libsvn_repos/notify.c new file mode 100644 index 0000000..ac2bca4 --- /dev/null +++ b/subversion/libsvn_repos/notify.c @@ -0,0 +1,44 @@ +/* notify.c --- notifcation system + * + * ==================================================================== + * 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 <stdio.h> +#include <string.h> +#include <ctype.h> + +#include "svn_pools.h" +#include "svn_repos.h" +#include "repos.h" +#include "svn_private_config.h" +#include "private/svn_utf_private.h" + + + +svn_repos_notify_t * +svn_repos_notify_create(svn_repos_notify_action_t action, + apr_pool_t *result_pool) +{ + svn_repos_notify_t *notify = apr_pcalloc(result_pool, sizeof(*notify)); + + notify->action = action; + + return notify; +} diff --git a/subversion/libsvn_repos/replay.c b/subversion/libsvn_repos/replay.c new file mode 100644 index 0000000..985a673 --- /dev/null +++ b/subversion/libsvn_repos/replay.c @@ -0,0 +1,1591 @@ +/* + * replay.c: an editor driver for changes made in a given revision + * or transaction + * + * ==================================================================== + * 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_hash.h> + +#include "svn_types.h" +#include "svn_delta.h" +#include "svn_hash.h" +#include "svn_fs.h" +#include "svn_checksum.h" +#include "svn_repos.h" +#include "svn_sorts.h" +#include "svn_props.h" +#include "svn_pools.h" +#include "svn_path.h" +#include "svn_private_config.h" +#include "private/svn_fspath.h" +#include "private/svn_repos_private.h" +#include "private/svn_delta_private.h" + + +/*** Backstory ***/ + +/* The year was 2003. Subversion usage was rampant in the world, and + there was a rapidly growing issues database to prove it. To make + matters worse, svn_repos_dir_delta() had simply outgrown itself. + No longer content to simply describe the differences between two + trees, the function had been slowly bearing the added + responsibility of representing the actions that had been taken to + cause those differences -- a burden it was never meant to bear. + Now grown into a twisted mess of razor-sharp metal and glass, and + trembling with a sort of momentarily stayed spring force, + svn_repos_dir_delta was a timebomb poised for total annihilation of + the American Midwest. + + Subversion needed a change. + + Changes, in fact. And not just in the literary segue sense. What + Subversion desperately needed was a new mechanism solely + responsible for replaying repository actions back to some + interested party -- to translate and retransmit the contents of the + Berkeley 'changes' database file. */ + +/*** Overview ***/ + +/* The filesystem keeps a record of high-level actions that affect the + files and directories in itself. The 'changes' table records + additions, deletions, textual and property modifications, and so + on. The goal of the functions in this file is to examine those + change records, and use them to drive an editor interface in such a + way as to effectively replay those actions. + + This is critically different than what svn_repos_dir_delta() was + designed to do. That function describes, in the simplest way it + can, how to transform one tree into another. It doesn't care + whether or not this was the same way a user might have done this + transformation. More to the point, it doesn't care if this is how + those differences *did* come into being. And it is for this reason + that it cannot be relied upon for tasks such as the repository + dumpfile-generation code, which is supposed to represent not + changes, but actions that cause changes. + + So, what's the plan here? + + First, we fetch the changes for a particular revision or + transaction. We get these as an array, sorted chronologically. + From this array we will build a hash, keyed on the path associated + with each change item, and whose values are arrays of changes made + to that path, again preserving the chronological ordering. + + Once our hash is built, we then sort all the keys of the hash (the + paths) using a depth-first directory sort routine. + + Finally, we drive an editor, moving down our list of sorted paths, + and manufacturing any intermediate editor calls (directory openings + and closures) needed to navigate between each successive path. For + each path, we replay the sorted actions that occurred at that path. + + When we've finished the editor drive, we should have fully replayed + the filesystem events that occurred in that revision or transaction + (though not necessarily in the same order in which they + occurred). */ + +/* #define USE_EV2_IMPL */ + + +/*** Helper functions. ***/ + + +/* Information for an active copy, that is a directory which we are currently + working on and which was added with history. */ +struct copy_info +{ + /* Destination relpath (relative to the root of the . */ + const char *path; + + /* Copy source path (expressed as an absolute FS path) or revision. + NULL and SVN_INVALID_REVNUM if this is an add without history, + nested inside an add with history. */ + const char *copyfrom_path; + svn_revnum_t copyfrom_rev; +}; + +struct path_driver_cb_baton +{ + const svn_delta_editor_t *editor; + void *edit_baton; + + /* The root of the revision we're replaying. */ + svn_fs_root_t *root; + + /* The root of the previous revision. If this is non-NULL it means that + we are supposed to generate props and text deltas relative to it. */ + svn_fs_root_t *compare_root; + + apr_hash_t *changed_paths; + + svn_repos_authz_func_t authz_read_func; + void *authz_read_baton; + + const char *base_path; /* relpath */ + + svn_revnum_t low_water_mark; + /* Stack of active copy operations. */ + apr_array_header_t *copies; + + /* The global pool for this replay operation. */ + apr_pool_t *pool; +}; + +/* Recursively traverse EDIT_PATH (as it exists under SOURCE_ROOT) emitting + the appropriate editor calls to add it and its children without any + history. This is meant to be used when either a subset of the tree + has been ignored and we need to copy something from that subset to + the part of the tree we do care about, or if a subset of the tree is + unavailable because of authz and we need to use it as the source of + a copy. */ +static svn_error_t * +add_subdir(svn_fs_root_t *source_root, + svn_fs_root_t *target_root, + const svn_delta_editor_t *editor, + void *edit_baton, + const char *edit_path, + void *parent_baton, + const char *source_fspath, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_hash_t *changed_paths, + apr_pool_t *pool, + void **dir_baton) +{ + apr_pool_t *subpool = svn_pool_create(pool); + apr_hash_index_t *hi, *phi; + apr_hash_t *dirents; + apr_hash_t *props; + + SVN_ERR(editor->add_directory(edit_path, parent_baton, NULL, + SVN_INVALID_REVNUM, pool, dir_baton)); + + SVN_ERR(svn_fs_node_proplist(&props, target_root, edit_path, pool)); + + for (phi = apr_hash_first(pool, props); phi; phi = apr_hash_next(phi)) + { + const void *key; + void *val; + + svn_pool_clear(subpool); + apr_hash_this(phi, &key, NULL, &val); + SVN_ERR(editor->change_dir_prop(*dir_baton, key, val, subpool)); + } + + /* We have to get the dirents from the source path, not the target, + because we want nested copies from *readable* paths to be handled by + path_driver_cb_func, not add_subdir (in order to preserve history). */ + SVN_ERR(svn_fs_dir_entries(&dirents, source_root, source_fspath, pool)); + + for (hi = apr_hash_first(pool, dirents); hi; hi = apr_hash_next(hi)) + { + svn_fs_path_change2_t *change; + svn_boolean_t readable = TRUE; + svn_fs_dirent_t *dent; + const char *copyfrom_path = NULL; + svn_revnum_t copyfrom_rev = SVN_INVALID_REVNUM; + const char *new_edit_path; + void *val; + + svn_pool_clear(subpool); + + apr_hash_this(hi, NULL, NULL, &val); + + dent = val; + + new_edit_path = svn_relpath_join(edit_path, dent->name, subpool); + + /* If a file or subdirectory of the copied directory is listed as a + changed path (because it was modified after the copy but before the + commit), we remove it from the changed_paths hash so that future + calls to path_driver_cb_func will ignore it. */ + change = svn_hash_gets(changed_paths, new_edit_path); + if (change) + { + svn_hash_sets(changed_paths, new_edit_path, NULL); + + /* If it's a delete, skip this entry. */ + if (change->change_kind == svn_fs_path_change_delete) + continue; + + /* If it's a replacement, check for copyfrom info (if we + don't have it already. */ + if (change->change_kind == svn_fs_path_change_replace) + { + if (! change->copyfrom_known) + { + SVN_ERR(svn_fs_copied_from(&change->copyfrom_rev, + &change->copyfrom_path, + target_root, new_edit_path, pool)); + change->copyfrom_known = TRUE; + } + copyfrom_path = change->copyfrom_path; + copyfrom_rev = change->copyfrom_rev; + } + } + + if (authz_read_func) + SVN_ERR(authz_read_func(&readable, target_root, new_edit_path, + authz_read_baton, pool)); + + if (! readable) + continue; + + if (dent->kind == svn_node_dir) + { + svn_fs_root_t *new_source_root; + const char *new_source_fspath; + void *new_dir_baton; + + if (copyfrom_path) + { + svn_fs_t *fs = svn_fs_root_fs(source_root); + SVN_ERR(svn_fs_revision_root(&new_source_root, fs, + copyfrom_rev, pool)); + new_source_fspath = copyfrom_path; + } + else + { + new_source_root = source_root; + new_source_fspath = svn_fspath__join(source_fspath, dent->name, + subpool); + } + + /* ### authz considerations? + * + * I think not; when path_driver_cb_func() calls add_subdir(), it + * passes SOURCE_ROOT and SOURCE_FSPATH that are unreadable. + */ + if (change && change->change_kind == svn_fs_path_change_replace + && copyfrom_path == NULL) + { + SVN_ERR(editor->add_directory(new_edit_path, *dir_baton, + NULL, SVN_INVALID_REVNUM, + subpool, &new_dir_baton)); + } + else + { + SVN_ERR(add_subdir(new_source_root, target_root, + editor, edit_baton, new_edit_path, + *dir_baton, new_source_fspath, + authz_read_func, authz_read_baton, + changed_paths, subpool, &new_dir_baton)); + } + + SVN_ERR(editor->close_directory(new_dir_baton, subpool)); + } + else if (dent->kind == svn_node_file) + { + svn_txdelta_window_handler_t delta_handler; + void *delta_handler_baton, *file_baton; + svn_txdelta_stream_t *delta_stream; + svn_checksum_t *checksum; + + SVN_ERR(editor->add_file(new_edit_path, *dir_baton, NULL, + SVN_INVALID_REVNUM, pool, &file_baton)); + + SVN_ERR(svn_fs_node_proplist(&props, target_root, + new_edit_path, subpool)); + + for (phi = apr_hash_first(pool, props); phi; phi = apr_hash_next(phi)) + { + const void *key; + + apr_hash_this(phi, &key, NULL, &val); + SVN_ERR(editor->change_file_prop(file_baton, key, val, subpool)); + } + + SVN_ERR(editor->apply_textdelta(file_baton, NULL, pool, + &delta_handler, + &delta_handler_baton)); + + SVN_ERR(svn_fs_get_file_delta_stream(&delta_stream, NULL, NULL, + target_root, new_edit_path, + pool)); + + SVN_ERR(svn_txdelta_send_txstream(delta_stream, + delta_handler, + delta_handler_baton, + pool)); + + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, target_root, + new_edit_path, TRUE, pool)); + SVN_ERR(editor->close_file(file_baton, + svn_checksum_to_cstring(checksum, pool), + pool)); + } + else + SVN_ERR_MALFUNCTION(); + } + + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + +/* Given PATH deleted under ROOT, return in READABLE whether the path was + readable prior to the deletion. Consult COPIES (a stack of 'struct + copy_info') and AUTHZ_READ_FUNC. */ +static svn_error_t * +was_readable(svn_boolean_t *readable, + svn_fs_root_t *root, + const char *path, + apr_array_header_t *copies, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_fs_root_t *inquire_root; + const char *inquire_path; + struct copy_info *info = NULL; + const char *relpath; + + /* Short circuit. */ + if (! authz_read_func) + { + *readable = TRUE; + return SVN_NO_ERROR; + } + + if (copies->nelts != 0) + info = APR_ARRAY_IDX(copies, copies->nelts - 1, struct copy_info *); + + /* Are we under a copy? */ + if (info && (relpath = svn_relpath_skip_ancestor(info->path, path))) + { + SVN_ERR(svn_fs_revision_root(&inquire_root, svn_fs_root_fs(root), + info->copyfrom_rev, scratch_pool)); + inquire_path = svn_fspath__join(info->copyfrom_path, relpath, + scratch_pool); + } + else + { + /* Compute the revision that ROOT is based on. (Note that ROOT is not + r0's root, since this function is only called for deletions.) + ### Need a more succinct way to express this */ + svn_revnum_t inquire_rev = SVN_INVALID_REVNUM; + if (svn_fs_is_txn_root(root)) + inquire_rev = svn_fs_txn_root_base_revision(root); + if (svn_fs_is_revision_root(root)) + inquire_rev = svn_fs_revision_root_revision(root)-1; + SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(inquire_rev)); + + SVN_ERR(svn_fs_revision_root(&inquire_root, svn_fs_root_fs(root), + inquire_rev, scratch_pool)); + inquire_path = path; + } + + SVN_ERR(authz_read_func(readable, inquire_root, inquire_path, + authz_read_baton, result_pool)); + + return SVN_NO_ERROR; +} + +/* Initialize COPYFROM_ROOT, COPYFROM_PATH, and COPYFROM_REV with the + revision root, fspath, and revnum of the copyfrom of CHANGE, which + corresponds to PATH under ROOT. If the copyfrom info is valid + (i.e., is not (NULL, SVN_INVALID_REVNUM)), then initialize SRC_READABLE + too, consulting AUTHZ_READ_FUNC and AUTHZ_READ_BATON if provided. + + NOTE: If the copyfrom information in CHANGE is marked as unknown + (meaning, its ->copyfrom_rev and ->copyfrom_path cannot be + trusted), this function will also update those members of the + CHANGE structure to carry accurate copyfrom information. */ +static svn_error_t * +fill_copyfrom(svn_fs_root_t **copyfrom_root, + const char **copyfrom_path, + svn_revnum_t *copyfrom_rev, + svn_boolean_t *src_readable, + svn_fs_root_t *root, + svn_fs_path_change2_t *change, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + const char *path, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (! change->copyfrom_known) + { + SVN_ERR(svn_fs_copied_from(&(change->copyfrom_rev), + &(change->copyfrom_path), + root, path, result_pool)); + change->copyfrom_known = TRUE; + } + *copyfrom_rev = change->copyfrom_rev; + *copyfrom_path = change->copyfrom_path; + + if (*copyfrom_path && SVN_IS_VALID_REVNUM(*copyfrom_rev)) + { + SVN_ERR(svn_fs_revision_root(copyfrom_root, + svn_fs_root_fs(root), + *copyfrom_rev, result_pool)); + + if (authz_read_func) + { + SVN_ERR(authz_read_func(src_readable, *copyfrom_root, + *copyfrom_path, + authz_read_baton, result_pool)); + } + else + *src_readable = TRUE; + } + else + { + *copyfrom_root = NULL; + /* SRC_READABLE left uninitialized */ + } + return SVN_NO_ERROR; +} + +static svn_error_t * +path_driver_cb_func(void **dir_baton, + void *parent_baton, + void *callback_baton, + const char *edit_path, + apr_pool_t *pool) +{ + struct path_driver_cb_baton *cb = callback_baton; + const svn_delta_editor_t *editor = cb->editor; + void *edit_baton = cb->edit_baton; + svn_fs_root_t *root = cb->root; + svn_fs_path_change2_t *change; + svn_boolean_t do_add = FALSE, do_delete = FALSE; + void *file_baton = NULL; + svn_revnum_t copyfrom_rev; + const char *copyfrom_path; + svn_fs_root_t *source_root = cb->compare_root; + const char *source_fspath = NULL; + const char *base_path = cb->base_path; + + *dir_baton = NULL; + + /* Initialize SOURCE_FSPATH. */ + if (source_root) + source_fspath = svn_fspath__canonicalize(edit_path, pool); + + /* First, flush the copies stack so it only contains ancestors of path. */ + while (cb->copies->nelts > 0 + && ! svn_dirent_is_ancestor(APR_ARRAY_IDX(cb->copies, + cb->copies->nelts - 1, + struct copy_info *)->path, + edit_path)) + apr_array_pop(cb->copies); + + change = svn_hash_gets(cb->changed_paths, edit_path); + if (! change) + { + /* This can only happen if the path was removed from cb->changed_paths + by an earlier call to add_subdir, which means the path was already + handled and we should simply ignore it. */ + return SVN_NO_ERROR; + } + switch (change->change_kind) + { + case svn_fs_path_change_add: + do_add = TRUE; + break; + + case svn_fs_path_change_delete: + do_delete = TRUE; + break; + + case svn_fs_path_change_replace: + do_add = TRUE; + do_delete = TRUE; + break; + + case svn_fs_path_change_modify: + default: + /* do nothing */ + break; + } + + /* Handle any deletions. */ + if (do_delete) + { + svn_boolean_t readable; + + /* Issue #4121: delete under under a copy, of a path that was unreadable + at its pre-copy location. */ + SVN_ERR(was_readable(&readable, root, edit_path, cb->copies, + cb->authz_read_func, cb->authz_read_baton, + pool, pool)); + if (readable) + SVN_ERR(editor->delete_entry(edit_path, SVN_INVALID_REVNUM, + parent_baton, pool)); + } + + /* Fetch the node kind if it makes sense to do so. */ + if (! do_delete || do_add) + { + if (change->node_kind == svn_node_unknown) + SVN_ERR(svn_fs_check_path(&(change->node_kind), root, edit_path, pool)); + if ((change->node_kind != svn_node_dir) && + (change->node_kind != svn_node_file)) + return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, + _("Filesystem path '%s' is neither a file " + "nor a directory"), edit_path); + } + + /* Handle any adds/opens. */ + if (do_add) + { + svn_boolean_t src_readable; + svn_fs_root_t *copyfrom_root; + + /* Was this node copied? */ + SVN_ERR(fill_copyfrom(©from_root, ©from_path, ©from_rev, + &src_readable, root, change, + cb->authz_read_func, cb->authz_read_baton, + edit_path, pool, pool)); + + /* If we have a copyfrom path, and we can't read it or we're just + ignoring it, or the copyfrom rev is prior to the low water mark + then we just null them out and do a raw add with no history at + all. */ + if (copyfrom_path + && ((! src_readable) + || (svn_relpath_skip_ancestor(base_path, copyfrom_path + 1) == NULL) + || (cb->low_water_mark > copyfrom_rev))) + { + copyfrom_path = NULL; + copyfrom_rev = SVN_INVALID_REVNUM; + } + + /* Do the right thing based on the path KIND. */ + if (change->node_kind == svn_node_dir) + { + /* If this is a copy, but we can't represent it as such, + then we just do a recursive add of the source path + contents. */ + if (change->copyfrom_path && ! copyfrom_path) + { + SVN_ERR(add_subdir(copyfrom_root, root, editor, edit_baton, + edit_path, parent_baton, change->copyfrom_path, + cb->authz_read_func, cb->authz_read_baton, + cb->changed_paths, pool, dir_baton)); + } + else + { + SVN_ERR(editor->add_directory(edit_path, parent_baton, + copyfrom_path, copyfrom_rev, + pool, dir_baton)); + } + } + else + { + SVN_ERR(editor->add_file(edit_path, parent_baton, copyfrom_path, + copyfrom_rev, pool, &file_baton)); + } + + /* If we represent this as a copy... */ + if (copyfrom_path) + { + /* If it is a directory, make sure descendants get the correct + delta source by remembering that we are operating inside a + (possibly nested) copy operation. */ + if (change->node_kind == svn_node_dir) + { + struct copy_info *info = apr_pcalloc(cb->pool, sizeof(*info)); + + info->path = apr_pstrdup(cb->pool, edit_path); + info->copyfrom_path = apr_pstrdup(cb->pool, copyfrom_path); + info->copyfrom_rev = copyfrom_rev; + + APR_ARRAY_PUSH(cb->copies, struct copy_info *) = info; + } + + /* Save the source so that we can use it later, when we + need to generate text and prop deltas. */ + source_root = copyfrom_root; + source_fspath = copyfrom_path; + } + else + /* Else, we are an add without history... */ + { + /* If an ancestor is added with history, we need to forget about + that here, go on with life and repeat all the mistakes of our + past... */ + if (change->node_kind == svn_node_dir && cb->copies->nelts > 0) + { + struct copy_info *info = apr_pcalloc(cb->pool, sizeof(*info)); + + info->path = apr_pstrdup(cb->pool, edit_path); + info->copyfrom_path = NULL; + info->copyfrom_rev = SVN_INVALID_REVNUM; + + APR_ARRAY_PUSH(cb->copies, struct copy_info *) = info; + } + source_root = NULL; + source_fspath = NULL; + } + } + else if (! do_delete) + { + /* Do the right thing based on the path KIND (and the presence + of a PARENT_BATON). */ + if (change->node_kind == svn_node_dir) + { + if (parent_baton) + { + SVN_ERR(editor->open_directory(edit_path, parent_baton, + SVN_INVALID_REVNUM, + pool, dir_baton)); + } + else + { + SVN_ERR(editor->open_root(edit_baton, SVN_INVALID_REVNUM, + pool, dir_baton)); + } + } + else + { + SVN_ERR(editor->open_file(edit_path, parent_baton, SVN_INVALID_REVNUM, + pool, &file_baton)); + } + /* If we are inside an add with history, we need to adjust the + delta source. */ + if (cb->copies->nelts > 0) + { + struct copy_info *info = APR_ARRAY_IDX(cb->copies, + cb->copies->nelts - 1, + struct copy_info *); + if (info->copyfrom_path) + { + const char *relpath = svn_relpath_skip_ancestor(info->path, + edit_path); + SVN_ERR_ASSERT(relpath && *relpath); + SVN_ERR(svn_fs_revision_root(&source_root, + svn_fs_root_fs(root), + info->copyfrom_rev, pool)); + source_fspath = svn_fspath__join(info->copyfrom_path, + relpath, pool); + } + else + { + /* This is an add without history, nested inside an + add with history. We have no delta source in this case. */ + source_root = NULL; + source_fspath = NULL; + } + } + } + + if (! do_delete || do_add) + { + /* Is this a copy that was downgraded to a raw add? (If so, + we'll need to transmit properties and file contents and such + for it regardless of what the CHANGE structure's text_mod + and prop_mod flags say.) */ + svn_boolean_t downgraded_copy = (change->copyfrom_known + && change->copyfrom_path + && (! copyfrom_path)); + + /* Handle property modifications. */ + if (change->prop_mod || downgraded_copy) + { + if (cb->compare_root) + { + apr_array_header_t *prop_diffs; + apr_hash_t *old_props; + apr_hash_t *new_props; + int i; + + if (source_root) + SVN_ERR(svn_fs_node_proplist(&old_props, source_root, + source_fspath, pool)); + else + old_props = apr_hash_make(pool); + + SVN_ERR(svn_fs_node_proplist(&new_props, root, edit_path, pool)); + + SVN_ERR(svn_prop_diffs(&prop_diffs, new_props, old_props, + pool)); + + for (i = 0; i < prop_diffs->nelts; ++i) + { + svn_prop_t *pc = &APR_ARRAY_IDX(prop_diffs, i, svn_prop_t); + if (change->node_kind == svn_node_dir) + SVN_ERR(editor->change_dir_prop(*dir_baton, pc->name, + pc->value, pool)); + else if (change->node_kind == svn_node_file) + SVN_ERR(editor->change_file_prop(file_baton, pc->name, + pc->value, pool)); + } + } + else + { + /* Just do a dummy prop change to signal that there are *any* + propmods. */ + if (change->node_kind == svn_node_dir) + SVN_ERR(editor->change_dir_prop(*dir_baton, "", NULL, + pool)); + else if (change->node_kind == svn_node_file) + SVN_ERR(editor->change_file_prop(file_baton, "", NULL, + pool)); + } + } + + /* Handle textual modifications. */ + if (change->node_kind == svn_node_file + && (change->text_mod || downgraded_copy)) + { + svn_txdelta_window_handler_t delta_handler; + void *delta_handler_baton; + const char *hex_digest = NULL; + + if (cb->compare_root && source_root && source_fspath) + { + svn_checksum_t *checksum; + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, + source_root, source_fspath, TRUE, + pool)); + hex_digest = svn_checksum_to_cstring(checksum, pool); + } + + SVN_ERR(editor->apply_textdelta(file_baton, hex_digest, pool, + &delta_handler, + &delta_handler_baton)); + if (cb->compare_root) + { + svn_txdelta_stream_t *delta_stream; + + SVN_ERR(svn_fs_get_file_delta_stream(&delta_stream, source_root, + source_fspath, root, + edit_path, pool)); + SVN_ERR(svn_txdelta_send_txstream(delta_stream, delta_handler, + delta_handler_baton, pool)); + } + else + SVN_ERR(delta_handler(NULL, delta_handler_baton)); + } + } + + /* Close the file baton if we opened it. */ + if (file_baton) + { + svn_checksum_t *checksum; + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, root, edit_path, + TRUE, pool)); + SVN_ERR(editor->close_file(file_baton, + svn_checksum_to_cstring(checksum, pool), + pool)); + } + + return SVN_NO_ERROR; +} + +#ifdef USE_EV2_IMPL +static svn_error_t * +fetch_kind_func(svn_node_kind_t *kind, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *scratch_pool) +{ + svn_fs_root_t *root = baton; + svn_fs_root_t *prev_root; + svn_fs_t *fs = svn_fs_root_fs(root); + + if (!SVN_IS_VALID_REVNUM(base_revision)) + base_revision = svn_fs_revision_root_revision(root) - 1; + + SVN_ERR(svn_fs_revision_root(&prev_root, fs, base_revision, scratch_pool)); + SVN_ERR(svn_fs_check_path(kind, prev_root, path, scratch_pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_props_func(apr_hash_t **props, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_fs_root_t *root = baton; + svn_fs_root_t *prev_root; + svn_fs_t *fs = svn_fs_root_fs(root); + + if (!SVN_IS_VALID_REVNUM(base_revision)) + base_revision = svn_fs_revision_root_revision(root) - 1; + + SVN_ERR(svn_fs_revision_root(&prev_root, fs, base_revision, scratch_pool)); + SVN_ERR(svn_fs_node_proplist(props, prev_root, path, result_pool)); + + return SVN_NO_ERROR; +} +#endif + + + + +svn_error_t * +svn_repos_replay2(svn_fs_root_t *root, + const char *base_path, + svn_revnum_t low_water_mark, + svn_boolean_t send_deltas, + const svn_delta_editor_t *editor, + void *edit_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ +#ifndef USE_EV2_IMPL + apr_hash_t *fs_changes; + apr_hash_t *changed_paths; + apr_hash_index_t *hi; + apr_array_header_t *paths; + struct path_driver_cb_baton cb_baton; + + /* Special-case r0, which we know is an empty revision; if we don't + special-case it we might end up trying to compare it to "r-1". */ + if (svn_fs_is_revision_root(root) && svn_fs_revision_root_revision(root) == 0) + { + SVN_ERR(editor->set_target_revision(edit_baton, 0, pool)); + return SVN_NO_ERROR; + } + + /* Fetch the paths changed under ROOT. */ + SVN_ERR(svn_fs_paths_changed2(&fs_changes, root, pool)); + + if (! base_path) + base_path = ""; + else if (base_path[0] == '/') + ++base_path; + + /* Make an array from the keys of our CHANGED_PATHS hash, and copy + the values into a new hash whose keys have no leading slashes. */ + paths = apr_array_make(pool, apr_hash_count(fs_changes), + sizeof(const char *)); + changed_paths = apr_hash_make(pool); + for (hi = apr_hash_first(pool, fs_changes); hi; hi = apr_hash_next(hi)) + { + const void *key; + void *val; + apr_ssize_t keylen; + const char *path; + svn_fs_path_change2_t *change; + svn_boolean_t allowed = TRUE; + + apr_hash_this(hi, &key, &keylen, &val); + path = key; + change = val; + + if (authz_read_func) + SVN_ERR(authz_read_func(&allowed, root, path, authz_read_baton, + pool)); + + if (allowed) + { + if (path[0] == '/') + { + path++; + keylen--; + } + + /* If the base_path doesn't match the top directory of this path + we don't want anything to do with it... */ + if (svn_relpath_skip_ancestor(base_path, path) != NULL) + { + APR_ARRAY_PUSH(paths, const char *) = path; + apr_hash_set(changed_paths, path, keylen, change); + } + /* ...unless this was a change to one of the parent directories of + base_path. */ + else if (svn_relpath_skip_ancestor(path, base_path) != NULL) + { + APR_ARRAY_PUSH(paths, const char *) = path; + apr_hash_set(changed_paths, path, keylen, change); + } + } + } + + /* If we were not given a low water mark, assume that everything is there, + all the way back to revision 0. */ + if (! SVN_IS_VALID_REVNUM(low_water_mark)) + low_water_mark = 0; + + /* Initialize our callback baton. */ + cb_baton.editor = editor; + cb_baton.edit_baton = edit_baton; + cb_baton.root = root; + cb_baton.changed_paths = changed_paths; + cb_baton.authz_read_func = authz_read_func; + cb_baton.authz_read_baton = authz_read_baton; + cb_baton.base_path = base_path; + cb_baton.low_water_mark = low_water_mark; + cb_baton.compare_root = NULL; + + if (send_deltas) + { + SVN_ERR(svn_fs_revision_root(&cb_baton.compare_root, + svn_fs_root_fs(root), + svn_fs_is_revision_root(root) + ? svn_fs_revision_root_revision(root) - 1 + : svn_fs_txn_root_base_revision(root), + pool)); + } + + cb_baton.copies = apr_array_make(pool, 4, sizeof(struct copy_info *)); + cb_baton.pool = pool; + + /* Determine the revision to use throughout the edit, and call + EDITOR's set_target_revision() function. */ + if (svn_fs_is_revision_root(root)) + { + svn_revnum_t revision = svn_fs_revision_root_revision(root); + SVN_ERR(editor->set_target_revision(edit_baton, revision, pool)); + } + + /* Call the path-based editor driver. */ + return svn_delta_path_driver2(editor, edit_baton, + paths, TRUE, + path_driver_cb_func, &cb_baton, pool); +#else + svn_editor_t *editorv2; + struct svn_delta__extra_baton *exb; + svn_delta__unlock_func_t unlock_func; + svn_boolean_t send_abs_paths; + const char *repos_root = ""; + void *unlock_baton; + + /* Special-case r0, which we know is an empty revision; if we don't + special-case it we might end up trying to compare it to "r-1". */ + if (svn_fs_is_revision_root(root) + && svn_fs_revision_root_revision(root) == 0) + { + SVN_ERR(editor->set_target_revision(edit_baton, 0, pool)); + return SVN_NO_ERROR; + } + + /* Determine the revision to use throughout the edit, and call + EDITOR's set_target_revision() function. */ + if (svn_fs_is_revision_root(root)) + { + svn_revnum_t revision = svn_fs_revision_root_revision(root); + SVN_ERR(editor->set_target_revision(edit_baton, revision, pool)); + } + + if (! base_path) + base_path = ""; + else if (base_path[0] == '/') + ++base_path; + + /* Use the shim to convert our editor to an Ev2 editor, and pass it down + the stack. */ + SVN_ERR(svn_delta__editor_from_delta(&editorv2, &exb, + &unlock_func, &unlock_baton, + editor, edit_baton, + &send_abs_paths, + repos_root, "", + NULL, NULL, + fetch_kind_func, root, + fetch_props_func, root, + pool, pool)); + + /* Tell the shim that we're starting the process. */ + SVN_ERR(exb->start_edit(exb->baton, svn_fs_revision_root_revision(root))); + + /* ### We're ignoring SEND_DELTAS here. */ + SVN_ERR(svn_repos__replay_ev2(root, base_path, low_water_mark, + editorv2, authz_read_func, authz_read_baton, + pool)); + + return SVN_NO_ERROR; +#endif +} + + +/***************************************************************** + * Ev2 Implementation * + *****************************************************************/ + +/* Recursively traverse EDIT_PATH (as it exists under SOURCE_ROOT) emitting + the appropriate editor calls to add it and its children without any + history. This is meant to be used when either a subset of the tree + has been ignored and we need to copy something from that subset to + the part of the tree we do care about, or if a subset of the tree is + unavailable because of authz and we need to use it as the source of + a copy. */ +static svn_error_t * +add_subdir_ev2(svn_fs_root_t *source_root, + svn_fs_root_t *target_root, + svn_editor_t *editor, + const char *repos_relpath, + const char *source_fspath, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_hash_t *changed_paths, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_pool_t *iterpool = svn_pool_create(scratch_pool); + apr_hash_index_t *hi; + apr_hash_t *dirents; + apr_hash_t *props = NULL; + apr_array_header_t *children = NULL; + + SVN_ERR(svn_fs_node_proplist(&props, target_root, repos_relpath, + scratch_pool)); + + SVN_ERR(svn_editor_add_directory(editor, repos_relpath, children, + props, SVN_INVALID_REVNUM)); + + /* We have to get the dirents from the source path, not the target, + because we want nested copies from *readable* paths to be handled by + path_driver_cb_func, not add_subdir (in order to preserve history). */ + SVN_ERR(svn_fs_dir_entries(&dirents, source_root, source_fspath, + scratch_pool)); + + for (hi = apr_hash_first(scratch_pool, dirents); hi; hi = apr_hash_next(hi)) + { + svn_fs_path_change2_t *change; + svn_boolean_t readable = TRUE; + svn_fs_dirent_t *dent = svn__apr_hash_index_val(hi); + const char *copyfrom_path = NULL; + svn_revnum_t copyfrom_rev = SVN_INVALID_REVNUM; + const char *child_relpath; + + svn_pool_clear(iterpool); + + child_relpath = svn_relpath_join(repos_relpath, dent->name, iterpool); + + /* If a file or subdirectory of the copied directory is listed as a + changed path (because it was modified after the copy but before the + commit), we remove it from the changed_paths hash so that future + calls to path_driver_cb_func will ignore it. */ + change = svn_hash_gets(changed_paths, child_relpath); + if (change) + { + svn_hash_sets(changed_paths, child_relpath, NULL); + + /* If it's a delete, skip this entry. */ + if (change->change_kind == svn_fs_path_change_delete) + continue; + + /* If it's a replacement, check for copyfrom info (if we + don't have it already. */ + if (change->change_kind == svn_fs_path_change_replace) + { + if (! change->copyfrom_known) + { + SVN_ERR(svn_fs_copied_from(&change->copyfrom_rev, + &change->copyfrom_path, + target_root, child_relpath, + result_pool)); + change->copyfrom_known = TRUE; + } + copyfrom_path = change->copyfrom_path; + copyfrom_rev = change->copyfrom_rev; + } + } + + if (authz_read_func) + SVN_ERR(authz_read_func(&readable, target_root, child_relpath, + authz_read_baton, iterpool)); + + if (! readable) + continue; + + if (dent->kind == svn_node_dir) + { + svn_fs_root_t *new_source_root; + const char *new_source_fspath; + + if (copyfrom_path) + { + svn_fs_t *fs = svn_fs_root_fs(source_root); + SVN_ERR(svn_fs_revision_root(&new_source_root, fs, + copyfrom_rev, result_pool)); + new_source_fspath = copyfrom_path; + } + else + { + new_source_root = source_root; + new_source_fspath = svn_fspath__join(source_fspath, dent->name, + iterpool); + } + + /* ### authz considerations? + * + * I think not; when path_driver_cb_func() calls add_subdir(), it + * passes SOURCE_ROOT and SOURCE_FSPATH that are unreadable. + */ + if (change && change->change_kind == svn_fs_path_change_replace + && copyfrom_path == NULL) + { + SVN_ERR(svn_editor_add_directory(editor, child_relpath, + children, props, + SVN_INVALID_REVNUM)); + } + else + { + SVN_ERR(add_subdir_ev2(new_source_root, target_root, + editor, child_relpath, + new_source_fspath, + authz_read_func, authz_read_baton, + changed_paths, result_pool, iterpool)); + } + } + else if (dent->kind == svn_node_file) + { + svn_checksum_t *checksum; + svn_stream_t *contents; + + SVN_ERR(svn_fs_node_proplist(&props, target_root, + child_relpath, iterpool)); + + SVN_ERR(svn_fs_file_contents(&contents, target_root, + child_relpath, iterpool)); + + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_sha1, + target_root, + child_relpath, TRUE, iterpool)); + + SVN_ERR(svn_editor_add_file(editor, child_relpath, checksum, + contents, props, SVN_INVALID_REVNUM)); + } + else + SVN_ERR_MALFUNCTION(); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +static svn_error_t * +replay_node(svn_fs_root_t *root, + const char *repos_relpath, + svn_editor_t *editor, + svn_revnum_t low_water_mark, + const char *base_repos_relpath, + apr_array_header_t *copies, + apr_hash_t *changed_paths, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_fs_path_change2_t *change; + svn_boolean_t do_add = FALSE; + svn_boolean_t do_delete = FALSE; + svn_revnum_t copyfrom_rev; + const char *copyfrom_path; + svn_revnum_t replaces_rev; + + /* First, flush the copies stack so it only contains ancestors of path. */ + while (copies->nelts > 0 + && (svn_relpath_skip_ancestor(APR_ARRAY_IDX(copies, + copies->nelts - 1, + struct copy_info *)->path, + repos_relpath) == NULL) ) + apr_array_pop(copies); + + change = svn_hash_gets(changed_paths, repos_relpath); + if (! change) + { + /* This can only happen if the path was removed from changed_paths + by an earlier call to add_subdir, which means the path was already + handled and we should simply ignore it. */ + return SVN_NO_ERROR; + } + switch (change->change_kind) + { + case svn_fs_path_change_add: + do_add = TRUE; + break; + + case svn_fs_path_change_delete: + do_delete = TRUE; + break; + + case svn_fs_path_change_replace: + do_add = TRUE; + do_delete = TRUE; + break; + + case svn_fs_path_change_modify: + default: + /* do nothing */ + break; + } + + /* Handle any deletions. */ + if (do_delete && ! do_add) + { + svn_boolean_t readable; + + /* Issue #4121: delete under under a copy, of a path that was unreadable + at its pre-copy location. */ + SVN_ERR(was_readable(&readable, root, repos_relpath, copies, + authz_read_func, authz_read_baton, + scratch_pool, scratch_pool)); + if (readable) + SVN_ERR(svn_editor_delete(editor, repos_relpath, SVN_INVALID_REVNUM)); + + return SVN_NO_ERROR; + } + + /* Handle replacements. */ + if (do_delete && do_add) + replaces_rev = svn_fs_revision_root_revision(root); + else + replaces_rev = SVN_INVALID_REVNUM; + + /* Fetch the node kind if it makes sense to do so. */ + if (! do_delete || do_add) + { + if (change->node_kind == svn_node_unknown) + SVN_ERR(svn_fs_check_path(&(change->node_kind), root, repos_relpath, + scratch_pool)); + if ((change->node_kind != svn_node_dir) && + (change->node_kind != svn_node_file)) + return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, + _("Filesystem path '%s' is neither a file " + "nor a directory"), repos_relpath); + } + + /* Handle any adds/opens. */ + if (do_add) + { + svn_boolean_t src_readable; + svn_fs_root_t *copyfrom_root; + + /* Was this node copied? */ + SVN_ERR(fill_copyfrom(©from_root, ©from_path, ©from_rev, + &src_readable, root, change, + authz_read_func, authz_read_baton, + repos_relpath, scratch_pool, scratch_pool)); + + /* If we have a copyfrom path, and we can't read it or we're just + ignoring it, or the copyfrom rev is prior to the low water mark + then we just null them out and do a raw add with no history at + all. */ + if (copyfrom_path + && ((! src_readable) + || (svn_relpath_skip_ancestor(base_repos_relpath, + copyfrom_path + 1) == NULL) + || (low_water_mark > copyfrom_rev))) + { + copyfrom_path = NULL; + copyfrom_rev = SVN_INVALID_REVNUM; + } + + /* Do the right thing based on the path KIND. */ + if (change->node_kind == svn_node_dir) + { + /* If this is a copy, but we can't represent it as such, + then we just do a recursive add of the source path + contents. */ + if (change->copyfrom_path && ! copyfrom_path) + { + SVN_ERR(add_subdir_ev2(copyfrom_root, root, editor, + repos_relpath, change->copyfrom_path, + authz_read_func, authz_read_baton, + changed_paths, result_pool, + scratch_pool)); + } + else + { + if (copyfrom_path) + { + if (copyfrom_path[0] == '/') + ++copyfrom_path; + SVN_ERR(svn_editor_copy(editor, copyfrom_path, copyfrom_rev, + repos_relpath, replaces_rev)); + } + else + { + apr_array_header_t *children; + apr_hash_t *props; + apr_hash_t *dirents; + + SVN_ERR(svn_fs_dir_entries(&dirents, root, repos_relpath, + scratch_pool)); + SVN_ERR(svn_hash_keys(&children, dirents, scratch_pool)); + + SVN_ERR(svn_fs_node_proplist(&props, root, repos_relpath, + scratch_pool)); + + SVN_ERR(svn_editor_add_directory(editor, repos_relpath, + children, props, + replaces_rev)); + } + } + } + else + { + if (copyfrom_path) + { + if (copyfrom_path[0] == '/') + ++copyfrom_path; + SVN_ERR(svn_editor_copy(editor, copyfrom_path, copyfrom_rev, + repos_relpath, replaces_rev)); + } + else + { + apr_hash_t *props; + svn_checksum_t *checksum; + svn_stream_t *contents; + + SVN_ERR(svn_fs_node_proplist(&props, root, repos_relpath, + scratch_pool)); + + SVN_ERR(svn_fs_file_contents(&contents, root, repos_relpath, + scratch_pool)); + + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_sha1, root, + repos_relpath, TRUE, scratch_pool)); + + SVN_ERR(svn_editor_add_file(editor, repos_relpath, checksum, + contents, props, replaces_rev)); + } + } + + /* If we represent this as a copy... */ + if (copyfrom_path) + { + /* If it is a directory, make sure descendants get the correct + delta source by remembering that we are operating inside a + (possibly nested) copy operation. */ + if (change->node_kind == svn_node_dir) + { + struct copy_info *info = apr_pcalloc(result_pool, sizeof(*info)); + + info->path = apr_pstrdup(result_pool, repos_relpath); + info->copyfrom_path = apr_pstrdup(result_pool, copyfrom_path); + info->copyfrom_rev = copyfrom_rev; + + APR_ARRAY_PUSH(copies, struct copy_info *) = info; + } + } + else + /* Else, we are an add without history... */ + { + /* If an ancestor is added with history, we need to forget about + that here, go on with life and repeat all the mistakes of our + past... */ + if (change->node_kind == svn_node_dir && copies->nelts > 0) + { + struct copy_info *info = apr_pcalloc(result_pool, sizeof(*info)); + + info->path = apr_pstrdup(result_pool, repos_relpath); + info->copyfrom_path = NULL; + info->copyfrom_rev = SVN_INVALID_REVNUM; + + APR_ARRAY_PUSH(copies, struct copy_info *) = info; + } + } + } + else if (! do_delete) + { + /* If we are inside an add with history, we need to adjust the + delta source. */ + if (copies->nelts > 0) + { + struct copy_info *info = APR_ARRAY_IDX(copies, + copies->nelts - 1, + struct copy_info *); + if (info->copyfrom_path) + { + const char *relpath = svn_relpath_skip_ancestor(info->path, + repos_relpath); + SVN_ERR_ASSERT(relpath && *relpath); + repos_relpath = svn_relpath_join(info->copyfrom_path, + relpath, scratch_pool); + } + } + } + + if (! do_delete && !do_add) + { + apr_hash_t *props = NULL; + + /* Is this a copy that was downgraded to a raw add? (If so, + we'll need to transmit properties and file contents and such + for it regardless of what the CHANGE structure's text_mod + and prop_mod flags say.) */ + svn_boolean_t downgraded_copy = (change->copyfrom_known + && change->copyfrom_path + && (! copyfrom_path)); + + /* Handle property modifications. */ + if (change->prop_mod || downgraded_copy) + { + SVN_ERR(svn_fs_node_proplist(&props, root, repos_relpath, + scratch_pool)); + } + + /* Handle textual modifications. */ + if (change->node_kind == svn_node_file + && (change->text_mod || change->prop_mod || downgraded_copy)) + { + svn_checksum_t *checksum = NULL; + svn_stream_t *contents = NULL; + + if (change->text_mod) + { + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_sha1, + root, repos_relpath, TRUE, + scratch_pool)); + + SVN_ERR(svn_fs_file_contents(&contents, root, repos_relpath, + scratch_pool)); + } + + SVN_ERR(svn_editor_alter_file(editor, repos_relpath, + SVN_INVALID_REVNUM, props, checksum, + contents)); + } + + if (change->node_kind == svn_node_dir + && (change->prop_mod || downgraded_copy)) + { + apr_array_header_t *children = NULL; + + SVN_ERR(svn_editor_alter_directory(editor, repos_relpath, + SVN_INVALID_REVNUM, children, + props)); + } + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos__replay_ev2(svn_fs_root_t *root, + const char *base_repos_relpath, + svn_revnum_t low_water_mark, + svn_editor_t *editor, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *scratch_pool) +{ + apr_hash_t *fs_changes; + apr_hash_t *changed_paths; + apr_hash_index_t *hi; + apr_array_header_t *paths; + apr_array_header_t *copies; + apr_pool_t *iterpool; + svn_error_t *err = SVN_NO_ERROR; + int i; + + SVN_ERR_ASSERT(!svn_dirent_is_absolute(base_repos_relpath)); + + /* Special-case r0, which we know is an empty revision; if we don't + special-case it we might end up trying to compare it to "r-1". */ + if (svn_fs_is_revision_root(root) + && svn_fs_revision_root_revision(root) == 0) + { + return SVN_NO_ERROR; + } + + /* Fetch the paths changed under ROOT. */ + SVN_ERR(svn_fs_paths_changed2(&fs_changes, root, scratch_pool)); + + /* Make an array from the keys of our CHANGED_PATHS hash, and copy + the values into a new hash whose keys have no leading slashes. */ + paths = apr_array_make(scratch_pool, apr_hash_count(fs_changes), + sizeof(const char *)); + changed_paths = apr_hash_make(scratch_pool); + for (hi = apr_hash_first(scratch_pool, fs_changes); hi; + hi = apr_hash_next(hi)) + { + const void *key; + void *val; + apr_ssize_t keylen; + const char *path; + svn_fs_path_change2_t *change; + svn_boolean_t allowed = TRUE; + + apr_hash_this(hi, &key, &keylen, &val); + path = key; + change = val; + + if (authz_read_func) + SVN_ERR(authz_read_func(&allowed, root, path, authz_read_baton, + scratch_pool)); + + if (allowed) + { + if (path[0] == '/') + { + path++; + keylen--; + } + + /* If the base_path doesn't match the top directory of this path + we don't want anything to do with it... */ + if (svn_relpath_skip_ancestor(base_repos_relpath, path) != NULL) + { + APR_ARRAY_PUSH(paths, const char *) = path; + apr_hash_set(changed_paths, path, keylen, change); + } + /* ...unless this was a change to one of the parent directories of + base_path. */ + else if (svn_relpath_skip_ancestor(path, base_repos_relpath) != NULL) + { + APR_ARRAY_PUSH(paths, const char *) = path; + apr_hash_set(changed_paths, path, keylen, change); + } + } + } + + /* If we were not given a low water mark, assume that everything is there, + all the way back to revision 0. */ + if (! SVN_IS_VALID_REVNUM(low_water_mark)) + low_water_mark = 0; + + copies = apr_array_make(scratch_pool, 4, sizeof(struct copy_info *)); + + /* Sort the paths. Although not strictly required by the API, this has + the pleasant side effect of maintaining a consistent ordering of + dumpfile contents. */ + qsort(paths->elts, paths->nelts, paths->elt_size, svn_sort_compare_paths); + + /* Now actually handle the various paths. */ + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < paths->nelts; i++) + { + const char *repos_relpath = APR_ARRAY_IDX(paths, i, const char *); + + svn_pool_clear(iterpool); + err = replay_node(root, repos_relpath, editor, low_water_mark, + base_repos_relpath, copies, changed_paths, + authz_read_func, authz_read_baton, + scratch_pool, iterpool); + if (err) + break; + } + + if (err) + return svn_error_compose_create(err, svn_editor_abort(editor)); + else + SVN_ERR(svn_editor_complete(editor)); + + svn_pool_destroy(iterpool); + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_repos/reporter.c b/subversion/libsvn_repos/reporter.c new file mode 100644 index 0000000..a9d1eff --- /dev/null +++ b/subversion/libsvn_repos/reporter.c @@ -0,0 +1,1610 @@ +/* + * reporter.c : `reporter' vtable routines for updates. + * + * ==================================================================== + * 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 "svn_dirent_uri.h" +#include "svn_hash.h" +#include "svn_path.h" +#include "svn_types.h" +#include "svn_error.h" +#include "svn_error_codes.h" +#include "svn_fs.h" +#include "svn_repos.h" +#include "svn_pools.h" +#include "svn_props.h" +#include "repos.h" +#include "svn_private_config.h" + +#include "private/svn_dep_compat.h" +#include "private/svn_fspath.h" +#include "private/svn_subr_private.h" +#include "private/svn_string_private.h" + +#define NUM_CACHED_SOURCE_ROOTS 4 + +/* Theory of operation: we write report operations out to a spill-buffer + as we receive them. When the report is finished, we read the + operations back out again, using them to guide the progression of + the delta between the source and target revs. + + Spill-buffer content format: we use a simple ad-hoc format to store the + report operations. Each report operation is the concatention of + the following ("+/-" indicates the single character '+' or '-'; + <length> and <revnum> are written out as decimal strings): + + +/- '-' marks the end of the report + If previous is +: + <length>:<bytes> Length-counted path string + +/- '+' indicates the presence of link_path + If previous is +: + <length>:<bytes> Length-counted link_path string + +/- '+' indicates presence of revnum + If previous is +: + <revnum>: Revnum of set_path or link_path + +/- '+' indicates depth other than svn_depth_infinity + If previous is +: + <depth>: "X","E","F","M" => + svn_depth_{exclude,empty,files,immediates} + +/- '+' indicates start_empty field set + +/- '+' indicates presence of lock_token field. + If previous is +: + <length>:<bytes> Length-counted lock_token string + + Terminology: for brevity, this file frequently uses the prefixes + "s_" for source, "t_" for target, and "e_" for editor. Also, to + avoid overloading the word "target", we talk about the source + "anchor and operand", rather than the usual "anchor and target". */ + +/* Describes the state of a working copy subtree, as given by a + report. Because we keep a lookahead pathinfo, we need to allocate + each one of these things in a subpool of the report baton and free + it when done. */ +typedef struct path_info_t +{ + const char *path; /* path, munged to be anchor-relative */ + const char *link_path; /* NULL for set_path or delete_path */ + svn_revnum_t rev; /* SVN_INVALID_REVNUM for delete_path */ + svn_depth_t depth; /* Depth of this path, meaningless for files */ + svn_boolean_t start_empty; /* Meaningless for delete_path */ + const char *lock_token; /* NULL if no token */ + apr_pool_t *pool; /* Container pool */ +} path_info_t; + +/* Describes the standard revision properties that are relevant for + reports. Since a particular revision will often show up more than + once in the report, we cache these properties for the time of the + report generation. */ +typedef struct revision_info_t +{ + svn_revnum_t rev; /* revision number */ + svn_string_t* date; /* revision timestamp */ + svn_string_t* author; /* name of the revisions' author */ +} revision_info_t; + +/* A structure used by the routines within the `reporter' vtable, + driven by the client as it describes its working copy revisions. */ +typedef struct report_baton_t +{ + /* Parameters remembered from svn_repos_begin_report3 */ + svn_repos_t *repos; + const char *fs_base; /* fspath corresponding to wc anchor */ + const char *s_operand; /* anchor-relative wc target (may be empty) */ + svn_revnum_t t_rev; /* Revnum which the edit will bring the wc to */ + const char *t_path; /* FS path the edit will bring the wc to */ + svn_boolean_t text_deltas; /* Whether to report text deltas */ + apr_size_t zero_copy_limit; /* Max item size that will be sent using + the zero-copy code path. */ + + /* If the client requested a specific depth, record it here; if the + client did not, then this is svn_depth_unknown, and the depth of + information transmitted from server to client will be governed + strictly by the path-associated depths recorded in the report. */ + svn_depth_t requested_depth; + + svn_boolean_t ignore_ancestry; + svn_boolean_t send_copyfrom_args; + svn_boolean_t is_switch; + const svn_delta_editor_t *editor; + void *edit_baton; + svn_repos_authz_func_t authz_read_func; + void *authz_read_baton; + + /* The spill-buffer holding the report. */ + svn_spillbuf_reader_t *reader; + + /* For the actual editor drive, we'll need a lookahead path info + entry, a cache of FS roots, and a pool to store them. */ + path_info_t *lookahead; + svn_fs_root_t *t_root; + svn_fs_root_t *s_roots[NUM_CACHED_SOURCE_ROOTS]; + + /* Cache for revision properties. This is used to eliminate redundant + revprop fetching. */ + apr_hash_t *revision_infos; + + /* This will not change. So, fetch it once and reuse it. */ + svn_string_t *repos_uuid; + apr_pool_t *pool; +} report_baton_t; + +/* The type of a function that accepts changes to an object's property + list. OBJECT is the object whose properties are being changed. + NAME is the name of the property to change. VALUE is the new value + for the property, or zero if the property should be deleted. */ +typedef svn_error_t *proplist_change_fn_t(report_baton_t *b, void *object, + const char *name, + const svn_string_t *value, + apr_pool_t *pool); + +static svn_error_t *delta_dirs(report_baton_t *b, svn_revnum_t s_rev, + const char *s_path, const char *t_path, + void *dir_baton, const char *e_path, + svn_boolean_t start_empty, + svn_depth_t wc_depth, + svn_depth_t requested_depth, + apr_pool_t *pool); + +/* --- READING PREVIOUSLY STORED REPORT INFORMATION --- */ + +static svn_error_t * +read_number(apr_uint64_t *num, svn_spillbuf_reader_t *reader, apr_pool_t *pool) +{ + char c; + + *num = 0; + while (1) + { + SVN_ERR(svn_spillbuf__reader_getc(&c, reader, pool)); + if (c == ':') + break; + *num = *num * 10 + (c - '0'); + } + return SVN_NO_ERROR; +} + +static svn_error_t * +read_string(const char **str, svn_spillbuf_reader_t *reader, apr_pool_t *pool) +{ + apr_uint64_t len; + apr_size_t size; + apr_size_t amt; + char *buf; + + SVN_ERR(read_number(&len, reader, pool)); + + /* Len can never be less than zero. But could len be so large that + len + 1 wraps around and we end up passing 0 to apr_palloc(), + thus getting a pointer to no storage? Probably not (16 exabyte + string, anyone?) but let's be future-proof anyway. */ + if (len + 1 < len || len + 1 > APR_SIZE_MAX) + { + /* xgettext doesn't expand preprocessor definitions, so we must + pass translatable string to apr_psprintf() function to create + intermediate string with appropriate format specifier. */ + return svn_error_createf(SVN_ERR_REPOS_BAD_REVISION_REPORT, NULL, + apr_psprintf(pool, + _("Invalid length (%%%s) when " + "about to read a string"), + APR_UINT64_T_FMT), + len); + } + + size = (apr_size_t)len; + buf = apr_palloc(pool, size+1); + if (size > 0) + { + SVN_ERR(svn_spillbuf__reader_read(&amt, reader, buf, size, pool)); + SVN_ERR_ASSERT(amt == size); + } + buf[len] = 0; + *str = buf; + return SVN_NO_ERROR; +} + +static svn_error_t * +read_rev(svn_revnum_t *rev, svn_spillbuf_reader_t *reader, apr_pool_t *pool) +{ + char c; + apr_uint64_t num; + + SVN_ERR(svn_spillbuf__reader_getc(&c, reader, pool)); + if (c == '+') + { + SVN_ERR(read_number(&num, reader, pool)); + *rev = (svn_revnum_t) num; + } + else + *rev = SVN_INVALID_REVNUM; + return SVN_NO_ERROR; +} + +/* Read a single character to set *DEPTH (having already read '+') + from READER. PATH is the path to which the depth applies, and is + used for error reporting only. */ +static svn_error_t * +read_depth(svn_depth_t *depth, svn_spillbuf_reader_t *reader, const char *path, + apr_pool_t *pool) +{ + char c; + + SVN_ERR(svn_spillbuf__reader_getc(&c, reader, pool)); + switch (c) + { + case 'X': + *depth = svn_depth_exclude; + break; + case 'E': + *depth = svn_depth_empty; + break; + case 'F': + *depth = svn_depth_files; + break; + case 'M': + *depth = svn_depth_immediates; + break; + + /* Note that we do not tolerate explicit representation of + svn_depth_infinity here, because that's not how + write_path_info() writes it. */ + default: + return svn_error_createf(SVN_ERR_REPOS_BAD_REVISION_REPORT, NULL, + _("Invalid depth (%c) for path '%s'"), c, path); + } + + return SVN_NO_ERROR; +} + +/* Read a report operation *PI out of READER. Set *PI to NULL if we + have reached the end of the report. */ +static svn_error_t * +read_path_info(path_info_t **pi, + svn_spillbuf_reader_t *reader, + apr_pool_t *pool) +{ + char c; + + SVN_ERR(svn_spillbuf__reader_getc(&c, reader, pool)); + if (c == '-') + { + *pi = NULL; + return SVN_NO_ERROR; + } + + *pi = apr_palloc(pool, sizeof(**pi)); + SVN_ERR(read_string(&(*pi)->path, reader, pool)); + SVN_ERR(svn_spillbuf__reader_getc(&c, reader, pool)); + if (c == '+') + SVN_ERR(read_string(&(*pi)->link_path, reader, pool)); + else + (*pi)->link_path = NULL; + SVN_ERR(read_rev(&(*pi)->rev, reader, pool)); + SVN_ERR(svn_spillbuf__reader_getc(&c, reader, pool)); + if (c == '+') + SVN_ERR(read_depth(&((*pi)->depth), reader, (*pi)->path, pool)); + else + (*pi)->depth = svn_depth_infinity; + SVN_ERR(svn_spillbuf__reader_getc(&c, reader, pool)); + (*pi)->start_empty = (c == '+'); + SVN_ERR(svn_spillbuf__reader_getc(&c, reader, pool)); + if (c == '+') + SVN_ERR(read_string(&(*pi)->lock_token, reader, pool)); + else + (*pi)->lock_token = NULL; + (*pi)->pool = pool; + return SVN_NO_ERROR; +} + +/* Return true if PI's path is a child of PREFIX (which has length PLEN). */ +static svn_boolean_t +relevant(path_info_t *pi, const char *prefix, apr_size_t plen) +{ + return (pi && strncmp(pi->path, prefix, plen) == 0 && + (!*prefix || pi->path[plen] == '/')); +} + +/* Fetch the next pathinfo from B->reader for a descendant of + PREFIX. If the next pathinfo is for an immediate child of PREFIX, + set *ENTRY to the path component of the report information and + *INFO to the path information for that entry. If the next pathinfo + is for a grandchild or other more remote descendant of PREFIX, set + *ENTRY to the immediate child corresponding to that descendant and + set *INFO to NULL. If the next pathinfo is not for a descendant of + PREFIX, or if we reach the end of the report, set both *ENTRY and + *INFO to NULL. + + At all times, B->lookahead is presumed to be the next pathinfo not + yet returned as an immediate child, or NULL if we have reached the + end of the report. Because we use a lookahead element, we can't + rely on the usual nested pool lifetimes, so allocate each pathinfo + in a subpool of the report baton's pool. The caller should delete + (*INFO)->pool when it is done with the information. */ +static svn_error_t * +fetch_path_info(report_baton_t *b, const char **entry, path_info_t **info, + const char *prefix, apr_pool_t *pool) +{ + apr_size_t plen = strlen(prefix); + const char *relpath, *sep; + apr_pool_t *subpool; + + if (!relevant(b->lookahead, prefix, plen)) + { + /* No more entries relevant to prefix. */ + *entry = NULL; + *info = NULL; + } + else + { + /* Take a look at the prefix-relative part of the path. */ + relpath = b->lookahead->path + (*prefix ? plen + 1 : 0); + sep = strchr(relpath, '/'); + if (sep) + { + /* Return the immediate child part; do not advance. */ + *entry = apr_pstrmemdup(pool, relpath, sep - relpath); + *info = NULL; + } + else + { + /* This is an immediate child; return it and advance. */ + *entry = relpath; + *info = b->lookahead; + subpool = svn_pool_create(b->pool); + SVN_ERR(read_path_info(&b->lookahead, b->reader, subpool)); + } + } + return SVN_NO_ERROR; +} + +/* Skip all path info entries relevant to *PREFIX. Call this when the + editor drive skips a directory. */ +static svn_error_t * +skip_path_info(report_baton_t *b, const char *prefix) +{ + apr_size_t plen = strlen(prefix); + apr_pool_t *subpool; + + while (relevant(b->lookahead, prefix, plen)) + { + svn_pool_destroy(b->lookahead->pool); + subpool = svn_pool_create(b->pool); + SVN_ERR(read_path_info(&b->lookahead, b->reader, subpool)); + } + return SVN_NO_ERROR; +} + +/* Return true if there is at least one path info entry relevant to *PREFIX. */ +static svn_boolean_t +any_path_info(report_baton_t *b, const char *prefix) +{ + return relevant(b->lookahead, prefix, strlen(prefix)); +} + +/* --- DRIVING THE EDITOR ONCE THE REPORT IS FINISHED --- */ + +/* While driving the editor, the target root will remain constant, but + we may have to jump around between source roots depending on the + state of the working copy. If we were to open a root each time we + revisit a rev, we would get no benefit from node-id caching; on the + other hand, if we hold open all the roots we ever visit, we'll use + an unbounded amount of memory. As a compromise, we maintain a + fixed-size LRU cache of source roots. get_source_root retrieves a + root from the cache, using POOL to allocate the new root if + necessary. Be careful not to hold onto the root for too long, + particularly after recursing, since another call to get_source_root + can close it. */ +static svn_error_t * +get_source_root(report_baton_t *b, svn_fs_root_t **s_root, svn_revnum_t rev) +{ + int i; + svn_fs_root_t *root, *prev = NULL; + + /* Look for the desired root in the cache, sliding all the unmatched + entries backwards a slot to make room for the right one. */ + for (i = 0; i < NUM_CACHED_SOURCE_ROOTS; i++) + { + root = b->s_roots[i]; + b->s_roots[i] = prev; + if (root && svn_fs_revision_root_revision(root) == rev) + break; + prev = root; + } + + /* If we didn't find it, throw out the oldest root and open a new one. */ + if (i == NUM_CACHED_SOURCE_ROOTS) + { + if (prev) + svn_fs_close_root(prev); + SVN_ERR(svn_fs_revision_root(&root, b->repos->fs, rev, b->pool)); + } + + /* Assign the desired root to the first cache slot and hand it back. */ + b->s_roots[0] = root; + *s_root = root; + return SVN_NO_ERROR; +} + +/* Call the directory property-setting function of B->editor to set + the property NAME to VALUE on DIR_BATON. */ +static svn_error_t * +change_dir_prop(report_baton_t *b, void *dir_baton, const char *name, + const svn_string_t *value, apr_pool_t *pool) +{ + return svn_error_trace(b->editor->change_dir_prop(dir_baton, name, value, + pool)); +} + +/* Call the file property-setting function of B->editor to set the + property NAME to VALUE on FILE_BATON. */ +static svn_error_t * +change_file_prop(report_baton_t *b, void *file_baton, const char *name, + const svn_string_t *value, apr_pool_t *pool) +{ + return svn_error_trace(b->editor->change_file_prop(file_baton, name, value, + pool)); +} + +/* For the report B, return the relevant revprop data of revision REV in + REVISION_INFO. The revision info will be allocated in b->pool. + Temporaries get allocated on SCRATCH_POOL. */ +static svn_error_t * +get_revision_info(report_baton_t *b, + svn_revnum_t rev, + revision_info_t** revision_info, + apr_pool_t *scratch_pool) +{ + apr_hash_t *r_props; + svn_string_t *cdate, *author; + revision_info_t* info; + + /* Try to find the info in the report's cache */ + info = apr_hash_get(b->revision_infos, &rev, sizeof(rev)); + if (!info) + { + /* Info is not available, yet. + Get all revprops. */ + SVN_ERR(svn_fs_revision_proplist(&r_props, + b->repos->fs, + rev, + scratch_pool)); + + /* Extract the committed-date. */ + cdate = svn_hash_gets(r_props, SVN_PROP_REVISION_DATE); + + /* Extract the last-author. */ + author = svn_hash_gets(r_props, SVN_PROP_REVISION_AUTHOR); + + /* Create a result object */ + info = apr_palloc(b->pool, sizeof(*info)); + info->rev = rev; + info->date = cdate ? svn_string_dup(cdate, b->pool) : NULL; + info->author = author ? svn_string_dup(author, b->pool) : NULL; + + /* Cache it */ + apr_hash_set(b->revision_infos, &info->rev, sizeof(rev), info); + } + + *revision_info = info; + return SVN_NO_ERROR; +} + + +/* Generate the appropriate property editing calls to turn the + properties of S_REV/S_PATH into those of B->t_root/T_PATH. If + S_PATH is NULL, this is an add, so assume the target starts with no + properties. Pass OBJECT on to the editor function wrapper + CHANGE_FN. */ +static svn_error_t * +delta_proplists(report_baton_t *b, svn_revnum_t s_rev, const char *s_path, + const char *t_path, const char *lock_token, + proplist_change_fn_t *change_fn, + void *object, apr_pool_t *pool) +{ + svn_fs_root_t *s_root; + apr_hash_t *s_props = NULL, *t_props; + apr_array_header_t *prop_diffs; + int i; + svn_revnum_t crev; + revision_info_t *revision_info; + svn_boolean_t changed; + const svn_prop_t *pc; + svn_lock_t *lock; + apr_hash_index_t *hi; + + /* Fetch the created-rev and send entry props. */ + SVN_ERR(svn_fs_node_created_rev(&crev, b->t_root, t_path, pool)); + if (SVN_IS_VALID_REVNUM(crev)) + { + /* convert committed-rev to string */ + char buf[SVN_INT64_BUFFER_SIZE]; + svn_string_t cr_str; + cr_str.data = buf; + cr_str.len = svn__i64toa(buf, crev); + + /* Transmit the committed-rev. */ + SVN_ERR(change_fn(b, object, + SVN_PROP_ENTRY_COMMITTED_REV, &cr_str, pool)); + + SVN_ERR(get_revision_info(b, crev, &revision_info, pool)); + + /* Transmit the committed-date. */ + if (revision_info->date || s_path) + SVN_ERR(change_fn(b, object, SVN_PROP_ENTRY_COMMITTED_DATE, + revision_info->date, pool)); + + /* Transmit the last-author. */ + if (revision_info->author || s_path) + SVN_ERR(change_fn(b, object, SVN_PROP_ENTRY_LAST_AUTHOR, + revision_info->author, pool)); + + /* Transmit the UUID. */ + SVN_ERR(change_fn(b, object, SVN_PROP_ENTRY_UUID, + b->repos_uuid, pool)); + } + + /* Update lock properties. */ + if (lock_token) + { + SVN_ERR(svn_fs_get_lock(&lock, b->repos->fs, t_path, pool)); + + /* Delete a defunct lock. */ + if (! lock || strcmp(lock_token, lock->token) != 0) + SVN_ERR(change_fn(b, object, SVN_PROP_ENTRY_LOCK_TOKEN, + NULL, pool)); + } + + if (s_path) + { + SVN_ERR(get_source_root(b, &s_root, s_rev)); + + /* Is this deltification worth our time? */ + SVN_ERR(svn_fs_props_changed(&changed, b->t_root, t_path, s_root, + s_path, pool)); + if (! changed) + return SVN_NO_ERROR; + + /* If so, go ahead and get the source path's properties. */ + SVN_ERR(svn_fs_node_proplist(&s_props, s_root, s_path, pool)); + } + + /* Get the target path's properties */ + SVN_ERR(svn_fs_node_proplist(&t_props, b->t_root, t_path, pool)); + + if (s_props && apr_hash_count(s_props)) + { + /* Now transmit the differences. */ + SVN_ERR(svn_prop_diffs(&prop_diffs, t_props, s_props, pool)); + for (i = 0; i < prop_diffs->nelts; i++) + { + pc = &APR_ARRAY_IDX(prop_diffs, i, svn_prop_t); + SVN_ERR(change_fn(b, object, pc->name, pc->value, pool)); + } + } + else if (apr_hash_count(t_props)) + { + /* So source, i.e. all new. Transmit all target props. */ + for (hi = apr_hash_first(pool, t_props); hi; hi = apr_hash_next(hi)) + { + const void *key; + void *val; + + apr_hash_this(hi, &key, NULL, &val); + SVN_ERR(change_fn(b, object, key, val, pool)); + } + } + + return SVN_NO_ERROR; +} + +/* Baton type to be passed into send_zero_copy_delta. + */ +typedef struct zero_copy_baton_t +{ + /* don't process data larger than this limit */ + apr_size_t zero_copy_limit; + + /* window handler and baton to send the data to */ + svn_txdelta_window_handler_t dhandler; + void *dbaton; + + /* return value: will be set to TRUE, if the data was processed. */ + svn_boolean_t zero_copy_succeeded; +} zero_copy_baton_t; + +/* Implement svn_fs_process_contents_func_t. If LEN is smaller than the + * limit given in *BATON, send the CONTENTS as an delta windows to the + * handler given in BATON and set the ZERO_COPY_SUCCEEDED flag in that + * BATON. Otherwise, reset it to FALSE. + * Use POOL for temporary allocations. + */ +static svn_error_t * +send_zero_copy_delta(const unsigned char *contents, + apr_size_t len, + void *baton, + apr_pool_t *pool) +{ + zero_copy_baton_t *zero_copy_baton = baton; + + /* if the item is too large, the caller must revert to traditional + streaming code. */ + if (len > zero_copy_baton->zero_copy_limit) + { + zero_copy_baton->zero_copy_succeeded = FALSE; + return SVN_NO_ERROR; + } + + SVN_ERR(svn_txdelta_send_contents(contents, len, + zero_copy_baton->dhandler, + zero_copy_baton->dbaton, pool)); + + /* all fine now */ + zero_copy_baton->zero_copy_succeeded = TRUE; + return SVN_NO_ERROR; +} + + +/* Make the appropriate edits on FILE_BATON to change its contents and + properties from those in S_REV/S_PATH to those in B->t_root/T_PATH, + possibly using LOCK_TOKEN to determine if the client's lock on the file + is defunct. */ +static svn_error_t * +delta_files(report_baton_t *b, void *file_baton, svn_revnum_t s_rev, + const char *s_path, const char *t_path, const char *lock_token, + apr_pool_t *pool) +{ + svn_boolean_t changed; + svn_fs_root_t *s_root = NULL; + svn_txdelta_stream_t *dstream = NULL; + svn_checksum_t *s_checksum; + const char *s_hex_digest = NULL; + svn_txdelta_window_handler_t dhandler; + void *dbaton; + + /* Compare the files' property lists. */ + SVN_ERR(delta_proplists(b, s_rev, s_path, t_path, lock_token, + change_file_prop, file_baton, pool)); + + if (s_path) + { + SVN_ERR(get_source_root(b, &s_root, s_rev)); + + /* We're not interested in the theoretical difference between "has + contents which have not changed with respect to" and "has the same + actual contents as" when sending text-deltas. If we know the + delta is an empty one, we avoiding sending it in either case. */ + SVN_ERR(svn_repos__compare_files(&changed, b->t_root, t_path, + s_root, s_path, pool)); + + if (!changed) + return SVN_NO_ERROR; + + SVN_ERR(svn_fs_file_checksum(&s_checksum, svn_checksum_md5, s_root, + s_path, TRUE, pool)); + s_hex_digest = svn_checksum_to_cstring(s_checksum, pool); + } + + /* Send the delta stream if desired, or just a NULL window if not. */ + SVN_ERR(b->editor->apply_textdelta(file_baton, s_hex_digest, pool, + &dhandler, &dbaton)); + + if (dhandler != svn_delta_noop_window_handler) + { + if (b->text_deltas) + { + /* if we send deltas against empty streams, we may use our + zero-copy code. */ + if (b->zero_copy_limit > 0 && s_path == NULL) + { + zero_copy_baton_t baton; + svn_boolean_t called = FALSE; + + baton.zero_copy_limit = b->zero_copy_limit; + baton.dhandler = dhandler; + baton.dbaton = dbaton; + baton.zero_copy_succeeded = FALSE; + SVN_ERR(svn_fs_try_process_file_contents(&called, + b->t_root, t_path, + send_zero_copy_delta, + &baton, pool)); + + /* data has been available and small enough, + i.e. been processed? */ + if (called && baton.zero_copy_succeeded) + return SVN_NO_ERROR; + } + + SVN_ERR(svn_fs_get_file_delta_stream(&dstream, s_root, s_path, + b->t_root, t_path, pool)); + SVN_ERR(svn_txdelta_send_txstream(dstream, dhandler, dbaton, pool)); + } + else + SVN_ERR(dhandler(NULL, dbaton)); + } + + return SVN_NO_ERROR; +} + +/* Determine if the user is authorized to view B->t_root/PATH. */ +static svn_error_t * +check_auth(report_baton_t *b, svn_boolean_t *allowed, const char *path, + apr_pool_t *pool) +{ + if (b->authz_read_func) + return svn_error_trace(b->authz_read_func(allowed, b->t_root, path, + b->authz_read_baton, pool)); + *allowed = TRUE; + return SVN_NO_ERROR; +} + +/* Create a dirent in *ENTRY for the given ROOT and PATH. We use this to + replace the source or target dirent when a report pathinfo tells us to + change paths or revisions. */ +static svn_error_t * +fake_dirent(const svn_fs_dirent_t **entry, svn_fs_root_t *root, + const char *path, apr_pool_t *pool) +{ + svn_node_kind_t kind; + svn_fs_dirent_t *ent; + + SVN_ERR(svn_fs_check_path(&kind, root, path, pool)); + if (kind == svn_node_none) + *entry = NULL; + else + { + ent = apr_palloc(pool, sizeof(**entry)); + /* ### All callers should be updated to pass just one of these + formats */ + ent->name = (*path == '/') ? svn_fspath__basename(path, pool) + : svn_relpath_basename(path, pool); + SVN_ERR(svn_fs_node_id(&ent->id, root, path, pool)); + ent->kind = kind; + *entry = ent; + } + return SVN_NO_ERROR; +} + + +/* Given REQUESTED_DEPTH, WC_DEPTH and the current entry's KIND, + determine whether we need to send the whole entry, not just deltas. + Please refer to delta_dirs' docstring for an explanation of the + conditionals below. */ +static svn_boolean_t +is_depth_upgrade(svn_depth_t wc_depth, + svn_depth_t requested_depth, + svn_node_kind_t kind) +{ + if (requested_depth == svn_depth_unknown + || requested_depth <= wc_depth + || wc_depth == svn_depth_immediates) + return FALSE; + + if (kind == svn_node_file + && wc_depth == svn_depth_files) + return FALSE; + + if (kind == svn_node_dir + && wc_depth == svn_depth_empty + && requested_depth == svn_depth_files) + return FALSE; + + return TRUE; +} + + +/* Call the B->editor's add_file() function to create PATH as a child + of PARENT_BATON, returning a new baton in *NEW_FILE_BATON. + However, make an attempt to send 'copyfrom' arguments if they're + available, by examining the closest copy of the original file + O_PATH within B->t_root. If any copyfrom args are discovered, + return those in *COPYFROM_PATH and *COPYFROM_REV; otherwise leave + those return args untouched. */ +static svn_error_t * +add_file_smartly(report_baton_t *b, + const char *path, + void *parent_baton, + const char *o_path, + void **new_file_baton, + const char **copyfrom_path, + svn_revnum_t *copyfrom_rev, + apr_pool_t *pool) +{ + /* ### TODO: use a subpool to do this work, clear it at the end? */ + svn_fs_t *fs = svn_repos_fs(b->repos); + svn_fs_root_t *closest_copy_root = NULL; + const char *closest_copy_path = NULL; + + /* Pre-emptively assume no copyfrom args exist. */ + *copyfrom_path = NULL; + *copyfrom_rev = SVN_INVALID_REVNUM; + + if (b->send_copyfrom_args) + { + /* Find the destination of the nearest 'copy event' which may have + caused o_path@t_root to exist. svn_fs_closest_copy only returns paths + starting with '/', so make sure o_path always starts with a '/' + too. */ + if (*o_path != '/') + o_path = apr_pstrcat(pool, "/", o_path, (char *)NULL); + + SVN_ERR(svn_fs_closest_copy(&closest_copy_root, &closest_copy_path, + b->t_root, o_path, pool)); + if (closest_copy_root != NULL) + { + /* If the destination of the copy event is the same path as + o_path, then we've found something interesting that should + have 'copyfrom' history. */ + if (strcmp(closest_copy_path, o_path) == 0) + { + SVN_ERR(svn_fs_copied_from(copyfrom_rev, copyfrom_path, + closest_copy_root, closest_copy_path, + pool)); + if (b->authz_read_func) + { + svn_boolean_t allowed; + svn_fs_root_t *copyfrom_root; + SVN_ERR(svn_fs_revision_root(©from_root, fs, + *copyfrom_rev, pool)); + SVN_ERR(b->authz_read_func(&allowed, copyfrom_root, + *copyfrom_path, b->authz_read_baton, + pool)); + if (! allowed) + { + *copyfrom_path = NULL; + *copyfrom_rev = SVN_INVALID_REVNUM; + } + } + } + } + } + + return svn_error_trace(b->editor->add_file(path, parent_baton, + *copyfrom_path, *copyfrom_rev, + pool, new_file_baton)); +} + + +/* Emit a series of editing operations to transform a source entry to + a target entry. + + S_REV and S_PATH specify the source entry. S_ENTRY contains the + already-looked-up information about the node-revision existing at + that location. S_PATH and S_ENTRY may be NULL if the entry does + not exist in the source. S_PATH may be non-NULL and S_ENTRY may be + NULL if the caller expects INFO to modify the source to an existing + location. + + B->t_root and T_PATH specify the target entry. T_ENTRY contains + the already-looked-up information about the node-revision existing + at that location. T_PATH and T_ENTRY may be NULL if the entry does + not exist in the target. + + DIR_BATON and E_PATH contain the parameters which should be passed + to the editor calls--DIR_BATON for the parent directory baton and + E_PATH for the pathname. (E_PATH is the anchor-relative working + copy pathname, which may differ from the source and target + pathnames if the report contains a link_path.) + + INFO contains the report information for this working copy path, or + NULL if there is none. This function will internally modify the + source and target entries as appropriate based on the report + information. + + WC_DEPTH and REQUESTED_DEPTH are propagated to delta_dirs() if + necessary. Refer to delta_dirs' docstring to find out what + should happen for various combinations of WC_DEPTH/REQUESTED_DEPTH. */ +static svn_error_t * +update_entry(report_baton_t *b, svn_revnum_t s_rev, const char *s_path, + const svn_fs_dirent_t *s_entry, const char *t_path, + const svn_fs_dirent_t *t_entry, void *dir_baton, + const char *e_path, path_info_t *info, svn_depth_t wc_depth, + svn_depth_t requested_depth, apr_pool_t *pool) +{ + svn_fs_root_t *s_root; + svn_boolean_t allowed, related; + void *new_baton; + svn_checksum_t *checksum; + const char *hex_digest; + + /* For non-switch operations, follow link_path in the target. */ + if (info && info->link_path && !b->is_switch) + { + t_path = info->link_path; + SVN_ERR(fake_dirent(&t_entry, b->t_root, t_path, pool)); + } + + if (info && !SVN_IS_VALID_REVNUM(info->rev)) + { + /* Delete this entry in the source. */ + s_path = NULL; + s_entry = NULL; + } + else if (info && s_path) + { + /* Follow the rev and possibly path in this entry. */ + s_path = (info->link_path) ? info->link_path : s_path; + s_rev = info->rev; + SVN_ERR(get_source_root(b, &s_root, s_rev)); + SVN_ERR(fake_dirent(&s_entry, s_root, s_path, pool)); + } + + /* Don't let the report carry us somewhere nonexistent. */ + if (s_path && !s_entry) + return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, + _("Working copy path '%s' does not exist in " + "repository"), e_path); + + /* If the source and target both exist and are of the same kind, + then find out whether they're related. If they're exactly the + same, then we don't have to do anything (unless the report has + changes to the source). If we're ignoring ancestry, then any two + nodes of the same type are related enough for us. */ + related = FALSE; + if (s_entry && t_entry && s_entry->kind == t_entry->kind) + { + int distance = svn_fs_compare_ids(s_entry->id, t_entry->id); + if (distance == 0 && !any_path_info(b, e_path) + && (requested_depth <= wc_depth || t_entry->kind == svn_node_file)) + { + if (!info) + return SVN_NO_ERROR; + + if (!info->start_empty) + { + svn_lock_t *lock; + + if (!info->lock_token) + return SVN_NO_ERROR; + + SVN_ERR(svn_fs_get_lock(&lock, b->repos->fs, t_path, pool)); + if (lock && (strcmp(lock->token, info->lock_token) == 0)) + return SVN_NO_ERROR; + } + } + + related = (distance != -1 || b->ignore_ancestry); + } + + /* If there's a source and it's not related to the target, nuke it. */ + if (s_entry && !related) + { + svn_revnum_t deleted_rev; + + SVN_ERR(svn_repos_deleted_rev(svn_fs_root_fs(b->t_root), t_path, + s_rev, b->t_rev, &deleted_rev, + pool)); + + if (!SVN_IS_VALID_REVNUM(deleted_rev)) + { + /* Two possibilities: either the thing doesn't exist in S_REV; or + it wasn't deleted between S_REV and B->T_REV. In the first case, + I think we should leave DELETED_REV as SVN_INVALID_REVNUM, but + in the second, it should be set to B->T_REV-1 for the call to + delete_entry() below. */ + svn_node_kind_t kind; + + SVN_ERR(svn_fs_check_path(&kind, b->t_root, t_path, pool)); + if (kind != svn_node_none) + deleted_rev = b->t_rev - 1; + } + + SVN_ERR(b->editor->delete_entry(e_path, deleted_rev, dir_baton, + pool)); + s_path = NULL; + } + + /* If there's no target, we have nothing more to do. */ + if (!t_entry) + return svn_error_trace(skip_path_info(b, e_path)); + + /* Check if the user is authorized to find out about the target. */ + SVN_ERR(check_auth(b, &allowed, t_path, pool)); + if (!allowed) + { + if (t_entry->kind == svn_node_dir) + SVN_ERR(b->editor->absent_directory(e_path, dir_baton, pool)); + else + SVN_ERR(b->editor->absent_file(e_path, dir_baton, pool)); + return svn_error_trace(skip_path_info(b, e_path)); + } + + if (t_entry->kind == svn_node_dir) + { + if (related) + SVN_ERR(b->editor->open_directory(e_path, dir_baton, s_rev, pool, + &new_baton)); + else + SVN_ERR(b->editor->add_directory(e_path, dir_baton, NULL, + SVN_INVALID_REVNUM, pool, + &new_baton)); + + SVN_ERR(delta_dirs(b, s_rev, s_path, t_path, new_baton, e_path, + info ? info->start_empty : FALSE, + wc_depth, requested_depth, pool)); + return svn_error_trace(b->editor->close_directory(new_baton, pool)); + } + else + { + if (related) + { + SVN_ERR(b->editor->open_file(e_path, dir_baton, s_rev, pool, + &new_baton)); + SVN_ERR(delta_files(b, new_baton, s_rev, s_path, t_path, + info ? info->lock_token : NULL, pool)); + } + else + { + svn_revnum_t copyfrom_rev = SVN_INVALID_REVNUM; + const char *copyfrom_path = NULL; + SVN_ERR(add_file_smartly(b, e_path, dir_baton, t_path, &new_baton, + ©from_path, ©from_rev, pool)); + if (! copyfrom_path) + /* Send txdelta between empty file (s_path@s_rev doesn't + exist) and added file (t_path@t_root). */ + SVN_ERR(delta_files(b, new_baton, s_rev, s_path, t_path, + info ? info->lock_token : NULL, pool)); + else + /* Send txdelta between copied file (copyfrom_path@copyfrom_rev) + and added file (tpath@t_root). */ + SVN_ERR(delta_files(b, new_baton, copyfrom_rev, copyfrom_path, + t_path, info ? info->lock_token : NULL, pool)); + } + + SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, b->t_root, + t_path, TRUE, pool)); + hex_digest = svn_checksum_to_cstring(checksum, pool); + return svn_error_trace(b->editor->close_file(new_baton, hex_digest, + pool)); + } +} + +/* A helper macro for when we have to recurse into subdirectories. */ +#define DEPTH_BELOW_HERE(depth) ((depth) == svn_depth_immediates) ? \ + svn_depth_empty : (depth) + +/* Emit edits within directory DIR_BATON (with corresponding path + E_PATH) with the changes from the directory S_REV/S_PATH to the + directory B->t_rev/T_PATH. S_PATH may be NULL if the entry does + not exist in the source. + + WC_DEPTH is this path's depth as reported by set_path/link_path. + REQUESTED_DEPTH is derived from the depth set by + svn_repos_begin_report(). + + When iterating over this directory's entries, the following tables + describe what happens for all possible combinations + of WC_DEPTH/REQUESTED_DEPTH (rows represent WC_DEPTH, columns + represent REQUESTED_DEPTH): + + Legend: + X: ignore this entry (it's either below the requested depth, or + if the requested depth is svn_depth_unknown, below the working + copy depth) + o: handle this entry normally + U: handle the entry as if it were a newly added repository path + (the client is upgrading to a deeper wc and doesn't currently + have this entry, but it should be there after the upgrade, so we + need to send the whole thing, not just deltas) + + For files: + ______________________________________________________________ + | req. depth| unknown | empty | files | immediates | infinity | + |wc. depth | | | | | | + |___________|_________|_______|_______|____________|__________| + |empty | X | X | U | U | U | + |___________|_________|_______|_______|____________|__________| + |files | o | X | o | o | o | + |___________|_________|_______|_______|____________|__________| + |immediates | o | X | o | o | o | + |___________|_________|_______|_______|____________|__________| + |infinity | o | X | o | o | o | + |___________|_________|_______|_______|____________|__________| + + For directories: + ______________________________________________________________ + | req. depth| unknown | empty | files | immediates | infinity | + |wc. depth | | | | | | + |___________|_________|_______|_______|____________|__________| + |empty | X | X | X | U | U | + |___________|_________|_______|_______|____________|__________| + |files | X | X | X | U | U | + |___________|_________|_______|_______|____________|__________| + |immediates | o | X | X | o | o | + |___________|_________|_______|_______|____________|__________| + |infinity | o | X | X | o | o | + |___________|_________|_______|_______|____________|__________| + + These rules are enforced by the is_depth_upgrade() function and by + various other checks below. +*/ +static svn_error_t * +delta_dirs(report_baton_t *b, svn_revnum_t s_rev, const char *s_path, + const char *t_path, void *dir_baton, const char *e_path, + svn_boolean_t start_empty, svn_depth_t wc_depth, + svn_depth_t requested_depth, apr_pool_t *pool) +{ + svn_fs_root_t *s_root; + apr_hash_t *s_entries = NULL, *t_entries; + apr_hash_index_t *hi; + apr_pool_t *subpool; + const char *name, *s_fullpath, *t_fullpath, *e_fullpath; + path_info_t *info; + + /* Compare the property lists. If we're starting empty, pass a NULL + source path so that we add all the properties. + + When we support directory locks, we must pass the lock token here. */ + SVN_ERR(delta_proplists(b, s_rev, start_empty ? NULL : s_path, t_path, + NULL, change_dir_prop, dir_baton, pool)); + + if (requested_depth > svn_depth_empty + || requested_depth == svn_depth_unknown) + { + /* Get the list of entries in each of source and target. */ + if (s_path && !start_empty) + { + SVN_ERR(get_source_root(b, &s_root, s_rev)); + SVN_ERR(svn_fs_dir_entries(&s_entries, s_root, s_path, pool)); + } + SVN_ERR(svn_fs_dir_entries(&t_entries, b->t_root, t_path, pool)); + + /* Iterate over the report information for this directory. */ + subpool = svn_pool_create(pool); + + while (1) + { + const svn_fs_dirent_t *s_entry, *t_entry; + + svn_pool_clear(subpool); + SVN_ERR(fetch_path_info(b, &name, &info, e_path, subpool)); + if (!name) + break; + + /* Invalid revnum means we should delete, unless this is + just an excluded subpath. */ + if (info + && !SVN_IS_VALID_REVNUM(info->rev) + && info->depth != svn_depth_exclude) + { + /* We want to perform deletes before non-replacement adds, + for graceful handling of case-only renames on + case-insensitive client filesystems. So, if the report + item is a delete, remove the entry from the source hash, + but don't update the entry yet. */ + if (s_entries) + svn_hash_sets(s_entries, name, NULL); + continue; + } + + e_fullpath = svn_relpath_join(e_path, name, subpool); + t_fullpath = svn_fspath__join(t_path, name, subpool); + t_entry = svn_hash_gets(t_entries, name); + s_fullpath = s_path ? svn_fspath__join(s_path, name, subpool) : NULL; + s_entry = s_entries ? + svn_hash_gets(s_entries, name) : NULL; + + /* The only special cases here are + + - When requested_depth is files but the reported path is + a directory. This is technically a client error, but we + handle it anyway, by skipping the entry. + + - When the reported depth is svn_depth_exclude. + */ + if ((! info || info->depth != svn_depth_exclude) + && (requested_depth != svn_depth_files + || ((! t_entry || t_entry->kind != svn_node_dir) + && (! s_entry || s_entry->kind != svn_node_dir)))) + SVN_ERR(update_entry(b, s_rev, s_fullpath, s_entry, t_fullpath, + t_entry, dir_baton, e_fullpath, info, + info ? info->depth + : DEPTH_BELOW_HERE(wc_depth), + DEPTH_BELOW_HERE(requested_depth), subpool)); + + /* Don't revisit this name in the target or source entries. */ + svn_hash_sets(t_entries, name, NULL); + if (s_entries + /* Keep the entry for later process if it is reported as + excluded and got deleted in repos. */ + && (! info || info->depth != svn_depth_exclude || t_entry)) + svn_hash_sets(s_entries, name, NULL); + + /* pathinfo entries live in their own subpools due to lookahead, + so we need to clear each one out as we finish with it. */ + if (info) + svn_pool_destroy(info->pool); + } + + /* Remove any deleted entries. Do this before processing the + target, for graceful handling of case-only renames. */ + if (s_entries) + { + for (hi = apr_hash_first(pool, s_entries); + hi; + hi = apr_hash_next(hi)) + { + const svn_fs_dirent_t *s_entry; + + svn_pool_clear(subpool); + s_entry = svn__apr_hash_index_val(hi); + + if (svn_hash_gets(t_entries, s_entry->name) == NULL) + { + svn_revnum_t deleted_rev; + + if (s_entry->kind == svn_node_file + && wc_depth < svn_depth_files) + continue; + + if (s_entry->kind == svn_node_dir + && (wc_depth < svn_depth_immediates + || requested_depth == svn_depth_files)) + continue; + + /* There is no corresponding target entry, so delete. */ + e_fullpath = svn_relpath_join(e_path, s_entry->name, subpool); + SVN_ERR(svn_repos_deleted_rev(svn_fs_root_fs(b->t_root), + svn_fspath__join(t_path, + s_entry->name, + subpool), + s_rev, b->t_rev, + &deleted_rev, subpool)); + + SVN_ERR(b->editor->delete_entry(e_fullpath, + deleted_rev, + dir_baton, subpool)); + } + } + } + + /* Loop over the dirents in the target. */ + for (hi = apr_hash_first(pool, t_entries); hi; hi = apr_hash_next(hi)) + { + const svn_fs_dirent_t *s_entry, *t_entry; + + svn_pool_clear(subpool); + t_entry = svn__apr_hash_index_val(hi); + + if (is_depth_upgrade(wc_depth, requested_depth, t_entry->kind)) + { + /* We're making the working copy deeper, pretend the source + doesn't exist. */ + s_entry = NULL; + s_fullpath = NULL; + } + else + { + if (t_entry->kind == svn_node_file + && requested_depth == svn_depth_unknown + && wc_depth < svn_depth_files) + continue; + + if (t_entry->kind == svn_node_dir + && (wc_depth < svn_depth_immediates + || requested_depth == svn_depth_files)) + continue; + + /* Look for an entry with the same name + in the source dirents. */ + s_entry = s_entries ? + svn_hash_gets(s_entries, t_entry->name) + : NULL; + s_fullpath = s_entry ? + svn_fspath__join(s_path, t_entry->name, subpool) : NULL; + } + + /* Compose the report, editor, and target paths for this entry. */ + e_fullpath = svn_relpath_join(e_path, t_entry->name, subpool); + t_fullpath = svn_fspath__join(t_path, t_entry->name, subpool); + + SVN_ERR(update_entry(b, s_rev, s_fullpath, s_entry, t_fullpath, + t_entry, dir_baton, e_fullpath, NULL, + DEPTH_BELOW_HERE(wc_depth), + DEPTH_BELOW_HERE(requested_depth), + subpool)); + } + + + /* Destroy iteration subpool. */ + svn_pool_destroy(subpool); + } + return SVN_NO_ERROR; +} + +static svn_error_t * +drive(report_baton_t *b, svn_revnum_t s_rev, path_info_t *info, + apr_pool_t *pool) +{ + const char *t_anchor, *s_fullpath; + svn_boolean_t allowed, info_is_set_path; + svn_fs_root_t *s_root; + const svn_fs_dirent_t *s_entry, *t_entry; + void *root_baton; + + /* Compute the target path corresponding to the working copy anchor, + and check its authorization. */ + t_anchor = *b->s_operand ? svn_fspath__dirname(b->t_path, pool) : b->t_path; + SVN_ERR(check_auth(b, &allowed, t_anchor, pool)); + if (!allowed) + return svn_error_create + (SVN_ERR_AUTHZ_ROOT_UNREADABLE, NULL, + _("Not authorized to open root of edit operation")); + + /* Collect information about the source and target nodes. */ + s_fullpath = svn_fspath__join(b->fs_base, b->s_operand, pool); + SVN_ERR(get_source_root(b, &s_root, s_rev)); + SVN_ERR(fake_dirent(&s_entry, s_root, s_fullpath, pool)); + SVN_ERR(fake_dirent(&t_entry, b->t_root, b->t_path, pool)); + + /* If the operand is a locally added file or directory, it won't + exist in the source, so accept that. */ + info_is_set_path = (SVN_IS_VALID_REVNUM(info->rev) && !info->link_path); + if (info_is_set_path && !s_entry) + s_fullpath = NULL; + + /* Check if the target path exists first. */ + if (!*b->s_operand && !(t_entry)) + return svn_error_createf(SVN_ERR_FS_PATH_SYNTAX, NULL, + _("Target path '%s' does not exist"), + b->t_path); + + /* If the anchor is the operand, the source and target must be dirs. + Check this before opening the root to avoid modifying the wc. */ + else if (!*b->s_operand && (!s_entry || s_entry->kind != svn_node_dir + || t_entry->kind != svn_node_dir)) + return svn_error_create(SVN_ERR_FS_PATH_SYNTAX, NULL, + _("Cannot replace a directory from within")); + + SVN_ERR(b->editor->set_target_revision(b->edit_baton, b->t_rev, pool)); + SVN_ERR(b->editor->open_root(b->edit_baton, s_rev, pool, &root_baton)); + + /* If the anchor is the operand, diff the two directories; otherwise + update the operand within the anchor directory. */ + if (!*b->s_operand) + SVN_ERR(delta_dirs(b, s_rev, s_fullpath, b->t_path, root_baton, + "", info->start_empty, info->depth, b->requested_depth, + pool)); + else + SVN_ERR(update_entry(b, s_rev, s_fullpath, s_entry, b->t_path, + t_entry, root_baton, b->s_operand, info, + info->depth, b->requested_depth, pool)); + + return svn_error_trace(b->editor->close_directory(root_baton, pool)); +} + +/* Initialize the baton fields for editor-driving, and drive the editor. */ +static svn_error_t * +finish_report(report_baton_t *b, apr_pool_t *pool) +{ + path_info_t *info; + apr_pool_t *subpool; + svn_revnum_t s_rev; + int i; + + /* Save our pool to manage the lookahead and fs_root cache with. */ + b->pool = pool; + + /* Add the end marker. */ + SVN_ERR(svn_spillbuf__reader_write(b->reader, "-", 1, pool)); + + /* Read the first pathinfo from the report and verify that it is a top-level + set_path entry. */ + SVN_ERR(read_path_info(&info, b->reader, pool)); + if (!info || strcmp(info->path, b->s_operand) != 0 + || info->link_path || !SVN_IS_VALID_REVNUM(info->rev)) + return svn_error_create(SVN_ERR_REPOS_BAD_REVISION_REPORT, NULL, + _("Invalid report for top level of working copy")); + s_rev = info->rev; + + /* Initialize the lookahead pathinfo. */ + subpool = svn_pool_create(pool); + SVN_ERR(read_path_info(&b->lookahead, b->reader, subpool)); + + if (b->lookahead && strcmp(b->lookahead->path, b->s_operand) == 0) + { + /* If the operand of the wc operation is switched or deleted, + then info above is just a place-holder, and the only thing we + have to do is pass the revision it contains to open_root. + The next pathinfo actually describes the target. */ + if (!*b->s_operand) + return svn_error_create(SVN_ERR_REPOS_BAD_REVISION_REPORT, NULL, + _("Two top-level reports with no target")); + /* If the client issued a set-path followed by a delete-path, we need + to respect the depth set by the initial set-path. */ + if (! SVN_IS_VALID_REVNUM(b->lookahead->rev)) + { + b->lookahead->depth = info->depth; + } + info = b->lookahead; + SVN_ERR(read_path_info(&b->lookahead, b->reader, subpool)); + } + + /* Open the target root and initialize the source root cache. */ + SVN_ERR(svn_fs_revision_root(&b->t_root, b->repos->fs, b->t_rev, pool)); + for (i = 0; i < NUM_CACHED_SOURCE_ROOTS; i++) + b->s_roots[i] = NULL; + + { + svn_error_t *err = svn_error_trace(drive(b, s_rev, info, pool)); + + if (err == SVN_NO_ERROR) + return svn_error_trace(b->editor->close_edit(b->edit_baton, pool)); + + return svn_error_trace( + svn_error_compose_create(err, + b->editor->abort_edit(b->edit_baton, + pool))); + } +} + +/* --- COLLECTING THE REPORT INFORMATION --- */ + +/* Record a report operation into the spill buffer. Return an error + if DEPTH is svn_depth_unknown. */ +static svn_error_t * +write_path_info(report_baton_t *b, const char *path, const char *lpath, + svn_revnum_t rev, svn_depth_t depth, + svn_boolean_t start_empty, + const char *lock_token, apr_pool_t *pool) +{ + const char *lrep, *rrep, *drep, *ltrep, *rep; + + /* Munge the path to be anchor-relative, so that we can use edit paths + as report paths. */ + path = svn_relpath_join(b->s_operand, path, pool); + + lrep = lpath ? apr_psprintf(pool, "+%" APR_SIZE_T_FMT ":%s", + strlen(lpath), lpath) : "-"; + rrep = (SVN_IS_VALID_REVNUM(rev)) ? + apr_psprintf(pool, "+%ld:", rev) : "-"; + + if (depth == svn_depth_exclude) + drep = "+X"; + else if (depth == svn_depth_empty) + drep = "+E"; + else if (depth == svn_depth_files) + drep = "+F"; + else if (depth == svn_depth_immediates) + drep = "+M"; + else if (depth == svn_depth_infinity) + drep = "-"; + else + return svn_error_createf(SVN_ERR_REPOS_BAD_ARGS, NULL, + _("Unsupported report depth '%s'"), + svn_depth_to_word(depth)); + + ltrep = lock_token ? apr_psprintf(pool, "+%" APR_SIZE_T_FMT ":%s", + strlen(lock_token), lock_token) : "-"; + rep = apr_psprintf(pool, "+%" APR_SIZE_T_FMT ":%s%s%s%s%c%s", + strlen(path), path, lrep, rrep, drep, + start_empty ? '+' : '-', ltrep); + return svn_error_trace( + svn_spillbuf__reader_write(b->reader, rep, strlen(rep), pool)); +} + +svn_error_t * +svn_repos_set_path3(void *baton, const char *path, svn_revnum_t rev, + svn_depth_t depth, svn_boolean_t start_empty, + const char *lock_token, apr_pool_t *pool) +{ + return svn_error_trace( + write_path_info(baton, path, NULL, rev, depth, start_empty, + lock_token, pool)); +} + +svn_error_t * +svn_repos_link_path3(void *baton, const char *path, const char *link_path, + svn_revnum_t rev, svn_depth_t depth, + svn_boolean_t start_empty, + const char *lock_token, apr_pool_t *pool) +{ + if (depth == svn_depth_exclude) + return svn_error_create(SVN_ERR_REPOS_BAD_ARGS, NULL, + _("Depth 'exclude' not supported for link")); + + return svn_error_trace( + write_path_info(baton, path, link_path, rev, depth, + start_empty, lock_token, pool)); +} + +svn_error_t * +svn_repos_delete_path(void *baton, const char *path, apr_pool_t *pool) +{ + /* We pass svn_depth_infinity because deletion of a path always + deletes everything underneath it. */ + return svn_error_trace( + write_path_info(baton, path, NULL, SVN_INVALID_REVNUM, + svn_depth_infinity, FALSE, NULL, pool)); +} + +svn_error_t * +svn_repos_finish_report(void *baton, apr_pool_t *pool) +{ + report_baton_t *b = baton; + + return svn_error_trace(finish_report(b, pool)); +} + +svn_error_t * +svn_repos_abort_report(void *baton, apr_pool_t *pool) +{ + return SVN_NO_ERROR; +} + +/* --- BEGINNING THE REPORT --- */ + + +svn_error_t * +svn_repos_begin_report3(void **report_baton, + svn_revnum_t revnum, + svn_repos_t *repos, + const char *fs_base, + const char *s_operand, + const char *switch_path, + svn_boolean_t text_deltas, + svn_depth_t depth, + svn_boolean_t ignore_ancestry, + svn_boolean_t send_copyfrom_args, + const svn_delta_editor_t *editor, + void *edit_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_size_t zero_copy_limit, + apr_pool_t *pool) +{ + report_baton_t *b; + const char *uuid; + + if (depth == svn_depth_exclude) + return svn_error_create(SVN_ERR_REPOS_BAD_ARGS, NULL, + _("Request depth 'exclude' not supported")); + + SVN_ERR(svn_fs_get_uuid(repos->fs, &uuid, pool)); + + /* Build a reporter baton. Copy strings in case the caller doesn't + keep track of them. */ + b = apr_palloc(pool, sizeof(*b)); + b->repos = repos; + b->fs_base = svn_fspath__canonicalize(fs_base, pool); + b->s_operand = apr_pstrdup(pool, s_operand); + b->t_rev = revnum; + b->t_path = switch_path ? svn_fspath__canonicalize(switch_path, pool) + : svn_fspath__join(b->fs_base, s_operand, pool); + b->text_deltas = text_deltas; + b->zero_copy_limit = zero_copy_limit; + b->requested_depth = depth; + b->ignore_ancestry = ignore_ancestry; + b->send_copyfrom_args = send_copyfrom_args; + b->is_switch = (switch_path != NULL); + b->editor = editor; + b->edit_baton = edit_baton; + b->authz_read_func = authz_read_func; + b->authz_read_baton = authz_read_baton; + b->revision_infos = apr_hash_make(pool); + b->pool = pool; + b->reader = svn_spillbuf__reader_create(1000 /* blocksize */, + 1000000 /* maxsize */, + pool); + b->repos_uuid = svn_string_create(uuid, pool); + + /* Hand reporter back to client. */ + *report_baton = b; + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_repos/repos.c b/subversion/libsvn_repos/repos.c new file mode 100644 index 0000000..9f10c06 --- /dev/null +++ b/subversion/libsvn_repos/repos.c @@ -0,0 +1,2132 @@ +/* repos.c : repository creation; shared and exclusive repository locking + * + * ==================================================================== + * 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_file_io.h> + +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_utf.h" +#include "svn_time.h" +#include "svn_fs.h" +#include "svn_ra.h" /* for SVN_RA_CAPABILITY_* */ +#include "svn_repos.h" +#include "svn_hash.h" +#include "svn_version.h" +#include "svn_config.h" + +#include "private/svn_repos_private.h" +#include "private/svn_subr_private.h" +#include "svn_private_config.h" /* for SVN_TEMPLATE_ROOT_DIR */ + +#include "repos.h" + +/* Used to terminate lines in large multi-line string literals. */ +#define NL APR_EOL_STR + + +/* Path accessor functions. */ + + +const char * +svn_repos_path(svn_repos_t *repos, apr_pool_t *pool) +{ + return apr_pstrdup(pool, repos->path); +} + + +const char * +svn_repos_db_env(svn_repos_t *repos, apr_pool_t *pool) +{ + return apr_pstrdup(pool, repos->db_path); +} + + +const char * +svn_repos_conf_dir(svn_repos_t *repos, apr_pool_t *pool) +{ + return apr_pstrdup(pool, repos->conf_path); +} + + +const char * +svn_repos_svnserve_conf(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->conf_path, SVN_REPOS__CONF_SVNSERVE_CONF, pool); +} + + +const char * +svn_repos_lock_dir(svn_repos_t *repos, apr_pool_t *pool) +{ + return apr_pstrdup(pool, repos->lock_path); +} + + +const char * +svn_repos_db_lockfile(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->lock_path, SVN_REPOS__DB_LOCKFILE, pool); +} + + +const char * +svn_repos_db_logs_lockfile(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->lock_path, SVN_REPOS__DB_LOGS_LOCKFILE, pool); +} + +const char * +svn_repos_hook_dir(svn_repos_t *repos, apr_pool_t *pool) +{ + return apr_pstrdup(pool, repos->hook_path); +} + + +const char * +svn_repos_start_commit_hook(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->hook_path, SVN_REPOS__HOOK_START_COMMIT, pool); +} + + +const char * +svn_repos_pre_commit_hook(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->hook_path, SVN_REPOS__HOOK_PRE_COMMIT, pool); +} + + +const char * +svn_repos_pre_lock_hook(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->hook_path, SVN_REPOS__HOOK_PRE_LOCK, pool); +} + + +const char * +svn_repos_pre_unlock_hook(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->hook_path, SVN_REPOS__HOOK_PRE_UNLOCK, pool); +} + +const char * +svn_repos_post_lock_hook(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->hook_path, SVN_REPOS__HOOK_POST_LOCK, pool); +} + + +const char * +svn_repos_post_unlock_hook(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->hook_path, SVN_REPOS__HOOK_POST_UNLOCK, pool); +} + + +const char * +svn_repos_post_commit_hook(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->hook_path, SVN_REPOS__HOOK_POST_COMMIT, pool); +} + + +const char * +svn_repos_pre_revprop_change_hook(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->hook_path, SVN_REPOS__HOOK_PRE_REVPROP_CHANGE, + pool); +} + + +const char * +svn_repos_post_revprop_change_hook(svn_repos_t *repos, apr_pool_t *pool) +{ + return svn_dirent_join(repos->hook_path, SVN_REPOS__HOOK_POST_REVPROP_CHANGE, + pool); +} + +static svn_error_t * +create_repos_dir(const char *path, apr_pool_t *pool) +{ + svn_error_t *err; + + err = svn_io_dir_make(path, APR_OS_DEFAULT, pool); + if (err && (APR_STATUS_IS_EEXIST(err->apr_err))) + { + svn_boolean_t is_empty; + + svn_error_clear(err); + + SVN_ERR(svn_io_dir_empty(&is_empty, path, pool)); + + if (is_empty) + err = NULL; + else + err = svn_error_createf(SVN_ERR_DIR_NOT_EMPTY, 0, + _("'%s' exists and is non-empty"), + svn_dirent_local_style(path, pool)); + } + + return svn_error_trace(err); +} + +static const char * bdb_lock_file_contents = + "DB lock file, representing locks on the versioned filesystem." NL + "" NL + "All accessors -- both readers and writers -- of the repository's" NL + "Berkeley DB environment take out shared locks on this file, and" NL + "each accessor removes its lock when done. If and when the DB" NL + "recovery procedure is run, the recovery code takes out an" NL + "exclusive lock on this file, so we can be sure no one else is" NL + "using the DB during the recovery." NL + "" NL + "You should never have to edit or remove this file." NL; + +static const char * bdb_logs_lock_file_contents = + "DB logs lock file, representing locks on the versioned filesystem logs." NL + "" NL + "All log manipulators of the repository's Berkeley DB environment" NL + "take out exclusive locks on this file to ensure that only one" NL + "accessor manipulates the logs at a time." NL + "" NL + "You should never have to edit or remove this file." NL; + +static const char * pre12_compat_unneeded_file_contents = + "This file is not used by Subversion 1.3.x or later." NL + "However, its existence is required for compatibility with" NL + "Subversion 1.2.x or earlier." NL; + +/* Create the DB logs lockfile. */ +static svn_error_t * +create_db_logs_lock(svn_repos_t *repos, apr_pool_t *pool) { + const char *contents; + const char *lockfile_path; + + lockfile_path = svn_repos_db_logs_lockfile(repos, pool); + if (strcmp(repos->fs_type, SVN_FS_TYPE_BDB) == 0) + contents = bdb_logs_lock_file_contents; + else + contents = pre12_compat_unneeded_file_contents; + + SVN_ERR_W(svn_io_file_create(lockfile_path, contents, pool), + _("Creating db logs lock file")); + + return SVN_NO_ERROR; +} + +/* Create the DB lockfile. */ +static svn_error_t * +create_db_lock(svn_repos_t *repos, apr_pool_t *pool) { + const char *contents; + const char *lockfile_path; + + lockfile_path = svn_repos_db_lockfile(repos, pool); + if (strcmp(repos->fs_type, SVN_FS_TYPE_BDB) == 0) + contents = bdb_lock_file_contents; + else + contents = pre12_compat_unneeded_file_contents; + + SVN_ERR_W(svn_io_file_create(lockfile_path, contents, pool), + _("Creating db lock file")); + + return SVN_NO_ERROR; +} + +static svn_error_t * +create_locks(svn_repos_t *repos, apr_pool_t *pool) +{ + /* Create the locks directory. */ + SVN_ERR_W(create_repos_dir(repos->lock_path, pool), + _("Creating lock dir")); + + SVN_ERR(create_db_lock(repos, pool)); + return create_db_logs_lock(repos, pool); +} + + +#define HOOKS_ENVIRONMENT_TEXT \ + "# The hook program typically does not inherit the environment of" NL \ + "# its parent process. For example, a common problem is for the" NL \ + "# PATH environment variable to not be set to its usual value, so" NL \ + "# that subprograms fail to launch unless invoked via absolute path." NL \ + "# If you're having unexpected problems with a hook program, the" NL \ + "# culprit may be unusual (or missing) environment variables." NL + +#define PREWRITTEN_HOOKS_TEXT \ + "# For more examples and pre-written hooks, see those in" NL \ + "# the Subversion repository at" NL \ + "# http://svn.apache.org/repos/asf/subversion/trunk/tools/hook-scripts/ and" NL \ + "# http://svn.apache.org/repos/asf/subversion/trunk/contrib/hook-scripts/" NL + + +static svn_error_t * +create_hooks(svn_repos_t *repos, apr_pool_t *pool) +{ + const char *this_path, *contents; + + /* Create the hook directory. */ + SVN_ERR_W(create_repos_dir(repos->hook_path, pool), + _("Creating hook directory")); + + /*** Write a default template for each standard hook file. */ + + /* Start-commit hook. */ + { + this_path = apr_psprintf(pool, "%s%s", + svn_repos_start_commit_hook(repos, pool), + SVN_REPOS__HOOK_DESC_EXT); + +#define SCRIPT_NAME SVN_REPOS__HOOK_START_COMMIT + + contents = +"#!/bin/sh" NL +"" NL +"# START-COMMIT HOOK" NL +"#" NL +"# The start-commit hook is invoked immediately after a Subversion txn is" NL +"# created and populated with initial revprops in the process of doing a" NL +"# commit. Subversion runs this hook by invoking a program (script, " NL +"# executable, binary, etc.) named '"SCRIPT_NAME"' (for which this file" NL +"# is a template) with the following ordered arguments:" NL +"#" NL +"# [1] REPOS-PATH (the path to this repository)" NL +"# [2] USER (the authenticated user attempting to commit)" NL +"# [3] CAPABILITIES (a colon-separated list of capabilities reported" NL +"# by the client; see note below)" NL +"# [4] TXN-NAME (the name of the commit txn just created)" NL +"#" NL +"# Note: The CAPABILITIES parameter is new in Subversion 1.5, and 1.5" NL +"# clients will typically report at least the \"" \ + SVN_RA_CAPABILITY_MERGEINFO "\" capability." NL +"# If there are other capabilities, then the list is colon-separated," NL +"# e.g.: \"" SVN_RA_CAPABILITY_MERGEINFO ":some-other-capability\" " \ + "(the order is undefined)." NL +"#" NL +"# Note: The TXN-NAME parameter is new in Subversion 1.8. Prior to version" NL +"# 1.8, the start-commit hook was invoked before the commit txn was even" NL +"# created, so the ability to inspect the commit txn and its metadata from" NL +"# within the start-commit hook was not possible." NL +"# " NL +"# The list is self-reported by the client. Therefore, you should not" NL +"# make security assumptions based on the capabilities list, nor should" NL +"# you assume that clients reliably report every capability they have." NL +"#" NL +"# The working directory for this hook program's invocation is undefined," NL +"# so the program should set one explicitly if it cares." NL +"#" NL +"# If the hook program exits with success, the commit continues; but" NL +"# if it exits with failure (non-zero), the commit is stopped before" NL +"# a Subversion txn is created, and STDERR is returned to the client." NL +"#" NL +"# On a Unix system, the normal procedure is to have '"SCRIPT_NAME"'" NL +"# invoke other programs to do the real work, though it may do the" NL +"# work itself too." NL +"#" NL +"# Note that '"SCRIPT_NAME"' must be executable by the user(s) who will" NL +"# invoke it (typically the user httpd runs as), and that user must" NL +"# have filesystem-level permission to access the repository." NL +"#" NL +"# On a Windows system, you should name the hook program" NL +"# '"SCRIPT_NAME".bat' or '"SCRIPT_NAME".exe'," NL +"# but the basic idea is the same." NL +"# " NL +HOOKS_ENVIRONMENT_TEXT +"# " NL +"# Here is an example hook script, for a Unix /bin/sh interpreter." NL +PREWRITTEN_HOOKS_TEXT +"" NL +"" NL +"REPOS=\"$1\"" NL +"USER=\"$2\"" NL +"" NL +"commit-allower.pl --repository \"$REPOS\" --user \"$USER\" || exit 1" NL +"special-auth-check.py --user \"$USER\" --auth-level 3 || exit 1" NL +"" NL +"# All checks passed, so allow the commit." NL +"exit 0" NL; + +#undef SCRIPT_NAME + + SVN_ERR_W(svn_io_file_create(this_path, contents, pool), + _("Creating start-commit hook")); + + SVN_ERR(svn_io_set_file_executable(this_path, TRUE, FALSE, pool)); + } /* end start-commit hook */ + + /* Pre-commit hook. */ + { + this_path = apr_psprintf(pool, "%s%s", + svn_repos_pre_commit_hook(repos, pool), + SVN_REPOS__HOOK_DESC_EXT); + +#define SCRIPT_NAME SVN_REPOS__HOOK_PRE_COMMIT + + contents = +"#!/bin/sh" NL +"" NL +"# PRE-COMMIT HOOK" NL +"#" NL +"# The pre-commit hook is invoked before a Subversion txn is" NL +"# committed. Subversion runs this hook by invoking a program" NL +"# (script, executable, binary, etc.) named '"SCRIPT_NAME"' (for which" NL +"# this file is a template), with the following ordered arguments:" NL +"#" NL +"# [1] REPOS-PATH (the path to this repository)" NL +"# [2] TXN-NAME (the name of the txn about to be committed)" NL +"#" NL +"# [STDIN] LOCK-TOKENS ** the lock tokens are passed via STDIN." NL +"#" NL +"# If STDIN contains the line \"LOCK-TOKENS:\\n\" (the \"\\n\" denotes a" NL +"# single newline), the lines following it are the lock tokens for" NL +"# this commit. The end of the list is marked by a line containing" NL +"# only a newline character." NL +"#" NL +"# Each lock token line consists of a URI-escaped path, followed" NL +"# by the separator character '|', followed by the lock token string," NL +"# followed by a newline." NL +"#" NL +"# The default working directory for the invocation is undefined, so" NL +"# the program should set one explicitly if it cares." NL +"#" NL +"# If the hook program exits with success, the txn is committed; but" NL +"# if it exits with failure (non-zero), the txn is aborted, no commit" NL +"# takes place, and STDERR is returned to the client. The hook" NL +"# program can use the 'svnlook' utility to help it examine the txn." NL +"#" NL +"# On a Unix system, the normal procedure is to have '"SCRIPT_NAME"'" NL +"# invoke other programs to do the real work, though it may do the" NL +"# work itself too." NL +"#" NL +"# *** NOTE: THE HOOK PROGRAM MUST NOT MODIFY THE TXN, EXCEPT ***" NL +"# *** FOR REVISION PROPERTIES (like svn:log or svn:author). ***" NL +"#" NL +"# This is why we recommend using the read-only 'svnlook' utility." NL +"# In the future, Subversion may enforce the rule that pre-commit" NL +"# hooks should not modify the versioned data in txns, or else come" NL +"# up with a mechanism to make it safe to do so (by informing the" NL +"# committing client of the changes). However, right now neither" NL +"# mechanism is implemented, so hook writers just have to be careful." NL +"#" NL +"# Note that '"SCRIPT_NAME"' must be executable by the user(s) who will" NL +"# invoke it (typically the user httpd runs as), and that user must" NL +"# have filesystem-level permission to access the repository." NL +"#" NL +"# On a Windows system, you should name the hook program" NL +"# '"SCRIPT_NAME".bat' or '"SCRIPT_NAME".exe'," NL +"# but the basic idea is the same." NL +"#" NL +HOOKS_ENVIRONMENT_TEXT +"# " NL +"# Here is an example hook script, for a Unix /bin/sh interpreter." NL +PREWRITTEN_HOOKS_TEXT +"" NL +"" NL +"REPOS=\"$1\"" NL +"TXN=\"$2\"" NL +"" NL +"# Make sure that the log message contains some text." NL +"SVNLOOK=" SVN_BINDIR "/svnlook" NL +"$SVNLOOK log -t \"$TXN\" \"$REPOS\" | \\" NL +" grep \"[a-zA-Z0-9]\" > /dev/null || exit 1" NL +"" NL +"# Check that the author of this commit has the rights to perform" NL +"# the commit on the files and directories being modified." NL +"commit-access-control.pl \"$REPOS\" \"$TXN\" commit-access-control.cfg || exit 1" + NL +"" NL +"# All checks passed, so allow the commit." NL +"exit 0" NL; + +#undef SCRIPT_NAME + + SVN_ERR_W(svn_io_file_create(this_path, contents, pool), + _("Creating pre-commit hook")); + + SVN_ERR(svn_io_set_file_executable(this_path, TRUE, FALSE, pool)); + } /* end pre-commit hook */ + + + /* Pre-revprop-change hook. */ + { + this_path = apr_psprintf(pool, "%s%s", + svn_repos_pre_revprop_change_hook(repos, pool), + SVN_REPOS__HOOK_DESC_EXT); + +#define SCRIPT_NAME SVN_REPOS__HOOK_PRE_REVPROP_CHANGE + + contents = +"#!/bin/sh" NL +"" NL +"# PRE-REVPROP-CHANGE HOOK" NL +"#" NL +"# The pre-revprop-change hook is invoked before a revision property" NL +"# is added, modified or deleted. Subversion runs this hook by invoking" NL +"# a program (script, executable, binary, etc.) named '"SCRIPT_NAME"'" NL +"# (for which this file is a template), with the following ordered" NL +"# arguments:" NL +"#" NL +"# [1] REPOS-PATH (the path to this repository)" NL +"# [2] REV (the revision being tweaked)" NL +"# [3] USER (the username of the person tweaking the property)" NL +"# [4] PROPNAME (the property being set on the revision)" NL +"# [5] ACTION (the property is being 'A'dded, 'M'odified, or 'D'eleted)" + NL +"#" NL +"# [STDIN] PROPVAL ** the new property value is passed via STDIN." NL +"#" NL +"# If the hook program exits with success, the propchange happens; but" NL +"# if it exits with failure (non-zero), the propchange doesn't happen." NL +"# The hook program can use the 'svnlook' utility to examine the " NL +"# existing value of the revision property." NL +"#" NL +"# WARNING: unlike other hooks, this hook MUST exist for revision" NL +"# properties to be changed. If the hook does not exist, Subversion " NL +"# will behave as if the hook were present, but failed. The reason" NL +"# for this is that revision properties are UNVERSIONED, meaning that" NL +"# a successful propchange is destructive; the old value is gone" NL +"# forever. We recommend the hook back up the old value somewhere." NL +"#" NL +"# On a Unix system, the normal procedure is to have '"SCRIPT_NAME"'" NL +"# invoke other programs to do the real work, though it may do the" NL +"# work itself too." NL +"#" NL +"# Note that '"SCRIPT_NAME"' must be executable by the user(s) who will" NL +"# invoke it (typically the user httpd runs as), and that user must" NL +"# have filesystem-level permission to access the repository." NL +"#" NL +"# On a Windows system, you should name the hook program" NL +"# '"SCRIPT_NAME".bat' or '"SCRIPT_NAME".exe'," NL +"# but the basic idea is the same." NL +"#" NL +HOOKS_ENVIRONMENT_TEXT +"# " NL +"# Here is an example hook script, for a Unix /bin/sh interpreter." NL +PREWRITTEN_HOOKS_TEXT +"" NL +"" NL +"REPOS=\"$1\"" NL +"REV=\"$2\"" NL +"USER=\"$3\"" NL +"PROPNAME=\"$4\"" NL +"ACTION=\"$5\"" NL +"" NL +"if [ \"$ACTION\" = \"M\" -a \"$PROPNAME\" = \"svn:log\" ]; then exit 0; fi" NL +"" NL +"echo \"Changing revision properties other than svn:log is prohibited\" >&2" NL +"exit 1" NL; + +#undef SCRIPT_NAME + + SVN_ERR_W(svn_io_file_create(this_path, contents, pool), + _("Creating pre-revprop-change hook")); + + SVN_ERR(svn_io_set_file_executable(this_path, TRUE, FALSE, pool)); + } /* end pre-revprop-change hook */ + + + /* Pre-lock hook. */ + { + this_path = apr_psprintf(pool, "%s%s", + svn_repos_pre_lock_hook(repos, pool), + SVN_REPOS__HOOK_DESC_EXT); + +#define SCRIPT_NAME SVN_REPOS__HOOK_PRE_LOCK + + contents = +"#!/bin/sh" NL +"" NL +"# PRE-LOCK HOOK" NL +"#" NL +"# The pre-lock hook is invoked before an exclusive lock is" NL +"# created. Subversion runs this hook by invoking a program " NL +"# (script, executable, binary, etc.) named '"SCRIPT_NAME"' (for which" NL +"# this file is a template), with the following ordered arguments:" NL +"#" NL +"# [1] REPOS-PATH (the path to this repository)" NL +"# [2] PATH (the path in the repository about to be locked)" NL +"# [3] USER (the user creating the lock)" NL +"# [4] COMMENT (the comment of the lock)" NL +"# [5] STEAL-LOCK (1 if the user is trying to steal the lock, else 0)" NL +"#" NL +"# If the hook program outputs anything on stdout, the output string will" NL +"# be used as the lock token for this lock operation. If you choose to use" NL +"# this feature, you must guarantee the tokens generated are unique across" NL +"# the repository each time." NL +"#" NL +"# The default working directory for the invocation is undefined, so" NL +"# the program should set one explicitly if it cares." NL +"#" NL +"# If the hook program exits with success, the lock is created; but" NL +"# if it exits with failure (non-zero), the lock action is aborted" NL +"# and STDERR is returned to the client." NL +"" NL +"# On a Unix system, the normal procedure is to have '"SCRIPT_NAME"'" NL +"# invoke other programs to do the real work, though it may do the" NL +"# work itself too." NL +"#" NL +"# Note that '"SCRIPT_NAME"' must be executable by the user(s) who will" NL +"# invoke it (typically the user httpd runs as), and that user must" NL +"# have filesystem-level permission to access the repository." NL +"#" NL +"# On a Windows system, you should name the hook program" NL +"# '"SCRIPT_NAME".bat' or '"SCRIPT_NAME".exe'," NL +"# but the basic idea is the same." NL +"#" NL +"# Here is an example hook script, for a Unix /bin/sh interpreter:" NL +"" NL +"REPOS=\"$1\"" NL +"PATH=\"$2\"" NL +"USER=\"$3\"" NL +"COMMENT=\"$4\"" NL +"STEAL=\"$5\"" NL +"" NL +"# If a lock exists and is owned by a different person, don't allow it" NL +"# to be stolen (e.g., with 'svn lock --force ...')." NL +"" NL +"# (Maybe this script could send email to the lock owner?)" NL +"SVNLOOK=" SVN_BINDIR "/svnlook" NL +"GREP=/bin/grep" NL +"SED=/bin/sed" NL +"" NL +"LOCK_OWNER=`$SVNLOOK lock \"$REPOS\" \"$PATH\" | \\" NL +" $GREP '^Owner: ' | $SED 's/Owner: //'`" NL +"" NL +"# If we get no result from svnlook, there's no lock, allow the lock to" NL +"# happen:" NL +"if [ \"$LOCK_OWNER\" = \"\" ]; then" NL +" exit 0" NL +"fi" NL +"" NL +"# If the person locking matches the lock's owner, allow the lock to" NL +"# happen:" NL +"if [ \"$LOCK_OWNER\" = \"$USER\" ]; then" NL +" exit 0" NL +"fi" NL +"" NL +"# Otherwise, we've got an owner mismatch, so return failure:" NL +"echo \"Error: $PATH already locked by ${LOCK_OWNER}.\" 1>&2" NL +"exit 1" NL; + +#undef SCRIPT_NAME + + SVN_ERR_W(svn_io_file_create(this_path, contents, pool), + "Creating pre-lock hook"); + + SVN_ERR(svn_io_set_file_executable(this_path, TRUE, FALSE, pool)); + } /* end pre-lock hook */ + + + /* Pre-unlock hook. */ + { + this_path = apr_psprintf(pool, "%s%s", + svn_repos_pre_unlock_hook(repos, pool), + SVN_REPOS__HOOK_DESC_EXT); + +#define SCRIPT_NAME SVN_REPOS__HOOK_PRE_UNLOCK + + contents = +"#!/bin/sh" NL +"" NL +"# PRE-UNLOCK HOOK" NL +"#" NL +"# The pre-unlock hook is invoked before an exclusive lock is" NL +"# destroyed. Subversion runs this hook by invoking a program " NL +"# (script, executable, binary, etc.) named '"SCRIPT_NAME"' (for which" NL +"# this file is a template), with the following ordered arguments:" NL +"#" NL +"# [1] REPOS-PATH (the path to this repository)" NL +"# [2] PATH (the path in the repository about to be unlocked)" NL +"# [3] USER (the user destroying the lock)" NL +"# [4] TOKEN (the lock token to be destroyed)" NL +"# [5] BREAK-UNLOCK (1 if the user is breaking the lock, else 0)" NL +"#" NL +"# The default working directory for the invocation is undefined, so" NL +"# the program should set one explicitly if it cares." NL +"#" NL +"# If the hook program exits with success, the lock is destroyed; but" NL +"# if it exits with failure (non-zero), the unlock action is aborted" NL +"# and STDERR is returned to the client." NL +"" NL +"# On a Unix system, the normal procedure is to have '"SCRIPT_NAME"'" NL +"# invoke other programs to do the real work, though it may do the" NL +"# work itself too." NL +"#" NL +"# Note that '"SCRIPT_NAME"' must be executable by the user(s) who will" NL +"# invoke it (typically the user httpd runs as), and that user must" NL +"# have filesystem-level permission to access the repository." NL +"#" NL +"# On a Windows system, you should name the hook program" NL +"# '"SCRIPT_NAME".bat' or '"SCRIPT_NAME".exe'," NL +"# but the basic idea is the same." NL +"#" NL +"# Here is an example hook script, for a Unix /bin/sh interpreter:" NL +"" NL +"REPOS=\"$1\"" NL +"PATH=\"$2\"" NL +"USER=\"$3\"" NL +"TOKEN=\"$4\"" NL +"BREAK=\"$5\"" NL +"" NL +"# If a lock is owned by a different person, don't allow it be broken." NL +"# (Maybe this script could send email to the lock owner?)" NL +"" NL +"SVNLOOK=" SVN_BINDIR "/svnlook" NL +"GREP=/bin/grep" NL +"SED=/bin/sed" NL +"" NL +"LOCK_OWNER=`$SVNLOOK lock \"$REPOS\" \"$PATH\" | \\" NL +" $GREP '^Owner: ' | $SED 's/Owner: //'`" NL +"" NL +"# If we get no result from svnlook, there's no lock, return success:" NL +"if [ \"$LOCK_OWNER\" = \"\" ]; then" NL +" exit 0" NL +"fi" NL +"" NL +"# If the person unlocking matches the lock's owner, return success:" NL +"if [ \"$LOCK_OWNER\" = \"$USER\" ]; then" NL +" exit 0" NL +"fi" NL +"" NL +"# Otherwise, we've got an owner mismatch, so return failure:" NL +"echo \"Error: $PATH locked by ${LOCK_OWNER}.\" 1>&2" NL +"exit 1" NL; + +#undef SCRIPT_NAME + + SVN_ERR_W(svn_io_file_create(this_path, contents, pool), + "Creating pre-unlock hook"); + + SVN_ERR(svn_io_set_file_executable(this_path, TRUE, FALSE, pool)); + } /* end pre-unlock hook */ + + + + /* Post-commit hook. */ + { + this_path = apr_psprintf(pool, "%s%s", + svn_repos_post_commit_hook(repos, pool), + SVN_REPOS__HOOK_DESC_EXT); + +#define SCRIPT_NAME SVN_REPOS__HOOK_POST_COMMIT + + contents = +"#!/bin/sh" NL +"" NL +"# POST-COMMIT HOOK" NL +"#" NL +"# The post-commit hook is invoked after a commit. Subversion runs" NL +"# this hook by invoking a program (script, executable, binary, etc.)" NL +"# named '"SCRIPT_NAME"' (for which this file is a template) with the " NL +"# following ordered arguments:" NL +"#" NL +"# [1] REPOS-PATH (the path to this repository)" NL +"# [2] REV (the number of the revision just committed)" NL +"# [3] TXN-NAME (the name of the transaction that has become REV)" NL +"#" NL +"# The default working directory for the invocation is undefined, so" NL +"# the program should set one explicitly if it cares." NL +"#" NL +"# Because the commit has already completed and cannot be undone," NL +"# the exit code of the hook program is ignored. The hook program" NL +"# can use the 'svnlook' utility to help it examine the" NL +"# newly-committed tree." NL +"#" NL +"# On a Unix system, the normal procedure is to have '"SCRIPT_NAME"'" NL +"# invoke other programs to do the real work, though it may do the" NL +"# work itself too." NL +"#" NL +"# Note that '"SCRIPT_NAME"' must be executable by the user(s) who will" NL +"# invoke it (typically the user httpd runs as), and that user must" NL +"# have filesystem-level permission to access the repository." NL +"#" NL +"# On a Windows system, you should name the hook program" NL +"# '"SCRIPT_NAME".bat' or '"SCRIPT_NAME".exe'," NL +"# but the basic idea is the same." NL +"# " NL +HOOKS_ENVIRONMENT_TEXT +"# " NL +"# Here is an example hook script, for a Unix /bin/sh interpreter." NL +PREWRITTEN_HOOKS_TEXT +"" NL +"" NL +"REPOS=\"$1\"" NL +"REV=\"$2\"" NL +"TXN_NAME=\"$3\"" NL + NL +"mailer.py commit \"$REPOS\" \"$REV\" /path/to/mailer.conf" NL; + +#undef SCRIPT_NAME + + SVN_ERR_W(svn_io_file_create(this_path, contents, pool), + _("Creating post-commit hook")); + + SVN_ERR(svn_io_set_file_executable(this_path, TRUE, FALSE, pool)); + } /* end post-commit hook */ + + + /* Post-lock hook. */ + { + this_path = apr_psprintf(pool, "%s%s", + svn_repos_post_lock_hook(repos, pool), + SVN_REPOS__HOOK_DESC_EXT); + +#define SCRIPT_NAME SVN_REPOS__HOOK_POST_LOCK + + contents = +"#!/bin/sh" NL +"" NL +"# POST-LOCK HOOK" NL +"#" NL +"# The post-lock hook is run after a path is locked. Subversion runs" NL +"# this hook by invoking a program (script, executable, binary, etc.)" NL +"# named '"SCRIPT_NAME"' (for which this file is a template) with the " NL +"# following ordered arguments:" NL +"#" NL +"# [1] REPOS-PATH (the path to this repository)" NL +"# [2] USER (the user who created the lock)" NL +"#" NL +"# The paths that were just locked are passed to the hook via STDIN (as" NL +"# of Subversion 1.2, only one path is passed per invocation, but the" NL +"# plan is to pass all locked paths at once, so the hook program" NL +"# should be written accordingly)." NL +"#" NL +"# The default working directory for the invocation is undefined, so" NL +"# the program should set one explicitly if it cares." NL +"#" NL +"# Because the lock has already been created and cannot be undone," NL +"# the exit code of the hook program is ignored. The hook program" NL +"# can use the 'svnlook' utility to help it examine the" NL +"# newly-created lock." NL +"#" NL +"# On a Unix system, the normal procedure is to have '"SCRIPT_NAME"'" NL +"# invoke other programs to do the real work, though it may do the" NL +"# work itself too." NL +"#" NL +"# Note that '"SCRIPT_NAME"' must be executable by the user(s) who will" NL +"# invoke it (typically the user httpd runs as), and that user must" NL +"# have filesystem-level permission to access the repository." NL +"#" NL +"# On a Windows system, you should name the hook program" NL +"# '"SCRIPT_NAME".bat' or '"SCRIPT_NAME".exe'," NL +"# but the basic idea is the same." NL +"# " NL +"# Here is an example hook script, for a Unix /bin/sh interpreter:" NL +"" NL +"REPOS=\"$1\"" NL +"USER=\"$2\"" NL +"" NL +"# Send email to interested parties, let them know a lock was created:" NL +"mailer.py lock \"$REPOS\" \"$USER\" /path/to/mailer.conf" NL; + +#undef SCRIPT_NAME + + SVN_ERR_W(svn_io_file_create(this_path, contents, pool), + "Creating post-lock hook"); + + SVN_ERR(svn_io_set_file_executable(this_path, TRUE, FALSE, pool)); + } /* end post-lock hook */ + + + /* Post-unlock hook. */ + { + this_path = apr_psprintf(pool, "%s%s", + svn_repos_post_unlock_hook(repos, pool), + SVN_REPOS__HOOK_DESC_EXT); + +#define SCRIPT_NAME SVN_REPOS__HOOK_POST_UNLOCK + + contents = +"#!/bin/sh" NL +"" NL +"# POST-UNLOCK HOOK" NL +"#" NL +"# The post-unlock hook runs after a path is unlocked. Subversion runs" NL +"# this hook by invoking a program (script, executable, binary, etc.)" NL +"# named '"SCRIPT_NAME"' (for which this file is a template) with the " NL +"# following ordered arguments:" NL +"#" NL +"# [1] REPOS-PATH (the path to this repository)" NL +"# [2] USER (the user who destroyed the lock)" NL +"#" NL +"# The paths that were just unlocked are passed to the hook via STDIN" NL +"# (as of Subversion 1.2, only one path is passed per invocation, but" NL +"# the plan is to pass all unlocked paths at once, so the hook program" NL +"# should be written accordingly)." NL +"#" NL +"# The default working directory for the invocation is undefined, so" NL +"# the program should set one explicitly if it cares." NL +"#" NL +"# Because the lock has already been destroyed and cannot be undone," NL +"# the exit code of the hook program is ignored." NL +"#" NL +"# On a Unix system, the normal procedure is to have '"SCRIPT_NAME"'" NL +"# invoke other programs to do the real work, though it may do the" NL +"# work itself too." NL +"#" NL +"# Note that '"SCRIPT_NAME"' must be executable by the user(s) who will" NL +"# invoke it (typically the user httpd runs as), and that user must" NL +"# have filesystem-level permission to access the repository." NL +"#" NL +"# On a Windows system, you should name the hook program" NL +"# '"SCRIPT_NAME".bat' or '"SCRIPT_NAME".exe'," NL +"# but the basic idea is the same." NL +"# " NL +"# Here is an example hook script, for a Unix /bin/sh interpreter:" NL +"" NL +"REPOS=\"$1\"" NL +"USER=\"$2\"" NL +"" NL +"# Send email to interested parties, let them know a lock was removed:" NL +"mailer.py unlock \"$REPOS\" \"$USER\" /path/to/mailer.conf" NL; + +#undef SCRIPT_NAME + + SVN_ERR_W(svn_io_file_create(this_path, contents, pool), + "Creating post-unlock hook"); + + SVN_ERR(svn_io_set_file_executable(this_path, TRUE, FALSE, pool)); + } /* end post-unlock hook */ + + + /* Post-revprop-change hook. */ + { + this_path = apr_psprintf(pool, "%s%s", + svn_repos_post_revprop_change_hook(repos, pool), + SVN_REPOS__HOOK_DESC_EXT); + +#define SCRIPT_NAME SVN_REPOS__HOOK_POST_REVPROP_CHANGE + + contents = +"#!/bin/sh" NL +"" NL +"# POST-REVPROP-CHANGE HOOK" NL +"#" NL +"# The post-revprop-change hook is invoked after a revision property" NL +"# has been added, modified or deleted. Subversion runs this hook by" NL +"# invoking a program (script, executable, binary, etc.) named" NL +"# '"SCRIPT_NAME"' (for which this file is a template), with the" NL +"# following ordered arguments:" NL +"#" NL +"# [1] REPOS-PATH (the path to this repository)" NL +"# [2] REV (the revision that was tweaked)" NL +"# [3] USER (the username of the person tweaking the property)" NL +"# [4] PROPNAME (the property that was changed)" NL +"# [5] ACTION (the property was 'A'dded, 'M'odified, or 'D'eleted)" NL +"#" NL +"# [STDIN] PROPVAL ** the old property value is passed via STDIN." NL +"#" NL +"# Because the propchange has already completed and cannot be undone," NL +"# the exit code of the hook program is ignored. The hook program" NL +"# can use the 'svnlook' utility to help it examine the" NL +"# new property value." NL +"#" NL +"# On a Unix system, the normal procedure is to have '"SCRIPT_NAME"'" NL +"# invoke other programs to do the real work, though it may do the" NL +"# work itself too." NL +"#" NL +"# Note that '"SCRIPT_NAME"' must be executable by the user(s) who will" NL +"# invoke it (typically the user httpd runs as), and that user must" NL +"# have filesystem-level permission to access the repository." NL +"#" NL +"# On a Windows system, you should name the hook program" NL +"# '"SCRIPT_NAME".bat' or '"SCRIPT_NAME".exe'," NL +"# but the basic idea is the same." NL +"# " NL +HOOKS_ENVIRONMENT_TEXT +"# " NL +"# Here is an example hook script, for a Unix /bin/sh interpreter." NL +PREWRITTEN_HOOKS_TEXT +"" NL +"" NL +"REPOS=\"$1\"" NL +"REV=\"$2\"" NL +"USER=\"$3\"" NL +"PROPNAME=\"$4\"" NL +"ACTION=\"$5\"" NL +"" NL +"mailer.py propchange2 \"$REPOS\" \"$REV\" \"$USER\" \"$PROPNAME\" " +"\"$ACTION\" /path/to/mailer.conf" NL; + +#undef SCRIPT_NAME + + SVN_ERR_W(svn_io_file_create(this_path, contents, pool), + _("Creating post-revprop-change hook")); + + SVN_ERR(svn_io_set_file_executable(this_path, TRUE, FALSE, pool)); + } /* end post-revprop-change hook */ + + return SVN_NO_ERROR; +} + +static svn_error_t * +create_conf(svn_repos_t *repos, apr_pool_t *pool) +{ + SVN_ERR_W(create_repos_dir(repos->conf_path, pool), + _("Creating conf directory")); + + /* Write a default template for svnserve.conf. */ + { + static const char * const svnserve_conf_contents = +"### This file controls the configuration of the svnserve daemon, if you" NL +"### use it to allow access to this repository. (If you only allow" NL +"### access through http: and/or file: URLs, then this file is" NL +"### irrelevant.)" NL +"" NL +"### Visit http://subversion.apache.org/ for more information." NL +"" NL +"[general]" NL +"### The anon-access and auth-access options control access to the" NL +"### repository for unauthenticated (a.k.a. anonymous) users and" NL +"### authenticated users, respectively." NL +"### Valid values are \"write\", \"read\", and \"none\"." NL +"### Setting the value to \"none\" prohibits both reading and writing;" NL +"### \"read\" allows read-only access, and \"write\" allows complete " NL +"### read/write access to the repository." NL +"### The sample settings below are the defaults and specify that anonymous" NL +"### users have read-only access to the repository, while authenticated" NL +"### users have read and write access to the repository." NL +"# anon-access = read" NL +"# auth-access = write" NL +"### The password-db option controls the location of the password" NL +"### database file. Unless you specify a path starting with a /," NL +"### the file's location is relative to the directory containing" NL +"### this configuration file." NL +"### If SASL is enabled (see below), this file will NOT be used." NL +"### Uncomment the line below to use the default password file." NL +"# password-db = passwd" NL +"### The authz-db option controls the location of the authorization" NL +"### rules for path-based access control. Unless you specify a path" NL +"### starting with a /, the file's location is relative to the" NL +"### directory containing this file. The specified path may be a" NL +"### repository relative URL (^/) or an absolute file:// URL to a text" NL +"### file in a Subversion repository. If you don't specify an authz-db," NL +"### no path-based access control is done." NL +"### Uncomment the line below to use the default authorization file." NL +"# authz-db = " SVN_REPOS__CONF_AUTHZ NL +"### The groups-db option controls the location of the groups file." NL +"### Unless you specify a path starting with a /, the file's location is" NL +"### relative to the directory containing this file. The specified path" NL +"### may be a repository relative URL (^/) or an absolute file:// URL to a" NL +"### text file in a Subversion repository." NL +"# groups-db = " SVN_REPOS__CONF_GROUPS NL +"### This option specifies the authentication realm of the repository." NL +"### If two repositories have the same authentication realm, they should" NL +"### have the same password database, and vice versa. The default realm" NL +"### is repository's uuid." NL +"# realm = My First Repository" NL +"### The force-username-case option causes svnserve to case-normalize" NL +"### usernames before comparing them against the authorization rules in the" NL +"### authz-db file configured above. Valid values are \"upper\" (to upper-" NL +"### case the usernames), \"lower\" (to lowercase the usernames), and" NL +"### \"none\" (to compare usernames as-is without case conversion, which" NL +"### is the default behavior)." NL +"# force-username-case = none" NL +"### The hooks-env options specifies a path to the hook script environment " NL +"### configuration file. This option overrides the per-repository default" NL +"### and can be used to configure the hook script environment for multiple " NL +"### repositories in a single file, if an absolute path is specified." NL +"### Unless you specify an absolute path, the file's location is relative" NL +"### to the directory containing this file." NL +"# hooks-env = " SVN_REPOS__CONF_HOOKS_ENV NL +"" NL +"[sasl]" NL +"### This option specifies whether you want to use the Cyrus SASL" NL +"### library for authentication. Default is false." NL +"### This section will be ignored if svnserve is not built with Cyrus" NL +"### SASL support; to check, run 'svnserve --version' and look for a line" NL +"### reading 'Cyrus SASL authentication is available.'" NL +"# use-sasl = true" NL +"### These options specify the desired strength of the security layer" NL +"### that you want SASL to provide. 0 means no encryption, 1 means" NL +"### integrity-checking only, values larger than 1 are correlated" NL +"### to the effective key length for encryption (e.g. 128 means 128-bit" NL +"### encryption). The values below are the defaults." NL +"# min-encryption = 0" NL +"# max-encryption = 256" NL; + + SVN_ERR_W(svn_io_file_create(svn_repos_svnserve_conf(repos, pool), + svnserve_conf_contents, pool), + _("Creating svnserve.conf file")); + } + + { + static const char * const passwd_contents = +"### This file is an example password file for svnserve." NL +"### Its format is similar to that of svnserve.conf. As shown in the" NL +"### example below it contains one section labelled [users]." NL +"### The name and password for each user follow, one account per line." NL +"" NL +"[users]" NL +"# harry = harryssecret" NL +"# sally = sallyssecret" NL; + + SVN_ERR_W(svn_io_file_create(svn_dirent_join(repos->conf_path, + SVN_REPOS__CONF_PASSWD, + pool), + passwd_contents, pool), + _("Creating passwd file")); + } + + { + static const char * const authz_contents = +"### This file is an example authorization file for svnserve." NL +"### Its format is identical to that of mod_authz_svn authorization" NL +"### files." NL +"### As shown below each section defines authorizations for the path and" NL +"### (optional) repository specified by the section name." NL +"### The authorizations follow. An authorization line can refer to:" NL +"### - a single user," NL +"### - a group of users defined in a special [groups] section," NL +"### - an alias defined in a special [aliases] section," NL +"### - all authenticated users, using the '$authenticated' token," NL +"### - only anonymous users, using the '$anonymous' token," NL +"### - anyone, using the '*' wildcard." NL +"###" NL +"### A match can be inverted by prefixing the rule with '~'. Rules can" NL +"### grant read ('r') access, read-write ('rw') access, or no access" NL +"### ('')." NL +"" NL +"[aliases]" NL +"# joe = /C=XZ/ST=Dessert/L=Snake City/O=Snake Oil, Ltd./OU=Research Institute/CN=Joe Average" NL +"" NL +"[groups]" NL +"# harry_and_sally = harry,sally" NL +"# harry_sally_and_joe = harry,sally,&joe" NL +"" NL +"# [/foo/bar]" NL +"# harry = rw" NL +"# &joe = r" NL +"# * =" NL +"" NL +"# [repository:/baz/fuz]" NL +"# @harry_and_sally = rw" NL +"# * = r" NL; + + SVN_ERR_W(svn_io_file_create(svn_dirent_join(repos->conf_path, + SVN_REPOS__CONF_AUTHZ, + pool), + authz_contents, pool), + _("Creating authz file")); + } + + { + static const char * const hooks_env_contents = +"### This file is an example hook script environment configuration file." NL +"### Hook scripts run in an empty environment by default." NL +"### As shown below each section defines environment variables for a" NL +"### particular hook script. The [default] section defines environment" NL +"### variables for all hook scripts, unless overridden by a hook-specific" NL +"### section." NL +"" NL +"### This example configures a UTF-8 locale for all hook scripts, so that " NL +"### special characters, such as umlauts, may be printed to stderr." NL +"### If UTF-8 is used with a mod_dav_svn server, the SVNUseUTF8 option must" NL +"### also be set to 'yes' in httpd.conf." NL +"### With svnserve, the LANG environment variable of the svnserve process" NL +"### must be set to the same value as given here." NL +"[default]" NL +"LANG = en_US.UTF-8" NL +"" NL +"### This sets the PATH environment variable for the pre-commit hook." NL +"[pre-commit]" NL +"PATH = /usr/local/bin:/usr/bin:/usr/sbin" NL; + + SVN_ERR_W(svn_io_file_create(svn_dirent_join(repos->conf_path, + SVN_REPOS__CONF_HOOKS_ENV \ + SVN_REPOS__HOOK_DESC_EXT, + pool), + hooks_env_contents, pool), + _("Creating hooks-env file")); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos_hooks_setenv(svn_repos_t *repos, + const char *hooks_env_path, + apr_pool_t *scratch_pool) +{ + if (hooks_env_path == NULL) + repos->hooks_env_path = svn_dirent_join(repos->conf_path, + SVN_REPOS__CONF_HOOKS_ENV, + repos->pool); + else if (!svn_dirent_is_absolute(hooks_env_path)) + repos->hooks_env_path = svn_dirent_join(repos->conf_path, + hooks_env_path, + repos->pool); + else + repos->hooks_env_path = apr_pstrdup(repos->pool, hooks_env_path); + + return SVN_NO_ERROR; +} + +/* Allocate and return a new svn_repos_t * object, initializing the + directory pathname members based on PATH, and initializing the + REPOSITORY_CAPABILITIES member. + The members FS, FORMAT, and FS_TYPE are *not* initialized (they are null), + and it is the caller's responsibility to fill them in if needed. */ +static svn_repos_t * +create_svn_repos_t(const char *path, apr_pool_t *pool) +{ + svn_repos_t *repos = apr_pcalloc(pool, sizeof(*repos)); + + repos->path = apr_pstrdup(pool, path); + repos->db_path = svn_dirent_join(path, SVN_REPOS__DB_DIR, pool); + repos->conf_path = svn_dirent_join(path, SVN_REPOS__CONF_DIR, pool); + repos->hook_path = svn_dirent_join(path, SVN_REPOS__HOOK_DIR, pool); + repos->lock_path = svn_dirent_join(path, SVN_REPOS__LOCK_DIR, pool); + repos->hooks_env_path = NULL; + repos->repository_capabilities = apr_hash_make(pool); + repos->pool = pool; + + return repos; +} + + +static svn_error_t * +create_repos_structure(svn_repos_t *repos, + const char *path, + apr_hash_t *fs_config, + apr_pool_t *pool) +{ + /* Create the top-level repository directory. */ + SVN_ERR_W(create_repos_dir(path, pool), + _("Could not create top-level directory")); + + /* Create the DAV sandbox directory if pre-1.4 or pre-1.5-compatible. */ + if (fs_config + && (svn_hash_gets(fs_config, SVN_FS_CONFIG_PRE_1_4_COMPATIBLE) + || svn_hash_gets(fs_config, SVN_FS_CONFIG_PRE_1_5_COMPATIBLE))) + { + const char *dav_path = svn_dirent_join(repos->path, + SVN_REPOS__DAV_DIR, pool); + SVN_ERR_W(create_repos_dir(dav_path, pool), + _("Creating DAV sandbox dir")); + } + + /* Create the lock directory. */ + SVN_ERR(create_locks(repos, pool)); + + /* Create the hooks directory. */ + SVN_ERR(create_hooks(repos, pool)); + + /* Create the conf directory. */ + SVN_ERR(create_conf(repos, pool)); + + /* Write the top-level README file. */ + { + const char * const readme_header = + "This is a Subversion repository; use the 'svnadmin' and 'svnlook' " NL + "tools to examine it. Do not add, delete, or modify files here " NL + "unless you know how to avoid corrupting the repository." NL + "" NL; + const char * const readme_bdb_insert = + "The directory \"" SVN_REPOS__DB_DIR "\" contains a Berkeley DB environment." NL + "you may need to tweak the values in \"" SVN_REPOS__DB_DIR "/DB_CONFIG\" to match the" NL + "requirements of your site." NL + "" NL; + const char * const readme_footer = + "Visit http://subversion.apache.org/ for more information." NL; + apr_file_t *f; + apr_size_t written; + + SVN_ERR(svn_io_file_open(&f, + svn_dirent_join(path, SVN_REPOS__README, pool), + (APR_WRITE | APR_CREATE | APR_EXCL), + APR_OS_DEFAULT, pool)); + + SVN_ERR(svn_io_file_write_full(f, readme_header, strlen(readme_header), + &written, pool)); + if (strcmp(repos->fs_type, SVN_FS_TYPE_BDB) == 0) + SVN_ERR(svn_io_file_write_full(f, readme_bdb_insert, + strlen(readme_bdb_insert), + &written, pool)); + SVN_ERR(svn_io_file_write_full(f, readme_footer, strlen(readme_footer), + &written, pool)); + + return svn_io_file_close(f, pool); + } +} + + +/* There is, at present, nothing within the direct responsibility + of libsvn_repos which requires locking. For historical compatibility + reasons, the BDB libsvn_fs backend does not do its own locking, expecting + libsvn_repos to do the locking for it. Here we take care of that + backend-specific requirement. + The kind of lock is controlled by EXCLUSIVE and NONBLOCKING. + The lock is scoped to POOL. */ +static svn_error_t * +lock_repos(svn_repos_t *repos, + svn_boolean_t exclusive, + svn_boolean_t nonblocking, + apr_pool_t *pool) +{ + if (strcmp(repos->fs_type, SVN_FS_TYPE_BDB) == 0) + { + svn_error_t *err; + const char *lockfile_path = svn_repos_db_lockfile(repos, pool); + + err = svn_io_file_lock2(lockfile_path, exclusive, nonblocking, pool); + if (err != NULL && APR_STATUS_IS_EAGAIN(err->apr_err)) + return svn_error_trace(err); + SVN_ERR_W(err, _("Error opening db lockfile")); + } + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_create(svn_repos_t **repos_p, + const char *path, + const char *unused_1, + const char *unused_2, + apr_hash_t *config, + apr_hash_t *fs_config, + apr_pool_t *pool) +{ + svn_repos_t *repos; + svn_error_t *err; + const char *root_path; + const char *local_abspath; + + /* Allocate a repository object, filling in the format we will create. */ + repos = create_svn_repos_t(path, pool); + repos->format = SVN_REPOS__FORMAT_NUMBER; + + /* Discover the type of the filesystem we are about to create. */ + repos->fs_type = svn_hash__get_cstring(fs_config, SVN_FS_CONFIG_FS_TYPE, + DEFAULT_FS_TYPE); + if (svn_hash__get_bool(fs_config, SVN_FS_CONFIG_PRE_1_4_COMPATIBLE, FALSE)) + repos->format = SVN_REPOS__FORMAT_NUMBER_LEGACY; + + /* Don't create a repository inside another repository. */ + SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, pool)); + root_path = svn_repos_find_root_path(local_abspath, pool); + if (root_path != NULL) + { + if (strcmp(root_path, local_abspath) == 0) + return svn_error_createf(SVN_ERR_REPOS_BAD_ARGS, NULL, + _("'%s' is an existing repository"), + svn_dirent_local_style(root_path, pool)); + else + return svn_error_createf(SVN_ERR_REPOS_BAD_ARGS, NULL, + _("'%s' is a subdirectory of an existing " + "repository " "rooted at '%s'"), + svn_dirent_local_style(local_abspath, pool), + svn_dirent_local_style(root_path, pool)); + } + + /* Create the various files and subdirectories for the repository. */ + SVN_ERR_W(create_repos_structure(repos, path, fs_config, pool), + _("Repository creation failed")); + + /* Lock if needed. */ + SVN_ERR(lock_repos(repos, FALSE, FALSE, pool)); + + /* Create an environment for the filesystem. */ + if ((err = svn_fs_create(&repos->fs, repos->db_path, fs_config, pool))) + { + /* If there was an error making the filesytem, e.g. unknown/supported + * filesystem type. Clean up after ourselves. Yes this is safe because + * create_repos_structure will fail if the path existed before we started + * so we can't accidentally remove a directory that previously existed. + */ + + return svn_error_trace( + svn_error_compose_create( + err, + svn_io_remove_dir2(path, FALSE, NULL, NULL, pool))); + } + + /* This repository is ready. Stamp it with a format number. */ + SVN_ERR(svn_io_write_version_file + (svn_dirent_join(path, SVN_REPOS__FORMAT, pool), + repos->format, pool)); + + *repos_p = repos; + return SVN_NO_ERROR; +} + + +/* Check if @a path is the root of a repository by checking if the + * path contains the expected files and directories. Return TRUE + * on errors (which would be permission errors, probably) so that + * we the user will see them after we try to open the repository + * for real. */ +static svn_boolean_t +check_repos_path(const char *path, + apr_pool_t *pool) +{ + svn_node_kind_t kind; + svn_error_t *err; + + err = svn_io_check_path(svn_dirent_join(path, SVN_REPOS__FORMAT, pool), + &kind, pool); + if (err) + { + svn_error_clear(err); + return TRUE; + } + if (kind != svn_node_file) + return FALSE; + + /* Check the db/ subdir, but allow it to be a symlink (Subversion + works just fine if it's a symlink). */ + err = svn_io_check_resolved_path + (svn_dirent_join(path, SVN_REPOS__DB_DIR, pool), &kind, pool); + if (err) + { + svn_error_clear(err); + return TRUE; + } + if (kind != svn_node_dir) + return FALSE; + + return TRUE; +} + + +/* Verify that REPOS's format is suitable. + Use POOL for temporary allocation. */ +static svn_error_t * +check_repos_format(svn_repos_t *repos, + apr_pool_t *pool) +{ + int format; + const char *format_path; + + format_path = svn_dirent_join(repos->path, SVN_REPOS__FORMAT, pool); + SVN_ERR(svn_io_read_version_file(&format, format_path, pool)); + + if (format != SVN_REPOS__FORMAT_NUMBER && + format != SVN_REPOS__FORMAT_NUMBER_LEGACY) + { + return svn_error_createf + (SVN_ERR_REPOS_UNSUPPORTED_VERSION, NULL, + _("Expected repository format '%d' or '%d'; found format '%d'"), + SVN_REPOS__FORMAT_NUMBER_LEGACY, SVN_REPOS__FORMAT_NUMBER, + format); + } + + repos->format = format; + + return SVN_NO_ERROR; +} + + +/* Set *REPOS_P to a repository at PATH which has been opened. + See lock_repos() above regarding EXCLUSIVE and NONBLOCKING. + OPEN_FS indicates whether the Subversion filesystem should be opened, + the handle being placed into repos->fs. + Do all allocation in POOL. */ +static svn_error_t * +get_repos(svn_repos_t **repos_p, + const char *path, + svn_boolean_t exclusive, + svn_boolean_t nonblocking, + svn_boolean_t open_fs, + apr_hash_t *fs_config, + apr_pool_t *pool) +{ + svn_repos_t *repos; + + /* Allocate a repository object. */ + repos = create_svn_repos_t(path, pool); + + /* Verify the validity of our repository format. */ + SVN_ERR(check_repos_format(repos, pool)); + + /* Discover the FS type. */ + SVN_ERR(svn_fs_type(&repos->fs_type, repos->db_path, pool)); + + /* Lock if needed. */ + SVN_ERR(lock_repos(repos, exclusive, nonblocking, pool)); + + /* Open up the filesystem only after obtaining the lock. */ + if (open_fs) + SVN_ERR(svn_fs_open(&repos->fs, repos->db_path, fs_config, pool)); + +#ifdef SVN_DEBUG_CRASH_AT_REPOS_OPEN + /* If $PATH/config/debug-abort exists, crash the server here. + This debugging feature can be used to test client recovery + when the server crashes. + + See: Issue #4274 */ + { + svn_node_kind_t kind; + svn_error_t *err = svn_io_check_path( + svn_dirent_join(repos->conf_path, "debug-abort", pool), + &kind, pool); + svn_error_clear(err); + if (!err && kind == svn_node_file) + SVN_ERR_MALFUNCTION_NO_RETURN(); + } +#endif /* SVN_DEBUG_CRASH_AT_REPOS_OPEN */ + + *repos_p = repos; + return SVN_NO_ERROR; +} + + + +const char * +svn_repos_find_root_path(const char *path, + apr_pool_t *pool) +{ + const char *candidate = path; + const char *decoded; + svn_error_t *err; + + while (1) + { + /* Try to decode the path, so we don't fail if it contains characters + that aren't supported by the OS filesystem. The subversion fs + isn't restricted by the OS filesystem character set. */ + err = svn_path_cstring_from_utf8(&decoded, candidate, pool); + if (!err && check_repos_path(candidate, pool)) + break; + svn_error_clear(err); + + if (svn_path_is_empty(candidate) || + svn_dirent_is_root(candidate, strlen(candidate))) + return NULL; + + candidate = svn_dirent_dirname(candidate, pool); + } + + return candidate; +} + + +svn_error_t * +svn_repos_open2(svn_repos_t **repos_p, + const char *path, + apr_hash_t *fs_config, + apr_pool_t *pool) +{ + /* Fetch a repository object initialized with a shared read/write + lock on the database. */ + + return get_repos(repos_p, path, FALSE, FALSE, TRUE, fs_config, pool); +} + + +svn_error_t * +svn_repos_upgrade2(const char *path, + svn_boolean_t nonblocking, + svn_repos_notify_func_t notify_func, + void *notify_baton, + apr_pool_t *pool) +{ + svn_repos_t *repos; + const char *format_path; + int format; + apr_pool_t *subpool = svn_pool_create(pool); + + /* Fetch a repository object; for the Berkeley DB backend, it is + initialized with an EXCLUSIVE lock on the database. This will at + least prevent others from trying to read or write to it while we + run recovery. (Other backends should do their own locking; see + lock_repos.) */ + SVN_ERR(get_repos(&repos, path, TRUE, nonblocking, FALSE, NULL, subpool)); + + if (notify_func) + { + /* We notify *twice* here, because there are two different logistical + actions occuring. */ + svn_repos_notify_t *notify = svn_repos_notify_create( + svn_repos_notify_mutex_acquired, subpool); + notify_func(notify_baton, notify, subpool); + + notify->action = svn_repos_notify_upgrade_start; + notify_func(notify_baton, notify, subpool); + } + + /* Try to overwrite with its own contents. We do this only to + verify that we can, because we don't want to actually bump the + format of the repository until our underlying filesystem claims + to have been upgraded correctly. */ + format_path = svn_dirent_join(repos->path, SVN_REPOS__FORMAT, subpool); + SVN_ERR(svn_io_read_version_file(&format, format_path, subpool)); + SVN_ERR(svn_io_write_version_file(format_path, format, subpool)); + + /* Try to upgrade the filesystem. */ + SVN_ERR(svn_fs_upgrade(repos->db_path, subpool)); + + /* Now overwrite our format file with the latest version. */ + SVN_ERR(svn_io_write_version_file(format_path, SVN_REPOS__FORMAT_NUMBER, + subpool)); + + /* Close shop and free the subpool, to release the exclusive lock. */ + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_delete(const char *path, + apr_pool_t *pool) +{ + const char *db_path = svn_dirent_join(path, SVN_REPOS__DB_DIR, pool); + + /* Delete the filesystem environment... */ + SVN_ERR(svn_fs_delete_fs(db_path, pool)); + + /* ...then blow away everything else. */ + return svn_io_remove_dir2(path, FALSE, NULL, NULL, pool); +} + + +/* Repository supports the capability. */ +static const char *capability_yes = "yes"; +/* Repository does not support the capability. */ +static const char *capability_no = "no"; + +svn_error_t * +svn_repos_has_capability(svn_repos_t *repos, + svn_boolean_t *has, + const char *capability, + apr_pool_t *pool) +{ + const char *val = svn_hash_gets(repos->repository_capabilities, capability); + + if (val == capability_yes) + { + *has = TRUE; + } + else if (val == capability_no) + { + *has = FALSE; + } + /* Else don't know, so investigate. */ + else if (strcmp(capability, SVN_REPOS_CAPABILITY_MERGEINFO) == 0) + { + svn_error_t *err; + svn_fs_root_t *root; + svn_mergeinfo_catalog_t ignored; + apr_array_header_t *paths = apr_array_make(pool, 1, + sizeof(char *)); + + SVN_ERR(svn_fs_revision_root(&root, repos->fs, 0, pool)); + APR_ARRAY_PUSH(paths, const char *) = ""; + err = svn_fs_get_mergeinfo2(&ignored, root, paths, FALSE, FALSE, + TRUE, pool, pool); + + if (err) + { + if (err->apr_err == SVN_ERR_UNSUPPORTED_FEATURE) + { + svn_error_clear(err); + svn_hash_sets(repos->repository_capabilities, + SVN_REPOS_CAPABILITY_MERGEINFO, capability_no); + *has = FALSE; + } + else if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + /* Mergeinfo requests use relative paths, and anyway we're + in r0, so we're likely to get this error -- but it + means the repository supports mergeinfo! */ + svn_error_clear(err); + svn_hash_sets(repos->repository_capabilities, + SVN_REPOS_CAPABILITY_MERGEINFO, capability_yes); + *has = TRUE; + } + else + { + return svn_error_trace(err); + } + } + else + { + svn_hash_sets(repos->repository_capabilities, + SVN_REPOS_CAPABILITY_MERGEINFO, capability_yes); + *has = TRUE; + } + } + else + { + return svn_error_createf(SVN_ERR_UNKNOWN_CAPABILITY, 0, + _("unknown capability '%s'"), capability); + } + + return SVN_NO_ERROR; +} + +svn_fs_t * +svn_repos_fs(svn_repos_t *repos) +{ + if (! repos) + return NULL; + return repos->fs; +} + + +/* For historical reasons, for the Berkeley DB backend, this code uses + * repository locking, which is motivated by the need to support the + * Berkeley DB error DB_RUN_RECOVERY. (FSFS takes care of locking + * itself, inside its implementation of svn_fs_recover.) Here's how + * it works: + * + * Every accessor of a repository's database takes out a shared lock + * on the repository -- both readers and writers get shared locks, and + * there can be an unlimited number of shared locks simultaneously. + * + * Sometimes, a db access returns the error DB_RUN_RECOVERY. When + * this happens, we need to run svn_fs_recover() on the db + * with no other accessors present. So we take out an exclusive lock + * on the repository. From the moment we request the exclusive lock, + * no more shared locks are granted, and when the last shared lock + * disappears, the exclusive lock is granted. As soon as we get it, + * we can run recovery. + * + * We assume that once any berkeley call returns DB_RUN_RECOVERY, they + * all do, until recovery is run. + */ + +svn_error_t * +svn_repos_recover4(const char *path, + svn_boolean_t nonblocking, + svn_repos_notify_func_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void * cancel_baton, + apr_pool_t *pool) +{ + svn_repos_t *repos; + apr_pool_t *subpool = svn_pool_create(pool); + + /* Fetch a repository object; for the Berkeley DB backend, it is + initialized with an EXCLUSIVE lock on the database. This will at + least prevent others from trying to read or write to it while we + run recovery. (Other backends should do their own locking; see + lock_repos.) */ + SVN_ERR(get_repos(&repos, path, TRUE, nonblocking, + FALSE, /* don't try to open the db yet. */ + NULL, + subpool)); + + if (notify_func) + { + /* We notify *twice* here, because there are two different logistical + actions occuring. */ + svn_repos_notify_t *notify = svn_repos_notify_create( + svn_repos_notify_mutex_acquired, subpool); + notify_func(notify_baton, notify, subpool); + + notify->action = svn_repos_notify_recover_start; + notify_func(notify_baton, notify, subpool); + } + + /* Recover the database to a consistent state. */ + SVN_ERR(svn_fs_recover(repos->db_path, cancel_func, cancel_baton, subpool)); + + /* Close shop and free the subpool, to release the exclusive lock. */ + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + +struct freeze_baton_t { + apr_array_header_t *paths; + int counter; + svn_repos_freeze_func_t freeze_func; + void *freeze_baton; +}; + +static svn_error_t * +multi_freeze(void *baton, + apr_pool_t *pool) +{ + struct freeze_baton_t *fb = baton; + + if (fb->counter == fb->paths->nelts) + { + SVN_ERR(fb->freeze_func(fb->freeze_baton, pool)); + return SVN_NO_ERROR; + } + else + { + /* Using a subpool as the only way to unlock the repos lock used + by BDB is to clear the pool used to take the lock. */ + apr_pool_t *subpool = svn_pool_create(pool); + const char *path = APR_ARRAY_IDX(fb->paths, fb->counter, const char *); + svn_repos_t *repos; + + ++fb->counter; + + SVN_ERR(get_repos(&repos, path, + TRUE /* exclusive (only applies to BDB) */, + FALSE /* non-blocking */, + FALSE /* open-fs */, + NULL, subpool)); + + + if (strcmp(repos->fs_type, SVN_FS_TYPE_BDB) == 0) + { + svn_error_t *err = multi_freeze(fb, subpool); + + svn_pool_destroy(subpool); + + return err; + } + else + { + SVN_ERR(svn_fs_open(&repos->fs, repos->db_path, NULL, subpool)); + SVN_ERR(svn_fs_freeze(svn_repos_fs(repos), multi_freeze, fb, + subpool)); + } + + svn_pool_destroy(subpool); + } + + return SVN_NO_ERROR; +} + +/* For BDB we fall back on BDB's repos layer lock which means that the + repository is unreadable while frozen. + + For FSFS we delegate to the FS layer which uses the FSFS write-lock + and an SQLite reserved lock which means the repository is readable + while frozen. */ +svn_error_t * +svn_repos_freeze(apr_array_header_t *paths, + svn_repos_freeze_func_t freeze_func, + void *freeze_baton, + apr_pool_t *pool) +{ + struct freeze_baton_t fb; + + fb.paths = paths; + fb.counter = 0; + fb.freeze_func = freeze_func; + fb.freeze_baton = freeze_baton; + + SVN_ERR(multi_freeze(&fb, pool)); + + return SVN_NO_ERROR; +} + +svn_error_t *svn_repos_db_logfiles(apr_array_header_t **logfiles, + const char *path, + svn_boolean_t only_unused, + apr_pool_t *pool) +{ + svn_repos_t *repos; + int i; + + SVN_ERR(get_repos(&repos, path, + FALSE, FALSE, + FALSE, /* Do not open fs. */ + NULL, + pool)); + + SVN_ERR(svn_fs_berkeley_logfiles(logfiles, + svn_repos_db_env(repos, pool), + only_unused, + pool)); + + /* Loop, printing log files. */ + for (i = 0; i < (*logfiles)->nelts; i++) + { + const char ** log_file = &(APR_ARRAY_IDX(*logfiles, i, const char *)); + *log_file = svn_dirent_join(SVN_REPOS__DB_DIR, *log_file, pool); + } + + return SVN_NO_ERROR; +} + +/* Baton for hotcopy_structure(). */ +struct hotcopy_ctx_t { + const char *dest; /* target location to construct */ + size_t src_len; /* len of the source path*/ + + /* As in svn_repos_hotcopy2() */ + svn_boolean_t incremental; + svn_cancel_func_t cancel_func; + void *cancel_baton; +}; + +/* Copy the repository structure of PATH to BATON->DEST, with exception of + * @c SVN_REPOS__DB_DIR, @c SVN_REPOS__LOCK_DIR and @c SVN_REPOS__FORMAT; + * those directories and files are handled separately. + * + * BATON is a (struct hotcopy_ctx_t *). BATON->SRC_LEN is the length + * of PATH. + * + * Implements svn_io_walk_func_t. + */ +static svn_error_t *hotcopy_structure(void *baton, + const char *path, + const apr_finfo_t *finfo, + apr_pool_t *pool) +{ + const struct hotcopy_ctx_t *ctx = baton; + const char *sub_path; + const char *target; + + if (ctx->cancel_func) + SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); + + if (strlen(path) == ctx->src_len) + { + sub_path = ""; + } + else + { + sub_path = &path[ctx->src_len+1]; + + /* Check if we are inside db directory and if so skip it */ + if (svn_path_compare_paths + (svn_dirent_get_longest_ancestor(SVN_REPOS__DB_DIR, sub_path, pool), + SVN_REPOS__DB_DIR) == 0) + return SVN_NO_ERROR; + + if (svn_path_compare_paths + (svn_dirent_get_longest_ancestor(SVN_REPOS__LOCK_DIR, sub_path, + pool), + SVN_REPOS__LOCK_DIR) == 0) + return SVN_NO_ERROR; + + if (svn_path_compare_paths + (svn_dirent_get_longest_ancestor(SVN_REPOS__FORMAT, sub_path, pool), + SVN_REPOS__FORMAT) == 0) + return SVN_NO_ERROR; + } + + target = svn_dirent_join(ctx->dest, sub_path, pool); + + if (finfo->filetype == APR_DIR) + { + svn_error_t *err; + + err = create_repos_dir(target, pool); + if (ctx->incremental && err && err->apr_err == SVN_ERR_DIR_NOT_EMPTY) + { + svn_error_clear(err); + err = SVN_NO_ERROR; + } + return svn_error_trace(err); + } + else if (finfo->filetype == APR_REG) + return svn_io_copy_file(path, target, TRUE, pool); + else if (finfo->filetype == APR_LNK) + return svn_io_copy_link(path, target, pool); + else + return SVN_NO_ERROR; +} + + +/** Obtain a lock on db logs lock file. Create one if it does not exist. + */ +static svn_error_t * +lock_db_logs_file(svn_repos_t *repos, + svn_boolean_t exclusive, + apr_pool_t *pool) +{ + const char * lock_file = svn_repos_db_logs_lockfile(repos, pool); + + /* Try to create a lock file, in case if it is missing. As in case of the + repositories created before hotcopy functionality. */ + svn_error_clear(create_db_logs_lock(repos, pool)); + + return svn_io_file_lock2(lock_file, exclusive, FALSE, pool); +} + + +/* Make a copy of a repository with hot backup of fs. */ +svn_error_t * +svn_repos_hotcopy2(const char *src_path, + const char *dst_path, + svn_boolean_t clean_logs, + svn_boolean_t incremental, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + svn_repos_t *src_repos; + svn_repos_t *dst_repos; + struct hotcopy_ctx_t hotcopy_context; + svn_error_t *err; + const char *src_abspath; + const char *dst_abspath; + + SVN_ERR(svn_dirent_get_absolute(&src_abspath, src_path, pool)); + SVN_ERR(svn_dirent_get_absolute(&dst_abspath, dst_path, pool)); + if (strcmp(src_abspath, dst_abspath) == 0) + return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, + _("Hotcopy source and destination are equal")); + + /* Try to open original repository */ + SVN_ERR(get_repos(&src_repos, src_abspath, + FALSE, FALSE, + FALSE, /* don't try to open the db yet. */ + NULL, + pool)); + + /* If we are going to clean logs, then get an exclusive lock on + db-logs.lock, to ensure that no one else will work with logs. + + If we are just copying, then get a shared lock to ensure that + no one else will clean logs while we copying them */ + + SVN_ERR(lock_db_logs_file(src_repos, clean_logs, pool)); + + /* Copy the repository to a new path, with exception of + specially handled directories */ + + hotcopy_context.dest = dst_abspath; + hotcopy_context.src_len = strlen(src_abspath); + hotcopy_context.incremental = incremental; + hotcopy_context.cancel_func = cancel_func; + hotcopy_context.cancel_baton = cancel_baton; + SVN_ERR(svn_io_dir_walk2(src_abspath, + 0, + hotcopy_structure, + &hotcopy_context, + pool)); + + /* Prepare dst_repos object so that we may create locks, + so that we may open repository */ + + dst_repos = create_svn_repos_t(dst_abspath, pool); + dst_repos->fs_type = src_repos->fs_type; + dst_repos->format = src_repos->format; + + err = create_locks(dst_repos, pool); + if (err) + { + if (incremental && err->apr_err == SVN_ERR_DIR_NOT_EMPTY) + svn_error_clear(err); + else + return svn_error_trace(err); + } + + err = svn_io_dir_make_sgid(dst_repos->db_path, APR_OS_DEFAULT, pool); + if (err) + { + if (incremental && APR_STATUS_IS_EEXIST(err->apr_err)) + svn_error_clear(err); + else + return svn_error_trace(err); + } + + /* Exclusively lock the new repository. + No one should be accessing it at the moment */ + SVN_ERR(lock_repos(dst_repos, TRUE, FALSE, pool)); + + SVN_ERR(svn_fs_hotcopy2(src_repos->db_path, dst_repos->db_path, + clean_logs, incremental, + cancel_func, cancel_baton, pool)); + + /* Destination repository is ready. Stamp it with a format number. */ + return svn_io_write_version_file + (svn_dirent_join(dst_repos->path, SVN_REPOS__FORMAT, pool), + dst_repos->format, pool); +} + +svn_error_t * +svn_repos_hotcopy(const char *src_path, + const char *dst_path, + svn_boolean_t clean_logs, + apr_pool_t *pool) +{ + return svn_error_trace(svn_repos_hotcopy2(src_path, dst_path, clean_logs, + FALSE, NULL, NULL, pool)); +} + +/* Return the library version number. */ +const svn_version_t * +svn_repos_version(void) +{ + SVN_VERSION_BODY; +} + + + +svn_error_t * +svn_repos_stat(svn_dirent_t **dirent, + svn_fs_root_t *root, + const char *path, + apr_pool_t *pool) +{ + svn_node_kind_t kind; + svn_dirent_t *ent; + const char *datestring; + apr_hash_t *prophash; + + SVN_ERR(svn_fs_check_path(&kind, root, path, pool)); + + if (kind == svn_node_none) + { + *dirent = NULL; + return SVN_NO_ERROR; + } + + ent = svn_dirent_create(pool); + ent->kind = kind; + + if (kind == svn_node_file) + SVN_ERR(svn_fs_file_length(&(ent->size), root, path, pool)); + + SVN_ERR(svn_fs_node_proplist(&prophash, root, path, pool)); + if (apr_hash_count(prophash) > 0) + ent->has_props = TRUE; + + SVN_ERR(svn_repos_get_committed_info(&(ent->created_rev), + &datestring, + &(ent->last_author), + root, path, pool)); + if (datestring) + SVN_ERR(svn_time_from_cstring(&(ent->time), datestring, pool)); + + *dirent = ent; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos_remember_client_capabilities(svn_repos_t *repos, + const apr_array_header_t *capabilities) +{ + repos->client_capabilities = capabilities; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos__fs_type(const char **fs_type, + const char *repos_path, + apr_pool_t *pool) +{ + svn_repos_t repos; + repos.path = (char*)repos_path; + + SVN_ERR(check_repos_format(&repos, pool)); + + return svn_fs_type(fs_type, + svn_dirent_join(repos_path, SVN_REPOS__DB_DIR, pool), + pool); +} diff --git a/subversion/libsvn_repos/repos.h b/subversion/libsvn_repos/repos.h new file mode 100644 index 0000000..fd5b0b4 --- /dev/null +++ b/subversion/libsvn_repos/repos.h @@ -0,0 +1,425 @@ +/* repos.h : interface to Subversion repository, private to libsvn_repos + * + * ==================================================================== + * 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. + * ==================================================================== + */ + +#ifndef SVN_LIBSVN_REPOS_H +#define SVN_LIBSVN_REPOS_H + +#include <apr_pools.h> +#include <apr_hash.h> + +#include "svn_fs.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + +/* Repository format number. + + Formats 0, 1 and 2 were pre-1.0. + + Format 3 was current for 1.0 through to 1.3. + + Format 4 was an abortive experiment during the development of the + locking feature in the lead up to 1.2. + + Format 5 was new in 1.4, and is the first format which may contain + BDB or FSFS filesystems with a FS format other than 1, since prior + formats are accepted by some versions of Subversion which do not + pay attention to the FS format number. +*/ +#define SVN_REPOS__FORMAT_NUMBER SVN_REPOS__FORMAT_NUMBER_1_4 +#define SVN_REPOS__FORMAT_NUMBER_1_4 5 +#define SVN_REPOS__FORMAT_NUMBER_LEGACY 3 + + +/*** Repository layout. ***/ + +/* The top-level repository dir contains a README and various + subdirectories. */ +#define SVN_REPOS__README "README.txt" /* Explanation for trespassers. */ +#define SVN_REPOS__FORMAT "format" /* Stores the current version + of the repository. */ +#define SVN_REPOS__DB_DIR "db" /* Where Berkeley lives. */ +#define SVN_REPOS__DAV_DIR "dav" /* DAV sandbox, for pre-1.5 */ +#define SVN_REPOS__LOCK_DIR "locks" /* Lock files live here. */ +#define SVN_REPOS__HOOK_DIR "hooks" /* Hook programs. */ +#define SVN_REPOS__CONF_DIR "conf" /* Configuration files. */ + +/* Things for which we keep lockfiles. */ +#define SVN_REPOS__DB_LOCKFILE "db.lock" /* Our Berkeley lockfile. */ +#define SVN_REPOS__DB_LOGS_LOCKFILE "db-logs.lock" /* BDB logs lockfile. */ + +/* In the repository hooks directory, look for these files. */ +#define SVN_REPOS__HOOK_START_COMMIT "start-commit" +#define SVN_REPOS__HOOK_PRE_COMMIT "pre-commit" +#define SVN_REPOS__HOOK_POST_COMMIT "post-commit" +#define SVN_REPOS__HOOK_READ_SENTINEL "read-sentinels" +#define SVN_REPOS__HOOK_WRITE_SENTINEL "write-sentinels" +#define SVN_REPOS__HOOK_PRE_REVPROP_CHANGE "pre-revprop-change" +#define SVN_REPOS__HOOK_POST_REVPROP_CHANGE "post-revprop-change" +#define SVN_REPOS__HOOK_PRE_LOCK "pre-lock" +#define SVN_REPOS__HOOK_POST_LOCK "post-lock" +#define SVN_REPOS__HOOK_PRE_UNLOCK "pre-unlock" +#define SVN_REPOS__HOOK_POST_UNLOCK "post-unlock" + + +/* The extension added to the names of example hook scripts. */ +#define SVN_REPOS__HOOK_DESC_EXT ".tmpl" + +/* The file which contains a custom set of environment variables + * passed inherited to hook scripts, in the repository conf directory. */ +#define SVN_REPOS__CONF_HOOKS_ENV "hooks-env" +/* The name of the default section in the hooks-env config file. */ +#define SVN_REPOS__HOOKS_ENV_DEFAULT_SECTION "default" + +/* The configuration file for svnserve, in the repository conf directory. */ +#define SVN_REPOS__CONF_SVNSERVE_CONF "svnserve.conf" + +/* In the svnserve default configuration, these are the suggested + locations for the passwd, authz and groups files (in the repository + conf directory), and we put example templates there. */ +#define SVN_REPOS__CONF_PASSWD "passwd" +#define SVN_REPOS__CONF_AUTHZ "authz" +#define SVN_REPOS__CONF_GROUPS "groups" + +/* The Repository object, created by svn_repos_open2() and + svn_repos_create(). */ +struct svn_repos_t +{ + /* A Subversion filesystem object. */ + svn_fs_t *fs; + + /* The path to the repository's top-level directory. */ + char *path; + + /* The path to the repository's conf directory. */ + char *conf_path; + + /* The path to the repository's hooks directory. */ + char *hook_path; + + /* The path to the repository's locks directory. */ + char *lock_path; + + /* The path to the Berkeley DB filesystem environment. */ + char *db_path; + + /* The format number of this repository. */ + int format; + + /* The path to the repository's hooks enviroment file. If NULL, hooks run + * in an empty environment. */ + const char *hooks_env_path; + + /* The FS backend in use within this repository. */ + const char *fs_type; + + /* If non-null, a list of all the capabilities the client (on the + current connection) has self-reported. Each element is a + 'const char *', one of SVN_RA_CAPABILITY_*. + + Note: it is somewhat counterintuitive that we store the client's + capabilities, which are session-specific, on the repository + object. You'd think the capabilities here would represent the + *repository's* capabilities, but no, they represent the + client's -- we just don't have any other place to persist them. */ + const apr_array_header_t *client_capabilities; + + /* Maps SVN_REPOS_CAPABILITY_foo keys to "yes" or "no" values. + If a capability is not yet discovered, it is absent from the table. + Most likely the keys and values are constants anyway (and + sufficiently well-informed internal code may just compare against + those constants' addresses, therefore). */ + apr_hash_t *repository_capabilities; + + /* Pool from which this structure was allocated. Also used for + auxiliary repository-related data that requires a matching + lifespan. (As the svn_repos_t structure tends to be relatively + long-lived, please be careful regarding this pool's usage.) */ + apr_pool_t *pool; +}; + + +/*** Hook-running Functions ***/ + +/* Set *HOOKS_ENV_P to the parsed contents of the hooks-env file + LOCAL_ABSPATH, allocated in RESULT_POOL. (This result is suitable + for delivery to the various hook wrapper functions which accept a + 'hooks_env' parameter.) If LOCAL_ABSPATH is NULL, set *HOOKS_ENV_P + to NULL. + + Use SCRATCH_POOL for temporary allocations. */ +svn_error_t * +svn_repos__parse_hooks_env(apr_hash_t **hooks_env_p, + const char *local_abspath, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + +/* Run the start-commit hook for REPOS. Use POOL for any temporary + allocations. If the hook fails, return SVN_ERR_REPOS_HOOK_FAILURE. + + HOOKS_ENV is a hash of hook script environment information returned + via svn_repos__parse_hooks_env() (or NULL if no such information is + available). + + USER is the authenticated name of the user starting the commit. + + CAPABILITIES is a list of 'const char *' capability names (using + SVN_RA_CAPABILITY_*) that the client has self-reported. Note that + there is no guarantee the client is telling the truth: the hook + should not make security assumptions based on the capabilities. + + TXN_NAME is the name of the commit transaction that's just been + created. */ +svn_error_t * +svn_repos__hooks_start_commit(svn_repos_t *repos, + apr_hash_t *hooks_env, + const char *user, + const apr_array_header_t *capabilities, + const char *txn_name, + apr_pool_t *pool); + +/* Run the pre-commit hook for REPOS. Use POOL for any temporary + allocations. If the hook fails, return SVN_ERR_REPOS_HOOK_FAILURE. + + HOOKS_ENV is a hash of hook script environment information returned + via svn_repos__parse_hooks_env() (or NULL if no such information is + available). + + TXN_NAME is the name of the transaction that is being committed. */ +svn_error_t * +svn_repos__hooks_pre_commit(svn_repos_t *repos, + apr_hash_t *hooks_env, + const char *txn_name, + apr_pool_t *pool); + +/* Run the post-commit hook for REPOS. Use POOL for any temporary + allocations. If the hook fails, run SVN_ERR_REPOS_HOOK_FAILURE. + + HOOKS_ENV is a hash of hook script environment information returned + via svn_repos__parse_hooks_env() (or NULL if no such information is + available). + + REV is the revision that was created as a result of the commit. */ +svn_error_t * +svn_repos__hooks_post_commit(svn_repos_t *repos, + apr_hash_t *hooks_env, + svn_revnum_t rev, + const char *txn_name, + apr_pool_t *pool); + +/* Run the pre-revprop-change hook for REPOS. Use POOL for any + temporary allocations. If the hook fails, return + SVN_ERR_REPOS_HOOK_FAILURE. + + HOOKS_ENV is a hash of hook script environment information returned + via svn_repos__parse_hooks_env() (or NULL if no such information is + available). + + REV is the revision whose property is being changed. + AUTHOR is the authenticated name of the user changing the prop. + NAME is the name of the property being changed. + NEW_VALUE is the new value of the property. + ACTION is indicates if the property is being 'A'dded, 'M'odified, + or 'D'eleted. + + The pre-revprop-change hook will have the new property value + written to its stdin. If the property is being deleted, no data + will be written. */ +svn_error_t * +svn_repos__hooks_pre_revprop_change(svn_repos_t *repos, + apr_hash_t *hooks_env, + svn_revnum_t rev, + const char *author, + const char *name, + const svn_string_t *new_value, + char action, + apr_pool_t *pool); + +/* Run the pre-revprop-change hook for REPOS. Use POOL for any + temporary allocations. If the hook fails, return + SVN_ERR_REPOS_HOOK_FAILURE. + + HOOKS_ENV is a hash of hook script environment information returned + via svn_repos__parse_hooks_env() (or NULL if no such information is + available). + + REV is the revision whose property was changed. + AUTHOR is the authenticated name of the user who changed the prop. + NAME is the name of the property that was changed, and OLD_VALUE is + that property's value immediately before the change, or null if + none. ACTION indicates if the property was 'A'dded, 'M'odified, + or 'D'eleted. + + The old value will be passed to the post-revprop hook on stdin. If + the property is being created, no data will be written. */ +svn_error_t * +svn_repos__hooks_post_revprop_change(svn_repos_t *repos, + apr_hash_t *hooks_env, + svn_revnum_t rev, + const char *author, + const char *name, + const svn_string_t *old_value, + char action, + apr_pool_t *pool); + +/* Run the pre-lock hook for REPOS. Use POOL for any temporary + allocations. If the hook fails, return SVN_ERR_REPOS_HOOK_FAILURE. + + HOOKS_ENV is a hash of hook script environment information returned + via svn_repos__parse_hooks_env() (or NULL if no such information is + available). + + PATH is the path being locked, USERNAME is the person doing it, + COMMENT is the comment of the lock, and is treated as an empty + string when NULL is given. STEAL-LOCK is a flag if the user is + stealing the lock. + + If TOKEN is non-null, set *TOKEN to a new lock token generated by + the pre-lock hook, if any (see the pre-lock hook template for more + information). If TOKEN is non-null but the hook does not return + any token, then set *TOKEN to empty string. */ + +svn_error_t * +svn_repos__hooks_pre_lock(svn_repos_t *repos, + apr_hash_t *hooks_env, + const char **token, + const char *path, + const char *username, + const char *comment, + svn_boolean_t steal_lock, + apr_pool_t *pool); + +/* Run the post-lock hook for REPOS. Use POOL for any temporary + allocations. If the hook fails, return SVN_ERR_REPOS_HOOK_FAILURE. + + HOOKS_ENV is a hash of hook script environment information returned + via svn_repos__parse_hooks_env() (or NULL if no such information is + available). + + PATHS is an array of paths being locked, USERNAME is the person + who did it. */ +svn_error_t * +svn_repos__hooks_post_lock(svn_repos_t *repos, + apr_hash_t *hooks_env, + const apr_array_header_t *paths, + const char *username, + apr_pool_t *pool); + +/* Run the pre-unlock hook for REPOS. Use POOL for any temporary + allocations. If the hook fails, return SVN_ERR_REPOS_HOOK_FAILURE. + + HOOKS_ENV is a hash of hook script environment information returned + via svn_repos__parse_hooks_env() (or NULL if no such information is + available). + + PATH is the path being unlocked, USERNAME is the person doing it, + TOKEN is the lock token to be unlocked which should not be NULL, + and BREAK-LOCK is a flag if the user is breaking the lock. */ +svn_error_t * +svn_repos__hooks_pre_unlock(svn_repos_t *repos, + apr_hash_t *hooks_env, + const char *path, + const char *username, + const char *token, + svn_boolean_t break_lock, + apr_pool_t *pool); + +/* Run the post-unlock hook for REPOS. Use POOL for any temporary + allocations. If the hook fails, return SVN_ERR_REPOS_HOOK_FAILURE. + + HOOKS_ENV is a hash of hook script environment information returned + via svn_repos__parse_hooks_env() (or NULL if no such information is + available). + + PATHS is an array of paths being unlocked, USERNAME is the person + who did it. */ +svn_error_t * +svn_repos__hooks_post_unlock(svn_repos_t *repos, + apr_hash_t *hooks_env, + const apr_array_header_t *paths, + const char *username, + apr_pool_t *pool); + + +/*** Authz Functions ***/ + +/* Read authz configuration data from PATH into *AUTHZ_P, allocated + in POOL. If GROUPS_PATH is set, use the global groups parsed from it. + + PATH and GROUPS_PATH may be a dirent or a registry path and iff ACCEPT_URLS + is set it may also be an absolute file url. + + If PATH or GROUPS_PATH is not a valid authz rule file, then return + SVN_AUTHZ_INVALID_CONFIG. The contents of *AUTHZ_P is then + undefined. If MUST_EXIST is TRUE, a missing authz or global groups file + is also an error. */ +svn_error_t * +svn_repos__authz_read(svn_authz_t **authz_p, + const char *path, + const char *groups_path, + svn_boolean_t must_exist, + svn_boolean_t accept_urls, + apr_pool_t *pool); + + +/*** Utility Functions ***/ + +/* Set *CHANGED_P to TRUE if ROOT1/PATH1 and ROOT2/PATH2 have + different contents, FALSE if they have the same contents. + Use POOL for temporary allocation. */ +svn_error_t * +svn_repos__compare_files(svn_boolean_t *changed_p, + svn_fs_root_t *root1, + const char *path1, + svn_fs_root_t *root2, + const char *path2, + apr_pool_t *pool); + +/* Set *PREV_PATH and *PREV_REV to the path and revision which + represent the location at which PATH in FS was located immediately + prior to REVISION iff there was a copy operation (to PATH or one of + its parent directories) between that previous location and + PATH@REVISION, and set *APPEARED_REV to the first revision in which + PATH@REVISION appeared at PATH as a result of that copy operation. + + If there was no such copy operation in that portion + of PATH's history, set *PREV_PATH to NULL, and set *PREV_REV and + *APPEARED_REV to SVN_INVALID_REVNUM. + + NOTE: Any of PREV_PATH, PREV_REV, and APPEARED_REV may be NULL to + if that information is of no interest to the caller. */ +svn_error_t * +svn_repos__prev_location(svn_revnum_t *appeared_rev, + const char **prev_path, + svn_revnum_t *prev_rev, + svn_fs_t *fs, + svn_revnum_t revision, + const char *path, + apr_pool_t *pool); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* SVN_LIBSVN_REPOS_H */ diff --git a/subversion/libsvn_repos/rev_hunt.c b/subversion/libsvn_repos/rev_hunt.c new file mode 100644 index 0000000..77b1f2a --- /dev/null +++ b/subversion/libsvn_repos/rev_hunt.c @@ -0,0 +1,1699 @@ +/* rev_hunt.c --- routines to hunt down particular fs revisions and + * their properties. + * + * ==================================================================== + * 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 <string.h> +#include "svn_compat.h" +#include "svn_private_config.h" +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_error_codes.h" +#include "svn_fs.h" +#include "svn_repos.h" +#include "svn_string.h" +#include "svn_time.h" +#include "svn_sorts.h" +#include "svn_props.h" +#include "svn_mergeinfo.h" +#include "repos.h" +#include "private/svn_fspath.h" + + +/* Note: this binary search assumes that the datestamp properties on + each revision are in chronological order. That is if revision A > + revision B, then A's datestamp is younger then B's datestamp. + + If someone comes along and sets a bogus datestamp, this routine + might not work right. + + ### todo: you know, we *could* have svn_fs_change_rev_prop() do + some semantic checking when it's asked to change special reserved + svn: properties. It could prevent such a problem. */ + + +/* helper for svn_repos_dated_revision(). + + Set *TM to the apr_time_t datestamp on revision REV in FS. */ +static svn_error_t * +get_time(apr_time_t *tm, + svn_fs_t *fs, + svn_revnum_t rev, + apr_pool_t *pool) +{ + svn_string_t *date_str; + + SVN_ERR(svn_fs_revision_prop(&date_str, fs, rev, SVN_PROP_REVISION_DATE, + pool)); + if (! date_str) + return svn_error_createf + (SVN_ERR_FS_GENERAL, NULL, + _("Failed to find time on revision %ld"), rev); + + return svn_time_from_cstring(tm, date_str->data, pool); +} + + +svn_error_t * +svn_repos_dated_revision(svn_revnum_t *revision, + svn_repos_t *repos, + apr_time_t tm, + apr_pool_t *pool) +{ + svn_revnum_t rev_mid, rev_top, rev_bot, rev_latest; + apr_time_t this_time; + svn_fs_t *fs = repos->fs; + + /* Initialize top and bottom values of binary search. */ + SVN_ERR(svn_fs_youngest_rev(&rev_latest, fs, pool)); + rev_bot = 0; + rev_top = rev_latest; + + while (rev_bot <= rev_top) + { + rev_mid = (rev_top + rev_bot) / 2; + SVN_ERR(get_time(&this_time, fs, rev_mid, pool)); + + if (this_time > tm)/* we've overshot */ + { + apr_time_t previous_time; + + if ((rev_mid - 1) < 0) + { + *revision = 0; + break; + } + + /* see if time falls between rev_mid and rev_mid-1: */ + SVN_ERR(get_time(&previous_time, fs, rev_mid - 1, pool)); + if (previous_time <= tm) + { + *revision = rev_mid - 1; + break; + } + + rev_top = rev_mid - 1; + } + + else if (this_time < tm) /* we've undershot */ + { + apr_time_t next_time; + + if ((rev_mid + 1) > rev_latest) + { + *revision = rev_latest; + break; + } + + /* see if time falls between rev_mid and rev_mid+1: */ + SVN_ERR(get_time(&next_time, fs, rev_mid + 1, pool)); + if (next_time > tm) + { + *revision = rev_mid; + break; + } + + rev_bot = rev_mid + 1; + } + + else + { + *revision = rev_mid; /* exact match! */ + break; + } + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_get_committed_info(svn_revnum_t *committed_rev, + const char **committed_date, + const char **last_author, + svn_fs_root_t *root, + const char *path, + apr_pool_t *pool) +{ + apr_hash_t *revprops; + + svn_fs_t *fs = svn_fs_root_fs(root); + + /* ### It might be simpler just to declare that revision + properties have char * (i.e., UTF-8) values, not arbitrary + binary values, hmmm. */ + svn_string_t *committed_date_s, *last_author_s; + + /* Get the CR field out of the node's skel. */ + SVN_ERR(svn_fs_node_created_rev(committed_rev, root, path, pool)); + + /* Get the revision properties of this revision. */ + SVN_ERR(svn_fs_revision_proplist(&revprops, fs, *committed_rev, pool)); + + /* Extract date and author from these revprops. */ + committed_date_s = apr_hash_get(revprops, + SVN_PROP_REVISION_DATE, + sizeof(SVN_PROP_REVISION_DATE)-1); + last_author_s = apr_hash_get(revprops, + SVN_PROP_REVISION_AUTHOR, + sizeof(SVN_PROP_REVISION_AUTHOR)-1); + + *committed_date = committed_date_s ? committed_date_s->data : NULL; + *last_author = last_author_s ? last_author_s->data : NULL; + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos_history2(svn_fs_t *fs, + const char *path, + svn_repos_history_func_t history_func, + void *history_baton, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_revnum_t start, + svn_revnum_t end, + svn_boolean_t cross_copies, + apr_pool_t *pool) +{ + svn_fs_history_t *history; + apr_pool_t *oldpool = svn_pool_create(pool); + apr_pool_t *newpool = svn_pool_create(pool); + const char *history_path; + svn_revnum_t history_rev; + svn_fs_root_t *root; + + /* Validate the revisions. */ + if (! SVN_IS_VALID_REVNUM(start)) + return svn_error_createf + (SVN_ERR_FS_NO_SUCH_REVISION, 0, + _("Invalid start revision %ld"), start); + if (! SVN_IS_VALID_REVNUM(end)) + return svn_error_createf + (SVN_ERR_FS_NO_SUCH_REVISION, 0, + _("Invalid end revision %ld"), end); + + /* Ensure that the input is ordered. */ + if (start > end) + { + svn_revnum_t tmprev = start; + start = end; + end = tmprev; + } + + /* Get a revision root for END, and an initial HISTORY baton. */ + SVN_ERR(svn_fs_revision_root(&root, fs, end, pool)); + + if (authz_read_func) + { + svn_boolean_t readable; + SVN_ERR(authz_read_func(&readable, root, path, + authz_read_baton, pool)); + if (! readable) + return svn_error_create(SVN_ERR_AUTHZ_UNREADABLE, NULL, NULL); + } + + SVN_ERR(svn_fs_node_history(&history, root, path, oldpool)); + + /* Now, we loop over the history items, calling svn_fs_history_prev(). */ + do + { + /* Note that we have to do some crazy pool work here. We can't + get rid of the old history until we use it to get the new, so + we alternate back and forth between our subpools. */ + apr_pool_t *tmppool; + svn_error_t *err; + + SVN_ERR(svn_fs_history_prev(&history, history, cross_copies, newpool)); + + /* Only continue if there is further history to deal with. */ + if (! history) + break; + + /* Fetch the location information for this history step. */ + SVN_ERR(svn_fs_history_location(&history_path, &history_rev, + history, newpool)); + + /* If this history item predates our START revision, quit + here. */ + if (history_rev < start) + break; + + /* Is the history item readable? If not, quit. */ + if (authz_read_func) + { + svn_boolean_t readable; + svn_fs_root_t *history_root; + SVN_ERR(svn_fs_revision_root(&history_root, fs, + history_rev, newpool)); + SVN_ERR(authz_read_func(&readable, history_root, history_path, + authz_read_baton, newpool)); + if (! readable) + break; + } + + /* Call the user-provided callback function. */ + err = history_func(history_baton, history_path, history_rev, newpool); + if (err) + { + if (err->apr_err == SVN_ERR_CEASE_INVOCATION) + { + svn_error_clear(err); + goto cleanup; + } + else + { + return svn_error_trace(err); + } + } + + /* We're done with the old history item, so we can clear its + pool, and then toggle our notion of "the old pool". */ + svn_pool_clear(oldpool); + tmppool = oldpool; + oldpool = newpool; + newpool = tmppool; + } + while (history); /* shouldn't hit this */ + + cleanup: + svn_pool_destroy(oldpool); + svn_pool_destroy(newpool); + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_deleted_rev(svn_fs_t *fs, + const char *path, + svn_revnum_t start, + svn_revnum_t end, + svn_revnum_t *deleted, + apr_pool_t *pool) +{ + apr_pool_t *subpool; + svn_fs_root_t *root, *copy_root; + const char *copy_path; + svn_revnum_t mid_rev; + const svn_fs_id_t *start_node_id, *curr_node_id; + svn_error_t *err; + + /* Validate the revision range. */ + if (! SVN_IS_VALID_REVNUM(start)) + return svn_error_createf + (SVN_ERR_FS_NO_SUCH_REVISION, 0, + _("Invalid start revision %ld"), start); + if (! SVN_IS_VALID_REVNUM(end)) + return svn_error_createf + (SVN_ERR_FS_NO_SUCH_REVISION, 0, + _("Invalid end revision %ld"), end); + + /* Ensure that the input is ordered. */ + if (start > end) + { + svn_revnum_t tmprev = start; + start = end; + end = tmprev; + } + + /* Ensure path exists in fs at start revision. */ + SVN_ERR(svn_fs_revision_root(&root, fs, start, pool)); + err = svn_fs_node_id(&start_node_id, root, path, pool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + /* Path must exist in fs at start rev. */ + *deleted = SVN_INVALID_REVNUM; + svn_error_clear(err); + return SVN_NO_ERROR; + } + return svn_error_trace(err); + } + + /* Ensure path was deleted at or before end revision. */ + SVN_ERR(svn_fs_revision_root(&root, fs, end, pool)); + err = svn_fs_node_id(&curr_node_id, root, path, pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + } + else if (err) + { + return svn_error_trace(err); + } + else + { + /* path exists in the end node and the end node is equivalent + or otherwise equivalent to the start node. This can mean + a few things: + + 1) The end node *is* simply the start node, uncopied + and unmodified in the start to end range. + + 2) The start node was modified, but never copied. + + 3) The start node was copied, but this copy occurred at + start or some rev *previous* to start, this is + effectively the same situation as 1 if the node was + never modified or 2 if it was. + + In the first three cases the path was not deleted in + the specified range and we are done, but in the following + cases the start node must have been deleted at least once: + + 4) The start node was deleted and replaced by a copy of + itself at some rev between start and end. This copy + may itself have been replaced with copies of itself. + + 5) The start node was deleted and replaced by a node which + it does not share any history with. + */ + SVN_ERR(svn_fs_node_id(&curr_node_id, root, path, pool)); + if (svn_fs_compare_ids(start_node_id, curr_node_id) != -1) + { + SVN_ERR(svn_fs_closest_copy(©_root, ©_path, root, + path, pool)); + if (!copy_root || + (svn_fs_revision_root_revision(copy_root) <= start)) + { + /* Case 1,2 or 3, nothing more to do. */ + *deleted = SVN_INVALID_REVNUM; + return SVN_NO_ERROR; + } + } + } + + /* If we get here we know that path exists in rev start and was deleted + at least once before rev end. To find the revision path was first + deleted we use a binary search. The rules for the determining if + the deletion comes before or after a given median revision are + described by this matrix: + + | Most recent copy event that + | caused mid node to exist. + |----------------------------------------------------- + Compare path | | | | + at start and | Copied at | Copied at | Never copied | + mid nodes. | rev > start | rev <= start | | + | | | | + -------------------------------------------------------------------| + Mid node is | A) Start node | | + equivalent to | replaced with | E) Mid node == start node, | + start node | an unmodified | look HIGHER. | + | copy of | | + | itself, | | + | look LOWER. | | + -------------------------------------------------------------------| + Mid node is | B) Start node | | + otherwise | replaced with | F) Mid node is a modified | + related to | a modified | version of start node, | + start node | copy of | look HIGHER. | + | itself, | | + | look LOWER. | | + -------------------------------------------------------------------| + Mid node is | | + unrelated to | C) Start node replaced with unrelated mid node, | + start node | look LOWER. | + | | + -------------------------------------------------------------------| + Path doesn't | | + exist at mid | D) Start node deleted before mid node, | + node | look LOWER | + | | + -------------------------------------------------------------------- + */ + + mid_rev = (start + end) / 2; + subpool = svn_pool_create(pool); + + while (1) + { + svn_pool_clear(subpool); + + /* Get revision root and node id for mid_rev at that revision. */ + SVN_ERR(svn_fs_revision_root(&root, fs, mid_rev, subpool)); + err = svn_fs_node_id(&curr_node_id, root, path, subpool); + + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + /* Case D: Look lower in the range. */ + svn_error_clear(err); + end = mid_rev; + mid_rev = (start + mid_rev) / 2; + } + else + return svn_error_trace(err); + } + else + { + /* Determine the relationship between the start node + and the current node. */ + int cmp = svn_fs_compare_ids(start_node_id, curr_node_id); + SVN_ERR(svn_fs_closest_copy(©_root, ©_path, root, + path, subpool)); + if (cmp == -1 || + (copy_root && + (svn_fs_revision_root_revision(copy_root) > start))) + { + /* Cases A, B, C: Look at lower revs. */ + end = mid_rev; + mid_rev = (start + mid_rev) / 2; + } + else if (end - mid_rev == 1) + { + /* Found the node path was deleted. */ + *deleted = end; + break; + } + else + { + /* Cases E, F: Look at higher revs. */ + start = mid_rev; + mid_rev = (start + end) / 2; + } + } + } + + svn_pool_destroy(subpool); + return SVN_NO_ERROR; +} + + +/* Helper func: return SVN_ERR_AUTHZ_UNREADABLE if ROOT/PATH is + unreadable. */ +static svn_error_t * +check_readability(svn_fs_root_t *root, + const char *path, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + svn_boolean_t readable; + SVN_ERR(authz_read_func(&readable, root, path, authz_read_baton, pool)); + if (! readable) + return svn_error_create(SVN_ERR_AUTHZ_UNREADABLE, NULL, + _("Unreadable path encountered; access denied")); + return SVN_NO_ERROR; +} + + +/* The purpose of this function is to discover if fs_path@future_rev + * is derived from fs_path@peg_rev. The return is placed in *is_ancestor. */ + +static svn_error_t * +check_ancestry_of_peg_path(svn_boolean_t *is_ancestor, + svn_fs_t *fs, + const char *fs_path, + svn_revnum_t peg_revision, + svn_revnum_t future_revision, + apr_pool_t *pool) +{ + svn_fs_root_t *root; + svn_fs_history_t *history; + const char *path = NULL; + svn_revnum_t revision; + apr_pool_t *lastpool, *currpool; + + lastpool = svn_pool_create(pool); + currpool = svn_pool_create(pool); + + SVN_ERR(svn_fs_revision_root(&root, fs, future_revision, pool)); + + SVN_ERR(svn_fs_node_history(&history, root, fs_path, lastpool)); + + /* Since paths that are different according to strcmp may still be + equivalent (due to number of consecutive slashes and the fact that + "" is the same as "/"), we get the "canonical" path in the first + iteration below so that the comparison after the loop will work + correctly. */ + fs_path = NULL; + + while (1) + { + apr_pool_t *tmppool; + + SVN_ERR(svn_fs_history_prev(&history, history, TRUE, currpool)); + + if (!history) + break; + + SVN_ERR(svn_fs_history_location(&path, &revision, history, currpool)); + + if (!fs_path) + fs_path = apr_pstrdup(pool, path); + + if (revision <= peg_revision) + break; + + /* Clear old pool and flip. */ + svn_pool_clear(lastpool); + tmppool = lastpool; + lastpool = currpool; + currpool = tmppool; + } + + /* We must have had at least one iteration above where we + reassigned fs_path. Else, the path wouldn't have existed at + future_revision and svn_fs_history would have thrown. */ + SVN_ERR_ASSERT(fs_path != NULL); + + *is_ancestor = (history && strcmp(path, fs_path) == 0); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos__prev_location(svn_revnum_t *appeared_rev, + const char **prev_path, + svn_revnum_t *prev_rev, + svn_fs_t *fs, + svn_revnum_t revision, + const char *path, + apr_pool_t *pool) +{ + svn_fs_root_t *root, *copy_root; + const char *copy_path, *copy_src_path, *remainder; + svn_revnum_t copy_src_rev; + + /* Initialize return variables. */ + if (appeared_rev) + *appeared_rev = SVN_INVALID_REVNUM; + if (prev_rev) + *prev_rev = SVN_INVALID_REVNUM; + if (prev_path) + *prev_path = NULL; + + /* Ask about the most recent copy which affected PATH@REVISION. If + there was no such copy, we're done. */ + SVN_ERR(svn_fs_revision_root(&root, fs, revision, pool)); + SVN_ERR(svn_fs_closest_copy(©_root, ©_path, root, path, pool)); + if (! copy_root) + return SVN_NO_ERROR; + + /* Ultimately, it's not the path of the closest copy's source that + we care about -- it's our own path's location in the copy source + revision. So we'll tack the relative path that expresses the + difference between the copy destination and our path in the copy + revision onto the copy source path to determine this information. + + In other words, if our path is "/branches/my-branch/foo/bar", and + we know that the closest relevant copy was a copy of "/trunk" to + "/branches/my-branch", then that relative path under the copy + destination is "/foo/bar". Tacking that onto the copy source + path tells us that our path was located at "/trunk/foo/bar" + before the copy. + */ + SVN_ERR(svn_fs_copied_from(©_src_rev, ©_src_path, + copy_root, copy_path, pool)); + remainder = svn_fspath__skip_ancestor(copy_path, path); + if (prev_path) + *prev_path = svn_fspath__join(copy_src_path, remainder, pool); + if (appeared_rev) + *appeared_rev = svn_fs_revision_root_revision(copy_root); + if (prev_rev) + *prev_rev = copy_src_rev; + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_trace_node_locations(svn_fs_t *fs, + apr_hash_t **locations, + const char *fs_path, + svn_revnum_t peg_revision, + const apr_array_header_t *location_revisions_orig, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + apr_array_header_t *location_revisions; + svn_revnum_t *revision_ptr, *revision_ptr_end; + svn_fs_root_t *root; + const char *path; + svn_revnum_t revision; + svn_boolean_t is_ancestor; + apr_pool_t *lastpool, *currpool; + const svn_fs_id_t *id; + + SVN_ERR_ASSERT(location_revisions_orig->elt_size == sizeof(svn_revnum_t)); + + /* Ensure that FS_PATH is absolute, because our path-math below will + depend on that being the case. */ + if (*fs_path != '/') + fs_path = apr_pstrcat(pool, "/", fs_path, (char *)NULL); + + /* Another sanity check. */ + if (authz_read_func) + { + svn_fs_root_t *peg_root; + SVN_ERR(svn_fs_revision_root(&peg_root, fs, peg_revision, pool)); + SVN_ERR(check_readability(peg_root, fs_path, + authz_read_func, authz_read_baton, pool)); + } + + *locations = apr_hash_make(pool); + + /* We flip between two pools in the second loop below. */ + lastpool = svn_pool_create(pool); + currpool = svn_pool_create(pool); + + /* First - let's sort the array of the revisions from the greatest revision + * downward, so it will be easier to search on. */ + location_revisions = apr_array_copy(pool, location_revisions_orig); + qsort(location_revisions->elts, location_revisions->nelts, + sizeof(*revision_ptr), svn_sort_compare_revisions); + + revision_ptr = (svn_revnum_t *)location_revisions->elts; + revision_ptr_end = revision_ptr + location_revisions->nelts; + + /* Ignore revisions R that are younger than the peg_revisions where + path@peg_revision is not an ancestor of path@R. */ + is_ancestor = FALSE; + while (revision_ptr < revision_ptr_end && *revision_ptr > peg_revision) + { + svn_pool_clear(currpool); + SVN_ERR(check_ancestry_of_peg_path(&is_ancestor, fs, fs_path, + peg_revision, *revision_ptr, + currpool)); + if (is_ancestor) + break; + ++revision_ptr; + } + + revision = is_ancestor ? *revision_ptr : peg_revision; + path = fs_path; + if (authz_read_func) + { + SVN_ERR(svn_fs_revision_root(&root, fs, revision, pool)); + SVN_ERR(check_readability(root, fs_path, authz_read_func, + authz_read_baton, pool)); + } + + while (revision_ptr < revision_ptr_end) + { + apr_pool_t *tmppool; + svn_revnum_t appeared_rev, prev_rev; + const char *prev_path; + + /* Find the target of the innermost copy relevant to path@revision. + The copy may be of path itself, or of a parent directory. */ + SVN_ERR(svn_repos__prev_location(&appeared_rev, &prev_path, &prev_rev, + fs, revision, path, currpool)); + if (! prev_path) + break; + + if (authz_read_func) + { + svn_boolean_t readable; + svn_fs_root_t *tmp_root; + + SVN_ERR(svn_fs_revision_root(&tmp_root, fs, revision, currpool)); + SVN_ERR(authz_read_func(&readable, tmp_root, path, + authz_read_baton, currpool)); + if (! readable) + { + svn_pool_destroy(lastpool); + svn_pool_destroy(currpool); + + return SVN_NO_ERROR; + } + } + + /* Assign the current path to all younger revisions until we reach + the copy target rev. */ + while ((revision_ptr < revision_ptr_end) + && (*revision_ptr >= appeared_rev)) + { + /* *revision_ptr is allocated out of pool, so we can point + to in the hash table. */ + apr_hash_set(*locations, revision_ptr, sizeof(*revision_ptr), + apr_pstrdup(pool, path)); + revision_ptr++; + } + + /* Ignore all revs between the copy target rev and the copy + source rev (non-inclusive). */ + while ((revision_ptr < revision_ptr_end) + && (*revision_ptr > prev_rev)) + revision_ptr++; + + /* State update. */ + path = prev_path; + revision = prev_rev; + + /* Clear last pool and switch. */ + svn_pool_clear(lastpool); + tmppool = lastpool; + lastpool = currpool; + currpool = tmppool; + } + + /* There are no copies relevant to path@revision. So any remaining + revisions either predate the creation of path@revision or have + the node existing at the same path. We will look up path@lrev + for each remaining location-revision and make sure it is related + to path@revision. */ + SVN_ERR(svn_fs_revision_root(&root, fs, revision, currpool)); + SVN_ERR(svn_fs_node_id(&id, root, path, pool)); + while (revision_ptr < revision_ptr_end) + { + svn_node_kind_t kind; + const svn_fs_id_t *lrev_id; + + svn_pool_clear(currpool); + SVN_ERR(svn_fs_revision_root(&root, fs, *revision_ptr, currpool)); + SVN_ERR(svn_fs_check_path(&kind, root, path, currpool)); + if (kind == svn_node_none) + break; + SVN_ERR(svn_fs_node_id(&lrev_id, root, path, currpool)); + if (! svn_fs_check_related(id, lrev_id)) + break; + + /* The node exists at the same path; record that and advance. */ + apr_hash_set(*locations, revision_ptr, sizeof(*revision_ptr), + apr_pstrdup(pool, path)); + revision_ptr++; + } + + /* Ignore any remaining location-revisions; they predate the + creation of path@revision. */ + + svn_pool_destroy(lastpool); + svn_pool_destroy(currpool); + + return SVN_NO_ERROR; +} + + +/* Transmit SEGMENT through RECEIVER/RECEIVER_BATON iff a portion of + its revision range fits between END_REV and START_REV, possibly + cropping the range so that it fits *entirely* in that range. */ +static svn_error_t * +maybe_crop_and_send_segment(svn_location_segment_t *segment, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_location_segment_receiver_t receiver, + void *receiver_baton, + apr_pool_t *pool) +{ + /* We only want to transmit this segment if some portion of it + is between our END_REV and START_REV. */ + if (! ((segment->range_start > start_rev) + || (segment->range_end < end_rev))) + { + /* Correct our segment range when the range straddles one of + our requested revision boundaries. */ + if (segment->range_start < end_rev) + segment->range_start = end_rev; + if (segment->range_end > start_rev) + segment->range_end = start_rev; + SVN_ERR(receiver(segment, receiver_baton, pool)); + } + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_node_location_segments(svn_repos_t *repos, + const char *path, + svn_revnum_t peg_revision, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_location_segment_receiver_t receiver, + void *receiver_baton, + 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_stringbuf_t *current_path; + svn_revnum_t youngest_rev = SVN_INVALID_REVNUM, current_rev; + apr_pool_t *subpool; + + /* No PEG_REVISION? We'll use HEAD. */ + if (! SVN_IS_VALID_REVNUM(peg_revision)) + { + SVN_ERR(svn_fs_youngest_rev(&youngest_rev, fs, pool)); + peg_revision = youngest_rev; + } + + /* No START_REV? We'll use HEAD (which we may have already fetched). */ + if (! SVN_IS_VALID_REVNUM(start_rev)) + { + if (SVN_IS_VALID_REVNUM(youngest_rev)) + start_rev = youngest_rev; + else + SVN_ERR(svn_fs_youngest_rev(&start_rev, fs, pool)); + } + + /* No END_REV? We'll use 0. */ + end_rev = SVN_IS_VALID_REVNUM(end_rev) ? end_rev : 0; + + /* Are the revision properly ordered? They better be -- the API + demands it. */ + SVN_ERR_ASSERT(end_rev <= start_rev); + SVN_ERR_ASSERT(start_rev <= peg_revision); + + /* Ensure that PATH is absolute, because our path-math will depend + on that being the case. */ + if (*path != '/') + path = apr_pstrcat(pool, "/", path, (char *)NULL); + + /* Auth check. */ + if (authz_read_func) + { + svn_fs_root_t *peg_root; + SVN_ERR(svn_fs_revision_root(&peg_root, fs, peg_revision, pool)); + SVN_ERR(check_readability(peg_root, path, + authz_read_func, authz_read_baton, pool)); + } + + /* Okay, let's get searching! */ + subpool = svn_pool_create(pool); + current_rev = peg_revision; + current_path = svn_stringbuf_create(path, pool); + while (current_rev >= end_rev) + { + svn_revnum_t appeared_rev, prev_rev; + const char *cur_path, *prev_path; + svn_location_segment_t *segment; + + svn_pool_clear(subpool); + + cur_path = apr_pstrmemdup(subpool, current_path->data, + current_path->len); + segment = apr_pcalloc(subpool, sizeof(*segment)); + segment->range_end = current_rev; + segment->range_start = end_rev; + /* segment path should be absolute without leading '/'. */ + segment->path = cur_path + 1; + + SVN_ERR(svn_repos__prev_location(&appeared_rev, &prev_path, &prev_rev, + fs, current_rev, cur_path, subpool)); + + /* If there are no previous locations for this thing (meaning, + it originated at the current path), then we simply need to + find its revision of origin to populate our final segment. + Otherwise, the APPEARED_REV is the start of current segment's + range. */ + if (! prev_path) + { + svn_fs_root_t *revroot; + SVN_ERR(svn_fs_revision_root(&revroot, fs, current_rev, subpool)); + SVN_ERR(svn_fs_node_origin_rev(&(segment->range_start), revroot, + cur_path, subpool)); + if (segment->range_start < end_rev) + segment->range_start = end_rev; + current_rev = SVN_INVALID_REVNUM; + } + else + { + segment->range_start = appeared_rev; + svn_stringbuf_set(current_path, prev_path); + current_rev = prev_rev; + } + + /* Report our segment, providing it passes authz muster. */ + if (authz_read_func) + { + svn_boolean_t readable; + svn_fs_root_t *cur_rev_root; + + /* authz_read_func requires path to have a leading slash. */ + const char *abs_path = apr_pstrcat(subpool, "/", segment->path, + (char *)NULL); + + SVN_ERR(svn_fs_revision_root(&cur_rev_root, fs, + segment->range_end, subpool)); + SVN_ERR(authz_read_func(&readable, cur_rev_root, abs_path, + authz_read_baton, subpool)); + if (! readable) + return SVN_NO_ERROR; + } + + /* Transmit the segment (if it's within the scope of our concern). */ + SVN_ERR(maybe_crop_and_send_segment(segment, start_rev, end_rev, + receiver, receiver_baton, subpool)); + + /* If we've set CURRENT_REV to SVN_INVALID_REVNUM, we're done + (and didn't ever reach END_REV). */ + if (! SVN_IS_VALID_REVNUM(current_rev)) + break; + + /* If there's a gap in the history, we need to report as much + (if the gap is within the scope of our concern). */ + if (segment->range_start - current_rev > 1) + { + svn_location_segment_t *gap_segment; + gap_segment = apr_pcalloc(subpool, sizeof(*gap_segment)); + gap_segment->range_end = segment->range_start - 1; + gap_segment->range_start = current_rev + 1; + gap_segment->path = NULL; + SVN_ERR(maybe_crop_and_send_segment(gap_segment, start_rev, end_rev, + receiver, receiver_baton, + subpool)); + } + } + svn_pool_destroy(subpool); + return SVN_NO_ERROR; +} + +/* Get the mergeinfo for PATH in REPOS at REVNUM and store it in MERGEINFO. */ +static svn_error_t * +get_path_mergeinfo(apr_hash_t **mergeinfo, + svn_fs_t *fs, + const char *path, + svn_revnum_t revnum, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_mergeinfo_catalog_t tmp_catalog; + svn_fs_root_t *root; + apr_array_header_t *paths = apr_array_make(scratch_pool, 1, + sizeof(const char *)); + + APR_ARRAY_PUSH(paths, const char *) = path; + + SVN_ERR(svn_fs_revision_root(&root, fs, revnum, scratch_pool)); + /* We do not need to call svn_repos_fs_get_mergeinfo() (which performs authz) + because we will filter out unreadable revisions in + find_interesting_revision(), above */ + SVN_ERR(svn_fs_get_mergeinfo2(&tmp_catalog, root, paths, + svn_mergeinfo_inherited, FALSE, TRUE, + result_pool, scratch_pool)); + + *mergeinfo = svn_hash_gets(tmp_catalog, path); + if (!*mergeinfo) + *mergeinfo = apr_hash_make(result_pool); + + return SVN_NO_ERROR; +} + +static APR_INLINE svn_boolean_t +is_path_in_hash(apr_hash_t *duplicate_path_revs, + const char *path, + svn_revnum_t revision, + apr_pool_t *pool) +{ + const char *key = apr_psprintf(pool, "%s:%ld", path, revision); + void *ptr; + + ptr = svn_hash_gets(duplicate_path_revs, key); + return ptr != NULL; +} + +struct path_revision +{ + svn_revnum_t revnum; + const char *path; + + /* Does this path_rev have merges to also be included? */ + apr_hash_t *merged_mergeinfo; + + /* Is this a merged revision? */ + svn_boolean_t merged; +}; + +/* Check for merges in OLD_PATH_REV->PATH at OLD_PATH_REV->REVNUM. Store + the mergeinfo difference in *MERGED_MERGEINFO, allocated in POOL. The + returned *MERGED_MERGEINFO will be NULL if there are no changes. */ +static svn_error_t * +get_merged_mergeinfo(apr_hash_t **merged_mergeinfo, + svn_repos_t *repos, + struct path_revision *old_path_rev, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_hash_t *curr_mergeinfo, *prev_mergeinfo, *deleted, *changed; + svn_error_t *err; + svn_fs_root_t *root; + apr_hash_t *changed_paths; + const char *path = old_path_rev->path; + + /* Getting/parsing/diffing svn:mergeinfo is expensive, so only do it + if there is a property change. */ + SVN_ERR(svn_fs_revision_root(&root, repos->fs, old_path_rev->revnum, + scratch_pool)); + SVN_ERR(svn_fs_paths_changed2(&changed_paths, root, scratch_pool)); + while (1) + { + svn_fs_path_change2_t *changed_path = svn_hash_gets(changed_paths, path); + if (changed_path && changed_path->prop_mod) + break; + if (svn_fspath__is_root(path, strlen(path))) + { + *merged_mergeinfo = NULL; + return SVN_NO_ERROR; + } + path = svn_fspath__dirname(path, scratch_pool); + } + + /* First, find the mergeinfo difference for old_path_rev->revnum, and + old_path_rev->revnum - 1. */ + err = get_path_mergeinfo(&curr_mergeinfo, repos->fs, old_path_rev->path, + old_path_rev->revnum, 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 is if there are + no mergeinfo differences. */ + svn_error_clear(err); + *merged_mergeinfo = NULL; + return SVN_NO_ERROR; + } + else + { + return svn_error_trace(err); + } + } + + err = get_path_mergeinfo(&prev_mergeinfo, repos->fs, old_path_rev->path, + old_path_rev->revnum - 1, scratch_pool, + scratch_pool); + if (err && (err->apr_err == SVN_ERR_FS_NOT_FOUND + || err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)) + { + /* If the path doesn't exist in the previous revision or it does exist + but has invalid mergeinfo (Issue #3896), assume no merges. */ + svn_error_clear(err); + *merged_mergeinfo = NULL; + return SVN_NO_ERROR; + } + else + SVN_ERR(err); + + /* Then calculate and merge the differences. */ + SVN_ERR(svn_mergeinfo_diff2(&deleted, &changed, prev_mergeinfo, + curr_mergeinfo, FALSE, result_pool, + scratch_pool)); + SVN_ERR(svn_mergeinfo_merge2(changed, deleted, result_pool, scratch_pool)); + + /* Store the result. */ + if (apr_hash_count(changed)) + *merged_mergeinfo = changed; + else + *merged_mergeinfo = NULL; + + return SVN_NO_ERROR; +} + +static svn_error_t * +find_interesting_revisions(apr_array_header_t *path_revisions, + svn_repos_t *repos, + const char *path, + svn_revnum_t start, + svn_revnum_t end, + svn_boolean_t include_merged_revisions, + svn_boolean_t mark_as_merged, + apr_hash_t *duplicate_path_revs, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_pool_t *iterpool, *last_pool; + svn_fs_history_t *history; + svn_fs_root_t *root; + svn_node_kind_t kind; + + /* We switch between two pools while looping, since we need information from + the last iteration to be available. */ + iterpool = svn_pool_create(scratch_pool); + last_pool = svn_pool_create(scratch_pool); + + /* The path had better be a file in this revision. */ + SVN_ERR(svn_fs_revision_root(&root, repos->fs, end, scratch_pool)); + SVN_ERR(svn_fs_check_path(&kind, root, path, scratch_pool)); + if (kind != svn_node_file) + return svn_error_createf + (SVN_ERR_FS_NOT_FILE, NULL, _("'%s' is not a file in revision %ld"), + path, end); + + /* Open a history object. */ + SVN_ERR(svn_fs_node_history(&history, root, path, scratch_pool)); + while (1) + { + struct path_revision *path_rev; + svn_revnum_t tmp_revnum; + const char *tmp_path; + apr_pool_t *tmp_pool; + + svn_pool_clear(iterpool); + + /* Fetch the history object to walk through. */ + SVN_ERR(svn_fs_history_prev(&history, history, TRUE, iterpool)); + if (!history) + break; + SVN_ERR(svn_fs_history_location(&tmp_path, &tmp_revnum, + history, iterpool)); + + /* Check to see if we already saw this path (and it's ancestors) */ + if (include_merged_revisions + && is_path_in_hash(duplicate_path_revs, tmp_path, + tmp_revnum, iterpool)) + break; + + /* Check authorization. */ + if (authz_read_func) + { + svn_boolean_t readable; + svn_fs_root_t *tmp_root; + + SVN_ERR(svn_fs_revision_root(&tmp_root, repos->fs, tmp_revnum, + iterpool)); + SVN_ERR(authz_read_func(&readable, tmp_root, tmp_path, + authz_read_baton, iterpool)); + if (! readable) + break; + } + + /* We didn't break, so we must really want this path-rev. */ + path_rev = apr_palloc(result_pool, sizeof(*path_rev)); + path_rev->path = apr_pstrdup(result_pool, tmp_path); + path_rev->revnum = tmp_revnum; + path_rev->merged = mark_as_merged; + APR_ARRAY_PUSH(path_revisions, struct path_revision *) = path_rev; + + if (include_merged_revisions) + SVN_ERR(get_merged_mergeinfo(&path_rev->merged_mergeinfo, repos, + path_rev, result_pool, iterpool)); + else + path_rev->merged_mergeinfo = NULL; + + /* Add the path/rev pair to the hash, so we can filter out future + occurrences of it. We only care about this if including merged + revisions, 'cause that's the only time we can have duplicates. */ + svn_hash_sets(duplicate_path_revs, + apr_psprintf(result_pool, "%s:%ld", path_rev->path, + path_rev->revnum), + (void *)0xdeadbeef); + + if (path_rev->revnum <= start) + break; + + /* Swap pools. */ + tmp_pool = iterpool; + iterpool = last_pool; + last_pool = tmp_pool; + } + + svn_pool_destroy(iterpool); + svn_pool_destroy(last_pool); + + return SVN_NO_ERROR; +} + +/* Comparison function to sort path/revisions in increasing order */ +static int +compare_path_revisions(const void *a, const void *b) +{ + struct path_revision *a_pr = *(struct path_revision *const *)a; + struct path_revision *b_pr = *(struct path_revision *const *)b; + + if (a_pr->revnum == b_pr->revnum) + return 0; + + return a_pr->revnum < b_pr->revnum ? 1 : -1; +} + +static svn_error_t * +find_merged_revisions(apr_array_header_t **merged_path_revisions_out, + svn_revnum_t start, + const apr_array_header_t *mainline_path_revisions, + svn_repos_t *repos, + apr_hash_t *duplicate_path_revs, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const apr_array_header_t *old; + apr_array_header_t *new_merged_path_revs; + apr_pool_t *iterpool, *last_pool; + apr_array_header_t *merged_path_revisions = + apr_array_make(scratch_pool, 0, sizeof(struct path_revision *)); + + old = mainline_path_revisions; + iterpool = svn_pool_create(scratch_pool); + last_pool = svn_pool_create(scratch_pool); + + do + { + int i; + apr_pool_t *temp_pool; + + svn_pool_clear(iterpool); + new_merged_path_revs = apr_array_make(iterpool, 0, + sizeof(struct path_revision *)); + + /* Iterate over OLD, checking for non-empty mergeinfo. If found, gather + path_revisions for any merged revisions, and store those in NEW. */ + for (i = 0; i < old->nelts; i++) + { + apr_pool_t *iterpool2; + apr_hash_index_t *hi; + struct path_revision *old_pr = APR_ARRAY_IDX(old, i, + struct path_revision *); + if (!old_pr->merged_mergeinfo) + continue; + + iterpool2 = svn_pool_create(iterpool); + + /* Determine and trace the merge sources. */ + for (hi = apr_hash_first(iterpool, old_pr->merged_mergeinfo); hi; + hi = apr_hash_next(hi)) + { + apr_pool_t *iterpool3; + svn_rangelist_t *rangelist; + const char *path; + int j; + + svn_pool_clear(iterpool2); + iterpool3 = svn_pool_create(iterpool2); + + apr_hash_this(hi, (void *) &path, NULL, (void *) &rangelist); + + for (j = 0; j < rangelist->nelts; j++) + { + svn_merge_range_t *range = APR_ARRAY_IDX(rangelist, j, + svn_merge_range_t *); + svn_node_kind_t kind; + svn_fs_root_t *root; + + if (range->end < start) + continue; + + svn_pool_clear(iterpool3); + + SVN_ERR(svn_fs_revision_root(&root, repos->fs, range->end, + iterpool3)); + SVN_ERR(svn_fs_check_path(&kind, root, path, iterpool3)); + if (kind != svn_node_file) + continue; + + /* Search and find revisions to add to the NEW list. */ + SVN_ERR(find_interesting_revisions(new_merged_path_revs, + repos, path, + range->start, range->end, + TRUE, TRUE, + duplicate_path_revs, + authz_read_func, + authz_read_baton, + result_pool, iterpool3)); + } + svn_pool_destroy(iterpool3); + } + svn_pool_destroy(iterpool2); + } + + /* Append the newly found path revisions with the old ones. */ + merged_path_revisions = apr_array_append(iterpool, merged_path_revisions, + new_merged_path_revs); + + /* Swap data structures */ + old = new_merged_path_revs; + temp_pool = last_pool; + last_pool = iterpool; + iterpool = temp_pool; + } + while (new_merged_path_revs->nelts > 0); + + /* Sort MERGED_PATH_REVISIONS in increasing order by REVNUM. */ + qsort(merged_path_revisions->elts, merged_path_revisions->nelts, + sizeof(struct path_revision *), compare_path_revisions); + + /* Copy to the output array. */ + *merged_path_revisions_out = apr_array_copy(result_pool, + merged_path_revisions); + + svn_pool_destroy(iterpool); + svn_pool_destroy(last_pool); + + return SVN_NO_ERROR; +} + +struct send_baton +{ + apr_pool_t *iterpool; + apr_pool_t *last_pool; + apr_hash_t *last_props; + const char *last_path; + svn_fs_root_t *last_root; +}; + +/* Send PATH_REV to HANDLER and HANDLER_BATON, using information provided by + SB. */ +static svn_error_t * +send_path_revision(struct path_revision *path_rev, + svn_repos_t *repos, + struct send_baton *sb, + svn_file_rev_handler_t handler, + void *handler_baton) +{ + apr_hash_t *rev_props; + apr_hash_t *props; + apr_array_header_t *prop_diffs; + svn_fs_root_t *root; + svn_txdelta_stream_t *delta_stream; + svn_txdelta_window_handler_t delta_handler = NULL; + void *delta_baton = NULL; + apr_pool_t *tmp_pool; /* For swapping */ + svn_boolean_t contents_changed; + + svn_pool_clear(sb->iterpool); + + /* Get the revision properties. */ + SVN_ERR(svn_fs_revision_proplist(&rev_props, repos->fs, + path_rev->revnum, sb->iterpool)); + + /* Open the revision root. */ + SVN_ERR(svn_fs_revision_root(&root, repos->fs, path_rev->revnum, + sb->iterpool)); + + /* Get the file's properties for this revision and compute the diffs. */ + SVN_ERR(svn_fs_node_proplist(&props, root, path_rev->path, + sb->iterpool)); + SVN_ERR(svn_prop_diffs(&prop_diffs, props, sb->last_props, + sb->iterpool)); + + /* Check if the contents changed. */ + /* Special case: In the first revision, we always provide a delta. */ + if (sb->last_root) + SVN_ERR(svn_fs_contents_changed(&contents_changed, sb->last_root, + sb->last_path, root, path_rev->path, + sb->iterpool)); + else + contents_changed = TRUE; + + /* We have all we need, give to the handler. */ + SVN_ERR(handler(handler_baton, path_rev->path, path_rev->revnum, + rev_props, path_rev->merged, + contents_changed ? &delta_handler : NULL, + contents_changed ? &delta_baton : NULL, + prop_diffs, sb->iterpool)); + + /* Compute and send delta if client asked for it. + Note that this was initialized to NULL, so if !contents_changed, + no deltas will be computed. */ + if (delta_handler) + { + /* Get the content delta. */ + SVN_ERR(svn_fs_get_file_delta_stream(&delta_stream, + sb->last_root, sb->last_path, + root, path_rev->path, + sb->iterpool)); + /* And send. */ + SVN_ERR(svn_txdelta_send_txstream(delta_stream, + delta_handler, delta_baton, + sb->iterpool)); + } + + /* Remember root, path and props for next iteration. */ + sb->last_root = root; + sb->last_path = path_rev->path; + sb->last_props = props; + + /* Swap the pools. */ + tmp_pool = sb->iterpool; + sb->iterpool = sb->last_pool; + sb->last_pool = tmp_pool; + + return SVN_NO_ERROR; +} + +/* Similar to svn_repos_get_file_revs2() but returns paths while walking + history instead of after collecting all history. + + This allows implementing clients to immediately start processing and + stop when they got the information they need. (E.g. all or a specific set + of lines were modified) */ +static svn_error_t * +get_file_revs_backwards(svn_repos_t *repos, + const char *path, + svn_revnum_t start, + svn_revnum_t end, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_file_rev_handler_t handler, + void *handler_baton, + apr_pool_t *scratch_pool) +{ + apr_pool_t *iterpool, *last_pool; + svn_fs_history_t *history; + svn_fs_root_t *root; + svn_node_kind_t kind; + struct send_baton sb; + + /* We switch between two pools while looping and so does the path-rev + handler for actually reported revisions. We do this as we + need just information from last iteration to be available. */ + + iterpool = svn_pool_create(scratch_pool); + last_pool = svn_pool_create(scratch_pool); + sb.iterpool = svn_pool_create(scratch_pool); + sb.last_pool = svn_pool_create(scratch_pool); + + /* We want the first txdelta to be against the empty file. */ + sb.last_root = NULL; + sb.last_path = NULL; + + /* Create an empty hash table for the first property diff. */ + sb.last_props = apr_hash_make(sb.last_pool); + + /* The path had better be a file in this revision. */ + SVN_ERR(svn_fs_revision_root(&root, repos->fs, end, scratch_pool)); + SVN_ERR(svn_fs_check_path(&kind, root, path, scratch_pool)); + if (kind != svn_node_file) + return svn_error_createf(SVN_ERR_FS_NOT_FILE, + NULL, _("'%s' is not a file in revision %ld"), + path, end); + + /* Open a history object. */ + SVN_ERR(svn_fs_node_history(&history, root, path, scratch_pool)); + while (1) + { + struct path_revision *path_rev; + svn_revnum_t tmp_revnum; + const char *tmp_path; + + svn_pool_clear(iterpool); + + /* Fetch the history object to walk through. */ + SVN_ERR(svn_fs_history_prev(&history, history, TRUE, iterpool)); + if (!history) + break; + SVN_ERR(svn_fs_history_location(&tmp_path, &tmp_revnum, + history, iterpool)); + + /* Check authorization. */ + if (authz_read_func) + { + svn_boolean_t readable; + svn_fs_root_t *tmp_root; + + SVN_ERR(svn_fs_revision_root(&tmp_root, repos->fs, tmp_revnum, + iterpool)); + SVN_ERR(authz_read_func(&readable, tmp_root, tmp_path, + authz_read_baton, iterpool)); + if (! readable) + break; + } + + /* We didn't break, so we must really want this path-rev. */ + path_rev = apr_palloc(iterpool, sizeof(*path_rev)); + path_rev->path = tmp_path; + path_rev->revnum = tmp_revnum; + path_rev->merged = FALSE; + + SVN_ERR(send_path_revision(path_rev, repos, &sb, + handler, handler_baton)); + + if (path_rev->revnum <= start) + break; + + /* Swap pools. */ + { + apr_pool_t *tmp_pool = iterpool; + iterpool = last_pool; + last_pool = tmp_pool; + } + } + + svn_pool_destroy(iterpool); + svn_pool_destroy(last_pool); + svn_pool_destroy(sb.last_pool); + svn_pool_destroy(sb.iterpool); + + return SVN_NO_ERROR; + +} + + +/* We don't yet support sending revisions in reverse order; the caller wait + * until we've traced back through the entire history, and then accept + * them from oldest to youngest. Someday this may change, but in the meantime, + * the general algorithm is thus: + * + * 1) Trace back through the history of an object, adding each revision + * found to the MAINLINE_PATH_REVISIONS array, marking any which were + * merges. + * 2) If INCLUDE_MERGED_REVISIONS is TRUE, we repeat Step 1 on each of the + * merged revisions, including them in the MERGED_PATH_REVISIONS, and using + * DUPLICATE_PATH_REVS to avoid tracing the same paths of history multiple + * times. + * 3) Send both MAINLINE_PATH_REVISIONS and MERGED_PATH_REVISIONS from + * oldest to youngest, interleaving as appropriate. This is implemented + * similar to an insertion sort, but instead of inserting into another + * array, we just call the appropriate handler. + * + * 2013-02: Added a very simple reverse for mainline only changes. Before this, + * this would return an error (path not found) or just the first + * revision before end. + */ +svn_error_t * +svn_repos_get_file_revs2(svn_repos_t *repos, + const char *path, + svn_revnum_t start, + svn_revnum_t end, + svn_boolean_t include_merged_revisions, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_file_rev_handler_t handler, + void *handler_baton, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *mainline_path_revisions, *merged_path_revisions; + apr_hash_t *duplicate_path_revs; + struct send_baton sb; + int mainline_pos, merged_pos; + + if (end < start) + { + if (include_merged_revisions) + return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, NULL); + + return svn_error_trace( + get_file_revs_backwards(repos, path, + end, start, + authz_read_func, + authz_read_baton, + handler, + handler_baton, + scratch_pool)); + } + + /* We switch between two pools while looping, since we need information from + the last iteration to be available. */ + sb.iterpool = svn_pool_create(scratch_pool); + sb.last_pool = svn_pool_create(scratch_pool); + + /* We want the first txdelta to be against the empty file. */ + sb.last_root = NULL; + sb.last_path = NULL; + + /* Create an empty hash table for the first property diff. */ + sb.last_props = apr_hash_make(sb.last_pool); + + + /* Get the revisions we are interested in. */ + duplicate_path_revs = apr_hash_make(scratch_pool); + mainline_path_revisions = apr_array_make(scratch_pool, 100, + sizeof(struct path_revision *)); + SVN_ERR(find_interesting_revisions(mainline_path_revisions, repos, path, + start, end, include_merged_revisions, + FALSE, duplicate_path_revs, + authz_read_func, authz_read_baton, + scratch_pool, sb.iterpool)); + + /* If we are including merged revisions, go get those, too. */ + if (include_merged_revisions) + SVN_ERR(find_merged_revisions(&merged_path_revisions, start, + mainline_path_revisions, repos, + duplicate_path_revs, authz_read_func, + authz_read_baton, + scratch_pool, sb.iterpool)); + else + merged_path_revisions = apr_array_make(scratch_pool, 0, + sizeof(struct path_revision *)); + + /* We must have at least one revision to get. */ + SVN_ERR_ASSERT(mainline_path_revisions->nelts > 0); + + /* Walk through both mainline and merged revisions, and send them in + reverse chronological order, interleaving as appropriate. */ + mainline_pos = mainline_path_revisions->nelts - 1; + merged_pos = merged_path_revisions->nelts - 1; + while (mainline_pos >= 0 && merged_pos >= 0) + { + struct path_revision *main_pr = APR_ARRAY_IDX(mainline_path_revisions, + mainline_pos, + struct path_revision *); + struct path_revision *merged_pr = APR_ARRAY_IDX(merged_path_revisions, + merged_pos, + struct path_revision *); + + if (main_pr->revnum <= merged_pr->revnum) + { + SVN_ERR(send_path_revision(main_pr, repos, &sb, handler, + handler_baton)); + mainline_pos -= 1; + } + else + { + SVN_ERR(send_path_revision(merged_pr, repos, &sb, handler, + handler_baton)); + merged_pos -= 1; + } + } + + /* Send any remaining revisions from the mainline list. */ + for (; mainline_pos >= 0; mainline_pos -= 1) + { + struct path_revision *main_pr = APR_ARRAY_IDX(mainline_path_revisions, + mainline_pos, + struct path_revision *); + SVN_ERR(send_path_revision(main_pr, repos, &sb, handler, handler_baton)); + } + + /* Ditto for the merged list. */ + for (; merged_pos >= 0; merged_pos -= 1) + { + struct path_revision *merged_pr = APR_ARRAY_IDX(merged_path_revisions, + merged_pos, + struct path_revision *); + SVN_ERR(send_path_revision(merged_pr, repos, &sb, handler, + handler_baton)); + } + + svn_pool_destroy(sb.last_pool); + svn_pool_destroy(sb.iterpool); + + return SVN_NO_ERROR; +} |