Return-Path: X-Original-To: apmail-subversion-commits-archive@minotaur.apache.org Delivered-To: apmail-subversion-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 114897F00 for ; Thu, 15 Dec 2011 11:03:37 +0000 (UTC) Received: (qmail 28980 invoked by uid 500); 15 Dec 2011 11:03:37 -0000 Delivered-To: apmail-subversion-commits-archive@subversion.apache.org Received: (qmail 28958 invoked by uid 500); 15 Dec 2011 11:03:36 -0000 Mailing-List: contact commits-help@subversion.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@subversion.apache.org Delivered-To: mailing list commits@subversion.apache.org Received: (qmail 28951 invoked by uid 99); 15 Dec 2011 11:03:36 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 15 Dec 2011 11:03:36 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=5.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 15 Dec 2011 11:03:30 +0000 Received: from eris.apache.org (localhost [127.0.0.1]) by eris.apache.org (Postfix) with ESMTP id 69CFB23888D2 for ; Thu, 15 Dec 2011 11:03:09 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r1214697 - in /subversion/trunk/subversion: include/ libsvn_fs/ libsvn_fs_base/ libsvn_fs_fs/ libsvn_repos/ svnadmin/ tests/cmdline/ Date: Thu, 15 Dec 2011 11:03:08 -0000 To: commits@subversion.apache.org From: stsp@apache.org X-Mailer: svnmailer-1.0.8-patched Message-Id: <20111215110309.69CFB23888D2@eris.apache.org> Author: stsp Date: Thu Dec 15 11:03:08 2011 New Revision: 1214697 URL: http://svn.apache.org/viewvc?rev=1214697&view=rev Log: New and improved implementation of 'hotcopy' for FSFS. This is based on the old implementation, adds support for cancellation, supports an incremental hotcopy mode (addresses issue #3815, "incremental hotcopy, fsfs-only"), has better input parameter verification, and always uses absolute paths internally (if called from the repos layer). The incremental mode is invoked via "svnadmin hotcopy --incremental". In incremental mode, only files which are new or have changed in type (e.g. file->symlink), size, or mtime since the last hotcopy are copied to the destination. This mode supports writing to existing hotcopies of the repository. Before copying it verifies that the UUID of the destination matches the UUID of the source, that the destination has no newer revisions than the source, and that the filesystem format and configuration of the source and destination are equal. It also removes stale locks from the destination (by removing all locks from the destination and then copying all locks from source to destination -- this is racy and may need to be revisited). Performance improvements of incremental mode seem to be quite significant. Copying the ASF repository (a copy from r0 to r421895, about 15GB in size), using the original hotcopy implementation, took about 150 minutes (source and destination residing on the same disk volume of a software RAID1 configuration on an OpenBSD FFS2 filesystem with softupdates disabled). A subsequent incremental copy that doesn't copy any changed files from source to destination completes in about 2 minutes. Behaviour of the non-incremental mode is affected slightly. It still refuses to copy to an existing destination. However, readers can peek at the destination filesystem while the hotcopy is in progress and see newer revisions appearing as the hotcopy progresses. This change was made to avoid having two separate implementations of hotcopy in FSFS, one for incremental and one for non-incremental mode. A write lock is now always held on the destination while a hotcopy is in progress. In the old implementation it was possible for a client to commit to the destination while a hotcopy was in progress, racing the hotcopy operation. The BDB hotcopy implementation is not affected, except that it throws an error if incremental hotcopy mode is requested. Cancellation support is only added at the API level of the BDB implementation in this commit, but is not actually implemented. With lots of suggestions from danielsh, thanks. * subversion/svnadmin/main.c (options_table): Adjust description of --incremental option. (cmd_table): Update help text of 'svnadmin hotcopy'. (subcommand_hotcopy): Call svn_repos_hotcopy2(). * subversion/include/svn_fs.h (svn_fs_hotcopy2): Declare. * subversion/include/svn_repos.h (svn_repos_hotcopy2): Declare. * subversion/libsvn_fs_fs/fs.c (fs_hotcopy): Adjust for new vtable 'hotcopy' prototype, adding source and destination filesystem parameters (instead of just their paths), the new 'incremental' parameter, and a cancellation function. * subversion/libsvn_fs_fs/fs_fs.c (svn_fs_fs__hotcopy): Moved down to the near the bottom of this file and mostly reimplemented. (hotcopy_io_dir_file_copy, entry_name_to_utf8, hotcopy_io_copy_dir_recursively): New helpers. Based on similar functions in libsvn_subr/io.c. (hotcopy_copy_shard_file, hotcopy_copy_packed_shard, hotcopy_update_current, hotcopy_remove_rev_files, hotcopy_incremental_check_preconditions, hotcopy_create_empty_dest): New helpers. (hotcopy_body_baton, hotcopy_body): New. This function contains those parts of the hotcopy code which run with the FSFS write-lock held on the destination filesystem. * subversion/libsvn_fs_fs/fs_fs.h (svn_fs_fs__hotcopy): Update declaration, adding parameters for source and destination filesystem, a boolean to enabled incremental mode, and cancellation support. * subversion/libsvn_fs/fs-loader.h (fs_library_vtable_t): Update declaration of 'hotcopy' as in previous. * subversion/libsvn_fs/fs-loader.c (svn_fs_hotcopy2): New. Adds support for incremental mode and cancellation. * subversion/tests/cmdline/svnadmin_tests.py (hotcopy_incremental, test_list): New, fairly simple, test. The hotcopy tests are currently few in number. None of the existing and new tests verify the complete result of the hotcopy but run 'svnadmin dump' on the destination after the hotcopy operation. This doesn't verify if all files in the repository were copied correctly (rings a bell wrt r905303). These deficiencies in testing will be addressed in a later commit. * subversion/libsvn_fs_base/fs.c (base_hotcopy): Change signature in accordance with API changes made above. But don't change the implementation. Refuse to operate in incremental mode. * subversion/libsvn_repos/repos.c (hotcopy_ctx_t): Add new parameters for incremental mode and cancellation. (hotcopy_structure): Add cancellation support. Tolerate existing destination directory in incremental hotcopy mode. (svn_repos_hotcopy2): Add support for cancellation and incremental mode. Convert input source and destionation paths to absolute paths. Verify that source and destination parameters are different directories. Apart from the general hilarity of copying things onto themselves this avoids a dead-lock when trying to hotcopy the rep-cache sqlite db onto itself. Tolerate some existing directories in incremental mode. (svn_repos_hotcopy): Reimplement as wrapper around svn_repos_hotcopy2(). Modified: subversion/trunk/subversion/include/svn_fs.h subversion/trunk/subversion/include/svn_repos.h subversion/trunk/subversion/libsvn_fs/fs-loader.c subversion/trunk/subversion/libsvn_fs/fs-loader.h subversion/trunk/subversion/libsvn_fs_base/fs.c subversion/trunk/subversion/libsvn_fs_fs/fs.c subversion/trunk/subversion/libsvn_fs_fs/fs_fs.c subversion/trunk/subversion/libsvn_fs_fs/fs_fs.h subversion/trunk/subversion/libsvn_repos/repos.c subversion/trunk/subversion/svnadmin/main.c subversion/trunk/subversion/tests/cmdline/svnadmin_tests.py Modified: subversion/trunk/subversion/include/svn_fs.h URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/include/svn_fs.h?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/include/svn_fs.h (original) +++ subversion/trunk/subversion/include/svn_fs.h Thu Dec 15 11:03:08 2011 @@ -309,6 +309,28 @@ svn_fs_delete_fs(const char *path, * means deleting copied, unused logfiles for a Berkeley DB source * filesystem. * + * If @a incremental is TRUE, make an effort to not re-copy information + * already present in the destination. If incremental hotcopy is not + * implemented, raise SVN_ERR_UNSUPPORTED_FEATURE. + * + * Use @a scratch_pool for temporary allocations. + * + * @since New in 1.8. + */ +svn_error_t * +svn_fs_hotcopy2(const char *src_path, + const char *dest_path, + svn_boolean_t clean, + svn_boolean_t incremental, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *scratch_pool); + +/** + * Like svn_fs_hotcopy2(), but without the @a incremental parameter + * and without cancellation support. + * + * @deprecated Provided for backward compatibility with the 1.7 API. * @since New in 1.1. */ svn_error_t * Modified: subversion/trunk/subversion/include/svn_repos.h URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/include/svn_repos.h?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/include/svn_repos.h (original) +++ subversion/trunk/subversion/include/svn_repos.h Thu Dec 15 11:03:08 2011 @@ -516,6 +516,27 @@ svn_repos_fs(svn_repos_t *repos); * source filesystem as part of the copy operation; currently, this * means deleting copied, unused logfiles for a Berkeley DB source * repository. + * + * If @a incremental is TRUE, make an effort to not re-copy information + * already present in the destination. If incremental hotcopy is not + * implemented by the filesystem backend, raise SVN_ERR_UNSUPPORTED_FEATURE. + * + * @since New in 1.8. + */ +svn_error_t * +svn_repos_hotcopy2(const char *src_path, + const char *dst_path, + svn_boolean_t clean_logs, + svn_boolean_t incremental, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool); + +/** + * Like svn_repos_hotcopy2(), but without the @a incremental parameter + * and without cancellation support. + * + * @deprecated Provided for backward compatibility with the 1.6 API. */ svn_error_t * svn_repos_hotcopy(const char *src_path, Modified: subversion/trunk/subversion/libsvn_fs/fs-loader.c URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_fs/fs-loader.c?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/libsvn_fs/fs-loader.c (original) +++ subversion/trunk/subversion/libsvn_fs/fs-loader.c Thu Dec 15 11:03:08 2011 @@ -424,16 +424,72 @@ svn_fs_delete_fs(const char *path, apr_p } svn_error_t * -svn_fs_hotcopy(const char *src_path, const char *dest_path, - svn_boolean_t clean, apr_pool_t *pool) +svn_fs_hotcopy2(const char *src_path, const char *dst_path, + svn_boolean_t clean, svn_boolean_t incremental, + svn_cancel_func_t cancel_func, void *cancel_baton, + apr_pool_t *scratch_pool) { fs_library_vtable_t *vtable; - const char *fs_type; + const char *src_fs_type; + svn_fs_t *src_fs; + svn_fs_t *dst_fs; + const char *dst_fs_type; + svn_node_kind_t dst_kind; + + if (strcmp(src_path, dst_path) == 0) + return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, + _("Hotcopy source and destination are equal")); + + SVN_ERR(svn_fs_type(&src_fs_type, src_path, scratch_pool)); + SVN_ERR(get_library_vtable(&vtable, src_fs_type, scratch_pool)); + src_fs = fs_new(NULL, scratch_pool); + dst_fs = fs_new(NULL, scratch_pool); + + SVN_ERR(svn_io_check_path(dst_path, &dst_kind, scratch_pool)); + if (dst_kind == svn_node_file) + return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, + _("'%s' already exists and is a file"), + svn_dirent_local_style(dst_path, + scratch_pool)); + if (dst_kind == svn_node_unknown) + return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, + _("'%s' already exists and has an unknown " + "node kind"), + svn_dirent_local_style(dst_path, + scratch_pool)); + if (dst_kind == svn_node_dir) + { + svn_node_kind_t type_file_kind; + + SVN_ERR(svn_io_check_path(svn_dirent_join(dst_path, + FS_TYPE_FILENAME, + scratch_pool), + &type_file_kind, scratch_pool)); + if (type_file_kind != svn_node_none) + { + SVN_ERR(svn_fs_type(&dst_fs_type, dst_path, scratch_pool)); + if (strcmp(src_fs_type, dst_fs_type) != 0) + return svn_error_createf( + SVN_ERR_ILLEGAL_TARGET, NULL, + _("The filesystem type of the hotcopy source " + "('%s') does not match the filesystem " + "type of the hotcopy destination ('%s')"), + src_fs_type, dst_fs_type); + } + } - SVN_ERR(svn_fs_type(&fs_type, src_path, pool)); - SVN_ERR(get_library_vtable(&vtable, fs_type, pool)); - SVN_ERR(vtable->hotcopy(src_path, dest_path, clean, pool)); - return svn_error_trace(write_fs_type(dest_path, fs_type, pool)); + SVN_ERR(vtable->hotcopy(src_fs, dst_fs, src_path, dst_path, clean, + incremental, cancel_func, cancel_baton, + scratch_pool)); + return svn_error_trace(write_fs_type(dst_path, src_fs_type, scratch_pool)); +} + +svn_error_t * +svn_fs_hotcopy(const char *src_path, const char *dest_path, + svn_boolean_t clean, apr_pool_t *pool) +{ + return svn_error_trace(svn_fs_hotcopy2(src_path, dest_path, clean, + FALSE, NULL, NULL, pool)); } svn_error_t * Modified: subversion/trunk/subversion/libsvn_fs/fs-loader.h URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_fs/fs-loader.h?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/libsvn_fs/fs-loader.h (original) +++ subversion/trunk/subversion/libsvn_fs/fs-loader.h Thu Dec 15 11:03:08 2011 @@ -93,8 +93,11 @@ typedef struct fs_library_vtable_t apr_pool_t *pool, apr_pool_t *common_pool); svn_error_t *(*delete_fs)(const char *path, apr_pool_t *pool); - svn_error_t *(*hotcopy)(const char *src_path, const char *dest_path, - svn_boolean_t clean, apr_pool_t *pool); + svn_error_t *(*hotcopy)(svn_fs_t *src_fs, svn_fs_t *dst_fs, + const char *src_path, const char *dst_path, + svn_boolean_t clean, svn_boolean_t incremental, + svn_cancel_func_t cancel_func, void *cancel_baton, + apr_pool_t *pool); const char *(*get_description)(void); svn_error_t *(*recover)(svn_fs_t *fs, svn_cancel_func_t cancel_func, void *cancel_baton, Modified: subversion/trunk/subversion/libsvn_fs_base/fs.c URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_fs_base/fs.c?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/libsvn_fs_base/fs.c (original) +++ subversion/trunk/subversion/libsvn_fs_base/fs.c Thu Dec 15 11:03:08 2011 @@ -1165,9 +1165,14 @@ copy_db_file_safely(const char *src_dir, static svn_error_t * -base_hotcopy(const char *src_path, +base_hotcopy(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + const char *src_path, const char *dest_path, svn_boolean_t clean_logs, + svn_boolean_t incremental, + svn_cancel_func_t cancel_func, + void *cancel_baton, apr_pool_t *pool) { svn_error_t *err; @@ -1175,6 +1180,11 @@ base_hotcopy(const char *src_path, svn_boolean_t log_autoremove = FALSE; int format; + if (incremental) + return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("BDB repositories do not support incremental " + "hotcopy")); + /* Check the FS format number to be certain that we know how to hotcopy this FS. Pre-1.2 filesystems did not have a format file (you could say they were format "0"), so we will error here. This is not Modified: subversion/trunk/subversion/libsvn_fs_fs/fs.c URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_fs_fs/fs.c?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/libsvn_fs_fs/fs.c (original) +++ subversion/trunk/subversion/libsvn_fs_fs/fs.c Thu Dec 15 11:03:08 2011 @@ -276,16 +276,28 @@ fs_pack(svn_fs_t *fs, /* This implements the fs_library_vtable_t.hotcopy() API. Copy a - possibly live Subversion filesystem from SRC_PATH to DEST_PATH. + possibly live Subversion filesystem SRC_FS from SRC_PATH to a + DST_FS at DEST_PATH. If INCREMENTAL is TRUE, make an effort not to + re-copy data which already exists in DST_FS. The CLEAN_LOGS argument is ignored and included for Subversion 1.0.x compatibility. Perform all temporary allocations in POOL. */ static svn_error_t * -fs_hotcopy(const char *src_path, - const char *dest_path, +fs_hotcopy(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + const char *src_path, + const char *dst_path, svn_boolean_t clean_logs, + svn_boolean_t incremental, + svn_cancel_func_t cancel_func, + void *cancel_baton, apr_pool_t *pool) { - return svn_fs_fs__hotcopy(src_path, dest_path, pool); + SVN_ERR(initialize_fs_struct(src_fs)); + SVN_ERR(fs_serialized_init(src_fs, pool, pool)); + SVN_ERR(initialize_fs_struct(dst_fs)); + SVN_ERR(fs_serialized_init(dst_fs, pool, pool)); + return svn_fs_fs__hotcopy(src_fs, dst_fs, src_path, dst_path, + incremental, cancel_func, cancel_baton, pool); } Modified: subversion/trunk/subversion/libsvn_fs_fs/fs_fs.c URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_fs_fs/fs_fs.c?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/libsvn_fs_fs/fs_fs.c (original) +++ subversion/trunk/subversion/libsvn_fs_fs/fs_fs.c Thu Dec 15 11:03:08 2011 @@ -1421,238 +1421,6 @@ get_youngest(svn_revnum_t *youngest_p, return SVN_NO_ERROR; } -svn_error_t * -svn_fs_fs__hotcopy(const char *src_path, - const char *dst_path, - apr_pool_t *pool) -{ - const char *src_subdir, *dst_subdir; - svn_revnum_t youngest, rev, min_unpacked_rev; - apr_pool_t *iterpool; - svn_node_kind_t kind; - int format, max_files_per_dir; - - /* Check format to be sure we know how to hotcopy this FS. */ - SVN_ERR(read_format(&format, &max_files_per_dir, - svn_dirent_join(src_path, PATH_FORMAT, pool), - pool)); - SVN_ERR(check_format(format)); - - /* Try to copy the config. - * - * ### We try copying the config file before doing anything else, - * ### because higher layers will abort the hotcopy if we throw - * ### an error from this function, and that renders the hotcopy - * ### unusable anyway. */ - if (format >= SVN_FS_FS__MIN_CONFIG_FILE) - { - svn_error_t *err; - - err = svn_io_dir_file_copy(src_path, dst_path, PATH_CONFIG, pool); - if (err) - { - if (APR_STATUS_IS_ENOENT(err->apr_err)) - { - /* 1.6.0 to 1.6.11 did not copy the configuration file during - * hotcopy. So if we're hotcopying a repository which has been - * created as a hotcopy itself, it's possible that fsfs.conf - * does not exist. Ask the user to re-create it. - * - * ### It would be nice to make this a non-fatal error, - * ### but this function does not get an svn_fs_t object - * ### so we have no way of just printing a warning via - * ### the fs->warning() callback. */ - - const char *msg; - const char *src_abspath; - const char *dst_abspath; - const char *config_relpath; - svn_error_t *err2; - - config_relpath = svn_dirent_join(src_path, PATH_CONFIG, pool); - err2 = svn_dirent_get_absolute(&src_abspath, src_path, pool); - if (err2) - return svn_error_trace(svn_error_compose_create(err, err2)); - err2 = svn_dirent_get_absolute(&dst_abspath, dst_path, pool); - if (err2) - return svn_error_trace(svn_error_compose_create(err, err2)); - - /* ### hack: strip off the 'db/' directory from paths so - * ### they make sense to the user */ - src_abspath = svn_dirent_dirname(src_abspath, pool); - dst_abspath = svn_dirent_dirname(dst_abspath, pool); - - msg = apr_psprintf(pool, - _("Failed to create hotcopy at '%s'. " - "The file '%s' is missing from the source " - "repository. Please create this file, for " - "instance by running 'svnadmin upgrade %s'"), - dst_abspath, config_relpath, src_abspath); - return svn_error_quick_wrap(err, msg); - } - else - return svn_error_trace(err); - } - } - - /* Copy the 'current' file. */ - SVN_ERR(svn_io_dir_file_copy(src_path, dst_path, PATH_CURRENT, pool)); - - /* Copy the uuid. */ - SVN_ERR(svn_io_dir_file_copy(src_path, dst_path, PATH_UUID, pool)); - - /* Copy the rep cache before copying the rev files to make sure all - cached references will be present in the copy. */ - src_subdir = svn_dirent_join(src_path, REP_CACHE_DB_NAME, pool); - dst_subdir = svn_dirent_join(dst_path, REP_CACHE_DB_NAME, pool); - SVN_ERR(svn_io_check_path(src_subdir, &kind, pool)); - if (kind == svn_node_file) - SVN_ERR(svn_sqlite__hotcopy(src_subdir, dst_subdir, pool)); - - /* Copy the min unpacked rev, and read its value. */ - if (format >= SVN_FS_FS__MIN_PACKED_FORMAT) - { - const char *min_unpacked_rev_path; - min_unpacked_rev_path = svn_dirent_join(src_path, PATH_MIN_UNPACKED_REV, - pool); - - SVN_ERR(svn_io_dir_file_copy(src_path, dst_path, PATH_MIN_UNPACKED_REV, - pool)); - SVN_ERR(read_min_unpacked_rev(&min_unpacked_rev, min_unpacked_rev_path, - pool)); - } - else - { - min_unpacked_rev = 0; - } - - /* Find the youngest revision from this 'current' file. */ - SVN_ERR(get_youngest(&youngest, dst_path, pool)); - - /* Copy the necessary rev files. */ - src_subdir = svn_dirent_join(src_path, PATH_REVS_DIR, pool); - dst_subdir = svn_dirent_join(dst_path, PATH_REVS_DIR, pool); - - SVN_ERR(svn_io_make_dir_recursively(dst_subdir, pool)); - - iterpool = svn_pool_create(pool); - /* First, copy packed shards. */ - for (rev = 0; rev < min_unpacked_rev; rev += max_files_per_dir) - { - const char *packed_shard = apr_psprintf(iterpool, "%ld.pack", - rev / max_files_per_dir); - const char *src_subdir_packed_shard; - src_subdir_packed_shard = svn_dirent_join(src_subdir, packed_shard, - iterpool); - - SVN_ERR(svn_io_copy_dir_recursively(src_subdir_packed_shard, - dst_subdir, packed_shard, - TRUE /* copy_perms */, - NULL /* cancel_func */, NULL, - iterpool)); - svn_pool_clear(iterpool); - } - - /* Then, copy non-packed shards. */ - SVN_ERR_ASSERT(rev == min_unpacked_rev); - for (; rev <= youngest; rev++) - { - const char *src_subdir_shard = src_subdir, - *dst_subdir_shard = dst_subdir; - - if (max_files_per_dir) - { - const char *shard = apr_psprintf(iterpool, "%ld", - rev / max_files_per_dir); - src_subdir_shard = svn_dirent_join(src_subdir, shard, iterpool); - dst_subdir_shard = svn_dirent_join(dst_subdir, shard, iterpool); - - if (rev % max_files_per_dir == 0) - { - SVN_ERR(svn_io_dir_make(dst_subdir_shard, APR_OS_DEFAULT, - iterpool)); - SVN_ERR(svn_io_copy_perms(dst_subdir, dst_subdir_shard, - iterpool)); - } - } - - SVN_ERR(svn_io_dir_file_copy(src_subdir_shard, dst_subdir_shard, - apr_psprintf(iterpool, "%ld", rev), - iterpool)); - svn_pool_clear(iterpool); - } - - /* Copy the necessary revprop files. */ - src_subdir = svn_dirent_join(src_path, PATH_REVPROPS_DIR, pool); - dst_subdir = svn_dirent_join(dst_path, PATH_REVPROPS_DIR, pool); - - SVN_ERR(svn_io_make_dir_recursively(dst_subdir, pool)); - - for (rev = 0; rev <= youngest; rev++) - { - const char *src_subdir_shard = src_subdir, - *dst_subdir_shard = dst_subdir; - - svn_pool_clear(iterpool); - - if (max_files_per_dir) - { - const char *shard = apr_psprintf(iterpool, "%ld", - rev / max_files_per_dir); - src_subdir_shard = svn_dirent_join(src_subdir, shard, iterpool); - dst_subdir_shard = svn_dirent_join(dst_subdir, shard, iterpool); - - if (rev % max_files_per_dir == 0) - { - SVN_ERR(svn_io_dir_make(dst_subdir_shard, APR_OS_DEFAULT, - iterpool)); - SVN_ERR(svn_io_copy_perms(dst_subdir, dst_subdir_shard, - iterpool)); - } - } - - SVN_ERR(svn_io_dir_file_copy(src_subdir_shard, dst_subdir_shard, - apr_psprintf(iterpool, "%ld", rev), - iterpool)); - } - - svn_pool_destroy(iterpool); - - /* Make an empty transactions directory for now. Eventually some - method of copying in progress transactions will need to be - developed.*/ - dst_subdir = svn_dirent_join(dst_path, PATH_TXNS_DIR, pool); - SVN_ERR(svn_io_make_dir_recursively(dst_subdir, pool)); - if (format >= SVN_FS_FS__MIN_PROTOREVS_DIR_FORMAT) - { - dst_subdir = svn_dirent_join(dst_path, PATH_TXN_PROTOS_DIR, pool); - SVN_ERR(svn_io_make_dir_recursively(dst_subdir, pool)); - } - - /* Now copy the locks tree. */ - src_subdir = svn_dirent_join(src_path, PATH_LOCKS_DIR, pool); - SVN_ERR(svn_io_check_path(src_subdir, &kind, pool)); - if (kind == svn_node_dir) - SVN_ERR(svn_io_copy_dir_recursively(src_subdir, dst_path, - PATH_LOCKS_DIR, TRUE, NULL, - NULL, pool)); - - /* Now copy the node-origins cache tree. */ - src_subdir = svn_dirent_join(src_path, PATH_NODE_ORIGINS_DIR, pool); - SVN_ERR(svn_io_check_path(src_subdir, &kind, pool)); - if (kind == svn_node_dir) - SVN_ERR(svn_io_copy_dir_recursively(src_subdir, dst_path, - PATH_NODE_ORIGINS_DIR, TRUE, NULL, - NULL, pool)); - - /* Copy the txn-current file. */ - if (format >= SVN_FS_FS__MIN_TXN_CURRENT_FORMAT) - SVN_ERR(svn_io_dir_file_copy(src_path, dst_path, PATH_TXN_CURRENT, pool)); - - /* Hotcopied FS is complete. Stamp it with a format file. */ - return write_format(svn_dirent_join(dst_path, PATH_FORMAT, pool), - format, max_files_per_dir, FALSE, pool); -} svn_error_t * svn_fs_fs__youngest_rev(svn_revnum_t *youngest_p, @@ -7845,3 +7613,933 @@ svn_fs_fs__verify(svn_fs_t *fs, return SVN_NO_ERROR; } + + + +/** Hotcopy. **/ + +/* Like svn_io_dir_file_copy(), but doesn't copy files that exist at + * the destination and do not differ in terms of kind, size, and mtime. */ +static svn_error_t * +hotcopy_io_dir_file_copy(const char *src_path, + const char *dst_path, + const char *file, + apr_pool_t *scratch_pool) +{ + const svn_io_dirent2_t *src_dirent; + const svn_io_dirent2_t *dst_dirent; + const char *src_target; + const char *dst_target; + + /* Does the destination already exist? If not, we must copy it. */ + dst_target = svn_dirent_join(dst_path, file, scratch_pool); + SVN_ERR(svn_io_stat_dirent(&dst_dirent, dst_target, TRUE, + scratch_pool, scratch_pool)); + if (dst_dirent->kind != svn_node_none) + { + /* If the destination's stat information indicates that the file + * is equal to the source, don't bother copying the file again. */ + src_target = svn_dirent_join(src_path, file, scratch_pool); + SVN_ERR(svn_io_stat_dirent(&src_dirent, src_target, FALSE, + scratch_pool, scratch_pool)); + if (src_dirent->kind == dst_dirent->kind && + src_dirent->special == dst_dirent->special && + src_dirent->filesize == dst_dirent->filesize && + src_dirent->mtime <= dst_dirent->mtime) + return SVN_NO_ERROR; + } + + return svn_error_trace(svn_io_dir_file_copy(src_path, dst_path, file, + scratch_pool)); +} + +/* Set *NAME_P to the UTF-8 representation of directory entry NAME. + * NAME is in the the internal encoding used by APR; PARENT is in + * UTF-8 and in internal (not local) style. + * + * Use PARENT only for generating an error string if the conversion + * fails because NAME could not be represented in UTF-8. In that + * case, return a two-level error in which the outer error's message + * mentions PARENT, but the inner error's message does not mention + * NAME (except possibly in hex) since NAME may not be printable. + * Such a compound error at least allows the user to go looking in the + * right directory for the problem. + * + * If there is any other error, just return that error directly. + * + * If there is any error, the effect on *NAME_P is undefined. + * + * *NAME_P and NAME may refer to the same storage. + */ +static svn_error_t * +entry_name_to_utf8(const char **name_p, + const char *name, + const char *parent, + apr_pool_t *pool) +{ + svn_error_t *err = svn_path_cstring_to_utf8(name_p, name, pool); + if (err && err->apr_err == APR_EINVAL) + { + return svn_error_createf(err->apr_err, err, + _("Error converting entry " + "in directory '%s' to UTF-8"), + svn_dirent_local_style(parent, pool)); + } + return err; +} + +/* Like svn_io_copy_dir_recursively() but doesn't copy regular files that + * exist in the destination and do not differ from the source in terms of + * kind, size, and mtime. */ +static svn_error_t * +hotcopy_io_copy_dir_recursively(const char *src, + const char *dst_parent, + const char *dst_basename, + svn_boolean_t copy_perms, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + svn_node_kind_t kind; + apr_status_t status; + const char *dst_path; + apr_dir_t *this_dir; + apr_finfo_t this_entry; + apr_int32_t flags = APR_FINFO_TYPE | APR_FINFO_NAME; + + /* Make a subpool for recursion */ + apr_pool_t *subpool = svn_pool_create(pool); + + /* The 'dst_path' is simply dst_parent/dst_basename */ + dst_path = svn_dirent_join(dst_parent, dst_basename, pool); + + /* Sanity checks: SRC and DST_PARENT are directories, and + DST_BASENAME doesn't already exist in DST_PARENT. */ + SVN_ERR(svn_io_check_path(src, &kind, subpool)); + if (kind != svn_node_dir) + return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, + _("Source '%s' is not a directory"), + svn_dirent_local_style(src, pool)); + + SVN_ERR(svn_io_check_path(dst_parent, &kind, subpool)); + if (kind != svn_node_dir) + return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, + _("Destination '%s' is not a directory"), + svn_dirent_local_style(dst_parent, pool)); + + SVN_ERR(svn_io_check_path(dst_path, &kind, subpool)); + if (kind != svn_node_none) + return svn_error_createf(SVN_ERR_ENTRY_EXISTS, NULL, + _("Destination '%s' already exists"), + svn_dirent_local_style(dst_path, pool)); + + /* Create the new directory. */ + /* ### TODO: copy permissions (needs apr_file_attrs_get()) */ + SVN_ERR(svn_io_dir_make(dst_path, APR_OS_DEFAULT, pool)); + + /* Loop over the dirents in SRC. ('.' and '..' are auto-excluded) */ + SVN_ERR(svn_io_dir_open(&this_dir, src, subpool)); + + for (status = apr_dir_read(&this_entry, flags, this_dir); + status == APR_SUCCESS; + status = apr_dir_read(&this_entry, flags, this_dir)) + { + if ((this_entry.name[0] == '.') + && ((this_entry.name[1] == '\0') + || ((this_entry.name[1] == '.') + && (this_entry.name[2] == '\0')))) + { + continue; + } + else + { + const char *src_target, *entryname_utf8; + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + SVN_ERR(entry_name_to_utf8(&entryname_utf8, this_entry.name, + src, subpool)); + src_target = svn_dirent_join(src, entryname_utf8, subpool); + + if (this_entry.filetype == APR_REG) /* regular file */ + { + SVN_ERR(hotcopy_io_dir_file_copy(src_target, dst_path, + entryname_utf8, subpool)); + } + else if (this_entry.filetype == APR_LNK) /* symlink */ + { + const char *dst_target = svn_dirent_join(dst_path, + entryname_utf8, + subpool); + SVN_ERR(svn_io_copy_link(src_target, dst_target, + subpool)); + } + else if (this_entry.filetype == APR_DIR) /* recurse */ + { + /* Prevent infinite recursion by filtering off our + newly created destination path. */ + if (strcmp(src, dst_parent) == 0 + && strcmp(entryname_utf8, dst_basename) == 0) + continue; + + SVN_ERR(hotcopy_io_copy_dir_recursively(src_target, + dst_path, + entryname_utf8, + copy_perms, + cancel_func, + cancel_baton, + subpool)); + } + /* ### support other APR node types someday?? */ + + } + } + + if (! (APR_STATUS_IS_ENOENT(status))) + return svn_error_wrap_apr(status, _("Can't read directory '%s'"), + svn_dirent_local_style(src, pool)); + + status = apr_dir_close(this_dir); + if (status) + return svn_error_wrap_apr(status, _("Error closing directory '%s'"), + svn_dirent_local_style(src, pool)); + + /* Free any memory used by recursion */ + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + +/* Copy an un-packed revision or revprop file for revision REV from SRC_SUBDIR + * to DST_SUBDIR. Assume a sharding layout based on MAX_FILES_PER_DIR. + * Use SCRATCH_POOL for temporary allocations. */ +static svn_error_t * +hotcopy_copy_shard_file(const char *src_subdir, + const char *dst_subdir, + svn_revnum_t rev, + int max_files_per_dir, + apr_pool_t *scratch_pool) +{ + const char *src_subdir_shard = src_subdir, + *dst_subdir_shard = dst_subdir; + + if (max_files_per_dir) + { + const char *shard = apr_psprintf(scratch_pool, "%ld", + rev / max_files_per_dir); + src_subdir_shard = svn_dirent_join(src_subdir, shard, scratch_pool); + dst_subdir_shard = svn_dirent_join(dst_subdir, shard, scratch_pool); + + if (rev % max_files_per_dir == 0) + { + SVN_ERR(svn_io_make_dir_recursively(dst_subdir_shard, scratch_pool)); + SVN_ERR(svn_io_copy_perms(dst_subdir, dst_subdir_shard, + scratch_pool)); + } + } + + if (rev == 0) + { + /* Revision zero is special. It might already exist in the destination + * even if the destination is otherwise empty, because it is created + * during hotcopy initialisation. In this case it will differ from + * revision zero in the source (because of creation time). + * So always copy it. */ + SVN_ERR(svn_io_dir_file_copy(src_subdir_shard, dst_subdir_shard, + apr_psprintf(scratch_pool, "%ld", rev), + scratch_pool)); + } + else + SVN_ERR(hotcopy_io_dir_file_copy(src_subdir_shard, dst_subdir_shard, + apr_psprintf(scratch_pool, "%ld", rev), + scratch_pool)); + return SVN_NO_ERROR; +} + + +/* Copy a packed shard containing revision REV, and which contains + * MAX_FILES_PER_DIR revisions, from SRC_FS to DST_FS. + * Update *DST_MIN_UNPACKED_REV in case the shard is new in DST_FS. + * Do not re-copy data which already exists in DST_FS. + * Use SCRATCH_POOL for temporary allocations. */ +static svn_error_t * +hotcopy_copy_packed_shard(svn_revnum_t *dst_min_unpacked_rev, + svn_fs_t *src_fs, + svn_fs_t *dst_fs, + svn_revnum_t rev, + int max_files_per_dir, + apr_pool_t *scratch_pool) +{ + const char *src_subdir; + const char *dst_subdir; + const char *packed_shard; + const char *src_subdir_packed_shard; + svn_revnum_t shard_rev; + svn_revnum_t revprop_rev; + apr_pool_t *iterpool; + + /* Copy the pack file. */ + src_subdir = svn_dirent_join(src_fs->path, PATH_REVS_DIR, scratch_pool); + dst_subdir = svn_dirent_join(dst_fs->path, PATH_REVS_DIR, scratch_pool); + shard_rev = rev / max_files_per_dir; + packed_shard = apr_psprintf(iterpool, "%ld.pack", shard_rev); + src_subdir_packed_shard = svn_dirent_join(src_subdir, packed_shard, + scratch_pool); + SVN_ERR(hotcopy_io_copy_dir_recursively(src_subdir_packed_shard, + dst_subdir, packed_shard, + TRUE /* copy_perms */, + NULL /* cancel_func */, NULL, + scratch_pool)); + + /* Copy revprops belonging to revisions in this pack. */ + src_subdir = svn_dirent_join(src_fs->path, PATH_REVPROPS_DIR, scratch_pool); + dst_subdir = svn_dirent_join(dst_fs->path, PATH_REVPROPS_DIR, scratch_pool); + iterpool = svn_pool_create(scratch_pool); + for (revprop_rev = shard_rev; + revprop_rev < shard_rev + max_files_per_dir; + revprop_rev++) + { + svn_pool_clear(iterpool); + + SVN_ERR(hotcopy_copy_shard_file(src_subdir, dst_subdir, + rev, max_files_per_dir, + iterpool)); + } + svn_pool_destroy(iterpool); + + /* If necessary, update the min-unpacked rev file in the hotcopy. */ + if (*dst_min_unpacked_rev < shard_rev + max_files_per_dir) + { + *dst_min_unpacked_rev = shard_rev + max_files_per_dir; + SVN_ERR(write_revnum_file(dst_fs->path, PATH_MIN_UNPACKED_REV, + *dst_min_unpacked_rev, + scratch_pool)); + } + + return SVN_NO_ERROR; +} + +/* If NEW_YOUNGEST is younger than *DST_YOUNGEST, update the 'current' + * file in DST_FS and set *DST_YOUNGEST to NEW_YOUNGEST. + * Use SCRATCH_POOL for temporary allocations. */ +static svn_error_t * +hotcopy_update_current(svn_revnum_t *dst_youngest, + svn_fs_t *dst_fs, + svn_revnum_t new_youngest, + apr_pool_t *scratch_pool) +{ + char next_node_id[MAX_KEY_SIZE] = "0"; + char next_copy_id[MAX_KEY_SIZE] = "0"; + fs_fs_data_t *dst_ffd = dst_fs->fsap_data; + + if (*dst_youngest >= new_youngest) + return SVN_NO_ERROR; + + /* If necessary, get new current next_node and next_copy IDs. */ + if (dst_ffd->format < SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT) + { + apr_off_t root_offset; + apr_file_t *rev_file; + + if (dst_ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) + SVN_ERR(update_min_unpacked_rev(dst_fs, scratch_pool)); + + SVN_ERR(open_pack_or_rev_file(&rev_file, dst_fs, new_youngest, + scratch_pool)); + SVN_ERR(get_root_changes_offset(&root_offset, NULL, rev_file, + dst_fs, new_youngest, scratch_pool)); + SVN_ERR(recover_find_max_ids(dst_fs, new_youngest, rev_file, + root_offset, next_node_id, next_copy_id, + scratch_pool)); + SVN_ERR(svn_io_file_close(rev_file, scratch_pool)); + } + + /* Update 'current'. */ + SVN_ERR(write_current(dst_fs, new_youngest, next_node_id, next_copy_id, + scratch_pool)); + + *dst_youngest = new_youngest; + + return SVN_NO_ERROR; +} + + +/* Remove revisions between START_REV (inclusive) and END_REV (non-inclusive) + * from DST_FS. Assume sharding as per MAX_FILES_PER_DIR. + * Use SCRATCH_POOL for temporary allocations. */ +static svn_error_t * +hotcopy_remove_rev_files(svn_fs_t *dst_fs, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + int max_files_per_dir, + apr_pool_t *scratch_pool) +{ + const char *dst_subdir; + const char *shard; + const char *dst_subdir_shard; + svn_revnum_t rev; + apr_pool_t *iterpool; + + SVN_ERR_ASSERT(start_rev <= end_rev); + + dst_subdir = svn_dirent_join(dst_fs->path, PATH_REVS_DIR, scratch_pool); + + /* Pre-compute paths for initial shard. */ + shard = apr_psprintf(iterpool, "%ld", start_rev / max_files_per_dir); + dst_subdir_shard = svn_dirent_join(dst_subdir, shard, scratch_pool); + + iterpool = svn_pool_create(scratch_pool); + for (rev = start_rev; rev < end_rev; rev++) + { + const char *rev_path; + + svn_pool_clear(iterpool); + + /* If necessary, update paths for shard. */ + if (rev % max_files_per_dir == 0) + { + shard = apr_psprintf(iterpool, "%ld", rev / max_files_per_dir); + dst_subdir_shard = svn_dirent_join(dst_subdir, shard, iterpool); + } + + rev_path = svn_dirent_join(dst_subdir_shard, + apr_psprintf(iterpool, "%ld", rev), + iterpool); + + /* Make the rev file writable and remove it. */ + SVN_ERR(svn_io_set_file_read_write(rev_path, TRUE, iterpool)); + SVN_ERR(svn_io_remove_file2(rev_path, TRUE, iterpool)); + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Verify that DST_FS is a suitable destination for an incremental + * hotcopy from SRC_FS. */ +static svn_error_t * +hotcopy_incremental_check_preconditions(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + apr_pool_t *pool) +{ + fs_fs_data_t *src_ffd = src_fs->fsap_data; + fs_fs_data_t *dst_ffd = dst_fs->fsap_data; + + /* We only support incremental hotcopy between the same format. */ + if (src_ffd->format != dst_ffd->format) + return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("The FSFS format (%d) of the hotcopy source does not match the " + "FSFS format (%d) of the hotcopy destination; please upgrade " + "both repositories to the same format"), + src_ffd->format, dst_ffd->format); + + /* Make sure the UUID of source and destination match up. + * We don't want to copy over a different repository. */ + if (strcmp(src_ffd->uuid, dst_ffd->uuid) != 0) + return svn_error_create(SVN_ERR_RA_UUID_MISMATCH, NULL, + _("The UUID of the hotcopy source does " + "not match the UUID of the hotcopy " + "destination")); + + /* Also require same shard size. */ + if (src_ffd->max_files_per_dir != dst_ffd->max_files_per_dir) + return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("The sharding layout configuration " + "of the hotcopy source does not match " + "the sharding layout configuration of " + "the hotcopy destination")); + return SVN_NO_ERROR; +} + + +/* Baton for hotcopy_body(). */ +struct hotcopy_body_baton { + svn_fs_t *src_fs; + svn_fs_t *dst_fs; + svn_boolean_t incremental; + svn_cancel_func_t cancel_func; + void *cancel_baton; +} hotcopy_body_baton; + +/* Perform a hotcopy, either normal or incremental. + * + * Normal hotcopy assumes that the destination exists as an empty + * directory. It behaves like an incremental hotcopy except that + * none of the copied files already exist in the destination. + * + * An incremental hotcopy copies only changed or new files to the destination, + * and removes files from the destination no longer present in the source. + * While the incremental hotcopy is running, readers should still be able + * to access the destintation repository without error and should not see + * revisions currently in progress of being copied. Readers are able to see + * new fully copied revisions even if the entire incremental hotcopy procedure + * has not yet completed. + * + * Writers are blocked out completely during the entire incremental hotcopy + * process to ensure consistency. This function assumes that the repository + * write-lock is held. + */ +static svn_error_t * +hotcopy_body(void *baton, apr_pool_t *pool) +{ + struct hotcopy_body_baton *hbb = baton; + svn_fs_t *src_fs = hbb->src_fs; + fs_fs_data_t *src_ffd = src_fs->fsap_data; + svn_fs_t *dst_fs = hbb->dst_fs; + fs_fs_data_t *dst_ffd = dst_fs->fsap_data; + int max_files_per_dir = src_ffd->max_files_per_dir; + svn_boolean_t incremental = hbb->incremental; + svn_cancel_func_t cancel_func = hbb->cancel_func; + void* cancel_baton = hbb->cancel_baton; + svn_revnum_t src_youngest; + svn_revnum_t dst_youngest; + svn_revnum_t rev; + svn_revnum_t src_min_unpacked_rev; + svn_revnum_t dst_min_unpacked_rev; + const char *src_subdir; + const char *dst_subdir; + const char *revprop_src_subdir; + const char *revprop_dst_subdir; + apr_pool_t *iterpool; + svn_node_kind_t kind; + + /* Try to copy the config. + * + * ### We try copying the config file before doing anything else, + * ### because higher layers will abort the hotcopy if we throw + * ### an error from this function, and that renders the hotcopy + * ### unusable anyway. */ + if (src_ffd->format >= SVN_FS_FS__MIN_CONFIG_FILE) + { + svn_error_t *err; + + err = svn_io_dir_file_copy(src_fs->path, dst_fs->path, PATH_CONFIG, + pool); + if (err) + { + if (APR_STATUS_IS_ENOENT(err->apr_err)) + { + /* 1.6.0 to 1.6.11 did not copy the configuration file during + * hotcopy. So if we're hotcopying a repository which has been + * created as a hotcopy itself, it's possible that fsfs.conf + * does not exist. Ask the user to re-create it. + * + * ### It would be nice to make this a non-fatal error, + * ### but this function does not get an svn_fs_t object + * ### so we have no way of just printing a warning via + * ### the fs->warning() callback. */ + + const char *msg; + const char *src_abspath; + const char *dst_abspath; + const char *config_relpath; + svn_error_t *err2; + + config_relpath = svn_dirent_join(src_fs->path, PATH_CONFIG, pool); + err2 = svn_dirent_get_absolute(&src_abspath, src_fs->path, pool); + if (err2) + return svn_error_trace(svn_error_compose_create(err, err2)); + err2 = svn_dirent_get_absolute(&dst_abspath, dst_fs->path, pool); + if (err2) + return svn_error_trace(svn_error_compose_create(err, err2)); + + /* ### hack: strip off the 'db/' directory from paths so + * ### they make sense to the user */ + src_abspath = svn_dirent_dirname(src_abspath, pool); + dst_abspath = svn_dirent_dirname(dst_abspath, pool); + + msg = apr_psprintf(pool, + _("Failed to create hotcopy at '%s'. " + "The file '%s' is missing from the source " + "repository. Please create this file, for " + "instance by running 'svnadmin upgrade %s'"), + dst_abspath, config_relpath, src_abspath); + return svn_error_quick_wrap(err, msg); + } + else + return svn_error_trace(err); + } + } + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Find the youngest revision in the source and destination. + * We only support hotcopies from sources with an equal or greater amount + * of revisions than the destination. + * This also catches the case where users accidentally swap the + * source and destination arguments. */ + SVN_ERR(get_youngest(&src_youngest, src_fs->path, pool)); + if (incremental) + { + SVN_ERR(get_youngest(&dst_youngest, dst_fs->path, pool)); + if (src_youngest < dst_youngest) + return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("The hotcopy destination already contains more revisions " + "(%lu) than the hotcopy source contains (%lu); are source " + "and destination swapped?"), + dst_youngest, src_youngest); + } + else + dst_youngest = 0; + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Copy the min unpacked rev, and read its value. */ + if (src_ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) + { + const char *min_unpacked_rev_path; + + min_unpacked_rev_path = svn_dirent_join(src_fs->path, + PATH_MIN_UNPACKED_REV, + pool); + SVN_ERR(read_min_unpacked_rev(&src_min_unpacked_rev, + min_unpacked_rev_path, + pool)); + + min_unpacked_rev_path = svn_dirent_join(dst_fs->path, + PATH_MIN_UNPACKED_REV, + pool); + SVN_ERR(read_min_unpacked_rev(&dst_min_unpacked_rev, + min_unpacked_rev_path, + pool)); + + /* We only support packs coming from the hotcopy source. + * The destination should not be packed independently from + * the source. This also catches the case where users accidentally + * swap the source and destination arguments. */ + if (src_min_unpacked_rev < dst_min_unpacked_rev) + return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("The hotcopy destination already contains " + "more packed revisions (%lu) than the " + "hotcopy source contains (%lu)"), + dst_min_unpacked_rev - 1, + src_min_unpacked_rev - 1); + + SVN_ERR(svn_io_dir_file_copy(src_fs->path, dst_fs->path, + PATH_MIN_UNPACKED_REV, pool)); + } + else + src_min_unpacked_rev = 0; + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* + * Copy the necessary rev files. + */ + + src_subdir = svn_dirent_join(src_fs->path, PATH_REVS_DIR, pool); + dst_subdir = svn_dirent_join(dst_fs->path, PATH_REVS_DIR, pool); + SVN_ERR(svn_io_make_dir_recursively(dst_subdir, pool)); + + iterpool = svn_pool_create(pool); + /* First, copy packed shards. */ + for (rev = 0; rev < src_min_unpacked_rev; rev += max_files_per_dir) + { + svn_pool_clear(iterpool); + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Copy the packed shard. */ + SVN_ERR(hotcopy_copy_packed_shard(&dst_min_unpacked_rev, + src_fs, dst_fs, + rev, max_files_per_dir, + iterpool)); + + /* If necessary, update 'current' to the most recent packed rev, + * so readers can see new revisions which arrived in this pack. */ + SVN_ERR(hotcopy_update_current(&dst_youngest, dst_fs, + rev + max_files_per_dir - 1, + iterpool)); + + /* Remove revision files which are now packed. */ + if (incremental) + SVN_ERR(hotcopy_remove_rev_files(dst_fs, rev, rev + max_files_per_dir, + max_files_per_dir, iterpool)); + } + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Now, copy pairs of non-packed evisions and revprop files. + * If necessary, update 'current' after copying all files from a shard. */ + SVN_ERR_ASSERT(rev == src_min_unpacked_rev); + revprop_src_subdir = svn_dirent_join(src_fs->path, PATH_REVPROPS_DIR, pool); + revprop_dst_subdir = svn_dirent_join(dst_fs->path, PATH_REVPROPS_DIR, pool); + SVN_ERR(svn_io_make_dir_recursively(revprop_dst_subdir, pool)); + for (; rev <= src_youngest; rev++) + { + svn_error_t *err; + + svn_pool_clear(iterpool); + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Copy the rev file. */ + err = hotcopy_copy_shard_file(src_subdir, dst_subdir, + rev, max_files_per_dir, + iterpool); + if (err) + { + if (APR_STATUS_IS_ENOENT(err->apr_err) && + src_ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) + { + svn_error_clear(err); + + /* The source rev file does not exist. This can happen if the + * source repository is being packed concurrently with this + * hotcopy operation. + * + * If the new revision is now packed, and the youngest revision + * we're interested in is not inside this pack, try to copy the + * pack instead. + * + * If the youngest revision ended up being packed, don't try + * to be smart and work around this. Just abort the hotcopy. */ + SVN_ERR(update_min_unpacked_rev(src_fs, pool)); + if (is_packed_rev(src_fs, rev)) + { + if (is_packed_rev(src_fs, src_youngest)) + return svn_error_createf( + SVN_ERR_FS_NO_SUCH_REVISION, NULL, + _("The assumed HEAD revision (%lu) of the " + "hotcopy source has been packed while the " + "hotcopy was in progress; please restart " + "the hotcopy operation"), + src_youngest); + + SVN_ERR(hotcopy_copy_packed_shard(&dst_min_unpacked_rev, + src_fs, dst_fs, + rev, max_files_per_dir, + iterpool)); + rev = dst_min_unpacked_rev; + continue; + } + else + return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, + _("Revision %lu disappeared from the " + "hotcopy source while hotcopy was " + "in progress"), rev); + } + else + return svn_error_trace(err); + } + + /* Copy the revprop file. */ + SVN_ERR(hotcopy_copy_shard_file(revprop_src_subdir, + revprop_dst_subdir, + rev, max_files_per_dir, + iterpool)); + + /* After completing a full shard, update 'current'. */ + if (rev % max_files_per_dir == 0) + SVN_ERR(hotcopy_update_current(&dst_youngest, dst_fs, rev, iterpool)); + } + svn_pool_destroy(iterpool); + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* We assume that all revisions were copied now, and didn't exit the above + * loop early. 'rev' was last incremented during exit of the loop. */ + SVN_ERR_ASSERT(rev == src_youngest + 1); + + /* All revisions were copied. Update 'current'. */ + SVN_ERR(hotcopy_update_current(&dst_youngest, dst_fs, src_youngest, pool)); + + /* Replace the locks tree. + * This is racy in case readers are currently trying to list locks in + * the destination. However, we need to get rid of stale locks. + * This is the simplest way of doing this, so we accept this small race. */ + dst_subdir = svn_dirent_join(dst_fs->path, PATH_LOCKS_DIR, pool); + SVN_ERR(svn_io_remove_dir2(dst_subdir, TRUE, cancel_func, cancel_baton, + pool)); + src_subdir = svn_dirent_join(src_fs->path, PATH_LOCKS_DIR, pool); + SVN_ERR(svn_io_check_path(src_subdir, &kind, pool)); + if (kind == svn_node_dir) + SVN_ERR(svn_io_copy_dir_recursively(src_subdir, dst_fs->path, + PATH_LOCKS_DIR, TRUE, + cancel_func, cancel_baton, pool)); + + /* Now copy the node-origins cache tree. */ + src_subdir = svn_dirent_join(src_fs->path, PATH_NODE_ORIGINS_DIR, pool); + SVN_ERR(svn_io_check_path(src_subdir, &kind, pool)); + if (kind == svn_node_dir) + SVN_ERR(hotcopy_io_copy_dir_recursively(src_subdir, dst_fs->path, + PATH_NODE_ORIGINS_DIR, TRUE, + cancel_func, cancel_baton, pool)); + + /* + * NB: Data copied below is only read by writers, not readers. + * Writers are still locked out at this point. + */ + + if (dst_ffd->format >= SVN_FS_FS__MIN_REP_SHARING_FORMAT) + { + /* Copy the rep cache and then remove entries for revisions + * younger than the destination's youngest revision. */ + src_subdir = svn_dirent_join(src_fs->path, REP_CACHE_DB_NAME, pool); + dst_subdir = svn_dirent_join(dst_fs->path, REP_CACHE_DB_NAME, pool); + SVN_ERR(svn_io_check_path(src_subdir, &kind, pool)); + if (kind == svn_node_file) + { + SVN_ERR(svn_sqlite__hotcopy(src_subdir, dst_subdir, pool)); + SVN_ERR(svn_fs_fs__del_rep_reference(dst_fs, dst_youngest, pool)); + } + } + + /* Copy the txn-current file. */ + if (dst_ffd->format >= SVN_FS_FS__MIN_TXN_CURRENT_FORMAT) + SVN_ERR(svn_io_dir_file_copy(src_fs->path, dst_fs->path, + PATH_TXN_CURRENT, pool)); + + /* Hotcopied FS is complete. Stamp it with a format file. */ + SVN_ERR(write_format(svn_dirent_join(dst_fs->path, PATH_FORMAT, pool), + dst_ffd->format, max_files_per_dir, TRUE, pool)); + + return SVN_NO_ERROR; +} + + +/* Create an empty filesystem at DST_FS at DST_PATH with the same + * configuration as SRC_FS (uuid, format, and other parameters). + * After creation DST_FS has no revisions, not even revision zero. */ +static svn_error_t * +hotcopy_create_empty_dest(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + const char *dst_path, + apr_pool_t *pool) +{ + fs_fs_data_t *src_ffd = src_fs->fsap_data; + fs_fs_data_t *dst_ffd = dst_fs->fsap_data; + + dst_fs->path = apr_pstrdup(pool, dst_path); + + dst_ffd->max_files_per_dir = src_ffd->max_files_per_dir; + dst_ffd->config = src_ffd->config; + dst_ffd->format = src_ffd->format; + + /* Create the revision data directories. */ + if (dst_ffd->max_files_per_dir) + SVN_ERR(svn_io_make_dir_recursively(path_rev_shard(dst_fs, 0, pool), + pool)); + else + SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(dst_path, + PATH_REVS_DIR, pool), + pool)); + + /* Create the revprops directory. */ + if (src_ffd->max_files_per_dir) + SVN_ERR(svn_io_make_dir_recursively(path_revprops_shard(dst_fs, 0, pool), + pool)); + else + SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(dst_path, + PATH_REVPROPS_DIR, + pool), + pool)); + + /* Create the transaction directory. */ + SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(dst_path, PATH_TXNS_DIR, + pool), + pool)); + + /* Create the protorevs directory. */ + if (dst_ffd->format >= SVN_FS_FS__MIN_PROTOREVS_DIR_FORMAT) + SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(dst_path, + PATH_TXN_PROTOS_DIR, + pool), + pool)); + + /* Create the 'current' file. */ + SVN_ERR(svn_io_file_create(svn_fs_fs__path_current(dst_fs, pool), + (dst_ffd->format >= + SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT + ? "0\n" : "0 1 1\n"), + pool)); + + /* Create lock file and UUID. */ + SVN_ERR(svn_io_file_create(path_lock(dst_fs, pool), "", pool)); + SVN_ERR(svn_fs_fs__set_uuid(dst_fs, src_ffd->uuid, pool)); + + /* Create the min unpacked rev file. */ + if (dst_ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) + SVN_ERR(svn_io_file_create(path_min_unpacked_rev(dst_fs, pool), + "0\n", pool)); + /* Create the txn-current file if the repository supports + the transaction sequence file. */ + if (dst_ffd->format >= SVN_FS_FS__MIN_TXN_CURRENT_FORMAT) + { + SVN_ERR(svn_io_file_create(path_txn_current(dst_fs, pool), + "0\n", pool)); + SVN_ERR(svn_io_file_create(path_txn_current_lock(dst_fs, pool), + "", pool)); + } + + dst_ffd->youngest_rev_cache = 0; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_fs_fs__hotcopy(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + const char *src_path, + const char *dst_path, + svn_boolean_t incremental, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + struct hotcopy_body_baton hbb; + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + SVN_ERR(svn_fs_fs__open(src_fs, src_path, pool)); + + if (incremental) + { + const char *dst_format_abspath; + svn_node_kind_t dst_format_kind; + + /* Check destination format to be sure we know how to incrementally + * hotcopy to the destination FS. */ + dst_format_abspath = svn_dirent_join(dst_path, PATH_FORMAT, pool); + SVN_ERR(svn_io_check_path(dst_format_abspath, &dst_format_kind, pool)); + if (dst_format_kind == svn_node_none) + { + /* Destination doesn't exist yet. Perform a normal hotcopy to a + * empty destination using the same configuration as the source. */ + SVN_ERR(hotcopy_create_empty_dest(src_fs, dst_fs, dst_path, pool)); + } + else + { + /* Check the existing repository. */ + SVN_ERR(svn_fs_fs__open(dst_fs, dst_path, pool)); + SVN_ERR(hotcopy_incremental_check_preconditions(src_fs, dst_fs, + pool)); + } + } + else + { + /* Start out with an empty destination using the same configuration + * as the source. */ + SVN_ERR(hotcopy_create_empty_dest(src_fs, dst_fs, dst_path, pool)); + } + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + hbb.src_fs = src_fs; + hbb.dst_fs = dst_fs; + hbb.incremental = incremental; + hbb.cancel_func = cancel_func; + hbb.cancel_baton = cancel_baton; + SVN_ERR(svn_fs_fs__with_write_lock(dst_fs, hotcopy_body, &hbb, pool)); + + return SVN_NO_ERROR; +} Modified: subversion/trunk/subversion/libsvn_fs_fs/fs_fs.h URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_fs_fs/fs_fs.h?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/libsvn_fs_fs/fs_fs.h (original) +++ subversion/trunk/subversion/libsvn_fs_fs/fs_fs.h Thu Dec 15 11:03:08 2011 @@ -44,11 +44,17 @@ svn_error_t *svn_fs_fs__verify(svn_fs_t void *cancel_baton, apr_pool_t *pool); -/* Copy the fsfs filesystem at SRC_PATH into a new copy at DST_PATH. - Use POOL for temporary allocations. */ -svn_error_t *svn_fs_fs__hotcopy(const char *src_path, - const char *dst_path, - apr_pool_t *pool); +/* Copy the fsfs filesystem SRC_FS at SRC_PATH into a new copy DST_FS at + * DST_PATH. If INCREMENTAL is TRUE, do not re-copy data which already + * exists in DST_FS. Use POOL for temporary allocations. */ +svn_error_t * svn_fs_fs__hotcopy(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + const char *src_path, + const char *dst_path, + svn_boolean_t incremental, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool); /* Recover the fsfs associated with filesystem FS. Use optional CANCEL_FUNC/CANCEL_BATON for cancellation support. Modified: subversion/trunk/subversion/libsvn_repos/repos.c URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_repos/repos.c?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/libsvn_repos/repos.c (original) +++ subversion/trunk/subversion/libsvn_repos/repos.c Thu Dec 15 11:03:08 2011 @@ -1699,6 +1699,9 @@ svn_error_t *svn_repos_db_logfiles(apr_a struct hotcopy_ctx_t { const char *dest; /* target location to construct */ size_t src_len; /* len of the source path*/ + svn_boolean_t incremental; + svn_cancel_func_t cancel_func; + void *cancel_baton; }; /** Called by (svn_io_dir_walk2). @@ -1719,6 +1722,9 @@ static svn_error_t *hotcopy_structure(vo const char *sub_path; const char *target; + if (ctx->cancel_func) + SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); + if (strlen(path) == ctx->src_len) { sub_path = ""; @@ -1748,7 +1754,17 @@ static svn_error_t *hotcopy_structure(vo target = svn_dirent_join(ctx->dest, sub_path, pool); if (finfo->filetype == APR_DIR) - return create_repos_dir(target, pool); + { + svn_error_t *err; + + err = create_repos_dir(target, pool); + if (ctx->incremental && err && err->apr_err == SVN_ERR_DIR_NOT_EMPTY) + { + svn_error_clear(err); + err = SVN_NO_ERROR; + } + return svn_error_trace(err); + } else if (finfo->filetype == APR_REG) return svn_io_copy_file(path, target, TRUE, pool); else if (finfo->filetype == APR_LNK) @@ -1777,17 +1793,29 @@ lock_db_logs_file(svn_repos_t *repos, /* Make a copy of a repository with hot backup of fs. */ svn_error_t * -svn_repos_hotcopy(const char *src_path, - const char *dst_path, - svn_boolean_t clean_logs, - apr_pool_t *pool) +svn_repos_hotcopy2(const char *src_path, + const char *dst_path, + svn_boolean_t clean_logs, + svn_boolean_t incremental, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) { svn_repos_t *src_repos; svn_repos_t *dst_repos; struct hotcopy_ctx_t hotcopy_context; + svn_error_t *err; + const char *src_abspath; + const char *dst_abspath; + + SVN_ERR(svn_dirent_get_absolute(&src_abspath, src_path, pool)); + SVN_ERR(svn_dirent_get_absolute(&dst_abspath, dst_path, pool)); + if (strcmp(src_abspath, dst_abspath) == 0) + return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, + _("Hotcopy source and destination are equal")); /* Try to open original repository */ - SVN_ERR(get_repos(&src_repos, src_path, + SVN_ERR(get_repos(&src_repos, src_abspath, FALSE, FALSE, FALSE, /* don't try to open the db yet. */ NULL, @@ -1804,9 +1832,12 @@ svn_repos_hotcopy(const char *src_path, /* Copy the repository to a new path, with exception of specially handled directories */ - hotcopy_context.dest = dst_path; - hotcopy_context.src_len = strlen(src_path); - SVN_ERR(svn_io_dir_walk2(src_path, + hotcopy_context.dest = dst_abspath; + hotcopy_context.src_len = strlen(src_abspath); + hotcopy_context.incremental = incremental; + hotcopy_context.cancel_func = cancel_func; + hotcopy_context.cancel_baton = cancel_baton; + SVN_ERR(svn_io_dir_walk2(src_abspath, 0, hotcopy_structure, &hotcopy_context, @@ -1815,20 +1846,35 @@ svn_repos_hotcopy(const char *src_path, /* Prepare dst_repos object so that we may create locks, so that we may open repository */ - dst_repos = create_svn_repos_t(dst_path, pool); + dst_repos = create_svn_repos_t(dst_abspath, pool); dst_repos->fs_type = src_repos->fs_type; dst_repos->format = src_repos->format; - SVN_ERR(create_locks(dst_repos, pool)); + err = create_locks(dst_repos, pool); + if (err) + { + if (incremental && err->apr_err == SVN_ERR_DIR_NOT_EMPTY) + svn_error_clear(err); + else + return svn_error_trace(err); + } - SVN_ERR(svn_io_dir_make_sgid(dst_repos->db_path, APR_OS_DEFAULT, pool)); + err = svn_io_dir_make_sgid(dst_repos->db_path, APR_OS_DEFAULT, pool); + if (err) + { + if (incremental && APR_STATUS_IS_EEXIST(err->apr_err)) + svn_error_clear(err); + else + return svn_error_trace(err); + } /* Exclusively lock the new repository. No one should be accessing it at the moment */ SVN_ERR(lock_repos(dst_repos, TRUE, FALSE, pool)); - SVN_ERR(svn_fs_hotcopy(src_repos->db_path, dst_repos->db_path, - clean_logs, pool)); + SVN_ERR(svn_fs_hotcopy2(src_repos->db_path, dst_repos->db_path, + clean_logs, incremental, + cancel_func, cancel_baton, pool)); /* Destination repository is ready. Stamp it with a format number. */ return svn_io_write_version_file @@ -1836,6 +1882,16 @@ svn_repos_hotcopy(const char *src_path, dst_repos->format, pool); } +svn_error_t * +svn_repos_hotcopy(const char *src_path, + const char *dst_path, + svn_boolean_t clean_logs, + apr_pool_t *pool) +{ + return svn_error_trace(svn_repos_hotcopy2(src_path, dst_path, clean_logs, + FALSE, NULL, NULL, pool)); +} + /* Return the library version number. */ const svn_version_t * svn_repos_version(void) Modified: subversion/trunk/subversion/svnadmin/main.c URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/svnadmin/main.c?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/svnadmin/main.c (original) +++ subversion/trunk/subversion/svnadmin/main.c Thu Dec 15 11:03:08 2011 @@ -208,7 +208,7 @@ static const apr_getopt_option_t options N_("specify revision number ARG (or X:Y range)")}, {"incremental", svnadmin__incremental, 0, - N_("dump incrementally")}, + N_("dump or hotcopy incrementally")}, {"deltas", svnadmin__deltas, 0, N_("use deltas in dump output")}, @@ -332,8 +332,10 @@ static const svn_opt_subcommand_desc2_t {"hotcopy", subcommand_hotcopy, {0}, N_ ("usage: svnadmin hotcopy REPOS_PATH NEW_REPOS_PATH\n\n" - "Makes a hot copy of a repository.\n"), - {svnadmin__clean_logs} }, + "Makes a hot copy of a repository.\n" + "If --incremental is passed, data which already exists at the destination\n" + "is not copied again (for FSFS repositories only).\n"), + {svnadmin__clean_logs, svnadmin__incremental} }, {"list-dblogs", subcommand_list_dblogs, {0}, N_ ("usage: svnadmin list-dblogs REPOS_PATH\n\n" @@ -1431,8 +1433,9 @@ subcommand_hotcopy(apr_getopt_t *os, voi new_repos_path = APR_ARRAY_IDX(targets, 0, const char *); SVN_ERR(target_arg_to_dirent(&new_repos_path, new_repos_path, pool)); - return svn_repos_hotcopy(opt_state->repository_path, new_repos_path, - opt_state->clean_logs, pool); + return svn_repos_hotcopy2(opt_state->repository_path, new_repos_path, + opt_state->clean_logs, opt_state->incremental, + check_cancel, NULL, pool); } Modified: subversion/trunk/subversion/tests/cmdline/svnadmin_tests.py URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svnadmin_tests.py?rev=1214697&r1=1214696&r2=1214697&view=diff ============================================================================== --- subversion/trunk/subversion/tests/cmdline/svnadmin_tests.py (original) +++ subversion/trunk/subversion/tests/cmdline/svnadmin_tests.py Thu Dec 15 11:03:08 2011 @@ -1510,6 +1510,36 @@ def load_ranges(sbox): svntest.verify.compare_and_display_lines("Dump files", "DUMP", expected_dump, new_dumpdata) +@SkipUnless(svntest.main.is_fs_type_fsfs) +def hotcopy_incremental(sbox): + "'svnadmin hotcopy --incremental PATH .'" + sbox.build() + + backup_dir, backup_url = sbox.add_repo_path('backup') + os.mkdir(backup_dir) + cwd = os.getcwd() + + for i in [1, 2, 3]: + os.chdir(backup_dir) + svntest.actions.run_and_verify_svnadmin( + None, None, [], + "hotcopy", "--incremental", os.path.join(cwd, sbox.repo_dir), '.') + + os.chdir(cwd) + + exit_code, origout, origerr = svntest.main.run_svnadmin("dump", + sbox.repo_dir, + '--quiet') + exit_code, backout, backerr = svntest.main.run_svnadmin("dump", + backup_dir, + '--quiet') + if origerr or backerr or origout != backout: + raise svntest.Failure + + if i < 3: + sbox.simple_mkdir("newdir-%i" % i) + sbox.simple_commit() + ######################################################################## # Run the tests @@ -1541,6 +1571,7 @@ test_list = [ None, verify_non_utf8_paths, test_lslocks_and_rmlocks, load_ranges, + hotcopy_incremental, ] if __name__ == '__main__':