summaryrefslogtreecommitdiffstats
path: root/subversion/libsvn_repos
diff options
context:
space:
mode:
Diffstat (limited to 'subversion/libsvn_repos')
-rw-r--r--subversion/libsvn_repos/authz.c1075
-rw-r--r--subversion/libsvn_repos/commit.c1381
-rw-r--r--subversion/libsvn_repos/delta.c1074
-rw-r--r--subversion/libsvn_repos/deprecated.c1017
-rw-r--r--subversion/libsvn_repos/dump.c1503
-rw-r--r--subversion/libsvn_repos/fs-wrap.c844
-rw-r--r--subversion/libsvn_repos/hooks.c890
-rw-r--r--subversion/libsvn_repos/load-fs-vtable.c1140
-rw-r--r--subversion/libsvn_repos/load.c684
-rw-r--r--subversion/libsvn_repos/log.c2369
-rw-r--r--subversion/libsvn_repos/node_tree.c431
-rw-r--r--subversion/libsvn_repos/notify.c44
-rw-r--r--subversion/libsvn_repos/replay.c1591
-rw-r--r--subversion/libsvn_repos/reporter.c1610
-rw-r--r--subversion/libsvn_repos/repos.c2132
-rw-r--r--subversion/libsvn_repos/repos.h425
-rw-r--r--subversion/libsvn_repos/rev_hunt.c1699
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(&copy_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(&copy_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(&copyfrom_rev, &copyfrom_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(&copyfrom_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(&copyfrom_rev, &copyfrom_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(&copyfrom_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(&copyfrom_rev, &copyfrom_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, &current, 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(&copyfrom_root, &copyfrom_path, &copyfrom_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(&copyfrom_root, &copyfrom_path, &copyfrom_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(&copyfrom_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,
+ &copyfrom_path, &copyfrom_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(&copy_root, &copy_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(&copy_root, &copy_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(&copy_root, &copy_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(&copy_src_rev, &copy_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;
+}
OpenPOWER on IntegriCloud