Git的三棵树

在了解reset特性之前,先了解 Git 的三棵树含义。 “树” 在我们这里的实际意思是 “文件的集合”,而不是指特定的数据结构。 Git 作为一个系统,是以它的一般操作来管理并操纵这三棵树的:

image-20231122163553891

HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 通常,理解 HEAD 的最简方式,就是将它看做 该分支上的最后一次提交 的快照。

Index(索引)

索引是你的 预期的下一次提交。 我们也会将这个概念引用为 Git 的“暂存区”,这就是当你运行 git commit 时 Git 看起来的样子。

Working Directory(工作目录)

工作目录(通常也叫 工作区)。 另外两棵树以一种高效但并不直观的方式,将它们的内容存储在 .git 文件夹中。 工作目录会将它们解包为实际的文件以便编辑。 你可以把工作目录当做 沙盒。在你将修改提交到暂存区并记录到历史之前,可以随意更改。

Git的工作流程

经典的 Git 工作流程是通过操纵这三个区域来以更加连续的状态记录项目快照的。

reset workflow

这里面的三个操作含义分别如下:

  • Stage Files:通过git add暂存文件到Index,便于下一次提交。
  • Commit:提交Index中的内容,并将其保存为一个永久的快照,然后创建一个指向该快照的提交对象。HEAD指针也会在提交完后指向该快照。
  • Checkout the project:在本图示例中,它表示提交完后,HEAD指针指向当前工作区的快照。此外,Git有一个git checkout`操作,可以将HEAD指针指向指定的分支,从而将工作区的内容切换到指定分支的对应的索引,再将索引的内容拷贝到工作区。

实际的可视化过程如下:

假设现在有一个新目录,其中有一个文件(file.txt)。将该文件称为v1版本,并标记为蓝色。在这个目录路径下现在运行git init,这会创建一个Git仓库,其中HEAD引用指向未创建的master分支。

image-20231124142318855

此时,只有工作目录有内容。

现在我们想要提交这个文件,所以用 git add 来获取工作目录中的内容,并将其复制到索引中。

image-20231124142355657

接着运行 git commit,它会取得索引中的内容并将它保存为一个永久的快照, 然后创建一个指向该快照的提交对象,最后更新 master 来指向本次提交。

image-20231124142425038

此时如果我们运行 git status,会发现没有任何改动,因为现在三棵树完全相同。

现在我们想要对文件进行修改然后提交它。 我们将会经历同样的过程;首先在工作目录中修改文件。 我们称其为该文件的 v2 版本,并将它标记为红色。

image-20231124142447025

如果现在运行 git status,我们会看到文件显示在 “Changes not staged for commit” 下面并被标记为红色,因为该条目在索引与工作目录之间存在不同。 接着我们运行 git add 来将它暂存到索引中。

image-20231124142509271

此时,由于索引和 HEAD 不同,若运行 git status 的话就会看到 “Changes to be committed” 下的该文件变为绿色 ——也就是说,现在预期的下一次提交与上一次提交不同。 最后,我们运行 git commit 来完成提交。

image-20231124142538812

现在运行 git status 会没有输出,因为三棵树又变得相同了。

切换分支或克隆的过程也类似。 当检出一个分支时,它会修改 HEAD 指向新的分支引用,将 索引 填充为该次提交的快照, 然后将 索引 的内容复制到 工作目录 中。

至此,Git工作流程的可视化过程就大致描述完了,通过图形的方式,更加直观的展示了一个文件在Git仓库下经过 Working Directory -> Index -> HEAD 的过程。

Git的reset

reset即为重置的意思,它主要可以做三件事:

  1. 移动HEAD指针(--soft)
  2. 更新索引(--mixed)
  3. 更新工作目录(--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仓库,它现有两个提交版本,如下:

image-20231124160427130

它的工作区是干净的,如下:

image-20231124160450320

现在针对上诉情况,各自执行--soft--mixed--hard三种参数。

第一种:执行git reset --soft HEAD~,再看git loggit status

image-20231124160625031

通过git log发现,HEAD指针已经发生了变更。

通过git status发现,所有文件记录还是处于已暂存状态,表示还存在于索引中。


第二种:执行git reset --mixed HEAD~,再看git loggit status

image-20231124161148785

通过git log发现,HEAD指针已经发生了变更。

通过git status发现,forgotten_file文件处于已变更但未暂存状态,而version文件处于未跟踪状态(因为它在上一次提交中属于新文件)。这种情况,表示重置到了没有执行git add之前,文件记录已经从索引中回退。


第二种:执行git reset --hard HEAD~,再看git loggit status

image-20231124161638120

通过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 resetgit checkout命令都具有修改HEAD指针指向的功能,只不过git reset是在当前分支移动HEAD指针,而git checkout是切换HEAD指针到指定分支。

reset 一样,checkout 也操纵三棵树,不过它有一点不同,这取决于你是否传给该命令一个文件路径。

不带路径

运行 git checkout [branch] 与运行 git reset --hard [branch] 非常相似,它会更新所有三棵树使其看起来像 [branch],不过有两点重要的区别。

首先不同于 reset --hardcheckout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢。 其实它还更聪明一些。它会在工作目录中先试着简单合并一下,这样所有 还未修改过的 文件都会被更新。 而 reset --hard 则会不做检查就全面地替换所有东西。

第二个重要的区别是 checkout 如何更新 HEAD。 reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。

例如,假设我们有 masterdevelop 分支,它们分别指向不同的提交;我们现在在 develop 上(所以 HEAD 指向它)。 如果我们运行 git reset master,那么 develop 自身现在会和 master 指向同一个提交。 而如果我们运行 git checkout master 的话,develop 不会移动,HEAD 自身会移动。 现在 HEAD 将会指向 master

所以,虽然在这两种情况下我们都移动 HEAD 使其指向了提交 A,但 做法 是非常不同的。 reset 会移动 HEAD 分支的指向,而 checkout 则移动 HEAD 自身。

image-20231124164518277

带路径

运行 checkout 的另一种方式就是指定一个文件路径,这会像 reset 一样不会移动 HEAD。 它就像 git reset [branch] file 那样用该次提交中的那个文件来更新索引,但是它也会覆盖工作目录中对应的文件。 它就像是 git reset --hard [branch] file(如果 reset 允许你这样运行的话), 这样对工作目录并不安全,它也不会移动 HEAD。

此外,同 git resetgit add 一样,checkout 也接受一个 --patch 选项,允许你根据选择一块一块地恢复文件内容。