Jump to…
snowinitial commitqoxwzsukwmkx1mo
Matt W1#!/usr/bin/env bash
Matt W2#
Matt W3# Generate the jj/git fixture corpus the indexer is tested against.
Matt W4#
Matt W5# Spec §12: "a script that drives the real jj and git CLIs to produce repositories
Matt W6# exhibiting every state the indexer must handle. Commit the script, generate the
Matt W7# fixtures in CI. Never hand-author Git objects."
Matt W8#
Matt W9# Output: fixtures/repos/<case>.git — bare repositories, exactly what Dogfood
Matt W10# stores on disk. The indexer reads these; nothing here is hand-written.
Matt W11#
Matt W12# Usage: fixtures/gen.sh [output_dir]
Matt W13# Env: JJ_VERSION pin (default below). CI must pin so a jj format change fails
Matt W14# a test instead of silently corrupting the index.
Matt W15
Matt W16set -euo pipefail
Matt W17
Matt W18JJ_VERSION="${JJ_VERSION:-0.43.0}"
Matt W19ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Matt W20OUT="${1:-$ROOT/repos}"
Matt W21BIN="$ROOT/bin"
Matt W22WORK="$(mktemp -d)"
Matt W23trap 'rm -rf "$WORK"' EXIT
Matt W24
Matt W25# ─── toolchain ───────────────────────────────────────────────────────────────
Matt W26
Matt W27export PATH="$BIN:$PATH"
Matt W28
Matt W29if ! command -v jj >/dev/null 2>&1 || [[ "$(jj --version | awk '{print $2}' | cut -d- -f1)" != "$JJ_VERSION" ]]; then
Matt W30 echo "==> fetching jj $JJ_VERSION"
Matt W31 mkdir -p "$BIN"
Matt W32 curl -fsSL "https://github.com/jj-vcs/jj/releases/download/v${JJ_VERSION}/jj-v${JJ_VERSION}-x86_64-unknown-linux-musl.tar.gz" \
Matt W33 | tar xz -C "$BIN" jj
Matt W34 chmod +x "$BIN/jj"
Matt W35fi
Matt W36
Matt W37echo "==> jj: $(jj --version)"
Matt W38echo "==> git: $(git --version)"
Matt W39
Matt W40# Deterministic identity and timestamps. Without pinned times the change ids stay
Matt W41# stable (they are random) but the commit ids do not, and the tests assert on the
Matt W42# relationship between them, not on literal values — so we pin what we can and
Matt W43# the corpus manifest records the rest.
Matt W44export JJ_CONFIG="$WORK/jjconfig.toml"
Matt W45cat > "$JJ_CONFIG" <<'EOF'
Matt W46[user]
Matt W47name = "Fixture Author"
Matt W48email = "fixture@dogfood.sh"
Matt W49
Matt W50[ui]
Matt W51paginate = "never"
Matt W52color = "never"
Matt W53
Matt W54# The `rewritten` case force-pushes the same change five times. jj defaults to
Matt W55# treating commits reachable from a remote bookmark as immutable, which is right
Matt W56# for humans and wrong for a fixture that must simulate exactly that workflow.
Matt W57[revset-aliases]
Matt W58"immutable_heads()" = "none()"
Matt W59EOF
Matt W60
Matt W61export GIT_AUTHOR_NAME="Fixture Author"
Matt W62export GIT_AUTHOR_EMAIL="fixture@dogfood.sh"
Matt W63export GIT_COMMITTER_NAME="Fixture Author"
Matt W64export GIT_COMMITTER_EMAIL="fixture@dogfood.sh"
Matt W65
Matt W66rm -rf "$OUT"
Matt W67mkdir -p "$OUT"
Matt W68
Matt W69MANIFEST="$OUT/manifest.json"
Matt W70# Opened here, closed at the bottom. `note` appends ",<key>:<value>" entries, so
Matt W71# the object starts with a sentinel key to make the leading comma always valid.
Matt W72printf '{"jj_version":"%s","cases":{"_generated_by":"fixtures/gen.sh"' "$JJ_VERSION" > "$MANIFEST.tmp"
Matt W73
Matt W74# Record a case's interesting revisions into the manifest so Rust tests can assert
Matt W75# against named commits without re-deriving them.
Matt W76note() { # note <case> <json>
Matt W77 printf ',"%s":%s' "$1" "$2" >> "$MANIFEST.tmp"
Matt W78}
Matt W79
Matt W80# Publish a working repo as a bare repo under $OUT, the way a push would land it.
Matt W81publish() { # publish <case> <workdir> [default_branch]
Matt W82 local case="$1" work="$2" head="${3:-main}"
Matt W83 git init --bare -q "$OUT/$case.git"
Matt W84 git -C "$work" push -q --all "$OUT/$case.git"
Matt W85 git -C "$work" push -q --tags "$OUT/$case.git" 2>/dev/null || true
Matt W86 # `git init --bare` leaves HEAD on refs/heads/master, which none of these
Matt W87 # repos have. Dogfood creates repos with HEAD pointing at the default
Matt W88 # bookmark, so the corpus must match or every fixture has an unborn HEAD.
Matt W89 git --git-dir="$OUT/$case.git" symbolic-ref HEAD "refs/heads/$head"
Matt W90}
Matt W91
Matt W92cid() { jj -R "$1" log --no-graph --ignore-working-copy -T 'commit_id' -r "$2"; }
Matt W93chid() { jj -R "$1" log --no-graph --ignore-working-copy -T 'change_id' -r "$2"; }
Matt W94
Matt W95# ─────────────────────────────────────────────────────────────────────────────
Matt W96# case: basic — jj-authored linear history, one bookmark
Matt W97#
Matt W98# The baseline. Every commit carries a `change-id` header.
Matt W99# ─────────────────────────────────────────────────────────────────────────────
Matt W100echo "==> case: basic"
Matt W101W="$WORK/basic"
Matt W102jj git init --colocate "$W" >/dev/null 2>&1
Matt W103(
Matt W104 cd "$W"
Matt W105 echo "one" > a.txt
Matt W106 jj describe -m "add a" >/dev/null
Matt W107 jj new -m "add b" >/dev/null
Matt W108 echo "two" > b.txt
Matt W109 jj new -m "add c" >/dev/null
Matt W110 echo "three" > c.txt
Matt W111 jj bookmark create main -r @- >/dev/null
Matt W112)
Matt W113note basic "$(printf '{"head_change":"%s","head_commit":"%s"}' "$(chid "$W" 'bookmarks(main)')" "$(cid "$W" 'bookmarks(main)')")"
Matt W114publish basic "$W"
Matt W115
Matt W116# ─────────────────────────────────────────────────────────────────────────────
Matt W117# case: rewritten — the property the whole product rests on
Matt W118#
Matt W119# One change, rewritten five times (spec §5: "a change that is rewritten five
Matt W120# times"). The change id must be identical across all five; the commit ids must
Matt W121# all differ. Every intermediate commit is kept reachable via a tag so the bare
Matt W122# repo actually contains them and the test can walk the whole rewrite chain.
Matt W123# ─────────────────────────────────────────────────────────────────────────────
Matt W124echo "==> case: rewritten"
Matt W125W="$WORK/rewritten"
Matt W126jj git init --colocate "$W" >/dev/null 2>&1
Matt W127git init --bare -q "$OUT/rewritten.git"
Matt W128(cd "$W" && jj git remote add origin "$OUT/rewritten.git" >/dev/null 2>&1)
Matt W129(
Matt W130 cd "$W"
Matt W131 echo "v1" > f.txt
Matt W132 jj describe -m "evolving change v1" >/dev/null
Matt W133 jj bookmark create main -r @ >/dev/null
Matt W134 jj git push -b main --allow-empty-description >/dev/null 2>&1
Matt W135)
Matt W136TARGET="$(chid "$W" 'bookmarks(main)')"
Matt W137REWRITES="$(cid "$W" 'bookmarks(main)')"
Matt W138for i in 2 3 4 5; do
Matt W139 (
Matt W140 cd "$W"
Matt W141 echo "v$i" > f.txt
Matt W142 jj describe -r 'bookmarks(main)' -m "evolving change v$i" >/dev/null
Matt W143 # Force-push each rewrite, exactly as a user amending and re-pushing does.
Matt W144 # The bare repo accumulates every revision as an unreferenced object, which
Matt W145 # is precisely the state the indexer sees in production.
Matt W146 jj git push -b main >/dev/null 2>&1
Matt W147 )
Matt W148 REWRITES="$REWRITES $(cid "$W" 'bookmarks(main)')"
Matt W149done
Matt W150git --git-dir="$OUT/rewritten.git" symbolic-ref HEAD refs/heads/main
Matt W151# All five commit ids must differ; the change id must be identical across them.
Matt W152note rewritten "$(printf '{"stable_change":"%s","revisions":["%s"]}' \
Matt W153 "$TARGET" "$(echo "$REWRITES" | sed 's/ /","/g')")"
Matt W154
Matt W155# ─────────────────────────────────────────────────────────────────────────────
Matt W156# case: plain-git — no change-id header anywhere
Matt W157#
Matt W158# Drives the synthetic-identity fallback (spec §4). extract_change_id must return
Matt W159# None for every commit here.
Matt W160# ─────────────────────────────────────────────────────────────────────────────
Matt W161echo "==> case: plain-git"
Matt W162W="$WORK/plaingit"
Matt W163git init -q "$W"
Matt W164(
Matt W165 cd "$W"
Matt W166 git symbolic-ref HEAD refs/heads/main
Matt W167 echo "x" > f.txt && git add -A && git commit -qm "plain commit one"
Matt W168 echo "y" >> f.txt && git add -A && git commit -qm "plain commit two"
Matt W169)
Matt W170note plain_git "$(printf '{"head_commit":"%s"}' "$(git -C "$W" rev-parse HEAD)")"
Matt W171publish plain-git "$W"
Matt W172
Matt W173# ─────────────────────────────────────────────────────────────────────────────
Matt W174# case: mixed — jj commits on top of plain-git commits in one history
Matt W175#
Matt W176# The realistic case for a repo migrated to jj. The indexer must produce real
Matt W177# changes for the jj commits and synthetic ones for the git commits, in a single
Matt W178# connected graph.
Matt W179# ─────────────────────────────────────────────────────────────────────────────
Matt W180echo "==> case: mixed"
Matt W181W="$WORK/mixed"
Matt W182git init -q "$W"
Matt W183(
Matt W184 cd "$W"
Matt W185 git symbolic-ref HEAD refs/heads/main
Matt W186 echo "legacy" > old.txt && git add -A && git commit -qm "pre-jj history"
Matt W187)
Matt W188(
Matt W189 cd "$W"
Matt W190 jj git init --colocate . >/dev/null 2>&1
Matt W191 jj new main -m "first jj change on top" >/dev/null
Matt W192 echo "new" > new.txt
Matt W193 jj bookmark set main -r @ >/dev/null 2>&1 || jj bookmark create main -r @ >/dev/null
Matt W194)
Matt W195publish mixed "$W"
Matt W196
Matt W197# ─────────────────────────────────────────────────────────────────────────────
Matt W198# case: stack — a chain of changes, none merged
Matt W199#
Matt W200# Exercises stack edge computation (spec §4). Four changes stacked on main.
Matt W201# ─────────────────────────────────────────────────────────────────────────────
Matt W202echo "==> case: stack"
Matt W203W="$WORK/stack"
Matt W204jj git init --colocate "$W" >/dev/null 2>&1
Matt W205(
Matt W206 cd "$W"
Matt W207 echo "base" > base.txt
Matt W208 jj describe -m "base of stack" >/dev/null
Matt W209 jj bookmark create main -r @ >/dev/null
Matt W210 for n in 1 2 3 4; do
Matt W211 jj new -m "stacked change $n" >/dev/null
Matt W212 echo "layer $n" > "layer$n.txt"
Matt W213 done
Matt W214 jj bookmark create top -r @ >/dev/null
Matt W215)
Matt W216note stack "$(printf '{"bottom":"%s","top":"%s"}' "$(chid "$W" 'bookmarks(main)')" "$(chid "$W" 'bookmarks(top)')")"
Matt W217publish stack "$W"
Matt W218
Matt W219# ─────────────────────────────────────────────────────────────────────────────
Matt W220# case: merge — a two-parent jj commit
Matt W221# ─────────────────────────────────────────────────────────────────────────────
Matt W222echo "==> case: merge"
Matt W223W="$WORK/merge"
Matt W224jj git init --colocate "$W" >/dev/null 2>&1
Matt W225(
Matt W226 cd "$W"
Matt W227 # NOTE: revisions are addressed by captured change id, never by a
Matt W228 # description() revset. In jj 0.43 `description("x")` is an EXACT match, not a
Matt W229 # substring match (substring: needs an explicit `substring:` prefix), which
Matt W230 # silently returns the empty set and produces a corrupt fixture.
Matt W231 echo "base" > base.txt
Matt W232 jj describe -m "merge base" >/dev/null
Matt W233 BASE="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
Matt W234 jj new -m "left side" >/dev/null && echo L > l.txt
Matt W235 LEFT="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
Matt W236 jj new -m "right side" "$BASE" >/dev/null && echo R > r.txt
Matt W237 RIGHT="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
Matt W238 jj new "$LEFT" "$RIGHT" -m "the merge" >/dev/null
Matt W239 jj bookmark create main -r @ >/dev/null
Matt W240)
Matt W241note merge "$(printf '{"merge_commit":"%s","merge_change":"%s"}' "$(cid "$W" 'bookmarks(main)')" "$(chid "$W" 'bookmarks(main)')")"
Matt W242publish merge "$W"
Matt W243
Matt W244# ─────────────────────────────────────────────────────────────────────────────
Matt W245# case: conflict — a real 2-sided conflict
Matt W246#
Matt W247# Produces the `jj:trees` + `jj:conflict-labels` headers and the
Matt W248# .jjconflict-side-N / .jjconflict-base-N tree layout documented in
Matt W249# docs/change-id-format.md §6. Drives conflict detection and the conflict viewer.
Matt W250# ─────────────────────────────────────────────────────────────────────────────
Matt W251echo "==> case: conflict"
Matt W252W="$WORK/conflict"
Matt W253jj git init --colocate "$W" >/dev/null 2>&1
Matt W254(
Matt W255 cd "$W"
Matt W256 printf 'line1\nline2\nline3\n' > c.txt
Matt W257 jj describe -m "conflict base" >/dev/null
Matt W258 BASE="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
Matt W259 jj new -m "side A" >/dev/null && printf 'line1\nAAA\nline3\n' > c.txt
Matt W260 SIDEA="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
Matt W261 jj new -m "side B" "$BASE" >/dev/null && printf 'line1\nBBB\nline3\n' > c.txt
Matt W262 SIDEB="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
Matt W263 # Both sides edit line 2 of c.txt from the same base -> a real 2-sided conflict.
Matt W264 jj new "$SIDEA" "$SIDEB" -m "conflicted merge" >/dev/null
Matt W265 jj bookmark create main -r @ >/dev/null
Matt W266)
Matt W267note conflict "$(printf '{"conflicted_commit":"%s","conflicted_change":"%s"}' "$(cid "$W" 'bookmarks(main)')" "$(chid "$W" 'bookmarks(main)')")"
Matt W268publish conflict "$W"
Matt W269
Matt W270# ─────────────────────────────────────────────────────────────────────────────
Matt W271# case: signed — change-id alongside a multi-line gpgsig header
Matt W272#
Matt W273# The parser hazard from docs/change-id-format.md §2: a naive line scan can walk
Matt W274# into the base64 signature body. Uses an SSH signing key so CI needs no GPG.
Matt W275# ─────────────────────────────────────────────────────────────────────────────
Matt W276echo "==> case: signed"
Matt W277W="$WORK/signed"
Matt W278KEY="$WORK/signkey"
Matt W279ssh-keygen -t ed25519 -N "" -f "$KEY" -q
Matt W280jj git init --colocate "$W" >/dev/null 2>&1
Matt W281(
Matt W282 cd "$W"
Matt W283 git config gpg.format ssh
Matt W284 git config user.signingkey "$KEY.pub"
Matt W285 echo "signed content" > s.txt
Matt W286 jj describe -m "a signed change" >/dev/null
Matt W287 jj sign -r @ \
Matt W288 --config 'signing.behavior="own"' \
Matt W289 --config 'signing.backend="ssh"' \
Matt W290 --config "signing.key=\"$KEY\"" >/dev/null 2>&1
Matt W291 jj bookmark create main -r @ >/dev/null
Matt W292)
Matt W293note signed "$(printf '{"signed_commit":"%s","signed_change":"%s"}' "$(cid "$W" 'bookmarks(main)')" "$(chid "$W" 'bookmarks(main)')")"
Matt W294publish signed "$W"
Matt W295
Matt W296# ─────────────────────────────────────────────────────────────────────────────
Matt W297# case: renames — for comment anchor rebasing across a file rename (spec §5)
Matt W298# ─────────────────────────────────────────────────────────────────────────────
Matt W299echo "==> case: renames"
Matt W300W="$WORK/renames"
Matt W301jj git init --colocate "$W" >/dev/null 2>&1
Matt W302(
Matt W303 cd "$W"
Matt W304 printf 'alpha\nbravo\ncharlie\ndelta\necho\n' > original.txt
Matt W305 jj describe -m "before rename" >/dev/null
Matt W306 jj bookmark create main -r @ >/dev/null
Matt W307 jj new -m "rename and edit" >/dev/null
Matt W308 git mv original.txt renamed.txt 2>/dev/null || mv original.txt renamed.txt
Matt W309 printf 'alpha\nbravo\nCHARLIE\ndelta\necho\nfoxtrot\n' > renamed.txt
Matt W310 jj bookmark create renamed -r @ >/dev/null
Matt W311)
Matt W312publish renames "$W"
Matt W313
Matt W314# ─────────────────────────────────────────────────────────────────────────────
Matt W315# case: whitespace — anchor rebasing must not treat reindentation as a content
Matt W316# change (spec §5: "whitespace-only changes")
Matt W317# ─────────────────────────────────────────────────────────────────────────────
Matt W318echo "==> case: whitespace"
Matt W319W="$WORK/whitespace"
Matt W320jj git init --colocate "$W" >/dev/null 2>&1
Matt W321(
Matt W322 cd "$W"
Matt W323 printf 'fn main() {\nlet x = 1;\nprintln!("{}", x);\n}\n' > m.rs
Matt W324 jj describe -m "unindented" >/dev/null
Matt W325 jj bookmark create main -r @ >/dev/null
Matt W326 jj new -m "reindent only" >/dev/null
Matt W327 printf 'fn main() {\n let x = 1;\n println!("{}", x);\n}\n' > m.rs
Matt W328 jj bookmark create reindented -r @ >/dev/null
Matt W329)
Matt W330publish whitespace "$W"
Matt W331
Matt W332# ─────────────────────────────────────────────────────────────────────────────
Matt W333# case: hostile — inputs the security tests in spec §9 need
Matt W334#
Matt W335# A repo containing a symlink escaping the repo root, a path with unusual
Matt W336# characters, and a deeply nested tree. Serving any of these naively is a CVE.
Matt W337# ─────────────────────────────────────────────────────────────────────────────
Matt W338echo "==> case: hostile"
Matt W339W="$WORK/hostile"
Matt W340git init -q "$W"
Matt W341(
Matt W342 cd "$W"
Matt W343 git symbolic-ref HEAD refs/heads/main
Matt W344 ln -s /etc/passwd escape-absolute
Matt W345 ln -s ../../../../etc/shadow escape-relative
Matt W346 mkdir -p a/b/c/d/e/f/g/h
Matt W347 echo "deep" > a/b/c/d/e/f/g/h/deep.txt
Matt W348 printf 'no trailing newline' > weird-name$'\t'tab.txt 2>/dev/null || echo skip > normal.txt
Matt W349 echo "content" > "spaces in name.txt"
Matt W350 git add -A && git commit -qm "hostile paths"
Matt W351)
Matt W352publish hostile "$W"
Matt W353
Matt W354# ─── finish ──────────────────────────────────────────────────────────────────
Matt W355
Matt W356printf '}}' >> "$MANIFEST.tmp"
Matt W357# Reformat, and fail loudly on malformed JSON rather than shipping a corpus whose
Matt W358# manifest the Rust tests cannot parse.
Matt W359python3 -c "
Matt W360import json
Matt W361raw = open('$MANIFEST.tmp').read()
Matt W362json.dump(json.loads(raw), open('$MANIFEST','w'), indent=2)
Matt W363print('==> manifest ok')
Matt W364"
Matt W365rm -f "$MANIFEST.tmp"
Matt W366
Matt W367echo
Matt W368echo "==> corpus written to $OUT"
Matt W369ls -1 "$OUT" | sed 's/^/ /'

369 lines · Shell