Skip to content

Commit fe4968c

Browse files
committed
Edit all messages of consecutive squashed commits at once
Closes #77
1 parent 5a10920 commit fe4968c

File tree

2 files changed

+160
-9
lines changed

2 files changed

+160
-9
lines changed

gitrevise/todo.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import re
44
from enum import Enum
5-
from typing import List, Optional
5+
from typing import List, Optional, Tuple
66

77
from .odb import Commit, MissingObject, Repository
88
from .utils import cut_commit, edit_commit_message, run_editor, run_sequence_editor
@@ -242,27 +242,63 @@ def edit_todos(
242242
return result
243243

244244

245+
def is_fixup(todo: Step) -> bool:
246+
return todo.kind in (StepKind.FIXUP, StepKind.SQUASH)
247+
248+
249+
def squash_message_template(
250+
target_message: bytes, fixups: List[Tuple[StepKind, bytes]]
251+
) -> bytes:
252+
fused = (
253+
b"# This is a combination of %d commits.\n" % (len(fixups) + 1)
254+
+ b"# This is the 1st commit message:\n"
255+
+ b"\n"
256+
+ target_message
257+
)
258+
259+
for index, (kind, message) in enumerate(fixups):
260+
fused += b"\n"
261+
if kind == StepKind.FIXUP:
262+
fused += (
263+
b"# The commit message #%d will be skipped:\n" % (index + 2)
264+
+ b"\n"
265+
+ b"".join(b"# " + line for line in message.splitlines(keepends=True))
266+
)
267+
else:
268+
assert kind == StepKind.SQUASH
269+
fused += b"# This is the commit message #%d:\n\n%s" % (index + 2, message)
270+
271+
return fused
272+
273+
245274
def apply_todos(
246275
current: Optional[Commit],
247276
todos: List[Step],
248277
reauthor: bool = False,
249278
) -> Commit:
250-
for step in todos:
279+
fixups: List[Tuple[StepKind, bytes]] = []
280+
281+
for index, step in enumerate(todos):
251282
rebased = step.commit.rebase(current).update(message=step.message)
252283
if step.kind == StepKind.PICK:
253284
current = rebased
254-
elif step.kind == StepKind.FIXUP:
285+
elif is_fixup(step):
255286
if current is None:
256287
raise ValueError("Cannot apply fixup as first commit")
288+
if not fixups:
289+
fixup_target_message = current.message
290+
fixups.append((step.kind, rebased.message))
257291
current = current.update(tree=rebased.tree())
292+
is_last_fixup = index + 1 == len(todos) or not is_fixup(todos[index + 1])
293+
if is_last_fixup:
294+
if any(kind == StepKind.SQUASH for kind, message in fixups):
295+
current = current.update(
296+
message=squash_message_template(fixup_target_message, fixups)
297+
)
298+
current = edit_commit_message(current)
299+
fixups.clear()
258300
elif step.kind == StepKind.REWORD:
259301
current = edit_commit_message(rebased)
260-
elif step.kind == StepKind.SQUASH:
261-
if current is None:
262-
raise ValueError("Cannot apply squash as first commit")
263-
fused = current.message + b"\n\n" + rebased.message
264-
current = current.update(tree=rebased.tree(), message=fused)
265-
current = edit_commit_message(current)
266302
elif step.kind == StepKind.CUT:
267303
current = cut_commit(rebased)
268304
elif step.kind == StepKind.INDEX:

tests/test_fixup.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,118 @@ def test_autosquash_multiline_summary(repo: Repository) -> None:
396396
new = repo.get_commit("HEAD")
397397
assert old != new, "commit was modified"
398398
assert old.parents() == new.parents(), "parents are unchanged"
399+
400+
401+
def test_autosquash_multiple_squashes() -> None:
402+
bash(
403+
"""
404+
git commit --allow-empty -m 'initial commit'
405+
git commit --allow-empty -m 'target'
406+
git commit --allow-empty --squash :/^target --no-edit
407+
git commit --allow-empty --squash :/^target --no-edit
408+
"""
409+
)
410+
411+
with editor_main([":/^init", "--autosquash"], input=b"") as ed:
412+
with ed.next_file() as f:
413+
assert f.startswith_dedent(
414+
"""\
415+
# This is a combination of 3 commits.
416+
# This is the 1st commit message:
417+
418+
target
419+
420+
# This is the commit message #2:
421+
422+
squash! target
423+
424+
# This is the commit message #3:
425+
426+
squash! target
427+
"""
428+
)
429+
f.replace_dedent("two squashes")
430+
431+
432+
def test_autosquash_squash_and_fixup() -> None:
433+
bash(
434+
"""
435+
git commit --allow-empty -m 'initial commit'
436+
git commit --allow-empty -m 'fixup-target'
437+
git commit --allow-empty --fixup :/^fixup-target --no-edit
438+
git commit --allow-empty -m 'squash-target'
439+
git commit --allow-empty --squash :/^squash-target --no-edit
440+
"""
441+
)
442+
443+
with editor_main([":/^init", "--autosquash"], input=b"") as ed:
444+
with ed.next_file() as f:
445+
assert f.startswith_dedent(
446+
"""\
447+
# This is a combination of 2 commits.
448+
# This is the 1st commit message:
449+
450+
squash-target
451+
452+
# This is the commit message #2:
453+
454+
squash! squash-target
455+
"""
456+
)
457+
f.replace_dedent("squash + fixup")
458+
459+
460+
def test_autosquash_multiple_squashes_with_fixup() -> None:
461+
bash(
462+
"""
463+
git commit --allow-empty -m 'initial commit'
464+
git commit --allow-empty -m 'target'
465+
git commit --allow-empty --squash :/^target --no-edit
466+
git commit --allow-empty --fixup :/^target --no-edit
467+
git commit --allow-empty --squash :/^target --no-edit
468+
git commit --allow-empty -m 'unrelated'
469+
"""
470+
)
471+
with editor_main([":/^init", "--autosquash"], input=b"") as ed:
472+
with ed.next_file() as f:
473+
assert f.startswith_dedent(
474+
"""\
475+
# This is a combination of 4 commits.
476+
# This is the 1st commit message:
477+
478+
target
479+
480+
# This is the commit message #2:
481+
482+
squash! target
483+
484+
# The commit message #3 will be skipped:
485+
486+
# fixup! target
487+
488+
# This is the commit message #4:
489+
490+
squash! target
491+
"""
492+
)
493+
f.replace_dedent("squashes + fixup")
494+
495+
496+
def test_autosquash_multiple_independent_squashes() -> None:
497+
bash(
498+
"""
499+
git commit --allow-empty -m 'initial commit'
500+
git commit --allow-empty -m 'target1'
501+
git commit --allow-empty -m 'target2'
502+
git commit --allow-empty --squash :/^target1 --no-edit
503+
git commit --allow-empty --squash :/^target2 --no-edit
504+
"""
505+
)
506+
507+
with editor_main([":/^init", "--autosquash"], input=b"") as ed:
508+
with ed.next_file() as f:
509+
assert f.startswith_dedent("# This is a combination of 2 commits.")
510+
f.replace_dedent("squash 1")
511+
with ed.next_file() as f:
512+
assert f.startswith_dedent("# This is a combination of 2 commits.")
513+
f.replace_dedent("squash 2")

0 commit comments

Comments
 (0)