DEV Community

Kunal
Kunal

Posted on Originally published at kunalganglani.com

How to Use Advanced Git Commands Safely [2026 Alias Kit]

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:

  1. Before I rewrite anything, I create a restore point. A lightweight tag or backup branch is enough.
  2. When I panic, I don’t guess. I open the reflog. I find the exact pre-disaster SHA.
  3. I force push like an adult. --force-with-lease is the default. --force is 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.

  1. Inspect your recent HEAD positions
   git reflog --date=local -n 25
Enter fullscreen mode Exit fullscreen mode

You’ll see entries like HEAD@{0}, HEAD@{1}, etc. That’s your timeline.

  1. Identify the entry right before the reset

Look for something like:

  • reset: moving to HEAD~3
  • or checkout: moving from ...
  1. Restore your branch tip to that SHA

If you’re on the branch you want to restore:

   git reset --hard <sha-from-reflog>
Enter fullscreen mode Exit fullscreen mode

If you want to be extra cautious, create a rescue branch first:

   git branch rescue/<name> <sha-from-reflog>
   git switch rescue/<name>
Enter fullscreen mode Exit fullscreen mode
  1. If you only need one commit back, cherry-pick it
   git cherry-pick <sha>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That’s it.

Case B: you finished the rebase, but it was a mistake

  1. Find the pre-rebase HEAD in the reflog
   git reflog --date=local | head -n 30
Enter fullscreen mode Exit fullscreen mode

Look for entries like:

  • rebase (start)
  • rebase (finish)

The SHA before rebase (start) is your old branch tip.

  1. Move your branch back
   git reset --hard <pre-rebase-sha>
Enter fullscreen mode Exit fullscreen mode
  1. 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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • Or a tag:
  git tag rewrite-safety/<branch>/$(date +%Y-%m-%d)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Make small commits while working.
  2. The moment I notice “this should have been part of commit X,” I make a fixup commit right then:
   git commit --fixup <sha>
Enter fullscreen mode Exit fullscreen mode
  1. Before I push for review, I rewrite:
   git rebase -i --autosquash origin/main
Enter fullscreen mode Exit fullscreen mode

Inside the interactive list:

  • reword for the one commit message that will confuse future-you
  • fixup / squash for 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
Enter fullscreen mode Exit fullscreen mode

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.

  • --force says: “I don’t care what happened on the remote. Replace it.”
  • --force-with-lease says: “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
Enter fullscreen mode Exit fullscreen mode

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 main daily 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
Enter fullscreen mode Exit fullscreen mode
  • Diff what rerere would apply:
  git rerere diff
Enter fullscreen mode Exit fullscreen mode
  • Blow away rerere’s metadata (rare, but sometimes you want a clean slate):
  git rerere clear
Enter fullscreen mode Exit fullscreen mode
  • Forget a resolution for a specific path:
  git rerere forget path/to/file
Enter fullscreen mode Exit fullscreen mode

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 main for the hotfix

Commands:

# from the main repo
mkdir -p ../wt

git worktree add ../wt/hotfix -b hotfix/issue-123 origin/main
cd ../wt/hotfix
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • Remove when done:
  git worktree remove ../wt/review-feature
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 checkout paths 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-lease is 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
Enter fullscreen mode Exit fullscreen mode

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 log format 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:

  1. Start work on a feature branch.
  2. Enable rerere once. Conflicts get cheaper over time.
  3. Use worktrees when you need parallelism. No stashing. No thrash.
  4. Before opening a PR, clean up with interactive rebase + autosquash.
  5. Force push with lease.
  6. 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)