Files
cdesouza-chromium 218d8ebb4b [brockit] Fix git_status trimming issue (#35908)
This PR fixes `lift` being broken due to git status output being
trimmed, and tilting the xy axis for git status. On top of that, this PR
overhauls `GitStatus` to be more accurate and comprehensive, as there
were a few corner cases.
2026-04-27 10:29:03 +01:00

107 lines
4.1 KiB
Python

# Copyright (c) 2025 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
from dataclasses import dataclass, field
import repository
class GitStatus:
'''Parses `git status --porcelain` output into categorized file lists.
Files are grouped into two areas based on the XY status code from the
porcelain output: `staged` (X column, index vs. HEAD) and `unstaged`
(Y column, working tree vs. index). Staged renames are stored separately
in `renamed` as (old_path, new_path) tuples because they span two paths.
A single file can appear in both areas simultaneously — for example, a
file staged as modified (X='M') and then further edited (Y='M') will
appear in both `staged.modified` and `unstaged.modified`.
'''
@dataclass
class Area:
'''A view of file changes within one git status area.
Each field holds the relative paths of files in that state. A path
appears in exactly one field within an area.
Fields:
added: Paths that are new in this area (X='A' or Y='?').
modified: Paths that were changed relative to the previous state
(X='M' or Y='M').
deleted: Paths that were removed relative to the previous state
(X='D' or Y='D').
'''
added: list[str] = field(default_factory=list)
modified: list[str] = field(default_factory=list)
deleted: list[str] = field(default_factory=list)
def __init__(self):
porcelain = repository.brave.run_git('status',
'--porcelain',
no_trim=True)
self.staged: GitStatus.Area = GitStatus.Area()
self.unstaged: GitStatus.Area = GitStatus.Area()
# Staged renames as (old_path, new_path) tuples. Kept separate from
# Area because a rename inherently involves two paths.
self.renamed: list[tuple[str, str]] = []
for line in porcelain.splitlines():
xy = line[:2]
path = line[3:]
x, y = xy[0], xy[1]
if x == 'M':
self.staged.modified.append(path)
elif x == 'A':
self.staged.added.append(path)
elif x == 'D':
self.staged.deleted.append(path)
elif x == 'R':
old_path, new_path = path.split(' -> ', 1)
self.renamed.append((old_path, new_path))
path = new_path
if y == 'M':
self.unstaged.modified.append(path)
elif y == '?':
self.unstaged.added.append(path)
elif y == 'D':
self.unstaged.deleted.append(path)
def get_all_staged_entries(self) -> list[str]:
'''Returns a flat list of all paths with staged changes.
For renames, the new (current) path is included.
'''
return (self.staged.added + self.staged.modified +
self.staged.deleted +
[new_path for _, new_path in self.renamed])
def has_staged_files(self) -> bool:
'''Returns True if any staged changes exist.'''
return bool(self.staged.added or self.staged.modified
or self.staged.deleted or self.renamed)
def has_deleted_patch_files(self) -> bool:
'''Returns True if any patch files under patches/ are deleted.
Checks both staged and unstaged deletions, since a deletion can be
present in either area.
'''
all_deleted = self.staged.deleted + self.unstaged.deleted
return any(
path.startswith('patches/') and path.endswith('.patch')
for path in all_deleted)
def has_untracked_patch_files(self) -> bool:
'''Returns True if any patch files under patches/ are untracked.'''
return any(
path.startswith('patches/') and path.endswith('.patch')
for path in self.unstaged.added)