summaryrefslogtreecommitdiff
path: root/subversion/libsvn_fs_fs/transaction.c
diff options
context:
space:
mode:
Diffstat (limited to 'subversion/libsvn_fs_fs/transaction.c')
-rw-r--r--subversion/libsvn_fs_fs/transaction.c3964
1 files changed, 3964 insertions, 0 deletions
diff --git a/subversion/libsvn_fs_fs/transaction.c b/subversion/libsvn_fs_fs/transaction.c
new file mode 100644
index 0000000..bc93a5c
--- /dev/null
+++ b/subversion/libsvn_fs_fs/transaction.c
@@ -0,0 +1,3964 @@
+/* transaction.c --- transaction-related functions of FSFS
+ *
+ * ====================================================================
+ * 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 "transaction.h"
+
+#include <assert.h>
+#include <apr_sha1.h>
+
+#include "svn_hash.h"
+#include "svn_props.h"
+#include "svn_sorts.h"
+#include "svn_time.h"
+#include "svn_dirent_uri.h"
+
+#include "fs_fs.h"
+#include "index.h"
+#include "tree.h"
+#include "util.h"
+#include "id.h"
+#include "low_level.h"
+#include "temp_serializer.h"
+#include "cached_data.h"
+#include "lock.h"
+#include "rep-cache.h"
+
+#include "private/svn_fs_util.h"
+#include "private/svn_fspath.h"
+#include "private/svn_sorts_private.h"
+#include "private/svn_subr_private.h"
+#include "private/svn_string_private.h"
+#include "../libsvn_fs/fs-loader.h"
+
+#include "svn_private_config.h"
+
+/* Return the name of the sha1->rep mapping file in transaction TXN_ID
+ * within FS for the given SHA1 checksum. Use POOL for allocations.
+ */
+static APR_INLINE const char *
+path_txn_sha1(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ const unsigned char *sha1,
+ apr_pool_t *pool)
+{
+ svn_checksum_t checksum;
+ checksum.digest = sha1;
+ checksum.kind = svn_checksum_sha1;
+
+ return svn_dirent_join(svn_fs_fs__path_txn_dir(fs, txn_id, pool),
+ svn_checksum_to_cstring(&checksum, pool),
+ pool);
+}
+
+static APR_INLINE const char *
+path_txn_changes(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ return svn_dirent_join(svn_fs_fs__path_txn_dir(fs, txn_id, pool),
+ PATH_CHANGES, pool);
+}
+
+static APR_INLINE const char *
+path_txn_props(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ return svn_dirent_join(svn_fs_fs__path_txn_dir(fs, txn_id, pool),
+ PATH_TXN_PROPS, pool);
+}
+
+static APR_INLINE const char *
+path_txn_props_final(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ return svn_dirent_join(svn_fs_fs__path_txn_dir(fs, txn_id, pool),
+ PATH_TXN_PROPS_FINAL, pool);
+}
+
+static APR_INLINE const char *
+path_txn_next_ids(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ return svn_dirent_join(svn_fs_fs__path_txn_dir(fs, txn_id, pool),
+ PATH_NEXT_IDS, pool);
+}
+
+
+/* The vtable associated with an open transaction object. */
+static txn_vtable_t txn_vtable = {
+ svn_fs_fs__commit_txn,
+ svn_fs_fs__abort_txn,
+ svn_fs_fs__txn_prop,
+ svn_fs_fs__txn_proplist,
+ svn_fs_fs__change_txn_prop,
+ svn_fs_fs__txn_root,
+ svn_fs_fs__change_txn_props
+};
+
+/* FSFS-specific data being attached to svn_fs_txn_t.
+ */
+typedef struct fs_txn_data_t
+{
+ /* Strongly typed representation of the TXN's ID member. */
+ svn_fs_fs__id_part_t txn_id;
+} fs_txn_data_t;
+
+const svn_fs_fs__id_part_t *
+svn_fs_fs__txn_get_id(svn_fs_txn_t *txn)
+{
+ fs_txn_data_t *ftd = txn->fsap_data;
+ return &ftd->txn_id;
+}
+
+/* Functions for working with shared transaction data. */
+
+/* Return the transaction object for transaction TXN_ID from the
+ transaction list of filesystem FS (which must already be locked via the
+ txn_list_lock mutex). If the transaction does not exist in the list,
+ then create a new transaction object and return it (if CREATE_NEW is
+ true) or return NULL (otherwise). */
+static fs_fs_shared_txn_data_t *
+get_shared_txn(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ svn_boolean_t create_new)
+{
+ fs_fs_data_t *ffd = fs->fsap_data;
+ fs_fs_shared_data_t *ffsd = ffd->shared;
+ fs_fs_shared_txn_data_t *txn;
+
+ for (txn = ffsd->txns; txn; txn = txn->next)
+ if (svn_fs_fs__id_part_eq(&txn->txn_id, txn_id))
+ break;
+
+ if (txn || !create_new)
+ return txn;
+
+ /* Use the transaction object from the (single-object) freelist,
+ if one is available, or otherwise create a new object. */
+ if (ffsd->free_txn)
+ {
+ txn = ffsd->free_txn;
+ ffsd->free_txn = NULL;
+ }
+ else
+ {
+ apr_pool_t *subpool = svn_pool_create(ffsd->common_pool);
+ txn = apr_palloc(subpool, sizeof(*txn));
+ txn->pool = subpool;
+ }
+
+ txn->txn_id = *txn_id;
+ txn->being_written = FALSE;
+
+ /* Link this transaction into the head of the list. We will typically
+ be dealing with only one active transaction at a time, so it makes
+ sense for searches through the transaction list to look at the
+ newest transactions first. */
+ txn->next = ffsd->txns;
+ ffsd->txns = txn;
+
+ return txn;
+}
+
+/* Free the transaction object for transaction TXN_ID, and remove it
+ from the transaction list of filesystem FS (which must already be
+ locked via the txn_list_lock mutex). Do nothing if the transaction
+ does not exist. */
+static void
+free_shared_txn(svn_fs_t *fs, const svn_fs_fs__id_part_t *txn_id)
+{
+ fs_fs_data_t *ffd = fs->fsap_data;
+ fs_fs_shared_data_t *ffsd = ffd->shared;
+ fs_fs_shared_txn_data_t *txn, *prev = NULL;
+
+ for (txn = ffsd->txns; txn; prev = txn, txn = txn->next)
+ if (svn_fs_fs__id_part_eq(&txn->txn_id, txn_id))
+ break;
+
+ if (!txn)
+ return;
+
+ if (prev)
+ prev->next = txn->next;
+ else
+ ffsd->txns = txn->next;
+
+ /* As we typically will be dealing with one transaction after another,
+ we will maintain a single-object free list so that we can hopefully
+ keep reusing the same transaction object. */
+ if (!ffsd->free_txn)
+ ffsd->free_txn = txn;
+ else
+ svn_pool_destroy(txn->pool);
+}
+
+
+/* Obtain a lock on the transaction list of filesystem FS, call BODY
+ with FS, BATON, and POOL, and then unlock the transaction list.
+ Return what BODY returned. */
+static svn_error_t *
+with_txnlist_lock(svn_fs_t *fs,
+ svn_error_t *(*body)(svn_fs_t *fs,
+ const void *baton,
+ apr_pool_t *pool),
+ const void *baton,
+ apr_pool_t *pool)
+{
+ fs_fs_data_t *ffd = fs->fsap_data;
+ fs_fs_shared_data_t *ffsd = ffd->shared;
+
+ SVN_MUTEX__WITH_LOCK(ffsd->txn_list_lock,
+ body(fs, baton, pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* A structure used by unlock_proto_rev() and unlock_proto_rev_body(),
+ which see. */
+struct unlock_proto_rev_baton
+{
+ svn_fs_fs__id_part_t txn_id;
+ void *lockcookie;
+};
+
+/* Callback used in the implementation of unlock_proto_rev(). */
+static svn_error_t *
+unlock_proto_rev_body(svn_fs_t *fs, const void *baton, apr_pool_t *pool)
+{
+ const struct unlock_proto_rev_baton *b = baton;
+ apr_file_t *lockfile = b->lockcookie;
+ fs_fs_shared_txn_data_t *txn = get_shared_txn(fs, &b->txn_id, FALSE);
+ apr_status_t apr_err;
+
+ if (!txn)
+ return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
+ _("Can't unlock unknown transaction '%s'"),
+ svn_fs_fs__id_txn_unparse(&b->txn_id, pool));
+ if (!txn->being_written)
+ return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
+ _("Can't unlock nonlocked transaction '%s'"),
+ svn_fs_fs__id_txn_unparse(&b->txn_id, pool));
+
+ apr_err = apr_file_unlock(lockfile);
+ if (apr_err)
+ return svn_error_wrap_apr
+ (apr_err,
+ _("Can't unlock prototype revision lockfile for transaction '%s'"),
+ svn_fs_fs__id_txn_unparse(&b->txn_id, pool));
+ apr_err = apr_file_close(lockfile);
+ if (apr_err)
+ return svn_error_wrap_apr
+ (apr_err,
+ _("Can't close prototype revision lockfile for transaction '%s'"),
+ svn_fs_fs__id_txn_unparse(&b->txn_id, pool));
+
+ txn->being_written = FALSE;
+
+ return SVN_NO_ERROR;
+}
+
+/* Unlock the prototype revision file for transaction TXN_ID in filesystem
+ FS using cookie LOCKCOOKIE. The original prototype revision file must
+ have been closed _before_ calling this function.
+
+ Perform temporary allocations in POOL. */
+static svn_error_t *
+unlock_proto_rev(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ void *lockcookie,
+ apr_pool_t *pool)
+{
+ struct unlock_proto_rev_baton b;
+
+ b.txn_id = *txn_id;
+ b.lockcookie = lockcookie;
+ return with_txnlist_lock(fs, unlock_proto_rev_body, &b, pool);
+}
+
+/* A structure used by get_writable_proto_rev() and
+ get_writable_proto_rev_body(), which see. */
+struct get_writable_proto_rev_baton
+{
+ void **lockcookie;
+ svn_fs_fs__id_part_t txn_id;
+};
+
+/* Callback used in the implementation of get_writable_proto_rev(). */
+static svn_error_t *
+get_writable_proto_rev_body(svn_fs_t *fs, const void *baton, apr_pool_t *pool)
+{
+ const struct get_writable_proto_rev_baton *b = baton;
+ void **lockcookie = b->lockcookie;
+ fs_fs_shared_txn_data_t *txn = get_shared_txn(fs, &b->txn_id, TRUE);
+
+ /* First, ensure that no thread in this process (including this one)
+ is currently writing to this transaction's proto-rev file. */
+ if (txn->being_written)
+ return svn_error_createf(SVN_ERR_FS_REP_BEING_WRITTEN, NULL,
+ _("Cannot write to the prototype revision file "
+ "of transaction '%s' because a previous "
+ "representation is currently being written by "
+ "this process"),
+ svn_fs_fs__id_txn_unparse(&b->txn_id, pool));
+
+
+ /* We know that no thread in this process is writing to the proto-rev
+ file, and by extension, that no thread in this process is holding a
+ lock on the prototype revision lock file. It is therefore safe
+ for us to attempt to lock this file, to see if any other process
+ is holding a lock. */
+
+ {
+ apr_file_t *lockfile;
+ apr_status_t apr_err;
+ const char *lockfile_path
+ = svn_fs_fs__path_txn_proto_rev_lock(fs, &b->txn_id, pool);
+
+ /* Open the proto-rev lockfile, creating it if necessary, as it may
+ not exist if the transaction dates from before the lockfiles were
+ introduced.
+
+ ### We'd also like to use something like svn_io_file_lock2(), but
+ that forces us to create a subpool just to be able to unlock
+ the file, which seems a waste. */
+ SVN_ERR(svn_io_file_open(&lockfile, lockfile_path,
+ APR_WRITE | APR_CREATE, APR_OS_DEFAULT, pool));
+
+ apr_err = apr_file_lock(lockfile,
+ APR_FLOCK_EXCLUSIVE | APR_FLOCK_NONBLOCK);
+ if (apr_err)
+ {
+ svn_error_clear(svn_io_file_close(lockfile, pool));
+
+ if (APR_STATUS_IS_EAGAIN(apr_err))
+ return svn_error_createf(SVN_ERR_FS_REP_BEING_WRITTEN, NULL,
+ _("Cannot write to the prototype revision "
+ "file of transaction '%s' because a "
+ "previous representation is currently "
+ "being written by another process"),
+ svn_fs_fs__id_txn_unparse(&b->txn_id,
+ pool));
+
+ return svn_error_wrap_apr(apr_err,
+ _("Can't get exclusive lock on file '%s'"),
+ svn_dirent_local_style(lockfile_path, pool));
+ }
+
+ *lockcookie = lockfile;
+ }
+
+ /* We've successfully locked the transaction; mark it as such. */
+ txn->being_written = TRUE;
+
+ return SVN_NO_ERROR;
+}
+
+/* Make sure the length ACTUAL_LENGTH of the proto-revision file PROTO_REV
+ of transaction TXN_ID in filesystem FS matches the proto-index file.
+ Trim any crash / failure related extra data from the proto-rev file.
+
+ If the prototype revision file is too short, we can't do much but bail out.
+
+ Perform all allocations in POOL. */
+static svn_error_t *
+auto_truncate_proto_rev(svn_fs_t *fs,
+ apr_file_t *proto_rev,
+ apr_off_t actual_length,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ /* Only relevant for newer FSFS formats. */
+ if (svn_fs_fs__use_log_addressing(fs))
+ {
+ /* Determine file range covered by the proto-index so far. Note that
+ we always append to both file, i.e. the last index entry also
+ corresponds to the last addition in the rev file. */
+ const char *path = svn_fs_fs__path_p2l_proto_index(fs, txn_id, pool);
+ apr_file_t *file;
+ apr_off_t indexed_length;
+
+ SVN_ERR(svn_fs_fs__p2l_proto_index_open(&file, path, pool));
+ SVN_ERR(svn_fs_fs__p2l_proto_index_next_offset(&indexed_length, file,
+ pool));
+ SVN_ERR(svn_io_file_close(file, pool));
+
+ /* Handle mismatches. */
+ if (indexed_length < actual_length)
+ SVN_ERR(svn_io_file_trunc(proto_rev, indexed_length, pool));
+ else if (indexed_length > actual_length)
+ return svn_error_createf(SVN_ERR_FS_INDEX_INCONSISTENT,
+ NULL,
+ _("p2l proto index offset %s beyond proto"
+ "rev file size %s for TXN %s"),
+ apr_off_t_toa(pool, indexed_length),
+ apr_off_t_toa(pool, actual_length),
+ svn_fs_fs__id_txn_unparse(txn_id, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Get a handle to the prototype revision file for transaction TXN_ID in
+ filesystem FS, and lock it for writing. Return FILE, a file handle
+ positioned at the end of the file, and LOCKCOOKIE, a cookie that
+ should be passed to unlock_proto_rev() to unlock the file once FILE
+ has been closed.
+
+ If the prototype revision file is already locked, return error
+ SVN_ERR_FS_REP_BEING_WRITTEN.
+
+ Perform all allocations in POOL. */
+static svn_error_t *
+get_writable_proto_rev(apr_file_t **file,
+ void **lockcookie,
+ svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ struct get_writable_proto_rev_baton b;
+ svn_error_t *err;
+ apr_off_t end_offset = 0;
+
+ b.lockcookie = lockcookie;
+ b.txn_id = *txn_id;
+
+ SVN_ERR(with_txnlist_lock(fs, get_writable_proto_rev_body, &b, pool));
+
+ /* Now open the prototype revision file and seek to the end. */
+ err = svn_io_file_open(file,
+ svn_fs_fs__path_txn_proto_rev(fs, txn_id, pool),
+ APR_READ | APR_WRITE | APR_BUFFERED, APR_OS_DEFAULT,
+ pool);
+
+ /* You might expect that we could dispense with the following seek
+ and achieve the same thing by opening the file using APR_APPEND.
+ Unfortunately, APR's buffered file implementation unconditionally
+ places its initial file pointer at the start of the file (even for
+ files opened with APR_APPEND), so we need this seek to reconcile
+ the APR file pointer to the OS file pointer (since we need to be
+ able to read the current file position later). */
+ if (!err)
+ err = svn_io_file_seek(*file, APR_END, &end_offset, pool);
+
+ /* We don't want unused sections (such as leftovers from failed delta
+ stream) in our file. If we use log addressing, we would need an
+ index entry for the unused section and that section would need to
+ be all NUL by convention. So, detect and fix those cases by truncating
+ the protorev file. */
+ if (!err)
+ err = auto_truncate_proto_rev(fs, *file, end_offset, txn_id, pool);
+
+ if (err)
+ {
+ err = svn_error_compose_create(
+ err,
+ unlock_proto_rev(fs, txn_id, *lockcookie, pool));
+
+ *lockcookie = NULL;
+ }
+
+ return svn_error_trace(err);
+}
+
+/* Callback used in the implementation of purge_shared_txn(). */
+static svn_error_t *
+purge_shared_txn_body(svn_fs_t *fs, const void *baton, apr_pool_t *pool)
+{
+ const svn_fs_fs__id_part_t *txn_id = baton;
+
+ free_shared_txn(fs, txn_id);
+ svn_fs_fs__reset_txn_caches(fs);
+
+ return SVN_NO_ERROR;
+}
+
+/* Purge the shared data for transaction TXN_ID in filesystem FS.
+ Perform all allocations in POOL. */
+static svn_error_t *
+purge_shared_txn(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ return with_txnlist_lock(fs, purge_shared_txn_body, txn_id, pool);
+}
+
+
+svn_error_t *
+svn_fs_fs__put_node_revision(svn_fs_t *fs,
+ const svn_fs_id_t *id,
+ node_revision_t *noderev,
+ svn_boolean_t fresh_txn_root,
+ apr_pool_t *pool)
+{
+ fs_fs_data_t *ffd = fs->fsap_data;
+ apr_file_t *noderev_file;
+
+ noderev->is_fresh_txn_root = fresh_txn_root;
+
+ if (! svn_fs_fs__id_is_txn(id))
+ return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
+ _("Attempted to write to non-transaction '%s'"),
+ svn_fs_fs__id_unparse(id, pool)->data);
+
+ SVN_ERR(svn_io_file_open(&noderev_file,
+ svn_fs_fs__path_txn_node_rev(fs, id, pool),
+ APR_WRITE | APR_CREATE | APR_TRUNCATE
+ | APR_BUFFERED, APR_OS_DEFAULT, pool));
+
+ SVN_ERR(svn_fs_fs__write_noderev(svn_stream_from_aprfile2(noderev_file, TRUE,
+ pool),
+ noderev, ffd->format,
+ svn_fs_fs__fs_supports_mergeinfo(fs),
+ pool));
+
+ SVN_ERR(svn_io_file_close(noderev_file, pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* For the in-transaction NODEREV within FS, write the sha1->rep mapping
+ * file in the respective transaction, if rep sharing has been enabled etc.
+ * Use SCATCH_POOL for temporary allocations.
+ */
+static svn_error_t *
+store_sha1_rep_mapping(svn_fs_t *fs,
+ node_revision_t *noderev,
+ apr_pool_t *scratch_pool)
+{
+ fs_fs_data_t *ffd = fs->fsap_data;
+
+ /* if rep sharing has been enabled and the noderev has a data rep and
+ * its SHA-1 is known, store the rep struct under its SHA1. */
+ if ( ffd->rep_sharing_allowed
+ && noderev->data_rep
+ && noderev->data_rep->has_sha1)
+ {
+ apr_file_t *rep_file;
+ const char *file_name = path_txn_sha1(fs,
+ &noderev->data_rep->txn_id,
+ noderev->data_rep->sha1_digest,
+ scratch_pool);
+ svn_stringbuf_t *rep_string
+ = svn_fs_fs__unparse_representation(noderev->data_rep,
+ ffd->format,
+ (noderev->kind == svn_node_dir),
+ scratch_pool, scratch_pool);
+ SVN_ERR(svn_io_file_open(&rep_file, file_name,
+ APR_WRITE | APR_CREATE | APR_TRUNCATE
+ | APR_BUFFERED, APR_OS_DEFAULT, scratch_pool));
+
+ SVN_ERR(svn_io_file_write_full(rep_file, rep_string->data,
+ rep_string->len, NULL, scratch_pool));
+
+ SVN_ERR(svn_io_file_close(rep_file, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+unparse_dir_entry(svn_fs_dirent_t *dirent,
+ svn_stream_t *stream,
+ apr_pool_t *pool)
+{
+ const char *val
+ = apr_psprintf(pool, "%s %s",
+ (dirent->kind == svn_node_file) ? SVN_FS_FS__KIND_FILE
+ : SVN_FS_FS__KIND_DIR,
+ svn_fs_fs__id_unparse(dirent->id, pool)->data);
+
+ SVN_ERR(svn_stream_printf(stream, pool, "K %" APR_SIZE_T_FMT "\n%s\n"
+ "V %" APR_SIZE_T_FMT "\n%s\n",
+ strlen(dirent->name), dirent->name,
+ strlen(val), val));
+ return SVN_NO_ERROR;
+}
+
+/* Write the directory given as array of dirent structs in ENTRIES to STREAM.
+ Perform temporary allocations in POOL. */
+static svn_error_t *
+unparse_dir_entries(apr_array_header_t *entries,
+ svn_stream_t *stream,
+ apr_pool_t *pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(pool);
+ int i;
+ for (i = 0; i < entries->nelts; ++i)
+ {
+ svn_fs_dirent_t *dirent;
+
+ svn_pool_clear(iterpool);
+ dirent = APR_ARRAY_IDX(entries, i, svn_fs_dirent_t *);
+ SVN_ERR(unparse_dir_entry(dirent, stream, iterpool));
+ }
+
+ SVN_ERR(svn_stream_printf(stream, pool, "%s\n", SVN_HASH_TERMINATOR));
+
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Return a deep copy of SOURCE and allocate it in RESULT_POOL.
+ */
+static svn_fs_path_change2_t *
+path_change_dup(const svn_fs_path_change2_t *source,
+ apr_pool_t *result_pool)
+{
+ svn_fs_path_change2_t *result = apr_pmemdup(result_pool, source,
+ sizeof(*source));
+ result->node_rev_id = svn_fs_fs__id_copy(source->node_rev_id, result_pool);
+ if (source->copyfrom_path)
+ result->copyfrom_path = apr_pstrdup(result_pool, source->copyfrom_path);
+
+ return result;
+}
+
+/* Merge the internal-use-only CHANGE into a hash of public-FS
+ svn_fs_path_change2_t CHANGED_PATHS, collapsing multiple changes into a
+ single summarical (is that real word?) change per path. DELETIONS is
+ also a path->svn_fs_path_change2_t hash and contains all the deletions
+ that got turned into a replacement. */
+static svn_error_t *
+fold_change(apr_hash_t *changed_paths,
+ apr_hash_t *deletions,
+ const change_t *change)
+{
+ apr_pool_t *pool = apr_hash_pool_get(changed_paths);
+ svn_fs_path_change2_t *old_change, *new_change;
+ const svn_string_t *path = &change->path;
+ const svn_fs_path_change2_t *info = &change->info;
+
+ if ((old_change = apr_hash_get(changed_paths, path->data, path->len)))
+ {
+ /* This path already exists in the hash, so we have to merge
+ this change into the already existing one. */
+
+ /* Sanity check: only allow NULL node revision ID in the
+ `reset' case. */
+ if ((! info->node_rev_id)
+ && (info->change_kind != svn_fs_path_change_reset))
+ return svn_error_create
+ (SVN_ERR_FS_CORRUPT, NULL,
+ _("Missing required node revision ID"));
+
+ /* Sanity check: we should be talking about the same node
+ revision ID as our last change except where the last change
+ was a deletion. */
+ if (info->node_rev_id
+ && (! svn_fs_fs__id_eq(old_change->node_rev_id, info->node_rev_id))
+ && (old_change->change_kind != svn_fs_path_change_delete))
+ return svn_error_create
+ (SVN_ERR_FS_CORRUPT, NULL,
+ _("Invalid change ordering: new node revision ID "
+ "without delete"));
+
+ /* Sanity check: an add, replacement, or reset must be the first
+ thing to follow a deletion. */
+ if ((old_change->change_kind == svn_fs_path_change_delete)
+ && (! ((info->change_kind == svn_fs_path_change_replace)
+ || (info->change_kind == svn_fs_path_change_reset)
+ || (info->change_kind == svn_fs_path_change_add))))
+ return svn_error_create
+ (SVN_ERR_FS_CORRUPT, NULL,
+ _("Invalid change ordering: non-add change on deleted path"));
+
+ /* Sanity check: an add can't follow anything except
+ a delete or reset. */
+ if ((info->change_kind == svn_fs_path_change_add)
+ && (old_change->change_kind != svn_fs_path_change_delete)
+ && (old_change->change_kind != svn_fs_path_change_reset))
+ return svn_error_create
+ (SVN_ERR_FS_CORRUPT, NULL,
+ _("Invalid change ordering: add change on preexisting path"));
+
+ /* Now, merge that change in. */
+ switch (info->change_kind)
+ {
+ case svn_fs_path_change_reset:
+ /* A reset here will simply remove the path change from the
+ hash. */
+ apr_hash_set(changed_paths, path->data, path->len, NULL);
+ break;
+
+ case svn_fs_path_change_delete:
+ if (old_change->change_kind == svn_fs_path_change_add)
+ {
+ /* If the path was introduced in this transaction via an
+ add, and we are deleting it, just remove the path
+ altogether. (The caller will delete any child paths.) */
+ apr_hash_set(changed_paths, path->data, path->len, NULL);
+ }
+ else if (old_change->change_kind == svn_fs_path_change_replace)
+ {
+ /* A deleting a 'replace' restore the original deletion. */
+ new_change = apr_hash_get(deletions, path->data, path->len);
+ SVN_ERR_ASSERT(new_change);
+ apr_hash_set(changed_paths, path->data, path->len, new_change);
+ }
+ else
+ {
+ /* A deletion overrules a previous change (modify). */
+ new_change = path_change_dup(info, pool);
+ apr_hash_set(changed_paths, path->data, path->len, new_change);
+ }
+ break;
+
+ case svn_fs_path_change_add:
+ case svn_fs_path_change_replace:
+ /* An add at this point must be following a previous delete,
+ so treat it just like a replace. Remember the original
+ deletion such that we are able to delete this path again
+ (the replacement may have changed node kind and id). */
+ new_change = path_change_dup(info, pool);
+ new_change->change_kind = svn_fs_path_change_replace;
+
+ apr_hash_set(changed_paths, path->data, path->len, new_change);
+
+ /* Remember the original change.
+ * Make sure to allocate the hash key in a durable pool. */
+ apr_hash_set(deletions,
+ apr_pstrmemdup(apr_hash_pool_get(deletions),
+ path->data, path->len),
+ path->len, old_change);
+ break;
+
+ case svn_fs_path_change_modify:
+ default:
+ /* If the new change modifies some attribute of the node, set
+ the corresponding flag, whether it already was set or not.
+ Note: We do not reset a flag to FALSE if a change is undone. */
+ if (info->text_mod)
+ old_change->text_mod = TRUE;
+ if (info->prop_mod)
+ old_change->prop_mod = TRUE;
+ if (info->mergeinfo_mod == svn_tristate_true)
+ old_change->mergeinfo_mod = svn_tristate_true;
+ break;
+ }
+ }
+ else
+ {
+ /* Add this path. The API makes no guarantees that this (new) key
+ will not be retained. Thus, we copy the key into the target pool
+ to ensure a proper lifetime. */
+ apr_hash_set(changed_paths,
+ apr_pstrmemdup(pool, path->data, path->len), path->len,
+ path_change_dup(info, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Baton type to be used with process_changes(). */
+typedef struct process_changes_baton_t
+{
+ /* Folded list of path changes. */
+ apr_hash_t *changed_paths;
+
+ /* Path changes that are deletions and have been turned into
+ replacements. If those replacements get deleted again, this
+ container contains the record that we have to revert to. */
+ apr_hash_t *deletions;
+} process_changes_baton_t;
+
+/* An implementation of svn_fs_fs__change_receiver_t.
+ Examine all the changed path entries in CHANGES and store them in
+ *CHANGED_PATHS. Folding is done to remove redundant or unnecessary
+ data. Do all allocations in POOL. */
+static svn_error_t *
+process_changes(void *baton_p,
+ change_t *change,
+ apr_pool_t *scratch_pool)
+{
+ process_changes_baton_t *baton = baton_p;
+
+ SVN_ERR(fold_change(baton->changed_paths, baton->deletions, change));
+
+ /* Now, if our change was a deletion or replacement, we have to
+ blow away any changes thus far on paths that are (or, were)
+ children of this path.
+ ### i won't bother with another iteration pool here -- at
+ most we talking about a few extra dups of paths into what
+ is already a temporary subpool.
+ */
+
+ if ((change->info.change_kind == svn_fs_path_change_delete)
+ || (change->info.change_kind == svn_fs_path_change_replace))
+ {
+ apr_hash_index_t *hi;
+
+ /* a potential child path must contain at least 2 more chars
+ (the path separator plus at least one char for the name).
+ Also, we should not assume that all paths have been normalized
+ i.e. some might have trailing path separators.
+ */
+ apr_ssize_t path_len = change->path.len;
+ apr_ssize_t min_child_len = path_len == 0
+ ? 1
+ : change->path.data[path_len-1] == '/'
+ ? path_len + 1
+ : path_len + 2;
+
+ /* CAUTION: This is the inner loop of an O(n^2) algorithm.
+ The number of changes to process may be >> 1000.
+ Therefore, keep the inner loop as tight as possible.
+ */
+ for (hi = apr_hash_first(scratch_pool, baton->changed_paths);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ /* KEY is the path. */
+ const void *path;
+ apr_ssize_t klen;
+ svn_fs_path_change2_t *old_change;
+ apr_hash_this(hi, &path, &klen, (void**)&old_change);
+
+ /* If we come across a child of our path, remove it.
+ Call svn_fspath__skip_ancestor only if there is a chance that
+ this is actually a sub-path.
+ */
+ if (klen >= min_child_len)
+ {
+ const char *child;
+
+ child = svn_fspath__skip_ancestor(change->path.data, path);
+ if (child && child[0] != '\0')
+ {
+ apr_hash_set(baton->changed_paths, path, klen, NULL);
+ }
+ }
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__txn_changes_fetch(apr_hash_t **changed_paths_p,
+ svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ apr_file_t *file;
+ apr_hash_t *changed_paths = apr_hash_make(pool);
+ apr_pool_t *scratch_pool = svn_pool_create(pool);
+ process_changes_baton_t baton;
+
+ baton.changed_paths = changed_paths;
+ baton.deletions = apr_hash_make(scratch_pool);
+
+ SVN_ERR(svn_io_file_open(&file,
+ path_txn_changes(fs, txn_id, scratch_pool),
+ APR_READ | APR_BUFFERED, APR_OS_DEFAULT,
+ scratch_pool));
+
+ SVN_ERR(svn_fs_fs__read_changes_incrementally(
+ svn_stream_from_aprfile2(file, TRUE,
+ scratch_pool),
+ process_changes, &baton,
+ scratch_pool));
+ svn_pool_destroy(scratch_pool);
+
+ *changed_paths_p = changed_paths;
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_fs_fs__paths_changed(apr_hash_t **changed_paths_p,
+ svn_fs_t *fs,
+ svn_revnum_t rev,
+ apr_pool_t *pool)
+{
+ apr_hash_t *changed_paths;
+ apr_array_header_t *changes;
+ int i;
+
+ SVN_ERR(svn_fs_fs__get_changes(&changes, fs, rev, pool));
+
+ changed_paths = svn_hash__make(pool);
+ for (i = 0; i < changes->nelts; ++i)
+ {
+ change_t *change = APR_ARRAY_IDX(changes, i, change_t *);
+ apr_hash_set(changed_paths, change->path.data, change->path.len,
+ &change->info);
+ }
+
+ *changed_paths_p = changed_paths;
+
+ return SVN_NO_ERROR;
+}
+
+/* Copy a revision node-rev SRC into the current transaction TXN_ID in
+ the filesystem FS. This is only used to create the root of a transaction.
+ Allocations are from POOL. */
+static svn_error_t *
+create_new_txn_noderev_from_rev(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ svn_fs_id_t *src,
+ apr_pool_t *pool)
+{
+ node_revision_t *noderev;
+ const svn_fs_fs__id_part_t *node_id, *copy_id;
+
+ SVN_ERR(svn_fs_fs__get_node_revision(&noderev, fs, src, pool, pool));
+
+ if (svn_fs_fs__id_is_txn(noderev->id))
+ return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
+ _("Copying from transactions not allowed"));
+
+ noderev->predecessor_id = noderev->id;
+ noderev->predecessor_count++;
+ noderev->copyfrom_path = NULL;
+ noderev->copyfrom_rev = SVN_INVALID_REVNUM;
+
+ /* For the transaction root, the copyroot never changes. */
+
+ node_id = svn_fs_fs__id_node_id(noderev->id);
+ copy_id = svn_fs_fs__id_copy_id(noderev->id);
+ noderev->id = svn_fs_fs__id_txn_create(node_id, copy_id, txn_id, pool);
+
+ return svn_fs_fs__put_node_revision(fs, noderev->id, noderev, TRUE, pool);
+}
+
+/* A structure used by get_and_increment_txn_key_body(). */
+struct get_and_increment_txn_key_baton {
+ svn_fs_t *fs;
+ apr_uint64_t txn_number;
+ apr_pool_t *pool;
+};
+
+/* Callback used in the implementation of create_txn_dir(). This gets
+ the current base 36 value in PATH_TXN_CURRENT and increments it.
+ It returns the original value by the baton. */
+static svn_error_t *
+get_and_increment_txn_key_body(void *baton, apr_pool_t *pool)
+{
+ struct get_and_increment_txn_key_baton *cb = baton;
+ const char *txn_current_filename
+ = svn_fs_fs__path_txn_current(cb->fs, pool);
+ char new_id_str[SVN_INT64_BUFFER_SIZE + 1]; /* add space for a newline */
+ apr_size_t line_length;
+
+ svn_stringbuf_t *buf;
+ SVN_ERR(svn_fs_fs__read_content(&buf, txn_current_filename, cb->pool));
+
+ /* assign the current txn counter value to our result */
+ cb->txn_number = svn__base36toui64(NULL, buf->data);
+
+ /* remove trailing newlines */
+ line_length = svn__ui64tobase36(new_id_str, cb->txn_number+1);
+ new_id_str[line_length] = '\n';
+
+ /* Increment the key and add a trailing \n to the string so the
+ txn-current file has a newline in it. */
+ SVN_ERR(svn_io_write_atomic(txn_current_filename, new_id_str,
+ line_length + 1,
+ txn_current_filename /* copy_perms path */,
+ pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Create a unique directory for a transaction in FS based on revision REV.
+ Return the ID for this transaction in *ID_P and *TXN_ID. Use a sequence
+ value in the transaction ID to prevent reuse of transaction IDs. */
+static svn_error_t *
+create_txn_dir(const char **id_p,
+ svn_fs_fs__id_part_t *txn_id,
+ svn_fs_t *fs,
+ svn_revnum_t rev,
+ apr_pool_t *pool)
+{
+ struct get_and_increment_txn_key_baton cb;
+ const char *txn_dir;
+
+ /* Get the current transaction sequence value, which is a base-36
+ number, from the txn-current file, and write an
+ incremented value back out to the file. Place the revision
+ number the transaction is based off into the transaction id. */
+ cb.pool = pool;
+ cb.fs = fs;
+ SVN_ERR(svn_fs_fs__with_txn_current_lock(fs,
+ get_and_increment_txn_key_body,
+ &cb,
+ pool));
+ txn_id->revision = rev;
+ txn_id->number = cb.txn_number;
+
+ *id_p = svn_fs_fs__id_txn_unparse(txn_id, pool);
+ txn_dir = svn_fs_fs__path_txn_dir(fs, txn_id, pool);
+
+ return svn_io_dir_make(txn_dir, APR_OS_DEFAULT, pool);
+}
+
+/* Create a unique directory for a transaction in FS based on revision
+ REV. Return the ID for this transaction in *ID_P and *TXN_ID. This
+ implementation is used in svn 1.4 and earlier repositories and is
+ kept in 1.5 and greater to support the --pre-1.4-compatible and
+ --pre-1.5-compatible repository creation options. Reused
+ transaction IDs are possible with this implementation. */
+static svn_error_t *
+create_txn_dir_pre_1_5(const char **id_p,
+ svn_fs_fs__id_part_t *txn_id,
+ svn_fs_t *fs,
+ svn_revnum_t rev,
+ apr_pool_t *pool)
+{
+ unsigned int i;
+ apr_pool_t *subpool;
+ const char *unique_path, *prefix;
+
+ /* Try to create directories named "<txndir>/<rev>-<uniqueifier>.txn". */
+ prefix = svn_dirent_join(svn_fs_fs__path_txns_dir(fs, pool),
+ apr_psprintf(pool, "%ld", rev), pool);
+
+ subpool = svn_pool_create(pool);
+ for (i = 1; i <= 99999; i++)
+ {
+ svn_error_t *err;
+
+ svn_pool_clear(subpool);
+ unique_path = apr_psprintf(subpool, "%s-%u" PATH_EXT_TXN, prefix, i);
+ err = svn_io_dir_make(unique_path, APR_OS_DEFAULT, subpool);
+ if (! err)
+ {
+ /* We succeeded. Return the basename minus the ".txn" extension. */
+ const char *name = svn_dirent_basename(unique_path, subpool);
+ *id_p = apr_pstrndup(pool, name,
+ strlen(name) - strlen(PATH_EXT_TXN));
+ SVN_ERR(svn_fs_fs__id_txn_parse(txn_id, *id_p));
+ svn_pool_destroy(subpool);
+ return SVN_NO_ERROR;
+ }
+ if (! APR_STATUS_IS_EEXIST(err->apr_err))
+ return svn_error_trace(err);
+ svn_error_clear(err);
+ }
+
+ return svn_error_createf(SVN_ERR_IO_UNIQUE_NAMES_EXHAUSTED,
+ NULL,
+ _("Unable to create transaction directory "
+ "in '%s' for revision %ld"),
+ svn_dirent_local_style(fs->path, pool),
+ rev);
+}
+
+svn_error_t *
+svn_fs_fs__create_txn(svn_fs_txn_t **txn_p,
+ svn_fs_t *fs,
+ svn_revnum_t rev,
+ apr_pool_t *pool)
+{
+ fs_fs_data_t *ffd = fs->fsap_data;
+ svn_fs_txn_t *txn;
+ fs_txn_data_t *ftd;
+ svn_fs_id_t *root_id;
+
+ txn = apr_pcalloc(pool, sizeof(*txn));
+ ftd = apr_pcalloc(pool, sizeof(*ftd));
+
+ /* Get the txn_id. */
+ if (ffd->format >= SVN_FS_FS__MIN_TXN_CURRENT_FORMAT)
+ SVN_ERR(create_txn_dir(&txn->id, &ftd->txn_id, fs, rev, pool));
+ else
+ SVN_ERR(create_txn_dir_pre_1_5(&txn->id, &ftd->txn_id, fs, rev, pool));
+
+ txn->fs = fs;
+ txn->base_rev = rev;
+
+ txn->vtable = &txn_vtable;
+ txn->fsap_data = ftd;
+ *txn_p = txn;
+
+ /* Create a new root node for this transaction. */
+ SVN_ERR(svn_fs_fs__rev_get_root(&root_id, fs, rev, pool, pool));
+ SVN_ERR(create_new_txn_noderev_from_rev(fs, &ftd->txn_id, root_id, pool));
+
+ /* Create an empty rev file. */
+ SVN_ERR(svn_io_file_create_empty(
+ svn_fs_fs__path_txn_proto_rev(fs, &ftd->txn_id, pool),
+ pool));
+
+ /* Create an empty rev-lock file. */
+ SVN_ERR(svn_io_file_create_empty(
+ svn_fs_fs__path_txn_proto_rev_lock(fs, &ftd->txn_id, pool),
+ pool));
+
+ /* Create an empty changes file. */
+ SVN_ERR(svn_io_file_create_empty(path_txn_changes(fs, &ftd->txn_id, pool),
+ pool));
+
+ /* Create the next-ids file. */
+ return svn_io_file_create(path_txn_next_ids(fs, &ftd->txn_id, pool),
+ "0 0\n", pool);
+}
+
+/* Store the property list for transaction TXN_ID in PROPLIST.
+ Perform temporary allocations in POOL. */
+static svn_error_t *
+get_txn_proplist(apr_hash_t *proplist,
+ svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ svn_stream_t *stream;
+ svn_error_t *err;
+
+ /* Check for issue #3696. (When we find and fix the cause, we can change
+ * this to an assertion.) */
+ if (!txn_id || !svn_fs_fs__id_txn_used(txn_id))
+ return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+ _("Internal error: a null transaction id was "
+ "passed to get_txn_proplist()"));
+
+ /* Open the transaction properties file. */
+ SVN_ERR(svn_stream_open_readonly(&stream, path_txn_props(fs, txn_id, pool),
+ pool, pool));
+
+ /* Read in the property list. */
+ err = svn_hash_read2(proplist, stream, SVN_HASH_TERMINATOR, pool);
+ if (err)
+ {
+ svn_error_clear(svn_stream_close(stream));
+ return svn_error_quick_wrapf(err,
+ _("malformed property list in transaction '%s'"),
+ path_txn_props(fs, txn_id, pool));
+ }
+
+ return svn_stream_close(stream);
+}
+
+/* Save the property list PROPS as the revprops for transaction TXN_ID
+ in FS. Perform temporary allocations in POOL. */
+static svn_error_t *
+set_txn_proplist(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_hash_t *props,
+ svn_boolean_t final,
+ apr_pool_t *pool)
+{
+ svn_stringbuf_t *buf;
+ svn_stream_t *stream;
+
+ /* Write out the new file contents to BUF. */
+ buf = svn_stringbuf_create_ensure(1024, pool);
+ stream = svn_stream_from_stringbuf(buf, pool);
+ SVN_ERR(svn_hash_write2(props, stream, SVN_HASH_TERMINATOR, pool));
+ SVN_ERR(svn_stream_close(stream));
+
+ /* Open the transaction properties file and write new contents to it. */
+ SVN_ERR(svn_io_write_atomic((final
+ ? path_txn_props_final(fs, txn_id, pool)
+ : path_txn_props(fs, txn_id, pool)),
+ buf->data, buf->len,
+ NULL /* copy_perms_path */, pool));
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_fs_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_fs_fs__change_txn_props(txn, props, pool);
+}
+
+svn_error_t *
+svn_fs_fs__change_txn_props(svn_fs_txn_t *txn,
+ const apr_array_header_t *props,
+ apr_pool_t *pool)
+{
+ fs_txn_data_t *ftd = txn->fsap_data;
+ apr_hash_t *txn_prop = apr_hash_make(pool);
+ int i;
+ svn_error_t *err;
+
+ err = get_txn_proplist(txn_prop, txn->fs, &ftd->txn_id, pool);
+ /* Here - and here only - we need to deal with the possibility that the
+ transaction property file doesn't yet exist. The rest of the
+ implementation assumes that the file exists, but we're called to set the
+ initial transaction properties as the transaction is being created. */
+ if (err && (APR_STATUS_IS_ENOENT(err->apr_err)))
+ svn_error_clear(err);
+ else if (err)
+ return svn_error_trace(err);
+
+ for (i = 0; i < props->nelts; i++)
+ {
+ svn_prop_t *prop = &APR_ARRAY_IDX(props, i, svn_prop_t);
+
+ if (svn_hash_gets(txn_prop, SVN_FS__PROP_TXN_CLIENT_DATE)
+ && !strcmp(prop->name, SVN_PROP_REVISION_DATE))
+ svn_hash_sets(txn_prop, SVN_FS__PROP_TXN_CLIENT_DATE,
+ svn_string_create("1", pool));
+
+ svn_hash_sets(txn_prop, prop->name, prop->value);
+ }
+
+ /* Create a new version of the file and write out the new props. */
+ /* Open the transaction properties file. */
+ SVN_ERR(set_txn_proplist(txn->fs, &ftd->txn_id, txn_prop, FALSE, pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__get_txn(transaction_t **txn_p,
+ svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ transaction_t *txn;
+ node_revision_t *noderev;
+ svn_fs_id_t *root_id;
+
+ txn = apr_pcalloc(pool, sizeof(*txn));
+ root_id = svn_fs_fs__id_txn_create_root(txn_id, pool);
+
+ SVN_ERR(svn_fs_fs__get_node_revision(&noderev, fs, root_id, pool, pool));
+
+ txn->root_id = svn_fs_fs__id_copy(noderev->id, pool);
+ txn->base_id = svn_fs_fs__id_copy(noderev->predecessor_id, pool);
+ txn->copies = NULL;
+
+ *txn_p = txn;
+
+ return SVN_NO_ERROR;
+}
+
+/* Write out the currently available next node_id NODE_ID and copy_id
+ COPY_ID for transaction TXN_ID in filesystem FS. The next node-id is
+ used both for creating new unique nodes for the given transaction, as
+ well as uniquifying representations. Perform temporary allocations in
+ POOL. */
+static svn_error_t *
+write_next_ids(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_uint64_t node_id,
+ apr_uint64_t copy_id,
+ apr_pool_t *pool)
+{
+ apr_file_t *file;
+ char buffer[2 * SVN_INT64_BUFFER_SIZE + 2];
+ char *p = buffer;
+
+ p += svn__ui64tobase36(p, node_id);
+ *(p++) = ' ';
+ p += svn__ui64tobase36(p, copy_id);
+ *(p++) = '\n';
+ *(p++) = '\0';
+
+ SVN_ERR(svn_io_file_open(&file,
+ path_txn_next_ids(fs, txn_id, pool),
+ APR_WRITE | APR_TRUNCATE,
+ APR_OS_DEFAULT, pool));
+ SVN_ERR(svn_io_file_write_full(file, buffer, p - buffer, NULL, pool));
+ return svn_io_file_close(file, pool);
+}
+
+/* Find out what the next unique node-id and copy-id are for
+ transaction TXN_ID in filesystem FS. Store the results in *NODE_ID
+ and *COPY_ID. The next node-id is used both for creating new unique
+ nodes for the given transaction, as well as uniquifying representations.
+ Perform all allocations in POOL. */
+static svn_error_t *
+read_next_ids(apr_uint64_t *node_id,
+ apr_uint64_t *copy_id,
+ svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ svn_stringbuf_t *buf;
+ const char *str;
+ SVN_ERR(svn_fs_fs__read_content(&buf,
+ path_txn_next_ids(fs, txn_id, pool),
+ pool));
+
+ /* Parse this into two separate strings. */
+
+ str = buf->data;
+ *node_id = svn__base36toui64(&str, str);
+ if (*str != ' ')
+ return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
+ _("next-id file corrupt"));
+
+ ++str;
+ *copy_id = svn__base36toui64(&str, str);
+ if (*str != '\n')
+ return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
+ _("next-id file corrupt"));
+
+ return SVN_NO_ERROR;
+}
+
+/* Get a new and unique to this transaction node-id for transaction
+ TXN_ID in filesystem FS. Store the new node-id in *NODE_ID_P.
+ Node-ids are guaranteed to be unique to this transction, but may
+ not necessarily be sequential. Perform all allocations in POOL. */
+static svn_error_t *
+get_new_txn_node_id(svn_fs_fs__id_part_t *node_id_p,
+ svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ apr_uint64_t node_id, copy_id;
+
+ /* First read in the current next-ids file. */
+ SVN_ERR(read_next_ids(&node_id, &copy_id, fs, txn_id, pool));
+
+ node_id_p->revision = SVN_INVALID_REVNUM;
+ node_id_p->number = node_id;
+
+ SVN_ERR(write_next_ids(fs, txn_id, ++node_id, copy_id, pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__reserve_copy_id(svn_fs_fs__id_part_t *copy_id_p,
+ svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ apr_uint64_t node_id, copy_id;
+
+ /* First read in the current next-ids file. */
+ SVN_ERR(read_next_ids(&node_id, &copy_id, fs, txn_id, pool));
+
+ /* this is an in-txn ID now */
+ copy_id_p->revision = SVN_INVALID_REVNUM;
+ copy_id_p->number = copy_id;
+
+ /* Update the ID counter file */
+ SVN_ERR(write_next_ids(fs, txn_id, node_id, ++copy_id, pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__create_node(const svn_fs_id_t **id_p,
+ svn_fs_t *fs,
+ node_revision_t *noderev,
+ const svn_fs_fs__id_part_t *copy_id,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ svn_fs_fs__id_part_t node_id;
+ const svn_fs_id_t *id;
+
+ /* Get a new node-id for this node. */
+ SVN_ERR(get_new_txn_node_id(&node_id, fs, txn_id, pool));
+
+ id = svn_fs_fs__id_txn_create(&node_id, copy_id, txn_id, pool);
+
+ noderev->id = id;
+
+ SVN_ERR(svn_fs_fs__put_node_revision(fs, noderev->id, noderev, FALSE, pool));
+
+ *id_p = id;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__purge_txn(svn_fs_t *fs,
+ const char *txn_id_str,
+ apr_pool_t *pool)
+{
+ fs_fs_data_t *ffd = fs->fsap_data;
+ svn_fs_fs__id_part_t txn_id;
+ SVN_ERR(svn_fs_fs__id_txn_parse(&txn_id, txn_id_str));
+
+ /* Remove the shared transaction object associated with this transaction. */
+ SVN_ERR(purge_shared_txn(fs, &txn_id, pool));
+ /* Remove the directory associated with this transaction. */
+ SVN_ERR(svn_io_remove_dir2(svn_fs_fs__path_txn_dir(fs, &txn_id, pool),
+ FALSE, NULL, NULL, pool));
+ if (ffd->format >= SVN_FS_FS__MIN_PROTOREVS_DIR_FORMAT)
+ {
+ /* Delete protorev and its lock, which aren't in the txn
+ directory. It's OK if they don't exist (for example, if this
+ is post-commit and the proto-rev has been moved into
+ place). */
+ SVN_ERR(svn_io_remove_file2(
+ svn_fs_fs__path_txn_proto_rev(fs, &txn_id, pool),
+ TRUE, pool));
+ SVN_ERR(svn_io_remove_file2(
+ svn_fs_fs__path_txn_proto_rev_lock(fs, &txn_id, pool),
+ TRUE, pool));
+ }
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_fs_fs__abort_txn(svn_fs_txn_t *txn,
+ apr_pool_t *pool)
+{
+ SVN_ERR(svn_fs__check_fs(txn->fs, TRUE));
+
+ /* Now, purge the transaction. */
+ SVN_ERR_W(svn_fs_fs__purge_txn(txn->fs, txn->id, pool),
+ apr_psprintf(pool, _("Transaction '%s' cleanup failed"),
+ txn->id));
+
+ return SVN_NO_ERROR;
+}
+
+/* Assign the UNIQUIFIER member of REP based on the current state of TXN_ID
+ * in FS. Allocate the uniquifier in POOL.
+ */
+static svn_error_t *
+set_uniquifier(svn_fs_t *fs,
+ representation_t *rep,
+ apr_pool_t *pool)
+{
+ svn_fs_fs__id_part_t temp;
+ fs_fs_data_t *ffd = fs->fsap_data;
+
+ if (ffd->format >= SVN_FS_FS__MIN_REP_SHARING_FORMAT)
+ {
+ SVN_ERR(get_new_txn_node_id(&temp, fs, &rep->txn_id, pool));
+ rep->uniquifier.noderev_txn_id = rep->txn_id;
+ rep->uniquifier.number = temp.number;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Return TRUE if the TXN_ID member of REP is in use.
+ */
+static svn_boolean_t
+is_txn_rep(const representation_t *rep)
+{
+ return svn_fs_fs__id_txn_used(&rep->txn_id);
+}
+
+/* Mark the TXN_ID member of REP as "unused".
+ */
+static void
+reset_txn_in_rep(representation_t *rep)
+{
+ svn_fs_fs__id_txn_reset(&rep->txn_id);
+}
+
+svn_error_t *
+svn_fs_fs__set_entry(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ node_revision_t *parent_noderev,
+ const char *name,
+ const svn_fs_id_t *id,
+ svn_node_kind_t kind,
+ apr_pool_t *pool)
+{
+ representation_t *rep = parent_noderev->data_rep;
+ const char *filename
+ = svn_fs_fs__path_txn_node_children(fs, parent_noderev->id, pool);
+ apr_file_t *file;
+ svn_stream_t *out;
+ fs_fs_data_t *ffd = fs->fsap_data;
+ apr_pool_t *subpool = svn_pool_create(pool);
+
+ if (!rep || !is_txn_rep(rep))
+ {
+ apr_array_header_t *entries;
+
+ /* Before we can modify the directory, we need to dump its old
+ contents into a mutable representation file. */
+ SVN_ERR(svn_fs_fs__rep_contents_dir(&entries, fs, parent_noderev,
+ subpool, subpool));
+ SVN_ERR(svn_io_file_open(&file, filename,
+ APR_WRITE | APR_CREATE | APR_BUFFERED,
+ APR_OS_DEFAULT, pool));
+ out = svn_stream_from_aprfile2(file, TRUE, pool);
+ SVN_ERR(unparse_dir_entries(entries, out, subpool));
+
+ svn_pool_clear(subpool);
+
+ /* Mark the node-rev's data rep as mutable. */
+ rep = apr_pcalloc(pool, sizeof(*rep));
+ rep->revision = SVN_INVALID_REVNUM;
+ rep->txn_id = *txn_id;
+ SVN_ERR(set_uniquifier(fs, rep, pool));
+ parent_noderev->data_rep = rep;
+ SVN_ERR(svn_fs_fs__put_node_revision(fs, parent_noderev->id,
+ parent_noderev, FALSE, pool));
+ }
+ else
+ {
+ /* The directory rep is already mutable, so just open it for append. */
+ SVN_ERR(svn_io_file_open(&file, filename, APR_WRITE | APR_APPEND,
+ APR_OS_DEFAULT, pool));
+ out = svn_stream_from_aprfile2(file, TRUE, pool);
+ }
+
+ /* if we have a directory cache for this transaction, update it */
+ if (ffd->txn_dir_cache)
+ {
+ /* build parameters: (name, new entry) pair */
+ const char *key =
+ svn_fs_fs__id_unparse(parent_noderev->id, subpool)->data;
+ replace_baton_t baton;
+
+ baton.name = name;
+ baton.new_entry = NULL;
+
+ if (id)
+ {
+ baton.new_entry = apr_pcalloc(subpool, sizeof(*baton.new_entry));
+ baton.new_entry->name = name;
+ baton.new_entry->kind = kind;
+ baton.new_entry->id = id;
+ }
+
+ /* actually update the cached directory (if cached) */
+ SVN_ERR(svn_cache__set_partial(ffd->txn_dir_cache, key,
+ svn_fs_fs__replace_dir_entry, &baton,
+ subpool));
+ }
+ svn_pool_clear(subpool);
+
+ /* Append an incremental hash entry for the entry change. */
+ if (id)
+ {
+ svn_fs_dirent_t entry;
+ entry.name = name;
+ entry.id = id;
+ entry.kind = kind;
+
+ SVN_ERR(unparse_dir_entry(&entry, out, subpool));
+ }
+ else
+ {
+ SVN_ERR(svn_stream_printf(out, subpool, "D %" APR_SIZE_T_FMT "\n%s\n",
+ strlen(name), name));
+ }
+
+ SVN_ERR(svn_io_file_close(file, subpool));
+ svn_pool_destroy(subpool);
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__add_change(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ const char *path,
+ const svn_fs_id_t *id,
+ svn_fs_path_change_kind_t change_kind,
+ svn_boolean_t text_mod,
+ svn_boolean_t prop_mod,
+ svn_boolean_t mergeinfo_mod,
+ svn_node_kind_t node_kind,
+ svn_revnum_t copyfrom_rev,
+ const char *copyfrom_path,
+ apr_pool_t *pool)
+{
+ apr_file_t *file;
+ svn_fs_path_change2_t *change;
+ apr_hash_t *changes = apr_hash_make(pool);
+
+ /* Not using APR_BUFFERED to append change in one atomic write operation. */
+ SVN_ERR(svn_io_file_open(&file, path_txn_changes(fs, txn_id, pool),
+ APR_APPEND | APR_WRITE | APR_CREATE,
+ APR_OS_DEFAULT, pool));
+
+ change = svn_fs__path_change_create_internal(id, change_kind, pool);
+ change->text_mod = text_mod;
+ change->prop_mod = prop_mod;
+ change->mergeinfo_mod = mergeinfo_mod
+ ? svn_tristate_true
+ : svn_tristate_false;
+ change->node_kind = node_kind;
+ change->copyfrom_known = TRUE;
+ change->copyfrom_rev = copyfrom_rev;
+ if (copyfrom_path)
+ change->copyfrom_path = apr_pstrdup(pool, copyfrom_path);
+
+ svn_hash_sets(changes, path, change);
+ SVN_ERR(svn_fs_fs__write_changes(svn_stream_from_aprfile2(file, TRUE, pool),
+ fs, changes, FALSE, pool));
+
+ return svn_io_file_close(file, pool);
+}
+
+/* If the transaction TXN_ID in FS uses logical addressing, store the
+ * (ITEM_INDEX, OFFSET) pair in the txn's log-to-phys proto index file.
+ * Use POOL for allocations.
+ */
+static svn_error_t *
+store_l2p_index_entry(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_off_t offset,
+ apr_uint64_t item_index,
+ apr_pool_t *pool)
+{
+ if (svn_fs_fs__use_log_addressing(fs))
+ {
+ const char *path = svn_fs_fs__path_l2p_proto_index(fs, txn_id, pool);
+ apr_file_t *file;
+ SVN_ERR(svn_fs_fs__l2p_proto_index_open(&file, path, pool));
+ SVN_ERR(svn_fs_fs__l2p_proto_index_add_entry(file, offset,
+ item_index, pool));
+ SVN_ERR(svn_io_file_close(file, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* If the transaction TXN_ID in FS uses logical addressing, store ENTRY
+ * in the phys-to-log proto index file of transaction TXN_ID.
+ * Use POOL for allocations.
+ */
+static svn_error_t *
+store_p2l_index_entry(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ svn_fs_fs__p2l_entry_t *entry,
+ apr_pool_t *pool)
+{
+ if (svn_fs_fs__use_log_addressing(fs))
+ {
+ const char *path = svn_fs_fs__path_p2l_proto_index(fs, txn_id, pool);
+ apr_file_t *file;
+ SVN_ERR(svn_fs_fs__p2l_proto_index_open(&file, path, pool));
+ SVN_ERR(svn_fs_fs__p2l_proto_index_add_entry(file, entry, pool));
+ SVN_ERR(svn_io_file_close(file, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Allocate an item index for the given MY_OFFSET in the transaction TXN_ID
+ * of file system FS and return it in *ITEM_INDEX. For old formats, it
+ * will simply return the offset as item index; in new formats, it will
+ * increment the txn's item index counter file and store the mapping in
+ * the proto index file. Use POOL for allocations.
+ */
+static svn_error_t *
+allocate_item_index(apr_uint64_t *item_index,
+ svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_off_t my_offset,
+ apr_pool_t *pool)
+{
+ if (svn_fs_fs__use_log_addressing(fs))
+ {
+ apr_file_t *file;
+ char buffer[SVN_INT64_BUFFER_SIZE] = { 0 };
+ svn_boolean_t eof = FALSE;
+ apr_size_t to_write;
+ apr_size_t read;
+ apr_off_t offset = 0;
+
+ /* read number, increment it and write it back to disk */
+ SVN_ERR(svn_io_file_open(&file,
+ svn_fs_fs__path_txn_item_index(fs, txn_id, pool),
+ APR_READ | APR_WRITE | APR_CREATE | APR_BUFFERED,
+ APR_OS_DEFAULT, pool));
+ SVN_ERR(svn_io_file_read_full2(file, buffer, sizeof(buffer)-1,
+ &read, &eof, pool));
+ if (read)
+ SVN_ERR(svn_cstring_atoui64(item_index, buffer));
+ else
+ *item_index = SVN_FS_FS__ITEM_INDEX_FIRST_USER;
+
+ to_write = svn__ui64toa(buffer, *item_index + 1);
+ SVN_ERR(svn_io_file_seek(file, APR_SET, &offset, pool));
+ SVN_ERR(svn_io_file_write_full(file, buffer, to_write, NULL, pool));
+ SVN_ERR(svn_io_file_close(file, pool));
+
+ /* write log-to-phys index */
+ SVN_ERR(store_l2p_index_entry(fs, txn_id, my_offset, *item_index, pool));
+ }
+ else
+ {
+ *item_index = (apr_uint64_t)my_offset;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Baton used by fnv1a_write_handler to calculate the FNV checksum
+ * before passing the data on to the INNER_STREAM.
+ */
+typedef struct fnv1a_stream_baton_t
+{
+ svn_stream_t *inner_stream;
+ svn_checksum_ctx_t *context;
+} fnv1a_stream_baton_t;
+
+/* Implement svn_write_fn_t.
+ * Update checksum and pass data on to inner stream.
+ */
+static svn_error_t *
+fnv1a_write_handler(void *baton,
+ const char *data,
+ apr_size_t *len)
+{
+ fnv1a_stream_baton_t *b = baton;
+
+ SVN_ERR(svn_checksum_update(b->context, data, *len));
+ SVN_ERR(svn_stream_write(b->inner_stream, data, len));
+
+ return SVN_NO_ERROR;
+}
+
+/* Return a stream that calculates a FNV checksum in *CONTEXT
+ * over all data written to the stream and passes that data on
+ * to INNER_STREAM. Allocate objects in POOL.
+ */
+static svn_stream_t *
+fnv1a_wrap_stream(svn_checksum_ctx_t **context,
+ svn_stream_t *inner_stream,
+ apr_pool_t *pool)
+{
+ svn_stream_t *outer_stream;
+
+ fnv1a_stream_baton_t *baton = apr_pcalloc(pool, sizeof(*baton));
+ baton->inner_stream = inner_stream;
+ baton->context = svn_checksum_ctx_create(svn_checksum_fnv1a_32x4, pool);
+ *context = baton->context;
+
+ outer_stream = svn_stream_create(baton, pool);
+ svn_stream_set_write(outer_stream, fnv1a_write_handler);
+
+ return outer_stream;
+}
+
+/* Set *DIGEST to the FNV checksum calculated in CONTEXT.
+ * Use SCRATCH_POOL for temporary allocations.
+ */
+static svn_error_t *
+fnv1a_checksum_finalize(apr_uint32_t *digest,
+ svn_checksum_ctx_t *context,
+ apr_pool_t *scratch_pool)
+{
+ svn_checksum_t *checksum;
+
+ SVN_ERR(svn_checksum_final(&checksum, context, scratch_pool));
+ SVN_ERR_ASSERT(checksum->kind == svn_checksum_fnv1a_32x4);
+ *digest = ntohl(*(const apr_uint32_t *)(checksum->digest));
+
+ return SVN_NO_ERROR;
+}
+
+/* This baton is used by the representation writing streams. It keeps
+ track of the checksum information as well as the total size of the
+ representation so far. */
+struct rep_write_baton
+{
+ /* The FS we are writing to. */
+ svn_fs_t *fs;
+
+ /* Actual file to which we are writing. */
+ svn_stream_t *rep_stream;
+
+ /* A stream from the delta combiner. Data written here gets
+ deltified, then eventually written to rep_stream. */
+ svn_stream_t *delta_stream;
+
+ /* Where is this representation header stored. */
+ apr_off_t rep_offset;
+
+ /* Start of the actual data. */
+ apr_off_t delta_start;
+
+ /* How many bytes have been written to this rep already. */
+ svn_filesize_t rep_size;
+
+ /* The node revision for which we're writing out info. */
+ node_revision_t *noderev;
+
+ /* Actual output file. */
+ apr_file_t *file;
+ /* Lock 'cookie' used to unlock the output file once we've finished
+ writing to it. */
+ void *lockcookie;
+
+ svn_checksum_ctx_t *md5_checksum_ctx;
+ svn_checksum_ctx_t *sha1_checksum_ctx;
+
+ /* calculate a modified FNV-1a checksum of the on-disk representation */
+ svn_checksum_ctx_t *fnv1a_checksum_ctx;
+
+ /* Local / scratch pool, available for temporary allocations. */
+ apr_pool_t *scratch_pool;
+
+ /* Outer / result pool. */
+ apr_pool_t *result_pool;
+};
+
+/* Handler for the write method of the representation writable stream.
+ BATON is a rep_write_baton, DATA is the data to write, and *LEN is
+ the length of this data. */
+static svn_error_t *
+rep_write_contents(void *baton,
+ const char *data,
+ apr_size_t *len)
+{
+ struct rep_write_baton *b = baton;
+
+ SVN_ERR(svn_checksum_update(b->md5_checksum_ctx, data, *len));
+ SVN_ERR(svn_checksum_update(b->sha1_checksum_ctx, data, *len));
+ b->rep_size += *len;
+
+ /* If we are writing a delta, use that stream. */
+ if (b->delta_stream)
+ return svn_stream_write(b->delta_stream, data, len);
+ else
+ return svn_stream_write(b->rep_stream, data, len);
+}
+
+/* Set *SPANNED to the number of shards touched when walking WALK steps on
+ * NODEREV's predecessor chain in FS. Use POOL for temporary allocations.
+ */
+static svn_error_t *
+shards_spanned(int *spanned,
+ svn_fs_t *fs,
+ node_revision_t *noderev,
+ int walk,
+ apr_pool_t *pool)
+{
+ fs_fs_data_t *ffd = fs->fsap_data;
+ int shard_size = ffd->max_files_per_dir ? ffd->max_files_per_dir : 1;
+ apr_pool_t *iterpool;
+
+ int count = walk ? 1 : 0; /* The start of a walk already touches a shard. */
+ svn_revnum_t shard, last_shard = ffd->youngest_rev_cache / shard_size;
+ iterpool = svn_pool_create(pool);
+ while (walk-- && noderev->predecessor_count)
+ {
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_fs_fs__get_node_revision(&noderev, fs,
+ noderev->predecessor_id, pool,
+ iterpool));
+ shard = svn_fs_fs__id_rev(noderev->id) / shard_size;
+ if (shard != last_shard)
+ {
+ ++count;
+ last_shard = shard;
+ }
+ }
+ svn_pool_destroy(iterpool);
+
+ *spanned = count;
+ return SVN_NO_ERROR;
+}
+
+/* Given a node-revision NODEREV in filesystem FS, return the
+ representation in *REP to use as the base for a text representation
+ delta if PROPS is FALSE. If PROPS has been set, a suitable props
+ base representation will be returned. Perform temporary allocations
+ in *POOL. */
+static svn_error_t *
+choose_delta_base(representation_t **rep,
+ svn_fs_t *fs,
+ node_revision_t *noderev,
+ svn_boolean_t props,
+ apr_pool_t *pool)
+{
+ /* The zero-based index (counting from the "oldest" end), along NODEREVs line
+ * predecessors, of the node-rev we will use as delta base. */
+ int count;
+ /* The length of the linear part of a delta chain. (Delta chains use
+ * skip-delta bits for the high-order bits and are linear in the low-order
+ * bits.) */
+ int walk;
+ node_revision_t *base;
+ fs_fs_data_t *ffd = fs->fsap_data;
+ apr_pool_t *iterpool;
+
+ /* If we have no predecessors, or that one is empty, then use the empty
+ * stream as a base. */
+ if (! noderev->predecessor_count)
+ {
+ *rep = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ /* Flip the rightmost '1' bit of the predecessor count to determine
+ which file rev (counting from 0) we want to use. (To see why
+ count & (count - 1) unsets the rightmost set bit, think about how
+ you decrement a binary number.) */
+ count = noderev->predecessor_count;
+ count = count & (count - 1);
+
+ /* Finding the delta base over a very long distance can become extremely
+ expensive for very deep histories, possibly causing client timeouts etc.
+ OTOH, this is a rare operation and its gains are minimal. Lets simply
+ start deltification anew close every other 1000 changes or so. */
+ walk = noderev->predecessor_count - count;
+ if (walk > (int)ffd->max_deltification_walk)
+ {
+ *rep = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ /* We use skip delta for limiting the number of delta operations
+ along very long node histories. Close to HEAD however, we create
+ a linear history to minimize delta size. */
+ if (walk < (int)ffd->max_linear_deltification)
+ {
+ int shards;
+ SVN_ERR(shards_spanned(&shards, fs, noderev, walk, pool));
+
+ /* We also don't want the linear deltification to span more shards
+ than if deltas we used in a simple skip-delta scheme. */
+ if ((1 << (--shards)) <= walk)
+ count = noderev->predecessor_count - 1;
+ }
+
+ /* Walk back a number of predecessors equal to the difference
+ between count and the original predecessor count. (For example,
+ if noderev has ten predecessors and we want the eighth file rev,
+ walk back two predecessors.) */
+ base = noderev;
+ iterpool = svn_pool_create(pool);
+ while ((count++) < noderev->predecessor_count)
+ {
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_fs_fs__get_node_revision(&base, fs,
+ base->predecessor_id, pool,
+ iterpool));
+ }
+ svn_pool_destroy(iterpool);
+
+ /* return a suitable base representation */
+ *rep = props ? base->prop_rep : base->data_rep;
+
+ /* if we encountered a shared rep, its parent chain may be different
+ * from the node-rev parent chain. */
+ if (*rep)
+ {
+ int chain_length = 0;
+ int shard_count = 0;
+
+ /* Very short rep bases are simply not worth it as we are unlikely
+ * to re-coup the deltification space overhead of 20+ bytes. */
+ svn_filesize_t rep_size = (*rep)->expanded_size
+ ? (*rep)->expanded_size
+ : (*rep)->size;
+ if (rep_size < 64)
+ {
+ *rep = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ /* Check whether the length of the deltification chain is acceptable.
+ * Otherwise, shared reps may form a non-skipping delta chain in
+ * extreme cases. */
+ SVN_ERR(svn_fs_fs__rep_chain_length(&chain_length, &shard_count,
+ *rep, fs, pool));
+
+ /* Some reasonable limit, depending on how acceptable longer linear
+ * chains are in this repo. Also, allow for some minimal chain. */
+ if (chain_length >= 2 * (int)ffd->max_linear_deltification + 2)
+ *rep = NULL;
+ else
+ /* To make it worth opening additional shards / pack files, we
+ * require that the reps have a certain minimal size. To deltify
+ * against a rep in different shard, the lower limit is 512 bytes
+ * and doubles with every extra shard to visit along the delta
+ * chain. */
+ if ( shard_count > 1
+ && ((svn_filesize_t)128 << shard_count) >= rep_size)
+ *rep = NULL;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Something went wrong and the pool for the rep write is being
+ cleared before we've finished writing the rep. So we need
+ to remove the rep from the protorevfile and we need to unlock
+ the protorevfile. */
+static apr_status_t
+rep_write_cleanup(void *data)
+{
+ struct rep_write_baton *b = data;
+ svn_error_t *err;
+
+ /* Truncate and close the protorevfile. */
+ err = svn_io_file_trunc(b->file, b->rep_offset, b->scratch_pool);
+ err = svn_error_compose_create(err, svn_io_file_close(b->file,
+ b->scratch_pool));
+
+ /* Remove our lock regardless of any preceding errors so that the
+ being_written flag is always removed and stays consistent with the
+ file lock which will be removed no matter what since the pool is
+ going away. */
+ err = svn_error_compose_create(err,
+ unlock_proto_rev(b->fs,
+ svn_fs_fs__id_txn_id(b->noderev->id),
+ b->lockcookie, b->scratch_pool));
+ if (err)
+ {
+ apr_status_t rc = err->apr_err;
+ svn_error_clear(err);
+ return rc;
+ }
+
+ return APR_SUCCESS;
+}
+
+/* Get a rep_write_baton and store it in *WB_P for the representation
+ indicated by NODEREV in filesystem FS. Perform allocations in
+ POOL. Only appropriate for file contents, not for props or
+ directory contents. */
+static svn_error_t *
+rep_write_get_baton(struct rep_write_baton **wb_p,
+ svn_fs_t *fs,
+ node_revision_t *noderev,
+ apr_pool_t *pool)
+{
+ struct rep_write_baton *b;
+ apr_file_t *file;
+ representation_t *base_rep;
+ svn_stream_t *source;
+ svn_txdelta_window_handler_t wh;
+ void *whb;
+ fs_fs_data_t *ffd = fs->fsap_data;
+ int diff_version = ffd->format >= SVN_FS_FS__MIN_SVNDIFF1_FORMAT ? 1 : 0;
+ svn_fs_fs__rep_header_t header = { 0 };
+
+ b = apr_pcalloc(pool, sizeof(*b));
+
+ b->sha1_checksum_ctx = svn_checksum_ctx_create(svn_checksum_sha1, pool);
+ b->md5_checksum_ctx = svn_checksum_ctx_create(svn_checksum_md5, pool);
+
+ b->fs = fs;
+ b->result_pool = pool;
+ b->scratch_pool = svn_pool_create(pool);
+ b->rep_size = 0;
+ b->noderev = noderev;
+
+ /* Open the prototype rev file and seek to its end. */
+ SVN_ERR(get_writable_proto_rev(&file, &b->lockcookie,
+ fs, svn_fs_fs__id_txn_id(noderev->id),
+ b->scratch_pool));
+
+ b->file = file;
+ b->rep_stream = fnv1a_wrap_stream(&b->fnv1a_checksum_ctx,
+ svn_stream_from_aprfile2(file, TRUE,
+ b->scratch_pool),
+ b->scratch_pool);
+
+ SVN_ERR(svn_fs_fs__get_file_offset(&b->rep_offset, file, b->scratch_pool));
+
+ /* Get the base for this delta. */
+ SVN_ERR(choose_delta_base(&base_rep, fs, noderev, FALSE, b->scratch_pool));
+ SVN_ERR(svn_fs_fs__get_contents(&source, fs, base_rep, TRUE,
+ b->scratch_pool));
+
+ /* Write out the rep header. */
+ if (base_rep)
+ {
+ header.base_revision = base_rep->revision;
+ header.base_item_index = base_rep->item_index;
+ header.base_length = base_rep->size;
+ header.type = svn_fs_fs__rep_delta;
+ }
+ else
+ {
+ header.type = svn_fs_fs__rep_self_delta;
+ }
+ SVN_ERR(svn_fs_fs__write_rep_header(&header, b->rep_stream,
+ b->scratch_pool));
+
+ /* Now determine the offset of the actual svndiff data. */
+ SVN_ERR(svn_fs_fs__get_file_offset(&b->delta_start, file,
+ b->scratch_pool));
+
+ /* Cleanup in case something goes wrong. */
+ apr_pool_cleanup_register(b->scratch_pool, b, rep_write_cleanup,
+ apr_pool_cleanup_null);
+
+ /* Prepare to write the svndiff data. */
+ svn_txdelta_to_svndiff3(&wh,
+ &whb,
+ b->rep_stream,
+ diff_version,
+ ffd->delta_compression_level,
+ pool);
+
+ b->delta_stream = svn_txdelta_target_push(wh, whb, source,
+ b->scratch_pool);
+
+ *wb_p = b;
+
+ return SVN_NO_ERROR;
+}
+
+/* For REP->SHA1_CHECKSUM, try to find an already existing representation
+ in FS and return it in *OUT_REP. If no such representation exists or
+ if rep sharing has been disabled for FS, NULL will be returned. Since
+ there may be new duplicate representations within the same uncommitted
+ revision, those can be passed in REPS_HASH (maps a sha1 digest onto
+ representation_t*), otherwise pass in NULL for REPS_HASH.
+
+ The content of both representations will be compared, taking REP's content
+ from FILE at OFFSET. Only if they actually match, will *OLD_REP not be
+ NULL.
+
+ Use RESULT_POOL for *OLD_REP allocations and SCRATCH_POOL for temporaries.
+ The lifetime of *OLD_REP is limited by both, RESULT_POOL and REP lifetime.
+ */
+static svn_error_t *
+get_shared_rep(representation_t **old_rep,
+ svn_fs_t *fs,
+ representation_t *rep,
+ apr_file_t *file,
+ apr_off_t offset,
+ apr_hash_t *reps_hash,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err;
+ fs_fs_data_t *ffd = fs->fsap_data;
+
+ svn_checksum_t checksum;
+ checksum.digest = rep->sha1_digest;
+ checksum.kind = svn_checksum_sha1;
+
+ /* Return NULL, if rep sharing has been disabled. */
+ *old_rep = NULL;
+ if (!ffd->rep_sharing_allowed)
+ return SVN_NO_ERROR;
+
+ /* Check and see if we already have a representation somewhere that's
+ identical to the one we just wrote out. Start with the hash lookup
+ because it is cheepest. */
+ if (reps_hash)
+ *old_rep = apr_hash_get(reps_hash,
+ rep->sha1_digest,
+ APR_SHA1_DIGESTSIZE);
+
+ /* If we haven't found anything yet, try harder and consult our DB. */
+ if (*old_rep == NULL)
+ {
+ err = svn_fs_fs__get_rep_reference(old_rep, fs, &checksum, result_pool);
+ /* ### Other error codes that we shouldn't mask out? */
+ if (err == SVN_NO_ERROR)
+ {
+ if (*old_rep)
+ SVN_ERR(svn_fs_fs__check_rep(*old_rep, fs, NULL, scratch_pool));
+ }
+ else if (err->apr_err == SVN_ERR_FS_CORRUPT
+ || SVN_ERROR_IN_CATEGORY(err->apr_err,
+ SVN_ERR_MALFUNC_CATEGORY_START))
+ {
+ /* Fatal error; don't mask it.
+
+ In particular, this block is triggered when the rep-cache refers
+ to revisions in the future. We signal that as a corruption situation
+ since, once those revisions are less than youngest (because of more
+ commits), the rep-cache would be invalid.
+ */
+ SVN_ERR(err);
+ }
+ else
+ {
+ /* Something's wrong with the rep-sharing index. We can continue
+ without rep-sharing, but warn.
+ */
+ (fs->warning)(fs->warning_baton, err);
+ svn_error_clear(err);
+ *old_rep = NULL;
+ }
+ }
+
+ /* look for intra-revision matches (usually data reps but not limited
+ to them in case props happen to look like some data rep)
+ */
+ if (*old_rep == NULL && is_txn_rep(rep))
+ {
+ svn_node_kind_t kind;
+ const char *file_name
+ = path_txn_sha1(fs, &rep->txn_id, rep->sha1_digest, scratch_pool);
+
+ /* in our txn, is there a rep file named with the wanted SHA1?
+ If so, read it and use that rep.
+ */
+ SVN_ERR(svn_io_check_path(file_name, &kind, scratch_pool));
+ if (kind == svn_node_file)
+ {
+ svn_stringbuf_t *rep_string;
+ SVN_ERR(svn_stringbuf_from_file2(&rep_string, file_name,
+ scratch_pool));
+ SVN_ERR(svn_fs_fs__parse_representation(old_rep, rep_string,
+ result_pool, scratch_pool));
+ }
+ }
+
+ if (!*old_rep)
+ return SVN_NO_ERROR;
+
+ /* We don't want 0-length PLAIN representations to replace non-0-length
+ ones (see issue #4554). Take into account that EXPANDED_SIZE may be
+ 0 in which case we have to check the on-disk SIZE. Also, this doubles
+ as a simple guard against general rep-cache induced corruption. */
+ if ( ((*old_rep)->expanded_size != rep->expanded_size)
+ || ((rep->expanded_size == 0) && ((*old_rep)->size != rep->size)))
+ {
+ *old_rep = NULL;
+ }
+ else
+ {
+ /* Add information that is missing in the cached data.
+ Use the old rep for this content. */
+ memcpy((*old_rep)->md5_digest, rep->md5_digest, sizeof(rep->md5_digest));
+ (*old_rep)->uniquifier = rep->uniquifier;
+ }
+
+ /* If we (very likely) found a matching representation, compare the actual
+ * contents such that we can be sure that no rep-cache.db corruption or
+ * hash collision produced a false positive. */
+ if (*old_rep)
+ {
+ apr_off_t old_position;
+ svn_stream_t *contents;
+ svn_stream_t *old_contents;
+ svn_boolean_t same;
+
+ /* The existing representation may itself be part of the current
+ * transaction. In that case, it may be in different stages of
+ * the commit finalization process.
+ *
+ * OLD_REP_NORM is the same as that OLD_REP but it is assigned
+ * explicitly to REP's transaction if OLD_REP does not point
+ * to an already committed revision. This then prevents the
+ * revision lookup and the txn data will be accessed.
+ */
+ representation_t old_rep_norm = **old_rep;
+ if ( !SVN_IS_VALID_REVNUM(old_rep_norm.revision)
+ || old_rep_norm.revision > ffd->youngest_rev_cache)
+ old_rep_norm.txn_id = rep->txn_id;
+
+ /* Make sure we can later restore FILE's current position. */
+ SVN_ERR(svn_fs_fs__get_file_offset(&old_position, file, scratch_pool));
+
+ /* Compare the two representations.
+ * Note that the stream comparison might also produce MD5 checksum
+ * errors or other failures in case of SHA1 collisions. */
+ SVN_ERR(svn_fs_fs__get_contents_from_file(&contents, fs, rep, file,
+ offset, scratch_pool));
+ SVN_ERR(svn_fs_fs__get_contents(&old_contents, fs, &old_rep_norm,
+ FALSE, scratch_pool));
+ err = svn_stream_contents_same2(&same, contents, old_contents,
+ scratch_pool);
+
+ /* A mismatch should be extremely rare.
+ * If it does happen, reject the commit. */
+ if (!same || err)
+ {
+ /* SHA1 collision or worse. */
+ svn_stringbuf_t *old_rep_str
+ = svn_fs_fs__unparse_representation(*old_rep,
+ ffd->format, FALSE,
+ scratch_pool,
+ scratch_pool);
+ svn_stringbuf_t *rep_str
+ = svn_fs_fs__unparse_representation(rep,
+ ffd->format, FALSE,
+ scratch_pool,
+ scratch_pool);
+ const char *checksum__str
+ = svn_checksum_to_cstring_display(&checksum, scratch_pool);
+
+ return svn_error_createf(SVN_ERR_FS_GENERAL,
+ err, "SHA1 of reps '%s' and '%s' "
+ "matches (%s) but contents differ",
+ old_rep_str->data, rep_str->data,
+ checksum__str);
+ }
+
+ /* Restore FILE's read / write position. */
+ SVN_ERR(svn_io_file_seek(file, APR_SET, &old_position, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Copy the hash sum calculation results from MD5_CTX, SHA1_CTX into REP.
+ * Use POOL for allocations.
+ */
+static svn_error_t *
+digests_final(representation_t *rep,
+ const svn_checksum_ctx_t *md5_ctx,
+ const svn_checksum_ctx_t *sha1_ctx,
+ apr_pool_t *pool)
+{
+ svn_checksum_t *checksum;
+
+ SVN_ERR(svn_checksum_final(&checksum, md5_ctx, pool));
+ memcpy(rep->md5_digest, checksum->digest, svn_checksum_size(checksum));
+ SVN_ERR(svn_checksum_final(&checksum, sha1_ctx, pool));
+ rep->has_sha1 = checksum != NULL;
+ if (rep->has_sha1)
+ memcpy(rep->sha1_digest, checksum->digest, svn_checksum_size(checksum));
+
+ return SVN_NO_ERROR;
+}
+
+/* Close handler for the representation write stream. BATON is a
+ rep_write_baton. Writes out a new node-rev that correctly
+ references the representation we just finished writing. */
+static svn_error_t *
+rep_write_contents_close(void *baton)
+{
+ struct rep_write_baton *b = baton;
+ representation_t *rep;
+ representation_t *old_rep;
+ apr_off_t offset;
+
+ rep = apr_pcalloc(b->result_pool, sizeof(*rep));
+
+ /* Close our delta stream so the last bits of svndiff are written
+ out. */
+ if (b->delta_stream)
+ SVN_ERR(svn_stream_close(b->delta_stream));
+
+ /* Determine the length of the svndiff data. */
+ SVN_ERR(svn_fs_fs__get_file_offset(&offset, b->file, b->scratch_pool));
+ rep->size = offset - b->delta_start;
+
+ /* Fill in the rest of the representation field. */
+ rep->expanded_size = b->rep_size;
+ rep->txn_id = *svn_fs_fs__id_txn_id(b->noderev->id);
+ SVN_ERR(set_uniquifier(b->fs, rep, b->scratch_pool));
+ rep->revision = SVN_INVALID_REVNUM;
+
+ /* Finalize the checksum. */
+ SVN_ERR(digests_final(rep, b->md5_checksum_ctx, b->sha1_checksum_ctx,
+ b->result_pool));
+
+ /* Check and see if we already have a representation somewhere that's
+ identical to the one we just wrote out. */
+ SVN_ERR(get_shared_rep(&old_rep, b->fs, rep, b->file, b->rep_offset, NULL,
+ b->result_pool, b->scratch_pool));
+
+ if (old_rep)
+ {
+ /* We need to erase from the protorev the data we just wrote. */
+ SVN_ERR(svn_io_file_trunc(b->file, b->rep_offset, b->scratch_pool));
+
+ /* Use the old rep for this content. */
+ b->noderev->data_rep = old_rep;
+ }
+ else
+ {
+ /* Write out our cosmetic end marker. */
+ SVN_ERR(svn_stream_puts(b->rep_stream, "ENDREP\n"));
+ SVN_ERR(allocate_item_index(&rep->item_index, b->fs, &rep->txn_id,
+ b->rep_offset, b->scratch_pool));
+
+ b->noderev->data_rep = rep;
+ }
+
+ /* Remove cleanup callback. */
+ apr_pool_cleanup_kill(b->scratch_pool, b, rep_write_cleanup);
+
+ /* Write out the new node-rev information. */
+ SVN_ERR(svn_fs_fs__put_node_revision(b->fs, b->noderev->id, b->noderev,
+ FALSE, b->scratch_pool));
+ if (!old_rep)
+ {
+ svn_fs_fs__p2l_entry_t entry;
+
+ entry.offset = b->rep_offset;
+ SVN_ERR(svn_fs_fs__get_file_offset(&offset, b->file, b->scratch_pool));
+ entry.size = offset - b->rep_offset;
+ entry.type = SVN_FS_FS__ITEM_TYPE_FILE_REP;
+ entry.item.revision = SVN_INVALID_REVNUM;
+ entry.item.number = rep->item_index;
+ SVN_ERR(fnv1a_checksum_finalize(&entry.fnv1_checksum,
+ b->fnv1a_checksum_ctx,
+ b->scratch_pool));
+
+ SVN_ERR(store_p2l_index_entry(b->fs, &rep->txn_id, &entry,
+ b->scratch_pool));
+ }
+
+ SVN_ERR(svn_io_file_close(b->file, b->scratch_pool));
+
+ /* Write the sha1->rep mapping *after* we successfully written node
+ * revision to disk. */
+ if (!old_rep)
+ SVN_ERR(store_sha1_rep_mapping(b->fs, b->noderev, b->scratch_pool));
+
+ SVN_ERR(unlock_proto_rev(b->fs, &rep->txn_id, b->lockcookie,
+ b->scratch_pool));
+ svn_pool_destroy(b->scratch_pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Store a writable stream in *CONTENTS_P that will receive all data
+ written and store it as the file data representation referenced by
+ NODEREV in filesystem FS. Perform temporary allocations in
+ POOL. Only appropriate for file data, not props or directory
+ contents. */
+static svn_error_t *
+set_representation(svn_stream_t **contents_p,
+ svn_fs_t *fs,
+ node_revision_t *noderev,
+ apr_pool_t *pool)
+{
+ struct rep_write_baton *wb;
+
+ if (! svn_fs_fs__id_is_txn(noderev->id))
+ return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
+ _("Attempted to write to non-transaction '%s'"),
+ svn_fs_fs__id_unparse(noderev->id, pool)->data);
+
+ SVN_ERR(rep_write_get_baton(&wb, fs, noderev, pool));
+
+ *contents_p = svn_stream_create(wb, pool);
+ svn_stream_set_write(*contents_p, rep_write_contents);
+ svn_stream_set_close(*contents_p, rep_write_contents_close);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__set_contents(svn_stream_t **stream,
+ svn_fs_t *fs,
+ node_revision_t *noderev,
+ apr_pool_t *pool)
+{
+ if (noderev->kind != svn_node_file)
+ return svn_error_create(SVN_ERR_FS_NOT_FILE, NULL,
+ _("Can't set text contents of a directory"));
+
+ return set_representation(stream, fs, noderev, pool);
+}
+
+svn_error_t *
+svn_fs_fs__create_successor(const svn_fs_id_t **new_id_p,
+ svn_fs_t *fs,
+ const svn_fs_id_t *old_idp,
+ node_revision_t *new_noderev,
+ const svn_fs_fs__id_part_t *copy_id,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ const svn_fs_id_t *id;
+
+ if (! copy_id)
+ copy_id = svn_fs_fs__id_copy_id(old_idp);
+ id = svn_fs_fs__id_txn_create(svn_fs_fs__id_node_id(old_idp), copy_id,
+ txn_id, pool);
+
+ new_noderev->id = id;
+
+ if (! new_noderev->copyroot_path)
+ {
+ new_noderev->copyroot_path = apr_pstrdup(pool,
+ new_noderev->created_path);
+ new_noderev->copyroot_rev = svn_fs_fs__id_rev(new_noderev->id);
+ }
+
+ SVN_ERR(svn_fs_fs__put_node_revision(fs, new_noderev->id, new_noderev, FALSE,
+ pool));
+
+ *new_id_p = id;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__set_proplist(svn_fs_t *fs,
+ node_revision_t *noderev,
+ apr_hash_t *proplist,
+ apr_pool_t *pool)
+{
+ const char *filename
+ = svn_fs_fs__path_txn_node_props(fs, noderev->id, pool);
+ apr_file_t *file;
+ svn_stream_t *out;
+
+ /* Dump the property list to the mutable property file. */
+ SVN_ERR(svn_io_file_open(&file, filename,
+ APR_WRITE | APR_CREATE | APR_TRUNCATE
+ | APR_BUFFERED, APR_OS_DEFAULT, pool));
+ out = svn_stream_from_aprfile2(file, TRUE, pool);
+ SVN_ERR(svn_hash_write2(proplist, out, SVN_HASH_TERMINATOR, pool));
+ SVN_ERR(svn_io_file_close(file, pool));
+
+ /* Mark the node-rev's prop rep as mutable, if not already done. */
+ if (!noderev->prop_rep || !is_txn_rep(noderev->prop_rep))
+ {
+ noderev->prop_rep = apr_pcalloc(pool, sizeof(*noderev->prop_rep));
+ noderev->prop_rep->txn_id = *svn_fs_fs__id_txn_id(noderev->id);
+ SVN_ERR(svn_fs_fs__put_node_revision(fs, noderev->id, noderev, FALSE,
+ pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* This baton is used by the stream created for write_container_rep. */
+struct write_container_baton
+{
+ svn_stream_t *stream;
+
+ apr_size_t size;
+
+ svn_checksum_ctx_t *md5_ctx;
+ svn_checksum_ctx_t *sha1_ctx;
+};
+
+/* The handler for the write_container_rep stream. BATON is a
+ write_container_baton, DATA has the data to write and *LEN is the number
+ of bytes to write. */
+static svn_error_t *
+write_container_handler(void *baton,
+ const char *data,
+ apr_size_t *len)
+{
+ struct write_container_baton *whb = baton;
+
+ SVN_ERR(svn_checksum_update(whb->md5_ctx, data, *len));
+ SVN_ERR(svn_checksum_update(whb->sha1_ctx, data, *len));
+
+ SVN_ERR(svn_stream_write(whb->stream, data, len));
+ whb->size += *len;
+
+ return SVN_NO_ERROR;
+}
+
+/* Callback function type. Write the data provided by BATON into STREAM. */
+typedef svn_error_t *
+(* collection_writer_t)(svn_stream_t *stream, void *baton, apr_pool_t *pool);
+
+/* Implement collection_writer_t writing the C string->svn_string_t hash
+ given as BATON. */
+static svn_error_t *
+write_hash_to_stream(svn_stream_t *stream,
+ void *baton,
+ apr_pool_t *pool)
+{
+ apr_hash_t *hash = baton;
+ SVN_ERR(svn_hash_write2(hash, stream, SVN_HASH_TERMINATOR, pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Implement collection_writer_t writing the svn_fs_dirent_t* array given
+ as BATON. */
+static svn_error_t *
+write_directory_to_stream(svn_stream_t *stream,
+ void *baton,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *dir = baton;
+ SVN_ERR(unparse_dir_entries(dir, stream, pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Write out the COLLECTION as a text representation to file FILE using
+ WRITER. In the process, record position, the total size of the dump and
+ MD5 as well as SHA1 in REP. Add the representation of type ITEM_TYPE to
+ the indexes if necessary.
+
+ If ALLOW_REP_SHARING is FALSE, rep-sharing will not be used, regardless
+ of any other option and rep-sharing settings. If rep sharing has been
+ enabled and REPS_HASH is not NULL, it will be used in addition to the
+ on-disk cache to find earlier reps with the same content. If such
+ existing reps can be found, we will truncate the one just written from
+ the file and return the existing rep.
+
+ Perform temporary allocations in SCRATCH_POOL. */
+static svn_error_t *
+write_container_rep(representation_t *rep,
+ apr_file_t *file,
+ void *collection,
+ collection_writer_t writer,
+ svn_fs_t *fs,
+ apr_hash_t *reps_hash,
+ svn_boolean_t allow_rep_sharing,
+ apr_uint32_t item_type,
+ apr_pool_t *scratch_pool)
+{
+ svn_stream_t *stream;
+ struct write_container_baton *whb;
+ svn_checksum_ctx_t *fnv1a_checksum_ctx;
+ apr_off_t offset = 0;
+ svn_fs_fs__p2l_entry_t entry;
+
+ SVN_ERR(svn_fs_fs__get_file_offset(&offset, file, scratch_pool));
+
+ whb = apr_pcalloc(scratch_pool, sizeof(*whb));
+
+ whb->stream = fnv1a_wrap_stream(&fnv1a_checksum_ctx,
+ svn_stream_from_aprfile2(file, TRUE,
+ scratch_pool),
+ scratch_pool);
+ whb->size = 0;
+ whb->md5_ctx = svn_checksum_ctx_create(svn_checksum_md5, scratch_pool);
+ whb->sha1_ctx = svn_checksum_ctx_create(svn_checksum_sha1, scratch_pool);
+
+ stream = svn_stream_create(whb, scratch_pool);
+ svn_stream_set_write(stream, write_container_handler);
+
+ SVN_ERR(svn_stream_puts(whb->stream, "PLAIN\n"));
+
+ SVN_ERR(writer(stream, collection, scratch_pool));
+
+ /* Store the results. */
+ SVN_ERR(digests_final(rep, whb->md5_ctx, whb->sha1_ctx, scratch_pool));
+
+ /* Update size info. */
+ rep->expanded_size = whb->size;
+ rep->size = whb->size;
+
+ /* Check and see if we already have a representation somewhere that's
+ identical to the one we just wrote out. */
+ if (allow_rep_sharing)
+ {
+ representation_t *old_rep;
+ SVN_ERR(get_shared_rep(&old_rep, fs, rep, file, offset, reps_hash,
+ scratch_pool, scratch_pool));
+
+ if (old_rep)
+ {
+ /* We need to erase from the protorev the data we just wrote. */
+ SVN_ERR(svn_io_file_trunc(file, offset, scratch_pool));
+
+ /* Use the old rep for this content. */
+ memcpy(rep, old_rep, sizeof (*rep));
+ return SVN_NO_ERROR;
+ }
+ }
+
+ /* Write out our cosmetic end marker. */
+ SVN_ERR(svn_stream_puts(whb->stream, "ENDREP\n"));
+
+ SVN_ERR(allocate_item_index(&rep->item_index, fs, &rep->txn_id,
+ offset, scratch_pool));
+
+ entry.offset = offset;
+ SVN_ERR(svn_fs_fs__get_file_offset(&offset, file, scratch_pool));
+ entry.size = offset - entry.offset;
+ entry.type = item_type;
+ entry.item.revision = SVN_INVALID_REVNUM;
+ entry.item.number = rep->item_index;
+ SVN_ERR(fnv1a_checksum_finalize(&entry.fnv1_checksum,
+ fnv1a_checksum_ctx,
+ scratch_pool));
+
+ SVN_ERR(store_p2l_index_entry(fs, &rep->txn_id, &entry, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Write out the COLLECTION pertaining to the NODEREV in FS as a deltified
+ text representation to file FILE using WRITER. In the process, record the
+ total size and the md5 digest in REP and add the representation of type
+ ITEM_TYPE to the indexes if necessary.
+
+ If ALLOW_REP_SHARING is FALSE, rep-sharing will not be used, regardless
+ of any other option and rep-sharing settings. If rep sharing has been
+ enabled and REPS_HASH is not NULL, it will be used in addition to the
+ on-disk cache to find earlier reps with the same content. If such
+ existing reps can be found, we will truncate the one just written from
+ the file and return the existing rep.
+
+ If ITEM_TYPE is IS_PROPS equals SVN_FS_FS__ITEM_TYPE_*_PROPS, assume
+ that we want to a props representation as the base for our delta.
+ Perform temporary allocations in SCRATCH_POOL.
+ */
+static svn_error_t *
+write_container_delta_rep(representation_t *rep,
+ apr_file_t *file,
+ void *collection,
+ collection_writer_t writer,
+ svn_fs_t *fs,
+ node_revision_t *noderev,
+ apr_hash_t *reps_hash,
+ svn_boolean_t allow_rep_sharing,
+ apr_uint32_t item_type,
+ apr_pool_t *scratch_pool)
+{
+ svn_txdelta_window_handler_t diff_wh;
+ void *diff_whb;
+
+ svn_stream_t *file_stream;
+ svn_stream_t *stream;
+ representation_t *base_rep;
+ svn_checksum_ctx_t *fnv1a_checksum_ctx;
+ svn_stream_t *source;
+ svn_fs_fs__rep_header_t header = { 0 };
+ svn_fs_fs__p2l_entry_t entry;
+
+ apr_off_t rep_end = 0;
+ apr_off_t delta_start = 0;
+ apr_off_t offset = 0;
+
+ struct write_container_baton *whb;
+ fs_fs_data_t *ffd = fs->fsap_data;
+ int diff_version = ffd->format >= SVN_FS_FS__MIN_SVNDIFF1_FORMAT ? 1 : 0;
+ svn_boolean_t is_props = (item_type == SVN_FS_FS__ITEM_TYPE_FILE_PROPS)
+ || (item_type == SVN_FS_FS__ITEM_TYPE_DIR_PROPS);
+
+ /* Get the base for this delta. */
+ SVN_ERR(choose_delta_base(&base_rep, fs, noderev, is_props, scratch_pool));
+ SVN_ERR(svn_fs_fs__get_contents(&source, fs, base_rep, FALSE, scratch_pool));
+
+ SVN_ERR(svn_fs_fs__get_file_offset(&offset, file, scratch_pool));
+
+ /* Write out the rep header. */
+ if (base_rep)
+ {
+ header.base_revision = base_rep->revision;
+ header.base_item_index = base_rep->item_index;
+ header.base_length = base_rep->size;
+ header.type = svn_fs_fs__rep_delta;
+ }
+ else
+ {
+ header.type = svn_fs_fs__rep_self_delta;
+ }
+
+ file_stream = fnv1a_wrap_stream(&fnv1a_checksum_ctx,
+ svn_stream_from_aprfile2(file, TRUE,
+ scratch_pool),
+ scratch_pool);
+ SVN_ERR(svn_fs_fs__write_rep_header(&header, file_stream, scratch_pool));
+ SVN_ERR(svn_fs_fs__get_file_offset(&delta_start, file, scratch_pool));
+
+ /* Prepare to write the svndiff data. */
+ svn_txdelta_to_svndiff3(&diff_wh,
+ &diff_whb,
+ file_stream,
+ diff_version,
+ ffd->delta_compression_level,
+ scratch_pool);
+
+ whb = apr_pcalloc(scratch_pool, sizeof(*whb));
+ whb->stream = svn_txdelta_target_push(diff_wh, diff_whb, source,
+ scratch_pool);
+ whb->size = 0;
+ whb->md5_ctx = svn_checksum_ctx_create(svn_checksum_md5, scratch_pool);
+ whb->sha1_ctx = svn_checksum_ctx_create(svn_checksum_sha1, scratch_pool);
+
+ /* serialize the hash */
+ stream = svn_stream_create(whb, scratch_pool);
+ svn_stream_set_write(stream, write_container_handler);
+
+ SVN_ERR(writer(stream, collection, scratch_pool));
+ SVN_ERR(svn_stream_close(whb->stream));
+
+ /* Store the results. */
+ SVN_ERR(digests_final(rep, whb->md5_ctx, whb->sha1_ctx, scratch_pool));
+
+ /* Update size info. */
+ SVN_ERR(svn_fs_fs__get_file_offset(&rep_end, file, scratch_pool));
+ rep->size = rep_end - delta_start;
+ rep->expanded_size = whb->size;
+
+ /* Check and see if we already have a representation somewhere that's
+ identical to the one we just wrote out. */
+ if (allow_rep_sharing)
+ {
+ representation_t *old_rep;
+ SVN_ERR(get_shared_rep(&old_rep, fs, rep, file, offset, reps_hash,
+ scratch_pool, scratch_pool));
+
+ if (old_rep)
+ {
+ /* We need to erase from the protorev the data we just wrote. */
+ SVN_ERR(svn_io_file_trunc(file, offset, scratch_pool));
+
+ /* Use the old rep for this content. */
+ memcpy(rep, old_rep, sizeof (*rep));
+ return SVN_NO_ERROR;
+ }
+ }
+
+ /* Write out our cosmetic end marker. */
+ SVN_ERR(svn_stream_puts(file_stream, "ENDREP\n"));
+
+ SVN_ERR(allocate_item_index(&rep->item_index, fs, &rep->txn_id,
+ offset, scratch_pool));
+
+ entry.offset = offset;
+ SVN_ERR(svn_fs_fs__get_file_offset(&offset, file, scratch_pool));
+ entry.size = offset - entry.offset;
+ entry.type = item_type;
+ entry.item.revision = SVN_INVALID_REVNUM;
+ entry.item.number = rep->item_index;
+ SVN_ERR(fnv1a_checksum_finalize(&entry.fnv1_checksum,
+ fnv1a_checksum_ctx,
+ scratch_pool));
+
+ SVN_ERR(store_p2l_index_entry(fs, &rep->txn_id, &entry, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Sanity check ROOT_NODEREV, a candidate for being the root node-revision
+ of (not yet committed) revision REV in FS. Use POOL for temporary
+ allocations.
+
+ If you change this function, consider updating svn_fs_fs__verify() too.
+ */
+static svn_error_t *
+validate_root_noderev(svn_fs_t *fs,
+ node_revision_t *root_noderev,
+ svn_revnum_t rev,
+ apr_pool_t *pool)
+{
+ svn_revnum_t head_revnum = rev-1;
+ int head_predecessor_count;
+
+ SVN_ERR_ASSERT(rev > 0);
+
+ /* Compute HEAD_PREDECESSOR_COUNT. */
+ {
+ svn_fs_root_t *head_revision;
+ const svn_fs_id_t *head_root_id;
+ node_revision_t *head_root_noderev;
+
+ /* Get /@HEAD's noderev. */
+ SVN_ERR(svn_fs_fs__revision_root(&head_revision, fs, head_revnum, pool));
+ SVN_ERR(svn_fs_fs__node_id(&head_root_id, head_revision, "/", pool));
+ SVN_ERR(svn_fs_fs__get_node_revision(&head_root_noderev, fs, head_root_id,
+ pool, pool));
+ head_predecessor_count = head_root_noderev->predecessor_count;
+ }
+
+ /* Check that the root noderev's predecessor count equals REV.
+
+ This kind of corruption was seen on svn.apache.org (both on
+ the root noderev and on other fspaths' noderevs); see
+ issue #4129.
+
+ Normally (rev == root_noderev->predecessor_count), but here we
+ use a more roundabout check that should only trigger on new instances
+ of the corruption, rather then trigger on each and every new commit
+ to a repository that has triggered the bug somewhere in its root
+ noderev's history.
+ */
+ if (root_noderev->predecessor_count != -1
+ && (root_noderev->predecessor_count - head_predecessor_count)
+ != (rev - head_revnum))
+ {
+ return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
+ _("predecessor count for "
+ "the root node-revision is wrong: "
+ "found (%d+%ld != %d), committing r%ld"),
+ head_predecessor_count,
+ rev - head_revnum, /* This is equal to 1. */
+ root_noderev->predecessor_count,
+ rev);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Given the potentially txn-local id PART, update that to a permanent ID
+ * based on the REVISION currently being written and the START_ID for that
+ * revision. Use the repo FORMAT to decide which implementation to use.
+ */
+static void
+get_final_id(svn_fs_fs__id_part_t *part,
+ svn_revnum_t revision,
+ apr_uint64_t start_id,
+ int format)
+{
+ if (part->revision == SVN_INVALID_REVNUM)
+ {
+ if (format >= SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT)
+ {
+ part->revision = revision;
+ }
+ else
+ {
+ part->revision = 0;
+ part->number += start_id;
+ }
+ }
+}
+
+/* Copy a node-revision specified by id ID in fileystem FS from a
+ transaction into the proto-rev-file FILE. Set *NEW_ID_P to a
+ pointer to the new node-id which will be allocated in POOL.
+ If this is a directory, copy all children as well.
+
+ START_NODE_ID and START_COPY_ID are
+ the first available node and copy ids for this filesystem, for older
+ FS formats.
+
+ REV is the revision number that this proto-rev-file will represent.
+
+ INITIAL_OFFSET is the offset of the proto-rev-file on entry to
+ commit_body.
+
+ If REPS_TO_CACHE is not NULL, append to it a copy (allocated in
+ REPS_POOL) of each data rep that is new in this revision.
+
+ If REPS_HASH is not NULL, append copies (allocated in REPS_POOL)
+ of the representations of each property rep that is new in this
+ revision.
+
+ AT_ROOT is true if the node revision being written is the root
+ node-revision. It is only controls additional sanity checking
+ logic.
+
+ Temporary allocations are also from POOL. */
+static svn_error_t *
+write_final_rev(const svn_fs_id_t **new_id_p,
+ apr_file_t *file,
+ svn_revnum_t rev,
+ svn_fs_t *fs,
+ const svn_fs_id_t *id,
+ apr_uint64_t start_node_id,
+ apr_uint64_t start_copy_id,
+ apr_off_t initial_offset,
+ apr_array_header_t *reps_to_cache,
+ apr_hash_t *reps_hash,
+ apr_pool_t *reps_pool,
+ svn_boolean_t at_root,
+ apr_pool_t *pool)
+{
+ node_revision_t *noderev;
+ apr_off_t my_offset;
+ const svn_fs_id_t *new_id;
+ svn_fs_fs__id_part_t node_id, copy_id, rev_item;
+ fs_fs_data_t *ffd = fs->fsap_data;
+ const svn_fs_fs__id_part_t *txn_id = svn_fs_fs__id_txn_id(id);
+ svn_stream_t *file_stream;
+ svn_checksum_ctx_t *fnv1a_checksum_ctx;
+ apr_pool_t *subpool;
+
+ *new_id_p = NULL;
+
+ /* Check to see if this is a transaction node. */
+ if (! svn_fs_fs__id_is_txn(id))
+ return SVN_NO_ERROR;
+
+ subpool = svn_pool_create(pool);
+ SVN_ERR(svn_fs_fs__get_node_revision(&noderev, fs, id, pool, subpool));
+
+ if (noderev->kind == svn_node_dir)
+ {
+ apr_array_header_t *entries;
+ int i;
+
+ /* This is a directory. Write out all the children first. */
+
+ SVN_ERR(svn_fs_fs__rep_contents_dir(&entries, fs, noderev, pool,
+ subpool));
+ for (i = 0; i < entries->nelts; ++i)
+ {
+ svn_fs_dirent_t *dirent
+ = APR_ARRAY_IDX(entries, i, svn_fs_dirent_t *);
+
+ svn_pool_clear(subpool);
+ SVN_ERR(write_final_rev(&new_id, file, rev, fs, dirent->id,
+ start_node_id, start_copy_id, initial_offset,
+ reps_to_cache, reps_hash, reps_pool, FALSE,
+ subpool));
+ if (new_id && (svn_fs_fs__id_rev(new_id) == rev))
+ dirent->id = svn_fs_fs__id_copy(new_id, pool);
+ }
+
+ if (noderev->data_rep && is_txn_rep(noderev->data_rep))
+ {
+ /* Write out the contents of this directory as a text rep. */
+ noderev->data_rep->revision = rev;
+ if (ffd->deltify_directories)
+ SVN_ERR(write_container_delta_rep(noderev->data_rep, file,
+ entries,
+ write_directory_to_stream,
+ fs, noderev, NULL, FALSE,
+ SVN_FS_FS__ITEM_TYPE_DIR_REP,
+ pool));
+ else
+ SVN_ERR(write_container_rep(noderev->data_rep, file, entries,
+ write_directory_to_stream, fs, NULL,
+ FALSE, SVN_FS_FS__ITEM_TYPE_DIR_REP,
+ pool));
+
+ reset_txn_in_rep(noderev->data_rep);
+ }
+ }
+ else
+ {
+ /* This is a file. We should make sure the data rep, if it
+ exists in a "this" state, gets rewritten to our new revision
+ num. */
+
+ if (noderev->data_rep && is_txn_rep(noderev->data_rep))
+ {
+ reset_txn_in_rep(noderev->data_rep);
+ noderev->data_rep->revision = rev;
+
+ if (!svn_fs_fs__use_log_addressing(fs))
+ {
+ /* See issue 3845. Some unknown mechanism caused the
+ protorev file to get truncated, so check for that
+ here. */
+ if (noderev->data_rep->item_index + noderev->data_rep->size
+ > initial_offset)
+ return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
+ _("Truncated protorev file detected"));
+ }
+ }
+ }
+
+ svn_pool_destroy(subpool);
+
+ /* Fix up the property reps. */
+ if (noderev->prop_rep && is_txn_rep(noderev->prop_rep))
+ {
+ apr_hash_t *proplist;
+ apr_uint32_t item_type = noderev->kind == svn_node_dir
+ ? SVN_FS_FS__ITEM_TYPE_DIR_PROPS
+ : SVN_FS_FS__ITEM_TYPE_FILE_PROPS;
+ SVN_ERR(svn_fs_fs__get_proplist(&proplist, fs, noderev, pool));
+
+ noderev->prop_rep->revision = rev;
+
+ if (ffd->deltify_properties)
+ SVN_ERR(write_container_delta_rep(noderev->prop_rep, file, proplist,
+ write_hash_to_stream, fs, noderev,
+ reps_hash, TRUE, item_type, pool));
+ else
+ SVN_ERR(write_container_rep(noderev->prop_rep, file, proplist,
+ write_hash_to_stream, fs, reps_hash,
+ TRUE, item_type, pool));
+
+ reset_txn_in_rep(noderev->prop_rep);
+ }
+
+ /* Convert our temporary ID into a permanent revision one. */
+ node_id = *svn_fs_fs__id_node_id(noderev->id);
+ get_final_id(&node_id, rev, start_node_id, ffd->format);
+ copy_id = *svn_fs_fs__id_copy_id(noderev->id);
+ get_final_id(&copy_id, rev, start_copy_id, ffd->format);
+
+ if (noderev->copyroot_rev == SVN_INVALID_REVNUM)
+ noderev->copyroot_rev = rev;
+
+ /* root nodes have a fixed ID in log addressing mode */
+ SVN_ERR(svn_fs_fs__get_file_offset(&my_offset, file, pool));
+ if (svn_fs_fs__use_log_addressing(fs) && at_root)
+ {
+ /* reference the root noderev from the log-to-phys index */
+ rev_item.number = SVN_FS_FS__ITEM_INDEX_ROOT_NODE;
+ SVN_ERR(store_l2p_index_entry(fs, txn_id, my_offset,
+ rev_item.number, pool));
+ }
+ else
+ SVN_ERR(allocate_item_index(&rev_item.number, fs, txn_id,
+ my_offset, pool));
+
+ rev_item.revision = rev;
+ new_id = svn_fs_fs__id_rev_create(&node_id, &copy_id, &rev_item, pool);
+
+ noderev->id = new_id;
+
+ if (ffd->rep_sharing_allowed)
+ {
+ /* Save the data representation's hash in the rep cache. */
+ if ( noderev->data_rep && noderev->kind == svn_node_file
+ && noderev->data_rep->revision == rev)
+ {
+ SVN_ERR_ASSERT(reps_to_cache && reps_pool);
+ APR_ARRAY_PUSH(reps_to_cache, representation_t *)
+ = svn_fs_fs__rep_copy(noderev->data_rep, reps_pool);
+ }
+
+ if (noderev->prop_rep && noderev->prop_rep->revision == rev)
+ {
+ /* Add new property reps to hash and on-disk cache. */
+ representation_t *copy
+ = svn_fs_fs__rep_copy(noderev->prop_rep, reps_pool);
+
+ SVN_ERR_ASSERT(reps_to_cache && reps_pool);
+ APR_ARRAY_PUSH(reps_to_cache, representation_t *) = copy;
+
+ apr_hash_set(reps_hash,
+ copy->sha1_digest,
+ APR_SHA1_DIGESTSIZE,
+ copy);
+ }
+ }
+
+ /* don't serialize SHA1 for dirs to disk (waste of space) */
+ if (noderev->data_rep && noderev->kind == svn_node_dir)
+ noderev->data_rep->has_sha1 = FALSE;
+
+ /* don't serialize SHA1 for props to disk (waste of space) */
+ if (noderev->prop_rep)
+ noderev->prop_rep->has_sha1 = FALSE;
+
+ /* Workaround issue #4031: is-fresh-txn-root in revision files. */
+ noderev->is_fresh_txn_root = FALSE;
+
+ /* Write out our new node-revision. */
+ if (at_root)
+ SVN_ERR(validate_root_noderev(fs, noderev, rev, pool));
+
+ file_stream = fnv1a_wrap_stream(&fnv1a_checksum_ctx,
+ svn_stream_from_aprfile2(file, TRUE, pool),
+ pool);
+ SVN_ERR(svn_fs_fs__write_noderev(file_stream, noderev, ffd->format,
+ svn_fs_fs__fs_supports_mergeinfo(fs),
+ pool));
+
+ /* reference the root noderev from the log-to-phys index */
+ if (svn_fs_fs__use_log_addressing(fs))
+ {
+ svn_fs_fs__p2l_entry_t entry;
+ rev_item.revision = SVN_INVALID_REVNUM;
+
+ entry.offset = my_offset;
+ SVN_ERR(svn_fs_fs__get_file_offset(&my_offset, file, pool));
+ entry.size = my_offset - entry.offset;
+ entry.type = SVN_FS_FS__ITEM_TYPE_NODEREV;
+ entry.item = rev_item;
+ SVN_ERR(fnv1a_checksum_finalize(&entry.fnv1_checksum,
+ fnv1a_checksum_ctx,
+ pool));
+
+ SVN_ERR(store_p2l_index_entry(fs, txn_id, &entry, pool));
+ }
+
+ /* Return our ID that references the revision file. */
+ *new_id_p = noderev->id;
+
+ return SVN_NO_ERROR;
+}
+
+/* Write the changed path info CHANGED_PATHS from transaction TXN_ID to the
+ permanent rev-file FILE in filesystem FS. *OFFSET_P is set the to offset
+ in the file of the beginning of this information. Perform temporary
+ allocations in POOL. */
+static svn_error_t *
+write_final_changed_path_info(apr_off_t *offset_p,
+ apr_file_t *file,
+ svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_hash_t *changed_paths,
+ apr_pool_t *pool)
+{
+ apr_off_t offset;
+ svn_stream_t *stream;
+ svn_checksum_ctx_t *fnv1a_checksum_ctx;
+
+ SVN_ERR(svn_fs_fs__get_file_offset(&offset, file, pool));
+
+ /* write to target file & calculate checksum */
+ stream = fnv1a_wrap_stream(&fnv1a_checksum_ctx,
+ svn_stream_from_aprfile2(file, TRUE, pool),
+ pool);
+ SVN_ERR(svn_fs_fs__write_changes(stream, fs, changed_paths, TRUE, pool));
+
+ *offset_p = offset;
+
+ /* reference changes from the indexes */
+ if (svn_fs_fs__use_log_addressing(fs))
+ {
+ svn_fs_fs__p2l_entry_t entry;
+
+ entry.offset = offset;
+ SVN_ERR(svn_fs_fs__get_file_offset(&offset, file, pool));
+ entry.size = offset - entry.offset;
+ entry.type = SVN_FS_FS__ITEM_TYPE_CHANGES;
+ entry.item.revision = SVN_INVALID_REVNUM;
+ entry.item.number = SVN_FS_FS__ITEM_INDEX_CHANGES;
+ SVN_ERR(fnv1a_checksum_finalize(&entry.fnv1_checksum,
+ fnv1a_checksum_ctx,
+ pool));
+
+ SVN_ERR(store_p2l_index_entry(fs, txn_id, &entry, pool));
+ SVN_ERR(store_l2p_index_entry(fs, txn_id, entry.offset,
+ SVN_FS_FS__ITEM_INDEX_CHANGES, pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Open a new svn_fs_t handle to FS, set that handle's concept of "current
+ youngest revision" to NEW_REV, and call svn_fs_fs__verify_root() on
+ NEW_REV's revision root.
+
+ Intended to be called as the very last step in a commit before 'current'
+ is bumped. This implies that we are holding the write lock. */
+static svn_error_t *
+verify_as_revision_before_current_plus_plus(svn_fs_t *fs,
+ svn_revnum_t new_rev,
+ apr_pool_t *pool)
+{
+#ifdef SVN_DEBUG
+ fs_fs_data_t *ffd = fs->fsap_data;
+ svn_fs_t *ft; /* fs++ == ft */
+ svn_fs_root_t *root;
+ fs_fs_data_t *ft_ffd;
+ apr_hash_t *fs_config;
+
+ SVN_ERR_ASSERT(ffd->svn_fs_open_);
+
+ /* make sure FT does not simply return data cached by other instances
+ * but actually retrieves it from disk at least once.
+ */
+ fs_config = apr_hash_make(pool);
+ svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_CACHE_NS,
+ svn_uuid_generate(pool));
+ SVN_ERR(ffd->svn_fs_open_(&ft, fs->path,
+ fs_config,
+ pool,
+ pool));
+ ft_ffd = ft->fsap_data;
+ /* Don't let FT consult rep-cache.db, either. */
+ ft_ffd->rep_sharing_allowed = FALSE;
+
+ /* Time travel! */
+ ft_ffd->youngest_rev_cache = new_rev;
+
+ SVN_ERR(svn_fs_fs__revision_root(&root, ft, new_rev, pool));
+ SVN_ERR_ASSERT(root->is_txn_root == FALSE && root->rev == new_rev);
+ SVN_ERR_ASSERT(ft_ffd->youngest_rev_cache == new_rev);
+ SVN_ERR(svn_fs_fs__verify_root(root, pool));
+#endif /* SVN_DEBUG */
+
+ return SVN_NO_ERROR;
+}
+
+/* Update the 'current' file to hold the correct next node and copy_ids
+ from transaction TXN_ID in filesystem FS. The current revision is
+ set to REV. Perform temporary allocations in POOL. */
+static svn_error_t *
+write_final_current(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ svn_revnum_t rev,
+ apr_uint64_t start_node_id,
+ apr_uint64_t start_copy_id,
+ apr_pool_t *pool)
+{
+ apr_uint64_t txn_node_id;
+ apr_uint64_t txn_copy_id;
+ fs_fs_data_t *ffd = fs->fsap_data;
+
+ if (ffd->format >= SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT)
+ return svn_fs_fs__write_current(fs, rev, 0, 0, pool);
+
+ /* To find the next available ids, we add the id that used to be in
+ the 'current' file, to the next ids from the transaction file. */
+ SVN_ERR(read_next_ids(&txn_node_id, &txn_copy_id, fs, txn_id, pool));
+
+ start_node_id += txn_node_id;
+ start_copy_id += txn_copy_id;
+
+ return svn_fs_fs__write_current(fs, rev, start_node_id, start_copy_id,
+ pool);
+}
+
+/* Verify that the user registered with FS has all the locks necessary to
+ permit all the changes associated with TXN_NAME.
+ The FS write lock is assumed to be held by the caller. */
+static svn_error_t *
+verify_locks(svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_hash_t *changed_paths,
+ apr_pool_t *pool)
+{
+ apr_pool_t *iterpool;
+ apr_array_header_t *changed_paths_sorted;
+ svn_stringbuf_t *last_recursed = NULL;
+ int i;
+
+ /* Make an array of the changed paths, and sort them depth-first-ily. */
+ changed_paths_sorted = svn_sort__hash(changed_paths,
+ svn_sort_compare_items_as_paths,
+ pool);
+
+ /* Now, traverse the array of changed paths, verify locks. Note
+ that if we need to do a recursive verification a path, we'll skip
+ over children of that path when we get to them. */
+ iterpool = svn_pool_create(pool);
+ for (i = 0; i < changed_paths_sorted->nelts; i++)
+ {
+ const svn_sort__item_t *item;
+ const char *path;
+ svn_fs_path_change2_t *change;
+ svn_boolean_t recurse = TRUE;
+
+ svn_pool_clear(iterpool);
+
+ item = &APR_ARRAY_IDX(changed_paths_sorted, i, svn_sort__item_t);
+
+ /* Fetch the change associated with our path. */
+ path = item->key;
+ change = item->value;
+
+ /* If this path has already been verified as part of a recursive
+ check of one of its parents, no need to do it again. */
+ if (last_recursed
+ && svn_fspath__skip_ancestor(last_recursed->data, path))
+ continue;
+
+ /* What does it mean to succeed at lock verification for a given
+ path? For an existing file or directory getting modified
+ (text, props), it means we hold the lock on the file or
+ directory. For paths being added or removed, we need to hold
+ the locks for that path and any children of that path.
+
+ WHEW! We have no reliable way to determine the node kind
+ of deleted items, but fortunately we are going to do a
+ recursive check on deleted paths regardless of their kind. */
+ if (change->change_kind == svn_fs_path_change_modify)
+ recurse = FALSE;
+ SVN_ERR(svn_fs_fs__allow_locked_operation(path, fs, recurse, TRUE,
+ iterpool));
+
+ /* If we just did a recursive check, remember the path we
+ checked (so children can be skipped). */
+ if (recurse)
+ {
+ if (! last_recursed)
+ last_recursed = svn_stringbuf_create(path, pool);
+ else
+ svn_stringbuf_set(last_recursed, path);
+ }
+ }
+ svn_pool_destroy(iterpool);
+ return SVN_NO_ERROR;
+}
+
+/* Return in *PATH the path to a file containing the properties that
+ make up the final revision properties file. This involves setting
+ svn:date and removing any temporary properties associated with the
+ commit flags. */
+static svn_error_t *
+write_final_revprop(const char **path,
+ svn_fs_txn_t *txn,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ apr_hash_t *txnprops;
+ svn_boolean_t final_mods = FALSE;
+ svn_string_t date;
+ svn_string_t *client_date;
+
+ SVN_ERR(svn_fs_fs__txn_proplist(&txnprops, txn, pool));
+
+ /* Remove any temporary txn props representing 'flags'. */
+ if (svn_hash_gets(txnprops, SVN_FS__PROP_TXN_CHECK_OOD))
+ {
+ svn_hash_sets(txnprops, SVN_FS__PROP_TXN_CHECK_OOD, NULL);
+ final_mods = TRUE;
+ }
+
+ if (svn_hash_gets(txnprops, SVN_FS__PROP_TXN_CHECK_LOCKS))
+ {
+ svn_hash_sets(txnprops, SVN_FS__PROP_TXN_CHECK_LOCKS, NULL);
+ final_mods = TRUE;
+ }
+
+ client_date = svn_hash_gets(txnprops, SVN_FS__PROP_TXN_CLIENT_DATE);
+ if (client_date)
+ {
+ svn_hash_sets(txnprops, SVN_FS__PROP_TXN_CLIENT_DATE, NULL);
+ final_mods = TRUE;
+ }
+
+ /* Update commit time to ensure that svn:date revprops remain ordered if
+ requested. */
+ if (!client_date || strcmp(client_date->data, "1"))
+ {
+ date.data = svn_time_to_cstring(apr_time_now(), pool);
+ date.len = strlen(date.data);
+ svn_hash_sets(txnprops, SVN_PROP_REVISION_DATE, &date);
+ final_mods = TRUE;
+ }
+
+ if (final_mods)
+ {
+ SVN_ERR(set_txn_proplist(txn->fs, txn_id, txnprops, TRUE, pool));
+ *path = path_txn_props_final(txn->fs, txn_id, pool);
+ }
+ else
+ {
+ *path = path_txn_props(txn->fs, txn_id, pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__add_index_data(svn_fs_t *fs,
+ apr_file_t *file,
+ const char *l2p_proto_index,
+ const char *p2l_proto_index,
+ svn_revnum_t revision,
+ apr_pool_t *pool)
+{
+ apr_off_t l2p_offset;
+ apr_off_t p2l_offset;
+ svn_stringbuf_t *footer;
+ unsigned char footer_length;
+ svn_checksum_t *l2p_checksum;
+ svn_checksum_t *p2l_checksum;
+
+ /* Append the actual index data to the pack file. */
+ l2p_offset = 0;
+ SVN_ERR(svn_io_file_seek(file, APR_END, &l2p_offset, pool));
+ SVN_ERR(svn_fs_fs__l2p_index_append(&l2p_checksum, fs, file,
+ l2p_proto_index, revision,
+ pool, pool));
+
+ p2l_offset = 0;
+ SVN_ERR(svn_io_file_seek(file, APR_END, &p2l_offset, pool));
+ SVN_ERR(svn_fs_fs__p2l_index_append(&p2l_checksum, fs, file,
+ p2l_proto_index, revision,
+ pool, pool));
+
+ /* Append footer. */
+ footer = svn_fs_fs__unparse_footer(l2p_offset, l2p_checksum,
+ p2l_offset, p2l_checksum, pool, pool);
+ SVN_ERR(svn_io_file_write_full(file, footer->data, footer->len, NULL,
+ pool));
+
+ footer_length = footer->len;
+ SVN_ERR_ASSERT(footer_length == footer->len);
+ SVN_ERR(svn_io_file_write_full(file, &footer_length, 1, NULL, pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Baton used for commit_body below. */
+struct commit_baton {
+ svn_revnum_t *new_rev_p;
+ svn_fs_t *fs;
+ svn_fs_txn_t *txn;
+ apr_array_header_t *reps_to_cache;
+ apr_hash_t *reps_hash;
+ apr_pool_t *reps_pool;
+};
+
+/* The work-horse for svn_fs_fs__commit, called with the FS write lock.
+ This implements the svn_fs_fs__with_write_lock() 'body' callback
+ type. BATON is a 'struct commit_baton *'. */
+static svn_error_t *
+commit_body(void *baton, apr_pool_t *pool)
+{
+ struct commit_baton *cb = baton;
+ fs_fs_data_t *ffd = cb->fs->fsap_data;
+ const char *old_rev_filename, *rev_filename, *proto_filename;
+ const char *revprop_filename, *final_revprop;
+ const svn_fs_id_t *root_id, *new_root_id;
+ apr_uint64_t start_node_id;
+ apr_uint64_t start_copy_id;
+ svn_revnum_t old_rev, new_rev;
+ apr_file_t *proto_file;
+ void *proto_file_lockcookie;
+ apr_off_t initial_offset, changed_path_offset;
+ const svn_fs_fs__id_part_t *txn_id = svn_fs_fs__txn_get_id(cb->txn);
+ apr_hash_t *changed_paths;
+
+ /* Re-Read the current repository format. All our repo upgrade and
+ config evaluation strategies are such that existing information in
+ FS and FFD remains valid.
+
+ Although we don't recommend upgrading hot repositories, people may
+ still do it and we must make sure to either handle them gracefully
+ or to error out.
+
+ Committing pre-format 3 txns will fail after upgrade to format 3+
+ because the proto-rev cannot be found; no further action needed.
+ Upgrades from pre-f7 to f7+ means a potential change in addressing
+ mode for the final rev. We must be sure to detect that cause because
+ the failure would only manifest once the new revision got committed.
+ */
+ SVN_ERR(svn_fs_fs__read_format_file(cb->fs, pool));
+
+ /* Read the current youngest revision and, possibly, the next available
+ node id and copy id (for old format filesystems). Update the cached
+ value for the youngest revision, because we have just checked it. */
+ SVN_ERR(svn_fs_fs__read_current(&old_rev, &start_node_id, &start_copy_id,
+ cb->fs, pool));
+ ffd->youngest_rev_cache = old_rev;
+
+ /* Check to make sure this transaction is based off the most recent
+ revision. */
+ if (cb->txn->base_rev != old_rev)
+ return svn_error_create(SVN_ERR_FS_TXN_OUT_OF_DATE, NULL,
+ _("Transaction out of date"));
+
+ /* We need the changes list for verification as well as for writing it
+ to the final rev file. */
+ SVN_ERR(svn_fs_fs__txn_changes_fetch(&changed_paths, cb->fs, txn_id,
+ pool));
+
+ /* Locks may have been added (or stolen) between the calling of
+ previous svn_fs.h functions and svn_fs_commit_txn(), so we need
+ to re-examine every changed-path in the txn and re-verify all
+ discovered locks. */
+ SVN_ERR(verify_locks(cb->fs, txn_id, changed_paths, pool));
+
+ /* We are going to be one better than this puny old revision. */
+ new_rev = old_rev + 1;
+
+ /* Get a write handle on the proto revision file. */
+ SVN_ERR(get_writable_proto_rev(&proto_file, &proto_file_lockcookie,
+ cb->fs, txn_id, pool));
+ SVN_ERR(svn_fs_fs__get_file_offset(&initial_offset, proto_file, pool));
+
+ /* Write out all the node-revisions and directory contents. */
+ root_id = svn_fs_fs__id_txn_create_root(txn_id, pool);
+ SVN_ERR(write_final_rev(&new_root_id, proto_file, new_rev, cb->fs, root_id,
+ start_node_id, start_copy_id, initial_offset,
+ cb->reps_to_cache, cb->reps_hash, cb->reps_pool,
+ TRUE, pool));
+
+ /* Write the changed-path information. */
+ SVN_ERR(write_final_changed_path_info(&changed_path_offset, proto_file,
+ cb->fs, txn_id, changed_paths,
+ pool));
+
+ if (svn_fs_fs__use_log_addressing(cb->fs))
+ {
+ /* Append the index data to the rev file. */
+ SVN_ERR(svn_fs_fs__add_index_data(cb->fs, proto_file,
+ svn_fs_fs__path_l2p_proto_index(cb->fs, txn_id, pool),
+ svn_fs_fs__path_p2l_proto_index(cb->fs, txn_id, pool),
+ new_rev, pool));
+ }
+ else
+ {
+ /* Write the final line. */
+
+ svn_stringbuf_t *trailer
+ = svn_fs_fs__unparse_revision_trailer
+ ((apr_off_t)svn_fs_fs__id_item(new_root_id),
+ changed_path_offset,
+ pool);
+ SVN_ERR(svn_io_file_write_full(proto_file, trailer->data, trailer->len,
+ NULL, pool));
+ }
+
+ SVN_ERR(svn_io_file_flush_to_disk(proto_file, pool));
+ SVN_ERR(svn_io_file_close(proto_file, pool));
+
+ /* We don't unlock the prototype revision file immediately to avoid a
+ race with another caller writing to the prototype revision file
+ before we commit it. */
+
+ /* Create the shard for the rev and revprop file, if we're sharding and
+ this is the first revision of a new shard. We don't care if this
+ fails because the shard already existed for some reason. */
+ if (ffd->max_files_per_dir && new_rev % ffd->max_files_per_dir == 0)
+ {
+ /* Create the revs shard. */
+ {
+ const char *new_dir
+ = svn_fs_fs__path_rev_shard(cb->fs, new_rev, pool);
+ svn_error_t *err
+ = svn_io_dir_make(new_dir, APR_OS_DEFAULT, pool);
+ if (err && !APR_STATUS_IS_EEXIST(err->apr_err))
+ return svn_error_trace(err);
+ svn_error_clear(err);
+ SVN_ERR(svn_io_copy_perms(svn_dirent_join(cb->fs->path,
+ PATH_REVS_DIR,
+ pool),
+ new_dir, pool));
+ }
+
+ /* Create the revprops shard. */
+ SVN_ERR_ASSERT(! svn_fs_fs__is_packed_revprop(cb->fs, new_rev));
+ {
+ const char *new_dir
+ = svn_fs_fs__path_revprops_shard(cb->fs, new_rev, pool);
+ svn_error_t *err
+ = svn_io_dir_make(new_dir, APR_OS_DEFAULT, pool);
+ if (err && !APR_STATUS_IS_EEXIST(err->apr_err))
+ return svn_error_trace(err);
+ svn_error_clear(err);
+ SVN_ERR(svn_io_copy_perms(svn_dirent_join(cb->fs->path,
+ PATH_REVPROPS_DIR,
+ pool),
+ new_dir, pool));
+ }
+ }
+
+ /* Move the finished rev file into place.
+
+ ### This "breaks" the transaction by removing the protorev file
+ ### but the revision is not yet complete. If this commit does
+ ### not complete for any reason the transaction will be lost. */
+ old_rev_filename = svn_fs_fs__path_rev_absolute(cb->fs, old_rev, pool);
+ rev_filename = svn_fs_fs__path_rev(cb->fs, new_rev, pool);
+ proto_filename = svn_fs_fs__path_txn_proto_rev(cb->fs, txn_id, pool);
+ SVN_ERR(svn_fs_fs__move_into_place(proto_filename, rev_filename,
+ old_rev_filename, pool));
+
+ /* Now that we've moved the prototype revision file out of the way,
+ we can unlock it (since further attempts to write to the file
+ will fail as it no longer exists). We must do this so that we can
+ remove the transaction directory later. */
+ SVN_ERR(unlock_proto_rev(cb->fs, txn_id, proto_file_lockcookie, pool));
+
+ /* Move the revprops file into place. */
+ SVN_ERR_ASSERT(! svn_fs_fs__is_packed_revprop(cb->fs, new_rev));
+ SVN_ERR(write_final_revprop(&revprop_filename, cb->txn, txn_id, pool));
+ final_revprop = svn_fs_fs__path_revprops(cb->fs, new_rev, pool);
+ SVN_ERR(svn_fs_fs__move_into_place(revprop_filename, final_revprop,
+ old_rev_filename, pool));
+
+ /* Update the 'current' file. */
+ SVN_ERR(verify_as_revision_before_current_plus_plus(cb->fs, new_rev, pool));
+ SVN_ERR(write_final_current(cb->fs, txn_id, new_rev, start_node_id,
+ start_copy_id, pool));
+
+ /* At this point the new revision is committed and globally visible
+ so let the caller know it succeeded by giving it the new revision
+ number, which fulfills svn_fs_commit_txn() contract. Any errors
+ after this point do not change the fact that a new revision was
+ created. */
+ *cb->new_rev_p = new_rev;
+
+ ffd->youngest_rev_cache = new_rev;
+
+ /* Remove this transaction directory. */
+ SVN_ERR(svn_fs_fs__purge_txn(cb->fs, cb->txn->id, pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Add the representations in REPS_TO_CACHE (an array of representation_t *)
+ * to the rep-cache database of FS. */
+static svn_error_t *
+write_reps_to_cache(svn_fs_t *fs,
+ const apr_array_header_t *reps_to_cache,
+ apr_pool_t *scratch_pool)
+{
+ int i;
+
+ for (i = 0; i < reps_to_cache->nelts; i++)
+ {
+ representation_t *rep = APR_ARRAY_IDX(reps_to_cache, i, representation_t *);
+
+ SVN_ERR(svn_fs_fs__set_rep_reference(fs, rep, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__commit(svn_revnum_t *new_rev_p,
+ svn_fs_t *fs,
+ svn_fs_txn_t *txn,
+ apr_pool_t *pool)
+{
+ struct commit_baton cb;
+ fs_fs_data_t *ffd = fs->fsap_data;
+
+ cb.new_rev_p = new_rev_p;
+ cb.fs = fs;
+ cb.txn = txn;
+
+ if (ffd->rep_sharing_allowed)
+ {
+ cb.reps_to_cache = apr_array_make(pool, 5, sizeof(representation_t *));
+ cb.reps_hash = apr_hash_make(pool);
+ cb.reps_pool = pool;
+ }
+ else
+ {
+ cb.reps_to_cache = NULL;
+ cb.reps_hash = NULL;
+ cb.reps_pool = NULL;
+ }
+
+ SVN_ERR(svn_fs_fs__with_write_lock(fs, commit_body, &cb, pool));
+
+ /* At this point, *NEW_REV_P has been set, so errors below won't affect
+ the success of the commit. (See svn_fs_commit_txn().) */
+
+ if (ffd->rep_sharing_allowed)
+ {
+ svn_error_t *err;
+
+ SVN_ERR(svn_fs_fs__open_rep_cache(fs, pool));
+
+ /* Write new entries to the rep-sharing database.
+ *
+ * We use an sqlite transaction to speed things up;
+ * see <http://www.sqlite.org/faq.html#q19>.
+ */
+ /* ### A commit that touches thousands of files will starve other
+ (reader/writer) commits for the duration of the below call.
+ Maybe write in batches? */
+ SVN_ERR(svn_sqlite__begin_transaction(ffd->rep_cache_db));
+ err = write_reps_to_cache(fs, cb.reps_to_cache, pool);
+ err = svn_sqlite__finish_transaction(ffd->rep_cache_db, err);
+
+ if (svn_error_find_cause(err, SVN_SQLITE__ERR_ROLLBACK_FAILED))
+ {
+ /* Failed rollback means that our db connection is unusable, and
+ the only thing we can do is close it. The connection will be
+ reopened during the next operation with rep-cache.db. */
+ return svn_error_trace(
+ svn_error_compose_create(err,
+ svn_fs_fs__close_rep_cache(fs)));
+ }
+ else if (err)
+ return svn_error_trace(err);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_fs_fs__list_transactions(apr_array_header_t **names_p,
+ svn_fs_t *fs,
+ apr_pool_t *pool)
+{
+ const char *txn_dir;
+ apr_hash_t *dirents;
+ apr_hash_index_t *hi;
+ apr_array_header_t *names;
+ apr_size_t ext_len = strlen(PATH_EXT_TXN);
+
+ names = apr_array_make(pool, 1, sizeof(const char *));
+
+ /* Get the transactions directory. */
+ txn_dir = svn_fs_fs__path_txns_dir(fs, pool);
+
+ /* Now find a listing of this directory. */
+ SVN_ERR(svn_io_get_dirents3(&dirents, txn_dir, TRUE, pool, pool));
+
+ /* Loop through all the entries and return anything that ends with '.txn'. */
+ for (hi = apr_hash_first(pool, dirents); hi; hi = apr_hash_next(hi))
+ {
+ const char *name = apr_hash_this_key(hi);
+ apr_ssize_t klen = apr_hash_this_key_len(hi);
+ const char *id;
+
+ /* The name must end with ".txn" to be considered a transaction. */
+ if ((apr_size_t) klen <= ext_len
+ || (strcmp(name + klen - ext_len, PATH_EXT_TXN)) != 0)
+ continue;
+
+ /* Truncate the ".txn" extension and store the ID. */
+ id = apr_pstrndup(pool, name, strlen(name) - ext_len);
+ APR_ARRAY_PUSH(names, const char *) = id;
+ }
+
+ *names_p = names;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__open_txn(svn_fs_txn_t **txn_p,
+ svn_fs_t *fs,
+ const char *name,
+ apr_pool_t *pool)
+{
+ svn_fs_txn_t *txn;
+ fs_txn_data_t *ftd;
+ svn_node_kind_t kind;
+ transaction_t *local_txn;
+ svn_fs_fs__id_part_t txn_id;
+
+ SVN_ERR(svn_fs_fs__id_txn_parse(&txn_id, name));
+
+ /* First check to see if the directory exists. */
+ SVN_ERR(svn_io_check_path(svn_fs_fs__path_txn_dir(fs, &txn_id, pool),
+ &kind, pool));
+
+ /* Did we find it? */
+ if (kind != svn_node_dir)
+ return svn_error_createf(SVN_ERR_FS_NO_SUCH_TRANSACTION, NULL,
+ _("No such transaction '%s'"),
+ name);
+
+ txn = apr_pcalloc(pool, sizeof(*txn));
+ ftd = apr_pcalloc(pool, sizeof(*ftd));
+ ftd->txn_id = txn_id;
+
+ /* Read in the root node of this transaction. */
+ txn->id = apr_pstrdup(pool, name);
+ txn->fs = fs;
+
+ SVN_ERR(svn_fs_fs__get_txn(&local_txn, fs, &txn_id, pool));
+
+ txn->base_rev = svn_fs_fs__id_rev(local_txn->base_id);
+
+ txn->vtable = &txn_vtable;
+ txn->fsap_data = ftd;
+ *txn_p = txn;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__txn_proplist(apr_hash_t **table_p,
+ svn_fs_txn_t *txn,
+ apr_pool_t *pool)
+{
+ apr_hash_t *proplist = apr_hash_make(pool);
+ SVN_ERR(get_txn_proplist(proplist, txn->fs, svn_fs_fs__txn_get_id(txn),
+ pool));
+ *table_p = proplist;
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_fs_fs__delete_node_revision(svn_fs_t *fs,
+ const svn_fs_id_t *id,
+ apr_pool_t *pool)
+{
+ node_revision_t *noderev;
+
+ SVN_ERR(svn_fs_fs__get_node_revision(&noderev, fs, id, pool, pool));
+
+ /* Delete any mutable property representation. */
+ if (noderev->prop_rep && is_txn_rep(noderev->prop_rep))
+ SVN_ERR(svn_io_remove_file2(svn_fs_fs__path_txn_node_props(fs, id, pool),
+ FALSE, pool));
+
+ /* Delete any mutable data representation. */
+ if (noderev->data_rep && is_txn_rep(noderev->data_rep)
+ && noderev->kind == svn_node_dir)
+ {
+ fs_fs_data_t *ffd = fs->fsap_data;
+ SVN_ERR(svn_io_remove_file2(svn_fs_fs__path_txn_node_children(fs, id,
+ pool),
+ FALSE, pool));
+
+ /* remove the corresponding entry from the cache, if such exists */
+ if (ffd->txn_dir_cache)
+ {
+ const char *key = svn_fs_fs__id_unparse(id, pool)->data;
+ SVN_ERR(svn_cache__set(ffd->txn_dir_cache, key, NULL, pool));
+ }
+ }
+
+ return svn_io_remove_file2(svn_fs_fs__path_txn_node_rev(fs, id, pool),
+ FALSE, pool);
+}
+
+
+
+/*** Transactions ***/
+
+svn_error_t *
+svn_fs_fs__get_txn_ids(const svn_fs_id_t **root_id_p,
+ const svn_fs_id_t **base_root_id_p,
+ svn_fs_t *fs,
+ const svn_fs_fs__id_part_t *txn_id,
+ apr_pool_t *pool)
+{
+ transaction_t *txn;
+ SVN_ERR(svn_fs_fs__get_txn(&txn, fs, txn_id, pool));
+ *root_id_p = txn->root_id;
+ *base_root_id_p = txn->base_id;
+ return SVN_NO_ERROR;
+}
+
+
+/* Generic transaction operations. */
+
+svn_error_t *
+svn_fs_fs__txn_prop(svn_string_t **value_p,
+ svn_fs_txn_t *txn,
+ const char *propname,
+ apr_pool_t *pool)
+{
+ apr_hash_t *table;
+ svn_fs_t *fs = txn->fs;
+
+ SVN_ERR(svn_fs__check_fs(fs, TRUE));
+ SVN_ERR(svn_fs_fs__txn_proplist(&table, txn, pool));
+
+ *value_p = svn_hash_gets(table, propname);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_fs__begin_txn(svn_fs_txn_t **txn_p,
+ svn_fs_t *fs,
+ svn_revnum_t rev,
+ apr_uint32_t flags,
+ apr_pool_t *pool)
+{
+ svn_string_t date;
+ fs_txn_data_t *ftd;
+ apr_hash_t *props = apr_hash_make(pool);
+
+ SVN_ERR(svn_fs__check_fs(fs, TRUE));
+
+ SVN_ERR(svn_fs_fs__create_txn(txn_p, fs, rev, pool));
+
+ /* Put a datestamp on the newly created txn, so we always know
+ exactly how old it is. (This will help sysadmins identify
+ long-abandoned txns that may need to be manually removed.) When
+ a txn is promoted to a revision, this property will be
+ automatically overwritten with a revision datestamp. */
+ date.data = svn_time_to_cstring(apr_time_now(), pool);
+ date.len = strlen(date.data);
+
+ svn_hash_sets(props, SVN_PROP_REVISION_DATE, &date);
+
+ /* Set temporary txn props that represent the requested 'flags'
+ behaviors. */
+ if (flags & SVN_FS_TXN_CHECK_OOD)
+ svn_hash_sets(props, SVN_FS__PROP_TXN_CHECK_OOD,
+ svn_string_create("true", pool));
+
+ if (flags & SVN_FS_TXN_CHECK_LOCKS)
+ svn_hash_sets(props, SVN_FS__PROP_TXN_CHECK_LOCKS,
+ svn_string_create("true", pool));
+
+ if (flags & SVN_FS_TXN_CLIENT_DATE)
+ svn_hash_sets(props, SVN_FS__PROP_TXN_CLIENT_DATE,
+ svn_string_create("0", pool));
+
+ ftd = (*txn_p)->fsap_data;
+ return svn_error_trace(set_txn_proplist(fs, &ftd->txn_id, props, FALSE,
+ pool));
+}