How to merge main into a branch in Git (and when to rebase instead)
Your feature branch is behind main and you need to catch up before opening a PR. Here are the two commands that solve it, the trade-off between merge and rebase, and the conflict cases that bite people every time.
It is Wednesday afternoon. Your feature branch is two weeks old. main has shipped seventeen commits since you forked. The PR review is finally green, but the merge button is grey, with a “this branch is out of date” warning underneath it. You need to bring main into your branch and you would rather not turn the rest of the day into a rebase forensics session.
The short version of what to do:
git fetch origin
git merge origin/main
That is it. Two commands. Your branch now contains every commit on main, your branch’s own commits, and a merge commit that joins the two histories together. Push and the PR is up to date.
The longer version is about when this is the right move and when git rebase is. Both bring main into your branch. Both resolve the “out of date” warning. They produce different histories, and the choice has consequences for the people who read your PR and the people who run git log six months from now.
Merge or rebase: the actual difference
git merge origin/main keeps your branch’s history intact and adds a merge commit at the tip that has two parents: your previous tip and the current main. The history becomes a graph. Anyone reading git log sees that your branch was synced with main at a specific moment in time.
git rebase origin/main takes your branch’s commits, rewinds them, applies main’s new commits on top of the base, and then reapplies your commits one by one. The history becomes a straight line. The merge commit is gone because the commit graph no longer has a fork.
Two questions decide which one to reach for.
Has anyone else pulled your branch? If yes, rebase rewrites history they already have. They will see “your branch and origin/branch have diverged” the next time they fetch, and the recovery is awkward enough that most teams treat shared branches as merge-only by policy.
Does your team care about a linear history on main? Some teams enforce a no-merge-commits-on-main policy through GitHub branch protection. In that world, rebasing your feature branch onto main before merging is the only way to keep main linear. Other teams accept merge commits as a useful signal of when integration happened. Both work. Pick one and write it down so nobody has to guess.
If you are alone on the branch and your team prefers linear history, rebase. Otherwise, merge.
What “merge main into branch” actually does step by step
git fetch origin
This updates your local view of the remote without touching your working tree. After this, origin/main is the most recent commit on the remote main. Your local main branch (if you have one) is untouched until you git pull or git merge it explicitly.
People skip git fetch and use git pull origin main instead. That works, but it merges into whatever branch you are currently on, which is sometimes what you want and sometimes not. Splitting fetch from merge makes the intent visible.
git merge origin/main
This brings the new commits from origin/main into your current branch. Git computes the three-way merge, identifies conflicts, and either creates a merge commit or fast-forwards if your branch had no commits ahead.
If a conflict appears, Git stops and tells you which files are in conflict. Resolve them in your editor, run git add on the resolved files, then run git merge --continue. A plain git commit works too because Git knows it is in a merge state (it has .git/MERGE_HEAD), but --continue is the more explicit form and makes the intent visible in your shell history.
If you want to abort and start over: git merge --abort returns the working tree to the state before the merge.
The rebase version, with the part nobody mentions
git fetch origin
git rebase origin/main
Git rewinds your branch’s commits, applies the new main commits to the base, and then reapplies your commits one by one. If a commit conflicts, Git stops at that commit. You resolve, git add, then git rebase --continue. Repeat for every conflicted commit.
The part nobody mentions: if you have ten commits on your branch and the first one creates a function that the tenth one renames, you may resolve the same conflict ten times, once per commit. The remedy is to squash your branch first (git rebase -i origin/main with squash actions, or git reset --soft origin/main && git commit) and then rebase a single commit onto main. The conflict resolution happens once.
After a rebase, the next git push will be rejected because your local branch has rewritten history compared to the remote. The fix is git push --force-with-lease (not plain --force, which can clobber work someone else pushed since you fetched). --force-with-lease checks that the remote tip is what you expected before overwriting.
Conflicts: the three that actually bite
The Git documentation enumerates conflict types abstractly. In practice, three patterns produce most of the pain.
Both sides edited the same line. Git marks the conflict, you pick one (or write a third version), git add, continue. Mechanical, irritating, fine.
You edited a file that the other side deleted. Git tells you the file is in conflict, but the actual question is: do you want the file back, or do you want the deletion? git rm <file> to accept the deletion, or git add <file> to keep your edited version. Picking the wrong one quietly is how files reappear in a codebase months after a deletion.
Logical conflicts that Git does not see. Your branch added a caller to getUser(id). main renamed it to getUserById(id). The two changes do not touch the same lines, so Git merges cleanly. The result does not compile. The only fix is to run your test suite after every merge or rebase, before pushing. The PR review missed it because the PR diff was clean before the merge. CI catches it. If CI is slow or flaky, this is the bug that ends up in main.
When “merge main into branch” is a smell
If you merge main into your branch once a week, that is normal. If you merge it once a day because every other PR landing breaks yours, your team has outgrown the “everyone merges manually” workflow.
The structural fix is a merge queue. The queue tests each PR against the latest main before merging, in the order they will land, so the “your branch is behind” loop disappears. The PR you reviewed is the PR that ships. No more pre-merge re-syncs. No more “I had to rebase and now CI is red on an unrelated test.”
Mergify’s merge queue was built around this exact problem and handles squash, rebase, and merge-commit modes per branch protection rule. The TLA+ verification of the core algorithm is worth reading if you want the proof that “the commit you tested is the commit you land.”
Cheat sheet
| Situation | Command |
|---|---|
| Update your local view of the remote | git fetch origin |
Bring main into your branch, keep history graph | git merge origin/main |
Bring main into your branch, keep linear history | git rebase origin/main |
| Conflict resolved, continue the merge | git add <files> then git merge --continue |
| Conflict resolved, continue the rebase | git add <files> then git rebase --continue |
| Abort and start over | git merge --abort or git rebase --abort |
| Push after a rebase | git push --force-with-lease |
| Squash branch to one commit before rebasing | git reset --soft origin/main && git commit |
The choice between merge and rebase matters less than picking one and being consistent. Pick the one your team’s branch-protection rules prefer. Write it in CONTRIBUTING.md. Move on.