The last argument to git checkout -b bar foo
is really just a commit-ID. You used the branch name foo
, but git resolves this to a commit-ID. The effect is that git creates the new branch label bar
with that same commit-ID.
Let's say you have the following commit graph:
A <- B <- C <- D
\
E
(ASCII does not have a proper up-and-left arrow so imagine the \
as having an arrow-head pointing to C
, i.e., the parent commit of commit E
is C
). Each letter represents one unique SHA-1 ID corresponding to a commit. Commit D
has its (single) parent as commit C
, and so does commit E
, so the branches fork at commit C
.
Now let's paste on some branch labels as well. The branch named bra
contains the commit ID of commit A
, so it points to A
. The branch named brb
contains the commit ID of commit B
, so it points to B
. Branch brc
points to C
, and so on. (I can't really draw all of these properly in ASCII so we must simply imagine them.)
Let's make one more branch, say, master
, that also points at commit D
.
For super-concreteness, let's say the SHA-1 for D
is 5f95c9f850b19b368c43ae399cc831b17a26a5ac
.
Now if I do this:
git checkout -b new 5f95c9f850b19b368c43ae399cc831b17a26a5ac
which branch would you say branch new
is based on? The new branch label new
points to commit D
, just like the existing branch labels brd
and master
. So does new
originate from brd
, or master
, or ...?
You may have an answer to this, but git doesn't. Branch labels in git are ephemeral: they can be swept away and/or re-created or renamed at any time. (If I delete the label master
, so that only brd
points to D
, you could then say that branch new
is based on branch brd
. But what if I then rename brd
to bird
?)
The only things really permanent in git are the SHA-1 values, which depend entirely on the contents of the underlying object. Create the exact same object contents and you get the exact same SHA-1 hash. Alter a single bit anywhere in any object, and you get a new, different SHA-1.
Since commits record their parent commit(s) (multiple parents in the case of a merge), the graph constructed by starting at some reference and working backwards to a parent-less commit (a root commit) are permanent (provided that the starting-point remains in the repository, i.e., is never garbage-collected), but the labels are not. (Commits are garbage collected only when no no external reference—branch, tag, etc.—points to them and no other already-retained repository object1 points to them, so a single reference to a graph-tip like E
suffices to retain the entire chain of commits. But if you delete that reference, the chain becomes vulnerable.)
1These "other repository objects" are necessarily either other commits, or annotated tag objects, as those are the only two that are supposed to contain the SHA-1 of a commit. Annotated tag objects themselves should be pointed-to—referenced—by a corresponding lightweight tag, or possibly another annotated tag.
Incidentally, git branch
can help you out if you want to ask a different question instead. Let's say you have a commit graph that looks more like this:
...-o-o-o-o-o-A <-- master
\ /
o-o-B <-- feature1
\
C <-- feature2
where each o
node represents an "uninteresting" (and un-labeled) commit. The labeled branch-tip commits here (A
, B
, and C
) are the branch-tips of master
, feature1
, and feature2
. (It's likely that feature2
was created by branching off of feature1
, and feature1
was created by branching off master
, but usually this is not particularly interesting either.)
What you often want to know, if you are to work on branch master
now, is: "what branches are merged in, and what branches are not merged?" The git branch
command can answer this:
git branch --merged master
and:
git branch --no-merged master
Both commands ask git branch
to start at the commit identified by master
(i.e., commit A
) and "work backwards" along the commit graph.
With --merged
, it should print the names of branches that, when working backwards from master
, the commit identified by those branches is reached. So we start at A
and step back to the right-most o
. This is a merge commit (with two parents), so we then step back along both parents, finding the next o
commit and commit B
. Commit B
is pointed-to by branch name feature1
. It is reached from master
, so git branch
prints feature1
. This is a branch that "is merged into" branch master
.
With --no-merged
, the git branch
command should print those branches whose commits are not reached. As before, we start at A
and work backwards, finding the right-most o
that is a merge. We work backwards along both parents, finding another o
and B
; continuing back we found two more o
s, another two o
s, and then rejoin the left-most o
and work back through the ...
section. At no point do we reach commit C
—so git branch
prints feature2
: this is a branch that is not "merged into" branch master
.
There's one more trick up git branch
's sleeve (if it has any sleeves in the first place :-) ). We can ask for git branch --contains
and give it a particular commit-ID, or anything that resolves to a commit-ID. Let's say we give it the ID of commit B
.
This time, with --contains
, the git branch
command starts at every branch-tip and begins working backwards. If it can reach the commit ID we gave it, it prints the branch-name. As before, when we start from commit A
(master
), we reach commit B
. When we start from commit B
(feature1
), we of course reach commit B
immediately; and when we start from commit C
(feature2
), we reach commit B
. So git branch --contains feature1
prints all three branch-names.
(If we were to ask git branch
which branches contain the right-most o
commit, only master
would contain it. The same would be true of most of the top-line o
commits, except for the left-most one; branches feature1
and feature2
also contain that commit, and any earlier ones in the ...
section.)