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
« 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.
4This module provides Git-related functionality following crackerjack
5architecture patterns with single responsibility principle.
6"""
8from __future__ import annotations
10import subprocess # nosec B404
11from typing import TYPE_CHECKING
13if TYPE_CHECKING:
14 from pathlib import Path
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 = []
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
28 return staged_files, untracked_files
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"]
36 formatted = ["📁 Untracked Files:"]
37 for file in untracked_files[:10]: # Limit display
38 formatted.append(f" • {file}")
40 if len(untracked_files) > 10:
41 formatted.append(f" ... and {len(untracked_files) - 10} more files")
43 return formatted
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
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
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 )
76 if _run_git_command(["git", "add", "-A"], current_dir, output):
77 return True
79 output.append("⚠️ Failed to stage changes")
80 return False
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
96 output.append("⚠️ Commit failed")
97 return False, output
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 )
114 if result.returncode == 0:
115 return True
117 stderr = result.stderr.strip()
118 if stderr:
119 output.append(f"⚠️ {' '.join(command[1:3])} failed: {stderr}")
120 return False
123def _optimize_git_repository(current_dir: Path) -> list[str]:
124 """Optimize Git repository with garbage collection and pruning."""
125 optimization_results = []
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 )
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()}")
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 )
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 )
161 except Exception as e:
162 optimization_results.append(f"⚠️ Git optimization error: {e}")
164 return optimization_results