主要内容包括以下几个方面:
分支在版本控制系统中代表一系列的并行开发工作。接下来我们将会看到,它在隔绝和分离不同工作方面是非常有用的。例如分支可以用来阻止用户当前开发某个特性的工作对bug修复方面的影响。
单个Git版本库可以拥有任意数量的分支。不过在类似Git这样的分布式版本控制系统中,可能会在单个项目下存在多个版本库(forks),其中有些是公开的,有些是私有的,每个版本库都会有自己的本地分支。在分支层面了解版本库之间的团队协作之前,我们需要知道可能遇到的本地和远程版本库的分支类型。接下来的内容将会介绍如何使用这些分支,以及人们在单个版本库中使用多个分支的原因。
历史回顾:分支管理的演变
早期的分布式版本控制系统在每个版本库模型中只允许使用一个分支。Bazaar(后来的Bazaar-NG)和Mercurial官方文档在其诞生初期就向用户建议在克隆版本库时新建一个分支。
另外一方面,Git软件从发布之初就支持在单个版本库中使用多个分支。不过在刚发布时,它的架构是一个中心式多分支版本库和其他单分支版本库进行交互(例如,帮助手册中gitrepository-layout(7)介绍的普通目录.git/branches指定URL和拉取分支),不过Git的出现是对以往传统软件的颠覆。
因为在Git中创建分支非常简单(合并操作也很容易),使用它协作开发也非常灵活,因此其逐渐受到开发人员的青睐,即使是独立开发工作也是如此。这导致了非常实用的主题分支工作流的普及。
将一系列的某类开发工作隔离的原因很多,这也是导致了多种分支的出现。不同类型的分支的用途也各不相同。某些分支是长期的,甚至是永久的,同时某些分支是短期的并且在它们失效之后还可能会被删除。某些分支专门用于发布软件,某些分支则不是。
长期或者永久性分支是为了保证持续性而引入的(永久性从某种意义上来说至少是很长的一段时间)。
从协作开发的角度来看,长期分支可以作为用户下一次更新数据或者发布变更的基础。这意味着用户可以放心地从远程版本库长期分支的任意位置(fork)开始他们自己的工作,并且可以确保将自己的工作成果方便地整合。
当然,用户在公共版本库中可以找到的只有长期分支。大部分情况下,这些分支将永远不会被重置(新版本总是由旧的版本衍生而来)。不过也有一些特殊情况,新的预览版程序发布之后,某些分支会被重新构造(此时需要执行强制拉取操作),某些分支可能是快进式的。上述特例都会在开发者文档中明确提及,防止用户碰到不必要的麻烦。
将正在开发的一系列工作(其中可能包括临时的不稳定代码)和维护工作(其中的内容只包括bug修复)隔离是分支的用途之一。通常这类分支或多或少都存在一些。这种分支的主要目的是将和稳定性有关的开发工作整合起来,其涵盖范围包括项目维护、稳定版程序、测试版程序、项目开发等多个环节。
这些分支来自稳定性依次递减的层级工作成果集合,如下图所示。需要注意的是,在实际开发工作中,渐进稳定性分支的存在形式不会像图片展示的那样简单。在拉取节点之后的分支上也可能出现新的修订记录。不过即使发生过合并操作,大体的框架仍是一致的。
线性视图和“筒仓”视角下的渐进稳定性分支。
在线性视图中,越稳定的修订版本在提交历史中记录历史越久远,不太稳定的修订记录在提交历史中离当前时间更近。或者,我们可以将分支当作工作流水线研发工作的进展依赖变更记录的稳定程度(节点)一般的规则通常是将更稳定的分支合并到较不稳定的分支中,即向上合并,这使得分支可以保持整体的筒仓造型(参考下图)。这是因为合并操作通常意味着将被合并分支的所有变更都添加到目标分支中。因此将一个不太稳定的分支合并到一个更稳定的分支中会对稳定的分支产生不良影响,有悖于稳定分支建立的初衷。
一般来说,我们可以从节点分支上看到以下不同层次的稳定性:
保留多个长期分支并不是必需的,不过对于大型或者复杂的项目来说经常会很有帮助。通常在操作层面,每个级别的稳定性是和其自身平台、开发环境和给定分支的平台一一对应的。
为发布项目新版本而做的准备工作是漫长而复杂的。每一版本分支可以为用户提供不少帮助。这类版本分支可以将正在进行的研发工作与将要发布的新版本程序隔离。这样一来,其他开发人员可以继续进行新特性的研发和集成测试,同时质监部门在版本管理人员的帮助下可以对候选版本进行稳定性测试。
在创建一个新的软件版本之后,保留这类每一版本分支可以让我们方便地维护软件以往的候选版本。在此期间,这类分支可以用来当作存放修复bug提交的地方,以及创建候选版本软件的镜像。
并不是每个项目都可以找到每一版本分支的用武之地。用户可以在稳定节点分支上为一个新的候选版本做准备,或者使用一个独立的版本库代替使用独立分支。当然,并不是所有项目都需要提供除最新版本之外的多版本支持。
这类分支的命名通常是和新发布的版本有关的,例如release-v1.4或者v1.4.x(命名时最好不要和候选版名称对应的标签重名)。
Hotfix分支和候选版分支类似,不过其是计划外的候选版本。它们的用途是处理某个上线产品或者广泛部署的版本出现意外的情况,通常是为了解决上线产品中某些关键的问题(一般是一个比较严重的安全漏洞)。这类分支可以被看作一个长期的bug修复主题分支。
如前所述,开发者的项目客户因为有一些特别的需求,需要项目支持某些定制特性;又或者用户在部署网站时需要一些特殊的条件。假如这些需求无法通过简单地修改配置文件来满足,那么开发者最好为这些客户定制需求创建一个独立开发分支。
但是开发者又不希望将上述开发工作和其他工作一直保持隔离,而是希望在适当的时机将它们和主分支合并。一种方案是为每一定制特性集合、每一客户或者每一开发创建一个分支;另外一种方案是使用独立的版本库。上述两种方案都支持并行开发,并且迁移变更也很方便。
假定你正在研发一个Web项目,希望使用版本控制系统实现网站部署的自动化。一种解决方案是建立一个监控程序监控特定分支的变化(例如名为“deploy”的分支)。该分支被更新的同时自动更新和重新加载网站应用。
当然,不只有这一种解决方案。另外一种解决方案是使用独立的部署版本库,并建立对应的钩子,当执行推送操作时同时触发更新网站应用的操作。或者用户可以在公共版本库上配置一个钩子,以便在推送某个特定分支时触发重新部署网站应用的指令。
这类技术除了可以用于部署应用之外,还可以用于持续集成(Continuous Integration,CI),推送更新到某个特定分支后会触发运行测试用例的操作(触发器可以通过在该分支上创建一个新提交或者从其他分支合并而来)。
远程版本库中有一种分支可以对推送操作进行特殊处理,用到这种技术的地方很多,其中就包括辅助协作开发。它可以用来为某个项目启用匿名推送访问控制。
目前为止,向大家介绍的所有分支在用途和管理方式上都各不相同。从技术层面上看(从提交视图上),它们都是一样的。不过孤儿分支是个特例。
孤儿分支是并行无关联的一系列开发工作,即它们和项目主线历史修订没有共享的修订。在DAG视图中它们是用无关联的子图来表示的,并且和主DAG图形没有任何交互关系。大部分情况下,它们签出的文件也是不相关的。
这类分支也是一种在单个版本库中存放不相关内容的技巧,它可以看作版本库隔离的替代性方案(在使用独立的版本库存放各自独立的内容时,用户也许会使用某些命名约定来修饰版本库名称,例如相同的前缀)。
在长期分支是为了持续性而存在的同时,创建短期或者临时分支的目的通常是为了解决单个问题,并且相关问题解决之后,它们通常会被删除。它们存在周期的长短是和解决相关问题所花费的时间密切相关的。它们的目标是有时间限制的。
因为它们的临时性,它们常见于开发者或者集成管理者(维护人员)的本地私人版本库,并且不会被推送到分布式的公共版本上。如果它们出现在了公共版本库上,那么它们只可能出现在独立开发者的公共版本库中,被当作某个pull请求的目标。
分支常用来隔离和集成开发工作的不同子集。因为分支和合并都非常简单,我们甚至可以像前文所述那样,进一步根据稳定性级别创建对应的分支。我们还可以为每个独立的问题创建独立分支。
主题分支目标是为每个主题创建一个新分支,即一个新特性或者一个bug修复。这种类型的分支的目的既包括后续开发工作功能特性的整合(每一个提交应该是自包含结构,方便日后代码审核),也要包括将开发某个特性的工作和其他主题隔离开来。采用主题分支后,特性相关的变更可以保持一定的独立性,避免和其他提交混杂在一起;同时也方便用户将整个分支当作一个单元统一移除(或者回退)、审核和集成变更。
主题分支上的提交记录最终目标是涵盖产品的某个预览版本。这意味着短期分支最终会被整合到长期分支中,即渐进稳定性工作成果收集,之后被删除。为了更方便地集成主题分支,推荐的做法是通过最原始、最稳定的版本创建这类分支,并最终将该主题分支与之合并。通常这意味着从一个渐进稳定性分支创建一个新的主题分支。不过如果给定特性依赖的主题不在稳定分支中的话,用户还需要添加相应的依赖引用到主题分支中。
注意,如果事后证明用户提取了错误的分支,还可以通过变基操作解决,因为主题分支并不是公开的。
大家很容易区分出用于bug修复的这类特殊的主题分支。这种分支应该是从它适用的最原始的集成分支(包含bug的最稳定分支)创建的。这通常代表用户提取的内容是维护分支或者剔除了所有集成分支,这一点是和稳定分支的外部引用不同的。bug修复分支的目标是被整合到相对长期性的集成分支中。
bug修复分支可以被当作一个短期的长期hotfix分支等价物。
使用它们的效果远好于简单地在维护分支(或者相应的集成分支)上提交修复提交。
大家可以将HEAD分离的状态当作临时分支的极端情况,以至于这种分支甚至都没有名字。在少数情况下,Git会自动使用这种匿名分支,例如在执行二分查找和变基操作时。
因为在Git中只允许出现一条匿名分支,并且该分支还必须是用户当前使用的分支。因此,比较推荐的做法是创建一个包含临时名称的临时分支,用户在后续开发过程中还可以修改上述临时分支的名称。
脱离HEAD的应用场景之一是某些概念的验证性工作。当然,如果事后证明这些验证性工作是有价值的,用户还需要给这个分支设置适当的名称(或者用户还需要进行分支切换)。从一个匿名分支向一个具名分支的切换非常简单。用户只需要根据当前脱离HEAD的状态创建一个新分支即可。
现在我们已经了解了分支的种类和用途,接下来介绍一些分支的用法。需要注意的是,用户应该因地制宜地使用不同分支。例如,小型项目最好使用更简单的分支工作流,而较大型的项目则可能需要使用更高级的工作流。
本小节将会向大家介绍多种标准工作流的使用。每种工作流可以通过采用的不同分支类型进行识别。我们除了可以了解给定分支中正在进行开发的工作组织形式之外,还可以知道在将要发布软件新版本时需要完成哪些工作(分清工作内容的主次)。另外,本小节还会介绍选定工作流之后分支内部的演化过程。
最简单的工作流之一是只使用一个独立的集成分支。 这类分支有时也叫主干,在Git系统中,它们通常就是master分支(创建版本库时的默认分支)。在这种工作流以纯粹的版本记录来看,至少在平常的开发工作中,用户应该将所有提交添加到上述分支上。这种工作方式是由使用中心式版本控制决定的,它适合处理分支和合并操作花费代价太大,但是用户希望避免处理过多分支的情况。
这种工作流更高级的形式中,用户还可以使用主题分支和每个特性的短期分支,并在后续工作中将上述分支合并到主干分支中,从而替代直接在主干分支添加提交的做法。
在这种工作流中,当软件新的发行版本确定后,我们可以在主干分支之外创建一个新的预览版分支。这么做的目的是避免给预览版稳定性和正在开发的工作造成不良影响。原则是所有和稳定性相关的工作继续在预览版分支上推进,同时所有研发工作放在主干分支上进行,将要发布发行预览版程序从预览版分支上提取(添加标签),并将之作为新版本程序的最终版本。
对于给定版本的预览版分支,用户后续还可以用它来收集bug修复方案和提取次要版本。
这类简单工作的不足之处在于,在开发过程中,用户经常会碰到软件不稳定的状态。这种情况下,选取一个足够稳定的节点发布新版本将会变得很困难。一种替代性的解决方案是在预览版分支上创建一个恢复性提交,将工作内容恢复到发布前的某个状态。但是这样一来工作量会非常大,并且项目历史记录也会变得难以回溯。
这种工作流的另外一个不足之处就是,某些特性乍一看会让人眼前一亮,但是后续使用过程中会带来不少麻烦。这也是采用这种工作流后需要处理的问题。如果在开发过程的后期发现为某些特性创建多个特性提交记录并不是一个好主意,恢复它们到以前的状态将会非常困难,尤其是当它们的提交遍布项目历史的时间线时。
此外,主干和预览版分支工作流也没有为查找不同特性之间的不良交互提供固有机制,即集成测试。
尽管存在这些问题,这种简单的工作流对于小型团队来说仍是非常合适的。
为了能够提供一系列性能稳定的软件产品和在实践中对其进行测试,并将其当作一种浮动性的测试版本,用户需要将程序稳定性的工作与处于测试阶段的研发工作和不稳定代码隔离。这就是节点分支的用途所在——整合软件不同程度的稳定性和成熟的部分(这种分支有时也称集成分支或者渐进稳定性分支)。如下图所示,我们可以看到渐进稳定性分支和线性历史简易演示代码的图形和筒仓视图。接下来将要介绍采用这种分支的节点分支工作流。
除了将稳定的和不稳定的开发工作保持隔离之外,有时还需要一个持续进行的维护性分支。如果产品只有一个版本需要维护,并且创建新的预览版过程足够简单,用户还可以像上文所述那样采用节点型分支。
这里说的足够简单的意思是指用户可以在稳定分支之外只创建下一个主预览版分支。
在这种情况下,用户至少会拥有3个集成分支:
在采用这种工作流时可以只使用集成分支。如果有必要的话,可以在维护分支上提交bug修复提交记录,然后将它们合并到相应的稳定性分支和开发分支中。用户可以根据通过完备测试的工作成果在稳定性分支上创建提交,然后根据需要再将它们合并到开发分支上(例如新的开发工作依赖上述开发工作的话)。可以将处于测试阶段的开发工作放在开发分支上。在日常开发工作中,用户最好永远不要将不稳定的工作成果合并到更稳定的分支上,否则可能会影响相关分支的稳定性。比较推荐的做法永远是将较稳定的分支合并到较不稳定的那一个上,如上图所示。
当然,这也需要用户事先拥有对正在研发的功能特性是否稳定的研判能力。当然还存在这样的潜在假设,那就是项目之初各功能特性之间能够完全兼容。用户对实际的预期是美好的,不过每个开发特性从理想的萌芽到结出现实之果的过程,在上述特性臻于完美之前,都需要经历严格试验和激烈的讨论。这个问题可以通过引入下文将要介绍的主题分支来解决。
在纯节点分支工作流中,用户可以在维护分支之外创建次版本分支(包含bug修复提交),可以在稳定版本分支之外创建主版本分支(包含新功能特性)。在一个主版本发布之后,稳定性工作分支被合并到维护分支后就可以为刚创建的新版本分支提供支持了。从这一点来看,一个不稳定(开发)分支可以被合并到一个稳定分支中。这也是在上游合并操作中唯一出现的不稳定版本被合并到更稳定版本的情况。
如下图所示,主题分支工作流背后的理念是为每个主题创建一个独立的短期分支,以便所有提交都从属于特定分支(相关的开发工作都通过分支进行分类)。每个分支的目的明确,即开发某个新特性或者创建一个bug修复提交。
包含一个集成分支(master)和3个主题或者特性分支的主题分支工作流。其中的主题分支包括:一个已经合并到集成分支主题分支(名字是newidea),另一个(iss-95-2)是依赖其他特性分支(iss-95)的特性而存在的特性分支。
在主题分支工作流(也称特性分支工作流)中,用户至少会碰到两种不同类型的分支。首先,其中必须包含一个永久性(或者长期)的集成分支。这种类型的分支是纯粹用来进行合并操作的。集成分支是公开的。
其次,其中还包含相互独立的临时特性分支,每个临时分支的目的是开发某个特性或者对某个bug进行修复。它们用于执行包含某个特性或者bug修复相关的所有步骤,或者是一个开发人员负责的某类工作。当上述特性或者bug修复工作完成并被合并到集成分支后,这些临时分支就可以被删除了。主题分支通常是不公开的,也不会出现在公共版本库上。
当某个特性准备进行审核时,它的主题分支通常会执行变基操作方便集成,同时也使得提交历史记录结构更清晰,然后它们会被当作一个整体一起审核。主题分支可以被添加到pull请求中,或者作为一系列的补丁被发送(例如使用git format-patch和git send-email命令)。为了方便查看和管理,它们通常会被当作独立的主题分支存放在维护者的工作版本库中(例如它们被当作补丁发送时,可以使用git am --3way命令)。
然后,集成经理(“推举”版本库工作流中的维护人员,或者中心式版本库工作流中的其他开发人员)会审核每个主题分支并且决定这些分支是否符合集成到特定集成分支的条件。如果符合条件,那么它们将会被合并(也许还需要使用–no-off选项)。
分支工作流最简单的变体是只使用集成分支,不过通常用户会采用包含主题分支的节点分支工作流这种组合。
在这种常用的变体中,除非分支需要依赖其他特性,特性分支一般是根据给定的稳定分支的外部引用或者最新的主版本程序构建的。在后一种情况下,分支需要从与之相关的依赖项的基础进行构建,例如下图中的feat分支。bug修复分支是根据维护性分支构建的。
包含两个节点分支的主题分支工作流。在主题分支中,一个分支是被认为足够稳定,可以被合并到next(不稳定)和master(稳定)两个节点分支上;另一个分支(idea)已经合并到next分支,主要用于测试,以及刚从master分支创建的新分支(feat)。
当主题完成后,它们首先会被合并到开发集成分支(如next分支)上进行测试。如上图所示,主题分支idea和iss92都被合并到next分支了,但同时feat分支还处于研发状态。比较激进的用户可以使用给定的不稳定分支抢先尝试新功能了,当然,同时他们还需要承担程序崩溃和数据丢失的风险。
经过检验后,当上述特性被认为可以添加到下一个软件版本时,它们将会被合并到稳定性工作的集成分支(例如master)上。上图就包含这一个分支——iss92。在这一点上,在将它们合并到稳定性集成分支上之后,该主题分支就可以被删除了。
使用一个特性分支可以让和该主题相关的修订版本聚集到一处,不用和其他提交记录混淆。主题分支工作流还可以方便地将主题作为一个整体还原,以及同时移除一系列有问题的提交记录(将有问题的提交记录作为一个整体一并移除),用户就不必进行逐个还原操作了。
如果该特性分支事后证明并不成熟,因为它并没有简单地被合并到稳定分支上,只存在于开发分支上。当然,如果我们发现问题为时已晚的话,特性分支已经合并到稳定分支上了,那么还可以回退该合并操作。这是一个比回退单个提交更高级一点的操作,不过它比逐个回退单个提交给人带来的痛苦要少很多,因为用户需要确保每个被回退的提交都执行了正确的操作。
主题分支工作流中包含bug修复分支也是很常见的。唯一的差异是用户需要考虑将bug修复分支合并到哪一个集成分支中。当然,这种事情需要用户随机应变。也许bug修复分支只对维护分支有影响,因为它们可能是对稳定分支和开发分支中某个特性偶尔出现的问题相关的bug修复,那么它就只需要合并到该分支即可。当然,当问题只出在稳定性分支和开发分支上时,因为这个特性并没有出现在上一个版本中,那么维护分支就被排除在需要合并该特性的目标分支之外了。
使用独立的主题分支处理bug修复问题来替代直接在bug修复分支上创建提交还有另外一个好处。它允许用户轻松地纠正执行步骤中产生的错误,如果事实证明修复的分支超出预期的话。
举个例子,如果发现bug修复不只是对当前的工作有影响,还对维护性的版本有影响,通过主题分支,用户只需要将该修复分支与其他相关的分支合并即可。如果我们直接在稳定性分支上提交这个修复性提交的话则不属于这种情况。对于后一种情况,用户是无法使用合并的,因为这会影响维护分支的稳定性。用户需要拷贝包含修复记录的修订,通过拣选提交(cherry-picking)将它从被提交的源分支上移动到维护性分支上。不过这意味着重复提交,额外的拣选提交记录有时可能会对合并行为产生不良影响。
主题分支工作流还支持用户检测特性之间的兼容性,如果有必要的话后可以修复这些冲突。可以简单地创建一个一次性的集成分支,然后将包含若干特性的主题分支集成进去,来测试它们之间的兼容性。用户甚至可以专门发布用于集成测试的这类分支(即建议式更新或者简称pu),使得其他开发人员可以在开发过程中随时对它们进行校验。不过用户最好在开发文档说明中明确声明上述分支不能够作为基准进一步深入开发,因为它们每次重新创建都是来自一些不成熟的草稿。
假定我们现在正在使用3个节点性(集成)分支:用于维护最新版程序的maint分支,用于稳定性工作的master分支,用于功能开发的next分支。
维护者(版本项目经理)发布下一个新版本之前首先需要做的事情是确认master分支是否可以替代maint分支,即下一个版本中出现的bug是否都全部被修复了。用户可以通过如下命令的空白输出结果来确认这一点:
$ git log master..maint
如果上述命令的输出结果显示还有不少未合并的提交,那么维护者需要决定如何处理它们。如果这些bug修复没有对程序中其他部分有任何不良影响,那么维护者就可以简单地将maint分支合并到master分支上了(因为这是将更稳定的分支合并到较不稳定的分支)。
$ git tag -s -m "Foo version 1.4" v1.4 master
$ git push origin v1.4 master
上述命令假定项目Foo的公共版本库中的默认项是origin,我们还使用了两位数表示主版本号(该命名规则遵循语义版本规范:http://semver.org/)。
如果维护者希望软件兼容以往的历史版本,就需要拷贝一个旧的维护性分支,因为下一个步骤是为了维护刚发布的新版程序做准备的:
$ git branch maint-1.3.x maint
然后维护者将maint分支更新到新版本,扩展该分支(需要注意的是,maint分支是master分支的一个子集):
$ git checkout maint
$ git merge --ff-only master
如果上述第二行命令无法执行,这意味着maint分支上的某些提交并没有出现在master分支上;或者更精确地说,master分支并不是严格承袭自maint分支的。
因为通常我们会将特性逐个添加到master分支上,有可能部分主题分支被合并到next分支上,但是它们在合并到master分支之前就被丢弃了(或者它们还不成熟,所以还没来得及被合并)。这意味着虽然next分支是包含master分支的所有主题分支的替代品,但是master分支不一定必须是next分支的祖先。
这也是发布新版本之后扩展next分支要比扩展maint分支更复杂的原因。一种解决方案是回退并重新构造next分支:
$ git checkout next
$ git reset --hard master
$ git merge ai/topic_in_next_only_1...
用户可以使用如下代码根据未合并的主题分支对next分支进行重新构建:
$ git branch --no-merged next
在重新构建next分支之后,其他开发人员必须强制更新next分支;如果没有配置强制更新的话,那么系统将不会执行快进式操作:
$ git pull
From git://git.example.com/pub/scm/project
62b553c..c2e8e4b maint -> origin/maint
a9583af..c5b9256 master -> origin/master
+ 990ffec...cc831f2 next -> origin/next (forced update)
这里需要注意的是强制更新next分支的操作。
你会发现更高级的主题分支工作流是基于节点分支构建的。在某些情况下,引入更多分支模型也是很有必要的,充分利用多种分支类型:节点分支、发布分支、hotfix分支和主题分支。这类模型有时也被称为Git流。
这类开发模型采用两种长期节点分支,从包含集成了正在开发的最新成果的工作中把准备发布上线的部分隔离。我们一般称这些分支是master分支(稳定性分支)和开发分支(为下一版本手机功能特性),后者还可以用于每日构建。
这两种集成分支的寿命几乎是无限的。在工作流中这些分支还伴随了一些辅助分支,例如特性分支、发布分支和hotfix分支。
每个新特性都是在主题分支上开发完成的(有时这些分支也称特性分支),其命名通常会伴随相关特性。这些分支的提取源既可以是开发分支也可以是master分支,具体的细节需要根据工作流的特性和功能特性的需要而定。当某个功能特性开发完成时,就可以使用–no-ff选项将它的主题分支合并(因此总是存在一个合并提交方便描述该特性),集成到开发分支后进行集成测试。当它们通过测试能够添加到下一版本时,将会被合并到master分支。一个主题分支的存续周期取决于其开发周期的长短,当被合并后(或者被丢弃时)就会被删除。
发布分支的用途由两部分组成。在创建时,其目标是为发布新版本做准备。其中包括文件整理,应用次要bug修复,准备版本的元数据(例如版本号、程序发布名称等)。最后需要做的是使用主题分支,准备元数据可以直接在发布分支上完成。使用发布分支使得用户可以将正在开发的特性和将要发布的主版本隔离开来,保证程序的质量。
这类分支创建的时机是稳定分支成熟或者接近成熟,即新版本待发布的状态。这种分支的每个命名都和发布的程序有关,例如release-1.4或者release-v1.4.x。用户通常也会在新版本(如v1.4)发布之前,根据该分支创建一些预览版程序(如v1.4-rc1)。
该发布分支会一直存续到下一新版本发布为止,或者用于处理后期维护工作——修复给定分支的bug(当然,通常维护工作仅限于几个最新的或者流行的版本)。对于后一种情况,它会替换其他工作流中的maint分支。
hotfix分支和发布分支类似,不过它是专门用于修复预览版程序中意外出现的安全bug的。它们的命名类似hotfix-1.4.1。如果对应的发布分支(维护分支)已经不存在,那么一个hotfix分支创建通常和该版本对应的标签有关。这类分支的目的主要是为了解决一些已经发布的产品中意外出现的关键bug。当在这类分支上添加了一个修复提交后,相关的次级版本将会被移除(为每个这类分支)。
现在假定检查属于另外一种情况。如何使用分支管理bug修复,例如一个安全问题。这里需要用到的技术和普通开发稍有不同。
如前文主题分支工作流所述,虽然可以在受到bug影响的最稳定集成分支上直接创建一个bug修复提交,不过更好的做法是针对存在的问题创建一个独立的主题分支来解决该问题。
用户可以从最原始(最稳定)的集成分支提取受到bug影响的内容创建一个bug修复分支,也许所有分支都会受到该分支点的影响。用户将bug修复提交(也许会包含一组提交记录)添加到刚才创建的分支上。通过所有测试后,用户可以简单地将该bug修复分支合并到受到影响的集成分支上。
这种模型还可以用于解决开发初期分支之间的冲突(依赖)。假如用户目前正在开发一些新特性(在开发分支上),不过这些特性还不成熟。行文至此,用户应该注意到在开发版本中遇到bug时如何修复它们。你希望在bug修复的最终状态下开展工作,但是意识到团队其他开发人员也希望将这个bug修复后再继续工作。那么将bug修复提交记录提交到特性分支上是一个不错的方案。直接在集成分支上添加bug修复提交存在一个风险,那就是在开发过程中忘记将该bug修复提交合并到特性分支。
解决方案是在独立的主题分支上创建一个bug修复提交,然后将它们合并到正在开发的主题分支和集成测试分支上(可能还会有节点分支)。
用户可以使用类似的技术创建和管理用户定制特性的子集。用户只需为每个这类特性创建一个独立的主题分支,然后分别将它们合并到每个客户的自定义分支上。
如果存在和安全有关的因素,事情可能会稍微复杂一些。在解决一个严重的安全漏洞时,用户不仅希望修复当前的版本,可能还会考虑修复最流行的版本。为此,用户需要为多个维护路径创建一个hotfix分支(从特定版本提取):
$ git checkout -b hotfix-1.9.x v1.9.4
然后,需要将包含修复该问题的bug修复提交合并到刚才创建的hotfix分支中,最终创建bug修复的版本:
$ git merge CVE-2014-1234
$ git tag -s -m "Project 1.9.5" v1.9.5
如前文所述,在单个版本库中使用多个分支是非常有用的。简易的分支和合并操作使得用户能够构建强力的开发模型,可以充分利用诸如主题分支这类高级分支技术。这意味着远程版本库中也包含多个分支。因此我们的脚步不能只停留于版本库之间的交互。
我们还必须学习在远程版本库中多个分支之间的交互。同时还要考虑本地版本库中有多少分支是和远程版本库中的分支有关联的(或者其他引用)。另外,非常重要的一点是本地版本库中的标签和其他版本库中的标签之间的关系。
在软件开发过程中,上游一般代表某个项目的主要开发或者维护人员。我们可以将离“推举”版本库——即软件的官方源码库更近(版本库之间的层级)的版本库称为我们的上游。如果一个变更(一个补丁或者提交)被上游接受了,那么它同时也会被添加到软件的新版本中,所有处于下游的人们也会接收到它。
同样,如果本地分支中发生的变更能够完全合并和添加到远程版本库分支上的话,我们可以说远程版本库中给定分支是给定本地版本库中某个分支的上游。
上述远程版本库中的上游版本库和上游分支对应的给定分支,已经分别通过branch..Remote和branch..merge配置变量定义好了。上游分支可以通过@{upstream}或者快捷方式@{u}来表示。上游是在远程跟踪分支之外创建分支时设定的,用户可以使用git branch --set-upstream-to或者git push --set-upstream命令对它编辑。
上游不一定需要是远程版本中的分支,它也可以是一个本地分支,一般来说我们称之为跟踪分支多过上游。当然,本地分支是基于另外一个本地分支时,这一特性会非常有用,例如当一个主题分支是提取自其他主题分支而创建的(因为它包含的特性是后续分支继续工作的先决条件)。
在项目协作过程中,用户将会和很多版本库打交道(“Git流—— 一种成功的Git分支模型”)。用户打交道的每一个这种远程(公共)版本库内部都有自己的表示分支结构的方式。例如,远程版本库origin中的master分支的位置不一定必须和用户的版本库克隆中本地master分支的位置保持一致。换句话说,它们不需要在DAG视图中指向同一提交对象。
为了能够检查集成状态,例如远程版本库origin中还有哪些变更是用户本地还没有的,或者本地工作版本库中还有哪些新增变更记录是未发布的,用户需要知道本地分支在远程版本库中对应分支的位置。这也是远程跟踪分支的主要用途——远程版本库中对应分支的引用记录(如下图所示):
远程跟踪分支。在远程版本库origin中的master分支对应的远程跟踪分支是origin/master(全名是refs/remotes/origin/master)。fetch命令中灰色的文本代表默认参数。
为了跟踪记录远程版本的内部变化,远程跟踪分支是自动更新的,这意味着用户不能基于上述分支创建新的本地提交(因为在更新过程中用户会丢失这些提交)。用户需要为此创建一个本地分支。可以使用命令git checkout <>达到此目的,当然还需要假定用户在上述命令中指定的分支名并不存在。这个命令会在远程版本库分支<分支名>之外创建一个新的本地分支,然后为它设定相关的上游信息。
本地分支所在的命名空间是refs/heads/,同时给定远程版本库的远程跟踪分支所在的命名空间是refs/remotes/<远程版本库名称>/。不过它们都只是默认配置。拉取(推送)操作对应的配置变量remote.中记录了远程版本库中的分支(引用)和本地版本库中远程跟踪分支(或者其他引用)之间的映射关系。
这种映射被称为refspec,它既可以是显式声明的一对一分支映射,也可以是通配符表示的映射模式。
例如origin版本库中的默认映射配置如下:
[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*
上述内容的意思是说,远程版本库origin中master分支(全名refs/heads/master)中的内容将会被存放到本地版本库克隆的远程跟踪分支origin/master上(全名是refs/remotes/origin/master)。模式开头的符号+是告知Git系统以非快进方式更新远程跟踪分支,即不继承上一版本的信息值。
这种映射可以用于远程版本库拉取操作配置,也可以当作命令行参数(只使用引用的缩写就能完全满足需要了)。如果命令行中没有对应的refspecs映射,那么该配置只会影响当前登录的用户。
可以使用git push 命令将变更发送(发布)到远程版本库上,同时可以使用git fetch命令将远程版本库的变更下载到本地版本库。这两个命令发送变更的方向刚好是相反的。用户需要注意的是,这和用户自己的本地版本库有着一个显著的差异——用户能够在键盘上输入其他命令。
这也是本地到远程版本库没有类似git pull命令获取和集成变更的原因。根本没有人能够解决可能出现的冲突(问题在于集成变更是自动化完成的),特别是分支和标签的拉取与推送之间的差异。
用户经常会想将远程版本库上某个特定分支的变更集成到本地当前分支上。pull命令会下载相关变更(或者执行附加相关参数的git fetch命令),然后自动将上述变更集成到当前分支上。默认情况下,它会调用git merge
命令集成变更,不过用户还可以执行git rebase命令达到后者使用-rebase选项一样的效果,也可以使用git pull命令和配置选项pull.rebase
;或者使用branch.
为独立分支进行变基配置。
需要注意的是,如果远程版本库没有做相关配置(执行pull命令并指定URL),Git将会使用FETCH_HEAD引用存储获得的分支外部引用。还有,git request-pull命令专门为基于pull的工作流创建发布信息或者保留变更,例如“推举”版本库工作流的变体。它会创建一个和GitHub合并请求等效的纯文本,因此非常适合以电子邮件的形式发送。
一般来说,用户推送的目标版本库都是为了同步变更而创建并且是空的,即没有工作区。一个裸版本库中甚至没有当前分支的概念(HEAD)——没有工作树目录,因此也没有签出的分支。
不过有时用户也许会希望推送变更到非裸版本库。这是可能发生的,例如作为一种同步两个版本的方式,或者作为一种部署机制(例如一个Web页面或者Web应用)。默认情况下,Git在服务端(用户推送的目标非裸版本库)将会拒绝当前签出分支的引用更新。这也是它不将HEAD与工作区、暂存区保持同步的原因,如果你没有注意到这一点,就肯定会感到非常迷惑。不过用户可以通过设置receive.denyCurrentBranch变量来警告或者忽略(将其由默认值改为refuse)这类推送。
用户甚至可以通过和上述配置变量类似的updateInstead让Git更新自己的工作目录(必须清理好,保证其中不存在任何未提交的变更记录)。
一个替代性并且更灵活的解决方案是使用git push命令进行部署,在接收端配置相应的钩子。
用户经常从公共版本库中拉取所有公开的分支项目。大部分情况也可能只是为了获取所有分支的完整更新。这也是git clone命令在refspec中设定默认拉取映射的方式,即本远程版本库到本地版本库的映射规范。“拉取所有”规则常见的例外是随后的pull请求。不过在这种情况下,请求中已经显式声明了版本库和分支(或者是签名标签)的状态,可以在执行pull命令时附加相应的参数:git pull
。
另外一方面,在私人的工作版本库中,通常还有不少分支是用户不希望发布的,或者至少是用户不想马上就发布的。大部分情况下,我们只希望发布一个独立的分支,即目前用户研发完毕并通过测试的分支。当然,如果你是集成经理,你会选择发布一组经过精心筛选的分支子集,而不是单个分支。
这是拉取和推送之间另外一个重要的差异,同时也是Git不为推送操作设置默认refspec映射的原因(当然,用户也可以手工编辑该配置),不过会采用推送模式的机制来决定推送哪些内容。当然,这个配置变量只有在命令行中执行git push命令,没有显式声明推送分支状态时才起效。
使用git push命令进行脱机同步:
用户工作时使用机器A和机器B两台机器,其中每台机器都有自己的工作目录,常用的同步方式是在每台机器上互相使用git pull 命令。不过,在特殊情况下,用户可能只能使用单向连接进行访问(例如防火墙或者间接访问的原因)。现在假定用户可以在机器B上执行拉取和推送操作,但是在机器A上只能执行拉取操作。现在用户希望以某种方式将机器B上的变更推送到机器A上,这么做的结果会导致在机器A上执行拉取操作时发生混淆。为此用户需要通过refspec进行映射声明,即用户希望推送本地分支到远程跟踪分支。
machineB$ git push machineA:repo.git \
refs/heads/master:refs/remotes/ machineB/ master
第一个参数是类scp语法的URL地址,第2个参数是refspec映射地址。需要注意的是,用户完全可以通过编辑配置文件达到类似目的,这样做是为了避免重复劳动。
首先我们需要知道Git在访问远程版本库时是如何处理标签和分支的。
拉取分支的操作非常简单。使用默认配置的情况下,git fetch命令会下载变更记录和远程跟踪分支的更新(如果有的话)。后者是根据远程版本库的拉取refspec映射完成的。当然,上述映射规则也有例外情况。这种例外情况发生在生成版本库镜像时。在这种情况下,所有的远程版本库中的引用都存放在了本地版本库的同名目录下。git clone --mirror
命令会为origin远程版本库生成如下配置:
[remote "origin"]
url = https://git.example.com/project
fetch = +refs/*:refs/*
mirror = true
引用名和它指向的对象名一起被获取了,然后被写入到.git/FETCH_HEAD文件中。这种信息已经被占用了,例如被git pull命令;如果拉取操作是通过URL地址而非一个远程版本库名称的话,这样做是必需的。完成上述操作后,当我们通过URL地址拉取变更时,被拉取的分支上没有存储远程跟踪分支信息用于集成。
用户可以使用git branch -r -d
命令逐行删除远程跟踪分支配置;也可以使用git remote prune
命令(在时下流行的Git软件中使用git fetch --prune
命令)逐行删除远程版本库中已经不存在对应分支的远程跟踪分支。
拉取标签的情况稍有不同。我们希望不同开发人员能够在同一分支(例如master这样的集成分支)的不同版本库中进行独立开发,这将会需要所有开发人员都可以通过某个特定标签引用同一修订版本。这也是为什么远程版本库中的分支位置信息是单独存放在远程跟踪分支的命名空间refs/remotes/
下,而标签有对应的镜像,每个标签都是用相同的名字存放在命名空间refs/tags/*之下。
也可以将标签的位置存放在远程版本库中;当然还可以通过相应的拉取refspec进行映射配置,Git系统在这方面是非常灵活的。例如,可能有必要为一个子项目设置拉取配置,或者可以将标签信息存放在一个独立的命名空间中。
这也是为什么默认情况下,在下载变更记录时,Git还会拉取和本地化存储所有标签,并指向已下载的对象。用户可以使用–no-tags选项禁用自动标签关联。该选项还可以在命令行中作为参数使用,也可以通过remote.
配置变量进行设置。
用户还可以通过–tags选项让 Git 下载所有标签,或者为标签添加合适的拉取refspec值:
fetch = +refs/tags/*:refs/tags/*
推送和拉取不同。推送分支通常会受到推送模式的限制。用户将一个本地分支(有可能只是单独的一个当前分支)推送到远程版本上的某个特定分支以便更新该分支上的内容,例如从本地的分支refs/heads/到远程版本中的分支refs/heads/。通常它们的分支名都是一样的,不过也可以通过稍后介绍的上游具体配置来设定不同的名称。用户不需要声明完整的refspec映射名称:使用引用名称(如分支名)意味着将会推送到远程版本库上的同名引用上。如果它不存在,则会自动创建该引用。推送到HEAD意味着推送当前分支到远程版本库上的同名分支(如果没有推送到远程版本库的HEAD-,通常说明该引用不存在)。
通常用户推送标签需要在git push
命令中显式声明标签名(如果偶然出现标签和分支重名的情况,即都使用了+refs/tags/ :refs/tags/作为refspec映射,那么需要使用tag 加以区分)。可以使用–tags选项推送所有标签(配合使用相应的rfespec映射),使用–follow-tags选项启用自动标签关联功能(因为默认情况下,拉取操作是没有启用该功能的)。作为refspec映射的一种特殊情况,推送一个“空源”到远程版本库中的相关引用的含义是删除它们。–delete选项是git push命令使用这种类型的refspec映射的快捷方式。例如为了删除远程版本库中的名为experimental的引用,可以使用如下代码:
$ git push origin :experimental
注意,可以在远程版本库服务器上通过receive.denyDeletes变量或钩子禁止删除引用。
git push命令的行为在没有指定推送目标参数和没有配置推送refspec映射的情况下,是由推送模式决定的。不同的推送模式适用的协作工作流也不尽相同。
Git 2.0以上版本的软件采用的推送模式也称简单模式。它的设计理念非常简单:阻止推送变更到分支要强于私有变更给公共版本库带来的意外。在这种模式下,用户总会将当前分支的变更推送到远程版本库的同名分支上。如果推送的目标版本库和拉取的源版本库是一样的(中心式工作流),那么用户需要为当前分支设置好对应的上游。上游的名称可以和分支同名。这意味着,在中心式工作流中(拉取和推送的版本库是一样的),上游必须和当前分支(将要推送的)同名,使得上游就像获得了额外的安全保障一样。在三角工作流中,当用户推送到远程版本库中的目标和平时拉取的目标不一样时,它仍旧可以像访问当前分支那样正常运作。这是最稳妥的方案,非常适合入门用户,这也是它被当作默认模式的原因。用户可以使用git config push.default simple命令启用该模式。
在Git 2.0之前,该软件采用的默认推送模式是匹配模式。该模式在“推举”版本库工作流中对维护人员(有时也叫集成经理)最有用。但是大部分Git用户并非维护人员,这也是默认推送模式更换成简单模式的原因。维护人员一般会从其他开发人员那里收到变更提交记录,收到通过pull请求或者电子邮件发送的变更补丁后,维护人员会把它们集成到主题分支中。他们也可以为自己提交的工作成果创建相应的主题分支。然后这些主题分支经过考察后,会被集成到相应的集成分支中(例如maint、master和next分支)。上述操作都是在维护人员的私人版本库中完成的。公共的“推举”版本库(每个用户拉取的源版本库)中只包含长期分支(否则开发人员可能会发现自己正在开发的某个分支不知道哪天就消失了)。Git系统自身是无法判断一个分支是短期的还是长期的。在匹配模式下,Git将会推送所有和远程版本库中同名的本地分支。这意味着只有已经发布过的分支才会推送到远程版本库。为了让一个新的分支支持推送,用户需要显式声明这一点,例如使用如下代码:
$ git push origin maint-1.4
需要注意的是,在这种模式下和其他模式不同,执行git push命令时不提供分支列表,它也可以一次性推送多个分支,但是不一定会推送当前分支。
为了在全局启用匹配模式,用户可以执行如下命令:
$ git config push.default matching
如果用户希望为某个特定版本库启用该模式,那么需要使用由:组成的特殊refspec映射格式。假定上述版本库的名称是origin,现在希望对它执行非强制推送,那么可以执行如下代码:
$ git config remote.origin push :
当然,用户可以在命令行中使用推送匹配分支的refspec规范:
$ git push origin :
在中心式工作流中,只有一个公共的中心版本库供每个开发者推送访问。这个公共版本库将会只包含长期分支,通常只有maint和master分支,有时也只包含master分支。用户永远不要直接在master工作(当然使用简单的单个主题分支是个例外),不过可以在远程跟踪分支之外为每个特性创建一个独立的主题分支:
$ git checkout -b feature-foo origin/master
在中心式工作流中,集成是分布式的:在中心公共版本库中,每个开发人员都有责任合并变更(在他们自己的主题分支上),发布最终成果到master分支。用户需要更新本地的master分支,并将主题分支合并到其中,最后推送:
$ git checkout master
$ git pull
$ git merge feature-foo
$ git push origin master
一种替代性的解决方案是对远程跟踪分支上的主题分支进行变基要好过合并它们。变基之后,主题分支将变成远程版本库中master分支一个祖先,因此推送到master分支会很方便:
$ git checkout feature-foo
$ git pull --rebase
$ git push origin feature-foo:master
在上述两种情况下,用户可以将本地分支推送到远程版本上跟踪的对应分支(master分支在基于合并的工作流中,特性分支在基于变基的工作流中)。在这种情况下是版本库origin的master分支。
这就是上游推送模式的工作机制:
$ git config push.default upstream
这种模式使得Git可以将当前分支推送到远程版本库中特定的分支上——经常集成当前分支变更记录的分支。这种分支在远程版本库上就是上游分支(用户可以通过@{upstream}访问该上游分支)。启用该模式之后,上述示例最后的命令行语句都可以得到简化:
$ git push
上游的相关信息既可以是自动创建的(在提取远程跟踪分支时),也可以是通过–track选项显式声明的。它们一般存放在配置文件中,并且使用普通的配置工具就可以进行编辑。
此外,用户还可以通过如下命令对该信息进行修改:
$ git branch --set-upstream-to=<branchname>
在“推举”版本库工作流中,每个开发人员都拥有自己的私人和公开版本库。在这种模式下,用户从“推举”版本库中拉取变更,推送变更到自己的公共版本库中。在这种工作流中,用户可以创建一个新的主题分支,启动新特性的研发:
$ git checkout -b fix-tty-bug origin/master
当特性研发完成之后,用户将它们推送到自己的公共版本库上,可能还需要先执行变基操作,方便维护人员合并:
$ git push origin fix-tty-bug
这里假定用户使用pushurl配置三角工作流,推送的远程版本库名称是origin。与用户为自己的公共版本库选用的是独立的远程主机时,则还需要将这里的origin名称替换成相应的推送远程版本库名称(使用独立的版本库不仅可以用于推送,还可以用来实现不同机器之间的同步功能)。
为了配置Git能够在fix-tty-bug分支上只执行git push命令,用户需要让Git系统选择当前推送模式,相关代码如下:
$ git config push.default current
该模式将会推送当前分支到接收端的同名分支。需要注意的是,如果用户使用独立远程版本库来发布变更,则还需要配置remote.pushDefault选项,让它在执行git push命令时只支持发布推送。