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

1""" 

2Branch Manager - Manage local checkpoint branches. 

3 

4SPEC: .moai/specs/SPEC-CHECKPOINT-EVENT-001/spec.md 

5""" 

6 

7from datetime import datetime 

8 

9import git 

10 

11 

12class BranchManager: 

13 """Manage local checkpoint branches.""" 

14 

15 MAX_CHECKPOINTS = 10 

16 CHECKPOINT_PREFIX = "before-" 

17 

18 def __init__(self, repo: git.Repo): 

19 """ 

20 Initialize the BranchManager. 

21 

22 Args: 

23 repo: GitPython Repo instance. 

24 """ 

25 self.repo = repo 

26 self._old_branches: set[str] = set() 

27 

28 def create_checkpoint_branch(self, operation: str) -> str: 

29 """ 

30 Create a checkpoint branch. 

31 

32 SPEC requirement: before-{operation}-{timestamp} format for local branches. 

33 

34 Args: 

35 operation: Operation name (delete, refactor, merge, etc.). 

36 

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}" 

42 

43 # Create the branch from the current HEAD 

44 self.repo.create_head(branch_name) 

45 

46 # Remove old checkpoints using FIFO order 

47 self._enforce_max_checkpoints() 

48 

49 return branch_name 

50 

51 def branch_exists(self, branch_name: str) -> bool: 

52 """ 

53 Check if a branch exists. 

54 

55 Args: 

56 branch_name: Name of the branch to check. 

57 

58 Returns: 

59 True when the branch exists, otherwise False. 

60 """ 

61 return branch_name in [head.name for head in self.repo.heads] 

62 

63 def has_remote_tracking(self, branch_name: str) -> bool: 

64 """ 

65 Determine whether a remote tracking branch exists. 

66 

67 SPEC requirement: checkpoints must remain local-only branches. 

68 

69 Args: 

70 branch_name: Branch name to inspect. 

71 

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 

80 

81 def list_checkpoint_branches(self) -> list[str]: 

82 """ 

83 List all checkpoint branches. 

84 

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 ] 

93 

94 def mark_as_old(self, branch_name: str) -> None: 

95 """ 

96 Mark a branch as old (used for tests). 

97 

98 Args: 

99 branch_name: Branch to flag as old. 

100 """ 

101 self._old_branches.add(branch_name) 

102 

103 def cleanup_old_checkpoints(self, max_count: int) -> None: 

104 """ 

105 Clean up old checkpoint branches. 

106 

107 SPEC requirement: delete using FIFO when exceeding the maximum count. 

108 

109 Args: 

110 max_count: Maximum number of checkpoints to retain. 

111 """ 

112 checkpoints = self.list_checkpoint_branches() 

113 

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 ) 

118 

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) 

124 

125 def _enforce_max_checkpoints(self) -> None: 

126 """Maintain the maximum number of checkpoints (internal).""" 

127 checkpoints = self.list_checkpoint_branches() 

128 

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 ] 

135 

136 for branch_name in to_delete: 

137 self.repo.delete_head(branch_name, force=True)