reset与checkout

基础

首先,我们需要对git的存储逻辑分区有一定的概念:

  • working directory:工作目录,是本地直接编辑的可见部分
  • staging area:暂存区,暂存working directory中我们准备下一次提交的内容
  • repository:仓库,保存的是上一次提交的快照

执行git add 命令,可以将指定的文件(一般包含最新的修改)暂存到暂存区,而git commit 命令将当前暂存区中的内容执行一次新的提交。一般通过git add命令暂存新的修改内容,而通过git commit提交已经暂存的准备提交到仓库的内容。
git diff命令可以用来检查这三个逻辑分区的差异。例如

  • 执行git diff,查看工作目录和暂存区的差异
  • 执行git diff –staged,查看暂存区和仓库最新提交的差异
  • 执行git diff HEAD,查看工作目录与仓库最新的提交的差异

平时,开发项目的时候,我们可能想舍弃当前工作目录中的修改内容,或者将某一源文件还原到前几次提交的某一次提交时的内容,这时候我们想起了git中的checkout和reset好像有某些类似的功能,可以协助我们完成。但这两个命令有一定的差别,使用不当可能造成意想不到的结果,很可能最新修改好的程序就被覆盖了。本文通过一个小实验探究reset和checkout的功能以及差异。

准备工作

初始化一个目录为git仓库,目录中包含两个文件

1
2
3
> mkdir test
> cd test/
> git init

创建两个文件foo和bar,并写入第一条字符串”this is history1”,执行git status查看当前状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> touch foo bar
> echo 'this is history1' > bar
> echo 'this is history1' > foo
> git status
On branch master

No commits yet

Untracked files:
(use "git add <file>..." to include in what will be committed)
bar
foo

nothing added to commit but untracked files present (use "git add" to track)

执行git add命令,track两个文件,并写入提交信息”history1”

1
2
3
4
5
6
> git add bar foo
> git commit -m 'history1'
[master (root-commit) f010d6d] history1
2 files changed, 2 insertions(+)
create mode 100644 bar
create mode 100644 foo

在两个文件末尾追加”history2””history3”,再进行两次新的提交,每次的提交信息分别为”history2””history3”

1
2
3
4
5
6
7
8
9
10
11
12
> echo 'this is history2' >> foo
> echo 'this is history2' >> bar
> git add bar foo
> git commit -m 'history2'
[master 0c52905] history2
2 files changed, 2 insertions(+)
> echo 'this is history3' >> foo
> echo 'this is history3' >> bar
> git add bar foo
> git commit -m 'history3'
[master cabad03] history3
2 files changed, 2 insertions(+)

查看提交日志,此时仓库有了三次提交的记录

1
2
3
4
> git log --pretty=oneline
cabad03e9605163b939d137aacd28d3441f5ebea (HEAD -> master) history3
0c5290517531ada5847cc418e9db2941e33fb62e history2
f010d6d2831e827a16d4687076cf2de40f89076d history1

查看foo和bar的内容:

1
2
3
4
5
6
7
8
> cat bar
this is history1
this is history2
this is history3
rda@pa ~/T/test (master)> cat foo
this is history1
this is history2
this is history3

我们将基于这两个文件原有的内容做简单的添加,以及执行reset和checkout,探究两者的功能和差异。本文不涉及介绍git中指针,头指针的操作和其他概念。
在foo文件尾添加”this is stage”的文本,并执行git add foo,暂存该修改,然后再在文件尾添加”this is work dir”的文本

1
2
3
> echo 'this is stage' >> foo
> git add foo
> echo 'this is work dir' >> foo

执行git diff,查看工作目录和暂存区的差别

1
2
3
4
5
6
7
8
9
10
> git diff
diff --git a/foo b/foo
index fc8c574..64512a0 100644
--- a/foo
+++ b/foo
@@ -2,3 +2,4 @@ this is history1
this is history2
this is history3
this is stage
+this is work dir

执行git diff –staged,查看暂存区和仓库的差别

1
2
3
4
5
6
7
8
9
10
> git diff --staged
diff --git a/foo b/foo
index 55a27cf..fc8c574 100644
--- a/foo
+++ b/foo
@@ -1,3 +1,4 @@
this is history1
this is history2
this is history3
+this is stage

可见,相对于仓库,暂存区中添加了文本”this is stage”,而相对于暂存区,工作目录添加了”this is work dir”,同理,执行git diff HEAD,对比工作目录和仓库,文件foo追加了两条字符串

1
2
3
4
5
6
7
8
9
10
11
> git diff HEAD
diff --git a/foo b/foo
index 55a27cf..64512a0 100644
--- a/foo
+++ b/foo
@@ -1,3 +1,5 @@
this is history1
this is history2
this is history3
+this is stage
+this is work dir

我们也可以执行git status -s,左边显示的M表示foo已经修改并暂存,右边的M表示foo再暂存后又有新的修改

1
2
> git status -s
MM foo

reset

git reset使用respository中特定commit来重置head下的repository、stage、workspace,可进一步的细分成三种模式hard、sort、mixed(默认)

hard

git reset –hard会同时重置repository、stage、workspace。
执行git reset –hard HEAD,将当前头指针指向的commit复制到暂存区和工作目录,相当于丢弃了暂存区和工作目录的修改

1
2
3
4
5
6
7
8
9
10
11
12
 git reset --hard HEAD
HEAD is now at cabad03 history3
> git diff
> git diff --staged
> git diff HEAD
> cat foo
this is history1
this is history2
this is history3
> git status
On branch master
nothing to commit, working tree clean

可见,工作目录、暂存区和仓库的内容是一致的。
可以使用该命令“回退”到某一历史commit

1
2
3
4
5
6
7
8
> git reset --hard HEAD~
HEAD is now at 0c52905 history2
> git status
On branch master
nothing to commit, working tree clean
> git log --pretty=oneline
0c5290517531ada5847cc418e9db2941e33fb62e (HEAD -> master) history2
f010d6d2831e827a16d4687076cf2de40f89076d history1

可见,当前分支master和HEAD指针均移动到了上一次commit处,并且工作目录和暂存区与仓库保持一致
查看bar

1
2
3
> cat bar
this is history1
this is history2

内容也回退到了第二次提交的内容,因为我们操作的对象是所以被追踪的文件。

mixed

git reset –mixed会重置repository、stage,只保留workspace中的改动。
重复类似刚刚测试–hard的操作,我们同时在foo文件和bar文件末尾追加”this is stage”文本,并暂存一次,再追加”this is work dir”文本

1
2
3
> echo 'this is stage' >> foo
> git add foo
> echo 'this is work dir' >> foo

执行git diff、git diff –staged、git diff HEAD,查看三个逻辑区的差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
> git diff
diff --git a/foo b/foo
index 4c1d2ab..bd45079 100644
--- a/foo
+++ b/foo
@@ -1,3 +1,4 @@
this is history1
this is history2
this is stage
+this is work dir
> git diff --staged
diff --git a/foo b/foo
index 8e02c46..4c1d2ab 100644
--- a/foo
+++ b/foo
@@ -1,2 +1,3 @@
this is history1
this is history2
+this is stage
> git diff HEAD
diff --git a/foo b/foo
index 8e02c46..bd45079 100644
--- a/foo
+++ b/foo
@@ -1,2 +1,4 @@
this is history1
this is history2
+this is stage
+this is work dir

执行git reset –mixed HEAD,预期该命令执行结果是,暂存区的内容被丢弃,与最新的提交保持一致,而工作目录不变。

1
2
3
> git reset --mixed 
Unstaged changes after reset:
M foo

执行git diff、git diff –staged、git diff HEAD,查看三个逻辑区的差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> git diff
diff --git a/foo b/foo
index 8e02c46..bd45079 100644
--- a/foo
+++ b/foo
@@ -1,2 +1,4 @@
this is history1
this is history2
+this is stage
+this is work dir
> git diff --staged
> git diff HEAD
diff --git a/foo b/foo
index 8e02c46..bd45079 100644
--- a/foo
+++ b/foo
@@ -1,2 +1,4 @@
this is history1
this is history2
+this is stage
+this is work dir

此时,原本已经暂存的内容也被丢弃,算作了工作目录和上一次提交的差异。–mixed是默认项,因此可以省略。
执行git reset HEAD~,预期该指令执行结果是:
for:暂存区内容和第一次提交内容一致,均为”this is history1”,而工作目录不变,追加了三条字符串,分别是”this is history2””this is stage””this is work dir”
bar:暂存区和第一次提交一致,工作目录中的”this is history2”为新加的内容

1
2
3
4
> git reset HEAD~
Unstaged changes after reset:
M bar
M foo

执行git diff、git diff –staged、git diff HEAD,查看三个逻辑区的差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
> git diff 
diff --git a/bar b/bar
index 361b0e4..8e02c46 100644
--- a/bar
+++ b/bar
@@ -1 +1,2 @@
this is history1
+this is history2
diff --git a/foo b/foo
index 361b0e4..bd45079 100644
--- a/foo
+++ b/foo
@@ -1 +1,4 @@
this is history1
+this is history2
+this is stage
+this is work dir
> git diff --staged
> git diff HEAD
diff --git a/bar b/bar
index 361b0e4..8e02c46 100644
--- a/bar
+++ b/bar
@@ -1 +1,2 @@
this is history1
+this is history2
diff --git a/foo b/foo
index 361b0e4..bd45079 100644
--- a/foo
+++ b/foo
@@ -1 +1,4 @@
this is history1
+this is history2
+this is stage
+this is work dir

soft

git reset –soft会重置repository,但保留stage、workspace中的改动。
首先将仓库恢复到初始状态,即有三次commit,并且暂存区中有”this is stage”,工作目录中还额外添加了”this is work dir”。恢复的方法是,根据第三次commit,checkout到commit3(checkout可以移动HEAD指针,然后在commit3处新建一个分支,并将master合并到该分支即可)
执行git reset –soft HEAD~2,即reset到第一次commit,预期执行结果是:
foo文件:仓库仅有”this is history1”,暂存区中有”this is history2””this is history3””this is stage”,而工作目录额外有”this is work dir”
bar文件:仓库仅有”this is history1”,暂存区中有”this is history2””this is history3”,工作目录与暂存区内容一致

1
> git reset --soft HEAD~2

查看两个文件内容

1
2
3
4
5
6
7
8
9
10
> cat bar
this is history1
this is history2
this is history3
> cat foo
this is history1
this is history2
this is history3
this is stage
this is work dir

执行git diff、git diff –staged、git diff HEAD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
> git diff
diff --git a/foo b/foo
index fc8c574..64512a0 100644
--- a/foo
+++ b/foo
@@ -2,3 +2,4 @@ this is history1
this is history2
this is history3
this is stage
+this is work dir
> git diff --staged
diff --git a/bar b/bar
index 361b0e4..55a27cf 100644
--- a/bar
+++ b/bar
@@ -1 +1,3 @@
this is history1
+this is history2
+this is history3
diff --git a/foo b/foo
index 361b0e4..fc8c574 100644
--- a/foo
+++ b/foo
@@ -1 +1,4 @@
this is history1
+this is history2
+this is history3
+this is stage
> git diff HEAD
diff --git a/bar b/bar
index 361b0e4..55a27cf 100644
--- a/bar
+++ b/bar
@@ -1 +1,3 @@
this is history1
+this is history2
+this is history3
diff --git a/foo b/foo
index 361b0e4..64512a0 100644
--- a/foo
+++ b/foo
@@ -1 +1,5 @@
this is history1
+this is history2
+this is history3
+this is stage
+this is work dir

查看日志

1
2
> git log --pretty=oneline
f010d6d2831e827a16d4687076cf2de40f89076d (HEAD -> master) history1

不难发现,git reset实质是通过移动当前分支进行操作的,而hard、mixed、soft其实是用于指示git是否将仓库的内容覆盖到暂存区和工作目录

checkout

不同与reset,checkout可以对某一个文件进行“重置”操作。
再次将仓库还原到commit3,并且对foo和bar进行相同的操作,即:
追加一个文本并暂存,然后再次追加一个新的文本。我们称该状态为初始状态。

1
2
3
4
5
6
7
8
> echo 'this is stage' >> foo
> echo 'this is stage' >> bar
> git add foo bar
> echo 'this is work dir' >> foo
> echo 'this is work dir' >> bar
> git status -s
MM bar
MM foo

执行git checkout HEAD foo,git diff和git diff –staged

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> git checkout HEAD foo
Updated 1 path from 3ea0ad5
> git diff
diff --git a/bar b/bar
index fc8c574..64512a0 100644
--- a/bar
+++ b/bar
@@ -2,3 +2,4 @@ this is history1
this is history2
this is history3
this is stage
+this is work dir
> git diff --staged
diff --git a/bar b/bar
index 55a27cf..fc8c574 100644
--- a/bar
+++ b/bar
@@ -1,3 +1,4 @@
this is history1
this is history2
this is history3
+this is stage

可见,foo的暂存区和工作目录被HEAD处的提交覆盖了,相当于丢弃了暂存区和工作目录中的修改,而bar未受影响
同样,我们对bar进行操作,首先将仓库的状态恢复到初始状态。
执行git checkout HEAD~2 bar

1
2
> git checkout HEAD~2 bar
Updated 1 path from f6fa925

查看foo文件的各个逻辑区对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
> git diff foo
diff --git a/foo b/foo
index fc8c574..64512a0 100644
--- a/foo
+++ b/foo
@@ -2,3 +2,4 @@ this is history1
this is history2
this is history3
this is stage
+this is work dir
> git diff --staged foo
diff --git a/foo b/foo
index 55a27cf..fc8c574 100644
--- a/foo
+++ b/foo
@@ -1,3 +1,4 @@
this is history1
this is history2
this is history3
+this is stage
> git diff HEAD foo
diff --git a/foo b/foo
index 55a27cf..64512a0 100644
--- a/foo
+++ b/foo
@@ -1,3 +1,5 @@
this is history1
this is history2
this is history3
+this is stage
+this is work dir

未受到影响。
查看bar文件的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> git diff bar
> git diff --staged bar
diff --git a/bar b/bar
index 55a27cf..361b0e4 100644
--- a/bar
+++ b/bar
@@ -1,3 +1 @@
this is history1
-this is history2
-this is history3
> git diff HEAD bar
diff --git a/bar b/bar
index 55a27cf..361b0e4 100644
--- a/bar
+++ b/bar
@@ -1,3 +1 @@
this is history1
-this is history2
-this is history3

可见,git checkout HEAD~2 bar相当于将commit1中的bar复制到当前工作目录并暂存

总结

模式 reset reposity reset stage reset working directory
hard
mixed(default) x
soft x x
使用checkout,相当于从指定的commit中取出指定的文件,覆盖当前文件并暂存。 当checkout后未指定文件时,相当于移动HEAD指针。