dispatch-hook (5399B)
1 #!/usr/bin/env python3 2 """Test the publication guard using disposable repositories and local pushes.""" 3 4 from datetime import datetime, timedelta, timezone 5 from pathlib import Path 6 import os 7 import shutil 8 import subprocess 9 import tempfile 10 11 12 repo = Path(__file__).resolve().parent.parent 13 env = dict(os.environ, GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull) 14 15 with tempfile.TemporaryDirectory(prefix="dispatch-hook-") as directory: 16 root = Path(directory) 17 work = root / "work" 18 work.mkdir() 19 # Pushing must no longer invoke spellcheck. 20 commands = root / "commands" 21 commands.mkdir() 22 npx = commands / "npx" 23 npx.write_text('#!/usr/bin/env python3\n' 24 'import os, sys\n' 25 'sys.stderr.write("Unexpected npx invocation during push\\n")\n' 26 'sys.exit(1)\n') 27 npx.chmod(0o755) 28 env.update(PATH=f"{commands}{os.pathsep}{env['PATH']}", HOOK_TEST_WORK=str(work)) 29 30 def git(*args): 31 result = subprocess.run(["git", "-C", str(work), *args], env=env, 32 text=True, capture_output=True) 33 assert result.returncode == 0, result.stderr 34 return result.stdout.strip() 35 36 def post(name, hours=192, draft=False, date=True): 37 path = work / f"content/journal/dispatch-{name}/index.md" 38 path.parent.mkdir(parents=True, exist_ok=True) 39 timestamp = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat() 40 field = f"date: {timestamp}\n" if date else "" 41 path.write_text(f"---\ntitle: Test\n{field}draft: {str(draft).lower()}\n" 42 "references:\n- date: 2020-01-01\n---\n\nBody.\ndate: Leave this alone.\n") 43 return path 44 45 def commit(): 46 git("add", ".") 47 git("commit", "-m", "Test change") 48 49 def push(allowed, remote="origin", ref="main", message=None): 50 before = git("--git-dir", str(root / "origin.git"), "rev-parse", "main") 51 result = subprocess.run(["git", "-C", str(work), "push", remote, ref], 52 env=env, text=True, capture_output=True) 53 assert (result.returncode == 0) == allowed, result.stderr 54 if message: 55 assert message in result.stderr, result.stderr 56 if not allowed: 57 assert git("--git-dir", str(root / "origin.git"), "rev-parse", "main") == before 58 59 git("init", "-b", "main") 60 git("config", "user.name", "Hook Test") 61 git("config", "user.email", "[email protected]") 62 git("config", "commit.gpgsign", "false") 63 for remote in ("origin", "mirror"): 64 git("init", "--bare", str(root / f"{remote}.git")) 65 git("remote", "add", remote, str(root / f"{remote}.git")) 66 for file in (".githooks/pre-push", ".githooks/lib/check-dispatch-dates", "bin/timestamp"): 67 target = work / file 68 target.parent.mkdir(parents=True, exist_ok=True) 69 shutil.copy2(repo / file, target) 70 published = post("published") 71 post("draft", draft=True) 72 commit() 73 git("push", "origin", "main") 74 git("config", "core.hooksPath", ".githooks") 75 76 # Old posts remain editable and renameable without changing their dates. 77 published.write_text(published.read_text() + "Correction.\n") 78 commit() 79 push(True) 80 git("mv", "content/journal/dispatch-published", "content/journal/dispatch-renamed") 81 commit() 82 push(True) 83 84 # Drafts may be old; publishing them must use a recent committed timestamp. 85 post("new-draft", draft=True) 86 commit() 87 push(True) 88 draft = post("draft", draft=False) 89 commit() 90 push(False, message="publication date is") 91 original = draft.read_text() 92 result = subprocess.run([str(work / "bin/timestamp"), str(draft)], 93 env=env, text=True, capture_output=True) 94 assert result.returncode == 0, result.stderr 95 assert draft.read_text() != original 96 assert "- date: 2020-01-01" in draft.read_text() 97 assert "date: Leave this alone." in draft.read_text() 98 push(False, message="publication date is") # Working-tree changes don't count. 99 commit() 100 push(True) 101 102 # Accept under 24 hours; block over 24 hours and missing dates. 103 post("recent", hours=23) 104 commit() 105 push(True) 106 post("stale", hours=25) 107 commit() 108 push(False, message="maximum: 24 hours") 109 push(True, ref="main:refs/heads/writing") 110 push(True, remote="mirror") 111 git("reset", "--hard", "origin/main") 112 post("missing-date", date=False) 113 commit() 114 push(False, message="publication date is missing or invalid") 115 git("reset", "--hard", "origin/main") 116 117 # A differently named local branch still gets checked when publishing main. 118 git("checkout", "-b", "publishing") 119 post("from-branch") 120 commit() 121 push(False, ref="publishing:main", message="publication date is") 122 123 # No known remote baseline must fail clearly, rather than checking the archive. 124 sha = git("rev-parse", "HEAD") 125 result = subprocess.run([str(work / ".githooks/lib/check-dispatch-dates"), "origin"], 126 cwd=work, env=env, text=True, capture_output=True, 127 input=f"refs/heads/main {sha} refs/heads/main {'0' * 40}\n") 128 assert result.returncode != 0 and "does not exist yet" in result.stderr 129 130 print("Dispatch hook checks passed: real local pushes, draft publication, timestamps, renames, and remote/branch scope.")