20

A few days ago I had a master branch with a completely linear history. Then I created a feature branch, which we'll call feat/feature-a. I worked on that branch, then submitted it for code review to be merged into master.

While feat/feature-a was being reviewed, I wanted to work on another feature that relied on some code introduced by feat/feature-a. So I created a feat/feature-b branch from the feat/feature-a branch.

While I was working on feat/feature-b, feat/feature-a got merged into master. So now master has the code introduced by feat/feature-a. I now want to merge feat/feature-b into master, but I get a lot of merge conflicts that look like this:

<<<<<<< HEAD
=======
    // Some code that was introduced by feat/feature-b
>>>>>>> c948094... My commit message from feat/feature-b

My guess is that because I took feat/feature-a changes into my feat/feature-b branch, I'm now trying to "duplicate" those changes which is ending in merge conflicts.

I can resolve these manually, but they exist multiple times over tens of files, so I'd like to know a better solution if there is one.

Chris White
  • 865
  • 1
  • 9
  • 20
  • 1
    Was it an actual merge, or a squash, or a rebase, or something else? – Useless Feb 13 '17 at 18:12
  • Right now I'm rebasing `feat/feature-b` with master via `git rebase -i master`. I'm squashing all my commits on `feat/feature-b` down into 1 commit. – Chris White Feb 13 '17 at 18:14
  • I meant to ask, was the merge of `feature-a` a real merge. But, if the merge if `feature-b` isn't a real merge, that'll break it too – Useless Feb 13 '17 at 18:17
  • It was. `feat/feature-a` got squashed down into one commit and then merged into master, producing a merge commit. – Chris White Feb 13 '17 at 18:18
  • was that squashed commit still on `feat/feature-a`? So that's the parent of `feat/feature-b`? – Useless Feb 13 '17 at 18:19
  • Oh, and this is just for reference: please don't say _merge_ if you mean `rebase`, and please say if things have been squashed and how they're related. It's really confusing otherwise. – Useless Feb 13 '17 at 18:36
  • Squashing `feat/feature-a` is a destructive action, meaning that the commits `feat/feature-b` is based on are *not* in `master`, meaning that when you attempt to merge `feat/feature-b` you're (often) going to get conflicts. You're probably just going to have to deal with them. – Chris Rasys Feb 13 '17 at 18:38
  • @Useless, apologies. It's hard for me to use the correct terminology because... well I don't know it. :) – Chris White Feb 13 '17 at 19:01
  • @ChrisRasys, thanks. I think I'm going to propose we change our Git workflow to avoid things like this. – Chris White Feb 13 '17 at 19:01

2 Answers2

36

Summary: use git rebase --onto <target> <limit>

As Useless suggested in a comment, if you had a real merge, this should not happen. Here's what I mean by a "real merge", along with a diagram of how the branching looks if you draw the graph of the commits in question. We start with something like this:

...--E---H         <-- master
      \
       F--G        <-- feat/feature-a
           \
            I--J   <-- feat/feature-b

Here there are two commits (though the exact number does not matter) that are only on feat/feature-b, called I and J here; there are two commits that are on both feature branches, called F and G; and there is one commit that is only on master, called H. (Commits E and earlier are on all three branches.)

Suppose we make a real merge on master to bring in F and G. That looks like this, in graph form:

...--E---H--K      <-- master
      \    /
       F--G        <-- feat/feature-a
           \
            I--J   <-- feat/feature-b

Note that real merge K has, as its parent commit history pointers, both commit H (on master) and G (on feat/feature-a). Git therefore knows, later, that merging J means "start with G". (More precisely, commit G will be the merge base for this later merge.)

That merge would just work. But that's not what happened before: instead, whoever did the merge used the so-called "squash merge" feature. While squash-merge brings in the same changes that an actual merge would, it doesn't produce a merge at all. Instead, it produces a single commit that duplicates the work of the however-many-it-was commits that got merged. In our case, it duplicates the work from F and G, so it looks like this:

...--E---H--K      <-- master
      \
       F--G        <-- feat/feature-a
           \
            I--J   <-- feat/feature-b

Note the lack of a back-pointer from K to G.

Hence, when you go to merge (real or squash-not-really-a-"merge") feat/feature-b, Git thinks it should start with E. (Technically, E is the merge base, rather than G as in the earlier real merge case.) This, as you saw, winds up giving you a merge conflict. (Often it still "just works" anyway, but sometimes—as in this case—it doesn't.)

That's fine for the future, perhaps, but now the question is how to fix it.

What you want to do here is to copy the exclusively-feat/feature-b commits to new commits, that come after K. That is, we want the picture to look like this:

              I'-J'  <-- feat/feature-b
             /
...--E---H--K        <-- master
      \
       F--G          <-- feat/feature-a
           \
            I--J     [no longer needed]

The easiest way to do this is to rebase these commits, since rebase means copy. The problem is that a simple git checkout feat/feature-b; git rebase master will copy too many commits.

The solution is to tell git rebase which commits to copy. You do this by changing the argument from master to feat/feature-a (or the raw hash ID of commit G—basically, anything that identifies the first1 commit not to copy). But that tells git rebase to copy them to where they already are; so that's no good. So the solution for the new problem is to add --onto, which lets you split the "where the copies go" part from the "what to copy" part:

git checkout feat/feature-b
git rebase --onto master feat/feature-a

(this assumes you still have the name feat/feature-a pointing to commit G; if not, you'll have to find some other way to name commit G—you may wish to draw your own graph and/or or look closely at git log output, to find the commit hash).


1"First" in Git-style backwards fashion, that is. We start at the most recent commits, and follow the connections backwards to older commits. Git does everything backwards, so it helps to think backwards here. :-)

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
1

The simple model looks like this:

X  -> MA  <master
  \  /
   A      <feature-a

here, feature-a may have been squashed via rebase into a single commit, but the merge is still a real merge. Then, you have

X -> MA -> MB  <master
 \  /     /
  A ---> B     <feature-b

where feature-b is based on feature-a after any squashing, and also merged normally. This case should just work, because git can see that A is an ancestor of B and that you already merged it.

For comparison, this won't work cleanly:

X -> MA -> ...                  <master
|\  /      
| As                            <feature-a
|  |
|  ^--squash------<--
 \                   \
  A0 -> A1 -> ... -> An -> B    <feature-b

you squashed A0..n into As before merging feature-a, but feature-b was branched from An.

Now git has no idea how As and A0..n are related, so neither merging nor (simple) rebasing will work automatically. See torek's excellent answer if you want to use rebase --onto to fix this situation.

Useless
  • 64,155
  • 6
  • 88
  • 132