Originally published at kunalganglani.com — read it there for inline code, hero image, and live links.
You can learn advanced git commands aliases rewrite in an afternoon. The hard part is trusting yourself to use them on a random Tuesday at 4:47pm, five minutes before a deploy, with Slack popping off.
Here’s my stance: if you still treat rebase, reset --hard, and commit --amend like occult rituals, you’re leaving speed on the table. But if you’re rewriting history without safety nets, you’re basically waving around a loaded nail gun.
This post is a “rewrites + recovery” workflow. Every destructive move comes with an escape hatch that actually works. Then I’ll give you an alias kit that bakes the good habits in.
Also, if you copy-paste anything from this post, copy-paste the recovery bits first.
What is a “reflog-first” Git workflow?
A reflog-first Git workflow is the habit of treating Git reflogs as your primary recovery mechanism when you rewrite history, because reflogs record updates to references like HEAD and branch tips and let you get back to previous states after resets, rebases, and checkouts.
That’s not my cute philosophy. It’s straight out of the docs: reflogs “record when the tips of branches and other references were updated in the local repository” (git-reflog documentation). That one sentence is why I’m comfortable being aggressive with history cleanup.
A reflog-first workflow has three rules:
- Before I rewrite anything, I create a restore point. A lightweight tag or backup branch is enough.
- When I panic, I don’t guess. I open the reflog. I find the exact pre-disaster SHA.
-
I force push like an adult.
--force-with-leaseis the default.--forceis the exception.
If you operate this way, Git stops feeling “fragile” and starts behaving like what it is. A local database with an audit trail.
Undo git reset --hard with reflog (step-by-step)
Let’s do the classic stomach-drop moment: you ran git reset --hard, your working tree is “clean,” and the changes are gone.
If those changes lived in commits, they’re probably still there. You didn’t delete history. You moved a pointer and checked out a different state.
Here’s the recipe I actually use.
-
Inspect your recent
HEADpositions
git reflog --date=local -n 25
You’ll see entries like HEAD@{0}, HEAD@{1}, etc. That’s your timeline.
- Identify the entry right before the reset
Look for something like:
reset: moving to HEAD~3- or
checkout: moving from ...
- Restore your branch tip to that SHA
If you’re on the branch you want to restore:
git reset --hard <sha-from-reflog>
If you want to be extra cautious, create a rescue branch first:
git branch rescue/<name> <sha-from-reflog>
git switch rescue/<name>
- If you only need one commit back, cherry-pick it
git cherry-pick <sha>
Why this works is boring, and that’s the point. Reflogs exist specifically to record reference movement locally (git-reflog documentation). reset --hard is a ref update plus a checkout.
Two practical notes:
- This is local recovery. If you rewrote something and pushed it, your reflog doesn’t automatically save your teammates.
- Reflogs expire. On many setups, unreachable entries are kept for 30 days and reachable ones for 90 days by default. Don’t treat reflog as a backup strategy. Treat it like roadside assistance.
If you like the “make safety the default” idea, you’ll probably enjoy my post on safer defaults for code review automation. Different tool, same philosophy.
Undo a git rebase (and recover the pre-rebase branch tip)
Rebase is the rewrite you’ll use the most. It’s also the one people fear the most.
Mostly because they think the recovery is mysterious. It isn’t.
The warning that matters: rebasing rewrites commits by replaying them onto a new base. That changes commit IDs. That’s why rebasing pushed or shared commits is a collaboration hazard. The official reference spells out the behavior and caveats (git-rebase documentation), and Pro Git goes deep on why rewriting public history is dangerous (Scott Chacon and Ben Straub).
Now the practical recovery.
Case A: you’re mid-rebase and want to bail
git rebase --abort
That’s it.
Case B: you finished the rebase, but it was a mistake
-
Find the pre-rebase
HEADin the reflog
git reflog --date=local | head -n 30
Look for entries like:
rebase (start)rebase (finish)
The SHA before rebase (start) is your old branch tip.
- Move your branch back
git reset --hard <pre-rebase-sha>
- If you already force-pushed the rebased branch
You can still repair it, but now you’re in “talk to humans” territory. You’ll likely need to force push the restored tip.
This is exactly why I’m militant about --force-with-lease. It turns “I just deleted someone’s commits” into “push rejected, go coordinate.”
Case C: you rebased and dropped a commit accidentally
If it existed locally, it’s probably in the reflog. Create a branch from it and cherry-pick forward.
git branch rescue/dropped <sha>
That’s the reflog-first mindset in action.
If you’re doing stacked changes a lot, you’ll get even more mileage out of clean rebases. My stacked PRs workflow pairs nicely with the “fixup + autosquash” approach below.
Rewrite Git history safely: restore points, --force-with-lease, and a clean rebase loop
“Don’t rewrite history” is lazy advice. The correct advice is: rewrite history, but make it reversible and socially safe.
Here’s the loop I teach.
1) Create a restore point before you rewrite
Two easy options:
- A backup branch:
git branch backup/<branch>-before-rewrite
- Or a tag:
git tag rewrite-safety/<branch>/$(date +%Y-%m-%d)
This costs you two seconds and saves you twenty minutes of reflog archaeology.
2) Do a safe interactive rebase for cleanup (autosquash, fixup!, reword)
My default cleanup flow before opening a PR:
- Make small commits while working.
- The moment I notice “this should have been part of commit X,” I make a fixup commit right then:
git commit --fixup <sha>
- Before I push for review, I rewrite:
git rebase -i --autosquash origin/main
Inside the interactive list:
-
rewordfor the one commit message that will confuse future-you -
fixup/squashfor obvious cleanup
This is less error-prone than trying to craft perfect commits in real time.
If you have local changes while rebasing, --autostash can keep you moving:
git rebase --autostash origin/main
That flag is documented in the official reference (git-rebase documentation).
3) Force push safely: --force-with-lease vs --force
If you force push rewritten history, you’re overwriting the remote branch.
-
--forcesays: “I don’t care what happened on the remote. Replace it.” -
--force-with-leasesays: “Replace it only if the remote branch still points where I think it does.”
That second behavior is the difference between “I fixed my branch” and “I deleted a teammate’s work.”
I want --force-with-lease to be muscle memory. So I alias it.
One more safety habit that prevents Slack incidents: if a branch is truly shared, don’t rebase it. Merge it. Rebase your own topic branches. Yes, it’s boring. That’s why it works.
If you’re trying to bring the same guardrails-first mindset into AI tooling too, I’ve written a lot about shipping AI in production safely. Different domain, same theme. Defaults matter.
git rerere in practice: stop resolving the same conflict 12 times
If you’ve ever maintained a long-lived branch, you know this pain: you resolve the same conflict, you rebase tomorrow, and Git asks you to resolve the exact same conflict again.
That is a terrible use of a human brain.
rerere fixes it.
The manual defines it plainly: “reuse recorded resolution of conflicted merges” (git-rerere documentation). Git records the conflict hunks and how you resolved them. Next time the same conflict shows up, Git can apply your previous resolution automatically.
Enable rerere (global)
git config --global rerere.enabled true
You need rerere.enabled set for it to work (git-rerere documentation).
What rerere actually does (the mental model)
- On the first conflict, you resolve it manually.
- When you stage the resolution (
git add ...) and continue the merge/rebase, Git stores a “before/after” record. - On future conflicts with the same “before” shape, it can replay the “after.”
In real workflows, this matters most when you:
- rebase a feature branch onto
maindaily for 10+ days - maintain a release branch that regularly cherry-picks fixes
- have generated files that conflict predictably
Inspect, clear, and forget resolutions
You don’t need to poke rerere daily, but you should know how to debug it when it’s not doing what you expect.
- See what it has recorded / what’s pending:
git rerere status
git rerere remaining
- Diff what rerere would apply:
git rerere diff
- Blow away rerere’s metadata (rare, but sometimes you want a clean slate):
git rerere clear
- Forget a resolution for a specific path:
git rerere forget path/to/file
These commands are all in the manual (git-rerere documentation).
If you’ve never tried rerere, enable it for a week. The first time it auto-resolves a nasty conflict, you’ll wonder why this isn’t on by default.
Git worktrees: the fastest way to juggle hotfix + feature + PR review
Most devs solve “I need two branches at once” with stashing.
It works. It also makes your working directory feel like a junk drawer, and it’s way too easy to stash the wrong thing, apply the wrong stash, or forget what’s inside.
git worktree is the grown-up move. It lets you have multiple working directories attached to the same repo, sharing the .git object database. The docs describe it as managing “multiple working trees” (git-worktree documentation).
My three worktree workflows (the ones I actually use)
1) Hotfix while your feature branch is mid-rebase
- Worktree A: your feature branch with conflicts half-resolved
- Worktree B: clean
mainfor the hotfix
Commands:
# from the main repo
mkdir -p ../wt
git worktree add ../wt/hotfix -b hotfix/issue-123 origin/main
cd ../wt/hotfix
Now you can patch, test, and ship without touching your half-broken rebase.
2) Review a PR locally while continuing your own work
If your teammate’s PR needs a local run, don’t torch your current working directory.
# fetch the branch first if needed
git fetch origin feature/something
git worktree add ../wt/review-feature origin/feature/something
3) Run two versions side-by-side
Criminally underrated for debugging.
- Worktree A:
main - Worktree B: your branch
Run tests, benchmarks, or repro steps in both directories without switching. If you do performance work, this is the only sane way to compare.
Worktree hygiene
A couple of habits keep worktrees from turning into a pile of abandoned folders:
- List them:
git worktree list
- Remove when done:
git worktree remove ../wt/review-feature
I treat worktrees like disposable environments. Same mindset as dev containers. If you want that style of setup consistency, my self-hosted DevContainers guide is basically “worktrees for your entire toolchain.”
Here’s a short video that explains the concept visually if you want a 5-minute primer:
[YOUTUBE:8vsRb2mTBA8|learn git worktrees in under 5 minutes]
Clone a large repo faster with partial clone (and know the tradeoffs)
If you’re working in a monorepo, “clone takes forever” is not a fake problem. It shows up in CI agents, on fresh laptops, and in ephemeral dev environments where you rebuild from scratch.
Git has a built-in answer: partial clone.
The git clone docs describe --filter=<filter-spec> for omitting objects initially and fetching them on demand (see git-clone documentation). The practical filter most teams start with is blob:none.
The command I reach for
git clone --filter=blob:none <repo-url>
This says: “clone commits and trees, but don’t download file blobs until needed.”
Two scenarios where this matters:
- CI runners: you might only need a subset of files to build/test. Why download every blob?
- Dev containers: you recreate environments frequently. Saving even 30–60 seconds per clone compounds quickly.
Tradeoffs and limitations
Partial clone is not magic.
- The first time you
checkoutpaths you haven’t fetched blobs for, Git will fetch them. You’re moving time from “clone” to “first use.” - Some tooling that assumes a fully-populated working copy can behave strangely.
- If you combine this with sparse checkout, you can get even more aggressive. Separate rabbit hole.
I like partial clone because it’s a workflow improvement that doesn’t require team coordination. You can do it solo today.
My hardened Git alias kit (safe by default)
Most alias guides give you cute shortcuts. I don’t care about cute. I want aliases that change behavior.
Git aliases are a first-class feature in git config, including shell-command aliases that start with ! (git-config documentation).
Below is an opinionated kit designed around:
- Recovery-first: “undo” points you at reflog.
-
Safe force push:
--force-with-leaseis the default. - Readable history: logs tuned for code review.
Paste this into ~/.gitconfig or run the git config --global equivalents.
Alias table (what to install)
| Alias | Expands to | Why it exists |
|---|---|---|
lg |
log --graph --decorate --oneline --date=relative |
Fast mental model of what happened |
lga |
log --graph --decorate --oneline --all |
When you’re lost across branches |
st |
status -sb |
Compact status (-sb matters) |
fixup |
commit --fixup |
Makes autosquash a habit |
ri |
rebase -i --autosquash |
The cleanup flow I actually use |
fp |
push --force-with-lease |
Safe force push by default |
undo |
reflog --date=local -n 30 |
“Don’t guess. Open the reflog.” |
wt |
worktree |
Makes worktrees feel normal |
The actual config block
[alias]
st = status -sb
lg = log --graph --decorate --oneline --date=relative
lga = log --graph --decorate --oneline --all
# Rewrite workflow
fixup = commit --fixup
ri = rebase -i --autosquash
# Safe pushing
fp = push --force-with-lease
# Recovery
undo = reflog --date=local -n 30
# Worktrees
wt = worktree
Which aliases are too dangerous to install
Here are the ones I refuse to normalize on teams:
-
pushf = push --force(it turns a rare emergency tool into a reflex) -
nuke = reset --hard(it makes it way too easy to delete state casually)
If you really want a “nuke,” make it loud and interactive with a shell alias that prints the target branch and requires confirmation. But honestly, I’d rather you build the habit of backup branches + reflog.
If you’re already investing in developer ergonomics, you might also like my collection of tools on this site. Building the LLM pricing tracker and shipping 25+ tools taught me the same lesson over and over. Small, sharp defaults compound faster than grand process changes.
Team adoption tip: aliases should be optional, log formats should be shared
I don’t force my personal alias kit on teams. People have their own shell setups.
What I do standardize:
- a shared
git logformat for reviews - a documented “how we rewrite history” policy (
--force-with-lease, backup branch naming)
Same principle as my gitleaks + pre-commit + CI setup. Tooling works when the defaults are shared.
Putting it together: my daily workflow (worktrees + rerere + safe rewrite)
Once you combine all of this, Git stops being a bag of party tricks. It becomes a system you can trust.
Here’s the loop:
- Start work on a feature branch.
- Enable rerere once. Conflicts get cheaper over time.
- Use worktrees when you need parallelism. No stashing. No thrash.
- Before opening a PR, clean up with interactive rebase + autosquash.
- Force push with lease.
- If anything goes sideways, reflog first.
A concrete example from my day-to-day building this site: I maintain a bunch of small utilities and datasets (the LLM pricing tracker and 25+ tools under /tools). I’m constantly bouncing between “ship a tiny fix” and “keep a bigger refactor moving.” Worktrees keep me from context switching myself to death, and reflog is what lets me clean up history aggressively without getting punished.
If you take one thing from this post, take this: rewrite history more, not less. Just do it the way you’d do database migrations. Restore points. An audit trail. A rollback plan.
The engineers who move fastest in 2026 aren’t the ones who memorized the most Git commands. They’re the ones who made the dangerous ones boring.
Originally published on kunalganglani.com
Top comments (0)