git-scaffold

Getting started

Install

With Go on the machine, one command installs the latest release:

go install github.com/stephenc/git-scaffold@latest

Or take a prebuilt binary from the releases page. On Linux and macOS:

dir=$(mktemp -d) &&
archive="git-scaffold-$(uname -s)-$(uname -m).tar.gz" &&
curl -fsSL -o "$dir/$archive" \
  "https://github.com/stephenc/git-scaffold/releases/latest/download/$archive" &&
curl -fsSL -o "$dir/$archive.sha256" \
  "https://github.com/stephenc/git-scaffold/releases/latest/download/$archive.sha256" &&
if command -v sha256sum >/dev/null
then (cd "$dir" && sha256sum -c "$archive.sha256")
else (cd "$dir" && shasum -a 256 -c "$archive.sha256")
fi &&
tar -xzf "$dir/$archive" -C "$dir" git-scaffold &&
mkdir -p ~/.local/bin &&
install -m 755 "$dir/git-scaffold" ~/.local/bin/git-scaffold
[ -n "$dir" ] && rm -rf "$dir"
command -v git-scaffold

These commands write into a temporary directory, not the directory you are in. The two curl lines take the archive and the .sha256 file that the release holds beside it, and the check compares the two. install puts git-scaffold in ~/.local/bin, replacing a git-scaffold that is already there. If command -v git-scaffold shows nothing, add ~/.local/bin to the path of this shell.

On Windows, take git-scaffold-Windows-x86_64.zip (or git-scaffold-Windows-arm64.zip for an ARM machine), check it against the .sha256 file beside it, and put the git-scaffold.exe that it holds in a directory on your PATH.

The binaries hold no dynamic library, so each one runs on any machine of its system and architecture. Every command needs git on the path. Once git-scaffold is on the path, Git finds it as a subcommand: git scaffold ….

Initialize from a scaffold

Inside a Git worktree, point init at an upstream scaffold repository — any repository whose own .git-scaffold/config.toml declares the files it manages:

git scaffold init https://github.com/acme/go-template.git \
  --arg project_name=orders \
  --arg module=github.com/acme/orders

This creates .git-scaffold/config.toml, resolves the source, materializes every managed file, and writes the exact upstream commit to .git-scaffold/lock. Arguments without a source-side default are required; if one is missing, init fails clearly and tells you which. Add --ref <ref> to track a branch or tag other than the remote HEAD.

Commit all of .git-scaffold/ along with the materialized files.

The everyday cycle

git scaffold check      # exit 0: managed files match the locked materialization
git scaffold diff       # unified diff of working tree vs. locked materialization
git scaffold apply      # make the working tree match what is locked
git scaffold update     # re-resolve the ref and move to the new commit
git scaffold outdated   # exit 1 when an update is available

check, diff, and apply work against the locked commit — they never advance the lock just because the configured branch moved, and they avoid network access when the locked commit is available locally. Moving forward is always an explicit act:

$ git scaffold update
Updating scaffold

source: https://github.com/acme/go-template.git
ref:    main

71db8b7 → 93ca782

M .golangci.yml
M Makefile
A .github/workflows/security.yml
D .github/workflows/old-ci.yml

Updates are transactional: the complete new result is constructed and validated first, and on any failure — an argument problem, a patch that no longer applies — no files change and the lock stays where it was. Use git scaffold outdated in CI or cron to detect available updates cheaply, and git scaffold propose to turn an update into a pushed branch and pull request.

Adopting an existing repository

A repository that already has content — its own Makefile, its own workflows — can come under scaffold management without a scary overwrite step:

git scaffold init --existing https://github.com/acme/go-template.git \
  --arg project_name=orders \
  --arg module=github.com/acme/orders

Any managed file that differs from the scaffold's materialization is captured as an explicit override under .git-scaffold/patches/: a structured json-patch where the scaffold permits it for a JSON or YAML file and both sides parse (the file is normalized to the canonical serialization), otherwise a text-patch — a unified diff from materialized to existing content — with the pre-existing file left untouched. Pass --text-patch to capture everything as text patches. git scaffold check is clean immediately, and every divergence is now a visible patch you can whittle away over time.

Later, when you hand-edit a managed file and want to keep the change, git scaffold repatch rewrites the overrides from the working tree so check is clean again — see the command reference.

For binary-looking files (content containing NUL bytes) a text diff is not meaningful, so differing binary files cause a refusal that reports them. Adding --force alongside --existing overwrites only those binary files — text-adoptable files are still adopted, not overwritten.

Next steps