Diagnose and Recover from Branch Collisions

A collision is two writers changing the same branch independently: two machines, two people, or you and a tool or bot. Neither side sees the other’s commits until someone fetches or pushes. Placeholders below: <remote> (usually origin) and <branch>.

For the basic pull --rebase vs pull --no-rebase choice, see Resolve Divergent Branches. This page covers working out what happened, preventing it, and recovering when the simple fix has already gone wrong.

1. Identify it

Common symptoms

You seeIt means
! [rejected] ... (fetch first)Remote has commits you don’t. Fetch and integrate.
! [rejected] ... (non-fast-forward)Your branch and the remote have diverged, or the remote was rewritten.
Your branch and '<remote>/<branch>' have divergedBoth sides have unique commits.
fatal: refusing to merge unrelated historiesThe two sides share no common commit.
rebase in progress / You are in the middle of a mergeA previous integration attempt was never finished.
A commit you made isn’t on the remote or on another machineIt’s unpushed, or on a different branch.

Look before you act

git fetch only updates your remote-tracking refs. It never touches your branches or working tree, so it is always safe to run first.

git fetch <remote>
git status -sb                                    # ahead/behind counts
git log --oneline <remote>/<branch>..HEAD         # yours, not on remote
git log --oneline HEAD..<remote>/<branch>         # remote's, not yours
git log --left-right --oneline HEAD...<remote>/<branch>   # both, < yours > theirs
git log --graph --oneline --decorate --all -20    # the whole picture
git branch -vv                                    # local branches and their upstreams
git branch -r                                     # what exists on the remote
git merge-base HEAD <remote>/<branch>    # prints a commit; nothing = unrelated
git rev-list --max-parents=0 HEAD        # root commit(s) of each side

No merge-base means the histories are unrelated: a branch was re-created or the remote history rewritten. That is a different problem from a normal divergence. Don’t rebase across it; decide which side is authoritative first.

What state am I in right now?

git status       # names any in-progress rebase / merge / cherry-pick / bisect
git branch --show-current    # empty output = detached HEAD

While rebasing, HEAD is detached and, in conflict markers, “HEAD” / “ours” is the branch you’re rebasing onto; “theirs” is your own commit being replayed. It is backwards from a merge, and a common source of resolving a conflict the wrong way round.

”Where did my commit go?”

git log --all --oneline -10     # every branch, local and remote-tracking
git reflog -20                  # every place HEAD has been, even commits no
                                # branch points to any more
git branch --contains <sha>     # which branches include a given commit

A commit absent from every listing is usually unpushed on another machine or clone: run the same commands there.

2. Avoid it

  • Sync first. Before editing on any machine or session:
    git fetch <remote> && git status -sb
    Integrate if behind and your tree is clean.
  • One writer per branch. Give each person, machine, or tool its own branch and merge deliberately.
  • Push early and often. An unpushed commit is invisible to everyone else, and a small commit collides smaller.
  • Make git pull unambiguous. Pick a default so a pull never improvises:
    git config --global pull.rebase true    # replay local commits on top
    # or: git config --global pull.ff only  # refuse anything but fast-forward
  • Stage files by name rather than git add -A/git add ., so scratch files and unrelated edits don’t ride into a commit.
  • Finish what you start. Don’t leave a rebase or merge half done; run git status when you return to a repo.

3. Clean it up

Step 0: snapshot first

A branch is only a pointer. Add a second one and the commits can’t be lost whatever you do next.

git branch backup-<branch>

Copy out any untracked or uncommitted files you care about too, or git stash push -u them.

Pick a strategy

SituationStrategy
Few local commits, touching different files from the remote’sRebase (A)
You want to keep both histories visibleMerge (B)
Small local change, lots of conflicting remote changeReset and re-apply (C)
Local work is disposable, remote is rightReset to remote (D)
Remote is wrong, yours is right, and you’re allowed to overwrite itForce with lease (E)

A. Rebase: replay your commits on top

git pull --rebase <remote> <branch>
# on conflict: fix the files, remove every marker, then
git add <file>
git rebase --continue
git rebase --skip          # drop the current commit
git rebase --abort         # give up, back to where you started

Expect to resolve conflicts once per replayed commit.

B. Merge

git pull --no-rebase <remote> <branch>
# on conflict: fix files, then
git add <file>
git commit
git merge --abort          # give up, back to where you started

One resolution in total, at the cost of a merge commit.

C. Reset to the remote and re-apply by hand

git rebase --abort                         # or: git merge --abort
git diff <old-base> backup-<branch> -- <file>    # what you actually changed
git reset --hard <remote>/<branch>         # branch now matches the remote
git checkout backup-<branch> -- <paths-of-new-files>
# redo the edits by hand, skipping anything now obsolete, then
git add <files> && git commit -m "Re-apply local changes"
git push <remote> <branch>

git reset --hard throws away uncommitted changes to tracked files and moves the branch pointer. It is safe here only because step 0 preserved the old commits.

D. Throw away local and match the remote

git fetch <remote>
git reset --hard <remote>/<branch>

E. Overwrite the remote (history rewrite)

Only when you are sure the remote’s commits are the ones to lose, and nobody else builds on them:

git push --force-with-lease <remote> <branch>

--force-with-lease refuses if the remote moved since your last fetch. Plain --force doesn’t. Avoid either on shared or auto-deployed branches.

Unrelated histories

If merge-base found nothing, choose a side:

  • Remote is canonical: git reset --hard <remote>/<branch> (after the backup), then re-apply any commits you need with git cherry-pick <sha>.
  • You really want both: git merge --allow-unrelated-histories <remote>/<branch>, and expect conflicts in nearly every shared file.

Pulling single commits across

git cherry-pick <sha>              # one commit
git cherry-pick <sha1>..<sha2>     # a range, excluding sha1

If rebase --abort or a checkout refuses

error: The following untracked working tree files would be overwritten

Leftover untracked files collide with tracked ones. Copy them elsewhere, delete them from the working tree, and retry.

Check you didn’t commit conflict markers

git add doesn’t check whether a conflict is truly resolved. It will stage <<<<<<< lines happily.

git diff --check                                    # flags leftover markers
git grep -nE '^(<<<<<<<|=======|>>>>>>>)'            # search the tree

Tell-tale in a commit summary: an insertion count equal to your lines plus 3 (one line each for <<<<<<<, =======, >>>>>>>). Inspect the file as committed with git show <sha>:<file>.

Recover something that seems lost

git reflog                       # find the sha
git branch rescue <sha>          # pin it to a branch

Reflog entries are kept for weeks by default, so a “lost” commit is almost always still recoverable. Garbage collection only removes it after that.

Verify before pushing

git log --oneline <remote>/<branch>..HEAD   # only the commits you intend
git diff <remote>/<branch> --stat           # only the files you intend
git diff --check                            # no stray markers or whitespace errors

Then git push. Once pushed and confirmed, the backup branch can go: git branch -d backup-<branch> (use -D only if you’re sure its commits are safely elsewhere).

Shell differences

Git commands are identical on every platform. Only the surrounding file operations differ:

# PowerShell
mkdir ..\rescue
copy .\path\file1, .\path\file2 ..\rescue\
git show <sha>:<file> | findstr "<<<<<<<"
# bash
mkdir ../rescue && cp path/file1 path/file2 ../rescue/
git show <sha>:<file> | grep '<<<<<<<'