If I have a chain of branches
master -> branchA -> branchB -> branchC -> branchD
is it possible to fast-forward merge branch D, and all of the branches in the chain between it and master (A, B, and C), into master in a single git command?
If I have a chain of branches
master -> branchA -> branchB -> branchC -> branchD
is it possible to fast-forward merge branch D, and all of the branches in the chain between it and master (A, B, and C), into master in a single git command?
The answer is a sort of provisional yes.
The trick here is to realize that Git doesn't really care about branches. Git really cares about commits. When you talk about a "chain of branches", you're talking about a sort of optical illusion: you are seeing something Git doesn't.
To be properly precise here, we must define the term branch. When we try, we find that this is a big problem. Different people use the word differently, and one person will even use the word in more than one way, perhaps in the same sentence. (See What exactly do we mean by "branch"?) But if we consciously step back a bit, trying to avoid the word "branch", and look at the way commits work, it all begins to make sense.
Each commit has a number. The numbers aren't nice 1, 2, 3, style sequential numbers, though: they're random-looking hash IDs, like ae46588be0cd730430dded4491246dfb4eac5557
. (They're distinctly not random: they are checksums of the contents of each commit, so that every Git everywhere will compute the same hash ID for the same commit-bits. That's the key magic in Git, really.) There's no easy way to know which commit is which from the IDs.
So, what Git does is this: whenever you make a new commit, you make it from some existing commit.1 The existing commit is therefore the parent of the new commit. Git stores, in the commit's metadata, the hash ID of the parent of the new commit. This means that we can always start from the last commit and work backwards:
... <-F <-G <-H
If H
stands in for the ID of the last commit, then H
must contain, as its parent hash ID, the actual hash ID of commit G
. So if we can find H
and read it, that gives us the ID of G
. We use the ID of G
to find G
and read it, and that contains the ID of F
, and so on.
These backwards-pointing chains of commits are what are stored in the main Git database (along with the contents of each commit, also stored via hash IDs). But that still leaves us with the puzzle of how we find the last commit.
The very first commit someone makes in a new repository does not fit this pattern, but the solution to that dilemma is obvious: just don't give it a parent at all.
Git's answer to this puzzle is remarkably simple. Each branch name holds one (1) hash ID. That hash ID is, by definition, the last commit in the branch.
So, suppose we have:
A--B--C--D--E--F--G--H--I--J--K--L
as commits all in a line, and suppose that the name master
points to commit C
, the name feature
points to commit F
, the name hack
points to commit H
, and the name last
points to commit L
:
__feature
.
A--B--C--D--E--F--G--H--I--J--K--L <-- last
. .
---master --hack
Or we can draw it this way, perhaps more clearly, perhaps less clearly:
A--B--C <-- master
\
D--E--F <-- feature
\
G--H <-- hack
\
I--J--K--L <-- last
Note that no commit, once made, can ever be changed. These commits will always continue to point backwards like this: L
to K
, K
to J
, J
to I
, I
to H
, and so on. We can make other new commits, perhaps using the same files and messages and so on, that point to other different commits, but they will have different hash IDs.
Now, the thing about branch names is that they move. For L
to be the last commit on branch last
, if we make a new commit on L
, the new commit must point back to L
. But if we git checkout master
—which says to select commit C
—and make a new commit there, that new commit must point back to existing commit C
:
A--B--C--NEW <-- master (HEAD)
\
D--E--F <-- feature
\
G--H <-- hack
\
I--J--K--L <-- last
(Checking out a commit by using a branch name attaches the special name HEAD
to the branch name, which is why I've added HEAD
above.)
Going back to your original question, we now need to define the term fast-forward. A fast-forward operation, in Git, means that we have some existing chain of commits, with a name—any name will do though branch names are the usual kind of name in question here—pointing to one of them, but then some more commits "after" that name, like this:
V <-- option1
/
...--T--U <-- name
\
W--X--Y--Z <-- option2
To achieve the fast-forward operation, we tell Git: move the name so that it points to one of the later commits, without forcing the name to move backwards. So here, we can tell Git to move name
forward to V
, or forward to any of W-X-Y-Z
, and that's a fast-forward. Let's pick V
:
V <-- name, option1
/
...--T--U
\
W--X--Y--Z <-- option2
Now that we've done that, though, if we ask Git to move name
so that it points to any of W
, X
, Y
, or Z
, Git will have to first "move backwards" from V
to U
, before moving forwards again. That means the operation is not a fast-forward.
While the git merge
command can perform a fast-forward—and will do so automatically when it can—it's not the only part of Git that looks for fast-forward operations. In particular, both git fetch
and git push
can adjust names. The names git fetch
adjusts are normally remote-tracking names rather than branch names, but the names that you use with git push
usually tell some other Git repository to adjust its branch names, and those too will first check for fast-forward-ness.
In the end, it all comes down to your commit chains, and what exactly you mean by "branch". If you can draw your branches the first way we did with master
, feature
, hack
, and last
above, you can then ask Git to move the name master
forward:
A--B--C <-- master (HEAD)
\
D--E--F <-- feature
\
G--H <-- hack
\
I--J--K--L <-- last
by running git merge --ff-only last
or git merge --ff-only hash-of-L
. The --ff-only
option tells git merge
that if the operation isn't a fast-forward, this should fail. Note that:
A--B--C--D--NEW <-- master
\
E--...--L <-- last
would indeed fail. But --ff-only
isn't perfect here. Consider, for instance,
what happens if we have:
A--B--C <-- master (HEAD)
\
D--E--F--NEW <-- feature
\
G--H <-- hack
\
I--J--K--L <-- last
You can fast-forward master
to point to commit L
, giving:
A--B--C--D--E--F--NEW <-- feature
\
G--H <-- hack
\
I--J--K--L <-- last, master (HEAD)
But feature
remains one commit "ahead of" master
, as commit NEW
—which points back to F
—is only available by starting at feature
and working backwards.