Module 09

Advanced Git

Developer Tools Git, GitHub & GitHub Copilot

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 .gitignore mastery.

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

  1. Start editing a file, then git stash, switch branches, switch back, and git stash pop.
  2. Tag a commit with an annotated tag and run git show v1.0.0.
  3. Create a pre-commit hook that prints a message, commit something, and watch it fire.
  4. Use git blame on any file to find who wrote a specific line.

Key takeaways

  • stash shelves work; tags mark releases (annotated tags are real objects).
  • cherry-pick copies a single commit; bisect binary-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. .gitignore only affects untracked files — use git rm --cached to stop tracking something already committed.

Next: Module 10 — Meet GitHub Copilot