重要的是要意识到在 Git 中,分支并不是历史。 Commits 是历史。 (而且没有文件历史记录——只有提交。)请注意,每个提交都有一个“真实名称”,即其哈希 ID,即由 40 个十六进制字符组成的大而丑陋的字符串,例如 f84b9b09d40408cf91bbc500d9f190a7866c3e0f。
每次提交都会存储您的源代码树的完整快照,以及一些元数据。在这里,元数据是更有趣的部分。提交中的元数据包括:
- 您的姓名和电子邮件地址以及时间戳;
- 您的日志消息;和
- 其前任或父提交的哈希ID。
在这个特定的上下文中,最后一项是最重要的。如果我们用一个大写字母代替丑陋的大哈希 ID,我们可以画出这样的提交:
... <-F <-G <-H
H 是 last 提交。提交H 记住G 的哈希ID,所以我们说H 指向 G。同时G 记得它的父F,所以G 指向F,它指向E,依此类推。
这是存储库中的历史。 new 提交包括让 Git 冻结你的源代码树,添加日志消息等,并提出一个新的哈希 ID I。新的提交 I 将存储 H 的哈希 ID:
... <-H <-I
现在历史是一个更长的提交。
但是:我们如何知道哪个提交是链中的最后一个?在这个简化的例子中,大写字母很明显,但是对于看起来随机的真实哈希 ID,它不是。实际上,除了蛮力“列出每个提交,看看哪个是最后一个”之外,别无他法。所以 Git 给了我们分支名称,这有助于弱小的人类(以及 Git 本身,因为它也不是那么聪明)find last 提交:
...--F--G--H <-- master
变成:
...--G--H--I <-- master
在我们做出新的提交之后。
尽可能简单地说,branch name 只是指向(包含哈希 ID)last 提交,我们——和 Git——应该算作其中的一部分那个分支。
这意味着当你创建一个新的分支时,你会选择一些现有的提交并告诉 Git 为其添加另一个名称:
...--F--G--H <-- master, branch1
现在 Git 需要一种方法来了解使用哪个分支名称,因此我们让 Git 将特殊名称 HEAD 附加到两者之一:
...--F--G--H <-- master, branch1 (HEAD)
当我们进行新的提交时,Git 会更新 HEAD 所附加的名称,如下所示:
...--F--G--H <-- master
\
I <-- branch1 (HEAD)
注意,提交H,它是master的提示,也是在分支branch1上。
如果您创建另一个名称,只需将其绘制在它指向的任何位置。如果它指向H,则将其绘制为指向H。如果它指向I,如branch1,则将其绘制为指向I。然后添加更多提交,更新 HEAD 所附加的名称:
J--K <-- branch2 (HEAD)
/
...--F--G--H <-- master
\
I <-- branch1
或:
...--F--G--H <-- master
\
I <-- branch1
\
J--K <-- branch2 (HEAD)
或其他。
假设此时我们有:
...--F--G--H <-- master
\
I--L--M--N <-- branch1 (HEAD)
\
J--K <-- branch2
删除一个分支(名字)只是意味着删除名字。 commits 保持不受干扰——一旦提交,没有任何东西可以改变它。如果您无法再找到图表中的提交,则该提交现在很容易被删除。但是,如果您可以从某个现有名称开始,然后返回到提交,则提交本身是引用安全的。所以如果我们git checkout master(这样我们就可以删除branch1),我们会得到:
...--F--G--H <-- master (HEAD)
\
I--L--M--N [abandoned]
\
J--K <-- branch2
提交I 仍然可达,从K 开始,从branch2 向后工作:提交I 在branch1 和上>branch2。现在branch1 消失了,I 只在branch2 上,但它仍然可以通过 Git 的“从所有分支名称向后走”技巧到达,
但是,提交 L-M-N 已不受保护。经过一段时间后——通常有至少 30 天的宽限期,通过 Git 调用 reflogs 的机制——Git 的“垃圾收集器”最终将运行并清理 无法访问的对象(提交和其他对象)。这些提交最终将完全消失,只剩下:
...--F--G--H <-- master (HEAD)
\
I
\
J--K <-- branch2
这使它看起来像 branch1 从未存在过。