Skip to content

Commit d4492c9

Browse files
HaraldNordgrengitster
authored andcommitted
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 build the same editor template that "git rebase -i" shows for a squash, using add_squash_combination_header(), add_squash_message_header() and squash_subject_comment_len(). Only the message text differs, the changes are always folded in. Following autosquash, a fixup!'s message is commented out in full under a "will be skipped" header, while a squash! or amend! keeps its body with only the marker subject commented. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent a505da0 commit d4492c9

3 files changed

Lines changed: 196 additions & 4 deletions

File tree

Documentation/git-history.adoc

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,21 @@ like the arguments to linkgit:git-rev-list[1], so several arguments may be
117117
given, for example `HEAD~3..HEAD ^topic` to additionally exclude what is
118118
already on `topic`.
119119
+
120-
The oldest commit's message and authorship are preserved by default,
121-
unless you specify `--reedit-message`. A merge commit inside the range is
120+
The oldest commit's message and authorship are preserved by default. With
121+
`--reedit-message`, an editor opens pre-filled with the messages of all the
122+
folded commits so you can combine them. A merge commit inside the range is
122123
folded like any other, but the range must have a single base, so a range
123124
that reaches more than one entry point (for example a side branch that
124125
forked before the range and was later merged into it) is rejected.
125126
+
126127
Because the oldest commit's message is reused, the range may not begin
127128
with a `fixup!`, `squash!`, or `amend!` commit, whose target is
128-
necessarily outside the range.
129+
necessarily outside the range. The changes from every commit in the range
130+
are always folded in. Only the message text differs. With
131+
`--reedit-message` the template mirrors `git rebase -i`: the message of a
132+
`fixup!` elsewhere in the range is commented out in full, while a
133+
`squash!` or `amend!` keeps its message body with only the marker subject
134+
commented, so you can fold the remark into the result.
129135
+
130136
A branch or tag that points at a commit inside the range would be left
131137
dangling once those commits are folded away, so with the default

builtin/history.c

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,68 @@ static int find_interior_ref(const struct reference *ref, void *cb_data)
11141114
return 0;
11151115
}
11161116

1117+
static int build_squash_message(struct repository *repo,
1118+
struct commit *base,
1119+
struct commit *tip,
1120+
struct strbuf *out)
1121+
{
1122+
struct commit_list *commits = NULL, **tail = &commits, *c;
1123+
struct rev_info revs;
1124+
struct commit *commit;
1125+
struct strvec args = STRVEC_INIT;
1126+
int n = 0, total, ret;
1127+
1128+
repo_init_revisions(repo, &revs, NULL);
1129+
strvec_push(&args, "ignored");
1130+
strvec_push(&args, "--reverse");
1131+
strvec_push(&args, "--topo-order");
1132+
strvec_pushf(&args, "%s..%s", oid_to_hex(&base->object.oid),
1133+
oid_to_hex(&tip->object.oid));
1134+
setup_revisions_from_strvec(&args, &revs, NULL);
1135+
1136+
if (prepare_revision_walk(&revs) < 0) {
1137+
ret = error(_("error preparing revisions"));
1138+
goto out;
1139+
}
1140+
1141+
while ((commit = get_revision(&revs)))
1142+
tail = &commit_list_insert(commit, tail)->next;
1143+
total = commit_list_count(commits);
1144+
1145+
for (c = commits; c; c = c->next) {
1146+
const char *message, *body;
1147+
size_t commented_len;
1148+
int skip;
1149+
1150+
message = repo_logmsg_reencode(repo, c->item, NULL, NULL);
1151+
find_commit_subject(message, &body);
1152+
1153+
skip = starts_with(body, "fixup! ");
1154+
commented_len = skip ? strlen(body) :
1155+
squash_subject_comment_len(body, 1);
1156+
1157+
if (!n)
1158+
add_squash_combination_header(out, total);
1159+
strbuf_addch(out, '\n');
1160+
add_squash_message_header(out, ++n, skip);
1161+
strbuf_addstr(out, "\n\n");
1162+
strbuf_add_commented_lines(out, body, commented_len, comment_line_str);
1163+
strbuf_addstr(out, body + commented_len);
1164+
strbuf_complete_line(out);
1165+
1166+
repo_unuse_commit_buffer(repo, c->item, message);
1167+
}
1168+
1169+
ret = 0;
1170+
1171+
out:
1172+
commit_list_free(commits);
1173+
reset_revision_walk();
1174+
release_revisions(&revs);
1175+
strvec_clear(&args);
1176+
return ret;
1177+
}
1178+
11171179
static int cmd_history_squash(int argc,
11181180
const char **argv,
11191181
const char *prefix,
@@ -1138,6 +1200,7 @@ static int cmd_history_squash(int argc,
11381200
OPT_END(),
11391201
};
11401202
struct strbuf reflog_msg = STRBUF_INIT;
1203+
struct strbuf message = STRBUF_INIT;
11411204
struct oidset interior = OIDSET_INIT;
11421205
struct commit *base, *oldest, *tip, *rewritten;
11431206
const struct object_id *base_tree_oid, *tip_tree_oid;
@@ -1181,6 +1244,12 @@ static int cmd_history_squash(int argc,
11811244
}
11821245
}
11831246

1247+
if (flags & COMMIT_TREE_EDIT_MESSAGE) {
1248+
ret = build_squash_message(repo, base, tip, &message);
1249+
if (ret < 0)
1250+
goto out;
1251+
}
1252+
11841253
ret = setup_revwalk(repo, action, tip, &revs);
11851254
if (ret < 0)
11861255
goto out;
@@ -1189,7 +1258,8 @@ static int cmd_history_squash(int argc,
11891258
tip_tree_oid = &repo_get_commit_tree(repo, tip)->object.oid;
11901259
commit_list_append(base, &parents);
11911260

1192-
ret = commit_tree_ext(repo, "squash", oldest, NULL, parents,
1261+
ret = commit_tree_ext(repo, "squash", oldest,
1262+
message.len ? message.buf : NULL, parents,
11931263
base_tree_oid, tip_tree_oid, &rewritten, flags);
11941264
if (ret < 0) {
11951265
ret = error(_("failed writing squashed commit"));
@@ -1210,6 +1280,7 @@ static int cmd_history_squash(int argc,
12101280

12111281
out:
12121282
strbuf_release(&reflog_msg);
1283+
strbuf_release(&message);
12131284
oidset_clear(&interior);
12141285
commit_list_free(parents);
12151286
release_revisions(&revs);

t/t3455-history-squash.sh

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,121 @@ test_expect_success 'preserves authorship of the oldest commit' '
186186
test_cmp expect actual
187187
'
188188

189+
test_expect_success '--reedit-message offers every folded-in message' '
190+
git reset --hard start &&
191+
echo b >file &&
192+
git add file &&
193+
git commit -m "re-one subject" -m "re-one body line" &&
194+
test_commit --no-tag re-two file c &&
195+
test_commit re-three file d &&
196+
197+
write_script editor <<-\EOF &&
198+
cat "$1" >edited &&
199+
echo combined >"$1"
200+
EOF
201+
test_set_editor "$(pwd)/editor" &&
202+
git history squash --reedit-message start.. &&
203+
204+
cat >expect <<-EOF &&
205+
# This is a combination of 3 commits.
206+
# This is the 1st commit message:
207+
208+
re-one subject
209+
210+
re-one body line
211+
212+
# This is the commit message #2:
213+
214+
re-two
215+
216+
# This is the commit message #3:
217+
218+
re-three
219+
220+
# Please enter the commit message for the squash changes. Lines starting
221+
# with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
222+
# Changes to be committed:
223+
# modified: file
224+
#
225+
EOF
226+
test_cmp expect edited &&
227+
echo combined >expect &&
228+
git log --format="%s" -1 >actual &&
229+
test_cmp expect actual
230+
'
231+
232+
test_expect_success '--reedit-message handles fixup!, squash! and amend! like rebase' '
233+
git reset --hard start &&
234+
test_commit --no-tag mark-base file b &&
235+
printf "fixup! mark-base\n\nfixup body\n" >msg &&
236+
echo c >file &&
237+
git add file &&
238+
git commit -qF msg &&
239+
printf "squash! mark-base\n\nsquash remark\n" >msg &&
240+
echo d >file &&
241+
git add file &&
242+
git commit -qF msg &&
243+
printf "amend! mark-base\n\namended message\n" >msg &&
244+
echo e >file &&
245+
git add file &&
246+
git commit -qF msg &&
247+
248+
write_script editor <<-\EOF &&
249+
cat "$1" >edited
250+
EOF
251+
test_set_editor "$(pwd)/editor" &&
252+
git history squash --reedit-message start.. &&
253+
254+
cat >expect <<-EOF &&
255+
# This is a combination of 4 commits.
256+
# This is the 1st commit message:
257+
258+
mark-base
259+
260+
# The commit message #2 will be skipped:
261+
262+
# fixup! mark-base
263+
#
264+
# fixup body
265+
266+
# This is the commit message #3:
267+
268+
# squash! mark-base
269+
270+
squash remark
271+
272+
# This is the commit message #4:
273+
274+
# amend! mark-base
275+
276+
amended message
277+
278+
# Please enter the commit message for the squash changes. Lines starting
279+
# with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
280+
# Changes to be committed:
281+
# modified: file
282+
#
283+
EOF
284+
test_cmp expect edited &&
285+
git log -1 --format="%B" >final &&
286+
test_grep ! "fixup body" final &&
287+
test_grep "squash remark" final &&
288+
test_grep "amended message" final
289+
'
290+
291+
test_expect_success '--reedit-message aborts on an empty message' '
292+
git reset --hard three &&
293+
head_before=$(git rev-parse HEAD) &&
294+
295+
write_script editor <<-\EOF &&
296+
>"$1"
297+
EOF
298+
test_set_editor "$(pwd)/editor" &&
299+
test_must_fail git history squash --reedit-message start.. &&
300+
301+
test_cmp_rev "$head_before" HEAD
302+
'
303+
189304
test_expect_success '--update-refs=head only moves HEAD' '
190305
git reset --hard three &&
191306
git branch -f other HEAD &&

0 commit comments

Comments
 (0)