为了添加VonC 的答案,并将所有相关命令按字母顺序放入图片中,我将介绍:
git checkout
git reset
git restore
git switch
我还要再扔一个,名字错git revert
了。
从最终用户的角度
您只需要git checkout
、git reset
和git revert
。这些命令一直在 Git 中。
但git checkout
实际上有两种操作模式。一种模式是“安全”的:它不会意外破坏任何未保存的工作。另一种模式是“不安全的”:如果你使用它,它告诉 Git 清除一些未保存的文件,Git 假设 (a) 你知道它的意思,并且 (b) 你确实打算清除未保存的文件,所以 Git 会立即清除你未保存的文件。
这不是很友好,所以 Git 的人最终——经过多年的用户抱怨——分成git checkout
了两个新的命令。这导致我们:
从历史的角度
git restore
是新的,于 2019 年 8 月在 Git 2.23 中首次出现。 git reset
很老,一直在Git,可以追溯到2005年之前。这两个命令都有能力破坏未保存的工作。
该git switch
命令也是新的,git restore
在 Git 2.23 中引入。它实现了“安全的一半” git checkout
;git restore
实现“不安全的一半”。
你什么时候使用哪个命令?
这是最复杂的部分,要真正理解它,我们需要知道以下几项:
Git 真的是关于提交。提交存储在Git 存储库中。git push
和git fetch
命令将提交——整个提交,作为一个孤注一掷的交易 1——转移到另一个 Git。你要么拥有所有的承诺,要么你没有。其他命令,例如git merge
or git rebase
,都适用于本地提交。该pull
命令运行fetch
(以获取提交),然后是第二个命令,以便在提交位于本地时对其进行处理。
新提交添加到存储库。您几乎从不从存储库中删除提交。此处列出的五个命令中只有一个(checkout、reset、restore、revert 和 switch)能够删除提交。2
每个提交都由其哈希 ID 编号,该 ID 对于该特定提交是唯一的。它实际上是根据提交中的内容计算得出的,这就是 Git 如何使这些数字在所有 Git 中都能工作的方式。这意味着提交中的内容将永远冻结:如果您更改任何内容,您将得到一个带有新编号的新提交,而旧提交仍然存在,具有相同的旧编号。
每个提交都存储两件事:快照和元数据。元数据包括一些先前提交的哈希 ID。这使得提交形成了向后看的链。
分支名称包含一个提交的哈希 ID。这使得分支名称找到该提交,这又意味着两件事:
- 该特定提交是该分支的提示提交;和
- 导致并包括该提示提交的所有提交都在该分支上。
稍后我们还将讨论 Git 的索引以及您的工作树。它们与这些是分开的,但值得一提的是,特别是因为索引有三个名称:Git 有时称它为index,有时称它为staging area,有时——现在很少——称它为cache。这三个名字都指的是同一个东西。
我认为,通过分支名称的所有内容最好通过图片来理解(至少对于大多数人而言)。如果我们绘制一系列提交,较新的提交向右,使用o
每个提交并省略一些提交以获取空间或其他内容,我们会得到如下内容:
o--o---o <-- feature-top
/ \
o--o--o--o--...--o---o--o <-- main
\ /
o--o--...--o--o <-- feature-hull
如您所见,它是一个船库。有三个分支。主线分支包含每个 commit,包括顶行和底(外壳)行的所有提交。该feature-top
分支包含前三个提交以及沿左侧主线的三个提交,但不包含底行中的任何提交。提交之间的所有连接器都是——嗯,应该是,但我没有足够好的字体——单向箭头,指向左,或向下和向左,或向上和向左。
这些“箭头”,或从提交到提交的单向连接,在技术上是有向图中的弧或单向边。这个有向图是一个没有循环的图,使其成为有向无环图或DAG,它具有许多对 Git 有用的属性。
如果您只是使用 Git 在提交中存储文件,那么您真正关心的是圆形o
节点或顶点(同样是两个词),每个都用于存储您的文件,但您至少应该模糊知道它们是如何排列的。这很重要,尤其是因为merges。合并提交是那些具有两个输出弧的提交,向后指向 Git 所谓的两个父提交。子提交是“较晚”的提交:就像人类的父母总是比他们的孩子年长一样,Git 的父母提交也比他们的孩子年长。
不过,我们还需要一件事:新的提交从何而来? 我们注意到,提交中的内容——快照(保存所有文件)和元数据(保存 Git 保存的有关提交的其余信息)都是只读的。您的文件不仅被冻结,它们也被转换,然后转换后的数据被删除重复数据,因此即使每次提交都有每个文件的完整快照,存储库本身也保持相对较小。但这意味着提交中的文件只能被Git读取,而没有任何东西——甚至 Git 本身——可以写入给他们。它们被保存一次,并从那时起被删除重复数据。提交充当档案,几乎就像 tar 或 rar 或 winzip 或其他任何东西。
那么,要使用 Git 存储库,我们必须让 Git提取文件。这会将文件从某些提交中取出,将那些特殊的存档格式的东西变成常规的、可用的文件。请注意,Git 很可能能够存储您的计算机实际上无法存储的文件:一个典型的例子是一个名为 的文件aux.h
,对于某些 C 程序,在 Windows 机器上。我们不会详细介绍所有细节,但理论上仍然可以使用此存储库完成工作,该存储库可能构建在 Linux 系统上,即使您使用的是无法使用aux.h
直接存档。
无论如何,假设没有像 那样令人讨厌的小惊喜aux.h
,您只需运行git checkout
或从Gitgit switch
中获取一些提交。这将填充您的工作树,并从存储在某个分支的提示提交中的文件中填充它。提示提交再次是该分支上的最后一次提交,由分支名称找到。通过选择该分支名称作为当前分支,您的或选择该提交作为当前提交。您现在拥有来自该提交的所有文件,在您可以查看和处理它们的区域中:您的git checkout
git switch
工作树。
请注意,工作树中的文件实际上并不在 Git 本身中。它们只是从Git 中提取出来的。这很重要,因为当从git checkout
Git 中提取文件时,它实际上将每个文件放在两个地方。这些地方之一是您看到和处理/使用的普通日常文件。Git 将每个文件放入的另一个位置是 Git 的index。
正如我刚才提到的,索引有三个名称:索引、暂存区和缓存。所有都指的是同一件事:Git 粘贴每个文件的这些“副本”的地方。每一个实际上都是预先去重的,所以“复制”这个词有点错误,但是——不像它的其他大部分内容——Git实际上在隐藏去重方面做得很好。除非您开始使用 and 之类的内部命令git ls-files
,否则您git update-index
不需要了解这部分,并且可以将索引视为保存文件的副本,准备进入下一次提交。
这对你作为一个只使用Git 的人来说意味着 index / staging-area 作为你提议的下一次提交。当您运行时,Git 会将文件的这些git commit
副本打包为要存档在快照中的副本。您在工作树中的副本是您的;index / staging-area副本是Git 的,可以使用了。因此,如果您更改了副本并希望更改后的副本成为下一个快照中的内容,您必须告诉 Git:更新 Git 副本,在 Git 索引/暂存区域中。 你用. 3 _git add
git add
command 意味着使建议的下一个提交副本与工作树副本匹配。它是add
执行更新的命令:这是 Git 压缩和去重文件并使其准备好存档的时候,而不是在git commit
时间上。4
然后,假设您有一系列以以下结尾的提交hash-N
:
[hash1] <-[hash2] ... <-[hashN] <--branch
你运行git commit
,给它任何它需要的元数据(一个提交日志消息),你得到一个 N+1'th 提交:
[hash1] <-[hash2] ... <-[hashN] <-[hashN+1] <--branch
Git 自动更新分支名称以指向新的提交,因此它已被添加到分支中。
现在让我们看一下各种命令:
git checkout
: 这是一个大而复杂的命令。
我们已经看到了这个,或者至少看到了这个的一半。我们用它来挑选一个分支名称,因此是一个特定的提交。这种检查首先查看我们当前的提交、索引和工作树。它确保我们已经提交了所有修改过的文件,或者——这部分有点复杂——如果我们没有提交所有修改过的文件,切换到另一个分支是“安全的”。如果不安全,git checkout
则告诉您由于文件已修改而无法切换。如果安全,就会git checkout
切换;如果您不是要切换,则可以切换回去。(另请参阅当当前分支上有未提交的更改时签出另一个分支)
但是git checkout
有一个不安全的一半。假设您修改了工作树中的某个文件,例如README.md
oraux.h
或其他。你现在回头看看你改变了什么,然后想:不,那是个坏主意。我应该摆脱这种变化。我希望文件恢复原样。
要做到这一点——比如清除你的更改README.md
——你可以运行:
git checkout -- README.md
这里的--
部分是可选的。使用它是个好主意,因为它告诉 Git 后面的部分--
是文件名,而不是分支名。
假设您有一个名为的分支hello
和一个名为hello
. 有什么作用:
git checkout hello
意思是?我们是要求 Git 破坏文件 hello
以删除我们所做的更改,还是要求 Git 检查分支 hello
?为了明确这一点,您必须编写:
git checkout -- hello (clobber the file)
或者:
git checkout hello -- (get the branch)
这种情况下,存在具有相同名称的分支和文件或目录,是一种特别阴险的情况。它已经吸引了真正的用户。这就是为什么 git switch
现在存在。该git switch
命令绝不意味着破坏我的文件。它只意味着做安全的那种git checkout
。
(该git checkout
命令也已智能化,因此如果您有新命令并且运行“坏”的 ambiguous 类型git checkout
,Git 只会向您抱怨并且什么也不做。要么使用更智能的拆分命令,要么在正确的位置添加--
以选择您想要的操作。)
更准确地说,这种git checkout
理想拼写是Git 将文件从 Git 的索引复制到您的工作树的请求。这意味着破坏我的文件。您也可以运行,在其中将提交哈希 ID 5添加到命令中。这告诉 Git 从该提交中复制文件,首先复制到 Git 的索引,然后复制到您的工作树。这也意味着破坏我的文件:不同之处在于 Git 从哪里获取它正在提取的文件的副本。git checkout -- paths
git checkout tree-ish -- paths
如果您git add
在某个文件上运行并因此将其复制到 Git 的索引中,您需要从当前提交中取回它。Git索引中的副本是您编辑的副本。所以这两种形式的,带有提交哈希 ID(或 name )、可选的 和文件名,是我的文件形式的不安全破坏者。git checkout HEAD -- file
git add
git checkout
HEAD
--
git reset
: 这也是一个大而复杂的命令。
根据您的计算方式,最多有五到六种不同形式的git reset
. 我们将在这里集中讨论一个较小的子集。
git reset [ --hard | --mixed | --soft ] [ commit ]
在这里,我们要求 Git 做几件事。首先,如果我们给出一个commit
参数,例如HEAD
orHEAD~3
或一些这样的,我们选择了Git 应该重置为的特定提交。这是一种通过将提交从分支末端弹出来删除提交的命令。在此处列出的所有命令中,这是唯一删除任何提交的命令。另一个命令 - <code>git commit --amend - 具有在放置新替换时弹出最后一个提交的效果,但该命令仅限于弹出一个提交。
让我们用一张图来展示它。假设我们有:
...--E--F--G--H <-- branch
也就是说,这个名为 的分支branch
以四个提交结束,我们将按顺序调用其哈希 ID E
、F
、G
和H
。该名称branch
当前存储这些提交中最后一个的哈希 ID H
,. 如果我们使用git reset --hard HEAD~3
,我们是在告诉 Git 弹出最后三个提交。结果是:
F--G--H ???
/
...--E <-- branch
该名称branch
现在选择 commit E
,而不是 commit H
。如果我们没有写下(在纸上、在白板上、在文件中)最后三个提交的哈希 ID,它们就会变得有点难以找到。Git 确实提供了一种再次找到它们的方法,有一段时间,但大多数情况下它们似乎已经消失了。
该HEAD~3
命令的一部分是我们如何选择删除最后三个提交。它是 Git 中整个子主题的一部分,记录在gitrevisions 手册中,关于命名特定提交的方法。重置命令只需要实际提交的哈希 ID 或任何等效项,HEAD~3
这意味着返回三个第一父步骤,在这种情况下,这使我们从提交H
返回到提交E
。
--hard
部分是我们git reset
如何告诉 Git 如何处理 (a) 它的索引和 (b) 我们的工作树文件。我们在这里有三个选择:
--soft
告诉 Git:别管了。Git 将移动分支名称而不触及索引或我们的工作树。如果您git commit
现在运行,索引中(仍然)的任何内容都会进入新提交。如果索引与 commit 中的快照匹配H
,这将为您提供一个新的提交,其快照为H
,但其父级为E
,就好像所有提交F
都H
被折叠成一个新提交一样。人们通常称之为挤压。
--mixed
告诉 Git:重置你的索引,但不要管我的工作树。Git 将移动分支名称,然后将索引中的每个文件替换为新选择的 commit 中的文件。但是Git 会留下你所有的工作树文件。这意味着就 Git 而言,您可以开始git add
ing 文件以进行新的提交。您的新提交将不匹配H
,除非您使用git add
所有内容,因此这意味着您可以,例如,E+F
如果您愿意,可以构建一个新的中间提交,类似或类似的东西。
--hard
告诉 Git:重置你的索引和我的工作树。 Git 将移动分支名称,替换其索引中的所有文件,并替换工作树中的所有文件,所有这些都是一件大事。现在就好像您根本没有进行这三个提交。您不再拥有来自F
、 或G
、 或H
的文件:您拥有来自 commit 的文件E
。
请注意,如果您省略了commit
这种 (hard/soft/mixed)的部分reset
,Git 将使用HEAD
. 由于HEAD
命名当前提交(由当前分支名称选择),因此分支名称本身保持不变:它仍然选择与以前相同的提交。所以这只对--mixed
or有用--hard
,因为git reset --soft
没有提交哈希 ID,意味着不要移动分支名称,不要更改 Git 的索引,也不要触摸我的工作树。这就是这种方法git reset
可以做的三件事——移动分支名称、更改 Git 索引中的内容以及更改工作树中的内容——而你只是排除了所有这三件事。Git 什么都不做也没关系,但何必呢?
git reset [ tree-ish ] -- path
这是git reset
我们将在这里关心的另一种类型。这有点像混合重置,因为它意味着破坏文件的一些索引副本,但在这里您指定要破坏的文件。这也有点不像混合重置,因为这种git reset
永远不会移动分支名称。
相反,您选择要从某处复制的文件。某处是tree-ish
你给的;如果您不提供,则某处是HEAD
,即当前提交。这只能将建议的下一次提交中的文件恢复为它们在某些现有提交中的形式。通过默认为当前现有的提交,这种具有撤消. 6git reset -- path
git add -- path
还有其他几种形式git reset
。要了解它们的含义,请查阅文档。
git restore
: 这从git checkout
.
基本上,这与各种形式的git checkout
破坏git reset
文件(在您的工作树和/或 Git 的索引中)的作用相同。它比旧的-and-clobber-my-work 变体更智能,因为您可以在一个命令行中选择文件的来源和去向。git checkout
做你过去常做的事,你只需要跑。(在大多数情况下,您可以省略该部分,与.git checkout -- file
git restore --staged --worktree -- file
--
git checkout
git add
-whatever
做你过去常做的事,你只需要跑。也就是说,您告诉从暂存区/索引复制,这是如何操作的。git reset -- file
git restore --staged -- file
git restore
HEAD
git reset
请注意,您可以将某个现有提交中的文件复制到 Git 的索引,而无需触及该文件的工作树副本:这样做。你不能对 old这样做,但你可以对 old 这样做, as 。而且,您可以将某个现有提交中的文件复制到您的工作树,而无需触及暂存副本:这样做。重叠部分(恢复和重置)因为是新的而存在,这种恢复是有道理的;也许,理想情况下,我们应该总是在这里使用,而不是使用旧的做事方式,但 Git 试图保持向后兼容性。git restore --source commit --staged -- file
git checkout
git reset
git reset commit -- file
git restore --source commit --worktree -- file
git restore
git restore
git reset
新的能力——从任意源复制到你的工作树,而不接触 Git 的索引/暂存区副本——就是这样:新的。你以前做不到。(您之前可以运行,但这超出了我们要检查的五个命令。)git show commit:path > path
git switch
:这只是做“安全的一半” git checkout
。这就是你需要知道的一切。使用git switch
, without --force
, Git 不会覆盖你未保存的工作,即使你打错了或其他什么。旧git checkout
命令可能会覆盖未保存的工作:如果您的拼写错误将分支名称转换为文件名,例如,哎呀。
git revert
(为了完整起见,我添加了这个):这会产生一个新的 commit。新提交的重点是撤销某人在某些现有提交中所做的事情。因此,您需要命名恢复应该退出的现有提交。这个命令可能应该被命名为git backout
.
如果您退出最近的提交,这会恢复到最近的第二个快照:
...--G--H <-- branch
变成:
...--G--H--Ħ <-- branch
其中 commit Ħ
(H-bar)“撤消”提交H
,因此给我们留下了与commit相同的文件G
。但是我们不必撤消最近的提交。我们可以采取:
...--E--F--G--H <-- branch
并添加一个Ǝ
撤消的提交E
以获取:
...--E--F--G--H--Ǝ <-- branch
这可能与任何先前提交的源快照不匹配!
1 Git 正在慢慢地发展一种“部分获取”提交的工具,这样您就可以处理具有大量提交的大型存储库,而不必一次等待整个提交。现在这不是普通用户会看到的,而当它涉及到普通用户时,它意味着作为提交的基本“全有或全无”模式的附加组件。它将把这从“你要么有一个提交,要么没有”变成“你有一个提交——要么全部,要么部分承诺很快交付其余部分——或者没有;如果你有一部分提交,您可以使用该部件,但仅此而已”。
2即使那样,“已删除”的提交还没有消失:您可以将其取回。不过,这个答案不会涵盖如何做到这一点。另外,git commit --amend
是一个特殊情况,我们会提到,但在这里并没有真正涵盖。
3要从工作树和Git 的索引中删除文件,您可以使用git rm
. 如果您从工作树中删除文件,然后git add
在该文件名上运行,Git 将“添加”删除,因此也可以。
4如果您使用git commit -a
,届时 Git 将git add
在所有文件上运行。这是以一种棘手的方式完成的,可能会破坏一些编写不佳的预提交钩子。我建议学习两步过程,部分原因是那些写得很糟糕的钩子——尽管我会尽量避免或修复它们——部分原因是如果你试图避免像那些作者那样学习 Git 的索引写得不好的钩子确实如此,Git 以后会给你带来更多麻烦。
5这是一个树而不是提交的原因是你可以使用任何指定一些现有的内部 Git树对象的东西。但是,每个提交都有一个保存的快照,它适用于此处,并且是您通常放在这里的内容。
6与所有其他 Git 命令一样,您可以--
在命令和路径之间使用add
添加。这实际上是一个好习惯,因为这意味着你可以添加一个名为的路径-u
,如果你有这样的路径:git add -- -u
意味着添加名为的文件-u
,但git add -u
根本不意味着。当然,与名称与分支名称匹配的文件相比,名称与选项序列匹配的文件不太常见,也不令人惊讶:拥有一个dev
分支和一组名为dev/whatever
. 由于文件路径将使用目录匹配,对于添加、签出、重置和恢复,这些可能会混淆。该add
命令不采用分支名称不过,因此在这方面更安全。