Git
Git from the object store up: what add and commit write, how merges decide, how to undo and recover, release and CI workflows, big-repo tools and a leaked-secret playbook, with live labs and 13 runnable incidents.
Overview
How to use this sheet
Every table below lists a command, what it does, and how risky it is. The risk column uses these four labels, so you can scan for danger before you press Enter.
- Safe — Changes nothing, or only adds new objects and refs
- Rewrites history — Moves the branch to different commits: new ones with new IDs, or an older one. Fine on local work; others must recover if they already pulled it
- Loses uncommitted work — Overwrites files or the index. Nothing recorded those edits, so no command brings them back
- Deletes files — Removes untracked files from disk. Git never stored them
The reflog can undo almost anything that was once committed. It cannot undo the last two rows. When a command is marked with them, commit or stash first.
The page follows a change from your keyboard to production: how Git stores it, how branches combine, how to undo and recover, how it travels to a remote, how releases, CI and bots handle it, and what to do when it goes wrong. Thirteen runnable incidents close the page.
- Object trace — the real blob, tree and commit ids that add and commit write
- Three trees, commit graph, remotes, bisect — interactive Git models you drive with commands
- Merge trace — how a three-way merge decides each region, ported from Git's own merge code
- Rebase ids, reflog, bisect traces — why ids change, how reset --hard is undone, how bisect picks commits
- Six workflows — PR to production, hotfix backport, semantic-release, hooks and bots, monorepo CI, leaked secret
How Git stores your work
In plain words: Git is a photo album of your project. Every commit is a full photo of every file, labelled with a fingerprint of its contents, and each photo remembers the one taken before it. Same contents, same fingerprint, so nothing is stored twice.
A commit is a full snapshot, not a list of changes. Git stores four kinds of objects, each named by the SHA-1 of its own content, so identical content is stored once and any change to a file changes every id above it.
- blob — the bytes of one file, no name, no mode — sha1 of blob \0 + bytes
- tree — one directory: mode, name and id of each entry (blobs and subtrees) — sha1 of tree \0 + entries
- commit — one root tree, parent ids, author, committer, message — sha1 of commit \0 + those lines
- tag (annotated) — an object id, a tag name, the tagger and a message — sha1 of tag \0 + those lines
Because a commit names its parent, its id covers the whole history behind it. Change one old commit and every descendant gets a new id: that is why rebase, amend and filter-repo rewrite history.
- objects/ce/013625… — one loose object: the header and bytes, zlib-compressed, filed under its id
- objects/pack/pack-.pack and .idx — many objects in one file, most stored as deltas against similar objects; the .idx finds them by id. git gc writes them
- refs/heads/main — the commit id the branch points to: 40 hex characters and a newline
- packed-refs — many refs in one file, written by git pack-refs and git gc
- HEAD — ref: refs/heads/main, or a bare commit id when detached
- index — the staging area: path, mode, blob id and file stat data for every tracked file
- logs/HEAD, logs/refs/heads/… — the reflogs: every position HEAD and each branch has held
Check it yourself: the script hashes a file by hand, opens the compressed object, and packs it with git gc.
> [!NOTE] Ids are SHA-1 by default. git init --object-format=sha256 (Git 2.29+, marked experimental) creates a SHA-256 repository, which cannot yet exchange objects with SHA-1 repositories. Git 2.45 also added a second ref store, git init --ref-format=reftable.
The three trees
In plain words: Picture a desk, an outbox tray and a filing cabinet. You edit on the desk (working tree), put finished pages in the tray (git add, the index), and git commit files the whole tray in the cabinet (HEAD) as one snapshot.
Most commands copy content between three places: the working tree (your files), the index (what the next commit will contain) and HEAD (the last commit). Run commands in the lab and watch which of the three each one changes.
The three diff commands just pick two of these places.
- git diff — working tree with index — edits not yet staged
- git diff --staged — index with HEAD — what the next commit will contain (--cached is the same flag)
- git diff HEAD — working tree with HEAD — every change since the last commit
A change, from branch to production
In plain words: Every team change takes the same trip: a branch, a pull request, checks and review, a merge to main, then a release. Step through it once and the rest of this page is just the tools for each stop.
Setup and config
In plain words: Settings come in three layers, like house rules, your own rules and this room's rules. The most specific layer wins.
Git reads settings from several files. The narrowest one wins: a repository's setting (--local) beats your user-wide one (--global), which beats the machine-wide one (--system).
- git config --global user.name "Ada" — name recorded in your commits — Safe
- git config --global user.email ada@example.com — email recorded in your commits — Safe
- git config user.email work@example.com — override for this repository only (--local is the default when writing) — Safe
- git config --list --show-origin — every setting and the file it came from — Safe
- git config --global init.defaultBranch main — branch name for new repositories — Safe
- git config --global pull.rebase true — git pull rebases instead of merging — Safe
- git config --global push.autoSetupRemote true — first git push of a new branch sets its upstream (Git 2.37+) — Safe
- git config --global fetch.prune true — every fetch drops your origin/* copies of branches deleted on the remote — Safe
- git config --global rebase.autoStash true — stash local edits around a rebase, then restore them — Safe
- git config --global merge.conflictStyle zdiff3 — conflict markers also show the common ancestor (Git 2.35+) — Safe
- git config --global rerere.enabled true — record conflict resolutions and replay them next time — Safe
- git config --global diff.algorithm histogram — often clearer diffs for moved code than the default Myers — Safe
- git config --global alias.lg "log --oneline --graph --all" — make git lg a short command — Safe
- git config --global credential.helper cache — keep HTTPS credentials in memory (900 seconds by default) — Safe
Stage and commit
In plain words: Committing is packing a box. git add puts things in the box (all of a file, or with -p just the parts you pick); git commit seals it with a label. --amend reopens the last box and seals a new one in its place.
- git init / git clone — start a repository / copy one with its full history — Safe
- git status -sb — short status plus the branch and its upstream — Safe
- git add — stage a file, new or changed — Safe
- git add -p — stage hunk by hunk; you read every change first — Safe
- git add -u — stage changes and deletions of tracked files, no new files — Safe
- git add -A — stage everything: new, changed and deleted — Safe
- git commit -m "msg" — commit what is staged — Safe
- git commit -a -m "msg" — stage changed and deleted tracked files, then commit. New files are skipped — Safe
- git commit --amend — replace the last commit with a new one — Rewrites history
- git commit --amend --no-edit — add staged changes to the last commit, keep its message — Rewrites history
- git commit --fixup= — a fixup! commit that rebase --autosquash folds into — Safe
- git rm --cached — stop tracking a file but keep it on disk — Safe
- git mv old new — rename a tracked file — Safe
Look around: log, show, diff
In plain words: log is the timeline, show opens one entry in it, and diff puts two snapshots side by side and lists what changed.
- git log --oneline --graph --all — compact history of every branch as a graph — Safe
- git log --first-parent main — one line per merge into main, skipping the merged branches' own commits — Safe
- git log -p — every change to one file, with diffs — Safe
- git log --follow — history of one file across renames — Safe
- git log main..feature — commits on feature that are not on main — Safe
- git log --author=Ada --since=2.weeks — filter by author and date — Safe
- git show — one commit: message and diff — Safe
- git show : — a file as it was in that commit — Safe
- git diff main...feature — changes on feature since it split from main — Safe
- git diff --stat — files changed and line counts only — Safe
- git diff --word-diff — show changes word by word — Safe
- git range-diff main old-feature feature — compare two versions of a branch, such as before and after a rebase — Safe
Naming commits
In plain words: These are addresses for commits, like "three stops back on this line" (HEAD~3) or "the other line that joined here" (HEAD^2). Any command that wants a commit accepts any of them.
Any command that takes a commit accepts these forms.
- HEAD — the commit you are on
- HEAD~3 — three commits back, always following the first parent
- HEAD^2 — the second parent (only merge commits have one)
- A..B — commits reachable from B but not from A
- A...B — commits reachable from A or B but not both
- @{u} — the upstream of the current branch, such as origin/main
- @{-1} — the branch you were on before (git switch - uses it)
- HEAD@{2} — where HEAD was two moves ago, from the reflog
- main@{yesterday} — where main was yesterday, from the reflog
- :/fix login — the newest commit whose message matches the text
Branches
In plain words: A branch is a sticky note stuck on one commit. When you commit, Git moves the note you are on forward to the new commit. Deleting a branch peels off the note; the commits stay until Git cleans up.
A branch is a ref: a file (or a line in packed-refs) holding one commit id. Committing moves the branch you are on; checking out a tag or a commit leaves you on a detached HEAD, where new commits belong to no branch.
- git branch / git branch -vv — list branches / also show upstream and ahead or behind — Safe
- git switch — move to a branch — Safe
- git switch -c — create a branch here and move to it — Safe
- git switch - — go back to the previous branch — Safe
- git switch --detach — look at a commit without a branch — Safe
- git branch -m — rename the current branch — Safe
- git branch --merged — branches whose tips are already in HEAD, safe to delete — Safe
- git branch -d — delete a branch; refuses if it is not merged — Safe
- git branch -D — delete a branch even if unmerged — Safe if you act soon: the message prints its tip, and HEAD's reflog has it if you had it checked out
- git push origin --delete — delete the branch on the remote — Removes it for everyone
git switch and git restore (Git 2.23+) split the two jobs of the older git checkout: changing branches and overwriting files. With the split, you cannot throw away a file by mistyping a branch name.
Merge
In plain words: If main has not moved since you branched, merging just slides the main note forward (a fast-forward). If both moved, Git ties the two lines together with a merge commit that has two parents.
A merge joins two lines of work. If your branch has no commits the other lacks, Git just moves the pointer forward (a fast-forward). Otherwise it makes a merge commit with two parents.
- git merge — merge into the current branch; fast-forward when possible — Safe
- git merge --no-ff — always make a merge commit, keeping the branch visible — Safe
- git merge --ff-only — merge only if it is a fast-forward, else fail — Safe
- git merge --squash — stage all the branch's changes as one new change; you commit it — Safe
- git merge --abort — stop and try to return to the state before the merge — Safe if you started with a clean tree
- git diff --name-only --diff-filter=U — list files that still have conflicts — Safe
- git checkout --ours / --theirs — take one side's version of a conflicted file — Loses uncommitted work
- git merge-file -p ours base theirs — three-way merge of three files, printed; exit code = number of conflicts — Safe
How each file is merged
For every file both sides changed, Git diffs the merge base against each side and walks the regions. The trace below runs Git's own algorithm (ported from xdiff/xmerge.c, checked against git merge-file on 16,000 random inputs): type your own base, ours and theirs.
- changed on one side only — that side, silently
- the identical change on both sides — taken once, no conflict
- changed differently on both sides — conflict markers
- changes on adjacent lines, no unchanged line between — conflict too: Git needs a line of context to keep them apart
During a merge, ours is the branch you are on and theirs is the branch coming in. During a rebase the two are swapped, because Git replays your commits onto the other branch: ours is the upstream, theirs is your commit.
> [!NOTE] git merge (the ort strategy, default since Git 2.34) runs the same file merge with the histogram diff, and git merge-file with Myers, so on files with many repeated lines the two can draw hunk boundaries differently.
Rebase and rewriting history
In plain words: Rebase picks up your commits and replays them, one at a time, on top of the latest main, like re-applying your edits to the newest draft. The results are new copies with new ids, which is why you never rebase commits other people already have.
A rebase replays your commits one by one on a new base. Each replayed commit gets a new parent, and usually a new tree, so it gets a new ID.
- git rebase main — replay the current branch on top of main — Rewrites history
- git rebase -i HEAD~5 — edit the last five commits in a todo list — Rewrites history
- git rebase -i --autosquash main — same, with fixup! commits already moved into place — Rewrites history
- git rebase --onto main old-base — move the commits after old-base onto main — Rewrites history
- git rebase --update-refs main — also move other branches that point into the rebased commits (Git 2.38+) — Rewrites history
- git rebase --continue / --skip / --abort — after a conflict: go on, drop this commit, or return to the start — Safe
- git pull --rebase — fetch, then rebase local commits on the upstream — Rewrites history
--onto is the tool for stacked branches: after the branch underneath was squash-merged, it replays only your own commits. Try it here, then in incident 10 below.
The interactive todo list shows the oldest commit first. Change the word at the start of a line, or move lines to reorder commits.
- pick — keep the commit
- reword — keep it, edit the message
- edit — stop after it so you can amend or split it
- squash — fold into the commit above; combine both messages
- fixup — fold into the commit above; drop this message (fixup -C keeps this one instead)
- drop — remove the commit (deleting the line also works)
- exec — run a shell command at this point, such as the tests
- break — stop here; git rebase --continue resumes
Merge, squash or rebase into main
Pick one policy per repository and enforce it in the host's branch settings.
- Merge commit (--no-ff) — the branch's commits plus one merge commit — git log --first-parent reads one entry per pull request; git revert -m 1 undoes a whole PR — branch noise ("fix typo") lands on main unless authors clean up first
- Squash merge — one new commit per pull request — one commit to revert, cherry-pick or bisect per PR — the branch's commits are not on main: git branch -d refuses, and a branch stacked on it needs rebase --onto
- Rebase and merge — the branch's commits, replayed in a line — readable linear history with real per-step commits — every commit lands on main, so each should build; on GitHub this always writes new ids and committer info
Undo: pick the right command
In plain words: Shared mistakes get an opposite commit (revert), so nobody's history changes under them. Private mistakes can simply be erased (reset). One messed-up file is copied back from a good version (restore).
The right undo depends on where the mistake lives and whether anyone else has it.
The three reset modes differ only in how many of the three trees they reset. Try each one in the lab.
- git reset --soft — moved — kept — kept — Rewrites history (changes stay staged)
- git reset (--mixed, the default) — moved — reset — kept — Rewrites history (changes stay, unstaged)
- git reset --hard — moved — reset — reset (untracked files in the way too) — Loses uncommitted work
- git reset --keep — moved — reset — updated only where the commits differ — Safe: stops if that would overwrite a local edit
- git revert — new commit that reverses ; history is kept — Safe
- git revert -m 1 — revert a merge, keeping parent 1 as the mainline — Safe
- git reset --soft HEAD~1 — undo the last commit, keep its changes staged — Rewrites history
- git restore --staged — unstage a file; your edits stay — Safe
- git restore — throw away unstaged edits to a file — Loses uncommitted work
- git restore --source= — bring back a file as it was in — Loses uncommitted work
Recovery: what the reflog can bring back
In plain words: The reflog is Git's private undo history: every place your branch and HEAD pointed to, kept on your machine for about 90 days. If you ever committed something, it is almost always still there. If you never committed it, Git never saw it.
The reflog records every position of HEAD and of each branch in your repository. It is local only: clones and pushes never copy it. Step through a reset --hard and its undo with the real ids and the exact lines git reflog prints.
By default git gc expires reflog entries after 90 days (gc.reflogExpire), and entries no longer reachable from the branch tip after 30 days (gc.reflogExpireUnreachable).
- reset --hard over committed work — yes — git reflog, then git reset --hard HEAD@{n}
- a rebase or merge you regret — yes — git reset --hard ORIG_HEAD right after it, or the reflog
- deleted an unmerged branch — yes — the (was ) in the delete message, or git reflog, then git branch
- dropped or cleared a stash — maybe — git fsck --unreachable lists stash commits no ref points to
- reset --hard or restore over uncommitted edits — no — nothing recorded them
- git clean -f — no — untracked files were never stored
- a teammate force-pushed over your commits — only from a clone that still has them — your own reflog, if you had fetched or made those commits
- git reflog — list where HEAD has been — Safe
- git reflog show — the same for one branch — Safe
- git branch rescue — put a branch on a lost commit so it stays safe — Safe
ORIG_HEAD is written by reset, merge, rebase and am before they move HEAD, so it is the quickest way back from any of them.
Remotes: fetch, pull, push
In plain words: origin/main is a bookmark of where the server's main was the last time you looked. fetch refreshes the bookmark without touching your work, pull is fetch plus combining it into your branch, and push sends your commits up.
A remote is another repository; origin/main is your local, read-only copy of where its main was at your last fetch. Fetch and push move commits and update those copies.
- git remote -v — list remotes and their URLs — Safe
- git remote add upstream — add another remote, such as the original of a fork — Safe
- git fetch --prune — download new commits and drop your origin/* copies of branches deleted on the remote; your own branches do not move — Safe
- git pull — fetch, then integrate. With no setting, it only fast-forwards and fails when the histories diverged — Safe
- git pull --rebase — fetch, then rebase your local commits on top — Rewrites history
- git push -u origin — push and set the upstream — Safe
- git push --force-with-lease — force only if the remote is still where your last fetch saw it — Rewrites history
- git push --force-with-lease --force-if-includes — also refuse if you never integrated the remote tip you are replacing (Git 2.30+) — Rewrites history
- git push --force — replace the remote branch no matter what is there — Rewrites history (can erase others' commits)
--force-with-lease compares against your remote-tracking branch. Anything that fetches in the background (an editor, a cron job) updates that branch and quietly removes the protection. --force-if-includes closes that gap by also requiring the remote tip to be in your branch's reflog. Incident 13 shows both refusals with real output.
Cherry-pick, tags and releases
In plain words: Cherry-pick copies one change, not the whole branch: the fix comes over as a new commit with the same edits. A tag is a permanent name on one commit, like v1.2.0, so a release can always be found again.
Cherry-pick copies one commit's change onto another branch as a new commit. It is how a fix reaches a release line without the rest of main.
- git cherry-pick — copy one commit's change onto this branch as a new commit — Safe
- git cherry-pick -x — also note the source commit in the message — Safe
- git cherry-pick A^..B — copy the range A through B — Safe
- git cherry-pick --continue / --abort — after a conflict: go on, or return to the start — Safe
- git log --oneline --cherry-mark main...release — = marks commits whose change is on both branches — Safe
- git tag v1.2.0 — lightweight tag: just a name for a commit — Safe
- git tag -a v1.2.0 -m "Release" — annotated tag with message, tagger and date (for releases) — Safe
- git tag -s v1.2.0 -m "Release" — signed annotated tag — Safe
- git describe — nearest annotated tag plus commits since, such as v1.4.1-3-g1a0c619 — Safe
- git push origin v1.2.0 — push one tag; plain git push sends no tags — Safe
- git push --follow-tags — push branches plus annotated tags that point into them — Safe
- git tag -d v1.2.0 — delete a local tag — Safe
Trunk first, then backport
In trunk-based development everything merges to main; a release branch is cut per minor version and only receives fixes. A fix goes to main first, then is cherry-picked with -x and tagged.
Versions from commit messages
With Conventional Commits, the release tool reads the version bump from the messages and does the tagging and publishing in CI.
- fix: handle 429 / perf: … — patch: 2.3.1 → 2.3.2
- feat: add retry option — minor: 2.3.1 → 2.4.0
- a BREAKING CHANGE: … footer — major: 2.3.1 → 3.0.0
- docs:, chore:, test:, style: — no release
- feat!: … without the footer — no release with the default Angular preset; set preset: "conventionalcommits"
Signed commits and tags
A signature proves which key made a commit or tag, not that the code is good. Git 2.34+ can sign with an SSH key (OpenSSH 8.8+) as well as GPG.
- use your SSH key — git config --global gpg.format ssh and git config --global user.signingkey ~/.ssh/id_ed25519.pub
- sign every commit and tag — git config --global commit.gpgSign true and git config --global tag.gpgSign true
- sign one — git commit -S -m "…" / git tag -s v1.2.0 -m "…"
- verify — git verify-commit HEAD, git verify-tag v1.2.0, git log --show-signature; SSH needs gpg.ssh.allowedSignersFile
Verifying a good SSH signature prints this (real output).
Hosts show a commit as verified when the key is registered to the author's account. Rebasing or filtering signed commits creates new, unsigned (or re-signed by you) commits.
Stash
In plain words: A stash is a drawer for unfinished work. git stash sweeps your uncommitted edits into it so the desk is clean (to switch branches or pull), and git stash pop puts them back.
A stash saves uncommitted changes on a stack and cleans the working tree.
- git stash push -m "msg" — stash tracked changes (git stash alone does the same) — Safe
- git stash -u — also stash untracked files — Safe
- git stash -a — also stash ignored files — Safe
- git stash --staged — stash only what is staged (Git 2.35+) — Safe
- git stash -k — stash, but leave the staged changes in place — Safe
- git stash list / git stash show -p stash@{1} — list stashes / show one as a diff — Safe
- git stash apply — apply the newest stash and keep it — Safe
- git stash pop — apply the newest stash and drop it if it applied cleanly — Safe
- git stash branch — new branch from where the stash was made; applies the stash, then drops it — Safe
- git stash drop / git stash clear — delete one stash / all stashes — Loses uncommitted work (only fsck can find them)
Find a bug: bisect, blame, search
In plain words: Bisect is a guess-the-number game played on history: you say good or bad, Git halves the range, and in about ten steps it names the commit that broke things. Blame and the pickaxe answer who wrote a line and when a word appeared.
git bisect is a binary search over history. Mark commits in the lab, then step through the trace, which picks the same commits and prints the same counts as real git bisect.
Each test halves the range, so about 1,000 commits need about 10 tests (2 to the 10th is 1,024).
- git bisect start — begin, with one bad and one good commit — Safe
- git bisect good / git bisect bad — mark the commit Git checked out — Safe
- git bisect skip — this commit cannot be tested; pick another — Safe
- git bisect run ./test.sh — automate it with the script's exit code — Safe
- git bisect reset — finish and return to where you started — Safe
- git blame -w — last change to each line, ignoring whitespace — Safe
- git blame -L 40,60 — blame only lines 40 to 60 — Safe
- git blame -C — also follow lines moved or copied from files changed in the same commit — Safe
- git blame --ignore-revs-file .git-blame-ignore-revs — skip listed commits such as bulk reformatting — Safe
- git log -S'parseDate' — commits that added or removed that string (the pickaxe) — Safe
- git log -G'parse\w+' — commits whose added or removed lines match a regex — Safe
- git log --grep='timeout' — commits whose message matches — Safe
- git log -L:parseDate:src/date.js — history of one function — Safe
- git log --diff-filter=D -- — the commit that deleted a file — Safe
The script's exit code tells bisect run what to do.
- 0 — good
- 1 to 127, except 125 — bad
- 125 — cannot test; skip this commit
- anything else — stop the bisect
Ignore and clean
In plain words: .gitignore is a do-not-pack list, and it only works on files Git is not already tracking. git clean throws away files Git never saved, so there is no getting them back: always look first with -n.
- .gitignore — patterns for untracked files to ignore; tracked files are not affected — Safe
- git rm --cached — stop tracking a file so .gitignore applies to it — Safe
- git check-ignore -v — which ignore rule matches a path, and in which file — Safe
- git clean -n — dry run: list what would be deleted — Safe
- git clean -f — delete untracked files (-f is required by default) — Deletes files
- git clean -fd — also delete untracked directories — Deletes files
- git clean -fdx — also delete ignored files, such as .env and node_modules — Deletes files
- git clean -fX — delete only ignored files — Deletes files
Hooks, CI and bots
In plain words: Hooks are checks on your own laptop that you can skip. CI and branch protection are the guard at the gate that nobody can skip. Use the first for speed and the second for rules.
Hooks run on the developer's machine, before anything is shared; branch protection and required checks run on the server and cannot be skipped. Use both: hooks for fast feedback, CI for enforcement.
- .git/hooks/pre-commit — runs before a commit; non-zero exit stops it — Safe
- git commit --no-verify — skip the pre-commit and commit-msg hooks — Safe
- git config core.hooksPath .githooks — use a tracked hooks folder, since hooks are not cloned — Safe
- pre-commit install — install the pre-commit framework's hook from .pre-commit-config.yaml — Safe
- pre-commit run --all-files — run every hook on every file, as CI does — Safe
- SKIP= git commit … — skip one pre-commit hook — Safe
- gh pr merge --auto --squash — merge as soon as required checks and reviews pass — Safe
Big repositories and parallel work
In plain words: For a huge repository, download less: less history (--depth), fewer file contents (--filter), fewer folders on disk (sparse checkout), and big binaries kept outside Git (LFS).
A monorepo makes every clone, checkout and CI run pay for everyone's code. Git gives you four knobs: less history, fewer file contents, fewer files on disk, and path-filtered CI.
- CI that builds one commit — git clone --depth 1 — the latest commit only — log, blame and diffs against older commits fail until git fetch --deepen= or --unshallow
- full history, file contents on demand — git clone --filter=blob:none — every commit and tree; blobs download when checked out — the server must allow filters; a later git log -p or blame fetches old blobs
- commits only, trees on demand too — git clone --filter=tree:0 — the smallest clone that keeps history — commands that read old trees fetch them on demand: fine for one build, slow for daily work
- only some directories on disk — git sparse-checkout set services/api libs/common — those directories plus the root files — cone mode is the default since Git 2.37
- large binaries — git lfs track "*.psd" — small pointer files in Git, content on an LFS server — every clone needs git-lfs; moving existing files needs git lfs migrate import, a history rewrite
- two branches at once — git worktree add -b hotfix ../hotfix origin/main — a second working tree that shares the object store — a branch can be checked out in only one worktree at a time
- keep a large repo fast — git maintenance start — scheduled background tasks: prefetch, commit-graph, loose objects, incremental repack — runs from your OS scheduler; git maintenance unregister takes the repository off the schedule
git lfs track "*.psd" writes this rule to .gitattributes; commit that file too.
A leaked secret
In plain words: Once a password or key has been pushed, assume someone copied it. Change the lock first (rotate the secret), then clean the history; cleaning alone does not help.
Rotating the credential is the fix; rewriting history only limits further exposure. Plan the rewrite like an outage: every clone, fork and open pull request is affected.
- 1 — revoke or rotate the secret, check its usage logs — copies already exist where you cannot reach
- 2 — git clone --mirror, then git filter-repo --sensitive-data-removal --invert-paths --path (filter-repo 2.47+) — removes the file from every commit on every ref
- 3 — git push --force --mirror origin — replaces every branch and tag; GitHub refuses refs/pull/*
- 4 — ask the host's support to purge pull request refs and cached views — they hold the old commits
- 5 — everyone re-clones, or rebases (never merges) old branches — one merge of old history brings the secret back
- 6 — push protection, secret scanning, a pre-commit hook — stop the next one at the source
Incidents to practice
Each script below builds the broken repository, applies the fix, and checks the end state. Press Run, read the output, then change the fix and break it on purpose.
- 1 — undo the last commit, keep the work — git reset --soft HEAD~1
- 2 — committed on main instead of a branch — git branch then git reset --keep origin/main
- 3 — deleted an unmerged branch — git branch from the delete message or the reflog
- 4 — lost commits to reset --hard — git branch rescue HEAD@{1}
- 5 — one commit does two things — rebase -i with edit, reset HEAD~1, two commits
- 6 — merge conflict — read the markers, edit, git add, git commit
- 7 — which commit broke the test — git bisect run ./test.sh
- 8 — ship a fix on the release branch — git cherry-pick -x, git tag -a
- 9 — who added this string, who wrote this line — git log -S, git blame -L
- 10 — stacked branch after a squash merge — git rebase --onto main api
- 11 — a file in the last commit, a secret in history — git rm --cached + --amend; filter-repo
- 12 — take one file from another branch — git restore --source= --
- 13 — force-push without erasing a teammate — --force-with-lease --force-if-includes
- Undo the last commit, keep the work
You committed too early. reset --soft moves only the branch pointer, so the commit's changes stay staged, ready to edit and commit again.
> [!TIP] Already pushed? Do not reset a shared branch: git revert HEAD adds an inverse commit instead.
- Committed on main instead of a feature branch
Two commits landed on local main. A branch is only a name, so first put a name on the work, then move main back to where the remote has it.
> [!TIP] --keep rather than --hard: it refuses to run if it would overwrite an uncommitted edit, while --hard would silently discard it.
- Recover a deleted branch
git branch -D deletes only the name; the commits stay until garbage collection prunes them. The delete message prints the old tip, and the reflog records it too.
> [!TIP] Prefer git branch -d: it refuses to delete a branch whose commits are not merged.
- Commits lost to reset --hard
The commits still exist; nothing points at them. Pin the old tip with a branch first, so that nothing else (another reset, a gc) can lose it, then move back.
> [!WARNING] The script also shows the limit: a staged file that was never committed is gone after reset --hard. Commit or stash before any command marked Loses uncommitted work.
- Split one commit into two
A commit that fixes a bug and adds a feature cannot be reverted or cherry-picked separately. Interactive rebase stops at it (edit), reset HEAD~1 turns it back into working-tree changes, and you commit them in two parts. GIT_SEQUENCE_EDITOR edits the todo list from the script; by hand you would change pick to edit in your editor.
> [!TIP] For a commit that is not pushed yet, git reset HEAD~1 and git add -p do the same without a rebase.
- Resolve a merge conflict
Both branches changed the same line. With zdiff3 the markers show three versions: yours (HEAD), the common base (after |||||||), and theirs. The base tells you what each side changed: 30 became 45 on main and 60 on the feature.
> [!TIP] Not ready to decide? git merge --abort returns to the state before the merge.
- Find the commit that broke a test
git bisect run checks out commits and runs the test, reading only its exit code. The test script sits outside the repository so the same test runs at every commit; 39 candidates take 5 runs.
> [!TIP] Exit 125 from the script for commits that cannot be tested (it does not build); bisect skips them instead of blaming them.
- Ship a hotfix on the release branch
The release branch must get the fix and nothing else from main. cherry-pick -x copies the one commit and records its origin; --cherry-mark then proves the patch is on both lines.
> [!TIP] Fix on main first, then backport. A fix made only on the release branch is lost again in the next release.
- Who added this setting, and who wrote this line
The pickaxe -S lists commits that change how many times a string occurs, which finds both the commit that added it and the one that removed it. blame -L answers for specific lines in the current version.
> [!TIP] Blame shows the last commit that touched a line. If that was a reformat, add it to .git-blame-ignore-revs, or run git log -L 2,2:settings.ini for the line's full history.
- Move a stacked branch after a squash merge
ui was built on api, and api was squash-merged. main now has api's change as one new commit, so git rebase main would try to replay api's original commits again. --onto main api replays only the commits after api's tip.
> [!TIP] With git rebase --update-refs (Git 2.38+), rebasing the top branch of a stack also moves the branches below it.
- Remove a file from the last commit, and from history
If the commit is not pushed, git rm --cached plus --amend is enough. Once the file sits in older commits, every commit after it must be rewritten. The runner has no git-filter-repo, so the script falls back to git filter-branch, which the Git docs themselves warn is slow and easy to misuse.
> [!WARNING] If the file held a secret, rotate it first. A rewrite does not reach other clones, forks or caches.
- Restore one file from another branch
You want one file from an experiment, not the whole branch. git restore --source writes that file into your working tree only; nothing is staged or committed until you say so.
> [!TIP] Add --staged --worktree to stage it as well. The older git checkout tuning -- deploy.yml also works, and stages it immediately.
- Force-push without erasing a teammate's commit
Alice rebased a shared branch while Bob pushed to it. The script shows both protections refusing her push, then the fix: fetch, look at what she would erase, redo the rebase from the shared tip, and push.
> [!TIP] Rebase a branch only while you are its sole author; on shared branches, merge.
What you will be able to do
- Read any Git command as a move between the working tree, the index and HEAD, and predict the object ids it writes.
- Pick the right undo (revert, reset or restore) and say what it destroys.
- Recover lost commits with the reflog, and know which mistakes are final.
- Explain how a three-way merge decides each region, and why adjacent edits conflict.
- Choose a merge policy, and force-push without erasing a teammate's work.
- Run releases from trunk: backport with cherry-pick -x, tag, and let CI version and publish.
- Keep a monorepo fast with shallow, partial and sparse clones, and handle a leaked secret in the right order.
When to use it
- You made a mistake and need the command that undoes it without making it worse
- Before running a command, to check whether it rewrites history or discards work
- Choosing between merge, squash and rebase for a branch or a repository policy
- Shipping a hotfix to a release branch, or setting up automated versioning
- Finding the commit that introduced a bug, or why a line exists
- A push was rejected, or you need to force-push after a rebase
- CI or clones are slow on a large repository
- A secret was committed
Sign up free to read the full topic
More Quick References topics
Start free on Cariara · Coding problems · System design studio