一. 什么是分支
Git中的分支,其实本质上仅仅是个指向commit对象的可变指针。Git会使用master作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的master分支,它在每次提交的时候都会自动向前移动。
图 1. 分支其实就是从某个提交对象往回看的历史
二. 如何创建一个新的分支
采用git branch 命令,这回在当前的commit对象上新建一个分支指针:
三. Git是如何知道当前在哪个分支上工作
其实答案也很简单,它保存着一个名为 HEAD 的特别指针。
在 Git 中,它是一个指向你正在工作中的本地分支的指针(译注:将 HEAD 想象为当前分支的别名。)。每次提交后,HEAD随着分支一起向前移动。
创建和销毁一个分支是非常廉价的:由于 Git 中的分支实际上仅是一个包含所指对象校验和(40 个字符长度 SHA-1 字串)的文件,所以创建和销毁一个分支就变得非常廉价。说白了,新建一个分支就是向一个文件写入 41 个字节(外加一个换行符)那么简单,当然也就很快了。
四. 分支的新建与合并
1. 创建远程分支
直接基于master分支创建一个分支名dev2,且这个名字也是我想要的远程分支名。
如果执行git push origin dev2(或者git push origin dev2:dev2)后,就会在远程创建一个dev2的远程分支,它的所有代码是基于本地dev2。但是本地dev2和远程dev2并没有track起来,
当然还可以通过$ git remote add origin ssh://git@dev.lemote.com/rt4ls.git 这种方式来创建远程分支
2. 创建和远程分支关联的本地分支(跟踪分支)
(1)方法一
已经创建了dev3,但是没有和远程track起来,这个时候需要checkout到本地dev3分支上,执行这个命令,就能和远程track起来
$ git branch --set-upstream-to origin/dev3
Branch 'dev3' set up to track remote branch 'dev3' from 'origin'.
(2)方法二
直接新建一个本地分支,并和远程track 起来
git checkout --track origin/branch_name 这个时候就会在本地创建一个和远程同名的分支,且track起来
3. 分支的合并
分支的合并分成了两种,分为下面图1和图2两种情况:
(1)图1解释
图1 hotfix分支是从master分支所在点分化出来的
假设hotfix和issue53分支都是在C2的时候,从master分支创建出来的,在hotfix上面有一个或者多个提交(C4),现在要将hotfix合并到master分支上去,这个时候可以直接跳到master分支上,并且执行git merge hotfix,完全没有冲突,会自动merge。并且,master的指针会指到C4处,这个时候可以把Hotfix分支删除了。
Note:上面的现象也可以成为Fast forward. 由于当前 master 分支所在的提交对象是要并入的 hotfix 分支的直接上游,Git 只需把 master 分支指针直接右移。换句话说,如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。
(2)图2解释
图2:issue53分支可以不受影响的继续推进
图2,Git 没有简单地把分支指针右移,而是对三方合并后的结果重新做一个新的快照,并自动创建一个指向它的提交对象(C6)(见图 3-17)。这个提交对象比较特殊,它有两个祖先(C4 和 C5)。
值得一提的是 Git 可以自己裁决哪个共同祖先才是最佳合并基础。切换到master,合并issue3。
图3 Git自动创建了一个包含了合并结果的提交对象
note: 如果之前因为Hotfix分支和issue53有相同的修改文件,这个时候执行merge会产生冲突,那么就需要进行解决了。
(3)合并冲突解决
比如下面:
可以看到 =======
隔开的上半部分,是 HEAD
(即 master
分支,在运行 merge
命令时所切换到的分支)中的内容,下半部分是在 iss53
分支中的内容。解决冲突的办法无非是二者选其一或者由你亲自整合到一起。
我们可以手动修改,也可以使用git mergetool,修改完了以后再git add,一旦git add,就会认为所有的冲突已经解决好了。
五. 分支的管理
git branch
命令不仅仅能创建和删除分支,如果不加任何参数,它会给出当前所有分支的清单:
注意看 master
分支前的 *
字符:它表示当前所在的分支。也就是说,如果现在提交更新,master
分支将随着开发进度前移。若要查看各个分支最后一个提交对象的信息,运行 git branch -v
:
要从该清单中筛选出你已经(或尚未)与当前分支合并的分支,可以用 --merge
和 --no-merged
选项(Git 1.5.6 以上版本)。比如用 git branch --merge
查看哪些分支已被并入当前分支(译注:也就是说哪些分支是当前分支的直接上游。):
之前我们已经合并了 iss53
,所以在这里会看到它。一般来说,列表中没有 *
的分支通常都可以用 git branch -d
来删掉。原因很简单,既然已经把它们所包含的工作整合到了其他分支,删掉也不会损失什么。
另外可以用 git branch --no-merged
查看尚未合并的工作:
它会显示还未合并进来的分支。由于这些分支中还包含着尚未合并进来的工作成果,所以简单地用 git branch -d
删除该分支会提示错误,因为那样做会丢失数据:
不过,如果你确实想要删除该分支上的改动,可以用大写的删除选项 -D
强制执行,就像上面提示信息中给出的那样。
六. 远程分支
我们用 (远程仓库名)/(分支名)
这样的形式表示远程分支。比如我们想看看上次同 origin
仓库通讯时 master
分支的样子,就应该查看origin/master
分支。如果你和同伴一起修复某个问题,但他们先推送了一个 iss53
分支到远程仓库,虽然你可能也有一个本地的 iss53
分支,但指向服务器上最新更新的却应该是 origin/iss53
分支
举例说明。假设你们团队有个地址为 git.ourcompany.com
的 Git 服务器。如果你从这里克隆,Git 会自动为你将此远程仓库命名为 origin
,并下载其中所有的数据,建立一个指向它的 master
分支的指针,在本地命名为 origin/master
,但你无法在本地更改其数据。接着,Git 建立一个属于你自己的本地 master
分支,始于 origin
上 master
分支相同的位置,你可以就此开始工作
可以运行 git fetch origin
来同步远程服务器上的数据到本地。该命令首先找到 origin
是哪个服务器(本例为 git.ourcompany.com
),从上面获取你尚未拥有的数据,更新你本地的数据库,然后把 origin/master
的指针移到它最新的位置上
Note:当有多个远程分支的时候,可以git fetch 远程分支名来指定更新固定的远程数据到本地
删除远程分支:
直接Push一个空的分支到远程就可以了:git push [远程名] [本地分支]:[远程分支]
语法,如果省略 [本地分支]
,那就等于是在说“在这里提取空白然后把它变成[远程分支]
七. 分支的衍合
把一个分支中的修改整合到另一个分支的办法有两种:merge
和 rebase
(译注:rebase
的翻译暂定为“衍合”)
1. 回顾merge方式
现在切换到master分支,并且执行merge操作后:
git checkout master
git merge experiment
就会新产生一个提交记录C5,用它来整合了分叉的历史
merge
命令,它会把两个分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)进行三方合并,合并的结果是产生一个新的提交对象(C5)
2. rebase使用
还有另外一个选择:你可以把在 C3 里产生的变化补丁在 C4 的基础上重新打一遍。在 Git 里,这种操作叫做衍合(rebase)。有了 rebase
命令,就可以把在一个分支里提交的改变移到另一个分支里重放一遍。
在上面这个例子中,运行:(note:这里是切换到被合并的分支,让这个分支rebase到master,区别于merge命令的次序)
它的原理是回到两个分支最近的共同祖先,根据当前分支(也就是要进行衍合的分支 experiment
)后续的历次提交对象(这里只有一个 C3),生成一系列文件补丁,然后以基底分支(也就是主干分支 master
)最后一个提交对象(C4)为新的出发点,逐个应用之前准备好的补丁文件,最后会生成一个新的合并提交对象(C3'),从而改写 experiment
的提交历史,使它成为 master
分支的直接下游,如图4所示:
图 4. 把 C3 里产生的改变到 C4 上重演一遍。
现在回到 master
分支,进行一次快进合并(见图 5):
图5. master的快进
使用rebase会得到更为整洁的提交历史。merge和rebase都会得到相同的快照内容,只是提交历史信息不同。衍合是按照每行的修改次序重演一遍修改,而合并是把最终结果合在一起。
note:上面也可以采用git rebase master experment,就可以避免先checkout这个操作了。
3. rebase也可以放到其它分支进行,并不一定非得根据分化之前得分支
比如说现在有如下关系得分支提交记录:
server分支创建得基底是master分支,而client分支创建得基底是server分支
client分化之前得分支是server,但是现在想把client分支rebase到master分支上面去,这个时候就可以采用rebase --onto命令来进行,选项会指定新得基底分支是master:
接下来快进master分支:
现在我们决定把 server
分支的变化也包含进来。我们可以直接把 server
分支衍合到 master
,而不用手工切换到 server
分支后再执行衍合操作 — git rebase [主分支] [特性分支]
命令会先取出特性分支 server
,然后在主分支 master
上重演(就可以省区分支切换了,然后再rebase得操作):
最终得提交历史
4. rebase得风险
奇妙的衍合也并非完美无缺,要用它得遵守一条准则:
一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行衍合操作。
在进行衍合的时候,实际上抛弃了一些现存的提交对象而创造了一些类似但不同的新的提交对象。如果你把原来分支中的提交对象发布出去,并且其他人更新下载后在其基础上开展工作,而稍后你又用 git rebase
抛弃这些提交对象,把新的重演后的提交对象发布出去的话,你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容时,提交历史就会变得一团糟。
下面我们用一个实际例子来说明为什么公开的衍合会带来问题。假设你从一个中央服务器克隆然后在它的基础上搞了一些开发,提交历史类似图 3-36 所示:
图 3-36. 克隆一个仓库,在其基础上工作一番。
现在,某人在 C1 的基础上做了些改变,并合并他自己的分支得到结果 C6,推送到中央服务器。当你抓取并合并这些数据到你本地的开发分支中后,会得到合并结果 C7,历史提交会变成图 3-37 这样:
图 3-37. 抓取他人提交,并入自己主干。
接下来,那个推送 C6 上来的人决定用衍合取代之前的合并操作;继而又用 git push --force
覆盖了服务器上的历史,得到 C4'。而之后当你再从服务器上下载最新提交后,会得到:
图 3-38. 有人推送了衍合后得到的 C4',丢弃了你作为开发基础的 C4 和 C6。
下载更新后需要合并,但此时衍合产生的提交对象 C4' 的 SHA-1 校验值和之前 C4 完全不同,所以 Git 会把它们当作新的提交对象处理,而实际上此刻你的提交历史 C7 中早已经包含了 C4 的修改内容,于是合并操作会把 C7 和 C4' 合并为 C8(见图 3-39):
图 3-39. 你把相同的内容又合并了一遍,生成一个新的提交 C8。
C8 这一步的合并是迟早会发生的,因为只有这样你才能和其他协作者提交的内容保持同步。而在 C8 之后,你的提交历史里就会同时包含 C4 和 C4',两者有着不同的 SHA-1 校验值,如果用 git log
查看历史,会看到两个提交拥有相同的作者日期与说明,令人费解。而更糟的是,当你把这样的历史推送到服务器后,会再次把这些衍合后的提交引入到中央服务器,进一步困扰其他人(译注:这个例子中,出问题的责任方是那个发布了 C6 后又用衍合发布 C4' 的人,其他人会因此反馈双重历史到共享主干,从而混淆大家的视听。)。
git rebase
是对commit history的改写。当你要改写的commit history还没有被提交到远程repo的时候,也就是说,还没有与他人共享之前,commit history是你私人所有的,那么想怎么改写都可以。
而一旦被提交到远程后,这时如果再改写history,那么势必和他人的history长的就不一样了。git push
的时候,git会比较commit history,如果不一致,commit动作会被拒绝,唯一的办法就是带上-f
参数,强制要求commit,这时git会以committer的history覆写远程repo,从而完成代码的提交。虽然代码提交上去了,但是这样可能会造成别人工作成果的丢失,所以使用-f
参数要慎重。
如果把衍合当成一种在推送之前清理提交历史的手段,而且仅仅衍合那些尚未公开的提交对象,就没问题。如果衍合那些已经公开的提交对象,并且已经有人基于这些提交对象开展了后续开发工作的话,就会出现叫人沮丧的麻烦。
5. 避免git rebase产生的git push -f 操作。(慎用git push -f)
rebase风险主要是会导致git push -f 这种操作,因此,可以通过某些手段避免:就是改写了公有的commit history造成的。要解决这个问题,就要从提交流程上做规范。(note: eg提交流程规范根据单位自己开发团队统一规定,rebase可以用,但是要注意使用的步骤规范,避免风险;)
举个正确流程的栗子:
假设楼主的team中有两个developer:tom和jerry,他们共同使用一个远程repo,并各自clone到自己的机器上,为了简化描述,这里假设只有一个branch:master
。
这时tom机器的repo有两个branchmaster
, origin/master
而jerry的机器上也是有两个branchmaster
, origin/master
均如下图所示
tom和jerry分别各自开发自己的新feature,不断有新的commit提交到他们各自私有的commit history中,所以他们的master指针不断的向前推移,分别指向不同的commit。而又由于他们都没有git fetch
和git push
,所以他们的origin/master
都维持不变。
jerry的repo如下
commit history出现了分叉,要想把tom之前提交的内容包含到自己的工作中来,有一个方法就是git merge
,它会自动生成一个commit,既包含tom的提交,也包含jerry的提交,这样就把两个分叉的commit重新又合并在一起。但是这个自动生成的commit会有两个parent,review代码的时候必须要比较两次,很不方便。
jerry为了保证commit history的线性,决定采用另外一种方法,就是git rebase
。jerry的提交J1
这时还没有被提交到远程repo上去,也就是他完全私有的一个commit,所以使用git rebase
改写J1
的history完全没有问题,改写之后,如下
7. 保持历史提交的方法
这里例举一个方法,可能还有别的方法,欢迎补充:准备两个分支,master分支用来放真正稳定发布版本记录。然后就各个feature的分支,分支上正常提交,可以用rebase,可以用merge。但是一旦一个feture开发
完了以后,就只能保证这个feature往master上面合并的时候,只有一条提交记录.。这样一个feature就只有一条提交记录,就可以避免mster上面的提交历史凌乱的问题。
参考文献:
《Pro Git》
https://segmentfault.com/q/1010000000430041