mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-09-27 17:20:30 +00:00
git_refs: load HEAD with the ref snapshot on Git 2.45
Use for-each-ref --include-root-refs to resolve attached and detached HEAD in the same process as refs/* on Git 2.45 and newer. Limit the patterns to HEAD and refs so volatile pseudo-refs do not enter the cache, and retain the existing fallback for older Git and unborn HEADs. Bug: 543851900 Bug: 553599402 Change-Id: Ie091ca0757f21ec6b1e19d49e09d160e055ec639 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/622981 Tested-by: Gavin Mak <gavinmak@google.com> Reviewed-by: Brian Gan <brgan@google.com> Commit-Queue: Gavin Mak <gavinmak@google.com>
This commit is contained in:
committed by
gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
e5bbb5c9e6
commit
b85e76a86a
+32
-6
@@ -14,6 +14,7 @@
|
||||
|
||||
import os
|
||||
|
||||
from git_command import git_require
|
||||
from git_command import GitCommand
|
||||
import platform_utils
|
||||
from repo_trace import Trace
|
||||
@@ -41,6 +42,12 @@ class GitRefs:
|
||||
self._EnsureLoaded()
|
||||
return self._phyref
|
||||
|
||||
@property
|
||||
def head(self) -> str:
|
||||
"""Return HEAD's symbolic target or detached object ID."""
|
||||
self._EnsureLoaded()
|
||||
return self._symref.get(HEAD) or self._phyref.get(HEAD, "")
|
||||
|
||||
def get(self, name):
|
||||
try:
|
||||
return self.all[name]
|
||||
@@ -87,8 +94,12 @@ class GitRefs:
|
||||
self._symref = {}
|
||||
self._mtime = {}
|
||||
|
||||
self._ReadRefs()
|
||||
self._ReadSymbolicRef(HEAD)
|
||||
root_refs_loaded = self._ReadRefs()
|
||||
if not root_refs_loaded or (
|
||||
HEAD not in self._phyref and HEAD not in self._symref
|
||||
):
|
||||
# --include-root-refs does not report an unborn HEAD.
|
||||
self._ReadSymbolicRef(HEAD)
|
||||
|
||||
scan = self._symref
|
||||
attempts = 0
|
||||
@@ -113,18 +124,32 @@ class GitRefs:
|
||||
"""Check if a ref_id is a null object ID."""
|
||||
return ref_id and all(ch == "0" for ch in ref_id)
|
||||
|
||||
def _ReadRefs(self) -> None:
|
||||
"""Read all references using git for-each-ref."""
|
||||
def _ReadRefs(self) -> bool:
|
||||
"""Read all references using git for-each-ref.
|
||||
|
||||
Returns:
|
||||
Whether root refs, including HEAD when it exists, were loaded.
|
||||
"""
|
||||
include_root_refs = git_require((2, 45, 0))
|
||||
cmd = [
|
||||
"for-each-ref",
|
||||
"--format=%(objectname)%00%(refname)%00%(symref)",
|
||||
]
|
||||
if include_root_refs:
|
||||
cmd.insert(1, "--include-root-refs")
|
||||
# Avoid caching volatile root refs such as ORIG_HEAD. HEAD and
|
||||
# refs/* are the only namespaces GitRefs exposes to callers.
|
||||
cmd.extend([HEAD, "refs"])
|
||||
p = GitCommand(
|
||||
None,
|
||||
["for-each-ref", "--format=%(objectname)%00%(refname)%00%(symref)"],
|
||||
cmd,
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
bare=True,
|
||||
gitdir=self._gitdir,
|
||||
)
|
||||
if p.Wait() != 0:
|
||||
return
|
||||
return False
|
||||
|
||||
for line in p.stdout.splitlines():
|
||||
ref_id, name, symref = line.split("\0")
|
||||
@@ -132,6 +157,7 @@ class GitRefs:
|
||||
self._symref[name] = symref
|
||||
elif ref_id and not self._IsNullRef(ref_id):
|
||||
self._phyref[name] = ref_id
|
||||
return include_root_refs
|
||||
|
||||
def _ReadSymbolicRef(self, name: str) -> None:
|
||||
"""Read a symbolic reference."""
|
||||
|
||||
+147
-2
@@ -17,6 +17,8 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any, List
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import utils_for_test
|
||||
@@ -24,7 +26,7 @@ import utils_for_test
|
||||
import git_refs
|
||||
|
||||
|
||||
def _run(repo, *args):
|
||||
def _run(repo: str, *args: str) -> str:
|
||||
return subprocess.run(
|
||||
["git", "-C", repo, *args],
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -34,7 +36,7 @@ def _run(repo, *args):
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def _init_repo(tmp_path, reftable=False):
|
||||
def _init_repo(tmp_path: Path, reftable: bool = False) -> str:
|
||||
repo = os.path.join(tmp_path, "repo")
|
||||
ref_format = "reftable" if reftable else "files"
|
||||
utils_for_test.init_git_tree(repo, ref_format=ref_format)
|
||||
@@ -61,6 +63,149 @@ def test_reads_refs(tmp_path, reftable):
|
||||
assert refs.get(f"refs/heads/{branch}") == head
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reftable", [False, True])
|
||||
def test_reads_detached_head(tmp_path: Path, reftable: bool) -> None:
|
||||
if reftable and not utils_for_test.supports_reftable():
|
||||
pytest.skip("reftable not supported")
|
||||
|
||||
repo = _init_repo(tmp_path, reftable=reftable)
|
||||
head = _run(repo, "rev-parse", "HEAD")
|
||||
_run(repo, "checkout", "--detach", head)
|
||||
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||
|
||||
assert refs.symref("HEAD") == ""
|
||||
assert refs.head == head
|
||||
assert refs.get("HEAD") == head
|
||||
|
||||
|
||||
def test_reads_head_with_root_refs_in_one_command(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Git 2.45 and newer include HEAD in the ref snapshot."""
|
||||
head = "1" * 40
|
||||
commands = []
|
||||
|
||||
class FakeGitCommand:
|
||||
def __init__(
|
||||
self, _project: Any, cmdv: List[str], **_kwargs: Any
|
||||
) -> None:
|
||||
commands.append(cmdv)
|
||||
self.stdout = (
|
||||
f"{head}\0HEAD\0refs/heads/main\n"
|
||||
f"{head}\0refs/heads/main\0\n"
|
||||
)
|
||||
|
||||
def Wait(self) -> int:
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||
monkeypatch.setattr(git_refs, "git_require", lambda _version: True)
|
||||
refs = git_refs.GitRefs("/nonexistent")
|
||||
with mock.patch.object(refs, "_ReadSymbolicRef") as read_head:
|
||||
assert refs.get("HEAD") == head
|
||||
|
||||
assert commands == [
|
||||
[
|
||||
"for-each-ref",
|
||||
"--include-root-refs",
|
||||
"--format=%(objectname)%00%(refname)%00%(symref)",
|
||||
"HEAD",
|
||||
"refs",
|
||||
]
|
||||
]
|
||||
read_head.assert_not_called()
|
||||
|
||||
|
||||
def test_root_ref_snapshot_falls_back_for_unborn_head(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""An unborn HEAD still uses symbolic-ref after the ref snapshot."""
|
||||
|
||||
class FakeGitCommand:
|
||||
def __init__(
|
||||
self, _project: Any, _cmdv: List[str], **_kwargs: Any
|
||||
) -> None:
|
||||
self.stdout = ""
|
||||
|
||||
def Wait(self) -> int:
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||
monkeypatch.setattr(git_refs, "git_require", lambda _version: True)
|
||||
refs = git_refs.GitRefs("/nonexistent")
|
||||
|
||||
def read_head(name: str) -> None:
|
||||
assert name == "HEAD"
|
||||
refs._symref[name] = "refs/heads/main"
|
||||
|
||||
monkeypatch.setattr(refs, "_ReadSymbolicRef", read_head)
|
||||
|
||||
assert refs.symref("HEAD") == "refs/heads/main"
|
||||
|
||||
|
||||
def test_old_git_keeps_separate_head_fallback(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Git before 2.45 uses the original for-each-ref and HEAD calls."""
|
||||
head = "1" * 40
|
||||
commands = []
|
||||
|
||||
class FakeGitCommand:
|
||||
def __init__(
|
||||
self, _project: Any, cmdv: List[str], **_kwargs: Any
|
||||
) -> None:
|
||||
commands.append(cmdv)
|
||||
self.stdout = f"{head}\0refs/heads/main\0\n"
|
||||
|
||||
def Wait(self) -> int:
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||
monkeypatch.setattr(git_refs, "git_require", lambda _version: False)
|
||||
refs = git_refs.GitRefs("/nonexistent")
|
||||
|
||||
def read_head(name: str) -> None:
|
||||
assert name == "HEAD"
|
||||
refs._symref[name] = "refs/heads/main"
|
||||
|
||||
monkeypatch.setattr(refs, "_ReadSymbolicRef", read_head)
|
||||
|
||||
assert refs.get("HEAD") == head
|
||||
assert commands == [
|
||||
[
|
||||
"for-each-ref",
|
||||
"--format=%(objectname)%00%(refname)%00%(symref)",
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def test_for_each_ref_failure_falls_back_to_symbolic_ref(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""When for-each-ref fails, HEAD is still resolved via symbolic-ref."""
|
||||
|
||||
class FakeGitCommand:
|
||||
def __init__(
|
||||
self, _project: Any, _cmdv: List[str], **_kwargs: Any
|
||||
) -> None:
|
||||
self.stdout = ""
|
||||
|
||||
def Wait(self) -> int:
|
||||
return 1
|
||||
|
||||
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||
monkeypatch.setattr(git_refs, "git_require", lambda _version: True)
|
||||
refs = git_refs.GitRefs("/nonexistent")
|
||||
|
||||
def read_head(name: str) -> None:
|
||||
assert name == "HEAD"
|
||||
refs._symref[name] = "refs/heads/main"
|
||||
|
||||
monkeypatch.setattr(refs, "_ReadSymbolicRef", read_head)
|
||||
|
||||
assert refs.symref("HEAD") == "refs/heads/main"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reftable", [False, True])
|
||||
def test_updates_when_refs_change(tmp_path, reftable):
|
||||
if reftable and not utils_for_test.supports_reftable():
|
||||
|
||||
Reference in New Issue
Block a user