Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Merging a feature branch that is based off another feature branch

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.

like image 678
Chris White Avatar asked Feb 13 '17 18:02

Chris White


2 Answers

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. :-)

like image 167
torek Avatar answered Sep 24 '22 05:09

torek


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.

like image 42
Useless Avatar answered Sep 22 '22 05:09

Useless