git reset和git revert都可以用于撤销已存在的commit,其中git reset可以细化到针对单个文件进行操作。
首先, 一个Git仓库主要有三个重要的组成:工作目录,缓存区和提交历史,如图所示:
1. git reset
git reset通常用来撤销对缓存区和工作目录的修改。用法如下:
(1)git reset <file>
从缓存区移除特定的文件,但不改变工作目录。
(2)git reset
重设缓存区,匹配最近的一次提交,工作目录不变。
(3)git reset --hard
重设缓存区和工作目录,匹配最近的一次提交。除了取消缓存之外,--hard标记还会清除工作目录中所有未提交的更改,因此使用前确定你想扔掉你所有的本地开发。
(4)git reset <commit>
将当前分支的末端移动到commit,将缓存区重设到这个commit,但不改变工作目录。
(5)git reset --hard <commit>
将当前分支的末端移动到commit,并将缓存区和工作目录都重设到这个commit。
git reset命令会将HEAD指向你指定的commit,也就是说,可以直接通过git reset命令来移除你指定的commit之后的所有commit。例如,下面这两条命令让hotfix分支向后回退了两个commit:
git checkout hotfix
git reset HEAD~2
注意,hotfix分支现在最后的两个commit变成了悬挂提交,这意味着下次git进行垃圾回收的时候,这两个commit就会被删除。因此,如果你希望改变此git仓库的提交历史,你可以使用git reset命令。
除了在当前分支上操作,你还可以传入以下参数来更新缓存区和工作区:
--soft 缓存区和工作区都不会改变
--mixed 默认选项。缓存区和你指定的commit同步,工作区不发生改变。
--hard 缓存区和工作区都同步到你指定的commit
这些参数往往针对HEAD来使用。例如你add了某个你不想add的文件,这时可以运行git --mixed reset HEAD,此时暂存区被清空,而你的工作空间并没有改变。
或者你希望将最近的两个commit合并成为一个,此时你可以运行git --mixed reset HEAD~2,这样你的commit记录被改变,缓存区被清空,但工作空间没有改变,因此你可以重新add,再重新commit,这样两个commit记录就变成了一个。
另一方面,你希望完全放弃你没有提交的改动,你可以运行git reset --hard HEAD,这样你的缓存区和工作空间就被强行更新了。
针对已经提交到远程的commit,你希望reset到HEAD~1,此时运行:
(1)git reset HEAD~1 此时查看status,发现有待stage的文件,因为工作空间的文件与HEAD~1的文件不同,因此运行git checkout future2.txt即可。(或者最开始就运行:git reset --hard HEAD~1)
(2)然后执行强制推送:git push -f
git reset可以针对某个文件,此时reset命令会将你指定的文件加入到缓存区中(因此文件从HEAD状态变成了你指定的某个commit的状态)。
可以看到,暂存区产生了一个变更,这是因为文件从HEAD的内容变成了HEAD~2的内容(逻辑上),实际上,工作区域不会发生改变。而产生Changes not staged for commit的原因是此时工作区的文件和HEAD~2的文件不相同。
将暂存区的内容commit并push之后,此时future2.txt文件内容就回退为了两个commit之前的版本,并且commit log都没有更改。此时查看status:
发现future2.txt依旧待stage,这是因为git reset命令工作区不会发生改变,因此工作区的文件内容与HEAD下的文件内容不一致,此时运行:
git checkout future2.txt
来撤销所有的更改。再次查看status:
并且此时future2的文件内容已经处于最新状态。
git reset总结:(1)git reset在提交级别上,可以撤销已有的commit,并且清除git log,适用于消除还未提交到远程的commit,或者是自己私有的分支,或者并不在意篡改commit log的情况。
(2)git reset针对单个文件时,工作区不会被改变,暂存区一定改变。并且不会清除git log,往往最后需要通过git checkout <file>来同步工作区。 --soft、--mixed 和 --hard 对文件层面的 git reset 毫无作用,因为缓存区中的文件一定会变化,而工作目录中的文件一定不变。
2. git revert
git revert命令可以通过指定commit id来对某个commit进行撤销,此命令会生成一个新的commit来执行撤销动作,不会修改过去已有的commit,这避免了Git丢失项目历史,这一点对你的版本历史和协作的可靠性来说是很重要的。git revert用法如下:
(1)git revert <commit>
生成了一个新的提交,此提交用于撤销<commit>引入的修改。
例如,下面的命令会撤销HEAD前面的第二个commit:
git checkout hotfix
git revert HEAD~2
相比 git reset,它不会改变现在的提交历史。因此,git revert 可以用在公共分支上,git reset 应该用在私有分支上。你也可以把 git revert 当作撤销已经提交的更改,而 git reset HEAD 用来撤销没有提交的更改。
3. git checkout
git checkout这个命令有三个作用:检出文件,检出提交和检出分支。
(1)git checkout master
回到master分支。
(2)git checkout <commit> <file>
查看文件之前的版本。此时工作目录中下的<file>被替换为<commit>中的<file>,并将它加入缓存区。
(3)git checkout <commit>
更新工作目录中的所有文件,使得和某个特定提交下的文件一致。此操作会使你处于HEAD分离的状态,因此是只读操作。
git checkout命令除了用于常见的分支切换以外,还可以用于代码更改的撤销。例如:
当 future2.txt 存在还没有add的changes时,运行git checkout future2.txt 可以撤销future2.txt的更改
git checkout HEAD~2 future2.txt 可以将当前的future2.txt恢复为HEAD~2的版本,此时工作区和暂存区都会改变。
总结:如果文件A修改了,你希望撤销这个修改,那么可以使用以下方案:
(1)修改还未加入暂存区(还没有add),直接运行:
git checkout A
(2)修改已经加入了暂存区,但还没有commit:
git reset HEAD A(修改暂存区)
git checkout A(修改工作区)
(3)修改已经commit
git reset HEAD~1(reset到前一个commit)
git checkout A