已经有一些很好的答案,但您真正可以在这里使用的是图形说明。
理解分支和标签如何在 Git 中工作的关键是要认识到分支和标签 names 仅仅是辅助工具。他们几乎没有真正制作分支。 “分支”这个词本身在 Git 中实际上是模棱两可的。要查看有关此内容和几个图表的更多信息,请单击 What exactly do we mean by "branch"? 这描述了分支标签如何选择特定提交,尽管不是分支增长的过程。 (另请注意,Pro Git 书中的第一个图像很好,但和 Jubobs 一样,我不喜欢第二个 Pro Git 图像。)
我在 StackOverflow 帖子中使用了一种更简单的图表方法。以这个只有三个提交的存储库示例图为例。这三个提交中的每一个都有一个实际的哈希 ID——其中一个是那些缩写为 badf00d 和 cafedad 等等的丑陋的 40 个字符的东西——但我只给它们一个字母的名称,并将分支名称放在上面右边:
A <- B <- C <-- master
这里C 是我们最新的提交,在分支master。分支 name master 包含提交 C 的实际哈希 ID,例如 ac0ffee4cafedad5badf00d... 或其他。提交C 本身——实际的提交,存储在Git 的数据库中——有提交B 的ID。我们说master 指向 C,而C 指向B。 B 的 ID 为 A,因此它指向 A。 A 是任何人第一次提交,所以它不能指向任何地方。用 Git 的话来说,它没有 parent;这是一个根提交。
请注意,在这个系统中,父提交不知道他们有哪些孩子,但孩子确实知道他们的父母。为了在master 上进行新的提交,Git 将一些内容写入数据库,以新的提交对象结束。新的提交——我们称之为D——包含当前提交的 ID 作为其父提交,因此D 指向C。然后 Git 更新分支 name,master,使其指向D:
A <- B <- C <- D
(正如最近有人指出的那样,这很像挖掘家谱记录:你会发现“鲍勃琼斯,父母是亚瑟和莎莉”。显然鲍勃的出生记录不能列出 25 年后他会有一个女儿,所以它没有。同样,当你提交时,Git 知道提交的 parent 是谁,但它不知道提交是否会有孩子。)
考虑到这一点,请考虑这个图形片段。我已经停止绘制内部箭头以节省空间等,但请记住,它们总是指向 向后(在这些图中向左):
...--D--E--F <-- master
\
G--H <-- sidebr
我们通过在master 上进行两次新提交,即E 和F,以及通过创建一个新分支名称,sidebr,指向提交@ 到达这里987654352@。然后我们git checkout sidebr 进行两次新的提交。它们是G 和H。当我们制作G 时,提交D 是当前的,并且是sidebr 指向的内容。因此,Git 以D 作为其父级写入提交,并将sidebr 更改为指向G。现在 G 是最新的,我们以 G 作为其父提交新的提交 H,并更新 sidebr 以指向 H,这就是我们得到这个图表的方式。
标签时间
现在是为这些图片添加标签的时候了。标签几乎(但不完全)与分支相同。与分支名称一样,标签名称 指向 提交。主要区别1 是分支名称应该 移动:它们通过添加新提交自动增长。标签名称不应该移动。
因此,我们不妨将它们绘制在提交图的“内部”,而不是在右侧。 (或者,如果我们有颜色,我们可以为分支和标签名称使用不同的颜色——但我不能在 StackOverflow 上的文本中使用颜色。)所以让我们添加一个指向提交 E 的标签名称:
tag:v0.1
|
v
...--D--E--F <-- master
\
G--H <-- sidebr
这个标签指向提交E。它应该永远永远指向提交E。2 关于原始 Git 哈希 ID 有一个有趣的事实,在这里非常相关:哈希 ID 是完全确定的,并且完全基于提交的实际内容(或其他 Git 对象)。因此,标签名称只不过是为那些糟糕的哈希 ID 之一提供了一个人类可读的名称。如果我们都能记住17f9f635c101aef03874e1de1d8d0322187494b3,我们就不需要Git 存储库中的标签v2.6.0——但我肯定不会记住这一点。3支持>
在任何情况下,您都可以git checkout 任何通过其 ID 或任何解析为其 ID 的提交。标记名称适用于后者,当然更容易记住。因此,鉴于上图,我们可以通过以下方式检查提交 E:
git checkout v0.1
(tag: 不是标签名称的一部分,只是我写的东西来说明为什么我们在这里有一个箭头)。然而,这给了我们一个“分离的 HEAD”,这就是为什么我们 git checkout -b newbranch v0.1: 分配一个新的分支 name 来指向提交。现在我们需要重新绘制图表以提供更多空间:
tag:v0.1
|
| F <-- master
v /
...--D--E <-- newbranch (HEAD)
\
G--H <-- sidebr
我还添加了这个HEAD 的东西:这提醒我们现在在这个新分支。如果我们现在进行新的提交,这将按照通常的方式增长分支:
tag:v0.1
|
| F <-- master
v /
...--D--E--I <-- newbranch (HEAD)
\
G--H <-- sidebr
不应该移动的标签不会移动。 分支,然而,确实移动。我们可以进行任何我们喜欢的提交,并且每个提交都使当前分支(newbranch)提前合并我们的每个新提交。
1另一个重要的区别是标签名称存在于所有存储库的共享名称空间中,而分支则不存在。还有带注释的标签,让您有机会附加一些未解释的数据,Git 允许您使用 GPG 加密签署此类标签。但这是另一个讨论。
2可以强制移动标签,或者删除它并重新创建它指向不同的提交。有时甚至有充分的理由这样做。您只需要确定原因特别好,因为标签实际上只是哈希 ID 的人类可读名称,并且任何已经具有旧标签的存储库都可能认为旧标签到哈希- ID 映射仍然正确,即使您已强制移动标签。
3我使用git rev-parse v2.6.0 找到它。上面的哈希 ID 是带注释的标记对象的 ID(您可以通过克隆 Git 的 Git 存储库来找到它)。实际提交是be08dee9738eaaa0423885ed189c2b6ad8368cf0。有一种特殊的语法可以一步找到commit ID,例如使用git rev-parse,也可以使用git show v2.6.0读取标签对象,然后git show标签的目标对象查看提交。