check-dispatch-hook (4939B)
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 20 def git(*args): 21 result = subprocess.run(["git", "-C", str(work), *args], env=env, 22 text=True, capture_output=True) 23 assert result.returncode == 0, result.stderr 24 return result.stdout.strip() 25 26 def post(name, hours=192, draft=False, date=True): 27 path = work / f"content/journal/dispatch-{name}/index.md" 28 path.parent.mkdir(parents=True, exist_ok=True) 29 timestamp = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat() 30 field = f"date: {timestamp}\n" if date else "" 31 path.write_text(f"---\ntitle: Test\n{field}draft: {str(draft).lower()}\n" 32 "references:\n- date: 2020-01-01\n---\n\nBody.\ndate: Leave this alone.\n") 33 return path 34 35 def commit(): 36 git("add", ".") 37 git("commit", "-m", "Test change") 38 39 def push(allowed, remote="origin", ref="main", message=None): 40 before = git("--git-dir", str(root / "origin.git"), "rev-parse", "main") 41 result = subprocess.run(["git", "-C", str(work), "push", remote, ref], 42 env=env, text=True, capture_output=True) 43 assert (result.returncode == 0) == allowed, result.stderr 44 if message: 45 assert message in result.stderr, result.stderr 46 if not allowed: 47 assert git("--git-dir", str(root / "origin.git"), "rev-parse", "main") == before 48 49 git("init", "-b", "main") 50 git("config", "user.name", "Hook Test") 51 git("config", "user.email", "[email protected]") 52 git("config", "commit.gpgsign", "false") 53 for remote in ("origin", "mirror"): 54 git("init", "--bare", str(root / f"{remote}.git")) 55 git("remote", "add", remote, str(root / f"{remote}.git")) 56 for file in (".githooks/pre-push", "bin/check-dispatch-dates", "bin/timestamp"): 57 target = work / file 58 target.parent.mkdir(parents=True, exist_ok=True) 59 shutil.copy2(repo / file, target) 60 published = post("published") 61 post("draft", draft=True) 62 commit() 63 git("push", "origin", "main") 64 git("config", "core.hooksPath", ".githooks") 65 66 # Old posts remain editable and renameable without changing their dates. 67 published.write_text(published.read_text() + "Correction.\n") 68 commit() 69 push(True) 70 git("mv", "content/journal/dispatch-published", "content/journal/dispatch-renamed") 71 commit() 72 push(True) 73 74 # Drafts may be old; publishing them must use a recent committed timestamp. 75 post("new-draft", draft=True) 76 commit() 77 push(True) 78 draft = post("draft", draft=False) 79 commit() 80 push(False, message="publication date is") 81 original = draft.read_text() 82 result = subprocess.run([str(work / "bin/timestamp"), str(draft)], 83 env=env, text=True, capture_output=True) 84 assert result.returncode == 0, result.stderr 85 assert draft.read_text() != original 86 assert "- date: 2020-01-01" in draft.read_text() 87 assert "date: Leave this alone." in draft.read_text() 88 push(False, message="publication date is") # Working-tree changes don't count. 89 commit() 90 push(True) 91 92 # Accept under 24 hours; block over 24 hours and missing dates. 93 post("recent", hours=23) 94 commit() 95 push(True) 96 post("stale", hours=25) 97 commit() 98 push(False, message="maximum: 24 hours") 99 push(True, ref="main:refs/heads/writing") 100 push(True, remote="mirror") 101 git("reset", "--hard", "origin/main") 102 post("missing-date", date=False) 103 commit() 104 push(False, message="publication date is missing or invalid") 105 git("reset", "--hard", "origin/main") 106 107 # A differently named local branch still gets checked when publishing main. 108 git("checkout", "-b", "publishing") 109 post("from-branch") 110 commit() 111 push(False, ref="publishing:main", message="publication date is") 112 113 # No known remote baseline must fail clearly, rather than checking the archive. 114 sha = git("rev-parse", "HEAD") 115 result = subprocess.run([str(work / "bin/check-dispatch-dates"), "origin"], 116 cwd=work, env=env, text=True, capture_output=True, 117 input=f"refs/heads/main {sha} refs/heads/main {'0' * 40}\n") 118 assert result.returncode != 0 and "does not exist yet" in result.stderr 119 120 print("Dispatch hook checks passed: real local pushes, draft publication, timestamps, renames, and remote/branch scope.")