Skip to main content

Advanced Topics

Git Rebase - The Superpower

There is a popular debate by developers over which is better: git merge or git rebase. Personally, I think rebase can be a superpower!

Key Idea

Rebase creates a linearized commit history, making it much cleaner than merge commits with multiple parents.

Rebase takes commits from one branch and copies them on top of another branch, creating a linear history instead of the "entangled" history that merges create.

Basic Rebase

# Rebase current branch onto main
git rebase main

# Rebase specific branch onto upstream
git rebase <upstream> <branch>

Cherry-pick vs Interactive Rebase

Cherry-pick when you know exact commits:

git cherry-pick <commit1> <commit2> <commit3>

Interactive rebase for more control:

git rebase -i
Interactive rebase options

When you run git rebase -i, you'll see:

pick 1a2b3c Commit message A
pick 4d5e6f Commit message B
pick 7g8h9i Commit message C

Available actions:

  • pick: Keep the commit
  • drop: Remove the commit
  • edit: Pause to make changes
  • reword: Change commit message
  • squash: Combine with previous commit

If conflicts occur:

  1. Resolve conflicts manually
  2. git add resolved files
  3. git rebase --continue
  4. Or git rebase --abort to cancel
Do Not Forget

Never rebase commits that have been pushed and others are working from! Only rebase local changes before pushing.

When you rebase pushed commits that others have built upon, you create a nightmare scenario:

  1. Your rebase changes commit SHAs, making the "same" commits appear as different commits
  2. Collaborators who built on your original commits now have orphaned work
  3. When they try to merge/push, Git sees duplicate commits with different SHAs
  4. This creates merge conflicts, duplicate commits, and a tangled history that's nearly impossible to untangle
  5. The team ends up with a "commit soup" where the same changes exist multiple times with different hashes

The golden rule: rebase only unpushed commits.

Trick

Use git reflog and git reset to undo a rebase if needed.

RERERE (REuse REcorded REsolutions)

Often times people dislike git rebase because they find themselves constantly having to resolve the same conflicts over and over. This can happen if you have rebased a branch A->B->C onto D of main but then later rebase it onto a later commit E of main instead. The same conflicts you resolved the first rebase will happen again as your branch commits gets replayed through D! But one of the neat lesser known features that make this go away (and makes rebase great again) is RERERE.

RERERE when enabled saves how you resolved a previous confict in memory, so that if you're rebasing and hitting the same merge conflicts, Git will automatically fix that for you. Just run:

git config --global rerere.enabled true

Rebasing Stacked Branches

If you are rebasing stacked branches, the workflow can be quite tedious as it will not automatically update the refs of any other dependent branches. Say you have consecutive branches A -> B (branch1)-> C (branch2) --> D (branch3) and you rebase branch1 onto dev. Then the refs for branch2 and branch3 downstream will be outdated as they will still be pointing to the commits leading from old commits of branch1! You would have to sequentially checkout branch2 and rebase it onto branch1 and do the same for branch3, or rebase from branch3 at the very beginning and update branch1 and branch2 refs individually to the new commits. It doesn't have to be branches sitting sequentially, just as long as they have dependency on the branch being rebased.

Often times this happens when developers go back to fix a certain commit in a PR, and you have branches downstream of that PR you need to rebase on top of the fixed commit.

The solution: to make this workflow a lot more streamline, there is a git rebase --update-refs option that will automatically update the refs of any branches pointing to commits that are rebased, so you don't have to do it manually. Also see the jujutsu VCS on how this gets abstracted away and makes patching very easy.

Squashing

Squashing combines multiple commits into one, creating cleaner history.

Methods

1. Interactive Rebase: Use squash action in git rebase -i

2. Squash Merge (Recommended):

git merge --squash feature
Key Idea

Squash merge stages all feature branch changes as one change set without creating a merge commit.

Squash merge example

Before: A (main) → B → C (feature) After squash merge: A → D (main) where D combines B + C

Useful Commands

Stash

Stashing temporarily saves uncommitted changes so you can switch contexts.

# Stash changes
git stash

# Do other work (checkout commits, switch branches)
git checkout HEAD~3
git checkout feature

# Restore stashed changes
git stash pop

Revert

Revert creates a new commit that undoes the changes from a previous commit, preserving history.

# Revert the last commit
git revert HEAD

# Revert a specific commit
git revert <commit-hash>

# Revert multiple commits (creates separate revert commits)
git revert <commit1> <commit2> <commit3>

# Revert a merge commit (specify parent with -m)
git revert -m 1 <merge-commit>
Key Idea

Revert is the "safe" way to undo commits because it preserves history. Unlike git reset, which rewrites history, revert creates new commits that document the undoing.

Why revert > reset for shared commits:

  • Revert: Safe for pushed commits - creates new "opposite" commit
  • Reset: Dangerous for pushed commits - rewrites history and can break others' work
Detail

For merge commits, use -m 1 to revert to the first parent (usually main branch) or -m 2 for the second parent (feature branch).

Reflog

Reflog records where HEAD was and recent actions - useful for recovery.

# View reflog
git reflog

# Move to previous HEAD position
git reset --soft "HEAD@{2}"
Detail

HEAD@{n} refers to HEAD's position n moves ago in the reflog.

Aliases

Create shortcuts for commonly used commands:

# Simple alias
git config --global alias.co checkout

# Custom command combinations
git config --global alias.last 'log -1 HEAD'
Trick

Aliases save time on frequently used command combinations. Consider creating aliases for your most common workflows.

Range Notation

Range notation is a powerful way to specify commit ranges in Git commands.

Double Dot (A..B)

Commits reachable from B but not A

# See commits in feature not in main
git log main..feature

# Preview what you'll push
git log origin/main..HEAD

# See newly fetched changes
git log feature..origin/feature

# Diff between branches
git diff main..feature

Triple Dot (A...B)

Commits reachable from A or B but not both

# See all commits since divergence
git log main...feature
Key Idea

Range notation works with most Git commands - log, diff, cherry-pick, etc.

Test Your Knowledge

Question 1 of 4
What does 'git log main..feature' show?