Reputation: 5567
I was reading this article http://supercollider.github.io/development/git-cheat-sheet.html, which proposed the following workflow:
git checkout master
git pull --rebase # update local from remote
git rebase master chgs
git checkout master
git merge chgs
git push
I'm sure I don't understand how this works and why it is useful.
Doesn't the third line, git rebase master somechanges
, put commits from chgs
after the last commit from master
but without merging them, and leaves HEAD
pointing to the latest commit from chgs
, like this:
(master-commit-n-1)<--(master-commit-n)<--(chgs-commit-k-1)<--(chgs-commit-k)<--HEAD
Does that leave me with chgs
checked out? That's why I need to check out master
again?
And why do we need to merge the changes into master? Doesn't that make a graph like this:
(master-commit-n-1)<--(master-commit-n)<--(chgs-commit-k-1)<--(chgs-commit-k)
\.(master-with-merged-chgs-commit-k)<--HEAD
I don't see why the top fork is useful.
Upvotes: 1
Views: 2623
Reputation: 488183
In order:
Doesn't the third line,
git rebase master somechanges
, put commits fromchgs
after the last commit frommaster
but without merging them ...
Yes, assuming somechanges
is a typo for chgs
(the referenced page uses yet more spellings).
and leaves
HEAD
pointing to the latest commit fromchgs
Yes but only indirectly: HEAD
literally contains ref: refs/heads/chgs
, i.e., you are left with branch chgs
checked-out. It's chgs
itself that points to the latest commit.
Does that leave me with
chgs
checked out?
Yes.
Specifically, the "current branch", the one printed as on branch ...
when you use git status
, is just whatever refs/heads/branch
is in the file HEAD
, when HEAD
contains refs: refs/heads/branch
. Running git checkout branch
first makes sure it's OK to update the work tree, then does that and puts you on that branch, by rewriting HEAD
.
Internally, git rebase upstream branch
invokes git checkout branch
, so the whole process starts by getting you on the branch.
And why do we need to merge the changes into master? Doesn't that make a graph like [snipped]?
You need1 the merge
to move the label.
I prefer to draw the commit graph with letters, or sometimes just little o
nodes, with either single dashes or short arrows between them to denote the parenting relationships. And, stealing (but modifying) a leaf from git log --graph --oneline --decorate
, I add the branch name on the right with a longer arrow. If HEAD
names the branch, I add HEAD=
in front.
What you have right after the rebase can thus be drawn as:
... - E - F <-- master
\
G - H - I <-- HEAD=chgs
Here the commit labeled F
is the tip of branch master
, so master
points to F
(and F
points back to E
and so on). The tip of branch chgs
is commit I
; I
points back to H
; H
points back to G
; and G
points back to F
. And of course HEAD=chgs
so you're on that branch.
Once you git checkout master
, that updates your work tree as usual, and makes HEAD
point to master
which points to F
. Then if you run git merge chgs
, git looks to see if it's possible to do a "fast forward" merge:
... - E - F <-- HEAD=master
\
G - H - I <-- chgs
A fast forward merge is possible when the current commit (F
, in this case) is already an ancestor of the target commit (I
). If so, the branch label is peeled off the current commit and simply pasted onto the target commit:
... - E - F
\
G - H - I <-- chgs, HEAD=master
(There's no longer any ASCII-art-reason to keep the kink in the drawing [from F
down-and-right to G
], but I kept it for visual symmetry.)
As Cupcake noted in his faster answer, you can force a real merge with --no-ff
. I would draw this as:
... - E - F ------------- M <-- HEAD=master
\ /
G - H - I <-- chgs
In either case (fast forward, or "real merge"), once this is done, you can safely delete the branch label chgs
, as all of its commits are find-able by starting at master
and working backwards (along both branches if a "real merge"). Or, you can keep it and add more commits to it, if you prefer.
An interesting note here is that in this particular case the resulting work tree associated with commit M
is exactly the same as the one for commit I
. The key difference is that this creates an actual merge commit (a commit with multiple parents). The first parent is whatever commit was on the branch before—in this case, commit F
—and the remaining commits (just the one commit I
, here) are the branches being merged-in.
(You can override this—the work-tree being the same, I mean—with yet more merge flags, but in this case you would not want to. That sort of thing, overriding the work-tree, is mainly meant for "killing off" a branch while retaining the history: sort of a "look, we tried this and it didn't work" message to someone looking at the code next year.)
1Or don't need, if you don't want to move the labels. However, if you intend to push
your work back to someone, or let them pull
it from you, they will look at your labels. It's nice for them if you arrange your labels nicely for them. But it's always up to you.
Upvotes: 2
Reputation: 20620
The following explains in more detail what each of these commands is doing.
git checkout master
Switches to the master
branch.
Suppose locally we have this (where *
indicates the current branch):
M1 -- M2 = master*
\__ C1 = chgs
git pull --rebase
Updates local master
branch from the remote branch, rebasing any local changes that have been made to the master
branch so that they come after the remote changes.
Suppose this gives us a new M3 change:
M1 -- M2 -- M3 = master*
\__ C1 = chgs
git rebase master chgs
Rebases changes from the local chgs
branch so that they follow on from the local master
branch (but remain in their own branch). Also changes to the chgs
branch.
M1 -- M2 -- M3 = master
\__ C1' = chgs*
git checkout master
Switches back to the master
branch.
M1 -- M2 -- M3 = master*
\__ C1' = chgs
git merge chgs
Merges chgs
into master
.
M1 -- M2 -- M3 ------- M4 = master*
\__ C1' __/ = chgs
git push
Pushes these changes to the origin.
Upvotes: 1
Reputation:
Doesn't the third line,
git rebase master somechanges
, put commits fromchgs
after the last commit frommaster
but without merging them, and leavesHEAD
pointing to the latest commit fromchgs
, like this:(master-n-1) <- (master-n) <- (chgs-k-1) <- (chgs-k) <- HEAD
The effect of rebasing chgs
onto master
is equivalent to merging master
into chgs
, because the state of your code at the last commit will be equivalent to the state after a merge
.
Does that leave me with
chgs
checked out? That's why I need to check outmaster
again?
Yes.
And why do we need to merge the changes into master? Doesn't that make a graph like this:
(master-n-1) <- (master-n) <- (chgs-k-1) <- (chgs-k) \ (master-with-merged-chgs-k) <- HEAD
No. Because you rebased chgs
on top of master
, git will recognize that the last commit on chgs
effectively represents the state of master
"+" chgs
, so it just "fast-forwards" the master
branch to the tip of chgs
without making a merge commit:
(n-1) <- (n) <- (k-1) <- (k) <- HEAD/master/chgs
If you wanted to force git to do a merge-commit instead, you could pass the --no-ff
flag to merge
:
git merge --no-ff chgs
# Results in this
(n-1) <- (n) <- (k-1) <- (k) <- chgs
\ \
--------------(merge) <- HEAD/master
I don't see why the top fork is useful.
It's useful for updating feature branches with upstream changes from master
, without creating a bunch of redundant merge commits in the feature branch. The end result is that your development history becomes simpler and easier to work with, instead of containing a bunch of extra merge commits that you don't really need, and which just make the history more complicated and harder to manipulate.
Upvotes: 3