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