Prepare staged index generation and reconcile implementation paths
This commit is contained in:
parent
73801af1c8
commit
fd4a3ba00e
9 changed files with 576 additions and 16 deletions
104
scripts/generate_staged_work_records.py
Normal file
104
scripts/generate_staged_work_records.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regenerate WORK-RECORDS.md from staged Markdown, preserving unstaged edits.
|
||||
|
||||
Uses the existing State Hub renderer. No hub access, commit or push. Intended
|
||||
for a Make target invoked by pre-commit, before the unchanged generated guard.
|
||||
"""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
INDEX = 'WORK-RECORDS.md'
|
||||
|
||||
|
||||
def git(repo, *args):
|
||||
return subprocess.check_output(['git', '-C', str(repo), *args])
|
||||
|
||||
|
||||
def render(snapshot, slug, renderer_root):
|
||||
# Isolate owner imports and their transitive modules for each explicit root.
|
||||
# Repeated calls cannot accidentally reuse another checkout's cached module.
|
||||
code = """
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(sys.argv[1]).resolve() / 'scripts'))
|
||||
import consistency_check as owner
|
||||
if not owner._load_work_record_kind_registry():
|
||||
raise ValueError('canonical work-record kind registry unavailable')
|
||||
content = (owner._generate_work_record_index(Path(sys.argv[2]), sys.argv[3])
|
||||
or owner._work_record_index_document(sys.argv[3], []))
|
||||
sys.stdout.buffer.write(content.encode())
|
||||
"""
|
||||
return subprocess.check_output([sys.executable, '-c', code,
|
||||
str(renderer_root), str(snapshot), slug])
|
||||
|
||||
|
||||
def generate(repo, renderer_root, *, repo_slug, check=False):
|
||||
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", repo_slug):
|
||||
raise ValueError("repo slug must be the canonical lowercase repository identifier")
|
||||
repo = Path(git(repo, 'rev-parse', '--show-toplevel').decode().strip())
|
||||
staged_entries = git(repo, 'ls-files', '--stage', '-z')
|
||||
entries = []
|
||||
for entry in staged_entries.split(b'\0'):
|
||||
if not entry:
|
||||
continue
|
||||
metadata, path = entry.split(b'\t', 1)
|
||||
mode, oid, stage = metadata.split()
|
||||
if stage != b'0':
|
||||
raise ValueError('unmerged index; resolve conflicts before generating')
|
||||
path = path.decode()
|
||||
if path.endswith('.md') and path != INDEX:
|
||||
if mode not in (b'100644', b'100755'):
|
||||
raise ValueError('refuse non-regular Markdown source: ' + path)
|
||||
entries.append((path, oid.decode()))
|
||||
destination = repo / INDEX
|
||||
if destination.is_symlink():
|
||||
raise ValueError('refuse redirected generated index')
|
||||
indexed = subprocess.run(['git', '-C', str(repo), 'show', ':' + INDEX],
|
||||
capture_output=True)
|
||||
previous = indexed.stdout if indexed.returncode == 0 else None
|
||||
existing = destination.read_bytes() if destination.exists() else None
|
||||
if existing != previous:
|
||||
raise ValueError('unstaged WORK-RECORDS.md content; preserve or reconcile it first')
|
||||
with tempfile.TemporaryDirectory(prefix='staged-work-records-') as directory:
|
||||
snapshot = Path(directory)
|
||||
for path, oid in entries:
|
||||
target = snapshot / path
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(git(repo, 'cat-file', 'blob', oid))
|
||||
content = render(snapshot, repo_slug, renderer_root)
|
||||
if content == previous:
|
||||
return False
|
||||
if check:
|
||||
raise ValueError('staged WORK-RECORDS.md is stale')
|
||||
if staged_entries != git(repo, 'ls-files', '--stage', '-z'):
|
||||
raise ValueError('Git index changed during rendering; retry')
|
||||
if (destination.read_bytes() if destination.exists() else None) != existing:
|
||||
raise ValueError('WORK-RECORDS.md changed during rendering; retry')
|
||||
destination.write_bytes(content)
|
||||
subprocess.run(['git', '-C', str(repo), 'add', '--', INDEX], check=True)
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--repo', type=Path, default=Path.cwd())
|
||||
parser.add_argument('--repo-slug', required=True,
|
||||
help='canonical repository identity, independent of checkout path')
|
||||
parser.add_argument('--renderer-root', type=Path,
|
||||
default=Path(__file__).resolve().parents[2] / 'state-hub')
|
||||
parser.add_argument('--check', action='store_true')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
changed = generate(args.repo, args.renderer_root, repo_slug=args.repo_slug, check=args.check)
|
||||
except (OSError, ValueError, subprocess.CalledProcessError, ImportError) as exc:
|
||||
parser.exit(1, str(exc) + '\n')
|
||||
print('staged work-record index regenerated' if changed else 'staged work-record index current')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue