fix(git): replace git diff --cached with git status --porcelain in get_status
repo.is_dirty() and repo.index.diff("HEAD") internally call git diff --cached,
which fails with exit 129 (unknown option) in some environments.
Introduce _parse_status_porcelain() using git status --porcelain,
a self-contained command that avoids --cached entirely.
All 25 git route tests continue to pass.
This commit is contained in:
@@ -1192,12 +1192,62 @@ class GitService:
|
|||||||
raise HTTPException(status_code=500, detail=f"Git pull failed: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Git pull failed: {str(e)}")
|
||||||
# [/DEF:pull_changes:Function]
|
# [/DEF:pull_changes:Function]
|
||||||
|
|
||||||
|
# [DEF:_parse_status_porcelain:Function]
|
||||||
|
# @COMPLEXITY: 2
|
||||||
|
# @PURPOSE: Parse git status --porcelain output into staged, modified, and untracked file lists.
|
||||||
|
# @PRE: `repo` is an open GitPython Repo instance.
|
||||||
|
# @POST: Returns (staged, modified, untracked) tuple of file path lists.
|
||||||
|
# @RATIONALE: Avoids repo.is_dirty() / repo.index.diff("HEAD") which internally
|
||||||
|
# call git diff --cached, a flag unsupported in some Git environments (exit 129).
|
||||||
|
# Using git status --porcelain is self-contained and avoids the --cached flag entirely.
|
||||||
|
# @RELATION: CALLED_BY -> [GitService.get_status]
|
||||||
|
def _parse_status_porcelain(self, repo) -> tuple[list[str], list[str], list[str]]:
|
||||||
|
with belief_scope("GitService._parse_status_porcelain"):
|
||||||
|
staged: list[str] = []
|
||||||
|
modified: list[str] = []
|
||||||
|
untracked: list[str] = []
|
||||||
|
try:
|
||||||
|
output = repo.git.status("--porcelain")
|
||||||
|
except Exception:
|
||||||
|
logger.warning("[_parse_status_porcelain][Coherence:Failed] git status --porcelain failed")
|
||||||
|
return staged, modified, untracked
|
||||||
|
|
||||||
|
for line in output.split("\n"):
|
||||||
|
# Do NOT strip the line — porcelain v1 format "X Y PATH" uses
|
||||||
|
# leading space (X=' ') to indicate "unmodified in index".
|
||||||
|
# Stripping would shift column alignment.
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
# Untracked: "?? path"
|
||||||
|
if line.startswith("??"):
|
||||||
|
untracked.append(line[2:].strip())
|
||||||
|
continue
|
||||||
|
# Ignored: "!! path" (skip)
|
||||||
|
if line.startswith("!!"):
|
||||||
|
continue
|
||||||
|
# Normal entry: "XY path" or "XY orig -> dest" for renames
|
||||||
|
if len(line) < 3:
|
||||||
|
continue
|
||||||
|
x = line[0] # index (staged) status
|
||||||
|
y = line[1] # work-tree status
|
||||||
|
rest = line[3:] # strip "XY "
|
||||||
|
# For renames/copies: "R orig -> dest" — use the destination
|
||||||
|
path = rest.split(" -> ")[-1].strip() if " -> " in rest else rest.strip()
|
||||||
|
if x != " " and x != "?":
|
||||||
|
staged.append(path)
|
||||||
|
if y != " " and y != "?":
|
||||||
|
if path not in modified:
|
||||||
|
modified.append(path)
|
||||||
|
return staged, modified, untracked
|
||||||
|
# [/DEF:_parse_status_porcelain:Function]
|
||||||
|
|
||||||
# [DEF:get_status:Function]
|
# [DEF:get_status:Function]
|
||||||
# @PURPOSE: Get current repository status (dirty files, untracked, etc.)
|
# @PURPOSE: Get current repository status (dirty files, untracked, etc.)
|
||||||
# @PRE: Repository for dashboard_id exists.
|
# @PRE: Repository for dashboard_id exists.
|
||||||
# @POST: Returns a dictionary representing the Git status.
|
# @POST: Returns a dictionary representing the Git status.
|
||||||
# @RETURN: dict
|
# @RETURN: dict
|
||||||
# @RELATION: CALLS -> [GitService.get_repo]
|
# @RELATION: CALLS -> [GitService.get_repo]
|
||||||
|
# @RELATION: CALLS -> [GitService._parse_status_porcelain]
|
||||||
def get_status(self, dashboard_id: int) -> dict:
|
def get_status(self, dashboard_id: int) -> dict:
|
||||||
with belief_scope("GitService.get_status"):
|
with belief_scope("GitService.get_status"):
|
||||||
repo = self.get_repo(dashboard_id)
|
repo = self.get_repo(dashboard_id)
|
||||||
@@ -1237,10 +1287,11 @@ class GitService:
|
|||||||
ahead_count = 0
|
ahead_count = 0
|
||||||
behind_count = 0
|
behind_count = 0
|
||||||
|
|
||||||
is_dirty = repo.is_dirty(untracked_files=True)
|
# Use git status --porcelain to gather file state safely.
|
||||||
untracked_files = repo.untracked_files
|
# Avoids repo.is_dirty() and repo.index.diff("HEAD") which internally
|
||||||
modified_files = [item.a_path for item in repo.index.diff(None)]
|
# call git diff --cached -- a flag unsupported in some Git environments.
|
||||||
staged_files = [item.a_path for item in repo.index.diff("HEAD")] if has_commits else []
|
staged_files, modified_files, untracked_files = self._parse_status_porcelain(repo)
|
||||||
|
is_dirty = bool(staged_files or modified_files or untracked_files)
|
||||||
is_diverged = ahead_count > 0 and behind_count > 0
|
is_diverged = ahead_count > 0 and behind_count > 0
|
||||||
|
|
||||||
if is_diverged:
|
if is_diverged:
|
||||||
|
|||||||
Reference in New Issue
Block a user