Let's say if I did a
git checkout -b bar foo
or
git checkout foo
git branch bar
both should have created a branch bar
base on the branch foo
. But let's say the next day, if I want to verify that bar
really was created based on foo
, there is no easy way to do that in git, or is there?
Using the app SourceTree, it seems it is possible and easy to tell, that if bar
and foo
are displayed on the same row, that means they are the same at that stage, and if I commit anything in bar
, it will show that bar
is "extended" from foo
in the tree graph, so I can easily tell bar
was based on foo
. Some other questions I found in SO seem to suggest there is no easy way to tell, but I don't understand why. At least when it is possible to tell, shouldn't there be a command that tells it, such as something like git branch --show-base-on
or something like that, and when not possible to tell, then display "cannot tell because [some reason]"?
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.)
The reason that you cannot do this is that branches do not have a strong identity in Git. They are just lightweight "labels" that can be moved over time to indicate the current position of "something".
At any time in the future someone could create a new branch based of the point where you created your new branch, delete, rename or move the origin branch that the new branch was created off and make the information that you want to keep impossible to infer.
Git has no need to keep any information about the previous location of branch pointers, it only cares about the history of the actual commits in any given branch so if you have want branches to have a stronger identity than Git itself ascribes to them then you would need to store this information separately, say in a metadata branch or perhaps externally to Git.
One way would be to check if the commit represented by the foo HEAD is contained in bar branch, but since foo HEAD will move over time... that won't be valid over time.
See "Find the parent branch of a branch" to understand how hard ti would be to find an information (parent branch) which is essentially not memorized by Git at all.
As seen here, you could rephrase that as "What is the nearest commit that resides on a branch other than the current branch, and which branch is that?"
But again, that result can change over time, as branches can be removed, renamed, rebased.