Git-reset命令
Git的三棵树
在了解reset
特性之前,先了解 Git 的三棵树含义。 “树” 在我们这里的实际意思是 “文件的集合”,而不是指特定的数据结构。
Git 作为一个系统,是以它的一般操作来管理并操纵这三棵树的:
HEAD
HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 通常,理解 HEAD 的最简方式,就是将它看做 该分支上的最后一次提交 的快照。
Index(索引)
索引是你的 预期的下一次提交。 我们也会将这个概念引用为 Git 的“暂存区”,这就是当你运行 git commit
时 Git 看起来的样子。
Working Directory(工作目录)
工作目录(通常也叫 工作区)。 另外两棵树以一种高效但并不直观的方式,将它们的内容存储在 .git
文件夹中。 工作目录会将它们解包为实际的文件以便编辑。 你可以把工作目录当做 沙盒。在你将修改提交到暂存区并记录到历史之前,可以随意更改。
Git的工作流程
经典的 Git 工作流程是通过操纵这三个区域来以更加连续的状态记录项目快照的。
这里面的三个操作含义分别如下:
- Stage Files:通过
git add
暂存文件到Index,便于下一次提交。 - Commit:提交Index中的内容,并将其保存为一个永久的快照,然后创建一个指向该快照的提交对象。HEAD指针也会在提交完后指向该快照。
- Checkout the project:在本图示例中,它表示提交完后,HEAD指针指向当前工作区的快照。此外,Git有一个git checkout`操作,可以将HEAD指针指向指定的分支,从而将工作区的内容切换到指定分支的对应的索引,再将索引的内容拷贝到工作区。
实际的可视化过程如下:
假设现在有一个新目录,其中有一个文件(file.txt)。将该文件称为v1版本,并标记为蓝色。在这个目录路径下现在运行git init
,这会创建一个Git仓库,其中HEAD引用指向未创建的master
分支。
此时,只有工作目录有内容。
现在我们想要提交这个文件,所以用 git add
来获取工作目录中的内容,并将其复制到索引中。
接着运行 git commit
,它会取得索引中的内容并将它保存为一个永久的快照, 然后创建一个指向该快照的提交对象,最后更新 master
来指向本次提交。
此时如果我们运行 git status
,会发现没有任何改动,因为现在三棵树完全相同。
现在我们想要对文件进行修改然后提交它。 我们将会经历同样的过程;首先在工作目录中修改文件。 我们称其为该文件的 v2 版本,并将它标记为红色。
如果现在运行 git status
,我们会看到文件显示在 “Changes not staged for commit” 下面并被标记为红色,因为该条目在索引与工作目录之间存在不同。 接着我们运行 git add
来将它暂存到索引中。
此时,由于索引和 HEAD 不同,若运行 git status
的话就会看到 “Changes to be committed” 下的该文件变为绿色 ——也就是说,现在预期的下一次提交与上一次提交不同。 最后,我们运行 git commit
来完成提交。
现在运行 git status
会没有输出,因为三棵树又变得相同了。
切换分支或克隆的过程也类似。 当检出一个分支时,它会修改 HEAD 指向新的分支引用,将 索引 填充为该次提交的快照, 然后将 索引 的内容复制到 工作目录 中。
至此,Git工作流程的可视化过程就大致描述完了,通过图形的方式,更加直观的展示了一个文件在Git仓库下经过 Working Directory
-> Index
-> HEAD
的过程。
Git的reset
reset
即为重置的意思,它主要可以做三件事:
- 移动HEAD指针(--soft)
- 更新索引(--mixed)
- 更新工作目录(--hard)
git reset
的语法:git reset [--mixed | --soft | --hard] [<commit>]
--soft
:表示只移动HEAD指针。
--mixed
:表示移动HEAD指针并更新索引。
--hard
:表示移动HEAD指针,并更新索引和工作目录。
git reset
默认情况下是--mixed
级别。也就是说执行该命令后,他会移动HEAD指针并更新索引。
<commit>
表示想要移动的版本,可以使用版本号的简写(例如9e5e6a4
这种形式),也可以使用HEAD~
形式。
HEAD~
表示上一个版本,HEAD~2
表示上二个版本,以此类推。。。
接下来,解释下何为移动HEAD指针,何为更新索引,何为更新工作目录。
假设,现在有一个git仓库,它现有两个提交版本,如下:
它的工作区是干净的,如下:
现在针对上诉情况,各自执行--soft
、--mixed
、--hard
三种参数。
第一种:执行git reset --soft HEAD~
,再看git log
和git status
:
通过git log
发现,HEAD指针已经发生了变更。
通过git status
发现,所有文件记录还是处于已暂存状态,表示还存在于索引中。
第二种:执行git reset --mixed HEAD~
,再看git log
和git status
:
通过git log
发现,HEAD指针已经发生了变更。
通过git status
发现,forgotten_file文件处于已变更但未暂存状态,而version文件处于未跟踪状态(因为它在上一次提交中属于新文件)。这种情况,表示重置到了没有执行git add
之前,文件记录已经从索引中回退。
第二种:执行git reset --hard HEAD~
,再看git log
和git status
:
通过git log
发现,HEAD指针已经发生了变更。
通过git status
发现,工作目录下的文件都没有发生变更,表示所有文件记录都回退到了上一个版本,当前版本变更的文件内容都已消失。所以--hard
是一个危险操作!
Git reset <pathspec>
如果git reset
指定了一个作用路径(pathspec),那么他就会跳过第一步(移动HEAD指针),只在指定作用路径下重置文件记录。
语法为:git reset [<tree-ish>] [--] <pathspec>
<tree-ish>
表示分支版本,如果不指定,默认为HEAD,即当前版本。
[--]
表示重置操作,默认为--mixed。不能指定为--soft和--hard。
<pathspec>
表示作用路径,可以是具体文件,也可以是目录,也可以使用表达式。
示例:
-
git reset file.txt
其实就是git reset --mixed HEAD file.txt
的简写,表示将file.txt
文件取消暂存。与它相反的是git add file.txt
,表示将file.txt
暂存。在后期Git版本中,推荐使用
git restore --staged <file>
用于取消暂存。 -
git reset c54a forgotten_file
表示将forgotten_file
文件重置到c54a
这个版本,而将这个文件在c54a
版本之后的改动标记为未暂存。
git reset与git checkout的区别
总结:git reset
和git checkout
命令都具有修改HEAD指针指向的功能,只不过git reset
是在当前分支移动HEAD指针,而git checkout
是切换HEAD指针到指定分支。
和 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 自身。
带路径
运行 checkout
的另一种方式就是指定一个文件路径,这会像 reset
一样不会移动 HEAD。 它就像 git reset [branch] file
那样用该次提交中的那个文件来更新索引,但是它也会覆盖工作目录中对应的文件。 它就像是 git reset --hard [branch] file
(如果 reset
允许你这样运行的话), 这样对工作目录并不安全,它也不会移动 HEAD。
此外,同 git reset
和 git add
一样,checkout
也接受一个 --patch
选项,允许你根据选择一块一块地恢复文件内容。