From 9958841876dfd1f06dc47d7c75c4c2ee3940b460 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Sun, 28 Jun 2026 08:29:09 +0000 Subject: [PATCH] history: re-edit a squash with every message By default "git history squash" reuses the oldest commit's message. When --reedit-message is given it only reopened that one message, so the messages of the folded-in commits were lost. Gather the messages of every commit in the range, oldest first, and use them as the editor template when re-editing, mirroring how "git rebase -i" presents a squash. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/git-history.adoc | 5 +-- builtin/history.c | 61 +++++++++++++++++++++++++++++++++- t/t3455-history-squash.sh | 37 +++++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc index 123ad5d4bc..8d4398ab1b 100644 --- a/Documentation/git-history.adoc +++ b/Documentation/git-history.adoc @@ -114,8 +114,9 @@ arguments to linkgit:git-rev-list[1], so several arguments may be given, for example `@~3.. ^topic` to additionally exclude what is already on `topic`. + -The oldest commit's message and authorship are preserved by default, -unless you specify `--reedit-message`. A merge commit inside the range is +The oldest commit's message and authorship are preserved by default. With +`--reedit-message`, an editor opens pre-filled with the messages of all the +folded commits so you can combine them. A merge commit inside the range is folded like any other, but the range must have a single base, so a range that reaches more than one entry point (for example a side branch that forked before the range and was later merged into it) is rejected. diff --git a/builtin/history.c b/builtin/history.c index 5a1b42c063..1c31ea9118 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -1097,6 +1097,56 @@ static int find_interior_ref(const struct reference *ref, void *cb_data) return 0; } +static int build_squash_message(struct repository *repo, + struct commit *base, + struct commit *tip, + struct strbuf *out) +{ + struct rev_info revs; + struct commit *commit; + struct strvec args = STRVEC_INIT; + int n = 0, ret; + + repo_init_revisions(repo, &revs, NULL); + strvec_push(&args, "ignored"); + strvec_push(&args, "--reverse"); + strvec_push(&args, "--topo-order"); + strvec_pushf(&args, "%s..%s", oid_to_hex(&base->object.oid), + oid_to_hex(&tip->object.oid)); + setup_revisions_from_strvec(&args, &revs, NULL); + + if (prepare_revision_walk(&revs) < 0) { + ret = error(_("error preparing revisions")); + goto out; + } + + while ((commit = get_revision(&revs))) { + const char *message, *body; + struct strbuf one = STRBUF_INIT; + + message = repo_logmsg_reencode(repo, commit, NULL, NULL); + find_commit_subject(message, &body); + strbuf_addstr(&one, body); + strbuf_trim_trailing_newline(&one); + + if (n++) + strbuf_addch(out, '\n'); + strbuf_addbuf(out, &one); + strbuf_addch(out, '\n'); + + strbuf_release(&one); + repo_unuse_commit_buffer(repo, commit, message); + } + + ret = 0; + +out: + reset_revision_walk(); + release_revisions(&revs); + strvec_clear(&args); + return ret; +} + static int cmd_history_squash(int argc, const char **argv, const char *prefix, @@ -1121,6 +1171,7 @@ static int cmd_history_squash(int argc, OPT_END(), }; struct strbuf reflog_msg = STRBUF_INIT; + struct strbuf message = STRBUF_INIT; struct oidset interior = OIDSET_INIT; struct commit *base, *oldest, *tip, *rewritten; const struct object_id *base_tree_oid, *tip_tree_oid; @@ -1160,6 +1211,12 @@ static int cmd_history_squash(int argc, } } + if (flags & COMMIT_TREE_EDIT_MESSAGE) { + ret = build_squash_message(repo, base, tip, &message); + if (ret < 0) + goto out; + } + ret = setup_revwalk(repo, action, tip, &revs); if (ret < 0) goto out; @@ -1168,7 +1225,8 @@ static int cmd_history_squash(int argc, tip_tree_oid = &repo_get_commit_tree(repo, tip)->object.oid; commit_list_append(base, &parents); - ret = commit_tree_ext(repo, "squash", oldest, NULL, parents, + ret = commit_tree_ext(repo, "squash", oldest, + message.len ? message.buf : NULL, parents, base_tree_oid, tip_tree_oid, &rewritten, flags); if (ret < 0) { ret = error(_("failed writing squashed commit")); @@ -1189,6 +1247,7 @@ static int cmd_history_squash(int argc, out: strbuf_release(&reflog_msg); + strbuf_release(&message); oidset_clear(&interior); commit_list_free(parents); release_revisions(&revs); diff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh index 94ee54eb24..5ef6768826 100755 --- a/t/t3455-history-squash.sh +++ b/t/t3455-history-squash.sh @@ -164,6 +164,43 @@ test_expect_success 'preserves authorship of the oldest commit' ' test_cmp expect actual ' +test_expect_success '--reedit-message offers every folded-in message' ' + git reset --hard start && + echo b >file && + git add file && + git commit -m "re-one subject" -m "re-one body line" && + test_commit --no-tag re-two file c && + test_commit re-three file d && + + write_script editor <<-\EOF && + cp "$1" buffer && + echo combined >"$1" + EOF + test_set_editor "$(pwd)/editor" && + git history squash --reedit-message start.. && + + test_grep "re-one subject" buffer && + test_grep "re-one body line" buffer && + test_grep re-two buffer && + test_grep re-three buffer && + git log --format="%s" -1 >actual && + echo combined >expect && + test_cmp expect actual +' + +test_expect_success '--reedit-message aborts on an empty message' ' + git reset --hard three && + head_before=$(git rev-parse HEAD) && + + write_script editor <<-\EOF && + >"$1" + EOF + test_set_editor "$(pwd)/editor" && + test_must_fail git history squash --reedit-message start.. && + + test_cmp_rev "$head_before" HEAD +' + test_expect_success '--dry-run predicts the rewrite without performing it' ' git reset --hard three && head_before=$(git rev-parse HEAD) &&