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 <haraldnordgren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
seen
Harald Nordgren 2026-06-28 08:29:09 +00:00 committed by Junio C Hamano
parent eb5cd8dca6
commit 9958841876
3 changed files with 100 additions and 3 deletions

View File

@ -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.

View File

@ -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);

View File

@ -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) &&