GITLens 切换到提交使我的新提交消失了
GITLens switch to commit made my newer commits disappear
现在非常可怕的情况:我已经使用 VSCode
的 GitLens
扩展跳回到旧的提交。我想 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 答案,例如 master
或 main
或其他任何答案,另一个给出原始提交哈希 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
从那里继续前进,向后。提交 G
和 H
在哪里?他们不见了!
不过没关系:如果我们 运行 git log main
,git log
以名称 main
开头,而不是名称 HEAD
。找到提交 H
,git 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 checkout
或 git switch
:
...--G--H <-- develop (HEAD), main
请注意,我们仍在使用 commit H
。我们现在只是通过另一个 name 使用它。通过并包括 H
的提交是 在两个分支上 。
我们现在进行新的提交,这是我们在 Git 中的常用方式。准备就绪后,我们 运行 git commit
并提供 Git 一条日志消息以放入 元数据 以用于新提交。 Git现在:
- 保存每个文件的快照(de-duplicated 照常);
- 使用当前提交作为新提交的parent,这样我们的新提交——我们的ll call
I
——将向后指向现有提交 H
;
- 添加我们配置的
user.name
和 user.email
作为这个新提交的作者和提交者,使用“现在”作为 date-and-time;
- 使用我们的日志消息;和
- 实际上将所有这些作为提交写出,并为其分配了唯一的哈希 ID。 (唯一性部分来自 date-and-time 标记,部分来自输入哈希 ID
H
,部分来自我们保存的快照:everything 在新提交中组成新的 random-looking 哈希 ID,这就是我们无法预测它的原因。)
所以现在我们有了这个新的提交 I
,指向现有的提交 H
:
...--G--H
\
I
现在 Git 做了另一个使它全部工作的魔法:git commit
将 I
的哈希 ID 写入当前分支名称。即Git使用HEAD
找到当前分支的name,更新存储在的hash ID该分支名称。所以我们现在的照片是:
...--G--H <-- main
\
I <-- develop (HEAD)
名称HEAD
仍然附加到分支名称develop
,但分支名称develop
现在select提交I
,而不是提交H
.
提交 I
导致返回提交 H
。 name 只是让我们找到 commit。 commits 才是真正重要的:分支名称只是为了让我们找到 last 提交。无论该分支名称中的哈希 ID 是什么,Git 表示 that commit is the last 在该分支上提交。因此,由于 main
现在说 H
,H
是 last 在 main
上的提交;因为 develop
现在说 I
,所以 I
是 last 在 develop
上的提交。 H
之前的提交 仍在两个分支 上,但 I
仅在 develop
.
上
稍后,如果我们愿意,我们可以Git 移动名称main
。一旦我们将 main
移动到 I
:
...--G--H--I <-- develop, main
然后所有提交再次出现在两个分支上。 (这次我省略了 HEAD
因为我们可能不关心我们在哪个分支上,如果两个 select I
。事实上,我们可以删除任何一个名称 - 但不能同时删除 -因为两个名称select相同的提交,这就是我们需要找到正确哈希ID的全部。如果我们要写这个哈希ID 在某处,我们可能不需要 任何 名称。但那将是......充其量是令人讨厌的。我们有一台 计算机; 让我们它 为我们保存大丑陋的哈希 ID,用漂亮整洁的名字。)
现在非常可怕的情况:我已经使用 VSCode
的 GitLens
扩展跳回到旧的提交。我想 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 答案,例如 master
或 main
或其他任何答案,另一个给出原始提交哈希 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
从那里继续前进,向后。提交 G
和 H
在哪里?他们不见了!
不过没关系:如果我们 运行 git log main
,git log
以名称 main
开头,而不是名称 HEAD
。找到提交 H
,git 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 checkout
或 git switch
:
...--G--H <-- develop (HEAD), main
请注意,我们仍在使用 commit H
。我们现在只是通过另一个 name 使用它。通过并包括 H
的提交是 在两个分支上 。
我们现在进行新的提交,这是我们在 Git 中的常用方式。准备就绪后,我们 运行 git commit
并提供 Git 一条日志消息以放入 元数据 以用于新提交。 Git现在:
- 保存每个文件的快照(de-duplicated 照常);
- 使用当前提交作为新提交的parent,这样我们的新提交——我们的ll call
I
——将向后指向现有提交H
; - 添加我们配置的
user.name
和user.email
作为这个新提交的作者和提交者,使用“现在”作为 date-and-time; - 使用我们的日志消息;和
- 实际上将所有这些作为提交写出,并为其分配了唯一的哈希 ID。 (唯一性部分来自 date-and-time 标记,部分来自输入哈希 ID
H
,部分来自我们保存的快照:everything 在新提交中组成新的 random-looking 哈希 ID,这就是我们无法预测它的原因。)
所以现在我们有了这个新的提交 I
,指向现有的提交 H
:
...--G--H
\
I
现在 Git 做了另一个使它全部工作的魔法:git commit
将 I
的哈希 ID 写入当前分支名称。即Git使用HEAD
找到当前分支的name,更新存储在的hash ID该分支名称。所以我们现在的照片是:
...--G--H <-- main
\
I <-- develop (HEAD)
名称HEAD
仍然附加到分支名称develop
,但分支名称develop
现在select提交I
,而不是提交H
.
提交 I
导致返回提交 H
。 name 只是让我们找到 commit。 commits 才是真正重要的:分支名称只是为了让我们找到 last 提交。无论该分支名称中的哈希 ID 是什么,Git 表示 that commit is the last 在该分支上提交。因此,由于 main
现在说 H
,H
是 last 在 main
上的提交;因为 develop
现在说 I
,所以 I
是 last 在 develop
上的提交。 H
之前的提交 仍在两个分支 上,但 I
仅在 develop
.
稍后,如果我们愿意,我们可以Git 移动名称main
。一旦我们将 main
移动到 I
:
...--G--H--I <-- develop, main
然后所有提交再次出现在两个分支上。 (这次我省略了 HEAD
因为我们可能不关心我们在哪个分支上,如果两个 select I
。事实上,我们可以删除任何一个名称 - 但不能同时删除 -因为两个名称select相同的提交,这就是我们需要找到正确哈希ID的全部。如果我们要写这个哈希ID 在某处,我们可能不需要 任何 名称。但那将是......充其量是令人讨厌的。我们有一台 计算机; 让我们它 为我们保存大丑陋的哈希 ID,用漂亮整洁的名字。)