Skip to content

Commit 7aa012e

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 other commits in the range were lost. Gather the message of every commit in the range and build the same editor template that "git rebase -i --autosquash" shows for a squash, reusing add_squash_combination_header(), add_squash_message_header() and squash_subject_comment_len(). Feed the range through todo_list_rearrange_squash() so that each fixup!, squash! or amend! is grouped under the commit it targets rather than shown in commit order, exactly as autosquash would arrange them. Only the message text differs, the changes are always folded in. A fixup! message is commented out in full under a "will be skipped" header, a squash! keeps its body with only the marker subject commented, and an amend! replaces its target's message unless a squash! already folded into that target, in which case it behaves like a squash!. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent f11a7a5 commit 7aa012e

3 files changed

Lines changed: 328 additions & 4 deletions

File tree

Documentation/git-history.adoc

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,9 @@ given, for example `HEAD~3..HEAD ^topic` to additionally exclude what is
117117
already on `topic`. Rev-list options may also be given, but any that would
118118
change how the range is walked are overridden with a warning.
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.
@@ -127,7 +128,15 @@ A `fixup!`, `squash!`, or `amend!` commit is refused unless the commit it
127128
targets is also in the range, so the fold does not silently absorb a
128129
marker meant for a commit outside it. As an exception, a range made up
129130
entirely of markers for one target is combined into a single commit,
130-
keeping the last `amend!` message if there is one.
131+
keeping the last `amend!` message if there is one. The changes from every
132+
commit in the range are always folded in. Only the message text differs.
133+
With `--reedit-message` the template mirrors `git rebase -i --autosquash`:
134+
each `fixup!`, `squash!`, or `amend!` is grouped under the commit it
135+
targets rather than shown in commit order. A `fixup!` message is dropped
136+
(commented out in full), a `squash!` keeps its body with only the marker
137+
subject commented, and an `amend!` replaces its target's message, unless
138+
a `squash!` folded into that target first, in which case it keeps its
139+
body like a `squash!`.
131140
+
132141
A branch or tag that points at a commit inside the range would be left
133142
dangling once those commits are folded away, so with the default

builtin/history.c

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1211,6 +1211,102 @@ static int find_interior_ref(const struct reference *ref, void *cb_data)
12111211
return 0;
12121212
}
12131213

1214+
static bool amend_replaces_target(struct todo_list *todo, int target)
1215+
{
1216+
int i;
1217+
1218+
for (i = target + 1; i < todo->nr &&
1219+
todo->items[i].command != TODO_PICK; i++) {
1220+
if (todo->items[i].command == TODO_SQUASH)
1221+
return false;
1222+
if (todo->items[i].flags & TODO_REPLACE_FIXUP_MSG)
1223+
return true;
1224+
}
1225+
return false;
1226+
}
1227+
1228+
static int build_squash_message(struct repository *repo,
1229+
struct commit *base,
1230+
struct commit *tip,
1231+
struct strbuf *out)
1232+
{
1233+
struct rev_info revs;
1234+
struct commit *commit;
1235+
struct strvec args = STRVEC_INIT;
1236+
struct todo_list todo = TODO_LIST_INIT;
1237+
struct replay_opts opts = REPLAY_OPTS_INIT;
1238+
int i, nr_commits, ret;
1239+
1240+
repo_init_revisions(repo, &revs, NULL);
1241+
strvec_push(&args, "ignored");
1242+
strvec_push(&args, "--reverse");
1243+
strvec_push(&args, "--topo-order");
1244+
strvec_pushf(&args, "%s..%s", oid_to_hex(&base->object.oid),
1245+
oid_to_hex(&tip->object.oid));
1246+
setup_revisions_from_strvec(&args, &revs, NULL);
1247+
1248+
if (prepare_revision_walk(&revs) < 0) {
1249+
ret = error(_("error preparing revisions"));
1250+
goto out;
1251+
}
1252+
1253+
while ((commit = get_revision(&revs)))
1254+
strbuf_addf(&todo.buf, "pick %s\n",
1255+
oid_to_hex(&commit->object.oid));
1256+
1257+
if (todo_list_parse_insn_buffer(repo, &opts, todo.buf.buf, &todo) < 0 ||
1258+
todo_list_rearrange_squash(&todo) < 0) {
1259+
ret = error(_("could not prepare the squash message"));
1260+
goto out;
1261+
}
1262+
1263+
nr_commits = todo.nr;
1264+
for (i = 0; i < nr_commits; i++) {
1265+
struct todo_item *item = &todo.items[i];
1266+
const char *message, *body;
1267+
size_t commented_len;
1268+
bool skip, squashing;
1269+
1270+
squashing = item->command == TODO_SQUASH ||
1271+
(item->flags & TODO_REPLACE_FIXUP_MSG);
1272+
if (item->command == TODO_PICK)
1273+
skip = amend_replaces_target(&todo, i);
1274+
else
1275+
skip = !squashing;
1276+
1277+
message = repo_logmsg_reencode(repo, item->commit, NULL, NULL);
1278+
find_commit_subject(message, &body);
1279+
1280+
if (skip)
1281+
commented_len = strlen(body);
1282+
else if (squashing)
1283+
commented_len = squash_subject_comment_len(body, 1);
1284+
else
1285+
commented_len = 0;
1286+
1287+
if (!i)
1288+
add_squash_combination_header(out, nr_commits);
1289+
strbuf_addch(out, '\n');
1290+
add_squash_message_header(out, i + 1, skip);
1291+
strbuf_addstr(out, "\n\n");
1292+
strbuf_add_commented_lines(out, body, commented_len, comment_line_str);
1293+
strbuf_addstr(out, body + commented_len);
1294+
strbuf_complete_line(out);
1295+
1296+
repo_unuse_commit_buffer(repo, item->commit, message);
1297+
}
1298+
1299+
ret = 0;
1300+
1301+
out:
1302+
todo_list_release(&todo);
1303+
replay_opts_release(&opts);
1304+
reset_revision_walk();
1305+
release_revisions(&revs);
1306+
strvec_clear(&args);
1307+
return ret;
1308+
}
1309+
12141310
static int cmd_history_squash(int argc,
12151311
const char **argv,
12161312
const char *prefix,
@@ -1235,6 +1331,7 @@ static int cmd_history_squash(int argc,
12351331
OPT_END(),
12361332
};
12371333
struct strbuf reflog_msg = STRBUF_INIT;
1334+
struct strbuf message = STRBUF_INIT;
12381335
struct oidset interior = OIDSET_INIT;
12391336
struct commit *base, *oldest, *tip, *rewritten, *msg_source;
12401337
const struct object_id *base_tree_oid, *tip_tree_oid;
@@ -1279,6 +1376,12 @@ static int cmd_history_squash(int argc,
12791376
}
12801377
}
12811378

1379+
if (flags & COMMIT_TREE_EDIT_MESSAGE) {
1380+
ret = build_squash_message(repo, base, tip, &message);
1381+
if (ret < 0)
1382+
goto out;
1383+
}
1384+
12821385
ret = setup_revwalk(repo, action, tip, &revs);
12831386
if (ret < 0)
12841387
goto out;
@@ -1287,7 +1390,8 @@ static int cmd_history_squash(int argc,
12871390
tip_tree_oid = &repo_get_commit_tree(repo, tip)->object.oid;
12881391
commit_list_append(base, &parents);
12891392

1290-
ret = commit_tree_ext(repo, "squash", msg_source, NULL, parents,
1393+
ret = commit_tree_ext(repo, "squash", msg_source,
1394+
message.len ? message.buf : NULL, parents,
12911395
base_tree_oid, tip_tree_oid, &rewritten, flags);
12921396
if (ret < 0) {
12931397
ret = error(_("failed writing squashed commit"));
@@ -1308,6 +1412,7 @@ static int cmd_history_squash(int argc,
13081412

13091413
out:
13101414
strbuf_release(&reflog_msg);
1415+
strbuf_release(&message);
13111416
oidset_clear(&interior);
13121417
commit_list_free(parents);
13131418
release_revisions(&revs);

t/t3455-history-squash.sh

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,216 @@ test_expect_success 'preserves authorship of the oldest commit' '
250250
test_cmp expect actual
251251
'
252252

253+
test_expect_success '--reedit-message offers every folded-in message' '
254+
git reset --hard start &&
255+
echo b >file &&
256+
git add file &&
257+
git commit -m "re-one subject" -m "re-one body line" &&
258+
test_commit --no-tag re-two file c &&
259+
test_commit re-three file d &&
260+
261+
write_script editor <<-\EOF &&
262+
cat "$1" >edited &&
263+
echo combined >"$1"
264+
EOF
265+
test_set_editor "$(pwd)/editor" &&
266+
git history squash --reedit-message start.. &&
267+
268+
cat >expect <<-EOF &&
269+
# This is a combination of 3 commits.
270+
# This is the 1st commit message:
271+
272+
re-one subject
273+
274+
re-one body line
275+
276+
# This is the commit message #2:
277+
278+
re-two
279+
280+
# This is the commit message #3:
281+
282+
re-three
283+
284+
# Please enter the commit message for the squash changes. Lines starting
285+
# with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
286+
# Changes to be committed:
287+
# modified: file
288+
#
289+
EOF
290+
test_cmp expect edited &&
291+
echo combined >expect &&
292+
git log --format="%s" -1 >actual &&
293+
test_cmp expect actual
294+
'
295+
296+
test_expect_success '--reedit-message handles fixup!, squash! and amend! like rebase' '
297+
git reset --hard start &&
298+
test_commit --no-tag mark-base file b &&
299+
printf "fixup! mark-base\n\nfixup body\n" >msg &&
300+
echo c >file &&
301+
git add file &&
302+
git commit -qF msg &&
303+
printf "squash! mark-base\n\nsquash remark\n" >msg &&
304+
echo d >file &&
305+
git add file &&
306+
git commit -qF msg &&
307+
printf "amend! mark-base\n\namended message\n" >msg &&
308+
echo e >file &&
309+
git add file &&
310+
git commit -qF msg &&
311+
312+
write_script editor <<-\EOF &&
313+
cat "$1" >edited
314+
EOF
315+
test_set_editor "$(pwd)/editor" &&
316+
git history squash --reedit-message start.. &&
317+
318+
cat >expect <<-EOF &&
319+
# This is a combination of 4 commits.
320+
# This is the 1st commit message:
321+
322+
mark-base
323+
324+
# The commit message #2 will be skipped:
325+
326+
# fixup! mark-base
327+
#
328+
# fixup body
329+
330+
# This is the commit message #3:
331+
332+
# squash! mark-base
333+
334+
squash remark
335+
336+
# This is the commit message #4:
337+
338+
# amend! mark-base
339+
340+
amended message
341+
342+
# Please enter the commit message for the squash changes. Lines starting
343+
# with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
344+
# Changes to be committed:
345+
# modified: file
346+
#
347+
EOF
348+
test_cmp expect edited &&
349+
git log -1 --format="%B" >final &&
350+
test_grep ! "fixup body" final &&
351+
test_grep "squash remark" final &&
352+
test_grep "amended message" final
353+
'
354+
355+
test_expect_success '--reedit-message groups fixups under their targets' '
356+
git reset --hard start &&
357+
test_commit --no-tag alpha file a1 &&
358+
test_commit --no-tag beta file b1 &&
359+
printf "fixup! alpha\n" >msg &&
360+
echo a2 >file &&
361+
git add file &&
362+
git commit -qF msg &&
363+
printf "fixup! beta\n" >msg &&
364+
echo b2 >file &&
365+
git add file &&
366+
git commit -qF msg &&
367+
368+
write_script editor <<-\EOF &&
369+
cat "$1" >edited
370+
EOF
371+
test_set_editor "$(pwd)/editor" &&
372+
git history squash --reedit-message start.. &&
373+
374+
cat >expect <<-EOF &&
375+
# This is a combination of 4 commits.
376+
# This is the 1st commit message:
377+
378+
alpha
379+
380+
# The commit message #2 will be skipped:
381+
382+
# fixup! alpha
383+
384+
# This is the commit message #3:
385+
386+
beta
387+
388+
# The commit message #4 will be skipped:
389+
390+
# fixup! beta
391+
392+
# Please enter the commit message for the squash changes. Lines starting
393+
# with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
394+
# Changes to be committed:
395+
# modified: file
396+
#
397+
EOF
398+
test_cmp expect edited
399+
'
400+
401+
test_expect_success '--reedit-message lets amend! replace its target message' '
402+
git reset --hard start &&
403+
test_commit --no-tag mark-base file b &&
404+
printf "amend! mark-base\n\namended message\n" >msg &&
405+
echo c >file &&
406+
git add file &&
407+
git commit -qF msg &&
408+
printf "squash! mark-base\n\nsquash remark\n" >msg &&
409+
echo d >file &&
410+
git add file &&
411+
git commit -qF msg &&
412+
413+
write_script editor <<-\EOF &&
414+
cat "$1" >edited
415+
EOF
416+
test_set_editor "$(pwd)/editor" &&
417+
git history squash --reedit-message start.. &&
418+
419+
cat >expect <<-EOF &&
420+
# This is a combination of 3 commits.
421+
# The 1st commit message will be skipped:
422+
423+
# mark-base
424+
425+
# This is the commit message #2:
426+
427+
# amend! mark-base
428+
429+
amended message
430+
431+
# This is the commit message #3:
432+
433+
# squash! mark-base
434+
435+
squash remark
436+
437+
# Please enter the commit message for the squash changes. Lines starting
438+
# with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
439+
# Changes to be committed:
440+
# modified: file
441+
#
442+
EOF
443+
test_cmp expect edited &&
444+
git log -1 --format="%B" >final &&
445+
test_grep ! "mark-base" final &&
446+
test_grep "amended message" final &&
447+
test_grep "squash remark" final
448+
'
449+
450+
test_expect_success '--reedit-message aborts on an empty message' '
451+
git reset --hard three &&
452+
head_before=$(git rev-parse HEAD) &&
453+
454+
write_script editor <<-\EOF &&
455+
>"$1"
456+
EOF
457+
test_set_editor "$(pwd)/editor" &&
458+
test_must_fail git history squash --reedit-message start.. &&
459+
460+
test_cmp_rev "$head_before" HEAD
461+
'
462+
253463
test_expect_success '--update-refs=head only moves HEAD' '
254464
git reset --hard three &&
255465
git branch -f other HEAD &&

0 commit comments

Comments
 (0)