Advanced Git
Module 09 — Advanced Git
Goal: Round out your Git toolkit with the power features pros use weekly: stash, tags, cherry-pick, bisect, hooks, submodules, and
.gitignoremastery.
You can be highly productive without these, but each one will eventually save your day.
9.1 git stash — shelve work temporarily
You’re mid-change when an urgent fix lands on another branch. You don’t want to commit half-finished work. Stash it:
git stash # shelve all uncommitted changes, clean the working dir
git stash list # see your stashes
git stash pop # reapply the most recent stash and remove it from the list
git stash apply # reapply but keep it in the list
git stash drop # delete a stash
Stash with a label and include untracked files:
git stash push -u -m "half-done navbar"
Stashes are stored as special commits under .git/refs/stash — yet another use of the object model from Module 04.
9.2 Tags — marking releases
A tag is a permanent name for a specific commit, typically a release version. Unlike branches, tags don’t move.
git tag v1.0.0 # lightweight tag (just a pointer)
git tag -a v1.0.0 -m "First release" # annotated tag (recommended: has author, date, message)
git push origin v1.0.0 # tags aren't pushed by default
git push origin --tags # push all tags
Annotated tags are stored as their own tag objects (the fourth object type from Module 04). On GitHub, tags can become Releases with downloadable assets and notes.
9.3 git cherry-pick — grab one commit
Want just one commit from another branch, not the whole thing? Cherry-pick it — Git replays that single commit onto your current branch as a new commit:
git cherry-pick a1b2c3d
Useful for backporting a bug fix from main to a release branch without bringing along everything else.
9.4 git bisect — binary-search for the commit that broke things
A bug appeared somewhere in the last 200 commits but you don’t know where. Bisect finds it in ~8 steps using binary search:
git bisect start
git bisect bad # current commit is broken
git bisect good v1.0.0 # this old commit was fine
# Git checks out a commit halfway between. Test it, then tell Git:
git bisect good # ...or... git bisect bad
# Repeat. Git zeroes in on the first bad commit.
git bisect reset # when done, return to where you were
You can even automate it: git bisect run ./test.sh lets Git run your test script at each step and find the culprit unattended. This is a genuinely magical debugging tool.
9.5 Git hooks — automation at key moments
From Module 04, .git/hooks/ holds scripts Git runs automatically. Remove the .sample suffix and make a script executable to activate it. Common ones:
pre-commit— runs before a commit is created; abort the commit (exit non-zero) if linting/tests fail.commit-msg— validate the commit message format.pre-push— run the test suite before allowing a push.
Example pre-commit that blocks committing if tests fail:
#!/bin/sh
npm test || {
echo "Tests failed — commit aborted."
exit 1
}
Because .git/hooks/ isn’t committed, teams usually manage shared hooks with a tool like Husky (JavaScript) or pre-commit (Python), which install hooks for everyone.
9.6 Submodules — a repo inside a repo
A submodule embeds another Git repository inside yours at a pinned commit — handy for shared libraries you control separately.
git submodule add https://github.com/some/lib.git libs/lib
git submodule update --init --recursive # after cloning a repo that has submodules
The parent repo stores only a pointer to a specific commit of the submodule (in a .gitmodules file plus a special tree entry), not its contents. Submodules are powerful but fiddly — use them deliberately.
9.7 .gitignore mastery
From Module 03 you know .gitignore keeps files out of Git. The pattern rules:
node_modules/ # a folder anywhere named node_modules
*.log # any file ending in .log
/secret.txt # secret.txt only in the repo ROOT (leading slash = root)
build/ # the build folder
!important.log # EXCEPTION: do track this one despite *.log above
**/temp # temp folder at any depth
Key gotcha: .gitignore only affects untracked files. If a file is already committed, adding it to .gitignore does nothing — you must stop tracking it first:
git rm --cached secret.txt # stop tracking, keep the local file
You can also keep a personal, uncommitted ignore list in .git/info/exclude (Module 04), and a global one for all your repos:
git config --global core.excludesfile ~/.gitignore_global
9.8 A few more time-savers
git blame file.js # who last changed each line, and in which commit
git show a1b2c3d # full details + diff of one commit
git diff main..feature # what differs between two branches
git log --follow file.js # history of one file, even across renames
git worktree add ../hotfix # check out another branch in a separate folder simultaneously
git blame plus git show is the classic combo for “why on earth is this line here?” archaeology.
Try it yourself
- Start editing a file, then
git stash, switch branches, switch back, andgit stash pop. - Tag a commit with an annotated tag and run
git show v1.0.0. - Create a
pre-commithook that prints a message, commit something, and watch it fire. - Use
git blameon any file to find who wrote a specific line.
Key takeaways
stashshelves work; tags mark releases (annotated tags are real objects).cherry-pickcopies a single commit;bisectbinary-searches for the commit that introduced a bug.- Hooks automate checks at commit/push time; share them via Husky or pre-commit.
- Submodules embed pinned external repos.
.gitignoreonly affects untracked files — usegit rm --cachedto stop tracking something already committed.