I'm actually learning Git and I'm wondering what would be the best practice to achieve the "Desired state" :
Actual state:
Master A-----B------C
\
\
Dev D------E------F
Desired state:
Master
A-----B------C
|
|
Dev C------D------E------F
FYI : I'm in production on Master/Commit-B and I'm developping on Dev/Commit-D-E-F. I've seen that I want to commit a change on production but Dev/Commit-D-E-F are not ready to be merged in production.
I've read about the usefull stackoverflow post : How to merge a specific commit in Git using git cherry-pick
but I'm not too sure if i should be concern about "This changing of commit IDs breaks git's merging functionality among other things"? Should I need to change my commit vision to do not reproduce this scenario?
When my developpement will be finished I'm planning to merge the modification from Dev to Master as :
Future desired state:
Master
A-----B------C------D------E------F
As noted in comments, it's important to understand the loose relationship between commits and branches. A commit is either reachable from a branch, or not; it may be reachable from many branches. The fact that it was created while a particular branch was checked out, does not give it any special relationship with that branch.
What you've indicated is that you want the changes you made in C
to also be applied to the dev
branch. There are several ways to do this. First let's re-draw your "current state" in a notation that better reflects git concepts
A-----B------C <--(master)
\
\
D------E------F <--(dev)
Here we see that master
is a ref which is currently pointing to C
. A
and B
may have originally been created on master
, and certainly are reachable from master
(by commit parent pointers); but their relationship with master
is no stronger than that. In fact they are also reachable from dev
and have exactly the same relationship with dev
that they have with master
in the current state; which is one way of looking at why dev
reflects what you did in creating those commits.
Anyway, dev
is a ref pointing to F
, from which A
, B
, D
, and E
are also reachable.
Now there are a few additional problems with how your "desired state" is drawn; I'll draw up a few options for what you could do, and hopefully the concepts will be clearer.
So one option is to rebase dev
so that it includes C
. Technically this means that you create new commits (D'
, E'
, and F'
) that are the result of "replaying" the changes from D
, E
, and F
onto C
.
git rebase master dev
would give you
A-----B------C <--(master)
\
\
D'------E'------F' <--(dev)
Note that with this option, you don't need C'
because you're just making C
"reachable" from dev
. It is impossible to change D
's parent, though, so instead you create D'
(and then this in turn forces you to create E'
in place of E
, etc.)
Of note, D'
and E'
are untested states of the code. (So is F'
but you probably would be testing that anyway.) You may or may not care to go back and test these generated intermediate snapshots, but if you don't it might interfere with future efforts to track down a bug (e.g. using bisect
). If D'
and E'
aren't important enough to test, they probably aren't important enough to keep; so in that case you should consider doing the rebase interactively and squashing
commits E
and F
, giving
A-----B------C <--(master)
\
\
DEF <--(dev)
(where DEF
is a single commit that results from replaying changes from D
, E
, and F
).
When you want to incorporate dev
into master
, assuming no further changes occur to master
in between, you can choose to "fast forward" master
; in fact that's the default if you say
git checkout master
git merge dev
from this situation. This would give you
A-----B------C------D'------E'------F' <--(dev)(master)
(or
A-----B------C------DEF <--(dev)(master)
if you had used interactive rebase to squash the dev
commits). This is a result many people like for its simple linearity. Another option would be
git checkout master
git merge --no-ff dev
which gives you
A-----B------C --------------------M <--(master)
\ /
\ /
D'------E'------F' <--(dev)
where M
will have the same TREE
(content state) as F'
(since master
has no changes that weren't already accounted for in dev
). (Again D'
...F'
would be replaced by DEF
if you squashed during the original rebase.) This would be a little unusual, in that the rebase
is usually used to set up a fast-forward so you'd have a linear history. If you want to preserve the branch topology (which is why you'd use --no-ff
), you might choose a different means of reflecting C
's changes in dev
in the first place.
So going back to your current state, instead of rebasing you could merge from master
to dev
git checkout dev
git merge master
This would give you
A-----B--------------------C <--(master)
\ \
\ \
D------E------F------M <--(dev)
Again this makes C
changes reachable in dev
without duplicating the commit. In fact you don't duplicate or replace any existing commits this way, which is especially nice if the existing commits have been shared with other developers (e.g. pushed to origin).
But here you create a merge commit (M
), and some people consider these "backwards merge" commits to be ugly clutter. It's a somewhat understandable criticism; suppose you did this, then tested the merged result, then immediately merged back to master
; you'd either allow a fast-forward and get get
A-----B--------------------C
\ \
\ \
D------E------F------M <--(dev)(master)
which looks ok, but the parents of M
are not listed in the usually-expected order which may confuse people using --first-parent
to navigate the history...
or forbid fast-forward and get
A-----B--------------------C-----M2 <--(master)
\ \ /
\ \ /
D------E------F------M <--(dev)
which does seem a little weird.
One workflow that avoids these results is to consider the "backwards merge" temporary (for testing only) and undo it once testing is done.
git checkout dev
git reset --hard HEAD^
That again means that in the end you're storing probably-untested states of the code. Also, if M
had any conflicts, then you lose the resolution of those conflicts by doing this; and if dev
isn't immediately ready for re-integration into master
, you could end up re-doing that resolution repeatedly. git rerere
is available to mitigate this issue.
fwiw, my preference is to just accept the merge commit "clutter" as it generally doesn't hurt anything (and git can help you filter it out of log output anyway).
Anyway, you see that we still haven't had to create a C'
(because C
doesn't "belong to master
" in any way that prevents us incorporating it directly into dev
). Replaying the changes in C
to a new C'
commit "on dev
" is an option, but I recommend against it, because duplicate commits just create the risk of unnecessary merge conflicts down the line. If you really want to do it this way, you could use cherry-picking or, in the specific case you site, a so-called "squash merge" of master
into dev
. (There are other ways as well.)