55import argparse
66import logging
77import pathlib
8+ import subprocess
89import typing as t
910
1011from vcspull .config import filter_repos , find_config_files , load_configs
@@ -74,6 +75,23 @@ def create_status_subparser(parser: argparse.ArgumentParser) -> None:
7475 )
7576
7677
78+ def _run_git_command (
79+ repo_path : pathlib .Path ,
80+ * args : str ,
81+ ) -> subprocess .CompletedProcess [str ] | None :
82+ """Execute a git command and return the completed process."""
83+ try :
84+ return subprocess .run (
85+ ["git" , * args ],
86+ cwd = repo_path ,
87+ capture_output = True ,
88+ text = True ,
89+ check = True ,
90+ )
91+ except (subprocess .CalledProcessError , FileNotFoundError ):
92+ return None
93+
94+
7795def check_repo_status (repo : ConfigDict , detailed : bool = False ) -> dict [str , t .Any ]:
7896 """Check the status of a single repository.
7997
@@ -100,6 +118,9 @@ def check_repo_status(repo: ConfigDict, detailed: bool = False) -> dict[str, t.A
100118 "exists" : False ,
101119 "is_git" : False ,
102120 "clean" : None ,
121+ "branch" : None ,
122+ "ahead" : None ,
123+ "behind" : None ,
103124 }
104125
105126 # Check if repository exists
@@ -110,12 +131,52 @@ def check_repo_status(repo: ConfigDict, detailed: bool = False) -> dict[str, t.A
110131 if (repo_path / ".git" ).exists ():
111132 status ["is_git" ] = True
112133
113- # TODO: Add more detailed status checks when detailed=True
114- # - Check if clean/dirty
115- # - Check if ahead/behind remote
116- # - Check current branch
117- # For now, just mark as clean if .git exists
118- status ["clean" ] = True
134+ porcelain_result = _run_git_command (repo_path , "status" , "--porcelain" )
135+ if porcelain_result is not None :
136+ status ["clean" ] = porcelain_result .stdout .strip () == ""
137+ else :
138+ status ["clean" ] = True
139+
140+ if detailed :
141+ branch_result = _run_git_command (
142+ repo_path ,
143+ "rev-parse" ,
144+ "--abbrev-ref" ,
145+ "HEAD" ,
146+ )
147+ if branch_result is not None :
148+ status ["branch" ] = branch_result .stdout .strip ()
149+
150+ ahead = 0
151+ behind = 0
152+ upstream_available = _run_git_command (
153+ repo_path ,
154+ "rev-parse" ,
155+ "--abbrev-ref" ,
156+ "@{upstream}" ,
157+ )
158+ if upstream_available is not None :
159+ counts = _run_git_command (
160+ repo_path ,
161+ "rev-list" ,
162+ "--left-right" ,
163+ "--count" ,
164+ "@{upstream}...HEAD" ,
165+ )
166+ if counts is not None :
167+ parts = counts .stdout .strip ().split ()
168+ if len (parts ) == 2 :
169+ behind , ahead = (int (parts [0 ]), int (parts [1 ]))
170+ status ["ahead" ] = ahead
171+ status ["behind" ] = behind
172+
173+ # Maintain clean flag if porcelain failed
174+ if status ["clean" ] is None :
175+ status ["clean" ] = True
176+ else :
177+ status ["branch" ] = None
178+ status ["ahead" ] = None
179+ status ["behind" ] = None
119180
120181 return status
121182
@@ -249,10 +310,32 @@ def _format_status_line(
249310 status_color = colors .error (message )
250311 elif status ["is_git" ]:
251312 symbol = colors .success ("✓" )
252- message = "up to date" if status ["clean" ] else "dirty"
253- status_color = (
254- colors .success (message ) if status ["clean" ] else colors .warning (message )
255- )
313+ clean_state = status ["clean" ]
314+ ahead = status .get ("ahead" )
315+ behind = status .get ("behind" )
316+ if clean_state is False :
317+ message = "dirty"
318+ status_color = colors .warning (message )
319+ elif isinstance (ahead , int ) and isinstance (behind , int ):
320+ if ahead > 0 and behind > 0 :
321+ message = f"diverged (ahead { ahead } , behind { behind } )"
322+ status_color = colors .warning (message )
323+ elif ahead > 0 :
324+ message = f"ahead by { ahead } "
325+ status_color = colors .info (message )
326+ elif behind > 0 :
327+ message = f"behind by { behind } "
328+ status_color = colors .warning (message )
329+ else :
330+ message = "up to date"
331+ status_color = colors .success (message )
332+ else :
333+ message = "up to date" if clean_state else "dirty"
334+ status_color = (
335+ colors .success (message )
336+ if clean_state in {True , None }
337+ else colors .warning (message )
338+ )
256339 else :
257340 symbol = colors .warning ("⚠" )
258341 message = "not a git repo"
@@ -262,3 +345,10 @@ def _format_status_line(
262345
263346 if detailed :
264347 formatter .emit_text (f" { colors .muted ('Path:' )} { status ['path' ]} " )
348+ branch = status .get ("branch" )
349+ if branch :
350+ formatter .emit_text (f" { colors .muted ('Branch:' )} { branch } " )
351+ ahead = status .get ("ahead" )
352+ behind = status .get ("behind" )
353+ if isinstance (ahead , int ) and isinstance (behind , int ):
354+ formatter .emit_text (f" { colors .muted ('Ahead/Behind:' )} { ahead } /{ behind } " )
0 commit comments