Zero to Hero: git rebase
You're two eight hours deep developing on your feature branch, feeling ready to let your teammates go to town reviewing your work.
Unknown to you, 300 other engineers have pushed changes in the last hour to main
. The senior engineer on your team said you should rebase, so
you take a deep breath and type
git fetch origin main:main
git rebase origin/main
and then...

You're looking at a wall of conflicts that looks like someone fed your code through a blender. Files you've never touched are now yelling at you and you have no idea what any of it means.
Your carefully crafted conventional commits
have somehow become a tangled mess of "f*** this linter"
, "fix style"
, and "revert commit that ruined my marriage"
.
Your terminal is mocking you with cryptic error messages, and you're starting to question how much longer your team will put up with your incompetence.
You have a couple options, but let's talk a little more about the situation.
higher-velocity monorepo
In the higher-velocity monorepo there's maybe xx
-xxx
new commits pushed to main
every day. When a PR is merged into main
its commits are squashed into one commit where the commit message is the PR title.
Then that squashed commit gets thrown into a scheduled deployment train for a release to production.
Many teams in a repo like this adopt a "rebase if it's easy, merge if it's not" policy.
# quick check - are we badly behind?
git log --oneline HEAD..origin/main | wc -l
# < xx commits behind? rebase is probably fine
# > xxx commits behind? maybe merging is easier
But sometimes in a repository like this, you have to rebase.
- you want CI running against the actual codebase your code will land in
- it's easier to see commits pressed against the latest
main
than intertwined with your branch's commit history - whatever the reason is...
It's always really daunting having a lot of conflicts on your local with files you've never maintained or seen before from engineers you've never heard of (like who is Gaishriknani from Accounts Unreceivables and why is their commit failing in my rebase?).
Let's look at lower-velocity microrepositories, then talk about what to do with these conflicts.
lower-velocity microrepos

In lower-velocity microrepos, your team (~10) probably owns the repository and it's just you and your teammates pushing to it maybe x
-xx
times a day.
Maybe there's a similar merge process in the higher-velocity monorepo; squash branch commits to PR title, throw the commit in a deploy train or manually tag releases.
Since your teammates work on different features than yours, you might think you can avoid the chaos of merge conflicts, but if the codebase is small, the possibility of colliding with teammates is higher.
The good news is that small repos have less code to contextualize, and since your team owns it, you should be way more familiar with the conflicting code and can use your intuition to figure it out pretty quickly.
Systemically solving conflicts

Regardless of how often code is merged into main
, conflicts that are rooted in poor workflows can't be solved with better git commands.
You need systemic changes that reduce the likelihood of conflicts in the first place.
The approach I recommend is feature flagging and splitting the work up across multiple smaller branches.
In both rapid and slow environments, this has a lot of added benefits:
- smaller branches touch fewer files, making conflicts less likely and easier to resolve
- teammates can review and merge smaller changesets more quickly
- short-lived branches create a more linear, readable commit history
- small, focused branches let you jump between tasks without complex workspace management *
- feature flags let you merge code safely while controlling when features go live
- small, atomic changes are much easier to revert if something goes wrong
- multiple developers can work on the same feature area with less coordination overhead
If you still got a conflict during your rebase- whether you implemented these practices or not- that's okay!
1. Identify what's actually yours
# see which files you actually modified in your branch
git diff origin/main --name-only
# compare to the conflicted files Git is showing you
# anything not in your diff? probably not your problem
2. For files you never touched: take theirs
# you never modified this file, so take the main version
git checkout --theirs database-migration-utils.ts
git add database-migration-utils.ts
3. For files you did modify: actually resolve
These ones need your brain. Read the conflict, see what both sides are doing, then make them work together.
4. When you're unsure: check the blame
# who changed this recently? ask them
git blame asdf.go
5. Continue the rebase
git add <files>
git rebase --continue
If after running git rebase --continue
, you get thrown into a vi/vim session asking you to provide a commit message, just type :x
to
use the existing one in the rebase and exit
Most rebase conflicts in unrelated files are noise from the time gap between your branch and main. Take the main version for files you never intended to change, and only spend brain cycles on files that actually matter to your feature.
If you do all of the above, eventually you'll see this
Successfully rebased and updated refs/heads/<branch-name>.
Rebase git pull
disaster (and why it matters more in monorepos)
So you've finished rebasing, you've gone through all of the conflicts and you're ready to get your code up for your teammates to review. That's wonderful, I'm proud of you. Before you do that, there's one thing you must not do!
# after rebasing locally
git rebase origin/main
# ...
# Successfully rebased and updated refs/heads/<branch-name>.
# this will fail
git push origin feature-branch
# it prints this
# To https://github.com/user/repo.git
# ! [rejected] main -> main (non-fast-forward)
# error: failed to push some refs to 'https://github.com/user/repo.git'
# hint: Updates were rejected because the tip of your current branch is behind
# hint: its remote counterpart. Integrate the remote changes (e.g.
# hint: 'git pull ...') or push your changes forcefully (e.g. 'git push --force')
# hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Even though the hint tells you to git pull
, you must not listen to it!
Why does Git suggest git pull
?
Git doesn't know you intentionally rewrote history with a rebase. It just sees that your local branch has diverged from the remote branch, so it suggests the "safe" option of pulling to integrate changes.
Why this is wrong
- pulling would create a merge commit!!!!
- this would undo much of the benefit of rebasing (clean, linear history)
- you'd end up with the old commits AND the rebased commits
What you want instead
# safer force push that checks if remote has new commits
git push --force-with-lease
# or regular force push if you're certain
git push --force
What the git pull
actually does here
# git pull = git fetch + git merge
# Your local branch: A---B---C'---D' (rebased commits with new SHAs)
# Remote branch: A---B---C----D (original commits with old SHAs)
# Git creates a merge commit combining both:
# A---B---C----D
# \ \
# \ M (merge commit)
# \ /
# C'---D'
You now have duplicate commits! Every change exists twice with different SHAs. Confusing, right. Your "clean" rebase just became a very messy rebase.
In a large org with codeowners, this gets exponentially worse:
# Your rebased branch now shows:
- File A changed in commit C (original)
- File A changed in commit C' (rebased duplicate)
- File B changed in commit D (original)
- File B changed in commit D' (rebased duplicate)
- Merge commit M touching everything
!!! Every codeowner gets notified for BOTH versions of every change. If your branch touched 50 files across 10 teams, those teams now get notifications for 100+ file changes plus the merge commit. !!!
# Reviewers see this in the PR:
+ function addUser() { ... } // From commit C
- function addUser() { ... } // From commit C'
+ function addUser() { ... } // Same change, different SHA
Git doesn't know you intended to replace the old commits-- it thinks you wanted to combine them, so
git pull
treats your rebased commits as "new" changes to be merged with the "old" remote commits.
In a monorepo with hundreds of engineers, this creates organizational chaos - spamming codeowners, confusing reviewers, and potentially crashing review systems.
In a small microrepo? Your 10 teammates get some extra notifications, someone asks "wtf happened here?" in Slack, and you all move on with your lives. Still not ideal, but the stakes are dramatically lower.
I've done it once in both settings. What should have been executed after git rebase
completed is git push --force-with-lease
or git push --force
. The --force-with-lease
option is good if your branch has other folks pushing to it, but I don't believe in this workflow, so I personally git push --force
.
Sometimes the best way out of a Git hole is to just... not be in the hole. The best foot forward is to close the PR with a comment apologizing for the notification, and make a new branch.
# 1. make a new branch from your messed up branch
git checkout feature-branch-messy
git checkout -b feature-branch-fixed
# 2. reset to before you did the git pull (the clean rebased state)
# view the reflog to see what happened
git reflog
# look for the commit hash right before you did the `git pull`
# then reset to that specific commit
git reset --hard <commit-hash-before-pull>
# 3. force push the clean version
git push --force-with-lease origin feature-branch-fixed
And that's it! Send that PR to your coworkers for their edifying feedback.
