When using Git, how to tell which branch a new bra

2020-07-27 04:43发布

问题:

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]"?

回答1:

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 os, another two os, 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.)



回答2:

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.



回答3:

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.



标签: git