黑帽联盟

标题: Git之旅(10):后悔了,怎么办? [打印本页]

作者: 定位    时间: 2020-4-3 17:47
标题: Git之旅(10):后悔了,怎么办?
人都是会后悔的,你我都不能例外。

在使用Git进行版本管理的过程中,我们也会经常后悔的,比如,我写了一些代码,做了一些修改,为了能将这些修改创建为提交,我先进行了暂存操作,但是没有立即创建提交,也就是说,这些修改从工作区同步到了暂存区,以便将来能够创建提交,但是过了一会儿,我后悔了,我觉得这些修改并不适合用来创建下次的提交,我该怎么办呢?这些修改已经存在于工作区和暂存区了,我现在想要保留工作区的修改,但是不想让这些修改同时存在于暂存区,也就是说,我想要撤销暂存区中的变更,我该怎么办呢?

此时,我们只需要借助一条git命令就能达到我们的目的,这条命令就是"git reset HEAD",看到这条命令,是不是觉得特别熟悉,没错,前文中我们粗略的使用过"git reset"命令,但是并没有详细的介绍它,这篇文章我们就来仔细的聊聊"git reset"命令。

刚才说过,"git reset HEAD"命令可以帮助我们把暂存区恢复到未暂存任何工作区修改的状态(即与最新的commit的状态保持一致),那么现在我们就来通过实际的操作来演示一遍。
首先,创建一个用于演示的测试仓库,然后创建两个用来测试的文件,最后初始化第一个提交,命令如下:
$ git init test_repo
Initialized empty Git repository in D:/workspace/git/test_repo/.git/

$ cd test_repo/

$ echo 'test file1' > f1

$ echo 'test file2' > f2

$ git add -A

$ git commit -m 'init, add f1 and f2'
[master (root-commit) 1a09207] init, add f1 and f2
2 files changed, 2 insertions(+)
create mode 100644 f1
create mode 100644 f2

$ git log --oneline
1a09207 (HEAD -> master) init, add f1 and f2
如上述命令所示,我们创建了两个测试文件,f1和f2,并且创建了第一个提交,第一个提交的哈希码为"1a09207"。

准备工作完毕,现在我们来模拟一遍文章开头描述的场景,命令如下:
首先,在两个测试文件中各自添加一行新行,模拟修改操作,如下:
$ echo 'The second line in the f1 file' >> f1
$ echo 'The second line in the f2 file' >> f2
然后查看git仓库状态,发现有两个文件变更没有暂存,如下:
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified:   f1
modified:   f2

no changes added to commit (use "git add" and/or "git commit -a")
看工作区和暂存区的差异,可以看到新增的两行
$ git diff
diff --git a/f1 b/f1
index d9e310a..2041f47 100644
--- a/f1
+++ b/f1
@@ -1 +1,2 @@
test file1
+The second line in the f1 file
diff --git a/f2 b/f2
index d15d3b2..4d9f2f1 100644
--- a/f2
+++ b/f2
@@ -1 +1,2 @@
test file2
+The second line in the f2 file
将所有变更添加到暂存区
$ git add -A
再次查看仓库状态,发现两个测试文件的变更状态已经同步到暂存区,我们随时可以根据这些变更创建新的提交。
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified:   f1
modified:   f2
再次对比差异,可以看出工作区和暂存区已经没有任何差异。
$ git diff
刚才我将变更添加到暂存区是因为我觉的这些变更适合成为下一次的提交,但是我现在后悔了,我想要让这些变更从暂存区消失,我想要暂存区回到之前没有这些变更的状态,换句话说就是,我想要暂存区中的状态跟当前分支中最新提交中的状态一样,上文已经说过,我可以使用"git reset HEAD"命令来实现,我们来试试,如下:
$ git reset HEAD
Unstaged changes after reset:
M       f1
M       f2
执行上述命令后,可以从返回信息中看出,f1和f2的修改已经从暂存区移除了。
那么现在,我们来使用"git status"命令来看一下当前仓库的状态,如下:
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified:   f1
modified:   f2

no changes added to commit (use "git add" and/or "git commit -a")
从返回信息可以看出,关于f1和f2两个文件的修改操作已经变成了未暂存的状态。(在你的命令窗口中,f1和f2那两行文字应该是红色的,表示未暂存)
也就是说,刚才的reset命令已经生效了,这条命令成功的将已经同步到暂存区中的变更撤销了。
此时,再次比较工作区和暂存区的差异,如下:
$ git diff
diff --git a/f1 b/f1
index d9e310a..2041f47 100644
--- a/f1
+++ b/f1
@@ -1 +1,2 @@
test file1
+The second line in the f1 file
diff --git a/f2 b/f2
index d15d3b2..4d9f2f1 100644
--- a/f2
+++ b/f2
@@ -1 +1,2 @@
test file2
+The second line in the f2 file
可见,工作区中仍然是存在最新的变更内容的,只是暂存区中已经是"干净"的状态了(与当前分支最新提交的状态相同)。

上例中,我们一次性将所有暂存区中的修改都消除了,其实我们也可以指定只操作某个文件,操作如下:
我们先将工作区的变更再次同步到暂存区
$ git add -A
查看仓库状态,发现f1和f2的变更都已经同步到了暂存区。
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified:   f1
modified:   f2
假设此时,我们只是想要从暂存区去除f2的变更,f1的变更仍然保留在暂存区中,那么我们可以执行如下命令:
$ git reset HEAD -- f2
Unstaged changes after reset:
M       f2
执行上述命令后,再次查看仓库状态,可以看到如下信息。
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified:   f1

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

modified:   f2
从返回信息中可以看出,f1的变更状态仍然保存在暂存区,而f2的变更则未暂存。
从目前的状况来看,工作区、暂存区、以及最近的提交中的内容都是不同的,f1文件的变更已经存在于暂存区,而f2文件的只存在于工作区,在最近的提交中,则没有这两个文件的最新变更,因为我们还未根据任何最新的变更创建提交,如果此时我后悔了,我想要将所有区域全部恢复到目前最近一次提交中的状态,我该怎么办呢?也就是说,我想要放弃所有工作区和暂存区中的变更,无论是已经暂存的变更,还是未暂存的变更,我都不想要了,我现在就想让所有逻辑区域中的内容回到上一次提交时的状态,我该怎么办呢?我们可以使用如下命令:
$ git reset --hard HEAD
HEAD is now at 1a09207 init, add f1 and f2
执行上述命令,即可将所有区域还原到与最新的提交相同的状态,看到上述命令你可能会觉得特别熟悉,这条命令不就是我们前文中使用过的版本回退命令么?在前文中我们总结过,使用" git reset --hard"命令可以回退到之前的任意一个提交时的状态,只需要利用" git reset --hard"命令指定对应的commit ID即可,而前文中又总结过,"HEAD"表示当前分支的最新提交(此处可以把HEAD替换成最新提交的commit ID,效果是相同的),所以,当我们执行"git reset --hard HEAD"命令时,你可以理解成我们把所有逻辑区域都回退到了当前分支最新提交时的状态。

到目前为止,我后悔了两次,并且使用了如下两条命令:
git reset HEAD
git reset --hard HEAD
我们使用了"git reset HEAD"命令将所有已经暂存的变成从暂存区撤销了(即暂存区与最近提交中的状态一致)。
我们使用了"git reset --hard HEAD"命令将所有逻辑区域恢复成了最近的提交中的状态(即工作区、暂存区都与最近提交中的状态一致)。
你肯定已经发现了规律,其实,上述两条命令无非就是将最近的提交中的内容覆盖到了不同的逻辑区域中,换句话说就是:
"git reset HEAD"命令将最近提交中的内容覆盖到了暂存区
"git reset --hard HEAD"命令将最近提交中的内容覆盖到了暂存区和工作区

其实,当我们使用 "git reset HEAD"命令时,相当于使用了"git reset --mixed HEAD"命令,这两条命令是等效的,因为"git reset"命令的默认选项就是"--mixed",所以当我们不添加任何选项时,默认使用"--mixed"选项,所以,我们可以总结如下
"git reset --mixed HEAD"命令可以将已经暂存到暂存区中的变更撤销(或者说将最近提交中的内容同步到暂存区)
"git reset --hard HEAD"命令可以将最近提交以后的所有变更撤销,无论是否暂存(或者说将最近提交中的内容同步到工作区和暂存区)

现在来扩展一下,上述命令其实不仅仅适用于最新的提交,我们还可以把上述命令中的"HEAD"替换成任意一个提交的哈希码(commit ID),比如如下命令:
"git reset --hard commitID"
"git reset --mixed commitID"

上述两条命令作用如下
"git reset --hard commitID"命令会将HEAD指针指向commitID对应的提交,并且将对应提交中的内容同步到工作区(就像前文中执行回退操作时那样,HEAD指针、暂存区,以及工作区全部回到了指定提交时的状态,由于HEAD指针的指向也发生了变化,所以当前分支的最新提交也会变成commitID对应的提交),如下图所示
1.png

"git reset --mixed commitID"命令会将HEAD指针指向commitID对应的提交,并且将对应提交中的内容同步到暂存区(HEAD指针以及暂存区中的内容都发生了变化,但是不会影响工作区,也就是说,当前分支的最新提交会变成commitID对应的提交,对应提交的状态会同步到暂存区,但是工作区中的内容或者变更不受影响),如下图所示:
2.png

说到底,我们就是利用了"git reset"命令的不同选项,来操作了HEAD指针以及不同的逻辑区域罢了,除了"--mixed"选项和"--hard"选项,还有一个"--soft"选项,使用"--soft"选项时命令如下
"git reset --soft commitID"
此命令只将HEAD指针指向commitID对应的提交,但是不会操作暂存区和工作区,也就是说,当前分支中的最新提交会变成commitID对应的提交,工作区和暂存区中的内容或者变更不会受到任何影响,如下图所示:
3.png

我们可以用一张表格来总结git reset命令的不同选项影响的区域,如下表所示:


工作区
索引
HEAD
--soft
--mixed
--hard

需要注意的是,在一个人使用git进行版本管理时,你可以任性的使用git reset命令,但是当多人协作使用git仓库管理代码时,使用git reset命令需要当心,因为,当多人协作使用git时,别人可以通过远程仓库获取到你已经发布的提交(我们可以将提交推送到远程仓库中,以便其他同事获取到我们修改的最新的代码,远程仓库我们还没有总结,此处先不用纠结,我们只要知道远程仓库可以帮助我们方便的发布提交,以便共享给他人使用即可),如果我们通过git reset命令回退了已经发布的提交,并且将回退后的状态推送到了远程仓库,则会影响到其他人使用git,由于我们还没有总结远程仓库的相关内容,所以不理解没关系,先记住就好,到总结远程仓库的知识点时,我们自然会理解的。

通过上述总结,我们已经知道了如何将暂存区的变更撤销,以及如何将工作区和暂存区的变更全部撤销,除了这些情况下的撤销,你可能还会遇到一些别的情况,比如,你只想撤销工作区的变更,该怎么办呢?细分之下,这里有两种情况:
情况一:你只是在工作区进行了变更,还没有将对应变更添加到暂存区,此时你后悔了,你想要将工作区中的修改撤销,让工作区还原成最近一次提交时的样子。
情况二:你已经将部分变更添加到了暂存区,然后你又接着干活,在工作区产生了新的变更,这些新的变更还没有添加到暂存区,此时你后悔了,你想要将工作区中的新的变更(还没有添加到暂存区的新的变更)撤销,让工作区还原成最近一次暂存时的样子。
无论是情况一,还是情况二,我们都能通过同一条命令来撤销工作区中的修改,不过我们还是分情况来说。

我们先说说情况一
仍然使用刚才的测试仓库进行测试,两个测试文件,f1和f2,文件内容如下:
$ ls
f1  f2

$ cat *
test file1
test file2
这些内容已经存在于第一个提交当中
$ git log --oneline
1a09207 (HEAD -> master) init, add f1 and f2
现在,我分别对两个文件做一些修改,修改如下:
$ echo 111 >> f1
$ echo 222 >> f2
我分别在两个文件中添加了新行,但是我并没有将文件的变更添加到暂存区,git仓库状态如下
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified:   f1
modified:   f2

no changes added to commit (use "git add" and/or "git commit -a")
假设此时我后悔了,我想要将f2文件恢复成当前分支中最新的提交中f2文件的状态,则可以使用如下命令
$ git checkout -- f2
执行上述命令后,你会发现f2中的变更已经撤销了
如果你想要的一次性将所有工作区的变更全部撤销,也可以仓库的根目录中执行如下命令
$ git checkout -- ./*
执行上述命令可以将所有工作区的变更全部撤销,注意,这里指的工作区的所有变更是指已经被git跟踪的文件的变更,如果是新创建的文件,还没有被加入到版本库,则不能使用"git checkout -- ./*"命令撤销。

情况一说完了,现在来聊聊情况二
我又将f1和f2恢复到了最初的状态
$ ls
f1  f2

$ cat f1
test file1

$ cat f2
test file2
此时,我先对f1文件进行一次修改,然后将这次的变更添加到暂存区,操作如下:
$ echo 111 >> f1
$ git add f1
将这次修改添加到暂存区以后,我继续修改f1,操作如下
$ echo 111111111111 >> f1

$ cat f1
test file1
111
111111111111
我又在f1文件这种添加了一行新行。

假设此时,我后悔了,我想要将f1文件恢复到刚刚添加到暂存区时的状态,该怎么办呢?简单,仍然是使用git checkout命令即可,命令如下:
$ git checkout -- f1

$ cat f1
test file1
111
可以看到,执行"git checkout -- f1"命令后,工作区中最新的变更撤销了,变成了最近一次暂存时的状态。

从上述示例可以总结出一个规律,就是通过"git checkout -- file"命令可以帮助我们撤销工作区中的变更,但是这种撤销是分情况的,如果你撤销的文件已经添加到了暂存区中,并且在那之后又在工作区中进行了新修改,那么"git checkout -- file"命令会将工作区中的最新的变更撤销,将文件的状态还原成上次暂存时的状态,如果暂存区中并没有对应的文件的变更,那么"git checkout -- file"命令会将工作区中的变更撤销,将文件的状态还原成上次提交时的状态。
小结
将上述命令进行总结,以便回顾

首先要注意,如果提交已经推送到远程仓库,操作这些提交时需谨慎。

撤销已经添加到暂存区中的修改,即让暂存区与最近的提交保持一致,可以使用如下命令,如下三条命令等效
git reset
git reset HEAD
git reset --mixed HEAD
也可以针对某些文件撤销暂存区中的修改,命令如下
git reset HEAD -- file1 file2
撤销所有暂存区和工作区中的所有变更
git reset --hard HEAD
回退到指定的提交
git reset --hard commitID
你已经将部分提交暂存到了暂存区,然后继续在工作区工作,工作区产生了新的变更,但是这些新变更没有添加到暂存区,此时你创建了提交,刚刚创建完提交你就后悔了,你想要的回到提交创建前一刻的状态,可以使用如下命令
git reset --soft HEAD~
使用如下命令可以撤销工作区中file1文件的相关变更,可以细分为两种情况
git checkout -- file1
情况一:你先修改了file1,并且暂存了,然后又修改了file1,在工作区产生了新的变更,此时执行上述命令,会将工作区中最新的变更撤销,工作区中的file1将会变成暂存区中file1的状态。
情况二:你修改了file1,暂存区中没有file1相关的变更,此时执行上述命令,会将工作区中最新的变更撤销,工作区中的file1将会变成最近一次提交中file1的状态。

这篇文章就先总结到这里,希望能够对你有所帮助,加油~






欢迎光临 黑帽联盟 (https://bbs.cnblackhat.com/) Powered by Discuz! X2.5