mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
`GH.get_changed_files` used `gh pr view --json files --jq '.files[].path'`,
which reports only the destination path of a renamed file. Renaming a file out
of a protected folder therefore bypassed the read-only docs guard: no path in
the list started with the protected prefix, although the rename deletes the
file from the copied tree. The same blind spot made job filtering miss renames
out of a watched directory.
Switch to the REST files API (`pulls/{n}/files` for PRs, `commits/{sha}`
for branches) and emit `previous_filename` alongside `filename`, so both
sides of a rename appear in the list. Rename sources do not exist in the
checkout at `HEAD`, exactly like deleted files, which were always included;
all consumers of the list either match strings or check file existence first.
The REST endpoint with `--paginate` also lifts the 100-file cap of
`gh pr view --json files`.
Also document in `check_readonly_copies` that the input must cover both
sides of renames, and that standalone callers should pipe
`git diff --name-only --no-renames`.
Co-Authored-By: Claude Fable 5 <[email protected]>
107 lines
3.6 KiB
Python
107 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Guard for one-way-synced docs folders.
|
|
|
|
Some docs folders in this repo are *copies* whose canonical source lives in
|
|
another repository (e.g. ``docs/integrations/language-clients/python`` is owned
|
|
by ``clickhouse-connect``). Editing the copy here is almost always a mistake --
|
|
the next sync overwrites it. This check fails such edits and points the
|
|
contributor at the canonical source.
|
|
|
|
The protected folders and their owning repos are declared in
|
|
``readonly_copies.json`` next to this file. The check matches a list of changed
|
|
files (repo-relative paths, as they appear in the PR diff) against those
|
|
folders.
|
|
|
|
The input must cover *both* sides of renames: a rename out of a protected
|
|
folder is an edit to it (a deletion), and only the source path falls under the
|
|
protected prefix. ``Info().get_changed_files()``, which the Praktika job feeds
|
|
in, includes rename sources; standalone, pipe ``git diff --name-only
|
|
--no-renames`` (with rename detection disabled a rename is listed as a
|
|
deletion plus an addition, so both paths appear).
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
CONFIG_PATH = os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)), "readonly_copies.json"
|
|
)
|
|
|
|
|
|
def load_sources(config_path):
|
|
with open(config_path, encoding="utf-8") as f:
|
|
return json.load(f)["sources"]
|
|
|
|
|
|
def _normalize(path):
|
|
return path.strip().removeprefix("./")
|
|
|
|
|
|
def find_violations(changed_files, sources):
|
|
"""Return [(source, [offending files])] for every source that was touched."""
|
|
normalized = [_normalize(f) for f in changed_files]
|
|
violations = []
|
|
for src in sources:
|
|
prefix = src["path"].rstrip("/")
|
|
hit = [f for f in normalized if f == prefix or f.startswith(prefix + "/")]
|
|
if hit:
|
|
violations.append((src, sorted(hit)))
|
|
return violations
|
|
|
|
|
|
def format_message(violations):
|
|
lines = []
|
|
for src, files in violations:
|
|
owner = src.get("repo") or src.get("url", "")
|
|
subdir = src.get("subdir")
|
|
where = f" (in its `{subdir}` folder)" if subdir else ""
|
|
lines.append(f"'{src['path']}' is a one-way copy from {owner}{where}.")
|
|
if src.get("url"):
|
|
lines.append(f" Canonical source: {src['url']}")
|
|
lines.append(
|
|
" Edit it there -- changes made directly here are overwritten by the "
|
|
"next sync."
|
|
)
|
|
lines.append(" Offending files:")
|
|
lines.extend(f" {f}" for f in files)
|
|
lines.append("")
|
|
return "\n".join(lines).rstrip()
|
|
|
|
|
|
def check_readonly_copies(changed_files, config_path=CONFIG_PATH):
|
|
"""Return True if no read-only copied docs were edited, else False."""
|
|
sources = load_sources(config_path)
|
|
violations = find_violations(changed_files, sources)
|
|
if not violations:
|
|
print("OK: no edits to read-only copied docs.")
|
|
return True
|
|
print("Edits to read-only copied docs detected:\n")
|
|
print(format_message(violations))
|
|
return False
|
|
|
|
|
|
def _read_changed_files(args):
|
|
if args.changed_file:
|
|
return args.changed_file
|
|
return [line.strip() for line in sys.stdin.read().splitlines() if line.strip()]
|
|
|
|
|
|
def main(argv=None):
|
|
p = argparse.ArgumentParser(description=__doc__)
|
|
p.add_argument("--config", default=CONFIG_PATH, help="Path to readonly_copies.json.")
|
|
p.add_argument(
|
|
"--changed-file",
|
|
action="append",
|
|
default=[],
|
|
help="A changed file path (repeatable). If omitted, newline-separated "
|
|
"paths are read from stdin.",
|
|
)
|
|
args = p.parse_args(argv)
|
|
return 0 if check_readonly_copies(_read_changed_files(args), args.config) else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main()) |