- ποΈ Friday, 4:48 p.m. β engineer realizes a feature branch has the wrong starting commit
- πͺ Runs
git reset --hard origin/mainto "clean it up" β without committing the four hours of unstaged work - π
git statusis suddenly empty. So is the working tree. So isgit log - πͺ¦ Demo on Monday. No backups
- π¦ At 4:55 p.m. a senior engineer types
git reflog. Everything is still there. Five minutes of recovery vs five hours of rewriting
π€ Think: Git almost never actually deletes anything. Knowing where it hides things is the difference between panic and a 5-minute recovery.
By the end of this lecture you will:
| # | π Outcome |
|---|---|
| 1 | β Explain Git's three object types: blob, tree, commit |
| 2 | β Read what a ref is, and what HEAD really points at |
| 3 | β Recover from a "lost commit" using the reflog |
| 4 | β
Use reset --soft / --mixed / --hard deliberately |
| 5 | β
Choose between merge, rebase, and bisect for the job at hand |
| 6 | β
Use modern Git ergonomics: switch, restore, worktree, maintenance |
graph LR
A["π¦ .git/"] --> B["π§± Object Model"]
B --> C["π·οΈ Refs & HEAD"]
C --> D["πͺ€ Reflog"]
D --> E["βͺ Reset Modes"]
E --> F["π Merge vs Rebase"]
F --> G["π οΈ bisect / worktree"]
- π Slides 1-4 β Inside
.git/ - π Slides 5-9 β Refs, reflog, and three flavors of reset
- π Slides 10-14 β Merge, rebase, bisect, worktree
- π Slides 15-18 β Tags, stash, hooks, modern commands
- π Slides 19-22 β Antipatterns, real incidents, what's next
.git/
βββ HEAD # current branch ref
βββ config # local repo config
βββ objects/ # blobs, trees, commits β the whole history
β βββ 4b/ # subdirs by first 2 chars of SHA
β β βββ 825dc642cb6eb9a060e54bf8d69288fbee4904
β βββ info/
β βββ pack/ # packed objects after `git gc`
βββ refs/
β βββ heads/main # β SHA of latest commit
β βββ tags/
β βββ remotes/origin/
βββ logs/ # the reflog lives here
- ποΈ The
.git/directory is your repository. Delete it and the project is just a folder - π Every commit, blob, branch, and tag is reachable from this tree
- π§ͺ Try in your QuickNotes fork:
find .git/objects/ -type f | headβ those are your blobs and commits
graph LR
C["π Commit<br/>= snapshot + parent(s)"] --> T["π³ Tree<br/>= directory listing"]
T --> B["π Blob<br/>= file contents"]
C -. parent .-> C2["π Earlier Commit"]
| Type | Holds | Real example |
|---|---|---|
| π’ Blob | A file's bytes (no name, no metadata) | app/main.go content |
| π³ Tree | Names + modes + SHAs of blobs (and sub-trees) | The app/ directory listing |
| π Commit | Tree SHA + parent(s) + author + message | "feat(app): add /metrics" |
- π Every object is identified by the SHA-1 of its contents (Git is moving to SHA-256; new repos can opt in via
git init --object-format=sha256) - π Identical content β identical SHA β deduplication for free
π‘ Same
index.htmlin two branches? One blob, two trees referencing it.
# β
what's in HEAD?
$ git cat-file -t HEAD
commit
$ git cat-file -p HEAD
tree a1b2c3...
parent 4d5e6f...
author Dmitrii Creed <...> 1716728400 +0300
committer ...
feat(app): introduce QuickNotes
# β
peek into the tree
$ git cat-file -p a1b2c3
100644 blob d4e5f6... README.md
040000 tree 7a8b9c... app- π§ͺ
cat-file -t <sha>prints the type;-ppretty-prints - π§° These are the plumbing commands β what the porcelain (add, commit, log) calls underneath
- π¬ The
Pro Gitbook, chapter 10, walks the whole object model β required reading this week
A ref is a human-readable name pointing at a commit SHA. That's it.
| Ref | What it points at | Where it lives |
|---|---|---|
HEAD |
The current commit (usually via a branch) | .git/HEAD |
refs/heads/main |
Tip of main |
.git/refs/heads/main |
refs/tags/v1.0.0 |
A frozen commit | .git/refs/tags/v1.0.0 |
refs/remotes/origin/main |
Last known tip of remote main |
Updated by git fetch |
$ cat .git/HEAD
ref: refs/heads/feature/lab2
$ cat .git/refs/heads/feature/lab2
4b825dc642cb6eb9a060e54bf8d69288fbee4904- π― A "detached HEAD" simply means
HEADstores a SHA directly, not a branch ref
The reflog is a per-ref history of where it has been. Default retention: 90 days for reachable commits, 30 days for unreachable.
$ git reflog
b8fc480 HEAD@{0}: commit: feat(app): introduce QuickNotes
6f044dd HEAD@{1}: checkout: moving from main to s26-refactor
6f044dd HEAD@{2}: pull: Fast-forward
0a87e1c HEAD@{3}: commit: refactor: reduce prescriptiveness- π If your branch tip "disappears" β
git reflog, find the SHA,git reset --hard <sha>orgit branch rescue <sha> - π§ͺ Even after
reset --hard, the discarded commits are unreachable but still in.git/objects/untilgit gcruns
π¬ "Reflog is the most reassuring thing I learned in my first year with Git." β every senior engineer
graph LR
Cur["π HEAD now"] -- "reset --soft" --> A["β¬
οΈ HEAD moves<br/>π Index unchanged<br/>π Working tree unchanged"]
Cur -- "reset --mixed (default)" --> B["β¬
οΈ HEAD moves<br/>π Index reset<br/>π Working tree unchanged"]
Cur -- "reset --hard" --> C["β¬
οΈ HEAD moves<br/>π Index reset<br/>π₯ Working tree overwritten"]
| Mode | Touches HEAD | Touches Index | Touches Working tree | When to use |
|---|---|---|---|---|
--soft |
β | β | β | Re-arrange last few commits, keep changes staged |
--mixed |
β | β | β | "Un-add" files; keep edits in working tree |
--hard |
β | β | β | Burn down to a clean state β dangerous |
β οΈ Only--hardis destructive to uncommitted work. Always checkgit statusbefore running it.
Git 2.23 (Aug 2019) split the overloaded git checkout into two clearer commands:
| Old (still works) | New | What it does |
|---|---|---|
git checkout main |
git switch main |
Change branch |
git checkout -b feat/x |
git switch -c feat/x |
Create + switch |
git checkout main app/main.go |
git restore --source=main app/main.go |
Restore a file to a version |
git checkout -- app/main.go |
git restore app/main.go |
Discard working-tree changes |
- β
Prefer
switch/restorein new tutorials β intent is explicit - β Avoid
git checkoutfor file restore; the same command for "change branch" and "destroy my edits" is a footgun
# β
save current uncommitted work
$ git stash push -m "wip on /metrics"
# β
list stashes
$ git stash list
stash@{0}: On feature/lab2: wip on /metrics
# β
pop the most recent back (apply + delete)
$ git stash pop- πͺ€ Stash is not a substitute for a branch β entries don't survive
git gconce they fall out of the reflog (β€ 30 days unreachable) - π‘ Use
git stash --keep-indexto stash unstaged changes while keeping staged ones - π¨ Common bug: stash on branch A, switch to B, pop β conflicts. Pop on the same branch you stashed on
# β lightweight β just a ref, no metadata
git tag v1.0.0
# β
annotated β tagged object with message, author, signature
git tag -a -s v1.0.0 -m "First production release"
# π€ push tags (they don't go with regular push)
git push origin v1.0.0| Lightweight | Annotated |
|---|---|
Just refs/tags/X β SHA |
Full Git object with message |
| Cannot be signed | Can be GPG/SSH signed |
| Useful as private bookmarks | Use for all public releases |
π‘ Releases in CI/CD (next lecture) trigger on annotated, signed tags β that's how you prove the artifact really came from this codebase.
graph LR
subgraph "merge"
M1["A"] --> M2["B"] --> M3["C"]
M1 --> M4["D"] --> M5["E"]
M3 --> MM["M β¬
οΈ merge commit"]
M5 --> MM
end
subgraph "rebase"
R1["A"] --> R2["B"] --> R3["C"] --> R4["D'"] --> R5["E'"]
end
git merge |
git rebase |
|
|---|---|---|
| Resulting history | Preserves branch topology | Linear, easier to read |
| Creates new commits? | One merge commit | Rewrites every commit on the rebased branch |
| Safe on shared branches? | β | β β never rebase pushed commits others depend on |
| Conflicts resolved | Once | Once per commit on the rebased branch |
- π§ͺ Rule of thumb in this course: rebase your
feature/labNontomainbefore opening the PR; merge the PR itself β οΈ Never rebasemainitself β that's a public branch
QuickNotes returned a 500 yesterday but worked last week. Where did it break?
$ git bisect start
$ git bisect bad HEAD # current is broken
$ git bisect good v1.0.0 # this tag was fine
# Git checks out a commit ~midway
$ go test ./... # or any reproducer
$ git bisect good # or `bad`
# Git narrows further...
$ git bisect reset # when found- π― With N commits between good and bad, you find the culprit in logβ(N) steps
- π€ Want it automatic?
git bisect run go test ./...β Git iterates until the test starts failing - πͺ€ Real-world story: the Linux kernel team uses bisect to find regressions across tens of thousands of commits, multiple times a week
# β
keep main checked out in ., add an extra checkout for a hotfix
$ git worktree add ../quicknotes-hotfix hotfix/auth
$ cd ../quicknotes-hotfix
# do hotfix work...
$ cd -
# main checkout untouched the whole time
$ git worktree list
/home/you/quicknotes b8fc480 [feature/lab2]
/home/you/quicknotes-hotfix a1b2c3d [hotfix/auth]- π Available since Git 2.5 (Jul 2015), polished by 2.30+
- β‘ Faster than stashing + switching for "I need to look at another branch right now"
- π§Ή Clean up with
git worktree remove ../quicknotes-hotfix
π‘ We'll use worktrees in Lab 7 to keep the Ansible playbook and the app side-by-side without juggling stashes.
# β
schedule weekly maintenance (cron / launchd / systemd)
git maintenance start
# β
run it manually
git maintenance run --task=gc --task=loose-objects --task=incremental-repack- π§Ή Compacts loose objects, prunes the reflog, refreshes
commit-graphfor fastlog/merge/bisect - π On a multi-GB repo (Linux kernel, Chromium), this is the difference between
git logtaking 2s vs 45s - π Available since Git 2.29 (Oct 2020); enabled by default in many distros from 2.42+
.git/hooks/ is local to the repo. The pre-commit framework manages shareable hooks across the team:
# β
.pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: check-merge-conflict
- id: no-commit-to-branch
args: [--branch, main]- π‘οΈ Block bad commits at your laptop before CI burns minutes finding them
- π§ͺ In Lab 3, your CI will run
gofmt/go vet/go testβ pre-commit lets the same checks run before push - β Avoid running heavy test suites in pre-commit β engineers will disable hooks they find slow
| π₯ Antipattern | β Better |
|---|---|
git push --force to a shared branch |
git push --force-with-lease (refuses if remote moved) |
git reset --hard without checking git status first |
git stash push then reset; reflog will save you anyway |
git pull (which is fetch + merge) on a feature branch |
git fetch && git rebase origin/main for linear history |
| Committing secrets ("I'll remove it next push") | Add to .gitignore first; if leaked, rotate, then BFG-clean |
| 50-line commit messages with no subject | Subject β€ 50 chars, blank line, body explains why |
| Squash-merging a 200-commit PR | Split into smaller PRs; squash hides bisect points |
- ποΈ 2015 β A developer commits AWS access keys to a public repo
- π€ Within minutes, bots scrape GitHub for credentials and spin up cryptominers on their account
- πΈ The student's $0 AWS account ends up with a $2,300 bill by morning
- π οΈ Fixing leaked secrets means rewriting history with BFG Repo-Cleaner or
git filter-repoβ and rotating the credential, because once it's pushed, assume it's harvested - πͺ¦ Lesson:
.gitignoreyour.env, never commit*.pem, and run a secret scanner in CI (we will, Lab 9)
π€ Think: Why is "just delete the commit" not enough?
(Because everyone who cloned still has it. And bots cache it. And GitHub's API still serves the diff.)
- π§± Three objects, one truth: blobs are bytes, trees are listings, commits are snapshots
- π·οΈ A ref is just a name on a SHA β
HEAD, branches, tags, all the same idea - πͺ€ The reflog saves your job β Git almost never throws anything away
- βͺ
--soft / --mixed / --hardare different tools β pick deliberately - π Rebase your feature branch onto main, merge the PR β the best of both worlds
- π οΈ Modern commands (
switch,restore,worktree,maintenance) are not optional in 2026
π¬ "Git is hard. Print this lecture and put it on the wall." β every junior engineer eventually
- π Next lecture: CI/CD β turning every push into a test, build, and (eventually) deploy
- π§ͺ Lab 2: Explore Git's object model on the QuickNotes repo, force a
reset --hard, recover via reflog, tag a release, rebase a feature branch - π Read this week:
- π Pro Git β Chacon & Straub β Chapters 7 & 10 (the plumbing)
- π Git Magic β Ben Lynn β short, free, focused on day-2 problems
- π Git from the Bottom Up β John Wiegley β for understanding objects deeply
- π οΈ Tools to try:
- π
tigβ a curses interface overgit log(apt install tig) - π¨
git log --oneline --graph --all --decorateβ the only graph you need - π§° git-absorb β auto-creates fixup commits
- π
graph LR
P["π Last Week<br/>DevOps + Git"] --> Y["π You Are Here<br/>Git Internals"]
Y --> N["π€ Week 3<br/>CI/CD"]
N --> M["π» Week 4<br/>OS & Networking"]
π― Remember: You don't need to memorize every Git command. You need to know what's in
.git/, what HEAD points at, and how to find the reflog when something explodes.