summaryrefslogtreecommitdiff
path: root/subversion/libsvn_fs_x/transaction.c
diff options
context:
space:
mode:
Diffstat (limited to 'subversion/libsvn_fs_x/transaction.c')
-rw-r--r--subversion/libsvn_fs_x/transaction.c3782
1 files changed, 3782 insertions, 0 deletions
diff --git a/subversion/libsvn_fs_x/transaction.c b/subversion/libsvn_fs_x/transaction.c
new file mode 100644
index 0000000..5f3adc5
--- /dev/null
+++ b/subversion/libsvn_fs_x/transaction.c
@@ -0,0 +1,3782 @@
+/* transaction.c --- transaction-related functions of FSX
+ *
+ * ====================================================================
+ * 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_x.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 "index.h"
+
+#include "private/svn_fs_util.h"
+#include "private/svn_fspath.h"
+#include "private/svn_sorts_private.h"
+#include "private/svn_string_private.h"
+#include "private/svn_subr_private.h"
+#include "private/svn_io_private.h"
+#include "../libsvn_fs/fs-loader.h"
+
+#include "svn_private_config.h"
+
+/* The vtable associated with an open transaction object. */
+static txn_vtable_t txn_vtable = {
+ svn_fs_x__commit_txn,
+ svn_fs_x__abort_txn,
+ svn_fs_x__txn_prop,
+ svn_fs_x__txn_proplist,
+ svn_fs_x__change_txn_prop,
+ svn_fs_x__txn_root,
+ svn_fs_x__change_txn_props
+};
+
+/* FSX-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_x__txn_id_t txn_id;
+} fs_txn_data_t;
+
+svn_fs_x__txn_id_t
+svn_fs_x__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 svn_fs_x__shared_txn_data_t *
+get_shared_txn(svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ svn_boolean_t create_new)
+{
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+ svn_fs_x__shared_data_t *ffsd = ffd->shared;
+ svn_fs_x__shared_txn_data_t *txn;
+
+ for (txn = ffsd->txns; txn; txn = txn->next)
+ if (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, svn_fs_x__txn_id_t txn_id)
+{
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+ svn_fs_x__shared_data_t *ffsd = ffd->shared;
+ svn_fs_x__shared_txn_data_t *txn, *prev = NULL;
+
+ for (txn = ffsd->txns; txn; prev = txn, txn = txn->next)
+ if (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)
+{
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+ svn_fs_x__shared_data_t *ffsd = ffd->shared;
+
+ SVN_MUTEX__WITH_LOCK(ffsd->txn_list_lock,
+ body(fs, baton, pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Get a lock on empty file LOCK_FILENAME, creating it in RESULT_POOL. */
+static svn_error_t *
+get_lock_on_filesystem(const char *lock_filename,
+ apr_pool_t *result_pool)
+{
+ return svn_error_trace(svn_io__file_lock_autocreate(lock_filename,
+ result_pool));
+}
+
+/* Reset the HAS_WRITE_LOCK member in the FFD given as BATON_VOID.
+ When registered with the pool holding the lock on the lock file,
+ this makes sure the flag gets reset just before we release the lock. */
+static apr_status_t
+reset_lock_flag(void *baton_void)
+{
+ svn_fs_x__data_t *ffd = baton_void;
+ ffd->has_write_lock = FALSE;
+ return APR_SUCCESS;
+}
+
+/* Structure defining a file system lock to be acquired and the function
+ to be executed while the lock is held.
+
+ Instances of this structure may be nested to allow for multiple locks to
+ be taken out before executing the user-provided body. In that case, BODY
+ and BATON of the outer instances will be with_lock and a with_lock_baton_t
+ instance (transparently, no special treatment is required.). It is
+ illegal to attempt to acquire the same lock twice within the same lock
+ chain or via nesting calls using separate lock chains.
+
+ All instances along the chain share the same LOCK_POOL such that only one
+ pool needs to be created and cleared for all locks. We also allocate as
+ much data from that lock pool as possible to minimize memory usage in
+ caller pools. */
+typedef struct with_lock_baton_t
+{
+ /* The filesystem we operate on. Same for all instances along the chain. */
+ svn_fs_t *fs;
+
+ /* Mutex to complement the lock file in an APR threaded process.
+ No-op object for non-threaded processes but never NULL. */
+ svn_mutex__t *mutex;
+
+ /* Path to the file to lock. */
+ const char *lock_path;
+
+ /* If true, set FS->HAS_WRITE_LOCK after we acquired the lock. */
+ svn_boolean_t is_global_lock;
+
+ /* Function body to execute after we acquired the lock.
+ This may be user-provided or a nested call to with_lock(). */
+ svn_error_t *(*body)(void *baton,
+ apr_pool_t *scratch_pool);
+
+ /* Baton to pass to BODY; possibly NULL.
+ This may be user-provided or a nested lock baton instance. */
+ void *baton;
+
+ /* Pool for all allocations along the lock chain and BODY. Will hold the
+ file locks and gets destroyed after the outermost BODY returned,
+ releasing all file locks.
+ Same for all instances along the chain. */
+ apr_pool_t *lock_pool;
+
+ /* TRUE, iff BODY is the user-provided body. */
+ svn_boolean_t is_inner_most_lock;
+
+ /* TRUE, iff this is not a nested lock.
+ Then responsible for destroying LOCK_POOL. */
+ svn_boolean_t is_outer_most_lock;
+} with_lock_baton_t;
+
+/* Obtain a write lock on the file BATON->LOCK_PATH and call BATON->BODY
+ with BATON->BATON. If this is the outermost lock call, release all file
+ locks after the body returned. If BATON->IS_GLOBAL_LOCK is set, set the
+ HAS_WRITE_LOCK flag while we keep the write lock. */
+static svn_error_t *
+with_some_lock_file(with_lock_baton_t *baton)
+{
+ apr_pool_t *pool = baton->lock_pool;
+ svn_error_t *err = get_lock_on_filesystem(baton->lock_path, pool);
+
+ if (!err)
+ {
+ svn_fs_t *fs = baton->fs;
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+
+ if (baton->is_global_lock)
+ {
+ /* set the "got the lock" flag and register reset function */
+ apr_pool_cleanup_register(pool,
+ ffd,
+ reset_lock_flag,
+ apr_pool_cleanup_null);
+ ffd->has_write_lock = TRUE;
+ }
+
+ /* nobody else will modify the repo state
+ => read HEAD & pack info once */
+ if (baton->is_inner_most_lock)
+ {
+ err = svn_fs_x__update_min_unpacked_rev(fs, pool);
+ if (!err)
+ err = svn_fs_x__youngest_rev(&ffd->youngest_rev_cache, fs, pool);
+ }
+
+ if (!err)
+ err = baton->body(baton->baton, pool);
+ }
+
+ if (baton->is_outer_most_lock)
+ svn_pool_destroy(pool);
+
+ return svn_error_trace(err);
+}
+
+/* Wraps with_some_lock_file, protecting it with BATON->MUTEX.
+
+ SCRATCH_POOL is unused here and only provided for signature compatibility
+ with WITH_LOCK_BATON_T.BODY. */
+static svn_error_t *
+with_lock(void *baton,
+ apr_pool_t *scratch_pool)
+{
+ with_lock_baton_t *lock_baton = baton;
+ SVN_MUTEX__WITH_LOCK(lock_baton->mutex, with_some_lock_file(lock_baton));
+
+ return SVN_NO_ERROR;
+}
+
+/* Enum identifying a filesystem lock. */
+typedef enum lock_id_t
+{
+ write_lock,
+ txn_lock,
+ pack_lock
+} lock_id_t;
+
+/* Initialize BATON->MUTEX, BATON->LOCK_PATH and BATON->IS_GLOBAL_LOCK
+ according to the LOCK_ID. All other members of BATON must already be
+ valid. */
+static void
+init_lock_baton(with_lock_baton_t *baton,
+ lock_id_t lock_id)
+{
+ svn_fs_x__data_t *ffd = baton->fs->fsap_data;
+ svn_fs_x__shared_data_t *ffsd = ffd->shared;
+
+ switch (lock_id)
+ {
+ case write_lock:
+ baton->mutex = ffsd->fs_write_lock;
+ baton->lock_path = svn_fs_x__path_lock(baton->fs, baton->lock_pool);
+ baton->is_global_lock = TRUE;
+ break;
+
+ case txn_lock:
+ baton->mutex = ffsd->txn_current_lock;
+ baton->lock_path = svn_fs_x__path_txn_current_lock(baton->fs,
+ baton->lock_pool);
+ baton->is_global_lock = FALSE;
+ break;
+
+ case pack_lock:
+ baton->mutex = ffsd->fs_pack_lock;
+ baton->lock_path = svn_fs_x__path_pack_lock(baton->fs,
+ baton->lock_pool);
+ baton->is_global_lock = FALSE;
+ break;
+ }
+}
+
+/* Return the baton for the innermost lock of a (potential) lock chain.
+ The baton shall take out LOCK_ID from FS and execute BODY with BATON
+ while the lock is being held. Allocate the result in a sub-pool of
+ RESULT_POOL.
+ */
+static with_lock_baton_t *
+create_lock_baton(svn_fs_t *fs,
+ lock_id_t lock_id,
+ svn_error_t *(*body)(void *baton,
+ apr_pool_t *scratch_pool),
+ void *baton,
+ apr_pool_t *result_pool)
+{
+ /* Allocate everything along the lock chain into a single sub-pool.
+ This minimizes memory usage and cleanup overhead. */
+ apr_pool_t *lock_pool = svn_pool_create(result_pool);
+ with_lock_baton_t *result = apr_pcalloc(lock_pool, sizeof(*result));
+
+ /* Store parameters. */
+ result->fs = fs;
+ result->body = body;
+ result->baton = baton;
+
+ /* File locks etc. will use this pool as well for easy cleanup. */
+ result->lock_pool = lock_pool;
+
+ /* Right now, we are the first, (only, ) and last struct in the chain. */
+ result->is_inner_most_lock = TRUE;
+ result->is_outer_most_lock = TRUE;
+
+ /* Select mutex and lock file path depending on LOCK_ID.
+ Also, initialize dependent members (IS_GLOBAL_LOCK only, ATM). */
+ init_lock_baton(result, lock_id);
+
+ return result;
+}
+
+/* Return a baton that wraps NESTED and requests LOCK_ID as additional lock.
+ *
+ * That means, when you create a lock chain, start with the last / innermost
+ * lock to take out and add the first / outermost lock last.
+ */
+static with_lock_baton_t *
+chain_lock_baton(lock_id_t lock_id,
+ with_lock_baton_t *nested)
+{
+ /* Use the same pool for batons along the lock chain. */
+ apr_pool_t *lock_pool = nested->lock_pool;
+ with_lock_baton_t *result = apr_pcalloc(lock_pool, sizeof(*result));
+
+ /* All locks along the chain operate on the same FS. */
+ result->fs = nested->fs;
+
+ /* Execution of this baton means acquiring the nested lock and its
+ execution. */
+ result->body = with_lock;
+ result->baton = nested;
+
+ /* Shared among all locks along the chain. */
+ result->lock_pool = lock_pool;
+
+ /* We are the new outermost lock but surely not the innermost lock. */
+ result->is_inner_most_lock = FALSE;
+ result->is_outer_most_lock = TRUE;
+ nested->is_outer_most_lock = FALSE;
+
+ /* Select mutex and lock file path depending on LOCK_ID.
+ Also, initialize dependent members (IS_GLOBAL_LOCK only, ATM). */
+ init_lock_baton(result, lock_id);
+
+ return result;
+}
+
+svn_error_t *
+svn_fs_x__with_write_lock(svn_fs_t *fs,
+ svn_error_t *(*body)(void *baton,
+ apr_pool_t *scratch_pool),
+ void *baton,
+ apr_pool_t *scratch_pool)
+{
+ return svn_error_trace(
+ with_lock(create_lock_baton(fs, write_lock, body, baton,
+ scratch_pool),
+ scratch_pool));
+}
+
+svn_error_t *
+svn_fs_x__with_pack_lock(svn_fs_t *fs,
+ svn_error_t *(*body)(void *baton,
+ apr_pool_t *scratch_pool),
+ void *baton,
+ apr_pool_t *scratch_pool)
+{
+ return svn_error_trace(
+ with_lock(create_lock_baton(fs, pack_lock, body, baton,
+ scratch_pool),
+ scratch_pool));
+}
+
+svn_error_t *
+svn_fs_x__with_txn_current_lock(svn_fs_t *fs,
+ svn_error_t *(*body)(void *baton,
+ apr_pool_t *scratch_pool),
+ void *baton,
+ apr_pool_t *scratch_pool)
+{
+ return svn_error_trace(
+ with_lock(create_lock_baton(fs, txn_lock, body, baton,
+ scratch_pool),
+ scratch_pool));
+}
+
+svn_error_t *
+svn_fs_x__with_all_locks(svn_fs_t *fs,
+ svn_error_t *(*body)(void *baton,
+ apr_pool_t *scratch_pool),
+ void *baton,
+ apr_pool_t *scratch_pool)
+{
+ /* Be sure to use the correct lock ordering as documented in
+ fs_fs_shared_data_t. The lock chain is being created in
+ innermost (last to acquire) -> outermost (first to acquire) order. */
+ with_lock_baton_t *lock_baton
+ = create_lock_baton(fs, write_lock, body, baton, scratch_pool);
+
+ lock_baton = chain_lock_baton(pack_lock, lock_baton);
+ lock_baton = chain_lock_baton(txn_lock, lock_baton);
+
+ return svn_error_trace(with_lock(lock_baton, scratch_pool));
+}
+
+
+/* A structure used by unlock_proto_rev() and unlock_proto_rev_body(),
+ which see. */
+typedef struct unlock_proto_rev_baton_t
+{
+ svn_fs_x__txn_id_t txn_id;
+ void *lockcookie;
+} unlock_proto_rev_baton_t;
+
+/* 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 *scratch_pool)
+{
+ const unlock_proto_rev_baton_t *b = baton;
+ apr_file_t *lockfile = b->lockcookie;
+ svn_fs_x__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_x__txn_name(b->txn_id, scratch_pool));
+ if (!txn->being_written)
+ return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
+ _("Can't unlock nonlocked transaction '%s'"),
+ svn_fs_x__txn_name(b->txn_id, scratch_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_x__txn_name(b->txn_id, scratch_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_x__txn_name(b->txn_id, scratch_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 SCRATCH_POOL. */
+static svn_error_t *
+unlock_proto_rev(svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ void *lockcookie,
+ apr_pool_t *scratch_pool)
+{
+ unlock_proto_rev_baton_t b;
+
+ b.txn_id = txn_id;
+ b.lockcookie = lockcookie;
+ return with_txnlist_lock(fs, unlock_proto_rev_body, &b, scratch_pool);
+}
+
+/* A structure used by get_writable_proto_rev() and
+ get_writable_proto_rev_body(), which see. */
+typedef struct get_writable_proto_rev_baton_t
+{
+ void **lockcookie;
+ svn_fs_x__txn_id_t txn_id;
+} get_writable_proto_rev_baton_t;
+
+/* 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 *scratch_pool)
+{
+ const get_writable_proto_rev_baton_t *b = baton;
+ void **lockcookie = b->lockcookie;
+ svn_fs_x__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_x__txn_name(b->txn_id, scratch_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_x__path_txn_proto_rev_lock(fs, b->txn_id, scratch_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,
+ scratch_pool));
+
+ apr_err = apr_file_lock(lockfile,
+ APR_FLOCK_EXCLUSIVE | APR_FLOCK_NONBLOCK);
+ if (apr_err)
+ {
+ svn_error_clear(svn_io_file_close(lockfile, scratch_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_x__txn_name(b->txn_id,
+ scratch_pool));
+
+ return svn_error_wrap_apr(apr_err,
+ _("Can't get exclusive lock on file '%s'"),
+ svn_dirent_local_style(lockfile_path,
+ scratch_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 SCRATCH_POOL. */
+static svn_error_t *
+auto_truncate_proto_rev(svn_fs_t *fs,
+ apr_file_t *proto_rev,
+ apr_off_t actual_length,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *scratch_pool)
+{
+ /* 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_x__path_p2l_proto_index(fs, txn_id, scratch_pool);
+ apr_file_t *file;
+ apr_off_t indexed_length;
+
+ SVN_ERR(svn_fs_x__p2l_proto_index_open(&file, path, scratch_pool));
+ SVN_ERR(svn_fs_x__p2l_proto_index_next_offset(&indexed_length, file,
+ scratch_pool));
+ SVN_ERR(svn_io_file_close(file, scratch_pool));
+
+ /* Handle mismatches. */
+ if (indexed_length < actual_length)
+ SVN_ERR(svn_io_file_trunc(proto_rev, indexed_length, scratch_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(scratch_pool, indexed_length),
+ apr_off_t_toa(scratch_pool, actual_length),
+ svn_fs_x__txn_name(txn_id, scratch_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,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *pool)
+{
+ get_writable_proto_rev_baton_t 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_x__path_txn_proto_rev(fs, txn_id, pool),
+ 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 *scratch_pool)
+{
+ svn_fs_x__txn_id_t txn_id = *(const svn_fs_x__txn_id_t *)baton;
+
+ free_shared_txn(fs, txn_id);
+
+ return SVN_NO_ERROR;
+}
+
+/* Purge the shared data for transaction TXN_ID in filesystem FS.
+ Perform all temporary allocations in SCRATCH_POOL. */
+static svn_error_t *
+purge_shared_txn(svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *scratch_pool)
+{
+ return with_txnlist_lock(fs, purge_shared_txn_body, &txn_id, scratch_pool);
+}
+
+
+svn_boolean_t
+svn_fs_x__is_fresh_txn_root(svn_fs_x__noderev_t *noderev)
+{
+ /* Is it a root node? */
+ if (noderev->noderev_id.number != SVN_FS_X__ITEM_INDEX_ROOT_NODE)
+ return FALSE;
+
+ /* ... in a transaction? */
+ if (!svn_fs_x__is_txn(noderev->noderev_id.change_set))
+ return FALSE;
+
+ /* ... with no prop change in that txn?
+ (Once we set a property, the prop rep will never become NULL again.) */
+ if (noderev->prop_rep && svn_fs_x__is_txn(noderev->prop_rep->id.change_set))
+ return FALSE;
+
+ /* ... and no sub-tree change?
+ (Once we set a text, the data rep will never become NULL again.) */
+ if (noderev->data_rep && svn_fs_x__is_txn(noderev->data_rep->id.change_set))
+ return FALSE;
+
+ /* Root node of a txn with no changes. */
+ return TRUE;
+}
+
+svn_error_t *
+svn_fs_x__put_node_revision(svn_fs_t *fs,
+ svn_fs_x__noderev_t *noderev,
+ apr_pool_t *scratch_pool)
+{
+ apr_file_t *noderev_file;
+ const svn_fs_x__id_t *id = &noderev->noderev_id;
+
+ if (! svn_fs_x__is_txn(id->change_set))
+ return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
+ _("Attempted to write to non-transaction '%s'"),
+ svn_fs_x__id_unparse(id, scratch_pool)->data);
+
+ SVN_ERR(svn_io_file_open(&noderev_file,
+ svn_fs_x__path_txn_node_rev(fs, id, scratch_pool,
+ scratch_pool),
+ APR_WRITE | APR_CREATE | APR_TRUNCATE
+ | APR_BUFFERED, APR_OS_DEFAULT, scratch_pool));
+
+ SVN_ERR(svn_fs_x__write_noderev(svn_stream_from_aprfile2(noderev_file, TRUE,
+ scratch_pool),
+ noderev, scratch_pool));
+
+ SVN_ERR(svn_io_file_close(noderev_file, scratch_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,
+ svn_fs_x__noderev_t *noderev,
+ apr_pool_t *scratch_pool)
+{
+ svn_fs_x__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;
+ apr_int64_t txn_id
+ = svn_fs_x__get_txn_id(noderev->data_rep->id.change_set);
+ const char *file_name
+ = svn_fs_x__path_txn_sha1(fs, txn_id,
+ noderev->data_rep->sha1_digest,
+ scratch_pool);
+ svn_stringbuf_t *rep_string
+ = svn_fs_x__unparse_representation(noderev->data_rep,
+ (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_x__dirent_t *dirent,
+ svn_stream_t *stream,
+ apr_pool_t *scratch_pool)
+{
+ const char *val
+ = apr_psprintf(scratch_pool, "%s %s",
+ (dirent->kind == svn_node_file) ? SVN_FS_X__KIND_FILE
+ : SVN_FS_X__KIND_DIR,
+ svn_fs_x__id_unparse(&dirent->id, scratch_pool)->data);
+
+ SVN_ERR(svn_stream_printf(stream, scratch_pool, "K %" APR_SIZE_T_FMT
+ "\n%s\nV %" 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 SCRATCH_POOL. */
+static svn_error_t *
+unparse_dir_entries(apr_array_header_t *entries,
+ svn_stream_t *stream,
+ apr_pool_t *scratch_pool)
+{
+ apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+ int i;
+ for (i = 0; i < entries->nelts; ++i)
+ {
+ svn_fs_x__dirent_t *dirent;
+
+ svn_pool_clear(iterpool);
+ dirent = APR_ARRAY_IDX(entries, i, svn_fs_x__dirent_t *);
+ SVN_ERR(unparse_dir_entry(dirent, stream, iterpool));
+ }
+
+ SVN_ERR(svn_stream_printf(stream, scratch_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_x__change_t *
+path_change_dup(const svn_fs_x__change_t *source,
+ apr_pool_t *result_pool)
+{
+ svn_fs_x__change_t *result
+ = apr_pmemdup(result_pool, source, sizeof(*source));
+ result->path.data
+ = apr_pstrmemdup(result_pool, source->path.data, source->path.len);
+
+ 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_x__change_t CHANGED_PATHS, collapsing multiple changes into a
+ single summarical (is that real word?) change per path. DELETIONS is
+ also a path->svn_fs_x__change_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 svn_fs_x__change_t *change)
+{
+ apr_pool_t *pool = apr_hash_pool_get(changed_paths);
+ svn_fs_x__change_t *old_change, *new_change;
+ const svn_string_t *path = &change->path;
+
+ 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 unused node revision IDs in the
+ `reset' case. */
+ if ((! svn_fs_x__id_used(&change->noderev_id))
+ && (change->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 (svn_fs_x__id_used(&change->noderev_id)
+ && (!svn_fs_x__id_eq(&old_change->noderev_id, &change->noderev_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)
+ && (! ((change->change_kind == svn_fs_path_change_replace)
+ || (change->change_kind == svn_fs_path_change_reset)
+ || (change->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 ((change->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 (change->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(change, 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(change, 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 (change->text_mod)
+ old_change->text_mod = TRUE;
+ if (change->prop_mod)
+ old_change->prop_mod = TRUE;
+ if (change->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. */
+ new_change = path_change_dup(change, pool);
+ apr_hash_set(changed_paths, new_change->path.data,
+ new_change->path.len, new_change);
+ }
+
+ 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_x__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. Use SCRATCH_POOL for temporary allocations. */
+static svn_error_t *
+process_changes(void *baton_p,
+ svn_fs_x__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->change_kind == svn_fs_path_change_delete)
+ || (change->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;
+ apr_hash_this(hi, &path, &klen, NULL);
+
+ /* 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_x__txn_changes_fetch(apr_hash_t **changed_paths_p,
+ svn_fs_t *fs,
+ svn_fs_x__txn_id_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,
+ svn_fs_x__path_txn_changes(fs, txn_id, scratch_pool),
+ APR_READ | APR_BUFFERED, APR_OS_DEFAULT,
+ scratch_pool));
+
+ SVN_ERR(svn_fs_x__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;
+}
+
+/* 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.
+ Temporary allocations are from SCRATCH_POOL. */
+static svn_error_t *
+create_new_txn_noderev_from_rev(svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ svn_fs_x__id_t *src,
+ apr_pool_t *scratch_pool)
+{
+ svn_fs_x__noderev_t *noderev;
+ SVN_ERR(svn_fs_x__get_node_revision(&noderev, fs, src, scratch_pool,
+ scratch_pool));
+
+ /* This must be a root node. */
+ SVN_ERR_ASSERT( noderev->node_id.number == 0
+ && noderev->copy_id.number == 0);
+
+ if (svn_fs_x__is_txn(noderev->noderev_id.change_set))
+ return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
+ _("Copying from transactions not allowed"));
+
+ noderev->predecessor_id = noderev->noderev_id;
+ noderev->predecessor_count++;
+ noderev->copyfrom_path = NULL;
+ noderev->copyfrom_rev = SVN_INVALID_REVNUM;
+
+ /* For the transaction root, the copyroot never changes. */
+ svn_fs_x__init_txn_root(&noderev->noderev_id, txn_id);
+
+ return svn_fs_x__put_node_revision(fs, noderev, scratch_pool);
+}
+
+/* A structure used by get_and_increment_txn_key_body(). */
+typedef struct get_and_increment_txn_key_baton_t
+{
+ svn_fs_t *fs;
+ apr_uint64_t txn_number;
+} get_and_increment_txn_key_baton_t;
+
+/* 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 *scratch_pool)
+{
+ get_and_increment_txn_key_baton_t *cb = baton;
+ const char *txn_current_filename = svn_fs_x__path_txn_current(cb->fs,
+ scratch_pool);
+ const char *tmp_filename;
+ char new_id_str[SVN_INT64_BUFFER_SIZE];
+
+ svn_stringbuf_t *buf;
+ SVN_ERR(svn_fs_x__read_content(&buf, txn_current_filename, scratch_pool));
+
+ /* remove trailing newlines */
+ cb->txn_number = svn__base36toui64(NULL, buf->data);
+
+ /* 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_unique(&tmp_filename,
+ svn_dirent_dirname(txn_current_filename,
+ scratch_pool),
+ new_id_str,
+ svn__ui64tobase36(new_id_str, cb->txn_number+1),
+ svn_io_file_del_none, scratch_pool));
+ SVN_ERR(svn_fs_x__move_into_place(tmp_filename, txn_current_filename,
+ txn_current_filename, scratch_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_x__txn_id_t *txn_id,
+ svn_fs_t *fs,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ get_and_increment_txn_key_baton_t 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.fs = fs;
+ SVN_ERR(svn_fs_x__with_txn_current_lock(fs,
+ get_and_increment_txn_key_body,
+ &cb,
+ scratch_pool));
+ *txn_id = cb.txn_number;
+
+ *id_p = svn_fs_x__txn_name(*txn_id, result_pool);
+ txn_dir = svn_fs_x__path_txn_dir(fs, *txn_id, scratch_pool);
+
+ return svn_io_dir_make(txn_dir, APR_OS_DEFAULT, scratch_pool);
+}
+
+/* Create a new transaction in filesystem FS, based on revision REV,
+ and store it in *TXN_P, allocated in RESULT_POOL. Allocate necessary
+ temporaries from SCRATCH_POOL. */
+static svn_error_t *
+create_txn(svn_fs_txn_t **txn_p,
+ svn_fs_t *fs,
+ svn_revnum_t rev,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_fs_txn_t *txn;
+ fs_txn_data_t *ftd;
+ svn_fs_x__id_t root_id;
+
+ txn = apr_pcalloc(result_pool, sizeof(*txn));
+ ftd = apr_pcalloc(result_pool, sizeof(*ftd));
+
+ /* Valid revision number? */
+ SVN_ERR(svn_fs_x__ensure_revision_exists(rev, fs, scratch_pool));
+
+ /* Get the txn_id. */
+ SVN_ERR(create_txn_dir(&txn->id, &ftd->txn_id, fs, result_pool,
+ scratch_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_fs_x__init_rev_root(&root_id, rev);
+ SVN_ERR(create_new_txn_noderev_from_rev(fs, ftd->txn_id, &root_id,
+ scratch_pool));
+
+ /* Create an empty rev file. */
+ SVN_ERR(svn_io_file_create_empty(
+ svn_fs_x__path_txn_proto_rev(fs, ftd->txn_id, scratch_pool),
+ scratch_pool));
+
+ /* Create an empty rev-lock file. */
+ SVN_ERR(svn_io_file_create_empty(
+ svn_fs_x__path_txn_proto_rev_lock(fs, ftd->txn_id, scratch_pool),
+ scratch_pool));
+
+ /* Create an empty changes file. */
+ SVN_ERR(svn_io_file_create_empty(
+ svn_fs_x__path_txn_changes(fs, ftd->txn_id, scratch_pool),
+ scratch_pool));
+
+ /* Create the next-ids file. */
+ SVN_ERR(svn_io_file_create(
+ svn_fs_x__path_txn_next_ids(fs, ftd->txn_id, scratch_pool),
+ "0 0\n", scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* 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,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *pool)
+{
+ svn_stream_t *stream;
+
+ /* Check for issue #3696. (When we find and fix the cause, we can change
+ * this to an assertion.) */
+ if (txn_id == SVN_FS_X__INVALID_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,
+ svn_fs_x__path_txn_props(fs, txn_id, pool),
+ pool, pool));
+
+ /* Read in the property list. */
+ SVN_ERR(svn_hash_read2(proplist, stream, SVN_HASH_TERMINATOR, pool));
+
+ return svn_stream_close(stream);
+}
+
+/* Save the property list PROPS as the revprops for transaction TXN_ID
+ in FS. Perform temporary allocations in SCRATCH_POOL. */
+static svn_error_t *
+set_txn_proplist(svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_hash_t *props,
+ svn_boolean_t final,
+ apr_pool_t *scratch_pool)
+{
+ svn_stringbuf_t *buf;
+ svn_stream_t *stream;
+
+ /* Write out the new file contents to BUF. */
+ buf = svn_stringbuf_create_ensure(1024, scratch_pool);
+ stream = svn_stream_from_stringbuf(buf, scratch_pool);
+ SVN_ERR(svn_hash_write2(props, stream, SVN_HASH_TERMINATOR, scratch_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
+ ? svn_fs_x__path_txn_props_final(fs, txn_id,
+ scratch_pool)
+ : svn_fs_x__path_txn_props(fs, txn_id,
+ scratch_pool)),
+ buf->data, buf->len,
+ NULL /* copy_perms_path */, scratch_pool));
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_fs_x__change_txn_prop(svn_fs_txn_t *txn,
+ const char *name,
+ const svn_string_t *value,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *props = apr_array_make(scratch_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_x__change_txn_props(txn, props, scratch_pool);
+}
+
+svn_error_t *
+svn_fs_x__change_txn_props(svn_fs_txn_t *txn,
+ const apr_array_header_t *props,
+ apr_pool_t *scratch_pool)
+{
+ fs_txn_data_t *ftd = txn->fsap_data;
+ apr_hash_t *txn_prop = apr_hash_make(scratch_pool);
+ int i;
+ svn_error_t *err;
+
+ err = get_txn_proplist(txn_prop, txn->fs, ftd->txn_id, scratch_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", scratch_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,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__get_txn(svn_fs_x__transaction_t **txn_p,
+ svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *pool)
+{
+ svn_fs_x__transaction_t *txn;
+ svn_fs_x__noderev_t *noderev;
+ svn_fs_x__id_t root_id;
+
+ txn = apr_pcalloc(pool, sizeof(*txn));
+ txn->proplist = apr_hash_make(pool);
+
+ SVN_ERR(get_txn_proplist(txn->proplist, fs, txn_id, pool));
+ svn_fs_x__init_txn_root(&root_id, txn_id);
+
+ SVN_ERR(svn_fs_x__get_node_revision(&noderev, fs, &root_id, pool, pool));
+
+ txn->base_rev = svn_fs_x__get_revnum(noderev->predecessor_id.change_set);
+ txn->copies = NULL;
+
+ *txn_p = txn;
+
+ return SVN_NO_ERROR;
+}
+
+/* If it is supported by the format of file system FS, store the (ITEM_INDEX,
+ * OFFSET) pair in the log-to-phys proto index file of transaction TXN_ID.
+ * Use SCRATCH_POOL for temporary allocations.
+ */
+static svn_error_t *
+store_l2p_index_entry(svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_off_t offset,
+ apr_uint64_t item_index,
+ apr_pool_t *scratch_pool)
+{
+ const char *path = svn_fs_x__path_l2p_proto_index(fs, txn_id, scratch_pool);
+ apr_file_t *file;
+ SVN_ERR(svn_fs_x__l2p_proto_index_open(&file, path, scratch_pool));
+ SVN_ERR(svn_fs_x__l2p_proto_index_add_entry(file, offset, 0,
+ item_index, scratch_pool));
+ SVN_ERR(svn_io_file_close(file, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* If it is supported by the format of file system FS, store ENTRY in the
+ * phys-to-log proto index file of transaction TXN_ID.
+ * Use SCRATCH_POOL for temporary allocations.
+ */
+static svn_error_t *
+store_p2l_index_entry(svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ svn_fs_x__p2l_entry_t *entry,
+ apr_pool_t *scratch_pool)
+{
+ const char *path = svn_fs_x__path_p2l_proto_index(fs, txn_id, scratch_pool);
+ apr_file_t *file;
+ SVN_ERR(svn_fs_x__p2l_proto_index_open(&file, path, scratch_pool));
+ SVN_ERR(svn_fs_x__p2l_proto_index_add_entry(file, entry, scratch_pool));
+ SVN_ERR(svn_io_file_close(file, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Allocate an item index in the transaction TXN_ID of file system FS and
+ * return it in *ITEM_INDEX. Use SCRATCH_POOL for temporary allocations.
+ */
+static svn_error_t *
+allocate_item_index(apr_uint64_t *item_index,
+ svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *scratch_pool)
+{
+ 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 */
+ SVN_ERR(svn_io_file_open(&file,
+ svn_fs_x__path_txn_item_index(fs, txn_id,
+ scratch_pool),
+ APR_READ | APR_WRITE
+ | APR_CREATE | APR_BUFFERED,
+ APR_OS_DEFAULT, scratch_pool));
+ SVN_ERR(svn_io_file_read_full2(file, buffer, sizeof(buffer)-1,
+ &read, &eof, scratch_pool));
+ if (read)
+ SVN_ERR(svn_cstring_atoui64(item_index, buffer));
+ else
+ *item_index = SVN_FS_X__ITEM_INDEX_FIRST_USER;
+
+ /* increment it */
+ to_write = svn__ui64toa(buffer, *item_index + 1);
+
+ /* write it back to disk */
+ SVN_ERR(svn_io_file_seek(file, APR_SET, &offset, scratch_pool));
+ SVN_ERR(svn_io_file_write_full(file, buffer, to_write, NULL, scratch_pool));
+ SVN_ERR(svn_io_file_close(file, scratch_pool));
+
+ 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
+ SCRATCH_POOL. */
+static svn_error_t *
+write_next_ids(svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_uint64_t node_id,
+ apr_uint64_t copy_id,
+ apr_pool_t *scratch_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,
+ svn_fs_x__path_txn_next_ids(fs, txn_id,
+ scratch_pool),
+ APR_WRITE | APR_TRUNCATE,
+ APR_OS_DEFAULT, scratch_pool));
+ SVN_ERR(svn_io_file_write_full(file, buffer, p - buffer, NULL,
+ scratch_pool));
+ return svn_io_file_close(file, scratch_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 temporary allocations in SCRATCH_POOL. */
+static svn_error_t *
+read_next_ids(apr_uint64_t *node_id,
+ apr_uint64_t *copy_id,
+ svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *scratch_pool)
+{
+ svn_stringbuf_t *buf;
+ const char *str;
+ SVN_ERR(svn_fs_x__read_content(&buf,
+ svn_fs_x__path_txn_next_ids(fs, txn_id,
+ scratch_pool),
+ scratch_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 temporary allocations in SCRATCH_POOL. */
+static svn_error_t *
+get_new_txn_node_id(svn_fs_x__id_t *node_id_p,
+ svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *scratch_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, scratch_pool));
+
+ node_id_p->change_set = svn_fs_x__change_set_by_txn(txn_id);
+ node_id_p->number = node_id;
+
+ SVN_ERR(write_next_ids(fs, txn_id, ++node_id, copy_id, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__reserve_copy_id(svn_fs_x__id_t *copy_id_p,
+ svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *scratch_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, scratch_pool));
+
+ copy_id_p->change_set = svn_fs_x__change_set_by_txn(txn_id);
+ copy_id_p->number = copy_id;
+
+ SVN_ERR(write_next_ids(fs, txn_id, node_id, ++copy_id, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__create_node(svn_fs_t *fs,
+ svn_fs_x__noderev_t *noderev,
+ const svn_fs_x__id_t *copy_id,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *scratch_pool)
+{
+ /* Get a new node-id for this node. */
+ SVN_ERR(get_new_txn_node_id(&noderev->node_id, fs, txn_id, scratch_pool));
+
+ /* Assign copy-id. */
+ noderev->copy_id = *copy_id;
+
+ /* Noderev-id = Change set and item number within this change set. */
+ noderev->noderev_id.change_set = svn_fs_x__change_set_by_txn(txn_id);
+ SVN_ERR(allocate_item_index(&noderev->noderev_id.number, fs, txn_id,
+ scratch_pool));
+
+ SVN_ERR(svn_fs_x__put_node_revision(fs, noderev, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__purge_txn(svn_fs_t *fs,
+ const char *txn_id_str,
+ apr_pool_t *scratch_pool)
+{
+ svn_fs_x__txn_id_t txn_id;
+ SVN_ERR(svn_fs_x__txn_by_name(&txn_id, txn_id_str));
+
+ /* Remove the shared transaction object associated with this transaction. */
+ SVN_ERR(purge_shared_txn(fs, txn_id, scratch_pool));
+ /* Remove the directory associated with this transaction. */
+ SVN_ERR(svn_io_remove_dir2(svn_fs_x__path_txn_dir(fs, txn_id, scratch_pool),
+ FALSE, NULL, NULL, scratch_pool));
+
+ /* 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_x__path_txn_proto_rev(fs, txn_id, scratch_pool),
+ TRUE, scratch_pool));
+ SVN_ERR(svn_io_remove_file2(
+ svn_fs_x__path_txn_proto_rev_lock(fs, txn_id, scratch_pool),
+ TRUE, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_fs_x__abort_txn(svn_fs_txn_t *txn,
+ apr_pool_t *scratch_pool)
+{
+ SVN_ERR(svn_fs__check_fs(txn->fs, TRUE));
+
+ /* Now, purge the transaction. */
+ SVN_ERR_W(svn_fs_x__purge_txn(txn->fs, txn->id, scratch_pool),
+ apr_psprintf(scratch_pool, _("Transaction '%s' cleanup failed"),
+ txn->id));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__set_entry(svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ svn_fs_x__noderev_t *parent_noderev,
+ const char *name,
+ const svn_fs_x__id_t *id,
+ svn_node_kind_t kind,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_fs_x__representation_t *rep = parent_noderev->data_rep;
+ const char *filename
+ = svn_fs_x__path_txn_node_children(fs, &parent_noderev->noderev_id,
+ scratch_pool, scratch_pool);
+ apr_file_t *file;
+ svn_stream_t *out;
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+ apr_pool_t *subpool = svn_pool_create(scratch_pool);
+
+ if (!rep || !svn_fs_x__is_txn(rep->id.change_set))
+ {
+ 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_x__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, scratch_pool));
+ out = svn_stream_from_aprfile2(file, TRUE, scratch_pool);
+ SVN_ERR(unparse_dir_entries(entries, out, subpool));
+
+ svn_pool_clear(subpool);
+
+ /* Provide the parent with a data rep if it had none before
+ (directories so far empty). */
+ if (!rep)
+ {
+ rep = apr_pcalloc(result_pool, sizeof(*rep));
+ parent_noderev->data_rep = rep;
+ }
+
+ /* Mark the node-rev's data rep as mutable. */
+ rep->id.change_set = svn_fs_x__change_set_by_txn(txn_id);
+ rep->id.number = SVN_FS_X__ITEM_INDEX_UNUSED;
+
+ /* Save noderev to disk. */
+ SVN_ERR(svn_fs_x__put_node_revision(fs, parent_noderev, subpool));
+ }
+ 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, scratch_pool));
+ out = svn_stream_from_aprfile2(file, TRUE, scratch_pool);
+ }
+
+ /* update directory cache */
+ {
+ /* build parameters: (name, new entry) pair */
+ const svn_fs_x__id_t *key = &(parent_noderev->noderev_id);
+ 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->dir_cache, key,
+ svn_fs_x__replace_dir_entry, &baton,
+ subpool));
+ }
+ svn_pool_clear(subpool);
+
+ /* Append an incremental hash entry for the entry change. */
+ if (id)
+ {
+ svn_fs_x__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_x__add_change(svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ const char *path,
+ const svn_fs_x__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 *scratch_pool)
+{
+ apr_file_t *file;
+ svn_fs_x__change_t change;
+ apr_hash_t *changes = apr_hash_make(scratch_pool);
+
+ /* Not using APR_BUFFERED to append change in one atomic write operation. */
+ SVN_ERR(svn_io_file_open(&file,
+ svn_fs_x__path_txn_changes(fs, txn_id,
+ scratch_pool),
+ APR_APPEND | APR_WRITE | APR_CREATE,
+ APR_OS_DEFAULT, scratch_pool));
+
+ change.path.data = path;
+ change.path.len = strlen(path);
+ change.noderev_id = *id;
+ change.change_kind = change_kind;
+ 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(scratch_pool, copyfrom_path);
+
+ svn_hash_sets(changes, path, &change);
+ SVN_ERR(svn_fs_x__write_changes(svn_stream_from_aprfile2(file, TRUE,
+ scratch_pool),
+ fs, changes, FALSE, scratch_pool));
+
+ return svn_io_file_close(file, scratch_pool);
+}
+
+/* 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. */
+typedef struct rep_write_baton_t
+{
+ /* 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. */
+ svn_fs_x__noderev_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;
+
+ /* Receives the low-level checksum when closing REP_STREAM. */
+ apr_uint32_t fnv1a_checksum;
+
+ /* Local pool, available for allocations that must remain valid as long
+ as this baton is used but may be cleaned up immediately afterwards. */
+ apr_pool_t *local_pool;
+
+ /* Outer / result pool. */
+ apr_pool_t *result_pool;
+} rep_write_baton_t;
+
+/* Handler for the write method of the representation writable stream.
+ BATON is a rep_write_baton_t, 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)
+{
+ rep_write_baton_t *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;
+
+ return svn_stream_write(b->delta_stream, data, len);
+}
+
+/* Set *SPANNED to the number of shards touched when walking WALK steps on
+ * NODEREV's predecessor chain in FS.
+ * Use SCRATCH_POOL for temporary allocations.
+ */
+static svn_error_t *
+shards_spanned(int *spanned,
+ svn_fs_t *fs,
+ svn_fs_x__noderev_t *noderev,
+ int walk,
+ apr_pool_t *scratch_pool)
+{
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+ int shard_size = ffd->max_files_per_dir;
+ 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(scratch_pool);
+ while (walk-- && noderev->predecessor_count)
+ {
+ svn_fs_x__id_t id = noderev->predecessor_id;
+
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_fs_x__get_node_revision(&noderev, fs, &id, scratch_pool,
+ iterpool));
+ shard = svn_fs_x__get_revnum(id.change_set) / 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(svn_fs_x__representation_t **rep,
+ svn_fs_t *fs,
+ svn_fs_x__noderev_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;
+ svn_fs_x__noderev_t *base;
+ svn_fs_x__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_fs_x__id_t id = noderev->predecessor_id;
+ svn_pool_clear(iterpool);
+ SVN_ERR(svn_fs_x__get_node_revision(&base, fs, &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_x__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)
+{
+ svn_error_t *err;
+ rep_write_baton_t *b = data;
+ svn_fs_x__txn_id_t txn_id
+ = svn_fs_x__get_txn_id(b->noderev->noderev_id.change_set);
+
+ /* Truncate and close the protorevfile. */
+ err = svn_io_file_trunc(b->file, b->rep_offset, b->local_pool);
+ err = svn_error_compose_create(err, svn_io_file_close(b->file,
+ b->local_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, txn_id,
+ b->lockcookie,
+ b->local_pool));
+ if (err)
+ {
+ apr_status_t rc = err->apr_err;
+ svn_error_clear(err);
+ return rc;
+ }
+
+ return APR_SUCCESS;
+}
+
+/* Get a rep_write_baton_t, allocated from RESULT_POOL, and store it in
+ WB_P for the representation indicated by NODEREV in filesystem FS.
+ Only appropriate for file contents, not for props or directory contents.
+ */
+static svn_error_t *
+rep_write_get_baton(rep_write_baton_t **wb_p,
+ svn_fs_t *fs,
+ svn_fs_x__noderev_t *noderev,
+ apr_pool_t *result_pool)
+{
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+ rep_write_baton_t *b;
+ apr_file_t *file;
+ svn_fs_x__representation_t *base_rep;
+ svn_stream_t *source;
+ svn_txdelta_window_handler_t wh;
+ void *whb;
+ int diff_version = 1;
+ svn_fs_x__rep_header_t header = { 0 };
+ svn_fs_x__txn_id_t txn_id
+ = svn_fs_x__get_txn_id(noderev->noderev_id.change_set);
+
+ b = apr_pcalloc(result_pool, sizeof(*b));
+
+ b->sha1_checksum_ctx = svn_checksum_ctx_create(svn_checksum_sha1,
+ result_pool);
+ b->md5_checksum_ctx = svn_checksum_ctx_create(svn_checksum_md5,
+ result_pool);
+
+ b->fs = fs;
+ b->result_pool = result_pool;
+ b->local_pool = svn_pool_create(result_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, txn_id,
+ b->local_pool));
+
+ b->file = file;
+ b->rep_stream = svn_checksum__wrap_write_stream_fnv1a_32x4(
+ &b->fnv1a_checksum,
+ svn_stream_from_aprfile2(file, TRUE,
+ b->local_pool),
+ b->local_pool);
+
+ SVN_ERR(svn_fs_x__get_file_offset(&b->rep_offset, file, b->local_pool));
+
+ /* Get the base for this delta. */
+ SVN_ERR(choose_delta_base(&base_rep, fs, noderev, FALSE, b->local_pool));
+ SVN_ERR(svn_fs_x__get_contents(&source, fs, base_rep, TRUE,
+ b->local_pool));
+
+ /* Write out the rep header. */
+ if (base_rep)
+ {
+ header.base_revision = svn_fs_x__get_revnum(base_rep->id.change_set);
+ header.base_item_index = base_rep->id.number;
+ header.base_length = base_rep->size;
+ header.type = svn_fs_x__rep_delta;
+ }
+ else
+ {
+ header.type = svn_fs_x__rep_self_delta;
+ }
+ SVN_ERR(svn_fs_x__write_rep_header(&header, b->rep_stream,
+ b->local_pool));
+
+ /* Now determine the offset of the actual svndiff data. */
+ SVN_ERR(svn_fs_x__get_file_offset(&b->delta_start, file,
+ b->local_pool));
+
+ /* Cleanup in case something goes wrong. */
+ apr_pool_cleanup_register(b->local_pool, b, rep_write_cleanup,
+ apr_pool_cleanup_null);
+
+ /* Prepare to write the svndiff data. */
+ svn_txdelta_to_svndiff3(&wh,
+ &whb,
+ svn_stream_disown(b->rep_stream, b->result_pool),
+ diff_version,
+ ffd->delta_compression_level,
+ result_pool);
+
+ b->delta_stream = svn_txdelta_target_push(wh, whb, source,
+ b->result_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
+ svn_fs_x__representation_t*), otherwise pass in NULL for REPS_HASH.
+ 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(svn_fs_x__representation_t **old_rep,
+ svn_fs_t *fs,
+ svn_fs_x__representation_t *rep,
+ apr_hash_t *reps_hash,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_error_t *err;
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+
+ /* 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)
+ {
+ svn_checksum_t checksum;
+ checksum.digest = rep->sha1_digest;
+ checksum.kind = svn_checksum_sha1;
+ err = svn_fs_x__get_rep_reference(old_rep, fs, &checksum, result_pool,
+ scratch_pool);
+
+ /* ### Other error codes that we shouldn't mask out? */
+ if (err == SVN_NO_ERROR)
+ {
+ if (*old_rep)
+ SVN_ERR(svn_fs_x__check_rep(*old_rep, fs, 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 && svn_fs_x__is_txn(rep->id.change_set))
+ {
+ svn_node_kind_t kind;
+ const char *file_name
+ = svn_fs_x__path_txn_sha1(fs,
+ svn_fs_x__get_txn_id(rep->id.change_set),
+ 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_x__parse_representation(old_rep, rep_string,
+ result_pool, scratch_pool));
+ }
+ }
+
+ /* Add information that is missing in the cached data. */
+ if (*old_rep)
+ {
+ /* Use the old rep for this content. */
+ memcpy((*old_rep)->md5_digest, rep->md5_digest, sizeof(rep->md5_digest));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Copy the hash sum calculation results from MD5_CTX, SHA1_CTX into REP.
+ * Use SCRATCH_POOL for temporary allocations.
+ */
+static svn_error_t *
+digests_final(svn_fs_x__representation_t *rep,
+ const svn_checksum_ctx_t *md5_ctx,
+ const svn_checksum_ctx_t *sha1_ctx,
+ apr_pool_t *scratch_pool)
+{
+ svn_checksum_t *checksum;
+
+ SVN_ERR(svn_checksum_final(&checksum, md5_ctx, scratch_pool));
+ memcpy(rep->md5_digest, checksum->digest, svn_checksum_size(checksum));
+ SVN_ERR(svn_checksum_final(&checksum, sha1_ctx, scratch_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_t. 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)
+{
+ rep_write_baton_t *b = baton;
+ svn_fs_x__representation_t *rep;
+ svn_fs_x__representation_t *old_rep;
+ apr_off_t offset;
+ apr_int64_t txn_id;
+
+ rep = apr_pcalloc(b->result_pool, sizeof(*rep));
+
+ /* Close our delta stream so the last bits of svndiff are written
+ out. */
+ SVN_ERR(svn_stream_close(b->delta_stream));
+
+ /* Determine the length of the svndiff data. */
+ SVN_ERR(svn_fs_x__get_file_offset(&offset, b->file, b->local_pool));
+ rep->size = offset - b->delta_start;
+
+ /* Fill in the rest of the representation field. */
+ rep->expanded_size = b->rep_size;
+ txn_id = svn_fs_x__get_txn_id(b->noderev->noderev_id.change_set);
+ rep->id.change_set = svn_fs_x__change_set_by_txn(txn_id);
+
+ /* 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, NULL, b->result_pool,
+ b->local_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->local_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->id.number, b->fs, txn_id,
+ b->local_pool));
+ SVN_ERR(store_l2p_index_entry(b->fs, txn_id, b->rep_offset,
+ rep->id.number, b->local_pool));
+
+ b->noderev->data_rep = rep;
+ }
+
+ SVN_ERR(svn_stream_close(b->rep_stream));
+
+ /* Remove cleanup callback. */
+ apr_pool_cleanup_kill(b->local_pool, b, rep_write_cleanup);
+
+ /* Write out the new node-rev information. */
+ SVN_ERR(svn_fs_x__put_node_revision(b->fs, b->noderev, b->local_pool));
+ if (!old_rep)
+ {
+ svn_fs_x__p2l_entry_t entry;
+ svn_fs_x__id_t noderev_id;
+ noderev_id.change_set = SVN_FS_X__INVALID_CHANGE_SET;
+ noderev_id.number = rep->id.number;
+
+ entry.offset = b->rep_offset;
+ SVN_ERR(svn_fs_x__get_file_offset(&offset, b->file, b->local_pool));
+ entry.size = offset - b->rep_offset;
+ entry.type = SVN_FS_X__ITEM_TYPE_FILE_REP;
+ entry.item_count = 1;
+ entry.items = &noderev_id;
+ entry.fnv1_checksum = b->fnv1a_checksum;
+
+ SVN_ERR(store_sha1_rep_mapping(b->fs, b->noderev, b->local_pool));
+ SVN_ERR(store_p2l_index_entry(b->fs, txn_id, &entry, b->local_pool));
+ }
+
+ SVN_ERR(svn_io_file_close(b->file, b->local_pool));
+ SVN_ERR(unlock_proto_rev(b->fs, txn_id, b->lockcookie, b->local_pool));
+ svn_pool_destroy(b->local_pool);
+
+ return SVN_NO_ERROR;
+}
+
+/* Store a writable stream in *CONTENTS_P, allocated in RESULT_POOL, that
+ will receive all data written and store it as the file data representation
+ referenced by NODEREV in filesystem FS. 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,
+ svn_fs_x__noderev_t *noderev,
+ apr_pool_t *result_pool)
+{
+ rep_write_baton_t *wb;
+
+ if (! svn_fs_x__is_txn(noderev->noderev_id.change_set))
+ return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
+ _("Attempted to write to non-transaction '%s'"),
+ svn_fs_x__id_unparse(&noderev->noderev_id,
+ result_pool)->data);
+
+ SVN_ERR(rep_write_get_baton(&wb, fs, noderev, result_pool));
+
+ *contents_p = svn_stream_create(wb, result_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_x__set_contents(svn_stream_t **stream,
+ svn_fs_t *fs,
+ svn_fs_x__noderev_t *noderev,
+ apr_pool_t *result_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, result_pool);
+}
+
+svn_error_t *
+svn_fs_x__create_successor(svn_fs_t *fs,
+ svn_fs_x__noderev_t *new_noderev,
+ const svn_fs_x__id_t *copy_id,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *scratch_pool)
+{
+ new_noderev->copy_id = *copy_id;
+ new_noderev->noderev_id.change_set = svn_fs_x__change_set_by_txn(txn_id);
+ SVN_ERR(allocate_item_index(&new_noderev->noderev_id.number, fs, txn_id,
+ scratch_pool));
+
+ if (! new_noderev->copyroot_path)
+ {
+ new_noderev->copyroot_path
+ = apr_pstrdup(scratch_pool, new_noderev->created_path);
+ new_noderev->copyroot_rev
+ = svn_fs_x__get_revnum(new_noderev->noderev_id.change_set);
+ }
+
+ SVN_ERR(svn_fs_x__put_node_revision(fs, new_noderev, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__set_proplist(svn_fs_t *fs,
+ svn_fs_x__noderev_t *noderev,
+ apr_hash_t *proplist,
+ apr_pool_t *scratch_pool)
+{
+ const svn_fs_x__id_t *id = &noderev->noderev_id;
+ const char *filename = svn_fs_x__path_txn_node_props(fs, id, scratch_pool,
+ scratch_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, scratch_pool));
+ out = svn_stream_from_aprfile2(file, TRUE, scratch_pool);
+ SVN_ERR(svn_hash_write2(proplist, out, SVN_HASH_TERMINATOR, scratch_pool));
+ SVN_ERR(svn_io_file_close(file, scratch_pool));
+
+ /* Mark the node-rev's prop rep as mutable, if not already done. */
+ if (!noderev->prop_rep
+ || svn_fs_x__is_revision(noderev->prop_rep->id.change_set))
+ {
+ svn_fs_x__txn_id_t txn_id
+ = svn_fs_x__get_txn_id(noderev->noderev_id.change_set);
+ noderev->prop_rep = apr_pcalloc(scratch_pool, sizeof(*noderev->prop_rep));
+ noderev->prop_rep->id.change_set = id->change_set;
+ SVN_ERR(allocate_item_index(&noderev->prop_rep->id.number, fs,
+ txn_id, scratch_pool));
+ SVN_ERR(svn_fs_x__put_node_revision(fs, noderev, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* This baton is used by the stream created for write_container_rep. */
+typedef struct write_container_baton_t
+{
+ svn_stream_t *stream;
+
+ apr_size_t size;
+
+ svn_checksum_ctx_t *md5_ctx;
+ svn_checksum_ctx_t *sha1_ctx;
+} write_container_baton_t;
+
+/* The handler for the write_container_rep stream. BATON is a
+ write_container_baton_t, 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)
+{
+ write_container_baton_t *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 *scratch_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 *scratch_pool)
+{
+ apr_hash_t *hash = baton;
+ SVN_ERR(svn_hash_write2(hash, stream, SVN_HASH_TERMINATOR, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Implement collection_writer_t writing the svn_fs_x__dirent_t* array given
+ as BATON. */
+static svn_error_t *
+write_directory_to_stream(svn_stream_t *stream,
+ void *baton,
+ apr_pool_t *scratch_pool)
+{
+ apr_array_header_t *dir = baton;
+ SVN_ERR(unparse_dir_entries(dir, stream, 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 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. When 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.
+ If FINAL_REVISION is not SVN_INVALID_REVNUM, use it to determine whether
+ to write to the proto-index files.
+ Perform temporary allocations in SCRATCH_POOL.
+ */
+static svn_error_t *
+write_container_delta_rep(svn_fs_x__representation_t *rep,
+ apr_file_t *file,
+ void *collection,
+ collection_writer_t writer,
+ svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ svn_fs_x__noderev_t *noderev,
+ apr_hash_t *reps_hash,
+ apr_uint32_t item_type,
+ svn_revnum_t final_revision,
+ apr_pool_t *scratch_pool)
+{
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+ svn_txdelta_window_handler_t diff_wh;
+ void *diff_whb;
+
+ svn_stream_t *file_stream;
+ svn_stream_t *stream;
+ svn_fs_x__representation_t *base_rep;
+ svn_fs_x__representation_t *old_rep;
+ svn_fs_x__p2l_entry_t entry;
+ svn_stream_t *source;
+ svn_fs_x__rep_header_t header = { 0 };
+
+ apr_off_t rep_end = 0;
+ apr_off_t delta_start = 0;
+ apr_off_t offset = 0;
+
+ write_container_baton_t *whb;
+ int diff_version = 1;
+ svn_boolean_t is_props = (item_type == SVN_FS_X__ITEM_TYPE_FILE_PROPS)
+ || (item_type == SVN_FS_X__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_x__get_contents(&source, fs, base_rep, FALSE, scratch_pool));
+
+ SVN_ERR(svn_fs_x__get_file_offset(&offset, file, scratch_pool));
+
+ /* Write out the rep header. */
+ if (base_rep)
+ {
+ header.base_revision = svn_fs_x__get_revnum(base_rep->id.change_set);
+ header.base_item_index = base_rep->id.number;
+ header.base_length = base_rep->size;
+ header.type = svn_fs_x__rep_delta;
+ }
+ else
+ {
+ header.type = svn_fs_x__rep_self_delta;
+ }
+
+ file_stream = svn_checksum__wrap_write_stream_fnv1a_32x4(
+ &entry.fnv1_checksum,
+ svn_stream_from_aprfile2(file, TRUE,
+ scratch_pool),
+ scratch_pool);
+ SVN_ERR(svn_fs_x__write_rep_header(&header, file_stream, scratch_pool));
+ SVN_ERR(svn_fs_x__get_file_offset(&delta_start, file, scratch_pool));
+
+ /* Prepare to write the svndiff data. */
+ svn_txdelta_to_svndiff3(&diff_wh,
+ &diff_whb,
+ svn_stream_disown(file_stream, scratch_pool),
+ 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));
+
+ /* 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, fs, rep, reps_hash, scratch_pool,
+ scratch_pool));
+
+ if (old_rep)
+ {
+ SVN_ERR(svn_stream_close(file_stream));
+
+ /* 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));
+ }
+ else
+ {
+ svn_fs_x__id_t noderev_id;
+
+ /* Write out our cosmetic end marker. */
+ SVN_ERR(svn_fs_x__get_file_offset(&rep_end, file, scratch_pool));
+ SVN_ERR(svn_stream_puts(file_stream, "ENDREP\n"));
+ SVN_ERR(svn_stream_close(file_stream));
+
+ SVN_ERR(allocate_item_index(&rep->id.number, fs, txn_id,
+ scratch_pool));
+ SVN_ERR(store_l2p_index_entry(fs, txn_id, offset, rep->id.number,
+ scratch_pool));
+
+ noderev_id.change_set = SVN_FS_X__INVALID_CHANGE_SET;
+ noderev_id.number = rep->id.number;
+
+ entry.offset = offset;
+ SVN_ERR(svn_fs_x__get_file_offset(&offset, file, scratch_pool));
+ entry.size = offset - entry.offset;
+ entry.type = item_type;
+ entry.item_count = 1;
+ entry.items = &noderev_id;
+
+ SVN_ERR(store_p2l_index_entry(fs, txn_id, &entry, scratch_pool));
+
+ /* update the representation */
+ rep->expanded_size = whb->size;
+ rep->size = rep_end - delta_start;
+ }
+
+ 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 SCRATCH_POOL for temporary
+ allocations.
+
+ If you change this function, consider updating svn_fs_x__verify() too.
+ */
+static svn_error_t *
+validate_root_noderev(svn_fs_t *fs,
+ svn_fs_x__noderev_t *root_noderev,
+ svn_revnum_t rev,
+ apr_pool_t *scratch_pool)
+{
+ svn_revnum_t head_revnum = rev-1;
+ int head_predecessor_count;
+
+ SVN_ERR_ASSERT(rev > 0);
+
+ /* Compute HEAD_PREDECESSOR_COUNT. */
+ {
+ svn_fs_x__id_t head_root_id;
+ svn_fs_x__noderev_t *head_root_noderev;
+
+ /* Get /@HEAD's noderev. */
+ svn_fs_x__init_rev_root(&head_root_id, head_revnum);
+ SVN_ERR(svn_fs_x__get_node_revision(&head_root_noderev, fs,
+ &head_root_id, scratch_pool,
+ scratch_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 - 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.
+ */
+static void
+get_final_id(svn_fs_x__id_t *part,
+ svn_revnum_t revision)
+{
+ if (!svn_fs_x__is_revision(part->change_set))
+ part->change_set = svn_fs_x__change_set_by_rev(revision);
+}
+
+/* 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 noderev-id. 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 SCRATCH_POOL. */
+static svn_error_t *
+write_final_rev(svn_fs_x__id_t *new_id_p,
+ apr_file_t *file,
+ svn_revnum_t rev,
+ svn_fs_t *fs,
+ const svn_fs_x__id_t *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 *scratch_pool)
+{
+ svn_fs_x__noderev_t *noderev;
+ apr_off_t my_offset;
+ svn_fs_x__id_t new_id;
+ svn_fs_x__id_t noderev_id;
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+ svn_fs_x__txn_id_t txn_id = svn_fs_x__get_txn_id(id->change_set);
+ svn_fs_x__p2l_entry_t entry;
+ svn_fs_x__change_set_t change_set = svn_fs_x__change_set_by_rev(rev);
+ svn_stream_t *file_stream;
+ apr_pool_t *subpool;
+
+ /* Check to see if this is a transaction node. */
+ if (txn_id == SVN_FS_X__INVALID_TXN_ID)
+ {
+ svn_fs_x__id_reset(new_id_p);
+ return SVN_NO_ERROR;
+ }
+
+ subpool = svn_pool_create(scratch_pool);
+ SVN_ERR(svn_fs_x__get_node_revision(&noderev, fs, id, scratch_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_x__rep_contents_dir(&entries, fs, noderev, scratch_pool,
+ subpool));
+ for (i = 0; i < entries->nelts; ++i)
+ {
+ svn_fs_x__dirent_t *dirent = APR_ARRAY_IDX(entries, i,
+ svn_fs_x__dirent_t *);
+
+ svn_pool_clear(subpool);
+ SVN_ERR(write_final_rev(&new_id, file, rev, fs, &dirent->id,
+ initial_offset, reps_to_cache, reps_hash,
+ reps_pool, FALSE, subpool));
+ if ( svn_fs_x__id_used(&new_id)
+ && (svn_fs_x__get_revnum(new_id.change_set) == rev))
+ dirent->id = new_id;
+ }
+
+ if (noderev->data_rep
+ && ! svn_fs_x__is_revision(noderev->data_rep->id.change_set))
+ {
+ /* Write out the contents of this directory as a text rep. */
+ noderev->data_rep->id.change_set = change_set;
+ SVN_ERR(write_container_delta_rep(noderev->data_rep, file,
+ entries,
+ write_directory_to_stream,
+ fs, txn_id, noderev, NULL,
+ SVN_FS_X__ITEM_TYPE_DIR_REP,
+ rev, scratch_pool));
+ }
+ }
+ 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
+ && svn_fs_x__is_txn(noderev->data_rep->id.change_set))
+ {
+ noderev->data_rep->id.change_set = change_set;
+ }
+ }
+
+ svn_pool_destroy(subpool);
+
+ /* Fix up the property reps. */
+ if (noderev->prop_rep
+ && svn_fs_x__is_txn(noderev->prop_rep->id.change_set))
+ {
+ apr_hash_t *proplist;
+ apr_uint32_t item_type = noderev->kind == svn_node_dir
+ ? SVN_FS_X__ITEM_TYPE_DIR_PROPS
+ : SVN_FS_X__ITEM_TYPE_FILE_PROPS;
+ SVN_ERR(svn_fs_x__get_proplist(&proplist, fs, noderev, scratch_pool,
+ scratch_pool));
+
+ noderev->prop_rep->id.change_set = change_set;
+
+ SVN_ERR(write_container_delta_rep(noderev->prop_rep, file, proplist,
+ write_hash_to_stream, fs, txn_id,
+ noderev, reps_hash, item_type, rev,
+ scratch_pool));
+ }
+
+ /* Convert our temporary ID into a permanent revision one. */
+ get_final_id(&noderev->node_id, rev);
+ get_final_id(&noderev->copy_id, rev);
+ get_final_id(&noderev->noderev_id, rev);
+
+ if (noderev->copyroot_rev == SVN_INVALID_REVNUM)
+ noderev->copyroot_rev = rev;
+
+ SVN_ERR(svn_fs_x__get_file_offset(&my_offset, file, scratch_pool));
+
+ SVN_ERR(store_l2p_index_entry(fs, txn_id, my_offset,
+ noderev->noderev_id.number, scratch_pool));
+ new_id = noderev->noderev_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
+ && svn_fs_x__get_revnum(noderev->data_rep->id.change_set) == rev)
+ {
+ SVN_ERR_ASSERT(reps_to_cache && reps_pool);
+ APR_ARRAY_PUSH(reps_to_cache, svn_fs_x__representation_t *)
+ = svn_fs_x__rep_copy(noderev->data_rep, reps_pool);
+ }
+
+ if ( noderev->prop_rep
+ && svn_fs_x__get_revnum(noderev->prop_rep->id.change_set) == rev)
+ {
+ /* Add new property reps to hash and on-disk cache. */
+ svn_fs_x__representation_t *copy
+ = svn_fs_x__rep_copy(noderev->prop_rep, reps_pool);
+
+ SVN_ERR_ASSERT(reps_to_cache && reps_pool);
+ APR_ARRAY_PUSH(reps_to_cache, svn_fs_x__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;
+
+ /* Write out our new node-revision. */
+ if (at_root)
+ SVN_ERR(validate_root_noderev(fs, noderev, rev, scratch_pool));
+
+ file_stream = svn_checksum__wrap_write_stream_fnv1a_32x4(
+ &entry.fnv1_checksum,
+ svn_stream_from_aprfile2(file, TRUE,
+ scratch_pool),
+ scratch_pool);
+ SVN_ERR(svn_fs_x__write_noderev(file_stream, noderev, scratch_pool));
+ SVN_ERR(svn_stream_close(file_stream));
+
+ /* reference the root noderev from the log-to-phys index */
+ noderev_id = noderev->noderev_id;
+ noderev_id.change_set = SVN_FS_X__INVALID_CHANGE_SET;
+
+ entry.offset = my_offset;
+ SVN_ERR(svn_fs_x__get_file_offset(&my_offset, file, scratch_pool));
+ entry.size = my_offset - entry.offset;
+ entry.type = SVN_FS_X__ITEM_TYPE_NODEREV;
+ entry.item_count = 1;
+ entry.items = &noderev_id;
+
+ SVN_ERR(store_p2l_index_entry(fs, txn_id, &entry, scratch_pool));
+
+ /* Return our ID that references the revision file. */
+ *new_id_p = new_id;
+
+ return SVN_NO_ERROR;
+}
+
+/* Write the changed path info CHANGED_PATHS from transaction TXN_ID to the
+ permanent rev-file FILE representing NEW_REV in filesystem FS. *OFFSET_P
+ is set the to offset in the file of the beginning of this information.
+ NEW_REV is the revision currently being committed.
+ Perform temporary allocations in SCRATCH_POOL. */
+static svn_error_t *
+write_final_changed_path_info(apr_off_t *offset_p,
+ apr_file_t *file,
+ svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_hash_t *changed_paths,
+ svn_revnum_t new_rev,
+ apr_pool_t *scratch_pool)
+{
+ apr_off_t offset;
+ svn_stream_t *stream;
+ svn_fs_x__p2l_entry_t entry;
+ svn_fs_x__id_t rev_item
+ = {SVN_INVALID_REVNUM, SVN_FS_X__ITEM_INDEX_CHANGES};
+
+ SVN_ERR(svn_fs_x__get_file_offset(&offset, file, scratch_pool));
+
+ /* write to target file & calculate checksum */
+ stream = svn_checksum__wrap_write_stream_fnv1a_32x4(&entry.fnv1_checksum,
+ svn_stream_from_aprfile2(file, TRUE, scratch_pool),
+ scratch_pool);
+ SVN_ERR(svn_fs_x__write_changes(stream, fs, changed_paths, TRUE,
+ scratch_pool));
+ SVN_ERR(svn_stream_close(stream));
+
+ *offset_p = offset;
+
+ /* reference changes from the indexes */
+ entry.offset = offset;
+ SVN_ERR(svn_fs_x__get_file_offset(&offset, file, scratch_pool));
+ entry.size = offset - entry.offset;
+ entry.type = SVN_FS_X__ITEM_TYPE_CHANGES;
+ entry.item_count = 1;
+ entry.items = &rev_item;
+
+ SVN_ERR(store_p2l_index_entry(fs, txn_id, &entry, scratch_pool));
+ SVN_ERR(store_l2p_index_entry(fs, txn_id, entry.offset,
+ SVN_FS_X__ITEM_INDEX_CHANGES, scratch_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_x__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 *scratch_pool)
+{
+#ifdef SVN_DEBUG
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+ svn_fs_t *ft; /* fs++ == ft */
+ svn_fs_root_t *root;
+ svn_fs_x__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(scratch_pool);
+ svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_CACHE_NS,
+ svn_uuid_generate(scratch_pool));
+ SVN_ERR(ffd->svn_fs_open_(&ft, fs->path,
+ fs_config,
+ scratch_pool,
+ scratch_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_x__revision_root(&root, ft, new_rev, scratch_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_x__verify_root(root, scratch_pool));
+#endif /* SVN_DEBUG */
+
+ return SVN_NO_ERROR;
+}
+
+/* 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,
+ svn_fs_x__txn_id_t txn_id,
+ apr_hash_t *changed_paths,
+ apr_pool_t *scratch_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,
+ scratch_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(scratch_pool);
+ for (i = 0; i < changed_paths_sorted->nelts; i++)
+ {
+ const svn_sort__item_t *item;
+ const char *path;
+ svn_fs_x__change_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_x__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, scratch_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,
+ svn_fs_x__txn_id_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_x__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 = svn_fs_x__path_txn_props_final(txn->fs, txn_id, pool);
+ }
+ else
+ {
+ *path = svn_fs_x__path_txn_props(txn->fs, txn_id, pool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__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 *scratch_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, scratch_pool));
+ SVN_ERR(svn_fs_x__l2p_index_append(&l2p_checksum, fs, file,
+ l2p_proto_index, revision,
+ scratch_pool, scratch_pool));
+
+ p2l_offset = 0;
+ SVN_ERR(svn_io_file_seek(file, APR_END, &p2l_offset, scratch_pool));
+ SVN_ERR(svn_fs_x__p2l_index_append(&p2l_checksum, fs, file,
+ p2l_proto_index, revision,
+ scratch_pool, scratch_pool));
+
+ /* Append footer. */
+ footer = svn_fs_x__unparse_footer(l2p_offset, l2p_checksum,
+ p2l_offset, p2l_checksum, scratch_pool,
+ scratch_pool);
+ SVN_ERR(svn_io_file_write_full(file, footer->data, footer->len, NULL,
+ scratch_pool));
+
+ footer_length = footer->len;
+ SVN_ERR_ASSERT(footer_length == footer->len);
+ SVN_ERR(svn_io_file_write_full(file, &footer_length, 1, NULL,
+ scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Baton used for commit_body below. */
+typedef struct commit_baton_t {
+ 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;
+} commit_baton_t;
+
+/* The work-horse for svn_fs_x__commit, called with the FS write lock.
+ This implements the svn_fs_x__with_write_lock() 'body' callback
+ type. BATON is a 'commit_baton_t *'. */
+static svn_error_t *
+commit_body(void *baton,
+ apr_pool_t *scratch_pool)
+{
+ commit_baton_t *cb = baton;
+ svn_fs_x__data_t *ffd = cb->fs->fsap_data;
+ const char *old_rev_filename, *rev_filename, *proto_filename;
+ const char *revprop_filename, *final_revprop;
+ svn_fs_x__id_t root_id, new_root_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;
+ svn_fs_x__txn_id_t txn_id = svn_fs_x__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_x__read_format_file(cb->fs, scratch_pool));
+
+ /* Get the current youngest revision. */
+ SVN_ERR(svn_fs_x__youngest_rev(&old_rev, cb->fs, scratch_pool));
+
+ /* 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_x__txn_changes_fetch(&changed_paths, cb->fs, txn_id,
+ scratch_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, scratch_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, scratch_pool));
+ SVN_ERR(svn_fs_x__get_file_offset(&initial_offset, proto_file,
+ scratch_pool));
+
+ /* Write out all the node-revisions and directory contents. */
+ svn_fs_x__init_txn_root(&root_id, txn_id);
+ SVN_ERR(write_final_rev(&new_root_id, proto_file, new_rev, cb->fs, &root_id,
+ initial_offset, cb->reps_to_cache, cb->reps_hash,
+ cb->reps_pool, TRUE, scratch_pool));
+
+ /* Write the changed-path information. */
+ SVN_ERR(write_final_changed_path_info(&changed_path_offset, proto_file,
+ cb->fs, txn_id, changed_paths,
+ new_rev, scratch_pool));
+
+ /* Append the index data to the rev file. */
+ SVN_ERR(svn_fs_x__add_index_data(cb->fs, proto_file,
+ svn_fs_x__path_l2p_proto_index(cb->fs, txn_id, scratch_pool),
+ svn_fs_x__path_p2l_proto_index(cb->fs, txn_id, scratch_pool),
+ new_rev, scratch_pool));
+
+ SVN_ERR(svn_io_file_flush_to_disk(proto_file, scratch_pool));
+ SVN_ERR(svn_io_file_close(proto_file, scratch_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 (new_rev % ffd->max_files_per_dir == 0)
+ {
+ /* Create the revs shard. */
+ {
+ const char *new_dir
+ = svn_fs_x__path_rev_shard(cb->fs, new_rev, scratch_pool);
+ svn_error_t *err = svn_io_dir_make(new_dir, APR_OS_DEFAULT,
+ scratch_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,
+ scratch_pool),
+ new_dir, scratch_pool));
+ }
+
+ /* Create the revprops shard. */
+ SVN_ERR_ASSERT(! svn_fs_x__is_packed_revprop(cb->fs, new_rev));
+ {
+ const char *new_dir
+ = svn_fs_x__path_revprops_shard(cb->fs, new_rev, scratch_pool);
+ svn_error_t *err = svn_io_dir_make(new_dir, APR_OS_DEFAULT,
+ scratch_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,
+ scratch_pool),
+ new_dir, scratch_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_x__path_rev_absolute(cb->fs, old_rev,
+ scratch_pool);
+
+ rev_filename = svn_fs_x__path_rev(cb->fs, new_rev, scratch_pool);
+ proto_filename = svn_fs_x__path_txn_proto_rev(cb->fs, txn_id,
+ scratch_pool);
+ SVN_ERR(svn_fs_x__move_into_place(proto_filename, rev_filename,
+ old_rev_filename, scratch_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,
+ scratch_pool));
+
+ /* Move the revprops file into place. */
+ SVN_ERR_ASSERT(! svn_fs_x__is_packed_revprop(cb->fs, new_rev));
+ SVN_ERR(write_final_revprop(&revprop_filename, cb->txn, txn_id,
+ scratch_pool));
+ final_revprop = svn_fs_x__path_revprops(cb->fs, new_rev, scratch_pool);
+ SVN_ERR(svn_fs_x__move_into_place(revprop_filename, final_revprop,
+ old_rev_filename, scratch_pool));
+
+ /* Update the 'current' file. */
+ SVN_ERR(verify_as_revision_before_current_plus_plus(cb->fs, new_rev,
+ scratch_pool));
+ SVN_ERR(svn_fs_x__write_current(cb->fs, new_rev, scratch_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_x__purge_txn(cb->fs, cb->txn->id, scratch_pool));
+
+ return SVN_NO_ERROR;
+}
+
+/* Add the representations in REPS_TO_CACHE (an array of
+ * svn_fs_x__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++)
+ {
+ svn_fs_x__representation_t *rep
+ = APR_ARRAY_IDX(reps_to_cache, i, svn_fs_x__representation_t *);
+
+ /* FALSE because we don't care if another parallel commit happened to
+ * collide with us. (Non-parallel collisions will not be detected.) */
+ SVN_ERR(svn_fs_x__set_rep_reference(fs, rep, scratch_pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__commit(svn_revnum_t *new_rev_p,
+ svn_fs_t *fs,
+ svn_fs_txn_t *txn,
+ apr_pool_t *scratch_pool)
+{
+ commit_baton_t cb;
+ svn_fs_x__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(scratch_pool, 5,
+ sizeof(svn_fs_x__representation_t *));
+ cb.reps_hash = apr_hash_make(scratch_pool);
+ cb.reps_pool = scratch_pool;
+ }
+ else
+ {
+ cb.reps_to_cache = NULL;
+ cb.reps_hash = NULL;
+ cb.reps_pool = NULL;
+ }
+
+ SVN_ERR(svn_fs_x__with_write_lock(fs, commit_body, &cb, scratch_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_ERR(svn_fs_x__open_rep_cache(fs, scratch_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_SQLITE__WITH_TXN(
+ write_reps_to_cache(fs, cb.reps_to_cache, scratch_pool),
+ ffd->rep_cache_db);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_fs_x__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_x__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_x__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;
+ svn_fs_x__transaction_t *local_txn;
+ svn_fs_x__txn_id_t txn_id;
+
+ SVN_ERR(svn_fs_x__txn_by_name(&txn_id, name));
+
+ /* First check to see if the directory exists. */
+ SVN_ERR(svn_io_check_path(svn_fs_x__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_x__get_txn(&local_txn, fs, txn_id, pool));
+
+ txn->base_rev = local_txn->base_rev;
+
+ txn->vtable = &txn_vtable;
+ txn->fsap_data = ftd;
+ *txn_p = txn;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__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_x__txn_get_id(txn),
+ pool));
+ *table_p = proplist;
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__delete_node_revision(svn_fs_t *fs,
+ const svn_fs_x__id_t *id,
+ apr_pool_t *scratch_pool)
+{
+ svn_fs_x__noderev_t *noderev;
+ SVN_ERR(svn_fs_x__get_node_revision(&noderev, fs, id, scratch_pool,
+ scratch_pool));
+
+ /* Delete any mutable property representation. */
+ if (noderev->prop_rep
+ && svn_fs_x__is_txn(noderev->prop_rep->id.change_set))
+ SVN_ERR(svn_io_remove_file2(svn_fs_x__path_txn_node_props(fs, id,
+ scratch_pool,
+ scratch_pool),
+ FALSE, scratch_pool));
+
+ /* Delete any mutable data representation. */
+ if (noderev->data_rep
+ && svn_fs_x__is_txn(noderev->data_rep->id.change_set)
+ && noderev->kind == svn_node_dir)
+ {
+ svn_fs_x__data_t *ffd = fs->fsap_data;
+ const svn_fs_x__id_t *key = id;
+
+ SVN_ERR(svn_io_remove_file2(
+ svn_fs_x__path_txn_node_children(fs, id, scratch_pool,
+ scratch_pool),
+ FALSE, scratch_pool));
+
+ /* remove the corresponding entry from the cache, if such exists */
+ SVN_ERR(svn_cache__set(ffd->dir_cache, key, NULL, scratch_pool));
+ }
+
+ return svn_io_remove_file2(svn_fs_x__path_txn_node_rev(fs, id,
+ scratch_pool,
+ scratch_pool),
+ FALSE, scratch_pool);
+}
+
+
+
+/*** Transactions ***/
+
+svn_error_t *
+svn_fs_x__get_base_rev(svn_revnum_t *revnum,
+ svn_fs_t *fs,
+ svn_fs_x__txn_id_t txn_id,
+ apr_pool_t *scratch_pool)
+{
+ svn_fs_x__transaction_t *txn;
+ SVN_ERR(svn_fs_x__get_txn(&txn, fs, txn_id, scratch_pool));
+ *revnum = txn->base_rev;
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Generic transaction operations. */
+
+svn_error_t *
+svn_fs_x__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_x__txn_proplist(&table, txn, pool));
+
+ *value_p = svn_hash_gets(table, propname);
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_fs_x__begin_txn(svn_fs_txn_t **txn_p,
+ svn_fs_t *fs,
+ svn_revnum_t rev,
+ apr_uint32_t flags,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ svn_string_t date;
+ fs_txn_data_t *ftd;
+ apr_hash_t *props = apr_hash_make(scratch_pool);
+
+ SVN_ERR(svn_fs__check_fs(fs, TRUE));
+
+ SVN_ERR(create_txn(txn_p, fs, rev, result_pool, scratch_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(), scratch_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", scratch_pool));
+
+ if (flags & SVN_FS_TXN_CHECK_LOCKS)
+ svn_hash_sets(props, SVN_FS__PROP_TXN_CHECK_LOCKS,
+ svn_string_create("true", scratch_pool));
+
+ if (flags & SVN_FS_TXN_CLIENT_DATE)
+ svn_hash_sets(props, SVN_FS__PROP_TXN_CLIENT_DATE,
+ svn_string_create("0", scratch_pool));
+
+ ftd = (*txn_p)->fsap_data;
+ SVN_ERR(set_txn_proplist(fs, ftd->txn_id, props, FALSE, scratch_pool));
+
+ return SVN_NO_ERROR;
+}