Git 变基与结帐



已经有一段时间了,每当我想查看分支(远程/本地)中的新更改时,我都会对该分支进行checkout,但最近我遇到了rebase似乎是为此目的而创建的命令,我想知道这两种方法之间的区别。有人可以用简单的方式解释一下吗?

git checkout <branch_name>
git rebase <branch_name>

变基和签出是截然不同的命令,具有不同的目标。 这两个目标都不完全符合你自己的目标——这是或似乎是检查某些东西——但结账更接近。

有人可以解释一下Eli5吗?

恐怕我为此超过了词汇限制,但让我们从正确的基础知识开始,太多的 Git 用户跳过了这些基础知识(出于好或坏的原因,但最终结果很糟糕)。

Git 是关于提交的

Git 中的基本存储单位是提交。 Git存储库是提交的集合,存储在 Git 称为对象数据库的大型数据库中。 Git 存储库还有几个部分,我们稍后会谈到,但第一个部分(对象数据库)是必不可少的:没有它就没有存储库

对象数据库是一个简单的键值存储,使用 Git 调用的 OID或对象 ID 来查找对象。 对于我们的目的来说,最重要的对象类型(事实上,我们唯一真正关心的对象)是提交对象,它包含任何提交的第一部分。 所以我们的提交,在 Git 中,有这些 OID。 我们将它们称为哈希 ID,以避免陷入太多的 TLA(三个字母首字母缩略词)中,最终可能出现 RAS 综合征。 有些人称它们为 SHA 或 SHA-1,因为 Git 最初(和目前)使用 SHA-1 加密哈希作为其哈希 ID,但 Git 不再与 SHA-1 绑定,因此"哈希 ID"或"OID"更合适。

OID 或哈希 ID 是一个由字母和数字组成的大丑陋字符串,例如e54793a95afeea1e10de1e5ad7eab914e7416250。 这实际上是一个非常大的数字,以十六进制表示。 Git需要这些来查找其对象。 该 ID 对于该特定对象是唯一的:在大对象数据库中,任何其他对象都不能具有该 ID。 你所做的每一个提交都必须获得一个新的随机数字,从未使用过,永远不会在任何Git存储库中再次使用,除非它被用来存储你的提交。 使它真正起作用是很困难的 - 从技术上讲,这是不可能的1- 但哈希 ID 的庞大大小使其在实践中起作用。Git 世界末日可能会在某一天到来(参见新发现的 SHA-1 碰撞如何影响 Git?),但暂时不会

到来。

1参见鸽子洞原理。


Git 与分支或文件无关

如果 Git 提交不存储文件,Git 将毫无用处。 所以提交存储文件。 但是提交本身不是文件,文件也不是 Git 的"工作单元"。 Git 是关于提交的,它有点意外地故意包含文件。

分支这个词,在 Git 中,被严重过度使用,几乎到了毫无意义的地步。阿拉伯数字当人们在这里说分支时,至少有两三件事的意思,这可能会变得非常混乱,尽管一旦你掌握了基础知识,你会发现自己在所有其他人中随便把分支这个词扔在一个句子中,也许不止一次在同一句话中,每个词的含义都不同, 然而,整个事情似乎完全显而易见。

为了帮助保持直截了当,我喜欢(至少尝试)在引用mainmasterdevdevelopfeature等名称时使用短语分支名称。 在 Git 中,分支名称查找特定提交的快速且重要的方法。 人类使用这些是因为人类的大脑不擅长处理哈希ID:它们太大,丑陋,看起来很随机。

因此,存储库保留一个单独的数据库 - 另一个简单的键值存储 - 其中每个键都是一个名称,值是与该名称一起出现的丑陋的大哈希ID。 分支名称是 Git 在第二个数据库中粘贴的多种名称之一。 因此,您可以为 Git 指定一个分支名称;Git 将查找哈希 ID,并找到该分支的最新提交

从这个意义上说,我们在 Git 中使用分支(或者更准确地说,分支名称)来获取我们的提交。 但 Git 真的不是关于这些分支;它仍然是关于提交


2有关此问题的更极端示例,请参阅 Buffalo buffalo buffalo 有关 Git 滥用单词 branch的更多信息,请参阅"分支"到底是什么意思?


提交中的内容

现在我们知道 Git 是关于提交的,让我们来看看一个实际的原始提交。 这是我上面提到的一个:

$ git cat-file -p e54793a95afeea1e10de1e5ad7eab914e7416250
tree dc3d0156b95303a305c69ba9113c94ff114b7cd3
parent 565442c35884e320633328218e0f6dd13f3657d3
author Junio C Hamano <gitster@pobox.com> 1651786597 -0700
committer Junio C Hamano <gitster@pobox.com> 1651786597 -0700
Git 2.36.1
Signed-off-by: Junio C Hamano <gitster@pobox.com>

这是原始提交对象,它实际上完全由提交的元数据组成。

提交对象包含两部分:

  • 每个提交都有构成该特定提交的所有文件的完整快照。 在像上面这样的真实提交中,这是tree行,这是必需的:必须有一个且只有一个tree

  • 每个提交也有一些元数据。 这是上面的整个文本块,真的(包括tree行本身)。

请注意,元数据告诉我们谁进行了提交,以及何时进行:上面1651786597的幻数是一个日期和时间戳,表示Thu May 5 14:36:37 2022-0700是时区,在本例中为太平洋夏令时或 UTC-7。 (它可能是山地标准时间,也是UTC-7,目前正在亚利桑那州的纳瓦霍族地区使用,但你可以很肯定地打赌,这不是Junio Hamano当时的实际位置。 它还具有提交者的提交消息,在这种情况下非常短:例如,与f8781bfda31756acdc0ae77da7e70337aedae7c9中的片段进行比较:

2.36 gitk/diff-tree --stdin regression fix
This only surfaced as a regression after 2.36 release, but the
breakage was already there with us for at least a year.
The diff_free() call is to be used after we completely finished with
a diffopt structure.  After "git diff A B" finishes producing
output, calling it before process exit is fine.  But there are
commands that prepares diff_options struct once, compares two sets
of paths, releases resources that were used to do the comparison,
then reuses the same diff_option struct to go on to compare the next
two sets of paths, like "git log -p".
After "git log -p" finishes showing a single commit, calling it
before it goes on to the next commit is NOT fine.  There is a
mechanism, the .no_free member in diff_options struct, to help "git
log" to avoid calling diff_free() after showing each commit and ...

这是一个更好的提交消息。 (不包括更新的测试和log-tree.c中的注释,修复本身只是在builtin/diff-tree.c中添加了三行。

Git 自行设置的元数据的另一个非常重要的部分是parent行。 可以有多个parent行(或者很少没有父行),因为每个提交在其元数据中都携带父哈希 ID列表。 这些只是存储库中一些现有提交的原始哈希 ID,当您或 Junio 或任何人添加新提交时,这些 ID 就在那里。 我们稍后会看到这些是做什么用的。

到目前为止的评论

一个存储库有两个数据库:

  • 一个(通常更大)包含提交和其他对象。 这些具有哈希 ID;Git需要哈希 ID 才能找到它们。
  • 另一个(通常要小得多)包含名称,例如分支和标记名称,并将每个名称映射到一个哈希 ID。 对于分支名称,根据定义,我们在此处获得的一个哈希 ID 是该分支的最新提交
  • 提交是所有这些存在的原因。 每个存储两个东西:完整快照和一些元数据。

工作树

现在,在 Git 中使哈希 ID 工作的技巧之一是任何对象的任何部分都不能更改。 提交一旦完成,就是永远的方式。 该提交具有该哈希 ID 保存这些文件元数据,因此具有该父项(或父项)等。一切都永远冻结了。

提交中的文件以特殊的只读、压缩(有时是高度压缩)、重复数据删除格式存储。 这样可以避免存储库膨胀,即使大多数提交大多重用其父提交中的大多数文件。 由于文件已消除重复,因此重复项实际上不占用任何空间。 只有更改的文件需要任何空间。

但有一个明显的问题:

  • 只有Git可以读取这些压缩和去重的文件。
  • 没有任何东西,甚至 Git 本身,可以编写它们。

如果我们要完成任何工作,我们必须有普通的文件,普通程序可以读取和写入。 我们将从哪里得到这些?

Git 的答案是,对于任何非裸存储库,3提供一个可以完成工作的区域。 Git 将这个区域(一个目录树或充满文件夹的文件夹,或任何你喜欢的术语)称为你的工作树,简称为工作树事实上,典型的设置是将存储库正确存在于工作树顶层的隐藏.git目录中。 它里面的所有东西都是Git 的;它外面的所有东西,在工作树的顶层,以及它里面的任何子目录(文件夹),除了.git本身,都是你的


3存储库是没有工作树的存储库。 这可能看起来有点多余或毫无意义,但它实际上有一个功能:请参阅尝试解决 Git 的问题是什么 --裸存储库?


git checkoutgit switch是关于什么的

当你签出一些提交(带有git checkoutgit switch和分支名称)时,你是在告诉 Git:

  • 使用分支名称按哈希 ID 查找最新提交。
  • 从我的工作树中删除我一直使用的任何提交中出现的所有文件。
  • 将我刚刚命名的提交中出现的所有文件替换到我的工作树中。

Git 在这里尽可能走了一条很大的捷径:如果你从提交a123456移动到b789abc,并且这两个提交中的大多数文件都经过重复数据删除,Git 实际上不会为这些文件的删除和替换而烦恼。 这个快捷方式在以后变得很重要,但是如果您开始将git checkout/git switch视为意义:删除当前提交的文件,更改为新的当前提交,并提取这些文件,那么您有一个良好的开端。

提交如何串在一起

现在让我们重新审视一下提交本身。 每个提交在其元数据中都有一组parent行。大多数提交(到目前为止在大多数存储库中)只有一个父级,这就是要开始的事情。

让我们在一个简单的、微小的三提交存储库中绘制提交。 这三个提交将有三个大而丑陋的随机哈希ID,但与其编造一些,不如按该顺序将它们称为ABC提交。 提交A是第一次提交 - 这有点特别,因为它没有父提交 - 然后你在使用提交A时进行了B,并在使用B时进行了C。 所以我们有这个:

A <-B <-C

也就是说,提交C最新的提交,有一些文件作为其快照,并且作为其父级,提交B的原始哈希 ID 。 我们说C指向B.

同时,提交B将一些文件作为其快照,并将提交A作为其父级。 我们说B指向A

您的分支名称(我们假设为main)指向最新的提交C

A--B--C   <-- main

(在这里,我懒得将提交之间的箭头绘制为箭头,但它们仍然是向后箭头,真的)。

当你git checkout main时,Git 会将所有提交C的文件提取到你的工作树中。 您可以查看和编辑这些文件。

如果您确实编辑了一些内容,则可以使用git addgit commit进行新的提交。 这个新提交得到了一个全新的,从未在宇宙中的任何 Git 存储库中使用过的哈希 ID,但我们只称这个新提交为D。 Git 将安排新的提交D向后指向现有的提交C,因为C是你一直在使用的那个,所以让我们绘制新的提交D

A--B--C   <-- main

D

(从DC向左的向后斜杠是我对箭头懒惰的原因——有一些箭头字体,但它们在 StackOverflow 上效果不佳,所以我们只需要想象从DC的箭头。

但是现在D最新的main提交,所以git commit也将D的哈希ID存储到名称main,以便main现在指向D

A--B--C

D   <-- main

(现在没有理由使用额外的线条来画东西;我只是为了视觉对称而保留它)。

这是分支在 Git 中增长的一种方式。签出分支,以便它是您当前的分支。 它最尖端的提交(此图形中朝右的提交,或输出中朝git log --graph顶部的提交)将成为您当前的提交,这些就是您在工作树中看到的文件。 您编辑这些文件,使用git add并运行git commit,Git 将新文件打包到新的提交中,然后使用自动重复数据消除功能,这样,如果您将文件更改BA中的方式,它就会在此处删除重复数据!— 到一个新的提交中,然后将提交的哈希 ID 填充到当前分支名称中。

分支如何形成

假设我们从相同的三提交存储库开始:

A--B--C   <-- main

现在让我们创建一个新的分支名称dev。 此名称必须指向某个现有提交。 只有三个提交,所以我们必须选择ABC中的一个,作为dev指向的名称。 显而易见的方法是最新的:我们可能不需要及时返回提交BA开始添加新提交。 因此,让我们添加dev,以便它也指向C,方法是运行:

git branch dev

我们得到:

A--B--C   <-- dev, main

从我们的图纸中很难看出:我们是dev还是main? 也就是说,如果我们运行git status,它会说"在分支开发"或"在分支主上"? 让我们添加一个特殊名称,HEAD像这样全部大写,并将其附加到两个分支名称之一,以显示我们正在使用的名称

A--B--C   <-- dev, main (HEAD)

我们在"分支main上"。 如果我们现在进行新的提交,提交D将像往常一样指向提交C,Git 会将新的哈希 ID 粘贴到名称main中。

但是如果我们运行:

git checkout dev

Git 将从我们的工作树中删除所有提交C文件,并放入所有提交C文件。 (看起来有点傻,不是吗? 捷径! Git 实际上不会任何这些! 现在我们有:

A--B--C   <-- dev (HEAD), main

当我们进行新的提交D时,我们会得到:

A--B--C   <-- main

D   <-- dev (HEAD)

如果我们git checkout main,Git 将删除提交D文件并安装提交C文件,我们将返回到:

A--B--C   <-- main (HEAD)

D   <-- dev

如果我们现在进行另一个新提交,我们将得到:

E   <-- main (HEAD)
/
A--B--C

D   <-- dev

这就是分支在 Git 中的工作方式。分支名称,如maindev,会挑选出最后一个提交。 从那里开始,Git 向后工作。 提交E可能是最后一个提交main,但提交A-B-Cmain,因为我们从E开始并向后工作时会到达它们。

同时,提交D是最后一个dev提交,但提交A-B-Cdev,因为我们从D开始并向后工作时会到达它们。 提交D不在main,因为当我们从E开始并向后工作时,我们永远不会达到提交D:这直接跳过D

回顾

我们现在知道:

  • Git 是关于提交的
  • 提交存储快照和元数据。
  • 我们使用分支名称将提交组织到分支中,以查找最后一个提交。
  • 我们签出提交以将其文件视为文件,并对其进行处理。 否则,它们是只有 Git 才能看到的特殊奇怪的 Gitty 东西。
  • 任何提交的任何部分一旦完成就无法更改。

现在我们将进入git rebase.

git rebase是关于什么的

我们经常发现自己在使用 Git 并陷入这种情况:

F--G--H   <-- main
/
...--A--B

C--D--E   <-- feature (HEAD)

我们对自己说:天哪,如果我们晚点开始功能,main已经提交G和/或H了,那就太好了,因为我们现在需要这些功能。

提交C-D-E没有根本性的错误,我们可以只使用git merge,但无论出于何种原因——老板这么说,同事已经决定他们喜欢变基流程,不管它是什么——我们决定要"改进"我们的C-D-E提交。 我们将重新制作它们,以便它们F-G-H之后出现,如下所示:

C'-D'-E'   <-- improved-feature (HEAD)
/
F--G--H   <-- main
/
...--A--B

C--D--E   <-- feature

从字面上看,我们可以通过签出提交H,创建一个新分支,然后重新做我们的工作来做到这一点:

git switch main
git switch -c improved-feature
... redo a bunch of work ...

git rebase所做的是为我们自动化。 如果我们手动执行此操作,则每个"重做"步骤都将涉及使用git cherry-pick(我不会在这里详细介绍)。git rebase命令为我们自动挑选樱桃,然后添加另一个扭曲:它不需要像improved-feature这样的分支名称,它只是将旧分支名称从旧提交中拉出并使其指向新分支名称:

C'-D'-E'   <-- feature (HEAD)
/
F--G--H   <-- main
/
...--A--B

C--D--E   [abandoned]

旧的废弃提交实际上仍然存在,在 Git 中,至少 30 天左右。 但是由于没有可以找到它们的名称,只有当您保存了它们的哈希 ID 或有一些技巧来查找这些哈希 ID 时,您才能看到这些提交。

当变基完全完成后,我们的原始提交将被复制到新的和改进的提交中。 新的提交具有新的和不同的哈希 ID,但由于没有人注意到实际的哈希 ID,因此查看此存储库的人只会看到三个仅feature分支提交,并假设它们已神奇地更改为新的改进提交。5


4Git 内置了一些方便的技巧,但我们不会在这里介绍它们。

5Git 看到了真相,如果您将 Git 存储库连接到其他一些 Git 存储库,他们将拥有......关于这个的话,或者长时间的谈话,如果你不知道自己在做什么,它可能会造成一团糟。 基本上,如果他们仍然有你的原件,当你认为你已经摆脱了它们时,你最终可以把它们拿回来! 每当您连接两个 Git 存储库时,您通常都会用一只手来处理它拥有的任何新提交,而另一只提交是缺失的。 这就是哈希 ID 的魔力真正发挥作用的地方:它们仅通过哈希 ID 来完成这一切。

这里的底线是,只有当这些提交的所有用户都同意这些提交可以重定基址时,才应该变基提交。 如果你是唯一的用户,你只需要同意自己,所以这要容易得多。 否则,在开始变基之前,请提前征得所有其他用户的同意。

要审查远程分支(我还没有),我更喜欢git switch aBranch:它的猜测模式会自动设置远程跟踪分支origin/aBranch,允许我做简单的git pull在将来的审查实例中更新它。

那将和git switch -c <branch> --track <remote>/<branch>

我也更喜欢设置

git config --global pull.rebase true
git config --global rebase.autoStash true

这样,该分支上的git pull会将我的任何本地提交都基于更新的分支,不仅供我查看,还用于检查我的本地(尚未推送)代码/提交是否仍然在更新的远程分支之上工作。

相关内容

  • 没有找到相关文章

最新更新