* [PATCH 0/4] builtin/history: some smaller UI improvements
@ 2026-02-12 12:44 Patrick Steinhardt
2026-02-12 12:44 ` [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
` (5 more replies)
0 siblings, 6 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-12 12:44 UTC (permalink / raw)
To: git
Hi,
this patch series contains a small set of UI improvements for
git-history(1):
- The first two commits adapt git-history(1) so that it performs
verifications before asking the user for input.
- The last two commits rework the "--ref-action=" option to instead be
split up into "--dry-run" and "--update-refs=" so that the option is
less focussed on technical implementation details.
I decided to send this patch series as a small incremental step before
sending `git history split`, also because that series conflicts with
aa/add-p-no-auto-advance.
Thanks!
Patrick
---
Patrick Steinhardt (4):
builtin/history: perform revwalk checks before asking for user input
builtin/history: check for merges before asking for user input
builtin/history: replace "--ref-action=print" with "--dry-run"
builtin/history: rename "--ref-action=" to "--update-refs="
Documentation/git-history.adoc | 12 +-
builtin/history.c | 255 ++++++++++++++++++++++++++---------------
t/t3451-history-reword.sh | 20 +++-
3 files changed, 181 insertions(+), 106 deletions(-)
---
base-commit: 6fcee4785280a08e7f271bd015a4dc33753e2886
change-id: 20260212-b4-pks-history-dry-run-2b840e530ae6
^ permalink raw reply [flat|nested] 34+ messages in thread
* [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input
2026-02-12 12:44 [PATCH 0/4] builtin/history: some smaller UI improvements Patrick Steinhardt
@ 2026-02-12 12:44 ` Patrick Steinhardt
2026-02-12 20:04 ` Junio C Hamano
2026-02-12 12:44 ` [PATCH 2/4] builtin/history: check for merges " Patrick Steinhardt
` (4 subsequent siblings)
5 siblings, 1 reply; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-12 12:44 UTC (permalink / raw)
To: git
When setting up the revision walk in git-history(1) we also perform some
verifications whether the request actually looks sane. Unfortunately,
these verifications come _after_ we have already asked the user for the
commit message of the commit that is to be rewritten. So in case any of
the verifications fails, the user will have lost their modifications.
Extract the function to set up the revision walk and call it before we
ask for user input to fix this.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/history.c | 69 +++++++++++++++++++++++++++++------------------
t/t3451-history-reword.sh | 2 +-
2 files changed, 44 insertions(+), 27 deletions(-)
diff --git a/builtin/history.c b/builtin/history.c
index 8dcb9a6046..1de51372ea 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -177,30 +177,15 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
return 0;
}
-static int handle_reference_updates(enum ref_action action,
- struct repository *repo,
- struct commit *original,
- struct commit *rewritten,
- const char *reflog_msg)
+static int setup_revwalk(struct repository *repo,
+ enum ref_action action,
+ struct commit *original,
+ struct rev_info *revs)
{
- const struct name_decoration *decoration;
- struct replay_revisions_options opts = { 0 };
- struct replay_result result = { 0 };
- struct ref_transaction *transaction = NULL;
struct strvec args = STRVEC_INIT;
- struct strbuf err = STRBUF_INIT;
- struct commit *head = NULL;
- struct rev_info revs;
- char hex[GIT_MAX_HEXSZ + 1];
- bool detached_head;
- int head_flags = 0;
int ret;
- refs_read_ref_full(get_main_ref_store(repo), "HEAD",
- RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
- detached_head = !(head_flags & REF_ISSYMREF);
-
- repo_init_revisions(repo, &revs, NULL);
+ repo_init_revisions(repo, revs, NULL);
strvec_push(&args, "ignored");
strvec_push(&args, "--reverse");
strvec_push(&args, "--topo-order");
@@ -224,6 +209,7 @@ static int handle_reference_updates(enum ref_action action,
*/
if (action == REF_ACTION_HEAD) {
struct commit_list *from_list = NULL;
+ struct commit *head;
head = lookup_commit_reference_by_name("HEAD");
if (!head) {
@@ -250,20 +236,47 @@ static int handle_reference_updates(enum ref_action action,
strvec_push(&args, "HEAD");
}
- setup_revisions_from_strvec(&args, &revs, NULL);
+ setup_revisions_from_strvec(&args, revs, NULL);
if (args.nr != 1)
BUG("revisions were set up with invalid argument");
+ ret = 0;
+
+out:
+ strvec_clear(&args);
+ return ret;
+}
+
+static int handle_reference_updates(struct rev_info *revs,
+ enum ref_action action,
+ struct commit *original,
+ struct commit *rewritten,
+ const char *reflog_msg)
+{
+ const struct name_decoration *decoration;
+ struct replay_revisions_options opts = { 0 };
+ struct replay_result result = { 0 };
+ struct ref_transaction *transaction = NULL;
+ struct strbuf err = STRBUF_INIT;
+ char hex[GIT_MAX_HEXSZ + 1];
+ bool detached_head;
+ int head_flags = 0;
+ int ret;
+
+ refs_read_ref_full(get_main_ref_store(revs->repo), "HEAD",
+ RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
+ detached_head = !(head_flags & REF_ISSYMREF);
+
opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
- ret = replay_revisions(&revs, &opts, &result);
+ ret = replay_revisions(revs, &opts, &result);
if (ret)
goto out;
switch (action) {
case REF_ACTION_BRANCHES:
case REF_ACTION_HEAD:
- transaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);
+ transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
if (!transaction) {
ret = error(_("failed to begin ref transaction: %s"), err.buf);
goto out;
@@ -343,9 +356,7 @@ static int handle_reference_updates(enum ref_action action,
out:
ref_transaction_free(transaction);
replay_result_release(&result);
- release_revisions(&revs);
strbuf_release(&err);
- strvec_clear(&args);
return ret;
}
@@ -367,6 +378,7 @@ static int cmd_history_reword(int argc,
};
struct strbuf reflog_msg = STRBUF_INIT;
struct commit *original, *rewritten;
+ struct rev_info revs;
int ret;
argc = parse_options(argc, argv, prefix, options, usage, 0);
@@ -385,6 +397,10 @@ static int cmd_history_reword(int argc,
goto out;
}
+ ret = setup_revwalk(repo, action, original, &revs);
+ if (ret)
+ goto out;
+
ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
if (ret < 0) {
ret = error(_("failed writing reworded commit"));
@@ -393,7 +409,7 @@ static int cmd_history_reword(int argc,
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
- ret = handle_reference_updates(action, repo, original, rewritten,
+ ret = handle_reference_updates(&revs, action, original, rewritten,
reflog_msg.buf);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
@@ -404,6 +420,7 @@ static int cmd_history_reword(int argc,
out:
strbuf_release(&reflog_msg);
+ release_revisions(&revs);
return ret;
}
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 3594421b68..6775ed62f9 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '
# When told to update HEAD, only, the command will refuse to
# rewrite commits that are not an ancestor of HEAD.
- test_must_fail git history reword --ref-action=head theirs 2>err &&
+ test_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&
test_grep "rewritten commit must be an ancestor of HEAD" err &&
reword_with_message --ref-action=head base >updates <<-\EOF &&
--
2.53.0.295.g64333814d3.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH 2/4] builtin/history: check for merges before asking for user input
2026-02-12 12:44 [PATCH 0/4] builtin/history: some smaller UI improvements Patrick Steinhardt
2026-02-12 12:44 ` [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
@ 2026-02-12 12:44 ` Patrick Steinhardt
2026-02-12 22:20 ` D. Ben Knoble
2026-02-12 12:44 ` [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
` (3 subsequent siblings)
5 siblings, 1 reply; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-12 12:44 UTC (permalink / raw)
To: git
The replay infrastructure is not yet capable of replaying merge commits.
Unfortunately, we only notice that we're about to replay merges after we
have already asked the user for input, so any commit message that the
user may have written will be discarded in that case.
Fix this by checking whether the revwalk contains merge commits before
we ask for user input.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/history.c | 40 ++++++++++++++++++++++++++++++++++++++++
t/t3451-history-reword.sh | 2 +-
2 files changed, 41 insertions(+), 1 deletion(-)
diff --git a/builtin/history.c b/builtin/history.c
index 1de51372ea..ca0cdb6a58 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -177,6 +177,42 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
return 0;
}
+static int revwalk_contains_merges(struct repository *repo,
+ const struct strvec *revwalk_args)
+{
+ struct strvec args = STRVEC_INIT;
+ struct rev_info revs;
+ int ret;
+
+ for (size_t i = 0; i < revwalk_args->nr; i++)
+ strvec_push(&args, revwalk_args->v[i]);
+ strvec_push(&args, "--min-parents=2");
+
+ repo_init_revisions(repo, &revs, NULL);
+
+ setup_revisions_from_strvec(&args, &revs, NULL);
+ if (args.nr != 1)
+ BUG("revisions were set up with invalid argument");
+
+ if (prepare_revision_walk(&revs) < 0) {
+ ret = error(_("error preparing revisions"));
+ goto out;
+ }
+
+ if (get_revision(&revs)) {
+ ret = error(_("replaying merge commits is not supported yet!"));
+ goto out;
+ }
+
+ reset_revision_walk();
+ ret = 0;
+
+out:
+ release_revisions(&revs);
+ strvec_clear(&args);
+ return ret;
+}
+
static int setup_revwalk(struct repository *repo,
enum ref_action action,
struct commit *original,
@@ -236,6 +272,10 @@ static int setup_revwalk(struct repository *repo,
strvec_push(&args, "HEAD");
}
+ ret = revwalk_contains_merges(repo, &args);
+ if (ret < 0)
+ goto out;
+
setup_revisions_from_strvec(&args, revs, NULL);
if (args.nr != 1)
BUG("revisions were set up with invalid argument");
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 6775ed62f9..12a9a7d051 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '
# It is not possible to replay merge commits embedded in the
# history (yet).
- test_must_fail git history reword HEAD~ 2>err &&
+ test_must_fail git -c core.editor=false history reword HEAD~ 2>err &&
test_grep "replaying merge commits is not supported yet" err &&
# But it is possible to reword a merge commit directly.
--
2.53.0.295.g64333814d3.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run"
2026-02-12 12:44 [PATCH 0/4] builtin/history: some smaller UI improvements Patrick Steinhardt
2026-02-12 12:44 ` [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
2026-02-12 12:44 ` [PATCH 2/4] builtin/history: check for merges " Patrick Steinhardt
@ 2026-02-12 12:44 ` Patrick Steinhardt
2026-02-12 20:19 ` Junio C Hamano
2026-02-12 22:20 ` D. Ben Knoble
2026-02-12 12:44 ` [PATCH 4/4] builtin/history: rename "--ref-action=" to "--update-refs=" Patrick Steinhardt
` (2 subsequent siblings)
5 siblings, 2 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-12 12:44 UTC (permalink / raw)
To: git
The git-history(1) command has the ability to perform a dry-run
that will not end up modifying any references. Instead, we'll only print
any ref updates that would happen as a consequence of performing the
operation.
This mode is somewhat hidden though behind the "--ref-action=print"
option. This command line option has its origin in git-replay(1), where
it's probably an okayish interface as this command is sitting more on
the plumbing side of tools. But git-history(1) is a user-facing tool,
and this way of achieving a dry-run is way too technical and thus not
very discoverable.
Besides usability issues, it also has another issue: the dry-run mode
will always operate as if the user wanted to rewrite all branches. But
in fact, the user also has the option to only update the HEAD reference,
and they might want to perform a dry-run of such an operation, too. We
could of course introduce "--ref-actoin=print-head", but that would
become even less ergonomic.
Replace "--ref-action=print" with a new "--dry-run" toggle. This new
toggle works with both "--ref-action={head,branches}" and is way more
discoverable.
Add a test to verify that both "--ref-action=" values behave as
expected.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 12 ++--
builtin/history.c | 150 ++++++++++++++++++++++-------------------
t/t3451-history-reword.sh | 12 +++-
3 files changed, 96 insertions(+), 78 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 154e262b76..33353815cb 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
SYNOPSIS
--------
[synopsis]
-git history reword <commit> [--ref-action=(branches|head|print)]
+git history reword <commit> [--dry-run] [--ref-action=(branches|head)]
DESCRIPTION
-----------
@@ -60,13 +60,15 @@ The following commands are available to rewrite history in different ways:
OPTIONS
-------
-`--ref-action=(branches|head|print)`::
+`--dry-run`::
+ Do not update any references, but instead print any ref updates in a
+ format that can be consumed by linkgit:git-update-ref[1].
+
+`--ref-action=(branches|head)`::
Control which references will be updated by the command, if any. With
`branches`, all local branches that point to commits which are
descendants of the original commit will be rewritten. With `head`, only
- the current `HEAD` reference will be rewritten. With `print`, all
- updates as they would be performed with `branches` are printed in a
- format that can be consumed by linkgit:git-update-ref[1].
+ the current `HEAD` reference will be rewritten.
GIT
---
diff --git a/builtin/history.c b/builtin/history.c
index ca0cdb6a58..40e2925cea 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -18,7 +18,7 @@
#include "wt-status.h"
#define GIT_HISTORY_REWORD_USAGE \
- N_("git history reword <commit> [--ref-action=(branches|head|print)]")
+ N_("git history reword <commit> [--dry-run] [--ref-action=(branches|head)]")
static void change_data_free(void *util, const char *str UNUSED)
{
@@ -155,7 +155,6 @@ enum ref_action {
REF_ACTION_DEFAULT,
REF_ACTION_BRANCHES,
REF_ACTION_HEAD,
- REF_ACTION_PRINT,
};
static int parse_ref_action(const struct option *opt, const char *value, int unset)
@@ -167,10 +166,8 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
*action = REF_ACTION_BRANCHES;
} else if (!strcmp(value, "head")) {
*action = REF_ACTION_HEAD;
- } else if (!strcmp(value, "print")) {
- *action = REF_ACTION_PRINT;
} else {
- return error(_("%s expects one of 'branches', 'head' or 'print'"),
+ return error(_("%s expects one of 'branches' or 'head'"),
opt->long_name);
}
@@ -287,11 +284,29 @@ static int setup_revwalk(struct repository *repo,
return ret;
}
+static int handle_ref_update(struct ref_transaction *transaction,
+ const char *refname,
+ const struct object_id *new_oid,
+ const struct object_id *old_oid,
+ const char *reflog_msg,
+ struct strbuf *err)
+{
+ if (!transaction) {
+ printf("update %s %s %s\n",
+ refname, oid_to_hex(new_oid), oid_to_hex(old_oid));
+ return 0;
+ }
+
+ return ref_transaction_update(transaction, refname, new_oid, old_oid,
+ NULL, NULL, 0, reflog_msg, err);
+}
+
static int handle_reference_updates(struct rev_info *revs,
enum ref_action action,
struct commit *original,
struct commit *rewritten,
- const char *reflog_msg)
+ const char *reflog_msg,
+ int dry_run)
{
const struct name_decoration *decoration;
struct replay_revisions_options opts = { 0 };
@@ -313,82 +328,72 @@ static int handle_reference_updates(struct rev_info *revs,
if (ret)
goto out;
- switch (action) {
- case REF_ACTION_BRANCHES:
- case REF_ACTION_HEAD:
+ if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
+ BUG("unsupported ref action %d", action);
+
+ if (!dry_run) {
transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
if (!transaction) {
ret = error(_("failed to begin ref transaction: %s"), err.buf);
goto out;
}
+ }
- for (size_t i = 0; i < result.updates_nr; i++) {
- ret = ref_transaction_update(transaction,
- result.updates[i].refname,
- &result.updates[i].new_oid,
- &result.updates[i].old_oid,
- NULL, NULL, 0, reflog_msg, &err);
- if (ret) {
- ret = error(_("failed to update ref '%s': %s"),
- result.updates[i].refname, err.buf);
- goto out;
- }
+ for (size_t i = 0; i < result.updates_nr; i++) {
+ ret = handle_ref_update(transaction,
+ result.updates[i].refname,
+ &result.updates[i].new_oid,
+ &result.updates[i].old_oid,
+ reflog_msg, &err);
+ if (ret) {
+ ret = error(_("failed to update ref '%s': %s"),
+ result.updates[i].refname, err.buf);
+ goto out;
}
+ }
+
+ /*
+ * `replay_revisions()` only updates references that are
+ * ancestors of `rewritten`, so we need to manually
+ * handle updating references that point to `original`.
+ */
+ for (decoration = get_name_decoration(&original->object);
+ decoration;
+ decoration = decoration->next)
+ {
+ if (decoration->type != DECORATION_REF_LOCAL &&
+ decoration->type != DECORATION_REF_HEAD)
+ continue;
+
+ if (action == REF_ACTION_HEAD &&
+ decoration->type != DECORATION_REF_HEAD)
+ continue;
/*
- * `replay_revisions()` only updates references that are
- * ancestors of `rewritten`, so we need to manually
- * handle updating references that point to `original`.
+ * We only need to update HEAD separately in case it's
+ * detached. If it's not we'd already update the branch
+ * it is pointing to.
*/
- for (decoration = get_name_decoration(&original->object);
- decoration;
- decoration = decoration->next)
- {
- if (decoration->type != DECORATION_REF_LOCAL &&
- decoration->type != DECORATION_REF_HEAD)
- continue;
-
- if (action == REF_ACTION_HEAD &&
- decoration->type != DECORATION_REF_HEAD)
- continue;
-
- /*
- * We only need to update HEAD separately in case it's
- * detached. If it's not we'd already update the branch
- * it is pointing to.
- */
- if (action == REF_ACTION_BRANCHES &&
- decoration->type == DECORATION_REF_HEAD &&
- !detached_head)
- continue;
-
- ret = ref_transaction_update(transaction,
- decoration->name,
- &rewritten->object.oid,
- &original->object.oid,
- NULL, NULL, 0, reflog_msg, &err);
- if (ret) {
- ret = error(_("failed to update ref '%s': %s"),
- decoration->name, err.buf);
- goto out;
- }
- }
-
- if (ref_transaction_commit(transaction, &err)) {
- ret = error(_("failed to commit ref transaction: %s"), err.buf);
+ if (action == REF_ACTION_BRANCHES &&
+ decoration->type == DECORATION_REF_HEAD &&
+ !detached_head)
+ continue;
+
+ ret = handle_ref_update(transaction,
+ decoration->name,
+ &rewritten->object.oid,
+ &original->object.oid,
+ reflog_msg, &err);
+ if (ret) {
+ ret = error(_("failed to update ref '%s': %s"),
+ decoration->name, err.buf);
goto out;
}
+ }
- break;
- case REF_ACTION_PRINT:
- for (size_t i = 0; i < result.updates_nr; i++)
- printf("update %s %s %s\n",
- result.updates[i].refname,
- oid_to_hex(&result.updates[i].new_oid),
- oid_to_hex(&result.updates[i].old_oid));
- break;
- default:
- BUG("unsupported ref action %d", action);
+ if (transaction && ref_transaction_commit(transaction, &err)) {
+ ret = error(_("failed to commit ref transaction: %s"), err.buf);
+ goto out;
}
ret = 0;
@@ -410,10 +415,13 @@ static int cmd_history_reword(int argc,
NULL,
};
enum ref_action action = REF_ACTION_DEFAULT;
+ int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
- N_("control ref update behavior (branches|head|print)"),
+ N_("control ref update behavior (branches|head)"),
PARSE_OPT_NONEG, parse_ref_action),
+ OPT_BOOL('n', "dry-run", &dry_run,
+ N_("perform a dry-run without updating any refs")),
OPT_END(),
};
struct strbuf reflog_msg = STRBUF_INIT;
@@ -450,7 +458,7 @@ static int cmd_history_reword(int argc,
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf);
+ reflog_msg.buf, dry_run);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 12a9a7d051..702d40dc06 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -221,7 +221,7 @@ test_expect_success 'can reword a merge commit' '
)
'
-test_expect_success '--ref-action=print prints ref updates without modifying repo' '
+test_expect_success '--dry-run prints ref updates without modifying repo' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
@@ -233,7 +233,15 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep
test_commit theirs &&
git refs list >refs-expect &&
- reword_with_message --ref-action=print base >updates <<-\EOF &&
+ reword_with_message --dry-run --ref-action=head base >updates <<-\EOF &&
+ reworded commit
+ EOF
+ git refs list >refs-actual &&
+ test_cmp refs-expect refs-actual &&
+ test_grep "update refs/heads/branch" updates &&
+ test_grep ! "update refs/heads/main" updates &&
+
+ reword_with_message --dry-run base >updates <<-\EOF &&
reworded commit
EOF
git refs list >refs-actual &&
--
2.53.0.295.g64333814d3.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH 4/4] builtin/history: rename "--ref-action=" to "--update-refs="
2026-02-12 12:44 [PATCH 0/4] builtin/history: some smaller UI improvements Patrick Steinhardt
` (2 preceding siblings ...)
2026-02-12 12:44 ` [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
@ 2026-02-12 12:44 ` Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
5 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-12 12:44 UTC (permalink / raw)
To: git
With the preceding commit we have changed "--ref-action=" to only
control which refs are supposed to be updated, not what happens with
them. As a consequence, the option is now somewhat misnamed, as we don't
control the action itself anymore.
Rename it to "--update-refs=" to better align it with its new use.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 4 ++--
builtin/history.c | 8 ++++----
t/t3451-history-reword.sh | 8 ++++----
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 33353815cb..b73fb009ab 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
SYNOPSIS
--------
[synopsis]
-git history reword <commit> [--dry-run] [--ref-action=(branches|head)]
+git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
DESCRIPTION
-----------
@@ -64,7 +64,7 @@ OPTIONS
Do not update any references, but instead print any ref updates in a
format that can be consumed by linkgit:git-update-ref[1].
-`--ref-action=(branches|head)`::
+`--update-refs=(branches|head)`::
Control which references will be updated by the command, if any. With
`branches`, all local branches that point to commits which are
descendants of the original commit will be rewritten. With `head`, only
diff --git a/builtin/history.c b/builtin/history.c
index 40e2925cea..30ab10442c 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -18,7 +18,7 @@
#include "wt-status.h"
#define GIT_HISTORY_REWORD_USAGE \
- N_("git history reword <commit> [--dry-run] [--ref-action=(branches|head)]")
+ N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
static void change_data_free(void *util, const char *str UNUSED)
{
@@ -259,7 +259,7 @@ static int setup_revwalk(struct repository *repo,
goto out;
} else if (!ret) {
ret = error(_("rewritten commit must be an ancestor "
- "of HEAD when using --ref-action=head"));
+ "of HEAD when using --update-refs=head"));
goto out;
}
@@ -417,8 +417,8 @@ static int cmd_history_reword(int argc,
enum ref_action action = REF_ACTION_DEFAULT;
int dry_run = 0;
struct option options[] = {
- OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
- N_("control ref update behavior (branches|head)"),
+ OPT_CALLBACK_F(0, "update-refs", &action, N_("<action>"),
+ N_("control which refs should be updated (branches|head)"),
PARSE_OPT_NONEG, parse_ref_action),
OPT_BOOL('n', "dry-run", &dry_run,
N_("perform a dry-run without updating any refs")),
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 702d40dc06..de7b357685 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -233,7 +233,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '
test_commit theirs &&
git refs list >refs-expect &&
- reword_with_message --dry-run --ref-action=head base >updates <<-\EOF &&
+ reword_with_message --dry-run --update-refs=head base >updates <<-\EOF &&
reworded commit
EOF
git refs list >refs-actual &&
@@ -258,7 +258,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '
)
'
-test_expect_success '--ref-action=head updates only HEAD' '
+test_expect_success '--update-refs=head updates only HEAD' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
@@ -271,10 +271,10 @@ test_expect_success '--ref-action=head updates only HEAD' '
# When told to update HEAD, only, the command will refuse to
# rewrite commits that are not an ancestor of HEAD.
- test_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&
+ test_must_fail git -c core.editor=false history reword --update-refs=head theirs 2>err &&
test_grep "rewritten commit must be an ancestor of HEAD" err &&
- reword_with_message --ref-action=head base >updates <<-\EOF &&
+ reword_with_message --update-refs=head base >updates <<-\EOF &&
reworded base
EOF
expect_log HEAD <<-\EOF &&
--
2.53.0.295.g64333814d3.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* Re: [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input
2026-02-12 12:44 ` [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
@ 2026-02-12 20:04 ` Junio C Hamano
2026-02-13 5:51 ` Patrick Steinhardt
0 siblings, 1 reply; 34+ messages in thread
From: Junio C Hamano @ 2026-02-12 20:04 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> When setting up the revision walk in git-history(1) we also perform some
> verifications whether the request actually looks sane. Unfortunately,
> these verifications come _after_ we have already asked the user for the
> commit message of the commit that is to be rewritten. So in case any of
> the verifications fails, the user will have lost their modifications.
>
> Extract the function to set up the revision walk and call it before we
> ask for user input to fix this.
That's a huge usability improvement. Nice.
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> builtin/history.c | 69 +++++++++++++++++++++++++++++------------------
> t/t3451-history-reword.sh | 2 +-
> 2 files changed, 44 insertions(+), 27 deletions(-)
> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
> index 3594421b68..6775ed62f9 100755
> --- a/t/t3451-history-reword.sh
> +++ b/t/t3451-history-reword.sh
> @@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '
>
> # When told to update HEAD, only, the command will refuse to
> # rewrite commits that are not an ancestor of HEAD.
> - test_must_fail git history reword --ref-action=head theirs 2>err &&
> + test_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&
> test_grep "rewritten commit must be an ancestor of HEAD" err &&
This ensures that the editor is never consulted? How? Running the
"false" editor would give us a different error, like "your editor
exited with non-zero status, telling us to abort" or something?
Thanks.
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run"
2026-02-12 12:44 ` [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
@ 2026-02-12 20:19 ` Junio C Hamano
2026-02-13 5:50 ` Patrick Steinhardt
2026-02-12 22:20 ` D. Ben Knoble
1 sibling, 1 reply; 34+ messages in thread
From: Junio C Hamano @ 2026-02-12 20:19 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> The git-history(1) command has the ability to perform a dry-run
> that will not end up modifying any references. Instead, we'll only print
> any ref updates that would happen as a consequence of performing the
> operation.
>
> This mode is somewhat hidden though behind the "--ref-action=print"
> option. This command line option has its origin in git-replay(1), where
> it's probably an okayish interface as this command is sitting more on
> the plumbing side of tools. But git-history(1) is a user-facing tool,
> and this way of achieving a dry-run is way too technical and thus not
> very discoverable.
>
> Besides usability issues, it also has another issue: the dry-run mode
> will always operate as if the user wanted to rewrite all branches. But
> in fact, the user also has the option to only update the HEAD reference,
> and they might want to perform a dry-run of such an operation, too. We
> could of course introduce "--ref-actoin=print-head", but that would
> become even less ergonomic.
>
> Replace "--ref-action=print" with a new "--dry-run" toggle. This new
> toggle works with both "--ref-action={head,branches}" and is way more
> discoverable.
>
> Add a test to verify that both "--ref-action=" values behave as
> expected.
This "--dry-run" mode still creates necessary new objects, right?
Describing this "--dry-run" as not updating any refs is a very good
thing, which is what the documentation update says, but at the same
time, we should clearly promise that the necessary new objects are
still created, so should the user then choose to update refs to
point at the reported objects, it will not result in a repository
corruption.
Other than that, looking good.
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH 2/4] builtin/history: check for merges before asking for user input
2026-02-12 12:44 ` [PATCH 2/4] builtin/history: check for merges " Patrick Steinhardt
@ 2026-02-12 22:20 ` D. Ben Knoble
2026-02-12 22:26 ` Junio C Hamano
2026-02-13 5:51 ` Patrick Steinhardt
0 siblings, 2 replies; 34+ messages in thread
From: D. Ben Knoble @ 2026-02-12 22:20 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> The replay infrastructure is not yet capable of replaying merge commits.
> Unfortunately, we only notice that we're about to replay merges after we
> have already asked the user for input, so any commit message that the
> user may have written will be discarded in that case.
>
> Fix this by checking whether the revwalk contains merge commits before
> we ask for user input.
Indeed, that would be irritating :)
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> builtin/history.c | 40 ++++++++++++++++++++++++++++++++++++++++
> t/t3451-history-reword.sh | 2 +-
> 2 files changed, 41 insertions(+), 1 deletion(-)
>
> diff --git a/builtin/history.c b/builtin/history.c
> index 1de51372ea..ca0cdb6a58 100644
> --- a/builtin/history.c
> +++ b/builtin/history.c
> @@ -177,6 +177,42 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
> return 0;
> }
>
> +static int revwalk_contains_merges(struct repository *repo,
> + const struct strvec *revwalk_args)
> +{
> + struct strvec args = STRVEC_INIT;
> + struct rev_info revs;
> + int ret;
> +
> + for (size_t i = 0; i < revwalk_args->nr; i++)
> + strvec_push(&args, revwalk_args->v[i]);
I'm surprised we don't have anything like Rust's Vec::append or
Vec::extend_from_slice
> + strvec_push(&args, "--min-parents=2");
And this is the key for detecting merges…
> +
> + repo_init_revisions(repo, &revs, NULL);
> +
> + setup_revisions_from_strvec(&args, &revs, NULL);
> + if (args.nr != 1)
> + BUG("revisions were set up with invalid argument");
> +
> + if (prepare_revision_walk(&revs) < 0) {
> + ret = error(_("error preparing revisions"));
> + goto out;
> + }
> +
> + if (get_revision(&revs)) {
> + ret = error(_("replaying merge commits is not supported yet!"));
> + goto out;
> + }
…which nearly confused me here, since I didn't see how get_revision
was supposed to tell if there was a merge! But it makes sense all
together.
> +
> + reset_revision_walk();
> + ret = 0;
> +
> +out:
> + release_revisions(&revs);
> + strvec_clear(&args);
> + return ret;
> +}
> +
> static int setup_revwalk(struct repository *repo,
> enum ref_action action,
> struct commit *original,
> @@ -236,6 +272,10 @@ static int setup_revwalk(struct repository *repo,
> strvec_push(&args, "HEAD");
> }
>
> + ret = revwalk_contains_merges(repo, &args);
> + if (ret < 0)
> + goto out;
> +
> setup_revisions_from_strvec(&args, revs, NULL);
> if (args.nr != 1)
> BUG("revisions were set up with invalid argument");
> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
> index 6775ed62f9..12a9a7d051 100755
> --- a/t/t3451-history-reword.sh
> +++ b/t/t3451-history-reword.sh
> @@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '
>
> # It is not possible to replay merge commits embedded in the
> # history (yet).
> - test_must_fail git history reword HEAD~ 2>err &&
> + test_must_fail git -c core.editor=false history reword HEAD~ 2>err &&
> test_grep "replaying merge commits is not supported yet" err &&
>
> # But it is possible to reword a merge commit directly.
Hm, I don't quite see how "false" tests that we don't invoke the
editor at all, though I'm not sure if that behavior should be encoded
in the test or not. (Looks like Junio noticed the same in 1/4; I'm
getting better at reading my mail before sending, sometimes… :)
Otherwise looks good!
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run"
2026-02-12 12:44 ` [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
2026-02-12 20:19 ` Junio C Hamano
@ 2026-02-12 22:20 ` D. Ben Knoble
2026-02-13 5:50 ` Patrick Steinhardt
1 sibling, 1 reply; 34+ messages in thread
From: D. Ben Knoble @ 2026-02-12 22:20 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> The git-history(1) command has the ability to perform a dry-run
> that will not end up modifying any references. Instead, we'll only print
> any ref updates that would happen as a consequence of performing the
> operation.
>
> This mode is somewhat hidden though behind the "--ref-action=print"
> option. This command line option has its origin in git-replay(1), where
> it's probably an okayish interface as this command is sitting more on
> the plumbing side of tools. But git-history(1) is a user-facing tool,
> and this way of achieving a dry-run is way too technical and thus not
> very discoverable.
>
> Besides usability issues, it also has another issue: the dry-run mode
> will always operate as if the user wanted to rewrite all branches. But
> in fact, the user also has the option to only update the HEAD reference,
> and they might want to perform a dry-run of such an operation, too. We
> could of course introduce "--ref-actoin=print-head", but that would
> become even less ergonomic.
>
> Replace "--ref-action=print" with a new "--dry-run" toggle. This new
> toggle works with both "--ref-action={head,branches}" and is way more
> discoverable.
>
> Add a test to verify that both "--ref-action=" values behave as
> expected.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 12 ++--
> builtin/history.c | 150 ++++++++++++++++++++++-------------------
> t/t3451-history-reword.sh | 12 +++-
> 3 files changed, 96 insertions(+), 78 deletions(-)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index 154e262b76..33353815cb 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
> SYNOPSIS
> --------
> [synopsis]
> -git history reword <commit> [--ref-action=(branches|head|print)]
> +git history reword <commit> [--dry-run] [--ref-action=(branches|head)]
>
> DESCRIPTION
> -----------
> @@ -60,13 +60,15 @@ The following commands are available to rewrite history in different ways:
> OPTIONS
> -------
>
> -`--ref-action=(branches|head|print)`::
> +`--dry-run`::
> + Do not update any references, but instead print any ref updates in a
> + format that can be consumed by linkgit:git-update-ref[1].
> +
> +`--ref-action=(branches|head)`::
> Control which references will be updated by the command, if any. With
> `branches`, all local branches that point to commits which are
> descendants of the original commit will be rewritten. With `head`, only
> - the current `HEAD` reference will be rewritten. With `print`, all
> - updates as they would be performed with `branches` are printed in a
> - format that can be consumed by linkgit:git-update-ref[1].
> + the current `HEAD` reference will be rewritten.
Unrelated to this series, I've just realized I can't tell from these
docs what the default --ref-action is. The code says "branches."
>
> GIT
> ---
> diff --git a/builtin/history.c b/builtin/history.c
> index ca0cdb6a58..40e2925cea 100644
> --- a/builtin/history.c
> +++ b/builtin/history.c
> @@ -18,7 +18,7 @@
> #include "wt-status.h"
>
> #define GIT_HISTORY_REWORD_USAGE \
> - N_("git history reword <commit> [--ref-action=(branches|head|print)]")
> + N_("git history reword <commit> [--dry-run] [--ref-action=(branches|head)]")
>
> static void change_data_free(void *util, const char *str UNUSED)
> {
> @@ -155,7 +155,6 @@ enum ref_action {
> REF_ACTION_DEFAULT,
> REF_ACTION_BRANCHES,
> REF_ACTION_HEAD,
> - REF_ACTION_PRINT,
> };
>
> static int parse_ref_action(const struct option *opt, const char *value, int unset)
> @@ -167,10 +166,8 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
> *action = REF_ACTION_BRANCHES;
> } else if (!strcmp(value, "head")) {
> *action = REF_ACTION_HEAD;
> - } else if (!strcmp(value, "print")) {
> - *action = REF_ACTION_PRINT;
> } else {
> - return error(_("%s expects one of 'branches', 'head' or 'print'"),
> + return error(_("%s expects one of 'branches' or 'head'"),
> opt->long_name);
> }
>
> @@ -287,11 +284,29 @@ static int setup_revwalk(struct repository *repo,
> return ret;
> }
>
> +static int handle_ref_update(struct ref_transaction *transaction,
> + const char *refname,
> + const struct object_id *new_oid,
> + const struct object_id *old_oid,
> + const char *reflog_msg,
> + struct strbuf *err)
> +{
> + if (!transaction) {
> + printf("update %s %s %s\n",
> + refname, oid_to_hex(new_oid), oid_to_hex(old_oid));
> + return 0;
> + }
> +
> + return ref_transaction_update(transaction, refname, new_oid, old_oid,
> + NULL, NULL, 0, reflog_msg, err);
> +}
> +
> static int handle_reference_updates(struct rev_info *revs,
> enum ref_action action,
> struct commit *original,
> struct commit *rewritten,
> - const char *reflog_msg)
> + const char *reflog_msg,
> + int dry_run)
> {
> const struct name_decoration *decoration;
> struct replay_revisions_options opts = { 0 };
> @@ -313,82 +328,72 @@ static int handle_reference_updates(struct rev_info *revs,
> if (ret)
> goto out;
>
> - switch (action) {
> - case REF_ACTION_BRANCHES:
> - case REF_ACTION_HEAD:
> + if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
> + BUG("unsupported ref action %d", action);
> +
> + if (!dry_run) {
> transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
> if (!transaction) {
> ret = error(_("failed to begin ref transaction: %s"), err.buf);
> goto out;
> }
> + }
It took me longer than I'd like to admit to realize that only
initializing the transaction this way is safe, because we change the
handler below:
>
> - for (size_t i = 0; i < result.updates_nr; i++) {
> - ret = ref_transaction_update(transaction,
> - result.updates[i].refname,
> - &result.updates[i].new_oid,
> - &result.updates[i].old_oid,
> - NULL, NULL, 0, reflog_msg, &err);
> - if (ret) {
> - ret = error(_("failed to update ref '%s': %s"),
> - result.updates[i].refname, err.buf);
> - goto out;
> - }
> + for (size_t i = 0; i < result.updates_nr; i++) {
> + ret = handle_ref_update(transaction,
> + result.updates[i].refname,
> + &result.updates[i].new_oid,
> + &result.updates[i].old_oid,
> + reflog_msg, &err);
> + if (ret) {
> + ret = error(_("failed to update ref '%s': %s"),
> + result.updates[i].refname, err.buf);
> + goto out;
> }
> + }
To use our new "NULL-safe transaction update" function. Phew. Using
"-b" with git-diff/show/etc. helps quite a bit.
> +
> + /*
> + * `replay_revisions()` only updates references that are
> + * ancestors of `rewritten`, so we need to manually
> + * handle updating references that point to `original`.
> + */
> + for (decoration = get_name_decoration(&original->object);
> + decoration;
> + decoration = decoration->next)
> + {
> + if (decoration->type != DECORATION_REF_LOCAL &&
> + decoration->type != DECORATION_REF_HEAD)
> + continue;
> +
> + if (action == REF_ACTION_HEAD &&
> + decoration->type != DECORATION_REF_HEAD)
> + continue;
>
> /*
> - * `replay_revisions()` only updates references that are
> - * ancestors of `rewritten`, so we need to manually
> - * handle updating references that point to `original`.
> + * We only need to update HEAD separately in case it's
> + * detached. If it's not we'd already update the branch
> + * it is pointing to.
> */
> - for (decoration = get_name_decoration(&original->object);
> - decoration;
> - decoration = decoration->next)
> - {
> - if (decoration->type != DECORATION_REF_LOCAL &&
> - decoration->type != DECORATION_REF_HEAD)
> - continue;
> -
> - if (action == REF_ACTION_HEAD &&
> - decoration->type != DECORATION_REF_HEAD)
> - continue;
> -
> - /*
> - * We only need to update HEAD separately in case it's
> - * detached. If it's not we'd already update the branch
> - * it is pointing to.
> - */
> - if (action == REF_ACTION_BRANCHES &&
> - decoration->type == DECORATION_REF_HEAD &&
> - !detached_head)
> - continue;
> -
> - ret = ref_transaction_update(transaction,
> - decoration->name,
> - &rewritten->object.oid,
> - &original->object.oid,
> - NULL, NULL, 0, reflog_msg, &err);
> - if (ret) {
> - ret = error(_("failed to update ref '%s': %s"),
> - decoration->name, err.buf);
> - goto out;
> - }
> - }
> -
> - if (ref_transaction_commit(transaction, &err)) {
> - ret = error(_("failed to commit ref transaction: %s"), err.buf);
> + if (action == REF_ACTION_BRANCHES &&
> + decoration->type == DECORATION_REF_HEAD &&
> + !detached_head)
> + continue;
> +
> + ret = handle_ref_update(transaction,
> + decoration->name,
> + &rewritten->object.oid,
> + &original->object.oid,
> + reflog_msg, &err);
> + if (ret) {
> + ret = error(_("failed to update ref '%s': %s"),
> + decoration->name, err.buf);
> goto out;
> }
> + }
>
> - break;
> - case REF_ACTION_PRINT:
> - for (size_t i = 0; i < result.updates_nr; i++)
> - printf("update %s %s %s\n",
> - result.updates[i].refname,
> - oid_to_hex(&result.updates[i].new_oid),
> - oid_to_hex(&result.updates[i].old_oid));
> - break;
> - default:
> - BUG("unsupported ref action %d", action);
> + if (transaction && ref_transaction_commit(transaction, &err)) {
> + ret = error(_("failed to commit ref transaction: %s"), err.buf);
> + goto out;
> }
All effectively whitespace change, except for the new transaction
check, which is sensible.
>
> ret = 0;
> @@ -410,10 +415,13 @@ static int cmd_history_reword(int argc,
> NULL,
> };
> enum ref_action action = REF_ACTION_DEFAULT;
> + int dry_run = 0;
> struct option options[] = {
> OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
> - N_("control ref update behavior (branches|head|print)"),
> + N_("control ref update behavior (branches|head)"),
> PARSE_OPT_NONEG, parse_ref_action),
> + OPT_BOOL('n', "dry-run", &dry_run,
> + N_("perform a dry-run without updating any refs")),
> OPT_END(),
> };
> struct strbuf reflog_msg = STRBUF_INIT;
> @@ -450,7 +458,7 @@ static int cmd_history_reword(int argc,
> strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
>
> ret = handle_reference_updates(&revs, action, original, rewritten,
> - reflog_msg.buf);
> + reflog_msg.buf, dry_run);
> if (ret < 0) {
> ret = error(_("failed replaying descendants"));
> goto out;
> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
> index 12a9a7d051..702d40dc06 100755
> --- a/t/t3451-history-reword.sh
> +++ b/t/t3451-history-reword.sh
> @@ -221,7 +221,7 @@ test_expect_success 'can reword a merge commit' '
> )
> '
>
> -test_expect_success '--ref-action=print prints ref updates without modifying repo' '
> +test_expect_success '--dry-run prints ref updates without modifying repo' '
> test_when_finished "rm -rf repo" &&
> git init repo --initial-branch=main &&
> (
> @@ -233,7 +233,15 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep
> test_commit theirs &&
>
> git refs list >refs-expect &&
> - reword_with_message --ref-action=print base >updates <<-\EOF &&
> + reword_with_message --dry-run --ref-action=head base >updates <<-\EOF &&
> + reworded commit
> + EOF
> + git refs list >refs-actual &&
> + test_cmp refs-expect refs-actual &&
> + test_grep "update refs/heads/branch" updates &&
> + test_grep ! "update refs/heads/main" updates &&
> +
> + reword_with_message --dry-run base >updates <<-\EOF &&
> reworded commit
> EOF
> git refs list >refs-actual &&
>
> --
> 2.53.0.295.g64333814d3.dirty
>
>
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH 2/4] builtin/history: check for merges before asking for user input
2026-02-12 22:20 ` D. Ben Knoble
@ 2026-02-12 22:26 ` Junio C Hamano
2026-02-13 5:51 ` Patrick Steinhardt
2026-02-13 5:51 ` Patrick Steinhardt
1 sibling, 1 reply; 34+ messages in thread
From: Junio C Hamano @ 2026-02-12 22:26 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: Patrick Steinhardt, git
"D. Ben Knoble" <ben.knoble@gmail.com> writes:
> On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:
>>
>> The replay infrastructure is not yet capable of replaying merge commits.
>> Unfortunately, we only notice that we're about to replay merges after we
>> have already asked the user for input, so any commit message that the
>> user may have written will be discarded in that case.
>>
>> Fix this by checking whether the revwalk contains merge commits before
>> we ask for user input.
>
> Indeed, that would be irritating :)
>
>>
>> Signed-off-by: Patrick Steinhardt <ps@pks.im>
>> ---
>> builtin/history.c | 40 ++++++++++++++++++++++++++++++++++++++++
>> t/t3451-history-reword.sh | 2 +-
>> 2 files changed, 41 insertions(+), 1 deletion(-)
>>
>> diff --git a/builtin/history.c b/builtin/history.c
>> index 1de51372ea..ca0cdb6a58 100644
>> --- a/builtin/history.c
>> +++ b/builtin/history.c
>> @@ -177,6 +177,42 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
>> return 0;
>> }
>>
>> +static int revwalk_contains_merges(struct repository *repo,
>> + const struct strvec *revwalk_args)
>> +{
>> + struct strvec args = STRVEC_INIT;
>> + struct rev_info revs;
>> + int ret;
>> +
>> + for (size_t i = 0; i < revwalk_args->nr; i++)
>> + strvec_push(&args, revwalk_args->v[i]);
>
> I'm surprised we don't have anything like Rust's Vec::append or
> Vec::extend_from_slice
strvec_pushv() is what you are looking for.
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run"
2026-02-12 22:20 ` D. Ben Knoble
@ 2026-02-13 5:50 ` Patrick Steinhardt
0 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 5:50 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: git
On Thu, Feb 12, 2026 at 05:20:13PM -0500, D. Ben Knoble wrote:
> On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:
> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> > index 154e262b76..33353815cb 100644
> > --- a/Documentation/git-history.adoc
> > +++ b/Documentation/git-history.adoc
> > @@ -60,13 +60,15 @@ The following commands are available to rewrite history in different ways:
> > OPTIONS
> > -------
> >
> > -`--ref-action=(branches|head|print)`::
> > +`--dry-run`::
> > + Do not update any references, but instead print any ref updates in a
> > + format that can be consumed by linkgit:git-update-ref[1].
> > +
> > +`--ref-action=(branches|head)`::
> > Control which references will be updated by the command, if any. With
> > `branches`, all local branches that point to commits which are
> > descendants of the original commit will be rewritten. With `head`, only
> > - the current `HEAD` reference will be rewritten. With `print`, all
> > - updates as they would be performed with `branches` are printed in a
> > - format that can be consumed by linkgit:git-update-ref[1].
> > + the current `HEAD` reference will be rewritten.
>
> Unrelated to this series, I've just realized I can't tell from these
> docs what the default --ref-action is. The code says "branches."
Good point indeed. I'll add another patch on top, thanks!
Patrick
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run"
2026-02-12 20:19 ` Junio C Hamano
@ 2026-02-13 5:50 ` Patrick Steinhardt
0 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 5:50 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Thu, Feb 12, 2026 at 12:19:58PM -0800, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> > The git-history(1) command has the ability to perform a dry-run
> > that will not end up modifying any references. Instead, we'll only print
> > any ref updates that would happen as a consequence of performing the
> > operation.
> >
> > This mode is somewhat hidden though behind the "--ref-action=print"
> > option. This command line option has its origin in git-replay(1), where
> > it's probably an okayish interface as this command is sitting more on
> > the plumbing side of tools. But git-history(1) is a user-facing tool,
> > and this way of achieving a dry-run is way too technical and thus not
> > very discoverable.
> >
> > Besides usability issues, it also has another issue: the dry-run mode
> > will always operate as if the user wanted to rewrite all branches. But
> > in fact, the user also has the option to only update the HEAD reference,
> > and they might want to perform a dry-run of such an operation, too. We
> > could of course introduce "--ref-actoin=print-head", but that would
> > become even less ergonomic.
> >
> > Replace "--ref-action=print" with a new "--dry-run" toggle. This new
> > toggle works with both "--ref-action={head,branches}" and is way more
> > discoverable.
> >
> > Add a test to verify that both "--ref-action=" values behave as
> > expected.
>
> This "--dry-run" mode still creates necessary new objects, right?
>
> Describing this "--dry-run" as not updating any refs is a very good
> thing, which is what the documentation update says, but at the same
> time, we should clearly promise that the necessary new objects are
> still created, so should the user then choose to update refs to
> point at the reported objects, it will not result in a repository
> corruption.
>
> Other than that, looking good.
Good point, will add.
Patrick
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input
2026-02-12 20:04 ` Junio C Hamano
@ 2026-02-13 5:51 ` Patrick Steinhardt
2026-02-13 17:02 ` Junio C Hamano
0 siblings, 1 reply; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 5:51 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Thu, Feb 12, 2026 at 12:04:50PM -0800, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> > diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
> > index 3594421b68..6775ed62f9 100755
> > --- a/t/t3451-history-reword.sh
> > +++ b/t/t3451-history-reword.sh
> > @@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '
> >
> > # When told to update HEAD, only, the command will refuse to
> > # rewrite commits that are not an ancestor of HEAD.
> > - test_must_fail git history reword --ref-action=head theirs 2>err &&
> > + test_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&
> > test_grep "rewritten commit must be an ancestor of HEAD" err &&
>
> This ensures that the editor is never consulted? How? Running the
> "false" editor would give us a different error, like "your editor
> exited with non-zero status, telling us to abort" or something?
Yup, exactly that. We'd see "Aborting commit as launching the editor
failed." instead of the above error message.
Patrick
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH 2/4] builtin/history: check for merges before asking for user input
2026-02-12 22:20 ` D. Ben Knoble
2026-02-12 22:26 ` Junio C Hamano
@ 2026-02-13 5:51 ` Patrick Steinhardt
2026-02-13 13:42 ` Ben Knoble
1 sibling, 1 reply; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 5:51 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: git
On Thu, Feb 12, 2026 at 05:20:08PM -0500, D. Ben Knoble wrote:
> On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:
> > diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
> > index 6775ed62f9..12a9a7d051 100755
> > --- a/t/t3451-history-reword.sh
> > +++ b/t/t3451-history-reword.sh
> > @@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '
> >
> > # It is not possible to replay merge commits embedded in the
> > # history (yet).
> > - test_must_fail git history reword HEAD~ 2>err &&
> > + test_must_fail git -c core.editor=false history reword HEAD~ 2>err &&
> > test_grep "replaying merge commits is not supported yet" err &&
> >
> > # But it is possible to reword a merge commit directly.
>
> Hm, I don't quite see how "false" tests that we don't invoke the
> editor at all, though I'm not sure if that behavior should be encoded
> in the test or not. (Looks like Junio noticed the same in 1/4; I'm
> getting better at reading my mail before sending, sometimes… :)
>
> Otherwise looks good!
As replied in the other thread, this would abort with "Aborting commit
as launching the editor failed." if Git tried to execute the editor.
I'm not quite sure whether I get the other remark about "should be
encoded in the test or not". Do you mean to say we should use a separate
test?
Patrick
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH 2/4] builtin/history: check for merges before asking for user input
2026-02-12 22:26 ` Junio C Hamano
@ 2026-02-13 5:51 ` Patrick Steinhardt
0 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 5:51 UTC (permalink / raw)
To: Junio C Hamano; +Cc: D. Ben Knoble, git
On Thu, Feb 12, 2026 at 02:26:46PM -0800, Junio C Hamano wrote:
> "D. Ben Knoble" <ben.knoble@gmail.com> writes:
> > On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:
> >> diff --git a/builtin/history.c b/builtin/history.c
> >> index 1de51372ea..ca0cdb6a58 100644
> >> --- a/builtin/history.c
> >> +++ b/builtin/history.c
> >> @@ -177,6 +177,42 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
> >> return 0;
> >> }
> >>
> >> +static int revwalk_contains_merges(struct repository *repo,
> >> + const struct strvec *revwalk_args)
> >> +{
> >> + struct strvec args = STRVEC_INIT;
> >> + struct rev_info revs;
> >> + int ret;
> >> +
> >> + for (size_t i = 0; i < revwalk_args->nr; i++)
> >> + strvec_push(&args, revwalk_args->v[i]);
> >
> > I'm surprised we don't have anything like Rust's Vec::append or
> > Vec::extend_from_slice
>
> strvec_pushv() is what you are looking for.
Ah, indeed, will use.
Patrick
^ permalink raw reply [flat|nested] 34+ messages in thread
* [PATCH v2 0/5] builtin/history: some smaller UI improvements
2026-02-12 12:44 [PATCH 0/4] builtin/history: some smaller UI improvements Patrick Steinhardt
` (3 preceding siblings ...)
2026-02-12 12:44 ` [PATCH 4/4] builtin/history: rename "--ref-action=" to "--update-refs=" Patrick Steinhardt
@ 2026-02-13 9:12 ` Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 1/5] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
` (4 more replies)
2026-02-16 6:45 ` [PATCH v3 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
5 siblings, 5 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 9:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble
Hi,
this patch series contains a small set of UI improvements for
git-history(1):
- The first two commits adapt git-history(1) so that it performs
verifications before asking the user for input.
- The last two commits rework the "--ref-action=" option to instead be
split up into "--dry-run" and "--update-refs=" so that the option is
less focussed on technical implementation details.
I decided to send this patch series as a small incremental step before
sending `git history split`, also because that series conflicts with
aa/add-p-no-auto-advance.
Changes in v2:
- Use `strvec_pushv()` instead of looping around `strvec_push()`.
- Document that "--dry-run" writes objects for later use.
- Document the default value of "--update-refs=".
- Mention the subtlety around false(1) in the commit messages.
- Link to v1: https://lore.kernel.org/r/20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im
Thanks!
Patrick
---
Patrick Steinhardt (5):
builtin/history: perform revwalk checks before asking for user input
builtin/history: check for merges before asking for user input
builtin/history: replace "--ref-action=print" with "--dry-run"
builtin/history: rename "--ref-action=" to "--update-refs="
Documentation/git-history: document default for "--update-refs="
Documentation/git-history.adoc | 14 ++-
builtin/history.c | 254 ++++++++++++++++++++++++++---------------
t/t3451-history-reword.sh | 20 +++-
3 files changed, 182 insertions(+), 106 deletions(-)
Range-diff versus v1:
1: a4b0654c49 ! 1: 495a8e7a1a builtin/history: perform revwalk checks before asking for user input
@@ Commit message
Extract the function to set up the revision walk and call it before we
ask for user input to fix this.
+ Adapt one of the tests that is expected to fail because of this check
+ to use false(1) as editor. If the editor had been executed by Git, it
+ would fail with the error message "Aborting commit as launching the
+ editor failed."
+
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## builtin/history.c ##
2: 3ea8f7740a ! 2: 13c5edbe7b builtin/history: check for merges before asking for user input
@@ Commit message
Fix this by checking whether the revwalk contains merge commits before
we ask for user input.
+ Adapt one of the tests that is expected to fail because of this check
+ to use false(1) as editor. If the editor had been executed by Git, it
+ would fail with the error message "Aborting commit as launching the
+ editor failed."
+
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## builtin/history.c ##
@@ builtin/history.c: static int parse_ref_action(const struct option *opt, const c
+ struct rev_info revs;
+ int ret;
+
-+ for (size_t i = 0; i < revwalk_args->nr; i++)
-+ strvec_push(&args, revwalk_args->v[i]);
++ strvec_pushv(&args, revwalk_args->v);
+ strvec_push(&args, "--min-parents=2");
+
+ repo_init_revisions(repo, &revs, NULL);
3: 4e605e65e0 ! 3: 7226c4d1af builtin/history: replace "--ref-action=print" with "--dry-run"
@@ Commit message
Add a test to verify that both "--ref-action=" values behave as
expected.
+ This patch is best viewed with "--ignore-space-change".
+
Signed-off-by: Patrick Steinhardt <ps@pks.im>
## Documentation/git-history.adoc ##
@@ Documentation/git-history.adoc: The following commands are available to rewrite
-`--ref-action=(branches|head|print)`::
+`--dry-run`::
+ Do not update any references, but instead print any ref updates in a
-+ format that can be consumed by linkgit:git-update-ref[1].
++ format that can be consumed by linkgit:git-update-ref[1]. Necessary new
++ objects will be written into the repository, so applying these printed
++ ref updates is generally safe.
+
+`--ref-action=(branches|head)`::
Control which references will be updated by the command, if any. With
4: 1da7284902 ! 4: 7f41026981 builtin/history: rename "--ref-action=" to "--update-refs="
@@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history
DESCRIPTION
-----------
@@ Documentation/git-history.adoc: OPTIONS
- Do not update any references, but instead print any ref updates in a
- format that can be consumed by linkgit:git-update-ref[1].
+ objects will be written into the repository, so applying these printed
+ ref updates is generally safe.
-`--ref-action=(branches|head)`::
+`--update-refs=(branches|head)`::
-: ---------- > 5: 0bc831fcab Documentation/git-history: document default for "--update-refs="
---
base-commit: 6fcee4785280a08e7f271bd015a4dc33753e2886
change-id: 20260212-b4-pks-history-dry-run-2b840e530ae6
^ permalink raw reply [flat|nested] 34+ messages in thread
* [PATCH v2 1/5] builtin/history: perform revwalk checks before asking for user input
2026-02-13 9:12 ` [PATCH v2 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
@ 2026-02-13 9:12 ` Patrick Steinhardt
2026-02-13 17:20 ` Junio C Hamano
2026-02-13 9:12 ` [PATCH v2 2/5] builtin/history: check for merges " Patrick Steinhardt
` (3 subsequent siblings)
4 siblings, 1 reply; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 9:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble
When setting up the revision walk in git-history(1) we also perform some
verifications whether the request actually looks sane. Unfortunately,
these verifications come _after_ we have already asked the user for the
commit message of the commit that is to be rewritten. So in case any of
the verifications fails, the user will have lost their modifications.
Extract the function to set up the revision walk and call it before we
ask for user input to fix this.
Adapt one of the tests that is expected to fail because of this check
to use false(1) as editor. If the editor had been executed by Git, it
would fail with the error message "Aborting commit as launching the
editor failed."
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/history.c | 69 +++++++++++++++++++++++++++++------------------
t/t3451-history-reword.sh | 2 +-
2 files changed, 44 insertions(+), 27 deletions(-)
diff --git a/builtin/history.c b/builtin/history.c
index 8dcb9a6046..1de51372ea 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -177,30 +177,15 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
return 0;
}
-static int handle_reference_updates(enum ref_action action,
- struct repository *repo,
- struct commit *original,
- struct commit *rewritten,
- const char *reflog_msg)
+static int setup_revwalk(struct repository *repo,
+ enum ref_action action,
+ struct commit *original,
+ struct rev_info *revs)
{
- const struct name_decoration *decoration;
- struct replay_revisions_options opts = { 0 };
- struct replay_result result = { 0 };
- struct ref_transaction *transaction = NULL;
struct strvec args = STRVEC_INIT;
- struct strbuf err = STRBUF_INIT;
- struct commit *head = NULL;
- struct rev_info revs;
- char hex[GIT_MAX_HEXSZ + 1];
- bool detached_head;
- int head_flags = 0;
int ret;
- refs_read_ref_full(get_main_ref_store(repo), "HEAD",
- RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
- detached_head = !(head_flags & REF_ISSYMREF);
-
- repo_init_revisions(repo, &revs, NULL);
+ repo_init_revisions(repo, revs, NULL);
strvec_push(&args, "ignored");
strvec_push(&args, "--reverse");
strvec_push(&args, "--topo-order");
@@ -224,6 +209,7 @@ static int handle_reference_updates(enum ref_action action,
*/
if (action == REF_ACTION_HEAD) {
struct commit_list *from_list = NULL;
+ struct commit *head;
head = lookup_commit_reference_by_name("HEAD");
if (!head) {
@@ -250,20 +236,47 @@ static int handle_reference_updates(enum ref_action action,
strvec_push(&args, "HEAD");
}
- setup_revisions_from_strvec(&args, &revs, NULL);
+ setup_revisions_from_strvec(&args, revs, NULL);
if (args.nr != 1)
BUG("revisions were set up with invalid argument");
+ ret = 0;
+
+out:
+ strvec_clear(&args);
+ return ret;
+}
+
+static int handle_reference_updates(struct rev_info *revs,
+ enum ref_action action,
+ struct commit *original,
+ struct commit *rewritten,
+ const char *reflog_msg)
+{
+ const struct name_decoration *decoration;
+ struct replay_revisions_options opts = { 0 };
+ struct replay_result result = { 0 };
+ struct ref_transaction *transaction = NULL;
+ struct strbuf err = STRBUF_INIT;
+ char hex[GIT_MAX_HEXSZ + 1];
+ bool detached_head;
+ int head_flags = 0;
+ int ret;
+
+ refs_read_ref_full(get_main_ref_store(revs->repo), "HEAD",
+ RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
+ detached_head = !(head_flags & REF_ISSYMREF);
+
opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
- ret = replay_revisions(&revs, &opts, &result);
+ ret = replay_revisions(revs, &opts, &result);
if (ret)
goto out;
switch (action) {
case REF_ACTION_BRANCHES:
case REF_ACTION_HEAD:
- transaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);
+ transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
if (!transaction) {
ret = error(_("failed to begin ref transaction: %s"), err.buf);
goto out;
@@ -343,9 +356,7 @@ static int handle_reference_updates(enum ref_action action,
out:
ref_transaction_free(transaction);
replay_result_release(&result);
- release_revisions(&revs);
strbuf_release(&err);
- strvec_clear(&args);
return ret;
}
@@ -367,6 +378,7 @@ static int cmd_history_reword(int argc,
};
struct strbuf reflog_msg = STRBUF_INIT;
struct commit *original, *rewritten;
+ struct rev_info revs;
int ret;
argc = parse_options(argc, argv, prefix, options, usage, 0);
@@ -385,6 +397,10 @@ static int cmd_history_reword(int argc,
goto out;
}
+ ret = setup_revwalk(repo, action, original, &revs);
+ if (ret)
+ goto out;
+
ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
if (ret < 0) {
ret = error(_("failed writing reworded commit"));
@@ -393,7 +409,7 @@ static int cmd_history_reword(int argc,
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
- ret = handle_reference_updates(action, repo, original, rewritten,
+ ret = handle_reference_updates(&revs, action, original, rewritten,
reflog_msg.buf);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
@@ -404,6 +420,7 @@ static int cmd_history_reword(int argc,
out:
strbuf_release(&reflog_msg);
+ release_revisions(&revs);
return ret;
}
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 3594421b68..6775ed62f9 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '
# When told to update HEAD, only, the command will refuse to
# rewrite commits that are not an ancestor of HEAD.
- test_must_fail git history reword --ref-action=head theirs 2>err &&
+ test_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&
test_grep "rewritten commit must be an ancestor of HEAD" err &&
reword_with_message --ref-action=head base >updates <<-\EOF &&
--
2.53.0.352.gd1286b26eb.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH v2 2/5] builtin/history: check for merges before asking for user input
2026-02-13 9:12 ` [PATCH v2 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 1/5] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
@ 2026-02-13 9:12 ` Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 3/5] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
` (2 subsequent siblings)
4 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 9:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble
The replay infrastructure is not yet capable of replaying merge commits.
Unfortunately, we only notice that we're about to replay merges after we
have already asked the user for input, so any commit message that the
user may have written will be discarded in that case.
Fix this by checking whether the revwalk contains merge commits before
we ask for user input.
Adapt one of the tests that is expected to fail because of this check
to use false(1) as editor. If the editor had been executed by Git, it
would fail with the error message "Aborting commit as launching the
editor failed."
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/history.c | 39 +++++++++++++++++++++++++++++++++++++++
t/t3451-history-reword.sh | 2 +-
2 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/builtin/history.c b/builtin/history.c
index 1de51372ea..ff90e93d6e 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -177,6 +177,41 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
return 0;
}
+static int revwalk_contains_merges(struct repository *repo,
+ const struct strvec *revwalk_args)
+{
+ struct strvec args = STRVEC_INIT;
+ struct rev_info revs;
+ int ret;
+
+ strvec_pushv(&args, revwalk_args->v);
+ strvec_push(&args, "--min-parents=2");
+
+ repo_init_revisions(repo, &revs, NULL);
+
+ setup_revisions_from_strvec(&args, &revs, NULL);
+ if (args.nr != 1)
+ BUG("revisions were set up with invalid argument");
+
+ if (prepare_revision_walk(&revs) < 0) {
+ ret = error(_("error preparing revisions"));
+ goto out;
+ }
+
+ if (get_revision(&revs)) {
+ ret = error(_("replaying merge commits is not supported yet!"));
+ goto out;
+ }
+
+ reset_revision_walk();
+ ret = 0;
+
+out:
+ release_revisions(&revs);
+ strvec_clear(&args);
+ return ret;
+}
+
static int setup_revwalk(struct repository *repo,
enum ref_action action,
struct commit *original,
@@ -236,6 +271,10 @@ static int setup_revwalk(struct repository *repo,
strvec_push(&args, "HEAD");
}
+ ret = revwalk_contains_merges(repo, &args);
+ if (ret < 0)
+ goto out;
+
setup_revisions_from_strvec(&args, revs, NULL);
if (args.nr != 1)
BUG("revisions were set up with invalid argument");
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 6775ed62f9..12a9a7d051 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '
# It is not possible to replay merge commits embedded in the
# history (yet).
- test_must_fail git history reword HEAD~ 2>err &&
+ test_must_fail git -c core.editor=false history reword HEAD~ 2>err &&
test_grep "replaying merge commits is not supported yet" err &&
# But it is possible to reword a merge commit directly.
--
2.53.0.352.gd1286b26eb.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH v2 3/5] builtin/history: replace "--ref-action=print" with "--dry-run"
2026-02-13 9:12 ` [PATCH v2 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 1/5] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 2/5] builtin/history: check for merges " Patrick Steinhardt
@ 2026-02-13 9:12 ` Patrick Steinhardt
2026-02-13 17:30 ` Kristoffer Haugsbakk
2026-02-13 9:12 ` [PATCH v2 4/5] builtin/history: rename "--ref-action=" to "--update-refs=" Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 5/5] Documentation/git-history: document default for "--update-refs=" Patrick Steinhardt
4 siblings, 1 reply; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 9:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble
The git-history(1) command has the ability to perform a dry-run
that will not end up modifying any references. Instead, we'll only print
any ref updates that would happen as a consequence of performing the
operation.
This mode is somewhat hidden though behind the "--ref-action=print"
option. This command line option has its origin in git-replay(1), where
it's probably an okayish interface as this command is sitting more on
the plumbing side of tools. But git-history(1) is a user-facing tool,
and this way of achieving a dry-run is way too technical and thus not
very discoverable.
Besides usability issues, it also has another issue: the dry-run mode
will always operate as if the user wanted to rewrite all branches. But
in fact, the user also has the option to only update the HEAD reference,
and they might want to perform a dry-run of such an operation, too. We
could of course introduce "--ref-actoin=print-head", but that would
become even less ergonomic.
Replace "--ref-action=print" with a new "--dry-run" toggle. This new
toggle works with both "--ref-action={head,branches}" and is way more
discoverable.
Add a test to verify that both "--ref-action=" values behave as
expected.
This patch is best viewed with "--ignore-space-change".
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 14 ++--
builtin/history.c | 150 ++++++++++++++++++++++-------------------
t/t3451-history-reword.sh | 12 +++-
3 files changed, 98 insertions(+), 78 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 154e262b76..df2900ac2f 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
SYNOPSIS
--------
[synopsis]
-git history reword <commit> [--ref-action=(branches|head|print)]
+git history reword <commit> [--dry-run] [--ref-action=(branches|head)]
DESCRIPTION
-----------
@@ -60,13 +60,17 @@ The following commands are available to rewrite history in different ways:
OPTIONS
-------
-`--ref-action=(branches|head|print)`::
+`--dry-run`::
+ Do not update any references, but instead print any ref updates in a
+ format that can be consumed by linkgit:git-update-ref[1]. Necessary new
+ objects will be written into the repository, so applying these printed
+ ref updates is generally safe.
+
+`--ref-action=(branches|head)`::
Control which references will be updated by the command, if any. With
`branches`, all local branches that point to commits which are
descendants of the original commit will be rewritten. With `head`, only
- the current `HEAD` reference will be rewritten. With `print`, all
- updates as they would be performed with `branches` are printed in a
- format that can be consumed by linkgit:git-update-ref[1].
+ the current `HEAD` reference will be rewritten.
GIT
---
diff --git a/builtin/history.c b/builtin/history.c
index ff90e93d6e..c135361c67 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -18,7 +18,7 @@
#include "wt-status.h"
#define GIT_HISTORY_REWORD_USAGE \
- N_("git history reword <commit> [--ref-action=(branches|head|print)]")
+ N_("git history reword <commit> [--dry-run] [--ref-action=(branches|head)]")
static void change_data_free(void *util, const char *str UNUSED)
{
@@ -155,7 +155,6 @@ enum ref_action {
REF_ACTION_DEFAULT,
REF_ACTION_BRANCHES,
REF_ACTION_HEAD,
- REF_ACTION_PRINT,
};
static int parse_ref_action(const struct option *opt, const char *value, int unset)
@@ -167,10 +166,8 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
*action = REF_ACTION_BRANCHES;
} else if (!strcmp(value, "head")) {
*action = REF_ACTION_HEAD;
- } else if (!strcmp(value, "print")) {
- *action = REF_ACTION_PRINT;
} else {
- return error(_("%s expects one of 'branches', 'head' or 'print'"),
+ return error(_("%s expects one of 'branches' or 'head'"),
opt->long_name);
}
@@ -286,11 +283,29 @@ static int setup_revwalk(struct repository *repo,
return ret;
}
+static int handle_ref_update(struct ref_transaction *transaction,
+ const char *refname,
+ const struct object_id *new_oid,
+ const struct object_id *old_oid,
+ const char *reflog_msg,
+ struct strbuf *err)
+{
+ if (!transaction) {
+ printf("update %s %s %s\n",
+ refname, oid_to_hex(new_oid), oid_to_hex(old_oid));
+ return 0;
+ }
+
+ return ref_transaction_update(transaction, refname, new_oid, old_oid,
+ NULL, NULL, 0, reflog_msg, err);
+}
+
static int handle_reference_updates(struct rev_info *revs,
enum ref_action action,
struct commit *original,
struct commit *rewritten,
- const char *reflog_msg)
+ const char *reflog_msg,
+ int dry_run)
{
const struct name_decoration *decoration;
struct replay_revisions_options opts = { 0 };
@@ -312,82 +327,72 @@ static int handle_reference_updates(struct rev_info *revs,
if (ret)
goto out;
- switch (action) {
- case REF_ACTION_BRANCHES:
- case REF_ACTION_HEAD:
+ if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
+ BUG("unsupported ref action %d", action);
+
+ if (!dry_run) {
transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
if (!transaction) {
ret = error(_("failed to begin ref transaction: %s"), err.buf);
goto out;
}
+ }
- for (size_t i = 0; i < result.updates_nr; i++) {
- ret = ref_transaction_update(transaction,
- result.updates[i].refname,
- &result.updates[i].new_oid,
- &result.updates[i].old_oid,
- NULL, NULL, 0, reflog_msg, &err);
- if (ret) {
- ret = error(_("failed to update ref '%s': %s"),
- result.updates[i].refname, err.buf);
- goto out;
- }
+ for (size_t i = 0; i < result.updates_nr; i++) {
+ ret = handle_ref_update(transaction,
+ result.updates[i].refname,
+ &result.updates[i].new_oid,
+ &result.updates[i].old_oid,
+ reflog_msg, &err);
+ if (ret) {
+ ret = error(_("failed to update ref '%s': %s"),
+ result.updates[i].refname, err.buf);
+ goto out;
}
+ }
+
+ /*
+ * `replay_revisions()` only updates references that are
+ * ancestors of `rewritten`, so we need to manually
+ * handle updating references that point to `original`.
+ */
+ for (decoration = get_name_decoration(&original->object);
+ decoration;
+ decoration = decoration->next)
+ {
+ if (decoration->type != DECORATION_REF_LOCAL &&
+ decoration->type != DECORATION_REF_HEAD)
+ continue;
+
+ if (action == REF_ACTION_HEAD &&
+ decoration->type != DECORATION_REF_HEAD)
+ continue;
/*
- * `replay_revisions()` only updates references that are
- * ancestors of `rewritten`, so we need to manually
- * handle updating references that point to `original`.
+ * We only need to update HEAD separately in case it's
+ * detached. If it's not we'd already update the branch
+ * it is pointing to.
*/
- for (decoration = get_name_decoration(&original->object);
- decoration;
- decoration = decoration->next)
- {
- if (decoration->type != DECORATION_REF_LOCAL &&
- decoration->type != DECORATION_REF_HEAD)
- continue;
-
- if (action == REF_ACTION_HEAD &&
- decoration->type != DECORATION_REF_HEAD)
- continue;
-
- /*
- * We only need to update HEAD separately in case it's
- * detached. If it's not we'd already update the branch
- * it is pointing to.
- */
- if (action == REF_ACTION_BRANCHES &&
- decoration->type == DECORATION_REF_HEAD &&
- !detached_head)
- continue;
-
- ret = ref_transaction_update(transaction,
- decoration->name,
- &rewritten->object.oid,
- &original->object.oid,
- NULL, NULL, 0, reflog_msg, &err);
- if (ret) {
- ret = error(_("failed to update ref '%s': %s"),
- decoration->name, err.buf);
- goto out;
- }
- }
-
- if (ref_transaction_commit(transaction, &err)) {
- ret = error(_("failed to commit ref transaction: %s"), err.buf);
+ if (action == REF_ACTION_BRANCHES &&
+ decoration->type == DECORATION_REF_HEAD &&
+ !detached_head)
+ continue;
+
+ ret = handle_ref_update(transaction,
+ decoration->name,
+ &rewritten->object.oid,
+ &original->object.oid,
+ reflog_msg, &err);
+ if (ret) {
+ ret = error(_("failed to update ref '%s': %s"),
+ decoration->name, err.buf);
goto out;
}
+ }
- break;
- case REF_ACTION_PRINT:
- for (size_t i = 0; i < result.updates_nr; i++)
- printf("update %s %s %s\n",
- result.updates[i].refname,
- oid_to_hex(&result.updates[i].new_oid),
- oid_to_hex(&result.updates[i].old_oid));
- break;
- default:
- BUG("unsupported ref action %d", action);
+ if (transaction && ref_transaction_commit(transaction, &err)) {
+ ret = error(_("failed to commit ref transaction: %s"), err.buf);
+ goto out;
}
ret = 0;
@@ -409,10 +414,13 @@ static int cmd_history_reword(int argc,
NULL,
};
enum ref_action action = REF_ACTION_DEFAULT;
+ int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
- N_("control ref update behavior (branches|head|print)"),
+ N_("control ref update behavior (branches|head)"),
PARSE_OPT_NONEG, parse_ref_action),
+ OPT_BOOL('n', "dry-run", &dry_run,
+ N_("perform a dry-run without updating any refs")),
OPT_END(),
};
struct strbuf reflog_msg = STRBUF_INIT;
@@ -449,7 +457,7 @@ static int cmd_history_reword(int argc,
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf);
+ reflog_msg.buf, dry_run);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 12a9a7d051..702d40dc06 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -221,7 +221,7 @@ test_expect_success 'can reword a merge commit' '
)
'
-test_expect_success '--ref-action=print prints ref updates without modifying repo' '
+test_expect_success '--dry-run prints ref updates without modifying repo' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
@@ -233,7 +233,15 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep
test_commit theirs &&
git refs list >refs-expect &&
- reword_with_message --ref-action=print base >updates <<-\EOF &&
+ reword_with_message --dry-run --ref-action=head base >updates <<-\EOF &&
+ reworded commit
+ EOF
+ git refs list >refs-actual &&
+ test_cmp refs-expect refs-actual &&
+ test_grep "update refs/heads/branch" updates &&
+ test_grep ! "update refs/heads/main" updates &&
+
+ reword_with_message --dry-run base >updates <<-\EOF &&
reworded commit
EOF
git refs list >refs-actual &&
--
2.53.0.352.gd1286b26eb.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH v2 4/5] builtin/history: rename "--ref-action=" to "--update-refs="
2026-02-13 9:12 ` [PATCH v2 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
` (2 preceding siblings ...)
2026-02-13 9:12 ` [PATCH v2 3/5] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
@ 2026-02-13 9:12 ` Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 5/5] Documentation/git-history: document default for "--update-refs=" Patrick Steinhardt
4 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 9:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble
With the preceding commit we have changed "--ref-action=" to only
control which refs are supposed to be updated, not what happens with
them. As a consequence, the option is now somewhat misnamed, as we don't
control the action itself anymore.
Rename it to "--update-refs=" to better align it with its new use.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 4 ++--
builtin/history.c | 8 ++++----
t/t3451-history-reword.sh | 8 ++++----
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index df2900ac2f..4dbe665ec4 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
SYNOPSIS
--------
[synopsis]
-git history reword <commit> [--dry-run] [--ref-action=(branches|head)]
+git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
DESCRIPTION
-----------
@@ -66,7 +66,7 @@ OPTIONS
objects will be written into the repository, so applying these printed
ref updates is generally safe.
-`--ref-action=(branches|head)`::
+`--update-refs=(branches|head)`::
Control which references will be updated by the command, if any. With
`branches`, all local branches that point to commits which are
descendants of the original commit will be rewritten. With `head`, only
diff --git a/builtin/history.c b/builtin/history.c
index c135361c67..1cf6c668cf 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -18,7 +18,7 @@
#include "wt-status.h"
#define GIT_HISTORY_REWORD_USAGE \
- N_("git history reword <commit> [--dry-run] [--ref-action=(branches|head)]")
+ N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
static void change_data_free(void *util, const char *str UNUSED)
{
@@ -258,7 +258,7 @@ static int setup_revwalk(struct repository *repo,
goto out;
} else if (!ret) {
ret = error(_("rewritten commit must be an ancestor "
- "of HEAD when using --ref-action=head"));
+ "of HEAD when using --update-refs=head"));
goto out;
}
@@ -416,8 +416,8 @@ static int cmd_history_reword(int argc,
enum ref_action action = REF_ACTION_DEFAULT;
int dry_run = 0;
struct option options[] = {
- OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
- N_("control ref update behavior (branches|head)"),
+ OPT_CALLBACK_F(0, "update-refs", &action, N_("<action>"),
+ N_("control which refs should be updated (branches|head)"),
PARSE_OPT_NONEG, parse_ref_action),
OPT_BOOL('n', "dry-run", &dry_run,
N_("perform a dry-run without updating any refs")),
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 702d40dc06..de7b357685 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -233,7 +233,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '
test_commit theirs &&
git refs list >refs-expect &&
- reword_with_message --dry-run --ref-action=head base >updates <<-\EOF &&
+ reword_with_message --dry-run --update-refs=head base >updates <<-\EOF &&
reworded commit
EOF
git refs list >refs-actual &&
@@ -258,7 +258,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '
)
'
-test_expect_success '--ref-action=head updates only HEAD' '
+test_expect_success '--update-refs=head updates only HEAD' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
@@ -271,10 +271,10 @@ test_expect_success '--ref-action=head updates only HEAD' '
# When told to update HEAD, only, the command will refuse to
# rewrite commits that are not an ancestor of HEAD.
- test_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&
+ test_must_fail git -c core.editor=false history reword --update-refs=head theirs 2>err &&
test_grep "rewritten commit must be an ancestor of HEAD" err &&
- reword_with_message --ref-action=head base >updates <<-\EOF &&
+ reword_with_message --update-refs=head base >updates <<-\EOF &&
reworded base
EOF
expect_log HEAD <<-\EOF &&
--
2.53.0.352.gd1286b26eb.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH v2 5/5] Documentation/git-history: document default for "--update-refs="
2026-02-13 9:12 ` [PATCH v2 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
` (3 preceding siblings ...)
2026-02-13 9:12 ` [PATCH v2 4/5] builtin/history: rename "--ref-action=" to "--update-refs=" Patrick Steinhardt
@ 2026-02-13 9:12 ` Patrick Steinhardt
2026-02-13 17:21 ` Junio C Hamano
4 siblings, 1 reply; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-13 9:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble
While we document the values that can be passed to the "--update-refs="
option, we don't give the user any hint what the default behaviour is.
Document it.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 4dbe665ec4..cc019de697 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -70,7 +70,7 @@ OPTIONS
Control which references will be updated by the command, if any. With
`branches`, all local branches that point to commits which are
descendants of the original commit will be rewritten. With `head`, only
- the current `HEAD` reference will be rewritten.
+ the current `HEAD` reference will be rewritten. Defaults to `branches`.
GIT
---
--
2.53.0.352.gd1286b26eb.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* Re: [PATCH 2/4] builtin/history: check for merges before asking for user input
2026-02-13 5:51 ` Patrick Steinhardt
@ 2026-02-13 13:42 ` Ben Knoble
0 siblings, 0 replies; 34+ messages in thread
From: Ben Knoble @ 2026-02-13 13:42 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
> Le 13 févr. 2026 à 00:51, Patrick Steinhardt <ps@pks.im> a écrit :
>
> On Thu, Feb 12, 2026 at 05:20:08PM -0500, D. Ben Knoble wrote:
>>> On Thu, Feb 12, 2026 at 7:45 AM Patrick Steinhardt <ps@pks.im> wrote:
>>> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
>>> index 6775ed62f9..12a9a7d051 100755
>>> --- a/t/t3451-history-reword.sh
>>> +++ b/t/t3451-history-reword.sh
>>> @@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '
>>>
>>> # It is not possible to replay merge commits embedded in the
>>> # history (yet).
>>> - test_must_fail git history reword HEAD~ 2>err &&
>>> + test_must_fail git -c core.editor=false history reword HEAD~ 2>err &&
>>> test_grep "replaying merge commits is not supported yet" err &&
>>>
>>> # But it is possible to reword a merge commit directly.
>>
>> Hm, I don't quite see how "false" tests that we don't invoke the
>> editor at all, though I'm not sure if that behavior should be encoded
>> in the test or not. (Looks like Junio noticed the same in 1/4; I'm
>> getting better at reading my mail before sending, sometimes… :)
>>
>> Otherwise looks good!
>
> As replied in the other thread, this would abort with "Aborting commit
> as launching the editor failed." if Git tried to execute the editor.
>
> I'm not quite sure whether I get the other remark about "should be
> encoded in the test or not". Do you mean to say we should use a separate
> test?
>
> Patrick
No, 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 :)
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input
2026-02-13 5:51 ` Patrick Steinhardt
@ 2026-02-13 17:02 ` Junio C Hamano
0 siblings, 0 replies; 34+ messages in thread
From: Junio C Hamano @ 2026-02-13 17:02 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git
Patrick Steinhardt <ps@pks.im> writes:
> On Thu, Feb 12, 2026 at 12:04:50PM -0800, Junio C Hamano wrote:
>> Patrick Steinhardt <ps@pks.im> writes:
>> > diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
>> > index 3594421b68..6775ed62f9 100755
>> > --- a/t/t3451-history-reword.sh
>> > +++ b/t/t3451-history-reword.sh
>> > @@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '
>> >
>> > # When told to update HEAD, only, the command will refuse to
>> > # rewrite commits that are not an ancestor of HEAD.
>> > - test_must_fail git history reword --ref-action=head theirs 2>err &&
>> > + test_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&
>> > test_grep "rewritten commit must be an ancestor of HEAD" err &&
>>
>> This ensures that the editor is never consulted? How? Running the
>> "false" editor would give us a different error, like "your editor
>> exited with non-zero status, telling us to abort" or something?
>
> Yup, exactly that. We'd see "Aborting commit as launching the editor
> failed." instead of the above error message.
I see at least two people wondered during the review, so perhaps
# When told to update HEAD, only, the command will refuse to
# rewrite commits that are not an ancestor of HEAD.
- test_must_fail git history reword --ref-action=head theirs 2>err &&
+ # Use the "false" editor that shows a different error when run
+ test_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&
test_grep "rewritten commit must be an ancestor of HEAD" err &&
would help future readers. Or it might be too much.
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH v2 1/5] builtin/history: perform revwalk checks before asking for user input
2026-02-13 9:12 ` [PATCH v2 1/5] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
@ 2026-02-13 17:20 ` Junio C Hamano
0 siblings, 0 replies; 34+ messages in thread
From: Junio C Hamano @ 2026-02-13 17:20 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, D. Ben Knoble
Patrick Steinhardt <ps@pks.im> writes:
> Adapt one of the tests that is expected to fail because of this check
> to use false(1) as editor. If the editor had been executed by Git, it
> would fail with the error message "Aborting commit as launching the
> editor failed."
I very much appreciate this new description. OK, we expect the test
to fail due to the check in question, and ensure that the editor is
not invoked, by looking at the error message and making sure it is
not the abort due to the editor returning false.
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH v2 5/5] Documentation/git-history: document default for "--update-refs="
2026-02-13 9:12 ` [PATCH v2 5/5] Documentation/git-history: document default for "--update-refs=" Patrick Steinhardt
@ 2026-02-13 17:21 ` Junio C Hamano
0 siblings, 0 replies; 34+ messages in thread
From: Junio C Hamano @ 2026-02-13 17:21 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, D. Ben Knoble
Patrick Steinhardt <ps@pks.im> writes:
> While we document the values that can be passed to the "--update-refs="
> option, we don't give the user any hint what the default behaviour is.
> Document it.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> Documentation/git-history.adoc | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
> index 4dbe665ec4..cc019de697 100644
> --- a/Documentation/git-history.adoc
> +++ b/Documentation/git-history.adoc
> @@ -70,7 +70,7 @@ OPTIONS
> Control which references will be updated by the command, if any. With
> `branches`, all local branches that point to commits which are
> descendants of the original commit will be rewritten. With `head`, only
> - the current `HEAD` reference will be rewritten.
> + the current `HEAD` reference will be rewritten. Defaults to `branches`.
Good to see the default clearly described. Thanks.
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH v2 3/5] builtin/history: replace "--ref-action=print" with "--dry-run"
2026-02-13 9:12 ` [PATCH v2 3/5] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
@ 2026-02-13 17:30 ` Kristoffer Haugsbakk
2026-02-16 6:39 ` Patrick Steinhardt
0 siblings, 1 reply; 34+ messages in thread
From: Kristoffer Haugsbakk @ 2026-02-13 17:30 UTC (permalink / raw)
To: Patrick Steinhardt, git; +Cc: Junio C Hamano, D. Ben Knoble
On Fri, Feb 13, 2026, at 10:12, Patrick Steinhardt wrote:
> The git-history(1) command has the ability to perform a dry-run
> that will not end up modifying any references. Instead, we'll only print
> any ref updates that would happen as a consequence of performing the
> operation.
>
> This mode is somewhat hidden though behind the "--ref-action=print"
> option. This command line option has its origin in git-replay(1), where
> it's probably an okayish interface as this command is sitting more on
> the plumbing side of tools. But git-history(1) is a user-facing tool,
> and this way of achieving a dry-run is way too technical and thus not
> very discoverable.
It makes sense to use this command as a dry run and then pass the output
on to git-update-ref(1) for the wet run.
git history --dry-run ... | git update-ref --stdin
Looks good.
>
> Besides usability issues, it also has another issue: the dry-run mode
> will always operate as if the user wanted to rewrite all branches. But
> in fact, the user also has the option to only update the HEAD reference,
> and they might want to perform a dry-run of such an operation, too. We
> could of course introduce "--ref-actoin=print-head", but that would
s/--ref-actoin/--ref-action/
> become even less ergonomic.
>
> Replace "--ref-action=print" with a new "--dry-run" toggle. This new
> toggle works with both "--ref-action={head,branches}" and is way more
> discoverable.
>
> Add a test to verify that both "--ref-action=" values behave as
> expected.
>
> This patch is best viewed with "--ignore-space-change".
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>[snip]
> +`--ref-action=(branches|head)`::
> Control which references will be updated by the command, if any. With
> `branches`, all local branches that point to commits which are
> descendants of the original commit will be rewritten. With `head`, only
> - the current `HEAD` reference will be rewritten. With `print`, all
> - updates as they would be performed with `branches` are printed in a
> - format that can be consumed by linkgit:git-update-ref[1].
> + the current `HEAD` reference will be rewritten.
`HEAD` is mentioned here because it could be detached `HEAD`. So you
can’t just say the current branch.
“the current `HEAD` reference” seems a bit much. Is this less precise?
only `HEAD` is rewritten.
>
> GIT
> ---
>[snip]
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [PATCH v2 3/5] builtin/history: replace "--ref-action=print" with "--dry-run"
2026-02-13 17:30 ` Kristoffer Haugsbakk
@ 2026-02-16 6:39 ` Patrick Steinhardt
2026-02-18 16:09 ` Kristoffer Haugsbakk
0 siblings, 1 reply; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-16 6:39 UTC (permalink / raw)
To: Kristoffer Haugsbakk; +Cc: git, Junio C Hamano, D. Ben Knoble
On Fri, Feb 13, 2026 at 06:30:10PM +0100, Kristoffer Haugsbakk wrote:
[snip]
> > Besides usability issues, it also has another issue: the dry-run mode
> > will always operate as if the user wanted to rewrite all branches. But
> > in fact, the user also has the option to only update the HEAD reference,
> > and they might want to perform a dry-run of such an operation, too. We
> > could of course introduce "--ref-actoin=print-head", but that would
>
> s/--ref-actoin/--ref-action/
Indeed, I've queued this fix locally and will send it out with the next
version.
> >[snip]
> > +`--ref-action=(branches|head)`::
> > Control which references will be updated by the command, if any. With
> > `branches`, all local branches that point to commits which are
> > descendants of the original commit will be rewritten. With `head`, only
> > - the current `HEAD` reference will be rewritten. With `print`, all
> > - updates as they would be performed with `branches` are printed in a
> > - format that can be consumed by linkgit:git-update-ref[1].
> > + the current `HEAD` reference will be rewritten.
>
> `HEAD` is mentioned here because it could be detached `HEAD`. So you
> can’t just say the current branch.
>
> “the current `HEAD` reference” seems a bit much. Is this less precise?
It's not, but this commit doesn't rewrite any of the description, it
only deletes the sentence that becomes out-of-date now. So I'd prefer to
keep this as-is if you don't mind.
If you feel strongly I'm happy to add another commit on top.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 34+ messages in thread
* [PATCH v3 0/5] builtin/history: some smaller UI improvements
2026-02-12 12:44 [PATCH 0/4] builtin/history: some smaller UI improvements Patrick Steinhardt
` (4 preceding siblings ...)
2026-02-13 9:12 ` [PATCH v2 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
@ 2026-02-16 6:45 ` Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 1/5] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
` (4 more replies)
5 siblings, 5 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-16 6:45 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble, Kristoffer Haugsbakk
Hi,
this patch series contains a small set of UI improvements for
git-history(1):
- The first two commits adapt git-history(1) so that it performs
verifications before asking the user for input.
- The last two commits rework the "--ref-action=" option to instead be
split up into "--dry-run" and "--update-refs=" so that the option is
less focussed on technical implementation details.
I decided to send this patch series as a small incremental step before
sending `git history split`, also because that series conflicts with
aa/add-p-no-auto-advance.
Changes in v3:
- Fix a typo in the commit message.
- Link to v2: https://lore.kernel.org/r/20260213-b4-pks-history-dry-run-v2-0-756ac376e9e5@pks.im
Changes in v2:
- Use `strvec_pushv()` instead of looping around `strvec_push()`.
- Document that "--dry-run" writes objects for later use.
- Document the default value of "--update-refs=".
- Mention the subtlety around false(1) in the commit messages.
- Link to v1: https://lore.kernel.org/r/20260212-b4-pks-history-dry-run-v1-0-1ce03d631c1b@pks.im
Thanks!
Patrick
---
Patrick Steinhardt (5):
builtin/history: perform revwalk checks before asking for user input
builtin/history: check for merges before asking for user input
builtin/history: replace "--ref-action=print" with "--dry-run"
builtin/history: rename "--ref-action=" to "--update-refs="
Documentation/git-history: document default for "--update-refs="
Documentation/git-history.adoc | 14 ++-
builtin/history.c | 254 ++++++++++++++++++++++++++---------------
t/t3451-history-reword.sh | 20 +++-
3 files changed, 182 insertions(+), 106 deletions(-)
Range-diff versus v2:
1: 26db5f0113 = 1: 21b4933c39 builtin/history: perform revwalk checks before asking for user input
2: 9bcde0b932 = 2: 37e42cf770 builtin/history: check for merges before asking for user input
3: e070d8ecfa ! 3: fcfd80b2b5 builtin/history: replace "--ref-action=print" with "--dry-run"
@@ Commit message
will always operate as if the user wanted to rewrite all branches. But
in fact, the user also has the option to only update the HEAD reference,
and they might want to perform a dry-run of such an operation, too. We
- could of course introduce "--ref-actoin=print-head", but that would
+ could of course introduce "--ref-action=print-head", but that would
become even less ergonomic.
Replace "--ref-action=print" with a new "--dry-run" toggle. This new
4: 59f470b158 = 4: 8333a4ebd6 builtin/history: rename "--ref-action=" to "--update-refs="
5: bde39d43a9 = 5: 9ef41103f0 Documentation/git-history: document default for "--update-refs="
---
base-commit: 6fcee4785280a08e7f271bd015a4dc33753e2886
change-id: 20260212-b4-pks-history-dry-run-2b840e530ae6
^ permalink raw reply [flat|nested] 34+ messages in thread
* [PATCH v3 1/5] builtin/history: perform revwalk checks before asking for user input
2026-02-16 6:45 ` [PATCH v3 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
@ 2026-02-16 6:45 ` Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 2/5] builtin/history: check for merges " Patrick Steinhardt
` (3 subsequent siblings)
4 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-16 6:45 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble, Kristoffer Haugsbakk
When setting up the revision walk in git-history(1) we also perform some
verifications whether the request actually looks sane. Unfortunately,
these verifications come _after_ we have already asked the user for the
commit message of the commit that is to be rewritten. So in case any of
the verifications fails, the user will have lost their modifications.
Extract the function to set up the revision walk and call it before we
ask for user input to fix this.
Adapt one of the tests that is expected to fail because of this check
to use false(1) as editor. If the editor had been executed by Git, it
would fail with the error message "Aborting commit as launching the
editor failed."
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/history.c | 69 +++++++++++++++++++++++++++++------------------
t/t3451-history-reword.sh | 2 +-
2 files changed, 44 insertions(+), 27 deletions(-)
diff --git a/builtin/history.c b/builtin/history.c
index 8dcb9a6046..1de51372ea 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -177,30 +177,15 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
return 0;
}
-static int handle_reference_updates(enum ref_action action,
- struct repository *repo,
- struct commit *original,
- struct commit *rewritten,
- const char *reflog_msg)
+static int setup_revwalk(struct repository *repo,
+ enum ref_action action,
+ struct commit *original,
+ struct rev_info *revs)
{
- const struct name_decoration *decoration;
- struct replay_revisions_options opts = { 0 };
- struct replay_result result = { 0 };
- struct ref_transaction *transaction = NULL;
struct strvec args = STRVEC_INIT;
- struct strbuf err = STRBUF_INIT;
- struct commit *head = NULL;
- struct rev_info revs;
- char hex[GIT_MAX_HEXSZ + 1];
- bool detached_head;
- int head_flags = 0;
int ret;
- refs_read_ref_full(get_main_ref_store(repo), "HEAD",
- RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
- detached_head = !(head_flags & REF_ISSYMREF);
-
- repo_init_revisions(repo, &revs, NULL);
+ repo_init_revisions(repo, revs, NULL);
strvec_push(&args, "ignored");
strvec_push(&args, "--reverse");
strvec_push(&args, "--topo-order");
@@ -224,6 +209,7 @@ static int handle_reference_updates(enum ref_action action,
*/
if (action == REF_ACTION_HEAD) {
struct commit_list *from_list = NULL;
+ struct commit *head;
head = lookup_commit_reference_by_name("HEAD");
if (!head) {
@@ -250,20 +236,47 @@ static int handle_reference_updates(enum ref_action action,
strvec_push(&args, "HEAD");
}
- setup_revisions_from_strvec(&args, &revs, NULL);
+ setup_revisions_from_strvec(&args, revs, NULL);
if (args.nr != 1)
BUG("revisions were set up with invalid argument");
+ ret = 0;
+
+out:
+ strvec_clear(&args);
+ return ret;
+}
+
+static int handle_reference_updates(struct rev_info *revs,
+ enum ref_action action,
+ struct commit *original,
+ struct commit *rewritten,
+ const char *reflog_msg)
+{
+ const struct name_decoration *decoration;
+ struct replay_revisions_options opts = { 0 };
+ struct replay_result result = { 0 };
+ struct ref_transaction *transaction = NULL;
+ struct strbuf err = STRBUF_INIT;
+ char hex[GIT_MAX_HEXSZ + 1];
+ bool detached_head;
+ int head_flags = 0;
+ int ret;
+
+ refs_read_ref_full(get_main_ref_store(revs->repo), "HEAD",
+ RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
+ detached_head = !(head_flags & REF_ISSYMREF);
+
opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
- ret = replay_revisions(&revs, &opts, &result);
+ ret = replay_revisions(revs, &opts, &result);
if (ret)
goto out;
switch (action) {
case REF_ACTION_BRANCHES:
case REF_ACTION_HEAD:
- transaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);
+ transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
if (!transaction) {
ret = error(_("failed to begin ref transaction: %s"), err.buf);
goto out;
@@ -343,9 +356,7 @@ static int handle_reference_updates(enum ref_action action,
out:
ref_transaction_free(transaction);
replay_result_release(&result);
- release_revisions(&revs);
strbuf_release(&err);
- strvec_clear(&args);
return ret;
}
@@ -367,6 +378,7 @@ static int cmd_history_reword(int argc,
};
struct strbuf reflog_msg = STRBUF_INIT;
struct commit *original, *rewritten;
+ struct rev_info revs;
int ret;
argc = parse_options(argc, argv, prefix, options, usage, 0);
@@ -385,6 +397,10 @@ static int cmd_history_reword(int argc,
goto out;
}
+ ret = setup_revwalk(repo, action, original, &revs);
+ if (ret)
+ goto out;
+
ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
if (ret < 0) {
ret = error(_("failed writing reworded commit"));
@@ -393,7 +409,7 @@ static int cmd_history_reword(int argc,
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
- ret = handle_reference_updates(action, repo, original, rewritten,
+ ret = handle_reference_updates(&revs, action, original, rewritten,
reflog_msg.buf);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
@@ -404,6 +420,7 @@ static int cmd_history_reword(int argc,
out:
strbuf_release(&reflog_msg);
+ release_revisions(&revs);
return ret;
}
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 3594421b68..6775ed62f9 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -263,7 +263,7 @@ test_expect_success '--ref-action=head updates only HEAD' '
# When told to update HEAD, only, the command will refuse to
# rewrite commits that are not an ancestor of HEAD.
- test_must_fail git history reword --ref-action=head theirs 2>err &&
+ test_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&
test_grep "rewritten commit must be an ancestor of HEAD" err &&
reword_with_message --ref-action=head base >updates <<-\EOF &&
--
2.53.0.352.gd1286b26eb.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH v3 2/5] builtin/history: check for merges before asking for user input
2026-02-16 6:45 ` [PATCH v3 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 1/5] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
@ 2026-02-16 6:45 ` Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 3/5] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
` (2 subsequent siblings)
4 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-16 6:45 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble, Kristoffer Haugsbakk
The replay infrastructure is not yet capable of replaying merge commits.
Unfortunately, we only notice that we're about to replay merges after we
have already asked the user for input, so any commit message that the
user may have written will be discarded in that case.
Fix this by checking whether the revwalk contains merge commits before
we ask for user input.
Adapt one of the tests that is expected to fail because of this check
to use false(1) as editor. If the editor had been executed by Git, it
would fail with the error message "Aborting commit as launching the
editor failed."
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/history.c | 39 +++++++++++++++++++++++++++++++++++++++
t/t3451-history-reword.sh | 2 +-
2 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/builtin/history.c b/builtin/history.c
index 1de51372ea..ff90e93d6e 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -177,6 +177,41 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
return 0;
}
+static int revwalk_contains_merges(struct repository *repo,
+ const struct strvec *revwalk_args)
+{
+ struct strvec args = STRVEC_INIT;
+ struct rev_info revs;
+ int ret;
+
+ strvec_pushv(&args, revwalk_args->v);
+ strvec_push(&args, "--min-parents=2");
+
+ repo_init_revisions(repo, &revs, NULL);
+
+ setup_revisions_from_strvec(&args, &revs, NULL);
+ if (args.nr != 1)
+ BUG("revisions were set up with invalid argument");
+
+ if (prepare_revision_walk(&revs) < 0) {
+ ret = error(_("error preparing revisions"));
+ goto out;
+ }
+
+ if (get_revision(&revs)) {
+ ret = error(_("replaying merge commits is not supported yet!"));
+ goto out;
+ }
+
+ reset_revision_walk();
+ ret = 0;
+
+out:
+ release_revisions(&revs);
+ strvec_clear(&args);
+ return ret;
+}
+
static int setup_revwalk(struct repository *repo,
enum ref_action action,
struct commit *original,
@@ -236,6 +271,10 @@ static int setup_revwalk(struct repository *repo,
strvec_push(&args, "HEAD");
}
+ ret = revwalk_contains_merges(repo, &args);
+ if (ret < 0)
+ goto out;
+
setup_revisions_from_strvec(&args, revs, NULL);
if (args.nr != 1)
BUG("revisions were set up with invalid argument");
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 6775ed62f9..12a9a7d051 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -203,7 +203,7 @@ test_expect_success 'can reword a merge commit' '
# It is not possible to replay merge commits embedded in the
# history (yet).
- test_must_fail git history reword HEAD~ 2>err &&
+ test_must_fail git -c core.editor=false history reword HEAD~ 2>err &&
test_grep "replaying merge commits is not supported yet" err &&
# But it is possible to reword a merge commit directly.
--
2.53.0.352.gd1286b26eb.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH v3 3/5] builtin/history: replace "--ref-action=print" with "--dry-run"
2026-02-16 6:45 ` [PATCH v3 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 1/5] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 2/5] builtin/history: check for merges " Patrick Steinhardt
@ 2026-02-16 6:45 ` Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 4/5] builtin/history: rename "--ref-action=" to "--update-refs=" Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 5/5] Documentation/git-history: document default for "--update-refs=" Patrick Steinhardt
4 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-16 6:45 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble, Kristoffer Haugsbakk
The git-history(1) command has the ability to perform a dry-run
that will not end up modifying any references. Instead, we'll only print
any ref updates that would happen as a consequence of performing the
operation.
This mode is somewhat hidden though behind the "--ref-action=print"
option. This command line option has its origin in git-replay(1), where
it's probably an okayish interface as this command is sitting more on
the plumbing side of tools. But git-history(1) is a user-facing tool,
and this way of achieving a dry-run is way too technical and thus not
very discoverable.
Besides usability issues, it also has another issue: the dry-run mode
will always operate as if the user wanted to rewrite all branches. But
in fact, the user also has the option to only update the HEAD reference,
and they might want to perform a dry-run of such an operation, too. We
could of course introduce "--ref-action=print-head", but that would
become even less ergonomic.
Replace "--ref-action=print" with a new "--dry-run" toggle. This new
toggle works with both "--ref-action={head,branches}" and is way more
discoverable.
Add a test to verify that both "--ref-action=" values behave as
expected.
This patch is best viewed with "--ignore-space-change".
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 14 ++--
builtin/history.c | 150 ++++++++++++++++++++++-------------------
t/t3451-history-reword.sh | 12 +++-
3 files changed, 98 insertions(+), 78 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 154e262b76..df2900ac2f 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
SYNOPSIS
--------
[synopsis]
-git history reword <commit> [--ref-action=(branches|head|print)]
+git history reword <commit> [--dry-run] [--ref-action=(branches|head)]
DESCRIPTION
-----------
@@ -60,13 +60,17 @@ The following commands are available to rewrite history in different ways:
OPTIONS
-------
-`--ref-action=(branches|head|print)`::
+`--dry-run`::
+ Do not update any references, but instead print any ref updates in a
+ format that can be consumed by linkgit:git-update-ref[1]. Necessary new
+ objects will be written into the repository, so applying these printed
+ ref updates is generally safe.
+
+`--ref-action=(branches|head)`::
Control which references will be updated by the command, if any. With
`branches`, all local branches that point to commits which are
descendants of the original commit will be rewritten. With `head`, only
- the current `HEAD` reference will be rewritten. With `print`, all
- updates as they would be performed with `branches` are printed in a
- format that can be consumed by linkgit:git-update-ref[1].
+ the current `HEAD` reference will be rewritten.
GIT
---
diff --git a/builtin/history.c b/builtin/history.c
index ff90e93d6e..c135361c67 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -18,7 +18,7 @@
#include "wt-status.h"
#define GIT_HISTORY_REWORD_USAGE \
- N_("git history reword <commit> [--ref-action=(branches|head|print)]")
+ N_("git history reword <commit> [--dry-run] [--ref-action=(branches|head)]")
static void change_data_free(void *util, const char *str UNUSED)
{
@@ -155,7 +155,6 @@ enum ref_action {
REF_ACTION_DEFAULT,
REF_ACTION_BRANCHES,
REF_ACTION_HEAD,
- REF_ACTION_PRINT,
};
static int parse_ref_action(const struct option *opt, const char *value, int unset)
@@ -167,10 +166,8 @@ static int parse_ref_action(const struct option *opt, const char *value, int uns
*action = REF_ACTION_BRANCHES;
} else if (!strcmp(value, "head")) {
*action = REF_ACTION_HEAD;
- } else if (!strcmp(value, "print")) {
- *action = REF_ACTION_PRINT;
} else {
- return error(_("%s expects one of 'branches', 'head' or 'print'"),
+ return error(_("%s expects one of 'branches' or 'head'"),
opt->long_name);
}
@@ -286,11 +283,29 @@ static int setup_revwalk(struct repository *repo,
return ret;
}
+static int handle_ref_update(struct ref_transaction *transaction,
+ const char *refname,
+ const struct object_id *new_oid,
+ const struct object_id *old_oid,
+ const char *reflog_msg,
+ struct strbuf *err)
+{
+ if (!transaction) {
+ printf("update %s %s %s\n",
+ refname, oid_to_hex(new_oid), oid_to_hex(old_oid));
+ return 0;
+ }
+
+ return ref_transaction_update(transaction, refname, new_oid, old_oid,
+ NULL, NULL, 0, reflog_msg, err);
+}
+
static int handle_reference_updates(struct rev_info *revs,
enum ref_action action,
struct commit *original,
struct commit *rewritten,
- const char *reflog_msg)
+ const char *reflog_msg,
+ int dry_run)
{
const struct name_decoration *decoration;
struct replay_revisions_options opts = { 0 };
@@ -312,82 +327,72 @@ static int handle_reference_updates(struct rev_info *revs,
if (ret)
goto out;
- switch (action) {
- case REF_ACTION_BRANCHES:
- case REF_ACTION_HEAD:
+ if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
+ BUG("unsupported ref action %d", action);
+
+ if (!dry_run) {
transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
if (!transaction) {
ret = error(_("failed to begin ref transaction: %s"), err.buf);
goto out;
}
+ }
- for (size_t i = 0; i < result.updates_nr; i++) {
- ret = ref_transaction_update(transaction,
- result.updates[i].refname,
- &result.updates[i].new_oid,
- &result.updates[i].old_oid,
- NULL, NULL, 0, reflog_msg, &err);
- if (ret) {
- ret = error(_("failed to update ref '%s': %s"),
- result.updates[i].refname, err.buf);
- goto out;
- }
+ for (size_t i = 0; i < result.updates_nr; i++) {
+ ret = handle_ref_update(transaction,
+ result.updates[i].refname,
+ &result.updates[i].new_oid,
+ &result.updates[i].old_oid,
+ reflog_msg, &err);
+ if (ret) {
+ ret = error(_("failed to update ref '%s': %s"),
+ result.updates[i].refname, err.buf);
+ goto out;
}
+ }
+
+ /*
+ * `replay_revisions()` only updates references that are
+ * ancestors of `rewritten`, so we need to manually
+ * handle updating references that point to `original`.
+ */
+ for (decoration = get_name_decoration(&original->object);
+ decoration;
+ decoration = decoration->next)
+ {
+ if (decoration->type != DECORATION_REF_LOCAL &&
+ decoration->type != DECORATION_REF_HEAD)
+ continue;
+
+ if (action == REF_ACTION_HEAD &&
+ decoration->type != DECORATION_REF_HEAD)
+ continue;
/*
- * `replay_revisions()` only updates references that are
- * ancestors of `rewritten`, so we need to manually
- * handle updating references that point to `original`.
+ * We only need to update HEAD separately in case it's
+ * detached. If it's not we'd already update the branch
+ * it is pointing to.
*/
- for (decoration = get_name_decoration(&original->object);
- decoration;
- decoration = decoration->next)
- {
- if (decoration->type != DECORATION_REF_LOCAL &&
- decoration->type != DECORATION_REF_HEAD)
- continue;
-
- if (action == REF_ACTION_HEAD &&
- decoration->type != DECORATION_REF_HEAD)
- continue;
-
- /*
- * We only need to update HEAD separately in case it's
- * detached. If it's not we'd already update the branch
- * it is pointing to.
- */
- if (action == REF_ACTION_BRANCHES &&
- decoration->type == DECORATION_REF_HEAD &&
- !detached_head)
- continue;
-
- ret = ref_transaction_update(transaction,
- decoration->name,
- &rewritten->object.oid,
- &original->object.oid,
- NULL, NULL, 0, reflog_msg, &err);
- if (ret) {
- ret = error(_("failed to update ref '%s': %s"),
- decoration->name, err.buf);
- goto out;
- }
- }
-
- if (ref_transaction_commit(transaction, &err)) {
- ret = error(_("failed to commit ref transaction: %s"), err.buf);
+ if (action == REF_ACTION_BRANCHES &&
+ decoration->type == DECORATION_REF_HEAD &&
+ !detached_head)
+ continue;
+
+ ret = handle_ref_update(transaction,
+ decoration->name,
+ &rewritten->object.oid,
+ &original->object.oid,
+ reflog_msg, &err);
+ if (ret) {
+ ret = error(_("failed to update ref '%s': %s"),
+ decoration->name, err.buf);
goto out;
}
+ }
- break;
- case REF_ACTION_PRINT:
- for (size_t i = 0; i < result.updates_nr; i++)
- printf("update %s %s %s\n",
- result.updates[i].refname,
- oid_to_hex(&result.updates[i].new_oid),
- oid_to_hex(&result.updates[i].old_oid));
- break;
- default:
- BUG("unsupported ref action %d", action);
+ if (transaction && ref_transaction_commit(transaction, &err)) {
+ ret = error(_("failed to commit ref transaction: %s"), err.buf);
+ goto out;
}
ret = 0;
@@ -409,10 +414,13 @@ static int cmd_history_reword(int argc,
NULL,
};
enum ref_action action = REF_ACTION_DEFAULT;
+ int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
- N_("control ref update behavior (branches|head|print)"),
+ N_("control ref update behavior (branches|head)"),
PARSE_OPT_NONEG, parse_ref_action),
+ OPT_BOOL('n', "dry-run", &dry_run,
+ N_("perform a dry-run without updating any refs")),
OPT_END(),
};
struct strbuf reflog_msg = STRBUF_INIT;
@@ -449,7 +457,7 @@ static int cmd_history_reword(int argc,
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf);
+ reflog_msg.buf, dry_run);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 12a9a7d051..702d40dc06 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -221,7 +221,7 @@ test_expect_success 'can reword a merge commit' '
)
'
-test_expect_success '--ref-action=print prints ref updates without modifying repo' '
+test_expect_success '--dry-run prints ref updates without modifying repo' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
@@ -233,7 +233,15 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep
test_commit theirs &&
git refs list >refs-expect &&
- reword_with_message --ref-action=print base >updates <<-\EOF &&
+ reword_with_message --dry-run --ref-action=head base >updates <<-\EOF &&
+ reworded commit
+ EOF
+ git refs list >refs-actual &&
+ test_cmp refs-expect refs-actual &&
+ test_grep "update refs/heads/branch" updates &&
+ test_grep ! "update refs/heads/main" updates &&
+
+ reword_with_message --dry-run base >updates <<-\EOF &&
reworded commit
EOF
git refs list >refs-actual &&
--
2.53.0.352.gd1286b26eb.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH v3 4/5] builtin/history: rename "--ref-action=" to "--update-refs="
2026-02-16 6:45 ` [PATCH v3 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
` (2 preceding siblings ...)
2026-02-16 6:45 ` [PATCH v3 3/5] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
@ 2026-02-16 6:45 ` Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 5/5] Documentation/git-history: document default for "--update-refs=" Patrick Steinhardt
4 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-16 6:45 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble, Kristoffer Haugsbakk
With the preceding commit we have changed "--ref-action=" to only
control which refs are supposed to be updated, not what happens with
them. As a consequence, the option is now somewhat misnamed, as we don't
control the action itself anymore.
Rename it to "--update-refs=" to better align it with its new use.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 4 ++--
builtin/history.c | 8 ++++----
t/t3451-history-reword.sh | 8 ++++----
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index df2900ac2f..4dbe665ec4 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
SYNOPSIS
--------
[synopsis]
-git history reword <commit> [--dry-run] [--ref-action=(branches|head)]
+git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
DESCRIPTION
-----------
@@ -66,7 +66,7 @@ OPTIONS
objects will be written into the repository, so applying these printed
ref updates is generally safe.
-`--ref-action=(branches|head)`::
+`--update-refs=(branches|head)`::
Control which references will be updated by the command, if any. With
`branches`, all local branches that point to commits which are
descendants of the original commit will be rewritten. With `head`, only
diff --git a/builtin/history.c b/builtin/history.c
index c135361c67..1cf6c668cf 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -18,7 +18,7 @@
#include "wt-status.h"
#define GIT_HISTORY_REWORD_USAGE \
- N_("git history reword <commit> [--dry-run] [--ref-action=(branches|head)]")
+ N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
static void change_data_free(void *util, const char *str UNUSED)
{
@@ -258,7 +258,7 @@ static int setup_revwalk(struct repository *repo,
goto out;
} else if (!ret) {
ret = error(_("rewritten commit must be an ancestor "
- "of HEAD when using --ref-action=head"));
+ "of HEAD when using --update-refs=head"));
goto out;
}
@@ -416,8 +416,8 @@ static int cmd_history_reword(int argc,
enum ref_action action = REF_ACTION_DEFAULT;
int dry_run = 0;
struct option options[] = {
- OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
- N_("control ref update behavior (branches|head)"),
+ OPT_CALLBACK_F(0, "update-refs", &action, N_("<action>"),
+ N_("control which refs should be updated (branches|head)"),
PARSE_OPT_NONEG, parse_ref_action),
OPT_BOOL('n', "dry-run", &dry_run,
N_("perform a dry-run without updating any refs")),
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index 702d40dc06..de7b357685 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -233,7 +233,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '
test_commit theirs &&
git refs list >refs-expect &&
- reword_with_message --dry-run --ref-action=head base >updates <<-\EOF &&
+ reword_with_message --dry-run --update-refs=head base >updates <<-\EOF &&
reworded commit
EOF
git refs list >refs-actual &&
@@ -258,7 +258,7 @@ test_expect_success '--dry-run prints ref updates without modifying repo' '
)
'
-test_expect_success '--ref-action=head updates only HEAD' '
+test_expect_success '--update-refs=head updates only HEAD' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
@@ -271,10 +271,10 @@ test_expect_success '--ref-action=head updates only HEAD' '
# When told to update HEAD, only, the command will refuse to
# rewrite commits that are not an ancestor of HEAD.
- test_must_fail git -c core.editor=false history reword --ref-action=head theirs 2>err &&
+ test_must_fail git -c core.editor=false history reword --update-refs=head theirs 2>err &&
test_grep "rewritten commit must be an ancestor of HEAD" err &&
- reword_with_message --ref-action=head base >updates <<-\EOF &&
+ reword_with_message --update-refs=head base >updates <<-\EOF &&
reworded base
EOF
expect_log HEAD <<-\EOF &&
--
2.53.0.352.gd1286b26eb.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* [PATCH v3 5/5] Documentation/git-history: document default for "--update-refs="
2026-02-16 6:45 ` [PATCH v3 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
` (3 preceding siblings ...)
2026-02-16 6:45 ` [PATCH v3 4/5] builtin/history: rename "--ref-action=" to "--update-refs=" Patrick Steinhardt
@ 2026-02-16 6:45 ` Patrick Steinhardt
4 siblings, 0 replies; 34+ messages in thread
From: Patrick Steinhardt @ 2026-02-16 6:45 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, D. Ben Knoble, Kristoffer Haugsbakk
While we document the values that can be passed to the "--update-refs="
option, we don't give the user any hint what the default behaviour is.
Document it.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
Documentation/git-history.adoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 4dbe665ec4..cc019de697 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -70,7 +70,7 @@ OPTIONS
Control which references will be updated by the command, if any. With
`branches`, all local branches that point to commits which are
descendants of the original commit will be rewritten. With `head`, only
- the current `HEAD` reference will be rewritten.
+ the current `HEAD` reference will be rewritten. Defaults to `branches`.
GIT
---
--
2.53.0.352.gd1286b26eb.dirty
^ permalink raw reply related [flat|nested] 34+ messages in thread
* Re: [PATCH v2 3/5] builtin/history: replace "--ref-action=print" with "--dry-run"
2026-02-16 6:39 ` Patrick Steinhardt
@ 2026-02-18 16:09 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 34+ messages in thread
From: Kristoffer Haugsbakk @ 2026-02-18 16:09 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, Junio C Hamano, D. Ben Knoble
On Mon, Feb 16, 2026, at 07:39, Patrick Steinhardt wrote:
>[snip]
>> `HEAD` is mentioned here because it could be detached `HEAD`. So you
>> can’t just say the current branch.
>>
>> “the current `HEAD` reference” seems a bit much. Is this less precise?
>
> It's not, but this commit doesn't rewrite any of the description, it
> only deletes the sentence that becomes out-of-date now.
I thought I would unceremoniously bring it up since you are the
only/sole author of this document. ;)
> So I'd prefer to keep this as-is if you don't mind.
Yeah that’s okay. :)
>
> If you feel strongly I'm happy to add another commit on top.
^ permalink raw reply [flat|nested] 34+ messages in thread
end of thread, other threads:[~2026-02-18 16:10 UTC | newest]
Thread overview: 34+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-12 12:44 [PATCH 0/4] builtin/history: some smaller UI improvements Patrick Steinhardt
2026-02-12 12:44 ` [PATCH 1/4] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
2026-02-12 20:04 ` Junio C Hamano
2026-02-13 5:51 ` Patrick Steinhardt
2026-02-13 17:02 ` Junio C Hamano
2026-02-12 12:44 ` [PATCH 2/4] builtin/history: check for merges " Patrick Steinhardt
2026-02-12 22:20 ` D. Ben Knoble
2026-02-12 22:26 ` Junio C Hamano
2026-02-13 5:51 ` Patrick Steinhardt
2026-02-13 5:51 ` Patrick Steinhardt
2026-02-13 13:42 ` Ben Knoble
2026-02-12 12:44 ` [PATCH 3/4] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
2026-02-12 20:19 ` Junio C Hamano
2026-02-13 5:50 ` Patrick Steinhardt
2026-02-12 22:20 ` D. Ben Knoble
2026-02-13 5:50 ` Patrick Steinhardt
2026-02-12 12:44 ` [PATCH 4/4] builtin/history: rename "--ref-action=" to "--update-refs=" Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 1/5] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
2026-02-13 17:20 ` Junio C Hamano
2026-02-13 9:12 ` [PATCH v2 2/5] builtin/history: check for merges " Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 3/5] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
2026-02-13 17:30 ` Kristoffer Haugsbakk
2026-02-16 6:39 ` Patrick Steinhardt
2026-02-18 16:09 ` Kristoffer Haugsbakk
2026-02-13 9:12 ` [PATCH v2 4/5] builtin/history: rename "--ref-action=" to "--update-refs=" Patrick Steinhardt
2026-02-13 9:12 ` [PATCH v2 5/5] Documentation/git-history: document default for "--update-refs=" Patrick Steinhardt
2026-02-13 17:21 ` Junio C Hamano
2026-02-16 6:45 ` [PATCH v3 0/5] builtin/history: some smaller UI improvements Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 1/5] builtin/history: perform revwalk checks before asking for user input Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 2/5] builtin/history: check for merges " Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 3/5] builtin/history: replace "--ref-action=print" with "--dry-run" Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 4/5] builtin/history: rename "--ref-action=" to "--update-refs=" Patrick Steinhardt
2026-02-16 6:45 ` [PATCH v3 5/5] Documentation/git-history: document default for "--update-refs=" Patrick Steinhardt
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox