Coverage for src / gitq / git_split.py: 91%
53 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-15 15:32 -0400
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-15 15:32 -0400
1#!/usr/bin/env python3
3import sys
4import argparse
5from contextlib import contextmanager
6from typing import Iterator
7from dataclasses import dataclass, field
9from . import continuations
10from .continuations import Continuation, EditBranch, Suspend, Abort
11from .git_swap import edit_commit, PickCherryWithReference
13description = """
14Splits a single commit into two or more commits. The user can add a
15additional commits, while holding the final content constant.
17Checks out COMMIT with its changes staged, via `git reset --soft HEAD^`,
18suspending so the user can make one or more new commits. After the user
19continues, COMMIT will be restored with its original content, and git-split
20will suspend again so the user can amend the commit message with
21`git commit --amend`.
23After continuing again, it replays the remaining commits from above COMMIT
24on top.
25"""
28@dataclass
29class SuspendForAmend(Continuation):
30 """Suspend after the inner block completes, so the user can run
31 `git commit --amend` before continuing."""
33 target: str
34 done: bool = field(default=False)
36 @contextmanager
37 def impl(self) -> Iterator[None]:
38 yield
39 if not self.done:
40 self.done = True
41 raise Suspend(
42 status=f"Commit {self.target} was split.\n"
43 + "Edit the message with: git commit --amend\n"
44 + "Then resume with: git split --continue."
45 )
48class Main(continuations.Main):
50 tool = "git-split"
51 suspend_message = "Resume with `git split --continue` when done."
53 def main(self):
54 parser = argparse.ArgumentParser(
55 description=description, formatter_class=argparse.RawDescriptionHelpFormatter
56 )
57 parser.add_argument("commit", nargs="?", metavar="COMMIT")
58 parser.add_argument(
59 "--continue",
60 "-c",
61 action="store_true",
62 dest="resume",
63 )
64 parser.add_argument(
65 "--abort",
66 action="store_true",
67 )
68 parser.add_argument("--status", action="store_true", help="print status")
69 args = parser.parse_args()
71 if args.resume:
72 self.resume(None)
73 return
75 if args.abort:
76 self.resume(Abort())
78 if args.status:
79 self.status()
80 return
82 if not args.commit: 82 ↛ 83line 82 didn't jump to line 83 because the condition on line 82 was never true
83 parser.print_usage()
84 sys.exit(1)
86 with self.setup():
87 commit = self.git.commit(args.commit)
88 sha = commit.sha
89 target = self.git.abbrev(sha)
90 status = (
91 f"Splitting {target}.\n"
92 + "Changes from the commit are now staged.\n"
93 + "Make one or more commits here, then resume with: git split --continue."
94 )
96 with EditBranch(message="git-split"):
97 with edit_commit(commit, git=self.git, edit=True):
98 with SuspendForAmend(target):
99 with PickCherryWithReference(cherry=sha, reference=sha):
100 self.git("reset", "--soft", "HEAD^")
101 raise Suspend(status=status)
104main = Main()
106if __name__ == "__main__": 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true
107 main()