GITLens 切换到提交使我的新提交消失了

GITLens switch to commit made my newer commits disappear

现在非常可怕的情况:我已经使用 VSCodeGitLens 扩展跳回到旧的提交。我想 checkout 提交,将其定位在 COMMITS 侧边栏中,右键单击并选择 Switch to Commit...。我确实希望检出那个提交,然后能够检出回到我当前的状态。

现在 运行 git log 仅向我显示直到我选择的提交点为止的提交日志。这很可怕。我的新提交在哪里?

就像现在一样,我无法找到我的新提交并返回它们。在切换到旧提交之前我做了一个新提交,所以我 100% 确定应该有更新的提交。 这是一个新项目,我还没有承诺任何远程位置,所以 git pull 不能保释我。

我真的希望有人能帮助我,我不想浪费 2 天的工作...

好的,克服我最初的恐慌帮助我找到了这个解决方案:

git 的 reflog 存储了在 git 中所做更改的所有信息。

Runnin git reflog 给了我这条线:

e7aaac3 HEAD@{3}: commit: Some Commit...

有了这个我可以 git checkout HEAD@{3} 这使我进入了最新的提交(但在尝试检索提交之前我已经移动了 HEAD )。 现在为了在不使用 reset 命令的情况下将 HEAD 干净地返回到此提交,我确实创建了一个新分支,然后将该分支合并回 main.

对于 Git 的新手来说,这个 可怕的。但别担心:所有的提交都还在。

各种 GUI,包括 Visual Studio,会阻止对 Git 的访问(这可能是好事也可能是坏事,具体取决于您的观点),因此您看不到真正发生的事情,而且我不 使用 这些 GUI,因为它们使您无法看到正在发生的事情,所以我不能准确地说出您的 GUI 中每个点击按钮的作用。 Git,然而,工作是这样的:

  • 始终存在1一个当前提交。 Git 为这次提交有一个特殊的名字:HEAD,全部大写,就像这样。2

  • 最多次,还有一个当前分支。 Git 有一个特殊名称,您可以通过它访问当前分支:HEAD.

此时你可能——事实上,你应该——object:我们如何知道 HEAD 是指 commit 还是分行名称? Git的回答是:我现在想选哪个就选哪个吧。一些事情需要一个分支名称,在这种情况下,HEAD变成分支名称。有些东西需要一个提交,在这种情况下HEAD变成提交。基本上有两种内部方式 Git 必须询问 现在的 HEAD 是什么 。一个给出 branch-name 答案,例如 mastermain 或其他任何答案,另一个给出原始提交哈希 ID。

好的,考虑到这一点,我们现在记得 git log 打印出这样的日志:

commit eb27b338a3e71c7c4079fbac8aeae3f8fbb5c687 (...)
Author: ...
   ...

commit fe3fec53a63a1c186452f61b0e55ac2837bf18a1
...

也就是说,我们看到所有这些奇怪的哈希 ID 溢出,一次一个。哈希 ID 是每个提交的实际 true-names。每个提交都有一个 globally-unique 哈希 ID:不允许两个 不同的 提交 ever 具有相同的提交。这就是哈希 ID 如此大且丑陋的原因。它们 看起来 随机。它们实际上不是随机的,但它们 不可预测。3

分支名称 main 转换为提交哈希 ID。原始哈希 ID 已经 哈希 ID。无论哪种方式,给定正确的哈希 ID,Git 都可以找到提交。

每个提交都包含每个文件的完整快照,4 加上一些 元数据: 关于提交本身的信息,例如谁做了它,什么时候,以及他们当时可以写的日志消息。对于 Git 本身至关重要,此元数据中的一项是 上一次提交的原始哈希 ID

这里还有一个关于提交的随机事实需要记住:一旦提交,任何提交的任何部分都不能更改.这就是哈希 ID 的实际工作方式,这对于 Git 成为 分布式 版本控制系统至关重要。但这也意味着没有 Git 提交可以包含其未来 children 提交的原始哈希 ID,因为我们不知道当我们创建提交。提交 可以 存储它们 parent 的“名称”(哈希 ID),因为我们 在创建 时知道它们的祖先children.

这对我们来说意味着 commits 记住他们的 parents,这形成了一种 backwards-looking链。我们所要做的就是记住 latest 提交的原始哈希 ID。当我们这样做时,我们最终会得到一条可以像这样绘制的链:

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

在这里,name main 保存了 最新提交 的真实哈希 ID,为了绘图的目的,我们只需调用 H。提交 H 依次持有早期提交 G 的哈希 ID,后者持有 still-earlier 提交 F 的哈希 ID,依此类推。

我们现在可以看到 git log 是如何工作的:它从 当前提交开始 H,由 select 编辑 当前分支main。要使 main 成为 当前分支,我们 附加 特殊名称 HEAD 到名称 main :

...--F--G--H   <-- main (HEAD)

Git 使用 HEAD 找到 main,使用 main 找到 H,并向我们展示 H。然后 Git 使用 H 找到 G 并显示给我们 G;然后它使用 G 来查找 F,依此类推。

当我们想查看任何历史提交时,我们通过哈希ID将其挑出,并告诉Git:附加HEAD 直接到该提交 。我们可以这样画:

...--F   <-- HEAD
      \
       G--H   <-- main

当我们现在 运行 git log 时,Git 将 HEAD 转换为哈希 ID——这次它直接找到了;没有附加 分支名称 — 并向我们显示提交 F。然后git log从那里继续前进,向后。提交 GH 在哪里?他们不见了!

不过没关系:如果我们 运行 git log main,git log 以名称 main 开头,而不是名称 HEAD。找到提交 Hgit log 显示;然后 git log 移动到 G,依此类推。或者,我们甚至可以 运行:

git log --branches

或:

git log --all

git log找到所有分支所有参考(“参考”包括分支和标签,但也包括其他各种名称)。

(这引出了另一个单独的 can-of-worms,这是关于 git log 如何处理“希望”“同时”显示多个提交的情况。我赢了在这个答案中根本就没有去过那里。)

这种“查看历史提交”模式在 Git 中称为 分离 HEAD 模式。那是因为特殊名称 HEAD 不再 附加到 分支名称。要 re-attach 您的 HEAD,您只需选择一个分支名称,使用 git checkout 或(Git 2.23 或更高版本)git switch:

git switch main

例如。您现在已经检查了分支名称 main selects 和 HEAD 现在 re-attached 到名称 main.[=125 的提交=]

在我们停下来之前,还有一件非常重要的事情需要学习,那就是:树枝如何生长。但是让我先把脚注去掉。


1这个规则有一个例外,在一个完全没有提交的新的、完全空的存储库中是必需的。稍后可以在 non-empty 存储库中以一种奇怪的方式使用该异常。不过你不会用到这个。

2小写变体 head 通常在 Windows 和 macOS 上“有效”(但在 Linux 和其他系统上无效) .但是,这是具有欺骗性的,因为如果您开始使用 git worktree 功能,head(小写) 不会 正常工作——它有时会让您错误提交!—而 HEAD(大写)是。如果您不喜欢输入 all-caps,请考虑使用 shorthand @ 字符,您可以使用它来代替 HEAD.

3Git 在这里使用加密散列:与加密货币中发现的相同类型的东西,虽然没有那么严格(Git 目前仍然使用 SHA -1,这在密码学术语中已经过时了)。

4快照以特殊的read-only、Git-only、压缩和de-duplicated格式存储。 Git 显示 提交为“自上次提交以来的更改”,但 stores 提交为快照。


Git 树枝如何生长

假设我们有以下情况:

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

我们现在想做一个新提交,但我们想把它放在新分支上。所以我们首先作为 Git 创建一个新的分支名称,并将该名称也指向提交 H:

git branch develop

这导致:

...--G--H   <-- develop, main (HEAD)

现在我们选择 develop 作为名字 HEAD attached-to, git checkoutgit switch:

...--G--H   <-- develop (HEAD), main

请注意,我们仍在使用 commit H。我们现在只是通过另一个 name 使用它。通过并包括 H 的提交是 在两个分支上

我们现在进行新的提交,这是我们在 Git 中的常用方式。准备就绪后,我们 运行 git commit 并提供 Git 一条日志消息以放入 元数据 以用于新提交。 Git现在:

  • 保存每个文件的快照(de-duplicated 照常);
  • 使用当前提交作为新提交的parent,这样我们的新提交——我们的ll call I——将向后指向现有提交 H;
  • 添加我们配置的 user.nameuser.email 作为这个新提交的作者和提交者,使用“现在”作为 date-and-time;
  • 使用我们的日志消息;和
  • 实际上将所有这些作为提交写出,并为其分配了唯一的哈希 ID。 (唯一性部分来自 date-and-time 标记,部分来自输入哈希 ID H,部分来自我们保存的快照:everything 在新提交中组成新的 random-looking 哈希 ID,这就是我们无法预测它的原因。)

所以现在我们有了这个新的提交 I,指向现有的提交 H:

...--G--H
         \
          I

现在 Git 做了另一个使它全部工作的魔法:git commitI 的哈希 ID 写入当前分支名称。即Git使用HEAD找到当前分支的name更新存储在的hash ID该分支名称。所以我们现在的照片是:

...--G--H   <-- main
         \
          I   <-- develop (HEAD)

名称HEAD仍然附加到分支名称develop,但分支名称develop现在select提交I,而不是提交H.

提交 I 导致返回提交 Hname 只是让我们找到 commitcommits 才是真正重要的:分支名称只是为了让我们找到 last 提交。无论该分支名称中的哈希 ID 是什么,Git 表示 that commit is the last 在该分支上提交。因此,由于 main 现在说 HHlastmain 上的提交;因为 develop 现在说 I,所以 Ilastdevelop 上的提交。 H 之前的提交 仍在两个分支 上,但 I 仅在 develop.

稍后,如果我们愿意,我们可以Git 移动名称main。一旦我们将 main 移动到 I:

...--G--H--I   <-- develop, main

然后所有提交再次出现在两个分支上。 (这次我省略了 HEAD 因为我们可能不关心我们在哪个分支上,如果两个 select I。事实上,我们可以删除任何一个名称 - 但不能同时删除 -因为两个名称select相同的提交,这就是我们需要找到正确哈希ID的全部。如果我们要写这个哈希ID 在某处,我们可能不需要 任何 名称。但那将是......充其量是令人讨厌的。我们有一台 计算机; 让我们 为我们保存大丑陋的哈希 ID,用漂亮整洁的名字。)