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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1210,7 +1210,7 @@ one verdict, and its exit code:
| `NOT A SHAPE ROOT` | 2 | neither manifest is there — adopt, or scaffold |

Exit 3 is usage or environment (a flag argparse rejects is exit 2) — nothing about
it is a statement about the tree you pointed at: no such path, a refused
it is a statement about the tree you pointed at: no such path, a path this user cannot search, a refused
`--placement-plan`, a `shape-doctor.py` outside a checkout of the standard, or
**this** checkout unable to answer (`CANNOT ANSWER`): a pin's commit it lacks, a
partial clone missing what is compared, a read that fails, a validator of its
Expand Down
11 changes: 6 additions & 5 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,12 @@ over <path> (default `.`) and prints one row per check and ONE verdict:
`COMPLIANT`, `COMPLIANT, SHAPE BEHIND`, `DRIFTED`, `MISPLACED`, `INVALID`,
`NOT A SHAPE ROOT` or `CANNOT ANSWER`. Every finding names the command that
fixes it. Exit 0 compliant, 1 findings, 2 not a shape root, 3 when it cannot
answer (`CANNOT ANSWER`, no such <path>, a `--placement-plan` it cannot
write); this command's own refusals exit 2. `--json` is the same report as
one JSON object. The doctor writes no file but the `--placement-plan <file>`
you name, at the path you give (a relative <file> lands in the directory you
ran this from, even inside the repository), and moves nothing.
answer (`CANNOT ANSWER`, no such <path>, a <path> this user cannot search, a
`--placement-plan` it cannot write); this command's own refusals exit 2.
`--json` is the same report as one JSON object. The doctor writes no file but
the `--placement-plan <file>` you name, at the path you give (a relative
<file> lands in the directory you ran this from, even inside the repository),
and moves nothing.

Run beside a checkout of the standard, it uses that one. Run from anywhere
else, it clones the standard into a temporary directory first and KEEPS it,
Expand Down
6 changes: 3 additions & 3 deletions docs/handbook.html
Original file line number Diff line number Diff line change
Expand Up @@ -1548,7 +1548,7 @@ <h2>Bootstrap is COPIED into the project, not fetched</h2>

<section id="current">

<!-- reads README.md § Keeping a project's shape current: blob df780eb5a96cdcaf9b30a8ac8162bca00f90405b -->
<!-- reads README.md § Keeping a project's shape current: blob 5edbfec7e824a4064e9ff5331da208f21cab7423 -->

<p class="eyebrow">Reference</p>
<h2>Keeping a project&rsquo;s shape current</h2>
Expand Down Expand Up @@ -1610,7 +1610,7 @@ <h2>Keeping a project&rsquo;s shape current</h2>

<section id="doctor">

<!-- reads README.md § Keeping a project's shape current: blob df780eb5a96cdcaf9b30a8ac8162bca00f90405b -->
<!-- reads README.md § Keeping a project's shape current: blob 5edbfec7e824a4064e9ff5331da208f21cab7423 -->

<p class="eyebrow">Reference</p>
<h2>Is this repository compliant? <code>openRepoShape --doctor</code></h2>
Expand Down Expand Up @@ -1671,7 +1671,7 @@ <h2>Is this repository compliant? <code>openRepoShape --doctor</code></h2>
</tbody>
</table>
<p>Exit 3 is usage or environment (a flag argparse rejects is exit 2) &mdash; nothing about
it is a statement about the tree you pointed at: no such path, a refused
it is a statement about the tree you pointed at: no such path, a path this user cannot search, a refused
<code>--placement-plan</code>, a <code>shape-doctor.py</code> not in a checkout of the
standard, or <em>this</em> checkout unable to answer (<code>CANNOT ANSWER</code>): a
pin&rsquo;s commit it lacks, a partial clone missing what is compared, a read that fails, a
Expand Down
11 changes: 6 additions & 5 deletions openRepoShape
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,12 @@ over <path> (default `.`) and prints one row per check and ONE verdict:
`COMPLIANT`, `COMPLIANT, SHAPE BEHIND`, `DRIFTED`, `MISPLACED`, `INVALID`,
`NOT A SHAPE ROOT` or `CANNOT ANSWER`. Every finding names the command that
fixes it. Exit 0 compliant, 1 findings, 2 not a shape root, 3 when it cannot
answer (`CANNOT ANSWER`, no such <path>, a `--placement-plan` it cannot
write); this command's own refusals exit 2. `--json` is the same report as
one JSON object. The doctor writes no file but the `--placement-plan <file>`
you name, at the path you give (a relative <file> lands in the directory you
ran this from, even inside the repository), and moves nothing.
answer (`CANNOT ANSWER`, no such <path>, a <path> this user cannot search, a
`--placement-plan` it cannot write); this command's own refusals exit 2.
`--json` is the same report as one JSON object. The doctor writes no file but
the `--placement-plan <file>` you name, at the path you give (a relative
<file> lands in the directory you ran this from, even inside the repository),
and moves nothing.

Run beside a checkout of the standard, it uses that one. Run from anywhere
else, it clones the standard into a temporary directory first and KEEPS it,
Expand Down
47 changes: 43 additions & 4 deletions scripts/shape_advisory.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@
from __future__ import annotations

import datetime as _dt
import errno
import os
import re
import stat
import sys
from pathlib import Path

Expand Down Expand Up @@ -326,6 +328,41 @@ def as_dict(self) -> dict:
"leg": self.leg}


#: The `OSError`s that mean NOTHING IS THERE, where any other is a `stat` the
#: operating system refused: no such file, a part of the path that is not a
#: directory, a bad descriptor, a symlink loop, and Windows' "not ready",
#: "invalid name" and "cannot resolve" -- the ones `Path.is_file()` answered
#: False to on Python 3.9 to 3.13. The same set as `validate-pins.py`'s
#: `_ABSENT_ERRNOS` and `shape-doctor.py`'s `ABSENT_ERRNOS` (#430's rule).
#: Kept here, not in `repo_shape.py`: that file is a shape copy every root
#: pins, and this one is not.
_ABSENT_ERRNOS = (errno.ENOENT, errno.ENOTDIR, errno.EBADF, errno.ELOOP)
_ABSENT_WINERRORS = (21, 123, 1921)


def _is_a(path: Path, kind) -> bool:
"""Whether `path` is there and of `kind` (`stat.S_ISREG`, or any mode),
raising the `OSError` of a `stat` the operating system REFUSED, as
`validate-pins.py`'s `_is_a` asks it (#430).

NOT `Path.is_file()` OR `Path.exists()`, whose answer depends on the
Python: up to 3.13 they raise a refused `stat`, and from 3.14 they answer
False to any `OSError`, so a `single-repository.yaml` in a folder this
user cannot search read as no record at all, and the advisory said
nothing of a file it could not read. A name no system takes is nothing
there, as `is_file()` answered it."""
try:
mode = path.stat().st_mode
except OSError as exc:
if (exc.errno in _ABSENT_ERRNOS
or getattr(exc, "winerror", None) in _ABSENT_WINERRORS):
return False
raise
except ValueError:
return False
return kind(mode)


class Tree:
"""The files of one repository's root, read from the disk or a commit.

Expand All @@ -342,7 +379,9 @@ class Tree:
file, or None when there is no file to read. An undecodable file RAISES
`UnicodeDecodeError`, because "it is there and could not be read" is a
different answer from "it is not there", and the record reports the
first.
first. So does a file whose `stat` the operating system REFUSES: `kind()`
raises it (`_is_a`, #430), and every reader's net reads the file as there
and unreadable, on every Python.
"""

def __init__(self, root: Path, commit: str | None = None):
Expand All @@ -352,9 +391,9 @@ def __init__(self, root: Path, commit: str | None = None):
def kind(self, rel: str) -> str | None:
if self.commit is None:
path = self.root / rel
if path.is_file():
return "file"
return "dir" if path.exists() else None
if not _is_a(path, lambda _mode: True):
return None
return "file" if _is_a(path, stat.S_ISREG) else "dir"
try:
found = git_out(["cat-file", "-t", f"{self.commit}:{rel}"],
cwd=self.root)
Expand Down
Loading
Loading