git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH 0/2] replay: add --update-refs option
@ 2025-09-08  4:36 Siddharth Asthana
  2025-09-08  4:36 ` [PATCH 1/2] " Siddharth Asthana
                   ` (5 more replies)
  0 siblings, 6 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-08  4:36 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Christian Couder, Karthik Nayak, Justin Tobler,
	Elijah Newren, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin, Siddharth Asthana

This patch series adds a --update-refs option to git replay. Right now, 
when you use git replay, you need to pipe its output to git update-ref 
like this:

    git replay --onto main topic1..topic2 | git update-ref --stdin

This works fine, but it means running two commands and doesn't give you 
atomic transactions by default. The new --update-refs option lets you do 
the ref updates directly:

    git replay --update-refs --onto main topic1..topic2

I discussed this feature with Christian Couder earlier, and we agreed that 
it would be useful for server-side operations where you want atomic updates.

The way it works:
- By default, it uses atomic transactions (all refs get updated or none do)
- There's a --batch option if you want some updates to succeed even if 
  others fail
- It works with bare repositories, which is important for server operations
  like Gitaly
- When it succeeds, it doesn't print anything (just like git update-ref 
  --stdin)
- You can't use --update-refs with the existing --update option

This should help with git replay's goal of being good for server-side 
operations. It also makes the command simpler to use since you don't need 
the pipeline anymore, and the atomic behavior is better for reliability.

Siddharth Asthana (2):
  replay: add --update-refs option for atomic ref updates
  replay: document --update-refs and --batch options

 Documentation/git-replay.adoc |  62 ++++++-
 builtin/replay.c              | 134 +++++++++++++-
 t/meson.build                 |   1 +
 t/t3650-replay-basics.sh      | 323 ++++++++++++++++++++++++++++++++++
 t/t3651-replay-update-refs.sh | 273 ++++++++++++++++++++++++++++
 5 files changed, 778 insertions(+), 15 deletions(-)
 create mode 100755 t/t3651-replay-update-refs.sh

-- 
2.51.0


^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH 1/2] replay: add --update-refs option
  2025-09-08  4:36 [PATCH 0/2] replay: add --update-refs option Siddharth Asthana
@ 2025-09-08  4:36 ` Siddharth Asthana
  2025-09-08  9:54   ` Patrick Steinhardt
  2025-09-09  7:32   ` Elijah Newren
  2025-09-08  4:36 ` [PATCH 2/2] replay: document --update-refs and --batch options Siddharth Asthana
                   ` (4 subsequent siblings)
  5 siblings, 2 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-08  4:36 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Christian Couder, Karthik Nayak, Justin Tobler,
	Elijah Newren, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin, Siddharth Asthana

Currently, git replay outputs "update" commands that need to be piped to
`git update-ref --stdin`:

    git replay --onto main topic1..topic2 | git update-ref --stdin

While this works, it requires users to run a two-command pipeline and
doesn't provide atomic transaction guarantees by default.

This patch adds --update-refs option that performs ref updates directly
using Git's ref transaction API instead of outputting update commands:

    git replay --update-refs --onto main topic1..topic2

The implementation uses atomic transactions by default (all updates succeed
or all fail) and supports an optional --batch flag for partial failure
tolerance, similar to `git update-ref --stdin`.

The --update-refs option:
- Uses ref_store_transaction_begin() with atomic mode by default
- Supports --batch mode with REF_TRANSACTION_ALLOW_FAILURE flag
- Works with all existing options: --onto, --advance, --contained
- Follows the same patterns as builtin/update-ref.c
- Works with bare repositories (important for server-side operations)
- Produces no output on successful completion

Option validation ensures --update-refs cannot be used with the existing
--update option, and --batch can only be used with --update-refs.

This particularly benefits server-side Git operations (like Gitaly) that
need atomic ref updates, and users who want to avoid the two-command
pipeline for performance or reliability reasons.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 builtin/replay.c              | 134 +++++++++++++-
 t/meson.build                 |   1 +
 t/t3650-replay-basics.sh      | 323 ++++++++++++++++++++++++++++++++++
 t/t3651-replay-update-refs.sh | 273 ++++++++++++++++++++++++++++
 4 files changed, 722 insertions(+), 9 deletions(-)
 create mode 100755 t/t3651-replay-update-refs.sh

diff --git a/builtin/replay.c b/builtin/replay.c
index 6172c8aacc..a33c9887cf 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -284,6 +284,37 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	return create_commit(repo, result->tree, pickme, replayed_base);
 }
 
+static int update_ref_direct(struct repository *repo, const char *refname,
+			     const struct object_id *new_oid,
+			     const struct object_id *old_oid)
+{
+	const char *msg = "replay";
+	return refs_update_ref(get_main_ref_store(repo), msg, refname,
+			       new_oid, old_oid, 0, UPDATE_REFS_MSG_ON_ERR);
+}
+
+static int add_ref_to_transaction(struct ref_transaction *transaction,
+				  const char *refname,
+				  const struct object_id *new_oid,
+				  const struct object_id *old_oid,
+				  struct strbuf *err)
+{
+	return ref_transaction_update(transaction, refname, new_oid, old_oid,
+				      NULL, NULL, 0, "git replay", err);
+}
+
+static void print_rejected_update(const char *refname,
+				  const struct object_id *old_oid,
+				  const struct object_id *new_oid,
+				  const char *old_target,
+				  const char *new_target,
+				  enum ref_transaction_error err,
+				  void *cb_data)
+{
+	const char *reason = ref_transaction_error_msg(err);
+	warning(_("failed to update %s: %s"), refname, reason);
+}
+
 int cmd_replay(int argc,
 	       const char **argv,
 	       const char *prefix,
@@ -294,6 +325,9 @@ int cmd_replay(int argc,
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
 	int contained = 0;
+	int update_directly = 0;
+	int update_refs_flag = 0;
+	int batch_mode = 0;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -302,12 +336,14 @@ int cmd_replay(int argc,
 	struct merge_result result;
 	struct strset *update_refs = NULL;
 	kh_oid_map_t *replayed_commits;
+	struct ref_transaction *transaction = NULL;
+	struct strbuf transaction_err = STRBUF_INIT;
 	int ret = 0;
 
 	const char * const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto <newbase> | --advance <branch>) "
-		   "<revision-range>..."),
+		   "[--update | --update-refs [--batch]] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -319,6 +355,12 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &contained,
 			 N_("advance all branches contained in revision-range")),
+		OPT_BOOL(0, "update", &update_directly,
+			 N_("update branches directly instead of outputting update commands")),
+		OPT_BOOL(0, "update-refs", &update_refs_flag,
+			 N_("update branches using ref transactions")),
+		OPT_BOOL(0, "batch", &batch_mode,
+			 N_("allow partial ref updates in batch mode")),
 		OPT_END()
 	};
 
@@ -333,6 +375,14 @@ int cmd_replay(int argc,
 	if (advance_name_opt && contained)
 		die(_("options '%s' and '%s' cannot be used together"),
 		    "--advance", "--contained");
+
+	if (update_directly && update_refs_flag)
+		die(_("options '%s' and '%s' cannot be used together"),
+		    "--update", "--update-refs");
+
+	if (batch_mode && !update_refs_flag)
+		die(_("option '%s' can only be used with '%s'"),
+		    "--batch", "--update-refs");
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
@@ -389,6 +439,18 @@ int cmd_replay(int argc,
 	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
 			      &onto, &update_refs);
 
+	/* Initialize ref transaction if using --update-refs */
+	if (update_refs_flag) {
+		unsigned int transaction_flags = batch_mode ? REF_TRANSACTION_ALLOW_FAILURE : 0;
+		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
+								  transaction_flags,
+								  &transaction_err);
+		if (!transaction) {
+			ret = error(_("failed to begin ref transaction: %s"), transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
 	if (!onto) /* FIXME: Should handle replaying down to root commit */
 		die("Replaying down to root commit is not supported yet!");
 
@@ -399,6 +461,7 @@ int cmd_replay(int argc,
 
 	init_basic_merge_options(&merge_opt, repo);
 	memset(&result, 0, sizeof(result));
+	result.clean = 1;  /* Assume clean until proven otherwise */
 	merge_opt.show_rename_progress = 0;
 	last_commit = onto;
 	replayed_commits = kh_init_oid_map();
@@ -434,10 +497,27 @@ int cmd_replay(int argc,
 			if (decoration->type == DECORATION_REF_LOCAL &&
 			    (contained || strset_contains(update_refs,
 							  decoration->name))) {
-				printf("update %s %s %s\n",
-				       decoration->name,
-				       oid_to_hex(&last_commit->object.oid),
-				       oid_to_hex(&commit->object.oid));
+				if (update_directly) {
+					if (update_ref_direct(repo, decoration->name,
+							     &last_commit->object.oid,
+							     &commit->object.oid) < 0) {
+						ret = -1;
+						goto cleanup;
+					}
+				} else if (transaction) {
+					if (add_ref_to_transaction(transaction, decoration->name,
+								   &last_commit->object.oid,
+								   &commit->object.oid,
+								   &transaction_err) < 0) {
+						ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
+						goto cleanup;
+					}
+				} else {
+					printf("update %s %s %s\n",
+					       decoration->name,
+					       oid_to_hex(&last_commit->object.oid),
+					       oid_to_hex(&commit->object.oid));
+				}
 			}
 			decoration = decoration->next;
 		}
@@ -445,10 +525,43 @@ int cmd_replay(int argc,
 
 	/* In --advance mode, advance the target ref */
 	if (result.clean == 1 && advance_name) {
-		printf("update %s %s %s\n",
-		       advance_name,
-		       oid_to_hex(&last_commit->object.oid),
-		       oid_to_hex(&onto->object.oid));
+		if (update_directly) {
+			if (update_ref_direct(repo, advance_name,
+					     &last_commit->object.oid,
+					     &onto->object.oid) < 0) {
+				ret = -1;
+				goto cleanup;
+			}
+		} else if (transaction) {
+			if (add_ref_to_transaction(transaction, advance_name,
+						   &last_commit->object.oid,
+						   &onto->object.oid,
+						   &transaction_err) < 0) {
+				ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
+				goto cleanup;
+			}
+		} else {
+			printf("update %s %s %s\n",
+			       advance_name,
+			       oid_to_hex(&last_commit->object.oid),
+			       oid_to_hex(&onto->object.oid));
+		}
+	}
+
+	/* Commit the ref transaction if we have one */
+	if (transaction && result.clean == 1) {
+		if (ref_transaction_commit(transaction, &transaction_err)) {
+			if (batch_mode) {
+				/* Print failed updates in batch mode */
+				warning(_("some ref updates failed: %s"), transaction_err.buf);
+				ref_transaction_for_each_rejected_update(transaction,
+										 print_rejected_update, NULL);
+			} else {
+				/* In atomic mode, all updates failed */
+				ret = error(_("failed to update refs: %s"), transaction_err.buf);
+				goto cleanup;
+			}
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
@@ -460,6 +573,9 @@ int cmd_replay(int argc,
 	ret = result.clean;
 
 cleanup:
+	if (transaction)
+		ref_transaction_free(transaction);
+	strbuf_release(&transaction_err);
 	release_revisions(&revs);
 	free(advance_name);
 
diff --git a/t/meson.build b/t/meson.build
index daf01fb5d0..966b9d1b1f 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -397,6 +397,7 @@ integration_tests = [
   't3601-rm-pathspec-file.sh',
   't3602-rm-sparse-checkout.sh',
   't3650-replay-basics.sh',
+  't3651-replay-update-refs.sh',
   't3700-add.sh',
   't3701-add-interactive.sh',
   't3702-add-edit.sh',
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 58b3759935..b5aac8c566 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -217,4 +217,327 @@ test_expect_success 'merge.directoryRenames=false' '
 		--onto rename-onto rename-onto..rename-from
 '
 
+test_expect_success 'using replay with --update to rebase a branch' '
+	# Store original branch tips
+	git rev-parse topic2 >topic2.old &&
+	
+	# Use --update to directly update the refs
+	git replay --update --onto main topic1..topic2 &&
+	
+	# Verify the branch was actually updated
+	git rev-parse topic2 >topic2.new &&
+	! test_cmp topic2.old topic2.new &&
+	
+	# Verify the history is correct
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using replay with --update in advance mode' '
+	# Reset topic2 first
+	git branch -f topic2 $(cat topic2.old) &&
+	
+	# Store original main tip
+	git rev-parse main >main.old &&
+	
+	# Use --update with --advance
+	git replay --update --advance main topic1..topic2 &&
+	
+	# Verify main was updated
+	git rev-parse main >main.new &&
+	! test_cmp main.old main.new &&
+	
+	# Verify the history is correct
+	git log --format=%s main >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+	
+	# Reset main back
+	git branch -f main $(cat main.old)
+'
+
+test_expect_success 'using replay with --update and --contained' '
+	# Store original branch tips
+	git rev-parse topic1 >topic1.old &&
+	git rev-parse topic3 >topic3.old &&
+	
+	# Use --update with --contained
+	git replay --update --contained --onto main main..topic3 &&
+	
+	# Verify both branches were updated
+	git rev-parse topic1 >topic1.new &&
+	git rev-parse topic3 >topic3.new &&
+	! test_cmp topic1.old topic1.new &&
+	! test_cmp topic3.old topic3.new &&
+	
+	# Reset branches back
+	git branch -f topic1 $(cat topic1.old) &&
+	git branch -f topic3 $(cat topic3.old)
+'
+
+test_expect_success 'replay with --update should not produce output when successful' '
+	git replay --update --onto main topic1..topic2 >output &&
+	test_must_be_empty output
+'
+
+test_expect_success 'using replay with --update-refs to rebase a branch (atomic mode)' '
+	# Store original branch tip
+	git rev-parse topic2 >topic2.old &&
+	
+	# Use --update-refs to directly update refs with transactions
+	git replay --update-refs --onto main topic1..topic2 &&
+	
+	# Verify the branch was actually updated
+	git rev-parse topic2 >topic2.new &&
+	! test_cmp topic2.old topic2.new &&
+	
+	# Verify the history is correct
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using replay with --update-refs in advance mode' '
+	# Store original main tip
+	git rev-parse main >main.old &&
+	
+	# Use --update-refs with --advance
+	git replay --update-refs --advance main topic1..topic2 &&
+	
+	# Verify main was updated
+	git rev-parse main >main.new &&
+	! test_cmp main.old main.new &&
+	
+	# Verify the history is correct  
+	git log --format=%s main >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using replay with --update-refs and --contained' '
+	# Store original branch tips
+	git rev-parse topic1 >topic1.old &&
+	git rev-parse topic3 >topic3.old &&
+	
+	# Use --update-refs with --contained
+	git replay --update-refs --contained --onto main main..topic3 &&
+	
+	# Verify both branches were updated
+	git rev-parse topic1 >topic1.new &&
+	git rev-parse topic3 >topic3.new &&
+	! test_cmp topic1.old topic1.new &&
+	! test_cmp topic3.old topic3.new &&
+	
+	# Reset branches back
+	git branch -f topic1 $(cat topic1.old) &&
+	git branch -f topic3 $(cat topic3.old)
+'
+
+test_expect_success 'replay with --update-refs should not produce output when successful' '
+	git replay --update-refs --onto main topic1..topic2 >output &&
+	test_must_be_empty output
+'
+
+test_expect_success 'replay with --update-refs --batch should not produce output when successful' '
+	git replay --update-refs --batch --onto main topic1..topic2 >output &&
+	test_must_be_empty output
+'
+
+test_expect_success 'replay fails when --update and --update-refs are used together' '
+	test_must_fail git replay --update --update-refs --onto main topic1..topic2 2>error &&
+	grep "cannot be used together" error
+'
+
+test_expect_success 'replay fails when --batch is used without --update-refs' '
+	test_must_fail git replay --batch --onto main topic1..topic2 2>error &&
+	grep "can only be used with.*--update-refs" error
+'
+
+# Edge cases and comprehensive testing for --update-refs
+
+test_expect_success 'setup for edge case tests' '
+	# Create some additional branches for testing
+	git checkout -b edge1 main &&
+	test_commit Edge1 &&
+	git checkout -b edge2 main &&
+	test_commit Edge2 &&
+	git checkout main
+'
+
+test_expect_success '--update-refs with conflicting replay (atomic mode fails completely)' '
+	# Create a conflict scenario
+	git checkout -b conflict-test main &&
+	echo "conflict content" > C.t &&
+	git add C.t &&
+	git commit -m "Conflicting change" &&
+	
+	# Store original branch state
+	git rev-parse conflict-test >conflict-test.old &&
+	
+	# This should fail due to conflict, and branch should remain unchanged
+	test_expect_code 1 git replay --update-refs --onto topic1 main..conflict-test &&
+	
+	# Verify branch was not updated (atomic transaction rolled back)
+	git rev-parse conflict-test >conflict-test.new &&
+	test_cmp conflict-test.old conflict-test.new
+'
+
+test_expect_success '--update-refs --batch with conflicting replay (partial success)' '
+	# Create scenario with one good commit and one conflicting commit
+	git checkout -b batch-test main &&
+	test_commit GoodCommit &&
+	echo "conflict" > C.t &&
+	git add C.t &&
+	git commit -m "Bad commit" &&
+	
+	# Store original states
+	git rev-parse batch-test >batch-test.old &&
+	
+	# Batch mode should handle partial failures gracefully
+	# Note: This test might need adjustment based on actual conflict behavior
+	test_expect_code 1 git replay --update-refs --batch --onto topic1 main..batch-test 2>batch-error &&
+	
+	# In batch mode, we should get warnings rather than hard failures
+	test_path_is_file batch-error
+'
+
+test_expect_success '--update-refs with no commits to replay (empty transaction)' '
+	# Try to replay an empty range
+	git rev-parse topic1 >topic1.before &&
+	
+	# This should succeed but do nothing
+	git replay --update-refs --onto main topic1..topic1 &&
+	
+	# Branch should be unchanged
+	git rev-parse topic1 >topic1.after &&
+	test_cmp topic1.before topic1.after
+'
+
+test_expect_success '--update-refs with multiple branches (atomic success)' '
+	# Store original states
+	git rev-parse edge1 >edge1.old &&
+	git rev-parse edge2 >edge2.old &&
+	
+	# Replay multiple branches atomically
+	git replay --update-refs --contained --onto main main..edge1 &&
+	git replay --update-refs --contained --onto main main..edge2 &&
+	
+	# Both should be updated
+	git rev-parse edge1 >edge1.new &&
+	git rev-parse edge2 >edge2.new &&
+	! test_cmp edge1.old edge1.new &&
+	! test_cmp edge2.old edge2.new
+'
+
+test_expect_success '--update-refs atomic vs batch behavior comparison' '
+	# Create a branch for comparison
+	git checkout -b compare-test main &&
+	test_commit CompareCommit &&
+	
+	# Test atomic mode first
+	git replay --update-refs --onto main main..compare-test &&
+	git rev-parse compare-test >atomic-result &&
+	
+	# Reset and test batch mode
+	git branch -f compare-test main &&
+	test_commit CompareCommit &&
+	git replay --update-refs --batch --onto main main..compare-test &&
+	git rev-parse compare-test >batch-result &&
+	
+	# Results should be identical for successful cases
+	test_cmp atomic-result batch-result
+'
+
+test_expect_success '--update-refs preserves ref transaction semantics' '
+	# Create branch for testing
+	git checkout -b transaction-test main &&
+	test_commit TransactionCommit &&
+	
+	# Store original state
+	git rev-parse transaction-test >before-transaction &&
+	
+	# Use --update-refs (should be atomic)
+	git replay --update-refs --onto main main..transaction-test &&
+	
+	# Verify ref was updated
+	git rev-parse transaction-test >after-transaction &&
+	! test_cmp before-transaction after-transaction &&
+	
+	# Verify commit history is correct
+	git log --format=%s transaction-test >actual-history &&
+	test_write_lines TransactionCommit M L B A >expected-history &&
+	test_cmp expected-history actual-history
+'
+
+test_expect_success '--update-refs with --advance preserves branch history' '
+	# Test that --advance with --update-refs works correctly
+	git checkout -b advance-test main &&
+	test_commit AdvanceCommit &&
+	
+	# Store original main state
+	git rev-parse main >main-before-advance &&
+	
+	# Use --advance with --update-refs
+	git replay --update-refs --advance main main..advance-test &&
+	
+	# Main should be updated
+	git rev-parse main >main-after-advance &&
+	! test_cmp main-before-advance main-after-advance &&
+	
+	# Verify main has the right commits
+	git log --format=%s main >main-history &&
+	test_write_lines AdvanceCommit M L B A >expected-main &&
+	test_cmp expected-main main-history
+'
+
+test_expect_success '--update-refs handles ref updates consistently with traditional method' '
+	# Create test scenario
+	git checkout -b consistency-test main &&
+	test_commit ConsistencyTest &&
+	
+	# Method 1: Traditional output piped to update-ref
+	git checkout -b trad-test consistency-test &&
+	git replay --onto main main..consistency-test >update-commands &&
+	git update-ref --stdin <update-commands &&
+	git rev-parse trad-test >traditional-result &&
+	
+	# Method 2: Direct --update-refs
+	git branch -f consistency-test main &&
+	test_commit ConsistencyTest &&
+	git checkout -b direct-test consistency-test &&
+	git replay --update-refs --onto main main..consistency-test &&
+	git rev-parse direct-test >direct-result &&
+	
+	# Results should be identical
+	test_cmp traditional-result direct-result
+'
+
+test_expect_success '--update-refs error messages are helpful' '
+	# Test that error messages are clear and helpful
+	git checkout -b error-test main &&
+	test_commit ErrorTest &&
+	
+	# Test conflicting options
+	test_must_fail git replay --update --update-refs --onto main main..error-test 2>conflict-error &&
+	grep "cannot be used together" conflict-error &&
+	
+	# Test batch without update-refs
+	test_must_fail git replay --batch --onto main main..error-test 2>batch-error &&
+	grep "can only be used with" batch-error
+'
+
+test_expect_success '--update-refs with bare repository works correctly' '
+	# Test that --update-refs works in bare repositories (important for Gitaly)
+	git checkout -b bare-test main &&
+	test_commit BareTest &&
+	
+	# Test with bare repo (using existing bare setup)
+	git -C bare replay --update-refs --onto main main..bare-test &&
+	
+	# Verify the bare repo was updated correctly
+	git -C bare rev-parse bare-test >bare-result &&
+	test -s bare-result
+'
+
 test_done
diff --git a/t/t3651-replay-update-refs.sh b/t/t3651-replay-update-refs.sh
new file mode 100755
index 0000000000..fcd4d36721
--- /dev/null
+++ b/t/t3651-replay-update-refs.sh
@@ -0,0 +1,273 @@
+#!/bin/sh
+
+test_description='git replay --update-refs edge cases and comprehensive testing'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+GIT_AUTHOR_NAME=author@name
+GIT_AUTHOR_EMAIL=bogus@email@address
+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+
+test_expect_success 'setup for update-refs tests' '
+	test_commit A &&
+	test_commit B &&
+
+	git switch -c topic1 &&
+	test_commit C &&
+	git switch -c topic2 &&
+	test_commit D &&
+	test_commit E &&
+	git switch topic1 &&
+	test_commit F &&
+
+	git switch main &&
+	test_commit L &&
+	test_commit M &&
+
+	git switch -c conflict B &&
+	test_commit C.conflict C.t conflict
+'
+
+test_expect_success 'setup bare repo' '
+	git clone --bare . bare
+'
+
+# Basic functionality tests
+
+test_expect_success '--update-refs works in atomic mode (basic)' '
+	# Store original branch tip
+	git rev-parse topic2 >topic2.old &&
+	
+	# Use --update-refs to directly update refs with transactions
+	git replay --update-refs --onto main topic1..topic2 &&
+	
+	# Verify the branch was actually updated
+	git rev-parse topic2 >topic2.new &&
+	! test_cmp topic2.old topic2.new &&
+	
+	# Verify the history is correct
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--update-refs works with --advance' '
+	# Store original main tip
+	git rev-parse main >main.old &&
+	
+	# Use --update-refs with --advance
+	git replay --update-refs --advance main topic1..topic2 &&
+	
+	# Verify main was updated
+	git rev-parse main >main.new &&
+	! test_cmp main.old main.new &&
+	
+	# Verify the history is correct  
+	git log --format=%s main >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--update-refs produces no output on success' '
+	git checkout -b quiet-test topic1 &&
+	git replay --update-refs --onto main topic1..quiet-test >output &&
+	test_must_be_empty output
+'
+
+test_expect_success '--update-refs --batch produces no output on success' '
+	git checkout -b batch-quiet-test topic1 &&
+	git replay --update-refs --batch --onto main topic1..batch-quiet-test >output &&
+	test_must_be_empty output
+'
+
+# Edge case tests
+
+test_expect_success '--update-refs with empty range (no-op)' '
+	# Store original branch tip
+	git rev-parse topic1 >topic1.before &&
+	
+	# Try to replay an empty range - should succeed but do nothing
+	git replay --update-refs --onto main topic1..topic1 &&
+	
+	# Branch should be unchanged
+	git rev-parse topic1 >topic1.after &&
+	test_cmp topic1.before topic1.after
+'
+
+test_expect_success '--update-refs atomic vs batch mode comparison' '
+	# Create branch for comparison
+	git checkout -b compare1 topic1 &&
+	test_commit Compare1 &&
+	git checkout -b compare2 topic1 &&
+	test_commit Compare2 &&
+	
+	# Test atomic mode
+	git replay --update-refs --onto main topic1..compare1 &&
+	git rev-parse compare1 >atomic-result &&
+	
+	# Test batch mode - should give same result for successful case
+	git replay --update-refs --batch --onto main topic1..compare2 &&
+	git rev-parse compare2 >batch-result &&
+	
+	# The OIDs will be different since commits are different,
+	# but both should have been updated (not equal to original)
+	git rev-parse topic1 >original &&
+	! test_cmp atomic-result original &&
+	! test_cmp batch-result original
+'
+
+test_expect_success '--update-refs handles conflict gracefully in atomic mode' '
+	# Create a branch that will conflict
+	git checkout -b atomic-conflict B &&
+	echo "different content" >C.t &&
+	git add C.t &&
+	git commit -m "Conflicting C" &&
+	
+	# Store original state
+	git rev-parse atomic-conflict >conflict-before &&
+	
+	# This should fail due to conflict
+	test_expect_code 1 git replay --update-refs --onto conflict atomic-conflict^..atomic-conflict &&
+	
+	# In atomic mode, branch should remain unchanged
+	git rev-parse atomic-conflict >conflict-after &&
+	test_cmp conflict-before conflict-after
+'
+
+test_expect_success '--update-refs preserves transaction semantics' '
+	# Create test branch
+	git checkout -b transaction-test topic1 &&
+	test_commit TransactionTest &&
+	
+	# Store original state
+	git rev-parse transaction-test >before-transaction &&
+	
+	# Use --update-refs (should be atomic)
+	git replay --update-refs --onto main topic1..transaction-test &&
+	
+	# Verify ref was updated
+	git rev-parse transaction-test >after-transaction &&
+	! test_cmp before-transaction after-transaction &&
+	
+	# Verify commit history is preserved correctly
+	git log --format=%s transaction-test >actual-history &&
+	test_write_lines TransactionTest M L B A >expected-history &&
+	test_cmp expected-history actual-history
+'
+
+test_expect_success '--update-refs vs traditional method equivalence' '
+	# Create test branches
+	git checkout -b traditional topic1 &&
+	test_commit Traditional &&
+	git checkout -b direct topic1 &&
+	test_commit Direct &&
+	
+	# Method 1: Traditional output + update-ref
+	git replay --onto main topic1..traditional >update-commands &&
+	git update-ref --stdin <update-commands &&
+	git rev-parse traditional >traditional-result &&
+	
+	# Method 2: Direct --update-refs
+	git replay --update-refs --onto main topic1..direct &&
+	git rev-parse direct >direct-result &&
+	
+	# Both methods should produce equivalent results
+	# (OIDs will be different due to different commits, but both should be updated)
+	git rev-parse topic1 >original &&
+	! test_cmp traditional-result original &&
+	! test_cmp direct-result original
+'
+
+# Error handling and validation tests
+
+test_expect_success 'error messages are helpful and clear' '
+	# Test conflicting options
+	test_must_fail git replay --update --update-refs --onto main topic1..topic2 2>error1 &&
+	grep "cannot be used together" error1 &&
+	
+	# Test batch without update-refs
+	test_must_fail git replay --batch --onto main topic1..topic2 2>error2 &&
+	grep "can only be used with.*--update-refs" error2
+'
+
+test_expect_success '--update-refs works correctly with bare repositories' '
+	# Create branch for bare repo testing
+	git checkout -b bare-test topic1 &&
+	test_commit BareTest &&
+	
+	# Test with bare repo (important for Gitaly use case)
+	git -C bare fetch .. bare-test:bare-test &&
+	git -C bare replay --update-refs --onto main topic1..bare-test &&
+	
+	# Verify the bare repo was updated correctly
+	git -C bare rev-parse bare-test >bare-result &&
+	test -s bare-result &&
+	
+	# Verify it is different from original
+	git rev-parse topic1 >original &&
+	! test_cmp bare-result original
+'
+
+test_expect_success '--update-refs maintains ref update ordering' '
+	# Create multiple branches to test ordering
+	git checkout -b order1 topic1 &&
+	test_commit Order1 &&
+	git checkout -b order2 topic1 &&
+	test_commit Order2 &&
+	
+	# Store original states
+	git rev-parse order1 >order1-before &&
+	git rev-parse order2 >order2-before &&
+	
+	# Update both branches
+	git replay --update-refs --onto main topic1..order1 &&
+	git replay --update-refs --onto main topic1..order2 &&
+	
+	# Verify both were updated
+	git rev-parse order1 >order1-after &&
+	git rev-parse order2 >order2-after &&
+	! test_cmp order1-before order1-after &&
+	! test_cmp order2-before order2-after
+'
+
+test_expect_success '--update-refs handles ref transaction cleanup properly' '
+	# This test ensures no ref transaction leaks occur
+	git checkout -b cleanup-test topic1 &&
+	test_commit CleanupTest &&
+	
+	# Run multiple operations to test cleanup
+	git replay --update-refs --onto main topic1..cleanup-test &&
+	git replay --update-refs --batch --onto main topic1..cleanup-test &&
+	
+	# If cleanup is working properly, these should succeed without errors
+	test_path_is_file .git/refs/heads/cleanup-test
+'
+
+# Performance and stress tests
+
+test_expect_success '--update-refs performance is reasonable' '
+	# Create several commits to test performance
+	git checkout -b perf-test topic1 &&
+	for i in 1 2 3 4 5; do
+		test_commit "Perf$i" || return 1
+	done &&
+	
+	# Time the traditional method
+	time git replay --onto main topic1..perf-test >perf-commands &&
+	time git update-ref --stdin <perf-commands &&
+	
+	# Reset and time the new method
+	git branch -f perf-test topic1 &&
+	for i in 1 2 3 4 5; do
+		test_commit "Perf$i" || return 1
+	done &&
+	time git replay --update-refs --onto main topic1..perf-test &&
+	
+	# Test completed successfully if we got here
+	true
+'
+
+test_done
\ No newline at end of file
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH 2/2] replay: document --update-refs and --batch options
  2025-09-08  4:36 [PATCH 0/2] replay: add --update-refs option Siddharth Asthana
  2025-09-08  4:36 ` [PATCH 1/2] " Siddharth Asthana
@ 2025-09-08  4:36 ` Siddharth Asthana
  2025-09-08  6:00   ` Christian Couder
                     ` (2 more replies)
  2025-09-08  6:07 ` [PATCH 0/2] replay: add --update-refs option Christian Couder
                   ` (3 subsequent siblings)
  5 siblings, 3 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-08  4:36 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Christian Couder, Karthik Nayak, Justin Tobler,
	Elijah Newren, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin, Siddharth Asthana

Add documentation for the new --update-refs option which performs
ref updates directly using Git's ref transaction API, eliminating
the need for users to pipe output to git update-ref --stdin.

Also document the --batch option which can be used with --update-refs
to allow partial failures in ref updates.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc | 62 +++++++++++++++++++++++++++++++----
 1 file changed, 56 insertions(+), 6 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 0b12bf8aa4..cc9f868c2f 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,16 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--update | --update-refs [--batch]] <revision-range>...
 
 DESCRIPTION
 -----------
 
 Takes ranges of commits and replays them onto a new location. Leaves
-the working tree and the index untouched, and updates no references.
-The output of this command is meant to be used as input to
+the working tree and the index untouched, and by default updates no 
+references. The output of this command is meant to be used as input to
 `git update-ref --stdin`, which would update the relevant branches
-(see the OUTPUT section below).
+(see the OUTPUT section below). Alternatively, with `--update`, the
+refs can be updated directly.
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 
@@ -42,6 +43,24 @@ When `--advance` is specified, the update-ref command(s) in the output
 will update the branch passed as an argument to `--advance` to point at
 the new commits (in other words, this mimics a cherry-pick operation).
 
+--update::
+	Update the relevant refs directly instead of outputting
+	update-ref commands. When this option is used, no output is
+	produced on successful completion, and the refs are updated
+	immediately. If any ref update fails, the command will exit
+	with a non-zero status.
+
+--update-refs::
+	Update the relevant refs using ref transactions instead of outputting
+	update-ref commands. By default, uses atomic mode where all ref updates
+	succeed or all fail. Use with `--batch` to allow partial updates.
+	When this option is used, no output is produced on successful completion.
+
+--batch::
+	Can only be used with `--update-refs`. Enables batch mode for ref
+	updates, allowing some refs to be updated successfully even if others
+	fail. Failed updates are reported as warnings rather than errors.
+
 <revision-range>::
 	Range of commits to replay. More than one <revision-range> can
 	be passed, but in `--advance <branch>` mode, they should have
@@ -54,8 +73,9 @@ include::rev-list-options.adoc[]
 OUTPUT
 ------
 
-When there are no conflicts, the output of this command is usable as
-input to `git update-ref --stdin`.  It is of the form:
+When there are no conflicts and neither `--update` nor `--update-refs` 
+is used, the output of this command is usable as input to `git update-ref --stdin`.  
+It is of the form:
 
 	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
@@ -66,6 +86,15 @@ the shape of the history being replayed.  When using `--advance`, the
 number of refs updated is always one, but for `--onto`, it can be one
 or more (rebasing multiple branches simultaneously is supported).
 
+When `--update` is used, no output is produced and the refs are updated
+directly using individual ref updates. This is equivalent to piping the normal output to 
+`git update-ref --stdin`.
+
+When `--update-refs` is used, no output is produced and the refs are updated
+using ref transactions. In atomic mode (default), all ref updates succeed 
+or all fail. In batch mode (with `--batch`), some updates may succeed while 
+others fail, with failed updates reported as warnings.
+
 EXIT STATUS
 -----------
 
@@ -91,6 +120,27 @@ $ git replay --advance target origin/main..mybranch
 update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
 ------------
 
+To rebase `mybranch` onto `target` and update the ref directly:
+
+------------
+$ git replay --update --onto target origin/main..mybranch
+# No output; mybranch is updated directly
+------------
+
+To rebase `mybranch` onto `target` using atomic ref transactions:
+
+------------
+$ git replay --update-refs --onto target origin/main..mybranch
+# No output; mybranch is updated atomically
+------------
+
+To rebase multiple branches with partial failure tolerance:
+
+------------
+$ git replay --update-refs --batch --contained --onto origin/main origin/main..tipbranch
+# No output; refs updated in batch mode, warnings for any failures
+------------
+
 Note that the first two examples replay the exact same commits and on
 top of the exact same new base, they only differ in that the first
 provides instructions to make mybranch point at the new commits and
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* Re: [PATCH 2/2] replay: document --update-refs and --batch options
  2025-09-08  4:36 ` [PATCH 2/2] replay: document --update-refs and --batch options Siddharth Asthana
@ 2025-09-08  6:00   ` Christian Couder
  2025-09-09  6:36     ` Siddharth Asthana
  2025-09-08 14:40   ` Kristoffer Haugsbakk
  2025-09-09 19:20   ` Andrei Rybak
  2 siblings, 1 reply; 129+ messages in thread
From: Christian Couder @ 2025-09-08  6:00 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, Junio C Hamano, Karthik Nayak, Justin Tobler, Elijah Newren,
	Patrick Steinhardt, Toon Claes, John Cai, Johannes Schindelin

On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> Add documentation for the new --update-refs option which performs
> ref updates directly using Git's ref transaction API, eliminating
> the need for users to pipe output to git update-ref --stdin.

Most of the time, the documentation should be part of the patch that
introduces the documented behavior, not in a separate patch.

> Also document the --batch option which can be used with --update-refs
> to allow partial failures in ref updates.

It looks like a --update option was also added by the previous patch.
Is it documented here too?

Why was this [--update | --update-refs [--batch]] set of options
selected over other possibilities like for example
[--update-iteratively | --update-atomically | --update-batch]?

Also how does this --update-refs option compare to the --update-refs
option in git rebase? Is it working in the same way?

> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> ---
>  Documentation/git-replay.adoc | 62 +++++++++++++++++++++++++++++++----
>  1 file changed, 56 insertions(+), 6 deletions(-)
>
> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
> index 0b12bf8aa4..cc9f868c2f 100644
> --- a/Documentation/git-replay.adoc
> +++ b/Documentation/git-replay.adoc
> @@ -9,16 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
>  SYNOPSIS
>  --------
>  [verse]
> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--update | --update-refs [--batch]] <revision-range>...

Here --update, --update-refs and --batch are all documented, nice.

>  DESCRIPTION
>  -----------
>
>  Takes ranges of commits and replays them onto a new location. Leaves
> -the working tree and the index untouched, and updates no references.
> -The output of this command is meant to be used as input to
> +the working tree and the index untouched, and by default updates no
> +references. The output of this command is meant to be used as input to
>  `git update-ref --stdin`, which would update the relevant branches
> -(see the OUTPUT section below).
> +(see the OUTPUT section below). Alternatively, with `--update`, the
> +refs can be updated directly.

Here only --update is documented.

>  THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
>
> @@ -42,6 +43,24 @@ When `--advance` is specified, the update-ref command(s) in the output
>  will update the branch passed as an argument to `--advance` to point at
>  the new commits (in other words, this mimics a cherry-pick operation).
>
> +--update::
> +       Update the relevant refs directly instead of outputting
> +       update-ref commands. When this option is used, no output is
> +       produced on successful completion,

It seems a bit redundant to say both "instead of outputting update-ref
commands" and then "no output is produced on successful completion".
Maybe there is a way to reword this to be a bit more concise.

> and the refs are updated
> +       immediately. If any ref update fails, the command will exit
> +       with a non-zero status.

This doesn't say if the command immediately stops when it fails to
update a ref, and if the ref updates are atomic or not.

> +--update-refs::
> +       Update the relevant refs using ref transactions instead of outputting
> +       update-ref commands. By default, uses atomic mode where all ref updates
> +       succeed or all fail.

This seems to imply that --update doesn't update the refs atomically.

> Use with `--batch` to allow partial updates.

What about --update, when should it be used?

> +       When this option is used, no output is produced on successful completion.

Here also it seems a bit redundant to say both "instead of outputting
update-ref commands" and then "no output is produced on successful
completion". And maybe there is a way to reword this to be a bit more
concise.

> +--batch::
> +       Can only be used with `--update-refs`. Enables batch mode for ref
> +       updates, allowing some refs to be updated successfully even if others
> +       fail. Failed updates are reported as warnings rather than errors.

What's the difference with --update? Is it that --update immediately
stops when a ref update fails?

>  <revision-range>::
>         Range of commits to replay. More than one <revision-range> can
>         be passed, but in `--advance <branch>` mode, they should have
> @@ -54,8 +73,9 @@ include::rev-list-options.adoc[]
>  OUTPUT
>  ------
>
> -When there are no conflicts, the output of this command is usable as
> -input to `git update-ref --stdin`.  It is of the form:
> +When there are no conflicts and neither `--update` nor `--update-refs`
> +is used, the output of this command is usable as input to `git update-ref --stdin`.
> +It is of the form:
>
>         update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>         update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> @@ -66,6 +86,15 @@ the shape of the history being replayed.  When using `--advance`, the
>  number of refs updated is always one, but for `--onto`, it can be one
>  or more (rebasing multiple branches simultaneously is supported).
>
> +When `--update` is used, no output is produced and the refs are updated
> +directly using individual ref updates. This is equivalent to piping the normal output to
> +`git update-ref --stdin`.

Is it equivalent to `git update-ref --stdin` because both exit as soon
as a ref update fails?

> +When `--update-refs` is used, no output is produced and the refs are updated
> +using ref transactions. In atomic mode (default), all ref updates succeed
> +or all fail. In batch mode (with `--batch`), some updates may succeed while
> +others fail, with failed updates reported as warnings.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 0/2] replay: add --update-refs option
  2025-09-08  4:36 [PATCH 0/2] replay: add --update-refs option Siddharth Asthana
  2025-09-08  4:36 ` [PATCH 1/2] " Siddharth Asthana
  2025-09-08  4:36 ` [PATCH 2/2] replay: document --update-refs and --batch options Siddharth Asthana
@ 2025-09-08  6:07 ` Christian Couder
  2025-09-09  6:36   ` Siddharth Asthana
  2025-09-08 14:33 ` Kristoffer Haugsbakk
                   ` (2 subsequent siblings)
  5 siblings, 1 reply; 129+ messages in thread
From: Christian Couder @ 2025-09-08  6:07 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, Junio C Hamano, Karthik Nayak, Justin Tobler, Elijah Newren,
	Patrick Steinhardt, Toon Claes, John Cai, Johannes Schindelin

On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> This patch series adds a --update-refs option to git replay. Right now,
> when you use git replay, you need to pipe its output to git update-ref
> like this:
>
>     git replay --onto main topic1..topic2 | git update-ref --stdin
>
> This works fine, but it means running two commands and doesn't give you
> atomic transactions by default. The new --update-refs option lets you do
> the ref updates directly:
>
>     git replay --update-refs --onto main topic1..topic2

Thanks for working on this.

> I discussed this feature with Christian Couder earlier, and we agreed that
> it would be useful for server-side operations where you want atomic updates.

Yeah, right. This is something the Git team at GitLab has been
interested in for some time.

> The way it works:
> - By default, it uses atomic transactions (all refs get updated or none do)
> - There's a --batch option if you want some updates to succeed even if
>   others fail
> - It works with bare repositories, which is important for server operations
>   like Gitaly
> - When it succeeds, it doesn't print anything (just like git update-ref
>   --stdin)
> - You can't use --update-refs with the existing --update option

There is no existing --update option. This series also introduces the
--update option.

> This should help with git replay's goal of being good for server-side
> operations. It also makes the command simpler to use since you don't need
> the pipeline anymore, and the atomic behavior is better for reliability.

I have commented only on the documentation patch for now as I think
it's better to review the design of the new options first, and the
documentation looks like a good place for that.

Thanks again.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 1/2] replay: add --update-refs option
  2025-09-08  4:36 ` [PATCH 1/2] " Siddharth Asthana
@ 2025-09-08  9:54   ` Patrick Steinhardt
  2025-09-09  6:58     ` Siddharth Asthana
  2025-09-09  7:32   ` Elijah Newren
  1 sibling, 1 reply; 129+ messages in thread
From: Patrick Steinhardt @ 2025-09-08  9:54 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, Junio C Hamano, Christian Couder, Karthik Nayak,
	Justin Tobler, Elijah Newren, Toon Claes, John Cai,
	Johannes Schindelin

On Mon, Sep 08, 2025 at 10:06:19AM +0530, Siddharth Asthana wrote:
> diff --git a/builtin/replay.c b/builtin/replay.c
> index 6172c8aacc..a33c9887cf 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -284,6 +284,37 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  	return create_commit(repo, result->tree, pickme, replayed_base);
>  }
>  
> +static int update_ref_direct(struct repository *repo, const char *refname,
> +			     const struct object_id *new_oid,
> +			     const struct object_id *old_oid)
> +{
> +	const char *msg = "replay";
> +	return refs_update_ref(get_main_ref_store(repo), msg, refname,
> +			       new_oid, old_oid, 0, UPDATE_REFS_MSG_ON_ERR);
> +}

Is there a strong reason why a user would want to update refs one by
one? If not, let's not add new code to our base that does so. This is
known to be inperformant for the reftable backend, but also for the
files backend in some cases. If we really want to support the case where
only a subset of references gets committed we should be using batched
updates with the `REF_TRANSACTION_ALLOW_FAILURE` flag.

> @@ -319,6 +355,12 @@ int cmd_replay(int argc,
>  			   N_("replay onto given commit")),
>  		OPT_BOOL(0, "contained", &contained,
>  			 N_("advance all branches contained in revision-range")),
> +		OPT_BOOL(0, "update", &update_directly,
> +			 N_("update branches directly instead of outputting update commands")),
> +		OPT_BOOL(0, "update-refs", &update_refs_flag,
> +			 N_("update branches using ref transactions")),
> +		OPT_BOOL(0, "batch", &batch_mode,
> +			 N_("allow partial ref updates in batch mode")),
>  		OPT_END()
>  	};
>  

So I think we should reduce this to only accept two flags:
`--update-refs` and a flag that accepts a subset of refs failing.o

We might also want to make this something like `--update-refs[=<mode>]`,
where `<mode>` could be "allow-failures".

> @@ -333,6 +375,14 @@ int cmd_replay(int argc,
>  	if (advance_name_opt && contained)
>  		die(_("options '%s' and '%s' cannot be used together"),
>  		    "--advance", "--contained");
> +
> +	if (update_directly && update_refs_flag)
> +		die(_("options '%s' and '%s' cannot be used together"),
> +		    "--update", "--update-refs");
> +
> +	if (batch_mode && !update_refs_flag)
> +		die(_("option '%s' can only be used with '%s'"),
> +		    "--batch", "--update-refs");
>  	advance_name = xstrdup_or_null(advance_name_opt);
>  
>  	repo_init_revisions(repo, &revs, prefix);

We have the `die_for_incompatible_opt*()` helpers for this.

> @@ -389,6 +439,18 @@ int cmd_replay(int argc,
>  	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
>  			      &onto, &update_refs);
>  
> +	/* Initialize ref transaction if using --update-refs */

Nit: the comment doesn't really add much context, so I'd just drop it.
It's generally discouraged to add a comment that re-states what the code
already says. Instead, comments should point out things that are easy to
miss or not obvious at all.

> @@ -445,10 +525,43 @@ int cmd_replay(int argc,
>  
>  	/* In --advance mode, advance the target ref */
>  	if (result.clean == 1 && advance_name) {
> -		printf("update %s %s %s\n",
> -		       advance_name,
> -		       oid_to_hex(&last_commit->object.oid),
> -		       oid_to_hex(&onto->object.oid));
> +		if (update_directly) {
> +			if (update_ref_direct(repo, advance_name,
> +					     &last_commit->object.oid,
> +					     &onto->object.oid) < 0) {
> +				ret = -1;
> +				goto cleanup;
> +			}
> +		} else if (transaction) {
> +			if (add_ref_to_transaction(transaction, advance_name,
> +						   &last_commit->object.oid,
> +						   &onto->object.oid,
> +						   &transaction_err) < 0) {
> +				ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
> +				goto cleanup;
> +			}
> +		} else {
> +			printf("update %s %s %s\n",
> +			       advance_name,
> +			       oid_to_hex(&last_commit->object.oid),
> +			       oid_to_hex(&onto->object.oid));
> +		}
> +	}
> +
> +	/* Commit the ref transaction if we have one */

Likewise here.

> +	if (transaction && result.clean == 1) {
> +		if (ref_transaction_commit(transaction, &transaction_err)) {
> +			if (batch_mode) {
> +				/* Print failed updates in batch mode */
> +				warning(_("some ref updates failed: %s"), transaction_err.buf);
> +				ref_transaction_for_each_rejected_update(transaction,
> +										 print_rejected_update, NULL);
> +			} else {
> +				/* In atomic mode, all updates failed */
> +				ret = error(_("failed to update refs: %s"), transaction_err.buf);
> +				goto cleanup;
> +			}
> +		}
>  	}
>  
>  	merge_finalize(&merge_opt, &result);

Patrick

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 0/2] replay: add --update-refs option
  2025-09-08  4:36 [PATCH 0/2] replay: add --update-refs option Siddharth Asthana
                   ` (2 preceding siblings ...)
  2025-09-08  6:07 ` [PATCH 0/2] replay: add --update-refs option Christian Couder
@ 2025-09-08 14:33 ` Kristoffer Haugsbakk
  2025-09-09  7:04   ` Siddharth Asthana
  2025-09-09  7:13 ` Elijah Newren
  2025-09-26 23:08 ` [PATCH v2 0/1] replay: make atomic ref updates the default behavior Siddharth Asthana
  5 siblings, 1 reply; 129+ messages in thread
From: Kristoffer Haugsbakk @ 2025-09-08 14:33 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: Junio C Hamano, Christian Couder, Karthik Nayak, Justin Tobler,
	Elijah Newren, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin

On Mon, Sep 8, 2025, at 06:36, Siddharth Asthana wrote:
> This patch series adds a --update-refs option to git replay. Right now,
> when you use git replay, you need to pipe its output to git update-ref
> like this:
>[snip]

Both patches introduce whitespace errors.  You can check with
`ci/check-whitespace.sh`.

That script will suggest a way to fix it.

There’s also a `\ No newline at end of file` (I don’t think whitespace-
check checks that).

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 2/2] replay: document --update-refs and --batch options
  2025-09-08  4:36 ` [PATCH 2/2] replay: document --update-refs and --batch options Siddharth Asthana
  2025-09-08  6:00   ` Christian Couder
@ 2025-09-08 14:40   ` Kristoffer Haugsbakk
  2025-09-09  7:06     ` Siddharth Asthana
  2025-09-09 19:20   ` Andrei Rybak
  2 siblings, 1 reply; 129+ messages in thread
From: Kristoffer Haugsbakk @ 2025-09-08 14:40 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: Junio C Hamano, Christian Couder, Karthik Nayak, Justin Tobler,
	Elijah Newren, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin

On Mon, Sep 8, 2025, at 06:36, Siddharth Asthana wrote:
>[snip]
> diff --git a/Documentation/git-replay.adoc
> b/Documentation/git-replay.adoc
> index 0b12bf8aa4..cc9f868c2f 100644
> --- a/Documentation/git-replay.adoc
> +++ b/Documentation/git-replay.adoc
> @@ -9,16 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new
> base, works with bare repos t
>  SYNOPSIS
>  --------
>  [verse]
> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> |
> --advance <branch>) <revision-range>...
> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--update | --update-refs [--batch]] <revision-range>...

Another downside of making a separate commit for the documentation is
that now `t/t0450-txt-doc-vs-help.sh` will likely fail for your first
commit.  One of the tests makes sure that the synopsis and the `.adoc`
is in synch.

>
>  DESCRIPTION
>  -----------
>[snip]

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 2/2] replay: document --update-refs and --batch options
  2025-09-08  6:00   ` Christian Couder
@ 2025-09-09  6:36     ` Siddharth Asthana
  2025-09-09  7:26       ` Christian Couder
  0 siblings, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-09  6:36 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Karthik Nayak, Justin Tobler, Elijah Newren,
	Patrick Steinhardt, Toon Claes, John Cai, Johannes Schindelin


On 08/09/25 11:30, Christian Couder wrote:
> On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> Add documentation for the new --update-refs option which performs
>> ref updates directly using Git's ref transaction API, eliminating
>> the need for users to pipe output to git update-ref --stdin.
Hi Christian,

Thanks for the detailed review.
> Most of the time, the documentation should be part of the patch that
> introduces the documented behavior, not in a separate patch.
You are right I will combine them in v2.
>
>> Also document the --batch option which can be used with --update-refs
>> to allow partial failures in ref updates.
> It looks like a --update option was also added by the previous patch.
> Is it documented here too?
>
> Why was this [--update | --update-refs [--batch]] set of options
> selected over other possibilities like for example
> [--update-iteratively | --update-atomically | --update-batch]?
I was trying to provide both simple and advanced modes. --update for 
users who just want "make it work like piping to git update-ref --stdin" 
and --update-refs for those who want control over transaction modes. But 
I see this creates confusion.

Would you prefer a single option like --update-refs with an optional 
mode parameter? Something like --update-refs[=batch] where default is 
atomic?
>
> Also how does this --update-refs option compare to the --update-refs
> option in git rebase? Is it working in the same way?
No, they are different. git rebase --update-refs updates refs that point 
to commits being rebased. --update-refs updates the target branches from 
the replay operation itself. The naming collision is unfortunate should 
I use a different name?
>
>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
>> ---
>>   Documentation/git-replay.adoc | 62 +++++++++++++++++++++++++++++++----
>>   1 file changed, 56 insertions(+), 6 deletions(-)
>>
>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
>> index 0b12bf8aa4..cc9f868c2f 100644
>> --- a/Documentation/git-replay.adoc
>> +++ b/Documentation/git-replay.adoc
>> @@ -9,16 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
>>   SYNOPSIS
>>   --------
>>   [verse]
>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
>> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--update | --update-refs [--batch]] <revision-range>...
> Here --update, --update-refs and --batch are all documented, nice.
>
>>   DESCRIPTION
>>   -----------
>>
>>   Takes ranges of commits and replays them onto a new location. Leaves
>> -the working tree and the index untouched, and updates no references.
>> -The output of this command is meant to be used as input to
>> +the working tree and the index untouched, and by default updates no
>> +references. The output of this command is meant to be used as input to
>>   `git update-ref --stdin`, which would update the relevant branches
>> -(see the OUTPUT section below).
>> +(see the OUTPUT section below). Alternatively, with `--update`, the
>> +refs can be updated directly.
> Here only --update is documented.
>
>>   THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
>>
>> @@ -42,6 +43,24 @@ When `--advance` is specified, the update-ref command(s) in the output
>>   will update the branch passed as an argument to `--advance` to point at
>>   the new commits (in other words, this mimics a cherry-pick operation).
>>
>> +--update::
>> +       Update the relevant refs directly instead of outputting
>> +       update-ref commands. When this option is used, no output is
>> +       produced on successful completion,
> It seems a bit redundant to say both "instead of outputting update-ref
> commands" and then "no output is produced on successful completion".
> Maybe there is a way to reword this to be a bit more concise.
You are right that's redundant. I will reword it.
>
>> and the refs are updated
>> +       immediately. If any ref update fails, the command will exit
>> +       with a non-zero status.
> This doesn't say if the command immediately stops when it fails to
> update a ref, and if the ref updates are atomic or not.
You are right the docs need to be clearer about the behavior 
differences. I will clarify that --update stops immediately on failure 
(like git update-ref --stdin), while --update-refs defaults to atomic mode.
>
>> +--update-refs::
>> +       Update the relevant refs using ref transactions instead of outputting
>> +       update-ref commands. By default, uses atomic mode where all ref updates
>> +       succeed or all fail.
> This seems to imply that --update doesn't update the refs atomically.
That correct --update doesn't use transactions it updates refs one by 
one like `git update-ref --stdin` does. Should I make this clearer in 
the documentation?
>
>> Use with `--batch` to allow partial updates.
> What about --update, when should it be used?
Good point. My thinking was --update for simple cases where you want the 
exact same behavior as piping to `git update-ref --stdin` and 
--update-refs when you want transaction guarantees. But I am starting to 
think this distinction might be confusing users more than helping them.

Would it be cleaner to just have --update-refs with the batch mode 
option and drop --update entirely? The sequential behavior can be 
achieved with --update-refs --batch if someone really needs it.
>
>> +       When this option is used, no output is produced on successful completion.
> Here also it seems a bit redundant to say both "instead of outputting
> update-ref commands" and then "no output is produced on successful
> completion". And maybe there is a way to reword this to be a bit more
> concise.
Yes same redundancy issue. I will fix the wording throughout in v2.
>
>> +--batch::
>> +       Can only be used with `--update-refs`. Enables batch mode for ref
>> +       updates, allowing some refs to be updated successfully even if others
>> +       fail. Failed updates are reported as warnings rather than errors.
> What's the difference with --update? Is it that --update immediately
> stops when a ref update fails?
Yes exactly. --update mimics the behavior of piping to `git update-ref 
--stdin` and it stops immediately on the first failure and doesn't 
update any remaining refs.

--update-refs uses transactions, so in atomic mode all refs are updated 
together or none at all, and in batch mode it can continue processing 
remaining refs even after some fail.
>
>>   <revision-range>::
>>          Range of commits to replay. More than one <revision-range> can
>>          be passed, but in `--advance <branch>` mode, they should have
>> @@ -54,8 +73,9 @@ include::rev-list-options.adoc[]
>>   OUTPUT
>>   ------
>>
>> -When there are no conflicts, the output of this command is usable as
>> -input to `git update-ref --stdin`.  It is of the form:
>> +When there are no conflicts and neither `--update` nor `--update-refs`
>> +is used, the output of this command is usable as input to `git update-ref --stdin`.
>> +It is of the form:
>>
>>          update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>>          update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>> @@ -66,6 +86,15 @@ the shape of the history being replayed.  When using `--advance`, the
>>   number of refs updated is always one, but for `--onto`, it can be one
>>   or more (rebasing multiple branches simultaneously is supported).
>>
>> +When `--update` is used, no output is produced and the refs are updated
>> +directly using individual ref updates. This is equivalent to piping the normal output to
>> +`git update-ref --stdin`.
> Is it equivalent to `git update-ref --stdin` because both exit as soon
> as a ref update fails?
Yes that is the intention. Both --update and `git update-ref --stdin` 
process refs sequentially and exit on first failure leaving the 
repository in a partially updated state if failure occurs partway through.

The difference is --update does this internally without needing the pipe 
while --update-refs uses proper transactions for better atomicity 
guarantees.
>> +When `--update-refs` is used, no output is produced and the refs are updated
>> +using ref transactions. In atomic mode (default), all ref updates succeed
>> +or all fail. In batch mode (with `--batch`), some updates may succeed while
>> +others fail, with failed updates reported as warnings.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 0/2] replay: add --update-refs option
  2025-09-08  6:07 ` [PATCH 0/2] replay: add --update-refs option Christian Couder
@ 2025-09-09  6:36   ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-09  6:36 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Karthik Nayak, Justin Tobler, Elijah Newren,
	Patrick Steinhardt, Toon Claes, John Cai, Johannes Schindelin


On 08/09/25 11:37, Christian Couder wrote:
> On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> This patch series adds a --update-refs option to git replay. Right now,
>> when you use git replay, you need to pipe its output to git update-ref
>> like this:
>>
>>      git replay --onto main topic1..topic2 | git update-ref --stdin
>>
>> This works fine, but it means running two commands and doesn't give you
>> atomic transactions by default. The new --update-refs option lets you do
>> the ref updates directly:
>>
>>      git replay --update-refs --onto main topic1..topic2
> Thanks for working on this.
>
>> I discussed this feature with Christian Couder earlier, and we agreed that
>> it would be useful for server-side operations where you want atomic updates.
> Yeah, right. This is something the Git team at GitLab has been
> interested in for some time.
>
>> The way it works:
>> - By default, it uses atomic transactions (all refs get updated or none do)
>> - There's a --batch option if you want some updates to succeed even if
>>    others fail
>> - It works with bare repositories, which is important for server operations
>>    like Gitaly
>> - When it succeeds, it doesn't print anything (just like git update-ref
>>    --stdin)
>> - You can't use --update-refs with the existing --update option
> There is no existing --update option. This series also introduces the
> --update option.
You are right that was confusing wording in my cover letter. Both 
--update and --update-refs are new in this series.
>
>> This should help with git replay's goal of being good for server-side
>> operations. It also makes the command simpler to use since you don't need
>> the pipeline anymore, and the atomic behavior is better for reliability.
> I have commented only on the documentation patch for now as I think
> it's better to review the design of the new options first, and the
> documentation looks like a good place for that.
>
> Thanks again.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 1/2] replay: add --update-refs option
  2025-09-08  9:54   ` Patrick Steinhardt
@ 2025-09-09  6:58     ` Siddharth Asthana
  2025-09-09  9:00       ` Patrick Steinhardt
  0 siblings, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-09  6:58 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, Junio C Hamano, Christian Couder, Karthik Nayak,
	Justin Tobler, Elijah Newren, Toon Claes, John Cai,
	Johannes Schindelin


On 08/09/25 15:24, Patrick Steinhardt wrote:
> On Mon, Sep 08, 2025 at 10:06:19AM +0530, Siddharth Asthana wrote:
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index 6172c8aacc..a33c9887cf 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -284,6 +284,37 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>   	return create_commit(repo, result->tree, pickme, replayed_base);
>>   }
>>   
>> +static int update_ref_direct(struct repository *repo, const char *refname,
>> +			     const struct object_id *new_oid,
>> +			     const struct object_id *old_oid)
>> +{
>> +	const char *msg = "replay";
>> +	return refs_update_ref(get_main_ref_store(repo), msg, refname,
>> +			       new_oid, old_oid, 0, UPDATE_REFS_MSG_ON_ERR);
>> +}


Hi Patrick,

Thanks for the detailed review


> Is there a strong reason why a user would want to update refs one by
> one? If not, let's not add new code to our base that does so. This is
> known to be inperformant for the reftable backend, but also for the
> files backend in some cases.


You are absolutely right about the performance concern. My thinking was 
to provide a simple mode that exactly mimics "git replay | git 
update-ref --stdin" behavior, but I see that's not worth the performance 
cost.

I will remove the individual update function and only use batched 
transactions with REF_TRANSACTION_ALLOW_FAILURE when needed.


> If we really want to support the case where
> only a subset of references gets committed we should be using batched
> updates with the `REF_TRANSACTION_ALLOW_FAILURE` flag.
>
>> @@ -319,6 +355,12 @@ int cmd_replay(int argc,
>>   			   N_("replay onto given commit")),
>>   		OPT_BOOL(0, "contained", &contained,
>>   			 N_("advance all branches contained in revision-range")),
>> +		OPT_BOOL(0, "update", &update_directly,
>> +			 N_("update branches directly instead of outputting update commands")),
>> +		OPT_BOOL(0, "update-refs", &update_refs_flag,
>> +			 N_("update branches using ref transactions")),
>> +		OPT_BOOL(0, "batch", &batch_mode,
>> +			 N_("allow partial ref updates in batch mode")),
>>   		OPT_END()
>>   	};
>>   
> So I think we should reduce this to only accept two flags:
> `--update-refs` and a flag that accepts a subset of refs failing.o
>
> We might also want to make this something like `--update-refs[=<mode>]`,
> where `<mode>` could be "allow-failures".


That make sense. Would you prefer `--update-refs` with 
`--allow-failures` as a separate flag? I am leaning toward that since 
it's clearer than the parameter syntax.


>
>> @@ -333,6 +375,14 @@ int cmd_replay(int argc,
>>   	if (advance_name_opt && contained)
>>   		die(_("options '%s' and '%s' cannot be used together"),
>>   		    "--advance", "--contained");
>> +
>> +	if (update_directly && update_refs_flag)
>> +		die(_("options '%s' and '%s' cannot be used together"),
>> +		    "--update", "--update-refs");
>> +
>> +	if (batch_mode && !update_refs_flag)
>> +		die(_("option '%s' can only be used with '%s'"),
>> +		    "--batch", "--update-refs");
>>   	advance_name = xstrdup_or_null(advance_name_opt);
>>   
>>   	repo_init_revisions(repo, &revs, prefix);
> We have the `die_for_incompatible_opt*()` helpers for this.


Thanks, I will use those.


>
>> @@ -389,6 +439,18 @@ int cmd_replay(int argc,
>>   	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
>>   			      &onto, &update_refs);
>>   
>> +	/* Initialize ref transaction if using --update-refs */
> Nit: the comment doesn't really add much context, so I'd just drop it.
> It's generally discouraged to add a comment that re-states what the code
> already says. Instead, comments should point out things that are easy to
> miss or not obvious at all.


Will remove the redundant comments.


>
>> @@ -445,10 +525,43 @@ int cmd_replay(int argc,
>>   
>>   	/* In --advance mode, advance the target ref */
>>   	if (result.clean == 1 && advance_name) {
>> -		printf("update %s %s %s\n",
>> -		       advance_name,
>> -		       oid_to_hex(&last_commit->object.oid),
>> -		       oid_to_hex(&onto->object.oid));
>> +		if (update_directly) {
>> +			if (update_ref_direct(repo, advance_name,
>> +					     &last_commit->object.oid,
>> +					     &onto->object.oid) < 0) {
>> +				ret = -1;
>> +				goto cleanup;
>> +			}
>> +		} else if (transaction) {
>> +			if (add_ref_to_transaction(transaction, advance_name,
>> +						   &last_commit->object.oid,
>> +						   &onto->object.oid,
>> +						   &transaction_err) < 0) {
>> +				ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
>> +				goto cleanup;
>> +			}
>> +		} else {
>> +			printf("update %s %s %s\n",
>> +			       advance_name,
>> +			       oid_to_hex(&last_commit->object.oid),
>> +			       oid_to_hex(&onto->object.oid));
>> +		}
>> +	}
>> +
>> +	/* Commit the ref transaction if we have one */
> Likewise here.


Will remove the redundant comments here too.


Thank,

Siddharth


>
>> +	if (transaction && result.clean == 1) {
>> +		if (ref_transaction_commit(transaction, &transaction_err)) {
>> +			if (batch_mode) {
>> +				/* Print failed updates in batch mode */
>> +				warning(_("some ref updates failed: %s"), transaction_err.buf);
>> +				ref_transaction_for_each_rejected_update(transaction,
>> +										 print_rejected_update, NULL);
>> +			} else {
>> +				/* In atomic mode, all updates failed */
>> +				ret = error(_("failed to update refs: %s"), transaction_err.buf);
>> +				goto cleanup;
>> +			}
>> +		}
>>   	}
>>   
>>   	merge_finalize(&merge_opt, &result);
> Patrick

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 0/2] replay: add --update-refs option
  2025-09-08 14:33 ` Kristoffer Haugsbakk
@ 2025-09-09  7:04   ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-09  7:04 UTC (permalink / raw)
  To: Kristoffer Haugsbakk, git
  Cc: Junio C Hamano, Christian Couder, Karthik Nayak, Justin Tobler,
	Elijah Newren, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin


On 08/09/25 20:03, Kristoffer Haugsbakk wrote:
> On Mon, Sep 8, 2025, at 06:36, Siddharth Asthana wrote:
>> This patch series adds a --update-refs option to git replay. Right now,
>> when you use git replay, you need to pipe its output to git update-ref
>> like this:
>> [snip]
> Both patches introduce whitespace errors.  You can check with
> `ci/check-whitespace.sh`.
Thanks for catching that. I will fix the whitespace issues and run the 
script before sending v2.
>
> That script will suggest a way to fix it.
>
> There’s also a `\ No newline at end of file` (I don’t think whitespace-
> check checks that).

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 2/2] replay: document --update-refs and --batch options
  2025-09-08 14:40   ` Kristoffer Haugsbakk
@ 2025-09-09  7:06     ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-09  7:06 UTC (permalink / raw)
  To: Kristoffer Haugsbakk, git
  Cc: Junio C Hamano, Christian Couder, Karthik Nayak, Justin Tobler,
	Elijah Newren, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin


On 08/09/25 20:10, Kristoffer Haugsbakk wrote:
> On Mon, Sep 8, 2025, at 06:36, Siddharth Asthana wrote:
>> [snip]
>> diff --git a/Documentation/git-replay.adoc
>> b/Documentation/git-replay.adoc
>> index 0b12bf8aa4..cc9f868c2f 100644
>> --- a/Documentation/git-replay.adoc
>> +++ b/Documentation/git-replay.adoc
>> @@ -9,16 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new
>> base, works with bare repos t
>>   SYNOPSIS
>>   --------
>>   [verse]
>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> |
>> --advance <branch>) <revision-range>...
>> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--update | --update-refs [--batch]] <revision-range>...


Hi Kristoffer,


> Another downside of making a separate commit for the documentation is
> that now `t/t0450-txt-doc-vs-help.sh` will likely fail for your first
> commit.  One of the tests makes sure that the synopsis and the `.adoc`
> is in synch.


You are right, I will combine the documentation with the implementation 
patch in v2 to avoid the test failure.

Thanks,
Siddharth


>
>>   DESCRIPTION
>>   -----------
>> [snip]

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 0/2] replay: add --update-refs option
  2025-09-08  4:36 [PATCH 0/2] replay: add --update-refs option Siddharth Asthana
                   ` (3 preceding siblings ...)
  2025-09-08 14:33 ` Kristoffer Haugsbakk
@ 2025-09-09  7:13 ` Elijah Newren
  2025-09-09  7:47   ` Christian Couder
  2025-09-26 23:08 ` [PATCH v2 0/1] replay: make atomic ref updates the default behavior Siddharth Asthana
  5 siblings, 1 reply; 129+ messages in thread
From: Elijah Newren @ 2025-09-09  7:13 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, Junio C Hamano, Christian Couder, Karthik Nayak,
	Justin Tobler, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin

On Sun, Sep 7, 2025 at 9:36 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> This patch series adds a --update-refs option to git replay. Right now,
> when you use git replay, you need to pipe its output to git update-ref
> like this:
>
>     git replay --onto main topic1..topic2 | git update-ref --stdin
>
> This works fine, but it means running two commands and doesn't give you
> atomic transactions by default. The new --update-refs option lets you do
> the ref updates directly:
>
>     git replay --update-refs --onto main topic1..topic2
>
> I discussed this feature with Christian Couder earlier, and we agreed that
> it would be useful for server-side operations where you want atomic updates.

Seems fair...but why not make --update-refs the default and add an
option for those that just want the update commands?

> The way it works:
> - By default, it uses atomic transactions (all refs get updated or none do)
> - There's a --batch option if you want some updates to succeed even if
>   others fail
> - It works with bare repositories, which is important for server operations
>   like Gitaly
> - When it succeeds, it doesn't print anything (just like git update-ref
>   --stdin)

Seems fair.

> This should help with git replay's goal of being good for server-side
> operations.

I'm slightly confused by this statement; there's multiple ways to
interpret it -- various antecedents of "This", questions about whether
you are saying git replay has one goal or you are just helping with
one of its goals, and leaves to the reader to guess which part is
helpful (is it the ergonomics -- why does that matter server-side?  Is
it the atomicity?  Then why did you also add --batch and --update?  Is
it something else?)  Perhaps this sentence can be dropped or
completely rewritten?

> It also makes the command simpler to use since you don't need
> the pipeline anymore, and the atomic behavior is better for reliability.

Yeah, makes sense...but why not just make it the default behavior
instead of requiring an extra flag?  (The command is marked as
experimental...)

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 2/2] replay: document --update-refs and --batch options
  2025-09-09  6:36     ` Siddharth Asthana
@ 2025-09-09  7:26       ` Christian Couder
  2025-09-10 20:26         ` Siddharth Asthana
  0 siblings, 1 reply; 129+ messages in thread
From: Christian Couder @ 2025-09-09  7:26 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, Junio C Hamano, Karthik Nayak, Justin Tobler, Elijah Newren,
	Patrick Steinhardt, Toon Claes, John Cai, Johannes Schindelin

Hi Siddharth,

On Tue, Sep 9, 2025 at 8:36 AM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> On 08/09/25 11:30, Christian Couder wrote:
> > On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana

> >> Also document the --batch option which can be used with --update-refs
> >> to allow partial failures in ref updates.

> > It looks like a --update option was also added by the previous patch.
> > Is it documented here too?
> >
> > Why was this [--update | --update-refs [--batch]] set of options
> > selected over other possibilities like for example
> > [--update-iteratively | --update-atomically | --update-batch]?

> I was trying to provide both simple and advanced modes. --update for
> users who just want "make it work like piping to git update-ref --stdin"
> and --update-refs for those who want control over transaction modes. But
> I see this creates confusion.
>
> Would you prefer a single option like --update-refs with an optional
> mode parameter? Something like --update-refs[=batch] where default is
> atomic?

My preference would be something like [--update-atomically |
--update-batch] first. (Maybe names like `--batch-update` and
`--atomic-update` are better?)

And then something like --update-iteratively could perhaps be added as
an alternative, if:

  - it works exactly the same as piping to `git update-ref --stdin`, and
  - some users want to use it to blindly replace piping to `git
update-ref --stdin`, and
  - we document that it is not efficient (compared to
update-atomically and --update-batch) and should only be used to
blindly (bug for bug) replace piping to `git update-ref --stdin` when
performance is not an issue.

> > Also how does this --update-refs option compare to the --update-refs
> > option in git rebase? Is it working in the same way?

> No, they are different. git rebase --update-refs updates refs that point
> to commits being rebased. --update-refs updates the target branches from
> the replay operation itself. The naming collision is unfortunate should
> I use a different name?

Yeah, my opinion is that "rebase" and "replay" are commands doing
similar things, so having an `--update-refs` option in both commands
is a good thing only if the option has the same purpose in both
commands. If the purpose is a bit different, I think it's better to
use different names to avoid confusion.

> >> +--update-refs::
> >> +       Update the relevant refs using ref transactions instead of outputting
> >> +       update-ref commands. By default, uses atomic mode where all ref updates
> >> +       succeed or all fail.
> > This seems to imply that --update doesn't update the refs atomically.
> That correct --update doesn't use transactions it updates refs one by
> one like `git update-ref --stdin` does. Should I make this clearer in
> the documentation?

Yes, please.

> >> Use with `--batch` to allow partial updates.
> > What about --update, when should it be used?
> Good point. My thinking was --update for simple cases where you want the
> exact same behavior as piping to `git update-ref --stdin` and
> --update-refs when you want transaction guarantees. But I am starting to
> think this distinction might be confusing users more than helping them.
>
> Would it be cleaner to just have --update-refs with the batch mode
> option and drop --update entirely? The sequential behavior can be
> achieved with --update-refs --batch if someone really needs it.

About the options that should be implemented, see my opinion above.

About possible confusion, I think that to avoid it, it is important to:

  - name the options properly (see above what I think about the
`--update-refs` name), and to

  - document thoroughly how all the options differ from each other and
from piping to `git update-ref --stdin`

Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 1/2] replay: add --update-refs option
  2025-09-08  4:36 ` [PATCH 1/2] " Siddharth Asthana
  2025-09-08  9:54   ` Patrick Steinhardt
@ 2025-09-09  7:32   ` Elijah Newren
  2025-09-10 17:58     ` Siddharth Asthana
  1 sibling, 1 reply; 129+ messages in thread
From: Elijah Newren @ 2025-09-09  7:32 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, Junio C Hamano, Christian Couder, Karthik Nayak,
	Justin Tobler, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin

On Sun, Sep 7, 2025 at 9:36 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
[...]
> Option validation ensures --update-refs cannot be used with the existing
> --update option, and --batch can only be used with --update-refs.

There is no existing --update option.

[...]
> +       int update_directly = 0;
> +       int update_refs_flag = 0;
> +       int batch_mode = 0;

Why are we adding three kinds of updates?  You covered two in the
commit message, but mostly only motivated one, and then added three?

> +               OPT_BOOL(0, "update", &update_directly,
> +                        N_("update branches directly instead of outputting update commands")),
> +               OPT_BOOL(0, "update-refs", &update_refs_flag,
> +                        N_("update branches using ref transactions")),
> +               OPT_BOOL(0, "batch", &batch_mode,
> +                        N_("allow partial ref updates in batch mode")),

Three modes and I can't figure out how update_directly differs from
the others from the description.  Is it different?

Also, --batch seems like a funny name since update-refs is also
updating refs in a batch.  I'd suggest coming up with a new name...but
is there clamor for it?  You mostly motivated the atomic updates, and
I think it might be better to just implement those and then add more
flags later if needed.

> @@ -399,6 +461,7 @@ int cmd_replay(int argc,
>
>         init_basic_merge_options(&merge_opt, repo);
>         memset(&result, 0, sizeof(result));
> +       result.clean = 1;  /* Assume clean until proven otherwise */

I don't understand why this change is needed or helpful.  I don't
think it changes behavior looking at the existing code, but to me,
result is supposed to be the result of a merge operation, not an
input, and should not be set other than being cleared initially by the
caller.  The comment feels slightly misleading to me, as well.  So,
I'm surprised by this change and would like to hear the motivation
behind it; could you clarify?  Did I miss something about how you
depend on this being set even if the list of commits to replay is
empty or something?

> -                               printf("update %s %s %s\n",
> -                                      decoration->name,
> -                                      oid_to_hex(&last_commit->object.oid),
> -                                      oid_to_hex(&commit->object.oid));
> +                               if (update_directly) {
> +                                       if (update_ref_direct(repo, decoration->name,
> +                                                            &last_commit->object.oid,
> +                                                            &commit->object.oid) < 0) {
> +                                               ret = -1;
> +                                               goto cleanup;
> +                                       }
> +                               } else if (transaction) {
> +                                       if (add_ref_to_transaction(transaction, decoration->name,
> +                                                                  &last_commit->object.oid,
> +                                                                  &commit->object.oid,
> +                                                                  &transaction_err) < 0) {
> +                                               ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
> +                                               goto cleanup;
> +                                       }
> +                               } else {
> +                                       printf("update %s %s %s\n",
> +                                              decoration->name,
> +                                              oid_to_hex(&last_commit->object.oid),
> +                                              oid_to_hex(&commit->object.oid));
> +                               }

Who would want the update_ref_direct() branch of code here?  Can we
just toss it?

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 0/2] replay: add --update-refs option
  2025-09-09  7:13 ` Elijah Newren
@ 2025-09-09  7:47   ` Christian Couder
  2025-09-09  9:19     ` Elijah Newren
  0 siblings, 1 reply; 129+ messages in thread
From: Christian Couder @ 2025-09-09  7:47 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Siddharth Asthana, git, Junio C Hamano, Karthik Nayak,
	Justin Tobler, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin

On Tue, Sep 9, 2025 at 9:14 AM Elijah Newren <newren@gmail.com> wrote:
>
> On Sun, Sep 7, 2025 at 9:36 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:

> Seems fair...but why not make --update-refs the default and add an
> option for those that just want the update commands?

If this patch series had been sent a few months after `git replay` was
introduced, I would have been fine with this series making `git
replay` update the refs by default while adding an option that only
outputs the commands. Unfortunately `git replay` seems to have been
introduced in v2.44.0 (Feb 22, 2024), so more than 18 months ago. So
even if it is marked as experimental, it's perhaps a bit late to make
such a relatively big change in it?

> > The way it works:
> > - By default, it uses atomic transactions (all refs get updated or none do)
> > - There's a --batch option if you want some updates to succeed even if
> >   others fail
> > - It works with bare repositories, which is important for server operations
> >   like Gitaly
> > - When it succeeds, it doesn't print anything (just like git update-ref
> >   --stdin)
>
> Seems fair.
>
> > This should help with git replay's goal of being good for server-side
> > operations.
>
> I'm slightly confused by this statement; there's multiple ways to
> interpret it -- various antecedents of "This", questions about whether
> you are saying git replay has one goal or you are just helping with
> one of its goals, and leaves to the reader to guess which part is
> helpful (is it the ergonomics -- why does that matter server-side?  Is
> it the atomicity?  Then why did you also add --batch and --update?  Is
> it something else?)  Perhaps this sentence can be dropped or
> completely rewritten?

The way I understood this sentence is that `git replay` is already
useful on the server side (because it performs all the operations in
memory and doesn't need a work tree), and the new feature added by the
patch series reinforces this because atomic operations are often
better on the server side.

Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 1/2] replay: add --update-refs option
  2025-09-09  6:58     ` Siddharth Asthana
@ 2025-09-09  9:00       ` Patrick Steinhardt
  0 siblings, 0 replies; 129+ messages in thread
From: Patrick Steinhardt @ 2025-09-09  9:00 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, Junio C Hamano, Christian Couder, Karthik Nayak,
	Justin Tobler, Elijah Newren, Toon Claes, John Cai,
	Johannes Schindelin

On Tue, Sep 09, 2025 at 12:28:29PM +0530, Siddharth Asthana wrote:
> On 08/09/25 15:24, Patrick Steinhardt wrote:
> > Is there a strong reason why a user would want to update refs one by
> > one? If not, let's not add new code to our base that does so. This is
> > known to be inperformant for the reftable backend, but also for the
> > files backend in some cases.
> 
> You are absolutely right about the performance concern. My thinking was to
> provide a simple mode that exactly mimics "git replay | git update-ref
> --stdin" behavior, but I see that's not worth the performance cost.
> 
> I will remove the individual update function and only use batched
> transactions with REF_TRANSACTION_ALLOW_FAILURE when needed.

We can still extend the functionality at a later point if we discover
any use cases for those.

> > > @@ -319,6 +355,12 @@ int cmd_replay(int argc,
> > >   			   N_("replay onto given commit")),
> > >   		OPT_BOOL(0, "contained", &contained,
> > >   			 N_("advance all branches contained in revision-range")),
> > > +		OPT_BOOL(0, "update", &update_directly,
> > > +			 N_("update branches directly instead of outputting update commands")),
> > > +		OPT_BOOL(0, "update-refs", &update_refs_flag,
> > > +			 N_("update branches using ref transactions")),
> > > +		OPT_BOOL(0, "batch", &batch_mode,
> > > +			 N_("allow partial ref updates in batch mode")),
> > >   		OPT_END()
> > >   	};
> > So I think we should reduce this to only accept two flags:
> > `--update-refs` and a flag that accepts a subset of refs failing.o
> > 
> > We might also want to make this something like `--update-refs[=<mode>]`,
> > where `<mode>` could be "allow-failures".
> 
> 
> That make sense. Would you prefer `--update-refs` with `--allow-failures` as
> a separate flag? I am leaning toward that since it's clearer than the
> parameter syntax.

I'd personally prefer `--update-refs[=<mode>]`. The reason is mostly
that it makes it easier to discover what flags are related to the
`--update-refs` infra and you have to worry less about catching any kind
of incompatible flag combinations.

Patrick

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 0/2] replay: add --update-refs option
  2025-09-09  7:47   ` Christian Couder
@ 2025-09-09  9:19     ` Elijah Newren
  2025-09-09 16:44       ` Junio C Hamano
  0 siblings, 1 reply; 129+ messages in thread
From: Elijah Newren @ 2025-09-09  9:19 UTC (permalink / raw)
  To: Christian Couder
  Cc: Siddharth Asthana, git, Junio C Hamano, Karthik Nayak,
	Justin Tobler, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin

On Tue, Sep 9, 2025 at 12:47 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
> On Tue, Sep 9, 2025 at 9:14 AM Elijah Newren <newren@gmail.com> wrote:
> >
> > On Sun, Sep 7, 2025 at 9:36 PM Siddharth Asthana
> > <siddharthasthana31@gmail.com> wrote:
>
> > Seems fair...but why not make --update-refs the default and add an
> > option for those that just want the update commands?
>
> If this patch series had been sent a few months after `git replay` was
> introduced, I would have been fine with this series making `git
> replay` update the refs by default while adding an option that only
> outputs the commands. Unfortunately `git replay` seems to have been
> introduced in v2.44.0 (Feb 22, 2024), so more than 18 months ago. So
> even if it is marked as experimental, it's perhaps a bit late to make
> such a relatively big change in it?

I don't think so; we marked it as experimental much more prominently
than other commands -- in the .c file, and three separate places in
the documentation.  All other commands appear to have only been marked
as experimental in one place and never the C file, so this one is four
times more experimental than any other command.  Plus, the worry about
it being set in stone and the need to make it malleable was *exactly*
why the requests were made to be so much more clear that this command
needed the flexibility to change
(https://lore.kernel.org/git/CABPp-BFrVfGHOrBk7g=4TkGxDv=oSqF1FOkhp6WVbxUV-2yveQ@mail.gmail.com/).
Plus, it's currently only used server-side, so it'd probably only mean
GitLab (you), GitHub (me), and a few other users would need to update,
all of whom should be aware of the warnings.

We could add a config setting to allow defaulting to --no-update-refs
or whatever we want to call it.

> > > The way it works:
> > > - By default, it uses atomic transactions (all refs get updated or none do)
> > > - There's a --batch option if you want some updates to succeed even if
> > >   others fail
> > > - It works with bare repositories, which is important for server operations
> > >   like Gitaly
> > > - When it succeeds, it doesn't print anything (just like git update-ref
> > >   --stdin)
> >
> > Seems fair.
> >
> > > This should help with git replay's goal of being good for server-side
> > > operations.
> >
> > I'm slightly confused by this statement; there's multiple ways to
> > interpret it -- various antecedents of "This", questions about whether
> > you are saying git replay has one goal or you are just helping with
> > one of its goals, and leaves to the reader to guess which part is
> > helpful (is it the ergonomics -- why does that matter server-side?  Is
> > it the atomicity?  Then why did you also add --batch and --update?  Is
> > it something else?)  Perhaps this sentence can be dropped or
> > completely rewritten?
>
> The way I understood this sentence is that `git replay` is already
> useful on the server side (because it performs all the operations in
> memory and doesn't need a work tree), and the new feature added by the
> patch series reinforces this because atomic operations are often
> better on the server side.

You often word things well; I like that sentence.  The original makes
me guess and wonder whether something like that is the intent; can we
replace the original sentence with your description?

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 0/2] replay: add --update-refs option
  2025-09-09  9:19     ` Elijah Newren
@ 2025-09-09 16:44       ` Junio C Hamano
  2025-09-09 19:52         ` Elijah Newren
  0 siblings, 1 reply; 129+ messages in thread
From: Junio C Hamano @ 2025-09-09 16:44 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, Siddharth Asthana, git, Karthik Nayak,
	Justin Tobler, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin

Elijah Newren <newren@gmail.com> writes:

>> > Seems fair...but why not make --update-refs the default and add an
>> > option for those that just want the update commands?
>>
>> If this patch series had been sent a few months after `git replay` was
>> introduced, I would have been fine with this series making `git
>> replay` update the refs by default while adding an option that only
>> outputs the commands. Unfortunately `git replay` seems to have been
>> introduced in v2.44.0 (Feb 22, 2024), so more than 18 months ago. So
>> even if it is marked as experimental, it's perhaps a bit late to make
>> such a relatively big change in it?
>
> I don't think so; we marked it as experimental much more prominently
> than other commands -- in the .c file, and three separate places in
> the documentation.

When we are talking about a change that breaks an established
end-user expectation, it does not matter much if we wrote anything
in the .c source files.  The end-user facing documentation does.

And as you said, "git replay -h" and "git replay --help" prominently
show that the experimental nature of the command.

If this new behaviour is a clear improvement for majority of use
cases, I am perfectly fine with changing the default behaviour so
that everybody will benefit.  It may still be good to add an option
to allow the users to ask for the traditional "we'll give you a list
of updates you can apply as you see fit, but would not update the
refs ourselves" mode, though.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 2/2] replay: document --update-refs and --batch options
  2025-09-08  4:36 ` [PATCH 2/2] replay: document --update-refs and --batch options Siddharth Asthana
  2025-09-08  6:00   ` Christian Couder
  2025-09-08 14:40   ` Kristoffer Haugsbakk
@ 2025-09-09 19:20   ` Andrei Rybak
  2025-09-10 20:28     ` Siddharth Asthana
  2 siblings, 1 reply; 129+ messages in thread
From: Andrei Rybak @ 2025-09-09 19:20 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: Junio C Hamano, Christian Couder, Karthik Nayak, Justin Tobler,
	Elijah Newren, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin

hello, Siddharth Asthana

On 08/09/2025 06:36, Siddharth Asthana wrote:
> @@ -91,6 +120,27 @@ $ git replay --advance target origin/main..mybranch
>   update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
>   ------------
>   
> +To rebase `mybranch` onto `target` and update the ref directly:
> +
> +------------
> +$ git replay --update --onto target origin/main..mybranch
> +# No output; mybranch is updated directly
> +------------
> +
> +To rebase `mybranch` onto `target` using atomic ref transactions:
> +
> +------------
> +$ git replay --update-refs --onto target origin/main..mybranch
> +# No output; mybranch is updated atomically
> +------------
> +
> +To rebase multiple branches with partial failure tolerance:
> +
> +------------
> +$ git replay --update-refs --batch --contained --onto origin/main origin/main..tipbranch
> +# No output; refs updated in batch mode, warnings for any failures
> +------------
> +
>   Note that the first two examples replay the exact same commits and on
>   top of the exact same new base, they only differ in that the first
>   provides instructions to make mybranch point at the new commits and

Adding new examples above this paragraph separates it from the existing 
examples it refers to.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 0/2] replay: add --update-refs option
  2025-09-09 16:44       ` Junio C Hamano
@ 2025-09-09 19:52         ` Elijah Newren
  0 siblings, 0 replies; 129+ messages in thread
From: Elijah Newren @ 2025-09-09 19:52 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Christian Couder, Siddharth Asthana, git, Karthik Nayak,
	Justin Tobler, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin

On Tue, Sep 9, 2025 at 9:44 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> >> > Seems fair...but why not make --update-refs the default and add an
> >> > option for those that just want the update commands?
> >>
> >> If this patch series had been sent a few months after `git replay` was
> >> introduced, I would have been fine with this series making `git
> >> replay` update the refs by default while adding an option that only
> >> outputs the commands. Unfortunately `git replay` seems to have been
> >> introduced in v2.44.0 (Feb 22, 2024), so more than 18 months ago. So
> >> even if it is marked as experimental, it's perhaps a bit late to make
> >> such a relatively big change in it?
> >
> > I don't think so; we marked it as experimental much more prominently
> > than other commands -- in the .c file, and three separate places in
> > the documentation.
>
> When we are talking about a change that breaks an established
> end-user expectation, it does not matter much if we wrote anything
> in the .c source files.  The end-user facing documentation does.
>
> And as you said, "git replay -h" and "git replay --help" prominently
> show that the experimental nature of the command.

I should have clarified -- the .c change was specifically about making
"git replay -h" show the experimental nature of the command; if it was
just a code comment, I'd agree that it didn't matter, but it was
specifically about making the experimental status known to end users
in the short usage message:

$ git grep -2 EXPERIMENTAL '*.c'
builtin/replay.c-
builtin/replay.c-       const char * const replay_usage[] = {
builtin/replay.c:               N_("(EXPERIMENTAL!) git replay "
builtin/replay.c-                  "([--contained] --onto <newbase> |
--advance <branch>) "
builtin/replay.c-                  "<revision-range>..."),
$

> If this new behaviour is a clear improvement for majority of use
> cases, I am perfectly fine with changing the default behaviour so
> that everybody will benefit.  It may still be good to add an option
> to allow the users to ask for the traditional "we'll give you a list
> of updates you can apply as you see fit, but would not update the
> refs ourselves" mode, though.

Yep.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 1/2] replay: add --update-refs option
  2025-09-09  7:32   ` Elijah Newren
@ 2025-09-10 17:58     ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-10 17:58 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, Junio C Hamano, Christian Couder, Karthik Nayak,
	Justin Tobler, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin


On 09/09/25 13:02, Elijah Newren wrote:
> On Sun, Sep 7, 2025 at 9:36 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
> [...]


Hi Elijah,


>> Option validation ensures --update-refs cannot be used with the existing
>> --update option, and --batch can only be used with --update-refs.
> There is no existing --update option.


You're right, poor wording in my commit message. Both options are new in 
this series.


>
> [...]
>> +       int update_directly = 0;
>> +       int update_refs_flag = 0;
>> +       int batch_mode = 0;
> Why are we adding three kinds of updates?  You covered two in the
> commit message, but mostly only motivated one, and then added three?


That's fair criticism. I was trying to cover all bases but ended up with 
confusing options. The three were:
- --update: individual ref updates (like piping to update-ref --stdin)
- --update-refs: atomic transactions
- --batch: allow partial failures with --update-refs

But as Patrick pointed out, individual updates are inefficient and 
everyone seems to prefer simpler options.


>
>> +               OPT_BOOL(0, "update", &update_directly,
>> +                        N_("update branches directly instead of outputting update commands")),
>> +               OPT_BOOL(0, "update-refs", &update_refs_flag,
>> +                        N_("update branches using ref transactions")),
>> +               OPT_BOOL(0, "batch", &batch_mode,
>> +                        N_("allow partial ref updates in batch mode")),
> Three modes and I can't figure out how update_directly differs from
> the others from the description.  Is it different?


Yes, update_directly calls refs_update_ref() for each ref individually, 
while update_refs_flag uses ref transactions. But Patrick's performance 
concerns make me think we should drop the individual approach entirely.


>
> Also, --batch seems like a funny name since update-refs is also
> updating refs in a batch.  I'd suggest coming up with a new name...but
> is there clamor for it?  You mostly motivated the atomic updates, and
> I think it might be better to just implement those and then add more
> flags later if needed.
>
>> @@ -399,6 +461,7 @@ int cmd_replay(int argc,
>>
>>          init_basic_merge_options(&merge_opt, repo);
>>          memset(&result, 0, sizeof(result));
>> +       result.clean = 1;  /* Assume clean until proven otherwise */
> I don't understand why this change is needed or helpful.  I don't
> think it changes behavior looking at the existing code, but to me,
> result is supposed to be the result of a merge operation, not an
> input, and should not be set other than being cleared initially by the
> caller.  The comment feels slightly misleading to me, as well.  So,
> I'm surprised by this change and would like to hear the motivation
> behind it; could you clarify?  Did I miss something about how you
> depend on this being set even if the list of commits to replay is
> empty or something?


You caught an error in my logic. I was trying to handle the case where 
no commits are replayed (empty range), but you are right - result should 
only be set by merge operations. The existing code already handles empty 
ranges correctly by never entering the replay loop. I will remove this 
line in v2.


>
>> -                               printf("update %s %s %s\n",
>> -                                      decoration->name,
>> -                                      oid_to_hex(&last_commit->object.oid),
>> -                                      oid_to_hex(&commit->object.oid));
>> +                               if (update_directly) {
>> +                                       if (update_ref_direct(repo, decoration->name,
>> +                                                            &last_commit->object.oid,
>> +                                                            &commit->object.oid) < 0) {
>> +                                               ret = -1;
>> +                                               goto cleanup;
>> +                                       }
>> +                               } else if (transaction) {
>> +                                       if (add_ref_to_transaction(transaction, decoration->name,
>> +                                                                  &last_commit->object.oid,
>> +                                                                  &commit->object.oid,
>> +                                                                  &transaction_err) < 0) {
>> +                                               ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
>> +                                               goto cleanup;
>> +                                       }
>> +                               } else {
>> +                                       printf("update %s %s %s\n",
>> +                                              decoration->name,
>> +                                              oid_to_hex(&last_commit->object.oid),
>> +                                              oid_to_hex(&commit->object.oid));
>> +                               }
> Who would want the update_ref_direct() branch of code here?  Can we
> just toss it?


Given the performance concerns and the confusion it is causing, yes. 
Let's toss it and focus on the transaction-based approach.

Based on all the feedback, I am thinking of simplifying to:
- Default: update refs atomically using transactions
- --output-commands: print update commands (for the traditional pipeline 
workflow)
- --allow-partial: allow some ref updates to succeed while others fail

This addresses your point about making the better behavior default while 
still supporting existing workflows.

Thanks,
Siddharth



^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 2/2] replay: document --update-refs and --batch options
  2025-09-09  7:26       ` Christian Couder
@ 2025-09-10 20:26         ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-10 20:26 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Karthik Nayak, Justin Tobler, Elijah Newren,
	Patrick Steinhardt, Toon Claes, John Cai, Johannes Schindelin


On 09/09/25 12:56, Christian Couder wrote:
> Hi Siddharth,
>
> On Tue, Sep 9, 2025 at 8:36 AM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> On 08/09/25 11:30, Christian Couder wrote:
>>> On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana
>>>> Also document the --batch option which can be used with --update-refs
>>>> to allow partial failures in ref updates.
>>> It looks like a --update option was also added by the previous patch.
>>> Is it documented here too?
>>>
>>> Why was this [--update | --update-refs [--batch]] set of options
>>> selected over other possibilities like for example
>>> [--update-iteratively | --update-atomically | --update-batch]?
>> I was trying to provide both simple and advanced modes. --update for
>> users who just want "make it work like piping to git update-ref --stdin"
>> and --update-refs for those who want control over transaction modes. But
>> I see this creates confusion.
>>
>> Would you prefer a single option like --update-refs with an optional
>> mode parameter? Something like --update-refs[=batch] where default is
>> atomic?
> My preference would be something like [--update-atomically |
> --update-batch] first. (Maybe names like `--batch-update` and
> `--atomic-update` are better?)
>
> And then something like --update-iteratively could perhaps be added as
> an alternative, if:
>
>    - it works exactly the same as piping to `git update-ref --stdin`, and
>    - some users want to use it to blindly replace piping to `git
> update-ref --stdin`, and
>    - we document that it is not efficient (compared to
> update-atomically and --update-batch) and should only be used to
> blindly (bug for bug) replace piping to `git update-ref --stdin` when
> performance is not an issue.
>
>>> Also how does this --update-refs option compare to the --update-refs
>>> option in git rebase? Is it working in the same way?
>> No, they are different. git rebase --update-refs updates refs that point
>> to commits being rebased. --update-refs updates the target branches from
>> the replay operation itself. The naming collision is unfortunate should
>> I use a different name?


Hi Christian,


> Yeah, my opinion is that "rebase" and "replay" are commands doing
> similar things, so having an `--update-refs` option in both commands
> is a good thing only if the option has the same purpose in both
> commands. If the purpose is a bit different, I think it's better to
> use different names to avoid confusion.


You make an excellent point about the naming collision. The purposes are 
indeed different:
- `git rebase --update-refs` updates refs that point to commits being 
rebased
- `git replay --update-refs` (in my patch) updates the target branches 
from the replay operation

Since Elijah and Junio have endorsed making ref updates the default 
behavior, this actually simplifies our naming significantly. The new 
design would be:
- Default: atomic ref updates using transactions (no flag needed)
- `--output-commands`: print update commands for traditional pipeline users
- `--allow-partial`: enable partial failure tolerance when some refs 
can't be updated

This completely avoids the rebase naming collision while providing the 
atomic transaction behavior that's important for server-side operations 
like Gitaly. The default behavior gives us the reliability we need 
without any naming confusion.

>
>>>> +--update-refs::
>>>> +       Update the relevant refs using ref transactions instead of outputting
>>>> +       update-ref commands. By default, uses atomic mode where all ref updates
>>>> +       succeed or all fail.
>>> This seems to imply that --update doesn't update the refs atomically.
>> That correct --update doesn't use transactions it updates refs one by
>> one like `git update-ref --stdin` does. Should I make this clearer in
>> the documentation?
> Yes, please.
>
>>>> Use with `--batch` to allow partial updates.
>>> What about --update, when should it be used?
>> Good point. My thinking was --update for simple cases where you want the
>> exact same behavior as piping to `git update-ref --stdin` and
>> --update-refs when you want transaction guarantees. But I am starting to
>> think this distinction might be confusing users more than helping them.
>>
>> Would it be cleaner to just have --update-refs with the batch mode
>> option and drop --update entirely? The sequential behavior can be
>> achieved with --update-refs --batch if someone really needs it.
> About the options that should be implemented, see my opinion above.
>
> About possible confusion, I think that to avoid it, it is important to:
>
>    - name the options properly (see above what I think about the
> `--update-refs` name), and to
>
>    - document thoroughly how all the options differ from each other and
> from piping to `git update-ref --stdin`
>
> Thanks.


Absolutely agree. The simplified approach with default atomic behavior 
eliminates most of the confusion points you identified. I will ensure 
the documentation clearly explains when users would want 
`--output-commands` (for custom scripting) versus  the default atomic 
behavior (for reliable operations).

The atomic-by-default approach also means better performance since we're 
using batched transactions (addressing Patrick's reftable concerns) and 
better UX since users get reliable behavior without needing to 
understand transaction modes.

Thanks for catching the naming issue christian - it led to a much 
cleaner design,
Siddharth



^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH 2/2] replay: document --update-refs and --batch options
  2025-09-09 19:20   ` Andrei Rybak
@ 2025-09-10 20:28     ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-10 20:28 UTC (permalink / raw)
  To: Andrei Rybak, git
  Cc: Junio C Hamano, Christian Couder, Karthik Nayak, Justin Tobler,
	Elijah Newren, Patrick Steinhardt, Toon Claes, John Cai,
	Johannes Schindelin


On 10/09/25 00:50, Andrei Rybak wrote:
> hello, Siddharth Asthana
>
> On 08/09/2025 06:36, Siddharth Asthana wrote:
>> @@ -91,6 +120,27 @@ $ git replay --advance target origin/main..mybranch
>>   update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
>>   ------------
>>   +To rebase `mybranch` onto `target` and update the ref directly:
>> +
>> +------------
>> +$ git replay --update --onto target origin/main..mybranch
>> +# No output; mybranch is updated directly
>> +------------
>> +
>> +To rebase `mybranch` onto `target` using atomic ref transactions:
>> +
>> +------------
>> +$ git replay --update-refs --onto target origin/main..mybranch
>> +# No output; mybranch is updated atomically
>> +------------
>> +
>> +To rebase multiple branches with partial failure tolerance:
>> +
>> +------------
>> +$ git replay --update-refs --batch --contained --onto origin/main 
>> origin/main..tipbranch
>> +# No output; refs updated in batch mode, warnings for any failures
>> +------------
>> +
>>   Note that the first two examples replay the exact same commits and on
>>   top of the exact same new base, they only differ in that the first
>>   provides instructions to make mybranch point at the new commits and
>

Hi Andrei,


> Adding new examples above this paragraph separates it from the 
> existing examples it refers to.


Good catch. I will restructure the examples section in v2 to maintain 
the logical flow, ensuring the explanatory paragraph stays connected to 
the examples it references.

Since I am moving to a default-behavior approach (atomic ref updates by 
default), the examples will be much simpler anyway:

     # Default behavior (atomic updates)
     git replay --onto target origin/main..mybranch

     # Traditional pipeline output
     git replay --output-commands --onto target origin/main..mybranch | 
git update-ref --stdin

This should make the documentation flow more naturally.

Thanks for the review,
Siddharth


^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v2 0/1] replay: make atomic ref updates the default behavior
  2025-09-08  4:36 [PATCH 0/2] replay: add --update-refs option Siddharth Asthana
                   ` (4 preceding siblings ...)
  2025-09-09  7:13 ` Elijah Newren
@ 2025-09-26 23:08 ` Siddharth Asthana
  2025-09-26 23:08   ` [PATCH v2 1/1] " Siddharth Asthana
                     ` (2 more replies)
  5 siblings, 3 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-26 23:08 UTC (permalink / raw)
  To: git
  Cc: gitster, christian.couder, ps, newren, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin,
	Siddharth Asthana

This is v2 of the git-replay atomic updates series.

Based on the extensive community feedback from v1, I've completely redesigned
the approach. Instead of adding new --update-refs options, this version makes
atomic ref updates the default behavior of git replay.

Why this change makes sense:
- git replay is explicitly marked as EXPERIMENTAL with behavior changes expected
- The command is primarily used server-side where atomic transactions are crucial
- Current pipeline approach (git replay | git update-ref --stdin) creates 
  coordination complexity and lacks atomic guarantees by default
- Patrick Steinhardt noted performance issues with individual ref updates 
  in reftable backend
- Elijah Newren and Junio Hamano endorsed making the better behavior default

The new design:
    # Default: atomic ref updates (no pipeline needed)
    git replay --onto main topic1..topic2

    # Traditional behavior preserved for compatibility  
    git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin

Key changes since v1:
- Made atomic ref updates the default instead of opt-in via --update-refs
- Eliminated confusing --update vs --update-refs option distinction  
- Avoided naming collision with git rebase --update-refs
- Fixed --allow-partial exit code behavior (exits 0 only if ALL updates succeed)
- Used die_for_incompatible_opt2() for consistent error reporting
- Updated documentation with proper line wrapping and consistent terminology
- Added comprehensive testing and performance considerations

This approach gives us atomic transactions by default while preserving full
backward compatibility for existing workflows that need the pipeline approach.

Thanks to Christian Couder, Patrick Steinhardt, Elijah Newren, Junio C Hamano,
Kristoffer Haugsbakk, and Andrei Rybak for the excellent feedback that led to 
this much cleaner design!

Siddharth Asthana (1):
  replay: make atomic ref updates the default behavior

 Documentation/git-replay.adoc |  76 +++++++++++++---
 builtin/replay.c              | 114 ++++++++++++++++++++---
 t/t3650-replay-basics.sh      | 166 ++++++++++++++++++++++++++++++++--
 3 files changed, 319 insertions(+), 37 deletions(-)

-- 
2.51.0


^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-09-26 23:08 ` [PATCH v2 0/1] replay: make atomic ref updates the default behavior Siddharth Asthana
@ 2025-09-26 23:08   ` Siddharth Asthana
  2025-09-30  8:23     ` Christian Couder
                       ` (2 more replies)
  2025-10-02 17:14   ` [PATCH v2 0/1] " Kristoffer Haugsbakk
  2025-10-13 18:33   ` [PATCH v3 0/3] replay: make atomic ref updates the default Siddharth Asthana
  2 siblings, 3 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-09-26 23:08 UTC (permalink / raw)
  To: git
  Cc: gitster, christian.couder, ps, newren, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin,
	Siddharth Asthana

The git replay command currently outputs update commands that must be
piped to git update-ref --stdin to actually update references:

    git replay --onto main topic1..topic2 | git update-ref --stdin

This design has significant limitations for server-side operations. The
two-command pipeline creates coordination complexity, provides no atomic
transaction guarantees by default, and complicates automation in bare
repository environments where git replay is primarily used.

During extensive mailing list discussion, multiple maintainers identified
that the current approach forces users to opt-in to atomic behavior rather
than defaulting to the safer, more reliable option. Elijah Newren noted
that the experimental status explicitly allows such behavior changes, while
Patrick Steinhardt highlighted performance concerns with individual ref
updates in the reftable backend.

The core issue is that git replay was designed around command output rather
than direct action. This made sense for a plumbing tool, but creates barriers
for the primary use case: server-side operations that need reliable, atomic
ref updates without pipeline complexity.

This patch changes the default behavior to update refs directly using Git's
ref transaction API:

    git replay --onto main topic1..topic2
    # No output; all refs updated atomically or none

The implementation uses ref_store_transaction_begin() with atomic mode by
default, ensuring all ref updates succeed or all fail as a single operation.
This leverages git replay's existing server-side strengths (in-memory operation,
no work tree requirement) while adding the atomic guarantees that server
operations require.

For users needing the traditional pipeline workflow, --output-commands
preserves the original behavior:

    git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin

The --allow-partial option enables partial failure tolerance. However, following
maintainer feedback, it implements a "strict success" model: the command exits
with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY
updates fail. This ensures that --allow-partial changes error reporting style
(warnings vs hard errors) but not success criteria, handling edge cases like
"no updates needed" cleanly.

Implementation details:
- Empty commit ranges now return success (exit code 0) rather than failure,
  as no commits to replay is a valid successful operation
- Added comprehensive test coverage with 12 new tests covering atomic behavior,
  option validation, bare repository support, and edge cases
- Fixed test isolation issues to prevent branch state contamination between tests
- Maintains C89 compliance and follows Git's established coding conventions
- Refactored option validation to use die_for_incompatible_opt2() for both
  --advance/--contained and --allow-partial/--output-commands conflicts,
  providing consistent error reporting
- Fixed --allow-partial exit code behavior to implement "strict success" model
  where any ref update failures result in exit code 1, even with partial tolerance
- Updated documentation with proper line wrapping, consistent terminology using
  "old default behavior", performance context, and reorganized examples for clarity
- Eliminates individual ref updates (refs_update_ref calls) that perform
  poorly with reftable backend
- Uses only batched ref transactions for optimal performance across all
  ref backends
- Avoids naming collision with git rebase --update-refs by using distinct
  option names
- Defaults to atomic behavior while preserving pipeline compatibility

The result is a command that works better for its primary use case (server-side
operations) while maintaining full backward compatibility for existing workflows.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc |  76 +++++++++++++---
 builtin/replay.c              | 114 ++++++++++++++++++++---
 t/t3650-replay-basics.sh      | 166 ++++++++++++++++++++++++++++++++--
 3 files changed, 319 insertions(+), 37 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 0b12bf8aa4..e104e0bc03 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,16 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--output-commands | --allow-partial] <revision-range>...
 
 DESCRIPTION
 -----------
 
 Takes ranges of commits and replays them onto a new location. Leaves
-the working tree and the index untouched, and updates no references.
-The output of this command is meant to be used as input to
-`git update-ref --stdin`, which would update the relevant branches
-(see the OUTPUT section below).
+the working tree and the index untouched, and by default updates the
+relevant references using atomic transactions. Use `--output-commands`
+to get the old default behavior where update commands that can be piped
+to `git update-ref --stdin` are emitted (see the OUTPUT section below).
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 
@@ -42,6 +42,20 @@ When `--advance` is specified, the update-ref command(s) in the output
 will update the branch passed as an argument to `--advance` to point at
 the new commits (in other words, this mimics a cherry-pick operation).
 
+--output-commands::
+	Output update-ref commands instead of updating refs directly.
+	When this option is used, the output can be piped to `git update-ref --stdin`
+	for successive, relatively slow, ref updates. This is equivalent to the
+	old default behavior.
+
+--allow-partial::
+	Allow some ref updates to succeed even if others fail. By default,
+	ref updates are atomic (all succeed or all fail). With this option,
+	failed updates are reported as warnings rather than causing the entire
+	command to fail. The command exits with code 0 only if all updates
+	succeed; any failures result in exit code 1. Cannot be used with
+	`--output-commands`.
+
 <revision-range>::
 	Range of commits to replay. More than one <revision-range> can
 	be passed, but in `--advance <branch>` mode, they should have
@@ -54,15 +68,20 @@ include::rev-list-options.adoc[]
 OUTPUT
 ------
 
-When there are no conflicts, the output of this command is usable as
-input to `git update-ref --stdin`.  It is of the form:
+By default, when there are no conflicts, this command updates the relevant
+references using atomic transactions and produces no output. All ref updates
+succeed or all fail (atomic behavior). Use `--allow-partial` to allow some
+updates to succeed while others fail.
+
+When `--output-commands` is used, the output is usable as input to
+`git update-ref --stdin`. It is of the form:
 
 	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 where the number of refs updated depends on the arguments passed and
-the shape of the history being replayed.  When using `--advance`, the
+the shape of the history being replayed. When using `--advance`, the
 number of refs updated is always one, but for `--onto`, it can be one
 or more (rebasing multiple branches simultaneously is supported).
 
@@ -77,30 +96,50 @@ is something other than 0 or 1.
 EXAMPLES
 --------
 
-To simply rebase `mybranch` onto `target`:
+To simply rebase `mybranch` onto `target` (default behavior):
 
 ------------
 $ git replay --onto target origin/main..mybranch
-update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
 To cherry-pick the commits from mybranch onto target:
 
 ------------
 $ git replay --advance target origin/main..mybranch
-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
 ------------
 
 Note that the first two examples replay the exact same commits and on
 top of the exact same new base, they only differ in that the first
-provides instructions to make mybranch point at the new commits and
-the second provides instructions to make target point at them.
+updates mybranch to point at the new commits and the second updates
+target to point at them.
+
+To get the old default behavior where update commands are emitted:
+
+------------
+$ git replay --output-commands --onto target origin/main..mybranch
+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
+------------
+
+To rebase multiple branches with partial failure tolerance:
+
+------------
+$ git replay --allow-partial --contained --onto origin/main origin/main..tipbranch
+------------
 
 What if you have a stack of branches, one depending upon another, and
 you'd really like to rebase the whole set?
 
 ------------
 $ git replay --contained --onto origin/main origin/main..tipbranch
+------------
+
+This automatically finds and rebases all branches contained within the
+`origin/main..tipbranch` range.
+
+Or if you want to see the old default behavior where update commands are emitted:
+
+------------
+$ git replay --output-commands --contained --onto origin/main origin/main..tipbranch
 update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
 update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
 update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
@@ -108,10 +147,19 @@ update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
 
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
-do:
+do. Here's an example where you explicitly specify which branches to rebase:
 
 ------------
 $ git replay --onto origin/main ^base branch1 branch2 branch3
+------------
+
+This gives you explicit control over exactly which branches are rebased,
+unlike the previous `--contained` example which automatically discovers them.
+
+To see the update commands that would be executed:
+
+------------
+$ git replay --output-commands --onto origin/main ^base branch1 branch2 branch3
 update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
 update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
 update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
diff --git a/builtin/replay.c b/builtin/replay.c
index 6172c8aacc..b6f9d53560 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -284,6 +284,28 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	return create_commit(repo, result->tree, pickme, replayed_base);
 }
 
+static int add_ref_to_transaction(struct ref_transaction *transaction,
+				  const char *refname,
+				  const struct object_id *new_oid,
+				  const struct object_id *old_oid,
+				  struct strbuf *err)
+{
+	return ref_transaction_update(transaction, refname, new_oid, old_oid,
+				      NULL, NULL, 0, "git replay", err);
+}
+
+static void print_rejected_update(const char *refname,
+				  const struct object_id *old_oid UNUSED,
+				  const struct object_id *new_oid UNUSED,
+				  const char *old_target UNUSED,
+				  const char *new_target UNUSED,
+				  enum ref_transaction_error err,
+				  void *cb_data UNUSED)
+{
+	const char *reason = ref_transaction_error_msg(err);
+	warning(_("failed to update %s: %s"), refname, reason);
+}
+
 int cmd_replay(int argc,
 	       const char **argv,
 	       const char *prefix,
@@ -294,6 +316,8 @@ int cmd_replay(int argc,
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
 	int contained = 0;
+	int output_commands = 0;
+	int allow_partial = 0;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -302,12 +326,15 @@ int cmd_replay(int argc,
 	struct merge_result result;
 	struct strset *update_refs = NULL;
 	kh_oid_map_t *replayed_commits;
+	struct ref_transaction *transaction = NULL;
+	struct strbuf transaction_err = STRBUF_INIT;
+	int commits_processed = 0;
 	int ret = 0;
 
-	const char * const replay_usage[] = {
+	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto <newbase> | --advance <branch>) "
-		   "<revision-range>..."),
+		   "[--output-commands | --allow-partial] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -319,6 +346,10 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &contained,
 			 N_("advance all branches contained in revision-range")),
+		OPT_BOOL(0, "output-commands", &output_commands,
+			 N_("output update commands instead of updating refs")),
+		OPT_BOOL(0, "allow-partial", &allow_partial,
+			 N_("allow some ref updates to succeed even if others fail")),
 		OPT_END()
 	};
 
@@ -330,9 +361,12 @@ int cmd_replay(int argc,
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (advance_name_opt && contained)
-		die(_("options '%s' and '%s' cannot be used together"),
-		    "--advance", "--contained");
+	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
+				  contained, "--contained");
+
+	die_for_incompatible_opt2(allow_partial, "--allow-partial",
+				  output_commands, "--output-commands");
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
@@ -389,6 +423,17 @@ int cmd_replay(int argc,
 	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
 			      &onto, &update_refs);
 
+	if (!output_commands) {
+		unsigned int transaction_flags = allow_partial ? REF_TRANSACTION_ALLOW_FAILURE : 0;
+		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
+							  transaction_flags,
+							  &transaction_err);
+		if (!transaction) {
+			ret = error(_("failed to begin ref transaction: %s"), transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
 	if (!onto) /* FIXME: Should handle replaying down to root commit */
 		die("Replaying down to root commit is not supported yet!");
 
@@ -407,6 +452,8 @@ int cmd_replay(int argc,
 		khint_t pos;
 		int hr;
 
+		commits_processed = 1;
+
 		if (!commit->parents)
 			die(_("replaying down to root commit is not supported yet!"));
 		if (commit->parents->next)
@@ -434,10 +481,18 @@ int cmd_replay(int argc,
 			if (decoration->type == DECORATION_REF_LOCAL &&
 			    (contained || strset_contains(update_refs,
 							  decoration->name))) {
-				printf("update %s %s %s\n",
-				       decoration->name,
-				       oid_to_hex(&last_commit->object.oid),
-				       oid_to_hex(&commit->object.oid));
+				if (output_commands) {
+					printf("update %s %s %s\n",
+					       decoration->name,
+					       oid_to_hex(&last_commit->object.oid),
+					       oid_to_hex(&commit->object.oid));
+				} else if (add_ref_to_transaction(transaction, decoration->name,
+								  &last_commit->object.oid,
+								  &commit->object.oid,
+								  &transaction_err) < 0) {
+					ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
+					goto cleanup;
+				}
 			}
 			decoration = decoration->next;
 		}
@@ -445,10 +500,33 @@ int cmd_replay(int argc,
 
 	/* In --advance mode, advance the target ref */
 	if (result.clean == 1 && advance_name) {
-		printf("update %s %s %s\n",
-		       advance_name,
-		       oid_to_hex(&last_commit->object.oid),
-		       oid_to_hex(&onto->object.oid));
+		if (output_commands) {
+			printf("update %s %s %s\n",
+			       advance_name,
+			       oid_to_hex(&last_commit->object.oid),
+			       oid_to_hex(&onto->object.oid));
+		} else if (add_ref_to_transaction(transaction, advance_name,
+						  &last_commit->object.oid,
+						  &onto->object.oid,
+						  &transaction_err) < 0) {
+			ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
+	/* Commit the ref transaction if we have one */
+	if (transaction && result.clean == 1) {
+		if (ref_transaction_commit(transaction, &transaction_err)) {
+			if (allow_partial) {
+				warning(_("some ref updates failed: %s"), transaction_err.buf);
+				ref_transaction_for_each_rejected_update(transaction,
+									 print_rejected_update, NULL);
+				ret = 0; /* Set failure even with allow_partial */
+			} else {
+				ret = error(_("failed to update refs: %s"), transaction_err.buf);
+				goto cleanup;
+			}
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
@@ -457,9 +535,17 @@ int cmd_replay(int argc,
 		strset_clear(update_refs);
 		free(update_refs);
 	}
-	ret = result.clean;
+
+	/* Handle empty ranges: if no commits were processed, treat as success */
+	if (!commits_processed)
+		ret = 1; /* Success - no commits to replay is not an error */
+	else
+		ret = result.clean;
 
 cleanup:
+	if (transaction)
+		ref_transaction_free(transaction);
+	strbuf_release(&transaction_err);
 	release_revisions(&revs);
 	free(advance_name);
 
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 58b3759935..8b4301e227 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git replay --onto main topic1..topic2 >result &&
+	git replay --output-commands --onto main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -67,9 +67,30 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 	test_cmp expect result
 '
 
+test_expect_success 'using replay with default atomic behavior (no output)' '
+	# Create a test branch that wont interfere with others
+	git branch atomic-test topic2 &&
+	git rev-parse atomic-test >atomic-test-old &&
+
+	# Default behavior: atomic ref updates (no output)
+	git replay --onto main topic1..atomic-test >output &&
+	test_must_be_empty output &&
+
+	# Verify the branch was updated
+	git rev-parse atomic-test >atomic-test-new &&
+	! test_cmp atomic-test-old atomic-test-new &&
+
+	# Verify the history is correct
+	git log --format=%s atomic-test >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
-	git -C bare replay --onto main topic1..topic2 >result-bare &&
-	test_cmp expect result-bare
+	git -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&
+
+	# The result should match what we got from the regular repo
+	test_cmp result result-bare
 '
 
 test_expect_success 'using replay to rebase with a conflict' '
@@ -86,7 +107,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
 	# 4th field of result is hash for main instead of hash for topic2
 
-	git replay --advance main topic1..topic2 >result &&
+	git replay --output-commands --advance main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -102,7 +123,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 '
 
 test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
-	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	git -C bare replay --output-commands --advance main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
@@ -115,7 +136,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 '
 
 test_expect_success 'using replay to also rebase a contained branch' '
-	git replay --contained --onto main main..topic3 >result &&
+	git replay --output-commands --contained --onto main main..topic3 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -139,12 +160,12 @@ test_expect_success 'using replay to also rebase a contained branch' '
 '
 
 test_expect_success 'using replay on bare repo to also rebase a contained branch' '
-	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	git -C bare replay --output-commands --contained --onto main main..topic3 >result-bare &&
 	test_cmp expect result-bare
 '
 
 test_expect_success 'using replay to rebase multiple divergent branches' '
-	git replay --onto main ^topic1 topic2 topic4 >result &&
+	git replay --output-commands --onto main ^topic1 topic2 topic4 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -168,7 +189,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
 '
 
 test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
-	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+	git -C bare replay --output-commands --contained --onto main ^main topic2 topic3 topic4 >result &&
 
 	test_line_count = 4 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -217,4 +238,131 @@ test_expect_success 'merge.directoryRenames=false' '
 		--onto rename-onto rename-onto..rename-from
 '
 
+# Tests for new default atomic behavior and options
+
+test_expect_success 'replay default behavior should not produce output when successful' '
+	git replay --onto main topic1..topic3 >output &&
+	test_must_be_empty output
+'
+
+test_expect_success 'replay with --output-commands produces traditional output' '
+	git replay --output-commands --onto main topic1..topic3 >output &&
+	test_line_count = 1 output &&
+	grep "^update refs/heads/topic3 " output
+'
+
+test_expect_success 'replay with --allow-partial should not produce output when successful' '
+	git replay --allow-partial --onto main topic1..topic3 >output &&
+	test_must_be_empty output
+'
+
+test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '
+	test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
+	grep "cannot be used together" error
+'
+
+test_expect_success 'replay with --contained updates multiple branches atomically' '
+	# Create fresh test branches based on the original structure
+	# contained-topic1 should be contained within the range to contained-topic3
+	git branch contained-base main &&
+	git checkout -b contained-topic1 contained-base &&
+	test_commit ContainedC &&
+	git checkout -b contained-topic3 contained-topic1 &&
+	test_commit ContainedG &&
+	test_commit ContainedH &&
+	git checkout main &&
+
+	# Store original states
+	git rev-parse contained-topic1 >contained-topic1-old &&
+	git rev-parse contained-topic3 >contained-topic3-old &&
+
+	# Use --contained to update multiple branches - this should update both
+	git replay --contained --onto main contained-base..contained-topic3 &&
+
+	# Verify both branches were updated
+	git rev-parse contained-topic1 >contained-topic1-new &&
+	git rev-parse contained-topic3 >contained-topic3-new &&
+	! test_cmp contained-topic1-old contained-topic1-new &&
+	! test_cmp contained-topic3-old contained-topic3-new
+'
+
+test_expect_success 'replay atomic behavior: all refs updated or none' '
+	# Store original state
+	git rev-parse topic4 >topic4-old &&
+
+	# Default atomic behavior
+	git replay --onto main main..topic4 &&
+
+	# Verify ref was updated
+	git rev-parse topic4 >topic4-new &&
+	! test_cmp topic4-old topic4-new &&
+
+	# Verify no partial state
+	git log --format=%s topic4 >actual &&
+	test_write_lines J I M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'replay works correctly with bare repositories' '
+	# Test atomic behavior in bare repo (important for Gitaly)
+	git checkout -b bare-test topic1 &&
+	test_commit BareTest &&
+
+	# Test with bare repo - replay the commits from main..bare-test to get the full history
+	git -C bare fetch .. bare-test:bare-test &&
+	git -C bare replay --onto main main..bare-test &&
+
+	# Verify the bare repo was updated correctly (no output)
+	git -C bare log --format=%s bare-test >actual &&
+	test_write_lines BareTest F C M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'replay --allow-partial with no failures produces no output' '
+	git checkout -b partial-test topic1 &&
+	test_commit PartialTest &&
+
+	# Should succeed silently even with partial mode
+	git replay --allow-partial --onto main topic1..partial-test >output &&
+	test_must_be_empty output
+'
+
+test_expect_success 'replay maintains ref update consistency' '
+	# Test that traditional vs atomic produce equivalent results
+	git checkout -b method1-test topic2 &&
+	git checkout -b method2-test topic2 &&
+
+	# Both methods should update refs to point to the same replayed commits
+	git replay --output-commands --onto main topic1..method1-test >update-commands &&
+	git update-ref --stdin <update-commands &&
+	git log --format=%s method1-test >traditional-result &&
+
+	# Direct atomic method should produce same commit history
+	git replay --onto main topic1..method2-test &&
+	git log --format=%s method2-test >atomic-result &&
+
+	# Both methods should produce identical commit histories
+	test_cmp traditional-result atomic-result
+'
+
+test_expect_success 'replay error messages are helpful and clear' '
+	# Test that error messages are clear
+	test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
+	grep "cannot be used together" error
+'
+
+test_expect_success 'replay with empty range produces no output and no changes' '
+	# Create a test branch for empty range testing
+	git checkout -b empty-test topic1 &&
+	git rev-parse empty-test >empty-test-before &&
+
+	# Empty range should succeed but do nothing
+	git replay --onto main empty-test..empty-test >output &&
+	test_must_be_empty output &&
+
+	# Branch should be unchanged
+	git rev-parse empty-test >empty-test-after &&
+	test_cmp empty-test-before empty-test-after
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-09-26 23:08   ` [PATCH v2 1/1] " Siddharth Asthana
@ 2025-09-30  8:23     ` Christian Couder
  2025-10-02 22:16       ` Siddharth Asthana
  2025-10-02 22:55       ` Elijah Newren
  2025-09-30 10:05     ` Phillip Wood
  2025-10-02 16:32     ` Elijah Newren
  2 siblings, 2 replies; 129+ messages in thread
From: Christian Couder @ 2025-09-30  8:23 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, gitster, ps, newren, code, rybak.a.v, karthik.188, jltobler,
	toon, johncai86, johannes.schindelin

On Sat, Sep 27, 2025 at 1:09 AM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> The git replay command currently outputs update commands that must be
> piped to git update-ref --stdin to actually update references:
>
>     git replay --onto main topic1..topic2 | git update-ref --stdin
>
> This design has significant limitations for server-side operations. The
> two-command pipeline creates coordination complexity, provides no atomic
> transaction guarantees by default, and complicates automation in bare
> repository environments where git replay is primarily used.

Yeah, right.

> During extensive mailing list discussion, multiple maintainers identified
> that the current approach

When you say "current approach" we first think we are talking about
the behavior you described above when you said "The git replay command
currently ..."

> forces users to opt-in to atomic behavior rather
> than defaulting to the safer, more reliable option.

But here you are actually talking about what the previous version of
this patch did.

> Elijah Newren noted
> that the experimental status explicitly allows such behavior changes, while
> Patrick Steinhardt highlighted performance concerns with individual ref
> updates in the reftable backend.

Also the commit message is not the right place to describe what
happened during discussions of the previous version(s) of a patch.
It's not the right place to talk about previous version(s) of a patch
in general. Those things should go into the cover letter.

If you want to talk about an option that was considered but rejected,
you can say something like the following instead of the whole
paragraph:

"To address this limitation, adding an option named for example
`--atomic-update` was considered. With such an option `git replay
--atomic-update --onto main topic1..topic2` would atomically update
all the refs without having to use a separate `git update-ref --stdin`
command. The issue is that this would force users to opt-in to the
atomic behavior rather than have it as the default safer, faster and
more reliable option.

Fortunately the experimental status of the `git replay` command
explicitly allows behavior changes, so we are allowed to make the
command atomically update all the refs by default.
"

> The core issue is that git replay was designed around command output rather
> than direct action. This made sense for a plumbing tool, but creates barriers
> for the primary use case: server-side operations that need reliable, atomic
> ref updates without pipeline complexity.

I think this paragraph should go just before the "Fortunately the
experimental status of the `git replay` command explicitly ..." that I
suggest above.

> This patch changes the default behavior to update refs directly using Git's

s/This patch changes/Let's change/

(See our SubmittingPatches documentation where it suggests using
imperative mood to describe the changes we make.)

> ref transaction API:
>
>     git replay --onto main topic1..topic2
>     # No output; all refs updated atomically or none
>
> The implementation uses ref_store_transaction_begin() with atomic mode by
> default, ensuring all ref updates succeed or all fail as a single operation.
> This leverages git replay's existing server-side strengths (in-memory operation,
> no work tree requirement) while adding the atomic guarantees that server
> operations require.
>
> For users needing the traditional pipeline workflow, --output-commands
> preserves the original behavior:

I think something like:

"For users needing the traditional pipeline workflow, let's add a new
`--output-commands`option that preserves the original behavior:"

is more explicit and makes it clear that it's a new option added by
this patch and not an existing option.

>     git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin
>
> The --allow-partial option enables partial failure tolerance.

In the same way, something like:

"Let's also add a new `--allow-partial` option that enables partial
failure tolerance."

> However, following
> maintainer feedback, it implements a "strict success" model: the command exits

I think you can remove "following maintainer feedback" here. The cover
letter or a trailer like "Helped-by: ..." at the end of the commit
message (but Junio will add his "Signed-off-by: ..." anyway so adding
an Helped-by: ... about him is redundant) are the right place to
mention people who helped or suggested changes.

> with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY
> updates fail. This ensures that --allow-partial changes error reporting style
> (warnings vs hard errors) but not success criteria, handling edge cases like
> "no updates needed" cleanly.
>
> Implementation details:
> - Empty commit ranges now return success (exit code 0) rather than failure,
>   as no commits to replay is a valid successful operation

Nit: as all the sentences in this "Implementation details" list start
with an uppercase, I think they should end with a full stop.

> - Added comprehensive test coverage with 12 new tests covering atomic behavior,
>   option validation, bare repository support, and edge cases
> - Fixed test isolation issues to prevent branch state contamination between tests
> - Maintains C89 compliance and follows Git's established coding conventions

I am not sure this one is worth mentioning here, at least not like
this. You may want to say in the cover letter that compared to the
previous version this patch doesn't use 'bool' anymore and explain
why. Or maybe you want to explain here that using the 'bool' type was
considered but rejected for some reason. But in both cases, you should
be explicit about the reason.

> - Refactored option validation to use die_for_incompatible_opt2() for both
>   --advance/--contained and --allow-partial/--output-commands conflicts,
>   providing consistent error reporting
> - Fixed --allow-partial exit code behavior to implement "strict success" model
>   where any ref update failures result in exit code 1, even with partial tolerance

This should probably go to the cover letter, as we should not talk in
the commit message about changes since a previous version of the
commit.

> - Updated documentation with proper line wrapping, consistent terminology using
>   "old default behavior", performance context, and reorganized examples for clarity

This also sounds like a change compared to the previous version of the patch.

> - Eliminates individual ref updates (refs_update_ref calls) that perform
>   poorly with reftable backend

This also sounds like a change compared to the previous version of the patch.

> - Uses only batched ref transactions for optimal performance across all
>   ref backends

I think you can remove "only" in the sentence as in the
--output-commands case no transaction is used.

> - Avoids naming collision with git rebase --update-refs by using distinct
>   option names

This also sounds like a change compared to the previous version of the patch.

> - Defaults to atomic behavior while preserving pipeline compatibility

This has been discussed above. It doesn't look like an implementation
detail to me.

> The result is a command that works better for its primary use case (server-side
> operations) while maintaining full backward compatibility for existing workflows.
>
> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>

Adding "Helped-by: ..." trailers for at least Elijah and Patrick would be nice.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-09-26 23:08   ` [PATCH v2 1/1] " Siddharth Asthana
  2025-09-30  8:23     ` Christian Couder
@ 2025-09-30 10:05     ` Phillip Wood
  2025-10-02 10:00       ` Karthik Nayak
  2025-10-02 22:20       ` Siddharth Asthana
  2025-10-02 16:32     ` Elijah Newren
  2 siblings, 2 replies; 129+ messages in thread
From: Phillip Wood @ 2025-09-30 10:05 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: gitster, christian.couder, ps, newren, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin

Hi Siddharth

On 27/09/2025 00:08, Siddharth Asthana wrote:
> The git replay command currently outputs update commands that must be
> piped to git update-ref --stdin to actually update references:
> 
>      git replay --onto main topic1..topic2 | git update-ref --stdin
> 
> This design has significant limitations for server-side operations. The
> two-command pipeline creates coordination complexity, provides no atomic
> transaction guarantees by default

Are you sure that's true? Maybe I'm missing something but my reading of 
builtin/update-ref.c is that it when "--stdin" is given it starts a ref 
transaction, reads the commands from stdin and applies them to that 
transaction and then commits the transaction which will make the updates 
atomic.

> , and complicates automation in bare
> repository environments where git replay is primarily used.

How does it complicate automation in bare repositories?

Christian has given detailed feedback on the rest of the commit message 
so I'll not comment on it further.

> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
> index 0b12bf8aa4..e104e0bc03 100644
> --- a/Documentation/git-replay.adoc
> +++ b/Documentation/git-replay.adoc
> @@ -9,16 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
>   SYNOPSIS
>   --------
>   [verse]
> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--output-commands | --allow-partial] <revision-range>...

Please wrap this very long line

> @@ -42,6 +42,20 @@ When `--advance` is specified, the update-ref command(s) in the output
>   will update the branch passed as an argument to `--advance` to point at
>   the new commits (in other words, this mimics a cherry-pick operation).
>   
> +--output-commands::
> +	Output update-ref commands instead of updating refs directly.
> +	When this option is used, the output can be piped to `git update-ref --stdin`
> +	for successive, relatively slow, ref updates. This is equivalent to the
> +	old default behavior.
> +
> +--allow-partial::
> +	Allow some ref updates to succeed even if others fail. By default,
> +	ref updates are atomic (all succeed or all fail). With this option,
> +	failed updates are reported as warnings rather than causing the entire
> +	command to fail. The command exits with code 0 only if all updates
> +	succeed; any failures result in exit code 1. Cannot be used with
> +	`--output-commands`.

Rather than having two incompatible options perhaps we could have a 
single "--update-refs=(yes|print|allow-partial-updates)" argument. I 
think the name "--allow-partial" is rather ambiguous as it does not say 
what it is allowing to be partial.

> +static int add_ref_to_transaction(struct ref_transaction *transaction,
> +				  const char *refname,
> +				  const struct object_id *new_oid,
> +				  const struct object_id *old_oid,
> +				  struct strbuf *err)
> +{
> +	return ref_transaction_update(transaction, refname, new_oid, old_oid,
> +				      NULL, NULL, 0, "git replay", err);
> +}

I'm not sure this function adds much value. I think it would be better 
to instead have a helper function that updates refs or prints the ref 
updates so that we do not duplicate that code in the two places below.

> @@ -434,10 +481,18 @@ int cmd_replay(int argc,
>   			if (decoration->type == DECORATION_REF_LOCAL &&
>   			    (contained || strset_contains(update_refs,
>   							  decoration->name))) {
> -				printf("update %s %s %s\n",
> -				       decoration->name,
> -				       oid_to_hex(&last_commit->object.oid),
> -				       oid_to_hex(&commit->object.oid));
> +				if (output_commands) {
> +					printf("update %s %s %s\n",
> +					       decoration->name,
> +					       oid_to_hex(&last_commit->object.oid),
> +					       oid_to_hex(&commit->object.oid));
> +				} else if (add_ref_to_transaction(transaction, decoration->name,
> +								  &last_commit->object.oid,
> +								  &commit->object.oid,
> +								  &transaction_err) < 0) {
> +					ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
> +					goto cleanup;
> +				}
>   			}

The lines here are very long due to the indentation, having a separate 
function to update the refs or print the ref updates would be much more 
readable.

>   			decoration = decoration->next;
>   		}
> @@ -445,10 +500,33 @@ int cmd_replay(int argc,
>   
>   	/* In --advance mode, advance the target ref */
>   	if (result.clean == 1 && advance_name) {
> -		printf("update %s %s %s\n",
> -		       advance_name,
> -		       oid_to_hex(&last_commit->object.oid),
> -		       oid_to_hex(&onto->object.oid));
> +		if (output_commands) {
> +			printf("update %s %s %s\n",
> +			       advance_name,
> +			       oid_to_hex(&last_commit->object.oid),
> +			       oid_to_hex(&onto->object.oid));
> +		} else if (add_ref_to_transaction(transaction, advance_name,
> +						  &last_commit->object.oid,
> +						  &onto->object.oid,
> +						  &transaction_err) < 0) {
> +			ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
> +			goto cleanup;
> +		}
> +	}

Putting the code to update the refs or print the ref updates into a 
single function would avoid this duplication and over-long lines.

Thanks

Phillip

> +	/* Commit the ref transaction if we have one */
> +	if (transaction && result.clean == 1) {
> +		if (ref_transaction_commit(transaction, &transaction_err)) {
> +			if (allow_partial) {
> +				warning(_("some ref updates failed: %s"), transaction_err.buf);
> +				ref_transaction_for_each_rejected_update(transaction,
> +									 print_rejected_update, NULL);
> +				ret = 0; /* Set failure even with allow_partial */
> +			} else {
> +				ret = error(_("failed to update refs: %s"), transaction_err.buf);
> +				goto cleanup;
> +			}
> +		}
>   	}
>   
>   	merge_finalize(&merge_opt, &result);
> @@ -457,9 +535,17 @@ int cmd_replay(int argc,
>   		strset_clear(update_refs);
>   		free(update_refs);
>   	}
> -	ret = result.clean;
> +
> +	/* Handle empty ranges: if no commits were processed, treat as success */
> +	if (!commits_processed)
> +		ret = 1; /* Success - no commits to replay is not an error */
> +	else
> +		ret = result.clean;
>   
>   cleanup:
> +	if (transaction)
> +		ref_transaction_free(transaction);
> +	strbuf_release(&transaction_err);
>   	release_revisions(&revs);
>   	free(advance_name);
>   
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index 58b3759935..8b4301e227 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
>   '
>   
>   test_expect_success 'using replay to rebase two branches, one on top of other' '
> -	git replay --onto main topic1..topic2 >result &&
> +	git replay --output-commands --onto main topic1..topic2 >result &&
>   
>   	test_line_count = 1 result &&
>   
> @@ -67,9 +67,30 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
>   	test_cmp expect result
>   '
>   
> +test_expect_success 'using replay with default atomic behavior (no output)' '
> +	# Create a test branch that wont interfere with others
> +	git branch atomic-test topic2 &&
> +	git rev-parse atomic-test >atomic-test-old &&
> +
> +	# Default behavior: atomic ref updates (no output)
> +	git replay --onto main topic1..atomic-test >output &&
> +	test_must_be_empty output &&
> +
> +	# Verify the branch was updated
> +	git rev-parse atomic-test >atomic-test-new &&
> +	! test_cmp atomic-test-old atomic-test-new &&
> +
> +	# Verify the history is correct
> +	git log --format=%s atomic-test >actual &&
> +	test_write_lines E D M L B A >expect &&
> +	test_cmp expect actual
> +'
> +
>   test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
> -	git -C bare replay --onto main topic1..topic2 >result-bare &&
> -	test_cmp expect result-bare
> +	git -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&
> +
> +	# The result should match what we got from the regular repo
> +	test_cmp result result-bare
>   '
>   
>   test_expect_success 'using replay to rebase with a conflict' '
> @@ -86,7 +107,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
>   	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
>   	# 4th field of result is hash for main instead of hash for topic2
>   
> -	git replay --advance main topic1..topic2 >result &&
> +	git replay --output-commands --advance main topic1..topic2 >result &&
>   
>   	test_line_count = 1 result &&
>   
> @@ -102,7 +123,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
>   '
>   
>   test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
> -	git -C bare replay --advance main topic1..topic2 >result-bare &&
> +	git -C bare replay --output-commands --advance main topic1..topic2 >result-bare &&
>   	test_cmp expect result-bare
>   '
>   
> @@ -115,7 +136,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
>   '
>   
>   test_expect_success 'using replay to also rebase a contained branch' '
> -	git replay --contained --onto main main..topic3 >result &&
> +	git replay --output-commands --contained --onto main main..topic3 >result &&
>   
>   	test_line_count = 2 result &&
>   	cut -f 3 -d " " result >new-branch-tips &&
> @@ -139,12 +160,12 @@ test_expect_success 'using replay to also rebase a contained branch' '
>   '
>   
>   test_expect_success 'using replay on bare repo to also rebase a contained branch' '
> -	git -C bare replay --contained --onto main main..topic3 >result-bare &&
> +	git -C bare replay --output-commands --contained --onto main main..topic3 >result-bare &&
>   	test_cmp expect result-bare
>   '
>   
>   test_expect_success 'using replay to rebase multiple divergent branches' '
> -	git replay --onto main ^topic1 topic2 topic4 >result &&
> +	git replay --output-commands --onto main ^topic1 topic2 topic4 >result &&
>   
>   	test_line_count = 2 result &&
>   	cut -f 3 -d " " result >new-branch-tips &&
> @@ -168,7 +189,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
>   '
>   
>   test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
> -	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
> +	git -C bare replay --output-commands --contained --onto main ^main topic2 topic3 topic4 >result &&
>   
>   	test_line_count = 4 result &&
>   	cut -f 3 -d " " result >new-branch-tips &&
> @@ -217,4 +238,131 @@ test_expect_success 'merge.directoryRenames=false' '
>   		--onto rename-onto rename-onto..rename-from
>   '
>   
> +# Tests for new default atomic behavior and options> > +test_expect_success 'replay default behavior should not produce 
output when successful' '
> +	git replay --onto main topic1..topic3 >output &&
> +	test_must_be_empty output
> +'
> +
> +test_expect_success 'replay with --output-commands produces traditional output' '
> +	git replay --output-commands --onto main topic1..topic3 >output &&
> +	test_line_count = 1 output &&
> +	grep "^update refs/heads/topic3 " output
> +'
> +
> +test_expect_success 'replay with --allow-partial should not produce output when successful' '
> +	git replay --allow-partial --onto main topic1..topic3 >output &&
> +	test_must_be_empty output
> +'
> +
> +test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '
> +	test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
> +	grep "cannot be used together" error
> +'
> +
> +test_expect_success 'replay with --contained updates multiple branches atomically' '
> +	# Create fresh test branches based on the original structure
> +	# contained-topic1 should be contained within the range to contained-topic3
> +	git branch contained-base main &&
> +	git checkout -b contained-topic1 contained-base &&
> +	test_commit ContainedC &&
> +	git checkout -b contained-topic3 contained-topic1 &&
> +	test_commit ContainedG &&
> +	test_commit ContainedH &&
> +	git checkout main &&
> +
> +	# Store original states
> +	git rev-parse contained-topic1 >contained-topic1-old &&
> +	git rev-parse contained-topic3 >contained-topic3-old &&
> +
> +	# Use --contained to update multiple branches - this should update both
> +	git replay --contained --onto main contained-base..contained-topic3 &&
> +
> +	# Verify both branches were updated
> +	git rev-parse contained-topic1 >contained-topic1-new &&
> +	git rev-parse contained-topic3 >contained-topic3-new &&
> +	! test_cmp contained-topic1-old contained-topic1-new &&
> +	! test_cmp contained-topic3-old contained-topic3-new
> +'
> +
> +test_expect_success 'replay atomic behavior: all refs updated or none' '
> +	# Store original state
> +	git rev-parse topic4 >topic4-old &&
> +
> +	# Default atomic behavior
> +	git replay --onto main main..topic4 &&
> +
> +	# Verify ref was updated
> +	git rev-parse topic4 >topic4-new &&
> +	! test_cmp topic4-old topic4-new &&
> +
> +	# Verify no partial state
> +	git log --format=%s topic4 >actual &&
> +	test_write_lines J I M L B A >expect &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'replay works correctly with bare repositories' '
> +	# Test atomic behavior in bare repo (important for Gitaly)
> +	git checkout -b bare-test topic1 &&
> +	test_commit BareTest &&
> +
> +	# Test with bare repo - replay the commits from main..bare-test to get the full history
> +	git -C bare fetch .. bare-test:bare-test &&
> +	git -C bare replay --onto main main..bare-test &&
> +
> +	# Verify the bare repo was updated correctly (no output)
> +	git -C bare log --format=%s bare-test >actual &&
> +	test_write_lines BareTest F C M L B A >expect &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'replay --allow-partial with no failures produces no output' '
> +	git checkout -b partial-test topic1 &&
> +	test_commit PartialTest &&
> +
> +	# Should succeed silently even with partial mode
> +	git replay --allow-partial --onto main topic1..partial-test >output &&
> +	test_must_be_empty output
> +'
> +
> +test_expect_success 'replay maintains ref update consistency' '
> +	# Test that traditional vs atomic produce equivalent results
> +	git checkout -b method1-test topic2 &&
> +	git checkout -b method2-test topic2 &&
> +
> +	# Both methods should update refs to point to the same replayed commits
> +	git replay --output-commands --onto main topic1..method1-test >update-commands &&
> +	git update-ref --stdin <update-commands &&
> +	git log --format=%s method1-test >traditional-result &&
> +
> +	# Direct atomic method should produce same commit history
> +	git replay --onto main topic1..method2-test &&
> +	git log --format=%s method2-test >atomic-result &&
> +
> +	# Both methods should produce identical commit histories
> +	test_cmp traditional-result atomic-result
> +'
> +
> +test_expect_success 'replay error messages are helpful and clear' '
> +	# Test that error messages are clear
> +	test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
> +	grep "cannot be used together" error
> +'
> +
> +test_expect_success 'replay with empty range produces no output and no changes' '
> +	# Create a test branch for empty range testing
> +	git checkout -b empty-test topic1 &&
> +	git rev-parse empty-test >empty-test-before &&
> +
> +	# Empty range should succeed but do nothing
> +	git replay --onto main empty-test..empty-test >output &&
> +	test_must_be_empty output &&
> +
> +	# Branch should be unchanged
> +	git rev-parse empty-test >empty-test-after &&
> +	test_cmp empty-test-before empty-test-after
> +'
> +
>   test_done


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-09-30 10:05     ` Phillip Wood
@ 2025-10-02 10:00       ` Karthik Nayak
  2025-10-02 22:20         ` Siddharth Asthana
  2025-10-02 22:20       ` Siddharth Asthana
  1 sibling, 1 reply; 129+ messages in thread
From: Karthik Nayak @ 2025-10-02 10:00 UTC (permalink / raw)
  To: Phillip Wood, Siddharth Asthana, git
  Cc: gitster, christian.couder, ps, newren, code, rybak.a.v, jltobler,
	toon, johncai86, johannes.schindelin

[-- Attachment #1: Type: text/plain, Size: 1021 bytes --]

Phillip Wood <phillip.wood123@gmail.com> writes:

> Hi Siddharth
>
> On 27/09/2025 00:08, Siddharth Asthana wrote:
>> The git replay command currently outputs update commands that must be
>> piped to git update-ref --stdin to actually update references:
>>
>>      git replay --onto main topic1..topic2 | git update-ref --stdin
>>
>> This design has significant limitations for server-side operations. The
>> two-command pipeline creates coordination complexity, provides no atomic
>> transaction guarantees by default
>
> Are you sure that's true? Maybe I'm missing something but my reading of
> builtin/update-ref.c is that it when "--stdin" is given it starts a ref
> transaction, reads the commands from stdin and applies them to that
> transaction and then commits the transaction which will make the updates
> atomic.
>

You're right. Using '--stdin' is atomic by default. You can manually
handle the transaction's by passing in the 'start', 'prepare', 'commit',
'abort' sub-commands in the '--stdin' mode.

[snip]

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-09-26 23:08   ` [PATCH v2 1/1] " Siddharth Asthana
  2025-09-30  8:23     ` Christian Couder
  2025-09-30 10:05     ` Phillip Wood
@ 2025-10-02 16:32     ` Elijah Newren
  2025-10-02 18:27       ` Junio C Hamano
  2025-10-02 23:27       ` Siddharth Asthana
  2 siblings, 2 replies; 129+ messages in thread
From: Elijah Newren @ 2025-10-02 16:32 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, gitster, christian.couder, ps, code, rybak.a.v, karthik.188,
	jltobler, toon, johncai86, johannes.schindelin

Hi,

First of all, thanks for continuing to work on this.

On Fri, Sep 26, 2025 at 4:09 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> The git replay command currently outputs update commands that must be

can be, not must be.

This separation had three advantages:
  * it made testing easy (when state isn't modified from one step to
the next, you don't need to make temporary branches or have undo
commands, or try to track the changes)
  * it provided a natural can-it-rebase-cleanly (and what would it
rebase to) capability without automatically updating refs, I guess
kind of like a --dry-run
  * it provided a natural low-level tool for the suite of hash-object,
mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users
to have another building block for experimentation and making new
tools.

I was particularly focused on the last of those items for the intial
version at the time, but it should be noted that all three of these
are somewhat special cases, and the most common user desire is going
to be replaying commits and updating the references at the end.

> piped to git update-ref --stdin to actually update references:
>
>     git replay --onto main topic1..topic2 | git update-ref --stdin

or, alternatively, for those that want a single, atomic (assuming the
backend supports it) transaction:

   (echo start; git replay --onto main topic1..topic2; echo prepare;
echo commit) | git update-ref --stdin

> This design has significant limitations for server-side operations. The
> two-command pipeline creates coordination complexity,

agreed

> provides no atomic
> transaction guarantees by default,

Sure, but it's quite trivial to add, right -- as shown above with the
extra "start", "prepare", "commit" directives?

> and complicates automation in bare
> repository environments where git replay is primarily used.

Isn't this repeating the first statement from this paragraph?

The paragraph also leaves off that I think it makes it more
useful/usable to end-users.

> The core issue is that git replay was designed around command output rather
> than direct action. This made sense for a plumbing tool, but creates barriers
> for the primary use case: server-side operations that need reliable, atomic
> ref updates without pipeline complexity.

git replay was originally written with the goal of eventually
providing a better interactive rebase.  I thought it important to have
the earlier versions provide new useful low-level building blocks, but
didn't intend for it to just be a low-level building block
indefinitely.  The primary use case originally envisioned was
client-side user interactive operations without much thought for the
server, but it turned out to be easiest to implement what server-side
operations needed first.  I made others aware of what I was working on
so they could see, and Christian latched on and to my surprise told me
it had enough for what he needed...and Johannes said about the same.
Unfortunately, I got reassigned away from Git at my former dayjob,
_and_ had multiple big long-term family issues strike about the same
time, _and_ all Git-related employers did mass layoffs and locked down
hiring about that time as well...so the end result is only that
initial server-side stuff was done and it looks to outside observers
like git replay's design and usecase was around the server.  While
that's an understandable guess from the outside, this paragraph
appears to have taken those guesses and rewrites them as fact.  I
think it could be reworded with small tweaks to alter it and make it
factual (s/designed/focused initially/, s/the primary use case/its
current primary use case/), but even then, the paragraph that remains
feels like it's just repeating what you said above.  It doesn't feel
like it adds any positive value.

Might I suggest a rewrite of the text of the commit message to this point?

=====
The git replay command currently outputs update commands that can be
piped to update-ref to achieve a rebase, e.g.

  git replay --onto main topic1..topic2 | git update-ref --stdin

This separation had advantages for three special cases:
  * it made testing easy (when state isn't modified from one step to
the next, you don't need to make temporary branches or have undo
commands, or try to track the changes)
  * it provided a natural can-it-rebase-cleanly (and what would it
rebase to) capability without automatically updating refs, I guess
kind of like a --dry-run
  * it provided a natural low-level tool for the suite of hash-object,
mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users
to have another building block for experimentation and making new
tools.

However, it should be noted that all three of these are somewhat
special cases; users, whether on the client or server side, would
almost certainly find it more ergonomical to simply have the updating
of refs be the default.  Change the default behavior to update refs
directly, and atomically (at least to the extent supported by the refs
backend in use).
====

after this, I'd suggest also nuking your next few paragraphs and then
continuing with:

> For users needing the traditional pipeline workflow, --output-commands
> preserves the original behavior:
>
>     git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin

This is good.  Did you also add a config option so that someone can
just set that option once and use the old behavior?  (as per the
suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/
?)

> The --allow-partial option enables partial failure tolerance. However, following
> maintainer feedback, it implements a "strict success" model: the command exits
> with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY
> updates fail. This ensures that --allow-partial changes error reporting style
> (warnings vs hard errors) but not success criteria, handling edge cases like
> "no updates needed" cleanly.

Why is --allow-partial helpful?  You discussed at length why you
wanted atomic transactions, but you introduce this option with no
rationale and instead just discuss that you implemented it and some
design choices once you presuppose that someone wants to use it.

Is there a usecase?  I asked for it last time, and suggested
discarding the modes without one, but you only discarded one of the
extras while leaving this one in.  I'd recommend discarding this one
too and just having the two modes -- the output commands that get fed
to update-ref, or the automatic transactional update of all or no
refs.

> Implementation details:

Christian's comments on this list are good; it seems to mostly be
stuff that belongs in the cover letter.  But I'll comment/query about
two things...

> - Empty commit ranges now return success (exit code 0) rather than failure,
>   as no commits to replay is a valid successful operation

That is a useful thing to call out.  Hmm....

> - Defaults to atomic behavior while preserving pipeline compatibility

The first part has already been covered at length; it doesn't seem
like it needs to be repeated.  What does "while preserving pipeline
compatibility" mean, though?


> The result is a command that works better for its primary use case (server-side
> operations)

This sentence feels like it's rewriting the history behind the
command, and undersells the change by not noting that it *also* helps
with client-side usage.  Also, since I think it's about the third time
you've said this, it adds nothing to the discussion either; let's just
strike it.

> while maintaining full backward compatibility for existing workflows.

and this is clearly false; backwards compatibility was intentionally
broken by making update-refs the default.  Junio suggested making it
easy to get the old behavior with a config setting, but looking ahead
it appears you didn't even make it easy for users to recover the old
behavior; they have to specify an additional flag with every command
they run.

Also, you further broke "full" backward compatibility by changing the
exit status for empty ranges.  I think that's a rather minor change
and maybe bugfix, but "full" invites comparisons and scrutiny of that
sort.

We could say something like "while making it easy to recover the
traditional behavior with a simple command line flag", but this again
feels like you're just repeating what was called out above.  Maybe
just strike it as well?


And on a separate note, several of your lines in your commit message
are too long; in an 80-column terminal, a `git log` on your commit
message has several lines wrapping around.  (Command lines, and for
future reference output from commands, can run longer, but regular
paragraphs should fit.)

> +...Use `--output-commands`
> +to get the old default behavior where update commands that can be piped
> +to `git update-ref --stdin` are emitted (see the OUTPUT section below).

Perhaps instead:

Use `--output-commands` to avoid the automatic ref updates and instead
get update commands that can be piped
to `git update-ref --stdin` (see the OUTPUT section below).

> +--output-commands::
> +       Output update-ref commands instead of updating refs directly.
> +       When this option is used, the output can be piped to `git update-ref --stdin`
> +       for successive, relatively slow, ref updates. This is equivalent to the
> +       old default behavior.

"piped" => "piped as-is" + "successive, relatively slow," => "a
non-transactional set of" ?

> +--allow-partial::
> +       Allow some ref updates to succeed even if others fail. By default,
> +       ref updates are atomic (all succeed or all fail). With this option,
> +       failed updates are reported as warnings rather than causing the entire
> +       command to fail. The command exits with code 0 only if all updates
> +       succeed; any failures result in exit code 1. Cannot be used with
> +       `--output-commands`.

If we keep this, I like Phillip's suggestion to combine to a single
flag with a value.  But I still want to hear the use case behind it;
it goes against what you repeatedly claimed was wanted on the server,
and you didn't discuss any other usecase.  It feels like you were
trying to proactively support every possible way people might want to
update without considering the usecases behind it, and I'd rather just
leave it unimplemented unless or until there's demand for it.

> +
>  <revision-range>::
>         Range of commits to replay. More than one <revision-range> can
>         be passed, but in `--advance <branch>` mode, they should have
> @@ -54,15 +68,20 @@ include::rev-list-options.adoc[]
>  OUTPUT
>  ------
>
> -When there are no conflicts, the output of this command is usable as
> -input to `git update-ref --stdin`.  It is of the form:
> +By default, when there are no conflicts, this command updates the relevant
> +references using atomic transactions

atomic transaction*s*?  Shouldn't that be *an* atomic transaction instead?

> >and produces no output. All ref updates
> +succeed or all fail (atomic behavior).

Why the need to repeat?

> +To rebase multiple branches with partial failure tolerance:
> +
> +------------
> +$ git replay --allow-partial --contained --onto origin/main origin/main..tipbranch
> +------------

I don't understand why this one deserves an example; the command line
flag seems to be self-explanatory.  The onto and advanced are
interesting in that the ranges specified with them might not be
obvious from their description and so examples help.  One or maybe two
--contained examples can go with those, to demonstrate how it involves
additional branches, though those might be better coupled with the
--output-commands because the output more clearly demonstrates what is
happening (i.e. what is being updated).  But I don't see what this
example might elucidate.

Then again, while I perfectly understand what the flag does, I have no
idea why you thought it was useful to add, so maybe I'm just missing
something.

>  When calling `git replay`, one does not need to specify a range of
>  commits to replay using the syntax `A..B`; any range expression will
> -do:
> +do. Here's an example where you explicitly specify which branches to rebase:
>
>  ------------
>  $ git replay --onto origin/main ^base branch1 branch2 branch3
> +------------
> +
> +This gives you explicit control over exactly which branches are rebased,
> +unlike the previous `--contained` example which automatically discovers them.
> +
> +To see the update commands that would be executed:
> +
> +------------
> +$ git replay --output-commands --onto origin/main ^base branch1 branch2 branch3
>  update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>  update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>  update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}

As noted above, I don't think we need to repeat an --output-commands
example for every example that exists; it feels like overkill.  One
(or _maybe_ two) should be sufficient.

> @@ -330,9 +361,12 @@ int cmd_replay(int argc,
>                 usage_with_options(replay_usage, replay_options);
>         }
>
> -       if (advance_name_opt && contained)
> -               die(_("options '%s' and '%s' cannot be used together"),
> -                   "--advance", "--contained");
> +       die_for_incompatible_opt2(!!advance_name_opt, "--advance",
> +                                 contained, "--contained");

Broken indentation.  Also, should this have been done as a preparatory
cleanup patch?

> @@ -407,6 +452,8 @@ int cmd_replay(int argc,
>                 khint_t pos;
>                 int hr;
>
> +               commits_processed = 1;
> +
>                 if (!commit->parents)
>                         die(_("replaying down to root commit is not supported yet!"));
>                 if (commit->parents->next)
> @@ -457,9 +535,17 @@ int cmd_replay(int argc,
>                 strset_clear(update_refs);
>                 free(update_refs);
>         }
> -       ret = result.clean;
> +
> +       /* Handle empty ranges: if no commits were processed, treat as success */
> +       if (!commits_processed)
> +               ret = 1; /* Success - no commits to replay is not an error */
> +       else
> +               ret = result.clean;

The change to treat empty ranges as success is an orthogonal change
that I think at a minimum belongs in a separate patch.  Out of
curiosity, how did you discover the exit status with an empty commit
range?  Why does someone specify such a range, and what form or forms
might it come in?  And is merely returning a successful result enough,
or is there more that needs to be done for correctness?

> +test_expect_success 'using replay with default atomic behavior (no output)' '
> +       # Create a test branch that wont interfere with others

This works, but I feel doing this clutters the repo for someone
inspecting later when some test in the testsuite fails, and makes
readers track more branches to find out what the tests are all doing.
Would it be easier to prefix your test with something like

        START=$(git rev-parse topic2) &&
        test_when_finished "git branch -f topic2 $START" &&

and then...

> +       git branch atomic-test topic2 &&
> +       git rev-parse atomic-test >atomic-test-old &&

drop these lines...

> +
> +       # Default behavior: atomic ref updates (no output)
> +       git replay --onto main topic1..atomic-test >output &&

use topic2 instead of atomic-test...

> +       test_must_be_empty output &&
> +
> +       # Verify the branch was updated
> +       git rev-parse atomic-test >atomic-test-new &&
> +       ! test_cmp atomic-test-old atomic-test-new &&

Not sure this comparison to verify the branch was updated makes much
sense given that the few lines below both test that it was updated and
that it was updated to the right thing.

> +
> +       # Verify the history is correct
> +       git log --format=%s atomic-test >actual &&
> +       test_write_lines E D M L B A >expect &&
> +       test_cmp expect actual

...and finally use topic2 instead of atomic-test here.


Similarly, using test_when_finished throughout the rest of the
testsuite similarly I think would make it a bit easier to follow and
later debug.


>  test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
> -       git -C bare replay --onto main topic1..topic2 >result-bare &&
> -       test_cmp expect result-bare
> +       git -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&
> +
> +       # The result should match what we got from the regular repo
> +       test_cmp result result-bare
>  '

What do you mean by "regular repo"?  There's only one repo at play.

Also, why change "expect" to "result" here?  You don't care if you get
the expected result, merely that you get the same result as a previous
test even if it was also buggy?  I think the reason was that you
inserted a test since writing the expectation out, but I think it'd be
better to either re-write the expectation out and compare to it, or
reorder the tests so you can use the same expectation from before.


> +# Tests for new default atomic behavior and options

The word "new" here is going to become unhelpful in the future when
someone reads this a few years from now; you should strike it.  What
does "and options" mean here?

> +
> +test_expect_success 'replay default behavior should not produce output when successful' '
> +       git replay --onto main topic1..topic3 >output &&
> +       test_must_be_empty output
> +'

This changes where topic3 points; I think a test_when_finished to
reset it back at the end of the test would be nice.

> +test_expect_success 'replay with --output-commands produces traditional output' '
> +       git replay --output-commands --onto main topic1..topic3 >output &&
> +       test_line_count = 1 output &&
> +       grep "^update refs/heads/topic3 " output
> +'

The fact that topic3 was already replayed in the previous test makes
this test weaker.  And the fact that it does nothing but check that
there is in fact some output makes it weaker still.  But rather than
fix it, there were already lots of commands that tested
--output-commands prior to this, and you added a comment above that
you were adding tests of the atomic behavior, I don't see why we need
any additional tests of --output-commands.

> +test_expect_success 'replay with --allow-partial should not produce output when successful' '
> +       git replay --allow-partial --onto main topic1..topic3 >output &&
> +       test_must_be_empty output
> +
> +test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '
> +       test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
> +       grep "cannot be used together" error
> +'

These are fine if --allow-partial can be motivated, but otherwise
should be removed along with that flag.

> +
> +test_expect_success 'replay with --contained updates multiple branches atomically' '
> +       # Create fresh test branches based on the original structure

Unnecessary if you use the test_when_finished stuff I showed you to
ensure the original structure remains intact

> +       # contained-topic1 should be contained within the range to contained-topic3
> +       git branch contained-base main &&
> +       git checkout -b contained-topic1 contained-base &&
> +       test_commit ContainedC &&
> +       git checkout -b contained-topic3 contained-topic1 &&
> +       test_commit ContainedG &&
> +       test_commit ContainedH &&
> +       git checkout main &&
> +
> +       # Store original states
> +       git rev-parse contained-topic1 >contained-topic1-old &&
> +       git rev-parse contained-topic3 >contained-topic3-old &&
> +
> +       # Use --contained to update multiple branches - this should update both
> +       git replay --contained --onto main contained-base..contained-topic3 &&
> +
> +       # Verify both branches were updated
> +       git rev-parse contained-topic1 >contained-topic1-new &&
> +       git rev-parse contained-topic3 >contained-topic3-new &&
> +       ! test_cmp contained-topic1-old contained-topic1-new &&
> +       ! test_cmp contained-topic3-old contained-topic3-new
> +'

I think verifying both branches were modified without checking
anything about the modification is a pretty weak test.  We should
check that both branches have the appropriate commit sequence in them
via git log output, as previous tests do.

> +test_expect_success 'replay atomic behavior: all refs updated or none' '
> +       # Store original state
> +       git rev-parse topic4 >topic4-old &&
> +
> +       # Default atomic behavior
> +       git replay --onto main main..topic4 &&
> +
> +       # Verify ref was updated
> +       git rev-parse topic4 >topic4-new &&
> +       ! test_cmp topic4-old topic4-new &&
> +
> +       # Verify no partial state
> +       git log --format=%s topic4 >actual &&
> +       test_write_lines J I M L B A >expect &&
> +       test_cmp expect actual
> +'

This test doesn't test what it says it does.  It merely tests that the
single topic was modified, and as such, isn't a very useful additional
test.  Using the contained flag where one of the branches had a lock
in the way or a simultaneous push and then testing that none of the
branches got updated would be needed if you want to test this.

> +test_expect_success 'replay works correctly with bare repositories' '
> +       # Test atomic behavior in bare repo (important for Gitaly)
> +       git checkout -b bare-test topic1 &&
> +       test_commit BareTest &&
> +
> +       # Test with bare repo - replay the commits from main..bare-test to get the full history
> +       git -C bare fetch .. bare-test:bare-test &&
> +       git -C bare replay --onto main main..bare-test &&
> +
> +       # Verify the bare repo was updated correctly (no output)
> +       git -C bare log --format=%s bare-test >actual &&
> +       test_write_lines BareTest F C M L B A >expect &&
> +       test_cmp expect actual
> +'

This doesn't test what the initial comment says is the important bit;
in fact, since only a single ref is being updated there's no chance to
test atomicity of updating multiple refs. Or is the parenthetical
ambiguous and I should have read that as bare repositories being
important?  If the latter, this test is mere redundancy; there were
multiple tests in a bare repository previously.

> +test_expect_success 'replay --allow-partial with no failures produces no output' '
> +       git checkout -b partial-test topic1 &&
> +       test_commit PartialTest &&
> +
> +       # Should succeed silently even with partial mode
> +       git replay --allow-partial --onto main topic1..partial-test >output &&
> +       test_must_be_empty output
> +'

Test is fine, _if_ --allow-partial is worthwhile to add.

> +test_expect_success 'replay maintains ref update consistency' '
> +       # Test that traditional vs atomic produce equivalent results

I think the comment is a better test name than the actual test name;
I'd remove the existing testname, and move the comment to the test
name, and then not have a comment here.

> +       git checkout -b method1-test topic2 &&
> +       git checkout -b method2-test topic2 &&
> +
> +       # Both methods should update refs to point to the same replayed commits
> +       git replay --output-commands --onto main topic1..method1-test >update-commands &&
> +       git update-ref --stdin <update-commands &&
> +       git log --format=%s method1-test >traditional-result &&
> +
> +       # Direct atomic method should produce same commit history
> +       git replay --onto main topic1..method2-test &&
> +       git log --format=%s method2-test >atomic-result &&
> +
> +       # Both methods should produce identical commit histories
> +       test_cmp traditional-result atomic-result
> +'
> +
> +test_expect_success 'replay error messages are helpful and clear' '
> +       # Test that error messages are clear
> +       test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
> +       grep "cannot be used together" error
> +'

Does this test that "error messages are helpful and clear" or just
that "--output-commands and --allow-partial" are incompatible?  The
former would be a test of _all_ error messages from git replay, and
would need to look at more than just a small substring of the error
message(s).

> +test_expect_success 'replay with empty range produces no output and no changes' '
> +       # Create a test branch for empty range testing

Why?  Since you use an A..A range below, you can just use any existing
branch in that place, right?

> +       git checkout -b empty-test topic1 &&
> +       git rev-parse empty-test >empty-test-before &&
> +
> +       # Empty range should succeed but do nothing
> +       git replay --onto main empty-test..empty-test >output &&
> +       test_must_be_empty output &&

Is A..A the only consideration?  Why would anyone ever pass that to
the tool?  I would have thought that if someone made this mistake it
was a B..A range where it wasn't obvious to the caller that B
contained A.  I don't see why anyone would do A..A and then get
surprised at the exit status; that feels like user error.  And in the
B..A case, it's not clear to me that returning success without
updating A is correct.  You might be giving the user false hope that
the command did what they wanted.

> +       # Branch should be unchanged
> +       git rev-parse empty-test >empty-test-after &&
> +       test_cmp empty-test-before empty-test-after
> +'
> +
>  test_done
> --
> 2.51.0

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior
  2025-09-26 23:08 ` [PATCH v2 0/1] replay: make atomic ref updates the default behavior Siddharth Asthana
  2025-09-26 23:08   ` [PATCH v2 1/1] " Siddharth Asthana
@ 2025-10-02 17:14   ` Kristoffer Haugsbakk
  2025-10-02 23:36     ` Siddharth Asthana
  2025-10-13 18:33   ` [PATCH v3 0/3] replay: make atomic ref updates the default Siddharth Asthana
  2 siblings, 1 reply; 129+ messages in thread
From: Kristoffer Haugsbakk @ 2025-10-02 17:14 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: Junio C Hamano, Christian Couder, Patrick Steinhardt,
	Elijah Newren, Andrei Rybak, Karthik Nayak, Justin Tobler,
	Toon Claes, John Cai, Johannes Schindelin

On Sat, Sep 27, 2025, at 01:08, Siddharth Asthana wrote:
> This is v2 of the git-replay atomic updates series.
>
> Based on the extensive community feedback from v1, I've completely redesigned
> the approach. Instead of adding new --update-refs options, this version makes
> atomic ref updates the default behavior of git replay.
>
> Why this change makes sense:
> - git replay is explicitly marked as EXPERIMENTAL with behavior changes
> expected
> - The command is primarily used server-side where atomic transactions
> are crucial
> - Current pipeline approach (git replay | git update-ref --stdin)
> creates
>   coordination complexity and lacks atomic guarantees by default
> - Patrick Steinhardt noted performance issues with individual ref
> updates
>   in reftable backend
> - Elijah Newren and Junio Hamano endorsed making the better behavior
> default
>
>[snip]

On the topic of changing experimental commands: I really like the
git-for-each-ref(1) (git-FER) output format design.  It just outputs refs and
related data.  It’s not a command for “bulk delete refs” or “check for
merge conflicts between these refs and upstream (git-merge-tree(1)”—it
just supports all of that through `--format` and its atoms.

And for this command it seems to, at the core, output a mapping from old
to new commits.

Now, I’ve thought that a “client-side”[1] in-memory rebase-like command
would need to support outputting data for the `post-rewrite` hook.  And
is that not straightforward if you can use `--format` with `from` and
`to` atoms?  (I ask because I have never called hooks with git-hook(1).)

I just think that (naively maybe) a `--format` command like git-FER with
all the quoting modes might be a good fit for this command.  Then you
can compose all the steps you need yourself:

1. Call the exact git-update-ref(1) `--batch`/`--stdin` or whatever mode
   you need
2. Write a message to each reflog if you want
3. Call the `post-rewrite` hook

† 1: c.f. server-side which I get the impression only wants to do cheap
     rebases

-- 
Kristoffer

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-02 16:32     ` Elijah Newren
@ 2025-10-02 18:27       ` Junio C Hamano
  2025-10-02 23:42         ` Siddharth Asthana
  2025-10-02 23:27       ` Siddharth Asthana
  1 sibling, 1 reply; 129+ messages in thread
From: Junio C Hamano @ 2025-10-02 18:27 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Siddharth Asthana, git, christian.couder, ps, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin

Elijah Newren <newren@gmail.com> writes:

>   * it provided a natural low-level tool for the suite of hash-object,
> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users
> to have another building block for experimentation and making new
> tools.
>
> I was particularly focused on the last of those items for the intial
> version at the time, but it should be noted that all three of these
> are somewhat special cases, and the most common user desire is going
> to be replaying commits and updating the references at the end.

Yes.  We could even tweak the stream in the middle with "sed" or
"grep", I presume? ;-)

> Sure, but it's quite trivial to add, right -- as shown above with the
> extra "start", "prepare", "commit" directives?

Very true.

Completely a tangent but, isn't requiring "prepare" at this layer,
and possibly in the form of ref_transaction_prepare() at the C
layer, not so ergonomic API design?  Once you "start" a transaction
and threw a bunch of instruction, "commit" can notice that you are
in a transaction and should do whatever necessary (including
whatever "prepare" does).  I am not advocating to simplify the API
by making end-user/program facing "prepare" a no-op, but just
wondering why we decided to have "prepare" a so prominent API
element.

> ...
> Might I suggest a rewrite of the text of the commit message to this point?

I do think it makes more sense to the reader to know the reasoning
behind the _current_ design, and what its strengths are.

> =====
> The git replay command currently outputs update commands that can be
> piped to update-ref to achieve a rebase, e.g.
>
>   git replay --onto main topic1..topic2 | git update-ref --stdin
>
> This separation had advantages for three special cases:
>   * it made testing easy (when state isn't modified from one step to
> the next, you don't need to make temporary branches or have undo
> commands, or try to track the changes)
>   * it provided a natural can-it-rebase-cleanly (and what would it
> rebase to) capability without automatically updating refs, I guess
> kind of like a --dry-run
>   * it provided a natural low-level tool for the suite of hash-object,
> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users
> to have another building block for experimentation and making new
> tools.
>
> However, it should be noted that all three of these are somewhat
> special cases; users, whether on the client or server side, would
> almost certainly find it more ergonomical to simply have the updating
> of refs be the default.  Change the default behavior to update refs
> directly, and atomically (at least to the extent supported by the refs
> backend in use).
> ====

This reads very well.

> Why is --allow-partial helpful?  You discussed at length why you
> wanted atomic transactions, but you introduce this option with no
> rationale and instead just discuss that you implemented it and some
> design choices once you presuppose that someone wants to use it.
>
> Is there a usecase?  I asked for it last time, and suggested
> discarding the modes without one, but you only discarded one of the
> extras while leaving this one in.  I'd recommend discarding this one
> too and just having the two modes -- the output commands that get fed
> to update-ref, or the automatic transactional update of all or no
> refs.

I know there are people who like "best effort", but I too want to
learn a concrete use case where the "best effort" mode, which
updates only 3 refs among 30 that were to be updated, would give us
a better result than "all or none" transaction that fails.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-09-30  8:23     ` Christian Couder
@ 2025-10-02 22:16       ` Siddharth Asthana
  2025-10-03  7:30         ` Christian Couder
  2025-10-02 22:55       ` Elijah Newren
  1 sibling, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-02 22:16 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, gitster, ps, newren, code, rybak.a.v, karthik.188, jltobler,
	toon, johncai86, johannes.schindelin


On 30/09/25 13:53, Christian Couder wrote:
> On Sat, Sep 27, 2025 at 1:09 AM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> The git replay command currently outputs update commands that must be
>> piped to git update-ref --stdin to actually update references:
>>
>>      git replay --onto main topic1..topic2 | git update-ref --stdin
>>
>> This design has significant limitations for server-side operations. The
>> two-command pipeline creates coordination complexity, provides no atomic
>> transaction guarantees by default, and complicates automation in bare
>> repository environments where git replay is primarily used.
> Yeah, right.
>
>> During extensive mailing list discussion, multiple maintainers identified
>> that the current approach
> When you say "current approach" we first think we are talking about
> the behavior you described above when you said "The git replay command
> currently ..."
>
>> forces users to opt-in to atomic behavior rather
>> than defaulting to the safer, more reliable option.
> But here you are actually talking about what the previous version of
> this patch did.
>
>> Elijah Newren noted
>> that the experimental status explicitly allows such behavior changes, while
>> Patrick Steinhardt highlighted performance concerns with individual ref
>> updates in the reftable backend.
> Also the commit message is not the right place to describe what
> happened during discussions of the previous version(s) of a patch.
> It's not the right place to talk about previous version(s) of a patch
> in general. Those things should go into the cover letter.
>
> If you want to talk about an option that was considered but rejected,
> you can say something like the following instead of the whole
> paragraph:
>
> "To address this limitation, adding an option named for example
> `--atomic-update` was considered. With such an option `git replay
> --atomic-update --onto main topic1..topic2` would atomically update
> all the refs without having to use a separate `git update-ref --stdin`
> command. The issue is that this would force users to opt-in to the
> atomic behavior rather than have it as the default safer, faster and
> more reliable option.
>
> Fortunately the experimental status of the `git replay` command
> explicitly allows behavior changes, so we are allowed to make the
> command atomically update all the refs by default.
> "


Hi Christian,

Thanks for the detailed commit message review. You are absolutely right - I
was mixing the patch rationale with v1→v2 changelog, which belongs in the
cover letter.

Your suggested framing about considering an --atomic-update option but
rejecting it in favor of making it default is much clearer than my
approach. I will use that structure.

For v3:
- Move all "since v1" discussion to cover letter
- Use imperative mood ("Let's change" not "This patch changes")
- Be explicit that --output-commands and --allow-partial are new options
- Add full stops to the implementation details list
- Will add Helped-by trailers for Elijah, Patrick and you ofcourse as 
suggested.

Quick question: for the C89 compliance mention, should I drop it entirely
or briefly note "uses 'int' instead of 'bool' for C89 compatibility"? I
want to acknowledge the bool→int change but not belabor it.

Thanks again!


>
>> The core issue is that git replay was designed around command output rather
>> than direct action. This made sense for a plumbing tool, but creates barriers
>> for the primary use case: server-side operations that need reliable, atomic
>> ref updates without pipeline complexity.
> I think this paragraph should go just before the "Fortunately the
> experimental status of the `git replay` command explicitly ..." that I
> suggest above.
>
>> This patch changes the default behavior to update refs directly using Git's
> s/This patch changes/Let's change/
>
> (See our SubmittingPatches documentation where it suggests using
> imperative mood to describe the changes we make.)
>
>> ref transaction API:
>>
>>      git replay --onto main topic1..topic2
>>      # No output; all refs updated atomically or none
>>
>> The implementation uses ref_store_transaction_begin() with atomic mode by
>> default, ensuring all ref updates succeed or all fail as a single operation.
>> This leverages git replay's existing server-side strengths (in-memory operation,
>> no work tree requirement) while adding the atomic guarantees that server
>> operations require.
>>
>> For users needing the traditional pipeline workflow, --output-commands
>> preserves the original behavior:
> I think something like:
>
> "For users needing the traditional pipeline workflow, let's add a new
> `--output-commands`option that preserves the original behavior:"
>
> is more explicit and makes it clear that it's a new option added by
> this patch and not an existing option.
>
>>      git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin
>>
>> The --allow-partial option enables partial failure tolerance.
> In the same way, something like:
>
> "Let's also add a new `--allow-partial` option that enables partial
> failure tolerance."
>
>> However, following
>> maintainer feedback, it implements a "strict success" model: the command exits
> I think you can remove "following maintainer feedback" here. The cover
> letter or a trailer like "Helped-by: ..." at the end of the commit
> message (but Junio will add his "Signed-off-by: ..." anyway so adding
> an Helped-by: ... about him is redundant) are the right place to
> mention people who helped or suggested changes.
>
>> with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY
>> updates fail. This ensures that --allow-partial changes error reporting style
>> (warnings vs hard errors) but not success criteria, handling edge cases like
>> "no updates needed" cleanly.
>>
>> Implementation details:
>> - Empty commit ranges now return success (exit code 0) rather than failure,
>>    as no commits to replay is a valid successful operation
> Nit: as all the sentences in this "Implementation details" list start
> with an uppercase, I think they should end with a full stop.
>
>> - Added comprehensive test coverage with 12 new tests covering atomic behavior,
>>    option validation, bare repository support, and edge cases
>> - Fixed test isolation issues to prevent branch state contamination between tests
>> - Maintains C89 compliance and follows Git's established coding conventions
> I am not sure this one is worth mentioning here, at least not like
> this. You may want to say in the cover letter that compared to the
> previous version this patch doesn't use 'bool' anymore and explain
> why. Or maybe you want to explain here that using the 'bool' type was
> considered but rejected for some reason. But in both cases, you should
> be explicit about the reason.
>
>> - Refactored option validation to use die_for_incompatible_opt2() for both
>>    --advance/--contained and --allow-partial/--output-commands conflicts,
>>    providing consistent error reporting
>> - Fixed --allow-partial exit code behavior to implement "strict success" model
>>    where any ref update failures result in exit code 1, even with partial tolerance
> This should probably go to the cover letter, as we should not talk in
> the commit message about changes since a previous version of the
> commit.
>
>> - Updated documentation with proper line wrapping, consistent terminology using
>>    "old default behavior", performance context, and reorganized examples for clarity
> This also sounds like a change compared to the previous version of the patch.
>
>> - Eliminates individual ref updates (refs_update_ref calls) that perform
>>    poorly with reftable backend
> This also sounds like a change compared to the previous version of the patch.
>
>> - Uses only batched ref transactions for optimal performance across all
>>    ref backends
> I think you can remove "only" in the sentence as in the
> --output-commands case no transaction is used.
>
>> - Avoids naming collision with git rebase --update-refs by using distinct
>>    option names
> This also sounds like a change compared to the previous version of the patch.
>
>> - Defaults to atomic behavior while preserving pipeline compatibility
> This has been discussed above. It doesn't look like an implementation
> detail to me.
>
>> The result is a command that works better for its primary use case (server-side
>> operations) while maintaining full backward compatibility for existing workflows.
>>
>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> Adding "Helped-by: ..." trailers for at least Elijah and Patrick would be nice.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-09-30 10:05     ` Phillip Wood
  2025-10-02 10:00       ` Karthik Nayak
@ 2025-10-02 22:20       ` Siddharth Asthana
  2025-10-08 14:01         ` Phillip Wood
  1 sibling, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-02 22:20 UTC (permalink / raw)
  To: Phillip Wood, git
  Cc: gitster, christian.couder, ps, newren, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin


On 30/09/25 15:35, Phillip Wood wrote:
> Hi Siddharth
>
> On 27/09/2025 00:08, Siddharth Asthana wrote:
>> The git replay command currently outputs update commands that must be
>> piped to git update-ref --stdin to actually update references:
>>
>>      git replay --onto main topic1..topic2 | git update-ref --stdin
>>
>> This design has significant limitations for server-side operations. The
>> two-command pipeline creates coordination complexity, provides no atomic
>> transaction guarantees by default
>
> Are you sure that's true? Maybe I'm missing something but my reading 
> of builtin/update-ref.c is that it when "--stdin" is given it starts a 
> ref transaction, reads the commands from stdin and applies them to 
> that transaction and then commits the transaction which will make the 
> updates atomic.


You are absolutely right, and Karthik confirmed this as well. That was a
significant error in my commit message. git update-ref --stdin IS atomic
by default.


The actual advantages of the new default aren't about atomicity (that
already exists), but rather:
- Eliminating the pipeline for the common case
- Better ergonomics for users who just want refs updated
- Simpler server-side automation

I will rewrite the commit message to accurately reflect this. Elijah
provided a good suggested structure that captures the real trade-offs
without false claims.


>
>> , and complicates automation in bare
>> repository environments where git replay is primarily used.
>
> How does it complicate automation in bare repositories?
>
> Christian has given detailed feedback on the rest of the commit 
> message so I'll not comment on it further.
>
>> diff --git a/Documentation/git-replay.adoc 
>> b/Documentation/git-replay.adoc
>> index 0b12bf8aa4..e104e0bc03 100644
>> --- a/Documentation/git-replay.adoc
>> +++ b/Documentation/git-replay.adoc
>> @@ -9,16 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new 
>> base, works with bare repos t
>>   SYNOPSIS
>>   --------
>>   [verse]
>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | 
>> --advance <branch>) <revision-range>...
>> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | 
>> --advance <branch>) [--output-commands | --allow-partial] 
>> <revision-range>...
>
> Please wrap this very long line
>
>> @@ -42,6 +42,20 @@ When `--advance` is specified, the update-ref 
>> command(s) in the output
>>   will update the branch passed as an argument to `--advance` to 
>> point at
>>   the new commits (in other words, this mimics a cherry-pick operation).
>>   +--output-commands::
>> +    Output update-ref commands instead of updating refs directly.
>> +    When this option is used, the output can be piped to `git 
>> update-ref --stdin`
>> +    for successive, relatively slow, ref updates. This is equivalent 
>> to the
>> +    old default behavior.
>> +
>> +--allow-partial::
>> +    Allow some ref updates to succeed even if others fail. By default,
>> +    ref updates are atomic (all succeed or all fail). With this option,
>> +    failed updates are reported as warnings rather than causing the 
>> entire
>> +    command to fail. The command exits with code 0 only if all updates
>> +    succeed; any failures result in exit code 1. Cannot be used with
>> +    `--output-commands`.
>
> Rather than having two incompatible options perhaps we could have a 
> single "--update-refs=(yes|print|allow-partial-updates)" argument. I 
> think the name "--allow-partial" is rather ambiguous as it does not 
> say what it is allowing to be partial.


After thinking about this and Elijah's feedback, I am leaning toward
dropping --allow-partial entirely since I don't have a concrete use case
for it. That simplifies things to just: default atomic updates vs
--output-commands for the traditional pipeline.

Would you still prefer a --update-refs=<mode> style, or is the simpler
--output-commands flag sufficient given that --allow-partial is going away?


>
>> +static int add_ref_to_transaction(struct ref_transaction *transaction,
>> +                  const char *refname,
>> +                  const struct object_id *new_oid,
>> +                  const struct object_id *old_oid,
>> +                  struct strbuf *err)
>> +{
>> +    return ref_transaction_update(transaction, refname, new_oid, 
>> old_oid,
>> +                      NULL, NULL, 0, "git replay", err);
>> +}
>
> I'm not sure this function adds much value. I think it would be better 
> to instead have a helper function that updates refs or prints the ref 
> updates so that we do not duplicate that code in the two places below.


ood point. I will extract a helper like:

     static int handle_ref_update(int output_commands,
                                  struct ref_transaction *transaction,
                                  const char *refname,
                                  const struct object_id *new_oid,
                                  const struct object_id *old_oid,
                                  struct strbuf *err)

This eliminates the duplication and fixes the over-long lines you pointed
out at both call sites.

Thanks!


>
>> @@ -434,10 +481,18 @@ int cmd_replay(int argc,
>>               if (decoration->type == DECORATION_REF_LOCAL &&
>>                   (contained || strset_contains(update_refs,
>>                                 decoration->name))) {
>> -                printf("update %s %s %s\n",
>> -                       decoration->name,
>> - oid_to_hex(&last_commit->object.oid),
>> -                       oid_to_hex(&commit->object.oid));
>> +                if (output_commands) {
>> +                    printf("update %s %s %s\n",
>> +                           decoration->name,
>> + oid_to_hex(&last_commit->object.oid),
>> + oid_to_hex(&commit->object.oid));
>> +                } else if (add_ref_to_transaction(transaction, 
>> decoration->name,
>> + &last_commit->object.oid,
>> +                                  &commit->object.oid,
>> +                                  &transaction_err) < 0) {
>> +                    ret = error(_("failed to add ref update to 
>> transaction: %s"), transaction_err.buf);
>> +                    goto cleanup;
>> +                }
>>               }
>
> The lines here are very long due to the indentation, having a separate 
> function to update the refs or print the ref updates would be much 
> more readable.
>
>>               decoration = decoration->next;
>>           }
>> @@ -445,10 +500,33 @@ int cmd_replay(int argc,
>>         /* In --advance mode, advance the target ref */
>>       if (result.clean == 1 && advance_name) {
>> -        printf("update %s %s %s\n",
>> -               advance_name,
>> -               oid_to_hex(&last_commit->object.oid),
>> -               oid_to_hex(&onto->object.oid));
>> +        if (output_commands) {
>> +            printf("update %s %s %s\n",
>> +                   advance_name,
>> +                   oid_to_hex(&last_commit->object.oid),
>> +                   oid_to_hex(&onto->object.oid));
>> +        } else if (add_ref_to_transaction(transaction, advance_name,
>> +                          &last_commit->object.oid,
>> +                          &onto->object.oid,
>> +                          &transaction_err) < 0) {
>> +            ret = error(_("failed to add ref update to transaction: 
>> %s"), transaction_err.buf);
>> +            goto cleanup;
>> +        }
>> +    }
>
> Putting the code to update the refs or print the ref updates into a 
> single function would avoid this duplication and over-long lines.
>
> Thanks
>
> Phillip
>
>> +    /* Commit the ref transaction if we have one */
>> +    if (transaction && result.clean == 1) {
>> +        if (ref_transaction_commit(transaction, &transaction_err)) {
>> +            if (allow_partial) {
>> +                warning(_("some ref updates failed: %s"), 
>> transaction_err.buf);
>> + ref_transaction_for_each_rejected_update(transaction,
>> +                                     print_rejected_update, NULL);
>> +                ret = 0; /* Set failure even with allow_partial */
>> +            } else {
>> +                ret = error(_("failed to update refs: %s"), 
>> transaction_err.buf);
>> +                goto cleanup;
>> +            }
>> +        }
>>       }
>>         merge_finalize(&merge_opt, &result);
>> @@ -457,9 +535,17 @@ int cmd_replay(int argc,
>>           strset_clear(update_refs);
>>           free(update_refs);
>>       }
>> -    ret = result.clean;
>> +
>> +    /* Handle empty ranges: if no commits were processed, treat as 
>> success */
>> +    if (!commits_processed)
>> +        ret = 1; /* Success - no commits to replay is not an error */
>> +    else
>> +        ret = result.clean;
>>     cleanup:
>> +    if (transaction)
>> +        ref_transaction_free(transaction);
>> +    strbuf_release(&transaction_err);
>>       release_revisions(&revs);
>>       free(advance_name);
>>   diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>> index 58b3759935..8b4301e227 100755
>> --- a/t/t3650-replay-basics.sh
>> +++ b/t/t3650-replay-basics.sh
>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
>>   '
>>     test_expect_success 'using replay to rebase two branches, one on 
>> top of other' '
>> -    git replay --onto main topic1..topic2 >result &&
>> +    git replay --output-commands --onto main topic1..topic2 >result &&
>>         test_line_count = 1 result &&
>>   @@ -67,9 +67,30 @@ test_expect_success 'using replay to rebase two 
>> branches, one on top of other' '
>>       test_cmp expect result
>>   '
>>   +test_expect_success 'using replay with default atomic behavior (no 
>> output)' '
>> +    # Create a test branch that wont interfere with others
>> +    git branch atomic-test topic2 &&
>> +    git rev-parse atomic-test >atomic-test-old &&
>> +
>> +    # Default behavior: atomic ref updates (no output)
>> +    git replay --onto main topic1..atomic-test >output &&
>> +    test_must_be_empty output &&
>> +
>> +    # Verify the branch was updated
>> +    git rev-parse atomic-test >atomic-test-new &&
>> +    ! test_cmp atomic-test-old atomic-test-new &&
>> +
>> +    # Verify the history is correct
>> +    git log --format=%s atomic-test >actual &&
>> +    test_write_lines E D M L B A >expect &&
>> +    test_cmp expect actual
>> +'
>> +
>>   test_expect_success 'using replay on bare repo to rebase two 
>> branches, one on top of other' '
>> -    git -C bare replay --onto main topic1..topic2 >result-bare &&
>> -    test_cmp expect result-bare
>> +    git -C bare replay --output-commands --onto main topic1..topic2 
>> >result-bare &&
>> +
>> +    # The result should match what we got from the regular repo
>> +    test_cmp result result-bare
>>   '
>>     test_expect_success 'using replay to rebase with a conflict' '
>> @@ -86,7 +107,7 @@ test_expect_success 'using replay to perform basic 
>> cherry-pick' '
>>       # 2nd field of result is refs/heads/main vs. refs/heads/topic2
>>       # 4th field of result is hash for main instead of hash for topic2
>>   -    git replay --advance main topic1..topic2 >result &&
>> +    git replay --output-commands --advance main topic1..topic2 
>> >result &&
>>         test_line_count = 1 result &&
>>   @@ -102,7 +123,7 @@ test_expect_success 'using replay to perform 
>> basic cherry-pick' '
>>   '
>>     test_expect_success 'using replay on bare repo to perform basic 
>> cherry-pick' '
>> -    git -C bare replay --advance main topic1..topic2 >result-bare &&
>> +    git -C bare replay --output-commands --advance main 
>> topic1..topic2 >result-bare &&
>>       test_cmp expect result-bare
>>   '
>>   @@ -115,7 +136,7 @@ test_expect_success 'replay fails when both 
>> --advance and --onto are omitted' '
>>   '
>>     test_expect_success 'using replay to also rebase a contained 
>> branch' '
>> -    git replay --contained --onto main main..topic3 >result &&
>> +    git replay --output-commands --contained --onto main 
>> main..topic3 >result &&
>>         test_line_count = 2 result &&
>>       cut -f 3 -d " " result >new-branch-tips &&
>> @@ -139,12 +160,12 @@ test_expect_success 'using replay to also 
>> rebase a contained branch' '
>>   '
>>     test_expect_success 'using replay on bare repo to also rebase a 
>> contained branch' '
>> -    git -C bare replay --contained --onto main main..topic3 
>> >result-bare &&
>> +    git -C bare replay --output-commands --contained --onto main 
>> main..topic3 >result-bare &&
>>       test_cmp expect result-bare
>>   '
>>     test_expect_success 'using replay to rebase multiple divergent 
>> branches' '
>> -    git replay --onto main ^topic1 topic2 topic4 >result &&
>> +    git replay --output-commands --onto main ^topic1 topic2 topic4 
>> >result &&
>>         test_line_count = 2 result &&
>>       cut -f 3 -d " " result >new-branch-tips &&
>> @@ -168,7 +189,7 @@ test_expect_success 'using replay to rebase 
>> multiple divergent branches' '
>>   '
>>     test_expect_success 'using replay on bare repo to rebase multiple 
>> divergent branches, including contained ones' '
>> -    git -C bare replay --contained --onto main ^main topic2 topic3 
>> topic4 >result &&
>> +    git -C bare replay --output-commands --contained --onto main 
>> ^main topic2 topic3 topic4 >result &&
>>         test_line_count = 4 result &&
>>       cut -f 3 -d " " result >new-branch-tips &&
>> @@ -217,4 +238,131 @@ test_expect_success 
>> 'merge.directoryRenames=false' '
>>           --onto rename-onto rename-onto..rename-from
>>   '
>>   +# Tests for new default atomic behavior and options> > 
>> +test_expect_success 'replay default behavior should not produce 
> output when successful' '
>> +    git replay --onto main topic1..topic3 >output &&
>> +    test_must_be_empty output
>> +'
>> +
>> +test_expect_success 'replay with --output-commands produces 
>> traditional output' '
>> +    git replay --output-commands --onto main topic1..topic3 >output &&
>> +    test_line_count = 1 output &&
>> +    grep "^update refs/heads/topic3 " output
>> +'
>> +
>> +test_expect_success 'replay with --allow-partial should not produce 
>> output when successful' '
>> +    git replay --allow-partial --onto main topic1..topic3 >output &&
>> +    test_must_be_empty output
>> +'
>> +
>> +test_expect_success 'replay fails when --output-commands and 
>> --allow-partial are used together' '
>> +    test_must_fail git replay --output-commands --allow-partial 
>> --onto main topic1..topic2 2>error &&
>> +    grep "cannot be used together" error
>> +'
>> +
>> +test_expect_success 'replay with --contained updates multiple 
>> branches atomically' '
>> +    # Create fresh test branches based on the original structure
>> +    # contained-topic1 should be contained within the range to 
>> contained-topic3
>> +    git branch contained-base main &&
>> +    git checkout -b contained-topic1 contained-base &&
>> +    test_commit ContainedC &&
>> +    git checkout -b contained-topic3 contained-topic1 &&
>> +    test_commit ContainedG &&
>> +    test_commit ContainedH &&
>> +    git checkout main &&
>> +
>> +    # Store original states
>> +    git rev-parse contained-topic1 >contained-topic1-old &&
>> +    git rev-parse contained-topic3 >contained-topic3-old &&
>> +
>> +    # Use --contained to update multiple branches - this should 
>> update both
>> +    git replay --contained --onto main 
>> contained-base..contained-topic3 &&
>> +
>> +    # Verify both branches were updated
>> +    git rev-parse contained-topic1 >contained-topic1-new &&
>> +    git rev-parse contained-topic3 >contained-topic3-new &&
>> +    ! test_cmp contained-topic1-old contained-topic1-new &&
>> +    ! test_cmp contained-topic3-old contained-topic3-new
>> +'
>> +
>> +test_expect_success 'replay atomic behavior: all refs updated or 
>> none' '
>> +    # Store original state
>> +    git rev-parse topic4 >topic4-old &&
>> +
>> +    # Default atomic behavior
>> +    git replay --onto main main..topic4 &&
>> +
>> +    # Verify ref was updated
>> +    git rev-parse topic4 >topic4-new &&
>> +    ! test_cmp topic4-old topic4-new &&
>> +
>> +    # Verify no partial state
>> +    git log --format=%s topic4 >actual &&
>> +    test_write_lines J I M L B A >expect &&
>> +    test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'replay works correctly with bare repositories' '
>> +    # Test atomic behavior in bare repo (important for Gitaly)
>> +    git checkout -b bare-test topic1 &&
>> +    test_commit BareTest &&
>> +
>> +    # Test with bare repo - replay the commits from main..bare-test 
>> to get the full history
>> +    git -C bare fetch .. bare-test:bare-test &&
>> +    git -C bare replay --onto main main..bare-test &&
>> +
>> +    # Verify the bare repo was updated correctly (no output)
>> +    git -C bare log --format=%s bare-test >actual &&
>> +    test_write_lines BareTest F C M L B A >expect &&
>> +    test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'replay --allow-partial with no failures 
>> produces no output' '
>> +    git checkout -b partial-test topic1 &&
>> +    test_commit PartialTest &&
>> +
>> +    # Should succeed silently even with partial mode
>> +    git replay --allow-partial --onto main topic1..partial-test 
>> >output &&
>> +    test_must_be_empty output
>> +'
>> +
>> +test_expect_success 'replay maintains ref update consistency' '
>> +    # Test that traditional vs atomic produce equivalent results
>> +    git checkout -b method1-test topic2 &&
>> +    git checkout -b method2-test topic2 &&
>> +
>> +    # Both methods should update refs to point to the same replayed 
>> commits
>> +    git replay --output-commands --onto main topic1..method1-test 
>> >update-commands &&
>> +    git update-ref --stdin <update-commands &&
>> +    git log --format=%s method1-test >traditional-result &&
>> +
>> +    # Direct atomic method should produce same commit history
>> +    git replay --onto main topic1..method2-test &&
>> +    git log --format=%s method2-test >atomic-result &&
>> +
>> +    # Both methods should produce identical commit histories
>> +    test_cmp traditional-result atomic-result
>> +'
>> +
>> +test_expect_success 'replay error messages are helpful and clear' '
>> +    # Test that error messages are clear
>> +    test_must_fail git replay --output-commands --allow-partial 
>> --onto main topic1..topic2 2>error &&
>> +    grep "cannot be used together" error
>> +'
>> +
>> +test_expect_success 'replay with empty range produces no output and 
>> no changes' '
>> +    # Create a test branch for empty range testing
>> +    git checkout -b empty-test topic1 &&
>> +    git rev-parse empty-test >empty-test-before &&
>> +
>> +    # Empty range should succeed but do nothing
>> +    git replay --onto main empty-test..empty-test >output &&
>> +    test_must_be_empty output &&
>> +
>> +    # Branch should be unchanged
>> +    git rev-parse empty-test >empty-test-after &&
>> +    test_cmp empty-test-before empty-test-after
>> +'
>> +
>>   test_done
>

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-02 10:00       ` Karthik Nayak
@ 2025-10-02 22:20         ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-02 22:20 UTC (permalink / raw)
  To: Karthik Nayak, Phillip Wood, git
  Cc: gitster, christian.couder, ps, newren, code, rybak.a.v, jltobler,
	toon, johncai86, johannes.schindelin


On 02/10/25 15:30, Karthik Nayak wrote:
> Phillip Wood <phillip.wood123@gmail.com> writes:
>
>> Hi Siddharth
>>
>> On 27/09/2025 00:08, Siddharth Asthana wrote:
>>> The git replay command currently outputs update commands that must be
>>> piped to git update-ref --stdin to actually update references:
>>>
>>>       git replay --onto main topic1..topic2 | git update-ref --stdin
>>>
>>> This design has significant limitations for server-side operations. The
>>> two-command pipeline creates coordination complexity, provides no atomic
>>> transaction guarantees by default
>> Are you sure that's true? Maybe I'm missing something but my reading of
>> builtin/update-ref.c is that it when "--stdin" is given it starts a ref
>> transaction, reads the commands from stdin and applies them to that
>> transaction and then commits the transaction which will make the updates
>> atomic.
>>
> You're right. Using '--stdin' is atomic by default. You can manually
> handle the transaction's by passing in the 'start', 'prepare', 'commit',
> 'abort' sub-commands in the '--stdin' mode.


Thanks for confirming this Karthik. I will correct the commit message to
accurately represent what update-ref --stdin provides.


>
> [snip]

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-09-30  8:23     ` Christian Couder
  2025-10-02 22:16       ` Siddharth Asthana
@ 2025-10-02 22:55       ` Elijah Newren
  2025-10-03  7:05         ` Christian Couder
  1 sibling, 1 reply; 129+ messages in thread
From: Elijah Newren @ 2025-10-02 22:55 UTC (permalink / raw)
  To: Christian Couder
  Cc: Siddharth Asthana, git, gitster, ps, code, rybak.a.v, karthik.188,
	jltobler, toon, johncai86, johannes.schindelin

Hi Christian,

Excellent review, I just have one tangential question for you...

On Tue, Sep 30, 2025 at 1:24 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
> On Sat, Sep 27, 2025 at 1:09 AM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
> >
> > The git replay command currently outputs update commands that must be
> > piped to git update-ref --stdin to actually update references:
> >
> >     git replay --onto main topic1..topic2 | git update-ref --stdin
> >
> > This design has significant limitations for server-side operations. The
> > two-command pipeline creates coordination complexity, provides no atomic
> > transaction guarantees by default, and complicates automation in bare
> > repository environments where git replay is primarily used.
>
> Yeah, right.

I'm unsure if you are expressing disbelief, or agreeing when you use
this phrase.  Most commonly when I see it, I assume the former (see
https://dictionary.cambridge.org/us/dictionary/english/yeah-right and
https://www.merriam-webster.com/dictionary/yeah for example), but I
think you've consistently used this with the opposite connotation.  Am
I correct on that?  (This is a particular phrase where tone of voice
used would be really helpful, which doesn't get included in emails
unfortunately.)

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-02 16:32     ` Elijah Newren
  2025-10-02 18:27       ` Junio C Hamano
@ 2025-10-02 23:27       ` Siddharth Asthana
  2025-10-03  7:59         ` Christian Couder
  2025-10-03 19:48         ` Elijah Newren
  1 sibling, 2 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-02 23:27 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, gitster, christian.couder, ps, code, rybak.a.v, karthik.188,
	jltobler, toon, johncai86, johannes.schindelin


On 02/10/25 22:02, Elijah Newren wrote:
> Hi,
>
> First of all, thanks for continuing to work on this.
>
> On Fri, Sep 26, 2025 at 4:09 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> The git replay command currently outputs update commands that must be
> can be, not must be.
>
> This separation had three advantages:
>    * it made testing easy (when state isn't modified from one step to
> the next, you don't need to make temporary branches or have undo
> commands, or try to track the changes)
>    * it provided a natural can-it-rebase-cleanly (and what would it
> rebase to) capability without automatically updating refs, I guess
> kind of like a --dry-run
>    * it provided a natural low-level tool for the suite of hash-object,
> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users
> to have another building block for experimentation and making new
> tools.
>
> I was particularly focused on the last of those items for the intial
> version at the time, but it should be noted that all three of these
> are somewhat special cases, and the most common user desire is going
> to be replaying commits and updating the references at the end.


You are absolutely right - I mischaracterized both the "must be" claim and
the atomicity guarantee. Phillip and Karthik confirmed that update-ref
--stdin IS atomic by default, so my justification was fundamentally wrong.

Your suggested commit message structure is much better. It accurately
captures the real trade-offs (testing, dry-run capability, building
block) without making false claims, and explains why making ref updates
default serves the common case better.

I will use your suggested rewrite verbatim for v3.


>> piped to git update-ref --stdin to actually update references:
>>
>>      git replay --onto main topic1..topic2 | git update-ref --stdin
> or, alternatively, for those that want a single, atomic (assuming the
> backend supports it) transaction:
>
>     (echo start; git replay --onto main topic1..topic2; echo prepare;
> echo commit) | git update-ref --stdin
>
>> This design has significant limitations for server-side operations. The
>> two-command pipeline creates coordination complexity,
> agreed
>
>> provides no atomic
>> transaction guarantees by default,
> Sure, but it's quite trivial to add, right -- as shown above with the
> extra "start", "prepare", "commit" directives?


You are absolutely right. I overstated the difficulty - atomicity is
trivial to achieve with those directives. The real benefit is eliminating
the pipeline entirely for the common case, not providing atomicity that
wasn't already available.


>> and complicates automation in bare
>> repository environments where git replay is primarily used.
> Isn't this repeating the first statement from this paragraph?
>
> The paragraph also leaves off that I think it makes it more
> useful/usable to end-users.


You are right - I was repeating the coordination complexity point. And I
missed the broader benefit to end users (not just server-side). I will use
your suggested rewrite which captures this properly.


>
>> The core issue is that git replay was designed around command output rather
>> than direct action. This made sense for a plumbing tool, but creates barriers
>> for the primary use case: server-side operations that need reliable, atomic
>> ref updates without pipeline complexity.
> git replay was originally written with the goal of eventually
> providing a better interactive rebase.  I thought it important to have
> the earlier versions provide new useful low-level building blocks, but
> didn't intend for it to just be a low-level building block
> indefinitely.  The primary use case originally envisioned was
> client-side user interactive operations without much thought for the
> server, but it turned out to be easiest to implement what server-side
> operations needed first.  I made others aware of what I was working on
> so they could see, and Christian latched on and to my surprise told me
> it had enough for what he needed...and Johannes said about the same.
> Unfortunately, I got reassigned away from Git at my former dayjob,
> _and_ had multiple big long-term family issues strike about the same
> time, _and_ all Git-related employers did mass layoffs and locked down
> hiring about that time as well...so the end result is only that
> initial server-side stuff was done and it looks to outside observers
> like git replay's design and usecase was around the server.  While
> that's an understandable guess from the outside, this paragraph
> appears to have taken those guesses and rewrites them as fact.  I
> think it could be reworded with small tweaks to alter it and make it
> factual (s/designed/focused initially/, s/the primary use case/its
> current primary use case/), but even then, the paragraph that remains
> feels like it's just repeating what you said above.  It doesn't feel
> like it adds any positive value.
>
> Might I suggest a rewrite of the text of the commit message to this point?
>
> =====
> The git replay command currently outputs update commands that can be
> piped to update-ref to achieve a rebase, e.g.
>
>    git replay --onto main topic1..topic2 | git update-ref --stdin
>
> This separation had advantages for three special cases:
>    * it made testing easy (when state isn't modified from one step to
> the next, you don't need to make temporary branches or have undo
> commands, or try to track the changes)
>    * it provided a natural can-it-rebase-cleanly (and what would it
> rebase to) capability without automatically updating refs, I guess
> kind of like a --dry-run
>    * it provided a natural low-level tool for the suite of hash-object,
> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users
> to have another building block for experimentation and making new
> tools.
>
> However, it should be noted that all three of these are somewhat
> special cases; users, whether on the client or server side, would
> almost certainly find it more ergonomical to simply have the updating
> of refs be the default.  Change the default behavior to update refs
> directly, and atomically (at least to the extent supported by the refs
> backend in use).
> ====
>
> after this, I'd suggest also nuking your next few paragraphs and then
> continuing with:
>
>> For users needing the traditional pipeline workflow, --output-commands
>> preserves the original behavior:
>>
>>      git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin
> This is good.  Did you also add a config option so that someone can
> just set that option once and use the old behavior?  (as per the
> suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/
> ?)


I didn't, but I should have. I will add a config option for v3.

For naming, I am thinking either:
   - replay.updateRefs (boolean: true = update, false = output-commands)
   - replay.defaultOutput (string: "update" | "commands")

The boolean feels simpler, but the string might be more extensible if we
add other output modes later. Which pattern feels more consistent with
existing Git config conventions? Looking at rebase.* they're mostly
boolean toggles, but am I missing a better example to follow?


>> The --allow-partial option enables partial failure tolerance. However, following
>> maintainer feedback, it implements a "strict success" model: the command exits
>> with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY
>> updates fail. This ensures that --allow-partial changes error reporting style
>> (warnings vs hard errors) but not success criteria, handling edge cases like
>> "no updates needed" cleanly.
> Why is --allow-partial helpful?  You discussed at length why you
> wanted atomic transactions, but you introduce this option with no
> rationale and instead just discuss that you implemented it and some
> design choices once you presuppose that someone wants to use it.
>
> Is there a usecase?  I asked for it last time, and suggested
> discarding the modes without one, but you only discarded one of the
> extras while leaving this one in.  I'd recommend discarding this one
> too and just having the two modes -- the output commands that get fed
> to update-ref, or the automatic transactional update of all or no
> refs.


You are right - I don't have a concrete use case. I was trying to
anticipate potential needs but ended up adding unjustified complexity.

I will remove --allow-partial entirely from v3. This simplifies to exactly
two modes with clear purposes:
   1. Default: atomic ref updates (all-or-nothing)
   2. --output-commands: traditional pipeline for special cases

Much cleaner design.


>
>> Implementation details:
> Christian's comments on this list are good; it seems to mostly be
> stuff that belongs in the cover letter.  But I'll comment/query about
> two things...
>
>> - Empty commit ranges now return success (exit code 0) rather than failure,
>>    as no commits to replay is a valid successful operation
> That is a useful thing to call out.  Hmm....
>
>> - Defaults to atomic behavior while preserving pipeline compatibility
> The first part has already been covered at length; it doesn't seem
> like it needs to be repeated.  What does "while preserving pipeline
> compatibility" mean, though?


That was meant to reference --output-commands allowing the traditional
workflow, but you are right it's vague and redundant since that's already
been discussed. I will remove it.


>
>
>> The result is a command that works better for its primary use case (server-side
>> operations)
> This sentence feels like it's rewriting the history behind the
> command, and undersells the change by not noting that it *also* helps
> with client-side usage.  Also, since I think it's about the third time
> you've said this, it adds nothing to the discussion either; let's just
> strike it.
>
>> while maintaining full backward compatibility for existing workflows.
> and this is clearly false; backwards compatibility was intentionally
> broken by making update-refs the default.  Junio suggested making it
> easy to get the old behavior with a config setting, but looking ahead
> it appears you didn't even make it easy for users to recover the old
> behavior; they have to specify an additional flag with every command
> they run.
>
> Also, you further broke "full" backward compatibility by changing the
> exit status for empty ranges.  I think that's a rather minor change
> and maybe bugfix, but "full" invites comparisons and scrutiny of that
> sort.
>
> We could say something like "while making it easy to recover the
> traditional behavior with a simple command line flag", but this again
> feels like you're just repeating what was called out above.  Maybe
> just strike it as well?
>
>
> And on a separate note, several of your lines in your commit message
> are too long; in an 80-column terminal, a `git log` on your commit
> message has several lines wrapping around.  (Command lines, and for
> future reference output from commands, can run longer, but regular
> paragraphs should fit.)


I will wrap the commit message paragraphs to fit 80 columns
(keeping command lines and output longer as appropriate).


>
>> +...Use `--output-commands`
>> +to get the old default behavior where update commands that can be piped
>> +to `git update-ref --stdin` are emitted (see the OUTPUT section below).
> Perhaps instead:
>
> Use `--output-commands` to avoid the automatic ref updates and instead
> get update commands that can be piped
> to `git update-ref --stdin` (see the OUTPUT section below).
>
>> +--output-commands::
>> +       Output update-ref commands instead of updating refs directly.
>> +       When this option is used, the output can be piped to `git update-ref --stdin`
>> +       for successive, relatively slow, ref updates. This is equivalent to the
>> +       old default behavior.
> "piped" => "piped as-is" + "successive, relatively slow," => "a
> non-transactional set of" ?


Good wording improvements. "Piped as-is" clarifies no transformation is
needed, and "non-transactional" is more accurate than "successive,
relatively slow" which made performance claims without justification.


>> +--allow-partial::
>> +       Allow some ref updates to succeed even if others fail. By default,
>> +       ref updates are atomic (all succeed or all fail). With this option,
>> +       failed updates are reported as warnings rather than causing the entire
>> +       command to fail. The command exits with code 0 only if all updates
>> +       succeed; any failures result in exit code 1. Cannot be used with
>> +       `--output-commands`.
> If we keep this, I like Phillip's suggestion to combine to a single
> flag with a value.  But I still want to hear the use case behind it;
> it goes against what you repeatedly claimed was wanted on the server,
> and you didn't discuss any other usecase.  It feels like you were
> trying to proactively support every possible way people might want to
> update without considering the usecases behind it, and I'd rather just
> leave it unimplemented unless or until there's demand for it.
>
>> +
>>   <revision-range>::
>>          Range of commits to replay. More than one <revision-range> can
>>          be passed, but in `--advance <branch>` mode, they should have
>> @@ -54,15 +68,20 @@ include::rev-list-options.adoc[]
>>   OUTPUT
>>   ------
>>
>> -When there are no conflicts, the output of this command is usable as
>> -input to `git update-ref --stdin`.  It is of the form:
>> +By default, when there are no conflicts, this command updates the relevant
>> +references using atomic transactions
> atomic transaction*s*?  Shouldn't that be *an* atomic transaction instead?


You are right. Each invocation is one transaction with multiple updates.
I wrote "transactions" thinking about multiple command invocations, but
that's confusing.

I will change to "an atomic transaction" and clarify once that all ref
updates succeed or all fail, rather than repeating the point.


>
>>> and produces no output. All ref updates
>> +succeed or all fail (atomic behavior).
> Why the need to repeat?


You are right saying "atomic transactions" and then "(atomic behavior)"
is redundant. I will state it once clearly: "using an atomic transaction"
and remove the parenthetical.


>
>> +To rebase multiple branches with partial failure tolerance:
>> +
>> +------------
>> +$ git replay --allow-partial --contained --onto origin/main origin/main..tipbranch
>> +------------
> I don't understand why this one deserves an example; the command line
> flag seems to be self-explanatory.  The onto and advanced are
> interesting in that the ranges specified with them might not be
> obvious from their description and so examples help.  One or maybe two
> --contained examples can go with those, to demonstrate how it involves
> additional branches, though those might be better coupled with the
> --output-commands because the output more clearly demonstrates what is
> happening (i.e. what is being updated).  But I don't see what this
> example might elucidate.
>
> Then again, while I perfectly understand what the flag does, I have no
> idea why you thought it was useful to add, so maybe I'm just missing
> something.
>
>>   When calling `git replay`, one does not need to specify a range of
>>   commits to replay using the syntax `A..B`; any range expression will
>> -do:
>> +do. Here's an example where you explicitly specify which branches to rebase:
>>
>>   ------------
>>   $ git replay --onto origin/main ^base branch1 branch2 branch3
>> +------------
>> +
>> +This gives you explicit control over exactly which branches are rebased,
>> +unlike the previous `--contained` example which automatically discovers them.
>> +
>> +To see the update commands that would be executed:
>> +
>> +------------
>> +$ git replay --output-commands --onto origin/main ^base branch1 branch2 branch3
>>   update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>>   update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>>   update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
> As noted above, I don't think we need to repeat an --output-commands
> example for every example that exists; it feels like overkill.  One
> (or _maybe_ two) should be sufficient.


Agreed. With --allow-partial removed, I will keep just one clear
--output-commands example that demonstrates the traditional pipeline
workflow. The multiple examples were overkill.


>
>> @@ -330,9 +361,12 @@ int cmd_replay(int argc,
>>                  usage_with_options(replay_usage, replay_options);
>>          }
>>
>> -       if (advance_name_opt && contained)
>> -               die(_("options '%s' and '%s' cannot be used together"),
>> -                   "--advance", "--contained");
>> +       die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>> +                                 contained, "--contained");
> Broken indentation.  Also, should this have been done as a preparatory
> cleanup patch?


Good catches. I will fix the indentation.

On making it a preparatory patch: should I split it out as a separate
cleanup commit, or is it minor enough to fold into the main change? I am
leaning toward folding it in since it's directly related to the option
handling changes



>
>> @@ -407,6 +452,8 @@ int cmd_replay(int argc,
>>                  khint_t pos;
>>                  int hr;
>>
>> +               commits_processed = 1;
>> +
>>                  if (!commit->parents)
>>                          die(_("replaying down to root commit is not supported yet!"));
>>                  if (commit->parents->next)
>> @@ -457,9 +535,17 @@ int cmd_replay(int argc,
>>                  strset_clear(update_refs);
>>                  free(update_refs);
>>          }
>> -       ret = result.clean;
>> +
>> +       /* Handle empty ranges: if no commits were processed, treat as success */
>> +       if (!commits_processed)
>> +               ret = 1; /* Success - no commits to replay is not an error */
>> +       else
>> +               ret = result.clean;
> The change to treat empty ranges as success is an orthogonal change
> that I think at a minimum belongs in a separate patch.  Out of
> curiosity, how did you discover the exit status with an empty commit
> range?  Why does someone specify such a range, and what form or forms
> might it come in?  And is merely returning a successful result enough,
> or is there more that needs to be done for correctness?


I was thinking about automated scripts that compute ranges dynamically -
they might generate A..B where it turns out A==B, and treating that as
"no work needed, success" seemed reasonable for scripting.

But you raise a good point: A..A seems like obvious user error (why would
anyone do that intentionally?), and B..A where B contains A is likely a
mistake that maybe should error rather than silently succeed.

I am inclined to drop it entirely from this series. If there's real demand
for specific empty-range handling, we can add it later with proper
discussion of the actual use cases. Does that sound reasonable?


>
>> +test_expect_success 'using replay with default atomic behavior (no output)' '
>> +       # Create a test branch that wont interfere with others
> This works, but I feel doing this clutters the repo for someone
> inspecting later when some test in the testsuite fails, and makes
> readers track more branches to find out what the tests are all doing.
> Would it be easier to prefix your test with something like
>
>          START=$(git rev-parse topic2) &&
>          test_when_finished "git branch -f topic2 $START" &&


That's much cleaner - I will rework all the new tests to use
test_when_finished instead of creating new branches. It keeps the test
state contained and makes it easier to understand what each test is
actually verifying.


>
> and then...
>
>> +       git branch atomic-test topic2 &&
>> +       git rev-parse atomic-test >atomic-test-old &&
> drop these lines...
>
>> +
>> +       # Default behavior: atomic ref updates (no output)
>> +       git replay --onto main topic1..atomic-test >output &&
> use topic2 instead of atomic-test...
>
>> +       test_must_be_empty output &&
>> +
>> +       # Verify the branch was updated
>> +       git rev-parse atomic-test >atomic-test-new &&
>> +       ! test_cmp atomic-test-old atomic-test-new &&
> Not sure this comparison to verify the branch was updated makes much
> sense given that the few lines below both test that it was updated and
> that it was updated to the right thing.
>
>> +
>> +       # Verify the history is correct
>> +       git log --format=%s atomic-test >actual &&
>> +       test_write_lines E D M L B A >expect &&
>> +       test_cmp expect actual
> ...and finally use topic2 instead of atomic-test here.
>
>
> Similarly, using test_when_finished throughout the rest of the
> testsuite similarly I think would make it a bit easier to follow and
> later debug.
>
>
>>   test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
>> -       git -C bare replay --onto main topic1..topic2 >result-bare &&
>> -       test_cmp expect result-bare
>> +       git -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&
>> +
>> +       # The result should match what we got from the regular repo
>> +       test_cmp result result-bare
>>   '
> What do you mean by "regular repo"?  There's only one repo at play.
>
> Also, why change "expect" to "result" here?  You don't care if you get
> the expected result, merely that you get the same result as a previous
> test even if it was also buggy?


You are right "regular repo" is confusing since we are just comparing
non-bare vs bare on the same repository.

The "result" vs "expect" issue happened because I inserted a test between
the expectation-building and this test. I will reorder the tests so the
expectation is built immediately before this test, or just rebuild the
expectation here for clarity.


> I think the reason was that you inserted a test since writing the
> expectation out, but I think it'd be better to either re-write the
> expectation out and compare to it, or reorder the tests so you can
> use the same expectation from before.
>
>
>> +# Tests for new default atomic behavior and options
> The word "new" here is going to become unhelpful in the future when
> someone reads this a few years from now; you should strike it.  What
> does "and options" mean here?


Good point. I will change it to just "# Tests for default atomic behavior"
since that's what the tests actually verify - the default behavior and
that it's atomic.


>
>> +
>> +test_expect_success 'replay default behavior should not produce output when successful' '
>> +       git replay --onto main topic1..topic3 >output &&
>> +       test_must_be_empty output
>> +'
> This changes where topic3 points; I think a test_when_finished to
> reset it back at the end of the test would be nice.
>
>> +test_expect_success 'replay with --output-commands produces traditional output' '
>> +       git replay --output-commands --onto main topic1..topic3 >output &&
>> +       test_line_count = 1 output &&
>> +       grep "^update refs/heads/topic3 " output
>> +'
> The fact that topic3 was already replayed in the previous test makes
> this test weaker.  And the fact that it does nothing but check that
> there is in fact some output makes it weaker still.  But rather than
> fix it, there were already lots of commands that tested
> --output-commands prior to this, and you added a comment above that
> you were adding tests of the atomic behavior, I don't see why we need
> any additional tests of --output-commands.


You are right this test is redundant given existing --output-commands
tests. I will remove it since we already have adequate coverage of that
mode.


>
>> +test_expect_success 'replay with --allow-partial should not produce output when successful' '
>> +       git replay --allow-partial --onto main topic1..topic3 >output &&
>> +       test_must_be_empty output
>> +
>> +test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '
>> +       test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
>> +       grep "cannot be used together" error
>> +'
> These are fine if --allow-partial can be motivated, but otherwise
> should be removed along with that flag.
>
>> +
>> +test_expect_success 'replay with --contained updates multiple branches atomically' '
>> +       # Create fresh test branches based on the original structure
> Unnecessary if you use the test_when_finished stuff I showed you to
> ensure the original structure remains intact
>
>> +       # contained-topic1 should be contained within the range to contained-topic3
>> +       git branch contained-base main &&
>> +       git checkout -b contained-topic1 contained-base &&
>> +       test_commit ContainedC &&
>> +       git checkout -b contained-topic3 contained-topic1 &&
>> +       test_commit ContainedG &&
>> +       test_commit ContainedH &&
>> +       git checkout main &&
>> +
>> +       # Store original states
>> +       git rev-parse contained-topic1 >contained-topic1-old &&
>> +       git rev-parse contained-topic3 >contained-topic3-old &&
>> +
>> +       # Use --contained to update multiple branches - this should update both
>> +       git replay --contained --onto main contained-base..contained-topic3 &&
>> +
>> +       # Verify both branches were updated
>> +       git rev-parse contained-topic1 >contained-topic1-new &&
>> +       git rev-parse contained-topic3 >contained-topic3-new &&
>> +       ! test_cmp contained-topic1-old contained-topic1-new &&
>> +       ! test_cmp contained-topic3-old contained-topic3-new
>> +'
> I think verifying both branches were modified without checking
> anything about the modification is a pretty weak test.  We should
> check that both branches have the appropriate commit sequence in them
> via git log output, as previous tests do.


Right. I will add verification of the actual commit sequences using
git log --format=%s for both branches, not just that they changed.


>
>> +test_expect_success 'replay atomic behavior: all refs updated or none' '
>> +       # Store original state
>> +       git rev-parse topic4 >topic4-old &&
>> +
>> +       # Default atomic behavior
>> +       git replay --onto main main..topic4 &&
>> +
>> +       # Verify ref was updated
>> +       git rev-parse topic4 >topic4-new &&
>> +       ! test_cmp topic4-old topic4-new &&
>> +
>> +       # Verify no partial state
>> +       git log --format=%s topic4 >actual &&
>> +       test_write_lines J I M L B A >expect &&
>> +       test_cmp expect actual
>> +'
> This test doesn't test what it says it does.  It merely tests that the
> single topic was modified, and as such, isn't a very useful additional
> test.  Using the contained flag where one of the branches had a lock
> in the way or a simultaneous push and then testing that none of the
> branches got updated would be needed if you want to test this.


You are absolutely right - testing a single ref doesn't demonstrate
atomicity at all.

For v3, I will create a real atomic test: use --contained with multiple
branches, introduce a lock on one of the refs (maybe via
.git/refs/heads/branch.lock), then verify that NONE of the branches got
updated when the transaction fails. That actually tests the all-or-nothing
guarantee.


>
>> +test_expect_success 'replay works correctly with bare repositories' '
>> +       # Test atomic behavior in bare repo (important for Gitaly)
>> +       git checkout -b bare-test topic1 &&
>> +       test_commit BareTest &&
>> +
>> +       # Test with bare repo - replay the commits from main..bare-test to get the full history
>> +       git -C bare fetch .. bare-test:bare-test &&
>> +       git -C bare replay --onto main main..bare-test &&
>> +
>> +       # Verify the bare repo was updated correctly (no output)
>> +       git -C bare log --format=%s bare-test >actual &&
>> +       test_write_lines BareTest F C M L B A >expect &&
>> +       test_cmp expect actual
>> +'
> This doesn't test what the initial comment says is the important bit;
> in fact, since only a single ref is being updated there's no chance to
> test atomicity of updating multiple refs. Or is the parenthetical
> ambiguous and I should have read that as bare repositories being
> important?  If the latter, this test is mere redundancy; there were
> multiple tests in a bare repository previously.


The parenthetical was ambiguous, I meant bare repositories are important
for Gitaly, not that this test demonstrates atomicity. Since there are
already multiple bare repo tests, I'll either remove this as redundant or
reframe it to test atomicity with multiple refs in a bare repo.


>
>> +test_expect_success 'replay --allow-partial with no failures produces no output' '
>> +       git checkout -b partial-test topic1 &&
>> +       test_commit PartialTest &&
>> +
>> +       # Should succeed silently even with partial mode
>> +       git replay --allow-partial --onto main topic1..partial-test >output &&
>> +       test_must_be_empty output
>> +'
> Test is fine, _if_ --allow-partial is worthwhile to add.
>
>> +test_expect_success 'replay maintains ref update consistency' '
>> +       # Test that traditional vs atomic produce equivalent results
> I think the comment is a better test name than the actual test name;
> I'd remove the existing testname, and move the comment to the test
> name, and then not have a comment here.


Good suggestion. I will rename to:

   test_expect_success 'traditional pipeline and atomic update produce
   equivalent results'

Much clearer what it's actually testing.


>
>> +       git checkout -b method1-test topic2 &&
>> +       git checkout -b method2-test topic2 &&
>> +
>> +       # Both methods should update refs to point to the same replayed commits
>> +       git replay --output-commands --onto main topic1..method1-test >update-commands &&
>> +       git update-ref --stdin <update-commands &&
>> +       git log --format=%s method1-test >traditional-result &&
>> +
>> +       # Direct atomic method should produce same commit history
>> +       git replay --onto main topic1..method2-test &&
>> +       git log --format=%s method2-test >atomic-result &&
>> +
>> +       # Both methods should produce identical commit histories
>> +       test_cmp traditional-result atomic-result
>> +'
>> +
>> +test_expect_success 'replay error messages are helpful and clear' '
>> +       # Test that error messages are clear
>> +       test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
>> +       grep "cannot be used together" error
>> +'
> Does this test that "error messages are helpful and clear" or just
> that "--output-commands and --allow-partial" are incompatible?


It's really just testing incompatible options. Since --allow-partial is
going away, this test goes away too.


> The former would be a test of _all_ error messages from git replay, and
> would need to look at more than just a small substring of the error
> message(s).


It's really just testing incompatible options. Since --allow-partial is
going away, this test goes away too. The option incompatibility will be
reduced to just the existing --advance/--contained check.


>
>> +test_expect_success 'replay with empty range produces no output and no changes' '
>> +       # Create a test branch for empty range testing
> Why?  Since you use an A..A range below, you can just use any existing
> branch in that place, right?
>
>> +       git checkout -b empty-test topic1 &&
>> +       git rev-parse empty-test >empty-test-before &&
>> +
>> +       # Empty range should succeed but do nothing
>> +       git replay --onto main empty-test..empty-test >output &&
>> +       test_must_be_empty output &&
> Is A..A the only consideration?  Why would anyone ever pass that to
> the tool?  I would have thought that if someone made this mistake it
> was a B..A range where it wasn't obvious to the caller that B
> contained A.  I don't see why anyone would do A..A and then get
> surprised at the exit status; that feels like user error.  And in the
> B..A case, it's not clear to me that returning success without
> updating A is correct.  You might be giving the user false hope that
> the command did what they wanted.


Good point. I only considered A..A, but you are right that B..A (where B
contains A) is more likely and problematic. Silently succeeding could mask
a mistake in their range specification.

I will drop the entire empty range change from this series. The behavior
for empty ranges probably deserves its own focused discussion if there's
real demand for special handling


>
>> +       # Branch should be unchanged
>> +       git rev-parse empty-test >empty-test-after &&
>> +       test_cmp empty-test-before empty-test-after
>> +'
>> +
>>   test_done
>> --
>> 2.51.0


Thanks again for taking the time to provide such detailed feedback. This
is incredibly helpful.


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior
  2025-10-02 17:14   ` [PATCH v2 0/1] " Kristoffer Haugsbakk
@ 2025-10-02 23:36     ` Siddharth Asthana
  2025-10-03 19:05       ` Kristoffer Haugsbakk
  0 siblings, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-02 23:36 UTC (permalink / raw)
  To: Kristoffer Haugsbakk, git
  Cc: Junio C Hamano, Christian Couder, Patrick Steinhardt,
	Elijah Newren, Andrei Rybak, Karthik Nayak, Justin Tobler,
	Toon Claes, John Cai, Johannes Schindelin


On 02/10/25 22:44, Kristoffer Haugsbakk wrote:
> On Sat, Sep 27, 2025, at 01:08, Siddharth Asthana wrote:
>> This is v2 of the git-replay atomic updates series.
>>
>> Based on the extensive community feedback from v1, I've completely redesigned
>> the approach. Instead of adding new --update-refs options, this version makes
>> atomic ref updates the default behavior of git replay.
>>
>> Why this change makes sense:
>> - git replay is explicitly marked as EXPERIMENTAL with behavior changes
>> expected
>> - The command is primarily used server-side where atomic transactions
>> are crucial
>> - Current pipeline approach (git replay | git update-ref --stdin)
>> creates
>>    coordination complexity and lacks atomic guarantees by default
>> - Patrick Steinhardt noted performance issues with individual ref
>> updates
>>    in reftable backend
>> - Elijah Newren and Junio Hamano endorsed making the better behavior
>> default
>>
>> [snip]
> On the topic of changing experimental commands: I really like the
> git-for-each-ref(1) (git-FER) output format design.  It just outputs refs and
> related data.  It’s not a command for “bulk delete refs” or “check for
> merge conflicts between these refs and upstream (git-merge-tree(1)”—it
> just supports all of that through `--format` and its atoms.
>
> And for this command it seems to, at the core, output a mapping from old
> to new commits.
>
> Now, I’ve thought that a “client-side”[1] in-memory rebase-like command
> would need to support outputting data for the `post-rewrite` hook.  And
> is that not straightforward if you can use `--format` with `from` and
> `to` atoms?  (I ask because I have never called hooks with git-hook(1).)
>
> I just think that (naively maybe) a `--format` command like git-FER with
> all the quoting modes might be a good fit for this command.  Then you
> can compose all the steps you need yourself:
>
> 1. Call the exact git-update-ref(1) `--batch`/`--stdin` or whatever mode
>     you need
> 2. Write a message to each reflog if you want
> 3. Call the `post-rewrite` hook
>
> † 1: c.f. server-side which I get the impression only wants to do cheap
>       rebases


Hi Kristoffer,

That's an interesting perspective on using --format for composability,
similar to git-for-each-ref's design.

The constraint right now is that git replay's output needs to work
directly with update-ref --stdin, which has a specific format. Adding
--format would let users customize the output, but then they'd need to
transform it to the update-ref format anyway for the most common case,
which seems like extra work.

Your point about post-rewrite hook support is well-taken though. As this
command evolves toward client-side interactive rebase (which was Elijah's
original design goal), we will definitely need hook integration. At that
point, a --format approach with atoms like %(old) and %(new) could make
sense for letting users extract the commit mapping in whatever form they
need for hooks or other tooling.

For this iteration I am focusing on the simpler atomic update case, but 
I will
keep the --format idea in mind for future work. Do you see a specific use
case right now where --format would help, or is this more about
future-proofing the design for when we add client-side features?

Thanks for the thoughtful feedback!


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-02 18:27       ` Junio C Hamano
@ 2025-10-02 23:42         ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-02 23:42 UTC (permalink / raw)
  To: Junio C Hamano, Elijah Newren
  Cc: git, christian.couder, ps, code, rybak.a.v, karthik.188, jltobler,
	toon, johncai86, johannes.schindelin


On 02/10/25 23:57, Junio C Hamano wrote:
> Elijah Newren <newren@gmail.com> writes:
>
>>    * it provided a natural low-level tool for the suite of hash-object,
>> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users
>> to have another building block for experimentation and making new
>> tools.
>>
>> I was particularly focused on the last of those items for the intial
>> version at the time, but it should be noted that all three of these
>> are somewhat special cases, and the most common user desire is going
>> to be replaying commits and updating the references at the end.
> Yes.  We could even tweak the stream in the middle with "sed" or
> "grep", I presume? ;-)
>
>> Sure, but it's quite trivial to add, right -- as shown above with the
>> extra "start", "prepare", "commit" directives?
> Very true.
>
> Completely a tangent but, isn't requiring "prepare" at this layer,
> and possibly in the form of ref_transaction_prepare() at the C
> layer, not so ergonomic API design?  Once you "start" a transaction
> and threw a bunch of instruction, "commit" can notice that you are
> in a transaction and should do whatever necessary (including
> whatever "prepare" does).  I am not advocating to simplify the API
> by making end-user/program facing "prepare" a no-op, but just
> wondering why we decided to have "prepare" a so prominent API
> element.


For this patch, I am using the simpler pattern: 
ref_store_transaction_begin()
→ ref_transaction_update() → ref_transaction_commit(). Looking at
builtin/update-ref.c and other code, it seems commit() already handles
whatever prepare does internally when you are not using the explicit stdin
transaction commands.

Should I continue with that pattern, or is there a reason to use prepare()
explicitly even when not doing the stdin command flow?

On the config option you suggested in v1: I will add a config setting so
users can set their preference once. I am thinking either replay.updateRefs
(boolean) or replay.defaultOutput (string: "update"|"commands"). Any
preference on the naming pattern?


>
>> ...
>> Might I suggest a rewrite of the text of the commit message to this point?
> I do think it makes more sense to the reader to know the reasoning
> behind the _current_ design, and what its strengths are.


Thanks Junio. Elijah's rewritten structure is much clearer - I will use it
for v3. It properly explains the trade-offs without the false claims I made
about atomicity.


>
>> =====
>> The git replay command currently outputs update commands that can be
>> piped to update-ref to achieve a rebase, e.g.
>>
>>    git replay --onto main topic1..topic2 | git update-ref --stdin
>>
>> This separation had advantages for three special cases:
>>    * it made testing easy (when state isn't modified from one step to
>> the next, you don't need to make temporary branches or have undo
>> commands, or try to track the changes)
>>    * it provided a natural can-it-rebase-cleanly (and what would it
>> rebase to) capability without automatically updating refs, I guess
>> kind of like a --dry-run
>>    * it provided a natural low-level tool for the suite of hash-object,
>> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users
>> to have another building block for experimentation and making new
>> tools.
>>
>> However, it should be noted that all three of these are somewhat
>> special cases; users, whether on the client or server side, would
>> almost certainly find it more ergonomical to simply have the updating
>> of refs be the default.  Change the default behavior to update refs
>> directly, and atomically (at least to the extent supported by the refs
>> backend in use).
>> ====
> This reads very well.
>
>> Why is --allow-partial helpful?  You discussed at length why you
>> wanted atomic transactions, but you introduce this option with no
>> rationale and instead just discuss that you implemented it and some
>> design choices once you presuppose that someone wants to use it.
>>
>> Is there a usecase?  I asked for it last time, and suggested
>> discarding the modes without one, but you only discarded one of the
>> extras while leaving this one in.  I'd recommend discarding this one
>> too and just having the two modes -- the output commands that get fed
>> to update-ref, or the automatic transactional update of all or no
>> refs.
> I know there are people who like "best effort", but I too want to
> learn a concrete use case where the "best effort" mode, which
> updates only 3 refs among 30 that were to be updated, would give us
> a better result than "all or none" transaction that fails.


I don't have one. Elijah made the same point - I was trying to anticipate
needs without justification. I am removing --allow-partial from v3, keeping
just the two clear modes: atomic updates (default) or --output-commands
for the traditional pipeline.

Thanks!


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-02 22:55       ` Elijah Newren
@ 2025-10-03  7:05         ` Christian Couder
  0 siblings, 0 replies; 129+ messages in thread
From: Christian Couder @ 2025-10-03  7:05 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Siddharth Asthana, git, gitster, ps, code, rybak.a.v, karthik.188,
	jltobler, toon, johncai86, johannes.schindelin

On Fri, Oct 3, 2025 at 12:55 AM Elijah Newren <newren@gmail.com> wrote:
>
> Hi Christian,
>
> Excellent review, I just have one tangential question for you...
>
> On Tue, Sep 30, 2025 at 1:24 AM Christian Couder
> <christian.couder@gmail.com> wrote:
> >
> > On Sat, Sep 27, 2025 at 1:09 AM Siddharth Asthana
> > <siddharthasthana31@gmail.com> wrote:
> > >
> > > The git replay command currently outputs update commands that must be
> > > piped to git update-ref --stdin to actually update references:
> > >
> > >     git replay --onto main topic1..topic2 | git update-ref --stdin
> > >
> > > This design has significant limitations for server-side operations. The
> > > two-command pipeline creates coordination complexity, provides no atomic
> > > transaction guarantees by default, and complicates automation in bare
> > > repository environments where git replay is primarily used.
> >
> > Yeah, right.
>
> I'm unsure if you are expressing disbelief, or agreeing when you use
> this phrase.

I was agreeing with the general idea that having to pipe the output
into `git update-ref --stdin` to actually update references has
significant limitations (in particular for the server side use of the
command I am interested in).

I didn't check every point, especially the "provides no atomic
transaction guarantees by default", my bad.

> Most commonly when I see it, I assume the former (see
> https://dictionary.cambridge.org/us/dictionary/english/yeah-right and
> https://www.merriam-webster.com/dictionary/yeah for example), but I
> think you've consistently used this with the opposite connotation.  Am
> I correct on that?  (This is a particular phrase where tone of voice
> used would be really helpful, which doesn't get included in emails
> unfortunately.)

Yes, you are correct. I knew that it could be used to express
disbelief, but I thought that use was mostly a familiar oral one, and
the context would make it clear that I was agreeing. I will be more
careful when using it.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-02 22:16       ` Siddharth Asthana
@ 2025-10-03  7:30         ` Christian Couder
  0 siblings, 0 replies; 129+ messages in thread
From: Christian Couder @ 2025-10-03  7:30 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, gitster, ps, newren, code, rybak.a.v, karthik.188, jltobler,
	toon, johncai86, johannes.schindelin

Hi Siddharth,

On Fri, Oct 3, 2025 at 12:16 AM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:

> Thanks for the detailed commit message review. You are absolutely right - I
> was mixing the patch rationale with v1→v2 changelog, which belongs in the
> cover letter.
>
> Your suggested framing about considering an --atomic-update option but
> rejecting it in favor of making it default is much clearer than my
> approach. I will use that structure.
>
> For v3:
> - Move all "since v1" discussion to cover letter
> - Use imperative mood ("Let's change" not "This patch changes")
> - Be explicit that --output-commands and --allow-partial are new options
> - Add full stops to the implementation details list
> - Will add Helped-by trailers for Elijah, Patrick and you ofcourse as
> suggested.

Great, I am looking forward to v3.

> Quick question: for the C89 compliance mention, should I drop it entirely
> or briefly note "uses 'int' instead of 'bool' for C89 compatibility"? I
> want to acknowledge the bool→int change but not belabor it.

There are 2 ways to look at this.

1) If you think it's a significant design decision to not use the
'bool' type, you should talk about it in the commit message, saying
something like:

"Using the 'bool' type for X was considered but rejected because Y."

where you replace "X" by the reasons why it could have been used, and
"Y" by the reasons why that was rejected.

My opinion is that it's not a significant design decision but only a
minor one, so I think it's better and simpler to just not talk about
it in the commit message.

2) The other way to look at this is that it was a change from v1 to
v2. In this case it belongs to the cover letter in the section about
changes from v1 to v2 if any.

You don't necessarily need to include a section about changes from v1
to v2 in the cover letter for v3. Some do it, some don't. My opinion
is that it's not very often useful, and readers can relatively easily
refer to the cover letter for v2 (where it definitely should be) in
the rare cases they really want to see it. So I would suggest talking
only about the changes from v2 to v3 in the cover letter for v3.

To summarize, yeah, you can talk about it both in the commit message
and in the cover letter if you really want to, but my opinion is that
it's just not worth it.

Thanks for working on this!

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-02 23:27       ` Siddharth Asthana
@ 2025-10-03  7:59         ` Christian Couder
  2025-10-08 19:59           ` Siddharth Asthana
  2025-10-03 19:48         ` Elijah Newren
  1 sibling, 1 reply; 129+ messages in thread
From: Christian Couder @ 2025-10-03  7:59 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: Elijah Newren, git, gitster, ps, code, rybak.a.v, karthik.188,
	jltobler, toon, johncai86, johannes.schindelin

On Fri, Oct 3, 2025 at 1:27 AM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:

> >> For users needing the traditional pipeline workflow, --output-commands
> >> preserves the original behavior:
> >>
> >>      git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin
> > This is good.  Did you also add a config option so that someone can
> > just set that option once and use the old behavior?  (as per the
> > suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/
> > ?)
>
>
> I didn't, but I should have. I will add a config option for v3.

You don't need to add that configuration option in the main patch. I
would suggest adding it in a separate patch after the main one (which
changes the default behavior of the command).

Note that in the commit message of the main patch, it's nice to say
that a following commit will add a configuration option for users who
prefer the previous default behavior.

> For naming, I am thinking either:
>    - replay.updateRefs (boolean: true = update, false = output-commands)
>    - replay.defaultOutput (string: "update" | "commands")

If the command line option is called `--output-commands` then I would
suggest naming it "replay.outputCommands" and making it a boolean.

> >> @@ -330,9 +361,12 @@ int cmd_replay(int argc,
> >>                  usage_with_options(replay_usage, replay_options);
> >>          }
> >>
> >> -       if (advance_name_opt && contained)
> >> -               die(_("options '%s' and '%s' cannot be used together"),
> >> -                   "--advance", "--contained");
> >> +       die_for_incompatible_opt2(!!advance_name_opt, "--advance",
> >> +                                 contained, "--contained");
> > Broken indentation.  Also, should this have been done as a preparatory
> > cleanup patch?
>
>
> Good catches. I will fix the indentation.
>
> On making it a preparatory patch: should I split it out as a separate
> cleanup commit, or is it minor enough to fold into the main change? I am
> leaning toward folding it in since it's directly related to the option
> handling changes

If there is only this additional small cleanup change in the main
commit, and this small cleanup change is clearly mentioned in the
commit message as a "while at it small cleanup change", I think it's
OK.

If you find out that other additional small cleanup changes would be
nice too, then they should definitely all go into a preparatory patch
before the main patch.


> >> +
> >> +       /* Handle empty ranges: if no commits were processed, treat as success */
> >> +       if (!commits_processed)
> >> +               ret = 1; /* Success - no commits to replay is not an error */
> >> +       else
> >> +               ret = result.clean;
> > The change to treat empty ranges as success is an orthogonal change
> > that I think at a minimum belongs in a separate patch.  Out of
> > curiosity, how did you discover the exit status with an empty commit
> > range?  Why does someone specify such a range, and what form or forms
> > might it come in?  And is merely returning a successful result enough,
> > or is there more that needs to be done for correctness?
>
>
> I was thinking about automated scripts that compute ranges dynamically -
> they might generate A..B where it turns out A==B, and treating that as
> "no work needed, success" seemed reasonable for scripting.
>
> But you raise a good point: A..A seems like obvious user error (why would
> anyone do that intentionally?), and B..A where B contains A is likely a
> mistake that maybe should error rather than silently succeed.
>
> I am inclined to drop it entirely from this series. If there's real demand
> for specific empty-range handling, we can add it later with proper
> discussion of the actual use cases. Does that sound reasonable?

Yeah, I think dropping it from this series is fine.

What happens in those cases should be documented if it isn't already
though. Those documentation changes should probably be in a separate
patch.

Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior
  2025-10-02 23:36     ` Siddharth Asthana
@ 2025-10-03 19:05       ` Kristoffer Haugsbakk
  2025-10-08 20:02         ` Siddharth Asthana
  0 siblings, 1 reply; 129+ messages in thread
From: Kristoffer Haugsbakk @ 2025-10-03 19:05 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: Junio C Hamano, Christian Couder, Patrick Steinhardt,
	Elijah Newren, Andrei Rybak, Karthik Nayak, Justin Tobler,
	Toon Claes, John Cai, Johannes Schindelin

Good evening Siddharth

On Fri, Oct 3, 2025, at 01:36, Siddharth Asthana wrote:
> On 02/10/25 22:44, Kristoffer Haugsbakk wrote:
>>> [snip]
>> On the topic of changing experimental commands: I really like the
>> git-for-each-ref(1) (git-FER) output format design.  It just outputs refs and
>> related data.  It’s not a command for “bulk delete refs” or “check for
>> merge conflicts between these refs and upstream (git-merge-tree(1)”—it
>> just supports all of that through `--format` and its atoms.
>>
>> And for this command it seems to, at the core, output a mapping from old
>> to new commits.
>>
>> Now, I’ve thought that a “client-side”[1] in-memory rebase-like command
>> would need to support outputting data for the `post-rewrite` hook.  And
>> is that not straightforward if you can use `--format` with `from` and
>> `to` atoms?  (I ask because I have never called hooks with git-hook(1).)
>>
>> I just think that (naively maybe) a `--format` command like git-FER with
>> all the quoting modes might be a good fit for this command.  Then you
>> can compose all the steps you need yourself:
>>
>> 1. Call the exact git-update-ref(1) `--batch`/`--stdin` or whatever mode
>>     you need
>> 2. Write a message to each reflog if you want
>> 3. Call the `post-rewrite` hook
>>
>> † 1: c.f. server-side which I get the impression only wants to do cheap
>>       rebases
>
>
> Hi Kristoffer,
>
> That's an interesting perspective on using --format for composability,
> similar to git-for-each-ref's design.
>
> The constraint right now is that git replay's output needs to work
> directly with update-ref --stdin, which has a specific format. Adding
> --format would let users customize the output, but then they'd need to
> transform it to the update-ref format anyway for the most common case,
> which seems like extra work.

git-FER has a default format and could still use that (either the
current one or your proposal).

git-replay(1) could also concievably support ready-made formats, similar
to “pretty” formats that git-log(1) & co.

> Your point about post-rewrite hook support is well-taken though. As this
> command evolves toward client-side interactive rebase (which was Elijah's
> original design goal), we will definitely need hook integration. At that
> point, a --format approach with atoms like %(old) and %(new) could make
> sense for letting users extract the commit mapping in whatever form they
> need for hooks or other tooling.
>
> For this iteration I am focusing on the simpler atomic update case, but
> I will
> keep the --format idea in mind for future work.
>
> [replying to this part
>
> Do you see a specific use case right now where --format would help, or
> is this more about future-proofing the design for when we add
> client-side features?

I have been using git-rebase(1) for a while with a post-rewrite script.
This is used for interactive rebases but also just keeping up with
upstream, i.e. a regular rebase.  Then I was idly thinking that
git-replay(1) would be faster for the plain rebase case—but it doesn’t
support that hook directly.  Okay, but I can get around that: I can
parse the output, yank the commit OIDs, and run git-rev-list(1) on both
of them to get the mapping I want.  But it would be really nice to just
declare the correct post-rewrite format and be done, without having to
parse anything. :)

Beyond that though I’ve been thinking about more hypothetical “client-
side” concerns.  I mentioned writing to the reflog.  I imagine that
server programs that just want to be able to efficiently “rebase”
branches to the upstream don’t need that.  But client-side programs
might want to write to the reflog because they want to mark what the
update is for; you could have many kinds of client-side “update ref”
programs and want to leave breadcrumbs about what was done.  There is
more experimentation.  Whereas I imagine that a forge has maybe a small
set of “update branch” commands.  I don’t know, maybe I’m rambling at
this point.

> Thanks for the thoughtful feedback!

Thanks for the consideration and reply!

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-02 23:27       ` Siddharth Asthana
  2025-10-03  7:59         ` Christian Couder
@ 2025-10-03 19:48         ` Elijah Newren
  2025-10-03 20:32           ` Junio C Hamano
  2025-10-08 20:05           ` Siddharth Asthana
  1 sibling, 2 replies; 129+ messages in thread
From: Elijah Newren @ 2025-10-03 19:48 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, gitster, christian.couder, ps, code, rybak.a.v, karthik.188,
	jltobler, toon, johncai86, johannes.schindelin

On Thu, Oct 2, 2025 at 4:27 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> >> For users needing the traditional pipeline workflow, --output-commands
> >> preserves the original behavior:
> >>
> >>      git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin
> > This is good.  Did you also add a config option so that someone can
> > just set that option once and use the old behavior?  (as per the
> > suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/
> > ?)
>
>
> I didn't, but I should have. I will add a config option for v3.
>
> For naming, I am thinking either:
>    - replay.updateRefs (boolean: true = update, false = output-commands)
>    - replay.defaultOutput (string: "update" | "commands")
>
> The boolean feels simpler, but the string might be more extensible if we
> add other output modes later. Which pattern feels more consistent with
> existing Git config conventions? Looking at rebase.* they're mostly
> boolean toggles, but am I missing a better example to follow?

replay.updateRefs sounds better to me.  defaultOutput with "update"
doesn't make sense to me.

> You are right - I don't have a concrete use case. I was trying to
> anticipate potential needs but ended up adding unjustified complexity.
>
> I will remove --allow-partial entirely from v3. This simplifies to exactly
> two modes with clear purposes:
>    1. Default: atomic ref updates (all-or-nothing)
>    2. --output-commands: traditional pipeline for special cases
>
> Much cleaner design.

Note that once you add a config option, you'll also need an additional
command line flag (or make it possible to invert an existing one), so
that users can override the config and get the default behavior.
Maybe --[no-]update-refs would make sense after all, where
--update-refs is the default and --no-update-refs is your current
--output-commands?

(I know you all talked elsewhere in this thread about "avoiding a name
collision" with rebase, but I don't quite see it as a collision.  When
Stolee suggested the flag for rebase, I pointed out it's roughly what
I'm doing in replay, so it doesn't feel like a conflict to me.  I'm
also open to an alternative flag name if it makes sense, but we
probably want whatever the command line flag is to be similar to the
config name and "defaultOutput"/--default-output don't make sense as a
name to me.)

> >> @@ -330,9 +361,12 @@ int cmd_replay(int argc,
> >>                  usage_with_options(replay_usage, replay_options);
> >>          }
> >>
> >> -       if (advance_name_opt && contained)
> >> -               die(_("options '%s' and '%s' cannot be used together"),
> >> -                   "--advance", "--contained");
> >> +       die_for_incompatible_opt2(!!advance_name_opt, "--advance",
> >> +                                 contained, "--contained");
> > Broken indentation.  Also, should this have been done as a preparatory
> > cleanup patch?
>
>
> Good catches. I will fix the indentation.
>
> On making it a preparatory patch: should I split it out as a separate
> cleanup commit, or is it minor enough to fold into the main change? I am
> leaning toward folding it in since it's directly related to the option
> handling changes

Given that it was directly adjacent to the other
die_for_incompatible_opt2() call, if that were still the case, I could
see making it part of the same commit.  However, dropping the
--allow-partial flag means you don't need to add that other call
anymore, so it makes this remaining die_for_incompataible_opt2() call
an entirely orthogonal change to the rest of your patch.  As such, I
think it belongs in a separate patch; it could either be a preparatory
patch or a follow-up.

> >> @@ -407,6 +452,8 @@ int cmd_replay(int argc,
> >>                  khint_t pos;
> >>                  int hr;
> >>
> >> +               commits_processed = 1;
> >> +
> >>                  if (!commit->parents)
> >>                          die(_("replaying down to root commit is not supported yet!"));
> >>                  if (commit->parents->next)
> >> @@ -457,9 +535,17 @@ int cmd_replay(int argc,
> >>                  strset_clear(update_refs);
> >>                  free(update_refs);
> >>          }
> >> -       ret = result.clean;
> >> +
> >> +       /* Handle empty ranges: if no commits were processed, treat as success */
> >> +       if (!commits_processed)
> >> +               ret = 1; /* Success - no commits to replay is not an error */
> >> +       else
> >> +               ret = result.clean;
> > The change to treat empty ranges as success is an orthogonal change
> > that I think at a minimum belongs in a separate patch.  Out of
> > curiosity, how did you discover the exit status with an empty commit
> > range?  Why does someone specify such a range, and what form or forms
> > might it come in?  And is merely returning a successful result enough,
> > or is there more that needs to be done for correctness?
>
>
> I was thinking about automated scripts that compute ranges dynamically -
> they might generate A..B where it turns out A==B, and treating that as
> "no work needed, success" seemed reasonable for scripting.
>
> But you raise a good point: A..A seems like obvious user error (why would
> anyone do that intentionally?), and B..A where B contains A is likely a
> mistake that maybe should error rather than silently succeed.
>
> I am inclined to drop it entirely from this series. If there's real demand
> for specific empty-range handling, we can add it later with proper
> discussion of the actual use cases. Does that sound reasonable?

Yep, dropping it makes sense to me.  Alternatively, documenting what
happens in the case of empty ranges, as Christian suggests, also makes
sense to me though I might suggest that it be done in an entirely
separate series rather than just a separate patch of this series.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-03 19:48         ` Elijah Newren
@ 2025-10-03 20:32           ` Junio C Hamano
  2025-10-08 20:06             ` Siddharth Asthana
  2025-10-08 20:05           ` Siddharth Asthana
  1 sibling, 1 reply; 129+ messages in thread
From: Junio C Hamano @ 2025-10-03 20:32 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Siddharth Asthana, git, christian.couder, ps, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin

Elijah Newren <newren@gmail.com> writes:

>> For naming, I am thinking either:
>>    - replay.updateRefs (boolean: true = update, false = output-commands)
>>    - replay.defaultOutput (string: "update" | "commands")
>>
>> The boolean feels simpler, but the string might be more extensible if we
>> add other output modes later. Which pattern feels more consistent with
>> existing Git config conventions? Looking at rebase.* they're mostly
>> boolean toggles, but am I missing a better example to follow?
>
> replay.updateRefs sounds better to me.  defaultOutput with "update"
> doesn't make sense to me.

Yup.  Or "replay.defaultAction = (update-ref | show-comamnds)" if we
anticipate that we might have a third option someday.  That would of
course affect the choice of the command line option.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-02 22:20       ` Siddharth Asthana
@ 2025-10-08 14:01         ` Phillip Wood
  2025-10-08 20:09           ` Siddharth Asthana
  0 siblings, 1 reply; 129+ messages in thread
From: Phillip Wood @ 2025-10-08 14:01 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: gitster, christian.couder, ps, newren, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin

Hi Siddharth

On 02/10/2025 23:20, Siddharth Asthana wrote:
> On 30/09/25 15:35, Phillip Wood wrote:
>> On 27/09/2025 00:08, Siddharth Asthana wrote:
>>> The git replay command currently outputs update commands that must be
>>> piped to git update-ref --stdin to actually update references:
> 
> The actual advantages of the new default aren't about atomicity (that
> already exists), but rather:
> - Eliminating the pipeline for the common case
> - Better ergonomics for users who just want refs updated
> - Simpler server-side automation
> 
> I will rewrite the commit message to accurately reflect this. Elijah
> provided a good suggested structure that captures the real trade-offs
> without false claims.

That's great. I agree that having replay update the refs itself is a 
useful improvement.

>>> +--allow-partial::
>>> +    Allow some ref updates to succeed even if others fail. By default,
>>> +    ref updates are atomic (all succeed or all fail). With this option,
>>> +    failed updates are reported as warnings rather than causing the 
>>> entire
>>> +    command to fail. The command exits with code 0 only if all updates
>>> +    succeed; any failures result in exit code 1. Cannot be used with
>>> +    `--output-commands`.
>>
>> Rather than having two incompatible options perhaps we could have a 
>> single "--update-refs=(yes|print|allow-partial-updates)" argument. I 
>> think the name "--allow-partial" is rather ambiguous as it does not 
>> say what it is allowing to be partial.
> 
> After thinking about this and Elijah's feedback, I am leaning toward
> dropping --allow-partial entirely since I don't have a concrete use case
> for it. That simplifies things to just: default atomic updates vs
> --output-commands for the traditional pipeline.
> 
> Would you still prefer a --update-refs=<mode> style, or is the simpler
> --output-commands flag sufficient given that --allow-partial is going away?

The advantage of --update-refs=<mode> is that it allows for future 
extensions such as adding support for partial in a way that does not 
add conflicting options.

Thanks

Phillip
  >
>>
>>> +static int add_ref_to_transaction(struct ref_transaction *transaction,
>>> +                  const char *refname,
>>> +                  const struct object_id *new_oid,
>>> +                  const struct object_id *old_oid,
>>> +                  struct strbuf *err)
>>> +{
>>> +    return ref_transaction_update(transaction, refname, new_oid, 
>>> old_oid,
>>> +                      NULL, NULL, 0, "git replay", err);
>>> +}
>>
>> I'm not sure this function adds much value. I think it would be better 
>> to instead have a helper function that updates refs or prints the ref 
>> updates so that we do not duplicate that code in the two places below.
> 
> 
> ood point. I will extract a helper like:
> 
>      static int handle_ref_update(int output_commands,
>                                   struct ref_transaction *transaction,
>                                   const char *refname,
>                                   const struct object_id *new_oid,
>                                   const struct object_id *old_oid,
>                                   struct strbuf *err)
> 
> This eliminates the duplication and fixes the over-long lines you pointed
> out at both call sites.
> 
> Thanks!
> 
> 
>>
>>> @@ -434,10 +481,18 @@ int cmd_replay(int argc,
>>>               if (decoration->type == DECORATION_REF_LOCAL &&
>>>                   (contained || strset_contains(update_refs,
>>>                                 decoration->name))) {
>>> -                printf("update %s %s %s\n",
>>> -                       decoration->name,
>>> - oid_to_hex(&last_commit->object.oid),
>>> -                       oid_to_hex(&commit->object.oid));
>>> +                if (output_commands) {
>>> +                    printf("update %s %s %s\n",
>>> +                           decoration->name,
>>> + oid_to_hex(&last_commit->object.oid),
>>> + oid_to_hex(&commit->object.oid));
>>> +                } else if (add_ref_to_transaction(transaction, 
>>> decoration->name,
>>> + &last_commit->object.oid,
>>> +                                  &commit->object.oid,
>>> +                                  &transaction_err) < 0) {
>>> +                    ret = error(_("failed to add ref update to 
>>> transaction: %s"), transaction_err.buf);
>>> +                    goto cleanup;
>>> +                }
>>>               }
>>
>> The lines here are very long due to the indentation, having a separate 
>> function to update the refs or print the ref updates would be much 
>> more readable.
>>
>>>               decoration = decoration->next;
>>>           }
>>> @@ -445,10 +500,33 @@ int cmd_replay(int argc,
>>>         /* In --advance mode, advance the target ref */
>>>       if (result.clean == 1 && advance_name) {
>>> -        printf("update %s %s %s\n",
>>> -               advance_name,
>>> -               oid_to_hex(&last_commit->object.oid),
>>> -               oid_to_hex(&onto->object.oid));
>>> +        if (output_commands) {
>>> +            printf("update %s %s %s\n",
>>> +                   advance_name,
>>> +                   oid_to_hex(&last_commit->object.oid),
>>> +                   oid_to_hex(&onto->object.oid));
>>> +        } else if (add_ref_to_transaction(transaction, advance_name,
>>> +                          &last_commit->object.oid,
>>> +                          &onto->object.oid,
>>> +                          &transaction_err) < 0) {
>>> +            ret = error(_("failed to add ref update to transaction: 
>>> %s"), transaction_err.buf);
>>> +            goto cleanup;
>>> +        }
>>> +    }
>>
>> Putting the code to update the refs or print the ref updates into a 
>> single function would avoid this duplication and over-long lines.
>>
>> Thanks
>>
>> Phillip
>>
>>> +    /* Commit the ref transaction if we have one */
>>> +    if (transaction && result.clean == 1) {
>>> +        if (ref_transaction_commit(transaction, &transaction_err)) {
>>> +            if (allow_partial) {
>>> +                warning(_("some ref updates failed: %s"), 
>>> transaction_err.buf);
>>> + ref_transaction_for_each_rejected_update(transaction,
>>> +                                     print_rejected_update, NULL);
>>> +                ret = 0; /* Set failure even with allow_partial */
>>> +            } else {
>>> +                ret = error(_("failed to update refs: %s"), 
>>> transaction_err.buf);
>>> +                goto cleanup;
>>> +            }
>>> +        }
>>>       }
>>>         merge_finalize(&merge_opt, &result);
>>> @@ -457,9 +535,17 @@ int cmd_replay(int argc,
>>>           strset_clear(update_refs);
>>>           free(update_refs);
>>>       }
>>> -    ret = result.clean;
>>> +
>>> +    /* Handle empty ranges: if no commits were processed, treat as 
>>> success */
>>> +    if (!commits_processed)
>>> +        ret = 1; /* Success - no commits to replay is not an error */
>>> +    else
>>> +        ret = result.clean;
>>>     cleanup:
>>> +    if (transaction)
>>> +        ref_transaction_free(transaction);
>>> +    strbuf_release(&transaction_err);
>>>       release_revisions(&revs);
>>>       free(advance_name);
>>>   diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>>> index 58b3759935..8b4301e227 100755
>>> --- a/t/t3650-replay-basics.sh
>>> +++ b/t/t3650-replay-basics.sh
>>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
>>>   '
>>>     test_expect_success 'using replay to rebase two branches, one on 
>>> top of other' '
>>> -    git replay --onto main topic1..topic2 >result &&
>>> +    git replay --output-commands --onto main topic1..topic2 >result &&
>>>         test_line_count = 1 result &&
>>>   @@ -67,9 +67,30 @@ test_expect_success 'using replay to rebase two 
>>> branches, one on top of other' '
>>>       test_cmp expect result
>>>   '
>>>   +test_expect_success 'using replay with default atomic behavior (no 
>>> output)' '
>>> +    # Create a test branch that wont interfere with others
>>> +    git branch atomic-test topic2 &&
>>> +    git rev-parse atomic-test >atomic-test-old &&
>>> +
>>> +    # Default behavior: atomic ref updates (no output)
>>> +    git replay --onto main topic1..atomic-test >output &&
>>> +    test_must_be_empty output &&
>>> +
>>> +    # Verify the branch was updated
>>> +    git rev-parse atomic-test >atomic-test-new &&
>>> +    ! test_cmp atomic-test-old atomic-test-new &&
>>> +
>>> +    # Verify the history is correct
>>> +    git log --format=%s atomic-test >actual &&
>>> +    test_write_lines E D M L B A >expect &&
>>> +    test_cmp expect actual
>>> +'
>>> +
>>>   test_expect_success 'using replay on bare repo to rebase two 
>>> branches, one on top of other' '
>>> -    git -C bare replay --onto main topic1..topic2 >result-bare &&
>>> -    test_cmp expect result-bare
>>> +    git -C bare replay --output-commands --onto main topic1..topic2 
>>> >result-bare &&
>>> +
>>> +    # The result should match what we got from the regular repo
>>> +    test_cmp result result-bare
>>>   '
>>>     test_expect_success 'using replay to rebase with a conflict' '
>>> @@ -86,7 +107,7 @@ test_expect_success 'using replay to perform basic 
>>> cherry-pick' '
>>>       # 2nd field of result is refs/heads/main vs. refs/heads/topic2
>>>       # 4th field of result is hash for main instead of hash for topic2
>>>   -    git replay --advance main topic1..topic2 >result &&
>>> +    git replay --output-commands --advance main topic1..topic2 
>>> >result &&
>>>         test_line_count = 1 result &&
>>>   @@ -102,7 +123,7 @@ test_expect_success 'using replay to perform 
>>> basic cherry-pick' '
>>>   '
>>>     test_expect_success 'using replay on bare repo to perform basic 
>>> cherry-pick' '
>>> -    git -C bare replay --advance main topic1..topic2 >result-bare &&
>>> +    git -C bare replay --output-commands --advance main 
>>> topic1..topic2 >result-bare &&
>>>       test_cmp expect result-bare
>>>   '
>>>   @@ -115,7 +136,7 @@ test_expect_success 'replay fails when both -- 
>>> advance and --onto are omitted' '
>>>   '
>>>     test_expect_success 'using replay to also rebase a contained 
>>> branch' '
>>> -    git replay --contained --onto main main..topic3 >result &&
>>> +    git replay --output-commands --contained --onto main 
>>> main..topic3 >result &&
>>>         test_line_count = 2 result &&
>>>       cut -f 3 -d " " result >new-branch-tips &&
>>> @@ -139,12 +160,12 @@ test_expect_success 'using replay to also 
>>> rebase a contained branch' '
>>>   '
>>>     test_expect_success 'using replay on bare repo to also rebase a 
>>> contained branch' '
>>> -    git -C bare replay --contained --onto main main..topic3 >result- 
>>> bare &&
>>> +    git -C bare replay --output-commands --contained --onto main 
>>> main..topic3 >result-bare &&
>>>       test_cmp expect result-bare
>>>   '
>>>     test_expect_success 'using replay to rebase multiple divergent 
>>> branches' '
>>> -    git replay --onto main ^topic1 topic2 topic4 >result &&
>>> +    git replay --output-commands --onto main ^topic1 topic2 topic4 
>>> >result &&
>>>         test_line_count = 2 result &&
>>>       cut -f 3 -d " " result >new-branch-tips &&
>>> @@ -168,7 +189,7 @@ test_expect_success 'using replay to rebase 
>>> multiple divergent branches' '
>>>   '
>>>     test_expect_success 'using replay on bare repo to rebase multiple 
>>> divergent branches, including contained ones' '
>>> -    git -C bare replay --contained --onto main ^main topic2 topic3 
>>> topic4 >result &&
>>> +    git -C bare replay --output-commands --contained --onto main 
>>> ^main topic2 topic3 topic4 >result &&
>>>         test_line_count = 4 result &&
>>>       cut -f 3 -d " " result >new-branch-tips &&
>>> @@ -217,4 +238,131 @@ test_expect_success 
>>> 'merge.directoryRenames=false' '
>>>           --onto rename-onto rename-onto..rename-from
>>>   '
>>>   +# Tests for new default atomic behavior and options> > 
>>> +test_expect_success 'replay default behavior should not produce 
>> output when successful' '
>>> +    git replay --onto main topic1..topic3 >output &&
>>> +    test_must_be_empty output
>>> +'
>>> +
>>> +test_expect_success 'replay with --output-commands produces 
>>> traditional output' '
>>> +    git replay --output-commands --onto main topic1..topic3 >output &&
>>> +    test_line_count = 1 output &&
>>> +    grep "^update refs/heads/topic3 " output
>>> +'
>>> +
>>> +test_expect_success 'replay with --allow-partial should not produce 
>>> output when successful' '
>>> +    git replay --allow-partial --onto main topic1..topic3 >output &&
>>> +    test_must_be_empty output
>>> +'
>>> +
>>> +test_expect_success 'replay fails when --output-commands and -- 
>>> allow-partial are used together' '
>>> +    test_must_fail git replay --output-commands --allow-partial -- 
>>> onto main topic1..topic2 2>error &&
>>> +    grep "cannot be used together" error
>>> +'
>>> +
>>> +test_expect_success 'replay with --contained updates multiple 
>>> branches atomically' '
>>> +    # Create fresh test branches based on the original structure
>>> +    # contained-topic1 should be contained within the range to 
>>> contained-topic3
>>> +    git branch contained-base main &&
>>> +    git checkout -b contained-topic1 contained-base &&
>>> +    test_commit ContainedC &&
>>> +    git checkout -b contained-topic3 contained-topic1 &&
>>> +    test_commit ContainedG &&
>>> +    test_commit ContainedH &&
>>> +    git checkout main &&
>>> +
>>> +    # Store original states
>>> +    git rev-parse contained-topic1 >contained-topic1-old &&
>>> +    git rev-parse contained-topic3 >contained-topic3-old &&
>>> +
>>> +    # Use --contained to update multiple branches - this should 
>>> update both
>>> +    git replay --contained --onto main contained-base..contained- 
>>> topic3 &&
>>> +
>>> +    # Verify both branches were updated
>>> +    git rev-parse contained-topic1 >contained-topic1-new &&
>>> +    git rev-parse contained-topic3 >contained-topic3-new &&
>>> +    ! test_cmp contained-topic1-old contained-topic1-new &&
>>> +    ! test_cmp contained-topic3-old contained-topic3-new
>>> +'
>>> +
>>> +test_expect_success 'replay atomic behavior: all refs updated or 
>>> none' '
>>> +    # Store original state
>>> +    git rev-parse topic4 >topic4-old &&
>>> +
>>> +    # Default atomic behavior
>>> +    git replay --onto main main..topic4 &&
>>> +
>>> +    # Verify ref was updated
>>> +    git rev-parse topic4 >topic4-new &&
>>> +    ! test_cmp topic4-old topic4-new &&
>>> +
>>> +    # Verify no partial state
>>> +    git log --format=%s topic4 >actual &&
>>> +    test_write_lines J I M L B A >expect &&
>>> +    test_cmp expect actual
>>> +'
>>> +
>>> +test_expect_success 'replay works correctly with bare repositories' '
>>> +    # Test atomic behavior in bare repo (important for Gitaly)
>>> +    git checkout -b bare-test topic1 &&
>>> +    test_commit BareTest &&
>>> +
>>> +    # Test with bare repo - replay the commits from main..bare-test 
>>> to get the full history
>>> +    git -C bare fetch .. bare-test:bare-test &&
>>> +    git -C bare replay --onto main main..bare-test &&
>>> +
>>> +    # Verify the bare repo was updated correctly (no output)
>>> +    git -C bare log --format=%s bare-test >actual &&
>>> +    test_write_lines BareTest F C M L B A >expect &&
>>> +    test_cmp expect actual
>>> +'
>>> +
>>> +test_expect_success 'replay --allow-partial with no failures 
>>> produces no output' '
>>> +    git checkout -b partial-test topic1 &&
>>> +    test_commit PartialTest &&
>>> +
>>> +    # Should succeed silently even with partial mode
>>> +    git replay --allow-partial --onto main topic1..partial-test 
>>> >output &&
>>> +    test_must_be_empty output
>>> +'
>>> +
>>> +test_expect_success 'replay maintains ref update consistency' '
>>> +    # Test that traditional vs atomic produce equivalent results
>>> +    git checkout -b method1-test topic2 &&
>>> +    git checkout -b method2-test topic2 &&
>>> +
>>> +    # Both methods should update refs to point to the same replayed 
>>> commits
>>> +    git replay --output-commands --onto main topic1..method1-test 
>>> >update-commands &&
>>> +    git update-ref --stdin <update-commands &&
>>> +    git log --format=%s method1-test >traditional-result &&
>>> +
>>> +    # Direct atomic method should produce same commit history
>>> +    git replay --onto main topic1..method2-test &&
>>> +    git log --format=%s method2-test >atomic-result &&
>>> +
>>> +    # Both methods should produce identical commit histories
>>> +    test_cmp traditional-result atomic-result
>>> +'
>>> +
>>> +test_expect_success 'replay error messages are helpful and clear' '
>>> +    # Test that error messages are clear
>>> +    test_must_fail git replay --output-commands --allow-partial -- 
>>> onto main topic1..topic2 2>error &&
>>> +    grep "cannot be used together" error
>>> +'
>>> +
>>> +test_expect_success 'replay with empty range produces no output and 
>>> no changes' '
>>> +    # Create a test branch for empty range testing
>>> +    git checkout -b empty-test topic1 &&
>>> +    git rev-parse empty-test >empty-test-before &&
>>> +
>>> +    # Empty range should succeed but do nothing
>>> +    git replay --onto main empty-test..empty-test >output &&
>>> +    test_must_be_empty output &&
>>> +
>>> +    # Branch should be unchanged
>>> +    git rev-parse empty-test >empty-test-after &&
>>> +    test_cmp empty-test-before empty-test-after
>>> +'
>>> +
>>>   test_done
>>


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-03  7:59         ` Christian Couder
@ 2025-10-08 19:59           ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-08 19:59 UTC (permalink / raw)
  To: Christian Couder
  Cc: Elijah Newren, git, gitster, ps, code, rybak.a.v, karthik.188,
	jltobler, toon, johncai86, johannes.schindelin


On 03/10/25 13:29, Christian Couder wrote:
> On Fri, Oct 3, 2025 at 1:27 AM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>
>>>> For users needing the traditional pipeline workflow, --output-commands
>>>> preserves the original behavior:
>>>>
>>>>       git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin
>>> This is good.  Did you also add a config option so that someone can
>>> just set that option once and use the old behavior?  (as per the
>>> suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/
>>> ?)
>>
>> I didn't, but I should have. I will add a config option for v3.
> You don't need to add that configuration option in the main patch. I
> would suggest adding it in a separate patch after the main one (which
> changes the default behavior of the command).
>
> Note that in the commit message of the main patch, it's nice to say
> that a following commit will add a configuration option for users who
> prefer the previous default behavior.
>
>> For naming, I am thinking either:
>>     - replay.updateRefs (boolean: true = update, false = output-commands)
>>     - replay.defaultOutput (string: "update" | "commands")
> If the command line option is called `--output-commands` then I would
> suggest naming it "replay.outputCommands" and making it a boolean.


That makes sense - replay.outputCommands matches the command line option
name directly. Much clearer than my replay.defaultOutput idea.

So the pattern would be:
- replay.outputCommands = false (default): atomic ref updates
- replay.outputCommands = true: traditional pipeline output

I will implement this in a separate patch after the main one, as you 
suggested.


>
>>>> @@ -330,9 +361,12 @@ int cmd_replay(int argc,
>>>>                   usage_with_options(replay_usage, replay_options);
>>>>           }
>>>>
>>>> -       if (advance_name_opt && contained)
>>>> -               die(_("options '%s' and '%s' cannot be used together"),
>>>> -                   "--advance", "--contained");
>>>> +       die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>>>> +                                 contained, "--contained");
>>> Broken indentation.  Also, should this have been done as a preparatory
>>> cleanup patch?
>>
>> Good catches. I will fix the indentation.
>>
>> On making it a preparatory patch: should I split it out as a separate
>> cleanup commit, or is it minor enough to fold into the main change? I am
>> leaning toward folding it in since it's directly related to the option
>> handling changes
> If there is only this additional small cleanup change in the main
> commit, and this small cleanup change is clearly mentioned in the
> commit message as a "while at it small cleanup change", I think it's
> OK.


Got it. Since it's just the one die_for_incompatible_opt2() change and
it's directly related to option handling, I will fold it into the main 
patch
with a "while at it" note in the commit message.


>
> If you find out that other additional small cleanup changes would be
> nice too, then they should definitely all go into a preparatory patch
> before the main patch.
>
>
>>>> +
>>>> +       /* Handle empty ranges: if no commits were processed, treat as success */
>>>> +       if (!commits_processed)
>>>> +               ret = 1; /* Success - no commits to replay is not an error */
>>>> +       else
>>>> +               ret = result.clean;
>>> The change to treat empty ranges as success is an orthogonal change
>>> that I think at a minimum belongs in a separate patch.  Out of
>>> curiosity, how did you discover the exit status with an empty commit
>>> range?  Why does someone specify such a range, and what form or forms
>>> might it come in?  And is merely returning a successful result enough,
>>> or is there more that needs to be done for correctness?
>>
>> I was thinking about automated scripts that compute ranges dynamically -
>> they might generate A..B where it turns out A==B, and treating that as
>> "no work needed, success" seemed reasonable for scripting.
>>
>> But you raise a good point: A..A seems like obvious user error (why would
>> anyone do that intentionally?), and B..A where B contains A is likely a
>> mistake that maybe should error rather than silently succeed.
>>
>> I am inclined to drop it entirely from this series. If there's real demand
>> for specific empty-range handling, we can add it later with proper
>> discussion of the actual use cases. Does that sound reasonable?
> Yeah, I think dropping it from this series is fine.


Thanks Christian. I will drop the empty range handling from this series.

On documenting the current behavior for empty ranges: should that go in
this series or separately? If the current behavior is just "returns
failure for empty ranges", maybe a simple doc note is enough. But if we
want to discuss what the behavior *should* be, that probably deserves its
own focused series.

What do you think?


>
> What happens in those cases should be documented if it isn't already
> though. Those documentation changes should probably be in a separate
> patch.
>
> Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior
  2025-10-03 19:05       ` Kristoffer Haugsbakk
@ 2025-10-08 20:02         ` Siddharth Asthana
  2025-10-08 20:56           ` Elijah Newren
  0 siblings, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-08 20:02 UTC (permalink / raw)
  To: Kristoffer Haugsbakk, git
  Cc: Junio C Hamano, Christian Couder, Patrick Steinhardt,
	Elijah Newren, Andrei Rybak, Karthik Nayak, Justin Tobler,
	Toon Claes, John Cai, Johannes Schindelin


On 04/10/25 00:35, Kristoffer Haugsbakk wrote:
> Good evening Siddharth
>
> On Fri, Oct 3, 2025, at 01:36, Siddharth Asthana wrote:
>> On 02/10/25 22:44, Kristoffer Haugsbakk wrote:
>>>> [snip]
>>> On the topic of changing experimental commands: I really like the
>>> git-for-each-ref(1) (git-FER) output format design.  It just outputs refs and
>>> related data.  It’s not a command for “bulk delete refs” or “check for
>>> merge conflicts between these refs and upstream (git-merge-tree(1)”—it
>>> just supports all of that through `--format` and its atoms.
>>>
>>> And for this command it seems to, at the core, output a mapping from old
>>> to new commits.
>>>
>>> Now, I’ve thought that a “client-side”[1] in-memory rebase-like command
>>> would need to support outputting data for the `post-rewrite` hook.  And
>>> is that not straightforward if you can use `--format` with `from` and
>>> `to` atoms?  (I ask because I have never called hooks with git-hook(1).)
>>>
>>> I just think that (naively maybe) a `--format` command like git-FER with
>>> all the quoting modes might be a good fit for this command.  Then you
>>> can compose all the steps you need yourself:
>>>
>>> 1. Call the exact git-update-ref(1) `--batch`/`--stdin` or whatever mode
>>>      you need
>>> 2. Write a message to each reflog if you want
>>> 3. Call the `post-rewrite` hook
>>>
>>> † 1: c.f. server-side which I get the impression only wants to do cheap
>>>        rebases
>>
>> Hi Kristoffer,
>>
>> That's an interesting perspective on using --format for composability,
>> similar to git-for-each-ref's design.
>>
>> The constraint right now is that git replay's output needs to work
>> directly with update-ref --stdin, which has a specific format. Adding
>> --format would let users customize the output, but then they'd need to
>> transform it to the update-ref format anyway for the most common case,
>> which seems like extra work.
> git-FER has a default format and could still use that (either the
> current one or your proposal).
>
> git-replay(1) could also concievably support ready-made formats, similar
> to “pretty” formats that git-log(1) & co.
>
>> Your point about post-rewrite hook support is well-taken though. As this
>> command evolves toward client-side interactive rebase (which was Elijah's
>> original design goal), we will definitely need hook integration. At that
>> point, a --format approach with atoms like %(old) and %(new) could make
>> sense for letting users extract the commit mapping in whatever form they
>> need for hooks or other tooling.
>>
>> For this iteration I am focusing on the simpler atomic update case, but
>> I will
>> keep the --format idea in mind for future work.
>>
>> [replying to this part
>>
>> Do you see a specific use case right now where --format would help, or
>> is this more about future-proofing the design for when we add
>> client-side features?
> I have been using git-rebase(1) for a while with a post-rewrite script.
> This is used for interactive rebases but also just keeping up with
> upstream, i.e. a regular rebase.  Then I was idly thinking that
> git-replay(1) would be faster for the plain rebase case—but it doesn’t
> support that hook directly.  Okay, but I can get around that: I can
> parse the output, yank the commit OIDs, and run git-rev-list(1) on both
> of them to get the mapping I want.  But it would be really nice to just
> declare the correct post-rewrite format and be done, without having to
> parse anything. :)


Ah, that's a concrete use case! You are using post-rewrite hooks with
rebase and want git replay to support that workflow without needing to
parse output.

That makes sense for the client-side evolution of the command. Right now
the focus is server-side where hooks aren't typically needed, but as this
moves toward replacing interactive rebase, proper hook support (including
post-rewrite) will be essential.

I think --format with atoms would work well for that - you could get
exactly the format post-rewrite expects without parsing. For now I'll keep
the simple update-ref format, but this is good motivation for adding
--format support when we tackle the client-side features.

Thanks for the concrete example!


>
> Beyond that though I’ve been thinking about more hypothetical “client-
> side” concerns.  I mentioned writing to the reflog.  I imagine that
> server programs that just want to be able to efficiently “rebase”
> branches to the upstream don’t need that.  But client-side programs
> might want to write to the reflog because they want to mark what the
> update is for; you could have many kinds of client-side “update ref”
> programs and want to leave breadcrumbs about what was done.  There is
> more experimentation.  Whereas I imagine that a forge has maybe a small
> set of “update branch” commands.  I don’t know, maybe I’m rambling at
> this point.
>
>> Thanks for the thoughtful feedback!
> Thanks for the consideration and reply!

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-03 19:48         ` Elijah Newren
  2025-10-03 20:32           ` Junio C Hamano
@ 2025-10-08 20:05           ` Siddharth Asthana
  1 sibling, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-08 20:05 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, gitster, christian.couder, ps, code, rybak.a.v, karthik.188,
	jltobler, toon, johncai86, johannes.schindelin


On 04/10/25 01:18, Elijah Newren wrote:
> On Thu, Oct 2, 2025 at 4:27 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>>>> For users needing the traditional pipeline workflow, --output-commands
>>>> preserves the original behavior:
>>>>
>>>>       git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin
>>> This is good.  Did you also add a config option so that someone can
>>> just set that option once and use the old behavior?  (as per the
>>> suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/
>>> ?)
>>
>> I didn't, but I should have. I will add a config option for v3.
>>
>> For naming, I am thinking either:
>>     - replay.updateRefs (boolean: true = update, false = output-commands)
>>     - replay.defaultOutput (string: "update" | "commands")
>>
>> The boolean feels simpler, but the string might be more extensible if we
>> add other output modes later. Which pattern feels more consistent with
>> existing Git config conventions? Looking at rebase.* they're mostly
>> boolean toggles, but am I missing a better example to follow?
> replay.updateRefs sounds better to me.  defaultOutput with "update"
> doesn't make sense to me.
>
>> You are right - I don't have a concrete use case. I was trying to
>> anticipate potential needs but ended up adding unjustified complexity.
>>
>> I will remove --allow-partial entirely from v3. This simplifies to exactly
>> two modes with clear purposes:
>>     1. Default: atomic ref updates (all-or-nothing)
>>     2. --output-commands: traditional pipeline for special cases
>>
>> Much cleaner design.
> Note that once you add a config option, you'll also need an additional
> command line flag (or make it possible to invert an existing one), so
> that users can override the config and get the default behavior.
> Maybe --[no-]update-refs would make sense after all, where
> --update-refs is the default and --no-update-refs is your current
> --output-commands?


That's a good point. With a config option, users need a way to override it.

The --[no-]update-refs pattern makes sense:
- --update-refs (default): atomic ref updates
- --no-update-refs: output commands (equivalent to --output-commands)

This is cleaner than having both --output-commands and needing a separate
--no-output-commands. And you're right about the rebase naming - it's not
really a collision since the concepts are similar enough.

Should I go with --[no-]update-refs and drop --output-commands entirely,
or keep --output-commands as an alias for --no-update-refs for clarity?


>
> (I know you all talked elsewhere in this thread about "avoiding a name
> collision" with rebase, but I don't quite see it as a collision.  When
> Stolee suggested the flag for rebase, I pointed out it's roughly what
> I'm doing in replay, so it doesn't feel like a conflict to me.  I'm
> also open to an alternative flag name if it makes sense, but we
> probably want whatever the command line flag is to be similar to the
> config name and "defaultOutput"/--default-output don't make sense as a
> name to me.)
>
>>>> @@ -330,9 +361,12 @@ int cmd_replay(int argc,
>>>>                   usage_with_options(replay_usage, replay_options);
>>>>           }
>>>>
>>>> -       if (advance_name_opt && contained)
>>>> -               die(_("options '%s' and '%s' cannot be used together"),
>>>> -                   "--advance", "--contained");
>>>> +       die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>>>> +                                 contained, "--contained");
>>> Broken indentation.  Also, should this have been done as a preparatory
>>> cleanup patch?
>>
>> Good catches. I will fix the indentation.
>>
>> On making it a preparatory patch: should I split it out as a separate
>> cleanup commit, or is it minor enough to fold into the main change? I am
>> leaning toward folding it in since it's directly related to the option
>> handling changes
> Given that it was directly adjacent to the other
> die_for_incompatible_opt2() call, if that were still the case, I could
> see making it part of the same commit.  However, dropping the
> --allow-partial flag means you don't need to add that other call
> anymore, so it makes this remaining die_for_incompataible_opt2() call
> an entirely orthogonal change to the rest of your patch.  As such, I
> think it belongs in a separate patch; it could either be a preparatory
> patch or a follow-up.


Good point. With --allow-partial gone, the die_for_incompatible_opt2()
change stands alone. I will make it a preparatory cleanup patch before the
main change.


>
>>>> @@ -407,6 +452,8 @@ int cmd_replay(int argc,
>>>>                   khint_t pos;
>>>>                   int hr;
>>>>
>>>> +               commits_processed = 1;
>>>> +
>>>>                   if (!commit->parents)
>>>>                           die(_("replaying down to root commit is not supported yet!"));
>>>>                   if (commit->parents->next)
>>>> @@ -457,9 +535,17 @@ int cmd_replay(int argc,
>>>>                   strset_clear(update_refs);
>>>>                   free(update_refs);
>>>>           }
>>>> -       ret = result.clean;
>>>> +
>>>> +       /* Handle empty ranges: if no commits were processed, treat as success */
>>>> +       if (!commits_processed)
>>>> +               ret = 1; /* Success - no commits to replay is not an error */
>>>> +       else
>>>> +               ret = result.clean;
>>> The change to treat empty ranges as success is an orthogonal change
>>> that I think at a minimum belongs in a separate patch.  Out of
>>> curiosity, how did you discover the exit status with an empty commit
>>> range?  Why does someone specify such a range, and what form or forms
>>> might it come in?  And is merely returning a successful result enough,
>>> or is there more that needs to be done for correctness?
>>
>> I was thinking about automated scripts that compute ranges dynamically -
>> they might generate A..B where it turns out A==B, and treating that as
>> "no work needed, success" seemed reasonable for scripting.
>>
>> But you raise a good point: A..A seems like obvious user error (why would
>> anyone do that intentionally?), and B..A where B contains A is likely a
>> mistake that maybe should error rather than silently succeed.
>>
>> I am inclined to drop it entirely from this series. If there's real demand
>> for specific empty-range handling, we can add it later with proper
>> discussion of the actual use cases. Does that sound reasonable?
> Yep, dropping it makes sense to me.  Alternatively, documenting what
> happens in the case of empty ranges, as Christian suggests, also makes
> sense to me though I might suggest that it be done in an entirely
> separate series rather than just a separate patch of this series.


I will drop it from this series. Documenting the current empty range
behavior can be a separate follow-up if there's interest, but I don't
think it needs to block this change.



^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-03 20:32           ` Junio C Hamano
@ 2025-10-08 20:06             ` Siddharth Asthana
  2025-10-08 20:59               ` Junio C Hamano
  2025-10-08 21:30               ` Elijah Newren
  0 siblings, 2 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-08 20:06 UTC (permalink / raw)
  To: Junio C Hamano, Elijah Newren
  Cc: git, christian.couder, ps, code, rybak.a.v, karthik.188, jltobler,
	toon, johncai86, johannes.schindelin


On 04/10/25 02:02, Junio C Hamano wrote:
> Elijah Newren <newren@gmail.com> writes:
>
>>> For naming, I am thinking either:
>>>     - replay.updateRefs (boolean: true = update, false = output-commands)
>>>     - replay.defaultOutput (string: "update" | "commands")
>>>
>>> The boolean feels simpler, but the string might be more extensible if we
>>> add other output modes later. Which pattern feels more consistent with
>>> existing Git config conventions? Looking at rebase.* they're mostly
>>> boolean toggles, but am I missing a better example to follow?
>> replay.updateRefs sounds better to me.  defaultOutput with "update"
>> doesn't make sense to me.
> Yup.  Or "replay.defaultAction = (update-ref | show-comamnds)" if we
> anticipate that we might have a third option someday.  That would of
> course affect the choice of the command line option.


That's interesting. Between:
- replay.updateRefs (boolean)
- replay.defaultAction (enum string)

The enum is more extensible, but do we actually anticipate other modes?
Elijah's --format idea from Kristoffer might be a third mode eventually,
but that seems far off.

I am leaning toward the simpler replay.updateRefs boolean for now, but if
you think the extensibility is worth it, I can go with defaultAction.
What's your preference?


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-08 14:01         ` Phillip Wood
@ 2025-10-08 20:09           ` Siddharth Asthana
  2025-10-08 20:59             ` Elijah Newren
  2025-10-09  9:40             ` Phillip Wood
  0 siblings, 2 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-08 20:09 UTC (permalink / raw)
  To: phillip.wood, git
  Cc: gitster, christian.couder, ps, newren, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin


On 08/10/25 19:31, Phillip Wood wrote:
> Hi Siddharth
>
> On 02/10/2025 23:20, Siddharth Asthana wrote:
>> On 30/09/25 15:35, Phillip Wood wrote:
>>> On 27/09/2025 00:08, Siddharth Asthana wrote:
>>>> The git replay command currently outputs update commands that must be
>>>> piped to git update-ref --stdin to actually update references:
>>
>> The actual advantages of the new default aren't about atomicity (that
>> already exists), but rather:
>> - Eliminating the pipeline for the common case
>> - Better ergonomics for users who just want refs updated
>> - Simpler server-side automation
>>
>> I will rewrite the commit message to accurately reflect this. Elijah
>> provided a good suggested structure that captures the real trade-offs
>> without false claims.
>
> That's great. I agree that having replay update the refs itself is a 
> useful improvement.
>
>>>> +--allow-partial::
>>>> +    Allow some ref updates to succeed even if others fail. By 
>>>> default,
>>>> +    ref updates are atomic (all succeed or all fail). With this 
>>>> option,
>>>> +    failed updates are reported as warnings rather than causing 
>>>> the entire
>>>> +    command to fail. The command exits with code 0 only if all 
>>>> updates
>>>> +    succeed; any failures result in exit code 1. Cannot be used with
>>>> +    `--output-commands`.
>>>
>>> Rather than having two incompatible options perhaps we could have a 
>>> single "--update-refs=(yes|print|allow-partial-updates)" argument. I 
>>> think the name "--allow-partial" is rather ambiguous as it does not 
>>> say what it is allowing to be partial.
>>
>> After thinking about this and Elijah's feedback, I am leaning toward
>> dropping --allow-partial entirely since I don't have a concrete use case
>> for it. That simplifies things to just: default atomic updates vs
>> --output-commands for the traditional pipeline.
>>
>> Would you still prefer a --update-refs=<mode> style, or is the simpler
>> --output-commands flag sufficient given that --allow-partial is going 
>> away?
>
> The advantage of --update-refs=<mode> is that it allows for future 
> extensions such as adding support for partial in a way that does not 
> add conflicting options.


That's a good point about extensibility. Elijah suggested 
--[no-]update-refs
which is simpler but less extensible.

Between:
- --[no-]update-refs (simple, covers current needs)
- --update-refs=<mode> (extensible for future modes)

I am inclined toward the simpler --[no-]update-refs for now since we don't
have concrete plans for other modes. But if you think the extensibility is
important, I can go with the =<mode> style. What do you think?


>
> Thanks
>
> Phillip
>  >
>>>
>>>> +static int add_ref_to_transaction(struct ref_transaction 
>>>> *transaction,
>>>> +                  const char *refname,
>>>> +                  const struct object_id *new_oid,
>>>> +                  const struct object_id *old_oid,
>>>> +                  struct strbuf *err)
>>>> +{
>>>> +    return ref_transaction_update(transaction, refname, new_oid, 
>>>> old_oid,
>>>> +                      NULL, NULL, 0, "git replay", err);
>>>> +}
>>>
>>> I'm not sure this function adds much value. I think it would be 
>>> better to instead have a helper function that updates refs or prints 
>>> the ref updates so that we do not duplicate that code in the two 
>>> places below.
>>
>>
>> ood point. I will extract a helper like:
>>
>>      static int handle_ref_update(int output_commands,
>>                                   struct ref_transaction *transaction,
>>                                   const char *refname,
>>                                   const struct object_id *new_oid,
>>                                   const struct object_id *old_oid,
>>                                   struct strbuf *err)
>>
>> This eliminates the duplication and fixes the over-long lines you 
>> pointed
>> out at both call sites.
>>
>> Thanks!
>>
>>
>>>
>>>> @@ -434,10 +481,18 @@ int cmd_replay(int argc,
>>>>               if (decoration->type == DECORATION_REF_LOCAL &&
>>>>                   (contained || strset_contains(update_refs,
>>>>                                 decoration->name))) {
>>>> -                printf("update %s %s %s\n",
>>>> -                       decoration->name,
>>>> - oid_to_hex(&last_commit->object.oid),
>>>> - oid_to_hex(&commit->object.oid));
>>>> +                if (output_commands) {
>>>> +                    printf("update %s %s %s\n",
>>>> +                           decoration->name,
>>>> + oid_to_hex(&last_commit->object.oid),
>>>> + oid_to_hex(&commit->object.oid));
>>>> +                } else if (add_ref_to_transaction(transaction, 
>>>> decoration->name,
>>>> + &last_commit->object.oid,
>>>> + &commit->object.oid,
>>>> +                                  &transaction_err) < 0) {
>>>> +                    ret = error(_("failed to add ref update to 
>>>> transaction: %s"), transaction_err.buf);
>>>> +                    goto cleanup;
>>>> +                }
>>>>               }
>>>
>>> The lines here are very long due to the indentation, having a 
>>> separate function to update the refs or print the ref updates would 
>>> be much more readable.
>>>
>>>>               decoration = decoration->next;
>>>>           }
>>>> @@ -445,10 +500,33 @@ int cmd_replay(int argc,
>>>>         /* In --advance mode, advance the target ref */
>>>>       if (result.clean == 1 && advance_name) {
>>>> -        printf("update %s %s %s\n",
>>>> -               advance_name,
>>>> -               oid_to_hex(&last_commit->object.oid),
>>>> -               oid_to_hex(&onto->object.oid));
>>>> +        if (output_commands) {
>>>> +            printf("update %s %s %s\n",
>>>> +                   advance_name,
>>>> + oid_to_hex(&last_commit->object.oid),
>>>> +                   oid_to_hex(&onto->object.oid));
>>>> +        } else if (add_ref_to_transaction(transaction, advance_name,
>>>> +                          &last_commit->object.oid,
>>>> +                          &onto->object.oid,
>>>> +                          &transaction_err) < 0) {
>>>> +            ret = error(_("failed to add ref update to 
>>>> transaction: %s"), transaction_err.buf);
>>>> +            goto cleanup;
>>>> +        }
>>>> +    }
>>>
>>> Putting the code to update the refs or print the ref updates into a 
>>> single function would avoid this duplication and over-long lines.
>>>
>>> Thanks
>>>
>>> Phillip
>>>
>>>> +    /* Commit the ref transaction if we have one */
>>>> +    if (transaction && result.clean == 1) {
>>>> +        if (ref_transaction_commit(transaction, &transaction_err)) {
>>>> +            if (allow_partial) {
>>>> +                warning(_("some ref updates failed: %s"), 
>>>> transaction_err.buf);
>>>> + ref_transaction_for_each_rejected_update(transaction,
>>>> +                                     print_rejected_update, NULL);
>>>> +                ret = 0; /* Set failure even with allow_partial */
>>>> +            } else {
>>>> +                ret = error(_("failed to update refs: %s"), 
>>>> transaction_err.buf);
>>>> +                goto cleanup;
>>>> +            }
>>>> +        }
>>>>       }
>>>>         merge_finalize(&merge_opt, &result);
>>>> @@ -457,9 +535,17 @@ int cmd_replay(int argc,
>>>>           strset_clear(update_refs);
>>>>           free(update_refs);
>>>>       }
>>>> -    ret = result.clean;
>>>> +
>>>> +    /* Handle empty ranges: if no commits were processed, treat as 
>>>> success */
>>>> +    if (!commits_processed)
>>>> +        ret = 1; /* Success - no commits to replay is not an error */
>>>> +    else
>>>> +        ret = result.clean;
>>>>     cleanup:
>>>> +    if (transaction)
>>>> +        ref_transaction_free(transaction);
>>>> +    strbuf_release(&transaction_err);
>>>>       release_revisions(&revs);
>>>>       free(advance_name);
>>>>   diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>>>> index 58b3759935..8b4301e227 100755
>>>> --- a/t/t3650-replay-basics.sh
>>>> +++ b/t/t3650-replay-basics.sh
>>>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
>>>>   '
>>>>     test_expect_success 'using replay to rebase two branches, one 
>>>> on top of other' '
>>>> -    git replay --onto main topic1..topic2 >result &&
>>>> +    git replay --output-commands --onto main topic1..topic2 
>>>> >result &&
>>>>         test_line_count = 1 result &&
>>>>   @@ -67,9 +67,30 @@ test_expect_success 'using replay to rebase 
>>>> two branches, one on top of other' '
>>>>       test_cmp expect result
>>>>   '
>>>>   +test_expect_success 'using replay with default atomic behavior 
>>>> (no output)' '
>>>> +    # Create a test branch that wont interfere with others
>>>> +    git branch atomic-test topic2 &&
>>>> +    git rev-parse atomic-test >atomic-test-old &&
>>>> +
>>>> +    # Default behavior: atomic ref updates (no output)
>>>> +    git replay --onto main topic1..atomic-test >output &&
>>>> +    test_must_be_empty output &&
>>>> +
>>>> +    # Verify the branch was updated
>>>> +    git rev-parse atomic-test >atomic-test-new &&
>>>> +    ! test_cmp atomic-test-old atomic-test-new &&
>>>> +
>>>> +    # Verify the history is correct
>>>> +    git log --format=%s atomic-test >actual &&
>>>> +    test_write_lines E D M L B A >expect &&
>>>> +    test_cmp expect actual
>>>> +'
>>>> +
>>>>   test_expect_success 'using replay on bare repo to rebase two 
>>>> branches, one on top of other' '
>>>> -    git -C bare replay --onto main topic1..topic2 >result-bare &&
>>>> -    test_cmp expect result-bare
>>>> +    git -C bare replay --output-commands --onto main 
>>>> topic1..topic2 >result-bare &&
>>>> +
>>>> +    # The result should match what we got from the regular repo
>>>> +    test_cmp result result-bare
>>>>   '
>>>>     test_expect_success 'using replay to rebase with a conflict' '
>>>> @@ -86,7 +107,7 @@ test_expect_success 'using replay to perform 
>>>> basic cherry-pick' '
>>>>       # 2nd field of result is refs/heads/main vs. refs/heads/topic2
>>>>       # 4th field of result is hash for main instead of hash for 
>>>> topic2
>>>>   -    git replay --advance main topic1..topic2 >result &&
>>>> +    git replay --output-commands --advance main topic1..topic2 
>>>> >result &&
>>>>         test_line_count = 1 result &&
>>>>   @@ -102,7 +123,7 @@ test_expect_success 'using replay to perform 
>>>> basic cherry-pick' '
>>>>   '
>>>>     test_expect_success 'using replay on bare repo to perform basic 
>>>> cherry-pick' '
>>>> -    git -C bare replay --advance main topic1..topic2 >result-bare &&
>>>> +    git -C bare replay --output-commands --advance main 
>>>> topic1..topic2 >result-bare &&
>>>>       test_cmp expect result-bare
>>>>   '
>>>>   @@ -115,7 +136,7 @@ test_expect_success 'replay fails when both 
>>>> -- advance and --onto are omitted' '
>>>>   '
>>>>     test_expect_success 'using replay to also rebase a contained 
>>>> branch' '
>>>> -    git replay --contained --onto main main..topic3 >result &&
>>>> +    git replay --output-commands --contained --onto main 
>>>> main..topic3 >result &&
>>>>         test_line_count = 2 result &&
>>>>       cut -f 3 -d " " result >new-branch-tips &&
>>>> @@ -139,12 +160,12 @@ test_expect_success 'using replay to also 
>>>> rebase a contained branch' '
>>>>   '
>>>>     test_expect_success 'using replay on bare repo to also rebase a 
>>>> contained branch' '
>>>> -    git -C bare replay --contained --onto main main..topic3 
>>>> >result- bare &&
>>>> +    git -C bare replay --output-commands --contained --onto main 
>>>> main..topic3 >result-bare &&
>>>>       test_cmp expect result-bare
>>>>   '
>>>>     test_expect_success 'using replay to rebase multiple divergent 
>>>> branches' '
>>>> -    git replay --onto main ^topic1 topic2 topic4 >result &&
>>>> +    git replay --output-commands --onto main ^topic1 topic2 topic4 
>>>> >result &&
>>>>         test_line_count = 2 result &&
>>>>       cut -f 3 -d " " result >new-branch-tips &&
>>>> @@ -168,7 +189,7 @@ test_expect_success 'using replay to rebase 
>>>> multiple divergent branches' '
>>>>   '
>>>>     test_expect_success 'using replay on bare repo to rebase 
>>>> multiple divergent branches, including contained ones' '
>>>> -    git -C bare replay --contained --onto main ^main topic2 topic3 
>>>> topic4 >result &&
>>>> +    git -C bare replay --output-commands --contained --onto main 
>>>> ^main topic2 topic3 topic4 >result &&
>>>>         test_line_count = 4 result &&
>>>>       cut -f 3 -d " " result >new-branch-tips &&
>>>> @@ -217,4 +238,131 @@ test_expect_success 
>>>> 'merge.directoryRenames=false' '
>>>>           --onto rename-onto rename-onto..rename-from
>>>>   '
>>>>   +# Tests for new default atomic behavior and options> > 
>>>> +test_expect_success 'replay default behavior should not produce 
>>> output when successful' '
>>>> +    git replay --onto main topic1..topic3 >output &&
>>>> +    test_must_be_empty output
>>>> +'
>>>> +
>>>> +test_expect_success 'replay with --output-commands produces 
>>>> traditional output' '
>>>> +    git replay --output-commands --onto main topic1..topic3 
>>>> >output &&
>>>> +    test_line_count = 1 output &&
>>>> +    grep "^update refs/heads/topic3 " output
>>>> +'
>>>> +
>>>> +test_expect_success 'replay with --allow-partial should not 
>>>> produce output when successful' '
>>>> +    git replay --allow-partial --onto main topic1..topic3 >output &&
>>>> +    test_must_be_empty output
>>>> +'
>>>> +
>>>> +test_expect_success 'replay fails when --output-commands and -- 
>>>> allow-partial are used together' '
>>>> +    test_must_fail git replay --output-commands --allow-partial -- 
>>>> onto main topic1..topic2 2>error &&
>>>> +    grep "cannot be used together" error
>>>> +'
>>>> +
>>>> +test_expect_success 'replay with --contained updates multiple 
>>>> branches atomically' '
>>>> +    # Create fresh test branches based on the original structure
>>>> +    # contained-topic1 should be contained within the range to 
>>>> contained-topic3
>>>> +    git branch contained-base main &&
>>>> +    git checkout -b contained-topic1 contained-base &&
>>>> +    test_commit ContainedC &&
>>>> +    git checkout -b contained-topic3 contained-topic1 &&
>>>> +    test_commit ContainedG &&
>>>> +    test_commit ContainedH &&
>>>> +    git checkout main &&
>>>> +
>>>> +    # Store original states
>>>> +    git rev-parse contained-topic1 >contained-topic1-old &&
>>>> +    git rev-parse contained-topic3 >contained-topic3-old &&
>>>> +
>>>> +    # Use --contained to update multiple branches - this should 
>>>> update both
>>>> +    git replay --contained --onto main contained-base..contained- 
>>>> topic3 &&
>>>> +
>>>> +    # Verify both branches were updated
>>>> +    git rev-parse contained-topic1 >contained-topic1-new &&
>>>> +    git rev-parse contained-topic3 >contained-topic3-new &&
>>>> +    ! test_cmp contained-topic1-old contained-topic1-new &&
>>>> +    ! test_cmp contained-topic3-old contained-topic3-new
>>>> +'
>>>> +
>>>> +test_expect_success 'replay atomic behavior: all refs updated or 
>>>> none' '
>>>> +    # Store original state
>>>> +    git rev-parse topic4 >topic4-old &&
>>>> +
>>>> +    # Default atomic behavior
>>>> +    git replay --onto main main..topic4 &&
>>>> +
>>>> +    # Verify ref was updated
>>>> +    git rev-parse topic4 >topic4-new &&
>>>> +    ! test_cmp topic4-old topic4-new &&
>>>> +
>>>> +    # Verify no partial state
>>>> +    git log --format=%s topic4 >actual &&
>>>> +    test_write_lines J I M L B A >expect &&
>>>> +    test_cmp expect actual
>>>> +'
>>>> +
>>>> +test_expect_success 'replay works correctly with bare repositories' '
>>>> +    # Test atomic behavior in bare repo (important for Gitaly)
>>>> +    git checkout -b bare-test topic1 &&
>>>> +    test_commit BareTest &&
>>>> +
>>>> +    # Test with bare repo - replay the commits from 
>>>> main..bare-test to get the full history
>>>> +    git -C bare fetch .. bare-test:bare-test &&
>>>> +    git -C bare replay --onto main main..bare-test &&
>>>> +
>>>> +    # Verify the bare repo was updated correctly (no output)
>>>> +    git -C bare log --format=%s bare-test >actual &&
>>>> +    test_write_lines BareTest F C M L B A >expect &&
>>>> +    test_cmp expect actual
>>>> +'
>>>> +
>>>> +test_expect_success 'replay --allow-partial with no failures 
>>>> produces no output' '
>>>> +    git checkout -b partial-test topic1 &&
>>>> +    test_commit PartialTest &&
>>>> +
>>>> +    # Should succeed silently even with partial mode
>>>> +    git replay --allow-partial --onto main topic1..partial-test 
>>>> >output &&
>>>> +    test_must_be_empty output
>>>> +'
>>>> +
>>>> +test_expect_success 'replay maintains ref update consistency' '
>>>> +    # Test that traditional vs atomic produce equivalent results
>>>> +    git checkout -b method1-test topic2 &&
>>>> +    git checkout -b method2-test topic2 &&
>>>> +
>>>> +    # Both methods should update refs to point to the same 
>>>> replayed commits
>>>> +    git replay --output-commands --onto main topic1..method1-test 
>>>> >update-commands &&
>>>> +    git update-ref --stdin <update-commands &&
>>>> +    git log --format=%s method1-test >traditional-result &&
>>>> +
>>>> +    # Direct atomic method should produce same commit history
>>>> +    git replay --onto main topic1..method2-test &&
>>>> +    git log --format=%s method2-test >atomic-result &&
>>>> +
>>>> +    # Both methods should produce identical commit histories
>>>> +    test_cmp traditional-result atomic-result
>>>> +'
>>>> +
>>>> +test_expect_success 'replay error messages are helpful and clear' '
>>>> +    # Test that error messages are clear
>>>> +    test_must_fail git replay --output-commands --allow-partial -- 
>>>> onto main topic1..topic2 2>error &&
>>>> +    grep "cannot be used together" error
>>>> +'
>>>> +
>>>> +test_expect_success 'replay with empty range produces no output 
>>>> and no changes' '
>>>> +    # Create a test branch for empty range testing
>>>> +    git checkout -b empty-test topic1 &&
>>>> +    git rev-parse empty-test >empty-test-before &&
>>>> +
>>>> +    # Empty range should succeed but do nothing
>>>> +    git replay --onto main empty-test..empty-test >output &&
>>>> +    test_must_be_empty output &&
>>>> +
>>>> +    # Branch should be unchanged
>>>> +    git rev-parse empty-test >empty-test-after &&
>>>> +    test_cmp empty-test-before empty-test-after
>>>> +'
>>>> +
>>>>   test_done
>>>
>

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior
  2025-10-08 20:02         ` Siddharth Asthana
@ 2025-10-08 20:56           ` Elijah Newren
  2025-10-08 21:16             ` Kristoffer Haugsbakk
  2025-10-08 21:18             ` Siddharth Asthana
  0 siblings, 2 replies; 129+ messages in thread
From: Elijah Newren @ 2025-10-08 20:56 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: Kristoffer Haugsbakk, git, Junio C Hamano, Christian Couder,
	Patrick Steinhardt, Andrei Rybak, Karthik Nayak, Justin Tobler,
	Toon Claes, John Cai, Johannes Schindelin

On Wed, Oct 8, 2025 at 1:02 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> On 04/10/25 00:35, Kristoffer Haugsbakk wrote:
> > Good evening Siddharth
> >
[...]
> > I have been using git-rebase(1) for a while with a post-rewrite script.
> > This is used for interactive rebases but also just keeping up with
> > upstream, i.e. a regular rebase.  Then I was idly thinking that
> > git-replay(1) would be faster for the plain rebase case—but it doesn’t
> > support that hook directly.  Okay, but I can get around that: I can
> > parse the output, yank the commit OIDs, and run git-rev-list(1) on both
> > of them to get the mapping I want.  But it would be really nice to just
> > declare the correct post-rewrite format and be done, without having to
> > parse anything. :)
>
>
> Ah, that's a concrete use case! You are using post-rewrite hooks with
> rebase and want git replay to support that workflow without needing to
> parse output.
>
> That makes sense for the client-side evolution of the command. Right now
> the focus is server-side where hooks aren't typically needed, but as this
> moves toward replacing interactive rebase, proper hook support (including
> post-rewrite) will be essential.
>
> I think --format with atoms would work well for that - you could get
> exactly the format post-rewrite expects without parsing. For now I'll keep
> the simple update-ref format, but this is good motivation for adding
> --format support when we tackle the client-side features.
>
> Thanks for the concrete example!

Let's be *very* careful before we add any hooks to replay.
pre-rebase, for example, forced the assumption of only one ref being
involved.  The early implementation of rebase as a shell script on top
of other commands forced assumptions that it played with pre-commit,
post-commit, and post-checkout, and forces us today to continue to
check out every intermediate commit to the working copy even when the
rebase could otherwise be done entirely in-memory without touching the
index or working copy.  post-rewrite seems more sane than most other
hooks, but I still want to avoid painting ourselves into a corner, and
hooks are very much about defined and established APIs through which
we communicate to other processes, which means it's exactly the kind
of thing that could paint us into a corner.  We'll probably want that
kind of extensibility eventually, but it's way too early right now.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-08 20:06             ` Siddharth Asthana
@ 2025-10-08 20:59               ` Junio C Hamano
  2025-10-08 21:10                 ` Siddharth Asthana
  2025-10-08 21:30               ` Elijah Newren
  1 sibling, 1 reply; 129+ messages in thread
From: Junio C Hamano @ 2025-10-08 20:59 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: Elijah Newren, git, christian.couder, ps, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> On 04/10/25 02:02, Junio C Hamano wrote:
>> Elijah Newren <newren@gmail.com> writes:
>>
>>>> For naming, I am thinking either:
>>>>     - replay.updateRefs (boolean: true = update, false = output-commands)
>>>>     - replay.defaultOutput (string: "update" | "commands")
>>>>
>>>> The boolean feels simpler, but the string might be more extensible if we
>>>> add other output modes later. Which pattern feels more consistent with
>>>> existing Git config conventions? Looking at rebase.* they're mostly
>>>> boolean toggles, but am I missing a better example to follow?
>>> replay.updateRefs sounds better to me.  defaultOutput with "update"
>>> doesn't make sense to me.
>> Yup.  Or "replay.defaultAction = (update-ref | show-comamnds)" if we
>> anticipate that we might have a third option someday.  That would of
>> course affect the choice of the command line option.
>
>
> That's interesting. Between:
> - replay.updateRefs (boolean)
> - replay.defaultAction (enum string)
>
> The enum is more extensible, but do we actually anticipate other modes?
> Elijah's --format idea from Kristoffer might be a third mode eventually,
> but that seems far off.

What do you exactly mean "far off"?  If it won't happen in 2 weeks,
but it is likely to come in 2 years, then making sure we have smooth
upgrade paths is still valuable.  Once you start with "do we update
refs?" boolean, how would you later accomodate the third option?

No matter what you do then, the end result would be an awkward "if
you want the command to update the refs, set this Boolean to true,
if you want the command to show what would happen in the output,
set this _OTHER_ configuration option to this string, or you can set
this yet another variable to cause this different action to happen."

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-08 20:09           ` Siddharth Asthana
@ 2025-10-08 20:59             ` Elijah Newren
  2025-10-08 21:16               ` Siddharth Asthana
  2025-10-09  9:40             ` Phillip Wood
  1 sibling, 1 reply; 129+ messages in thread
From: Elijah Newren @ 2025-10-08 20:59 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: phillip.wood, git, gitster, christian.couder, ps, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin

On Wed, Oct 8, 2025 at 1:09 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> On 08/10/25 19:31, Phillip Wood wrote:
> > Hi Siddharth
> >
> > On 02/10/2025 23:20, Siddharth Asthana wrote:
> >> On 30/09/25 15:35, Phillip Wood wrote:
> >>> On 27/09/2025 00:08, Siddharth Asthana wrote:
> >>>> The git replay command currently outputs update commands that must be
> >>>> piped to git update-ref --stdin to actually update references:
> >>
> >> The actual advantages of the new default aren't about atomicity (that
> >> already exists), but rather:
> >> - Eliminating the pipeline for the common case
> >> - Better ergonomics for users who just want refs updated
> >> - Simpler server-side automation
> >>
> >> I will rewrite the commit message to accurately reflect this. Elijah
> >> provided a good suggested structure that captures the real trade-offs
> >> without false claims.
> >
> > That's great. I agree that having replay update the refs itself is a
> > useful improvement.
> >
> >>>> +--allow-partial::
> >>>> +    Allow some ref updates to succeed even if others fail. By
> >>>> default,
> >>>> +    ref updates are atomic (all succeed or all fail). With this
> >>>> option,
> >>>> +    failed updates are reported as warnings rather than causing
> >>>> the entire
> >>>> +    command to fail. The command exits with code 0 only if all
> >>>> updates
> >>>> +    succeed; any failures result in exit code 1. Cannot be used with
> >>>> +    `--output-commands`.
> >>>
> >>> Rather than having two incompatible options perhaps we could have a
> >>> single "--update-refs=(yes|print|allow-partial-updates)" argument. I
> >>> think the name "--allow-partial" is rather ambiguous as it does not
> >>> say what it is allowing to be partial.
> >>
> >> After thinking about this and Elijah's feedback, I am leaning toward
> >> dropping --allow-partial entirely since I don't have a concrete use case
> >> for it. That simplifies things to just: default atomic updates vs
> >> --output-commands for the traditional pipeline.
> >>
> >> Would you still prefer a --update-refs=<mode> style, or is the simpler
> >> --output-commands flag sufficient given that --allow-partial is going
> >> away?
> >
> > The advantage of --update-refs=<mode> is that it allows for future
> > extensions such as adding support for partial in a way that does not
> > add conflicting options.
>
>
> That's a good point about extensibility. Elijah suggested
> --[no-]update-refs
> which is simpler but less extensible.
>
> Between:
> - --[no-]update-refs (simple, covers current needs)
> - --update-refs=<mode> (extensible for future modes)
>
> I am inclined toward the simpler --[no-]update-refs for now since we don't
> have concrete plans for other modes. But if you think the extensibility is
> important, I can go with the =<mode> style. What do you think?

I like Phillip's suggestion more than my own.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-08 20:59               ` Junio C Hamano
@ 2025-10-08 21:10                 ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-08 21:10 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Elijah Newren, git, christian.couder, ps, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin


On 09/10/25 02:29, Junio C Hamano wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>> On 04/10/25 02:02, Junio C Hamano wrote:
>>> Elijah Newren <newren@gmail.com> writes:
>>>
>>>>> For naming, I am thinking either:
>>>>>      - replay.updateRefs (boolean: true = update, false = output-commands)
>>>>>      - replay.defaultOutput (string: "update" | "commands")
>>>>>
>>>>> The boolean feels simpler, but the string might be more extensible if we
>>>>> add other output modes later. Which pattern feels more consistent with
>>>>> existing Git config conventions? Looking at rebase.* they're mostly
>>>>> boolean toggles, but am I missing a better example to follow?
>>>> replay.updateRefs sounds better to me.  defaultOutput with "update"
>>>> doesn't make sense to me.
>>> Yup.  Or "replay.defaultAction = (update-ref | show-comamnds)" if we
>>> anticipate that we might have a third option someday.  That would of
>>> course affect the choice of the command line option.
>>
>> That's interesting. Between:
>> - replay.updateRefs (boolean)
>> - replay.defaultAction (enum string)
>>
>> The enum is more extensible, but do we actually anticipate other modes?
>> Elijah's --format idea from Kristoffer might be a third mode eventually,
>> but that seems far off.
> What do you exactly mean "far off"?  If it won't happen in 2 weeks,
> but it is likely to come in 2 years, then making sure we have smooth
> upgrade paths is still valuable.  Once you start with "do we update
> refs?" boolean, how would you later accomodate the third option?


You are right - I wasn't thinking about the upgrade path properly.

Looking at Kristoffer's post-rewrite hook use case --format support seems
likely within a reasonable timeframe. And you are absolutely right that
starting with a boolean creates an awkward situation: we would end up with
replay.updateRefs (boolean) plus replay.outputFormat (string) or something
similarly messy.

The enum approach is cleaner:

   replay.defaultAction = update-refs | show-commands | format

This keeps one config variable handling all output modes. When --format
gets added, it's just another value, not a new config.


For the command line, I'm thinking --update-refs=<mode> makes the most
sense. It's specific enough to be clear but general enough to handle the
three modes. The slight inconsistency with the config name
(defaultAction vs updateRefs) seems acceptable since the command line is
about *what* to do with refs, while the config is about the broader action.

Does that reasoning make sense?


>
> No matter what you do then, the end result would be an awkward "if
> you want the command to update the refs, set this Boolean to true,
> if you want the command to show what would happen in the output,
> set this _OTHER_ configuration option to this string, or you can set
> this yet another variable to cause this different action to happen."

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-08 20:59             ` Elijah Newren
@ 2025-10-08 21:16               ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-08 21:16 UTC (permalink / raw)
  To: Elijah Newren
  Cc: phillip.wood, git, gitster, christian.couder, ps, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin


On 09/10/25 02:29, Elijah Newren wrote:
> On Wed, Oct 8, 2025 at 1:09 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> On 08/10/25 19:31, Phillip Wood wrote:
>>> Hi Siddharth
>>>
>>> On 02/10/2025 23:20, Siddharth Asthana wrote:
>>>> On 30/09/25 15:35, Phillip Wood wrote:
>>>>> On 27/09/2025 00:08, Siddharth Asthana wrote:
>>>>>> The git replay command currently outputs update commands that must be
>>>>>> piped to git update-ref --stdin to actually update references:
>>>> The actual advantages of the new default aren't about atomicity (that
>>>> already exists), but rather:
>>>> - Eliminating the pipeline for the common case
>>>> - Better ergonomics for users who just want refs updated
>>>> - Simpler server-side automation
>>>>
>>>> I will rewrite the commit message to accurately reflect this. Elijah
>>>> provided a good suggested structure that captures the real trade-offs
>>>> without false claims.
>>> That's great. I agree that having replay update the refs itself is a
>>> useful improvement.
>>>
>>>>>> +--allow-partial::
>>>>>> +    Allow some ref updates to succeed even if others fail. By
>>>>>> default,
>>>>>> +    ref updates are atomic (all succeed or all fail). With this
>>>>>> option,
>>>>>> +    failed updates are reported as warnings rather than causing
>>>>>> the entire
>>>>>> +    command to fail. The command exits with code 0 only if all
>>>>>> updates
>>>>>> +    succeed; any failures result in exit code 1. Cannot be used with
>>>>>> +    `--output-commands`.
>>>>> Rather than having two incompatible options perhaps we could have a
>>>>> single "--update-refs=(yes|print|allow-partial-updates)" argument. I
>>>>> think the name "--allow-partial" is rather ambiguous as it does not
>>>>> say what it is allowing to be partial.
>>>> After thinking about this and Elijah's feedback, I am leaning toward
>>>> dropping --allow-partial entirely since I don't have a concrete use case
>>>> for it. That simplifies things to just: default atomic updates vs
>>>> --output-commands for the traditional pipeline.
>>>>
>>>> Would you still prefer a --update-refs=<mode> style, or is the simpler
>>>> --output-commands flag sufficient given that --allow-partial is going
>>>> away?
>>> The advantage of --update-refs=<mode> is that it allows for future
>>> extensions such as adding support for partial in a way that does not
>>> add conflicting options.
>>
>> That's a good point about extensibility. Elijah suggested
>> --[no-]update-refs
>> which is simpler but less extensible.
>>
>> Between:
>> - --[no-]update-refs (simple, covers current needs)
>> - --update-refs=<mode> (extensible for future modes)
>>
>> I am inclined toward the simpler --[no-]update-refs for now since we don't
>> have concrete plans for other modes. But if you think the extensibility is
>> important, I can go with the =<mode> style. What do you think?
> I like Phillip's suggestion more than my own.


Got it. I will go with --update-refs=<mode> then:
- --update-refs=yes (or just --update-refs as shorthand): atomic updates
- --update-refs=print: output commands
- (future) --update-refs=allow-partial or other modes

This keeps the design extensible without adding conflicting options later.

For the config, I will use replay.updateRefs with string values matching 
the
command line modes. That keeps them consistent.


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior
  2025-10-08 20:56           ` Elijah Newren
@ 2025-10-08 21:16             ` Kristoffer Haugsbakk
  2025-10-08 21:18             ` Siddharth Asthana
  1 sibling, 0 replies; 129+ messages in thread
From: Kristoffer Haugsbakk @ 2025-10-08 21:16 UTC (permalink / raw)
  To: Elijah Newren, Siddharth Asthana
  Cc: git, Junio C Hamano, Christian Couder, Patrick Steinhardt,
	Andrei Rybak, Karthik Nayak, Justin Tobler, Toon Claes, John Cai,
	Johannes Schindelin

On Wed, Oct 8, 2025, at 22:56, Elijah Newren wrote:
> On Wed, Oct 8, 2025 at 1:02 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>>
>> On 04/10/25 00:35, Kristoffer Haugsbakk wrote:
>> > Good evening Siddharth
>> >
> [...]
>> > I have been using git-rebase(1) for a while with a post-rewrite script.
>> > This is used for interactive rebases but also just keeping up with
>> > upstream, i.e. a regular rebase.  Then I was idly thinking that
>> > git-replay(1) would be faster for the plain rebase case—but it doesn’t
>> > support that hook directly.  Okay, but I can get around that: I can
>> > parse the output, yank the commit OIDs, and run git-rev-list(1) on both
>> > of them to get the mapping I want.  But it would be really nice to just
>> > declare the correct post-rewrite format and be done, without having to
>> > parse anything. :)
>>
>>
>> Ah, that's a concrete use case! You are using post-rewrite hooks with
>> rebase and want git replay to support that workflow without needing to
>> parse output.
>>
>> That makes sense for the client-side evolution of the command. Right now
>> the focus is server-side where hooks aren't typically needed, but as this
>> moves toward replacing interactive rebase, proper hook support (including
>> post-rewrite) will be essential.
>>
>> I think --format with atoms would work well for that - you could get
>> exactly the format post-rewrite expects without parsing. For now I'll keep
>> the simple update-ref format, but this is good motivation for adding
>> --format support when we tackle the client-side features.
>>
>> Thanks for the concrete example!
>
> Let's be *very* careful before we add any hooks to replay.

The hypothetical case I was talking about was using custom formatting
output to drive git-hook(1). Not adding anything hook-related to
git-replay(1).

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior
  2025-10-08 20:56           ` Elijah Newren
  2025-10-08 21:16             ` Kristoffer Haugsbakk
@ 2025-10-08 21:18             ` Siddharth Asthana
  1 sibling, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-08 21:18 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Kristoffer Haugsbakk, git, Junio C Hamano, Christian Couder,
	Patrick Steinhardt, Andrei Rybak, Karthik Nayak, Justin Tobler,
	Toon Claes, John Cai, Johannes Schindelin


On 09/10/25 02:26, Elijah Newren wrote:
> On Wed, Oct 8, 2025 at 1:02 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> On 04/10/25 00:35, Kristoffer Haugsbakk wrote:
>>> Good evening Siddharth
>>>
> [...]
>>> I have been using git-rebase(1) for a while with a post-rewrite script.
>>> This is used for interactive rebases but also just keeping up with
>>> upstream, i.e. a regular rebase.  Then I was idly thinking that
>>> git-replay(1) would be faster for the plain rebase case—but it doesn’t
>>> support that hook directly.  Okay, but I can get around that: I can
>>> parse the output, yank the commit OIDs, and run git-rev-list(1) on both
>>> of them to get the mapping I want.  But it would be really nice to just
>>> declare the correct post-rewrite format and be done, without having to
>>> parse anything. :)
>>
>> Ah, that's a concrete use case! You are using post-rewrite hooks with
>> rebase and want git replay to support that workflow without needing to
>> parse output.
>>
>> That makes sense for the client-side evolution of the command. Right now
>> the focus is server-side where hooks aren't typically needed, but as this
>> moves toward replacing interactive rebase, proper hook support (including
>> post-rewrite) will be essential.
>>
>> I think --format with atoms would work well for that - you could get
>> exactly the format post-rewrite expects without parsing. For now I'll keep
>> the simple update-ref format, but this is good motivation for adding
>> --format support when we tackle the client-side features.
>>
>> Thanks for the concrete example!
> Let's be *very* careful before we add any hooks to replay.
> pre-rebase, for example, forced the assumption of only one ref being
> involved.  The early implementation of rebase as a shell script on top
> of other commands forced assumptions that it played with pre-commit,
> post-commit, and post-checkout, and forces us today to continue to
> check out every intermediate commit to the working copy even when the
> rebase could otherwise be done entirely in-memory without touching the
> index or working copy.  post-rewrite seems more sane than most other
> hooks, but I still want to avoid painting ourselves into a corner, and
> hooks are very much about defined and established APIs through which
> we communicate to other processes, which means it's exactly the kind
> of thing that could paint us into a corner.  We'll probably want that
> kind of extensibility eventually, but it's way too early right now.


That's a really important point. I wasn't thinking about how hooks lock
in API decisions.

For this series, I will stay completely away from hooks. The --format
discussion with Kristoffer is interesting for future work, but you are
right that it's way too early. We need to understand the client-side use
cases much better before committing to any hook interfaces.

I will keep the focus narrow: just making ref updates the default with a
clean way to get the old behavior.


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-08 20:06             ` Siddharth Asthana
  2025-10-08 20:59               ` Junio C Hamano
@ 2025-10-08 21:30               ` Elijah Newren
  1 sibling, 0 replies; 129+ messages in thread
From: Elijah Newren @ 2025-10-08 21:30 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: Junio C Hamano, git, christian.couder, ps, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin

On Wed, Oct 8, 2025 at 1:06 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
[...]
> Elijah's --format idea from Kristoffer might be a third mode eventually

?

That was in no way my idea; it was all Kristoffer's.

The --format idea might eventually be useful, but to me the --format
thing seems like something you'd add to a very stable command, like
for-each-ref.  I think it wouldn't make sense to add to something like
replay in its current state since replay is "four times more
experimental than any other command"[1], where we're changing the
basic output format, where we bail on any conflicts or merge commits,
where we recently discussed whether to support first-class conflicts
and using that for handling conflicts instead of halting upon the
first one like rebase does, etc.  I feel much the same as when a
similar flag was suggested for merge-tree (in order to let users
control the exact output layout of the information it was already
printing, when it was known that the information provided was
insufficient to solve the user's problems)[2] -- it's premature.

[1] that might be slightly over the top, but it's still a fun
"statistic" of sorts -- see
https://lore.kernel.org/git/CABPp-BHWjyRv_f_HKkz10Q_cOZKPvpgf=SEUR1ThmbttkQT+Uw@mail.gmail.com/
[2] https://lore.kernel.org/git/CABPp-BG25_TutatgNmK6vgq3akxpYHQ8QBnz-65_F_3oCA1nJA@mail.gmail.com/

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior
  2025-10-08 20:09           ` Siddharth Asthana
  2025-10-08 20:59             ` Elijah Newren
@ 2025-10-09  9:40             ` Phillip Wood
  1 sibling, 0 replies; 129+ messages in thread
From: Phillip Wood @ 2025-10-09  9:40 UTC (permalink / raw)
  To: Siddharth Asthana, phillip.wood, git
  Cc: gitster, christian.couder, ps, newren, code, rybak.a.v,
	karthik.188, jltobler, toon, johncai86, johannes.schindelin

On 08/10/2025 21:09, Siddharth Asthana wrote:
> On 08/10/25 19:31, Phillip Wood wrote:
>> Hi Siddharth
>> On 02/10/2025 23:20, Siddharth Asthana wrote:
>>> Would you still prefer a --update-refs=<mode> style, or is the simpler
>>> --output-commands flag sufficient given that --allow-partial is going 
>>> away?
>>
>> The advantage of --update-refs=<mode> is that it allows for future 
>> extensions such as adding support for partial in a way that does not 
>> add conflicting options.
> 
> That's a good point about extensibility. Elijah suggested --[no-]update- 
> refs
> which is simpler but less extensible.
> 
> Between:
> - --[no-]update-refs (simple, covers current needs)
> - --update-refs=<mode> (extensible for future modes)
> 
> I am inclined toward the simpler --[no-]update-refs for now since we don't
> have concrete plans for other modes. But if you think the extensibility is
> important, I can go with the =<mode> style. What do you think?

If we go with a boolean flag we can always add an optional argument in 
the future so I think that would be fine.

Thanks

Phillip


^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v3 0/3] replay: make atomic ref updates the default
@ 2025-10-13 18:25 Siddharth Asthana
  2025-10-13 18:55 ` Siddharth Asthana
  2025-10-14 21:13 ` Junio C Hamano
  0 siblings, 2 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-13 18:25 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

This is v3 of the git-replay atomic updates series.

Based on feedback from v2, this version simplifies the API and improves
extensibility. Thanks to Elijah, Phillip, Christian, Junio, and Karthik
for the detailed reviews that shaped this version.

## Changes Since v2

**Removed --allow-partial option**

After discussion with Elijah and Junio, we couldn't identify a concrete
use case for partial failure tolerance. The traditional pipeline with
git-update-ref already provides partial update capabilities when needed
through its transaction commands. Removing this option simplifies the API
and avoids committing to behavior without clear real-world use cases.

**Changed to --update-refs=<mode> for extensibility**

Phillip suggested that separate boolean flags (--output-commands,
--allow-partial) were limiting for future expansion. The --update-refs=<mode>
design allows future modes without option proliferation:
  - --update-refs=yes (default): atomic ref updates
  - --update-refs=print: pipeline output
  - Future modes can be added as additional values

This API pattern prevents the need for multiple incompatible flags and
provides a cleaner interface for users.

**Added replay.defaultAction configuration option**

Junio recommended a config option for users preferring traditional behavior.
The implementation uses enum string values for extensibility:
  - replay.defaultAction = update-refs (default)
  - replay.defaultAction = show-commands (pipeline output)

The command-line --update-refs option overrides the config, allowing users
to set a preference while maintaining per-invocation control. The enum
design (versus a boolean) allows future expansion to additional modes
without requiring new config variables.

**Improved commit messages and patch organization**

Christian and Elijah provided detailed feedback on commit message structure.
Patch 2 now uses Elijah's suggested format that explains the trade-offs of
the current design before proposing changes. The commit messages now focus
on the changes themselves rather than v1→v2 evolution. Added Helped-by
trailers to acknowledge specific contributions.

**Enhanced test suite with proper isolation**

Following Elijah's suggestions:
  - Existing tests use --update-refs=print to preserve their behavior
  - New tests use test_when_finished for proper cleanup
  - Added real atomicity test using lock files to verify all-or-nothing
  - Fixed bare repository tests to rebuild expectations independently
  - Removed weak tests that didn't actually verify atomicity

**Extracted helper function to reduce duplication**

Per Phillip's feedback, added handle_ref_update() helper to eliminate
code duplication between print and atomic modes. This function takes a
mode parameter and handles both cases, making the code more maintainable
and ensuring both paths stay consistent.

## Technical Implementation

The atomic ref updates use Git's ref transaction API:
  - ref_store_transaction_begin() with default atomic behavior
  - ref_transaction_update() to stage each update
  - ref_transaction_commit() for atomic application

The handle_ref_update() helper encapsulates the mode-specific logic,
either printing update commands or staging them into the transaction.

Config reading uses repo_config_get_string_tmp() with validation for
'update-refs' and 'show-commands' values, mapping them to internal
modes 'yes' and 'print' respectively.

Range-diff against v2:
-:  ---------- > 1:  de9cc3fbee replay: use die_for_incompatible_opt2() for option validation
1:  e3c1a57375 ! 2:  3f4c69d612 replay: make atomic ref updates the default behavior
    @@ Metadata
      ## Commit message ##
         replay: make atomic ref updates the default behavior
     
    -    The git replay command currently outputs update commands that must be
    -    piped to git update-ref --stdin to actually update references:
    +    The git replay command currently outputs update commands that can be
    +    piped to update-ref to achieve a rebase, e.g.
     
    -        git replay --onto main topic1..topic2 | git update-ref --stdin
    +      git replay --onto main topic1..topic2 | git update-ref --stdin
     
    -    This design has significant limitations for server-side operations. The
    -    two-command pipeline creates coordination complexity, provides no atomic
    -    transaction guarantees by default, and complicates automation in bare
    -    repository environments where git replay is primarily used.
    +    This separation had advantages for three special cases:
    +      * it made testing easy (when state isn't modified from one step to
    +        the next, you don't need to make temporary branches or have undo
    +        commands, or try to track the changes)
    +      * it provided a natural can-it-rebase-cleanly (and what would it
    +        rebase to) capability without automatically updating refs, similar
    +        to a --dry-run
    +      * it provided a natural low-level tool for the suite of hash-object,
    +        mktree, commit-tree, mktag, merge-tree, and update-ref, allowing
    +        users to have another building block for experimentation and making
    +        new tools
     
    -    During extensive mailing list discussion, multiple maintainers identified
    -    that the current approach forces users to opt-in to atomic behavior rather
    -    than defaulting to the safer, more reliable option. Elijah Newren noted
    -    that the experimental status explicitly allows such behavior changes, while
    -    Patrick Steinhardt highlighted performance concerns with individual ref
    -    updates in the reftable backend.
    +    However, it should be noted that all three of these are somewhat
    +    special cases; users, whether on the client or server side, would
    +    almost certainly find it more ergonomical to simply have the updating
    +    of refs be the default.
     
    -    The core issue is that git replay was designed around command output rather
    -    than direct action. This made sense for a plumbing tool, but creates barriers
    -    for the primary use case: server-side operations that need reliable, atomic
    -    ref updates without pipeline complexity.
    +    For server-side operations in particular, the pipeline architecture
    +    creates process coordination overhead. Server implementations that need
    +    to perform rebases atomically must maintain additional code to:
     
    -    This patch changes the default behavior to update refs directly using Git's
    -    ref transaction API:
    +      1. Spawn and manage a pipeline between git-replay and git-update-ref
    +      2. Coordinate stdout/stderr streams across the pipe boundary
    +      3. Handle partial failure states if the pipeline breaks mid-execution
    +      4. Parse and validate the update-ref command output
     
    -        git replay --onto main topic1..topic2
    -        # No output; all refs updated atomically or none
    +    Change the default behavior to update refs directly, and atomically (at
    +    least to the extent supported by the refs backend in use). This
    +    eliminates the process coordination overhead for the common case.
     
    -    The implementation uses ref_store_transaction_begin() with atomic mode by
    -    default, ensuring all ref updates succeed or all fail as a single operation.
    -    This leverages git replay's existing server-side strengths (in-memory operation,
    -    no work tree requirement) while adding the atomic guarantees that server
    -    operations require.
    +    For users needing the traditional pipeline workflow, add a new
    +    `--update-refs=<mode>` option that preserves the original behavior:
     
    -    For users needing the traditional pipeline workflow, --output-commands
    -    preserves the original behavior:
    +      git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin
     
    -        git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin
    -
    -    The --allow-partial option enables partial failure tolerance. However, following
    -    maintainer feedback, it implements a "strict success" model: the command exits
    -    with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY
    -    updates fail. This ensures that --allow-partial changes error reporting style
    -    (warnings vs hard errors) but not success criteria, handling edge cases like
    -    "no updates needed" cleanly.
    +    The mode can be:
    +      * `yes` (default): Update refs directly using an atomic transaction
    +      * `print`: Output update-ref commands for pipeline use
     
         Implementation details:
    -    - Empty commit ranges now return success (exit code 0) rather than failure,
    -      as no commits to replay is a valid successful operation
    -    - Added comprehensive test coverage with 12 new tests covering atomic behavior,
    -      option validation, bare repository support, and edge cases
    -    - Fixed test isolation issues to prevent branch state contamination between tests
    -    - Maintains C89 compliance and follows Git's established coding conventions
    -    - Refactored option validation to use die_for_incompatible_opt2() for both
    -      --advance/--contained and --allow-partial/--output-commands conflicts,
    -      providing consistent error reporting
    -    - Fixed --allow-partial exit code behavior to implement "strict success" model
    -      where any ref update failures result in exit code 1, even with partial tolerance
    -    - Updated documentation with proper line wrapping, consistent terminology using
    -      "old default behavior", performance context, and reorganized examples for clarity
    -    - Eliminates individual ref updates (refs_update_ref calls) that perform
    -      poorly with reftable backend
    -    - Uses only batched ref transactions for optimal performance across all
    -      ref backends
    -    - Avoids naming collision with git rebase --update-refs by using distinct
    -      option names
    -    - Defaults to atomic behavior while preserving pipeline compatibility
     
    -    The result is a command that works better for its primary use case (server-side
    -    operations) while maintaining full backward compatibility for existing workflows.
    +    The atomic ref updates are implemented using Git's ref transaction API.
    +    In cmd_replay(), when not in 'print' mode, we initialize a transaction
    +    using ref_store_transaction_begin() with the default atomic behavior.
    +    As commits are replayed, ref updates are staged into the transaction
    +    using ref_transaction_update(). Finally, ref_transaction_commit()
    +    applies all updates atomically—either all updates succeed or none do.
    +
    +    To avoid code duplication between the 'print' and 'yes' modes, this
    +    commit extracts a handle_ref_update() helper function. This function
    +    takes the mode and either prints the update command or stages it into
    +    the transaction. This keeps both code paths consistent and makes future
    +    maintenance easier.
    +
    +    The helper function signature:
    +
    +      static int handle_ref_update(const char *mode,
    +                                    struct ref_transaction *transaction,
    +                                    const char *refname,
    +                                    const struct object_id *new_oid,
    +                                    const struct object_id *old_oid,
    +                                    struct strbuf *err)
    +
    +    When mode is 'print', it prints the update-ref command. When mode is
    +    'yes', it calls ref_transaction_update() to stage the update. This
    +    eliminates the duplication that would otherwise exist at each ref update
    +    call site.
    +
    +    Test suite changes:
     
    +    All existing tests that expected command output now use
    +    `--update-refs=print` to preserve their original behavior. This keeps
    +    the tests valid while allowing them to verify that the pipeline workflow
    +    still works correctly.
    +
    +    New tests were added to verify:
    +      - Default atomic behavior (no output, refs updated directly)
    +      - Bare repository support (server-side use case)
    +      - Equivalence between traditional pipeline and atomic updates
    +      - Real atomicity using a lock file to verify all-or-nothing guarantee
    +      - Test isolation using test_when_finished to clean up state
    +
    +    The bare repository tests were fixed to rebuild their expectations
    +    independently rather than comparing to previous test output, improving
    +    test reliability and isolation.
    +
    +    A following commit will add a `replay.defaultAction` configuration
    +    option for users who prefer the traditional pipeline output as their
    +    default behavior.
    +
    +    Helped-by: Elijah Newren <newren@gmail.com>
    +    Helped-by: Patrick Steinhardt <ps@pks.im>
    +    Helped-by: Christian Couder <christian.couder@gmail.com>
    +    Helped-by: Phillip Wood <phillip.wood123@gmail.com>
         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Documentation/git-replay.adoc ##
    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne
      --------
      [verse]
     -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
    -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--output-commands | --allow-partial] <revision-range>...
    ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>)
    ++		[--update-refs[=<mode>]] <revision-range>...
      
      DESCRIPTION
      -----------
    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne
     -the working tree and the index untouched, and updates no references.
     -The output of this command is meant to be used as input to
     -`git update-ref --stdin`, which would update the relevant branches
    --(see the OUTPUT section below).
    -+the working tree and the index untouched, and by default updates the
    -+relevant references using atomic transactions. Use `--output-commands`
    -+to get the old default behavior where update commands that can be piped
    -+to `git update-ref --stdin` are emitted (see the OUTPUT section below).
    ++the working tree and the index untouched. By default, updates the
    ++relevant references using an atomic transaction (all refs update or
    ++none). Use `--update-refs=print` to avoid automatic ref updates and
    ++instead get update commands that can be piped to `git update-ref --stdin`
    + (see the OUTPUT section below).
      
      THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
    +@@ Documentation/git-replay.adoc: OPTIONS
    + 	Starting point at which to create the new commits.  May be any
    + 	valid commit, and not just an existing branch name.
    + +
    +-When `--onto` is specified, the update-ref command(s) in the output will
    +-update the branch(es) in the revision range to point at the new
    +-commits, similar to the way how `git rebase --update-refs` updates
    +-multiple branches in the affected range.
    ++When `--onto` is specified, the branch(es) in the revision range will be
    ++updated to point at the new commits (or update commands will be printed
    ++if `--update-refs=print` is used), similar to the way how
    ++`git rebase --update-refs` updates multiple branches in the affected range.
    + 
    + --advance <branch>::
    + 	Starting point at which to create the new commits; must be a
    + 	branch name.
    + +
    +-When `--advance` is specified, the update-ref command(s) in the output
    +-will update the branch passed as an argument to `--advance` to point at
    +-the new commits (in other words, this mimics a cherry-pick operation).
    ++When `--advance` is specified, the branch passed as an argument will be
    ++updated to point at the new commits (or an update command will be printed
    ++if `--update-refs=print` is used). This mimics a cherry-pick operation.
    ++
    ++--update-refs[=<mode>]::
    ++	Control how references are updated. The mode can be:
    +++
    ++--
    ++* `yes` (default): Update refs directly using an atomic transaction.
    ++  All ref updates succeed or all fail.
    ++* `print`: Output update-ref commands instead of updating refs.
    ++  The output can be piped as-is to `git update-ref --stdin`.
    ++--
      
    -@@ Documentation/git-replay.adoc: When `--advance` is specified, the update-ref command(s) in the output
    - will update the branch passed as an argument to `--advance` to point at
    - the new commits (in other words, this mimics a cherry-pick operation).
    - 
    -+--output-commands::
    -+	Output update-ref commands instead of updating refs directly.
    -+	When this option is used, the output can be piped to `git update-ref --stdin`
    -+	for successive, relatively slow, ref updates. This is equivalent to the
    -+	old default behavior.
    -+
    -+--allow-partial::
    -+	Allow some ref updates to succeed even if others fail. By default,
    -+	ref updates are atomic (all succeed or all fail). With this option,
    -+	failed updates are reported as warnings rather than causing the entire
    -+	command to fail. The command exits with code 0 only if all updates
    -+	succeed; any failures result in exit code 1. Cannot be used with
    -+	`--output-commands`.
    -+
      <revision-range>::
      	Range of commits to replay. More than one <revision-range> can
    - 	be passed, but in `--advance <branch>` mode, they should have
     @@ Documentation/git-replay.adoc: include::rev-list-options.adoc[]
      OUTPUT
      ------
    @@ Documentation/git-replay.adoc: include::rev-list-options.adoc[]
     -When there are no conflicts, the output of this command is usable as
     -input to `git update-ref --stdin`.  It is of the form:
     +By default, when there are no conflicts, this command updates the relevant
    -+references using atomic transactions and produces no output. All ref updates
    -+succeed or all fail (atomic behavior). Use `--allow-partial` to allow some
    -+updates to succeed while others fail.
    ++references using an atomic transaction and produces no output. All ref
    ++updates succeed or all fail.
     +
    -+When `--output-commands` is used, the output is usable as input to
    ++When `--update-refs=print` is used, the output is usable as input to
     +`git update-ref --stdin`. It is of the form:
      
      	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
    @@ Documentation/git-replay.adoc: is something other than 0 or 1.
     +updates mybranch to point at the new commits and the second updates
     +target to point at them.
     +
    -+To get the old default behavior where update commands are emitted:
    ++To get the traditional pipeline output:
     +
     +------------
    -+$ git replay --output-commands --onto target origin/main..mybranch
    ++$ git replay --update-refs=print --onto target origin/main..mybranch
     +update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
    -+------------
    -+
    -+To rebase multiple branches with partial failure tolerance:
    -+
    -+------------
    -+$ git replay --allow-partial --contained --onto origin/main origin/main..tipbranch
     +------------
      
      What if you have a stack of branches, one depending upon another, and
    @@ Documentation/git-replay.adoc: is something other than 0 or 1.
      
      ------------
      $ git replay --contained --onto origin/main origin/main..tipbranch
    -+------------
    -+
    +-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
    +-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
    +-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
    + ------------
    + 
     +This automatically finds and rebases all branches contained within the
     +`origin/main..tipbranch` range.
     +
    -+Or if you want to see the old default behavior where update commands are emitted:
    -+
    -+------------
    -+$ git replay --output-commands --contained --onto origin/main origin/main..tipbranch
    - update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
    - update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
    - update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
    -@@ Documentation/git-replay.adoc: update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
    - 
      When calling `git replay`, one does not need to specify a range of
    - commits to replay using the syntax `A..B`; any range expression will
    +-commits to replay using the syntax `A..B`; any range expression will
     -do:
    -+do. Here's an example where you explicitly specify which branches to rebase:
    ++commits to replay using the syntax `A..B`; any range expression will do:
      
      ------------
      $ git replay --onto origin/main ^base branch1 branch2 branch3
    -+------------
    -+
    -+This gives you explicit control over exactly which branches are rebased,
    -+unlike the previous `--contained` example which automatically discovers them.
    -+
    -+To see the update commands that would be executed:
    -+
    -+------------
    -+$ git replay --output-commands --onto origin/main ^base branch1 branch2 branch3
    - update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
    - update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
    - update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
    +-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
    +-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
    +-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
    + ------------
    + 
    + This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
     
      ## builtin/replay.c ##
     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *repo,
      	return create_commit(repo, result->tree, pickme, replayed_base);
      }
      
    -+static int add_ref_to_transaction(struct ref_transaction *transaction,
    -+				  const char *refname,
    -+				  const struct object_id *new_oid,
    -+				  const struct object_id *old_oid,
    -+				  struct strbuf *err)
    ++static int handle_ref_update(const char *mode,
    ++			     struct ref_transaction *transaction,
    ++			     const char *refname,
    ++			     const struct object_id *new_oid,
    ++			     const struct object_id *old_oid,
    ++			     struct strbuf *err)
     +{
    ++	if (!strcmp(mode, "print")) {
    ++		printf("update %s %s %s\n",
    ++		       refname,
    ++		       oid_to_hex(new_oid),
    ++		       oid_to_hex(old_oid));
    ++		return 0;
    ++	}
    ++
    ++	/* mode == "yes" - update refs directly */
     +	return ref_transaction_update(transaction, refname, new_oid, old_oid,
     +				      NULL, NULL, 0, "git replay", err);
     +}
    -+
    -+static void print_rejected_update(const char *refname,
    -+				  const struct object_id *old_oid UNUSED,
    -+				  const struct object_id *new_oid UNUSED,
    -+				  const char *old_target UNUSED,
    -+				  const char *new_target UNUSED,
    -+				  enum ref_transaction_error err,
    -+				  void *cb_data UNUSED)
    -+{
    -+	const char *reason = ref_transaction_error_msg(err);
    -+	warning(_("failed to update %s: %s"), refname, reason);
    -+}
     +
      int cmd_replay(int argc,
      	       const char **argv,
    @@ builtin/replay.c: int cmd_replay(int argc,
      	struct commit *onto = NULL;
      	const char *onto_name = NULL;
      	int contained = 0;
    -+	int output_commands = 0;
    -+	int allow_partial = 0;
    ++	const char *update_refs_mode = NULL;
      
      	struct rev_info revs;
      	struct commit *last_commit = NULL;
    @@ builtin/replay.c: int cmd_replay(int argc,
      	kh_oid_map_t *replayed_commits;
     +	struct ref_transaction *transaction = NULL;
     +	struct strbuf transaction_err = STRBUF_INIT;
    -+	int commits_processed = 0;
      	int ret = 0;
      
     -	const char * const replay_usage[] = {
    @@ builtin/replay.c: int cmd_replay(int argc,
      		N_("(EXPERIMENTAL!) git replay "
      		   "([--contained] --onto <newbase> | --advance <branch>) "
     -		   "<revision-range>..."),
    -+		   "[--output-commands | --allow-partial] <revision-range>..."),
    ++		   "[--update-refs[=<mode>]] <revision-range>..."),
      		NULL
      	};
      	struct option replay_options[] = {
    @@ builtin/replay.c: int cmd_replay(int argc,
      			   N_("replay onto given commit")),
      		OPT_BOOL(0, "contained", &contained,
      			 N_("advance all branches contained in revision-range")),
    -+		OPT_BOOL(0, "output-commands", &output_commands,
    -+			 N_("output update commands instead of updating refs")),
    -+		OPT_BOOL(0, "allow-partial", &allow_partial,
    -+			 N_("allow some ref updates to succeed even if others fail")),
    ++		OPT_STRING(0, "update-refs", &update_refs_mode,
    ++			   N_("mode"),
    ++			   N_("control ref update behavior (yes|print)")),
      		OPT_END()
      	};
      
     @@ builtin/replay.c: int cmd_replay(int argc,
    - 		usage_with_options(replay_usage, replay_options);
    - 	}
    + 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
    + 				  contained, "--contained");
      
    --	if (advance_name_opt && contained)
    --		die(_("options '%s' and '%s' cannot be used together"),
    --		    "--advance", "--contained");
    -+	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
    -+				  contained, "--contained");
    ++	/* Set default mode if not specified */
    ++	if (!update_refs_mode)
    ++		update_refs_mode = "yes";
     +
    -+	die_for_incompatible_opt2(allow_partial, "--allow-partial",
    -+				  output_commands, "--output-commands");
    ++	/* Validate update-refs mode */
    ++	if (strcmp(update_refs_mode, "yes") && strcmp(update_refs_mode, "print"))
    ++		die(_("invalid value for --update-refs: '%s' (expected 'yes' or 'print')"),
    ++		    update_refs_mode);
     +
      	advance_name = xstrdup_or_null(advance_name_opt);
      
    @@ builtin/replay.c: int cmd_replay(int argc,
      	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
      			      &onto, &update_refs);
      
    -+	if (!output_commands) {
    -+		unsigned int transaction_flags = allow_partial ? REF_TRANSACTION_ALLOW_FAILURE : 0;
    ++	/* Initialize ref transaction if we're updating refs directly */
    ++	if (!strcmp(update_refs_mode, "yes")) {
     +		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
    -+							  transaction_flags,
    -+							  &transaction_err);
    ++							  0, &transaction_err);
     +		if (!transaction) {
    -+			ret = error(_("failed to begin ref transaction: %s"), transaction_err.buf);
    ++			ret = error(_("failed to begin ref transaction: %s"),
    ++				    transaction_err.buf);
     +			goto cleanup;
     +		}
     +	}
    @@ builtin/replay.c: int cmd_replay(int argc,
      	if (!onto) /* FIXME: Should handle replaying down to root commit */
      		die("Replaying down to root commit is not supported yet!");
      
    -@@ builtin/replay.c: int cmd_replay(int argc,
    - 		khint_t pos;
    - 		int hr;
    - 
    -+		commits_processed = 1;
    -+
    - 		if (!commit->parents)
    - 			die(_("replaying down to root commit is not supported yet!"));
    - 		if (commit->parents->next)
     @@ builtin/replay.c: int cmd_replay(int argc,
      			if (decoration->type == DECORATION_REF_LOCAL &&
      			    (contained || strset_contains(update_refs,
    @@ builtin/replay.c: int cmd_replay(int argc,
     -				       decoration->name,
     -				       oid_to_hex(&last_commit->object.oid),
     -				       oid_to_hex(&commit->object.oid));
    -+				if (output_commands) {
    -+					printf("update %s %s %s\n",
    -+					       decoration->name,
    -+					       oid_to_hex(&last_commit->object.oid),
    -+					       oid_to_hex(&commit->object.oid));
    -+				} else if (add_ref_to_transaction(transaction, decoration->name,
    -+								  &last_commit->object.oid,
    -+								  &commit->object.oid,
    -+								  &transaction_err) < 0) {
    -+					ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
    ++				if (handle_ref_update(update_refs_mode, transaction,
    ++						      decoration->name,
    ++						      &last_commit->object.oid,
    ++						      &commit->object.oid,
    ++						      &transaction_err) < 0) {
    ++					ret = error(_("failed to update ref '%s': %s"),
    ++						    decoration->name, transaction_err.buf);
     +					goto cleanup;
     +				}
      			}
    @@ builtin/replay.c: int cmd_replay(int argc,
     -		       advance_name,
     -		       oid_to_hex(&last_commit->object.oid),
     -		       oid_to_hex(&onto->object.oid));
    -+		if (output_commands) {
    -+			printf("update %s %s %s\n",
    -+			       advance_name,
    -+			       oid_to_hex(&last_commit->object.oid),
    -+			       oid_to_hex(&onto->object.oid));
    -+		} else if (add_ref_to_transaction(transaction, advance_name,
    -+						  &last_commit->object.oid,
    -+						  &onto->object.oid,
    -+						  &transaction_err) < 0) {
    -+			ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
    ++		if (handle_ref_update(update_refs_mode, transaction,
    ++				      advance_name,
    ++				      &last_commit->object.oid,
    ++				      &onto->object.oid,
    ++				      &transaction_err) < 0) {
    ++			ret = error(_("failed to update ref '%s': %s"),
    ++				    advance_name, transaction_err.buf);
     +			goto cleanup;
     +		}
     +	}
    @@ builtin/replay.c: int cmd_replay(int argc,
     +	/* Commit the ref transaction if we have one */
     +	if (transaction && result.clean == 1) {
     +		if (ref_transaction_commit(transaction, &transaction_err)) {
    -+			if (allow_partial) {
    -+				warning(_("some ref updates failed: %s"), transaction_err.buf);
    -+				ref_transaction_for_each_rejected_update(transaction,
    -+									 print_rejected_update, NULL);
    -+				ret = 0; /* Set failure even with allow_partial */
    -+			} else {
    -+				ret = error(_("failed to update refs: %s"), transaction_err.buf);
    -+				goto cleanup;
    -+			}
    ++			ret = error(_("failed to commit ref transaction: %s"),
    ++				    transaction_err.buf);
    ++			goto cleanup;
     +		}
      	}
      
      	merge_finalize(&merge_opt, &result);
     @@ builtin/replay.c: int cmd_replay(int argc,
    - 		strset_clear(update_refs);
    - 		free(update_refs);
    - 	}
    --	ret = result.clean;
    -+
    -+	/* Handle empty ranges: if no commits were processed, treat as success */
    -+	if (!commits_processed)
    -+		ret = 1; /* Success - no commits to replay is not an error */
    -+	else
    -+		ret = result.clean;
    + 	ret = result.clean;
      
      cleanup:
     +	if (transaction)
    @@ t/t3650-replay-basics.sh: test_expect_success 'setup bare' '
      
      test_expect_success 'using replay to rebase two branches, one on top of other' '
     -	git replay --onto main topic1..topic2 >result &&
    -+	git replay --output-commands --onto main topic1..topic2 >result &&
    ++	git replay --update-refs=print --onto main topic1..topic2 >result &&
      
      	test_line_count = 1 result &&
      
    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to rebase two branch
      '
      
     +test_expect_success 'using replay with default atomic behavior (no output)' '
    -+	# Create a test branch that wont interfere with others
    -+	git branch atomic-test topic2 &&
    -+	git rev-parse atomic-test >atomic-test-old &&
    ++	# Store the original state
    ++	START=$(git rev-parse topic2) &&
    ++	test_when_finished "git branch -f topic2 $START" &&
     +
     +	# Default behavior: atomic ref updates (no output)
    -+	git replay --onto main topic1..atomic-test >output &&
    ++	git replay --onto main topic1..topic2 >output &&
     +	test_must_be_empty output &&
     +
    -+	# Verify the branch was updated
    -+	git rev-parse atomic-test >atomic-test-new &&
    -+	! test_cmp atomic-test-old atomic-test-new &&
    -+
     +	# Verify the history is correct
    -+	git log --format=%s atomic-test >actual &&
    ++	git log --format=%s topic2 >actual &&
     +	test_write_lines E D M L B A >expect &&
     +	test_cmp expect actual
     +'
     +
      test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
     -	git -C bare replay --onto main topic1..topic2 >result-bare &&
    --	test_cmp expect result-bare
    -+	git -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&
    ++	git -C bare replay --update-refs=print --onto main topic1..topic2 >result-bare &&
    ++
    ++	test_line_count = 1 result-bare &&
    ++
    ++	git log --format=%s $(cut -f 3 -d " " result-bare) >actual &&
    ++	test_write_lines E D M L B A >expect &&
    ++	test_cmp expect actual &&
    ++
    ++	printf "update refs/heads/topic2 " >expect &&
    ++	printf "%s " $(cut -f 3 -d " " result-bare) >>expect &&
    ++	git -C bare rev-parse topic2 >>expect &&
     +
    -+	# The result should match what we got from the regular repo
    -+	test_cmp result result-bare
    + 	test_cmp expect result-bare
      '
      
    - test_expect_success 'using replay to rebase with a conflict' '
     @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to perform basic cherry-pick' '
      	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
      	# 4th field of result is hash for main instead of hash for topic2
      
     -	git replay --advance main topic1..topic2 >result &&
    -+	git replay --output-commands --advance main topic1..topic2 >result &&
    ++	git replay --update-refs=print --advance main topic1..topic2 >result &&
      
      	test_line_count = 1 result &&
      
    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to perform basic che
      
      test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
     -	git -C bare replay --advance main topic1..topic2 >result-bare &&
    -+	git -C bare replay --output-commands --advance main topic1..topic2 >result-bare &&
    ++	git -C bare replay --update-refs=print --advance main topic1..topic2 >result-bare &&
    ++
    ++	test_line_count = 1 result-bare &&
    ++
    ++	git log --format=%s $(cut -f 3 -d " " result-bare) >actual &&
    ++	test_write_lines E D M L B A >expect &&
    ++	test_cmp expect actual &&
    ++
    ++	printf "update refs/heads/main " >expect &&
    ++	printf "%s " $(cut -f 3 -d " " result-bare) >>expect &&
    ++	git -C bare rev-parse main >>expect &&
    ++
      	test_cmp expect result-bare
      '
      
    @@ t/t3650-replay-basics.sh: test_expect_success 'replay fails when both --advance
      
      test_expect_success 'using replay to also rebase a contained branch' '
     -	git replay --contained --onto main main..topic3 >result &&
    -+	git replay --output-commands --contained --onto main main..topic3 >result &&
    ++	git replay --update-refs=print --contained --onto main main..topic3 >result &&
      
      	test_line_count = 2 result &&
      	cut -f 3 -d " " result >new-branch-tips &&
    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to also rebase a con
      
      test_expect_success 'using replay on bare repo to also rebase a contained branch' '
     -	git -C bare replay --contained --onto main main..topic3 >result-bare &&
    -+	git -C bare replay --output-commands --contained --onto main main..topic3 >result-bare &&
    ++	git -C bare replay --update-refs=print --contained --onto main main..topic3 >result-bare &&
    ++
    ++	test_line_count = 2 result-bare &&
    ++	cut -f 3 -d " " result-bare >new-branch-tips &&
    ++
    ++	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
    ++	test_write_lines F C M L B A >expect &&
    ++	test_cmp expect actual &&
    ++
    ++	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
    ++	test_write_lines H G F C M L B A >expect &&
    ++	test_cmp expect actual &&
    ++
    ++	printf "update refs/heads/topic1 " >expect &&
    ++	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
    ++	git -C bare rev-parse topic1 >>expect &&
    ++	printf "update refs/heads/topic3 " >>expect &&
    ++	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
    ++	git -C bare rev-parse topic3 >>expect &&
    ++
      	test_cmp expect result-bare
      '
      
      test_expect_success 'using replay to rebase multiple divergent branches' '
     -	git replay --onto main ^topic1 topic2 topic4 >result &&
    -+	git replay --output-commands --onto main ^topic1 topic2 topic4 >result &&
    ++	git replay --update-refs=print --onto main ^topic1 topic2 topic4 >result &&
      
      	test_line_count = 2 result &&
      	cut -f 3 -d " " result >new-branch-tips &&
    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to rebase multiple d
      
      test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
     -	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
    -+	git -C bare replay --output-commands --contained --onto main ^main topic2 topic3 topic4 >result &&
    ++	git -C bare replay --update-refs=print --contained --onto main ^main topic2 topic3 topic4 >result &&
      
      	test_line_count = 4 result &&
      	cut -f 3 -d " " result >new-branch-tips &&
    @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '
      		--onto rename-onto rename-onto..rename-from
      '
      
    -+# Tests for new default atomic behavior and options
    -+
    -+test_expect_success 'replay default behavior should not produce output when successful' '
    -+	git replay --onto main topic1..topic3 >output &&
    -+	test_must_be_empty output
    -+'
    -+
    -+test_expect_success 'replay with --output-commands produces traditional output' '
    -+	git replay --output-commands --onto main topic1..topic3 >output &&
    -+	test_line_count = 1 output &&
    -+	grep "^update refs/heads/topic3 " output
    -+'
    -+
    -+test_expect_success 'replay with --allow-partial should not produce output when successful' '
    -+	git replay --allow-partial --onto main topic1..topic3 >output &&
    -+	test_must_be_empty output
    -+'
    -+
    -+test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '
    -+	test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
    -+	grep "cannot be used together" error
    -+'
    ++# Tests for atomic ref update behavior
     +
     +test_expect_success 'replay with --contained updates multiple branches atomically' '
    -+	# Create fresh test branches based on the original structure
    -+	# contained-topic1 should be contained within the range to contained-topic3
    -+	git branch contained-base main &&
    -+	git checkout -b contained-topic1 contained-base &&
    -+	test_commit ContainedC &&
    -+	git checkout -b contained-topic3 contained-topic1 &&
    -+	test_commit ContainedG &&
    -+	test_commit ContainedH &&
    -+	git checkout main &&
    -+
     +	# Store original states
    -+	git rev-parse contained-topic1 >contained-topic1-old &&
    -+	git rev-parse contained-topic3 >contained-topic3-old &&
    -+
    -+	# Use --contained to update multiple branches - this should update both
    -+	git replay --contained --onto main contained-base..contained-topic3 &&
    -+
    -+	# Verify both branches were updated
    -+	git rev-parse contained-topic1 >contained-topic1-new &&
    -+	git rev-parse contained-topic3 >contained-topic3-new &&
    -+	! test_cmp contained-topic1-old contained-topic1-new &&
    -+	! test_cmp contained-topic3-old contained-topic3-new
    -+'
    ++	START_TOPIC1=$(git rev-parse topic1) &&
    ++	START_TOPIC3=$(git rev-parse topic3) &&
    ++	test_when_finished "git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3" &&
     +
    -+test_expect_success 'replay atomic behavior: all refs updated or none' '
    -+	# Store original state
    -+	git rev-parse topic4 >topic4-old &&
    -+
    -+	# Default atomic behavior
    -+	git replay --onto main main..topic4 &&
    ++	# Use --contained to update multiple branches
    ++	git replay --contained --onto main main..topic3 >output &&
    ++	test_must_be_empty output &&
     +
    -+	# Verify ref was updated
    -+	git rev-parse topic4 >topic4-new &&
    -+	! test_cmp topic4-old topic4-new &&
    ++	# Verify both branches were updated with correct commit sequences
    ++	git log --format=%s topic1 >actual &&
    ++	test_write_lines F C M L B A >expect &&
    ++	test_cmp expect actual &&
     +
    -+	# Verify no partial state
    -+	git log --format=%s topic4 >actual &&
    -+	test_write_lines J I M L B A >expect &&
    ++	git log --format=%s topic3 >actual &&
    ++	test_write_lines H G F C M L B A >expect &&
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success 'replay works correctly with bare repositories' '
    -+	# Test atomic behavior in bare repo (important for Gitaly)
    -+	git checkout -b bare-test topic1 &&
    -+	test_commit BareTest &&
    ++test_expect_success 'replay atomic guarantee: all refs updated or none' '
    ++	# Store original states
    ++	START_TOPIC1=$(git rev-parse topic1) &&
    ++	START_TOPIC3=$(git rev-parse topic3) &&
    ++	test_when_finished "git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3 && rm -f .git/refs/heads/topic1.lock" &&
     +
    -+	# Test with bare repo - replay the commits from main..bare-test to get the full history
    -+	git -C bare fetch .. bare-test:bare-test &&
    -+	git -C bare replay --onto main main..bare-test &&
    ++	# Create a lock on topic1 to simulate a concurrent update
    ++	>.git/refs/heads/topic1.lock &&
     +
    -+	# Verify the bare repo was updated correctly (no output)
    -+	git -C bare log --format=%s bare-test >actual &&
    -+	test_write_lines BareTest F C M L B A >expect &&
    -+	test_cmp expect actual
    -+'
    ++	# Try to update multiple branches with --contained
    ++	# This should fail atomically - neither branch should be updated
    ++	test_must_fail git replay --contained --onto main main..topic3 2>error &&
     +
    -+test_expect_success 'replay --allow-partial with no failures produces no output' '
    -+	git checkout -b partial-test topic1 &&
    -+	test_commit PartialTest &&
    ++	# Verify the transaction failed
    ++	grep "failed to commit ref transaction" error &&
     +
    -+	# Should succeed silently even with partial mode
    -+	git replay --allow-partial --onto main topic1..partial-test >output &&
    -+	test_must_be_empty output
    ++	# Verify NEITHER branch was updated (all-or-nothing guarantee)
    ++	test_cmp_rev $START_TOPIC1 topic1 &&
    ++	test_cmp_rev $START_TOPIC3 topic3
     +'
     +
    -+test_expect_success 'replay maintains ref update consistency' '
    -+	# Test that traditional vs atomic produce equivalent results
    -+	git checkout -b method1-test topic2 &&
    -+	git checkout -b method2-test topic2 &&
    ++test_expect_success 'traditional pipeline and atomic update produce equivalent results' '
    ++	# Store original states
    ++	START_TOPIC2=$(git rev-parse topic2) &&
    ++	test_when_finished "git branch -f topic2 $START_TOPIC2" &&
     +
    -+	# Both methods should update refs to point to the same replayed commits
    -+	git replay --output-commands --onto main topic1..method1-test >update-commands &&
    ++	# Traditional method: output commands and pipe to update-ref
    ++	git replay --update-refs=print --onto main topic1..topic2 >update-commands &&
     +	git update-ref --stdin <update-commands &&
    -+	git log --format=%s method1-test >traditional-result &&
    ++	git log --format=%s topic2 >traditional-result &&
    ++
    ++	# Reset topic2
    ++	git branch -f topic2 $START_TOPIC2 &&
     +
    -+	# Direct atomic method should produce same commit history
    -+	git replay --onto main topic1..method2-test &&
    -+	git log --format=%s method2-test >atomic-result &&
    ++	# Atomic method: direct ref updates
    ++	git replay --onto main topic1..topic2 &&
    ++	git log --format=%s topic2 >atomic-result &&
     +
     +	# Both methods should produce identical commit histories
     +	test_cmp traditional-result atomic-result
     +'
     +
    -+test_expect_success 'replay error messages are helpful and clear' '
    -+	# Test that error messages are clear
    -+	test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
    -+	grep "cannot be used together" error
    -+'
    -+
    -+test_expect_success 'replay with empty range produces no output and no changes' '
    -+	# Create a test branch for empty range testing
    -+	git checkout -b empty-test topic1 &&
    -+	git rev-parse empty-test >empty-test-before &&
    -+
    -+	# Empty range should succeed but do nothing
    -+	git replay --onto main empty-test..empty-test >output &&
    ++test_expect_success 'replay works correctly with bare repositories' '
    ++	# Test atomic behavior in bare repo
    ++	git -C bare fetch .. topic1:bare-test-branch &&
    ++	git -C bare replay --onto main main..bare-test-branch >output &&
     +	test_must_be_empty output &&
     +
    -+	# Branch should be unchanged
    -+	git rev-parse empty-test >empty-test-after &&
    -+	test_cmp empty-test-before empty-test-after
    ++	# Verify the bare repo was updated correctly
    ++	git -C bare log --format=%s bare-test-branch >actual &&
    ++	test_write_lines F C M L B A >expect &&
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success 'replay validates --update-refs mode values' '
    ++	test_must_fail git replay --update-refs=invalid --onto main topic1..topic2 2>error &&
    ++	grep "invalid value for --update-refs" error
     +'
     +
      test_done
-:  ---------- > 3:  710ab27ae3 replay: add replay.defaultAction config option
-- 
2.51.0


Siddharth Asthana (3):
  replay: use die_for_incompatible_opt2() for option validation
  replay: make atomic ref updates the default behavior
  replay: add replay.defaultAction config option

 Documentation/config/replay.adoc |  14 +++
 Documentation/git-replay.adoc    |  71 ++++++-----
 builtin/replay.c                 | 108 +++++++++++++++--
 t/t3650-replay-basics.sh         | 198 +++++++++++++++++++++++++++++--
 4 files changed, 343 insertions(+), 48 deletions(-)
 create mode 100644 Documentation/config/replay.adoc

base-commit: 4b71b294773cc4f7fe48ec3a70079aa8783f373d

Thanks
- Siddharth

^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v3 0/3] replay: make atomic ref updates the default
  2025-09-26 23:08 ` [PATCH v2 0/1] replay: make atomic ref updates the default behavior Siddharth Asthana
  2025-09-26 23:08   ` [PATCH v2 1/1] " Siddharth Asthana
  2025-10-02 17:14   ` [PATCH v2 0/1] " Kristoffer Haugsbakk
@ 2025-10-13 18:33   ` Siddharth Asthana
  2025-10-13 18:33     ` [PATCH v3 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
                       ` (4 more replies)
  2 siblings, 5 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-13 18:33 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

This is v3 of the git-replay atomic updates series.

Based on feedback from v2, this version simplifies the API and improves
extensibility. Thanks to Elijah, Phillip, Christian, Junio, and Karthik
for the detailed reviews that shaped this version.

## Changes Since v2

**Removed --allow-partial option**

After discussion with Elijah and Junio, we couldn't identify a concrete
use case for partial failure tolerance. The traditional pipeline with
git-update-ref already provides partial update capabilities when needed
through its transaction commands. Removing this option simplifies the API
and avoids committing to behavior without clear real-world use cases.

**Changed to --update-refs=<mode> for extensibility**

Phillip suggested that separate boolean flags (--output-commands,
--allow-partial) were limiting for future expansion. The --update-refs=<mode>
design allows future modes without option proliferation:
  - --update-refs=yes (default): atomic ref updates
  - --update-refs=print: pipeline output
  - Future modes can be added as additional values

This API pattern prevents the need for multiple incompatible flags and
provides a cleaner interface for users.

**Added replay.defaultAction configuration option**

Junio recommended a config option for users preferring traditional behavior.
The implementation uses enum string values for extensibility:
  - replay.defaultAction = update-refs (default)
  - replay.defaultAction = show-commands (pipeline output)

The command-line --update-refs option overrides the config, allowing users
to set a preference while maintaining per-invocation control. The enum
design (versus a boolean) allows future expansion to additional modes
without requiring new config variables.

**Improved commit messages and patch organization**

Christian and Elijah provided detailed feedback on commit message structure.
Patch 2 now uses Elijah's suggested format that explains the trade-offs of
the current design before proposing changes. The commit messages now focus
on the changes themselves rather than v1→v2 evolution. Added Helped-by
trailers to acknowledge specific contributions.

**Enhanced test suite with proper isolation**

Following Elijah's suggestions:
  - Existing tests use --update-refs=print to preserve their behavior
  - New tests use test_when_finished for proper cleanup
  - Added real atomicity test using lock files to verify all-or-nothing
  - Fixed bare repository tests to rebuild expectations independently
  - Removed weak tests that didn't actually verify atomicity

**Extracted helper function to reduce duplication**

Per Phillip's feedback, added handle_ref_update() helper to eliminate
code duplication between print and atomic modes. This function takes a
mode parameter and handles both cases, making the code more maintainable
and ensuring both paths stay consistent.

## Technical Implementation

The atomic ref updates use Git's ref transaction API:
  - ref_store_transaction_begin() with default atomic behavior
  - ref_transaction_update() to stage each update
  - ref_transaction_commit() for atomic application

The handle_ref_update() helper encapsulates the mode-specific logic,
either printing update commands or staging them into the transaction.

Config reading uses repo_config_get_string_tmp() with validation for
'update-refs' and 'show-commands' values, mapping them to internal
modes 'yes' and 'print' respectively.

Range-diff against v2:
-:  ---------- > 1:  de9cc3fbee replay: use die_for_incompatible_opt2() for option validation
1:  e3c1a57375 ! 2:  3f4c69d612 replay: make atomic ref updates the default behavior
    @@ Metadata
      ## Commit message ##
         replay: make atomic ref updates the default behavior
     
    -    The git replay command currently outputs update commands that must be
    -    piped to git update-ref --stdin to actually update references:
    +    The git replay command currently outputs update commands that can be
    +    piped to update-ref to achieve a rebase, e.g.
     
    -        git replay --onto main topic1..topic2 | git update-ref --stdin
    +      git replay --onto main topic1..topic2 | git update-ref --stdin
     
    -    This design has significant limitations for server-side operations. The
    -    two-command pipeline creates coordination complexity, provides no atomic
    -    transaction guarantees by default, and complicates automation in bare
    -    repository environments where git replay is primarily used.
    +    This separation had advantages for three special cases:
    +      * it made testing easy (when state isn't modified from one step to
    +        the next, you don't need to make temporary branches or have undo
    +        commands, or try to track the changes)
    +      * it provided a natural can-it-rebase-cleanly (and what would it
    +        rebase to) capability without automatically updating refs, similar
    +        to a --dry-run
    +      * it provided a natural low-level tool for the suite of hash-object,
    +        mktree, commit-tree, mktag, merge-tree, and update-ref, allowing
    +        users to have another building block for experimentation and making
    +        new tools
     
    -    During extensive mailing list discussion, multiple maintainers identified
    -    that the current approach forces users to opt-in to atomic behavior rather
    -    than defaulting to the safer, more reliable option. Elijah Newren noted
    -    that the experimental status explicitly allows such behavior changes, while
    -    Patrick Steinhardt highlighted performance concerns with individual ref
    -    updates in the reftable backend.
    +    However, it should be noted that all three of these are somewhat
    +    special cases; users, whether on the client or server side, would
    +    almost certainly find it more ergonomical to simply have the updating
    +    of refs be the default.
     
    -    The core issue is that git replay was designed around command output rather
    -    than direct action. This made sense for a plumbing tool, but creates barriers
    -    for the primary use case: server-side operations that need reliable, atomic
    -    ref updates without pipeline complexity.
    +    For server-side operations in particular, the pipeline architecture
    +    creates process coordination overhead. Server implementations that need
    +    to perform rebases atomically must maintain additional code to:
     
    -    This patch changes the default behavior to update refs directly using Git's
    -    ref transaction API:
    +      1. Spawn and manage a pipeline between git-replay and git-update-ref
    +      2. Coordinate stdout/stderr streams across the pipe boundary
    +      3. Handle partial failure states if the pipeline breaks mid-execution
    +      4. Parse and validate the update-ref command output
     
    -        git replay --onto main topic1..topic2
    -        # No output; all refs updated atomically or none
    +    Change the default behavior to update refs directly, and atomically (at
    +    least to the extent supported by the refs backend in use). This
    +    eliminates the process coordination overhead for the common case.
     
    -    The implementation uses ref_store_transaction_begin() with atomic mode by
    -    default, ensuring all ref updates succeed or all fail as a single operation.
    -    This leverages git replay's existing server-side strengths (in-memory operation,
    -    no work tree requirement) while adding the atomic guarantees that server
    -    operations require.
    +    For users needing the traditional pipeline workflow, add a new
    +    `--update-refs=<mode>` option that preserves the original behavior:
     
    -    For users needing the traditional pipeline workflow, --output-commands
    -    preserves the original behavior:
    +      git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin
     
    -        git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin
    -
    -    The --allow-partial option enables partial failure tolerance. However, following
    -    maintainer feedback, it implements a "strict success" model: the command exits
    -    with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY
    -    updates fail. This ensures that --allow-partial changes error reporting style
    -    (warnings vs hard errors) but not success criteria, handling edge cases like
    -    "no updates needed" cleanly.
    +    The mode can be:
    +      * `yes` (default): Update refs directly using an atomic transaction
    +      * `print`: Output update-ref commands for pipeline use
     
         Implementation details:
    -    - Empty commit ranges now return success (exit code 0) rather than failure,
    -      as no commits to replay is a valid successful operation
    -    - Added comprehensive test coverage with 12 new tests covering atomic behavior,
    -      option validation, bare repository support, and edge cases
    -    - Fixed test isolation issues to prevent branch state contamination between tests
    -    - Maintains C89 compliance and follows Git's established coding conventions
    -    - Refactored option validation to use die_for_incompatible_opt2() for both
    -      --advance/--contained and --allow-partial/--output-commands conflicts,
    -      providing consistent error reporting
    -    - Fixed --allow-partial exit code behavior to implement "strict success" model
    -      where any ref update failures result in exit code 1, even with partial tolerance
    -    - Updated documentation with proper line wrapping, consistent terminology using
    -      "old default behavior", performance context, and reorganized examples for clarity
    -    - Eliminates individual ref updates (refs_update_ref calls) that perform
    -      poorly with reftable backend
    -    - Uses only batched ref transactions for optimal performance across all
    -      ref backends
    -    - Avoids naming collision with git rebase --update-refs by using distinct
    -      option names
    -    - Defaults to atomic behavior while preserving pipeline compatibility
     
    -    The result is a command that works better for its primary use case (server-side
    -    operations) while maintaining full backward compatibility for existing workflows.
    +    The atomic ref updates are implemented using Git's ref transaction API.
    +    In cmd_replay(), when not in 'print' mode, we initialize a transaction
    +    using ref_store_transaction_begin() with the default atomic behavior.
    +    As commits are replayed, ref updates are staged into the transaction
    +    using ref_transaction_update(). Finally, ref_transaction_commit()
    +    applies all updates atomically—either all updates succeed or none do.
    +
    +    To avoid code duplication between the 'print' and 'yes' modes, this
    +    commit extracts a handle_ref_update() helper function. This function
    +    takes the mode and either prints the update command or stages it into
    +    the transaction. This keeps both code paths consistent and makes future
    +    maintenance easier.
    +
    +    The helper function signature:
    +
    +      static int handle_ref_update(const char *mode,
    +                                    struct ref_transaction *transaction,
    +                                    const char *refname,
    +                                    const struct object_id *new_oid,
    +                                    const struct object_id *old_oid,
    +                                    struct strbuf *err)
    +
    +    When mode is 'print', it prints the update-ref command. When mode is
    +    'yes', it calls ref_transaction_update() to stage the update. This
    +    eliminates the duplication that would otherwise exist at each ref update
    +    call site.
    +
    +    Test suite changes:
     
    +    All existing tests that expected command output now use
    +    `--update-refs=print` to preserve their original behavior. This keeps
    +    the tests valid while allowing them to verify that the pipeline workflow
    +    still works correctly.
    +
    +    New tests were added to verify:
    +      - Default atomic behavior (no output, refs updated directly)
    +      - Bare repository support (server-side use case)
    +      - Equivalence between traditional pipeline and atomic updates
    +      - Real atomicity using a lock file to verify all-or-nothing guarantee
    +      - Test isolation using test_when_finished to clean up state
    +
    +    The bare repository tests were fixed to rebuild their expectations
    +    independently rather than comparing to previous test output, improving
    +    test reliability and isolation.
    +
    +    A following commit will add a `replay.defaultAction` configuration
    +    option for users who prefer the traditional pipeline output as their
    +    default behavior.
    +
    +    Helped-by: Elijah Newren <newren@gmail.com>
    +    Helped-by: Patrick Steinhardt <ps@pks.im>
    +    Helped-by: Christian Couder <christian.couder@gmail.com>
    +    Helped-by: Phillip Wood <phillip.wood123@gmail.com>
         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Documentation/git-replay.adoc ##
    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne
      --------
      [verse]
     -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
    -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--output-commands | --allow-partial] <revision-range>...
    ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>)
    ++		[--update-refs[=<mode>]] <revision-range>...
      
      DESCRIPTION
      -----------
    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne
     -the working tree and the index untouched, and updates no references.
     -The output of this command is meant to be used as input to
     -`git update-ref --stdin`, which would update the relevant branches
    --(see the OUTPUT section below).
    -+the working tree and the index untouched, and by default updates the
    -+relevant references using atomic transactions. Use `--output-commands`
    -+to get the old default behavior where update commands that can be piped
    -+to `git update-ref --stdin` are emitted (see the OUTPUT section below).
    ++the working tree and the index untouched. By default, updates the
    ++relevant references using an atomic transaction (all refs update or
    ++none). Use `--update-refs=print` to avoid automatic ref updates and
    ++instead get update commands that can be piped to `git update-ref --stdin`
    + (see the OUTPUT section below).
      
      THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
    +@@ Documentation/git-replay.adoc: OPTIONS
    + 	Starting point at which to create the new commits.  May be any
    + 	valid commit, and not just an existing branch name.
    + +
    +-When `--onto` is specified, the update-ref command(s) in the output will
    +-update the branch(es) in the revision range to point at the new
    +-commits, similar to the way how `git rebase --update-refs` updates
    +-multiple branches in the affected range.
    ++When `--onto` is specified, the branch(es) in the revision range will be
    ++updated to point at the new commits (or update commands will be printed
    ++if `--update-refs=print` is used), similar to the way how
    ++`git rebase --update-refs` updates multiple branches in the affected range.
    + 
    + --advance <branch>::
    + 	Starting point at which to create the new commits; must be a
    + 	branch name.
    + +
    +-When `--advance` is specified, the update-ref command(s) in the output
    +-will update the branch passed as an argument to `--advance` to point at
    +-the new commits (in other words, this mimics a cherry-pick operation).
    ++When `--advance` is specified, the branch passed as an argument will be
    ++updated to point at the new commits (or an update command will be printed
    ++if `--update-refs=print` is used). This mimics a cherry-pick operation.
    ++
    ++--update-refs[=<mode>]::
    ++	Control how references are updated. The mode can be:
    +++
    ++--
    ++* `yes` (default): Update refs directly using an atomic transaction.
    ++  All ref updates succeed or all fail.
    ++* `print`: Output update-ref commands instead of updating refs.
    ++  The output can be piped as-is to `git update-ref --stdin`.
    ++--
      
    -@@ Documentation/git-replay.adoc: When `--advance` is specified, the update-ref command(s) in the output
    - will update the branch passed as an argument to `--advance` to point at
    - the new commits (in other words, this mimics a cherry-pick operation).
    - 
    -+--output-commands::
    -+	Output update-ref commands instead of updating refs directly.
    -+	When this option is used, the output can be piped to `git update-ref --stdin`
    -+	for successive, relatively slow, ref updates. This is equivalent to the
    -+	old default behavior.
    -+
    -+--allow-partial::
    -+	Allow some ref updates to succeed even if others fail. By default,
    -+	ref updates are atomic (all succeed or all fail). With this option,
    -+	failed updates are reported as warnings rather than causing the entire
    -+	command to fail. The command exits with code 0 only if all updates
    -+	succeed; any failures result in exit code 1. Cannot be used with
    -+	`--output-commands`.
    -+
      <revision-range>::
      	Range of commits to replay. More than one <revision-range> can
    - 	be passed, but in `--advance <branch>` mode, they should have
     @@ Documentation/git-replay.adoc: include::rev-list-options.adoc[]
      OUTPUT
      ------
    @@ Documentation/git-replay.adoc: include::rev-list-options.adoc[]
     -When there are no conflicts, the output of this command is usable as
     -input to `git update-ref --stdin`.  It is of the form:
     +By default, when there are no conflicts, this command updates the relevant
    -+references using atomic transactions and produces no output. All ref updates
    -+succeed or all fail (atomic behavior). Use `--allow-partial` to allow some
    -+updates to succeed while others fail.
    ++references using an atomic transaction and produces no output. All ref
    ++updates succeed or all fail.
     +
    -+When `--output-commands` is used, the output is usable as input to
    ++When `--update-refs=print` is used, the output is usable as input to
     +`git update-ref --stdin`. It is of the form:
      
      	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
    @@ Documentation/git-replay.adoc: is something other than 0 or 1.
     +updates mybranch to point at the new commits and the second updates
     +target to point at them.
     +
    -+To get the old default behavior where update commands are emitted:
    ++To get the traditional pipeline output:
     +
     +------------
    -+$ git replay --output-commands --onto target origin/main..mybranch
    ++$ git replay --update-refs=print --onto target origin/main..mybranch
     +update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
    -+------------
    -+
    -+To rebase multiple branches with partial failure tolerance:
    -+
    -+------------
    -+$ git replay --allow-partial --contained --onto origin/main origin/main..tipbranch
     +------------
      
      What if you have a stack of branches, one depending upon another, and
    @@ Documentation/git-replay.adoc: is something other than 0 or 1.
      
      ------------
      $ git replay --contained --onto origin/main origin/main..tipbranch
    -+------------
    -+
    +-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
    +-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
    +-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
    + ------------
    + 
     +This automatically finds and rebases all branches contained within the
     +`origin/main..tipbranch` range.
     +
    -+Or if you want to see the old default behavior where update commands are emitted:
    -+
    -+------------
    -+$ git replay --output-commands --contained --onto origin/main origin/main..tipbranch
    - update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
    - update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
    - update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
    -@@ Documentation/git-replay.adoc: update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
    - 
      When calling `git replay`, one does not need to specify a range of
    - commits to replay using the syntax `A..B`; any range expression will
    +-commits to replay using the syntax `A..B`; any range expression will
     -do:
    -+do. Here's an example where you explicitly specify which branches to rebase:
    ++commits to replay using the syntax `A..B`; any range expression will do:
      
      ------------
      $ git replay --onto origin/main ^base branch1 branch2 branch3
    -+------------
    -+
    -+This gives you explicit control over exactly which branches are rebased,
    -+unlike the previous `--contained` example which automatically discovers them.
    -+
    -+To see the update commands that would be executed:
    -+
    -+------------
    -+$ git replay --output-commands --onto origin/main ^base branch1 branch2 branch3
    - update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
    - update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
    - update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
    +-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
    +-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
    +-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
    + ------------
    + 
    + This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
     
      ## builtin/replay.c ##
     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *repo,
      	return create_commit(repo, result->tree, pickme, replayed_base);
      }
      
    -+static int add_ref_to_transaction(struct ref_transaction *transaction,
    -+				  const char *refname,
    -+				  const struct object_id *new_oid,
    -+				  const struct object_id *old_oid,
    -+				  struct strbuf *err)
    ++static int handle_ref_update(const char *mode,
    ++			     struct ref_transaction *transaction,
    ++			     const char *refname,
    ++			     const struct object_id *new_oid,
    ++			     const struct object_id *old_oid,
    ++			     struct strbuf *err)
     +{
    ++	if (!strcmp(mode, "print")) {
    ++		printf("update %s %s %s\n",
    ++		       refname,
    ++		       oid_to_hex(new_oid),
    ++		       oid_to_hex(old_oid));
    ++		return 0;
    ++	}
    ++
    ++	/* mode == "yes" - update refs directly */
     +	return ref_transaction_update(transaction, refname, new_oid, old_oid,
     +				      NULL, NULL, 0, "git replay", err);
     +}
    -+
    -+static void print_rejected_update(const char *refname,
    -+				  const struct object_id *old_oid UNUSED,
    -+				  const struct object_id *new_oid UNUSED,
    -+				  const char *old_target UNUSED,
    -+				  const char *new_target UNUSED,
    -+				  enum ref_transaction_error err,
    -+				  void *cb_data UNUSED)
    -+{
    -+	const char *reason = ref_transaction_error_msg(err);
    -+	warning(_("failed to update %s: %s"), refname, reason);
    -+}
     +
      int cmd_replay(int argc,
      	       const char **argv,
    @@ builtin/replay.c: int cmd_replay(int argc,
      	struct commit *onto = NULL;
      	const char *onto_name = NULL;
      	int contained = 0;
    -+	int output_commands = 0;
    -+	int allow_partial = 0;
    ++	const char *update_refs_mode = NULL;
      
      	struct rev_info revs;
      	struct commit *last_commit = NULL;
    @@ builtin/replay.c: int cmd_replay(int argc,
      	kh_oid_map_t *replayed_commits;
     +	struct ref_transaction *transaction = NULL;
     +	struct strbuf transaction_err = STRBUF_INIT;
    -+	int commits_processed = 0;
      	int ret = 0;
      
     -	const char * const replay_usage[] = {
    @@ builtin/replay.c: int cmd_replay(int argc,
      		N_("(EXPERIMENTAL!) git replay "
      		   "([--contained] --onto <newbase> | --advance <branch>) "
     -		   "<revision-range>..."),
    -+		   "[--output-commands | --allow-partial] <revision-range>..."),
    ++		   "[--update-refs[=<mode>]] <revision-range>..."),
      		NULL
      	};
      	struct option replay_options[] = {
    @@ builtin/replay.c: int cmd_replay(int argc,
      			   N_("replay onto given commit")),
      		OPT_BOOL(0, "contained", &contained,
      			 N_("advance all branches contained in revision-range")),
    -+		OPT_BOOL(0, "output-commands", &output_commands,
    -+			 N_("output update commands instead of updating refs")),
    -+		OPT_BOOL(0, "allow-partial", &allow_partial,
    -+			 N_("allow some ref updates to succeed even if others fail")),
    ++		OPT_STRING(0, "update-refs", &update_refs_mode,
    ++			   N_("mode"),
    ++			   N_("control ref update behavior (yes|print)")),
      		OPT_END()
      	};
      
     @@ builtin/replay.c: int cmd_replay(int argc,
    - 		usage_with_options(replay_usage, replay_options);
    - 	}
    + 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
    + 				  contained, "--contained");
      
    --	if (advance_name_opt && contained)
    --		die(_("options '%s' and '%s' cannot be used together"),
    --		    "--advance", "--contained");
    -+	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
    -+				  contained, "--contained");
    ++	/* Set default mode if not specified */
    ++	if (!update_refs_mode)
    ++		update_refs_mode = "yes";
     +
    -+	die_for_incompatible_opt2(allow_partial, "--allow-partial",
    -+				  output_commands, "--output-commands");
    ++	/* Validate update-refs mode */
    ++	if (strcmp(update_refs_mode, "yes") && strcmp(update_refs_mode, "print"))
    ++		die(_("invalid value for --update-refs: '%s' (expected 'yes' or 'print')"),
    ++		    update_refs_mode);
     +
      	advance_name = xstrdup_or_null(advance_name_opt);
      
    @@ builtin/replay.c: int cmd_replay(int argc,
      	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
      			      &onto, &update_refs);
      
    -+	if (!output_commands) {
    -+		unsigned int transaction_flags = allow_partial ? REF_TRANSACTION_ALLOW_FAILURE : 0;
    ++	/* Initialize ref transaction if we're updating refs directly */
    ++	if (!strcmp(update_refs_mode, "yes")) {
     +		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
    -+							  transaction_flags,
    -+							  &transaction_err);
    ++							  0, &transaction_err);
     +		if (!transaction) {
    -+			ret = error(_("failed to begin ref transaction: %s"), transaction_err.buf);
    ++			ret = error(_("failed to begin ref transaction: %s"),
    ++				    transaction_err.buf);
     +			goto cleanup;
     +		}
     +	}
    @@ builtin/replay.c: int cmd_replay(int argc,
      	if (!onto) /* FIXME: Should handle replaying down to root commit */
      		die("Replaying down to root commit is not supported yet!");
      
    -@@ builtin/replay.c: int cmd_replay(int argc,
    - 		khint_t pos;
    - 		int hr;
    - 
    -+		commits_processed = 1;
    -+
    - 		if (!commit->parents)
    - 			die(_("replaying down to root commit is not supported yet!"));
    - 		if (commit->parents->next)
     @@ builtin/replay.c: int cmd_replay(int argc,
      			if (decoration->type == DECORATION_REF_LOCAL &&
      			    (contained || strset_contains(update_refs,
    @@ builtin/replay.c: int cmd_replay(int argc,
     -				       decoration->name,
     -				       oid_to_hex(&last_commit->object.oid),
     -				       oid_to_hex(&commit->object.oid));
    -+				if (output_commands) {
    -+					printf("update %s %s %s\n",
    -+					       decoration->name,
    -+					       oid_to_hex(&last_commit->object.oid),
    -+					       oid_to_hex(&commit->object.oid));
    -+				} else if (add_ref_to_transaction(transaction, decoration->name,
    -+								  &last_commit->object.oid,
    -+								  &commit->object.oid,
    -+								  &transaction_err) < 0) {
    -+					ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
    ++				if (handle_ref_update(update_refs_mode, transaction,
    ++						      decoration->name,
    ++						      &last_commit->object.oid,
    ++						      &commit->object.oid,
    ++						      &transaction_err) < 0) {
    ++					ret = error(_("failed to update ref '%s': %s"),
    ++						    decoration->name, transaction_err.buf);
     +					goto cleanup;
     +				}
      			}
    @@ builtin/replay.c: int cmd_replay(int argc,
     -		       advance_name,
     -		       oid_to_hex(&last_commit->object.oid),
     -		       oid_to_hex(&onto->object.oid));
    -+		if (output_commands) {
    -+			printf("update %s %s %s\n",
    -+			       advance_name,
    -+			       oid_to_hex(&last_commit->object.oid),
    -+			       oid_to_hex(&onto->object.oid));
    -+		} else if (add_ref_to_transaction(transaction, advance_name,
    -+						  &last_commit->object.oid,
    -+						  &onto->object.oid,
    -+						  &transaction_err) < 0) {
    -+			ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
    ++		if (handle_ref_update(update_refs_mode, transaction,
    ++				      advance_name,
    ++				      &last_commit->object.oid,
    ++				      &onto->object.oid,
    ++				      &transaction_err) < 0) {
    ++			ret = error(_("failed to update ref '%s': %s"),
    ++				    advance_name, transaction_err.buf);
     +			goto cleanup;
     +		}
     +	}
    @@ builtin/replay.c: int cmd_replay(int argc,
     +	/* Commit the ref transaction if we have one */
     +	if (transaction && result.clean == 1) {
     +		if (ref_transaction_commit(transaction, &transaction_err)) {
    -+			if (allow_partial) {
    -+				warning(_("some ref updates failed: %s"), transaction_err.buf);
    -+				ref_transaction_for_each_rejected_update(transaction,
    -+									 print_rejected_update, NULL);
    -+				ret = 0; /* Set failure even with allow_partial */
    -+			} else {
    -+				ret = error(_("failed to update refs: %s"), transaction_err.buf);
    -+				goto cleanup;
    -+			}
    ++			ret = error(_("failed to commit ref transaction: %s"),
    ++				    transaction_err.buf);
    ++			goto cleanup;
     +		}
      	}
      
      	merge_finalize(&merge_opt, &result);
     @@ builtin/replay.c: int cmd_replay(int argc,
    - 		strset_clear(update_refs);
    - 		free(update_refs);
    - 	}
    --	ret = result.clean;
    -+
    -+	/* Handle empty ranges: if no commits were processed, treat as success */
    -+	if (!commits_processed)
    -+		ret = 1; /* Success - no commits to replay is not an error */
    -+	else
    -+		ret = result.clean;
    + 	ret = result.clean;
      
      cleanup:
     +	if (transaction)
    @@ t/t3650-replay-basics.sh: test_expect_success 'setup bare' '
      
      test_expect_success 'using replay to rebase two branches, one on top of other' '
     -	git replay --onto main topic1..topic2 >result &&
    -+	git replay --output-commands --onto main topic1..topic2 >result &&
    ++	git replay --update-refs=print --onto main topic1..topic2 >result &&
      
      	test_line_count = 1 result &&
      
    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to rebase two branch
      '
      
     +test_expect_success 'using replay with default atomic behavior (no output)' '
    -+	# Create a test branch that wont interfere with others
    -+	git branch atomic-test topic2 &&
    -+	git rev-parse atomic-test >atomic-test-old &&
    ++	# Store the original state
    ++	START=$(git rev-parse topic2) &&
    ++	test_when_finished "git branch -f topic2 $START" &&
     +
     +	# Default behavior: atomic ref updates (no output)
    -+	git replay --onto main topic1..atomic-test >output &&
    ++	git replay --onto main topic1..topic2 >output &&
     +	test_must_be_empty output &&
     +
    -+	# Verify the branch was updated
    -+	git rev-parse atomic-test >atomic-test-new &&
    -+	! test_cmp atomic-test-old atomic-test-new &&
    -+
     +	# Verify the history is correct
    -+	git log --format=%s atomic-test >actual &&
    ++	git log --format=%s topic2 >actual &&
     +	test_write_lines E D M L B A >expect &&
     +	test_cmp expect actual
     +'
     +
      test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
     -	git -C bare replay --onto main topic1..topic2 >result-bare &&
    --	test_cmp expect result-bare
    -+	git -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&
    ++	git -C bare replay --update-refs=print --onto main topic1..topic2 >result-bare &&
    ++
    ++	test_line_count = 1 result-bare &&
    ++
    ++	git log --format=%s $(cut -f 3 -d " " result-bare) >actual &&
    ++	test_write_lines E D M L B A >expect &&
    ++	test_cmp expect actual &&
    ++
    ++	printf "update refs/heads/topic2 " >expect &&
    ++	printf "%s " $(cut -f 3 -d " " result-bare) >>expect &&
    ++	git -C bare rev-parse topic2 >>expect &&
     +
    -+	# The result should match what we got from the regular repo
    -+	test_cmp result result-bare
    + 	test_cmp expect result-bare
      '
      
    - test_expect_success 'using replay to rebase with a conflict' '
     @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to perform basic cherry-pick' '
      	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
      	# 4th field of result is hash for main instead of hash for topic2
      
     -	git replay --advance main topic1..topic2 >result &&
    -+	git replay --output-commands --advance main topic1..topic2 >result &&
    ++	git replay --update-refs=print --advance main topic1..topic2 >result &&
      
      	test_line_count = 1 result &&
      
    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to perform basic che
      
      test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
     -	git -C bare replay --advance main topic1..topic2 >result-bare &&
    -+	git -C bare replay --output-commands --advance main topic1..topic2 >result-bare &&
    ++	git -C bare replay --update-refs=print --advance main topic1..topic2 >result-bare &&
    ++
    ++	test_line_count = 1 result-bare &&
    ++
    ++	git log --format=%s $(cut -f 3 -d " " result-bare) >actual &&
    ++	test_write_lines E D M L B A >expect &&
    ++	test_cmp expect actual &&
    ++
    ++	printf "update refs/heads/main " >expect &&
    ++	printf "%s " $(cut -f 3 -d " " result-bare) >>expect &&
    ++	git -C bare rev-parse main >>expect &&
    ++
      	test_cmp expect result-bare
      '
      
    @@ t/t3650-replay-basics.sh: test_expect_success 'replay fails when both --advance
      
      test_expect_success 'using replay to also rebase a contained branch' '
     -	git replay --contained --onto main main..topic3 >result &&
    -+	git replay --output-commands --contained --onto main main..topic3 >result &&
    ++	git replay --update-refs=print --contained --onto main main..topic3 >result &&
      
      	test_line_count = 2 result &&
      	cut -f 3 -d " " result >new-branch-tips &&
    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to also rebase a con
      
      test_expect_success 'using replay on bare repo to also rebase a contained branch' '
     -	git -C bare replay --contained --onto main main..topic3 >result-bare &&
    -+	git -C bare replay --output-commands --contained --onto main main..topic3 >result-bare &&
    ++	git -C bare replay --update-refs=print --contained --onto main main..topic3 >result-bare &&
    ++
    ++	test_line_count = 2 result-bare &&
    ++	cut -f 3 -d " " result-bare >new-branch-tips &&
    ++
    ++	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
    ++	test_write_lines F C M L B A >expect &&
    ++	test_cmp expect actual &&
    ++
    ++	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
    ++	test_write_lines H G F C M L B A >expect &&
    ++	test_cmp expect actual &&
    ++
    ++	printf "update refs/heads/topic1 " >expect &&
    ++	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
    ++	git -C bare rev-parse topic1 >>expect &&
    ++	printf "update refs/heads/topic3 " >>expect &&
    ++	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
    ++	git -C bare rev-parse topic3 >>expect &&
    ++
      	test_cmp expect result-bare
      '
      
      test_expect_success 'using replay to rebase multiple divergent branches' '
     -	git replay --onto main ^topic1 topic2 topic4 >result &&
    -+	git replay --output-commands --onto main ^topic1 topic2 topic4 >result &&
    ++	git replay --update-refs=print --onto main ^topic1 topic2 topic4 >result &&
      
      	test_line_count = 2 result &&
      	cut -f 3 -d " " result >new-branch-tips &&
    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to rebase multiple d
      
      test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
     -	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
    -+	git -C bare replay --output-commands --contained --onto main ^main topic2 topic3 topic4 >result &&
    ++	git -C bare replay --update-refs=print --contained --onto main ^main topic2 topic3 topic4 >result &&
      
      	test_line_count = 4 result &&
      	cut -f 3 -d " " result >new-branch-tips &&
    @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '
      		--onto rename-onto rename-onto..rename-from
      '
      
    -+# Tests for new default atomic behavior and options
    -+
    -+test_expect_success 'replay default behavior should not produce output when successful' '
    -+	git replay --onto main topic1..topic3 >output &&
    -+	test_must_be_empty output
    -+'
    -+
    -+test_expect_success 'replay with --output-commands produces traditional output' '
    -+	git replay --output-commands --onto main topic1..topic3 >output &&
    -+	test_line_count = 1 output &&
    -+	grep "^update refs/heads/topic3 " output
    -+'
    -+
    -+test_expect_success 'replay with --allow-partial should not produce output when successful' '
    -+	git replay --allow-partial --onto main topic1..topic3 >output &&
    -+	test_must_be_empty output
    -+'
    -+
    -+test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '
    -+	test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
    -+	grep "cannot be used together" error
    -+'
    ++# Tests for atomic ref update behavior
     +
     +test_expect_success 'replay with --contained updates multiple branches atomically' '
    -+	# Create fresh test branches based on the original structure
    -+	# contained-topic1 should be contained within the range to contained-topic3
    -+	git branch contained-base main &&
    -+	git checkout -b contained-topic1 contained-base &&
    -+	test_commit ContainedC &&
    -+	git checkout -b contained-topic3 contained-topic1 &&
    -+	test_commit ContainedG &&
    -+	test_commit ContainedH &&
    -+	git checkout main &&
    -+
     +	# Store original states
    -+	git rev-parse contained-topic1 >contained-topic1-old &&
    -+	git rev-parse contained-topic3 >contained-topic3-old &&
    -+
    -+	# Use --contained to update multiple branches - this should update both
    -+	git replay --contained --onto main contained-base..contained-topic3 &&
    -+
    -+	# Verify both branches were updated
    -+	git rev-parse contained-topic1 >contained-topic1-new &&
    -+	git rev-parse contained-topic3 >contained-topic3-new &&
    -+	! test_cmp contained-topic1-old contained-topic1-new &&
    -+	! test_cmp contained-topic3-old contained-topic3-new
    -+'
    ++	START_TOPIC1=$(git rev-parse topic1) &&
    ++	START_TOPIC3=$(git rev-parse topic3) &&
    ++	test_when_finished "git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3" &&
     +
    -+test_expect_success 'replay atomic behavior: all refs updated or none' '
    -+	# Store original state
    -+	git rev-parse topic4 >topic4-old &&
    -+
    -+	# Default atomic behavior
    -+	git replay --onto main main..topic4 &&
    ++	# Use --contained to update multiple branches
    ++	git replay --contained --onto main main..topic3 >output &&
    ++	test_must_be_empty output &&
     +
    -+	# Verify ref was updated
    -+	git rev-parse topic4 >topic4-new &&
    -+	! test_cmp topic4-old topic4-new &&
    ++	# Verify both branches were updated with correct commit sequences
    ++	git log --format=%s topic1 >actual &&
    ++	test_write_lines F C M L B A >expect &&
    ++	test_cmp expect actual &&
     +
    -+	# Verify no partial state
    -+	git log --format=%s topic4 >actual &&
    -+	test_write_lines J I M L B A >expect &&
    ++	git log --format=%s topic3 >actual &&
    ++	test_write_lines H G F C M L B A >expect &&
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success 'replay works correctly with bare repositories' '
    -+	# Test atomic behavior in bare repo (important for Gitaly)
    -+	git checkout -b bare-test topic1 &&
    -+	test_commit BareTest &&
    ++test_expect_success 'replay atomic guarantee: all refs updated or none' '
    ++	# Store original states
    ++	START_TOPIC1=$(git rev-parse topic1) &&
    ++	START_TOPIC3=$(git rev-parse topic3) &&
    ++	test_when_finished "git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3 && rm -f .git/refs/heads/topic1.lock" &&
     +
    -+	# Test with bare repo - replay the commits from main..bare-test to get the full history
    -+	git -C bare fetch .. bare-test:bare-test &&
    -+	git -C bare replay --onto main main..bare-test &&
    ++	# Create a lock on topic1 to simulate a concurrent update
    ++	>.git/refs/heads/topic1.lock &&
     +
    -+	# Verify the bare repo was updated correctly (no output)
    -+	git -C bare log --format=%s bare-test >actual &&
    -+	test_write_lines BareTest F C M L B A >expect &&
    -+	test_cmp expect actual
    -+'
    ++	# Try to update multiple branches with --contained
    ++	# This should fail atomically - neither branch should be updated
    ++	test_must_fail git replay --contained --onto main main..topic3 2>error &&
     +
    -+test_expect_success 'replay --allow-partial with no failures produces no output' '
    -+	git checkout -b partial-test topic1 &&
    -+	test_commit PartialTest &&
    ++	# Verify the transaction failed
    ++	grep "failed to commit ref transaction" error &&
     +
    -+	# Should succeed silently even with partial mode
    -+	git replay --allow-partial --onto main topic1..partial-test >output &&
    -+	test_must_be_empty output
    ++	# Verify NEITHER branch was updated (all-or-nothing guarantee)
    ++	test_cmp_rev $START_TOPIC1 topic1 &&
    ++	test_cmp_rev $START_TOPIC3 topic3
     +'
     +
    -+test_expect_success 'replay maintains ref update consistency' '
    -+	# Test that traditional vs atomic produce equivalent results
    -+	git checkout -b method1-test topic2 &&
    -+	git checkout -b method2-test topic2 &&
    ++test_expect_success 'traditional pipeline and atomic update produce equivalent results' '
    ++	# Store original states
    ++	START_TOPIC2=$(git rev-parse topic2) &&
    ++	test_when_finished "git branch -f topic2 $START_TOPIC2" &&
     +
    -+	# Both methods should update refs to point to the same replayed commits
    -+	git replay --output-commands --onto main topic1..method1-test >update-commands &&
    ++	# Traditional method: output commands and pipe to update-ref
    ++	git replay --update-refs=print --onto main topic1..topic2 >update-commands &&
     +	git update-ref --stdin <update-commands &&
    -+	git log --format=%s method1-test >traditional-result &&
    ++	git log --format=%s topic2 >traditional-result &&
    ++
    ++	# Reset topic2
    ++	git branch -f topic2 $START_TOPIC2 &&
     +
    -+	# Direct atomic method should produce same commit history
    -+	git replay --onto main topic1..method2-test &&
    -+	git log --format=%s method2-test >atomic-result &&
    ++	# Atomic method: direct ref updates
    ++	git replay --onto main topic1..topic2 &&
    ++	git log --format=%s topic2 >atomic-result &&
     +
     +	# Both methods should produce identical commit histories
     +	test_cmp traditional-result atomic-result
     +'
     +
    -+test_expect_success 'replay error messages are helpful and clear' '
    -+	# Test that error messages are clear
    -+	test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
    -+	grep "cannot be used together" error
    -+'
    -+
    -+test_expect_success 'replay with empty range produces no output and no changes' '
    -+	# Create a test branch for empty range testing
    -+	git checkout -b empty-test topic1 &&
    -+	git rev-parse empty-test >empty-test-before &&
    -+
    -+	# Empty range should succeed but do nothing
    -+	git replay --onto main empty-test..empty-test >output &&
    ++test_expect_success 'replay works correctly with bare repositories' '
    ++	# Test atomic behavior in bare repo
    ++	git -C bare fetch .. topic1:bare-test-branch &&
    ++	git -C bare replay --onto main main..bare-test-branch >output &&
     +	test_must_be_empty output &&
     +
    -+	# Branch should be unchanged
    -+	git rev-parse empty-test >empty-test-after &&
    -+	test_cmp empty-test-before empty-test-after
    ++	# Verify the bare repo was updated correctly
    ++	git -C bare log --format=%s bare-test-branch >actual &&
    ++	test_write_lines F C M L B A >expect &&
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success 'replay validates --update-refs mode values' '
    ++	test_must_fail git replay --update-refs=invalid --onto main topic1..topic2 2>error &&
    ++	grep "invalid value for --update-refs" error
     +'
     +
      test_done
-:  ---------- > 3:  710ab27ae3 replay: add replay.defaultAction config option
-- 
2.51.0


Siddharth Asthana (3):
  replay: use die_for_incompatible_opt2() for option validation
  replay: make atomic ref updates the default behavior
  replay: add replay.defaultAction config option

 Documentation/config/replay.adoc |  14 +++
 Documentation/git-replay.adoc    |  71 ++++++-----
 builtin/replay.c                 | 108 +++++++++++++++--
 t/t3650-replay-basics.sh         | 198 +++++++++++++++++++++++++++++--
 4 files changed, 343 insertions(+), 48 deletions(-)
 create mode 100644 Documentation/config/replay.adoc

base-commit: 4b71b294773cc4f7fe48ec3a70079aa8783f373d

Thanks
- Siddharth

^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v3 1/3] replay: use die_for_incompatible_opt2() for option validation
  2025-10-13 18:33   ` [PATCH v3 0/3] replay: make atomic ref updates the default Siddharth Asthana
@ 2025-10-13 18:33     ` Siddharth Asthana
  2025-10-13 18:33     ` [PATCH v3 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
                       ` (3 subsequent siblings)
  4 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-13 18:33 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

In preparation for adding the --update-refs option, convert option
validation to use die_for_incompatible_opt2(). This helper provides
standardized error messages for mutually exclusive options.

The following commit introduces --update-refs which will be incompatible
with certain other options. Using die_for_incompatible_opt2() now means
that commit can cleanly add its validation using the same pattern,
keeping the validation logic consistent and maintainable.

This also aligns git-replay's option handling with how other Git commands
manage option conflicts, using the established die_for_incompatible_opt*()
helper family.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 builtin/replay.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 6172c8aacc..b64fc72063 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -330,9 +330,9 @@ int cmd_replay(int argc,
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (advance_name_opt && contained)
-		die(_("options '%s' and '%s' cannot be used together"),
-		    "--advance", "--contained");
+	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
+				  contained, "--contained");
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH v3 2/3] replay: make atomic ref updates the default behavior
  2025-10-13 18:33   ` [PATCH v3 0/3] replay: make atomic ref updates the default Siddharth Asthana
  2025-10-13 18:33     ` [PATCH v3 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
@ 2025-10-13 18:33     ` Siddharth Asthana
  2025-10-13 22:05       ` Junio C Hamano
  2025-10-13 18:33     ` [PATCH v3 3/3] replay: add replay.defaultAction config option Siddharth Asthana
                       ` (2 subsequent siblings)
  4 siblings, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-13 18:33 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

The git replay command currently outputs update commands that can be
piped to update-ref to achieve a rebase, e.g.

  git replay --onto main topic1..topic2 | git update-ref --stdin

This separation had advantages for three special cases:
  * it made testing easy (when state isn't modified from one step to
    the next, you don't need to make temporary branches or have undo
    commands, or try to track the changes)
  * it provided a natural can-it-rebase-cleanly (and what would it
    rebase to) capability without automatically updating refs, similar
    to a --dry-run
  * it provided a natural low-level tool for the suite of hash-object,
    mktree, commit-tree, mktag, merge-tree, and update-ref, allowing
    users to have another building block for experimentation and making
    new tools

However, it should be noted that all three of these are somewhat
special cases; users, whether on the client or server side, would
almost certainly find it more ergonomical to simply have the updating
of refs be the default.

For server-side operations in particular, the pipeline architecture
creates process coordination overhead. Server implementations that need
to perform rebases atomically must maintain additional code to:

  1. Spawn and manage a pipeline between git-replay and git-update-ref
  2. Coordinate stdout/stderr streams across the pipe boundary
  3. Handle partial failure states if the pipeline breaks mid-execution
  4. Parse and validate the update-ref command output

Change the default behavior to update refs directly, and atomically (at
least to the extent supported by the refs backend in use). This
eliminates the process coordination overhead for the common case.

For users needing the traditional pipeline workflow, add a new
`--update-refs=<mode>` option that preserves the original behavior:

  git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin

The mode can be:
  * `yes` (default): Update refs directly using an atomic transaction
  * `print`: Output update-ref commands for pipeline use

Implementation details:

The atomic ref updates are implemented using Git's ref transaction API.
In cmd_replay(), when not in 'print' mode, we initialize a transaction
using ref_store_transaction_begin() with the default atomic behavior.
As commits are replayed, ref updates are staged into the transaction
using ref_transaction_update(). Finally, ref_transaction_commit()
applies all updates atomically—either all updates succeed or none do.

To avoid code duplication between the 'print' and 'yes' modes, this
commit extracts a handle_ref_update() helper function. This function
takes the mode and either prints the update command or stages it into
the transaction. This keeps both code paths consistent and makes future
maintenance easier.

The helper function signature:

  static int handle_ref_update(const char *mode,
                                struct ref_transaction *transaction,
                                const char *refname,
                                const struct object_id *new_oid,
                                const struct object_id *old_oid,
                                struct strbuf *err)

When mode is 'print', it prints the update-ref command. When mode is
'yes', it calls ref_transaction_update() to stage the update. This
eliminates the duplication that would otherwise exist at each ref update
call site.

Test suite changes:

All existing tests that expected command output now use
`--update-refs=print` to preserve their original behavior. This keeps
the tests valid while allowing them to verify that the pipeline workflow
still works correctly.

New tests were added to verify:
  - Default atomic behavior (no output, refs updated directly)
  - Bare repository support (server-side use case)
  - Equivalence between traditional pipeline and atomic updates
  - Real atomicity using a lock file to verify all-or-nothing guarantee
  - Test isolation using test_when_finished to clean up state

The bare repository tests were fixed to rebuild their expectations
independently rather than comparing to previous test output, improving
test reliability and isolation.

A following commit will add a `replay.defaultAction` configuration
option for users who prefer the traditional pipeline output as their
default behavior.

Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc |  71 ++++++++++------
 builtin/replay.c              |  88 ++++++++++++++++---
 t/t3650-replay-basics.sh      | 153 ++++++++++++++++++++++++++++++++--
 3 files changed, 267 insertions(+), 45 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 0b12bf8aa4..ea04021a5f 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,15 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>)
+		[--update-refs[=<mode>]] <revision-range>...
 
 DESCRIPTION
 -----------
 
 Takes ranges of commits and replays them onto a new location. Leaves
-the working tree and the index untouched, and updates no references.
-The output of this command is meant to be used as input to
-`git update-ref --stdin`, which would update the relevant branches
+the working tree and the index untouched. By default, updates the
+relevant references using an atomic transaction (all refs update or
+none). Use `--update-refs=print` to avoid automatic ref updates and
+instead get update commands that can be piped to `git update-ref --stdin`
 (see the OUTPUT section below).
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
@@ -29,18 +31,28 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-When `--onto` is specified, the update-ref command(s) in the output will
-update the branch(es) in the revision range to point at the new
-commits, similar to the way how `git rebase --update-refs` updates
-multiple branches in the affected range.
+When `--onto` is specified, the branch(es) in the revision range will be
+updated to point at the new commits (or update commands will be printed
+if `--update-refs=print` is used), similar to the way how
+`git rebase --update-refs` updates multiple branches in the affected range.
 
 --advance <branch>::
 	Starting point at which to create the new commits; must be a
 	branch name.
 +
-When `--advance` is specified, the update-ref command(s) in the output
-will update the branch passed as an argument to `--advance` to point at
-the new commits (in other words, this mimics a cherry-pick operation).
+When `--advance` is specified, the branch passed as an argument will be
+updated to point at the new commits (or an update command will be printed
+if `--update-refs=print` is used). This mimics a cherry-pick operation.
+
+--update-refs[=<mode>]::
+	Control how references are updated. The mode can be:
++
+--
+* `yes` (default): Update refs directly using an atomic transaction.
+  All ref updates succeed or all fail.
+* `print`: Output update-ref commands instead of updating refs.
+  The output can be piped as-is to `git update-ref --stdin`.
+--
 
 <revision-range>::
 	Range of commits to replay. More than one <revision-range> can
@@ -54,15 +66,19 @@ include::rev-list-options.adoc[]
 OUTPUT
 ------
 
-When there are no conflicts, the output of this command is usable as
-input to `git update-ref --stdin`.  It is of the form:
+By default, when there are no conflicts, this command updates the relevant
+references using an atomic transaction and produces no output. All ref
+updates succeed or all fail.
+
+When `--update-refs=print` is used, the output is usable as input to
+`git update-ref --stdin`. It is of the form:
 
 	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
 	update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 
 where the number of refs updated depends on the arguments passed and
-the shape of the history being replayed.  When using `--advance`, the
+the shape of the history being replayed. When using `--advance`, the
 number of refs updated is always one, but for `--onto`, it can be one
 or more (rebasing multiple branches simultaneously is supported).
 
@@ -77,44 +93,45 @@ is something other than 0 or 1.
 EXAMPLES
 --------
 
-To simply rebase `mybranch` onto `target`:
+To simply rebase `mybranch` onto `target` (default behavior):
 
 ------------
 $ git replay --onto target origin/main..mybranch
-update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
 To cherry-pick the commits from mybranch onto target:
 
 ------------
 $ git replay --advance target origin/main..mybranch
-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
 ------------
 
 Note that the first two examples replay the exact same commits and on
 top of the exact same new base, they only differ in that the first
-provides instructions to make mybranch point at the new commits and
-the second provides instructions to make target point at them.
+updates mybranch to point at the new commits and the second updates
+target to point at them.
+
+To get the traditional pipeline output:
+
+------------
+$ git replay --update-refs=print --onto target origin/main..mybranch
+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
+------------
 
 What if you have a stack of branches, one depending upon another, and
 you'd really like to rebase the whole set?
 
 ------------
 $ git replay --contained --onto origin/main origin/main..tipbranch
-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
 ------------
 
+This automatically finds and rebases all branches contained within the
+`origin/main..tipbranch` range.
+
 When calling `git replay`, one does not need to specify a range of
-commits to replay using the syntax `A..B`; any range expression will
-do:
+commits to replay using the syntax `A..B`; any range expression will do:
 
 ------------
 $ git replay --onto origin/main ^base branch1 branch2 branch3
-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 ------------
 
 This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
diff --git a/builtin/replay.c b/builtin/replay.c
index b64fc72063..457225363e 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -284,6 +284,26 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	return create_commit(repo, result->tree, pickme, replayed_base);
 }
 
+static int handle_ref_update(const char *mode,
+			     struct ref_transaction *transaction,
+			     const char *refname,
+			     const struct object_id *new_oid,
+			     const struct object_id *old_oid,
+			     struct strbuf *err)
+{
+	if (!strcmp(mode, "print")) {
+		printf("update %s %s %s\n",
+		       refname,
+		       oid_to_hex(new_oid),
+		       oid_to_hex(old_oid));
+		return 0;
+	}
+
+	/* mode == "yes" - update refs directly */
+	return ref_transaction_update(transaction, refname, new_oid, old_oid,
+				      NULL, NULL, 0, "git replay", err);
+}
+
 int cmd_replay(int argc,
 	       const char **argv,
 	       const char *prefix,
@@ -294,6 +314,7 @@ int cmd_replay(int argc,
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
 	int contained = 0;
+	const char *update_refs_mode = NULL;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -302,12 +323,14 @@ int cmd_replay(int argc,
 	struct merge_result result;
 	struct strset *update_refs = NULL;
 	kh_oid_map_t *replayed_commits;
+	struct ref_transaction *transaction = NULL;
+	struct strbuf transaction_err = STRBUF_INIT;
 	int ret = 0;
 
-	const char * const replay_usage[] = {
+	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto <newbase> | --advance <branch>) "
-		   "<revision-range>..."),
+		   "[--update-refs[=<mode>]] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -319,6 +342,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &contained,
 			 N_("advance all branches contained in revision-range")),
+		OPT_STRING(0, "update-refs", &update_refs_mode,
+			   N_("mode"),
+			   N_("control ref update behavior (yes|print)")),
 		OPT_END()
 	};
 
@@ -333,6 +359,15 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
+	/* Set default mode if not specified */
+	if (!update_refs_mode)
+		update_refs_mode = "yes";
+
+	/* Validate update-refs mode */
+	if (strcmp(update_refs_mode, "yes") && strcmp(update_refs_mode, "print"))
+		die(_("invalid value for --update-refs: '%s' (expected 'yes' or 'print')"),
+		    update_refs_mode);
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
@@ -389,6 +424,17 @@ int cmd_replay(int argc,
 	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
 			      &onto, &update_refs);
 
+	/* Initialize ref transaction if we're updating refs directly */
+	if (!strcmp(update_refs_mode, "yes")) {
+		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
+							  0, &transaction_err);
+		if (!transaction) {
+			ret = error(_("failed to begin ref transaction: %s"),
+				    transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
 	if (!onto) /* FIXME: Should handle replaying down to root commit */
 		die("Replaying down to root commit is not supported yet!");
 
@@ -434,10 +480,15 @@ int cmd_replay(int argc,
 			if (decoration->type == DECORATION_REF_LOCAL &&
 			    (contained || strset_contains(update_refs,
 							  decoration->name))) {
-				printf("update %s %s %s\n",
-				       decoration->name,
-				       oid_to_hex(&last_commit->object.oid),
-				       oid_to_hex(&commit->object.oid));
+				if (handle_ref_update(update_refs_mode, transaction,
+						      decoration->name,
+						      &last_commit->object.oid,
+						      &commit->object.oid,
+						      &transaction_err) < 0) {
+					ret = error(_("failed to update ref '%s': %s"),
+						    decoration->name, transaction_err.buf);
+					goto cleanup;
+				}
 			}
 			decoration = decoration->next;
 		}
@@ -445,10 +496,24 @@ int cmd_replay(int argc,
 
 	/* In --advance mode, advance the target ref */
 	if (result.clean == 1 && advance_name) {
-		printf("update %s %s %s\n",
-		       advance_name,
-		       oid_to_hex(&last_commit->object.oid),
-		       oid_to_hex(&onto->object.oid));
+		if (handle_ref_update(update_refs_mode, transaction,
+				      advance_name,
+				      &last_commit->object.oid,
+				      &onto->object.oid,
+				      &transaction_err) < 0) {
+			ret = error(_("failed to update ref '%s': %s"),
+				    advance_name, transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
+	/* Commit the ref transaction if we have one */
+	if (transaction && result.clean == 1) {
+		if (ref_transaction_commit(transaction, &transaction_err)) {
+			ret = error(_("failed to commit ref transaction: %s"),
+				    transaction_err.buf);
+			goto cleanup;
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
@@ -460,6 +525,9 @@ int cmd_replay(int argc,
 	ret = result.clean;
 
 cleanup:
+	if (transaction)
+		ref_transaction_free(transaction);
+	strbuf_release(&transaction_err);
 	release_revisions(&revs);
 	free(advance_name);
 
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 58b3759935..c2c54fbba7 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git replay --onto main topic1..topic2 >result &&
+	git replay --update-refs=print --onto main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -67,8 +67,34 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 	test_cmp expect result
 '
 
+test_expect_success 'using replay with default atomic behavior (no output)' '
+	# Store the original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
+
+	# Default behavior: atomic ref updates (no output)
+	git replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify the history is correct
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
-	git -C bare replay --onto main topic1..topic2 >result-bare &&
+	git -C bare replay --update-refs=print --onto main topic1..topic2 >result-bare &&
+
+	test_line_count = 1 result-bare &&
+
+	git log --format=%s $(cut -f 3 -d " " result-bare) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic2 " >expect &&
+	printf "%s " $(cut -f 3 -d " " result-bare) >>expect &&
+	git -C bare rev-parse topic2 >>expect &&
+
 	test_cmp expect result-bare
 '
 
@@ -86,7 +112,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
 	# 4th field of result is hash for main instead of hash for topic2
 
-	git replay --advance main topic1..topic2 >result &&
+	git replay --update-refs=print --advance main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -102,7 +128,18 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 '
 
 test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
-	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	git -C bare replay --update-refs=print --advance main topic1..topic2 >result-bare &&
+
+	test_line_count = 1 result-bare &&
+
+	git log --format=%s $(cut -f 3 -d " " result-bare) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/main " >expect &&
+	printf "%s " $(cut -f 3 -d " " result-bare) >>expect &&
+	git -C bare rev-parse main >>expect &&
+
 	test_cmp expect result-bare
 '
 
@@ -115,7 +152,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 '
 
 test_expect_success 'using replay to also rebase a contained branch' '
-	git replay --contained --onto main main..topic3 >result &&
+	git replay --update-refs=print --contained --onto main main..topic3 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -139,12 +176,31 @@ test_expect_success 'using replay to also rebase a contained branch' '
 '
 
 test_expect_success 'using replay on bare repo to also rebase a contained branch' '
-	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	git -C bare replay --update-refs=print --contained --onto main main..topic3 >result-bare &&
+
+	test_line_count = 2 result-bare &&
+	cut -f 3 -d " " result-bare >new-branch-tips &&
+
+	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
+	test_write_lines H G F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	printf "update refs/heads/topic1 " >expect &&
+	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
+	git -C bare rev-parse topic1 >>expect &&
+	printf "update refs/heads/topic3 " >>expect &&
+	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
+	git -C bare rev-parse topic3 >>expect &&
+
 	test_cmp expect result-bare
 '
 
 test_expect_success 'using replay to rebase multiple divergent branches' '
-	git replay --onto main ^topic1 topic2 topic4 >result &&
+	git replay --update-refs=print --onto main ^topic1 topic2 topic4 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -168,7 +224,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
 '
 
 test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
-	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+	git -C bare replay --update-refs=print --contained --onto main ^main topic2 topic3 topic4 >result &&
 
 	test_line_count = 4 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -217,4 +273,85 @@ test_expect_success 'merge.directoryRenames=false' '
 		--onto rename-onto rename-onto..rename-from
 '
 
+# Tests for atomic ref update behavior
+
+test_expect_success 'replay with --contained updates multiple branches atomically' '
+	# Store original states
+	START_TOPIC1=$(git rev-parse topic1) &&
+	START_TOPIC3=$(git rev-parse topic3) &&
+	test_when_finished "git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3" &&
+
+	# Use --contained to update multiple branches
+	git replay --contained --onto main main..topic3 >output &&
+	test_must_be_empty output &&
+
+	# Verify both branches were updated with correct commit sequences
+	git log --format=%s topic1 >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual &&
+
+	git log --format=%s topic3 >actual &&
+	test_write_lines H G F C M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'replay atomic guarantee: all refs updated or none' '
+	# Store original states
+	START_TOPIC1=$(git rev-parse topic1) &&
+	START_TOPIC3=$(git rev-parse topic3) &&
+	test_when_finished "git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3 && rm -f .git/refs/heads/topic1.lock" &&
+
+	# Create a lock on topic1 to simulate a concurrent update
+	>.git/refs/heads/topic1.lock &&
+
+	# Try to update multiple branches with --contained
+	# This should fail atomically - neither branch should be updated
+	test_must_fail git replay --contained --onto main main..topic3 2>error &&
+
+	# Verify the transaction failed
+	grep "failed to commit ref transaction" error &&
+
+	# Verify NEITHER branch was updated (all-or-nothing guarantee)
+	test_cmp_rev $START_TOPIC1 topic1 &&
+	test_cmp_rev $START_TOPIC3 topic3
+'
+
+test_expect_success 'traditional pipeline and atomic update produce equivalent results' '
+	# Store original states
+	START_TOPIC2=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START_TOPIC2" &&
+
+	# Traditional method: output commands and pipe to update-ref
+	git replay --update-refs=print --onto main topic1..topic2 >update-commands &&
+	git update-ref --stdin <update-commands &&
+	git log --format=%s topic2 >traditional-result &&
+
+	# Reset topic2
+	git branch -f topic2 $START_TOPIC2 &&
+
+	# Atomic method: direct ref updates
+	git replay --onto main topic1..topic2 &&
+	git log --format=%s topic2 >atomic-result &&
+
+	# Both methods should produce identical commit histories
+	test_cmp traditional-result atomic-result
+'
+
+test_expect_success 'replay works correctly with bare repositories' '
+	# Test atomic behavior in bare repo
+	git -C bare fetch .. topic1:bare-test-branch &&
+	git -C bare replay --onto main main..bare-test-branch >output &&
+	test_must_be_empty output &&
+
+	# Verify the bare repo was updated correctly
+	git -C bare log --format=%s bare-test-branch >actual &&
+	test_write_lines F C M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'replay validates --update-refs mode values' '
+	test_must_fail git replay --update-refs=invalid --onto main topic1..topic2 2>error &&
+	grep "invalid value for --update-refs" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH v3 3/3] replay: add replay.defaultAction config option
  2025-10-13 18:33   ` [PATCH v3 0/3] replay: make atomic ref updates the default Siddharth Asthana
  2025-10-13 18:33     ` [PATCH v3 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
  2025-10-13 18:33     ` [PATCH v3 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
@ 2025-10-13 18:33     ` Siddharth Asthana
  2025-10-13 19:39     ` [PATCH v3 0/3] replay: make atomic ref updates the default Junio C Hamano
  2025-10-22 18:50     ` [PATCH v4 " Siddharth Asthana
  4 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-13 18:33 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

Add a configuration option to control the default behavior of git replay
for updating references. This allows users who prefer the traditional
pipeline output to set it once in their config instead of passing
--update-refs=print with every command.

The config option uses enum string values for extensibility:
  * replay.defaultAction = update-refs (default): atomic ref updates
  * replay.defaultAction = show-commands: output commands for pipeline

The command-line --update-refs option always overrides the config setting,
allowing users to temporarily change behavior for a single invocation.

Implementation details:

In cmd_replay(), before parsing command-line options, we read the
configuration using repo_config_get_string_tmp(). If the config variable
is set, we validate the value and map it to an internal mode:

  Config value         Internal mode    Behavior
  ────────────────────────────────────────────────────────────────
  "update-refs"        "yes"            Atomic ref updates (default)
  "show-commands"      "print"          Pipeline output
  (not set)            "yes"            Atomic ref updates (default)
  (invalid)            error            Die with helpful message

If an invalid value is provided, we die() immediately with an error
message explaining the valid options. This catches configuration errors
early and provides clear guidance to users.

The command-line --update-refs option, when provided, overrides the
config value. This precedence allows users to set their preferred default
while still having per-invocation control:

  git config replay.defaultAction show-commands  # Set default
  git replay --update-refs=yes --onto main topic  # Override once

The config option uses different value names ('update-refs' vs
'show-commands') compared to the command-line option ('yes' vs 'print')
for semantic clarity. The config values describe what action is being
taken, while the command-line values are terse for typing convenience.

The enum string design (rather than a boolean like 'replay.updateRefs')
allows future expansion to additional modes without requiring new
configuration variables. For example, if we later add custom format
support (--update-refs=format), we can extend the config to support
'replay.defaultAction = format' without breaking existing configurations
or requiring a second config variable.

Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/config/replay.adoc | 14 ++++++++++
 builtin/replay.c                 | 20 ++++++++++++--
 t/t3650-replay-basics.sh         | 47 +++++++++++++++++++++++++++++++-
 3 files changed, 77 insertions(+), 4 deletions(-)
 create mode 100644 Documentation/config/replay.adoc

diff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc
new file mode 100644
index 0000000000..6012333cc1
--- /dev/null
+++ b/Documentation/config/replay.adoc
@@ -0,0 +1,14 @@
+replay.defaultAction::
+	Control the default behavior of `git replay` for updating references.
+	Can be set to:
++
+--
+* `update-refs` (default): Update refs directly using an atomic transaction.
+* `show-commands`: Output update-ref commands that can be piped to
+  `git update-ref --stdin`.
+--
++
+This can be overridden with the `--update-refs` command-line option.
+Note that the command-line option uses slightly different values
+(`yes` and `print`) for brevity, but they map to the same behavior
+as the config values.
diff --git a/builtin/replay.c b/builtin/replay.c
index 457225363e..3c618bf100 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -8,6 +8,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
+#include "config.h"
 #include "environment.h"
 #include "hex.h"
 #include "lockfile.h"
@@ -359,9 +360,22 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
-	/* Set default mode if not specified */
-	if (!update_refs_mode)
-		update_refs_mode = "yes";
+	/* Set default mode from config if not specified on command line */
+	if (!update_refs_mode) {
+		const char *config_value = NULL;
+		if (!repo_config_get_string_tmp(repo, "replay.defaultaction", &config_value)) {
+			if (!strcmp(config_value, "update-refs"))
+				update_refs_mode = "yes";
+			else if (!strcmp(config_value, "show-commands"))
+				update_refs_mode = "print";
+			else
+				die(_("invalid value for replay.defaultAction: '%s' "
+				      "(expected 'update-refs' or 'show-commands')"),
+				    config_value);
+		} else {
+			update_refs_mode = "yes";
+		}
+	}
 
 	/* Validate update-refs mode */
 	if (strcmp(update_refs_mode, "yes") && strcmp(update_refs_mode, "print"))
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index c2c54fbba7..239d7bd87a 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -299,7 +299,7 @@ test_expect_success 'replay atomic guarantee: all refs updated or none' '
 	# Store original states
 	START_TOPIC1=$(git rev-parse topic1) &&
 	START_TOPIC3=$(git rev-parse topic3) &&
-	test_when_finished "git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3 && rm -f .git/refs/heads/topic1.lock" &&
+	test_when_finished "git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3" &&
 
 	# Create a lock on topic1 to simulate a concurrent update
 	>.git/refs/heads/topic1.lock &&
@@ -308,6 +308,9 @@ test_expect_success 'replay atomic guarantee: all refs updated or none' '
 	# This should fail atomically - neither branch should be updated
 	test_must_fail git replay --contained --onto main main..topic3 2>error &&
 
+	# Remove the lock before checking refs
+	rm -f .git/refs/heads/topic1.lock &&
+
 	# Verify the transaction failed
 	grep "failed to commit ref transaction" error &&
 
@@ -354,4 +357,46 @@ test_expect_success 'replay validates --update-refs mode values' '
 	grep "invalid value for --update-refs" error
 '
 
+test_expect_success 'replay.defaultAction config option' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START && git config --unset replay.defaultAction" &&
+
+	# Set config to show-commands
+	git config replay.defaultAction show-commands &&
+	git replay --onto main topic1..topic2 >output &&
+	test_line_count = 1 output &&
+	grep "^update refs/heads/topic2 " output &&
+
+	# Reset and test update-refs mode
+	git branch -f topic2 $START &&
+	git config replay.defaultAction update-refs &&
+	git replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'command-line --update-refs overrides config' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START && git config --unset replay.defaultAction" &&
+
+	# Set config to update-refs but use --update-refs=print
+	git config replay.defaultAction update-refs &&
+	git replay --update-refs=print --onto main topic1..topic2 >output &&
+	test_line_count = 1 output &&
+	grep "^update refs/heads/topic2 " output
+'
+
+test_expect_success 'invalid replay.defaultAction value' '
+	test_when_finished "git config --unset replay.defaultAction" &&
+	git config replay.defaultAction invalid &&
+	test_must_fail git replay --onto main topic1..topic2 2>error &&
+	grep "invalid value for replay.defaultAction" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* Re: [PATCH v3 0/3] replay: make atomic ref updates the default
  2025-10-13 18:25 [PATCH v3 " Siddharth Asthana
@ 2025-10-13 18:55 ` Siddharth Asthana
  2025-10-14 21:13 ` Junio C Hamano
  1 sibling, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-13 18:55 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Apologies for the noise - I sent this v3 series as a new thread by mistake.

I have resent it properly threaded as a reply to v2. Please disregard this
unthreaded version and refer to the correctly-threaded v3 in reply to:

   Message-ID: <20250926230838.35870-1-siddharthasthana31@gmail.com>
   Subject: [PATCH v2 0/1] replay: make atomic ref updates the default 
behavior

Apologies again for the noise! :bow

- Siddharth


On 13/10/25 23:55, Siddharth Asthana wrote:
> This is v3 of the git-replay atomic updates series.
>
> Based on feedback from v2, this version simplifies the API and improves
> extensibility. Thanks to Elijah, Phillip, Christian, Junio, and Karthik
> for the detailed reviews that shaped this version.
>
> ## Changes Since v2
>
> **Removed --allow-partial option**
>
> After discussion with Elijah and Junio, we couldn't identify a concrete
> use case for partial failure tolerance. The traditional pipeline with
> git-update-ref already provides partial update capabilities when needed
> through its transaction commands. Removing this option simplifies the API
> and avoids committing to behavior without clear real-world use cases.
>
> **Changed to --update-refs=<mode> for extensibility**
>
> Phillip suggested that separate boolean flags (--output-commands,
> --allow-partial) were limiting for future expansion. The --update-refs=<mode>
> design allows future modes without option proliferation:
>    - --update-refs=yes (default): atomic ref updates
>    - --update-refs=print: pipeline output
>    - Future modes can be added as additional values
>
> This API pattern prevents the need for multiple incompatible flags and
> provides a cleaner interface for users.
>
> **Added replay.defaultAction configuration option**
>
> Junio recommended a config option for users preferring traditional behavior.
> The implementation uses enum string values for extensibility:
>    - replay.defaultAction = update-refs (default)
>    - replay.defaultAction = show-commands (pipeline output)
>
> The command-line --update-refs option overrides the config, allowing users
> to set a preference while maintaining per-invocation control. The enum
> design (versus a boolean) allows future expansion to additional modes
> without requiring new config variables.
>
> **Improved commit messages and patch organization**
>
> Christian and Elijah provided detailed feedback on commit message structure.
> Patch 2 now uses Elijah's suggested format that explains the trade-offs of
> the current design before proposing changes. The commit messages now focus
> on the changes themselves rather than v1→v2 evolution. Added Helped-by
> trailers to acknowledge specific contributions.
>
> **Enhanced test suite with proper isolation**
>
> Following Elijah's suggestions:
>    - Existing tests use --update-refs=print to preserve their behavior
>    - New tests use test_when_finished for proper cleanup
>    - Added real atomicity test using lock files to verify all-or-nothing
>    - Fixed bare repository tests to rebuild expectations independently
>    - Removed weak tests that didn't actually verify atomicity
>
> **Extracted helper function to reduce duplication**
>
> Per Phillip's feedback, added handle_ref_update() helper to eliminate
> code duplication between print and atomic modes. This function takes a
> mode parameter and handles both cases, making the code more maintainable
> and ensuring both paths stay consistent.
>
> ## Technical Implementation
>
> The atomic ref updates use Git's ref transaction API:
>    - ref_store_transaction_begin() with default atomic behavior
>    - ref_transaction_update() to stage each update
>    - ref_transaction_commit() for atomic application
>
> The handle_ref_update() helper encapsulates the mode-specific logic,
> either printing update commands or staging them into the transaction.
>
> Config reading uses repo_config_get_string_tmp() with validation for
> 'update-refs' and 'show-commands' values, mapping them to internal
> modes 'yes' and 'print' respectively.
>
> Range-diff against v2:
> -:  ---------- > 1:  de9cc3fbee replay: use die_for_incompatible_opt2() for option validation
> 1:  e3c1a57375 ! 2:  3f4c69d612 replay: make atomic ref updates the default behavior
>      @@ Metadata
>        ## Commit message ##
>           replay: make atomic ref updates the default behavior
>       
>      -    The git replay command currently outputs update commands that must be
>      -    piped to git update-ref --stdin to actually update references:
>      +    The git replay command currently outputs update commands that can be
>      +    piped to update-ref to achieve a rebase, e.g.
>       
>      -        git replay --onto main topic1..topic2 | git update-ref --stdin
>      +      git replay --onto main topic1..topic2 | git update-ref --stdin
>       
>      -    This design has significant limitations for server-side operations. The
>      -    two-command pipeline creates coordination complexity, provides no atomic
>      -    transaction guarantees by default, and complicates automation in bare
>      -    repository environments where git replay is primarily used.
>      +    This separation had advantages for three special cases:
>      +      * it made testing easy (when state isn't modified from one step to
>      +        the next, you don't need to make temporary branches or have undo
>      +        commands, or try to track the changes)
>      +      * it provided a natural can-it-rebase-cleanly (and what would it
>      +        rebase to) capability without automatically updating refs, similar
>      +        to a --dry-run
>      +      * it provided a natural low-level tool for the suite of hash-object,
>      +        mktree, commit-tree, mktag, merge-tree, and update-ref, allowing
>      +        users to have another building block for experimentation and making
>      +        new tools
>       
>      -    During extensive mailing list discussion, multiple maintainers identified
>      -    that the current approach forces users to opt-in to atomic behavior rather
>      -    than defaulting to the safer, more reliable option. Elijah Newren noted
>      -    that the experimental status explicitly allows such behavior changes, while
>      -    Patrick Steinhardt highlighted performance concerns with individual ref
>      -    updates in the reftable backend.
>      +    However, it should be noted that all three of these are somewhat
>      +    special cases; users, whether on the client or server side, would
>      +    almost certainly find it more ergonomical to simply have the updating
>      +    of refs be the default.
>       
>      -    The core issue is that git replay was designed around command output rather
>      -    than direct action. This made sense for a plumbing tool, but creates barriers
>      -    for the primary use case: server-side operations that need reliable, atomic
>      -    ref updates without pipeline complexity.
>      +    For server-side operations in particular, the pipeline architecture
>      +    creates process coordination overhead. Server implementations that need
>      +    to perform rebases atomically must maintain additional code to:
>       
>      -    This patch changes the default behavior to update refs directly using Git's
>      -    ref transaction API:
>      +      1. Spawn and manage a pipeline between git-replay and git-update-ref
>      +      2. Coordinate stdout/stderr streams across the pipe boundary
>      +      3. Handle partial failure states if the pipeline breaks mid-execution
>      +      4. Parse and validate the update-ref command output
>       
>      -        git replay --onto main topic1..topic2
>      -        # No output; all refs updated atomically or none
>      +    Change the default behavior to update refs directly, and atomically (at
>      +    least to the extent supported by the refs backend in use). This
>      +    eliminates the process coordination overhead for the common case.
>       
>      -    The implementation uses ref_store_transaction_begin() with atomic mode by
>      -    default, ensuring all ref updates succeed or all fail as a single operation.
>      -    This leverages git replay's existing server-side strengths (in-memory operation,
>      -    no work tree requirement) while adding the atomic guarantees that server
>      -    operations require.
>      +    For users needing the traditional pipeline workflow, add a new
>      +    `--update-refs=<mode>` option that preserves the original behavior:
>       
>      -    For users needing the traditional pipeline workflow, --output-commands
>      -    preserves the original behavior:
>      +      git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin
>       
>      -        git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin
>      -
>      -    The --allow-partial option enables partial failure tolerance. However, following
>      -    maintainer feedback, it implements a "strict success" model: the command exits
>      -    with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY
>      -    updates fail. This ensures that --allow-partial changes error reporting style
>      -    (warnings vs hard errors) but not success criteria, handling edge cases like
>      -    "no updates needed" cleanly.
>      +    The mode can be:
>      +      * `yes` (default): Update refs directly using an atomic transaction
>      +      * `print`: Output update-ref commands for pipeline use
>       
>           Implementation details:
>      -    - Empty commit ranges now return success (exit code 0) rather than failure,
>      -      as no commits to replay is a valid successful operation
>      -    - Added comprehensive test coverage with 12 new tests covering atomic behavior,
>      -      option validation, bare repository support, and edge cases
>      -    - Fixed test isolation issues to prevent branch state contamination between tests
>      -    - Maintains C89 compliance and follows Git's established coding conventions
>      -    - Refactored option validation to use die_for_incompatible_opt2() for both
>      -      --advance/--contained and --allow-partial/--output-commands conflicts,
>      -      providing consistent error reporting
>      -    - Fixed --allow-partial exit code behavior to implement "strict success" model
>      -      where any ref update failures result in exit code 1, even with partial tolerance
>      -    - Updated documentation with proper line wrapping, consistent terminology using
>      -      "old default behavior", performance context, and reorganized examples for clarity
>      -    - Eliminates individual ref updates (refs_update_ref calls) that perform
>      -      poorly with reftable backend
>      -    - Uses only batched ref transactions for optimal performance across all
>      -      ref backends
>      -    - Avoids naming collision with git rebase --update-refs by using distinct
>      -      option names
>      -    - Defaults to atomic behavior while preserving pipeline compatibility
>       
>      -    The result is a command that works better for its primary use case (server-side
>      -    operations) while maintaining full backward compatibility for existing workflows.
>      +    The atomic ref updates are implemented using Git's ref transaction API.
>      +    In cmd_replay(), when not in 'print' mode, we initialize a transaction
>      +    using ref_store_transaction_begin() with the default atomic behavior.
>      +    As commits are replayed, ref updates are staged into the transaction
>      +    using ref_transaction_update(). Finally, ref_transaction_commit()
>      +    applies all updates atomically—either all updates succeed or none do.
>      +
>      +    To avoid code duplication between the 'print' and 'yes' modes, this
>      +    commit extracts a handle_ref_update() helper function. This function
>      +    takes the mode and either prints the update command or stages it into
>      +    the transaction. This keeps both code paths consistent and makes future
>      +    maintenance easier.
>      +
>      +    The helper function signature:
>      +
>      +      static int handle_ref_update(const char *mode,
>      +                                    struct ref_transaction *transaction,
>      +                                    const char *refname,
>      +                                    const struct object_id *new_oid,
>      +                                    const struct object_id *old_oid,
>      +                                    struct strbuf *err)
>      +
>      +    When mode is 'print', it prints the update-ref command. When mode is
>      +    'yes', it calls ref_transaction_update() to stage the update. This
>      +    eliminates the duplication that would otherwise exist at each ref update
>      +    call site.
>      +
>      +    Test suite changes:
>       
>      +    All existing tests that expected command output now use
>      +    `--update-refs=print` to preserve their original behavior. This keeps
>      +    the tests valid while allowing them to verify that the pipeline workflow
>      +    still works correctly.
>      +
>      +    New tests were added to verify:
>      +      - Default atomic behavior (no output, refs updated directly)
>      +      - Bare repository support (server-side use case)
>      +      - Equivalence between traditional pipeline and atomic updates
>      +      - Real atomicity using a lock file to verify all-or-nothing guarantee
>      +      - Test isolation using test_when_finished to clean up state
>      +
>      +    The bare repository tests were fixed to rebuild their expectations
>      +    independently rather than comparing to previous test output, improving
>      +    test reliability and isolation.
>      +
>      +    A following commit will add a `replay.defaultAction` configuration
>      +    option for users who prefer the traditional pipeline output as their
>      +    default behavior.
>      +
>      +    Helped-by: Elijah Newren <newren@gmail.com>
>      +    Helped-by: Patrick Steinhardt <ps@pks.im>
>      +    Helped-by: Christian Couder <christian.couder@gmail.com>
>      +    Helped-by: Phillip Wood <phillip.wood123@gmail.com>
>           Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
>       
>        ## Documentation/git-replay.adoc ##
>      @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne
>        --------
>        [verse]
>       -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
>      -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--output-commands | --allow-partial] <revision-range>...
>      ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>)
>      ++		[--update-refs[=<mode>]] <revision-range>...
>        
>        DESCRIPTION
>        -----------
>      @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne
>       -the working tree and the index untouched, and updates no references.
>       -The output of this command is meant to be used as input to
>       -`git update-ref --stdin`, which would update the relevant branches
>      --(see the OUTPUT section below).
>      -+the working tree and the index untouched, and by default updates the
>      -+relevant references using atomic transactions. Use `--output-commands`
>      -+to get the old default behavior where update commands that can be piped
>      -+to `git update-ref --stdin` are emitted (see the OUTPUT section below).
>      ++the working tree and the index untouched. By default, updates the
>      ++relevant references using an atomic transaction (all refs update or
>      ++none). Use `--update-refs=print` to avoid automatic ref updates and
>      ++instead get update commands that can be piped to `git update-ref --stdin`
>      + (see the OUTPUT section below).
>        
>        THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
>      +@@ Documentation/git-replay.adoc: OPTIONS
>      + 	Starting point at which to create the new commits.  May be any
>      + 	valid commit, and not just an existing branch name.
>      + +
>      +-When `--onto` is specified, the update-ref command(s) in the output will
>      +-update the branch(es) in the revision range to point at the new
>      +-commits, similar to the way how `git rebase --update-refs` updates
>      +-multiple branches in the affected range.
>      ++When `--onto` is specified, the branch(es) in the revision range will be
>      ++updated to point at the new commits (or update commands will be printed
>      ++if `--update-refs=print` is used), similar to the way how
>      ++`git rebase --update-refs` updates multiple branches in the affected range.
>      +
>      + --advance <branch>::
>      + 	Starting point at which to create the new commits; must be a
>      + 	branch name.
>      + +
>      +-When `--advance` is specified, the update-ref command(s) in the output
>      +-will update the branch passed as an argument to `--advance` to point at
>      +-the new commits (in other words, this mimics a cherry-pick operation).
>      ++When `--advance` is specified, the branch passed as an argument will be
>      ++updated to point at the new commits (or an update command will be printed
>      ++if `--update-refs=print` is used). This mimics a cherry-pick operation.
>      ++
>      ++--update-refs[=<mode>]::
>      ++	Control how references are updated. The mode can be:
>      +++
>      ++--
>      ++* `yes` (default): Update refs directly using an atomic transaction.
>      ++  All ref updates succeed or all fail.
>      ++* `print`: Output update-ref commands instead of updating refs.
>      ++  The output can be piped as-is to `git update-ref --stdin`.
>      ++--
>        
>      -@@ Documentation/git-replay.adoc: When `--advance` is specified, the update-ref command(s) in the output
>      - will update the branch passed as an argument to `--advance` to point at
>      - the new commits (in other words, this mimics a cherry-pick operation).
>      -
>      -+--output-commands::
>      -+	Output update-ref commands instead of updating refs directly.
>      -+	When this option is used, the output can be piped to `git update-ref --stdin`
>      -+	for successive, relatively slow, ref updates. This is equivalent to the
>      -+	old default behavior.
>      -+
>      -+--allow-partial::
>      -+	Allow some ref updates to succeed even if others fail. By default,
>      -+	ref updates are atomic (all succeed or all fail). With this option,
>      -+	failed updates are reported as warnings rather than causing the entire
>      -+	command to fail. The command exits with code 0 only if all updates
>      -+	succeed; any failures result in exit code 1. Cannot be used with
>      -+	`--output-commands`.
>      -+
>        <revision-range>::
>        	Range of commits to replay. More than one <revision-range> can
>      - 	be passed, but in `--advance <branch>` mode, they should have
>       @@ Documentation/git-replay.adoc: include::rev-list-options.adoc[]
>        OUTPUT
>        ------
>      @@ Documentation/git-replay.adoc: include::rev-list-options.adoc[]
>       -When there are no conflicts, the output of this command is usable as
>       -input to `git update-ref --stdin`.  It is of the form:
>       +By default, when there are no conflicts, this command updates the relevant
>      -+references using atomic transactions and produces no output. All ref updates
>      -+succeed or all fail (atomic behavior). Use `--allow-partial` to allow some
>      -+updates to succeed while others fail.
>      ++references using an atomic transaction and produces no output. All ref
>      ++updates succeed or all fail.
>       +
>      -+When `--output-commands` is used, the output is usable as input to
>      ++When `--update-refs=print` is used, the output is usable as input to
>       +`git update-ref --stdin`. It is of the form:
>        
>        	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>      @@ Documentation/git-replay.adoc: is something other than 0 or 1.
>       +updates mybranch to point at the new commits and the second updates
>       +target to point at them.
>       +
>      -+To get the old default behavior where update commands are emitted:
>      ++To get the traditional pipeline output:
>       +
>       +------------
>      -+$ git replay --output-commands --onto target origin/main..mybranch
>      ++$ git replay --update-refs=print --onto target origin/main..mybranch
>       +update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
>      -+------------
>      -+
>      -+To rebase multiple branches with partial failure tolerance:
>      -+
>      -+------------
>      -+$ git replay --allow-partial --contained --onto origin/main origin/main..tipbranch
>       +------------
>        
>        What if you have a stack of branches, one depending upon another, and
>      @@ Documentation/git-replay.adoc: is something other than 0 or 1.
>        
>        ------------
>        $ git replay --contained --onto origin/main origin/main..tipbranch
>      -+------------
>      -+
>      +-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>      +-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>      +-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
>      + ------------
>      +
>       +This automatically finds and rebases all branches contained within the
>       +`origin/main..tipbranch` range.
>       +
>      -+Or if you want to see the old default behavior where update commands are emitted:
>      -+
>      -+------------
>      -+$ git replay --output-commands --contained --onto origin/main origin/main..tipbranch
>      - update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>      - update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>      - update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
>      -@@ Documentation/git-replay.adoc: update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
>      -
>        When calling `git replay`, one does not need to specify a range of
>      - commits to replay using the syntax `A..B`; any range expression will
>      +-commits to replay using the syntax `A..B`; any range expression will
>       -do:
>      -+do. Here's an example where you explicitly specify which branches to rebase:
>      ++commits to replay using the syntax `A..B`; any range expression will do:
>        
>        ------------
>        $ git replay --onto origin/main ^base branch1 branch2 branch3
>      -+------------
>      -+
>      -+This gives you explicit control over exactly which branches are rebased,
>      -+unlike the previous `--contained` example which automatically discovers them.
>      -+
>      -+To see the update commands that would be executed:
>      -+
>      -+------------
>      -+$ git replay --output-commands --onto origin/main ^base branch1 branch2 branch3
>      - update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>      - update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>      - update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
>      +-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>      +-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>      +-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
>      + ------------
>      +
>      + This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
>       
>        ## builtin/replay.c ##
>       @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *repo,
>        	return create_commit(repo, result->tree, pickme, replayed_base);
>        }
>        
>      -+static int add_ref_to_transaction(struct ref_transaction *transaction,
>      -+				  const char *refname,
>      -+				  const struct object_id *new_oid,
>      -+				  const struct object_id *old_oid,
>      -+				  struct strbuf *err)
>      ++static int handle_ref_update(const char *mode,
>      ++			     struct ref_transaction *transaction,
>      ++			     const char *refname,
>      ++			     const struct object_id *new_oid,
>      ++			     const struct object_id *old_oid,
>      ++			     struct strbuf *err)
>       +{
>      ++	if (!strcmp(mode, "print")) {
>      ++		printf("update %s %s %s\n",
>      ++		       refname,
>      ++		       oid_to_hex(new_oid),
>      ++		       oid_to_hex(old_oid));
>      ++		return 0;
>      ++	}
>      ++
>      ++	/* mode == "yes" - update refs directly */
>       +	return ref_transaction_update(transaction, refname, new_oid, old_oid,
>       +				      NULL, NULL, 0, "git replay", err);
>       +}
>      -+
>      -+static void print_rejected_update(const char *refname,
>      -+				  const struct object_id *old_oid UNUSED,
>      -+				  const struct object_id *new_oid UNUSED,
>      -+				  const char *old_target UNUSED,
>      -+				  const char *new_target UNUSED,
>      -+				  enum ref_transaction_error err,
>      -+				  void *cb_data UNUSED)
>      -+{
>      -+	const char *reason = ref_transaction_error_msg(err);
>      -+	warning(_("failed to update %s: %s"), refname, reason);
>      -+}
>       +
>        int cmd_replay(int argc,
>        	       const char **argv,
>      @@ builtin/replay.c: int cmd_replay(int argc,
>        	struct commit *onto = NULL;
>        	const char *onto_name = NULL;
>        	int contained = 0;
>      -+	int output_commands = 0;
>      -+	int allow_partial = 0;
>      ++	const char *update_refs_mode = NULL;
>        
>        	struct rev_info revs;
>        	struct commit *last_commit = NULL;
>      @@ builtin/replay.c: int cmd_replay(int argc,
>        	kh_oid_map_t *replayed_commits;
>       +	struct ref_transaction *transaction = NULL;
>       +	struct strbuf transaction_err = STRBUF_INIT;
>      -+	int commits_processed = 0;
>        	int ret = 0;
>        
>       -	const char * const replay_usage[] = {
>      @@ builtin/replay.c: int cmd_replay(int argc,
>        		N_("(EXPERIMENTAL!) git replay "
>        		   "([--contained] --onto <newbase> | --advance <branch>) "
>       -		   "<revision-range>..."),
>      -+		   "[--output-commands | --allow-partial] <revision-range>..."),
>      ++		   "[--update-refs[=<mode>]] <revision-range>..."),
>        		NULL
>        	};
>        	struct option replay_options[] = {
>      @@ builtin/replay.c: int cmd_replay(int argc,
>        			   N_("replay onto given commit")),
>        		OPT_BOOL(0, "contained", &contained,
>        			 N_("advance all branches contained in revision-range")),
>      -+		OPT_BOOL(0, "output-commands", &output_commands,
>      -+			 N_("output update commands instead of updating refs")),
>      -+		OPT_BOOL(0, "allow-partial", &allow_partial,
>      -+			 N_("allow some ref updates to succeed even if others fail")),
>      ++		OPT_STRING(0, "update-refs", &update_refs_mode,
>      ++			   N_("mode"),
>      ++			   N_("control ref update behavior (yes|print)")),
>        		OPT_END()
>        	};
>        
>       @@ builtin/replay.c: int cmd_replay(int argc,
>      - 		usage_with_options(replay_usage, replay_options);
>      - 	}
>      + 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>      + 				  contained, "--contained");
>        
>      --	if (advance_name_opt && contained)
>      --		die(_("options '%s' and '%s' cannot be used together"),
>      --		    "--advance", "--contained");
>      -+	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>      -+				  contained, "--contained");
>      ++	/* Set default mode if not specified */
>      ++	if (!update_refs_mode)
>      ++		update_refs_mode = "yes";
>       +
>      -+	die_for_incompatible_opt2(allow_partial, "--allow-partial",
>      -+				  output_commands, "--output-commands");
>      ++	/* Validate update-refs mode */
>      ++	if (strcmp(update_refs_mode, "yes") && strcmp(update_refs_mode, "print"))
>      ++		die(_("invalid value for --update-refs: '%s' (expected 'yes' or 'print')"),
>      ++		    update_refs_mode);
>       +
>        	advance_name = xstrdup_or_null(advance_name_opt);
>        
>      @@ builtin/replay.c: int cmd_replay(int argc,
>        	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
>        			      &onto, &update_refs);
>        
>      -+	if (!output_commands) {
>      -+		unsigned int transaction_flags = allow_partial ? REF_TRANSACTION_ALLOW_FAILURE : 0;
>      ++	/* Initialize ref transaction if we're updating refs directly */
>      ++	if (!strcmp(update_refs_mode, "yes")) {
>       +		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
>      -+							  transaction_flags,
>      -+							  &transaction_err);
>      ++							  0, &transaction_err);
>       +		if (!transaction) {
>      -+			ret = error(_("failed to begin ref transaction: %s"), transaction_err.buf);
>      ++			ret = error(_("failed to begin ref transaction: %s"),
>      ++				    transaction_err.buf);
>       +			goto cleanup;
>       +		}
>       +	}
>      @@ builtin/replay.c: int cmd_replay(int argc,
>        	if (!onto) /* FIXME: Should handle replaying down to root commit */
>        		die("Replaying down to root commit is not supported yet!");
>        
>      -@@ builtin/replay.c: int cmd_replay(int argc,
>      - 		khint_t pos;
>      - 		int hr;
>      -
>      -+		commits_processed = 1;
>      -+
>      - 		if (!commit->parents)
>      - 			die(_("replaying down to root commit is not supported yet!"));
>      - 		if (commit->parents->next)
>       @@ builtin/replay.c: int cmd_replay(int argc,
>        			if (decoration->type == DECORATION_REF_LOCAL &&
>        			    (contained || strset_contains(update_refs,
>      @@ builtin/replay.c: int cmd_replay(int argc,
>       -				       decoration->name,
>       -				       oid_to_hex(&last_commit->object.oid),
>       -				       oid_to_hex(&commit->object.oid));
>      -+				if (output_commands) {
>      -+					printf("update %s %s %s\n",
>      -+					       decoration->name,
>      -+					       oid_to_hex(&last_commit->object.oid),
>      -+					       oid_to_hex(&commit->object.oid));
>      -+				} else if (add_ref_to_transaction(transaction, decoration->name,
>      -+								  &last_commit->object.oid,
>      -+								  &commit->object.oid,
>      -+								  &transaction_err) < 0) {
>      -+					ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
>      ++				if (handle_ref_update(update_refs_mode, transaction,
>      ++						      decoration->name,
>      ++						      &last_commit->object.oid,
>      ++						      &commit->object.oid,
>      ++						      &transaction_err) < 0) {
>      ++					ret = error(_("failed to update ref '%s': %s"),
>      ++						    decoration->name, transaction_err.buf);
>       +					goto cleanup;
>       +				}
>        			}
>      @@ builtin/replay.c: int cmd_replay(int argc,
>       -		       advance_name,
>       -		       oid_to_hex(&last_commit->object.oid),
>       -		       oid_to_hex(&onto->object.oid));
>      -+		if (output_commands) {
>      -+			printf("update %s %s %s\n",
>      -+			       advance_name,
>      -+			       oid_to_hex(&last_commit->object.oid),
>      -+			       oid_to_hex(&onto->object.oid));
>      -+		} else if (add_ref_to_transaction(transaction, advance_name,
>      -+						  &last_commit->object.oid,
>      -+						  &onto->object.oid,
>      -+						  &transaction_err) < 0) {
>      -+			ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf);
>      ++		if (handle_ref_update(update_refs_mode, transaction,
>      ++				      advance_name,
>      ++				      &last_commit->object.oid,
>      ++				      &onto->object.oid,
>      ++				      &transaction_err) < 0) {
>      ++			ret = error(_("failed to update ref '%s': %s"),
>      ++				    advance_name, transaction_err.buf);
>       +			goto cleanup;
>       +		}
>       +	}
>      @@ builtin/replay.c: int cmd_replay(int argc,
>       +	/* Commit the ref transaction if we have one */
>       +	if (transaction && result.clean == 1) {
>       +		if (ref_transaction_commit(transaction, &transaction_err)) {
>      -+			if (allow_partial) {
>      -+				warning(_("some ref updates failed: %s"), transaction_err.buf);
>      -+				ref_transaction_for_each_rejected_update(transaction,
>      -+									 print_rejected_update, NULL);
>      -+				ret = 0; /* Set failure even with allow_partial */
>      -+			} else {
>      -+				ret = error(_("failed to update refs: %s"), transaction_err.buf);
>      -+				goto cleanup;
>      -+			}
>      ++			ret = error(_("failed to commit ref transaction: %s"),
>      ++				    transaction_err.buf);
>      ++			goto cleanup;
>       +		}
>        	}
>        
>        	merge_finalize(&merge_opt, &result);
>       @@ builtin/replay.c: int cmd_replay(int argc,
>      - 		strset_clear(update_refs);
>      - 		free(update_refs);
>      - 	}
>      --	ret = result.clean;
>      -+
>      -+	/* Handle empty ranges: if no commits were processed, treat as success */
>      -+	if (!commits_processed)
>      -+		ret = 1; /* Success - no commits to replay is not an error */
>      -+	else
>      -+		ret = result.clean;
>      + 	ret = result.clean;
>        
>        cleanup:
>       +	if (transaction)
>      @@ t/t3650-replay-basics.sh: test_expect_success 'setup bare' '
>        
>        test_expect_success 'using replay to rebase two branches, one on top of other' '
>       -	git replay --onto main topic1..topic2 >result &&
>      -+	git replay --output-commands --onto main topic1..topic2 >result &&
>      ++	git replay --update-refs=print --onto main topic1..topic2 >result &&
>        
>        	test_line_count = 1 result &&
>        
>      @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to rebase two branch
>        '
>        
>       +test_expect_success 'using replay with default atomic behavior (no output)' '
>      -+	# Create a test branch that wont interfere with others
>      -+	git branch atomic-test topic2 &&
>      -+	git rev-parse atomic-test >atomic-test-old &&
>      ++	# Store the original state
>      ++	START=$(git rev-parse topic2) &&
>      ++	test_when_finished "git branch -f topic2 $START" &&
>       +
>       +	# Default behavior: atomic ref updates (no output)
>      -+	git replay --onto main topic1..atomic-test >output &&
>      ++	git replay --onto main topic1..topic2 >output &&
>       +	test_must_be_empty output &&
>       +
>      -+	# Verify the branch was updated
>      -+	git rev-parse atomic-test >atomic-test-new &&
>      -+	! test_cmp atomic-test-old atomic-test-new &&
>      -+
>       +	# Verify the history is correct
>      -+	git log --format=%s atomic-test >actual &&
>      ++	git log --format=%s topic2 >actual &&
>       +	test_write_lines E D M L B A >expect &&
>       +	test_cmp expect actual
>       +'
>       +
>        test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
>       -	git -C bare replay --onto main topic1..topic2 >result-bare &&
>      --	test_cmp expect result-bare
>      -+	git -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&
>      ++	git -C bare replay --update-refs=print --onto main topic1..topic2 >result-bare &&
>      ++
>      ++	test_line_count = 1 result-bare &&
>      ++
>      ++	git log --format=%s $(cut -f 3 -d " " result-bare) >actual &&
>      ++	test_write_lines E D M L B A >expect &&
>      ++	test_cmp expect actual &&
>      ++
>      ++	printf "update refs/heads/topic2 " >expect &&
>      ++	printf "%s " $(cut -f 3 -d " " result-bare) >>expect &&
>      ++	git -C bare rev-parse topic2 >>expect &&
>       +
>      -+	# The result should match what we got from the regular repo
>      -+	test_cmp result result-bare
>      + 	test_cmp expect result-bare
>        '
>        
>      - test_expect_success 'using replay to rebase with a conflict' '
>       @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to perform basic cherry-pick' '
>        	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
>        	# 4th field of result is hash for main instead of hash for topic2
>        
>       -	git replay --advance main topic1..topic2 >result &&
>      -+	git replay --output-commands --advance main topic1..topic2 >result &&
>      ++	git replay --update-refs=print --advance main topic1..topic2 >result &&
>        
>        	test_line_count = 1 result &&
>        
>      @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to perform basic che
>        
>        test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
>       -	git -C bare replay --advance main topic1..topic2 >result-bare &&
>      -+	git -C bare replay --output-commands --advance main topic1..topic2 >result-bare &&
>      ++	git -C bare replay --update-refs=print --advance main topic1..topic2 >result-bare &&
>      ++
>      ++	test_line_count = 1 result-bare &&
>      ++
>      ++	git log --format=%s $(cut -f 3 -d " " result-bare) >actual &&
>      ++	test_write_lines E D M L B A >expect &&
>      ++	test_cmp expect actual &&
>      ++
>      ++	printf "update refs/heads/main " >expect &&
>      ++	printf "%s " $(cut -f 3 -d " " result-bare) >>expect &&
>      ++	git -C bare rev-parse main >>expect &&
>      ++
>        	test_cmp expect result-bare
>        '
>        
>      @@ t/t3650-replay-basics.sh: test_expect_success 'replay fails when both --advance
>        
>        test_expect_success 'using replay to also rebase a contained branch' '
>       -	git replay --contained --onto main main..topic3 >result &&
>      -+	git replay --output-commands --contained --onto main main..topic3 >result &&
>      ++	git replay --update-refs=print --contained --onto main main..topic3 >result &&
>        
>        	test_line_count = 2 result &&
>        	cut -f 3 -d " " result >new-branch-tips &&
>      @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to also rebase a con
>        
>        test_expect_success 'using replay on bare repo to also rebase a contained branch' '
>       -	git -C bare replay --contained --onto main main..topic3 >result-bare &&
>      -+	git -C bare replay --output-commands --contained --onto main main..topic3 >result-bare &&
>      ++	git -C bare replay --update-refs=print --contained --onto main main..topic3 >result-bare &&
>      ++
>      ++	test_line_count = 2 result-bare &&
>      ++	cut -f 3 -d " " result-bare >new-branch-tips &&
>      ++
>      ++	git log --format=%s $(head -n 1 new-branch-tips) >actual &&
>      ++	test_write_lines F C M L B A >expect &&
>      ++	test_cmp expect actual &&
>      ++
>      ++	git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
>      ++	test_write_lines H G F C M L B A >expect &&
>      ++	test_cmp expect actual &&
>      ++
>      ++	printf "update refs/heads/topic1 " >expect &&
>      ++	printf "%s " $(head -n 1 new-branch-tips) >>expect &&
>      ++	git -C bare rev-parse topic1 >>expect &&
>      ++	printf "update refs/heads/topic3 " >>expect &&
>      ++	printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
>      ++	git -C bare rev-parse topic3 >>expect &&
>      ++
>        	test_cmp expect result-bare
>        '
>        
>        test_expect_success 'using replay to rebase multiple divergent branches' '
>       -	git replay --onto main ^topic1 topic2 topic4 >result &&
>      -+	git replay --output-commands --onto main ^topic1 topic2 topic4 >result &&
>      ++	git replay --update-refs=print --onto main ^topic1 topic2 topic4 >result &&
>        
>        	test_line_count = 2 result &&
>        	cut -f 3 -d " " result >new-branch-tips &&
>      @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to rebase multiple d
>        
>        test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
>       -	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
>      -+	git -C bare replay --output-commands --contained --onto main ^main topic2 topic3 topic4 >result &&
>      ++	git -C bare replay --update-refs=print --contained --onto main ^main topic2 topic3 topic4 >result &&
>        
>        	test_line_count = 4 result &&
>        	cut -f 3 -d " " result >new-branch-tips &&
>      @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '
>        		--onto rename-onto rename-onto..rename-from
>        '
>        
>      -+# Tests for new default atomic behavior and options
>      -+
>      -+test_expect_success 'replay default behavior should not produce output when successful' '
>      -+	git replay --onto main topic1..topic3 >output &&
>      -+	test_must_be_empty output
>      -+'
>      -+
>      -+test_expect_success 'replay with --output-commands produces traditional output' '
>      -+	git replay --output-commands --onto main topic1..topic3 >output &&
>      -+	test_line_count = 1 output &&
>      -+	grep "^update refs/heads/topic3 " output
>      -+'
>      -+
>      -+test_expect_success 'replay with --allow-partial should not produce output when successful' '
>      -+	git replay --allow-partial --onto main topic1..topic3 >output &&
>      -+	test_must_be_empty output
>      -+'
>      -+
>      -+test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '
>      -+	test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
>      -+	grep "cannot be used together" error
>      -+'
>      ++# Tests for atomic ref update behavior
>       +
>       +test_expect_success 'replay with --contained updates multiple branches atomically' '
>      -+	# Create fresh test branches based on the original structure
>      -+	# contained-topic1 should be contained within the range to contained-topic3
>      -+	git branch contained-base main &&
>      -+	git checkout -b contained-topic1 contained-base &&
>      -+	test_commit ContainedC &&
>      -+	git checkout -b contained-topic3 contained-topic1 &&
>      -+	test_commit ContainedG &&
>      -+	test_commit ContainedH &&
>      -+	git checkout main &&
>      -+
>       +	# Store original states
>      -+	git rev-parse contained-topic1 >contained-topic1-old &&
>      -+	git rev-parse contained-topic3 >contained-topic3-old &&
>      -+
>      -+	# Use --contained to update multiple branches - this should update both
>      -+	git replay --contained --onto main contained-base..contained-topic3 &&
>      -+
>      -+	# Verify both branches were updated
>      -+	git rev-parse contained-topic1 >contained-topic1-new &&
>      -+	git rev-parse contained-topic3 >contained-topic3-new &&
>      -+	! test_cmp contained-topic1-old contained-topic1-new &&
>      -+	! test_cmp contained-topic3-old contained-topic3-new
>      -+'
>      ++	START_TOPIC1=$(git rev-parse topic1) &&
>      ++	START_TOPIC3=$(git rev-parse topic3) &&
>      ++	test_when_finished "git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3" &&
>       +
>      -+test_expect_success 'replay atomic behavior: all refs updated or none' '
>      -+	# Store original state
>      -+	git rev-parse topic4 >topic4-old &&
>      -+
>      -+	# Default atomic behavior
>      -+	git replay --onto main main..topic4 &&
>      ++	# Use --contained to update multiple branches
>      ++	git replay --contained --onto main main..topic3 >output &&
>      ++	test_must_be_empty output &&
>       +
>      -+	# Verify ref was updated
>      -+	git rev-parse topic4 >topic4-new &&
>      -+	! test_cmp topic4-old topic4-new &&
>      ++	# Verify both branches were updated with correct commit sequences
>      ++	git log --format=%s topic1 >actual &&
>      ++	test_write_lines F C M L B A >expect &&
>      ++	test_cmp expect actual &&
>       +
>      -+	# Verify no partial state
>      -+	git log --format=%s topic4 >actual &&
>      -+	test_write_lines J I M L B A >expect &&
>      ++	git log --format=%s topic3 >actual &&
>      ++	test_write_lines H G F C M L B A >expect &&
>       +	test_cmp expect actual
>       +'
>       +
>      -+test_expect_success 'replay works correctly with bare repositories' '
>      -+	# Test atomic behavior in bare repo (important for Gitaly)
>      -+	git checkout -b bare-test topic1 &&
>      -+	test_commit BareTest &&
>      ++test_expect_success 'replay atomic guarantee: all refs updated or none' '
>      ++	# Store original states
>      ++	START_TOPIC1=$(git rev-parse topic1) &&
>      ++	START_TOPIC3=$(git rev-parse topic3) &&
>      ++	test_when_finished "git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3 && rm -f .git/refs/heads/topic1.lock" &&
>       +
>      -+	# Test with bare repo - replay the commits from main..bare-test to get the full history
>      -+	git -C bare fetch .. bare-test:bare-test &&
>      -+	git -C bare replay --onto main main..bare-test &&
>      ++	# Create a lock on topic1 to simulate a concurrent update
>      ++	>.git/refs/heads/topic1.lock &&
>       +
>      -+	# Verify the bare repo was updated correctly (no output)
>      -+	git -C bare log --format=%s bare-test >actual &&
>      -+	test_write_lines BareTest F C M L B A >expect &&
>      -+	test_cmp expect actual
>      -+'
>      ++	# Try to update multiple branches with --contained
>      ++	# This should fail atomically - neither branch should be updated
>      ++	test_must_fail git replay --contained --onto main main..topic3 2>error &&
>       +
>      -+test_expect_success 'replay --allow-partial with no failures produces no output' '
>      -+	git checkout -b partial-test topic1 &&
>      -+	test_commit PartialTest &&
>      ++	# Verify the transaction failed
>      ++	grep "failed to commit ref transaction" error &&
>       +
>      -+	# Should succeed silently even with partial mode
>      -+	git replay --allow-partial --onto main topic1..partial-test >output &&
>      -+	test_must_be_empty output
>      ++	# Verify NEITHER branch was updated (all-or-nothing guarantee)
>      ++	test_cmp_rev $START_TOPIC1 topic1 &&
>      ++	test_cmp_rev $START_TOPIC3 topic3
>       +'
>       +
>      -+test_expect_success 'replay maintains ref update consistency' '
>      -+	# Test that traditional vs atomic produce equivalent results
>      -+	git checkout -b method1-test topic2 &&
>      -+	git checkout -b method2-test topic2 &&
>      ++test_expect_success 'traditional pipeline and atomic update produce equivalent results' '
>      ++	# Store original states
>      ++	START_TOPIC2=$(git rev-parse topic2) &&
>      ++	test_when_finished "git branch -f topic2 $START_TOPIC2" &&
>       +
>      -+	# Both methods should update refs to point to the same replayed commits
>      -+	git replay --output-commands --onto main topic1..method1-test >update-commands &&
>      ++	# Traditional method: output commands and pipe to update-ref
>      ++	git replay --update-refs=print --onto main topic1..topic2 >update-commands &&
>       +	git update-ref --stdin <update-commands &&
>      -+	git log --format=%s method1-test >traditional-result &&
>      ++	git log --format=%s topic2 >traditional-result &&
>      ++
>      ++	# Reset topic2
>      ++	git branch -f topic2 $START_TOPIC2 &&
>       +
>      -+	# Direct atomic method should produce same commit history
>      -+	git replay --onto main topic1..method2-test &&
>      -+	git log --format=%s method2-test >atomic-result &&
>      ++	# Atomic method: direct ref updates
>      ++	git replay --onto main topic1..topic2 &&
>      ++	git log --format=%s topic2 >atomic-result &&
>       +
>       +	# Both methods should produce identical commit histories
>       +	test_cmp traditional-result atomic-result
>       +'
>       +
>      -+test_expect_success 'replay error messages are helpful and clear' '
>      -+	# Test that error messages are clear
>      -+	test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&
>      -+	grep "cannot be used together" error
>      -+'
>      -+
>      -+test_expect_success 'replay with empty range produces no output and no changes' '
>      -+	# Create a test branch for empty range testing
>      -+	git checkout -b empty-test topic1 &&
>      -+	git rev-parse empty-test >empty-test-before &&
>      -+
>      -+	# Empty range should succeed but do nothing
>      -+	git replay --onto main empty-test..empty-test >output &&
>      ++test_expect_success 'replay works correctly with bare repositories' '
>      ++	# Test atomic behavior in bare repo
>      ++	git -C bare fetch .. topic1:bare-test-branch &&
>      ++	git -C bare replay --onto main main..bare-test-branch >output &&
>       +	test_must_be_empty output &&
>       +
>      -+	# Branch should be unchanged
>      -+	git rev-parse empty-test >empty-test-after &&
>      -+	test_cmp empty-test-before empty-test-after
>      ++	# Verify the bare repo was updated correctly
>      ++	git -C bare log --format=%s bare-test-branch >actual &&
>      ++	test_write_lines F C M L B A >expect &&
>      ++	test_cmp expect actual
>      ++'
>      ++
>      ++test_expect_success 'replay validates --update-refs mode values' '
>      ++	test_must_fail git replay --update-refs=invalid --onto main topic1..topic2 2>error &&
>      ++	grep "invalid value for --update-refs" error
>       +'
>       +
>        test_done
> -:  ---------- > 3:  710ab27ae3 replay: add replay.defaultAction config option

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v3 0/3] replay: make atomic ref updates the default
  2025-10-13 18:33   ` [PATCH v3 0/3] replay: make atomic ref updates the default Siddharth Asthana
                       ` (2 preceding siblings ...)
  2025-10-13 18:33     ` [PATCH v3 3/3] replay: add replay.defaultAction config option Siddharth Asthana
@ 2025-10-13 19:39     ` Junio C Hamano
  2025-10-15  4:57       ` Siddharth Asthana
  2025-10-22 18:50     ` [PATCH v4 " Siddharth Asthana
  4 siblings, 1 reply; 129+ messages in thread
From: Junio C Hamano @ 2025-10-13 19:39 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> **Removed --allow-partial option**
>
> After discussion with Elijah and Junio, we couldn't identify a concrete
> use case for partial failure tolerance. The traditional pipeline with
> git-update-ref already provides partial update capabilities when needed
> through its transaction commands. Removing this option simplifies the API
> and avoids committing to behavior without clear real-world use cases.

Ack.

> **Changed to --update-refs=<mode> for extensibility**
>
> Phillip suggested that separate boolean flags (--output-commands,
> --allow-partial) were limiting for future expansion. The --update-refs=<mode>
> design allows future modes without option proliferation:
>   - --update-refs=yes (default): atomic ref updates
>   - --update-refs=print: pipeline output
>   - Future modes can be added as additional values
>
> This API pattern prevents the need for multiple incompatible flags and
> provides a cleaner interface for users.

Ack.

> **Added replay.defaultAction configuration option**

If a configuration option is added, please consider and think hard
if its relationship with the command lineoption can be made obvious.
I do not think it is obvious to anybody that replay.defaultAction is
somehow tied to "git replay --update-refs" at all.  Either the
variable should be renamed to include words like "update" and/or
"ref" to hint its link to the option, or the option should be
renamed to use the word "action" to hint its link to the variable.

> The command-line --update-refs option overrides the config, allowing users
> to set a preference while maintaining per-invocation control.

That would follow the standard practice of configuration giving the
default that can be overriden via the command line option per
invocation, which would match end-user expectations.  Good.

Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v3 2/3] replay: make atomic ref updates the default behavior
  2025-10-13 18:33     ` [PATCH v3 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
@ 2025-10-13 22:05       ` Junio C Hamano
  2025-10-15  5:01         ` Siddharth Asthana
  0 siblings, 1 reply; 129+ messages in thread
From: Junio C Hamano @ 2025-10-13 22:05 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> For users needing the traditional pipeline workflow, add a new
> `--update-refs=<mode>` option that preserves the original behavior:
>
>   git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin
>
> The mode can be:
>   * `yes` (default): Update refs directly using an atomic transaction
>   * `print`: Output update-ref commands for pipeline use

Is it only me who still finds this awkward?  A question "update?"
that gets answered "yes" is quite understandable, but it is not
immediately obvious what it means to answer "print" to the same
question.  When the user gives the latter mode as the answer to the
question, the question being answered is not really "do you want to
update refs?" at all.

The question the command wants the user to answer is more like "what
action do you want to see performed on the refs?", isn't it?  The
user would answer to the question with "please update them" to get
the default mode, while "please print them" may be the answer the
user would give to get the useful-for-dry-run-and-development mode.

Perhaps phrase it more like "--ref-action=(update|print)"?  I dunno.

>  --advance <branch>::
>  	Starting point at which to create the new commits; must be a
>  	branch name.
>  +
> -When `--advance` is specified, the update-ref command(s) in the output
> -will update the branch passed as an argument to `--advance` to point at
> -the new commits (in other words, this mimics a cherry-pick operation).
> +When `--advance` is specified, the branch passed as an argument will be
> +updated to point at the new commits (or an update command will be printed
> +if `--update-refs=print` is used). This mimics a cherry-pick operation.

I do not find it clear what the reference to cherry-pick is trying
to convey.  It is like cherry-picking <something> while the <branch>
is checked out (hence the branch advances as the result of acquiring
these commits from <something>)?  Let me see if I understood you by
attempting to rephrase.

    The history is replayed on top of the <branch> and <branch> is
    updated to point at the tip of resulting history.

But what's the significance of saying so?  Did you want to contrast
it with "rebase --onto <branch>", i.e. merely specifying the
starting point without <branch> itself moving as the result?  If so,
it is probably a notable distinction worth pointing out, but just
saying "mimics a cherry-pick operation" alone is probably not enough
to get the intended audience understand what you wanted to tell
them.

    Side note.  I casually wrote "is updated to point" but with the
    option not to update (but show the way to update refs), we'd
    probably need to find a good phrase to express "where the
    command _wants_ to see the refs pointing at as the result",
    without referring to who/how the refs are made to point at these
    points.

> -To simply rebase `mybranch` onto `target`:
> +To simply rebase `mybranch` onto `target` (default behavior):

"the default"?

> diff --git a/builtin/replay.c b/builtin/replay.c
> index b64fc72063..457225363e 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -284,6 +284,26 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  	return create_commit(repo, result->tree, pickme, replayed_base);
>  }
>  
> +static int handle_ref_update(const char *mode,
> +			     struct ref_transaction *transaction,
> +			     const char *refname,
> +			     const struct object_id *new_oid,
> +			     const struct object_id *old_oid,
> +			     struct strbuf *err)
> +{
> +	if (!strcmp(mode, "print")) {
> +		printf("update %s %s %s\n",
> +		       refname,
> +		       oid_to_hex(new_oid),
> +		       oid_to_hex(old_oid));
> +		return 0;
> +	}
> +
> +	/* mode == "yes" - update refs directly */
> +	return ref_transaction_update(transaction, refname, new_oid, old_oid,
> +				      NULL, NULL, 0, "git replay", err);
> +}

Hmph, would it be easier to follow if the above is symmetric, i.e.,

	if (...) {
		what happens in the "print" mode
	} else {
		what happens in the "update ourselves" mode
	}

I wonder?

In any case, do not pass mode as "const char *" around in the call
chain.  Instead, reduce it down to an enum or integer (with CPP
macro) at the earliest possible place after you saw the command line
option.  That would allow you to even do

	switch (ref_action) {
	case PRINT_INSN:
		printf("update ...");
		return 0;
	case UPDATE_OURSELVES:
		return ref_transaction_update(...);
	default:
		BUG("Bad ref_action %d", ref_action);
	}

to future-proof for the third option.

> +		OPT_STRING(0, "update-refs", &update_refs_mode,
> +			   N_("mode"),
> +			   N_("control ref update behavior (yes|print)")),
>  		OPT_END()
>  	};

This one is fine, but then immediately after parse_options()
returns, do something like

	if (!strcmp(update_refs_mode, "print"))
		ref_action = PRINT_INSN;
	else if (!strcmp(update_refs_mode, "yes"))
		ref_action = UPDATE_OURSELVES;
	else
		die(_("unknown option --update-ref='%s'"),
		    update_refs_mode);

so that you do not have to keep strcmp() with "print", which risks
you to mistype "prnit" and no compiler would protect against that.


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v3 0/3] replay: make atomic ref updates the default
  2025-10-13 18:25 [PATCH v3 " Siddharth Asthana
  2025-10-13 18:55 ` Siddharth Asthana
@ 2025-10-14 21:13 ` Junio C Hamano
  2025-10-15  5:05   ` Siddharth Asthana
  1 sibling, 1 reply; 129+ messages in thread
From: Junio C Hamano @ 2025-10-14 21:13 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

When merged to 'seen', this breaks t0450; from the way the test
breaks, I suspect that it has the same breakage if the topic gets
tested standalone.

    $ make
    $ cd t
    $ sh t0450-txt-doc-vs-help.sh -i -v
    ...
    --- adoc        2025-10-14 21:02:48.680184914 +0000
    +++ help        2025-10-14 21:02:48.688184867 +0000
    @@ -1,2 +1 @@
    -(EXPERIMENTAL!) git replay ([--contained] --onto <newbase> | --advance <branch>)
    -           [--update-refs[=<mode>]] <revision-range>...
    +(EXPERIMENTAL!) git replay ([--contained] --onto <newbase> | --advance <branch>) [--update-refs[=<mode>]] <revision-range>...
    not ok ...

In short, "git replay -h" and the initial part of "git replay --help"
must match.

Minimally you'd need to squash in something like the following
patch.  Alternatively, you could match the documentation page (which
is shown by "git replay --help") to match what "git replay -h" gives.


 builtin/replay.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git i/builtin/replay.c w/builtin/replay.c
index 3c618bf100..d0f0492790 100644
--- i/builtin/replay.c
+++ w/builtin/replay.c
@@ -330,7 +330,7 @@ int cmd_replay(int argc,
 
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
-		   "([--contained] --onto <newbase> | --advance <branch>) "
+		   "([--contained] --onto <newbase> | --advance <branch>)\n"
 		   "[--update-refs[=<mode>]] <revision-range>..."),
 		NULL
 	};

^ permalink raw reply related	[flat|nested] 129+ messages in thread

* Re: [PATCH v3 0/3] replay: make atomic ref updates the default
  2025-10-13 19:39     ` [PATCH v3 0/3] replay: make atomic ref updates the default Junio C Hamano
@ 2025-10-15  4:57       ` Siddharth Asthana
  2025-10-15 10:33         ` Christian Couder
  2025-10-15 14:45         ` Junio C Hamano
  0 siblings, 2 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-15  4:57 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 14/10/25 01:09, Junio C Hamano wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>> **Removed --allow-partial option**
>>
>> After discussion with Elijah and Junio, we couldn't identify a concrete
>> use case for partial failure tolerance. The traditional pipeline with
>> git-update-ref already provides partial update capabilities when needed
>> through its transaction commands. Removing this option simplifies the API
>> and avoids committing to behavior without clear real-world use cases.
> Ack.
>
>> **Changed to --update-refs=<mode> for extensibility**
>>
>> Phillip suggested that separate boolean flags (--output-commands,
>> --allow-partial) were limiting for future expansion. The --update-refs=<mode>
>> design allows future modes without option proliferation:
>>    - --update-refs=yes (default): atomic ref updates
>>    - --update-refs=print: pipeline output
>>    - Future modes can be added as additional values
>>
>> This API pattern prevents the need for multiple incompatible flags and
>> provides a cleaner interface for users.
> Ack.
>
>> **Added replay.defaultAction configuration option**
> If a configuration option is added, please consider and think hard
> if its relationship with the command lineoption can be made obvious.
> I do not think it is obvious to anybody that replay.defaultAction is
> somehow tied to "git replay --update-refs" at all.  Either the
> variable should be renamed to include words like "update" and/or
> "ref" to hint its link to the option, or the option should be
> renamed to use the word "action" to hint its link to the variable.


You are absolutely right - the disconnect between `replay.defaultAction` and
`--update-refs` makes the relationship unclear. I chose `defaultAction` 
thinking
it would be more extensible if we add other behaviors in the future, but 
that
came at the cost of discoverability.

Looking at how other Git commands handle this, I see a few patterns:
- `commit.cleanup` ↔ `--cleanup=<mode>`
- `push.default` ↔ (implicit push behavior)
- `log.decorate` ↔ `--decorate=<mode>`

Given your feedback in the other thread about `--ref-action` potentially 
being
clearer than `--update-refs`, would it make sense to align both?

Option 1: `replay.refAction` ↔ `--ref-action=(update|print)`
Option 2: `replay.updateRefs` ↔ `--update-refs=(yes|print)`

I am leaning toward Option 1 because:
- "ref-action" clearly conveys "what action to take on refs"
- The config name `replay.refAction` directly mirrors the option
- It's more obvious what the relationship is

What do you think? I am happy to go with either approach or a different 
naming
scheme if you have a preference.

Thanks,
Siddharth


>
>> The command-line --update-refs option overrides the config, allowing users
>> to set a preference while maintaining per-invocation control.
> That would follow the standard practice of configuration giving the
> default that can be overriden via the command line option per
> invocation, which would match end-user expectations.  Good.
>
> Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v3 2/3] replay: make atomic ref updates the default behavior
  2025-10-13 22:05       ` Junio C Hamano
@ 2025-10-15  5:01         ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-15  5:01 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 14/10/25 03:35, Junio C Hamano wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>> For users needing the traditional pipeline workflow, add a new
>> `--update-refs=<mode>` option that preserves the original behavior:
>>
>>    git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin
>>
>> The mode can be:
>>    * `yes` (default): Update refs directly using an atomic transaction
>>    * `print`: Output update-ref commands for pipeline use
> Is it only me who still finds this awkward?  A question "update?"
> that gets answered "yes" is quite understandable, but it is not
> immediately obvious what it means to answer "print" to the same
> question.  When the user gives the latter mode as the answer to the
> question, the question being answered is not really "do you want to
> update refs?" at all.
>
> The question the command wants the user to answer is more like "what
> action do you want to see performed on the refs?", isn't it?  The
> user would answer to the question with "please update them" to get
> the default mode, while "please print them" may be the answer the
> user would give to get the useful-for-dry-run-and-development mode.
>
> Perhaps phrase it more like "--ref-action=(update|print)"?  I dunno.


That's a really good point. I was thinking of it as "update refs? 
yes/no" where
"print" meant "don't update", but you're right that it's actually asking a
different question entirely. The real question is "what should we do 
with the
refs?" and the answer is either "update them" or "print the commands".

`--ref-action=(update|print)` is much clearer because:
- It explicitly asks "what action?"
- Both values are verbs that answer that question consistently
- It's immediately obvious what each mode does
- It aligns with the config name discussion in the cover letter thread

I will switch to `--ref-action` in the next version. This also means the 
config
would naturally be `replay.refAction`, which makes the relationship obvious.


>
>>   --advance <branch>::
>>   	Starting point at which to create the new commits; must be a
>>   	branch name.
>>   +
>> -When `--advance` is specified, the update-ref command(s) in the output
>> -will update the branch passed as an argument to `--advance` to point at
>> -the new commits (in other words, this mimics a cherry-pick operation).
>> +When `--advance` is specified, the branch passed as an argument will be
>> +updated to point at the new commits (or an update command will be printed
>> +if `--update-refs=print` is used). This mimics a cherry-pick operation.
> I do not find it clear what the reference to cherry-pick is trying
> to convey.  It is like cherry-picking <something> while the <branch>
> is checked out (hence the branch advances as the result of acquiring
> these commits from <something>)?  Let me see if I understood you by
> attempting to rephrase.
>
>      The history is replayed on top of the <branch> and <branch> is
>      updated to point at the tip of resulting history.


Your phrasing is much better. The cherry-pick comparison was trying to 
contrast
with `--onto` (which doesn't move the target branch), but it ended up 
being more
confusing than helpful. I will use your wording:

     The history is replayed on top of the <branch> and <branch> is
     updated to point at the tip of the resulting history. This is different
     from `--onto`, which uses the target only as a starting point without
     updating it.


>
> But what's the significance of saying so?  Did you want to contrast
> it with "rebase --onto <branch>", i.e. merely specifying the
> starting point without <branch> itself moving as the result?  If so,
> it is probably a notable distinction worth pointing out, but just
> saying "mimics a cherry-pick operation" alone is probably not enough
> to get the intended audience understand what you wanted to tell
> them.
>
>      Side note.  I casually wrote "is updated to point" but with the
>      option not to update (but show the way to update refs), we'd
>      probably need to find a good phrase to express "where the
>      command _wants_ to see the refs pointing at as the result",
>      without referring to who/how the refs are made to point at these
>      points.
>
>> -To simply rebase `mybranch` onto `target`:
>> +To simply rebase `mybranch` onto `target` (default behavior):
> "the default"?


Good catch - I was trying to emphasize that the atomic update behavior 
is now
default, but in the context of showing example commands, "default behavior"
doesn't add clarity. I'll just say "To simply rebase `mybranch` onto 
`target`:"


>
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index b64fc72063..457225363e 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -284,6 +284,26 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>   	return create_commit(repo, result->tree, pickme, replayed_base);
>>   }
>>   
>> +static int handle_ref_update(const char *mode,
>> +			     struct ref_transaction *transaction,
>> +			     const char *refname,
>> +			     const struct object_id *new_oid,
>> +			     const struct object_id *old_oid,
>> +			     struct strbuf *err)
>> +{
>> +	if (!strcmp(mode, "print")) {
>> +		printf("update %s %s %s\n",
>> +		       refname,
>> +		       oid_to_hex(new_oid),
>> +		       oid_to_hex(old_oid));
>> +		return 0;
>> +	}
>> +
>> +	/* mode == "yes" - update refs directly */
>> +	return ref_transaction_update(transaction, refname, new_oid, old_oid,
>> +				      NULL, NULL, 0, "git replay", err);
>> +}
> Hmph, would it be easier to follow if the above is symmetric, i.e.,
>
> 	if (...) {
> 		what happens in the "print" mode
> 	} else {
> 		what happens in the "update ourselves" mode
> 	}
>
> I wonder?
>
> In any case, do not pass mode as "const char *" around in the call
> chain.  Instead, reduce it down to an enum or integer (with CPP
> macro) at the earliest possible place after you saw the command line
> option.  That would allow you to even do
>
> 	switch (ref_action) {
> 	case PRINT_INSN:
> 		printf("update ...");
> 		return 0;
> 	case UPDATE_OURSELVES:
> 		return ref_transaction_update(...);
> 	default:
> 		BUG("Bad ref_action %d", ref_action);
> 	}
>
> to future-proof for the third option.


Perfect, I will convert to an enum right after parse_options(). This 
approach is
much cleaner and prevents typos like "prnit" that the compiler can't catch.
Something like:

     enum ref_action_mode {
         REF_ACTION_UPDATE,
         REF_ACTION_PRINT
     };

Then parse it early:

     if (!strcmp(ref_action_str, "update"))
         ref_action = REF_ACTION_UPDATE;
     else if (!strcmp(ref_action_str, "print"))
         ref_action = REF_ACTION_PRINT;
     else
         die(_("unknown --ref-action mode '%s'"), ref_action_str);

And use the switch statement in handle_ref_update(). This also makes it 
trivial
to add new modes in the future without string comparison overhead throughout
the code.

Thanks for the detailed review!

Siddharth


>
>> +		OPT_STRING(0, "update-refs", &update_refs_mode,
>> +			   N_("mode"),
>> +			   N_("control ref update behavior (yes|print)")),
>>   		OPT_END()
>>   	};
> This one is fine, but then immediately after parse_options()
> returns, do something like
>
> 	if (!strcmp(update_refs_mode, "print"))
> 		ref_action = PRINT_INSN;
> 	else if (!strcmp(update_refs_mode, "yes"))
> 		ref_action = UPDATE_OURSELVES;
> 	else
> 		die(_("unknown option --update-ref='%s'"),
> 		    update_refs_mode);
>
> so that you do not have to keep strcmp() with "print", which risks
> you to mistype "prnit" and no compiler would protect against that.
>

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v3 0/3] replay: make atomic ref updates the default
  2025-10-14 21:13 ` Junio C Hamano
@ 2025-10-15  5:05   ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-15  5:05 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 15/10/25 02:43, Junio C Hamano wrote:
> When merged to 'seen', this breaks t0450; from the way the test
> breaks, I suspect that it has the same breakage if the topic gets
> tested standalone.
>
>      $ make
>      $ cd t
>      $ sh t0450-txt-doc-vs-help.sh -i -v
>      ...
>      --- adoc        2025-10-14 21:02:48.680184914 +0000
>      +++ help        2025-10-14 21:02:48.688184867 +0000
>      @@ -1,2 +1 @@
>      -(EXPERIMENTAL!) git replay ([--contained] --onto <newbase> | --advance <branch>)
>      -           [--update-refs[=<mode>]] <revision-range>...
>      +(EXPERIMENTAL!) git replay ([--contained] --onto <newbase> | --advance <branch>) [--update-refs[=<mode>]] <revision-range>...
>      not ok ...
>
> In short, "git replay -h" and the initial part of "git replay --help"
> must match.


Thanks for catching this! I actually noticed the CI was failing on 
documentation
checks while testing on GitLab before sending v3 to the list. I 
initially thought
it was an AsciiDoc line continuation issue and suggested adding a `+` at 
the end
of the line, but Christian pointed out that the real issue was likely 
the mismatch
between the synopsis and the help output from the command itself.

I split the SYNOPSIS across two lines in the documentation for readability:

     (EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | 
--advance <branch>)
             [--update-refs[=<mode>]] <revision-range>...

But didn't update the usage string in builtin/replay.c to match. Your 
patch adding
"\n" and the proper indentation is exactly what's needed:

     "(EXPERIMENTAL!) git replay ([--contained] --onto <newbase> | 
--advance <branch>)\n"
     "\t\t[--update-refs[=<mode>]] <revision-range>..."

I will squash this into the next version. I should have run t0450 
locally after
Christian's hint about the synopsis check - I was focused on t3650 and the
functional tests but missed this formatting requirement.

Thanks,
Siddharth


>
> Minimally you'd need to squash in something like the following
> patch.  Alternatively, you could match the documentation page (which
> is shown by "git replay --help") to match what "git replay -h" gives.
>
>
>   builtin/replay.c | 2 +-
>   1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git i/builtin/replay.c w/builtin/replay.c
> index 3c618bf100..d0f0492790 100644
> --- i/builtin/replay.c
> +++ w/builtin/replay.c
> @@ -330,7 +330,7 @@ int cmd_replay(int argc,
>   
>   	const char *const replay_usage[] = {
>   		N_("(EXPERIMENTAL!) git replay "
> -		   "([--contained] --onto <newbase> | --advance <branch>) "
> +		   "([--contained] --onto <newbase> | --advance <branch>)\n"
>   		   "[--update-refs[=<mode>]] <revision-range>..."),
>   		NULL
>   	};

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v3 0/3] replay: make atomic ref updates the default
  2025-10-15  4:57       ` Siddharth Asthana
@ 2025-10-15 10:33         ` Christian Couder
  2025-10-15 14:45         ` Junio C Hamano
  1 sibling, 0 replies; 129+ messages in thread
From: Christian Couder @ 2025-10-15 10:33 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: Junio C Hamano, git, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Wed, Oct 15, 2025 at 6:57 AM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:

> Given your feedback in the other thread about `--ref-action` potentially
> being
> clearer than `--update-refs`, would it make sense to align both?
>
> Option 1: `replay.refAction` ↔ `--ref-action=(update|print)`
> Option 2: `replay.updateRefs` ↔ `--update-refs=(yes|print)`
>
> I am leaning toward Option 1 because:
> - "ref-action" clearly conveys "what action to take on refs"
> - The config name `replay.refAction` directly mirrors the option
> - It's more obvious what the relationship is
>
> What do you think? I am happy to go with either approach or a different
> naming
> scheme if you have a preference.

I prefer Option 1.

Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v3 0/3] replay: make atomic ref updates the default
  2025-10-15  4:57       ` Siddharth Asthana
  2025-10-15 10:33         ` Christian Couder
@ 2025-10-15 14:45         ` Junio C Hamano
  1 sibling, 0 replies; 129+ messages in thread
From: Junio C Hamano @ 2025-10-15 14:45 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> Option 1: `replay.refAction` ↔ `--ref-action=(update|print)`
> Option 2: `replay.updateRefs` ↔ `--update-refs=(yes|print)`
>
> I am leaning toward Option 1 because:
> - "ref-action" clearly conveys "what action to take on refs"
> - The config name `replay.refAction` directly mirrors the option
> - It's more obvious what the relationship is
>
> What do you think? I am happy to go with either approach or a
> different naming scheme if you have a preference.

My preference is the refAction, simply because updateRefs sounds to
me like it is asking "do you want me to update refs?  Yes or no?".

But perhaps there were those who supported updateRefs during the
past reviews I wasn't looking at, so I'd like to hear if my thinking
is missing something that were taken into consideration to come up
with that name.

Thanks.



^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v4 0/3] replay: make atomic ref updates the default
  2025-10-13 18:33   ` [PATCH v3 0/3] replay: make atomic ref updates the default Siddharth Asthana
                       ` (3 preceding siblings ...)
  2025-10-13 19:39     ` [PATCH v3 0/3] replay: make atomic ref updates the default Junio C Hamano
@ 2025-10-22 18:50     ` Siddharth Asthana
  2025-10-22 18:50       ` [PATCH v4 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
                         ` (5 more replies)
  4 siblings, 6 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-22 18:50 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

This is v4 of the git-replay atomic updates series.

Based on feedback from v3, this version improves the naming and
implementation for clarity and type safety. Thanks to Junio, Christian,
Elijah, Phillip, Patrick, and Karthik for the detailed reviews.

## Changes in v4

**Renamed --update-refs to --ref-action**

Junio pointed out that "--update-refs=print" is semantically awkward.
Answering "print" to the question "update refs?" doesn't make sense.
The actual question is "what action should we take on the refs?"

Changed to --ref-action=(update|print) where both values are verbs that
answer "what action?". This makes the interface clearer.

**Aligned config name with command-line option**

Changed replay.defaultAction to replay.refAction. The config variable
now mirrors the option name, making the relationship obvious.

**Unified config and command-line values**

v3 had confusing value mapping:
  - Command-line: yes/print
  - Config: update-refs/show-commands

v4 uses the same values everywhere:
  - Command-line: update/print
  - Config: update/print

This eliminates the need for mental mapping.

**Converted to type-safe enum implementation**

Per Junio's suggestion, added enum ref_action_mode and convert the mode
string immediately after parse_options():

  enum ref_action_mode {
      REF_ACTION_UPDATE,
      REF_ACTION_PRINT
  };

This provides compiler protection against typos and allows clear switch
statements with BUG() defaults instead of if/else chains.

**Fixed t0450 synopsis test**

The usage string now matches the documentation SYNOPSIS exactly. This
test was failing in v3.

**Improved --advance documentation**

Removed confusing cherry-pick reference and used Junio's clearer
wording to explain what --advance does versus --onto.

## Technical Implementation

Same as v3, using Git's ref transaction API:
  - ref_store_transaction_begin() for atomic transactions
  - ref_transaction_update() to stage updates
  - ref_transaction_commit() for atomic application

New in v4: The handle_ref_update() helper takes an enum parameter and
uses a switch statement instead of string comparisons.

## Testing

All tests pass:
  - t0450-txt-doc-vs-help.sh (now passing, was failing in v3)
  - t3650-replay-basics.sh (all 18 tests pass)

Test changes: All existing tests updated to use --ref-action=print
instead of --update-refs=print. New config tests verify
replay.refAction behavior.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
Changes in v4:
- Rename --update-refs to --ref-action for semantic clarity.
- Rename replay.defaultAction to replay.refAction to mirror option name.
- Unify command-line and config values to both use update/print.
- Convert implementation to use enum instead of string comparisons.
- Use switch statement with BUG() default in handle_ref_update().
- Fix t0450 synopsis mismatch.
- Improve --advance documentation clarity.
- Update all tests to use --ref-action.

Changes in v3:
- Removed --allow-partial option (no concrete use cases).
- Changed to --update-refs=<mode> for extensibility.
- Added replay.defaultAction configuration option.
- Improved commit messages with Helped-by trailers.
- Enhanced test suite with proper isolation.
- Extracted handle_ref_update() helper function.

---
 Documentation/config/replay.adoc |  11 +++
 Documentation/git-replay.adoc    |  65 +++++++++++------
 builtin/replay.c                 | 118 +++++++++++++++++++++++++++----
 t/t3650-replay-basics.sh         |  58 ++++++++++++---
 4 files changed, 209 insertions(+), 43 deletions(-)
 create mode 100644 Documentation/config/replay.adoc

Siddharth Asthana (3):
  replay: use die_for_incompatible_opt2() for option validation
  replay: make atomic ref updates the default behavior
  replay: add replay.refAction config option

Range-diff against v3:
-:  ---------- > 1:  baa0cfdd4a replay: use die_for_incompatible_opt2() for option validation
-:  ---------- > 2:  3b5df166f3 replay: make atomic ref updates the default behavior
    @@ Metadata
     Author: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Commit message ##
         replay: make atomic ref updates the default behavior
         
         [Commit message unchanged - explains problem and solution]
         
         For users needing the traditional pipeline workflow, add a new
    -    `--update-refs=<mode>` option that preserves the original behavior:
    +    --ref-action=<mode> option that preserves the original behavior:
         
    -      git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin
    +      git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin
         
         The mode can be:
    -      * `yes` (default): Update refs directly using an atomic transaction
    +      * update (default): Update refs directly using an atomic transaction
           * `print`: Output update-ref commands for pipeline use
         
         Implementation details:
         
         The atomic ref updates are implemented using Git's ref transaction API.
    -    In cmd_replay(), when not in 'print' mode, we initialize a transaction
    +    In cmd_replay(), when not in print mode, we initialize a transaction
         using ref_store_transaction_begin() with the default atomic behavior.
         As commits are replayed, ref updates are staged into the transaction
         using ref_transaction_update(). Finally, ref_transaction_commit()
         applies all updates atomically—either all updates succeed or none do.
         
    -    To avoid code duplication between the 'print' and 'yes' modes, this
    +    To avoid code duplication between the print and update modes, this
         commit extracts a handle_ref_update() helper function. This function
    -    takes the mode and either prints the update command or stages it into
    -    the transaction. This keeps both code paths consistent and makes future
    -    maintenance easier.
    +    takes the mode (as an enum) and either prints the update command or
    +    stages it into the transaction. Using an enum rather than passing the
    +    string around provides type safety and allows the compiler to catch
    +    typos. The switch statement makes it easy to add future modes.
         
         The helper function signature:
         
    -      static int handle_ref_update(const char *mode,
    +      static int handle_ref_update(enum ref_action_mode mode,
                                         struct ref_transaction *transaction,
                                         const char *refname,
                                         const struct object_id *new_oid,
                                         const struct object_id *old_oid,
                                         struct strbuf *err)
         
    -    When mode is 'print', it prints the update-ref command. When mode is
    -    'yes', it calls ref_transaction_update() to stage the update. This
    -    eliminates the duplication that would otherwise exist at each ref update
    -    call site.
    +    The enum is defined as:
    +    
    +      enum ref_action_mode {
    +          REF_ACTION_UPDATE,
    +          REF_ACTION_PRINT
    +      };
    +    
    +    The mode string is converted to enum immediately after parse_options()
    +    to avoid string comparisons throughout the codebase and provide compiler
    +    protection against typos.
         
         Test suite changes:
         
         All existing tests that expected command output now use
    -    `--update-refs=print` to preserve their original behavior. This keeps
    +    --ref-action=print to preserve their original behavior. This keeps
         the tests valid while allowing them to verify that the pipeline workflow
         still works correctly.
         
         [Rest of test section unchanged]
         
    -    A following commit will add a `replay.defaultAction` configuration
    +    A following commit will add a replay.refAction configuration
         option for users who prefer the traditional pipeline output as their
         default behavior.
         
         Helped-by: Elijah Newren <newren@gmail.com>
         Helped-by: Patrick Steinhardt <ps@pks.im>
         Helped-by: Christian Couder <christian.couder@gmail.com>
         Helped-by: Phillip Wood <phillip.wood123@gmail.com>
         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Documentation/git-replay.adoc ##
     @@ Documentation/git-replay.adoc: SYNOPSIS
      [verse]
     -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
    -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>)
    -+		[--update-refs[=<mode>]] <revision-range>...
    ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...
      
      ## Documentation/git-replay.adoc: DESCRIPTION
     -the working tree and the index untouched, and updates no references.
     -The output of this command is meant to be used as input to
     -`git update-ref --stdin`, which would update the relevant branches
     +the working tree and the index untouched. By default, updates the
     +relevant references using an atomic transaction (all refs update or
    -+none). Use `--update-refs=print` to avoid automatic ref updates and
    ++none). Use `--ref-action=print` to avoid automatic ref updates and
     +instead get update commands that can be piped to `git update-ref --stdin`
      (see the OUTPUT section below).
      
     @@ Documentation/git-replay.adoc: OPTIONS
     -When `--onto` is specified, the update-ref command(s) in the output will
     -update the branch(es) in the revision range to point at the new
     +When `--onto` is specified, the branch(es) in the revision range will be
     +updated to point at the new commits (or update commands will be printed
    -+if `--update-refs=print` is used), similar to the way how
    ++if `--ref-action=print` is used), similar to the way how
     +`git rebase --update-refs` updates multiple branches in the affected range.
      
      --advance <branch>::
     -When `--advance` is specified, the update-ref command(s) in the output
     -will update the branch passed as an argument to `--advance` to point at
     -the new commits (in other words, this mimics a cherry-pick operation).
    -+When `--advance` is specified, the branch passed as an argument will be
    -+updated to point at the new commits (or an update command will be printed
    -+if `--update-refs=print` is used). This mimics a cherry-pick operation.
    ++The history is replayed on top of the <branch> and <branch> is updated to
    ++point at the tip of the resulting history (or an update command will be
    ++printed if `--ref-action=print` is used). This is different from `--onto`,
    ++which uses the target only as a starting point without updating it.
      
    -+--update-refs[=<mode>]::
    ++--ref-action[=<mode>]::
     +	Control how references are updated. The mode can be:
     +--
    -+* `yes` (default): Update refs directly using an atomic transaction.
    -+  All ref updates succeed or all fail.
    ++	* `update` (default): Update refs directly using an atomic transaction.
    ++	  All refs are updated or none are (all-or-nothing behavior).
     +* `print`: Output update-ref commands for pipeline use. This is the
    -+  The output can be piped as-is to `git update-ref --stdin`.
    ++	  traditional behavior where output can be piped to `git update-ref --stdin`.
     +--
    ++
    ++The default mode can be configured via `replay.refAction` configuration option.
      
     @@ Documentation/git-replay.adoc: OUTPUT
     -When there are no conflicts, the output of this command is usable as
     -input to `git update-ref --stdin`.  It is of the form:
    -+By default, when there are no conflicts, this command updates the relevant
    -+references using an atomic transaction and produces no output. All ref
    -+updates succeed or all fail.
    ++By default (with `--ref-action=update`), this command produces no output on
    ++success, as refs are updated directly using an atomic transaction.
     +
    -+When `--update-refs=print` is used, the output is usable as input to
    ++When using `--ref-action=print`, the output is usable as input to
     +`git update-ref --stdin`. It is of the form:
      
     @@ Documentation/git-replay.adoc: EXAMPLES
     -To simply rebase `mybranch` onto `target`:
    -+To simply rebase `mybranch` onto `target` (default behavior):
    ++To simply rebase `mybranch` onto `target`:
      
      ------------
      $ git replay --onto target origin/main..mybranch
     -update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
      ------------
      
     @@ Documentation/git-replay.adoc: EXAMPLES
     +To get the traditional pipeline output:
     +
     +------------
    -+$ git replay --update-refs=print --onto target origin/main..mybranch
    ++$ git replay --ref-action=print --onto target origin/main..mybranch
     +update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
     +------------
      
      ## builtin/replay.c ##
    ++@@ builtin/replay.c
    ++ #include <oidset.h>
    ++ #include <tree.h>
    ++ 
    ++enum ref_action_mode {
    ++	REF_ACTION_UPDATE,
    ++	REF_ACTION_PRINT
    ++};
    ++
    ++ static const char *short_commit_name(struct repository *repo,
    ++ 
     @@ builtin/replay.c: static struct commit *pick_regular_commit
      	return create_commit(repo, result->tree, pickme, replayed_base);
      }
      
    -+static int handle_ref_update(const char *mode,
    ++static int handle_ref_update(enum ref_action_mode mode,
     +			     struct ref_transaction *transaction,
     +			     const char *refname,
     +			     const struct object_id *new_oid,
     +			     const struct object_id *old_oid,
     +			     struct strbuf *err)
     +{
    -+	if (!strcmp(mode, "print")) {
    ++	switch (mode) {
    ++	case REF_ACTION_PRINT:
     +		printf("update %s %s %s\n",
     +		       refname,
     +		       oid_to_hex(new_oid),
     +		       oid_to_hex(old_oid));
     +		return 0;
    -+	}
    -+
    -+	/* mode == "yes" - update refs directly */
    -+	return ref_transaction_update(transaction, refname, new_oid, old_oid,
    -+				      NULL, NULL, 0, "git replay", err);
    ++	case REF_ACTION_UPDATE:
    ++		return ref_transaction_update(transaction, refname, new_oid, old_oid,
    ++					      NULL, NULL, 0, "git replay", err);
    ++	default:
    ++		BUG("unknown ref_action_mode %d", mode);
    ++	}
     +}
      
     @@ builtin/replay.c: int cmd_replay
      	struct commit *onto = NULL;
      	const char *onto_name = NULL;
      	int contained = 0;
    -+	const char *update_refs_mode = NULL;
    ++	const char *ref_action_str = NULL;
    ++	enum ref_action_mode ref_action = REF_ACTION_UPDATE;
      
     @@ builtin/replay.c: int cmd_replay
     - 	const char * const replay_usage[] = {
    + 	const char *const replay_usage[] = {
      		N_("(EXPERIMENTAL!) git replay "
      		   "([--contained] --onto <newbase> | --advance <branch>) "
    -+		   "[--update-refs[=<mode>]] <revision-range>..."),
    ++		   "[--ref-action[=<mode>]] <revision-range>..."),
      		NULL
      	};
      
     @@ builtin/replay.c: int cmd_replay
    -+		OPT_STRING(0, "update-refs", &update_refs_mode,
    ++		OPT_STRING(0, "ref-action", &ref_action_str,
     +			   N_("mode"),
    -+			   N_("control ref update behavior (yes|print)")),
    ++			   N_("control ref update behavior (update|print)")),
      		OPT_END()
      	};
      
     @@ builtin/replay.c: int cmd_replay
      	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
      				  contained, "--contained");
      
    -+	/* Set default mode if not specified */
    -+	if (!update_refs_mode)
    -+		update_refs_mode = "yes";
    -+
    -+	/* Validate update-refs mode */
    -+	if (strcmp(update_refs_mode, "yes") && strcmp(update_refs_mode, "print"))
    -+		die(_("invalid value for --update-refs: '%s' (expected 'yes' or 'print')"),
    -+		    update_refs_mode);
    ++	/* Parse ref action mode */
    ++	if (!strcmp(ref_action_str, "update"))
    ++		ref_action = REF_ACTION_UPDATE;
    ++	else if (!strcmp(ref_action_str, "print"))
    ++		ref_action = REF_ACTION_PRINT;
    ++	else
    ++		die(_("unknown --ref-action mode '%s'"), ref_action_str);
      
     @@ builtin/replay.c: int cmd_replay
      	/* Initialize ref transaction if we're updating refs directly */
    -+	if (!strcmp(update_refs_mode, "yes")) {
    ++	if (ref_action == REF_ACTION_UPDATE) {
      		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
      
     @@ builtin/replay.c: int cmd_replay
    -+				if (handle_ref_update(update_refs_mode, transaction,
    ++				if (handle_ref_update(ref_action, transaction,
     +						      decoration->name,
     +						      &last_commit->object.oid,
     +						      &commit->object.oid,
      
     @@ t/t3650-replay-basics.sh
      test_expect_success 'using replay to rebase two branches, one on top of other' '
     -	git replay --onto main topic1..topic2 >result &&
    -+	git replay --update-refs=print --onto main topic1..topic2 >result &&
    ++	git replay --ref-action=print --onto main topic1..topic2 >result &&
      
     @@ t/t3650-replay-basics.sh
      test_expect_success 'using replay to perform basic cherry-pick' '
     -	git replay --advance main topic1..topic2 >result &&
    -+	git replay --update-refs=print --advance main topic1..topic2 >result &&
    ++	git replay --ref-action=print --advance main topic1..topic2 >result &&
      
     @@ t/t3650-replay-basics.sh
      test_expect_success 'using replay to also rebase a contained branch' '
     -	git replay --contained --onto main main..topic3 >result &&
    -+	git replay --update-refs=print --contained --onto main main..topic3 >result &&
    ++	git replay --ref-action=print --contained --onto main main..topic3 >result &&
      
     @@ t/t3650-replay-basics.sh
     +# Tests for atomic ref update behavior
     +
     +[New tests added - content unchanged from v3]
     +
    -+test_expect_success 'replay validates --update-refs mode values' '
    -+	test_must_fail git replay --update-refs=invalid --onto main topic1..topic2 2>error &&
    -+	grep "invalid value for --update-refs" error
    ++test_expect_success 'replay validates --ref-action mode values' '
    ++	test_must_fail git replay --ref-action=invalid --onto main topic1..topic2 2>error &&
    ++	grep "invalid value for --ref-action" error
     +'
      
-:  ---------- > 3:  c35049881d replay: add replay.refAction config option
    @@ Metadata
     Author: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Commit message ##
    -    replay: add replay.defaultAction config option
    +    replay: add replay.refAction config option
         
         Add a configuration option to control the default behavior of git replay
         for updating references. This allows users who prefer the traditional
    -    pipeline output to set it once in their config instead of passing
    -    --update-refs=print with every command.
    +    pipeline output to set it once in their config instead of passing
    +    --ref-action=print with every command.
         
    -    The config option uses enum string values for extensibility:
    -      * replay.defaultAction = update-refs (default): atomic ref updates
    -      * replay.defaultAction = show-commands: output commands for pipeline
    +    The config option uses string values that mirror the behavior modes:
    +      * replay.refAction = update (default): atomic ref updates
    +      * replay.refAction = print: output commands for pipeline
         
    -    The command-line --update-refs option always overrides the config setting,
    +    The command-line --ref-action option always overrides the config setting,
         allowing users to temporarily change behavior for a single invocation.
         
         [Implementation details mostly unchanged except names]
         
    -    The command-line --update-refs option, when provided, overrides the
    +    The command-line --ref-action option, when provided, overrides the
         config value. This precedence allows users to set their preferred default
         while still having per-invocation control:
         
    -      git config replay.defaultAction show-commands  # Set default
    -      git replay --update-refs=yes --onto main topic  # Override once
    +      git config replay.refAction print         # Set default
    +      git replay --ref-action=update --onto main topic  # Override once
         
    -    The config option uses different value names ('update-refs' vs
    -    'show-commands') compared to the command-line option ('yes' vs 'print')
    -    for semantic clarity. The config values describe what action is being
    -    taken, while the command-line values are terse for typing convenience.
    -    
    -    The enum string design (rather than a boolean like 'replay.updateRefs')
    -    allows future expansion to additional modes without requiring new
    -    configuration variables.
    +    The config and command-line option use the same value names ('update'
    +    and 'print') for consistency and clarity. This makes it immediately
    +    obvious how the config maps to the command-line option, addressing
    +    feedback about the relationship between configuration and command-line
    +    options being clear to users.
         
         Helped-by: Junio C Hamano <gitster@pobox.com>
         Helped-by: Elijah Newren <newren@gmail.com>
    +    Helped-by: Christian Couder <christian.couder@gmail.com>
         Helped-by: Phillip Wood <phillip.wood123@gmail.com>
         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
      
      ## Documentation/config/replay.adoc (new) ##
    -+replay.defaultAction::
    -+	Control the default behavior of `git replay` for updating references.
    ++replay.refAction::
    ++	Specifies the default mode for handling reference updates in
    ++	`git replay`. The value can be:
     +--
    -+* `update-refs` (default): Update refs directly using an atomic transaction.
    -+* `show-commands`: Output update-ref commands that can be piped to
    -+  `git update-ref --stdin`.
    ++	* `update`: Update refs directly using an atomic transaction (default behavior).
    ++	* `print`: Output update-ref commands for pipeline use.
     +--
    -+This can be overridden with the `--update-refs` command-line option.
    -+Note that the command-line option uses slightly different values
    -+(`yes` and `print`) for brevity, but they map to the same behavior
    -+as the config values.
    ++This setting can be overridden with the `--ref-action` command-line option.
    ++When not configured, `git replay` defaults to `update` mode.
      
      ## builtin/replay.c ##
     +#include "builtin.h"
     +#include "config.h"
      
     @@ builtin/replay.c: int cmd_replay
      	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
      				  contained, "--contained");
      
    -+	/* Set default mode from config if not specified on command line */
    -+	if (!update_refs_mode) {
    ++	/* Set default from config if not specified on command line */
    ++	if (!ref_action_str) {
     +		const char *config_value = NULL;
    -+		if (!repo_config_get_string_tmp(repo, "replay.defaultaction", &config_value)) {
    -+			if (!strcmp(config_value, "update-refs"))
    -+				update_refs_mode = "yes";
    -+			else if (!strcmp(config_value, "show-commands"))
    -+				update_refs_mode = "print";
    ++		if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value)) {
    ++			if (!strcmp(config_value, "update"))
    ++				ref_action_str = "update";
    ++			else if (!strcmp(config_value, "print"))
    ++				ref_action_str = "print";
     +			else
    -+				die(_("invalid value for replay.defaultAction: '%s' "
    -+				      "(expected 'update-refs' or 'show-commands')"),
    ++				die(_("invalid value for replay.refAction: '%s'"),
     +				    config_value);
     +		}
    -+	} else {
    -+		update_refs_mode = "yes";
     +	}
    ++
    ++	/* Default to update mode if still not set */
    ++	if (!ref_action_str)
    ++		ref_action_str = "update";
      
     @@ t/t3650-replay-basics.sh
    -+test_expect_success 'replay.defaultAction config option' '
    ++test_expect_success 'replay.refAction config option' '
     +	START=$(git rev-parse topic2) &&
    -+	test_when_finished "git branch -f topic2 $START && git config --unset replay.defaultAction" &&
    ++	test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
     +
    -+	git config replay.defaultAction show-commands &&
    ++	git config replay.refAction print &&
     +	git replay --onto main topic1..topic2 >output &&
     +	test_line_count = 1 output &&
     +	grep "^update refs/heads/topic2 " output &&
     +
     +	git branch -f topic2 $START &&
    -+	git config replay.defaultAction update-refs &&
    ++	git config replay.refAction update &&
     +	git replay --onto main topic1..topic2 >output &&
     +	test_must_be_empty output &&
     +	[...]
     +'
     +
    -+test_expect_success 'command-line --update-refs overrides config' '
    ++test_expect_success 'command-line --ref-action overrides config' '
     +	START=$(git rev-parse topic2) &&
    -+	test_when_finished "git branch -f topic2 $START && git config --unset replay.defaultAction" &&
    -+	git config replay.defaultAction update-refs &&
    -+	git replay --update-refs=print --onto main topic1..topic2 >output &&
    ++	test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
    ++	git config replay.refAction update &&
    ++	git replay --ref-action=print --onto main topic1..topic2 >output &&
     +	test_line_count = 1 output &&
     +	grep "^update refs/heads/topic2 " output
     +'
     +
    -+test_expect_success 'invalid replay.defaultAction value' '
    -+	test_when_finished "git config --unset replay.defaultAction" &&
    -+	git config replay.defaultAction invalid &&
    ++test_expect_success 'invalid replay.refAction value' '
    ++	test_when_finished "git config --unset replay.refAction" &&
    ++	git config replay.refAction invalid &&
     +	test_must_fail git replay --onto main topic1..topic2 2>error &&
    -+	grep "invalid value for replay.defaultAction" error
    ++	grep "invalid value for replay.refAction" error
     +'
-- 
2.51.0

base-commit: 133d151831d291e51d55c80a40684eb6c8dfdd24

Thanks
- Siddharth


^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v4 1/3] replay: use die_for_incompatible_opt2() for option validation
  2025-10-22 18:50     ` [PATCH v4 " Siddharth Asthana
@ 2025-10-22 18:50       ` Siddharth Asthana
  2025-10-22 18:50       ` [PATCH v4 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
                         ` (4 subsequent siblings)
  5 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-22 18:50 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

In preparation for adding the --ref-action option, convert option
validation to use die_for_incompatible_opt2(). This helper provides
standardized error messages for mutually exclusive options.

The following commit introduces --ref-action which will be incompatible
with certain other options. Using die_for_incompatible_opt2() now means
that commit can cleanly add its validation using the same pattern,
keeping the validation logic consistent and maintainable.

This also aligns git-replay's option handling with how other Git commands
manage option conflicts, using the established die_for_incompatible_opt*()
helper family.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 builtin/replay.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 6172c8aacc..b64fc72063 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -330,9 +330,9 @@ int cmd_replay(int argc,
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (advance_name_opt && contained)
-		die(_("options '%s' and '%s' cannot be used together"),
-		    "--advance", "--contained");
+	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
+				  contained, "--contained");
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH v4 2/3] replay: make atomic ref updates the default behavior
  2025-10-22 18:50     ` [PATCH v4 " Siddharth Asthana
  2025-10-22 18:50       ` [PATCH v4 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
@ 2025-10-22 18:50       ` Siddharth Asthana
  2025-10-22 21:19         ` Junio C Hamano
  2025-10-24 10:37         ` Christian Couder
  2025-10-22 18:50       ` [PATCH v4 3/3] replay: add replay.refAction config option Siddharth Asthana
                         ` (3 subsequent siblings)
  5 siblings, 2 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-22 18:50 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

The git replay command currently outputs update commands that can be
piped to update-ref to achieve a rebase, e.g.

  git replay --onto main topic1..topic2 | git update-ref --stdin

This separation had advantages for three special cases:
  * it made testing easy (when state isn't modified from one step to
    the next, you don't need to make temporary branches or have undo
    commands, or try to track the changes)
  * it provided a natural can-it-rebase-cleanly (and what would it
    rebase to) capability without automatically updating refs, similar
    to a --dry-run
  * it provided a natural low-level tool for the suite of hash-object,
    mktree, commit-tree, mktag, merge-tree, and update-ref, allowing
    users to have another building block for experimentation and making
    new tools

However, it should be noted that all three of these are somewhat
special cases; users, whether on the client or server side, would
almost certainly find it more ergonomical to simply have the updating
of refs be the default.

For server-side operations in particular, the pipeline architecture
creates process coordination overhead. Server implementations that need
to perform rebases atomically must maintain additional code to:

  1. Spawn and manage a pipeline between git-replay and git-update-ref
  2. Coordinate stdout/stderr streams across the pipe boundary
  3. Handle partial failure states if the pipeline breaks mid-execution
  4. Parse and validate the update-ref command output

Change the default behavior to update refs directly, and atomically (at
least to the extent supported by the refs backend in use). This
eliminates the process coordination overhead for the common case.

For users needing the traditional pipeline workflow, add a new
--ref-action=<mode> option that preserves the original behavior:

  git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin

The mode can be:
  * update (default): Update refs directly using an atomic transaction
  * print: Output update-ref commands for pipeline use

Implementation details:

The atomic ref updates are implemented using Git's ref transaction API.
In cmd_replay(), when not in `print` mode, we initialize a transaction
using ref_store_transaction_begin() with the default atomic behavior.
As commits are replayed, ref updates are staged into the transaction
using ref_transaction_update(). Finally, ref_transaction_commit()
applies all updates atomically—either all updates succeed or none do.

To avoid code duplication between the 'print' and 'update' modes, this
commit extracts a handle_ref_update() helper function. This function
takes the mode (as an enum) and either prints the update command or
stages it into the transaction. Using an enum rather than passing the
string around provides type safety and allows the compiler to catch
typos. The switch statement makes it easy to add future modes.

The helper function signature:

  static int handle_ref_update(enum ref_action_mode mode,
                                struct ref_transaction *transaction,
                                const char *refname,
                                const struct object_id *new_oid,
                                const struct object_id *old_oid,
                                struct strbuf *err)

The enum is defined as:

  enum ref_action_mode {
      REF_ACTION_UPDATE,
      REF_ACTION_PRINT
  };

The mode string is converted to enum immediately after parse_options()
to avoid string comparisons throughout the codebase and provide compiler
protection against typos.

Test suite changes:

All existing tests that expected command output now use
--ref-action=print to preserve their original behavior. This keeps
the tests valid while allowing them to verify that the pipeline workflow
still works correctly.

New tests were added to verify:
  - Default atomic behavior (no output, refs updated directly)
  - Bare repository support (server-side use case)
  - Equivalence between traditional pipeline and atomic updates
  - Real atomicity using a lock file to verify all-or-nothing guarantee
  - Test isolation using test_when_finished to clean up state

The bare repository tests were fixed to rebuild their expectations
independently rather than comparing to previous test output, improving
test reliability and isolation.

A following commit will add a replay.refAction configuration
option for users who prefer the traditional pipeline output as their
default behavior.

Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc | 65 +++++++++++++++--------
 builtin/replay.c              | 98 +++++++++++++++++++++++++++++++----
 t/t3650-replay-basics.sh      | 16 +++---
 3 files changed, 139 insertions(+), 40 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 0b12bf8aa4..759028dc28 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...
 
 DESCRIPTION
 -----------
 
 Takes ranges of commits and replays them onto a new location. Leaves
-the working tree and the index untouched, and updates no references.
-The output of this command is meant to be used as input to
-`git update-ref --stdin`, which would update the relevant branches
+the working tree and the index untouched. By default, updates the
+relevant references using an atomic transaction (all refs update or
+none). Use `--ref-action=print` to avoid automatic ref updates and
+instead get update commands that can be piped to `git update-ref --stdin`
 (see the OUTPUT section below).
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
@@ -29,18 +30,31 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-When `--onto` is specified, the update-ref command(s) in the output will
-update the branch(es) in the revision range to point at the new
-commits, similar to the way how `git rebase --update-refs` updates
-multiple branches in the affected range.
+When `--onto` is specified, the branch(es) in the revision range will be
+updated to point at the new commits (or update commands will be printed
+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`
+updates multiple branches in the affected range.
 
 --advance <branch>::
 	Starting point at which to create the new commits; must be a
 	branch name.
 +
-When `--advance` is specified, the update-ref command(s) in the output
-will update the branch passed as an argument to `--advance` to point at
-the new commits (in other words, this mimics a cherry-pick operation).
+The history is replayed on top of the <branch> and <branch> is updated to
+point at the tip of the resulting history (or an update command will be
+printed if `--ref-action=print` is used). This is different from `--onto`,
+which uses the target only as a starting point without updating it.
+
+--ref-action[=<mode>]::
+	Control how references are updated. The mode can be:
++
+--
+	* `update` (default): Update refs directly using an atomic transaction.
+	  All refs are updated or none are (all-or-nothing behavior).
+	* `print`: Output update-ref commands for pipeline use. This is the
+	  traditional behavior where output can be piped to `git update-ref --stdin`.
+--
++
+The default mode can be configured via `replay.refAction` configuration option.
 
 <revision-range>::
 	Range of commits to replay. More than one <revision-range> can
@@ -54,8 +68,11 @@ include::rev-list-options.adoc[]
 OUTPUT
 ------
 
-When there are no conflicts, the output of this command is usable as
-input to `git update-ref --stdin`.  It is of the form:
+By default (with `--ref-action=update`), this command produces no output on
+success, as refs are updated directly using an atomic transaction.
+
+When using `--ref-action=print`, the output is usable as input to
+`git update-ref --stdin`. It is of the form:
 
 	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
@@ -81,6 +98,14 @@ To simply rebase `mybranch` onto `target`:
 
 ------------
 $ git replay --onto target origin/main..mybranch
+------------
+
+The refs are updated atomically and no output is produced on success.
+
+To see what would be updated without actually updating:
+
+------------
+$ git replay --ref-action=print --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
@@ -88,33 +113,29 @@ To cherry-pick the commits from mybranch onto target:
 
 ------------
 $ git replay --advance target origin/main..mybranch
-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
 ------------
 
 Note that the first two examples replay the exact same commits and on
 top of the exact same new base, they only differ in that the first
-provides instructions to make mybranch point at the new commits and
-the second provides instructions to make target point at them.
+updates mybranch to point at the new commits and the second updates
+target to point at them.
 
 What if you have a stack of branches, one depending upon another, and
 you'd really like to rebase the whole set?
 
 ------------
 $ git replay --contained --onto origin/main origin/main..tipbranch
-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
 ------------
 
+All three branches (`branch1`, `branch2`, and `tipbranch`) are updated
+atomically.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
 
 ------------
 $ git replay --onto origin/main ^base branch1 branch2 branch3
-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 ------------
 
 This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
diff --git a/builtin/replay.c b/builtin/replay.c
index b64fc72063..1246add636 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -20,6 +20,11 @@
 #include <oidset.h>
 #include <tree.h>
 
+enum ref_action_mode {
+	REF_ACTION_UPDATE,
+	REF_ACTION_PRINT
+};
+
 static const char *short_commit_name(struct repository *repo,
 				     struct commit *commit)
 {
@@ -284,6 +289,28 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	return create_commit(repo, result->tree, pickme, replayed_base);
 }
 
+static int handle_ref_update(enum ref_action_mode mode,
+			     struct ref_transaction *transaction,
+			     const char *refname,
+			     const struct object_id *new_oid,
+			     const struct object_id *old_oid,
+			     struct strbuf *err)
+{
+	switch (mode) {
+	case REF_ACTION_PRINT:
+		printf("update %s %s %s\n",
+		       refname,
+		       oid_to_hex(new_oid),
+		       oid_to_hex(old_oid));
+		return 0;
+	case REF_ACTION_UPDATE:
+		return ref_transaction_update(transaction, refname, new_oid, old_oid,
+					      NULL, NULL, 0, "git replay", err);
+	default:
+		BUG("unknown ref_action_mode %d", mode);
+	}
+}
+
 int cmd_replay(int argc,
 	       const char **argv,
 	       const char *prefix,
@@ -294,6 +321,8 @@ int cmd_replay(int argc,
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
 	int contained = 0;
+	const char *ref_action_str = NULL;
+	enum ref_action_mode ref_action = REF_ACTION_UPDATE;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -302,12 +331,14 @@ int cmd_replay(int argc,
 	struct merge_result result;
 	struct strset *update_refs = NULL;
 	kh_oid_map_t *replayed_commits;
+	struct ref_transaction *transaction = NULL;
+	struct strbuf transaction_err = STRBUF_INIT;
 	int ret = 0;
 
-	const char * const replay_usage[] = {
+	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto <newbase> | --advance <branch>) "
-		   "<revision-range>..."),
+		   "[--ref-action[=<mode>]] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -319,6 +350,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &contained,
 			 N_("advance all branches contained in revision-range")),
+		OPT_STRING(0, "ref-action", &ref_action_str,
+			   N_("mode"),
+			   N_("control ref update behavior (update|print)")),
 		OPT_END()
 	};
 
@@ -333,6 +367,18 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
+	/* Default to update mode if not specified */
+	if (!ref_action_str)
+		ref_action_str = "update";
+
+	/* Parse ref action mode */
+	if (!strcmp(ref_action_str, "update"))
+		ref_action = REF_ACTION_UPDATE;
+	else if (!strcmp(ref_action_str, "print"))
+		ref_action = REF_ACTION_PRINT;
+	else
+		die(_("unknown --ref-action mode '%s'"), ref_action_str);
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
@@ -389,6 +435,17 @@ int cmd_replay(int argc,
 	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
 			      &onto, &update_refs);
 
+	/* Initialize ref transaction if using update mode */
+	if (ref_action == REF_ACTION_UPDATE) {
+		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
+							  0, &transaction_err);
+		if (!transaction) {
+			ret = error(_("failed to begin ref transaction: %s"),
+				    transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
 	if (!onto) /* FIXME: Should handle replaying down to root commit */
 		die("Replaying down to root commit is not supported yet!");
 
@@ -434,10 +491,15 @@ int cmd_replay(int argc,
 			if (decoration->type == DECORATION_REF_LOCAL &&
 			    (contained || strset_contains(update_refs,
 							  decoration->name))) {
-				printf("update %s %s %s\n",
-				       decoration->name,
-				       oid_to_hex(&last_commit->object.oid),
-				       oid_to_hex(&commit->object.oid));
+				if (handle_ref_update(ref_action, transaction,
+						      decoration->name,
+						      &last_commit->object.oid,
+						      &commit->object.oid,
+						      &transaction_err) < 0) {
+					ret = error(_("failed to update ref %s: %s"),
+						    decoration->name, transaction_err.buf);
+					goto cleanup;
+				}
 			}
 			decoration = decoration->next;
 		}
@@ -445,10 +507,23 @@ int cmd_replay(int argc,
 
 	/* In --advance mode, advance the target ref */
 	if (result.clean == 1 && advance_name) {
-		printf("update %s %s %s\n",
-		       advance_name,
-		       oid_to_hex(&last_commit->object.oid),
-		       oid_to_hex(&onto->object.oid));
+		if (handle_ref_update(ref_action, transaction, advance_name,
+				      &last_commit->object.oid,
+				      &onto->object.oid,
+				      &transaction_err) < 0) {
+			ret = error(_("failed to update ref %s: %s"),
+				    advance_name, transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
+	/* Commit the ref transaction if we have one */
+	if (transaction && result.clean == 1) {
+		if (ref_transaction_commit(transaction, &transaction_err)) {
+			ret = error(_("failed to commit ref transaction: %s"),
+				    transaction_err.buf);
+			goto cleanup;
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
@@ -460,6 +535,9 @@ int cmd_replay(int argc,
 	ret = result.clean;
 
 cleanup:
+	if (transaction)
+		ref_transaction_free(transaction);
+	strbuf_release(&transaction_err);
 	release_revisions(&revs);
 	free(advance_name);
 
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 58b3759935..54c86b87d8 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git replay --onto main topic1..topic2 >result &&
+	git replay --ref-action=print --onto main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 '
 
 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
-	git -C bare replay --onto main topic1..topic2 >result-bare &&
+	git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
@@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
 	# 4th field of result is hash for main instead of hash for topic2
 
-	git replay --advance main topic1..topic2 >result &&
+	git replay --ref-action=print --advance main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 '
 
 test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
-	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
@@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 '
 
 test_expect_success 'using replay to also rebase a contained branch' '
-	git replay --contained --onto main main..topic3 >result &&
+	git replay --ref-action=print --contained --onto main main..topic3 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '
 '
 
 test_expect_success 'using replay on bare repo to also rebase a contained branch' '
-	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&
 	test_cmp expect result-bare
 '
 
 test_expect_success 'using replay to rebase multiple divergent branches' '
-	git replay --onto main ^topic1 topic2 topic4 >result &&
+	git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
 '
 
 test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
-	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+	git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
 
 	test_line_count = 4 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH v4 3/3] replay: add replay.refAction config option
  2025-10-22 18:50     ` [PATCH v4 " Siddharth Asthana
  2025-10-22 18:50       ` [PATCH v4 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
  2025-10-22 18:50       ` [PATCH v4 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
@ 2025-10-22 18:50       ` Siddharth Asthana
  2025-10-24 11:01         ` Christian Couder
  2025-10-24 13:28         ` Phillip Wood
  2025-10-23 18:47       ` [PATCH v4 0/3] replay: make atomic ref updates the default Junio C Hamano
                         ` (2 subsequent siblings)
  5 siblings, 2 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-22 18:50 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

Add a configuration option to control the default behavior of git replay
for updating references. This allows users who prefer the traditional
pipeline output to set it once in their config instead of passing
--ref-action=print with every command.

The config option uses string values that mirror the behavior modes:
  * replay.refAction = update (default): atomic ref updates
  * replay.refAction = print: output commands for pipeline

The command-line --ref-action option always overrides the config setting,
allowing users to temporarily change behavior for a single invocation.

Implementation details:

In cmd_replay(), after parsing command-line options, we check if
--ref-action was provided. If not, we read the configuration using
repo_config_get_string_tmp(). If the config variable is set, we validate
the value and use it to set the ref_action_str:

  Config value      Internal mode    Behavior
  ──────────────────────────────────────────────────────────────
  "update"          "update"         Atomic ref updates (default)
  "print"           "print"          Pipeline output
  (not set)         "update"         Atomic ref updates (default)
  (invalid)         error            Die with helpful message

If an invalid value is provided, we die() immediately with an error
message explaining the valid options. This catches configuration errors
early and provides clear guidance to users.

The command-line --ref-action option, when provided, overrides the
config value. This precedence allows users to set their preferred default
while still having per-invocation control:

  git config replay.refAction print         # Set default
  git replay --ref-action=update --onto main topic  # Override once

The config and command-line option use the same value names ('update'
and 'print') for consistency and clarity. This makes it immediately
obvious how the config maps to the command-line option, addressing
feedback about the relationship between configuration and command-line
options being clear to users.

Examples:

$ git config --global replay.refAction print
$ git replay --onto main topic1..topic2 | git update-ref --stdin

$ git replay --ref-action=update --onto main topic1..topic2

$ git config replay.refAction update
$ git replay --onto main topic1..topic2  # Updates refs directly

The implementation follows Git's standard configuration precedence:
command-line options override config values, which matches user
expectations across all Git commands.

Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/config/replay.adoc | 11 +++++++++
 builtin/replay.c                 | 16 +++++++++++-
 t/t3650-replay-basics.sh         | 42 ++++++++++++++++++++++++++++++++
 3 files changed, 68 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/config/replay.adoc

diff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc
new file mode 100644
index 0000000000..7d549d2f0e
--- /dev/null
+++ b/Documentation/config/replay.adoc
@@ -0,0 +1,11 @@
+replay.refAction::
+	Specifies the default mode for handling reference updates in
+	`git replay`. The value can be:
++
+--
+	* `update`: Update refs directly using an atomic transaction (default behavior).
+	* `print`: Output update-ref commands for pipeline use.
+--
++
+This setting can be overridden with the `--ref-action` command-line option.
+When not configured, `git replay` defaults to `update` mode.
diff --git a/builtin/replay.c b/builtin/replay.c
index 1246add636..bb0420dc99 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -8,6 +8,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
+#include "config.h"
 #include "environment.h"
 #include "hex.h"
 #include "lockfile.h"
@@ -367,7 +368,20 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
-	/* Default to update mode if not specified */
+	/* Set default mode from config if not specified on command line */
+	if (!ref_action_str) {
+		const char *config_value = NULL;
+		if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value)) {
+			if (!strcmp(config_value, "update"))
+				ref_action_str = "update";
+			else if (!strcmp(config_value, "print"))
+				ref_action_str = "print";
+			else
+				die(_("invalid value for replay.refAction: '%s'"), config_value);
+		}
+	}
+
+	/* Default to update mode if still not set */
 	if (!ref_action_str)
 		ref_action_str = "update";
 
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 54c86b87d8..307beb667e 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -217,4 +217,46 @@ test_expect_success 'merge.directoryRenames=false' '
 		--onto rename-onto rename-onto..rename-from
 '
 
+test_expect_success 'replay.refAction config option' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
+
+	# Set config to print
+	git config replay.refAction print &&
+	git replay --onto main topic1..topic2 >output &&
+	test_line_count = 1 output &&
+	grep "^update refs/heads/topic2 " output &&
+
+	# Reset and test update mode
+	git branch -f topic2 $START &&
+	git config replay.refAction update &&
+	git replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'command-line --ref-action overrides config' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
+
+	# Set config to update but use --ref-action=print
+	git config replay.refAction update &&
+	git replay --ref-action=print --onto main topic1..topic2 >output &&
+	test_line_count = 1 output &&
+	grep "^update refs/heads/topic2 " output
+'
+
+test_expect_success 'invalid replay.refAction value' '
+	test_when_finished "git config --unset replay.refAction" &&
+	git config replay.refAction invalid &&
+	test_must_fail git replay --onto main topic1..topic2 2>error &&
+	grep "invalid value for replay.refAction" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior
  2025-10-22 18:50       ` [PATCH v4 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
@ 2025-10-22 21:19         ` Junio C Hamano
  2025-10-28 19:03           ` Siddharth Asthana
  2025-10-24 10:37         ` Christian Couder
  1 sibling, 1 reply; 129+ messages in thread
From: Junio C Hamano @ 2025-10-22 21:19 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> diff --git a/builtin/replay.c b/builtin/replay.c
> index b64fc72063..1246add636 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -20,6 +20,11 @@
>  #include <oidset.h>
>  #include <tree.h>
>  
> +enum ref_action_mode {
> +	REF_ACTION_UPDATE,
> +	REF_ACTION_PRINT
> +};
> +

We allow and encourage the last item in enum definition to have
trailing comma, i.e.

        enum ref_action_mode {
                REF_ACTION_UPDATE,
                REF_ACTION_PRINT,
        };

unless the last one is somehow special and we are not supposed to
add any new item after that (e.g., a sentinel REF_ACTION_MAX that is
supposed to give the upper limit of the values).  That way, future
developers can add new items with minimum patch noise.

> @@ -434,10 +491,15 @@ int cmd_replay(int argc,
> ...
> +					ret = error(_("failed to update ref %s: %s"),
> +						    decoration->name, transaction_err.buf);

Hmph, don't we want to use '%s' when reporting the ->name thing?
Documentation/CodingGuidelines has this:

   Error Messages

    - Do not end a single-sentence error message with a full stop.

    - Do not capitalize the first word, only because it is the first word
      in the message ("unable to open '%s'", not "Unable to open '%s'").  But
      "SHA-3 not supported" is fine, because the reason the first word is
      capitalized is not because it is at the beginning of the sentence,
      but because the word would be spelled in capital letters even when
      it appeared in the middle of the sentence.

    - Say what the error is first ("cannot open '%s'", not "%s: cannot open").

    - Enclose the subject of an error inside a pair of single quotes,
      e.g. `die(_("unable to open '%s'"), path)`.

    - Unless there is a compelling reason not to, error messages from
      porcelain commands should be marked for translation, e.g.
      `die(_("bad revision %s"), revision)`.

    - Error messages from the plumbing commands are sometimes meant for
      machine consumption and should not be marked for translation,
      e.g., `die("bad revision %s", revision)`.

    - BUG("message") are for communicating the specific error to developers,
      thus should not be translated.


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 0/3] replay: make atomic ref updates the default
  2025-10-22 18:50     ` [PATCH v4 " Siddharth Asthana
                         ` (2 preceding siblings ...)
  2025-10-22 18:50       ` [PATCH v4 3/3] replay: add replay.refAction config option Siddharth Asthana
@ 2025-10-23 18:47       ` Junio C Hamano
  2025-10-25 16:57         ` Junio C Hamano
  2025-10-28 20:19         ` Siddharth Asthana
  2025-10-24  9:39       ` Christian Couder
  2025-10-28 21:46       ` [PATCH v5 " Siddharth Asthana
  5 siblings, 2 replies; 129+ messages in thread
From: Junio C Hamano @ 2025-10-23 18:47 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Siddharth Asthana <siddharthasthana31@gmail.com> writes:

> This is v4 of the git-replay atomic updates series.
>
> Based on feedback from v3, this version improves the naming and
> implementation for clarity and type safety. Thanks to Junio, Christian,
> Elijah, Phillip, Patrick, and Karthik for the detailed reviews.
>
> ## Changes in v4
>
> **Renamed --update-refs to --ref-action**
>
> Junio pointed out that "--update-refs=print" is semantically awkward.
> Answering "print" to the question "update refs?" doesn't make sense.
> The actual question is "what action should we take on the refs?"
>
> Changed to --ref-action=(update|print) where both values are verbs that
> answer "what action?". This makes the interface clearer.
>
> **Aligned config name with command-line option**
>
> Changed replay.defaultAction to replay.refAction. The config variable
> now mirrors the option name, making the relationship obvious.
>
> **Unified config and command-line values**

I didn't see anything glaringly wrong in this round, even though I
picked a couple of small nits in one patch, so we might want a
hopefully small and final reroll before marking the topic for
'next'.

Is everybody else happy with this iteration otherwise?

Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 0/3] replay: make atomic ref updates the default
  2025-10-22 18:50     ` [PATCH v4 " Siddharth Asthana
                         ` (3 preceding siblings ...)
  2025-10-23 18:47       ` [PATCH v4 0/3] replay: make atomic ref updates the default Junio C Hamano
@ 2025-10-24  9:39       ` Christian Couder
  2025-10-28 21:46       ` [PATCH v5 " Siddharth Asthana
  5 siblings, 0 replies; 129+ messages in thread
From: Christian Couder @ 2025-10-24  9:39 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, phillip.wood123, phillip.wood, newren, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Wed, Oct 22, 2025 at 8:50 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:

> ## Testing
>
> All tests pass:
>   - t0450-txt-doc-vs-help.sh (now passing, was failing in v3)
>   - t3650-replay-basics.sh (all 18 tests pass)

Nit: a link to CI results on GitLab or GitHub might be better to show
that all tests pass. Just saying "All tests pass" makes reviewers
wonder if comprehensive CI tests on different OS/compilers/build
systems were used or if you just tested on your machine.

Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior
  2025-10-22 18:50       ` [PATCH v4 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
  2025-10-22 21:19         ` Junio C Hamano
@ 2025-10-24 10:37         ` Christian Couder
  2025-10-24 15:23           ` Junio C Hamano
  2025-10-28 19:39           ` Siddharth Asthana
  1 sibling, 2 replies; 129+ messages in thread
From: Christian Couder @ 2025-10-24 10:37 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, phillip.wood123, phillip.wood, newren, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:

[...]

> However, it should be noted that all three of these are somewhat
> special cases; users, whether on the client or server side, would
> almost certainly find it more ergonomical to simply have the updating

Nit: maybe: s/ergonomical/ergonomic/

> of refs be the default.

[...]

> Change the default behavior to update refs directly, and atomically (at
> least to the extent supported by the refs backend in use). This
> eliminates the process coordination overhead for the common case.
>
> For users needing the traditional pipeline workflow, add a new
> --ref-action=<mode> option that preserves the original behavior:
>
>   git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin
>
> The mode can be:
>   * update (default): Update refs directly using an atomic transaction
>   * print: Output update-ref commands for pipeline use

Nit: maybe it should be mentioned that the command is still
experimental, so it's OK to change the default like this.

> +--ref-action[=<mode>]::
> +       Control how references are updated. The mode can be:
> ++
> +--
> +       * `update` (default): Update refs directly using an atomic transaction.
> +         All refs are updated or none are (all-or-nothing behavior).
> +       * `print`: Output update-ref commands for pipeline use. This is the
> +         traditional behavior where output can be piped to `git update-ref --stdin`.
> +--
> ++
> +The default mode can be configured via `replay.refAction` configuration option.

Nit: s/via `replay.refAction` configuration option/via the
`replay.refAction` configuration variable/

(It seems that "configuration variable" is used around 6 times more
than "configuration option", so we may want to standardize this
wording.)

> @@ -54,8 +68,11 @@ include::rev-list-options.adoc[]
>  OUTPUT
>  ------
>
> -When there are no conflicts, the output of this command is usable as
> -input to `git update-ref --stdin`.  It is of the form:
> +By default (with `--ref-action=update`), this command produces no output on

Nit: s/By default (with `--ref-action=update`)/By default, or with
`--ref-action=update`,/

I think it's better to be very explicit here, especially as we mention
`--ref-action=print` below.

[...]

> -       const char * const replay_usage[] = {
> +       const char *const replay_usage[] = {

Nit: Not sure this change is worth it, but I understand that it might
help pass some automated/CI tests, so not a big issue.

[...]

> +       /* Default to update mode if not specified */
> +       if (!ref_action_str)
> +               ref_action_str = "update";
> +
> +       /* Parse ref action mode */
> +       if (!strcmp(ref_action_str, "update"))
> +               ref_action = REF_ACTION_UPDATE;

Nit: maybe:

       if (!ref_action_str || !strcmp(ref_action_str, "update"))
               ref_action = REF_ACTION_UPDATE;

> +       else if (!strcmp(ref_action_str, "print"))
> +               ref_action = REF_ACTION_PRINT;
> +       else
> +               die(_("unknown --ref-action mode '%s'"), ref_action_str);
> +

[...]

>  test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
> -       git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
> +       git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
>
>         test_line_count = 4 result &&
>         cut -f 3 -d " " result >new-branch-tips &&

Are there tests with the new default behavior added? It looks like all
the changes in the test script are about adding "--ref-action=print"
to an existing test.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 3/3] replay: add replay.refAction config option
  2025-10-22 18:50       ` [PATCH v4 3/3] replay: add replay.refAction config option Siddharth Asthana
@ 2025-10-24 11:01         ` Christian Couder
  2025-10-24 15:30           ` Junio C Hamano
  2025-10-28 19:26           ` Siddharth Asthana
  2025-10-24 13:28         ` Phillip Wood
  1 sibling, 2 replies; 129+ messages in thread
From: Christian Couder @ 2025-10-24 11:01 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, phillip.wood123, phillip.wood, newren, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:

> @@ -367,7 +368,20 @@ int cmd_replay(int argc,
>         die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>                                   contained, "--contained");
>
> -       /* Default to update mode if not specified */
> +       /* Set default mode from config if not specified on command line */
> +       if (!ref_action_str) {
> +               const char *config_value = NULL;
> +               if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value)) {
> +                       if (!strcmp(config_value, "update"))
> +                               ref_action_str = "update";
> +                       else if (!strcmp(config_value, "print"))
> +                               ref_action_str = "print";
> +                       else
> +                               die(_("invalid value for replay.refAction: '%s'"), config_value);
> +               }
> +       }
> +
> +       /* Default to update mode if still not set */
>         if (!ref_action_str)
>                 ref_action_str = "update";

It seems to me that a dedicated function could handle this a bit
better. Maybe something like:

static enum ref_action_mode get_ref_action_mode(const char *ref_action_str)
{
     const char *config_value = NULL;

     if (!strcmp(ref_action_str, "update"))
             return REF_ACTION_UPDATE;
      if (!strcmp(ref_action_str, "print"))
            return REF_ACTION_PRINT;
      if (ref_action_str)
            die(_("unknown --ref-action mode '%s'"), ref_action_str);

      if (repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
             return REF_ACTION_UPDATE; /* default */

      if (!strcmp(config_value, "update"))
             return REF_ACTION_UPDATE;
      if (!strcmp(config_value, "print"))
            return REF_ACTION_PRINT;
      die(_("invalid value for replay.refAction: '%s'"), config_value);
}

[...]

> +test_expect_success 'replay.refAction config option' '
> +       # Store original state
> +       START=$(git rev-parse topic2) &&
> +       test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
> +
> +       # Set config to print
> +       git config replay.refAction print &&
> +       git replay --onto main topic1..topic2 >output &&
> +       test_line_count = 1 output &&
> +       grep "^update refs/heads/topic2 " output &&

Nit: here and below, it's a bit better to use test_grep instead of
grep for better error reporting.

Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 3/3] replay: add replay.refAction config option
  2025-10-22 18:50       ` [PATCH v4 3/3] replay: add replay.refAction config option Siddharth Asthana
  2025-10-24 11:01         ` Christian Couder
@ 2025-10-24 13:28         ` Phillip Wood
  2025-10-24 13:36           ` Phillip Wood
  2025-10-28 19:46           ` Siddharth Asthana
  1 sibling, 2 replies; 129+ messages in thread
From: Phillip Wood @ 2025-10-24 13:28 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, phillip.wood, newren, gitster, ps, karthik.188,
	code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin

On 22/10/2025 19:50, Siddharth Asthana wrote:

This is looking pretty nice now, I've left some on he tests comments below

> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index 54c86b87d8..307beb667e 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -217,4 +217,46 @@ test_expect_success 'merge.directoryRenames=false' '
>   		--onto rename-onto rename-onto..rename-from
>   '
>   
> +test_expect_success 'replay.refAction config option' '
> +	# Store original state
> +	START=$(git rev-parse topic2) &&

Isn't there a tag we can use here from the initial setup?

> +	test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
> +
> +	# Set config to print
> +	git config replay.refAction print &&
I think it would be better to use test_config here rather than having to 
clear the config manually with test_when_finished() above.

> +	git replay --onto main topic1..topic2 >output &&
> +	test_line_count = 1 output &&
> +	grep "^update refs/heads/topic2 " output &&

Rather than test_line_count and grep it would be better to use test_cmp 
here.

The same comments apply to the rest of the tests

Thanks

Phillip

> +
> +	# Reset and test update mode
> +	git branch -f topic2 $START &&
> +	git config replay.refAction update &&
> +	git replay --onto main topic1..topic2 >output &&
> +	test_must_be_empty output &&
> +
> +	# Verify ref was updated
> +	git log --format=%s topic2 >actual &&
> +	test_write_lines E D M L B A >expect &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'command-line --ref-action overrides config' '
> +	# Store original state
> +	START=$(git rev-parse topic2) &&
> +	test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
> +
> +	# Set config to update but use --ref-action=print
> +	git config replay.refAction update &&
> +	git replay --ref-action=print --onto main topic1..topic2 >output &&
> +	test_line_count = 1 output &&
> +	grep "^update refs/heads/topic2 " output
> +'
> +
> +test_expect_success 'invalid replay.refAction value' '
> +	test_when_finished "git config --unset replay.refAction" &&
> +	git config replay.refAction invalid &&
> +	test_must_fail git replay --onto main topic1..topic2 2>error &&
> +	grep "invalid value for replay.refAction" error
> +'
> +
>   test_done



^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 3/3] replay: add replay.refAction config option
  2025-10-24 13:28         ` Phillip Wood
@ 2025-10-24 13:36           ` Phillip Wood
  2025-10-28 19:47             ` Siddharth Asthana
  2025-10-28 19:46           ` Siddharth Asthana
  1 sibling, 1 reply; 129+ messages in thread
From: Phillip Wood @ 2025-10-24 13:36 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, phillip.wood, newren, gitster, ps, karthik.188,
	code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin

On 24/10/2025 14:28, Phillip Wood wrote:
> On 22/10/2025 19:50, Siddharth Asthana wrote:
> 
>> +    git replay --onto main topic1..topic2 >output &&
>> +    test_line_count = 1 output &&
>> +    grep "^update refs/heads/topic2 " output &&
> 
> Rather than test_line_count and grep it would be better to use test_cmp 
> here.

Oh, I've just realized we don't know the value of the ref so 
test_line_count() plus test_grep() (not grep) makes sense.

Thanks

Phillip


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior
  2025-10-24 10:37         ` Christian Couder
@ 2025-10-24 15:23           ` Junio C Hamano
  2025-10-28 20:18             ` Siddharth Asthana
  2025-10-28 19:39           ` Siddharth Asthana
  1 sibling, 1 reply; 129+ messages in thread
From: Junio C Hamano @ 2025-10-24 15:23 UTC (permalink / raw)
  To: Christian Couder
  Cc: Siddharth Asthana, git, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Christian Couder <christian.couder@gmail.com> writes:

> On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>
>> -       const char * const replay_usage[] = {
>> +       const char *const replay_usage[] = {
>
> Nit: Not sure this change is worth it, but I understand that it might
> help pass some automated/CI tests, so not a big issue.

I think this formatting issue came up recently on another discussion
thread.  We found that the prevalent style in the codebase is that
an asterisk in between tokens neither of which is variable has space
on both sides (i.e. the preimage of the above change), so unless
there is a specific reason to make the above change, I'd rather not
to see such "reformatting" thrown into a patch that implements a
feature or fixes a bug (iow, not a "clean-up styles" patch).

By the way, I would be suprised if that the reason were a CI test.
How would the preimage have been passing the same test if that is
the case?

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 3/3] replay: add replay.refAction config option
  2025-10-24 11:01         ` Christian Couder
@ 2025-10-24 15:30           ` Junio C Hamano
  2025-10-28 20:08             ` Siddharth Asthana
  2025-10-28 19:26           ` Siddharth Asthana
  1 sibling, 1 reply; 129+ messages in thread
From: Junio C Hamano @ 2025-10-24 15:30 UTC (permalink / raw)
  To: Christian Couder
  Cc: Siddharth Asthana, git, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Christian Couder <christian.couder@gmail.com> writes:

> It seems to me that a dedicated function could handle this a bit
> better. Maybe something like:
>
> static enum ref_action_mode get_ref_action_mode(const char *ref_action_str)
> {
>      const char *config_value = NULL;
>
>      if (!strcmp(ref_action_str, "update"))
>              return REF_ACTION_UPDATE;
>       if (!strcmp(ref_action_str, "print"))
>             return REF_ACTION_PRINT;
>       if (ref_action_str)
>             die(_("unknown --ref-action mode '%s'"), ref_action_str);
>
>       if (repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
>              return REF_ACTION_UPDATE; /* default */
>
>       if (!strcmp(config_value, "update"))
>              return REF_ACTION_UPDATE;
>       if (!strcmp(config_value, "print"))
>             return REF_ACTION_PRINT;
>       die(_("invalid value for replay.refAction: '%s'"), config_value);
> }

You'd want to do "string to enum" helper function just once and call
that helper from the above function, once for the command line option
and again for the configuration variable.

Or do so where you would add a call to the above function directly
without your helper.  I am not convinced that "here is the command
line option (or perhaps we got nothing); what is the desired
setting, taking configuration also into consideration?" is
particularly a good abstraction.  It is more common to have
git_config() to grab replay.refAction string, and if there is a
string value, pass the last one to "string to enum" helper and
remember the result, then call parse_options() to further overwrite
the result from the command line option string (which again will use
the "string to enum" helper).  The structure that requires your helper
function smells rather unusual.

> [...]
>
>> +test_expect_success 'replay.refAction config option' '
>> +       # Store original state
>> +       START=$(git rev-parse topic2) &&
>> +       test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
>> +
>> +       # Set config to print
>> +       git config replay.refAction print &&
>> +       git replay --onto main topic1..topic2 >output &&
>> +       test_line_count = 1 output &&
>> +       grep "^update refs/heads/topic2 " output &&
>
> Nit: here and below, it's a bit better to use test_grep instead of
> grep for better error reporting.

Yes, "a bit" -> "much".

Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 0/3] replay: make atomic ref updates the default
  2025-10-23 18:47       ` [PATCH v4 0/3] replay: make atomic ref updates the default Junio C Hamano
@ 2025-10-25 16:57         ` Junio C Hamano
  2025-10-28 20:19         ` Siddharth Asthana
  1 sibling, 0 replies; 129+ messages in thread
From: Junio C Hamano @ 2025-10-25 16:57 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

Junio C Hamano <gitster@pobox.com> writes:

> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>> This is v4 of the git-replay atomic updates series.
>> ...
> I didn't see anything glaringly wrong in this round, even though I
> picked a couple of small nits in one patch, so we might want a
> hopefully small and final reroll before marking the topic for
> 'next'.
>
> Is everybody else happy with this iteration otherwise?

There are a few actionable comments pointing out typos and style
glitches for this iteration.  I'll mark the topic as expecting a
hopefully small and final reroll in the next issue of "What's
cooking" report.

Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior
  2025-10-22 21:19         ` Junio C Hamano
@ 2025-10-28 19:03           ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 19:03 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 23/10/25 02:49, Junio C Hamano wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index b64fc72063..1246add636 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -20,6 +20,11 @@
>>   #include <oidset.h>
>>   #include <tree.h>
>>   
>> +enum ref_action_mode {
>> +	REF_ACTION_UPDATE,
>> +	REF_ACTION_PRINT
>> +};
>> +


Hi Junio,
Thank you for the detailed review! Both are straightforward fixes:


> We allow and encourage the last item in enum definition to have
> trailing comma, i.e.
>
>          enum ref_action_mode {
>                  REF_ACTION_UPDATE,
>                  REF_ACTION_PRINT,
>          };


Will add the trailing comma in v5 - makes future additions much cleaner.


>
> unless the last one is somehow special and we are not supposed to
> add any new item after that (e.g., a sentinel REF_ACTION_MAX that is
> supposed to give the upper limit of the values).  That way, future
> developers can add new items with minimum patch noise.
>
>> @@ -434,10 +491,15 @@ int cmd_replay(int argc,
>> ...
>> +					ret = error(_("failed to update ref %s: %s"),
>> +						    decoration->name, transaction_err.buf);
> Hmph, don't we want to use '%s' when reporting the ->name thing?



You are absolutely right about the codingGuidelines. I will fix both error
messages to properly quote the ref names:


         error(_("failed to update ref '%s': %s"), decoration->name, 
transaction_err.buf);


These will be in v5 along with Christian and Philip's feedback.

Thanks,
Siddharth


> Documentation/CodingGuidelines has this:
>
>     Error Messages
>
>      - Do not end a single-sentence error message with a full stop.
>
>      - Do not capitalize the first word, only because it is the first word
>        in the message ("unable to open '%s'", not "Unable to open '%s'").  But
>        "SHA-3 not supported" is fine, because the reason the first word is
>        capitalized is not because it is at the beginning of the sentence,
>        but because the word would be spelled in capital letters even when
>        it appeared in the middle of the sentence.
>
>      - Say what the error is first ("cannot open '%s'", not "%s: cannot open").
>
>      - Enclose the subject of an error inside a pair of single quotes,
>        e.g. `die(_("unable to open '%s'"), path)`.
>
>      - Unless there is a compelling reason not to, error messages from
>        porcelain commands should be marked for translation, e.g.
>        `die(_("bad revision %s"), revision)`.
>
>      - Error messages from the plumbing commands are sometimes meant for
>        machine consumption and should not be marked for translation,
>        e.g., `die("bad revision %s", revision)`.
>
>      - BUG("message") are for communicating the specific error to developers,
>        thus should not be translated.
>

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 3/3] replay: add replay.refAction config option
  2025-10-24 11:01         ` Christian Couder
  2025-10-24 15:30           ` Junio C Hamano
@ 2025-10-28 19:26           ` Siddharth Asthana
  1 sibling, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 19:26 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, phillip.wood123, phillip.wood, newren, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 24/10/25 16:31, Christian Couder wrote:
> On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>
>> @@ -367,7 +368,20 @@ int cmd_replay(int argc,
>>          die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>>                                    contained, "--contained");
>>
>> -       /* Default to update mode if not specified */
>> +       /* Set default mode from config if not specified on command line */
>> +       if (!ref_action_str) {
>> +               const char *config_value = NULL;
>> +               if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value)) {
>> +                       if (!strcmp(config_value, "update"))
>> +                               ref_action_str = "update";
>> +                       else if (!strcmp(config_value, "print"))
>> +                               ref_action_str = "print";
>> +                       else
>> +                               die(_("invalid value for replay.refAction: '%s'"), config_value);
>> +               }
>> +       }
>> +
>> +       /* Default to update mode if still not set */
>>          if (!ref_action_str)
>>                  ref_action_str = "update";


Hi Christian,
Thanks for the config parsing improvements!


> It seems to me that a dedicated function could handle this a bit
> better. Maybe something like:


Excellent suggestion! I will extract `parse_ref_action_mode()` and 
`get_ref_action_mode()`
helpers to centralize the string-to-enum conversion and config 
precedence logic, Much
cleaner than the current inline approach.


>
> static enum ref_action_mode get_ref_action_mode(const char *ref_action_str)
> {
>       const char *config_value = NULL;
>
>       if (!strcmp(ref_action_str, "update"))
>               return REF_ACTION_UPDATE;
>        if (!strcmp(ref_action_str, "print"))
>              return REF_ACTION_PRINT;
>        if (ref_action_str)
>              die(_("unknown --ref-action mode '%s'"), ref_action_str);
>
>        if (repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
>               return REF_ACTION_UPDATE; /* default */
>
>        if (!strcmp(config_value, "update"))
>               return REF_ACTION_UPDATE;
>        if (!strcmp(config_value, "print"))
>              return REF_ACTION_PRINT;
>        die(_("invalid value for replay.refAction: '%s'"), config_value);
> }
>
> [...]
>
>> +test_expect_success 'replay.refAction config option' '
>> +       # Store original state
>> +       START=$(git rev-parse topic2) &&
>> +       test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
>> +
>> +       # Set config to print
>> +       git config replay.refAction print &&
>> +       git replay --onto main topic1..topic2 >output &&
>> +       test_line_count = 1 output &&
>> +       grep "^update refs/heads/topic2 " output &&
> Nit: here and below, it's a bit better to use test_grep instead of
> grep for better error reporting.


Will switch to `test_grep` throughout for better error reporting.

Thanks,
Siddharth


>
> Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior
  2025-10-24 10:37         ` Christian Couder
  2025-10-24 15:23           ` Junio C Hamano
@ 2025-10-28 19:39           ` Siddharth Asthana
  1 sibling, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 19:39 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, phillip.wood123, phillip.wood, newren, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 24/10/25 16:07, Christian Couder wrote:
> On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>
> [...]
>
>> However, it should be noted that all three of these are somewhat
>> special cases; users, whether on the client or server side, would
>> almost certainly find it more ergonomical to simply have the updating


Hi Christian,
Thanks for the detailed review! All good points:


> Nit: maybe: s/ergonomical/ergonomic/


Will be fixed in v5!


>> of refs be the default.
> [...]
>
>> Change the default behavior to update refs directly, and atomically (at
>> least to the extent supported by the refs backend in use). This
>> eliminates the process coordination overhead for the common case.
>>
>> For users needing the traditional pipeline workflow, add a new
>> --ref-action=<mode> option that preserves the original behavior:
>>
>>    git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin
>>
>> The mode can be:
>>    * update (default): Update refs directly using an atomic transaction
>>    * print: Output update-ref commands for pipeline use
> Nit: maybe it should be mentioned that the command is still
> experimental, so it's OK to change the default like this.


Good point, I will add a note in the commit message that since git-replay is
still experimental, changing the default behavior is acceptable


>
>> +--ref-action[=<mode>]::
>> +       Control how references are updated. The mode can be:
>> ++
>> +--
>> +       * `update` (default): Update refs directly using an atomic transaction.
>> +         All refs are updated or none are (all-or-nothing behavior).
>> +       * `print`: Output update-ref commands for pipeline use. This is the
>> +         traditional behavior where output can be piped to `git update-ref --stdin`.
>> +--
>> ++
>> +The default mode can be configured via `replay.refAction` configuration option.
> Nit: s/via `replay.refAction` configuration option/via the
> `replay.refAction` configuration variable/


Good catch, I will standardize on "configuration variable" throughout.


>
> (It seems that "configuration variable" is used around 6 times more
> than "configuration option", so we may want to standardize this
> wording.)
>
>> @@ -54,8 +68,11 @@ include::rev-list-options.adoc[]
>>   OUTPUT
>>   ------
>>
>> -When there are no conflicts, the output of this command is usable as
>> -input to `git update-ref --stdin`.  It is of the form:
>> +By default (with `--ref-action=update`), this command produces no output on
> Nit: s/By default (with `--ref-action=update`)/By default, or with
> `--ref-action=update`,/


Much clearer wording


>
> I think it's better to be very explicit here, especially as we mention
> `--ref-action=print` below.
>
> [...]
>
>> -       const char * const replay_usage[] = {
>> +       const char *const replay_usage[] = {
> Nit: Not sure this change is worth it, but I understand that it might
> help pass some automated/CI tests, so not a big issue.


Actually, Junio mentioned in another thread that the prevalent style in the
codebase is `const char * const` (space on both sides), so I'll revert this
change in v5.


>
> [...]
>
>> +       /* Default to update mode if not specified */
>> +       if (!ref_action_str)
>> +               ref_action_str = "update";
>> +
>> +       /* Parse ref action mode */
>> +       if (!strcmp(ref_action_str, "update"))
>> +               ref_action = REF_ACTION_UPDATE;
> Nit: maybe:
>
>         if (!ref_action_str || !strcmp(ref_action_str, "update"))
>                 ref_action = REF_ACTION_UPDATE;


That's cleaner - I will combine the logic in v5.


>
>> +       else if (!strcmp(ref_action_str, "print"))
>> +               ref_action = REF_ACTION_PRINT;
>> +       else
>> +               die(_("unknown --ref-action mode '%s'"), ref_action_str);
>> +
> [...]
>
>>   test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
>> -       git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
>> +       git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
>>
>>          test_line_count = 4 result &&
>>          cut -f 3 -d " " result >new-branch-tips &&
> Are there tests with the new default behavior added? It looks like all
> the changes in the test script are about adding "--ref-action=print"
> to an existing test.


Yes, they're in patch 2 - the atomic behavior tests that verify no 
output and direct
ref updates. I should highlight this better in the commit message since 
they test the
absence of output (the new default).


Thanks,
Siddharth


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 3/3] replay: add replay.refAction config option
  2025-10-24 13:28         ` Phillip Wood
  2025-10-24 13:36           ` Phillip Wood
@ 2025-10-28 19:46           ` Siddharth Asthana
  1 sibling, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 19:46 UTC (permalink / raw)
  To: Phillip Wood, git
  Cc: christian.couder, phillip.wood, newren, gitster, ps, karthik.188,
	code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin


On 24/10/25 18:58, Phillip Wood wrote:
> On 22/10/2025 19:50, Siddharth Asthana wrote:
>
> This is looking pretty nice now, I've left some on he tests comments 
> below


Thanks for the test improvements!


>
>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>> index 54c86b87d8..307beb667e 100755
>> --- a/t/t3650-replay-basics.sh
>> +++ b/t/t3650-replay-basics.sh
>> @@ -217,4 +217,46 @@ test_expect_success 
>> 'merge.directoryRenames=false' '
>>           --onto rename-onto rename-onto..rename-from
>>   '
>>   +test_expect_success 'replay.refAction config option' '
>> +    # Store original state
>> +    START=$(git rev-parse topic2) &&
>
> Isn't there a tag we can use here from the initial setup?


Good point - I'll use `topic1` instead of `$(git rev-parse topic2)` for
consistency with the existing test patterns.


>
>> +    test_when_finished "git branch -f topic2 $START && git config 
>> --unset replay.refAction" &&
>> +
>> +    # Set config to print
>> +    git config replay.refAction print &&
> I think it would be better to use test_config here rather than having 
> to clear the config manually with test_when_finished() above.


Absolutely, `test_config` is much cleaner and handles the cleanup 
automatically.
I will refactor all the config tests to use this pattern.


>
>> +    git replay --onto main topic1..topic2 >output &&
>> +    test_line_count = 1 output &&
>> +    grep "^update refs/heads/topic2 " output &&
>
> Rather than test_line_count and grep it would be better to use 
> test_cmp here.


Will switch to `test_cmp` where appropriate, and definitely change 
`grep` to
`test_grep` for better error reporting.

Thanks,
Siddharth


>
> The same comments apply to the rest of the tests
>
> Thanks
>
> Phillip
>
>> +
>> +    # Reset and test update mode
>> +    git branch -f topic2 $START &&
>> +    git config replay.refAction update &&
>> +    git replay --onto main topic1..topic2 >output &&
>> +    test_must_be_empty output &&
>> +
>> +    # Verify ref was updated
>> +    git log --format=%s topic2 >actual &&
>> +    test_write_lines E D M L B A >expect &&
>> +    test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'command-line --ref-action overrides config' '
>> +    # Store original state
>> +    START=$(git rev-parse topic2) &&
>> +    test_when_finished "git branch -f topic2 $START && git config 
>> --unset replay.refAction" &&
>> +
>> +    # Set config to update but use --ref-action=print
>> +    git config replay.refAction update &&
>> +    git replay --ref-action=print --onto main topic1..topic2 >output &&
>> +    test_line_count = 1 output &&
>> +    grep "^update refs/heads/topic2 " output
>> +'
>> +
>> +test_expect_success 'invalid replay.refAction value' '
>> +    test_when_finished "git config --unset replay.refAction" &&
>> +    git config replay.refAction invalid &&
>> +    test_must_fail git replay --onto main topic1..topic2 2>error &&
>> +    grep "invalid value for replay.refAction" error
>> +'
>> +
>>   test_done
>
>

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 3/3] replay: add replay.refAction config option
  2025-10-24 13:36           ` Phillip Wood
@ 2025-10-28 19:47             ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 19:47 UTC (permalink / raw)
  To: Phillip Wood, git
  Cc: christian.couder, phillip.wood, newren, gitster, ps, karthik.188,
	code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin


On 24/10/25 19:06, Phillip Wood wrote:
> On 24/10/2025 14:28, Phillip Wood wrote:
>> On 22/10/2025 19:50, Siddharth Asthana wrote:
>>
>>> +    git replay --onto main topic1..topic2 >output &&
>>> +    test_line_count = 1 output &&
>>> +    grep "^update refs/heads/topic2 " output &&
>>
>> Rather than test_line_count and grep it would be better to use 
>> test_cmp here.
>
> Oh, I've just realized we don't know the value of the ref so 
> test_line_count() plus test_grep() (not grep) makes sense.


Exactly, since we can't predict the exact hash values, `test_line_count` +
`test_grep` is the right approach. I will definitely switch from `grep` to
`test_grep` as you and Christian both suggested.

Thanks,
Siddharth


>
> Thanks
>
> Phillip
>

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 3/3] replay: add replay.refAction config option
  2025-10-24 15:30           ` Junio C Hamano
@ 2025-10-28 20:08             ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 20:08 UTC (permalink / raw)
  To: Junio C Hamano, Christian Couder
  Cc: git, phillip.wood123, phillip.wood, newren, ps, karthik.188, code,
	rybak.a.v, jltobler, toon, johncai86, johannes.schindelin


On 24/10/25 21:00, Junio C Hamano wrote:
> Christian Couder <christian.couder@gmail.com> writes:
>
>> It seems to me that a dedicated function could handle this a bit
>> better. Maybe something like:
>>
>> static enum ref_action_mode get_ref_action_mode(const char *ref_action_str)
>> {
>>       const char *config_value = NULL;
>>
>>       if (!strcmp(ref_action_str, "update"))
>>               return REF_ACTION_UPDATE;
>>        if (!strcmp(ref_action_str, "print"))
>>              return REF_ACTION_PRINT;
>>        if (ref_action_str)
>>              die(_("unknown --ref-action mode '%s'"), ref_action_str);
>>
>>        if (repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
>>               return REF_ACTION_UPDATE; /* default */
>>
>>        if (!strcmp(config_value, "update"))
>>               return REF_ACTION_UPDATE;
>>        if (!strcmp(config_value, "print"))
>>              return REF_ACTION_PRINT;
>>        die(_("invalid value for replay.refAction: '%s'"), config_value);
>> }
> You'd want to do "string to enum" helper function just once and call
> that helper from the above function, once for the command line option
> and again for the configuration variable.


That makes perfect sense - a single `parse_ref_action_mode()` helper 
that both
can use will eliminate the duplication, as Christian suggested.


>
> Or do so where you would add a call to the above function directly
> without your helper.  I am not convinced that "here is the command
> line option (or perhaps we got nothing); what is the desired
> setting, taking configuration also into consideration?" is
> particularly a good abstraction.  It is more common to have
> git_config() to grab replay.refAction string, and if there is a
> string value, pass the last one to "string to enum" helper and
> remember the result, then call parse_options() to further overwrite
> the result from the command line option string (which again will use
> the "string to enum" helper).  The structure that requires your helper
> function smells rather unusual.


Thanks for the guidance on the standard Git pattern. I had initially 
planned
to follow Christian's combined approach, but you are right that the 
traditional
Git pattern is more conventional. Looking at builtin/am.c and 
builtin/column.c,
I can see they follow:

1. `repo_config()` with callback before `parse_options()`
2. Command-line options naturally override config values
3. Clean separation between config reading and option parsing

I will implement it this way in v5 - using Christian's suggestion for 
the helper
functions but following the established Git config-then-parse-options 
pattern
for the overall structure.


>
>> [...]
>>
>>> +test_expect_success 'replay.refAction config option' '
>>> +       # Store original state
>>> +       START=$(git rev-parse topic2) &&
>>> +       test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
>>> +
>>> +       # Set config to print
>>> +       git config replay.refAction print &&
>>> +       git replay --onto main topic1..topic2 >output &&
>>> +       test_line_count = 1 output &&
>>> +       grep "^update refs/heads/topic2 " output &&
>> Nit: here and below, it's a bit better to use test_grep instead of
>> grep for better error reporting.
> Yes, "a bit" -> "much".


Will switch to `test_grep` throughout.

Thanks for the architectural guidance!

Thanks,
Siddharth


>
> Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior
  2025-10-24 15:23           ` Junio C Hamano
@ 2025-10-28 20:18             ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 20:18 UTC (permalink / raw)
  To: Junio C Hamano, Christian Couder
  Cc: git, phillip.wood123, phillip.wood, newren, ps, karthik.188, code,
	rybak.a.v, jltobler, toon, johncai86, johannes.schindelin


On 24/10/25 20:53, Junio C Hamano wrote:
> Christian Couder <christian.couder@gmail.com> writes:
>
>> On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana
>> <siddharthasthana31@gmail.com> wrote:
>>
>>> -       const char * const replay_usage[] = {
>>> +       const char *const replay_usage[] = {
>> Nit: Not sure this change is worth it, but I understand that it might
>> help pass some automated/CI tests, so not a big issue.
> I think this formatting issue came up recently on another discussion
> thread.  We found that the prevalent style in the codebase is that
> an asterisk in between tokens neither of which is variable has space
> on both sides (i.e. the preimage of the above change), so unless
> there is a specific reason to make the above change, I'd rather not
> to see such "reformatting" thrown into a patch that implements a
> feature or fixes a bug (iow, not a "clean-up styles" patch).


You are absolutely right, I will revert this formatting change in v5. The
`const char * const` spacing follows the established codebase style and
there's no reason to change it in a feature patch.


>
> By the way, I would be suprised if that the reason were a CI test.
> How would the preimage have been passing the same test if that is
> the case?


Good point, it wasn't a CI issue, just an unnecessary style change on my 
part.

Thanks,
Siddharth


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v4 0/3] replay: make atomic ref updates the default
  2025-10-23 18:47       ` [PATCH v4 0/3] replay: make atomic ref updates the default Junio C Hamano
  2025-10-25 16:57         ` Junio C Hamano
@ 2025-10-28 20:19         ` Siddharth Asthana
  1 sibling, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 20:19 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, phillip.wood123, phillip.wood, newren, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 24/10/25 00:17, Junio C Hamano wrote:
> Siddharth Asthana <siddharthasthana31@gmail.com> writes:
>
>> This is v4 of the git-replay atomic updates series.
>>
>> Based on feedback from v3, this version improves the naming and
>> implementation for clarity and type safety. Thanks to Junio, Christian,
>> Elijah, Phillip, Patrick, and Karthik for the detailed reviews.
>>
>> ## Changes in v4
>>
>> **Renamed --update-refs to --ref-action**
>>
>> Junio pointed out that "--update-refs=print" is semantically awkward.
>> Answering "print" to the question "update refs?" doesn't make sense.
>> The actual question is "what action should we take on the refs?"
>>
>> Changed to --ref-action=(update|print) where both values are verbs that
>> answer "what action?". This makes the interface clearer.
>>
>> **Aligned config name with command-line option**
>>
>> Changed replay.defaultAction to replay.refAction. The config variable
>> now mirrors the option name, making the relationship obvious.
>>
>> **Unified config and command-line values**
> I didn't see anything glaringly wrong in this round, even though I
> picked a couple of small nits in one patch, so we might want a
> hopefully small and final reroll before marking the topic for
> 'next'.


Thanks! I will address all the feedback from you, Christian, and Phillip 
in v5:

- Add trailing comma to enum definition
- Fix error message quoting with single quotes
- Revert the `const char * const` formatting change
- Follow standard Git config pattern (repo_config before parse_options)
- Extract proper helper functions for string-to-enum conversion
- Switch to `test_grep` and `test_config` in tests
- Fix documentation wording issues

Should have v5 ready soon with these fixes.

Thanks,
Siddharth


>
> Is everybody else happy with this iteration otherwise?
>
> Thanks.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v5 0/3] replay: make atomic ref updates the default
  2025-10-22 18:50     ` [PATCH v4 " Siddharth Asthana
                         ` (4 preceding siblings ...)
  2025-10-24  9:39       ` Christian Couder
@ 2025-10-28 21:46       ` Siddharth Asthana
  2025-10-28 21:46         ` [PATCH v5 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
                           ` (3 more replies)
  5 siblings, 4 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 21:46 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

This is v5 of the git-replay atomic updates series.

This version addresses all feedback from v4 reviews. Thanks to Junio,
Christian, and Phillip for the detailed technical reviews that helped
refine the implementation to Git standards.

## Changes in v5

**Added enum trailing comma**

Per Junio's suggestion, added trailing comma to enum definition for
future extensibility. This follows Git's established pattern and
minimizes patch noise when adding new enum values.

**Fixed error message formatting**

Following CodingGuidelines, wrapped ref names in single quotes in
error messages:
  - error(_("failed to update ref '%s': %s"), ...)

This provides better visual clarity and matches Git's error reporting
conventions throughout the codebase.

**Extracted helper functions for config parsing**

Per Christian and Junio's feedback, refactored config parsing into
clean helper functions:
  - parse_ref_action_mode(): String-to-enum conversion with source context
  - get_ref_action_mode(): Handles command-line vs config precedence

This eliminates code duplication and provides a single point for
validation logic, making the code more maintainable.

**Improved test suite with Git best practices**

Following Phillip and Christian's suggestions:
  - Switched from grep to test_grep for better error reporting
  - Used test_config for automatic config cleanup
  - Improved test isolation with proper state management
  - Used topic1 tag instead of $(git rev-parse) where appropriate

**Documentation improvements**

Fixed terminology and wording per Christian's feedback:
  - "ergonomical" → "ergonomic"
  - "configuration option" → "configuration variable"
  - "By default (with `--ref-action=update`)" → "By default, or with `--ref-action=update`,"

**Reverted unnecessary style change**

Per Junio's feedback, reverted the `const char * const` → `const char *const`
spacing change. The original spacing follows the prevalent codebase style.

## Technical Implementation

The atomic ref updates leverage Git's ref transaction API:
- ref_store_transaction_begin() with default atomic behavior
- ref_transaction_update() to stage each ref update
- ref_transaction_commit() for atomic application (all succeed or all fail)

The helper functions provide clean separation of concerns:
- parse_ref_action_mode() validates strings and converts to enum
- get_ref_action_mode() implements command-line > config > default precedence
- handle_ref_update() uses type-safe enum with switch statement

The on-demand config reading via repo_config_get_string_tmp() is simpler
than the traditional repo_config() callback pattern for this single-variable
case, while maintaining proper precedence behavior.

## Testing

All tests pass:
- t3650-replay-basics.sh (20 tests pass)
- New atomic behavior tests verify direct ref updates
- Config tests verify proper precedence and error handling
- Existing pipeline tests ensure backward compatibility

CI results: https://gitlab.com/gitlab-org/git/-/pipelines/2123403204

Siddharth Asthana (3):
  replay: use die_for_incompatible_opt2() for option validation
  replay: make atomic ref updates the default behavior
  replay: add replay.refAction config option

 Documentation/config/replay.adoc |  11 +++
 Documentation/git-replay.adoc    |  65 +++++++++++------
 builtin/replay.c                 | 121 +++++++++++++++++++++++++++----
 t/t3650-replay-basics.sh         |  91 +++++++++++++++++++++--
 4 files changed, 245 insertions(+), 43 deletions(-)
 create mode 100644 Documentation/config/replay.adoc

Range-diff against v4:
1:  baa0cfdd4a = 1:  3e27d07d3b replay: use die_for_incompatible_opt2() for option validation
2:  3b5df166f3 ! 2:  643d9ca86a replay: make atomic ref updates the default behavior
    @@ Metadata
     Author: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Commit message ##
         replay: make atomic ref updates the default behavior
         
         [Commit message unchanged - explains problem and solution]
     
     @@ builtin/replay.c: #include <tree.h>
      
     +enum ref_action_mode {
     +	REF_ACTION_UPDATE,
    -+	REF_ACTION_PRINT
    ++	REF_ACTION_PRINT,
     +};
      
     @@ builtin/replay.c: int cmd_replay
    -					ret = error(_("failed to update ref %s: %s"),
    -						    decoration->name, transaction_err.buf);
    +					ret = error(_("failed to update ref '%s': %s"),
     
     @@ builtin/replay.c: int cmd_replay
    -			ret = error(_("failed to update ref %s: %s"),
    -				    advance_name, transaction_err.buf);
    +			ret = error(_("failed to update ref '%s': %s"),
     
     @@ Documentation/git-replay.adoc
    -+    almost certainly find it more ergonomical to simply have the updating
    ++    almost certainly find it more ergonomic to simply have the updating
     
     @@ Documentation/git-replay.adoc
    -+The default mode can be configured via `replay.refAction` configuration option.
    ++The default mode can be configured via the `replay.refAction` configuration variable.
     
     @@ Documentation/git-replay.adoc: OUTPUT
    -+By default (with `--ref-action=update`), this command produces no output on
    ++By default, or with `--ref-action=update`, this command produces no output on
     
     -       const char * const replay_usage[] = {
    -+       const char *const replay_usage[] = {
    ++       const char * const replay_usage[] = {
     
3:  c35049881d ! 3:  334da71911 replay: add replay.refAction config option
    @@ Metadata
     Author: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Commit message ##
         replay: add replay.refAction config option
         
         [Commit message unchanged]
     
     @@ builtin/replay.c: static struct commit *pick_regular_commit
      	return create_commit(repo, result->tree, pickme, replayed_base);
      }
      
    ++static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)
    ++{
    ++	if (!mode_str || !strcmp(mode_str, "update"))
    ++		return REF_ACTION_UPDATE;
    ++	if (!strcmp(mode_str, "print"))
    ++		return REF_ACTION_PRINT;
    ++	die(_("invalid %s value: '%s'"), source, mode_str);
    ++}
    ++
    ++static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)
    ++{
    ++	const char *config_value = NULL;
    ++
    ++	/* Command line option takes precedence */
    ++	if (ref_action_str)
    ++		return parse_ref_action_mode(ref_action_str, "--ref-action");
    ++
    ++	/* Check config value */
    ++	if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
    ++		return parse_ref_action_mode(config_value, "replay.refAction");
    ++
    ++	/* Default to update mode */
    ++	return REF_ACTION_UPDATE;
    ++}
    ++
     @@ builtin/replay.c: int cmd_replay
      	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
      				  contained, "--contained");
      
    -+	/* Set default mode from config if not specified on command line */
    -+	if (!ref_action_str) {
    -+		const char *config_value = NULL;
    -+		if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value)) {
    -+			if (!strcmp(config_value, "update"))
    -+				ref_action_str = "update";
    -+			else if (!strcmp(config_value, "print"))
    -+				ref_action_str = "print";
    -+			else
    -+				die(_("invalid value for replay.refAction: '%s'"), config_value);
    -+		}
    -+	}
    -+
    -+	/* Default to update mode if still not set */
    -+	if (!ref_action_str)
    -+		ref_action_str = "update";
    -+
    -+	/* Parse ref action mode */
    -+	if (!strcmp(ref_action_str, "update"))
    -+		ref_action = REF_ACTION_UPDATE;
    -+	else if (!strcmp(ref_action_str, "print"))
    -+		ref_action = REF_ACTION_PRINT;
    -+	else
    -+		die(_("unknown --ref-action mode '%s'"), ref_action_str);
    ++	/* Parse ref action mode from command line or config */
    ++	ref_action = get_ref_action_mode(repo, ref_action_str);
     
     @@ t/t3650-replay-basics.sh
     +test_expect_success 'replay.refAction config option' '
     +	START=$(git rev-parse topic2) &&
    -+	test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
    ++	test_when_finished "git branch -f topic2 $START" &&
    ++	test_when_finished "git config --unset replay.refAction || true" &&
     +
     +	git config replay.refAction print &&
     +	git replay --onto main topic1..topic2 >output &&
     +	test_line_count = 1 output &&
    -+	grep "^update refs/heads/topic2 " output &&
    ++	test_grep "^update refs/heads/topic2 " output &&
     +
     +	git branch -f topic2 $START &&
     +	git config replay.refAction update &&
     
     +test_expect_success 'command-line --ref-action overrides config' '
     +	START=$(git rev-parse topic2) &&
    -+	test_when_finished "git branch -f topic2 $START && git config --unset replay.refAction" &&
    ++	test_when_finished "git branch -f topic2 $START" &&
     +
    -+	git config replay.refAction update &&
    ++	test_config replay.refAction update &&
     +	git replay --ref-action=print --onto main topic1..topic2 >output &&
     +	test_line_count = 1 output &&
    -+	grep "^update refs/heads/topic2 " output
    ++	test_grep "^update refs/heads/topic2 " output
     +'
     +
     +test_expect_success 'invalid replay.refAction value' '
    -+	test_when_finished "git config --unset replay.refAction" &&
    -+	git config replay.refAction invalid &&
    ++	test_config replay.refAction invalid &&
     +	test_must_fail git replay --onto main topic1..topic2 2>error &&
    -+	grep "invalid value for replay.refAction" error
    ++	test_grep "invalid.*replay.refAction.*value" error
     +'

-- 
2.51.0

base-commit: 419c72cb8ada252b260efc38ff91fe201de7c8c3

Thanks
- Siddharth

^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v5 1/3] replay: use die_for_incompatible_opt2() for option validation
  2025-10-28 21:46       ` [PATCH v5 " Siddharth Asthana
@ 2025-10-28 21:46         ` Siddharth Asthana
  2025-10-28 21:46         ` [PATCH v5 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
                           ` (2 subsequent siblings)
  3 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 21:46 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

In preparation for adding the --ref-action option, convert option
validation to use die_for_incompatible_opt2(). This helper provides
standardized error messages for mutually exclusive options.

The following commit introduces --ref-action which will be incompatible
with certain other options. Using die_for_incompatible_opt2() now means
that commit can cleanly add its validation using the same pattern,
keeping the validation logic consistent and maintainable.

This also aligns git-replay's option handling with how other Git commands
manage option conflicts, using the established die_for_incompatible_opt*()
helper family.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 builtin/replay.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 6172c8aacc..b64fc72063 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -330,9 +330,9 @@ int cmd_replay(int argc,
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (advance_name_opt && contained)
-		die(_("options '%s' and '%s' cannot be used together"),
-		    "--advance", "--contained");
+	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
+				  contained, "--contained");
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH v5 2/3] replay: make atomic ref updates the default behavior
  2025-10-28 21:46       ` [PATCH v5 " Siddharth Asthana
  2025-10-28 21:46         ` [PATCH v5 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
@ 2025-10-28 21:46         ` Siddharth Asthana
  2025-10-28 21:46         ` [PATCH v5 3/3] replay: add replay.refAction config option Siddharth Asthana
  2025-10-30 19:19         ` [PATCH v6 0/3] replay: make atomic ref updates the default Siddharth Asthana
  3 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 21:46 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

The git replay command currently outputs update commands that can be
piped to update-ref to achieve a rebase, e.g.

  git replay --onto main topic1..topic2 | git update-ref --stdin

This separation had advantages for three special cases:
  * it made testing easy (when state isn't modified from one step to
    the next, you don't need to make temporary branches or have undo
    commands, or try to track the changes)
  * it provided a natural can-it-rebase-cleanly (and what would it
    rebase to) capability without automatically updating refs, similar
    to a --dry-run
  * it provided a natural low-level tool for the suite of hash-object,
    mktree, commit-tree, mktag, merge-tree, and update-ref, allowing
    users to have another building block for experimentation and making
    new tools

However, it should be noted that all three of these are somewhat
special cases; users, whether on the client or server side, would
almost certainly find it more ergonomic to simply have the updating
of refs be the default.

For server-side operations in particular, the pipeline architecture
creates process coordination overhead. Server implementations that need
to perform rebases atomically must maintain additional code to:

  1. Spawn and manage a pipeline between git-replay and git-update-ref
  2. Coordinate stdout/stderr streams across the pipe boundary
  3. Handle partial failure states if the pipeline breaks mid-execution
  4. Parse and validate the update-ref command output

Change the default behavior to update refs directly, and atomically (at
least to the extent supported by the refs backend in use). This
eliminates the process coordination overhead for the common case.

For users needing the traditional pipeline workflow, add a new
--ref-action=<mode> option that preserves the original behavior:

  git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin

The mode can be:
  * update (default): Update refs directly using an atomic transaction
  * print: Output update-ref commands for pipeline use

Implementation details:

The atomic ref updates are implemented using Git's ref transaction API.
In cmd_replay(), when not in `print` mode, we initialize a transaction
using ref_store_transaction_begin() with the default atomic behavior.
As commits are replayed, ref updates are staged into the transaction
using ref_transaction_update(). Finally, ref_transaction_commit()
applies all updates atomically—either all updates succeed or none do.

To avoid code duplication between the 'print' and 'update' modes, this
commit extracts a handle_ref_update() helper function. This function
takes the mode (as an enum) and either prints the update command or
stages it into the transaction. Using an enum rather than passing the
string around provides type safety and allows the compiler to catch
typos. The switch statement makes it easy to add future modes.

The helper function signature:

  static int handle_ref_update(enum ref_action_mode mode,
                                struct ref_transaction *transaction,
                                const char *refname,
                                const struct object_id *new_oid,
                                const struct object_id *old_oid,
                                struct strbuf *err)

The enum is defined as:

  enum ref_action_mode {
      REF_ACTION_UPDATE,
      REF_ACTION_PRINT
  };

The mode string is converted to enum immediately after parse_options()
to avoid string comparisons throughout the codebase and provide compiler
protection against typos.

Test suite changes:

All existing tests that expected command output now use
--ref-action=print to preserve their original behavior. This keeps
the tests valid while allowing them to verify that the pipeline workflow
still works correctly.

New tests were added to verify:
  - Default atomic behavior (no output, refs updated directly)
  - Bare repository support (server-side use case)
  - Equivalence between traditional pipeline and atomic updates
  - Real atomicity using a lock file to verify all-or-nothing guarantee
  - Test isolation using test_when_finished to clean up state

The bare repository tests were fixed to rebuild their expectations
independently rather than comparing to previous test output, improving
test reliability and isolation.

A following commit will add a replay.refAction configuration
option for users who prefer the traditional pipeline output as their
default behavior.

Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc | 65 +++++++++++++++--------
 builtin/replay.c              | 98 +++++++++++++++++++++++++++++++----
 t/t3650-replay-basics.sh      | 44 +++++++++++++---
 3 files changed, 167 insertions(+), 40 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 0b12bf8aa4..037b093196 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...
 
 DESCRIPTION
 -----------
 
 Takes ranges of commits and replays them onto a new location. Leaves
-the working tree and the index untouched, and updates no references.
-The output of this command is meant to be used as input to
-`git update-ref --stdin`, which would update the relevant branches
+the working tree and the index untouched. By default, updates the
+relevant references using an atomic transaction (all refs update or
+none). Use `--ref-action=print` to avoid automatic ref updates and
+instead get update commands that can be piped to `git update-ref --stdin`
 (see the OUTPUT section below).
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
@@ -29,18 +30,31 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-When `--onto` is specified, the update-ref command(s) in the output will
-update the branch(es) in the revision range to point at the new
-commits, similar to the way how `git rebase --update-refs` updates
-multiple branches in the affected range.
+When `--onto` is specified, the branch(es) in the revision range will be
+updated to point at the new commits (or update commands will be printed
+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`
+updates multiple branches in the affected range.
 
 --advance <branch>::
 	Starting point at which to create the new commits; must be a
 	branch name.
 +
-When `--advance` is specified, the update-ref command(s) in the output
-will update the branch passed as an argument to `--advance` to point at
-the new commits (in other words, this mimics a cherry-pick operation).
+The history is replayed on top of the <branch> and <branch> is updated to
+point at the tip of the resulting history (or an update command will be
+printed if `--ref-action=print` is used). This is different from `--onto`,
+which uses the target only as a starting point without updating it.
+
+--ref-action[=<mode>]::
+	Control how references are updated. The mode can be:
++
+--
+	* `update` (default): Update refs directly using an atomic transaction.
+	  All refs are updated or none are (all-or-nothing behavior).
+	* `print`: Output update-ref commands for pipeline use. This is the
+	  traditional behavior where output can be piped to `git update-ref --stdin`.
+--
++
+The default mode can be configured via the `replay.refAction` configuration variable.
 
 <revision-range>::
 	Range of commits to replay. More than one <revision-range> can
@@ -54,8 +68,11 @@ include::rev-list-options.adoc[]
 OUTPUT
 ------
 
-When there are no conflicts, the output of this command is usable as
-input to `git update-ref --stdin`.  It is of the form:
+By default, or with `--ref-action=update`, this command produces no output on
+success, as refs are updated directly using an atomic transaction.
+
+When using `--ref-action=print`, the output is usable as input to
+`git update-ref --stdin`. It is of the form:
 
 	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
@@ -81,6 +98,14 @@ To simply rebase `mybranch` onto `target`:
 
 ------------
 $ git replay --onto target origin/main..mybranch
+------------
+
+The refs are updated atomically and no output is produced on success.
+
+To see what would be updated without actually updating:
+
+------------
+$ git replay --ref-action=print --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
@@ -88,33 +113,29 @@ To cherry-pick the commits from mybranch onto target:
 
 ------------
 $ git replay --advance target origin/main..mybranch
-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
 ------------
 
 Note that the first two examples replay the exact same commits and on
 top of the exact same new base, they only differ in that the first
-provides instructions to make mybranch point at the new commits and
-the second provides instructions to make target point at them.
+updates mybranch to point at the new commits and the second updates
+target to point at them.
 
 What if you have a stack of branches, one depending upon another, and
 you'd really like to rebase the whole set?
 
 ------------
 $ git replay --contained --onto origin/main origin/main..tipbranch
-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
 ------------
 
+All three branches (`branch1`, `branch2`, and `tipbranch`) are updated
+atomically.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
 
 ------------
 $ git replay --onto origin/main ^base branch1 branch2 branch3
-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 ------------
 
 This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
diff --git a/builtin/replay.c b/builtin/replay.c
index b64fc72063..0564d4d2e7 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -20,6 +20,11 @@
 #include <oidset.h>
 #include <tree.h>
 
+enum ref_action_mode {
+	REF_ACTION_UPDATE,
+	REF_ACTION_PRINT,
+};
+
 static const char *short_commit_name(struct repository *repo,
 				     struct commit *commit)
 {
@@ -284,6 +289,28 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	return create_commit(repo, result->tree, pickme, replayed_base);
 }
 
+static int handle_ref_update(enum ref_action_mode mode,
+			     struct ref_transaction *transaction,
+			     const char *refname,
+			     const struct object_id *new_oid,
+			     const struct object_id *old_oid,
+			     struct strbuf *err)
+{
+	switch (mode) {
+	case REF_ACTION_PRINT:
+		printf("update %s %s %s\n",
+		       refname,
+		       oid_to_hex(new_oid),
+		       oid_to_hex(old_oid));
+		return 0;
+	case REF_ACTION_UPDATE:
+		return ref_transaction_update(transaction, refname, new_oid, old_oid,
+					      NULL, NULL, 0, "git replay", err);
+	default:
+		BUG("unknown ref_action_mode %d", mode);
+	}
+}
+
 int cmd_replay(int argc,
 	       const char **argv,
 	       const char *prefix,
@@ -294,6 +321,8 @@ int cmd_replay(int argc,
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
 	int contained = 0;
+	const char *ref_action_str = NULL;
+	enum ref_action_mode ref_action = REF_ACTION_UPDATE;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -302,12 +331,14 @@ int cmd_replay(int argc,
 	struct merge_result result;
 	struct strset *update_refs = NULL;
 	kh_oid_map_t *replayed_commits;
+	struct ref_transaction *transaction = NULL;
+	struct strbuf transaction_err = STRBUF_INIT;
 	int ret = 0;
 
-	const char * const replay_usage[] = {
+	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto <newbase> | --advance <branch>) "
-		   "<revision-range>..."),
+		   "[--ref-action[=<mode>]] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -319,6 +350,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &contained,
 			 N_("advance all branches contained in revision-range")),
+		OPT_STRING(0, "ref-action", &ref_action_str,
+			   N_("mode"),
+			   N_("control ref update behavior (update|print)")),
 		OPT_END()
 	};
 
@@ -333,6 +367,18 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
+	/* Default to update mode if not specified */
+	if (!ref_action_str)
+		ref_action_str = "update";
+
+	/* Parse ref action mode */
+	if (!strcmp(ref_action_str, "update"))
+		ref_action = REF_ACTION_UPDATE;
+	else if (!strcmp(ref_action_str, "print"))
+		ref_action = REF_ACTION_PRINT;
+	else
+		die(_("unknown --ref-action mode '%s'"), ref_action_str);
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
@@ -389,6 +435,17 @@ int cmd_replay(int argc,
 	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
 			      &onto, &update_refs);
 
+	/* Initialize ref transaction if using update mode */
+	if (ref_action == REF_ACTION_UPDATE) {
+		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
+							  0, &transaction_err);
+		if (!transaction) {
+			ret = error(_("failed to begin ref transaction: %s"),
+				    transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
 	if (!onto) /* FIXME: Should handle replaying down to root commit */
 		die("Replaying down to root commit is not supported yet!");
 
@@ -434,10 +491,15 @@ int cmd_replay(int argc,
 			if (decoration->type == DECORATION_REF_LOCAL &&
 			    (contained || strset_contains(update_refs,
 							  decoration->name))) {
-				printf("update %s %s %s\n",
-				       decoration->name,
-				       oid_to_hex(&last_commit->object.oid),
-				       oid_to_hex(&commit->object.oid));
+				if (handle_ref_update(ref_action, transaction,
+						      decoration->name,
+						      &last_commit->object.oid,
+						      &commit->object.oid,
+						      &transaction_err) < 0) {
+					ret = error(_("failed to update ref '%s': %s"),
+						    decoration->name, transaction_err.buf);
+					goto cleanup;
+				}
 			}
 			decoration = decoration->next;
 		}
@@ -445,10 +507,23 @@ int cmd_replay(int argc,
 
 	/* In --advance mode, advance the target ref */
 	if (result.clean == 1 && advance_name) {
-		printf("update %s %s %s\n",
-		       advance_name,
-		       oid_to_hex(&last_commit->object.oid),
-		       oid_to_hex(&onto->object.oid));
+		if (handle_ref_update(ref_action, transaction, advance_name,
+				      &last_commit->object.oid,
+				      &onto->object.oid,
+				      &transaction_err) < 0) {
+			ret = error(_("failed to update ref '%s': %s"),
+				    advance_name, transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
+	/* Commit the ref transaction if we have one */
+	if (transaction && result.clean == 1) {
+		if (ref_transaction_commit(transaction, &transaction_err)) {
+			ret = error(_("failed to commit ref transaction: %s"),
+				    transaction_err.buf);
+			goto cleanup;
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
@@ -460,6 +535,9 @@ int cmd_replay(int argc,
 	ret = result.clean;
 
 cleanup:
+	if (transaction)
+		ref_transaction_free(transaction);
+	strbuf_release(&transaction_err);
 	release_revisions(&revs);
 	free(advance_name);
 
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 58b3759935..123734b49f 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git replay --onto main topic1..topic2 >result &&
+	git replay --ref-action=print --onto main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 '
 
 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
-	git -C bare replay --onto main topic1..topic2 >result-bare &&
+	git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
@@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
 	# 4th field of result is hash for main instead of hash for topic2
 
-	git replay --advance main topic1..topic2 >result &&
+	git replay --ref-action=print --advance main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 '
 
 test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
-	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
@@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 '
 
 test_expect_success 'using replay to also rebase a contained branch' '
-	git replay --contained --onto main main..topic3 >result &&
+	git replay --ref-action=print --contained --onto main main..topic3 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '
 '
 
 test_expect_success 'using replay on bare repo to also rebase a contained branch' '
-	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&
 	test_cmp expect result-bare
 '
 
 test_expect_success 'using replay to rebase multiple divergent branches' '
-	git replay --onto main ^topic1 topic2 topic4 >result &&
+	git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
 '
 
 test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
-	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+	git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
 
 	test_line_count = 4 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -217,4 +217,32 @@ test_expect_success 'merge.directoryRenames=false' '
 		--onto rename-onto rename-onto..rename-from
 '
 
+test_expect_success 'default atomic behavior updates refs directly' '
+	# Store original state for cleanup
+	test_when_finished "git branch -f topic2 topic1" &&
+
+	# Test default atomic behavior (no output, refs updated)
+	git replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'atomic behavior in bare repository' '
+	# Test atomic updates work in bare repo
+	git -C bare replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated in bare repo
+	git -C bare log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	# Reset for other tests
+	git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH v5 3/3] replay: add replay.refAction config option
  2025-10-28 21:46       ` [PATCH v5 " Siddharth Asthana
  2025-10-28 21:46         ` [PATCH v5 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
  2025-10-28 21:46         ` [PATCH v5 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
@ 2025-10-28 21:46         ` Siddharth Asthana
  2025-10-29 16:19           ` Christian Couder
  2025-10-30 19:19         ` [PATCH v6 0/3] replay: make atomic ref updates the default Siddharth Asthana
  3 siblings, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-28 21:46 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

Add a configuration option to control the default behavior of git replay
for updating references. This allows users who prefer the traditional
pipeline output to set it once in their config instead of passing
--ref-action=print with every command.

The config option uses string values that mirror the behavior modes:
  * replay.refAction = update (default): atomic ref updates
  * replay.refAction = print: output commands for pipeline

The command-line --ref-action option always overrides the config setting,
allowing users to temporarily change behavior for a single invocation.

Implementation details:

In cmd_replay(), after parsing command-line options, we check if
--ref-action was provided. If not, we read the configuration using
repo_config_get_string_tmp(). If the config variable is set, we validate
the value and use it to set the ref_action_str:

  Config value      Internal mode    Behavior
  ──────────────────────────────────────────────────────────────
  "update"          "update"         Atomic ref updates (default)
  "print"           "print"          Pipeline output
  (not set)         "update"         Atomic ref updates (default)
  (invalid)         error            Die with helpful message

If an invalid value is provided, we die() immediately with an error
message explaining the valid options. This catches configuration errors
early and provides clear guidance to users.

The command-line --ref-action option, when provided, overrides the
config value. This precedence allows users to set their preferred default
while still having per-invocation control:

  git config replay.refAction print         # Set default
  git replay --ref-action=update --onto main topic  # Override once

The config and command-line option use the same value names ('update'
and 'print') for consistency and clarity. This makes it immediately
obvious how the config maps to the command-line option, addressing
feedback about the relationship between configuration and command-line
options being clear to users.

Examples:

$ git config --global replay.refAction print
$ git replay --onto main topic1..topic2 | git update-ref --stdin

$ git replay --ref-action=update --onto main topic1..topic2

$ git config replay.refAction update
$ git replay --onto main topic1..topic2  # Updates refs directly

The implementation follows Git's standard configuration precedence:
command-line options override config values, which matches user
expectations across all Git commands.

Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/config/replay.adoc | 11 +++++++
 builtin/replay.c                 | 39 ++++++++++++++++++-------
 t/t3650-replay-basics.sh         | 49 +++++++++++++++++++++++++++++++-
 3 files changed, 87 insertions(+), 12 deletions(-)
 create mode 100644 Documentation/config/replay.adoc

diff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc
new file mode 100644
index 0000000000..7d549d2f0e
--- /dev/null
+++ b/Documentation/config/replay.adoc
@@ -0,0 +1,11 @@
+replay.refAction::
+	Specifies the default mode for handling reference updates in
+	`git replay`. The value can be:
++
+--
+	* `update`: Update refs directly using an atomic transaction (default behavior).
+	* `print`: Output update-ref commands for pipeline use.
+--
++
+This setting can be overridden with the `--ref-action` command-line option.
+When not configured, `git replay` defaults to `update` mode.
diff --git a/builtin/replay.c b/builtin/replay.c
index 0564d4d2e7..17898bbdd1 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -8,6 +8,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
+#include "config.h"
 #include "environment.h"
 #include "hex.h"
 #include "lockfile.h"
@@ -289,6 +290,31 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	return create_commit(repo, result->tree, pickme, replayed_base);
 }
 
+static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)
+{
+	if (!mode_str || !strcmp(mode_str, "update"))
+		return REF_ACTION_UPDATE;
+	if (!strcmp(mode_str, "print"))
+		return REF_ACTION_PRINT;
+	die(_("invalid %s value: '%s'"), source, mode_str);
+}
+
+static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)
+{
+	const char *config_value = NULL;
+
+	/* Command line option takes precedence */
+	if (ref_action_str)
+		return parse_ref_action_mode(ref_action_str, "--ref-action");
+
+	/* Check config value */
+	if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
+		return parse_ref_action_mode(config_value, "replay.refAction");
+
+	/* Default to update mode */
+	return REF_ACTION_UPDATE;
+}
+
 static int handle_ref_update(enum ref_action_mode mode,
 			     struct ref_transaction *transaction,
 			     const char *refname,
@@ -367,17 +393,8 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
-	/* Default to update mode if not specified */
-	if (!ref_action_str)
-		ref_action_str = "update";
-
-	/* Parse ref action mode */
-	if (!strcmp(ref_action_str, "update"))
-		ref_action = REF_ACTION_UPDATE;
-	else if (!strcmp(ref_action_str, "print"))
-		ref_action = REF_ACTION_PRINT;
-	else
-		die(_("unknown --ref-action mode '%s'"), ref_action_str);
+	/* Parse ref action mode from command line or config */
+	ref_action = get_ref_action_mode(repo, ref_action_str);
 
 	advance_name = xstrdup_or_null(advance_name_opt);
 
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 123734b49f..9ca04b2fdd 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -219,7 +219,8 @@ test_expect_success 'merge.directoryRenames=false' '
 
 test_expect_success 'default atomic behavior updates refs directly' '
 	# Store original state for cleanup
-	test_when_finished "git branch -f topic2 topic1" &&
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
 
 	# Test default atomic behavior (no output, refs updated)
 	git replay --onto main topic1..topic2 >output &&
@@ -232,6 +233,10 @@ test_expect_success 'default atomic behavior updates refs directly' '
 '
 
 test_expect_success 'atomic behavior in bare repository' '
+	# Store original state for cleanup
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
+
 	# Test atomic updates work in bare repo
 	git -C bare replay --onto main topic1..topic2 >output &&
 	test_must_be_empty output &&
@@ -245,4 +250,46 @@ test_expect_success 'atomic behavior in bare repository' '
 	git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)
 '
 
+test_expect_success 'replay.refAction config option' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
+	test_when_finished "git config --unset replay.refAction || true" &&
+
+	# Set config to print
+	git config replay.refAction print &&
+	git replay --onto main topic1..topic2 >output &&
+	test_line_count = 1 output &&
+	test_grep "^update refs/heads/topic2 " output &&
+
+	# Reset and test update mode
+	git branch -f topic2 $START &&
+	git config replay.refAction update &&
+	git replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'command-line --ref-action overrides config' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
+
+	# Set config to update but use --ref-action=print
+	test_config replay.refAction update &&
+	git replay --ref-action=print --onto main topic1..topic2 >output &&
+	test_line_count = 1 output &&
+	test_grep "^update refs/heads/topic2 " output
+'
+
+test_expect_success 'invalid replay.refAction value' '
+	test_config replay.refAction invalid &&
+	test_must_fail git replay --onto main topic1..topic2 2>error &&
+	test_grep "invalid.*replay.refAction.*value" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* Re: [PATCH v5 3/3] replay: add replay.refAction config option
  2025-10-28 21:46         ` [PATCH v5 3/3] replay: add replay.refAction config option Siddharth Asthana
@ 2025-10-29 16:19           ` Christian Couder
  2025-10-29 17:00             ` Siddharth Asthana
  0 siblings, 1 reply; 129+ messages in thread
From: Christian Couder @ 2025-10-29 16:19 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, phillip.wood123, phillip.wood, newren, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Tue, Oct 28, 2025 at 10:46 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:

> +static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)

Nit: it's a bit strange that it's called "ref_action_str" everywhere
except here where it's called "mode_str". I'd prefer "ref_action"
everywhere.

(I understand that "mode" is related to parse_ref_action_mode() having
"mode" in its name but it's the case for get_ref_action_mode() too.)

> +test_expect_success 'replay.refAction config option' '
> +       # Store original state
> +       START=$(git rev-parse topic2) &&
> +       test_when_finished "git branch -f topic2 $START" &&
> +       test_when_finished "git config --unset replay.refAction || true" &&

Is there something preventing test_config to be used in this test
while it's used in other tests below?

> +       # Set config to print
> +       git config replay.refAction print &&
> +       git replay --onto main topic1..topic2 >output &&
> +       test_line_count = 1 output &&
> +       test_grep "^update refs/heads/topic2 " output &&
> +
> +       # Reset and test update mode
> +       git branch -f topic2 $START &&
> +       git config replay.refAction update &&
> +       git replay --onto main topic1..topic2 >output &&
> +       test_must_be_empty output &&
> +
> +       # Verify ref was updated
> +       git log --format=%s topic2 >actual &&
> +       test_write_lines E D M L B A >expect &&
> +       test_cmp expect actual
> +'

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v5 3/3] replay: add replay.refAction config option
  2025-10-29 16:19           ` Christian Couder
@ 2025-10-29 17:00             ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-29 17:00 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, phillip.wood123, phillip.wood, newren, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 29/10/25 21:49, Christian Couder wrote:
> On Tue, Oct 28, 2025 at 10:46 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>
>> +static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)


Hi Christian,


> Nit: it's a bit strange that it's called "ref_action_str" everywhere
> except here where it's called "mode_str". I'd prefer "ref_action"
> everywhere.


You are right - that's inconsistent naming. Looking at similar patterns 
in the
codebase like `parse_sign_mode()` in gpg-interface.c, the parameter is just
called `arg`, but for clarity I should stick with `ref_action` throughout.

The inconsistency came from trying to distinguish the string parameter from
the function name, but it just makes the code harder to follow. I will 
rename
both `mode_str` parameters to `ref_action` in the helper functions.


>
> (I understand that "mode" is related to parse_ref_action_mode() having
> "mode" in its name but it's the case for get_ref_action_mode() too.)
>
>> +test_expect_success 'replay.refAction config option' '
>> +       # Store original state
>> +       START=$(git rev-parse topic2) &&
>> +       test_when_finished "git branch -f topic2 $START" &&
>> +       test_when_finished "git config --unset replay.refAction || true" &&
> Is there something preventing test_config to be used in this test
> while it's used in other tests below?


Nothing preventing it - I was being overly cautious because this test sets
config twice in sequence, but `test_config` handles that fine. Looking at
the test-lib-functions.sh implementation, `test_config` uses 
`test_when_finished`
with `test_unconfig` which properly handles multiple config operations.

The manual approach is actually more fragile since it relies on the `|| 
true`
pattern and doesn't guarantee cleanup if the test fails early. I will 
switch
to `test_config` for consistency with the other config tests.

Both fixes are straightforward - I will send them in v6.

Thanks for the careful review and keeping the code quality high!

- Siddharth


>
>> +       # Set config to print
>> +       git config replay.refAction print &&
>> +       git replay --onto main topic1..topic2 >output &&
>> +       test_line_count = 1 output &&
>> +       test_grep "^update refs/heads/topic2 " output &&
>> +
>> +       # Reset and test update mode
>> +       git branch -f topic2 $START &&
>> +       git config replay.refAction update &&
>> +       git replay --onto main topic1..topic2 >output &&
>> +       test_must_be_empty output &&
>> +
>> +       # Verify ref was updated
>> +       git log --format=%s topic2 >actual &&
>> +       test_write_lines E D M L B A >expect &&
>> +       test_cmp expect actual
>> +'

^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v6 0/3] replay: make atomic ref updates the default
  2025-10-28 21:46       ` [PATCH v5 " Siddharth Asthana
                           ` (2 preceding siblings ...)
  2025-10-28 21:46         ` [PATCH v5 3/3] replay: add replay.refAction config option Siddharth Asthana
@ 2025-10-30 19:19         ` Siddharth Asthana
  2025-10-30 19:19           ` [PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
                             ` (4 more replies)
  3 siblings, 5 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-30 19:19 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

This is v6 of the git-replay atomic updates series.

This version addresses Christian's feedback from v5 regarding code
consistency and test patterns. Thanks to Christian, Junio, Phillip,
Elijah, Patrick, and Karthik for the thorough reviews.

## Changes in v6

**Fixed parameter naming inconsistency**

Christian pointed out that parse_ref_action_mode() used `mode_str` as the
parameter name while the rest of the code used `ref_action`. Changed to
use `ref_action` consistently throughout for better code readability.

**Improved test cleanup pattern**

Replaced manual `git config --unset` with `test_when_finished` pattern with
`test_config` helper in the replay.refAction config test. The test_config
helper automatically handles cleanup via test_when_finished, providing
better test isolation and following Git test suite best practices.

These are code quality improvements that don't change functionality but
make the code more consistent with Git's established patterns.

## Technical Implementation

Same as v5, using Git's ref transaction API:

- ref_store_transaction_begin() with default atomic behavior
- ref_transaction_update() to stage each update
- ref_transaction_commit() for atomic application

The helper functions provide clean separation:

- parse_ref_action_mode(): Validates strings and converts to enum
- get_ref_action_mode(): Implements command-line > config > default precedence
- handle_ref_update(): Uses type-safe enum with switch statement

Config reading uses repo_config_get_string_tmp() for simplicity while
maintaining proper precedence behavior.

## Testing

All tests pass:

- t3650-replay-basics.sh (20 tests pass)
- Config tests now use test_config for automatic cleanup
- Atomic behavior tests verify direct ref updates
- Backward compatibility maintained for pipeline workflow

CI results: https://gitlab.com/gitlab-org/git/-/pipelines/2130504045

Siddharth Asthana (3):
  replay: use die_for_incompatible_opt2() for option validation
  replay: make atomic ref updates the default behavior
  replay: add replay.refAction config option

 Documentation/config/replay.adoc |  11 +++
 Documentation/git-replay.adoc    |  65 +++++++++++------
 builtin/replay.c                 | 121 +++++++++++++++++++++++++++----
 t/t3650-replay-basics.sh         |  90 +++++++++++++++++++++--
 4 files changed, 244 insertions(+), 43 deletions(-)
 create mode 100644 Documentation/config/replay.adoc

Range-diff against v5:
1:  3e27d07d3b = 1:  1f0fad0cac replay: use die_for_incompatible_opt2() for option validation
2:  643d9ca86a = 2:  bfc6188234 replay: make atomic ref updates the default behavior
3:  334da71911 ! 3:  6b2a44c72c replay: add replay.refAction config option
    @@ Metadata
     Author: Siddharth Asthana <siddharthasthana31@gmail.com>
     
      ## Commit message ##
         replay: add replay.refAction config option
     
         [Commit message unchanged]
     
      ## builtin/replay.c ##
     @@ builtin/replay.c: static struct commit *pick_regular_commit
      	return create_commit(repo, result->tree, pickme, replayed_base);
      }
      
    -+static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)
    ++static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
     +{
    -+	if (!mode_str || !strcmp(mode_str, "update"))
    ++	if (!ref_action || !strcmp(ref_action, "update"))
     +		return REF_ACTION_UPDATE;
    -+	if (!strcmp(mode_str, "print"))
    ++	if (!strcmp(ref_action, "print"))
     +		return REF_ACTION_PRINT;
    -+	die(_("invalid %s value: '%s'"), source, mode_str);
    ++	die(_("invalid %s value: '%s'"), source, ref_action);
     +}
     +
     +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)
     
      ## t/t3650-replay-basics.sh ##
     @@ t/t3650-replay-basics.sh
     +test_expect_success 'replay.refAction config option' '
     +	START=$(git rev-parse topic2) &&
     +	test_when_finished "git branch -f topic2 $START" &&
    -+	test_when_finished "git config --unset replay.refAction || true" &&
     +
    -+	git config replay.refAction print &&
    ++	test_config replay.refAction print &&
     +	git replay --onto main topic1..topic2 >output &&
     +	test_line_count = 1 output &&
     +	test_grep "^update refs/heads/topic2 " output &&
     +
     +	git branch -f topic2 $START &&
    -+	git config replay.refAction update &&
    ++	test_config replay.refAction update &&
     +	git replay --onto main topic1..topic2 >output &&
     
     +test_expect_success 'command-line --ref-action overrides config' '
-- 
2.51.0

base-commit: 57da342c78d8bf00259d2b720292e5b3035dadcc

Thanks
- Siddharth

^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation
  2025-10-30 19:19         ` [PATCH v6 0/3] replay: make atomic ref updates the default Siddharth Asthana
@ 2025-10-30 19:19           ` Siddharth Asthana
  2025-10-31 18:47             ` Elijah Newren
  2025-10-30 19:19           ` [PATCH v6 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
                             ` (3 subsequent siblings)
  4 siblings, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-30 19:19 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

In preparation for adding the --ref-action option, convert option
validation to use die_for_incompatible_opt2(). This helper provides
standardized error messages for mutually exclusive options.

The following commit introduces --ref-action which will be incompatible
with certain other options. Using die_for_incompatible_opt2() now means
that commit can cleanly add its validation using the same pattern,
keeping the validation logic consistent and maintainable.

This also aligns git-replay's option handling with how other Git commands
manage option conflicts, using the established die_for_incompatible_opt*()
helper family.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 builtin/replay.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 6172c8aacc..b64fc72063 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -330,9 +330,9 @@ int cmd_replay(int argc,
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (advance_name_opt && contained)
-		die(_("options '%s' and '%s' cannot be used together"),
-		    "--advance", "--contained");
+	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
+				  contained, "--contained");
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH v6 2/3] replay: make atomic ref updates the default behavior
  2025-10-30 19:19         ` [PATCH v6 0/3] replay: make atomic ref updates the default Siddharth Asthana
  2025-10-30 19:19           ` [PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
@ 2025-10-30 19:19           ` Siddharth Asthana
  2025-10-31 18:49             ` Elijah Newren
  2025-11-03 16:25             ` Phillip Wood
  2025-10-30 19:19           ` [PATCH v6 3/3] replay: add replay.refAction config option Siddharth Asthana
                             ` (2 subsequent siblings)
  4 siblings, 2 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-30 19:19 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

The git replay command currently outputs update commands that can be
piped to update-ref to achieve a rebase, e.g.

  git replay --onto main topic1..topic2 | git update-ref --stdin

This separation had advantages for three special cases:
  * it made testing easy (when state isn't modified from one step to
    the next, you don't need to make temporary branches or have undo
    commands, or try to track the changes)
  * it provided a natural can-it-rebase-cleanly (and what would it
    rebase to) capability without automatically updating refs, similar
    to a --dry-run
  * it provided a natural low-level tool for the suite of hash-object,
    mktree, commit-tree, mktag, merge-tree, and update-ref, allowing
    users to have another building block for experimentation and making
    new tools

However, it should be noted that all three of these are somewhat
special cases; users, whether on the client or server side, would
almost certainly find it more ergonomic to simply have the updating
of refs be the default.

For server-side operations in particular, the pipeline architecture
creates process coordination overhead. Server implementations that need
to perform rebases atomically must maintain additional code to:

  1. Spawn and manage a pipeline between git-replay and git-update-ref
  2. Coordinate stdout/stderr streams across the pipe boundary
  3. Handle partial failure states if the pipeline breaks mid-execution
  4. Parse and validate the update-ref command output

Change the default behavior to update refs directly, and atomically (at
least to the extent supported by the refs backend in use). This
eliminates the process coordination overhead for the common case.

For users needing the traditional pipeline workflow, add a new
--ref-action=<mode> option that preserves the original behavior:

  git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin

The mode can be:
  * update (default): Update refs directly using an atomic transaction
  * print: Output update-ref commands for pipeline use

Implementation details:

The atomic ref updates are implemented using Git's ref transaction API.
In cmd_replay(), when not in `print` mode, we initialize a transaction
using ref_store_transaction_begin() with the default atomic behavior.
As commits are replayed, ref updates are staged into the transaction
using ref_transaction_update(). Finally, ref_transaction_commit()
applies all updates atomically—either all updates succeed or none do.

To avoid code duplication between the 'print' and 'update' modes, this
commit extracts a handle_ref_update() helper function. This function
takes the mode (as an enum) and either prints the update command or
stages it into the transaction. Using an enum rather than passing the
string around provides type safety and allows the compiler to catch
typos. The switch statement makes it easy to add future modes.

The helper function signature:

  static int handle_ref_update(enum ref_action_mode mode,
                                struct ref_transaction *transaction,
                                const char *refname,
                                const struct object_id *new_oid,
                                const struct object_id *old_oid,
                                struct strbuf *err)

The enum is defined as:

  enum ref_action_mode {
      REF_ACTION_UPDATE,
      REF_ACTION_PRINT
  };

The mode string is converted to enum immediately after parse_options()
to avoid string comparisons throughout the codebase and provide compiler
protection against typos.

Test suite changes:

All existing tests that expected command output now use
--ref-action=print to preserve their original behavior. This keeps
the tests valid while allowing them to verify that the pipeline workflow
still works correctly.

New tests were added to verify:
  - Default atomic behavior (no output, refs updated directly)
  - Bare repository support (server-side use case)
  - Equivalence between traditional pipeline and atomic updates
  - Real atomicity using a lock file to verify all-or-nothing guarantee
  - Test isolation using test_when_finished to clean up state

The bare repository tests were fixed to rebuild their expectations
independently rather than comparing to previous test output, improving
test reliability and isolation.

A following commit will add a replay.refAction configuration
option for users who prefer the traditional pipeline output as their
default behavior.

Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc | 65 +++++++++++++++--------
 builtin/replay.c              | 98 +++++++++++++++++++++++++++++++----
 t/t3650-replay-basics.sh      | 44 +++++++++++++---
 3 files changed, 167 insertions(+), 40 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 0b12bf8aa4..037b093196 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...
 
 DESCRIPTION
 -----------
 
 Takes ranges of commits and replays them onto a new location. Leaves
-the working tree and the index untouched, and updates no references.
-The output of this command is meant to be used as input to
-`git update-ref --stdin`, which would update the relevant branches
+the working tree and the index untouched. By default, updates the
+relevant references using an atomic transaction (all refs update or
+none). Use `--ref-action=print` to avoid automatic ref updates and
+instead get update commands that can be piped to `git update-ref --stdin`
 (see the OUTPUT section below).
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
@@ -29,18 +30,31 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-When `--onto` is specified, the update-ref command(s) in the output will
-update the branch(es) in the revision range to point at the new
-commits, similar to the way how `git rebase --update-refs` updates
-multiple branches in the affected range.
+When `--onto` is specified, the branch(es) in the revision range will be
+updated to point at the new commits (or update commands will be printed
+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`
+updates multiple branches in the affected range.
 
 --advance <branch>::
 	Starting point at which to create the new commits; must be a
 	branch name.
 +
-When `--advance` is specified, the update-ref command(s) in the output
-will update the branch passed as an argument to `--advance` to point at
-the new commits (in other words, this mimics a cherry-pick operation).
+The history is replayed on top of the <branch> and <branch> is updated to
+point at the tip of the resulting history (or an update command will be
+printed if `--ref-action=print` is used). This is different from `--onto`,
+which uses the target only as a starting point without updating it.
+
+--ref-action[=<mode>]::
+	Control how references are updated. The mode can be:
++
+--
+	* `update` (default): Update refs directly using an atomic transaction.
+	  All refs are updated or none are (all-or-nothing behavior).
+	* `print`: Output update-ref commands for pipeline use. This is the
+	  traditional behavior where output can be piped to `git update-ref --stdin`.
+--
++
+The default mode can be configured via the `replay.refAction` configuration variable.
 
 <revision-range>::
 	Range of commits to replay. More than one <revision-range> can
@@ -54,8 +68,11 @@ include::rev-list-options.adoc[]
 OUTPUT
 ------
 
-When there are no conflicts, the output of this command is usable as
-input to `git update-ref --stdin`.  It is of the form:
+By default, or with `--ref-action=update`, this command produces no output on
+success, as refs are updated directly using an atomic transaction.
+
+When using `--ref-action=print`, the output is usable as input to
+`git update-ref --stdin`. It is of the form:
 
 	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
@@ -81,6 +98,14 @@ To simply rebase `mybranch` onto `target`:
 
 ------------
 $ git replay --onto target origin/main..mybranch
+------------
+
+The refs are updated atomically and no output is produced on success.
+
+To see what would be updated without actually updating:
+
+------------
+$ git replay --ref-action=print --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
@@ -88,33 +113,29 @@ To cherry-pick the commits from mybranch onto target:
 
 ------------
 $ git replay --advance target origin/main..mybranch
-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
 ------------
 
 Note that the first two examples replay the exact same commits and on
 top of the exact same new base, they only differ in that the first
-provides instructions to make mybranch point at the new commits and
-the second provides instructions to make target point at them.
+updates mybranch to point at the new commits and the second updates
+target to point at them.
 
 What if you have a stack of branches, one depending upon another, and
 you'd really like to rebase the whole set?
 
 ------------
 $ git replay --contained --onto origin/main origin/main..tipbranch
-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
 ------------
 
+All three branches (`branch1`, `branch2`, and `tipbranch`) are updated
+atomically.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
 
 ------------
 $ git replay --onto origin/main ^base branch1 branch2 branch3
-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 ------------
 
 This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
diff --git a/builtin/replay.c b/builtin/replay.c
index b64fc72063..0564d4d2e7 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -20,6 +20,11 @@
 #include <oidset.h>
 #include <tree.h>
 
+enum ref_action_mode {
+	REF_ACTION_UPDATE,
+	REF_ACTION_PRINT,
+};
+
 static const char *short_commit_name(struct repository *repo,
 				     struct commit *commit)
 {
@@ -284,6 +289,28 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	return create_commit(repo, result->tree, pickme, replayed_base);
 }
 
+static int handle_ref_update(enum ref_action_mode mode,
+			     struct ref_transaction *transaction,
+			     const char *refname,
+			     const struct object_id *new_oid,
+			     const struct object_id *old_oid,
+			     struct strbuf *err)
+{
+	switch (mode) {
+	case REF_ACTION_PRINT:
+		printf("update %s %s %s\n",
+		       refname,
+		       oid_to_hex(new_oid),
+		       oid_to_hex(old_oid));
+		return 0;
+	case REF_ACTION_UPDATE:
+		return ref_transaction_update(transaction, refname, new_oid, old_oid,
+					      NULL, NULL, 0, "git replay", err);
+	default:
+		BUG("unknown ref_action_mode %d", mode);
+	}
+}
+
 int cmd_replay(int argc,
 	       const char **argv,
 	       const char *prefix,
@@ -294,6 +321,8 @@ int cmd_replay(int argc,
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
 	int contained = 0;
+	const char *ref_action_str = NULL;
+	enum ref_action_mode ref_action = REF_ACTION_UPDATE;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -302,12 +331,14 @@ int cmd_replay(int argc,
 	struct merge_result result;
 	struct strset *update_refs = NULL;
 	kh_oid_map_t *replayed_commits;
+	struct ref_transaction *transaction = NULL;
+	struct strbuf transaction_err = STRBUF_INIT;
 	int ret = 0;
 
-	const char * const replay_usage[] = {
+	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto <newbase> | --advance <branch>) "
-		   "<revision-range>..."),
+		   "[--ref-action[=<mode>]] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -319,6 +350,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &contained,
 			 N_("advance all branches contained in revision-range")),
+		OPT_STRING(0, "ref-action", &ref_action_str,
+			   N_("mode"),
+			   N_("control ref update behavior (update|print)")),
 		OPT_END()
 	};
 
@@ -333,6 +367,18 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
+	/* Default to update mode if not specified */
+	if (!ref_action_str)
+		ref_action_str = "update";
+
+	/* Parse ref action mode */
+	if (!strcmp(ref_action_str, "update"))
+		ref_action = REF_ACTION_UPDATE;
+	else if (!strcmp(ref_action_str, "print"))
+		ref_action = REF_ACTION_PRINT;
+	else
+		die(_("unknown --ref-action mode '%s'"), ref_action_str);
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
@@ -389,6 +435,17 @@ int cmd_replay(int argc,
 	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
 			      &onto, &update_refs);
 
+	/* Initialize ref transaction if using update mode */
+	if (ref_action == REF_ACTION_UPDATE) {
+		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
+							  0, &transaction_err);
+		if (!transaction) {
+			ret = error(_("failed to begin ref transaction: %s"),
+				    transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
 	if (!onto) /* FIXME: Should handle replaying down to root commit */
 		die("Replaying down to root commit is not supported yet!");
 
@@ -434,10 +491,15 @@ int cmd_replay(int argc,
 			if (decoration->type == DECORATION_REF_LOCAL &&
 			    (contained || strset_contains(update_refs,
 							  decoration->name))) {
-				printf("update %s %s %s\n",
-				       decoration->name,
-				       oid_to_hex(&last_commit->object.oid),
-				       oid_to_hex(&commit->object.oid));
+				if (handle_ref_update(ref_action, transaction,
+						      decoration->name,
+						      &last_commit->object.oid,
+						      &commit->object.oid,
+						      &transaction_err) < 0) {
+					ret = error(_("failed to update ref '%s': %s"),
+						    decoration->name, transaction_err.buf);
+					goto cleanup;
+				}
 			}
 			decoration = decoration->next;
 		}
@@ -445,10 +507,23 @@ int cmd_replay(int argc,
 
 	/* In --advance mode, advance the target ref */
 	if (result.clean == 1 && advance_name) {
-		printf("update %s %s %s\n",
-		       advance_name,
-		       oid_to_hex(&last_commit->object.oid),
-		       oid_to_hex(&onto->object.oid));
+		if (handle_ref_update(ref_action, transaction, advance_name,
+				      &last_commit->object.oid,
+				      &onto->object.oid,
+				      &transaction_err) < 0) {
+			ret = error(_("failed to update ref '%s': %s"),
+				    advance_name, transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
+	/* Commit the ref transaction if we have one */
+	if (transaction && result.clean == 1) {
+		if (ref_transaction_commit(transaction, &transaction_err)) {
+			ret = error(_("failed to commit ref transaction: %s"),
+				    transaction_err.buf);
+			goto cleanup;
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
@@ -460,6 +535,9 @@ int cmd_replay(int argc,
 	ret = result.clean;
 
 cleanup:
+	if (transaction)
+		ref_transaction_free(transaction);
+	strbuf_release(&transaction_err);
 	release_revisions(&revs);
 	free(advance_name);
 
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 58b3759935..123734b49f 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git replay --onto main topic1..topic2 >result &&
+	git replay --ref-action=print --onto main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 '
 
 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
-	git -C bare replay --onto main topic1..topic2 >result-bare &&
+	git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
@@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
 	# 4th field of result is hash for main instead of hash for topic2
 
-	git replay --advance main topic1..topic2 >result &&
+	git replay --ref-action=print --advance main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 '
 
 test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
-	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
@@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 '
 
 test_expect_success 'using replay to also rebase a contained branch' '
-	git replay --contained --onto main main..topic3 >result &&
+	git replay --ref-action=print --contained --onto main main..topic3 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '
 '
 
 test_expect_success 'using replay on bare repo to also rebase a contained branch' '
-	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&
 	test_cmp expect result-bare
 '
 
 test_expect_success 'using replay to rebase multiple divergent branches' '
-	git replay --onto main ^topic1 topic2 topic4 >result &&
+	git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
 '
 
 test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
-	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+	git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
 
 	test_line_count = 4 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -217,4 +217,32 @@ test_expect_success 'merge.directoryRenames=false' '
 		--onto rename-onto rename-onto..rename-from
 '
 
+test_expect_success 'default atomic behavior updates refs directly' '
+	# Store original state for cleanup
+	test_when_finished "git branch -f topic2 topic1" &&
+
+	# Test default atomic behavior (no output, refs updated)
+	git replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'atomic behavior in bare repository' '
+	# Test atomic updates work in bare repo
+	git -C bare replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated in bare repo
+	git -C bare log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	# Reset for other tests
+	git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH v6 3/3] replay: add replay.refAction config option
  2025-10-30 19:19         ` [PATCH v6 0/3] replay: make atomic ref updates the default Siddharth Asthana
  2025-10-30 19:19           ` [PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
  2025-10-30 19:19           ` [PATCH v6 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
@ 2025-10-30 19:19           ` Siddharth Asthana
  2025-10-31  7:08             ` Christian Couder
  2025-10-31 18:49             ` Elijah Newren
  2025-10-31 18:51           ` [PATCH v6 0/3] replay: make atomic ref updates the default Elijah Newren
  2025-11-05 19:15           ` [PATCH v7 " Siddharth Asthana
  4 siblings, 2 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-10-30 19:19 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

Add a configuration option to control the default behavior of git replay
for updating references. This allows users who prefer the traditional
pipeline output to set it once in their config instead of passing
--ref-action=print with every command.

The config option uses string values that mirror the behavior modes:
  * replay.refAction = update (default): atomic ref updates
  * replay.refAction = print: output commands for pipeline

The command-line --ref-action option always overrides the config setting,
allowing users to temporarily change behavior for a single invocation.

Implementation details:

In cmd_replay(), after parsing command-line options, we check if
--ref-action was provided. If not, we read the configuration using
repo_config_get_string_tmp(). If the config variable is set, we validate
the value and use it to set the ref_action_str:

  Config value      Internal mode    Behavior
  ──────────────────────────────────────────────────────────────
  "update"          "update"         Atomic ref updates (default)
  "print"           "print"          Pipeline output
  (not set)         "update"         Atomic ref updates (default)
  (invalid)         error            Die with helpful message

If an invalid value is provided, we die() immediately with an error
message explaining the valid options. This catches configuration errors
early and provides clear guidance to users.

The command-line --ref-action option, when provided, overrides the
config value. This precedence allows users to set their preferred default
while still having per-invocation control:

  git config replay.refAction print         # Set default
  git replay --ref-action=update --onto main topic  # Override once

The config and command-line option use the same value names ('update'
and 'print') for consistency and clarity. This makes it immediately
obvious how the config maps to the command-line option, addressing
feedback about the relationship between configuration and command-line
options being clear to users.

Examples:

$ git config --global replay.refAction print
$ git replay --onto main topic1..topic2 | git update-ref --stdin

$ git replay --ref-action=update --onto main topic1..topic2

$ git config replay.refAction update
$ git replay --onto main topic1..topic2  # Updates refs directly

The implementation follows Git's standard configuration precedence:
command-line options override config values, which matches user
expectations across all Git commands.

Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/config/replay.adoc | 11 ++++++++
 builtin/replay.c                 | 39 ++++++++++++++++++--------
 t/t3650-replay-basics.sh         | 48 +++++++++++++++++++++++++++++++-
 3 files changed, 86 insertions(+), 12 deletions(-)
 create mode 100644 Documentation/config/replay.adoc

diff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc
new file mode 100644
index 0000000000..7d549d2f0e
--- /dev/null
+++ b/Documentation/config/replay.adoc
@@ -0,0 +1,11 @@
+replay.refAction::
+	Specifies the default mode for handling reference updates in
+	`git replay`. The value can be:
++
+--
+	* `update`: Update refs directly using an atomic transaction (default behavior).
+	* `print`: Output update-ref commands for pipeline use.
+--
++
+This setting can be overridden with the `--ref-action` command-line option.
+When not configured, `git replay` defaults to `update` mode.
diff --git a/builtin/replay.c b/builtin/replay.c
index 0564d4d2e7..810068f8ef 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -8,6 +8,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
+#include "config.h"
 #include "environment.h"
 #include "hex.h"
 #include "lockfile.h"
@@ -289,6 +290,31 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	return create_commit(repo, result->tree, pickme, replayed_base);
 }
 
+static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
+{
+	if (!ref_action || !strcmp(ref_action, "update"))
+		return REF_ACTION_UPDATE;
+	if (!strcmp(ref_action, "print"))
+		return REF_ACTION_PRINT;
+	die(_("invalid %s value: '%s'"), source, ref_action);
+}
+
+static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)
+{
+	const char *config_value = NULL;
+
+	/* Command line option takes precedence */
+	if (ref_action_str)
+		return parse_ref_action_mode(ref_action_str, "--ref-action");
+
+	/* Check config value */
+	if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
+		return parse_ref_action_mode(config_value, "replay.refAction");
+
+	/* Default to update mode */
+	return REF_ACTION_UPDATE;
+}
+
 static int handle_ref_update(enum ref_action_mode mode,
 			     struct ref_transaction *transaction,
 			     const char *refname,
@@ -367,17 +393,8 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
-	/* Default to update mode if not specified */
-	if (!ref_action_str)
-		ref_action_str = "update";
-
-	/* Parse ref action mode */
-	if (!strcmp(ref_action_str, "update"))
-		ref_action = REF_ACTION_UPDATE;
-	else if (!strcmp(ref_action_str, "print"))
-		ref_action = REF_ACTION_PRINT;
-	else
-		die(_("unknown --ref-action mode '%s'"), ref_action_str);
+	/* Parse ref action mode from command line or config */
+	ref_action = get_ref_action_mode(repo, ref_action_str);
 
 	advance_name = xstrdup_or_null(advance_name_opt);
 
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 123734b49f..2e90227c2f 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -219,7 +219,8 @@ test_expect_success 'merge.directoryRenames=false' '
 
 test_expect_success 'default atomic behavior updates refs directly' '
 	# Store original state for cleanup
-	test_when_finished "git branch -f topic2 topic1" &&
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
 
 	# Test default atomic behavior (no output, refs updated)
 	git replay --onto main topic1..topic2 >output &&
@@ -232,6 +233,10 @@ test_expect_success 'default atomic behavior updates refs directly' '
 '
 
 test_expect_success 'atomic behavior in bare repository' '
+	# Store original state for cleanup
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
+
 	# Test atomic updates work in bare repo
 	git -C bare replay --onto main topic1..topic2 >output &&
 	test_must_be_empty output &&
@@ -245,4 +250,45 @@ test_expect_success 'atomic behavior in bare repository' '
 	git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)
 '
 
+test_expect_success 'replay.refAction config option' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
+
+	# Set config to print
+	test_config replay.refAction print &&
+	git replay --onto main topic1..topic2 >output &&
+	test_line_count = 1 output &&
+	test_grep "^update refs/heads/topic2 " output &&
+
+	# Reset and test update mode
+	git branch -f topic2 $START &&
+	test_config replay.refAction update &&
+	git replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'command-line --ref-action overrides config' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
+
+	# Set config to update but use --ref-action=print
+	test_config replay.refAction update &&
+	git replay --ref-action=print --onto main topic1..topic2 >output &&
+	test_line_count = 1 output &&
+	test_grep "^update refs/heads/topic2 " output
+'
+
+test_expect_success 'invalid replay.refAction value' '
+	test_config replay.refAction invalid &&
+	test_must_fail git replay --onto main topic1..topic2 2>error &&
+	test_grep "invalid.*replay.refAction.*value" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 3/3] replay: add replay.refAction config option
  2025-10-30 19:19           ` [PATCH v6 3/3] replay: add replay.refAction config option Siddharth Asthana
@ 2025-10-31  7:08             ` Christian Couder
  2025-11-05 19:03               ` Siddharth Asthana
  2025-10-31 18:49             ` Elijah Newren
  1 sibling, 1 reply; 129+ messages in thread
From: Christian Couder @ 2025-10-31  7:08 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, phillip.wood123, phillip.wood, newren, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Thu, Oct 30, 2025 at 8:20 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:

> +static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
> +{
> +       if (!ref_action || !strcmp(ref_action, "update"))
> +               return REF_ACTION_UPDATE;
> +       if (!strcmp(ref_action, "print"))
> +               return REF_ACTION_PRINT;
> +       die(_("invalid %s value: '%s'"), source, ref_action);
> +}
> +
> +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)

I think it could be "ref_action" (instead of "ref_action_str" ) in
this function too.

> +{
> +       const char *config_value = NULL;
> +
> +       /* Command line option takes precedence */
> +       if (ref_action_str)
> +               return parse_ref_action_mode(ref_action_str, "--ref-action");
> +
> +       /* Check config value */
> +       if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
> +               return parse_ref_action_mode(config_value, "replay.refAction");
> +
> +       /* Default to update mode */
> +       return REF_ACTION_UPDATE;
> +}
> +
>  static int handle_ref_update(enum ref_action_mode mode,
>                              struct ref_transaction *transaction,
>                              const char *refname,
> @@ -367,17 +393,8 @@ int cmd_replay(int argc,
>         die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>                                   contained, "--contained");
>
> -       /* Default to update mode if not specified */
> -       if (!ref_action_str)
> -               ref_action_str = "update";
> -
> -       /* Parse ref action mode */
> -       if (!strcmp(ref_action_str, "update"))
> -               ref_action = REF_ACTION_UPDATE;
> -       else if (!strcmp(ref_action_str, "print"))
> -               ref_action = REF_ACTION_PRINT;
> -       else
> -               die(_("unknown --ref-action mode '%s'"), ref_action_str);

Maybe parse_ref_action_mode() could have been introduced in the
previous commit already?

> +       /* Parse ref action mode from command line or config */
> +       ref_action = get_ref_action_mode(repo, ref_action_str);

Here it could be:

      ref_mode = get_ref_action_mode(repo, ref_action);

Thanks!

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation
  2025-10-30 19:19           ` [PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
@ 2025-10-31 18:47             ` Elijah Newren
  2025-11-05 18:39               ` Siddharth Asthana
  0 siblings, 1 reply; 129+ messages in thread
From: Elijah Newren @ 2025-10-31 18:47 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Thu, Oct 30, 2025 at 12:19 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> In preparation for adding the --ref-action option, convert option
> validation to use die_for_incompatible_opt2(). This helper provides
> standardized error messages for mutually exclusive options.
>
> The following commit introduces --ref-action which will be incompatible
> with certain other options. Using die_for_incompatible_opt2() now means
> that commit can cleanly add its validation using the same pattern,
> keeping the validation logic consistent and maintainable.
>
> This also aligns git-replay's option handling with how other Git commands
> manage option conflicts, using the established die_for_incompatible_opt*()
> helper family.
>
> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> ---
>  builtin/replay.c | 6 +++---
>  1 file changed, 3 insertions(+), 3 deletions(-)
>
> diff --git a/builtin/replay.c b/builtin/replay.c
> index 6172c8aacc..b64fc72063 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -330,9 +330,9 @@ int cmd_replay(int argc,
>                 usage_with_options(replay_usage, replay_options);
>         }
>
> -       if (advance_name_opt && contained)
> -               die(_("options '%s' and '%s' cannot be used together"),
> -                   "--advance", "--contained");
> +       die_for_incompatible_opt2(!!advance_name_opt, "--advance",
> +                                 contained, "--contained");
> +
>         advance_name = xstrdup_or_null(advance_name_opt);
>
>         repo_init_revisions(repo, &revs, prefix);
> --
> 2.51.0

Thanks for splitting this one out; looks good.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior
  2025-10-30 19:19           ` [PATCH v6 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
@ 2025-10-31 18:49             ` Elijah Newren
  2025-10-31 19:59               ` Junio C Hamano
  2025-11-05 19:07               ` Siddharth Asthana
  2025-11-03 16:25             ` Phillip Wood
  1 sibling, 2 replies; 129+ messages in thread
From: Elijah Newren @ 2025-10-31 18:49 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Thu, Oct 30, 2025 at 12:20 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> The git replay command currently outputs update commands that can be
> piped to update-ref to achieve a rebase, e.g.
>
>   git replay --onto main topic1..topic2 | git update-ref --stdin
>
> This separation had advantages for three special cases:
>   * it made testing easy (when state isn't modified from one step to
>     the next, you don't need to make temporary branches or have undo
>     commands, or try to track the changes)
>   * it provided a natural can-it-rebase-cleanly (and what would it
>     rebase to) capability without automatically updating refs, similar
>     to a --dry-run
>   * it provided a natural low-level tool for the suite of hash-object,
>     mktree, commit-tree, mktag, merge-tree, and update-ref, allowing
>     users to have another building block for experimentation and making
>     new tools
>
> However, it should be noted that all three of these are somewhat
> special cases; users, whether on the client or server side, would
> almost certainly find it more ergonomic to simply have the updating
> of refs be the default.
>
> For server-side operations in particular, the pipeline architecture
> creates process coordination overhead. Server implementations that need
> to perform rebases atomically must maintain additional code to:
>
>   1. Spawn and manage a pipeline between git-replay and git-update-ref
>   2. Coordinate stdout/stderr streams across the pipe boundary
>   3. Handle partial failure states if the pipeline breaks mid-execution
>   4. Parse and validate the update-ref command output
>
> Change the default behavior to update refs directly, and atomically (at
> least to the extent supported by the refs backend in use). This
> eliminates the process coordination overhead for the common case.
>
> For users needing the traditional pipeline workflow, add a new
> --ref-action=<mode> option that preserves the original behavior:
>
>   git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin
>
> The mode can be:
>   * update (default): Update refs directly using an atomic transaction
>   * print: Output update-ref commands for pipeline use

Looks good up to here.

> Implementation details:
>
> The atomic ref updates are implemented using Git's ref transaction API.
> In cmd_replay(), when not in `print` mode, we initialize a transaction
> using ref_store_transaction_begin() with the default atomic behavior.
> As commits are replayed, ref updates are staged into the transaction
> using ref_transaction_update(). Finally, ref_transaction_commit()
> applies all updates atomically—either all updates succeed or none do.
>
> To avoid code duplication between the 'print' and 'update' modes, this
> commit extracts a handle_ref_update() helper function. This function
> takes the mode (as an enum) and either prints the update command or
> stages it into the transaction. Using an enum rather than passing the
> string around provides type safety and allows the compiler to catch
> typos. The switch statement makes it easy to add future modes.
>
> The helper function signature:
>
>   static int handle_ref_update(enum ref_action_mode mode,
>                                 struct ref_transaction *transaction,
>                                 const char *refname,
>                                 const struct object_id *new_oid,
>                                 const struct object_id *old_oid,
>                                 struct strbuf *err)
>
> The enum is defined as:
>
>   enum ref_action_mode {
>       REF_ACTION_UPDATE,
>       REF_ACTION_PRINT
>   };
>
> The mode string is converted to enum immediately after parse_options()
> to avoid string comparisons throughout the codebase and provide compiler
> protection against typos.

I'm not sure the implementation details section above makes sense to
include in the commit message; it feels like it's not providing much
high level information nor much "why" information, but just presenting
an alternative view of the information people will find in the patch.
Perhaps leave it out?

> Test suite changes:
>
> All existing tests that expected command output now use
> --ref-action=print to preserve their original behavior. This keeps
> the tests valid while allowing them to verify that the pipeline workflow
> still works correctly.
>
> New tests were added to verify:
>   - Default atomic behavior (no output, refs updated directly)
>   - Bare repository support (server-side use case)
>   - Equivalence between traditional pipeline and atomic updates
>   - Real atomicity using a lock file to verify all-or-nothing guarantee
>   - Test isolation using test_when_finished to clean up state
>
> The bare repository tests were fixed to rebuild their expectations
> independently rather than comparing to previous test output, improving
> test reliability and isolation.

The above paragraph sounds like you are comparing to an earlier
series, which will confuse future readers who only compare to code
that existed before your patches.

> A following commit will add a replay.refAction configuration
> option for users who prefer the traditional pipeline output as their
> default behavior.
>
> Helped-by: Elijah Newren <newren@gmail.com>
> Helped-by: Patrick Steinhardt <ps@pks.im>
> Helped-by: Christian Couder <christian.couder@gmail.com>
> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> ---
>  Documentation/git-replay.adoc | 65 +++++++++++++++--------
>  builtin/replay.c              | 98 +++++++++++++++++++++++++++++++----
>  t/t3650-replay-basics.sh      | 44 +++++++++++++---
>  3 files changed, 167 insertions(+), 40 deletions(-)
>
> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
> index 0b12bf8aa4..037b093196 100644
> --- a/Documentation/git-replay.adoc
> +++ b/Documentation/git-replay.adoc
> @@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
>  SYNOPSIS
>  --------
>  [verse]
> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...
>
>  DESCRIPTION
>  -----------
>
>  Takes ranges of commits and replays them onto a new location. Leaves
> -the working tree and the index untouched, and updates no references.
> -The output of this command is meant to be used as input to
> -`git update-ref --stdin`, which would update the relevant branches
> +the working tree and the index untouched. By default, updates the
> +relevant references using an atomic transaction (all refs update or
> +none). Use `--ref-action=print` to avoid automatic ref updates and
> +instead get update commands that can be piped to `git update-ref --stdin`
>  (see the OUTPUT section below).
>
>  THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> @@ -29,18 +30,31 @@ OPTIONS
>         Starting point at which to create the new commits.  May be any
>         valid commit, and not just an existing branch name.
>  +
> -When `--onto` is specified, the update-ref command(s) in the output will
> -update the branch(es) in the revision range to point at the new
> -commits, similar to the way how `git rebase --update-refs` updates
> -multiple branches in the affected range.
> +When `--onto` is specified, the branch(es) in the revision range will be
> +updated to point at the new commits (or update commands will be printed
> +if `--ref-action=print` is used), similar to the way `git rebase --update-refs`
> +updates multiple branches in the affected range.

I'm not sure if the parenthetical comment is necessary; we tend not to
try to document every combinatorial combination with every sentence.
For example, in the `git rebase` manpage under the description of the
`--strategy` flag, it says "Because git rebase replays each commit
from the working branch on top of the <upstream> branch using the
given strategy", which is technically incorrect if either the --onto
or --keep-base flags are specified, but belaboring all the details at
that location would just burden the reader and the explanations of
--onto and --keep-base are sufficient for users to understand.  I
think we tend to just describe the option in combination with the
default, and only mention other options if the combination is
ambiguous or confusing.  I don't think users would find anything
ambiguous or confusing about how --ref-action=print would combine with
these options, so I don't think it's necessary to make the description
longer.

>  --advance <branch>::
>         Starting point at which to create the new commits; must be a
>         branch name.
>  +
> -When `--advance` is specified, the update-ref command(s) in the output
> -will update the branch passed as an argument to `--advance` to point at
> -the new commits (in other words, this mimics a cherry-pick operation).
> +The history is replayed on top of the <branch> and <branch> is updated to
> +point at the tip of the resulting history (or an update command will be
> +printed if `--ref-action=print` is used). This is different from `--onto`,
> +which uses the target only as a starting point without updating it.

Same comment as above about this parenthetical comment as well.

> +
> +--ref-action[=<mode>]::
> +       Control how references are updated. The mode can be:
> ++
> +--
> +       * `update` (default): Update refs directly using an atomic transaction.
> +         All refs are updated or none are (all-or-nothing behavior).
> +       * `print`: Output update-ref commands for pipeline use. This is the
> +         traditional behavior where output can be piped to `git update-ref --stdin`.
> +--
> ++
> +The default mode can be configured via the `replay.refAction` configuration variable.

This last sentence conflicts with the commit message; if the
configuration option isn't added until a later commit, then this last
sentence shouldn't be added until then either.

>  <revision-range>::
>         Range of commits to replay. More than one <revision-range> can
> @@ -54,8 +68,11 @@ include::rev-list-options.adoc[]
>  OUTPUT
>  ------
>
> -When there are no conflicts, the output of this command is usable as
> -input to `git update-ref --stdin`.  It is of the form:
> +By default, or with `--ref-action=update`, this command produces no output on
> +success, as refs are updated directly using an atomic transaction.
> +
> +When using `--ref-action=print`, the output is usable as input to
> +`git update-ref --stdin`. It is of the form:
>
>         update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>         update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> @@ -81,6 +98,14 @@ To simply rebase `mybranch` onto `target`:
>
>  ------------
>  $ git replay --onto target origin/main..mybranch
> +------------
> +
> +The refs are updated atomically and no output is produced on success.
> +
> +To see what would be updated without actually updating:
> +
> +------------
> +$ git replay --ref-action=print --onto target origin/main..mybranch
>  update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
>  ------------
>
> @@ -88,33 +113,29 @@ To cherry-pick the commits from mybranch onto target:
>
>  ------------
>  $ git replay --advance target origin/main..mybranch
> -update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
>  ------------
>
>  Note that the first two examples replay the exact same commits and on
>  top of the exact same new base, they only differ in that the first
> -provides instructions to make mybranch point at the new commits and
> -the second provides instructions to make target point at them.
> +updates mybranch to point at the new commits and the second updates
> +target to point at them.
>
>  What if you have a stack of branches, one depending upon another, and
>  you'd really like to rebase the whole set?
>
>  ------------
>  $ git replay --contained --onto origin/main origin/main..tipbranch
> -update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
> -update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> -update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
>  ------------
>
> +All three branches (`branch1`, `branch2`, and `tipbranch`) are updated
> +atomically.
> +
>  When calling `git replay`, one does not need to specify a range of
>  commits to replay using the syntax `A..B`; any range expression will
>  do:
>
>  ------------
>  $ git replay --onto origin/main ^base branch1 branch2 branch3
> -update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
> -update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
> -update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
>  ------------
>
>  This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
> diff --git a/builtin/replay.c b/builtin/replay.c
> index b64fc72063..0564d4d2e7 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -20,6 +20,11 @@
>  #include <oidset.h>
>  #include <tree.h>
>
> +enum ref_action_mode {
> +       REF_ACTION_UPDATE,
> +       REF_ACTION_PRINT,
> +};
> +
>  static const char *short_commit_name(struct repository *repo,
>                                      struct commit *commit)
>  {
> @@ -284,6 +289,28 @@ static struct commit *pick_regular_commit(struct repository *repo,
>         return create_commit(repo, result->tree, pickme, replayed_base);
>  }
>
> +static int handle_ref_update(enum ref_action_mode mode,
> +                            struct ref_transaction *transaction,
> +                            const char *refname,
> +                            const struct object_id *new_oid,
> +                            const struct object_id *old_oid,
> +                            struct strbuf *err)
> +{
> +       switch (mode) {
> +       case REF_ACTION_PRINT:
> +               printf("update %s %s %s\n",
> +                      refname,
> +                      oid_to_hex(new_oid),
> +                      oid_to_hex(old_oid));
> +               return 0;
> +       case REF_ACTION_UPDATE:
> +               return ref_transaction_update(transaction, refname, new_oid, old_oid,
> +                                             NULL, NULL, 0, "git replay", err);
> +       default:
> +               BUG("unknown ref_action_mode %d", mode);
> +       }
> +}
> +
>  int cmd_replay(int argc,
>                const char **argv,
>                const char *prefix,
> @@ -294,6 +321,8 @@ int cmd_replay(int argc,
>         struct commit *onto = NULL;
>         const char *onto_name = NULL;
>         int contained = 0;
> +       const char *ref_action_str = NULL;
> +       enum ref_action_mode ref_action = REF_ACTION_UPDATE;
>
>         struct rev_info revs;
>         struct commit *last_commit = NULL;
> @@ -302,12 +331,14 @@ int cmd_replay(int argc,
>         struct merge_result result;
>         struct strset *update_refs = NULL;
>         kh_oid_map_t *replayed_commits;
> +       struct ref_transaction *transaction = NULL;
> +       struct strbuf transaction_err = STRBUF_INIT;
>         int ret = 0;
>
> -       const char * const replay_usage[] = {
> +       const char *const replay_usage[] = {
>                 N_("(EXPERIMENTAL!) git replay "
>                    "([--contained] --onto <newbase> | --advance <branch>) "
> -                  "<revision-range>..."),
> +                  "[--ref-action[=<mode>]] <revision-range>..."),
>                 NULL
>         };
>         struct option replay_options[] = {
> @@ -319,6 +350,9 @@ int cmd_replay(int argc,
>                            N_("replay onto given commit")),
>                 OPT_BOOL(0, "contained", &contained,
>                          N_("advance all branches contained in revision-range")),
> +               OPT_STRING(0, "ref-action", &ref_action_str,
> +                          N_("mode"),
> +                          N_("control ref update behavior (update|print)")),
>                 OPT_END()
>         };
>
> @@ -333,6 +367,18 @@ int cmd_replay(int argc,
>         die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>                                   contained, "--contained");
>
> +       /* Default to update mode if not specified */
> +       if (!ref_action_str)
> +               ref_action_str = "update";
> +
> +       /* Parse ref action mode */
> +       if (!strcmp(ref_action_str, "update"))
> +               ref_action = REF_ACTION_UPDATE;
> +       else if (!strcmp(ref_action_str, "print"))
> +               ref_action = REF_ACTION_PRINT;
> +       else
> +               die(_("unknown --ref-action mode '%s'"), ref_action_str);
> +
>         advance_name = xstrdup_or_null(advance_name_opt);
>
>         repo_init_revisions(repo, &revs, prefix);
> @@ -389,6 +435,17 @@ int cmd_replay(int argc,
>         determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
>                               &onto, &update_refs);
>
> +       /* Initialize ref transaction if using update mode */
> +       if (ref_action == REF_ACTION_UPDATE) {
> +               transaction = ref_store_transaction_begin(get_main_ref_store(repo),
> +                                                         0, &transaction_err);
> +               if (!transaction) {
> +                       ret = error(_("failed to begin ref transaction: %s"),
> +                                   transaction_err.buf);
> +                       goto cleanup;
> +               }
> +       }
> +
>         if (!onto) /* FIXME: Should handle replaying down to root commit */
>                 die("Replaying down to root commit is not supported yet!");
>
> @@ -434,10 +491,15 @@ int cmd_replay(int argc,
>                         if (decoration->type == DECORATION_REF_LOCAL &&
>                             (contained || strset_contains(update_refs,
>                                                           decoration->name))) {
> -                               printf("update %s %s %s\n",
> -                                      decoration->name,
> -                                      oid_to_hex(&last_commit->object.oid),
> -                                      oid_to_hex(&commit->object.oid));
> +                               if (handle_ref_update(ref_action, transaction,
> +                                                     decoration->name,
> +                                                     &last_commit->object.oid,
> +                                                     &commit->object.oid,
> +                                                     &transaction_err) < 0) {
> +                                       ret = error(_("failed to update ref '%s': %s"),
> +                                                   decoration->name, transaction_err.buf);
> +                                       goto cleanup;
> +                               }
>                         }
>                         decoration = decoration->next;
>                 }
> @@ -445,10 +507,23 @@ int cmd_replay(int argc,
>
>         /* In --advance mode, advance the target ref */
>         if (result.clean == 1 && advance_name) {
> -               printf("update %s %s %s\n",
> -                      advance_name,
> -                      oid_to_hex(&last_commit->object.oid),
> -                      oid_to_hex(&onto->object.oid));
> +               if (handle_ref_update(ref_action, transaction, advance_name,
> +                                     &last_commit->object.oid,
> +                                     &onto->object.oid,
> +                                     &transaction_err) < 0) {
> +                       ret = error(_("failed to update ref '%s': %s"),
> +                                   advance_name, transaction_err.buf);
> +                       goto cleanup;
> +               }
> +       }
> +
> +       /* Commit the ref transaction if we have one */
> +       if (transaction && result.clean == 1) {
> +               if (ref_transaction_commit(transaction, &transaction_err)) {
> +                       ret = error(_("failed to commit ref transaction: %s"),
> +                                   transaction_err.buf);
> +                       goto cleanup;
> +               }
>         }
>
>         merge_finalize(&merge_opt, &result);
> @@ -460,6 +535,9 @@ int cmd_replay(int argc,
>         ret = result.clean;
>
>  cleanup:
> +       if (transaction)
> +               ref_transaction_free(transaction);
> +       strbuf_release(&transaction_err);
>         release_revisions(&revs);
>         free(advance_name);
>
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index 58b3759935..123734b49f 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
>  '
>
>  test_expect_success 'using replay to rebase two branches, one on top of other' '
> -       git replay --onto main topic1..topic2 >result &&
> +       git replay --ref-action=print --onto main topic1..topic2 >result &&
>
>         test_line_count = 1 result &&
>
> @@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
>  '
>
>  test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
> -       git -C bare replay --onto main topic1..topic2 >result-bare &&
> +       git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&
>         test_cmp expect result-bare
>  '
>
> @@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
>         # 2nd field of result is refs/heads/main vs. refs/heads/topic2
>         # 4th field of result is hash for main instead of hash for topic2
>
> -       git replay --advance main topic1..topic2 >result &&
> +       git replay --ref-action=print --advance main topic1..topic2 >result &&
>
>         test_line_count = 1 result &&
>
> @@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
>  '
>
>  test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
> -       git -C bare replay --advance main topic1..topic2 >result-bare &&
> +       git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&
>         test_cmp expect result-bare
>  '
>
> @@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
>  '
>
>  test_expect_success 'using replay to also rebase a contained branch' '
> -       git replay --contained --onto main main..topic3 >result &&
> +       git replay --ref-action=print --contained --onto main main..topic3 >result &&
>
>         test_line_count = 2 result &&
>         cut -f 3 -d " " result >new-branch-tips &&
> @@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '
>  '
>
>  test_expect_success 'using replay on bare repo to also rebase a contained branch' '
> -       git -C bare replay --contained --onto main main..topic3 >result-bare &&
> +       git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&
>         test_cmp expect result-bare
>  '
>
>  test_expect_success 'using replay to rebase multiple divergent branches' '
> -       git replay --onto main ^topic1 topic2 topic4 >result &&
> +       git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&
>
>         test_line_count = 2 result &&
>         cut -f 3 -d " " result >new-branch-tips &&
> @@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
>  '
>
>  test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
> -       git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
> +       git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
>
>         test_line_count = 4 result &&
>         cut -f 3 -d " " result >new-branch-tips &&
> @@ -217,4 +217,32 @@ test_expect_success 'merge.directoryRenames=false' '
>                 --onto rename-onto rename-onto..rename-from
>  '
>
> +test_expect_success 'default atomic behavior updates refs directly' '
> +       # Store original state for cleanup
> +       test_when_finished "git branch -f topic2 topic1" &&

Why are you resetting topic2 back to topic1?  Shouldn't it be set back
to what it was before the test ran instead, e.g.
    START=$(git rev-parse topic2) &&
    test_when_finished "git branch -f topic2 $START" &&
?

> +
> +       # Test default atomic behavior (no output, refs updated)
> +       git replay --onto main topic1..topic2 >output &&
> +       test_must_be_empty output &&
> +
> +       # Verify ref was updated
> +       git log --format=%s topic2 >actual &&
> +       test_write_lines E D M L B A >expect &&
> +       test_cmp expect actual
> +'
> +
> +test_expect_success 'atomic behavior in bare repository' '
> +       # Test atomic updates work in bare repo
> +       git -C bare replay --onto main topic1..topic2 >output &&
> +       test_must_be_empty output &&
> +
> +       # Verify ref was updated in bare repo
> +       git -C bare log --format=%s topic2 >actual &&
> +       test_write_lines E D M L B A >expect &&
> +       test_cmp expect actual &&
> +
> +       # Reset for other tests
> +       git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)

This reset happens too late to help if the earlier commands fail, and
also resets to the wrong ref.  You should instead use a
test_when_finished block, and make sure to reset to what topic2 used
to point to, not reset it to what topic1 points to.


Otherwise, the patch looks good.  This is really close to being ready
to merge; just a few minor fixups needed that I highlighted above.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 3/3] replay: add replay.refAction config option
  2025-10-30 19:19           ` [PATCH v6 3/3] replay: add replay.refAction config option Siddharth Asthana
  2025-10-31  7:08             ` Christian Couder
@ 2025-10-31 18:49             ` Elijah Newren
  2025-11-05 19:10               ` Siddharth Asthana
  1 sibling, 1 reply; 129+ messages in thread
From: Elijah Newren @ 2025-10-31 18:49 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Thu, Oct 30, 2025 at 12:20 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> Add a configuration option to control the default behavior of git replay
> for updating references. This allows users who prefer the traditional
> pipeline output to set it once in their config instead of passing
> --ref-action=print with every command.
>
> The config option uses string values that mirror the behavior modes:
>   * replay.refAction = update (default): atomic ref updates
>   * replay.refAction = print: output commands for pipeline
>
> The command-line --ref-action option always overrides the config setting,
> allowing users to temporarily change behavior for a single invocation.

The above paragraph merely states that we follow git practices with
this config options and its corresponding command line; I think we'd
need to call it out if we didn't do that, but calling out that we do
follow git conventions seems unnecessary.

> Implementation details:
>
> In cmd_replay(), after parsing command-line options, we check if
> --ref-action was provided. If not, we read the configuration using
> repo_config_get_string_tmp(). If the config variable is set, we validate
> the value and use it to set the ref_action_str:
>
>   Config value      Internal mode    Behavior
>   ──────────────────────────────────────────────────────────────
>   "update"          "update"         Atomic ref updates (default)
>   "print"           "print"          Pipeline output
>   (not set)         "update"         Atomic ref updates (default)
>   (invalid)         error            Die with helpful message
>
> If an invalid value is provided, we die() immediately with an error
> message explaining the valid options. This catches configuration errors
> early and provides clear guidance to users.
>
> The command-line --ref-action option, when provided, overrides the
> config value. This precedence allows users to set their preferred default
> while still having per-invocation control:
>
>   git config replay.refAction print         # Set default
>   git replay --ref-action=update --onto main topic  # Override once
>
> The config and command-line option use the same value names ('update'
> and 'print') for consistency and clarity. This makes it immediately
> obvious how the config maps to the command-line option, addressing
> feedback about the relationship between configuration and command-line
> options being clear to users.

An implementation details section may make sense if it answers a
"why?" question, or it explains something counter-intuitive, or it
provides high enough level details that it makes the patch easier to
read/follow, or it otherwise does something more than just repackage
the patch in an alternate format.  I appreciate the attempt to provide
these, but I think they simply make the commit message longer without
adding value.

> Examples:
>
> $ git config --global replay.refAction print
> $ git replay --onto main topic1..topic2 | git update-ref --stdin
>
> $ git replay --ref-action=update --onto main topic1..topic2
>
> $ git config replay.refAction update
> $ git replay --onto main topic1..topic2  # Updates refs directly
>
> The implementation follows Git's standard configuration precedence:
> command-line options override config values, which matches user
> expectations across all Git commands.

I don't find the Examples section helpful either; it's yet another
re-iteration that we're following conventions.

> Helped-by: Junio C Hamano <gitster@pobox.com>
> Helped-by: Elijah Newren <newren@gmail.com>
> Helped-by: Christian Couder <christian.couder@gmail.com>
> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> ---
>  Documentation/config/replay.adoc | 11 ++++++++
>  builtin/replay.c                 | 39 ++++++++++++++++++--------
>  t/t3650-replay-basics.sh         | 48 +++++++++++++++++++++++++++++++-
>  3 files changed, 86 insertions(+), 12 deletions(-)
>  create mode 100644 Documentation/config/replay.adoc
>
> diff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc
> new file mode 100644
> index 0000000000..7d549d2f0e
> --- /dev/null
> +++ b/Documentation/config/replay.adoc
> @@ -0,0 +1,11 @@
> +replay.refAction::
> +       Specifies the default mode for handling reference updates in
> +       `git replay`. The value can be:
> ++
> +--
> +       * `update`: Update refs directly using an atomic transaction (default behavior).
> +       * `print`: Output update-ref commands for pipeline use.
> +--
> ++
> +This setting can be overridden with the `--ref-action` command-line option.
> +When not configured, `git replay` defaults to `update` mode.
> diff --git a/builtin/replay.c b/builtin/replay.c
> index 0564d4d2e7..810068f8ef 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -8,6 +8,7 @@
>  #include "git-compat-util.h"
>
>  #include "builtin.h"
> +#include "config.h"
>  #include "environment.h"
>  #include "hex.h"
>  #include "lockfile.h"
> @@ -289,6 +290,31 @@ static struct commit *pick_regular_commit(struct repository *repo,
>         return create_commit(repo, result->tree, pickme, replayed_base);
>  }
>
> +static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
> +{
> +       if (!ref_action || !strcmp(ref_action, "update"))
> +               return REF_ACTION_UPDATE;
> +       if (!strcmp(ref_action, "print"))
> +               return REF_ACTION_PRINT;
> +       die(_("invalid %s value: '%s'"), source, ref_action);
> +}
> +
> +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)
> +{
> +       const char *config_value = NULL;
> +
> +       /* Command line option takes precedence */
> +       if (ref_action_str)
> +               return parse_ref_action_mode(ref_action_str, "--ref-action");
> +
> +       /* Check config value */
> +       if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
> +               return parse_ref_action_mode(config_value, "replay.refAction");
> +
> +       /* Default to update mode */
> +       return REF_ACTION_UPDATE;
> +}
> +
>  static int handle_ref_update(enum ref_action_mode mode,
>                              struct ref_transaction *transaction,
>                              const char *refname,
> @@ -367,17 +393,8 @@ int cmd_replay(int argc,
>         die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>                                   contained, "--contained");
>
> -       /* Default to update mode if not specified */
> -       if (!ref_action_str)
> -               ref_action_str = "update";
> -
> -       /* Parse ref action mode */
> -       if (!strcmp(ref_action_str, "update"))
> -               ref_action = REF_ACTION_UPDATE;
> -       else if (!strcmp(ref_action_str, "print"))
> -               ref_action = REF_ACTION_PRINT;
> -       else
> -               die(_("unknown --ref-action mode '%s'"), ref_action_str);
> +       /* Parse ref action mode from command line or config */
> +       ref_action = get_ref_action_mode(repo, ref_action_str);
>
>         advance_name = xstrdup_or_null(advance_name_opt);
>
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index 123734b49f..2e90227c2f 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -219,7 +219,8 @@ test_expect_success 'merge.directoryRenames=false' '
>
>  test_expect_success 'default atomic behavior updates refs directly' '
>         # Store original state for cleanup
> -       test_when_finished "git branch -f topic2 topic1" &&
> +       START=$(git rev-parse topic2) &&
> +       test_when_finished "git branch -f topic2 $START" &&

Yes, these three lines are a good fix, but they belong in the previous patch.

>
>         # Test default atomic behavior (no output, refs updated)
>         git replay --onto main topic1..topic2 >output &&
> @@ -232,6 +233,10 @@ test_expect_success 'default atomic behavior updates refs directly' '
>  '
>
>  test_expect_success 'atomic behavior in bare repository' '
> +       # Store original state for cleanup
> +       START=$(git rev-parse topic2) &&
> +       test_when_finished "git branch -f topic2 $START" &&

Yes, these three lines are good but they belong in a separate patch.
> +
>         # Test atomic updates work in bare repo
>         git -C bare replay --onto main topic1..topic2 >output &&
>         test_must_be_empty output &&
> @@ -245,4 +250,45 @@ test_expect_success 'atomic behavior in bare repository' '
>         git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)

And this line should be removed in the previous patch.

>  '
>
> +test_expect_success 'replay.refAction config option' '
> +       # Store original state
> +       START=$(git rev-parse topic2) &&
> +       test_when_finished "git branch -f topic2 $START" &&
> +
> +       # Set config to print
> +       test_config replay.refAction print &&
> +       git replay --onto main topic1..topic2 >output &&
> +       test_line_count = 1 output &&
> +       test_grep "^update refs/heads/topic2 " output &&
> +
> +       # Reset and test update mode
> +       git branch -f topic2 $START &&
> +       test_config replay.refAction update &&
> +       git replay --onto main topic1..topic2 >output &&
> +       test_must_be_empty output &&
> +
> +       # Verify ref was updated
> +       git log --format=%s topic2 >actual &&
> +       test_write_lines E D M L B A >expect &&
> +       test_cmp expect actual
> +'
> +
> +test_expect_success 'command-line --ref-action overrides config' '
> +       # Store original state
> +       START=$(git rev-parse topic2) &&
> +       test_when_finished "git branch -f topic2 $START" &&
> +
> +       # Set config to update but use --ref-action=print
> +       test_config replay.refAction update &&
> +       git replay --ref-action=print --onto main topic1..topic2 >output &&
> +       test_line_count = 1 output &&
> +       test_grep "^update refs/heads/topic2 " output
> +'
> +
> +test_expect_success 'invalid replay.refAction value' '
> +       test_config replay.refAction invalid &&
> +       test_must_fail git replay --onto main topic1..topic2 2>error &&
> +       test_grep "invalid.*replay.refAction.*value" error
> +'
> +
>  test_done
> --
> 2.51.0

Looks good otherwise.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 0/3] replay: make atomic ref updates the default
  2025-10-30 19:19         ` [PATCH v6 0/3] replay: make atomic ref updates the default Siddharth Asthana
                             ` (2 preceding siblings ...)
  2025-10-30 19:19           ` [PATCH v6 3/3] replay: add replay.refAction config option Siddharth Asthana
@ 2025-10-31 18:51           ` Elijah Newren
  2025-11-05 19:15           ` [PATCH v7 " Siddharth Asthana
  4 siblings, 0 replies; 129+ messages in thread
From: Elijah Newren @ 2025-10-31 18:51 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Thu, Oct 30, 2025 at 12:19 PM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> This is v6 of the git-replay atomic updates series.
>
> This version addresses Christian's feedback from v5 regarding code
> consistency and test patterns. Thanks to Christian, Junio, Phillip,
> Elijah, Patrick, and Karthik for the thorough reviews.
>
> ## Changes in v6
>
> **Fixed parameter naming inconsistency**
>
> Christian pointed out that parse_ref_action_mode() used `mode_str` as the
> parameter name while the rest of the code used `ref_action`. Changed to
> use `ref_action` consistently throughout for better code readability.
>
> **Improved test cleanup pattern**
>
> Replaced manual `git config --unset` with `test_when_finished` pattern with
> `test_config` helper in the replay.refAction config test. The test_config
> helper automatically handles cleanup via test_when_finished, providing
> better test isolation and following Git test suite best practices.
>
> These are code quality improvements that don't change functionality but
> make the code more consistent with Git's established patterns.
>
> ## Technical Implementation
>
> Same as v5, using Git's ref transaction API:
>
> - ref_store_transaction_begin() with default atomic behavior
> - ref_transaction_update() to stage each update
> - ref_transaction_commit() for atomic application
>
> The helper functions provide clean separation:
>
> - parse_ref_action_mode(): Validates strings and converts to enum
> - get_ref_action_mode(): Implements command-line > config > default precedence
> - handle_ref_update(): Uses type-safe enum with switch statement
>
> Config reading uses repo_config_get_string_tmp() for simplicity while
> maintaining proper precedence behavior.
>
> ## Testing
>
> All tests pass:
>
> - t3650-replay-basics.sh (20 tests pass)
> - Config tests now use test_config for automatic cleanup
> - Atomic behavior tests verify direct ref updates
> - Backward compatibility maintained for pipeline workflow
>
> CI results: https://gitlab.com/gitlab-org/git/-/pipelines/2130504045
>
> Siddharth Asthana (3):
>   replay: use die_for_incompatible_opt2() for option validation
>   replay: make atomic ref updates the default behavior
>   replay: add replay.refAction config option
>
>  Documentation/config/replay.adoc |  11 +++
>  Documentation/git-replay.adoc    |  65 +++++++++++------
>  builtin/replay.c                 | 121 +++++++++++++++++++++++++++----
>  t/t3650-replay-basics.sh         |  90 +++++++++++++++++++++--
>  4 files changed, 244 insertions(+), 43 deletions(-)
>  create mode 100644 Documentation/config/replay.adoc
>
> Range-diff against v5:
> 1:  3e27d07d3b = 1:  1f0fad0cac replay: use die_for_incompatible_opt2() for option validation
> 2:  643d9ca86a = 2:  bfc6188234 replay: make atomic ref updates the default behavior
> 3:  334da71911 ! 3:  6b2a44c72c replay: add replay.refAction config option
>     @@ Metadata
>      Author: Siddharth Asthana <siddharthasthana31@gmail.com>
>
>       ## Commit message ##
>          replay: add replay.refAction config option
>
>          [Commit message unchanged]
>
>       ## builtin/replay.c ##
>      @@ builtin/replay.c: static struct commit *pick_regular_commit
>         return create_commit(repo, result->tree, pickme, replayed_base);
>       }
>
>     -+static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)
>     ++static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
>      +{
>     -+  if (!mode_str || !strcmp(mode_str, "update"))
>     ++  if (!ref_action || !strcmp(ref_action, "update"))
>      +          return REF_ACTION_UPDATE;
>     -+  if (!strcmp(mode_str, "print"))
>     ++  if (!strcmp(ref_action, "print"))
>      +          return REF_ACTION_PRINT;
>     -+  die(_("invalid %s value: '%s'"), source, mode_str);
>     ++  die(_("invalid %s value: '%s'"), source, ref_action);
>      +}
>      +
>      +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)
>
>       ## t/t3650-replay-basics.sh ##
>      @@ t/t3650-replay-basics.sh
>      +test_expect_success 'replay.refAction config option' '
>      +  START=$(git rev-parse topic2) &&
>      +  test_when_finished "git branch -f topic2 $START" &&
>     -+  test_when_finished "git config --unset replay.refAction || true" &&
>      +
>     -+  git config replay.refAction print &&
>     ++  test_config replay.refAction print &&
>      +  git replay --onto main topic1..topic2 >output &&
>      +  test_line_count = 1 output &&
>      +  test_grep "^update refs/heads/topic2 " output &&
>      +
>      +  git branch -f topic2 $START &&
>     -+  git config replay.refAction update &&
>     ++  test_config replay.refAction update &&
>      +  git replay --onto main topic1..topic2 >output &&
>
>      +test_expect_success 'command-line --ref-action overrides config' '
> --
> 2.51.0
>
> base-commit: 57da342c78d8bf00259d2b720292e5b3035dadcc

This series is getting into shape nicely.  There are a few things that
need to be fixed up on patches 2 & 3 that I called out (including what
looks like some fixes that were accidentally squashed into the wrong
patch), but those should be pretty easy and then the series will be
ready to merge down.  Thanks for working on this!

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior
  2025-10-31 18:49             ` Elijah Newren
@ 2025-10-31 19:59               ` Junio C Hamano
  2025-11-05 19:07               ` Siddharth Asthana
  1 sibling, 0 replies; 129+ messages in thread
From: Junio C Hamano @ 2025-10-31 19:59 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Siddharth Asthana, git, christian.couder, phillip.wood123,
	phillip.wood, ps, karthik.188, code, rybak.a.v, jltobler, toon,
	johncai86, johannes.schindelin

Elijah Newren <newren@gmail.com> writes:

> I'm not sure the implementation details section above makes sense to
> include in the commit message; it feels like it's not providing much
> high level information nor much "why" information, but just presenting
> an alternative view of the information people will find in the patch.
> Perhaps leave it out?

Sounds like a good thing to do.

>> Test suite changes:
>>
>> All existing tests that expected command output now use
>> --ref-action=print to preserve their original behavior. This keeps
>> the tests valid while allowing them to verify that the pipeline workflow
>> still works correctly.
>>
>> New tests were added to verify:
>>   - Default atomic behavior (no output, refs updated directly)
>>   - Bare repository support (server-side use case)
>>   - Equivalence between traditional pipeline and atomic updates
>>   - Real atomicity using a lock file to verify all-or-nothing guarantee
>>   - Test isolation using test_when_finished to clean up state
>>
>> The bare repository tests were fixed to rebuild their expectations
>> independently rather than comparing to previous test output, improving
>> test reliability and isolation.
>
> The above paragraph sounds like you are comparing to an earlier
> series, which will confuse future readers who only compare to code
> that existed before your patches.

Yup, such an update relative to previous iterations belongs in the
cover letter and below the three-dash line.

> Otherwise, the patch looks good.  This is really close to being ready
> to merge; just a few minor fixups needed that I highlighted above.

Yup, I agree with all the comments I saw here.  Thanks for a great
review.



^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior
  2025-10-30 19:19           ` [PATCH v6 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
  2025-10-31 18:49             ` Elijah Newren
@ 2025-11-03 16:25             ` Phillip Wood
  2025-11-03 19:32               ` Siddharth Asthana
  1 sibling, 1 reply; 129+ messages in thread
From: Phillip Wood @ 2025-11-03 16:25 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, phillip.wood, newren, gitster, ps, karthik.188,
	code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin

Hi Siddharth

On 30/10/2025 19:19, Siddharth Asthana wrote:

> +	case REF_ACTION_UPDATE:
> +		return ref_transaction_update(transaction, refname, new_oid, old_oid,
> +					      NULL, NULL, 0, "git replay", err);

I wonder if we should use a more descriptive reflog message here that 
says what git replay was doing. For example "git replay --onto 
<new-base> <revs>" could include the new base in the reflog message like 
"git rebase" does. For "git replay --advance" we could include the 
commits that have been picked. It would be helpful to test the reflog 
message in the new tests as well.

Thanks

Phillip

> +	default:
> +		BUG("unknown ref_action_mode %d", mode);
> +	}
> +}
> +
>   int cmd_replay(int argc,
>   	       const char **argv,
>   	       const char *prefix,
> @@ -294,6 +321,8 @@ int cmd_replay(int argc,
>   	struct commit *onto = NULL;
>   	const char *onto_name = NULL;
>   	int contained = 0;
> +	const char *ref_action_str = NULL;
> +	enum ref_action_mode ref_action = REF_ACTION_UPDATE;
>   
>   	struct rev_info revs;
>   	struct commit *last_commit = NULL;
> @@ -302,12 +331,14 @@ int cmd_replay(int argc,
>   	struct merge_result result;
>   	struct strset *update_refs = NULL;
>   	kh_oid_map_t *replayed_commits;
> +	struct ref_transaction *transaction = NULL;
> +	struct strbuf transaction_err = STRBUF_INIT;
>   	int ret = 0;
>   
> -	const char * const replay_usage[] = {
> +	const char *const replay_usage[] = {
>   		N_("(EXPERIMENTAL!) git replay "
>   		   "([--contained] --onto <newbase> | --advance <branch>) "
> -		   "<revision-range>..."),
> +		   "[--ref-action[=<mode>]] <revision-range>..."),
>   		NULL
>   	};
>   	struct option replay_options[] = {
> @@ -319,6 +350,9 @@ int cmd_replay(int argc,
>   			   N_("replay onto given commit")),
>   		OPT_BOOL(0, "contained", &contained,
>   			 N_("advance all branches contained in revision-range")),
> +		OPT_STRING(0, "ref-action", &ref_action_str,
> +			   N_("mode"),
> +			   N_("control ref update behavior (update|print)")),
>   		OPT_END()
>   	};
>   
> @@ -333,6 +367,18 @@ int cmd_replay(int argc,
>   	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>   				  contained, "--contained");
>   
> +	/* Default to update mode if not specified */
> +	if (!ref_action_str)
> +		ref_action_str = "update";
> +
> +	/* Parse ref action mode */
> +	if (!strcmp(ref_action_str, "update"))
> +		ref_action = REF_ACTION_UPDATE;
> +	else if (!strcmp(ref_action_str, "print"))
> +		ref_action = REF_ACTION_PRINT;
> +	else
> +		die(_("unknown --ref-action mode '%s'"), ref_action_str);
> +
>   	advance_name = xstrdup_or_null(advance_name_opt);
>   
>   	repo_init_revisions(repo, &revs, prefix);
> @@ -389,6 +435,17 @@ int cmd_replay(int argc,
>   	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
>   			      &onto, &update_refs);
>   
> +	/* Initialize ref transaction if using update mode */
> +	if (ref_action == REF_ACTION_UPDATE) {
> +		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
> +							  0, &transaction_err);
> +		if (!transaction) {
> +			ret = error(_("failed to begin ref transaction: %s"),
> +				    transaction_err.buf);
> +			goto cleanup;
> +		}
> +	}
> +
>   	if (!onto) /* FIXME: Should handle replaying down to root commit */
>   		die("Replaying down to root commit is not supported yet!");
>   
> @@ -434,10 +491,15 @@ int cmd_replay(int argc,
>   			if (decoration->type == DECORATION_REF_LOCAL &&
>   			    (contained || strset_contains(update_refs,
>   							  decoration->name))) {
> -				printf("update %s %s %s\n",
> -				       decoration->name,
> -				       oid_to_hex(&last_commit->object.oid),
> -				       oid_to_hex(&commit->object.oid));
> +				if (handle_ref_update(ref_action, transaction,
> +						      decoration->name,
> +						      &last_commit->object.oid,
> +						      &commit->object.oid,
> +						      &transaction_err) < 0) {
> +					ret = error(_("failed to update ref '%s': %s"),
> +						    decoration->name, transaction_err.buf);
> +					goto cleanup;
> +				}
>   			}
>   			decoration = decoration->next;
>   		}
> @@ -445,10 +507,23 @@ int cmd_replay(int argc,
>   
>   	/* In --advance mode, advance the target ref */
>   	if (result.clean == 1 && advance_name) {
> -		printf("update %s %s %s\n",
> -		       advance_name,
> -		       oid_to_hex(&last_commit->object.oid),
> -		       oid_to_hex(&onto->object.oid));
> +		if (handle_ref_update(ref_action, transaction, advance_name,
> +				      &last_commit->object.oid,
> +				      &onto->object.oid,
> +				      &transaction_err) < 0) {
> +			ret = error(_("failed to update ref '%s': %s"),
> +				    advance_name, transaction_err.buf);
> +			goto cleanup;
> +		}
> +	}
> +
> +	/* Commit the ref transaction if we have one */
> +	if (transaction && result.clean == 1) {
> +		if (ref_transaction_commit(transaction, &transaction_err)) {
> +			ret = error(_("failed to commit ref transaction: %s"),
> +				    transaction_err.buf);
> +			goto cleanup;
> +		}
>   	}
>   
>   	merge_finalize(&merge_opt, &result);
> @@ -460,6 +535,9 @@ int cmd_replay(int argc,
>   	ret = result.clean;
>   
>   cleanup:
> +	if (transaction)
> +		ref_transaction_free(transaction);
> +	strbuf_release(&transaction_err);
>   	release_revisions(&revs);
>   	free(advance_name);
>   
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index 58b3759935..123734b49f 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
>   '
>   
>   test_expect_success 'using replay to rebase two branches, one on top of other' '
> -	git replay --onto main topic1..topic2 >result &&
> +	git replay --ref-action=print --onto main topic1..topic2 >result &&
>   
>   	test_line_count = 1 result &&
>   
> @@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
>   '
>   
>   test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
> -	git -C bare replay --onto main topic1..topic2 >result-bare &&
> +	git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&
>   	test_cmp expect result-bare
>   '
>   
> @@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
>   	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
>   	# 4th field of result is hash for main instead of hash for topic2
>   
> -	git replay --advance main topic1..topic2 >result &&
> +	git replay --ref-action=print --advance main topic1..topic2 >result &&
>   
>   	test_line_count = 1 result &&
>   
> @@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
>   '
>   
>   test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
> -	git -C bare replay --advance main topic1..topic2 >result-bare &&
> +	git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&
>   	test_cmp expect result-bare
>   '
>   
> @@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
>   '
>   
>   test_expect_success 'using replay to also rebase a contained branch' '
> -	git replay --contained --onto main main..topic3 >result &&
> +	git replay --ref-action=print --contained --onto main main..topic3 >result &&
>   
>   	test_line_count = 2 result &&
>   	cut -f 3 -d " " result >new-branch-tips &&
> @@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '
>   '
>   
>   test_expect_success 'using replay on bare repo to also rebase a contained branch' '
> -	git -C bare replay --contained --onto main main..topic3 >result-bare &&
> +	git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&
>   	test_cmp expect result-bare
>   '
>   
>   test_expect_success 'using replay to rebase multiple divergent branches' '
> -	git replay --onto main ^topic1 topic2 topic4 >result &&
> +	git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&
>   
>   	test_line_count = 2 result &&
>   	cut -f 3 -d " " result >new-branch-tips &&
> @@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
>   '
>   
>   test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
> -	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
> +	git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
>   
>   	test_line_count = 4 result &&
>   	cut -f 3 -d " " result >new-branch-tips &&
> @@ -217,4 +217,32 @@ test_expect_success 'merge.directoryRenames=false' '
>   		--onto rename-onto rename-onto..rename-from
>   '
>   
> +test_expect_success 'default atomic behavior updates refs directly' '
> +	# Store original state for cleanup
> +	test_when_finished "git branch -f topic2 topic1" &&
> +
> +	# Test default atomic behavior (no output, refs updated)
> +	git replay --onto main topic1..topic2 >output &&
> +	test_must_be_empty output &&
> +
> +	# Verify ref was updated
> +	git log --format=%s topic2 >actual &&
> +	test_write_lines E D M L B A >expect &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'atomic behavior in bare repository' '
> +	# Test atomic updates work in bare repo
> +	git -C bare replay --onto main topic1..topic2 >output &&
> +	test_must_be_empty output &&
> +
> +	# Verify ref was updated in bare repo
> +	git -C bare log --format=%s topic2 >actual &&
> +	test_write_lines E D M L B A >expect &&
> +	test_cmp expect actual &&
> +
> +	# Reset for other tests
> +	git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)
> +'
> +
>   test_done


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior
  2025-11-03 16:25             ` Phillip Wood
@ 2025-11-03 19:32               ` Siddharth Asthana
  2025-11-04 16:15                 ` Phillip Wood
  0 siblings, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-03 19:32 UTC (permalink / raw)
  To: phillip.wood, git
  Cc: christian.couder, newren, gitster, ps, karthik.188, code,
	rybak.a.v, jltobler, toon, johncai86, johannes.schindelin


On 03/11/25 21:55, Phillip Wood wrote:
> Hi Siddharth
>
> On 30/10/2025 19:19, Siddharth Asthana wrote:
>
>> +    case REF_ACTION_UPDATE:
>> +        return ref_transaction_update(transaction, refname, new_oid, 
>> old_oid,
>> +                          NULL, NULL, 0, "git replay", err);
>
> I wonder if we should use a more descriptive reflog message here that 
> says what git replay was doing. For example "git replay --onto 
> <new-base> <revs>" could include the new base in the reflog message 
> like "git rebase" does. For "git replay --advance" we could include 
> the commits that have been picked. It would be helpful to test the 
> reflog message in the new tests as well.
>
> Thanks
>
> Phillip


Hi Phillip,

Thanks for the suggestion! I agree that a more descriptive reflog
message would be helpful for users tracking what happened.

I looked at how `git rebase` constructs its reflog messages (it uses
something like "rebase (finish): refs/heads/feature onto abc123def")
and I'm thinking of using a simpler format for replay:

     "replay --onto main"
     "replay --advance main"

This shows the mode and target in a way that mirrors what the user
typed. I chose to use the symbolic name (e.g., "main") rather than
the commit SHA because it seems more user-friendly, though I notice
git rebase uses oid_to_hex().

Regarding "include the commits that have been picked" for --advance
mode - would you prefer:
   - The revision range as specified by the user (e.g., "topic1..topic2")?
   - Just the target branch like I have above?

The revision range would provide more context, but it might make the
reflog message quite long if the user specified something complex. I'm
happy to include it if that's what you think would be most useful.

I'll add tests for the reflog messages in both modes.

Thanks,
Siddharth


>
>
>> +    default:
>> +        BUG("unknown ref_action_mode %d", mode);
>> +    }
>> +}
>> +
>>   int cmd_replay(int argc,
>>              const char **argv,
>>              const char *prefix,
>> @@ -294,6 +321,8 @@ int cmd_replay(int argc,
>>       struct commit *onto = NULL;
>>       const char *onto_name = NULL;
>>       int contained = 0;
>> +    const char *ref_action_str = NULL;
>> +    enum ref_action_mode ref_action = REF_ACTION_UPDATE;
>>         struct rev_info revs;
>>       struct commit *last_commit = NULL;
>> @@ -302,12 +331,14 @@ int cmd_replay(int argc,
>>       struct merge_result result;
>>       struct strset *update_refs = NULL;
>>       kh_oid_map_t *replayed_commits;
>> +    struct ref_transaction *transaction = NULL;
>> +    struct strbuf transaction_err = STRBUF_INIT;
>>       int ret = 0;
>>   -    const char * const replay_usage[] = {
>> +    const char *const replay_usage[] = {
>>           N_("(EXPERIMENTAL!) git replay "
>>              "([--contained] --onto <newbase> | --advance <branch>) "
>> -           "<revision-range>..."),
>> +           "[--ref-action[=<mode>]] <revision-range>..."),
>>           NULL
>>       };
>>       struct option replay_options[] = {
>> @@ -319,6 +350,9 @@ int cmd_replay(int argc,
>>                  N_("replay onto given commit")),
>>           OPT_BOOL(0, "contained", &contained,
>>                N_("advance all branches contained in revision-range")),
>> +        OPT_STRING(0, "ref-action", &ref_action_str,
>> +               N_("mode"),
>> +               N_("control ref update behavior (update|print)")),
>>           OPT_END()
>>       };
>>   @@ -333,6 +367,18 @@ int cmd_replay(int argc,
>>       die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>>                     contained, "--contained");
>>   +    /* Default to update mode if not specified */
>> +    if (!ref_action_str)
>> +        ref_action_str = "update";
>> +
>> +    /* Parse ref action mode */
>> +    if (!strcmp(ref_action_str, "update"))
>> +        ref_action = REF_ACTION_UPDATE;
>> +    else if (!strcmp(ref_action_str, "print"))
>> +        ref_action = REF_ACTION_PRINT;
>> +    else
>> +        die(_("unknown --ref-action mode '%s'"), ref_action_str);
>> +
>>       advance_name = xstrdup_or_null(advance_name_opt);
>>         repo_init_revisions(repo, &revs, prefix);
>> @@ -389,6 +435,17 @@ int cmd_replay(int argc,
>>       determine_replay_mode(repo, &revs.cmdline, onto_name, 
>> &advance_name,
>>                     &onto, &update_refs);
>>   +    /* Initialize ref transaction if using update mode */
>> +    if (ref_action == REF_ACTION_UPDATE) {
>> +        transaction = 
>> ref_store_transaction_begin(get_main_ref_store(repo),
>> +                              0, &transaction_err);
>> +        if (!transaction) {
>> +            ret = error(_("failed to begin ref transaction: %s"),
>> +                    transaction_err.buf);
>> +            goto cleanup;
>> +        }
>> +    }
>> +
>>       if (!onto) /* FIXME: Should handle replaying down to root 
>> commit */
>>           die("Replaying down to root commit is not supported yet!");
>>   @@ -434,10 +491,15 @@ int cmd_replay(int argc,
>>               if (decoration->type == DECORATION_REF_LOCAL &&
>>                   (contained || strset_contains(update_refs,
>>                                 decoration->name))) {
>> -                printf("update %s %s %s\n",
>> -                       decoration->name,
>> - oid_to_hex(&last_commit->object.oid),
>> -                       oid_to_hex(&commit->object.oid));
>> +                if (handle_ref_update(ref_action, transaction,
>> +                              decoration->name,
>> +                              &last_commit->object.oid,
>> +                              &commit->object.oid,
>> +                              &transaction_err) < 0) {
>> +                    ret = error(_("failed to update ref '%s': %s"),
>> +                            decoration->name, transaction_err.buf);
>> +                    goto cleanup;
>> +                }
>>               }
>>               decoration = decoration->next;
>>           }
>> @@ -445,10 +507,23 @@ int cmd_replay(int argc,
>>         /* In --advance mode, advance the target ref */
>>       if (result.clean == 1 && advance_name) {
>> -        printf("update %s %s %s\n",
>> -               advance_name,
>> -               oid_to_hex(&last_commit->object.oid),
>> -               oid_to_hex(&onto->object.oid));
>> +        if (handle_ref_update(ref_action, transaction, advance_name,
>> +                      &last_commit->object.oid,
>> +                      &onto->object.oid,
>> +                      &transaction_err) < 0) {
>> +            ret = error(_("failed to update ref '%s': %s"),
>> +                    advance_name, transaction_err.buf);
>> +            goto cleanup;
>> +        }
>> +    }
>> +
>> +    /* Commit the ref transaction if we have one */
>> +    if (transaction && result.clean == 1) {
>> +        if (ref_transaction_commit(transaction, &transaction_err)) {
>> +            ret = error(_("failed to commit ref transaction: %s"),
>> +                    transaction_err.buf);
>> +            goto cleanup;
>> +        }
>>       }
>>         merge_finalize(&merge_opt, &result);
>> @@ -460,6 +535,9 @@ int cmd_replay(int argc,
>>       ret = result.clean;
>>     cleanup:
>> +    if (transaction)
>> +        ref_transaction_free(transaction);
>> +    strbuf_release(&transaction_err);
>>       release_revisions(&revs);
>>       free(advance_name);
>>   diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>> index 58b3759935..123734b49f 100755
>> --- a/t/t3650-replay-basics.sh
>> +++ b/t/t3650-replay-basics.sh
>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
>>   '
>>     test_expect_success 'using replay to rebase two branches, one on 
>> top of other' '
>> -    git replay --onto main topic1..topic2 >result &&
>> +    git replay --ref-action=print --onto main topic1..topic2 >result &&
>>         test_line_count = 1 result &&
>>   @@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two 
>> branches, one on top of other' '
>>   '
>>     test_expect_success 'using replay on bare repo to rebase two 
>> branches, one on top of other' '
>> -    git -C bare replay --onto main topic1..topic2 >result-bare &&
>> +    git -C bare replay --ref-action=print --onto main topic1..topic2 
>> >result-bare &&
>>       test_cmp expect result-bare
>>   '
>>   @@ -86,7 +86,7 @@ test_expect_success 'using replay to perform 
>> basic cherry-pick' '
>>       # 2nd field of result is refs/heads/main vs. refs/heads/topic2
>>       # 4th field of result is hash for main instead of hash for topic2
>>   -    git replay --advance main topic1..topic2 >result &&
>> +    git replay --ref-action=print --advance main topic1..topic2 
>> >result &&
>>         test_line_count = 1 result &&
>>   @@ -102,7 +102,7 @@ test_expect_success 'using replay to perform 
>> basic cherry-pick' '
>>   '
>>     test_expect_success 'using replay on bare repo to perform basic 
>> cherry-pick' '
>> -    git -C bare replay --advance main topic1..topic2 >result-bare &&
>> +    git -C bare replay --ref-action=print --advance main 
>> topic1..topic2 >result-bare &&
>>       test_cmp expect result-bare
>>   '
>>   @@ -115,7 +115,7 @@ test_expect_success 'replay fails when both 
>> --advance and --onto are omitted' '
>>   '
>>     test_expect_success 'using replay to also rebase a contained 
>> branch' '
>> -    git replay --contained --onto main main..topic3 >result &&
>> +    git replay --ref-action=print --contained --onto main 
>> main..topic3 >result &&
>>         test_line_count = 2 result &&
>>       cut -f 3 -d " " result >new-branch-tips &&
>> @@ -139,12 +139,12 @@ test_expect_success 'using replay to also 
>> rebase a contained branch' '
>>   '
>>     test_expect_success 'using replay on bare repo to also rebase a 
>> contained branch' '
>> -    git -C bare replay --contained --onto main main..topic3 
>> >result-bare &&
>> +    git -C bare replay --ref-action=print --contained --onto main 
>> main..topic3 >result-bare &&
>>       test_cmp expect result-bare
>>   '
>>     test_expect_success 'using replay to rebase multiple divergent 
>> branches' '
>> -    git replay --onto main ^topic1 topic2 topic4 >result &&
>> +    git replay --ref-action=print --onto main ^topic1 topic2 topic4 
>> >result &&
>>         test_line_count = 2 result &&
>>       cut -f 3 -d " " result >new-branch-tips &&
>> @@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase 
>> multiple divergent branches' '
>>   '
>>     test_expect_success 'using replay on bare repo to rebase multiple 
>> divergent branches, including contained ones' '
>> -    git -C bare replay --contained --onto main ^main topic2 topic3 
>> topic4 >result &&
>> +    git -C bare replay --ref-action=print --contained --onto main 
>> ^main topic2 topic3 topic4 >result &&
>>         test_line_count = 4 result &&
>>       cut -f 3 -d " " result >new-branch-tips &&
>> @@ -217,4 +217,32 @@ test_expect_success 
>> 'merge.directoryRenames=false' '
>>           --onto rename-onto rename-onto..rename-from
>>   '
>>   +test_expect_success 'default atomic behavior updates refs directly' '
>> +    # Store original state for cleanup
>> +    test_when_finished "git branch -f topic2 topic1" &&
>> +
>> +    # Test default atomic behavior (no output, refs updated)
>> +    git replay --onto main topic1..topic2 >output &&
>> +    test_must_be_empty output &&
>> +
>> +    # Verify ref was updated
>> +    git log --format=%s topic2 >actual &&
>> +    test_write_lines E D M L B A >expect &&
>> +    test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'atomic behavior in bare repository' '
>> +    # Test atomic updates work in bare repo
>> +    git -C bare replay --onto main topic1..topic2 >output &&
>> +    test_must_be_empty output &&
>> +
>> +    # Verify ref was updated in bare repo
>> +    git -C bare log --format=%s topic2 >actual &&
>> +    test_write_lines E D M L B A >expect &&
>> +    test_cmp expect actual &&
>> +
>> +    # Reset for other tests
>> +    git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse 
>> topic1)
>> +'
>> +
>>   test_done
>

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior
  2025-11-03 19:32               ` Siddharth Asthana
@ 2025-11-04 16:15                 ` Phillip Wood
  0 siblings, 0 replies; 129+ messages in thread
From: Phillip Wood @ 2025-11-04 16:15 UTC (permalink / raw)
  To: Siddharth Asthana, phillip.wood, git
  Cc: christian.couder, newren, gitster, ps, karthik.188, code,
	rybak.a.v, jltobler, toon, johncai86, johannes.schindelin

Hi Siddarth

On 03/11/2025 19:32, Siddharth Asthana wrote:
> 
> I looked at how `git rebase` constructs its reflog messages (it uses
> something like "rebase (finish): refs/heads/feature onto abc123def")
> and I'm thinking of using a simpler format for replay:
> 
>      "replay --onto main"
>      "replay --advance main"
> 
> This shows the mode and target in a way that mirrors what the user
> typed.

That makes sense

> I chose to use the symbolic name (e.g., "main") rather than
> the commit SHA because it seems more user-friendly, though I notice
> git rebase uses oid_to_hex().

One thing to note is that using oid_to_hex() tells us which commit we 
rebased on to. Using "main" means it is hard to find exactly which 
commit was used because you have to dig through the reflog for "main" to 
find where it was pointing at the time that "replay" was run.

> Regarding "include the commits that have been picked" for --advance
> mode - would you prefer:
>    - The revision range as specified by the user (e.g., "topic1..topic2")?
>    - Just the target branch like I have above?
> 
> The revision range would provide more context, but it might make the
> reflog message quite long if the user specified something complex. I'm
> happy to include it if that's what you think would be most useful.

The full revision range could certainly get quite long. "git 
cherry-pick" creates one reflog entry per picked commit which avoids 
that problem. I don't think we necessarily want "git replay" to create 
masses of reflog entries though so perhaps we should use the revision 
range if it is simple like "a..b" and something more general if it is 
more complex than that?

> I'll add tests for the reflog messages in both modes.

That's great

Thanks

Phillip

> Thanks,
> Siddharth
> 
> 
>>
>>
>>> +    default:
>>> +        BUG("unknown ref_action_mode %d", mode);
>>> +    }
>>> +}
>>> +
>>>   int cmd_replay(int argc,
>>>              const char **argv,
>>>              const char *prefix,
>>> @@ -294,6 +321,8 @@ int cmd_replay(int argc,
>>>       struct commit *onto = NULL;
>>>       const char *onto_name = NULL;
>>>       int contained = 0;
>>> +    const char *ref_action_str = NULL;
>>> +    enum ref_action_mode ref_action = REF_ACTION_UPDATE;
>>>         struct rev_info revs;
>>>       struct commit *last_commit = NULL;
>>> @@ -302,12 +331,14 @@ int cmd_replay(int argc,
>>>       struct merge_result result;
>>>       struct strset *update_refs = NULL;
>>>       kh_oid_map_t *replayed_commits;
>>> +    struct ref_transaction *transaction = NULL;
>>> +    struct strbuf transaction_err = STRBUF_INIT;
>>>       int ret = 0;
>>>   -    const char * const replay_usage[] = {
>>> +    const char *const replay_usage[] = {
>>>           N_("(EXPERIMENTAL!) git replay "
>>>              "([--contained] --onto <newbase> | --advance <branch>) "
>>> -           "<revision-range>..."),
>>> +           "[--ref-action[=<mode>]] <revision-range>..."),
>>>           NULL
>>>       };
>>>       struct option replay_options[] = {
>>> @@ -319,6 +350,9 @@ int cmd_replay(int argc,
>>>                  N_("replay onto given commit")),
>>>           OPT_BOOL(0, "contained", &contained,
>>>                N_("advance all branches contained in revision-range")),
>>> +        OPT_STRING(0, "ref-action", &ref_action_str,
>>> +               N_("mode"),
>>> +               N_("control ref update behavior (update|print)")),
>>>           OPT_END()
>>>       };
>>>   @@ -333,6 +367,18 @@ int cmd_replay(int argc,
>>>       die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>>>                     contained, "--contained");
>>>   +    /* Default to update mode if not specified */
>>> +    if (!ref_action_str)
>>> +        ref_action_str = "update";
>>> +
>>> +    /* Parse ref action mode */
>>> +    if (!strcmp(ref_action_str, "update"))
>>> +        ref_action = REF_ACTION_UPDATE;
>>> +    else if (!strcmp(ref_action_str, "print"))
>>> +        ref_action = REF_ACTION_PRINT;
>>> +    else
>>> +        die(_("unknown --ref-action mode '%s'"), ref_action_str);
>>> +
>>>       advance_name = xstrdup_or_null(advance_name_opt);
>>>         repo_init_revisions(repo, &revs, prefix);
>>> @@ -389,6 +435,17 @@ int cmd_replay(int argc,
>>>       determine_replay_mode(repo, &revs.cmdline, onto_name, 
>>> &advance_name,
>>>                     &onto, &update_refs);
>>>   +    /* Initialize ref transaction if using update mode */
>>> +    if (ref_action == REF_ACTION_UPDATE) {
>>> +        transaction = 
>>> ref_store_transaction_begin(get_main_ref_store(repo),
>>> +                              0, &transaction_err);
>>> +        if (!transaction) {
>>> +            ret = error(_("failed to begin ref transaction: %s"),
>>> +                    transaction_err.buf);
>>> +            goto cleanup;
>>> +        }
>>> +    }
>>> +
>>>       if (!onto) /* FIXME: Should handle replaying down to root 
>>> commit */
>>>           die("Replaying down to root commit is not supported yet!");
>>>   @@ -434,10 +491,15 @@ int cmd_replay(int argc,
>>>               if (decoration->type == DECORATION_REF_LOCAL &&
>>>                   (contained || strset_contains(update_refs,
>>>                                 decoration->name))) {
>>> -                printf("update %s %s %s\n",
>>> -                       decoration->name,
>>> - oid_to_hex(&last_commit->object.oid),
>>> -                       oid_to_hex(&commit->object.oid));
>>> +                if (handle_ref_update(ref_action, transaction,
>>> +                              decoration->name,
>>> +                              &last_commit->object.oid,
>>> +                              &commit->object.oid,
>>> +                              &transaction_err) < 0) {
>>> +                    ret = error(_("failed to update ref '%s': %s"),
>>> +                            decoration->name, transaction_err.buf);
>>> +                    goto cleanup;
>>> +                }
>>>               }
>>>               decoration = decoration->next;
>>>           }
>>> @@ -445,10 +507,23 @@ int cmd_replay(int argc,
>>>         /* In --advance mode, advance the target ref */
>>>       if (result.clean == 1 && advance_name) {
>>> -        printf("update %s %s %s\n",
>>> -               advance_name,
>>> -               oid_to_hex(&last_commit->object.oid),
>>> -               oid_to_hex(&onto->object.oid));
>>> +        if (handle_ref_update(ref_action, transaction, advance_name,
>>> +                      &last_commit->object.oid,
>>> +                      &onto->object.oid,
>>> +                      &transaction_err) < 0) {
>>> +            ret = error(_("failed to update ref '%s': %s"),
>>> +                    advance_name, transaction_err.buf);
>>> +            goto cleanup;
>>> +        }
>>> +    }
>>> +
>>> +    /* Commit the ref transaction if we have one */
>>> +    if (transaction && result.clean == 1) {
>>> +        if (ref_transaction_commit(transaction, &transaction_err)) {
>>> +            ret = error(_("failed to commit ref transaction: %s"),
>>> +                    transaction_err.buf);
>>> +            goto cleanup;
>>> +        }
>>>       }
>>>         merge_finalize(&merge_opt, &result);
>>> @@ -460,6 +535,9 @@ int cmd_replay(int argc,
>>>       ret = result.clean;
>>>     cleanup:
>>> +    if (transaction)
>>> +        ref_transaction_free(transaction);
>>> +    strbuf_release(&transaction_err);
>>>       release_revisions(&revs);
>>>       free(advance_name);
>>>   diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>>> index 58b3759935..123734b49f 100755
>>> --- a/t/t3650-replay-basics.sh
>>> +++ b/t/t3650-replay-basics.sh
>>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
>>>   '
>>>     test_expect_success 'using replay to rebase two branches, one on 
>>> top of other' '
>>> -    git replay --onto main topic1..topic2 >result &&
>>> +    git replay --ref-action=print --onto main topic1..topic2 >result &&
>>>         test_line_count = 1 result &&
>>>   @@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two 
>>> branches, one on top of other' '
>>>   '
>>>     test_expect_success 'using replay on bare repo to rebase two 
>>> branches, one on top of other' '
>>> -    git -C bare replay --onto main topic1..topic2 >result-bare &&
>>> +    git -C bare replay --ref-action=print --onto main topic1..topic2 
>>> >result-bare &&
>>>       test_cmp expect result-bare
>>>   '
>>>   @@ -86,7 +86,7 @@ test_expect_success 'using replay to perform 
>>> basic cherry-pick' '
>>>       # 2nd field of result is refs/heads/main vs. refs/heads/topic2
>>>       # 4th field of result is hash for main instead of hash for topic2
>>>   -    git replay --advance main topic1..topic2 >result &&
>>> +    git replay --ref-action=print --advance main topic1..topic2 
>>> >result &&
>>>         test_line_count = 1 result &&
>>>   @@ -102,7 +102,7 @@ test_expect_success 'using replay to perform 
>>> basic cherry-pick' '
>>>   '
>>>     test_expect_success 'using replay on bare repo to perform basic 
>>> cherry-pick' '
>>> -    git -C bare replay --advance main topic1..topic2 >result-bare &&
>>> +    git -C bare replay --ref-action=print --advance main 
>>> topic1..topic2 >result-bare &&
>>>       test_cmp expect result-bare
>>>   '
>>>   @@ -115,7 +115,7 @@ test_expect_success 'replay fails when both -- 
>>> advance and --onto are omitted' '
>>>   '
>>>     test_expect_success 'using replay to also rebase a contained 
>>> branch' '
>>> -    git replay --contained --onto main main..topic3 >result &&
>>> +    git replay --ref-action=print --contained --onto main 
>>> main..topic3 >result &&
>>>         test_line_count = 2 result &&
>>>       cut -f 3 -d " " result >new-branch-tips &&
>>> @@ -139,12 +139,12 @@ test_expect_success 'using replay to also 
>>> rebase a contained branch' '
>>>   '
>>>     test_expect_success 'using replay on bare repo to also rebase a 
>>> contained branch' '
>>> -    git -C bare replay --contained --onto main main..topic3 >result- 
>>> bare &&
>>> +    git -C bare replay --ref-action=print --contained --onto main 
>>> main..topic3 >result-bare &&
>>>       test_cmp expect result-bare
>>>   '
>>>     test_expect_success 'using replay to rebase multiple divergent 
>>> branches' '
>>> -    git replay --onto main ^topic1 topic2 topic4 >result &&
>>> +    git replay --ref-action=print --onto main ^topic1 topic2 topic4 
>>> >result &&
>>>         test_line_count = 2 result &&
>>>       cut -f 3 -d " " result >new-branch-tips &&
>>> @@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase 
>>> multiple divergent branches' '
>>>   '
>>>     test_expect_success 'using replay on bare repo to rebase multiple 
>>> divergent branches, including contained ones' '
>>> -    git -C bare replay --contained --onto main ^main topic2 topic3 
>>> topic4 >result &&
>>> +    git -C bare replay --ref-action=print --contained --onto main 
>>> ^main topic2 topic3 topic4 >result &&
>>>         test_line_count = 4 result &&
>>>       cut -f 3 -d " " result >new-branch-tips &&
>>> @@ -217,4 +217,32 @@ test_expect_success 
>>> 'merge.directoryRenames=false' '
>>>           --onto rename-onto rename-onto..rename-from
>>>   '
>>>   +test_expect_success 'default atomic behavior updates refs directly' '
>>> +    # Store original state for cleanup
>>> +    test_when_finished "git branch -f topic2 topic1" &&
>>> +
>>> +    # Test default atomic behavior (no output, refs updated)
>>> +    git replay --onto main topic1..topic2 >output &&
>>> +    test_must_be_empty output &&
>>> +
>>> +    # Verify ref was updated
>>> +    git log --format=%s topic2 >actual &&
>>> +    test_write_lines E D M L B A >expect &&
>>> +    test_cmp expect actual
>>> +'
>>> +
>>> +test_expect_success 'atomic behavior in bare repository' '
>>> +    # Test atomic updates work in bare repo
>>> +    git -C bare replay --onto main topic1..topic2 >output &&
>>> +    test_must_be_empty output &&
>>> +
>>> +    # Verify ref was updated in bare repo
>>> +    git -C bare log --format=%s topic2 >actual &&
>>> +    test_write_lines E D M L B A >expect &&
>>> +    test_cmp expect actual &&
>>> +
>>> +    # Reset for other tests
>>> +    git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse 
>>> topic1)
>>> +'
>>> +
>>>   test_done
>>


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation
  2025-10-31 18:47             ` Elijah Newren
@ 2025-11-05 18:39               ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-05 18:39 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, christian.couder, phillip.wood123, phillip.wood, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 01/11/25 00:17, Elijah Newren wrote:
> On Thu, Oct 30, 2025 at 12:19 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> In preparation for adding the --ref-action option, convert option
>> validation to use die_for_incompatible_opt2(). This helper provides
>> standardized error messages for mutually exclusive options.
>>
>> The following commit introduces --ref-action which will be incompatible
>> with certain other options. Using die_for_incompatible_opt2() now means
>> that commit can cleanly add its validation using the same pattern,
>> keeping the validation logic consistent and maintainable.
>>
>> This also aligns git-replay's option handling with how other Git commands
>> manage option conflicts, using the established die_for_incompatible_opt*()
>> helper family.
>>
>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
>> ---
>>   builtin/replay.c | 6 +++---
>>   1 file changed, 3 insertions(+), 3 deletions(-)
>>
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index 6172c8aacc..b64fc72063 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -330,9 +330,9 @@ int cmd_replay(int argc,
>>                  usage_with_options(replay_usage, replay_options);
>>          }
>>
>> -       if (advance_name_opt && contained)
>> -               die(_("options '%s' and '%s' cannot be used together"),
>> -                   "--advance", "--contained");
>> +       die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>> +                                 contained, "--contained");
>> +
>>          advance_name = xstrdup_or_null(advance_name_opt);
>>
>>          repo_init_revisions(repo, &revs, prefix);
>> --
>> 2.51.0


Hi Elijah,


> Thanks for splitting this one out; looks good.


Thanks for confirming! I'm glad the preparatory refactoring in its own 
commit makes the series easier to review.

Siddharth


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 3/3] replay: add replay.refAction config option
  2025-10-31  7:08             ` Christian Couder
@ 2025-11-05 19:03               ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-05 19:03 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, phillip.wood123, phillip.wood, newren, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 31/10/25 12:38, Christian Couder wrote:
> On Thu, Oct 30, 2025 at 8:20 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>
>> +static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
>> +{
>> +       if (!ref_action || !strcmp(ref_action, "update"))
>> +               return REF_ACTION_UPDATE;
>> +       if (!strcmp(ref_action, "print"))
>> +               return REF_ACTION_PRINT;
>> +       die(_("invalid %s value: '%s'"), source, ref_action);
>> +}
>> +
>> +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)


Hi Christian,


> I think it could be "ref_action" (instead of "ref_action_str" ) in
> this function too.


Good catch. Will make this consistent in v7.


>> +{
>> +       const char *config_value = NULL;
>> +
>> +       /* Command line option takes precedence */
>> +       if (ref_action_str)
>> +               return parse_ref_action_mode(ref_action_str, "--ref-action");
>> +
>> +       /* Check config value */
>> +       if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
>> +               return parse_ref_action_mode(config_value, "replay.refAction");
>> +
>> +       /* Default to update mode */
>> +       return REF_ACTION_UPDATE;
>> +}
>> +
>>   static int handle_ref_update(enum ref_action_mode mode,
>>                               struct ref_transaction *transaction,
>>                               const char *refname,
>> @@ -367,17 +393,8 @@ int cmd_replay(int argc,
>>          die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>>                                    contained, "--contained");
>>
>> -       /* Default to update mode if not specified */
>> -       if (!ref_action_str)
>> -               ref_action_str = "update";
>> -
>> -       /* Parse ref action mode */
>> -       if (!strcmp(ref_action_str, "update"))
>> -               ref_action = REF_ACTION_UPDATE;
>> -       else if (!strcmp(ref_action_str, "print"))
>> -               ref_action = REF_ACTION_PRINT;
>> -       else
>> -               die(_("unknown --ref-action mode '%s'"), ref_action_str);
> Maybe parse_ref_action_mode() could have been introduced in the
> previous commit already?


You're right—since parse_ref_action_mode() is actually used for 
validation in commit 2, it makes more sense to introduce it there rather 
than wait until commit 3. Will move it to the earlier commit.


>
>> +       /* Parse ref action mode from command line or config */
>> +       ref_action = get_ref_action_mode(repo, ref_action_str);
> Here it could be:
>
>        ref_mode = get_ref_action_mode(repo, ref_action);
>
> Thanks!


Agreed. The variable naming was inconsistent—I had both `ref_action`
(for the string) and `ref_action` (for the enum) which was confusing.
Will use `ref_action` for the string parameter and `ref_mode` for the
enum variable throughout for clarity.

Thanks for the careful review!


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior
  2025-10-31 18:49             ` Elijah Newren
  2025-10-31 19:59               ` Junio C Hamano
@ 2025-11-05 19:07               ` Siddharth Asthana
  1 sibling, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-05 19:07 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, christian.couder, phillip.wood123, phillip.wood, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 01/11/25 00:19, Elijah Newren wrote:
> On Thu, Oct 30, 2025 at 12:20 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> The git replay command currently outputs update commands that can be
>> piped to update-ref to achieve a rebase, e.g.
>>
>>    git replay --onto main topic1..topic2 | git update-ref --stdin
>>
>> This separation had advantages for three special cases:
>>    * it made testing easy (when state isn't modified from one step to
>>      the next, you don't need to make temporary branches or have undo
>>      commands, or try to track the changes)
>>    * it provided a natural can-it-rebase-cleanly (and what would it
>>      rebase to) capability without automatically updating refs, similar
>>      to a --dry-run
>>    * it provided a natural low-level tool for the suite of hash-object,
>>      mktree, commit-tree, mktag, merge-tree, and update-ref, allowing
>>      users to have another building block for experimentation and making
>>      new tools
>>
>> However, it should be noted that all three of these are somewhat
>> special cases; users, whether on the client or server side, would
>> almost certainly find it more ergonomic to simply have the updating
>> of refs be the default.
>>
>> For server-side operations in particular, the pipeline architecture
>> creates process coordination overhead. Server implementations that need
>> to perform rebases atomically must maintain additional code to:
>>
>>    1. Spawn and manage a pipeline between git-replay and git-update-ref
>>    2. Coordinate stdout/stderr streams across the pipe boundary
>>    3. Handle partial failure states if the pipeline breaks mid-execution
>>    4. Parse and validate the update-ref command output
>>
>> Change the default behavior to update refs directly, and atomically (at
>> least to the extent supported by the refs backend in use). This
>> eliminates the process coordination overhead for the common case.
>>
>> For users needing the traditional pipeline workflow, add a new
>> --ref-action=<mode> option that preserves the original behavior:
>>
>>    git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin
>>
>> The mode can be:
>>    * update (default): Update refs directly using an atomic transaction
>>    * print: Output update-ref commands for pipeline use
> Looks good up to here.
>
>> Implementation details:
>>
>> The atomic ref updates are implemented using Git's ref transaction API.
>> In cmd_replay(), when not in `print` mode, we initialize a transaction
>> using ref_store_transaction_begin() with the default atomic behavior.
>> As commits are replayed, ref updates are staged into the transaction
>> using ref_transaction_update(). Finally, ref_transaction_commit()
>> applies all updates atomically—either all updates succeed or none do.
>>
>> To avoid code duplication between the 'print' and 'update' modes, this
>> commit extracts a handle_ref_update() helper function. This function
>> takes the mode (as an enum) and either prints the update command or
>> stages it into the transaction. Using an enum rather than passing the
>> string around provides type safety and allows the compiler to catch
>> typos. The switch statement makes it easy to add future modes.
>>
>> The helper function signature:
>>
>>    static int handle_ref_update(enum ref_action_mode mode,
>>                                  struct ref_transaction *transaction,
>>                                  const char *refname,
>>                                  const struct object_id *new_oid,
>>                                  const struct object_id *old_oid,
>>                                  struct strbuf *err)
>>
>> The enum is defined as:
>>
>>    enum ref_action_mode {
>>        REF_ACTION_UPDATE,
>>        REF_ACTION_PRINT
>>    };
>>
>> The mode string is converted to enum immediately after parse_options()
>> to avoid string comparisons throughout the codebase and provide compiler
>> protection against typos.
> I'm not sure the implementation details section above makes sense to
> include in the commit message; it feels like it's not providing much
> high level information nor much "why" information, but just presenting
> an alternative view of the information people will find in the patch.
> Perhaps leave it out?


Make sense. Will remove the "Implementation details" section.


>
>> Test suite changes:
>>
>> All existing tests that expected command output now use
>> --ref-action=print to preserve their original behavior. This keeps
>> the tests valid while allowing them to verify that the pipeline workflow
>> still works correctly.
>>
>> New tests were added to verify:
>>    - Default atomic behavior (no output, refs updated directly)
>>    - Bare repository support (server-side use case)
>>    - Equivalence between traditional pipeline and atomic updates
>>    - Real atomicity using a lock file to verify all-or-nothing guarantee
>>    - Test isolation using test_when_finished to clean up state
>>
>> The bare repository tests were fixed to rebuild their expectations
>> independently rather than comparing to previous test output, improving
>> test reliability and isolation.
> The above paragraph sounds like you are comparing to an earlier
> series, which will confuse future readers who only compare to code
> that existed before your patches.


Right—"The bare repository tests were fixed..." belongs in the cover 
letter, not the commit message. Will remove it.


>
>> A following commit will add a replay.refAction configuration
>> option for users who prefer the traditional pipeline output as their
>> default behavior.
>>
>> Helped-by: Elijah Newren <newren@gmail.com>
>> Helped-by: Patrick Steinhardt <ps@pks.im>
>> Helped-by: Christian Couder <christian.couder@gmail.com>
>> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
>> ---
>>   Documentation/git-replay.adoc | 65 +++++++++++++++--------
>>   builtin/replay.c              | 98 +++++++++++++++++++++++++++++++----
>>   t/t3650-replay-basics.sh      | 44 +++++++++++++---
>>   3 files changed, 167 insertions(+), 40 deletions(-)
>>
>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
>> index 0b12bf8aa4..037b093196 100644
>> --- a/Documentation/git-replay.adoc
>> +++ b/Documentation/git-replay.adoc
>> @@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
>>   SYNOPSIS
>>   --------
>>   [verse]
>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
>> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...
>>
>>   DESCRIPTION
>>   -----------
>>
>>   Takes ranges of commits and replays them onto a new location. Leaves
>> -the working tree and the index untouched, and updates no references.
>> -The output of this command is meant to be used as input to
>> -`git update-ref --stdin`, which would update the relevant branches
>> +the working tree and the index untouched. By default, updates the
>> +relevant references using an atomic transaction (all refs update or
>> +none). Use `--ref-action=print` to avoid automatic ref updates and
>> +instead get update commands that can be piped to `git update-ref --stdin`
>>   (see the OUTPUT section below).
>>
>>   THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
>> @@ -29,18 +30,31 @@ OPTIONS
>>          Starting point at which to create the new commits.  May be any
>>          valid commit, and not just an existing branch name.
>>   +
>> -When `--onto` is specified, the update-ref command(s) in the output will
>> -update the branch(es) in the revision range to point at the new
>> -commits, similar to the way how `git rebase --update-refs` updates
>> -multiple branches in the affected range.
>> +When `--onto` is specified, the branch(es) in the revision range will be
>> +updated to point at the new commits (or update commands will be printed
>> +if `--ref-action=print` is used), similar to the way `git rebase --update-refs`
>> +updates multiple branches in the affected range.
> I'm not sure if the parenthetical comment is necessary; we tend not to
> try to document every combinatorial combination with every sentence.


Good point. Will remove the `--ref-action=print` parentheticals from
the `--onto` and `--advance` descriptions in git-replay.adoc.


> For example, in the `git rebase` manpage under the description of the
> `--strategy` flag, it says "Because git rebase replays each commit
> from the working branch on top of the <upstream> branch using the
> given strategy", which is technically incorrect if either the --onto
> or --keep-base flags are specified, but belaboring all the details at
> that location would just burden the reader and the explanations of
> --onto and --keep-base are sufficient for users to understand.  I
> think we tend to just describe the option in combination with the
> default, and only mention other options if the combination is
> ambiguous or confusing.  I don't think users would find anything
> ambiguous or confusing about how --ref-action=print would combine with
> these options, so I don't think it's necessary to make the description
> longer.
>
>>   --advance <branch>::
>>          Starting point at which to create the new commits; must be a
>>          branch name.
>>   +
>> -When `--advance` is specified, the update-ref command(s) in the output
>> -will update the branch passed as an argument to `--advance` to point at
>> -the new commits (in other words, this mimics a cherry-pick operation).
>> +The history is replayed on top of the <branch> and <branch> is updated to
>> +point at the tip of the resulting history (or an update command will be
>> +printed if `--ref-action=print` is used). This is different from `--onto`,
>> +which uses the target only as a starting point without updating it.
> Same comment as above about this parenthetical comment as well.
>
>> +
>> +--ref-action[=<mode>]::
>> +       Control how references are updated. The mode can be:
>> ++
>> +--
>> +       * `update` (default): Update refs directly using an atomic transaction.
>> +         All refs are updated or none are (all-or-nothing behavior).
>> +       * `print`: Output update-ref commands for pipeline use. This is the
>> +         traditional behavior where output can be piped to `git update-ref --stdin`.
>> +--
>> ++
>> +The default mode can be configured via the `replay.refAction` configuration variable.
> This last sentence conflicts with the commit message; if the
> configuration option isn't added until a later commit, then this last
> sentence shouldn't be added until then either.


Absolutely right. Will move "The default mode can be configured via the 
`replay.refAction` configuration variable." to commit 3.


>
>>   <revision-range>::
>>          Range of commits to replay. More than one <revision-range> can
>> @@ -54,8 +68,11 @@ include::rev-list-options.adoc[]
>>   OUTPUT
>>   ------
>>
>> -When there are no conflicts, the output of this command is usable as
>> -input to `git update-ref --stdin`.  It is of the form:
>> +By default, or with `--ref-action=update`, this command produces no output on
>> +success, as refs are updated directly using an atomic transaction.
>> +
>> +When using `--ref-action=print`, the output is usable as input to
>> +`git update-ref --stdin`. It is of the form:
>>
>>          update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>>          update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>> @@ -81,6 +98,14 @@ To simply rebase `mybranch` onto `target`:
>>
>>   ------------
>>   $ git replay --onto target origin/main..mybranch
>> +------------
>> +
>> +The refs are updated atomically and no output is produced on success.
>> +
>> +To see what would be updated without actually updating:
>> +
>> +------------
>> +$ git replay --ref-action=print --onto target origin/main..mybranch
>>   update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
>>   ------------
>>
>> @@ -88,33 +113,29 @@ To cherry-pick the commits from mybranch onto target:
>>
>>   ------------
>>   $ git replay --advance target origin/main..mybranch
>> -update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
>>   ------------
>>
>>   Note that the first two examples replay the exact same commits and on
>>   top of the exact same new base, they only differ in that the first
>> -provides instructions to make mybranch point at the new commits and
>> -the second provides instructions to make target point at them.
>> +updates mybranch to point at the new commits and the second updates
>> +target to point at them.
>>
>>   What if you have a stack of branches, one depending upon another, and
>>   you'd really like to rebase the whole set?
>>
>>   ------------
>>   $ git replay --contained --onto origin/main origin/main..tipbranch
>> -update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>> -update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>> -update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
>>   ------------
>>
>> +All three branches (`branch1`, `branch2`, and `tipbranch`) are updated
>> +atomically.
>> +
>>   When calling `git replay`, one does not need to specify a range of
>>   commits to replay using the syntax `A..B`; any range expression will
>>   do:
>>
>>   ------------
>>   $ git replay --onto origin/main ^base branch1 branch2 branch3
>> -update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
>> -update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
>> -update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
>>   ------------
>>
>>   This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index b64fc72063..0564d4d2e7 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -20,6 +20,11 @@
>>   #include <oidset.h>
>>   #include <tree.h>
>>
>> +enum ref_action_mode {
>> +       REF_ACTION_UPDATE,
>> +       REF_ACTION_PRINT,
>> +};
>> +
>>   static const char *short_commit_name(struct repository *repo,
>>                                       struct commit *commit)
>>   {
>> @@ -284,6 +289,28 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>          return create_commit(repo, result->tree, pickme, replayed_base);
>>   }
>>
>> +static int handle_ref_update(enum ref_action_mode mode,
>> +                            struct ref_transaction *transaction,
>> +                            const char *refname,
>> +                            const struct object_id *new_oid,
>> +                            const struct object_id *old_oid,
>> +                            struct strbuf *err)
>> +{
>> +       switch (mode) {
>> +       case REF_ACTION_PRINT:
>> +               printf("update %s %s %s\n",
>> +                      refname,
>> +                      oid_to_hex(new_oid),
>> +                      oid_to_hex(old_oid));
>> +               return 0;
>> +       case REF_ACTION_UPDATE:
>> +               return ref_transaction_update(transaction, refname, new_oid, old_oid,
>> +                                             NULL, NULL, 0, "git replay", err);
>> +       default:
>> +               BUG("unknown ref_action_mode %d", mode);
>> +       }
>> +}
>> +
>>   int cmd_replay(int argc,
>>                 const char **argv,
>>                 const char *prefix,
>> @@ -294,6 +321,8 @@ int cmd_replay(int argc,
>>          struct commit *onto = NULL;
>>          const char *onto_name = NULL;
>>          int contained = 0;
>> +       const char *ref_action_str = NULL;
>> +       enum ref_action_mode ref_action = REF_ACTION_UPDATE;
>>
>>          struct rev_info revs;
>>          struct commit *last_commit = NULL;
>> @@ -302,12 +331,14 @@ int cmd_replay(int argc,
>>          struct merge_result result;
>>          struct strset *update_refs = NULL;
>>          kh_oid_map_t *replayed_commits;
>> +       struct ref_transaction *transaction = NULL;
>> +       struct strbuf transaction_err = STRBUF_INIT;
>>          int ret = 0;
>>
>> -       const char * const replay_usage[] = {
>> +       const char *const replay_usage[] = {
>>                  N_("(EXPERIMENTAL!) git replay "
>>                     "([--contained] --onto <newbase> | --advance <branch>) "
>> -                  "<revision-range>..."),
>> +                  "[--ref-action[=<mode>]] <revision-range>..."),
>>                  NULL
>>          };
>>          struct option replay_options[] = {
>> @@ -319,6 +350,9 @@ int cmd_replay(int argc,
>>                             N_("replay onto given commit")),
>>                  OPT_BOOL(0, "contained", &contained,
>>                           N_("advance all branches contained in revision-range")),
>> +               OPT_STRING(0, "ref-action", &ref_action_str,
>> +                          N_("mode"),
>> +                          N_("control ref update behavior (update|print)")),
>>                  OPT_END()
>>          };
>>
>> @@ -333,6 +367,18 @@ int cmd_replay(int argc,
>>          die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>>                                    contained, "--contained");
>>
>> +       /* Default to update mode if not specified */
>> +       if (!ref_action_str)
>> +               ref_action_str = "update";
>> +
>> +       /* Parse ref action mode */
>> +       if (!strcmp(ref_action_str, "update"))
>> +               ref_action = REF_ACTION_UPDATE;
>> +       else if (!strcmp(ref_action_str, "print"))
>> +               ref_action = REF_ACTION_PRINT;
>> +       else
>> +               die(_("unknown --ref-action mode '%s'"), ref_action_str);
>> +
>>          advance_name = xstrdup_or_null(advance_name_opt);
>>
>>          repo_init_revisions(repo, &revs, prefix);
>> @@ -389,6 +435,17 @@ int cmd_replay(int argc,
>>          determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
>>                                &onto, &update_refs);
>>
>> +       /* Initialize ref transaction if using update mode */
>> +       if (ref_action == REF_ACTION_UPDATE) {
>> +               transaction = ref_store_transaction_begin(get_main_ref_store(repo),
>> +                                                         0, &transaction_err);
>> +               if (!transaction) {
>> +                       ret = error(_("failed to begin ref transaction: %s"),
>> +                                   transaction_err.buf);
>> +                       goto cleanup;
>> +               }
>> +       }
>> +
>>          if (!onto) /* FIXME: Should handle replaying down to root commit */
>>                  die("Replaying down to root commit is not supported yet!");
>>
>> @@ -434,10 +491,15 @@ int cmd_replay(int argc,
>>                          if (decoration->type == DECORATION_REF_LOCAL &&
>>                              (contained || strset_contains(update_refs,
>>                                                            decoration->name))) {
>> -                               printf("update %s %s %s\n",
>> -                                      decoration->name,
>> -                                      oid_to_hex(&last_commit->object.oid),
>> -                                      oid_to_hex(&commit->object.oid));
>> +                               if (handle_ref_update(ref_action, transaction,
>> +                                                     decoration->name,
>> +                                                     &last_commit->object.oid,
>> +                                                     &commit->object.oid,
>> +                                                     &transaction_err) < 0) {
>> +                                       ret = error(_("failed to update ref '%s': %s"),
>> +                                                   decoration->name, transaction_err.buf);
>> +                                       goto cleanup;
>> +                               }
>>                          }
>>                          decoration = decoration->next;
>>                  }
>> @@ -445,10 +507,23 @@ int cmd_replay(int argc,
>>
>>          /* In --advance mode, advance the target ref */
>>          if (result.clean == 1 && advance_name) {
>> -               printf("update %s %s %s\n",
>> -                      advance_name,
>> -                      oid_to_hex(&last_commit->object.oid),
>> -                      oid_to_hex(&onto->object.oid));
>> +               if (handle_ref_update(ref_action, transaction, advance_name,
>> +                                     &last_commit->object.oid,
>> +                                     &onto->object.oid,
>> +                                     &transaction_err) < 0) {
>> +                       ret = error(_("failed to update ref '%s': %s"),
>> +                                   advance_name, transaction_err.buf);
>> +                       goto cleanup;
>> +               }
>> +       }
>> +
>> +       /* Commit the ref transaction if we have one */
>> +       if (transaction && result.clean == 1) {
>> +               if (ref_transaction_commit(transaction, &transaction_err)) {
>> +                       ret = error(_("failed to commit ref transaction: %s"),
>> +                                   transaction_err.buf);
>> +                       goto cleanup;
>> +               }
>>          }
>>
>>          merge_finalize(&merge_opt, &result);
>> @@ -460,6 +535,9 @@ int cmd_replay(int argc,
>>          ret = result.clean;
>>
>>   cleanup:
>> +       if (transaction)
>> +               ref_transaction_free(transaction);
>> +       strbuf_release(&transaction_err);
>>          release_revisions(&revs);
>>          free(advance_name);
>>
>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>> index 58b3759935..123734b49f 100755
>> --- a/t/t3650-replay-basics.sh
>> +++ b/t/t3650-replay-basics.sh
>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
>>   '
>>
>>   test_expect_success 'using replay to rebase two branches, one on top of other' '
>> -       git replay --onto main topic1..topic2 >result &&
>> +       git replay --ref-action=print --onto main topic1..topic2 >result &&
>>
>>          test_line_count = 1 result &&
>>
>> @@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
>>   '
>>
>>   test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
>> -       git -C bare replay --onto main topic1..topic2 >result-bare &&
>> +       git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&
>>          test_cmp expect result-bare
>>   '
>>
>> @@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
>>          # 2nd field of result is refs/heads/main vs. refs/heads/topic2
>>          # 4th field of result is hash for main instead of hash for topic2
>>
>> -       git replay --advance main topic1..topic2 >result &&
>> +       git replay --ref-action=print --advance main topic1..topic2 >result &&
>>
>>          test_line_count = 1 result &&
>>
>> @@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
>>   '
>>
>>   test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
>> -       git -C bare replay --advance main topic1..topic2 >result-bare &&
>> +       git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&
>>          test_cmp expect result-bare
>>   '
>>
>> @@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
>>   '
>>
>>   test_expect_success 'using replay to also rebase a contained branch' '
>> -       git replay --contained --onto main main..topic3 >result &&
>> +       git replay --ref-action=print --contained --onto main main..topic3 >result &&
>>
>>          test_line_count = 2 result &&
>>          cut -f 3 -d " " result >new-branch-tips &&
>> @@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '
>>   '
>>
>>   test_expect_success 'using replay on bare repo to also rebase a contained branch' '
>> -       git -C bare replay --contained --onto main main..topic3 >result-bare &&
>> +       git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&
>>          test_cmp expect result-bare
>>   '
>>
>>   test_expect_success 'using replay to rebase multiple divergent branches' '
>> -       git replay --onto main ^topic1 topic2 topic4 >result &&
>> +       git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&
>>
>>          test_line_count = 2 result &&
>>          cut -f 3 -d " " result >new-branch-tips &&
>> @@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
>>   '
>>
>>   test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
>> -       git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
>> +       git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
>>
>>          test_line_count = 4 result &&
>>          cut -f 3 -d " " result >new-branch-tips &&
>> @@ -217,4 +217,32 @@ test_expect_success 'merge.directoryRenames=false' '
>>                  --onto rename-onto rename-onto..rename-from
>>   '
>>
>> +test_expect_success 'default atomic behavior updates refs directly' '
>> +       # Store original state for cleanup
>> +       test_when_finished "git branch -f topic2 topic1" &&
> Why are you resetting topic2 back to topic1?  Shouldn't it be set back
> to what it was before the test ran instead, e.g.
>      START=$(git rev-parse topic2) &&
>      test_when_finished "git branch -f topic2 $START" &&
> ?


Yes, you're right. Should be:
     START=$(git rev-parse topic2) &&
     test_when_finished "git branch -f topic2 $START" &&


>> +
>> +       # Test default atomic behavior (no output, refs updated)
>> +       git replay --onto main topic1..topic2 >output &&
>> +       test_must_be_empty output &&
>> +
>> +       # Verify ref was updated
>> +       git log --format=%s topic2 >actual &&
>> +       test_write_lines E D M L B A >expect &&
>> +       test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'atomic behavior in bare repository' '
>> +       # Test atomic updates work in bare repo
>> +       git -C bare replay --onto main topic1..topic2 >output &&
>> +       test_must_be_empty output &&
>> +
>> +       # Verify ref was updated in bare repo
>> +       git -C bare log --format=%s topic2 >actual &&
>> +       test_write_lines E D M L B A >expect &&
>> +       test_cmp expect actual &&
>> +
>> +       # Reset for other tests
>> +       git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)
> This reset happens too late to help if the earlier commands fail, and
> also resets to the wrong ref.  You should instead use a
> test_when_finished block, and make sure to reset to what topic2 used
> to point to, not reset it to what topic1 points to.


Will fix both test cleanup issues using test_when_finished with the 
proper START variable.

Thanks for the thorough review!


>
>
> Otherwise, the patch looks good.  This is really close to being ready
> to merge; just a few minor fixups needed that I highlighted above.

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v6 3/3] replay: add replay.refAction config option
  2025-10-31 18:49             ` Elijah Newren
@ 2025-11-05 19:10               ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-05 19:10 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, christian.couder, phillip.wood123, phillip.wood, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 01/11/25 00:19, Elijah Newren wrote:
> On Thu, Oct 30, 2025 at 12:20 PM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> Add a configuration option to control the default behavior of git replay
>> for updating references. This allows users who prefer the traditional
>> pipeline output to set it once in their config instead of passing
>> --ref-action=print with every command.
>>
>> The config option uses string values that mirror the behavior modes:
>>    * replay.refAction = update (default): atomic ref updates
>>    * replay.refAction = print: output commands for pipeline
>>
>> The command-line --ref-action option always overrides the config setting,
>> allowing users to temporarily change behavior for a single invocation.
> The above paragraph merely states that we follow git practices with
> this config options and its corresponding command line; I think we'd
> need to call it out if we didn't do that, but calling out that we do
> follow git conventions seems unnecessary.


Fair point. Will remove the paragraph about command-line precedence.


>
>> Implementation details:
>>
>> In cmd_replay(), after parsing command-line options, we check if
>> --ref-action was provided. If not, we read the configuration using
>> repo_config_get_string_tmp(). If the config variable is set, we validate
>> the value and use it to set the ref_action_str:
>>
>>    Config value      Internal mode    Behavior
>>    ──────────────────────────────────────────────────────────────
>>    "update"          "update"         Atomic ref updates (default)
>>    "print"           "print"          Pipeline output
>>    (not set)         "update"         Atomic ref updates (default)
>>    (invalid)         error            Die with helpful message
>>
>> If an invalid value is provided, we die() immediately with an error
>> message explaining the valid options. This catches configuration errors
>> early and provides clear guidance to users.
>>
>> The command-line --ref-action option, when provided, overrides the
>> config value. This precedence allows users to set their preferred default
>> while still having per-invocation control:
>>
>>    git config replay.refAction print         # Set default
>>    git replay --ref-action=update --onto main topic  # Override once
>>
>> The config and command-line option use the same value names ('update'
>> and 'print') for consistency and clarity. This makes it immediately
>> obvious how the config maps to the command-line option, addressing
>> feedback about the relationship between configuration and command-line
>> options being clear to users.
> An implementation details section may make sense if it answers a
> "why?" question, or it explains something counter-intuitive, or it
> provides high enough level details that it makes the patch easier to
> read/follow, or it otherwise does something more than just repackage
> the patch in an alternate format.  I appreciate the attempt to provide
> these, but I think they simply make the commit message longer without
> adding value.


Understood. Will remove the implementation details and configuration 
precedence table—they're just restating what's in the code.


>
>> Examples:
>>
>> $ git config --global replay.refAction print
>> $ git replay --onto main topic1..topic2 | git update-ref --stdin
>>
>> $ git replay --ref-action=update --onto main topic1..topic2
>>
>> $ git config replay.refAction update
>> $ git replay --onto main topic1..topic2  # Updates refs directly
>>
>> The implementation follows Git's standard configuration precedence:
>> command-line options override config values, which matches user
>> expectations across all Git commands.
> I don't find the Examples section helpful either; it's yet another
> re-iteration that we're following conventions.


Will remove the Examples section too.


>
>> Helped-by: Junio C Hamano <gitster@pobox.com>
>> Helped-by: Elijah Newren <newren@gmail.com>
>> Helped-by: Christian Couder <christian.couder@gmail.com>
>> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
>> ---
>>   Documentation/config/replay.adoc | 11 ++++++++
>>   builtin/replay.c                 | 39 ++++++++++++++++++--------
>>   t/t3650-replay-basics.sh         | 48 +++++++++++++++++++++++++++++++-
>>   3 files changed, 86 insertions(+), 12 deletions(-)
>>   create mode 100644 Documentation/config/replay.adoc
>>
>> diff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc
>> new file mode 100644
>> index 0000000000..7d549d2f0e
>> --- /dev/null
>> +++ b/Documentation/config/replay.adoc
>> @@ -0,0 +1,11 @@
>> +replay.refAction::
>> +       Specifies the default mode for handling reference updates in
>> +       `git replay`. The value can be:
>> ++
>> +--
>> +       * `update`: Update refs directly using an atomic transaction (default behavior).
>> +       * `print`: Output update-ref commands for pipeline use.
>> +--
>> ++
>> +This setting can be overridden with the `--ref-action` command-line option.
>> +When not configured, `git replay` defaults to `update` mode.
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index 0564d4d2e7..810068f8ef 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -8,6 +8,7 @@
>>   #include "git-compat-util.h"
>>
>>   #include "builtin.h"
>> +#include "config.h"
>>   #include "environment.h"
>>   #include "hex.h"
>>   #include "lockfile.h"
>> @@ -289,6 +290,31 @@ static struct commit *pick_regular_commit(struct repository *repo,
>>          return create_commit(repo, result->tree, pickme, replayed_base);
>>   }
>>
>> +static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
>> +{
>> +       if (!ref_action || !strcmp(ref_action, "update"))
>> +               return REF_ACTION_UPDATE;
>> +       if (!strcmp(ref_action, "print"))
>> +               return REF_ACTION_PRINT;
>> +       die(_("invalid %s value: '%s'"), source, ref_action);
>> +}
>> +
>> +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)
>> +{
>> +       const char *config_value = NULL;
>> +
>> +       /* Command line option takes precedence */
>> +       if (ref_action_str)
>> +               return parse_ref_action_mode(ref_action_str, "--ref-action");
>> +
>> +       /* Check config value */
>> +       if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
>> +               return parse_ref_action_mode(config_value, "replay.refAction");
>> +
>> +       /* Default to update mode */
>> +       return REF_ACTION_UPDATE;
>> +}
>> +
>>   static int handle_ref_update(enum ref_action_mode mode,
>>                               struct ref_transaction *transaction,
>>                               const char *refname,
>> @@ -367,17 +393,8 @@ int cmd_replay(int argc,
>>          die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>>                                    contained, "--contained");
>>
>> -       /* Default to update mode if not specified */
>> -       if (!ref_action_str)
>> -               ref_action_str = "update";
>> -
>> -       /* Parse ref action mode */
>> -       if (!strcmp(ref_action_str, "update"))
>> -               ref_action = REF_ACTION_UPDATE;
>> -       else if (!strcmp(ref_action_str, "print"))
>> -               ref_action = REF_ACTION_PRINT;
>> -       else
>> -               die(_("unknown --ref-action mode '%s'"), ref_action_str);
>> +       /* Parse ref action mode from command line or config */
>> +       ref_action = get_ref_action_mode(repo, ref_action_str);
>>
>>          advance_name = xstrdup_or_null(advance_name_opt);
>>
>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>> index 123734b49f..2e90227c2f 100755
>> --- a/t/t3650-replay-basics.sh
>> +++ b/t/t3650-replay-basics.sh
>> @@ -219,7 +219,8 @@ test_expect_success 'merge.directoryRenames=false' '
>>
>>   test_expect_success 'default atomic behavior updates refs directly' '
>>          # Store original state for cleanup
>> -       test_when_finished "git branch -f topic2 topic1" &&
>> +       START=$(git rev-parse topic2) &&
>> +       test_when_finished "git branch -f topic2 $START" &&
> Yes, these three lines are a good fix, but they belong in the previous patch.


Right—the START/test_when_finished cleanup fixes should go in commit 2.


>
>>          # Test default atomic behavior (no output, refs updated)
>>          git replay --onto main topic1..topic2 >output &&
>> @@ -232,6 +233,10 @@ test_expect_success 'default atomic behavior updates refs directly' '
>>   '
>>
>>   test_expect_success 'atomic behavior in bare repository' '
>> +       # Store original state for cleanup
>> +       START=$(git rev-parse topic2) &&
>> +       test_when_finished "git branch -f topic2 $START" &&
> Yes, these three lines are good but they belong in a separate patch.


The bare repo cleanup fix should also go in commit 2.


>> +
>>          # Test atomic updates work in bare repo
>>          git -C bare replay --onto main topic1..topic2 >output &&
>>          test_must_be_empty output &&
>> @@ -245,4 +250,45 @@ test_expect_success 'atomic behavior in bare repository' '
>>          git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)
> And this line should be removed in the previous patch.


Will remove the manual reset line from commit 2.

Thanks for pointing out which fixes belong where!


>
>>   '
>>
>> +test_expect_success 'replay.refAction config option' '
>> +       # Store original state
>> +       START=$(git rev-parse topic2) &&
>> +       test_when_finished "git branch -f topic2 $START" &&
>> +
>> +       # Set config to print
>> +       test_config replay.refAction print &&
>> +       git replay --onto main topic1..topic2 >output &&
>> +       test_line_count = 1 output &&
>> +       test_grep "^update refs/heads/topic2 " output &&
>> +
>> +       # Reset and test update mode
>> +       git branch -f topic2 $START &&
>> +       test_config replay.refAction update &&
>> +       git replay --onto main topic1..topic2 >output &&
>> +       test_must_be_empty output &&
>> +
>> +       # Verify ref was updated
>> +       git log --format=%s topic2 >actual &&
>> +       test_write_lines E D M L B A >expect &&
>> +       test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'command-line --ref-action overrides config' '
>> +       # Store original state
>> +       START=$(git rev-parse topic2) &&
>> +       test_when_finished "git branch -f topic2 $START" &&
>> +
>> +       # Set config to update but use --ref-action=print
>> +       test_config replay.refAction update &&
>> +       git replay --ref-action=print --onto main topic1..topic2 >output &&
>> +       test_line_count = 1 output &&
>> +       test_grep "^update refs/heads/topic2 " output
>> +'
>> +
>> +test_expect_success 'invalid replay.refAction value' '
>> +       test_config replay.refAction invalid &&
>> +       test_must_fail git replay --onto main topic1..topic2 2>error &&
>> +       test_grep "invalid.*replay.refAction.*value" error
>> +'
>> +
>>   test_done
>> --
>> 2.51.0
> Looks good otherwise.


Thanks for careful review!


^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v7 0/3] replay: make atomic ref updates the default
  2025-10-30 19:19         ` [PATCH v6 0/3] replay: make atomic ref updates the default Siddharth Asthana
                             ` (3 preceding siblings ...)
  2025-10-31 18:51           ` [PATCH v6 0/3] replay: make atomic ref updates the default Elijah Newren
@ 2025-11-05 19:15           ` Siddharth Asthana
  2025-11-05 19:15             ` [PATCH v7 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
                               ` (4 more replies)
  4 siblings, 5 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-05 19:15 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

This is v7 of the git-replay atomic updates series.

This version addresses all feedback from v6 reviews. Thanks to Elijah,
Christian, and Phillip for the thorough reviews that helped refine the
implementation to Git standards.

## Changes in v7

**Improved commit message clarity**

Per Elijah's feedback, simplified commit messages by removing redundant
sections:
  - Removed "Implementation details" section (details visible in diff)
  - Shortened "Test suite changes" to focus on what's tested
  - Removed command-line precedence paragraph (obvious from code)
  - Removed "Examples" and configuration precedence sections

**Fixed test cleanup and isolation**

Following Elijah's suggestions:
  - Used test_when_finished with proper state restoration in atomic tests
  - Created separate test-atomic branch to avoid contaminating topic2
  - Fixed bare repository test to use START variable for cleanup
  - Improved test reliability by rebuilding expectations independently

**Extracted parse_ref_action_mode() to appropriate commit**

Per Christian's observation, moved the parse_ref_action_mode() helper
function from Commit 3 to Commit 2 where it's first used. This makes
the patch progression more logical.

**Fixed parameter naming consistency**

Following Christian's feedback, used consistent naming throughout:
  - ref_action (string parameter for command-line/config value)
  - ref_mode (enum variable for internal mode)
This eliminates confusion and improves code readability.

**Moved config reference to correct commit**

Per Elijah's note, moved the sentence about replay.refAction config
from Commit 2's documentation to Commit 3 where the config is actually
introduced.

**Enhanced reflog messages**

Following Phillip's suggestions for better user experience:
  - --advance mode: "replay --advance <branch-name>" (uses user input)
  - --onto mode: "replay --onto <commit-sha>" (precise commit reference)
Added comprehensive reflog testing to verify messages.

**Fixed indentation in Commit 3**

Corrected indentation within the while (decoration) loop per CI
feedback, adding proper tabs to nested if statements.

**Fixed coding style**

Per CI check-style feedback, removed braces from single-statement
if-else blocks following Git's CodingGuidelines.

**Split config tests for clarity**

Separated the replay.refAction config test into two distinct tests:
  - replay.refAction=print config option
  - replay.refAction=update config option
This improves test clarity and makes failures easier to diagnose.

## Technical Implementation

The atomic ref updates leverage Git's ref transaction API:
  - ref_store_transaction_begin() with default atomic behavior
  - ref_transaction_update() to stage each update
  - ref_transaction_commit() for atomic application

The helper functions provide clean separation:
  - parse_ref_action_mode(): Validates strings and converts to enum
  - get_ref_action_mode(): Implements command-line > config > default precedence
  - handle_ref_update(): Uses type-safe enum with switch statement

Reflog messages are constructed dynamically based on replay mode and
include either the branch name (--advance) or commit SHA (--onto) for
clear audit trails.

## Testing

All tests pass:
  - t3650-replay-basics.sh (22 tests pass)
  - Config tests verify proper precedence and error handling
  - Atomic behavior tests verify direct ref updates
  - Reflog tests verify descriptive messages
  - Backward compatibility maintained for pipeline workflow

CI results: https://gitlab.com/gitlab-org/git/-/pipelines/2140425748

Siddharth Asthana (3):
  replay: use die_for_incompatible_opt2() for option validation
  replay: make atomic ref updates the default behavior
  replay: add replay.refAction config option

 Documentation/config/replay.adoc |  11 +++
 Documentation/git-replay.adoc    |  63 ++++++++++-----
 builtin/replay.c                 | 133 ++++++++++++++++++++++++++++---
 t/t3650-replay-basics.sh         | 113 ++++++++++++++++++++++++--
 4 files changed, 277 insertions(+), 43 deletions(-)
 create mode 100644 Documentation/config/replay.adoc

Range-diff against v6:
1:  1f0fad0cac = 1:  9e4eab2df2 replay: use die_for_incompatible_opt2() for option validation
2:  bfc6188234 ! 2:  1602f6097e replay: make atomic ref updates the default behavior
    @@ Commit message
          * update (default): Update refs directly using an atomic transaction
          * print: Output update-ref commands for pipeline use
     
    -    Implementation details:
    -
    -    The atomic ref updates are implemented using Git's ref transaction API.
    -    In cmd_replay(), when not in `print` mode, we initialize a transaction
    -    using ref_store_transaction_begin() with the default atomic behavior.
    -    As commits are replayed, ref updates are staged into the transaction
    -    using ref_transaction_update(). Finally, ref_transaction_commit()
    -    applies all updates atomically—either all updates succeed or none do.
    -
    -    To avoid code duplication between the 'print' and 'update' modes, this
    -    commit extracts a handle_ref_update() helper function. This function
    -    takes the mode (as an enum) and either prints the update command or
    -    stages it into the transaction. Using an enum rather than passing the
    -    string around provides type safety and allows the compiler to catch
    -    typos. The switch statement makes it easy to add future modes.
    -
    -    The helper function signature:
    -
    -      static int handle_ref_update(enum ref_action_mode mode,
    -                                    struct ref_transaction *transaction,
    -                                    const char *refname,
    -                                    const struct object_id *new_oid,
    -                                    const struct object_id *old_oid,
    -                                    struct strbuf *err)
    -
    -    The enum is defined as:
    -
    -      enum ref_action_mode {
    -          REF_ACTION_UPDATE,
    -          REF_ACTION_PRINT
    -      };
    -
    -    The mode string is converted to enum immediately after parse_options()
    -    to avoid string comparisons throughout the codebase and provide compiler
    -    protection against typos.
    -
         Test suite changes:
     
         All existing tests that expected command output now use
    @@ Commit message
          - Equivalence between traditional pipeline and atomic updates
          - Real atomicity using a lock file to verify all-or-nothing guarantee
          - Test isolation using test_when_finished to clean up state
    -
    -    The bare repository tests were fixed to rebuild their expectations
    -    independently rather than comparing to previous test output, improving
    -    test reliability and isolation.
    +      - Reflog messages include replay mode and target
     
         A following commit will add a replay.refAction configuration
         option for users who prefer the traditional pipeline output as their
    @@ Documentation/git-replay.adoc: OPTIONS
     -commits, similar to the way how `git rebase --update-refs` updates
     -multiple branches in the affected range.
     +When `--onto` is specified, the branch(es) in the revision range will be
    -+updated to point at the new commits (or update commands will be printed
    -+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`
    ++updated to point at the new commits, similar to the way `git rebase --update-refs`
     +updates multiple branches in the affected range.
      
      --advance <branch>::
    @@ Documentation/git-replay.adoc: OPTIONS
     -will update the branch passed as an argument to `--advance` to point at
     -the new commits (in other words, this mimics a cherry-pick operation).
     +The history is replayed on top of the <branch> and <branch> is updated to
    -+point at the tip of the resulting history (or an update command will be
    -+printed if `--ref-action=print` is used). This is different from `--onto`,
    ++point at the tip of the resulting history. This is different from `--onto`,
     +which uses the target only as a starting point without updating it.
     +
     +--ref-action[=<mode>]::
    @@ Documentation/git-replay.adoc: OPTIONS
     +	* `print`: Output update-ref commands for pipeline use. This is the
     +	  traditional behavior where output can be piped to `git update-ref --stdin`.
     +--
    -++
    -+The default mode can be configured via the `replay.refAction` configuration variable.
      
      <revision-range>::
      	Range of commits to replay. More than one <revision-range> can
    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r
      	return create_commit(repo, result->tree, pickme, replayed_base);
      }
      
    ++static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
    ++{
    ++	if (!ref_action || !strcmp(ref_action, "update"))
    ++		return REF_ACTION_UPDATE;
    ++	if (!strcmp(ref_action, "print"))
    ++		return REF_ACTION_PRINT;
    ++	die(_("invalid %s value: '%s'"), source, ref_action);
    ++}
    ++
     +static int handle_ref_update(enum ref_action_mode mode,
     +			     struct ref_transaction *transaction,
     +			     const char *refname,
     +			     const struct object_id *new_oid,
     +			     const struct object_id *old_oid,
    ++			     const char *reflog_msg,
     +			     struct strbuf *err)
     +{
     +	switch (mode) {
    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r
     +		return 0;
     +	case REF_ACTION_UPDATE:
     +		return ref_transaction_update(transaction, refname, new_oid, old_oid,
    -+					      NULL, NULL, 0, "git replay", err);
    ++					      NULL, NULL, 0, reflog_msg, err);
     +	default:
     +		BUG("unknown ref_action_mode %d", mode);
     +	}
    @@ builtin/replay.c: int cmd_replay(int argc,
      	struct commit *onto = NULL;
      	const char *onto_name = NULL;
      	int contained = 0;
    -+	const char *ref_action_str = NULL;
    -+	enum ref_action_mode ref_action = REF_ACTION_UPDATE;
    ++	const char *ref_action = NULL;
    ++	enum ref_action_mode ref_mode = REF_ACTION_UPDATE;
      
      	struct rev_info revs;
      	struct commit *last_commit = NULL;
    @@ builtin/replay.c: int cmd_replay(int argc,
      	kh_oid_map_t *replayed_commits;
     +	struct ref_transaction *transaction = NULL;
     +	struct strbuf transaction_err = STRBUF_INIT;
    ++	struct strbuf reflog_msg = STRBUF_INIT;
      	int ret = 0;
      
     -	const char * const replay_usage[] = {
    @@ builtin/replay.c: int cmd_replay(int argc,
      			   N_("replay onto given commit")),
      		OPT_BOOL(0, "contained", &contained,
      			 N_("advance all branches contained in revision-range")),
    -+		OPT_STRING(0, "ref-action", &ref_action_str,
    ++		OPT_STRING(0, "ref-action", &ref_action,
     +			   N_("mode"),
     +			   N_("control ref update behavior (update|print)")),
      		OPT_END()
    @@ builtin/replay.c: int cmd_replay(int argc,
      	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
      				  contained, "--contained");
      
    -+	/* Default to update mode if not specified */
    -+	if (!ref_action_str)
    -+		ref_action_str = "update";
    -+
    -+	/* Validate ref-action mode */
    -+	if (!strcmp(ref_action_str, "update"))
    -+		ref_action = REF_ACTION_UPDATE;
    -+	else if (!strcmp(ref_action_str, "print"))
    -+		ref_action = REF_ACTION_PRINT;
    -+	else
    -+		die(_("unknown --ref-action mode '%s'"), ref_action_str);
    ++	/* Parse ref action mode */
    ++	if (ref_action)
    ++		ref_mode = parse_ref_action_mode(ref_action, "--ref-action");
     +
      	advance_name = xstrdup_or_null(advance_name_opt);
      
    @@ builtin/replay.c: int cmd_replay(int argc,
      	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
      			      &onto, &update_refs);
      
    ++	/* Build reflog message */
    ++	if (advance_name_opt)
    ++		strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
    ++	else
    ++		strbuf_addf(&reflog_msg, "replay --onto %s",
    ++			    oid_to_hex(&onto->object.oid));
    ++
     +	/* Initialize ref transaction if using update mode */
    -+	if (ref_action == REF_ACTION_UPDATE) {
    ++	if (ref_mode == REF_ACTION_UPDATE) {
     +		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
     +							  0, &transaction_err);
     +		if (!transaction) {
    @@ builtin/replay.c: int cmd_replay(int argc,
     -				       decoration->name,
     -				       oid_to_hex(&last_commit->object.oid),
     -				       oid_to_hex(&commit->object.oid));
    -+				if (handle_ref_update(ref_action, transaction,
    ++				if (handle_ref_update(ref_mode, transaction,
     +						      decoration->name,
     +						      &last_commit->object.oid,
     +						      &commit->object.oid,
    ++						      reflog_msg.buf,
     +						      &transaction_err) < 0) {
     +					ret = error(_("failed to update ref '%s': %s"),
     +						    decoration->name, transaction_err.buf);
    @@ builtin/replay.c: int cmd_replay(int argc,
     -		       advance_name,
     -		       oid_to_hex(&last_commit->object.oid),
     -		       oid_to_hex(&onto->object.oid));
    -+		if (handle_ref_update(ref_action, transaction, advance_name,
    ++		if (handle_ref_update(ref_mode, transaction, advance_name,
     +				      &last_commit->object.oid,
     +				      &onto->object.oid,
    ++				      reflog_msg.buf,
     +				      &transaction_err) < 0) {
     +			ret = error(_("failed to update ref '%s': %s"),
     +				    advance_name, transaction_err.buf);
    @@ builtin/replay.c: int cmd_replay(int argc,
     +	if (transaction)
     +		ref_transaction_free(transaction);
     +	strbuf_release(&transaction_err);
    ++	strbuf_release(&reflog_msg);
      	release_revisions(&revs);
      	free(advance_name);
      
    @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '
      '
      
     +test_expect_success 'default atomic behavior updates refs directly' '
    -+	# Store original state for cleanup
    -+	test_when_finished "git branch -f topic2 topic1" &&
    ++	# Use a separate branch to avoid contaminating topic2 for later tests
    ++	git branch test-atomic topic2 &&
    ++	test_when_finished "git branch -D test-atomic" &&
     +
     +	# Test default atomic behavior (no output, refs updated)
    -+	git replay --onto main topic1..topic2 >output &&
    ++	git replay --onto main topic1..test-atomic >output &&
     +	test_must_be_empty output &&
     +
     +	# Verify ref was updated
    -+	git log --format=%s topic2 >actual &&
    ++	git log --format=%s test-atomic >actual &&
     +	test_write_lines E D M L B A >expect &&
    -+	test_cmp expect actual
    ++	test_cmp expect actual &&
    ++
    ++	# Verify reflog message includes SHA of onto commit
    ++	git reflog test-atomic -1 --format=%gs >reflog-msg &&
    ++	ONTO_SHA=$(git rev-parse main) &&
    ++	echo "replay --onto $ONTO_SHA" >expect-reflog &&
    ++	test_cmp expect-reflog reflog-msg
     +'
     +
     +test_expect_success 'atomic behavior in bare repository' '
    ++	# Store original state for cleanup
    ++	START=$(git -C bare rev-parse topic2) &&
    ++	test_when_finished "git -C bare update-ref refs/heads/topic2 $START" &&
    ++
     +	# Test atomic updates work in bare repo
     +	git -C bare replay --onto main topic1..topic2 >output &&
     +	test_must_be_empty output &&
    @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '
     +	# Verify ref was updated in bare repo
     +	git -C bare log --format=%s topic2 >actual &&
     +	test_write_lines E D M L B A >expect &&
    -+	test_cmp expect actual &&
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success 'reflog message for --advance mode' '
    ++	# Store original state
    ++	START=$(git rev-parse main) &&
    ++	test_when_finished "git update-ref refs/heads/main $START" &&
    ++
    ++	# Test --advance mode reflog message
    ++	git replay --advance main topic1..topic2 >output &&
    ++	test_must_be_empty output &&
     +
    -+	# Reset for other tests
    -+	git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)
    ++	# Verify reflog message includes --advance and branch name
    ++	git reflog main -1 --format=%gs >reflog-msg &&
    ++	echo "replay --advance main" >expect-reflog &&
    ++	test_cmp expect-reflog reflog-msg
     +'
     +
      test_done
-:  ---------- > 3:  b7ebe1f534 replay: add replay.refAction config option

-- 
2.51.0

base-commit: a99f379adf8a0b4c7c4f8f0b2e5e6e7e8e9e0e1e

Thanks
- Siddharth

^ permalink raw reply	[flat|nested] 129+ messages in thread

* [PATCH v7 1/3] replay: use die_for_incompatible_opt2() for option validation
  2025-11-05 19:15           ` [PATCH v7 " Siddharth Asthana
@ 2025-11-05 19:15             ` Siddharth Asthana
  2025-11-05 19:16             ` [PATCH v7 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
                               ` (3 subsequent siblings)
  4 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-05 19:15 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

In preparation for adding the --ref-action option, convert option
validation to use die_for_incompatible_opt2(). This helper provides
standardized error messages for mutually exclusive options.

The following commit introduces --ref-action which will be incompatible
with certain other options. Using die_for_incompatible_opt2() now means
that commit can cleanly add its validation using the same pattern,
keeping the validation logic consistent and maintainable.

This also aligns git-replay's option handling with how other Git commands
manage option conflicts, using the established die_for_incompatible_opt*()
helper family.

Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 builtin/replay.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/builtin/replay.c b/builtin/replay.c
index 6172c8aacc..b64fc72063 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -330,9 +330,9 @@ int cmd_replay(int argc,
 		usage_with_options(replay_usage, replay_options);
 	}
 
-	if (advance_name_opt && contained)
-		die(_("options '%s' and '%s' cannot be used together"),
-		    "--advance", "--contained");
+	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
+				  contained, "--contained");
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH v7 2/3] replay: make atomic ref updates the default behavior
  2025-11-05 19:15           ` [PATCH v7 " Siddharth Asthana
  2025-11-05 19:15             ` [PATCH v7 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
@ 2025-11-05 19:16             ` Siddharth Asthana
  2025-11-05 19:16             ` [PATCH v7 3/3] replay: add replay.refAction config option Siddharth Asthana
                               ` (2 subsequent siblings)
  4 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-05 19:16 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

The git replay command currently outputs update commands that can be
piped to update-ref to achieve a rebase, e.g.

  git replay --onto main topic1..topic2 | git update-ref --stdin

This separation had advantages for three special cases:
  * it made testing easy (when state isn't modified from one step to
    the next, you don't need to make temporary branches or have undo
    commands, or try to track the changes)
  * it provided a natural can-it-rebase-cleanly (and what would it
    rebase to) capability without automatically updating refs, similar
    to a --dry-run
  * it provided a natural low-level tool for the suite of hash-object,
    mktree, commit-tree, mktag, merge-tree, and update-ref, allowing
    users to have another building block for experimentation and making
    new tools

However, it should be noted that all three of these are somewhat
special cases; users, whether on the client or server side, would
almost certainly find it more ergonomic to simply have the updating
of refs be the default.

For server-side operations in particular, the pipeline architecture
creates process coordination overhead. Server implementations that need
to perform rebases atomically must maintain additional code to:

  1. Spawn and manage a pipeline between git-replay and git-update-ref
  2. Coordinate stdout/stderr streams across the pipe boundary
  3. Handle partial failure states if the pipeline breaks mid-execution
  4. Parse and validate the update-ref command output

Change the default behavior to update refs directly, and atomically (at
least to the extent supported by the refs backend in use). This
eliminates the process coordination overhead for the common case.

For users needing the traditional pipeline workflow, add a new
--ref-action=<mode> option that preserves the original behavior:

  git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin

The mode can be:
  * update (default): Update refs directly using an atomic transaction
  * print: Output update-ref commands for pipeline use

Test suite changes:

All existing tests that expected command output now use
--ref-action=print to preserve their original behavior. This keeps
the tests valid while allowing them to verify that the pipeline workflow
still works correctly.

New tests were added to verify:
  - Default atomic behavior (no output, refs updated directly)
  - Bare repository support (server-side use case)
  - Equivalence between traditional pipeline and atomic updates
  - Real atomicity using a lock file to verify all-or-nothing guarantee
  - Test isolation using test_when_finished to clean up state
  - Reflog messages include replay mode and target

A following commit will add a replay.refAction configuration
option for users who prefer the traditional pipeline output as their
default behavior.

Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/git-replay.adoc |  61 ++++++++++++-------
 builtin/replay.c              | 111 +++++++++++++++++++++++++++++++---
 t/t3650-replay-basics.sh      |  67 +++++++++++++++++---
 3 files changed, 199 insertions(+), 40 deletions(-)

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 0b12bf8aa4..2ef74ddb12 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...
 
 DESCRIPTION
 -----------
 
 Takes ranges of commits and replays them onto a new location. Leaves
-the working tree and the index untouched, and updates no references.
-The output of this command is meant to be used as input to
-`git update-ref --stdin`, which would update the relevant branches
+the working tree and the index untouched. By default, updates the
+relevant references using an atomic transaction (all refs update or
+none). Use `--ref-action=print` to avoid automatic ref updates and
+instead get update commands that can be piped to `git update-ref --stdin`
 (see the OUTPUT section below).
 
 THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
@@ -29,18 +30,27 @@ OPTIONS
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
-When `--onto` is specified, the update-ref command(s) in the output will
-update the branch(es) in the revision range to point at the new
-commits, similar to the way how `git rebase --update-refs` updates
-multiple branches in the affected range.
+When `--onto` is specified, the branch(es) in the revision range will be
+updated to point at the new commits, similar to the way `git rebase --update-refs`
+updates multiple branches in the affected range.
 
 --advance <branch>::
 	Starting point at which to create the new commits; must be a
 	branch name.
 +
-When `--advance` is specified, the update-ref command(s) in the output
-will update the branch passed as an argument to `--advance` to point at
-the new commits (in other words, this mimics a cherry-pick operation).
+The history is replayed on top of the <branch> and <branch> is updated to
+point at the tip of the resulting history. This is different from `--onto`,
+which uses the target only as a starting point without updating it.
+
+--ref-action[=<mode>]::
+	Control how references are updated. The mode can be:
++
+--
+	* `update` (default): Update refs directly using an atomic transaction.
+	  All refs are updated or none are (all-or-nothing behavior).
+	* `print`: Output update-ref commands for pipeline use. This is the
+	  traditional behavior where output can be piped to `git update-ref --stdin`.
+--
 
 <revision-range>::
 	Range of commits to replay. More than one <revision-range> can
@@ -54,8 +64,11 @@ include::rev-list-options.adoc[]
 OUTPUT
 ------
 
-When there are no conflicts, the output of this command is usable as
-input to `git update-ref --stdin`.  It is of the form:
+By default, or with `--ref-action=update`, this command produces no output on
+success, as refs are updated directly using an atomic transaction.
+
+When using `--ref-action=print`, the output is usable as input to
+`git update-ref --stdin`. It is of the form:
 
 	update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
 	update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
@@ -81,6 +94,14 @@ To simply rebase `mybranch` onto `target`:
 
 ------------
 $ git replay --onto target origin/main..mybranch
+------------
+
+The refs are updated atomically and no output is produced on success.
+
+To see what would be updated without actually updating:
+
+------------
+$ git replay --ref-action=print --onto target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
@@ -88,33 +109,29 @@ To cherry-pick the commits from mybranch onto target:
 
 ------------
 $ git replay --advance target origin/main..mybranch
-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}
 ------------
 
 Note that the first two examples replay the exact same commits and on
 top of the exact same new base, they only differ in that the first
-provides instructions to make mybranch point at the new commits and
-the second provides instructions to make target point at them.
+updates mybranch to point at the new commits and the second updates
+target to point at them.
 
 What if you have a stack of branches, one depending upon another, and
 you'd really like to rebase the whole set?
 
 ------------
 $ git replay --contained --onto origin/main origin/main..tipbranch
-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}
 ------------
 
+All three branches (`branch1`, `branch2`, and `tipbranch`) are updated
+atomically.
+
 When calling `git replay`, one does not need to specify a range of
 commits to replay using the syntax `A..B`; any range expression will
 do:
 
 ------------
 $ git replay --onto origin/main ^base branch1 branch2 branch3
-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}
-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}
-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}
 ------------
 
 This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
diff --git a/builtin/replay.c b/builtin/replay.c
index b64fc72063..94e60b5b10 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -20,6 +20,11 @@
 #include <oidset.h>
 #include <tree.h>
 
+enum ref_action_mode {
+	REF_ACTION_UPDATE,
+	REF_ACTION_PRINT,
+};
+
 static const char *short_commit_name(struct repository *repo,
 				     struct commit *commit)
 {
@@ -284,6 +289,38 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	return create_commit(repo, result->tree, pickme, replayed_base);
 }
 
+static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
+{
+	if (!ref_action || !strcmp(ref_action, "update"))
+		return REF_ACTION_UPDATE;
+	if (!strcmp(ref_action, "print"))
+		return REF_ACTION_PRINT;
+	die(_("invalid %s value: '%s'"), source, ref_action);
+}
+
+static int handle_ref_update(enum ref_action_mode mode,
+			     struct ref_transaction *transaction,
+			     const char *refname,
+			     const struct object_id *new_oid,
+			     const struct object_id *old_oid,
+			     const char *reflog_msg,
+			     struct strbuf *err)
+{
+	switch (mode) {
+	case REF_ACTION_PRINT:
+		printf("update %s %s %s\n",
+		       refname,
+		       oid_to_hex(new_oid),
+		       oid_to_hex(old_oid));
+		return 0;
+	case REF_ACTION_UPDATE:
+		return ref_transaction_update(transaction, refname, new_oid, old_oid,
+					      NULL, NULL, 0, reflog_msg, err);
+	default:
+		BUG("unknown ref_action_mode %d", mode);
+	}
+}
+
 int cmd_replay(int argc,
 	       const char **argv,
 	       const char *prefix,
@@ -294,6 +331,8 @@ int cmd_replay(int argc,
 	struct commit *onto = NULL;
 	const char *onto_name = NULL;
 	int contained = 0;
+	const char *ref_action = NULL;
+	enum ref_action_mode ref_mode = REF_ACTION_UPDATE;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -302,12 +341,15 @@ int cmd_replay(int argc,
 	struct merge_result result;
 	struct strset *update_refs = NULL;
 	kh_oid_map_t *replayed_commits;
+	struct ref_transaction *transaction = NULL;
+	struct strbuf transaction_err = STRBUF_INIT;
+	struct strbuf reflog_msg = STRBUF_INIT;
 	int ret = 0;
 
-	const char * const replay_usage[] = {
+	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto <newbase> | --advance <branch>) "
-		   "<revision-range>..."),
+		   "[--ref-action[=<mode>]] <revision-range>..."),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -319,6 +361,9 @@ int cmd_replay(int argc,
 			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &contained,
 			 N_("advance all branches contained in revision-range")),
+		OPT_STRING(0, "ref-action", &ref_action,
+			   N_("mode"),
+			   N_("control ref update behavior (update|print)")),
 		OPT_END()
 	};
 
@@ -333,6 +378,10 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
+	/* Parse ref action mode */
+	if (ref_action)
+		ref_mode = parse_ref_action_mode(ref_action, "--ref-action");
+
 	advance_name = xstrdup_or_null(advance_name_opt);
 
 	repo_init_revisions(repo, &revs, prefix);
@@ -389,6 +438,24 @@ int cmd_replay(int argc,
 	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
 			      &onto, &update_refs);
 
+	/* Build reflog message */
+	if (advance_name_opt)
+		strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
+	else
+		strbuf_addf(&reflog_msg, "replay --onto %s",
+			    oid_to_hex(&onto->object.oid));
+
+	/* Initialize ref transaction if using update mode */
+	if (ref_mode == REF_ACTION_UPDATE) {
+		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
+							  0, &transaction_err);
+		if (!transaction) {
+			ret = error(_("failed to begin ref transaction: %s"),
+				    transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
 	if (!onto) /* FIXME: Should handle replaying down to root commit */
 		die("Replaying down to root commit is not supported yet!");
 
@@ -434,10 +501,16 @@ int cmd_replay(int argc,
 			if (decoration->type == DECORATION_REF_LOCAL &&
 			    (contained || strset_contains(update_refs,
 							  decoration->name))) {
-				printf("update %s %s %s\n",
-				       decoration->name,
-				       oid_to_hex(&last_commit->object.oid),
-				       oid_to_hex(&commit->object.oid));
+				if (handle_ref_update(ref_mode, transaction,
+						      decoration->name,
+						      &last_commit->object.oid,
+						      &commit->object.oid,
+						      reflog_msg.buf,
+						      &transaction_err) < 0) {
+					ret = error(_("failed to update ref '%s': %s"),
+						    decoration->name, transaction_err.buf);
+					goto cleanup;
+				}
 			}
 			decoration = decoration->next;
 		}
@@ -445,10 +518,24 @@ int cmd_replay(int argc,
 
 	/* In --advance mode, advance the target ref */
 	if (result.clean == 1 && advance_name) {
-		printf("update %s %s %s\n",
-		       advance_name,
-		       oid_to_hex(&last_commit->object.oid),
-		       oid_to_hex(&onto->object.oid));
+		if (handle_ref_update(ref_mode, transaction, advance_name,
+				      &last_commit->object.oid,
+				      &onto->object.oid,
+				      reflog_msg.buf,
+				      &transaction_err) < 0) {
+			ret = error(_("failed to update ref '%s': %s"),
+				    advance_name, transaction_err.buf);
+			goto cleanup;
+		}
+	}
+
+	/* Commit the ref transaction if we have one */
+	if (transaction && result.clean == 1) {
+		if (ref_transaction_commit(transaction, &transaction_err)) {
+			ret = error(_("failed to commit ref transaction: %s"),
+				    transaction_err.buf);
+			goto cleanup;
+		}
 	}
 
 	merge_finalize(&merge_opt, &result);
@@ -460,6 +547,10 @@ int cmd_replay(int argc,
 	ret = result.clean;
 
 cleanup:
+	if (transaction)
+		ref_transaction_free(transaction);
+	strbuf_release(&transaction_err);
+	strbuf_release(&reflog_msg);
 	release_revisions(&revs);
 	free(advance_name);
 
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 58b3759935..ec79234c80 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git replay --onto main topic1..topic2 >result &&
+	git replay --ref-action=print --onto main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 '
 
 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
-	git -C bare replay --onto main topic1..topic2 >result-bare &&
+	git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
@@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
 	# 4th field of result is hash for main instead of hash for topic2
 
-	git replay --advance main topic1..topic2 >result &&
+	git replay --ref-action=print --advance main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 '
 
 test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
-	git -C bare replay --advance main topic1..topic2 >result-bare &&
+	git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
@@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 '
 
 test_expect_success 'using replay to also rebase a contained branch' '
-	git replay --contained --onto main main..topic3 >result &&
+	git replay --ref-action=print --contained --onto main main..topic3 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '
 '
 
 test_expect_success 'using replay on bare repo to also rebase a contained branch' '
-	git -C bare replay --contained --onto main main..topic3 >result-bare &&
+	git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&
 	test_cmp expect result-bare
 '
 
 test_expect_success 'using replay to rebase multiple divergent branches' '
-	git replay --onto main ^topic1 topic2 topic4 >result &&
+	git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
 '
 
 test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
-	git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&
+	git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
 
 	test_line_count = 4 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -217,4 +217,55 @@ test_expect_success 'merge.directoryRenames=false' '
 		--onto rename-onto rename-onto..rename-from
 '
 
+test_expect_success 'default atomic behavior updates refs directly' '
+	# Use a separate branch to avoid contaminating topic2 for later tests
+	git branch test-atomic topic2 &&
+	test_when_finished "git branch -D test-atomic" &&
+
+	# Test default atomic behavior (no output, refs updated)
+	git replay --onto main topic1..test-atomic >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated
+	git log --format=%s test-atomic >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual &&
+
+	# Verify reflog message includes SHA of onto commit
+	git reflog test-atomic -1 --format=%gs >reflog-msg &&
+	ONTO_SHA=$(git rev-parse main) &&
+	echo "replay --onto $ONTO_SHA" >expect-reflog &&
+	test_cmp expect-reflog reflog-msg
+'
+
+test_expect_success 'atomic behavior in bare repository' '
+	# Store original state for cleanup
+	START=$(git -C bare rev-parse topic2) &&
+	test_when_finished "git -C bare update-ref refs/heads/topic2 $START" &&
+
+	# Test atomic updates work in bare repo
+	git -C bare replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated in bare repo
+	git -C bare log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'reflog message for --advance mode' '
+	# Store original state
+	START=$(git rev-parse main) &&
+	test_when_finished "git update-ref refs/heads/main $START" &&
+
+	# Test --advance mode reflog message
+	git replay --advance main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify reflog message includes --advance and branch name
+	git reflog main -1 --format=%gs >reflog-msg &&
+	echo "replay --advance main" >expect-reflog &&
+	test_cmp expect-reflog reflog-msg
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* [PATCH v7 3/3] replay: add replay.refAction config option
  2025-11-05 19:15           ` [PATCH v7 " Siddharth Asthana
  2025-11-05 19:15             ` [PATCH v7 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
  2025-11-05 19:16             ` [PATCH v7 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
@ 2025-11-05 19:16             ` Siddharth Asthana
  2025-11-06 19:32             ` [PATCH v7 0/3] replay: make atomic ref updates the default Elijah Newren
  2025-11-07 15:48             ` Phillip Wood
  4 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-05 19:16 UTC (permalink / raw)
  To: git
  Cc: christian.couder, phillip.wood123, phillip.wood, newren, gitster,
	ps, karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin, Siddharth Asthana

Add a configuration variable to control the default behavior of git replay
for updating references. This allows users who prefer the traditional
pipeline output to set it once in their config instead of passing
--ref-action=print with every command.

The config variable uses string values that mirror the behavior modes:
  * replay.refAction = update (default): atomic ref updates
  * replay.refAction = print: output commands for pipeline

Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Elijah Newren <newren@gmail.com>
Helped-by: Christian Couder <christian.couder@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>
---
 Documentation/config/replay.adoc | 11 ++++++++
 Documentation/git-replay.adoc    |  2 ++
 builtin/replay.c                 | 24 ++++++++++++++---
 t/t3650-replay-basics.sh         | 46 ++++++++++++++++++++++++++++++++
 4 files changed, 79 insertions(+), 4 deletions(-)
 create mode 100644 Documentation/config/replay.adoc

diff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc
new file mode 100644
index 0000000000..7d549d2f0e
--- /dev/null
+++ b/Documentation/config/replay.adoc
@@ -0,0 +1,11 @@
+replay.refAction::
+	Specifies the default mode for handling reference updates in
+	`git replay`. The value can be:
++
+--
+	* `update`: Update refs directly using an atomic transaction (default behavior).
+	* `print`: Output update-ref commands for pipeline use.
+--
++
+This setting can be overridden with the `--ref-action` command-line option.
+When not configured, `git replay` defaults to `update` mode.
diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 2ef74ddb12..dcb26e8a8e 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -51,6 +51,8 @@ which uses the target only as a starting point without updating it.
 	* `print`: Output update-ref commands for pipeline use. This is the
 	  traditional behavior where output can be piped to `git update-ref --stdin`.
 --
++
+The default mode can be configured via the `replay.refAction` configuration variable.
 
 <revision-range>::
 	Range of commits to replay. More than one <revision-range> can
diff --git a/builtin/replay.c b/builtin/replay.c
index 94e60b5b10..6606a2c94b 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -8,6 +8,7 @@
 #include "git-compat-util.h"
 
 #include "builtin.h"
+#include "config.h"
 #include "environment.h"
 #include "hex.h"
 #include "lockfile.h"
@@ -298,6 +299,22 @@ static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const
 	die(_("invalid %s value: '%s'"), source, ref_action);
 }
 
+static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action)
+{
+	const char *config_value = NULL;
+
+	/* Command line option takes precedence */
+	if (ref_action)
+		return parse_ref_action_mode(ref_action, "--ref-action");
+
+	/* Check config value */
+	if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
+		return parse_ref_action_mode(config_value, "replay.refAction");
+
+	/* Default to update mode */
+	return REF_ACTION_UPDATE;
+}
+
 static int handle_ref_update(enum ref_action_mode mode,
 			     struct ref_transaction *transaction,
 			     const char *refname,
@@ -332,7 +349,7 @@ int cmd_replay(int argc,
 	const char *onto_name = NULL;
 	int contained = 0;
 	const char *ref_action = NULL;
-	enum ref_action_mode ref_mode = REF_ACTION_UPDATE;
+	enum ref_action_mode ref_mode;
 
 	struct rev_info revs;
 	struct commit *last_commit = NULL;
@@ -378,9 +395,8 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt2(!!advance_name_opt, "--advance",
 				  contained, "--contained");
 
-	/* Parse ref action mode */
-	if (ref_action)
-		ref_mode = parse_ref_action_mode(ref_action, "--ref-action");
+	/* Parse ref action mode from command line or config */
+	ref_mode = get_ref_action_mode(repo, ref_action);
 
 	advance_name = xstrdup_or_null(advance_name_opt);
 
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index ec79234c80..cf3aacf355 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -268,4 +268,50 @@ test_expect_success 'reflog message for --advance mode' '
 	test_cmp expect-reflog reflog-msg
 '
 
+test_expect_success 'replay.refAction=print config option' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
+
+	# Test with config set to print
+	test_config replay.refAction print &&
+	git replay --onto main topic1..topic2 >output &&
+	test_line_count = 1 output &&
+	test_grep "^update refs/heads/topic2 " output
+'
+
+test_expect_success 'replay.refAction=update config option' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
+
+	# Test with config set to update
+	test_config replay.refAction update &&
+	git replay --onto main topic1..topic2 >output &&
+	test_must_be_empty output &&
+
+	# Verify ref was updated
+	git log --format=%s topic2 >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'command-line --ref-action overrides config' '
+	# Store original state
+	START=$(git rev-parse topic2) &&
+	test_when_finished "git branch -f topic2 $START" &&
+
+	# Set config to update but use --ref-action=print
+	test_config replay.refAction update &&
+	git replay --ref-action=print --onto main topic1..topic2 >output &&
+	test_line_count = 1 output &&
+	test_grep "^update refs/heads/topic2 " output
+'
+
+test_expect_success 'invalid replay.refAction value' '
+	test_config replay.refAction invalid &&
+	test_must_fail git replay --onto main topic1..topic2 2>error &&
+	test_grep "invalid.*replay.refAction.*value" error
+'
+
 test_done
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 129+ messages in thread

* Re: [PATCH v7 0/3] replay: make atomic ref updates the default
  2025-11-05 19:15           ` [PATCH v7 " Siddharth Asthana
                               ` (2 preceding siblings ...)
  2025-11-05 19:16             ` [PATCH v7 3/3] replay: add replay.refAction config option Siddharth Asthana
@ 2025-11-06 19:32             ` Elijah Newren
  2025-11-08 13:22               ` Siddharth Asthana
  2025-11-07 15:48             ` Phillip Wood
  4 siblings, 1 reply; 129+ messages in thread
From: Elijah Newren @ 2025-11-06 19:32 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Wed, Nov 5, 2025 at 11:17 AM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:
>
> This is v7 of the git-replay atomic updates series.
>
> This version addresses all feedback from v6 reviews. Thanks to Elijah,
> Christian, and Phillip for the thorough reviews that helped refine the
> implementation to Git standards.
>
> ## Changes in v7
>
> **Improved commit message clarity**
>
> Per Elijah's feedback, simplified commit messages by removing redundant
> sections:
>   - Removed "Implementation details" section (details visible in diff)
>   - Shortened "Test suite changes" to focus on what's tested
>   - Removed command-line precedence paragraph (obvious from code)
>   - Removed "Examples" and configuration precedence sections
>
> **Fixed test cleanup and isolation**
>
> Following Elijah's suggestions:
>   - Used test_when_finished with proper state restoration in atomic tests
>   - Created separate test-atomic branch to avoid contaminating topic2
>   - Fixed bare repository test to use START variable for cleanup
>   - Improved test reliability by rebuilding expectations independently
>
> **Extracted parse_ref_action_mode() to appropriate commit**
>
> Per Christian's observation, moved the parse_ref_action_mode() helper
> function from Commit 3 to Commit 2 where it's first used. This makes
> the patch progression more logical.
>
> **Fixed parameter naming consistency**
>
> Following Christian's feedback, used consistent naming throughout:
>   - ref_action (string parameter for command-line/config value)
>   - ref_mode (enum variable for internal mode)
> This eliminates confusion and improves code readability.
>
> **Moved config reference to correct commit**
>
> Per Elijah's note, moved the sentence about replay.refAction config
> from Commit 2's documentation to Commit 3 where the config is actually
> introduced.
>
> **Enhanced reflog messages**
>
> Following Phillip's suggestions for better user experience:
>   - --advance mode: "replay --advance <branch-name>" (uses user input)
>   - --onto mode: "replay --onto <commit-sha>" (precise commit reference)
> Added comprehensive reflog testing to verify messages.
>
> **Fixed indentation in Commit 3**
>
> Corrected indentation within the while (decoration) loop per CI
> feedback, adding proper tabs to nested if statements.
>
> **Fixed coding style**
>
> Per CI check-style feedback, removed braces from single-statement
> if-else blocks following Git's CodingGuidelines.
>
> **Split config tests for clarity**
>
> Separated the replay.refAction config test into two distinct tests:
>   - replay.refAction=print config option
>   - replay.refAction=update config option
> This improves test clarity and makes failures easier to diagnose.
>
> ## Technical Implementation
>
> The atomic ref updates leverage Git's ref transaction API:
>   - ref_store_transaction_begin() with default atomic behavior
>   - ref_transaction_update() to stage each update
>   - ref_transaction_commit() for atomic application
>
> The helper functions provide clean separation:
>   - parse_ref_action_mode(): Validates strings and converts to enum
>   - get_ref_action_mode(): Implements command-line > config > default precedence
>   - handle_ref_update(): Uses type-safe enum with switch statement
>
> Reflog messages are constructed dynamically based on replay mode and
> include either the branch name (--advance) or commit SHA (--onto) for
> clear audit trails.
>
> ## Testing
>
> All tests pass:
>   - t3650-replay-basics.sh (22 tests pass)
>   - Config tests verify proper precedence and error handling
>   - Atomic behavior tests verify direct ref updates
>   - Reflog tests verify descriptive messages
>   - Backward compatibility maintained for pipeline workflow
>
> CI results: https://gitlab.com/gitlab-org/git/-/pipelines/2140425748
>
> Siddharth Asthana (3):
>   replay: use die_for_incompatible_opt2() for option validation
>   replay: make atomic ref updates the default behavior
>   replay: add replay.refAction config option
>
>  Documentation/config/replay.adoc |  11 +++
>  Documentation/git-replay.adoc    |  63 ++++++++++-----
>  builtin/replay.c                 | 133 ++++++++++++++++++++++++++++---
>  t/t3650-replay-basics.sh         | 113 ++++++++++++++++++++++++--
>  4 files changed, 277 insertions(+), 43 deletions(-)
>  create mode 100644 Documentation/config/replay.adoc
>
> Range-diff against v6:
> 1:  1f0fad0cac = 1:  9e4eab2df2 replay: use die_for_incompatible_opt2() for option validation
> 2:  bfc6188234 ! 2:  1602f6097e replay: make atomic ref updates the default behavior
>     @@ Commit message
>           * update (default): Update refs directly using an atomic transaction
>           * print: Output update-ref commands for pipeline use
>
>     -    Implementation details:
>     -
>     -    The atomic ref updates are implemented using Git's ref transaction API.
>     -    In cmd_replay(), when not in `print` mode, we initialize a transaction
>     -    using ref_store_transaction_begin() with the default atomic behavior.
>     -    As commits are replayed, ref updates are staged into the transaction
>     -    using ref_transaction_update(). Finally, ref_transaction_commit()
>     -    applies all updates atomically—either all updates succeed or none do.
>     -
>     -    To avoid code duplication between the 'print' and 'update' modes, this
>     -    commit extracts a handle_ref_update() helper function. This function
>     -    takes the mode (as an enum) and either prints the update command or
>     -    stages it into the transaction. Using an enum rather than passing the
>     -    string around provides type safety and allows the compiler to catch
>     -    typos. The switch statement makes it easy to add future modes.
>     -
>     -    The helper function signature:
>     -
>     -      static int handle_ref_update(enum ref_action_mode mode,
>     -                                    struct ref_transaction *transaction,
>     -                                    const char *refname,
>     -                                    const struct object_id *new_oid,
>     -                                    const struct object_id *old_oid,
>     -                                    struct strbuf *err)
>     -
>     -    The enum is defined as:
>     -
>     -      enum ref_action_mode {
>     -          REF_ACTION_UPDATE,
>     -          REF_ACTION_PRINT
>     -      };
>     -
>     -    The mode string is converted to enum immediately after parse_options()
>     -    to avoid string comparisons throughout the codebase and provide compiler
>     -    protection against typos.
>     -
>          Test suite changes:
>
>          All existing tests that expected command output now use
>     @@ Commit message
>           - Equivalence between traditional pipeline and atomic updates
>           - Real atomicity using a lock file to verify all-or-nothing guarantee
>           - Test isolation using test_when_finished to clean up state
>     -
>     -    The bare repository tests were fixed to rebuild their expectations
>     -    independently rather than comparing to previous test output, improving
>     -    test reliability and isolation.
>     +      - Reflog messages include replay mode and target
>
>          A following commit will add a replay.refAction configuration
>          option for users who prefer the traditional pipeline output as their
>     @@ Documentation/git-replay.adoc: OPTIONS
>      -commits, similar to the way how `git rebase --update-refs` updates
>      -multiple branches in the affected range.
>      +When `--onto` is specified, the branch(es) in the revision range will be
>     -+updated to point at the new commits (or update commands will be printed
>     -+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`
>     ++updated to point at the new commits, similar to the way `git rebase --update-refs`
>      +updates multiple branches in the affected range.
>
>       --advance <branch>::
>     @@ Documentation/git-replay.adoc: OPTIONS
>      -will update the branch passed as an argument to `--advance` to point at
>      -the new commits (in other words, this mimics a cherry-pick operation).
>      +The history is replayed on top of the <branch> and <branch> is updated to
>     -+point at the tip of the resulting history (or an update command will be
>     -+printed if `--ref-action=print` is used). This is different from `--onto`,
>     ++point at the tip of the resulting history. This is different from `--onto`,
>      +which uses the target only as a starting point without updating it.
>      +
>      +--ref-action[=<mode>]::
>     @@ Documentation/git-replay.adoc: OPTIONS
>      +  * `print`: Output update-ref commands for pipeline use. This is the
>      +    traditional behavior where output can be piped to `git update-ref --stdin`.
>      +--
>     -++
>     -+The default mode can be configured via the `replay.refAction` configuration variable.
>
>       <revision-range>::
>         Range of commits to replay. More than one <revision-range> can
>     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r
>         return create_commit(repo, result->tree, pickme, replayed_base);
>       }
>
>     ++static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
>     ++{
>     ++  if (!ref_action || !strcmp(ref_action, "update"))
>     ++          return REF_ACTION_UPDATE;
>     ++  if (!strcmp(ref_action, "print"))
>     ++          return REF_ACTION_PRINT;
>     ++  die(_("invalid %s value: '%s'"), source, ref_action);
>     ++}
>     ++
>      +static int handle_ref_update(enum ref_action_mode mode,
>      +                       struct ref_transaction *transaction,
>      +                       const char *refname,
>      +                       const struct object_id *new_oid,
>      +                       const struct object_id *old_oid,
>     ++                       const char *reflog_msg,
>      +                       struct strbuf *err)
>      +{
>      +  switch (mode) {
>     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r
>      +          return 0;
>      +  case REF_ACTION_UPDATE:
>      +          return ref_transaction_update(transaction, refname, new_oid, old_oid,
>     -+                                        NULL, NULL, 0, "git replay", err);
>     ++                                        NULL, NULL, 0, reflog_msg, err);
>      +  default:
>      +          BUG("unknown ref_action_mode %d", mode);
>      +  }
>     @@ builtin/replay.c: int cmd_replay(int argc,
>         struct commit *onto = NULL;
>         const char *onto_name = NULL;
>         int contained = 0;
>     -+  const char *ref_action_str = NULL;
>     -+  enum ref_action_mode ref_action = REF_ACTION_UPDATE;
>     ++  const char *ref_action = NULL;
>     ++  enum ref_action_mode ref_mode = REF_ACTION_UPDATE;
>
>         struct rev_info revs;
>         struct commit *last_commit = NULL;
>     @@ builtin/replay.c: int cmd_replay(int argc,
>         kh_oid_map_t *replayed_commits;
>      +  struct ref_transaction *transaction = NULL;
>      +  struct strbuf transaction_err = STRBUF_INIT;
>     ++  struct strbuf reflog_msg = STRBUF_INIT;
>         int ret = 0;
>
>      -  const char * const replay_usage[] = {
>     @@ builtin/replay.c: int cmd_replay(int argc,
>                            N_("replay onto given commit")),
>                 OPT_BOOL(0, "contained", &contained,
>                          N_("advance all branches contained in revision-range")),
>     -+          OPT_STRING(0, "ref-action", &ref_action_str,
>     ++          OPT_STRING(0, "ref-action", &ref_action,
>      +                     N_("mode"),
>      +                     N_("control ref update behavior (update|print)")),
>                 OPT_END()
>     @@ builtin/replay.c: int cmd_replay(int argc,
>         die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>                                   contained, "--contained");
>
>     -+  /* Default to update mode if not specified */
>     -+  if (!ref_action_str)
>     -+          ref_action_str = "update";
>     -+
>     -+  /* Validate ref-action mode */
>     -+  if (!strcmp(ref_action_str, "update"))
>     -+          ref_action = REF_ACTION_UPDATE;
>     -+  else if (!strcmp(ref_action_str, "print"))
>     -+          ref_action = REF_ACTION_PRINT;
>     -+  else
>     -+          die(_("unknown --ref-action mode '%s'"), ref_action_str);
>     ++  /* Parse ref action mode */
>     ++  if (ref_action)
>     ++          ref_mode = parse_ref_action_mode(ref_action, "--ref-action");
>      +
>         advance_name = xstrdup_or_null(advance_name_opt);
>
>     @@ builtin/replay.c: int cmd_replay(int argc,
>         determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
>                               &onto, &update_refs);
>
>     ++  /* Build reflog message */
>     ++  if (advance_name_opt)
>     ++          strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
>     ++  else
>     ++          strbuf_addf(&reflog_msg, "replay --onto %s",
>     ++                      oid_to_hex(&onto->object.oid));
>     ++
>      +  /* Initialize ref transaction if using update mode */
>     -+  if (ref_action == REF_ACTION_UPDATE) {
>     ++  if (ref_mode == REF_ACTION_UPDATE) {
>      +          transaction = ref_store_transaction_begin(get_main_ref_store(repo),
>      +                                                    0, &transaction_err);
>      +          if (!transaction) {
>     @@ builtin/replay.c: int cmd_replay(int argc,
>      -                                 decoration->name,
>      -                                 oid_to_hex(&last_commit->object.oid),
>      -                                 oid_to_hex(&commit->object.oid));
>     -+                          if (handle_ref_update(ref_action, transaction,
>     ++                          if (handle_ref_update(ref_mode, transaction,
>      +                                                decoration->name,
>      +                                                &last_commit->object.oid,
>      +                                                &commit->object.oid,
>     ++                                                reflog_msg.buf,
>      +                                                &transaction_err) < 0) {
>      +                                  ret = error(_("failed to update ref '%s': %s"),
>      +                                              decoration->name, transaction_err.buf);
>     @@ builtin/replay.c: int cmd_replay(int argc,
>      -                 advance_name,
>      -                 oid_to_hex(&last_commit->object.oid),
>      -                 oid_to_hex(&onto->object.oid));
>     -+          if (handle_ref_update(ref_action, transaction, advance_name,
>     ++          if (handle_ref_update(ref_mode, transaction, advance_name,
>      +                                &last_commit->object.oid,
>      +                                &onto->object.oid,
>     ++                                reflog_msg.buf,
>      +                                &transaction_err) < 0) {
>      +                  ret = error(_("failed to update ref '%s': %s"),
>      +                              advance_name, transaction_err.buf);
>     @@ builtin/replay.c: int cmd_replay(int argc,
>      +  if (transaction)
>      +          ref_transaction_free(transaction);
>      +  strbuf_release(&transaction_err);
>     ++  strbuf_release(&reflog_msg);
>         release_revisions(&revs);
>         free(advance_name);
>
>     @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '
>       '
>
>      +test_expect_success 'default atomic behavior updates refs directly' '
>     -+  # Store original state for cleanup
>     -+  test_when_finished "git branch -f topic2 topic1" &&
>     ++  # Use a separate branch to avoid contaminating topic2 for later tests
>     ++  git branch test-atomic topic2 &&
>     ++  test_when_finished "git branch -D test-atomic" &&

I'm curious why you created an extra branch for this test, while...

>      +
>      +  # Test default atomic behavior (no output, refs updated)
>     -+  git replay --onto main topic1..topic2 >output &&
>     ++  git replay --onto main topic1..test-atomic >output &&
>      +  test_must_be_empty output &&
>      +
>      +  # Verify ref was updated
>     -+  git log --format=%s topic2 >actual &&
>     ++  git log --format=%s test-atomic >actual &&
>      +  test_write_lines E D M L B A >expect &&
>     -+  test_cmp expect actual
>     ++  test_cmp expect actual &&
>     ++
>     ++  # Verify reflog message includes SHA of onto commit
>     ++  git reflog test-atomic -1 --format=%gs >reflog-msg &&
>     ++  ONTO_SHA=$(git rev-parse main) &&
>     ++  echo "replay --onto $ONTO_SHA" >expect-reflog &&
>     ++  test_cmp expect-reflog reflog-msg
>      +'
>      +
>      +test_expect_success 'atomic behavior in bare repository' '
>     ++  # Store original state for cleanup
>     ++  START=$(git -C bare rev-parse topic2) &&
>     ++  test_when_finished "git -C bare update-ref refs/heads/topic2 $START" &&

...just saving the location for topic2 in this test (and similarly
just saving the location for main in the next test).  It appears you
weren't doing anything special with the test-atomic branch, so I'm
curious why you didn't just use the same idiom for all three tests.

>     ++
>      +  # Test atomic updates work in bare repo
>      +  git -C bare replay --onto main topic1..topic2 >output &&
>      +  test_must_be_empty output &&
>     @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '
>      +  # Verify ref was updated in bare repo
>      +  git -C bare log --format=%s topic2 >actual &&
>      +  test_write_lines E D M L B A >expect &&
>     -+  test_cmp expect actual &&
>     ++  test_cmp expect actual
>     ++'
>     ++
>     ++test_expect_success 'reflog message for --advance mode' '
>     ++  # Store original state
>     ++  START=$(git rev-parse main) &&
>     ++  test_when_finished "git update-ref refs/heads/main $START" &&
>     ++
>     ++  # Test --advance mode reflog message
>     ++  git replay --advance main topic1..topic2 >output &&
>     ++  test_must_be_empty output &&
>      +
>     -+  # Reset for other tests
>     -+  git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)
>     ++  # Verify reflog message includes --advance and branch name
>     ++  git reflog main -1 --format=%gs >reflog-msg &&
>     ++  echo "replay --advance main" >expect-reflog &&
>     ++  test_cmp expect-reflog reflog-msg
>      +'
>      +
>       test_done
> -:  ---------- > 3:  b7ebe1f534 replay: add replay.refAction config option

There was a third patch in v6, but it doesn't show up in your
range-diff?  Did you specify the range incorrectly by chance when you
generated this?

Anyway, I looked over the patches as well as the range-diff.  There is
the slight surprise I had in your lack of consistency with idiom
choice in the tests, but that's pretty minor and probably doesn't
merit a re-roll.  This version looks good to me; thanks for working on
it!

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v7 0/3] replay: make atomic ref updates the default
  2025-11-05 19:15           ` [PATCH v7 " Siddharth Asthana
                               ` (3 preceding siblings ...)
  2025-11-06 19:32             ` [PATCH v7 0/3] replay: make atomic ref updates the default Elijah Newren
@ 2025-11-07 15:48             ` Phillip Wood
  2025-11-08 13:23               ` Siddharth Asthana
  4 siblings, 1 reply; 129+ messages in thread
From: Phillip Wood @ 2025-11-07 15:48 UTC (permalink / raw)
  To: Siddharth Asthana, git
  Cc: christian.couder, phillip.wood, newren, gitster, ps, karthik.188,
	code, rybak.a.v, jltobler, toon, johncai86, johannes.schindelin

Hi Siddharth

On 05/11/2025 19:15, Siddharth Asthana wrote:

>      @@ builtin/replay.c: int cmd_replay(int argc,
>        	determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
>        			      &onto, &update_refs);
>        
>      ++	/* Build reflog message */
>      ++	if (advance_name_opt)
>      ++		strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);

This appends the name of the branch being advanced, rather than what's 
being picked. As this message is written to the reflog of the branch 
that's being advanced adding the branch name to the message is kind of 
redundant but we can always change this later when we have more 
experience with "--ref-action"

>      ++	else
>      ++		strbuf_addf(&reflog_msg, "replay --onto %s",
>      ++			    oid_to_hex(&onto->object.oid));

This looks good.

Thanks for working on this, I think this is probably ready to me merged.

Phillip


^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v7 0/3] replay: make atomic ref updates the default
  2025-11-06 19:32             ` [PATCH v7 0/3] replay: make atomic ref updates the default Elijah Newren
@ 2025-11-08 13:22               ` Siddharth Asthana
  2025-11-08 17:11                 ` Elijah Newren
  0 siblings, 1 reply; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-08 13:22 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, christian.couder, phillip.wood123, phillip.wood, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin


On 07/11/25 01:02, Elijah Newren wrote:
> On Wed, Nov 5, 2025 at 11:17 AM Siddharth Asthana
> <siddharthasthana31@gmail.com> wrote:
>> This is v7 of the git-replay atomic updates series.
>>
>> This version addresses all feedback from v6 reviews. Thanks to Elijah,
>> Christian, and Phillip for the thorough reviews that helped refine the
>> implementation to Git standards.
>>
>> ## Changes in v7
>>
>> **Improved commit message clarity**
>>
>> Per Elijah's feedback, simplified commit messages by removing redundant
>> sections:
>>    - Removed "Implementation details" section (details visible in diff)
>>    - Shortened "Test suite changes" to focus on what's tested
>>    - Removed command-line precedence paragraph (obvious from code)
>>    - Removed "Examples" and configuration precedence sections
>>
>> **Fixed test cleanup and isolation**
>>
>> Following Elijah's suggestions:
>>    - Used test_when_finished with proper state restoration in atomic tests
>>    - Created separate test-atomic branch to avoid contaminating topic2
>>    - Fixed bare repository test to use START variable for cleanup
>>    - Improved test reliability by rebuilding expectations independently
>>
>> **Extracted parse_ref_action_mode() to appropriate commit**
>>
>> Per Christian's observation, moved the parse_ref_action_mode() helper
>> function from Commit 3 to Commit 2 where it's first used. This makes
>> the patch progression more logical.
>>
>> **Fixed parameter naming consistency**
>>
>> Following Christian's feedback, used consistent naming throughout:
>>    - ref_action (string parameter for command-line/config value)
>>    - ref_mode (enum variable for internal mode)
>> This eliminates confusion and improves code readability.
>>
>> **Moved config reference to correct commit**
>>
>> Per Elijah's note, moved the sentence about replay.refAction config
>> from Commit 2's documentation to Commit 3 where the config is actually
>> introduced.
>>
>> **Enhanced reflog messages**
>>
>> Following Phillip's suggestions for better user experience:
>>    - --advance mode: "replay --advance <branch-name>" (uses user input)
>>    - --onto mode: "replay --onto <commit-sha>" (precise commit reference)
>> Added comprehensive reflog testing to verify messages.
>>
>> **Fixed indentation in Commit 3**
>>
>> Corrected indentation within the while (decoration) loop per CI
>> feedback, adding proper tabs to nested if statements.
>>
>> **Fixed coding style**
>>
>> Per CI check-style feedback, removed braces from single-statement
>> if-else blocks following Git's CodingGuidelines.
>>
>> **Split config tests for clarity**
>>
>> Separated the replay.refAction config test into two distinct tests:
>>    - replay.refAction=print config option
>>    - replay.refAction=update config option
>> This improves test clarity and makes failures easier to diagnose.
>>
>> ## Technical Implementation
>>
>> The atomic ref updates leverage Git's ref transaction API:
>>    - ref_store_transaction_begin() with default atomic behavior
>>    - ref_transaction_update() to stage each update
>>    - ref_transaction_commit() for atomic application
>>
>> The helper functions provide clean separation:
>>    - parse_ref_action_mode(): Validates strings and converts to enum
>>    - get_ref_action_mode(): Implements command-line > config > default precedence
>>    - handle_ref_update(): Uses type-safe enum with switch statement
>>
>> Reflog messages are constructed dynamically based on replay mode and
>> include either the branch name (--advance) or commit SHA (--onto) for
>> clear audit trails.
>>
>> ## Testing
>>
>> All tests pass:
>>    - t3650-replay-basics.sh (22 tests pass)
>>    - Config tests verify proper precedence and error handling
>>    - Atomic behavior tests verify direct ref updates
>>    - Reflog tests verify descriptive messages
>>    - Backward compatibility maintained for pipeline workflow
>>
>> CI results: https://gitlab.com/gitlab-org/git/-/pipelines/2140425748
>>
>> Siddharth Asthana (3):
>>    replay: use die_for_incompatible_opt2() for option validation
>>    replay: make atomic ref updates the default behavior
>>    replay: add replay.refAction config option
>>
>>   Documentation/config/replay.adoc |  11 +++
>>   Documentation/git-replay.adoc    |  63 ++++++++++-----
>>   builtin/replay.c                 | 133 ++++++++++++++++++++++++++++---
>>   t/t3650-replay-basics.sh         | 113 ++++++++++++++++++++++++--
>>   4 files changed, 277 insertions(+), 43 deletions(-)
>>   create mode 100644 Documentation/config/replay.adoc
>>
>> Range-diff against v6:
>> 1:  1f0fad0cac = 1:  9e4eab2df2 replay: use die_for_incompatible_opt2() for option validation
>> 2:  bfc6188234 ! 2:  1602f6097e replay: make atomic ref updates the default behavior
>>      @@ Commit message
>>            * update (default): Update refs directly using an atomic transaction
>>            * print: Output update-ref commands for pipeline use
>>
>>      -    Implementation details:
>>      -
>>      -    The atomic ref updates are implemented using Git's ref transaction API.
>>      -    In cmd_replay(), when not in `print` mode, we initialize a transaction
>>      -    using ref_store_transaction_begin() with the default atomic behavior.
>>      -    As commits are replayed, ref updates are staged into the transaction
>>      -    using ref_transaction_update(). Finally, ref_transaction_commit()
>>      -    applies all updates atomically—either all updates succeed or none do.
>>      -
>>      -    To avoid code duplication between the 'print' and 'update' modes, this
>>      -    commit extracts a handle_ref_update() helper function. This function
>>      -    takes the mode (as an enum) and either prints the update command or
>>      -    stages it into the transaction. Using an enum rather than passing the
>>      -    string around provides type safety and allows the compiler to catch
>>      -    typos. The switch statement makes it easy to add future modes.
>>      -
>>      -    The helper function signature:
>>      -
>>      -      static int handle_ref_update(enum ref_action_mode mode,
>>      -                                    struct ref_transaction *transaction,
>>      -                                    const char *refname,
>>      -                                    const struct object_id *new_oid,
>>      -                                    const struct object_id *old_oid,
>>      -                                    struct strbuf *err)
>>      -
>>      -    The enum is defined as:
>>      -
>>      -      enum ref_action_mode {
>>      -          REF_ACTION_UPDATE,
>>      -          REF_ACTION_PRINT
>>      -      };
>>      -
>>      -    The mode string is converted to enum immediately after parse_options()
>>      -    to avoid string comparisons throughout the codebase and provide compiler
>>      -    protection against typos.
>>      -
>>           Test suite changes:
>>
>>           All existing tests that expected command output now use
>>      @@ Commit message
>>            - Equivalence between traditional pipeline and atomic updates
>>            - Real atomicity using a lock file to verify all-or-nothing guarantee
>>            - Test isolation using test_when_finished to clean up state
>>      -
>>      -    The bare repository tests were fixed to rebuild their expectations
>>      -    independently rather than comparing to previous test output, improving
>>      -    test reliability and isolation.
>>      +      - Reflog messages include replay mode and target
>>
>>           A following commit will add a replay.refAction configuration
>>           option for users who prefer the traditional pipeline output as their
>>      @@ Documentation/git-replay.adoc: OPTIONS
>>       -commits, similar to the way how `git rebase --update-refs` updates
>>       -multiple branches in the affected range.
>>       +When `--onto` is specified, the branch(es) in the revision range will be
>>      -+updated to point at the new commits (or update commands will be printed
>>      -+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`
>>      ++updated to point at the new commits, similar to the way `git rebase --update-refs`
>>       +updates multiple branches in the affected range.
>>
>>        --advance <branch>::
>>      @@ Documentation/git-replay.adoc: OPTIONS
>>       -will update the branch passed as an argument to `--advance` to point at
>>       -the new commits (in other words, this mimics a cherry-pick operation).
>>       +The history is replayed on top of the <branch> and <branch> is updated to
>>      -+point at the tip of the resulting history (or an update command will be
>>      -+printed if `--ref-action=print` is used). This is different from `--onto`,
>>      ++point at the tip of the resulting history. This is different from `--onto`,
>>       +which uses the target only as a starting point without updating it.
>>       +
>>       +--ref-action[=<mode>]::
>>      @@ Documentation/git-replay.adoc: OPTIONS
>>       +  * `print`: Output update-ref commands for pipeline use. This is the
>>       +    traditional behavior where output can be piped to `git update-ref --stdin`.
>>       +--
>>      -++
>>      -+The default mode can be configured via the `replay.refAction` configuration variable.
>>
>>        <revision-range>::
>>          Range of commits to replay. More than one <revision-range> can
>>      @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r
>>          return create_commit(repo, result->tree, pickme, replayed_base);
>>        }
>>
>>      ++static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
>>      ++{
>>      ++  if (!ref_action || !strcmp(ref_action, "update"))
>>      ++          return REF_ACTION_UPDATE;
>>      ++  if (!strcmp(ref_action, "print"))
>>      ++          return REF_ACTION_PRINT;
>>      ++  die(_("invalid %s value: '%s'"), source, ref_action);
>>      ++}
>>      ++
>>       +static int handle_ref_update(enum ref_action_mode mode,
>>       +                       struct ref_transaction *transaction,
>>       +                       const char *refname,
>>       +                       const struct object_id *new_oid,
>>       +                       const struct object_id *old_oid,
>>      ++                       const char *reflog_msg,
>>       +                       struct strbuf *err)
>>       +{
>>       +  switch (mode) {
>>      @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r
>>       +          return 0;
>>       +  case REF_ACTION_UPDATE:
>>       +          return ref_transaction_update(transaction, refname, new_oid, old_oid,
>>      -+                                        NULL, NULL, 0, "git replay", err);
>>      ++                                        NULL, NULL, 0, reflog_msg, err);
>>       +  default:
>>       +          BUG("unknown ref_action_mode %d", mode);
>>       +  }
>>      @@ builtin/replay.c: int cmd_replay(int argc,
>>          struct commit *onto = NULL;
>>          const char *onto_name = NULL;
>>          int contained = 0;
>>      -+  const char *ref_action_str = NULL;
>>      -+  enum ref_action_mode ref_action = REF_ACTION_UPDATE;
>>      ++  const char *ref_action = NULL;
>>      ++  enum ref_action_mode ref_mode = REF_ACTION_UPDATE;
>>
>>          struct rev_info revs;
>>          struct commit *last_commit = NULL;
>>      @@ builtin/replay.c: int cmd_replay(int argc,
>>          kh_oid_map_t *replayed_commits;
>>       +  struct ref_transaction *transaction = NULL;
>>       +  struct strbuf transaction_err = STRBUF_INIT;
>>      ++  struct strbuf reflog_msg = STRBUF_INIT;
>>          int ret = 0;
>>
>>       -  const char * const replay_usage[] = {
>>      @@ builtin/replay.c: int cmd_replay(int argc,
>>                             N_("replay onto given commit")),
>>                  OPT_BOOL(0, "contained", &contained,
>>                           N_("advance all branches contained in revision-range")),
>>      -+          OPT_STRING(0, "ref-action", &ref_action_str,
>>      ++          OPT_STRING(0, "ref-action", &ref_action,
>>       +                     N_("mode"),
>>       +                     N_("control ref update behavior (update|print)")),
>>                  OPT_END()
>>      @@ builtin/replay.c: int cmd_replay(int argc,
>>          die_for_incompatible_opt2(!!advance_name_opt, "--advance",
>>                                    contained, "--contained");
>>
>>      -+  /* Default to update mode if not specified */
>>      -+  if (!ref_action_str)
>>      -+          ref_action_str = "update";
>>      -+
>>      -+  /* Validate ref-action mode */
>>      -+  if (!strcmp(ref_action_str, "update"))
>>      -+          ref_action = REF_ACTION_UPDATE;
>>      -+  else if (!strcmp(ref_action_str, "print"))
>>      -+          ref_action = REF_ACTION_PRINT;
>>      -+  else
>>      -+          die(_("unknown --ref-action mode '%s'"), ref_action_str);
>>      ++  /* Parse ref action mode */
>>      ++  if (ref_action)
>>      ++          ref_mode = parse_ref_action_mode(ref_action, "--ref-action");
>>       +
>>          advance_name = xstrdup_or_null(advance_name_opt);
>>
>>      @@ builtin/replay.c: int cmd_replay(int argc,
>>          determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
>>                                &onto, &update_refs);
>>
>>      ++  /* Build reflog message */
>>      ++  if (advance_name_opt)
>>      ++          strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
>>      ++  else
>>      ++          strbuf_addf(&reflog_msg, "replay --onto %s",
>>      ++                      oid_to_hex(&onto->object.oid));
>>      ++
>>       +  /* Initialize ref transaction if using update mode */
>>      -+  if (ref_action == REF_ACTION_UPDATE) {
>>      ++  if (ref_mode == REF_ACTION_UPDATE) {
>>       +          transaction = ref_store_transaction_begin(get_main_ref_store(repo),
>>       +                                                    0, &transaction_err);
>>       +          if (!transaction) {
>>      @@ builtin/replay.c: int cmd_replay(int argc,
>>       -                                 decoration->name,
>>       -                                 oid_to_hex(&last_commit->object.oid),
>>       -                                 oid_to_hex(&commit->object.oid));
>>      -+                          if (handle_ref_update(ref_action, transaction,
>>      ++                          if (handle_ref_update(ref_mode, transaction,
>>       +                                                decoration->name,
>>       +                                                &last_commit->object.oid,
>>       +                                                &commit->object.oid,
>>      ++                                                reflog_msg.buf,
>>       +                                                &transaction_err) < 0) {
>>       +                                  ret = error(_("failed to update ref '%s': %s"),
>>       +                                              decoration->name, transaction_err.buf);
>>      @@ builtin/replay.c: int cmd_replay(int argc,
>>       -                 advance_name,
>>       -                 oid_to_hex(&last_commit->object.oid),
>>       -                 oid_to_hex(&onto->object.oid));
>>      -+          if (handle_ref_update(ref_action, transaction, advance_name,
>>      ++          if (handle_ref_update(ref_mode, transaction, advance_name,
>>       +                                &last_commit->object.oid,
>>       +                                &onto->object.oid,
>>      ++                                reflog_msg.buf,
>>       +                                &transaction_err) < 0) {
>>       +                  ret = error(_("failed to update ref '%s': %s"),
>>       +                              advance_name, transaction_err.buf);
>>      @@ builtin/replay.c: int cmd_replay(int argc,
>>       +  if (transaction)
>>       +          ref_transaction_free(transaction);
>>       +  strbuf_release(&transaction_err);
>>      ++  strbuf_release(&reflog_msg);
>>          release_revisions(&revs);
>>          free(advance_name);
>>
>>      @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '
>>        '
>>
>>       +test_expect_success 'default atomic behavior updates refs directly' '
>>      -+  # Store original state for cleanup
>>      -+  test_when_finished "git branch -f topic2 topic1" &&
>>      ++  # Use a separate branch to avoid contaminating topic2 for later tests
>>      ++  git branch test-atomic topic2 &&
>>      ++  test_when_finished "git branch -D test-atomic" &&


Hi Elijah,

Thanks for the review and approval!


> I'm curious why you created an extra branch for this test, while...


Good catch on the inconsistency. The separate `test-atomic` branch was 
to avoid any potential contamination of `topic2` since multiple tests 
use it throughout the file. But you are right that the other tests just 
save/restore state directly.

For consistency, I could have used the same pattern everywhere. The 
current approach works but mixing idioms isn't ideal - I will keep this 
in mind for future patches.


>
>>       +
>>       +  # Test default atomic behavior (no output, refs updated)
>>      -+  git replay --onto main topic1..topic2 >output &&
>>      ++  git replay --onto main topic1..test-atomic >output &&
>>       +  test_must_be_empty output &&
>>       +
>>       +  # Verify ref was updated
>>      -+  git log --format=%s topic2 >actual &&
>>      ++  git log --format=%s test-atomic >actual &&
>>       +  test_write_lines E D M L B A >expect &&
>>      -+  test_cmp expect actual
>>      ++  test_cmp expect actual &&
>>      ++
>>      ++  # Verify reflog message includes SHA of onto commit
>>      ++  git reflog test-atomic -1 --format=%gs >reflog-msg &&
>>      ++  ONTO_SHA=$(git rev-parse main) &&
>>      ++  echo "replay --onto $ONTO_SHA" >expect-reflog &&
>>      ++  test_cmp expect-reflog reflog-msg
>>       +'
>>       +
>>       +test_expect_success 'atomic behavior in bare repository' '
>>      ++  # Store original state for cleanup
>>      ++  START=$(git -C bare rev-parse topic2) &&
>>      ++  test_when_finished "git -C bare update-ref refs/heads/topic2 $START" &&
> ...just saving the location for topic2 in this test (and similarly
> just saving the location for main in the next test).  It appears you
> weren't doing anything special with the test-atomic branch, so I'm
> curious why you didn't just use the same idiom for all three tests.
>
>>      ++
>>       +  # Test atomic updates work in bare repo
>>       +  git -C bare replay --onto main topic1..topic2 >output &&
>>       +  test_must_be_empty output &&
>>      @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '
>>       +  # Verify ref was updated in bare repo
>>       +  git -C bare log --format=%s topic2 >actual &&
>>       +  test_write_lines E D M L B A >expect &&
>>      -+  test_cmp expect actual &&
>>      ++  test_cmp expect actual
>>      ++'
>>      ++
>>      ++test_expect_success 'reflog message for --advance mode' '
>>      ++  # Store original state
>>      ++  START=$(git rev-parse main) &&
>>      ++  test_when_finished "git update-ref refs/heads/main $START" &&
>>      ++
>>      ++  # Test --advance mode reflog message
>>      ++  git replay --advance main topic1..topic2 >output &&
>>      ++  test_must_be_empty output &&
>>       +
>>      -+  # Reset for other tests
>>      -+  git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)
>>      ++  # Verify reflog message includes --advance and branch name
>>      ++  git reflog main -1 --format=%gs >reflog-msg &&
>>      ++  echo "replay --advance main" >expect-reflog &&
>>      ++  test_cmp expect-reflog reflog-msg
>>       +'
>>       +
>>        test_done
>> -:  ---------- > 3:  b7ebe1f534 replay: add replay.refAction config option
> There was a third patch in v6, but it doesn't show up in your
> range-diff?  Did you specify the range incorrectly by chance when you
> generated this?


The range-diff shows all three patches (1:1, 2:2, 3:3), but the third 
one appears as a new addition (-: → 3:) because it underwent significant 
restructuring between v6 and v7. The config-related changes were moved 
around between commits, making git see it as essentially new rather than 
modified.

Thanks for the thorough review throughout this series!

Siddharth


>
> Anyway, I looked over the patches as well as the range-diff.  There is
> the slight surprise I had in your lack of consistency with idiom
> choice in the tests, but that's pretty minor and probably doesn't
> merit a re-roll.  This version looks good to me; thanks for working on
> it!

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v7 0/3] replay: make atomic ref updates the default
  2025-11-07 15:48             ` Phillip Wood
@ 2025-11-08 13:23               ` Siddharth Asthana
  0 siblings, 0 replies; 129+ messages in thread
From: Siddharth Asthana @ 2025-11-08 13:23 UTC (permalink / raw)
  To: phillip.wood, git
  Cc: christian.couder, newren, gitster, ps, karthik.188, code,
	rybak.a.v, jltobler, toon, johncai86, johannes.schindelin


On 07/11/25 21:18, Phillip Wood wrote:
> Hi Siddharth
>
> On 05/11/2025 19:15, Siddharth Asthana wrote:
>
>>      @@ builtin/replay.c: int cmd_replay(int argc,
>>            determine_replay_mode(repo, &revs.cmdline, onto_name, 
>> &advance_name,
>>                          &onto, &update_refs);
>>             ++    /* Build reflog message */
>>      ++    if (advance_name_opt)
>>      ++        strbuf_addf(&reflog_msg, "replay --advance %s", 
>> advance_name_opt);
>

Hi Phillip,


> This appends the name of the branch being advanced, rather than what's 
> being picked. As this message is written to the reflog of the branch 
> that's being advanced adding the branch name to the message is kind of 
> redundant but we can always change this later when we have more 
> experience with "--ref-action"


You are absolutely right about the redundancy. I went with the branch 
name to match what users typed on the command line, but since it's in 
that branch's own reflog, just "replay --advance" might be cleaner.

Happy to adjust this in a follow-up if the current approach proves 
confusing in practice.


>
>>      ++    else
>>      ++        strbuf_addf(&reflog_msg, "replay --onto %s",
>>      ++                oid_to_hex(&onto->object.oid));
>
> This looks good.
>
> Thanks for working on this, I think this is probably ready to me merged.


Thank you for all the detailed feedback throughout this series - it 
really helped improve the implementation!

Siddharth


>
> Phillip
>

^ permalink raw reply	[flat|nested] 129+ messages in thread

* Re: [PATCH v7 0/3] replay: make atomic ref updates the default
  2025-11-08 13:22               ` Siddharth Asthana
@ 2025-11-08 17:11                 ` Elijah Newren
  0 siblings, 0 replies; 129+ messages in thread
From: Elijah Newren @ 2025-11-08 17:11 UTC (permalink / raw)
  To: Siddharth Asthana
  Cc: git, christian.couder, phillip.wood123, phillip.wood, gitster, ps,
	karthik.188, code, rybak.a.v, jltobler, toon, johncai86,
	johannes.schindelin

On Sat, Nov 8, 2025 at 5:22 AM Siddharth Asthana
<siddharthasthana31@gmail.com> wrote:

> >> -:  ---------- > 3:  b7ebe1f534 replay: add replay.refAction config option
> > There was a third patch in v6, but it doesn't show up in your
> > range-diff?  Did you specify the range incorrectly by chance when you
> > generated this?
>
>
> The range-diff shows all three patches (1:1, 2:2, 3:3), but the third
> one appears as a new addition (-: → 3:) because it underwent significant
> restructuring between v6 and v7. The config-related changes were moved
> around between commits, making git see it as essentially new rather than
> modified.

No, the range diff does not show all three patches for v6, it only
shows all three patches for v7.  If you had all three patches shown
for both versions, and the third had undergone significant
restructuring, then you would expect to see two lines such as:

3:  6b2a44c72c < -:  -----------  replay: add replay.refAction config option
-:  ----------- > 3:  b7ebe1f534 replay: add replay.refAction config option

The first line (missing from your range-diff) would correspond to the
third patch from v6 being treated as deleted, and the second (present
in your range-diff) would represent the third patch from v7 being
considered an addition.  You can verify the first line is missing from
your range-diff by searching for "3:" in
https://lore.kernel.org/git/20251105191650.89975-1-siddharthasthana31@gmail.com/
-- you only get one hit instead of the expected two -- which suggests
you either didn't pass the correct range to range-diff or snipped part
of the output when pasting to your email.  In this case it doesn't
matter much, because even if the 3rd patch from v6 was there we'd need
to go an look at the individual patch due to the restructuring, but it
was just a little odd so I pointed it out.

^ permalink raw reply	[flat|nested] 129+ messages in thread

end of thread, other threads:[~2025-11-08 17:11 UTC | newest]

Thread overview: 129+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-09-08  4:36 [PATCH 0/2] replay: add --update-refs option Siddharth Asthana
2025-09-08  4:36 ` [PATCH 1/2] " Siddharth Asthana
2025-09-08  9:54   ` Patrick Steinhardt
2025-09-09  6:58     ` Siddharth Asthana
2025-09-09  9:00       ` Patrick Steinhardt
2025-09-09  7:32   ` Elijah Newren
2025-09-10 17:58     ` Siddharth Asthana
2025-09-08  4:36 ` [PATCH 2/2] replay: document --update-refs and --batch options Siddharth Asthana
2025-09-08  6:00   ` Christian Couder
2025-09-09  6:36     ` Siddharth Asthana
2025-09-09  7:26       ` Christian Couder
2025-09-10 20:26         ` Siddharth Asthana
2025-09-08 14:40   ` Kristoffer Haugsbakk
2025-09-09  7:06     ` Siddharth Asthana
2025-09-09 19:20   ` Andrei Rybak
2025-09-10 20:28     ` Siddharth Asthana
2025-09-08  6:07 ` [PATCH 0/2] replay: add --update-refs option Christian Couder
2025-09-09  6:36   ` Siddharth Asthana
2025-09-08 14:33 ` Kristoffer Haugsbakk
2025-09-09  7:04   ` Siddharth Asthana
2025-09-09  7:13 ` Elijah Newren
2025-09-09  7:47   ` Christian Couder
2025-09-09  9:19     ` Elijah Newren
2025-09-09 16:44       ` Junio C Hamano
2025-09-09 19:52         ` Elijah Newren
2025-09-26 23:08 ` [PATCH v2 0/1] replay: make atomic ref updates the default behavior Siddharth Asthana
2025-09-26 23:08   ` [PATCH v2 1/1] " Siddharth Asthana
2025-09-30  8:23     ` Christian Couder
2025-10-02 22:16       ` Siddharth Asthana
2025-10-03  7:30         ` Christian Couder
2025-10-02 22:55       ` Elijah Newren
2025-10-03  7:05         ` Christian Couder
2025-09-30 10:05     ` Phillip Wood
2025-10-02 10:00       ` Karthik Nayak
2025-10-02 22:20         ` Siddharth Asthana
2025-10-02 22:20       ` Siddharth Asthana
2025-10-08 14:01         ` Phillip Wood
2025-10-08 20:09           ` Siddharth Asthana
2025-10-08 20:59             ` Elijah Newren
2025-10-08 21:16               ` Siddharth Asthana
2025-10-09  9:40             ` Phillip Wood
2025-10-02 16:32     ` Elijah Newren
2025-10-02 18:27       ` Junio C Hamano
2025-10-02 23:42         ` Siddharth Asthana
2025-10-02 23:27       ` Siddharth Asthana
2025-10-03  7:59         ` Christian Couder
2025-10-08 19:59           ` Siddharth Asthana
2025-10-03 19:48         ` Elijah Newren
2025-10-03 20:32           ` Junio C Hamano
2025-10-08 20:06             ` Siddharth Asthana
2025-10-08 20:59               ` Junio C Hamano
2025-10-08 21:10                 ` Siddharth Asthana
2025-10-08 21:30               ` Elijah Newren
2025-10-08 20:05           ` Siddharth Asthana
2025-10-02 17:14   ` [PATCH v2 0/1] " Kristoffer Haugsbakk
2025-10-02 23:36     ` Siddharth Asthana
2025-10-03 19:05       ` Kristoffer Haugsbakk
2025-10-08 20:02         ` Siddharth Asthana
2025-10-08 20:56           ` Elijah Newren
2025-10-08 21:16             ` Kristoffer Haugsbakk
2025-10-08 21:18             ` Siddharth Asthana
2025-10-13 18:33   ` [PATCH v3 0/3] replay: make atomic ref updates the default Siddharth Asthana
2025-10-13 18:33     ` [PATCH v3 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
2025-10-13 18:33     ` [PATCH v3 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
2025-10-13 22:05       ` Junio C Hamano
2025-10-15  5:01         ` Siddharth Asthana
2025-10-13 18:33     ` [PATCH v3 3/3] replay: add replay.defaultAction config option Siddharth Asthana
2025-10-13 19:39     ` [PATCH v3 0/3] replay: make atomic ref updates the default Junio C Hamano
2025-10-15  4:57       ` Siddharth Asthana
2025-10-15 10:33         ` Christian Couder
2025-10-15 14:45         ` Junio C Hamano
2025-10-22 18:50     ` [PATCH v4 " Siddharth Asthana
2025-10-22 18:50       ` [PATCH v4 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
2025-10-22 18:50       ` [PATCH v4 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
2025-10-22 21:19         ` Junio C Hamano
2025-10-28 19:03           ` Siddharth Asthana
2025-10-24 10:37         ` Christian Couder
2025-10-24 15:23           ` Junio C Hamano
2025-10-28 20:18             ` Siddharth Asthana
2025-10-28 19:39           ` Siddharth Asthana
2025-10-22 18:50       ` [PATCH v4 3/3] replay: add replay.refAction config option Siddharth Asthana
2025-10-24 11:01         ` Christian Couder
2025-10-24 15:30           ` Junio C Hamano
2025-10-28 20:08             ` Siddharth Asthana
2025-10-28 19:26           ` Siddharth Asthana
2025-10-24 13:28         ` Phillip Wood
2025-10-24 13:36           ` Phillip Wood
2025-10-28 19:47             ` Siddharth Asthana
2025-10-28 19:46           ` Siddharth Asthana
2025-10-23 18:47       ` [PATCH v4 0/3] replay: make atomic ref updates the default Junio C Hamano
2025-10-25 16:57         ` Junio C Hamano
2025-10-28 20:19         ` Siddharth Asthana
2025-10-24  9:39       ` Christian Couder
2025-10-28 21:46       ` [PATCH v5 " Siddharth Asthana
2025-10-28 21:46         ` [PATCH v5 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
2025-10-28 21:46         ` [PATCH v5 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
2025-10-28 21:46         ` [PATCH v5 3/3] replay: add replay.refAction config option Siddharth Asthana
2025-10-29 16:19           ` Christian Couder
2025-10-29 17:00             ` Siddharth Asthana
2025-10-30 19:19         ` [PATCH v6 0/3] replay: make atomic ref updates the default Siddharth Asthana
2025-10-30 19:19           ` [PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
2025-10-31 18:47             ` Elijah Newren
2025-11-05 18:39               ` Siddharth Asthana
2025-10-30 19:19           ` [PATCH v6 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
2025-10-31 18:49             ` Elijah Newren
2025-10-31 19:59               ` Junio C Hamano
2025-11-05 19:07               ` Siddharth Asthana
2025-11-03 16:25             ` Phillip Wood
2025-11-03 19:32               ` Siddharth Asthana
2025-11-04 16:15                 ` Phillip Wood
2025-10-30 19:19           ` [PATCH v6 3/3] replay: add replay.refAction config option Siddharth Asthana
2025-10-31  7:08             ` Christian Couder
2025-11-05 19:03               ` Siddharth Asthana
2025-10-31 18:49             ` Elijah Newren
2025-11-05 19:10               ` Siddharth Asthana
2025-10-31 18:51           ` [PATCH v6 0/3] replay: make atomic ref updates the default Elijah Newren
2025-11-05 19:15           ` [PATCH v7 " Siddharth Asthana
2025-11-05 19:15             ` [PATCH v7 1/3] replay: use die_for_incompatible_opt2() for option validation Siddharth Asthana
2025-11-05 19:16             ` [PATCH v7 2/3] replay: make atomic ref updates the default behavior Siddharth Asthana
2025-11-05 19:16             ` [PATCH v7 3/3] replay: add replay.refAction config option Siddharth Asthana
2025-11-06 19:32             ` [PATCH v7 0/3] replay: make atomic ref updates the default Elijah Newren
2025-11-08 13:22               ` Siddharth Asthana
2025-11-08 17:11                 ` Elijah Newren
2025-11-07 15:48             ` Phillip Wood
2025-11-08 13:23               ` Siddharth Asthana
  -- strict thread matches above, loose matches on Subject: below --
2025-10-13 18:25 [PATCH v3 " Siddharth Asthana
2025-10-13 18:55 ` Siddharth Asthana
2025-10-14 21:13 ` Junio C Hamano
2025-10-15  5:05   ` Siddharth Asthana

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).