Tooling: Hooks, Submodules, Worktrees, and Friends

This chapter covers the tools around git: hooks, submodules, worktrees, aliases, configuration, and LFS.

Aliases

Git aliases are shortcuts for commands you run often. Add them to ~/.gitconfig or via git config:

git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.st status
git config --global alias.lg "log --oneline --graph --decorate --all"
git config --global alias.last "log -1 HEAD --stat"
git config --global alias.uncommit "reset --soft HEAD~1"

Now:

git lg
git uncommit

Aliases can call external commands with !:

git config --global alias.today "!git log --since='00:00' --author=\"$(git config user.name)\""

Be conservative. A repo full of custom aliases you can't remember is worse than vanilla git.

Configuration Hierarchy

Git config lives at three levels; the most specific one wins:

git config --system   # /etc/gitconfig
git config --global   # ~/.gitconfig
git config --local    # .git/config (per-repo, the default)

Show the effective value and where it came from:

git config --show-origin user.email
# file:/Users/ada/.gitconfig ada@example.com

List everything:

git config --list --show-origin

Settings worth knowing:

git config --global core.editor "vim"
git config --global core.pager "less -FRX"
git config --global core.autocrlf input           # macOS/Linux
git config --global core.autocrlf true             # Windows
git config --global pull.rebase true
git config --global rerere.enabled true
git config --global fetch.prune true
git config --global diff.colorMoved "zebra"
git config --global merge.conflictStyle "zdiff3"
git config --global init.defaultBranch main

Per-repo config (for a client's repo that needs a different email, for example):

cd client-repo
git config user.email "ada@client.com"

That setting only applies in this repo; the global one still applies everywhere else.

Hooks

Hooks are shell scripts git runs at specific points in its lifecycle. They live in .git/hooks/. Every new repo has samples (.sample suffix) as starting points.

The ones teams use most:

pre-commit      runs before git commit. Return non-zero to abort.
commit-msg      edit or validate the commit message.
pre-push        runs before git push. Abort to reject the push.
post-merge      runs after a successful merge or pull.
post-checkout   runs after switching branches.

A trivial pre-commit to block commits that introduce TODOs:

#!/usr/bin/env bash
# .git/hooks/pre-commit
if git diff --cached | grep -E '^\+.*TODO'; then
    echo "Refusing to commit: staged changes introduce TODOs."
    exit 1
fi

Make it executable:

chmod +x .git/hooks/pre-commit

Hook Managers

Hooks in .git/hooks/ are not checked into the repo. For team-wide hooks you need a manager:

  • pre-commit (pre-commit.com): Python, language-agnostic hooks. The most popular.
  • Husky: JavaScript/Node ecosystems.
  • Lefthook: Go-based, fast, multi-stage.

Typical flow: commit a .pre-commit-config.yaml to the repo, every contributor runs pre-commit install once, and the tool wires up real hooks in .git/hooks/.

Example .pre-commit-config.yaml:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
  - repo: https://github.com/psf/black
    rev: 24.4.0
    hooks:
      - id: black

Run on demand:

pre-commit run --all-files

Keep hooks fast. A five-second hook is tolerable; a 30-second one means people start running --no-verify.

Worktrees

A worktree is a second checkout of the same repo, attached to a different branch. One .git/ directory, multiple working directories.

git worktree add ../notes-hotfix hotfix/session-leak

That creates a directory ../notes-hotfix checked out at hotfix/session-leak. You can work in both directories at once; git push and git fetch operate on the shared .git/.

List and remove:

git worktree list
git worktree remove ../notes-hotfix

Great for:

  • Fixing a production hotfix while your feature branch stays checked out.
  • Running a long test on one branch while you code on another.
  • Avoiding the "stash everything, switch, do the thing, switch back, pop stash" cycle.

Worktrees share the same .git/objects/, so they're cheap. You can have 10 of them; the only cost is disk for the checked-out files.

Submodules

A submodule is a git repo embedded inside another git repo, pinned to a specific commit.

git submodule add https://github.com/someone/lib.git vendor/lib
git commit -m "add lib as submodule"

The outer repo stores vendor/lib as a pointer to a specific commit in the submodule's repo, plus a .gitmodules file.

Cloning a repo with submodules:

git clone --recurse-submodules git@github.com:ada/notes.git
# or, after cloning:
git submodule update --init --recursive

Updating a submodule to its latest upstream:

cd vendor/lib
git pull
cd ../..
git add vendor/lib
git commit -m "bump lib to latest"

Submodules are notoriously confusing. Common issues: out-of-date pointers, dirty submodule state blocking merges, contributors forgetting --recurse-submodules.

If you have the choice, prefer a package manager (npm, cargo, pip, go mod) over submodules. Submodules are for when you genuinely need source-level access to another repo from this one, not for dependency management.

Subtree as an Alternative

git subtree merges another repo's contents directly into your tree. No pointer, no extra clone step. Simpler to use, messier to contribute fixes upstream.

git subtree add --prefix=vendor/lib https://github.com/someone/lib.git main --squash
git subtree pull --prefix=vendor/lib https://github.com/someone/lib.git main --squash

Git LFS (Large File Storage)

Git is bad at large binaries. Each version of a 50MB file is stored in full, cloning takes forever, and repos bloat fast.

Git LFS replaces large files in the repo with small pointer files; the actual contents live on an LFS server. Most hosting providers (GitHub, GitLab) include LFS storage.

Setup:

git lfs install                       # once per machine
git lfs track "*.psd"                 # track this pattern with LFS
git add .gitattributes
git add design.psd
git commit -m "add design file"
git push

.gitattributes is where LFS patterns are recorded (checked in alongside the code).

When to reach for LFS:

  • Design files (.psd, .ai, .sketch).
  • Large images, video, or audio.
  • Binary build artifacts you must version for some reason.
  • ML model checkpoints over a few MB.

When not to:

  • Regular code files.
  • Small configuration binaries.
  • Things that would be better stored outside the repo entirely.

LFS has costs: bandwidth quotas, server-side storage, extra setup for contributors. Use it when the alternative is a 3GB clone, not because files happen to be binary.

Bisect, Briefly

Chapter 5 introduced git bisect. As a reminder:

git bisect start HEAD v1.4.0
git bisect run ./test.sh

Worth mentioning in the tooling chapter because many teams never use it. If you track down "when did X break" by reading commits, try bisect instead. It's almost always faster.

Custom Commands

Any executable named git-foo on your PATH becomes a git subcommand you can invoke as git foo.

# ~/bin/git-unpushed
#!/usr/bin/env bash
git log @{push}.. --oneline 2>/dev/null || git log --oneline --branches --not --remotes
chmod +x ~/bin/git-unpushed
git unpushed

Handy for team-specific shortcuts. Keep them in a dotfiles repo.

Common Pitfalls

Hooks that slow commits to a crawl. Developers run --no-verify. Every hook should take under a second or move to pre-push.

Committing to .git/hooks/. Those files aren't in the repo. Use a hook manager.

Nested submodules. A submodule that has submodules. Every command grows another --recursive. Avoid if possible.

Huge files without LFS. Once a big file is committed, it's in history. Cloning downloads all history. Consider git-filter-repo to excise it, then use LFS going forward.

Worktrees with uncommitted changes. Don't remove a worktree that has uncommitted work; the changes are gone. git status in the worktree first.

Next Steps

Continue to 12-best-practices.md for the conventions that keep a repo healthy.