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,

Reply via email to