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
26 changes: 16 additions & 10 deletions docs/repository-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,36 @@ which retargets the open PRs; everyone then updates their clones.
`vX.Y.Z` first.
- **A repository with no final tags and nothing published** tags `v0.0.0` on the last
commit that should not appear in the changelog. The first release is then `0.1.0`
(or `1.0.0` when started with **major**). Pushing a tag runs the workflows of the
tagged commit: if one of them publishes on tag pushes, disable it while you push
(`gh workflow disable`, then `gh workflow enable`).
(or `1.0.0` when started with **major**).

```bash
git tag -a v0.0.0 -m "Start of the changelog" <commit>
git push origin v0.0.0
just changelog # what the first release will contain
```

Pushing a tag, `v0.0.0` or a published version's, runs the workflows of the tagged
commit: if one of them publishes on tag pushes, disable it while you push
(`gh workflow disable`, then `gh workflow enable`).

**The releases before the baseline** belong in `CHANGELOG.md` too. Generate their
sections in the PR that adds the stubs (step 4), with the `cliff.toml` of the
release-tools tag the stubs pin, from a worktree at the baseline tag so that later
commits stay out. The command replaces an existing `CHANGELOG.md`: keep or merge a
hand-written one first.

The subshell removes the worktree and the temporary directory however it ends:

```bash
tmp=$(mktemp -d)
git worktree add "$tmp/base" vX.Y.Z # the baseline tag
gh api -H "Accept: application/vnd.github.raw" \
"repos/Algorithmiq/release-tools/contents/cliff.toml?ref=<the pinned tag>" > "$tmp/cliff.toml"
GITHUB_REPOSITORY=$REPO uvx --from git-cliff==2.14.2 git-cliff \
--config "$tmp/cliff.toml" --repository "$tmp/base" --output CHANGELOG.md
git worktree remove "$tmp/base"
(
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"; git worktree prune' EXIT
git worktree add "$tmp/base" vX.Y.Z # the baseline tag
gh api -H "Accept: application/vnd.github.raw" \
"repos/Algorithmiq/release-tools/contents/cliff.toml?ref=<the pinned tag>" > "$tmp/cliff.toml"
GITHUB_REPOSITORY=$REPO uvx --from git-cliff==2.14.2 git-cliff \
--config "$tmp/cliff.toml" --repository "$tmp/base" --output CHANGELOG.md
)
```

Each final tag gets a `## X.Y.Z (date)` section; pre-release tags are skipped. Titles
Expand Down
67 changes: 56 additions & 11 deletions scripts/check_internal_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def git(*args: str) -> str:
).stdout


def non_public_repositories() -> set[str]:
"""Return the organisation's non-public repositories; none if gh can't list them."""
def non_public_repositories() -> set[str] | None:
"""Return the organisation's non-public repositories; None if gh can't list them."""
try:
listing = subprocess.run(
[
Expand All @@ -66,11 +66,13 @@ def non_public_repositories() -> set[str]:
capture_output=True,
text=True,
).stdout
except (OSError, subprocess.CalledProcessError):
return set()
return {
repo["name"] for repo in json.loads(listing) if repo["visibility"] != "PUBLIC"
}
return {
repo["name"]
for repo in json.loads(listing)
if repo["visibility"] != "PUBLIC"
}
except (OSError, subprocess.CalledProcessError, ValueError, KeyError, TypeError):
return None


def allowed_names() -> set[str]:
Expand All @@ -92,8 +94,11 @@ def pushed_range() -> str:
# Each commit is diffed against its first parent, merges included (a conflict
# resolution can add lines of its own), rather than the range as a whole, where what one
# commit adds and a later one removes cancels out. %x00 starts the line of each commit:
# no line of a diff or of --name-only starts with NUL.
# no line of a diff or of --name-only starts with NUL. Paths are written out as they
# are, except those git still quotes (see unquote).
LOG_PER_COMMIT = (
"-c",
"core.quotePath=false",
"log",
"--format=%x00%H",
"--diff-merges=first-parent",
Expand All @@ -105,6 +110,36 @@ def pushed_range() -> str:
)


# An escape in a path git quotes: three octal digits, a letter, a quote or a backslash.
GIT_ESCAPE = re.compile(rb"\\([0-7]{3}|.)")
GIT_ESCAPES = {
b"a": b"\a",
b"b": b"\b",
b"t": b"\t",
b"n": b"\n",
b"v": b"\v",
b"f": b"\f",
b"r": b"\r",
}


def unquote(path: str) -> str:
"""Return PATH as it is named, when git wrote it quoted.

With core.quotePath=false git still quotes a path that holds a double quote, a
backslash or a control character: in double quotes, with C-style escapes.
"""
quoted = re.fullmatch(r'"(.+)"', path)
if not quoted:
return path

def unescape(match: re.Match[bytes]) -> bytes:
code = match[1]
return bytes([int(code, 8)]) if code.isdigit() else GIT_ESCAPES.get(code, code)

return GIT_ESCAPE.sub(unescape, quoted[1].encode()).decode(errors="replace")


def added_lines(revisions: str) -> list[tuple[str, str]]:
"""Return (where, line) for every line a commit of REVISIONS adds."""
found: list[tuple[str, str]] = []
Expand All @@ -119,8 +154,10 @@ def added_lines(revisions: str) -> list[tuple[str, str]]:
in_header = False
elif in_header:
# Only here: an added line "++ x" also reads "+++ x" in the hunk.
# A path with a space ends in a tab there, for patch(1).
if line.startswith("+++ "):
path = line.removeprefix("+++ ").removeprefix("b/")
name = line.removeprefix("+++ ").removesuffix("\t")
path = unquote(name).removeprefix("b/")
elif line.startswith("+"):
found.append((f"commit {commit} {path}", line[1:]))
return found
Expand All @@ -134,7 +171,8 @@ def changed_paths(revisions: str) -> list[tuple[str, str]]:
if line.startswith("\0"):
commit = line[1:11]
elif line:
found.append((f"commit {commit} {line}", line))
path = unquote(line)
found.append((f"commit {commit} {path}", path))
return found


Expand Down Expand Up @@ -163,13 +201,20 @@ def leaks(texts: list[tuple[str, str]], names: set[str]) -> list[str]:
def main() -> int:
"""Check the push; return the exit status."""
names = non_public_repositories()
if not names:
if names is None:
print(
"check-internal-names: gh can't list the organisation's non-public "
"repositories; skipped.",
file=sys.stderr,
)
return 0
if not names:
print(
"check-internal-names: gh lists no non-public repositories of the "
"organisation; nothing to check.",
file=sys.stderr,
)
return 0
allowed = allowed_names()
found = leaks(
pushed_text(pushed_range()), {n for n in names if n.lower() not in allowed}
Expand Down
Loading
Loading