【问题标题】:Git branch -d newBranch, and then git checkot master didn't undo the changes in newBranchgit branch -d new Branch,然后git checkout master没有撤消newBranch中的改动
【发布时间】:2020-10-09 07:07:39
【问题描述】:

这是我第一次遇到这种情况,我很震惊。 昨天我用git checkout -b recipients 创建了一个新分支。我用git branch检查了我在哪里。我做了一些改变,然后我想用git checkout master回到master。 但是即使我回到了主人那里,变化仍然存在。

我做错了什么?

谢谢。

update1:​​现在我认为我没有提交也没有隐藏,但正如你所看到的,git 没有抱怨(就像我认为在那些情况下所做的那样)

【问题讨论】:

  • 您是否在recipeients 分支中提交了您的更改?
  • 嗯,你基本上是在同一个确切的提交,为什么你会期望有什么不同?反正你什么都没做。如果您更改的文件不与分支之间不同的文件相交,则它们将保持修改状态并切换分支。由于分支的提交完全相同,所以一切都保持不变。

标签: git git-branch


【解决方案1】:

你需要重新调整你的心智模式。您认为 Git 与 files 一起工作,但这不是 Git 的内部方式。在内部,Git 使用 commits。要了解这一切是如何运作的——从而理解刚刚发生的事情——你必须了解很多关于提交以及 Git 如何生成新提交的知识。

所以:

  • 每个提交都有一个唯一的编号。这些数字不是简单的计数:我们没有提交 #1 后跟 2、3 等等。相反,每个数字都是一个大而丑陋且随机的散列 ID,例如 e1cfff676549cdcd702cbac105468723ef2722f4。提交存储在一个大的key-value database 中,提交的唯一编号作为键,提交的内容作为值。

  • 这些数字实际上根本不是随机的。相反,它们是提交中任何内容的加密校验和。 Git 可以通过其哈希 ID 找到任何提交或任何其他内部 Git 对象。但是,由于 ID 是内容的校验和,因此内容自动为只读。如果您要从数据库中取出一个,大惊小怪地只更改一个位,然后将结果放回去,您将得到一个新的不同的提交,带有一个新的不同的密钥。旧的提交仍然存在,在其原始密钥下。

  • 提交中的内容分为两部分。其中一部分是 Git 知道的所有文件的完整快照。我们稍后会回到这一点,但现在,让我们注意每个文件本身都以只读、压缩和去重的格式存储,只有 Git 本身可以读取。提交的另一部分是它的元数据:有关提交的信息,例如谁提交、何时提交等。作为此元数据的一部分,Git 存储 previous 提交的原始哈希 ID(键值数据库中的键)。

这三个事实有很多后果,但现在我们最关心的是提交及其所有存储文件的只读性。如果提交 in 中的文件确实不能被更改——而且它们不能——那么我们如何获得我们可以更改的文件?这是 Git 的 indexstaging area 的用武之地,也是你的工作树 的用武之地。

你的工作树

您可以查看和使用的文件副本根本不在 Git 中。它们在您的工作树中。这些是普通的、普通的、日常的文件。当您第一次克隆存储库时,您的工作树是完全空的。 Git 从某个提交中提取文件并填充您的工作树,现在您有了文件。这也解决了非 Git 程序无法读取提交中的文件这一事实。1

在您告诉 Git 将文件提取到您的工作树中之后,这些文件以及整个树本身都将由您随意处理。除非您告诉 Git 这样做,否则 Git 实际上不会使用这些文件。您可能希望 git commit 使用它们,但实际上并没有,2 我们将在下一节中看到。


1实际上有多种内部格式,其中一种——称为松散对象——一点也不难,所以有些程序可以或可以读取这些直接文件。然而,packed 形式的对象更加复杂。一些程序可以读取包文件,但它们似乎都面向使用或直接使用 Git。让 Git 处理这些就简单多了。

2一些选项和参数使git commit使用工作树文件。我们将在下面简单介绍。


Git 的索引

其他版本控制系统停留在“一个文件的两个副本”的事情上:有一个已提交的版本,采用某种内部格式,实际上无法更改,还有一个您可以使用的纯文件版本。无论拼写如何,他们的“提交”动词都使用普通文件。但是 Git 并没有这样做:相反,Git 添加了一个第三个​​版本,在提交副本和工作树副本之间。

这第三个“副本”——“副本”这个词在这里用引号引起来,因为它是内部的、去重复的格式,所以它实际上是自动共享的——每个文件都在 Git 中有三个名字。这个东西叫做index——一个没有实际意义的名字——或者staging area,指的是你使用它的方式。第三个名字,现在大多只出现在 --cached 选项中,是 cache,它指的是 Git 在内部使用这个东西来让 Git 运行得更快的方式。

最初,索引(或暂存区)保存当前提交中每个文件的相同副本。此副本采用内部格式,已准备好进入您将进行的 next 提交。但它实际上并不是一个提交,因此与真正的提交不同,它不是只读的。您无法更改其中的现有文件,但可以将工作树文件的新副本放入其中。

如果这没有意义,请换成这样想:当您运行git add <em>file</em> 时,Git 所做的是读取命名文件的工作树版本。 Git 在您运行 git add 时将其压缩为只读、​​去重 格式。如果该文件已经在任何其他提交中,Git 只会重新使用冻结的副本。如果没有,Git 现在已经准备了一个可冻结的副本。3 无论哪种方式,索引仍然准备好提交。

因此,索引或暂存区(如果您更喜欢该术语)随时准备就绪,4 可以进行 next 提交。那么,考虑它的一个好方法是索引包含您的提议的下一次提交。该提议独立于您的工作树。 Git 将在git commit 时间使用索引中的任何内容进行新的提交。该索引以准备提交的形式保存了 Git 知道的所有文件。

有一些提交选项与这个模型有点不同。特别是,git commit -agit commit --include <em>file</em>更新索引的缩写,就像我运行git add,然后使用更新的索引提交。还有git commit --only <em>file</em>,它更复杂,我们不会在这里介绍。5 但是将索引/暂存区域考虑为建议的下一次提交确实有效;就这样吧。


3从技术上讲,git add 只是从文件中创建一个新的或重用的 blob 对象,然后将 blob 的哈希写入索引。如果 blob 对象已经在数据库中,Git 会重新使用现有的 blob;否则数据库中有一个新的 blob 对象。如果您最终从不提交它,那么该 blob 对象会使数据库混乱一段时间,但最终git gc 会找到它并将其删除。通常情况下,没有人关心这里的一点点膨胀,但偶尔有人会不小心git add 一个千兆字节的文件,这有点痛苦。

4在冲突合并期间,索引会扩展。此扩展索引尚未准备好提交。这是当前 Git 的一个痛点,因为任何冲突都会占用索引和工作树对,直到冲突解决或正在进行的操作被中止。但是,git worktree add 解决了大部分问题。

5这些通过制作一两个临时索引文件来工作。然后 Git 将更新临时索引文件并在提交时使用这些文件。细节可能会变得很棘手。通常你不需要关心,但如果你选择编写 Git pre-commit hook,了解所有细节很重要。


关于分支名称的简短部分

以上是我们需要介绍 Git 刚刚所做的所有内容,但可能会让您对一些事情感到困惑,所以在结束之前让我们再回顾一下。回到顶部的三个关于提交的事实。如果需要,请再次通读第三个要点:每个提交都存储其 上一个 提交的 实际哈希 ID。 Git 将此称为(暗示为子)提交的 父级

这意味着提交形成向后看的链。如果我们知道某个链中 last 提交的哈希 ID,这就是我们所需要的。例如,考虑一下这个简单的提交链图,其中我们使用单个大写字母来代表真正的哈希 ID:

... <-F <-G <-H

H 是此链中的 last 提交。 Git 可以使用它的哈希 ID——真正的 ID,大而丑的唯一 ID,实际上不是字母 H——来查找提交的内容,获取所有文件的快照和元数据。元数据包含一个哈希 ID,它是之前提交 G 的实际哈希 ID。 GH 的父级; HG 的孩子。

因此,给定 H 的哈希 ID,Git 可以使用它来找到 G 的哈希 ID,这让 Git 可以获取 G 的完整快照和 G 的哈希 ID'我的父母,F。这让 Git 可以同时获取 F 的完整快照和 F 父级的哈希 ID。这一直重复,一直回到存储库中的第一个提交——它没有父级,因为它不能有父级——并且 存储库中的历史记录,从开始于H 并向后工作。

不过,所有这一切的关键是我们必须知道提交H 的实际哈希ID。我们在哪里可以得到这个哈希 ID?我们可以把它写下来:我们可以说我想要一个以提交H 结束的分支,因此在纸上或白板或其他任何东西上写下H 的实际哈希ID。但是我们有一台电脑。为什么不让计算机记下这个哈希 ID?

这就是分支名称:它只是存储一个哈希 ID 的地方。分支名称,无论是masterdeveloprecipients 还是其他任何名称,都只存储一个哈希ID。根据定义,该哈希 ID 是 that 链中的 last 提交。即使之后有更多的提交,它仍然是那个分支的最后一个。

当您创建一个新的分支名称时,您选择一些现有的提交并为该提交创建一个新名称。因此,如果我们现在使用master,我们可能会:

...--G--H   <-- master (HEAD)

如果我们现在创建一个新分支,而不指定要使用的特定提交,Git 将使用 当前提交,这里是提交 H,并为其命名:

...--G--H   <-- master, recipients (HEAD)

这里有趣的(HEAD) 注释告诉我们(和Git)这些名称中的哪个是当前名称。名称本身告诉我们(或至少是 Git)哪个 commit当前提交。 (人类往往不会关注那些又大又丑的散列 ID,因为它们又大又丑,而且,名称可以完成所有工作。)

所以现在你可以看到你做了什么

你有一些提交——我们在你的问题中看不到它的哈希 ID,但它有一些唯一的哈希 ID——作为你的 当前提交,你的 当前分支名称,即master。然后,您使用git checkout -b recipients 添加新的分支名称,而无需更改 Git 索引或工作树中的任何内容。两个分支名称都选择了相同的提交。

然后,您运行git checkout master。这告诉 Git:开始使用名称 master 作为当前名称。 Git 发现当前的提交 H (或者你喜欢的任何字母,或者使用真正的哈希 ID,但它又大又丑),它应该改用 ... commit @ 987654372@,因为master 选择了相同的提交。

如果您检查了一个不同的提交,如果另一个分支名称选择了另一个提交,就会发生这种情况,事情可能会有所不同。但是你一直在同一个提交上。您只是在更改用于查找提交 Hname。所以 Git 不需要 更新它的索引。

如果你有:

          I--J   <-- branch1
         /
...--G--H   <-- master
         \
          K--L   <-- branch2

在您的存储库中,您可以使用三个不同的名称,它们都使用不同的“最后一次提交”:名称master 选择提交H,名称@987654378 @选择提交J,名称branch2选择提交L

请注意,通过并包括H 的提交都在所有三个分支上,即使Hmaster 上的最后提交。提交 I-J 仅适用于 branch1,提交 K-L 仅适用于 branch2

Git 允许您随时移动分支名称。一些命名动作比其他的“更特别”。当您选择要使用的分支(使用git checkout)时,Git 必须将该提交的文件复制到它自己的索引中——以便它们准备好进入新的提交——以及你的工作树;6 但一旦完成,添加 new 提交会使当前 name 指向 新提交。所以这是一个非常常见的分支增长方式:你检查它,然后添加新的提交。但名称不会自行移动:您必须首先实际进行新提交,或者使用 Git 的 change the commit selected by a branch name 命令之一。


6这种“从提交中复制文件”更新 Git 的索引和您的工作树,通常要求索引和您的工作树都是“干净的”。 clean 这个词在这里的定义不是很好,有时你可以切换分支,即使它们提到了不同的提交,没有一个干净的索引/暂存区域和/或工作树。有关更多信息,请参阅Checkout another branch when there are uncommitted changes on the current branch

【讨论】:

  • 我回到分支recpieints,提交并返回master。有效!!谢谢。
  • 是的:这创建了一个新的提交,这使得当时的当前分支名称提前包含该新提交。新提交指向旧提交(仍然是master 的提示),因此新提交将master-tip-commit 作为其父提交。提交图的绘图将显示这一点。另见Pretty git branch graphs
猜你喜欢
  • 2011-10-01
  • 2012-04-17
  • 1970-01-01
  • 1970-01-01
  • 2011-12-20
  • 2020-03-05
  • 2020-12-02
  • 2013-05-04
  • 2017-02-15
相关资源
最近更新 更多