git rebase的用法

495 阅读4分钟

git 作为代码管理工具,我们通常使用 merge(普通合并)和 rebase(衍合或变基)来合并代码,它们的区别在哪呢?

快进合并

什么是快进合并?如下,合并 dev 分支到 master 分支时,master 分支在 dev 分支的起点 B 后并没有做任何更改,dev 分支包含所有 master 分支上的历史,因此只要把 master 分支的位置移动到 dev 的最新提交上,分支就合并了,这叫做快进(fast-forward)合并。

                 dev
                  |
      C1━━━>D1━━━>E1
      /
A━━━>B
     |
   master

//快进合并
                     dev
                      |
 A━━━>B━━━C1━━━>D1━━━>E1
                      |
                    master

基本的合并操作

如下,在 commit 为 B 时开出一个分支 dev,并开发到 E1,master 分支也到了 D2。

                 dev
                  |
      C1━━━>D1━━━>E1
      /
A━━━>B━━━>C2━━━>D2
                |
              master

将分支 dev merge 到 master:

//在master分支上
$ git merge dev

可以看到,merge 会产生一个多余的提交历史 F:

                  dev
                  |
      C1━━━>D1━━━>E1
      /            \
A━━━>B━━━>C2━━━>D2━━F
                    |
                  master

通常,我们在开发分支,想要与 master 的更新保持一致,就可以在开发分支 rebase master。

//在dev分支上
$ git rebase master
                              dev
                               |
      C2━━━>D2━━━C1'━━━>D1'━━━>E1'
      /
A━━━>B━━━>C2━━━>D2
                |
              master

可以看到,dev 分支上原有的 C1、D1、E1 被删除后重新提交为 C1'、D1'、E1'了,它的原理是找到两个分支最近的祖先提交(B),然后根据当前分支(dev,要衍合的分支)后续的提交生成一些列补丁文件(C1'、D1'、E1'),然后以基底分支(master 分支)最后一个提交为基准点(D2),逐个应用这些补丁文件,生成新的提交,从而改变了提交历史,使其成为 master 分支的直接下游。 现在就可以回到 master 分支进行快进合并了,最终的提交历史如下所示,比较干净整洁。

                                dev
                                 |
A━━━B━━━C2━━━>D2━━━C1'━━━>D1'━━━>E1'
                                 |
                               master

rebase 存在的问题

考虑如下的情景,我的项目里有两个分支,一个主分支 master,一个开发分支 dev。

   dev
    |
A━━━B
    |
  master

现在小明在 dev 分支上开发,并把新的提交 C1、C2 推送到了远程服务器(origin/dev)。

            dev
             |
       C1━━━>C2
      /
A━━━B
    |
  master

小红在 dev 分支小明的提交基础上高高兴兴地开发,并新提交了 C3、C4。

                        dev
                         |
       C1━━━>C2━━━>C3━━━>C4
      /
A━━━B
    |
  master

现在小明想同步 master 上的最新内容(小东提交到 master 上的 C),在 dev 上进行了 rebase,之前的提交 C1、C2 被删除掉变成了 C1'和 C2'了,小明继续开发到了 D1。

                       dev
                        |
          C1’━━━>C2‘━━━>D1
         /
A━━━B━━━C
        |
      master

小明接着把当前的修改推送到远程服务器,结果报错:

 ! [rejected]        dev -> dev (non-fast-forward)
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.

表示不能进行快进合并,小明推送前服务器端的 dev 分支如下:

               dev
                |
A━━━>B━━━>C1━━━>C2

显然小明本地的分支因为 rebase,原有的提交 C1、C2 没有了,也就说小明本地的 dev 分支并没有包含服务器端 dev 分支的全部提交历史,也就不能进行提交。只能先 pull 服务器端的代码并解冲突。

          remotes/origin/dev
                  |
      C1━━━━━━━━━>C2
     /              \
A━━━B    🤣🤣🤣🤣     \
     \                \
      C━>C1’━>C2‘━>D1━>F
                       |
                      dev

提交线图就变成了下面这样:

可以看到小明本地实际上已经提交过C1、C2了,因为rebase的关系变成了C1'、C2',与服务器中的C1、C2 SHA-1不一样了,从而当新的提交处理。于是我们就能看到提交信息、内容一样的提交,提交线就会变得混乱,让人感到困惑。 当小红拉取小明的代码后,因为在小红的本地存在已经被撤销的C1、C2,她不得不再次合并有着同样内容的C1'、C2'的提交,事情变得更加糟糕。这里,问题就出在小明身上,他发布了C1、C2,又通过rebase重新发布了C1’、C2‘。这就是rebase出问题的本质,如果可以明确知道当前的rebase操作不会撤销已发布的提交,就可以放心的使用。通常,大家在同一个仓库上进行操作,在本地通过git pull --rebase来拉取代码时就不会有太大的问题,因为对于当前分支而言,新增的提交通常是未发布的。

关于 rebase,有一条金科玉律,就是不要在公共分支进行 rebase。 如果在已经被推送到公共仓库的提交上执行变基命令,并因此丢弃了一些别人的开发所基于的提交,那你就有麻烦了,面对同事的疯狂吐槽吧!!!