mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-10-22 05:08:25 +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:
1 parent
e5bbb5c9e6
commit
b85e76a86a
2 files changed
+179
-8
No files matched your search
+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