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!
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 commitdrop
: Remove the commitedit
: Pause to make changesreword
: Change commit messagesquash
: Combine with previous commit
If conflicts occur:
- Resolve conflicts manually
git add
resolved filesgit rebase --continue
- Or
git rebase --abort
to cancel
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:
- Your rebase changes commit SHAs, making the "same" commits appear as different commits
- Collaborators who built on your original commits now have orphaned work
- When they try to merge/push, Git sees duplicate commits with different SHAs
- This creates merge conflicts, duplicate commits, and a tangled history that's nearly impossible to untangle
- 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.
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
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>
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
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}"
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'
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
Range notation works with most Git commands - log
, diff
, cherry-pick
, etc.