Question

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.

Was it helpful?

Solution 2

In order:

Doesn't the third line, git rebase master somechanges, put commits from chgs after the last commit from master 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 from chgs

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.

OTHER TIPS

# 1

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

# 2

Does that leave me with chgs checked out? That's why I need to check out master again?

Yes.

# 3

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

# 4

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.

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top