Git - merge feature branch on more than one branch

231 views Asked by At

I have some questions about git and good practices ...

The state of the git repository is:

V1.0  :       B.A--B.B
V1.1  :      /  C.A--C.B
            /  /
master: A--B--C--D

I have a master, and 2 versions : 1.0 and 1.1.

A new feature must be developed and will have to be applied on 2 branches : V1.1 and master.

What's the better way to do that ? I guess I have to create a feature branch, but based on which one? master or V1.1?

What will be the best merge strategy once the development is validated ? merge ? cherry-pick ? rebase?

The feature branch will be pushed to upstream, because I won't be the only one to work on it. There will also have more than one commit.

Thanks for your help !


If the feature branch is based on master, I'll have this :

V1.0  :       B.A--B.B
otherbranch :/  C.A--C.B
            /  /
master: A--B--C--D
                  \
topicbranch:       E--F--G

Once the feature development is finished, I can easily merge master and topicbranch to add the new feature into master.

But how to add commit E, F and G into otherbranch (just after C.B)? That's where I think that

checkout otherbranch;
git merge topicbranch;

won't work, because it will also add the commit D.

2

There are 2 answers

1
torek On BEST ANSWER

You are correct: merging the new topic branch into V1.1 will bring in commit D. (Merging it into V1.0 will bring in both C and D.)

I'll just assume we're working on V1.1; the same apply to V1.0 except there will sometimes be more to do to handle C as well. (And I'm not sure why you changed V1.1 to otherbranch in your edit but I'll go with otherbranch below because I did a lot of copy-pasting....)

You have at least the following options:

  • Bring in D via a "real" merge (with --no-ff if needed, although it isn't), then revert it.
  • Do a "real" merge (as if with --no-ff) with --no-commit, then reverse-apply D (easiest with git revert --no-commit, but you can also git show D | git apply -R -, for instance), and commit the result.
  • Do a "squash" merge, then remove D as with either of the "real" merges.
  • Cherry-pick (with or without --no-commit) E, F, and G.

These all result in somewhat different commit graphs, except that squash-merging and "un-applying" D and then making one single commit produces the same graph (and files) as cherry-picking E-through-G with --no-commit and then committing the result.

A real merge gives you an actual merge commit:

V1.0  :       B.A--B.B
otherbranch :/  C.A--C.B-----M
            /  /            /
master: A--B--C--D         /
                  \       /
topicbranch:       E--F--G

If you allow git merge to make the commit (and it is able to do so on its own) the resulting tree will contain the changes made in D since the merge-base for otherbranch and topicbranch is commit C. But if you suppress the commit, and then—in the work directory—back out the changes made in commit D, and only then commit the resulting work-directory files, the tree for commit M will omit the changes in D.

The drawback is that on a future attempt to merge master or topicbranch into otherbranch, git will believe that the changes for D are in there (because they were, you just removed them).

Making a separate revert commit for D gives this graph:

V1.0  :       B.A--B.B
otherbranch :/  C.A--C.B-----M--R
            /  /            /
master: A--B--C--D         /
                  \       /
topicbranch:       E--F--G

where R is the "revert D" change. Again, a future attempt to merge master or topicbranch into otherbranch won't attempt to introduce the changes from commit D as git can tell that they were already put in. (The only difference between this and the previous scenario is that the fact that you took D out is explicitly recorded as a separate commit—not that anyone is likely to notice unless they go look, but it might be easier for them to find if/when they do go look, than if this is just mushed together into the merge commit M.)

In git, a "squash merge" commit is exactly the same as a regular merge commit except that M only has one parent. Git goes through the same merge machinery as usual, it just does not make the final commit, nor set things up so that your manual git commit will make a two-parent commit. So you get this graph:

V1.0  :       B.A--B.B
otherbranch :/  C.A--C.B-----M
            /  /
master: A--B--C--D
                  \
topicbranch:       E--F--G

(where calling this commit M is kind of a lie since it's not a merge, but what the heck :-) ). Again you can choose whether to reverse D before making commit M, or after, with the same reasoning applying. In either case, though, since M is not an actual merge commit, a later attempt to merge in the branch will try to introduce D, since the merge-base has not moved. (It will also try to introduce E through G again of course. If you get lucky, git will be able to tell that they're already in there, and the merge will just automatically "think" something like "oh, OK, already there, proceed!". If not, you'll have some manual cleaning-up to do.)

Finally, cherry-picking commits makes copies of their changes, or—with --no-commit—just applies their changes and skips the commit part. So assuming you combine all three into a commit N, you get this as the graph:

V1.0  :       B.A--B.B
otherbranch :/  C.A--C.B---N
            /  /
master: A--B--C--D
                  \
topicbranch:       E--F--G

If you leave them separate you get E'--F'--G' (where these names indicate the patches you'll see if you git show the commits will basically match the patches you see for git show of E, F, and G). If you combine them into one big new patch N, then N will look very similar to the way M looks in the squash-merge case, except that you won't have to "un-apply" D as you won't have brought those changes forward.

Either way, you then have to repeat the whole process for branch V1.0.

(But note: if you have one commit N that introduces E+F+G as it were, you can do one git cherry-pick of N. Which is not actually any easier than one cherry-pick of E F G, except maybe to think about.)

5
randomusername On

Generally speaking the master branch is for development of new features. So you would

git checkout master HEAD^
git branch --track topicbranch

and you would use topicbranch to develop the new feature. This way every time the master branch was updated you could do

git checkout topicbranch
git pull

to pull the updates into your branch from the upstream/mainstream branch without too much difficulty.

The big issue with what you are suggesting is that a particular "version" should never change. It should sit in the development history with a tag like v1.0 referring back to it. If you want a different topic branch to also have access to your new topic branch, you can just do

git checkout otherbranch
git merge topicbranch

and treat the upstream of otherbranch as just topicbranch.


As a rule of thumb, you always want to use git merge to combine two refs in your repository and create separate branches to hold each of these. This is because git merge has a number of features that make everything cleaner. Things like git cherry-pick are only meant for when you make a mistake or experience an epiphany only after you've committed a change to one branch.