{"thread":{"id":"64109","subject":"[PATCH 0/2] replay: add --update-refs option","startedAt":"2025-09-08T04:36:31Z","lastAt":"2025-11-08T17:11:58Z","messageCount":125,"participants":["Siddharth Asthana","Christian Couder","Patrick Steinhardt","Kristoffer Haugsbakk","Elijah Newren","Junio C Hamano","Andrei Rybak","Phillip Wood","Karthik Nayak"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"525750","messageId":"20250908043620.57848-1-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":null,"subject":"[PATCH 0/2] replay: add --update-refs option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-08T04:36:18Z","receivedAt":"2025-09-08T04:36:31Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"This patch series adds a --update-refs option to git replay. Right now, \nwhen you use git replay, you need to pipe its output to git update-ref \nlike this:\n\n    git replay --onto main topic1..topic2 | git update-ref --stdin\n\nThis works fine, but it means running two commands and doesn't give you \natomic transactions by default. The new --update-refs option lets you do \nthe ref updates directly:\n\n    git replay --update-refs --onto main topic1..topic2\n\nI discussed this feature with Christian Couder earlier, and we agreed that \nit would be useful for server-side operations where you want atomic updates.\n\nThe way it works:\n- By default, it uses atomic transactions (all refs get updated or none do)\n- There's a --batch option if you want some updates to succeed even if \n  others fail\n- It works with bare repositories, which is important for server operations\n  like Gitaly\n- When it succeeds, it doesn't print anything (just like git update-ref \n  --stdin)\n- You can't use --update-refs with the existing --update option\n\nThis should help with git replay's goal of being good for server-side \noperations. It also makes the command simpler to use since you don't need \nthe pipeline anymore, and the atomic behavior is better for reliability.\n\nSiddharth Asthana (2):\n  replay: add --update-refs option for atomic ref updates\n  replay: document --update-refs and --batch options\n\n Documentation/git-replay.adoc |  62 ++++++-\n builtin/replay.c              | 134 +++++++++++++-\n t/meson.build                 |   1 +\n t/t3650-replay-basics.sh      | 323 ++++++++++++++++++++++++++++++++++\n t/t3651-replay-update-refs.sh | 273 ++++++++++++++++++++++++++++\n 5 files changed, 778 insertions(+), 15 deletions(-)\n create mode 100755 t/t3651-replay-update-refs.sh\n\n-- \n2.51.0\n\n"},{"id":"525751","messageId":"20250908043620.57848-2-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20250908043620.57848-1-siddharthasthana31@gmail.com","subject":"[PATCH 1/2] replay: add --update-refs option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-08T04:36:19Z","receivedAt":"2025-09-08T04:36:38Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"Currently, git replay outputs \"update\" commands that need to be piped to\n`git update-ref --stdin`:\n\n    git replay --onto main topic1..topic2 | git update-ref --stdin\n\nWhile this works, it requires users to run a two-command pipeline and\ndoesn't provide atomic transaction guarantees by default.\n\nThis patch adds --update-refs option that performs ref updates directly\nusing Git's ref transaction API instead of outputting update commands:\n\n    git replay --update-refs --onto main topic1..topic2\n\nThe implementation uses atomic transactions by default (all updates succeed\nor all fail) and supports an optional --batch flag for partial failure\ntolerance, similar to `git update-ref --stdin`.\n\nThe --update-refs option:\n- Uses ref_store_transaction_begin() with atomic mode by default\n- Supports --batch mode with REF_TRANSACTION_ALLOW_FAILURE flag\n- Works with all existing options: --onto, --advance, --contained\n- Follows the same patterns as builtin/update-ref.c\n- Works with bare repositories (important for server-side operations)\n- Produces no output on successful completion\n\nOption validation ensures --update-refs cannot be used with the existing\n--update option, and --batch can only be used with --update-refs.\n\nThis particularly benefits server-side Git operations (like Gitaly) that\nneed atomic ref updates, and users who want to avoid the two-command\npipeline for performance or reliability reasons.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n builtin/replay.c              | 134 +++++++++++++-\n t/meson.build                 |   1 +\n t/t3650-replay-basics.sh      | 323 ++++++++++++++++++++++++++++++++++\n t/t3651-replay-update-refs.sh | 273 ++++++++++++++++++++++++++++\n 4 files changed, 722 insertions(+), 9 deletions(-)\n create mode 100755 t/t3651-replay-update-refs.sh\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 6172c8aacc..a33c9887cf 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -284,6 +284,37 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \treturn create_commit(repo, result->tree, pickme, replayed_base);\n }\n \n+static int update_ref_direct(struct repository *repo, const char *refname,\n+\t\t\t     const struct object_id *new_oid,\n+\t\t\t     const struct object_id *old_oid)\n+{\n+\tconst char *msg = \"replay\";\n+\treturn refs_update_ref(get_main_ref_store(repo), msg, refname,\n+\t\t\t       new_oid, old_oid, 0, UPDATE_REFS_MSG_ON_ERR);\n+}\n+\n+static int add_ref_to_transaction(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  struct strbuf *err)\n+{\n+\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n+\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n+}\n+\n+static void print_rejected_update(const char *refname,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const char *old_target,\n+\t\t\t\t  const char *new_target,\n+\t\t\t\t  enum ref_transaction_error err,\n+\t\t\t\t  void *cb_data)\n+{\n+\tconst char *reason = ref_transaction_error_msg(err);\n+\twarning(_(\"failed to update %s: %s\"), refname, reason);\n+}\n+\n int cmd_replay(int argc,\n \t       const char **argv,\n \t       const char *prefix,\n@@ -294,6 +325,9 @@ int cmd_replay(int argc,\n \tstruct commit *onto = NULL;\n \tconst char *onto_name = NULL;\n \tint contained = 0;\n+\tint update_directly = 0;\n+\tint update_refs_flag = 0;\n+\tint batch_mode = 0;\n \n \tstruct rev_info revs;\n \tstruct commit *last_commit = NULL;\n@@ -302,12 +336,14 @@ int cmd_replay(int argc,\n \tstruct merge_result result;\n \tstruct strset *update_refs = NULL;\n \tkh_oid_map_t *replayed_commits;\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strbuf transaction_err = STRBUF_INIT;\n \tint ret = 0;\n \n \tconst char * const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n-\t\t   \"<revision-range>...\"),\n+\t\t   \"[--update | --update-refs [--batch]] <revision-range>...\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -319,6 +355,12 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &contained,\n \t\t\t N_(\"advance all branches contained in revision-range\")),\n+\t\tOPT_BOOL(0, \"update\", &update_directly,\n+\t\t\t N_(\"update branches directly instead of outputting update commands\")),\n+\t\tOPT_BOOL(0, \"update-refs\", &update_refs_flag,\n+\t\t\t N_(\"update branches using ref transactions\")),\n+\t\tOPT_BOOL(0, \"batch\", &batch_mode,\n+\t\t\t N_(\"allow partial ref updates in batch mode\")),\n \t\tOPT_END()\n \t};\n \n@@ -333,6 +375,14 @@ int cmd_replay(int argc,\n \tif (advance_name_opt && contained)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n \t\t    \"--advance\", \"--contained\");\n+\n+\tif (update_directly && update_refs_flag)\n+\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t    \"--update\", \"--update-refs\");\n+\n+\tif (batch_mode && !update_refs_flag)\n+\t\tdie(_(\"option '%s' can only be used with '%s'\"),\n+\t\t    \"--batch\", \"--update-refs\");\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n@@ -389,6 +439,18 @@ int cmd_replay(int argc,\n \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n \t\t\t      &onto, &update_refs);\n \n+\t/* Initialize ref transaction if using --update-refs */\n+\tif (update_refs_flag) {\n+\t\tunsigned int transaction_flags = batch_mode ? REF_TRANSACTION_ALLOW_FAILURE : 0;\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n+\t\t\t\t\t\t\t\t  transaction_flags,\n+\t\t\t\t\t\t\t\t  &transaction_err);\n+\t\tif (!transaction) {\n+\t\t\tret = error(_(\"failed to begin ref transaction: %s\"), transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n \tif (!onto) /* FIXME: Should handle replaying down to root commit */\n \t\tdie(\"Replaying down to root commit is not supported yet!\");\n \n@@ -399,6 +461,7 @@ int cmd_replay(int argc,\n \n \tinit_basic_merge_options(&merge_opt, repo);\n \tmemset(&result, 0, sizeof(result));\n+\tresult.clean = 1;  /* Assume clean until proven otherwise */\n \tmerge_opt.show_rename_progress = 0;\n \tlast_commit = onto;\n \treplayed_commits = kh_init_oid_map();\n@@ -434,10 +497,27 @@ int cmd_replay(int argc,\n \t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n \t\t\t    (contained || strset_contains(update_refs,\n \t\t\t\t\t\t\t  decoration->name))) {\n-\t\t\t\tprintf(\"update %s %s %s\\n\",\n-\t\t\t\t       decoration->name,\n-\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t\t\t       oid_to_hex(&commit->object.oid));\n+\t\t\t\tif (update_directly) {\n+\t\t\t\t\tif (update_ref_direct(repo, decoration->name,\n+\t\t\t\t\t\t\t     &last_commit->object.oid,\n+\t\t\t\t\t\t\t     &commit->object.oid) < 0) {\n+\t\t\t\t\t\tret = -1;\n+\t\t\t\t\t\tgoto cleanup;\n+\t\t\t\t\t}\n+\t\t\t\t} else if (transaction) {\n+\t\t\t\t\tif (add_ref_to_transaction(transaction, decoration->name,\n+\t\t\t\t\t\t\t\t   &last_commit->object.oid,\n+\t\t\t\t\t\t\t\t   &commit->object.oid,\n+\t\t\t\t\t\t\t\t   &transaction_err) < 0) {\n+\t\t\t\t\t\tret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n+\t\t\t\t\t\tgoto cleanup;\n+\t\t\t\t\t}\n+\t\t\t\t} else {\n+\t\t\t\t\tprintf(\"update %s %s %s\\n\",\n+\t\t\t\t\t       decoration->name,\n+\t\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n+\t\t\t\t\t       oid_to_hex(&commit->object.oid));\n+\t\t\t\t}\n \t\t\t}\n \t\t\tdecoration = decoration->next;\n \t\t}\n@@ -445,10 +525,43 @@ int cmd_replay(int argc,\n \n \t/* In --advance mode, advance the target ref */\n \tif (result.clean == 1 && advance_name) {\n-\t\tprintf(\"update %s %s %s\\n\",\n-\t\t       advance_name,\n-\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t       oid_to_hex(&onto->object.oid));\n+\t\tif (update_directly) {\n+\t\t\tif (update_ref_direct(repo, advance_name,\n+\t\t\t\t\t     &last_commit->object.oid,\n+\t\t\t\t\t     &onto->object.oid) < 0) {\n+\t\t\t\tret = -1;\n+\t\t\t\tgoto cleanup;\n+\t\t\t}\n+\t\t} else if (transaction) {\n+\t\t\tif (add_ref_to_transaction(transaction, advance_name,\n+\t\t\t\t\t\t   &last_commit->object.oid,\n+\t\t\t\t\t\t   &onto->object.oid,\n+\t\t\t\t\t\t   &transaction_err) < 0) {\n+\t\t\t\tret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n+\t\t\t\tgoto cleanup;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tprintf(\"update %s %s %s\\n\",\n+\t\t\t       advance_name,\n+\t\t\t       oid_to_hex(&last_commit->object.oid),\n+\t\t\t       oid_to_hex(&onto->object.oid));\n+\t\t}\n+\t}\n+\n+\t/* Commit the ref transaction if we have one */\n+\tif (transaction && result.clean == 1) {\n+\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n+\t\t\tif (batch_mode) {\n+\t\t\t\t/* Print failed updates in batch mode */\n+\t\t\t\twarning(_(\"some ref updates failed: %s\"), transaction_err.buf);\n+\t\t\t\tref_transaction_for_each_rejected_update(transaction,\n+\t\t\t\t\t\t\t\t\t\t print_rejected_update, NULL);\n+\t\t\t} else {\n+\t\t\t\t/* In atomic mode, all updates failed */\n+\t\t\t\tret = error(_(\"failed to update refs: %s\"), transaction_err.buf);\n+\t\t\t\tgoto cleanup;\n+\t\t\t}\n+\t\t}\n \t}\n \n \tmerge_finalize(&merge_opt, &result);\n@@ -460,6 +573,9 @@ int cmd_replay(int argc,\n \tret = result.clean;\n \n cleanup:\n+\tif (transaction)\n+\t\tref_transaction_free(transaction);\n+\tstrbuf_release(&transaction_err);\n \trelease_revisions(&revs);\n \tfree(advance_name);\n \ndiff --git a/t/meson.build b/t/meson.build\nindex daf01fb5d0..966b9d1b1f 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -397,6 +397,7 @@ integration_tests = [\n   't3601-rm-pathspec-file.sh',\n   't3602-rm-sparse-checkout.sh',\n   't3650-replay-basics.sh',\n+  't3651-replay-update-refs.sh',\n   't3700-add.sh',\n   't3701-add-interactive.sh',\n   't3702-add-edit.sh',\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 58b3759935..b5aac8c566 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -217,4 +217,327 @@ test_expect_success 'merge.directoryRenames=false' '\n \t\t--onto rename-onto rename-onto..rename-from\n '\n \n+test_expect_success 'using replay with --update to rebase a branch' '\n+\t# Store original branch tips\n+\tgit rev-parse topic2 >topic2.old &&\n+\t\n+\t# Use --update to directly update the refs\n+\tgit replay --update --onto main topic1..topic2 &&\n+\t\n+\t# Verify the branch was actually updated\n+\tgit rev-parse topic2 >topic2.new &&\n+\t! test_cmp topic2.old topic2.new &&\n+\t\n+\t# Verify the history is correct\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using replay with --update in advance mode' '\n+\t# Reset topic2 first\n+\tgit branch -f topic2 $(cat topic2.old) &&\n+\t\n+\t# Store original main tip\n+\tgit rev-parse main >main.old &&\n+\t\n+\t# Use --update with --advance\n+\tgit replay --update --advance main topic1..topic2 &&\n+\t\n+\t# Verify main was updated\n+\tgit rev-parse main >main.new &&\n+\t! test_cmp main.old main.new &&\n+\t\n+\t# Verify the history is correct\n+\tgit log --format=%s main >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual &&\n+\t\n+\t# Reset main back\n+\tgit branch -f main $(cat main.old)\n+'\n+\n+test_expect_success 'using replay with --update and --contained' '\n+\t# Store original branch tips\n+\tgit rev-parse topic1 >topic1.old &&\n+\tgit rev-parse topic3 >topic3.old &&\n+\t\n+\t# Use --update with --contained\n+\tgit replay --update --contained --onto main main..topic3 &&\n+\t\n+\t# Verify both branches were updated\n+\tgit rev-parse topic1 >topic1.new &&\n+\tgit rev-parse topic3 >topic3.new &&\n+\t! test_cmp topic1.old topic1.new &&\n+\t! test_cmp topic3.old topic3.new &&\n+\t\n+\t# Reset branches back\n+\tgit branch -f topic1 $(cat topic1.old) &&\n+\tgit branch -f topic3 $(cat topic3.old)\n+'\n+\n+test_expect_success 'replay with --update should not produce output when successful' '\n+\tgit replay --update --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output\n+'\n+\n+test_expect_success 'using replay with --update-refs to rebase a branch (atomic mode)' '\n+\t# Store original branch tip\n+\tgit rev-parse topic2 >topic2.old &&\n+\t\n+\t# Use --update-refs to directly update refs with transactions\n+\tgit replay --update-refs --onto main topic1..topic2 &&\n+\t\n+\t# Verify the branch was actually updated\n+\tgit rev-parse topic2 >topic2.new &&\n+\t! test_cmp topic2.old topic2.new &&\n+\t\n+\t# Verify the history is correct\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using replay with --update-refs in advance mode' '\n+\t# Store original main tip\n+\tgit rev-parse main >main.old &&\n+\t\n+\t# Use --update-refs with --advance\n+\tgit replay --update-refs --advance main topic1..topic2 &&\n+\t\n+\t# Verify main was updated\n+\tgit rev-parse main >main.new &&\n+\t! test_cmp main.old main.new &&\n+\t\n+\t# Verify the history is correct  \n+\tgit log --format=%s main >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using replay with --update-refs and --contained' '\n+\t# Store original branch tips\n+\tgit rev-parse topic1 >topic1.old &&\n+\tgit rev-parse topic3 >topic3.old &&\n+\t\n+\t# Use --update-refs with --contained\n+\tgit replay --update-refs --contained --onto main main..topic3 &&\n+\t\n+\t# Verify both branches were updated\n+\tgit rev-parse topic1 >topic1.new &&\n+\tgit rev-parse topic3 >topic3.new &&\n+\t! test_cmp topic1.old topic1.new &&\n+\t! test_cmp topic3.old topic3.new &&\n+\t\n+\t# Reset branches back\n+\tgit branch -f topic1 $(cat topic1.old) &&\n+\tgit branch -f topic3 $(cat topic3.old)\n+'\n+\n+test_expect_success 'replay with --update-refs should not produce output when successful' '\n+\tgit replay --update-refs --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output\n+'\n+\n+test_expect_success 'replay with --update-refs --batch should not produce output when successful' '\n+\tgit replay --update-refs --batch --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output\n+'\n+\n+test_expect_success 'replay fails when --update and --update-refs are used together' '\n+\ttest_must_fail git replay --update --update-refs --onto main topic1..topic2 2>error &&\n+\tgrep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'replay fails when --batch is used without --update-refs' '\n+\ttest_must_fail git replay --batch --onto main topic1..topic2 2>error &&\n+\tgrep \"can only be used with.*--update-refs\" error\n+'\n+\n+# Edge cases and comprehensive testing for --update-refs\n+\n+test_expect_success 'setup for edge case tests' '\n+\t# Create some additional branches for testing\n+\tgit checkout -b edge1 main &&\n+\ttest_commit Edge1 &&\n+\tgit checkout -b edge2 main &&\n+\ttest_commit Edge2 &&\n+\tgit checkout main\n+'\n+\n+test_expect_success '--update-refs with conflicting replay (atomic mode fails completely)' '\n+\t# Create a conflict scenario\n+\tgit checkout -b conflict-test main &&\n+\techo \"conflict content\" > C.t &&\n+\tgit add C.t &&\n+\tgit commit -m \"Conflicting change\" &&\n+\t\n+\t# Store original branch state\n+\tgit rev-parse conflict-test >conflict-test.old &&\n+\t\n+\t# This should fail due to conflict, and branch should remain unchanged\n+\ttest_expect_code 1 git replay --update-refs --onto topic1 main..conflict-test &&\n+\t\n+\t# Verify branch was not updated (atomic transaction rolled back)\n+\tgit rev-parse conflict-test >conflict-test.new &&\n+\ttest_cmp conflict-test.old conflict-test.new\n+'\n+\n+test_expect_success '--update-refs --batch with conflicting replay (partial success)' '\n+\t# Create scenario with one good commit and one conflicting commit\n+\tgit checkout -b batch-test main &&\n+\ttest_commit GoodCommit &&\n+\techo \"conflict\" > C.t &&\n+\tgit add C.t &&\n+\tgit commit -m \"Bad commit\" &&\n+\t\n+\t# Store original states\n+\tgit rev-parse batch-test >batch-test.old &&\n+\t\n+\t# Batch mode should handle partial failures gracefully\n+\t# Note: This test might need adjustment based on actual conflict behavior\n+\ttest_expect_code 1 git replay --update-refs --batch --onto topic1 main..batch-test 2>batch-error &&\n+\t\n+\t# In batch mode, we should get warnings rather than hard failures\n+\ttest_path_is_file batch-error\n+'\n+\n+test_expect_success '--update-refs with no commits to replay (empty transaction)' '\n+\t# Try to replay an empty range\n+\tgit rev-parse topic1 >topic1.before &&\n+\t\n+\t# This should succeed but do nothing\n+\tgit replay --update-refs --onto main topic1..topic1 &&\n+\t\n+\t# Branch should be unchanged\n+\tgit rev-parse topic1 >topic1.after &&\n+\ttest_cmp topic1.before topic1.after\n+'\n+\n+test_expect_success '--update-refs with multiple branches (atomic success)' '\n+\t# Store original states\n+\tgit rev-parse edge1 >edge1.old &&\n+\tgit rev-parse edge2 >edge2.old &&\n+\t\n+\t# Replay multiple branches atomically\n+\tgit replay --update-refs --contained --onto main main..edge1 &&\n+\tgit replay --update-refs --contained --onto main main..edge2 &&\n+\t\n+\t# Both should be updated\n+\tgit rev-parse edge1 >edge1.new &&\n+\tgit rev-parse edge2 >edge2.new &&\n+\t! test_cmp edge1.old edge1.new &&\n+\t! test_cmp edge2.old edge2.new\n+'\n+\n+test_expect_success '--update-refs atomic vs batch behavior comparison' '\n+\t# Create a branch for comparison\n+\tgit checkout -b compare-test main &&\n+\ttest_commit CompareCommit &&\n+\t\n+\t# Test atomic mode first\n+\tgit replay --update-refs --onto main main..compare-test &&\n+\tgit rev-parse compare-test >atomic-result &&\n+\t\n+\t# Reset and test batch mode\n+\tgit branch -f compare-test main &&\n+\ttest_commit CompareCommit &&\n+\tgit replay --update-refs --batch --onto main main..compare-test &&\n+\tgit rev-parse compare-test >batch-result &&\n+\t\n+\t# Results should be identical for successful cases\n+\ttest_cmp atomic-result batch-result\n+'\n+\n+test_expect_success '--update-refs preserves ref transaction semantics' '\n+\t# Create branch for testing\n+\tgit checkout -b transaction-test main &&\n+\ttest_commit TransactionCommit &&\n+\t\n+\t# Store original state\n+\tgit rev-parse transaction-test >before-transaction &&\n+\t\n+\t# Use --update-refs (should be atomic)\n+\tgit replay --update-refs --onto main main..transaction-test &&\n+\t\n+\t# Verify ref was updated\n+\tgit rev-parse transaction-test >after-transaction &&\n+\t! test_cmp before-transaction after-transaction &&\n+\t\n+\t# Verify commit history is correct\n+\tgit log --format=%s transaction-test >actual-history &&\n+\ttest_write_lines TransactionCommit M L B A >expected-history &&\n+\ttest_cmp expected-history actual-history\n+'\n+\n+test_expect_success '--update-refs with --advance preserves branch history' '\n+\t# Test that --advance with --update-refs works correctly\n+\tgit checkout -b advance-test main &&\n+\ttest_commit AdvanceCommit &&\n+\t\n+\t# Store original main state\n+\tgit rev-parse main >main-before-advance &&\n+\t\n+\t# Use --advance with --update-refs\n+\tgit replay --update-refs --advance main main..advance-test &&\n+\t\n+\t# Main should be updated\n+\tgit rev-parse main >main-after-advance &&\n+\t! test_cmp main-before-advance main-after-advance &&\n+\t\n+\t# Verify main has the right commits\n+\tgit log --format=%s main >main-history &&\n+\ttest_write_lines AdvanceCommit M L B A >expected-main &&\n+\ttest_cmp expected-main main-history\n+'\n+\n+test_expect_success '--update-refs handles ref updates consistently with traditional method' '\n+\t# Create test scenario\n+\tgit checkout -b consistency-test main &&\n+\ttest_commit ConsistencyTest &&\n+\t\n+\t# Method 1: Traditional output piped to update-ref\n+\tgit checkout -b trad-test consistency-test &&\n+\tgit replay --onto main main..consistency-test >update-commands &&\n+\tgit update-ref --stdin <update-commands &&\n+\tgit rev-parse trad-test >traditional-result &&\n+\t\n+\t# Method 2: Direct --update-refs\n+\tgit branch -f consistency-test main &&\n+\ttest_commit ConsistencyTest &&\n+\tgit checkout -b direct-test consistency-test &&\n+\tgit replay --update-refs --onto main main..consistency-test &&\n+\tgit rev-parse direct-test >direct-result &&\n+\t\n+\t# Results should be identical\n+\ttest_cmp traditional-result direct-result\n+'\n+\n+test_expect_success '--update-refs error messages are helpful' '\n+\t# Test that error messages are clear and helpful\n+\tgit checkout -b error-test main &&\n+\ttest_commit ErrorTest &&\n+\t\n+\t# Test conflicting options\n+\ttest_must_fail git replay --update --update-refs --onto main main..error-test 2>conflict-error &&\n+\tgrep \"cannot be used together\" conflict-error &&\n+\t\n+\t# Test batch without update-refs\n+\ttest_must_fail git replay --batch --onto main main..error-test 2>batch-error &&\n+\tgrep \"can only be used with\" batch-error\n+'\n+\n+test_expect_success '--update-refs with bare repository works correctly' '\n+\t# Test that --update-refs works in bare repositories (important for Gitaly)\n+\tgit checkout -b bare-test main &&\n+\ttest_commit BareTest &&\n+\t\n+\t# Test with bare repo (using existing bare setup)\n+\tgit -C bare replay --update-refs --onto main main..bare-test &&\n+\t\n+\t# Verify the bare repo was updated correctly\n+\tgit -C bare rev-parse bare-test >bare-result &&\n+\ttest -s bare-result\n+'\n+\n test_done\ndiff --git a/t/t3651-replay-update-refs.sh b/t/t3651-replay-update-refs.sh\nnew file mode 100755\nindex 0000000000..fcd4d36721\n--- /dev/null\n+++ b/t/t3651-replay-update-refs.sh\n@@ -0,0 +1,273 @@\n+#!/bin/sh\n+\n+test_description='git replay --update-refs edge cases and comprehensive testing'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+GIT_AUTHOR_NAME=author@name\n+GIT_AUTHOR_EMAIL=bogus@email@address\n+export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL\n+\n+test_expect_success 'setup for update-refs tests' '\n+\ttest_commit A &&\n+\ttest_commit B &&\n+\n+\tgit switch -c topic1 &&\n+\ttest_commit C &&\n+\tgit switch -c topic2 &&\n+\ttest_commit D &&\n+\ttest_commit E &&\n+\tgit switch topic1 &&\n+\ttest_commit F &&\n+\n+\tgit switch main &&\n+\ttest_commit L &&\n+\ttest_commit M &&\n+\n+\tgit switch -c conflict B &&\n+\ttest_commit C.conflict C.t conflict\n+'\n+\n+test_expect_success 'setup bare repo' '\n+\tgit clone --bare . bare\n+'\n+\n+# Basic functionality tests\n+\n+test_expect_success '--update-refs works in atomic mode (basic)' '\n+\t# Store original branch tip\n+\tgit rev-parse topic2 >topic2.old &&\n+\t\n+\t# Use --update-refs to directly update refs with transactions\n+\tgit replay --update-refs --onto main topic1..topic2 &&\n+\t\n+\t# Verify the branch was actually updated\n+\tgit rev-parse topic2 >topic2.new &&\n+\t! test_cmp topic2.old topic2.new &&\n+\t\n+\t# Verify the history is correct\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--update-refs works with --advance' '\n+\t# Store original main tip\n+\tgit rev-parse main >main.old &&\n+\t\n+\t# Use --update-refs with --advance\n+\tgit replay --update-refs --advance main topic1..topic2 &&\n+\t\n+\t# Verify main was updated\n+\tgit rev-parse main >main.new &&\n+\t! test_cmp main.old main.new &&\n+\t\n+\t# Verify the history is correct  \n+\tgit log --format=%s main >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--update-refs produces no output on success' '\n+\tgit checkout -b quiet-test topic1 &&\n+\tgit replay --update-refs --onto main topic1..quiet-test >output &&\n+\ttest_must_be_empty output\n+'\n+\n+test_expect_success '--update-refs --batch produces no output on success' '\n+\tgit checkout -b batch-quiet-test topic1 &&\n+\tgit replay --update-refs --batch --onto main topic1..batch-quiet-test >output &&\n+\ttest_must_be_empty output\n+'\n+\n+# Edge case tests\n+\n+test_expect_success '--update-refs with empty range (no-op)' '\n+\t# Store original branch tip\n+\tgit rev-parse topic1 >topic1.before &&\n+\t\n+\t# Try to replay an empty range - should succeed but do nothing\n+\tgit replay --update-refs --onto main topic1..topic1 &&\n+\t\n+\t# Branch should be unchanged\n+\tgit rev-parse topic1 >topic1.after &&\n+\ttest_cmp topic1.before topic1.after\n+'\n+\n+test_expect_success '--update-refs atomic vs batch mode comparison' '\n+\t# Create branch for comparison\n+\tgit checkout -b compare1 topic1 &&\n+\ttest_commit Compare1 &&\n+\tgit checkout -b compare2 topic1 &&\n+\ttest_commit Compare2 &&\n+\t\n+\t# Test atomic mode\n+\tgit replay --update-refs --onto main topic1..compare1 &&\n+\tgit rev-parse compare1 >atomic-result &&\n+\t\n+\t# Test batch mode - should give same result for successful case\n+\tgit replay --update-refs --batch --onto main topic1..compare2 &&\n+\tgit rev-parse compare2 >batch-result &&\n+\t\n+\t# The OIDs will be different since commits are different,\n+\t# but both should have been updated (not equal to original)\n+\tgit rev-parse topic1 >original &&\n+\t! test_cmp atomic-result original &&\n+\t! test_cmp batch-result original\n+'\n+\n+test_expect_success '--update-refs handles conflict gracefully in atomic mode' '\n+\t# Create a branch that will conflict\n+\tgit checkout -b atomic-conflict B &&\n+\techo \"different content\" >C.t &&\n+\tgit add C.t &&\n+\tgit commit -m \"Conflicting C\" &&\n+\t\n+\t# Store original state\n+\tgit rev-parse atomic-conflict >conflict-before &&\n+\t\n+\t# This should fail due to conflict\n+\ttest_expect_code 1 git replay --update-refs --onto conflict atomic-conflict^..atomic-conflict &&\n+\t\n+\t# In atomic mode, branch should remain unchanged\n+\tgit rev-parse atomic-conflict >conflict-after &&\n+\ttest_cmp conflict-before conflict-after\n+'\n+\n+test_expect_success '--update-refs preserves transaction semantics' '\n+\t# Create test branch\n+\tgit checkout -b transaction-test topic1 &&\n+\ttest_commit TransactionTest &&\n+\t\n+\t# Store original state\n+\tgit rev-parse transaction-test >before-transaction &&\n+\t\n+\t# Use --update-refs (should be atomic)\n+\tgit replay --update-refs --onto main topic1..transaction-test &&\n+\t\n+\t# Verify ref was updated\n+\tgit rev-parse transaction-test >after-transaction &&\n+\t! test_cmp before-transaction after-transaction &&\n+\t\n+\t# Verify commit history is preserved correctly\n+\tgit log --format=%s transaction-test >actual-history &&\n+\ttest_write_lines TransactionTest M L B A >expected-history &&\n+\ttest_cmp expected-history actual-history\n+'\n+\n+test_expect_success '--update-refs vs traditional method equivalence' '\n+\t# Create test branches\n+\tgit checkout -b traditional topic1 &&\n+\ttest_commit Traditional &&\n+\tgit checkout -b direct topic1 &&\n+\ttest_commit Direct &&\n+\t\n+\t# Method 1: Traditional output + update-ref\n+\tgit replay --onto main topic1..traditional >update-commands &&\n+\tgit update-ref --stdin <update-commands &&\n+\tgit rev-parse traditional >traditional-result &&\n+\t\n+\t# Method 2: Direct --update-refs\n+\tgit replay --update-refs --onto main topic1..direct &&\n+\tgit rev-parse direct >direct-result &&\n+\t\n+\t# Both methods should produce equivalent results\n+\t# (OIDs will be different due to different commits, but both should be updated)\n+\tgit rev-parse topic1 >original &&\n+\t! test_cmp traditional-result original &&\n+\t! test_cmp direct-result original\n+'\n+\n+# Error handling and validation tests\n+\n+test_expect_success 'error messages are helpful and clear' '\n+\t# Test conflicting options\n+\ttest_must_fail git replay --update --update-refs --onto main topic1..topic2 2>error1 &&\n+\tgrep \"cannot be used together\" error1 &&\n+\t\n+\t# Test batch without update-refs\n+\ttest_must_fail git replay --batch --onto main topic1..topic2 2>error2 &&\n+\tgrep \"can only be used with.*--update-refs\" error2\n+'\n+\n+test_expect_success '--update-refs works correctly with bare repositories' '\n+\t# Create branch for bare repo testing\n+\tgit checkout -b bare-test topic1 &&\n+\ttest_commit BareTest &&\n+\t\n+\t# Test with bare repo (important for Gitaly use case)\n+\tgit -C bare fetch .. bare-test:bare-test &&\n+\tgit -C bare replay --update-refs --onto main topic1..bare-test &&\n+\t\n+\t# Verify the bare repo was updated correctly\n+\tgit -C bare rev-parse bare-test >bare-result &&\n+\ttest -s bare-result &&\n+\t\n+\t# Verify it is different from original\n+\tgit rev-parse topic1 >original &&\n+\t! test_cmp bare-result original\n+'\n+\n+test_expect_success '--update-refs maintains ref update ordering' '\n+\t# Create multiple branches to test ordering\n+\tgit checkout -b order1 topic1 &&\n+\ttest_commit Order1 &&\n+\tgit checkout -b order2 topic1 &&\n+\ttest_commit Order2 &&\n+\t\n+\t# Store original states\n+\tgit rev-parse order1 >order1-before &&\n+\tgit rev-parse order2 >order2-before &&\n+\t\n+\t# Update both branches\n+\tgit replay --update-refs --onto main topic1..order1 &&\n+\tgit replay --update-refs --onto main topic1..order2 &&\n+\t\n+\t# Verify both were updated\n+\tgit rev-parse order1 >order1-after &&\n+\tgit rev-parse order2 >order2-after &&\n+\t! test_cmp order1-before order1-after &&\n+\t! test_cmp order2-before order2-after\n+'\n+\n+test_expect_success '--update-refs handles ref transaction cleanup properly' '\n+\t# This test ensures no ref transaction leaks occur\n+\tgit checkout -b cleanup-test topic1 &&\n+\ttest_commit CleanupTest &&\n+\t\n+\t# Run multiple operations to test cleanup\n+\tgit replay --update-refs --onto main topic1..cleanup-test &&\n+\tgit replay --update-refs --batch --onto main topic1..cleanup-test &&\n+\t\n+\t# If cleanup is working properly, these should succeed without errors\n+\ttest_path_is_file .git/refs/heads/cleanup-test\n+'\n+\n+# Performance and stress tests\n+\n+test_expect_success '--update-refs performance is reasonable' '\n+\t# Create several commits to test performance\n+\tgit checkout -b perf-test topic1 &&\n+\tfor i in 1 2 3 4 5; do\n+\t\ttest_commit \"Perf$i\" || return 1\n+\tdone &&\n+\t\n+\t# Time the traditional method\n+\ttime git replay --onto main topic1..perf-test >perf-commands &&\n+\ttime git update-ref --stdin <perf-commands &&\n+\t\n+\t# Reset and time the new method\n+\tgit branch -f perf-test topic1 &&\n+\tfor i in 1 2 3 4 5; do\n+\t\ttest_commit \"Perf$i\" || return 1\n+\tdone &&\n+\ttime git replay --update-refs --onto main topic1..perf-test &&\n+\t\n+\t# Test completed successfully if we got here\n+\ttrue\n+'\n+\n+test_done\n\\ No newline at end of file\n-- \n2.51.0\n\n"},{"id":"525752","messageId":"20250908043620.57848-3-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20250908043620.57848-1-siddharthasthana31@gmail.com","subject":"[PATCH 2/2] replay: document --update-refs and --batch options","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-08T04:36:20Z","receivedAt":"2025-09-08T04:36:44Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"Add documentation for the new --update-refs option which performs\nref updates directly using Git's ref transaction API, eliminating\nthe need for users to pipe output to git update-ref --stdin.\n\nAlso document the --batch option which can be used with --update-refs\nto allow partial failures in ref updates.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc | 62 +++++++++++++++++++++++++++++++----\n 1 file changed, 56 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 0b12bf8aa4..cc9f868c2f 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,16 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--update | --update-refs [--batch]] <revision-range>...\n \n DESCRIPTION\n -----------\n \n Takes ranges of commits and replays them onto a new location. Leaves\n-the working tree and the index untouched, and updates no references.\n-The output of this command is meant to be used as input to\n+the working tree and the index untouched, and by default updates no \n+references. The output of this command is meant to be used as input to\n `git update-ref --stdin`, which would update the relevant branches\n-(see the OUTPUT section below).\n+(see the OUTPUT section below). Alternatively, with `--update`, the\n+refs can be updated directly.\n \n THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n \n@@ -42,6 +43,24 @@ When `--advance` is specified, the update-ref command(s) in the output\n will update the branch passed as an argument to `--advance` to point at\n the new commits (in other words, this mimics a cherry-pick operation).\n \n+--update::\n+\tUpdate the relevant refs directly instead of outputting\n+\tupdate-ref commands. When this option is used, no output is\n+\tproduced on successful completion, and the refs are updated\n+\timmediately. If any ref update fails, the command will exit\n+\twith a non-zero status.\n+\n+--update-refs::\n+\tUpdate the relevant refs using ref transactions instead of outputting\n+\tupdate-ref commands. By default, uses atomic mode where all ref updates\n+\tsucceed or all fail. Use with `--batch` to allow partial updates.\n+\tWhen this option is used, no output is produced on successful completion.\n+\n+--batch::\n+\tCan only be used with `--update-refs`. Enables batch mode for ref\n+\tupdates, allowing some refs to be updated successfully even if others\n+\tfail. Failed updates are reported as warnings rather than errors.\n+\n <revision-range>::\n \tRange of commits to replay. More than one <revision-range> can\n \tbe passed, but in `--advance <branch>` mode, they should have\n@@ -54,8 +73,9 @@ include::rev-list-options.adoc[]\n OUTPUT\n ------\n \n-When there are no conflicts, the output of this command is usable as\n-input to `git update-ref --stdin`.  It is of the form:\n+When there are no conflicts and neither `--update` nor `--update-refs` \n+is used, the output of this command is usable as input to `git update-ref --stdin`.  \n+It is of the form:\n \n \tupdate refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n \tupdate refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n@@ -66,6 +86,15 @@ the shape of the history being replayed.  When using `--advance`, the\n number of refs updated is always one, but for `--onto`, it can be one\n or more (rebasing multiple branches simultaneously is supported).\n \n+When `--update` is used, no output is produced and the refs are updated\n+directly using individual ref updates. This is equivalent to piping the normal output to \n+`git update-ref --stdin`.\n+\n+When `--update-refs` is used, no output is produced and the refs are updated\n+using ref transactions. In atomic mode (default), all ref updates succeed \n+or all fail. In batch mode (with `--batch`), some updates may succeed while \n+others fail, with failed updates reported as warnings.\n+\n EXIT STATUS\n -----------\n \n@@ -91,6 +120,27 @@ $ git replay --advance target origin/main..mybranch\n update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n ------------\n \n+To rebase `mybranch` onto `target` and update the ref directly:\n+\n+------------\n+$ git replay --update --onto target origin/main..mybranch\n+# No output; mybranch is updated directly\n+------------\n+\n+To rebase `mybranch` onto `target` using atomic ref transactions:\n+\n+------------\n+$ git replay --update-refs --onto target origin/main..mybranch\n+# No output; mybranch is updated atomically\n+------------\n+\n+To rebase multiple branches with partial failure tolerance:\n+\n+------------\n+$ git replay --update-refs --batch --contained --onto origin/main origin/main..tipbranch\n+# No output; refs updated in batch mode, warnings for any failures\n+------------\n+\n Note that the first two examples replay the exact same commits and on\n top of the exact same new base, they only differ in that the first\n provides instructions to make mybranch point at the new commits and\n-- \n2.51.0\n\n"},{"id":"525764","messageId":"CAP8UFD3Db-n3CY=KBpn-2Nt=SYY=5ckF3J_4ho6C19SVcrfdsQ@mail.gmail.com","threadId":"64109","inReplyTo":"20250908043620.57848-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH 2/2] replay: document --update-refs and --batch options","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T06:00:44Z","receivedAt":"2025-09-08T06:00:57Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> Add documentation for the new --update-refs option which performs\n> ref updates directly using Git's ref transaction API, eliminating\n> the need for users to pipe output to git update-ref --stdin.\n\nMost of the time, the documentation should be part of the patch that\nintroduces the documented behavior, not in a separate patch.\n\n> Also document the --batch option which can be used with --update-refs\n> to allow partial failures in ref updates.\n\nIt looks like a --update option was also added by the previous patch.\nIs it documented here too?\n\nWhy was this [--update | --update-refs [--batch]] set of options\nselected over other possibilities like for example\n[--update-iteratively | --update-atomically | --update-batch]?\n\nAlso how does this --update-refs option compare to the --update-refs\noption in git rebase? Is it working in the same way?\n\n> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> ---\n>  Documentation/git-replay.adoc | 62 +++++++++++++++++++++++++++++++----\n>  1 file changed, 56 insertions(+), 6 deletions(-)\n>\n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index 0b12bf8aa4..cc9f868c2f 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -9,16 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--update | --update-refs [--batch]] <revision-range>...\n\nHere --update, --update-refs and --batch are all documented, nice.\n\n>  DESCRIPTION\n>  -----------\n>\n>  Takes ranges of commits and replays them onto a new location. Leaves\n> -the working tree and the index untouched, and updates no references.\n> -The output of this command is meant to be used as input to\n> +the working tree and the index untouched, and by default updates no\n> +references. The output of this command is meant to be used as input to\n>  `git update-ref --stdin`, which would update the relevant branches\n> -(see the OUTPUT section below).\n> +(see the OUTPUT section below). Alternatively, with `--update`, the\n> +refs can be updated directly.\n\nHere only --update is documented.\n\n>  THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n>\n> @@ -42,6 +43,24 @@ When `--advance` is specified, the update-ref command(s) in the output\n>  will update the branch passed as an argument to `--advance` to point at\n>  the new commits (in other words, this mimics a cherry-pick operation).\n>\n> +--update::\n> +       Update the relevant refs directly instead of outputting\n> +       update-ref commands. When this option is used, no output is\n> +       produced on successful completion,\n\nIt seems a bit redundant to say both \"instead of outputting update-ref\ncommands\" and then \"no output is produced on successful completion\".\nMaybe there is a way to reword this to be a bit more concise.\n\n> and the refs are updated\n> +       immediately. If any ref update fails, the command will exit\n> +       with a non-zero status.\n\nThis doesn't say if the command immediately stops when it fails to\nupdate a ref, and if the ref updates are atomic or not.\n\n> +--update-refs::\n> +       Update the relevant refs using ref transactions instead of outputting\n> +       update-ref commands. By default, uses atomic mode where all ref updates\n> +       succeed or all fail.\n\nThis seems to imply that --update doesn't update the refs atomically.\n\n> Use with `--batch` to allow partial updates.\n\nWhat about --update, when should it be used?\n\n> +       When this option is used, no output is produced on successful completion.\n\nHere also it seems a bit redundant to say both \"instead of outputting\nupdate-ref commands\" and then \"no output is produced on successful\ncompletion\". And maybe there is a way to reword this to be a bit more\nconcise.\n\n> +--batch::\n> +       Can only be used with `--update-refs`. Enables batch mode for ref\n> +       updates, allowing some refs to be updated successfully even if others\n> +       fail. Failed updates are reported as warnings rather than errors.\n\nWhat's the difference with --update? Is it that --update immediately\nstops when a ref update fails?\n\n>  <revision-range>::\n>         Range of commits to replay. More than one <revision-range> can\n>         be passed, but in `--advance <branch>` mode, they should have\n> @@ -54,8 +73,9 @@ include::rev-list-options.adoc[]\n>  OUTPUT\n>  ------\n>\n> -When there are no conflicts, the output of this command is usable as\n> -input to `git update-ref --stdin`.  It is of the form:\n> +When there are no conflicts and neither `--update` nor `--update-refs`\n> +is used, the output of this command is usable as input to `git update-ref --stdin`.\n> +It is of the form:\n>\n>         update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n>         update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n> @@ -66,6 +86,15 @@ the shape of the history being replayed.  When using `--advance`, the\n>  number of refs updated is always one, but for `--onto`, it can be one\n>  or more (rebasing multiple branches simultaneously is supported).\n>\n> +When `--update` is used, no output is produced and the refs are updated\n> +directly using individual ref updates. This is equivalent to piping the normal output to\n> +`git update-ref --stdin`.\n\nIs it equivalent to `git update-ref --stdin` because both exit as soon\nas a ref update fails?\n\n> +When `--update-refs` is used, no output is produced and the refs are updated\n> +using ref transactions. In atomic mode (default), all ref updates succeed\n> +or all fail. In batch mode (with `--batch`), some updates may succeed while\n> +others fail, with failed updates reported as warnings.\n"},{"id":"525765","messageId":"CAP8UFD2XyqgypPfkQav4Fub0AEwyJjXpvfwMPe-adWyCKRa7fQ@mail.gmail.com","threadId":"64109","inReplyTo":"20250908043620.57848-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH 0/2] replay: add --update-refs option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-08T06:07:14Z","receivedAt":"2025-09-08T06:07:27Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> This patch series adds a --update-refs option to git replay. Right now,\n> when you use git replay, you need to pipe its output to git update-ref\n> like this:\n>\n>     git replay --onto main topic1..topic2 | git update-ref --stdin\n>\n> This works fine, but it means running two commands and doesn't give you\n> atomic transactions by default. The new --update-refs option lets you do\n> the ref updates directly:\n>\n>     git replay --update-refs --onto main topic1..topic2\n\nThanks for working on this.\n\n> I discussed this feature with Christian Couder earlier, and we agreed that\n> it would be useful for server-side operations where you want atomic updates.\n\nYeah, right. This is something the Git team at GitLab has been\ninterested in for some time.\n\n> The way it works:\n> - By default, it uses atomic transactions (all refs get updated or none do)\n> - There's a --batch option if you want some updates to succeed even if\n>   others fail\n> - It works with bare repositories, which is important for server operations\n>   like Gitaly\n> - When it succeeds, it doesn't print anything (just like git update-ref\n>   --stdin)\n> - You can't use --update-refs with the existing --update option\n\nThere is no existing --update option. This series also introduces the\n--update option.\n\n> This should help with git replay's goal of being good for server-side\n> operations. It also makes the command simpler to use since you don't need\n> the pipeline anymore, and the atomic behavior is better for reliability.\n\nI have commented only on the documentation patch for now as I think\nit's better to review the design of the new options first, and the\ndocumentation looks like a good place for that.\n\nThanks again.\n"},{"id":"525787","messageId":"aL6n8KEHSDii5Wd1@pks.im","threadId":"64109","inReplyTo":"20250908043620.57848-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH 1/2] replay: add --update-refs option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-08T09:54:56Z","receivedAt":"2025-09-08T09:55:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Sep 08, 2025 at 10:06:19AM +0530, Siddharth Asthana wrote:\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index 6172c8aacc..a33c9887cf 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -284,6 +284,37 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>  \treturn create_commit(repo, result->tree, pickme, replayed_base);\n>  }\n>  \n> +static int update_ref_direct(struct repository *repo, const char *refname,\n> +\t\t\t     const struct object_id *new_oid,\n> +\t\t\t     const struct object_id *old_oid)\n> +{\n> +\tconst char *msg = \"replay\";\n> +\treturn refs_update_ref(get_main_ref_store(repo), msg, refname,\n> +\t\t\t       new_oid, old_oid, 0, UPDATE_REFS_MSG_ON_ERR);\n> +}\n\nIs there a strong reason why a user would want to update refs one by\none? If not, let's not add new code to our base that does so. This is\nknown to be inperformant for the reftable backend, but also for the\nfiles backend in some cases. If we really want to support the case where\nonly a subset of references gets committed we should be using batched\nupdates with the `REF_TRANSACTION_ALLOW_FAILURE` flag.\n\n> @@ -319,6 +355,12 @@ int cmd_replay(int argc,\n>  \t\t\t   N_(\"replay onto given commit\")),\n>  \t\tOPT_BOOL(0, \"contained\", &contained,\n>  \t\t\t N_(\"advance all branches contained in revision-range\")),\n> +\t\tOPT_BOOL(0, \"update\", &update_directly,\n> +\t\t\t N_(\"update branches directly instead of outputting update commands\")),\n> +\t\tOPT_BOOL(0, \"update-refs\", &update_refs_flag,\n> +\t\t\t N_(\"update branches using ref transactions\")),\n> +\t\tOPT_BOOL(0, \"batch\", &batch_mode,\n> +\t\t\t N_(\"allow partial ref updates in batch mode\")),\n>  \t\tOPT_END()\n>  \t};\n>  \n\nSo I think we should reduce this to only accept two flags:\n`--update-refs` and a flag that accepts a subset of refs failing.o\n\nWe might also want to make this something like `--update-refs[=<mode>]`,\nwhere `<mode>` could be \"allow-failures\".\n\n> @@ -333,6 +375,14 @@ int cmd_replay(int argc,\n>  \tif (advance_name_opt && contained)\n>  \t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n>  \t\t    \"--advance\", \"--contained\");\n> +\n> +\tif (update_directly && update_refs_flag)\n> +\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n> +\t\t    \"--update\", \"--update-refs\");\n> +\n> +\tif (batch_mode && !update_refs_flag)\n> +\t\tdie(_(\"option '%s' can only be used with '%s'\"),\n> +\t\t    \"--batch\", \"--update-refs\");\n>  \tadvance_name = xstrdup_or_null(advance_name_opt);\n>  \n>  \trepo_init_revisions(repo, &revs, prefix);\n\nWe have the `die_for_incompatible_opt*()` helpers for this.\n\n> @@ -389,6 +439,18 @@ int cmd_replay(int argc,\n>  \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n>  \t\t\t      &onto, &update_refs);\n>  \n> +\t/* Initialize ref transaction if using --update-refs */\n\nNit: the comment doesn't really add much context, so I'd just drop it.\nIt's generally discouraged to add a comment that re-states what the code\nalready says. Instead, comments should point out things that are easy to\nmiss or not obvious at all.\n\n> @@ -445,10 +525,43 @@ int cmd_replay(int argc,\n>  \n>  \t/* In --advance mode, advance the target ref */\n>  \tif (result.clean == 1 && advance_name) {\n> -\t\tprintf(\"update %s %s %s\\n\",\n> -\t\t       advance_name,\n> -\t\t       oid_to_hex(&last_commit->object.oid),\n> -\t\t       oid_to_hex(&onto->object.oid));\n> +\t\tif (update_directly) {\n> +\t\t\tif (update_ref_direct(repo, advance_name,\n> +\t\t\t\t\t     &last_commit->object.oid,\n> +\t\t\t\t\t     &onto->object.oid) < 0) {\n> +\t\t\t\tret = -1;\n> +\t\t\t\tgoto cleanup;\n> +\t\t\t}\n> +\t\t} else if (transaction) {\n> +\t\t\tif (add_ref_to_transaction(transaction, advance_name,\n> +\t\t\t\t\t\t   &last_commit->object.oid,\n> +\t\t\t\t\t\t   &onto->object.oid,\n> +\t\t\t\t\t\t   &transaction_err) < 0) {\n> +\t\t\t\tret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n> +\t\t\t\tgoto cleanup;\n> +\t\t\t}\n> +\t\t} else {\n> +\t\t\tprintf(\"update %s %s %s\\n\",\n> +\t\t\t       advance_name,\n> +\t\t\t       oid_to_hex(&last_commit->object.oid),\n> +\t\t\t       oid_to_hex(&onto->object.oid));\n> +\t\t}\n> +\t}\n> +\n> +\t/* Commit the ref transaction if we have one */\n\nLikewise here.\n\n> +\tif (transaction && result.clean == 1) {\n> +\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n> +\t\t\tif (batch_mode) {\n> +\t\t\t\t/* Print failed updates in batch mode */\n> +\t\t\t\twarning(_(\"some ref updates failed: %s\"), transaction_err.buf);\n> +\t\t\t\tref_transaction_for_each_rejected_update(transaction,\n> +\t\t\t\t\t\t\t\t\t\t print_rejected_update, NULL);\n> +\t\t\t} else {\n> +\t\t\t\t/* In atomic mode, all updates failed */\n> +\t\t\t\tret = error(_(\"failed to update refs: %s\"), transaction_err.buf);\n> +\t\t\t\tgoto cleanup;\n> +\t\t\t}\n> +\t\t}\n>  \t}\n>  \n>  \tmerge_finalize(&merge_opt, &result);\n\nPatrick\n"},{"id":"525826","messageId":"e8bca1d7-96d7-42b3-95b7-6a525fd3f67d@app.fastmail.com","threadId":"64109","inReplyTo":"20250908043620.57848-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH 0/2] replay: add --update-refs option","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-09-08T14:33:13Z","receivedAt":"2025-09-08T14:35:56Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Sep 8, 2025, at 06:36, Siddharth Asthana wrote:\n> This patch series adds a --update-refs option to git replay. Right now,\n> when you use git replay, you need to pipe its output to git update-ref\n> like this:\n>[snip]\n\nBoth patches introduce whitespace errors.  You can check with\n`ci/check-whitespace.sh`.\n\nThat script will suggest a way to fix it.\n\nThere’s also a `\\ No newline at end of file` (I don’t think whitespace-\ncheck checks that).\n"},{"id":"525827","messageId":"ecdd1191-844b-47ca-9737-cc2ffb72b37d@app.fastmail.com","threadId":"64109","inReplyTo":"20250908043620.57848-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH 2/2] replay: document --update-refs and --batch options","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-09-08T14:40:02Z","receivedAt":"2025-09-08T14:40:24Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Sep 8, 2025, at 06:36, Siddharth Asthana wrote:\n>[snip]\n> diff --git a/Documentation/git-replay.adoc\n> b/Documentation/git-replay.adoc\n> index 0b12bf8aa4..cc9f868c2f 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -9,16 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new\n> base, works with bare repos t\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> |\n> --advance <branch>) <revision-range>...\n> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--update | --update-refs [--batch]] <revision-range>...\n\nAnother downside of making a separate commit for the documentation is\nthat now `t/t0450-txt-doc-vs-help.sh` will likely fail for your first\ncommit.  One of the tests makes sure that the synopsis and the `.adoc`\nis in synch.\n\n>\n>  DESCRIPTION\n>  -----------\n>[snip]\n"},{"id":"525902","messageId":"7f90e1b6-acba-40f2-9e51-ad09c2bf6999@gmail.com","threadId":"64109","inReplyTo":"CAP8UFD3Db-n3CY=KBpn-2Nt=SYY=5ckF3J_4ho6C19SVcrfdsQ@mail.gmail.com","subject":"Re: [PATCH 2/2] replay: document --update-refs and --batch options","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-09T06:36:32Z","receivedAt":"2025-09-09T06:36:38Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 08/09/25 11:30, Christian Couder wrote:\n> On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> Add documentation for the new --update-refs option which performs\n>> ref updates directly using Git's ref transaction API, eliminating\n>> the need for users to pipe output to git update-ref --stdin.\nHi Christian,\n\nThanks for the detailed review.\n> Most of the time, the documentation should be part of the patch that\n> introduces the documented behavior, not in a separate patch.\nYou are right I will combine them in v2.\n>\n>> Also document the --batch option which can be used with --update-refs\n>> to allow partial failures in ref updates.\n> It looks like a --update option was also added by the previous patch.\n> Is it documented here too?\n>\n> Why was this [--update | --update-refs [--batch]] set of options\n> selected over other possibilities like for example\n> [--update-iteratively | --update-atomically | --update-batch]?\nI was trying to provide both simple and advanced modes. --update for \nusers who just want \"make it work like piping to git update-ref --stdin\" \nand --update-refs for those who want control over transaction modes. But \nI see this creates confusion.\n\nWould you prefer a single option like --update-refs with an optional \nmode parameter? Something like --update-refs[=batch] where default is \natomic?\n>\n> Also how does this --update-refs option compare to the --update-refs\n> option in git rebase? Is it working in the same way?\nNo, they are different. git rebase --update-refs updates refs that point \nto commits being rebased. --update-refs updates the target branches from \nthe replay operation itself. The naming collision is unfortunate should \nI use a different name?\n>\n>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n>> ---\n>>   Documentation/git-replay.adoc | 62 +++++++++++++++++++++++++++++++----\n>>   1 file changed, 56 insertions(+), 6 deletions(-)\n>>\n>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n>> index 0b12bf8aa4..cc9f868c2f 100644\n>> --- a/Documentation/git-replay.adoc\n>> +++ b/Documentation/git-replay.adoc\n>> @@ -9,16 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>>   SYNOPSIS\n>>   --------\n>>   [verse]\n>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n>> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--update | --update-refs [--batch]] <revision-range>...\n> Here --update, --update-refs and --batch are all documented, nice.\n>\n>>   DESCRIPTION\n>>   -----------\n>>\n>>   Takes ranges of commits and replays them onto a new location. Leaves\n>> -the working tree and the index untouched, and updates no references.\n>> -The output of this command is meant to be used as input to\n>> +the working tree and the index untouched, and by default updates no\n>> +references. The output of this command is meant to be used as input to\n>>   `git update-ref --stdin`, which would update the relevant branches\n>> -(see the OUTPUT section below).\n>> +(see the OUTPUT section below). Alternatively, with `--update`, the\n>> +refs can be updated directly.\n> Here only --update is documented.\n>\n>>   THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n>>\n>> @@ -42,6 +43,24 @@ When `--advance` is specified, the update-ref command(s) in the output\n>>   will update the branch passed as an argument to `--advance` to point at\n>>   the new commits (in other words, this mimics a cherry-pick operation).\n>>\n>> +--update::\n>> +       Update the relevant refs directly instead of outputting\n>> +       update-ref commands. When this option is used, no output is\n>> +       produced on successful completion,\n> It seems a bit redundant to say both \"instead of outputting update-ref\n> commands\" and then \"no output is produced on successful completion\".\n> Maybe there is a way to reword this to be a bit more concise.\nYou are right that's redundant. I will reword it.\n>\n>> and the refs are updated\n>> +       immediately. If any ref update fails, the command will exit\n>> +       with a non-zero status.\n> This doesn't say if the command immediately stops when it fails to\n> update a ref, and if the ref updates are atomic or not.\nYou are right the docs need to be clearer about the behavior \ndifferences. I will clarify that --update stops immediately on failure \n(like git update-ref --stdin), while --update-refs defaults to atomic mode.\n>\n>> +--update-refs::\n>> +       Update the relevant refs using ref transactions instead of outputting\n>> +       update-ref commands. By default, uses atomic mode where all ref updates\n>> +       succeed or all fail.\n> This seems to imply that --update doesn't update the refs atomically.\nThat correct --update doesn't use transactions it updates refs one by \none like `git update-ref --stdin` does. Should I make this clearer in \nthe documentation?\n>\n>> Use with `--batch` to allow partial updates.\n> What about --update, when should it be used?\nGood point. My thinking was --update for simple cases where you want the \nexact same behavior as piping to `git update-ref --stdin` and \n--update-refs when you want transaction guarantees. But I am starting to \nthink this distinction might be confusing users more than helping them.\n\nWould it be cleaner to just have --update-refs with the batch mode \noption and drop --update entirely? The sequential behavior can be \nachieved with --update-refs --batch if someone really needs it.\n>\n>> +       When this option is used, no output is produced on successful completion.\n> Here also it seems a bit redundant to say both \"instead of outputting\n> update-ref commands\" and then \"no output is produced on successful\n> completion\". And maybe there is a way to reword this to be a bit more\n> concise.\nYes same redundancy issue. I will fix the wording throughout in v2.\n>\n>> +--batch::\n>> +       Can only be used with `--update-refs`. Enables batch mode for ref\n>> +       updates, allowing some refs to be updated successfully even if others\n>> +       fail. Failed updates are reported as warnings rather than errors.\n> What's the difference with --update? Is it that --update immediately\n> stops when a ref update fails?\nYes exactly. --update mimics the behavior of piping to `git update-ref \n--stdin` and it stops immediately on the first failure and doesn't \nupdate any remaining refs.\n\n--update-refs uses transactions, so in atomic mode all refs are updated \ntogether or none at all, and in batch mode it can continue processing \nremaining refs even after some fail.\n>\n>>   <revision-range>::\n>>          Range of commits to replay. More than one <revision-range> can\n>>          be passed, but in `--advance <branch>` mode, they should have\n>> @@ -54,8 +73,9 @@ include::rev-list-options.adoc[]\n>>   OUTPUT\n>>   ------\n>>\n>> -When there are no conflicts, the output of this command is usable as\n>> -input to `git update-ref --stdin`.  It is of the form:\n>> +When there are no conflicts and neither `--update` nor `--update-refs`\n>> +is used, the output of this command is usable as input to `git update-ref --stdin`.\n>> +It is of the form:\n>>\n>>          update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n>>          update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n>> @@ -66,6 +86,15 @@ the shape of the history being replayed.  When using `--advance`, the\n>>   number of refs updated is always one, but for `--onto`, it can be one\n>>   or more (rebasing multiple branches simultaneously is supported).\n>>\n>> +When `--update` is used, no output is produced and the refs are updated\n>> +directly using individual ref updates. This is equivalent to piping the normal output to\n>> +`git update-ref --stdin`.\n> Is it equivalent to `git update-ref --stdin` because both exit as soon\n> as a ref update fails?\nYes that is the intention. Both --update and `git update-ref --stdin` \nprocess refs sequentially and exit on first failure leaving the \nrepository in a partially updated state if failure occurs partway through.\n\nThe difference is --update does this internally without needing the pipe \nwhile --update-refs uses proper transactions for better atomicity \nguarantees.\n>> +When `--update-refs` is used, no output is produced and the refs are updated\n>> +using ref transactions. In atomic mode (default), all ref updates succeed\n>> +or all fail. In batch mode (with `--batch`), some updates may succeed while\n>> +others fail, with failed updates reported as warnings.\n"},{"id":"525903","messageId":"f4025223-a0ac-416f-b489-d42a07acc0b7@gmail.com","threadId":"64109","inReplyTo":"CAP8UFD2XyqgypPfkQav4Fub0AEwyJjXpvfwMPe-adWyCKRa7fQ@mail.gmail.com","subject":"Re: [PATCH 0/2] replay: add --update-refs option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-09T06:36:45Z","receivedAt":"2025-09-09T06:36:51Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 08/09/25 11:37, Christian Couder wrote:\n> On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> This patch series adds a --update-refs option to git replay. Right now,\n>> when you use git replay, you need to pipe its output to git update-ref\n>> like this:\n>>\n>>      git replay --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> This works fine, but it means running two commands and doesn't give you\n>> atomic transactions by default. The new --update-refs option lets you do\n>> the ref updates directly:\n>>\n>>      git replay --update-refs --onto main topic1..topic2\n> Thanks for working on this.\n>\n>> I discussed this feature with Christian Couder earlier, and we agreed that\n>> it would be useful for server-side operations where you want atomic updates.\n> Yeah, right. This is something the Git team at GitLab has been\n> interested in for some time.\n>\n>> The way it works:\n>> - By default, it uses atomic transactions (all refs get updated or none do)\n>> - There's a --batch option if you want some updates to succeed even if\n>>    others fail\n>> - It works with bare repositories, which is important for server operations\n>>    like Gitaly\n>> - When it succeeds, it doesn't print anything (just like git update-ref\n>>    --stdin)\n>> - You can't use --update-refs with the existing --update option\n> There is no existing --update option. This series also introduces the\n> --update option.\nYou are right that was confusing wording in my cover letter. Both \n--update and --update-refs are new in this series.\n>\n>> This should help with git replay's goal of being good for server-side\n>> operations. It also makes the command simpler to use since you don't need\n>> the pipeline anymore, and the atomic behavior is better for reliability.\n> I have commented only on the documentation patch for now as I think\n> it's better to review the design of the new options first, and the\n> documentation looks like a good place for that.\n>\n> Thanks again.\n"},{"id":"525905","messageId":"c7615356-04cc-47e2-a894-4d24e416e4ad@gmail.com","threadId":"64109","inReplyTo":"aL6n8KEHSDii5Wd1@pks.im","subject":"Re: [PATCH 1/2] replay: add --update-refs option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-09T06:58:29Z","receivedAt":"2025-09-09T06:58:35Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 08/09/25 15:24, Patrick Steinhardt wrote:\n> On Mon, Sep 08, 2025 at 10:06:19AM +0530, Siddharth Asthana wrote:\n>> diff --git a/builtin/replay.c b/builtin/replay.c\n>> index 6172c8aacc..a33c9887cf 100644\n>> --- a/builtin/replay.c\n>> +++ b/builtin/replay.c\n>> @@ -284,6 +284,37 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>>   \treturn create_commit(repo, result->tree, pickme, replayed_base);\n>>   }\n>>   \n>> +static int update_ref_direct(struct repository *repo, const char *refname,\n>> +\t\t\t     const struct object_id *new_oid,\n>> +\t\t\t     const struct object_id *old_oid)\n>> +{\n>> +\tconst char *msg = \"replay\";\n>> +\treturn refs_update_ref(get_main_ref_store(repo), msg, refname,\n>> +\t\t\t       new_oid, old_oid, 0, UPDATE_REFS_MSG_ON_ERR);\n>> +}\n\n\nHi Patrick,\n\nThanks for the detailed review\n\n\n> Is there a strong reason why a user would want to update refs one by\n> one? If not, let's not add new code to our base that does so. This is\n> known to be inperformant for the reftable backend, but also for the\n> files backend in some cases.\n\n\nYou are absolutely right about the performance concern. My thinking was \nto provide a simple mode that exactly mimics \"git replay | git \nupdate-ref --stdin\" behavior, but I see that's not worth the performance \ncost.\n\nI will remove the individual update function and only use batched \ntransactions with REF_TRANSACTION_ALLOW_FAILURE when needed.\n\n\n> If we really want to support the case where\n> only a subset of references gets committed we should be using batched\n> updates with the `REF_TRANSACTION_ALLOW_FAILURE` flag.\n>\n>> @@ -319,6 +355,12 @@ int cmd_replay(int argc,\n>>   \t\t\t   N_(\"replay onto given commit\")),\n>>   \t\tOPT_BOOL(0, \"contained\", &contained,\n>>   \t\t\t N_(\"advance all branches contained in revision-range\")),\n>> +\t\tOPT_BOOL(0, \"update\", &update_directly,\n>> +\t\t\t N_(\"update branches directly instead of outputting update commands\")),\n>> +\t\tOPT_BOOL(0, \"update-refs\", &update_refs_flag,\n>> +\t\t\t N_(\"update branches using ref transactions\")),\n>> +\t\tOPT_BOOL(0, \"batch\", &batch_mode,\n>> +\t\t\t N_(\"allow partial ref updates in batch mode\")),\n>>   \t\tOPT_END()\n>>   \t};\n>>   \n> So I think we should reduce this to only accept two flags:\n> `--update-refs` and a flag that accepts a subset of refs failing.o\n>\n> We might also want to make this something like `--update-refs[=<mode>]`,\n> where `<mode>` could be \"allow-failures\".\n\n\nThat make sense. Would you prefer `--update-refs` with \n`--allow-failures` as a separate flag? I am leaning toward that since \nit's clearer than the parameter syntax.\n\n\n>\n>> @@ -333,6 +375,14 @@ int cmd_replay(int argc,\n>>   \tif (advance_name_opt && contained)\n>>   \t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n>>   \t\t    \"--advance\", \"--contained\");\n>> +\n>> +\tif (update_directly && update_refs_flag)\n>> +\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n>> +\t\t    \"--update\", \"--update-refs\");\n>> +\n>> +\tif (batch_mode && !update_refs_flag)\n>> +\t\tdie(_(\"option '%s' can only be used with '%s'\"),\n>> +\t\t    \"--batch\", \"--update-refs\");\n>>   \tadvance_name = xstrdup_or_null(advance_name_opt);\n>>   \n>>   \trepo_init_revisions(repo, &revs, prefix);\n> We have the `die_for_incompatible_opt*()` helpers for this.\n\n\nThanks, I will use those.\n\n\n>\n>> @@ -389,6 +439,18 @@ int cmd_replay(int argc,\n>>   \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n>>   \t\t\t      &onto, &update_refs);\n>>   \n>> +\t/* Initialize ref transaction if using --update-refs */\n> Nit: the comment doesn't really add much context, so I'd just drop it.\n> It's generally discouraged to add a comment that re-states what the code\n> already says. Instead, comments should point out things that are easy to\n> miss or not obvious at all.\n\n\nWill remove the redundant comments.\n\n\n>\n>> @@ -445,10 +525,43 @@ int cmd_replay(int argc,\n>>   \n>>   \t/* In --advance mode, advance the target ref */\n>>   \tif (result.clean == 1 && advance_name) {\n>> -\t\tprintf(\"update %s %s %s\\n\",\n>> -\t\t       advance_name,\n>> -\t\t       oid_to_hex(&last_commit->object.oid),\n>> -\t\t       oid_to_hex(&onto->object.oid));\n>> +\t\tif (update_directly) {\n>> +\t\t\tif (update_ref_direct(repo, advance_name,\n>> +\t\t\t\t\t     &last_commit->object.oid,\n>> +\t\t\t\t\t     &onto->object.oid) < 0) {\n>> +\t\t\t\tret = -1;\n>> +\t\t\t\tgoto cleanup;\n>> +\t\t\t}\n>> +\t\t} else if (transaction) {\n>> +\t\t\tif (add_ref_to_transaction(transaction, advance_name,\n>> +\t\t\t\t\t\t   &last_commit->object.oid,\n>> +\t\t\t\t\t\t   &onto->object.oid,\n>> +\t\t\t\t\t\t   &transaction_err) < 0) {\n>> +\t\t\t\tret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n>> +\t\t\t\tgoto cleanup;\n>> +\t\t\t}\n>> +\t\t} else {\n>> +\t\t\tprintf(\"update %s %s %s\\n\",\n>> +\t\t\t       advance_name,\n>> +\t\t\t       oid_to_hex(&last_commit->object.oid),\n>> +\t\t\t       oid_to_hex(&onto->object.oid));\n>> +\t\t}\n>> +\t}\n>> +\n>> +\t/* Commit the ref transaction if we have one */\n> Likewise here.\n\n\nWill remove the redundant comments here too.\n\n\nThank,\n\nSiddharth\n\n\n>\n>> +\tif (transaction && result.clean == 1) {\n>> +\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n>> +\t\t\tif (batch_mode) {\n>> +\t\t\t\t/* Print failed updates in batch mode */\n>> +\t\t\t\twarning(_(\"some ref updates failed: %s\"), transaction_err.buf);\n>> +\t\t\t\tref_transaction_for_each_rejected_update(transaction,\n>> +\t\t\t\t\t\t\t\t\t\t print_rejected_update, NULL);\n>> +\t\t\t} else {\n>> +\t\t\t\t/* In atomic mode, all updates failed */\n>> +\t\t\t\tret = error(_(\"failed to update refs: %s\"), transaction_err.buf);\n>> +\t\t\t\tgoto cleanup;\n>> +\t\t\t}\n>> +\t\t}\n>>   \t}\n>>   \n>>   \tmerge_finalize(&merge_opt, &result);\n> Patrick\n"},{"id":"525907","messageId":"24e0eefb-da7e-4ebe-b417-993b9b105160@gmail.com","threadId":"64109","inReplyTo":"e8bca1d7-96d7-42b3-95b7-6a525fd3f67d@app.fastmail.com","subject":"Re: [PATCH 0/2] replay: add --update-refs option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-09T07:04:57Z","receivedAt":"2025-09-09T07:05:04Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 08/09/25 20:03, Kristoffer Haugsbakk wrote:\n> On Mon, Sep 8, 2025, at 06:36, Siddharth Asthana wrote:\n>> This patch series adds a --update-refs option to git replay. Right now,\n>> when you use git replay, you need to pipe its output to git update-ref\n>> like this:\n>> [snip]\n> Both patches introduce whitespace errors.  You can check with\n> `ci/check-whitespace.sh`.\nThanks for catching that. I will fix the whitespace issues and run the \nscript before sending v2.\n>\n> That script will suggest a way to fix it.\n>\n> There’s also a `\\ No newline at end of file` (I don’t think whitespace-\n> check checks that).\n"},{"id":"525908","messageId":"d070bbe1-8142-4811-b8ec-6d705b969381@gmail.com","threadId":"64109","inReplyTo":"ecdd1191-844b-47ca-9737-cc2ffb72b37d@app.fastmail.com","subject":"Re: [PATCH 2/2] replay: document --update-refs and --batch options","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-09T07:06:08Z","receivedAt":"2025-09-09T07:06:14Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 08/09/25 20:10, Kristoffer Haugsbakk wrote:\n> On Mon, Sep 8, 2025, at 06:36, Siddharth Asthana wrote:\n>> [snip]\n>> diff --git a/Documentation/git-replay.adoc\n>> b/Documentation/git-replay.adoc\n>> index 0b12bf8aa4..cc9f868c2f 100644\n>> --- a/Documentation/git-replay.adoc\n>> +++ b/Documentation/git-replay.adoc\n>> @@ -9,16 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new\n>> base, works with bare repos t\n>>   SYNOPSIS\n>>   --------\n>>   [verse]\n>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> |\n>> --advance <branch>) <revision-range>...\n>> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--update | --update-refs [--batch]] <revision-range>...\n\n\nHi Kristoffer,\n\n\n> Another downside of making a separate commit for the documentation is\n> that now `t/t0450-txt-doc-vs-help.sh` will likely fail for your first\n> commit.  One of the tests makes sure that the synopsis and the `.adoc`\n> is in synch.\n\n\nYou are right, I will combine the documentation with the implementation \npatch in v2 to avoid the test failure.\n\nThanks,\nSiddharth\n\n\n>\n>>   DESCRIPTION\n>>   -----------\n>> [snip]\n"},{"id":"525912","messageId":"CABPp-BG6A_mwxQheE5ED5HQj7STVtf1_9NhSmjmzRPB7QkdWyg@mail.gmail.com","threadId":"64109","inReplyTo":"20250908043620.57848-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH 0/2] replay: add --update-refs option","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-09-09T07:13:53Z","receivedAt":"2025-09-09T07:14:05Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sun, Sep 7, 2025 at 9:36 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> This patch series adds a --update-refs option to git replay. Right now,\n> when you use git replay, you need to pipe its output to git update-ref\n> like this:\n>\n>     git replay --onto main topic1..topic2 | git update-ref --stdin\n>\n> This works fine, but it means running two commands and doesn't give you\n> atomic transactions by default. The new --update-refs option lets you do\n> the ref updates directly:\n>\n>     git replay --update-refs --onto main topic1..topic2\n>\n> I discussed this feature with Christian Couder earlier, and we agreed that\n> it would be useful for server-side operations where you want atomic updates.\n\nSeems fair...but why not make --update-refs the default and add an\noption for those that just want the update commands?\n\n> The way it works:\n> - By default, it uses atomic transactions (all refs get updated or none do)\n> - There's a --batch option if you want some updates to succeed even if\n>   others fail\n> - It works with bare repositories, which is important for server operations\n>   like Gitaly\n> - When it succeeds, it doesn't print anything (just like git update-ref\n>   --stdin)\n\nSeems fair.\n\n> This should help with git replay's goal of being good for server-side\n> operations.\n\nI'm slightly confused by this statement; there's multiple ways to\ninterpret it -- various antecedents of \"This\", questions about whether\nyou are saying git replay has one goal or you are just helping with\none of its goals, and leaves to the reader to guess which part is\nhelpful (is it the ergonomics -- why does that matter server-side?  Is\nit the atomicity?  Then why did you also add --batch and --update?  Is\nit something else?)  Perhaps this sentence can be dropped or\ncompletely rewritten?\n\n> It also makes the command simpler to use since you don't need\n> the pipeline anymore, and the atomic behavior is better for reliability.\n\nYeah, makes sense...but why not just make it the default behavior\ninstead of requiring an extra flag?  (The command is marked as\nexperimental...)\n"},{"id":"525913","messageId":"CAP8UFD1J8fgjZ+din3P_=FjZZFJ+ocqvwTFjBNjpnhrx6=nMqg@mail.gmail.com","threadId":"64109","inReplyTo":"7f90e1b6-acba-40f2-9e51-ad09c2bf6999@gmail.com","subject":"Re: [PATCH 2/2] replay: document --update-refs and --batch options","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-09T07:26:33Z","receivedAt":"2025-09-09T07:26:48Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Hi Siddharth,\n\nOn Tue, Sep 9, 2025 at 8:36 AM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> On 08/09/25 11:30, Christian Couder wrote:\n> > On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana\n\n> >> Also document the --batch option which can be used with --update-refs\n> >> to allow partial failures in ref updates.\n\n> > It looks like a --update option was also added by the previous patch.\n> > Is it documented here too?\n> >\n> > Why was this [--update | --update-refs [--batch]] set of options\n> > selected over other possibilities like for example\n> > [--update-iteratively | --update-atomically | --update-batch]?\n\n> I was trying to provide both simple and advanced modes. --update for\n> users who just want \"make it work like piping to git update-ref --stdin\"\n> and --update-refs for those who want control over transaction modes. But\n> I see this creates confusion.\n>\n> Would you prefer a single option like --update-refs with an optional\n> mode parameter? Something like --update-refs[=batch] where default is\n> atomic?\n\nMy preference would be something like [--update-atomically |\n--update-batch] first. (Maybe names like `--batch-update` and\n`--atomic-update` are better?)\n\nAnd then something like --update-iteratively could perhaps be added as\nan alternative, if:\n\n  - it works exactly the same as piping to `git update-ref --stdin`, and\n  - some users want to use it to blindly replace piping to `git\nupdate-ref --stdin`, and\n  - we document that it is not efficient (compared to\nupdate-atomically and --update-batch) and should only be used to\nblindly (bug for bug) replace piping to `git update-ref --stdin` when\nperformance is not an issue.\n\n> > Also how does this --update-refs option compare to the --update-refs\n> > option in git rebase? Is it working in the same way?\n\n> No, they are different. git rebase --update-refs updates refs that point\n> to commits being rebased. --update-refs updates the target branches from\n> the replay operation itself. The naming collision is unfortunate should\n> I use a different name?\n\nYeah, my opinion is that \"rebase\" and \"replay\" are commands doing\nsimilar things, so having an `--update-refs` option in both commands\nis a good thing only if the option has the same purpose in both\ncommands. If the purpose is a bit different, I think it's better to\nuse different names to avoid confusion.\n\n> >> +--update-refs::\n> >> +       Update the relevant refs using ref transactions instead of outputting\n> >> +       update-ref commands. By default, uses atomic mode where all ref updates\n> >> +       succeed or all fail.\n> > This seems to imply that --update doesn't update the refs atomically.\n> That correct --update doesn't use transactions it updates refs one by\n> one like `git update-ref --stdin` does. Should I make this clearer in\n> the documentation?\n\nYes, please.\n\n> >> Use with `--batch` to allow partial updates.\n> > What about --update, when should it be used?\n> Good point. My thinking was --update for simple cases where you want the\n> exact same behavior as piping to `git update-ref --stdin` and\n> --update-refs when you want transaction guarantees. But I am starting to\n> think this distinction might be confusing users more than helping them.\n>\n> Would it be cleaner to just have --update-refs with the batch mode\n> option and drop --update entirely? The sequential behavior can be\n> achieved with --update-refs --batch if someone really needs it.\n\nAbout the options that should be implemented, see my opinion above.\n\nAbout possible confusion, I think that to avoid it, it is important to:\n\n  - name the options properly (see above what I think about the\n`--update-refs` name), and to\n\n  - document thoroughly how all the options differ from each other and\nfrom piping to `git update-ref --stdin`\n\nThanks.\n"},{"id":"525914","messageId":"CABPp-BEmOor3CLAY6y50DuGR1K7WYu+PVsXXWOOaofXzJpavMg@mail.gmail.com","threadId":"64109","inReplyTo":"20250908043620.57848-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH 1/2] replay: add --update-refs option","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-09-09T07:32:42Z","receivedAt":"2025-09-09T07:32:54Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sun, Sep 7, 2025 at 9:36 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n[...]\n> Option validation ensures --update-refs cannot be used with the existing\n> --update option, and --batch can only be used with --update-refs.\n\nThere is no existing --update option.\n\n[...]\n> +       int update_directly = 0;\n> +       int update_refs_flag = 0;\n> +       int batch_mode = 0;\n\nWhy are we adding three kinds of updates?  You covered two in the\ncommit message, but mostly only motivated one, and then added three?\n\n> +               OPT_BOOL(0, \"update\", &update_directly,\n> +                        N_(\"update branches directly instead of outputting update commands\")),\n> +               OPT_BOOL(0, \"update-refs\", &update_refs_flag,\n> +                        N_(\"update branches using ref transactions\")),\n> +               OPT_BOOL(0, \"batch\", &batch_mode,\n> +                        N_(\"allow partial ref updates in batch mode\")),\n\nThree modes and I can't figure out how update_directly differs from\nthe others from the description.  Is it different?\n\nAlso, --batch seems like a funny name since update-refs is also\nupdating refs in a batch.  I'd suggest coming up with a new name...but\nis there clamor for it?  You mostly motivated the atomic updates, and\nI think it might be better to just implement those and then add more\nflags later if needed.\n\n> @@ -399,6 +461,7 @@ int cmd_replay(int argc,\n>\n>         init_basic_merge_options(&merge_opt, repo);\n>         memset(&result, 0, sizeof(result));\n> +       result.clean = 1;  /* Assume clean until proven otherwise */\n\nI don't understand why this change is needed or helpful.  I don't\nthink it changes behavior looking at the existing code, but to me,\nresult is supposed to be the result of a merge operation, not an\ninput, and should not be set other than being cleared initially by the\ncaller.  The comment feels slightly misleading to me, as well.  So,\nI'm surprised by this change and would like to hear the motivation\nbehind it; could you clarify?  Did I miss something about how you\ndepend on this being set even if the list of commits to replay is\nempty or something?\n\n> -                               printf(\"update %s %s %s\\n\",\n> -                                      decoration->name,\n> -                                      oid_to_hex(&last_commit->object.oid),\n> -                                      oid_to_hex(&commit->object.oid));\n> +                               if (update_directly) {\n> +                                       if (update_ref_direct(repo, decoration->name,\n> +                                                            &last_commit->object.oid,\n> +                                                            &commit->object.oid) < 0) {\n> +                                               ret = -1;\n> +                                               goto cleanup;\n> +                                       }\n> +                               } else if (transaction) {\n> +                                       if (add_ref_to_transaction(transaction, decoration->name,\n> +                                                                  &last_commit->object.oid,\n> +                                                                  &commit->object.oid,\n> +                                                                  &transaction_err) < 0) {\n> +                                               ret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n> +                                               goto cleanup;\n> +                                       }\n> +                               } else {\n> +                                       printf(\"update %s %s %s\\n\",\n> +                                              decoration->name,\n> +                                              oid_to_hex(&last_commit->object.oid),\n> +                                              oid_to_hex(&commit->object.oid));\n> +                               }\n\nWho would want the update_ref_direct() branch of code here?  Can we\njust toss it?\n"},{"id":"525919","messageId":"CAP8UFD3GU5Xwq7WMihmHtpWc-GjB-guTU6JHG7BdkhxukMihNQ@mail.gmail.com","threadId":"64109","inReplyTo":"CABPp-BG6A_mwxQheE5ED5HQj7STVtf1_9NhSmjmzRPB7QkdWyg@mail.gmail.com","subject":"Re: [PATCH 0/2] replay: add --update-refs option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-09T07:47:24Z","receivedAt":"2025-09-09T07:47:38Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Sep 9, 2025 at 9:14 AM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Sun, Sep 7, 2025 at 9:36 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n\n> Seems fair...but why not make --update-refs the default and add an\n> option for those that just want the update commands?\n\nIf this patch series had been sent a few months after `git replay` was\nintroduced, I would have been fine with this series making `git\nreplay` update the refs by default while adding an option that only\noutputs the commands. Unfortunately `git replay` seems to have been\nintroduced in v2.44.0 (Feb 22, 2024), so more than 18 months ago. So\neven if it is marked as experimental, it's perhaps a bit late to make\nsuch a relatively big change in it?\n\n> > The way it works:\n> > - By default, it uses atomic transactions (all refs get updated or none do)\n> > - There's a --batch option if you want some updates to succeed even if\n> >   others fail\n> > - It works with bare repositories, which is important for server operations\n> >   like Gitaly\n> > - When it succeeds, it doesn't print anything (just like git update-ref\n> >   --stdin)\n>\n> Seems fair.\n>\n> > This should help with git replay's goal of being good for server-side\n> > operations.\n>\n> I'm slightly confused by this statement; there's multiple ways to\n> interpret it -- various antecedents of \"This\", questions about whether\n> you are saying git replay has one goal or you are just helping with\n> one of its goals, and leaves to the reader to guess which part is\n> helpful (is it the ergonomics -- why does that matter server-side?  Is\n> it the atomicity?  Then why did you also add --batch and --update?  Is\n> it something else?)  Perhaps this sentence can be dropped or\n> completely rewritten?\n\nThe way I understood this sentence is that `git replay` is already\nuseful on the server side (because it performs all the operations in\nmemory and doesn't need a work tree), and the new feature added by the\npatch series reinforces this because atomic operations are often\nbetter on the server side.\n\nThanks.\n"},{"id":"525936","messageId":"aL_svO5Ils8r9DkT@pks.im","threadId":"64109","inReplyTo":"c7615356-04cc-47e2-a894-4d24e416e4ad@gmail.com","subject":"Re: [PATCH 1/2] replay: add --update-refs option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-09T09:00:44Z","receivedAt":"2025-09-09T09:00:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Sep 09, 2025 at 12:28:29PM +0530, Siddharth Asthana wrote:\n> On 08/09/25 15:24, Patrick Steinhardt wrote:\n> > Is there a strong reason why a user would want to update refs one by\n> > one? If not, let's not add new code to our base that does so. This is\n> > known to be inperformant for the reftable backend, but also for the\n> > files backend in some cases.\n> \n> You are absolutely right about the performance concern. My thinking was to\n> provide a simple mode that exactly mimics \"git replay | git update-ref\n> --stdin\" behavior, but I see that's not worth the performance cost.\n> \n> I will remove the individual update function and only use batched\n> transactions with REF_TRANSACTION_ALLOW_FAILURE when needed.\n\nWe can still extend the functionality at a later point if we discover\nany use cases for those.\n\n> > > @@ -319,6 +355,12 @@ int cmd_replay(int argc,\n> > >   \t\t\t   N_(\"replay onto given commit\")),\n> > >   \t\tOPT_BOOL(0, \"contained\", &contained,\n> > >   \t\t\t N_(\"advance all branches contained in revision-range\")),\n> > > +\t\tOPT_BOOL(0, \"update\", &update_directly,\n> > > +\t\t\t N_(\"update branches directly instead of outputting update commands\")),\n> > > +\t\tOPT_BOOL(0, \"update-refs\", &update_refs_flag,\n> > > +\t\t\t N_(\"update branches using ref transactions\")),\n> > > +\t\tOPT_BOOL(0, \"batch\", &batch_mode,\n> > > +\t\t\t N_(\"allow partial ref updates in batch mode\")),\n> > >   \t\tOPT_END()\n> > >   \t};\n> > So I think we should reduce this to only accept two flags:\n> > `--update-refs` and a flag that accepts a subset of refs failing.o\n> > \n> > We might also want to make this something like `--update-refs[=<mode>]`,\n> > where `<mode>` could be \"allow-failures\".\n> \n> \n> That make sense. Would you prefer `--update-refs` with `--allow-failures` as\n> a separate flag? I am leaning toward that since it's clearer than the\n> parameter syntax.\n\nI'd personally prefer `--update-refs[=<mode>]`. The reason is mostly\nthat it makes it easier to discover what flags are related to the\n`--update-refs` infra and you have to worry less about catching any kind\nof incompatible flag combinations.\n\nPatrick\n"},{"id":"525939","messageId":"CABPp-BHWjyRv_f_HKkz10Q_cOZKPvpgf=SEUR1ThmbttkQT+Uw@mail.gmail.com","threadId":"64109","inReplyTo":"CAP8UFD3GU5Xwq7WMihmHtpWc-GjB-guTU6JHG7BdkhxukMihNQ@mail.gmail.com","subject":"Re: [PATCH 0/2] replay: add --update-refs option","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-09-09T09:19:03Z","receivedAt":"2025-09-09T09:19:16Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Sep 9, 2025 at 12:47 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> On Tue, Sep 9, 2025 at 9:14 AM Elijah Newren <newren@gmail.com> wrote:\n> >\n> > On Sun, Sep 7, 2025 at 9:36 PM Siddharth Asthana\n> > <siddharthasthana31@gmail.com> wrote:\n>\n> > Seems fair...but why not make --update-refs the default and add an\n> > option for those that just want the update commands?\n>\n> If this patch series had been sent a few months after `git replay` was\n> introduced, I would have been fine with this series making `git\n> replay` update the refs by default while adding an option that only\n> outputs the commands. Unfortunately `git replay` seems to have been\n> introduced in v2.44.0 (Feb 22, 2024), so more than 18 months ago. So\n> even if it is marked as experimental, it's perhaps a bit late to make\n> such a relatively big change in it?\n\nI don't think so; we marked it as experimental much more prominently\nthan other commands -- in the .c file, and three separate places in\nthe documentation.  All other commands appear to have only been marked\nas experimental in one place and never the C file, so this one is four\ntimes more experimental than any other command.  Plus, the worry about\nit being set in stone and the need to make it malleable was *exactly*\nwhy the requests were made to be so much more clear that this command\nneeded the flexibility to change\n(https://lore.kernel.org/git/CABPp-BFrVfGHOrBk7g=4TkGxDv=oSqF1FOkhp6WVbxUV-2yveQ@mail.gmail.com/).\nPlus, it's currently only used server-side, so it'd probably only mean\nGitLab (you), GitHub (me), and a few other users would need to update,\nall of whom should be aware of the warnings.\n\nWe could add a config setting to allow defaulting to --no-update-refs\nor whatever we want to call it.\n\n> > > The way it works:\n> > > - By default, it uses atomic transactions (all refs get updated or none do)\n> > > - There's a --batch option if you want some updates to succeed even if\n> > >   others fail\n> > > - It works with bare repositories, which is important for server operations\n> > >   like Gitaly\n> > > - When it succeeds, it doesn't print anything (just like git update-ref\n> > >   --stdin)\n> >\n> > Seems fair.\n> >\n> > > This should help with git replay's goal of being good for server-side\n> > > operations.\n> >\n> > I'm slightly confused by this statement; there's multiple ways to\n> > interpret it -- various antecedents of \"This\", questions about whether\n> > you are saying git replay has one goal or you are just helping with\n> > one of its goals, and leaves to the reader to guess which part is\n> > helpful (is it the ergonomics -- why does that matter server-side?  Is\n> > it the atomicity?  Then why did you also add --batch and --update?  Is\n> > it something else?)  Perhaps this sentence can be dropped or\n> > completely rewritten?\n>\n> The way I understood this sentence is that `git replay` is already\n> useful on the server side (because it performs all the operations in\n> memory and doesn't need a work tree), and the new feature added by the\n> patch series reinforces this because atomic operations are often\n> better on the server side.\n\nYou often word things well; I like that sentence.  The original makes\nme guess and wonder whether something like that is the intent; can we\nreplace the original sentence with your description?\n"},{"id":"525968","messageId":"xmqq5xdrvand.fsf@gitster.g","threadId":"64109","inReplyTo":"CABPp-BHWjyRv_f_HKkz10Q_cOZKPvpgf=SEUR1ThmbttkQT+Uw@mail.gmail.com","subject":"Re: [PATCH 0/2] replay: add --update-refs option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-09-09T16:44:06Z","receivedAt":"2025-09-09T16:44:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> > Seems fair...but why not make --update-refs the default and add an\n>> > option for those that just want the update commands?\n>>\n>> If this patch series had been sent a few months after `git replay` was\n>> introduced, I would have been fine with this series making `git\n>> replay` update the refs by default while adding an option that only\n>> outputs the commands. Unfortunately `git replay` seems to have been\n>> introduced in v2.44.0 (Feb 22, 2024), so more than 18 months ago. So\n>> even if it is marked as experimental, it's perhaps a bit late to make\n>> such a relatively big change in it?\n>\n> I don't think so; we marked it as experimental much more prominently\n> than other commands -- in the .c file, and three separate places in\n> the documentation.\n\nWhen we are talking about a change that breaks an established\nend-user expectation, it does not matter much if we wrote anything\nin the .c source files.  The end-user facing documentation does.\n\nAnd as you said, \"git replay -h\" and \"git replay --help\" prominently\nshow that the experimental nature of the command.\n\nIf this new behaviour is a clear improvement for majority of use\ncases, I am perfectly fine with changing the default behaviour so\nthat everybody will benefit.  It may still be good to add an option\nto allow the users to ask for the traditional \"we'll give you a list\nof updates you can apply as you see fit, but would not update the\nrefs ourselves\" mode, though.\n"},{"id":"525980","messageId":"0683661d-3e70-40e7-9f14-c1702d17fb80@gmail.com","threadId":"64109","inReplyTo":"20250908043620.57848-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH 2/2] replay: document --update-refs and --batch options","fromName":"Andrei Rybak","fromEmail":"rybak.a.v@gmail.com","sentAt":"2025-09-09T19:20:17Z","receivedAt":"2025-09-09T19:20:22Z","isPatch":true,"sender":{"key":"rybak.a.v@gmail.com","avatar":"https://avatars.githubusercontent.com/u/624072?v=4"},"body":"hello, Siddharth Asthana\n\nOn 08/09/2025 06:36, Siddharth Asthana wrote:\n> @@ -91,6 +120,27 @@ $ git replay --advance target origin/main..mybranch\n>   update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n>   ------------\n>   \n> +To rebase `mybranch` onto `target` and update the ref directly:\n> +\n> +------------\n> +$ git replay --update --onto target origin/main..mybranch\n> +# No output; mybranch is updated directly\n> +------------\n> +\n> +To rebase `mybranch` onto `target` using atomic ref transactions:\n> +\n> +------------\n> +$ git replay --update-refs --onto target origin/main..mybranch\n> +# No output; mybranch is updated atomically\n> +------------\n> +\n> +To rebase multiple branches with partial failure tolerance:\n> +\n> +------------\n> +$ git replay --update-refs --batch --contained --onto origin/main origin/main..tipbranch\n> +# No output; refs updated in batch mode, warnings for any failures\n> +------------\n> +\n>   Note that the first two examples replay the exact same commits and on\n>   top of the exact same new base, they only differ in that the first\n>   provides instructions to make mybranch point at the new commits and\n\nAdding new examples above this paragraph separates it from the existing \nexamples it refers to.\n"},{"id":"525991","messageId":"CABPp-BEyVSrEkPwsc31g69SZEXNffa64HPNeG-FU+hhQMz_y=A@mail.gmail.com","threadId":"64109","inReplyTo":"xmqq5xdrvand.fsf@gitster.g","subject":"Re: [PATCH 0/2] replay: add --update-refs option","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-09-09T19:52:17Z","receivedAt":"2025-09-09T19:52:31Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Sep 9, 2025 at 9:44 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> >> > Seems fair...but why not make --update-refs the default and add an\n> >> > option for those that just want the update commands?\n> >>\n> >> If this patch series had been sent a few months after `git replay` was\n> >> introduced, I would have been fine with this series making `git\n> >> replay` update the refs by default while adding an option that only\n> >> outputs the commands. Unfortunately `git replay` seems to have been\n> >> introduced in v2.44.0 (Feb 22, 2024), so more than 18 months ago. So\n> >> even if it is marked as experimental, it's perhaps a bit late to make\n> >> such a relatively big change in it?\n> >\n> > I don't think so; we marked it as experimental much more prominently\n> > than other commands -- in the .c file, and three separate places in\n> > the documentation.\n>\n> When we are talking about a change that breaks an established\n> end-user expectation, it does not matter much if we wrote anything\n> in the .c source files.  The end-user facing documentation does.\n>\n> And as you said, \"git replay -h\" and \"git replay --help\" prominently\n> show that the experimental nature of the command.\n\nI should have clarified -- the .c change was specifically about making\n\"git replay -h\" show the experimental nature of the command; if it was\njust a code comment, I'd agree that it didn't matter, but it was\nspecifically about making the experimental status known to end users\nin the short usage message:\n\n$ git grep -2 EXPERIMENTAL '*.c'\nbuiltin/replay.c-\nbuiltin/replay.c-       const char * const replay_usage[] = {\nbuiltin/replay.c:               N_(\"(EXPERIMENTAL!) git replay \"\nbuiltin/replay.c-                  \"([--contained] --onto <newbase> |\n--advance <branch>) \"\nbuiltin/replay.c-                  \"<revision-range>...\"),\n$\n\n> If this new behaviour is a clear improvement for majority of use\n> cases, I am perfectly fine with changing the default behaviour so\n> that everybody will benefit.  It may still be good to add an option\n> to allow the users to ask for the traditional \"we'll give you a list\n> of updates you can apply as you see fit, but would not update the\n> refs ourselves\" mode, though.\n\nYep.\n"},{"id":"526062","messageId":"aa4d5d0d-3c13-4381-8194-2b0c178b3cca@gmail.com","threadId":"64109","inReplyTo":"CABPp-BEmOor3CLAY6y50DuGR1K7WYu+PVsXXWOOaofXzJpavMg@mail.gmail.com","subject":"Re: [PATCH 1/2] replay: add --update-refs option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-10T17:58:16Z","receivedAt":"2025-09-10T17:58:23Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 09/09/25 13:02, Elijah Newren wrote:\n> On Sun, Sep 7, 2025 at 9:36 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n> [...]\n\n\nHi Elijah,\n\n\n>> Option validation ensures --update-refs cannot be used with the existing\n>> --update option, and --batch can only be used with --update-refs.\n> There is no existing --update option.\n\n\nYou're right, poor wording in my commit message. Both options are new in \nthis series.\n\n\n>\n> [...]\n>> +       int update_directly = 0;\n>> +       int update_refs_flag = 0;\n>> +       int batch_mode = 0;\n> Why are we adding three kinds of updates?  You covered two in the\n> commit message, but mostly only motivated one, and then added three?\n\n\nThat's fair criticism. I was trying to cover all bases but ended up with \nconfusing options. The three were:\n- --update: individual ref updates (like piping to update-ref --stdin)\n- --update-refs: atomic transactions\n- --batch: allow partial failures with --update-refs\n\nBut as Patrick pointed out, individual updates are inefficient and \neveryone seems to prefer simpler options.\n\n\n>\n>> +               OPT_BOOL(0, \"update\", &update_directly,\n>> +                        N_(\"update branches directly instead of outputting update commands\")),\n>> +               OPT_BOOL(0, \"update-refs\", &update_refs_flag,\n>> +                        N_(\"update branches using ref transactions\")),\n>> +               OPT_BOOL(0, \"batch\", &batch_mode,\n>> +                        N_(\"allow partial ref updates in batch mode\")),\n> Three modes and I can't figure out how update_directly differs from\n> the others from the description.  Is it different?\n\n\nYes, update_directly calls refs_update_ref() for each ref individually, \nwhile update_refs_flag uses ref transactions. But Patrick's performance \nconcerns make me think we should drop the individual approach entirely.\n\n\n>\n> Also, --batch seems like a funny name since update-refs is also\n> updating refs in a batch.  I'd suggest coming up with a new name...but\n> is there clamor for it?  You mostly motivated the atomic updates, and\n> I think it might be better to just implement those and then add more\n> flags later if needed.\n>\n>> @@ -399,6 +461,7 @@ int cmd_replay(int argc,\n>>\n>>          init_basic_merge_options(&merge_opt, repo);\n>>          memset(&result, 0, sizeof(result));\n>> +       result.clean = 1;  /* Assume clean until proven otherwise */\n> I don't understand why this change is needed or helpful.  I don't\n> think it changes behavior looking at the existing code, but to me,\n> result is supposed to be the result of a merge operation, not an\n> input, and should not be set other than being cleared initially by the\n> caller.  The comment feels slightly misleading to me, as well.  So,\n> I'm surprised by this change and would like to hear the motivation\n> behind it; could you clarify?  Did I miss something about how you\n> depend on this being set even if the list of commits to replay is\n> empty or something?\n\n\nYou caught an error in my logic. I was trying to handle the case where \nno commits are replayed (empty range), but you are right - result should \nonly be set by merge operations. The existing code already handles empty \nranges correctly by never entering the replay loop. I will remove this \nline in v2.\n\n\n>\n>> -                               printf(\"update %s %s %s\\n\",\n>> -                                      decoration->name,\n>> -                                      oid_to_hex(&last_commit->object.oid),\n>> -                                      oid_to_hex(&commit->object.oid));\n>> +                               if (update_directly) {\n>> +                                       if (update_ref_direct(repo, decoration->name,\n>> +                                                            &last_commit->object.oid,\n>> +                                                            &commit->object.oid) < 0) {\n>> +                                               ret = -1;\n>> +                                               goto cleanup;\n>> +                                       }\n>> +                               } else if (transaction) {\n>> +                                       if (add_ref_to_transaction(transaction, decoration->name,\n>> +                                                                  &last_commit->object.oid,\n>> +                                                                  &commit->object.oid,\n>> +                                                                  &transaction_err) < 0) {\n>> +                                               ret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n>> +                                               goto cleanup;\n>> +                                       }\n>> +                               } else {\n>> +                                       printf(\"update %s %s %s\\n\",\n>> +                                              decoration->name,\n>> +                                              oid_to_hex(&last_commit->object.oid),\n>> +                                              oid_to_hex(&commit->object.oid));\n>> +                               }\n> Who would want the update_ref_direct() branch of code here?  Can we\n> just toss it?\n\n\nGiven the performance concerns and the confusion it is causing, yes. \nLet's toss it and focus on the transaction-based approach.\n\nBased on all the feedback, I am thinking of simplifying to:\n- Default: update refs atomically using transactions\n- --output-commands: print update commands (for the traditional pipeline \nworkflow)\n- --allow-partial: allow some ref updates to succeed while others fail\n\nThis addresses your point about making the better behavior default while \nstill supporting existing workflows.\n\nThanks,\nSiddharth\n\n\n"},{"id":"526081","messageId":"58064c0d-1139-4c57-ae34-756e52bf5695@gmail.com","threadId":"64109","inReplyTo":"CAP8UFD1J8fgjZ+din3P_=FjZZFJ+ocqvwTFjBNjpnhrx6=nMqg@mail.gmail.com","subject":"Re: [PATCH 2/2] replay: document --update-refs and --batch options","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-10T20:26:06Z","receivedAt":"2025-09-10T20:26:13Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 09/09/25 12:56, Christian Couder wrote:\n> Hi Siddharth,\n>\n> On Tue, Sep 9, 2025 at 8:36 AM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> On 08/09/25 11:30, Christian Couder wrote:\n>>> On Mon, Sep 8, 2025 at 6:36 AM Siddharth Asthana\n>>>> Also document the --batch option which can be used with --update-refs\n>>>> to allow partial failures in ref updates.\n>>> It looks like a --update option was also added by the previous patch.\n>>> Is it documented here too?\n>>>\n>>> Why was this [--update | --update-refs [--batch]] set of options\n>>> selected over other possibilities like for example\n>>> [--update-iteratively | --update-atomically | --update-batch]?\n>> I was trying to provide both simple and advanced modes. --update for\n>> users who just want \"make it work like piping to git update-ref --stdin\"\n>> and --update-refs for those who want control over transaction modes. But\n>> I see this creates confusion.\n>>\n>> Would you prefer a single option like --update-refs with an optional\n>> mode parameter? Something like --update-refs[=batch] where default is\n>> atomic?\n> My preference would be something like [--update-atomically |\n> --update-batch] first. (Maybe names like `--batch-update` and\n> `--atomic-update` are better?)\n>\n> And then something like --update-iteratively could perhaps be added as\n> an alternative, if:\n>\n>    - it works exactly the same as piping to `git update-ref --stdin`, and\n>    - some users want to use it to blindly replace piping to `git\n> update-ref --stdin`, and\n>    - we document that it is not efficient (compared to\n> update-atomically and --update-batch) and should only be used to\n> blindly (bug for bug) replace piping to `git update-ref --stdin` when\n> performance is not an issue.\n>\n>>> Also how does this --update-refs option compare to the --update-refs\n>>> option in git rebase? Is it working in the same way?\n>> No, they are different. git rebase --update-refs updates refs that point\n>> to commits being rebased. --update-refs updates the target branches from\n>> the replay operation itself. The naming collision is unfortunate should\n>> I use a different name?\n\n\nHi Christian,\n\n\n> Yeah, my opinion is that \"rebase\" and \"replay\" are commands doing\n> similar things, so having an `--update-refs` option in both commands\n> is a good thing only if the option has the same purpose in both\n> commands. If the purpose is a bit different, I think it's better to\n> use different names to avoid confusion.\n\n\nYou make an excellent point about the naming collision. The purposes are \nindeed different:\n- `git rebase --update-refs` updates refs that point to commits being \nrebased\n- `git replay --update-refs` (in my patch) updates the target branches \nfrom the replay operation\n\nSince Elijah and Junio have endorsed making ref updates the default \nbehavior, this actually simplifies our naming significantly. The new \ndesign would be:\n- Default: atomic ref updates using transactions (no flag needed)\n- `--output-commands`: print update commands for traditional pipeline users\n- `--allow-partial`: enable partial failure tolerance when some refs \ncan't be updated\n\nThis completely avoids the rebase naming collision while providing the \natomic transaction behavior that's important for server-side operations \nlike Gitaly. The default behavior gives us the reliability we need \nwithout any naming confusion.\n\n>\n>>>> +--update-refs::\n>>>> +       Update the relevant refs using ref transactions instead of outputting\n>>>> +       update-ref commands. By default, uses atomic mode where all ref updates\n>>>> +       succeed or all fail.\n>>> This seems to imply that --update doesn't update the refs atomically.\n>> That correct --update doesn't use transactions it updates refs one by\n>> one like `git update-ref --stdin` does. Should I make this clearer in\n>> the documentation?\n> Yes, please.\n>\n>>>> Use with `--batch` to allow partial updates.\n>>> What about --update, when should it be used?\n>> Good point. My thinking was --update for simple cases where you want the\n>> exact same behavior as piping to `git update-ref --stdin` and\n>> --update-refs when you want transaction guarantees. But I am starting to\n>> think this distinction might be confusing users more than helping them.\n>>\n>> Would it be cleaner to just have --update-refs with the batch mode\n>> option and drop --update entirely? The sequential behavior can be\n>> achieved with --update-refs --batch if someone really needs it.\n> About the options that should be implemented, see my opinion above.\n>\n> About possible confusion, I think that to avoid it, it is important to:\n>\n>    - name the options properly (see above what I think about the\n> `--update-refs` name), and to\n>\n>    - document thoroughly how all the options differ from each other and\n> from piping to `git update-ref --stdin`\n>\n> Thanks.\n\n\nAbsolutely agree. The simplified approach with default atomic behavior \neliminates most of the confusion points you identified. I will ensure \nthe documentation clearly explains when users would want \n`--output-commands` (for custom scripting) versus  the default atomic \nbehavior (for reliable operations).\n\nThe atomic-by-default approach also means better performance since we're \nusing batched transactions (addressing Patrick's reftable concerns) and \nbetter UX since users get reliable behavior without needing to \nunderstand transaction modes.\n\nThanks for catching the naming issue christian - it led to a much \ncleaner design,\nSiddharth\n\n\n"},{"id":"526082","messageId":"1c18057f-44fb-4442-831c-9d9f53a4ec0c@gmail.com","threadId":"64109","inReplyTo":"0683661d-3e70-40e7-9f14-c1702d17fb80@gmail.com","subject":"Re: [PATCH 2/2] replay: document --update-refs and --batch options","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-10T20:28:25Z","receivedAt":"2025-09-10T20:28:32Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 10/09/25 00:50, Andrei Rybak wrote:\n> hello, Siddharth Asthana\n>\n> On 08/09/2025 06:36, Siddharth Asthana wrote:\n>> @@ -91,6 +120,27 @@ $ git replay --advance target origin/main..mybranch\n>>   update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n>>   ------------\n>>   +To rebase `mybranch` onto `target` and update the ref directly:\n>> +\n>> +------------\n>> +$ git replay --update --onto target origin/main..mybranch\n>> +# No output; mybranch is updated directly\n>> +------------\n>> +\n>> +To rebase `mybranch` onto `target` using atomic ref transactions:\n>> +\n>> +------------\n>> +$ git replay --update-refs --onto target origin/main..mybranch\n>> +# No output; mybranch is updated atomically\n>> +------------\n>> +\n>> +To rebase multiple branches with partial failure tolerance:\n>> +\n>> +------------\n>> +$ git replay --update-refs --batch --contained --onto origin/main \n>> origin/main..tipbranch\n>> +# No output; refs updated in batch mode, warnings for any failures\n>> +------------\n>> +\n>>   Note that the first two examples replay the exact same commits and on\n>>   top of the exact same new base, they only differ in that the first\n>>   provides instructions to make mybranch point at the new commits and\n>\n\nHi Andrei,\n\n\n> Adding new examples above this paragraph separates it from the \n> existing examples it refers to.\n\n\nGood catch. I will restructure the examples section in v2 to maintain \nthe logical flow, ensuring the explanatory paragraph stays connected to \nthe examples it references.\n\nSince I am moving to a default-behavior approach (atomic ref updates by \ndefault), the examples will be much simpler anyway:\n\n     # Default behavior (atomic updates)\n     git replay --onto target origin/main..mybranch\n\n     # Traditional pipeline output\n     git replay --output-commands --onto target origin/main..mybranch | \ngit update-ref --stdin\n\nThis should make the documentation flow more naturally.\n\nThanks for the review,\nSiddharth\n\n"},{"id":"527465","messageId":"20250926230838.35870-1-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20250908043620.57848-1-siddharthasthana31@gmail.com","subject":"[PATCH v2 0/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-26T23:08:37Z","receivedAt":"2025-09-26T23:08:51Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"This is v2 of the git-replay atomic updates series.\n\nBased on the extensive community feedback from v1, I've completely redesigned\nthe approach. Instead of adding new --update-refs options, this version makes\natomic ref updates the default behavior of git replay.\n\nWhy this change makes sense:\n- git replay is explicitly marked as EXPERIMENTAL with behavior changes expected\n- The command is primarily used server-side where atomic transactions are crucial\n- Current pipeline approach (git replay | git update-ref --stdin) creates \n  coordination complexity and lacks atomic guarantees by default\n- Patrick Steinhardt noted performance issues with individual ref updates \n  in reftable backend\n- Elijah Newren and Junio Hamano endorsed making the better behavior default\n\nThe new design:\n    # Default: atomic ref updates (no pipeline needed)\n    git replay --onto main topic1..topic2\n\n    # Traditional behavior preserved for compatibility  \n    git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n\nKey changes since v1:\n- Made atomic ref updates the default instead of opt-in via --update-refs\n- Eliminated confusing --update vs --update-refs option distinction  \n- Avoided naming collision with git rebase --update-refs\n- Fixed --allow-partial exit code behavior (exits 0 only if ALL updates succeed)\n- Used die_for_incompatible_opt2() for consistent error reporting\n- Updated documentation with proper line wrapping and consistent terminology\n- Added comprehensive testing and performance considerations\n\nThis approach gives us atomic transactions by default while preserving full\nbackward compatibility for existing workflows that need the pipeline approach.\n\nThanks to Christian Couder, Patrick Steinhardt, Elijah Newren, Junio C Hamano,\nKristoffer Haugsbakk, and Andrei Rybak for the excellent feedback that led to \nthis much cleaner design!\n\nSiddharth Asthana (1):\n  replay: make atomic ref updates the default behavior\n\n Documentation/git-replay.adoc |  76 +++++++++++++---\n builtin/replay.c              | 114 ++++++++++++++++++++---\n t/t3650-replay-basics.sh      | 166 ++++++++++++++++++++++++++++++++--\n 3 files changed, 319 insertions(+), 37 deletions(-)\n\n-- \n2.51.0\n\n"},{"id":"527466","messageId":"20250926230838.35870-2-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20250926230838.35870-1-siddharthasthana31@gmail.com","subject":"[PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-09-26T23:08:38Z","receivedAt":"2025-09-26T23:09:00Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"The git replay command currently outputs update commands that must be\npiped to git update-ref --stdin to actually update references:\n\n    git replay --onto main topic1..topic2 | git update-ref --stdin\n\nThis design has significant limitations for server-side operations. The\ntwo-command pipeline creates coordination complexity, provides no atomic\ntransaction guarantees by default, and complicates automation in bare\nrepository environments where git replay is primarily used.\n\nDuring extensive mailing list discussion, multiple maintainers identified\nthat the current approach forces users to opt-in to atomic behavior rather\nthan defaulting to the safer, more reliable option. Elijah Newren noted\nthat the experimental status explicitly allows such behavior changes, while\nPatrick Steinhardt highlighted performance concerns with individual ref\nupdates in the reftable backend.\n\nThe core issue is that git replay was designed around command output rather\nthan direct action. This made sense for a plumbing tool, but creates barriers\nfor the primary use case: server-side operations that need reliable, atomic\nref updates without pipeline complexity.\n\nThis patch changes the default behavior to update refs directly using Git's\nref transaction API:\n\n    git replay --onto main topic1..topic2\n    # No output; all refs updated atomically or none\n\nThe implementation uses ref_store_transaction_begin() with atomic mode by\ndefault, ensuring all ref updates succeed or all fail as a single operation.\nThis leverages git replay's existing server-side strengths (in-memory operation,\nno work tree requirement) while adding the atomic guarantees that server\noperations require.\n\nFor users needing the traditional pipeline workflow, --output-commands\npreserves the original behavior:\n\n    git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n\nThe --allow-partial option enables partial failure tolerance. However, following\nmaintainer feedback, it implements a \"strict success\" model: the command exits\nwith code 0 only if ALL ref updates succeed, and exits with code 1 if ANY\nupdates fail. This ensures that --allow-partial changes error reporting style\n(warnings vs hard errors) but not success criteria, handling edge cases like\n\"no updates needed\" cleanly.\n\nImplementation details:\n- Empty commit ranges now return success (exit code 0) rather than failure,\n  as no commits to replay is a valid successful operation\n- Added comprehensive test coverage with 12 new tests covering atomic behavior,\n  option validation, bare repository support, and edge cases\n- Fixed test isolation issues to prevent branch state contamination between tests\n- Maintains C89 compliance and follows Git's established coding conventions\n- Refactored option validation to use die_for_incompatible_opt2() for both\n  --advance/--contained and --allow-partial/--output-commands conflicts,\n  providing consistent error reporting\n- Fixed --allow-partial exit code behavior to implement \"strict success\" model\n  where any ref update failures result in exit code 1, even with partial tolerance\n- Updated documentation with proper line wrapping, consistent terminology using\n  \"old default behavior\", performance context, and reorganized examples for clarity\n- Eliminates individual ref updates (refs_update_ref calls) that perform\n  poorly with reftable backend\n- Uses only batched ref transactions for optimal performance across all\n  ref backends\n- Avoids naming collision with git rebase --update-refs by using distinct\n  option names\n- Defaults to atomic behavior while preserving pipeline compatibility\n\nThe result is a command that works better for its primary use case (server-side\noperations) while maintaining full backward compatibility for existing workflows.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc |  76 +++++++++++++---\n builtin/replay.c              | 114 ++++++++++++++++++++---\n t/t3650-replay-basics.sh      | 166 ++++++++++++++++++++++++++++++++--\n 3 files changed, 319 insertions(+), 37 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 0b12bf8aa4..e104e0bc03 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,16 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--output-commands | --allow-partial] <revision-range>...\n \n DESCRIPTION\n -----------\n \n Takes ranges of commits and replays them onto a new location. Leaves\n-the working tree and the index untouched, and updates no references.\n-The output of this command is meant to be used as input to\n-`git update-ref --stdin`, which would update the relevant branches\n-(see the OUTPUT section below).\n+the working tree and the index untouched, and by default updates the\n+relevant references using atomic transactions. Use `--output-commands`\n+to get the old default behavior where update commands that can be piped\n+to `git update-ref --stdin` are emitted (see the OUTPUT section below).\n \n THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n \n@@ -42,6 +42,20 @@ When `--advance` is specified, the update-ref command(s) in the output\n will update the branch passed as an argument to `--advance` to point at\n the new commits (in other words, this mimics a cherry-pick operation).\n \n+--output-commands::\n+\tOutput update-ref commands instead of updating refs directly.\n+\tWhen this option is used, the output can be piped to `git update-ref --stdin`\n+\tfor successive, relatively slow, ref updates. This is equivalent to the\n+\told default behavior.\n+\n+--allow-partial::\n+\tAllow some ref updates to succeed even if others fail. By default,\n+\tref updates are atomic (all succeed or all fail). With this option,\n+\tfailed updates are reported as warnings rather than causing the entire\n+\tcommand to fail. The command exits with code 0 only if all updates\n+\tsucceed; any failures result in exit code 1. Cannot be used with\n+\t`--output-commands`.\n+\n <revision-range>::\n \tRange of commits to replay. More than one <revision-range> can\n \tbe passed, but in `--advance <branch>` mode, they should have\n@@ -54,15 +68,20 @@ include::rev-list-options.adoc[]\n OUTPUT\n ------\n \n-When there are no conflicts, the output of this command is usable as\n-input to `git update-ref --stdin`.  It is of the form:\n+By default, when there are no conflicts, this command updates the relevant\n+references using atomic transactions and produces no output. All ref updates\n+succeed or all fail (atomic behavior). Use `--allow-partial` to allow some\n+updates to succeed while others fail.\n+\n+When `--output-commands` is used, the output is usable as input to\n+`git update-ref --stdin`. It is of the form:\n \n \tupdate refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n \tupdate refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n \tupdate refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n \n where the number of refs updated depends on the arguments passed and\n-the shape of the history being replayed.  When using `--advance`, the\n+the shape of the history being replayed. When using `--advance`, the\n number of refs updated is always one, but for `--onto`, it can be one\n or more (rebasing multiple branches simultaneously is supported).\n \n@@ -77,30 +96,50 @@ is something other than 0 or 1.\n EXAMPLES\n --------\n \n-To simply rebase `mybranch` onto `target`:\n+To simply rebase `mybranch` onto `target` (default behavior):\n \n ------------\n $ git replay --onto target origin/main..mybranch\n-update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n ------------\n \n To cherry-pick the commits from mybranch onto target:\n \n ------------\n $ git replay --advance target origin/main..mybranch\n-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n ------------\n \n Note that the first two examples replay the exact same commits and on\n top of the exact same new base, they only differ in that the first\n-provides instructions to make mybranch point at the new commits and\n-the second provides instructions to make target point at them.\n+updates mybranch to point at the new commits and the second updates\n+target to point at them.\n+\n+To get the old default behavior where update commands are emitted:\n+\n+------------\n+$ git replay --output-commands --onto target origin/main..mybranch\n+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n+------------\n+\n+To rebase multiple branches with partial failure tolerance:\n+\n+------------\n+$ git replay --allow-partial --contained --onto origin/main origin/main..tipbranch\n+------------\n \n What if you have a stack of branches, one depending upon another, and\n you'd really like to rebase the whole set?\n \n ------------\n $ git replay --contained --onto origin/main origin/main..tipbranch\n+------------\n+\n+This automatically finds and rebases all branches contained within the\n+`origin/main..tipbranch` range.\n+\n+Or if you want to see the old default behavior where update commands are emitted:\n+\n+------------\n+$ git replay --output-commands --contained --onto origin/main origin/main..tipbranch\n update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n@@ -108,10 +147,19 @@ update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n \n When calling `git replay`, one does not need to specify a range of\n commits to replay using the syntax `A..B`; any range expression will\n-do:\n+do. Here's an example where you explicitly specify which branches to rebase:\n \n ------------\n $ git replay --onto origin/main ^base branch1 branch2 branch3\n+------------\n+\n+This gives you explicit control over exactly which branches are rebased,\n+unlike the previous `--contained` example which automatically discovers them.\n+\n+To see the update commands that would be executed:\n+\n+------------\n+$ git replay --output-commands --onto origin/main ^base branch1 branch2 branch3\n update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 6172c8aacc..b6f9d53560 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -284,6 +284,28 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \treturn create_commit(repo, result->tree, pickme, replayed_base);\n }\n \n+static int add_ref_to_transaction(struct ref_transaction *transaction,\n+\t\t\t\t  const char *refname,\n+\t\t\t\t  const struct object_id *new_oid,\n+\t\t\t\t  const struct object_id *old_oid,\n+\t\t\t\t  struct strbuf *err)\n+{\n+\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n+\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n+}\n+\n+static void print_rejected_update(const char *refname,\n+\t\t\t\t  const struct object_id *old_oid UNUSED,\n+\t\t\t\t  const struct object_id *new_oid UNUSED,\n+\t\t\t\t  const char *old_target UNUSED,\n+\t\t\t\t  const char *new_target UNUSED,\n+\t\t\t\t  enum ref_transaction_error err,\n+\t\t\t\t  void *cb_data UNUSED)\n+{\n+\tconst char *reason = ref_transaction_error_msg(err);\n+\twarning(_(\"failed to update %s: %s\"), refname, reason);\n+}\n+\n int cmd_replay(int argc,\n \t       const char **argv,\n \t       const char *prefix,\n@@ -294,6 +316,8 @@ int cmd_replay(int argc,\n \tstruct commit *onto = NULL;\n \tconst char *onto_name = NULL;\n \tint contained = 0;\n+\tint output_commands = 0;\n+\tint allow_partial = 0;\n \n \tstruct rev_info revs;\n \tstruct commit *last_commit = NULL;\n@@ -302,12 +326,15 @@ int cmd_replay(int argc,\n \tstruct merge_result result;\n \tstruct strset *update_refs = NULL;\n \tkh_oid_map_t *replayed_commits;\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strbuf transaction_err = STRBUF_INIT;\n+\tint commits_processed = 0;\n \tint ret = 0;\n \n-\tconst char * const replay_usage[] = {\n+\tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n-\t\t   \"<revision-range>...\"),\n+\t\t   \"[--output-commands | --allow-partial] <revision-range>...\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -319,6 +346,10 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &contained,\n \t\t\t N_(\"advance all branches contained in revision-range\")),\n+\t\tOPT_BOOL(0, \"output-commands\", &output_commands,\n+\t\t\t N_(\"output update commands instead of updating refs\")),\n+\t\tOPT_BOOL(0, \"allow-partial\", &allow_partial,\n+\t\t\t N_(\"allow some ref updates to succeed even if others fail\")),\n \t\tOPT_END()\n \t};\n \n@@ -330,9 +361,12 @@ int cmd_replay(int argc,\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n-\tif (advance_name_opt && contained)\n-\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n-\t\t    \"--advance\", \"--contained\");\n+\tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n+\t\t\t\t  contained, \"--contained\");\n+\n+\tdie_for_incompatible_opt2(allow_partial, \"--allow-partial\",\n+\t\t\t\t  output_commands, \"--output-commands\");\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n@@ -389,6 +423,17 @@ int cmd_replay(int argc,\n \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n \t\t\t      &onto, &update_refs);\n \n+\tif (!output_commands) {\n+\t\tunsigned int transaction_flags = allow_partial ? REF_TRANSACTION_ALLOW_FAILURE : 0;\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n+\t\t\t\t\t\t\t  transaction_flags,\n+\t\t\t\t\t\t\t  &transaction_err);\n+\t\tif (!transaction) {\n+\t\t\tret = error(_(\"failed to begin ref transaction: %s\"), transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n \tif (!onto) /* FIXME: Should handle replaying down to root commit */\n \t\tdie(\"Replaying down to root commit is not supported yet!\");\n \n@@ -407,6 +452,8 @@ int cmd_replay(int argc,\n \t\tkhint_t pos;\n \t\tint hr;\n \n+\t\tcommits_processed = 1;\n+\n \t\tif (!commit->parents)\n \t\t\tdie(_(\"replaying down to root commit is not supported yet!\"));\n \t\tif (commit->parents->next)\n@@ -434,10 +481,18 @@ int cmd_replay(int argc,\n \t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n \t\t\t    (contained || strset_contains(update_refs,\n \t\t\t\t\t\t\t  decoration->name))) {\n-\t\t\t\tprintf(\"update %s %s %s\\n\",\n-\t\t\t\t       decoration->name,\n-\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t\t\t       oid_to_hex(&commit->object.oid));\n+\t\t\t\tif (output_commands) {\n+\t\t\t\t\tprintf(\"update %s %s %s\\n\",\n+\t\t\t\t\t       decoration->name,\n+\t\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n+\t\t\t\t\t       oid_to_hex(&commit->object.oid));\n+\t\t\t\t} else if (add_ref_to_transaction(transaction, decoration->name,\n+\t\t\t\t\t\t\t\t  &last_commit->object.oid,\n+\t\t\t\t\t\t\t\t  &commit->object.oid,\n+\t\t\t\t\t\t\t\t  &transaction_err) < 0) {\n+\t\t\t\t\tret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n+\t\t\t\t\tgoto cleanup;\n+\t\t\t\t}\n \t\t\t}\n \t\t\tdecoration = decoration->next;\n \t\t}\n@@ -445,10 +500,33 @@ int cmd_replay(int argc,\n \n \t/* In --advance mode, advance the target ref */\n \tif (result.clean == 1 && advance_name) {\n-\t\tprintf(\"update %s %s %s\\n\",\n-\t\t       advance_name,\n-\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t       oid_to_hex(&onto->object.oid));\n+\t\tif (output_commands) {\n+\t\t\tprintf(\"update %s %s %s\\n\",\n+\t\t\t       advance_name,\n+\t\t\t       oid_to_hex(&last_commit->object.oid),\n+\t\t\t       oid_to_hex(&onto->object.oid));\n+\t\t} else if (add_ref_to_transaction(transaction, advance_name,\n+\t\t\t\t\t\t  &last_commit->object.oid,\n+\t\t\t\t\t\t  &onto->object.oid,\n+\t\t\t\t\t\t  &transaction_err) < 0) {\n+\t\t\tret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n+\t/* Commit the ref transaction if we have one */\n+\tif (transaction && result.clean == 1) {\n+\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n+\t\t\tif (allow_partial) {\n+\t\t\t\twarning(_(\"some ref updates failed: %s\"), transaction_err.buf);\n+\t\t\t\tref_transaction_for_each_rejected_update(transaction,\n+\t\t\t\t\t\t\t\t\t print_rejected_update, NULL);\n+\t\t\t\tret = 0; /* Set failure even with allow_partial */\n+\t\t\t} else {\n+\t\t\t\tret = error(_(\"failed to update refs: %s\"), transaction_err.buf);\n+\t\t\t\tgoto cleanup;\n+\t\t\t}\n+\t\t}\n \t}\n \n \tmerge_finalize(&merge_opt, &result);\n@@ -457,9 +535,17 @@ int cmd_replay(int argc,\n \t\tstrset_clear(update_refs);\n \t\tfree(update_refs);\n \t}\n-\tret = result.clean;\n+\n+\t/* Handle empty ranges: if no commits were processed, treat as success */\n+\tif (!commits_processed)\n+\t\tret = 1; /* Success - no commits to replay is not an error */\n+\telse\n+\t\tret = result.clean;\n \n cleanup:\n+\tif (transaction)\n+\t\tref_transaction_free(transaction);\n+\tstrbuf_release(&transaction_err);\n \trelease_revisions(&revs);\n \tfree(advance_name);\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 58b3759935..8b4301e227 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n '\n \n test_expect_success 'using replay to rebase two branches, one on top of other' '\n-\tgit replay --onto main topic1..topic2 >result &&\n+\tgit replay --output-commands --onto main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -67,9 +67,30 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n \ttest_cmp expect result\n '\n \n+test_expect_success 'using replay with default atomic behavior (no output)' '\n+\t# Create a test branch that wont interfere with others\n+\tgit branch atomic-test topic2 &&\n+\tgit rev-parse atomic-test >atomic-test-old &&\n+\n+\t# Default behavior: atomic ref updates (no output)\n+\tgit replay --onto main topic1..atomic-test >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify the branch was updated\n+\tgit rev-parse atomic-test >atomic-test-new &&\n+\t! test_cmp atomic-test-old atomic-test-new &&\n+\n+\t# Verify the history is correct\n+\tgit log --format=%s atomic-test >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n-\tgit -C bare replay --onto main topic1..topic2 >result-bare &&\n-\ttest_cmp expect result-bare\n+\tgit -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&\n+\n+\t# The result should match what we got from the regular repo\n+\ttest_cmp result result-bare\n '\n \n test_expect_success 'using replay to rebase with a conflict' '\n@@ -86,7 +107,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n \t# 4th field of result is hash for main instead of hash for topic2\n \n-\tgit replay --advance main topic1..topic2 >result &&\n+\tgit replay --output-commands --advance main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -102,7 +123,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n '\n \n test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n-\tgit -C bare replay --advance main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --output-commands --advance main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -115,7 +136,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n '\n \n test_expect_success 'using replay to also rebase a contained branch' '\n-\tgit replay --contained --onto main main..topic3 >result &&\n+\tgit replay --output-commands --contained --onto main main..topic3 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -139,12 +160,12 @@ test_expect_success 'using replay to also rebase a contained branch' '\n '\n \n test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n-\tgit -C bare replay --contained --onto main main..topic3 >result-bare &&\n+\tgit -C bare replay --output-commands --contained --onto main main..topic3 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n test_expect_success 'using replay to rebase multiple divergent branches' '\n-\tgit replay --onto main ^topic1 topic2 topic4 >result &&\n+\tgit replay --output-commands --onto main ^topic1 topic2 topic4 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -168,7 +189,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n '\n \n test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n-\tgit -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n+\tgit -C bare replay --output-commands --contained --onto main ^main topic2 topic3 topic4 >result &&\n \n \ttest_line_count = 4 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -217,4 +238,131 @@ test_expect_success 'merge.directoryRenames=false' '\n \t\t--onto rename-onto rename-onto..rename-from\n '\n \n+# Tests for new default atomic behavior and options\n+\n+test_expect_success 'replay default behavior should not produce output when successful' '\n+\tgit replay --onto main topic1..topic3 >output &&\n+\ttest_must_be_empty output\n+'\n+\n+test_expect_success 'replay with --output-commands produces traditional output' '\n+\tgit replay --output-commands --onto main topic1..topic3 >output &&\n+\ttest_line_count = 1 output &&\n+\tgrep \"^update refs/heads/topic3 \" output\n+'\n+\n+test_expect_success 'replay with --allow-partial should not produce output when successful' '\n+\tgit replay --allow-partial --onto main topic1..topic3 >output &&\n+\ttest_must_be_empty output\n+'\n+\n+test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '\n+\ttest_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&\n+\tgrep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'replay with --contained updates multiple branches atomically' '\n+\t# Create fresh test branches based on the original structure\n+\t# contained-topic1 should be contained within the range to contained-topic3\n+\tgit branch contained-base main &&\n+\tgit checkout -b contained-topic1 contained-base &&\n+\ttest_commit ContainedC &&\n+\tgit checkout -b contained-topic3 contained-topic1 &&\n+\ttest_commit ContainedG &&\n+\ttest_commit ContainedH &&\n+\tgit checkout main &&\n+\n+\t# Store original states\n+\tgit rev-parse contained-topic1 >contained-topic1-old &&\n+\tgit rev-parse contained-topic3 >contained-topic3-old &&\n+\n+\t# Use --contained to update multiple branches - this should update both\n+\tgit replay --contained --onto main contained-base..contained-topic3 &&\n+\n+\t# Verify both branches were updated\n+\tgit rev-parse contained-topic1 >contained-topic1-new &&\n+\tgit rev-parse contained-topic3 >contained-topic3-new &&\n+\t! test_cmp contained-topic1-old contained-topic1-new &&\n+\t! test_cmp contained-topic3-old contained-topic3-new\n+'\n+\n+test_expect_success 'replay atomic behavior: all refs updated or none' '\n+\t# Store original state\n+\tgit rev-parse topic4 >topic4-old &&\n+\n+\t# Default atomic behavior\n+\tgit replay --onto main main..topic4 &&\n+\n+\t# Verify ref was updated\n+\tgit rev-parse topic4 >topic4-new &&\n+\t! test_cmp topic4-old topic4-new &&\n+\n+\t# Verify no partial state\n+\tgit log --format=%s topic4 >actual &&\n+\ttest_write_lines J I M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'replay works correctly with bare repositories' '\n+\t# Test atomic behavior in bare repo (important for Gitaly)\n+\tgit checkout -b bare-test topic1 &&\n+\ttest_commit BareTest &&\n+\n+\t# Test with bare repo - replay the commits from main..bare-test to get the full history\n+\tgit -C bare fetch .. bare-test:bare-test &&\n+\tgit -C bare replay --onto main main..bare-test &&\n+\n+\t# Verify the bare repo was updated correctly (no output)\n+\tgit -C bare log --format=%s bare-test >actual &&\n+\ttest_write_lines BareTest F C M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'replay --allow-partial with no failures produces no output' '\n+\tgit checkout -b partial-test topic1 &&\n+\ttest_commit PartialTest &&\n+\n+\t# Should succeed silently even with partial mode\n+\tgit replay --allow-partial --onto main topic1..partial-test >output &&\n+\ttest_must_be_empty output\n+'\n+\n+test_expect_success 'replay maintains ref update consistency' '\n+\t# Test that traditional vs atomic produce equivalent results\n+\tgit checkout -b method1-test topic2 &&\n+\tgit checkout -b method2-test topic2 &&\n+\n+\t# Both methods should update refs to point to the same replayed commits\n+\tgit replay --output-commands --onto main topic1..method1-test >update-commands &&\n+\tgit update-ref --stdin <update-commands &&\n+\tgit log --format=%s method1-test >traditional-result &&\n+\n+\t# Direct atomic method should produce same commit history\n+\tgit replay --onto main topic1..method2-test &&\n+\tgit log --format=%s method2-test >atomic-result &&\n+\n+\t# Both methods should produce identical commit histories\n+\ttest_cmp traditional-result atomic-result\n+'\n+\n+test_expect_success 'replay error messages are helpful and clear' '\n+\t# Test that error messages are clear\n+\ttest_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&\n+\tgrep \"cannot be used together\" error\n+'\n+\n+test_expect_success 'replay with empty range produces no output and no changes' '\n+\t# Create a test branch for empty range testing\n+\tgit checkout -b empty-test topic1 &&\n+\tgit rev-parse empty-test >empty-test-before &&\n+\n+\t# Empty range should succeed but do nothing\n+\tgit replay --onto main empty-test..empty-test >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Branch should be unchanged\n+\tgit rev-parse empty-test >empty-test-after &&\n+\ttest_cmp empty-test-before empty-test-after\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"527621","messageId":"CAP8UFD0POvYDgGtEx8GBhvKkd8XzzWQsy8XxAKL9M3+uz3ka+w@mail.gmail.com","threadId":"64109","inReplyTo":"20250926230838.35870-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-30T08:23:49Z","receivedAt":"2025-09-30T08:24:03Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Sat, Sep 27, 2025 at 1:09 AM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> The git replay command currently outputs update commands that must be\n> piped to git update-ref --stdin to actually update references:\n>\n>     git replay --onto main topic1..topic2 | git update-ref --stdin\n>\n> This design has significant limitations for server-side operations. The\n> two-command pipeline creates coordination complexity, provides no atomic\n> transaction guarantees by default, and complicates automation in bare\n> repository environments where git replay is primarily used.\n\nYeah, right.\n\n> During extensive mailing list discussion, multiple maintainers identified\n> that the current approach\n\nWhen you say \"current approach\" we first think we are talking about\nthe behavior you described above when you said \"The git replay command\ncurrently ...\"\n\n> forces users to opt-in to atomic behavior rather\n> than defaulting to the safer, more reliable option.\n\nBut here you are actually talking about what the previous version of\nthis patch did.\n\n> Elijah Newren noted\n> that the experimental status explicitly allows such behavior changes, while\n> Patrick Steinhardt highlighted performance concerns with individual ref\n> updates in the reftable backend.\n\nAlso the commit message is not the right place to describe what\nhappened during discussions of the previous version(s) of a patch.\nIt's not the right place to talk about previous version(s) of a patch\nin general. Those things should go into the cover letter.\n\nIf you want to talk about an option that was considered but rejected,\nyou can say something like the following instead of the whole\nparagraph:\n\n\"To address this limitation, adding an option named for example\n`--atomic-update` was considered. With such an option `git replay\n--atomic-update --onto main topic1..topic2` would atomically update\nall the refs without having to use a separate `git update-ref --stdin`\ncommand. The issue is that this would force users to opt-in to the\natomic behavior rather than have it as the default safer, faster and\nmore reliable option.\n\nFortunately the experimental status of the `git replay` command\nexplicitly allows behavior changes, so we are allowed to make the\ncommand atomically update all the refs by default.\n\"\n\n> The core issue is that git replay was designed around command output rather\n> than direct action. This made sense for a plumbing tool, but creates barriers\n> for the primary use case: server-side operations that need reliable, atomic\n> ref updates without pipeline complexity.\n\nI think this paragraph should go just before the \"Fortunately the\nexperimental status of the `git replay` command explicitly ...\" that I\nsuggest above.\n\n> This patch changes the default behavior to update refs directly using Git's\n\ns/This patch changes/Let's change/\n\n(See our SubmittingPatches documentation where it suggests using\nimperative mood to describe the changes we make.)\n\n> ref transaction API:\n>\n>     git replay --onto main topic1..topic2\n>     # No output; all refs updated atomically or none\n>\n> The implementation uses ref_store_transaction_begin() with atomic mode by\n> default, ensuring all ref updates succeed or all fail as a single operation.\n> This leverages git replay's existing server-side strengths (in-memory operation,\n> no work tree requirement) while adding the atomic guarantees that server\n> operations require.\n>\n> For users needing the traditional pipeline workflow, --output-commands\n> preserves the original behavior:\n\nI think something like:\n\n\"For users needing the traditional pipeline workflow, let's add a new\n`--output-commands`option that preserves the original behavior:\"\n\nis more explicit and makes it clear that it's a new option added by\nthis patch and not an existing option.\n\n>     git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n>\n> The --allow-partial option enables partial failure tolerance.\n\nIn the same way, something like:\n\n\"Let's also add a new `--allow-partial` option that enables partial\nfailure tolerance.\"\n\n> However, following\n> maintainer feedback, it implements a \"strict success\" model: the command exits\n\nI think you can remove \"following maintainer feedback\" here. The cover\nletter or a trailer like \"Helped-by: ...\" at the end of the commit\nmessage (but Junio will add his \"Signed-off-by: ...\" anyway so adding\nan Helped-by: ... about him is redundant) are the right place to\nmention people who helped or suggested changes.\n\n> with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY\n> updates fail. This ensures that --allow-partial changes error reporting style\n> (warnings vs hard errors) but not success criteria, handling edge cases like\n> \"no updates needed\" cleanly.\n>\n> Implementation details:\n> - Empty commit ranges now return success (exit code 0) rather than failure,\n>   as no commits to replay is a valid successful operation\n\nNit: as all the sentences in this \"Implementation details\" list start\nwith an uppercase, I think they should end with a full stop.\n\n> - Added comprehensive test coverage with 12 new tests covering atomic behavior,\n>   option validation, bare repository support, and edge cases\n> - Fixed test isolation issues to prevent branch state contamination between tests\n> - Maintains C89 compliance and follows Git's established coding conventions\n\nI am not sure this one is worth mentioning here, at least not like\nthis. You may want to say in the cover letter that compared to the\nprevious version this patch doesn't use 'bool' anymore and explain\nwhy. Or maybe you want to explain here that using the 'bool' type was\nconsidered but rejected for some reason. But in both cases, you should\nbe explicit about the reason.\n\n> - Refactored option validation to use die_for_incompatible_opt2() for both\n>   --advance/--contained and --allow-partial/--output-commands conflicts,\n>   providing consistent error reporting\n> - Fixed --allow-partial exit code behavior to implement \"strict success\" model\n>   where any ref update failures result in exit code 1, even with partial tolerance\n\nThis should probably go to the cover letter, as we should not talk in\nthe commit message about changes since a previous version of the\ncommit.\n\n> - Updated documentation with proper line wrapping, consistent terminology using\n>   \"old default behavior\", performance context, and reorganized examples for clarity\n\nThis also sounds like a change compared to the previous version of the patch.\n\n> - Eliminates individual ref updates (refs_update_ref calls) that perform\n>   poorly with reftable backend\n\nThis also sounds like a change compared to the previous version of the patch.\n\n> - Uses only batched ref transactions for optimal performance across all\n>   ref backends\n\nI think you can remove \"only\" in the sentence as in the\n--output-commands case no transaction is used.\n\n> - Avoids naming collision with git rebase --update-refs by using distinct\n>   option names\n\nThis also sounds like a change compared to the previous version of the patch.\n\n> - Defaults to atomic behavior while preserving pipeline compatibility\n\nThis has been discussed above. It doesn't look like an implementation\ndetail to me.\n\n> The result is a command that works better for its primary use case (server-side\n> operations) while maintaining full backward compatibility for existing workflows.\n>\n> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n\nAdding \"Helped-by: ...\" trailers for at least Elijah and Patrick would be nice.\n"},{"id":"527624","messageId":"9052eccc-1121-442f-ad51-4fe9217024a0@gmail.com","threadId":"64109","inReplyTo":"20250926230838.35870-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-30T10:05:20Z","receivedAt":"2025-09-30T10:05:15Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Siddharth\n\nOn 27/09/2025 00:08, Siddharth Asthana wrote:\n> The git replay command currently outputs update commands that must be\n> piped to git update-ref --stdin to actually update references:\n> \n>      git replay --onto main topic1..topic2 | git update-ref --stdin\n> \n> This design has significant limitations for server-side operations. The\n> two-command pipeline creates coordination complexity, provides no atomic\n> transaction guarantees by default\n\nAre you sure that's true? Maybe I'm missing something but my reading of \nbuiltin/update-ref.c is that it when \"--stdin\" is given it starts a ref \ntransaction, reads the commands from stdin and applies them to that \ntransaction and then commits the transaction which will make the updates \natomic.\n\n> , and complicates automation in bare\n> repository environments where git replay is primarily used.\n\nHow does it complicate automation in bare repositories?\n\nChristian has given detailed feedback on the rest of the commit message \nso I'll not comment on it further.\n\n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index 0b12bf8aa4..e104e0bc03 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -9,16 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>   SYNOPSIS\n>   --------\n>   [verse]\n> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--output-commands | --allow-partial] <revision-range>...\n\nPlease wrap this very long line\n\n> @@ -42,6 +42,20 @@ When `--advance` is specified, the update-ref command(s) in the output\n>   will update the branch passed as an argument to `--advance` to point at\n>   the new commits (in other words, this mimics a cherry-pick operation).\n>   \n> +--output-commands::\n> +\tOutput update-ref commands instead of updating refs directly.\n> +\tWhen this option is used, the output can be piped to `git update-ref --stdin`\n> +\tfor successive, relatively slow, ref updates. This is equivalent to the\n> +\told default behavior.\n> +\n> +--allow-partial::\n> +\tAllow some ref updates to succeed even if others fail. By default,\n> +\tref updates are atomic (all succeed or all fail). With this option,\n> +\tfailed updates are reported as warnings rather than causing the entire\n> +\tcommand to fail. The command exits with code 0 only if all updates\n> +\tsucceed; any failures result in exit code 1. Cannot be used with\n> +\t`--output-commands`.\n\nRather than having two incompatible options perhaps we could have a \nsingle \"--update-refs=(yes|print|allow-partial-updates)\" argument. I \nthink the name \"--allow-partial\" is rather ambiguous as it does not say \nwhat it is allowing to be partial.\n\n> +static int add_ref_to_transaction(struct ref_transaction *transaction,\n> +\t\t\t\t  const char *refname,\n> +\t\t\t\t  const struct object_id *new_oid,\n> +\t\t\t\t  const struct object_id *old_oid,\n> +\t\t\t\t  struct strbuf *err)\n> +{\n> +\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n> +\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n> +}\n\nI'm not sure this function adds much value. I think it would be better \nto instead have a helper function that updates refs or prints the ref \nupdates so that we do not duplicate that code in the two places below.\n\n> @@ -434,10 +481,18 @@ int cmd_replay(int argc,\n>   \t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n>   \t\t\t    (contained || strset_contains(update_refs,\n>   \t\t\t\t\t\t\t  decoration->name))) {\n> -\t\t\t\tprintf(\"update %s %s %s\\n\",\n> -\t\t\t\t       decoration->name,\n> -\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n> -\t\t\t\t       oid_to_hex(&commit->object.oid));\n> +\t\t\t\tif (output_commands) {\n> +\t\t\t\t\tprintf(\"update %s %s %s\\n\",\n> +\t\t\t\t\t       decoration->name,\n> +\t\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n> +\t\t\t\t\t       oid_to_hex(&commit->object.oid));\n> +\t\t\t\t} else if (add_ref_to_transaction(transaction, decoration->name,\n> +\t\t\t\t\t\t\t\t  &last_commit->object.oid,\n> +\t\t\t\t\t\t\t\t  &commit->object.oid,\n> +\t\t\t\t\t\t\t\t  &transaction_err) < 0) {\n> +\t\t\t\t\tret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n> +\t\t\t\t\tgoto cleanup;\n> +\t\t\t\t}\n>   \t\t\t}\n\nThe lines here are very long due to the indentation, having a separate \nfunction to update the refs or print the ref updates would be much more \nreadable.\n\n>   \t\t\tdecoration = decoration->next;\n>   \t\t}\n> @@ -445,10 +500,33 @@ int cmd_replay(int argc,\n>   \n>   \t/* In --advance mode, advance the target ref */\n>   \tif (result.clean == 1 && advance_name) {\n> -\t\tprintf(\"update %s %s %s\\n\",\n> -\t\t       advance_name,\n> -\t\t       oid_to_hex(&last_commit->object.oid),\n> -\t\t       oid_to_hex(&onto->object.oid));\n> +\t\tif (output_commands) {\n> +\t\t\tprintf(\"update %s %s %s\\n\",\n> +\t\t\t       advance_name,\n> +\t\t\t       oid_to_hex(&last_commit->object.oid),\n> +\t\t\t       oid_to_hex(&onto->object.oid));\n> +\t\t} else if (add_ref_to_transaction(transaction, advance_name,\n> +\t\t\t\t\t\t  &last_commit->object.oid,\n> +\t\t\t\t\t\t  &onto->object.oid,\n> +\t\t\t\t\t\t  &transaction_err) < 0) {\n> +\t\t\tret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n> +\t\t\tgoto cleanup;\n> +\t\t}\n> +\t}\n\nPutting the code to update the refs or print the ref updates into a \nsingle function would avoid this duplication and over-long lines.\n\nThanks\n\nPhillip\n\n> +\t/* Commit the ref transaction if we have one */\n> +\tif (transaction && result.clean == 1) {\n> +\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n> +\t\t\tif (allow_partial) {\n> +\t\t\t\twarning(_(\"some ref updates failed: %s\"), transaction_err.buf);\n> +\t\t\t\tref_transaction_for_each_rejected_update(transaction,\n> +\t\t\t\t\t\t\t\t\t print_rejected_update, NULL);\n> +\t\t\t\tret = 0; /* Set failure even with allow_partial */\n> +\t\t\t} else {\n> +\t\t\t\tret = error(_(\"failed to update refs: %s\"), transaction_err.buf);\n> +\t\t\t\tgoto cleanup;\n> +\t\t\t}\n> +\t\t}\n>   \t}\n>   \n>   \tmerge_finalize(&merge_opt, &result);\n> @@ -457,9 +535,17 @@ int cmd_replay(int argc,\n>   \t\tstrset_clear(update_refs);\n>   \t\tfree(update_refs);\n>   \t}\n> -\tret = result.clean;\n> +\n> +\t/* Handle empty ranges: if no commits were processed, treat as success */\n> +\tif (!commits_processed)\n> +\t\tret = 1; /* Success - no commits to replay is not an error */\n> +\telse\n> +\t\tret = result.clean;\n>   \n>   cleanup:\n> +\tif (transaction)\n> +\t\tref_transaction_free(transaction);\n> +\tstrbuf_release(&transaction_err);\n>   \trelease_revisions(&revs);\n>   \tfree(advance_name);\n>   \n> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n> index 58b3759935..8b4301e227 100755\n> --- a/t/t3650-replay-basics.sh\n> +++ b/t/t3650-replay-basics.sh\n> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n>   '\n>   \n>   test_expect_success 'using replay to rebase two branches, one on top of other' '\n> -\tgit replay --onto main topic1..topic2 >result &&\n> +\tgit replay --output-commands --onto main topic1..topic2 >result &&\n>   \n>   \ttest_line_count = 1 result &&\n>   \n> @@ -67,9 +67,30 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n>   \ttest_cmp expect result\n>   '\n>   \n> +test_expect_success 'using replay with default atomic behavior (no output)' '\n> +\t# Create a test branch that wont interfere with others\n> +\tgit branch atomic-test topic2 &&\n> +\tgit rev-parse atomic-test >atomic-test-old &&\n> +\n> +\t# Default behavior: atomic ref updates (no output)\n> +\tgit replay --onto main topic1..atomic-test >output &&\n> +\ttest_must_be_empty output &&\n> +\n> +\t# Verify the branch was updated\n> +\tgit rev-parse atomic-test >atomic-test-new &&\n> +\t! test_cmp atomic-test-old atomic-test-new &&\n> +\n> +\t# Verify the history is correct\n> +\tgit log --format=%s atomic-test >actual &&\n> +\ttest_write_lines E D M L B A >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n>   test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n> -\tgit -C bare replay --onto main topic1..topic2 >result-bare &&\n> -\ttest_cmp expect result-bare\n> +\tgit -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&\n> +\n> +\t# The result should match what we got from the regular repo\n> +\ttest_cmp result result-bare\n>   '\n>   \n>   test_expect_success 'using replay to rebase with a conflict' '\n> @@ -86,7 +107,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n>   \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n>   \t# 4th field of result is hash for main instead of hash for topic2\n>   \n> -\tgit replay --advance main topic1..topic2 >result &&\n> +\tgit replay --output-commands --advance main topic1..topic2 >result &&\n>   \n>   \ttest_line_count = 1 result &&\n>   \n> @@ -102,7 +123,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n>   '\n>   \n>   test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n> -\tgit -C bare replay --advance main topic1..topic2 >result-bare &&\n> +\tgit -C bare replay --output-commands --advance main topic1..topic2 >result-bare &&\n>   \ttest_cmp expect result-bare\n>   '\n>   \n> @@ -115,7 +136,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n>   '\n>   \n>   test_expect_success 'using replay to also rebase a contained branch' '\n> -\tgit replay --contained --onto main main..topic3 >result &&\n> +\tgit replay --output-commands --contained --onto main main..topic3 >result &&\n>   \n>   \ttest_line_count = 2 result &&\n>   \tcut -f 3 -d \" \" result >new-branch-tips &&\n> @@ -139,12 +160,12 @@ test_expect_success 'using replay to also rebase a contained branch' '\n>   '\n>   \n>   test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n> -\tgit -C bare replay --contained --onto main main..topic3 >result-bare &&\n> +\tgit -C bare replay --output-commands --contained --onto main main..topic3 >result-bare &&\n>   \ttest_cmp expect result-bare\n>   '\n>   \n>   test_expect_success 'using replay to rebase multiple divergent branches' '\n> -\tgit replay --onto main ^topic1 topic2 topic4 >result &&\n> +\tgit replay --output-commands --onto main ^topic1 topic2 topic4 >result &&\n>   \n>   \ttest_line_count = 2 result &&\n>   \tcut -f 3 -d \" \" result >new-branch-tips &&\n> @@ -168,7 +189,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n>   '\n>   \n>   test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n> -\tgit -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n> +\tgit -C bare replay --output-commands --contained --onto main ^main topic2 topic3 topic4 >result &&\n>   \n>   \ttest_line_count = 4 result &&\n>   \tcut -f 3 -d \" \" result >new-branch-tips &&\n> @@ -217,4 +238,131 @@ test_expect_success 'merge.directoryRenames=false' '\n>   \t\t--onto rename-onto rename-onto..rename-from\n>   '\n>   \n> +# Tests for new default atomic behavior and options> > +test_expect_success 'replay default behavior should not produce \noutput when successful' '\n> +\tgit replay --onto main topic1..topic3 >output &&\n> +\ttest_must_be_empty output\n> +'\n> +\n> +test_expect_success 'replay with --output-commands produces traditional output' '\n> +\tgit replay --output-commands --onto main topic1..topic3 >output &&\n> +\ttest_line_count = 1 output &&\n> +\tgrep \"^update refs/heads/topic3 \" output\n> +'\n> +\n> +test_expect_success 'replay with --allow-partial should not produce output when successful' '\n> +\tgit replay --allow-partial --onto main topic1..topic3 >output &&\n> +\ttest_must_be_empty output\n> +'\n> +\n> +test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '\n> +\ttest_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&\n> +\tgrep \"cannot be used together\" error\n> +'\n> +\n> +test_expect_success 'replay with --contained updates multiple branches atomically' '\n> +\t# Create fresh test branches based on the original structure\n> +\t# contained-topic1 should be contained within the range to contained-topic3\n> +\tgit branch contained-base main &&\n> +\tgit checkout -b contained-topic1 contained-base &&\n> +\ttest_commit ContainedC &&\n> +\tgit checkout -b contained-topic3 contained-topic1 &&\n> +\ttest_commit ContainedG &&\n> +\ttest_commit ContainedH &&\n> +\tgit checkout main &&\n> +\n> +\t# Store original states\n> +\tgit rev-parse contained-topic1 >contained-topic1-old &&\n> +\tgit rev-parse contained-topic3 >contained-topic3-old &&\n> +\n> +\t# Use --contained to update multiple branches - this should update both\n> +\tgit replay --contained --onto main contained-base..contained-topic3 &&\n> +\n> +\t# Verify both branches were updated\n> +\tgit rev-parse contained-topic1 >contained-topic1-new &&\n> +\tgit rev-parse contained-topic3 >contained-topic3-new &&\n> +\t! test_cmp contained-topic1-old contained-topic1-new &&\n> +\t! test_cmp contained-topic3-old contained-topic3-new\n> +'\n> +\n> +test_expect_success 'replay atomic behavior: all refs updated or none' '\n> +\t# Store original state\n> +\tgit rev-parse topic4 >topic4-old &&\n> +\n> +\t# Default atomic behavior\n> +\tgit replay --onto main main..topic4 &&\n> +\n> +\t# Verify ref was updated\n> +\tgit rev-parse topic4 >topic4-new &&\n> +\t! test_cmp topic4-old topic4-new &&\n> +\n> +\t# Verify no partial state\n> +\tgit log --format=%s topic4 >actual &&\n> +\ttest_write_lines J I M L B A >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'replay works correctly with bare repositories' '\n> +\t# Test atomic behavior in bare repo (important for Gitaly)\n> +\tgit checkout -b bare-test topic1 &&\n> +\ttest_commit BareTest &&\n> +\n> +\t# Test with bare repo - replay the commits from main..bare-test to get the full history\n> +\tgit -C bare fetch .. bare-test:bare-test &&\n> +\tgit -C bare replay --onto main main..bare-test &&\n> +\n> +\t# Verify the bare repo was updated correctly (no output)\n> +\tgit -C bare log --format=%s bare-test >actual &&\n> +\ttest_write_lines BareTest F C M L B A >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'replay --allow-partial with no failures produces no output' '\n> +\tgit checkout -b partial-test topic1 &&\n> +\ttest_commit PartialTest &&\n> +\n> +\t# Should succeed silently even with partial mode\n> +\tgit replay --allow-partial --onto main topic1..partial-test >output &&\n> +\ttest_must_be_empty output\n> +'\n> +\n> +test_expect_success 'replay maintains ref update consistency' '\n> +\t# Test that traditional vs atomic produce equivalent results\n> +\tgit checkout -b method1-test topic2 &&\n> +\tgit checkout -b method2-test topic2 &&\n> +\n> +\t# Both methods should update refs to point to the same replayed commits\n> +\tgit replay --output-commands --onto main topic1..method1-test >update-commands &&\n> +\tgit update-ref --stdin <update-commands &&\n> +\tgit log --format=%s method1-test >traditional-result &&\n> +\n> +\t# Direct atomic method should produce same commit history\n> +\tgit replay --onto main topic1..method2-test &&\n> +\tgit log --format=%s method2-test >atomic-result &&\n> +\n> +\t# Both methods should produce identical commit histories\n> +\ttest_cmp traditional-result atomic-result\n> +'\n> +\n> +test_expect_success 'replay error messages are helpful and clear' '\n> +\t# Test that error messages are clear\n> +\ttest_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&\n> +\tgrep \"cannot be used together\" error\n> +'\n> +\n> +test_expect_success 'replay with empty range produces no output and no changes' '\n> +\t# Create a test branch for empty range testing\n> +\tgit checkout -b empty-test topic1 &&\n> +\tgit rev-parse empty-test >empty-test-before &&\n> +\n> +\t# Empty range should succeed but do nothing\n> +\tgit replay --onto main empty-test..empty-test >output &&\n> +\ttest_must_be_empty output &&\n> +\n> +\t# Branch should be unchanged\n> +\tgit rev-parse empty-test >empty-test-after &&\n> +\ttest_cmp empty-test-before empty-test-after\n> +'\n> +\n>   test_done\n\n"},{"id":"527778","messageId":"CAOLa=ZQjMzCiVd8tRXtJJ8yXxLgwGQDgOZW3F86h9jC71NJm5w@mail.gmail.com","threadId":"64109","inReplyTo":"9052eccc-1121-442f-ad51-4fe9217024a0@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-02T10:00:30Z","receivedAt":"2025-10-02T10:00:33Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> Hi Siddharth\n>\n> On 27/09/2025 00:08, Siddharth Asthana wrote:\n>> The git replay command currently outputs update commands that must be\n>> piped to git update-ref --stdin to actually update references:\n>>\n>>      git replay --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> This design has significant limitations for server-side operations. The\n>> two-command pipeline creates coordination complexity, provides no atomic\n>> transaction guarantees by default\n>\n> Are you sure that's true? Maybe I'm missing something but my reading of\n> builtin/update-ref.c is that it when \"--stdin\" is given it starts a ref\n> transaction, reads the commands from stdin and applies them to that\n> transaction and then commits the transaction which will make the updates\n> atomic.\n>\n\nYou're right. Using '--stdin' is atomic by default. You can manually\nhandle the transaction's by passing in the 'start', 'prepare', 'commit',\n'abort' sub-commands in the '--stdin' mode.\n\n[snip]\n"},{"id":"527810","messageId":"CABPp-BEh7VEM6UQjkK3CxJcv54vEmueTmh9+-SyTKUxgy7Mkcg@mail.gmail.com","threadId":"64109","inReplyTo":"20250926230838.35870-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-10-02T16:32:51Z","receivedAt":"2025-10-02T16:33:04Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Hi,\n\nFirst of all, thanks for continuing to work on this.\n\nOn Fri, Sep 26, 2025 at 4:09 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> The git replay command currently outputs update commands that must be\n\ncan be, not must be.\n\nThis separation had three advantages:\n  * it made testing easy (when state isn't modified from one step to\nthe next, you don't need to make temporary branches or have undo\ncommands, or try to track the changes)\n  * it provided a natural can-it-rebase-cleanly (and what would it\nrebase to) capability without automatically updating refs, I guess\nkind of like a --dry-run\n  * it provided a natural low-level tool for the suite of hash-object,\nmktree, commit-tree, mktag, merge-tree, and update-ref, allowing users\nto have another building block for experimentation and making new\ntools.\n\nI was particularly focused on the last of those items for the intial\nversion at the time, but it should be noted that all three of these\nare somewhat special cases, and the most common user desire is going\nto be replaying commits and updating the references at the end.\n\n> piped to git update-ref --stdin to actually update references:\n>\n>     git replay --onto main topic1..topic2 | git update-ref --stdin\n\nor, alternatively, for those that want a single, atomic (assuming the\nbackend supports it) transaction:\n\n   (echo start; git replay --onto main topic1..topic2; echo prepare;\necho commit) | git update-ref --stdin\n\n> This design has significant limitations for server-side operations. The\n> two-command pipeline creates coordination complexity,\n\nagreed\n\n> provides no atomic\n> transaction guarantees by default,\n\nSure, but it's quite trivial to add, right -- as shown above with the\nextra \"start\", \"prepare\", \"commit\" directives?\n\n> and complicates automation in bare\n> repository environments where git replay is primarily used.\n\nIsn't this repeating the first statement from this paragraph?\n\nThe paragraph also leaves off that I think it makes it more\nuseful/usable to end-users.\n\n> The core issue is that git replay was designed around command output rather\n> than direct action. This made sense for a plumbing tool, but creates barriers\n> for the primary use case: server-side operations that need reliable, atomic\n> ref updates without pipeline complexity.\n\ngit replay was originally written with the goal of eventually\nproviding a better interactive rebase.  I thought it important to have\nthe earlier versions provide new useful low-level building blocks, but\ndidn't intend for it to just be a low-level building block\nindefinitely.  The primary use case originally envisioned was\nclient-side user interactive operations without much thought for the\nserver, but it turned out to be easiest to implement what server-side\noperations needed first.  I made others aware of what I was working on\nso they could see, and Christian latched on and to my surprise told me\nit had enough for what he needed...and Johannes said about the same.\nUnfortunately, I got reassigned away from Git at my former dayjob,\n_and_ had multiple big long-term family issues strike about the same\ntime, _and_ all Git-related employers did mass layoffs and locked down\nhiring about that time as well...so the end result is only that\ninitial server-side stuff was done and it looks to outside observers\nlike git replay's design and usecase was around the server.  While\nthat's an understandable guess from the outside, this paragraph\nappears to have taken those guesses and rewrites them as fact.  I\nthink it could be reworded with small tweaks to alter it and make it\nfactual (s/designed/focused initially/, s/the primary use case/its\ncurrent primary use case/), but even then, the paragraph that remains\nfeels like it's just repeating what you said above.  It doesn't feel\nlike it adds any positive value.\n\nMight I suggest a rewrite of the text of the commit message to this point?\n\n=====\nThe git replay command currently outputs update commands that can be\npiped to update-ref to achieve a rebase, e.g.\n\n  git replay --onto main topic1..topic2 | git update-ref --stdin\n\nThis separation had advantages for three special cases:\n  * it made testing easy (when state isn't modified from one step to\nthe next, you don't need to make temporary branches or have undo\ncommands, or try to track the changes)\n  * it provided a natural can-it-rebase-cleanly (and what would it\nrebase to) capability without automatically updating refs, I guess\nkind of like a --dry-run\n  * it provided a natural low-level tool for the suite of hash-object,\nmktree, commit-tree, mktag, merge-tree, and update-ref, allowing users\nto have another building block for experimentation and making new\ntools.\n\nHowever, it should be noted that all three of these are somewhat\nspecial cases; users, whether on the client or server side, would\nalmost certainly find it more ergonomical to simply have the updating\nof refs be the default.  Change the default behavior to update refs\ndirectly, and atomically (at least to the extent supported by the refs\nbackend in use).\n====\n\nafter this, I'd suggest also nuking your next few paragraphs and then\ncontinuing with:\n\n> For users needing the traditional pipeline workflow, --output-commands\n> preserves the original behavior:\n>\n>     git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n\nThis is good.  Did you also add a config option so that someone can\njust set that option once and use the old behavior?  (as per the\nsuggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/\n?)\n\n> The --allow-partial option enables partial failure tolerance. However, following\n> maintainer feedback, it implements a \"strict success\" model: the command exits\n> with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY\n> updates fail. This ensures that --allow-partial changes error reporting style\n> (warnings vs hard errors) but not success criteria, handling edge cases like\n> \"no updates needed\" cleanly.\n\nWhy is --allow-partial helpful?  You discussed at length why you\nwanted atomic transactions, but you introduce this option with no\nrationale and instead just discuss that you implemented it and some\ndesign choices once you presuppose that someone wants to use it.\n\nIs there a usecase?  I asked for it last time, and suggested\ndiscarding the modes without one, but you only discarded one of the\nextras while leaving this one in.  I'd recommend discarding this one\ntoo and just having the two modes -- the output commands that get fed\nto update-ref, or the automatic transactional update of all or no\nrefs.\n\n> Implementation details:\n\nChristian's comments on this list are good; it seems to mostly be\nstuff that belongs in the cover letter.  But I'll comment/query about\ntwo things...\n\n> - Empty commit ranges now return success (exit code 0) rather than failure,\n>   as no commits to replay is a valid successful operation\n\nThat is a useful thing to call out.  Hmm....\n\n> - Defaults to atomic behavior while preserving pipeline compatibility\n\nThe first part has already been covered at length; it doesn't seem\nlike it needs to be repeated.  What does \"while preserving pipeline\ncompatibility\" mean, though?\n\n\n> The result is a command that works better for its primary use case (server-side\n> operations)\n\nThis sentence feels like it's rewriting the history behind the\ncommand, and undersells the change by not noting that it *also* helps\nwith client-side usage.  Also, since I think it's about the third time\nyou've said this, it adds nothing to the discussion either; let's just\nstrike it.\n\n> while maintaining full backward compatibility for existing workflows.\n\nand this is clearly false; backwards compatibility was intentionally\nbroken by making update-refs the default.  Junio suggested making it\neasy to get the old behavior with a config setting, but looking ahead\nit appears you didn't even make it easy for users to recover the old\nbehavior; they have to specify an additional flag with every command\nthey run.\n\nAlso, you further broke \"full\" backward compatibility by changing the\nexit status for empty ranges.  I think that's a rather minor change\nand maybe bugfix, but \"full\" invites comparisons and scrutiny of that\nsort.\n\nWe could say something like \"while making it easy to recover the\ntraditional behavior with a simple command line flag\", but this again\nfeels like you're just repeating what was called out above.  Maybe\njust strike it as well?\n\n\nAnd on a separate note, several of your lines in your commit message\nare too long; in an 80-column terminal, a `git log` on your commit\nmessage has several lines wrapping around.  (Command lines, and for\nfuture reference output from commands, can run longer, but regular\nparagraphs should fit.)\n\n> +...Use `--output-commands`\n> +to get the old default behavior where update commands that can be piped\n> +to `git update-ref --stdin` are emitted (see the OUTPUT section below).\n\nPerhaps instead:\n\nUse `--output-commands` to avoid the automatic ref updates and instead\nget update commands that can be piped\nto `git update-ref --stdin` (see the OUTPUT section below).\n\n> +--output-commands::\n> +       Output update-ref commands instead of updating refs directly.\n> +       When this option is used, the output can be piped to `git update-ref --stdin`\n> +       for successive, relatively slow, ref updates. This is equivalent to the\n> +       old default behavior.\n\n\"piped\" => \"piped as-is\" + \"successive, relatively slow,\" => \"a\nnon-transactional set of\" ?\n\n> +--allow-partial::\n> +       Allow some ref updates to succeed even if others fail. By default,\n> +       ref updates are atomic (all succeed or all fail). With this option,\n> +       failed updates are reported as warnings rather than causing the entire\n> +       command to fail. The command exits with code 0 only if all updates\n> +       succeed; any failures result in exit code 1. Cannot be used with\n> +       `--output-commands`.\n\nIf we keep this, I like Phillip's suggestion to combine to a single\nflag with a value.  But I still want to hear the use case behind it;\nit goes against what you repeatedly claimed was wanted on the server,\nand you didn't discuss any other usecase.  It feels like you were\ntrying to proactively support every possible way people might want to\nupdate without considering the usecases behind it, and I'd rather just\nleave it unimplemented unless or until there's demand for it.\n\n> +\n>  <revision-range>::\n>         Range of commits to replay. More than one <revision-range> can\n>         be passed, but in `--advance <branch>` mode, they should have\n> @@ -54,15 +68,20 @@ include::rev-list-options.adoc[]\n>  OUTPUT\n>  ------\n>\n> -When there are no conflicts, the output of this command is usable as\n> -input to `git update-ref --stdin`.  It is of the form:\n> +By default, when there are no conflicts, this command updates the relevant\n> +references using atomic transactions\n\natomic transaction*s*?  Shouldn't that be *an* atomic transaction instead?\n\n> >and produces no output. All ref updates\n> +succeed or all fail (atomic behavior).\n\nWhy the need to repeat?\n\n> +To rebase multiple branches with partial failure tolerance:\n> +\n> +------------\n> +$ git replay --allow-partial --contained --onto origin/main origin/main..tipbranch\n> +------------\n\nI don't understand why this one deserves an example; the command line\nflag seems to be self-explanatory.  The onto and advanced are\ninteresting in that the ranges specified with them might not be\nobvious from their description and so examples help.  One or maybe two\n--contained examples can go with those, to demonstrate how it involves\nadditional branches, though those might be better coupled with the\n--output-commands because the output more clearly demonstrates what is\nhappening (i.e. what is being updated).  But I don't see what this\nexample might elucidate.\n\nThen again, while I perfectly understand what the flag does, I have no\nidea why you thought it was useful to add, so maybe I'm just missing\nsomething.\n\n>  When calling `git replay`, one does not need to specify a range of\n>  commits to replay using the syntax `A..B`; any range expression will\n> -do:\n> +do. Here's an example where you explicitly specify which branches to rebase:\n>\n>  ------------\n>  $ git replay --onto origin/main ^base branch1 branch2 branch3\n> +------------\n> +\n> +This gives you explicit control over exactly which branches are rebased,\n> +unlike the previous `--contained` example which automatically discovers them.\n> +\n> +To see the update commands that would be executed:\n> +\n> +------------\n> +$ git replay --output-commands --onto origin/main ^base branch1 branch2 branch3\n>  update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n>  update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n>  update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n\nAs noted above, I don't think we need to repeat an --output-commands\nexample for every example that exists; it feels like overkill.  One\n(or _maybe_ two) should be sufficient.\n\n> @@ -330,9 +361,12 @@ int cmd_replay(int argc,\n>                 usage_with_options(replay_usage, replay_options);\n>         }\n>\n> -       if (advance_name_opt && contained)\n> -               die(_(\"options '%s' and '%s' cannot be used together\"),\n> -                   \"--advance\", \"--contained\");\n> +       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n> +                                 contained, \"--contained\");\n\nBroken indentation.  Also, should this have been done as a preparatory\ncleanup patch?\n\n> @@ -407,6 +452,8 @@ int cmd_replay(int argc,\n>                 khint_t pos;\n>                 int hr;\n>\n> +               commits_processed = 1;\n> +\n>                 if (!commit->parents)\n>                         die(_(\"replaying down to root commit is not supported yet!\"));\n>                 if (commit->parents->next)\n> @@ -457,9 +535,17 @@ int cmd_replay(int argc,\n>                 strset_clear(update_refs);\n>                 free(update_refs);\n>         }\n> -       ret = result.clean;\n> +\n> +       /* Handle empty ranges: if no commits were processed, treat as success */\n> +       if (!commits_processed)\n> +               ret = 1; /* Success - no commits to replay is not an error */\n> +       else\n> +               ret = result.clean;\n\nThe change to treat empty ranges as success is an orthogonal change\nthat I think at a minimum belongs in a separate patch.  Out of\ncuriosity, how did you discover the exit status with an empty commit\nrange?  Why does someone specify such a range, and what form or forms\nmight it come in?  And is merely returning a successful result enough,\nor is there more that needs to be done for correctness?\n\n> +test_expect_success 'using replay with default atomic behavior (no output)' '\n> +       # Create a test branch that wont interfere with others\n\nThis works, but I feel doing this clutters the repo for someone\ninspecting later when some test in the testsuite fails, and makes\nreaders track more branches to find out what the tests are all doing.\nWould it be easier to prefix your test with something like\n\n        START=$(git rev-parse topic2) &&\n        test_when_finished \"git branch -f topic2 $START\" &&\n\nand then...\n\n> +       git branch atomic-test topic2 &&\n> +       git rev-parse atomic-test >atomic-test-old &&\n\ndrop these lines...\n\n> +\n> +       # Default behavior: atomic ref updates (no output)\n> +       git replay --onto main topic1..atomic-test >output &&\n\nuse topic2 instead of atomic-test...\n\n> +       test_must_be_empty output &&\n> +\n> +       # Verify the branch was updated\n> +       git rev-parse atomic-test >atomic-test-new &&\n> +       ! test_cmp atomic-test-old atomic-test-new &&\n\nNot sure this comparison to verify the branch was updated makes much\nsense given that the few lines below both test that it was updated and\nthat it was updated to the right thing.\n\n> +\n> +       # Verify the history is correct\n> +       git log --format=%s atomic-test >actual &&\n> +       test_write_lines E D M L B A >expect &&\n> +       test_cmp expect actual\n\n...and finally use topic2 instead of atomic-test here.\n\n\nSimilarly, using test_when_finished throughout the rest of the\ntestsuite similarly I think would make it a bit easier to follow and\nlater debug.\n\n\n>  test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n> -       git -C bare replay --onto main topic1..topic2 >result-bare &&\n> -       test_cmp expect result-bare\n> +       git -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&\n> +\n> +       # The result should match what we got from the regular repo\n> +       test_cmp result result-bare\n>  '\n\nWhat do you mean by \"regular repo\"?  There's only one repo at play.\n\nAlso, why change \"expect\" to \"result\" here?  You don't care if you get\nthe expected result, merely that you get the same result as a previous\ntest even if it was also buggy?  I think the reason was that you\ninserted a test since writing the expectation out, but I think it'd be\nbetter to either re-write the expectation out and compare to it, or\nreorder the tests so you can use the same expectation from before.\n\n\n> +# Tests for new default atomic behavior and options\n\nThe word \"new\" here is going to become unhelpful in the future when\nsomeone reads this a few years from now; you should strike it.  What\ndoes \"and options\" mean here?\n\n> +\n> +test_expect_success 'replay default behavior should not produce output when successful' '\n> +       git replay --onto main topic1..topic3 >output &&\n> +       test_must_be_empty output\n> +'\n\nThis changes where topic3 points; I think a test_when_finished to\nreset it back at the end of the test would be nice.\n\n> +test_expect_success 'replay with --output-commands produces traditional output' '\n> +       git replay --output-commands --onto main topic1..topic3 >output &&\n> +       test_line_count = 1 output &&\n> +       grep \"^update refs/heads/topic3 \" output\n> +'\n\nThe fact that topic3 was already replayed in the previous test makes\nthis test weaker.  And the fact that it does nothing but check that\nthere is in fact some output makes it weaker still.  But rather than\nfix it, there were already lots of commands that tested\n--output-commands prior to this, and you added a comment above that\nyou were adding tests of the atomic behavior, I don't see why we need\nany additional tests of --output-commands.\n\n> +test_expect_success 'replay with --allow-partial should not produce output when successful' '\n> +       git replay --allow-partial --onto main topic1..topic3 >output &&\n> +       test_must_be_empty output\n> +\n> +test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '\n> +       test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&\n> +       grep \"cannot be used together\" error\n> +'\n\nThese are fine if --allow-partial can be motivated, but otherwise\nshould be removed along with that flag.\n\n> +\n> +test_expect_success 'replay with --contained updates multiple branches atomically' '\n> +       # Create fresh test branches based on the original structure\n\nUnnecessary if you use the test_when_finished stuff I showed you to\nensure the original structure remains intact\n\n> +       # contained-topic1 should be contained within the range to contained-topic3\n> +       git branch contained-base main &&\n> +       git checkout -b contained-topic1 contained-base &&\n> +       test_commit ContainedC &&\n> +       git checkout -b contained-topic3 contained-topic1 &&\n> +       test_commit ContainedG &&\n> +       test_commit ContainedH &&\n> +       git checkout main &&\n> +\n> +       # Store original states\n> +       git rev-parse contained-topic1 >contained-topic1-old &&\n> +       git rev-parse contained-topic3 >contained-topic3-old &&\n> +\n> +       # Use --contained to update multiple branches - this should update both\n> +       git replay --contained --onto main contained-base..contained-topic3 &&\n> +\n> +       # Verify both branches were updated\n> +       git rev-parse contained-topic1 >contained-topic1-new &&\n> +       git rev-parse contained-topic3 >contained-topic3-new &&\n> +       ! test_cmp contained-topic1-old contained-topic1-new &&\n> +       ! test_cmp contained-topic3-old contained-topic3-new\n> +'\n\nI think verifying both branches were modified without checking\nanything about the modification is a pretty weak test.  We should\ncheck that both branches have the appropriate commit sequence in them\nvia git log output, as previous tests do.\n\n> +test_expect_success 'replay atomic behavior: all refs updated or none' '\n> +       # Store original state\n> +       git rev-parse topic4 >topic4-old &&\n> +\n> +       # Default atomic behavior\n> +       git replay --onto main main..topic4 &&\n> +\n> +       # Verify ref was updated\n> +       git rev-parse topic4 >topic4-new &&\n> +       ! test_cmp topic4-old topic4-new &&\n> +\n> +       # Verify no partial state\n> +       git log --format=%s topic4 >actual &&\n> +       test_write_lines J I M L B A >expect &&\n> +       test_cmp expect actual\n> +'\n\nThis test doesn't test what it says it does.  It merely tests that the\nsingle topic was modified, and as such, isn't a very useful additional\ntest.  Using the contained flag where one of the branches had a lock\nin the way or a simultaneous push and then testing that none of the\nbranches got updated would be needed if you want to test this.\n\n> +test_expect_success 'replay works correctly with bare repositories' '\n> +       # Test atomic behavior in bare repo (important for Gitaly)\n> +       git checkout -b bare-test topic1 &&\n> +       test_commit BareTest &&\n> +\n> +       # Test with bare repo - replay the commits from main..bare-test to get the full history\n> +       git -C bare fetch .. bare-test:bare-test &&\n> +       git -C bare replay --onto main main..bare-test &&\n> +\n> +       # Verify the bare repo was updated correctly (no output)\n> +       git -C bare log --format=%s bare-test >actual &&\n> +       test_write_lines BareTest F C M L B A >expect &&\n> +       test_cmp expect actual\n> +'\n\nThis doesn't test what the initial comment says is the important bit;\nin fact, since only a single ref is being updated there's no chance to\ntest atomicity of updating multiple refs. Or is the parenthetical\nambiguous and I should have read that as bare repositories being\nimportant?  If the latter, this test is mere redundancy; there were\nmultiple tests in a bare repository previously.\n\n> +test_expect_success 'replay --allow-partial with no failures produces no output' '\n> +       git checkout -b partial-test topic1 &&\n> +       test_commit PartialTest &&\n> +\n> +       # Should succeed silently even with partial mode\n> +       git replay --allow-partial --onto main topic1..partial-test >output &&\n> +       test_must_be_empty output\n> +'\n\nTest is fine, _if_ --allow-partial is worthwhile to add.\n\n> +test_expect_success 'replay maintains ref update consistency' '\n> +       # Test that traditional vs atomic produce equivalent results\n\nI think the comment is a better test name than the actual test name;\nI'd remove the existing testname, and move the comment to the test\nname, and then not have a comment here.\n\n> +       git checkout -b method1-test topic2 &&\n> +       git checkout -b method2-test topic2 &&\n> +\n> +       # Both methods should update refs to point to the same replayed commits\n> +       git replay --output-commands --onto main topic1..method1-test >update-commands &&\n> +       git update-ref --stdin <update-commands &&\n> +       git log --format=%s method1-test >traditional-result &&\n> +\n> +       # Direct atomic method should produce same commit history\n> +       git replay --onto main topic1..method2-test &&\n> +       git log --format=%s method2-test >atomic-result &&\n> +\n> +       # Both methods should produce identical commit histories\n> +       test_cmp traditional-result atomic-result\n> +'\n> +\n> +test_expect_success 'replay error messages are helpful and clear' '\n> +       # Test that error messages are clear\n> +       test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&\n> +       grep \"cannot be used together\" error\n> +'\n\nDoes this test that \"error messages are helpful and clear\" or just\nthat \"--output-commands and --allow-partial\" are incompatible?  The\nformer would be a test of _all_ error messages from git replay, and\nwould need to look at more than just a small substring of the error\nmessage(s).\n\n> +test_expect_success 'replay with empty range produces no output and no changes' '\n> +       # Create a test branch for empty range testing\n\nWhy?  Since you use an A..A range below, you can just use any existing\nbranch in that place, right?\n\n> +       git checkout -b empty-test topic1 &&\n> +       git rev-parse empty-test >empty-test-before &&\n> +\n> +       # Empty range should succeed but do nothing\n> +       git replay --onto main empty-test..empty-test >output &&\n> +       test_must_be_empty output &&\n\nIs A..A the only consideration?  Why would anyone ever pass that to\nthe tool?  I would have thought that if someone made this mistake it\nwas a B..A range where it wasn't obvious to the caller that B\ncontained A.  I don't see why anyone would do A..A and then get\nsurprised at the exit status; that feels like user error.  And in the\nB..A case, it's not clear to me that returning success without\nupdating A is correct.  You might be giving the user false hope that\nthe command did what they wanted.\n\n> +       # Branch should be unchanged\n> +       git rev-parse empty-test >empty-test-after &&\n> +       test_cmp empty-test-before empty-test-after\n> +'\n> +\n>  test_done\n> --\n> 2.51.0\n"},{"id":"527814","messageId":"f0abdc27-6850-4b9d-b4eb-a1c92f731142@app.fastmail.com","threadId":"64109","inReplyTo":"20250926230838.35870-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2025-10-02T17:14:44Z","receivedAt":"2025-10-02T17:15:08Z","isPatch":true,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"On Sat, Sep 27, 2025, at 01:08, Siddharth Asthana wrote:\n> This is v2 of the git-replay atomic updates series.\n>\n> Based on the extensive community feedback from v1, I've completely redesigned\n> the approach. Instead of adding new --update-refs options, this version makes\n> atomic ref updates the default behavior of git replay.\n>\n> Why this change makes sense:\n> - git replay is explicitly marked as EXPERIMENTAL with behavior changes\n> expected\n> - The command is primarily used server-side where atomic transactions\n> are crucial\n> - Current pipeline approach (git replay | git update-ref --stdin)\n> creates\n>   coordination complexity and lacks atomic guarantees by default\n> - Patrick Steinhardt noted performance issues with individual ref\n> updates\n>   in reftable backend\n> - Elijah Newren and Junio Hamano endorsed making the better behavior\n> default\n>\n>[snip]\n\nOn the topic of changing experimental commands: I really like the\ngit-for-each-ref(1) (git-FER) output format design.  It just outputs refs and\nrelated data.  It’s not a command for “bulk delete refs” or “check for\nmerge conflicts between these refs and upstream (git-merge-tree(1)”—it\njust supports all of that through `--format` and its atoms.\n\nAnd for this command it seems to, at the core, output a mapping from old\nto new commits.\n\nNow, I’ve thought that a “client-side”[1] in-memory rebase-like command\nwould need to support outputting data for the `post-rewrite` hook.  And\nis that not straightforward if you can use `--format` with `from` and\n`to` atoms?  (I ask because I have never called hooks with git-hook(1).)\n\nI just think that (naively maybe) a `--format` command like git-FER with\nall the quoting modes might be a good fit for this command.  Then you\ncan compose all the steps you need yourself:\n\n1. Call the exact git-update-ref(1) `--batch`/`--stdin` or whatever mode\n   you need\n2. Write a message to each reflog if you want\n3. Call the `post-rewrite` hook\n\n† 1: c.f. server-side which I get the impression only wants to do cheap\n     rebases\n\n-- \nKristoffer\n"},{"id":"527820","messageId":"xmqq7bxdw44y.fsf@gitster.g","threadId":"64109","inReplyTo":"CABPp-BEh7VEM6UQjkK3CxJcv54vEmueTmh9+-SyTKUxgy7Mkcg@mail.gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-02T18:27:25Z","receivedAt":"2025-10-02T18:27:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>   * it provided a natural low-level tool for the suite of hash-object,\n> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users\n> to have another building block for experimentation and making new\n> tools.\n>\n> I was particularly focused on the last of those items for the intial\n> version at the time, but it should be noted that all three of these\n> are somewhat special cases, and the most common user desire is going\n> to be replaying commits and updating the references at the end.\n\nYes.  We could even tweak the stream in the middle with \"sed\" or\n\"grep\", I presume? ;-)\n\n> Sure, but it's quite trivial to add, right -- as shown above with the\n> extra \"start\", \"prepare\", \"commit\" directives?\n\nVery true.\n\nCompletely a tangent but, isn't requiring \"prepare\" at this layer,\nand possibly in the form of ref_transaction_prepare() at the C\nlayer, not so ergonomic API design?  Once you \"start\" a transaction\nand threw a bunch of instruction, \"commit\" can notice that you are\nin a transaction and should do whatever necessary (including\nwhatever \"prepare\" does).  I am not advocating to simplify the API\nby making end-user/program facing \"prepare\" a no-op, but just\nwondering why we decided to have \"prepare\" a so prominent API\nelement.\n\n> ...\n> Might I suggest a rewrite of the text of the commit message to this point?\n\nI do think it makes more sense to the reader to know the reasoning\nbehind the _current_ design, and what its strengths are.\n\n> =====\n> The git replay command currently outputs update commands that can be\n> piped to update-ref to achieve a rebase, e.g.\n>\n>   git replay --onto main topic1..topic2 | git update-ref --stdin\n>\n> This separation had advantages for three special cases:\n>   * it made testing easy (when state isn't modified from one step to\n> the next, you don't need to make temporary branches or have undo\n> commands, or try to track the changes)\n>   * it provided a natural can-it-rebase-cleanly (and what would it\n> rebase to) capability without automatically updating refs, I guess\n> kind of like a --dry-run\n>   * it provided a natural low-level tool for the suite of hash-object,\n> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users\n> to have another building block for experimentation and making new\n> tools.\n>\n> However, it should be noted that all three of these are somewhat\n> special cases; users, whether on the client or server side, would\n> almost certainly find it more ergonomical to simply have the updating\n> of refs be the default.  Change the default behavior to update refs\n> directly, and atomically (at least to the extent supported by the refs\n> backend in use).\n> ====\n\nThis reads very well.\n\n> Why is --allow-partial helpful?  You discussed at length why you\n> wanted atomic transactions, but you introduce this option with no\n> rationale and instead just discuss that you implemented it and some\n> design choices once you presuppose that someone wants to use it.\n>\n> Is there a usecase?  I asked for it last time, and suggested\n> discarding the modes without one, but you only discarded one of the\n> extras while leaving this one in.  I'd recommend discarding this one\n> too and just having the two modes -- the output commands that get fed\n> to update-ref, or the automatic transactional update of all or no\n> refs.\n\nI know there are people who like \"best effort\", but I too want to\nlearn a concrete use case where the \"best effort\" mode, which\nupdates only 3 refs among 30 that were to be updated, would give us\na better result than \"all or none\" transaction that fails.\n"},{"id":"527838","messageId":"4a5eaefb-79cd-4b7b-ab3a-cbab648280f6@gmail.com","threadId":"64109","inReplyTo":"CAP8UFD0POvYDgGtEx8GBhvKkd8XzzWQsy8XxAKL9M3+uz3ka+w@mail.gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-02T22:16:25Z","receivedAt":"2025-10-02T22:16:32Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 30/09/25 13:53, Christian Couder wrote:\n> On Sat, Sep 27, 2025 at 1:09 AM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> The git replay command currently outputs update commands that must be\n>> piped to git update-ref --stdin to actually update references:\n>>\n>>      git replay --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> This design has significant limitations for server-side operations. The\n>> two-command pipeline creates coordination complexity, provides no atomic\n>> transaction guarantees by default, and complicates automation in bare\n>> repository environments where git replay is primarily used.\n> Yeah, right.\n>\n>> During extensive mailing list discussion, multiple maintainers identified\n>> that the current approach\n> When you say \"current approach\" we first think we are talking about\n> the behavior you described above when you said \"The git replay command\n> currently ...\"\n>\n>> forces users to opt-in to atomic behavior rather\n>> than defaulting to the safer, more reliable option.\n> But here you are actually talking about what the previous version of\n> this patch did.\n>\n>> Elijah Newren noted\n>> that the experimental status explicitly allows such behavior changes, while\n>> Patrick Steinhardt highlighted performance concerns with individual ref\n>> updates in the reftable backend.\n> Also the commit message is not the right place to describe what\n> happened during discussions of the previous version(s) of a patch.\n> It's not the right place to talk about previous version(s) of a patch\n> in general. Those things should go into the cover letter.\n>\n> If you want to talk about an option that was considered but rejected,\n> you can say something like the following instead of the whole\n> paragraph:\n>\n> \"To address this limitation, adding an option named for example\n> `--atomic-update` was considered. With such an option `git replay\n> --atomic-update --onto main topic1..topic2` would atomically update\n> all the refs without having to use a separate `git update-ref --stdin`\n> command. The issue is that this would force users to opt-in to the\n> atomic behavior rather than have it as the default safer, faster and\n> more reliable option.\n>\n> Fortunately the experimental status of the `git replay` command\n> explicitly allows behavior changes, so we are allowed to make the\n> command atomically update all the refs by default.\n> \"\n\n\nHi Christian,\n\nThanks for the detailed commit message review. You are absolutely right - I\nwas mixing the patch rationale with v1→v2 changelog, which belongs in the\ncover letter.\n\nYour suggested framing about considering an --atomic-update option but\nrejecting it in favor of making it default is much clearer than my\napproach. I will use that structure.\n\nFor v3:\n- Move all \"since v1\" discussion to cover letter\n- Use imperative mood (\"Let's change\" not \"This patch changes\")\n- Be explicit that --output-commands and --allow-partial are new options\n- Add full stops to the implementation details list\n- Will add Helped-by trailers for Elijah, Patrick and you ofcourse as \nsuggested.\n\nQuick question: for the C89 compliance mention, should I drop it entirely\nor briefly note \"uses 'int' instead of 'bool' for C89 compatibility\"? I\nwant to acknowledge the bool→int change but not belabor it.\n\nThanks again!\n\n\n>\n>> The core issue is that git replay was designed around command output rather\n>> than direct action. This made sense for a plumbing tool, but creates barriers\n>> for the primary use case: server-side operations that need reliable, atomic\n>> ref updates without pipeline complexity.\n> I think this paragraph should go just before the \"Fortunately the\n> experimental status of the `git replay` command explicitly ...\" that I\n> suggest above.\n>\n>> This patch changes the default behavior to update refs directly using Git's\n> s/This patch changes/Let's change/\n>\n> (See our SubmittingPatches documentation where it suggests using\n> imperative mood to describe the changes we make.)\n>\n>> ref transaction API:\n>>\n>>      git replay --onto main topic1..topic2\n>>      # No output; all refs updated atomically or none\n>>\n>> The implementation uses ref_store_transaction_begin() with atomic mode by\n>> default, ensuring all ref updates succeed or all fail as a single operation.\n>> This leverages git replay's existing server-side strengths (in-memory operation,\n>> no work tree requirement) while adding the atomic guarantees that server\n>> operations require.\n>>\n>> For users needing the traditional pipeline workflow, --output-commands\n>> preserves the original behavior:\n> I think something like:\n>\n> \"For users needing the traditional pipeline workflow, let's add a new\n> `--output-commands`option that preserves the original behavior:\"\n>\n> is more explicit and makes it clear that it's a new option added by\n> this patch and not an existing option.\n>\n>>      git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> The --allow-partial option enables partial failure tolerance.\n> In the same way, something like:\n>\n> \"Let's also add a new `--allow-partial` option that enables partial\n> failure tolerance.\"\n>\n>> However, following\n>> maintainer feedback, it implements a \"strict success\" model: the command exits\n> I think you can remove \"following maintainer feedback\" here. The cover\n> letter or a trailer like \"Helped-by: ...\" at the end of the commit\n> message (but Junio will add his \"Signed-off-by: ...\" anyway so adding\n> an Helped-by: ... about him is redundant) are the right place to\n> mention people who helped or suggested changes.\n>\n>> with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY\n>> updates fail. This ensures that --allow-partial changes error reporting style\n>> (warnings vs hard errors) but not success criteria, handling edge cases like\n>> \"no updates needed\" cleanly.\n>>\n>> Implementation details:\n>> - Empty commit ranges now return success (exit code 0) rather than failure,\n>>    as no commits to replay is a valid successful operation\n> Nit: as all the sentences in this \"Implementation details\" list start\n> with an uppercase, I think they should end with a full stop.\n>\n>> - Added comprehensive test coverage with 12 new tests covering atomic behavior,\n>>    option validation, bare repository support, and edge cases\n>> - Fixed test isolation issues to prevent branch state contamination between tests\n>> - Maintains C89 compliance and follows Git's established coding conventions\n> I am not sure this one is worth mentioning here, at least not like\n> this. You may want to say in the cover letter that compared to the\n> previous version this patch doesn't use 'bool' anymore and explain\n> why. Or maybe you want to explain here that using the 'bool' type was\n> considered but rejected for some reason. But in both cases, you should\n> be explicit about the reason.\n>\n>> - Refactored option validation to use die_for_incompatible_opt2() for both\n>>    --advance/--contained and --allow-partial/--output-commands conflicts,\n>>    providing consistent error reporting\n>> - Fixed --allow-partial exit code behavior to implement \"strict success\" model\n>>    where any ref update failures result in exit code 1, even with partial tolerance\n> This should probably go to the cover letter, as we should not talk in\n> the commit message about changes since a previous version of the\n> commit.\n>\n>> - Updated documentation with proper line wrapping, consistent terminology using\n>>    \"old default behavior\", performance context, and reorganized examples for clarity\n> This also sounds like a change compared to the previous version of the patch.\n>\n>> - Eliminates individual ref updates (refs_update_ref calls) that perform\n>>    poorly with reftable backend\n> This also sounds like a change compared to the previous version of the patch.\n>\n>> - Uses only batched ref transactions for optimal performance across all\n>>    ref backends\n> I think you can remove \"only\" in the sentence as in the\n> --output-commands case no transaction is used.\n>\n>> - Avoids naming collision with git rebase --update-refs by using distinct\n>>    option names\n> This also sounds like a change compared to the previous version of the patch.\n>\n>> - Defaults to atomic behavior while preserving pipeline compatibility\n> This has been discussed above. It doesn't look like an implementation\n> detail to me.\n>\n>> The result is a command that works better for its primary use case (server-side\n>> operations) while maintaining full backward compatibility for existing workflows.\n>>\n>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> Adding \"Helped-by: ...\" trailers for at least Elijah and Patrick would be nice.\n"},{"id":"527839","messageId":"9d310bd5-453f-43a4-b477-ba02baa7a664@gmail.com","threadId":"64109","inReplyTo":"9052eccc-1121-442f-ad51-4fe9217024a0@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-02T22:20:01Z","receivedAt":"2025-10-02T22:20:09Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 30/09/25 15:35, Phillip Wood wrote:\n> Hi Siddharth\n>\n> On 27/09/2025 00:08, Siddharth Asthana wrote:\n>> The git replay command currently outputs update commands that must be\n>> piped to git update-ref --stdin to actually update references:\n>>\n>>      git replay --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> This design has significant limitations for server-side operations. The\n>> two-command pipeline creates coordination complexity, provides no atomic\n>> transaction guarantees by default\n>\n> Are you sure that's true? Maybe I'm missing something but my reading \n> of builtin/update-ref.c is that it when \"--stdin\" is given it starts a \n> ref transaction, reads the commands from stdin and applies them to \n> that transaction and then commits the transaction which will make the \n> updates atomic.\n\n\nYou are absolutely right, and Karthik confirmed this as well. That was a\nsignificant error in my commit message. git update-ref --stdin IS atomic\nby default.\n\n\nThe actual advantages of the new default aren't about atomicity (that\nalready exists), but rather:\n- Eliminating the pipeline for the common case\n- Better ergonomics for users who just want refs updated\n- Simpler server-side automation\n\nI will rewrite the commit message to accurately reflect this. Elijah\nprovided a good suggested structure that captures the real trade-offs\nwithout false claims.\n\n\n>\n>> , and complicates automation in bare\n>> repository environments where git replay is primarily used.\n>\n> How does it complicate automation in bare repositories?\n>\n> Christian has given detailed feedback on the rest of the commit \n> message so I'll not comment on it further.\n>\n>> diff --git a/Documentation/git-replay.adoc \n>> b/Documentation/git-replay.adoc\n>> index 0b12bf8aa4..e104e0bc03 100644\n>> --- a/Documentation/git-replay.adoc\n>> +++ b/Documentation/git-replay.adoc\n>> @@ -9,16 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new \n>> base, works with bare repos t\n>>   SYNOPSIS\n>>   --------\n>>   [verse]\n>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | \n>> --advance <branch>) <revision-range>...\n>> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | \n>> --advance <branch>) [--output-commands | --allow-partial] \n>> <revision-range>...\n>\n> Please wrap this very long line\n>\n>> @@ -42,6 +42,20 @@ When `--advance` is specified, the update-ref \n>> command(s) in the output\n>>   will update the branch passed as an argument to `--advance` to \n>> point at\n>>   the new commits (in other words, this mimics a cherry-pick operation).\n>>   +--output-commands::\n>> +    Output update-ref commands instead of updating refs directly.\n>> +    When this option is used, the output can be piped to `git \n>> update-ref --stdin`\n>> +    for successive, relatively slow, ref updates. This is equivalent \n>> to the\n>> +    old default behavior.\n>> +\n>> +--allow-partial::\n>> +    Allow some ref updates to succeed even if others fail. By default,\n>> +    ref updates are atomic (all succeed or all fail). With this option,\n>> +    failed updates are reported as warnings rather than causing the \n>> entire\n>> +    command to fail. The command exits with code 0 only if all updates\n>> +    succeed; any failures result in exit code 1. Cannot be used with\n>> +    `--output-commands`.\n>\n> Rather than having two incompatible options perhaps we could have a \n> single \"--update-refs=(yes|print|allow-partial-updates)\" argument. I \n> think the name \"--allow-partial\" is rather ambiguous as it does not \n> say what it is allowing to be partial.\n\n\nAfter thinking about this and Elijah's feedback, I am leaning toward\ndropping --allow-partial entirely since I don't have a concrete use case\nfor it. That simplifies things to just: default atomic updates vs\n--output-commands for the traditional pipeline.\n\nWould you still prefer a --update-refs=<mode> style, or is the simpler\n--output-commands flag sufficient given that --allow-partial is going away?\n\n\n>\n>> +static int add_ref_to_transaction(struct ref_transaction *transaction,\n>> +                  const char *refname,\n>> +                  const struct object_id *new_oid,\n>> +                  const struct object_id *old_oid,\n>> +                  struct strbuf *err)\n>> +{\n>> +    return ref_transaction_update(transaction, refname, new_oid, \n>> old_oid,\n>> +                      NULL, NULL, 0, \"git replay\", err);\n>> +}\n>\n> I'm not sure this function adds much value. I think it would be better \n> to instead have a helper function that updates refs or prints the ref \n> updates so that we do not duplicate that code in the two places below.\n\n\nood point. I will extract a helper like:\n\n     static int handle_ref_update(int output_commands,\n                                  struct ref_transaction *transaction,\n                                  const char *refname,\n                                  const struct object_id *new_oid,\n                                  const struct object_id *old_oid,\n                                  struct strbuf *err)\n\nThis eliminates the duplication and fixes the over-long lines you pointed\nout at both call sites.\n\nThanks!\n\n\n>\n>> @@ -434,10 +481,18 @@ int cmd_replay(int argc,\n>>               if (decoration->type == DECORATION_REF_LOCAL &&\n>>                   (contained || strset_contains(update_refs,\n>>                                 decoration->name))) {\n>> -                printf(\"update %s %s %s\\n\",\n>> -                       decoration->name,\n>> - oid_to_hex(&last_commit->object.oid),\n>> -                       oid_to_hex(&commit->object.oid));\n>> +                if (output_commands) {\n>> +                    printf(\"update %s %s %s\\n\",\n>> +                           decoration->name,\n>> + oid_to_hex(&last_commit->object.oid),\n>> + oid_to_hex(&commit->object.oid));\n>> +                } else if (add_ref_to_transaction(transaction, \n>> decoration->name,\n>> + &last_commit->object.oid,\n>> +                                  &commit->object.oid,\n>> +                                  &transaction_err) < 0) {\n>> +                    ret = error(_(\"failed to add ref update to \n>> transaction: %s\"), transaction_err.buf);\n>> +                    goto cleanup;\n>> +                }\n>>               }\n>\n> The lines here are very long due to the indentation, having a separate \n> function to update the refs or print the ref updates would be much \n> more readable.\n>\n>>               decoration = decoration->next;\n>>           }\n>> @@ -445,10 +500,33 @@ int cmd_replay(int argc,\n>>         /* In --advance mode, advance the target ref */\n>>       if (result.clean == 1 && advance_name) {\n>> -        printf(\"update %s %s %s\\n\",\n>> -               advance_name,\n>> -               oid_to_hex(&last_commit->object.oid),\n>> -               oid_to_hex(&onto->object.oid));\n>> +        if (output_commands) {\n>> +            printf(\"update %s %s %s\\n\",\n>> +                   advance_name,\n>> +                   oid_to_hex(&last_commit->object.oid),\n>> +                   oid_to_hex(&onto->object.oid));\n>> +        } else if (add_ref_to_transaction(transaction, advance_name,\n>> +                          &last_commit->object.oid,\n>> +                          &onto->object.oid,\n>> +                          &transaction_err) < 0) {\n>> +            ret = error(_(\"failed to add ref update to transaction: \n>> %s\"), transaction_err.buf);\n>> +            goto cleanup;\n>> +        }\n>> +    }\n>\n> Putting the code to update the refs or print the ref updates into a \n> single function would avoid this duplication and over-long lines.\n>\n> Thanks\n>\n> Phillip\n>\n>> +    /* Commit the ref transaction if we have one */\n>> +    if (transaction && result.clean == 1) {\n>> +        if (ref_transaction_commit(transaction, &transaction_err)) {\n>> +            if (allow_partial) {\n>> +                warning(_(\"some ref updates failed: %s\"), \n>> transaction_err.buf);\n>> + ref_transaction_for_each_rejected_update(transaction,\n>> +                                     print_rejected_update, NULL);\n>> +                ret = 0; /* Set failure even with allow_partial */\n>> +            } else {\n>> +                ret = error(_(\"failed to update refs: %s\"), \n>> transaction_err.buf);\n>> +                goto cleanup;\n>> +            }\n>> +        }\n>>       }\n>>         merge_finalize(&merge_opt, &result);\n>> @@ -457,9 +535,17 @@ int cmd_replay(int argc,\n>>           strset_clear(update_refs);\n>>           free(update_refs);\n>>       }\n>> -    ret = result.clean;\n>> +\n>> +    /* Handle empty ranges: if no commits were processed, treat as \n>> success */\n>> +    if (!commits_processed)\n>> +        ret = 1; /* Success - no commits to replay is not an error */\n>> +    else\n>> +        ret = result.clean;\n>>     cleanup:\n>> +    if (transaction)\n>> +        ref_transaction_free(transaction);\n>> +    strbuf_release(&transaction_err);\n>>       release_revisions(&revs);\n>>       free(advance_name);\n>>   diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>> index 58b3759935..8b4301e227 100755\n>> --- a/t/t3650-replay-basics.sh\n>> +++ b/t/t3650-replay-basics.sh\n>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n>>   '\n>>     test_expect_success 'using replay to rebase two branches, one on \n>> top of other' '\n>> -    git replay --onto main topic1..topic2 >result &&\n>> +    git replay --output-commands --onto main topic1..topic2 >result &&\n>>         test_line_count = 1 result &&\n>>   @@ -67,9 +67,30 @@ test_expect_success 'using replay to rebase two \n>> branches, one on top of other' '\n>>       test_cmp expect result\n>>   '\n>>   +test_expect_success 'using replay with default atomic behavior (no \n>> output)' '\n>> +    # Create a test branch that wont interfere with others\n>> +    git branch atomic-test topic2 &&\n>> +    git rev-parse atomic-test >atomic-test-old &&\n>> +\n>> +    # Default behavior: atomic ref updates (no output)\n>> +    git replay --onto main topic1..atomic-test >output &&\n>> +    test_must_be_empty output &&\n>> +\n>> +    # Verify the branch was updated\n>> +    git rev-parse atomic-test >atomic-test-new &&\n>> +    ! test_cmp atomic-test-old atomic-test-new &&\n>> +\n>> +    # Verify the history is correct\n>> +    git log --format=%s atomic-test >actual &&\n>> +    test_write_lines E D M L B A >expect &&\n>> +    test_cmp expect actual\n>> +'\n>> +\n>>   test_expect_success 'using replay on bare repo to rebase two \n>> branches, one on top of other' '\n>> -    git -C bare replay --onto main topic1..topic2 >result-bare &&\n>> -    test_cmp expect result-bare\n>> +    git -C bare replay --output-commands --onto main topic1..topic2 \n>> >result-bare &&\n>> +\n>> +    # The result should match what we got from the regular repo\n>> +    test_cmp result result-bare\n>>   '\n>>     test_expect_success 'using replay to rebase with a conflict' '\n>> @@ -86,7 +107,7 @@ test_expect_success 'using replay to perform basic \n>> cherry-pick' '\n>>       # 2nd field of result is refs/heads/main vs. refs/heads/topic2\n>>       # 4th field of result is hash for main instead of hash for topic2\n>>   -    git replay --advance main topic1..topic2 >result &&\n>> +    git replay --output-commands --advance main topic1..topic2 \n>> >result &&\n>>         test_line_count = 1 result &&\n>>   @@ -102,7 +123,7 @@ test_expect_success 'using replay to perform \n>> basic cherry-pick' '\n>>   '\n>>     test_expect_success 'using replay on bare repo to perform basic \n>> cherry-pick' '\n>> -    git -C bare replay --advance main topic1..topic2 >result-bare &&\n>> +    git -C bare replay --output-commands --advance main \n>> topic1..topic2 >result-bare &&\n>>       test_cmp expect result-bare\n>>   '\n>>   @@ -115,7 +136,7 @@ test_expect_success 'replay fails when both \n>> --advance and --onto are omitted' '\n>>   '\n>>     test_expect_success 'using replay to also rebase a contained \n>> branch' '\n>> -    git replay --contained --onto main main..topic3 >result &&\n>> +    git replay --output-commands --contained --onto main \n>> main..topic3 >result &&\n>>         test_line_count = 2 result &&\n>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>> @@ -139,12 +160,12 @@ test_expect_success 'using replay to also \n>> rebase a contained branch' '\n>>   '\n>>     test_expect_success 'using replay on bare repo to also rebase a \n>> contained branch' '\n>> -    git -C bare replay --contained --onto main main..topic3 \n>> >result-bare &&\n>> +    git -C bare replay --output-commands --contained --onto main \n>> main..topic3 >result-bare &&\n>>       test_cmp expect result-bare\n>>   '\n>>     test_expect_success 'using replay to rebase multiple divergent \n>> branches' '\n>> -    git replay --onto main ^topic1 topic2 topic4 >result &&\n>> +    git replay --output-commands --onto main ^topic1 topic2 topic4 \n>> >result &&\n>>         test_line_count = 2 result &&\n>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>> @@ -168,7 +189,7 @@ test_expect_success 'using replay to rebase \n>> multiple divergent branches' '\n>>   '\n>>     test_expect_success 'using replay on bare repo to rebase multiple \n>> divergent branches, including contained ones' '\n>> -    git -C bare replay --contained --onto main ^main topic2 topic3 \n>> topic4 >result &&\n>> +    git -C bare replay --output-commands --contained --onto main \n>> ^main topic2 topic3 topic4 >result &&\n>>         test_line_count = 4 result &&\n>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>> @@ -217,4 +238,131 @@ test_expect_success \n>> 'merge.directoryRenames=false' '\n>>           --onto rename-onto rename-onto..rename-from\n>>   '\n>>   +# Tests for new default atomic behavior and options> > \n>> +test_expect_success 'replay default behavior should not produce \n> output when successful' '\n>> +    git replay --onto main topic1..topic3 >output &&\n>> +    test_must_be_empty output\n>> +'\n>> +\n>> +test_expect_success 'replay with --output-commands produces \n>> traditional output' '\n>> +    git replay --output-commands --onto main topic1..topic3 >output &&\n>> +    test_line_count = 1 output &&\n>> +    grep \"^update refs/heads/topic3 \" output\n>> +'\n>> +\n>> +test_expect_success 'replay with --allow-partial should not produce \n>> output when successful' '\n>> +    git replay --allow-partial --onto main topic1..topic3 >output &&\n>> +    test_must_be_empty output\n>> +'\n>> +\n>> +test_expect_success 'replay fails when --output-commands and \n>> --allow-partial are used together' '\n>> +    test_must_fail git replay --output-commands --allow-partial \n>> --onto main topic1..topic2 2>error &&\n>> +    grep \"cannot be used together\" error\n>> +'\n>> +\n>> +test_expect_success 'replay with --contained updates multiple \n>> branches atomically' '\n>> +    # Create fresh test branches based on the original structure\n>> +    # contained-topic1 should be contained within the range to \n>> contained-topic3\n>> +    git branch contained-base main &&\n>> +    git checkout -b contained-topic1 contained-base &&\n>> +    test_commit ContainedC &&\n>> +    git checkout -b contained-topic3 contained-topic1 &&\n>> +    test_commit ContainedG &&\n>> +    test_commit ContainedH &&\n>> +    git checkout main &&\n>> +\n>> +    # Store original states\n>> +    git rev-parse contained-topic1 >contained-topic1-old &&\n>> +    git rev-parse contained-topic3 >contained-topic3-old &&\n>> +\n>> +    # Use --contained to update multiple branches - this should \n>> update both\n>> +    git replay --contained --onto main \n>> contained-base..contained-topic3 &&\n>> +\n>> +    # Verify both branches were updated\n>> +    git rev-parse contained-topic1 >contained-topic1-new &&\n>> +    git rev-parse contained-topic3 >contained-topic3-new &&\n>> +    ! test_cmp contained-topic1-old contained-topic1-new &&\n>> +    ! test_cmp contained-topic3-old contained-topic3-new\n>> +'\n>> +\n>> +test_expect_success 'replay atomic behavior: all refs updated or \n>> none' '\n>> +    # Store original state\n>> +    git rev-parse topic4 >topic4-old &&\n>> +\n>> +    # Default atomic behavior\n>> +    git replay --onto main main..topic4 &&\n>> +\n>> +    # Verify ref was updated\n>> +    git rev-parse topic4 >topic4-new &&\n>> +    ! test_cmp topic4-old topic4-new &&\n>> +\n>> +    # Verify no partial state\n>> +    git log --format=%s topic4 >actual &&\n>> +    test_write_lines J I M L B A >expect &&\n>> +    test_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'replay works correctly with bare repositories' '\n>> +    # Test atomic behavior in bare repo (important for Gitaly)\n>> +    git checkout -b bare-test topic1 &&\n>> +    test_commit BareTest &&\n>> +\n>> +    # Test with bare repo - replay the commits from main..bare-test \n>> to get the full history\n>> +    git -C bare fetch .. bare-test:bare-test &&\n>> +    git -C bare replay --onto main main..bare-test &&\n>> +\n>> +    # Verify the bare repo was updated correctly (no output)\n>> +    git -C bare log --format=%s bare-test >actual &&\n>> +    test_write_lines BareTest F C M L B A >expect &&\n>> +    test_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'replay --allow-partial with no failures \n>> produces no output' '\n>> +    git checkout -b partial-test topic1 &&\n>> +    test_commit PartialTest &&\n>> +\n>> +    # Should succeed silently even with partial mode\n>> +    git replay --allow-partial --onto main topic1..partial-test \n>> >output &&\n>> +    test_must_be_empty output\n>> +'\n>> +\n>> +test_expect_success 'replay maintains ref update consistency' '\n>> +    # Test that traditional vs atomic produce equivalent results\n>> +    git checkout -b method1-test topic2 &&\n>> +    git checkout -b method2-test topic2 &&\n>> +\n>> +    # Both methods should update refs to point to the same replayed \n>> commits\n>> +    git replay --output-commands --onto main topic1..method1-test \n>> >update-commands &&\n>> +    git update-ref --stdin <update-commands &&\n>> +    git log --format=%s method1-test >traditional-result &&\n>> +\n>> +    # Direct atomic method should produce same commit history\n>> +    git replay --onto main topic1..method2-test &&\n>> +    git log --format=%s method2-test >atomic-result &&\n>> +\n>> +    # Both methods should produce identical commit histories\n>> +    test_cmp traditional-result atomic-result\n>> +'\n>> +\n>> +test_expect_success 'replay error messages are helpful and clear' '\n>> +    # Test that error messages are clear\n>> +    test_must_fail git replay --output-commands --allow-partial \n>> --onto main topic1..topic2 2>error &&\n>> +    grep \"cannot be used together\" error\n>> +'\n>> +\n>> +test_expect_success 'replay with empty range produces no output and \n>> no changes' '\n>> +    # Create a test branch for empty range testing\n>> +    git checkout -b empty-test topic1 &&\n>> +    git rev-parse empty-test >empty-test-before &&\n>> +\n>> +    # Empty range should succeed but do nothing\n>> +    git replay --onto main empty-test..empty-test >output &&\n>> +    test_must_be_empty output &&\n>> +\n>> +    # Branch should be unchanged\n>> +    git rev-parse empty-test >empty-test-after &&\n>> +    test_cmp empty-test-before empty-test-after\n>> +'\n>> +\n>>   test_done\n>\n"},{"id":"527840","messageId":"64b63d62-482d-42b2-8090-60aac8f505d4@gmail.com","threadId":"64109","inReplyTo":"CAOLa=ZQjMzCiVd8tRXtJJ8yXxLgwGQDgOZW3F86h9jC71NJm5w@mail.gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-02T22:20:55Z","receivedAt":"2025-10-02T22:21:02Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 02/10/25 15:30, Karthik Nayak wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>\n>> Hi Siddharth\n>>\n>> On 27/09/2025 00:08, Siddharth Asthana wrote:\n>>> The git replay command currently outputs update commands that must be\n>>> piped to git update-ref --stdin to actually update references:\n>>>\n>>>       git replay --onto main topic1..topic2 | git update-ref --stdin\n>>>\n>>> This design has significant limitations for server-side operations. The\n>>> two-command pipeline creates coordination complexity, provides no atomic\n>>> transaction guarantees by default\n>> Are you sure that's true? Maybe I'm missing something but my reading of\n>> builtin/update-ref.c is that it when \"--stdin\" is given it starts a ref\n>> transaction, reads the commands from stdin and applies them to that\n>> transaction and then commits the transaction which will make the updates\n>> atomic.\n>>\n> You're right. Using '--stdin' is atomic by default. You can manually\n> handle the transaction's by passing in the 'start', 'prepare', 'commit',\n> 'abort' sub-commands in the '--stdin' mode.\n\n\nThanks for confirming this Karthik. I will correct the commit message to\naccurately represent what update-ref --stdin provides.\n\n\n>\n> [snip]\n"},{"id":"527854","messageId":"CABPp-BGcbdygEjndAjXo9utUhTac7JTHscX4iiwk4UZcHonXvg@mail.gmail.com","threadId":"64109","inReplyTo":"CAP8UFD0POvYDgGtEx8GBhvKkd8XzzWQsy8XxAKL9M3+uz3ka+w@mail.gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-10-02T22:55:22Z","receivedAt":"2025-10-02T22:55:36Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Hi Christian,\n\nExcellent review, I just have one tangential question for you...\n\nOn Tue, Sep 30, 2025 at 1:24 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> On Sat, Sep 27, 2025 at 1:09 AM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n> >\n> > The git replay command currently outputs update commands that must be\n> > piped to git update-ref --stdin to actually update references:\n> >\n> >     git replay --onto main topic1..topic2 | git update-ref --stdin\n> >\n> > This design has significant limitations for server-side operations. The\n> > two-command pipeline creates coordination complexity, provides no atomic\n> > transaction guarantees by default, and complicates automation in bare\n> > repository environments where git replay is primarily used.\n>\n> Yeah, right.\n\nI'm unsure if you are expressing disbelief, or agreeing when you use\nthis phrase.  Most commonly when I see it, I assume the former (see\nhttps://dictionary.cambridge.org/us/dictionary/english/yeah-right and\nhttps://www.merriam-webster.com/dictionary/yeah for example), but I\nthink you've consistently used this with the opposite connotation.  Am\nI correct on that?  (This is a particular phrase where tone of voice\nused would be really helpful, which doesn't get included in emails\nunfortunately.)\n"},{"id":"527856","messageId":"0fba2f5e-03cd-439b-90bd-f613fcc4ae23@gmail.com","threadId":"64109","inReplyTo":"CABPp-BEh7VEM6UQjkK3CxJcv54vEmueTmh9+-SyTKUxgy7Mkcg@mail.gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-02T23:27:11Z","receivedAt":"2025-10-02T23:27:19Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 02/10/25 22:02, Elijah Newren wrote:\n> Hi,\n>\n> First of all, thanks for continuing to work on this.\n>\n> On Fri, Sep 26, 2025 at 4:09 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> The git replay command currently outputs update commands that must be\n> can be, not must be.\n>\n> This separation had three advantages:\n>    * it made testing easy (when state isn't modified from one step to\n> the next, you don't need to make temporary branches or have undo\n> commands, or try to track the changes)\n>    * it provided a natural can-it-rebase-cleanly (and what would it\n> rebase to) capability without automatically updating refs, I guess\n> kind of like a --dry-run\n>    * it provided a natural low-level tool for the suite of hash-object,\n> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users\n> to have another building block for experimentation and making new\n> tools.\n>\n> I was particularly focused on the last of those items for the intial\n> version at the time, but it should be noted that all three of these\n> are somewhat special cases, and the most common user desire is going\n> to be replaying commits and updating the references at the end.\n\n\nYou are absolutely right - I mischaracterized both the \"must be\" claim and\nthe atomicity guarantee. Phillip and Karthik confirmed that update-ref\n--stdin IS atomic by default, so my justification was fundamentally wrong.\n\nYour suggested commit message structure is much better. It accurately\ncaptures the real trade-offs (testing, dry-run capability, building\nblock) without making false claims, and explains why making ref updates\ndefault serves the common case better.\n\nI will use your suggested rewrite verbatim for v3.\n\n\n>> piped to git update-ref --stdin to actually update references:\n>>\n>>      git replay --onto main topic1..topic2 | git update-ref --stdin\n> or, alternatively, for those that want a single, atomic (assuming the\n> backend supports it) transaction:\n>\n>     (echo start; git replay --onto main topic1..topic2; echo prepare;\n> echo commit) | git update-ref --stdin\n>\n>> This design has significant limitations for server-side operations. The\n>> two-command pipeline creates coordination complexity,\n> agreed\n>\n>> provides no atomic\n>> transaction guarantees by default,\n> Sure, but it's quite trivial to add, right -- as shown above with the\n> extra \"start\", \"prepare\", \"commit\" directives?\n\n\nYou are absolutely right. I overstated the difficulty - atomicity is\ntrivial to achieve with those directives. The real benefit is eliminating\nthe pipeline entirely for the common case, not providing atomicity that\nwasn't already available.\n\n\n>> and complicates automation in bare\n>> repository environments where git replay is primarily used.\n> Isn't this repeating the first statement from this paragraph?\n>\n> The paragraph also leaves off that I think it makes it more\n> useful/usable to end-users.\n\n\nYou are right - I was repeating the coordination complexity point. And I\nmissed the broader benefit to end users (not just server-side). I will use\nyour suggested rewrite which captures this properly.\n\n\n>\n>> The core issue is that git replay was designed around command output rather\n>> than direct action. This made sense for a plumbing tool, but creates barriers\n>> for the primary use case: server-side operations that need reliable, atomic\n>> ref updates without pipeline complexity.\n> git replay was originally written with the goal of eventually\n> providing a better interactive rebase.  I thought it important to have\n> the earlier versions provide new useful low-level building blocks, but\n> didn't intend for it to just be a low-level building block\n> indefinitely.  The primary use case originally envisioned was\n> client-side user interactive operations without much thought for the\n> server, but it turned out to be easiest to implement what server-side\n> operations needed first.  I made others aware of what I was working on\n> so they could see, and Christian latched on and to my surprise told me\n> it had enough for what he needed...and Johannes said about the same.\n> Unfortunately, I got reassigned away from Git at my former dayjob,\n> _and_ had multiple big long-term family issues strike about the same\n> time, _and_ all Git-related employers did mass layoffs and locked down\n> hiring about that time as well...so the end result is only that\n> initial server-side stuff was done and it looks to outside observers\n> like git replay's design and usecase was around the server.  While\n> that's an understandable guess from the outside, this paragraph\n> appears to have taken those guesses and rewrites them as fact.  I\n> think it could be reworded with small tweaks to alter it and make it\n> factual (s/designed/focused initially/, s/the primary use case/its\n> current primary use case/), but even then, the paragraph that remains\n> feels like it's just repeating what you said above.  It doesn't feel\n> like it adds any positive value.\n>\n> Might I suggest a rewrite of the text of the commit message to this point?\n>\n> =====\n> The git replay command currently outputs update commands that can be\n> piped to update-ref to achieve a rebase, e.g.\n>\n>    git replay --onto main topic1..topic2 | git update-ref --stdin\n>\n> This separation had advantages for three special cases:\n>    * it made testing easy (when state isn't modified from one step to\n> the next, you don't need to make temporary branches or have undo\n> commands, or try to track the changes)\n>    * it provided a natural can-it-rebase-cleanly (and what would it\n> rebase to) capability without automatically updating refs, I guess\n> kind of like a --dry-run\n>    * it provided a natural low-level tool for the suite of hash-object,\n> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users\n> to have another building block for experimentation and making new\n> tools.\n>\n> However, it should be noted that all three of these are somewhat\n> special cases; users, whether on the client or server side, would\n> almost certainly find it more ergonomical to simply have the updating\n> of refs be the default.  Change the default behavior to update refs\n> directly, and atomically (at least to the extent supported by the refs\n> backend in use).\n> ====\n>\n> after this, I'd suggest also nuking your next few paragraphs and then\n> continuing with:\n>\n>> For users needing the traditional pipeline workflow, --output-commands\n>> preserves the original behavior:\n>>\n>>      git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n> This is good.  Did you also add a config option so that someone can\n> just set that option once and use the old behavior?  (as per the\n> suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/\n> ?)\n\n\nI didn't, but I should have. I will add a config option for v3.\n\nFor naming, I am thinking either:\n   - replay.updateRefs (boolean: true = update, false = output-commands)\n   - replay.defaultOutput (string: \"update\" | \"commands\")\n\nThe boolean feels simpler, but the string might be more extensible if we\nadd other output modes later. Which pattern feels more consistent with\nexisting Git config conventions? Looking at rebase.* they're mostly\nboolean toggles, but am I missing a better example to follow?\n\n\n>> The --allow-partial option enables partial failure tolerance. However, following\n>> maintainer feedback, it implements a \"strict success\" model: the command exits\n>> with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY\n>> updates fail. This ensures that --allow-partial changes error reporting style\n>> (warnings vs hard errors) but not success criteria, handling edge cases like\n>> \"no updates needed\" cleanly.\n> Why is --allow-partial helpful?  You discussed at length why you\n> wanted atomic transactions, but you introduce this option with no\n> rationale and instead just discuss that you implemented it and some\n> design choices once you presuppose that someone wants to use it.\n>\n> Is there a usecase?  I asked for it last time, and suggested\n> discarding the modes without one, but you only discarded one of the\n> extras while leaving this one in.  I'd recommend discarding this one\n> too and just having the two modes -- the output commands that get fed\n> to update-ref, or the automatic transactional update of all or no\n> refs.\n\n\nYou are right - I don't have a concrete use case. I was trying to\nanticipate potential needs but ended up adding unjustified complexity.\n\nI will remove --allow-partial entirely from v3. This simplifies to exactly\ntwo modes with clear purposes:\n   1. Default: atomic ref updates (all-or-nothing)\n   2. --output-commands: traditional pipeline for special cases\n\nMuch cleaner design.\n\n\n>\n>> Implementation details:\n> Christian's comments on this list are good; it seems to mostly be\n> stuff that belongs in the cover letter.  But I'll comment/query about\n> two things...\n>\n>> - Empty commit ranges now return success (exit code 0) rather than failure,\n>>    as no commits to replay is a valid successful operation\n> That is a useful thing to call out.  Hmm....\n>\n>> - Defaults to atomic behavior while preserving pipeline compatibility\n> The first part has already been covered at length; it doesn't seem\n> like it needs to be repeated.  What does \"while preserving pipeline\n> compatibility\" mean, though?\n\n\nThat was meant to reference --output-commands allowing the traditional\nworkflow, but you are right it's vague and redundant since that's already\nbeen discussed. I will remove it.\n\n\n>\n>\n>> The result is a command that works better for its primary use case (server-side\n>> operations)\n> This sentence feels like it's rewriting the history behind the\n> command, and undersells the change by not noting that it *also* helps\n> with client-side usage.  Also, since I think it's about the third time\n> you've said this, it adds nothing to the discussion either; let's just\n> strike it.\n>\n>> while maintaining full backward compatibility for existing workflows.\n> and this is clearly false; backwards compatibility was intentionally\n> broken by making update-refs the default.  Junio suggested making it\n> easy to get the old behavior with a config setting, but looking ahead\n> it appears you didn't even make it easy for users to recover the old\n> behavior; they have to specify an additional flag with every command\n> they run.\n>\n> Also, you further broke \"full\" backward compatibility by changing the\n> exit status for empty ranges.  I think that's a rather minor change\n> and maybe bugfix, but \"full\" invites comparisons and scrutiny of that\n> sort.\n>\n> We could say something like \"while making it easy to recover the\n> traditional behavior with a simple command line flag\", but this again\n> feels like you're just repeating what was called out above.  Maybe\n> just strike it as well?\n>\n>\n> And on a separate note, several of your lines in your commit message\n> are too long; in an 80-column terminal, a `git log` on your commit\n> message has several lines wrapping around.  (Command lines, and for\n> future reference output from commands, can run longer, but regular\n> paragraphs should fit.)\n\n\nI will wrap the commit message paragraphs to fit 80 columns\n(keeping command lines and output longer as appropriate).\n\n\n>\n>> +...Use `--output-commands`\n>> +to get the old default behavior where update commands that can be piped\n>> +to `git update-ref --stdin` are emitted (see the OUTPUT section below).\n> Perhaps instead:\n>\n> Use `--output-commands` to avoid the automatic ref updates and instead\n> get update commands that can be piped\n> to `git update-ref --stdin` (see the OUTPUT section below).\n>\n>> +--output-commands::\n>> +       Output update-ref commands instead of updating refs directly.\n>> +       When this option is used, the output can be piped to `git update-ref --stdin`\n>> +       for successive, relatively slow, ref updates. This is equivalent to the\n>> +       old default behavior.\n> \"piped\" => \"piped as-is\" + \"successive, relatively slow,\" => \"a\n> non-transactional set of\" ?\n\n\nGood wording improvements. \"Piped as-is\" clarifies no transformation is\nneeded, and \"non-transactional\" is more accurate than \"successive,\nrelatively slow\" which made performance claims without justification.\n\n\n>> +--allow-partial::\n>> +       Allow some ref updates to succeed even if others fail. By default,\n>> +       ref updates are atomic (all succeed or all fail). With this option,\n>> +       failed updates are reported as warnings rather than causing the entire\n>> +       command to fail. The command exits with code 0 only if all updates\n>> +       succeed; any failures result in exit code 1. Cannot be used with\n>> +       `--output-commands`.\n> If we keep this, I like Phillip's suggestion to combine to a single\n> flag with a value.  But I still want to hear the use case behind it;\n> it goes against what you repeatedly claimed was wanted on the server,\n> and you didn't discuss any other usecase.  It feels like you were\n> trying to proactively support every possible way people might want to\n> update without considering the usecases behind it, and I'd rather just\n> leave it unimplemented unless or until there's demand for it.\n>\n>> +\n>>   <revision-range>::\n>>          Range of commits to replay. More than one <revision-range> can\n>>          be passed, but in `--advance <branch>` mode, they should have\n>> @@ -54,15 +68,20 @@ include::rev-list-options.adoc[]\n>>   OUTPUT\n>>   ------\n>>\n>> -When there are no conflicts, the output of this command is usable as\n>> -input to `git update-ref --stdin`.  It is of the form:\n>> +By default, when there are no conflicts, this command updates the relevant\n>> +references using atomic transactions\n> atomic transaction*s*?  Shouldn't that be *an* atomic transaction instead?\n\n\nYou are right. Each invocation is one transaction with multiple updates.\nI wrote \"transactions\" thinking about multiple command invocations, but\nthat's confusing.\n\nI will change to \"an atomic transaction\" and clarify once that all ref\nupdates succeed or all fail, rather than repeating the point.\n\n\n>\n>>> and produces no output. All ref updates\n>> +succeed or all fail (atomic behavior).\n> Why the need to repeat?\n\n\nYou are right saying \"atomic transactions\" and then \"(atomic behavior)\"\nis redundant. I will state it once clearly: \"using an atomic transaction\"\nand remove the parenthetical.\n\n\n>\n>> +To rebase multiple branches with partial failure tolerance:\n>> +\n>> +------------\n>> +$ git replay --allow-partial --contained --onto origin/main origin/main..tipbranch\n>> +------------\n> I don't understand why this one deserves an example; the command line\n> flag seems to be self-explanatory.  The onto and advanced are\n> interesting in that the ranges specified with them might not be\n> obvious from their description and so examples help.  One or maybe two\n> --contained examples can go with those, to demonstrate how it involves\n> additional branches, though those might be better coupled with the\n> --output-commands because the output more clearly demonstrates what is\n> happening (i.e. what is being updated).  But I don't see what this\n> example might elucidate.\n>\n> Then again, while I perfectly understand what the flag does, I have no\n> idea why you thought it was useful to add, so maybe I'm just missing\n> something.\n>\n>>   When calling `git replay`, one does not need to specify a range of\n>>   commits to replay using the syntax `A..B`; any range expression will\n>> -do:\n>> +do. Here's an example where you explicitly specify which branches to rebase:\n>>\n>>   ------------\n>>   $ git replay --onto origin/main ^base branch1 branch2 branch3\n>> +------------\n>> +\n>> +This gives you explicit control over exactly which branches are rebased,\n>> +unlike the previous `--contained` example which automatically discovers them.\n>> +\n>> +To see the update commands that would be executed:\n>> +\n>> +------------\n>> +$ git replay --output-commands --onto origin/main ^base branch1 branch2 branch3\n>>   update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n>>   update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n>>   update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n> As noted above, I don't think we need to repeat an --output-commands\n> example for every example that exists; it feels like overkill.  One\n> (or _maybe_ two) should be sufficient.\n\n\nAgreed. With --allow-partial removed, I will keep just one clear\n--output-commands example that demonstrates the traditional pipeline\nworkflow. The multiple examples were overkill.\n\n\n>\n>> @@ -330,9 +361,12 @@ int cmd_replay(int argc,\n>>                  usage_with_options(replay_usage, replay_options);\n>>          }\n>>\n>> -       if (advance_name_opt && contained)\n>> -               die(_(\"options '%s' and '%s' cannot be used together\"),\n>> -                   \"--advance\", \"--contained\");\n>> +       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>> +                                 contained, \"--contained\");\n> Broken indentation.  Also, should this have been done as a preparatory\n> cleanup patch?\n\n\nGood catches. I will fix the indentation.\n\nOn making it a preparatory patch: should I split it out as a separate\ncleanup commit, or is it minor enough to fold into the main change? I am\nleaning toward folding it in since it's directly related to the option\nhandling changes\n\n\n\n>\n>> @@ -407,6 +452,8 @@ int cmd_replay(int argc,\n>>                  khint_t pos;\n>>                  int hr;\n>>\n>> +               commits_processed = 1;\n>> +\n>>                  if (!commit->parents)\n>>                          die(_(\"replaying down to root commit is not supported yet!\"));\n>>                  if (commit->parents->next)\n>> @@ -457,9 +535,17 @@ int cmd_replay(int argc,\n>>                  strset_clear(update_refs);\n>>                  free(update_refs);\n>>          }\n>> -       ret = result.clean;\n>> +\n>> +       /* Handle empty ranges: if no commits were processed, treat as success */\n>> +       if (!commits_processed)\n>> +               ret = 1; /* Success - no commits to replay is not an error */\n>> +       else\n>> +               ret = result.clean;\n> The change to treat empty ranges as success is an orthogonal change\n> that I think at a minimum belongs in a separate patch.  Out of\n> curiosity, how did you discover the exit status with an empty commit\n> range?  Why does someone specify such a range, and what form or forms\n> might it come in?  And is merely returning a successful result enough,\n> or is there more that needs to be done for correctness?\n\n\nI was thinking about automated scripts that compute ranges dynamically -\nthey might generate A..B where it turns out A==B, and treating that as\n\"no work needed, success\" seemed reasonable for scripting.\n\nBut you raise a good point: A..A seems like obvious user error (why would\nanyone do that intentionally?), and B..A where B contains A is likely a\nmistake that maybe should error rather than silently succeed.\n\nI am inclined to drop it entirely from this series. If there's real demand\nfor specific empty-range handling, we can add it later with proper\ndiscussion of the actual use cases. Does that sound reasonable?\n\n\n>\n>> +test_expect_success 'using replay with default atomic behavior (no output)' '\n>> +       # Create a test branch that wont interfere with others\n> This works, but I feel doing this clutters the repo for someone\n> inspecting later when some test in the testsuite fails, and makes\n> readers track more branches to find out what the tests are all doing.\n> Would it be easier to prefix your test with something like\n>\n>          START=$(git rev-parse topic2) &&\n>          test_when_finished \"git branch -f topic2 $START\" &&\n\n\nThat's much cleaner - I will rework all the new tests to use\ntest_when_finished instead of creating new branches. It keeps the test\nstate contained and makes it easier to understand what each test is\nactually verifying.\n\n\n>\n> and then...\n>\n>> +       git branch atomic-test topic2 &&\n>> +       git rev-parse atomic-test >atomic-test-old &&\n> drop these lines...\n>\n>> +\n>> +       # Default behavior: atomic ref updates (no output)\n>> +       git replay --onto main topic1..atomic-test >output &&\n> use topic2 instead of atomic-test...\n>\n>> +       test_must_be_empty output &&\n>> +\n>> +       # Verify the branch was updated\n>> +       git rev-parse atomic-test >atomic-test-new &&\n>> +       ! test_cmp atomic-test-old atomic-test-new &&\n> Not sure this comparison to verify the branch was updated makes much\n> sense given that the few lines below both test that it was updated and\n> that it was updated to the right thing.\n>\n>> +\n>> +       # Verify the history is correct\n>> +       git log --format=%s atomic-test >actual &&\n>> +       test_write_lines E D M L B A >expect &&\n>> +       test_cmp expect actual\n> ...and finally use topic2 instead of atomic-test here.\n>\n>\n> Similarly, using test_when_finished throughout the rest of the\n> testsuite similarly I think would make it a bit easier to follow and\n> later debug.\n>\n>\n>>   test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n>> -       git -C bare replay --onto main topic1..topic2 >result-bare &&\n>> -       test_cmp expect result-bare\n>> +       git -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&\n>> +\n>> +       # The result should match what we got from the regular repo\n>> +       test_cmp result result-bare\n>>   '\n> What do you mean by \"regular repo\"?  There's only one repo at play.\n>\n> Also, why change \"expect\" to \"result\" here?  You don't care if you get\n> the expected result, merely that you get the same result as a previous\n> test even if it was also buggy?\n\n\nYou are right \"regular repo\" is confusing since we are just comparing\nnon-bare vs bare on the same repository.\n\nThe \"result\" vs \"expect\" issue happened because I inserted a test between\nthe expectation-building and this test. I will reorder the tests so the\nexpectation is built immediately before this test, or just rebuild the\nexpectation here for clarity.\n\n\n> I think the reason was that you inserted a test since writing the\n> expectation out, but I think it'd be better to either re-write the\n> expectation out and compare to it, or reorder the tests so you can\n> use the same expectation from before.\n>\n>\n>> +# Tests for new default atomic behavior and options\n> The word \"new\" here is going to become unhelpful in the future when\n> someone reads this a few years from now; you should strike it.  What\n> does \"and options\" mean here?\n\n\nGood point. I will change it to just \"# Tests for default atomic behavior\"\nsince that's what the tests actually verify - the default behavior and\nthat it's atomic.\n\n\n>\n>> +\n>> +test_expect_success 'replay default behavior should not produce output when successful' '\n>> +       git replay --onto main topic1..topic3 >output &&\n>> +       test_must_be_empty output\n>> +'\n> This changes where topic3 points; I think a test_when_finished to\n> reset it back at the end of the test would be nice.\n>\n>> +test_expect_success 'replay with --output-commands produces traditional output' '\n>> +       git replay --output-commands --onto main topic1..topic3 >output &&\n>> +       test_line_count = 1 output &&\n>> +       grep \"^update refs/heads/topic3 \" output\n>> +'\n> The fact that topic3 was already replayed in the previous test makes\n> this test weaker.  And the fact that it does nothing but check that\n> there is in fact some output makes it weaker still.  But rather than\n> fix it, there were already lots of commands that tested\n> --output-commands prior to this, and you added a comment above that\n> you were adding tests of the atomic behavior, I don't see why we need\n> any additional tests of --output-commands.\n\n\nYou are right this test is redundant given existing --output-commands\ntests. I will remove it since we already have adequate coverage of that\nmode.\n\n\n>\n>> +test_expect_success 'replay with --allow-partial should not produce output when successful' '\n>> +       git replay --allow-partial --onto main topic1..topic3 >output &&\n>> +       test_must_be_empty output\n>> +\n>> +test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '\n>> +       test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&\n>> +       grep \"cannot be used together\" error\n>> +'\n> These are fine if --allow-partial can be motivated, but otherwise\n> should be removed along with that flag.\n>\n>> +\n>> +test_expect_success 'replay with --contained updates multiple branches atomically' '\n>> +       # Create fresh test branches based on the original structure\n> Unnecessary if you use the test_when_finished stuff I showed you to\n> ensure the original structure remains intact\n>\n>> +       # contained-topic1 should be contained within the range to contained-topic3\n>> +       git branch contained-base main &&\n>> +       git checkout -b contained-topic1 contained-base &&\n>> +       test_commit ContainedC &&\n>> +       git checkout -b contained-topic3 contained-topic1 &&\n>> +       test_commit ContainedG &&\n>> +       test_commit ContainedH &&\n>> +       git checkout main &&\n>> +\n>> +       # Store original states\n>> +       git rev-parse contained-topic1 >contained-topic1-old &&\n>> +       git rev-parse contained-topic3 >contained-topic3-old &&\n>> +\n>> +       # Use --contained to update multiple branches - this should update both\n>> +       git replay --contained --onto main contained-base..contained-topic3 &&\n>> +\n>> +       # Verify both branches were updated\n>> +       git rev-parse contained-topic1 >contained-topic1-new &&\n>> +       git rev-parse contained-topic3 >contained-topic3-new &&\n>> +       ! test_cmp contained-topic1-old contained-topic1-new &&\n>> +       ! test_cmp contained-topic3-old contained-topic3-new\n>> +'\n> I think verifying both branches were modified without checking\n> anything about the modification is a pretty weak test.  We should\n> check that both branches have the appropriate commit sequence in them\n> via git log output, as previous tests do.\n\n\nRight. I will add verification of the actual commit sequences using\ngit log --format=%s for both branches, not just that they changed.\n\n\n>\n>> +test_expect_success 'replay atomic behavior: all refs updated or none' '\n>> +       # Store original state\n>> +       git rev-parse topic4 >topic4-old &&\n>> +\n>> +       # Default atomic behavior\n>> +       git replay --onto main main..topic4 &&\n>> +\n>> +       # Verify ref was updated\n>> +       git rev-parse topic4 >topic4-new &&\n>> +       ! test_cmp topic4-old topic4-new &&\n>> +\n>> +       # Verify no partial state\n>> +       git log --format=%s topic4 >actual &&\n>> +       test_write_lines J I M L B A >expect &&\n>> +       test_cmp expect actual\n>> +'\n> This test doesn't test what it says it does.  It merely tests that the\n> single topic was modified, and as such, isn't a very useful additional\n> test.  Using the contained flag where one of the branches had a lock\n> in the way or a simultaneous push and then testing that none of the\n> branches got updated would be needed if you want to test this.\n\n\nYou are absolutely right - testing a single ref doesn't demonstrate\natomicity at all.\n\nFor v3, I will create a real atomic test: use --contained with multiple\nbranches, introduce a lock on one of the refs (maybe via\n.git/refs/heads/branch.lock), then verify that NONE of the branches got\nupdated when the transaction fails. That actually tests the all-or-nothing\nguarantee.\n\n\n>\n>> +test_expect_success 'replay works correctly with bare repositories' '\n>> +       # Test atomic behavior in bare repo (important for Gitaly)\n>> +       git checkout -b bare-test topic1 &&\n>> +       test_commit BareTest &&\n>> +\n>> +       # Test with bare repo - replay the commits from main..bare-test to get the full history\n>> +       git -C bare fetch .. bare-test:bare-test &&\n>> +       git -C bare replay --onto main main..bare-test &&\n>> +\n>> +       # Verify the bare repo was updated correctly (no output)\n>> +       git -C bare log --format=%s bare-test >actual &&\n>> +       test_write_lines BareTest F C M L B A >expect &&\n>> +       test_cmp expect actual\n>> +'\n> This doesn't test what the initial comment says is the important bit;\n> in fact, since only a single ref is being updated there's no chance to\n> test atomicity of updating multiple refs. Or is the parenthetical\n> ambiguous and I should have read that as bare repositories being\n> important?  If the latter, this test is mere redundancy; there were\n> multiple tests in a bare repository previously.\n\n\nThe parenthetical was ambiguous, I meant bare repositories are important\nfor Gitaly, not that this test demonstrates atomicity. Since there are\nalready multiple bare repo tests, I'll either remove this as redundant or\nreframe it to test atomicity with multiple refs in a bare repo.\n\n\n>\n>> +test_expect_success 'replay --allow-partial with no failures produces no output' '\n>> +       git checkout -b partial-test topic1 &&\n>> +       test_commit PartialTest &&\n>> +\n>> +       # Should succeed silently even with partial mode\n>> +       git replay --allow-partial --onto main topic1..partial-test >output &&\n>> +       test_must_be_empty output\n>> +'\n> Test is fine, _if_ --allow-partial is worthwhile to add.\n>\n>> +test_expect_success 'replay maintains ref update consistency' '\n>> +       # Test that traditional vs atomic produce equivalent results\n> I think the comment is a better test name than the actual test name;\n> I'd remove the existing testname, and move the comment to the test\n> name, and then not have a comment here.\n\n\nGood suggestion. I will rename to:\n\n   test_expect_success 'traditional pipeline and atomic update produce\n   equivalent results'\n\nMuch clearer what it's actually testing.\n\n\n>\n>> +       git checkout -b method1-test topic2 &&\n>> +       git checkout -b method2-test topic2 &&\n>> +\n>> +       # Both methods should update refs to point to the same replayed commits\n>> +       git replay --output-commands --onto main topic1..method1-test >update-commands &&\n>> +       git update-ref --stdin <update-commands &&\n>> +       git log --format=%s method1-test >traditional-result &&\n>> +\n>> +       # Direct atomic method should produce same commit history\n>> +       git replay --onto main topic1..method2-test &&\n>> +       git log --format=%s method2-test >atomic-result &&\n>> +\n>> +       # Both methods should produce identical commit histories\n>> +       test_cmp traditional-result atomic-result\n>> +'\n>> +\n>> +test_expect_success 'replay error messages are helpful and clear' '\n>> +       # Test that error messages are clear\n>> +       test_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&\n>> +       grep \"cannot be used together\" error\n>> +'\n> Does this test that \"error messages are helpful and clear\" or just\n> that \"--output-commands and --allow-partial\" are incompatible?\n\n\nIt's really just testing incompatible options. Since --allow-partial is\ngoing away, this test goes away too.\n\n\n> The former would be a test of _all_ error messages from git replay, and\n> would need to look at more than just a small substring of the error\n> message(s).\n\n\nIt's really just testing incompatible options. Since --allow-partial is\ngoing away, this test goes away too. The option incompatibility will be\nreduced to just the existing --advance/--contained check.\n\n\n>\n>> +test_expect_success 'replay with empty range produces no output and no changes' '\n>> +       # Create a test branch for empty range testing\n> Why?  Since you use an A..A range below, you can just use any existing\n> branch in that place, right?\n>\n>> +       git checkout -b empty-test topic1 &&\n>> +       git rev-parse empty-test >empty-test-before &&\n>> +\n>> +       # Empty range should succeed but do nothing\n>> +       git replay --onto main empty-test..empty-test >output &&\n>> +       test_must_be_empty output &&\n> Is A..A the only consideration?  Why would anyone ever pass that to\n> the tool?  I would have thought that if someone made this mistake it\n> was a B..A range where it wasn't obvious to the caller that B\n> contained A.  I don't see why anyone would do A..A and then get\n> surprised at the exit status; that feels like user error.  And in the\n> B..A case, it's not clear to me that returning success without\n> updating A is correct.  You might be giving the user false hope that\n> the command did what they wanted.\n\n\nGood point. I only considered A..A, but you are right that B..A (where B\ncontains A) is more likely and problematic. Silently succeeding could mask\na mistake in their range specification.\n\nI will drop the entire empty range change from this series. The behavior\nfor empty ranges probably deserves its own focused discussion if there's\nreal demand for special handling\n\n\n>\n>> +       # Branch should be unchanged\n>> +       git rev-parse empty-test >empty-test-after &&\n>> +       test_cmp empty-test-before empty-test-after\n>> +'\n>> +\n>>   test_done\n>> --\n>> 2.51.0\n\n\nThanks again for taking the time to provide such detailed feedback. This\nis incredibly helpful.\n\n"},{"id":"527861","messageId":"61107972-5755-49b9-a126-9442418ddff0@gmail.com","threadId":"64109","inReplyTo":"f0abdc27-6850-4b9d-b4eb-a1c92f731142@app.fastmail.com","subject":"Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-02T23:36:45Z","receivedAt":"2025-10-02T23:36:52Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 02/10/25 22:44, Kristoffer Haugsbakk wrote:\n> On Sat, Sep 27, 2025, at 01:08, Siddharth Asthana wrote:\n>> This is v2 of the git-replay atomic updates series.\n>>\n>> Based on the extensive community feedback from v1, I've completely redesigned\n>> the approach. Instead of adding new --update-refs options, this version makes\n>> atomic ref updates the default behavior of git replay.\n>>\n>> Why this change makes sense:\n>> - git replay is explicitly marked as EXPERIMENTAL with behavior changes\n>> expected\n>> - The command is primarily used server-side where atomic transactions\n>> are crucial\n>> - Current pipeline approach (git replay | git update-ref --stdin)\n>> creates\n>>    coordination complexity and lacks atomic guarantees by default\n>> - Patrick Steinhardt noted performance issues with individual ref\n>> updates\n>>    in reftable backend\n>> - Elijah Newren and Junio Hamano endorsed making the better behavior\n>> default\n>>\n>> [snip]\n> On the topic of changing experimental commands: I really like the\n> git-for-each-ref(1) (git-FER) output format design.  It just outputs refs and\n> related data.  It’s not a command for “bulk delete refs” or “check for\n> merge conflicts between these refs and upstream (git-merge-tree(1)”—it\n> just supports all of that through `--format` and its atoms.\n>\n> And for this command it seems to, at the core, output a mapping from old\n> to new commits.\n>\n> Now, I’ve thought that a “client-side”[1] in-memory rebase-like command\n> would need to support outputting data for the `post-rewrite` hook.  And\n> is that not straightforward if you can use `--format` with `from` and\n> `to` atoms?  (I ask because I have never called hooks with git-hook(1).)\n>\n> I just think that (naively maybe) a `--format` command like git-FER with\n> all the quoting modes might be a good fit for this command.  Then you\n> can compose all the steps you need yourself:\n>\n> 1. Call the exact git-update-ref(1) `--batch`/`--stdin` or whatever mode\n>     you need\n> 2. Write a message to each reflog if you want\n> 3. Call the `post-rewrite` hook\n>\n> † 1: c.f. server-side which I get the impression only wants to do cheap\n>       rebases\n\n\nHi Kristoffer,\n\nThat's an interesting perspective on using --format for composability,\nsimilar to git-for-each-ref's design.\n\nThe constraint right now is that git replay's output needs to work\ndirectly with update-ref --stdin, which has a specific format. Adding\n--format would let users customize the output, but then they'd need to\ntransform it to the update-ref format anyway for the most common case,\nwhich seems like extra work.\n\nYour point about post-rewrite hook support is well-taken though. As this\ncommand evolves toward client-side interactive rebase (which was Elijah's\noriginal design goal), we will definitely need hook integration. At that\npoint, a --format approach with atoms like %(old) and %(new) could make\nsense for letting users extract the commit mapping in whatever form they\nneed for hooks or other tooling.\n\nFor this iteration I am focusing on the simpler atomic update case, but \nI will\nkeep the --format idea in mind for future work. Do you see a specific use\ncase right now where --format would help, or is this more about\nfuture-proofing the design for when we add client-side features?\n\nThanks for the thoughtful feedback!\n\n"},{"id":"527862","messageId":"d78578d2-2df1-4e10-89fa-154cdf574fd7@gmail.com","threadId":"64109","inReplyTo":"xmqq7bxdw44y.fsf@gitster.g","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-02T23:42:45Z","receivedAt":"2025-10-02T23:42:52Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 02/10/25 23:57, Junio C Hamano wrote:\n> Elijah Newren <newren@gmail.com> writes:\n>\n>>    * it provided a natural low-level tool for the suite of hash-object,\n>> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users\n>> to have another building block for experimentation and making new\n>> tools.\n>>\n>> I was particularly focused on the last of those items for the intial\n>> version at the time, but it should be noted that all three of these\n>> are somewhat special cases, and the most common user desire is going\n>> to be replaying commits and updating the references at the end.\n> Yes.  We could even tweak the stream in the middle with \"sed\" or\n> \"grep\", I presume? ;-)\n>\n>> Sure, but it's quite trivial to add, right -- as shown above with the\n>> extra \"start\", \"prepare\", \"commit\" directives?\n> Very true.\n>\n> Completely a tangent but, isn't requiring \"prepare\" at this layer,\n> and possibly in the form of ref_transaction_prepare() at the C\n> layer, not so ergonomic API design?  Once you \"start\" a transaction\n> and threw a bunch of instruction, \"commit\" can notice that you are\n> in a transaction and should do whatever necessary (including\n> whatever \"prepare\" does).  I am not advocating to simplify the API\n> by making end-user/program facing \"prepare\" a no-op, but just\n> wondering why we decided to have \"prepare\" a so prominent API\n> element.\n\n\nFor this patch, I am using the simpler pattern: \nref_store_transaction_begin()\n→ ref_transaction_update() → ref_transaction_commit(). Looking at\nbuiltin/update-ref.c and other code, it seems commit() already handles\nwhatever prepare does internally when you are not using the explicit stdin\ntransaction commands.\n\nShould I continue with that pattern, or is there a reason to use prepare()\nexplicitly even when not doing the stdin command flow?\n\nOn the config option you suggested in v1: I will add a config setting so\nusers can set their preference once. I am thinking either replay.updateRefs\n(boolean) or replay.defaultOutput (string: \"update\"|\"commands\"). Any\npreference on the naming pattern?\n\n\n>\n>> ...\n>> Might I suggest a rewrite of the text of the commit message to this point?\n> I do think it makes more sense to the reader to know the reasoning\n> behind the _current_ design, and what its strengths are.\n\n\nThanks Junio. Elijah's rewritten structure is much clearer - I will use it\nfor v3. It properly explains the trade-offs without the false claims I made\nabout atomicity.\n\n\n>\n>> =====\n>> The git replay command currently outputs update commands that can be\n>> piped to update-ref to achieve a rebase, e.g.\n>>\n>>    git replay --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> This separation had advantages for three special cases:\n>>    * it made testing easy (when state isn't modified from one step to\n>> the next, you don't need to make temporary branches or have undo\n>> commands, or try to track the changes)\n>>    * it provided a natural can-it-rebase-cleanly (and what would it\n>> rebase to) capability without automatically updating refs, I guess\n>> kind of like a --dry-run\n>>    * it provided a natural low-level tool for the suite of hash-object,\n>> mktree, commit-tree, mktag, merge-tree, and update-ref, allowing users\n>> to have another building block for experimentation and making new\n>> tools.\n>>\n>> However, it should be noted that all three of these are somewhat\n>> special cases; users, whether on the client or server side, would\n>> almost certainly find it more ergonomical to simply have the updating\n>> of refs be the default.  Change the default behavior to update refs\n>> directly, and atomically (at least to the extent supported by the refs\n>> backend in use).\n>> ====\n> This reads very well.\n>\n>> Why is --allow-partial helpful?  You discussed at length why you\n>> wanted atomic transactions, but you introduce this option with no\n>> rationale and instead just discuss that you implemented it and some\n>> design choices once you presuppose that someone wants to use it.\n>>\n>> Is there a usecase?  I asked for it last time, and suggested\n>> discarding the modes without one, but you only discarded one of the\n>> extras while leaving this one in.  I'd recommend discarding this one\n>> too and just having the two modes -- the output commands that get fed\n>> to update-ref, or the automatic transactional update of all or no\n>> refs.\n> I know there are people who like \"best effort\", but I too want to\n> learn a concrete use case where the \"best effort\" mode, which\n> updates only 3 refs among 30 that were to be updated, would give us\n> a better result than \"all or none\" transaction that fails.\n\n\nI don't have one. Elijah made the same point - I was trying to anticipate\nneeds without justification. I am removing --allow-partial from v3, keeping\njust the two clear modes: atomic updates (default) or --output-commands\nfor the traditional pipeline.\n\nThanks!\n\n"},{"id":"527866","messageId":"CAP8UFD294t9qhQBjRS5cun4fwga0BseRHFmOapG0gpKS3r-6UQ@mail.gmail.com","threadId":"64109","inReplyTo":"CABPp-BGcbdygEjndAjXo9utUhTac7JTHscX4iiwk4UZcHonXvg@mail.gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-10-03T07:05:27Z","receivedAt":"2025-10-03T07:05:41Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Fri, Oct 3, 2025 at 12:55 AM Elijah Newren <newren@gmail.com> wrote:\n>\n> Hi Christian,\n>\n> Excellent review, I just have one tangential question for you...\n>\n> On Tue, Sep 30, 2025 at 1:24 AM Christian Couder\n> <christian.couder@gmail.com> wrote:\n> >\n> > On Sat, Sep 27, 2025 at 1:09 AM Siddharth Asthana\n> > <siddharthasthana31@gmail.com> wrote:\n> > >\n> > > The git replay command currently outputs update commands that must be\n> > > piped to git update-ref --stdin to actually update references:\n> > >\n> > >     git replay --onto main topic1..topic2 | git update-ref --stdin\n> > >\n> > > This design has significant limitations for server-side operations. The\n> > > two-command pipeline creates coordination complexity, provides no atomic\n> > > transaction guarantees by default, and complicates automation in bare\n> > > repository environments where git replay is primarily used.\n> >\n> > Yeah, right.\n>\n> I'm unsure if you are expressing disbelief, or agreeing when you use\n> this phrase.\n\nI was agreeing with the general idea that having to pipe the output\ninto `git update-ref --stdin` to actually update references has\nsignificant limitations (in particular for the server side use of the\ncommand I am interested in).\n\nI didn't check every point, especially the \"provides no atomic\ntransaction guarantees by default\", my bad.\n\n> Most commonly when I see it, I assume the former (see\n> https://dictionary.cambridge.org/us/dictionary/english/yeah-right and\n> https://www.merriam-webster.com/dictionary/yeah for example), but I\n> think you've consistently used this with the opposite connotation.  Am\n> I correct on that?  (This is a particular phrase where tone of voice\n> used would be really helpful, which doesn't get included in emails\n> unfortunately.)\n\nYes, you are correct. I knew that it could be used to express\ndisbelief, but I thought that use was mostly a familiar oral one, and\nthe context would make it clear that I was agreeing. I will be more\ncareful when using it.\n"},{"id":"527867","messageId":"CAP8UFD1Z1waDT6jxYrfzxuEVz1Jnb2uwP7YbB4a6=AhmtLKcLg@mail.gmail.com","threadId":"64109","inReplyTo":"4a5eaefb-79cd-4b7b-ab3a-cbab648280f6@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-10-03T07:30:19Z","receivedAt":"2025-10-03T07:30:33Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Hi Siddharth,\n\nOn Fri, Oct 3, 2025 at 12:16 AM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n\n> Thanks for the detailed commit message review. You are absolutely right - I\n> was mixing the patch rationale with v1→v2 changelog, which belongs in the\n> cover letter.\n>\n> Your suggested framing about considering an --atomic-update option but\n> rejecting it in favor of making it default is much clearer than my\n> approach. I will use that structure.\n>\n> For v3:\n> - Move all \"since v1\" discussion to cover letter\n> - Use imperative mood (\"Let's change\" not \"This patch changes\")\n> - Be explicit that --output-commands and --allow-partial are new options\n> - Add full stops to the implementation details list\n> - Will add Helped-by trailers for Elijah, Patrick and you ofcourse as\n> suggested.\n\nGreat, I am looking forward to v3.\n\n> Quick question: for the C89 compliance mention, should I drop it entirely\n> or briefly note \"uses 'int' instead of 'bool' for C89 compatibility\"? I\n> want to acknowledge the bool→int change but not belabor it.\n\nThere are 2 ways to look at this.\n\n1) If you think it's a significant design decision to not use the\n'bool' type, you should talk about it in the commit message, saying\nsomething like:\n\n\"Using the 'bool' type for X was considered but rejected because Y.\"\n\nwhere you replace \"X\" by the reasons why it could have been used, and\n\"Y\" by the reasons why that was rejected.\n\nMy opinion is that it's not a significant design decision but only a\nminor one, so I think it's better and simpler to just not talk about\nit in the commit message.\n\n2) The other way to look at this is that it was a change from v1 to\nv2. In this case it belongs to the cover letter in the section about\nchanges from v1 to v2 if any.\n\nYou don't necessarily need to include a section about changes from v1\nto v2 in the cover letter for v3. Some do it, some don't. My opinion\nis that it's not very often useful, and readers can relatively easily\nrefer to the cover letter for v2 (where it definitely should be) in\nthe rare cases they really want to see it. So I would suggest talking\nonly about the changes from v2 to v3 in the cover letter for v3.\n\nTo summarize, yeah, you can talk about it both in the commit message\nand in the cover letter if you really want to, but my opinion is that\nit's just not worth it.\n\nThanks for working on this!\n"},{"id":"527869","messageId":"CAP8UFD1JBeGxV65DFCs9dSkYwMpSBhWCZoj6dXCwmKgZnR_=KA@mail.gmail.com","threadId":"64109","inReplyTo":"0fba2f5e-03cd-439b-90bd-f613fcc4ae23@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-10-03T07:59:48Z","receivedAt":"2025-10-03T08:00:02Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Fri, Oct 3, 2025 at 1:27 AM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n\n> >> For users needing the traditional pipeline workflow, --output-commands\n> >> preserves the original behavior:\n> >>\n> >>      git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n> > This is good.  Did you also add a config option so that someone can\n> > just set that option once and use the old behavior?  (as per the\n> > suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/\n> > ?)\n>\n>\n> I didn't, but I should have. I will add a config option for v3.\n\nYou don't need to add that configuration option in the main patch. I\nwould suggest adding it in a separate patch after the main one (which\nchanges the default behavior of the command).\n\nNote that in the commit message of the main patch, it's nice to say\nthat a following commit will add a configuration option for users who\nprefer the previous default behavior.\n\n> For naming, I am thinking either:\n>    - replay.updateRefs (boolean: true = update, false = output-commands)\n>    - replay.defaultOutput (string: \"update\" | \"commands\")\n\nIf the command line option is called `--output-commands` then I would\nsuggest naming it \"replay.outputCommands\" and making it a boolean.\n\n> >> @@ -330,9 +361,12 @@ int cmd_replay(int argc,\n> >>                  usage_with_options(replay_usage, replay_options);\n> >>          }\n> >>\n> >> -       if (advance_name_opt && contained)\n> >> -               die(_(\"options '%s' and '%s' cannot be used together\"),\n> >> -                   \"--advance\", \"--contained\");\n> >> +       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n> >> +                                 contained, \"--contained\");\n> > Broken indentation.  Also, should this have been done as a preparatory\n> > cleanup patch?\n>\n>\n> Good catches. I will fix the indentation.\n>\n> On making it a preparatory patch: should I split it out as a separate\n> cleanup commit, or is it minor enough to fold into the main change? I am\n> leaning toward folding it in since it's directly related to the option\n> handling changes\n\nIf there is only this additional small cleanup change in the main\ncommit, and this small cleanup change is clearly mentioned in the\ncommit message as a \"while at it small cleanup change\", I think it's\nOK.\n\nIf you find out that other additional small cleanup changes would be\nnice too, then they should definitely all go into a preparatory patch\nbefore the main patch.\n\n\n> >> +\n> >> +       /* Handle empty ranges: if no commits were processed, treat as success */\n> >> +       if (!commits_processed)\n> >> +               ret = 1; /* Success - no commits to replay is not an error */\n> >> +       else\n> >> +               ret = result.clean;\n> > The change to treat empty ranges as success is an orthogonal change\n> > that I think at a minimum belongs in a separate patch.  Out of\n> > curiosity, how did you discover the exit status with an empty commit\n> > range?  Why does someone specify such a range, and what form or forms\n> > might it come in?  And is merely returning a successful result enough,\n> > or is there more that needs to be done for correctness?\n>\n>\n> I was thinking about automated scripts that compute ranges dynamically -\n> they might generate A..B where it turns out A==B, and treating that as\n> \"no work needed, success\" seemed reasonable for scripting.\n>\n> But you raise a good point: A..A seems like obvious user error (why would\n> anyone do that intentionally?), and B..A where B contains A is likely a\n> mistake that maybe should error rather than silently succeed.\n>\n> I am inclined to drop it entirely from this series. If there's real demand\n> for specific empty-range handling, we can add it later with proper\n> discussion of the actual use cases. Does that sound reasonable?\n\nYeah, I think dropping it from this series is fine.\n\nWhat happens in those cases should be documented if it isn't already\nthough. Those documentation changes should probably be in a separate\npatch.\n\nThanks.\n"},{"id":"527898","messageId":"6d19a0c4-f000-43f5-b2e1-f84f341063a9@app.fastmail.com","threadId":"64109","inReplyTo":"61107972-5755-49b9-a126-9442418ddff0@gmail.com","subject":"Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2025-10-03T19:05:11Z","receivedAt":"2025-10-03T19:05:33Z","isPatch":true,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"Good evening Siddharth\n\nOn Fri, Oct 3, 2025, at 01:36, Siddharth Asthana wrote:\n> On 02/10/25 22:44, Kristoffer Haugsbakk wrote:\n>>> [snip]\n>> On the topic of changing experimental commands: I really like the\n>> git-for-each-ref(1) (git-FER) output format design.  It just outputs refs and\n>> related data.  It’s not a command for “bulk delete refs” or “check for\n>> merge conflicts between these refs and upstream (git-merge-tree(1)”—it\n>> just supports all of that through `--format` and its atoms.\n>>\n>> And for this command it seems to, at the core, output a mapping from old\n>> to new commits.\n>>\n>> Now, I’ve thought that a “client-side”[1] in-memory rebase-like command\n>> would need to support outputting data for the `post-rewrite` hook.  And\n>> is that not straightforward if you can use `--format` with `from` and\n>> `to` atoms?  (I ask because I have never called hooks with git-hook(1).)\n>>\n>> I just think that (naively maybe) a `--format` command like git-FER with\n>> all the quoting modes might be a good fit for this command.  Then you\n>> can compose all the steps you need yourself:\n>>\n>> 1. Call the exact git-update-ref(1) `--batch`/`--stdin` or whatever mode\n>>     you need\n>> 2. Write a message to each reflog if you want\n>> 3. Call the `post-rewrite` hook\n>>\n>> † 1: c.f. server-side which I get the impression only wants to do cheap\n>>       rebases\n>\n>\n> Hi Kristoffer,\n>\n> That's an interesting perspective on using --format for composability,\n> similar to git-for-each-ref's design.\n>\n> The constraint right now is that git replay's output needs to work\n> directly with update-ref --stdin, which has a specific format. Adding\n> --format would let users customize the output, but then they'd need to\n> transform it to the update-ref format anyway for the most common case,\n> which seems like extra work.\n\ngit-FER has a default format and could still use that (either the\ncurrent one or your proposal).\n\ngit-replay(1) could also concievably support ready-made formats, similar\nto “pretty” formats that git-log(1) & co.\n\n> Your point about post-rewrite hook support is well-taken though. As this\n> command evolves toward client-side interactive rebase (which was Elijah's\n> original design goal), we will definitely need hook integration. At that\n> point, a --format approach with atoms like %(old) and %(new) could make\n> sense for letting users extract the commit mapping in whatever form they\n> need for hooks or other tooling.\n>\n> For this iteration I am focusing on the simpler atomic update case, but\n> I will\n> keep the --format idea in mind for future work.\n>\n> [replying to this part\n>\n> Do you see a specific use case right now where --format would help, or\n> is this more about future-proofing the design for when we add\n> client-side features?\n\nI have been using git-rebase(1) for a while with a post-rewrite script.\nThis is used for interactive rebases but also just keeping up with\nupstream, i.e. a regular rebase.  Then I was idly thinking that\ngit-replay(1) would be faster for the plain rebase case—but it doesn’t\nsupport that hook directly.  Okay, but I can get around that: I can\nparse the output, yank the commit OIDs, and run git-rev-list(1) on both\nof them to get the mapping I want.  But it would be really nice to just\ndeclare the correct post-rewrite format and be done, without having to\nparse anything. :)\n\nBeyond that though I’ve been thinking about more hypothetical “client-\nside” concerns.  I mentioned writing to the reflog.  I imagine that\nserver programs that just want to be able to efficiently “rebase”\nbranches to the upstream don’t need that.  But client-side programs\nmight want to write to the reflog because they want to mark what the\nupdate is for; you could have many kinds of client-side “update ref”\nprograms and want to leave breadcrumbs about what was done.  There is\nmore experimentation.  Whereas I imagine that a forge has maybe a small\nset of “update branch” commands.  I don’t know, maybe I’m rambling at\nthis point.\n\n> Thanks for the thoughtful feedback!\n\nThanks for the consideration and reply!\n"},{"id":"527900","messageId":"CABPp-BE9TV58duojhF_+R6bKDF6-L0md6j+1VeRFd8CJWF++LQ@mail.gmail.com","threadId":"64109","inReplyTo":"0fba2f5e-03cd-439b-90bd-f613fcc4ae23@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-10-03T19:48:39Z","receivedAt":"2025-10-03T19:48:53Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Oct 2, 2025 at 4:27 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> >> For users needing the traditional pipeline workflow, --output-commands\n> >> preserves the original behavior:\n> >>\n> >>      git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n> > This is good.  Did you also add a config option so that someone can\n> > just set that option once and use the old behavior?  (as per the\n> > suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/\n> > ?)\n>\n>\n> I didn't, but I should have. I will add a config option for v3.\n>\n> For naming, I am thinking either:\n>    - replay.updateRefs (boolean: true = update, false = output-commands)\n>    - replay.defaultOutput (string: \"update\" | \"commands\")\n>\n> The boolean feels simpler, but the string might be more extensible if we\n> add other output modes later. Which pattern feels more consistent with\n> existing Git config conventions? Looking at rebase.* they're mostly\n> boolean toggles, but am I missing a better example to follow?\n\nreplay.updateRefs sounds better to me.  defaultOutput with \"update\"\ndoesn't make sense to me.\n\n> You are right - I don't have a concrete use case. I was trying to\n> anticipate potential needs but ended up adding unjustified complexity.\n>\n> I will remove --allow-partial entirely from v3. This simplifies to exactly\n> two modes with clear purposes:\n>    1. Default: atomic ref updates (all-or-nothing)\n>    2. --output-commands: traditional pipeline for special cases\n>\n> Much cleaner design.\n\nNote that once you add a config option, you'll also need an additional\ncommand line flag (or make it possible to invert an existing one), so\nthat users can override the config and get the default behavior.\nMaybe --[no-]update-refs would make sense after all, where\n--update-refs is the default and --no-update-refs is your current\n--output-commands?\n\n(I know you all talked elsewhere in this thread about \"avoiding a name\ncollision\" with rebase, but I don't quite see it as a collision.  When\nStolee suggested the flag for rebase, I pointed out it's roughly what\nI'm doing in replay, so it doesn't feel like a conflict to me.  I'm\nalso open to an alternative flag name if it makes sense, but we\nprobably want whatever the command line flag is to be similar to the\nconfig name and \"defaultOutput\"/--default-output don't make sense as a\nname to me.)\n\n> >> @@ -330,9 +361,12 @@ int cmd_replay(int argc,\n> >>                  usage_with_options(replay_usage, replay_options);\n> >>          }\n> >>\n> >> -       if (advance_name_opt && contained)\n> >> -               die(_(\"options '%s' and '%s' cannot be used together\"),\n> >> -                   \"--advance\", \"--contained\");\n> >> +       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n> >> +                                 contained, \"--contained\");\n> > Broken indentation.  Also, should this have been done as a preparatory\n> > cleanup patch?\n>\n>\n> Good catches. I will fix the indentation.\n>\n> On making it a preparatory patch: should I split it out as a separate\n> cleanup commit, or is it minor enough to fold into the main change? I am\n> leaning toward folding it in since it's directly related to the option\n> handling changes\n\nGiven that it was directly adjacent to the other\ndie_for_incompatible_opt2() call, if that were still the case, I could\nsee making it part of the same commit.  However, dropping the\n--allow-partial flag means you don't need to add that other call\nanymore, so it makes this remaining die_for_incompataible_opt2() call\nan entirely orthogonal change to the rest of your patch.  As such, I\nthink it belongs in a separate patch; it could either be a preparatory\npatch or a follow-up.\n\n> >> @@ -407,6 +452,8 @@ int cmd_replay(int argc,\n> >>                  khint_t pos;\n> >>                  int hr;\n> >>\n> >> +               commits_processed = 1;\n> >> +\n> >>                  if (!commit->parents)\n> >>                          die(_(\"replaying down to root commit is not supported yet!\"));\n> >>                  if (commit->parents->next)\n> >> @@ -457,9 +535,17 @@ int cmd_replay(int argc,\n> >>                  strset_clear(update_refs);\n> >>                  free(update_refs);\n> >>          }\n> >> -       ret = result.clean;\n> >> +\n> >> +       /* Handle empty ranges: if no commits were processed, treat as success */\n> >> +       if (!commits_processed)\n> >> +               ret = 1; /* Success - no commits to replay is not an error */\n> >> +       else\n> >> +               ret = result.clean;\n> > The change to treat empty ranges as success is an orthogonal change\n> > that I think at a minimum belongs in a separate patch.  Out of\n> > curiosity, how did you discover the exit status with an empty commit\n> > range?  Why does someone specify such a range, and what form or forms\n> > might it come in?  And is merely returning a successful result enough,\n> > or is there more that needs to be done for correctness?\n>\n>\n> I was thinking about automated scripts that compute ranges dynamically -\n> they might generate A..B where it turns out A==B, and treating that as\n> \"no work needed, success\" seemed reasonable for scripting.\n>\n> But you raise a good point: A..A seems like obvious user error (why would\n> anyone do that intentionally?), and B..A where B contains A is likely a\n> mistake that maybe should error rather than silently succeed.\n>\n> I am inclined to drop it entirely from this series. If there's real demand\n> for specific empty-range handling, we can add it later with proper\n> discussion of the actual use cases. Does that sound reasonable?\n\nYep, dropping it makes sense to me.  Alternatively, documenting what\nhappens in the case of empty ranges, as Christian suggests, also makes\nsense to me though I might suggest that it be done in an entirely\nseparate series rather than just a separate patch of this series.\n"},{"id":"527902","messageId":"xmqqh5wfu3o6.fsf@gitster.g","threadId":"64109","inReplyTo":"CABPp-BE9TV58duojhF_+R6bKDF6-L0md6j+1VeRFd8CJWF++LQ@mail.gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-03T20:32:41Z","receivedAt":"2025-10-03T20:32:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> For naming, I am thinking either:\n>>    - replay.updateRefs (boolean: true = update, false = output-commands)\n>>    - replay.defaultOutput (string: \"update\" | \"commands\")\n>>\n>> The boolean feels simpler, but the string might be more extensible if we\n>> add other output modes later. Which pattern feels more consistent with\n>> existing Git config conventions? Looking at rebase.* they're mostly\n>> boolean toggles, but am I missing a better example to follow?\n>\n> replay.updateRefs sounds better to me.  defaultOutput with \"update\"\n> doesn't make sense to me.\n\nYup.  Or \"replay.defaultAction = (update-ref | show-comamnds)\" if we\nanticipate that we might have a third option someday.  That would of\ncourse affect the choice of the command line option.\n"},{"id":"528252","messageId":"d9764c7b-8de2-4b54-8c44-a4bd7f5860e8@gmail.com","threadId":"64109","inReplyTo":"9d310bd5-453f-43a4-b477-ba02baa7a664@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-10-08T14:01:13Z","receivedAt":"2025-10-08T14:01:19Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Siddharth\n\nOn 02/10/2025 23:20, Siddharth Asthana wrote:\n> On 30/09/25 15:35, Phillip Wood wrote:\n>> On 27/09/2025 00:08, Siddharth Asthana wrote:\n>>> The git replay command currently outputs update commands that must be\n>>> piped to git update-ref --stdin to actually update references:\n> \n> The actual advantages of the new default aren't about atomicity (that\n> already exists), but rather:\n> - Eliminating the pipeline for the common case\n> - Better ergonomics for users who just want refs updated\n> - Simpler server-side automation\n> \n> I will rewrite the commit message to accurately reflect this. Elijah\n> provided a good suggested structure that captures the real trade-offs\n> without false claims.\n\nThat's great. I agree that having replay update the refs itself is a \nuseful improvement.\n\n>>> +--allow-partial::\n>>> +    Allow some ref updates to succeed even if others fail. By default,\n>>> +    ref updates are atomic (all succeed or all fail). With this option,\n>>> +    failed updates are reported as warnings rather than causing the \n>>> entire\n>>> +    command to fail. The command exits with code 0 only if all updates\n>>> +    succeed; any failures result in exit code 1. Cannot be used with\n>>> +    `--output-commands`.\n>>\n>> Rather than having two incompatible options perhaps we could have a \n>> single \"--update-refs=(yes|print|allow-partial-updates)\" argument. I \n>> think the name \"--allow-partial\" is rather ambiguous as it does not \n>> say what it is allowing to be partial.\n> \n> After thinking about this and Elijah's feedback, I am leaning toward\n> dropping --allow-partial entirely since I don't have a concrete use case\n> for it. That simplifies things to just: default atomic updates vs\n> --output-commands for the traditional pipeline.\n> \n> Would you still prefer a --update-refs=<mode> style, or is the simpler\n> --output-commands flag sufficient given that --allow-partial is going away?\n\nThe advantage of --update-refs=<mode> is that it allows for future \nextensions such as adding support for partial in a way that does not \nadd conflicting options.\n\nThanks\n\nPhillip\n  >\n>>\n>>> +static int add_ref_to_transaction(struct ref_transaction *transaction,\n>>> +                  const char *refname,\n>>> +                  const struct object_id *new_oid,\n>>> +                  const struct object_id *old_oid,\n>>> +                  struct strbuf *err)\n>>> +{\n>>> +    return ref_transaction_update(transaction, refname, new_oid, \n>>> old_oid,\n>>> +                      NULL, NULL, 0, \"git replay\", err);\n>>> +}\n>>\n>> I'm not sure this function adds much value. I think it would be better \n>> to instead have a helper function that updates refs or prints the ref \n>> updates so that we do not duplicate that code in the two places below.\n> \n> \n> ood point. I will extract a helper like:\n> \n>      static int handle_ref_update(int output_commands,\n>                                   struct ref_transaction *transaction,\n>                                   const char *refname,\n>                                   const struct object_id *new_oid,\n>                                   const struct object_id *old_oid,\n>                                   struct strbuf *err)\n> \n> This eliminates the duplication and fixes the over-long lines you pointed\n> out at both call sites.\n> \n> Thanks!\n> \n> \n>>\n>>> @@ -434,10 +481,18 @@ int cmd_replay(int argc,\n>>>               if (decoration->type == DECORATION_REF_LOCAL &&\n>>>                   (contained || strset_contains(update_refs,\n>>>                                 decoration->name))) {\n>>> -                printf(\"update %s %s %s\\n\",\n>>> -                       decoration->name,\n>>> - oid_to_hex(&last_commit->object.oid),\n>>> -                       oid_to_hex(&commit->object.oid));\n>>> +                if (output_commands) {\n>>> +                    printf(\"update %s %s %s\\n\",\n>>> +                           decoration->name,\n>>> + oid_to_hex(&last_commit->object.oid),\n>>> + oid_to_hex(&commit->object.oid));\n>>> +                } else if (add_ref_to_transaction(transaction, \n>>> decoration->name,\n>>> + &last_commit->object.oid,\n>>> +                                  &commit->object.oid,\n>>> +                                  &transaction_err) < 0) {\n>>> +                    ret = error(_(\"failed to add ref update to \n>>> transaction: %s\"), transaction_err.buf);\n>>> +                    goto cleanup;\n>>> +                }\n>>>               }\n>>\n>> The lines here are very long due to the indentation, having a separate \n>> function to update the refs or print the ref updates would be much \n>> more readable.\n>>\n>>>               decoration = decoration->next;\n>>>           }\n>>> @@ -445,10 +500,33 @@ int cmd_replay(int argc,\n>>>         /* In --advance mode, advance the target ref */\n>>>       if (result.clean == 1 && advance_name) {\n>>> -        printf(\"update %s %s %s\\n\",\n>>> -               advance_name,\n>>> -               oid_to_hex(&last_commit->object.oid),\n>>> -               oid_to_hex(&onto->object.oid));\n>>> +        if (output_commands) {\n>>> +            printf(\"update %s %s %s\\n\",\n>>> +                   advance_name,\n>>> +                   oid_to_hex(&last_commit->object.oid),\n>>> +                   oid_to_hex(&onto->object.oid));\n>>> +        } else if (add_ref_to_transaction(transaction, advance_name,\n>>> +                          &last_commit->object.oid,\n>>> +                          &onto->object.oid,\n>>> +                          &transaction_err) < 0) {\n>>> +            ret = error(_(\"failed to add ref update to transaction: \n>>> %s\"), transaction_err.buf);\n>>> +            goto cleanup;\n>>> +        }\n>>> +    }\n>>\n>> Putting the code to update the refs or print the ref updates into a \n>> single function would avoid this duplication and over-long lines.\n>>\n>> Thanks\n>>\n>> Phillip\n>>\n>>> +    /* Commit the ref transaction if we have one */\n>>> +    if (transaction && result.clean == 1) {\n>>> +        if (ref_transaction_commit(transaction, &transaction_err)) {\n>>> +            if (allow_partial) {\n>>> +                warning(_(\"some ref updates failed: %s\"), \n>>> transaction_err.buf);\n>>> + ref_transaction_for_each_rejected_update(transaction,\n>>> +                                     print_rejected_update, NULL);\n>>> +                ret = 0; /* Set failure even with allow_partial */\n>>> +            } else {\n>>> +                ret = error(_(\"failed to update refs: %s\"), \n>>> transaction_err.buf);\n>>> +                goto cleanup;\n>>> +            }\n>>> +        }\n>>>       }\n>>>         merge_finalize(&merge_opt, &result);\n>>> @@ -457,9 +535,17 @@ int cmd_replay(int argc,\n>>>           strset_clear(update_refs);\n>>>           free(update_refs);\n>>>       }\n>>> -    ret = result.clean;\n>>> +\n>>> +    /* Handle empty ranges: if no commits were processed, treat as \n>>> success */\n>>> +    if (!commits_processed)\n>>> +        ret = 1; /* Success - no commits to replay is not an error */\n>>> +    else\n>>> +        ret = result.clean;\n>>>     cleanup:\n>>> +    if (transaction)\n>>> +        ref_transaction_free(transaction);\n>>> +    strbuf_release(&transaction_err);\n>>>       release_revisions(&revs);\n>>>       free(advance_name);\n>>>   diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>>> index 58b3759935..8b4301e227 100755\n>>> --- a/t/t3650-replay-basics.sh\n>>> +++ b/t/t3650-replay-basics.sh\n>>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n>>>   '\n>>>     test_expect_success 'using replay to rebase two branches, one on \n>>> top of other' '\n>>> -    git replay --onto main topic1..topic2 >result &&\n>>> +    git replay --output-commands --onto main topic1..topic2 >result &&\n>>>         test_line_count = 1 result &&\n>>>   @@ -67,9 +67,30 @@ test_expect_success 'using replay to rebase two \n>>> branches, one on top of other' '\n>>>       test_cmp expect result\n>>>   '\n>>>   +test_expect_success 'using replay with default atomic behavior (no \n>>> output)' '\n>>> +    # Create a test branch that wont interfere with others\n>>> +    git branch atomic-test topic2 &&\n>>> +    git rev-parse atomic-test >atomic-test-old &&\n>>> +\n>>> +    # Default behavior: atomic ref updates (no output)\n>>> +    git replay --onto main topic1..atomic-test >output &&\n>>> +    test_must_be_empty output &&\n>>> +\n>>> +    # Verify the branch was updated\n>>> +    git rev-parse atomic-test >atomic-test-new &&\n>>> +    ! test_cmp atomic-test-old atomic-test-new &&\n>>> +\n>>> +    # Verify the history is correct\n>>> +    git log --format=%s atomic-test >actual &&\n>>> +    test_write_lines E D M L B A >expect &&\n>>> +    test_cmp expect actual\n>>> +'\n>>> +\n>>>   test_expect_success 'using replay on bare repo to rebase two \n>>> branches, one on top of other' '\n>>> -    git -C bare replay --onto main topic1..topic2 >result-bare &&\n>>> -    test_cmp expect result-bare\n>>> +    git -C bare replay --output-commands --onto main topic1..topic2 \n>>> >result-bare &&\n>>> +\n>>> +    # The result should match what we got from the regular repo\n>>> +    test_cmp result result-bare\n>>>   '\n>>>     test_expect_success 'using replay to rebase with a conflict' '\n>>> @@ -86,7 +107,7 @@ test_expect_success 'using replay to perform basic \n>>> cherry-pick' '\n>>>       # 2nd field of result is refs/heads/main vs. refs/heads/topic2\n>>>       # 4th field of result is hash for main instead of hash for topic2\n>>>   -    git replay --advance main topic1..topic2 >result &&\n>>> +    git replay --output-commands --advance main topic1..topic2 \n>>> >result &&\n>>>         test_line_count = 1 result &&\n>>>   @@ -102,7 +123,7 @@ test_expect_success 'using replay to perform \n>>> basic cherry-pick' '\n>>>   '\n>>>     test_expect_success 'using replay on bare repo to perform basic \n>>> cherry-pick' '\n>>> -    git -C bare replay --advance main topic1..topic2 >result-bare &&\n>>> +    git -C bare replay --output-commands --advance main \n>>> topic1..topic2 >result-bare &&\n>>>       test_cmp expect result-bare\n>>>   '\n>>>   @@ -115,7 +136,7 @@ test_expect_success 'replay fails when both -- \n>>> advance and --onto are omitted' '\n>>>   '\n>>>     test_expect_success 'using replay to also rebase a contained \n>>> branch' '\n>>> -    git replay --contained --onto main main..topic3 >result &&\n>>> +    git replay --output-commands --contained --onto main \n>>> main..topic3 >result &&\n>>>         test_line_count = 2 result &&\n>>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>>> @@ -139,12 +160,12 @@ test_expect_success 'using replay to also \n>>> rebase a contained branch' '\n>>>   '\n>>>     test_expect_success 'using replay on bare repo to also rebase a \n>>> contained branch' '\n>>> -    git -C bare replay --contained --onto main main..topic3 >result- \n>>> bare &&\n>>> +    git -C bare replay --output-commands --contained --onto main \n>>> main..topic3 >result-bare &&\n>>>       test_cmp expect result-bare\n>>>   '\n>>>     test_expect_success 'using replay to rebase multiple divergent \n>>> branches' '\n>>> -    git replay --onto main ^topic1 topic2 topic4 >result &&\n>>> +    git replay --output-commands --onto main ^topic1 topic2 topic4 \n>>> >result &&\n>>>         test_line_count = 2 result &&\n>>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>>> @@ -168,7 +189,7 @@ test_expect_success 'using replay to rebase \n>>> multiple divergent branches' '\n>>>   '\n>>>     test_expect_success 'using replay on bare repo to rebase multiple \n>>> divergent branches, including contained ones' '\n>>> -    git -C bare replay --contained --onto main ^main topic2 topic3 \n>>> topic4 >result &&\n>>> +    git -C bare replay --output-commands --contained --onto main \n>>> ^main topic2 topic3 topic4 >result &&\n>>>         test_line_count = 4 result &&\n>>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>>> @@ -217,4 +238,131 @@ test_expect_success \n>>> 'merge.directoryRenames=false' '\n>>>           --onto rename-onto rename-onto..rename-from\n>>>   '\n>>>   +# Tests for new default atomic behavior and options> > \n>>> +test_expect_success 'replay default behavior should not produce \n>> output when successful' '\n>>> +    git replay --onto main topic1..topic3 >output &&\n>>> +    test_must_be_empty output\n>>> +'\n>>> +\n>>> +test_expect_success 'replay with --output-commands produces \n>>> traditional output' '\n>>> +    git replay --output-commands --onto main topic1..topic3 >output &&\n>>> +    test_line_count = 1 output &&\n>>> +    grep \"^update refs/heads/topic3 \" output\n>>> +'\n>>> +\n>>> +test_expect_success 'replay with --allow-partial should not produce \n>>> output when successful' '\n>>> +    git replay --allow-partial --onto main topic1..topic3 >output &&\n>>> +    test_must_be_empty output\n>>> +'\n>>> +\n>>> +test_expect_success 'replay fails when --output-commands and -- \n>>> allow-partial are used together' '\n>>> +    test_must_fail git replay --output-commands --allow-partial -- \n>>> onto main topic1..topic2 2>error &&\n>>> +    grep \"cannot be used together\" error\n>>> +'\n>>> +\n>>> +test_expect_success 'replay with --contained updates multiple \n>>> branches atomically' '\n>>> +    # Create fresh test branches based on the original structure\n>>> +    # contained-topic1 should be contained within the range to \n>>> contained-topic3\n>>> +    git branch contained-base main &&\n>>> +    git checkout -b contained-topic1 contained-base &&\n>>> +    test_commit ContainedC &&\n>>> +    git checkout -b contained-topic3 contained-topic1 &&\n>>> +    test_commit ContainedG &&\n>>> +    test_commit ContainedH &&\n>>> +    git checkout main &&\n>>> +\n>>> +    # Store original states\n>>> +    git rev-parse contained-topic1 >contained-topic1-old &&\n>>> +    git rev-parse contained-topic3 >contained-topic3-old &&\n>>> +\n>>> +    # Use --contained to update multiple branches - this should \n>>> update both\n>>> +    git replay --contained --onto main contained-base..contained- \n>>> topic3 &&\n>>> +\n>>> +    # Verify both branches were updated\n>>> +    git rev-parse contained-topic1 >contained-topic1-new &&\n>>> +    git rev-parse contained-topic3 >contained-topic3-new &&\n>>> +    ! test_cmp contained-topic1-old contained-topic1-new &&\n>>> +    ! test_cmp contained-topic3-old contained-topic3-new\n>>> +'\n>>> +\n>>> +test_expect_success 'replay atomic behavior: all refs updated or \n>>> none' '\n>>> +    # Store original state\n>>> +    git rev-parse topic4 >topic4-old &&\n>>> +\n>>> +    # Default atomic behavior\n>>> +    git replay --onto main main..topic4 &&\n>>> +\n>>> +    # Verify ref was updated\n>>> +    git rev-parse topic4 >topic4-new &&\n>>> +    ! test_cmp topic4-old topic4-new &&\n>>> +\n>>> +    # Verify no partial state\n>>> +    git log --format=%s topic4 >actual &&\n>>> +    test_write_lines J I M L B A >expect &&\n>>> +    test_cmp expect actual\n>>> +'\n>>> +\n>>> +test_expect_success 'replay works correctly with bare repositories' '\n>>> +    # Test atomic behavior in bare repo (important for Gitaly)\n>>> +    git checkout -b bare-test topic1 &&\n>>> +    test_commit BareTest &&\n>>> +\n>>> +    # Test with bare repo - replay the commits from main..bare-test \n>>> to get the full history\n>>> +    git -C bare fetch .. bare-test:bare-test &&\n>>> +    git -C bare replay --onto main main..bare-test &&\n>>> +\n>>> +    # Verify the bare repo was updated correctly (no output)\n>>> +    git -C bare log --format=%s bare-test >actual &&\n>>> +    test_write_lines BareTest F C M L B A >expect &&\n>>> +    test_cmp expect actual\n>>> +'\n>>> +\n>>> +test_expect_success 'replay --allow-partial with no failures \n>>> produces no output' '\n>>> +    git checkout -b partial-test topic1 &&\n>>> +    test_commit PartialTest &&\n>>> +\n>>> +    # Should succeed silently even with partial mode\n>>> +    git replay --allow-partial --onto main topic1..partial-test \n>>> >output &&\n>>> +    test_must_be_empty output\n>>> +'\n>>> +\n>>> +test_expect_success 'replay maintains ref update consistency' '\n>>> +    # Test that traditional vs atomic produce equivalent results\n>>> +    git checkout -b method1-test topic2 &&\n>>> +    git checkout -b method2-test topic2 &&\n>>> +\n>>> +    # Both methods should update refs to point to the same replayed \n>>> commits\n>>> +    git replay --output-commands --onto main topic1..method1-test \n>>> >update-commands &&\n>>> +    git update-ref --stdin <update-commands &&\n>>> +    git log --format=%s method1-test >traditional-result &&\n>>> +\n>>> +    # Direct atomic method should produce same commit history\n>>> +    git replay --onto main topic1..method2-test &&\n>>> +    git log --format=%s method2-test >atomic-result &&\n>>> +\n>>> +    # Both methods should produce identical commit histories\n>>> +    test_cmp traditional-result atomic-result\n>>> +'\n>>> +\n>>> +test_expect_success 'replay error messages are helpful and clear' '\n>>> +    # Test that error messages are clear\n>>> +    test_must_fail git replay --output-commands --allow-partial -- \n>>> onto main topic1..topic2 2>error &&\n>>> +    grep \"cannot be used together\" error\n>>> +'\n>>> +\n>>> +test_expect_success 'replay with empty range produces no output and \n>>> no changes' '\n>>> +    # Create a test branch for empty range testing\n>>> +    git checkout -b empty-test topic1 &&\n>>> +    git rev-parse empty-test >empty-test-before &&\n>>> +\n>>> +    # Empty range should succeed but do nothing\n>>> +    git replay --onto main empty-test..empty-test >output &&\n>>> +    test_must_be_empty output &&\n>>> +\n>>> +    # Branch should be unchanged\n>>> +    git rev-parse empty-test >empty-test-after &&\n>>> +    test_cmp empty-test-before empty-test-after\n>>> +'\n>>> +\n>>>   test_done\n>>\n\n"},{"id":"528301","messageId":"b3369f52-5391-4b00-8051-57617f998734@gmail.com","threadId":"64109","inReplyTo":"CAP8UFD1JBeGxV65DFCs9dSkYwMpSBhWCZoj6dXCwmKgZnR_=KA@mail.gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-08T19:59:38Z","receivedAt":"2025-10-08T19:59:45Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 03/10/25 13:29, Christian Couder wrote:\n> On Fri, Oct 3, 2025 at 1:27 AM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>\n>>>> For users needing the traditional pipeline workflow, --output-commands\n>>>> preserves the original behavior:\n>>>>\n>>>>       git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n>>> This is good.  Did you also add a config option so that someone can\n>>> just set that option once and use the old behavior?  (as per the\n>>> suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/\n>>> ?)\n>>\n>> I didn't, but I should have. I will add a config option for v3.\n> You don't need to add that configuration option in the main patch. I\n> would suggest adding it in a separate patch after the main one (which\n> changes the default behavior of the command).\n>\n> Note that in the commit message of the main patch, it's nice to say\n> that a following commit will add a configuration option for users who\n> prefer the previous default behavior.\n>\n>> For naming, I am thinking either:\n>>     - replay.updateRefs (boolean: true = update, false = output-commands)\n>>     - replay.defaultOutput (string: \"update\" | \"commands\")\n> If the command line option is called `--output-commands` then I would\n> suggest naming it \"replay.outputCommands\" and making it a boolean.\n\n\nThat makes sense - replay.outputCommands matches the command line option\nname directly. Much clearer than my replay.defaultOutput idea.\n\nSo the pattern would be:\n- replay.outputCommands = false (default): atomic ref updates\n- replay.outputCommands = true: traditional pipeline output\n\nI will implement this in a separate patch after the main one, as you \nsuggested.\n\n\n>\n>>>> @@ -330,9 +361,12 @@ int cmd_replay(int argc,\n>>>>                   usage_with_options(replay_usage, replay_options);\n>>>>           }\n>>>>\n>>>> -       if (advance_name_opt && contained)\n>>>> -               die(_(\"options '%s' and '%s' cannot be used together\"),\n>>>> -                   \"--advance\", \"--contained\");\n>>>> +       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>>>> +                                 contained, \"--contained\");\n>>> Broken indentation.  Also, should this have been done as a preparatory\n>>> cleanup patch?\n>>\n>> Good catches. I will fix the indentation.\n>>\n>> On making it a preparatory patch: should I split it out as a separate\n>> cleanup commit, or is it minor enough to fold into the main change? I am\n>> leaning toward folding it in since it's directly related to the option\n>> handling changes\n> If there is only this additional small cleanup change in the main\n> commit, and this small cleanup change is clearly mentioned in the\n> commit message as a \"while at it small cleanup change\", I think it's\n> OK.\n\n\nGot it. Since it's just the one die_for_incompatible_opt2() change and\nit's directly related to option handling, I will fold it into the main \npatch\nwith a \"while at it\" note in the commit message.\n\n\n>\n> If you find out that other additional small cleanup changes would be\n> nice too, then they should definitely all go into a preparatory patch\n> before the main patch.\n>\n>\n>>>> +\n>>>> +       /* Handle empty ranges: if no commits were processed, treat as success */\n>>>> +       if (!commits_processed)\n>>>> +               ret = 1; /* Success - no commits to replay is not an error */\n>>>> +       else\n>>>> +               ret = result.clean;\n>>> The change to treat empty ranges as success is an orthogonal change\n>>> that I think at a minimum belongs in a separate patch.  Out of\n>>> curiosity, how did you discover the exit status with an empty commit\n>>> range?  Why does someone specify such a range, and what form or forms\n>>> might it come in?  And is merely returning a successful result enough,\n>>> or is there more that needs to be done for correctness?\n>>\n>> I was thinking about automated scripts that compute ranges dynamically -\n>> they might generate A..B where it turns out A==B, and treating that as\n>> \"no work needed, success\" seemed reasonable for scripting.\n>>\n>> But you raise a good point: A..A seems like obvious user error (why would\n>> anyone do that intentionally?), and B..A where B contains A is likely a\n>> mistake that maybe should error rather than silently succeed.\n>>\n>> I am inclined to drop it entirely from this series. If there's real demand\n>> for specific empty-range handling, we can add it later with proper\n>> discussion of the actual use cases. Does that sound reasonable?\n> Yeah, I think dropping it from this series is fine.\n\n\nThanks Christian. I will drop the empty range handling from this series.\n\nOn documenting the current behavior for empty ranges: should that go in\nthis series or separately? If the current behavior is just \"returns\nfailure for empty ranges\", maybe a simple doc note is enough. But if we\nwant to discuss what the behavior *should* be, that probably deserves its\nown focused series.\n\nWhat do you think?\n\n\n>\n> What happens in those cases should be documented if it isn't already\n> though. Those documentation changes should probably be in a separate\n> patch.\n>\n> Thanks.\n"},{"id":"528302","messageId":"38742a2f-5c5b-48f8-a9fd-acea47b7ce71@gmail.com","threadId":"64109","inReplyTo":"6d19a0c4-f000-43f5-b2e1-f84f341063a9@app.fastmail.com","subject":"Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-08T20:02:02Z","receivedAt":"2025-10-08T20:02:10Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 04/10/25 00:35, Kristoffer Haugsbakk wrote:\n> Good evening Siddharth\n>\n> On Fri, Oct 3, 2025, at 01:36, Siddharth Asthana wrote:\n>> On 02/10/25 22:44, Kristoffer Haugsbakk wrote:\n>>>> [snip]\n>>> On the topic of changing experimental commands: I really like the\n>>> git-for-each-ref(1) (git-FER) output format design.  It just outputs refs and\n>>> related data.  It’s not a command for “bulk delete refs” or “check for\n>>> merge conflicts between these refs and upstream (git-merge-tree(1)”—it\n>>> just supports all of that through `--format` and its atoms.\n>>>\n>>> And for this command it seems to, at the core, output a mapping from old\n>>> to new commits.\n>>>\n>>> Now, I’ve thought that a “client-side”[1] in-memory rebase-like command\n>>> would need to support outputting data for the `post-rewrite` hook.  And\n>>> is that not straightforward if you can use `--format` with `from` and\n>>> `to` atoms?  (I ask because I have never called hooks with git-hook(1).)\n>>>\n>>> I just think that (naively maybe) a `--format` command like git-FER with\n>>> all the quoting modes might be a good fit for this command.  Then you\n>>> can compose all the steps you need yourself:\n>>>\n>>> 1. Call the exact git-update-ref(1) `--batch`/`--stdin` or whatever mode\n>>>      you need\n>>> 2. Write a message to each reflog if you want\n>>> 3. Call the `post-rewrite` hook\n>>>\n>>> † 1: c.f. server-side which I get the impression only wants to do cheap\n>>>        rebases\n>>\n>> Hi Kristoffer,\n>>\n>> That's an interesting perspective on using --format for composability,\n>> similar to git-for-each-ref's design.\n>>\n>> The constraint right now is that git replay's output needs to work\n>> directly with update-ref --stdin, which has a specific format. Adding\n>> --format would let users customize the output, but then they'd need to\n>> transform it to the update-ref format anyway for the most common case,\n>> which seems like extra work.\n> git-FER has a default format and could still use that (either the\n> current one or your proposal).\n>\n> git-replay(1) could also concievably support ready-made formats, similar\n> to “pretty” formats that git-log(1) & co.\n>\n>> Your point about post-rewrite hook support is well-taken though. As this\n>> command evolves toward client-side interactive rebase (which was Elijah's\n>> original design goal), we will definitely need hook integration. At that\n>> point, a --format approach with atoms like %(old) and %(new) could make\n>> sense for letting users extract the commit mapping in whatever form they\n>> need for hooks or other tooling.\n>>\n>> For this iteration I am focusing on the simpler atomic update case, but\n>> I will\n>> keep the --format idea in mind for future work.\n>>\n>> [replying to this part\n>>\n>> Do you see a specific use case right now where --format would help, or\n>> is this more about future-proofing the design for when we add\n>> client-side features?\n> I have been using git-rebase(1) for a while with a post-rewrite script.\n> This is used for interactive rebases but also just keeping up with\n> upstream, i.e. a regular rebase.  Then I was idly thinking that\n> git-replay(1) would be faster for the plain rebase case—but it doesn’t\n> support that hook directly.  Okay, but I can get around that: I can\n> parse the output, yank the commit OIDs, and run git-rev-list(1) on both\n> of them to get the mapping I want.  But it would be really nice to just\n> declare the correct post-rewrite format and be done, without having to\n> parse anything. :)\n\n\nAh, that's a concrete use case! You are using post-rewrite hooks with\nrebase and want git replay to support that workflow without needing to\nparse output.\n\nThat makes sense for the client-side evolution of the command. Right now\nthe focus is server-side where hooks aren't typically needed, but as this\nmoves toward replacing interactive rebase, proper hook support (including\npost-rewrite) will be essential.\n\nI think --format with atoms would work well for that - you could get\nexactly the format post-rewrite expects without parsing. For now I'll keep\nthe simple update-ref format, but this is good motivation for adding\n--format support when we tackle the client-side features.\n\nThanks for the concrete example!\n\n\n>\n> Beyond that though I’ve been thinking about more hypothetical “client-\n> side” concerns.  I mentioned writing to the reflog.  I imagine that\n> server programs that just want to be able to efficiently “rebase”\n> branches to the upstream don’t need that.  But client-side programs\n> might want to write to the reflog because they want to mark what the\n> update is for; you could have many kinds of client-side “update ref”\n> programs and want to leave breadcrumbs about what was done.  There is\n> more experimentation.  Whereas I imagine that a forge has maybe a small\n> set of “update branch” commands.  I don’t know, maybe I’m rambling at\n> this point.\n>\n>> Thanks for the thoughtful feedback!\n> Thanks for the consideration and reply!\n"},{"id":"528303","messageId":"d4ef2c70-d03f-4c89-87fb-0ef4dba1bdd0@gmail.com","threadId":"64109","inReplyTo":"CABPp-BE9TV58duojhF_+R6bKDF6-L0md6j+1VeRFd8CJWF++LQ@mail.gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-08T20:05:18Z","receivedAt":"2025-10-08T20:05:25Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 04/10/25 01:18, Elijah Newren wrote:\n> On Thu, Oct 2, 2025 at 4:27 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>>>> For users needing the traditional pipeline workflow, --output-commands\n>>>> preserves the original behavior:\n>>>>\n>>>>       git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n>>> This is good.  Did you also add a config option so that someone can\n>>> just set that option once and use the old behavior?  (as per the\n>>> suggestion at https://lore.kernel.org/git/xmqq5xdrvand.fsf@gitster.g/\n>>> ?)\n>>\n>> I didn't, but I should have. I will add a config option for v3.\n>>\n>> For naming, I am thinking either:\n>>     - replay.updateRefs (boolean: true = update, false = output-commands)\n>>     - replay.defaultOutput (string: \"update\" | \"commands\")\n>>\n>> The boolean feels simpler, but the string might be more extensible if we\n>> add other output modes later. Which pattern feels more consistent with\n>> existing Git config conventions? Looking at rebase.* they're mostly\n>> boolean toggles, but am I missing a better example to follow?\n> replay.updateRefs sounds better to me.  defaultOutput with \"update\"\n> doesn't make sense to me.\n>\n>> You are right - I don't have a concrete use case. I was trying to\n>> anticipate potential needs but ended up adding unjustified complexity.\n>>\n>> I will remove --allow-partial entirely from v3. This simplifies to exactly\n>> two modes with clear purposes:\n>>     1. Default: atomic ref updates (all-or-nothing)\n>>     2. --output-commands: traditional pipeline for special cases\n>>\n>> Much cleaner design.\n> Note that once you add a config option, you'll also need an additional\n> command line flag (or make it possible to invert an existing one), so\n> that users can override the config and get the default behavior.\n> Maybe --[no-]update-refs would make sense after all, where\n> --update-refs is the default and --no-update-refs is your current\n> --output-commands?\n\n\nThat's a good point. With a config option, users need a way to override it.\n\nThe --[no-]update-refs pattern makes sense:\n- --update-refs (default): atomic ref updates\n- --no-update-refs: output commands (equivalent to --output-commands)\n\nThis is cleaner than having both --output-commands and needing a separate\n--no-output-commands. And you're right about the rebase naming - it's not\nreally a collision since the concepts are similar enough.\n\nShould I go with --[no-]update-refs and drop --output-commands entirely,\nor keep --output-commands as an alias for --no-update-refs for clarity?\n\n\n>\n> (I know you all talked elsewhere in this thread about \"avoiding a name\n> collision\" with rebase, but I don't quite see it as a collision.  When\n> Stolee suggested the flag for rebase, I pointed out it's roughly what\n> I'm doing in replay, so it doesn't feel like a conflict to me.  I'm\n> also open to an alternative flag name if it makes sense, but we\n> probably want whatever the command line flag is to be similar to the\n> config name and \"defaultOutput\"/--default-output don't make sense as a\n> name to me.)\n>\n>>>> @@ -330,9 +361,12 @@ int cmd_replay(int argc,\n>>>>                   usage_with_options(replay_usage, replay_options);\n>>>>           }\n>>>>\n>>>> -       if (advance_name_opt && contained)\n>>>> -               die(_(\"options '%s' and '%s' cannot be used together\"),\n>>>> -                   \"--advance\", \"--contained\");\n>>>> +       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>>>> +                                 contained, \"--contained\");\n>>> Broken indentation.  Also, should this have been done as a preparatory\n>>> cleanup patch?\n>>\n>> Good catches. I will fix the indentation.\n>>\n>> On making it a preparatory patch: should I split it out as a separate\n>> cleanup commit, or is it minor enough to fold into the main change? I am\n>> leaning toward folding it in since it's directly related to the option\n>> handling changes\n> Given that it was directly adjacent to the other\n> die_for_incompatible_opt2() call, if that were still the case, I could\n> see making it part of the same commit.  However, dropping the\n> --allow-partial flag means you don't need to add that other call\n> anymore, so it makes this remaining die_for_incompataible_opt2() call\n> an entirely orthogonal change to the rest of your patch.  As such, I\n> think it belongs in a separate patch; it could either be a preparatory\n> patch or a follow-up.\n\n\nGood point. With --allow-partial gone, the die_for_incompatible_opt2()\nchange stands alone. I will make it a preparatory cleanup patch before the\nmain change.\n\n\n>\n>>>> @@ -407,6 +452,8 @@ int cmd_replay(int argc,\n>>>>                   khint_t pos;\n>>>>                   int hr;\n>>>>\n>>>> +               commits_processed = 1;\n>>>> +\n>>>>                   if (!commit->parents)\n>>>>                           die(_(\"replaying down to root commit is not supported yet!\"));\n>>>>                   if (commit->parents->next)\n>>>> @@ -457,9 +535,17 @@ int cmd_replay(int argc,\n>>>>                   strset_clear(update_refs);\n>>>>                   free(update_refs);\n>>>>           }\n>>>> -       ret = result.clean;\n>>>> +\n>>>> +       /* Handle empty ranges: if no commits were processed, treat as success */\n>>>> +       if (!commits_processed)\n>>>> +               ret = 1; /* Success - no commits to replay is not an error */\n>>>> +       else\n>>>> +               ret = result.clean;\n>>> The change to treat empty ranges as success is an orthogonal change\n>>> that I think at a minimum belongs in a separate patch.  Out of\n>>> curiosity, how did you discover the exit status with an empty commit\n>>> range?  Why does someone specify such a range, and what form or forms\n>>> might it come in?  And is merely returning a successful result enough,\n>>> or is there more that needs to be done for correctness?\n>>\n>> I was thinking about automated scripts that compute ranges dynamically -\n>> they might generate A..B where it turns out A==B, and treating that as\n>> \"no work needed, success\" seemed reasonable for scripting.\n>>\n>> But you raise a good point: A..A seems like obvious user error (why would\n>> anyone do that intentionally?), and B..A where B contains A is likely a\n>> mistake that maybe should error rather than silently succeed.\n>>\n>> I am inclined to drop it entirely from this series. If there's real demand\n>> for specific empty-range handling, we can add it later with proper\n>> discussion of the actual use cases. Does that sound reasonable?\n> Yep, dropping it makes sense to me.  Alternatively, documenting what\n> happens in the case of empty ranges, as Christian suggests, also makes\n> sense to me though I might suggest that it be done in an entirely\n> separate series rather than just a separate patch of this series.\n\n\nI will drop it from this series. Documenting the current empty range\nbehavior can be a separate follow-up if there's interest, but I don't\nthink it needs to block this change.\n\n\n"},{"id":"528304","messageId":"ea7aa170-400c-47fa-b3f0-2623fcbfcaea@gmail.com","threadId":"64109","inReplyTo":"xmqqh5wfu3o6.fsf@gitster.g","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-08T20:06:40Z","receivedAt":"2025-10-08T20:06:47Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 04/10/25 02:02, Junio C Hamano wrote:\n> Elijah Newren <newren@gmail.com> writes:\n>\n>>> For naming, I am thinking either:\n>>>     - replay.updateRefs (boolean: true = update, false = output-commands)\n>>>     - replay.defaultOutput (string: \"update\" | \"commands\")\n>>>\n>>> The boolean feels simpler, but the string might be more extensible if we\n>>> add other output modes later. Which pattern feels more consistent with\n>>> existing Git config conventions? Looking at rebase.* they're mostly\n>>> boolean toggles, but am I missing a better example to follow?\n>> replay.updateRefs sounds better to me.  defaultOutput with \"update\"\n>> doesn't make sense to me.\n> Yup.  Or \"replay.defaultAction = (update-ref | show-comamnds)\" if we\n> anticipate that we might have a third option someday.  That would of\n> course affect the choice of the command line option.\n\n\nThat's interesting. Between:\n- replay.updateRefs (boolean)\n- replay.defaultAction (enum string)\n\nThe enum is more extensible, but do we actually anticipate other modes?\nElijah's --format idea from Kristoffer might be a third mode eventually,\nbut that seems far off.\n\nI am leaning toward the simpler replay.updateRefs boolean for now, but if\nyou think the extensibility is worth it, I can go with defaultAction.\nWhat's your preference?\n\n"},{"id":"528305","messageId":"1bfffc20-7e25-4633-a0b8-6660913a74dd@gmail.com","threadId":"64109","inReplyTo":"d9764c7b-8de2-4b54-8c44-a4bd7f5860e8@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-08T20:09:00Z","receivedAt":"2025-10-08T20:09:08Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 08/10/25 19:31, Phillip Wood wrote:\n> Hi Siddharth\n>\n> On 02/10/2025 23:20, Siddharth Asthana wrote:\n>> On 30/09/25 15:35, Phillip Wood wrote:\n>>> On 27/09/2025 00:08, Siddharth Asthana wrote:\n>>>> The git replay command currently outputs update commands that must be\n>>>> piped to git update-ref --stdin to actually update references:\n>>\n>> The actual advantages of the new default aren't about atomicity (that\n>> already exists), but rather:\n>> - Eliminating the pipeline for the common case\n>> - Better ergonomics for users who just want refs updated\n>> - Simpler server-side automation\n>>\n>> I will rewrite the commit message to accurately reflect this. Elijah\n>> provided a good suggested structure that captures the real trade-offs\n>> without false claims.\n>\n> That's great. I agree that having replay update the refs itself is a \n> useful improvement.\n>\n>>>> +--allow-partial::\n>>>> +    Allow some ref updates to succeed even if others fail. By \n>>>> default,\n>>>> +    ref updates are atomic (all succeed or all fail). With this \n>>>> option,\n>>>> +    failed updates are reported as warnings rather than causing \n>>>> the entire\n>>>> +    command to fail. The command exits with code 0 only if all \n>>>> updates\n>>>> +    succeed; any failures result in exit code 1. Cannot be used with\n>>>> +    `--output-commands`.\n>>>\n>>> Rather than having two incompatible options perhaps we could have a \n>>> single \"--update-refs=(yes|print|allow-partial-updates)\" argument. I \n>>> think the name \"--allow-partial\" is rather ambiguous as it does not \n>>> say what it is allowing to be partial.\n>>\n>> After thinking about this and Elijah's feedback, I am leaning toward\n>> dropping --allow-partial entirely since I don't have a concrete use case\n>> for it. That simplifies things to just: default atomic updates vs\n>> --output-commands for the traditional pipeline.\n>>\n>> Would you still prefer a --update-refs=<mode> style, or is the simpler\n>> --output-commands flag sufficient given that --allow-partial is going \n>> away?\n>\n> The advantage of --update-refs=<mode> is that it allows for future \n> extensions such as adding support for partial in a way that does not \n> add conflicting options.\n\n\nThat's a good point about extensibility. Elijah suggested \n--[no-]update-refs\nwhich is simpler but less extensible.\n\nBetween:\n- --[no-]update-refs (simple, covers current needs)\n- --update-refs=<mode> (extensible for future modes)\n\nI am inclined toward the simpler --[no-]update-refs for now since we don't\nhave concrete plans for other modes. But if you think the extensibility is\nimportant, I can go with the =<mode> style. What do you think?\n\n\n>\n> Thanks\n>\n> Phillip\n>  >\n>>>\n>>>> +static int add_ref_to_transaction(struct ref_transaction \n>>>> *transaction,\n>>>> +                  const char *refname,\n>>>> +                  const struct object_id *new_oid,\n>>>> +                  const struct object_id *old_oid,\n>>>> +                  struct strbuf *err)\n>>>> +{\n>>>> +    return ref_transaction_update(transaction, refname, new_oid, \n>>>> old_oid,\n>>>> +                      NULL, NULL, 0, \"git replay\", err);\n>>>> +}\n>>>\n>>> I'm not sure this function adds much value. I think it would be \n>>> better to instead have a helper function that updates refs or prints \n>>> the ref updates so that we do not duplicate that code in the two \n>>> places below.\n>>\n>>\n>> ood point. I will extract a helper like:\n>>\n>>      static int handle_ref_update(int output_commands,\n>>                                   struct ref_transaction *transaction,\n>>                                   const char *refname,\n>>                                   const struct object_id *new_oid,\n>>                                   const struct object_id *old_oid,\n>>                                   struct strbuf *err)\n>>\n>> This eliminates the duplication and fixes the over-long lines you \n>> pointed\n>> out at both call sites.\n>>\n>> Thanks!\n>>\n>>\n>>>\n>>>> @@ -434,10 +481,18 @@ int cmd_replay(int argc,\n>>>>               if (decoration->type == DECORATION_REF_LOCAL &&\n>>>>                   (contained || strset_contains(update_refs,\n>>>>                                 decoration->name))) {\n>>>> -                printf(\"update %s %s %s\\n\",\n>>>> -                       decoration->name,\n>>>> - oid_to_hex(&last_commit->object.oid),\n>>>> - oid_to_hex(&commit->object.oid));\n>>>> +                if (output_commands) {\n>>>> +                    printf(\"update %s %s %s\\n\",\n>>>> +                           decoration->name,\n>>>> + oid_to_hex(&last_commit->object.oid),\n>>>> + oid_to_hex(&commit->object.oid));\n>>>> +                } else if (add_ref_to_transaction(transaction, \n>>>> decoration->name,\n>>>> + &last_commit->object.oid,\n>>>> + &commit->object.oid,\n>>>> +                                  &transaction_err) < 0) {\n>>>> +                    ret = error(_(\"failed to add ref update to \n>>>> transaction: %s\"), transaction_err.buf);\n>>>> +                    goto cleanup;\n>>>> +                }\n>>>>               }\n>>>\n>>> The lines here are very long due to the indentation, having a \n>>> separate function to update the refs or print the ref updates would \n>>> be much more readable.\n>>>\n>>>>               decoration = decoration->next;\n>>>>           }\n>>>> @@ -445,10 +500,33 @@ int cmd_replay(int argc,\n>>>>         /* In --advance mode, advance the target ref */\n>>>>       if (result.clean == 1 && advance_name) {\n>>>> -        printf(\"update %s %s %s\\n\",\n>>>> -               advance_name,\n>>>> -               oid_to_hex(&last_commit->object.oid),\n>>>> -               oid_to_hex(&onto->object.oid));\n>>>> +        if (output_commands) {\n>>>> +            printf(\"update %s %s %s\\n\",\n>>>> +                   advance_name,\n>>>> + oid_to_hex(&last_commit->object.oid),\n>>>> +                   oid_to_hex(&onto->object.oid));\n>>>> +        } else if (add_ref_to_transaction(transaction, advance_name,\n>>>> +                          &last_commit->object.oid,\n>>>> +                          &onto->object.oid,\n>>>> +                          &transaction_err) < 0) {\n>>>> +            ret = error(_(\"failed to add ref update to \n>>>> transaction: %s\"), transaction_err.buf);\n>>>> +            goto cleanup;\n>>>> +        }\n>>>> +    }\n>>>\n>>> Putting the code to update the refs or print the ref updates into a \n>>> single function would avoid this duplication and over-long lines.\n>>>\n>>> Thanks\n>>>\n>>> Phillip\n>>>\n>>>> +    /* Commit the ref transaction if we have one */\n>>>> +    if (transaction && result.clean == 1) {\n>>>> +        if (ref_transaction_commit(transaction, &transaction_err)) {\n>>>> +            if (allow_partial) {\n>>>> +                warning(_(\"some ref updates failed: %s\"), \n>>>> transaction_err.buf);\n>>>> + ref_transaction_for_each_rejected_update(transaction,\n>>>> +                                     print_rejected_update, NULL);\n>>>> +                ret = 0; /* Set failure even with allow_partial */\n>>>> +            } else {\n>>>> +                ret = error(_(\"failed to update refs: %s\"), \n>>>> transaction_err.buf);\n>>>> +                goto cleanup;\n>>>> +            }\n>>>> +        }\n>>>>       }\n>>>>         merge_finalize(&merge_opt, &result);\n>>>> @@ -457,9 +535,17 @@ int cmd_replay(int argc,\n>>>>           strset_clear(update_refs);\n>>>>           free(update_refs);\n>>>>       }\n>>>> -    ret = result.clean;\n>>>> +\n>>>> +    /* Handle empty ranges: if no commits were processed, treat as \n>>>> success */\n>>>> +    if (!commits_processed)\n>>>> +        ret = 1; /* Success - no commits to replay is not an error */\n>>>> +    else\n>>>> +        ret = result.clean;\n>>>>     cleanup:\n>>>> +    if (transaction)\n>>>> +        ref_transaction_free(transaction);\n>>>> +    strbuf_release(&transaction_err);\n>>>>       release_revisions(&revs);\n>>>>       free(advance_name);\n>>>>   diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>>>> index 58b3759935..8b4301e227 100755\n>>>> --- a/t/t3650-replay-basics.sh\n>>>> +++ b/t/t3650-replay-basics.sh\n>>>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n>>>>   '\n>>>>     test_expect_success 'using replay to rebase two branches, one \n>>>> on top of other' '\n>>>> -    git replay --onto main topic1..topic2 >result &&\n>>>> +    git replay --output-commands --onto main topic1..topic2 \n>>>> >result &&\n>>>>         test_line_count = 1 result &&\n>>>>   @@ -67,9 +67,30 @@ test_expect_success 'using replay to rebase \n>>>> two branches, one on top of other' '\n>>>>       test_cmp expect result\n>>>>   '\n>>>>   +test_expect_success 'using replay with default atomic behavior \n>>>> (no output)' '\n>>>> +    # Create a test branch that wont interfere with others\n>>>> +    git branch atomic-test topic2 &&\n>>>> +    git rev-parse atomic-test >atomic-test-old &&\n>>>> +\n>>>> +    # Default behavior: atomic ref updates (no output)\n>>>> +    git replay --onto main topic1..atomic-test >output &&\n>>>> +    test_must_be_empty output &&\n>>>> +\n>>>> +    # Verify the branch was updated\n>>>> +    git rev-parse atomic-test >atomic-test-new &&\n>>>> +    ! test_cmp atomic-test-old atomic-test-new &&\n>>>> +\n>>>> +    # Verify the history is correct\n>>>> +    git log --format=%s atomic-test >actual &&\n>>>> +    test_write_lines E D M L B A >expect &&\n>>>> +    test_cmp expect actual\n>>>> +'\n>>>> +\n>>>>   test_expect_success 'using replay on bare repo to rebase two \n>>>> branches, one on top of other' '\n>>>> -    git -C bare replay --onto main topic1..topic2 >result-bare &&\n>>>> -    test_cmp expect result-bare\n>>>> +    git -C bare replay --output-commands --onto main \n>>>> topic1..topic2 >result-bare &&\n>>>> +\n>>>> +    # The result should match what we got from the regular repo\n>>>> +    test_cmp result result-bare\n>>>>   '\n>>>>     test_expect_success 'using replay to rebase with a conflict' '\n>>>> @@ -86,7 +107,7 @@ test_expect_success 'using replay to perform \n>>>> basic cherry-pick' '\n>>>>       # 2nd field of result is refs/heads/main vs. refs/heads/topic2\n>>>>       # 4th field of result is hash for main instead of hash for \n>>>> topic2\n>>>>   -    git replay --advance main topic1..topic2 >result &&\n>>>> +    git replay --output-commands --advance main topic1..topic2 \n>>>> >result &&\n>>>>         test_line_count = 1 result &&\n>>>>   @@ -102,7 +123,7 @@ test_expect_success 'using replay to perform \n>>>> basic cherry-pick' '\n>>>>   '\n>>>>     test_expect_success 'using replay on bare repo to perform basic \n>>>> cherry-pick' '\n>>>> -    git -C bare replay --advance main topic1..topic2 >result-bare &&\n>>>> +    git -C bare replay --output-commands --advance main \n>>>> topic1..topic2 >result-bare &&\n>>>>       test_cmp expect result-bare\n>>>>   '\n>>>>   @@ -115,7 +136,7 @@ test_expect_success 'replay fails when both \n>>>> -- advance and --onto are omitted' '\n>>>>   '\n>>>>     test_expect_success 'using replay to also rebase a contained \n>>>> branch' '\n>>>> -    git replay --contained --onto main main..topic3 >result &&\n>>>> +    git replay --output-commands --contained --onto main \n>>>> main..topic3 >result &&\n>>>>         test_line_count = 2 result &&\n>>>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>>>> @@ -139,12 +160,12 @@ test_expect_success 'using replay to also \n>>>> rebase a contained branch' '\n>>>>   '\n>>>>     test_expect_success 'using replay on bare repo to also rebase a \n>>>> contained branch' '\n>>>> -    git -C bare replay --contained --onto main main..topic3 \n>>>> >result- bare &&\n>>>> +    git -C bare replay --output-commands --contained --onto main \n>>>> main..topic3 >result-bare &&\n>>>>       test_cmp expect result-bare\n>>>>   '\n>>>>     test_expect_success 'using replay to rebase multiple divergent \n>>>> branches' '\n>>>> -    git replay --onto main ^topic1 topic2 topic4 >result &&\n>>>> +    git replay --output-commands --onto main ^topic1 topic2 topic4 \n>>>> >result &&\n>>>>         test_line_count = 2 result &&\n>>>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>>>> @@ -168,7 +189,7 @@ test_expect_success 'using replay to rebase \n>>>> multiple divergent branches' '\n>>>>   '\n>>>>     test_expect_success 'using replay on bare repo to rebase \n>>>> multiple divergent branches, including contained ones' '\n>>>> -    git -C bare replay --contained --onto main ^main topic2 topic3 \n>>>> topic4 >result &&\n>>>> +    git -C bare replay --output-commands --contained --onto main \n>>>> ^main topic2 topic3 topic4 >result &&\n>>>>         test_line_count = 4 result &&\n>>>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>>>> @@ -217,4 +238,131 @@ test_expect_success \n>>>> 'merge.directoryRenames=false' '\n>>>>           --onto rename-onto rename-onto..rename-from\n>>>>   '\n>>>>   +# Tests for new default atomic behavior and options> > \n>>>> +test_expect_success 'replay default behavior should not produce \n>>> output when successful' '\n>>>> +    git replay --onto main topic1..topic3 >output &&\n>>>> +    test_must_be_empty output\n>>>> +'\n>>>> +\n>>>> +test_expect_success 'replay with --output-commands produces \n>>>> traditional output' '\n>>>> +    git replay --output-commands --onto main topic1..topic3 \n>>>> >output &&\n>>>> +    test_line_count = 1 output &&\n>>>> +    grep \"^update refs/heads/topic3 \" output\n>>>> +'\n>>>> +\n>>>> +test_expect_success 'replay with --allow-partial should not \n>>>> produce output when successful' '\n>>>> +    git replay --allow-partial --onto main topic1..topic3 >output &&\n>>>> +    test_must_be_empty output\n>>>> +'\n>>>> +\n>>>> +test_expect_success 'replay fails when --output-commands and -- \n>>>> allow-partial are used together' '\n>>>> +    test_must_fail git replay --output-commands --allow-partial -- \n>>>> onto main topic1..topic2 2>error &&\n>>>> +    grep \"cannot be used together\" error\n>>>> +'\n>>>> +\n>>>> +test_expect_success 'replay with --contained updates multiple \n>>>> branches atomically' '\n>>>> +    # Create fresh test branches based on the original structure\n>>>> +    # contained-topic1 should be contained within the range to \n>>>> contained-topic3\n>>>> +    git branch contained-base main &&\n>>>> +    git checkout -b contained-topic1 contained-base &&\n>>>> +    test_commit ContainedC &&\n>>>> +    git checkout -b contained-topic3 contained-topic1 &&\n>>>> +    test_commit ContainedG &&\n>>>> +    test_commit ContainedH &&\n>>>> +    git checkout main &&\n>>>> +\n>>>> +    # Store original states\n>>>> +    git rev-parse contained-topic1 >contained-topic1-old &&\n>>>> +    git rev-parse contained-topic3 >contained-topic3-old &&\n>>>> +\n>>>> +    # Use --contained to update multiple branches - this should \n>>>> update both\n>>>> +    git replay --contained --onto main contained-base..contained- \n>>>> topic3 &&\n>>>> +\n>>>> +    # Verify both branches were updated\n>>>> +    git rev-parse contained-topic1 >contained-topic1-new &&\n>>>> +    git rev-parse contained-topic3 >contained-topic3-new &&\n>>>> +    ! test_cmp contained-topic1-old contained-topic1-new &&\n>>>> +    ! test_cmp contained-topic3-old contained-topic3-new\n>>>> +'\n>>>> +\n>>>> +test_expect_success 'replay atomic behavior: all refs updated or \n>>>> none' '\n>>>> +    # Store original state\n>>>> +    git rev-parse topic4 >topic4-old &&\n>>>> +\n>>>> +    # Default atomic behavior\n>>>> +    git replay --onto main main..topic4 &&\n>>>> +\n>>>> +    # Verify ref was updated\n>>>> +    git rev-parse topic4 >topic4-new &&\n>>>> +    ! test_cmp topic4-old topic4-new &&\n>>>> +\n>>>> +    # Verify no partial state\n>>>> +    git log --format=%s topic4 >actual &&\n>>>> +    test_write_lines J I M L B A >expect &&\n>>>> +    test_cmp expect actual\n>>>> +'\n>>>> +\n>>>> +test_expect_success 'replay works correctly with bare repositories' '\n>>>> +    # Test atomic behavior in bare repo (important for Gitaly)\n>>>> +    git checkout -b bare-test topic1 &&\n>>>> +    test_commit BareTest &&\n>>>> +\n>>>> +    # Test with bare repo - replay the commits from \n>>>> main..bare-test to get the full history\n>>>> +    git -C bare fetch .. bare-test:bare-test &&\n>>>> +    git -C bare replay --onto main main..bare-test &&\n>>>> +\n>>>> +    # Verify the bare repo was updated correctly (no output)\n>>>> +    git -C bare log --format=%s bare-test >actual &&\n>>>> +    test_write_lines BareTest F C M L B A >expect &&\n>>>> +    test_cmp expect actual\n>>>> +'\n>>>> +\n>>>> +test_expect_success 'replay --allow-partial with no failures \n>>>> produces no output' '\n>>>> +    git checkout -b partial-test topic1 &&\n>>>> +    test_commit PartialTest &&\n>>>> +\n>>>> +    # Should succeed silently even with partial mode\n>>>> +    git replay --allow-partial --onto main topic1..partial-test \n>>>> >output &&\n>>>> +    test_must_be_empty output\n>>>> +'\n>>>> +\n>>>> +test_expect_success 'replay maintains ref update consistency' '\n>>>> +    # Test that traditional vs atomic produce equivalent results\n>>>> +    git checkout -b method1-test topic2 &&\n>>>> +    git checkout -b method2-test topic2 &&\n>>>> +\n>>>> +    # Both methods should update refs to point to the same \n>>>> replayed commits\n>>>> +    git replay --output-commands --onto main topic1..method1-test \n>>>> >update-commands &&\n>>>> +    git update-ref --stdin <update-commands &&\n>>>> +    git log --format=%s method1-test >traditional-result &&\n>>>> +\n>>>> +    # Direct atomic method should produce same commit history\n>>>> +    git replay --onto main topic1..method2-test &&\n>>>> +    git log --format=%s method2-test >atomic-result &&\n>>>> +\n>>>> +    # Both methods should produce identical commit histories\n>>>> +    test_cmp traditional-result atomic-result\n>>>> +'\n>>>> +\n>>>> +test_expect_success 'replay error messages are helpful and clear' '\n>>>> +    # Test that error messages are clear\n>>>> +    test_must_fail git replay --output-commands --allow-partial -- \n>>>> onto main topic1..topic2 2>error &&\n>>>> +    grep \"cannot be used together\" error\n>>>> +'\n>>>> +\n>>>> +test_expect_success 'replay with empty range produces no output \n>>>> and no changes' '\n>>>> +    # Create a test branch for empty range testing\n>>>> +    git checkout -b empty-test topic1 &&\n>>>> +    git rev-parse empty-test >empty-test-before &&\n>>>> +\n>>>> +    # Empty range should succeed but do nothing\n>>>> +    git replay --onto main empty-test..empty-test >output &&\n>>>> +    test_must_be_empty output &&\n>>>> +\n>>>> +    # Branch should be unchanged\n>>>> +    git rev-parse empty-test >empty-test-after &&\n>>>> +    test_cmp empty-test-before empty-test-after\n>>>> +'\n>>>> +\n>>>>   test_done\n>>>\n>\n"},{"id":"528315","messageId":"CABPp-BFHiwTwNmk3DHSQsXocYYbcaQV8TfVs052v9xFE2NYjWA@mail.gmail.com","threadId":"64109","inReplyTo":"38742a2f-5c5b-48f8-a9fd-acea47b7ce71@gmail.com","subject":"Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-10-08T20:56:35Z","receivedAt":"2025-10-08T20:56:47Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Oct 8, 2025 at 1:02 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> On 04/10/25 00:35, Kristoffer Haugsbakk wrote:\n> > Good evening Siddharth\n> >\n[...]\n> > I have been using git-rebase(1) for a while with a post-rewrite script.\n> > This is used for interactive rebases but also just keeping up with\n> > upstream, i.e. a regular rebase.  Then I was idly thinking that\n> > git-replay(1) would be faster for the plain rebase case—but it doesn’t\n> > support that hook directly.  Okay, but I can get around that: I can\n> > parse the output, yank the commit OIDs, and run git-rev-list(1) on both\n> > of them to get the mapping I want.  But it would be really nice to just\n> > declare the correct post-rewrite format and be done, without having to\n> > parse anything. :)\n>\n>\n> Ah, that's a concrete use case! You are using post-rewrite hooks with\n> rebase and want git replay to support that workflow without needing to\n> parse output.\n>\n> That makes sense for the client-side evolution of the command. Right now\n> the focus is server-side where hooks aren't typically needed, but as this\n> moves toward replacing interactive rebase, proper hook support (including\n> post-rewrite) will be essential.\n>\n> I think --format with atoms would work well for that - you could get\n> exactly the format post-rewrite expects without parsing. For now I'll keep\n> the simple update-ref format, but this is good motivation for adding\n> --format support when we tackle the client-side features.\n>\n> Thanks for the concrete example!\n\nLet's be *very* careful before we add any hooks to replay.\npre-rebase, for example, forced the assumption of only one ref being\ninvolved.  The early implementation of rebase as a shell script on top\nof other commands forced assumptions that it played with pre-commit,\npost-commit, and post-checkout, and forces us today to continue to\ncheck out every intermediate commit to the working copy even when the\nrebase could otherwise be done entirely in-memory without touching the\nindex or working copy.  post-rewrite seems more sane than most other\nhooks, but I still want to avoid painting ourselves into a corner, and\nhooks are very much about defined and established APIs through which\nwe communicate to other processes, which means it's exactly the kind\nof thing that could paint us into a corner.  We'll probably want that\nkind of extensibility eventually, but it's way too early right now.\n"},{"id":"528316","messageId":"xmqq4is9f6tv.fsf@gitster.g","threadId":"64109","inReplyTo":"ea7aa170-400c-47fa-b3f0-2623fcbfcaea@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-08T20:59:24Z","receivedAt":"2025-10-08T20:59:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> On 04/10/25 02:02, Junio C Hamano wrote:\n>> Elijah Newren <newren@gmail.com> writes:\n>>\n>>>> For naming, I am thinking either:\n>>>>     - replay.updateRefs (boolean: true = update, false = output-commands)\n>>>>     - replay.defaultOutput (string: \"update\" | \"commands\")\n>>>>\n>>>> The boolean feels simpler, but the string might be more extensible if we\n>>>> add other output modes later. Which pattern feels more consistent with\n>>>> existing Git config conventions? Looking at rebase.* they're mostly\n>>>> boolean toggles, but am I missing a better example to follow?\n>>> replay.updateRefs sounds better to me.  defaultOutput with \"update\"\n>>> doesn't make sense to me.\n>> Yup.  Or \"replay.defaultAction = (update-ref | show-comamnds)\" if we\n>> anticipate that we might have a third option someday.  That would of\n>> course affect the choice of the command line option.\n>\n>\n> That's interesting. Between:\n> - replay.updateRefs (boolean)\n> - replay.defaultAction (enum string)\n>\n> The enum is more extensible, but do we actually anticipate other modes?\n> Elijah's --format idea from Kristoffer might be a third mode eventually,\n> but that seems far off.\n\nWhat do you exactly mean \"far off\"?  If it won't happen in 2 weeks,\nbut it is likely to come in 2 years, then making sure we have smooth\nupgrade paths is still valuable.  Once you start with \"do we update\nrefs?\" boolean, how would you later accomodate the third option?\n\nNo matter what you do then, the end result would be an awkward \"if\nyou want the command to update the refs, set this Boolean to true,\nif you want the command to show what would happen in the output,\nset this _OTHER_ configuration option to this string, or you can set\nthis yet another variable to cause this different action to happen.\"\n"},{"id":"528317","messageId":"CABPp-BHyKM9hVvTiPx=n9HzO7Mf9oHrJvWcvVi+HxxMXWqMekA@mail.gmail.com","threadId":"64109","inReplyTo":"1bfffc20-7e25-4633-a0b8-6660913a74dd@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-10-08T20:59:55Z","receivedAt":"2025-10-08T21:00:08Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Oct 8, 2025 at 1:09 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> On 08/10/25 19:31, Phillip Wood wrote:\n> > Hi Siddharth\n> >\n> > On 02/10/2025 23:20, Siddharth Asthana wrote:\n> >> On 30/09/25 15:35, Phillip Wood wrote:\n> >>> On 27/09/2025 00:08, Siddharth Asthana wrote:\n> >>>> The git replay command currently outputs update commands that must be\n> >>>> piped to git update-ref --stdin to actually update references:\n> >>\n> >> The actual advantages of the new default aren't about atomicity (that\n> >> already exists), but rather:\n> >> - Eliminating the pipeline for the common case\n> >> - Better ergonomics for users who just want refs updated\n> >> - Simpler server-side automation\n> >>\n> >> I will rewrite the commit message to accurately reflect this. Elijah\n> >> provided a good suggested structure that captures the real trade-offs\n> >> without false claims.\n> >\n> > That's great. I agree that having replay update the refs itself is a\n> > useful improvement.\n> >\n> >>>> +--allow-partial::\n> >>>> +    Allow some ref updates to succeed even if others fail. By\n> >>>> default,\n> >>>> +    ref updates are atomic (all succeed or all fail). With this\n> >>>> option,\n> >>>> +    failed updates are reported as warnings rather than causing\n> >>>> the entire\n> >>>> +    command to fail. The command exits with code 0 only if all\n> >>>> updates\n> >>>> +    succeed; any failures result in exit code 1. Cannot be used with\n> >>>> +    `--output-commands`.\n> >>>\n> >>> Rather than having two incompatible options perhaps we could have a\n> >>> single \"--update-refs=(yes|print|allow-partial-updates)\" argument. I\n> >>> think the name \"--allow-partial\" is rather ambiguous as it does not\n> >>> say what it is allowing to be partial.\n> >>\n> >> After thinking about this and Elijah's feedback, I am leaning toward\n> >> dropping --allow-partial entirely since I don't have a concrete use case\n> >> for it. That simplifies things to just: default atomic updates vs\n> >> --output-commands for the traditional pipeline.\n> >>\n> >> Would you still prefer a --update-refs=<mode> style, or is the simpler\n> >> --output-commands flag sufficient given that --allow-partial is going\n> >> away?\n> >\n> > The advantage of --update-refs=<mode> is that it allows for future\n> > extensions such as adding support for partial in a way that does not\n> > add conflicting options.\n>\n>\n> That's a good point about extensibility. Elijah suggested\n> --[no-]update-refs\n> which is simpler but less extensible.\n>\n> Between:\n> - --[no-]update-refs (simple, covers current needs)\n> - --update-refs=<mode> (extensible for future modes)\n>\n> I am inclined toward the simpler --[no-]update-refs for now since we don't\n> have concrete plans for other modes. But if you think the extensibility is\n> important, I can go with the =<mode> style. What do you think?\n\nI like Phillip's suggestion more than my own.\n"},{"id":"528319","messageId":"ca63e3a7-6b39-4e34-9ebb-a817971737b1@gmail.com","threadId":"64109","inReplyTo":"xmqq4is9f6tv.fsf@gitster.g","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-08T21:10:04Z","receivedAt":"2025-10-08T21:10:11Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 09/10/25 02:29, Junio C Hamano wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>> On 04/10/25 02:02, Junio C Hamano wrote:\n>>> Elijah Newren <newren@gmail.com> writes:\n>>>\n>>>>> For naming, I am thinking either:\n>>>>>      - replay.updateRefs (boolean: true = update, false = output-commands)\n>>>>>      - replay.defaultOutput (string: \"update\" | \"commands\")\n>>>>>\n>>>>> The boolean feels simpler, but the string might be more extensible if we\n>>>>> add other output modes later. Which pattern feels more consistent with\n>>>>> existing Git config conventions? Looking at rebase.* they're mostly\n>>>>> boolean toggles, but am I missing a better example to follow?\n>>>> replay.updateRefs sounds better to me.  defaultOutput with \"update\"\n>>>> doesn't make sense to me.\n>>> Yup.  Or \"replay.defaultAction = (update-ref | show-comamnds)\" if we\n>>> anticipate that we might have a third option someday.  That would of\n>>> course affect the choice of the command line option.\n>>\n>> That's interesting. Between:\n>> - replay.updateRefs (boolean)\n>> - replay.defaultAction (enum string)\n>>\n>> The enum is more extensible, but do we actually anticipate other modes?\n>> Elijah's --format idea from Kristoffer might be a third mode eventually,\n>> but that seems far off.\n> What do you exactly mean \"far off\"?  If it won't happen in 2 weeks,\n> but it is likely to come in 2 years, then making sure we have smooth\n> upgrade paths is still valuable.  Once you start with \"do we update\n> refs?\" boolean, how would you later accomodate the third option?\n\n\nYou are right - I wasn't thinking about the upgrade path properly.\n\nLooking at Kristoffer's post-rewrite hook use case --format support seems\nlikely within a reasonable timeframe. And you are absolutely right that\nstarting with a boolean creates an awkward situation: we would end up with\nreplay.updateRefs (boolean) plus replay.outputFormat (string) or something\nsimilarly messy.\n\nThe enum approach is cleaner:\n\n   replay.defaultAction = update-refs | show-commands | format\n\nThis keeps one config variable handling all output modes. When --format\ngets added, it's just another value, not a new config.\n\n\nFor the command line, I'm thinking --update-refs=<mode> makes the most\nsense. It's specific enough to be clear but general enough to handle the\nthree modes. The slight inconsistency with the config name\n(defaultAction vs updateRefs) seems acceptable since the command line is\nabout *what* to do with refs, while the config is about the broader action.\n\nDoes that reasoning make sense?\n\n\n>\n> No matter what you do then, the end result would be an awkward \"if\n> you want the command to update the refs, set this Boolean to true,\n> if you want the command to show what would happen in the output,\n> set this _OTHER_ configuration option to this string, or you can set\n> this yet another variable to cause this different action to happen.\"\n"},{"id":"528321","messageId":"5307ed25-b041-4a68-ad75-466f63851b01@gmail.com","threadId":"64109","inReplyTo":"CABPp-BHyKM9hVvTiPx=n9HzO7Mf9oHrJvWcvVi+HxxMXWqMekA@mail.gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-08T21:16:25Z","receivedAt":"2025-10-08T21:16:32Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 09/10/25 02:29, Elijah Newren wrote:\n> On Wed, Oct 8, 2025 at 1:09 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> On 08/10/25 19:31, Phillip Wood wrote:\n>>> Hi Siddharth\n>>>\n>>> On 02/10/2025 23:20, Siddharth Asthana wrote:\n>>>> On 30/09/25 15:35, Phillip Wood wrote:\n>>>>> On 27/09/2025 00:08, Siddharth Asthana wrote:\n>>>>>> The git replay command currently outputs update commands that must be\n>>>>>> piped to git update-ref --stdin to actually update references:\n>>>> The actual advantages of the new default aren't about atomicity (that\n>>>> already exists), but rather:\n>>>> - Eliminating the pipeline for the common case\n>>>> - Better ergonomics for users who just want refs updated\n>>>> - Simpler server-side automation\n>>>>\n>>>> I will rewrite the commit message to accurately reflect this. Elijah\n>>>> provided a good suggested structure that captures the real trade-offs\n>>>> without false claims.\n>>> That's great. I agree that having replay update the refs itself is a\n>>> useful improvement.\n>>>\n>>>>>> +--allow-partial::\n>>>>>> +    Allow some ref updates to succeed even if others fail. By\n>>>>>> default,\n>>>>>> +    ref updates are atomic (all succeed or all fail). With this\n>>>>>> option,\n>>>>>> +    failed updates are reported as warnings rather than causing\n>>>>>> the entire\n>>>>>> +    command to fail. The command exits with code 0 only if all\n>>>>>> updates\n>>>>>> +    succeed; any failures result in exit code 1. Cannot be used with\n>>>>>> +    `--output-commands`.\n>>>>> Rather than having two incompatible options perhaps we could have a\n>>>>> single \"--update-refs=(yes|print|allow-partial-updates)\" argument. I\n>>>>> think the name \"--allow-partial\" is rather ambiguous as it does not\n>>>>> say what it is allowing to be partial.\n>>>> After thinking about this and Elijah's feedback, I am leaning toward\n>>>> dropping --allow-partial entirely since I don't have a concrete use case\n>>>> for it. That simplifies things to just: default atomic updates vs\n>>>> --output-commands for the traditional pipeline.\n>>>>\n>>>> Would you still prefer a --update-refs=<mode> style, or is the simpler\n>>>> --output-commands flag sufficient given that --allow-partial is going\n>>>> away?\n>>> The advantage of --update-refs=<mode> is that it allows for future\n>>> extensions such as adding support for partial in a way that does not\n>>> add conflicting options.\n>>\n>> That's a good point about extensibility. Elijah suggested\n>> --[no-]update-refs\n>> which is simpler but less extensible.\n>>\n>> Between:\n>> - --[no-]update-refs (simple, covers current needs)\n>> - --update-refs=<mode> (extensible for future modes)\n>>\n>> I am inclined toward the simpler --[no-]update-refs for now since we don't\n>> have concrete plans for other modes. But if you think the extensibility is\n>> important, I can go with the =<mode> style. What do you think?\n> I like Phillip's suggestion more than my own.\n\n\nGot it. I will go with --update-refs=<mode> then:\n- --update-refs=yes (or just --update-refs as shorthand): atomic updates\n- --update-refs=print: output commands\n- (future) --update-refs=allow-partial or other modes\n\nThis keeps the design extensible without adding conflicting options later.\n\nFor the config, I will use replay.updateRefs with string values matching \nthe\ncommand line modes. That keeps them consistent.\n\n"},{"id":"528322","messageId":"84715a9a-f1d2-4baf-a025-46490052a27b@app.fastmail.com","threadId":"64109","inReplyTo":"CABPp-BFHiwTwNmk3DHSQsXocYYbcaQV8TfVs052v9xFE2NYjWA@mail.gmail.com","subject":"Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2025-10-08T21:16:47Z","receivedAt":"2025-10-08T21:17:09Z","isPatch":true,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"On Wed, Oct 8, 2025, at 22:56, Elijah Newren wrote:\n> On Wed, Oct 8, 2025 at 1:02 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>>\n>> On 04/10/25 00:35, Kristoffer Haugsbakk wrote:\n>> > Good evening Siddharth\n>> >\n> [...]\n>> > I have been using git-rebase(1) for a while with a post-rewrite script.\n>> > This is used for interactive rebases but also just keeping up with\n>> > upstream, i.e. a regular rebase.  Then I was idly thinking that\n>> > git-replay(1) would be faster for the plain rebase case—but it doesn’t\n>> > support that hook directly.  Okay, but I can get around that: I can\n>> > parse the output, yank the commit OIDs, and run git-rev-list(1) on both\n>> > of them to get the mapping I want.  But it would be really nice to just\n>> > declare the correct post-rewrite format and be done, without having to\n>> > parse anything. :)\n>>\n>>\n>> Ah, that's a concrete use case! You are using post-rewrite hooks with\n>> rebase and want git replay to support that workflow without needing to\n>> parse output.\n>>\n>> That makes sense for the client-side evolution of the command. Right now\n>> the focus is server-side where hooks aren't typically needed, but as this\n>> moves toward replacing interactive rebase, proper hook support (including\n>> post-rewrite) will be essential.\n>>\n>> I think --format with atoms would work well for that - you could get\n>> exactly the format post-rewrite expects without parsing. For now I'll keep\n>> the simple update-ref format, but this is good motivation for adding\n>> --format support when we tackle the client-side features.\n>>\n>> Thanks for the concrete example!\n>\n> Let's be *very* careful before we add any hooks to replay.\n\nThe hypothetical case I was talking about was using custom formatting\noutput to drive git-hook(1). Not adding anything hook-related to\ngit-replay(1).\n"},{"id":"528323","messageId":"c49fa739-9007-47f9-9914-9403937a47b4@gmail.com","threadId":"64109","inReplyTo":"CABPp-BFHiwTwNmk3DHSQsXocYYbcaQV8TfVs052v9xFE2NYjWA@mail.gmail.com","subject":"Re: [PATCH v2 0/1] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-08T21:18:46Z","receivedAt":"2025-10-08T21:18:52Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 09/10/25 02:26, Elijah Newren wrote:\n> On Wed, Oct 8, 2025 at 1:02 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> On 04/10/25 00:35, Kristoffer Haugsbakk wrote:\n>>> Good evening Siddharth\n>>>\n> [...]\n>>> I have been using git-rebase(1) for a while with a post-rewrite script.\n>>> This is used for interactive rebases but also just keeping up with\n>>> upstream, i.e. a regular rebase.  Then I was idly thinking that\n>>> git-replay(1) would be faster for the plain rebase case—but it doesn’t\n>>> support that hook directly.  Okay, but I can get around that: I can\n>>> parse the output, yank the commit OIDs, and run git-rev-list(1) on both\n>>> of them to get the mapping I want.  But it would be really nice to just\n>>> declare the correct post-rewrite format and be done, without having to\n>>> parse anything. :)\n>>\n>> Ah, that's a concrete use case! You are using post-rewrite hooks with\n>> rebase and want git replay to support that workflow without needing to\n>> parse output.\n>>\n>> That makes sense for the client-side evolution of the command. Right now\n>> the focus is server-side where hooks aren't typically needed, but as this\n>> moves toward replacing interactive rebase, proper hook support (including\n>> post-rewrite) will be essential.\n>>\n>> I think --format with atoms would work well for that - you could get\n>> exactly the format post-rewrite expects without parsing. For now I'll keep\n>> the simple update-ref format, but this is good motivation for adding\n>> --format support when we tackle the client-side features.\n>>\n>> Thanks for the concrete example!\n> Let's be *very* careful before we add any hooks to replay.\n> pre-rebase, for example, forced the assumption of only one ref being\n> involved.  The early implementation of rebase as a shell script on top\n> of other commands forced assumptions that it played with pre-commit,\n> post-commit, and post-checkout, and forces us today to continue to\n> check out every intermediate commit to the working copy even when the\n> rebase could otherwise be done entirely in-memory without touching the\n> index or working copy.  post-rewrite seems more sane than most other\n> hooks, but I still want to avoid painting ourselves into a corner, and\n> hooks are very much about defined and established APIs through which\n> we communicate to other processes, which means it's exactly the kind\n> of thing that could paint us into a corner.  We'll probably want that\n> kind of extensibility eventually, but it's way too early right now.\n\n\nThat's a really important point. I wasn't thinking about how hooks lock\nin API decisions.\n\nFor this series, I will stay completely away from hooks. The --format\ndiscussion with Kristoffer is interesting for future work, but you are\nright that it's way too early. We need to understand the client-side use\ncases much better before committing to any hook interfaces.\n\nI will keep the focus narrow: just making ref updates the default with a\nclean way to get the old behavior.\n\n"},{"id":"528327","messageId":"CABPp-BEuK1MWxmRc-a=1aPqLbXEZVF0qgHtYv_Z-1mXm3Ag5_w@mail.gmail.com","threadId":"64109","inReplyTo":"ea7aa170-400c-47fa-b3f0-2623fcbfcaea@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-10-08T21:30:30Z","receivedAt":"2025-10-08T21:30:42Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Oct 8, 2025 at 1:06 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n[...]\n> Elijah's --format idea from Kristoffer might be a third mode eventually\n\n?\n\nThat was in no way my idea; it was all Kristoffer's.\n\nThe --format idea might eventually be useful, but to me the --format\nthing seems like something you'd add to a very stable command, like\nfor-each-ref.  I think it wouldn't make sense to add to something like\nreplay in its current state since replay is \"four times more\nexperimental than any other command\"[1], where we're changing the\nbasic output format, where we bail on any conflicts or merge commits,\nwhere we recently discussed whether to support first-class conflicts\nand using that for handling conflicts instead of halting upon the\nfirst one like rebase does, etc.  I feel much the same as when a\nsimilar flag was suggested for merge-tree (in order to let users\ncontrol the exact output layout of the information it was already\nprinting, when it was known that the information provided was\ninsufficient to solve the user's problems)[2] -- it's premature.\n\n[1] that might be slightly over the top, but it's still a fun\n\"statistic\" of sorts -- see\nhttps://lore.kernel.org/git/CABPp-BHWjyRv_f_HKkz10Q_cOZKPvpgf=SEUR1ThmbttkQT+Uw@mail.gmail.com/\n[2] https://lore.kernel.org/git/CABPp-BG25_TutatgNmK6vgq3akxpYHQ8QBnz-65_F_3oCA1nJA@mail.gmail.com/\n"},{"id":"528378","messageId":"f6f3ca21-8894-4477-9a00-600cfa53a2ae@gmail.com","threadId":"64109","inReplyTo":"1bfffc20-7e25-4633-a0b8-6660913a74dd@gmail.com","subject":"Re: [PATCH v2 1/1] replay: make atomic ref updates the default behavior","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-10-09T09:40:23Z","receivedAt":"2025-10-09T09:40:30Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 08/10/2025 21:09, Siddharth Asthana wrote:\n> On 08/10/25 19:31, Phillip Wood wrote:\n>> Hi Siddharth\n>> On 02/10/2025 23:20, Siddharth Asthana wrote:\n>>> Would you still prefer a --update-refs=<mode> style, or is the simpler\n>>> --output-commands flag sufficient given that --allow-partial is going \n>>> away?\n>>\n>> The advantage of --update-refs=<mode> is that it allows for future \n>> extensions such as adding support for partial in a way that does not \n>> add conflicting options.\n> \n> That's a good point about extensibility. Elijah suggested --[no-]update- \n> refs\n> which is simpler but less extensible.\n> \n> Between:\n> - --[no-]update-refs (simple, covers current needs)\n> - --update-refs=<mode> (extensible for future modes)\n> \n> I am inclined toward the simpler --[no-]update-refs for now since we don't\n> have concrete plans for other modes. But if you think the extensibility is\n> important, I can go with the =<mode> style. What do you think?\n\nIf we go with a boolean flag we can always add an optional argument in \nthe future so I think that would be fine.\n\nThanks\n\nPhillip\n\n"},{"id":"528654","messageId":"20251013183311.33329-1-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20250926230838.35870-1-siddharthasthana31@gmail.com","subject":"[PATCH v3 0/3] replay: make atomic ref updates the default","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-13T18:33:08Z","receivedAt":"2025-10-13T18:33:22Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"This is v3 of the git-replay atomic updates series.\n\nBased on feedback from v2, this version simplifies the API and improves\nextensibility. Thanks to Elijah, Phillip, Christian, Junio, and Karthik\nfor the detailed reviews that shaped this version.\n\n## Changes Since v2\n\n**Removed --allow-partial option**\n\nAfter discussion with Elijah and Junio, we couldn't identify a concrete\nuse case for partial failure tolerance. The traditional pipeline with\ngit-update-ref already provides partial update capabilities when needed\nthrough its transaction commands. Removing this option simplifies the API\nand avoids committing to behavior without clear real-world use cases.\n\n**Changed to --update-refs=<mode> for extensibility**\n\nPhillip suggested that separate boolean flags (--output-commands,\n--allow-partial) were limiting for future expansion. The --update-refs=<mode>\ndesign allows future modes without option proliferation:\n  - --update-refs=yes (default): atomic ref updates\n  - --update-refs=print: pipeline output\n  - Future modes can be added as additional values\n\nThis API pattern prevents the need for multiple incompatible flags and\nprovides a cleaner interface for users.\n\n**Added replay.defaultAction configuration option**\n\nJunio recommended a config option for users preferring traditional behavior.\nThe implementation uses enum string values for extensibility:\n  - replay.defaultAction = update-refs (default)\n  - replay.defaultAction = show-commands (pipeline output)\n\nThe command-line --update-refs option overrides the config, allowing users\nto set a preference while maintaining per-invocation control. The enum\ndesign (versus a boolean) allows future expansion to additional modes\nwithout requiring new config variables.\n\n**Improved commit messages and patch organization**\n\nChristian and Elijah provided detailed feedback on commit message structure.\nPatch 2 now uses Elijah's suggested format that explains the trade-offs of\nthe current design before proposing changes. The commit messages now focus\non the changes themselves rather than v1→v2 evolution. Added Helped-by\ntrailers to acknowledge specific contributions.\n\n**Enhanced test suite with proper isolation**\n\nFollowing Elijah's suggestions:\n  - Existing tests use --update-refs=print to preserve their behavior\n  - New tests use test_when_finished for proper cleanup\n  - Added real atomicity test using lock files to verify all-or-nothing\n  - Fixed bare repository tests to rebuild expectations independently\n  - Removed weak tests that didn't actually verify atomicity\n\n**Extracted helper function to reduce duplication**\n\nPer Phillip's feedback, added handle_ref_update() helper to eliminate\ncode duplication between print and atomic modes. This function takes a\nmode parameter and handles both cases, making the code more maintainable\nand ensuring both paths stay consistent.\n\n## Technical Implementation\n\nThe atomic ref updates use Git's ref transaction API:\n  - ref_store_transaction_begin() with default atomic behavior\n  - ref_transaction_update() to stage each update\n  - ref_transaction_commit() for atomic application\n\nThe handle_ref_update() helper encapsulates the mode-specific logic,\neither printing update commands or staging them into the transaction.\n\nConfig reading uses repo_config_get_string_tmp() with validation for\n'update-refs' and 'show-commands' values, mapping them to internal\nmodes 'yes' and 'print' respectively.\n\nRange-diff against v2:\n-:  ---------- > 1:  de9cc3fbee replay: use die_for_incompatible_opt2() for option validation\n1:  e3c1a57375 ! 2:  3f4c69d612 replay: make atomic ref updates the default behavior\n    @@ Metadata\n      ## Commit message ##\n         replay: make atomic ref updates the default behavior\n     \n    -    The git replay command currently outputs update commands that must be\n    -    piped to git update-ref --stdin to actually update references:\n    +    The git replay command currently outputs update commands that can be\n    +    piped to update-ref to achieve a rebase, e.g.\n     \n    -        git replay --onto main topic1..topic2 | git update-ref --stdin\n    +      git replay --onto main topic1..topic2 | git update-ref --stdin\n     \n    -    This design has significant limitations for server-side operations. The\n    -    two-command pipeline creates coordination complexity, provides no atomic\n    -    transaction guarantees by default, and complicates automation in bare\n    -    repository environments where git replay is primarily used.\n    +    This separation had advantages for three special cases:\n    +      * it made testing easy (when state isn't modified from one step to\n    +        the next, you don't need to make temporary branches or have undo\n    +        commands, or try to track the changes)\n    +      * it provided a natural can-it-rebase-cleanly (and what would it\n    +        rebase to) capability without automatically updating refs, similar\n    +        to a --dry-run\n    +      * it provided a natural low-level tool for the suite of hash-object,\n    +        mktree, commit-tree, mktag, merge-tree, and update-ref, allowing\n    +        users to have another building block for experimentation and making\n    +        new tools\n     \n    -    During extensive mailing list discussion, multiple maintainers identified\n    -    that the current approach forces users to opt-in to atomic behavior rather\n    -    than defaulting to the safer, more reliable option. Elijah Newren noted\n    -    that the experimental status explicitly allows such behavior changes, while\n    -    Patrick Steinhardt highlighted performance concerns with individual ref\n    -    updates in the reftable backend.\n    +    However, it should be noted that all three of these are somewhat\n    +    special cases; users, whether on the client or server side, would\n    +    almost certainly find it more ergonomical to simply have the updating\n    +    of refs be the default.\n     \n    -    The core issue is that git replay was designed around command output rather\n    -    than direct action. This made sense for a plumbing tool, but creates barriers\n    -    for the primary use case: server-side operations that need reliable, atomic\n    -    ref updates without pipeline complexity.\n    +    For server-side operations in particular, the pipeline architecture\n    +    creates process coordination overhead. Server implementations that need\n    +    to perform rebases atomically must maintain additional code to:\n     \n    -    This patch changes the default behavior to update refs directly using Git's\n    -    ref transaction API:\n    +      1. Spawn and manage a pipeline between git-replay and git-update-ref\n    +      2. Coordinate stdout/stderr streams across the pipe boundary\n    +      3. Handle partial failure states if the pipeline breaks mid-execution\n    +      4. Parse and validate the update-ref command output\n     \n    -        git replay --onto main topic1..topic2\n    -        # No output; all refs updated atomically or none\n    +    Change the default behavior to update refs directly, and atomically (at\n    +    least to the extent supported by the refs backend in use). This\n    +    eliminates the process coordination overhead for the common case.\n     \n    -    The implementation uses ref_store_transaction_begin() with atomic mode by\n    -    default, ensuring all ref updates succeed or all fail as a single operation.\n    -    This leverages git replay's existing server-side strengths (in-memory operation,\n    -    no work tree requirement) while adding the atomic guarantees that server\n    -    operations require.\n    +    For users needing the traditional pipeline workflow, add a new\n    +    `--update-refs=<mode>` option that preserves the original behavior:\n     \n    -    For users needing the traditional pipeline workflow, --output-commands\n    -    preserves the original behavior:\n    +      git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin\n     \n    -        git replay --output-commands --onto main topic1..topic2 | git update-ref --stdin\n    -\n    -    The --allow-partial option enables partial failure tolerance. However, following\n    -    maintainer feedback, it implements a \"strict success\" model: the command exits\n    -    with code 0 only if ALL ref updates succeed, and exits with code 1 if ANY\n    -    updates fail. This ensures that --allow-partial changes error reporting style\n    -    (warnings vs hard errors) but not success criteria, handling edge cases like\n    -    \"no updates needed\" cleanly.\n    +    The mode can be:\n    +      * `yes` (default): Update refs directly using an atomic transaction\n    +      * `print`: Output update-ref commands for pipeline use\n     \n         Implementation details:\n    -    - Empty commit ranges now return success (exit code 0) rather than failure,\n    -      as no commits to replay is a valid successful operation\n    -    - Added comprehensive test coverage with 12 new tests covering atomic behavior,\n    -      option validation, bare repository support, and edge cases\n    -    - Fixed test isolation issues to prevent branch state contamination between tests\n    -    - Maintains C89 compliance and follows Git's established coding conventions\n    -    - Refactored option validation to use die_for_incompatible_opt2() for both\n    -      --advance/--contained and --allow-partial/--output-commands conflicts,\n    -      providing consistent error reporting\n    -    - Fixed --allow-partial exit code behavior to implement \"strict success\" model\n    -      where any ref update failures result in exit code 1, even with partial tolerance\n    -    - Updated documentation with proper line wrapping, consistent terminology using\n    -      \"old default behavior\", performance context, and reorganized examples for clarity\n    -    - Eliminates individual ref updates (refs_update_ref calls) that perform\n    -      poorly with reftable backend\n    -    - Uses only batched ref transactions for optimal performance across all\n    -      ref backends\n    -    - Avoids naming collision with git rebase --update-refs by using distinct\n    -      option names\n    -    - Defaults to atomic behavior while preserving pipeline compatibility\n     \n    -    The result is a command that works better for its primary use case (server-side\n    -    operations) while maintaining full backward compatibility for existing workflows.\n    +    The atomic ref updates are implemented using Git's ref transaction API.\n    +    In cmd_replay(), when not in 'print' mode, we initialize a transaction\n    +    using ref_store_transaction_begin() with the default atomic behavior.\n    +    As commits are replayed, ref updates are staged into the transaction\n    +    using ref_transaction_update(). Finally, ref_transaction_commit()\n    +    applies all updates atomically—either all updates succeed or none do.\n    +\n    +    To avoid code duplication between the 'print' and 'yes' modes, this\n    +    commit extracts a handle_ref_update() helper function. This function\n    +    takes the mode and either prints the update command or stages it into\n    +    the transaction. This keeps both code paths consistent and makes future\n    +    maintenance easier.\n    +\n    +    The helper function signature:\n    +\n    +      static int handle_ref_update(const char *mode,\n    +                                    struct ref_transaction *transaction,\n    +                                    const char *refname,\n    +                                    const struct object_id *new_oid,\n    +                                    const struct object_id *old_oid,\n    +                                    struct strbuf *err)\n    +\n    +    When mode is 'print', it prints the update-ref command. When mode is\n    +    'yes', it calls ref_transaction_update() to stage the update. This\n    +    eliminates the duplication that would otherwise exist at each ref update\n    +    call site.\n    +\n    +    Test suite changes:\n     \n    +    All existing tests that expected command output now use\n    +    `--update-refs=print` to preserve their original behavior. This keeps\n    +    the tests valid while allowing them to verify that the pipeline workflow\n    +    still works correctly.\n    +\n    +    New tests were added to verify:\n    +      - Default atomic behavior (no output, refs updated directly)\n    +      - Bare repository support (server-side use case)\n    +      - Equivalence between traditional pipeline and atomic updates\n    +      - Real atomicity using a lock file to verify all-or-nothing guarantee\n    +      - Test isolation using test_when_finished to clean up state\n    +\n    +    The bare repository tests were fixed to rebuild their expectations\n    +    independently rather than comparing to previous test output, improving\n    +    test reliability and isolation.\n    +\n    +    A following commit will add a `replay.defaultAction` configuration\n    +    option for users who prefer the traditional pipeline output as their\n    +    default behavior.\n    +\n    +    Helped-by: Elijah Newren <newren@gmail.com>\n    +    Helped-by: Patrick Steinhardt <ps@pks.im>\n    +    Helped-by: Christian Couder <christian.couder@gmail.com>\n    +    Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## Documentation/git-replay.adoc ##\n    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne\n      --------\n      [verse]\n     -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n    -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--output-commands | --allow-partial] <revision-range>...\n    ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>)\n    ++\t\t[--update-refs[=<mode>]] <revision-range>...\n      \n      DESCRIPTION\n      -----------\n    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne\n     -the working tree and the index untouched, and updates no references.\n     -The output of this command is meant to be used as input to\n     -`git update-ref --stdin`, which would update the relevant branches\n    --(see the OUTPUT section below).\n    -+the working tree and the index untouched, and by default updates the\n    -+relevant references using atomic transactions. Use `--output-commands`\n    -+to get the old default behavior where update commands that can be piped\n    -+to `git update-ref --stdin` are emitted (see the OUTPUT section below).\n    ++the working tree and the index untouched. By default, updates the\n    ++relevant references using an atomic transaction (all refs update or\n    ++none). Use `--update-refs=print` to avoid automatic ref updates and\n    ++instead get update commands that can be piped to `git update-ref --stdin`\n    + (see the OUTPUT section below).\n      \n      THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n    +@@ Documentation/git-replay.adoc: OPTIONS\n    + \tStarting point at which to create the new commits.  May be any\n    + \tvalid commit, and not just an existing branch name.\n    + +\n    +-When `--onto` is specified, the update-ref command(s) in the output will\n    +-update the branch(es) in the revision range to point at the new\n    +-commits, similar to the way how `git rebase --update-refs` updates\n    +-multiple branches in the affected range.\n    ++When `--onto` is specified, the branch(es) in the revision range will be\n    ++updated to point at the new commits (or update commands will be printed\n    ++if `--update-refs=print` is used), similar to the way how\n    ++`git rebase --update-refs` updates multiple branches in the affected range.\n    + \n    + --advance <branch>::\n    + \tStarting point at which to create the new commits; must be a\n    + \tbranch name.\n    + +\n    +-When `--advance` is specified, the update-ref command(s) in the output\n    +-will update the branch passed as an argument to `--advance` to point at\n    +-the new commits (in other words, this mimics a cherry-pick operation).\n    ++When `--advance` is specified, the branch passed as an argument will be\n    ++updated to point at the new commits (or an update command will be printed\n    ++if `--update-refs=print` is used). This mimics a cherry-pick operation.\n    ++\n    ++--update-refs[=<mode>]::\n    ++\tControl how references are updated. The mode can be:\n    +++\n    ++--\n    ++* `yes` (default): Update refs directly using an atomic transaction.\n    ++  All ref updates succeed or all fail.\n    ++* `print`: Output update-ref commands instead of updating refs.\n    ++  The output can be piped as-is to `git update-ref --stdin`.\n    ++--\n      \n    -@@ Documentation/git-replay.adoc: When `--advance` is specified, the update-ref command(s) in the output\n    - will update the branch passed as an argument to `--advance` to point at\n    - the new commits (in other words, this mimics a cherry-pick operation).\n    - \n    -+--output-commands::\n    -+\tOutput update-ref commands instead of updating refs directly.\n    -+\tWhen this option is used, the output can be piped to `git update-ref --stdin`\n    -+\tfor successive, relatively slow, ref updates. This is equivalent to the\n    -+\told default behavior.\n    -+\n    -+--allow-partial::\n    -+\tAllow some ref updates to succeed even if others fail. By default,\n    -+\tref updates are atomic (all succeed or all fail). With this option,\n    -+\tfailed updates are reported as warnings rather than causing the entire\n    -+\tcommand to fail. The command exits with code 0 only if all updates\n    -+\tsucceed; any failures result in exit code 1. Cannot be used with\n    -+\t`--output-commands`.\n    -+\n      <revision-range>::\n      \tRange of commits to replay. More than one <revision-range> can\n    - \tbe passed, but in `--advance <branch>` mode, they should have\n     @@ Documentation/git-replay.adoc: include::rev-list-options.adoc[]\n      OUTPUT\n      ------\n    @@ Documentation/git-replay.adoc: include::rev-list-options.adoc[]\n     -When there are no conflicts, the output of this command is usable as\n     -input to `git update-ref --stdin`.  It is of the form:\n     +By default, when there are no conflicts, this command updates the relevant\n    -+references using atomic transactions and produces no output. All ref updates\n    -+succeed or all fail (atomic behavior). Use `--allow-partial` to allow some\n    -+updates to succeed while others fail.\n    ++references using an atomic transaction and produces no output. All ref\n    ++updates succeed or all fail.\n     +\n    -+When `--output-commands` is used, the output is usable as input to\n    ++When `--update-refs=print` is used, the output is usable as input to\n     +`git update-ref --stdin`. It is of the form:\n      \n      \tupdate refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n    @@ Documentation/git-replay.adoc: is something other than 0 or 1.\n     +updates mybranch to point at the new commits and the second updates\n     +target to point at them.\n     +\n    -+To get the old default behavior where update commands are emitted:\n    ++To get the traditional pipeline output:\n     +\n     +------------\n    -+$ git replay --output-commands --onto target origin/main..mybranch\n    ++$ git replay --update-refs=print --onto target origin/main..mybranch\n     +update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n    -+------------\n    -+\n    -+To rebase multiple branches with partial failure tolerance:\n    -+\n    -+------------\n    -+$ git replay --allow-partial --contained --onto origin/main origin/main..tipbranch\n     +------------\n      \n      What if you have a stack of branches, one depending upon another, and\n    @@ Documentation/git-replay.adoc: is something other than 0 or 1.\n      \n      ------------\n      $ git replay --contained --onto origin/main origin/main..tipbranch\n    -+------------\n    -+\n    +-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n    +-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n    +-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n    + ------------\n    + \n     +This automatically finds and rebases all branches contained within the\n     +`origin/main..tipbranch` range.\n     +\n    -+Or if you want to see the old default behavior where update commands are emitted:\n    -+\n    -+------------\n    -+$ git replay --output-commands --contained --onto origin/main origin/main..tipbranch\n    - update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n    - update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n    - update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n    -@@ Documentation/git-replay.adoc: update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n    - \n      When calling `git replay`, one does not need to specify a range of\n    - commits to replay using the syntax `A..B`; any range expression will\n    +-commits to replay using the syntax `A..B`; any range expression will\n     -do:\n    -+do. Here's an example where you explicitly specify which branches to rebase:\n    ++commits to replay using the syntax `A..B`; any range expression will do:\n      \n      ------------\n      $ git replay --onto origin/main ^base branch1 branch2 branch3\n    -+------------\n    -+\n    -+This gives you explicit control over exactly which branches are rebased,\n    -+unlike the previous `--contained` example which automatically discovers them.\n    -+\n    -+To see the update commands that would be executed:\n    -+\n    -+------------\n    -+$ git replay --output-commands --onto origin/main ^base branch1 branch2 branch3\n    - update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n    - update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n    - update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n    +-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n    +-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n    +-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n    + ------------\n    + \n    + This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\n     \n      ## builtin/replay.c ##\n     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *repo,\n      \treturn create_commit(repo, result->tree, pickme, replayed_base);\n      }\n      \n    -+static int add_ref_to_transaction(struct ref_transaction *transaction,\n    -+\t\t\t\t  const char *refname,\n    -+\t\t\t\t  const struct object_id *new_oid,\n    -+\t\t\t\t  const struct object_id *old_oid,\n    -+\t\t\t\t  struct strbuf *err)\n    ++static int handle_ref_update(const char *mode,\n    ++\t\t\t     struct ref_transaction *transaction,\n    ++\t\t\t     const char *refname,\n    ++\t\t\t     const struct object_id *new_oid,\n    ++\t\t\t     const struct object_id *old_oid,\n    ++\t\t\t     struct strbuf *err)\n     +{\n    ++\tif (!strcmp(mode, \"print\")) {\n    ++\t\tprintf(\"update %s %s %s\\n\",\n    ++\t\t       refname,\n    ++\t\t       oid_to_hex(new_oid),\n    ++\t\t       oid_to_hex(old_oid));\n    ++\t\treturn 0;\n    ++\t}\n    ++\n    ++\t/* mode == \"yes\" - update refs directly */\n     +\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n     +\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n     +}\n    -+\n    -+static void print_rejected_update(const char *refname,\n    -+\t\t\t\t  const struct object_id *old_oid UNUSED,\n    -+\t\t\t\t  const struct object_id *new_oid UNUSED,\n    -+\t\t\t\t  const char *old_target UNUSED,\n    -+\t\t\t\t  const char *new_target UNUSED,\n    -+\t\t\t\t  enum ref_transaction_error err,\n    -+\t\t\t\t  void *cb_data UNUSED)\n    -+{\n    -+\tconst char *reason = ref_transaction_error_msg(err);\n    -+\twarning(_(\"failed to update %s: %s\"), refname, reason);\n    -+}\n     +\n      int cmd_replay(int argc,\n      \t       const char **argv,\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \tstruct commit *onto = NULL;\n      \tconst char *onto_name = NULL;\n      \tint contained = 0;\n    -+\tint output_commands = 0;\n    -+\tint allow_partial = 0;\n    ++\tconst char *update_refs_mode = NULL;\n      \n      \tstruct rev_info revs;\n      \tstruct commit *last_commit = NULL;\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \tkh_oid_map_t *replayed_commits;\n     +\tstruct ref_transaction *transaction = NULL;\n     +\tstruct strbuf transaction_err = STRBUF_INIT;\n    -+\tint commits_processed = 0;\n      \tint ret = 0;\n      \n     -\tconst char * const replay_usage[] = {\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\tN_(\"(EXPERIMENTAL!) git replay \"\n      \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n     -\t\t   \"<revision-range>...\"),\n    -+\t\t   \"[--output-commands | --allow-partial] <revision-range>...\"),\n    ++\t\t   \"[--update-refs[=<mode>]] <revision-range>...\"),\n      \t\tNULL\n      \t};\n      \tstruct option replay_options[] = {\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\t\t   N_(\"replay onto given commit\")),\n      \t\tOPT_BOOL(0, \"contained\", &contained,\n      \t\t\t N_(\"advance all branches contained in revision-range\")),\n    -+\t\tOPT_BOOL(0, \"output-commands\", &output_commands,\n    -+\t\t\t N_(\"output update commands instead of updating refs\")),\n    -+\t\tOPT_BOOL(0, \"allow-partial\", &allow_partial,\n    -+\t\t\t N_(\"allow some ref updates to succeed even if others fail\")),\n    ++\t\tOPT_STRING(0, \"update-refs\", &update_refs_mode,\n    ++\t\t\t   N_(\"mode\"),\n    ++\t\t\t   N_(\"control ref update behavior (yes|print)\")),\n      \t\tOPT_END()\n      \t};\n      \n     @@ builtin/replay.c: int cmd_replay(int argc,\n    - \t\tusage_with_options(replay_usage, replay_options);\n    - \t}\n    + \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n    + \t\t\t\t  contained, \"--contained\");\n      \n    --\tif (advance_name_opt && contained)\n    --\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n    --\t\t    \"--advance\", \"--contained\");\n    -+\tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n    -+\t\t\t\t  contained, \"--contained\");\n    ++\t/* Set default mode if not specified */\n    ++\tif (!update_refs_mode)\n    ++\t\tupdate_refs_mode = \"yes\";\n     +\n    -+\tdie_for_incompatible_opt2(allow_partial, \"--allow-partial\",\n    -+\t\t\t\t  output_commands, \"--output-commands\");\n    ++\t/* Validate update-refs mode */\n    ++\tif (strcmp(update_refs_mode, \"yes\") && strcmp(update_refs_mode, \"print\"))\n    ++\t\tdie(_(\"invalid value for --update-refs: '%s' (expected 'yes' or 'print')\"),\n    ++\t\t    update_refs_mode);\n     +\n      \tadvance_name = xstrdup_or_null(advance_name_opt);\n      \n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n      \t\t\t      &onto, &update_refs);\n      \n    -+\tif (!output_commands) {\n    -+\t\tunsigned int transaction_flags = allow_partial ? REF_TRANSACTION_ALLOW_FAILURE : 0;\n    ++\t/* Initialize ref transaction if we're updating refs directly */\n    ++\tif (!strcmp(update_refs_mode, \"yes\")) {\n     +\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n    -+\t\t\t\t\t\t\t  transaction_flags,\n    -+\t\t\t\t\t\t\t  &transaction_err);\n    ++\t\t\t\t\t\t\t  0, &transaction_err);\n     +\t\tif (!transaction) {\n    -+\t\t\tret = error(_(\"failed to begin ref transaction: %s\"), transaction_err.buf);\n    ++\t\t\tret = error(_(\"failed to begin ref transaction: %s\"),\n    ++\t\t\t\t    transaction_err.buf);\n     +\t\t\tgoto cleanup;\n     +\t\t}\n     +\t}\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \tif (!onto) /* FIXME: Should handle replaying down to root commit */\n      \t\tdie(\"Replaying down to root commit is not supported yet!\");\n      \n    -@@ builtin/replay.c: int cmd_replay(int argc,\n    - \t\tkhint_t pos;\n    - \t\tint hr;\n    - \n    -+\t\tcommits_processed = 1;\n    -+\n    - \t\tif (!commit->parents)\n    - \t\t\tdie(_(\"replaying down to root commit is not supported yet!\"));\n    - \t\tif (commit->parents->next)\n     @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n      \t\t\t    (contained || strset_contains(update_refs,\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     -\t\t\t\t       decoration->name,\n     -\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n     -\t\t\t\t       oid_to_hex(&commit->object.oid));\n    -+\t\t\t\tif (output_commands) {\n    -+\t\t\t\t\tprintf(\"update %s %s %s\\n\",\n    -+\t\t\t\t\t       decoration->name,\n    -+\t\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n    -+\t\t\t\t\t       oid_to_hex(&commit->object.oid));\n    -+\t\t\t\t} else if (add_ref_to_transaction(transaction, decoration->name,\n    -+\t\t\t\t\t\t\t\t  &last_commit->object.oid,\n    -+\t\t\t\t\t\t\t\t  &commit->object.oid,\n    -+\t\t\t\t\t\t\t\t  &transaction_err) < 0) {\n    -+\t\t\t\t\tret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n    ++\t\t\t\tif (handle_ref_update(update_refs_mode, transaction,\n    ++\t\t\t\t\t\t      decoration->name,\n    ++\t\t\t\t\t\t      &last_commit->object.oid,\n    ++\t\t\t\t\t\t      &commit->object.oid,\n    ++\t\t\t\t\t\t      &transaction_err) < 0) {\n    ++\t\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n    ++\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n     +\t\t\t\t\tgoto cleanup;\n     +\t\t\t\t}\n      \t\t\t}\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     -\t\t       advance_name,\n     -\t\t       oid_to_hex(&last_commit->object.oid),\n     -\t\t       oid_to_hex(&onto->object.oid));\n    -+\t\tif (output_commands) {\n    -+\t\t\tprintf(\"update %s %s %s\\n\",\n    -+\t\t\t       advance_name,\n    -+\t\t\t       oid_to_hex(&last_commit->object.oid),\n    -+\t\t\t       oid_to_hex(&onto->object.oid));\n    -+\t\t} else if (add_ref_to_transaction(transaction, advance_name,\n    -+\t\t\t\t\t\t  &last_commit->object.oid,\n    -+\t\t\t\t\t\t  &onto->object.oid,\n    -+\t\t\t\t\t\t  &transaction_err) < 0) {\n    -+\t\t\tret = error(_(\"failed to add ref update to transaction: %s\"), transaction_err.buf);\n    ++\t\tif (handle_ref_update(update_refs_mode, transaction,\n    ++\t\t\t\t      advance_name,\n    ++\t\t\t\t      &last_commit->object.oid,\n    ++\t\t\t\t      &onto->object.oid,\n    ++\t\t\t\t      &transaction_err) < 0) {\n    ++\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n    ++\t\t\t\t    advance_name, transaction_err.buf);\n     +\t\t\tgoto cleanup;\n     +\t\t}\n     +\t}\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     +\t/* Commit the ref transaction if we have one */\n     +\tif (transaction && result.clean == 1) {\n     +\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n    -+\t\t\tif (allow_partial) {\n    -+\t\t\t\twarning(_(\"some ref updates failed: %s\"), transaction_err.buf);\n    -+\t\t\t\tref_transaction_for_each_rejected_update(transaction,\n    -+\t\t\t\t\t\t\t\t\t print_rejected_update, NULL);\n    -+\t\t\t\tret = 0; /* Set failure even with allow_partial */\n    -+\t\t\t} else {\n    -+\t\t\t\tret = error(_(\"failed to update refs: %s\"), transaction_err.buf);\n    -+\t\t\t\tgoto cleanup;\n    -+\t\t\t}\n    ++\t\t\tret = error(_(\"failed to commit ref transaction: %s\"),\n    ++\t\t\t\t    transaction_err.buf);\n    ++\t\t\tgoto cleanup;\n     +\t\t}\n      \t}\n      \n      \tmerge_finalize(&merge_opt, &result);\n     @@ builtin/replay.c: int cmd_replay(int argc,\n    - \t\tstrset_clear(update_refs);\n    - \t\tfree(update_refs);\n    - \t}\n    --\tret = result.clean;\n    -+\n    -+\t/* Handle empty ranges: if no commits were processed, treat as success */\n    -+\tif (!commits_processed)\n    -+\t\tret = 1; /* Success - no commits to replay is not an error */\n    -+\telse\n    -+\t\tret = result.clean;\n    + \tret = result.clean;\n      \n      cleanup:\n     +\tif (transaction)\n    @@ t/t3650-replay-basics.sh: test_expect_success 'setup bare' '\n      \n      test_expect_success 'using replay to rebase two branches, one on top of other' '\n     -\tgit replay --onto main topic1..topic2 >result &&\n    -+\tgit replay --output-commands --onto main topic1..topic2 >result &&\n    ++\tgit replay --update-refs=print --onto main topic1..topic2 >result &&\n      \n      \ttest_line_count = 1 result &&\n      \n    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to rebase two branch\n      '\n      \n     +test_expect_success 'using replay with default atomic behavior (no output)' '\n    -+\t# Create a test branch that wont interfere with others\n    -+\tgit branch atomic-test topic2 &&\n    -+\tgit rev-parse atomic-test >atomic-test-old &&\n    ++\t# Store the original state\n    ++\tSTART=$(git rev-parse topic2) &&\n    ++\ttest_when_finished \"git branch -f topic2 $START\" &&\n     +\n     +\t# Default behavior: atomic ref updates (no output)\n    -+\tgit replay --onto main topic1..atomic-test >output &&\n    ++\tgit replay --onto main topic1..topic2 >output &&\n     +\ttest_must_be_empty output &&\n     +\n    -+\t# Verify the branch was updated\n    -+\tgit rev-parse atomic-test >atomic-test-new &&\n    -+\t! test_cmp atomic-test-old atomic-test-new &&\n    -+\n     +\t# Verify the history is correct\n    -+\tgit log --format=%s atomic-test >actual &&\n    ++\tgit log --format=%s topic2 >actual &&\n     +\ttest_write_lines E D M L B A >expect &&\n     +\ttest_cmp expect actual\n     +'\n     +\n      test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n     -\tgit -C bare replay --onto main topic1..topic2 >result-bare &&\n    --\ttest_cmp expect result-bare\n    -+\tgit -C bare replay --output-commands --onto main topic1..topic2 >result-bare &&\n    ++\tgit -C bare replay --update-refs=print --onto main topic1..topic2 >result-bare &&\n    ++\n    ++\ttest_line_count = 1 result-bare &&\n    ++\n    ++\tgit log --format=%s $(cut -f 3 -d \" \" result-bare) >actual &&\n    ++\ttest_write_lines E D M L B A >expect &&\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\tprintf \"update refs/heads/topic2 \" >expect &&\n    ++\tprintf \"%s \" $(cut -f 3 -d \" \" result-bare) >>expect &&\n    ++\tgit -C bare rev-parse topic2 >>expect &&\n     +\n    -+\t# The result should match what we got from the regular repo\n    -+\ttest_cmp result result-bare\n    + \ttest_cmp expect result-bare\n      '\n      \n    - test_expect_success 'using replay to rebase with a conflict' '\n     @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to perform basic cherry-pick' '\n      \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n      \t# 4th field of result is hash for main instead of hash for topic2\n      \n     -\tgit replay --advance main topic1..topic2 >result &&\n    -+\tgit replay --output-commands --advance main topic1..topic2 >result &&\n    ++\tgit replay --update-refs=print --advance main topic1..topic2 >result &&\n      \n      \ttest_line_count = 1 result &&\n      \n    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to perform basic che\n      \n      test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n     -\tgit -C bare replay --advance main topic1..topic2 >result-bare &&\n    -+\tgit -C bare replay --output-commands --advance main topic1..topic2 >result-bare &&\n    ++\tgit -C bare replay --update-refs=print --advance main topic1..topic2 >result-bare &&\n    ++\n    ++\ttest_line_count = 1 result-bare &&\n    ++\n    ++\tgit log --format=%s $(cut -f 3 -d \" \" result-bare) >actual &&\n    ++\ttest_write_lines E D M L B A >expect &&\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\tprintf \"update refs/heads/main \" >expect &&\n    ++\tprintf \"%s \" $(cut -f 3 -d \" \" result-bare) >>expect &&\n    ++\tgit -C bare rev-parse main >>expect &&\n    ++\n      \ttest_cmp expect result-bare\n      '\n      \n    @@ t/t3650-replay-basics.sh: test_expect_success 'replay fails when both --advance\n      \n      test_expect_success 'using replay to also rebase a contained branch' '\n     -\tgit replay --contained --onto main main..topic3 >result &&\n    -+\tgit replay --output-commands --contained --onto main main..topic3 >result &&\n    ++\tgit replay --update-refs=print --contained --onto main main..topic3 >result &&\n      \n      \ttest_line_count = 2 result &&\n      \tcut -f 3 -d \" \" result >new-branch-tips &&\n    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to also rebase a con\n      \n      test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n     -\tgit -C bare replay --contained --onto main main..topic3 >result-bare &&\n    -+\tgit -C bare replay --output-commands --contained --onto main main..topic3 >result-bare &&\n    ++\tgit -C bare replay --update-refs=print --contained --onto main main..topic3 >result-bare &&\n    ++\n    ++\ttest_line_count = 2 result-bare &&\n    ++\tcut -f 3 -d \" \" result-bare >new-branch-tips &&\n    ++\n    ++\tgit log --format=%s $(head -n 1 new-branch-tips) >actual &&\n    ++\ttest_write_lines F C M L B A >expect &&\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\tgit log --format=%s $(tail -n 1 new-branch-tips) >actual &&\n    ++\ttest_write_lines H G F C M L B A >expect &&\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\tprintf \"update refs/heads/topic1 \" >expect &&\n    ++\tprintf \"%s \" $(head -n 1 new-branch-tips) >>expect &&\n    ++\tgit -C bare rev-parse topic1 >>expect &&\n    ++\tprintf \"update refs/heads/topic3 \" >>expect &&\n    ++\tprintf \"%s \" $(tail -n 1 new-branch-tips) >>expect &&\n    ++\tgit -C bare rev-parse topic3 >>expect &&\n    ++\n      \ttest_cmp expect result-bare\n      '\n      \n      test_expect_success 'using replay to rebase multiple divergent branches' '\n     -\tgit replay --onto main ^topic1 topic2 topic4 >result &&\n    -+\tgit replay --output-commands --onto main ^topic1 topic2 topic4 >result &&\n    ++\tgit replay --update-refs=print --onto main ^topic1 topic2 topic4 >result &&\n      \n      \ttest_line_count = 2 result &&\n      \tcut -f 3 -d \" \" result >new-branch-tips &&\n    @@ t/t3650-replay-basics.sh: test_expect_success 'using replay to rebase multiple d\n      \n      test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n     -\tgit -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n    -+\tgit -C bare replay --output-commands --contained --onto main ^main topic2 topic3 topic4 >result &&\n    ++\tgit -C bare replay --update-refs=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n      \n      \ttest_line_count = 4 result &&\n      \tcut -f 3 -d \" \" result >new-branch-tips &&\n    @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '\n      \t\t--onto rename-onto rename-onto..rename-from\n      '\n      \n    -+# Tests for new default atomic behavior and options\n    -+\n    -+test_expect_success 'replay default behavior should not produce output when successful' '\n    -+\tgit replay --onto main topic1..topic3 >output &&\n    -+\ttest_must_be_empty output\n    -+'\n    -+\n    -+test_expect_success 'replay with --output-commands produces traditional output' '\n    -+\tgit replay --output-commands --onto main topic1..topic3 >output &&\n    -+\ttest_line_count = 1 output &&\n    -+\tgrep \"^update refs/heads/topic3 \" output\n    -+'\n    -+\n    -+test_expect_success 'replay with --allow-partial should not produce output when successful' '\n    -+\tgit replay --allow-partial --onto main topic1..topic3 >output &&\n    -+\ttest_must_be_empty output\n    -+'\n    -+\n    -+test_expect_success 'replay fails when --output-commands and --allow-partial are used together' '\n    -+\ttest_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&\n    -+\tgrep \"cannot be used together\" error\n    -+'\n    ++# Tests for atomic ref update behavior\n     +\n     +test_expect_success 'replay with --contained updates multiple branches atomically' '\n    -+\t# Create fresh test branches based on the original structure\n    -+\t# contained-topic1 should be contained within the range to contained-topic3\n    -+\tgit branch contained-base main &&\n    -+\tgit checkout -b contained-topic1 contained-base &&\n    -+\ttest_commit ContainedC &&\n    -+\tgit checkout -b contained-topic3 contained-topic1 &&\n    -+\ttest_commit ContainedG &&\n    -+\ttest_commit ContainedH &&\n    -+\tgit checkout main &&\n    -+\n     +\t# Store original states\n    -+\tgit rev-parse contained-topic1 >contained-topic1-old &&\n    -+\tgit rev-parse contained-topic3 >contained-topic3-old &&\n    -+\n    -+\t# Use --contained to update multiple branches - this should update both\n    -+\tgit replay --contained --onto main contained-base..contained-topic3 &&\n    -+\n    -+\t# Verify both branches were updated\n    -+\tgit rev-parse contained-topic1 >contained-topic1-new &&\n    -+\tgit rev-parse contained-topic3 >contained-topic3-new &&\n    -+\t! test_cmp contained-topic1-old contained-topic1-new &&\n    -+\t! test_cmp contained-topic3-old contained-topic3-new\n    -+'\n    ++\tSTART_TOPIC1=$(git rev-parse topic1) &&\n    ++\tSTART_TOPIC3=$(git rev-parse topic3) &&\n    ++\ttest_when_finished \"git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3\" &&\n     +\n    -+test_expect_success 'replay atomic behavior: all refs updated or none' '\n    -+\t# Store original state\n    -+\tgit rev-parse topic4 >topic4-old &&\n    -+\n    -+\t# Default atomic behavior\n    -+\tgit replay --onto main main..topic4 &&\n    ++\t# Use --contained to update multiple branches\n    ++\tgit replay --contained --onto main main..topic3 >output &&\n    ++\ttest_must_be_empty output &&\n     +\n    -+\t# Verify ref was updated\n    -+\tgit rev-parse topic4 >topic4-new &&\n    -+\t! test_cmp topic4-old topic4-new &&\n    ++\t# Verify both branches were updated with correct commit sequences\n    ++\tgit log --format=%s topic1 >actual &&\n    ++\ttest_write_lines F C M L B A >expect &&\n    ++\ttest_cmp expect actual &&\n     +\n    -+\t# Verify no partial state\n    -+\tgit log --format=%s topic4 >actual &&\n    -+\ttest_write_lines J I M L B A >expect &&\n    ++\tgit log --format=%s topic3 >actual &&\n    ++\ttest_write_lines H G F C M L B A >expect &&\n     +\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success 'replay works correctly with bare repositories' '\n    -+\t# Test atomic behavior in bare repo (important for Gitaly)\n    -+\tgit checkout -b bare-test topic1 &&\n    -+\ttest_commit BareTest &&\n    ++test_expect_success 'replay atomic guarantee: all refs updated or none' '\n    ++\t# Store original states\n    ++\tSTART_TOPIC1=$(git rev-parse topic1) &&\n    ++\tSTART_TOPIC3=$(git rev-parse topic3) &&\n    ++\ttest_when_finished \"git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3 && rm -f .git/refs/heads/topic1.lock\" &&\n     +\n    -+\t# Test with bare repo - replay the commits from main..bare-test to get the full history\n    -+\tgit -C bare fetch .. bare-test:bare-test &&\n    -+\tgit -C bare replay --onto main main..bare-test &&\n    ++\t# Create a lock on topic1 to simulate a concurrent update\n    ++\t>.git/refs/heads/topic1.lock &&\n     +\n    -+\t# Verify the bare repo was updated correctly (no output)\n    -+\tgit -C bare log --format=%s bare-test >actual &&\n    -+\ttest_write_lines BareTest F C M L B A >expect &&\n    -+\ttest_cmp expect actual\n    -+'\n    ++\t# Try to update multiple branches with --contained\n    ++\t# This should fail atomically - neither branch should be updated\n    ++\ttest_must_fail git replay --contained --onto main main..topic3 2>error &&\n     +\n    -+test_expect_success 'replay --allow-partial with no failures produces no output' '\n    -+\tgit checkout -b partial-test topic1 &&\n    -+\ttest_commit PartialTest &&\n    ++\t# Verify the transaction failed\n    ++\tgrep \"failed to commit ref transaction\" error &&\n     +\n    -+\t# Should succeed silently even with partial mode\n    -+\tgit replay --allow-partial --onto main topic1..partial-test >output &&\n    -+\ttest_must_be_empty output\n    ++\t# Verify NEITHER branch was updated (all-or-nothing guarantee)\n    ++\ttest_cmp_rev $START_TOPIC1 topic1 &&\n    ++\ttest_cmp_rev $START_TOPIC3 topic3\n     +'\n     +\n    -+test_expect_success 'replay maintains ref update consistency' '\n    -+\t# Test that traditional vs atomic produce equivalent results\n    -+\tgit checkout -b method1-test topic2 &&\n    -+\tgit checkout -b method2-test topic2 &&\n    ++test_expect_success 'traditional pipeline and atomic update produce equivalent results' '\n    ++\t# Store original states\n    ++\tSTART_TOPIC2=$(git rev-parse topic2) &&\n    ++\ttest_when_finished \"git branch -f topic2 $START_TOPIC2\" &&\n     +\n    -+\t# Both methods should update refs to point to the same replayed commits\n    -+\tgit replay --output-commands --onto main topic1..method1-test >update-commands &&\n    ++\t# Traditional method: output commands and pipe to update-ref\n    ++\tgit replay --update-refs=print --onto main topic1..topic2 >update-commands &&\n     +\tgit update-ref --stdin <update-commands &&\n    -+\tgit log --format=%s method1-test >traditional-result &&\n    ++\tgit log --format=%s topic2 >traditional-result &&\n    ++\n    ++\t# Reset topic2\n    ++\tgit branch -f topic2 $START_TOPIC2 &&\n     +\n    -+\t# Direct atomic method should produce same commit history\n    -+\tgit replay --onto main topic1..method2-test &&\n    -+\tgit log --format=%s method2-test >atomic-result &&\n    ++\t# Atomic method: direct ref updates\n    ++\tgit replay --onto main topic1..topic2 &&\n    ++\tgit log --format=%s topic2 >atomic-result &&\n     +\n     +\t# Both methods should produce identical commit histories\n     +\ttest_cmp traditional-result atomic-result\n     +'\n     +\n    -+test_expect_success 'replay error messages are helpful and clear' '\n    -+\t# Test that error messages are clear\n    -+\ttest_must_fail git replay --output-commands --allow-partial --onto main topic1..topic2 2>error &&\n    -+\tgrep \"cannot be used together\" error\n    -+'\n    -+\n    -+test_expect_success 'replay with empty range produces no output and no changes' '\n    -+\t# Create a test branch for empty range testing\n    -+\tgit checkout -b empty-test topic1 &&\n    -+\tgit rev-parse empty-test >empty-test-before &&\n    -+\n    -+\t# Empty range should succeed but do nothing\n    -+\tgit replay --onto main empty-test..empty-test >output &&\n    ++test_expect_success 'replay works correctly with bare repositories' '\n    ++\t# Test atomic behavior in bare repo\n    ++\tgit -C bare fetch .. topic1:bare-test-branch &&\n    ++\tgit -C bare replay --onto main main..bare-test-branch >output &&\n     +\ttest_must_be_empty output &&\n     +\n    -+\t# Branch should be unchanged\n    -+\tgit rev-parse empty-test >empty-test-after &&\n    -+\ttest_cmp empty-test-before empty-test-after\n    ++\t# Verify the bare repo was updated correctly\n    ++\tgit -C bare log --format=%s bare-test-branch >actual &&\n    ++\ttest_write_lines F C M L B A >expect &&\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n    ++test_expect_success 'replay validates --update-refs mode values' '\n    ++\ttest_must_fail git replay --update-refs=invalid --onto main topic1..topic2 2>error &&\n    ++\tgrep \"invalid value for --update-refs\" error\n     +'\n     +\n      test_done\n-:  ---------- > 3:  710ab27ae3 replay: add replay.defaultAction config option\n-- \n2.51.0\n\n\nSiddharth Asthana (3):\n  replay: use die_for_incompatible_opt2() for option validation\n  replay: make atomic ref updates the default behavior\n  replay: add replay.defaultAction config option\n\n Documentation/config/replay.adoc |  14 +++\n Documentation/git-replay.adoc    |  71 ++++++-----\n builtin/replay.c                 | 108 +++++++++++++++--\n t/t3650-replay-basics.sh         | 198 +++++++++++++++++++++++++++++--\n 4 files changed, 343 insertions(+), 48 deletions(-)\n create mode 100644 Documentation/config/replay.adoc\n\nbase-commit: 4b71b294773cc4f7fe48ec3a70079aa8783f373d\n\nThanks\n- Siddharth\n"},{"id":"528655","messageId":"20251013183311.33329-2-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251013183311.33329-1-siddharthasthana31@gmail.com","subject":"[PATCH v3 1/3] replay: use die_for_incompatible_opt2() for option validation","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-13T18:33:09Z","receivedAt":"2025-10-13T18:33:30Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"In preparation for adding the --update-refs option, convert option\nvalidation to use die_for_incompatible_opt2(). This helper provides\nstandardized error messages for mutually exclusive options.\n\nThe following commit introduces --update-refs which will be incompatible\nwith certain other options. Using die_for_incompatible_opt2() now means\nthat commit can cleanly add its validation using the same pattern,\nkeeping the validation logic consistent and maintainable.\n\nThis also aligns git-replay's option handling with how other Git commands\nmanage option conflicts, using the established die_for_incompatible_opt*()\nhelper family.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n builtin/replay.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 6172c8aacc..b64fc72063 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -330,9 +330,9 @@ int cmd_replay(int argc,\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n-\tif (advance_name_opt && contained)\n-\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n-\t\t    \"--advance\", \"--contained\");\n+\tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n+\t\t\t\t  contained, \"--contained\");\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n-- \n2.51.0\n\n"},{"id":"528656","messageId":"20251013183311.33329-3-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251013183311.33329-1-siddharthasthana31@gmail.com","subject":"[PATCH v3 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-13T18:33:10Z","receivedAt":"2025-10-13T18:33:36Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"The git replay command currently outputs update commands that can be\npiped to update-ref to achieve a rebase, e.g.\n\n  git replay --onto main topic1..topic2 | git update-ref --stdin\n\nThis separation had advantages for three special cases:\n  * it made testing easy (when state isn't modified from one step to\n    the next, you don't need to make temporary branches or have undo\n    commands, or try to track the changes)\n  * it provided a natural can-it-rebase-cleanly (and what would it\n    rebase to) capability without automatically updating refs, similar\n    to a --dry-run\n  * it provided a natural low-level tool for the suite of hash-object,\n    mktree, commit-tree, mktag, merge-tree, and update-ref, allowing\n    users to have another building block for experimentation and making\n    new tools\n\nHowever, it should be noted that all three of these are somewhat\nspecial cases; users, whether on the client or server side, would\nalmost certainly find it more ergonomical to simply have the updating\nof refs be the default.\n\nFor server-side operations in particular, the pipeline architecture\ncreates process coordination overhead. Server implementations that need\nto perform rebases atomically must maintain additional code to:\n\n  1. Spawn and manage a pipeline between git-replay and git-update-ref\n  2. Coordinate stdout/stderr streams across the pipe boundary\n  3. Handle partial failure states if the pipeline breaks mid-execution\n  4. Parse and validate the update-ref command output\n\nChange the default behavior to update refs directly, and atomically (at\nleast to the extent supported by the refs backend in use). This\neliminates the process coordination overhead for the common case.\n\nFor users needing the traditional pipeline workflow, add a new\n`--update-refs=<mode>` option that preserves the original behavior:\n\n  git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin\n\nThe mode can be:\n  * `yes` (default): Update refs directly using an atomic transaction\n  * `print`: Output update-ref commands for pipeline use\n\nImplementation details:\n\nThe atomic ref updates are implemented using Git's ref transaction API.\nIn cmd_replay(), when not in 'print' mode, we initialize a transaction\nusing ref_store_transaction_begin() with the default atomic behavior.\nAs commits are replayed, ref updates are staged into the transaction\nusing ref_transaction_update(). Finally, ref_transaction_commit()\napplies all updates atomically—either all updates succeed or none do.\n\nTo avoid code duplication between the 'print' and 'yes' modes, this\ncommit extracts a handle_ref_update() helper function. This function\ntakes the mode and either prints the update command or stages it into\nthe transaction. This keeps both code paths consistent and makes future\nmaintenance easier.\n\nThe helper function signature:\n\n  static int handle_ref_update(const char *mode,\n                                struct ref_transaction *transaction,\n                                const char *refname,\n                                const struct object_id *new_oid,\n                                const struct object_id *old_oid,\n                                struct strbuf *err)\n\nWhen mode is 'print', it prints the update-ref command. When mode is\n'yes', it calls ref_transaction_update() to stage the update. This\neliminates the duplication that would otherwise exist at each ref update\ncall site.\n\nTest suite changes:\n\nAll existing tests that expected command output now use\n`--update-refs=print` to preserve their original behavior. This keeps\nthe tests valid while allowing them to verify that the pipeline workflow\nstill works correctly.\n\nNew tests were added to verify:\n  - Default atomic behavior (no output, refs updated directly)\n  - Bare repository support (server-side use case)\n  - Equivalence between traditional pipeline and atomic updates\n  - Real atomicity using a lock file to verify all-or-nothing guarantee\n  - Test isolation using test_when_finished to clean up state\n\nThe bare repository tests were fixed to rebuild their expectations\nindependently rather than comparing to previous test output, improving\ntest reliability and isolation.\n\nA following commit will add a `replay.defaultAction` configuration\noption for users who prefer the traditional pipeline output as their\ndefault behavior.\n\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc |  71 ++++++++++------\n builtin/replay.c              |  88 ++++++++++++++++---\n t/t3650-replay-basics.sh      | 153 ++++++++++++++++++++++++++++++++--\n 3 files changed, 267 insertions(+), 45 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 0b12bf8aa4..ea04021a5f 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,15 +9,17 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>)\n+\t\t[--update-refs[=<mode>]] <revision-range>...\n \n DESCRIPTION\n -----------\n \n Takes ranges of commits and replays them onto a new location. Leaves\n-the working tree and the index untouched, and updates no references.\n-The output of this command is meant to be used as input to\n-`git update-ref --stdin`, which would update the relevant branches\n+the working tree and the index untouched. By default, updates the\n+relevant references using an atomic transaction (all refs update or\n+none). Use `--update-refs=print` to avoid automatic ref updates and\n+instead get update commands that can be piped to `git update-ref --stdin`\n (see the OUTPUT section below).\n \n THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n@@ -29,18 +31,28 @@ OPTIONS\n \tStarting point at which to create the new commits.  May be any\n \tvalid commit, and not just an existing branch name.\n +\n-When `--onto` is specified, the update-ref command(s) in the output will\n-update the branch(es) in the revision range to point at the new\n-commits, similar to the way how `git rebase --update-refs` updates\n-multiple branches in the affected range.\n+When `--onto` is specified, the branch(es) in the revision range will be\n+updated to point at the new commits (or update commands will be printed\n+if `--update-refs=print` is used), similar to the way how\n+`git rebase --update-refs` updates multiple branches in the affected range.\n \n --advance <branch>::\n \tStarting point at which to create the new commits; must be a\n \tbranch name.\n +\n-When `--advance` is specified, the update-ref command(s) in the output\n-will update the branch passed as an argument to `--advance` to point at\n-the new commits (in other words, this mimics a cherry-pick operation).\n+When `--advance` is specified, the branch passed as an argument will be\n+updated to point at the new commits (or an update command will be printed\n+if `--update-refs=print` is used). This mimics a cherry-pick operation.\n+\n+--update-refs[=<mode>]::\n+\tControl how references are updated. The mode can be:\n++\n+--\n+* `yes` (default): Update refs directly using an atomic transaction.\n+  All ref updates succeed or all fail.\n+* `print`: Output update-ref commands instead of updating refs.\n+  The output can be piped as-is to `git update-ref --stdin`.\n+--\n \n <revision-range>::\n \tRange of commits to replay. More than one <revision-range> can\n@@ -54,15 +66,19 @@ include::rev-list-options.adoc[]\n OUTPUT\n ------\n \n-When there are no conflicts, the output of this command is usable as\n-input to `git update-ref --stdin`.  It is of the form:\n+By default, when there are no conflicts, this command updates the relevant\n+references using an atomic transaction and produces no output. All ref\n+updates succeed or all fail.\n+\n+When `--update-refs=print` is used, the output is usable as input to\n+`git update-ref --stdin`. It is of the form:\n \n \tupdate refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n \tupdate refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n \tupdate refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n \n where the number of refs updated depends on the arguments passed and\n-the shape of the history being replayed.  When using `--advance`, the\n+the shape of the history being replayed. When using `--advance`, the\n number of refs updated is always one, but for `--onto`, it can be one\n or more (rebasing multiple branches simultaneously is supported).\n \n@@ -77,44 +93,45 @@ is something other than 0 or 1.\n EXAMPLES\n --------\n \n-To simply rebase `mybranch` onto `target`:\n+To simply rebase `mybranch` onto `target` (default behavior):\n \n ------------\n $ git replay --onto target origin/main..mybranch\n-update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n ------------\n \n To cherry-pick the commits from mybranch onto target:\n \n ------------\n $ git replay --advance target origin/main..mybranch\n-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n ------------\n \n Note that the first two examples replay the exact same commits and on\n top of the exact same new base, they only differ in that the first\n-provides instructions to make mybranch point at the new commits and\n-the second provides instructions to make target point at them.\n+updates mybranch to point at the new commits and the second updates\n+target to point at them.\n+\n+To get the traditional pipeline output:\n+\n+------------\n+$ git replay --update-refs=print --onto target origin/main..mybranch\n+update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n+------------\n \n What if you have a stack of branches, one depending upon another, and\n you'd really like to rebase the whole set?\n \n ------------\n $ git replay --contained --onto origin/main origin/main..tipbranch\n-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n ------------\n \n+This automatically finds and rebases all branches contained within the\n+`origin/main..tipbranch` range.\n+\n When calling `git replay`, one does not need to specify a range of\n-commits to replay using the syntax `A..B`; any range expression will\n-do:\n+commits to replay using the syntax `A..B`; any range expression will do:\n \n ------------\n $ git replay --onto origin/main ^base branch1 branch2 branch3\n-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n ------------\n \n This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex b64fc72063..457225363e 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -284,6 +284,26 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \treturn create_commit(repo, result->tree, pickme, replayed_base);\n }\n \n+static int handle_ref_update(const char *mode,\n+\t\t\t     struct ref_transaction *transaction,\n+\t\t\t     const char *refname,\n+\t\t\t     const struct object_id *new_oid,\n+\t\t\t     const struct object_id *old_oid,\n+\t\t\t     struct strbuf *err)\n+{\n+\tif (!strcmp(mode, \"print\")) {\n+\t\tprintf(\"update %s %s %s\\n\",\n+\t\t       refname,\n+\t\t       oid_to_hex(new_oid),\n+\t\t       oid_to_hex(old_oid));\n+\t\treturn 0;\n+\t}\n+\n+\t/* mode == \"yes\" - update refs directly */\n+\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n+\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n+}\n+\n int cmd_replay(int argc,\n \t       const char **argv,\n \t       const char *prefix,\n@@ -294,6 +314,7 @@ int cmd_replay(int argc,\n \tstruct commit *onto = NULL;\n \tconst char *onto_name = NULL;\n \tint contained = 0;\n+\tconst char *update_refs_mode = NULL;\n \n \tstruct rev_info revs;\n \tstruct commit *last_commit = NULL;\n@@ -302,12 +323,14 @@ int cmd_replay(int argc,\n \tstruct merge_result result;\n \tstruct strset *update_refs = NULL;\n \tkh_oid_map_t *replayed_commits;\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strbuf transaction_err = STRBUF_INIT;\n \tint ret = 0;\n \n-\tconst char * const replay_usage[] = {\n+\tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n-\t\t   \"<revision-range>...\"),\n+\t\t   \"[--update-refs[=<mode>]] <revision-range>...\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -319,6 +342,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &contained,\n \t\t\t N_(\"advance all branches contained in revision-range\")),\n+\t\tOPT_STRING(0, \"update-refs\", &update_refs_mode,\n+\t\t\t   N_(\"mode\"),\n+\t\t\t   N_(\"control ref update behavior (yes|print)\")),\n \t\tOPT_END()\n \t};\n \n@@ -333,6 +359,15 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n+\t/* Set default mode if not specified */\n+\tif (!update_refs_mode)\n+\t\tupdate_refs_mode = \"yes\";\n+\n+\t/* Validate update-refs mode */\n+\tif (strcmp(update_refs_mode, \"yes\") && strcmp(update_refs_mode, \"print\"))\n+\t\tdie(_(\"invalid value for --update-refs: '%s' (expected 'yes' or 'print')\"),\n+\t\t    update_refs_mode);\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n@@ -389,6 +424,17 @@ int cmd_replay(int argc,\n \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n \t\t\t      &onto, &update_refs);\n \n+\t/* Initialize ref transaction if we're updating refs directly */\n+\tif (!strcmp(update_refs_mode, \"yes\")) {\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n+\t\t\t\t\t\t\t  0, &transaction_err);\n+\t\tif (!transaction) {\n+\t\t\tret = error(_(\"failed to begin ref transaction: %s\"),\n+\t\t\t\t    transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n \tif (!onto) /* FIXME: Should handle replaying down to root commit */\n \t\tdie(\"Replaying down to root commit is not supported yet!\");\n \n@@ -434,10 +480,15 @@ int cmd_replay(int argc,\n \t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n \t\t\t    (contained || strset_contains(update_refs,\n \t\t\t\t\t\t\t  decoration->name))) {\n-\t\t\t\tprintf(\"update %s %s %s\\n\",\n-\t\t\t\t       decoration->name,\n-\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t\t\t       oid_to_hex(&commit->object.oid));\n+\t\t\t\tif (handle_ref_update(update_refs_mode, transaction,\n+\t\t\t\t\t\t      decoration->name,\n+\t\t\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t\t\t      &commit->object.oid,\n+\t\t\t\t\t\t      &transaction_err) < 0) {\n+\t\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n+\t\t\t\t\tgoto cleanup;\n+\t\t\t\t}\n \t\t\t}\n \t\t\tdecoration = decoration->next;\n \t\t}\n@@ -445,10 +496,24 @@ int cmd_replay(int argc,\n \n \t/* In --advance mode, advance the target ref */\n \tif (result.clean == 1 && advance_name) {\n-\t\tprintf(\"update %s %s %s\\n\",\n-\t\t       advance_name,\n-\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t       oid_to_hex(&onto->object.oid));\n+\t\tif (handle_ref_update(update_refs_mode, transaction,\n+\t\t\t\t      advance_name,\n+\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t      &onto->object.oid,\n+\t\t\t\t      &transaction_err) < 0) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    advance_name, transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n+\t/* Commit the ref transaction if we have one */\n+\tif (transaction && result.clean == 1) {\n+\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n+\t\t\tret = error(_(\"failed to commit ref transaction: %s\"),\n+\t\t\t\t    transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n \t}\n \n \tmerge_finalize(&merge_opt, &result);\n@@ -460,6 +525,9 @@ int cmd_replay(int argc,\n \tret = result.clean;\n \n cleanup:\n+\tif (transaction)\n+\t\tref_transaction_free(transaction);\n+\tstrbuf_release(&transaction_err);\n \trelease_revisions(&revs);\n \tfree(advance_name);\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 58b3759935..c2c54fbba7 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n '\n \n test_expect_success 'using replay to rebase two branches, one on top of other' '\n-\tgit replay --onto main topic1..topic2 >result &&\n+\tgit replay --update-refs=print --onto main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -67,8 +67,34 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n \ttest_cmp expect result\n '\n \n+test_expect_success 'using replay with default atomic behavior (no output)' '\n+\t# Store the original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n+\n+\t# Default behavior: atomic ref updates (no output)\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify the history is correct\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n-\tgit -C bare replay --onto main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --update-refs=print --onto main topic1..topic2 >result-bare &&\n+\n+\ttest_line_count = 1 result-bare &&\n+\n+\tgit log --format=%s $(cut -f 3 -d \" \" result-bare) >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual &&\n+\n+\tprintf \"update refs/heads/topic2 \" >expect &&\n+\tprintf \"%s \" $(cut -f 3 -d \" \" result-bare) >>expect &&\n+\tgit -C bare rev-parse topic2 >>expect &&\n+\n \ttest_cmp expect result-bare\n '\n \n@@ -86,7 +112,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n \t# 4th field of result is hash for main instead of hash for topic2\n \n-\tgit replay --advance main topic1..topic2 >result &&\n+\tgit replay --update-refs=print --advance main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -102,7 +128,18 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n '\n \n test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n-\tgit -C bare replay --advance main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --update-refs=print --advance main topic1..topic2 >result-bare &&\n+\n+\ttest_line_count = 1 result-bare &&\n+\n+\tgit log --format=%s $(cut -f 3 -d \" \" result-bare) >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual &&\n+\n+\tprintf \"update refs/heads/main \" >expect &&\n+\tprintf \"%s \" $(cut -f 3 -d \" \" result-bare) >>expect &&\n+\tgit -C bare rev-parse main >>expect &&\n+\n \ttest_cmp expect result-bare\n '\n \n@@ -115,7 +152,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n '\n \n test_expect_success 'using replay to also rebase a contained branch' '\n-\tgit replay --contained --onto main main..topic3 >result &&\n+\tgit replay --update-refs=print --contained --onto main main..topic3 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -139,12 +176,31 @@ test_expect_success 'using replay to also rebase a contained branch' '\n '\n \n test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n-\tgit -C bare replay --contained --onto main main..topic3 >result-bare &&\n+\tgit -C bare replay --update-refs=print --contained --onto main main..topic3 >result-bare &&\n+\n+\ttest_line_count = 2 result-bare &&\n+\tcut -f 3 -d \" \" result-bare >new-branch-tips &&\n+\n+\tgit log --format=%s $(head -n 1 new-branch-tips) >actual &&\n+\ttest_write_lines F C M L B A >expect &&\n+\ttest_cmp expect actual &&\n+\n+\tgit log --format=%s $(tail -n 1 new-branch-tips) >actual &&\n+\ttest_write_lines H G F C M L B A >expect &&\n+\ttest_cmp expect actual &&\n+\n+\tprintf \"update refs/heads/topic1 \" >expect &&\n+\tprintf \"%s \" $(head -n 1 new-branch-tips) >>expect &&\n+\tgit -C bare rev-parse topic1 >>expect &&\n+\tprintf \"update refs/heads/topic3 \" >>expect &&\n+\tprintf \"%s \" $(tail -n 1 new-branch-tips) >>expect &&\n+\tgit -C bare rev-parse topic3 >>expect &&\n+\n \ttest_cmp expect result-bare\n '\n \n test_expect_success 'using replay to rebase multiple divergent branches' '\n-\tgit replay --onto main ^topic1 topic2 topic4 >result &&\n+\tgit replay --update-refs=print --onto main ^topic1 topic2 topic4 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -168,7 +224,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n '\n \n test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n-\tgit -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n+\tgit -C bare replay --update-refs=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n \n \ttest_line_count = 4 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -217,4 +273,85 @@ test_expect_success 'merge.directoryRenames=false' '\n \t\t--onto rename-onto rename-onto..rename-from\n '\n \n+# Tests for atomic ref update behavior\n+\n+test_expect_success 'replay with --contained updates multiple branches atomically' '\n+\t# Store original states\n+\tSTART_TOPIC1=$(git rev-parse topic1) &&\n+\tSTART_TOPIC3=$(git rev-parse topic3) &&\n+\ttest_when_finished \"git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3\" &&\n+\n+\t# Use --contained to update multiple branches\n+\tgit replay --contained --onto main main..topic3 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify both branches were updated with correct commit sequences\n+\tgit log --format=%s topic1 >actual &&\n+\ttest_write_lines F C M L B A >expect &&\n+\ttest_cmp expect actual &&\n+\n+\tgit log --format=%s topic3 >actual &&\n+\ttest_write_lines H G F C M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'replay atomic guarantee: all refs updated or none' '\n+\t# Store original states\n+\tSTART_TOPIC1=$(git rev-parse topic1) &&\n+\tSTART_TOPIC3=$(git rev-parse topic3) &&\n+\ttest_when_finished \"git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3 && rm -f .git/refs/heads/topic1.lock\" &&\n+\n+\t# Create a lock on topic1 to simulate a concurrent update\n+\t>.git/refs/heads/topic1.lock &&\n+\n+\t# Try to update multiple branches with --contained\n+\t# This should fail atomically - neither branch should be updated\n+\ttest_must_fail git replay --contained --onto main main..topic3 2>error &&\n+\n+\t# Verify the transaction failed\n+\tgrep \"failed to commit ref transaction\" error &&\n+\n+\t# Verify NEITHER branch was updated (all-or-nothing guarantee)\n+\ttest_cmp_rev $START_TOPIC1 topic1 &&\n+\ttest_cmp_rev $START_TOPIC3 topic3\n+'\n+\n+test_expect_success 'traditional pipeline and atomic update produce equivalent results' '\n+\t# Store original states\n+\tSTART_TOPIC2=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START_TOPIC2\" &&\n+\n+\t# Traditional method: output commands and pipe to update-ref\n+\tgit replay --update-refs=print --onto main topic1..topic2 >update-commands &&\n+\tgit update-ref --stdin <update-commands &&\n+\tgit log --format=%s topic2 >traditional-result &&\n+\n+\t# Reset topic2\n+\tgit branch -f topic2 $START_TOPIC2 &&\n+\n+\t# Atomic method: direct ref updates\n+\tgit replay --onto main topic1..topic2 &&\n+\tgit log --format=%s topic2 >atomic-result &&\n+\n+\t# Both methods should produce identical commit histories\n+\ttest_cmp traditional-result atomic-result\n+'\n+\n+test_expect_success 'replay works correctly with bare repositories' '\n+\t# Test atomic behavior in bare repo\n+\tgit -C bare fetch .. topic1:bare-test-branch &&\n+\tgit -C bare replay --onto main main..bare-test-branch >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify the bare repo was updated correctly\n+\tgit -C bare log --format=%s bare-test-branch >actual &&\n+\ttest_write_lines F C M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'replay validates --update-refs mode values' '\n+\ttest_must_fail git replay --update-refs=invalid --onto main topic1..topic2 2>error &&\n+\tgrep \"invalid value for --update-refs\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"528657","messageId":"20251013183311.33329-4-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251013183311.33329-1-siddharthasthana31@gmail.com","subject":"[PATCH v3 3/3] replay: add replay.defaultAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-13T18:33:11Z","receivedAt":"2025-10-13T18:33:42Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"Add a configuration option to control the default behavior of git replay\nfor updating references. This allows users who prefer the traditional\npipeline output to set it once in their config instead of passing\n--update-refs=print with every command.\n\nThe config option uses enum string values for extensibility:\n  * replay.defaultAction = update-refs (default): atomic ref updates\n  * replay.defaultAction = show-commands: output commands for pipeline\n\nThe command-line --update-refs option always overrides the config setting,\nallowing users to temporarily change behavior for a single invocation.\n\nImplementation details:\n\nIn cmd_replay(), before parsing command-line options, we read the\nconfiguration using repo_config_get_string_tmp(). If the config variable\nis set, we validate the value and map it to an internal mode:\n\n  Config value         Internal mode    Behavior\n  ────────────────────────────────────────────────────────────────\n  \"update-refs\"        \"yes\"            Atomic ref updates (default)\n  \"show-commands\"      \"print\"          Pipeline output\n  (not set)            \"yes\"            Atomic ref updates (default)\n  (invalid)            error            Die with helpful message\n\nIf an invalid value is provided, we die() immediately with an error\nmessage explaining the valid options. This catches configuration errors\nearly and provides clear guidance to users.\n\nThe command-line --update-refs option, when provided, overrides the\nconfig value. This precedence allows users to set their preferred default\nwhile still having per-invocation control:\n\n  git config replay.defaultAction show-commands  # Set default\n  git replay --update-refs=yes --onto main topic  # Override once\n\nThe config option uses different value names ('update-refs' vs\n'show-commands') compared to the command-line option ('yes' vs 'print')\nfor semantic clarity. The config values describe what action is being\ntaken, while the command-line values are terse for typing convenience.\n\nThe enum string design (rather than a boolean like 'replay.updateRefs')\nallows future expansion to additional modes without requiring new\nconfiguration variables. For example, if we later add custom format\nsupport (--update-refs=format), we can extend the config to support\n'replay.defaultAction = format' without breaking existing configurations\nor requiring a second config variable.\n\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/config/replay.adoc | 14 ++++++++++\n builtin/replay.c                 | 20 ++++++++++++--\n t/t3650-replay-basics.sh         | 47 +++++++++++++++++++++++++++++++-\n 3 files changed, 77 insertions(+), 4 deletions(-)\n create mode 100644 Documentation/config/replay.adoc\n\ndiff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc\nnew file mode 100644\nindex 0000000000..6012333cc1\n--- /dev/null\n+++ b/Documentation/config/replay.adoc\n@@ -0,0 +1,14 @@\n+replay.defaultAction::\n+\tControl the default behavior of `git replay` for updating references.\n+\tCan be set to:\n++\n+--\n+* `update-refs` (default): Update refs directly using an atomic transaction.\n+* `show-commands`: Output update-ref commands that can be piped to\n+  `git update-ref --stdin`.\n+--\n++\n+This can be overridden with the `--update-refs` command-line option.\n+Note that the command-line option uses slightly different values\n+(`yes` and `print`) for brevity, but they map to the same behavior\n+as the config values.\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 457225363e..3c618bf100 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -8,6 +8,7 @@\n #include \"git-compat-util.h\"\n \n #include \"builtin.h\"\n+#include \"config.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"lockfile.h\"\n@@ -359,9 +360,22 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n-\t/* Set default mode if not specified */\n-\tif (!update_refs_mode)\n-\t\tupdate_refs_mode = \"yes\";\n+\t/* Set default mode from config if not specified on command line */\n+\tif (!update_refs_mode) {\n+\t\tconst char *config_value = NULL;\n+\t\tif (!repo_config_get_string_tmp(repo, \"replay.defaultaction\", &config_value)) {\n+\t\t\tif (!strcmp(config_value, \"update-refs\"))\n+\t\t\t\tupdate_refs_mode = \"yes\";\n+\t\t\telse if (!strcmp(config_value, \"show-commands\"))\n+\t\t\t\tupdate_refs_mode = \"print\";\n+\t\t\telse\n+\t\t\t\tdie(_(\"invalid value for replay.defaultAction: '%s' \"\n+\t\t\t\t      \"(expected 'update-refs' or 'show-commands')\"),\n+\t\t\t\t    config_value);\n+\t\t} else {\n+\t\t\tupdate_refs_mode = \"yes\";\n+\t\t}\n+\t}\n \n \t/* Validate update-refs mode */\n \tif (strcmp(update_refs_mode, \"yes\") && strcmp(update_refs_mode, \"print\"))\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex c2c54fbba7..239d7bd87a 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -299,7 +299,7 @@ test_expect_success 'replay atomic guarantee: all refs updated or none' '\n \t# Store original states\n \tSTART_TOPIC1=$(git rev-parse topic1) &&\n \tSTART_TOPIC3=$(git rev-parse topic3) &&\n-\ttest_when_finished \"git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3 && rm -f .git/refs/heads/topic1.lock\" &&\n+\ttest_when_finished \"git branch -f topic1 $START_TOPIC1 && git branch -f topic3 $START_TOPIC3\" &&\n \n \t# Create a lock on topic1 to simulate a concurrent update\n \t>.git/refs/heads/topic1.lock &&\n@@ -308,6 +308,9 @@ test_expect_success 'replay atomic guarantee: all refs updated or none' '\n \t# This should fail atomically - neither branch should be updated\n \ttest_must_fail git replay --contained --onto main main..topic3 2>error &&\n \n+\t# Remove the lock before checking refs\n+\trm -f .git/refs/heads/topic1.lock &&\n+\n \t# Verify the transaction failed\n \tgrep \"failed to commit ref transaction\" error &&\n \n@@ -354,4 +357,46 @@ test_expect_success 'replay validates --update-refs mode values' '\n \tgrep \"invalid value for --update-refs\" error\n '\n \n+test_expect_success 'replay.defaultAction config option' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.defaultAction\" &&\n+\n+\t# Set config to show-commands\n+\tgit config replay.defaultAction show-commands &&\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_line_count = 1 output &&\n+\tgrep \"^update refs/heads/topic2 \" output &&\n+\n+\t# Reset and test update-refs mode\n+\tgit branch -f topic2 $START &&\n+\tgit config replay.defaultAction update-refs &&\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'command-line --update-refs overrides config' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.defaultAction\" &&\n+\n+\t# Set config to update-refs but use --update-refs=print\n+\tgit config replay.defaultAction update-refs &&\n+\tgit replay --update-refs=print --onto main topic1..topic2 >output &&\n+\ttest_line_count = 1 output &&\n+\tgrep \"^update refs/heads/topic2 \" output\n+'\n+\n+test_expect_success 'invalid replay.defaultAction value' '\n+\ttest_when_finished \"git config --unset replay.defaultAction\" &&\n+\tgit config replay.defaultAction invalid &&\n+\ttest_must_fail git replay --onto main topic1..topic2 2>error &&\n+\tgrep \"invalid value for replay.defaultAction\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"528661","messageId":"xmqq7bwy1tgy.fsf@gitster.g","threadId":"64109","inReplyTo":"20251013183311.33329-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH v3 0/3] replay: make atomic ref updates the default","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-13T19:39:57Z","receivedAt":"2025-10-13T19:40:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> **Removed --allow-partial option**\n>\n> After discussion with Elijah and Junio, we couldn't identify a concrete\n> use case for partial failure tolerance. The traditional pipeline with\n> git-update-ref already provides partial update capabilities when needed\n> through its transaction commands. Removing this option simplifies the API\n> and avoids committing to behavior without clear real-world use cases.\n\nAck.\n\n> **Changed to --update-refs=<mode> for extensibility**\n>\n> Phillip suggested that separate boolean flags (--output-commands,\n> --allow-partial) were limiting for future expansion. The --update-refs=<mode>\n> design allows future modes without option proliferation:\n>   - --update-refs=yes (default): atomic ref updates\n>   - --update-refs=print: pipeline output\n>   - Future modes can be added as additional values\n>\n> This API pattern prevents the need for multiple incompatible flags and\n> provides a cleaner interface for users.\n\nAck.\n\n> **Added replay.defaultAction configuration option**\n\nIf a configuration option is added, please consider and think hard\nif its relationship with the command lineoption can be made obvious.\nI do not think it is obvious to anybody that replay.defaultAction is\nsomehow tied to \"git replay --update-refs\" at all.  Either the\nvariable should be renamed to include words like \"update\" and/or\n\"ref\" to hint its link to the option, or the option should be\nrenamed to use the word \"action\" to hint its link to the variable.\n\n> The command-line --update-refs option overrides the config, allowing users\n> to set a preference while maintaining per-invocation control.\n\nThat would follow the standard practice of configuration giving the\ndefault that can be overriden via the command line option per\ninvocation, which would match end-user expectations.  Good.\n\nThanks.\n"},{"id":"528673","messageId":"xmqqms5uzcd7.fsf@gitster.g","threadId":"64109","inReplyTo":"20251013183311.33329-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v3 2/3] replay: make atomic ref updates the default behavior","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-13T22:05:24Z","receivedAt":"2025-10-13T22:05:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> For users needing the traditional pipeline workflow, add a new\n> `--update-refs=<mode>` option that preserves the original behavior:\n>\n>   git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin\n>\n> The mode can be:\n>   * `yes` (default): Update refs directly using an atomic transaction\n>   * `print`: Output update-ref commands for pipeline use\n\nIs it only me who still finds this awkward?  A question \"update?\"\nthat gets answered \"yes\" is quite understandable, but it is not\nimmediately obvious what it means to answer \"print\" to the same\nquestion.  When the user gives the latter mode as the answer to the\nquestion, the question being answered is not really \"do you want to\nupdate refs?\" at all.\n\nThe question the command wants the user to answer is more like \"what\naction do you want to see performed on the refs?\", isn't it?  The\nuser would answer to the question with \"please update them\" to get\nthe default mode, while \"please print them\" may be the answer the\nuser would give to get the useful-for-dry-run-and-development mode.\n\nPerhaps phrase it more like \"--ref-action=(update|print)\"?  I dunno.\n\n>  --advance <branch>::\n>  \tStarting point at which to create the new commits; must be a\n>  \tbranch name.\n>  +\n> -When `--advance` is specified, the update-ref command(s) in the output\n> -will update the branch passed as an argument to `--advance` to point at\n> -the new commits (in other words, this mimics a cherry-pick operation).\n> +When `--advance` is specified, the branch passed as an argument will be\n> +updated to point at the new commits (or an update command will be printed\n> +if `--update-refs=print` is used). This mimics a cherry-pick operation.\n\nI do not find it clear what the reference to cherry-pick is trying\nto convey.  It is like cherry-picking <something> while the <branch>\nis checked out (hence the branch advances as the result of acquiring\nthese commits from <something>)?  Let me see if I understood you by\nattempting to rephrase.\n\n    The history is replayed on top of the <branch> and <branch> is\n    updated to point at the tip of resulting history.\n\nBut what's the significance of saying so?  Did you want to contrast\nit with \"rebase --onto <branch>\", i.e. merely specifying the\nstarting point without <branch> itself moving as the result?  If so,\nit is probably a notable distinction worth pointing out, but just\nsaying \"mimics a cherry-pick operation\" alone is probably not enough\nto get the intended audience understand what you wanted to tell\nthem.\n\n    Side note.  I casually wrote \"is updated to point\" but with the\n    option not to update (but show the way to update refs), we'd\n    probably need to find a good phrase to express \"where the\n    command _wants_ to see the refs pointing at as the result\",\n    without referring to who/how the refs are made to point at these\n    points.\n\n> -To simply rebase `mybranch` onto `target`:\n> +To simply rebase `mybranch` onto `target` (default behavior):\n\n\"the default\"?\n\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index b64fc72063..457225363e 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -284,6 +284,26 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>  \treturn create_commit(repo, result->tree, pickme, replayed_base);\n>  }\n>  \n> +static int handle_ref_update(const char *mode,\n> +\t\t\t     struct ref_transaction *transaction,\n> +\t\t\t     const char *refname,\n> +\t\t\t     const struct object_id *new_oid,\n> +\t\t\t     const struct object_id *old_oid,\n> +\t\t\t     struct strbuf *err)\n> +{\n> +\tif (!strcmp(mode, \"print\")) {\n> +\t\tprintf(\"update %s %s %s\\n\",\n> +\t\t       refname,\n> +\t\t       oid_to_hex(new_oid),\n> +\t\t       oid_to_hex(old_oid));\n> +\t\treturn 0;\n> +\t}\n> +\n> +\t/* mode == \"yes\" - update refs directly */\n> +\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n> +\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n> +}\n\nHmph, would it be easier to follow if the above is symmetric, i.e.,\n\n\tif (...) {\n\t\twhat happens in the \"print\" mode\n\t} else {\n\t\twhat happens in the \"update ourselves\" mode\n\t}\n\nI wonder?\n\nIn any case, do not pass mode as \"const char *\" around in the call\nchain.  Instead, reduce it down to an enum or integer (with CPP\nmacro) at the earliest possible place after you saw the command line\noption.  That would allow you to even do\n\n\tswitch (ref_action) {\n\tcase PRINT_INSN:\n\t\tprintf(\"update ...\");\n\t\treturn 0;\n\tcase UPDATE_OURSELVES:\n\t\treturn ref_transaction_update(...);\n\tdefault:\n\t\tBUG(\"Bad ref_action %d\", ref_action);\n\t}\n\nto future-proof for the third option.\n\n> +\t\tOPT_STRING(0, \"update-refs\", &update_refs_mode,\n> +\t\t\t   N_(\"mode\"),\n> +\t\t\t   N_(\"control ref update behavior (yes|print)\")),\n>  \t\tOPT_END()\n>  \t};\n\nThis one is fine, but then immediately after parse_options()\nreturns, do something like\n\n\tif (!strcmp(update_refs_mode, \"print\"))\n\t\tref_action = PRINT_INSN;\n\telse if (!strcmp(update_refs_mode, \"yes\"))\n\t\tref_action = UPDATE_OURSELVES;\n\telse\n\t\tdie(_(\"unknown option --update-ref='%s'\"),\n\t\t    update_refs_mode);\n\nso that you do not have to keep strcmp() with \"print\", which risks\nyou to mistype \"prnit\" and no compiler would protect against that.\n\n"},{"id":"528776","messageId":"a72a2d7e-06ec-4275-812a-cb1e20902c90@gmail.com","threadId":"64109","inReplyTo":"xmqq7bwy1tgy.fsf@gitster.g","subject":"Re: [PATCH v3 0/3] replay: make atomic ref updates the default","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-15T04:57:13Z","receivedAt":"2025-10-15T04:57:22Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 14/10/25 01:09, Junio C Hamano wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>> **Removed --allow-partial option**\n>>\n>> After discussion with Elijah and Junio, we couldn't identify a concrete\n>> use case for partial failure tolerance. The traditional pipeline with\n>> git-update-ref already provides partial update capabilities when needed\n>> through its transaction commands. Removing this option simplifies the API\n>> and avoids committing to behavior without clear real-world use cases.\n> Ack.\n>\n>> **Changed to --update-refs=<mode> for extensibility**\n>>\n>> Phillip suggested that separate boolean flags (--output-commands,\n>> --allow-partial) were limiting for future expansion. The --update-refs=<mode>\n>> design allows future modes without option proliferation:\n>>    - --update-refs=yes (default): atomic ref updates\n>>    - --update-refs=print: pipeline output\n>>    - Future modes can be added as additional values\n>>\n>> This API pattern prevents the need for multiple incompatible flags and\n>> provides a cleaner interface for users.\n> Ack.\n>\n>> **Added replay.defaultAction configuration option**\n> If a configuration option is added, please consider and think hard\n> if its relationship with the command lineoption can be made obvious.\n> I do not think it is obvious to anybody that replay.defaultAction is\n> somehow tied to \"git replay --update-refs\" at all.  Either the\n> variable should be renamed to include words like \"update\" and/or\n> \"ref\" to hint its link to the option, or the option should be\n> renamed to use the word \"action\" to hint its link to the variable.\n\n\nYou are absolutely right - the disconnect between `replay.defaultAction` and\n`--update-refs` makes the relationship unclear. I chose `defaultAction` \nthinking\nit would be more extensible if we add other behaviors in the future, but \nthat\ncame at the cost of discoverability.\n\nLooking at how other Git commands handle this, I see a few patterns:\n- `commit.cleanup` ↔ `--cleanup=<mode>`\n- `push.default` ↔ (implicit push behavior)\n- `log.decorate` ↔ `--decorate=<mode>`\n\nGiven your feedback in the other thread about `--ref-action` potentially \nbeing\nclearer than `--update-refs`, would it make sense to align both?\n\nOption 1: `replay.refAction` ↔ `--ref-action=(update|print)`\nOption 2: `replay.updateRefs` ↔ `--update-refs=(yes|print)`\n\nI am leaning toward Option 1 because:\n- \"ref-action\" clearly conveys \"what action to take on refs\"\n- The config name `replay.refAction` directly mirrors the option\n- It's more obvious what the relationship is\n\nWhat do you think? I am happy to go with either approach or a different \nnaming\nscheme if you have a preference.\n\nThanks,\nSiddharth\n\n\n>\n>> The command-line --update-refs option overrides the config, allowing users\n>> to set a preference while maintaining per-invocation control.\n> That would follow the standard practice of configuration giving the\n> default that can be overriden via the command line option per\n> invocation, which would match end-user expectations.  Good.\n>\n> Thanks.\n"},{"id":"528777","messageId":"1065906e-01d2-4b1d-9b43-ce53c5ea9d1b@gmail.com","threadId":"64109","inReplyTo":"xmqqms5uzcd7.fsf@gitster.g","subject":"Re: [PATCH v3 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-15T05:01:35Z","receivedAt":"2025-10-15T05:01:43Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 14/10/25 03:35, Junio C Hamano wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>> For users needing the traditional pipeline workflow, add a new\n>> `--update-refs=<mode>` option that preserves the original behavior:\n>>\n>>    git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> The mode can be:\n>>    * `yes` (default): Update refs directly using an atomic transaction\n>>    * `print`: Output update-ref commands for pipeline use\n> Is it only me who still finds this awkward?  A question \"update?\"\n> that gets answered \"yes\" is quite understandable, but it is not\n> immediately obvious what it means to answer \"print\" to the same\n> question.  When the user gives the latter mode as the answer to the\n> question, the question being answered is not really \"do you want to\n> update refs?\" at all.\n>\n> The question the command wants the user to answer is more like \"what\n> action do you want to see performed on the refs?\", isn't it?  The\n> user would answer to the question with \"please update them\" to get\n> the default mode, while \"please print them\" may be the answer the\n> user would give to get the useful-for-dry-run-and-development mode.\n>\n> Perhaps phrase it more like \"--ref-action=(update|print)\"?  I dunno.\n\n\nThat's a really good point. I was thinking of it as \"update refs? \nyes/no\" where\n\"print\" meant \"don't update\", but you're right that it's actually asking a\ndifferent question entirely. The real question is \"what should we do \nwith the\nrefs?\" and the answer is either \"update them\" or \"print the commands\".\n\n`--ref-action=(update|print)` is much clearer because:\n- It explicitly asks \"what action?\"\n- Both values are verbs that answer that question consistently\n- It's immediately obvious what each mode does\n- It aligns with the config name discussion in the cover letter thread\n\nI will switch to `--ref-action` in the next version. This also means the \nconfig\nwould naturally be `replay.refAction`, which makes the relationship obvious.\n\n\n>\n>>   --advance <branch>::\n>>   \tStarting point at which to create the new commits; must be a\n>>   \tbranch name.\n>>   +\n>> -When `--advance` is specified, the update-ref command(s) in the output\n>> -will update the branch passed as an argument to `--advance` to point at\n>> -the new commits (in other words, this mimics a cherry-pick operation).\n>> +When `--advance` is specified, the branch passed as an argument will be\n>> +updated to point at the new commits (or an update command will be printed\n>> +if `--update-refs=print` is used). This mimics a cherry-pick operation.\n> I do not find it clear what the reference to cherry-pick is trying\n> to convey.  It is like cherry-picking <something> while the <branch>\n> is checked out (hence the branch advances as the result of acquiring\n> these commits from <something>)?  Let me see if I understood you by\n> attempting to rephrase.\n>\n>      The history is replayed on top of the <branch> and <branch> is\n>      updated to point at the tip of resulting history.\n\n\nYour phrasing is much better. The cherry-pick comparison was trying to \ncontrast\nwith `--onto` (which doesn't move the target branch), but it ended up \nbeing more\nconfusing than helpful. I will use your wording:\n\n     The history is replayed on top of the <branch> and <branch> is\n     updated to point at the tip of the resulting history. This is different\n     from `--onto`, which uses the target only as a starting point without\n     updating it.\n\n\n>\n> But what's the significance of saying so?  Did you want to contrast\n> it with \"rebase --onto <branch>\", i.e. merely specifying the\n> starting point without <branch> itself moving as the result?  If so,\n> it is probably a notable distinction worth pointing out, but just\n> saying \"mimics a cherry-pick operation\" alone is probably not enough\n> to get the intended audience understand what you wanted to tell\n> them.\n>\n>      Side note.  I casually wrote \"is updated to point\" but with the\n>      option not to update (but show the way to update refs), we'd\n>      probably need to find a good phrase to express \"where the\n>      command _wants_ to see the refs pointing at as the result\",\n>      without referring to who/how the refs are made to point at these\n>      points.\n>\n>> -To simply rebase `mybranch` onto `target`:\n>> +To simply rebase `mybranch` onto `target` (default behavior):\n> \"the default\"?\n\n\nGood catch - I was trying to emphasize that the atomic update behavior \nis now\ndefault, but in the context of showing example commands, \"default behavior\"\ndoesn't add clarity. I'll just say \"To simply rebase `mybranch` onto \n`target`:\"\n\n\n>\n>> diff --git a/builtin/replay.c b/builtin/replay.c\n>> index b64fc72063..457225363e 100644\n>> --- a/builtin/replay.c\n>> +++ b/builtin/replay.c\n>> @@ -284,6 +284,26 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>>   \treturn create_commit(repo, result->tree, pickme, replayed_base);\n>>   }\n>>   \n>> +static int handle_ref_update(const char *mode,\n>> +\t\t\t     struct ref_transaction *transaction,\n>> +\t\t\t     const char *refname,\n>> +\t\t\t     const struct object_id *new_oid,\n>> +\t\t\t     const struct object_id *old_oid,\n>> +\t\t\t     struct strbuf *err)\n>> +{\n>> +\tif (!strcmp(mode, \"print\")) {\n>> +\t\tprintf(\"update %s %s %s\\n\",\n>> +\t\t       refname,\n>> +\t\t       oid_to_hex(new_oid),\n>> +\t\t       oid_to_hex(old_oid));\n>> +\t\treturn 0;\n>> +\t}\n>> +\n>> +\t/* mode == \"yes\" - update refs directly */\n>> +\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n>> +\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n>> +}\n> Hmph, would it be easier to follow if the above is symmetric, i.e.,\n>\n> \tif (...) {\n> \t\twhat happens in the \"print\" mode\n> \t} else {\n> \t\twhat happens in the \"update ourselves\" mode\n> \t}\n>\n> I wonder?\n>\n> In any case, do not pass mode as \"const char *\" around in the call\n> chain.  Instead, reduce it down to an enum or integer (with CPP\n> macro) at the earliest possible place after you saw the command line\n> option.  That would allow you to even do\n>\n> \tswitch (ref_action) {\n> \tcase PRINT_INSN:\n> \t\tprintf(\"update ...\");\n> \t\treturn 0;\n> \tcase UPDATE_OURSELVES:\n> \t\treturn ref_transaction_update(...);\n> \tdefault:\n> \t\tBUG(\"Bad ref_action %d\", ref_action);\n> \t}\n>\n> to future-proof for the third option.\n\n\nPerfect, I will convert to an enum right after parse_options(). This \napproach is\nmuch cleaner and prevents typos like \"prnit\" that the compiler can't catch.\nSomething like:\n\n     enum ref_action_mode {\n         REF_ACTION_UPDATE,\n         REF_ACTION_PRINT\n     };\n\nThen parse it early:\n\n     if (!strcmp(ref_action_str, \"update\"))\n         ref_action = REF_ACTION_UPDATE;\n     else if (!strcmp(ref_action_str, \"print\"))\n         ref_action = REF_ACTION_PRINT;\n     else\n         die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n\nAnd use the switch statement in handle_ref_update(). This also makes it \ntrivial\nto add new modes in the future without string comparison overhead throughout\nthe code.\n\nThanks for the detailed review!\n\nSiddharth\n\n\n>\n>> +\t\tOPT_STRING(0, \"update-refs\", &update_refs_mode,\n>> +\t\t\t   N_(\"mode\"),\n>> +\t\t\t   N_(\"control ref update behavior (yes|print)\")),\n>>   \t\tOPT_END()\n>>   \t};\n> This one is fine, but then immediately after parse_options()\n> returns, do something like\n>\n> \tif (!strcmp(update_refs_mode, \"print\"))\n> \t\tref_action = PRINT_INSN;\n> \telse if (!strcmp(update_refs_mode, \"yes\"))\n> \t\tref_action = UPDATE_OURSELVES;\n> \telse\n> \t\tdie(_(\"unknown option --update-ref='%s'\"),\n> \t\t    update_refs_mode);\n>\n> so that you do not have to keep strcmp() with \"print\", which risks\n> you to mistype \"prnit\" and no compiler would protect against that.\n>\n"},{"id":"528800","messageId":"CAP8UFD1LJkVmn4GFE2jmPGORRNVOe=vuC38fmra1TVL8cAsqRw@mail.gmail.com","threadId":"64109","inReplyTo":"a72a2d7e-06ec-4275-812a-cb1e20902c90@gmail.com","subject":"Re: [PATCH v3 0/3] replay: make atomic ref updates the default","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-10-15T10:33:19Z","receivedAt":"2025-10-15T10:33:33Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, Oct 15, 2025 at 6:57 AM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n\n> Given your feedback in the other thread about `--ref-action` potentially\n> being\n> clearer than `--update-refs`, would it make sense to align both?\n>\n> Option 1: `replay.refAction` ↔ `--ref-action=(update|print)`\n> Option 2: `replay.updateRefs` ↔ `--update-refs=(yes|print)`\n>\n> I am leaning toward Option 1 because:\n> - \"ref-action\" clearly conveys \"what action to take on refs\"\n> - The config name `replay.refAction` directly mirrors the option\n> - It's more obvious what the relationship is\n>\n> What do you think? I am happy to go with either approach or a different\n> naming\n> scheme if you have a preference.\n\nI prefer Option 1.\n\nThanks.\n"},{"id":"528817","messageId":"xmqq7bwww7dv.fsf@gitster.g","threadId":"64109","inReplyTo":"a72a2d7e-06ec-4275-812a-cb1e20902c90@gmail.com","subject":"Re: [PATCH v3 0/3] replay: make atomic ref updates the default","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-15T14:45:48Z","receivedAt":"2025-10-15T14:45:51Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> Option 1: `replay.refAction` ↔ `--ref-action=(update|print)`\n> Option 2: `replay.updateRefs` ↔ `--update-refs=(yes|print)`\n>\n> I am leaning toward Option 1 because:\n> - \"ref-action\" clearly conveys \"what action to take on refs\"\n> - The config name `replay.refAction` directly mirrors the option\n> - It's more obvious what the relationship is\n>\n> What do you think? I am happy to go with either approach or a\n> different naming scheme if you have a preference.\n\nMy preference is the refAction, simply because updateRefs sounds to\nme like it is asking \"do you want me to update refs?  Yes or no?\".\n\nBut perhaps there were those who supported updateRefs during the\npast reviews I wasn't looking at, so I'd like to hear if my thinking\nis missing something that were taken into consideration to come up\nwith that name.\n\nThanks.\n\n\n"},{"id":"529436","messageId":"20251022185045.29256-1-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251013183311.33329-1-siddharthasthana31@gmail.com","subject":"[PATCH v4 0/3] replay: make atomic ref updates the default","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-22T18:50:42Z","receivedAt":"2025-10-22T18:50:58Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"This is v4 of the git-replay atomic updates series.\n\nBased on feedback from v3, this version improves the naming and\nimplementation for clarity and type safety. Thanks to Junio, Christian,\nElijah, Phillip, Patrick, and Karthik for the detailed reviews.\n\n## Changes in v4\n\n**Renamed --update-refs to --ref-action**\n\nJunio pointed out that \"--update-refs=print\" is semantically awkward.\nAnswering \"print\" to the question \"update refs?\" doesn't make sense.\nThe actual question is \"what action should we take on the refs?\"\n\nChanged to --ref-action=(update|print) where both values are verbs that\nanswer \"what action?\". This makes the interface clearer.\n\n**Aligned config name with command-line option**\n\nChanged replay.defaultAction to replay.refAction. The config variable\nnow mirrors the option name, making the relationship obvious.\n\n**Unified config and command-line values**\n\nv3 had confusing value mapping:\n  - Command-line: yes/print\n  - Config: update-refs/show-commands\n\nv4 uses the same values everywhere:\n  - Command-line: update/print\n  - Config: update/print\n\nThis eliminates the need for mental mapping.\n\n**Converted to type-safe enum implementation**\n\nPer Junio's suggestion, added enum ref_action_mode and convert the mode\nstring immediately after parse_options():\n\n  enum ref_action_mode {\n      REF_ACTION_UPDATE,\n      REF_ACTION_PRINT\n  };\n\nThis provides compiler protection against typos and allows clear switch\nstatements with BUG() defaults instead of if/else chains.\n\n**Fixed t0450 synopsis test**\n\nThe usage string now matches the documentation SYNOPSIS exactly. This\ntest was failing in v3.\n\n**Improved --advance documentation**\n\nRemoved confusing cherry-pick reference and used Junio's clearer\nwording to explain what --advance does versus --onto.\n\n## Technical Implementation\n\nSame as v3, using Git's ref transaction API:\n  - ref_store_transaction_begin() for atomic transactions\n  - ref_transaction_update() to stage updates\n  - ref_transaction_commit() for atomic application\n\nNew in v4: The handle_ref_update() helper takes an enum parameter and\nuses a switch statement instead of string comparisons.\n\n## Testing\n\nAll tests pass:\n  - t0450-txt-doc-vs-help.sh (now passing, was failing in v3)\n  - t3650-replay-basics.sh (all 18 tests pass)\n\nTest changes: All existing tests updated to use --ref-action=print\ninstead of --update-refs=print. New config tests verify\nreplay.refAction behavior.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\nChanges in v4:\n- Rename --update-refs to --ref-action for semantic clarity.\n- Rename replay.defaultAction to replay.refAction to mirror option name.\n- Unify command-line and config values to both use update/print.\n- Convert implementation to use enum instead of string comparisons.\n- Use switch statement with BUG() default in handle_ref_update().\n- Fix t0450 synopsis mismatch.\n- Improve --advance documentation clarity.\n- Update all tests to use --ref-action.\n\nChanges in v3:\n- Removed --allow-partial option (no concrete use cases).\n- Changed to --update-refs=<mode> for extensibility.\n- Added replay.defaultAction configuration option.\n- Improved commit messages with Helped-by trailers.\n- Enhanced test suite with proper isolation.\n- Extracted handle_ref_update() helper function.\n\n---\n Documentation/config/replay.adoc |  11 +++\n Documentation/git-replay.adoc    |  65 +++++++++++------\n builtin/replay.c                 | 118 +++++++++++++++++++++++++++----\n t/t3650-replay-basics.sh         |  58 ++++++++++++---\n 4 files changed, 209 insertions(+), 43 deletions(-)\n create mode 100644 Documentation/config/replay.adoc\n\nSiddharth Asthana (3):\n  replay: use die_for_incompatible_opt2() for option validation\n  replay: make atomic ref updates the default behavior\n  replay: add replay.refAction config option\n\nRange-diff against v3:\n-:  ---------- > 1:  baa0cfdd4a replay: use die_for_incompatible_opt2() for option validation\n-:  ---------- > 2:  3b5df166f3 replay: make atomic ref updates the default behavior\n    @@ Metadata\n     Author: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## Commit message ##\n         replay: make atomic ref updates the default behavior\n         \n         [Commit message unchanged - explains problem and solution]\n         \n         For users needing the traditional pipeline workflow, add a new\n    -    `--update-refs=<mode>` option that preserves the original behavior:\n    +    --ref-action=<mode> option that preserves the original behavior:\n         \n    -      git replay --update-refs=print --onto main topic1..topic2 | git update-ref --stdin\n    +      git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin\n         \n         The mode can be:\n    -      * `yes` (default): Update refs directly using an atomic transaction\n    +      * update (default): Update refs directly using an atomic transaction\n           * `print`: Output update-ref commands for pipeline use\n         \n         Implementation details:\n         \n         The atomic ref updates are implemented using Git's ref transaction API.\n    -    In cmd_replay(), when not in 'print' mode, we initialize a transaction\n    +    In cmd_replay(), when not in print mode, we initialize a transaction\n         using ref_store_transaction_begin() with the default atomic behavior.\n         As commits are replayed, ref updates are staged into the transaction\n         using ref_transaction_update(). Finally, ref_transaction_commit()\n         applies all updates atomically—either all updates succeed or none do.\n         \n    -    To avoid code duplication between the 'print' and 'yes' modes, this\n    +    To avoid code duplication between the print and update modes, this\n         commit extracts a handle_ref_update() helper function. This function\n    -    takes the mode and either prints the update command or stages it into\n    -    the transaction. This keeps both code paths consistent and makes future\n    -    maintenance easier.\n    +    takes the mode (as an enum) and either prints the update command or\n    +    stages it into the transaction. Using an enum rather than passing the\n    +    string around provides type safety and allows the compiler to catch\n    +    typos. The switch statement makes it easy to add future modes.\n         \n         The helper function signature:\n         \n    -      static int handle_ref_update(const char *mode,\n    +      static int handle_ref_update(enum ref_action_mode mode,\n                                         struct ref_transaction *transaction,\n                                         const char *refname,\n                                         const struct object_id *new_oid,\n                                         const struct object_id *old_oid,\n                                         struct strbuf *err)\n         \n    -    When mode is 'print', it prints the update-ref command. When mode is\n    -    'yes', it calls ref_transaction_update() to stage the update. This\n    -    eliminates the duplication that would otherwise exist at each ref update\n    -    call site.\n    +    The enum is defined as:\n    +    \n    +      enum ref_action_mode {\n    +          REF_ACTION_UPDATE,\n    +          REF_ACTION_PRINT\n    +      };\n    +    \n    +    The mode string is converted to enum immediately after parse_options()\n    +    to avoid string comparisons throughout the codebase and provide compiler\n    +    protection against typos.\n         \n         Test suite changes:\n         \n         All existing tests that expected command output now use\n    -    `--update-refs=print` to preserve their original behavior. This keeps\n    +    --ref-action=print to preserve their original behavior. This keeps\n         the tests valid while allowing them to verify that the pipeline workflow\n         still works correctly.\n         \n         [Rest of test section unchanged]\n         \n    -    A following commit will add a `replay.defaultAction` configuration\n    +    A following commit will add a replay.refAction configuration\n         option for users who prefer the traditional pipeline output as their\n         default behavior.\n         \n         Helped-by: Elijah Newren <newren@gmail.com>\n         Helped-by: Patrick Steinhardt <ps@pks.im>\n         Helped-by: Christian Couder <christian.couder@gmail.com>\n         Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## Documentation/git-replay.adoc ##\n     @@ Documentation/git-replay.adoc: SYNOPSIS\n      [verse]\n     -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n    -+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>)\n    -+\t\t[--update-refs[=<mode>]] <revision-range>...\n    ++(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n      \n      ## Documentation/git-replay.adoc: DESCRIPTION\n     -the working tree and the index untouched, and updates no references.\n     -The output of this command is meant to be used as input to\n     -`git update-ref --stdin`, which would update the relevant branches\n     +the working tree and the index untouched. By default, updates the\n     +relevant references using an atomic transaction (all refs update or\n    -+none). Use `--update-refs=print` to avoid automatic ref updates and\n    ++none). Use `--ref-action=print` to avoid automatic ref updates and\n     +instead get update commands that can be piped to `git update-ref --stdin`\n      (see the OUTPUT section below).\n      \n     @@ Documentation/git-replay.adoc: OPTIONS\n     -When `--onto` is specified, the update-ref command(s) in the output will\n     -update the branch(es) in the revision range to point at the new\n     +When `--onto` is specified, the branch(es) in the revision range will be\n     +updated to point at the new commits (or update commands will be printed\n    -+if `--update-refs=print` is used), similar to the way how\n    ++if `--ref-action=print` is used), similar to the way how\n     +`git rebase --update-refs` updates multiple branches in the affected range.\n      \n      --advance <branch>::\n     -When `--advance` is specified, the update-ref command(s) in the output\n     -will update the branch passed as an argument to `--advance` to point at\n     -the new commits (in other words, this mimics a cherry-pick operation).\n    -+When `--advance` is specified, the branch passed as an argument will be\n    -+updated to point at the new commits (or an update command will be printed\n    -+if `--update-refs=print` is used). This mimics a cherry-pick operation.\n    ++The history is replayed on top of the <branch> and <branch> is updated to\n    ++point at the tip of the resulting history (or an update command will be\n    ++printed if `--ref-action=print` is used). This is different from `--onto`,\n    ++which uses the target only as a starting point without updating it.\n      \n    -+--update-refs[=<mode>]::\n    ++--ref-action[=<mode>]::\n     +\tControl how references are updated. The mode can be:\n     +--\n    -+* `yes` (default): Update refs directly using an atomic transaction.\n    -+  All ref updates succeed or all fail.\n    ++\t* `update` (default): Update refs directly using an atomic transaction.\n    ++\t  All refs are updated or none are (all-or-nothing behavior).\n     +* `print`: Output update-ref commands for pipeline use. This is the\n    -+  The output can be piped as-is to `git update-ref --stdin`.\n    ++\t  traditional behavior where output can be piped to `git update-ref --stdin`.\n     +--\n    ++\n    ++The default mode can be configured via `replay.refAction` configuration option.\n      \n     @@ Documentation/git-replay.adoc: OUTPUT\n     -When there are no conflicts, the output of this command is usable as\n     -input to `git update-ref --stdin`.  It is of the form:\n    -+By default, when there are no conflicts, this command updates the relevant\n    -+references using an atomic transaction and produces no output. All ref\n    -+updates succeed or all fail.\n    ++By default (with `--ref-action=update`), this command produces no output on\n    ++success, as refs are updated directly using an atomic transaction.\n     +\n    -+When `--update-refs=print` is used, the output is usable as input to\n    ++When using `--ref-action=print`, the output is usable as input to\n     +`git update-ref --stdin`. It is of the form:\n      \n     @@ Documentation/git-replay.adoc: EXAMPLES\n     -To simply rebase `mybranch` onto `target`:\n    -+To simply rebase `mybranch` onto `target` (default behavior):\n    ++To simply rebase `mybranch` onto `target`:\n      \n      ------------\n      $ git replay --onto target origin/main..mybranch\n     -update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n      ------------\n      \n     @@ Documentation/git-replay.adoc: EXAMPLES\n     +To get the traditional pipeline output:\n     +\n     +------------\n    -+$ git replay --update-refs=print --onto target origin/main..mybranch\n    ++$ git replay --ref-action=print --onto target origin/main..mybranch\n     +update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n     +------------\n      \n      ## builtin/replay.c ##\n    ++@@ builtin/replay.c\n    ++ #include <oidset.h>\n    ++ #include <tree.h>\n    ++ \n    ++enum ref_action_mode {\n    ++\tREF_ACTION_UPDATE,\n    ++\tREF_ACTION_PRINT\n    ++};\n    ++\n    ++ static const char *short_commit_name(struct repository *repo,\n    ++ \n     @@ builtin/replay.c: static struct commit *pick_regular_commit\n      \treturn create_commit(repo, result->tree, pickme, replayed_base);\n      }\n      \n    -+static int handle_ref_update(const char *mode,\n    ++static int handle_ref_update(enum ref_action_mode mode,\n     +\t\t\t     struct ref_transaction *transaction,\n     +\t\t\t     const char *refname,\n     +\t\t\t     const struct object_id *new_oid,\n     +\t\t\t     const struct object_id *old_oid,\n     +\t\t\t     struct strbuf *err)\n     +{\n    -+\tif (!strcmp(mode, \"print\")) {\n    ++\tswitch (mode) {\n    ++\tcase REF_ACTION_PRINT:\n     +\t\tprintf(\"update %s %s %s\\n\",\n     +\t\t       refname,\n     +\t\t       oid_to_hex(new_oid),\n     +\t\t       oid_to_hex(old_oid));\n     +\t\treturn 0;\n    -+\t}\n    -+\n    -+\t/* mode == \"yes\" - update refs directly */\n    -+\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n    -+\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n    ++\tcase REF_ACTION_UPDATE:\n    ++\t\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n    ++\t\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n    ++\tdefault:\n    ++\t\tBUG(\"unknown ref_action_mode %d\", mode);\n    ++\t}\n     +}\n      \n     @@ builtin/replay.c: int cmd_replay\n      \tstruct commit *onto = NULL;\n      \tconst char *onto_name = NULL;\n      \tint contained = 0;\n    -+\tconst char *update_refs_mode = NULL;\n    ++\tconst char *ref_action_str = NULL;\n    ++\tenum ref_action_mode ref_action = REF_ACTION_UPDATE;\n      \n     @@ builtin/replay.c: int cmd_replay\n     - \tconst char * const replay_usage[] = {\n    + \tconst char *const replay_usage[] = {\n      \t\tN_(\"(EXPERIMENTAL!) git replay \"\n      \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n    -+\t\t   \"[--update-refs[=<mode>]] <revision-range>...\"),\n    ++\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n      \t\tNULL\n      \t};\n      \n     @@ builtin/replay.c: int cmd_replay\n    -+\t\tOPT_STRING(0, \"update-refs\", &update_refs_mode,\n    ++\t\tOPT_STRING(0, \"ref-action\", &ref_action_str,\n     +\t\t\t   N_(\"mode\"),\n    -+\t\t\t   N_(\"control ref update behavior (yes|print)\")),\n    ++\t\t\t   N_(\"control ref update behavior (update|print)\")),\n      \t\tOPT_END()\n      \t};\n      \n     @@ builtin/replay.c: int cmd_replay\n      \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n      \t\t\t\t  contained, \"--contained\");\n      \n    -+\t/* Set default mode if not specified */\n    -+\tif (!update_refs_mode)\n    -+\t\tupdate_refs_mode = \"yes\";\n    -+\n    -+\t/* Validate update-refs mode */\n    -+\tif (strcmp(update_refs_mode, \"yes\") && strcmp(update_refs_mode, \"print\"))\n    -+\t\tdie(_(\"invalid value for --update-refs: '%s' (expected 'yes' or 'print')\"),\n    -+\t\t    update_refs_mode);\n    ++\t/* Parse ref action mode */\n    ++\tif (!strcmp(ref_action_str, \"update\"))\n    ++\t\tref_action = REF_ACTION_UPDATE;\n    ++\telse if (!strcmp(ref_action_str, \"print\"))\n    ++\t\tref_action = REF_ACTION_PRINT;\n    ++\telse\n    ++\t\tdie(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n      \n     @@ builtin/replay.c: int cmd_replay\n      \t/* Initialize ref transaction if we're updating refs directly */\n    -+\tif (!strcmp(update_refs_mode, \"yes\")) {\n    ++\tif (ref_action == REF_ACTION_UPDATE) {\n      \t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n      \n     @@ builtin/replay.c: int cmd_replay\n    -+\t\t\t\tif (handle_ref_update(update_refs_mode, transaction,\n    ++\t\t\t\tif (handle_ref_update(ref_action, transaction,\n     +\t\t\t\t\t\t      decoration->name,\n     +\t\t\t\t\t\t      &last_commit->object.oid,\n     +\t\t\t\t\t\t      &commit->object.oid,\n      \n     @@ t/t3650-replay-basics.sh\n      test_expect_success 'using replay to rebase two branches, one on top of other' '\n     -\tgit replay --onto main topic1..topic2 >result &&\n    -+\tgit replay --update-refs=print --onto main topic1..topic2 >result &&\n    ++\tgit replay --ref-action=print --onto main topic1..topic2 >result &&\n      \n     @@ t/t3650-replay-basics.sh\n      test_expect_success 'using replay to perform basic cherry-pick' '\n     -\tgit replay --advance main topic1..topic2 >result &&\n    -+\tgit replay --update-refs=print --advance main topic1..topic2 >result &&\n    ++\tgit replay --ref-action=print --advance main topic1..topic2 >result &&\n      \n     @@ t/t3650-replay-basics.sh\n      test_expect_success 'using replay to also rebase a contained branch' '\n     -\tgit replay --contained --onto main main..topic3 >result &&\n    -+\tgit replay --update-refs=print --contained --onto main main..topic3 >result &&\n    ++\tgit replay --ref-action=print --contained --onto main main..topic3 >result &&\n      \n     @@ t/t3650-replay-basics.sh\n     +# Tests for atomic ref update behavior\n     +\n     +[New tests added - content unchanged from v3]\n     +\n    -+test_expect_success 'replay validates --update-refs mode values' '\n    -+\ttest_must_fail git replay --update-refs=invalid --onto main topic1..topic2 2>error &&\n    -+\tgrep \"invalid value for --update-refs\" error\n    ++test_expect_success 'replay validates --ref-action mode values' '\n    ++\ttest_must_fail git replay --ref-action=invalid --onto main topic1..topic2 2>error &&\n    ++\tgrep \"invalid value for --ref-action\" error\n     +'\n      \n-:  ---------- > 3:  c35049881d replay: add replay.refAction config option\n    @@ Metadata\n     Author: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## Commit message ##\n    -    replay: add replay.defaultAction config option\n    +    replay: add replay.refAction config option\n         \n         Add a configuration option to control the default behavior of git replay\n         for updating references. This allows users who prefer the traditional\n    -    pipeline output to set it once in their config instead of passing\n    -    --update-refs=print with every command.\n    +    pipeline output to set it once in their config instead of passing\n    +    --ref-action=print with every command.\n         \n    -    The config option uses enum string values for extensibility:\n    -      * replay.defaultAction = update-refs (default): atomic ref updates\n    -      * replay.defaultAction = show-commands: output commands for pipeline\n    +    The config option uses string values that mirror the behavior modes:\n    +      * replay.refAction = update (default): atomic ref updates\n    +      * replay.refAction = print: output commands for pipeline\n         \n    -    The command-line --update-refs option always overrides the config setting,\n    +    The command-line --ref-action option always overrides the config setting,\n         allowing users to temporarily change behavior for a single invocation.\n         \n         [Implementation details mostly unchanged except names]\n         \n    -    The command-line --update-refs option, when provided, overrides the\n    +    The command-line --ref-action option, when provided, overrides the\n         config value. This precedence allows users to set their preferred default\n         while still having per-invocation control:\n         \n    -      git config replay.defaultAction show-commands  # Set default\n    -      git replay --update-refs=yes --onto main topic  # Override once\n    +      git config replay.refAction print         # Set default\n    +      git replay --ref-action=update --onto main topic  # Override once\n         \n    -    The config option uses different value names ('update-refs' vs\n    -    'show-commands') compared to the command-line option ('yes' vs 'print')\n    -    for semantic clarity. The config values describe what action is being\n    -    taken, while the command-line values are terse for typing convenience.\n    -    \n    -    The enum string design (rather than a boolean like 'replay.updateRefs')\n    -    allows future expansion to additional modes without requiring new\n    -    configuration variables.\n    +    The config and command-line option use the same value names ('update'\n    +    and 'print') for consistency and clarity. This makes it immediately\n    +    obvious how the config maps to the command-line option, addressing\n    +    feedback about the relationship between configuration and command-line\n    +    options being clear to users.\n         \n         Helped-by: Junio C Hamano <gitster@pobox.com>\n         Helped-by: Elijah Newren <newren@gmail.com>\n    +    Helped-by: Christian Couder <christian.couder@gmail.com>\n         Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n         Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n      \n      ## Documentation/config/replay.adoc (new) ##\n    -+replay.defaultAction::\n    -+\tControl the default behavior of `git replay` for updating references.\n    ++replay.refAction::\n    ++\tSpecifies the default mode for handling reference updates in\n    ++\t`git replay`. The value can be:\n     +--\n    -+* `update-refs` (default): Update refs directly using an atomic transaction.\n    -+* `show-commands`: Output update-ref commands that can be piped to\n    -+  `git update-ref --stdin`.\n    ++\t* `update`: Update refs directly using an atomic transaction (default behavior).\n    ++\t* `print`: Output update-ref commands for pipeline use.\n     +--\n    -+This can be overridden with the `--update-refs` command-line option.\n    -+Note that the command-line option uses slightly different values\n    -+(`yes` and `print`) for brevity, but they map to the same behavior\n    -+as the config values.\n    ++This setting can be overridden with the `--ref-action` command-line option.\n    ++When not configured, `git replay` defaults to `update` mode.\n      \n      ## builtin/replay.c ##\n     +#include \"builtin.h\"\n     +#include \"config.h\"\n      \n     @@ builtin/replay.c: int cmd_replay\n      \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n      \t\t\t\t  contained, \"--contained\");\n      \n    -+\t/* Set default mode from config if not specified on command line */\n    -+\tif (!update_refs_mode) {\n    ++\t/* Set default from config if not specified on command line */\n    ++\tif (!ref_action_str) {\n     +\t\tconst char *config_value = NULL;\n    -+\t\tif (!repo_config_get_string_tmp(repo, \"replay.defaultaction\", &config_value)) {\n    -+\t\t\tif (!strcmp(config_value, \"update-refs\"))\n    -+\t\t\t\tupdate_refs_mode = \"yes\";\n    -+\t\t\telse if (!strcmp(config_value, \"show-commands\"))\n    -+\t\t\t\tupdate_refs_mode = \"print\";\n    ++\t\tif (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value)) {\n    ++\t\t\tif (!strcmp(config_value, \"update\"))\n    ++\t\t\t\tref_action_str = \"update\";\n    ++\t\t\telse if (!strcmp(config_value, \"print\"))\n    ++\t\t\t\tref_action_str = \"print\";\n     +\t\t\telse\n    -+\t\t\t\tdie(_(\"invalid value for replay.defaultAction: '%s' \"\n    -+\t\t\t\t      \"(expected 'update-refs' or 'show-commands')\"),\n    ++\t\t\t\tdie(_(\"invalid value for replay.refAction: '%s'\"),\n     +\t\t\t\t    config_value);\n     +\t\t}\n    -+\t} else {\n    -+\t\tupdate_refs_mode = \"yes\";\n     +\t}\n    ++\n    ++\t/* Default to update mode if still not set */\n    ++\tif (!ref_action_str)\n    ++\t\tref_action_str = \"update\";\n      \n     @@ t/t3650-replay-basics.sh\n    -+test_expect_success 'replay.defaultAction config option' '\n    ++test_expect_success 'replay.refAction config option' '\n     +\tSTART=$(git rev-parse topic2) &&\n    -+\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.defaultAction\" &&\n    ++\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n     +\n    -+\tgit config replay.defaultAction show-commands &&\n    ++\tgit config replay.refAction print &&\n     +\tgit replay --onto main topic1..topic2 >output &&\n     +\ttest_line_count = 1 output &&\n     +\tgrep \"^update refs/heads/topic2 \" output &&\n     +\n     +\tgit branch -f topic2 $START &&\n    -+\tgit config replay.defaultAction update-refs &&\n    ++\tgit config replay.refAction update &&\n     +\tgit replay --onto main topic1..topic2 >output &&\n     +\ttest_must_be_empty output &&\n     +\t[...]\n     +'\n     +\n    -+test_expect_success 'command-line --update-refs overrides config' '\n    ++test_expect_success 'command-line --ref-action overrides config' '\n     +\tSTART=$(git rev-parse topic2) &&\n    -+\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.defaultAction\" &&\n    -+\tgit config replay.defaultAction update-refs &&\n    -+\tgit replay --update-refs=print --onto main topic1..topic2 >output &&\n    ++\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n    ++\tgit config replay.refAction update &&\n    ++\tgit replay --ref-action=print --onto main topic1..topic2 >output &&\n     +\ttest_line_count = 1 output &&\n     +\tgrep \"^update refs/heads/topic2 \" output\n     +'\n     +\n    -+test_expect_success 'invalid replay.defaultAction value' '\n    -+\ttest_when_finished \"git config --unset replay.defaultAction\" &&\n    -+\tgit config replay.defaultAction invalid &&\n    ++test_expect_success 'invalid replay.refAction value' '\n    ++\ttest_when_finished \"git config --unset replay.refAction\" &&\n    ++\tgit config replay.refAction invalid &&\n     +\ttest_must_fail git replay --onto main topic1..topic2 2>error &&\n    -+\tgrep \"invalid value for replay.defaultAction\" error\n    ++\tgrep \"invalid value for replay.refAction\" error\n     +'\n-- \n2.51.0\n\nbase-commit: 133d151831d291e51d55c80a40684eb6c8dfdd24\n\nThanks\n- Siddharth\n\n"},{"id":"529437","messageId":"20251022185045.29256-2-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251022185045.29256-1-siddharthasthana31@gmail.com","subject":"[PATCH v4 1/3] replay: use die_for_incompatible_opt2() for option validation","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-22T18:50:43Z","receivedAt":"2025-10-22T18:51:11Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"In preparation for adding the --ref-action option, convert option\nvalidation to use die_for_incompatible_opt2(). This helper provides\nstandardized error messages for mutually exclusive options.\n\nThe following commit introduces --ref-action which will be incompatible\nwith certain other options. Using die_for_incompatible_opt2() now means\nthat commit can cleanly add its validation using the same pattern,\nkeeping the validation logic consistent and maintainable.\n\nThis also aligns git-replay's option handling with how other Git commands\nmanage option conflicts, using the established die_for_incompatible_opt*()\nhelper family.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n builtin/replay.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 6172c8aacc..b64fc72063 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -330,9 +330,9 @@ int cmd_replay(int argc,\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n-\tif (advance_name_opt && contained)\n-\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n-\t\t    \"--advance\", \"--contained\");\n+\tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n+\t\t\t\t  contained, \"--contained\");\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n-- \n2.51.0\n\n"},{"id":"529438","messageId":"20251022185045.29256-3-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251022185045.29256-1-siddharthasthana31@gmail.com","subject":"[PATCH v4 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-22T18:50:44Z","receivedAt":"2025-10-22T18:51:19Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"The git replay command currently outputs update commands that can be\npiped to update-ref to achieve a rebase, e.g.\n\n  git replay --onto main topic1..topic2 | git update-ref --stdin\n\nThis separation had advantages for three special cases:\n  * it made testing easy (when state isn't modified from one step to\n    the next, you don't need to make temporary branches or have undo\n    commands, or try to track the changes)\n  * it provided a natural can-it-rebase-cleanly (and what would it\n    rebase to) capability without automatically updating refs, similar\n    to a --dry-run\n  * it provided a natural low-level tool for the suite of hash-object,\n    mktree, commit-tree, mktag, merge-tree, and update-ref, allowing\n    users to have another building block for experimentation and making\n    new tools\n\nHowever, it should be noted that all three of these are somewhat\nspecial cases; users, whether on the client or server side, would\nalmost certainly find it more ergonomical to simply have the updating\nof refs be the default.\n\nFor server-side operations in particular, the pipeline architecture\ncreates process coordination overhead. Server implementations that need\nto perform rebases atomically must maintain additional code to:\n\n  1. Spawn and manage a pipeline between git-replay and git-update-ref\n  2. Coordinate stdout/stderr streams across the pipe boundary\n  3. Handle partial failure states if the pipeline breaks mid-execution\n  4. Parse and validate the update-ref command output\n\nChange the default behavior to update refs directly, and atomically (at\nleast to the extent supported by the refs backend in use). This\neliminates the process coordination overhead for the common case.\n\nFor users needing the traditional pipeline workflow, add a new\n--ref-action=<mode> option that preserves the original behavior:\n\n  git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin\n\nThe mode can be:\n  * update (default): Update refs directly using an atomic transaction\n  * print: Output update-ref commands for pipeline use\n\nImplementation details:\n\nThe atomic ref updates are implemented using Git's ref transaction API.\nIn cmd_replay(), when not in `print` mode, we initialize a transaction\nusing ref_store_transaction_begin() with the default atomic behavior.\nAs commits are replayed, ref updates are staged into the transaction\nusing ref_transaction_update(). Finally, ref_transaction_commit()\napplies all updates atomically—either all updates succeed or none do.\n\nTo avoid code duplication between the 'print' and 'update' modes, this\ncommit extracts a handle_ref_update() helper function. This function\ntakes the mode (as an enum) and either prints the update command or\nstages it into the transaction. Using an enum rather than passing the\nstring around provides type safety and allows the compiler to catch\ntypos. The switch statement makes it easy to add future modes.\n\nThe helper function signature:\n\n  static int handle_ref_update(enum ref_action_mode mode,\n                                struct ref_transaction *transaction,\n                                const char *refname,\n                                const struct object_id *new_oid,\n                                const struct object_id *old_oid,\n                                struct strbuf *err)\n\nThe enum is defined as:\n\n  enum ref_action_mode {\n      REF_ACTION_UPDATE,\n      REF_ACTION_PRINT\n  };\n\nThe mode string is converted to enum immediately after parse_options()\nto avoid string comparisons throughout the codebase and provide compiler\nprotection against typos.\n\nTest suite changes:\n\nAll existing tests that expected command output now use\n--ref-action=print to preserve their original behavior. This keeps\nthe tests valid while allowing them to verify that the pipeline workflow\nstill works correctly.\n\nNew tests were added to verify:\n  - Default atomic behavior (no output, refs updated directly)\n  - Bare repository support (server-side use case)\n  - Equivalence between traditional pipeline and atomic updates\n  - Real atomicity using a lock file to verify all-or-nothing guarantee\n  - Test isolation using test_when_finished to clean up state\n\nThe bare repository tests were fixed to rebuild their expectations\nindependently rather than comparing to previous test output, improving\ntest reliability and isolation.\n\nA following commit will add a replay.refAction configuration\noption for users who prefer the traditional pipeline output as their\ndefault behavior.\n\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc | 65 +++++++++++++++--------\n builtin/replay.c              | 98 +++++++++++++++++++++++++++++++----\n t/t3650-replay-basics.sh      | 16 +++---\n 3 files changed, 139 insertions(+), 40 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 0b12bf8aa4..759028dc28 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n \n DESCRIPTION\n -----------\n \n Takes ranges of commits and replays them onto a new location. Leaves\n-the working tree and the index untouched, and updates no references.\n-The output of this command is meant to be used as input to\n-`git update-ref --stdin`, which would update the relevant branches\n+the working tree and the index untouched. By default, updates the\n+relevant references using an atomic transaction (all refs update or\n+none). Use `--ref-action=print` to avoid automatic ref updates and\n+instead get update commands that can be piped to `git update-ref --stdin`\n (see the OUTPUT section below).\n \n THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n@@ -29,18 +30,31 @@ OPTIONS\n \tStarting point at which to create the new commits.  May be any\n \tvalid commit, and not just an existing branch name.\n +\n-When `--onto` is specified, the update-ref command(s) in the output will\n-update the branch(es) in the revision range to point at the new\n-commits, similar to the way how `git rebase --update-refs` updates\n-multiple branches in the affected range.\n+When `--onto` is specified, the branch(es) in the revision range will be\n+updated to point at the new commits (or update commands will be printed\n+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`\n+updates multiple branches in the affected range.\n \n --advance <branch>::\n \tStarting point at which to create the new commits; must be a\n \tbranch name.\n +\n-When `--advance` is specified, the update-ref command(s) in the output\n-will update the branch passed as an argument to `--advance` to point at\n-the new commits (in other words, this mimics a cherry-pick operation).\n+The history is replayed on top of the <branch> and <branch> is updated to\n+point at the tip of the resulting history (or an update command will be\n+printed if `--ref-action=print` is used). This is different from `--onto`,\n+which uses the target only as a starting point without updating it.\n+\n+--ref-action[=<mode>]::\n+\tControl how references are updated. The mode can be:\n++\n+--\n+\t* `update` (default): Update refs directly using an atomic transaction.\n+\t  All refs are updated or none are (all-or-nothing behavior).\n+\t* `print`: Output update-ref commands for pipeline use. This is the\n+\t  traditional behavior where output can be piped to `git update-ref --stdin`.\n+--\n++\n+The default mode can be configured via `replay.refAction` configuration option.\n \n <revision-range>::\n \tRange of commits to replay. More than one <revision-range> can\n@@ -54,8 +68,11 @@ include::rev-list-options.adoc[]\n OUTPUT\n ------\n \n-When there are no conflicts, the output of this command is usable as\n-input to `git update-ref --stdin`.  It is of the form:\n+By default (with `--ref-action=update`), this command produces no output on\n+success, as refs are updated directly using an atomic transaction.\n+\n+When using `--ref-action=print`, the output is usable as input to\n+`git update-ref --stdin`. It is of the form:\n \n \tupdate refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n \tupdate refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n@@ -81,6 +98,14 @@ To simply rebase `mybranch` onto `target`:\n \n ------------\n $ git replay --onto target origin/main..mybranch\n+------------\n+\n+The refs are updated atomically and no output is produced on success.\n+\n+To see what would be updated without actually updating:\n+\n+------------\n+$ git replay --ref-action=print --onto target origin/main..mybranch\n update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n ------------\n \n@@ -88,33 +113,29 @@ To cherry-pick the commits from mybranch onto target:\n \n ------------\n $ git replay --advance target origin/main..mybranch\n-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n ------------\n \n Note that the first two examples replay the exact same commits and on\n top of the exact same new base, they only differ in that the first\n-provides instructions to make mybranch point at the new commits and\n-the second provides instructions to make target point at them.\n+updates mybranch to point at the new commits and the second updates\n+target to point at them.\n \n What if you have a stack of branches, one depending upon another, and\n you'd really like to rebase the whole set?\n \n ------------\n $ git replay --contained --onto origin/main origin/main..tipbranch\n-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n ------------\n \n+All three branches (`branch1`, `branch2`, and `tipbranch`) are updated\n+atomically.\n+\n When calling `git replay`, one does not need to specify a range of\n commits to replay using the syntax `A..B`; any range expression will\n do:\n \n ------------\n $ git replay --onto origin/main ^base branch1 branch2 branch3\n-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n ------------\n \n This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex b64fc72063..1246add636 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -20,6 +20,11 @@\n #include <oidset.h>\n #include <tree.h>\n \n+enum ref_action_mode {\n+\tREF_ACTION_UPDATE,\n+\tREF_ACTION_PRINT\n+};\n+\n static const char *short_commit_name(struct repository *repo,\n \t\t\t\t     struct commit *commit)\n {\n@@ -284,6 +289,28 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \treturn create_commit(repo, result->tree, pickme, replayed_base);\n }\n \n+static int handle_ref_update(enum ref_action_mode mode,\n+\t\t\t     struct ref_transaction *transaction,\n+\t\t\t     const char *refname,\n+\t\t\t     const struct object_id *new_oid,\n+\t\t\t     const struct object_id *old_oid,\n+\t\t\t     struct strbuf *err)\n+{\n+\tswitch (mode) {\n+\tcase REF_ACTION_PRINT:\n+\t\tprintf(\"update %s %s %s\\n\",\n+\t\t       refname,\n+\t\t       oid_to_hex(new_oid),\n+\t\t       oid_to_hex(old_oid));\n+\t\treturn 0;\n+\tcase REF_ACTION_UPDATE:\n+\t\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n+\t\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n+\tdefault:\n+\t\tBUG(\"unknown ref_action_mode %d\", mode);\n+\t}\n+}\n+\n int cmd_replay(int argc,\n \t       const char **argv,\n \t       const char *prefix,\n@@ -294,6 +321,8 @@ int cmd_replay(int argc,\n \tstruct commit *onto = NULL;\n \tconst char *onto_name = NULL;\n \tint contained = 0;\n+\tconst char *ref_action_str = NULL;\n+\tenum ref_action_mode ref_action = REF_ACTION_UPDATE;\n \n \tstruct rev_info revs;\n \tstruct commit *last_commit = NULL;\n@@ -302,12 +331,14 @@ int cmd_replay(int argc,\n \tstruct merge_result result;\n \tstruct strset *update_refs = NULL;\n \tkh_oid_map_t *replayed_commits;\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strbuf transaction_err = STRBUF_INIT;\n \tint ret = 0;\n \n-\tconst char * const replay_usage[] = {\n+\tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n-\t\t   \"<revision-range>...\"),\n+\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -319,6 +350,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &contained,\n \t\t\t N_(\"advance all branches contained in revision-range\")),\n+\t\tOPT_STRING(0, \"ref-action\", &ref_action_str,\n+\t\t\t   N_(\"mode\"),\n+\t\t\t   N_(\"control ref update behavior (update|print)\")),\n \t\tOPT_END()\n \t};\n \n@@ -333,6 +367,18 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n+\t/* Default to update mode if not specified */\n+\tif (!ref_action_str)\n+\t\tref_action_str = \"update\";\n+\n+\t/* Parse ref action mode */\n+\tif (!strcmp(ref_action_str, \"update\"))\n+\t\tref_action = REF_ACTION_UPDATE;\n+\telse if (!strcmp(ref_action_str, \"print\"))\n+\t\tref_action = REF_ACTION_PRINT;\n+\telse\n+\t\tdie(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n@@ -389,6 +435,17 @@ int cmd_replay(int argc,\n \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n \t\t\t      &onto, &update_refs);\n \n+\t/* Initialize ref transaction if using update mode */\n+\tif (ref_action == REF_ACTION_UPDATE) {\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n+\t\t\t\t\t\t\t  0, &transaction_err);\n+\t\tif (!transaction) {\n+\t\t\tret = error(_(\"failed to begin ref transaction: %s\"),\n+\t\t\t\t    transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n \tif (!onto) /* FIXME: Should handle replaying down to root commit */\n \t\tdie(\"Replaying down to root commit is not supported yet!\");\n \n@@ -434,10 +491,15 @@ int cmd_replay(int argc,\n \t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n \t\t\t    (contained || strset_contains(update_refs,\n \t\t\t\t\t\t\t  decoration->name))) {\n-\t\t\t\tprintf(\"update %s %s %s\\n\",\n-\t\t\t\t       decoration->name,\n-\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t\t\t       oid_to_hex(&commit->object.oid));\n+\t\t\t\tif (handle_ref_update(ref_action, transaction,\n+\t\t\t\t\t\t      decoration->name,\n+\t\t\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t\t\t      &commit->object.oid,\n+\t\t\t\t\t\t      &transaction_err) < 0) {\n+\t\t\t\t\tret = error(_(\"failed to update ref %s: %s\"),\n+\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n+\t\t\t\t\tgoto cleanup;\n+\t\t\t\t}\n \t\t\t}\n \t\t\tdecoration = decoration->next;\n \t\t}\n@@ -445,10 +507,23 @@ int cmd_replay(int argc,\n \n \t/* In --advance mode, advance the target ref */\n \tif (result.clean == 1 && advance_name) {\n-\t\tprintf(\"update %s %s %s\\n\",\n-\t\t       advance_name,\n-\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t       oid_to_hex(&onto->object.oid));\n+\t\tif (handle_ref_update(ref_action, transaction, advance_name,\n+\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t      &onto->object.oid,\n+\t\t\t\t      &transaction_err) < 0) {\n+\t\t\tret = error(_(\"failed to update ref %s: %s\"),\n+\t\t\t\t    advance_name, transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n+\t/* Commit the ref transaction if we have one */\n+\tif (transaction && result.clean == 1) {\n+\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n+\t\t\tret = error(_(\"failed to commit ref transaction: %s\"),\n+\t\t\t\t    transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n \t}\n \n \tmerge_finalize(&merge_opt, &result);\n@@ -460,6 +535,9 @@ int cmd_replay(int argc,\n \tret = result.clean;\n \n cleanup:\n+\tif (transaction)\n+\t\tref_transaction_free(transaction);\n+\tstrbuf_release(&transaction_err);\n \trelease_revisions(&revs);\n \tfree(advance_name);\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 58b3759935..54c86b87d8 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n '\n \n test_expect_success 'using replay to rebase two branches, one on top of other' '\n-\tgit replay --onto main topic1..topic2 >result &&\n+\tgit replay --ref-action=print --onto main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n '\n \n test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n-\tgit -C bare replay --onto main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n \t# 4th field of result is hash for main instead of hash for topic2\n \n-\tgit replay --advance main topic1..topic2 >result &&\n+\tgit replay --ref-action=print --advance main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n '\n \n test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n-\tgit -C bare replay --advance main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n '\n \n test_expect_success 'using replay to also rebase a contained branch' '\n-\tgit replay --contained --onto main main..topic3 >result &&\n+\tgit replay --ref-action=print --contained --onto main main..topic3 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '\n '\n \n test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n-\tgit -C bare replay --contained --onto main main..topic3 >result-bare &&\n+\tgit -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n test_expect_success 'using replay to rebase multiple divergent branches' '\n-\tgit replay --onto main ^topic1 topic2 topic4 >result &&\n+\tgit replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n '\n \n test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n-\tgit -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n+\tgit -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n \n \ttest_line_count = 4 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n-- \n2.51.0\n\n"},{"id":"529439","messageId":"20251022185045.29256-4-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251022185045.29256-1-siddharthasthana31@gmail.com","subject":"[PATCH v4 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-22T18:50:45Z","receivedAt":"2025-10-22T18:51:27Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"Add a configuration option to control the default behavior of git replay\nfor updating references. This allows users who prefer the traditional\npipeline output to set it once in their config instead of passing\n--ref-action=print with every command.\n\nThe config option uses string values that mirror the behavior modes:\n  * replay.refAction = update (default): atomic ref updates\n  * replay.refAction = print: output commands for pipeline\n\nThe command-line --ref-action option always overrides the config setting,\nallowing users to temporarily change behavior for a single invocation.\n\nImplementation details:\n\nIn cmd_replay(), after parsing command-line options, we check if\n--ref-action was provided. If not, we read the configuration using\nrepo_config_get_string_tmp(). If the config variable is set, we validate\nthe value and use it to set the ref_action_str:\n\n  Config value      Internal mode    Behavior\n  ──────────────────────────────────────────────────────────────\n  \"update\"          \"update\"         Atomic ref updates (default)\n  \"print\"           \"print\"          Pipeline output\n  (not set)         \"update\"         Atomic ref updates (default)\n  (invalid)         error            Die with helpful message\n\nIf an invalid value is provided, we die() immediately with an error\nmessage explaining the valid options. This catches configuration errors\nearly and provides clear guidance to users.\n\nThe command-line --ref-action option, when provided, overrides the\nconfig value. This precedence allows users to set their preferred default\nwhile still having per-invocation control:\n\n  git config replay.refAction print         # Set default\n  git replay --ref-action=update --onto main topic  # Override once\n\nThe config and command-line option use the same value names ('update'\nand 'print') for consistency and clarity. This makes it immediately\nobvious how the config maps to the command-line option, addressing\nfeedback about the relationship between configuration and command-line\noptions being clear to users.\n\nExamples:\n\n$ git config --global replay.refAction print\n$ git replay --onto main topic1..topic2 | git update-ref --stdin\n\n$ git replay --ref-action=update --onto main topic1..topic2\n\n$ git config replay.refAction update\n$ git replay --onto main topic1..topic2  # Updates refs directly\n\nThe implementation follows Git's standard configuration precedence:\ncommand-line options override config values, which matches user\nexpectations across all Git commands.\n\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/config/replay.adoc | 11 +++++++++\n builtin/replay.c                 | 16 +++++++++++-\n t/t3650-replay-basics.sh         | 42 ++++++++++++++++++++++++++++++++\n 3 files changed, 68 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/config/replay.adoc\n\ndiff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc\nnew file mode 100644\nindex 0000000000..7d549d2f0e\n--- /dev/null\n+++ b/Documentation/config/replay.adoc\n@@ -0,0 +1,11 @@\n+replay.refAction::\n+\tSpecifies the default mode for handling reference updates in\n+\t`git replay`. The value can be:\n++\n+--\n+\t* `update`: Update refs directly using an atomic transaction (default behavior).\n+\t* `print`: Output update-ref commands for pipeline use.\n+--\n++\n+This setting can be overridden with the `--ref-action` command-line option.\n+When not configured, `git replay` defaults to `update` mode.\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 1246add636..bb0420dc99 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -8,6 +8,7 @@\n #include \"git-compat-util.h\"\n \n #include \"builtin.h\"\n+#include \"config.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"lockfile.h\"\n@@ -367,7 +368,20 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n-\t/* Default to update mode if not specified */\n+\t/* Set default mode from config if not specified on command line */\n+\tif (!ref_action_str) {\n+\t\tconst char *config_value = NULL;\n+\t\tif (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value)) {\n+\t\t\tif (!strcmp(config_value, \"update\"))\n+\t\t\t\tref_action_str = \"update\";\n+\t\t\telse if (!strcmp(config_value, \"print\"))\n+\t\t\t\tref_action_str = \"print\";\n+\t\t\telse\n+\t\t\t\tdie(_(\"invalid value for replay.refAction: '%s'\"), config_value);\n+\t\t}\n+\t}\n+\n+\t/* Default to update mode if still not set */\n \tif (!ref_action_str)\n \t\tref_action_str = \"update\";\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 54c86b87d8..307beb667e 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -217,4 +217,46 @@ test_expect_success 'merge.directoryRenames=false' '\n \t\t--onto rename-onto rename-onto..rename-from\n '\n \n+test_expect_success 'replay.refAction config option' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n+\n+\t# Set config to print\n+\tgit config replay.refAction print &&\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_line_count = 1 output &&\n+\tgrep \"^update refs/heads/topic2 \" output &&\n+\n+\t# Reset and test update mode\n+\tgit branch -f topic2 $START &&\n+\tgit config replay.refAction update &&\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'command-line --ref-action overrides config' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n+\n+\t# Set config to update but use --ref-action=print\n+\tgit config replay.refAction update &&\n+\tgit replay --ref-action=print --onto main topic1..topic2 >output &&\n+\ttest_line_count = 1 output &&\n+\tgrep \"^update refs/heads/topic2 \" output\n+'\n+\n+test_expect_success 'invalid replay.refAction value' '\n+\ttest_when_finished \"git config --unset replay.refAction\" &&\n+\tgit config replay.refAction invalid &&\n+\ttest_must_fail git replay --onto main topic1..topic2 2>error &&\n+\tgrep \"invalid value for replay.refAction\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"529451","messageId":"xmqq7bwmy6r6.fsf@gitster.g","threadId":"64109","inReplyTo":"20251022185045.29256-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-22T21:19:09Z","receivedAt":"2025-10-22T21:19:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index b64fc72063..1246add636 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -20,6 +20,11 @@\n>  #include <oidset.h>\n>  #include <tree.h>\n>  \n> +enum ref_action_mode {\n> +\tREF_ACTION_UPDATE,\n> +\tREF_ACTION_PRINT\n> +};\n> +\n\nWe allow and encourage the last item in enum definition to have\ntrailing comma, i.e.\n\n        enum ref_action_mode {\n                REF_ACTION_UPDATE,\n                REF_ACTION_PRINT,\n        };\n\nunless the last one is somehow special and we are not supposed to\nadd any new item after that (e.g., a sentinel REF_ACTION_MAX that is\nsupposed to give the upper limit of the values).  That way, future\ndevelopers can add new items with minimum patch noise.\n\n> @@ -434,10 +491,15 @@ int cmd_replay(int argc,\n> ...\n> +\t\t\t\t\tret = error(_(\"failed to update ref %s: %s\"),\n> +\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n\nHmph, don't we want to use '%s' when reporting the ->name thing?\nDocumentation/CodingGuidelines has this:\n\n   Error Messages\n\n    - Do not end a single-sentence error message with a full stop.\n\n    - Do not capitalize the first word, only because it is the first word\n      in the message (\"unable to open '%s'\", not \"Unable to open '%s'\").  But\n      \"SHA-3 not supported\" is fine, because the reason the first word is\n      capitalized is not because it is at the beginning of the sentence,\n      but because the word would be spelled in capital letters even when\n      it appeared in the middle of the sentence.\n\n    - Say what the error is first (\"cannot open '%s'\", not \"%s: cannot open\").\n\n    - Enclose the subject of an error inside a pair of single quotes,\n      e.g. `die(_(\"unable to open '%s'\"), path)`.\n\n    - Unless there is a compelling reason not to, error messages from\n      porcelain commands should be marked for translation, e.g.\n      `die(_(\"bad revision %s\"), revision)`.\n\n    - Error messages from the plumbing commands are sometimes meant for\n      machine consumption and should not be marked for translation,\n      e.g., `die(\"bad revision %s\", revision)`.\n\n    - BUG(\"message\") are for communicating the specific error to developers,\n      thus should not be translated.\n\n"},{"id":"529516","messageId":"xmqq7bwlv4jh.fsf@gitster.g","threadId":"64109","inReplyTo":"20251022185045.29256-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 0/3] replay: make atomic ref updates the default","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-23T18:47:30Z","receivedAt":"2025-10-23T18:47:34Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n\n> This is v4 of the git-replay atomic updates series.\n>\n> Based on feedback from v3, this version improves the naming and\n> implementation for clarity and type safety. Thanks to Junio, Christian,\n> Elijah, Phillip, Patrick, and Karthik for the detailed reviews.\n>\n> ## Changes in v4\n>\n> **Renamed --update-refs to --ref-action**\n>\n> Junio pointed out that \"--update-refs=print\" is semantically awkward.\n> Answering \"print\" to the question \"update refs?\" doesn't make sense.\n> The actual question is \"what action should we take on the refs?\"\n>\n> Changed to --ref-action=(update|print) where both values are verbs that\n> answer \"what action?\". This makes the interface clearer.\n>\n> **Aligned config name with command-line option**\n>\n> Changed replay.defaultAction to replay.refAction. The config variable\n> now mirrors the option name, making the relationship obvious.\n>\n> **Unified config and command-line values**\n\nI didn't see anything glaringly wrong in this round, even though I\npicked a couple of small nits in one patch, so we might want a\nhopefully small and final reroll before marking the topic for\n'next'.\n\nIs everybody else happy with this iteration otherwise?\n\nThanks.\n"},{"id":"529576","messageId":"CAP8UFD0f030KHOJeM44k4wcxrEhFdzycvoaMtFZ-mTZ28LfJMw@mail.gmail.com","threadId":"64109","inReplyTo":"20251022185045.29256-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 0/3] replay: make atomic ref updates the default","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-10-24T09:39:05Z","receivedAt":"2025-10-24T09:39:20Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, Oct 22, 2025 at 8:50 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n\n> ## Testing\n>\n> All tests pass:\n>   - t0450-txt-doc-vs-help.sh (now passing, was failing in v3)\n>   - t3650-replay-basics.sh (all 18 tests pass)\n\nNit: a link to CI results on GitLab or GitHub might be better to show\nthat all tests pass. Just saying \"All tests pass\" makes reviewers\nwonder if comprehensive CI tests on different OS/compilers/build\nsystems were used or if you just tested on your machine.\n\nThanks.\n"},{"id":"529598","messageId":"CAP8UFD00rE7gF+baidmoi7nYwVKa3UDQgj+TB4wJLtjJF7u9gA@mail.gmail.com","threadId":"64109","inReplyTo":"20251022185045.29256-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-10-24T10:37:02Z","receivedAt":"2025-10-24T10:37:16Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n\n[...]\n\n> However, it should be noted that all three of these are somewhat\n> special cases; users, whether on the client or server side, would\n> almost certainly find it more ergonomical to simply have the updating\n\nNit: maybe: s/ergonomical/ergonomic/\n\n> of refs be the default.\n\n[...]\n\n> Change the default behavior to update refs directly, and atomically (at\n> least to the extent supported by the refs backend in use). This\n> eliminates the process coordination overhead for the common case.\n>\n> For users needing the traditional pipeline workflow, add a new\n> --ref-action=<mode> option that preserves the original behavior:\n>\n>   git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin\n>\n> The mode can be:\n>   * update (default): Update refs directly using an atomic transaction\n>   * print: Output update-ref commands for pipeline use\n\nNit: maybe it should be mentioned that the command is still\nexperimental, so it's OK to change the default like this.\n\n> +--ref-action[=<mode>]::\n> +       Control how references are updated. The mode can be:\n> ++\n> +--\n> +       * `update` (default): Update refs directly using an atomic transaction.\n> +         All refs are updated or none are (all-or-nothing behavior).\n> +       * `print`: Output update-ref commands for pipeline use. This is the\n> +         traditional behavior where output can be piped to `git update-ref --stdin`.\n> +--\n> ++\n> +The default mode can be configured via `replay.refAction` configuration option.\n\nNit: s/via `replay.refAction` configuration option/via the\n`replay.refAction` configuration variable/\n\n(It seems that \"configuration variable\" is used around 6 times more\nthan \"configuration option\", so we may want to standardize this\nwording.)\n\n> @@ -54,8 +68,11 @@ include::rev-list-options.adoc[]\n>  OUTPUT\n>  ------\n>\n> -When there are no conflicts, the output of this command is usable as\n> -input to `git update-ref --stdin`.  It is of the form:\n> +By default (with `--ref-action=update`), this command produces no output on\n\nNit: s/By default (with `--ref-action=update`)/By default, or with\n`--ref-action=update`,/\n\nI think it's better to be very explicit here, especially as we mention\n`--ref-action=print` below.\n\n[...]\n\n> -       const char * const replay_usage[] = {\n> +       const char *const replay_usage[] = {\n\nNit: Not sure this change is worth it, but I understand that it might\nhelp pass some automated/CI tests, so not a big issue.\n\n[...]\n\n> +       /* Default to update mode if not specified */\n> +       if (!ref_action_str)\n> +               ref_action_str = \"update\";\n> +\n> +       /* Parse ref action mode */\n> +       if (!strcmp(ref_action_str, \"update\"))\n> +               ref_action = REF_ACTION_UPDATE;\n\nNit: maybe:\n\n       if (!ref_action_str || !strcmp(ref_action_str, \"update\"))\n               ref_action = REF_ACTION_UPDATE;\n\n> +       else if (!strcmp(ref_action_str, \"print\"))\n> +               ref_action = REF_ACTION_PRINT;\n> +       else\n> +               die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n> +\n\n[...]\n\n>  test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n> -       git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n> +       git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n>\n>         test_line_count = 4 result &&\n>         cut -f 3 -d \" \" result >new-branch-tips &&\n\nAre there tests with the new default behavior added? It looks like all\nthe changes in the test script are about adding \"--ref-action=print\"\nto an existing test.\n"},{"id":"529599","messageId":"CAP8UFD3Bz+Yn4qtCrFoKcE=u-dAtK0cXON1nFMRL8n9wBSS8pg@mail.gmail.com","threadId":"64109","inReplyTo":"20251022185045.29256-4-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 3/3] replay: add replay.refAction config option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-10-24T11:01:32Z","receivedAt":"2025-10-24T11:01:47Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n\n> @@ -367,7 +368,20 @@ int cmd_replay(int argc,\n>         die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>                                   contained, \"--contained\");\n>\n> -       /* Default to update mode if not specified */\n> +       /* Set default mode from config if not specified on command line */\n> +       if (!ref_action_str) {\n> +               const char *config_value = NULL;\n> +               if (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value)) {\n> +                       if (!strcmp(config_value, \"update\"))\n> +                               ref_action_str = \"update\";\n> +                       else if (!strcmp(config_value, \"print\"))\n> +                               ref_action_str = \"print\";\n> +                       else\n> +                               die(_(\"invalid value for replay.refAction: '%s'\"), config_value);\n> +               }\n> +       }\n> +\n> +       /* Default to update mode if still not set */\n>         if (!ref_action_str)\n>                 ref_action_str = \"update\";\n\nIt seems to me that a dedicated function could handle this a bit\nbetter. Maybe something like:\n\nstatic enum ref_action_mode get_ref_action_mode(const char *ref_action_str)\n{\n     const char *config_value = NULL;\n\n     if (!strcmp(ref_action_str, \"update\"))\n             return REF_ACTION_UPDATE;\n      if (!strcmp(ref_action_str, \"print\"))\n            return REF_ACTION_PRINT;\n      if (ref_action_str)\n            die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n\n      if (repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n             return REF_ACTION_UPDATE; /* default */\n\n      if (!strcmp(config_value, \"update\"))\n             return REF_ACTION_UPDATE;\n      if (!strcmp(config_value, \"print\"))\n            return REF_ACTION_PRINT;\n      die(_(\"invalid value for replay.refAction: '%s'\"), config_value);\n}\n\n[...]\n\n> +test_expect_success 'replay.refAction config option' '\n> +       # Store original state\n> +       START=$(git rev-parse topic2) &&\n> +       test_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n> +\n> +       # Set config to print\n> +       git config replay.refAction print &&\n> +       git replay --onto main topic1..topic2 >output &&\n> +       test_line_count = 1 output &&\n> +       grep \"^update refs/heads/topic2 \" output &&\n\nNit: here and below, it's a bit better to use test_grep instead of\ngrep for better error reporting.\n\nThanks.\n"},{"id":"529601","messageId":"a4cd31ad-7086-4d05-ba00-db65ec24b45a@gmail.com","threadId":"64109","inReplyTo":"20251022185045.29256-4-siddharthasthana31@gmail.com","subject":"Re: [PATCH v4 3/3] replay: add replay.refAction config option","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-10-24T13:28:21Z","receivedAt":"2025-10-24T13:28:42Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 22/10/2025 19:50, Siddharth Asthana wrote:\n\nThis is looking pretty nice now, I've left some on he tests comments below\n\n> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n> index 54c86b87d8..307beb667e 100755\n> --- a/t/t3650-replay-basics.sh\n> +++ b/t/t3650-replay-basics.sh\n> @@ -217,4 +217,46 @@ test_expect_success 'merge.directoryRenames=false' '\n>   \t\t--onto rename-onto rename-onto..rename-from\n>   '\n>   \n> +test_expect_success 'replay.refAction config option' '\n> +\t# Store original state\n> +\tSTART=$(git rev-parse topic2) &&\n\nIsn't there a tag we can use here from the initial setup?\n\n> +\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n> +\n> +\t# Set config to print\n> +\tgit config replay.refAction print &&\nI think it would be better to use test_config here rather than having to \nclear the config manually with test_when_finished() above.\n\n> +\tgit replay --onto main topic1..topic2 >output &&\n> +\ttest_line_count = 1 output &&\n> +\tgrep \"^update refs/heads/topic2 \" output &&\n\nRather than test_line_count and grep it would be better to use test_cmp \nhere.\n\nThe same comments apply to the rest of the tests\n\nThanks\n\nPhillip\n\n> +\n> +\t# Reset and test update mode\n> +\tgit branch -f topic2 $START &&\n> +\tgit config replay.refAction update &&\n> +\tgit replay --onto main topic1..topic2 >output &&\n> +\ttest_must_be_empty output &&\n> +\n> +\t# Verify ref was updated\n> +\tgit log --format=%s topic2 >actual &&\n> +\ttest_write_lines E D M L B A >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'command-line --ref-action overrides config' '\n> +\t# Store original state\n> +\tSTART=$(git rev-parse topic2) &&\n> +\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n> +\n> +\t# Set config to update but use --ref-action=print\n> +\tgit config replay.refAction update &&\n> +\tgit replay --ref-action=print --onto main topic1..topic2 >output &&\n> +\ttest_line_count = 1 output &&\n> +\tgrep \"^update refs/heads/topic2 \" output\n> +'\n> +\n> +test_expect_success 'invalid replay.refAction value' '\n> +\ttest_when_finished \"git config --unset replay.refAction\" &&\n> +\tgit config replay.refAction invalid &&\n> +\ttest_must_fail git replay --onto main topic1..topic2 2>error &&\n> +\tgrep \"invalid value for replay.refAction\" error\n> +'\n> +\n>   test_done\n\n\n"},{"id":"529602","messageId":"7a3161d1-4e30-4156-876d-7eede4b06705@gmail.com","threadId":"64109","inReplyTo":"a4cd31ad-7086-4d05-ba00-db65ec24b45a@gmail.com","subject":"Re: [PATCH v4 3/3] replay: add replay.refAction config option","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-10-24T13:36:28Z","receivedAt":"2025-10-24T13:36:33Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 24/10/2025 14:28, Phillip Wood wrote:\n> On 22/10/2025 19:50, Siddharth Asthana wrote:\n> \n>> +    git replay --onto main topic1..topic2 >output &&\n>> +    test_line_count = 1 output &&\n>> +    grep \"^update refs/heads/topic2 \" output &&\n> \n> Rather than test_line_count and grep it would be better to use test_cmp \n> here.\n\nOh, I've just realized we don't know the value of the ref so \ntest_line_count() plus test_grep() (not grep) makes sense.\n\nThanks\n\nPhillip\n\n"},{"id":"529609","messageId":"xmqqbjlwqq6d.fsf@gitster.g","threadId":"64109","inReplyTo":"CAP8UFD00rE7gF+baidmoi7nYwVKa3UDQgj+TB4wJLtjJF7u9gA@mail.gmail.com","subject":"Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-24T15:23:38Z","receivedAt":"2025-10-24T15:23:42Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>\n>> -       const char * const replay_usage[] = {\n>> +       const char *const replay_usage[] = {\n>\n> Nit: Not sure this change is worth it, but I understand that it might\n> help pass some automated/CI tests, so not a big issue.\n\nI think this formatting issue came up recently on another discussion\nthread.  We found that the prevalent style in the codebase is that\nan asterisk in between tokens neither of which is variable has space\non both sides (i.e. the preimage of the above change), so unless\nthere is a specific reason to make the above change, I'd rather not\nto see such \"reformatting\" thrown into a patch that implements a\nfeature or fixes a bug (iow, not a \"clean-up styles\" patch).\n\nBy the way, I would be suprised if that the reason were a CI test.\nHow would the preimage have been passing the same test if that is\nthe case?\n"},{"id":"529610","messageId":"xmqq7bwkqpua.fsf@gitster.g","threadId":"64109","inReplyTo":"CAP8UFD3Bz+Yn4qtCrFoKcE=u-dAtK0cXON1nFMRL8n9wBSS8pg@mail.gmail.com","subject":"Re: [PATCH v4 3/3] replay: add replay.refAction config option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-24T15:30:53Z","receivedAt":"2025-10-24T15:30:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> It seems to me that a dedicated function could handle this a bit\n> better. Maybe something like:\n>\n> static enum ref_action_mode get_ref_action_mode(const char *ref_action_str)\n> {\n>      const char *config_value = NULL;\n>\n>      if (!strcmp(ref_action_str, \"update\"))\n>              return REF_ACTION_UPDATE;\n>       if (!strcmp(ref_action_str, \"print\"))\n>             return REF_ACTION_PRINT;\n>       if (ref_action_str)\n>             die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n>\n>       if (repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n>              return REF_ACTION_UPDATE; /* default */\n>\n>       if (!strcmp(config_value, \"update\"))\n>              return REF_ACTION_UPDATE;\n>       if (!strcmp(config_value, \"print\"))\n>             return REF_ACTION_PRINT;\n>       die(_(\"invalid value for replay.refAction: '%s'\"), config_value);\n> }\n\nYou'd want to do \"string to enum\" helper function just once and call\nthat helper from the above function, once for the command line option\nand again for the configuration variable.\n\nOr do so where you would add a call to the above function directly\nwithout your helper.  I am not convinced that \"here is the command\nline option (or perhaps we got nothing); what is the desired\nsetting, taking configuration also into consideration?\" is\nparticularly a good abstraction.  It is more common to have\ngit_config() to grab replay.refAction string, and if there is a\nstring value, pass the last one to \"string to enum\" helper and\nremember the result, then call parse_options() to further overwrite\nthe result from the command line option string (which again will use\nthe \"string to enum\" helper).  The structure that requires your helper\nfunction smells rather unusual.\n\n> [...]\n>\n>> +test_expect_success 'replay.refAction config option' '\n>> +       # Store original state\n>> +       START=$(git rev-parse topic2) &&\n>> +       test_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n>> +\n>> +       # Set config to print\n>> +       git config replay.refAction print &&\n>> +       git replay --onto main topic1..topic2 >output &&\n>> +       test_line_count = 1 output &&\n>> +       grep \"^update refs/heads/topic2 \" output &&\n>\n> Nit: here and below, it's a bit better to use test_grep instead of\n> grep for better error reporting.\n\nYes, \"a bit\" -> \"much\".\n\nThanks.\n"},{"id":"529646","messageId":"xmqqzf9encl1.fsf@gitster.g","threadId":"64109","inReplyTo":"xmqq7bwlv4jh.fsf@gitster.g","subject":"Re: [PATCH v4 0/3] replay: make atomic ref updates the default","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-25T16:57:46Z","receivedAt":"2025-10-25T16:57:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>> This is v4 of the git-replay atomic updates series.\n>> ...\n> I didn't see anything glaringly wrong in this round, even though I\n> picked a couple of small nits in one patch, so we might want a\n> hopefully small and final reroll before marking the topic for\n> 'next'.\n>\n> Is everybody else happy with this iteration otherwise?\n\nThere are a few actionable comments pointing out typos and style\nglitches for this iteration.  I'll mark the topic as expecting a\nhopefully small and final reroll in the next issue of \"What's\ncooking\" report.\n\nThanks.\n"},{"id":"529812","messageId":"6a41eae1-8d44-401b-85e2-4e52187da525@gmail.com","threadId":"64109","inReplyTo":"xmqq7bwmy6r6.fsf@gitster.g","subject":"Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T19:03:01Z","receivedAt":"2025-10-28T19:03:10Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 23/10/25 02:49, Junio C Hamano wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>> diff --git a/builtin/replay.c b/builtin/replay.c\n>> index b64fc72063..1246add636 100644\n>> --- a/builtin/replay.c\n>> +++ b/builtin/replay.c\n>> @@ -20,6 +20,11 @@\n>>   #include <oidset.h>\n>>   #include <tree.h>\n>>   \n>> +enum ref_action_mode {\n>> +\tREF_ACTION_UPDATE,\n>> +\tREF_ACTION_PRINT\n>> +};\n>> +\n\n\nHi Junio,\nThank you for the detailed review! Both are straightforward fixes:\n\n\n> We allow and encourage the last item in enum definition to have\n> trailing comma, i.e.\n>\n>          enum ref_action_mode {\n>                  REF_ACTION_UPDATE,\n>                  REF_ACTION_PRINT,\n>          };\n\n\nWill add the trailing comma in v5 - makes future additions much cleaner.\n\n\n>\n> unless the last one is somehow special and we are not supposed to\n> add any new item after that (e.g., a sentinel REF_ACTION_MAX that is\n> supposed to give the upper limit of the values).  That way, future\n> developers can add new items with minimum patch noise.\n>\n>> @@ -434,10 +491,15 @@ int cmd_replay(int argc,\n>> ...\n>> +\t\t\t\t\tret = error(_(\"failed to update ref %s: %s\"),\n>> +\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n> Hmph, don't we want to use '%s' when reporting the ->name thing?\n\n\n\nYou are absolutely right about the codingGuidelines. I will fix both error\nmessages to properly quote the ref names:\n\n\n         error(_(\"failed to update ref '%s': %s\"), decoration->name, \ntransaction_err.buf);\n\n\nThese will be in v5 along with Christian and Philip's feedback.\n\nThanks,\nSiddharth\n\n\n> Documentation/CodingGuidelines has this:\n>\n>     Error Messages\n>\n>      - Do not end a single-sentence error message with a full stop.\n>\n>      - Do not capitalize the first word, only because it is the first word\n>        in the message (\"unable to open '%s'\", not \"Unable to open '%s'\").  But\n>        \"SHA-3 not supported\" is fine, because the reason the first word is\n>        capitalized is not because it is at the beginning of the sentence,\n>        but because the word would be spelled in capital letters even when\n>        it appeared in the middle of the sentence.\n>\n>      - Say what the error is first (\"cannot open '%s'\", not \"%s: cannot open\").\n>\n>      - Enclose the subject of an error inside a pair of single quotes,\n>        e.g. `die(_(\"unable to open '%s'\"), path)`.\n>\n>      - Unless there is a compelling reason not to, error messages from\n>        porcelain commands should be marked for translation, e.g.\n>        `die(_(\"bad revision %s\"), revision)`.\n>\n>      - Error messages from the plumbing commands are sometimes meant for\n>        machine consumption and should not be marked for translation,\n>        e.g., `die(\"bad revision %s\", revision)`.\n>\n>      - BUG(\"message\") are for communicating the specific error to developers,\n>        thus should not be translated.\n>\n"},{"id":"529818","messageId":"3dc2325a-9551-41a4-a747-c9c2c4aeca94@gmail.com","threadId":"64109","inReplyTo":"CAP8UFD3Bz+Yn4qtCrFoKcE=u-dAtK0cXON1nFMRL8n9wBSS8pg@mail.gmail.com","subject":"Re: [PATCH v4 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T19:26:16Z","receivedAt":"2025-10-28T19:26:23Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 24/10/25 16:31, Christian Couder wrote:\n> On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>\n>> @@ -367,7 +368,20 @@ int cmd_replay(int argc,\n>>          die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>>                                    contained, \"--contained\");\n>>\n>> -       /* Default to update mode if not specified */\n>> +       /* Set default mode from config if not specified on command line */\n>> +       if (!ref_action_str) {\n>> +               const char *config_value = NULL;\n>> +               if (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value)) {\n>> +                       if (!strcmp(config_value, \"update\"))\n>> +                               ref_action_str = \"update\";\n>> +                       else if (!strcmp(config_value, \"print\"))\n>> +                               ref_action_str = \"print\";\n>> +                       else\n>> +                               die(_(\"invalid value for replay.refAction: '%s'\"), config_value);\n>> +               }\n>> +       }\n>> +\n>> +       /* Default to update mode if still not set */\n>>          if (!ref_action_str)\n>>                  ref_action_str = \"update\";\n\n\nHi Christian,\nThanks for the config parsing improvements!\n\n\n> It seems to me that a dedicated function could handle this a bit\n> better. Maybe something like:\n\n\nExcellent suggestion! I will extract `parse_ref_action_mode()` and \n`get_ref_action_mode()`\nhelpers to centralize the string-to-enum conversion and config \nprecedence logic, Much\ncleaner than the current inline approach.\n\n\n>\n> static enum ref_action_mode get_ref_action_mode(const char *ref_action_str)\n> {\n>       const char *config_value = NULL;\n>\n>       if (!strcmp(ref_action_str, \"update\"))\n>               return REF_ACTION_UPDATE;\n>        if (!strcmp(ref_action_str, \"print\"))\n>              return REF_ACTION_PRINT;\n>        if (ref_action_str)\n>              die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n>\n>        if (repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n>               return REF_ACTION_UPDATE; /* default */\n>\n>        if (!strcmp(config_value, \"update\"))\n>               return REF_ACTION_UPDATE;\n>        if (!strcmp(config_value, \"print\"))\n>              return REF_ACTION_PRINT;\n>        die(_(\"invalid value for replay.refAction: '%s'\"), config_value);\n> }\n>\n> [...]\n>\n>> +test_expect_success 'replay.refAction config option' '\n>> +       # Store original state\n>> +       START=$(git rev-parse topic2) &&\n>> +       test_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n>> +\n>> +       # Set config to print\n>> +       git config replay.refAction print &&\n>> +       git replay --onto main topic1..topic2 >output &&\n>> +       test_line_count = 1 output &&\n>> +       grep \"^update refs/heads/topic2 \" output &&\n> Nit: here and below, it's a bit better to use test_grep instead of\n> grep for better error reporting.\n\n\nWill switch to `test_grep` throughout for better error reporting.\n\nThanks,\nSiddharth\n\n\n>\n> Thanks.\n"},{"id":"529821","messageId":"0c58d734-d7fe-4b2a-8231-c123b74601d6@gmail.com","threadId":"64109","inReplyTo":"CAP8UFD00rE7gF+baidmoi7nYwVKa3UDQgj+TB4wJLtjJF7u9gA@mail.gmail.com","subject":"Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T19:39:11Z","receivedAt":"2025-10-28T19:39:19Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 24/10/25 16:07, Christian Couder wrote:\n> On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>\n> [...]\n>\n>> However, it should be noted that all three of these are somewhat\n>> special cases; users, whether on the client or server side, would\n>> almost certainly find it more ergonomical to simply have the updating\n\n\nHi Christian,\nThanks for the detailed review! All good points:\n\n\n> Nit: maybe: s/ergonomical/ergonomic/\n\n\nWill be fixed in v5!\n\n\n>> of refs be the default.\n> [...]\n>\n>> Change the default behavior to update refs directly, and atomically (at\n>> least to the extent supported by the refs backend in use). This\n>> eliminates the process coordination overhead for the common case.\n>>\n>> For users needing the traditional pipeline workflow, add a new\n>> --ref-action=<mode> option that preserves the original behavior:\n>>\n>>    git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> The mode can be:\n>>    * update (default): Update refs directly using an atomic transaction\n>>    * print: Output update-ref commands for pipeline use\n> Nit: maybe it should be mentioned that the command is still\n> experimental, so it's OK to change the default like this.\n\n\nGood point, I will add a note in the commit message that since git-replay is\nstill experimental, changing the default behavior is acceptable\n\n\n>\n>> +--ref-action[=<mode>]::\n>> +       Control how references are updated. The mode can be:\n>> ++\n>> +--\n>> +       * `update` (default): Update refs directly using an atomic transaction.\n>> +         All refs are updated or none are (all-or-nothing behavior).\n>> +       * `print`: Output update-ref commands for pipeline use. This is the\n>> +         traditional behavior where output can be piped to `git update-ref --stdin`.\n>> +--\n>> ++\n>> +The default mode can be configured via `replay.refAction` configuration option.\n> Nit: s/via `replay.refAction` configuration option/via the\n> `replay.refAction` configuration variable/\n\n\nGood catch, I will standardize on \"configuration variable\" throughout.\n\n\n>\n> (It seems that \"configuration variable\" is used around 6 times more\n> than \"configuration option\", so we may want to standardize this\n> wording.)\n>\n>> @@ -54,8 +68,11 @@ include::rev-list-options.adoc[]\n>>   OUTPUT\n>>   ------\n>>\n>> -When there are no conflicts, the output of this command is usable as\n>> -input to `git update-ref --stdin`.  It is of the form:\n>> +By default (with `--ref-action=update`), this command produces no output on\n> Nit: s/By default (with `--ref-action=update`)/By default, or with\n> `--ref-action=update`,/\n\n\nMuch clearer wording\n\n\n>\n> I think it's better to be very explicit here, especially as we mention\n> `--ref-action=print` below.\n>\n> [...]\n>\n>> -       const char * const replay_usage[] = {\n>> +       const char *const replay_usage[] = {\n> Nit: Not sure this change is worth it, but I understand that it might\n> help pass some automated/CI tests, so not a big issue.\n\n\nActually, Junio mentioned in another thread that the prevalent style in the\ncodebase is `const char * const` (space on both sides), so I'll revert this\nchange in v5.\n\n\n>\n> [...]\n>\n>> +       /* Default to update mode if not specified */\n>> +       if (!ref_action_str)\n>> +               ref_action_str = \"update\";\n>> +\n>> +       /* Parse ref action mode */\n>> +       if (!strcmp(ref_action_str, \"update\"))\n>> +               ref_action = REF_ACTION_UPDATE;\n> Nit: maybe:\n>\n>         if (!ref_action_str || !strcmp(ref_action_str, \"update\"))\n>                 ref_action = REF_ACTION_UPDATE;\n\n\nThat's cleaner - I will combine the logic in v5.\n\n\n>\n>> +       else if (!strcmp(ref_action_str, \"print\"))\n>> +               ref_action = REF_ACTION_PRINT;\n>> +       else\n>> +               die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n>> +\n> [...]\n>\n>>   test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n>> -       git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n>> +       git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n>>\n>>          test_line_count = 4 result &&\n>>          cut -f 3 -d \" \" result >new-branch-tips &&\n> Are there tests with the new default behavior added? It looks like all\n> the changes in the test script are about adding \"--ref-action=print\"\n> to an existing test.\n\n\nYes, they're in patch 2 - the atomic behavior tests that verify no \noutput and direct\nref updates. I should highlight this better in the commit message since \nthey test the\nabsence of output (the new default).\n\n\nThanks,\nSiddharth\n\n"},{"id":"529823","messageId":"359f1d65-b5b9-451a-95cc-c62343798c60@gmail.com","threadId":"64109","inReplyTo":"a4cd31ad-7086-4d05-ba00-db65ec24b45a@gmail.com","subject":"Re: [PATCH v4 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T19:46:34Z","receivedAt":"2025-10-28T19:46:41Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 24/10/25 18:58, Phillip Wood wrote:\n> On 22/10/2025 19:50, Siddharth Asthana wrote:\n>\n> This is looking pretty nice now, I've left some on he tests comments \n> below\n\n\nThanks for the test improvements!\n\n\n>\n>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>> index 54c86b87d8..307beb667e 100755\n>> --- a/t/t3650-replay-basics.sh\n>> +++ b/t/t3650-replay-basics.sh\n>> @@ -217,4 +217,46 @@ test_expect_success \n>> 'merge.directoryRenames=false' '\n>>           --onto rename-onto rename-onto..rename-from\n>>   '\n>>   +test_expect_success 'replay.refAction config option' '\n>> +    # Store original state\n>> +    START=$(git rev-parse topic2) &&\n>\n> Isn't there a tag we can use here from the initial setup?\n\n\nGood point - I'll use `topic1` instead of `$(git rev-parse topic2)` for\nconsistency with the existing test patterns.\n\n\n>\n>> +    test_when_finished \"git branch -f topic2 $START && git config \n>> --unset replay.refAction\" &&\n>> +\n>> +    # Set config to print\n>> +    git config replay.refAction print &&\n> I think it would be better to use test_config here rather than having \n> to clear the config manually with test_when_finished() above.\n\n\nAbsolutely, `test_config` is much cleaner and handles the cleanup \nautomatically.\nI will refactor all the config tests to use this pattern.\n\n\n>\n>> +    git replay --onto main topic1..topic2 >output &&\n>> +    test_line_count = 1 output &&\n>> +    grep \"^update refs/heads/topic2 \" output &&\n>\n> Rather than test_line_count and grep it would be better to use \n> test_cmp here.\n\n\nWill switch to `test_cmp` where appropriate, and definitely change \n`grep` to\n`test_grep` for better error reporting.\n\nThanks,\nSiddharth\n\n\n>\n> The same comments apply to the rest of the tests\n>\n> Thanks\n>\n> Phillip\n>\n>> +\n>> +    # Reset and test update mode\n>> +    git branch -f topic2 $START &&\n>> +    git config replay.refAction update &&\n>> +    git replay --onto main topic1..topic2 >output &&\n>> +    test_must_be_empty output &&\n>> +\n>> +    # Verify ref was updated\n>> +    git log --format=%s topic2 >actual &&\n>> +    test_write_lines E D M L B A >expect &&\n>> +    test_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'command-line --ref-action overrides config' '\n>> +    # Store original state\n>> +    START=$(git rev-parse topic2) &&\n>> +    test_when_finished \"git branch -f topic2 $START && git config \n>> --unset replay.refAction\" &&\n>> +\n>> +    # Set config to update but use --ref-action=print\n>> +    git config replay.refAction update &&\n>> +    git replay --ref-action=print --onto main topic1..topic2 >output &&\n>> +    test_line_count = 1 output &&\n>> +    grep \"^update refs/heads/topic2 \" output\n>> +'\n>> +\n>> +test_expect_success 'invalid replay.refAction value' '\n>> +    test_when_finished \"git config --unset replay.refAction\" &&\n>> +    git config replay.refAction invalid &&\n>> +    test_must_fail git replay --onto main topic1..topic2 2>error &&\n>> +    grep \"invalid value for replay.refAction\" error\n>> +'\n>> +\n>>   test_done\n>\n>\n"},{"id":"529824","messageId":"84729f5b-87a5-4a5e-a875-c28ddcea3b5b@gmail.com","threadId":"64109","inReplyTo":"7a3161d1-4e30-4156-876d-7eede4b06705@gmail.com","subject":"Re: [PATCH v4 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T19:47:34Z","receivedAt":"2025-10-28T19:47:41Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 24/10/25 19:06, Phillip Wood wrote:\n> On 24/10/2025 14:28, Phillip Wood wrote:\n>> On 22/10/2025 19:50, Siddharth Asthana wrote:\n>>\n>>> +    git replay --onto main topic1..topic2 >output &&\n>>> +    test_line_count = 1 output &&\n>>> +    grep \"^update refs/heads/topic2 \" output &&\n>>\n>> Rather than test_line_count and grep it would be better to use \n>> test_cmp here.\n>\n> Oh, I've just realized we don't know the value of the ref so \n> test_line_count() plus test_grep() (not grep) makes sense.\n\n\nExactly, since we can't predict the exact hash values, `test_line_count` +\n`test_grep` is the right approach. I will definitely switch from `grep` to\n`test_grep` as you and Christian both suggested.\n\nThanks,\nSiddharth\n\n\n>\n> Thanks\n>\n> Phillip\n>\n"},{"id":"529828","messageId":"5ba34d92-1032-43d0-806a-91e190b24524@gmail.com","threadId":"64109","inReplyTo":"xmqq7bwkqpua.fsf@gitster.g","subject":"Re: [PATCH v4 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T20:08:27Z","receivedAt":"2025-10-28T20:08:34Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 24/10/25 21:00, Junio C Hamano wrote:\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n>> It seems to me that a dedicated function could handle this a bit\n>> better. Maybe something like:\n>>\n>> static enum ref_action_mode get_ref_action_mode(const char *ref_action_str)\n>> {\n>>       const char *config_value = NULL;\n>>\n>>       if (!strcmp(ref_action_str, \"update\"))\n>>               return REF_ACTION_UPDATE;\n>>        if (!strcmp(ref_action_str, \"print\"))\n>>              return REF_ACTION_PRINT;\n>>        if (ref_action_str)\n>>              die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n>>\n>>        if (repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n>>               return REF_ACTION_UPDATE; /* default */\n>>\n>>        if (!strcmp(config_value, \"update\"))\n>>               return REF_ACTION_UPDATE;\n>>        if (!strcmp(config_value, \"print\"))\n>>              return REF_ACTION_PRINT;\n>>        die(_(\"invalid value for replay.refAction: '%s'\"), config_value);\n>> }\n> You'd want to do \"string to enum\" helper function just once and call\n> that helper from the above function, once for the command line option\n> and again for the configuration variable.\n\n\nThat makes perfect sense - a single `parse_ref_action_mode()` helper \nthat both\ncan use will eliminate the duplication, as Christian suggested.\n\n\n>\n> Or do so where you would add a call to the above function directly\n> without your helper.  I am not convinced that \"here is the command\n> line option (or perhaps we got nothing); what is the desired\n> setting, taking configuration also into consideration?\" is\n> particularly a good abstraction.  It is more common to have\n> git_config() to grab replay.refAction string, and if there is a\n> string value, pass the last one to \"string to enum\" helper and\n> remember the result, then call parse_options() to further overwrite\n> the result from the command line option string (which again will use\n> the \"string to enum\" helper).  The structure that requires your helper\n> function smells rather unusual.\n\n\nThanks for the guidance on the standard Git pattern. I had initially \nplanned\nto follow Christian's combined approach, but you are right that the \ntraditional\nGit pattern is more conventional. Looking at builtin/am.c and \nbuiltin/column.c,\nI can see they follow:\n\n1. `repo_config()` with callback before `parse_options()`\n2. Command-line options naturally override config values\n3. Clean separation between config reading and option parsing\n\nI will implement it this way in v5 - using Christian's suggestion for \nthe helper\nfunctions but following the established Git config-then-parse-options \npattern\nfor the overall structure.\n\n\n>\n>> [...]\n>>\n>>> +test_expect_success 'replay.refAction config option' '\n>>> +       # Store original state\n>>> +       START=$(git rev-parse topic2) &&\n>>> +       test_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n>>> +\n>>> +       # Set config to print\n>>> +       git config replay.refAction print &&\n>>> +       git replay --onto main topic1..topic2 >output &&\n>>> +       test_line_count = 1 output &&\n>>> +       grep \"^update refs/heads/topic2 \" output &&\n>> Nit: here and below, it's a bit better to use test_grep instead of\n>> grep for better error reporting.\n> Yes, \"a bit\" -> \"much\".\n\n\nWill switch to `test_grep` throughout.\n\nThanks for the architectural guidance!\n\nThanks,\nSiddharth\n\n\n>\n> Thanks.\n"},{"id":"529831","messageId":"90b3f359-6b24-4858-848a-531478b21f65@gmail.com","threadId":"64109","inReplyTo":"xmqqbjlwqq6d.fsf@gitster.g","subject":"Re: [PATCH v4 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T20:18:06Z","receivedAt":"2025-10-28T20:18:13Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 24/10/25 20:53, Junio C Hamano wrote:\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n>> On Wed, Oct 22, 2025 at 8:51 PM Siddharth Asthana\n>> <siddharthasthana31@gmail.com> wrote:\n>>\n>>> -       const char * const replay_usage[] = {\n>>> +       const char *const replay_usage[] = {\n>> Nit: Not sure this change is worth it, but I understand that it might\n>> help pass some automated/CI tests, so not a big issue.\n> I think this formatting issue came up recently on another discussion\n> thread.  We found that the prevalent style in the codebase is that\n> an asterisk in between tokens neither of which is variable has space\n> on both sides (i.e. the preimage of the above change), so unless\n> there is a specific reason to make the above change, I'd rather not\n> to see such \"reformatting\" thrown into a patch that implements a\n> feature or fixes a bug (iow, not a \"clean-up styles\" patch).\n\n\nYou are absolutely right, I will revert this formatting change in v5. The\n`const char * const` spacing follows the established codebase style and\nthere's no reason to change it in a feature patch.\n\n\n>\n> By the way, I would be suprised if that the reason were a CI test.\n> How would the preimage have been passing the same test if that is\n> the case?\n\n\nGood point, it wasn't a CI issue, just an unnecessary style change on my \npart.\n\nThanks,\nSiddharth\n\n"},{"id":"529832","messageId":"1a4740b8-eb63-4266-b627-d329451b7a4b@gmail.com","threadId":"64109","inReplyTo":"xmqq7bwlv4jh.fsf@gitster.g","subject":"Re: [PATCH v4 0/3] replay: make atomic ref updates the default","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T20:19:13Z","receivedAt":"2025-10-28T20:19:20Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 24/10/25 00:17, Junio C Hamano wrote:\n> Siddharth Asthana <siddharthasthana31@gmail.com> writes:\n>\n>> This is v4 of the git-replay atomic updates series.\n>>\n>> Based on feedback from v3, this version improves the naming and\n>> implementation for clarity and type safety. Thanks to Junio, Christian,\n>> Elijah, Phillip, Patrick, and Karthik for the detailed reviews.\n>>\n>> ## Changes in v4\n>>\n>> **Renamed --update-refs to --ref-action**\n>>\n>> Junio pointed out that \"--update-refs=print\" is semantically awkward.\n>> Answering \"print\" to the question \"update refs?\" doesn't make sense.\n>> The actual question is \"what action should we take on the refs?\"\n>>\n>> Changed to --ref-action=(update|print) where both values are verbs that\n>> answer \"what action?\". This makes the interface clearer.\n>>\n>> **Aligned config name with command-line option**\n>>\n>> Changed replay.defaultAction to replay.refAction. The config variable\n>> now mirrors the option name, making the relationship obvious.\n>>\n>> **Unified config and command-line values**\n> I didn't see anything glaringly wrong in this round, even though I\n> picked a couple of small nits in one patch, so we might want a\n> hopefully small and final reroll before marking the topic for\n> 'next'.\n\n\nThanks! I will address all the feedback from you, Christian, and Phillip \nin v5:\n\n- Add trailing comma to enum definition\n- Fix error message quoting with single quotes\n- Revert the `const char * const` formatting change\n- Follow standard Git config pattern (repo_config before parse_options)\n- Extract proper helper functions for string-to-enum conversion\n- Switch to `test_grep` and `test_config` in tests\n- Fix documentation wording issues\n\nShould have v5 ready soon with these fixes.\n\nThanks,\nSiddharth\n\n\n>\n> Is everybody else happy with this iteration otherwise?\n>\n> Thanks.\n"},{"id":"529836","messageId":"20251028214609.10041-1-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251022185045.29256-1-siddharthasthana31@gmail.com","subject":"[PATCH v5 0/3] replay: make atomic ref updates the default","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T21:46:06Z","receivedAt":"2025-10-28T21:46:22Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"This is v5 of the git-replay atomic updates series.\n\nThis version addresses all feedback from v4 reviews. Thanks to Junio,\nChristian, and Phillip for the detailed technical reviews that helped\nrefine the implementation to Git standards.\n\n## Changes in v5\n\n**Added enum trailing comma**\n\nPer Junio's suggestion, added trailing comma to enum definition for\nfuture extensibility. This follows Git's established pattern and\nminimizes patch noise when adding new enum values.\n\n**Fixed error message formatting**\n\nFollowing CodingGuidelines, wrapped ref names in single quotes in\nerror messages:\n  - error(_(\"failed to update ref '%s': %s\"), ...)\n\nThis provides better visual clarity and matches Git's error reporting\nconventions throughout the codebase.\n\n**Extracted helper functions for config parsing**\n\nPer Christian and Junio's feedback, refactored config parsing into\nclean helper functions:\n  - parse_ref_action_mode(): String-to-enum conversion with source context\n  - get_ref_action_mode(): Handles command-line vs config precedence\n\nThis eliminates code duplication and provides a single point for\nvalidation logic, making the code more maintainable.\n\n**Improved test suite with Git best practices**\n\nFollowing Phillip and Christian's suggestions:\n  - Switched from grep to test_grep for better error reporting\n  - Used test_config for automatic config cleanup\n  - Improved test isolation with proper state management\n  - Used topic1 tag instead of $(git rev-parse) where appropriate\n\n**Documentation improvements**\n\nFixed terminology and wording per Christian's feedback:\n  - \"ergonomical\" → \"ergonomic\"\n  - \"configuration option\" → \"configuration variable\"\n  - \"By default (with `--ref-action=update`)\" → \"By default, or with `--ref-action=update`,\"\n\n**Reverted unnecessary style change**\n\nPer Junio's feedback, reverted the `const char * const` → `const char *const`\nspacing change. The original spacing follows the prevalent codebase style.\n\n## Technical Implementation\n\nThe atomic ref updates leverage Git's ref transaction API:\n- ref_store_transaction_begin() with default atomic behavior\n- ref_transaction_update() to stage each ref update\n- ref_transaction_commit() for atomic application (all succeed or all fail)\n\nThe helper functions provide clean separation of concerns:\n- parse_ref_action_mode() validates strings and converts to enum\n- get_ref_action_mode() implements command-line > config > default precedence\n- handle_ref_update() uses type-safe enum with switch statement\n\nThe on-demand config reading via repo_config_get_string_tmp() is simpler\nthan the traditional repo_config() callback pattern for this single-variable\ncase, while maintaining proper precedence behavior.\n\n## Testing\n\nAll tests pass:\n- t3650-replay-basics.sh (20 tests pass)\n- New atomic behavior tests verify direct ref updates\n- Config tests verify proper precedence and error handling\n- Existing pipeline tests ensure backward compatibility\n\nCI results: https://gitlab.com/gitlab-org/git/-/pipelines/2123403204\n\nSiddharth Asthana (3):\n  replay: use die_for_incompatible_opt2() for option validation\n  replay: make atomic ref updates the default behavior\n  replay: add replay.refAction config option\n\n Documentation/config/replay.adoc |  11 +++\n Documentation/git-replay.adoc    |  65 +++++++++++------\n builtin/replay.c                 | 121 +++++++++++++++++++++++++++----\n t/t3650-replay-basics.sh         |  91 +++++++++++++++++++++--\n 4 files changed, 245 insertions(+), 43 deletions(-)\n create mode 100644 Documentation/config/replay.adoc\n\nRange-diff against v4:\n1:  baa0cfdd4a = 1:  3e27d07d3b replay: use die_for_incompatible_opt2() for option validation\n2:  3b5df166f3 ! 2:  643d9ca86a replay: make atomic ref updates the default behavior\n    @@ Metadata\n     Author: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## Commit message ##\n         replay: make atomic ref updates the default behavior\n         \n         [Commit message unchanged - explains problem and solution]\n     \n     @@ builtin/replay.c: #include <tree.h>\n      \n     +enum ref_action_mode {\n     +\tREF_ACTION_UPDATE,\n    -+\tREF_ACTION_PRINT\n    ++\tREF_ACTION_PRINT,\n     +};\n      \n     @@ builtin/replay.c: int cmd_replay\n    -\t\t\t\t\tret = error(_(\"failed to update ref %s: %s\"),\n    -\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n    +\t\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n     \n     @@ builtin/replay.c: int cmd_replay\n    -\t\t\tret = error(_(\"failed to update ref %s: %s\"),\n    -\t\t\t\t    advance_name, transaction_err.buf);\n    +\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n     \n     @@ Documentation/git-replay.adoc\n    -+    almost certainly find it more ergonomical to simply have the updating\n    ++    almost certainly find it more ergonomic to simply have the updating\n     \n     @@ Documentation/git-replay.adoc\n    -+The default mode can be configured via `replay.refAction` configuration option.\n    ++The default mode can be configured via the `replay.refAction` configuration variable.\n     \n     @@ Documentation/git-replay.adoc: OUTPUT\n    -+By default (with `--ref-action=update`), this command produces no output on\n    ++By default, or with `--ref-action=update`, this command produces no output on\n     \n     -       const char * const replay_usage[] = {\n    -+       const char *const replay_usage[] = {\n    ++       const char * const replay_usage[] = {\n     \n3:  c35049881d ! 3:  334da71911 replay: add replay.refAction config option\n    @@ Metadata\n     Author: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## Commit message ##\n         replay: add replay.refAction config option\n         \n         [Commit message unchanged]\n     \n     @@ builtin/replay.c: static struct commit *pick_regular_commit\n      \treturn create_commit(repo, result->tree, pickme, replayed_base);\n      }\n      \n    ++static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)\n    ++{\n    ++\tif (!mode_str || !strcmp(mode_str, \"update\"))\n    ++\t\treturn REF_ACTION_UPDATE;\n    ++\tif (!strcmp(mode_str, \"print\"))\n    ++\t\treturn REF_ACTION_PRINT;\n    ++\tdie(_(\"invalid %s value: '%s'\"), source, mode_str);\n    ++}\n    ++\n    ++static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)\n    ++{\n    ++\tconst char *config_value = NULL;\n    ++\n    ++\t/* Command line option takes precedence */\n    ++\tif (ref_action_str)\n    ++\t\treturn parse_ref_action_mode(ref_action_str, \"--ref-action\");\n    ++\n    ++\t/* Check config value */\n    ++\tif (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n    ++\t\treturn parse_ref_action_mode(config_value, \"replay.refAction\");\n    ++\n    ++\t/* Default to update mode */\n    ++\treturn REF_ACTION_UPDATE;\n    ++}\n    ++\n     @@ builtin/replay.c: int cmd_replay\n      \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n      \t\t\t\t  contained, \"--contained\");\n      \n    -+\t/* Set default mode from config if not specified on command line */\n    -+\tif (!ref_action_str) {\n    -+\t\tconst char *config_value = NULL;\n    -+\t\tif (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value)) {\n    -+\t\t\tif (!strcmp(config_value, \"update\"))\n    -+\t\t\t\tref_action_str = \"update\";\n    -+\t\t\telse if (!strcmp(config_value, \"print\"))\n    -+\t\t\t\tref_action_str = \"print\";\n    -+\t\t\telse\n    -+\t\t\t\tdie(_(\"invalid value for replay.refAction: '%s'\"), config_value);\n    -+\t\t}\n    -+\t}\n    -+\n    -+\t/* Default to update mode if still not set */\n    -+\tif (!ref_action_str)\n    -+\t\tref_action_str = \"update\";\n    -+\n    -+\t/* Parse ref action mode */\n    -+\tif (!strcmp(ref_action_str, \"update\"))\n    -+\t\tref_action = REF_ACTION_UPDATE;\n    -+\telse if (!strcmp(ref_action_str, \"print\"))\n    -+\t\tref_action = REF_ACTION_PRINT;\n    -+\telse\n    -+\t\tdie(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n    ++\t/* Parse ref action mode from command line or config */\n    ++\tref_action = get_ref_action_mode(repo, ref_action_str);\n     \n     @@ t/t3650-replay-basics.sh\n     +test_expect_success 'replay.refAction config option' '\n     +\tSTART=$(git rev-parse topic2) &&\n    -+\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n    ++\ttest_when_finished \"git branch -f topic2 $START\" &&\n    ++\ttest_when_finished \"git config --unset replay.refAction || true\" &&\n     +\n     +\tgit config replay.refAction print &&\n     +\tgit replay --onto main topic1..topic2 >output &&\n     +\ttest_line_count = 1 output &&\n    -+\tgrep \"^update refs/heads/topic2 \" output &&\n    ++\ttest_grep \"^update refs/heads/topic2 \" output &&\n     +\n     +\tgit branch -f topic2 $START &&\n     +\tgit config replay.refAction update &&\n     \n     +test_expect_success 'command-line --ref-action overrides config' '\n     +\tSTART=$(git rev-parse topic2) &&\n    -+\ttest_when_finished \"git branch -f topic2 $START && git config --unset replay.refAction\" &&\n    ++\ttest_when_finished \"git branch -f topic2 $START\" &&\n     +\n    -+\tgit config replay.refAction update &&\n    ++\ttest_config replay.refAction update &&\n     +\tgit replay --ref-action=print --onto main topic1..topic2 >output &&\n     +\ttest_line_count = 1 output &&\n    -+\tgrep \"^update refs/heads/topic2 \" output\n    ++\ttest_grep \"^update refs/heads/topic2 \" output\n     +'\n     +\n     +test_expect_success 'invalid replay.refAction value' '\n    -+\ttest_when_finished \"git config --unset replay.refAction\" &&\n    -+\tgit config replay.refAction invalid &&\n    ++\ttest_config replay.refAction invalid &&\n     +\ttest_must_fail git replay --onto main topic1..topic2 2>error &&\n    -+\tgrep \"invalid value for replay.refAction\" error\n    ++\ttest_grep \"invalid.*replay.refAction.*value\" error\n     +'\n\n-- \n2.51.0\n\nbase-commit: 419c72cb8ada252b260efc38ff91fe201de7c8c3\n\nThanks\n- Siddharth\n"},{"id":"529837","messageId":"20251028214609.10041-2-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251028214609.10041-1-siddharthasthana31@gmail.com","subject":"[PATCH v5 1/3] replay: use die_for_incompatible_opt2() for option validation","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T21:46:07Z","receivedAt":"2025-10-28T21:46:30Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"In preparation for adding the --ref-action option, convert option\nvalidation to use die_for_incompatible_opt2(). This helper provides\nstandardized error messages for mutually exclusive options.\n\nThe following commit introduces --ref-action which will be incompatible\nwith certain other options. Using die_for_incompatible_opt2() now means\nthat commit can cleanly add its validation using the same pattern,\nkeeping the validation logic consistent and maintainable.\n\nThis also aligns git-replay's option handling with how other Git commands\nmanage option conflicts, using the established die_for_incompatible_opt*()\nhelper family.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n builtin/replay.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 6172c8aacc..b64fc72063 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -330,9 +330,9 @@ int cmd_replay(int argc,\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n-\tif (advance_name_opt && contained)\n-\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n-\t\t    \"--advance\", \"--contained\");\n+\tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n+\t\t\t\t  contained, \"--contained\");\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n-- \n2.51.0\n\n"},{"id":"529838","messageId":"20251028214609.10041-3-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251028214609.10041-1-siddharthasthana31@gmail.com","subject":"[PATCH v5 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T21:46:08Z","receivedAt":"2025-10-28T21:46:38Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"The git replay command currently outputs update commands that can be\npiped to update-ref to achieve a rebase, e.g.\n\n  git replay --onto main topic1..topic2 | git update-ref --stdin\n\nThis separation had advantages for three special cases:\n  * it made testing easy (when state isn't modified from one step to\n    the next, you don't need to make temporary branches or have undo\n    commands, or try to track the changes)\n  * it provided a natural can-it-rebase-cleanly (and what would it\n    rebase to) capability without automatically updating refs, similar\n    to a --dry-run\n  * it provided a natural low-level tool for the suite of hash-object,\n    mktree, commit-tree, mktag, merge-tree, and update-ref, allowing\n    users to have another building block for experimentation and making\n    new tools\n\nHowever, it should be noted that all three of these are somewhat\nspecial cases; users, whether on the client or server side, would\nalmost certainly find it more ergonomic to simply have the updating\nof refs be the default.\n\nFor server-side operations in particular, the pipeline architecture\ncreates process coordination overhead. Server implementations that need\nto perform rebases atomically must maintain additional code to:\n\n  1. Spawn and manage a pipeline between git-replay and git-update-ref\n  2. Coordinate stdout/stderr streams across the pipe boundary\n  3. Handle partial failure states if the pipeline breaks mid-execution\n  4. Parse and validate the update-ref command output\n\nChange the default behavior to update refs directly, and atomically (at\nleast to the extent supported by the refs backend in use). This\neliminates the process coordination overhead for the common case.\n\nFor users needing the traditional pipeline workflow, add a new\n--ref-action=<mode> option that preserves the original behavior:\n\n  git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin\n\nThe mode can be:\n  * update (default): Update refs directly using an atomic transaction\n  * print: Output update-ref commands for pipeline use\n\nImplementation details:\n\nThe atomic ref updates are implemented using Git's ref transaction API.\nIn cmd_replay(), when not in `print` mode, we initialize a transaction\nusing ref_store_transaction_begin() with the default atomic behavior.\nAs commits are replayed, ref updates are staged into the transaction\nusing ref_transaction_update(). Finally, ref_transaction_commit()\napplies all updates atomically—either all updates succeed or none do.\n\nTo avoid code duplication between the 'print' and 'update' modes, this\ncommit extracts a handle_ref_update() helper function. This function\ntakes the mode (as an enum) and either prints the update command or\nstages it into the transaction. Using an enum rather than passing the\nstring around provides type safety and allows the compiler to catch\ntypos. The switch statement makes it easy to add future modes.\n\nThe helper function signature:\n\n  static int handle_ref_update(enum ref_action_mode mode,\n                                struct ref_transaction *transaction,\n                                const char *refname,\n                                const struct object_id *new_oid,\n                                const struct object_id *old_oid,\n                                struct strbuf *err)\n\nThe enum is defined as:\n\n  enum ref_action_mode {\n      REF_ACTION_UPDATE,\n      REF_ACTION_PRINT\n  };\n\nThe mode string is converted to enum immediately after parse_options()\nto avoid string comparisons throughout the codebase and provide compiler\nprotection against typos.\n\nTest suite changes:\n\nAll existing tests that expected command output now use\n--ref-action=print to preserve their original behavior. This keeps\nthe tests valid while allowing them to verify that the pipeline workflow\nstill works correctly.\n\nNew tests were added to verify:\n  - Default atomic behavior (no output, refs updated directly)\n  - Bare repository support (server-side use case)\n  - Equivalence between traditional pipeline and atomic updates\n  - Real atomicity using a lock file to verify all-or-nothing guarantee\n  - Test isolation using test_when_finished to clean up state\n\nThe bare repository tests were fixed to rebuild their expectations\nindependently rather than comparing to previous test output, improving\ntest reliability and isolation.\n\nA following commit will add a replay.refAction configuration\noption for users who prefer the traditional pipeline output as their\ndefault behavior.\n\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc | 65 +++++++++++++++--------\n builtin/replay.c              | 98 +++++++++++++++++++++++++++++++----\n t/t3650-replay-basics.sh      | 44 +++++++++++++---\n 3 files changed, 167 insertions(+), 40 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 0b12bf8aa4..037b093196 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n \n DESCRIPTION\n -----------\n \n Takes ranges of commits and replays them onto a new location. Leaves\n-the working tree and the index untouched, and updates no references.\n-The output of this command is meant to be used as input to\n-`git update-ref --stdin`, which would update the relevant branches\n+the working tree and the index untouched. By default, updates the\n+relevant references using an atomic transaction (all refs update or\n+none). Use `--ref-action=print` to avoid automatic ref updates and\n+instead get update commands that can be piped to `git update-ref --stdin`\n (see the OUTPUT section below).\n \n THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n@@ -29,18 +30,31 @@ OPTIONS\n \tStarting point at which to create the new commits.  May be any\n \tvalid commit, and not just an existing branch name.\n +\n-When `--onto` is specified, the update-ref command(s) in the output will\n-update the branch(es) in the revision range to point at the new\n-commits, similar to the way how `git rebase --update-refs` updates\n-multiple branches in the affected range.\n+When `--onto` is specified, the branch(es) in the revision range will be\n+updated to point at the new commits (or update commands will be printed\n+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`\n+updates multiple branches in the affected range.\n \n --advance <branch>::\n \tStarting point at which to create the new commits; must be a\n \tbranch name.\n +\n-When `--advance` is specified, the update-ref command(s) in the output\n-will update the branch passed as an argument to `--advance` to point at\n-the new commits (in other words, this mimics a cherry-pick operation).\n+The history is replayed on top of the <branch> and <branch> is updated to\n+point at the tip of the resulting history (or an update command will be\n+printed if `--ref-action=print` is used). This is different from `--onto`,\n+which uses the target only as a starting point without updating it.\n+\n+--ref-action[=<mode>]::\n+\tControl how references are updated. The mode can be:\n++\n+--\n+\t* `update` (default): Update refs directly using an atomic transaction.\n+\t  All refs are updated or none are (all-or-nothing behavior).\n+\t* `print`: Output update-ref commands for pipeline use. This is the\n+\t  traditional behavior where output can be piped to `git update-ref --stdin`.\n+--\n++\n+The default mode can be configured via the `replay.refAction` configuration variable.\n \n <revision-range>::\n \tRange of commits to replay. More than one <revision-range> can\n@@ -54,8 +68,11 @@ include::rev-list-options.adoc[]\n OUTPUT\n ------\n \n-When there are no conflicts, the output of this command is usable as\n-input to `git update-ref --stdin`.  It is of the form:\n+By default, or with `--ref-action=update`, this command produces no output on\n+success, as refs are updated directly using an atomic transaction.\n+\n+When using `--ref-action=print`, the output is usable as input to\n+`git update-ref --stdin`. It is of the form:\n \n \tupdate refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n \tupdate refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n@@ -81,6 +98,14 @@ To simply rebase `mybranch` onto `target`:\n \n ------------\n $ git replay --onto target origin/main..mybranch\n+------------\n+\n+The refs are updated atomically and no output is produced on success.\n+\n+To see what would be updated without actually updating:\n+\n+------------\n+$ git replay --ref-action=print --onto target origin/main..mybranch\n update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n ------------\n \n@@ -88,33 +113,29 @@ To cherry-pick the commits from mybranch onto target:\n \n ------------\n $ git replay --advance target origin/main..mybranch\n-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n ------------\n \n Note that the first two examples replay the exact same commits and on\n top of the exact same new base, they only differ in that the first\n-provides instructions to make mybranch point at the new commits and\n-the second provides instructions to make target point at them.\n+updates mybranch to point at the new commits and the second updates\n+target to point at them.\n \n What if you have a stack of branches, one depending upon another, and\n you'd really like to rebase the whole set?\n \n ------------\n $ git replay --contained --onto origin/main origin/main..tipbranch\n-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n ------------\n \n+All three branches (`branch1`, `branch2`, and `tipbranch`) are updated\n+atomically.\n+\n When calling `git replay`, one does not need to specify a range of\n commits to replay using the syntax `A..B`; any range expression will\n do:\n \n ------------\n $ git replay --onto origin/main ^base branch1 branch2 branch3\n-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n ------------\n \n This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex b64fc72063..0564d4d2e7 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -20,6 +20,11 @@\n #include <oidset.h>\n #include <tree.h>\n \n+enum ref_action_mode {\n+\tREF_ACTION_UPDATE,\n+\tREF_ACTION_PRINT,\n+};\n+\n static const char *short_commit_name(struct repository *repo,\n \t\t\t\t     struct commit *commit)\n {\n@@ -284,6 +289,28 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \treturn create_commit(repo, result->tree, pickme, replayed_base);\n }\n \n+static int handle_ref_update(enum ref_action_mode mode,\n+\t\t\t     struct ref_transaction *transaction,\n+\t\t\t     const char *refname,\n+\t\t\t     const struct object_id *new_oid,\n+\t\t\t     const struct object_id *old_oid,\n+\t\t\t     struct strbuf *err)\n+{\n+\tswitch (mode) {\n+\tcase REF_ACTION_PRINT:\n+\t\tprintf(\"update %s %s %s\\n\",\n+\t\t       refname,\n+\t\t       oid_to_hex(new_oid),\n+\t\t       oid_to_hex(old_oid));\n+\t\treturn 0;\n+\tcase REF_ACTION_UPDATE:\n+\t\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n+\t\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n+\tdefault:\n+\t\tBUG(\"unknown ref_action_mode %d\", mode);\n+\t}\n+}\n+\n int cmd_replay(int argc,\n \t       const char **argv,\n \t       const char *prefix,\n@@ -294,6 +321,8 @@ int cmd_replay(int argc,\n \tstruct commit *onto = NULL;\n \tconst char *onto_name = NULL;\n \tint contained = 0;\n+\tconst char *ref_action_str = NULL;\n+\tenum ref_action_mode ref_action = REF_ACTION_UPDATE;\n \n \tstruct rev_info revs;\n \tstruct commit *last_commit = NULL;\n@@ -302,12 +331,14 @@ int cmd_replay(int argc,\n \tstruct merge_result result;\n \tstruct strset *update_refs = NULL;\n \tkh_oid_map_t *replayed_commits;\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strbuf transaction_err = STRBUF_INIT;\n \tint ret = 0;\n \n-\tconst char * const replay_usage[] = {\n+\tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n-\t\t   \"<revision-range>...\"),\n+\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -319,6 +350,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &contained,\n \t\t\t N_(\"advance all branches contained in revision-range\")),\n+\t\tOPT_STRING(0, \"ref-action\", &ref_action_str,\n+\t\t\t   N_(\"mode\"),\n+\t\t\t   N_(\"control ref update behavior (update|print)\")),\n \t\tOPT_END()\n \t};\n \n@@ -333,6 +367,18 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n+\t/* Default to update mode if not specified */\n+\tif (!ref_action_str)\n+\t\tref_action_str = \"update\";\n+\n+\t/* Parse ref action mode */\n+\tif (!strcmp(ref_action_str, \"update\"))\n+\t\tref_action = REF_ACTION_UPDATE;\n+\telse if (!strcmp(ref_action_str, \"print\"))\n+\t\tref_action = REF_ACTION_PRINT;\n+\telse\n+\t\tdie(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n@@ -389,6 +435,17 @@ int cmd_replay(int argc,\n \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n \t\t\t      &onto, &update_refs);\n \n+\t/* Initialize ref transaction if using update mode */\n+\tif (ref_action == REF_ACTION_UPDATE) {\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n+\t\t\t\t\t\t\t  0, &transaction_err);\n+\t\tif (!transaction) {\n+\t\t\tret = error(_(\"failed to begin ref transaction: %s\"),\n+\t\t\t\t    transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n \tif (!onto) /* FIXME: Should handle replaying down to root commit */\n \t\tdie(\"Replaying down to root commit is not supported yet!\");\n \n@@ -434,10 +491,15 @@ int cmd_replay(int argc,\n \t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n \t\t\t    (contained || strset_contains(update_refs,\n \t\t\t\t\t\t\t  decoration->name))) {\n-\t\t\t\tprintf(\"update %s %s %s\\n\",\n-\t\t\t\t       decoration->name,\n-\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t\t\t       oid_to_hex(&commit->object.oid));\n+\t\t\t\tif (handle_ref_update(ref_action, transaction,\n+\t\t\t\t\t\t      decoration->name,\n+\t\t\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t\t\t      &commit->object.oid,\n+\t\t\t\t\t\t      &transaction_err) < 0) {\n+\t\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n+\t\t\t\t\tgoto cleanup;\n+\t\t\t\t}\n \t\t\t}\n \t\t\tdecoration = decoration->next;\n \t\t}\n@@ -445,10 +507,23 @@ int cmd_replay(int argc,\n \n \t/* In --advance mode, advance the target ref */\n \tif (result.clean == 1 && advance_name) {\n-\t\tprintf(\"update %s %s %s\\n\",\n-\t\t       advance_name,\n-\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t       oid_to_hex(&onto->object.oid));\n+\t\tif (handle_ref_update(ref_action, transaction, advance_name,\n+\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t      &onto->object.oid,\n+\t\t\t\t      &transaction_err) < 0) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    advance_name, transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n+\t/* Commit the ref transaction if we have one */\n+\tif (transaction && result.clean == 1) {\n+\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n+\t\t\tret = error(_(\"failed to commit ref transaction: %s\"),\n+\t\t\t\t    transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n \t}\n \n \tmerge_finalize(&merge_opt, &result);\n@@ -460,6 +535,9 @@ int cmd_replay(int argc,\n \tret = result.clean;\n \n cleanup:\n+\tif (transaction)\n+\t\tref_transaction_free(transaction);\n+\tstrbuf_release(&transaction_err);\n \trelease_revisions(&revs);\n \tfree(advance_name);\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 58b3759935..123734b49f 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n '\n \n test_expect_success 'using replay to rebase two branches, one on top of other' '\n-\tgit replay --onto main topic1..topic2 >result &&\n+\tgit replay --ref-action=print --onto main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n '\n \n test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n-\tgit -C bare replay --onto main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n \t# 4th field of result is hash for main instead of hash for topic2\n \n-\tgit replay --advance main topic1..topic2 >result &&\n+\tgit replay --ref-action=print --advance main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n '\n \n test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n-\tgit -C bare replay --advance main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n '\n \n test_expect_success 'using replay to also rebase a contained branch' '\n-\tgit replay --contained --onto main main..topic3 >result &&\n+\tgit replay --ref-action=print --contained --onto main main..topic3 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '\n '\n \n test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n-\tgit -C bare replay --contained --onto main main..topic3 >result-bare &&\n+\tgit -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n test_expect_success 'using replay to rebase multiple divergent branches' '\n-\tgit replay --onto main ^topic1 topic2 topic4 >result &&\n+\tgit replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n '\n \n test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n-\tgit -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n+\tgit -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n \n \ttest_line_count = 4 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -217,4 +217,32 @@ test_expect_success 'merge.directoryRenames=false' '\n \t\t--onto rename-onto rename-onto..rename-from\n '\n \n+test_expect_success 'default atomic behavior updates refs directly' '\n+\t# Store original state for cleanup\n+\ttest_when_finished \"git branch -f topic2 topic1\" &&\n+\n+\t# Test default atomic behavior (no output, refs updated)\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'atomic behavior in bare repository' '\n+\t# Test atomic updates work in bare repo\n+\tgit -C bare replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated in bare repo\n+\tgit -C bare log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual &&\n+\n+\t# Reset for other tests\n+\tgit -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"529839","messageId":"20251028214609.10041-4-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251028214609.10041-1-siddharthasthana31@gmail.com","subject":"[PATCH v5 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-28T21:46:09Z","receivedAt":"2025-10-28T21:46:45Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"Add a configuration option to control the default behavior of git replay\nfor updating references. This allows users who prefer the traditional\npipeline output to set it once in their config instead of passing\n--ref-action=print with every command.\n\nThe config option uses string values that mirror the behavior modes:\n  * replay.refAction = update (default): atomic ref updates\n  * replay.refAction = print: output commands for pipeline\n\nThe command-line --ref-action option always overrides the config setting,\nallowing users to temporarily change behavior for a single invocation.\n\nImplementation details:\n\nIn cmd_replay(), after parsing command-line options, we check if\n--ref-action was provided. If not, we read the configuration using\nrepo_config_get_string_tmp(). If the config variable is set, we validate\nthe value and use it to set the ref_action_str:\n\n  Config value      Internal mode    Behavior\n  ──────────────────────────────────────────────────────────────\n  \"update\"          \"update\"         Atomic ref updates (default)\n  \"print\"           \"print\"          Pipeline output\n  (not set)         \"update\"         Atomic ref updates (default)\n  (invalid)         error            Die with helpful message\n\nIf an invalid value is provided, we die() immediately with an error\nmessage explaining the valid options. This catches configuration errors\nearly and provides clear guidance to users.\n\nThe command-line --ref-action option, when provided, overrides the\nconfig value. This precedence allows users to set their preferred default\nwhile still having per-invocation control:\n\n  git config replay.refAction print         # Set default\n  git replay --ref-action=update --onto main topic  # Override once\n\nThe config and command-line option use the same value names ('update'\nand 'print') for consistency and clarity. This makes it immediately\nobvious how the config maps to the command-line option, addressing\nfeedback about the relationship between configuration and command-line\noptions being clear to users.\n\nExamples:\n\n$ git config --global replay.refAction print\n$ git replay --onto main topic1..topic2 | git update-ref --stdin\n\n$ git replay --ref-action=update --onto main topic1..topic2\n\n$ git config replay.refAction update\n$ git replay --onto main topic1..topic2  # Updates refs directly\n\nThe implementation follows Git's standard configuration precedence:\ncommand-line options override config values, which matches user\nexpectations across all Git commands.\n\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/config/replay.adoc | 11 +++++++\n builtin/replay.c                 | 39 ++++++++++++++++++-------\n t/t3650-replay-basics.sh         | 49 +++++++++++++++++++++++++++++++-\n 3 files changed, 87 insertions(+), 12 deletions(-)\n create mode 100644 Documentation/config/replay.adoc\n\ndiff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc\nnew file mode 100644\nindex 0000000000..7d549d2f0e\n--- /dev/null\n+++ b/Documentation/config/replay.adoc\n@@ -0,0 +1,11 @@\n+replay.refAction::\n+\tSpecifies the default mode for handling reference updates in\n+\t`git replay`. The value can be:\n++\n+--\n+\t* `update`: Update refs directly using an atomic transaction (default behavior).\n+\t* `print`: Output update-ref commands for pipeline use.\n+--\n++\n+This setting can be overridden with the `--ref-action` command-line option.\n+When not configured, `git replay` defaults to `update` mode.\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 0564d4d2e7..17898bbdd1 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -8,6 +8,7 @@\n #include \"git-compat-util.h\"\n \n #include \"builtin.h\"\n+#include \"config.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"lockfile.h\"\n@@ -289,6 +290,31 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \treturn create_commit(repo, result->tree, pickme, replayed_base);\n }\n \n+static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)\n+{\n+\tif (!mode_str || !strcmp(mode_str, \"update\"))\n+\t\treturn REF_ACTION_UPDATE;\n+\tif (!strcmp(mode_str, \"print\"))\n+\t\treturn REF_ACTION_PRINT;\n+\tdie(_(\"invalid %s value: '%s'\"), source, mode_str);\n+}\n+\n+static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)\n+{\n+\tconst char *config_value = NULL;\n+\n+\t/* Command line option takes precedence */\n+\tif (ref_action_str)\n+\t\treturn parse_ref_action_mode(ref_action_str, \"--ref-action\");\n+\n+\t/* Check config value */\n+\tif (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n+\t\treturn parse_ref_action_mode(config_value, \"replay.refAction\");\n+\n+\t/* Default to update mode */\n+\treturn REF_ACTION_UPDATE;\n+}\n+\n static int handle_ref_update(enum ref_action_mode mode,\n \t\t\t     struct ref_transaction *transaction,\n \t\t\t     const char *refname,\n@@ -367,17 +393,8 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n-\t/* Default to update mode if not specified */\n-\tif (!ref_action_str)\n-\t\tref_action_str = \"update\";\n-\n-\t/* Parse ref action mode */\n-\tif (!strcmp(ref_action_str, \"update\"))\n-\t\tref_action = REF_ACTION_UPDATE;\n-\telse if (!strcmp(ref_action_str, \"print\"))\n-\t\tref_action = REF_ACTION_PRINT;\n-\telse\n-\t\tdie(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n+\t/* Parse ref action mode from command line or config */\n+\tref_action = get_ref_action_mode(repo, ref_action_str);\n \n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 123734b49f..9ca04b2fdd 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -219,7 +219,8 @@ test_expect_success 'merge.directoryRenames=false' '\n \n test_expect_success 'default atomic behavior updates refs directly' '\n \t# Store original state for cleanup\n-\ttest_when_finished \"git branch -f topic2 topic1\" &&\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n \n \t# Test default atomic behavior (no output, refs updated)\n \tgit replay --onto main topic1..topic2 >output &&\n@@ -232,6 +233,10 @@ test_expect_success 'default atomic behavior updates refs directly' '\n '\n \n test_expect_success 'atomic behavior in bare repository' '\n+\t# Store original state for cleanup\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n+\n \t# Test atomic updates work in bare repo\n \tgit -C bare replay --onto main topic1..topic2 >output &&\n \ttest_must_be_empty output &&\n@@ -245,4 +250,46 @@ test_expect_success 'atomic behavior in bare repository' '\n \tgit -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n '\n \n+test_expect_success 'replay.refAction config option' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n+\ttest_when_finished \"git config --unset replay.refAction || true\" &&\n+\n+\t# Set config to print\n+\tgit config replay.refAction print &&\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_line_count = 1 output &&\n+\ttest_grep \"^update refs/heads/topic2 \" output &&\n+\n+\t# Reset and test update mode\n+\tgit branch -f topic2 $START &&\n+\tgit config replay.refAction update &&\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'command-line --ref-action overrides config' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n+\n+\t# Set config to update but use --ref-action=print\n+\ttest_config replay.refAction update &&\n+\tgit replay --ref-action=print --onto main topic1..topic2 >output &&\n+\ttest_line_count = 1 output &&\n+\ttest_grep \"^update refs/heads/topic2 \" output\n+'\n+\n+test_expect_success 'invalid replay.refAction value' '\n+\ttest_config replay.refAction invalid &&\n+\ttest_must_fail git replay --onto main topic1..topic2 2>error &&\n+\ttest_grep \"invalid.*replay.refAction.*value\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"529877","messageId":"CAP8UFD03fx+wKwJzDG8UZz=+S8=07hG6npNnebTmBxrcXNYqGQ@mail.gmail.com","threadId":"64109","inReplyTo":"20251028214609.10041-4-siddharthasthana31@gmail.com","subject":"Re: [PATCH v5 3/3] replay: add replay.refAction config option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-10-29T16:19:32Z","receivedAt":"2025-10-29T16:19:46Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Oct 28, 2025 at 10:46 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n\n> +static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)\n\nNit: it's a bit strange that it's called \"ref_action_str\" everywhere\nexcept here where it's called \"mode_str\". I'd prefer \"ref_action\"\neverywhere.\n\n(I understand that \"mode\" is related to parse_ref_action_mode() having\n\"mode\" in its name but it's the case for get_ref_action_mode() too.)\n\n> +test_expect_success 'replay.refAction config option' '\n> +       # Store original state\n> +       START=$(git rev-parse topic2) &&\n> +       test_when_finished \"git branch -f topic2 $START\" &&\n> +       test_when_finished \"git config --unset replay.refAction || true\" &&\n\nIs there something preventing test_config to be used in this test\nwhile it's used in other tests below?\n\n> +       # Set config to print\n> +       git config replay.refAction print &&\n> +       git replay --onto main topic1..topic2 >output &&\n> +       test_line_count = 1 output &&\n> +       test_grep \"^update refs/heads/topic2 \" output &&\n> +\n> +       # Reset and test update mode\n> +       git branch -f topic2 $START &&\n> +       git config replay.refAction update &&\n> +       git replay --onto main topic1..topic2 >output &&\n> +       test_must_be_empty output &&\n> +\n> +       # Verify ref was updated\n> +       git log --format=%s topic2 >actual &&\n> +       test_write_lines E D M L B A >expect &&\n> +       test_cmp expect actual\n> +'\n"},{"id":"529881","messageId":"dd147777-b7d2-4271-b0ab-6d580b8f2768@gmail.com","threadId":"64109","inReplyTo":"CAP8UFD03fx+wKwJzDG8UZz=+S8=07hG6npNnebTmBxrcXNYqGQ@mail.gmail.com","subject":"Re: [PATCH v5 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-29T17:00:21Z","receivedAt":"2025-10-29T17:00:30Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 29/10/25 21:49, Christian Couder wrote:\n> On Tue, Oct 28, 2025 at 10:46 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>\n>> +static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)\n\n\nHi Christian,\n\n\n> Nit: it's a bit strange that it's called \"ref_action_str\" everywhere\n> except here where it's called \"mode_str\". I'd prefer \"ref_action\"\n> everywhere.\n\n\nYou are right - that's inconsistent naming. Looking at similar patterns \nin the\ncodebase like `parse_sign_mode()` in gpg-interface.c, the parameter is just\ncalled `arg`, but for clarity I should stick with `ref_action` throughout.\n\nThe inconsistency came from trying to distinguish the string parameter from\nthe function name, but it just makes the code harder to follow. I will \nrename\nboth `mode_str` parameters to `ref_action` in the helper functions.\n\n\n>\n> (I understand that \"mode\" is related to parse_ref_action_mode() having\n> \"mode\" in its name but it's the case for get_ref_action_mode() too.)\n>\n>> +test_expect_success 'replay.refAction config option' '\n>> +       # Store original state\n>> +       START=$(git rev-parse topic2) &&\n>> +       test_when_finished \"git branch -f topic2 $START\" &&\n>> +       test_when_finished \"git config --unset replay.refAction || true\" &&\n> Is there something preventing test_config to be used in this test\n> while it's used in other tests below?\n\n\nNothing preventing it - I was being overly cautious because this test sets\nconfig twice in sequence, but `test_config` handles that fine. Looking at\nthe test-lib-functions.sh implementation, `test_config` uses \n`test_when_finished`\nwith `test_unconfig` which properly handles multiple config operations.\n\nThe manual approach is actually more fragile since it relies on the `|| \ntrue`\npattern and doesn't guarantee cleanup if the test fails early. I will \nswitch\nto `test_config` for consistency with the other config tests.\n\nBoth fixes are straightforward - I will send them in v6.\n\nThanks for the careful review and keeping the code quality high!\n\n- Siddharth\n\n\n>\n>> +       # Set config to print\n>> +       git config replay.refAction print &&\n>> +       git replay --onto main topic1..topic2 >output &&\n>> +       test_line_count = 1 output &&\n>> +       test_grep \"^update refs/heads/topic2 \" output &&\n>> +\n>> +       # Reset and test update mode\n>> +       git branch -f topic2 $START &&\n>> +       git config replay.refAction update &&\n>> +       git replay --onto main topic1..topic2 >output &&\n>> +       test_must_be_empty output &&\n>> +\n>> +       # Verify ref was updated\n>> +       git log --format=%s topic2 >actual &&\n>> +       test_write_lines E D M L B A >expect &&\n>> +       test_cmp expect actual\n>> +'\n"},{"id":"529979","messageId":"20251030191931.30837-1-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251028214609.10041-1-siddharthasthana31@gmail.com","subject":"[PATCH v6 0/3] replay: make atomic ref updates the default","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-30T19:19:28Z","receivedAt":"2025-10-30T19:19:44Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"This is v6 of the git-replay atomic updates series.\n\nThis version addresses Christian's feedback from v5 regarding code\nconsistency and test patterns. Thanks to Christian, Junio, Phillip,\nElijah, Patrick, and Karthik for the thorough reviews.\n\n## Changes in v6\n\n**Fixed parameter naming inconsistency**\n\nChristian pointed out that parse_ref_action_mode() used `mode_str` as the\nparameter name while the rest of the code used `ref_action`. Changed to\nuse `ref_action` consistently throughout for better code readability.\n\n**Improved test cleanup pattern**\n\nReplaced manual `git config --unset` with `test_when_finished` pattern with\n`test_config` helper in the replay.refAction config test. The test_config\nhelper automatically handles cleanup via test_when_finished, providing\nbetter test isolation and following Git test suite best practices.\n\nThese are code quality improvements that don't change functionality but\nmake the code more consistent with Git's established patterns.\n\n## Technical Implementation\n\nSame as v5, using Git's ref transaction API:\n\n- ref_store_transaction_begin() with default atomic behavior\n- ref_transaction_update() to stage each update\n- ref_transaction_commit() for atomic application\n\nThe helper functions provide clean separation:\n\n- parse_ref_action_mode(): Validates strings and converts to enum\n- get_ref_action_mode(): Implements command-line > config > default precedence\n- handle_ref_update(): Uses type-safe enum with switch statement\n\nConfig reading uses repo_config_get_string_tmp() for simplicity while\nmaintaining proper precedence behavior.\n\n## Testing\n\nAll tests pass:\n\n- t3650-replay-basics.sh (20 tests pass)\n- Config tests now use test_config for automatic cleanup\n- Atomic behavior tests verify direct ref updates\n- Backward compatibility maintained for pipeline workflow\n\nCI results: https://gitlab.com/gitlab-org/git/-/pipelines/2130504045\n\nSiddharth Asthana (3):\n  replay: use die_for_incompatible_opt2() for option validation\n  replay: make atomic ref updates the default behavior\n  replay: add replay.refAction config option\n\n Documentation/config/replay.adoc |  11 +++\n Documentation/git-replay.adoc    |  65 +++++++++++------\n builtin/replay.c                 | 121 +++++++++++++++++++++++++++----\n t/t3650-replay-basics.sh         |  90 +++++++++++++++++++++--\n 4 files changed, 244 insertions(+), 43 deletions(-)\n create mode 100644 Documentation/config/replay.adoc\n\nRange-diff against v5:\n1:  3e27d07d3b = 1:  1f0fad0cac replay: use die_for_incompatible_opt2() for option validation\n2:  643d9ca86a = 2:  bfc6188234 replay: make atomic ref updates the default behavior\n3:  334da71911 ! 3:  6b2a44c72c replay: add replay.refAction config option\n    @@ Metadata\n     Author: Siddharth Asthana <siddharthasthana31@gmail.com>\n     \n      ## Commit message ##\n         replay: add replay.refAction config option\n     \n         [Commit message unchanged]\n     \n      ## builtin/replay.c ##\n     @@ builtin/replay.c: static struct commit *pick_regular_commit\n      \treturn create_commit(repo, result->tree, pickme, replayed_base);\n      }\n      \n    -+static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)\n    ++static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n     +{\n    -+\tif (!mode_str || !strcmp(mode_str, \"update\"))\n    ++\tif (!ref_action || !strcmp(ref_action, \"update\"))\n     +\t\treturn REF_ACTION_UPDATE;\n    -+\tif (!strcmp(mode_str, \"print\"))\n    ++\tif (!strcmp(ref_action, \"print\"))\n     +\t\treturn REF_ACTION_PRINT;\n    -+\tdie(_(\"invalid %s value: '%s'\"), source, mode_str);\n    ++\tdie(_(\"invalid %s value: '%s'\"), source, ref_action);\n     +}\n     +\n     +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)\n     \n      ## t/t3650-replay-basics.sh ##\n     @@ t/t3650-replay-basics.sh\n     +test_expect_success 'replay.refAction config option' '\n     +\tSTART=$(git rev-parse topic2) &&\n     +\ttest_when_finished \"git branch -f topic2 $START\" &&\n    -+\ttest_when_finished \"git config --unset replay.refAction || true\" &&\n     +\n    -+\tgit config replay.refAction print &&\n    ++\ttest_config replay.refAction print &&\n     +\tgit replay --onto main topic1..topic2 >output &&\n     +\ttest_line_count = 1 output &&\n     +\ttest_grep \"^update refs/heads/topic2 \" output &&\n     +\n     +\tgit branch -f topic2 $START &&\n    -+\tgit config replay.refAction update &&\n    ++\ttest_config replay.refAction update &&\n     +\tgit replay --onto main topic1..topic2 >output &&\n     \n     +test_expect_success 'command-line --ref-action overrides config' '\n-- \n2.51.0\n\nbase-commit: 57da342c78d8bf00259d2b720292e5b3035dadcc\n\nThanks\n- Siddharth\n"},{"id":"529980","messageId":"20251030191931.30837-2-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251030191931.30837-1-siddharthasthana31@gmail.com","subject":"[PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-30T19:19:29Z","receivedAt":"2025-10-30T19:19:53Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"In preparation for adding the --ref-action option, convert option\nvalidation to use die_for_incompatible_opt2(). This helper provides\nstandardized error messages for mutually exclusive options.\n\nThe following commit introduces --ref-action which will be incompatible\nwith certain other options. Using die_for_incompatible_opt2() now means\nthat commit can cleanly add its validation using the same pattern,\nkeeping the validation logic consistent and maintainable.\n\nThis also aligns git-replay's option handling with how other Git commands\nmanage option conflicts, using the established die_for_incompatible_opt*()\nhelper family.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n builtin/replay.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 6172c8aacc..b64fc72063 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -330,9 +330,9 @@ int cmd_replay(int argc,\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n-\tif (advance_name_opt && contained)\n-\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n-\t\t    \"--advance\", \"--contained\");\n+\tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n+\t\t\t\t  contained, \"--contained\");\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n-- \n2.51.0\n\n"},{"id":"529981","messageId":"20251030191931.30837-3-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251030191931.30837-1-siddharthasthana31@gmail.com","subject":"[PATCH v6 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-30T19:19:30Z","receivedAt":"2025-10-30T19:20:01Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"The git replay command currently outputs update commands that can be\npiped to update-ref to achieve a rebase, e.g.\n\n  git replay --onto main topic1..topic2 | git update-ref --stdin\n\nThis separation had advantages for three special cases:\n  * it made testing easy (when state isn't modified from one step to\n    the next, you don't need to make temporary branches or have undo\n    commands, or try to track the changes)\n  * it provided a natural can-it-rebase-cleanly (and what would it\n    rebase to) capability without automatically updating refs, similar\n    to a --dry-run\n  * it provided a natural low-level tool for the suite of hash-object,\n    mktree, commit-tree, mktag, merge-tree, and update-ref, allowing\n    users to have another building block for experimentation and making\n    new tools\n\nHowever, it should be noted that all three of these are somewhat\nspecial cases; users, whether on the client or server side, would\nalmost certainly find it more ergonomic to simply have the updating\nof refs be the default.\n\nFor server-side operations in particular, the pipeline architecture\ncreates process coordination overhead. Server implementations that need\nto perform rebases atomically must maintain additional code to:\n\n  1. Spawn and manage a pipeline between git-replay and git-update-ref\n  2. Coordinate stdout/stderr streams across the pipe boundary\n  3. Handle partial failure states if the pipeline breaks mid-execution\n  4. Parse and validate the update-ref command output\n\nChange the default behavior to update refs directly, and atomically (at\nleast to the extent supported by the refs backend in use). This\neliminates the process coordination overhead for the common case.\n\nFor users needing the traditional pipeline workflow, add a new\n--ref-action=<mode> option that preserves the original behavior:\n\n  git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin\n\nThe mode can be:\n  * update (default): Update refs directly using an atomic transaction\n  * print: Output update-ref commands for pipeline use\n\nImplementation details:\n\nThe atomic ref updates are implemented using Git's ref transaction API.\nIn cmd_replay(), when not in `print` mode, we initialize a transaction\nusing ref_store_transaction_begin() with the default atomic behavior.\nAs commits are replayed, ref updates are staged into the transaction\nusing ref_transaction_update(). Finally, ref_transaction_commit()\napplies all updates atomically—either all updates succeed or none do.\n\nTo avoid code duplication between the 'print' and 'update' modes, this\ncommit extracts a handle_ref_update() helper function. This function\ntakes the mode (as an enum) and either prints the update command or\nstages it into the transaction. Using an enum rather than passing the\nstring around provides type safety and allows the compiler to catch\ntypos. The switch statement makes it easy to add future modes.\n\nThe helper function signature:\n\n  static int handle_ref_update(enum ref_action_mode mode,\n                                struct ref_transaction *transaction,\n                                const char *refname,\n                                const struct object_id *new_oid,\n                                const struct object_id *old_oid,\n                                struct strbuf *err)\n\nThe enum is defined as:\n\n  enum ref_action_mode {\n      REF_ACTION_UPDATE,\n      REF_ACTION_PRINT\n  };\n\nThe mode string is converted to enum immediately after parse_options()\nto avoid string comparisons throughout the codebase and provide compiler\nprotection against typos.\n\nTest suite changes:\n\nAll existing tests that expected command output now use\n--ref-action=print to preserve their original behavior. This keeps\nthe tests valid while allowing them to verify that the pipeline workflow\nstill works correctly.\n\nNew tests were added to verify:\n  - Default atomic behavior (no output, refs updated directly)\n  - Bare repository support (server-side use case)\n  - Equivalence between traditional pipeline and atomic updates\n  - Real atomicity using a lock file to verify all-or-nothing guarantee\n  - Test isolation using test_when_finished to clean up state\n\nThe bare repository tests were fixed to rebuild their expectations\nindependently rather than comparing to previous test output, improving\ntest reliability and isolation.\n\nA following commit will add a replay.refAction configuration\noption for users who prefer the traditional pipeline output as their\ndefault behavior.\n\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc | 65 +++++++++++++++--------\n builtin/replay.c              | 98 +++++++++++++++++++++++++++++++----\n t/t3650-replay-basics.sh      | 44 +++++++++++++---\n 3 files changed, 167 insertions(+), 40 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 0b12bf8aa4..037b093196 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n \n DESCRIPTION\n -----------\n \n Takes ranges of commits and replays them onto a new location. Leaves\n-the working tree and the index untouched, and updates no references.\n-The output of this command is meant to be used as input to\n-`git update-ref --stdin`, which would update the relevant branches\n+the working tree and the index untouched. By default, updates the\n+relevant references using an atomic transaction (all refs update or\n+none). Use `--ref-action=print` to avoid automatic ref updates and\n+instead get update commands that can be piped to `git update-ref --stdin`\n (see the OUTPUT section below).\n \n THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n@@ -29,18 +30,31 @@ OPTIONS\n \tStarting point at which to create the new commits.  May be any\n \tvalid commit, and not just an existing branch name.\n +\n-When `--onto` is specified, the update-ref command(s) in the output will\n-update the branch(es) in the revision range to point at the new\n-commits, similar to the way how `git rebase --update-refs` updates\n-multiple branches in the affected range.\n+When `--onto` is specified, the branch(es) in the revision range will be\n+updated to point at the new commits (or update commands will be printed\n+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`\n+updates multiple branches in the affected range.\n \n --advance <branch>::\n \tStarting point at which to create the new commits; must be a\n \tbranch name.\n +\n-When `--advance` is specified, the update-ref command(s) in the output\n-will update the branch passed as an argument to `--advance` to point at\n-the new commits (in other words, this mimics a cherry-pick operation).\n+The history is replayed on top of the <branch> and <branch> is updated to\n+point at the tip of the resulting history (or an update command will be\n+printed if `--ref-action=print` is used). This is different from `--onto`,\n+which uses the target only as a starting point without updating it.\n+\n+--ref-action[=<mode>]::\n+\tControl how references are updated. The mode can be:\n++\n+--\n+\t* `update` (default): Update refs directly using an atomic transaction.\n+\t  All refs are updated or none are (all-or-nothing behavior).\n+\t* `print`: Output update-ref commands for pipeline use. This is the\n+\t  traditional behavior where output can be piped to `git update-ref --stdin`.\n+--\n++\n+The default mode can be configured via the `replay.refAction` configuration variable.\n \n <revision-range>::\n \tRange of commits to replay. More than one <revision-range> can\n@@ -54,8 +68,11 @@ include::rev-list-options.adoc[]\n OUTPUT\n ------\n \n-When there are no conflicts, the output of this command is usable as\n-input to `git update-ref --stdin`.  It is of the form:\n+By default, or with `--ref-action=update`, this command produces no output on\n+success, as refs are updated directly using an atomic transaction.\n+\n+When using `--ref-action=print`, the output is usable as input to\n+`git update-ref --stdin`. It is of the form:\n \n \tupdate refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n \tupdate refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n@@ -81,6 +98,14 @@ To simply rebase `mybranch` onto `target`:\n \n ------------\n $ git replay --onto target origin/main..mybranch\n+------------\n+\n+The refs are updated atomically and no output is produced on success.\n+\n+To see what would be updated without actually updating:\n+\n+------------\n+$ git replay --ref-action=print --onto target origin/main..mybranch\n update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n ------------\n \n@@ -88,33 +113,29 @@ To cherry-pick the commits from mybranch onto target:\n \n ------------\n $ git replay --advance target origin/main..mybranch\n-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n ------------\n \n Note that the first two examples replay the exact same commits and on\n top of the exact same new base, they only differ in that the first\n-provides instructions to make mybranch point at the new commits and\n-the second provides instructions to make target point at them.\n+updates mybranch to point at the new commits and the second updates\n+target to point at them.\n \n What if you have a stack of branches, one depending upon another, and\n you'd really like to rebase the whole set?\n \n ------------\n $ git replay --contained --onto origin/main origin/main..tipbranch\n-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n ------------\n \n+All three branches (`branch1`, `branch2`, and `tipbranch`) are updated\n+atomically.\n+\n When calling `git replay`, one does not need to specify a range of\n commits to replay using the syntax `A..B`; any range expression will\n do:\n \n ------------\n $ git replay --onto origin/main ^base branch1 branch2 branch3\n-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n ------------\n \n This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex b64fc72063..0564d4d2e7 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -20,6 +20,11 @@\n #include <oidset.h>\n #include <tree.h>\n \n+enum ref_action_mode {\n+\tREF_ACTION_UPDATE,\n+\tREF_ACTION_PRINT,\n+};\n+\n static const char *short_commit_name(struct repository *repo,\n \t\t\t\t     struct commit *commit)\n {\n@@ -284,6 +289,28 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \treturn create_commit(repo, result->tree, pickme, replayed_base);\n }\n \n+static int handle_ref_update(enum ref_action_mode mode,\n+\t\t\t     struct ref_transaction *transaction,\n+\t\t\t     const char *refname,\n+\t\t\t     const struct object_id *new_oid,\n+\t\t\t     const struct object_id *old_oid,\n+\t\t\t     struct strbuf *err)\n+{\n+\tswitch (mode) {\n+\tcase REF_ACTION_PRINT:\n+\t\tprintf(\"update %s %s %s\\n\",\n+\t\t       refname,\n+\t\t       oid_to_hex(new_oid),\n+\t\t       oid_to_hex(old_oid));\n+\t\treturn 0;\n+\tcase REF_ACTION_UPDATE:\n+\t\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n+\t\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n+\tdefault:\n+\t\tBUG(\"unknown ref_action_mode %d\", mode);\n+\t}\n+}\n+\n int cmd_replay(int argc,\n \t       const char **argv,\n \t       const char *prefix,\n@@ -294,6 +321,8 @@ int cmd_replay(int argc,\n \tstruct commit *onto = NULL;\n \tconst char *onto_name = NULL;\n \tint contained = 0;\n+\tconst char *ref_action_str = NULL;\n+\tenum ref_action_mode ref_action = REF_ACTION_UPDATE;\n \n \tstruct rev_info revs;\n \tstruct commit *last_commit = NULL;\n@@ -302,12 +331,14 @@ int cmd_replay(int argc,\n \tstruct merge_result result;\n \tstruct strset *update_refs = NULL;\n \tkh_oid_map_t *replayed_commits;\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strbuf transaction_err = STRBUF_INIT;\n \tint ret = 0;\n \n-\tconst char * const replay_usage[] = {\n+\tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n-\t\t   \"<revision-range>...\"),\n+\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -319,6 +350,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &contained,\n \t\t\t N_(\"advance all branches contained in revision-range\")),\n+\t\tOPT_STRING(0, \"ref-action\", &ref_action_str,\n+\t\t\t   N_(\"mode\"),\n+\t\t\t   N_(\"control ref update behavior (update|print)\")),\n \t\tOPT_END()\n \t};\n \n@@ -333,6 +367,18 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n+\t/* Default to update mode if not specified */\n+\tif (!ref_action_str)\n+\t\tref_action_str = \"update\";\n+\n+\t/* Parse ref action mode */\n+\tif (!strcmp(ref_action_str, \"update\"))\n+\t\tref_action = REF_ACTION_UPDATE;\n+\telse if (!strcmp(ref_action_str, \"print\"))\n+\t\tref_action = REF_ACTION_PRINT;\n+\telse\n+\t\tdie(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n@@ -389,6 +435,17 @@ int cmd_replay(int argc,\n \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n \t\t\t      &onto, &update_refs);\n \n+\t/* Initialize ref transaction if using update mode */\n+\tif (ref_action == REF_ACTION_UPDATE) {\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n+\t\t\t\t\t\t\t  0, &transaction_err);\n+\t\tif (!transaction) {\n+\t\t\tret = error(_(\"failed to begin ref transaction: %s\"),\n+\t\t\t\t    transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n \tif (!onto) /* FIXME: Should handle replaying down to root commit */\n \t\tdie(\"Replaying down to root commit is not supported yet!\");\n \n@@ -434,10 +491,15 @@ int cmd_replay(int argc,\n \t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n \t\t\t    (contained || strset_contains(update_refs,\n \t\t\t\t\t\t\t  decoration->name))) {\n-\t\t\t\tprintf(\"update %s %s %s\\n\",\n-\t\t\t\t       decoration->name,\n-\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t\t\t       oid_to_hex(&commit->object.oid));\n+\t\t\t\tif (handle_ref_update(ref_action, transaction,\n+\t\t\t\t\t\t      decoration->name,\n+\t\t\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t\t\t      &commit->object.oid,\n+\t\t\t\t\t\t      &transaction_err) < 0) {\n+\t\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n+\t\t\t\t\tgoto cleanup;\n+\t\t\t\t}\n \t\t\t}\n \t\t\tdecoration = decoration->next;\n \t\t}\n@@ -445,10 +507,23 @@ int cmd_replay(int argc,\n \n \t/* In --advance mode, advance the target ref */\n \tif (result.clean == 1 && advance_name) {\n-\t\tprintf(\"update %s %s %s\\n\",\n-\t\t       advance_name,\n-\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t       oid_to_hex(&onto->object.oid));\n+\t\tif (handle_ref_update(ref_action, transaction, advance_name,\n+\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t      &onto->object.oid,\n+\t\t\t\t      &transaction_err) < 0) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    advance_name, transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n+\t/* Commit the ref transaction if we have one */\n+\tif (transaction && result.clean == 1) {\n+\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n+\t\t\tret = error(_(\"failed to commit ref transaction: %s\"),\n+\t\t\t\t    transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n \t}\n \n \tmerge_finalize(&merge_opt, &result);\n@@ -460,6 +535,9 @@ int cmd_replay(int argc,\n \tret = result.clean;\n \n cleanup:\n+\tif (transaction)\n+\t\tref_transaction_free(transaction);\n+\tstrbuf_release(&transaction_err);\n \trelease_revisions(&revs);\n \tfree(advance_name);\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 58b3759935..123734b49f 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n '\n \n test_expect_success 'using replay to rebase two branches, one on top of other' '\n-\tgit replay --onto main topic1..topic2 >result &&\n+\tgit replay --ref-action=print --onto main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n '\n \n test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n-\tgit -C bare replay --onto main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n \t# 4th field of result is hash for main instead of hash for topic2\n \n-\tgit replay --advance main topic1..topic2 >result &&\n+\tgit replay --ref-action=print --advance main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n '\n \n test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n-\tgit -C bare replay --advance main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n '\n \n test_expect_success 'using replay to also rebase a contained branch' '\n-\tgit replay --contained --onto main main..topic3 >result &&\n+\tgit replay --ref-action=print --contained --onto main main..topic3 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '\n '\n \n test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n-\tgit -C bare replay --contained --onto main main..topic3 >result-bare &&\n+\tgit -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n test_expect_success 'using replay to rebase multiple divergent branches' '\n-\tgit replay --onto main ^topic1 topic2 topic4 >result &&\n+\tgit replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n '\n \n test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n-\tgit -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n+\tgit -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n \n \ttest_line_count = 4 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -217,4 +217,32 @@ test_expect_success 'merge.directoryRenames=false' '\n \t\t--onto rename-onto rename-onto..rename-from\n '\n \n+test_expect_success 'default atomic behavior updates refs directly' '\n+\t# Store original state for cleanup\n+\ttest_when_finished \"git branch -f topic2 topic1\" &&\n+\n+\t# Test default atomic behavior (no output, refs updated)\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'atomic behavior in bare repository' '\n+\t# Test atomic updates work in bare repo\n+\tgit -C bare replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated in bare repo\n+\tgit -C bare log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual &&\n+\n+\t# Reset for other tests\n+\tgit -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"529982","messageId":"20251030191931.30837-4-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251030191931.30837-1-siddharthasthana31@gmail.com","subject":"[PATCH v6 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-10-30T19:19:31Z","receivedAt":"2025-10-30T19:20:10Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"Add a configuration option to control the default behavior of git replay\nfor updating references. This allows users who prefer the traditional\npipeline output to set it once in their config instead of passing\n--ref-action=print with every command.\n\nThe config option uses string values that mirror the behavior modes:\n  * replay.refAction = update (default): atomic ref updates\n  * replay.refAction = print: output commands for pipeline\n\nThe command-line --ref-action option always overrides the config setting,\nallowing users to temporarily change behavior for a single invocation.\n\nImplementation details:\n\nIn cmd_replay(), after parsing command-line options, we check if\n--ref-action was provided. If not, we read the configuration using\nrepo_config_get_string_tmp(). If the config variable is set, we validate\nthe value and use it to set the ref_action_str:\n\n  Config value      Internal mode    Behavior\n  ──────────────────────────────────────────────────────────────\n  \"update\"          \"update\"         Atomic ref updates (default)\n  \"print\"           \"print\"          Pipeline output\n  (not set)         \"update\"         Atomic ref updates (default)\n  (invalid)         error            Die with helpful message\n\nIf an invalid value is provided, we die() immediately with an error\nmessage explaining the valid options. This catches configuration errors\nearly and provides clear guidance to users.\n\nThe command-line --ref-action option, when provided, overrides the\nconfig value. This precedence allows users to set their preferred default\nwhile still having per-invocation control:\n\n  git config replay.refAction print         # Set default\n  git replay --ref-action=update --onto main topic  # Override once\n\nThe config and command-line option use the same value names ('update'\nand 'print') for consistency and clarity. This makes it immediately\nobvious how the config maps to the command-line option, addressing\nfeedback about the relationship between configuration and command-line\noptions being clear to users.\n\nExamples:\n\n$ git config --global replay.refAction print\n$ git replay --onto main topic1..topic2 | git update-ref --stdin\n\n$ git replay --ref-action=update --onto main topic1..topic2\n\n$ git config replay.refAction update\n$ git replay --onto main topic1..topic2  # Updates refs directly\n\nThe implementation follows Git's standard configuration precedence:\ncommand-line options override config values, which matches user\nexpectations across all Git commands.\n\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/config/replay.adoc | 11 ++++++++\n builtin/replay.c                 | 39 ++++++++++++++++++--------\n t/t3650-replay-basics.sh         | 48 +++++++++++++++++++++++++++++++-\n 3 files changed, 86 insertions(+), 12 deletions(-)\n create mode 100644 Documentation/config/replay.adoc\n\ndiff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc\nnew file mode 100644\nindex 0000000000..7d549d2f0e\n--- /dev/null\n+++ b/Documentation/config/replay.adoc\n@@ -0,0 +1,11 @@\n+replay.refAction::\n+\tSpecifies the default mode for handling reference updates in\n+\t`git replay`. The value can be:\n++\n+--\n+\t* `update`: Update refs directly using an atomic transaction (default behavior).\n+\t* `print`: Output update-ref commands for pipeline use.\n+--\n++\n+This setting can be overridden with the `--ref-action` command-line option.\n+When not configured, `git replay` defaults to `update` mode.\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 0564d4d2e7..810068f8ef 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -8,6 +8,7 @@\n #include \"git-compat-util.h\"\n \n #include \"builtin.h\"\n+#include \"config.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"lockfile.h\"\n@@ -289,6 +290,31 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \treturn create_commit(repo, result->tree, pickme, replayed_base);\n }\n \n+static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n+{\n+\tif (!ref_action || !strcmp(ref_action, \"update\"))\n+\t\treturn REF_ACTION_UPDATE;\n+\tif (!strcmp(ref_action, \"print\"))\n+\t\treturn REF_ACTION_PRINT;\n+\tdie(_(\"invalid %s value: '%s'\"), source, ref_action);\n+}\n+\n+static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)\n+{\n+\tconst char *config_value = NULL;\n+\n+\t/* Command line option takes precedence */\n+\tif (ref_action_str)\n+\t\treturn parse_ref_action_mode(ref_action_str, \"--ref-action\");\n+\n+\t/* Check config value */\n+\tif (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n+\t\treturn parse_ref_action_mode(config_value, \"replay.refAction\");\n+\n+\t/* Default to update mode */\n+\treturn REF_ACTION_UPDATE;\n+}\n+\n static int handle_ref_update(enum ref_action_mode mode,\n \t\t\t     struct ref_transaction *transaction,\n \t\t\t     const char *refname,\n@@ -367,17 +393,8 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n-\t/* Default to update mode if not specified */\n-\tif (!ref_action_str)\n-\t\tref_action_str = \"update\";\n-\n-\t/* Parse ref action mode */\n-\tif (!strcmp(ref_action_str, \"update\"))\n-\t\tref_action = REF_ACTION_UPDATE;\n-\telse if (!strcmp(ref_action_str, \"print\"))\n-\t\tref_action = REF_ACTION_PRINT;\n-\telse\n-\t\tdie(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n+\t/* Parse ref action mode from command line or config */\n+\tref_action = get_ref_action_mode(repo, ref_action_str);\n \n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 123734b49f..2e90227c2f 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -219,7 +219,8 @@ test_expect_success 'merge.directoryRenames=false' '\n \n test_expect_success 'default atomic behavior updates refs directly' '\n \t# Store original state for cleanup\n-\ttest_when_finished \"git branch -f topic2 topic1\" &&\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n \n \t# Test default atomic behavior (no output, refs updated)\n \tgit replay --onto main topic1..topic2 >output &&\n@@ -232,6 +233,10 @@ test_expect_success 'default atomic behavior updates refs directly' '\n '\n \n test_expect_success 'atomic behavior in bare repository' '\n+\t# Store original state for cleanup\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n+\n \t# Test atomic updates work in bare repo\n \tgit -C bare replay --onto main topic1..topic2 >output &&\n \ttest_must_be_empty output &&\n@@ -245,4 +250,45 @@ test_expect_success 'atomic behavior in bare repository' '\n \tgit -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n '\n \n+test_expect_success 'replay.refAction config option' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n+\n+\t# Set config to print\n+\ttest_config replay.refAction print &&\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_line_count = 1 output &&\n+\ttest_grep \"^update refs/heads/topic2 \" output &&\n+\n+\t# Reset and test update mode\n+\tgit branch -f topic2 $START &&\n+\ttest_config replay.refAction update &&\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'command-line --ref-action overrides config' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n+\n+\t# Set config to update but use --ref-action=print\n+\ttest_config replay.refAction update &&\n+\tgit replay --ref-action=print --onto main topic1..topic2 >output &&\n+\ttest_line_count = 1 output &&\n+\ttest_grep \"^update refs/heads/topic2 \" output\n+'\n+\n+test_expect_success 'invalid replay.refAction value' '\n+\ttest_config replay.refAction invalid &&\n+\ttest_must_fail git replay --onto main topic1..topic2 2>error &&\n+\ttest_grep \"invalid.*replay.refAction.*value\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"530018","messageId":"CAP8UFD2xJVtQMEFBQAZJP+kYq5iYCcQYn9WD_x+SO8grauPrZg@mail.gmail.com","threadId":"64109","inReplyTo":"20251030191931.30837-4-siddharthasthana31@gmail.com","subject":"Re: [PATCH v6 3/3] replay: add replay.refAction config option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-10-31T07:08:23Z","receivedAt":"2025-10-31T07:08:37Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Oct 30, 2025 at 8:20 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n\n> +static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n> +{\n> +       if (!ref_action || !strcmp(ref_action, \"update\"))\n> +               return REF_ACTION_UPDATE;\n> +       if (!strcmp(ref_action, \"print\"))\n> +               return REF_ACTION_PRINT;\n> +       die(_(\"invalid %s value: '%s'\"), source, ref_action);\n> +}\n> +\n> +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)\n\nI think it could be \"ref_action\" (instead of \"ref_action_str\" ) in\nthis function too.\n\n> +{\n> +       const char *config_value = NULL;\n> +\n> +       /* Command line option takes precedence */\n> +       if (ref_action_str)\n> +               return parse_ref_action_mode(ref_action_str, \"--ref-action\");\n> +\n> +       /* Check config value */\n> +       if (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n> +               return parse_ref_action_mode(config_value, \"replay.refAction\");\n> +\n> +       /* Default to update mode */\n> +       return REF_ACTION_UPDATE;\n> +}\n> +\n>  static int handle_ref_update(enum ref_action_mode mode,\n>                              struct ref_transaction *transaction,\n>                              const char *refname,\n> @@ -367,17 +393,8 @@ int cmd_replay(int argc,\n>         die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>                                   contained, \"--contained\");\n>\n> -       /* Default to update mode if not specified */\n> -       if (!ref_action_str)\n> -               ref_action_str = \"update\";\n> -\n> -       /* Parse ref action mode */\n> -       if (!strcmp(ref_action_str, \"update\"))\n> -               ref_action = REF_ACTION_UPDATE;\n> -       else if (!strcmp(ref_action_str, \"print\"))\n> -               ref_action = REF_ACTION_PRINT;\n> -       else\n> -               die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n\nMaybe parse_ref_action_mode() could have been introduced in the\nprevious commit already?\n\n> +       /* Parse ref action mode from command line or config */\n> +       ref_action = get_ref_action_mode(repo, ref_action_str);\n\nHere it could be:\n\n      ref_mode = get_ref_action_mode(repo, ref_action);\n\nThanks!\n"},{"id":"530044","messageId":"CABPp-BHyUFpFEK1YXSYQWEXSAa2fnUTsH9nsf=LgPs=GNQG2RQ@mail.gmail.com","threadId":"64109","inReplyTo":"20251030191931.30837-2-siddharthasthana31@gmail.com","subject":"Re: [PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-10-31T18:47:12Z","receivedAt":"2025-10-31T18:47:23Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Oct 30, 2025 at 12:19 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> In preparation for adding the --ref-action option, convert option\n> validation to use die_for_incompatible_opt2(). This helper provides\n> standardized error messages for mutually exclusive options.\n>\n> The following commit introduces --ref-action which will be incompatible\n> with certain other options. Using die_for_incompatible_opt2() now means\n> that commit can cleanly add its validation using the same pattern,\n> keeping the validation logic consistent and maintainable.\n>\n> This also aligns git-replay's option handling with how other Git commands\n> manage option conflicts, using the established die_for_incompatible_opt*()\n> helper family.\n>\n> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> ---\n>  builtin/replay.c | 6 +++---\n>  1 file changed, 3 insertions(+), 3 deletions(-)\n>\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index 6172c8aacc..b64fc72063 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -330,9 +330,9 @@ int cmd_replay(int argc,\n>                 usage_with_options(replay_usage, replay_options);\n>         }\n>\n> -       if (advance_name_opt && contained)\n> -               die(_(\"options '%s' and '%s' cannot be used together\"),\n> -                   \"--advance\", \"--contained\");\n> +       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n> +                                 contained, \"--contained\");\n> +\n>         advance_name = xstrdup_or_null(advance_name_opt);\n>\n>         repo_init_revisions(repo, &revs, prefix);\n> --\n> 2.51.0\n\nThanks for splitting this one out; looks good.\n"},{"id":"530045","messageId":"CABPp-BGmHegyqvN48vJO1Y9gWVDk5u2SO5_i9KMw2aoAtmNuyw@mail.gmail.com","threadId":"64109","inReplyTo":"20251030191931.30837-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-10-31T18:49:31Z","receivedAt":"2025-10-31T18:49:43Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Oct 30, 2025 at 12:20 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> The git replay command currently outputs update commands that can be\n> piped to update-ref to achieve a rebase, e.g.\n>\n>   git replay --onto main topic1..topic2 | git update-ref --stdin\n>\n> This separation had advantages for three special cases:\n>   * it made testing easy (when state isn't modified from one step to\n>     the next, you don't need to make temporary branches or have undo\n>     commands, or try to track the changes)\n>   * it provided a natural can-it-rebase-cleanly (and what would it\n>     rebase to) capability without automatically updating refs, similar\n>     to a --dry-run\n>   * it provided a natural low-level tool for the suite of hash-object,\n>     mktree, commit-tree, mktag, merge-tree, and update-ref, allowing\n>     users to have another building block for experimentation and making\n>     new tools\n>\n> However, it should be noted that all three of these are somewhat\n> special cases; users, whether on the client or server side, would\n> almost certainly find it more ergonomic to simply have the updating\n> of refs be the default.\n>\n> For server-side operations in particular, the pipeline architecture\n> creates process coordination overhead. Server implementations that need\n> to perform rebases atomically must maintain additional code to:\n>\n>   1. Spawn and manage a pipeline between git-replay and git-update-ref\n>   2. Coordinate stdout/stderr streams across the pipe boundary\n>   3. Handle partial failure states if the pipeline breaks mid-execution\n>   4. Parse and validate the update-ref command output\n>\n> Change the default behavior to update refs directly, and atomically (at\n> least to the extent supported by the refs backend in use). This\n> eliminates the process coordination overhead for the common case.\n>\n> For users needing the traditional pipeline workflow, add a new\n> --ref-action=<mode> option that preserves the original behavior:\n>\n>   git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin\n>\n> The mode can be:\n>   * update (default): Update refs directly using an atomic transaction\n>   * print: Output update-ref commands for pipeline use\n\nLooks good up to here.\n\n> Implementation details:\n>\n> The atomic ref updates are implemented using Git's ref transaction API.\n> In cmd_replay(), when not in `print` mode, we initialize a transaction\n> using ref_store_transaction_begin() with the default atomic behavior.\n> As commits are replayed, ref updates are staged into the transaction\n> using ref_transaction_update(). Finally, ref_transaction_commit()\n> applies all updates atomically—either all updates succeed or none do.\n>\n> To avoid code duplication between the 'print' and 'update' modes, this\n> commit extracts a handle_ref_update() helper function. This function\n> takes the mode (as an enum) and either prints the update command or\n> stages it into the transaction. Using an enum rather than passing the\n> string around provides type safety and allows the compiler to catch\n> typos. The switch statement makes it easy to add future modes.\n>\n> The helper function signature:\n>\n>   static int handle_ref_update(enum ref_action_mode mode,\n>                                 struct ref_transaction *transaction,\n>                                 const char *refname,\n>                                 const struct object_id *new_oid,\n>                                 const struct object_id *old_oid,\n>                                 struct strbuf *err)\n>\n> The enum is defined as:\n>\n>   enum ref_action_mode {\n>       REF_ACTION_UPDATE,\n>       REF_ACTION_PRINT\n>   };\n>\n> The mode string is converted to enum immediately after parse_options()\n> to avoid string comparisons throughout the codebase and provide compiler\n> protection against typos.\n\nI'm not sure the implementation details section above makes sense to\ninclude in the commit message; it feels like it's not providing much\nhigh level information nor much \"why\" information, but just presenting\nan alternative view of the information people will find in the patch.\nPerhaps leave it out?\n\n> Test suite changes:\n>\n> All existing tests that expected command output now use\n> --ref-action=print to preserve their original behavior. This keeps\n> the tests valid while allowing them to verify that the pipeline workflow\n> still works correctly.\n>\n> New tests were added to verify:\n>   - Default atomic behavior (no output, refs updated directly)\n>   - Bare repository support (server-side use case)\n>   - Equivalence between traditional pipeline and atomic updates\n>   - Real atomicity using a lock file to verify all-or-nothing guarantee\n>   - Test isolation using test_when_finished to clean up state\n>\n> The bare repository tests were fixed to rebuild their expectations\n> independently rather than comparing to previous test output, improving\n> test reliability and isolation.\n\nThe above paragraph sounds like you are comparing to an earlier\nseries, which will confuse future readers who only compare to code\nthat existed before your patches.\n\n> A following commit will add a replay.refAction configuration\n> option for users who prefer the traditional pipeline output as their\n> default behavior.\n>\n> Helped-by: Elijah Newren <newren@gmail.com>\n> Helped-by: Patrick Steinhardt <ps@pks.im>\n> Helped-by: Christian Couder <christian.couder@gmail.com>\n> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> ---\n>  Documentation/git-replay.adoc | 65 +++++++++++++++--------\n>  builtin/replay.c              | 98 +++++++++++++++++++++++++++++++----\n>  t/t3650-replay-basics.sh      | 44 +++++++++++++---\n>  3 files changed, 167 insertions(+), 40 deletions(-)\n>\n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index 0b12bf8aa4..037b093196 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n>\n>  DESCRIPTION\n>  -----------\n>\n>  Takes ranges of commits and replays them onto a new location. Leaves\n> -the working tree and the index untouched, and updates no references.\n> -The output of this command is meant to be used as input to\n> -`git update-ref --stdin`, which would update the relevant branches\n> +the working tree and the index untouched. By default, updates the\n> +relevant references using an atomic transaction (all refs update or\n> +none). Use `--ref-action=print` to avoid automatic ref updates and\n> +instead get update commands that can be piped to `git update-ref --stdin`\n>  (see the OUTPUT section below).\n>\n>  THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n> @@ -29,18 +30,31 @@ OPTIONS\n>         Starting point at which to create the new commits.  May be any\n>         valid commit, and not just an existing branch name.\n>  +\n> -When `--onto` is specified, the update-ref command(s) in the output will\n> -update the branch(es) in the revision range to point at the new\n> -commits, similar to the way how `git rebase --update-refs` updates\n> -multiple branches in the affected range.\n> +When `--onto` is specified, the branch(es) in the revision range will be\n> +updated to point at the new commits (or update commands will be printed\n> +if `--ref-action=print` is used), similar to the way `git rebase --update-refs`\n> +updates multiple branches in the affected range.\n\nI'm not sure if the parenthetical comment is necessary; we tend not to\ntry to document every combinatorial combination with every sentence.\nFor example, in the `git rebase` manpage under the description of the\n`--strategy` flag, it says \"Because git rebase replays each commit\nfrom the working branch on top of the <upstream> branch using the\ngiven strategy\", which is technically incorrect if either the --onto\nor --keep-base flags are specified, but belaboring all the details at\nthat location would just burden the reader and the explanations of\n--onto and --keep-base are sufficient for users to understand.  I\nthink we tend to just describe the option in combination with the\ndefault, and only mention other options if the combination is\nambiguous or confusing.  I don't think users would find anything\nambiguous or confusing about how --ref-action=print would combine with\nthese options, so I don't think it's necessary to make the description\nlonger.\n\n>  --advance <branch>::\n>         Starting point at which to create the new commits; must be a\n>         branch name.\n>  +\n> -When `--advance` is specified, the update-ref command(s) in the output\n> -will update the branch passed as an argument to `--advance` to point at\n> -the new commits (in other words, this mimics a cherry-pick operation).\n> +The history is replayed on top of the <branch> and <branch> is updated to\n> +point at the tip of the resulting history (or an update command will be\n> +printed if `--ref-action=print` is used). This is different from `--onto`,\n> +which uses the target only as a starting point without updating it.\n\nSame comment as above about this parenthetical comment as well.\n\n> +\n> +--ref-action[=<mode>]::\n> +       Control how references are updated. The mode can be:\n> ++\n> +--\n> +       * `update` (default): Update refs directly using an atomic transaction.\n> +         All refs are updated or none are (all-or-nothing behavior).\n> +       * `print`: Output update-ref commands for pipeline use. This is the\n> +         traditional behavior where output can be piped to `git update-ref --stdin`.\n> +--\n> ++\n> +The default mode can be configured via the `replay.refAction` configuration variable.\n\nThis last sentence conflicts with the commit message; if the\nconfiguration option isn't added until a later commit, then this last\nsentence shouldn't be added until then either.\n\n>  <revision-range>::\n>         Range of commits to replay. More than one <revision-range> can\n> @@ -54,8 +68,11 @@ include::rev-list-options.adoc[]\n>  OUTPUT\n>  ------\n>\n> -When there are no conflicts, the output of this command is usable as\n> -input to `git update-ref --stdin`.  It is of the form:\n> +By default, or with `--ref-action=update`, this command produces no output on\n> +success, as refs are updated directly using an atomic transaction.\n> +\n> +When using `--ref-action=print`, the output is usable as input to\n> +`git update-ref --stdin`. It is of the form:\n>\n>         update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n>         update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n> @@ -81,6 +98,14 @@ To simply rebase `mybranch` onto `target`:\n>\n>  ------------\n>  $ git replay --onto target origin/main..mybranch\n> +------------\n> +\n> +The refs are updated atomically and no output is produced on success.\n> +\n> +To see what would be updated without actually updating:\n> +\n> +------------\n> +$ git replay --ref-action=print --onto target origin/main..mybranch\n>  update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n>  ------------\n>\n> @@ -88,33 +113,29 @@ To cherry-pick the commits from mybranch onto target:\n>\n>  ------------\n>  $ git replay --advance target origin/main..mybranch\n> -update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n>  ------------\n>\n>  Note that the first two examples replay the exact same commits and on\n>  top of the exact same new base, they only differ in that the first\n> -provides instructions to make mybranch point at the new commits and\n> -the second provides instructions to make target point at them.\n> +updates mybranch to point at the new commits and the second updates\n> +target to point at them.\n>\n>  What if you have a stack of branches, one depending upon another, and\n>  you'd really like to rebase the whole set?\n>\n>  ------------\n>  $ git replay --contained --onto origin/main origin/main..tipbranch\n> -update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n> -update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n> -update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n>  ------------\n>\n> +All three branches (`branch1`, `branch2`, and `tipbranch`) are updated\n> +atomically.\n> +\n>  When calling `git replay`, one does not need to specify a range of\n>  commits to replay using the syntax `A..B`; any range expression will\n>  do:\n>\n>  ------------\n>  $ git replay --onto origin/main ^base branch1 branch2 branch3\n> -update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n> -update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n> -update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n>  ------------\n>\n>  This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index b64fc72063..0564d4d2e7 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -20,6 +20,11 @@\n>  #include <oidset.h>\n>  #include <tree.h>\n>\n> +enum ref_action_mode {\n> +       REF_ACTION_UPDATE,\n> +       REF_ACTION_PRINT,\n> +};\n> +\n>  static const char *short_commit_name(struct repository *repo,\n>                                      struct commit *commit)\n>  {\n> @@ -284,6 +289,28 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>         return create_commit(repo, result->tree, pickme, replayed_base);\n>  }\n>\n> +static int handle_ref_update(enum ref_action_mode mode,\n> +                            struct ref_transaction *transaction,\n> +                            const char *refname,\n> +                            const struct object_id *new_oid,\n> +                            const struct object_id *old_oid,\n> +                            struct strbuf *err)\n> +{\n> +       switch (mode) {\n> +       case REF_ACTION_PRINT:\n> +               printf(\"update %s %s %s\\n\",\n> +                      refname,\n> +                      oid_to_hex(new_oid),\n> +                      oid_to_hex(old_oid));\n> +               return 0;\n> +       case REF_ACTION_UPDATE:\n> +               return ref_transaction_update(transaction, refname, new_oid, old_oid,\n> +                                             NULL, NULL, 0, \"git replay\", err);\n> +       default:\n> +               BUG(\"unknown ref_action_mode %d\", mode);\n> +       }\n> +}\n> +\n>  int cmd_replay(int argc,\n>                const char **argv,\n>                const char *prefix,\n> @@ -294,6 +321,8 @@ int cmd_replay(int argc,\n>         struct commit *onto = NULL;\n>         const char *onto_name = NULL;\n>         int contained = 0;\n> +       const char *ref_action_str = NULL;\n> +       enum ref_action_mode ref_action = REF_ACTION_UPDATE;\n>\n>         struct rev_info revs;\n>         struct commit *last_commit = NULL;\n> @@ -302,12 +331,14 @@ int cmd_replay(int argc,\n>         struct merge_result result;\n>         struct strset *update_refs = NULL;\n>         kh_oid_map_t *replayed_commits;\n> +       struct ref_transaction *transaction = NULL;\n> +       struct strbuf transaction_err = STRBUF_INIT;\n>         int ret = 0;\n>\n> -       const char * const replay_usage[] = {\n> +       const char *const replay_usage[] = {\n>                 N_(\"(EXPERIMENTAL!) git replay \"\n>                    \"([--contained] --onto <newbase> | --advance <branch>) \"\n> -                  \"<revision-range>...\"),\n> +                  \"[--ref-action[=<mode>]] <revision-range>...\"),\n>                 NULL\n>         };\n>         struct option replay_options[] = {\n> @@ -319,6 +350,9 @@ int cmd_replay(int argc,\n>                            N_(\"replay onto given commit\")),\n>                 OPT_BOOL(0, \"contained\", &contained,\n>                          N_(\"advance all branches contained in revision-range\")),\n> +               OPT_STRING(0, \"ref-action\", &ref_action_str,\n> +                          N_(\"mode\"),\n> +                          N_(\"control ref update behavior (update|print)\")),\n>                 OPT_END()\n>         };\n>\n> @@ -333,6 +367,18 @@ int cmd_replay(int argc,\n>         die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>                                   contained, \"--contained\");\n>\n> +       /* Default to update mode if not specified */\n> +       if (!ref_action_str)\n> +               ref_action_str = \"update\";\n> +\n> +       /* Parse ref action mode */\n> +       if (!strcmp(ref_action_str, \"update\"))\n> +               ref_action = REF_ACTION_UPDATE;\n> +       else if (!strcmp(ref_action_str, \"print\"))\n> +               ref_action = REF_ACTION_PRINT;\n> +       else\n> +               die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n> +\n>         advance_name = xstrdup_or_null(advance_name_opt);\n>\n>         repo_init_revisions(repo, &revs, prefix);\n> @@ -389,6 +435,17 @@ int cmd_replay(int argc,\n>         determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n>                               &onto, &update_refs);\n>\n> +       /* Initialize ref transaction if using update mode */\n> +       if (ref_action == REF_ACTION_UPDATE) {\n> +               transaction = ref_store_transaction_begin(get_main_ref_store(repo),\n> +                                                         0, &transaction_err);\n> +               if (!transaction) {\n> +                       ret = error(_(\"failed to begin ref transaction: %s\"),\n> +                                   transaction_err.buf);\n> +                       goto cleanup;\n> +               }\n> +       }\n> +\n>         if (!onto) /* FIXME: Should handle replaying down to root commit */\n>                 die(\"Replaying down to root commit is not supported yet!\");\n>\n> @@ -434,10 +491,15 @@ int cmd_replay(int argc,\n>                         if (decoration->type == DECORATION_REF_LOCAL &&\n>                             (contained || strset_contains(update_refs,\n>                                                           decoration->name))) {\n> -                               printf(\"update %s %s %s\\n\",\n> -                                      decoration->name,\n> -                                      oid_to_hex(&last_commit->object.oid),\n> -                                      oid_to_hex(&commit->object.oid));\n> +                               if (handle_ref_update(ref_action, transaction,\n> +                                                     decoration->name,\n> +                                                     &last_commit->object.oid,\n> +                                                     &commit->object.oid,\n> +                                                     &transaction_err) < 0) {\n> +                                       ret = error(_(\"failed to update ref '%s': %s\"),\n> +                                                   decoration->name, transaction_err.buf);\n> +                                       goto cleanup;\n> +                               }\n>                         }\n>                         decoration = decoration->next;\n>                 }\n> @@ -445,10 +507,23 @@ int cmd_replay(int argc,\n>\n>         /* In --advance mode, advance the target ref */\n>         if (result.clean == 1 && advance_name) {\n> -               printf(\"update %s %s %s\\n\",\n> -                      advance_name,\n> -                      oid_to_hex(&last_commit->object.oid),\n> -                      oid_to_hex(&onto->object.oid));\n> +               if (handle_ref_update(ref_action, transaction, advance_name,\n> +                                     &last_commit->object.oid,\n> +                                     &onto->object.oid,\n> +                                     &transaction_err) < 0) {\n> +                       ret = error(_(\"failed to update ref '%s': %s\"),\n> +                                   advance_name, transaction_err.buf);\n> +                       goto cleanup;\n> +               }\n> +       }\n> +\n> +       /* Commit the ref transaction if we have one */\n> +       if (transaction && result.clean == 1) {\n> +               if (ref_transaction_commit(transaction, &transaction_err)) {\n> +                       ret = error(_(\"failed to commit ref transaction: %s\"),\n> +                                   transaction_err.buf);\n> +                       goto cleanup;\n> +               }\n>         }\n>\n>         merge_finalize(&merge_opt, &result);\n> @@ -460,6 +535,9 @@ int cmd_replay(int argc,\n>         ret = result.clean;\n>\n>  cleanup:\n> +       if (transaction)\n> +               ref_transaction_free(transaction);\n> +       strbuf_release(&transaction_err);\n>         release_revisions(&revs);\n>         free(advance_name);\n>\n> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n> index 58b3759935..123734b49f 100755\n> --- a/t/t3650-replay-basics.sh\n> +++ b/t/t3650-replay-basics.sh\n> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n>  '\n>\n>  test_expect_success 'using replay to rebase two branches, one on top of other' '\n> -       git replay --onto main topic1..topic2 >result &&\n> +       git replay --ref-action=print --onto main topic1..topic2 >result &&\n>\n>         test_line_count = 1 result &&\n>\n> @@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n>  '\n>\n>  test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n> -       git -C bare replay --onto main topic1..topic2 >result-bare &&\n> +       git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&\n>         test_cmp expect result-bare\n>  '\n>\n> @@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n>         # 2nd field of result is refs/heads/main vs. refs/heads/topic2\n>         # 4th field of result is hash for main instead of hash for topic2\n>\n> -       git replay --advance main topic1..topic2 >result &&\n> +       git replay --ref-action=print --advance main topic1..topic2 >result &&\n>\n>         test_line_count = 1 result &&\n>\n> @@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n>  '\n>\n>  test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n> -       git -C bare replay --advance main topic1..topic2 >result-bare &&\n> +       git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&\n>         test_cmp expect result-bare\n>  '\n>\n> @@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n>  '\n>\n>  test_expect_success 'using replay to also rebase a contained branch' '\n> -       git replay --contained --onto main main..topic3 >result &&\n> +       git replay --ref-action=print --contained --onto main main..topic3 >result &&\n>\n>         test_line_count = 2 result &&\n>         cut -f 3 -d \" \" result >new-branch-tips &&\n> @@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '\n>  '\n>\n>  test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n> -       git -C bare replay --contained --onto main main..topic3 >result-bare &&\n> +       git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&\n>         test_cmp expect result-bare\n>  '\n>\n>  test_expect_success 'using replay to rebase multiple divergent branches' '\n> -       git replay --onto main ^topic1 topic2 topic4 >result &&\n> +       git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&\n>\n>         test_line_count = 2 result &&\n>         cut -f 3 -d \" \" result >new-branch-tips &&\n> @@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n>  '\n>\n>  test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n> -       git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n> +       git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n>\n>         test_line_count = 4 result &&\n>         cut -f 3 -d \" \" result >new-branch-tips &&\n> @@ -217,4 +217,32 @@ test_expect_success 'merge.directoryRenames=false' '\n>                 --onto rename-onto rename-onto..rename-from\n>  '\n>\n> +test_expect_success 'default atomic behavior updates refs directly' '\n> +       # Store original state for cleanup\n> +       test_when_finished \"git branch -f topic2 topic1\" &&\n\nWhy are you resetting topic2 back to topic1?  Shouldn't it be set back\nto what it was before the test ran instead, e.g.\n    START=$(git rev-parse topic2) &&\n    test_when_finished \"git branch -f topic2 $START\" &&\n?\n\n> +\n> +       # Test default atomic behavior (no output, refs updated)\n> +       git replay --onto main topic1..topic2 >output &&\n> +       test_must_be_empty output &&\n> +\n> +       # Verify ref was updated\n> +       git log --format=%s topic2 >actual &&\n> +       test_write_lines E D M L B A >expect &&\n> +       test_cmp expect actual\n> +'\n> +\n> +test_expect_success 'atomic behavior in bare repository' '\n> +       # Test atomic updates work in bare repo\n> +       git -C bare replay --onto main topic1..topic2 >output &&\n> +       test_must_be_empty output &&\n> +\n> +       # Verify ref was updated in bare repo\n> +       git -C bare log --format=%s topic2 >actual &&\n> +       test_write_lines E D M L B A >expect &&\n> +       test_cmp expect actual &&\n> +\n> +       # Reset for other tests\n> +       git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n\nThis reset happens too late to help if the earlier commands fail, and\nalso resets to the wrong ref.  You should instead use a\ntest_when_finished block, and make sure to reset to what topic2 used\nto point to, not reset it to what topic1 points to.\n\n\nOtherwise, the patch looks good.  This is really close to being ready\nto merge; just a few minor fixups needed that I highlighted above.\n"},{"id":"530046","messageId":"CABPp-BE_pAQ8f-jjv16Ts-KRTEr3Qc402qRuJKFFW6G3J9shtA@mail.gmail.com","threadId":"64109","inReplyTo":"20251030191931.30837-4-siddharthasthana31@gmail.com","subject":"Re: [PATCH v6 3/3] replay: add replay.refAction config option","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-10-31T18:49:46Z","receivedAt":"2025-10-31T18:49:59Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Oct 30, 2025 at 12:20 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> Add a configuration option to control the default behavior of git replay\n> for updating references. This allows users who prefer the traditional\n> pipeline output to set it once in their config instead of passing\n> --ref-action=print with every command.\n>\n> The config option uses string values that mirror the behavior modes:\n>   * replay.refAction = update (default): atomic ref updates\n>   * replay.refAction = print: output commands for pipeline\n>\n> The command-line --ref-action option always overrides the config setting,\n> allowing users to temporarily change behavior for a single invocation.\n\nThe above paragraph merely states that we follow git practices with\nthis config options and its corresponding command line; I think we'd\nneed to call it out if we didn't do that, but calling out that we do\nfollow git conventions seems unnecessary.\n\n> Implementation details:\n>\n> In cmd_replay(), after parsing command-line options, we check if\n> --ref-action was provided. If not, we read the configuration using\n> repo_config_get_string_tmp(). If the config variable is set, we validate\n> the value and use it to set the ref_action_str:\n>\n>   Config value      Internal mode    Behavior\n>   ──────────────────────────────────────────────────────────────\n>   \"update\"          \"update\"         Atomic ref updates (default)\n>   \"print\"           \"print\"          Pipeline output\n>   (not set)         \"update\"         Atomic ref updates (default)\n>   (invalid)         error            Die with helpful message\n>\n> If an invalid value is provided, we die() immediately with an error\n> message explaining the valid options. This catches configuration errors\n> early and provides clear guidance to users.\n>\n> The command-line --ref-action option, when provided, overrides the\n> config value. This precedence allows users to set their preferred default\n> while still having per-invocation control:\n>\n>   git config replay.refAction print         # Set default\n>   git replay --ref-action=update --onto main topic  # Override once\n>\n> The config and command-line option use the same value names ('update'\n> and 'print') for consistency and clarity. This makes it immediately\n> obvious how the config maps to the command-line option, addressing\n> feedback about the relationship between configuration and command-line\n> options being clear to users.\n\nAn implementation details section may make sense if it answers a\n\"why?\" question, or it explains something counter-intuitive, or it\nprovides high enough level details that it makes the patch easier to\nread/follow, or it otherwise does something more than just repackage\nthe patch in an alternate format.  I appreciate the attempt to provide\nthese, but I think they simply make the commit message longer without\nadding value.\n\n> Examples:\n>\n> $ git config --global replay.refAction print\n> $ git replay --onto main topic1..topic2 | git update-ref --stdin\n>\n> $ git replay --ref-action=update --onto main topic1..topic2\n>\n> $ git config replay.refAction update\n> $ git replay --onto main topic1..topic2  # Updates refs directly\n>\n> The implementation follows Git's standard configuration precedence:\n> command-line options override config values, which matches user\n> expectations across all Git commands.\n\nI don't find the Examples section helpful either; it's yet another\nre-iteration that we're following conventions.\n\n> Helped-by: Junio C Hamano <gitster@pobox.com>\n> Helped-by: Elijah Newren <newren@gmail.com>\n> Helped-by: Christian Couder <christian.couder@gmail.com>\n> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> ---\n>  Documentation/config/replay.adoc | 11 ++++++++\n>  builtin/replay.c                 | 39 ++++++++++++++++++--------\n>  t/t3650-replay-basics.sh         | 48 +++++++++++++++++++++++++++++++-\n>  3 files changed, 86 insertions(+), 12 deletions(-)\n>  create mode 100644 Documentation/config/replay.adoc\n>\n> diff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc\n> new file mode 100644\n> index 0000000000..7d549d2f0e\n> --- /dev/null\n> +++ b/Documentation/config/replay.adoc\n> @@ -0,0 +1,11 @@\n> +replay.refAction::\n> +       Specifies the default mode for handling reference updates in\n> +       `git replay`. The value can be:\n> ++\n> +--\n> +       * `update`: Update refs directly using an atomic transaction (default behavior).\n> +       * `print`: Output update-ref commands for pipeline use.\n> +--\n> ++\n> +This setting can be overridden with the `--ref-action` command-line option.\n> +When not configured, `git replay` defaults to `update` mode.\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index 0564d4d2e7..810068f8ef 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -8,6 +8,7 @@\n>  #include \"git-compat-util.h\"\n>\n>  #include \"builtin.h\"\n> +#include \"config.h\"\n>  #include \"environment.h\"\n>  #include \"hex.h\"\n>  #include \"lockfile.h\"\n> @@ -289,6 +290,31 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>         return create_commit(repo, result->tree, pickme, replayed_base);\n>  }\n>\n> +static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n> +{\n> +       if (!ref_action || !strcmp(ref_action, \"update\"))\n> +               return REF_ACTION_UPDATE;\n> +       if (!strcmp(ref_action, \"print\"))\n> +               return REF_ACTION_PRINT;\n> +       die(_(\"invalid %s value: '%s'\"), source, ref_action);\n> +}\n> +\n> +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)\n> +{\n> +       const char *config_value = NULL;\n> +\n> +       /* Command line option takes precedence */\n> +       if (ref_action_str)\n> +               return parse_ref_action_mode(ref_action_str, \"--ref-action\");\n> +\n> +       /* Check config value */\n> +       if (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n> +               return parse_ref_action_mode(config_value, \"replay.refAction\");\n> +\n> +       /* Default to update mode */\n> +       return REF_ACTION_UPDATE;\n> +}\n> +\n>  static int handle_ref_update(enum ref_action_mode mode,\n>                              struct ref_transaction *transaction,\n>                              const char *refname,\n> @@ -367,17 +393,8 @@ int cmd_replay(int argc,\n>         die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>                                   contained, \"--contained\");\n>\n> -       /* Default to update mode if not specified */\n> -       if (!ref_action_str)\n> -               ref_action_str = \"update\";\n> -\n> -       /* Parse ref action mode */\n> -       if (!strcmp(ref_action_str, \"update\"))\n> -               ref_action = REF_ACTION_UPDATE;\n> -       else if (!strcmp(ref_action_str, \"print\"))\n> -               ref_action = REF_ACTION_PRINT;\n> -       else\n> -               die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n> +       /* Parse ref action mode from command line or config */\n> +       ref_action = get_ref_action_mode(repo, ref_action_str);\n>\n>         advance_name = xstrdup_or_null(advance_name_opt);\n>\n> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n> index 123734b49f..2e90227c2f 100755\n> --- a/t/t3650-replay-basics.sh\n> +++ b/t/t3650-replay-basics.sh\n> @@ -219,7 +219,8 @@ test_expect_success 'merge.directoryRenames=false' '\n>\n>  test_expect_success 'default atomic behavior updates refs directly' '\n>         # Store original state for cleanup\n> -       test_when_finished \"git branch -f topic2 topic1\" &&\n> +       START=$(git rev-parse topic2) &&\n> +       test_when_finished \"git branch -f topic2 $START\" &&\n\nYes, these three lines are a good fix, but they belong in the previous patch.\n\n>\n>         # Test default atomic behavior (no output, refs updated)\n>         git replay --onto main topic1..topic2 >output &&\n> @@ -232,6 +233,10 @@ test_expect_success 'default atomic behavior updates refs directly' '\n>  '\n>\n>  test_expect_success 'atomic behavior in bare repository' '\n> +       # Store original state for cleanup\n> +       START=$(git rev-parse topic2) &&\n> +       test_when_finished \"git branch -f topic2 $START\" &&\n\nYes, these three lines are good but they belong in a separate patch.\n> +\n>         # Test atomic updates work in bare repo\n>         git -C bare replay --onto main topic1..topic2 >output &&\n>         test_must_be_empty output &&\n> @@ -245,4 +250,45 @@ test_expect_success 'atomic behavior in bare repository' '\n>         git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n\nAnd this line should be removed in the previous patch.\n\n>  '\n>\n> +test_expect_success 'replay.refAction config option' '\n> +       # Store original state\n> +       START=$(git rev-parse topic2) &&\n> +       test_when_finished \"git branch -f topic2 $START\" &&\n> +\n> +       # Set config to print\n> +       test_config replay.refAction print &&\n> +       git replay --onto main topic1..topic2 >output &&\n> +       test_line_count = 1 output &&\n> +       test_grep \"^update refs/heads/topic2 \" output &&\n> +\n> +       # Reset and test update mode\n> +       git branch -f topic2 $START &&\n> +       test_config replay.refAction update &&\n> +       git replay --onto main topic1..topic2 >output &&\n> +       test_must_be_empty output &&\n> +\n> +       # Verify ref was updated\n> +       git log --format=%s topic2 >actual &&\n> +       test_write_lines E D M L B A >expect &&\n> +       test_cmp expect actual\n> +'\n> +\n> +test_expect_success 'command-line --ref-action overrides config' '\n> +       # Store original state\n> +       START=$(git rev-parse topic2) &&\n> +       test_when_finished \"git branch -f topic2 $START\" &&\n> +\n> +       # Set config to update but use --ref-action=print\n> +       test_config replay.refAction update &&\n> +       git replay --ref-action=print --onto main topic1..topic2 >output &&\n> +       test_line_count = 1 output &&\n> +       test_grep \"^update refs/heads/topic2 \" output\n> +'\n> +\n> +test_expect_success 'invalid replay.refAction value' '\n> +       test_config replay.refAction invalid &&\n> +       test_must_fail git replay --onto main topic1..topic2 2>error &&\n> +       test_grep \"invalid.*replay.refAction.*value\" error\n> +'\n> +\n>  test_done\n> --\n> 2.51.0\n\nLooks good otherwise.\n"},{"id":"530047","messageId":"CABPp-BE9wyPpmPoufVPAK94hjvZ5UMFe5CV0RhKY8pyPCxL3MQ@mail.gmail.com","threadId":"64109","inReplyTo":"20251030191931.30837-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH v6 0/3] replay: make atomic ref updates the default","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-10-31T18:51:21Z","receivedAt":"2025-10-31T18:51:33Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Oct 30, 2025 at 12:19 PM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> This is v6 of the git-replay atomic updates series.\n>\n> This version addresses Christian's feedback from v5 regarding code\n> consistency and test patterns. Thanks to Christian, Junio, Phillip,\n> Elijah, Patrick, and Karthik for the thorough reviews.\n>\n> ## Changes in v6\n>\n> **Fixed parameter naming inconsistency**\n>\n> Christian pointed out that parse_ref_action_mode() used `mode_str` as the\n> parameter name while the rest of the code used `ref_action`. Changed to\n> use `ref_action` consistently throughout for better code readability.\n>\n> **Improved test cleanup pattern**\n>\n> Replaced manual `git config --unset` with `test_when_finished` pattern with\n> `test_config` helper in the replay.refAction config test. The test_config\n> helper automatically handles cleanup via test_when_finished, providing\n> better test isolation and following Git test suite best practices.\n>\n> These are code quality improvements that don't change functionality but\n> make the code more consistent with Git's established patterns.\n>\n> ## Technical Implementation\n>\n> Same as v5, using Git's ref transaction API:\n>\n> - ref_store_transaction_begin() with default atomic behavior\n> - ref_transaction_update() to stage each update\n> - ref_transaction_commit() for atomic application\n>\n> The helper functions provide clean separation:\n>\n> - parse_ref_action_mode(): Validates strings and converts to enum\n> - get_ref_action_mode(): Implements command-line > config > default precedence\n> - handle_ref_update(): Uses type-safe enum with switch statement\n>\n> Config reading uses repo_config_get_string_tmp() for simplicity while\n> maintaining proper precedence behavior.\n>\n> ## Testing\n>\n> All tests pass:\n>\n> - t3650-replay-basics.sh (20 tests pass)\n> - Config tests now use test_config for automatic cleanup\n> - Atomic behavior tests verify direct ref updates\n> - Backward compatibility maintained for pipeline workflow\n>\n> CI results: https://gitlab.com/gitlab-org/git/-/pipelines/2130504045\n>\n> Siddharth Asthana (3):\n>   replay: use die_for_incompatible_opt2() for option validation\n>   replay: make atomic ref updates the default behavior\n>   replay: add replay.refAction config option\n>\n>  Documentation/config/replay.adoc |  11 +++\n>  Documentation/git-replay.adoc    |  65 +++++++++++------\n>  builtin/replay.c                 | 121 +++++++++++++++++++++++++++----\n>  t/t3650-replay-basics.sh         |  90 +++++++++++++++++++++--\n>  4 files changed, 244 insertions(+), 43 deletions(-)\n>  create mode 100644 Documentation/config/replay.adoc\n>\n> Range-diff against v5:\n> 1:  3e27d07d3b = 1:  1f0fad0cac replay: use die_for_incompatible_opt2() for option validation\n> 2:  643d9ca86a = 2:  bfc6188234 replay: make atomic ref updates the default behavior\n> 3:  334da71911 ! 3:  6b2a44c72c replay: add replay.refAction config option\n>     @@ Metadata\n>      Author: Siddharth Asthana <siddharthasthana31@gmail.com>\n>\n>       ## Commit message ##\n>          replay: add replay.refAction config option\n>\n>          [Commit message unchanged]\n>\n>       ## builtin/replay.c ##\n>      @@ builtin/replay.c: static struct commit *pick_regular_commit\n>         return create_commit(repo, result->tree, pickme, replayed_base);\n>       }\n>\n>     -+static enum ref_action_mode parse_ref_action_mode(const char *mode_str, const char *source)\n>     ++static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n>      +{\n>     -+  if (!mode_str || !strcmp(mode_str, \"update\"))\n>     ++  if (!ref_action || !strcmp(ref_action, \"update\"))\n>      +          return REF_ACTION_UPDATE;\n>     -+  if (!strcmp(mode_str, \"print\"))\n>     ++  if (!strcmp(ref_action, \"print\"))\n>      +          return REF_ACTION_PRINT;\n>     -+  die(_(\"invalid %s value: '%s'\"), source, mode_str);\n>     ++  die(_(\"invalid %s value: '%s'\"), source, ref_action);\n>      +}\n>      +\n>      +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)\n>\n>       ## t/t3650-replay-basics.sh ##\n>      @@ t/t3650-replay-basics.sh\n>      +test_expect_success 'replay.refAction config option' '\n>      +  START=$(git rev-parse topic2) &&\n>      +  test_when_finished \"git branch -f topic2 $START\" &&\n>     -+  test_when_finished \"git config --unset replay.refAction || true\" &&\n>      +\n>     -+  git config replay.refAction print &&\n>     ++  test_config replay.refAction print &&\n>      +  git replay --onto main topic1..topic2 >output &&\n>      +  test_line_count = 1 output &&\n>      +  test_grep \"^update refs/heads/topic2 \" output &&\n>      +\n>      +  git branch -f topic2 $START &&\n>     -+  git config replay.refAction update &&\n>     ++  test_config replay.refAction update &&\n>      +  git replay --onto main topic1..topic2 >output &&\n>\n>      +test_expect_success 'command-line --ref-action overrides config' '\n> --\n> 2.51.0\n>\n> base-commit: 57da342c78d8bf00259d2b720292e5b3035dadcc\n\nThis series is getting into shape nicely.  There are a few things that\nneed to be fixed up on patches 2 & 3 that I called out (including what\nlooks like some fixes that were accidentally squashed into the wrong\npatch), but those should be pretty easy and then the series will be\nready to merge down.  Thanks for working on this!\n"},{"id":"530050","messageId":"xmqqldkq266b.fsf@gitster.g","threadId":"64109","inReplyTo":"CABPp-BGmHegyqvN48vJO1Y9gWVDk5u2SO5_i9KMw2aoAtmNuyw@mail.gmail.com","subject":"Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-31T19:59:56Z","receivedAt":"2025-10-31T20:00:01Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> I'm not sure the implementation details section above makes sense to\n> include in the commit message; it feels like it's not providing much\n> high level information nor much \"why\" information, but just presenting\n> an alternative view of the information people will find in the patch.\n> Perhaps leave it out?\n\nSounds like a good thing to do.\n\n>> Test suite changes:\n>>\n>> All existing tests that expected command output now use\n>> --ref-action=print to preserve their original behavior. This keeps\n>> the tests valid while allowing them to verify that the pipeline workflow\n>> still works correctly.\n>>\n>> New tests were added to verify:\n>>   - Default atomic behavior (no output, refs updated directly)\n>>   - Bare repository support (server-side use case)\n>>   - Equivalence between traditional pipeline and atomic updates\n>>   - Real atomicity using a lock file to verify all-or-nothing guarantee\n>>   - Test isolation using test_when_finished to clean up state\n>>\n>> The bare repository tests were fixed to rebuild their expectations\n>> independently rather than comparing to previous test output, improving\n>> test reliability and isolation.\n>\n> The above paragraph sounds like you are comparing to an earlier\n> series, which will confuse future readers who only compare to code\n> that existed before your patches.\n\nYup, such an update relative to previous iterations belongs in the\ncover letter and below the three-dash line.\n\n> Otherwise, the patch looks good.  This is really close to being ready\n> to merge; just a few minor fixups needed that I highlighted above.\n\nYup, I agree with all the comments I saw here.  Thanks for a great\nreview.\n\n\n"},{"id":"530132","messageId":"a7f9c31b-2342-405e-848d-08b9de837dc6@gmail.com","threadId":"64109","inReplyTo":"20251030191931.30837-3-siddharthasthana31@gmail.com","subject":"Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-03T16:25:23Z","receivedAt":"2025-11-03T16:25:27Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Siddharth\n\nOn 30/10/2025 19:19, Siddharth Asthana wrote:\n\n> +\tcase REF_ACTION_UPDATE:\n> +\t\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n> +\t\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n\nI wonder if we should use a more descriptive reflog message here that \nsays what git replay was doing. For example \"git replay --onto \n<new-base> <revs>\" could include the new base in the reflog message like \n\"git rebase\" does. For \"git replay --advance\" we could include the \ncommits that have been picked. It would be helpful to test the reflog \nmessage in the new tests as well.\n\nThanks\n\nPhillip\n\n> +\tdefault:\n> +\t\tBUG(\"unknown ref_action_mode %d\", mode);\n> +\t}\n> +}\n> +\n>   int cmd_replay(int argc,\n>   \t       const char **argv,\n>   \t       const char *prefix,\n> @@ -294,6 +321,8 @@ int cmd_replay(int argc,\n>   \tstruct commit *onto = NULL;\n>   \tconst char *onto_name = NULL;\n>   \tint contained = 0;\n> +\tconst char *ref_action_str = NULL;\n> +\tenum ref_action_mode ref_action = REF_ACTION_UPDATE;\n>   \n>   \tstruct rev_info revs;\n>   \tstruct commit *last_commit = NULL;\n> @@ -302,12 +331,14 @@ int cmd_replay(int argc,\n>   \tstruct merge_result result;\n>   \tstruct strset *update_refs = NULL;\n>   \tkh_oid_map_t *replayed_commits;\n> +\tstruct ref_transaction *transaction = NULL;\n> +\tstruct strbuf transaction_err = STRBUF_INIT;\n>   \tint ret = 0;\n>   \n> -\tconst char * const replay_usage[] = {\n> +\tconst char *const replay_usage[] = {\n>   \t\tN_(\"(EXPERIMENTAL!) git replay \"\n>   \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n> -\t\t   \"<revision-range>...\"),\n> +\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n>   \t\tNULL\n>   \t};\n>   \tstruct option replay_options[] = {\n> @@ -319,6 +350,9 @@ int cmd_replay(int argc,\n>   \t\t\t   N_(\"replay onto given commit\")),\n>   \t\tOPT_BOOL(0, \"contained\", &contained,\n>   \t\t\t N_(\"advance all branches contained in revision-range\")),\n> +\t\tOPT_STRING(0, \"ref-action\", &ref_action_str,\n> +\t\t\t   N_(\"mode\"),\n> +\t\t\t   N_(\"control ref update behavior (update|print)\")),\n>   \t\tOPT_END()\n>   \t};\n>   \n> @@ -333,6 +367,18 @@ int cmd_replay(int argc,\n>   \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>   \t\t\t\t  contained, \"--contained\");\n>   \n> +\t/* Default to update mode if not specified */\n> +\tif (!ref_action_str)\n> +\t\tref_action_str = \"update\";\n> +\n> +\t/* Parse ref action mode */\n> +\tif (!strcmp(ref_action_str, \"update\"))\n> +\t\tref_action = REF_ACTION_UPDATE;\n> +\telse if (!strcmp(ref_action_str, \"print\"))\n> +\t\tref_action = REF_ACTION_PRINT;\n> +\telse\n> +\t\tdie(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n> +\n>   \tadvance_name = xstrdup_or_null(advance_name_opt);\n>   \n>   \trepo_init_revisions(repo, &revs, prefix);\n> @@ -389,6 +435,17 @@ int cmd_replay(int argc,\n>   \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n>   \t\t\t      &onto, &update_refs);\n>   \n> +\t/* Initialize ref transaction if using update mode */\n> +\tif (ref_action == REF_ACTION_UPDATE) {\n> +\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n> +\t\t\t\t\t\t\t  0, &transaction_err);\n> +\t\tif (!transaction) {\n> +\t\t\tret = error(_(\"failed to begin ref transaction: %s\"),\n> +\t\t\t\t    transaction_err.buf);\n> +\t\t\tgoto cleanup;\n> +\t\t}\n> +\t}\n> +\n>   \tif (!onto) /* FIXME: Should handle replaying down to root commit */\n>   \t\tdie(\"Replaying down to root commit is not supported yet!\");\n>   \n> @@ -434,10 +491,15 @@ int cmd_replay(int argc,\n>   \t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n>   \t\t\t    (contained || strset_contains(update_refs,\n>   \t\t\t\t\t\t\t  decoration->name))) {\n> -\t\t\t\tprintf(\"update %s %s %s\\n\",\n> -\t\t\t\t       decoration->name,\n> -\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n> -\t\t\t\t       oid_to_hex(&commit->object.oid));\n> +\t\t\t\tif (handle_ref_update(ref_action, transaction,\n> +\t\t\t\t\t\t      decoration->name,\n> +\t\t\t\t\t\t      &last_commit->object.oid,\n> +\t\t\t\t\t\t      &commit->object.oid,\n> +\t\t\t\t\t\t      &transaction_err) < 0) {\n> +\t\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n> +\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n> +\t\t\t\t\tgoto cleanup;\n> +\t\t\t\t}\n>   \t\t\t}\n>   \t\t\tdecoration = decoration->next;\n>   \t\t}\n> @@ -445,10 +507,23 @@ int cmd_replay(int argc,\n>   \n>   \t/* In --advance mode, advance the target ref */\n>   \tif (result.clean == 1 && advance_name) {\n> -\t\tprintf(\"update %s %s %s\\n\",\n> -\t\t       advance_name,\n> -\t\t       oid_to_hex(&last_commit->object.oid),\n> -\t\t       oid_to_hex(&onto->object.oid));\n> +\t\tif (handle_ref_update(ref_action, transaction, advance_name,\n> +\t\t\t\t      &last_commit->object.oid,\n> +\t\t\t\t      &onto->object.oid,\n> +\t\t\t\t      &transaction_err) < 0) {\n> +\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n> +\t\t\t\t    advance_name, transaction_err.buf);\n> +\t\t\tgoto cleanup;\n> +\t\t}\n> +\t}\n> +\n> +\t/* Commit the ref transaction if we have one */\n> +\tif (transaction && result.clean == 1) {\n> +\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n> +\t\t\tret = error(_(\"failed to commit ref transaction: %s\"),\n> +\t\t\t\t    transaction_err.buf);\n> +\t\t\tgoto cleanup;\n> +\t\t}\n>   \t}\n>   \n>   \tmerge_finalize(&merge_opt, &result);\n> @@ -460,6 +535,9 @@ int cmd_replay(int argc,\n>   \tret = result.clean;\n>   \n>   cleanup:\n> +\tif (transaction)\n> +\t\tref_transaction_free(transaction);\n> +\tstrbuf_release(&transaction_err);\n>   \trelease_revisions(&revs);\n>   \tfree(advance_name);\n>   \n> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n> index 58b3759935..123734b49f 100755\n> --- a/t/t3650-replay-basics.sh\n> +++ b/t/t3650-replay-basics.sh\n> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n>   '\n>   \n>   test_expect_success 'using replay to rebase two branches, one on top of other' '\n> -\tgit replay --onto main topic1..topic2 >result &&\n> +\tgit replay --ref-action=print --onto main topic1..topic2 >result &&\n>   \n>   \ttest_line_count = 1 result &&\n>   \n> @@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n>   '\n>   \n>   test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n> -\tgit -C bare replay --onto main topic1..topic2 >result-bare &&\n> +\tgit -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&\n>   \ttest_cmp expect result-bare\n>   '\n>   \n> @@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n>   \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n>   \t# 4th field of result is hash for main instead of hash for topic2\n>   \n> -\tgit replay --advance main topic1..topic2 >result &&\n> +\tgit replay --ref-action=print --advance main topic1..topic2 >result &&\n>   \n>   \ttest_line_count = 1 result &&\n>   \n> @@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n>   '\n>   \n>   test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n> -\tgit -C bare replay --advance main topic1..topic2 >result-bare &&\n> +\tgit -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&\n>   \ttest_cmp expect result-bare\n>   '\n>   \n> @@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n>   '\n>   \n>   test_expect_success 'using replay to also rebase a contained branch' '\n> -\tgit replay --contained --onto main main..topic3 >result &&\n> +\tgit replay --ref-action=print --contained --onto main main..topic3 >result &&\n>   \n>   \ttest_line_count = 2 result &&\n>   \tcut -f 3 -d \" \" result >new-branch-tips &&\n> @@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '\n>   '\n>   \n>   test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n> -\tgit -C bare replay --contained --onto main main..topic3 >result-bare &&\n> +\tgit -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&\n>   \ttest_cmp expect result-bare\n>   '\n>   \n>   test_expect_success 'using replay to rebase multiple divergent branches' '\n> -\tgit replay --onto main ^topic1 topic2 topic4 >result &&\n> +\tgit replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&\n>   \n>   \ttest_line_count = 2 result &&\n>   \tcut -f 3 -d \" \" result >new-branch-tips &&\n> @@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n>   '\n>   \n>   test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n> -\tgit -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n> +\tgit -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n>   \n>   \ttest_line_count = 4 result &&\n>   \tcut -f 3 -d \" \" result >new-branch-tips &&\n> @@ -217,4 +217,32 @@ test_expect_success 'merge.directoryRenames=false' '\n>   \t\t--onto rename-onto rename-onto..rename-from\n>   '\n>   \n> +test_expect_success 'default atomic behavior updates refs directly' '\n> +\t# Store original state for cleanup\n> +\ttest_when_finished \"git branch -f topic2 topic1\" &&\n> +\n> +\t# Test default atomic behavior (no output, refs updated)\n> +\tgit replay --onto main topic1..topic2 >output &&\n> +\ttest_must_be_empty output &&\n> +\n> +\t# Verify ref was updated\n> +\tgit log --format=%s topic2 >actual &&\n> +\ttest_write_lines E D M L B A >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'atomic behavior in bare repository' '\n> +\t# Test atomic updates work in bare repo\n> +\tgit -C bare replay --onto main topic1..topic2 >output &&\n> +\ttest_must_be_empty output &&\n> +\n> +\t# Verify ref was updated in bare repo\n> +\tgit -C bare log --format=%s topic2 >actual &&\n> +\ttest_write_lines E D M L B A >expect &&\n> +\ttest_cmp expect actual &&\n> +\n> +\t# Reset for other tests\n> +\tgit -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n> +'\n> +\n>   test_done\n\n"},{"id":"530148","messageId":"55e6620a-bf38-4c34-8d52-75d838ba087a@gmail.com","threadId":"64109","inReplyTo":"a7f9c31b-2342-405e-848d-08b9de837dc6@gmail.com","subject":"Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-03T19:32:03Z","receivedAt":"2025-11-03T19:32:11Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 03/11/25 21:55, Phillip Wood wrote:\n> Hi Siddharth\n>\n> On 30/10/2025 19:19, Siddharth Asthana wrote:\n>\n>> +    case REF_ACTION_UPDATE:\n>> +        return ref_transaction_update(transaction, refname, new_oid, \n>> old_oid,\n>> +                          NULL, NULL, 0, \"git replay\", err);\n>\n> I wonder if we should use a more descriptive reflog message here that \n> says what git replay was doing. For example \"git replay --onto \n> <new-base> <revs>\" could include the new base in the reflog message \n> like \"git rebase\" does. For \"git replay --advance\" we could include \n> the commits that have been picked. It would be helpful to test the \n> reflog message in the new tests as well.\n>\n> Thanks\n>\n> Phillip\n\n\nHi Phillip,\n\nThanks for the suggestion! I agree that a more descriptive reflog\nmessage would be helpful for users tracking what happened.\n\nI looked at how `git rebase` constructs its reflog messages (it uses\nsomething like \"rebase (finish): refs/heads/feature onto abc123def\")\nand I'm thinking of using a simpler format for replay:\n\n     \"replay --onto main\"\n     \"replay --advance main\"\n\nThis shows the mode and target in a way that mirrors what the user\ntyped. I chose to use the symbolic name (e.g., \"main\") rather than\nthe commit SHA because it seems more user-friendly, though I notice\ngit rebase uses oid_to_hex().\n\nRegarding \"include the commits that have been picked\" for --advance\nmode - would you prefer:\n   - The revision range as specified by the user (e.g., \"topic1..topic2\")?\n   - Just the target branch like I have above?\n\nThe revision range would provide more context, but it might make the\nreflog message quite long if the user specified something complex. I'm\nhappy to include it if that's what you think would be most useful.\n\nI'll add tests for the reflog messages in both modes.\n\nThanks,\nSiddharth\n\n\n>\n>\n>> +    default:\n>> +        BUG(\"unknown ref_action_mode %d\", mode);\n>> +    }\n>> +}\n>> +\n>>   int cmd_replay(int argc,\n>>              const char **argv,\n>>              const char *prefix,\n>> @@ -294,6 +321,8 @@ int cmd_replay(int argc,\n>>       struct commit *onto = NULL;\n>>       const char *onto_name = NULL;\n>>       int contained = 0;\n>> +    const char *ref_action_str = NULL;\n>> +    enum ref_action_mode ref_action = REF_ACTION_UPDATE;\n>>         struct rev_info revs;\n>>       struct commit *last_commit = NULL;\n>> @@ -302,12 +331,14 @@ int cmd_replay(int argc,\n>>       struct merge_result result;\n>>       struct strset *update_refs = NULL;\n>>       kh_oid_map_t *replayed_commits;\n>> +    struct ref_transaction *transaction = NULL;\n>> +    struct strbuf transaction_err = STRBUF_INIT;\n>>       int ret = 0;\n>>   -    const char * const replay_usage[] = {\n>> +    const char *const replay_usage[] = {\n>>           N_(\"(EXPERIMENTAL!) git replay \"\n>>              \"([--contained] --onto <newbase> | --advance <branch>) \"\n>> -           \"<revision-range>...\"),\n>> +           \"[--ref-action[=<mode>]] <revision-range>...\"),\n>>           NULL\n>>       };\n>>       struct option replay_options[] = {\n>> @@ -319,6 +350,9 @@ int cmd_replay(int argc,\n>>                  N_(\"replay onto given commit\")),\n>>           OPT_BOOL(0, \"contained\", &contained,\n>>                N_(\"advance all branches contained in revision-range\")),\n>> +        OPT_STRING(0, \"ref-action\", &ref_action_str,\n>> +               N_(\"mode\"),\n>> +               N_(\"control ref update behavior (update|print)\")),\n>>           OPT_END()\n>>       };\n>>   @@ -333,6 +367,18 @@ int cmd_replay(int argc,\n>>       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>>                     contained, \"--contained\");\n>>   +    /* Default to update mode if not specified */\n>> +    if (!ref_action_str)\n>> +        ref_action_str = \"update\";\n>> +\n>> +    /* Parse ref action mode */\n>> +    if (!strcmp(ref_action_str, \"update\"))\n>> +        ref_action = REF_ACTION_UPDATE;\n>> +    else if (!strcmp(ref_action_str, \"print\"))\n>> +        ref_action = REF_ACTION_PRINT;\n>> +    else\n>> +        die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n>> +\n>>       advance_name = xstrdup_or_null(advance_name_opt);\n>>         repo_init_revisions(repo, &revs, prefix);\n>> @@ -389,6 +435,17 @@ int cmd_replay(int argc,\n>>       determine_replay_mode(repo, &revs.cmdline, onto_name, \n>> &advance_name,\n>>                     &onto, &update_refs);\n>>   +    /* Initialize ref transaction if using update mode */\n>> +    if (ref_action == REF_ACTION_UPDATE) {\n>> +        transaction = \n>> ref_store_transaction_begin(get_main_ref_store(repo),\n>> +                              0, &transaction_err);\n>> +        if (!transaction) {\n>> +            ret = error(_(\"failed to begin ref transaction: %s\"),\n>> +                    transaction_err.buf);\n>> +            goto cleanup;\n>> +        }\n>> +    }\n>> +\n>>       if (!onto) /* FIXME: Should handle replaying down to root \n>> commit */\n>>           die(\"Replaying down to root commit is not supported yet!\");\n>>   @@ -434,10 +491,15 @@ int cmd_replay(int argc,\n>>               if (decoration->type == DECORATION_REF_LOCAL &&\n>>                   (contained || strset_contains(update_refs,\n>>                                 decoration->name))) {\n>> -                printf(\"update %s %s %s\\n\",\n>> -                       decoration->name,\n>> - oid_to_hex(&last_commit->object.oid),\n>> -                       oid_to_hex(&commit->object.oid));\n>> +                if (handle_ref_update(ref_action, transaction,\n>> +                              decoration->name,\n>> +                              &last_commit->object.oid,\n>> +                              &commit->object.oid,\n>> +                              &transaction_err) < 0) {\n>> +                    ret = error(_(\"failed to update ref '%s': %s\"),\n>> +                            decoration->name, transaction_err.buf);\n>> +                    goto cleanup;\n>> +                }\n>>               }\n>>               decoration = decoration->next;\n>>           }\n>> @@ -445,10 +507,23 @@ int cmd_replay(int argc,\n>>         /* In --advance mode, advance the target ref */\n>>       if (result.clean == 1 && advance_name) {\n>> -        printf(\"update %s %s %s\\n\",\n>> -               advance_name,\n>> -               oid_to_hex(&last_commit->object.oid),\n>> -               oid_to_hex(&onto->object.oid));\n>> +        if (handle_ref_update(ref_action, transaction, advance_name,\n>> +                      &last_commit->object.oid,\n>> +                      &onto->object.oid,\n>> +                      &transaction_err) < 0) {\n>> +            ret = error(_(\"failed to update ref '%s': %s\"),\n>> +                    advance_name, transaction_err.buf);\n>> +            goto cleanup;\n>> +        }\n>> +    }\n>> +\n>> +    /* Commit the ref transaction if we have one */\n>> +    if (transaction && result.clean == 1) {\n>> +        if (ref_transaction_commit(transaction, &transaction_err)) {\n>> +            ret = error(_(\"failed to commit ref transaction: %s\"),\n>> +                    transaction_err.buf);\n>> +            goto cleanup;\n>> +        }\n>>       }\n>>         merge_finalize(&merge_opt, &result);\n>> @@ -460,6 +535,9 @@ int cmd_replay(int argc,\n>>       ret = result.clean;\n>>     cleanup:\n>> +    if (transaction)\n>> +        ref_transaction_free(transaction);\n>> +    strbuf_release(&transaction_err);\n>>       release_revisions(&revs);\n>>       free(advance_name);\n>>   diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>> index 58b3759935..123734b49f 100755\n>> --- a/t/t3650-replay-basics.sh\n>> +++ b/t/t3650-replay-basics.sh\n>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n>>   '\n>>     test_expect_success 'using replay to rebase two branches, one on \n>> top of other' '\n>> -    git replay --onto main topic1..topic2 >result &&\n>> +    git replay --ref-action=print --onto main topic1..topic2 >result &&\n>>         test_line_count = 1 result &&\n>>   @@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two \n>> branches, one on top of other' '\n>>   '\n>>     test_expect_success 'using replay on bare repo to rebase two \n>> branches, one on top of other' '\n>> -    git -C bare replay --onto main topic1..topic2 >result-bare &&\n>> +    git -C bare replay --ref-action=print --onto main topic1..topic2 \n>> >result-bare &&\n>>       test_cmp expect result-bare\n>>   '\n>>   @@ -86,7 +86,7 @@ test_expect_success 'using replay to perform \n>> basic cherry-pick' '\n>>       # 2nd field of result is refs/heads/main vs. refs/heads/topic2\n>>       # 4th field of result is hash for main instead of hash for topic2\n>>   -    git replay --advance main topic1..topic2 >result &&\n>> +    git replay --ref-action=print --advance main topic1..topic2 \n>> >result &&\n>>         test_line_count = 1 result &&\n>>   @@ -102,7 +102,7 @@ test_expect_success 'using replay to perform \n>> basic cherry-pick' '\n>>   '\n>>     test_expect_success 'using replay on bare repo to perform basic \n>> cherry-pick' '\n>> -    git -C bare replay --advance main topic1..topic2 >result-bare &&\n>> +    git -C bare replay --ref-action=print --advance main \n>> topic1..topic2 >result-bare &&\n>>       test_cmp expect result-bare\n>>   '\n>>   @@ -115,7 +115,7 @@ test_expect_success 'replay fails when both \n>> --advance and --onto are omitted' '\n>>   '\n>>     test_expect_success 'using replay to also rebase a contained \n>> branch' '\n>> -    git replay --contained --onto main main..topic3 >result &&\n>> +    git replay --ref-action=print --contained --onto main \n>> main..topic3 >result &&\n>>         test_line_count = 2 result &&\n>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>> @@ -139,12 +139,12 @@ test_expect_success 'using replay to also \n>> rebase a contained branch' '\n>>   '\n>>     test_expect_success 'using replay on bare repo to also rebase a \n>> contained branch' '\n>> -    git -C bare replay --contained --onto main main..topic3 \n>> >result-bare &&\n>> +    git -C bare replay --ref-action=print --contained --onto main \n>> main..topic3 >result-bare &&\n>>       test_cmp expect result-bare\n>>   '\n>>     test_expect_success 'using replay to rebase multiple divergent \n>> branches' '\n>> -    git replay --onto main ^topic1 topic2 topic4 >result &&\n>> +    git replay --ref-action=print --onto main ^topic1 topic2 topic4 \n>> >result &&\n>>         test_line_count = 2 result &&\n>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>> @@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase \n>> multiple divergent branches' '\n>>   '\n>>     test_expect_success 'using replay on bare repo to rebase multiple \n>> divergent branches, including contained ones' '\n>> -    git -C bare replay --contained --onto main ^main topic2 topic3 \n>> topic4 >result &&\n>> +    git -C bare replay --ref-action=print --contained --onto main \n>> ^main topic2 topic3 topic4 >result &&\n>>         test_line_count = 4 result &&\n>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>> @@ -217,4 +217,32 @@ test_expect_success \n>> 'merge.directoryRenames=false' '\n>>           --onto rename-onto rename-onto..rename-from\n>>   '\n>>   +test_expect_success 'default atomic behavior updates refs directly' '\n>> +    # Store original state for cleanup\n>> +    test_when_finished \"git branch -f topic2 topic1\" &&\n>> +\n>> +    # Test default atomic behavior (no output, refs updated)\n>> +    git replay --onto main topic1..topic2 >output &&\n>> +    test_must_be_empty output &&\n>> +\n>> +    # Verify ref was updated\n>> +    git log --format=%s topic2 >actual &&\n>> +    test_write_lines E D M L B A >expect &&\n>> +    test_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'atomic behavior in bare repository' '\n>> +    # Test atomic updates work in bare repo\n>> +    git -C bare replay --onto main topic1..topic2 >output &&\n>> +    test_must_be_empty output &&\n>> +\n>> +    # Verify ref was updated in bare repo\n>> +    git -C bare log --format=%s topic2 >actual &&\n>> +    test_write_lines E D M L B A >expect &&\n>> +    test_cmp expect actual &&\n>> +\n>> +    # Reset for other tests\n>> +    git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse \n>> topic1)\n>> +'\n>> +\n>>   test_done\n>\n"},{"id":"530201","messageId":"f5cc5082-d3d0-4ec3-ac0e-56e2ad4ef23c@gmail.com","threadId":"64109","inReplyTo":"55e6620a-bf38-4c34-8d52-75d838ba087a@gmail.com","subject":"Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-04T16:15:23Z","receivedAt":"2025-11-04T16:15:27Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Siddarth\n\nOn 03/11/2025 19:32, Siddharth Asthana wrote:\n> \n> I looked at how `git rebase` constructs its reflog messages (it uses\n> something like \"rebase (finish): refs/heads/feature onto abc123def\")\n> and I'm thinking of using a simpler format for replay:\n> \n>      \"replay --onto main\"\n>      \"replay --advance main\"\n> \n> This shows the mode and target in a way that mirrors what the user\n> typed.\n\nThat makes sense\n\n> I chose to use the symbolic name (e.g., \"main\") rather than\n> the commit SHA because it seems more user-friendly, though I notice\n> git rebase uses oid_to_hex().\n\nOne thing to note is that using oid_to_hex() tells us which commit we \nrebased on to. Using \"main\" means it is hard to find exactly which \ncommit was used because you have to dig through the reflog for \"main\" to \nfind where it was pointing at the time that \"replay\" was run.\n\n> Regarding \"include the commits that have been picked\" for --advance\n> mode - would you prefer:\n>    - The revision range as specified by the user (e.g., \"topic1..topic2\")?\n>    - Just the target branch like I have above?\n> \n> The revision range would provide more context, but it might make the\n> reflog message quite long if the user specified something complex. I'm\n> happy to include it if that's what you think would be most useful.\n\nThe full revision range could certainly get quite long. \"git \ncherry-pick\" creates one reflog entry per picked commit which avoids \nthat problem. I don't think we necessarily want \"git replay\" to create \nmasses of reflog entries though so perhaps we should use the revision \nrange if it is simple like \"a..b\" and something more general if it is \nmore complex than that?\n\n> I'll add tests for the reflog messages in both modes.\n\nThat's great\n\nThanks\n\nPhillip\n\n> Thanks,\n> Siddharth\n> \n> \n>>\n>>\n>>> +    default:\n>>> +        BUG(\"unknown ref_action_mode %d\", mode);\n>>> +    }\n>>> +}\n>>> +\n>>>   int cmd_replay(int argc,\n>>>              const char **argv,\n>>>              const char *prefix,\n>>> @@ -294,6 +321,8 @@ int cmd_replay(int argc,\n>>>       struct commit *onto = NULL;\n>>>       const char *onto_name = NULL;\n>>>       int contained = 0;\n>>> +    const char *ref_action_str = NULL;\n>>> +    enum ref_action_mode ref_action = REF_ACTION_UPDATE;\n>>>         struct rev_info revs;\n>>>       struct commit *last_commit = NULL;\n>>> @@ -302,12 +331,14 @@ int cmd_replay(int argc,\n>>>       struct merge_result result;\n>>>       struct strset *update_refs = NULL;\n>>>       kh_oid_map_t *replayed_commits;\n>>> +    struct ref_transaction *transaction = NULL;\n>>> +    struct strbuf transaction_err = STRBUF_INIT;\n>>>       int ret = 0;\n>>>   -    const char * const replay_usage[] = {\n>>> +    const char *const replay_usage[] = {\n>>>           N_(\"(EXPERIMENTAL!) git replay \"\n>>>              \"([--contained] --onto <newbase> | --advance <branch>) \"\n>>> -           \"<revision-range>...\"),\n>>> +           \"[--ref-action[=<mode>]] <revision-range>...\"),\n>>>           NULL\n>>>       };\n>>>       struct option replay_options[] = {\n>>> @@ -319,6 +350,9 @@ int cmd_replay(int argc,\n>>>                  N_(\"replay onto given commit\")),\n>>>           OPT_BOOL(0, \"contained\", &contained,\n>>>                N_(\"advance all branches contained in revision-range\")),\n>>> +        OPT_STRING(0, \"ref-action\", &ref_action_str,\n>>> +               N_(\"mode\"),\n>>> +               N_(\"control ref update behavior (update|print)\")),\n>>>           OPT_END()\n>>>       };\n>>>   @@ -333,6 +367,18 @@ int cmd_replay(int argc,\n>>>       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>>>                     contained, \"--contained\");\n>>>   +    /* Default to update mode if not specified */\n>>> +    if (!ref_action_str)\n>>> +        ref_action_str = \"update\";\n>>> +\n>>> +    /* Parse ref action mode */\n>>> +    if (!strcmp(ref_action_str, \"update\"))\n>>> +        ref_action = REF_ACTION_UPDATE;\n>>> +    else if (!strcmp(ref_action_str, \"print\"))\n>>> +        ref_action = REF_ACTION_PRINT;\n>>> +    else\n>>> +        die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n>>> +\n>>>       advance_name = xstrdup_or_null(advance_name_opt);\n>>>         repo_init_revisions(repo, &revs, prefix);\n>>> @@ -389,6 +435,17 @@ int cmd_replay(int argc,\n>>>       determine_replay_mode(repo, &revs.cmdline, onto_name, \n>>> &advance_name,\n>>>                     &onto, &update_refs);\n>>>   +    /* Initialize ref transaction if using update mode */\n>>> +    if (ref_action == REF_ACTION_UPDATE) {\n>>> +        transaction = \n>>> ref_store_transaction_begin(get_main_ref_store(repo),\n>>> +                              0, &transaction_err);\n>>> +        if (!transaction) {\n>>> +            ret = error(_(\"failed to begin ref transaction: %s\"),\n>>> +                    transaction_err.buf);\n>>> +            goto cleanup;\n>>> +        }\n>>> +    }\n>>> +\n>>>       if (!onto) /* FIXME: Should handle replaying down to root \n>>> commit */\n>>>           die(\"Replaying down to root commit is not supported yet!\");\n>>>   @@ -434,10 +491,15 @@ int cmd_replay(int argc,\n>>>               if (decoration->type == DECORATION_REF_LOCAL &&\n>>>                   (contained || strset_contains(update_refs,\n>>>                                 decoration->name))) {\n>>> -                printf(\"update %s %s %s\\n\",\n>>> -                       decoration->name,\n>>> - oid_to_hex(&last_commit->object.oid),\n>>> -                       oid_to_hex(&commit->object.oid));\n>>> +                if (handle_ref_update(ref_action, transaction,\n>>> +                              decoration->name,\n>>> +                              &last_commit->object.oid,\n>>> +                              &commit->object.oid,\n>>> +                              &transaction_err) < 0) {\n>>> +                    ret = error(_(\"failed to update ref '%s': %s\"),\n>>> +                            decoration->name, transaction_err.buf);\n>>> +                    goto cleanup;\n>>> +                }\n>>>               }\n>>>               decoration = decoration->next;\n>>>           }\n>>> @@ -445,10 +507,23 @@ int cmd_replay(int argc,\n>>>         /* In --advance mode, advance the target ref */\n>>>       if (result.clean == 1 && advance_name) {\n>>> -        printf(\"update %s %s %s\\n\",\n>>> -               advance_name,\n>>> -               oid_to_hex(&last_commit->object.oid),\n>>> -               oid_to_hex(&onto->object.oid));\n>>> +        if (handle_ref_update(ref_action, transaction, advance_name,\n>>> +                      &last_commit->object.oid,\n>>> +                      &onto->object.oid,\n>>> +                      &transaction_err) < 0) {\n>>> +            ret = error(_(\"failed to update ref '%s': %s\"),\n>>> +                    advance_name, transaction_err.buf);\n>>> +            goto cleanup;\n>>> +        }\n>>> +    }\n>>> +\n>>> +    /* Commit the ref transaction if we have one */\n>>> +    if (transaction && result.clean == 1) {\n>>> +        if (ref_transaction_commit(transaction, &transaction_err)) {\n>>> +            ret = error(_(\"failed to commit ref transaction: %s\"),\n>>> +                    transaction_err.buf);\n>>> +            goto cleanup;\n>>> +        }\n>>>       }\n>>>         merge_finalize(&merge_opt, &result);\n>>> @@ -460,6 +535,9 @@ int cmd_replay(int argc,\n>>>       ret = result.clean;\n>>>     cleanup:\n>>> +    if (transaction)\n>>> +        ref_transaction_free(transaction);\n>>> +    strbuf_release(&transaction_err);\n>>>       release_revisions(&revs);\n>>>       free(advance_name);\n>>>   diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>>> index 58b3759935..123734b49f 100755\n>>> --- a/t/t3650-replay-basics.sh\n>>> +++ b/t/t3650-replay-basics.sh\n>>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n>>>   '\n>>>     test_expect_success 'using replay to rebase two branches, one on \n>>> top of other' '\n>>> -    git replay --onto main topic1..topic2 >result &&\n>>> +    git replay --ref-action=print --onto main topic1..topic2 >result &&\n>>>         test_line_count = 1 result &&\n>>>   @@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two \n>>> branches, one on top of other' '\n>>>   '\n>>>     test_expect_success 'using replay on bare repo to rebase two \n>>> branches, one on top of other' '\n>>> -    git -C bare replay --onto main topic1..topic2 >result-bare &&\n>>> +    git -C bare replay --ref-action=print --onto main topic1..topic2 \n>>> >result-bare &&\n>>>       test_cmp expect result-bare\n>>>   '\n>>>   @@ -86,7 +86,7 @@ test_expect_success 'using replay to perform \n>>> basic cherry-pick' '\n>>>       # 2nd field of result is refs/heads/main vs. refs/heads/topic2\n>>>       # 4th field of result is hash for main instead of hash for topic2\n>>>   -    git replay --advance main topic1..topic2 >result &&\n>>> +    git replay --ref-action=print --advance main topic1..topic2 \n>>> >result &&\n>>>         test_line_count = 1 result &&\n>>>   @@ -102,7 +102,7 @@ test_expect_success 'using replay to perform \n>>> basic cherry-pick' '\n>>>   '\n>>>     test_expect_success 'using replay on bare repo to perform basic \n>>> cherry-pick' '\n>>> -    git -C bare replay --advance main topic1..topic2 >result-bare &&\n>>> +    git -C bare replay --ref-action=print --advance main \n>>> topic1..topic2 >result-bare &&\n>>>       test_cmp expect result-bare\n>>>   '\n>>>   @@ -115,7 +115,7 @@ test_expect_success 'replay fails when both -- \n>>> advance and --onto are omitted' '\n>>>   '\n>>>     test_expect_success 'using replay to also rebase a contained \n>>> branch' '\n>>> -    git replay --contained --onto main main..topic3 >result &&\n>>> +    git replay --ref-action=print --contained --onto main \n>>> main..topic3 >result &&\n>>>         test_line_count = 2 result &&\n>>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>>> @@ -139,12 +139,12 @@ test_expect_success 'using replay to also \n>>> rebase a contained branch' '\n>>>   '\n>>>     test_expect_success 'using replay on bare repo to also rebase a \n>>> contained branch' '\n>>> -    git -C bare replay --contained --onto main main..topic3 >result- \n>>> bare &&\n>>> +    git -C bare replay --ref-action=print --contained --onto main \n>>> main..topic3 >result-bare &&\n>>>       test_cmp expect result-bare\n>>>   '\n>>>     test_expect_success 'using replay to rebase multiple divergent \n>>> branches' '\n>>> -    git replay --onto main ^topic1 topic2 topic4 >result &&\n>>> +    git replay --ref-action=print --onto main ^topic1 topic2 topic4 \n>>> >result &&\n>>>         test_line_count = 2 result &&\n>>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>>> @@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase \n>>> multiple divergent branches' '\n>>>   '\n>>>     test_expect_success 'using replay on bare repo to rebase multiple \n>>> divergent branches, including contained ones' '\n>>> -    git -C bare replay --contained --onto main ^main topic2 topic3 \n>>> topic4 >result &&\n>>> +    git -C bare replay --ref-action=print --contained --onto main \n>>> ^main topic2 topic3 topic4 >result &&\n>>>         test_line_count = 4 result &&\n>>>       cut -f 3 -d \" \" result >new-branch-tips &&\n>>> @@ -217,4 +217,32 @@ test_expect_success \n>>> 'merge.directoryRenames=false' '\n>>>           --onto rename-onto rename-onto..rename-from\n>>>   '\n>>>   +test_expect_success 'default atomic behavior updates refs directly' '\n>>> +    # Store original state for cleanup\n>>> +    test_when_finished \"git branch -f topic2 topic1\" &&\n>>> +\n>>> +    # Test default atomic behavior (no output, refs updated)\n>>> +    git replay --onto main topic1..topic2 >output &&\n>>> +    test_must_be_empty output &&\n>>> +\n>>> +    # Verify ref was updated\n>>> +    git log --format=%s topic2 >actual &&\n>>> +    test_write_lines E D M L B A >expect &&\n>>> +    test_cmp expect actual\n>>> +'\n>>> +\n>>> +test_expect_success 'atomic behavior in bare repository' '\n>>> +    # Test atomic updates work in bare repo\n>>> +    git -C bare replay --onto main topic1..topic2 >output &&\n>>> +    test_must_be_empty output &&\n>>> +\n>>> +    # Verify ref was updated in bare repo\n>>> +    git -C bare log --format=%s topic2 >actual &&\n>>> +    test_write_lines E D M L B A >expect &&\n>>> +    test_cmp expect actual &&\n>>> +\n>>> +    # Reset for other tests\n>>> +    git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse \n>>> topic1)\n>>> +'\n>>> +\n>>>   test_done\n>>\n\n"},{"id":"530260","messageId":"a030b02e-7ef2-44ef-9793-7b8db3abb7c3@gmail.com","threadId":"64109","inReplyTo":"CABPp-BHyUFpFEK1YXSYQWEXSAa2fnUTsH9nsf=LgPs=GNQG2RQ@mail.gmail.com","subject":"Re: [PATCH v6 1/3] replay: use die_for_incompatible_opt2() for option validation","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-05T18:39:17Z","receivedAt":"2025-11-05T18:39:24Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 01/11/25 00:17, Elijah Newren wrote:\n> On Thu, Oct 30, 2025 at 12:19 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> In preparation for adding the --ref-action option, convert option\n>> validation to use die_for_incompatible_opt2(). This helper provides\n>> standardized error messages for mutually exclusive options.\n>>\n>> The following commit introduces --ref-action which will be incompatible\n>> with certain other options. Using die_for_incompatible_opt2() now means\n>> that commit can cleanly add its validation using the same pattern,\n>> keeping the validation logic consistent and maintainable.\n>>\n>> This also aligns git-replay's option handling with how other Git commands\n>> manage option conflicts, using the established die_for_incompatible_opt*()\n>> helper family.\n>>\n>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n>> ---\n>>   builtin/replay.c | 6 +++---\n>>   1 file changed, 3 insertions(+), 3 deletions(-)\n>>\n>> diff --git a/builtin/replay.c b/builtin/replay.c\n>> index 6172c8aacc..b64fc72063 100644\n>> --- a/builtin/replay.c\n>> +++ b/builtin/replay.c\n>> @@ -330,9 +330,9 @@ int cmd_replay(int argc,\n>>                  usage_with_options(replay_usage, replay_options);\n>>          }\n>>\n>> -       if (advance_name_opt && contained)\n>> -               die(_(\"options '%s' and '%s' cannot be used together\"),\n>> -                   \"--advance\", \"--contained\");\n>> +       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>> +                                 contained, \"--contained\");\n>> +\n>>          advance_name = xstrdup_or_null(advance_name_opt);\n>>\n>>          repo_init_revisions(repo, &revs, prefix);\n>> --\n>> 2.51.0\n\n\nHi Elijah,\n\n\n> Thanks for splitting this one out; looks good.\n\n\nThanks for confirming! I'm glad the preparatory refactoring in its own \ncommit makes the series easier to review.\n\nSiddharth\n\n"},{"id":"530261","messageId":"33578d71-2145-4256-8c90-0039ecf8fdb9@gmail.com","threadId":"64109","inReplyTo":"CAP8UFD2xJVtQMEFBQAZJP+kYq5iYCcQYn9WD_x+SO8grauPrZg@mail.gmail.com","subject":"Re: [PATCH v6 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-05T19:03:32Z","receivedAt":"2025-11-05T19:03:41Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 31/10/25 12:38, Christian Couder wrote:\n> On Thu, Oct 30, 2025 at 8:20 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>\n>> +static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n>> +{\n>> +       if (!ref_action || !strcmp(ref_action, \"update\"))\n>> +               return REF_ACTION_UPDATE;\n>> +       if (!strcmp(ref_action, \"print\"))\n>> +               return REF_ACTION_PRINT;\n>> +       die(_(\"invalid %s value: '%s'\"), source, ref_action);\n>> +}\n>> +\n>> +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)\n\n\nHi Christian,\n\n\n> I think it could be \"ref_action\" (instead of \"ref_action_str\" ) in\n> this function too.\n\n\nGood catch. Will make this consistent in v7.\n\n\n>> +{\n>> +       const char *config_value = NULL;\n>> +\n>> +       /* Command line option takes precedence */\n>> +       if (ref_action_str)\n>> +               return parse_ref_action_mode(ref_action_str, \"--ref-action\");\n>> +\n>> +       /* Check config value */\n>> +       if (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n>> +               return parse_ref_action_mode(config_value, \"replay.refAction\");\n>> +\n>> +       /* Default to update mode */\n>> +       return REF_ACTION_UPDATE;\n>> +}\n>> +\n>>   static int handle_ref_update(enum ref_action_mode mode,\n>>                               struct ref_transaction *transaction,\n>>                               const char *refname,\n>> @@ -367,17 +393,8 @@ int cmd_replay(int argc,\n>>          die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>>                                    contained, \"--contained\");\n>>\n>> -       /* Default to update mode if not specified */\n>> -       if (!ref_action_str)\n>> -               ref_action_str = \"update\";\n>> -\n>> -       /* Parse ref action mode */\n>> -       if (!strcmp(ref_action_str, \"update\"))\n>> -               ref_action = REF_ACTION_UPDATE;\n>> -       else if (!strcmp(ref_action_str, \"print\"))\n>> -               ref_action = REF_ACTION_PRINT;\n>> -       else\n>> -               die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n> Maybe parse_ref_action_mode() could have been introduced in the\n> previous commit already?\n\n\nYou're right—since parse_ref_action_mode() is actually used for \nvalidation in commit 2, it makes more sense to introduce it there rather \nthan wait until commit 3. Will move it to the earlier commit.\n\n\n>\n>> +       /* Parse ref action mode from command line or config */\n>> +       ref_action = get_ref_action_mode(repo, ref_action_str);\n> Here it could be:\n>\n>        ref_mode = get_ref_action_mode(repo, ref_action);\n>\n> Thanks!\n\n\nAgreed. The variable naming was inconsistent—I had both `ref_action`\n(for the string) and `ref_action` (for the enum) which was confusing.\nWill use `ref_action` for the string parameter and `ref_mode` for the\nenum variable throughout for clarity.\n\nThanks for the careful review!\n\n"},{"id":"530262","messageId":"f24dd910-855d-4594-9e56-b0c8586eb1f9@gmail.com","threadId":"64109","inReplyTo":"CABPp-BGmHegyqvN48vJO1Y9gWVDk5u2SO5_i9KMw2aoAtmNuyw@mail.gmail.com","subject":"Re: [PATCH v6 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-05T19:07:46Z","receivedAt":"2025-11-05T19:07:53Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 01/11/25 00:19, Elijah Newren wrote:\n> On Thu, Oct 30, 2025 at 12:20 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> The git replay command currently outputs update commands that can be\n>> piped to update-ref to achieve a rebase, e.g.\n>>\n>>    git replay --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> This separation had advantages for three special cases:\n>>    * it made testing easy (when state isn't modified from one step to\n>>      the next, you don't need to make temporary branches or have undo\n>>      commands, or try to track the changes)\n>>    * it provided a natural can-it-rebase-cleanly (and what would it\n>>      rebase to) capability without automatically updating refs, similar\n>>      to a --dry-run\n>>    * it provided a natural low-level tool for the suite of hash-object,\n>>      mktree, commit-tree, mktag, merge-tree, and update-ref, allowing\n>>      users to have another building block for experimentation and making\n>>      new tools\n>>\n>> However, it should be noted that all three of these are somewhat\n>> special cases; users, whether on the client or server side, would\n>> almost certainly find it more ergonomic to simply have the updating\n>> of refs be the default.\n>>\n>> For server-side operations in particular, the pipeline architecture\n>> creates process coordination overhead. Server implementations that need\n>> to perform rebases atomically must maintain additional code to:\n>>\n>>    1. Spawn and manage a pipeline between git-replay and git-update-ref\n>>    2. Coordinate stdout/stderr streams across the pipe boundary\n>>    3. Handle partial failure states if the pipeline breaks mid-execution\n>>    4. Parse and validate the update-ref command output\n>>\n>> Change the default behavior to update refs directly, and atomically (at\n>> least to the extent supported by the refs backend in use). This\n>> eliminates the process coordination overhead for the common case.\n>>\n>> For users needing the traditional pipeline workflow, add a new\n>> --ref-action=<mode> option that preserves the original behavior:\n>>\n>>    git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> The mode can be:\n>>    * update (default): Update refs directly using an atomic transaction\n>>    * print: Output update-ref commands for pipeline use\n> Looks good up to here.\n>\n>> Implementation details:\n>>\n>> The atomic ref updates are implemented using Git's ref transaction API.\n>> In cmd_replay(), when not in `print` mode, we initialize a transaction\n>> using ref_store_transaction_begin() with the default atomic behavior.\n>> As commits are replayed, ref updates are staged into the transaction\n>> using ref_transaction_update(). Finally, ref_transaction_commit()\n>> applies all updates atomically—either all updates succeed or none do.\n>>\n>> To avoid code duplication between the 'print' and 'update' modes, this\n>> commit extracts a handle_ref_update() helper function. This function\n>> takes the mode (as an enum) and either prints the update command or\n>> stages it into the transaction. Using an enum rather than passing the\n>> string around provides type safety and allows the compiler to catch\n>> typos. The switch statement makes it easy to add future modes.\n>>\n>> The helper function signature:\n>>\n>>    static int handle_ref_update(enum ref_action_mode mode,\n>>                                  struct ref_transaction *transaction,\n>>                                  const char *refname,\n>>                                  const struct object_id *new_oid,\n>>                                  const struct object_id *old_oid,\n>>                                  struct strbuf *err)\n>>\n>> The enum is defined as:\n>>\n>>    enum ref_action_mode {\n>>        REF_ACTION_UPDATE,\n>>        REF_ACTION_PRINT\n>>    };\n>>\n>> The mode string is converted to enum immediately after parse_options()\n>> to avoid string comparisons throughout the codebase and provide compiler\n>> protection against typos.\n> I'm not sure the implementation details section above makes sense to\n> include in the commit message; it feels like it's not providing much\n> high level information nor much \"why\" information, but just presenting\n> an alternative view of the information people will find in the patch.\n> Perhaps leave it out?\n\n\nMake sense. Will remove the \"Implementation details\" section.\n\n\n>\n>> Test suite changes:\n>>\n>> All existing tests that expected command output now use\n>> --ref-action=print to preserve their original behavior. This keeps\n>> the tests valid while allowing them to verify that the pipeline workflow\n>> still works correctly.\n>>\n>> New tests were added to verify:\n>>    - Default atomic behavior (no output, refs updated directly)\n>>    - Bare repository support (server-side use case)\n>>    - Equivalence between traditional pipeline and atomic updates\n>>    - Real atomicity using a lock file to verify all-or-nothing guarantee\n>>    - Test isolation using test_when_finished to clean up state\n>>\n>> The bare repository tests were fixed to rebuild their expectations\n>> independently rather than comparing to previous test output, improving\n>> test reliability and isolation.\n> The above paragraph sounds like you are comparing to an earlier\n> series, which will confuse future readers who only compare to code\n> that existed before your patches.\n\n\nRight—\"The bare repository tests were fixed...\" belongs in the cover \nletter, not the commit message. Will remove it.\n\n\n>\n>> A following commit will add a replay.refAction configuration\n>> option for users who prefer the traditional pipeline output as their\n>> default behavior.\n>>\n>> Helped-by: Elijah Newren <newren@gmail.com>\n>> Helped-by: Patrick Steinhardt <ps@pks.im>\n>> Helped-by: Christian Couder <christian.couder@gmail.com>\n>> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n>> ---\n>>   Documentation/git-replay.adoc | 65 +++++++++++++++--------\n>>   builtin/replay.c              | 98 +++++++++++++++++++++++++++++++----\n>>   t/t3650-replay-basics.sh      | 44 +++++++++++++---\n>>   3 files changed, 167 insertions(+), 40 deletions(-)\n>>\n>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n>> index 0b12bf8aa4..037b093196 100644\n>> --- a/Documentation/git-replay.adoc\n>> +++ b/Documentation/git-replay.adoc\n>> @@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n>>   SYNOPSIS\n>>   --------\n>>   [verse]\n>> -(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n>> +(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n>>\n>>   DESCRIPTION\n>>   -----------\n>>\n>>   Takes ranges of commits and replays them onto a new location. Leaves\n>> -the working tree and the index untouched, and updates no references.\n>> -The output of this command is meant to be used as input to\n>> -`git update-ref --stdin`, which would update the relevant branches\n>> +the working tree and the index untouched. By default, updates the\n>> +relevant references using an atomic transaction (all refs update or\n>> +none). Use `--ref-action=print` to avoid automatic ref updates and\n>> +instead get update commands that can be piped to `git update-ref --stdin`\n>>   (see the OUTPUT section below).\n>>\n>>   THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n>> @@ -29,18 +30,31 @@ OPTIONS\n>>          Starting point at which to create the new commits.  May be any\n>>          valid commit, and not just an existing branch name.\n>>   +\n>> -When `--onto` is specified, the update-ref command(s) in the output will\n>> -update the branch(es) in the revision range to point at the new\n>> -commits, similar to the way how `git rebase --update-refs` updates\n>> -multiple branches in the affected range.\n>> +When `--onto` is specified, the branch(es) in the revision range will be\n>> +updated to point at the new commits (or update commands will be printed\n>> +if `--ref-action=print` is used), similar to the way `git rebase --update-refs`\n>> +updates multiple branches in the affected range.\n> I'm not sure if the parenthetical comment is necessary; we tend not to\n> try to document every combinatorial combination with every sentence.\n\n\nGood point. Will remove the `--ref-action=print` parentheticals from\nthe `--onto` and `--advance` descriptions in git-replay.adoc.\n\n\n> For example, in the `git rebase` manpage under the description of the\n> `--strategy` flag, it says \"Because git rebase replays each commit\n> from the working branch on top of the <upstream> branch using the\n> given strategy\", which is technically incorrect if either the --onto\n> or --keep-base flags are specified, but belaboring all the details at\n> that location would just burden the reader and the explanations of\n> --onto and --keep-base are sufficient for users to understand.  I\n> think we tend to just describe the option in combination with the\n> default, and only mention other options if the combination is\n> ambiguous or confusing.  I don't think users would find anything\n> ambiguous or confusing about how --ref-action=print would combine with\n> these options, so I don't think it's necessary to make the description\n> longer.\n>\n>>   --advance <branch>::\n>>          Starting point at which to create the new commits; must be a\n>>          branch name.\n>>   +\n>> -When `--advance` is specified, the update-ref command(s) in the output\n>> -will update the branch passed as an argument to `--advance` to point at\n>> -the new commits (in other words, this mimics a cherry-pick operation).\n>> +The history is replayed on top of the <branch> and <branch> is updated to\n>> +point at the tip of the resulting history (or an update command will be\n>> +printed if `--ref-action=print` is used). This is different from `--onto`,\n>> +which uses the target only as a starting point without updating it.\n> Same comment as above about this parenthetical comment as well.\n>\n>> +\n>> +--ref-action[=<mode>]::\n>> +       Control how references are updated. The mode can be:\n>> ++\n>> +--\n>> +       * `update` (default): Update refs directly using an atomic transaction.\n>> +         All refs are updated or none are (all-or-nothing behavior).\n>> +       * `print`: Output update-ref commands for pipeline use. This is the\n>> +         traditional behavior where output can be piped to `git update-ref --stdin`.\n>> +--\n>> ++\n>> +The default mode can be configured via the `replay.refAction` configuration variable.\n> This last sentence conflicts with the commit message; if the\n> configuration option isn't added until a later commit, then this last\n> sentence shouldn't be added until then either.\n\n\nAbsolutely right. Will move \"The default mode can be configured via the \n`replay.refAction` configuration variable.\" to commit 3.\n\n\n>\n>>   <revision-range>::\n>>          Range of commits to replay. More than one <revision-range> can\n>> @@ -54,8 +68,11 @@ include::rev-list-options.adoc[]\n>>   OUTPUT\n>>   ------\n>>\n>> -When there are no conflicts, the output of this command is usable as\n>> -input to `git update-ref --stdin`.  It is of the form:\n>> +By default, or with `--ref-action=update`, this command produces no output on\n>> +success, as refs are updated directly using an atomic transaction.\n>> +\n>> +When using `--ref-action=print`, the output is usable as input to\n>> +`git update-ref --stdin`. It is of the form:\n>>\n>>          update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n>>          update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n>> @@ -81,6 +98,14 @@ To simply rebase `mybranch` onto `target`:\n>>\n>>   ------------\n>>   $ git replay --onto target origin/main..mybranch\n>> +------------\n>> +\n>> +The refs are updated atomically and no output is produced on success.\n>> +\n>> +To see what would be updated without actually updating:\n>> +\n>> +------------\n>> +$ git replay --ref-action=print --onto target origin/main..mybranch\n>>   update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n>>   ------------\n>>\n>> @@ -88,33 +113,29 @@ To cherry-pick the commits from mybranch onto target:\n>>\n>>   ------------\n>>   $ git replay --advance target origin/main..mybranch\n>> -update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n>>   ------------\n>>\n>>   Note that the first two examples replay the exact same commits and on\n>>   top of the exact same new base, they only differ in that the first\n>> -provides instructions to make mybranch point at the new commits and\n>> -the second provides instructions to make target point at them.\n>> +updates mybranch to point at the new commits and the second updates\n>> +target to point at them.\n>>\n>>   What if you have a stack of branches, one depending upon another, and\n>>   you'd really like to rebase the whole set?\n>>\n>>   ------------\n>>   $ git replay --contained --onto origin/main origin/main..tipbranch\n>> -update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n>> -update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n>> -update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n>>   ------------\n>>\n>> +All three branches (`branch1`, `branch2`, and `tipbranch`) are updated\n>> +atomically.\n>> +\n>>   When calling `git replay`, one does not need to specify a range of\n>>   commits to replay using the syntax `A..B`; any range expression will\n>>   do:\n>>\n>>   ------------\n>>   $ git replay --onto origin/main ^base branch1 branch2 branch3\n>> -update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n>> -update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n>> -update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n>>   ------------\n>>\n>>   This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\n>> diff --git a/builtin/replay.c b/builtin/replay.c\n>> index b64fc72063..0564d4d2e7 100644\n>> --- a/builtin/replay.c\n>> +++ b/builtin/replay.c\n>> @@ -20,6 +20,11 @@\n>>   #include <oidset.h>\n>>   #include <tree.h>\n>>\n>> +enum ref_action_mode {\n>> +       REF_ACTION_UPDATE,\n>> +       REF_ACTION_PRINT,\n>> +};\n>> +\n>>   static const char *short_commit_name(struct repository *repo,\n>>                                       struct commit *commit)\n>>   {\n>> @@ -284,6 +289,28 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>>          return create_commit(repo, result->tree, pickme, replayed_base);\n>>   }\n>>\n>> +static int handle_ref_update(enum ref_action_mode mode,\n>> +                            struct ref_transaction *transaction,\n>> +                            const char *refname,\n>> +                            const struct object_id *new_oid,\n>> +                            const struct object_id *old_oid,\n>> +                            struct strbuf *err)\n>> +{\n>> +       switch (mode) {\n>> +       case REF_ACTION_PRINT:\n>> +               printf(\"update %s %s %s\\n\",\n>> +                      refname,\n>> +                      oid_to_hex(new_oid),\n>> +                      oid_to_hex(old_oid));\n>> +               return 0;\n>> +       case REF_ACTION_UPDATE:\n>> +               return ref_transaction_update(transaction, refname, new_oid, old_oid,\n>> +                                             NULL, NULL, 0, \"git replay\", err);\n>> +       default:\n>> +               BUG(\"unknown ref_action_mode %d\", mode);\n>> +       }\n>> +}\n>> +\n>>   int cmd_replay(int argc,\n>>                 const char **argv,\n>>                 const char *prefix,\n>> @@ -294,6 +321,8 @@ int cmd_replay(int argc,\n>>          struct commit *onto = NULL;\n>>          const char *onto_name = NULL;\n>>          int contained = 0;\n>> +       const char *ref_action_str = NULL;\n>> +       enum ref_action_mode ref_action = REF_ACTION_UPDATE;\n>>\n>>          struct rev_info revs;\n>>          struct commit *last_commit = NULL;\n>> @@ -302,12 +331,14 @@ int cmd_replay(int argc,\n>>          struct merge_result result;\n>>          struct strset *update_refs = NULL;\n>>          kh_oid_map_t *replayed_commits;\n>> +       struct ref_transaction *transaction = NULL;\n>> +       struct strbuf transaction_err = STRBUF_INIT;\n>>          int ret = 0;\n>>\n>> -       const char * const replay_usage[] = {\n>> +       const char *const replay_usage[] = {\n>>                  N_(\"(EXPERIMENTAL!) git replay \"\n>>                     \"([--contained] --onto <newbase> | --advance <branch>) \"\n>> -                  \"<revision-range>...\"),\n>> +                  \"[--ref-action[=<mode>]] <revision-range>...\"),\n>>                  NULL\n>>          };\n>>          struct option replay_options[] = {\n>> @@ -319,6 +350,9 @@ int cmd_replay(int argc,\n>>                             N_(\"replay onto given commit\")),\n>>                  OPT_BOOL(0, \"contained\", &contained,\n>>                           N_(\"advance all branches contained in revision-range\")),\n>> +               OPT_STRING(0, \"ref-action\", &ref_action_str,\n>> +                          N_(\"mode\"),\n>> +                          N_(\"control ref update behavior (update|print)\")),\n>>                  OPT_END()\n>>          };\n>>\n>> @@ -333,6 +367,18 @@ int cmd_replay(int argc,\n>>          die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>>                                    contained, \"--contained\");\n>>\n>> +       /* Default to update mode if not specified */\n>> +       if (!ref_action_str)\n>> +               ref_action_str = \"update\";\n>> +\n>> +       /* Parse ref action mode */\n>> +       if (!strcmp(ref_action_str, \"update\"))\n>> +               ref_action = REF_ACTION_UPDATE;\n>> +       else if (!strcmp(ref_action_str, \"print\"))\n>> +               ref_action = REF_ACTION_PRINT;\n>> +       else\n>> +               die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n>> +\n>>          advance_name = xstrdup_or_null(advance_name_opt);\n>>\n>>          repo_init_revisions(repo, &revs, prefix);\n>> @@ -389,6 +435,17 @@ int cmd_replay(int argc,\n>>          determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n>>                                &onto, &update_refs);\n>>\n>> +       /* Initialize ref transaction if using update mode */\n>> +       if (ref_action == REF_ACTION_UPDATE) {\n>> +               transaction = ref_store_transaction_begin(get_main_ref_store(repo),\n>> +                                                         0, &transaction_err);\n>> +               if (!transaction) {\n>> +                       ret = error(_(\"failed to begin ref transaction: %s\"),\n>> +                                   transaction_err.buf);\n>> +                       goto cleanup;\n>> +               }\n>> +       }\n>> +\n>>          if (!onto) /* FIXME: Should handle replaying down to root commit */\n>>                  die(\"Replaying down to root commit is not supported yet!\");\n>>\n>> @@ -434,10 +491,15 @@ int cmd_replay(int argc,\n>>                          if (decoration->type == DECORATION_REF_LOCAL &&\n>>                              (contained || strset_contains(update_refs,\n>>                                                            decoration->name))) {\n>> -                               printf(\"update %s %s %s\\n\",\n>> -                                      decoration->name,\n>> -                                      oid_to_hex(&last_commit->object.oid),\n>> -                                      oid_to_hex(&commit->object.oid));\n>> +                               if (handle_ref_update(ref_action, transaction,\n>> +                                                     decoration->name,\n>> +                                                     &last_commit->object.oid,\n>> +                                                     &commit->object.oid,\n>> +                                                     &transaction_err) < 0) {\n>> +                                       ret = error(_(\"failed to update ref '%s': %s\"),\n>> +                                                   decoration->name, transaction_err.buf);\n>> +                                       goto cleanup;\n>> +                               }\n>>                          }\n>>                          decoration = decoration->next;\n>>                  }\n>> @@ -445,10 +507,23 @@ int cmd_replay(int argc,\n>>\n>>          /* In --advance mode, advance the target ref */\n>>          if (result.clean == 1 && advance_name) {\n>> -               printf(\"update %s %s %s\\n\",\n>> -                      advance_name,\n>> -                      oid_to_hex(&last_commit->object.oid),\n>> -                      oid_to_hex(&onto->object.oid));\n>> +               if (handle_ref_update(ref_action, transaction, advance_name,\n>> +                                     &last_commit->object.oid,\n>> +                                     &onto->object.oid,\n>> +                                     &transaction_err) < 0) {\n>> +                       ret = error(_(\"failed to update ref '%s': %s\"),\n>> +                                   advance_name, transaction_err.buf);\n>> +                       goto cleanup;\n>> +               }\n>> +       }\n>> +\n>> +       /* Commit the ref transaction if we have one */\n>> +       if (transaction && result.clean == 1) {\n>> +               if (ref_transaction_commit(transaction, &transaction_err)) {\n>> +                       ret = error(_(\"failed to commit ref transaction: %s\"),\n>> +                                   transaction_err.buf);\n>> +                       goto cleanup;\n>> +               }\n>>          }\n>>\n>>          merge_finalize(&merge_opt, &result);\n>> @@ -460,6 +535,9 @@ int cmd_replay(int argc,\n>>          ret = result.clean;\n>>\n>>   cleanup:\n>> +       if (transaction)\n>> +               ref_transaction_free(transaction);\n>> +       strbuf_release(&transaction_err);\n>>          release_revisions(&revs);\n>>          free(advance_name);\n>>\n>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>> index 58b3759935..123734b49f 100755\n>> --- a/t/t3650-replay-basics.sh\n>> +++ b/t/t3650-replay-basics.sh\n>> @@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n>>   '\n>>\n>>   test_expect_success 'using replay to rebase two branches, one on top of other' '\n>> -       git replay --onto main topic1..topic2 >result &&\n>> +       git replay --ref-action=print --onto main topic1..topic2 >result &&\n>>\n>>          test_line_count = 1 result &&\n>>\n>> @@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n>>   '\n>>\n>>   test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n>> -       git -C bare replay --onto main topic1..topic2 >result-bare &&\n>> +       git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&\n>>          test_cmp expect result-bare\n>>   '\n>>\n>> @@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n>>          # 2nd field of result is refs/heads/main vs. refs/heads/topic2\n>>          # 4th field of result is hash for main instead of hash for topic2\n>>\n>> -       git replay --advance main topic1..topic2 >result &&\n>> +       git replay --ref-action=print --advance main topic1..topic2 >result &&\n>>\n>>          test_line_count = 1 result &&\n>>\n>> @@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n>>   '\n>>\n>>   test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n>> -       git -C bare replay --advance main topic1..topic2 >result-bare &&\n>> +       git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&\n>>          test_cmp expect result-bare\n>>   '\n>>\n>> @@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n>>   '\n>>\n>>   test_expect_success 'using replay to also rebase a contained branch' '\n>> -       git replay --contained --onto main main..topic3 >result &&\n>> +       git replay --ref-action=print --contained --onto main main..topic3 >result &&\n>>\n>>          test_line_count = 2 result &&\n>>          cut -f 3 -d \" \" result >new-branch-tips &&\n>> @@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '\n>>   '\n>>\n>>   test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n>> -       git -C bare replay --contained --onto main main..topic3 >result-bare &&\n>> +       git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&\n>>          test_cmp expect result-bare\n>>   '\n>>\n>>   test_expect_success 'using replay to rebase multiple divergent branches' '\n>> -       git replay --onto main ^topic1 topic2 topic4 >result &&\n>> +       git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&\n>>\n>>          test_line_count = 2 result &&\n>>          cut -f 3 -d \" \" result >new-branch-tips &&\n>> @@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n>>   '\n>>\n>>   test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n>> -       git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n>> +       git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n>>\n>>          test_line_count = 4 result &&\n>>          cut -f 3 -d \" \" result >new-branch-tips &&\n>> @@ -217,4 +217,32 @@ test_expect_success 'merge.directoryRenames=false' '\n>>                  --onto rename-onto rename-onto..rename-from\n>>   '\n>>\n>> +test_expect_success 'default atomic behavior updates refs directly' '\n>> +       # Store original state for cleanup\n>> +       test_when_finished \"git branch -f topic2 topic1\" &&\n> Why are you resetting topic2 back to topic1?  Shouldn't it be set back\n> to what it was before the test ran instead, e.g.\n>      START=$(git rev-parse topic2) &&\n>      test_when_finished \"git branch -f topic2 $START\" &&\n> ?\n\n\nYes, you're right. Should be:\n     START=$(git rev-parse topic2) &&\n     test_when_finished \"git branch -f topic2 $START\" &&\n\n\n>> +\n>> +       # Test default atomic behavior (no output, refs updated)\n>> +       git replay --onto main topic1..topic2 >output &&\n>> +       test_must_be_empty output &&\n>> +\n>> +       # Verify ref was updated\n>> +       git log --format=%s topic2 >actual &&\n>> +       test_write_lines E D M L B A >expect &&\n>> +       test_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'atomic behavior in bare repository' '\n>> +       # Test atomic updates work in bare repo\n>> +       git -C bare replay --onto main topic1..topic2 >output &&\n>> +       test_must_be_empty output &&\n>> +\n>> +       # Verify ref was updated in bare repo\n>> +       git -C bare log --format=%s topic2 >actual &&\n>> +       test_write_lines E D M L B A >expect &&\n>> +       test_cmp expect actual &&\n>> +\n>> +       # Reset for other tests\n>> +       git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n> This reset happens too late to help if the earlier commands fail, and\n> also resets to the wrong ref.  You should instead use a\n> test_when_finished block, and make sure to reset to what topic2 used\n> to point to, not reset it to what topic1 points to.\n\n\nWill fix both test cleanup issues using test_when_finished with the \nproper START variable.\n\nThanks for the thorough review!\n\n\n>\n>\n> Otherwise, the patch looks good.  This is really close to being ready\n> to merge; just a few minor fixups needed that I highlighted above.\n"},{"id":"530263","messageId":"3d1dcfe2-3d41-4c96-b44f-0611b11ce853@gmail.com","threadId":"64109","inReplyTo":"CABPp-BE_pAQ8f-jjv16Ts-KRTEr3Qc402qRuJKFFW6G3J9shtA@mail.gmail.com","subject":"Re: [PATCH v6 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-05T19:10:48Z","receivedAt":"2025-11-05T19:10:56Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 01/11/25 00:19, Elijah Newren wrote:\n> On Thu, Oct 30, 2025 at 12:20 PM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> Add a configuration option to control the default behavior of git replay\n>> for updating references. This allows users who prefer the traditional\n>> pipeline output to set it once in their config instead of passing\n>> --ref-action=print with every command.\n>>\n>> The config option uses string values that mirror the behavior modes:\n>>    * replay.refAction = update (default): atomic ref updates\n>>    * replay.refAction = print: output commands for pipeline\n>>\n>> The command-line --ref-action option always overrides the config setting,\n>> allowing users to temporarily change behavior for a single invocation.\n> The above paragraph merely states that we follow git practices with\n> this config options and its corresponding command line; I think we'd\n> need to call it out if we didn't do that, but calling out that we do\n> follow git conventions seems unnecessary.\n\n\nFair point. Will remove the paragraph about command-line precedence.\n\n\n>\n>> Implementation details:\n>>\n>> In cmd_replay(), after parsing command-line options, we check if\n>> --ref-action was provided. If not, we read the configuration using\n>> repo_config_get_string_tmp(). If the config variable is set, we validate\n>> the value and use it to set the ref_action_str:\n>>\n>>    Config value      Internal mode    Behavior\n>>    ──────────────────────────────────────────────────────────────\n>>    \"update\"          \"update\"         Atomic ref updates (default)\n>>    \"print\"           \"print\"          Pipeline output\n>>    (not set)         \"update\"         Atomic ref updates (default)\n>>    (invalid)         error            Die with helpful message\n>>\n>> If an invalid value is provided, we die() immediately with an error\n>> message explaining the valid options. This catches configuration errors\n>> early and provides clear guidance to users.\n>>\n>> The command-line --ref-action option, when provided, overrides the\n>> config value. This precedence allows users to set their preferred default\n>> while still having per-invocation control:\n>>\n>>    git config replay.refAction print         # Set default\n>>    git replay --ref-action=update --onto main topic  # Override once\n>>\n>> The config and command-line option use the same value names ('update'\n>> and 'print') for consistency and clarity. This makes it immediately\n>> obvious how the config maps to the command-line option, addressing\n>> feedback about the relationship between configuration and command-line\n>> options being clear to users.\n> An implementation details section may make sense if it answers a\n> \"why?\" question, or it explains something counter-intuitive, or it\n> provides high enough level details that it makes the patch easier to\n> read/follow, or it otherwise does something more than just repackage\n> the patch in an alternate format.  I appreciate the attempt to provide\n> these, but I think they simply make the commit message longer without\n> adding value.\n\n\nUnderstood. Will remove the implementation details and configuration \nprecedence table—they're just restating what's in the code.\n\n\n>\n>> Examples:\n>>\n>> $ git config --global replay.refAction print\n>> $ git replay --onto main topic1..topic2 | git update-ref --stdin\n>>\n>> $ git replay --ref-action=update --onto main topic1..topic2\n>>\n>> $ git config replay.refAction update\n>> $ git replay --onto main topic1..topic2  # Updates refs directly\n>>\n>> The implementation follows Git's standard configuration precedence:\n>> command-line options override config values, which matches user\n>> expectations across all Git commands.\n> I don't find the Examples section helpful either; it's yet another\n> re-iteration that we're following conventions.\n\n\nWill remove the Examples section too.\n\n\n>\n>> Helped-by: Junio C Hamano <gitster@pobox.com>\n>> Helped-by: Elijah Newren <newren@gmail.com>\n>> Helped-by: Christian Couder <christian.couder@gmail.com>\n>> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n>> Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n>> ---\n>>   Documentation/config/replay.adoc | 11 ++++++++\n>>   builtin/replay.c                 | 39 ++++++++++++++++++--------\n>>   t/t3650-replay-basics.sh         | 48 +++++++++++++++++++++++++++++++-\n>>   3 files changed, 86 insertions(+), 12 deletions(-)\n>>   create mode 100644 Documentation/config/replay.adoc\n>>\n>> diff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc\n>> new file mode 100644\n>> index 0000000000..7d549d2f0e\n>> --- /dev/null\n>> +++ b/Documentation/config/replay.adoc\n>> @@ -0,0 +1,11 @@\n>> +replay.refAction::\n>> +       Specifies the default mode for handling reference updates in\n>> +       `git replay`. The value can be:\n>> ++\n>> +--\n>> +       * `update`: Update refs directly using an atomic transaction (default behavior).\n>> +       * `print`: Output update-ref commands for pipeline use.\n>> +--\n>> ++\n>> +This setting can be overridden with the `--ref-action` command-line option.\n>> +When not configured, `git replay` defaults to `update` mode.\n>> diff --git a/builtin/replay.c b/builtin/replay.c\n>> index 0564d4d2e7..810068f8ef 100644\n>> --- a/builtin/replay.c\n>> +++ b/builtin/replay.c\n>> @@ -8,6 +8,7 @@\n>>   #include \"git-compat-util.h\"\n>>\n>>   #include \"builtin.h\"\n>> +#include \"config.h\"\n>>   #include \"environment.h\"\n>>   #include \"hex.h\"\n>>   #include \"lockfile.h\"\n>> @@ -289,6 +290,31 @@ static struct commit *pick_regular_commit(struct repository *repo,\n>>          return create_commit(repo, result->tree, pickme, replayed_base);\n>>   }\n>>\n>> +static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n>> +{\n>> +       if (!ref_action || !strcmp(ref_action, \"update\"))\n>> +               return REF_ACTION_UPDATE;\n>> +       if (!strcmp(ref_action, \"print\"))\n>> +               return REF_ACTION_PRINT;\n>> +       die(_(\"invalid %s value: '%s'\"), source, ref_action);\n>> +}\n>> +\n>> +static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action_str)\n>> +{\n>> +       const char *config_value = NULL;\n>> +\n>> +       /* Command line option takes precedence */\n>> +       if (ref_action_str)\n>> +               return parse_ref_action_mode(ref_action_str, \"--ref-action\");\n>> +\n>> +       /* Check config value */\n>> +       if (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n>> +               return parse_ref_action_mode(config_value, \"replay.refAction\");\n>> +\n>> +       /* Default to update mode */\n>> +       return REF_ACTION_UPDATE;\n>> +}\n>> +\n>>   static int handle_ref_update(enum ref_action_mode mode,\n>>                               struct ref_transaction *transaction,\n>>                               const char *refname,\n>> @@ -367,17 +393,8 @@ int cmd_replay(int argc,\n>>          die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>>                                    contained, \"--contained\");\n>>\n>> -       /* Default to update mode if not specified */\n>> -       if (!ref_action_str)\n>> -               ref_action_str = \"update\";\n>> -\n>> -       /* Parse ref action mode */\n>> -       if (!strcmp(ref_action_str, \"update\"))\n>> -               ref_action = REF_ACTION_UPDATE;\n>> -       else if (!strcmp(ref_action_str, \"print\"))\n>> -               ref_action = REF_ACTION_PRINT;\n>> -       else\n>> -               die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n>> +       /* Parse ref action mode from command line or config */\n>> +       ref_action = get_ref_action_mode(repo, ref_action_str);\n>>\n>>          advance_name = xstrdup_or_null(advance_name_opt);\n>>\n>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\n>> index 123734b49f..2e90227c2f 100755\n>> --- a/t/t3650-replay-basics.sh\n>> +++ b/t/t3650-replay-basics.sh\n>> @@ -219,7 +219,8 @@ test_expect_success 'merge.directoryRenames=false' '\n>>\n>>   test_expect_success 'default atomic behavior updates refs directly' '\n>>          # Store original state for cleanup\n>> -       test_when_finished \"git branch -f topic2 topic1\" &&\n>> +       START=$(git rev-parse topic2) &&\n>> +       test_when_finished \"git branch -f topic2 $START\" &&\n> Yes, these three lines are a good fix, but they belong in the previous patch.\n\n\nRight—the START/test_when_finished cleanup fixes should go in commit 2.\n\n\n>\n>>          # Test default atomic behavior (no output, refs updated)\n>>          git replay --onto main topic1..topic2 >output &&\n>> @@ -232,6 +233,10 @@ test_expect_success 'default atomic behavior updates refs directly' '\n>>   '\n>>\n>>   test_expect_success 'atomic behavior in bare repository' '\n>> +       # Store original state for cleanup\n>> +       START=$(git rev-parse topic2) &&\n>> +       test_when_finished \"git branch -f topic2 $START\" &&\n> Yes, these three lines are good but they belong in a separate patch.\n\n\nThe bare repo cleanup fix should also go in commit 2.\n\n\n>> +\n>>          # Test atomic updates work in bare repo\n>>          git -C bare replay --onto main topic1..topic2 >output &&\n>>          test_must_be_empty output &&\n>> @@ -245,4 +250,45 @@ test_expect_success 'atomic behavior in bare repository' '\n>>          git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n> And this line should be removed in the previous patch.\n\n\nWill remove the manual reset line from commit 2.\n\nThanks for pointing out which fixes belong where!\n\n\n>\n>>   '\n>>\n>> +test_expect_success 'replay.refAction config option' '\n>> +       # Store original state\n>> +       START=$(git rev-parse topic2) &&\n>> +       test_when_finished \"git branch -f topic2 $START\" &&\n>> +\n>> +       # Set config to print\n>> +       test_config replay.refAction print &&\n>> +       git replay --onto main topic1..topic2 >output &&\n>> +       test_line_count = 1 output &&\n>> +       test_grep \"^update refs/heads/topic2 \" output &&\n>> +\n>> +       # Reset and test update mode\n>> +       git branch -f topic2 $START &&\n>> +       test_config replay.refAction update &&\n>> +       git replay --onto main topic1..topic2 >output &&\n>> +       test_must_be_empty output &&\n>> +\n>> +       # Verify ref was updated\n>> +       git log --format=%s topic2 >actual &&\n>> +       test_write_lines E D M L B A >expect &&\n>> +       test_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'command-line --ref-action overrides config' '\n>> +       # Store original state\n>> +       START=$(git rev-parse topic2) &&\n>> +       test_when_finished \"git branch -f topic2 $START\" &&\n>> +\n>> +       # Set config to update but use --ref-action=print\n>> +       test_config replay.refAction update &&\n>> +       git replay --ref-action=print --onto main topic1..topic2 >output &&\n>> +       test_line_count = 1 output &&\n>> +       test_grep \"^update refs/heads/topic2 \" output\n>> +'\n>> +\n>> +test_expect_success 'invalid replay.refAction value' '\n>> +       test_config replay.refAction invalid &&\n>> +       test_must_fail git replay --onto main topic1..topic2 2>error &&\n>> +       test_grep \"invalid.*replay.refAction.*value\" error\n>> +'\n>> +\n>>   test_done\n>> --\n>> 2.51.0\n> Looks good otherwise.\n\n\nThanks for careful review!\n\n"},{"id":"530264","messageId":"20251105191650.89975-1-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251030191931.30837-1-siddharthasthana31@gmail.com","subject":"[PATCH v7 0/3] replay: make atomic ref updates the default","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-05T19:15:58Z","receivedAt":"2025-11-05T19:17:05Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"This is v7 of the git-replay atomic updates series.\n\nThis version addresses all feedback from v6 reviews. Thanks to Elijah,\nChristian, and Phillip for the thorough reviews that helped refine the\nimplementation to Git standards.\n\n## Changes in v7\n\n**Improved commit message clarity**\n\nPer Elijah's feedback, simplified commit messages by removing redundant\nsections:\n  - Removed \"Implementation details\" section (details visible in diff)\n  - Shortened \"Test suite changes\" to focus on what's tested\n  - Removed command-line precedence paragraph (obvious from code)\n  - Removed \"Examples\" and configuration precedence sections\n\n**Fixed test cleanup and isolation**\n\nFollowing Elijah's suggestions:\n  - Used test_when_finished with proper state restoration in atomic tests\n  - Created separate test-atomic branch to avoid contaminating topic2\n  - Fixed bare repository test to use START variable for cleanup\n  - Improved test reliability by rebuilding expectations independently\n\n**Extracted parse_ref_action_mode() to appropriate commit**\n\nPer Christian's observation, moved the parse_ref_action_mode() helper\nfunction from Commit 3 to Commit 2 where it's first used. This makes\nthe patch progression more logical.\n\n**Fixed parameter naming consistency**\n\nFollowing Christian's feedback, used consistent naming throughout:\n  - ref_action (string parameter for command-line/config value)\n  - ref_mode (enum variable for internal mode)\nThis eliminates confusion and improves code readability.\n\n**Moved config reference to correct commit**\n\nPer Elijah's note, moved the sentence about replay.refAction config\nfrom Commit 2's documentation to Commit 3 where the config is actually\nintroduced.\n\n**Enhanced reflog messages**\n\nFollowing Phillip's suggestions for better user experience:\n  - --advance mode: \"replay --advance <branch-name>\" (uses user input)\n  - --onto mode: \"replay --onto <commit-sha>\" (precise commit reference)\nAdded comprehensive reflog testing to verify messages.\n\n**Fixed indentation in Commit 3**\n\nCorrected indentation within the while (decoration) loop per CI\nfeedback, adding proper tabs to nested if statements.\n\n**Fixed coding style**\n\nPer CI check-style feedback, removed braces from single-statement\nif-else blocks following Git's CodingGuidelines.\n\n**Split config tests for clarity**\n\nSeparated the replay.refAction config test into two distinct tests:\n  - replay.refAction=print config option\n  - replay.refAction=update config option\nThis improves test clarity and makes failures easier to diagnose.\n\n## Technical Implementation\n\nThe atomic ref updates leverage Git's ref transaction API:\n  - ref_store_transaction_begin() with default atomic behavior\n  - ref_transaction_update() to stage each update\n  - ref_transaction_commit() for atomic application\n\nThe helper functions provide clean separation:\n  - parse_ref_action_mode(): Validates strings and converts to enum\n  - get_ref_action_mode(): Implements command-line > config > default precedence\n  - handle_ref_update(): Uses type-safe enum with switch statement\n\nReflog messages are constructed dynamically based on replay mode and\ninclude either the branch name (--advance) or commit SHA (--onto) for\nclear audit trails.\n\n## Testing\n\nAll tests pass:\n  - t3650-replay-basics.sh (22 tests pass)\n  - Config tests verify proper precedence and error handling\n  - Atomic behavior tests verify direct ref updates\n  - Reflog tests verify descriptive messages\n  - Backward compatibility maintained for pipeline workflow\n\nCI results: https://gitlab.com/gitlab-org/git/-/pipelines/2140425748\n\nSiddharth Asthana (3):\n  replay: use die_for_incompatible_opt2() for option validation\n  replay: make atomic ref updates the default behavior\n  replay: add replay.refAction config option\n\n Documentation/config/replay.adoc |  11 +++\n Documentation/git-replay.adoc    |  63 ++++++++++-----\n builtin/replay.c                 | 133 ++++++++++++++++++++++++++++---\n t/t3650-replay-basics.sh         | 113 ++++++++++++++++++++++++--\n 4 files changed, 277 insertions(+), 43 deletions(-)\n create mode 100644 Documentation/config/replay.adoc\n\nRange-diff against v6:\n1:  1f0fad0cac = 1:  9e4eab2df2 replay: use die_for_incompatible_opt2() for option validation\n2:  bfc6188234 ! 2:  1602f6097e replay: make atomic ref updates the default behavior\n    @@ Commit message\n          * update (default): Update refs directly using an atomic transaction\n          * print: Output update-ref commands for pipeline use\n     \n    -    Implementation details:\n    -\n    -    The atomic ref updates are implemented using Git's ref transaction API.\n    -    In cmd_replay(), when not in `print` mode, we initialize a transaction\n    -    using ref_store_transaction_begin() with the default atomic behavior.\n    -    As commits are replayed, ref updates are staged into the transaction\n    -    using ref_transaction_update(). Finally, ref_transaction_commit()\n    -    applies all updates atomically—either all updates succeed or none do.\n    -\n    -    To avoid code duplication between the 'print' and 'update' modes, this\n    -    commit extracts a handle_ref_update() helper function. This function\n    -    takes the mode (as an enum) and either prints the update command or\n    -    stages it into the transaction. Using an enum rather than passing the\n    -    string around provides type safety and allows the compiler to catch\n    -    typos. The switch statement makes it easy to add future modes.\n    -\n    -    The helper function signature:\n    -\n    -      static int handle_ref_update(enum ref_action_mode mode,\n    -                                    struct ref_transaction *transaction,\n    -                                    const char *refname,\n    -                                    const struct object_id *new_oid,\n    -                                    const struct object_id *old_oid,\n    -                                    struct strbuf *err)\n    -\n    -    The enum is defined as:\n    -\n    -      enum ref_action_mode {\n    -          REF_ACTION_UPDATE,\n    -          REF_ACTION_PRINT\n    -      };\n    -\n    -    The mode string is converted to enum immediately after parse_options()\n    -    to avoid string comparisons throughout the codebase and provide compiler\n    -    protection against typos.\n    -\n         Test suite changes:\n     \n         All existing tests that expected command output now use\n    @@ Commit message\n          - Equivalence between traditional pipeline and atomic updates\n          - Real atomicity using a lock file to verify all-or-nothing guarantee\n          - Test isolation using test_when_finished to clean up state\n    -\n    -    The bare repository tests were fixed to rebuild their expectations\n    -    independently rather than comparing to previous test output, improving\n    -    test reliability and isolation.\n    +      - Reflog messages include replay mode and target\n     \n         A following commit will add a replay.refAction configuration\n         option for users who prefer the traditional pipeline output as their\n    @@ Documentation/git-replay.adoc: OPTIONS\n     -commits, similar to the way how `git rebase --update-refs` updates\n     -multiple branches in the affected range.\n     +When `--onto` is specified, the branch(es) in the revision range will be\n    -+updated to point at the new commits (or update commands will be printed\n    -+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`\n    ++updated to point at the new commits, similar to the way `git rebase --update-refs`\n     +updates multiple branches in the affected range.\n      \n      --advance <branch>::\n    @@ Documentation/git-replay.adoc: OPTIONS\n     -will update the branch passed as an argument to `--advance` to point at\n     -the new commits (in other words, this mimics a cherry-pick operation).\n     +The history is replayed on top of the <branch> and <branch> is updated to\n    -+point at the tip of the resulting history (or an update command will be\n    -+printed if `--ref-action=print` is used). This is different from `--onto`,\n    ++point at the tip of the resulting history. This is different from `--onto`,\n     +which uses the target only as a starting point without updating it.\n     +\n     +--ref-action[=<mode>]::\n    @@ Documentation/git-replay.adoc: OPTIONS\n     +\t* `print`: Output update-ref commands for pipeline use. This is the\n     +\t  traditional behavior where output can be piped to `git update-ref --stdin`.\n     +--\n    -++\n    -+The default mode can be configured via the `replay.refAction` configuration variable.\n      \n      <revision-range>::\n      \tRange of commits to replay. More than one <revision-range> can\n    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r\n      \treturn create_commit(repo, result->tree, pickme, replayed_base);\n      }\n      \n    ++static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n    ++{\n    ++\tif (!ref_action || !strcmp(ref_action, \"update\"))\n    ++\t\treturn REF_ACTION_UPDATE;\n    ++\tif (!strcmp(ref_action, \"print\"))\n    ++\t\treturn REF_ACTION_PRINT;\n    ++\tdie(_(\"invalid %s value: '%s'\"), source, ref_action);\n    ++}\n    ++\n     +static int handle_ref_update(enum ref_action_mode mode,\n     +\t\t\t     struct ref_transaction *transaction,\n     +\t\t\t     const char *refname,\n     +\t\t\t     const struct object_id *new_oid,\n     +\t\t\t     const struct object_id *old_oid,\n    ++\t\t\t     const char *reflog_msg,\n     +\t\t\t     struct strbuf *err)\n     +{\n     +\tswitch (mode) {\n    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r\n     +\t\treturn 0;\n     +\tcase REF_ACTION_UPDATE:\n     +\t\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n    -+\t\t\t\t\t      NULL, NULL, 0, \"git replay\", err);\n    ++\t\t\t\t\t      NULL, NULL, 0, reflog_msg, err);\n     +\tdefault:\n     +\t\tBUG(\"unknown ref_action_mode %d\", mode);\n     +\t}\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \tstruct commit *onto = NULL;\n      \tconst char *onto_name = NULL;\n      \tint contained = 0;\n    -+\tconst char *ref_action_str = NULL;\n    -+\tenum ref_action_mode ref_action = REF_ACTION_UPDATE;\n    ++\tconst char *ref_action = NULL;\n    ++\tenum ref_action_mode ref_mode = REF_ACTION_UPDATE;\n      \n      \tstruct rev_info revs;\n      \tstruct commit *last_commit = NULL;\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \tkh_oid_map_t *replayed_commits;\n     +\tstruct ref_transaction *transaction = NULL;\n     +\tstruct strbuf transaction_err = STRBUF_INIT;\n    ++\tstruct strbuf reflog_msg = STRBUF_INIT;\n      \tint ret = 0;\n      \n     -\tconst char * const replay_usage[] = {\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\t\t   N_(\"replay onto given commit\")),\n      \t\tOPT_BOOL(0, \"contained\", &contained,\n      \t\t\t N_(\"advance all branches contained in revision-range\")),\n    -+\t\tOPT_STRING(0, \"ref-action\", &ref_action_str,\n    ++\t\tOPT_STRING(0, \"ref-action\", &ref_action,\n     +\t\t\t   N_(\"mode\"),\n     +\t\t\t   N_(\"control ref update behavior (update|print)\")),\n      \t\tOPT_END()\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n      \t\t\t\t  contained, \"--contained\");\n      \n    -+\t/* Default to update mode if not specified */\n    -+\tif (!ref_action_str)\n    -+\t\tref_action_str = \"update\";\n    -+\n    -+\t/* Validate ref-action mode */\n    -+\tif (!strcmp(ref_action_str, \"update\"))\n    -+\t\tref_action = REF_ACTION_UPDATE;\n    -+\telse if (!strcmp(ref_action_str, \"print\"))\n    -+\t\tref_action = REF_ACTION_PRINT;\n    -+\telse\n    -+\t\tdie(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n    ++\t/* Parse ref action mode */\n    ++\tif (ref_action)\n    ++\t\tref_mode = parse_ref_action_mode(ref_action, \"--ref-action\");\n     +\n      \tadvance_name = xstrdup_or_null(advance_name_opt);\n      \n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n      \t\t\t      &onto, &update_refs);\n      \n    ++\t/* Build reflog message */\n    ++\tif (advance_name_opt)\n    ++\t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", advance_name_opt);\n    ++\telse\n    ++\t\tstrbuf_addf(&reflog_msg, \"replay --onto %s\",\n    ++\t\t\t    oid_to_hex(&onto->object.oid));\n    ++\n     +\t/* Initialize ref transaction if using update mode */\n    -+\tif (ref_action == REF_ACTION_UPDATE) {\n    ++\tif (ref_mode == REF_ACTION_UPDATE) {\n     +\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n     +\t\t\t\t\t\t\t  0, &transaction_err);\n     +\t\tif (!transaction) {\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     -\t\t\t\t       decoration->name,\n     -\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n     -\t\t\t\t       oid_to_hex(&commit->object.oid));\n    -+\t\t\t\tif (handle_ref_update(ref_action, transaction,\n    ++\t\t\t\tif (handle_ref_update(ref_mode, transaction,\n     +\t\t\t\t\t\t      decoration->name,\n     +\t\t\t\t\t\t      &last_commit->object.oid,\n     +\t\t\t\t\t\t      &commit->object.oid,\n    ++\t\t\t\t\t\t      reflog_msg.buf,\n     +\t\t\t\t\t\t      &transaction_err) < 0) {\n     +\t\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n     +\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     -\t\t       advance_name,\n     -\t\t       oid_to_hex(&last_commit->object.oid),\n     -\t\t       oid_to_hex(&onto->object.oid));\n    -+\t\tif (handle_ref_update(ref_action, transaction, advance_name,\n    ++\t\tif (handle_ref_update(ref_mode, transaction, advance_name,\n     +\t\t\t\t      &last_commit->object.oid,\n     +\t\t\t\t      &onto->object.oid,\n    ++\t\t\t\t      reflog_msg.buf,\n     +\t\t\t\t      &transaction_err) < 0) {\n     +\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n     +\t\t\t\t    advance_name, transaction_err.buf);\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     +\tif (transaction)\n     +\t\tref_transaction_free(transaction);\n     +\tstrbuf_release(&transaction_err);\n    ++\tstrbuf_release(&reflog_msg);\n      \trelease_revisions(&revs);\n      \tfree(advance_name);\n      \n    @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '\n      '\n      \n     +test_expect_success 'default atomic behavior updates refs directly' '\n    -+\t# Store original state for cleanup\n    -+\ttest_when_finished \"git branch -f topic2 topic1\" &&\n    ++\t# Use a separate branch to avoid contaminating topic2 for later tests\n    ++\tgit branch test-atomic topic2 &&\n    ++\ttest_when_finished \"git branch -D test-atomic\" &&\n     +\n     +\t# Test default atomic behavior (no output, refs updated)\n    -+\tgit replay --onto main topic1..topic2 >output &&\n    ++\tgit replay --onto main topic1..test-atomic >output &&\n     +\ttest_must_be_empty output &&\n     +\n     +\t# Verify ref was updated\n    -+\tgit log --format=%s topic2 >actual &&\n    ++\tgit log --format=%s test-atomic >actual &&\n     +\ttest_write_lines E D M L B A >expect &&\n    -+\ttest_cmp expect actual\n    ++\ttest_cmp expect actual &&\n    ++\n    ++\t# Verify reflog message includes SHA of onto commit\n    ++\tgit reflog test-atomic -1 --format=%gs >reflog-msg &&\n    ++\tONTO_SHA=$(git rev-parse main) &&\n    ++\techo \"replay --onto $ONTO_SHA\" >expect-reflog &&\n    ++\ttest_cmp expect-reflog reflog-msg\n     +'\n     +\n     +test_expect_success 'atomic behavior in bare repository' '\n    ++\t# Store original state for cleanup\n    ++\tSTART=$(git -C bare rev-parse topic2) &&\n    ++\ttest_when_finished \"git -C bare update-ref refs/heads/topic2 $START\" &&\n    ++\n     +\t# Test atomic updates work in bare repo\n     +\tgit -C bare replay --onto main topic1..topic2 >output &&\n     +\ttest_must_be_empty output &&\n    @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '\n     +\t# Verify ref was updated in bare repo\n     +\tgit -C bare log --format=%s topic2 >actual &&\n     +\ttest_write_lines E D M L B A >expect &&\n    -+\ttest_cmp expect actual &&\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n    ++test_expect_success 'reflog message for --advance mode' '\n    ++\t# Store original state\n    ++\tSTART=$(git rev-parse main) &&\n    ++\ttest_when_finished \"git update-ref refs/heads/main $START\" &&\n    ++\n    ++\t# Test --advance mode reflog message\n    ++\tgit replay --advance main topic1..topic2 >output &&\n    ++\ttest_must_be_empty output &&\n     +\n    -+\t# Reset for other tests\n    -+\tgit -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n    ++\t# Verify reflog message includes --advance and branch name\n    ++\tgit reflog main -1 --format=%gs >reflog-msg &&\n    ++\techo \"replay --advance main\" >expect-reflog &&\n    ++\ttest_cmp expect-reflog reflog-msg\n     +'\n     +\n      test_done\n-:  ---------- > 3:  b7ebe1f534 replay: add replay.refAction config option\n\n-- \n2.51.0\n\nbase-commit: a99f379adf8a0b4c7c4f8f0b2e5e6e7e8e9e0e1e\n\nThanks\n- Siddharth\n"},{"id":"530265","messageId":"20251105191650.89975-2-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251105191650.89975-1-siddharthasthana31@gmail.com","subject":"[PATCH v7 1/3] replay: use die_for_incompatible_opt2() for option validation","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-05T19:15:59Z","receivedAt":"2025-11-05T19:17:14Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"In preparation for adding the --ref-action option, convert option\nvalidation to use die_for_incompatible_opt2(). This helper provides\nstandardized error messages for mutually exclusive options.\n\nThe following commit introduces --ref-action which will be incompatible\nwith certain other options. Using die_for_incompatible_opt2() now means\nthat commit can cleanly add its validation using the same pattern,\nkeeping the validation logic consistent and maintainable.\n\nThis also aligns git-replay's option handling with how other Git commands\nmanage option conflicts, using the established die_for_incompatible_opt*()\nhelper family.\n\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n builtin/replay.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 6172c8aacc..b64fc72063 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -330,9 +330,9 @@ int cmd_replay(int argc,\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n-\tif (advance_name_opt && contained)\n-\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n-\t\t    \"--advance\", \"--contained\");\n+\tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n+\t\t\t\t  contained, \"--contained\");\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n-- \n2.51.0\n\n"},{"id":"530266","messageId":"20251105191650.89975-3-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251105191650.89975-1-siddharthasthana31@gmail.com","subject":"[PATCH v7 2/3] replay: make atomic ref updates the default behavior","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-05T19:16:00Z","receivedAt":"2025-11-05T19:17:22Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"The git replay command currently outputs update commands that can be\npiped to update-ref to achieve a rebase, e.g.\n\n  git replay --onto main topic1..topic2 | git update-ref --stdin\n\nThis separation had advantages for three special cases:\n  * it made testing easy (when state isn't modified from one step to\n    the next, you don't need to make temporary branches or have undo\n    commands, or try to track the changes)\n  * it provided a natural can-it-rebase-cleanly (and what would it\n    rebase to) capability without automatically updating refs, similar\n    to a --dry-run\n  * it provided a natural low-level tool for the suite of hash-object,\n    mktree, commit-tree, mktag, merge-tree, and update-ref, allowing\n    users to have another building block for experimentation and making\n    new tools\n\nHowever, it should be noted that all three of these are somewhat\nspecial cases; users, whether on the client or server side, would\nalmost certainly find it more ergonomic to simply have the updating\nof refs be the default.\n\nFor server-side operations in particular, the pipeline architecture\ncreates process coordination overhead. Server implementations that need\nto perform rebases atomically must maintain additional code to:\n\n  1. Spawn and manage a pipeline between git-replay and git-update-ref\n  2. Coordinate stdout/stderr streams across the pipe boundary\n  3. Handle partial failure states if the pipeline breaks mid-execution\n  4. Parse and validate the update-ref command output\n\nChange the default behavior to update refs directly, and atomically (at\nleast to the extent supported by the refs backend in use). This\neliminates the process coordination overhead for the common case.\n\nFor users needing the traditional pipeline workflow, add a new\n--ref-action=<mode> option that preserves the original behavior:\n\n  git replay --ref-action=print --onto main topic1..topic2 | git update-ref --stdin\n\nThe mode can be:\n  * update (default): Update refs directly using an atomic transaction\n  * print: Output update-ref commands for pipeline use\n\nTest suite changes:\n\nAll existing tests that expected command output now use\n--ref-action=print to preserve their original behavior. This keeps\nthe tests valid while allowing them to verify that the pipeline workflow\nstill works correctly.\n\nNew tests were added to verify:\n  - Default atomic behavior (no output, refs updated directly)\n  - Bare repository support (server-side use case)\n  - Equivalence between traditional pipeline and atomic updates\n  - Real atomicity using a lock file to verify all-or-nothing guarantee\n  - Test isolation using test_when_finished to clean up state\n  - Reflog messages include replay mode and target\n\nA following commit will add a replay.refAction configuration\noption for users who prefer the traditional pipeline output as their\ndefault behavior.\n\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/git-replay.adoc |  61 ++++++++++++-------\n builtin/replay.c              | 111 +++++++++++++++++++++++++++++++---\n t/t3650-replay-basics.sh      |  67 +++++++++++++++++---\n 3 files changed, 199 insertions(+), 40 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 0b12bf8aa4..2ef74ddb12 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,15 +9,16 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch>) [--ref-action[=<mode>]] <revision-range>...\n \n DESCRIPTION\n -----------\n \n Takes ranges of commits and replays them onto a new location. Leaves\n-the working tree and the index untouched, and updates no references.\n-The output of this command is meant to be used as input to\n-`git update-ref --stdin`, which would update the relevant branches\n+the working tree and the index untouched. By default, updates the\n+relevant references using an atomic transaction (all refs update or\n+none). Use `--ref-action=print` to avoid automatic ref updates and\n+instead get update commands that can be piped to `git update-ref --stdin`\n (see the OUTPUT section below).\n \n THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n@@ -29,18 +30,27 @@ OPTIONS\n \tStarting point at which to create the new commits.  May be any\n \tvalid commit, and not just an existing branch name.\n +\n-When `--onto` is specified, the update-ref command(s) in the output will\n-update the branch(es) in the revision range to point at the new\n-commits, similar to the way how `git rebase --update-refs` updates\n-multiple branches in the affected range.\n+When `--onto` is specified, the branch(es) in the revision range will be\n+updated to point at the new commits, similar to the way `git rebase --update-refs`\n+updates multiple branches in the affected range.\n \n --advance <branch>::\n \tStarting point at which to create the new commits; must be a\n \tbranch name.\n +\n-When `--advance` is specified, the update-ref command(s) in the output\n-will update the branch passed as an argument to `--advance` to point at\n-the new commits (in other words, this mimics a cherry-pick operation).\n+The history is replayed on top of the <branch> and <branch> is updated to\n+point at the tip of the resulting history. This is different from `--onto`,\n+which uses the target only as a starting point without updating it.\n+\n+--ref-action[=<mode>]::\n+\tControl how references are updated. The mode can be:\n++\n+--\n+\t* `update` (default): Update refs directly using an atomic transaction.\n+\t  All refs are updated or none are (all-or-nothing behavior).\n+\t* `print`: Output update-ref commands for pipeline use. This is the\n+\t  traditional behavior where output can be piped to `git update-ref --stdin`.\n+--\n \n <revision-range>::\n \tRange of commits to replay. More than one <revision-range> can\n@@ -54,8 +64,11 @@ include::rev-list-options.adoc[]\n OUTPUT\n ------\n \n-When there are no conflicts, the output of this command is usable as\n-input to `git update-ref --stdin`.  It is of the form:\n+By default, or with `--ref-action=update`, this command produces no output on\n+success, as refs are updated directly using an atomic transaction.\n+\n+When using `--ref-action=print`, the output is usable as input to\n+`git update-ref --stdin`. It is of the form:\n \n \tupdate refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n \tupdate refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n@@ -81,6 +94,14 @@ To simply rebase `mybranch` onto `target`:\n \n ------------\n $ git replay --onto target origin/main..mybranch\n+------------\n+\n+The refs are updated atomically and no output is produced on success.\n+\n+To see what would be updated without actually updating:\n+\n+------------\n+$ git replay --ref-action=print --onto target origin/main..mybranch\n update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n ------------\n \n@@ -88,33 +109,29 @@ To cherry-pick the commits from mybranch onto target:\n \n ------------\n $ git replay --advance target origin/main..mybranch\n-update refs/heads/target ${NEW_target_HASH} ${OLD_target_HASH}\n ------------\n \n Note that the first two examples replay the exact same commits and on\n top of the exact same new base, they only differ in that the first\n-provides instructions to make mybranch point at the new commits and\n-the second provides instructions to make target point at them.\n+updates mybranch to point at the new commits and the second updates\n+target to point at them.\n \n What if you have a stack of branches, one depending upon another, and\n you'd really like to rebase the whole set?\n \n ------------\n $ git replay --contained --onto origin/main origin/main..tipbranch\n-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n-update refs/heads/tipbranch ${NEW_tipbranch_HASH} ${OLD_tipbranch_HASH}\n ------------\n \n+All three branches (`branch1`, `branch2`, and `tipbranch`) are updated\n+atomically.\n+\n When calling `git replay`, one does not need to specify a range of\n commits to replay using the syntax `A..B`; any range expression will\n do:\n \n ------------\n $ git replay --onto origin/main ^base branch1 branch2 branch3\n-update refs/heads/branch1 ${NEW_branch1_HASH} ${OLD_branch1_HASH}\n-update refs/heads/branch2 ${NEW_branch2_HASH} ${OLD_branch2_HASH}\n-update refs/heads/branch3 ${NEW_branch3_HASH} ${OLD_branch3_HASH}\n ------------\n \n This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex b64fc72063..94e60b5b10 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -20,6 +20,11 @@\n #include <oidset.h>\n #include <tree.h>\n \n+enum ref_action_mode {\n+\tREF_ACTION_UPDATE,\n+\tREF_ACTION_PRINT,\n+};\n+\n static const char *short_commit_name(struct repository *repo,\n \t\t\t\t     struct commit *commit)\n {\n@@ -284,6 +289,38 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \treturn create_commit(repo, result->tree, pickme, replayed_base);\n }\n \n+static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n+{\n+\tif (!ref_action || !strcmp(ref_action, \"update\"))\n+\t\treturn REF_ACTION_UPDATE;\n+\tif (!strcmp(ref_action, \"print\"))\n+\t\treturn REF_ACTION_PRINT;\n+\tdie(_(\"invalid %s value: '%s'\"), source, ref_action);\n+}\n+\n+static int handle_ref_update(enum ref_action_mode mode,\n+\t\t\t     struct ref_transaction *transaction,\n+\t\t\t     const char *refname,\n+\t\t\t     const struct object_id *new_oid,\n+\t\t\t     const struct object_id *old_oid,\n+\t\t\t     const char *reflog_msg,\n+\t\t\t     struct strbuf *err)\n+{\n+\tswitch (mode) {\n+\tcase REF_ACTION_PRINT:\n+\t\tprintf(\"update %s %s %s\\n\",\n+\t\t       refname,\n+\t\t       oid_to_hex(new_oid),\n+\t\t       oid_to_hex(old_oid));\n+\t\treturn 0;\n+\tcase REF_ACTION_UPDATE:\n+\t\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n+\t\t\t\t\t      NULL, NULL, 0, reflog_msg, err);\n+\tdefault:\n+\t\tBUG(\"unknown ref_action_mode %d\", mode);\n+\t}\n+}\n+\n int cmd_replay(int argc,\n \t       const char **argv,\n \t       const char *prefix,\n@@ -294,6 +331,8 @@ int cmd_replay(int argc,\n \tstruct commit *onto = NULL;\n \tconst char *onto_name = NULL;\n \tint contained = 0;\n+\tconst char *ref_action = NULL;\n+\tenum ref_action_mode ref_mode = REF_ACTION_UPDATE;\n \n \tstruct rev_info revs;\n \tstruct commit *last_commit = NULL;\n@@ -302,12 +341,15 @@ int cmd_replay(int argc,\n \tstruct merge_result result;\n \tstruct strset *update_refs = NULL;\n \tkh_oid_map_t *replayed_commits;\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strbuf transaction_err = STRBUF_INIT;\n+\tstruct strbuf reflog_msg = STRBUF_INIT;\n \tint ret = 0;\n \n-\tconst char * const replay_usage[] = {\n+\tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto <newbase> | --advance <branch>) \"\n-\t\t   \"<revision-range>...\"),\n+\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -319,6 +361,9 @@ int cmd_replay(int argc,\n \t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &contained,\n \t\t\t N_(\"advance all branches contained in revision-range\")),\n+\t\tOPT_STRING(0, \"ref-action\", &ref_action,\n+\t\t\t   N_(\"mode\"),\n+\t\t\t   N_(\"control ref update behavior (update|print)\")),\n \t\tOPT_END()\n \t};\n \n@@ -333,6 +378,10 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n+\t/* Parse ref action mode */\n+\tif (ref_action)\n+\t\tref_mode = parse_ref_action_mode(ref_action, \"--ref-action\");\n+\n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \n \trepo_init_revisions(repo, &revs, prefix);\n@@ -389,6 +438,24 @@ int cmd_replay(int argc,\n \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n \t\t\t      &onto, &update_refs);\n \n+\t/* Build reflog message */\n+\tif (advance_name_opt)\n+\t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", advance_name_opt);\n+\telse\n+\t\tstrbuf_addf(&reflog_msg, \"replay --onto %s\",\n+\t\t\t    oid_to_hex(&onto->object.oid));\n+\n+\t/* Initialize ref transaction if using update mode */\n+\tif (ref_mode == REF_ACTION_UPDATE) {\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo),\n+\t\t\t\t\t\t\t  0, &transaction_err);\n+\t\tif (!transaction) {\n+\t\t\tret = error(_(\"failed to begin ref transaction: %s\"),\n+\t\t\t\t    transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n \tif (!onto) /* FIXME: Should handle replaying down to root commit */\n \t\tdie(\"Replaying down to root commit is not supported yet!\");\n \n@@ -434,10 +501,16 @@ int cmd_replay(int argc,\n \t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n \t\t\t    (contained || strset_contains(update_refs,\n \t\t\t\t\t\t\t  decoration->name))) {\n-\t\t\t\tprintf(\"update %s %s %s\\n\",\n-\t\t\t\t       decoration->name,\n-\t\t\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t\t\t       oid_to_hex(&commit->object.oid));\n+\t\t\t\tif (handle_ref_update(ref_mode, transaction,\n+\t\t\t\t\t\t      decoration->name,\n+\t\t\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t\t\t      &commit->object.oid,\n+\t\t\t\t\t\t      reflog_msg.buf,\n+\t\t\t\t\t\t      &transaction_err) < 0) {\n+\t\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n+\t\t\t\t\tgoto cleanup;\n+\t\t\t\t}\n \t\t\t}\n \t\t\tdecoration = decoration->next;\n \t\t}\n@@ -445,10 +518,24 @@ int cmd_replay(int argc,\n \n \t/* In --advance mode, advance the target ref */\n \tif (result.clean == 1 && advance_name) {\n-\t\tprintf(\"update %s %s %s\\n\",\n-\t\t       advance_name,\n-\t\t       oid_to_hex(&last_commit->object.oid),\n-\t\t       oid_to_hex(&onto->object.oid));\n+\t\tif (handle_ref_update(ref_mode, transaction, advance_name,\n+\t\t\t\t      &last_commit->object.oid,\n+\t\t\t\t      &onto->object.oid,\n+\t\t\t\t      reflog_msg.buf,\n+\t\t\t\t      &transaction_err) < 0) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    advance_name, transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n+\t/* Commit the ref transaction if we have one */\n+\tif (transaction && result.clean == 1) {\n+\t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n+\t\t\tret = error(_(\"failed to commit ref transaction: %s\"),\n+\t\t\t\t    transaction_err.buf);\n+\t\t\tgoto cleanup;\n+\t\t}\n \t}\n \n \tmerge_finalize(&merge_opt, &result);\n@@ -460,6 +547,10 @@ int cmd_replay(int argc,\n \tret = result.clean;\n \n cleanup:\n+\tif (transaction)\n+\t\tref_transaction_free(transaction);\n+\tstrbuf_release(&transaction_err);\n+\tstrbuf_release(&reflog_msg);\n \trelease_revisions(&revs);\n \tfree(advance_name);\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 58b3759935..ec79234c80 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -52,7 +52,7 @@ test_expect_success 'setup bare' '\n '\n \n test_expect_success 'using replay to rebase two branches, one on top of other' '\n-\tgit replay --onto main topic1..topic2 >result &&\n+\tgit replay --ref-action=print --onto main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -68,7 +68,7 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n '\n \n test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n-\tgit -C bare replay --onto main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -86,7 +86,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n \t# 4th field of result is hash for main instead of hash for topic2\n \n-\tgit replay --advance main topic1..topic2 >result &&\n+\tgit replay --ref-action=print --advance main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -102,7 +102,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n '\n \n test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n-\tgit -C bare replay --advance main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -115,7 +115,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n '\n \n test_expect_success 'using replay to also rebase a contained branch' '\n-\tgit replay --contained --onto main main..topic3 >result &&\n+\tgit replay --ref-action=print --contained --onto main main..topic3 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -139,12 +139,12 @@ test_expect_success 'using replay to also rebase a contained branch' '\n '\n \n test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n-\tgit -C bare replay --contained --onto main main..topic3 >result-bare &&\n+\tgit -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n test_expect_success 'using replay to rebase multiple divergent branches' '\n-\tgit replay --onto main ^topic1 topic2 topic4 >result &&\n+\tgit replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -168,7 +168,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n '\n \n test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n-\tgit -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result &&\n+\tgit -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n \n \ttest_line_count = 4 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -217,4 +217,55 @@ test_expect_success 'merge.directoryRenames=false' '\n \t\t--onto rename-onto rename-onto..rename-from\n '\n \n+test_expect_success 'default atomic behavior updates refs directly' '\n+\t# Use a separate branch to avoid contaminating topic2 for later tests\n+\tgit branch test-atomic topic2 &&\n+\ttest_when_finished \"git branch -D test-atomic\" &&\n+\n+\t# Test default atomic behavior (no output, refs updated)\n+\tgit replay --onto main topic1..test-atomic >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated\n+\tgit log --format=%s test-atomic >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual &&\n+\n+\t# Verify reflog message includes SHA of onto commit\n+\tgit reflog test-atomic -1 --format=%gs >reflog-msg &&\n+\tONTO_SHA=$(git rev-parse main) &&\n+\techo \"replay --onto $ONTO_SHA\" >expect-reflog &&\n+\ttest_cmp expect-reflog reflog-msg\n+'\n+\n+test_expect_success 'atomic behavior in bare repository' '\n+\t# Store original state for cleanup\n+\tSTART=$(git -C bare rev-parse topic2) &&\n+\ttest_when_finished \"git -C bare update-ref refs/heads/topic2 $START\" &&\n+\n+\t# Test atomic updates work in bare repo\n+\tgit -C bare replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated in bare repo\n+\tgit -C bare log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'reflog message for --advance mode' '\n+\t# Store original state\n+\tSTART=$(git rev-parse main) &&\n+\ttest_when_finished \"git update-ref refs/heads/main $START\" &&\n+\n+\t# Test --advance mode reflog message\n+\tgit replay --advance main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify reflog message includes --advance and branch name\n+\tgit reflog main -1 --format=%gs >reflog-msg &&\n+\techo \"replay --advance main\" >expect-reflog &&\n+\ttest_cmp expect-reflog reflog-msg\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"530267","messageId":"20251105191650.89975-4-siddharthasthana31@gmail.com","threadId":"64109","inReplyTo":"20251105191650.89975-1-siddharthasthana31@gmail.com","subject":"[PATCH v7 3/3] replay: add replay.refAction config option","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-05T19:16:01Z","receivedAt":"2025-11-05T19:17:30Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"Add a configuration variable to control the default behavior of git replay\nfor updating references. This allows users who prefer the traditional\npipeline output to set it once in their config instead of passing\n--ref-action=print with every command.\n\nThe config variable uses string values that mirror the behavior modes:\n  * replay.refAction = update (default): atomic ref updates\n  * replay.refAction = print: output commands for pipeline\n\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Elijah Newren <newren@gmail.com>\nHelped-by: Christian Couder <christian.couder@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n---\n Documentation/config/replay.adoc | 11 ++++++++\n Documentation/git-replay.adoc    |  2 ++\n builtin/replay.c                 | 24 ++++++++++++++---\n t/t3650-replay-basics.sh         | 46 ++++++++++++++++++++++++++++++++\n 4 files changed, 79 insertions(+), 4 deletions(-)\n create mode 100644 Documentation/config/replay.adoc\n\ndiff --git a/Documentation/config/replay.adoc b/Documentation/config/replay.adoc\nnew file mode 100644\nindex 0000000000..7d549d2f0e\n--- /dev/null\n+++ b/Documentation/config/replay.adoc\n@@ -0,0 +1,11 @@\n+replay.refAction::\n+\tSpecifies the default mode for handling reference updates in\n+\t`git replay`. The value can be:\n++\n+--\n+\t* `update`: Update refs directly using an atomic transaction (default behavior).\n+\t* `print`: Output update-ref commands for pipeline use.\n+--\n++\n+This setting can be overridden with the `--ref-action` command-line option.\n+When not configured, `git replay` defaults to `update` mode.\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 2ef74ddb12..dcb26e8a8e 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -51,6 +51,8 @@ which uses the target only as a starting point without updating it.\n \t* `print`: Output update-ref commands for pipeline use. This is the\n \t  traditional behavior where output can be piped to `git update-ref --stdin`.\n --\n++\n+The default mode can be configured via the `replay.refAction` configuration variable.\n \n <revision-range>::\n \tRange of commits to replay. More than one <revision-range> can\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 94e60b5b10..6606a2c94b 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -8,6 +8,7 @@\n #include \"git-compat-util.h\"\n \n #include \"builtin.h\"\n+#include \"config.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"lockfile.h\"\n@@ -298,6 +299,22 @@ static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const\n \tdie(_(\"invalid %s value: '%s'\"), source, ref_action);\n }\n \n+static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action)\n+{\n+\tconst char *config_value = NULL;\n+\n+\t/* Command line option takes precedence */\n+\tif (ref_action)\n+\t\treturn parse_ref_action_mode(ref_action, \"--ref-action\");\n+\n+\t/* Check config value */\n+\tif (!repo_config_get_string_tmp(repo, \"replay.refAction\", &config_value))\n+\t\treturn parse_ref_action_mode(config_value, \"replay.refAction\");\n+\n+\t/* Default to update mode */\n+\treturn REF_ACTION_UPDATE;\n+}\n+\n static int handle_ref_update(enum ref_action_mode mode,\n \t\t\t     struct ref_transaction *transaction,\n \t\t\t     const char *refname,\n@@ -332,7 +349,7 @@ int cmd_replay(int argc,\n \tconst char *onto_name = NULL;\n \tint contained = 0;\n \tconst char *ref_action = NULL;\n-\tenum ref_action_mode ref_mode = REF_ACTION_UPDATE;\n+\tenum ref_action_mode ref_mode;\n \n \tstruct rev_info revs;\n \tstruct commit *last_commit = NULL;\n@@ -378,9 +395,8 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n \t\t\t\t  contained, \"--contained\");\n \n-\t/* Parse ref action mode */\n-\tif (ref_action)\n-\t\tref_mode = parse_ref_action_mode(ref_action, \"--ref-action\");\n+\t/* Parse ref action mode from command line or config */\n+\tref_mode = get_ref_action_mode(repo, ref_action);\n \n \tadvance_name = xstrdup_or_null(advance_name_opt);\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex ec79234c80..cf3aacf355 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -268,4 +268,50 @@ test_expect_success 'reflog message for --advance mode' '\n \ttest_cmp expect-reflog reflog-msg\n '\n \n+test_expect_success 'replay.refAction=print config option' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n+\n+\t# Test with config set to print\n+\ttest_config replay.refAction print &&\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_line_count = 1 output &&\n+\ttest_grep \"^update refs/heads/topic2 \" output\n+'\n+\n+test_expect_success 'replay.refAction=update config option' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n+\n+\t# Test with config set to update\n+\ttest_config replay.refAction update &&\n+\tgit replay --onto main topic1..topic2 >output &&\n+\ttest_must_be_empty output &&\n+\n+\t# Verify ref was updated\n+\tgit log --format=%s topic2 >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'command-line --ref-action overrides config' '\n+\t# Store original state\n+\tSTART=$(git rev-parse topic2) &&\n+\ttest_when_finished \"git branch -f topic2 $START\" &&\n+\n+\t# Set config to update but use --ref-action=print\n+\ttest_config replay.refAction update &&\n+\tgit replay --ref-action=print --onto main topic1..topic2 >output &&\n+\ttest_line_count = 1 output &&\n+\ttest_grep \"^update refs/heads/topic2 \" output\n+'\n+\n+test_expect_success 'invalid replay.refAction value' '\n+\ttest_config replay.refAction invalid &&\n+\ttest_must_fail git replay --onto main topic1..topic2 2>error &&\n+\ttest_grep \"invalid.*replay.refAction.*value\" error\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"530338","messageId":"CABPp-BE+-EvQhRBQy22kt9+p8Zw2fOS4oR+UBRZAgcxRhaxS9A@mail.gmail.com","threadId":"64109","inReplyTo":"20251105191650.89975-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH v7 0/3] replay: make atomic ref updates the default","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-06T19:32:23Z","receivedAt":"2025-11-06T19:32:36Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Nov 5, 2025 at 11:17 AM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n>\n> This is v7 of the git-replay atomic updates series.\n>\n> This version addresses all feedback from v6 reviews. Thanks to Elijah,\n> Christian, and Phillip for the thorough reviews that helped refine the\n> implementation to Git standards.\n>\n> ## Changes in v7\n>\n> **Improved commit message clarity**\n>\n> Per Elijah's feedback, simplified commit messages by removing redundant\n> sections:\n>   - Removed \"Implementation details\" section (details visible in diff)\n>   - Shortened \"Test suite changes\" to focus on what's tested\n>   - Removed command-line precedence paragraph (obvious from code)\n>   - Removed \"Examples\" and configuration precedence sections\n>\n> **Fixed test cleanup and isolation**\n>\n> Following Elijah's suggestions:\n>   - Used test_when_finished with proper state restoration in atomic tests\n>   - Created separate test-atomic branch to avoid contaminating topic2\n>   - Fixed bare repository test to use START variable for cleanup\n>   - Improved test reliability by rebuilding expectations independently\n>\n> **Extracted parse_ref_action_mode() to appropriate commit**\n>\n> Per Christian's observation, moved the parse_ref_action_mode() helper\n> function from Commit 3 to Commit 2 where it's first used. This makes\n> the patch progression more logical.\n>\n> **Fixed parameter naming consistency**\n>\n> Following Christian's feedback, used consistent naming throughout:\n>   - ref_action (string parameter for command-line/config value)\n>   - ref_mode (enum variable for internal mode)\n> This eliminates confusion and improves code readability.\n>\n> **Moved config reference to correct commit**\n>\n> Per Elijah's note, moved the sentence about replay.refAction config\n> from Commit 2's documentation to Commit 3 where the config is actually\n> introduced.\n>\n> **Enhanced reflog messages**\n>\n> Following Phillip's suggestions for better user experience:\n>   - --advance mode: \"replay --advance <branch-name>\" (uses user input)\n>   - --onto mode: \"replay --onto <commit-sha>\" (precise commit reference)\n> Added comprehensive reflog testing to verify messages.\n>\n> **Fixed indentation in Commit 3**\n>\n> Corrected indentation within the while (decoration) loop per CI\n> feedback, adding proper tabs to nested if statements.\n>\n> **Fixed coding style**\n>\n> Per CI check-style feedback, removed braces from single-statement\n> if-else blocks following Git's CodingGuidelines.\n>\n> **Split config tests for clarity**\n>\n> Separated the replay.refAction config test into two distinct tests:\n>   - replay.refAction=print config option\n>   - replay.refAction=update config option\n> This improves test clarity and makes failures easier to diagnose.\n>\n> ## Technical Implementation\n>\n> The atomic ref updates leverage Git's ref transaction API:\n>   - ref_store_transaction_begin() with default atomic behavior\n>   - ref_transaction_update() to stage each update\n>   - ref_transaction_commit() for atomic application\n>\n> The helper functions provide clean separation:\n>   - parse_ref_action_mode(): Validates strings and converts to enum\n>   - get_ref_action_mode(): Implements command-line > config > default precedence\n>   - handle_ref_update(): Uses type-safe enum with switch statement\n>\n> Reflog messages are constructed dynamically based on replay mode and\n> include either the branch name (--advance) or commit SHA (--onto) for\n> clear audit trails.\n>\n> ## Testing\n>\n> All tests pass:\n>   - t3650-replay-basics.sh (22 tests pass)\n>   - Config tests verify proper precedence and error handling\n>   - Atomic behavior tests verify direct ref updates\n>   - Reflog tests verify descriptive messages\n>   - Backward compatibility maintained for pipeline workflow\n>\n> CI results: https://gitlab.com/gitlab-org/git/-/pipelines/2140425748\n>\n> Siddharth Asthana (3):\n>   replay: use die_for_incompatible_opt2() for option validation\n>   replay: make atomic ref updates the default behavior\n>   replay: add replay.refAction config option\n>\n>  Documentation/config/replay.adoc |  11 +++\n>  Documentation/git-replay.adoc    |  63 ++++++++++-----\n>  builtin/replay.c                 | 133 ++++++++++++++++++++++++++++---\n>  t/t3650-replay-basics.sh         | 113 ++++++++++++++++++++++++--\n>  4 files changed, 277 insertions(+), 43 deletions(-)\n>  create mode 100644 Documentation/config/replay.adoc\n>\n> Range-diff against v6:\n> 1:  1f0fad0cac = 1:  9e4eab2df2 replay: use die_for_incompatible_opt2() for option validation\n> 2:  bfc6188234 ! 2:  1602f6097e replay: make atomic ref updates the default behavior\n>     @@ Commit message\n>           * update (default): Update refs directly using an atomic transaction\n>           * print: Output update-ref commands for pipeline use\n>\n>     -    Implementation details:\n>     -\n>     -    The atomic ref updates are implemented using Git's ref transaction API.\n>     -    In cmd_replay(), when not in `print` mode, we initialize a transaction\n>     -    using ref_store_transaction_begin() with the default atomic behavior.\n>     -    As commits are replayed, ref updates are staged into the transaction\n>     -    using ref_transaction_update(). Finally, ref_transaction_commit()\n>     -    applies all updates atomically—either all updates succeed or none do.\n>     -\n>     -    To avoid code duplication between the 'print' and 'update' modes, this\n>     -    commit extracts a handle_ref_update() helper function. This function\n>     -    takes the mode (as an enum) and either prints the update command or\n>     -    stages it into the transaction. Using an enum rather than passing the\n>     -    string around provides type safety and allows the compiler to catch\n>     -    typos. The switch statement makes it easy to add future modes.\n>     -\n>     -    The helper function signature:\n>     -\n>     -      static int handle_ref_update(enum ref_action_mode mode,\n>     -                                    struct ref_transaction *transaction,\n>     -                                    const char *refname,\n>     -                                    const struct object_id *new_oid,\n>     -                                    const struct object_id *old_oid,\n>     -                                    struct strbuf *err)\n>     -\n>     -    The enum is defined as:\n>     -\n>     -      enum ref_action_mode {\n>     -          REF_ACTION_UPDATE,\n>     -          REF_ACTION_PRINT\n>     -      };\n>     -\n>     -    The mode string is converted to enum immediately after parse_options()\n>     -    to avoid string comparisons throughout the codebase and provide compiler\n>     -    protection against typos.\n>     -\n>          Test suite changes:\n>\n>          All existing tests that expected command output now use\n>     @@ Commit message\n>           - Equivalence between traditional pipeline and atomic updates\n>           - Real atomicity using a lock file to verify all-or-nothing guarantee\n>           - Test isolation using test_when_finished to clean up state\n>     -\n>     -    The bare repository tests were fixed to rebuild their expectations\n>     -    independently rather than comparing to previous test output, improving\n>     -    test reliability and isolation.\n>     +      - Reflog messages include replay mode and target\n>\n>          A following commit will add a replay.refAction configuration\n>          option for users who prefer the traditional pipeline output as their\n>     @@ Documentation/git-replay.adoc: OPTIONS\n>      -commits, similar to the way how `git rebase --update-refs` updates\n>      -multiple branches in the affected range.\n>      +When `--onto` is specified, the branch(es) in the revision range will be\n>     -+updated to point at the new commits (or update commands will be printed\n>     -+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`\n>     ++updated to point at the new commits, similar to the way `git rebase --update-refs`\n>      +updates multiple branches in the affected range.\n>\n>       --advance <branch>::\n>     @@ Documentation/git-replay.adoc: OPTIONS\n>      -will update the branch passed as an argument to `--advance` to point at\n>      -the new commits (in other words, this mimics a cherry-pick operation).\n>      +The history is replayed on top of the <branch> and <branch> is updated to\n>     -+point at the tip of the resulting history (or an update command will be\n>     -+printed if `--ref-action=print` is used). This is different from `--onto`,\n>     ++point at the tip of the resulting history. This is different from `--onto`,\n>      +which uses the target only as a starting point without updating it.\n>      +\n>      +--ref-action[=<mode>]::\n>     @@ Documentation/git-replay.adoc: OPTIONS\n>      +  * `print`: Output update-ref commands for pipeline use. This is the\n>      +    traditional behavior where output can be piped to `git update-ref --stdin`.\n>      +--\n>     -++\n>     -+The default mode can be configured via the `replay.refAction` configuration variable.\n>\n>       <revision-range>::\n>         Range of commits to replay. More than one <revision-range> can\n>     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r\n>         return create_commit(repo, result->tree, pickme, replayed_base);\n>       }\n>\n>     ++static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n>     ++{\n>     ++  if (!ref_action || !strcmp(ref_action, \"update\"))\n>     ++          return REF_ACTION_UPDATE;\n>     ++  if (!strcmp(ref_action, \"print\"))\n>     ++          return REF_ACTION_PRINT;\n>     ++  die(_(\"invalid %s value: '%s'\"), source, ref_action);\n>     ++}\n>     ++\n>      +static int handle_ref_update(enum ref_action_mode mode,\n>      +                       struct ref_transaction *transaction,\n>      +                       const char *refname,\n>      +                       const struct object_id *new_oid,\n>      +                       const struct object_id *old_oid,\n>     ++                       const char *reflog_msg,\n>      +                       struct strbuf *err)\n>      +{\n>      +  switch (mode) {\n>     @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r\n>      +          return 0;\n>      +  case REF_ACTION_UPDATE:\n>      +          return ref_transaction_update(transaction, refname, new_oid, old_oid,\n>     -+                                        NULL, NULL, 0, \"git replay\", err);\n>     ++                                        NULL, NULL, 0, reflog_msg, err);\n>      +  default:\n>      +          BUG(\"unknown ref_action_mode %d\", mode);\n>      +  }\n>     @@ builtin/replay.c: int cmd_replay(int argc,\n>         struct commit *onto = NULL;\n>         const char *onto_name = NULL;\n>         int contained = 0;\n>     -+  const char *ref_action_str = NULL;\n>     -+  enum ref_action_mode ref_action = REF_ACTION_UPDATE;\n>     ++  const char *ref_action = NULL;\n>     ++  enum ref_action_mode ref_mode = REF_ACTION_UPDATE;\n>\n>         struct rev_info revs;\n>         struct commit *last_commit = NULL;\n>     @@ builtin/replay.c: int cmd_replay(int argc,\n>         kh_oid_map_t *replayed_commits;\n>      +  struct ref_transaction *transaction = NULL;\n>      +  struct strbuf transaction_err = STRBUF_INIT;\n>     ++  struct strbuf reflog_msg = STRBUF_INIT;\n>         int ret = 0;\n>\n>      -  const char * const replay_usage[] = {\n>     @@ builtin/replay.c: int cmd_replay(int argc,\n>                            N_(\"replay onto given commit\")),\n>                 OPT_BOOL(0, \"contained\", &contained,\n>                          N_(\"advance all branches contained in revision-range\")),\n>     -+          OPT_STRING(0, \"ref-action\", &ref_action_str,\n>     ++          OPT_STRING(0, \"ref-action\", &ref_action,\n>      +                     N_(\"mode\"),\n>      +                     N_(\"control ref update behavior (update|print)\")),\n>                 OPT_END()\n>     @@ builtin/replay.c: int cmd_replay(int argc,\n>         die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>                                   contained, \"--contained\");\n>\n>     -+  /* Default to update mode if not specified */\n>     -+  if (!ref_action_str)\n>     -+          ref_action_str = \"update\";\n>     -+\n>     -+  /* Validate ref-action mode */\n>     -+  if (!strcmp(ref_action_str, \"update\"))\n>     -+          ref_action = REF_ACTION_UPDATE;\n>     -+  else if (!strcmp(ref_action_str, \"print\"))\n>     -+          ref_action = REF_ACTION_PRINT;\n>     -+  else\n>     -+          die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n>     ++  /* Parse ref action mode */\n>     ++  if (ref_action)\n>     ++          ref_mode = parse_ref_action_mode(ref_action, \"--ref-action\");\n>      +\n>         advance_name = xstrdup_or_null(advance_name_opt);\n>\n>     @@ builtin/replay.c: int cmd_replay(int argc,\n>         determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n>                               &onto, &update_refs);\n>\n>     ++  /* Build reflog message */\n>     ++  if (advance_name_opt)\n>     ++          strbuf_addf(&reflog_msg, \"replay --advance %s\", advance_name_opt);\n>     ++  else\n>     ++          strbuf_addf(&reflog_msg, \"replay --onto %s\",\n>     ++                      oid_to_hex(&onto->object.oid));\n>     ++\n>      +  /* Initialize ref transaction if using update mode */\n>     -+  if (ref_action == REF_ACTION_UPDATE) {\n>     ++  if (ref_mode == REF_ACTION_UPDATE) {\n>      +          transaction = ref_store_transaction_begin(get_main_ref_store(repo),\n>      +                                                    0, &transaction_err);\n>      +          if (!transaction) {\n>     @@ builtin/replay.c: int cmd_replay(int argc,\n>      -                                 decoration->name,\n>      -                                 oid_to_hex(&last_commit->object.oid),\n>      -                                 oid_to_hex(&commit->object.oid));\n>     -+                          if (handle_ref_update(ref_action, transaction,\n>     ++                          if (handle_ref_update(ref_mode, transaction,\n>      +                                                decoration->name,\n>      +                                                &last_commit->object.oid,\n>      +                                                &commit->object.oid,\n>     ++                                                reflog_msg.buf,\n>      +                                                &transaction_err) < 0) {\n>      +                                  ret = error(_(\"failed to update ref '%s': %s\"),\n>      +                                              decoration->name, transaction_err.buf);\n>     @@ builtin/replay.c: int cmd_replay(int argc,\n>      -                 advance_name,\n>      -                 oid_to_hex(&last_commit->object.oid),\n>      -                 oid_to_hex(&onto->object.oid));\n>     -+          if (handle_ref_update(ref_action, transaction, advance_name,\n>     ++          if (handle_ref_update(ref_mode, transaction, advance_name,\n>      +                                &last_commit->object.oid,\n>      +                                &onto->object.oid,\n>     ++                                reflog_msg.buf,\n>      +                                &transaction_err) < 0) {\n>      +                  ret = error(_(\"failed to update ref '%s': %s\"),\n>      +                              advance_name, transaction_err.buf);\n>     @@ builtin/replay.c: int cmd_replay(int argc,\n>      +  if (transaction)\n>      +          ref_transaction_free(transaction);\n>      +  strbuf_release(&transaction_err);\n>     ++  strbuf_release(&reflog_msg);\n>         release_revisions(&revs);\n>         free(advance_name);\n>\n>     @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '\n>       '\n>\n>      +test_expect_success 'default atomic behavior updates refs directly' '\n>     -+  # Store original state for cleanup\n>     -+  test_when_finished \"git branch -f topic2 topic1\" &&\n>     ++  # Use a separate branch to avoid contaminating topic2 for later tests\n>     ++  git branch test-atomic topic2 &&\n>     ++  test_when_finished \"git branch -D test-atomic\" &&\n\nI'm curious why you created an extra branch for this test, while...\n\n>      +\n>      +  # Test default atomic behavior (no output, refs updated)\n>     -+  git replay --onto main topic1..topic2 >output &&\n>     ++  git replay --onto main topic1..test-atomic >output &&\n>      +  test_must_be_empty output &&\n>      +\n>      +  # Verify ref was updated\n>     -+  git log --format=%s topic2 >actual &&\n>     ++  git log --format=%s test-atomic >actual &&\n>      +  test_write_lines E D M L B A >expect &&\n>     -+  test_cmp expect actual\n>     ++  test_cmp expect actual &&\n>     ++\n>     ++  # Verify reflog message includes SHA of onto commit\n>     ++  git reflog test-atomic -1 --format=%gs >reflog-msg &&\n>     ++  ONTO_SHA=$(git rev-parse main) &&\n>     ++  echo \"replay --onto $ONTO_SHA\" >expect-reflog &&\n>     ++  test_cmp expect-reflog reflog-msg\n>      +'\n>      +\n>      +test_expect_success 'atomic behavior in bare repository' '\n>     ++  # Store original state for cleanup\n>     ++  START=$(git -C bare rev-parse topic2) &&\n>     ++  test_when_finished \"git -C bare update-ref refs/heads/topic2 $START\" &&\n\n...just saving the location for topic2 in this test (and similarly\njust saving the location for main in the next test).  It appears you\nweren't doing anything special with the test-atomic branch, so I'm\ncurious why you didn't just use the same idiom for all three tests.\n\n>     ++\n>      +  # Test atomic updates work in bare repo\n>      +  git -C bare replay --onto main topic1..topic2 >output &&\n>      +  test_must_be_empty output &&\n>     @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '\n>      +  # Verify ref was updated in bare repo\n>      +  git -C bare log --format=%s topic2 >actual &&\n>      +  test_write_lines E D M L B A >expect &&\n>     -+  test_cmp expect actual &&\n>     ++  test_cmp expect actual\n>     ++'\n>     ++\n>     ++test_expect_success 'reflog message for --advance mode' '\n>     ++  # Store original state\n>     ++  START=$(git rev-parse main) &&\n>     ++  test_when_finished \"git update-ref refs/heads/main $START\" &&\n>     ++\n>     ++  # Test --advance mode reflog message\n>     ++  git replay --advance main topic1..topic2 >output &&\n>     ++  test_must_be_empty output &&\n>      +\n>     -+  # Reset for other tests\n>     -+  git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n>     ++  # Verify reflog message includes --advance and branch name\n>     ++  git reflog main -1 --format=%gs >reflog-msg &&\n>     ++  echo \"replay --advance main\" >expect-reflog &&\n>     ++  test_cmp expect-reflog reflog-msg\n>      +'\n>      +\n>       test_done\n> -:  ---------- > 3:  b7ebe1f534 replay: add replay.refAction config option\n\nThere was a third patch in v6, but it doesn't show up in your\nrange-diff?  Did you specify the range incorrectly by chance when you\ngenerated this?\n\nAnyway, I looked over the patches as well as the range-diff.  There is\nthe slight surprise I had in your lack of consistency with idiom\nchoice in the tests, but that's pretty minor and probably doesn't\nmerit a re-roll.  This version looks good to me; thanks for working on\nit!\n"},{"id":"530377","messageId":"906fba13-fc84-411c-a43f-baaa2b90ed95@gmail.com","threadId":"64109","inReplyTo":"20251105191650.89975-1-siddharthasthana31@gmail.com","subject":"Re: [PATCH v7 0/3] replay: make atomic ref updates the default","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-07T15:48:30Z","receivedAt":"2025-11-07T15:48:35Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Siddharth\n\nOn 05/11/2025 19:15, Siddharth Asthana wrote:\n\n>      @@ builtin/replay.c: int cmd_replay(int argc,\n>        \tdetermine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n>        \t\t\t      &onto, &update_refs);\n>        \n>      ++\t/* Build reflog message */\n>      ++\tif (advance_name_opt)\n>      ++\t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", advance_name_opt);\n\nThis appends the name of the branch being advanced, rather than what's \nbeing picked. As this message is written to the reflog of the branch \nthat's being advanced adding the branch name to the message is kind of \nredundant but we can always change this later when we have more \nexperience with \"--ref-action\"\n\n>      ++\telse\n>      ++\t\tstrbuf_addf(&reflog_msg, \"replay --onto %s\",\n>      ++\t\t\t    oid_to_hex(&onto->object.oid));\n\nThis looks good.\n\nThanks for working on this, I think this is probably ready to me merged.\n\nPhillip\n\n"},{"id":"530399","messageId":"0545bc77-8d69-4cf5-8d1c-ba59035eb556@gmail.com","threadId":"64109","inReplyTo":"CABPp-BE+-EvQhRBQy22kt9+p8Zw2fOS4oR+UBRZAgcxRhaxS9A@mail.gmail.com","subject":"Re: [PATCH v7 0/3] replay: make atomic ref updates the default","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-08T13:22:28Z","receivedAt":"2025-11-08T13:22:36Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 07/11/25 01:02, Elijah Newren wrote:\n> On Wed, Nov 5, 2025 at 11:17 AM Siddharth Asthana\n> <siddharthasthana31@gmail.com> wrote:\n>> This is v7 of the git-replay atomic updates series.\n>>\n>> This version addresses all feedback from v6 reviews. Thanks to Elijah,\n>> Christian, and Phillip for the thorough reviews that helped refine the\n>> implementation to Git standards.\n>>\n>> ## Changes in v7\n>>\n>> **Improved commit message clarity**\n>>\n>> Per Elijah's feedback, simplified commit messages by removing redundant\n>> sections:\n>>    - Removed \"Implementation details\" section (details visible in diff)\n>>    - Shortened \"Test suite changes\" to focus on what's tested\n>>    - Removed command-line precedence paragraph (obvious from code)\n>>    - Removed \"Examples\" and configuration precedence sections\n>>\n>> **Fixed test cleanup and isolation**\n>>\n>> Following Elijah's suggestions:\n>>    - Used test_when_finished with proper state restoration in atomic tests\n>>    - Created separate test-atomic branch to avoid contaminating topic2\n>>    - Fixed bare repository test to use START variable for cleanup\n>>    - Improved test reliability by rebuilding expectations independently\n>>\n>> **Extracted parse_ref_action_mode() to appropriate commit**\n>>\n>> Per Christian's observation, moved the parse_ref_action_mode() helper\n>> function from Commit 3 to Commit 2 where it's first used. This makes\n>> the patch progression more logical.\n>>\n>> **Fixed parameter naming consistency**\n>>\n>> Following Christian's feedback, used consistent naming throughout:\n>>    - ref_action (string parameter for command-line/config value)\n>>    - ref_mode (enum variable for internal mode)\n>> This eliminates confusion and improves code readability.\n>>\n>> **Moved config reference to correct commit**\n>>\n>> Per Elijah's note, moved the sentence about replay.refAction config\n>> from Commit 2's documentation to Commit 3 where the config is actually\n>> introduced.\n>>\n>> **Enhanced reflog messages**\n>>\n>> Following Phillip's suggestions for better user experience:\n>>    - --advance mode: \"replay --advance <branch-name>\" (uses user input)\n>>    - --onto mode: \"replay --onto <commit-sha>\" (precise commit reference)\n>> Added comprehensive reflog testing to verify messages.\n>>\n>> **Fixed indentation in Commit 3**\n>>\n>> Corrected indentation within the while (decoration) loop per CI\n>> feedback, adding proper tabs to nested if statements.\n>>\n>> **Fixed coding style**\n>>\n>> Per CI check-style feedback, removed braces from single-statement\n>> if-else blocks following Git's CodingGuidelines.\n>>\n>> **Split config tests for clarity**\n>>\n>> Separated the replay.refAction config test into two distinct tests:\n>>    - replay.refAction=print config option\n>>    - replay.refAction=update config option\n>> This improves test clarity and makes failures easier to diagnose.\n>>\n>> ## Technical Implementation\n>>\n>> The atomic ref updates leverage Git's ref transaction API:\n>>    - ref_store_transaction_begin() with default atomic behavior\n>>    - ref_transaction_update() to stage each update\n>>    - ref_transaction_commit() for atomic application\n>>\n>> The helper functions provide clean separation:\n>>    - parse_ref_action_mode(): Validates strings and converts to enum\n>>    - get_ref_action_mode(): Implements command-line > config > default precedence\n>>    - handle_ref_update(): Uses type-safe enum with switch statement\n>>\n>> Reflog messages are constructed dynamically based on replay mode and\n>> include either the branch name (--advance) or commit SHA (--onto) for\n>> clear audit trails.\n>>\n>> ## Testing\n>>\n>> All tests pass:\n>>    - t3650-replay-basics.sh (22 tests pass)\n>>    - Config tests verify proper precedence and error handling\n>>    - Atomic behavior tests verify direct ref updates\n>>    - Reflog tests verify descriptive messages\n>>    - Backward compatibility maintained for pipeline workflow\n>>\n>> CI results: https://gitlab.com/gitlab-org/git/-/pipelines/2140425748\n>>\n>> Siddharth Asthana (3):\n>>    replay: use die_for_incompatible_opt2() for option validation\n>>    replay: make atomic ref updates the default behavior\n>>    replay: add replay.refAction config option\n>>\n>>   Documentation/config/replay.adoc |  11 +++\n>>   Documentation/git-replay.adoc    |  63 ++++++++++-----\n>>   builtin/replay.c                 | 133 ++++++++++++++++++++++++++++---\n>>   t/t3650-replay-basics.sh         | 113 ++++++++++++++++++++++++--\n>>   4 files changed, 277 insertions(+), 43 deletions(-)\n>>   create mode 100644 Documentation/config/replay.adoc\n>>\n>> Range-diff against v6:\n>> 1:  1f0fad0cac = 1:  9e4eab2df2 replay: use die_for_incompatible_opt2() for option validation\n>> 2:  bfc6188234 ! 2:  1602f6097e replay: make atomic ref updates the default behavior\n>>      @@ Commit message\n>>            * update (default): Update refs directly using an atomic transaction\n>>            * print: Output update-ref commands for pipeline use\n>>\n>>      -    Implementation details:\n>>      -\n>>      -    The atomic ref updates are implemented using Git's ref transaction API.\n>>      -    In cmd_replay(), when not in `print` mode, we initialize a transaction\n>>      -    using ref_store_transaction_begin() with the default atomic behavior.\n>>      -    As commits are replayed, ref updates are staged into the transaction\n>>      -    using ref_transaction_update(). Finally, ref_transaction_commit()\n>>      -    applies all updates atomically—either all updates succeed or none do.\n>>      -\n>>      -    To avoid code duplication between the 'print' and 'update' modes, this\n>>      -    commit extracts a handle_ref_update() helper function. This function\n>>      -    takes the mode (as an enum) and either prints the update command or\n>>      -    stages it into the transaction. Using an enum rather than passing the\n>>      -    string around provides type safety and allows the compiler to catch\n>>      -    typos. The switch statement makes it easy to add future modes.\n>>      -\n>>      -    The helper function signature:\n>>      -\n>>      -      static int handle_ref_update(enum ref_action_mode mode,\n>>      -                                    struct ref_transaction *transaction,\n>>      -                                    const char *refname,\n>>      -                                    const struct object_id *new_oid,\n>>      -                                    const struct object_id *old_oid,\n>>      -                                    struct strbuf *err)\n>>      -\n>>      -    The enum is defined as:\n>>      -\n>>      -      enum ref_action_mode {\n>>      -          REF_ACTION_UPDATE,\n>>      -          REF_ACTION_PRINT\n>>      -      };\n>>      -\n>>      -    The mode string is converted to enum immediately after parse_options()\n>>      -    to avoid string comparisons throughout the codebase and provide compiler\n>>      -    protection against typos.\n>>      -\n>>           Test suite changes:\n>>\n>>           All existing tests that expected command output now use\n>>      @@ Commit message\n>>            - Equivalence between traditional pipeline and atomic updates\n>>            - Real atomicity using a lock file to verify all-or-nothing guarantee\n>>            - Test isolation using test_when_finished to clean up state\n>>      -\n>>      -    The bare repository tests were fixed to rebuild their expectations\n>>      -    independently rather than comparing to previous test output, improving\n>>      -    test reliability and isolation.\n>>      +      - Reflog messages include replay mode and target\n>>\n>>           A following commit will add a replay.refAction configuration\n>>           option for users who prefer the traditional pipeline output as their\n>>      @@ Documentation/git-replay.adoc: OPTIONS\n>>       -commits, similar to the way how `git rebase --update-refs` updates\n>>       -multiple branches in the affected range.\n>>       +When `--onto` is specified, the branch(es) in the revision range will be\n>>      -+updated to point at the new commits (or update commands will be printed\n>>      -+if `--ref-action=print` is used), similar to the way `git rebase --update-refs`\n>>      ++updated to point at the new commits, similar to the way `git rebase --update-refs`\n>>       +updates multiple branches in the affected range.\n>>\n>>        --advance <branch>::\n>>      @@ Documentation/git-replay.adoc: OPTIONS\n>>       -will update the branch passed as an argument to `--advance` to point at\n>>       -the new commits (in other words, this mimics a cherry-pick operation).\n>>       +The history is replayed on top of the <branch> and <branch> is updated to\n>>      -+point at the tip of the resulting history (or an update command will be\n>>      -+printed if `--ref-action=print` is used). This is different from `--onto`,\n>>      ++point at the tip of the resulting history. This is different from `--onto`,\n>>       +which uses the target only as a starting point without updating it.\n>>       +\n>>       +--ref-action[=<mode>]::\n>>      @@ Documentation/git-replay.adoc: OPTIONS\n>>       +  * `print`: Output update-ref commands for pipeline use. This is the\n>>       +    traditional behavior where output can be piped to `git update-ref --stdin`.\n>>       +--\n>>      -++\n>>      -+The default mode can be configured via the `replay.refAction` configuration variable.\n>>\n>>        <revision-range>::\n>>          Range of commits to replay. More than one <revision-range> can\n>>      @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r\n>>          return create_commit(repo, result->tree, pickme, replayed_base);\n>>        }\n>>\n>>      ++static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n>>      ++{\n>>      ++  if (!ref_action || !strcmp(ref_action, \"update\"))\n>>      ++          return REF_ACTION_UPDATE;\n>>      ++  if (!strcmp(ref_action, \"print\"))\n>>      ++          return REF_ACTION_PRINT;\n>>      ++  die(_(\"invalid %s value: '%s'\"), source, ref_action);\n>>      ++}\n>>      ++\n>>       +static int handle_ref_update(enum ref_action_mode mode,\n>>       +                       struct ref_transaction *transaction,\n>>       +                       const char *refname,\n>>       +                       const struct object_id *new_oid,\n>>       +                       const struct object_id *old_oid,\n>>      ++                       const char *reflog_msg,\n>>       +                       struct strbuf *err)\n>>       +{\n>>       +  switch (mode) {\n>>      @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r\n>>       +          return 0;\n>>       +  case REF_ACTION_UPDATE:\n>>       +          return ref_transaction_update(transaction, refname, new_oid, old_oid,\n>>      -+                                        NULL, NULL, 0, \"git replay\", err);\n>>      ++                                        NULL, NULL, 0, reflog_msg, err);\n>>       +  default:\n>>       +          BUG(\"unknown ref_action_mode %d\", mode);\n>>       +  }\n>>      @@ builtin/replay.c: int cmd_replay(int argc,\n>>          struct commit *onto = NULL;\n>>          const char *onto_name = NULL;\n>>          int contained = 0;\n>>      -+  const char *ref_action_str = NULL;\n>>      -+  enum ref_action_mode ref_action = REF_ACTION_UPDATE;\n>>      ++  const char *ref_action = NULL;\n>>      ++  enum ref_action_mode ref_mode = REF_ACTION_UPDATE;\n>>\n>>          struct rev_info revs;\n>>          struct commit *last_commit = NULL;\n>>      @@ builtin/replay.c: int cmd_replay(int argc,\n>>          kh_oid_map_t *replayed_commits;\n>>       +  struct ref_transaction *transaction = NULL;\n>>       +  struct strbuf transaction_err = STRBUF_INIT;\n>>      ++  struct strbuf reflog_msg = STRBUF_INIT;\n>>          int ret = 0;\n>>\n>>       -  const char * const replay_usage[] = {\n>>      @@ builtin/replay.c: int cmd_replay(int argc,\n>>                             N_(\"replay onto given commit\")),\n>>                  OPT_BOOL(0, \"contained\", &contained,\n>>                           N_(\"advance all branches contained in revision-range\")),\n>>      -+          OPT_STRING(0, \"ref-action\", &ref_action_str,\n>>      ++          OPT_STRING(0, \"ref-action\", &ref_action,\n>>       +                     N_(\"mode\"),\n>>       +                     N_(\"control ref update behavior (update|print)\")),\n>>                  OPT_END()\n>>      @@ builtin/replay.c: int cmd_replay(int argc,\n>>          die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n>>                                    contained, \"--contained\");\n>>\n>>      -+  /* Default to update mode if not specified */\n>>      -+  if (!ref_action_str)\n>>      -+          ref_action_str = \"update\";\n>>      -+\n>>      -+  /* Validate ref-action mode */\n>>      -+  if (!strcmp(ref_action_str, \"update\"))\n>>      -+          ref_action = REF_ACTION_UPDATE;\n>>      -+  else if (!strcmp(ref_action_str, \"print\"))\n>>      -+          ref_action = REF_ACTION_PRINT;\n>>      -+  else\n>>      -+          die(_(\"unknown --ref-action mode '%s'\"), ref_action_str);\n>>      ++  /* Parse ref action mode */\n>>      ++  if (ref_action)\n>>      ++          ref_mode = parse_ref_action_mode(ref_action, \"--ref-action\");\n>>       +\n>>          advance_name = xstrdup_or_null(advance_name_opt);\n>>\n>>      @@ builtin/replay.c: int cmd_replay(int argc,\n>>          determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,\n>>                                &onto, &update_refs);\n>>\n>>      ++  /* Build reflog message */\n>>      ++  if (advance_name_opt)\n>>      ++          strbuf_addf(&reflog_msg, \"replay --advance %s\", advance_name_opt);\n>>      ++  else\n>>      ++          strbuf_addf(&reflog_msg, \"replay --onto %s\",\n>>      ++                      oid_to_hex(&onto->object.oid));\n>>      ++\n>>       +  /* Initialize ref transaction if using update mode */\n>>      -+  if (ref_action == REF_ACTION_UPDATE) {\n>>      ++  if (ref_mode == REF_ACTION_UPDATE) {\n>>       +          transaction = ref_store_transaction_begin(get_main_ref_store(repo),\n>>       +                                                    0, &transaction_err);\n>>       +          if (!transaction) {\n>>      @@ builtin/replay.c: int cmd_replay(int argc,\n>>       -                                 decoration->name,\n>>       -                                 oid_to_hex(&last_commit->object.oid),\n>>       -                                 oid_to_hex(&commit->object.oid));\n>>      -+                          if (handle_ref_update(ref_action, transaction,\n>>      ++                          if (handle_ref_update(ref_mode, transaction,\n>>       +                                                decoration->name,\n>>       +                                                &last_commit->object.oid,\n>>       +                                                &commit->object.oid,\n>>      ++                                                reflog_msg.buf,\n>>       +                                                &transaction_err) < 0) {\n>>       +                                  ret = error(_(\"failed to update ref '%s': %s\"),\n>>       +                                              decoration->name, transaction_err.buf);\n>>      @@ builtin/replay.c: int cmd_replay(int argc,\n>>       -                 advance_name,\n>>       -                 oid_to_hex(&last_commit->object.oid),\n>>       -                 oid_to_hex(&onto->object.oid));\n>>      -+          if (handle_ref_update(ref_action, transaction, advance_name,\n>>      ++          if (handle_ref_update(ref_mode, transaction, advance_name,\n>>       +                                &last_commit->object.oid,\n>>       +                                &onto->object.oid,\n>>      ++                                reflog_msg.buf,\n>>       +                                &transaction_err) < 0) {\n>>       +                  ret = error(_(\"failed to update ref '%s': %s\"),\n>>       +                              advance_name, transaction_err.buf);\n>>      @@ builtin/replay.c: int cmd_replay(int argc,\n>>       +  if (transaction)\n>>       +          ref_transaction_free(transaction);\n>>       +  strbuf_release(&transaction_err);\n>>      ++  strbuf_release(&reflog_msg);\n>>          release_revisions(&revs);\n>>          free(advance_name);\n>>\n>>      @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '\n>>        '\n>>\n>>       +test_expect_success 'default atomic behavior updates refs directly' '\n>>      -+  # Store original state for cleanup\n>>      -+  test_when_finished \"git branch -f topic2 topic1\" &&\n>>      ++  # Use a separate branch to avoid contaminating topic2 for later tests\n>>      ++  git branch test-atomic topic2 &&\n>>      ++  test_when_finished \"git branch -D test-atomic\" &&\n\n\nHi Elijah,\n\nThanks for the review and approval!\n\n\n> I'm curious why you created an extra branch for this test, while...\n\n\nGood catch on the inconsistency. The separate `test-atomic` branch was \nto avoid any potential contamination of `topic2` since multiple tests \nuse it throughout the file. But you are right that the other tests just \nsave/restore state directly.\n\nFor consistency, I could have used the same pattern everywhere. The \ncurrent approach works but mixing idioms isn't ideal - I will keep this \nin mind for future patches.\n\n\n>\n>>       +\n>>       +  # Test default atomic behavior (no output, refs updated)\n>>      -+  git replay --onto main topic1..topic2 >output &&\n>>      ++  git replay --onto main topic1..test-atomic >output &&\n>>       +  test_must_be_empty output &&\n>>       +\n>>       +  # Verify ref was updated\n>>      -+  git log --format=%s topic2 >actual &&\n>>      ++  git log --format=%s test-atomic >actual &&\n>>       +  test_write_lines E D M L B A >expect &&\n>>      -+  test_cmp expect actual\n>>      ++  test_cmp expect actual &&\n>>      ++\n>>      ++  # Verify reflog message includes SHA of onto commit\n>>      ++  git reflog test-atomic -1 --format=%gs >reflog-msg &&\n>>      ++  ONTO_SHA=$(git rev-parse main) &&\n>>      ++  echo \"replay --onto $ONTO_SHA\" >expect-reflog &&\n>>      ++  test_cmp expect-reflog reflog-msg\n>>       +'\n>>       +\n>>       +test_expect_success 'atomic behavior in bare repository' '\n>>      ++  # Store original state for cleanup\n>>      ++  START=$(git -C bare rev-parse topic2) &&\n>>      ++  test_when_finished \"git -C bare update-ref refs/heads/topic2 $START\" &&\n> ...just saving the location for topic2 in this test (and similarly\n> just saving the location for main in the next test).  It appears you\n> weren't doing anything special with the test-atomic branch, so I'm\n> curious why you didn't just use the same idiom for all three tests.\n>\n>>      ++\n>>       +  # Test atomic updates work in bare repo\n>>       +  git -C bare replay --onto main topic1..topic2 >output &&\n>>       +  test_must_be_empty output &&\n>>      @@ t/t3650-replay-basics.sh: test_expect_success 'merge.directoryRenames=false' '\n>>       +  # Verify ref was updated in bare repo\n>>       +  git -C bare log --format=%s topic2 >actual &&\n>>       +  test_write_lines E D M L B A >expect &&\n>>      -+  test_cmp expect actual &&\n>>      ++  test_cmp expect actual\n>>      ++'\n>>      ++\n>>      ++test_expect_success 'reflog message for --advance mode' '\n>>      ++  # Store original state\n>>      ++  START=$(git rev-parse main) &&\n>>      ++  test_when_finished \"git update-ref refs/heads/main $START\" &&\n>>      ++\n>>      ++  # Test --advance mode reflog message\n>>      ++  git replay --advance main topic1..topic2 >output &&\n>>      ++  test_must_be_empty output &&\n>>       +\n>>      -+  # Reset for other tests\n>>      -+  git -C bare update-ref refs/heads/topic2 $(git -C bare rev-parse topic1)\n>>      ++  # Verify reflog message includes --advance and branch name\n>>      ++  git reflog main -1 --format=%gs >reflog-msg &&\n>>      ++  echo \"replay --advance main\" >expect-reflog &&\n>>      ++  test_cmp expect-reflog reflog-msg\n>>       +'\n>>       +\n>>        test_done\n>> -:  ---------- > 3:  b7ebe1f534 replay: add replay.refAction config option\n> There was a third patch in v6, but it doesn't show up in your\n> range-diff?  Did you specify the range incorrectly by chance when you\n> generated this?\n\n\nThe range-diff shows all three patches (1:1, 2:2, 3:3), but the third \none appears as a new addition (-: → 3:) because it underwent significant \nrestructuring between v6 and v7. The config-related changes were moved \naround between commits, making git see it as essentially new rather than \nmodified.\n\nThanks for the thorough review throughout this series!\n\nSiddharth\n\n\n>\n> Anyway, I looked over the patches as well as the range-diff.  There is\n> the slight surprise I had in your lack of consistency with idiom\n> choice in the tests, but that's pretty minor and probably doesn't\n> merit a re-roll.  This version looks good to me; thanks for working on\n> it!\n"},{"id":"530400","messageId":"00a5a8f3-f761-46e8-84cc-4bd95db68b49@gmail.com","threadId":"64109","inReplyTo":"906fba13-fc84-411c-a43f-baaa2b90ed95@gmail.com","subject":"Re: [PATCH v7 0/3] replay: make atomic ref updates the default","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2025-11-08T13:23:38Z","receivedAt":"2025-11-08T13:23:45Z","isPatch":true,"sender":{"key":"siddharthasthana31@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53316982?v=4"},"body":"\nOn 07/11/25 21:18, Phillip Wood wrote:\n> Hi Siddharth\n>\n> On 05/11/2025 19:15, Siddharth Asthana wrote:\n>\n>>      @@ builtin/replay.c: int cmd_replay(int argc,\n>>            determine_replay_mode(repo, &revs.cmdline, onto_name, \n>> &advance_name,\n>>                          &onto, &update_refs);\n>>             ++    /* Build reflog message */\n>>      ++    if (advance_name_opt)\n>>      ++        strbuf_addf(&reflog_msg, \"replay --advance %s\", \n>> advance_name_opt);\n>\n\nHi Phillip,\n\n\n> This appends the name of the branch being advanced, rather than what's \n> being picked. As this message is written to the reflog of the branch \n> that's being advanced adding the branch name to the message is kind of \n> redundant but we can always change this later when we have more \n> experience with \"--ref-action\"\n\n\nYou are absolutely right about the redundancy. I went with the branch \nname to match what users typed on the command line, but since it's in \nthat branch's own reflog, just \"replay --advance\" might be cleaner.\n\nHappy to adjust this in a follow-up if the current approach proves \nconfusing in practice.\n\n\n>\n>>      ++    else\n>>      ++        strbuf_addf(&reflog_msg, \"replay --onto %s\",\n>>      ++                oid_to_hex(&onto->object.oid));\n>\n> This looks good.\n>\n> Thanks for working on this, I think this is probably ready to me merged.\n\n\nThank you for all the detailed feedback throughout this series - it \nreally helped improve the implementation!\n\nSiddharth\n\n\n>\n> Phillip\n>\n"},{"id":"530402","messageId":"CABPp-BFXvJQ9fj7zvj3brpcbzWxEXMFWeoVtQxMeNUrcbV9k+w@mail.gmail.com","threadId":"64109","inReplyTo":"0545bc77-8d69-4cf5-8d1c-ba59035eb556@gmail.com","subject":"Re: [PATCH v7 0/3] replay: make atomic ref updates the default","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-08T17:11:45Z","receivedAt":"2025-11-08T17:11:58Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Sat, Nov 8, 2025 at 5:22 AM Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n\n> >> -:  ---------- > 3:  b7ebe1f534 replay: add replay.refAction config option\n> > There was a third patch in v6, but it doesn't show up in your\n> > range-diff?  Did you specify the range incorrectly by chance when you\n> > generated this?\n>\n>\n> The range-diff shows all three patches (1:1, 2:2, 3:3), but the third\n> one appears as a new addition (-: → 3:) because it underwent significant\n> restructuring between v6 and v7. The config-related changes were moved\n> around between commits, making git see it as essentially new rather than\n> modified.\n\nNo, the range diff does not show all three patches for v6, it only\nshows all three patches for v7.  If you had all three patches shown\nfor both versions, and the third had undergone significant\nrestructuring, then you would expect to see two lines such as:\n\n3:  6b2a44c72c < -:  -----------  replay: add replay.refAction config option\n-:  ----------- > 3:  b7ebe1f534 replay: add replay.refAction config option\n\nThe first line (missing from your range-diff) would correspond to the\nthird patch from v6 being treated as deleted, and the second (present\nin your range-diff) would represent the third patch from v7 being\nconsidered an addition.  You can verify the first line is missing from\nyour range-diff by searching for \"3:\" in\nhttps://lore.kernel.org/git/20251105191650.89975-1-siddharthasthana31@gmail.com/\n-- you only get one hit instead of the expected two -- which suggests\nyou either didn't pass the correct range to range-diff or snipped part\nof the output when pasting to your email.  In this case it doesn't\nmatter much, because even if the 3rd patch from v6 was there we'd need\nto go an look at the individual patch due to the restructuring, but it\nwas just a little odd so I pointed it out.\n"}]}