Eli
Eli

Reputation: 38899

Git rebase conflicts after successful merge?

I have two branches: let's call them master and feature. I'm now trying to merge recent changes from master into feature. I normally prefer rebases over merges, but these two branches have diverged significantly, so I decided to do a merge instead to have a record of all the conflict resolution. I finished up the merge, and committed it on feature, and thought I was done. Everything looked good.

Now, I made a small change to master and wanted to just rebase that into feature, but git rebase master now gives me conflicts with older commits that were already dealt with in my last merge. Weirdly, git merge master gives no conflicts as expected. What gives?

Upvotes: 2

Views: 1095

Answers (2)

torek
torek

Reputation: 487725

Your phrasing ("rebase [a small change made to master] into feature") seems a bit weird to me, since normally you'll be rebasing feature onto master. (The word "into" doesn't really apply well.) That aside, I think it always helps to draw the commit graph. You can use gitk or a similar viewer to draw it, or git log --graph, perhaps with --oneline as well, to draw a vertically oriented graph. But here I'll draw a horizontal one, with single uppercase letters representing particularly interesting commits, and lowercase os representing more-boring commits.

For our purposes here I will leave out any configured upstreams (origin/master and/or origin/feature). If they do exist, you might want to add them to your own drawing, and then note that when git rebase makes copies of commits, it does not move any of the other labels (including these remote-tracking branches) pointing to existing commits: it only moves one single label, that of the current branch.

... - A - o - o - o - F      <-- master
       \               \
         B - C - D - E - G   <-- HEAD -> feature

With any luck this is reasonably close to your pre-rebase setup, and accurately reflects the result of your git merge. Both before and after you did the merge, commit A is the original base at which feature split off from master; commits B through E were made on feature; the various less-interesting o commits were made on master; and commit F was the tip of master. You were on branch feature (so that HEAD named branch feature, and git status said "on branch feature") and you ran git merge master and did the merge and committed.

This merge created the tip-most commit on feature, which is commit G, which is a merge commit.

After that, you checked out branch master (making HEAD point to the name master) and made a new commit that moved the tip of master, so let's add that to our graph-so-far:

... - A - o - o - o - F - H     <-- HEAD -> master
       \               \
         B - C - D - E - G      <-- feature

Finally, you wanted to rebase feature on the (new tip of) master so you did git checkout feature:

... - A - o - o - o - F - H     <-- master
       \               \
         B - C - D - E - G      <-- HEAD -> feature

and now you run the command git rebase master.

What rebase does is copy commits.

First, it has to find which commits to copy. The commits it should copy are those that are reachable from the current branch—i.e., from the name feature—but not from the branch whose name you give as the upstream, i.e., master.

Here's where we run into a rather large problem. Look at this (rather dense) paragraph from recent git rebase documentation:

All changes made by commits in the current branch but that are not in <upstream> are saved to a temporary area. This is the same set of commits that would be shown by git log <upstream>..HEAD; or by git log 'fork_point'..HEAD, if --fork-point is active (see the description on --fork-point below); or by git log HEAD, if the --root option is specified.

The fork point stuff is itself a bit complicated but we can ignore it for now because using the command git rebase master means it's turned off. You can therefore see the commits that are to be rebased with git log master..HEAD. This is commits B, C, D, E, and G (except that rebase normally tosses out merges).

You might wonder why B through E are included here, given that the merge base of master and feature is commit G. The problem is that, while merge commit G points back at commit F (reachable from master), commit F does not (and cannot) "point forward" to G. So when we start from the tip of master (new commit H) and work backwards, we get H, F, all the boring os, A, and everything before A: these are the commits excluded. When we start from the tip of feature (commit G) and work backwards, we get G, E, D, C, and B before we hit the first excluded one (A). Thus these are the candidates for rebasing.

If you allow the rebase to proceed, and resolve all the conflicts, you'll end up with:

                            B' - C' - D' - E'   <-- HEAD -> feature
                           /
... - A - o - o - o - F - H     <-- master
       \               \
         B - C - D - E - G      [abandoned]

(assuming all of the to-be-copied commits have actual changes to copy; commit G won't need to be copied as it won't contribute anything this time).

Upvotes: 2

scrowler
scrowler

Reputation: 24406

That behaviour is happening because when you merge master in, the changes go on the top/end of feature.

When you rebase, the changes from master exist at the beginning of feature.

This is why when you merge over the top, you fix all your conflicts, you merge again, then those conflicts are already resolved. When you rebase however, you have to merge the conflicts again because the previous commit where you resolved everything is at the end of the branch.

Sounds like you're best to choose either merging or rebasing and stick with it for the life of feature.

Upvotes: 2

Related Questions