Author: jpeck Date: Fri Jul 3 20:59:45 2026 New Revision: 1935862 Log: On the 'svnsync-atomic-author' branch:
Let svnsync set svn:author and svn:date in the commit itself when the destination supports it, instead of patching them up with a post-commit revprop change. This makes mirroring a revision atomic for ra_local destinations and removes the window where a mirrored revision briefly carries the wrong author and date. See discussion: https://lists.apache.org/thread/qnxqd4whjp62rk2pgndt8xyp40oqnom2 Introduce SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE, advertised by ra_local and wired through the standard capability negotiation for svn:// and http:// so future servers can opt in without client changes. Rev the repos-layer commit APIs to carry transaction flags (SVN_FS_TXN_CLIENT_DATE). * subversion/include/svn_ra.h (SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE): New capability. (svn_ra_get_commit_editor3): Allow svn:author/svn:date in REVPROP_TABLE when the session advertises the new capability. * subversion/include/svn_ra_svn.h (SVN_RA_SVN_CAP_COMMIT_PRESERVES_AUTHOR_DATE): New wire capability. * subversion/include/svn_dav.h (SVN_DAV_NS_DAV_SVN_COMMIT_PRESERVES_AUTHOR_DATE): New OPTIONS header token. * subversion/include/svn_repos.h (svn_repos_get_commit_editor6, svn_repos_fs_begin_txn_for_commit3): New, taking TXN_FLAGS. (svn_repos_get_commit_editor5, svn_repos_fs_begin_txn_for_commit2): Deprecate. * subversion/include/private/svn_repos_private.h (svn_repos__get_commit_ev2): Take TXN_FLAGS. * subversion/libsvn_repos/commit.c (edit_baton): Add TXN_FLAGS. (open_root): Pass them to svn_repos_fs_begin_txn_for_commit3(). (svn_repos_get_commit_editor6, svn_repos__get_commit_ev2): Accept and forward TXN_FLAGS. * subversion/libsvn_repos/fs-wrap.c (svn_repos_fs_begin_txn_for_commit3): New revision of ..._commit2(), OR-ing TXN_FLAGS into SVN_FS_TXN_CHECK_LOCKS. (svn_repos_fs_begin_txn_for_commit): Move to deprecated.c. * subversion/libsvn_repos/deprecated.c (svn_repos_get_commit_editor5, svn_repos_fs_begin_txn_for_commit2, svn_repos_fs_begin_txn_for_commit): Implement as wrappers. * subversion/libsvn_ra_local/ra_plugin.c (prepare_commit_revprops): New helper: default svn:author to the session username only when absent, and request SVN_FS_TXN_CLIENT_DATE iff the * is committed; otherwise it will be overwritten with the commit * * @since New in 1.16. svn_repos_t *repos, t32_t txn_flags, apr_pool_t *pool); /** Like svn_repos_fs_begin_txn_for_commit3(), but with @a flags /** Like svn_repos_fs_begin_txn_for_commit3(), but with @a txn_flags * set to #SVN_FS_TXN_CHECK_LOCKS only. * * @note @a revprop_table may contain an #SVN_PROP_REVISION_DATE property, Read 1 file ● Update(subversion\libsvn_repos\fs-wrap.c) Added 1 line, removed 1 line svn_repos_t *repos, svn_revnum_t rev, _t *pool) { apr_array_header_t *revprops; ● Update(subversion\libsvn_repos\fs-wrap.c) Added 1 line, removed 1 line /* Begin the transaction, ask for the fs to do on-the-fly lock checks. We fetch its name, too, so the start-commit hook can use it. */ SVN_ERR(svn_fs_begin_txn2(&txn, repos->fs, rev, SVN_FS_TXN_CHECK_LOCKS | flags, pool)); SVN_FS_TXN_CHECK_LOCKS | txn_flags, poo err = svn_fs_txn_name(&txn_name, txn, pool); if (err) return svn_error_compose_create(err, svn_fs_abort_txn(txn, pool)); Fix 5c — state commit_callback nullability on editor6 directly (replacing the stale pre-1.6 parenthetical): ● Update(subversion\include\svn_repos.h) Added 1 line, removed 2 lines * returns an error, that error will be returned from @c close_edit, * otherwise if there was a post-commit hook failure, then that error * will be returned with code SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED. * (Note that prior to Subversion 1.6, @a commit_callback cannot be @c NULL; * if you don't need a callback, pass a dummy function.) * @a commit_callback may be @c NULL. * * Calling @a (*editor)->abort_edit aborts the commit, and will also * abort the commit transaction unless @a txn was supplied (not @c You've hit your session limit · resets 9:50pm (Europe/London) /upgrade or /usage-credits to finish what you’re working on. ✻ Baked for 5m 21s ❯ Can you put together a brief commit message for this change Here's a commit message in the Apache Subversion log-message convention (summary paragraph plus per-file annotations), ready for svn commit -F: On the 'svnsync-atomic-author' branch: Let svnsync set svn:author and svn:date in the commit itself when the destination supports it, instead of patching them up with a post-commit revprop change. This makes mirroring a revision atomic for ra_local destinations and removes the window where a mirrored revision briefly ent changes. Rev the repos-layer commit APIs to carry transaction flags (SVN_FS_TXN_CLIENT_DATE). * subversion/include/svn_ra.h (SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE): New capability. (svn_ra_get_commit_editor3): Allow svn:author/svn:date in REVPROP_TABLE when the session advertises the new capability. * subversion/include/svn_ra_svn.h (SVN_RA_SVN_CAP_COMMIT_PRESERVES_AUTHOR_DATE): New wire capability. * subversion/include/svn_dav.h (SVN_DAV_NS_DAV_SVN_COMMIT_PRESERVES_AUTHOR_DATE): New OPTIONS header token. * subversion/include/svn_repos.h (svn_repos_get_commit_editor6, svn_repos_fs_begin_txn_for_commit3): New, taking TXN_FLAGS. (svn_repos_get_commit_editor5, svn_repos_fs_begin_txn_for_commit2): * subversion/libsvn_repos/commit.c (edit_baton): Add TXN_FLAGS. (open_root): Pass them to svn_repos_fs_begin_txn_for_commit3(). (svn_repos_get_commit_editor6, svn_repos__get_commit_ev2): Accept and forward TXN_FLAGS. * subversion/libsvn_repos/fs-wrap.c (svn_repos_fs_begin_txn_for_commit3): New revision of ..._commit2(), OR-ing TXN_FLAGS into SVN_FS_TXN_CHECK_LOCKS. (svn_repos_fs_begin_txn_for_commit): Move to deprecated.c. * subversion/libsvn_repos/deprecated.c (svn_repos_get_commit_editor5, svn_repos_fs_begin_txn_for_commit2, svn_repos_fs_begin_txn_for_commit): Implement as wrappers. * subversion/libsvn_ra_local/ra_plugin.c (svn_ra_local__get_commit_editor, svn_ra_local__get_commit_ev2): Use it. (svn_ra_local__has_capability): Advertise the new capability. * subversion/libsvn_ra_svn/client.c (ra_svn_has_capability): Map the new capability to its wire word. * subversion/libsvn_ra_serf/options.c (capabilities_headers_iterator_callback, options_response_handler): Parse and pre-seed the new capability. * subversion/mod_dav_svn/activity.c (dav_svn__create_txn), subversion/mod_dav_svn/lock.c (append_locks), subversion/svnserve/serve.c (commit): Track the API revs; no functional change. * subversion/svnsync/svnsync.c once per sync how revprops split between the commit and a follow-up revprop update. (replay_rev_started, replay_rev_finished, do_synchronize): Use that decision; skip the follow-up pass entirely when the commit carries author/date. * subversion/tests/libsvn_repos/repos-test.c (commit_add_file, commit_editor_preserve_revprops): New test for both SVN_FS_TXN_CLIENT_DATE polarities via the commit editor. * subversion/tests/cmdline/svnsync_tests.py (sync_to_local_preserves_author_date): New test: an ra_local sync preserves author/date with no post-commit revprop copy. Modified: subversion/branches/svnsync-atomic-author/subversion/include/private/svn_repos_private.h subversion/branches/svnsync-atomic-author/subversion/include/svn_dav.h subversion/branches/svnsync-atomic-author/subversion/include/svn_ra.h subversion/branches/svnsync-atomic-author/subversion/include/svn_ra_svn.h subversion/branches/svnsync-atomic-author/subversion/include/svn_repos.h subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_local/ra_plugin.c subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_serf/options.c subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_svn/client.c subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/commit.c subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/deprecated.c subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/fs-wrap.c subversion/branches/svnsync-atomic-author/subversion/mod_dav_svn/activity.c subversion/branches/svnsync-atomic-author/subversion/mod_dav_svn/lock.c subversion/branches/svnsync-atomic-author/subversion/svnserve/serve.c subversion/branches/svnsync-atomic-author/subversion/svnsync/svnsync.c subversion/branches/svnsync-atomic-author/subversion/tests/cmdline/svnsync_tests.py subversion/branches/svnsync-atomic-author/subversion/tests/libsvn_repos/repos-test.c Modified: subversion/branches/svnsync-atomic-author/subversion/include/private/svn_repos_private.h ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/include/private/svn_repos_private.h Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/include/private/svn_repos_private.h Fri Jul 3 20:59:45 2026 (r1935862) @@ -131,7 +131,13 @@ svn_repos__fs_type(const char **fs_type, apr_pool_t *pool); -/* Create a commit editor for REPOS, based on REVISION. */ +/* Create a commit editor for REPOS, based on REVISION. + * + * TXN_FLAGS are additional transaction flags (e.g. SVN_FS_TXN_CLIENT_DATE) + * OR-ed with SVN_FS_TXN_CHECK_LOCKS, which is always applied. When + * SVN_FS_TXN_CLIENT_DATE is set, a caller-supplied SVN_PROP_REVISION_DATE + * in REVPROPS is preserved instead of being overwritten at commit time. + */ svn_error_t * svn_repos__get_commit_ev2(svn_editor_t **editor, svn_repos_t *repos, @@ -139,6 +145,7 @@ svn_repos__get_commit_ev2(svn_editor_t * const char *authz_repos_name, const char *authz_user, apr_hash_t *revprops, + apr_uint32_t txn_flags, svn_commit_callback2_t commit_cb, void *commit_baton, svn_cancel_func_t cancel_func, Modified: subversion/branches/svnsync-atomic-author/subversion/include/svn_dav.h ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/include/svn_dav.h Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/include/svn_dav.h Fri Jul 3 20:59:45 2026 (r1935862) @@ -422,6 +422,16 @@ extern "C" { #define SVN_DAV_NS_DAV_SVN_PUT_RESULT_CHECKSUM\ SVN_DAV_PROP_NS_DAV "svn/put-result-checksum" +/** Presence of this in a DAV header in an OPTIONS response indicates + * that the transmitter (in this case, the server) preserves a + * client-supplied svn:author and svn:date in a commit's revision + * properties instead of overwriting them at commit time. + * + * @since New in 1.16. + */ +#define SVN_DAV_NS_DAV_SVN_COMMIT_PRESERVES_AUTHOR_DATE\ + SVN_DAV_PROP_NS_DAV "svn/commit-preserves-author-date" + /** @} */ /** @} */ Modified: subversion/branches/svnsync-atomic-author/subversion/include/svn_ra.h ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/include/svn_ra.h Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/include/svn_ra.h Fri Jul 3 20:59:45 2026 (r1935862) @@ -983,7 +983,10 @@ svn_ra_rev_prop(svn_ra_session_t *sessio * names to @c svn_string_t property values. The commit log message * is expected to be in the @c SVN_PROP_REVISION_LOG element. @a * revprop_table can not contain either of @c SVN_PROP_REVISION_DATE - * or @c SVN_PROP_REVISION_AUTHOR. + * or @c SVN_PROP_REVISION_AUTHOR, unless @a session advertises + * #SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE. If it does, both + * properties may be supplied and will be preserved verbatim as the + * new revision's svn:date and svn:author (since 1.16). * * Before @c close_edit returns, but after the commit has succeeded, * it will invoke @a commit_callback (if non-NULL) with filled-in @@ -2288,6 +2291,21 @@ svn_ra_has_capability(svn_ra_session_t * */ #define SVN_RA_CAPABILITY_LIST "list" +/** + * The capability of a destination to preserve a client-supplied + * svn:author and svn:date passed in a commit's revision properties, + * rather than overwriting them with the committing user and the commit + * time. This is useful for tools like svnsync that need to preserve + * the original author and date from the source repository. + * + * @note This is distinct from #SVN_RA_CAPABILITY_COMMIT_REVPROPS, which + * only indicates that arbitrary revision properties may be carried in a + * commit; svn:author and svn:date are still overwritten in that case. + * + * @since New in 1.16. + */ +#define SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE \ + "commit-preserves-author-date" /* *** PLEASE READ THIS IF YOU ADD A NEW CAPABILITY *** * Modified: subversion/branches/svnsync-atomic-author/subversion/include/svn_ra_svn.h ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/include/svn_ra_svn.h Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/include/svn_ra_svn.h Fri Jul 3 20:59:45 2026 (r1935862) @@ -71,6 +71,9 @@ extern "C" { #define SVN_RA_SVN_CAP_GET_FILE_REVS_REVERSE "file-revs-reverse" /* maps to SVN_RA_CAPABILITY_LIST */ #define SVN_RA_SVN_CAP_LIST "list" +/* maps to SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE */ +#define SVN_RA_SVN_CAP_COMMIT_PRESERVES_AUTHOR_DATE \ + "commit-preserves-author-date" /** ra_svn passes @c svn_dirent_t fields over the wire as a list of Modified: subversion/branches/svnsync-atomic-author/subversion/include/svn_repos.h ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/include/svn_repos.h Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/include/svn_repos.h Fri Jul 3 20:59:45 2026 (r1935862) @@ -1540,8 +1540,12 @@ svn_repos_replay(svn_fs_root_t *root, * of the commit transaction, including author and log message if * present. * - * @note #SVN_PROP_REVISION_DATE may be present in @a revprop_table, but - * it will be overwritten when the transaction is committed. + * @a txn_flags is passed to svn_repos_fs_begin_txn_for_commit3() when + * creating a new transaction. + * + * @note If #SVN_FS_TXN_CLIENT_DATE is included in @a txn_flags, the + * #SVN_PROP_REVISION_DATE in @a revprop_table will be preserved as + * the commit date; otherwise it will be overwritten with the commit time. * * Iff @a authz_callback is provided, check read/write authorizations * on paths accessed by editor operations. An operation which fails @@ -1550,6 +1554,9 @@ svn_repos_replay(svn_fs_root_t *root, * * Calling @a (*editor)->close_edit completes the commit. * + * @a commit_callback may be @c NULL if the caller does not need to be + * notified of the commit result. + * * If @a commit_callback is non-NULL, then before @c close_edit returns (but * after the commit has succeeded) @c close_edit will invoke * @a commit_callback with a filled-in #svn_commit_info_t *, @a commit_baton, @@ -1566,7 +1573,7 @@ svn_repos_replay(svn_fs_root_t *root, * NULL). Callers who supply their own transactions are responsible * for cleaning them up (either by committing them, or aborting them). * - * @since New in 1.5. Since 1.6, @a commit_callback can be @c NULL. + * @since New in 1.16. * * @note Yes, @a repos_url_decoded is a <em>decoded</em> URL. We realize * that's sorta wonky. Sorry about that. @@ -1576,6 +1583,32 @@ svn_repos_replay(svn_fs_root_t *root, * methods is a full, URI-encoded URL, not a relative path. */ svn_error_t * +svn_repos_get_commit_editor6(const svn_delta_editor_t **editor, + void **edit_baton, + svn_repos_t *repos, + svn_fs_txn_t *txn, + const char *repos_url_decoded, + const char *base_path, + apr_hash_t *revprop_table, + apr_uint32_t txn_flags, + svn_commit_callback2_t commit_callback, + void *commit_baton, + svn_repos_authz_callback_t authz_callback, + void *authz_baton, + apr_pool_t *pool); + +/** + * Similar to svn_repos_get_commit_editor6(), but with @a txn_flags + * always set to 0. + * + * @note #SVN_PROP_REVISION_DATE may be present in @a revprop_table, but + * it will be overwritten when the transaction is committed. + * + * @since New in 1.5. Since 1.6, @a commit_callback can be @c NULL. + * @deprecated Provided for backward compatibility with the 1.15 API. + */ +SVN_DEPRECATED +svn_error_t * svn_repos_get_commit_editor5(const svn_delta_editor_t **editor, void **edit_baton, svn_repos_t *repos, @@ -2510,16 +2543,42 @@ svn_repos_fs_commit_txn(const char **con * repository object which contains the filesystem. @a rev, @a * *txn_p, and @a pool are as in svn_fs_begin_txn(). * + * @a txn_flags are additional transaction flags OR-ed with + * #SVN_FS_TXN_CHECK_LOCKS, which is always applied; passing 0 therefore + * yields the same lock-checking behavior as the older + * svn_repos_fs_begin_txn_for_commit2(). + * * Before a txn is created, the repository's start-commit hooks are * run; if any of them fail, no txn is created, @a *txn_p is unaffected, * and #SVN_ERR_REPOS_HOOK_FAILURE is returned. * * @note @a revprop_table may contain an #SVN_PROP_REVISION_DATE property, + * which will be set on the transaction. If #SVN_FS_TXN_CLIENT_DATE is + * included in @a txn_flags, this date will be preserved when the transaction + * is committed; otherwise it will be overwritten with the commit time. + * + * @since New in 1.16. + */ +svn_error_t * +svn_repos_fs_begin_txn_for_commit3(svn_fs_txn_t **txn_p, + svn_repos_t *repos, + svn_revnum_t rev, + apr_hash_t *revprop_table, + apr_uint32_t txn_flags, + apr_pool_t *pool); + + +/** Like svn_repos_fs_begin_txn_for_commit3(), but with @a txn_flags + * set to #SVN_FS_TXN_CHECK_LOCKS only. + * + * @note @a revprop_table may contain an #SVN_PROP_REVISION_DATE property, * which will be set on the transaction, but that will be overwritten * when the transaction is committed. * * @since New in 1.5. + * @deprecated Provided for backward compatibility with the 1.15 API. */ +SVN_DEPRECATED svn_error_t * svn_repos_fs_begin_txn_for_commit2(svn_fs_txn_t **txn_p, svn_repos_t *repos, Modified: subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_local/ra_plugin.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_local/ra_plugin.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_local/ra_plugin.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -861,6 +861,28 @@ remap_commit_callback(svn_commit_callbac } } +/* Prepare REVPROPS, a mutable revprop hash owned by the caller, for an + ra_local commit and return the transaction flags it requires. + + When the caller did not supply an svn:author, default it to USERNAME. + Return SVN_FS_TXN_CLIENT_DATE iff the caller supplied an svn:date, so the + filesystem preserves that date instead of stamping the commit time. + + This lets tools like svnsync carry the original author and date through + the commit itself. Any allocations are made in POOL. */ +static apr_uint32_t +prepare_commit_revprops(apr_hash_t *revprops, + const char *username, + apr_pool_t *pool) +{ + if (! svn_hash_gets(revprops, SVN_PROP_REVISION_AUTHOR)) + svn_hash_sets(revprops, SVN_PROP_REVISION_AUTHOR, + svn_string_create(username, pool)); + + return svn_hash_gets(revprops, SVN_PROP_REVISION_DATE) + ? SVN_FS_TXN_CLIENT_DATE : 0; +} + static svn_error_t * svn_ra_local__get_commit_editor(svn_ra_session_t *session, const svn_delta_editor_t **editor, @@ -874,6 +896,7 @@ svn_ra_local__get_commit_editor(svn_ra_s { svn_ra_local__session_baton_t *sess = session->priv; struct deltify_etc_baton *deb = apr_palloc(pool, sizeof(*deb)); + apr_uint32_t txn_flags; /* Set repos_root_url in commit info */ remap_commit_callback(&callback, &callback_baton, session, @@ -896,20 +919,20 @@ svn_ra_local__get_commit_editor(svn_ra_s SVN_ERR(apply_lock_tokens(sess->fs, sess->fs_path->data, lock_tokens, session->pool, pool)); - /* Copy the revprops table so we can add the username. */ + /* Copy the revprops table so we can add/modify properties. */ revprop_table = apr_hash_copy(pool, revprop_table); - svn_hash_sets(revprop_table, SVN_PROP_REVISION_AUTHOR, - svn_string_create(sess->username, pool)); + txn_flags = prepare_commit_revprops(revprop_table, sess->username, pool); + svn_hash_sets(revprop_table, SVN_PROP_TXN_CLIENT_COMPAT_VERSION, svn_string_create(SVN_VER_NUMBER, pool)); svn_hash_sets(revprop_table, SVN_PROP_TXN_USER_AGENT, svn_string_create(sess->useragent, pool)); /* Get the repos commit-editor */ - return svn_repos_get_commit_editor5 + return svn_repos_get_commit_editor6 (editor, edit_baton, sess->repos, NULL, svn_path_uri_decode(sess->repos_url, pool), sess->fs_path->data, - revprop_table, deltify_etc, deb, NULL, NULL, pool); + revprop_table, txn_flags, deltify_etc, deb, NULL, NULL, pool); } @@ -1674,6 +1697,7 @@ svn_ra_local__has_capability(svn_ra_sess || strcmp(capability, SVN_RA_CAPABILITY_EPHEMERAL_TXNPROPS) == 0 || strcmp(capability, SVN_RA_CAPABILITY_GET_FILE_REVS_REVERSE) == 0 || strcmp(capability, SVN_RA_CAPABILITY_LIST) == 0 + || strcmp(capability, SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE) == 0 ) { *has = TRUE; @@ -1777,6 +1801,7 @@ svn_ra_local__get_commit_ev2(svn_editor_ { svn_ra_local__session_baton_t *sess = session->priv; struct deltify_etc_baton *deb = apr_palloc(result_pool, sizeof(*deb)); + apr_uint32_t txn_flags; remap_commit_callback(&commit_cb, &commit_baton, session, commit_cb, commit_baton, result_pool); @@ -1803,15 +1828,14 @@ svn_ra_local__get_commit_ev2(svn_editor_ SVN_ERR(apply_lock_tokens(sess->fs, sess->fs_path->data, lock_tokens, session->pool, scratch_pool)); - /* Copy the REVPROPS and insert the author/username. */ + /* Copy the REVPROPS so we can add/modify properties. */ revprops = apr_hash_copy(scratch_pool, revprops); - svn_hash_sets(revprops, SVN_PROP_REVISION_AUTHOR, - svn_string_create(sess->username, scratch_pool)); + txn_flags = prepare_commit_revprops(revprops, sess->username, scratch_pool); return svn_error_trace(svn_repos__get_commit_ev2( editor, sess->repos, NULL /* authz */, NULL /* authz_repos_name */, NULL /* authz_user */, - revprops, + revprops, txn_flags, deltify_etc, deb, cancel_func, cancel_baton, result_pool, scratch_pool)); } Modified: subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_serf/options.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_serf/options.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_serf/options.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -251,6 +251,13 @@ capabilities_headers_iterator_callback(v { session->supports_put_result_checksum = TRUE; } + if (svn_cstring_match_list( + SVN_DAV_NS_DAV_SVN_COMMIT_PRESERVES_AUTHOR_DATE, vals)) + { + svn_hash_sets(session->capabilities, + SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE, + capability_yes); + } } /* SVN-specific headers -- if present, server supports HTTP protocol v2 */ @@ -396,6 +403,9 @@ options_response_handler(serf_request_t capability_no); svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_LIST, capability_no); + svn_hash_sets(session->capabilities, + SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE, + capability_no); /* Then see which ones we can discover. */ serf_bucket_headers_do(hdrs, capabilities_headers_iterator_callback, Modified: subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_svn/client.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_svn/client.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/libsvn_ra_svn/client.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -3079,6 +3079,8 @@ ra_svn_has_capability(svn_ra_session_t * {SVN_RA_CAPABILITY_GET_FILE_REVS_REVERSE, SVN_RA_SVN_CAP_GET_FILE_REVS_REVERSE}, {SVN_RA_CAPABILITY_LIST, SVN_RA_SVN_CAP_LIST}, + {SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE, + SVN_RA_SVN_CAP_COMMIT_PRESERVES_AUTHOR_DATE}, {NULL, NULL} /* End of list marker */ }; Modified: subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/commit.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/commit.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/commit.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -88,6 +88,10 @@ struct edit_baton /* Does this set of interfaces 'own' the commit transaction? */ svn_boolean_t txn_owner; + /* Flags to pass to svn_repos_fs_begin_txn_for_commit3() when + creating the transaction (e.g., SVN_FS_TXN_CLIENT_DATE). */ + apr_uint32_t txn_flags; + /* svn transaction associated with this edit (created in open_root, or supplied by the public API caller). */ svn_fs_txn_t *txn; @@ -434,10 +438,11 @@ open_root(void *edit_baton, make our own. */ if (eb->txn_owner) { - SVN_ERR(svn_repos_fs_begin_txn_for_commit2(&(eb->txn), + SVN_ERR(svn_repos_fs_begin_txn_for_commit3(&(eb->txn), eb->repos, youngest, eb->revprop_table, + eb->txn_flags, eb->pool)); } else /* Even if we aren't the owner of the transaction, we might @@ -992,13 +997,14 @@ fetch_base_func(const char **filename, /*** Public interfaces. ***/ svn_error_t * -svn_repos_get_commit_editor5(const svn_delta_editor_t **editor, +svn_repos_get_commit_editor6(const svn_delta_editor_t **editor, void **edit_baton, svn_repos_t *repos, svn_fs_txn_t *txn, const char *repos_url_decoded, const char *base_path, apr_hash_t *revprop_table, + apr_uint32_t txn_flags, svn_commit_callback2_t commit_callback, void *commit_baton, svn_repos_authz_callback_t authz_callback, @@ -1058,6 +1064,7 @@ svn_repos_get_commit_editor5(const svn_d eb->fs = svn_repos_fs(repos); eb->txn = txn; eb->txn_owner = txn == NULL; + eb->txn_flags = txn_flags; *edit_baton = eb; *editor = e; @@ -1377,6 +1384,7 @@ svn_repos__get_commit_ev2(svn_editor_t * const char *authz_repos_name, const char *authz_user, apr_hash_t *revprops, + apr_uint32_t txn_flags, svn_commit_callback2_t commit_cb, void *commit_baton, svn_cancel_func_t cancel_func, @@ -1420,7 +1428,7 @@ svn_repos__get_commit_ev2(svn_editor_t * eb->commit_baton = commit_baton; SVN_ERR(svn_fs__editor_create(&eb->inner, &eb->txn_name, - repos->fs, SVN_FS_TXN_CHECK_LOCKS, + repos->fs, SVN_FS_TXN_CHECK_LOCKS | txn_flags, cancel_func, cancel_baton, result_pool, scratch_pool)); Modified: subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/deprecated.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/deprecated.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/deprecated.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -46,6 +46,27 @@ /*** From commit.c ***/ svn_error_t * +svn_repos_get_commit_editor5(const svn_delta_editor_t **editor, + void **edit_baton, + svn_repos_t *repos, + svn_fs_txn_t *txn, + const char *repos_url_decoded, + const char *base_path, + apr_hash_t *revprop_table, + svn_commit_callback2_t commit_callback, + void *commit_baton, + svn_repos_authz_callback_t authz_callback, + void *authz_baton, + apr_pool_t *pool) +{ + return svn_repos_get_commit_editor6(editor, edit_baton, repos, txn, + repos_url_decoded, base_path, + revprop_table, 0, + commit_callback, commit_baton, + authz_callback, authz_baton, pool); +} + +svn_error_t * svn_repos_get_commit_editor4(const svn_delta_editor_t **editor, void **edit_baton, svn_repos_t *repos, @@ -411,6 +432,36 @@ svn_repos_replay(svn_fs_root_t *root, /*** From fs-wrap.c ***/ svn_error_t * +svn_repos_fs_begin_txn_for_commit2(svn_fs_txn_t **txn_p, + svn_repos_t *repos, + svn_revnum_t rev, + apr_hash_t *revprop_table, + apr_pool_t *pool) +{ + return svn_repos_fs_begin_txn_for_commit3(txn_p, repos, rev, revprop_table, + 0, pool); +} + +svn_error_t * +svn_repos_fs_begin_txn_for_commit(svn_fs_txn_t **txn_p, + svn_repos_t *repos, + svn_revnum_t rev, + const char *author, + const char *log_msg, + apr_pool_t *pool) +{ + apr_hash_t *revprop_table = apr_hash_make(pool); + if (author) + svn_hash_sets(revprop_table, SVN_PROP_REVISION_AUTHOR, + svn_string_create(author, pool)); + if (log_msg) + svn_hash_sets(revprop_table, SVN_PROP_REVISION_LOG, + svn_string_create(log_msg, pool)); + return svn_repos_fs_begin_txn_for_commit2(txn_p, repos, rev, revprop_table, + pool); +} + +svn_error_t * svn_repos_fs_change_rev_prop3(svn_repos_t *repos, svn_revnum_t rev, const char *author, Modified: subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/fs-wrap.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/fs-wrap.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/libsvn_repos/fs-wrap.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -129,10 +129,11 @@ svn_repos_fs_commit_txn(const char **con svn_error_t * -svn_repos_fs_begin_txn_for_commit2(svn_fs_txn_t **txn_p, +svn_repos_fs_begin_txn_for_commit3(svn_fs_txn_t **txn_p, svn_repos_t *repos, svn_revnum_t rev, apr_hash_t *revprop_table, + apr_uint32_t txn_flags, apr_pool_t *pool) { apr_array_header_t *revprops; @@ -149,7 +150,7 @@ svn_repos_fs_begin_txn_for_commit2(svn_f /* Begin the transaction, ask for the fs to do on-the-fly lock checks. We fetch its name, too, so the start-commit hook can use it. */ SVN_ERR(svn_fs_begin_txn2(&txn, repos->fs, rev, - SVN_FS_TXN_CHECK_LOCKS, pool)); + SVN_FS_TXN_CHECK_LOCKS | txn_flags, pool)); err = svn_fs_txn_name(&txn_name, txn, pool); if (err) return svn_error_compose_create(err, svn_fs_abort_txn(txn, pool)); @@ -176,26 +177,6 @@ svn_repos_fs_begin_txn_for_commit2(svn_f } -svn_error_t * -svn_repos_fs_begin_txn_for_commit(svn_fs_txn_t **txn_p, - svn_repos_t *repos, - svn_revnum_t rev, - const char *author, - const char *log_msg, - apr_pool_t *pool) -{ - apr_hash_t *revprop_table = apr_hash_make(pool); - if (author) - svn_hash_sets(revprop_table, SVN_PROP_REVISION_AUTHOR, - svn_string_create(author, pool)); - if (log_msg) - svn_hash_sets(revprop_table, SVN_PROP_REVISION_LOG, - svn_string_create(log_msg, pool)); - return svn_repos_fs_begin_txn_for_commit2(txn_p, repos, rev, revprop_table, - pool); -} - - /*** Property wrappers ***/ svn_error_t * Modified: subversion/branches/svnsync-atomic-author/subversion/mod_dav_svn/activity.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/mod_dav_svn/activity.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/mod_dav_svn/activity.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -256,8 +256,9 @@ dav_svn__create_txn(dav_svn_repos *repos repos->pool); } - serr = svn_repos_fs_begin_txn_for_commit2(&txn, repos->repos, rev, - revprops, repos->pool); + serr = svn_repos_fs_begin_txn_for_commit3(&txn, repos->repos, rev, + revprops, 0 /* flags */, + repos->pool); if (serr != NULL) { return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, Modified: subversion/branches/svnsync-atomic-author/subversion/mod_dav_svn/lock.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/mod_dav_svn/lock.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/mod_dav_svn/lock.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -729,8 +729,9 @@ append_locks(dav_lockdb *lockdb, "Could not determine youngest revision", resource->pool); - if ((serr = svn_repos_fs_begin_txn_for_commit2(&txn, repos->repos, rev, + if ((serr = svn_repos_fs_begin_txn_for_commit3(&txn, repos->repos, rev, revprop_table, + 0 /* flags */, resource->pool))) return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, "Could not begin a transaction", Modified: subversion/branches/svnsync-atomic-author/subversion/svnserve/serve.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/svnserve/serve.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/svnserve/serve.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -1547,11 +1547,11 @@ commit(svn_ra_svn_conn_t *conn, ccb.date = &date; ccb.author = &author; ccb.post_commit_err = &post_commit_err; - /* ### Note that svn_repos_get_commit_editor5 actually wants a decoded URL. */ - SVN_CMD_ERR(svn_repos_get_commit_editor5 + /* ### Note that svn_repos_get_commit_editor6 actually wants a decoded URL. */ + SVN_CMD_ERR(svn_repos_get_commit_editor6 (&editor, &edit_baton, b->repository->repos, NULL, svn_path_uri_decode(b->repository->repos_url, pool), - b->repository->fs_path->data, revprop_table, + b->repository->fs_path->data, revprop_table, 0, commit_done, &ccb, authz_commit_cb, &ab, pool)); SVN_ERR(svn_ra_svn__write_cmd_response(conn, pool, "")); Modified: subversion/branches/svnsync-atomic-author/subversion/svnsync/svnsync.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/svnsync/svnsync.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/svnsync/svnsync.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -494,6 +494,14 @@ check_if_session_is_at_repos_root(svn_ra } +/* Return TRUE iff KEY names a property in the svnsync property namespace. */ +static svn_boolean_t +is_svnsync_prop(const char *key) +{ + return strncmp(key, SVNSYNC_PROP_PREFIX, + sizeof(SVNSYNC_PROP_PREFIX) - 1) == 0; +} + /* Remove the properties in TARGET_PROPS but not in SOURCE_PROPS from * revision REV of the repository associated with RA session SESSION. * @@ -519,8 +527,7 @@ remove_props_not_in_source(svn_ra_sessio svn_pool_clear(subpool); - if (rev == 0 && !strncmp(propname, SVNSYNC_PROP_PREFIX, - sizeof(SVNSYNC_PROP_PREFIX) - 1)) + if (rev == 0 && is_svnsync_prop(propname)) continue; /* Delete property if the name can't be found in SOURCE_PROPS. */ @@ -611,8 +618,7 @@ write_revprops(int *filtered_count, svn_pool_clear(subpool); - if (strncmp(propname, SVNSYNC_PROP_PREFIX, - sizeof(SVNSYNC_PROP_PREFIX) - 1) != 0) + if (! is_svnsync_prop(propname)) { if (old_rev_props) { @@ -1027,8 +1033,17 @@ typedef struct replay_baton_t { svn_ra_session_t *to_session; svn_revnum_t current_revision; subcommand_baton_t *sb; - svn_boolean_t has_commit_revprops_capability; svn_boolean_t has_atomic_revprops_capability; + + /* How each revision's properties are split between the commit and a + follow-up revprop update; resolved once by resolve_revprop_handling(), + which see. Each filter returns TRUE for the properties to *exclude* + from its pass. POST_COMMIT_FILTER is NULL when no follow-up pass is + needed. Properties in the svnsync bookkeeping namespace (svn:sync-*) + are never mirrored by either pass. */ + filter_func_t commit_filter; + filter_func_t post_commit_filter; + int normalized_rev_props_count; int normalized_node_props_count; const char *to_root; @@ -1070,15 +1085,9 @@ make_replay_baton(replay_baton_t **baton static svn_boolean_t filter_exclude_date_author_sync(const char *key) { - if (strcmp(key, SVN_PROP_REVISION_AUTHOR) == 0) - return TRUE; - else if (strcmp(key, SVN_PROP_REVISION_DATE) == 0) - return TRUE; - else if (strncmp(key, SVNSYNC_PROP_PREFIX, - sizeof(SVNSYNC_PROP_PREFIX) - 1) == 0) - return TRUE; - - return FALSE; + return strcmp(key, SVN_PROP_REVISION_AUTHOR) == 0 + || strcmp(key, SVN_PROP_REVISION_DATE) == 0 + || is_svnsync_prop(key); } /* Return FALSE iff KEY is the name of an svn:date or svn:author or any svnsync @@ -1091,7 +1100,6 @@ filter_include_date_author_sync(const ch return ! filter_exclude_date_author_sync(key); } - /* Return TRUE iff KEY is the name of the svn:log property. * Implements filter_func_t. Use with filter_props() to only exclude svn:log. */ @@ -1113,6 +1121,45 @@ filter_include_log(const char *key) return ! filter_exclude_log(key); } +/* Resolve, from the destination capabilities HAS_COMMIT_PRESERVES_AUTHOR_DATE + * and HAS_COMMIT_REVPROPS, how each revision's properties are divided between + * the commit itself and a follow-up revprop update, storing the decision on + * RB. RB->COMMIT_FILTER excludes properties from the commit. + * RB->POST_COMMIT_FILTER excludes properties from the follow-up update, or + * is NULL when no follow-up update is needed at all. The two filters are + * not exact complements: properties in the svnsync bookkeeping namespace + * (svn:sync-*) are excluded from both passes, since they describe the + * mirror relationship itself and must not be mirrored as payload. */ +static void +resolve_revprop_handling(replay_baton_t *rb, + svn_boolean_t has_commit_preserves_author_date, + svn_boolean_t has_commit_revprops) +{ + if (has_commit_preserves_author_date) + { + /* The destination preserves a client-supplied svn:author/svn:date in + the commit (e.g. file:// via ra_local), so commit every property + except the svnsync bookkeeping ones and leave nothing for a + follow-up. */ + rb->commit_filter = is_svnsync_prop; + rb->post_commit_filter = NULL; + } + else if (has_commit_revprops) + { + /* The destination accepts revprops in the commit but overwrites + svn:author/svn:date, so those are copied afterwards. */ + rb->commit_filter = filter_exclude_date_author_sync; + rb->post_commit_filter = filter_include_date_author_sync; + } + else + { + /* The destination only carries svn:log through the commit; every other + property is copied afterwards. */ + rb->commit_filter = filter_include_log; + rb->post_commit_filter = filter_exclude_log; + } +} + #ifdef ENABLE_EV2_SHIMS static svn_error_t * fetch_base_func(const char **filename, @@ -1281,18 +1328,12 @@ replay_rev_started(svn_revnum_t revision svn_string_createf(pool, "%ld", revision), pool)); - /* The actual copy is just a replay hooked up to a commit. Include - all the revision properties from the source repositories, except - 'svn:author' and 'svn:date', those are not guaranteed to get - through the editor anyway. - If we're syncing to an non-commit-revprops capable server, filter - out all revprops except svn:log and add them later in - revplay_rev_finished. */ - filtered = filter_props(&filtered_count, rev_props, - (rb->has_commit_revprops_capability - ? filter_exclude_date_author_sync - : filter_include_log), - pool); + /* The actual copy is just a replay hooked up to a commit. Send through + the commit editor whichever revision properties the destination can + carry in the commit itself; rb->commit_filter encodes that choice (see + resolve_revprop_handling()). Any properties it leaves behind are added + later in replay_rev_finished(). */ + filtered = filter_props(&filtered_count, rev_props, rb->commit_filter, pool); /* svn_ra_get_commit_editor3 requires the log message to be set. It's possible that we didn't receive 'svn:log' here, so we @@ -1353,9 +1394,8 @@ replay_rev_finished(svn_revnum_t revisio { apr_pool_t *subpool = svn_pool_create(pool); replay_baton_t *rb = replay_baton; - apr_hash_t *filtered, *existing_props; - int filtered_count; - int normalized_count; + apr_hash_t *existing_props; + int filtered_count = 0; const svn_string_t *rev_str; SVN_ERR(editor->close_edit(edit_baton, pool)); @@ -1371,24 +1411,28 @@ replay_rev_finished(svn_revnum_t revisio subpool)); - /* Ok, we're done with the data, now we just need to copy the remaining - 'svn:date' and 'svn:author' revprops and we're all set. - If the server doesn't support revprops-in-a-commit, we still have to - set all revision properties except svn:log. */ - filtered = filter_props(&filtered_count, rev_props, - (rb->has_commit_revprops_capability - ? filter_include_date_author_sync - : filter_exclude_log), - subpool); + /* Now copy whatever revision properties the commit could not carry. + rb->post_commit_filter excludes the properties already handled in + replay_rev_started(); it is NULL when no follow-up update is needed at + all. See resolve_revprop_handling(). */ + if (rb->post_commit_filter) + { + apr_hash_t *filtered; + int normalized_count; - /* If necessary, normalize encoding and line ending style, and add the number - of EOL-normalized properties to the overall count in the replay baton. */ - SVN_ERR(svnsync_normalize_revprops(filtered, &normalized_count, - rb->sb->source_prop_encoding, pool)); - rb->normalized_rev_props_count += normalized_count; + filtered = filter_props(&filtered_count, rev_props, + rb->post_commit_filter, subpool); - SVN_ERR(write_revprops(&filtered_count, rb->to_session, revision, filtered, - NULL, subpool)); + /* If necessary, normalize encoding and line ending style, and add the + number of EOL-normalized properties to the overall count in the + replay baton. */ + SVN_ERR(svnsync_normalize_revprops(filtered, &normalized_count, + rb->sb->source_prop_encoding, pool)); + rb->normalized_rev_props_count += normalized_count; + + SVN_ERR(write_revprops(&filtered_count, rb->to_session, revision, + filtered, NULL, subpool)); + } /* Remove all extra properties in TARGET. */ SVN_ERR(remove_props_not_in_source(rb->to_session, revision, @@ -1415,8 +1459,11 @@ replay_rev_finished(svn_revnum_t revisio ? &rev_str : NULL, NULL, subpool)); - /* Notify the user that we copied revision properties. */ - if (! rb->sb->quiet) + /* Notify the user that we copied revision properties, unless the commit + itself carried them (post_commit_filter is NULL), in which case + commit_callback already printed "Committed revision X." and there is + nothing more to report. */ + if (! rb->sb->quiet && rb->post_commit_filter) SVN_ERR(log_properties_copied(filtered_count > 0, revision, subpool)); svn_pool_destroy(subpool); @@ -1441,6 +1488,7 @@ do_synchronize(svn_ra_session_t *to_sess svn_revnum_t to_latest, copying, last_merged; svn_revnum_t start_revision, end_revision; replay_baton_t *rb; + svn_boolean_t has_commit_revprops, has_commit_preserves_author_date; int normalized_rev_props_count = 0; SVN_ERR(open_source_session(&from_session, &last_merged_rev, @@ -1541,8 +1589,7 @@ do_synchronize(svn_ra_session_t *to_sess /* For compatibility with older svnserve versions, check first if we support adding revprops to the commit. */ - SVN_ERR(svn_ra_has_capability(rb->to_session, - &rb->has_commit_revprops_capability, + SVN_ERR(svn_ra_has_capability(rb->to_session, &has_commit_revprops, SVN_RA_CAPABILITY_COMMIT_REVPROPS, pool)); @@ -1551,6 +1598,24 @@ do_synchronize(svn_ra_session_t *to_sess SVN_RA_CAPABILITY_ATOMIC_REVPROPS, pool)); + /* Check if the destination supports setting author/date directly in + the commit */ + SVN_ERR(svn_ra_has_capability(rb->to_session, + &has_commit_preserves_author_date, + SVN_RA_CAPABILITY_COMMIT_PRESERVES_AUTHOR_DATE, + pool)); + + /* Decide how each revision's properties are split between the + commit and a follow-up revprop update. */ + resolve_revprop_handling(rb, has_commit_preserves_author_date, + has_commit_revprops); + + if (! baton->quiet && has_commit_preserves_author_date) + SVN_ERR(svn_cmdline_printf(pool, + _("Destination supports commit-time author " + "and date; no post-commit revprop sync " + "needed.\n"))); + start_revision = last_merged + 1; end_revision = from_latest; Modified: subversion/branches/svnsync-atomic-author/subversion/tests/cmdline/svnsync_tests.py ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/tests/cmdline/svnsync_tests.py Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/tests/cmdline/svnsync_tests.py Fri Jul 3 20:59:45 2026 (r1935862) @@ -572,6 +572,56 @@ PROPS-END # Compare the dump produced by the mirror repository with expected verify_mirror(dest_sbox, dump_out) +def sync_to_local_preserves_author_date(sbox): + "sync to a local mirror preserves author/date" + + # Source repository with one real revision (the greek tree). + sbox.build(create_wc=False) + + # Capture the source's r1 svn:author and svn:date. + exit_code, src_author, errput = svntest.main.run_svn( + None, "propget", "--revprop", "-r", "1", "svn:author", sbox.repo_url) + exit_code, src_date, errput = svntest.main.run_svn( + None, "propget", "--revprop", "-r", "1", "svn:date", sbox.repo_url) + + # Empty destination mirror, addressed via file:// so ra_local is used. + dest_sbox = sbox.clone_dependent() + dest_sbox.build(create_wc=False, empty=True) + + exit_code, output, errput = svntest.main.run_svnlook("uuid", sbox.repo_dir) + svntest.actions.run_and_verify_svnadmin2(None, None, 0, + 'setuuid', dest_sbox.repo_dir, + output[0][:-1]) + svntest.actions.enable_revprop_changes(dest_sbox.repo_dir) + + dest_repo_url = dest_sbox.file_protocol_repo_url() + run_init(dest_repo_url, sbox.repo_url) + + # Sync. For an ra_local destination svnsync sets svn:author and svn:date + # during the commit itself, so the per-revision "Copied properties ..." + # line must NOT appear. + exit_code, output, errput = svntest.main.run_svnsync( + "synchronize", dest_repo_url, sbox.repo_url) + if errput: + raise SVNUnexpectedStderr(errput) + if [l for l in output if "Copied properties for revision 1" in l]: + raise svntest.Failure("svnsync performed a post-commit revprop copy " + "for an ra_local destination") + + # WITHOUT a separate copy-revprops step, the mirror must already carry the + # original author and date. + exit_code, dst_author, errput = svntest.main.run_svn( + None, "propget", "--revprop", "-r", "1", "svn:author", dest_repo_url) + exit_code, dst_date, errput = svntest.main.run_svn( + None, "propget", "--revprop", "-r", "1", "svn:date", dest_repo_url) + + if dst_author != src_author: + raise svntest.Failure("svn:author not preserved: expected %s, got %s" + % (src_author, dst_author)) + if dst_date != src_date: + raise svntest.Failure("svn:date not preserved: expected %s, got %s" + % (src_date, dst_date)) + def up_to_date_sync(sbox): """sync that does nothing""" @@ -625,6 +675,7 @@ test_list = [ None, fd_leak_sync_from_serf_to_local, # calls setrlimit mergeinfo_contains_r0, up_to_date_sync, + sync_to_local_preserves_author_date, ] if __name__ == '__main__': Modified: subversion/branches/svnsync-atomic-author/subversion/tests/libsvn_repos/repos-test.c ============================================================================== --- subversion/branches/svnsync-atomic-author/subversion/tests/libsvn_repos/repos-test.c Fri Jul 3 20:20:55 2026 (r1935861) +++ subversion/branches/svnsync-atomic-author/subversion/tests/libsvn_repos/repos-test.c Fri Jul 3 20:59:45 2026 (r1935862) @@ -3326,9 +3326,9 @@ prop_validation_commit_with_revprop(cons /* Make an arbitrary change and commit using above values... */ - SVN_ERR(svn_repos_get_commit_editor5(&editor, &edit_baton, repos, + SVN_ERR(svn_repos_get_commit_editor6(&editor, &edit_baton, repos, NULL, "file://test", "/", - revprop_table, + revprop_table, 0, NULL, NULL, NULL, NULL, pool)); SVN_ERR(editor->open_root(edit_baton, 0, pool, &root_baton)); @@ -3411,7 +3411,112 @@ prop_validation(const svn_test_opts_t *o } - +/* Drive the commit editor EDITOR/EDIT_BATON to add a single empty file + named FILENAME at the root, basing the txn on BASE_REV. */ +static svn_error_t * +commit_add_file(const svn_delta_editor_t *editor, + void *edit_baton, + const char *filename, + svn_revnum_t base_rev, + apr_pool_t *pool) +{ + void *root_baton; + void *file_baton; + + SVN_ERR(editor->open_root(edit_baton, base_rev, pool, &root_baton)); + SVN_ERR(editor->add_file(filename, root_baton, NULL, SVN_INVALID_REVNUM, + pool, &file_baton)); + SVN_ERR(editor->close_file(file_baton, NULL, pool)); + SVN_ERR(editor->close_directory(root_baton, pool)); + SVN_ERR(editor->close_edit(edit_baton, pool)); + + return SVN_NO_ERROR; +} + +/* Verify that svn_repos_get_commit_editor6() honors SVN_FS_TXN_CLIENT_DATE: + when the flag is set, a caller-supplied svn:author and svn:date survive + the commit verbatim; when it is not set, svn:date is overwritten with the + commit time. */ +static svn_error_t * +commit_editor_preserve_revprops(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_repos_t *repos; + svn_fs_t *fs; + const svn_delta_editor_t *editor; + void *edit_baton; + apr_hash_t *revprop_table; + svn_revnum_t youngest_rev; + svn_string_t *value; + const char *the_author = "the-original-author"; + /* A fixed date in the past, distinct from the current commit time. */ + const char *the_date = "2000-01-01T00:00:00.000000Z"; + + /* Create a filesystem and repository. */ + SVN_ERR(svn_test__create_repos( + &repos, "test-repo-commit-editor-preserve-revprops", opts, pool)); + fs = svn_repos_fs(repos); + + /* --- Case 1: with SVN_FS_TXN_CLIENT_DATE the date is preserved. --- */ + revprop_table = apr_hash_make(pool); + svn_hash_sets(revprop_table, SVN_PROP_REVISION_AUTHOR, + svn_string_create(the_author, pool)); + svn_hash_sets(revprop_table, SVN_PROP_REVISION_DATE, + svn_string_create(the_date, pool)); + svn_hash_sets(revprop_table, SVN_PROP_REVISION_LOG, + svn_string_create("preserve the date", pool)); + + SVN_ERR(svn_repos_get_commit_editor6(&editor, &edit_baton, repos, NULL, + "file://test", "/", revprop_table, + SVN_FS_TXN_CLIENT_DATE, + NULL, NULL, NULL, NULL, pool)); + SVN_ERR(commit_add_file(editor, edit_baton, "/f1", 0, pool)); + + SVN_ERR(svn_fs_youngest_rev(&youngest_rev, fs, pool)); + + SVN_ERR(svn_fs_revision_prop(&value, fs, youngest_rev, + SVN_PROP_REVISION_AUTHOR, pool)); + SVN_TEST_ASSERT(value != NULL); + SVN_TEST_STRING_ASSERT(value->data, the_author); + + SVN_ERR(svn_fs_revision_prop(&value, fs, youngest_rev, + SVN_PROP_REVISION_DATE, pool)); + SVN_TEST_ASSERT(value != NULL); + SVN_TEST_STRING_ASSERT(value->data, the_date); + + /* --- Case 2: without the flag, svn:date is overwritten. --- */ + revprop_table = apr_hash_make(pool); + svn_hash_sets(revprop_table, SVN_PROP_REVISION_AUTHOR, + svn_string_create(the_author, pool)); + svn_hash_sets(revprop_table, SVN_PROP_REVISION_DATE, + svn_string_create(the_date, pool)); + svn_hash_sets(revprop_table, SVN_PROP_REVISION_LOG, + svn_string_create("overwrite the date", pool)); + + SVN_ERR(svn_repos_get_commit_editor6(&editor, &edit_baton, repos, NULL, + "file://test", "/", revprop_table, + 0 /* no SVN_FS_TXN_CLIENT_DATE */, + NULL, NULL, NULL, NULL, pool)); + SVN_ERR(commit_add_file(editor, edit_baton, "/f2", youngest_rev, pool)); + + SVN_ERR(svn_fs_youngest_rev(&youngest_rev, fs, pool)); + + /* The author is still taken verbatim from the revprop table. */ + SVN_ERR(svn_fs_revision_prop(&value, fs, youngest_rev, + SVN_PROP_REVISION_AUTHOR, pool)); + SVN_TEST_ASSERT(value != NULL); + SVN_TEST_STRING_ASSERT(value->data, the_author); + + /* But the date must have been replaced with the commit time. */ + SVN_ERR(svn_fs_revision_prop(&value, fs, youngest_rev, + SVN_PROP_REVISION_DATE, pool)); + SVN_TEST_ASSERT(value != NULL); + SVN_TEST_ASSERT(strcmp(value->data, the_date) != 0); + + return SVN_NO_ERROR; +} + + /* Tests for svn_repos_get_logsN() */ /* Log receiver which simple increments a counter. */ @@ -4128,8 +4233,8 @@ deprecated_access_context_api(const svn_ SVN_ERR(svn_fs_set_access(svn_repos_fs(repos), access)); /* Commit a new revision. */ - SVN_ERR(svn_repos_fs_begin_txn_for_commit2(&txn, repos, 0, - apr_hash_make(pool), pool)); + SVN_ERR(svn_repos_fs_begin_txn_for_commit3(&txn, repos, 0, + apr_hash_make(pool), 0, pool)); SVN_ERR(svn_fs_txn_root(&root, txn, pool)); SVN_ERR(svn_fs_make_dir(root, "/whatever", pool)); SVN_ERR(svn_repos_fs_commit_txn(&conflict, repos, &new_rev, txn, pool)); @@ -4391,8 +4496,8 @@ commit_aborted_txn(const svn_test_opts_t opts, pool)); /* Create and abort the transaction. */ - SVN_ERR(svn_repos_fs_begin_txn_for_commit2(&txn, repos, 0, - apr_hash_make(pool), pool)); + SVN_ERR(svn_repos_fs_begin_txn_for_commit3(&txn, repos, 0, + apr_hash_make(pool), 0, pool)); SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); SVN_ERR(svn_fs_make_dir(txn_root, "/A", pool)); SVN_ERR(svn_fs_abort_txn(txn, pool)); @@ -4501,6 +4606,8 @@ static struct svn_test_descriptor_t test "test reporter and svn_depth_exclude"), SVN_TEST_OPTS_PASS(prop_validation, "test if revprops are validated by repos"), + SVN_TEST_OPTS_PASS(commit_editor_preserve_revprops, + "test SVN_FS_TXN_CLIENT_DATE in the commit editor"), SVN_TEST_OPTS_PASS(get_logs, "test svn_repos_get_logs ranges and limits"), SVN_TEST_OPTS_PASS(test_get_file_revs,
