Git from First PrinciplesPost 12 of 16 · 16 of 16 written
- How to use this guide
- 01Why version control exists
- 02Git's data model: a content-addressed object database
- 03References: branches, HEAD and tags are pointers
- 04The three areas: working tree, index, repository
- 05Daily commands, mapped onto the model
- 06Branching and integration: merge, rebase, cherry-pick
- 07Undoing things safely
- 08Remotes and collaboration
- 09Workflows and conventions
- 10Git for AI and ML work
- 11Git with AI coding agents
- 12Power tools
- 13Under the hood: build a commit by hand
- 14Debugging mindset and recovery playbook
- 15Practice path, fluency checklist and glossary
- 16Sources
12. Power tools
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 startedAutomate 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.
-wignores whitespace changes;-Cfollows lines moved or copied from other files.- List mass-reformatting commits in
.git-blame-ignore-revsand 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 outSubmodules: 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 --recursiveCommon 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 firstAliases 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 trueThen 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?