aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2026-02-27 15:11:50 -0800
committerJunio C Hamano <gitster@pobox.com>2026-02-27 15:11:50 -0800
commitbb9c781f4f4be2e6bf6285149ba7007fdaa735e7 (patch)
tree529e932891733333ad088881a46948f9e87c85aa
parentaa95f87c740011f7d21555c5ad7f0870faf4b5c8 (diff)
parent1278a26544e81dddf564fd7730890a7e023ed367 (diff)
downloadgit-bb9c781f4f4be2e6bf6285149ba7007fdaa735e7.tar.xz
Merge branch 'ps/history-ergonomics-updates'
UI improvements for "git history reword". * ps/history-ergonomics-updates: Documentation/git-history: document default for "--update-refs=" builtin/history: rename "--ref-action=" to "--update-refs=" builtin/history: replace "--ref-action=print" with "--dry-run" builtin/history: check for merges before asking for user input builtin/history: perform revwalk checks before asking for user input
-rw-r--r--Documentation/git-history.adoc14
-rw-r--r--builtin/history.c252
-rwxr-xr-xt/t3451-history-reword.sh20
3 files changed, 181 insertions, 105 deletions
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 154e262b76..cc019de697 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] [--update-refs=(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.
+
+`--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
- 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. Defaults to `branches`.
GIT
---
diff --git a/builtin/history.c b/builtin/history.c
index 8dcb9a6046..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> [--ref-action=(branches|head|print)]")
+ N_("git history reword <commit> [--dry-run] [--update-refs=(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,40 +166,58 @@ 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);
}
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 revwalk_contains_merges(struct repository *repo,
+ const struct strvec *revwalk_args)
{
- 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);
+ 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,
+ struct rev_info *revs)
+{
+ struct strvec args = STRVEC_INIT;
+ int ret;
+
+ repo_init_revisions(repo, revs, NULL);
strvec_push(&args, "ignored");
strvec_push(&args, "--reverse");
strvec_push(&args, "--topo-order");
@@ -224,6 +241,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) {
@@ -240,7 +258,7 @@ static int handle_reference_updates(enum ref_action action,
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;
}
@@ -250,92 +268,131 @@ static int handle_reference_updates(enum ref_action action,
strvec_push(&args, "HEAD");
}
- setup_revisions_from_strvec(&args, &revs, NULL);
+ 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");
+ ret = 0;
+
+out:
+ strvec_clear(&args);
+ 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,
+ int dry_run)
+{
+ 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);
+ 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`.
+ */
+ for (decoration = get_name_decoration(&original->object);
+ decoration;
+ decoration = decoration->next)
+ {
+ if (decoration->type != DECORATION_REF_LOCAL &&
+ 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;
+ if (action == REF_ACTION_HEAD &&
+ decoration->type != DECORATION_REF_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;
- }
- }
+ /*
+ * 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;
- if (ref_transaction_commit(transaction, &err)) {
- ret = error(_("failed to commit ref transaction: %s"), err.buf);
+ 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;
@@ -343,9 +400,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;
}
@@ -359,14 +414,18 @@ 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)"),
+ 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")),
OPT_END(),
};
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 +444,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,8 +456,8 @@ static int cmd_history_reword(int argc,
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
- ret = handle_reference_updates(action, repo, original, rewritten,
- reflog_msg.buf);
+ ret = handle_reference_updates(&revs, action, original, rewritten,
+ reflog_msg.buf, dry_run);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
@@ -404,6 +467,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..de7b357685 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.
@@ -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 --update-refs=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 &&
@@ -250,7 +258,7 @@ test_expect_success '--ref-action=print prints ref updates without modifying rep
)
'
-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 &&
(
@@ -263,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 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 &&