Advanced Git Tools
The power tools: debugging, automation, big files, releases, signing, and what's actually inside the .git folder.
git bisect — Find the Bug Commit
Bisect does a binary search through your commit history to find exactly which commit introduced a bug. Instead of manually checking commits one by one, Git cuts the search space in half each round — 100 commits takes only ~7 checks.
git bisect start
git bisect bad # current state is broken
git bisect good v1.2.0 # last known good (tag, hash, or branch)
# Git checks out a midpoint. Test it, then tell Git:
git bisect good # bug not present here
git bisect bad # bug present here
# Repeat until Git identifies the exact commit
git bisect reset # return to your branch when done
git bisect run npm test — Git runs the script at each step and determines good/bad automatically based on the exit code.git worktree — Two Branches at Once
A worktree lets you check out a second branch into a separate folder without stashing or cloning again. Both folders share the same .git history.
Typical use: you're mid-feature and an urgent hotfix arrives. Instead of stashing everything, open a second worktree for the hotfix.
# Check out the hotfix branch into a sibling folder
git worktree add ../MyApp-hotfix hotfix/login-crash
# Both folders are now independent working directories
# ./MyApp ← your feature, untouched
# ./MyApp-hotfix ← hotfix branch, ready to work in
git worktree list # see all active worktrees
git worktree remove ../MyApp-hotfix # clean up when done
Git Hooks — Automate Quality Gates
Hooks are scripts in .git/hooks/ that Git runs automatically at specific points. They're how teams enforce rules without relying on people to remember.
| Hook | When it runs | Common use |
|---|---|---|
pre-commit | Before commit is created | Run linter, block direct commits to main |
commit-msg | After you type the message | Enforce Conventional Commits format |
pre-push | Before push sends data | Run tests, block force-push to main |
.git/hooks/ are not tracked by Git — they won't be shared with the team. To share hooks, commit them to a .githooks/ folder and configure: git config core.hooksPath .githooks.Submodules — Using Another Repo Inside Your Repo
A submodule lets you include a separate Git repository inside your own. Use this when your team has a shared library that lives in its own repo and needs to be used by multiple other repos.
.sln file, use <ProjectReference> in your .csproj instead — that is just a normal .NET feature, not a submodule.Add a submodule:
git submodule add <url> libs/SharedLib
git commit -m "Add SharedLib as submodule"
git push
Clone a repo that has submodules:
# Clone and download submodules in one command
git clone --recurse-submodules <url>
# Or if you already cloned without submodules
git submodule update --init --recursive
Update a submodule to its latest version:
cd libs/SharedLib
git pull origin main
cd ../..
git add libs/SharedLib
git commit -m "Update SharedLib to latest"
git push
git pull followed by git submodule update --recursive to get the new version.Git LFS — Large Files
Git stores every version of every committed file forever, which is a problem for large binaries (design files, videos, datasets) — they bloat every clone. Git LFS replaces them in history with small pointer files, storing the actual content separately.
# One-time setup per machine
git lfs install
# Tell LFS which files to track (writes/updates .gitattributes)
git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes
git commit -m "chore: track large files with Git LFS"
After that, git add/commit/push work exactly as normal — LFS intercepts the tracked file types automatically.
Tags & Releases
Tags mark a specific point in history — typically a release version. Unlike branch names, tags do not move as you add more commits.
# Create a tag for the current commit
git tag -a v1.2.0 -m "Release version 1.2.0"
# Push the tag to the remote
git push origin v1.2.0
# List all tags
git tag
# Push all tags at once
git push origin --tags
On GitLab, tags appear under Repository → Tags and can be turned into Releases with release notes. On GitHub, tags appear under the repo's Releases tab, where you can create a Release directly from a tag.
Commit Signing — Verified Badges
A signed commit proves it really came from you, not someone who set user.email to your address. GitLab and GitHub both show a Verified badge next to signed commits.
Simplest method — sign with the SSH key you may already have set up (see SSH Key Setup):
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 a second time under your account's signing keys section (separate from the authentication key entry) — GitLab: Edit profile → SSH Keys → Usage type: Signing; GitHub: Settings → SSH and GPG keys → New SSH key → Key type: Signing Key.
gpg --full-generate-key to create a key, git config --global user.signingkey <key-id>, then git config --global commit.gpgsign true — and paste the exported public key (gpg --armor --export <key-id>) into the GPG Keys section instead of SSH Keys.Git Internals — How Git Actually Stores Things
You don't need this to use Git daily — but understanding it makes everything else make sense. Why does rebase "rewrite history"? Why is force-push dangerous? It comes from this.
Git stores four types of objects, each identified by a SHA hash:
| Object | What it stores |
|---|---|
| blob | Contents of one file (no filename, just bytes) |
| tree | A folder — maps filenames to blobs and sub-trees |
| commit | Pointer to a tree + parent commit(s) + author + message |
| tag | A named pointer to a specific commit |
Why this explains common confusion:
- Rebase "rewrites history" because a commit's hash is computed from its content + parent hash. Change the parent → new hash → it's literally a new commit object.
- Force-push is dangerous because the remote just stores which hash a branch name points to. Force-push replaces that pointer. The old commits still exist for ~30 days but nothing points to them.
- Git is fast because if two commits share identical files, they point to the same blob object — no duplication.
# Explore internals yourself
git cat-file -t HEAD # shows object type: "commit"
git cat-file -p HEAD # shows commit contents
git cat-file -p HEAD^{tree} # shows the root folder snapshot
GitFlow vs GitHub Flow
These are two common patterns for how a team organises branches. Choose one and stick to it.
| GitHub Flow | GitFlow | |
|---|---|---|
| Best for | Web apps, continuous delivery | Versioned releases, mobile, libraries |
| Main branches | main only | main + develop |
| Feature work | Branch off main, MR back to main | Branch off develop, MR back to develop |
| Releases | Deploy directly from main | Create a release/x.y branch, merge to main + tag |
| Hotfixes | Branch off main, MR to main | Branch off main, MR to main AND develop |
| Complexity | Simple — one long-lived branch | More structure, more branch management |