【Howto】在Git中怎么回滚

其实,这个是一类问题,在SVN里面,处理这个问题相对简单和直观,因为SVN的结构简单,只有工作副本和版本库副本两个副本,你要做的事情,无非就是处理工作副本,要么就是处理版本库副本。关于位置的概念,只有两个,工作副本,都是本地镜像,版本库副本,都在中心服务器上。commit能且只能保存在服务器上。回滚,本质上,只有两个情况,一个是没有commit,怎么回滚,一个是已经commit了,怎么回滚。

所以,在SVN中,回滚一个代码,事实上只有一个命令而已。svn revert。不指定版本号作为参数的话,就是放弃未提交的修改,指定版本号的话,就是回滚到指定的版本号(rev),但是实际上已经提交的更改,是不可以抹除的,你只能将一个文件回滚到某个过去的版本,然后重新提交来覆盖之前的变更。

到了Git里面,回滚这个事情,变复杂了。首先,我认为,工作副本的概念没有了,因为去中心化的设计,每个人本地硬盘上拥有的,都直接是版本库副本,没有所谓的本地副本的概念了,所有的人持有的都是版本库本身。就算在研发实践中,设置了中央版本库,但是中央版本库和本地版本库,在逻辑概念上是完全对等的。谁是中央,谁不是中央,其实是人为赋予的抽象地位。

另外一个显著的不同是,Git里的commit,都是直接提交到本地版本库的,这也就是commit速度飞快的原因。

在Git里,回滚这件事情,就一下子多出来好多的情况。

如果是一个人,直接通过git init在本地建立repository,不与任何人协作,有且仅有一个版本库。那么,情况只有两种:

  1. 未提交的变更
  2. 已经提交的变更

第一种情况,可以使用命令 git clean -fd ,来实现直接将所有的未提变化(untracked files)删除,-f的含义是force,强制,-d的意思是未提交的目录和文件都删除。

第二种情况,在单人开发的时候,也比较简单。很多处理方法,比如完全抹去这个commit(这在SVN里是做不到的),就像历史上从未发生过此事一样。要做到这一点,估计有好多种做法,但是我只掌握了一种,就是使用 git rebase -i HEAD2,这个命令的含义本身不是干这个事情的,只是我碰巧发现这么可以。-i的含义是interact,交互式,HEAD2的含义是,从最新的提交开始,显示两次提交的内容,然后你会在编辑页面看到两行提交的log,旧的在上面,新的在下面。如果你需要完全抹去最新的提交,就把下面的那行给删掉就行了。

这种情况下,根据我的实践经验来看,如果是一系列提交,要抹去中间的某个提交,十有八九不会顺利成功,原因也很简单,比如某a文件,你提交了10次,然后你想抹去第6次提交,那么git将极有可能不知道原来的第7次到第10次提交,应该怎么作用在a文件上,这时候,rebase过程被打断,你要手动处理冲突,才能完成任务。但是我想,一般我们不会干这种事情,因为,多数情况下,我们需要处理的都是最新几次连续的提交,比如想都放弃掉。

如果是跟他人协作开发的话,比如有一个管理员,设置了一个中央版本库,然后大家都以中央版本库的代码为基准,事情就将变得更加复杂一点,在上述两种情况之外,又会多出一些情况:

  1. 已经提交了,但是没有push到中央版本库
  2. 已经提交了,但是已经push到中央版本库

第一种情况,跟单人开发时候,第二种情况处理方法一样的。但是,你要非常小心的确认这件事情。该commit确实没有被push过。这种情况下,你甚至可以抹掉该commit,或者合并几个同样没有push的commit,都行。

第二种情况,你就要非常谨慎了。因为即使已经push过了,git不会阻止试图抹掉一个commit的行为,也不会阻止试图合并一些commit的行为。但是,你可能为此付出非常惨痛的代价,光是merge,就能累死你。这种情况下,我的建议,就是根本不要尝试去做。老老实实去checkout一个你认为没问题的版本,通过diff工具,将你想删掉的变更去掉,然后将这个作为一个新的commit附加到历史的洪流中。