基于开源项目的开发有两种主要工作模式。模式1是在从开源项目中拉出一个分支,在这个分支中开发新feature,完成后合并到upstream中。适用于本身是开源项目的developer。模式2是从开源项目中拉出分支后独立发展,但定期从upstream拉更新(如重要版本升级时)。无论是哪种,都会面临本地分支与upstream同步代码的问题。为此,git主要提供了两种方式:一种是merge,
一种是rebase。下面通过例子简单过一下它们的基本流程。
假设开源项目的git地址为git://xxx.org/xxx/project。早先从它上面拉出代码,作为git repository放在在本地的代码管理服务器上(比如gerrit服务器)。而在本地的开发机上有本地服务器上该项目git的clone。这样,在本地开发机上执行git remote -v结果类似于:
local ssh://jzj@gerrit3.company.com:xxxxx/xxx/project (fetch)
local ssh://jzj@gerrit3.company.com:xxxxx/xxx/project (push)
首先将开源项目git加入remote源列表,名称暂定为upstream。
$ git remote add upstream git://xxx.org/xxx/project
再将新加源的信息同步下来:
$ git fetch
然后git remote -v结果就变成:
upstream git://xxx.org/xxx/project (fetch)
upstream git://xxx.org/xxx/project (push)
local ssh://jzj@gerrit3.company.com:xxxxx/xxx/project (fetch)
local ssh://jzj@gerrit3.company.com:xxxxx/xxx/project (push)
现在当前git已经与两个源关联上了。
接下来从upstream源新建分支,称为upstream,它与开源项目git中的代码一致:
$ git checkout remotes/upstream/master -b upstream
假设其中git log为:
Commit 2 from branch upstream 2015/1/3
Commit 1 from branch upstream 2015/1/1
再从local源拉出分支,称为topic,它包含本地改动(比如新加feature):
$ git checkout remotes/local/master -b topic
当然,如果将来要提交到到gerrit服务器上,这儿用repo start topic .。假设其中log为:
Commit 1 from branch topic 2015/1/2
Merge方式是把branch A在branch B上没有的commit合并成一个commit,然后打在branch B上。如果要让topic分支同步upstream中的改动,执行下面命令:
$ git checkout topic
$ git merge upstream
如果想让upstream同步topic改动,把上面命令中分支名的位置换下就行。如果有冲突的话会让你解决(通过git diff查看冲突),解决完了以后git add + git commit(或git commit -a)。完成后在topic分支的git log里看到的log有类似下面的结果:
Merge branch ‘upstream‘ into topic
Commit 2 from branch upstream 2015/1/3
Commit 1 from branch topic 2015/1/2
Commit 1 from branch upstream 2015/1/1
两个branch中的commit以时间顺序排列,最顶上这个commit代表了这次merge的bundle commit。把它git reset掉的话所有原upstream上同步的改动都会消失。
Rebase方式是把branch A在branch B上没有的commit挨个在branch B上再重新打一遍。通过git rebase的文档,可以看到如果是git rebase upstream topic的话相当于git checkout topic + git rebase upstream。即把topic分支上的commit以upstream的HEAD为base重新打一遍,重复的commit会自动忽略。因此,如果是前面提到的工作模式1的话就用git
rebase topic upstream,工作模式2的话就用git rebase upstream topic。一般为了避免whitespace带来的错误,会加上--ignore-whitespace参数:
$ git rebase upstream topic --ignore-whitespace
更细粒度的rebase控制请参见-i参数。
在rebase过程中可能出现冲突,出现冲突时先用git diff看冲突情况。比如是a.c的话,解决冲突后执行:
$ git add a.c
然后执行下面命令继续欢快地rebase:
$ git rebase --continue
完成后topic分支的git log里是类似于这样的:
Commit 1 from branch topic 2015/1/2
Commit 2 from branch upstream 2015/1/3
Commit 1 from branch upstream 2015/1/1
可以看到,这里不是按时间顺序的。topic分支的commit在上面,而upstream的commit在下面。如果是git rebase topic upstream的话顺序就是相反的。最后,如果还需要让upstream分支也同步topic分支的代码。可以到upstream分支做一次fast-forward merge:
$ git checkout upstream
$ git merge topic
妥妥的。
同步完了之后,该git push就git push,该repo upload就repo upload,将同步的代码更新到本地的代码管理服务器上。
总结一下,从工作方式来说,merge与rebase最大的差别就是前者是把所有要打的commit作为一整个commit打到目标分支上,后者是一个个重新打(但注意打上去后<SHA1>不同了,也就是说,虽然内容相同,但此commit已经不是原来那个了)。从结果来看,merge后log中commit是按时间顺序的,最近的一个commit记录了这次merge操作。rebase后log中不是按时间顺序的,而是一个分支的commit在另一个分支的commit之后。总得来说,rebase比merge更灵活,更强大。但它的缺点是会改写历史。因此,不要对public的分支做rebase,否则你会冒着被同事砍死的风险(因为他可能基于改写前的历史做了改动)。基本原则是,当从远端同步代码到本地时,用rebase。当本地完成feature开发,同步回远端时用merge。