git checkout/git reset/git revert/git restore常用回退操作

Git 中没有真正的方法来做任何事情,这就是它的妙处!

前言

经常会听到别人说,如果时光可以倒流,我将会如何如何,可是现阶段的科技还达不到时光倒流的目的,或许在《三体》世界的四维裂缝里可以试一下。现实的世界中找不到后悔药,但是在代码的世界里却可以轻松实现,错误的BUG修改、砍掉的做了一半的功能都可以轻松回退,不留一丝痕迹,回滚之后一切又可以重新开始了。

代码回退

大型编程项目的开发往往伴随着版本工具的使用,其实引入代码版本控制工具,有一部分原因也是为了方便回退,回退操作每天都发生的,只是有时是我们显式的操作,有时却自然而然的进行着,我们切换着分支很可能就是从开发版本回退到一个稳定版本,我们查询日志,实际上是在记忆上回退我们整个的开发过程,找寻其中的问题和修改的内容。

Git管理下的各种文件状态

Git的使用中,由于一个文件存在好几种状态的变化,所以处理起回退要分情况进行,有些各式各样的命令最终分析起来其实作用是一样的。

说起Git常常会提到工作区、暂存区、版本库的概念,这是很通用的说法,其实工作区一般就是指我们能看到的文件、本地操作文件所在的目录,我们正常编写的代码文件、管理的资源文件都是在工作区里操作,这里的文件也不是全都平等的,又细分为受版本控制的文件和不受版本控制的文件。

提到暂存区就和index文件建立起了联系,工作区的新文件和已经修改的受版本控制的文件,使用 git add file_name 就可以加到暂存区,相当于登记报个名,以后提交到版本库的时候会把这些登记的文件都带上,实际上执行了 git add 命令的文件都生成了对应的 object 对象,放在.git/objects目录下,状态变成了 staged, 当提交到版本库时,分支会引用这些对象。

版本库就是文件修改的目的地了,最终的修改会提交到版本库,这时提交的文件状态变成 committed,其实也是一种 unmodified 状态,一路走来,版本库中记录了你的每一次提交,可以追溯你每一次修改的内容。

其实还有一个远程仓库的概念,一般确定本地仓库的修改没有问题了,或者要将本地代码远程备份时,可以将自己修改的分支推送到远程仓库,因为有时候我们也想回退已经推送到远程仓库的修改,所以这里先提一下远程仓库。

总结起来一个文件的状态通常可以分为:

  • 不受版本控制的 untracked 状态
  • 受版本控制并且已修改的 modified 状态
  • 受版本控制已修改并提交到暂存区的 staged 状态
  • 从暂存区已经提交到本地仓库的 committed 状态
  • 提交到本地仓库未修改或者从远程仓库克隆下来的 unmodified 状态

Git回退命令

上面提到了在 Git 这个版本控制工具下文件的各种状态,其实回退操作就是通过命令实现这些文件状态的“倒退”,进而达到回退操作的目的,下面一起先来了解下这些可以实现回退的命令。

git checkout

这个命令又出现了,上次是总结 git branch 分支操作的时候,git checkout 可以用来新建或者切换分支,这次总结回退版本的命令,git checkout 也可以用来回退文件版本,很神奇吧。

其实这个命令的作用就是它单词的本义——检出,他的常用操作也取自这个意思,比如 git checkout branch_name 切换分支操作,实际上就是把指定分支在仓库中对应的所有文件检出来覆盖当前工作区,最终表现就是切换了分支。

而针对于文件的检出可以使用 git checkout -- file_name,当不指定 commit id 就是将暂存区的内容恢复到工作区,也就可以达到回退本地修改的作用。

不过,这个身兼数职的 git checkout 命令现在可以轻松一些了,从 Git 2.23 版本开始引入了两个新的命令: git switch 用来切换分支,git restore用来还原工作区的文件,这个后面还会提到。

git revert

revert 这个词的意思是:归还,复原,回退,它和后面即将提到的 restore 在意思上简直无法区分,为了区别他们两个这里可以把 git revert 看成归还的意思,对某次提交执行 git revert 命令就是对这次修改执行一个归还操作,其实就是反向再修改一次。

要理解 git revert 就要从反向修改的含义来看,当我们再一个文件中添加一行内容,并提交到版本库后,产生一个提交id——commit-id-a,如果这时使用 git revert commit-id-a 命令,就相当于在工作区中的那个文件将刚在新加的一行内容删除掉,然后再进行一个提交。

注意,这个操作是会改变分支记录的,因为产生了新的提交。

git restore

这个命令是 Git 2.23 版本之后新加的,用来分担之前 git checkout 命令的功能,作用就是用暂存区或者版本库中的文件覆盖本地文件的修改可以达到回退修改的目的,同时也可以使用版本库中的文件覆盖暂存区的文件,达到回退git add 命令的目的。

注意,这个操作是不会影响分支记录的,就是相当于之前的 git checkout 命令重新检出一份文件来覆盖本地的修改。

git reset

reset 重新设置的意思,其实就是用来设置分支的头部指向,当进行了一系列的提交之后,忽然发现最近的几次提交有问题,想从提交记录中删除,这是就会用到 git reset 命令,这个命令后面跟 commit id,表示当前分支回退到这个 commit id 对应的状态,之后的日志记录被删除,工作区中的文件状态根据参数的不同会恢复到不同的状态。

  • --soft: 被回退的那些版本的修改会被放在暂存区,可以再次提交。

  • --mixed: 默认选项,被回退的那些版本的修改会放在工作目录,可以先加到暂存区,然后再提交。

  • --hard: 被回退的那些版本的修改会直接舍弃,好像它们没有来过一样。

这样来看,git set 命令好像是用来回退版本的,但是如果使用 git rest HEAD file_name 命令就可以将一个文件回退到 HEAD 指向版本所对应的状态,其实就是当前版本库中的状态,也就相当于还原了本地的修改。

git rm

临时插播的命令,本来删除不能算是回退,但是如果它和某些命令反着来就是一种回退,比如对一个新文件使用 git add newfile_name 命令,然后再使用 git rm --cached newfile_name 就可以将这个文件从暂存区移除掉,但是在工作区里没有消失,如果不加 --cached 参数,就会从工作区和版本库暂存区同时删除,相当于执行了 rm newfile_namegit add new_file 两条命令。

具体回退操作

说了这么多肯定有点懵,特别是一个相同的需求可以使用很多命令来实现的时候,接下来看一些具体需求,整个测试过程用上一篇总结《git branch常用分支操作》使用的 git 仓库来进行,远程地址是 git@gitee.com:myname/gitstart.git,下面测试开始,我们看一下这些情况怎么进行还原:

初始状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
albert@homepc MINGW64 /d/gitstart (dev)
$ ls
README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

nothing to commit, working tree clean

albert@homepc MINGW64 /d/gitstart (dev)
$ git branch -a
* dev
master
remotes/origin/dev
remotes/origin/master

albert@homepc MINGW64 /d/gitstart (dev)
$ git branch -vv
* dev 3226b63 [origin/dev] add readme file
master 3226b63 [origin/master] add readme file

还原00:工作区中未加到暂存区和版本库的文件,还原今天所做的修改

实话实说,办不到,没有加到过暂存区就没有被追踪,它的任何修改是没有办法回退的,可是使用 Ctrl+Z 碰碰运气,没准就退回到了你想要的状态。

还原01:工作区中未加到暂存区和版本库的文件,执行了 git add 操作

这种情况可以使用git rm --cached newfilegit restore --staged newfile 或者 git reset HEAD newfile 命令,使用后两个命令的时候不能是版本库的第一个文件。

git rm

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
albert@homepc MINGW64 /d/gitstart (dev)
$ echo "test data">new.txt

albert@homepc MINGW64 /d/gitstart (dev)
$ git add new.txt
warning: LF will be replaced by CRLF in new.txt.
The file will have its original line endings in your working directory

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: new.txt


albert@homepc MINGW64 /d/gitstart (dev)
$ git rm --cached new.txt
rm 'new.txt'

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

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

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

git restore

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
albert@homepc MINGW64 /d/gitstart (dev)
$ git add new.txt
warning: LF will be replaced by CRLF in new.txt.
The file will have its original line endings in your working directory

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: new.txt


albert@homepc MINGW64 /d/gitstart (dev)
$ git restore --staged new.txt

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

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

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

git reset

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
albert@homepc MINGW64 /d/gitstart (dev)
$ git add new.txt
warning: LF will be replaced by CRLF in new.txt.
The file will have its original line endings in your working directory

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: new.txt


albert@homepc MINGW64 /d/gitstart (dev)
$ git reset HEAD new.txt

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

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

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

还原02:版本库中的文件,修改或删除后未执行 git add 操作

我们直接修改 README.md 文件吧,删除刚才添加的未受版本管理的 new.txt,在 README.md 文件中添加内容,然后试着还原,这种情况常常出现在修改一个功能还未提交,但是先不要求修改了,可以直接还原。

这种情况可以使用git restore file_namegit checkout -- file_name 或者 git reset --hard HEAD 命令,最后的git reset 命令带有 --hard 参数不能再加文件目录,只能将工作区全还原。

git restore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
albert@homepc MINGW64 /d/gitstart (dev)
$ echo "new line">>README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

albert@homepc MINGW64 /d/gitstart (dev)
$ git restore README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

nothing to commit, working tree clean

git checkout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
albert@homepc MINGW64 /d/gitstart (dev)
$ echo "new line">>README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

albert@homepc MINGW64 /d/gitstart (dev)
$ git checkout -- README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

nothing to commit, working tree clean

git reset

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
albert@homepc MINGW64 /d/gitstart (dev)
$ echo "new line">>README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

albert@homepc MINGW64 /d/gitstart (dev)
$ git reset --hard HEAD README.md
fatal: Cannot do hard reset with paths.

albert@homepc MINGW64 /d/gitstart (dev)
$ git reset --hard HEAD
HEAD is now at 3226b63 add readme file

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

nothing to commit, working tree clean

还原03:版本库中的文件,修改或删除后执行了 git add 操作

使用了 git add 命令之后,文件的改变就放到了暂存区,这种情况可以使用git restore --staged file_name 或者 git reset HEAD file_name 命令。

git restore

执行 git restore --staged file_name 实际上是使用版本库中的文件覆盖暂存区中的数据,执行结束后文件状态变成了 <还原02> 中的情况。

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
48
49
50
51
albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

nothing to commit, working tree clean

albert@homepc MINGW64 /d/gitstart (dev)
$ echo "test add">>README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

albert@homepc MINGW64 /d/gitstart (dev)
$ git add README.md
warning: LF will be replaced by CRLF in README.md.
The file will have its original line endings in your working directory

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.md


albert@homepc MINGW64 /d/gitstart (dev)
$ git restore --staged README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

git reset

git reset 命令如果加上 --hard 参数不能再加文件目录,只能将工作区全还原,如果不加默认参数为 --mixed,执行之后修改的文件状态变成了 <还原02> 中的情况。

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
albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

albert@homepc MINGW64 /d/gitstart (dev)
$ git add README.md
warning: LF will be replaced by CRLF in README.md.
The file will have its original line endings in your working directory

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.md


albert@homepc MINGW64 /d/gitstart (dev)
$ git reset HEAD README.md
Unstaged changes after reset:
M README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

还原04:版本库中的文件,修改或删除后执行了 git addgit commit 操作

git commit 命令一旦执行了之后就形成了“历史”,我们叫做提交日志,要想回退就得有篡改历史的能力,很幸运 Git 给了我们这种能力,其实提交之后我们可以把本地文件反向修改,然后再提交一次,但是我们说的还原,一般都是只倒退,既然是错误的提交,我们就像把这段“历史”抹去,这时就要用到 git reset HEAD^ 命令。

执行这个命令之后,刚刚的提交记录就被抹掉了,文件状态就回到了 <还原02> 的情况,如果加上参数 --soft 就会回到 <还原03> 的情况,如果加上参数 --hard ,就不能添加 file_name 这个文件名,然后整个工作区倒退到上一次修改之前,其他两种参数 --mixed--soft 就可以指定添加名字。

这里的 HEAD^ 表示最新版本的前一版,也就是倒数第二版本,可以类推,HEAD^^ 表示倒数第三版本,HEAD^^^ 表示倒数第四版本。

另外还有另一种写法 HEAD~1 表示最新版本的前一版,也就是倒数第二版本,HEAD~2 表示倒数第三版本,HEAD~3 表示倒数第四版本。

其中 ^~ 的含义并不相同,涉及到合并分支的概念,有兴趣的话可以多了解下,这里就不展开了,继续还原当前这种情况,我们选择 git reset 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
albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

albert@homepc MINGW64 /d/gitstart (dev)
$ git add README.md
warning: LF will be replaced by CRLF in README.md.
The file will have its original line endings in your working directory

albert@homepc MINGW64 /d/gitstart (dev)
$ git commit -m"modify readme 1"
[dev 8a40f22] modify readme 1
1 file changed, 1 insertion(+)

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is ahead of 'origin/dev' by 1 commit.
(use "git push" to publish your local commits)

nothing to commit, working tree clean

albert@homepc MINGW64 /d/gitstart (dev)
$ git log -2
commit 8a40f229881da037ff99070fa205d7819ba9f51b (HEAD -> dev)
Author: albert <qianxuan101@163.com>
Date: Sat Mar 7 15:46:32 2020 +0800

modify readme 1

commit 3226b63185a16398a02d5eaea47c95309ba49588 (origin/master, origin/dev, release, master)
Author: albert <qianxuan101@163.com>
Date: Wed Feb 26 00:36:35 2020 +0800

add readme file

然后再还原试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
albert@homepc MINGW64 /d/gitstart (dev)
$ git reset HEAD^
Unstaged changes after reset:
M README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

albert@homepc MINGW64 /d/gitstart (dev)
$ git log -2
commit 3226b63185a16398a02d5eaea47c95309ba49588 (HEAD -> dev, origin/master, origin/dev, release, master)
Author: albert <qianxuan101@163.com>
Date: Wed Feb 26 00:36:35 2020 +0800

add readme file

怎么样,历史被我们抹除了,需要注意的是,如果想还原“历史”,那么 git set 命令后面不能跟文件名,也就是说必须整个还原到上一版本,否则就相当于将单个文件简单反向修改添加到暂存区,而之前对文件的修改保留在本地,文件的日志并没有回退,具体的文件状态还得你自己操作感受一下。

还原05:版本库中的文件,修改或删除后执行了 git addgit commitgit push 操作

这种情况就是还原远程仓库的日志记录了,实际上操作步骤先按照 <还原04> 来处理,然后将本地分支情况推送到远程分支即可。

我们先把刚才的修改提交,然后推送到远程分支,使用 git status 可以看到本地分支已经领先远程分支了(Your branch is ahead of ‘origin/dev’ by 1 commit.), git push 操作之后两个分支同步了。

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
albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is ahead of 'origin/dev' by 1 commit.
(use "git push" to publish your local commits)

nothing to commit, working tree clean

albert@homepc MINGW64 /d/gitstart (dev)
$ git log
commit a5b6c18db71a0487f6316f5db4304a99984f2ab3 (HEAD -> dev)
Author: albert <qianxuan101@163.com>
Date: Sat Mar 7 15:51:56 2020 +0800

modify readme 1

commit 3226b63185a16398a02d5eaea47c95309ba49588 (origin/master, origin/dev, release, master)
Author: albert <qianxuan101@163.com>
Date: Wed Feb 26 00:36:35 2020 +0800

add readme file

albert@homepc MINGW64 /d/gitstart (dev)
$ git push
Warning: Permanently added the ECDSA host key for IP address '180.97.125.228' to the list of known hosts.
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 286 bytes | 286.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: Powered by GITEE.COM [GNK-3.8]
To gitee.com:myname/gitstart.git
3226b63..a5b6c18 dev -> dev

这时通过远程仓库的管理软件,你可以看到远程分支已经有了最新的提交,然后我们可以参考 <还原04> 的情况,先将本地日志还原,再推送到远程仓库。

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
albert@homepc MINGW64 /d/gitstart (dev)
$ git reset HEAD^
Unstaged changes after reset:
M README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is behind 'origin/dev' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

albert@homepc MINGW64 /d/gitstart (dev)
$ git log
commit 3226b63185a16398a02d5eaea47c95309ba49588 (HEAD -> dev, origin/master, release, master)
Author: albert <qianxuan101@163.com>
Date: Wed Feb 26 00:36:35 2020 +0800

add readme file

albert@homepc MINGW64 /d/gitstart (dev)
$ git push
To gitee.com:myname/gitstart.git
! [rejected] dev -> dev (non-fast-forward)
error: failed to push some refs to 'git@gitee.com:myname/gitstart.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

和想象的不太一样的,这种情况是远程仓库的记录领先,无法直接推送,此时可以添加 -f 参数,用本地提交记录覆盖远程分支记录:

1
2
3
4
5
6
albert@homepc MINGW64 /d/gitstart (dev)
$ git push -f
Total 0 (delta 0), reused 0 (delta 0)
remote: Powered by GITEE.COM [GNK-3.8]
To gitee.com:myname/gitstart.git
+ a5b6c18...3226b63 dev -> dev (forced update)

这次再查询远程分支记录,发现也被回退了,目的达成。

还原06:两次git commit 之后产生两条日志,只还原第一次提交

这种情况其实发生了两次修改和两次提交,和 <还原05> 情况不同的是要还原的提交不是最后一次,如果使用 git reset 命令必然将最后一次修改也还原了,虽然不能直接完成,但是给我们提供了解决问题的思路:

第一种方法:直接使用 git reset HEAD^^ 命令还原两次提交,然后在工作区将文件按第二次修改再改一次进行提交,这种方法适用于想要抹除第一次提交历史的情况。

第二种方法:如果你不在意提交历史,只是想还原第一次修改,那么可以使用 git revert 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
48
49
50
51
52
53
54
55
56
57
58
albert@homepc MINGW64 /d/gitstart (dev)
$ git status
On branch dev
Your branch is up to date with 'origin/dev'.

nothing to commit, working tree clean

albert@homepc MINGW64 /d/gitstart (dev)
$ echo "m1">>README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git add README.md
warning: LF will be replaced by CRLF in README.md.
The file will have its original line endings in your working directory

albert@homepc MINGW64 /d/gitstart (dev)
$ git commit -m"modify README 1"
[dev e570df1] modify README 1
1 file changed, 1 insertion(+)

albert@homepc MINGW64 /d/gitstart (dev)
$ echo "m2">>README.md

albert@homepc MINGW64 /d/gitstart (dev)
$ git add README.md
warning: LF will be replaced by CRLF in README.md.
The file will have its original line endings in your working directory

albert@homepc MINGW64 /d/gitstart (dev)
$ git commit -m"modify README 2"
[dev 140547f] modify README 2
1 file changed, 1 insertion(+)
gi
albert@homepc MINGW64 /d/gitstart (dev)
$ git log
commit 140547f8d0b10d9a388beaf2ce522c38c878a839 (HEAD -> dev)
Author: albert <qianxuan101@163.com>
Date: Sat Mar 7 16:26:17 2020 +0800

modify README 2

commit e570df134b39ee7424bc8c48c1067e72c3fb9637
Author: albert <qianxuan101@163.com>
Date: Sat Mar 7 16:26:07 2020 +0800

modify README 1

commit 3226b63185a16398a02d5eaea47c95309ba49588 (origin/master, origin/dev, release, master)
Author: albert <qianxuan101@163.com>
Date: Wed Feb 26 00:36:35 2020 +0800

add readme file

albert@homepc MINGW64 /d/gitstart (dev)
$ cat README.md
learn git branch command
m1
m2

然后使用 git revert 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
albert@homepc MINGW64 /d/gitstart (dev)
$ git revert HEAD^
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
error: could not revert e570df1... modify README 1
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'

albert@homepc MINGW64 /d/gitstart (dev|REVERTING)
$ vi README.md

albert@homepc MINGW64 /d/gitstart (dev|REVERTING)
$ git add README.md

albert@homepc MINGW64 /d/gitstart (dev|REVERTING)
$ git commit
[dev 6ae97d0] Revert "modify README 1"
1 file changed, 1 deletion(-)

albert@homepc MINGW64 /d/gitstart (dev)
$ git log
commit 6ae97d0e136abc1ed241854298037ca9d1c4460c (HEAD -> dev)
Author: albert <qianxuan101@163.com>
Date: Sat Mar 7 16:31:50 2020 +0800

Revert "modify README 1"

This reverts commit e570df134b39ee7424bc8c48c1067e72c3fb9637.

commit 140547f8d0b10d9a388beaf2ce522c38c878a839
Author: albert <qianxuan101@163.com>
Date: Sat Mar 7 16:26:17 2020 +0800

modify README 2

commit e570df134b39ee7424bc8c48c1067e72c3fb9637
Author: albert <qianxuan101@163.com>
Date: Sat Mar 7 16:26:07 2020 +0800

modify README 1

commit 3226b63185a16398a02d5eaea47c95309ba49588 (origin/master, origin/dev, release, master)
Author: albert <qianxuan101@163.com>
Date: Wed Feb 26 00:36:35 2020 +0800

add readme file

因为修改了同一个文件,还原的时候还产生了冲突,解决冲突之后才提交,看日志发现这是一条新的记录,在实际操作的过程中可能会发生比这还要麻烦的场景,多练就好了。

常用集合

使用 Git 进行版本管理时,遇到的回退情况远不止这么多,这只是我目前常见的,之后遇到还会补充,每种情况我们其实不止有一种解决方式,接下来对于每种情况给一个我个人常用的处理方式,因为 git checkout 的作用被逐渐拆分成更具体的 git switchgit restore,我们尽量选择功能明确的命令:

  1. 还原00:工作区中未加到暂存区和版本库的文件,还原今天所做的修改
    • 尝试下Ctrl+z吧,不行就找找自动保存的缓存文件,看看能不能找到之前版本
  2. 还原01:工作区中未加到暂存区和版本库的文件,执行了 git add 操作
    • 直接使用 git restore --staged file_name 命令,如果版本不支持则使用 git rm --cached file_name
  3. 还原02:版本库中的文件,修改或删除后未执行 git add 操作
    • 直接使用 git restore file_name 命令,如果版本不支持则使用 git checkout -- file_name
  4. 还原03:版本库中的文件,修改或删除后执行了 git add 操作
    • 直接使用 git restore --staged file_name 命令,按 <还原02> 情况处理
  5. 还原04:版本库中的文件,修改或删除后执行了 git addgit commit 操作
    • 直接使用 git reset HEAD^ 命令,按 <还原02> 情况处理,或者使用 git reset --soft HEAD^ 命令,按 <还原03> 情况处理
  6. 还原05:版本库中的文件,修改或删除后执行了 git addgit commitgit push 操作
    • 先按照 <还原04> 情况处理,然后使用 git push -f 命令
  7. 还原06:两次git commit 之后产生两条日志,只还原第一次提交
    • 使用 git revert HEAD^ 命令,解决冲突后提交,revert 后面跟具体的 commit id 也可以。

总结

  • 参考这些具体的例子你会发现,很多操作选择在使用 git status 之后都有列举
  • 所以说 git status 是一个可以提示你做选择的强大帮手,不知所措时可以试试它
  • Git 2.23版本之后学会用 git switchgit restore 命令,因为之前 git checkout 背负了太多了
  • 最后放一幅图吧,只画了主要的,没有画出全部情况,否则会很乱,可以对照着练习一下

gitfilestate

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客