Coverage for src/workstack/status/collectors/git.py: 33%
61 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 09:31 -0400
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 09:31 -0400
1"""Git status collector."""
3import subprocess
4from pathlib import Path
6from workstack.core.context import WorkstackContext
7from workstack.status.collectors.base import StatusCollector
8from workstack.status.models.status_data import CommitInfo, GitStatus
11class GitStatusCollector(StatusCollector):
12 """Collects git repository status information."""
14 @property
15 def name(self) -> str:
16 """Name identifier for this collector."""
17 return "git"
19 def is_available(self, ctx: WorkstackContext, worktree_path: Path) -> bool:
20 """Check if git operations are available.
22 Args:
23 ctx: Workstack context
24 worktree_path: Path to worktree
26 Returns:
27 True if worktree exists and has git
28 """
29 if not worktree_path.exists():
30 return False
32 return True
34 def collect(
35 self, ctx: WorkstackContext, worktree_path: Path, repo_root: Path
36 ) -> GitStatus | None:
37 """Collect git status information.
39 Args:
40 ctx: Workstack context
41 worktree_path: Path to worktree
42 repo_root: Repository root path
44 Returns:
45 GitStatus with repository information or None if collection fails
46 """
47 branch = ctx.git_ops.get_current_branch(worktree_path)
48 if branch is None:
49 return None
51 # Get git status
52 staged, modified, untracked = self._get_file_status(worktree_path)
53 clean = len(staged) == 0 and len(modified) == 0 and len(untracked) == 0
55 # Get ahead/behind counts
56 ahead, behind = self._get_ahead_behind(worktree_path, branch)
58 # Get recent commits
59 recent_commits = self._get_recent_commits(worktree_path, limit=5)
61 return GitStatus(
62 branch=branch,
63 clean=clean,
64 ahead=ahead,
65 behind=behind,
66 staged_files=staged,
67 modified_files=modified,
68 untracked_files=untracked,
69 recent_commits=recent_commits,
70 )
72 def _get_file_status(self, cwd: Path) -> tuple[list[str], list[str], list[str]]:
73 """Get lists of staged, modified, and untracked files.
75 Args:
76 cwd: Working directory
78 Returns:
79 Tuple of (staged, modified, untracked) file lists
80 """
81 result = subprocess.run(
82 ["git", "status", "--porcelain"],
83 cwd=cwd,
84 capture_output=True,
85 text=True,
86 check=True,
87 )
89 staged = []
90 modified = []
91 untracked = []
93 for line in result.stdout.splitlines():
94 if not line:
95 continue
97 status_code = line[:2]
98 filename = line[3:]
100 # Check if file is staged (first character is not space)
101 if status_code[0] != " " and status_code[0] != "?":
102 staged.append(filename)
104 # Check if file is modified (second character is not space)
105 if status_code[1] != " " and status_code[1] != "?":
106 modified.append(filename)
108 # Check if file is untracked
109 if status_code == "??":
110 untracked.append(filename)
112 return staged, modified, untracked
114 def _get_ahead_behind(self, cwd: Path, branch: str) -> tuple[int, int]:
115 """Get number of commits ahead and behind tracking branch.
117 Args:
118 cwd: Working directory
119 branch: Current branch name
121 Returns:
122 Tuple of (ahead, behind) counts
123 """
124 # Check if branch has upstream
125 result = subprocess.run(
126 ["git", "rev-parse", "--abbrev-ref", f"{branch}@{{upstream}}"],
127 cwd=cwd,
128 capture_output=True,
129 text=True,
130 check=False,
131 )
133 if result.returncode != 0:
134 # No upstream branch
135 return 0, 0
137 upstream = result.stdout.strip()
139 # Get ahead/behind counts
140 result = subprocess.run(
141 ["git", "rev-list", "--left-right", "--count", f"{upstream}...HEAD"],
142 cwd=cwd,
143 capture_output=True,
144 text=True,
145 check=True,
146 )
148 parts = result.stdout.strip().split()
149 if len(parts) == 2:
150 behind = int(parts[0])
151 ahead = int(parts[1])
152 return ahead, behind
154 return 0, 0
156 def _get_recent_commits(self, cwd: Path, *, limit: int = 5) -> list[CommitInfo]:
157 """Get recent commit information.
159 Args:
160 cwd: Working directory
161 limit: Maximum number of commits to retrieve
163 Returns:
164 List of recent commits
165 """
166 result = subprocess.run(
167 [
168 "git",
169 "log",
170 f"-{limit}",
171 "--format=%H%x00%s%x00%an%x00%ar",
172 ],
173 cwd=cwd,
174 capture_output=True,
175 text=True,
176 check=True,
177 )
179 commits = []
180 for line in result.stdout.strip().split("\n"):
181 if not line:
182 continue
184 parts = line.split("\x00")
185 if len(parts) == 4:
186 commits.append(
187 CommitInfo(
188 sha=parts[0][:7], # Short SHA
189 message=parts[1],
190 author=parts[2],
191 date=parts[3],
192 )
193 )
195 return commits