Repository source: docs/runbooks/history_purge.md
History purge
Accepting a retraction or an operator deletion removes the post from the current
tree, but its prose stays recoverable from Git history on the server. The purge
removes every post and signature path named by an accepted delete receipt from
all history, commits a history rewrite record
so old /source/blob/<commit>/... and /source/commits/<commit> links keep
resolving, and rebuilds the read model and static pages. Receipts, requests and
signatures keep their exact bytes. The purge never touches a path without a
receipt.
Run it deliberately and in batches, not after every acceptance. Every commit after the first affected one gets a new identity.
What the purge does not reach
Your own backups, other clones and mirrors, downloads already made, provider or search-engine copies, and quotations in surviving posts. The command says so in its output. A mirror that later pulls from this repository must re-clone: its old history has diverged, and pushing or merging it back would reintroduce the purged objects.
Before you start
Confirm the deletions are accepted: the posts show the deletion notice and
records/post-removals/orrecords/post-retraction-acceptances/holds their receipts. The purge targets receipts only.Take a full backup of the repository including
.git, and verify it restores. A completed purge cannot be undone any other way. If images are enabled, note that image backups bind to a repository revision; take the repository backup and the image backup together.Expect writes to pause.
runholds the same lock as every writer for the whole purge plus a full rebuild; the site keeps serving pages meanwhile. Synthetic measurements on a 100,000-post, 100,000-commit repository took about 35 s for the rewrite, prune and verification, before the rebuild; usepreviewfor your own figure.Git 2.34 or later on the host; no other dependency.
Preview
./v3 purge-history preview --repository-root=/path/repository --database-path=/path/read-model.sqlite3
Preview changes nothing. It lists the receipts, the paths to purge, how many
commits exist and how many touch those paths, legacy posts that still lack a
timestamp record, anything that blocks a run, and the time a rewrite took on a
temporary clone (add --skip-dry-run to skip the clone). Exit code 0 means run
would proceed; 1 means it is blocked and the output says why:
uncommitted changes in the working tree: commit or discard them;
an interrupted instance import: finish it with
./v3 import-instance --resume;legacy posts without
Created-Atand without arecords/post-timestamps/record: bake their timestamps first, because the purge preserves author dates but a later history change might not;an earlier purge that did not finish (see Recovery);
nothing to purge.
Run
./v3 purge-history run --repository-root=/path/repository --database-path=/path/read-model.sqlite3 --static-html-root=/path/static_html
The command states the repository, the paths and the commit count and asks you
to type yes; anything else cancels with nothing changed. Pass --yes to skip
the question in a script; without a terminal and without --yes the run is
refused. The run snapshots every ref,
rewrites history, verifies that no ref still touches a purged path, and only then
discards the old objects: reflogs are expired, stale pseudo-refs removed and the
object database pruned. It then proves by object id that every old post blob and
every pre-purge commit is gone, commits the rewrite record with the subject
Record history purge <time>, rebuilds the read model, clears the activity commit
manifest cache, and regenerates static pages. While it works it reports each
phase on stderr with elapsed seconds, including a running commit count during
the rewrite and the line marking the point of no return, followed by the
rebuild's own progress. The result on stdout ends with Verified and the list
of copies the purge does not reach. Exit code 0 means verified.
Verify
git -C /path/repository log --all -- records/posts/<path from the preview> # prints nothing
git -C /path/repository log -1 --format=%s # Record history purge <time>
Open a purged post's old permalink: the deletion notice, as before. Open an old
/source/blob/<old commit>/<surviving path> link from a bookmark or an old
activity page: it serves the file, with the response header
X-Forum-Resolved-Commit naming the new identity. /source/commits/<old commit>
shows the mapped commit with a Requested-Commit line.
After the run
If images are enabled, take a fresh image backup. Earlier image backups verify only against the pre-purge repository backup, because their manifest names the pre-purge revision. Tell mirror operators to re-clone.
Recovery
Interrupted before old history was discarded (power loss, a killed process): the next
previewreports the interrupted purge and its phase. The original refs are still intact;runrestores them first and then purges.Interrupted after pruning, or verification failed because an object lingered (a
.keeppack, an alternate object store, a second worktree): the rewritten history and its record are in place, but the purge is not proven complete. Restore the repository from the pre-purge backup, remove the extra reference, and purge again.previewkeeps reporting the unfinished purge until then.Record not committed (the message says so and names the files): commit
records/history-rewrites/...by hand, then run./v3 rebuild.Site update incomplete after the record commit: the read model is marked stale; run
./v3 rebuildand, if static pages are served,./v3 build-static.