git - git 有没有办法将提交的更改转储到工作树?
问题描述
我提交了一些更改,其中包含我不想提交的更改,因此我想删除该提交,但保留已提交的分阶段和非分阶段更改,以便我可以在提交之前删除不需要的更改。我使用git reset --hard <hash>
但它恢复到 HEAD - 1 处的提交,这不仅删除了提交,还删除了提交之前的所有暂存和未暂存的更改。
有没有办法重置为提交,但将所有提交的更改(返回)转储到工作树,而不是删除该提交中记录的每个更改?换句话说,我怎样才能将所有已提交的更改返回到工作树?
解决方案
首先,请注意术语索引和暂存区域的含义相同。还有第三个术语,缓存,现在主要出现在标志中(git rm --cached
例如)。这些都指的是同一个基础实体。
接下来,虽然从更改的角度考虑通常很方便,但这最终会误导您,除非您牢记这一点:Git 不存储更改,而是存储快照。我们只有在比较两个快照时才能看到变化。我们将它们并排放置,就好像我们在玩找不同的游戏一样——或者更准确地说,我们让 Git 将它们并排放置并比较它们并告诉我们有什么不同。所以现在我们看到了这两个快照之间的变化。但是 Git 没有这些变化。它有两个快照,只是比较它们。
现在我们到了真正棘手的部分。我们知道:
每个提交都有一个唯一的哈希 ID,这是 Git 查找特定提交的方式;
每个提交存储两件事:
- 它包含 Git 在您或任何人制作快照时所知道的每个文件的完整快照;和
- 它有一些元数据,包括提交者的姓名和电子邮件地址、一些日期和时间戳等等——对于 Git 来说重要的是,它有一些早期提交的原始哈希 ID,所以Git 可以从每个提交到其父提交及时回退;
并且任何提交的所有部分都将永远冻结在时间中。
所以提交存储快照,Git 可以提取这些快照供我们处理。但是 Git 不只是将提交提取到工作区域中。其他版本控制系统有:它们有提交和工作树,这就是全部,以及您需要知道的所有内容。提交的版本一直被冻结,可用的版本是可用的,并且是可变的。这是两个“活动”版本,并为我们提供了一种查看更改内容的方法:只需将活动但冻结的快照与工作快照进行比较。
但无论出于何种原因,Git 都没有这样做。相反,Git 有三个活动版本。一个活动版本一直被冻结,就像往常一样。一个活动版本在您的工作树中,就像往常一样。但是在这两个版本之间,还有第三个快照。它是可变的,但它更像是冻结的副本而不是有用的副本。
每个文件的第三个副本,位于冻结提交和可用副本之间,是Git 的索引,或者至少是您需要担心的 Git 索引的一部分。1 您需要了解 Git 的索引,因为它充当您提议的下一次提交。
也就是说,当您运行时:
git commit
Git 会做的是:
- 收集适当的元数据,包括当前提交的哈希 ID;
- 制作一个新的(虽然不一定是唯一的2)快照;
- 使用快照和元数据进行新的、唯一的提交;3
- 将新提交的哈希 ID 写入当前分支名称。
此处的最后一步将新提交添加到当前分支。上面第 2 步中的快照是此时 Git 索引中的任何内容。所以在你运行之前git commit
,你必须更新 Git 的索引。这就是 Git 让你运行的原因git add
,即使对于 Git 已经知道的文件:你并没有完全添加文件。相反,您正在覆盖索引副本。
1其余部分是 Git 的缓存,通常不会在您面前全部显示出来。您可以在不了解缓存方面的情况下使用 Git。在不了解索引的情况下,很难(甚至不可能)很好地使用 Git。
2例如,如果您进行了提交,然后将其还原,则第二次提交将重新使用您在进行第一次提交之前拥有的快照。重新使用旧快照一点也不异常。
3与源快照不同,每次提交始终是唯一的。了解为什么会出现这种情况的一种方法是每次提交都有一个日期和时间。您必须在一秒钟内进行多次提交,以免它们中的任何一个获得相同的时间戳。即使这样,这些提交也可能具有不同的快照和/或不同的父提交哈希 ID,这会使它们保持不同。获得相同哈希 ID 的唯一方法是在相同的上一次提交之后,由同一个人同时提交相同的源。4
4或者,您可能会遇到哈希 ID 冲突,但这从未真正发生过。另请参阅新发现的 SHA-1 冲突如何影响 Git?
照片
让我们绘制一些提交的图片。让我们使用大写字母代替哈希 ID。我们将沿着主线分支有一个简单的提交链,还没有其他分支:
... <-F <-G <-H
在这里,H
代表链中最后一次提交的哈希 ID。CommitH
既有快照(无论何时你或任何人提交,都从 Git 的索引中保存H
)和元数据(提交人的姓名H
等)。在元数据中, commitH
存储了较早的 commitG
的原始哈希 ID。所以我们说H
指向 G
。
当然, CommitG
也有快照和元数据。该元数据使较早的提交G
指向更早的提交F
。F
依次提交更远的点。
这一直重复到第一次提交。作为第一,它不指向回,因为它不能;所以 Git 可以停在这里。Git 只需要能够找到最后一次提交。Git 需要它的哈希 ID。你可以自己输入,但那会很痛苦。您可以将其存储在某个文件中,但这会很烦人。您可以让Git为您存储它,这会很方便 — 这正是分支名称的用途和作用:
...--F--G--H <-- main
该名称仅包含链中 最后一次提交main
的一个哈希 ID 。
无论我们有多少名称和提交,这都是正确的:每个名称都包含一些实际有效提交的哈希 ID。让我们创建一个新名称 ,feature
它也指向H
,如下所示:
...--F--G--H <-- feature, main
现在我们需要一种方法来知道我们正在使用哪个名称。Git 将特殊名称附加HEAD
到分支名称之一,如下所示:
...--F--G--H <-- feature, main (HEAD)
我们现在 "on" main
,并使用commit H
。让我们使用git switch
orgit checkout
切换到name feature
:
...--F--G--H <-- feature (HEAD), main
没有其他任何改变:我们仍在使用 commit H
。但是我们使用它是因为name feature
。
如果我们进行新的提交——我们称之为 commit——commitI
将I
指向 commit H
,Git 会将 commitI
的哈希 ID 写入当前name。这将产生:
...--F--G--H <-- main
\
I <-- feature (HEAD)
现在,如果我们git checkout main
,Git 必须交换我们的工作树内容和我们提出的下一个提交内容。因此git checkout main
将翻转 Git 的索引和我们的工作树内容,以便它们匹配 commit H
。之后,git checkout feature
将它们翻转回来,使它们都匹配 commit I
。
如果我们在 上进行新的提交J
,feature
我们会得到:
...--F--G--H <-- main
\
I--J <-- feature (HEAD)
reset
命令:这很复杂!
git reset
命令很复杂。5 我们将在这里只查看命令的“whole commit”reset 变体——带 、 和 options 的命令--hard
——--soft
而--mixed
不是那些主要执行我们现在可以git restore
在 Git 2.23 及更高版本中执行的操作的命令。
这些“整体提交”重置操作采用一般形式:
git reset [<mode-flag>] [<commit>]
mode-flag
是--soft
、--mixed
或中的一个--hard
。6 说明commit
符——可以直接是原始哈希 ID,也可以是任何其他可以转换为提交哈希 ID 的东西,通过将其提供给它——git rev-parse
告诉我们将移动到哪个提交。
该命令做了三件事,除了你可以让它提前停止:
首先,它移动
HEAD
附加的分支名称。7 它只需将新的哈希 ID 写入分支名称即可。其次,它将 Git 索引中的内容替换为您选择的提交中的内容。
第三也是最后一个,它也用它在 Git 索引中替换的内容替换了你的工作树中的内容。
第一部分——移动HEAD
——总是会发生,但是如果你选择当前提交作为新的哈希 ID,那么“移动”就是从你所在的位置到你所在的位置:有点毫无意义。仅当您让命令继续执行步骤 2 和 3,或至少执行步骤 2 时,这才有意义。但它总是会发生。
的默认值是commit
当前提交。也就是说,如果您不选择新的提交,git reset
则会选择当前提交作为要移动的地方HEAD
。因此,如果您不选择新的提交,那么您将在第 1 步中执行“原地不动”的动作。没关系,只要你不让它停在那里:如果你git reset
在第 1 步之后停下来,让它留在原地,你做了很多工作却什么也没做。这并没有错,但这是浪费时间。
所以,现在让我们看一下标志:
--soft
告诉git reset
:做动作,然后停在那里。移动之前Git 的索引中的任何内容在之后仍然在 Git 的索引中。工作树中的任何内容都保持不变。--mixed
告诉git reset
:移动,然后覆盖你的索引,但不要管我的工作树。--hard
告诉git reset
:移动,然后覆盖你的索引和我的工作树。
所以,假设我们从这个开始:
...--F--G--H <-- main
\
I--J <-- feature (HEAD)
并选择 commitI
作为git reset
应该移动的地方feature
,这样我们最终得到:
...--F--G--H <-- main
\
I <-- feature (HEAD)
\
J
请注意提交J
仍然存在,但除非我们将哈希 ID 保存在某处,否则我们无法找到它。我们可以将J
' 的哈希 ID 保存在纸上、白板上、文件中、另一个分支名称中、标签名称中或其他任何地方。任何可以让我们输入或剪切和粘贴的东西,或者任何可以做的事情。然后我们可以创建一个新的名字 find J
。我们可以在执行之前执行此操作git reset
,例如:
git branch save
git reset --mixed <hash-of-I>
会让我们:
...--F--G--H <-- main
\
I <-- feature (HEAD)
\
J <-- save
其中名称save
保留了J
的哈希 ID。
,--mixed
如果我们在这里使用它,它会告诉 Git:根本不要碰我的工作树文件! 这并不意味着您将在工作树中拥有与 commit 完全相同的文件J
,因为也许您在执行git reset
. 这--mixed
意味着 Git 将使用来自I
. 但是 Git 不会在这里碰你的文件。只有 with--hard
才会git reset
触及您的文件。
(当然,如果你运行git checkout
or git switch
: 好吧,这些命令也应该会触及你的文件,所以这又会变得更加复杂。但现在不要担心,因为我们正在专注于git reset
.)
5我个人认为git reset
太复杂了,方法是git checkout
。Git 2.23 将旧的拆分git checkout
为git switch
和git restore
. 我认为git reset
应该同样分开。但现在还没有,所以除了写这个脚注之外,抱怨没有什么意义。
6还有--merge
和--keep
模式,但它们只是我打算忽略的更复杂的情况。
7在分离的 HEAD模式下,我在这里忽略它,它只是直接写入一个新的哈希 ID HEAD
。
概括
的默认设置git reset
是不理会您的文件 ( --mixed
)。您还可以告诉 Git 不理会自己的索引,使用--soft
: 当您想要使用 Git 索引中的内容进行新提交时,这有时很有用。假设你有:
...--G--H <-- main
\
I--J--K--L--M--N--O--P--Q--R <-- feature (HEAD)
I
其中的提交Q
都只是各种实验,而你的最后一次提交——提交R
——所有内容都处于最终状态。
然后,假设您希望使用来自 的快照但在 commit 之后进行新提交,并且您希望将其称为(updated) 上的最后一次提交。你可以这样做:R
I
feature
git checkout feature # if necessary - if you're not already there
git status # make sure commit R is healthy, etc
git reset --soft main # move the branch name but leave everything else
git commit
在 之后git reset
,我们有这张图片:
...--G--H <-- feature (HEAD), main
\
I--J--K--L--M--N--O--P--Q--R ???
现在很难找到I
提交R
。但是正确的文件现在在 Git 的索引中,可以提交了,所以git commit
我们可以调用一个新的提交S
(对于“squash”):
S <-- feature (HEAD)
/
...--G--H <-- main
\
I--J--K--L--M--N--O--P--Q--R ???
如果您要将快照与 中的快照进行比较R
,S
它们将是相同的。(这是另一种情况,Git 只是重新使用现有的快照。)但是由于我们看不到commits I-J-...-R
,现在看起来好像我们已经神奇地将所有提交压缩为一个:
S <-- feature (HEAD)
/
...--G--H <-- main
与S
它的 parent相比,我们看到与比较vs时H
看到的所有相同的变化。如果我们再也见不到,那可能就好了!H
R
I-J-...-R
这git reset --soft
很方便,因为我们可以移动分支名称并保留Git 索引和工作树中的所有内容。
在其他一些情况下,我们可能想要从R
. 在这里我们可以--mixed
重置 Git 的索引:
git reset main
git add <subset-of-files>
git commit
git add <rest-of-files>
git commit
这会给我们:
S--T <-- feature (HEAD)
/
...--G--H <-- main
中的快照与 中的快照T
匹配R
,并且中的快照S
只有几个更改的文件。在这里,我们使用--mixed
重置模式来保持工作树中的所有文件完整,但重置 Git 的索引。然后我们使用git add
更新 Git 的索引以匹配我们的工作树的一部分,提交一次到 make S
,并使用git add
更新我们的工作树的其余部分并再次提交到 make T
。
所以所有这些模式都有它们的用途,但要理解这些用途,你需要了解 Git 对 Git 的索引和你的工作树做了什么。
推荐阅读
- makefile - Makefile 跟踪目标未找到但存在并运行它
- javascript - 如何正确使用任何和/或未知函数参数
- flutter - 向列表中添加新项目后,如何在颤振中更新列表?
- inno-setup - Inno 设置 - 添加动画 gif
- python - 如何在保持 .py 源代码可编辑的同时将 python 打包为 exe?
- python - range(1:len(df)) 将 NaN 分配给数据帧中的最后一行
- python - 是否有执行浏览器中打开的所有 jupyter notebook 的一键快捷方式?
- python - 使用 Python 将变量写入 Excel 中的精确单元格
- sql - 如何有效地标记 SQL 中的连续变化
- azure - 如何获取其他 Azure Active Directory 组织的列表我是使用 API 的来宾用户,例如 Graph