-
A1. 附录 A:其他环境中的 Git
- A1.1 图形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ/PyCharm/WebStorm/PhpStorm/RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 总结
-
A2. 附录 B:将 Git 嵌入你的应用程序
-
A3. 附录 C:Git 命令
7.7 Git 工具 - 重识重置
重识重置
在继续讨论更专业的工具之前,让我们先谈谈 Git 的 reset
和 checkout
命令。当您第一次遇到这些命令时,它们是 Git 中最令人困惑的部分之一。它们的功能非常多,以至于似乎无法真正理解它们并正确地使用它们。为此,我们建议使用一个简单的隐喻。
三棵树
思考 reset
和 checkout
的一种更简单的方法是,将 Git 视为三种不同树的内容管理器。“树”在这里指的是“文件的集合”,而不是特定的数据结构。在某些情况下,索引并不完全像树一样工作,但就目前而言,以这种方式思考它更容易。
Git 作为一个系统,在其正常操作中管理和操作三棵树
树 | 角色 |
---|---|
HEAD |
最后一次提交的快照,下一个父节点 |
索引 |
提议的下一个提交快照 |
工作目录 |
沙盒 |
HEAD
HEAD 是指向当前分支引用的指针,该引用又指向该分支上最后一次提交。这意味着 HEAD 将是创建的下一个提交的父节点。通常最简单的方法是将 HEAD 视为**该分支上您最后一次提交的快照**。
事实上,查看该快照的样子非常容易。以下是如何获取 HEAD 快照中每个文件的实际目录列表和 SHA-1 校验和的示例
$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon 1301511835 -0700
committer Scott Chacon 1301511835 -0700
initial commit
$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152... README
100644 blob 8f94139338f9404f2... Rakefile
040000 tree 99f1a6d12cb4b6f19... lib
Git 的 cat-file
和 ls-tree
命令是“底层”命令,用于更低级的操作,在日常工作中很少使用,但它们有助于我们了解这里发生了什么。
索引
索引是您的**提议的下一个提交**。我们也一直将此概念称为 Git 的“暂存区”,因为这是您运行 git commit
时 Git 查看的内容。
Git 会将索引填充为一个列表,其中包含上次检出到工作目录的所有文件内容,以及它们最初检出时的样子。然后,您用这些文件的新版本替换其中一些文件,git commit
会将其转换为新提交的树。
$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README
100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rb
同样,这里我们使用的是 git ls-files
,它更像是一个幕后命令,向您展示当前索引的样子。
索引在技术上并不是树形结构——它实际上是作为扁平化的清单实现的——但就我们的目的而言,它足够接近。
工作目录
最后,您有您的工作目录(也常称为“工作树”)。另外两棵树以一种高效但不便的方式存储其内容,位于 .git
文件夹内。工作目录将它们解压成实际文件,这使得您更容易编辑它们。可以将工作目录视为一个沙盒,您可以在其中尝试更改,然后再将它们提交到暂存区(索引),然后提交到历史记录。
$ tree
.
├── README
├── Rakefile
└── lib
└── simplegit.rb
1 directory, 3 files
工作流程
Git 的典型工作流程是通过操作这三棵树,记录项目在连续改进状态下的快照。
让我们可视化这个过程:假设您进入一个新目录,其中包含一个文件。我们将此文件称为v1,并以蓝色表示。现在我们运行 git init
,它将创建一个 Git 仓库,其中包含一个指向未出生 master
分支的 HEAD 引用。
此时,只有工作目录树包含任何内容。
现在我们想提交此文件,因此我们使用 git add
将工作目录中的内容复制到索引中。
git add
时将文件复制到索引然后我们运行 git commit
,它获取索引的内容并将其保存为永久快照,创建一个指向该快照的提交对象,并将 master
更新为指向该提交。
git commit
步骤如果我们运行 git status
,我们将看不到任何更改,因为所有三个树都是相同的。
现在我们想更改该文件并提交它。我们将经历相同的过程;首先,我们在工作目录中更改文件。我们将此文件称为v2,并以红色表示。
如果我们现在运行 git status
,我们将看到该文件以红色显示为“Changes not staged for commit”,因为该条目在索引和工作目录之间存在差异。接下来,我们对其运行 git add
以将其暂存到我们的索引中。
此时,如果我们运行 git status
,我们将看到该文件以绿色显示在“Changes to be committed”下,因为索引和 HEAD 不同——也就是说,我们提出的下一个提交现在与我们的上一次提交不同。最后,我们运行 git commit
以完成提交。
git commit
步骤现在 git status
将不会输出任何内容,因为所有三个树再次相同。
切换分支或克隆会经历类似的过程。当您检出一个分支时,它会更改HEAD 以指向新的分支引用,用该提交的快照填充您的索引,然后将索引的内容复制到您的工作目录中。
重置的作用
在这样的上下文中,reset
命令更有意义。
为了这些示例的目的,假设我们再次修改了 file.txt
并第三次提交了它。所以现在我们的历史记录如下所示
现在让我们详细了解一下当您调用 reset
时它所做的操作。它以一种简单且可预测的方式直接操作这三棵树。它最多执行三个基本操作。
步骤 1:移动 HEAD
reset
将执行的第一件事是移动 HEAD 指向的内容。这与更改 HEAD 本身(这是 checkout
所做的)不同;reset
移动 HEAD 指向的分支。这意味着如果 HEAD 设置为 master
分支(即您当前位于 master
分支上),运行 git reset 9e5e6a4
将首先使 master
指向 9e5e6a4
。
无论您调用哪种形式的带有提交的 reset
,这都是它始终会尝试做的第一件事。对于 reset --soft
,它将在此处停止。
现在花点时间查看该图并了解发生了什么:它基本上撤消了上一个 git commit
命令。当您运行 git commit
时,Git 会创建一个新的提交并将 HEAD 指向的分支移到该提交。当您 reset
回到 HEAD~
(HEAD 的父级)时,您正在将分支移回其所在位置,而不会更改索引或工作目录。您现在可以更新索引并再次运行 git commit
以完成 git commit --amend
将执行的操作(请参阅 更改上次提交)。
步骤 2:更新索引(--mixed
)
请注意,如果您现在运行 git status
,您将看到索引与新的 HEAD 之间的差异以绿色显示。
reset
将执行的下一件事是使用 HEAD 现在指向的任何快照的内容更新索引。
如果您指定 --mixed
选项,reset
将在此处停止。这也是默认值,因此如果您根本没有指定任何选项(在这种情况下仅为 git reset HEAD~
),命令将在此处停止。
现在再次花点时间查看该图并了解发生了什么:它仍然撤消了您的上一次 commit
,但也取消暂存了所有内容。您回滚到运行所有 git add
和 git commit
命令之前。
步骤 3:更新工作目录(--hard
)
reset
将执行的第三件事是使工作目录看起来像索引。如果您使用 --hard
选项,它将继续到此阶段。
因此,让我们考虑一下刚刚发生了什么。您撤消了上一次提交、git add
和 git commit
命令,以及您在工作目录中所做的所有工作。
需要注意的是,此标志(--hard
)是使 reset
命令变得危险的唯一方法,也是 Git 实际上会破坏数据的极少数情况之一。其他任何 reset
调用都可以很容易地撤消,但 --hard
选项却无法撤消,因为它会强制覆盖工作目录中的文件。在这种特定情况下,我们仍然在 Git 数据库中的一个提交中拥有文件的v3版本,我们可以通过查看我们的 reflog
来找回它,但如果我们没有提交它,Git 仍然会覆盖该文件,并且将无法恢复。
回顾
reset
命令按特定顺序覆盖这三棵树,并在您告诉它停止时停止
-
移动 HEAD 指向的分支(如果为
--soft
,则在此处停止)。 -
使索引看起来像 HEAD(除非为
--hard
,否则在此处停止)。 -
使工作目录看起来像索引。
带路径的重置
这涵盖了 reset
在其基本形式下的行为,但您也可以为其提供要作用于的路径。如果您指定路径,reset
将跳过步骤 1,并将其余操作限制在特定文件或一组文件上。这实际上是有道理的——HEAD 只是一个指针,您不能指向一个提交的一部分和另一个提交的一部分。但索引和工作目录可以被部分更新,因此 reset 继续执行步骤 2 和 3。
因此,假设我们运行 git reset file.txt
。此表单(因为您没有指定提交 SHA-1 或分支,并且您没有指定 --soft
或 --hard
)是 git reset --mixed HEAD file.txt
的简写,它将
-
移动 HEAD 指向的分支(已跳过)。
-
使索引看起来像 HEAD(在此处停止)。
所以它基本上只是将 file.txt
从 HEAD 复制到索引。
这实际上具有取消暂存文件的效果。如果我们查看该命令的图并考虑 git add
的作用,它们是完全相反的。
这就是为什么 git status
命令的输出建议您运行此命令以取消暂存文件的原因(有关这方面的更多信息,请参阅 取消暂存已暂存的文件)。
我们可以很容易地不让人们认为我们的意思是“从 HEAD 中拉取数据”,而是指定要从中拉取文件版本的特定提交。我们只需运行类似 git reset eb43bf file.txt
的命令即可。
这实际上与我们在工作目录中将文件的内容还原到v1、对其运行 git add
,然后将其还原回v3的效果相同(无需真正执行所有这些步骤)。如果我们现在运行 git commit
,它将记录一个将该文件还原到v1的更改,即使我们实际上从未在工作目录中再次拥有它。
还需要注意的是,与 git add
一样,reset
命令将接受 --patch
选项以逐块取消暂存内容。因此,您可以选择性地取消暂存或还原内容。
压缩
让我们看看如何利用这个新获得的能力做一些有趣的事情——压缩提交。
假设你有一系列提交,其消息类似于“oops.”、“WIP”和“forgot this file”。你可以使用reset
快速轻松地将它们压缩成一个提交,让你看起来非常聪明。 压缩提交展示了另一种执行此操作的方法,但在本例中,使用reset
更简单。
假设你有一个项目,其中第一个提交包含一个文件,第二个提交添加了一个新文件并修改了第一个文件,第三个提交再次修改了第一个文件。第二个提交是工作进度,你想将其压缩。
你可以运行git reset --soft HEAD~2
将HEAD分支移回较旧的提交(你想要保留的最新提交)
然后只需再次运行git commit
现在你可以看到,你的可访问历史记录(你将推送的历史记录)看起来像是你有一个包含file-a.txt
v1版本的提交,然后第二个提交同时将file-a.txt
修改为v3并添加了file-b.txt
。包含文件v2版本的提交不再存在于历史记录中。
试试看
最后,你可能想知道checkout
和reset
之间有什么区别。与reset
类似,checkout
操作三个树,并且根据你是否为命令提供文件路径而略有不同。
无路径
运行git checkout [branch]
与运行git reset --hard [branch]
非常相似,因为它们都会更新所有三个树,使其看起来像[branch]
,但有两个重要的区别。
首先,与reset --hard
不同,checkout
是工作目录安全的;它会检查是否正在删除已修改的文件。实际上,它比这更智能——它尝试在工作目录中执行一个简单的合并,因此你没有更改的所有文件都将被更新。另一方面,reset --hard
将简单地替换所有内容,而无需检查。
第二个重要区别是checkout
如何更新HEAD。reset
将移动HEAD指向的分支,而checkout
将移动HEAD本身以指向另一个分支。
例如,假设我们有master
和develop
分支,它们指向不同的提交,我们目前在develop
上(因此HEAD指向它)。如果我们运行git reset master
,develop
本身现在将指向与master
相同的提交。如果我们改为运行git checkout master
,develop
不会移动,HEAD本身会移动。HEAD现在将指向master
。
因此,在这两种情况下,我们都将HEAD移动到指向提交A,但方式却大不相同。reset
将移动HEAD指向的分支,checkout
移动HEAD本身。
git checkout
和 git reset
带路径
运行checkout
的另一种方法是使用文件路径,这与reset
一样,不会移动HEAD。它就像git reset [branch] file
一样,它使用该提交中的该文件更新索引,但它还会覆盖工作目录中的文件。这将与git reset --hard [branch] file
完全相同(如果reset
允许你运行该命令)——它不是工作目录安全的,并且不会移动HEAD。
此外,与git reset
和git add
一样,checkout
将接受--patch
选项,允许你逐块选择性地恢复文件内容。
总结
希望现在你已经理解并对reset
命令有了更多了解,但可能仍然对它与checkout
到底有何不同感到有点困惑,并且可能无法记住所有不同调用的规则。
以下是一个备忘单,说明哪些命令会影响哪些树。“HEAD”列在该命令移动HEAD指向的引用(分支)时显示“REF”,在移动HEAD本身时显示“HEAD”。请特别注意“WD 安全?”列——如果显示否,请在运行该命令之前先思考一下。
HEAD | 索引 | 工作目录 | WD 安全? | |
---|---|---|---|---|
提交级别 |
||||
|
REF |
否 |
否 |
是 |
|
REF |
是 |
否 |
是 |
|
REF |
是 |
是 |
否 |
|
HEAD |
是 |
是 |
是 |
文件级别 |
||||
|
否 |
是 |
否 |
是 |
|
否 |
是 |
是 |
否 |