2

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?

randian
  • 73
  • 1
  • 6
  • 1
    You can find this out yourself by using `git merge --ff-only`. If a fast-forward is possible it will do it, otherwise it will create an error message. – mkrieger1 Jul 21 '20 at 20:31
  • 1
    @randian don't take it personally, but "Can I do..." without telling anyone what you've already tried doing, is one type of a question that it does not make much sense asking in pages like SO ;) See [some bad and good examples](http://www.catb.org/esr/faqs/smart-questions.html#examples). – natka_m Jul 21 '20 at 20:41
  • @mkrieger1 So the merge attempt automatically includes all branches between D and master, not just the changes between D and its immediate ancestor? – randian Jul 21 '20 at 20:41
  • That depends on the exact command you are using I think. Did you try anything? – mkrieger1 Jul 21 '20 at 21:01

1 Answers1

5

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.


Branch names find last commits

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.

Fast-forwards aren't just from merge

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.

Conclusion

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.

torek
  • 448,244
  • 59
  • 642
  • 775