Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3. Sync and the lockfile

This is where it clicks. You have a haw.toml from the last chapter. Now you’ll run haw sync to clone the fleet for real, read the lockfile that freezes it to exact commits, prove it’s reproducible, and — deliberately — break something to watch haw catch drift.

Everything here clones over HTTPS with no authentication, so you can actually run every command as you read. Keep the my-first-stack workspace from Chapter 2 open.

Syncing and pinning a fleet to a lockfile

Sync clones the fleet; the lockfile freezes it to exact commits — the whole reproducibility trick.

🎯 In this chapter, you’ll learn to…

  • Run haw sync to clone the fleet and generate haw.lock.
  • Read the fleet with haw tree and haw status, and understand every column.
  • Read the lockfile’s real fields — rev (the resolved SHA), source-rev, and branch.
  • Re-sync and confirm it’s idempotent — the lock, not the branch, is now the truth.
  • Cause drift on purpose and catch it with haw verify (exit 3), then restore.

The full compose loop you’re about to run: sync clones the fleet, then tree / status read it and the lock pins it.

🔄 1. Sync — clone everything and write the lock

One command materializes the tree:

haw sync

haw resolves each repo’s rev to an exact commit, clones it, and — because there’s no lockfile yet — writes one. You’ll see progress per repo and a summary. Afterwards, look around:

$ ls
haw.lock   haw.toml   hello-world/   spoon-knife/

There they are: two real, complete Git clones, plus a brand-new haw.lock. No submodules, no symlinks — you could cd hello-world && git log and it’s just Git.

Tip: haw sync is idempotent. Run it again and, since the lock already pins exact SHAs, haw just makes sure your tree matches — no surprises, safe to repeat in scripts and CI.

🔍 2. Explore the fleet

Now the read commands. First, the shape of things:

haw tree
haw.toml
└─ site
   ├─ hello-world  master  (https://github.com/octocat/Hello-World.git)
   └─ spoon-knife  main  (https://github.com/octocat/Spoon-Knife.git)

That’s your stack → repo tree: the stack site, the two repos under it, each with its declared rev and origin.

Now the health check:

haw status
REPO         BRANCH                   HEAD       DIRTY  DRIFT
hello-world  master                    7fd1a60b   -      -
spoon-knife  main                      d0dd1f61   -      -

Read the columns left to right: the repo, the branch it’s on, its short HEAD SHA, whether the working tree has uncommitted changes (DIRTY), and whether HEAD differs from the locked SHA (DRIFT). Right now everything is clean and in sync — the dashes mean “all good.”

Tip: On a terminal these are color-coded — cyan repo names, yellow revs, green for clean, red for drift. Pipe the output anywhere and it falls back to plain text automatically (it honors NO_COLOR), so it’s script-friendly by default.

🔒 3. Read the lockfile

Open haw.lock in your editor. Each repo is pinned to a full 40-character SHA in its rev field — the exact commit, not the branch name — while source-rev records what you asked for and branch records the branch that SHA came from:

# excerpt — your SHAs will differ
[[repo]]
name = "hello-world"
url = "https://github.com/octocat/Hello-World.git"
path = "hello-world"
rev = "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d"   # the resolved commit — this is the pin
source-rev = "master"                               # what you declared in haw.toml
branch = "master"                                   # the branch that SHA was resolved from
groups = ["core"]

This is the whole reproducibility trick. Your manifest said “master” (a moving target), but the lock froze the exact commit master pointed at when you synced — that’s the SHA now in rev. source-rev remembers your intent (master) and branch remembers where it came from, so haw can later re-resolve if you ask. Commit haw.lock alongside haw.toml, and anyone who clones and runs haw sync gets precisely these commits — not whatever master happens to be today.

Don’t hand-edit it. haw.lock is generated. You commit it, but you change intent in haw.toml and let haw lock / haw sync regenerate the lock.

♻️ 4. Prove it’s reproducible — re-run sync

haw sync

Because the lock exists, haw syncs to the pinned SHAs, not to wherever the branches have moved. Run haw status again and you’ll see the same clean fleet. That idempotence is the point: the lock, not the branch, is the source of truth now.

🧭 5. See drift with your own eyes

Reproducibility is only useful if you can detect when the tree wanders off the baseline. Let’s cause that on purpose. Move one repo to a different commit by hand:

cd hello-world
git checkout HEAD~1     # step one commit back — now HEAD ≠ the locked SHA
cd ..

Ask haw what it thinks:

haw status
REPO         BRANCH                   HEAD       DIRTY  DRIFT
hello-world  (detached)                553c2077   -      YES
spoon-knife  main                      d0dd1f61   -      -

There it is — DRIFT: YES on hello-world. Checking out a specific commit also detaches HEAD (hence (detached) in the BRANCH column), and its HEAD no longer matches the locked SHA. haw status flagged it, but for CI you want a command that fails on drift. That’s verify:

haw verify

verify asserts the on-disk tree matches haw.lock and exits 3 when it doesn’t — a clean, scriptable drift gate. Check the exit code:

haw verify; echo "exit code: $?"
  ✗ hello-world  drift (head != lock)
verify failed: 1 repo(s) diverge from haw.lock
exit code: 3

That exit 3 is what a CI pipeline keys on: “the tree drifted from the lock — stop the build.” (When everything matches, verify prints verified: tree matches haw.lock (2 repos) and exits 0.)

Wide-eyed, shocked reaction

haw verify finding drift in CI right before release.

Now put it back. haw sync restores every repo to its locked SHA:

haw sync
haw verify; echo "exit code: $?"
verified: tree matches haw.lock (2 repos)
exit code: 0

Clean again. You just watched the full loop: declare → sync → pin → detect drift → restore.

You just did the core loop. Declare intent, pin it, detect drift, and restore the baseline — the same four moves scale from two octocat repos to a hundred-repo fleet.

🙌 Your turn

Prove the lockfile really is the source of truth:

  • Open haw.lock and find each repo’s pinned SHA in its rev field. Confirm it’s a 40-char commit, not a branch name, and that source-rev still shows what you declared.
  • Drift one repo on purpose (cd hello-world && git checkout HEAD~1 && cd ..), run haw verify; echo $?, and confirm you get exit code 3.
  • Run haw sync to restore, then haw verify; echo $? again — back to exit 0. That round trip is the reproducibility guarantee.

✅ Recap

  • haw sync clones every repo and writes haw.lock — real Git clones, no submodules/symlinks. It’s idempotent.
  • haw tree shows the stack→repo shape; haw status shows branch/HEAD/dirty/drift per repo.
  • haw.lock pins each repo to an exact 40-char SHA in rev, with source-rev (your declared intent) and branch alongside — commit it for byte-identical rebuilds.
  • Drift = HEAD differs from the lock. haw status flags it; haw verify exits 3 on drift (your CI gate). haw sync restores the baseline.

👉 Next

You can compose and pin a fleet — now let’s live in it. Meet the cockpit that drives the whole thing from the keyboard → 4. The TUI cockpit.