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. The combined message is built before the
descendant walk so it is not disturbed by the flags that walk leaves on
the commits.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Harald Nordgren
2026-06-18 19:17:06 +00:00
committed by Junio C Hamano
parent 08ae39413a
commit 4fcf3481b2
3 changed files with 100 additions and 3 deletions
+3 -2
View File
@@ -111,8 +111,9 @@ history squash @~3..` folds the three most recent commits into one, and
`git history squash @~5..@~2` squashes an interior range while leaving
the two newest commits in place.
+
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.
+60 -1
View File
@@ -1047,6 +1047,56 @@ out:
return ret;
}
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,
@@ -1071,6 +1121,7 @@ static int cmd_history_squash(int argc,
OPT_END(),
};
struct strbuf reflog_msg = STRBUF_INIT;
struct strbuf message = STRBUF_INIT;
struct commit *base, *oldest, *tip, *rewritten;
const struct object_id *base_tree_oid, *tip_tree_oid;
struct commit_list *parents = NULL;
@@ -1091,6 +1142,12 @@ static int cmd_history_squash(int argc,
if (ret < 0)
goto out;
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;
@@ -1099,7 +1156,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"));
@@ -1120,6 +1178,7 @@ static int cmd_history_squash(int argc,
out:
strbuf_release(&reflog_msg);
strbuf_release(&message);
commit_list_free(parents);
release_revisions(&revs);
return ret;
+37
View File
@@ -135,6 +135,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 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.. &&
grep "re-one subject" buffer &&
grep "re-one body line" buffer &&
grep re-two buffer &&
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) &&