Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions templates/family-root/scripts/validate-family.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,8 +646,14 @@ def _checked_out(checkout: Path, path: str) -> bool:
# are separate templates, and this file is a pinned copy a holder re-copies
# one row at a time (#610's skew rule). `tests/test_validate_family_
# unreadable.py` holds `_member_git_dir` to the gate's `_leg_git_dir` over
# every shape of `gitdir: ` line the gate's rows write. A `HEAD` file itself
# at mode 000 is not asked here, since the gate does not ask it either.
# every shape of `gitdir: ` line the gate's rows write, and holds the open
# of a member's `HEAD` to the gate's open of a leg's.
#
# A `HEAD` FILE ITSELF AT MODE 000 STATS (#1256), so the `stat` of `HEAD`
# passed it to git, which could not read it: the run ended
# `member-commit-unresolvable`, with a fetch that cannot help. It is opened
# as the gate opens a leg's (#1222), and one this user cannot open is
# `member-unreadable`, naming that `HEAD`.

def _gitfile_opens(path: Path) -> bool:
"""Whether a member's `.git` that is a regular file, a gitfile, can be
Expand All @@ -656,7 +662,11 @@ def _gitfile_opens(path: Path) -> bool:
`OSError` of an open the operating system REFUSED raised, and True for
a `.git` that is no regular file, a checkout's own directory, which git
reads itself. Only a regular file is opened, since the open of a FIFO
waits for a writer."""
waits for a writer.

A MEMBER'S GIT DIRECTORY'S `HEAD` IS ASKED THE SAME WAY, for the same
reason (#1256), as the gate asks a leg's (#1222): see
`_refuse_an_unreadable_git_dir`."""
if not _is_a(path, stat.S_ISREG):
return True
try:
Expand Down Expand Up @@ -706,7 +716,18 @@ def _refuse_an_unreadable_git_dir(checkout: Path, path: str) -> None:
checkout's own directory, `.git/modules/<path>` for the one an absorbed
member's gitfile names, and whole where it is outside the holder. A git
directory with no `HEAD`, or a gitfile that names none, is left to git,
which answers for it as it always did."""
which answers for it as it always did.

A `HEAD` THIS USER CANNOT OPEN STATS (#1256, as #1222 found of a leg's),
so a `HEAD` at mode 000 passed, and git, which could not read it, ended
the run in `member-commit-unresolvable`, which says the pinned commit
is not in that member's object store and prescribes a fetch. A `HEAD`
that is there is opened as a gitfile is (`_gitfile_opens`), as the
gate opens a leg's: only where it is a regular file, since the open of
a FIFO waits for a writer, and an open that finds nothing is a git
directory with no `HEAD`. One this user cannot open is named,
`.git/modules/<path>/HEAD` or `<path>/.git/HEAD`, with the same
remedy."""
dotgit = checkout / ".git"
named = f"{path}/.git"
try:
Expand All @@ -717,9 +738,14 @@ def _refuse_an_unreadable_git_dir(checkout: Path, path: str) -> None:
found = Path(os.path.normpath(gitdir))
try:
named = found.relative_to(holder).as_posix()
named_head = f"{named}/HEAD"
except ValueError:
named = str(found)
_is_a(gitdir / "HEAD", _any_kind)
named_head = str(found / "HEAD")
head = gitdir / "HEAD"
if _is_a(head, _any_kind):
named = named_head
_gitfile_opens(head)
except OSError as exc:
raise _unreadable(MEMBER_UNREADABLE, named, exc) from exc

Expand Down Expand Up @@ -1724,9 +1750,10 @@ def _check_member_tree(member: Path, pin: dict, path: str, commit: str,
that is there. So is a `.git` gitfile this user cannot open
(`_gitfile_read`, #1221), and a git directory this user cannot search,
the `.git` itself or the one a gitfile names
(`_refuse_an_unreadable_git_dir`, #1221): each passed the `stat` and
reached git, and the run ended `member-commit-unresolvable`, which says
the pinned commit is missing and prescribes a fetch.
(`_refuse_an_unreadable_git_dir`, #1221), or whose `HEAD` it cannot
open (#1256): each passed the `stat` and reached git, and the run ended
`member-commit-unresolvable`, which says the pinned commit is missing
and prescribes a fetch.

The pinned commit's lookup and the digest are read inside `_git_runs`,
and nothing else here is (#928). All of this ran inside one, the `stat`
Expand Down
199 changes: 197 additions & 2 deletions tests/test_validate_family_unreadable.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,15 @@
each passed the `stat` and were refused `member-commit-unresolvable`, which
says the pinned commit is missing. Each is `member-unreadable` now, naming
the part, and a parity row holds `_member_git_dir` to the gate's
`_leg_git_dir`. A `HEAD` file itself at mode 000 is not asked, since the
gate does not ask it either.
`_leg_git_dir`.

A MEMBER GIT DIRECTORY'S `HEAD` THIS USER CANNOT OPEN (#1256), as #1222
found of a leg's in the gate: a `HEAD` at mode 000, under `.git/modules/`
or in a `.git` directory, stats, so #1221's `stat` of it passed, and git,
which could not read it, ended the run in `member-commit-unresolvable`
with a fetch. It is opened as the gate opens a leg's, only where it is a
regular file, and refused `member-unreadable`, naming that `HEAD`; a
parity row holds the name to the gate's.

WHAT IS ON DISK IS ASKED ONLY WHERE IT IS SAID (round 2, r956's SHOULD 1
on #956): `_forced` and `_staged_over` ask git first, and where HEAD
Expand Down Expand Up @@ -951,6 +958,107 @@ def test_a_member_git_directory_with_no_head_is_left_to_git(
assert code == 0, capsys.readouterr().err


# --- #1256: a member git directory's `HEAD` this user cannot open -----------

#: `(the state, its member git directory's `HEAD`)`: a gitfile member's,
#: under `.git/modules/`, and a `.git` directory's (#1256). Each `HEAD` is
#: the path the gate names, relative to the holder.
HEADS = [
pytest.param(as_made, f"{MEMBER_GIT_DIR}/HEAD", id="gitfile-member"),
pytest.param(own_git_directory, f"{ROW}/.git/HEAD",
id="git-directory-member"),
]


def worktree_head(holder: Path) -> str:
"""The `HEAD` of the git directory `a_worktree`'s gitfile names, outside
the holder, as the gate names it: whole."""
line = (holder / ROW / ".git").read_bytes().decode().strip()
return os.path.join(os.path.normpath(line[len("gitdir: "):]), "HEAD")


@needs_a_mode
@pytest.mark.parametrize("made,head", HEADS)
def test_a_member_git_directory_head_this_user_cannot_open_is_refused(
holder, request, made, head):
"""THE ISSUE'S CELLS (#1256), on this Python: the `HEAD` of the member's
git directory at mode 000. It stats, so #1221's `stat` of it passed,
and git, which could not read it, ended the run in
`member-commit-unresolvable`, "the pinned commit ... is not in that
member's object store", with a fetch. It is `member-unreadable`, exit
2, naming that `HEAD` with the `chmod` cure, and with the mode back the
gate passes, so the cure is the whole of it."""
extra = made(holder, request)
with mode_of(holder / head, 0):
result = validate(holder, *extra)
assert_refused(result.returncode, result.stderr, MEMBER_UNREADABLE, head)
assert "member-commit-unresolvable" not in result.stderr
again = validate(holder, *extra)
assert again.returncode == 0, again.stderr


@needs_a_mode
def test_a_linked_worktree_head_this_user_cannot_open_is_named_whole(
holder, request):
"""The same for a linked worktree's `HEAD`, in the git directory its
gitfile names outside the holder: named whole, as the gate names that
directory (#1221)."""
a_worktree(holder, request)
head = worktree_head(holder)
with mode_of(Path(head), 0):
result = validate(holder)
assert_refused(result.returncode, result.stderr, MEMBER_UNREADABLE, head)
assert "member-commit-unresolvable" not in result.stderr
assert validate(holder).returncode == 0


@pytest.mark.parametrize("error", CAUSES)
@pytest.mark.parametrize("made,head", HEADS)
def test_a_member_git_directory_head_whose_open_is_refused_is_refused(
holder, request, monkeypatch, capsys, error, made, head):
"""The same in-process, so on every platform and as root: the open of
the `HEAD` refused, each cause with its own cure (`assert_refused`)."""
extra = made(holder, request)
module = gate(holder, monkeypatch)
failing(monkeypatch, "open", "HEAD", error)
code = module.main(["--root", str(holder), *extra])
err = capsys.readouterr().err
assert_refused(code, err, MEMBER_UNREADABLE, head, error.strerror)
assert "member-commit-unresolvable" not in err


def test_the_member_tree_asked_alone_names_a_head_whole_outside(
holder, request, monkeypatch):
"""`_check_member_tree` asked alone, the open of the `HEAD` refused: it
is named relative to the holder, and whole where its git directory is
outside the holder, a linked worktree's."""
module = gate(holder, monkeypatch)
failing(monkeypatch, "open", "HEAD",
PermissionError(errno.EACCES, "Permission denied"))
args = (holder / ROW, {}, ROW, "0" * 40, "IRRS", module.Report(), "")
refusal = refusal_of(module, module._check_member_tree, *args)
assert refusal.code == MEMBER_UNREADABLE
assert refusal.detail == (f"{MEMBER_GIT_DIR}/HEAD: cannot be read "
"(Permission denied)")
a_worktree(holder, request)
refusal = refusal_of(module, module._check_member_tree, *args)
assert refusal.detail == (f"{module.spelled(worktree_head(holder))}: "
"cannot be read (Permission denied)")


def test_a_member_head_gone_when_it_is_opened_is_left_to_git(
holder, monkeypatch, capsys):
"""Asked as `_is_a` asks a `stat`: an open of the `HEAD` that finds
nothing at the name is a git directory with no `HEAD`, which git answers
for, and not a file this user cannot read. Here git reads it, so the run
passes."""
module = gate(holder, monkeypatch)
failing(monkeypatch, "open", "HEAD",
FileNotFoundError(errno.ENOENT, "No such file or directory"))
code = module.main(["--root", str(holder)])
assert code == 0, capsys.readouterr().err


@pytest.fixture(scope="module")
def sources():
"""This working tree's `validate-family.py` and the gate it copies, each
Expand Down Expand Up @@ -1023,3 +1131,90 @@ def test_a_member_git_is_opened_as_the_gate_opens_a_leg_s(
failing(monkeypatch, "open", ".git", OSError(errno.EIO, "opened"))
assert sources.family._gitfile_opens(dotgit) is True
assert sources.gate._gitfile_opens(dotgit) is True


def member_git_directory(tmp_path: Path, own_directory: bool) -> Path:
"""A member checkout at `ROW` under `tmp_path`, the holder, and its git
directory, which holds a `HEAD`: its own `.git` directory, or the one
under `.git/modules/` its gitfile names, as `family.py add` writes it.
Returns the git directory."""
checkout = tmp_path / ROW
checkout.mkdir(parents=True)
if own_directory:
gitdir = checkout / ".git"
gitdir.mkdir()
else:
gitdir = tmp_path / MEMBER_GIT_DIR
gitdir.mkdir(parents=True)
(checkout / ".git").write_bytes(
b"gitdir: ../../" + MEMBER_GIT_DIR.encode() + b"\n")
(gitdir / "HEAD").write_bytes(b"ref: refs/heads/main\n")
return gitdir


#: Each shape of member git directory `member_git_directory` makes.
OWN_DIRECTORY = [
pytest.param(False, id="gitfile-member"),
pytest.param(True, id="git-directory-member"),
]


@pytest.mark.parametrize("error", CAUSES)
@pytest.mark.parametrize("own_directory", OWN_DIRECTORY)
def test_a_member_head_is_named_as_the_gate_names_a_leg_s(
sources, tmp_path, monkeypatch, own_directory, error):
"""PARITY (#1256): the open of the `HEAD` refused, the family gate's
line is the leg gate's (#1222), naming the same `HEAD` relative to the
holder, `.git/modules/members/IRRS/HEAD` or `members/IRRS/.git/HEAD`,
with the system's reason. Each code and remedy is its own."""
gitdir = member_git_directory(tmp_path, own_directory)
failing(monkeypatch, "open", "HEAD", error)
family_refusal = refusal_of(
sources.family, sources.family._refuse_an_unreadable_git_dir,
tmp_path / ROW, ROW)
gate_refusal = refusal_of(
sources.gate, sources.gate._refuse_an_unreadable_git_dir, tmp_path,
tmp_path / ROW / ".git")
head = (gitdir / "HEAD").relative_to(tmp_path).as_posix()
assert family_refusal.code == MEMBER_UNREADABLE
assert family_refusal.detail == (
f"{head}: cannot be read ({error.strerror})")
assert gate_refusal.detail == family_refusal.detail


@pytest.mark.parametrize("own_directory", OWN_DIRECTORY)
@pytest.mark.parametrize("make", [
pytest.param(lambda head: head.mkdir(), id="directory"),
pytest.param(lambda head: os.mkfifo(head), id="fifo",
marks=pytest.mark.skipif(not hasattr(os, "mkfifo"),
reason="no FIFO on this platform")),
])
def test_a_member_head_that_is_no_regular_file_is_not_opened(
sources, tmp_path, monkeypatch, own_directory, make):
"""Only a `HEAD` that is a regular file is opened (#1256), in the family
gate as in the leg gate (#1222): one that is anything else is git's to
answer for, and the open of a FIFO would wait for a writer. An open of
either fails the row rather than hanging it."""
gitdir = member_git_directory(tmp_path, own_directory)
head = gitdir / "HEAD"
head.rename(gitdir / "HEAD-aside")
make(head)
failing(monkeypatch, "open", "HEAD", OSError(errno.EIO, "opened"))
assert sources.family._refuse_an_unreadable_git_dir(tmp_path / ROW,
ROW) is None
assert sources.gate._refuse_an_unreadable_git_dir(
tmp_path, tmp_path / ROW / ".git") is None


def test_a_member_head_no_system_call_can_name_is_left_to_git(sources,
tmp_path):
"""A `gitdir: ` line that holds a NUL names a git directory no system
call can take, so its `HEAD` is nothing there, as `_is_a` answers a
name the system cannot take: nothing is opened, no `ValueError` ends
the run, and git refuses the gitfile as it always did (#1256, as the
gate's #1222 row asks a leg's)."""
dotgit = tmp_path / ROW / ".git"
dotgit.parent.mkdir(parents=True)
dotgit.write_bytes(b"gitdir: ../m" + chr(0).encode("ascii") + b"x")
assert sources.family._refuse_an_unreadable_git_dir(tmp_path / ROW,
ROW) is None
Loading