新的工作树和分支有什么区别?

aan*_*lle 8 git version-control github

我是一个 git 新手,我对新的工作树和分支有点困惑。阅读文档后我仍然很困惑,所以我决定询问。

据我所知,Git 分支就像当前工作树中的一个新“分支”。如果我想开发一个新功能,我可以从主分支签出到我新创建的分支,开发该功能,然后将其合并/变基到主分支。

我想确认我的理解:同一存储库中新工作树的概念是我们创建一个全新的工作空间/树(具有单独的主分支)。而在那棵树上,我可以有很多新的树枝吗?

如果您能指出一个示例(在 Github 上),说明该存储库由多个工作树组成,以便我可以进一步研究,我将不胜感激。

谢谢你!

tor*_*rek 14

这里有一个区别,而且很重要,但理解它很棘手。为了实现这个目标,我们先从这个开始:归根结底,Git 的核心就是提交这与分支无关,但分支名称可以帮助我们(和 Git)找到提交。尽管每次提交都包含文件,但它也与文件无关。这实际上与提交有关。因此,我们需要从什么是提交以及它为您做什么开始。然后我们将继续分支名称以及它们如何查找提交,以及 Git 的索引工作树。一旦我们正确地涵盖了所有这些内容,我们就可以介绍git worktree它的特点。

\n

提交

\n

Git 中的提交存储两件事:

\n
    \n
  • 它具有每个文件的完整快照,并始终冻结,以您告诉 Git 制作快照时该文件的形式为准。(我们稍后会看到这些文件实际上来自哪里:令人惊讶的是,它不是您看到并使用的文件。)这些文件存储在一个特殊的、只读的、Git-only 的文件中,压缩和去重格式,只有 Git 本身可以使用。重复数据删除处理了以下事实:每个新的提交通常与一些较旧的现有提交具有大部分相同的文件。这些文件实际上是在提交\xe2\x80\x94 之间共享的,这是完全安全的,因为任何提交的任何部分都不能更改。

    \n
  • \n
  • 除了快照之外,每个提交都有一些元数据。元数据保存诸如谁进行提交以及何时提交等信息。您还可以在此处放置一条日志消息,解释您为何进行提交,以便其他人\xe2\x80\x94 或您未来的自己\xe2\x80\x94 可以回来看看您打算做什么。(最好对上一次提交的问题进行高级解释,以及本次提交正在采取哪些改进措施,以防您以后发现错误。没有必要进行低级解释有关各个更改行的级别详细信息,因为git diffgit show可以机械地显示这一点。)与快照一样,此元数据也始终冻结 \xe2\x80\x94 事实上,这是每个内部 Git 对象的一个​​功能:没有一个可以被冻结改变了,即使是 Git 本身也没有改变。

    \n
  • \n
\n

现在,要直接查找提交,Git 需要知道提交的哈希 ID。提交哈希 ID 是代表一个大十六进制数字的丑陋的大字符串,它实际上是提交内容的加密校验和。(这就是内容无法更改的原因:即使更改一位也会更改校验和。Git 确保在提取过程中,内容仍然是用于查找对象的密钥校验和。对象本身存储在密钥中-价值数据库,键是校验和。)

\n

因此,我们需要一些哈希 IDH来查找提交。该git log命令显示哈希 ID(完整或缩写,具体取决于您选择的日志格式)。但这些东西对人类来说毫无用处,因为人类常常无法211eca0895794362184da2be2a2d812d070719d3辨别21127fa9829da1f7b805e44517970194490567d0Git可以;计算机擅长处理这种挑剔的细节。因此,每个提交在其元数据中都会存储一个或多个早期提交的原始哈希 ID 。

\n

大多数提交只存储一个早期提交的哈希 ID。这形成了向后看的提交链。也就是说,如果我们以某种方式知道\xe2\x80\x94\xe2\x80\x94我们或分支上最新提交的哈希ID是(这里代表一些真实的,尽管丑陋的哈希ID),我们可以给出原始哈希 ID 到. 然后 Git 会查找 commit ,并且 commit在其元数据中包含一些早期提交的哈希 ID :mastermainHHgit logHHG

\n
          G <-H\n
Run Code Online (Sandbox Code Playgroud)\n

G它也是一个提交,所以它有元数据。GGit 可以使用刚刚从 获取的哈希 ID 从所有提交的大数据库中获取整个提交H,并且元数据存储一些更早提交的哈希 ID:

\n
... <-F <-G <-H\n
Run Code Online (Sandbox Code Playgroud)\n

git log命令可以永远保持此进程,或者更确切地说,只要它找到的每个提交都有一些先前的提交。该链最终在历史的开始处结束:在第一次提交时,\xe2\x80\x94 是第一个提交\xe2\x80\x94,其中没有先前的提交哈希 ID,因为它不能。

\n

因此,我们至少需要以某种方式神奇地知道最新提交的哈希 ID H。从那里开始,Git 可以向后工作,一次提交一个。这些提交实际上历史记录,存储在存储库中。每次提交都有一些元数据,告诉您是谁、何时以及为什么进行的;每个提交都有下一个较早提交的哈希 ID。每次提交都是截至该提交时整个文件集的完整快照。而且,通过比较任意两个相邻提交的内容,Git 可以告诉我们该提交中发生了什么变化。但我们确实有一个问题:我们需要知道最新提交的哈希 ID。

\n

分行名称

\n

这就是分支名称的用武之地。类似master或 的分支名称main仅包含一个提交哈希 ID该名称中存储的哈希 ID是大 Git 对象数据库中肯定存在的某个提交的哈希 ID,并且 \xe2\x80\x94 根据定义\xe2\x80\x94,该哈希 ID 告诉 Git 该提交是 / 中的最后一次提交在那个分支上:

\n
...--F--G--H   <-- main\n
Run Code Online (Sandbox Code Playgroud)\n

现在,仅仅因为H是最后一次提交main并不意味着“之后”没有任何提交H。例如,现在让我们创建一个新的分支名称 ,develop并使其暂时指向提交:H

\n
...--F--G--H   <-- develop, main\n
Run Code Online (Sandbox Code Playgroud)\n

无论我们给git checkoutor起什么名字git switch,Git 都会提取 commit HH毕竟,两个名字都选择 commit 。事实上,两个分支都包含相同的提交集。但 Git确实需要知道我们正在使用哪个名称HEAD,所以让我们为这些绘图添加一个特殊名称 :

\n
...--F--G--H   <-- develop, main (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

这里,HEAD是“附着” main。这意味着我们在分支上main,使用提交H。如果我们运行git checkout developor git switch develop,Git 将从提交切换H到提交H\xe2\x80\x94,这并不是一个很大的切换!\xe2\x80\x94 但现在将附加HEAD到名称develop

\n
...--F--G--H   <-- develop (HEAD), main\n
Run Code Online (Sandbox Code Playgroud)\n

从这里开始,让我们进行一项新的提交。Git 将为这个新提交提供一个新的、通用唯一的哈希 ID。1 我们将其命名为I,并像这样绘制它:

\n
...--F--G--H   <-- main\n            \\\n             I   <-- develop (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

提交I将向后指向它之前的提交H,我们从中成长I,并且\xe2\x80\x94这是偷偷摸摸的技巧\xe2\x80\x94现在Git将I\的哈希ID写入名称 develop。这推进了分支。现在develop包含 commitI作为其最终提交,以及之前的提交Hmain仍然结束于H

\n

这就是分支通过进行新提交而增长的方式:每次我们进行新提交时,分支名称也会前进。如果我们对 进行两次提交develop,我们会得到:

\n
...--F--G--H   <-- main\n            \\\n             I--J   <-- develop (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

如果我们翻转回main,并创建一个新的分支名称,或者直接在 上进行提交main,我们会发现这两个分支将出现分歧:

\n
             K--L   <-- main (HEAD)\n            /\n...--F--G--H\n            \\\n             I--J   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

如果我们认为直接进行这些提交main是一个坏主意,我们现在可以创建一个分支来指向L

\n
             K--L   <-- main (HEAD), feature\n            /\n...--F--G--H\n            \\\n             I--J   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

然后强制 Git 将名称移mainH(我们必须找到H\ 的哈希 ID,也许使用git log;然后我们可能会使用git reset --hardmove main):

\n
             K--L   <-- feature\n            /\n...--F--G--H   <-- main (HEAD)\n            \\\n             I--J   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

这强调的是分支名称的移动。通常,它们是通过您使用 向分支添加新提交来移动的git commit。但是您可以以任何方式移动它们,例如使用git reset或。git branch -fGit 只是使用分支名称来查找提交

\n

(如果你向后移动一些分支名称,一些提交可能会变得很难找到。例如,假设我们强制develop返回 commit 。之后I我们如何找到提交?有一些方法,但它会变得混乱,所以每当你\J强制使用分支名称,您应该首先仔细考虑。)

\n
\n

1这里的“普遍唯一”意味着这个哈希 ID 不仅不会出现在这个Git 存储库中,而且不会出现在任何其他 Git 存储库中,无论是现在、过去还是将来!Git 实际上不必满足这么强的约束\xe2\x80\x94新的哈希 ID 只需不存在于该 Git 存储库将“满足”的任何 Git 存储库中,除非两个会议 Git 共享此commit\xe2\x80\x94,但 Git 无论如何都会尝试实现它,因为我们不确定将来将连接哪些 Git 存储库。这就是哈希 ID 如此大且难看的原因。(事实证明,它们现在还不够大和丑陋它们是在 2005 年 Git 首次发布的时候,但时间在前进。)

\n
\n

Git 的索引和你的工作树

\n

我多次注意到提交中的文件都是只读的且仅限 Git,而且实际上无法使用。因此,当您签出提交时,Git 必须将这些文件复制出来,转换成您的计算机可以正常使用的形式。这些副本会进入您的工作树。这实际上非常简单且易于理解:提交保存了一个存档,必须将其取消存档才能使用。所以git checkout或者git switch这样做:它删除以前的签出(如果有),并将其替换为您选择的提交,这是某个分支名称选择的提交。

\n

(实际上,它实际上比这更奇特和更复杂。首先,为了快速进行,签出过程尝试不触及任何它不需要的文件;其次,为了避免丢弃未提交的工作,它必须检查以确保要删除和替换的文件中没有未提交的工作。但是“删除旧提交,放入新提交”作为您头脑中的起始模型就可以了。)

\n

不过,我之前也提到过,这git commit实际上并没有根据工作树中的内容创建新快照。相反,Git 会在当前提交的版本和工作树副本之间插入一个额外的“副本”\xe2\x80\x94(用引号引起来),稍后我将解释每个文件的\xe2\x80\x94 。这个额外的“副本”位于 Git 所称的索引暂存区域,或者 \xe2\x80\x94 如今很少\xe2\x80\x94缓存中。所有三个名称都指同一事物,我在这里称之为“索引”。

\n

索引\xe2\x80\x94 中的内容至少在最初、在全新检出\xe2\x80\x94 之后是当前提交中每个文件的预压缩和去重“副本” 。由于这些都当前提交中,因此索引根本不包含任何实际的文件副本,只是对准备重新提交的文件的引用。

\n

当您工作时,Git 希望您运行git add工作树中更改的每个文件。当你运行git add,Git:

\n
    \n
  • 检查该文件是否已在索引中:如果是,则该“副本”将被引导出去;
  • \n
  • 压缩并删除文件的工作树副本,并将放入其索引中。
  • \n
\n

因此,任何文件现在都在索引中,并且任何更新的文件都将其索引副本替换为新的、准备提交、压缩和去重复的数据。

\n

当您运行时git commit,Git 只是打包索引以用作新的提交快照。这意味着索引的作用是充当您建议的下一个快照。它一开始与当前提交匹配,但随着您运行而改变git add

\n

这就是为什么索引的名称之一是暂存区域:当您更新文件时,您将更新的文件安排在“舞台上”,准备进行快照。工作树中的副本是您可以处理的,除了:

\n
    \n
  • 当您由于切换到其他提交而使用git checkout或覆盖它们时;git switch
  • \n
  • 当你使用其他一些 Git 命令故意覆盖它们时,例如,放弃一个没有成功的实验;和
  • \n
  • 当您使用git add告诉 Git:从工作树复制回索引时,为下一次提交准备新副本
  • \n
\n

Git 索引中的“副本”(预先去重)供Git在下一次提交中使用。

\n

(索引在git merge操作过程中发挥了扩展的作用,尽管我们不会在这里介绍这一点。这种扩展的作用就是为什么“索引”可能是比“暂存区”更好的名称。但是“暂存区”是一个很好的选择在此情况下它如何工作的名称。)

\n

到目前为止的总结

\n
    \n
  • 分支名称通过指向某个特定提交来选择提交。
  • \n
  • 进行新的提交会导致当前分支名称指向新的提交。新提交的父提交是您刚刚在进行新提交之前签出的提交;现在新的提交就是您已签出的提交。
  • \n
  • orgit checkout命令git switch选择一个新的分支名称和/或提交以签出。(您可以使用分离 HEAD模式选择原始提交,但我们不会在这里介绍它。)假设您使用新的分支名称,Git 现在将附加HEAD 该分支名称,以便成为当前分支名称
  • \n
  • 每个提交都有一个唯一的、又大又难看的哈希 ID 号,并且每个提交(第一个提交明显例外,以及任何其他所谓的提交)向后指向其父级。尽管我们没有在这里讨论这一点,但合并提交的特殊之处在于它指向两个或多个父级,而不仅仅是一个父级。每个提交\xe2\x80\x94(包括每个合并提交\xe2\x80\x94)都存储您(或任何人)进行提交时 Git 索引中的所有文件的完整快照。
  • \n
  • 工作保存来自提交并进入 Git 索引的文件,然后进入工作树。现在您可以随意修改这些文件。在您git add使用它们或使用其他 Git 命令来替换工作树内容之前,这里的所有内容都可以由您使用。
  • \n
\n

添加工作树git worktree add

\n

上图中,有:

\n
    \n
  • 恰好是一个HEAD,它指定哪个分支是当前签出的分支
  • \n
  • 恰好一个索引;和
  • \n
  • 恰好是一棵工作树
  • \n
\n

这意味着,例如,如果我们正在开发某些新功能,并且出现了一些非常重要的必须立即修复的错误,我们必须:

\n
    \n
  • 保存我们所有正在进行的工作;
  • \n
  • 切换到需要重要错误修复的分支;
  • \n
  • 因此会破坏(a)我们的思路和(b)我们在工作树中正在进行的任何未提交的工作,或者需要很长时间来编译的工作等等。
  • \n
\n

如果这是一个问题,我们可以再次克隆源存储库。这得到了一个完全独立的存储库,其中我们有所有相同的提交\xe2\x80\x94,哈希 ID 匹配,因为它们实际上是相同的提交\xe2\x80\x94,但我们有一个新的工作树、索引和HEAD. 但是,如果我们能够避免运行git clone并投入大量磁盘空间和/或网络时间和/或为此所需的任何其他资源,那可能会很好。

\n

如果我们可以在不影响现有工作树的情况下,仅添加一棵新的工作树会怎么样? 我们还需要一个新的索引和一个新的索引HEAD,以便这个额外的工作树可以在其中签出不同的分支

\n

这正是git worktree add所做的。我们告诉 Git:创建一个新的工作树,并检查其中的一些分支。Git 制作所有三件事:\xe2\x80\x94 HEAD、索引和工作树\xe2\x80\x94,并执行git switch或在那里由其他分支名称选择的提交git checkout填充工作树。

\n

不过,有一个看起来很奇怪(乍一看)的约束:我们的新工作树必须位于与我们的主工作树或任何其他添加的工作树不同的分支上。git commit一旦您考虑如何自动更新签出的分支名称,原因就会变得更清楚。作为练习,考虑一下如果两个工作树都develop签出了分支,然后在其中一个树中进行了新的提交,会发生什么情况。另一个工作树中的文件和 Git 索引会发生什么情况?(我会为你回答这个问题:他们什么也没发生。)那么,当你进入另一棵工作树并尝试进行另一次新的提交时会发生什么?

\n

Git 并没有试图让这一切都起作用(人们可以想象各种方法来让它起作用),而是完全禁止它,这样问题就不会出现在第一位。这就是为什么存在这个奇怪的限制。此限制不适用于分离 HEAD 模式,因此添加的工作树可以在任何特定提交上处于分离 HEAD 模式,但同样,我们在这里还没有真正涵盖分离 HEAD 模式。

\n

回答您的一些具体问题

\n
\n

据我所知,Git 分支就像当前工作树中的一个新“分支”。

\n
\n

不:工作树只是你工作的地方。在 Git 中,术语“分支”是不明确的(请参阅“分支”到底是什么意思?),但分支名称(如maindevelop)只是我们(和 Git)用来查找一个特定哈希 ID 的名称。我们可以使该名称指向我们喜欢的任何提交。每个存储库都有自己的名称:如果我克隆您的 GitHub 存储库,我就有自己的分支名称。当您克隆 GitHub 存储库时,您会在克隆中获得自己的分支名称。GitHub 存储库有自己的分支名称。这些名称都不必匹配:如果我愿意的话,我可以在本地main调用它时与您一起工作niam(尽管这很愚蠢,而且只会为我带来额外的工作)。

\n
\n

同一存储库中新工作树的概念是我们创建一个全新的工作空间/树(具有单独的主分支)。

\n
\n

否:我们在存储库克隆中创建一个新的工作树,但共享其他所有内容。2 特别是,所有分支名称都是共享的。这一切都在一个克隆(一个存储库)中。

\n
\n

如果您能指出一个示例(在 Github 上),说明该存储库由多个工作树组成,以便我可以进一步研究,我将不胜感激。

\n
\n

这实际上是不可能的:首先,添加的工作树是特定于每个克隆的。此外,GitHub 存储库是所谓的存储库,根本没有工作树(因此没有人可以直接在 GitHub 上做任何工作,而不是我们首先在 GitHub 上登录)。

\n
\n

2除了索引和HEAD已经列举的那样,还有某些特定于工作树的特殊引用,例如ORIG_HEADMERGE_HEADCHERRY_PICK_HEAD等。当我写这篇文章时,我突然意识到我不确定是否FETCH_HEAD是工作树特定的。像二分之类的特殊参考也是特定于工作树的。这里的细节很混乱而且相当临时;最初的实施中遗漏了一些细节。

\n