让我们先快速回顾一下您已经知道的事情:
任何 Git 存储库中的基本存储单元都是commit。提交中有更小的单元——例如,提交包含文件,有点类似于原子如何保存质子、中子和电子——但提交本身就是你应该使用的容器。(在这个类比中,我们想做化学,而不是核物理。)
每个提交都有自己唯一的哈希 ID。这些哈希 ID 又大又丑,很难让人使用,所以 Git 有时会缩短它们以显示:例如,548af67
(它是更长的东西的缩写),be89a73
(又是 40 个字符长的东西的缩写) , 等等。我从你的git log --all --decorate --oneline --graph
输出中得到了这些。各地的每个 Git 存储库都同意这些哈希 ID 是为这些特定提交保留的,即使该存储库没有这些提交也是如此。
您始终可以使用原始哈希 ID 来引用您在自己的存储库中的任何提交。
提交本身包含:
数据:所有文件的快照。这与之前的提交没有区别。它是每个文件的完整副本。(这些文件以压缩、冻结、只读、Git-only 格式存储。因为它们是只读的,所以可以共享。例如,如果您的大多数提交都有一个README
文件,并且只有三个版本在README
90 次提交中,每 30 次提交更改一次,然后一个内部 Git 格式的 README 冻结副本服务于前 30 次提交,另一台服务器服务于接下来的 30 次,依此类推。)
元数据:有关提交的信息,例如提交人(姓名和电子邮件地址)、提交时间(日期和时间戳)以及提交原因(提交日志消息)。在此元数据中,每个提交都可以列出一些先前提交的原始哈希 ID。
大多数提交准确地列出了一个先前的提交哈希 ID。列出的提交是这个提交的父提交,即在这个提交之前的提交。一些提交列出了多个先前的提交,即,有多个父级。每个非空存储库中的一次提交是该存储库中的第一次提交,因此不列出任何父级。
每当 Git 有权访问一个提交时,Git 都可以查看该提交的父级(或父级),因此可以向后工作到前一个提交。这使 Git 可以访问父提交。所以现在 Git 可以找到另一个父级——这个父级的父级,即我们刚才的提交的祖父级——当然那个提交有一个父级。因此,Git只需从最后一次提交开始并向后工作,就可以找到整个历史记录。
分支名称找到特定的提交
但是提交哈希 ID看起来是随机的,并且是不可预测的。您和 Git 如何快速轻松地知道哪个提交是最后一个?这就是分支名称的来源。分支名称类似于master
或detached-head-after-gitlab-crush
存储一个提交哈希 ID。根据定义,该哈希 ID 是该分支中的最后一次提交。
让我们使用大写字母来代表实际的提交哈希 ID。我们会很快用完,这是 Git 不使用简单大写字母的原因之一,但它对我们的绘图来说是可以的。假设我们的存储库非常新,并且只有三个提交。第一个是 commit A
,因为它是第一个,它没有父级:
A
我们将调用第二次提交B
。它将第一次提交的哈希 ID 记住为其父级。所以我们会说 commitB
指向commit A
,然后画成这样:
A <-B
当然, commitC
包含 commit 的哈希 ID B
,因此C
向后指向B
:
A <-B <-C
为了C
快速查找,Git 将其哈希 ID 存储在name master
中:
A--B--C <-- master
(此时我们有点累了,变得懒惰了,将 commit 到 commit 的连接画成线,而不是箭头。请记住,它们仍然是箭头,它们来自 child 并指向 parent,永远不会从父级到子级。 每次提交的所有部分都被永久冻结,包括从其中出来的箭头,所以我们不能稍后返回并添加一个向前指向的箭头:我们进行了提交,它有给它的父母一两个向后的箭头,从那时起我们就被困住了。孩子们知道他们的父母是谁,但父母永远不知道他们的孩子是谁。)
现在我们有了这个,让我们在这张图片中添加另一个分支名称。而不是拼写crash
,crush
我将称之为develop
:
A--B--C <-- master, develop
现在让我们向我们的集合添加一个新的提交。我们使用 Git 中的常规流程来实现这一点。D
无论 Git 提供什么哈希 ID,我们都将调用新的 commit 。新提交D
将指向现有提交,因为我们将通过签出 commitC
开始工作。因此,一旦制作完成,它将如下所示: D
C
D
A--B--C
\
D
D
向上和向左指向C
,C
指向B
,等等。
这是HEAD
进来的地方
我们现在有一个问题。我们有两个分支名称。 哪一个应该记住新的提交D
?
为了告诉 Git 是哪一个,我们会将特殊名称(全部大写)附加到两个现有分支名称之一。HEAD
假设我们在进行新提交之前有这样的安排D
:
A--B--C <-- master (HEAD), develop
然后我们会得到这个:
A--B--C <-- develop
\
D <-- master (HEAD)
但如果这不是我们想要的,我们应该git checkout develop
首先。然后我们将有:
A--B--C <-- master, develop (HEAD)
当我们进行新的提交时D
,我们会得到:
A--B--C <-- master
\
D <-- develop (HEAD)
无论哪种方式,我们都会得到相同的提交集。不同之处在于,当 Git进行新提交时,它将新提交的哈希 ID 写入该名称HEAD
附加到的任何分支名称。然后该分支名称会自动指向新的提交。
事实上,新提交的父HEAD
提交是之前指向的提交的分支名称。根据定义,这就是我们的提交。我们使用了git checkout master
or git checkout develop
,但无论哪种方式,我们都选择了现有的 commit C
。
未附加到分支名称时会发生分离的HEADHEAD
现在我们有了:
A--B--C <-- master
\
D <-- develop (HEAD)
我们可以继续进行更多新的提交:
A--B--C <-- master
\
D--E--F <-- develop (HEAD)
例如。但如果我们愿意,我们可以把我们的头拿掉。Git 有一种模式,我们可以直接HEAD
指向任何现有的提交。例如,假设出于某种原因,我们想直接提出我们的观点 commit :HEAD
E
A--B--C <-- master
\
D--E <-- HEAD
\
F <-- develop
我们现在可以进行一个新的提交——我们称之为它G
——它将指向现有的提交E
。Git 会将新提交的哈希 ID(无论它可能是什么)写入分离的 HEAD 中,从而为我们提供:
A--B--C <-- master
\
D--E--G <-- HEAD
\
F <-- develop
这种模式本质上没有任何问题,但它使以后的事情变得更加困难。假设我们想C
再次查看提交。我们可能会跑git checkout master
。这会将HEAD
名称master
再次附加到名称:
A--B--C <-- master (HEAD)
\
D--E--G
\
F <-- develop
你将如何找到commit G
?我们可以C
很容易地找到:这是我们当前的提交和名称HEAD
,并且master
都找到了它。我们可以B
通过C
返回一个找到。我们无法D
从中找到C
,但我们可以F
从名称 develop
中找到。从F
,我们可以后退到E
,从那里到D
。但是我们不能前进。Git 的所有箭头都指向后面。不再有一种简单的方法可以找到 commit G
。
解决方案是在我们从G
. 这就是您之前在创建 name 时所做的detached-head-after-gitlab-crush
。如果我们知道G
(例如,如果它仍在屏幕上)的哈希 ID,我们可以用另一种方式做同样的事情:
git branch save-it <hash-of-G>
会成功的:
A--B--C <-- master (HEAD)
\
D--E--G <-- save-it
\
F <-- develop
现在我们可以使用 commitC
一段时间,甚至可以进行一个新的提交H
,使master
更改指向H
:
A--B--C--H <-- master (HEAD)
\
D--E--G <-- save-it
\
F <-- develop
我们所要做的就是返回G
,git checkout save-it
它附加HEAD
到名称save-it
(仍然指向G
):
A--B--C--H <-- master
\
D--E--G <-- save-it (HEAD)
\
F <-- develop
你需要做的是找出为什么你总是让你的 HEAD 脱离
虽然 Git 中的分离 HEAD 模式根本没有问题,但它很难使用。您必须手动创建和/或更新分支名称以记住您的提交。
每当您告诉 Git 时,Git 都会进入此分离的 HEAD 模式:
git checkout --detach master
例如说“我想使用由 标识的提交master
,但我想在分离的 HEAD 模式下执行此操作”。
每当您要求它通过原始哈希 ID 或任何非分支名称的名称签出(或切换到,使用新的 Git 2.23 及更高版本)提交时, Git 也会分离HEAD。这包括远程跟踪名称(如)和标签名称(如果您已创建标签)。git switch
origin/master
v1.2
某些命令,包括特别是git rebase
,将在运行时暂时分离 HEAD。如果他们无法完成,以至于您处于 rebase 的中间,他们将停止并让您处于这种分离的 HEAD 模式。然后,您必须选择是完成变基还是使用 完全终止它git rebase --abort
。(如果你不想做其中任何一个,你会有点卡住:你真的必须做其中一个。)
所以:找出你为什么一直进入这种分离的 HEAD 模式。你在做什么导致它?当你处于分离HEAD模式时git branch
,或者如果你不需要记住你在哪里现在——如果你故意查看一个历史提交,你可以并且可能确实找到了使用,例如,只需使用或将你的 HEAD 重新附加到现有的分支名称。但是除了那些你确实想要一个分离的 HEAD(使用标记的提交或查看历史提交)的特殊情况,或者在你完成之前处于分离的 HEAD 模式的基础上工作的情况,你可能不'吨git checkout -b
git switch -c
c
git log
git checkout
git switch
想在分离的 HEAD 模式下工作。所以,不要那样做!