Git from First PrinciplesPost 12 of 16 · 16 of 16 written

12. Power tools

Git · 12 of 163 min readOct 2026

Most of Git’s power tools answer one of two questions: “when, where and why did this change?” or “how do I work with a huge or composite repository?” Learn them once and they save days.

bisect: find the commit that broke it

bisect binary-searches the commit graph between a known-good and a known-bad commit. It needs about log₂(n) tests, so 1,000 commits take about 10.

git bisect start
git bisect bad                  # the current commit is broken
git bisect good v1.3.0          # this one worked
# Git checks out a commit halfway; test it, then answer:
git bisect good                 # or: git bisect bad
# repeat until Git names the first bad commit
git bisect reset                # go back to where you started

Automate it with git bisect run ./check.sh. Exit code 0 means good, 125 means “cannot test, skip”, and other codes from 1 to 127 mean bad.

For ML this is powerful. Write check.sh to run a small, seeded evaluation and exit non-zero if the metric falls below a threshold. Bisect then finds the commit that degraded accuracy. Make the check deterministic, or bisect will chase noise.

blame: the last change to each line

git blame -L 40,60 src/train.py shows, for each line, the commit that last changed it. The point is not to blame a person; it is to find the commit, then read its message and pull request for the why.

  • -w ignores whitespace changes; -C follows lines moved or copied from other files.
  • List mass-reformatting commits in .git-blame-ignore-revs and pass --ignore-revs-file .git-blame-ignore-revs. GitHub honours that file too.

Pickaxe and friends: when did this text appear?

git log -S "dropout=0.1" -p          # commits that added or removed this exact text
git log -G "lr\s*=" -p              # commits whose changed lines match a regex
git log -L :train_step:src/train.py  # the full history of one function
git grep "TODO" v1.2.0               # search an old snapshot without checking it out

Submodules: a repository inside a repository

A submodule makes the parent’s tree hold a special entry, a commit hash from another repository, plus a URL in .gitmodules. The parent therefore pins an exact commit of the child.

git submodule add https://github.com/org/eval-harness.git vendor/eval-harness
git clone --recurse-submodules <url>
git submodule update --init --recursive

Common pitfalls: forgetting to initialize, landing in detached HEAD inside the submodule, and needing commits in both repositories for one change. Use submodules only when you truly must pin another repository’s exact code. Often a pinned package is simpler: pip install "git+https://github.com/org/lib@<sha>". git subtree is another option; it copies the other project into a subfolder, which is simpler for everyone who clones.

Large repositories: shallow, partial and sparse

Technique Command What you skip Use when
Shallow clone git clone --depth 1 <url> Old history CI builds, a quick look
Blobless clone git clone --filter=blob:none <url> Old file contents, fetched on demand Long history, but you still want log and blame
Sparse checkout git sparse-checkout set src/ configs/ Folders you did not choose Monorepos
Skip LFS downloads GIT_LFS_SKIP_SMUDGE=1 git clone <url> Large LFS files; you get pointers only Inspecting a model repository without its weights

Shallow clones can give wrong answers for log, blame and merge bases. git fetch --unshallow restores the full history.

Settings worth turning on

git config --global merge.conflictStyle zdiff3   # show the base in conflicts
git config --global rerere.enabled true          # reuse recorded conflict resolutions
git config --global fetch.prune true             # drop bookmarks for deleted remote branches
git config --global push.autoSetupRemote true    # first push sets the upstream
git config --global rebase.autoStash true        # stash and restore edits around a rebase
git config --global rebase.updateRefs true       # move stacked branches during a rebase
git config --global diff.algorithm histogram     # often clearer diffs
git config --global commit.verbose true          # show the diff while writing messages
git config --global branch.sort -committerdate   # newest branches first

Aliases turn habits into single words:

git config --global alias.last "log -1 --stat"
git config --global alias.unstage "restore --staged"
git config --global alias.amend "commit --amend --no-edit"

Signing commits

The author field in a commit is plain text that anyone can set. A signature proves who made the commit, and hosts show it as “Verified”. Signing with your existing SSH key is the simplest route:

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

Then add the same public key on your host as a signing key.

.gitattributes: per-path behaviour

.gitattributes assigns behaviour to paths: line-ending normalization (* text=auto eol=lf stops Windows and macOS from fighting), custom diff drivers, LFS filters (Part 10), and marking generated files so hosts hide them in reviews.

Check yourself

  • About how many tests does bisect need across 1,000 commits?
  • What does a submodule actually store in the parent repository?
  • Which option lets you browse a huge model repository without downloading the weights?