本文介绍通过命令 git reset
, git rebase
, git revert
实现对已commit或push的文件进行撤销操作。
图示环境是 MacOS + GitLab + Sourcetree + Shell。 其中GitLab是笔者在MacOS上借助Docker自行搭建的git仓库管理系统。
1. 先来看 git reset 的用法
多次commit之后,test分支的文件状态:
需求:将test分支最近一次提交的 “commit fff file”{cid:dd55e5e} 这条记录撤销
git reset --soft HEAD~1
撤销最近一次的commit(撤销commit,不撤销git add)
git reset --mixed HEAD~1
撤销最近一次的commit(撤销commit,撤销git add)
git reset --hard HEAD~1
撤销最近一次的commit(撤销commit,撤销git add,工作区的代码改动将丢失。操作完成后回到上一次commit状态)
HEAD~1
的意思是最近一个版本,也可以写成HEAD^
如果需要撤回最近两次提交的commit,可以使用HEAD~2
,依次类推。
-
执行
git reset --soft HEAD~1
后,状态如下: -
执行
git reset --mixed HEAD~1
后,状态如下: -
执行
git reset --hard HEAD~1
后,状态如下:
可见,“commit fff file”{cid:dd55e5e} 这条记录在 git log
打印的提交日志中都不存在了。只是三种方式对工作空间的改动不一样。
- --soft 保留已撤销commit的代码变更,不会撤销git add,
- --mixed 保留已撤销commit的代码变更,撤销git add,
- --hard 删除已撤销commit的代码变更,撤销git add,工作空间回到上一次commit状态。
如果一次撤销多个commit,工作区状态如何?
2. git rebase 撤销某个commit
如果不是撤销最近的一个或多个commit,而是撤销某个commit呢?例如:
先来看看git reset 能否做到。git reset有个用法是git reset --soft/--mixed/--hard commitId
,如果用git reset --soft d9a16cc
,会撤销eee的提交记录吗?
看来并不能!!! git reset --soft commitId
的用法只是把HEAD指针指向${commitId}对应的提交记录,该记录之后的提交记录会被撤销。
如果${commitId}是最近的第二次提交,`git reset --soft commitId`相当于`git reset --soft HEAD~1`,如果${commitId}是最近的第三次提交,则相当于HEAD~2,依次类推。
用git rebase可以做到撤销某个commit。
如果要撤销“commit eee file”{cid:d9a16cc}这个提交,使用git rebase -i 9df3805
,其中「9df3805」是eee的上一次提交的commitId。当然,也可以使用git rebase -i HEAD~2
。
执行 git rebase -i 9df3805
之后,会出现下面的交互式vim编辑框,
按照图示将 "commit eee file" 左侧的pick改为d或者drop后,会丢掉对应的commit。从而达到撤销的目的。
用 git rebase 实现撤销和 git reset --hard 的效果类似,即「删除已撤销commit的代码变更,撤销git add,工作空间回到上一次commit状态」。如果被撤销commit的代码还有用,使用时须谨慎。
git rebase命令可以做很多工作,例如优化本地分支的提交记录,分支线性化处理(避免过多的merge出现)等等。
3. 撤销已push的文件
主要思路就是在本地分支撤销了commit之后,将变更推送到远端。但必须用git push -f
强制提交,否则会提交失败,原因是:本地的版本号低于远端的版本号。
需要注意的是,如果test分支不只是你自己一个人维护,别人也在向这个分支上push代码,在进行强制推送之前就要注意下了,有可能会把别人的提交撤销掉。
如果你是项目的owner,在本地master分支使用git rebase 或者 git reset撤销了一些commit之后,想要强制推送到远端,以使远端的记录也撤销掉。你会使用git push origin master -f
,但可能会遇到下面的错误。
意思就是master分支是“protected branch”,不允许强制变更。解决方法是登录GitLab,进入项目的设置页面,选择Repository,找到Protected Branches,对分支进行「Unprotect」即可。
风险问题大家根据项目情况自行评估。
「Unprotect」之后 push -f 就可以成功了。
4. git revert 回退
git revert 是一个很安全也很好用的命令,不同于git reset的重置,它是通过反向操作来完成撤销的。先来看用法。
需求:撤销“commit eee file”{cid:d9a16cc} 的变更。
git revert 后面一般跟commitId, 是你想回退的commit的id。例如在上图示例中,我想回退eee的提交,则commitId即是「d9a16cc」。 git revert 执行后会自动生成一个类似「Revert "commit message"」的新的commit。该commit的内容和需要revert的内容相反。若回退前新增了一个文件,revert后会将该文件删除;若会提前删除了一个文件的一行代码,revert后会将该文件的该行代码补回来。 如果需要将撤销更新到远端,push即可,不需要push -f。
总结1
- git rebase (drop) 相当于 get reset --hard,不会保留要撤销commit的代码变更。
- git reset 和 git rebase (drop) 都是通过删除之前commit的方式,达到撤销操作的目的,而 git revert 则是通过自动的反向操作完成这一目的。不同于前两个指令,git revert的HEAD指针是继续前进的。
- 根据要撤销commit所在分支的情况,选择适当的命令。
一般来说,git revert 更安全,但也会生成新的revert commit,如果撤销的commit很多的话,git log 不是很好看(当然,也有办法优化,可以通过git rebase 将多个revert commit 合并成一个。见总结2)。 如果要撤销的commit还没被推到远端,不妨使用 git rebase (drop) 或者 git reset。如果已经被推送到和他人共同维护的远端分支,或者已经被merge到主分支,最好使用git revert。
总结2
如果想使用git revert回退多个commit,且只生成一个 revert commit 提交,以使git log看上去更加简洁(有时候回退的多个commit实为同一功能),可以借助 git rebase 实现。
参考:git revert + git rebase, 一次性回退多个提交 原博主写的很详细,直接转载拿来用了。
正如前文讲的,git rebase 很有用。以后有时间,会讲讲 git rebase 对提交线性化的处理,这个在日常工作中git和svn同步开发时,非常有用。
总结3
如果使用git reset --hard 撤销了commit,而且也推送到了远端,后悔了想要找回已撤销commit的代码变更,不用担心,git reflog 完全可以做到。
或许你会发现,本文的示例图,初始状态都一样,即最近一次提交 "commit fff file"{cid:dd55e5e}。这是因为在做了撤销和回退操作后,都用 git reflog 回退了撤销操作。
推荐阅读
很开心你读完此篇。
我是夕月,程序媛一枚。
30而立,2020年是摸索前行的一年,和星辰一起立下了发文分享的flag。
如果你感兴趣,不妨常来看看,我们成长路上或许会有共鸣。
我们的博客站:xiyuechen.net 微信公众号「星辰和夕月」。