git - git checkout 真的复制提交吗?
问题描述
我有一个开发分支,这个分支有这些提交 X、Y、Z。当我从开发分支签出一个新功能(调用 FeatureX)时,FeatureX 中的提交仍然是 X、Y、Z 或被复制到 X'、Y ',Z'?请帮我!
解决方案
该git checkout
命令将文件从提交中复制出来。它必须这样做,因为提交中的文件不是可用的形式。但是在这种情况下,您提出了一个问题,其答案对您没有多大帮助。有一组更好的问题要问。
什么是 Git 存储库?
在大多数情况下,存储库是提交的集合。不过,存储库还有更多内容。例如,除了提交之外,每个存储库都有一个名称集合:分支名称、标签名称和其他此类名称。
数量更多取决于存储库的类型:
- 在服务器上发现的所谓的裸存储库大多停在这里。
- 您将使用的存储库类型添加了一个工作树,您将在其中进行实际工作,以及完成工作、从错误中恢复等所需的许多其他功能。
虽然这些很重要,但提交是这里的关键。分支名称也很重要,但这仅仅是因为它们可以帮助我们(和 Git)找到我们想要查找的提交。因此,作为 Git 新手,您需要了解什么是提交,以及为您做什么。
什么是提交?
提交是 Git 存储库中的主要存储单元。它具有以下特点:
每个提交都有编号。但是,这些数字并不是简单的计数:我们没有提交 #1,然后是提交 #2,然后是 #3 和 #4,依此类推。相反,每个数字都是唯一的,但看起来完全是随机的,而且拼写很有趣,就像一个大的十六进制数字。Git 调用这些哈希 ID。
每一次提交,一旦做出,就会永远完全冻结。
每个提交存储两件事:
它具有您所有文件的完整快照,包括您提交时文件中的任何内容。这是每个文件的一种存档,例如 tar 或 rar 存档。
显然这会占用大量的磁盘空间,因此 Git以一种特殊的、只读的、仅限 Git 的、压缩和去重的形式存储这些文件。这使得文件对除了 Git 本身之外的所有东西都无法使用(这就是为什么必须将文件复制出来,以将它们扩展为可用版本),但这意味着如果您进行一百万次提交,但继续重复使用您的文件,这几乎不占用任何空间——每个文件仍然只有一个副本,在所有百万次提交中重复使用。
git checkout
除了存储的文件之外,每个提交还存储一些元数据:关于提交本身的一些信息。例如,这包括提交人的姓名和电子邮件地址。它包括一个日期和时间戳,说明他们制作的时间。(实际上,每个提交都有两个,一个用于“作者”,一个用于“提交者”——它们通常是相同的,因此两者都没有意义,但它不会占用太多空间,所以没有人关心很多。)
还有更多可选功能,但我们现在并不真正需要关心这些。不过,我们在这里确实需要关心的一件事是元数据中的一个特定功能,这对 Git 本身至关重要:每个提交都存储一些先前提交的哈希 ID。 大多数提交只存储一个这样的哈希 ID;Git 调用这些普通提交。
提交形式链,就像一串珍珠
这意味着什么——这是你直接问题的关键——是每个提交“记住”它的父提交。我们可以直观地画出这一点,通过注意一些带有一些哈希 ID 的提交——我们就这么称呼它吧——将在那个提交中存储一些早期提交的H
哈希ID。hash
我们说后提交或子提交指向较早提交或父提交,我们可以将该指针绘制为从提交中出来的箭头:
<-H
如果我们调用较早的提交——使用前面G
的字母H
——并把它画进去,我们得到:
<-G <-H
当然,G
有自己的父级,F
,也有自己的父级,以此类推:
... <-F <-G <-H
这一直追溯到某人所做的第一次提交。 该提交不能向后指向,所以它不会:
A--B--C--...--G--H
CommitA
是第一个提交,是根提交,非空存储库必须至少具有其中一个。(Git 允许你制作多个,但是,好吧,我们不要去那里。)
这里我要说明两点:
我变得懒惰并停止在提交之间绘制箭头,只使用线条。但请记住,它们是单向箭头:Git 只能真正倒退。
虽然我在这里使用了简单的字母,很明显这
H
是最后一个,但在真实的存储库中,实际的哈希 ID 是这些可怕的、大而丑陋的随机十六进制数字,没有人能保持正确。
这第二点是一个大问题:我们如何H
轻松找到commit? 为了让Git做到这一点,我们必须告诉 Git commit 的实际哈希 ID H
。我们可能不得不把它写在某个地方,在纸上或白板上,然后输入。
但是,等一下,我们有一台电脑。计算机擅长这种记忆!让我们取散列 ID ,不管它是什么,然后在计算机的H
某个地方“写下来” 。让我们将它存储在文件、数据库或其他东西中。让我们使用一个易于记忆(对我们而言)的名称来保存哈希 ID。
这是分支名称进入图片的地方
如果我们像这样绘制我们的提交,您可以看到分支名称是如何工作的:
...--G--H <-- dev
分支名称——在我们这里的例子中是<code>dev——保存了序列中最后一次提交的真实哈希 ID 。那是提交。因此,通过并包括在内的提交是“在”分支上的。H
H
当您添加一个新的分支名称时,例如featureX
,我们只需让 Git 创建一个新文件,或在分支名称数据库中创建一个新条目,或其他任何内容,并在该名称中保存相同的哈希 ID:
...--G--H <-- dev, featureX
这意味着通过和包括的提交H
现在都在两个分支上。
如果您习惯了其他版本控制系统,您现在可能会反对:等等,等等,为什么它们都在两个分支上? 其他版本控制系统说提交只在一个分支上。Git 不是那些其他版本控制系统:在 Git 中,任何一次提交都可以在任意数量的分支上。
Git 提交的工作方式,我们使用分支名称来查找最后一个,然后从那里向后工作。所以 commitH
现在是两个分支上的最后一次提交。commit也H
指向更早的 commit G
——事实上,因为 commitH
永远不会改变,commitH
指向G
永远——所以ifH
位于两个分支上,G
也位于两个分支上。
特别的名字HEAD
这里还有一些事情要知道。尝试运行git status
。它的第一行是:
on branch dev
或者:
on branch featureX
管他呢。 Git 如何知道我们“在”哪个分支,这是什么意思?
我已经提到过——事实上有两次——git checkout
从提交中复制文件,在这个过程中解冻和取消 Git 化它们。这为我们提供了一组我们可以实际使用的文件,即与 / 一起工作。
该git checkout
命令还选择我们将“打开”的分支,正如git status
将要说的。
它这样做的方式是使用一个非常特殊的名称,HEAD
,它根本不是一个分支名称。(这个名字非常特别,如果它被损坏,Git 不再相信 Git 存储库是Git 存储库。它也是一种活动文件,所以如果你的计算机在存储库中工作时崩溃或断电,HEAD
文件有可能被损坏。幸运的是,通常很容易从这种情况中恢复。)Git 对这个名称所做的就是将它“附加”到一个分支名称,如下所示:
...--G--H <-- dev (HEAD), featureX
on branch dev
正如git status
将要说的,这意味着我们是。该git checkout
命令从 中提取所有文件H
,并附HEAD
加到名称dev
中。所以我们当前的分支是 nowdev
并且我们当前的提交是由 name 选择的dev
,即 commit H
。
如果我们运行git checkout featureX
,Git:
- 删除来自的文件
H
; - 移动
HEAD
以将其附加到featureX
; 和 - 放入来自
H
.
Git 实际上注意到第一步和最后一步是多余的,并且不会费心去做。所以唯一真正的变化是我们的绘图:
...--G--H <-- dev, featureX (HEAD)
但是,让我们看看当我们进行新的提交时会发生什么。
进行新的提交
我们将跳过有关提交的所有细节,包括为什么必须继续运行git add
。我们只需要注意,当你运行时git commit
,Git 实际上并没有使用你工作树中的文件——你可以看到和处理的文件——而是使用一些预先准备好的、预先去重的副本。这些是进入新提交的文件。
新的提交将获得一些看起来随机的(尽管是唯一的)哈希 ID,但我们称之为 commit I
。Git 将:
- 保存快照;
- 为元数据添加我们的姓名和电子邮件地址,以“现在”作为日期和时间;
- 将新提交的父级设置为当前哈希 ID
H
;和 - 写出提交,这是为新提交生成真实哈希 ID 的原因
I
。
如果我们只绘制提交本身,新提交I
将指向现有提交H
:
...--G--H
\
I
但是——分支名称呢? 这是真正偷偷摸摸的把戏。Git 现在将新提交的哈希 ID 写入附加的名称中HEAD
。由于我们的HEAD
曾经——现在仍然是——附加到 name featureX
,所以 namefeatureX
是被更新的:
...--G--H <-- dev
\
I <-- featureX (HEAD)
现在,通过H
的提交仍然在两个分支上,但新的提交I
只在分支上featureX
。
如果我们现在运行git checkout dev
,这就是我们的绘图中发生的情况:
...--G--H <-- dev (HEAD)
\
I <-- featureX
这一次,Git 删除了 commit 附带I
的文件并提取了commit 附带的文件H
。
git log
从这里向后工作
如果我们git log
现在运行,重新启动dev
,我们将根本看不到提交I
。原因很简单:git log
用来HEAD
查找分支名,然后使用分支名查找提交。这是将显示的第一个提交git log
:在这种情况下, commit H
。
然后,git log
将使用存储在commit中的父级移回较早的 commit 。Git 将向我们显示 的日志消息,然后使用其中的元数据移回提交。H
G
G
G
F
Git无法前进, from H
to I
:commitH
并不指向 commitI
,因为H
它是在之前I
存在的。Git 不知道I
将来会有什么哈希 ID(因为哈希 ID 是由提交的每个部分组成的,包括源快照和日期和时间戳)。
但是,如果我们运行git log featureX
, 或git checkout featureX
then git log
,Git将从提交开始I
并向后工作。这将向我们展示H
,G
也向我们展示F
。毕竟,这些提交都在两个分支上。
要记住什么
关于 Git,有很多东西要记住。这是一个很大的程序套件,有很多命令,还有很多奇特的能力等等。但是现在,你已经学会了:
- 存储库是关于提交的。提交保存快照和元数据,Git 通过看起来随机的哈希 ID 找到它们。
- Git可以通过名称查找提交:主要是分支名称,但也可以使用其他类型的名称,例如标签名称。这些名称存储一个哈希 ID。对于分支名称,根据定义,该哈希 ID 是分支中的最后一次提交。
- 一个提交向后链接到之前的提交——不是向前的,因为它不能;每次提交一旦完成,就会一直冻结。
- 分支名称本身会随着时间的推移而移动。分支名称移动的正常方式是向前一步,一次提交,在您进行新提交时。(还有其他 Git 命令可让您“快速”或“远”移动分支名称,并且以违反此处正常简单规则的方式。)
- 这种
git checkout
选择一个分支名称,切换到该分支,并从提交中获取文件——无论如何,该提交是该分支上的最后一个文件。 - 特殊名称
HEAD
,全部大写,是 Git 如何知道哪个分支名称是当前分支的方式。这反过来又确定了哪个提交是当前提交。
还有很多问题要问(例如:如果提交是快照,为什么git show
将其显示为更改?)但我会在这里停下来。
推荐阅读
- java - 从 Google 联系人迁移到 People API。为 Java 构建它的提示?
- php - 如何从另一个容器中使用 pdftk
- node.js - 如何获取所有 graphql 模式查询、变异、订阅
- amazon-web-services - 数据复制停止并且无法在 AWS Application Migration Service 中启动数据传输
- mongoose - Nestjs:如何使用猫鼬启动事务会话?
- java - 像在 ASP.NET 中一样,使用 JAXB 在多个命名空间中重用 Java 类
- git - 如何防止部分线路逆流
- python - Pandas 对具有来自其他列的条件的列上的选择数据块进行算术
- python - 尝试运行 hvplot 时,jupyter 实验室内核死亡
- c++ - 多维int全局数组的第一个元素的值在访问c ++时得到奇怪的数字