译者 | Video++极链信息技术后端Team
重新整理 | 手袋
Git组成部分和组织工作流
组成部分其本质是两个对准递交第一类的气门操作符。Git 留存的并非文档的变化或者差别,而是一连串相同关键时刻的文档镜像。在进行递交操作时,会留存两个递交第一类(commit object),在数次递交后,commit第一类形成连续的镜像链,组成部分操作符手动对准新一代一次递交。Git 的预设组成部分英文名字是 master。如下表所示图:
branch指示能随心所欲建立两个新组成部分,就像这样:
$git branch new_branch
而此指示实际是为当前递交第一类加进了两个捷伊操作符。此种组成部分形式比绝大多数版控制技术更加高性能,不论是建立还是转换都几乎能在一瞬间顺利完成。Git 引导在组织工作业务流程中频密地采用组成部分与分拆,这全然不会增加库房经济负担,并且能如前所述而此优点建立更民主自由和更可信的联合开发业务流程。
很多采用Git 的开发人员都讨厌采用此种形式来组织工作:仅在master组成部分上留存全然稳定的标识符,这些标识符一般来说处于已正式发布或等候正式发布的状况。此外采用一些中长期组成部分,比如说用develop组成部分升级换代优点,采用test组成部分复原bug,试验灵活性,直至标识符产品质量达到正式发布要求,再分拆到master组成部分,顺利完成两个版的开发。
相同的开发人员项目组能民主自由缔造适宜自己组织形式的组成部分思路。街道社区中也存在很多颇受热烈欢迎的业务流程实例,比如说经典之作的gitflow组织工作流、PR组织工作流、封闭式组织工作流之类,它们一般来说适用于于相同的合作形式,并并非这种强制性规范化。有兴趣的听众能继续深入细致积极探索,该处不再过多介绍。
merge
假设我们如前所述master组成部分建立了feature组成部分用来升级换代功能,经过一段时间开发之后,需要把feature的组成部分标识符分拆回到master,一般来说执行的操作是先检出master组成部分,然后执行git merge feature。
一般来说,在单人开发的情况下,merge一般来说会产生快进(fast-forward)形式的分拆。如果在子组成部分(feature)被建立之后,父组成部分(master)未产生捷伊修改和递交,此时把feature分拆回master,Git会在递交链上把master操作符简单的前移,使两个组成部分进度同步,并形成无组成部分记录的递交链。执行时在控制台输出Fast-forward标识。此种merge形式下不会产生冲突,git log指示会看到如下表所示记录:
但在项目组联合开发时,一般来说会多人修改同一远程组成部分。其中采用的pull和push指示实际包含了merge操作。这时git采用另外一种形式来进行组成部分分拆。目前只有一方修改的情况下,也能采用 —no-ff 参数来模拟此种形式。
这里使用了git最基础的三路递归分拆(recursive three-way merge),输出Merge made by the recursive strategy.标明分拆形式。此种分拆会形成带组成部分历史的递交链:
从图中能看出,此种merge形式实际在发起分拆的组成部分生成了两个带有Merge 标识的新递交。如果分拆时存在冲突,解决冲突后的最终内容也会包含在这个捷伊递交中。
看到这里,可能有人会有疑问,组织工作空间中自始至终只出现了两个组成部分,为什么会是三路分拆。从git 源码中能找到merge执行的入口,它有这样的方法签名:
能看出,除了含义明显的ours和theirs,还有两个待分拆的文档叫做ancestor。根据文档和源码注释,这个版实际是两个待分拆组成部分的公共部分。在我们的例子中就是建立新组成部分的那个递交第一类。
大体的业务流程是这样的,git merge会找出两个组成部分对准的新一代commit,找到他们最近的公共祖先,然后对每个待分拆的文档调用ll_merge,这个方法会比较各组成部分和祖先节点的差别。然后把这些差别整合成两个Merge递交,应用到当前组成部分上,生成最终的分拆结果。
如果两个组成部分之间有多个公共祖先,git会选出最合适的祖先节点依照同样规则进行递归分拆。能采用git merge-base —all指示列出所有的备选祖先节点。
Git还能一次性分拆多个组成部分,只需要简单的把组成部分名当做merge的参数依次列出:
此种思路被称为octopus,其中核心逻辑与three-way merge相同,不再详述,能通过阅读github上的源码和文档继续深入细致了解。
three-way merge机制有一定的隐患。如果其中两个待分拆组成部分,比如说ours,和ancestor版的某一部分标识符相同,但另两个待分拆组成部分theirs中有相同的修改,分拆的结果就会采用theirs组成部分相同的那部分,并不会依照修改的时间顺序来决定最终内容。在实际项目中可能会反复修改同一段标识符来响应需求变更,就有几率发生此种分拆结果与预计不符的情况,需要特别留意。
rebase
Git rebase,一般来说被称作变基或衍合, 能理解为另外一种分拆的形式,与merge 会留存组成部分结构和原始递交记录相同,rebase 是在公共祖先的基础上,把捷伊递交链截取下来,在目标组成部分上进行重放,逐个应用选中的递交来顺利完成分拆。
为了形象理解rebase的过程,能看下面例子:
采用 merge 分拆后:
下面采用rebase形式达到同样效果:
除了原本的多组成部分记录变为了直线递交链,还能注意到,其中原本在feature组成部分上的递交,rebase后的SHA编码发生了变化。rebase消除了真实历史,重新生成了捷伊递交。
和merge类似,rebase在遇到冲突时也会暂停,需要手动复原后才能继续。但是rebase的处理要相对繁琐一些,merge 如果发生 conflict,只需要在最终的Merge 递交上解决一次。而 rebase 的 conflict 可能发生在每一次递交的重新应用上,所以需要依次解决。
为了避免此种情况,能在与另一组成部分分拆之前,提前把所有需要递交分拆为两个递交。同样需要用到rebase指示。
执行这样两个指示来分拆当前最捷伊3个递交:
这条指示将打开两个编辑页面,我们能修改前面的指示来分拆或丢弃单个递交。
pick 表示将会应用这个递交。
squash 表示把当前递交分拆到前两个递交,它的前面必须至少有两个被pick的递交存在。
把某条递交注释或删除表示丢弃这条记录。
这里选择分拆第两个和第三个,丢弃第二个递交。
留存退出后进入捷伊编辑页面,提示编辑递交信息,这里选择不做改动。
再次留存退出后成功分拆顺利完成,形成这样的log:
git还有两个可爱的指示cherry-pick,一般来说译作拣选。它的参数是递交第一类的SHA编码,能视为针对单个递交的rebase操作。示例如下表所示:
总结
merge 和 rebase 的差别在于最终的历史记录,能发现 merge 保持了所有组成部分的原始修改记录,可能会包含很多不必要的信息;而 rebase相当于对历史记录做出修剪,能维持一条简单清晰的递交路线。
一般来说我们会在如前所述两个过时的版进行了本地修改的情况下采用rebase,在实际开发中经常会出现此种情况,当你在本地组成部分上组织工作了几天,突然想起应该push到远程库房时,远程组成部分已经被别人更新过了。此时你会得到两个reject信息。
有些人会选择用pull指示分拆远程和本地的同名组成部分,但pull实际执行了fetch和merge两个操作,会生成复杂的组成部分历史和两个多余的merge递交。你也能选择用fetch和rebase代替pull,始终生成两个美观的递交链。
rebase的另两个重要应用是分拆过多的本地递交。因为防止修改内容丢失,经常commit到本地仓库是两个很好的开发习惯。但是当需要递交到公共组成部分时,大量无明确意义的递交信息对历史记录造成不必要的干扰。此时你能用rebase指示把本地记录规范化化,再进行推送。
采用rebase的时候需要遵循一条重要原则:不要对在你的本地库房外有副本的递交记录进行变基。rebase的实质是丢弃一些现有的递交,然后相应地新建一些内容一样但实际上相同的递交。 如果其他人已经在这些递交上做出过大量修改、冲突分拆等组织工作,那么你的rebase将成为他们的恶梦。
对于采用rebase还是merge来分拆标识符,实际并没有什么固定的模式,取决于开发人员如何看待库房的历史记录。一些人认为历史记录应该反映全部真实变更细节,而另一些人认为历史记录应该是精心维护的变更目录。具体如何采用取决于项目合译者的一致共识。不论是merge还是rebase,都应该了解其中原理,避免危险操作,才能享受到Git诸多优点带来的便利。