为什么如果我在我的一个分支中进行更改,git 会更改每个分支?

edd*_*die 1 git branch github

在我的本地仓库中,我有 3-4 个分支,当我处理其中一个分支时,我在其中所做的更改会传播到我所有的其他分支。事实上,如果我是git reset --hard origin其中之一,它会重置每个本地分支。

我应该如何将我的更改限制在一个分支中?

请注意,我是 git 的菜鸟。

tor*_*rek 7

你真正需要的是一个好的教程。我不确定推荐哪一个——Git很难,而且有很多糟糕的教程,其中许多开始是好的和/或有好的意图,但最终遇到了困难的部分。:-)

不过,此时您需要知道的是,分支——或者更准确地说,分支名称——并没有多大意义。Git 真的是关于commits 的。在您进行新的提交(或以某种方式操作现有的提交)之前,您还没有在 Git 本身中做任何事情。

但是,关于提交的一件事是它们一直被冻结。您实际上无法更改任何提交中的任何内容。每个提交都以特殊的、只读的、仅限 Git 的、冻结的格式存储所有文件的完整快照。这意味着它们非常适合存档,但对于做任何工作完全没用。

出于这个原因,Git 为您提供了一个可以工作的领域。这个区域被称为(不同的)工作树,或工作树,或工作树(我自己喜欢带连字符的术语),或任何数量的其他类似名称。在这里,文件只是普通文件。这意味着您可以使用它们——因此术语工作树。当你和他们一起工作时,Git 大多不在乎:这个区域是给你的,它是你的工作树。Git 只是在必要时从提交中填写它。

指数

在 Git 中进行新的提交是很棘手的。其他版本控制系统要简单得多,因为在这些其他系统中,您的工作区域也是您建议的下一次提交。这在 Git 中并非如此!Git 还添加了一件你必须知道的事情,即使你真的不想知道。这东西超级重要,暴露给你,虽然你看不见

Git 称这个东西为index,或者staging area,或者有时——现在很少——缓存。这三个名字都可以指同一个东西。它以不同的方式使用(并且“缓存”术语现在主要用于内部数据结构,这就是它现在很少见的原因),但是对索引的一个相当不错的简短描述是它包含您提出的下一次提交。你可以把它看作持有的每个文件的副本,当前提交。1

当您更改工作树中的文件时, index 中的副本不会发生任何变化。它仍然与您选择的提交中的副本匹配。您必须运行git add以将文件工作树复制索引。现在索引副本不再与提交的副本匹配,因此您建议下一次提交与当前提交不同。

运行从现在索引中的任何内容git commit构建一个新的提交。因此,在 Git 中,您在工作树中工作,然后用于将更新的文件复制回索引,然后用于从索引进行新的提交。这是一种痛苦,这就是为什么其他系统不具备索引:他们没有让你更新在两者之间所有文件的副本。但是 Git 可以,而且最好习惯并熟悉它。有一些技巧可以尝试隐藏它,2但它们最终失败了:Git 中的某些事情只能通过指向索引来解释。git addgit commit

索引进行新提交,该新提交成为当前提交。现在当前提交和索引匹配。这也是 a 之后的正常情况git checkout:当前提交和索引正常匹配。请参阅下面的例外。


1从技术上讲,索引包含对内部 Git blob 对象引用。但是,将文件的索引“副本”视为真正的独立副本在大多数情况下都适用——只有当您开始了解 Git 内部结构时,您才必须了解 blob 对象。

2例如,您可以使用git commit -a代替git commit。这只是git add -u为你运行。该add -u步骤告诉 Git:对于已经在索引中的所有文件,检查它们是否可以git add对它们进行处理。如果是这样,现在就去做。 提交然后使用更新的索引。这里也有一些额外的复杂性,但只有在提交步骤本身失败时才会出现。尽管如此,只有在它们确实出现时,才能通过了解索引来正确解释它们。


在您有未提交的更改时检出另一个分支

当您进行git checkout某个特定的提交时(通过某个特定的分支名称找到),Git将从该提交中填充您的索引和工作树。如果旧提交和新提交中的文件相同,这可能会更新一些文件(索引和工作树中的文件)并保留其他文件。

但是,如果您对索引和/或工作树进行了一些更改但没有提交,Git 会尽可能地尝试保留该修改。这就是你一直看到的。在这种情况下,您当前的提交和索引匹配。(在某些情况下,工作树中发生的事情甚至更复杂。有关此方面的太多信息,请参阅在当前分支上有未提交的更改时检查另一个分支。)

当你做一个新的提交时,分支名称会以一种有趣的方式改变

Git 中的每个提交都有一个唯一的哈希 ID。这个哈希 ID 是一串丑陋的字母和数字。从技术上讲,它是提交内容的 SHA 校验和的十六进制表示;但关于它的主要事情是,每个地方的每个Git 都会同意这个提交获得这个哈希 ID,并且没有其他提交可以拥有这个哈希 ID。每个其他提交都有其他一些哈希 ID。

哈希 ID 看起来是随机的,人类不可能记住。该计算机可以记住他们为我们。这就是分支名称的真正含义。

请记住,我们在上面说过所有提交都被永久冻结。但是,对于分支名称而言,情况并非如此;如果是这样,这些名称就没有多大用处了。

Git 中的分支名称只包含一个提交的哈希 ID。根据定义,该提交是分支上的最后一次提交。

每个提交都包含一组以前的提交哈希 ID。大多数提交只保存一个哈希 ID。在这个提交(连同所有文件的快照)中的这个哈希 ID 是这个提交的提交。

每当一个 Git 项目(分支名称或提交)持有 Git 提交的哈希 ID 时,我们就说该项目指向该提交。所以像这样的分支名称master指向一个提交。该提交指向其父级。它的父节点指向另一个父节点,依此类推。

如果我们使用大写字母来代表丑陋的大哈希 ID,我们可以得出所有这些:

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

名称 master包含哈希 ID HH最后一次提交。Commit通过包含 commit 的哈希 IDH指向其直接父级。Commit因此指向它的parent ,它又指向回,依此类推。GGGF

这一切都在继续,随着这些向后指向的箭头,直到我们到达有史以来的第一个提交。它没有指向任何更远的地方,因为它不能。所以这就是行动最终停止的地方。于是有了这张图:

A--B--C--D--E--F--G--H   <-- master
Run Code Online (Sandbox Code Playgroud)

代表一个包含 8 个提交的 Git 存储库,每个提交都有自己唯一的哈希 ID 和一个分支名称, master

我们可以添加另一个分支名称,也指向 commit H,如下所示:

git branch develop
git checkout develop
Run Code Online (Sandbox Code Playgroud)

现在我们需要以一种方式来记住我们正在使用的分支名称。为此,让我们将特殊名称附加HEAD到两个分支名称之一:

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

请注意,所有八个提交都在两个分支上。(这是不寻常的:大多数版本控制系统不是这样工作的。)

现在让我们以通常的方式进行新的提交:更改工作树中的一些文件,用于git add将它们复制到索引中,然后运行git commit.

Git 现在要做的是将索引中的文件(它们已经处于冻结格式,准备提交)打包到新的提交中,将我们的姓名和电子邮件地址等放入新的提交中,然后为这个新提交计算新的、唯一的、通用的、跨所有 Git 无处不在的哈希 ID。我们是唯一一个这个提交的Git ,但是我们的哈希 ID 现在意味着这个提交,而不是其他的。3I不过,我们简称为 提交。GitI以 commitH作为其父级写出提交:

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

的最后一步git commit是棘手的部分:Git 现在将I的哈希 ID写入附加到的名称中HEAD。在这种情况下是develop

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

现在develop指向 commit I。通过承诺了H,这develop之前,仍然存在上develop。但是,该名称专门develop选择提交I。Git 现在可以从 at 开始I并反向工作到H、 then G、 thenF等等——或者它可以从 atmaster开始找到H,然后反向工作到 find G、 thenF等等。

这就是提交在分支上的含义。分支名标识的最后一次提交。然后,Git 使用从一个提交到其父提交的内部、向后、连接箭头来查找前一个提交,并继续这样做,直到它到达不再返回的提交为止。

每个提交都存储一个快照——一个完整的副本,即在提交时索引中的所有文件,以及这个元数据:谁提交,何时提交;父哈希 ID(两个或多个用于合并提交);以及一条日志消息,其中进行提交的人应该告诉您他们为什么进行提交。

因为每次提交都有一个唯一的哈希ID,并在宇宙中所有控释同意散列ID的手段承诺,你可以连接两台控释片一起,他们可以观察一下对方的哈希ID来看看谁拥有其呈交(S)。然后,一个 Git 可以向另一个 Git 提交一个拥有的、另一个想要的和没有的任何提交。这使用了大量的 CS 图论和其他技巧——例如增量编码——使发送 Git 能够向接收 Git 发送最少量的实际数据,这样即使每次提交都有所有文件的完整快照,发送方只向接收方发送更改


3正如您想象的那样,这使得哈希 ID 计算成为 Git 中真正的魔法来源。这有点棘手,但它确实在实践中起作用。散列 ID 可能会发生冲突,但这还不是真正的问题。另请参阅新发现的 SHA-1 冲突如何影响 Git?


概括:

  • 一个仓库是提交的集合,以及一些集名的。
  • 提交由哈希ID标识。每个都包含文件的快照以及元数据。
  • 每个分支名称或其他名称都包含一个提交的哈希 ID。这是链中的最后一次提交。
  • 每个提交都在其元数据中保存一定数量的先前提交的哈希 ID。至少有一次提交没有先前的提交,因为它是有史以来的第一次提交。其余的大多数都有一个:他们之前的一次提交。合并提交有两个或多个先前的提交。
  • 提交被永远冻结,但分支名称——挑选出最后一次提交——随着时间的推移而移动。要添加新提交,您或 Git 使其指向前一个提交,然后移动一些分支名称。
  • 传输(git fetchgit push)涉及连接两个 Git,让它们弄清楚它们共享哪些提交,以及发送方将发送哪些提交。接收方最终必须将最后一个哈希 ID保存在某处,以便接收方稍后可以再次找到这些提交,但我们还没有介绍它是如何工作的。
  • 同时,索引暂存区是您构建新提交的地方。你看不到里面有什么——无论如何都不是直接和容易的——但是git status,我们在这里没有涉及到,将比较其中的内容,并可以告诉你这些事情。您的工作树工作树是您可以查看和处理文件的地方。您必须将它们复制回索引/暂存区,以便进行新提交以保存更新文件的新快照。在您这样做之前,您所做的就是更改文件的工作树副本。