davideisinger.com

My personal website
Log | Files | Refs | README

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.")