Coverage for session_buddy / utils / git_utils.py: 22.92%

68 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-04 00:43 -0800

1#!/usr/bin/env python3 

2"""Git operation utilities for session management. 

3 

4This module provides Git-related functionality following crackerjack 

5architecture patterns with single responsibility principle. 

6""" 

7 

8from __future__ import annotations 

9 

10import subprocess # nosec B404 

11from typing import TYPE_CHECKING 

12 

13if TYPE_CHECKING: 

14 from pathlib import Path 

15 

16 

17def _parse_git_status(status_lines: list[str]) -> tuple[list[str], list[str]]: 

18 """Parse git status output into staged and untracked files.""" 

19 staged_files = [] 

20 untracked_files = [] 

21 

22 for line in status_lines: 

23 if line.startswith(("A ", "M ", "D ")): 

24 staged_files.append(line[3:]) # Remove status prefix 

25 elif line.startswith("?? "): 

26 untracked_files.append(line[3:]) # Remove ?? prefix 

27 

28 return staged_files, untracked_files 

29 

30 

31def _format_untracked_files(untracked_files: list[str]) -> list[str]: 

32 """Format untracked files for display.""" 

33 if not untracked_files: 

34 return ["✅ No untracked files"] 

35 

36 formatted = ["📁 Untracked Files:"] 

37 for file in untracked_files[:10]: # Limit display 

38 formatted.append(f"{file}") 

39 

40 if len(untracked_files) > 10: 

41 formatted.append(f" ... and {len(untracked_files) - 10} more files") 

42 

43 return formatted 

44 

45 

46def _stage_and_commit_files( 

47 current_dir: Path, 

48 commit_message: str, 

49 files_to_stage: list[str] | None = None, 

50) -> tuple[bool, list[str]]: 

51 """Stage files and create commit with given message.""" 

52 output: list[str] = [] 

53 try: 

54 stage_success = _stage_files(current_dir, files_to_stage, output) 

55 if not stage_success: 

56 return False, output 

57 

58 return _commit_staged_changes(current_dir, commit_message, output) 

59 except Exception as exc: 

60 output.append(f"❌ Git operation error: {exc}") 

61 return False, output 

62 

63 

64def _stage_files( 

65 current_dir: Path, 

66 files_to_stage: list[str] | None, 

67 output: list[str], 

68) -> bool: 

69 """Stage specified files or all changes.""" 

70 if files_to_stage: 

71 return all( 

72 _run_git_command(["git", "add", file_path], current_dir, output) 

73 for file_path in files_to_stage 

74 ) 

75 

76 if _run_git_command(["git", "add", "-A"], current_dir, output): 

77 return True 

78 

79 output.append("⚠️ Failed to stage changes") 

80 return False 

81 

82 

83def _commit_staged_changes( 

84 current_dir: Path, 

85 commit_message: str, 

86 output: list[str], 

87) -> tuple[bool, list[str]]: 

88 """Commit staged changes and update output log.""" 

89 success = _run_git_command( 

90 ["git", "commit", "-m", commit_message], current_dir, output 

91 ) 

92 if success: 

93 output.append(f"✅ Committed changes: {commit_message}") 

94 return True, output 

95 

96 output.append("⚠️ Commit failed") 

97 return False, output 

98 

99 

100def _run_git_command( 

101 command: list[str], 

102 current_dir: Path, 

103 output: list[str], 

104) -> bool: 

105 """Run a git command and append stderr output when it fails.""" 

106 result = subprocess.run( 

107 command, 

108 cwd=current_dir, 

109 capture_output=True, 

110 text=True, 

111 check=False, 

112 ) 

113 

114 if result.returncode == 0: 

115 return True 

116 

117 stderr = result.stderr.strip() 

118 if stderr: 

119 output.append(f"⚠️ {' '.join(command[1:3])} failed: {stderr}") 

120 return False 

121 

122 

123def _optimize_git_repository(current_dir: Path) -> list[str]: 

124 """Optimize Git repository with garbage collection and pruning.""" 

125 optimization_results = [] 

126 

127 try: 

128 # Git garbage collection 

129 gc_cmd = ["git", "gc", "--auto"] 

130 result = subprocess.run( 

131 gc_cmd, 

132 cwd=current_dir, 

133 capture_output=True, 

134 text=True, 

135 check=False, 

136 ) 

137 

138 if result.returncode == 0: 138 ↛ 141line 138 didn't jump to line 141 because the condition on line 138 was always true

139 optimization_results.append("🗑️ Git garbage collection completed") 

140 else: 

141 optimization_results.append(f"⚠️ Git gc failed: {result.stderr.strip()}") 

142 

143 # Prune remote tracking branches 

144 prune_cmd = ["git", "remote", "prune", "origin"] 

145 result = subprocess.run( 

146 prune_cmd, 

147 cwd=current_dir, 

148 capture_output=True, 

149 text=True, 

150 check=False, 

151 ) 

152 

153 if result.returncode == 0: 153 ↛ 157line 153 didn't jump to line 157 because the condition on line 153 was always true

154 optimization_results.append("🌿 Pruned remote tracking branches") 

155 else: 

156 # Remote prune failure is non-critical 

157 optimization_results.append( 

158 "ℹ️ Remote pruning skipped (no remote or access issues)", 

159 ) 

160 

161 except Exception as e: 

162 optimization_results.append(f"⚠️ Git optimization error: {e}") 

163 

164 return optimization_results