1#!/usr/bin/env python3

2"""Git operation utilities for session management.

3

4 This module provides Git-related functionality following crackerjack

5 architecture patterns with single responsibility principle.

6"""

7

8 from __future__ import annotations

9

10 import subprocess # nosec B404

11 from typing import TYPE_CHECKING

12

13 if TYPE_CHECKING:

14 from pathlib import Path

15

16

17 def _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

31 def _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

46 def _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

64 def _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

83 def _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

100 def _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

123 def _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 ↛ 141 line 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 ↛ 157 line 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