Coverage for src / moai_adk / core / git / branch_manager.py: 32.50%
40 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""
2Branch Manager - Manage local checkpoint branches.
4SPEC: .moai/specs/SPEC-CHECKPOINT-EVENT-001/spec.md
5"""
7from datetime import datetime
9import git
12class BranchManager:
13 """Manage local checkpoint branches."""
15 MAX_CHECKPOINTS = 10
16 CHECKPOINT_PREFIX = "before-"
18 def __init__(self, repo: git.Repo):
19 """
20 Initialize the BranchManager.
22 Args:
23 repo: GitPython Repo instance.
24 """
25 self.repo = repo
26 self._old_branches: set[str] = set()
28 def create_checkpoint_branch(self, operation: str) -> str:
29 """
30 Create a checkpoint branch.
32 SPEC requirement: before-{operation}-{timestamp} format for local branches.
34 Args:
35 operation: Operation name (delete, refactor, merge, etc.).
37 Returns:
38 Name of the created branch.
39 """
40 timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
41 branch_name = f"{self.CHECKPOINT_PREFIX}{operation}-{timestamp}"
43 # Create the branch from the current HEAD
44 self.repo.create_head(branch_name)
46 # Remove old checkpoints using FIFO order
47 self._enforce_max_checkpoints()
49 return branch_name
51 def branch_exists(self, branch_name: str) -> bool:
52 """
53 Check if a branch exists.
55 Args:
56 branch_name: Name of the branch to check.
58 Returns:
59 True when the branch exists, otherwise False.
60 """
61 return branch_name in [head.name for head in self.repo.heads]
63 def has_remote_tracking(self, branch_name: str) -> bool:
64 """
65 Determine whether a remote tracking branch exists.
67 SPEC requirement: checkpoints must remain local-only branches.
69 Args:
70 branch_name: Branch name to inspect.
72 Returns:
73 True if a tracking branch exists, otherwise False.
74 """
75 try:
76 branch = self.repo.heads[branch_name]
77 return branch.tracking_branch() is not None
78 except (IndexError, AttributeError):
79 return False
81 def list_checkpoint_branches(self) -> list[str]:
82 """
83 List all checkpoint branches.
85 Returns:
86 Names of checkpoint branches.
87 """
88 return [
89 head.name
90 for head in self.repo.heads
91 if head.name.startswith(self.CHECKPOINT_PREFIX)
92 ]
94 def mark_as_old(self, branch_name: str) -> None:
95 """
96 Mark a branch as old (used for tests).
98 Args:
99 branch_name: Branch to flag as old.
100 """
101 self._old_branches.add(branch_name)
103 def cleanup_old_checkpoints(self, max_count: int) -> None:
104 """
105 Clean up old checkpoint branches.
107 SPEC requirement: delete using FIFO when exceeding the maximum count.
109 Args:
110 max_count: Maximum number of checkpoints to retain.
111 """
112 checkpoints = self.list_checkpoint_branches()
114 # Sort in chronological order (branches marked via mark_as_old first)
115 sorted_checkpoints = sorted(
116 checkpoints, key=lambda name: (name not in self._old_branches, name)
117 )
119 # Delete the excess branches
120 to_delete = sorted_checkpoints[: len(sorted_checkpoints) - max_count]
121 for branch_name in to_delete:
122 if branch_name in [head.name for head in self.repo.heads]:
123 self.repo.delete_head(branch_name, force=True)
125 def _enforce_max_checkpoints(self) -> None:
126 """Maintain the maximum number of checkpoints (internal)."""
127 checkpoints = self.list_checkpoint_branches()
129 if len(checkpoints) > self.MAX_CHECKPOINTS:
130 # Sort alphabetically (older timestamps first)
131 sorted_checkpoints = sorted(checkpoints)
132 to_delete = sorted_checkpoints[
133 : len(sorted_checkpoints) - self.MAX_CHECKPOINTS
134 ]
136 for branch_name in to_delete:
137 self.repo.delete_head(branch_name, force=True)