mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-10-16 02:17:19 +00:00
project: read HEAD directly in-memory to avoid subprocesses
Benchmark: * repo upload frameworks/base: 2.84s -> 0.50s (-82.3%, 5.6x faster) * repo upload (3,045 projects): 7.66s -> 5.24s (-31.6%, 2.42s saved) From repo's inception through v2.56, GetHead() directly read the `.git/HEAD` file in Python. In commit52bab0ba("project: Use git rev-parse to read HEAD"), this was replaced with git subprocess calls on the premise that git provides a dedicated command. However, in large multi-project workspaces (such as Android with 3,000+ projects), spawning thousands of git processes introduced severe latency regressions during `repo upload` and `repo status`. Furthermore, switching to subprocesses broke detached HEADs and unborn branches (fixed in commits7f7d70efand8c3585f3by re-adding the v2.56 file-reading logic as an error recovery fallback). This patch restores fast in-memory reading as the primary path, while adding modern defensive safeguards: * Symbolic refs (`ref: refs/heads/...`): Strips whitespace and tabs and returns the ref directly in memory. * Detached HEAD: Validates 40-char SHA-1 and 64-char SHA-256 commit hashes via git_config.IsId(), normalizing to lowercase. * Symlinks: Detects filesystem symlinks via os.path.islink() and safely falls back to git symbolic-ref. * Fallback: Catches (OSError, AssertionError) and transparently falls back to native git commands for reftables, unexpected layouts, or filesystem errors. Unifies recovery fallback parsing with the fast path (CRLF/tabs, lowercase hashes, and consistent RelPath errors). In addition, this change substantially expands test coverage in tests/test_project.py, adding comprehensive unit tests for symbolic refs, whitespace/tabs, CRLF line endings, SHA-1, SHA-256, uppercase hash normalization, symlinks, corrupted worktrees, and fallback robustness. Test: ./run_tests tests/test_project.py Change-Id: Ib5c2530117c6939e4b9293feda81aa745c003c6a Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/623001 Commit-Queue: James Hawkins <jhawkins@google.com> Reviewed-by: Brian Gan <brgan@google.com> Tested-by: James Hawkins <jhawkins@google.com>
This commit is contained in:
1 parent
d7299422ae
commit
4fe87617ff
2 files changed
+281
-14
No files matched your search
+59
-14
@@ -4464,8 +4464,52 @@ class Project:
|
||||
|
||||
return dotgit if subpath is None else os.path.join(dotgit, subpath)
|
||||
|
||||
@staticmethod
|
||||
def _ParseHead(line: str) -> Optional[str]:
|
||||
"""Parse the content of a .git/HEAD file.
|
||||
|
||||
Handles both symbolic refs (e.g. 'ref: refs/heads/...') and raw
|
||||
commit IDs (40-hex SHA-1 or 64-hex SHA-256).
|
||||
|
||||
Returns:
|
||||
The ref name (e.g. 'refs/heads/main') or lowercase commit hash
|
||||
if valid, or None if empty or invalid.
|
||||
"""
|
||||
line = line.strip()
|
||||
if line.startswith("ref:"):
|
||||
ref = line[4:].strip()
|
||||
# Ensure the ref is not empty, pure whitespace, or the
|
||||
# "refs/heads/.invalid" placeholder used for unborn branches,
|
||||
# empty repositories, or when the reftables backend is used
|
||||
# (which will be the default in Git 3.0).
|
||||
if not ref or ref == R_HEADS + ".invalid":
|
||||
return None
|
||||
return ref
|
||||
else:
|
||||
# Normalize commit IDs to canonical lowercase hexadecimal,
|
||||
# matching the output format of `git rev-parse`.
|
||||
line_lower = line.lower()
|
||||
if IsId(line_lower):
|
||||
return line_lower
|
||||
return None
|
||||
|
||||
def GetHead(self):
|
||||
"""Return the ref that HEAD points to."""
|
||||
path = None
|
||||
try:
|
||||
# Catch AssertionError raised by GetDotgitPath when worktree
|
||||
# .git pointer file is malformed (e.g. missing 'gitdir:').
|
||||
path = self.GetDotgitPath(subpath=HEAD)
|
||||
if not platform_utils.islink(path):
|
||||
with open(
|
||||
path, "r", encoding="utf-8", errors="replace"
|
||||
) as fd:
|
||||
ref = self._ParseHead(fd.readline())
|
||||
if ref:
|
||||
return ref
|
||||
except (OSError, AssertionError):
|
||||
pass
|
||||
|
||||
try:
|
||||
return self.symbolic_ref("-q", HEAD, log_as_error=False)
|
||||
except GitError:
|
||||
@@ -4485,22 +4529,23 @@ class Project:
|
||||
|
||||
# Fallback to direct file reading for compatibility with broken
|
||||
# repos, e.g. if HEAD points to an unborn branch.
|
||||
path = self.GetDotgitPath(subpath=HEAD)
|
||||
if not path:
|
||||
raise NoManifestException(
|
||||
self._project.RelPath(local=False), str(e)
|
||||
)
|
||||
try:
|
||||
with open(path) as fd:
|
||||
line = fd.readline()
|
||||
with open(
|
||||
path, "r", encoding="utf-8", errors="replace"
|
||||
) as fd:
|
||||
ref = self._ParseHead(fd.readline())
|
||||
except OSError:
|
||||
raise NoManifestException(path, str(e))
|
||||
try:
|
||||
line = line.decode()
|
||||
except AttributeError:
|
||||
pass
|
||||
if line.startswith("ref: "):
|
||||
ref = line[5:-1]
|
||||
else:
|
||||
ref = line[:-1]
|
||||
if ref == R_HEADS + ".invalid":
|
||||
raise NoManifestException(path, str(e))
|
||||
raise NoManifestException(
|
||||
self._project.RelPath(local=False), str(e)
|
||||
)
|
||||
if not ref:
|
||||
raise NoManifestException(
|
||||
self._project.RelPath(local=False), str(e)
|
||||
)
|
||||
return ref
|
||||
|
||||
def SetHead(self, ref, message=None):
|
||||
|
||||
Reference in new issue
Block a user