{"thread":{"id":"64985","subject":"[PATCH 0/4] builtin/history: some smaller UI improvements","startedAt":"2026-02-12T12:44:41Z","lastAt":"2026-02-18T16:10:12Z","messageCount":34,"participants":["Patrick Steinhardt","Junio C Hamano","D. Ben Knoble","Ben Knoble","Kristoffer Haugsbakk"],"isPatch":true,"patchVersion":1,"patchTotal":4},"messages":[{"id":"535859","messageId":"20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im","threadId":"64985","inReplyTo":null,"subject":"[PATCH 0/4] builtin/history: some smaller UI improvements","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-12T12:44:33Z","receivedAt":"2026-02-12T12:44:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series contains a small set of UI improvements for\ngit-history(1):\n\n  - The first two commits adapt git-history(1) so that it performs\n    verifications before asking the user for input.\n\n  - The last two commits rework the \"--ref-action=\" option to instead be\n    split up into \"--dry-run\" and \"--update-refs=\" so that the option is\n    less focussed on technical implementation details.\n\nI decided to send this patch series as a small incremental step before\nsending `git history split`, also because that series conflicts with\naa/add-p-no-auto-advance.\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (4):\n      builtin/history: perform revwalk checks before asking for user input\n      builtin/history: check for merges before asking for user input\n      builtin/history: replace \"--ref-action=print\" with \"--dry-run\"\n      builtin/history: rename \"--ref-action=\" to \"--update-refs=\"\n\n Documentation/git-history.adoc |  12 +-\n builtin/history.c              | 255 ++++++++++++++++++++++++++---------------\n t/t3451-history-reword.sh      |  20 +++-\n 3 files changed, 181 insertions(+), 106 deletions(-)\n\n\n---\nbase-commit: 6fcee4785280a08e7f271bd015a4dc33753e2886\nchange-id: 20260212-b4-pks-history-dry-run-2b840e530ae6\n\n"},{"id":"535860","messageId":"20260212-b4-pks-history-dry-run-v1-1-1ce03d631c1b@pks.im","threadId":"64985","inReplyTo":"20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im","subject":"[PATCH 1/4] builtin/history: perform revwalk checks before asking for user input","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-12T12:44:34Z","receivedAt":"2026-02-12T12:44:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When setting up the revision walk in git-history(1) we also perform some\nverifications whether the request actually looks sane. Unfortunately,\nthese verifications come _after_ we have already asked the user for the\ncommit message of the commit that is to be rewritten. So in case any of\nthe verifications fails, the user will have lost their modifications.\n\nExtract the function to set up the revision walk and call it before we\nask for user input to fix this.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/history.c         | 69 +++++++++++++++++++++++++++++------------------\n t/t3451-history-reword.sh |  2 +-\n 2 files changed, 44 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 8dcb9a6046..1de51372ea 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -177,30 +177,15 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n \treturn 0;\n }\n \n-static int handle_reference_updates(enum ref_action action,\n-\t\t\t\t    struct repository *repo,\n-\t\t\t\t    struct commit *original,\n-\t\t\t\t    struct commit *rewritten,\n-\t\t\t\t    const char *reflog_msg)\n+static int setup_revwalk(struct repository *repo,\n+\t\t\t enum ref_action action,\n+\t\t\t struct commit *original,\n+\t\t\t struct rev_info *revs)\n {\n-\tconst struct name_decoration *decoration;\n-\tstruct replay_revisions_options opts = { 0 };\n-\tstruct replay_result result = { 0 };\n-\tstruct ref_transaction *transaction = NULL;\n \tstruct strvec args = STRVEC_INIT;\n-\tstruct strbuf err = STRBUF_INIT;\n-\tstruct commit *head = NULL;\n-\tstruct rev_info revs;\n-\tchar hex[GIT_MAX_HEXSZ + 1];\n-\tbool detached_head;\n-\tint head_flags = 0;\n \tint ret;\n \n-\trefs_read_ref_full(get_main_ref_store(repo), \"HEAD\",\n-\t\t\t   RESOLVE_REF_NO_RECURSE, NULL, &head_flags);\n-\tdetached_head = !(head_flags & REF_ISSYMREF);\n-\n-\trepo_init_revisions(repo, &revs, NULL);\n+\trepo_init_revisions(repo, revs, NULL);\n \tstrvec_push(&args, \"ignored\");\n \tstrvec_push(&args, \"--reverse\");\n \tstrvec_push(&args, \"--topo-order\");\n@@ -224,6 +209,7 @@ static int handle_reference_updates(enum ref_action action,\n \t */\n \tif (action == REF_ACTION_HEAD) {\n \t\tstruct commit_list *from_list = NULL;\n+\t\tstruct commit *head;\n \n \t\thead = lookup_commit_reference_by_name(\"HEAD\");\n \t\tif (!head) {\n@@ -250,20 +236,47 @@ static int handle_reference_updates(enum ref_action action,\n \t\tstrvec_push(&args, \"HEAD\");\n \t}\n \n-\tsetup_revisions_from_strvec(&args, &revs, NULL);\n+\tsetup_revisions_from_strvec(&args, revs, NULL);\n \tif (args.nr != 1)\n \t\tBUG(\"revisions were set up with invalid argument\");\n \n+\tret = 0;\n+\n+out:\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n+static int handle_reference_updates(struct rev_info *revs,\n+\t\t\t\t    enum ref_action action,\n+\t\t\t\t    struct commit *original,\n+\t\t\t\t    struct commit *rewritten,\n+\t\t\t\t    const char *reflog_msg)\n+{\n+\tconst struct name_decoration *decoration;\n+\tstruct replay_revisions_options opts = { 0 };\n+\tstruct replay_result result = { 0 };\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar hex[GIT_MAX_HEXSZ + 1];\n+\tbool detached_head;\n+\tint head_flags = 0;\n+\tint ret;\n+\n+\trefs_read_ref_full(get_main_ref_store(revs->repo), \"HEAD\",\n+\t\t\t   RESOLVE_REF_NO_RECURSE, NULL, &head_flags);\n+\tdetached_head = !(head_flags & REF_ISSYMREF);\n+\n \topts.onto = oid_to_hex_r(hex, &rewritten->object.oid);\n \n-\tret = replay_revisions(&revs, &opts, &result);\n+\tret = replay_revisions(revs, &opts, &result);\n \tif (ret)\n \t\tgoto out;\n \n \tswitch (action) {\n \tcase REF_ACTION_BRANCHES:\n \tcase REF_ACTION_HEAD:\n-\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);\n \t\tif (!transaction) {\n \t\t\tret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n \t\t\tgoto out;\n@@ -343,9 +356,7 @@ static int handle_reference_updates(enum ref_action action,\n out:\n \tref_transaction_free(transaction);\n \treplay_result_release(&result);\n-\trelease_revisions(&revs);\n \tstrbuf_release(&err);\n-\tstrvec_clear(&args);\n \treturn ret;\n }\n \n@@ -367,6 +378,7 @@ static int cmd_history_reword(int argc,\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n \tstruct commit *original, *rewritten;\n+\tstruct rev_info revs;\n \tint ret;\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n@@ -385,6 +397,10 @@ static int cmd_history_reword(int argc,\n \t\tgoto out;\n \t}\n \n+\tret = setup_revwalk(repo, action, original, &revs);\n+\tif (ret)\n+\t\tgoto out;\n+\n \tret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing reworded commit\"));\n@@ -393,7 +409,7 @@ static int cmd_history_reword(int argc,\n \n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n-\tret = handle_reference_updates(action, repo, original, rewritten,\n+\tret = handle_reference_updates(&revs, action, original, rewritten,\n \t\t\t\t       reflog_msg.buf);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n@@ -404,6 +420,7 @@ static int cmd_history_reword(int argc,\n \n out:\n \tstrbuf_release(&reflog_msg);\n+\trelease_revisions(&revs);\n \treturn ret;\n }\n \ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 3594421b68..6775ed62f9 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '\n \n \t\t# When told to update HEAD, only, the command will refuse to\n \t\t# rewrite commits that are not an ancestor of HEAD.\n-\t\ttest_must_fail git history reword --ref-action=head theirs 2>err &&\n+\t\ttest_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&\n \t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n \n \t\treword_with_message --ref-action=head base >updates <<-\\EOF &&\n\n-- \n2.53.0.295.g64333814d3.dirty\n\n"},{"id":"535861","messageId":"20260212-b4-pks-history-dry-run-v1-2-1ce03d631c1b@pks.im","threadId":"64985","inReplyTo":"20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im","subject":"[PATCH 2/4] builtin/history: check for merges before asking for user input","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-12T12:44:35Z","receivedAt":"2026-02-12T12:44:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The replay infrastructure is not yet capable of replaying merge commits.\nUnfortunately, we only notice that we're about to replay merges after we\nhave already asked the user for input, so any commit message that the\nuser may have written will be discarded in that case.\n\nFix this by checking whether the revwalk contains merge commits before\nwe ask for user input.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/history.c         | 40 ++++++++++++++++++++++++++++++++++++++++\n t/t3451-history-reword.sh |  2 +-\n 2 files changed, 41 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 1de51372ea..ca0cdb6a58 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -177,6 +177,42 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n \treturn 0;\n }\n \n+static int revwalk_contains_merges(struct repository *repo,\n+\t\t\t\t   const struct strvec *revwalk_args)\n+{\n+\tstruct strvec args = STRVEC_INIT;\n+\tstruct rev_info revs;\n+\tint ret;\n+\n+\tfor (size_t i = 0; i < revwalk_args->nr; i++)\n+\t\tstrvec_push(&args, revwalk_args->v[i]);\n+\tstrvec_push(&args, \"--min-parents=2\");\n+\n+\trepo_init_revisions(repo, &revs, NULL);\n+\n+\tsetup_revisions_from_strvec(&args, &revs, NULL);\n+\tif (args.nr != 1)\n+\t\tBUG(\"revisions were set up with invalid argument\");\n+\n+\tif (prepare_revision_walk(&revs) < 0) {\n+\t\tret = error(_(\"error preparing revisions\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (get_revision(&revs)) {\n+\t\tret = error(_(\"replaying merge commits is not supported yet!\"));\n+\t\tgoto out;\n+\t}\n+\n+\treset_revision_walk();\n+\tret = 0;\n+\n+out:\n+\trelease_revisions(&revs);\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n static int setup_revwalk(struct repository *repo,\n \t\t\t enum ref_action action,\n \t\t\t struct commit *original,\n@@ -236,6 +272,10 @@ static int setup_revwalk(struct repository *repo,\n \t\tstrvec_push(&args, \"HEAD\");\n \t}\n \n+\tret = revwalk_contains_merges(repo, &args);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n \tsetup_revisions_from_strvec(&args, revs, NULL);\n \tif (args.nr != 1)\n \t\tBUG(\"revisions were set up with invalid argument\");\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 6775ed62f9..12a9a7d051 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '\n \n \t\t# It is not possible to replay merge commits embedded in the\n \t\t# history (yet).\n-\t\ttest_must_fail git history reword HEAD~ 2>err &&\n+\t\ttest_must_fail git -c core.editor=false history reword HEAD~ 2>err &&\n \t\ttest_grep \"replaying merge commits is not supported yet\" err &&\n \n \t\t# But it is possible to reword a merge commit directly.\n\n-- \n2.53.0.295.g64333814d3.dirty\n\n"},{"id":"535862","messageId":"20260212-b4-pks-history-dry-run-v1-3-1ce03d631c1b@pks.im","threadId":"64985","inReplyTo":"20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im","subject":"[PATCH 3/4] builtin/history: replace \"--ref-action=print\" with \"--dry-run\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-12T12:44:36Z","receivedAt":"2026-02-12T12:44:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The git-history(1) command has the ability to perform a dry-run\nthat will not end up modifying any references. Instead, we'll only print\nany ref updates that would happen as a consequence of performing the\noperation.\n\nThis mode is somewhat hidden though behind the \"--ref-action=print\"\noption. This command line option has its origin in git-replay(1), where\nit's probably an okayish interface as this command is sitting more on\nthe plumbing side of tools. But git-history(1) is a user-facing tool,\nand this way of achieving a dry-run is way too technical and thus not\nvery discoverable.\n\nBesides usability issues, it also has another issue: the dry-run mode\nwill always operate as if the user wanted to rewrite all branches. But\nin fact, the user also has the option to only update the HEAD reference,\nand they might want to perform a dry-run of such an operation, too. We\ncould of course introduce \"--ref-actoin=print-head\", but that would\nbecome even less ergonomic.\n\nReplace \"--ref-action=print\" with a new \"--dry-run\" toggle. This new\ntoggle works with both \"--ref-action={head,branches}\" and is way more\ndiscoverable.\n\nAdd a test to verify that both \"--ref-action=\" values behave as\nexpected.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  12 ++--\n builtin/history.c              | 150 ++++++++++++++++++++++-------------------\n t/t3451-history-reword.sh      |  12 +++-\n 3 files changed, 96 insertions(+), 78 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 154e262b76..33353815cb 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history reword <commit> [--ref-action=(branches|head|print)]\n+git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\n \n DESCRIPTION\n -----------\n@@ -60,13 +60,15 @@ The following commands are available to rewrite history in different ways:\n OPTIONS\n -------\n \n-`--ref-action=(branches|head|print)`::\n+`--dry-run`::\n+\tDo not update any references, but instead print any ref updates in a\n+\tformat that can be consumed by linkgit:git-update-ref[1].\n+\n+`--ref-action=(branches|head)`::\n \tControl which references will be updated by the command, if any. With\n \t`branches`, all local branches that point to commits which are\n \tdescendants of the original commit will be rewritten. With `head`, only\n-\tthe current `HEAD` reference will be rewritten. With `print`, all\n-\tupdates as they would be performed with `branches` are printed in a\n-\tformat that can be consumed by linkgit:git-update-ref[1].\n+\tthe current `HEAD` reference will be rewritten.\n \n GIT\n ---\ndiff --git a/builtin/history.c b/builtin/history.c\nindex ca0cdb6a58..40e2925cea 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -18,7 +18,7 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--ref-action=(branches|head|print)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -155,7 +155,6 @@ enum ref_action {\n \tREF_ACTION_DEFAULT,\n \tREF_ACTION_BRANCHES,\n \tREF_ACTION_HEAD,\n-\tREF_ACTION_PRINT,\n };\n \n static int parse_ref_action(const struct option *opt, const char *value, int unset)\n@@ -167,10 +166,8 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n \t\t*action = REF_ACTION_BRANCHES;\n \t} else if (!strcmp(value, \"head\")) {\n \t\t*action = REF_ACTION_HEAD;\n-\t} else if (!strcmp(value, \"print\")) {\n-\t\t*action = REF_ACTION_PRINT;\n \t} else {\n-\t\treturn error(_(\"%s expects one of 'branches', 'head' or 'print'\"),\n+\t\treturn error(_(\"%s expects one of 'branches' or 'head'\"),\n \t\t\t     opt->long_name);\n \t}\n \n@@ -287,11 +284,29 @@ static int setup_revwalk(struct repository *repo,\n \treturn ret;\n }\n \n+static int handle_ref_update(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+\tif (!transaction) {\n+\t\tprintf(\"update %s %s %s\\n\",\n+\t\t       refname, oid_to_hex(new_oid), oid_to_hex(old_oid));\n+\t\treturn 0;\n+\t}\n+\n+\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n+\t\t\t\t      NULL, NULL, 0, reflog_msg, err);\n+}\n+\n static int handle_reference_updates(struct rev_info *revs,\n \t\t\t\t    enum ref_action action,\n \t\t\t\t    struct commit *original,\n \t\t\t\t    struct commit *rewritten,\n-\t\t\t\t    const char *reflog_msg)\n+\t\t\t\t    const char *reflog_msg,\n+\t\t\t\t    int dry_run)\n {\n \tconst struct name_decoration *decoration;\n \tstruct replay_revisions_options opts = { 0 };\n@@ -313,82 +328,72 @@ static int handle_reference_updates(struct rev_info *revs,\n \tif (ret)\n \t\tgoto out;\n \n-\tswitch (action) {\n-\tcase REF_ACTION_BRANCHES:\n-\tcase REF_ACTION_HEAD:\n+\tif (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)\n+\t\tBUG(\"unsupported ref action %d\", action);\n+\n+\tif (!dry_run) {\n \t\ttransaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);\n \t\tif (!transaction) {\n \t\t\tret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n \t\t\tgoto out;\n \t\t}\n+\t}\n \n-\t\tfor (size_t i = 0; i < result.updates_nr; i++) {\n-\t\t\tret = ref_transaction_update(transaction,\n-\t\t\t\t\t\t     result.updates[i].refname,\n-\t\t\t\t\t\t     &result.updates[i].new_oid,\n-\t\t\t\t\t\t     &result.updates[i].old_oid,\n-\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n-\t\t\tif (ret) {\n-\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n-\t\t\t\t\t    result.updates[i].refname, err.buf);\n-\t\t\t\tgoto out;\n-\t\t\t}\n+\tfor (size_t i = 0; i < result.updates_nr; i++) {\n+\t\tret = handle_ref_update(transaction,\n+\t\t\t\t\tresult.updates[i].refname,\n+\t\t\t\t\t&result.updates[i].new_oid,\n+\t\t\t\t\t&result.updates[i].old_oid,\n+\t\t\t\t\treflog_msg, &err);\n+\t\tif (ret) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    result.updates[i].refname, err.buf);\n+\t\t\tgoto out;\n \t\t}\n+\t}\n+\n+\t/*\n+\t * `replay_revisions()` only updates references that are\n+\t * ancestors of `rewritten`, so we need to manually\n+\t * handle updating references that point to `original`.\n+\t */\n+\tfor (decoration = get_name_decoration(&original->object);\n+\t     decoration;\n+\t     decoration = decoration->next)\n+\t{\n+\t\tif (decoration->type != DECORATION_REF_LOCAL &&\n+\t\t    decoration->type != DECORATION_REF_HEAD)\n+\t\t\tcontinue;\n+\n+\t\tif (action == REF_ACTION_HEAD &&\n+\t\t    decoration->type != DECORATION_REF_HEAD)\n+\t\t\tcontinue;\n \n \t\t/*\n-\t\t * `replay_revisions()` only updates references that are\n-\t\t * ancestors of `rewritten`, so we need to manually\n-\t\t * handle updating references that point to `original`.\n+\t\t * We only need to update HEAD separately in case it's\n+\t\t * detached. If it's not we'd already update the branch\n+\t\t * it is pointing to.\n \t\t */\n-\t\tfor (decoration = get_name_decoration(&original->object);\n-\t\t     decoration;\n-\t\t     decoration = decoration->next)\n-\t\t{\n-\t\t\tif (decoration->type != DECORATION_REF_LOCAL &&\n-\t\t\t    decoration->type != DECORATION_REF_HEAD)\n-\t\t\t\tcontinue;\n-\n-\t\t\tif (action == REF_ACTION_HEAD &&\n-\t\t\t    decoration->type != DECORATION_REF_HEAD)\n-\t\t\t\tcontinue;\n-\n-\t\t\t/*\n-\t\t\t * We only need to update HEAD separately in case it's\n-\t\t\t * detached. If it's not we'd already update the branch\n-\t\t\t * it is pointing to.\n-\t\t\t */\n-\t\t\tif (action == REF_ACTION_BRANCHES &&\n-\t\t\t    decoration->type == DECORATION_REF_HEAD &&\n-\t\t\t    !detached_head)\n-\t\t\t\tcontinue;\n-\n-\t\t\tret = ref_transaction_update(transaction,\n-\t\t\t\t\t\t     decoration->name,\n-\t\t\t\t\t\t     &rewritten->object.oid,\n-\t\t\t\t\t\t     &original->object.oid,\n-\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n-\t\t\tif (ret) {\n-\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n-\t\t\t\t\t    decoration->name, err.buf);\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\t\t}\n-\n-\t\tif (ref_transaction_commit(transaction, &err)) {\n-\t\t\tret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n+\t\tif (action == REF_ACTION_BRANCHES &&\n+\t\t    decoration->type == DECORATION_REF_HEAD &&\n+\t\t    !detached_head)\n+\t\t\tcontinue;\n+\n+\t\tret = handle_ref_update(transaction,\n+\t\t\t\t\tdecoration->name,\n+\t\t\t\t\t&rewritten->object.oid,\n+\t\t\t\t\t&original->object.oid,\n+\t\t\t\t\treflog_msg, &err);\n+\t\tif (ret) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    decoration->name, err.buf);\n \t\t\tgoto out;\n \t\t}\n+\t}\n \n-\t\tbreak;\n-\tcase REF_ACTION_PRINT:\n-\t\tfor (size_t i = 0; i < result.updates_nr; i++)\n-\t\t\tprintf(\"update %s %s %s\\n\",\n-\t\t\t       result.updates[i].refname,\n-\t\t\t       oid_to_hex(&result.updates[i].new_oid),\n-\t\t\t       oid_to_hex(&result.updates[i].old_oid));\n-\t\tbreak;\n-\tdefault:\n-\t\tBUG(\"unsupported ref action %d\", action);\n+\tif (transaction && ref_transaction_commit(transaction, &err)) {\n+\t\tret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n+\t\tgoto out;\n \t}\n \n \tret = 0;\n@@ -410,10 +415,13 @@ static int cmd_history_reword(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"ref-action\", &action, N_(\"<action>\"),\n-\t\t\t       N_(\"control ref update behavior (branches|head|print)\"),\n+\t\t\t       N_(\"control ref update behavior (branches|head)\"),\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n+\t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n+\t\t\t N_(\"perform a dry-run without updating any refs\")),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -450,7 +458,7 @@ static int cmd_history_reword(int argc,\n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf);\n+\t\t\t\t       reflog_msg.buf, dry_run);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 12a9a7d051..702d40dc06 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -221,7 +221,7 @@ test_expect_success 'can reword a merge commit' '\n \t)\n '\n \n-test_expect_success '--ref-action=print prints ref updates without modifying repo' '\n+test_expect_success '--dry-run prints ref updates without modifying repo' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\n \t(\n@@ -233,7 +233,15 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep\n \t\ttest_commit theirs &&\n \n \t\tgit refs list >refs-expect &&\n-\t\treword_with_message --ref-action=print base >updates <<-\\EOF &&\n+\t\treword_with_message --dry-run --ref-action=head base >updates <<-\\EOF &&\n+\t\treworded commit\n+\t\tEOF\n+\t\tgit refs list >refs-actual &&\n+\t\ttest_cmp refs-expect refs-actual &&\n+\t\ttest_grep \"update refs/heads/branch\" updates &&\n+\t\ttest_grep ! \"update refs/heads/main\" updates &&\n+\n+\t\treword_with_message --dry-run base >updates <<-\\EOF &&\n \t\treworded commit\n \t\tEOF\n \t\tgit refs list >refs-actual &&\n\n-- \n2.53.0.295.g64333814d3.dirty\n\n"},{"id":"535863","messageId":"20260212-b4-pks-history-dry-run-v1-4-1ce03d631c1b@pks.im","threadId":"64985","inReplyTo":"20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im","subject":"[PATCH 4/4] builtin/history: rename \"--ref-action=\" to \"--update-refs=\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-12T12:44:37Z","receivedAt":"2026-02-12T12:44:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With the preceding commit we have changed \"--ref-action=\" to only\ncontrol which refs are supposed to be updated, not what happens with\nthem. As a consequence, the option is now somewhat misnamed, as we don't\ncontrol the action itself anymore.\n\nRename it to \"--update-refs=\" to better align it with its new use.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc | 4 ++--\n builtin/history.c              | 8 ++++----\n t/t3451-history-reword.sh      | 8 ++++----\n 3 files changed, 10 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 33353815cb..b73fb009ab 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\n+git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n \n DESCRIPTION\n -----------\n@@ -64,7 +64,7 @@ OPTIONS\n \tDo not update any references, but instead print any ref updates in a\n \tformat that can be consumed by linkgit:git-update-ref[1].\n \n-`--ref-action=(branches|head)`::\n+`--update-refs=(branches|head)`::\n \tControl which references will be updated by the command, if any. With\n \t`branches`, all local branches that point to commits which are\n \tdescendants of the original commit will be rewritten. With `head`, only\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 40e2925cea..30ab10442c 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -18,7 +18,7 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -259,7 +259,7 @@ static int setup_revwalk(struct repository *repo,\n \t\t\tgoto out;\n \t\t} else if (!ret) {\n \t\t\tret = error(_(\"rewritten commit must be an ancestor \"\n-\t\t\t\t      \"of HEAD when using --ref-action=head\"));\n+\t\t\t\t      \"of HEAD when using --update-refs=head\"));\n \t\t\tgoto out;\n \t\t}\n \n@@ -417,8 +417,8 @@ static int cmd_history_reword(int argc,\n \tenum ref_action action = REF_ACTION_DEFAULT;\n \tint dry_run = 0;\n \tstruct option options[] = {\n-\t\tOPT_CALLBACK_F(0, \"ref-action\", &action, N_(\"<action>\"),\n-\t\t\t       N_(\"control ref update behavior (branches|head)\"),\n+\t\tOPT_CALLBACK_F(0, \"update-refs\", &action, N_(\"<action>\"),\n+\t\t\t       N_(\"control which refs should be updated (branches|head)\"),\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 702d40dc06..de7b357685 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -233,7 +233,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '\n \t\ttest_commit theirs &&\n \n \t\tgit refs list >refs-expect &&\n-\t\treword_with_message --dry-run --ref-action=head base >updates <<-\\EOF &&\n+\t\treword_with_message --dry-run --update-refs=head base >updates <<-\\EOF &&\n \t\treworded commit\n \t\tEOF\n \t\tgit refs list >refs-actual &&\n@@ -258,7 +258,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '\n \t)\n '\n \n-test_expect_success '--ref-action=head updates only HEAD' '\n+test_expect_success '--update-refs=head updates only HEAD' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\n \t(\n@@ -271,10 +271,10 @@ test_expect_success '--ref-action=head updates only HEAD' '\n \n \t\t# When told to update HEAD, only, the command will refuse to\n \t\t# rewrite commits that are not an ancestor of HEAD.\n-\t\ttest_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&\n+\t\ttest_must_fail git -c core.editor=false history reword --update-refs=head theirs 2>err &&\n \t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n \n-\t\treword_with_message --ref-action=head base >updates <<-\\EOF &&\n+\t\treword_with_message --update-refs=head base >updates <<-\\EOF &&\n \t\treworded base\n \t\tEOF\n \t\texpect_log HEAD <<-\\EOF &&\n\n-- \n2.53.0.295.g64333814d3.dirty\n\n"},{"id":"535883","messageId":"xmqqcy29ohi5.fsf@gitster.g","threadId":"64985","inReplyTo":"20260212-b4-pks-history-dry-run-v1-1-1ce03d631c1b@pks.im","subject":"Re: [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-12T20:04:50Z","receivedAt":"2026-02-12T20:04:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When setting up the revision walk in git-history(1) we also perform some\n> verifications whether the request actually looks sane. Unfortunately,\n> these verifications come _after_ we have already asked the user for the\n> commit message of the commit that is to be rewritten. So in case any of\n> the verifications fails, the user will have lost their modifications.\n>\n> Extract the function to set up the revision walk and call it before we\n> ask for user input to fix this.\n\nThat's a huge usability improvement.  Nice.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/history.c         | 69 +++++++++++++++++++++++++++++------------------\n>  t/t3451-history-reword.sh |  2 +-\n>  2 files changed, 44 insertions(+), 27 deletions(-)\n\n> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n> index 3594421b68..6775ed62f9 100755\n> --- a/t/t3451-history-reword.sh\n> +++ b/t/t3451-history-reword.sh\n> @@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '\n>  \n>  \t\t# When told to update HEAD, only, the command will refuse to\n>  \t\t# rewrite commits that are not an ancestor of HEAD.\n> -\t\ttest_must_fail git history reword --ref-action=head theirs 2>err &&\n> +\t\ttest_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&\n>  \t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n\nThis ensures that the editor is never consulted?  How?  Running the\n\"false\" editor would give us a different error, like \"your editor\nexited with non-zero status, telling us to abort\" or something?\n\nThanks.\n\n"},{"id":"535884","messageId":"xmqq5x81ogsx.fsf@gitster.g","threadId":"64985","inReplyTo":"20260212-b4-pks-history-dry-run-v1-3-1ce03d631c1b@pks.im","subject":"Re: [PATCH 3/4] builtin/history: replace \"--ref-action=print\" with \"--dry-run\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-12T20:19:58Z","receivedAt":"2026-02-12T20:20:01Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The git-history(1) command has the ability to perform a dry-run\n> that will not end up modifying any references. Instead, we'll only print\n> any ref updates that would happen as a consequence of performing the\n> operation.\n>\n> This mode is somewhat hidden though behind the \"--ref-action=print\"\n> option. This command line option has its origin in git-replay(1), where\n> it's probably an okayish interface as this command is sitting more on\n> the plumbing side of tools. But git-history(1) is a user-facing tool,\n> and this way of achieving a dry-run is way too technical and thus not\n> very discoverable.\n>\n> Besides usability issues, it also has another issue: the dry-run mode\n> will always operate as if the user wanted to rewrite all branches. But\n> in fact, the user also has the option to only update the HEAD reference,\n> and they might want to perform a dry-run of such an operation, too. We\n> could of course introduce \"--ref-actoin=print-head\", but that would\n> become even less ergonomic.\n>\n> Replace \"--ref-action=print\" with a new \"--dry-run\" toggle. This new\n> toggle works with both \"--ref-action={head,branches}\" and is way more\n> discoverable.\n>\n> Add a test to verify that both \"--ref-action=\" values behave as\n> expected.\n\nThis \"--dry-run\" mode still creates necessary new objects, right?\n\nDescribing this \"--dry-run\" as not updating any refs is a very good\nthing, which is what the documentation update says, but at the same\ntime, we should clearly promise that the necessary new objects are\nstill created, so should the user then choose to update refs to\npoint at the reported objects, it will not result in a repository\ncorruption.\n\nOther than that, looking good.\n\n"},{"id":"535891","messageId":"CALnO6CCciWF_GOd3WqOVwUDLoPRABNgDWbSu7FTgP8KLWQKvgQ@mail.gmail.com","threadId":"64985","inReplyTo":"20260212-b4-pks-history-dry-run-v1-2-1ce03d631c1b@pks.im","subject":"Re: [PATCH 2/4] builtin/history: check for merges before asking for user input","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-02-12T22:20:08Z","receivedAt":"2026-02-12T22:20:19Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> The replay infrastructure is not yet capable of replaying merge commits.\n> Unfortunately, we only notice that we're about to replay merges after we\n> have already asked the user for input, so any commit message that the\n> user may have written will be discarded in that case.\n>\n> Fix this by checking whether the revwalk contains merge commits before\n> we ask for user input.\n\nIndeed, that would be irritating :)\n\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/history.c         | 40 ++++++++++++++++++++++++++++++++++++++++\n>  t/t3451-history-reword.sh |  2 +-\n>  2 files changed, 41 insertions(+), 1 deletion(-)\n>\n> diff --git a/builtin/history.c b/builtin/history.c\n> index 1de51372ea..ca0cdb6a58 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -177,6 +177,42 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n>         return 0;\n>  }\n>\n> +static int revwalk_contains_merges(struct repository *repo,\n> +                                  const struct strvec *revwalk_args)\n> +{\n> +       struct strvec args = STRVEC_INIT;\n> +       struct rev_info revs;\n> +       int ret;\n> +\n> +       for (size_t i = 0; i < revwalk_args->nr; i++)\n> +               strvec_push(&args, revwalk_args->v[i]);\n\nI'm surprised we don't have anything like Rust's Vec::append or\nVec::extend_from_slice\n\n> +       strvec_push(&args, \"--min-parents=2\");\n\nAnd this is the key for detecting merges…\n\n> +\n> +       repo_init_revisions(repo, &revs, NULL);\n> +\n> +       setup_revisions_from_strvec(&args, &revs, NULL);\n> +       if (args.nr != 1)\n> +               BUG(\"revisions were set up with invalid argument\");\n> +\n> +       if (prepare_revision_walk(&revs) < 0) {\n> +               ret = error(_(\"error preparing revisions\"));\n> +               goto out;\n> +       }\n> +\n> +       if (get_revision(&revs)) {\n> +               ret = error(_(\"replaying merge commits is not supported yet!\"));\n> +               goto out;\n> +       }\n\n…which nearly confused me here, since I didn't see how get_revision\nwas supposed to tell if there was a merge! But it makes sense all\ntogether.\n\n> +\n> +       reset_revision_walk();\n> +       ret = 0;\n> +\n> +out:\n> +       release_revisions(&revs);\n> +       strvec_clear(&args);\n> +       return ret;\n> +}\n> +\n>  static int setup_revwalk(struct repository *repo,\n>                          enum ref_action action,\n>                          struct commit *original,\n> @@ -236,6 +272,10 @@ static int setup_revwalk(struct repository *repo,\n>                 strvec_push(&args, \"HEAD\");\n>         }\n>\n> +       ret = revwalk_contains_merges(repo, &args);\n> +       if (ret < 0)\n> +               goto out;\n> +\n>         setup_revisions_from_strvec(&args, revs, NULL);\n>         if (args.nr != 1)\n>                 BUG(\"revisions were set up with invalid argument\");\n> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n> index 6775ed62f9..12a9a7d051 100755\n> --- a/t/t3451-history-reword.sh\n> +++ b/t/t3451-history-reword.sh\n> @@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '\n>\n>                 # It is not possible to replay merge commits embedded in the\n>                 # history (yet).\n> -               test_must_fail git history reword HEAD~ 2>err &&\n> +               test_must_fail git -c core.editor=false history reword HEAD~ 2>err &&\n>                 test_grep \"replaying merge commits is not supported yet\" err &&\n>\n>                 # But it is possible to reword a merge commit directly.\n\nHm, I don't quite see how \"false\" tests that we don't invoke the\neditor at all, though I'm not sure if that behavior should be encoded\nin the test or not. (Looks like Junio noticed the same in 1/4; I'm\ngetting better at reading my mail before sending, sometimes… :)\n\nOtherwise looks good!\n\n\n--\nD. Ben Knoble\n"},{"id":"535892","messageId":"CALnO6CBterXWjecpRG7jTc4PaHDJLuY_FyrA0URC0TXBWSXgHQ@mail.gmail.com","threadId":"64985","inReplyTo":"20260212-b4-pks-history-dry-run-v1-3-1ce03d631c1b@pks.im","subject":"Re: [PATCH 3/4] builtin/history: replace \"--ref-action=print\" with \"--dry-run\"","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-02-12T22:20:13Z","receivedAt":"2026-02-12T22:20:24Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> The git-history(1) command has the ability to perform a dry-run\n> that will not end up modifying any references. Instead, we'll only print\n> any ref updates that would happen as a consequence of performing the\n> operation.\n>\n> This mode is somewhat hidden though behind the \"--ref-action=print\"\n> option. This command line option has its origin in git-replay(1), where\n> it's probably an okayish interface as this command is sitting more on\n> the plumbing side of tools. But git-history(1) is a user-facing tool,\n> and this way of achieving a dry-run is way too technical and thus not\n> very discoverable.\n>\n> Besides usability issues, it also has another issue: the dry-run mode\n> will always operate as if the user wanted to rewrite all branches. But\n> in fact, the user also has the option to only update the HEAD reference,\n> and they might want to perform a dry-run of such an operation, too. We\n> could of course introduce \"--ref-actoin=print-head\", but that would\n> become even less ergonomic.\n>\n> Replace \"--ref-action=print\" with a new \"--dry-run\" toggle. This new\n> toggle works with both \"--ref-action={head,branches}\" and is way more\n> discoverable.\n>\n> Add a test to verify that both \"--ref-action=\" values behave as\n> expected.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-history.adoc |  12 ++--\n>  builtin/history.c              | 150 ++++++++++++++++++++++-------------------\n>  t/t3451-history-reword.sh      |  12 +++-\n>  3 files changed, 96 insertions(+), 78 deletions(-)\n>\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 154e262b76..33353815cb 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history\n>  SYNOPSIS\n>  --------\n>  [synopsis]\n> -git history reword <commit> [--ref-action=(branches|head|print)]\n> +git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\n>\n>  DESCRIPTION\n>  -----------\n> @@ -60,13 +60,15 @@ The following commands are available to rewrite history in different ways:\n>  OPTIONS\n>  -------\n>\n> -`--ref-action=(branches|head|print)`::\n> +`--dry-run`::\n> +       Do not update any references, but instead print any ref updates in a\n> +       format that can be consumed by linkgit:git-update-ref[1].\n> +\n> +`--ref-action=(branches|head)`::\n>         Control which references will be updated by the command, if any. With\n>         `branches`, all local branches that point to commits which are\n>         descendants of the original commit will be rewritten. With `head`, only\n> -       the current `HEAD` reference will be rewritten. With `print`, all\n> -       updates as they would be performed with `branches` are printed in a\n> -       format that can be consumed by linkgit:git-update-ref[1].\n> +       the current `HEAD` reference will be rewritten.\n\nUnrelated to this series, I've just realized I can't tell from these\ndocs what the default --ref-action is. The code says \"branches.\"\n\n>\n>  GIT\n>  ---\n> diff --git a/builtin/history.c b/builtin/history.c\n> index ca0cdb6a58..40e2925cea 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -18,7 +18,7 @@\n>  #include \"wt-status.h\"\n>\n>  #define GIT_HISTORY_REWORD_USAGE \\\n> -       N_(\"git history reword <commit> [--ref-action=(branches|head|print)]\")\n> +       N_(\"git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\")\n>\n>  static void change_data_free(void *util, const char *str UNUSED)\n>  {\n> @@ -155,7 +155,6 @@ enum ref_action {\n>         REF_ACTION_DEFAULT,\n>         REF_ACTION_BRANCHES,\n>         REF_ACTION_HEAD,\n> -       REF_ACTION_PRINT,\n>  };\n>\n>  static int parse_ref_action(const struct option *opt, const char *value, int unset)\n> @@ -167,10 +166,8 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n>                 *action = REF_ACTION_BRANCHES;\n>         } else if (!strcmp(value, \"head\")) {\n>                 *action = REF_ACTION_HEAD;\n> -       } else if (!strcmp(value, \"print\")) {\n> -               *action = REF_ACTION_PRINT;\n>         } else {\n> -               return error(_(\"%s expects one of 'branches', 'head' or 'print'\"),\n> +               return error(_(\"%s expects one of 'branches' or 'head'\"),\n>                              opt->long_name);\n>         }\n>\n> @@ -287,11 +284,29 @@ static int setup_revwalk(struct repository *repo,\n>         return ret;\n>  }\n>\n> +static int handle_ref_update(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> +       if (!transaction) {\n> +               printf(\"update %s %s %s\\n\",\n> +                      refname, oid_to_hex(new_oid), oid_to_hex(old_oid));\n> +               return 0;\n> +       }\n> +\n> +       return ref_transaction_update(transaction, refname, new_oid, old_oid,\n> +                                     NULL, NULL, 0, reflog_msg, err);\n> +}\n> +\n>  static int handle_reference_updates(struct rev_info *revs,\n>                                     enum ref_action action,\n>                                     struct commit *original,\n>                                     struct commit *rewritten,\n> -                                   const char *reflog_msg)\n> +                                   const char *reflog_msg,\n> +                                   int dry_run)\n>  {\n>         const struct name_decoration *decoration;\n>         struct replay_revisions_options opts = { 0 };\n> @@ -313,82 +328,72 @@ static int handle_reference_updates(struct rev_info *revs,\n>         if (ret)\n>                 goto out;\n>\n> -       switch (action) {\n> -       case REF_ACTION_BRANCHES:\n> -       case REF_ACTION_HEAD:\n> +       if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)\n> +               BUG(\"unsupported ref action %d\", action);\n> +\n> +       if (!dry_run) {\n>                 transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);\n>                 if (!transaction) {\n>                         ret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n>                         goto out;\n>                 }\n> +       }\n\nIt took me longer than I'd like to admit to realize that only\ninitializing the transaction this way is safe, because we change the\nhandler below:\n\n>\n> -               for (size_t i = 0; i < result.updates_nr; i++) {\n> -                       ret = ref_transaction_update(transaction,\n> -                                                    result.updates[i].refname,\n> -                                                    &result.updates[i].new_oid,\n> -                                                    &result.updates[i].old_oid,\n> -                                                    NULL, NULL, 0, reflog_msg, &err);\n> -                       if (ret) {\n> -                               ret = error(_(\"failed to update ref '%s': %s\"),\n> -                                           result.updates[i].refname, err.buf);\n> -                               goto out;\n> -                       }\n> +       for (size_t i = 0; i < result.updates_nr; i++) {\n> +               ret = handle_ref_update(transaction,\n> +                                       result.updates[i].refname,\n> +                                       &result.updates[i].new_oid,\n> +                                       &result.updates[i].old_oid,\n> +                                       reflog_msg, &err);\n> +               if (ret) {\n> +                       ret = error(_(\"failed to update ref '%s': %s\"),\n> +                                   result.updates[i].refname, err.buf);\n> +                       goto out;\n>                 }\n> +       }\n\nTo use our new \"NULL-safe transaction update\" function. Phew. Using\n\"-b\" with git-diff/show/etc. helps quite a bit.\n\n> +\n> +       /*\n> +        * `replay_revisions()` only updates references that are\n> +        * ancestors of `rewritten`, so we need to manually\n> +        * handle updating references that point to `original`.\n> +        */\n> +       for (decoration = get_name_decoration(&original->object);\n> +            decoration;\n> +            decoration = decoration->next)\n> +       {\n> +               if (decoration->type != DECORATION_REF_LOCAL &&\n> +                   decoration->type != DECORATION_REF_HEAD)\n> +                       continue;\n> +\n> +               if (action == REF_ACTION_HEAD &&\n> +                   decoration->type != DECORATION_REF_HEAD)\n> +                       continue;\n>\n>                 /*\n> -                * `replay_revisions()` only updates references that are\n> -                * ancestors of `rewritten`, so we need to manually\n> -                * handle updating references that point to `original`.\n> +                * We only need to update HEAD separately in case it's\n> +                * detached. If it's not we'd already update the branch\n> +                * it is pointing to.\n>                  */\n> -               for (decoration = get_name_decoration(&original->object);\n> -                    decoration;\n> -                    decoration = decoration->next)\n> -               {\n> -                       if (decoration->type != DECORATION_REF_LOCAL &&\n> -                           decoration->type != DECORATION_REF_HEAD)\n> -                               continue;\n> -\n> -                       if (action == REF_ACTION_HEAD &&\n> -                           decoration->type != DECORATION_REF_HEAD)\n> -                               continue;\n> -\n> -                       /*\n> -                        * We only need to update HEAD separately in case it's\n> -                        * detached. If it's not we'd already update the branch\n> -                        * it is pointing to.\n> -                        */\n> -                       if (action == REF_ACTION_BRANCHES &&\n> -                           decoration->type == DECORATION_REF_HEAD &&\n> -                           !detached_head)\n> -                               continue;\n> -\n> -                       ret = ref_transaction_update(transaction,\n> -                                                    decoration->name,\n> -                                                    &rewritten->object.oid,\n> -                                                    &original->object.oid,\n> -                                                    NULL, NULL, 0, reflog_msg, &err);\n> -                       if (ret) {\n> -                               ret = error(_(\"failed to update ref '%s': %s\"),\n> -                                           decoration->name, err.buf);\n> -                               goto out;\n> -                       }\n> -               }\n> -\n> -               if (ref_transaction_commit(transaction, &err)) {\n> -                       ret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n> +               if (action == REF_ACTION_BRANCHES &&\n> +                   decoration->type == DECORATION_REF_HEAD &&\n> +                   !detached_head)\n> +                       continue;\n> +\n> +               ret = handle_ref_update(transaction,\n> +                                       decoration->name,\n> +                                       &rewritten->object.oid,\n> +                                       &original->object.oid,\n> +                                       reflog_msg, &err);\n> +               if (ret) {\n> +                       ret = error(_(\"failed to update ref '%s': %s\"),\n> +                                   decoration->name, err.buf);\n>                         goto out;\n>                 }\n> +       }\n>\n> -               break;\n> -       case REF_ACTION_PRINT:\n> -               for (size_t i = 0; i < result.updates_nr; i++)\n> -                       printf(\"update %s %s %s\\n\",\n> -                              result.updates[i].refname,\n> -                              oid_to_hex(&result.updates[i].new_oid),\n> -                              oid_to_hex(&result.updates[i].old_oid));\n> -               break;\n> -       default:\n> -               BUG(\"unsupported ref action %d\", action);\n> +       if (transaction && ref_transaction_commit(transaction, &err)) {\n> +               ret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n> +               goto out;\n>         }\n\nAll effectively whitespace change, except for the new transaction\ncheck, which is sensible.\n\n\n>\n>         ret = 0;\n> @@ -410,10 +415,13 @@ static int cmd_history_reword(int argc,\n>                 NULL,\n>         };\n>         enum ref_action action = REF_ACTION_DEFAULT;\n> +       int dry_run = 0;\n>         struct option options[] = {\n>                 OPT_CALLBACK_F(0, \"ref-action\", &action, N_(\"<action>\"),\n> -                              N_(\"control ref update behavior (branches|head|print)\"),\n> +                              N_(\"control ref update behavior (branches|head)\"),\n>                                PARSE_OPT_NONEG, parse_ref_action),\n> +               OPT_BOOL('n', \"dry-run\", &dry_run,\n> +                        N_(\"perform a dry-run without updating any refs\")),\n>                 OPT_END(),\n>         };\n>         struct strbuf reflog_msg = STRBUF_INIT;\n> @@ -450,7 +458,7 @@ static int cmd_history_reword(int argc,\n>         strbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n>\n>         ret = handle_reference_updates(&revs, action, original, rewritten,\n> -                                      reflog_msg.buf);\n> +                                      reflog_msg.buf, dry_run);\n>         if (ret < 0) {\n>                 ret = error(_(\"failed replaying descendants\"));\n>                 goto out;\n> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n> index 12a9a7d051..702d40dc06 100755\n> --- a/t/t3451-history-reword.sh\n> +++ b/t/t3451-history-reword.sh\n> @@ -221,7 +221,7 @@ test_expect_success 'can reword a merge commit' '\n>         )\n>  '\n>\n> -test_expect_success '--ref-action=print prints ref updates without modifying repo' '\n> +test_expect_success '--dry-run prints ref updates without modifying repo' '\n>         test_when_finished \"rm -rf repo\" &&\n>         git init repo --initial-branch=main &&\n>         (\n> @@ -233,7 +233,15 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep\n>                 test_commit theirs &&\n>\n>                 git refs list >refs-expect &&\n> -               reword_with_message --ref-action=print base >updates <<-\\EOF &&\n> +               reword_with_message --dry-run --ref-action=head base >updates <<-\\EOF &&\n> +               reworded commit\n> +               EOF\n> +               git refs list >refs-actual &&\n> +               test_cmp refs-expect refs-actual &&\n> +               test_grep \"update refs/heads/branch\" updates &&\n> +               test_grep ! \"update refs/heads/main\" updates &&\n> +\n> +               reword_with_message --dry-run base >updates <<-\\EOF &&\n>                 reworded commit\n>                 EOF\n>                 git refs list >refs-actual &&\n>\n> --\n> 2.53.0.295.g64333814d3.dirty\n>\n>\n\n\n--\nD. Ben Knoble\n"},{"id":"535895","messageId":"xmqq7bshmwd5.fsf@gitster.g","threadId":"64985","inReplyTo":"CALnO6CCciWF_GOd3WqOVwUDLoPRABNgDWbSu7FTgP8KLWQKvgQ@mail.gmail.com","subject":"Re: [PATCH 2/4] builtin/history: check for merges before asking for user input","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-12T22:26:46Z","receivedAt":"2026-02-12T22:26:49Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"D. Ben Knoble\" <ben.knoble@gmail.com> writes:\n\n> On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:\n>>\n>> The replay infrastructure is not yet capable of replaying merge commits.\n>> Unfortunately, we only notice that we're about to replay merges after we\n>> have already asked the user for input, so any commit message that the\n>> user may have written will be discarded in that case.\n>>\n>> Fix this by checking whether the revwalk contains merge commits before\n>> we ask for user input.\n>\n> Indeed, that would be irritating :)\n>\n>>\n>> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>> ---\n>>  builtin/history.c         | 40 ++++++++++++++++++++++++++++++++++++++++\n>>  t/t3451-history-reword.sh |  2 +-\n>>  2 files changed, 41 insertions(+), 1 deletion(-)\n>>\n>> diff --git a/builtin/history.c b/builtin/history.c\n>> index 1de51372ea..ca0cdb6a58 100644\n>> --- a/builtin/history.c\n>> +++ b/builtin/history.c\n>> @@ -177,6 +177,42 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n>>         return 0;\n>>  }\n>>\n>> +static int revwalk_contains_merges(struct repository *repo,\n>> +                                  const struct strvec *revwalk_args)\n>> +{\n>> +       struct strvec args = STRVEC_INIT;\n>> +       struct rev_info revs;\n>> +       int ret;\n>> +\n>> +       for (size_t i = 0; i < revwalk_args->nr; i++)\n>> +               strvec_push(&args, revwalk_args->v[i]);\n>\n> I'm surprised we don't have anything like Rust's Vec::append or\n> Vec::extend_from_slice\n\nstrvec_pushv() is what you are looking for.\n"},{"id":"535907","messageId":"aY67u1Iks4iPBMgl@pks.im","threadId":"64985","inReplyTo":"CALnO6CBterXWjecpRG7jTc4PaHDJLuY_FyrA0URC0TXBWSXgHQ@mail.gmail.com","subject":"Re: [PATCH 3/4] builtin/history: replace \"--ref-action=print\" with \"--dry-run\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T05:50:51Z","receivedAt":"2026-02-13T05:50:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Feb 12, 2026 at 05:20:13PM -0500, D. Ben Knoble wrote:\n> On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index 154e262b76..33353815cb 100644\n> > --- a/Documentation/git-history.adoc\n> > +++ b/Documentation/git-history.adoc\n> > @@ -60,13 +60,15 @@ The following commands are available to rewrite history in different ways:\n> >  OPTIONS\n> >  -------\n> >\n> > -`--ref-action=(branches|head|print)`::\n> > +`--dry-run`::\n> > +       Do not update any references, but instead print any ref updates in a\n> > +       format that can be consumed by linkgit:git-update-ref[1].\n> > +\n> > +`--ref-action=(branches|head)`::\n> >         Control which references will be updated by the command, if any. With\n> >         `branches`, all local branches that point to commits which are\n> >         descendants of the original commit will be rewritten. With `head`, only\n> > -       the current `HEAD` reference will be rewritten. With `print`, all\n> > -       updates as they would be performed with `branches` are printed in a\n> > -       format that can be consumed by linkgit:git-update-ref[1].\n> > +       the current `HEAD` reference will be rewritten.\n> \n> Unrelated to this series, I've just realized I can't tell from these\n> docs what the default --ref-action is. The code says \"branches.\"\n\nGood point indeed. I'll add another patch on top, thanks!\n\nPatrick\n"},{"id":"535908","messageId":"aY67wfgsxJhiSiEL@pks.im","threadId":"64985","inReplyTo":"xmqq5x81ogsx.fsf@gitster.g","subject":"Re: [PATCH 3/4] builtin/history: replace \"--ref-action=print\" with \"--dry-run\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T05:50:57Z","receivedAt":"2026-02-13T05:51:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Feb 12, 2026 at 12:19:58PM -0800, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > The git-history(1) command has the ability to perform a dry-run\n> > that will not end up modifying any references. Instead, we'll only print\n> > any ref updates that would happen as a consequence of performing the\n> > operation.\n> >\n> > This mode is somewhat hidden though behind the \"--ref-action=print\"\n> > option. This command line option has its origin in git-replay(1), where\n> > it's probably an okayish interface as this command is sitting more on\n> > the plumbing side of tools. But git-history(1) is a user-facing tool,\n> > and this way of achieving a dry-run is way too technical and thus not\n> > very discoverable.\n> >\n> > Besides usability issues, it also has another issue: the dry-run mode\n> > will always operate as if the user wanted to rewrite all branches. But\n> > in fact, the user also has the option to only update the HEAD reference,\n> > and they might want to perform a dry-run of such an operation, too. We\n> > could of course introduce \"--ref-actoin=print-head\", but that would\n> > become even less ergonomic.\n> >\n> > Replace \"--ref-action=print\" with a new \"--dry-run\" toggle. This new\n> > toggle works with both \"--ref-action={head,branches}\" and is way more\n> > discoverable.\n> >\n> > Add a test to verify that both \"--ref-action=\" values behave as\n> > expected.\n> \n> This \"--dry-run\" mode still creates necessary new objects, right?\n> \n> Describing this \"--dry-run\" as not updating any refs is a very good\n> thing, which is what the documentation update says, but at the same\n> time, we should clearly promise that the necessary new objects are\n> still created, so should the user then choose to update refs to\n> point at the reported objects, it will not result in a repository\n> corruption.\n> \n> Other than that, looking good.\n\nGood point, will add.\n\nPatrick\n"},{"id":"535909","messageId":"aY67ycgnisRjeguw@pks.im","threadId":"64985","inReplyTo":"xmqqcy29ohi5.fsf@gitster.g","subject":"Re: [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T05:51:05Z","receivedAt":"2026-02-13T05:51:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Feb 12, 2026 at 12:04:50PM -0800, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n> > index 3594421b68..6775ed62f9 100755\n> > --- a/t/t3451-history-reword.sh\n> > +++ b/t/t3451-history-reword.sh\n> > @@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '\n> >  \n> >  \t\t# When told to update HEAD, only, the command will refuse to\n> >  \t\t# rewrite commits that are not an ancestor of HEAD.\n> > -\t\ttest_must_fail git history reword --ref-action=head theirs 2>err &&\n> > +\t\ttest_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&\n> >  \t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n> \n> This ensures that the editor is never consulted?  How?  Running the\n> \"false\" editor would give us a different error, like \"your editor\n> exited with non-zero status, telling us to abort\" or something?\n\nYup, exactly that. We'd see \"Aborting commit as launching the editor\nfailed.\" instead of the above error message.\n\nPatrick\n"},{"id":"535910","messageId":"aY670nh0ElOZU46j@pks.im","threadId":"64985","inReplyTo":"CALnO6CCciWF_GOd3WqOVwUDLoPRABNgDWbSu7FTgP8KLWQKvgQ@mail.gmail.com","subject":"Re: [PATCH 2/4] builtin/history: check for merges before asking for user input","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T05:51:14Z","receivedAt":"2026-02-13T05:51:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Feb 12, 2026 at 05:20:08PM -0500, D. Ben Knoble wrote:\n> On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n> > index 6775ed62f9..12a9a7d051 100755\n> > --- a/t/t3451-history-reword.sh\n> > +++ b/t/t3451-history-reword.sh\n> > @@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '\n> >\n> >                 # It is not possible to replay merge commits embedded in the\n> >                 # history (yet).\n> > -               test_must_fail git history reword HEAD~ 2>err &&\n> > +               test_must_fail git -c core.editor=false history reword HEAD~ 2>err &&\n> >                 test_grep \"replaying merge commits is not supported yet\" err &&\n> >\n> >                 # But it is possible to reword a merge commit directly.\n> \n> Hm, I don't quite see how \"false\" tests that we don't invoke the\n> editor at all, though I'm not sure if that behavior should be encoded\n> in the test or not. (Looks like Junio noticed the same in 1/4; I'm\n> getting better at reading my mail before sending, sometimes… :)\n> \n> Otherwise looks good!\n\nAs replied in the other thread, this would abort with \"Aborting commit\nas launching the editor failed.\" if Git tried to execute the editor.\n\nI'm not quite sure whether I get the other remark about \"should be\nencoded in the test or not\". Do you mean to say we should use a separate\ntest?\n\nPatrick\n"},{"id":"535911","messageId":"aY672KjOwxyTFJli@pks.im","threadId":"64985","inReplyTo":"xmqq7bshmwd5.fsf@gitster.g","subject":"Re: [PATCH 2/4] builtin/history: check for merges before asking for user input","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T05:51:20Z","receivedAt":"2026-02-13T05:51:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Feb 12, 2026 at 02:26:46PM -0800, Junio C Hamano wrote:\n> \"D. Ben Knoble\" <ben.knoble@gmail.com> writes:\n> > On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >> diff --git a/builtin/history.c b/builtin/history.c\n> >> index 1de51372ea..ca0cdb6a58 100644\n> >> --- a/builtin/history.c\n> >> +++ b/builtin/history.c\n> >> @@ -177,6 +177,42 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n> >>         return 0;\n> >>  }\n> >>\n> >> +static int revwalk_contains_merges(struct repository *repo,\n> >> +                                  const struct strvec *revwalk_args)\n> >> +{\n> >> +       struct strvec args = STRVEC_INIT;\n> >> +       struct rev_info revs;\n> >> +       int ret;\n> >> +\n> >> +       for (size_t i = 0; i < revwalk_args->nr; i++)\n> >> +               strvec_push(&args, revwalk_args->v[i]);\n> >\n> > I'm surprised we don't have anything like Rust's Vec::append or\n> > Vec::extend_from_slice\n> \n> strvec_pushv() is what you are looking for.\n\nAh, indeed, will use.\n\nPatrick\n"},{"id":"535913","messageId":"20260213-b4-pks-history-dry-run-v2-0-756ac376e9e5@pks.im","threadId":"64985","inReplyTo":"20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im","subject":"[PATCH v2 0/5] builtin/history: some smaller UI improvements","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T09:12:46Z","receivedAt":"2026-02-13T09:12:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series contains a small set of UI improvements for\ngit-history(1):\n\n  - The first two commits adapt git-history(1) so that it performs\n    verifications before asking the user for input.\n\n  - The last two commits rework the \"--ref-action=\" option to instead be\n    split up into \"--dry-run\" and \"--update-refs=\" so that the option is\n    less focussed on technical implementation details.\n\nI decided to send this patch series as a small incremental step before\nsending `git history split`, also because that series conflicts with\naa/add-p-no-auto-advance.\n\nChanges in v2:\n  - Use `strvec_pushv()` instead of looping around `strvec_push()`.\n  - Document that \"--dry-run\" writes objects for later use.\n  - Document the default value of \"--update-refs=\".\n  - Mention the subtlety around false(1) in the commit messages.\n  - Link to v1: https://lore.kernel.org/r/20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (5):\n      builtin/history: perform revwalk checks before asking for user input\n      builtin/history: check for merges before asking for user input\n      builtin/history: replace \"--ref-action=print\" with \"--dry-run\"\n      builtin/history: rename \"--ref-action=\" to \"--update-refs=\"\n      Documentation/git-history: document default for \"--update-refs=\"\n\n Documentation/git-history.adoc |  14 ++-\n builtin/history.c              | 254 ++++++++++++++++++++++++++---------------\n t/t3451-history-reword.sh      |  20 +++-\n 3 files changed, 182 insertions(+), 106 deletions(-)\n\nRange-diff versus v1:\n\n1:  a4b0654c49 ! 1:  495a8e7a1a builtin/history: perform revwalk checks before asking for user input\n    @@ Commit message\n         Extract the function to set up the revision walk and call it before we\n         ask for user input to fix this.\n     \n    +    Adapt one of the tests that is expected to fail because of this check\n    +    to use false(1) as editor. If the editor had been executed by Git, it\n    +    would fail with the error message \"Aborting commit as launching the\n    +    editor failed.\"\n    +\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## builtin/history.c ##\n2:  3ea8f7740a ! 2:  13c5edbe7b builtin/history: check for merges before asking for user input\n    @@ Commit message\n         Fix this by checking whether the revwalk contains merge commits before\n         we ask for user input.\n     \n    +    Adapt one of the tests that is expected to fail because of this check\n    +    to use false(1) as editor. If the editor had been executed by Git, it\n    +    would fail with the error message \"Aborting commit as launching the\n    +    editor failed.\"\n    +\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## builtin/history.c ##\n    @@ builtin/history.c: static int parse_ref_action(const struct option *opt, const c\n     +\tstruct rev_info revs;\n     +\tint ret;\n     +\n    -+\tfor (size_t i = 0; i < revwalk_args->nr; i++)\n    -+\t\tstrvec_push(&args, revwalk_args->v[i]);\n    ++\tstrvec_pushv(&args, revwalk_args->v);\n     +\tstrvec_push(&args, \"--min-parents=2\");\n     +\n     +\trepo_init_revisions(repo, &revs, NULL);\n3:  4e605e65e0 ! 3:  7226c4d1af builtin/history: replace \"--ref-action=print\" with \"--dry-run\"\n    @@ Commit message\n         Add a test to verify that both \"--ref-action=\" values behave as\n         expected.\n     \n    +    This patch is best viewed with \"--ignore-space-change\".\n    +\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Documentation/git-history.adoc ##\n    @@ Documentation/git-history.adoc: The following commands are available to rewrite\n     -`--ref-action=(branches|head|print)`::\n     +`--dry-run`::\n     +\tDo not update any references, but instead print any ref updates in a\n    -+\tformat that can be consumed by linkgit:git-update-ref[1].\n    ++\tformat that can be consumed by linkgit:git-update-ref[1]. Necessary new\n    ++\tobjects will be written into the repository, so applying these printed\n    ++\tref updates is generally safe.\n     +\n     +`--ref-action=(branches|head)`::\n      \tControl which references will be updated by the command, if any. With\n4:  1da7284902 ! 4:  7f41026981 builtin/history: rename \"--ref-action=\" to \"--update-refs=\"\n    @@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history\n      DESCRIPTION\n      -----------\n     @@ Documentation/git-history.adoc: OPTIONS\n    - \tDo not update any references, but instead print any ref updates in a\n    - \tformat that can be consumed by linkgit:git-update-ref[1].\n    + \tobjects will be written into the repository, so applying these printed\n    + \tref updates is generally safe.\n      \n     -`--ref-action=(branches|head)`::\n     +`--update-refs=(branches|head)`::\n-:  ---------- > 5:  0bc831fcab Documentation/git-history: document default for \"--update-refs=\"\n\n---\nbase-commit: 6fcee4785280a08e7f271bd015a4dc33753e2886\nchange-id: 20260212-b4-pks-history-dry-run-2b840e530ae6\n\n"},{"id":"535914","messageId":"20260213-b4-pks-history-dry-run-v2-1-756ac376e9e5@pks.im","threadId":"64985","inReplyTo":"20260213-b4-pks-history-dry-run-v2-0-756ac376e9e5@pks.im","subject":"[PATCH v2 1/5] builtin/history: perform revwalk checks before asking for user input","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T09:12:47Z","receivedAt":"2026-02-13T09:13:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When setting up the revision walk in git-history(1) we also perform some\nverifications whether the request actually looks sane. Unfortunately,\nthese verifications come _after_ we have already asked the user for the\ncommit message of the commit that is to be rewritten. So in case any of\nthe verifications fails, the user will have lost their modifications.\n\nExtract the function to set up the revision walk and call it before we\nask for user input to fix this.\n\nAdapt one of the tests that is expected to fail because of this check\nto use false(1) as editor. If the editor had been executed by Git, it\nwould fail with the error message \"Aborting commit as launching the\neditor failed.\"\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/history.c         | 69 +++++++++++++++++++++++++++++------------------\n t/t3451-history-reword.sh |  2 +-\n 2 files changed, 44 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 8dcb9a6046..1de51372ea 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -177,30 +177,15 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n \treturn 0;\n }\n \n-static int handle_reference_updates(enum ref_action action,\n-\t\t\t\t    struct repository *repo,\n-\t\t\t\t    struct commit *original,\n-\t\t\t\t    struct commit *rewritten,\n-\t\t\t\t    const char *reflog_msg)\n+static int setup_revwalk(struct repository *repo,\n+\t\t\t enum ref_action action,\n+\t\t\t struct commit *original,\n+\t\t\t struct rev_info *revs)\n {\n-\tconst struct name_decoration *decoration;\n-\tstruct replay_revisions_options opts = { 0 };\n-\tstruct replay_result result = { 0 };\n-\tstruct ref_transaction *transaction = NULL;\n \tstruct strvec args = STRVEC_INIT;\n-\tstruct strbuf err = STRBUF_INIT;\n-\tstruct commit *head = NULL;\n-\tstruct rev_info revs;\n-\tchar hex[GIT_MAX_HEXSZ + 1];\n-\tbool detached_head;\n-\tint head_flags = 0;\n \tint ret;\n \n-\trefs_read_ref_full(get_main_ref_store(repo), \"HEAD\",\n-\t\t\t   RESOLVE_REF_NO_RECURSE, NULL, &head_flags);\n-\tdetached_head = !(head_flags & REF_ISSYMREF);\n-\n-\trepo_init_revisions(repo, &revs, NULL);\n+\trepo_init_revisions(repo, revs, NULL);\n \tstrvec_push(&args, \"ignored\");\n \tstrvec_push(&args, \"--reverse\");\n \tstrvec_push(&args, \"--topo-order\");\n@@ -224,6 +209,7 @@ static int handle_reference_updates(enum ref_action action,\n \t */\n \tif (action == REF_ACTION_HEAD) {\n \t\tstruct commit_list *from_list = NULL;\n+\t\tstruct commit *head;\n \n \t\thead = lookup_commit_reference_by_name(\"HEAD\");\n \t\tif (!head) {\n@@ -250,20 +236,47 @@ static int handle_reference_updates(enum ref_action action,\n \t\tstrvec_push(&args, \"HEAD\");\n \t}\n \n-\tsetup_revisions_from_strvec(&args, &revs, NULL);\n+\tsetup_revisions_from_strvec(&args, revs, NULL);\n \tif (args.nr != 1)\n \t\tBUG(\"revisions were set up with invalid argument\");\n \n+\tret = 0;\n+\n+out:\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n+static int handle_reference_updates(struct rev_info *revs,\n+\t\t\t\t    enum ref_action action,\n+\t\t\t\t    struct commit *original,\n+\t\t\t\t    struct commit *rewritten,\n+\t\t\t\t    const char *reflog_msg)\n+{\n+\tconst struct name_decoration *decoration;\n+\tstruct replay_revisions_options opts = { 0 };\n+\tstruct replay_result result = { 0 };\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar hex[GIT_MAX_HEXSZ + 1];\n+\tbool detached_head;\n+\tint head_flags = 0;\n+\tint ret;\n+\n+\trefs_read_ref_full(get_main_ref_store(revs->repo), \"HEAD\",\n+\t\t\t   RESOLVE_REF_NO_RECURSE, NULL, &head_flags);\n+\tdetached_head = !(head_flags & REF_ISSYMREF);\n+\n \topts.onto = oid_to_hex_r(hex, &rewritten->object.oid);\n \n-\tret = replay_revisions(&revs, &opts, &result);\n+\tret = replay_revisions(revs, &opts, &result);\n \tif (ret)\n \t\tgoto out;\n \n \tswitch (action) {\n \tcase REF_ACTION_BRANCHES:\n \tcase REF_ACTION_HEAD:\n-\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);\n \t\tif (!transaction) {\n \t\t\tret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n \t\t\tgoto out;\n@@ -343,9 +356,7 @@ static int handle_reference_updates(enum ref_action action,\n out:\n \tref_transaction_free(transaction);\n \treplay_result_release(&result);\n-\trelease_revisions(&revs);\n \tstrbuf_release(&err);\n-\tstrvec_clear(&args);\n \treturn ret;\n }\n \n@@ -367,6 +378,7 @@ static int cmd_history_reword(int argc,\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n \tstruct commit *original, *rewritten;\n+\tstruct rev_info revs;\n \tint ret;\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n@@ -385,6 +397,10 @@ static int cmd_history_reword(int argc,\n \t\tgoto out;\n \t}\n \n+\tret = setup_revwalk(repo, action, original, &revs);\n+\tif (ret)\n+\t\tgoto out;\n+\n \tret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing reworded commit\"));\n@@ -393,7 +409,7 @@ static int cmd_history_reword(int argc,\n \n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n-\tret = handle_reference_updates(action, repo, original, rewritten,\n+\tret = handle_reference_updates(&revs, action, original, rewritten,\n \t\t\t\t       reflog_msg.buf);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n@@ -404,6 +420,7 @@ static int cmd_history_reword(int argc,\n \n out:\n \tstrbuf_release(&reflog_msg);\n+\trelease_revisions(&revs);\n \treturn ret;\n }\n \ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 3594421b68..6775ed62f9 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '\n \n \t\t# When told to update HEAD, only, the command will refuse to\n \t\t# rewrite commits that are not an ancestor of HEAD.\n-\t\ttest_must_fail git history reword --ref-action=head theirs 2>err &&\n+\t\ttest_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&\n \t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n \n \t\treword_with_message --ref-action=head base >updates <<-\\EOF &&\n\n-- \n2.53.0.352.gd1286b26eb.dirty\n\n"},{"id":"535915","messageId":"20260213-b4-pks-history-dry-run-v2-2-756ac376e9e5@pks.im","threadId":"64985","inReplyTo":"20260213-b4-pks-history-dry-run-v2-0-756ac376e9e5@pks.im","subject":"[PATCH v2 2/5] builtin/history: check for merges before asking for user input","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T09:12:48Z","receivedAt":"2026-02-13T09:13:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The replay infrastructure is not yet capable of replaying merge commits.\nUnfortunately, we only notice that we're about to replay merges after we\nhave already asked the user for input, so any commit message that the\nuser may have written will be discarded in that case.\n\nFix this by checking whether the revwalk contains merge commits before\nwe ask for user input.\n\nAdapt one of the tests that is expected to fail because of this check\nto use false(1) as editor. If the editor had been executed by Git, it\nwould fail with the error message \"Aborting commit as launching the\neditor failed.\"\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/history.c         | 39 +++++++++++++++++++++++++++++++++++++++\n t/t3451-history-reword.sh |  2 +-\n 2 files changed, 40 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 1de51372ea..ff90e93d6e 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -177,6 +177,41 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n \treturn 0;\n }\n \n+static int revwalk_contains_merges(struct repository *repo,\n+\t\t\t\t   const struct strvec *revwalk_args)\n+{\n+\tstruct strvec args = STRVEC_INIT;\n+\tstruct rev_info revs;\n+\tint ret;\n+\n+\tstrvec_pushv(&args, revwalk_args->v);\n+\tstrvec_push(&args, \"--min-parents=2\");\n+\n+\trepo_init_revisions(repo, &revs, NULL);\n+\n+\tsetup_revisions_from_strvec(&args, &revs, NULL);\n+\tif (args.nr != 1)\n+\t\tBUG(\"revisions were set up with invalid argument\");\n+\n+\tif (prepare_revision_walk(&revs) < 0) {\n+\t\tret = error(_(\"error preparing revisions\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (get_revision(&revs)) {\n+\t\tret = error(_(\"replaying merge commits is not supported yet!\"));\n+\t\tgoto out;\n+\t}\n+\n+\treset_revision_walk();\n+\tret = 0;\n+\n+out:\n+\trelease_revisions(&revs);\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n static int setup_revwalk(struct repository *repo,\n \t\t\t enum ref_action action,\n \t\t\t struct commit *original,\n@@ -236,6 +271,10 @@ static int setup_revwalk(struct repository *repo,\n \t\tstrvec_push(&args, \"HEAD\");\n \t}\n \n+\tret = revwalk_contains_merges(repo, &args);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n \tsetup_revisions_from_strvec(&args, revs, NULL);\n \tif (args.nr != 1)\n \t\tBUG(\"revisions were set up with invalid argument\");\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 6775ed62f9..12a9a7d051 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '\n \n \t\t# It is not possible to replay merge commits embedded in the\n \t\t# history (yet).\n-\t\ttest_must_fail git history reword HEAD~ 2>err &&\n+\t\ttest_must_fail git -c core.editor=false history reword HEAD~ 2>err &&\n \t\ttest_grep \"replaying merge commits is not supported yet\" err &&\n \n \t\t# But it is possible to reword a merge commit directly.\n\n-- \n2.53.0.352.gd1286b26eb.dirty\n\n"},{"id":"535916","messageId":"20260213-b4-pks-history-dry-run-v2-3-756ac376e9e5@pks.im","threadId":"64985","inReplyTo":"20260213-b4-pks-history-dry-run-v2-0-756ac376e9e5@pks.im","subject":"[PATCH v2 3/5] builtin/history: replace \"--ref-action=print\" with \"--dry-run\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T09:12:49Z","receivedAt":"2026-02-13T09:13:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The git-history(1) command has the ability to perform a dry-run\nthat will not end up modifying any references. Instead, we'll only print\nany ref updates that would happen as a consequence of performing the\noperation.\n\nThis mode is somewhat hidden though behind the \"--ref-action=print\"\noption. This command line option has its origin in git-replay(1), where\nit's probably an okayish interface as this command is sitting more on\nthe plumbing side of tools. But git-history(1) is a user-facing tool,\nand this way of achieving a dry-run is way too technical and thus not\nvery discoverable.\n\nBesides usability issues, it also has another issue: the dry-run mode\nwill always operate as if the user wanted to rewrite all branches. But\nin fact, the user also has the option to only update the HEAD reference,\nand they might want to perform a dry-run of such an operation, too. We\ncould of course introduce \"--ref-actoin=print-head\", but that would\nbecome even less ergonomic.\n\nReplace \"--ref-action=print\" with a new \"--dry-run\" toggle. This new\ntoggle works with both \"--ref-action={head,branches}\" and is way more\ndiscoverable.\n\nAdd a test to verify that both \"--ref-action=\" values behave as\nexpected.\n\nThis patch is best viewed with \"--ignore-space-change\".\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  14 ++--\n builtin/history.c              | 150 ++++++++++++++++++++++-------------------\n t/t3451-history-reword.sh      |  12 +++-\n 3 files changed, 98 insertions(+), 78 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 154e262b76..df2900ac2f 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history reword <commit> [--ref-action=(branches|head|print)]\n+git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\n \n DESCRIPTION\n -----------\n@@ -60,13 +60,17 @@ The following commands are available to rewrite history in different ways:\n OPTIONS\n -------\n \n-`--ref-action=(branches|head|print)`::\n+`--dry-run`::\n+\tDo not update any references, but instead print any ref updates in a\n+\tformat that can be consumed by linkgit:git-update-ref[1]. Necessary new\n+\tobjects will be written into the repository, so applying these printed\n+\tref updates is generally safe.\n+\n+`--ref-action=(branches|head)`::\n \tControl which references will be updated by the command, if any. With\n \t`branches`, all local branches that point to commits which are\n \tdescendants of the original commit will be rewritten. With `head`, only\n-\tthe current `HEAD` reference will be rewritten. With `print`, all\n-\tupdates as they would be performed with `branches` are printed in a\n-\tformat that can be consumed by linkgit:git-update-ref[1].\n+\tthe current `HEAD` reference will be rewritten.\n \n GIT\n ---\ndiff --git a/builtin/history.c b/builtin/history.c\nindex ff90e93d6e..c135361c67 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -18,7 +18,7 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--ref-action=(branches|head|print)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -155,7 +155,6 @@ enum ref_action {\n \tREF_ACTION_DEFAULT,\n \tREF_ACTION_BRANCHES,\n \tREF_ACTION_HEAD,\n-\tREF_ACTION_PRINT,\n };\n \n static int parse_ref_action(const struct option *opt, const char *value, int unset)\n@@ -167,10 +166,8 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n \t\t*action = REF_ACTION_BRANCHES;\n \t} else if (!strcmp(value, \"head\")) {\n \t\t*action = REF_ACTION_HEAD;\n-\t} else if (!strcmp(value, \"print\")) {\n-\t\t*action = REF_ACTION_PRINT;\n \t} else {\n-\t\treturn error(_(\"%s expects one of 'branches', 'head' or 'print'\"),\n+\t\treturn error(_(\"%s expects one of 'branches' or 'head'\"),\n \t\t\t     opt->long_name);\n \t}\n \n@@ -286,11 +283,29 @@ static int setup_revwalk(struct repository *repo,\n \treturn ret;\n }\n \n+static int handle_ref_update(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+\tif (!transaction) {\n+\t\tprintf(\"update %s %s %s\\n\",\n+\t\t       refname, oid_to_hex(new_oid), oid_to_hex(old_oid));\n+\t\treturn 0;\n+\t}\n+\n+\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n+\t\t\t\t      NULL, NULL, 0, reflog_msg, err);\n+}\n+\n static int handle_reference_updates(struct rev_info *revs,\n \t\t\t\t    enum ref_action action,\n \t\t\t\t    struct commit *original,\n \t\t\t\t    struct commit *rewritten,\n-\t\t\t\t    const char *reflog_msg)\n+\t\t\t\t    const char *reflog_msg,\n+\t\t\t\t    int dry_run)\n {\n \tconst struct name_decoration *decoration;\n \tstruct replay_revisions_options opts = { 0 };\n@@ -312,82 +327,72 @@ static int handle_reference_updates(struct rev_info *revs,\n \tif (ret)\n \t\tgoto out;\n \n-\tswitch (action) {\n-\tcase REF_ACTION_BRANCHES:\n-\tcase REF_ACTION_HEAD:\n+\tif (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)\n+\t\tBUG(\"unsupported ref action %d\", action);\n+\n+\tif (!dry_run) {\n \t\ttransaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);\n \t\tif (!transaction) {\n \t\t\tret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n \t\t\tgoto out;\n \t\t}\n+\t}\n \n-\t\tfor (size_t i = 0; i < result.updates_nr; i++) {\n-\t\t\tret = ref_transaction_update(transaction,\n-\t\t\t\t\t\t     result.updates[i].refname,\n-\t\t\t\t\t\t     &result.updates[i].new_oid,\n-\t\t\t\t\t\t     &result.updates[i].old_oid,\n-\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n-\t\t\tif (ret) {\n-\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n-\t\t\t\t\t    result.updates[i].refname, err.buf);\n-\t\t\t\tgoto out;\n-\t\t\t}\n+\tfor (size_t i = 0; i < result.updates_nr; i++) {\n+\t\tret = handle_ref_update(transaction,\n+\t\t\t\t\tresult.updates[i].refname,\n+\t\t\t\t\t&result.updates[i].new_oid,\n+\t\t\t\t\t&result.updates[i].old_oid,\n+\t\t\t\t\treflog_msg, &err);\n+\t\tif (ret) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    result.updates[i].refname, err.buf);\n+\t\t\tgoto out;\n \t\t}\n+\t}\n+\n+\t/*\n+\t * `replay_revisions()` only updates references that are\n+\t * ancestors of `rewritten`, so we need to manually\n+\t * handle updating references that point to `original`.\n+\t */\n+\tfor (decoration = get_name_decoration(&original->object);\n+\t     decoration;\n+\t     decoration = decoration->next)\n+\t{\n+\t\tif (decoration->type != DECORATION_REF_LOCAL &&\n+\t\t    decoration->type != DECORATION_REF_HEAD)\n+\t\t\tcontinue;\n+\n+\t\tif (action == REF_ACTION_HEAD &&\n+\t\t    decoration->type != DECORATION_REF_HEAD)\n+\t\t\tcontinue;\n \n \t\t/*\n-\t\t * `replay_revisions()` only updates references that are\n-\t\t * ancestors of `rewritten`, so we need to manually\n-\t\t * handle updating references that point to `original`.\n+\t\t * We only need to update HEAD separately in case it's\n+\t\t * detached. If it's not we'd already update the branch\n+\t\t * it is pointing to.\n \t\t */\n-\t\tfor (decoration = get_name_decoration(&original->object);\n-\t\t     decoration;\n-\t\t     decoration = decoration->next)\n-\t\t{\n-\t\t\tif (decoration->type != DECORATION_REF_LOCAL &&\n-\t\t\t    decoration->type != DECORATION_REF_HEAD)\n-\t\t\t\tcontinue;\n-\n-\t\t\tif (action == REF_ACTION_HEAD &&\n-\t\t\t    decoration->type != DECORATION_REF_HEAD)\n-\t\t\t\tcontinue;\n-\n-\t\t\t/*\n-\t\t\t * We only need to update HEAD separately in case it's\n-\t\t\t * detached. If it's not we'd already update the branch\n-\t\t\t * it is pointing to.\n-\t\t\t */\n-\t\t\tif (action == REF_ACTION_BRANCHES &&\n-\t\t\t    decoration->type == DECORATION_REF_HEAD &&\n-\t\t\t    !detached_head)\n-\t\t\t\tcontinue;\n-\n-\t\t\tret = ref_transaction_update(transaction,\n-\t\t\t\t\t\t     decoration->name,\n-\t\t\t\t\t\t     &rewritten->object.oid,\n-\t\t\t\t\t\t     &original->object.oid,\n-\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n-\t\t\tif (ret) {\n-\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n-\t\t\t\t\t    decoration->name, err.buf);\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\t\t}\n-\n-\t\tif (ref_transaction_commit(transaction, &err)) {\n-\t\t\tret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n+\t\tif (action == REF_ACTION_BRANCHES &&\n+\t\t    decoration->type == DECORATION_REF_HEAD &&\n+\t\t    !detached_head)\n+\t\t\tcontinue;\n+\n+\t\tret = handle_ref_update(transaction,\n+\t\t\t\t\tdecoration->name,\n+\t\t\t\t\t&rewritten->object.oid,\n+\t\t\t\t\t&original->object.oid,\n+\t\t\t\t\treflog_msg, &err);\n+\t\tif (ret) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    decoration->name, err.buf);\n \t\t\tgoto out;\n \t\t}\n+\t}\n \n-\t\tbreak;\n-\tcase REF_ACTION_PRINT:\n-\t\tfor (size_t i = 0; i < result.updates_nr; i++)\n-\t\t\tprintf(\"update %s %s %s\\n\",\n-\t\t\t       result.updates[i].refname,\n-\t\t\t       oid_to_hex(&result.updates[i].new_oid),\n-\t\t\t       oid_to_hex(&result.updates[i].old_oid));\n-\t\tbreak;\n-\tdefault:\n-\t\tBUG(\"unsupported ref action %d\", action);\n+\tif (transaction && ref_transaction_commit(transaction, &err)) {\n+\t\tret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n+\t\tgoto out;\n \t}\n \n \tret = 0;\n@@ -409,10 +414,13 @@ static int cmd_history_reword(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"ref-action\", &action, N_(\"<action>\"),\n-\t\t\t       N_(\"control ref update behavior (branches|head|print)\"),\n+\t\t\t       N_(\"control ref update behavior (branches|head)\"),\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n+\t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n+\t\t\t N_(\"perform a dry-run without updating any refs\")),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -449,7 +457,7 @@ static int cmd_history_reword(int argc,\n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf);\n+\t\t\t\t       reflog_msg.buf, dry_run);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 12a9a7d051..702d40dc06 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -221,7 +221,7 @@ test_expect_success 'can reword a merge commit' '\n \t)\n '\n \n-test_expect_success '--ref-action=print prints ref updates without modifying repo' '\n+test_expect_success '--dry-run prints ref updates without modifying repo' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\n \t(\n@@ -233,7 +233,15 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep\n \t\ttest_commit theirs &&\n \n \t\tgit refs list >refs-expect &&\n-\t\treword_with_message --ref-action=print base >updates <<-\\EOF &&\n+\t\treword_with_message --dry-run --ref-action=head base >updates <<-\\EOF &&\n+\t\treworded commit\n+\t\tEOF\n+\t\tgit refs list >refs-actual &&\n+\t\ttest_cmp refs-expect refs-actual &&\n+\t\ttest_grep \"update refs/heads/branch\" updates &&\n+\t\ttest_grep ! \"update refs/heads/main\" updates &&\n+\n+\t\treword_with_message --dry-run base >updates <<-\\EOF &&\n \t\treworded commit\n \t\tEOF\n \t\tgit refs list >refs-actual &&\n\n-- \n2.53.0.352.gd1286b26eb.dirty\n\n"},{"id":"535917","messageId":"20260213-b4-pks-history-dry-run-v2-4-756ac376e9e5@pks.im","threadId":"64985","inReplyTo":"20260213-b4-pks-history-dry-run-v2-0-756ac376e9e5@pks.im","subject":"[PATCH v2 4/5] builtin/history: rename \"--ref-action=\" to \"--update-refs=\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T09:12:50Z","receivedAt":"2026-02-13T09:13:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With the preceding commit we have changed \"--ref-action=\" to only\ncontrol which refs are supposed to be updated, not what happens with\nthem. As a consequence, the option is now somewhat misnamed, as we don't\ncontrol the action itself anymore.\n\nRename it to \"--update-refs=\" to better align it with its new use.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc | 4 ++--\n builtin/history.c              | 8 ++++----\n t/t3451-history-reword.sh      | 8 ++++----\n 3 files changed, 10 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex df2900ac2f..4dbe665ec4 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\n+git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n \n DESCRIPTION\n -----------\n@@ -66,7 +66,7 @@ OPTIONS\n \tobjects will be written into the repository, so applying these printed\n \tref updates is generally safe.\n \n-`--ref-action=(branches|head)`::\n+`--update-refs=(branches|head)`::\n \tControl which references will be updated by the command, if any. With\n \t`branches`, all local branches that point to commits which are\n \tdescendants of the original commit will be rewritten. With `head`, only\ndiff --git a/builtin/history.c b/builtin/history.c\nindex c135361c67..1cf6c668cf 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -18,7 +18,7 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -258,7 +258,7 @@ static int setup_revwalk(struct repository *repo,\n \t\t\tgoto out;\n \t\t} else if (!ret) {\n \t\t\tret = error(_(\"rewritten commit must be an ancestor \"\n-\t\t\t\t      \"of HEAD when using --ref-action=head\"));\n+\t\t\t\t      \"of HEAD when using --update-refs=head\"));\n \t\t\tgoto out;\n \t\t}\n \n@@ -416,8 +416,8 @@ static int cmd_history_reword(int argc,\n \tenum ref_action action = REF_ACTION_DEFAULT;\n \tint dry_run = 0;\n \tstruct option options[] = {\n-\t\tOPT_CALLBACK_F(0, \"ref-action\", &action, N_(\"<action>\"),\n-\t\t\t       N_(\"control ref update behavior (branches|head)\"),\n+\t\tOPT_CALLBACK_F(0, \"update-refs\", &action, N_(\"<action>\"),\n+\t\t\t       N_(\"control which refs should be updated (branches|head)\"),\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 702d40dc06..de7b357685 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -233,7 +233,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '\n \t\ttest_commit theirs &&\n \n \t\tgit refs list >refs-expect &&\n-\t\treword_with_message --dry-run --ref-action=head base >updates <<-\\EOF &&\n+\t\treword_with_message --dry-run --update-refs=head base >updates <<-\\EOF &&\n \t\treworded commit\n \t\tEOF\n \t\tgit refs list >refs-actual &&\n@@ -258,7 +258,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '\n \t)\n '\n \n-test_expect_success '--ref-action=head updates only HEAD' '\n+test_expect_success '--update-refs=head updates only HEAD' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\n \t(\n@@ -271,10 +271,10 @@ test_expect_success '--ref-action=head updates only HEAD' '\n \n \t\t# When told to update HEAD, only, the command will refuse to\n \t\t# rewrite commits that are not an ancestor of HEAD.\n-\t\ttest_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&\n+\t\ttest_must_fail git -c core.editor=false history reword --update-refs=head theirs 2>err &&\n \t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n \n-\t\treword_with_message --ref-action=head base >updates <<-\\EOF &&\n+\t\treword_with_message --update-refs=head base >updates <<-\\EOF &&\n \t\treworded base\n \t\tEOF\n \t\texpect_log HEAD <<-\\EOF &&\n\n-- \n2.53.0.352.gd1286b26eb.dirty\n\n"},{"id":"535918","messageId":"20260213-b4-pks-history-dry-run-v2-5-756ac376e9e5@pks.im","threadId":"64985","inReplyTo":"20260213-b4-pks-history-dry-run-v2-0-756ac376e9e5@pks.im","subject":"[PATCH v2 5/5] Documentation/git-history: document default for \"--update-refs=\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-13T09:12:51Z","receivedAt":"2026-02-13T09:13:11Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we document the values that can be passed to the \"--update-refs=\"\noption, we don't give the user any hint what the default behaviour is.\nDocument it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 4dbe665ec4..cc019de697 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -70,7 +70,7 @@ OPTIONS\n \tControl which references will be updated by the command, if any. With\n \t`branches`, all local branches that point to commits which are\n \tdescendants of the original commit will be rewritten. With `head`, only\n-\tthe current `HEAD` reference will be rewritten.\n+\tthe current `HEAD` reference will be rewritten. Defaults to `branches`.\n \n GIT\n ---\n\n-- \n2.53.0.352.gd1286b26eb.dirty\n\n"},{"id":"535929","messageId":"3C51B8C4-E594-44ED-97F0-0EB7C36A3BCB@gmail.com","threadId":"64985","inReplyTo":"aY670nh0ElOZU46j@pks.im","subject":"Re: [PATCH 2/4] builtin/history: check for merges before asking for user input","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-02-13T13:42:35Z","receivedAt":"2026-02-13T13:42:47Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 13 févr. 2026 à 00:51, Patrick Steinhardt <ps@pks.im> a écrit :\n> \n> ﻿On Thu, Feb 12, 2026 at 05:20:08PM -0500, D. Ben Knoble wrote:\n>>> On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:\n>>> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n>>> index 6775ed62f9..12a9a7d051 100755\n>>> --- a/t/t3451-history-reword.sh\n>>> +++ b/t/t3451-history-reword.sh\n>>> @@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '\n>>> \n>>>                # It is not possible to replay merge commits embedded in the\n>>>                # history (yet).\n>>> -               test_must_fail git history reword HEAD~ 2>err &&\n>>> +               test_must_fail git -c core.editor=false history reword HEAD~ 2>err &&\n>>>                test_grep \"replaying merge commits is not supported yet\" err &&\n>>> \n>>>                # But it is possible to reword a merge commit directly.\n>> \n>> Hm, I don't quite see how \"false\" tests that we don't invoke the\n>> editor at all, though I'm not sure if that behavior should be encoded\n>> in the test or not. (Looks like Junio noticed the same in 1/4; I'm\n>> getting better at reading my mail before sending, sometimes… :)\n>> \n>> Otherwise looks good!\n> \n> As replied in the other thread, this would abort with \"Aborting commit\n> as launching the editor failed.\" if Git tried to execute the editor.\n> \n> I'm not quite sure whether I get the other remark about \"should be\n> encoded in the test or not\". Do you mean to say we should use a separate\n> test?\n> \n> Patrick\n\nNo, just musing on whether “did not launch editor” is an important behavior to avoid regressing. I think yes, and I have no real objection to it :)"},{"id":"535943","messageId":"xmqq1pioo9v4.fsf@gitster.g","threadId":"64985","inReplyTo":"aY67ycgnisRjeguw@pks.im","subject":"Re: [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-13T17:02:07Z","receivedAt":"2026-02-13T17:02:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Thu, Feb 12, 2026 at 12:04:50PM -0800, Junio C Hamano wrote:\n>> Patrick Steinhardt <ps@pks.im> writes:\n>> > diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n>> > index 3594421b68..6775ed62f9 100755\n>> > --- a/t/t3451-history-reword.sh\n>> > +++ b/t/t3451-history-reword.sh\n>> > @@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '\n>> >  \n>> >  \t\t# When told to update HEAD, only, the command will refuse to\n>> >  \t\t# rewrite commits that are not an ancestor of HEAD.\n>> > -\t\ttest_must_fail git history reword --ref-action=head theirs 2>err &&\n>> > +\t\ttest_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&\n>> >  \t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n>> \n>> This ensures that the editor is never consulted?  How?  Running the\n>> \"false\" editor would give us a different error, like \"your editor\n>> exited with non-zero status, telling us to abort\" or something?\n>\n> Yup, exactly that. We'd see \"Aborting commit as launching the editor\n> failed.\" instead of the above error message.\n\nI see at least two people wondered during the review, so perhaps\n\n \t\t# When told to update HEAD, only, the command will refuse to\n \t\t# rewrite commits that are not an ancestor of HEAD.\n-\t\ttest_must_fail git history reword --ref-action=head theirs 2>err &&\n+\t\t# Use the \"false\" editor that shows a different error when run\n+\t\ttest_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&\n  \t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n\nwould help future readers.  Or it might be too much.\n"},{"id":"535946","messageId":"xmqqseb4mufi.fsf@gitster.g","threadId":"64985","inReplyTo":"20260213-b4-pks-history-dry-run-v2-1-756ac376e9e5@pks.im","subject":"Re: [PATCH v2 1/5] builtin/history: perform revwalk checks before asking for user input","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-13T17:20:49Z","receivedAt":"2026-02-13T17:20:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Adapt one of the tests that is expected to fail because of this check\n> to use false(1) as editor. If the editor had been executed by Git, it\n> would fail with the error message \"Aborting commit as launching the\n> editor failed.\"\n\nI very much appreciate this new description.  OK, we expect the test\nto fail due to the check in question, and ensure that the editor is\nnot invoked, by looking at the error message and making sure it is\nnot the abort due to the editor returning false.\n\n"},{"id":"535947","messageId":"xmqqo6lsmued.fsf@gitster.g","threadId":"64985","inReplyTo":"20260213-b4-pks-history-dry-run-v2-5-756ac376e9e5@pks.im","subject":"Re: [PATCH v2 5/5] Documentation/git-history: document default for \"--update-refs=\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-13T17:21:30Z","receivedAt":"2026-02-13T17:21:33Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> While we document the values that can be passed to the \"--update-refs=\"\n> option, we don't give the user any hint what the default behaviour is.\n> Document it.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-history.adoc | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n>\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 4dbe665ec4..cc019de697 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -70,7 +70,7 @@ OPTIONS\n>  \tControl which references will be updated by the command, if any. With\n>  \t`branches`, all local branches that point to commits which are\n>  \tdescendants of the original commit will be rewritten. With `head`, only\n> -\tthe current `HEAD` reference will be rewritten.\n> +\tthe current `HEAD` reference will be rewritten. Defaults to `branches`.\n\nGood to see the default clearly described.  Thanks.\n\n"},{"id":"535949","messageId":"315801b6-bc9e-42b4-8356-12fd8b83223c@app.fastmail.com","threadId":"64985","inReplyTo":"20260213-b4-pks-history-dry-run-v2-3-756ac376e9e5@pks.im","subject":"Re: [PATCH v2 3/5] builtin/history: replace \"--ref-action=print\" with \"--dry-run\"","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-13T17:30:10Z","receivedAt":"2026-02-13T17:30:36Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Fri, Feb 13, 2026, at 10:12, Patrick Steinhardt wrote:\n> The git-history(1) command has the ability to perform a dry-run\n> that will not end up modifying any references. Instead, we'll only print\n> any ref updates that would happen as a consequence of performing the\n> operation.\n>\n> This mode is somewhat hidden though behind the \"--ref-action=print\"\n> option. This command line option has its origin in git-replay(1), where\n> it's probably an okayish interface as this command is sitting more on\n> the plumbing side of tools. But git-history(1) is a user-facing tool,\n> and this way of achieving a dry-run is way too technical and thus not\n> very discoverable.\n\nIt makes sense to use this command as a dry run and then pass the output\non to git-update-ref(1) for the wet run.\n\n    git history --dry-run ... | git update-ref --stdin\n\nLooks good.\n\n>\n> Besides usability issues, it also has another issue: the dry-run mode\n> will always operate as if the user wanted to rewrite all branches. But\n> in fact, the user also has the option to only update the HEAD reference,\n> and they might want to perform a dry-run of such an operation, too. We\n> could of course introduce \"--ref-actoin=print-head\", but that would\n\ns/--ref-actoin/--ref-action/\n\n> become even less ergonomic.\n>\n> Replace \"--ref-action=print\" with a new \"--dry-run\" toggle. This new\n> toggle works with both \"--ref-action={head,branches}\" and is way more\n> discoverable.\n>\n> Add a test to verify that both \"--ref-action=\" values behave as\n> expected.\n>\n> This patch is best viewed with \"--ignore-space-change\".\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>[snip]\n> +`--ref-action=(branches|head)`::\n>  \tControl which references will be updated by the command, if any. With\n>  \t`branches`, all local branches that point to commits which are\n>  \tdescendants of the original commit will be rewritten. With `head`, only\n> -\tthe current `HEAD` reference will be rewritten. With `print`, all\n> -\tupdates as they would be performed with `branches` are printed in a\n> -\tformat that can be consumed by linkgit:git-update-ref[1].\n> +\tthe current `HEAD` reference will be rewritten.\n\n`HEAD` is mentioned here because it could be detached `HEAD`. So you\ncan’t just say the current branch.\n\n“the current `HEAD` reference” seems a bit much. Is this less precise?\n\n     only `HEAD` is rewritten.\n\n>\n>  GIT\n>  ---\n>[snip]\n"},{"id":"536081","messageId":"aZK7jmvLwu1evxUp@pks.im","threadId":"64985","inReplyTo":"315801b6-bc9e-42b4-8356-12fd8b83223c@app.fastmail.com","subject":"Re: [PATCH v2 3/5] builtin/history: replace \"--ref-action=print\" with \"--dry-run\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-16T06:39:10Z","receivedAt":"2026-02-16T06:39:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Feb 13, 2026 at 06:30:10PM +0100, Kristoffer Haugsbakk wrote:\n[snip]\n> > Besides usability issues, it also has another issue: the dry-run mode\n> > will always operate as if the user wanted to rewrite all branches. But\n> > in fact, the user also has the option to only update the HEAD reference,\n> > and they might want to perform a dry-run of such an operation, too. We\n> > could of course introduce \"--ref-actoin=print-head\", but that would\n> \n> s/--ref-actoin/--ref-action/\n\nIndeed, I've queued this fix locally and will send it out with the next\nversion.\n\n> >[snip]\n> > +`--ref-action=(branches|head)`::\n> >  \tControl which references will be updated by the command, if any. With\n> >  \t`branches`, all local branches that point to commits which are\n> >  \tdescendants of the original commit will be rewritten. With `head`, only\n> > -\tthe current `HEAD` reference will be rewritten. With `print`, all\n> > -\tupdates as they would be performed with `branches` are printed in a\n> > -\tformat that can be consumed by linkgit:git-update-ref[1].\n> > +\tthe current `HEAD` reference will be rewritten.\n> \n> `HEAD` is mentioned here because it could be detached `HEAD`. So you\n> can’t just say the current branch.\n> \n> “the current `HEAD` reference” seems a bit much. Is this less precise?\n\nIt's not, but this commit doesn't rewrite any of the description, it\nonly deletes the sentence that becomes out-of-date now. So I'd prefer to\nkeep this as-is if you don't mind.\n\nIf you feel strongly I'm happy to add another commit on top.\n\nThanks!\n\nPatrick\n"},{"id":"536082","messageId":"20260216-b4-pks-history-dry-run-v3-0-c4db58a651fc@pks.im","threadId":"64985","inReplyTo":"20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im","subject":"[PATCH v3 0/5] builtin/history: some smaller UI improvements","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-16T06:45:43Z","receivedAt":"2026-02-16T06:45:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series contains a small set of UI improvements for\ngit-history(1):\n\n  - The first two commits adapt git-history(1) so that it performs\n    verifications before asking the user for input.\n\n  - The last two commits rework the \"--ref-action=\" option to instead be\n    split up into \"--dry-run\" and \"--update-refs=\" so that the option is\n    less focussed on technical implementation details.\n\nI decided to send this patch series as a small incremental step before\nsending `git history split`, also because that series conflicts with\naa/add-p-no-auto-advance.\n\nChanges in v3:\n  - Fix a typo in the commit message.\n  - Link to v2: https://lore.kernel.org/r/20260213-b4-pks-history-dry-run-v2-0-756ac376e9e5@pks.im\n\nChanges in v2:\n  - Use `strvec_pushv()` instead of looping around `strvec_push()`.\n  - Document that \"--dry-run\" writes objects for later use.\n  - Document the default value of \"--update-refs=\".\n  - Mention the subtlety around false(1) in the commit messages.\n  - Link to v1: https://lore.kernel.org/r/20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (5):\n      builtin/history: perform revwalk checks before asking for user input\n      builtin/history: check for merges before asking for user input\n      builtin/history: replace \"--ref-action=print\" with \"--dry-run\"\n      builtin/history: rename \"--ref-action=\" to \"--update-refs=\"\n      Documentation/git-history: document default for \"--update-refs=\"\n\n Documentation/git-history.adoc |  14 ++-\n builtin/history.c              | 254 ++++++++++++++++++++++++++---------------\n t/t3451-history-reword.sh      |  20 +++-\n 3 files changed, 182 insertions(+), 106 deletions(-)\n\nRange-diff versus v2:\n\n1:  26db5f0113 = 1:  21b4933c39 builtin/history: perform revwalk checks before asking for user input\n2:  9bcde0b932 = 2:  37e42cf770 builtin/history: check for merges before asking for user input\n3:  e070d8ecfa ! 3:  fcfd80b2b5 builtin/history: replace \"--ref-action=print\" with \"--dry-run\"\n    @@ Commit message\n         will always operate as if the user wanted to rewrite all branches. But\n         in fact, the user also has the option to only update the HEAD reference,\n         and they might want to perform a dry-run of such an operation, too. We\n    -    could of course introduce \"--ref-actoin=print-head\", but that would\n    +    could of course introduce \"--ref-action=print-head\", but that would\n         become even less ergonomic.\n     \n         Replace \"--ref-action=print\" with a new \"--dry-run\" toggle. This new\n4:  59f470b158 = 4:  8333a4ebd6 builtin/history: rename \"--ref-action=\" to \"--update-refs=\"\n5:  bde39d43a9 = 5:  9ef41103f0 Documentation/git-history: document default for \"--update-refs=\"\n\n---\nbase-commit: 6fcee4785280a08e7f271bd015a4dc33753e2886\nchange-id: 20260212-b4-pks-history-dry-run-2b840e530ae6\n\n"},{"id":"536083","messageId":"20260216-b4-pks-history-dry-run-v3-1-c4db58a651fc@pks.im","threadId":"64985","inReplyTo":"20260216-b4-pks-history-dry-run-v3-0-c4db58a651fc@pks.im","subject":"[PATCH v3 1/5] builtin/history: perform revwalk checks before asking for user input","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-16T06:45:44Z","receivedAt":"2026-02-16T06:45:56Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When setting up the revision walk in git-history(1) we also perform some\nverifications whether the request actually looks sane. Unfortunately,\nthese verifications come _after_ we have already asked the user for the\ncommit message of the commit that is to be rewritten. So in case any of\nthe verifications fails, the user will have lost their modifications.\n\nExtract the function to set up the revision walk and call it before we\nask for user input to fix this.\n\nAdapt one of the tests that is expected to fail because of this check\nto use false(1) as editor. If the editor had been executed by Git, it\nwould fail with the error message \"Aborting commit as launching the\neditor failed.\"\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/history.c         | 69 +++++++++++++++++++++++++++++------------------\n t/t3451-history-reword.sh |  2 +-\n 2 files changed, 44 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 8dcb9a6046..1de51372ea 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -177,30 +177,15 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n \treturn 0;\n }\n \n-static int handle_reference_updates(enum ref_action action,\n-\t\t\t\t    struct repository *repo,\n-\t\t\t\t    struct commit *original,\n-\t\t\t\t    struct commit *rewritten,\n-\t\t\t\t    const char *reflog_msg)\n+static int setup_revwalk(struct repository *repo,\n+\t\t\t enum ref_action action,\n+\t\t\t struct commit *original,\n+\t\t\t struct rev_info *revs)\n {\n-\tconst struct name_decoration *decoration;\n-\tstruct replay_revisions_options opts = { 0 };\n-\tstruct replay_result result = { 0 };\n-\tstruct ref_transaction *transaction = NULL;\n \tstruct strvec args = STRVEC_INIT;\n-\tstruct strbuf err = STRBUF_INIT;\n-\tstruct commit *head = NULL;\n-\tstruct rev_info revs;\n-\tchar hex[GIT_MAX_HEXSZ + 1];\n-\tbool detached_head;\n-\tint head_flags = 0;\n \tint ret;\n \n-\trefs_read_ref_full(get_main_ref_store(repo), \"HEAD\",\n-\t\t\t   RESOLVE_REF_NO_RECURSE, NULL, &head_flags);\n-\tdetached_head = !(head_flags & REF_ISSYMREF);\n-\n-\trepo_init_revisions(repo, &revs, NULL);\n+\trepo_init_revisions(repo, revs, NULL);\n \tstrvec_push(&args, \"ignored\");\n \tstrvec_push(&args, \"--reverse\");\n \tstrvec_push(&args, \"--topo-order\");\n@@ -224,6 +209,7 @@ static int handle_reference_updates(enum ref_action action,\n \t */\n \tif (action == REF_ACTION_HEAD) {\n \t\tstruct commit_list *from_list = NULL;\n+\t\tstruct commit *head;\n \n \t\thead = lookup_commit_reference_by_name(\"HEAD\");\n \t\tif (!head) {\n@@ -250,20 +236,47 @@ static int handle_reference_updates(enum ref_action action,\n \t\tstrvec_push(&args, \"HEAD\");\n \t}\n \n-\tsetup_revisions_from_strvec(&args, &revs, NULL);\n+\tsetup_revisions_from_strvec(&args, revs, NULL);\n \tif (args.nr != 1)\n \t\tBUG(\"revisions were set up with invalid argument\");\n \n+\tret = 0;\n+\n+out:\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n+static int handle_reference_updates(struct rev_info *revs,\n+\t\t\t\t    enum ref_action action,\n+\t\t\t\t    struct commit *original,\n+\t\t\t\t    struct commit *rewritten,\n+\t\t\t\t    const char *reflog_msg)\n+{\n+\tconst struct name_decoration *decoration;\n+\tstruct replay_revisions_options opts = { 0 };\n+\tstruct replay_result result = { 0 };\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tchar hex[GIT_MAX_HEXSZ + 1];\n+\tbool detached_head;\n+\tint head_flags = 0;\n+\tint ret;\n+\n+\trefs_read_ref_full(get_main_ref_store(revs->repo), \"HEAD\",\n+\t\t\t   RESOLVE_REF_NO_RECURSE, NULL, &head_flags);\n+\tdetached_head = !(head_flags & REF_ISSYMREF);\n+\n \topts.onto = oid_to_hex_r(hex, &rewritten->object.oid);\n \n-\tret = replay_revisions(&revs, &opts, &result);\n+\tret = replay_revisions(revs, &opts, &result);\n \tif (ret)\n \t\tgoto out;\n \n \tswitch (action) {\n \tcase REF_ACTION_BRANCHES:\n \tcase REF_ACTION_HEAD:\n-\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);\n \t\tif (!transaction) {\n \t\t\tret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n \t\t\tgoto out;\n@@ -343,9 +356,7 @@ static int handle_reference_updates(enum ref_action action,\n out:\n \tref_transaction_free(transaction);\n \treplay_result_release(&result);\n-\trelease_revisions(&revs);\n \tstrbuf_release(&err);\n-\tstrvec_clear(&args);\n \treturn ret;\n }\n \n@@ -367,6 +378,7 @@ static int cmd_history_reword(int argc,\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n \tstruct commit *original, *rewritten;\n+\tstruct rev_info revs;\n \tint ret;\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n@@ -385,6 +397,10 @@ static int cmd_history_reword(int argc,\n \t\tgoto out;\n \t}\n \n+\tret = setup_revwalk(repo, action, original, &revs);\n+\tif (ret)\n+\t\tgoto out;\n+\n \tret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing reworded commit\"));\n@@ -393,7 +409,7 @@ static int cmd_history_reword(int argc,\n \n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n-\tret = handle_reference_updates(action, repo, original, rewritten,\n+\tret = handle_reference_updates(&revs, action, original, rewritten,\n \t\t\t\t       reflog_msg.buf);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n@@ -404,6 +420,7 @@ static int cmd_history_reword(int argc,\n \n out:\n \tstrbuf_release(&reflog_msg);\n+\trelease_revisions(&revs);\n \treturn ret;\n }\n \ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 3594421b68..6775ed62f9 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '\n \n \t\t# When told to update HEAD, only, the command will refuse to\n \t\t# rewrite commits that are not an ancestor of HEAD.\n-\t\ttest_must_fail git history reword --ref-action=head theirs 2>err &&\n+\t\ttest_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&\n \t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n \n \t\treword_with_message --ref-action=head base >updates <<-\\EOF &&\n\n-- \n2.53.0.352.gd1286b26eb.dirty\n\n"},{"id":"536084","messageId":"20260216-b4-pks-history-dry-run-v3-2-c4db58a651fc@pks.im","threadId":"64985","inReplyTo":"20260216-b4-pks-history-dry-run-v3-0-c4db58a651fc@pks.im","subject":"[PATCH v3 2/5] builtin/history: check for merges before asking for user input","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-16T06:45:45Z","receivedAt":"2026-02-16T06:45:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The replay infrastructure is not yet capable of replaying merge commits.\nUnfortunately, we only notice that we're about to replay merges after we\nhave already asked the user for input, so any commit message that the\nuser may have written will be discarded in that case.\n\nFix this by checking whether the revwalk contains merge commits before\nwe ask for user input.\n\nAdapt one of the tests that is expected to fail because of this check\nto use false(1) as editor. If the editor had been executed by Git, it\nwould fail with the error message \"Aborting commit as launching the\neditor failed.\"\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/history.c         | 39 +++++++++++++++++++++++++++++++++++++++\n t/t3451-history-reword.sh |  2 +-\n 2 files changed, 40 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 1de51372ea..ff90e93d6e 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -177,6 +177,41 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n \treturn 0;\n }\n \n+static int revwalk_contains_merges(struct repository *repo,\n+\t\t\t\t   const struct strvec *revwalk_args)\n+{\n+\tstruct strvec args = STRVEC_INIT;\n+\tstruct rev_info revs;\n+\tint ret;\n+\n+\tstrvec_pushv(&args, revwalk_args->v);\n+\tstrvec_push(&args, \"--min-parents=2\");\n+\n+\trepo_init_revisions(repo, &revs, NULL);\n+\n+\tsetup_revisions_from_strvec(&args, &revs, NULL);\n+\tif (args.nr != 1)\n+\t\tBUG(\"revisions were set up with invalid argument\");\n+\n+\tif (prepare_revision_walk(&revs) < 0) {\n+\t\tret = error(_(\"error preparing revisions\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (get_revision(&revs)) {\n+\t\tret = error(_(\"replaying merge commits is not supported yet!\"));\n+\t\tgoto out;\n+\t}\n+\n+\treset_revision_walk();\n+\tret = 0;\n+\n+out:\n+\trelease_revisions(&revs);\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n static int setup_revwalk(struct repository *repo,\n \t\t\t enum ref_action action,\n \t\t\t struct commit *original,\n@@ -236,6 +271,10 @@ static int setup_revwalk(struct repository *repo,\n \t\tstrvec_push(&args, \"HEAD\");\n \t}\n \n+\tret = revwalk_contains_merges(repo, &args);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n \tsetup_revisions_from_strvec(&args, revs, NULL);\n \tif (args.nr != 1)\n \t\tBUG(\"revisions were set up with invalid argument\");\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 6775ed62f9..12a9a7d051 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '\n \n \t\t# It is not possible to replay merge commits embedded in the\n \t\t# history (yet).\n-\t\ttest_must_fail git history reword HEAD~ 2>err &&\n+\t\ttest_must_fail git -c core.editor=false history reword HEAD~ 2>err &&\n \t\ttest_grep \"replaying merge commits is not supported yet\" err &&\n \n \t\t# But it is possible to reword a merge commit directly.\n\n-- \n2.53.0.352.gd1286b26eb.dirty\n\n"},{"id":"536085","messageId":"20260216-b4-pks-history-dry-run-v3-3-c4db58a651fc@pks.im","threadId":"64985","inReplyTo":"20260216-b4-pks-history-dry-run-v3-0-c4db58a651fc@pks.im","subject":"[PATCH v3 3/5] builtin/history: replace \"--ref-action=print\" with \"--dry-run\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-16T06:45:46Z","receivedAt":"2026-02-16T06:46:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The git-history(1) command has the ability to perform a dry-run\nthat will not end up modifying any references. Instead, we'll only print\nany ref updates that would happen as a consequence of performing the\noperation.\n\nThis mode is somewhat hidden though behind the \"--ref-action=print\"\noption. This command line option has its origin in git-replay(1), where\nit's probably an okayish interface as this command is sitting more on\nthe plumbing side of tools. But git-history(1) is a user-facing tool,\nand this way of achieving a dry-run is way too technical and thus not\nvery discoverable.\n\nBesides usability issues, it also has another issue: the dry-run mode\nwill always operate as if the user wanted to rewrite all branches. But\nin fact, the user also has the option to only update the HEAD reference,\nand they might want to perform a dry-run of such an operation, too. We\ncould of course introduce \"--ref-action=print-head\", but that would\nbecome even less ergonomic.\n\nReplace \"--ref-action=print\" with a new \"--dry-run\" toggle. This new\ntoggle works with both \"--ref-action={head,branches}\" and is way more\ndiscoverable.\n\nAdd a test to verify that both \"--ref-action=\" values behave as\nexpected.\n\nThis patch is best viewed with \"--ignore-space-change\".\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  14 ++--\n builtin/history.c              | 150 ++++++++++++++++++++++-------------------\n t/t3451-history-reword.sh      |  12 +++-\n 3 files changed, 98 insertions(+), 78 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 154e262b76..df2900ac2f 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history reword <commit> [--ref-action=(branches|head|print)]\n+git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\n \n DESCRIPTION\n -----------\n@@ -60,13 +60,17 @@ The following commands are available to rewrite history in different ways:\n OPTIONS\n -------\n \n-`--ref-action=(branches|head|print)`::\n+`--dry-run`::\n+\tDo not update any references, but instead print any ref updates in a\n+\tformat that can be consumed by linkgit:git-update-ref[1]. Necessary new\n+\tobjects will be written into the repository, so applying these printed\n+\tref updates is generally safe.\n+\n+`--ref-action=(branches|head)`::\n \tControl which references will be updated by the command, if any. With\n \t`branches`, all local branches that point to commits which are\n \tdescendants of the original commit will be rewritten. With `head`, only\n-\tthe current `HEAD` reference will be rewritten. With `print`, all\n-\tupdates as they would be performed with `branches` are printed in a\n-\tformat that can be consumed by linkgit:git-update-ref[1].\n+\tthe current `HEAD` reference will be rewritten.\n \n GIT\n ---\ndiff --git a/builtin/history.c b/builtin/history.c\nindex ff90e93d6e..c135361c67 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -18,7 +18,7 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--ref-action=(branches|head|print)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -155,7 +155,6 @@ enum ref_action {\n \tREF_ACTION_DEFAULT,\n \tREF_ACTION_BRANCHES,\n \tREF_ACTION_HEAD,\n-\tREF_ACTION_PRINT,\n };\n \n static int parse_ref_action(const struct option *opt, const char *value, int unset)\n@@ -167,10 +166,8 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns\n \t\t*action = REF_ACTION_BRANCHES;\n \t} else if (!strcmp(value, \"head\")) {\n \t\t*action = REF_ACTION_HEAD;\n-\t} else if (!strcmp(value, \"print\")) {\n-\t\t*action = REF_ACTION_PRINT;\n \t} else {\n-\t\treturn error(_(\"%s expects one of 'branches', 'head' or 'print'\"),\n+\t\treturn error(_(\"%s expects one of 'branches' or 'head'\"),\n \t\t\t     opt->long_name);\n \t}\n \n@@ -286,11 +283,29 @@ static int setup_revwalk(struct repository *repo,\n \treturn ret;\n }\n \n+static int handle_ref_update(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+\tif (!transaction) {\n+\t\tprintf(\"update %s %s %s\\n\",\n+\t\t       refname, oid_to_hex(new_oid), oid_to_hex(old_oid));\n+\t\treturn 0;\n+\t}\n+\n+\treturn ref_transaction_update(transaction, refname, new_oid, old_oid,\n+\t\t\t\t      NULL, NULL, 0, reflog_msg, err);\n+}\n+\n static int handle_reference_updates(struct rev_info *revs,\n \t\t\t\t    enum ref_action action,\n \t\t\t\t    struct commit *original,\n \t\t\t\t    struct commit *rewritten,\n-\t\t\t\t    const char *reflog_msg)\n+\t\t\t\t    const char *reflog_msg,\n+\t\t\t\t    int dry_run)\n {\n \tconst struct name_decoration *decoration;\n \tstruct replay_revisions_options opts = { 0 };\n@@ -312,82 +327,72 @@ static int handle_reference_updates(struct rev_info *revs,\n \tif (ret)\n \t\tgoto out;\n \n-\tswitch (action) {\n-\tcase REF_ACTION_BRANCHES:\n-\tcase REF_ACTION_HEAD:\n+\tif (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)\n+\t\tBUG(\"unsupported ref action %d\", action);\n+\n+\tif (!dry_run) {\n \t\ttransaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);\n \t\tif (!transaction) {\n \t\t\tret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n \t\t\tgoto out;\n \t\t}\n+\t}\n \n-\t\tfor (size_t i = 0; i < result.updates_nr; i++) {\n-\t\t\tret = ref_transaction_update(transaction,\n-\t\t\t\t\t\t     result.updates[i].refname,\n-\t\t\t\t\t\t     &result.updates[i].new_oid,\n-\t\t\t\t\t\t     &result.updates[i].old_oid,\n-\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n-\t\t\tif (ret) {\n-\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n-\t\t\t\t\t    result.updates[i].refname, err.buf);\n-\t\t\t\tgoto out;\n-\t\t\t}\n+\tfor (size_t i = 0; i < result.updates_nr; i++) {\n+\t\tret = handle_ref_update(transaction,\n+\t\t\t\t\tresult.updates[i].refname,\n+\t\t\t\t\t&result.updates[i].new_oid,\n+\t\t\t\t\t&result.updates[i].old_oid,\n+\t\t\t\t\treflog_msg, &err);\n+\t\tif (ret) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    result.updates[i].refname, err.buf);\n+\t\t\tgoto out;\n \t\t}\n+\t}\n+\n+\t/*\n+\t * `replay_revisions()` only updates references that are\n+\t * ancestors of `rewritten`, so we need to manually\n+\t * handle updating references that point to `original`.\n+\t */\n+\tfor (decoration = get_name_decoration(&original->object);\n+\t     decoration;\n+\t     decoration = decoration->next)\n+\t{\n+\t\tif (decoration->type != DECORATION_REF_LOCAL &&\n+\t\t    decoration->type != DECORATION_REF_HEAD)\n+\t\t\tcontinue;\n+\n+\t\tif (action == REF_ACTION_HEAD &&\n+\t\t    decoration->type != DECORATION_REF_HEAD)\n+\t\t\tcontinue;\n \n \t\t/*\n-\t\t * `replay_revisions()` only updates references that are\n-\t\t * ancestors of `rewritten`, so we need to manually\n-\t\t * handle updating references that point to `original`.\n+\t\t * We only need to update HEAD separately in case it's\n+\t\t * detached. If it's not we'd already update the branch\n+\t\t * it is pointing to.\n \t\t */\n-\t\tfor (decoration = get_name_decoration(&original->object);\n-\t\t     decoration;\n-\t\t     decoration = decoration->next)\n-\t\t{\n-\t\t\tif (decoration->type != DECORATION_REF_LOCAL &&\n-\t\t\t    decoration->type != DECORATION_REF_HEAD)\n-\t\t\t\tcontinue;\n-\n-\t\t\tif (action == REF_ACTION_HEAD &&\n-\t\t\t    decoration->type != DECORATION_REF_HEAD)\n-\t\t\t\tcontinue;\n-\n-\t\t\t/*\n-\t\t\t * We only need to update HEAD separately in case it's\n-\t\t\t * detached. If it's not we'd already update the branch\n-\t\t\t * it is pointing to.\n-\t\t\t */\n-\t\t\tif (action == REF_ACTION_BRANCHES &&\n-\t\t\t    decoration->type == DECORATION_REF_HEAD &&\n-\t\t\t    !detached_head)\n-\t\t\t\tcontinue;\n-\n-\t\t\tret = ref_transaction_update(transaction,\n-\t\t\t\t\t\t     decoration->name,\n-\t\t\t\t\t\t     &rewritten->object.oid,\n-\t\t\t\t\t\t     &original->object.oid,\n-\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n-\t\t\tif (ret) {\n-\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n-\t\t\t\t\t    decoration->name, err.buf);\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\t\t}\n-\n-\t\tif (ref_transaction_commit(transaction, &err)) {\n-\t\t\tret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n+\t\tif (action == REF_ACTION_BRANCHES &&\n+\t\t    decoration->type == DECORATION_REF_HEAD &&\n+\t\t    !detached_head)\n+\t\t\tcontinue;\n+\n+\t\tret = handle_ref_update(transaction,\n+\t\t\t\t\tdecoration->name,\n+\t\t\t\t\t&rewritten->object.oid,\n+\t\t\t\t\t&original->object.oid,\n+\t\t\t\t\treflog_msg, &err);\n+\t\tif (ret) {\n+\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t    decoration->name, err.buf);\n \t\t\tgoto out;\n \t\t}\n+\t}\n \n-\t\tbreak;\n-\tcase REF_ACTION_PRINT:\n-\t\tfor (size_t i = 0; i < result.updates_nr; i++)\n-\t\t\tprintf(\"update %s %s %s\\n\",\n-\t\t\t       result.updates[i].refname,\n-\t\t\t       oid_to_hex(&result.updates[i].new_oid),\n-\t\t\t       oid_to_hex(&result.updates[i].old_oid));\n-\t\tbreak;\n-\tdefault:\n-\t\tBUG(\"unsupported ref action %d\", action);\n+\tif (transaction && ref_transaction_commit(transaction, &err)) {\n+\t\tret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n+\t\tgoto out;\n \t}\n \n \tret = 0;\n@@ -409,10 +414,13 @@ static int cmd_history_reword(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"ref-action\", &action, N_(\"<action>\"),\n-\t\t\t       N_(\"control ref update behavior (branches|head|print)\"),\n+\t\t\t       N_(\"control ref update behavior (branches|head)\"),\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n+\t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n+\t\t\t N_(\"perform a dry-run without updating any refs\")),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -449,7 +457,7 @@ static int cmd_history_reword(int argc,\n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf);\n+\t\t\t\t       reflog_msg.buf, dry_run);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 12a9a7d051..702d40dc06 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -221,7 +221,7 @@ test_expect_success 'can reword a merge commit' '\n \t)\n '\n \n-test_expect_success '--ref-action=print prints ref updates without modifying repo' '\n+test_expect_success '--dry-run prints ref updates without modifying repo' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\n \t(\n@@ -233,7 +233,15 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep\n \t\ttest_commit theirs &&\n \n \t\tgit refs list >refs-expect &&\n-\t\treword_with_message --ref-action=print base >updates <<-\\EOF &&\n+\t\treword_with_message --dry-run --ref-action=head base >updates <<-\\EOF &&\n+\t\treworded commit\n+\t\tEOF\n+\t\tgit refs list >refs-actual &&\n+\t\ttest_cmp refs-expect refs-actual &&\n+\t\ttest_grep \"update refs/heads/branch\" updates &&\n+\t\ttest_grep ! \"update refs/heads/main\" updates &&\n+\n+\t\treword_with_message --dry-run base >updates <<-\\EOF &&\n \t\treworded commit\n \t\tEOF\n \t\tgit refs list >refs-actual &&\n\n-- \n2.53.0.352.gd1286b26eb.dirty\n\n"},{"id":"536086","messageId":"20260216-b4-pks-history-dry-run-v3-4-c4db58a651fc@pks.im","threadId":"64985","inReplyTo":"20260216-b4-pks-history-dry-run-v3-0-c4db58a651fc@pks.im","subject":"[PATCH v3 4/5] builtin/history: rename \"--ref-action=\" to \"--update-refs=\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-16T06:45:47Z","receivedAt":"2026-02-16T06:46:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With the preceding commit we have changed \"--ref-action=\" to only\ncontrol which refs are supposed to be updated, not what happens with\nthem. As a consequence, the option is now somewhat misnamed, as we don't\ncontrol the action itself anymore.\n\nRename it to \"--update-refs=\" to better align it with its new use.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc | 4 ++--\n builtin/history.c              | 8 ++++----\n t/t3451-history-reword.sh      | 8 ++++----\n 3 files changed, 10 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex df2900ac2f..4dbe665ec4 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\n+git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n \n DESCRIPTION\n -----------\n@@ -66,7 +66,7 @@ OPTIONS\n \tobjects will be written into the repository, so applying these printed\n \tref updates is generally safe.\n \n-`--ref-action=(branches|head)`::\n+`--update-refs=(branches|head)`::\n \tControl which references will be updated by the command, if any. With\n \t`branches`, all local branches that point to commits which are\n \tdescendants of the original commit will be rewritten. With `head`, only\ndiff --git a/builtin/history.c b/builtin/history.c\nindex c135361c67..1cf6c668cf 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -18,7 +18,7 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--dry-run] [--ref-action=(branches|head)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -258,7 +258,7 @@ static int setup_revwalk(struct repository *repo,\n \t\t\tgoto out;\n \t\t} else if (!ret) {\n \t\t\tret = error(_(\"rewritten commit must be an ancestor \"\n-\t\t\t\t      \"of HEAD when using --ref-action=head\"));\n+\t\t\t\t      \"of HEAD when using --update-refs=head\"));\n \t\t\tgoto out;\n \t\t}\n \n@@ -416,8 +416,8 @@ static int cmd_history_reword(int argc,\n \tenum ref_action action = REF_ACTION_DEFAULT;\n \tint dry_run = 0;\n \tstruct option options[] = {\n-\t\tOPT_CALLBACK_F(0, \"ref-action\", &action, N_(\"<action>\"),\n-\t\t\t       N_(\"control ref update behavior (branches|head)\"),\n+\t\tOPT_CALLBACK_F(0, \"update-refs\", &action, N_(\"<action>\"),\n+\t\t\t       N_(\"control which refs should be updated (branches|head)\"),\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 702d40dc06..de7b357685 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -233,7 +233,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '\n \t\ttest_commit theirs &&\n \n \t\tgit refs list >refs-expect &&\n-\t\treword_with_message --dry-run --ref-action=head base >updates <<-\\EOF &&\n+\t\treword_with_message --dry-run --update-refs=head base >updates <<-\\EOF &&\n \t\treworded commit\n \t\tEOF\n \t\tgit refs list >refs-actual &&\n@@ -258,7 +258,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '\n \t)\n '\n \n-test_expect_success '--ref-action=head updates only HEAD' '\n+test_expect_success '--update-refs=head updates only HEAD' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\n \t(\n@@ -271,10 +271,10 @@ test_expect_success '--ref-action=head updates only HEAD' '\n \n \t\t# When told to update HEAD, only, the command will refuse to\n \t\t# rewrite commits that are not an ancestor of HEAD.\n-\t\ttest_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&\n+\t\ttest_must_fail git -c core.editor=false history reword --update-refs=head theirs 2>err &&\n \t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n \n-\t\treword_with_message --ref-action=head base >updates <<-\\EOF &&\n+\t\treword_with_message --update-refs=head base >updates <<-\\EOF &&\n \t\treworded base\n \t\tEOF\n \t\texpect_log HEAD <<-\\EOF &&\n\n-- \n2.53.0.352.gd1286b26eb.dirty\n\n"},{"id":"536087","messageId":"20260216-b4-pks-history-dry-run-v3-5-c4db58a651fc@pks.im","threadId":"64985","inReplyTo":"20260216-b4-pks-history-dry-run-v3-0-c4db58a651fc@pks.im","subject":"[PATCH v3 5/5] Documentation/git-history: document default for \"--update-refs=\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-16T06:45:48Z","receivedAt":"2026-02-16T06:46:07Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we document the values that can be passed to the \"--update-refs=\"\noption, we don't give the user any hint what the default behaviour is.\nDocument it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 4dbe665ec4..cc019de697 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -70,7 +70,7 @@ OPTIONS\n \tControl which references will be updated by the command, if any. With\n \t`branches`, all local branches that point to commits which are\n \tdescendants of the original commit will be rewritten. With `head`, only\n-\tthe current `HEAD` reference will be rewritten.\n+\tthe current `HEAD` reference will be rewritten. Defaults to `branches`.\n \n GIT\n ---\n\n-- \n2.53.0.352.gd1286b26eb.dirty\n\n"},{"id":"536302","messageId":"5cab5e92-268b-475d-9f29-13127c0641bb@app.fastmail.com","threadId":"64985","inReplyTo":"aZK7jmvLwu1evxUp@pks.im","subject":"Re: [PATCH v2 3/5] builtin/history: replace \"--ref-action=print\" with \"--dry-run\"","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-18T16:09:50Z","receivedAt":"2026-02-18T16:10:12Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Feb 16, 2026, at 07:39, Patrick Steinhardt wrote:\n>[snip]\n>> `HEAD` is mentioned here because it could be detached `HEAD`. So you\n>> can’t just say the current branch.\n>>\n>> “the current `HEAD` reference” seems a bit much. Is this less precise?\n>\n> It's not, but this commit doesn't rewrite any of the description, it\n> only deletes the sentence that becomes out-of-date now.\n\nI thought I would unceremoniously bring it up since you are the\nonly/sole author of this document. ;)\n\n> So I'd prefer to keep this as-is if you don't mind.\n\nYeah that’s okay. :)\n\n>\n> If you feel strongly I'm happy to add another commit on top.\n"}]}