git撤销commit,回退已经push的文件

文 | 夕月

本文介绍通过命令 git reset, git rebase, git revert 实现对已commit或push的文件进行撤销操作

图示环境是 MacOS + GitLab + Sourcetree + Shell。
其中GitLab是笔者在MacOS上借助Docker自行搭建的git仓库管理系统。

1. 先来看 git reset 的用法

《git撤销commit,回退已经push的文件》

多次commit之后,test分支的文件状态:
《git撤销commit,回退已经push的文件》

需求:将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,依次类推。

  1. 执行 git reset --soft HEAD~1 后,状态如下:
    《git撤销commit,回退已经push的文件》

  2. 执行 git reset --mixed HEAD~1 后,状态如下:
    《git撤销commit,回退已经push的文件》

  3. 执行 git reset --hard HEAD~1 后,状态如下:
    《git撤销commit,回退已经push的文件》

可见,“commit fff file”{cid:dd55e5e} 这条记录在 git log 打印的提交日志中都不存在了。只是三种方式对工作空间的改动不一样。

  • –soft 保留已撤销commit的代码变更,不会撤销git add,
  • –mixed 保留已撤销commit的代码变更,撤销git add,
  • –hard 删除已撤销commit的代码变更,撤销git add,工作空间回到上一次commit状态。

 
如果一次撤销多个commit,工作区状态如何?
《git撤销commit,回退已经push的文件》

 

2. git rebase 撤销某个commit

如果不是撤销最近的一个或多个commit,而是撤销某个commit呢?例如:
《git撤销commit,回退已经push的文件》

先来看看git reset 能否做到。git reset有个用法是git reset --soft/--mixed/--hard commitId,如果用git reset --soft d9a16cc,会撤销eee的提交记录吗?
《git撤销commit,回退已经push的文件》

看来并不能!!! 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编辑框,
《git撤销commit,回退已经push的文件》

按照图示将 "commit eee file" 左侧的pick改为d或者drop后,会丢掉对应的commit。从而达到撤销的目的。
《git撤销commit,回退已经push的文件》

用 git rebase 实现撤销和 git reset –hard 的效果类似,即「删除已撤销commit的代码变更,撤销git add,工作空间回到上一次commit状态」。如果被撤销commit的代码还有用,使用时须谨慎。

git rebase命令可以做很多工作,例如优化本地分支的提交记录,分支线性化处理(避免过多的merge出现)等等。

 

3. 撤销已push的文件

主要思路就是在本地分支撤销了commit之后,将变更推送到远端。但必须用git push -f强制提交,否则会提交失败,原因是:本地的版本号低于远端的版本号。

《git撤销commit,回退已经push的文件》

需要注意的是,如果test分支不只是你自己一个人维护,别人也在向这个分支上push代码,在进行强制推送之前就要注意下了,有可能会把别人的提交撤销掉。


如果你是项目的owner,在本地master分支使用git rebase 或者 git reset撤销了一些commit之后,想要强制推送到远端,以使远端的记录也撤销掉。你会使用git push origin master -f,但可能会遇到下面的错误。
《git撤销commit,回退已经push的文件》

意思就是master分支是“protected branch”,不允许强制变更。解决方法是登录GitLab,进入项目的设置页面,选择Repository,找到Protected Branches,对分支进行「Unprotect」即可。
《git撤销commit,回退已经push的文件》

风险问题大家根据项目情况自行评估。

「Unprotect」之后 push -f 就可以成功了。
《git撤销commit,回退已经push的文件》

 

4. git revert 回退

git revert 是一个很安全也很好用的命令,不同于git reset的重置,它是通过反向操作来完成撤销的。先来看用法。

需求:撤销“commit eee file”{cid:d9a16cc} 的变更。
《git撤销commit,回退已经push的文件》

git revert 后面一般跟commitId, 是你想回退的commit的id。例如在上图示例中,我想回退eee的提交,则commitId即是「d9a16cc」。
 
git revert 执行后会自动生成一个类似「Revert "commit message"」的新的commit。该commit的内容和需要revert的内容相反。若回退前新增了一个文件,revert后会将该文件删除;若会提前删除了一个文件的一行代码,revert后会将该文件的该行代码补回来。
 
如果需要将撤销更新到远端,push即可,不需要push -f。

 

总结1

  1. git rebase (drop) 相当于 get reset –hard,不会保留要撤销commit的代码变更。
  2. git reset 和 git rebase (drop) 都是通过删除之前commit的方式,达到撤销操作的目的,而 git revert 则是通过自动的反向操作完成这一目的。不同于前两个指令,git revert的HEAD指针是继续前进的。
  3. 根据要撤销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 回退了撤销操作。

 

推荐阅读
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注