Upstream wiped it. The fork still remembers.
Same SHA: upstream ✕ wiped · fork ● still alive. Classic scandal timeline GIF still in demo/timeline.gif.
Live demo → — offline dual-rail (Fork-witness A): upstream ✕ / fork ●. Fixtures only; no auth, no clone.
Or open demo/index.html after clone — same story, zero network.
Path B = fork-witness-a dual-rail. Path A (scandal-a) is alternate only. See launch/INSTALL_NO_NPM.md.
# zero install from GitHub (Path B):
npx github:CAOShurong/forcepush-ghost --fixture fork-witness-a
# from a clone (Path B):
node bin/forcepush-ghost.js --fixture fork-witness-a
# or: npm run path-b
# global from GitHub (still no registry):
# npm i -g github:CAOShurong/forcepush-ghost && forcepush-ghost --fixture fork-witness-a
# Path A alternate (classic scandal timeline):
node bin/forcepush-ghost.js --fixture scandal-a
# scripting: structured JSON of the same timeline object (Pages stays offline; live --vs is CLI-only)
node bin/forcepush-ghost.js --fixture fork-witness-a --json
# human-readable indent: add --pretty
node bin/forcepush-ghost.js --fixture fork-witness-a --json --prettynpx github:CAOShurong/forcepush-ghost owner/repo
# from a clone: node bin/forcepush-ghost.js owner/repoShowcase (real public repo with live ✕ on tip):
node bin/forcepush-ghost.js mrdoob/three.jsnode bin/forcepush-ghost.js upstream/repo --vs forkOwner
node bin/forcepush-ghost.js upstream/repo --vs forkOwner/forkRepo
node bin/forcepush-ghost.js upstream/repo --vs autoSame repo name when only forkOwner is given. Probes whether a fork still holds wiped / before tip SHAs after an upstream rewrite. Fail closed: API errors exit 2 and never substitute fixtures; fork missing a SHA prints upstream ✕ / fork ——. Same-repo --vs (upstream === fork) is refused with exit 2 — pick a different fork owner.
--vs auto enumerates capped public forks (hard cap 2 pages / ~60 forks, stargazers order), probes which still hold wiped/before tip SHA(s), and picks a public fork that still holds tip SHA (prefer higher stargazers among holders — never claim "best"/"most scandalous"). Find none → exit 2; never invent a dual-hit. Rate-limit Remaining=0 → stop early and say so. Prefer an explicit --vs forkOwner when you already know the witness.
Verified dual-hit showcase (Upstream ✕ | Fork ● on shared tip SHA):
node bin/forcepush-ghost.js mrdoob/three.js --vs alteredq
# optional alt fork: --vs brunosimon
# optional alt: pocketbase/pocketbase --vs fondogerPages stays offline fixtures — the demo never runs live --vs / --vs auto.
Live mode reads recent public GitHub Events for force-push signals and prints the same story timeline. Optional GITHUB_TOKEN / GH_TOKEN raises rate limits; if unset, the CLI soft-tries gh auth token when the GitHub CLI is logged in. The visual demo stays offline-fixtures so it never pretends it scanned a repo it did not.
A public timeline of commits that used to exist on a branch and then disappeared after a force-push. Offline fixtures are sanitized reconstructions so the story is always reproducible. Live scan is story/timeline only — not a secret scanner.
- Jenkins, Nov 2013 — misconfigured Gerrit replication force-pushed stale clones across ~186 repos → Fixture Scandal A (summary, InfoQ)
- CocoaPods Specs, Jan 2014 — libgit2/GitHub web-editor corruption forced a Specs history rewrite → Fixture Scandal B (postmortem)
- GitHub Protected Branches, Sep 2015 — product motivation included blocking accidental force-pushes that overwrite others' work → why wipe matters background only (announcement). This tool does not detect protected branches.
Names/SHAs in fixtures are fictional. Not live scans of jenkinsci/* or CocoaPods/Specs.
node bin/forcepush-ghost.js --fixture fork-witness-a # Path B primary — offline dual-rail
node bin/forcepush-ghost.js --fixture fork-witness-clean
node bin/forcepush-ghost.js --fixture scandal-a
node bin/forcepush-ghost.js --fixture scandal-b
node bin/forcepush-ghost.js --fixture cleanOpen the Live demo for the visual timeline (red ✕ = wiped).
Dual-rail fixtures: same SHA on upstream ✕ vs fork ●. Offline preview for Pages; live compare is CLI --vs only.
node bin/forcepush-ghost.js --fixture fork-witness-a
node bin/forcepush-ghost.js --fixture fork-witness-cleanPages: pick Fork-witness A in the demo select. Hook: Upstream wiped it. The fork still remembers.
- Live scan uses the public Events window only — older rewrites can fall outside it; Events pagination follows Link next up to 3 pages and stops early on rate-limit remaining=0 (no invented rows); PushEvents that omit
forced=trueget an honest compare budget (default 40) so busy-repo fast-forward noise does not starve later in-window diverged/before-missing tip discovery for--vs auto - Demo UI is offline fixtures; live
owner/repoand live--vs/--vs autoare CLI only (Pages never runs live--vs/--vs auto) - Offline fixtures are sanitized — not real scandals dressed up as live results
- Story/timeline positioning only — not a secret scanner and not secret recovery
- GitHub's Activity view can filter Force pushes and show a
beforeSHA — same shape as our ✕ timeline. We do not replace Activity, and we do not offer recovery/restore buttons - Registry
npx forcepush-ghostworks only after npm publish; until then use Pages,npx github:CAOShurong/forcepush-ghost, ornode bin/…from a clone
MIT
