Everything Git Can Do (Beyond add/commit/push)¶
Git is not really a "save my code" tool — it's a content-addressable object database with a very good CLI bolted on top. Once that clicks, most of the "advanced" features stop looking like tricks and start looking obvious. This guide goes from the object model up through the commands that actually get you out of trouble.
1. The object model (why everything else works)¶
Everything in a git repo is one of four object types, addressed by the SHA of its content:
- blob — file contents (no filename, no permissions)
- tree — a directory listing (names + modes + blob/tree SHAs)
- commit — a tree SHA + parent SHA(s) + metadata
- tag — a named pointer, optionally signed
A branch is just a 40-character file containing a commit SHA. HEAD is
just a file containing a branch name (or a SHA, in detached-HEAD state).
That's it. There's no magic underneath.
git cat-file -t <sha> # what kind of object is this?
git cat-file -p <sha> # print its raw content
git hash-object -w file # store a file as a blob, print its SHA
git rev-parse HEAD # resolve any ref to its SHA
The killer feature this unlocks: revision:path¶
Any path can be addressed at any point in history, independent of your working directory or index:
git show main~3:src/config.rs # that file, 3 commits back on main
git show abc1234:README.md > README.md # pull one historical file into your tree
git cat-file -e abc1234:path/to/file # does this path exist at that commit? (exit code only)
This is how you surgically restore individual files without touching anything else — no merge, no checkout, no conflicts. If you've ever had a bad commit corrupt a subset of files across a messy history, this is the tool: for every broken file, pull its content from the last known-good commit and overwrite it directly.
BASE=dd48453
git grep -l 'BROKEN_STRING' -- content/ | while read -r f; do
if git cat-file -e "$BASE:$f" 2>/dev/null; then
git show "$BASE:$f" > "$f"
echo "restored: $f"
else
echo "no clean version at $BASE: $f"
fi
done
No revert, no conflict markers, no risk of half-merging unrelated
restructuring that happened in between. This is usually a better answer
than git revert once a rename or restructure has happened downstream of
the bad commit — reverting replays a diff, and diffs stop applying
cleanly the moment the tree shape has changed.
2. Searching history, not just files¶
grep on your working directory only sees the current snapshot. Git can
search history itself:
# Find commits where a string was ADDED or REMOVED (the "pickaxe")
git log -S"someFunction" --oneline
# Same, but treats the argument as a regex
git log -G"foo.*bar" --oneline
# Search file CONTENTS at the current tree, not commit messages
git grep -n "TODO"
# Search file contents at a specific commit
git grep -n "TODO" abc1234
# Which commit last touched this line? (not just this file)
git blame -L 40,60 -- file.rs
# Follow a file across renames
git log --follow --oneline -- new/path/file.rs
# Find which commit introduced a bug (binary search over history)
git bisect start
git bisect bad # current commit is broken
git bisect good v1.2.0 # this old tag was fine
# git checks out the midpoint; you test and run:
git bisect good # or: git bisect bad
# repeat until git names the exact culprit commit
git bisect reset
-S vs -G trips people up: -S"foo" finds commits that changed the
number of occurrences of the literal string foo. -G"foo" finds
commits where a line matching the regex foo was added or removed,
regardless of count. -S is what you want 90% of the time ("when did this
exact string appear/disappear").
git bisect can also be automated with a script that exits non-zero on
failure:
git bisect start HEAD v1.2.0
git bisect run ./run-tests.sh
3. Recovery: reflog and dangling objects¶
The reflog is a local, per-repo log of everywhere HEAD and your branches
have pointed, kept for ~90 days by default. It survives resets, rebases,
and branch deletions — this is git's actual undo history.
git reflog # everywhere HEAD has been
git reflog show my-branch # everywhere a specific branch has been
git reset --hard HEAD@{2} # go back to where HEAD was 2 moves ago
git branch recovered abc1234 # resurrect a "lost" commit as a new branch
Even a git branch -D of an unmerged branch doesn't destroy the commits —
they become "dangling" (unreachable from any ref) but stay in the object
store until garbage collection runs:
git fsck --lost-found # find genuinely unreachable commits/blobs
Practical rule: nothing is really gone until git gc prunes it, and gc
respects the reflog's retention window. Panic less.
4. Rewriting history (locally, deliberately)¶
git commit --amend # fix the last commit's message/content
git rebase -i HEAD~5 # squash, reorder, reword, drop, edit
git rebase -i --autosquash HEAD~5 # auto-place fixup!/squash! commits
git commit --fixup <sha> # mark a commit as "fixes <sha>" for autosquash
git cherry-pick <sha> # replay one commit onto current branch
git cherry-pick <shaA>..<shaB> # replay a range
git rebase --onto newbase oldbase branch # move a whole branch to a new parent
--fixup + rebase -i --autosquash is underused: instead of hand-editing
the rebase todo list to reorder squashes, you commit a fixup, then run
autosquash and git places and marks it for you.
Range-diff compares two versions of a rebased/amended branch — "did my force-push actually change the content, or just the SHAs?":
git range-diff main~5..main main~5..main@{1}
Never rewrite pushed shared history without coordination — that's what
turns a revert --abort afternoon into everyone's problem.
5. Stash is a full mini-stack¶
git stash push -m "wip: auth refactor"
git stash push -- path/to/file.rs # stash only specific paths
git stash push --keep-index # stash unstaged changes only
git stash --include-untracked # also stash untracked files
git stash list # it's a stack, not a single slot
git stash show -p stash@{2} # inspect an old stash entry
git stash apply stash@{2} # apply without dropping
git stash branch new-branch stash@{1} # apply onto a fresh branch (great for conflicts)
6. Multiple working trees, one repo¶
git worktree lets you check out several branches into separate
directories simultaneously, all sharing one .git object store — no
cloning, no stashing to switch context:
git worktree add ../hotfix hotfix-branch
git worktree add ../review-pr-123 origin/pr-123
git worktree list
git worktree remove ../hotfix
Genuinely useful when a customer bug needs a fix while you're mid-refactor on main and don't want to stash/switch/unstash.
7. Partial and shallow clones (big-repo hygiene)¶
git clone --depth 1 <url> # shallow: history-free, fast
git clone --filter=blob:none <url> # full history, blobs fetched on demand
git clone --filter=tree:0 <url> # even leaner, trees on demand too
git sparse-checkout init --cone
git sparse-checkout set services/api docs # only materialize these dirs
--filter=blob:none + sparse-checkout is the real answer to "this
monorepo is huge and I only work in one subdirectory."
8. Attributes and hooks: repo-level automation¶
.gitattributes controls per-path behavior git applies automatically:
*.png binary
*.sh text eol=lf
*.rs diff=rust # custom diff driver
secrets.yaml filter=git-crypt diff=git-crypt
Hooks (.git/hooks/) run scripts at lifecycle points — pre-commit,
commit-msg, pre-push, post-checkout, etc. Not version-controlled by
default (they live outside the tracked tree), which is why tools like
pre-commit or husky exist to distribute and version hook config
alongside the repo. You can also point git at a tracked hooks directory:
git config core.hooksPath .githooks
9. Signing and provenance¶
git commit -S -m "message" # GPG-sign a commit
git tag -s v1.0.0 -m "release" # sign a tag
git config commit.gpgsign true # sign everything automatically
git verify-commit <sha>
git verify-tag v1.0.0
SSH-based signing is also supported now (gpg.format = ssh), so you can
sign commits with the same key you already use for pushing.
10. Patches without a shared remote¶
Git predates the "everyone pushes to the same server" workflow, and it still works fine without one:
git format-patch -3 # turn last 3 commits into .patch files
git format-patch origin/main # patches for everything ahead of origin/main
git am *.patch # apply patches as real commits (preserves authorship)
git bundle create repo.bundle main # a whole repo+history in ONE file
git bundle verify repo.bundle
git clone repo.bundle mycopy # clone from the bundle like any remote
bundle is the underrated one — it's a full offline transport mechanism.
Sneakernet a .bundle file over USB/email/Slack and the receiving end gets
real commit history, not a zip of files.
11. Notes: metadata without touching commits¶
git notes attaches extra data to a commit without changing its SHA —
useful for CI results, code review status, or build metadata tacked on
after the fact:
git notes add -m "CI: passed on 2026-08-30" <sha>
git log --show-notes
git notes show <sha>
12. Diagnosing conflicts and merges¶
git merge --no-commit --no-ff branch # merge but stop before committing, to inspect
git rerere # "reuse recorded resolution" — auto-resolves
# a conflict you've already fixed once before
git config rerere.enabled true
git mergetool # launch configured 3-way merge UI
git diff --diff-filter=U # list only currently-unmerged files
git log --merge # commits relevant to the current conflict
rerere is worth turning on globally — if you rebase a long-lived branch
repeatedly and keep hitting the same conflict each time, git remembers
your resolution and reapplies it automatically after the first time.
For the "revert conflicts with modify/delete because history moved on"
situation specifically: prefer the revision:path restore pattern from
section 1 over fighting git revert through a multi-commit conflict chain
— it sidesteps merge machinery entirely by treating each file as an
independent historical lookup rather than replaying a diff.
13. Maintenance and introspection¶
git gc # compress/prune the object database
git maintenance start # schedule background gc/prefetch tasks
git count-objects -v # size of your object store
git fsck # integrity check
git log --all --graph --oneline --decorate # the classic "what actually happened" view
git shortlog -sn # commit counts per author
git log --pretty=format:"%h %an %ar %s" # fully custom log formatting
14. Aliases and config tricks worth stealing¶
git config --global alias.lg "log --graph --oneline --decorate --all"
git config --global alias.undo "reset --soft HEAD~1"
git config --global alias.amend "commit --amend --no-edit"
git config --global push.autoSetupRemote true # no more -u on first push
git config --global rebase.autoStash true # auto-stash/pop around rebase
git config --global diff.colorMoved zebra # highlight moved (not just changed) lines
diff.colorMoved is a good one — normal diff shows a moved block as a
delete + an add; this makes git recognize and color it as a move instead.
15. Diffs can lie to you — read them literally¶
git diff shows exactly what it's asked to show, nothing more. Two
common surprises:
A file with every line marked changed, but the text looks identical. This is almost always a line-ending or whitespace conversion (CRLF↔LF, or trailing-whitespace stripping), not a real content change — git diffs line-by-line, so a wholesale EOL conversion looks like "delete everything, add everything back."
git diff --ignore-space-at-eol -- file.md # confirm it's just EOL noise
cat -A file.md | head -3 # ^M at line ends = CRLF present
git diff <file> returns nothing, even though the file is "wrong."
A bare git diff <path> only compares the working tree against the index
(i.e., uncommitted changes). If the questionable content was already
committed, there's nothing uncommitted to show — the content simply is
HEAD now. Check the actual committed content directly instead of diffing:
git grep -n 'suspect-string' -- path/to/file.md
git show HEAD:path/to/file.md | less
16. A single cutoff commit doesn't work for files added later¶
A bulk restore pattern like "pull every file's content from commit BASE"
(section 1) assumes every file already existed at BASE. Files created
after that commit won't be found there at all — checking existence with
cat-file -e "$BASE:$f" will report the file "doesn't exist" at that
revision, which just means it hadn't been added yet, not that no good
version exists.
The fix is to stop using one global cutoff and instead walk each file's own history to find the last commit before a bad string appears in that specific file:
for c in $(git log --follow --format=%H --reverse -- "$f"); do
if git show "$c:$f" 2>/dev/null | grep -q 'BAD_STRING'; then
break # this commit already has the bad content — stop
fi
good="$c" # otherwise, this commit is still clean — keep looking
done
Walking oldest-to-newest and stopping at the first "bad" commit correctly
finds the last good state per file, regardless of when that file was
introduced — no shared baseline commit required. --follow keeps this
working even across renames.
17. Sometimes there's no history to restore from — and git can tell you that¶
If a file's first ever commit already contains the bad content, there is no earlier clean version — the file was created that way, not corrupted later. That's a real, distinct outcome from "restore failed," and it's worth confirming explicitly rather than assuming the automation is broken:
# find the very first commit that added this file
git log --follow --diff-filter=A --format=%H -- path/to/file.md
# check whether the questionable content was present from the start
git show <that-first-sha>:path/to/file.md | grep -n 'BAD_STRING'
If the string is there in commit #1, the content was likely templated or generated with a placeholder that was never filled in — a content problem, not a version-control problem. At that point it's worth checking a couple of other places git might still have the real value before writing it in by hand:
git stash list # anything parked in a stash?
git log --all --oneline -- path/to/file.md # any branch/tag git knows about, not just current branch
git branch -a --contains <some-earlier-sha> # which branches descend from a known-good point?
--all is the key flag here — a normal git log only walks the current
branch's ancestry; --all searches every ref in the repo, which catches
content that landed on a feature branch that was never merged, or existed
before a squash collapsed it out of the main line.
The shape of it¶
Almost every "wow" git command above reduces to the same idea from
section 1: git addresses content by hash, not by working-directory
state, and most subcommands are just different lenses onto that same
object graph — history search (log -S, bisect), point-in-time file
access (show rev:path), alternate materializations of the graph
(worktree, sparse-checkout), or safety nets on top of it (reflog,
rerere, notes). Once you're reaching for "how do I get this exact
content out of the object store," instead of "what git command does the
thing I want," most of this becomes discoverable rather than memorized.