I’ve been using Git for over a decade, and I still learn new tricks. The difference between a developer who knows push/pull/commit and one who masters Git’s advanced features is enormous — especially when things go wrong (and they always do).

This isn’t a beginner’s guide. If you know how to commit, branch, and merge, you’re ready for the commands that will save your bacon when production is on fire and someone force-pushed to main.

Interactive Rebase: Rewriting History Like a Pro

Interactive rebase (git rebase -i) is the most powerful Git command you’ll ever learn. I use it daily to clean up my commit history before opening PRs.

# Rebase the last 5 commits
git rebase -i HEAD~5

# Rebase everything since branching from main
git rebase -i main

This opens your editor with something like:

pick a1b2c3d Add user model
pick e4f5g6h Fix typo in user model
pick i7j8k9l Add user service
pick m0n1o2p WIP: debugging
pick q3r4s5t Add user controller

The Commands I Actually Use

Command What It Does When I Use It
pick Keep commit as-is Most commits
squash Merge into previous commit Combining related changes
fixup Like squash but discard message Typo fixes, WIP commits
reword Change commit message only Clarifying intent
edit Pause to amend commit Split a commit, add files
drop Delete commit entirely Removing debug/WIP commits

My Workflow

# Before opening a PR, I always clean up:
git rebase -i main

# Turn this mess:
# "Add feature" → "fix typo" → "WIP" → "actually fix it" → "cleanup"
# Into this:
# "feat: Add user authentication with JWT"

Pro Tip: Use fixup commits during development. Name them fixup! <original commit message> and then run git rebase -i --autosquash main. Git automatically arranges the fixup commits under their targets. This is the cleanest workflow I’ve found.

# During development:
git commit -m "feat: Add login endpoint"
# ... later find a bug in that commit ...
git commit -m "fixup! feat: Add login endpoint"

# Before PR:
git rebase -i --autosquash main
# The fixup is automatically placed after its target

When something broke and you don’t know which commit caused it, git bisect is magic. It uses binary search to find the offending commit in O(log n) time.

# Start bisecting
git bisect start

# Mark current state as bad
git bisect bad

# Mark a known good commit (e.g., last release)
git bisect good v2.1.0

# Git checks out a middle commit. Test it, then:
git bisect good  # if this commit works fine
# or
git bisect bad   # if the bug exists here

# Repeat until Git identifies the exact commit
# "abc123 is the first bad commit"

# When done:
git bisect reset

Automated Bisect

The real power move is automating it with a test script:

# Automatically bisect using a test command
git bisect start HEAD v2.1.0
git bisect run npm test

# Or with a custom script:
git bisect run ./scripts/check-bug.sh
#!/bin/bash
# check-bug.sh - exits 0 if good, non-zero if bad
npm run build 2>/dev/null && curl -s localhost:3000/health | grep -q "ok"

I once used this to find a performance regression across 400 commits in about 3 minutes. Without bisect, that would have been hours of manual testing.

Git Worktrees: Multiple Branches Simultaneously

Worktrees let you check out multiple branches at once in separate directories. No more stashing and switching when you need to hotfix production while working on a feature.

# Create a worktree for a hotfix
git worktree add ../myapp-hotfix hotfix/critical-bug

# Now you have:
# ~/projects/myapp/          (your feature branch)
# ~/projects/myapp-hotfix/   (hotfix branch)

# Work on both simultaneously!
# When done with the hotfix:
git worktree remove ../myapp-hotfix

My Worktree Setup

# I keep a bare clone as the "main" repo
git clone --bare [email protected]:team/project.git project.git
cd project.git

# Create worktrees for each context
git worktree add ../project-main main
git worktree add ../project-feature feature/new-auth
git worktree add ../project-review pr/456

# List active worktrees
git worktree list

Pro Tip: If you’re doing code reviews while working on features, worktrees are a game-changer. Check out the PR branch in a separate worktree, review it in your IDE, and never disrupt your feature branch’s state.

Git Reflog: Your Safety Net

The reflog records every HEAD movement. Even if you “lose” commits through a bad rebase or reset, they’re still in the reflog for 90 days.

# View the reflog
git reflog

# Output:
# abc123 HEAD@{0}: rebase (finish): returning to refs/heads/main
# def456 HEAD@{1}: rebase (squash): feat: add authentication
# ghi789 HEAD@{2}: commit: WIP auth logic
# jkl012 HEAD@{3}: reset: moving to HEAD~3

# Recover a "lost" commit
git checkout HEAD@{3}
# or
git branch recovered-branch HEAD@{3}

Recovery Scenarios

Scenario: Accidentally ran git reset --hard

# Find where you were before the reset
git reflog
# Look for the commit before the reset entry
git reset --hard HEAD@{1}  # Go back to pre-reset state

Scenario: Bad rebase destroyed your branch

# Find the pre-rebase state
git reflog | grep "checkout: moving"
# Restore the branch
git branch my-branch-backup HEAD@{5}

Scenario: Deleted a branch with unmerged work

# Find the deleted branch's last commit
git reflog | grep "my-deleted-branch"
# Recreate it
git branch my-deleted-branch abc123

Cherry-Pick: Surgical Commit Transplants

Cherry-pick applies specific commits from one branch to another without merging everything:

# Apply a single commit
git cherry-pick abc123

# Apply multiple commits
git cherry-pick abc123 def456 ghi789

# Apply a range
git cherry-pick abc123..ghi789

# Cherry-pick without committing (stage changes only)
git cherry-pick --no-commit abc123

When I Use Cherry-Pick

  1. Hotfixes: Apply a bug fix from develop to release without merging everything
  2. Backporting: Apply a fix to an older release branch
  3. Extracting work: Pull specific commits from a messy branch into a clean one
# Backport a security fix to the v2 maintenance branch
git checkout release/v2
git cherry-pick -x abc123  # -x adds "cherry picked from" to the message

Pro Tip: Always use -x flag when cherry-picking between branches. It adds a reference to the original commit, making it easy to trace where the change came from during code archaeology.

Git Stash: Beyond the Basics

Everyone knows git stash, but most developers don’t use its full power:

# Stash with a descriptive message
git stash push -m "WIP: half-finished auth refactor"

# Stash specific files only
git stash push -m "just the config changes" -- config/ .env

# Stash including untracked files
git stash push -u -m "including new files"

# List stashes
git stash list
# stash@{0}: WIP: half-finished auth refactor
# stash@{1}: just the config changes

# Apply without removing from stash
git stash apply stash@{1}

# Create a branch from a stash
git stash branch feature/auth-refactor stash@{0}

Interactive Stashing

# Stash only some changes (interactive)
git stash push -p -m "partial stash"
# Shows each hunk and asks y/n — like interactive staging

Finding Who Changed What

# Search commit messages
git log --grep="payment" --oneline

# Find when a function was added/removed
git log -S "processPayment" --oneline

# Find when a regex pattern changed
git log -G "api/v[0-9]+" --oneline

# Blame with ignore whitespace and moved lines
git blame -w -M -C src/utils.ts

Pretty Log Formats

# My favorite log alias
git log --graph --oneline --all --decorate

# Add to ~/.gitconfig:
[alias]
  lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
  recent = for-each-ref --sort=-committerdate --format='%(refname:short) %(committerdate:relative)' refs/heads/ --count=10

Pro Tip: Add git recent alias to quickly see your recently worked-on branches. After a weekend, I always forget which branch I was on.

Git Hooks: Automating Quality

Here’s my standard pre-commit hook setup using Husky + lint-staged:

// package.json
{
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md}": ["prettier --write"]
  }
}
# .husky/pre-commit
#!/bin/sh
npx lint-staged
# .husky/commit-msg - enforce conventional commits
#!/bin/sh
npx commitlint --edit $1

For teams using GitHub Actions, hooks provide a first line of defense before CI even runs.

Useful Aliases

Here’s my complete .gitconfig aliases section:

[alias]
  # Shortcuts
  co = checkout
  br = branch
  ci = commit
  st = status -sb
  
  # Undo last commit (keep changes staged)
  undo = reset --soft HEAD~1
  
  # Amend without editing message
  amend = commit --amend --no-edit
  
  # Show what I did today
  today = log --since='6am' --oneline --author='Michael'
  
  # Delete all merged branches
  cleanup = "!git branch --merged | grep -v '\\*\\|main\\|develop' | xargs -n 1 git branch -d"
  
  # Force pull (reset to remote)
  forcepull = "!git fetch origin && git reset --hard origin/$(git branch --show-current)"
  
  # Create a WIP commit
  wip = "!git add -A && git commit -m 'WIP: [skip ci]'"
  
  # Unwip (undo WIP commit)
  unwip = "!git log -1 --format='%s' | grep -q 'WIP' && git reset HEAD~1"

Common Mistakes with Advanced Git

1. Rewriting Shared History

Never rebase or amend commits that have been pushed and are used by others. Use --force-with-lease instead of --force if you must push rewritten history:

# ❌ Dangerous: Overwrites remote regardless of others' work
git push --force

# ✅ Safer: Fails if remote has commits you haven't seen
git push --force-with-lease

2. Not Understanding Merge vs Rebase

Strategy When to Use Result
Merge Public/shared branches Preserves history, adds merge commit
Rebase Private/feature branches Linear history, cleaner log
Squash merge Feature → main Single commit per feature

3. Large Files in Git

Git isn’t designed for large binary files. Use Git LFS for anything over 1MB:

git lfs install
git lfs track "*.psd" "*.ai" "*.zip"
git add .gitattributes

4. Ignoring the Staging Area

The staging area (index) is a powerful feature, not an annoyance:

# Stage specific hunks interactively
git add -p

# Stage only part of a file's changes
# This lets you create focused, atomic commits

5. Not Using Git Maintenance

# Enable background maintenance (Git 2.30+)
git maintenance start

# This runs gc, prefetch, and commit-graph updates automatically
# Keeps large repos fast without manual intervention

Recovery Cheat Sheet

Disaster Recovery Command
Undo last commit (keep changes) git reset --soft HEAD~1
Undo last commit (discard changes) git reset --hard HEAD~1
Recover deleted branch git refloggit branch name HEAD@{n}
Fix last commit message git commit --amend
Remove file from last commit git reset HEAD~1 -- filegit commit --amend
Undo a merge git revert -m 1 <merge-commit>
Abort a bad rebase git rebase --abort
Recover from bad reset git refloggit reset HEAD@{n}
Remove sensitive data from history git filter-repo --path-glob '*.env' --invert-paths

If you’re working with VS Code, the GitLens extension visualizes much of this information inline, making recovery scenarios less scary.

FAQ

When should I rebase vs merge?

Rebase your feature branches onto main to keep a linear history. Use merge for integrating completed features into shared branches. My team’s rule: rebase for updating your branch, merge (or squash merge) for completing features. Never rebase branches that others are working on.

How do I undo a push to the wrong branch?

If caught quickly: git push origin +correct-branch and git push origin --delete wrong-branch. If others have already pulled, communicate with your team and use git revert instead of force-pushing. The revert creates a new commit that undoes the changes without rewriting history.

What’s the best Git branching strategy in 2025?

For most teams, trunk-based development with short-lived feature branches works best. Main is always deployable, features branch off main, and PRs are merged within 1-2 days. GitHub Flow is essentially this. GitFlow is overkill for most projects — only consider it if you maintain multiple production versions simultaneously.

How do I handle merge conflicts in a rebase?

Resolve conflicts file by file, then git add the resolved files and git rebase --continue. If it gets too messy, git rebase --abort starts fresh. Pro tip: enable rerere (git config rerere.enabled true) to have Git remember how you resolved conflicts and auto-apply the same resolution next time.

Should I sign my commits with GPG?

Yes, if your organization requires it or if you contribute to open source. GitHub shows a “Verified” badge on signed commits. Set it up once with git config commit.gpgsign true and forget about it. For personal projects, it’s nice-to-have but not critical.