Git正解 脱水版 【5. 分布式Git】

5.1 分布式工作流程

Git的分布式特性,可使协作开发更加灵活,在中心式版本控制系统(CVCS)中,每个用户可视为连接到中心hub的一个节点,而在Git中,每个用户既是节点又是hub,用户既可以向远程仓库推送数据,也可维护同一个远程仓库,由此产生了多种的工作流程,当然各有优缺点,而用户可单独选用,也可以混合使用。

中心式工作流

在CVCS中,只有一种协作模式,即中心式工作流,只有中心hub(或称仓库)可放置代码,所有用户节点必须连接到中心hub,才能进行开发,同时需要保证中心hub的数据同步,
在这里插入图片描述
这意味着,如果两个协作者同时基于中心hub的数据,进行后续变更,首个协作者者完成推送后,第二个协作者必须合并首个协作者的推送,再推送自己的变更,同时不能覆盖首个协作者的推送,这一点已被Git继承。

如果公司或团队内部,已经习惯了中心式工作流,迁移到Git,无需任何改变,只需配置一个仓库,将推送权限授予所有用户,Git同样不允许用户推送的相互覆盖。

举例说明,John和Jessica处于并行开发中,John率先完成开发,并推送到服务器,当Jessica完成开发后,向服务器推送数据时,却被服务器驳回,原因是,Jessica必须获取合并John的提交,首先完成与服务器的同步后,才可推送自己的数据,这类工作流的优势在于,大多数人已经习惯了这种模式,当然这类工作流并不局限于小团队,基于Git的分支功能,可满足多条分支上,数百人的同步开发。

集中管理工作流

Git允许用户同时使用多个远程仓库,每个开发者都能写入自己的远程仓库,并读取他人的仓库,在这种情况下,通常会存在一个标准仓库,或称之为官方仓库,如果用户想参与该项目,则需要创建一个官方仓库的公开副本,再将自己的开发结果,推送公开副本,之后可告知官方仓库的维护者,获取开发数据,然后维护者可将用户仓库,设为远程仓库,获取并测试用户的代码,最后合并到本地仓库,再推送到官方仓库,整个流程如下,

  1. 项目维护者可向官方仓库推送数据
  2. 代码贡献者需克隆官方仓库,再进行开发
  3. 代码贡献者将开发结果,推送到自己的远程仓库
  4. 代码贡献者使用邮件,告知维护者,申请合并自己的开发成果
  5. 维护者添加代码贡献者的远程仓库,进行本地合并
  6. 维护者将本地仓库的变更,推送到远程仓库

在这里插入图片描述
GitHub和GitLab均采用这类常见的工作流,任何人都能创建项目副本,并查看他人的开发成果,另一个优势在于,项目成员之间不会相互打扰,维护者可在任意时间,获取代码贡献者的数据,代码贡献者也无需等待自己的数据,被合并到官方仓库。

分层集中(司令官与副官)工作流

这是集中管理工作流的一个变种,适用于拥有数百位协作者的巨型项目,比如Linux内核,多位集中管理员负责整个仓库的不同部分,他们被称为副官,所有副官之上,还有一位司令官,司令官可将所有协作者的工作成果,推送到远程仓库,整个工作流程如下:

  1. 开发者基于远程仓库的最新master分支,创建特性分支,并开始工作,之后将衍合提交,可简化提交历史,便于上层的合并
  2. 副官们可将下层开发者们的特性分支,合并到自己的本地分支master
  3. 司令官可将副官们的master分支,合并到自己的本地分支master
  4. 最后,司令官可将本地分支master推送到远程仓库的分支master
    在这里插入图片描述
    上述工作流并不常见,只适用于巨型项目,或者需要分级管理的环境,它只允许司令官分配任务,收集来自不同部分的大量变更。

5.2 贡献代码

由于Git足够灵活,有多种协作开发的方式,给出一个贡献代码的标准方法,这相当困难,因为每个项目都存在差异,比如贡献者的数量,选定的工作流,用户权限,外部用户的沟通方式,

首先是贡献者的数量,在小团队中,贡献代码相对简单,而在大型项目中,数千位贡献者每天会产生成百上千个提交,更多的贡献者意味着,
保持代码的整洁,以及易于合并,会变得更加困难,所以每个贡献者都必须保证自己的代码,在合并过程中,不会出现过期或严重错误。

其次是工作流的选择,在中心式工作流中,所有用户是否都需要写入权限,项目维护者是否需要检查所有的补丁,而所有补丁需要通过同级评审还是核准,在分层管理中,如何保证提交的快速处理。

之后是用户权限,在工作流的影响下,用户是否拥有写入权限,在贡献代码时,则有相当大的差异,如果没有写入权限,项目需要提供一个接受贡献的方式或策略,这将影响到用户单次贡献的工作量,以及贡献的间隔时间。

只有想清楚以上问题的答案,才能得到一个有效的工作流,以及实现高效的代码贡献。

提交指南

在Git自身的项目中,提供了一份文档,其中包含了一些技巧,可基于补丁的方式,创建一个提交,参见Git源码包的Documentation/SubmittingPatches文件。

首先用户更新中,不能包含空白符号的错误,Git提供了一个检查工具git diff --check,如下的红色块,因为空白符的问题,可能会骚扰到其他协作者。
在这里插入图片描述
其次,保证每次提交都是一个独立逻辑单元,以使得每次提交都易于理解,千万不要采用扔垃圾的方式,打包提交,应当提供每次提交的明确的指向性,如果同一文件中,进行多次修改,可使用git add --patch,实现局部暂存,其实无论用户提交一次,还是提交十次,项目分支上,呈现的最后结果都是一致的,但提交十次,可使协作者更容易查看变更的细节。同时这类方式,更有利于提交的发布和恢复,之前的内容中,已经介绍了一些技巧,用于修整提交历史,以及暂存区文件的处理,以期望获得一个更简洁和易于理解的提交历史。

最后值得注意的事,养成高质量提交描述的编写习惯,可使协作开发更简单,一般情况下,提交描述只有一行,并且不超过50个字符,同时简明扼要,提交描述之后是一个空白行,空白行之后,将给出更多的提交细节,其中包含了修改原因,修改前后的对比,在提交描述的编写中,推荐使用一般语态,以下是Tim Pope总结的提交描述的模板,

标题,不超过50个字符的总结
 
如果有必要,应给出更详细的解释,但不要超过72个字符,
在某些情况下,标题行将视为邮件标题,后续文本将视为邮件正文,
使用空白行,分割标题行和正文,如上,除非正文被忽略,
如果忽略空白行,类似于rebase的工具,将无法正确解析。
 
使用一般语态,编写提交描述,更符合git merge和git revert等命令,
生成的结果。
 
后续描述,应放置在空白行之后。
 
 - 前缀也使用圆点符号
 
 - 前缀通常会使用连字符或星号,后跟一个空格符,并使用空白行,分割文本行,这可自行约定
 
 - 使用缩进

运行git log --no-merges,用户可查看Git自身项目中,包含的格式规整的提交描述,注意,本教程的大多数提交,为了简化描述,都没有提供一个格式规整的提交描述。

小型团队

如果是几人参与的私有闭源项目,所有项目成员都具有远程仓库的推送权限,在这种情况下,可选择简单的中心式工作流,即使在这类工作流中,Git相比于中心式CVS,同样具备优势。举例说明,两个开发者加一个远程仓库的故事,首位开发者John克隆了远程仓库,并完成了修改,再提交到本地仓库,

# John's Machine
$ git clone john@githost:simplegit.git
Cloning into 'simplegit'...
...
$ cd simplegit/
$ vim lib/simplegit.rb
$ git commit -am 'remove invalid default value'
[master 738ee87] remove invalid default value
1 files changed, 1 insertions(+), 1 deletions(-)

第二个开发者Jessica也克隆了远程仓库,并将一个变更,提交到本地仓库,

# Jessica's Machine
$ git clone jessica@githost:simplegit.git
Cloning into 'simplegit'...
...
$ cd simplegit/
$ vim TODO
$ git commit -am 'add reset task'
[master fbff5bc] add reset task
1 files changed, 1 insertions(+), 0 deletions(-)

之后Jessica将工作成果,推送到远程仓库,

# Jessica's Machine
$ git push origin master
...
To jessica@githost:simplegit.git
1edee6b..fbff5bc master -> master

在推送操作的输出结果中,最后一行给出了一个有价值的信息,基本格式为<原有提交的校验码>…<当前提交的校验码> [本地分支名] -> [远程分支名],继续故事,之后John也将本地仓库,推送到远程仓库,

# John's Machine
$ git push origin master
To john@githost:simplegit.git
! [rejected]
 master -> master (non-fast forward)
error: failed to push some refs to 'john@githost:simplegit.git'

John推送出错的原因,John的本地仓库与远程仓库不同步,首先John需下载Jessica的推送数据,注意Jessica的最后提交的验证码为fbff5bc,即下图的fbff5,738ee为John本地仓库的最新提交,

$ git fetch origin
...
From john@githost:simplegit
+ 049d078...fbff5bc master  -> origin/master

在这里插入图片描述
将Jessica的推送数据,与John的最新提交进行合并,

$ git merge origin/master
Merge made by the 'recursive' strategy.
TODO | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)

在这里插入图片描述
这时John需测试Jessica的推送数据,对自己的代码是否有影响,完成测试后,John可将合并生成的最新提交,推送到远程仓库,

$ git push origin master
...
To john@githost:simplegit.git
fbff5bc..72bbc59 master -> master

在这里插入图片描述
这时Jessica又创建了另一个分支issue54,并向本地仓库中,提交了三次更新,同时她并未获取远程仓库中,John的最新推送,
在这里插入图片描述
John告知Jessica,自己已向远程仓库,推送了最新数据,为了避免推送失败,Jessica需要获取远程仓库的最新数据,

# Jessica's Machine
$ git fetch origin
...
From jessica@githost:simplegit
fbff5bc..72bbc59 master -> origin/master

在这里插入图片描述
当新建分支的开发完成后,Jessica需要了解John的工作成果中,哪些可以合并,可运行git log进行查找,

$ git log --no-merges issue54..origin/master
commit 738ee872852dfaa9d6634e0dea7a324040193016
Author: John Smith <[email protected]>
Date: Fri May 29 16:01:27 2009 -0700
 
 remove invalid default value

issue54…origin/master格式是一个过滤器,用于显示,包含在origin/master分支,但并未包含在issue54分支的提交,基于上述输出,只有一个提交,需要Jessica进行合并,如果直接合并到新分支,该提交将改变Jessica本地仓库的历史记录,不可取,因此Jessica首先需要将新建分支,合并到本地master分支,再将John的工作成果(origin/master分支),合并到本地master分支,之后才可推送到远程仓库,为合并issue54分支,首先Jessica需切换到本地master分支,

$ git checkout master
Switched to branch 'master'
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.

Jessica可自由选择origin/master和issue54分支的合并次序,因为它们都是上游分支,次序不会产生任何问题,合并的最终结果都是一样的,只是提交的历史记录会有差别,Jessica决定首先合并issue54分支,

$ git merge issue54
Updating fbff5bc..4af4298
Fast forward
  README           | 1 +
  lib/simplegit.rb | 6 +++++-
  2 files changed, 6 insertions(+), 1 deletions(-)

合并未出现问题,Jessica继续合并,包含John推送的origin/master分支,

$ git merge origin/master
Auto-merging lib/simplegit.rb
Merge made by the 'recursive' strategy.
  lib/simplegit.rb | 2 +-
  1 files changed, 1 insertions(+), 1 deletions(-)

在这里插入图片描述
这时Jessica已完成了所有的本地合并,可将最新提交,推送到远程仓库,

$ git push origin master
...
To jessica@githost:simplegit.git
  72bbc59..8059c15 master -> master

在这里插入图片描述
当然这是一种最简单的工作流,对应的流程图如下,
在这里插入图片描述

分组协作

再来看一个更复杂的分组协作示例,John和Jessica负责开发功能A,Jessica又和Josie负责开发功能B,并使用集中管理工作流,每一组只有一人可管理分支,同时所有开发者中,也只有一人,可向远程仓库的master分支,推送数据,因此所有开发都将在组分支上完成,最后合并到一起,Jessica担负了两个小组的开发和管理任务,假定她已克隆了远程仓库,并决定先从功能A开始,于是创建featureA分支,并完成开发和提交,

# Jessica's Machine
$ git checkout -b featureA
Switched to a new branch 'featureA'
$ vim lib/simplegit.rb
$ git commit -am 'add limit to log function'
[featureA 3300904] add limit to log function
  1 files changed, 1 insertions(+), 1 deletions(-)

这时Jessica需要将工作成果,分享给John,因此将featureA分支推送到远程仓库,注意,Jessica并没有向远程仓库的master分支推送数据,当然她也没有权限,

$ git push -u origin featureA
...
To jessica@githost:simplegit.git
  * [new branch] featureA -> featureA

Jessica通过邮件,告知John,从远程仓库中,获取featureA分支,并开始等待John的反馈,这时Jessica决定开发功能B,之后基于远程master分支的本地副本origin/master,创建一个新分支featureB,注意,如果直接套用featureA的创建命令,等同于在featureA分支的基础上,创建featureB分支,

# Jessica's Machine
$ git fetch origin
$ git checkout -b featureB origin/master
Switched to a new branch 'featureB'

之后Jessica在featureB分支上,提交了若干更新:

$ vim lib/simplegit.rb
$ git commit -am 'made the ls-tree function recursive'
[featureB e5b0fdc] made the ls-tree function recursive
  1 files changed, 1 insertions(+), 1 deletions(-)
$ vim lib/simplegit.rb
$ git commit -am 'add ls-files'
[featureB 8512791] add ls-files
  1 files changed, 5 insertions(+), 0 deletions(-)

在这里插入图片描述
当Jessica准备推送featureB分支时,收到了Josie邮件,邮件中提到,Josie也在本地仓库中创建了新分支,完成了功能B的初始功能,并推送到远程仓库的featureBee分支,这时Jessica需要首先获取Josie的推送数据,

$ git fetch origin
...
From jessica@githost:simplegit
  * [new branch] featureBee -> origin/featureBee

假定Jessica当前处于featureB分支,可将origin/featureBee分支,合并到当前分支,

$ git merge origin/featureBee
Auto-merging lib/simplegit.rb
Merge made by the 'recursive' strategy.
  lib/simplegit.rb | 4 ++++
  1 files changed, 4 insertions(+), 0 deletions(-)

Jessica当然希望直接将合并后的featureB分支,直接推送到远程仓库,但是Josie已创建一个上游分支featureBee,因此Jessica必须使用分支映射功能,完成featureB分支的推送,

$ git push -u origin featureB:featureBee
...
To jessica@githost:simplegit.git
  fba9af8..cd685d1 featureB -> featureBee

上述操作即为分支名的映射,-u选项(–set-upstream的缩写)可配置分支名的映射。

之后Jessica又收到了John的邮件,邮件中提到,John已向远程仓库的featureA分支,推送了更新,这时Jessica运行git fetch,实现本地仓库与远程仓库的同步,

$ git fetch origin
...
From jessica@githost:simplegit
  3300904..aad881d featureA -> origin/featureA

Jessica可查看John所推送的提交,

$ git log featureA..origin/featureA
commit aad881d154acdaeb2b6b18ea0e827ed8a6d671e6
Author: John Smith <[email protected]>
Date: Fri May 29 19:57:33 2009 -0700
  changed log output to 30 from 25

之后Jessica需将John的推送,合并到本地featureA分支,

$ git checkout featureA
Switched to branch 'featureA'
$ git merge origin/featureA
Updating 3300904..aad881d
Fast forward
  lib/simplegit.rb | 10 +++++++++-
  1 files changed, 9 insertions(+), 1 deletions(-)

之后Jessica完成了功能A的开发,提交到本地featureA分支,并推送到远程仓库,

$ git commit -am 'small tweak'
[featureA 774b3ed] small tweak
  1 files changed, 1 insertions(+), 1 deletions(-)
$ git push
...
To jessica@githost:simplegit.git
  3300904..774b3ed featureA -> featureA

在这里插入图片描述
此时Jessica,Josie,John已完成了功能开发,项目管理者可将featureA和featureBee分支,合并到远程仓库的master分支,之后Jessica又将远程仓库,同步到本地仓库,如下,
在这里插入图片描述
许多开发团队迁移到Git的原因之一,Git允许多个开发组的并行,并在开发过程中,不断进行组分支的合并,这使得团队的开发组也能通过远程分支完成协作,且不会影响到整个团体的开发,上述工作流的流程图如下,
在这里插入图片描述

公共项目

公共项目的贡献稍有不同,代码贡献者一般不会有,直接更新项目分支的权限,必须使用其他方法,其一,采用Git的复制功能,大多数Git线上主机(GitHub,BitBucket,repo.or.cz等)都支持该功能,之后可通过邮件,发送更新补丁,完成代码贡献,而大多数公共项目的维护者也喜欢这种方式,即使在私有项目中,开发者也可使用rebase -i,将工作成果或是多个提交,转换成更新补丁,发送给项目维护者审核。

代码贡献者可在项目的原始页面中,找到Fork按钮,点击它,即可生成一个可写入的项目副本,这等同于创建一个可写入的远程仓库,该远程仓库将命名为myfork,

$ git remote add myfork <url>

之后贡献者可将提交,推送到这个远程仓库,当然更简单的方式,即为创建一个新分支,而不是直接合并到远程仓库的master分支,因为贡献者的提交不一定会被项目维护者采纳,如果直接合并到master分支,则无法保证master分支的同步,如果项目维护者接纳了贡献者的代码,贡献者可将官方远程仓库,同步到本地仓库,再将本地仓库,推送到(从属于贡献者的)副本远程仓库,贡献者首先将提交,推送到副本远程仓库,

$ git push -u myfork featureA

再通知官方远程仓库的维护者,请求接纳贡献者的提交,通常情况下,请求可在web页面上完成,比如GitHub就有类似的请求机制,同时贡献者还可使用git request-pull命令,以及邮件,向维护者发送请求。git request-pull命令包含两个参数,一是本地新建分支的基础分支,通常是官方仓库的master分支,二是贡献者的副本远程仓库的URL,基于这两个参数,该命令可将贡献者的所有提交,生成一个细节列表。假设Jessica向John发出接纳请求,同时Jessica在本地分支上,提交了两次更新,可运行

$ git request-pull origin/master myfork
The following changes since commit 1edee6b1d61823a2de3b09c160d7080b8d1b3a40:
Jessica Smith (1):
    added a new function
    
are available in the git repository at:
    git://githost/simplegit.git featureA
    
Jessica Smith (2):
    add limit to log function
    change log output to 30 from 25
    
lib/simplegit.rb | 10 +++++++++-
1 files changed, 9 insertions(+), 1 deletions(-)

以上输出将发送给维护者,其中提到,包含提交的远程分支名,所有提交的细节,以及副本远程仓库的地址。

代码贡献者不要将自己的提交,合并到master(即origin/master),一旦贡献提交被拒,也很容易丢弃提交和新建分支,重新开始,如果代码贡献者继续第二次贡献,依然需要使用master分支为基础,而不是自建分支。

$ git checkout -b featureB origin/master
... work ...
$ git commit
$ git push myfork featureB
$ git request-pull origin/master myfork
... email generated request pull to maintainer ...
$ git fetch origin

当然贡献者可以自建多个分支,并使用覆盖,衍合,修改等方法,对分支进行维护,但是应当保证每条分支之间的独立性,而不是相互影响,比如,
在这里插入图片描述
项目维护者获取到贡献者的提交后,在合并featureA分支时,出现冲突,这时维护者会将冲突,告知贡献者,由贡献者解决这些冲突,如下,贡献者将featureA分支衍合到origin/master,解决冲突后,再次请求维护者接纳,

$ git checkout featureA
$ git rebase origin/master
$ git push -f myfork featureA

在这里插入图片描述
由于贡献者使用了衍合,git push必须附带-f选项,以便新推送的featureA分支,可覆盖之前的featureA分支,同时之前的featureA分支不存在下游提交,否则覆盖将失败,另一个方法,将更新推送到另一个分支,比如featureAv2分支。

以下是可能出现的场景,维护者很欣赏featureB的设计思路,但觉得实现细节有瑕疵,这时贡献者需回到origin/master,再新建一个分支,用于镜像featureB分支,并重新实现featureB的设计思路,之后推送到副本远程仓库,

$ git checkout -b featureBv2 origin/master
$ git merge --squash featureB
... change implementation ...
$ git commit
$ git push myfork featureBv2

–squash选项可实现分支合并的所有操作,并会告知Git仓库,两条分支是镜像关系,因此不会产生合并提交,这意味着后续提交也只有一个父提交,实际上Git提供一种分支复制的方法,同样–no-commit选项也可省略分支合并之后的自动提交,但两者之间存在区别,后者主要用于测试分支合并是否存在冲突。之后通知维护者,请求接纳贡献者的featureBv2分支,
在这里插入图片描述

基于邮件的代码贡献

大多数项目都建立了一套接纳补丁的流程,由于项目之间存在差异,因此代码贡献者需要了解每个项目的特殊规则,一些早期的大型项目通常会基于一个开发者邮件列表,来接纳补丁。

这类工作流与之前介绍的工作流基本相似,贡献者需要为每个补丁,创建一个特性分支,不同之处在于,如何提交补丁,同时贡献者无需创建项目副本,无需建立副本远程仓库,只需将所有提交,转换成邮件,并基于开发者邮件列表,完成邮件发送,如果贡献者需要发送两次提交,首先需使用git format-patch,将提交转换成邮件(mbox格式),提交描述的首行将变成邮件标题,提交描述的剩余部分,以及由提交生成的补丁,将成为邮件正文,

$ git format-patch -M origin/master
0001-add-limit-to-log-function.patch
0002-changed-log-output-to-30-from-25.patch

format-patch可输出,已生成的补丁文件,-M选项可允许Git检查补丁文件是否重名,以下是补丁文件的内容,

$ cat 0001-add-limit-to-log-function.patch
From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001
From: Jessica Smith 
Date: Sun, 6 Apr 2008 10:17:23 -0700
Subject: [PATCH 1/2] add limit to log function
 
Limit log functionality to the first 20
 
---
 lib/simplegit.rb |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
 
diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index 76f47bc..f9815f1 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -14,7 +14,7 @@ class SimpleGit
   end
 
   def log(treeish = 'master')
-    command("git log #{treeish}")
+    command("git log -n 20 #{treeish}")
   end
 
   def ls_tree(treeish = 'master')
--
2.1.0

不方便放入提交描述的内容,可加入到补丁文件中,贡献者可在—行,与补丁首行(diff --git行)之间,添加相关文本,这些文本可被开发者读取,同时又不会影响到补丁文件的正文。

为了发送邮件,可使用命令行工具,或者将文本粘贴到邮件客户端,粘贴文本经常会产生格式问题,比如一些邮件客户端,不会保留空白行和空白字符,因此Git又提供一个工具,方便用户的邮件发送,即Gmail,在Git源码文件Documentation/SubmittingPatches最后,提供了不同邮件客户端的操作指南。

首先需设定~/.gitconfig文件的imap参数项,其中包含的参数,可使用git config设定,或是手动编辑配置文件,如下,

[imap]
  folder = "[Gmail]/Drafts"
  host = imaps://imap.gmail.com
  user = user@ gmail.com
  pass = YX]8g76G_2^sFbd
  port = 993
  sslverify = false

如果IMAP服务器没有启用SSL,最后两行无需配置,邮件服务器的前缀,应使用imap://,而不是imaps://,完成配置后,可使用git send-email,将补丁文件放入,指定IMAP服务器的Drafts目录,

$ cat *.patch |git imap-send
Resolving imap.gmail.com... ok
Connecting to [74.125.142.109]:993... ok
Logging in...
sending 2 messages
100% (2/2) done

之后进入Drafts目录,添加邮件的发送地址,邮件可能需要抄送给项目维护者或相关人员,邮件发送需使用SMTP服务器,所以在.gitconfig配置文件中,还需要设定sendemail参数项,如下,

[sendemail]
  smtpencryption = tls
  smtpserver = smtp.gmail.com
  smtpuser = [email protected]
  smtpserverport = 587

运行git send-email,完成补丁邮件的发送。

$ git send-email *.patch
0001-added-limit-to-log-function.patch
0002-changed-log-output-to-30-from-25.patch
Who should the emails appear to be from? [Jessica Smith <[email protected]>]
Emails will be sent from: Jessica Smith <[email protected]>
Who should the emails be sent to? [email protected]
Message-ID to be used as In-Reply-To for the first email? y

Git完成补丁邮件的发送后,将输出以下信息,

(mbox) Adding cc: Jessica Smith <[email protected]> from
  \line 'From: Jessica Smith <jessica@ example.com>'
OK. Log says:
Sendmail: /usr/sbin/sendmail -i [email protected]
From: Jessica Smith <[email protected]>
To: [email protected]
Subject: [PATCH 1/2] added limit to log function
Date: Sat, 30 May 2009 13:29:15 -0700
Message-Id: <[email protected]>
X-Mailer: git-send-email 1.6.2.rc1.20.g8c5b.dirty
In-Reply-To: <y>
References: <y>

Result: OK

5.3 项目维护

除了学习如何为项目贡献代码,用户还需要了解如何进行项目维护,其中包括,format-patch生成补丁或是邮件补丁的接纳,远程分支的处理,如何处理贡献者的提交,以获得仓库运行的长期稳定。

特性分支

关于特性分支,之前已介绍过多次,尝试使用特性分支(或称之为临时分支),可以保证分支之间不存在相互干扰,用户可随时弃用特性分支,回到初始点。

邮件补丁

如果需要将邮件补丁,集成到项目中,建议先放入特性分支,进行评估,补丁集成可使用两种方法,git apply和git am。

git apply

如果补丁来自于git diff或是diff变种(类unix命令,不推荐使用),补丁集成可使用git apply,假定补丁文件为/tmp/patch-ruby-client.patch,

$ git apply /tmp/patch-ruby-client.patch

被补丁修改过的文件,将放入当前工作区,git apply基本类似于patch -p1,但更加谨慎,并能实现少量的模糊匹配,如果补丁文件来自于git diff,git apply还可处理文件的添加,删除和重命名,而patch无法完成这类任务,最后git apply还引入了,全部采用或全部放弃的工作机制,这使得集成补丁文件,只有两种结果,一是全部采用,二是全部丢弃,但patch可实现补丁的局部采用,这会将工作区带入一个不可预知的状态,因此不推荐使用diff工具,生成Git补丁,同时git apply不会产生仓库提交,完成补丁集成后,用户需要将工作区的修改文件暂存,并手动提交。运行git apply --check,可测试补丁的集成结果。

$ git apply --check 0001-seeing-if-this-helps-the-gem.patch
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply

如果无输出信息,则表明补丁能被集成,如果测试失败,该命令将返回一个非零值,所以在shell脚本中,可使用该命令。

git am

代码贡献者还可使用format-patch生成补丁文件,同时补丁文件中,还包含了作者信息和提交描述,对于维护者来说,集成这类补丁文件则更加简单,因此强烈建议,贡献者使用format-patch。

运行git am,可集成format-patch生成的补丁,该命令可读取mbox文件,这是一种简单的能包含更多邮件信息的纯文本,如下,

From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001
From: Jessica Smith <[email protected]>
Date: Sun, 6 Apr 2008 10:17:23 -0700
Subject: [PATCH 1/2] add limit to log function

Limit log functionality to the first 20

以上是git format-patch输出的头几行,用于描述一个有效的mbox格式,维护者基于邮件或手动下载,得到一个mbox补丁后,可使用git am集成该补丁,邮件客户端可将多个补丁邮件,导出为一个mbox文件,之后可一次性集成多个补丁。

当补丁集成后,会自动生成一次提交,提交中包含的作者信息,来自邮件的From和Date项,提交描述来自邮件的Subject项,以及邮件正文,以下是一个邮件补丁的提交示例,

$ git log --pretty=fuller -1
commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Author:     Jessica Smith 
AuthorDate: Sun Apr 6 10:17:23 2008 -0700
Commit:     Scott Chacon  <[email protected]>
CommitDate: Thu Apr 9 09:19:06 2009 -0700
 
   add limit to log function
 
   Limit log functionality to the first 20

Commit给出了集成补丁的维护者,CommitDate给出了集成补丁的时间,Author给出了生成补丁的贡献者,AuthorDate给出了补丁的生成时间,补丁集成当然存在失败的可能性,也许是补丁过期的问题,或者是补丁之间的相互依赖,这种情况下,git am会报错,并推荐了几种常用的处理方法,

$ git am 0001-seeing-if-this-helps-the-gem.patch
Applying: seeing if this helps the gem
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply
Patch failed at 0001.
When you have resolved this problem run "git am --resolved".
If you would prefer to skip this patch, instead run "git am --skip".
To restore the original branch and stop patching run "git am --abort".

git am命令会在对应文件中,添加冲突标记,类似于合并冲突和衍合冲突,当然也能使用相同的解决方法,消除文件的冲突,暂存修改文件,运行git am --resolved,逐个解决冲突补丁,

(fix the file)
$ git add ticgit.gemspec
$ git am --resolved
Applying: seeing if this helps the gem

如果用户需要Git处理冲突,可附带-3选项,以使Git尝试三方合并,来解决冲突,同时-3选项并不是默认操作,如果提交补丁与特定仓库无关,-3选项将失效,如果提交补丁来自一个公开提交,-3选项可用于解决冲突,如下,

$ git am -3 0001-seeing-if-this-helps-the-gem.patch
Applying: seeing if this helps the gem
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
No changes -- Patch already applied.

如上所示,如果不附带-3选项,提交补丁的重复集成,将被视为一个冲突,如果用户需集成一组补丁(mbox格式),则使用交互模式(-i选项)运行am命令,在集成每个补丁前,将会询问用户如何处理,

$ git am -3 -i mbox
Commit Body is:
--------------------------
seeing if this helps the gem
--------------------------
Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all

用户可基于交互模式,直接查看补丁,以及避免补丁的重复集成,当所有提交补丁都集成到特性分支,之后可选择,是否添加到长期分支中。

远程分支

如果贡献者创建了自己的远程仓库,并推送了一些提交,之后将仓库URL和远程分支名,告知项目维护者,维护者则需要添加给远程仓库,并将远程分支合并到本地。

比如Jessica告知项目维护者,自己仓库的ruby-client分支上,开发了一个新功能,维护者需添加该远程分支,合并到本地,进行测试,

$ git remote add jessica git://github.com/jessica/myproject.git
$ git fetch jessica
$ git checkout -b rubyclient jessica/ruby-client

之后Jessica又告知维护者,在另一个远程分支上,又开发了一个新功能,这时维护者只需使用fetch和checkout,因为Jessica的远程仓库已经添加,如果团队成员很少,上述方法可行,如果开发成员很多,每次贡献只有一个补丁,采用邮件接纳的方式,远比每个成员都创建和维护自己的服务器,更加节省时间,维护者不会希望添加数百个远程仓库,而每个成员的贡献只有一两个,当然利用脚本和线上服务器,这类问题可以简化,这取决于团队规模和开发模式。

使用远程分支的另一个好处,能得到一个干净的提交历史,虽然合并问题不可避免,但是维护者可获知提交的位置,这有利于三方合并,而不必使用-3选项,去祈祷补丁能够生成一个提交,如果是临时成员,维护者不必保存该成员的远程仓库,可需使用git pull,实现单次获取,

$ git pull https://github.com/onetimeguy/project
From https://github.com/onetimeguy/project
* branch HEAD -> FETCH_HEAD
Merge made by the 'recursive' strategy.

查看分支的差异

项目维护者通常会创建一条特性分支,用于接纳贡献者的工作成果,首先维护者需要了解特性分支的所有提交中,哪些未合并到master分支,可使用–not选项,当然也可使用master…contrib格式,查看两条分支的差异,比如贡献者发送了两个补丁,维护者创建了一条contrib分支,用于合并贡献者的提交补丁,

$ git log contrib --not master
commit 5b6235bd297351589efc4d73316f0a68d484f118
Author: Scott Chacon  <[email protected]>
Date:   Fri Oct 24 09:53:59 2008 -0700
    seeing if this helps the gem
 
commit 7482e0d16d04bea79d0dba8988cc78df655f16a0
Author: Scott Chacon  <[email protected]>
Date:   Mon Oct 22 19:38:36 2008 -0700
    updated the gemspec to hopefully work better

查看每个提交的修改内容,可在git log命令中,附带-p选项,如果需查看两个分支的完整差异,可使用,

$ git diff master

上述命令的输出结果,可能会让人费解,如果master分支在前(更新),contrib分支在后(更旧),将产生一个奇怪的结果,因为Git会直接比较两个分支的最新提交,比如在master分支的最新提交中,a文件添加了一行文本,而Git直接比较的结果是,在contrib分支中,a文件被删去一行文本,这显然不是真实情况。

如果master分支是contrib分支的直接祖先,则不存在上述问题,一旦两条分支产生分叉点,差异比较的结果则是,contrib分支中添加的所有内容,将等同于在master分支中全部被删除,

如果需查看contrib分支的实际修改,即contrib分支合并到master分支,所引入的变更,则应当比较,contrib分支的最新提交,与contrib分支和master分支的公共祖先,因此必须首先找到公共祖先,

$ git merge-base contrib master 
36c7dba2c95e6bbb78dfa822519ecfec6e1ca649 
$ git diff 36c7db

这非常复杂,因此Git提供了一种更简洁的语法,即三点(…)格式,只需在diff命令中,在两个分支名之间,加入三点符号,即可显示公共祖先之后,后续分支(contrib)产生的所有变化。

$ git diff master...contrib

贡献集成

当维护者准备好,将特性分支集成到主线分支时,该如何进行?项目维护者进行项目维护时,该如何选择工作流?以下提供了一些方案,

合并工作流

最简单的工作流,将所有工作结果,都合并到master分支,如果master分支只保存稳定代码,那么维护者只能在特性分支中,完成新功能的开发,或者是验证贡献者的提交,之后再将特性分支,合并到master分支,再删除已合并的特性分支,并反复迭代该模式。

举例说明,仓库中包含两条分支,ruby_client和php_client,并准备先合并ruby_client,再合并php_client,
在这里插入图片描述
在这里插入图片描述
虽然上述工作流很简单,但依然存在出错的风险,尤其是处理大型(稳定占优)项目时,项目维护者必须准确了解,合并引入了哪些变化。因此在一些重要项目中,项目维护者通常会使用两段式合并方案,这时会存在两个长期分支master和develop,只有当一个稳定的版本出现时,才会更新master分支,而所有的新代码将集成到develop分支,同时维护者还需要定期,将这两条分支推送到公共仓库,下图是一条特性分支合并到develop分支的前后对比,
在这里插入图片描述
在这里插入图片描述
当维护者发布一个新版本之后,master分支可快进到develop分支,如下图,
在这里插入图片描述
这时项目用户完成仓库克隆后,既可以切换到master分支,构建项目的最新版本,也可切换到develop分支,尝试最新的功能,同时维护者还能创建一条integrate分支,用于集成所有的工作成果,当这条分支的代码逐渐稳定并通过测试,则能合并到develop分支,如果这条分支的稳定性已被证实,可直接合并到master分支。

多分支合并的工作流

Git自身项目中,包含了四条长期分支,master,next,pu(建议更新的功能),maint(已发布版本的维护),当贡献者提交工作成果时,将被合并到维护者的特性分支,用于评估安全性和稳定性,或者需要进一步的改进,如果贡献者的提交通过评估,将合并到next分支,这时所有协作者可将其,合并到自己的特性分支,
在这里插入图片描述
如果合并到特性分支的贡献者提交,还需要进一步的改进,将被合并到pu分支,当这些提交稳定后,可再次合并到master分支,这时基于master分支,可重新创建next和pu分支,这意味着master可提供最新的版本,而next会偶尔衍合,pu将频繁衍合,如下图,
在这里插入图片描述
当特性分支最终被合并到master分支后,则可从仓库中,删除该特性分支,另外还有一条maint分支,它基于最新的发布版本,用于提供发布版本的维护补丁,当用户完成Git自身仓库的克隆后,将看到上述四条分支,切换到不同分支,评估当前的开发进度,当然这取决于用户是否需要尝试最新功能,或者需要贡献自己的成功,同时项目维护者还提供一个结构化的工作流程,方便用户查看最新的贡献,应当注意,Git自身项目是一个特例,为了充分理解Git的维护方式,可查阅Git的维护者指南,即git/Documentation/howto/maintain-git.txt文件。

衍合与挑选工作流

有些维护者更喜欢衍合贡献者的提交,合并到master分支,而不是简单合并,这可获得一个更线性的提交历史,当维护者在特性分支上,合并了贡献者的提交后,可切换到master(或develop等)分支,衍合特性分支。

另一个引入贡献者提交的方式,即挑选,Git的挑选类似于单个提交的衍合,它将单个提交补丁重新集成到当前分支,如果在特性分支中,包含大量的贡献者提交,而维护者只想集成一部分贡献者提交时,可使用挑选,即使特性分支中,只有一个贡献者提交,维护者也可使用挑选,如下,
在这里插入图片描述
如果维护者只需将e43a6提交,引入master分支,可运行

$ git cherry-pick e43a6fd3e94888d76779ad79fb568ed180e5fcdf
Finished one cherry-pick.
[master]: created a0a41a9: "More friendly message when locking the index fails."
 3 files changed, 17 insertions(+), 3 deletions(-)

这时e43a6提交已引入master分支,并获得了一个新提交,由于提交日期不同,新提交具有一个不同的校验值,
在这里插入图片描述
这时维护者可删除特性分支,以及不想保留的剩余提交,

rerere命令

如果用户需要进行大量的合并和衍合,或是维护一个长期的特性分支,Git提供了一个辅助功能rerere,可解释为记录重用,等同于手工解决冲突的简化方法,当rerere配置为有效,Git可将每次合并的前后记录(实际为用户手动处理冲突的记录),保存成一个记录集合,当后续合并出现一个冲突时,将查找这个记录集合,如果找到与当前冲突匹配的记录,可自动修复冲突,避免对用户造成困扰。

该功能开启一个配置选项,即rerere.enabled,可放入全局配置中,

$ git config --global rerere.enabled true

配置完成之后,用户手动处理的所有合并冲突,都将被Git保存,以供将来查找,如果有必要,在git rerere运行时,用户可与rerere缓冲实现交互模式,如果独自运行(非交互),Git将检查记录集合(数据库),并尝试查找与当前合并冲突相类似的记录,并尝试自动修复(rerere.enabled必须设为true),同时rerere还包含了一些选项,用于查看冲突记录,移除缓冲的特定记录,清空缓冲等等功能

版本标签

维护者在发布一个新版本之前,可能会为该版本分配一个标签,以便今后重建该版本,以下命令将创建一个标签,-s选项,可用仓库私钥,生成标签的PGP公钥,

$ git tag -s v1.5 -m 'my signed 1.5 tag'
You need a passphrase to unlock the secret key for
user: "Scott Chacon <[email protected]>"
1024-bit DSA key, ID F721C45A, created 2009-02-09

创建标签后,将遇到另一个问题,如何通告标签的PGP公钥,项目维护者的解决方法,再创建一个公钥标签,指向标签的PGP公钥,运行gpg --list-keys,可显示仓库中保存的PGP公钥,

$ gpg --list-keys
/Users/schacon/.gnupg/pubring.gpg
---------------------------------
pub 1024D/F721C45A 2009-02-09 [expires: 2010-02-09]
uid                Scott Chacon <[email protected]>
sub 2048g/45D02282 2009-02-09 [expires: 2010-02-09]

导出数据库包含的标签公钥数据块,通过管道,传递给git hash-object,解析获得标签公钥的SHA-1校验码,

$ gpg -a --export F721C45A | git hash-object -w --stdin
659ef797d181633c87ec71ac3f9ba29fe5775b92

获得校验码之后,创建一个公钥标签,指向标签公钥的校验码,

$ git tag -a maintainer-pgp-pub 659ef797d181633c87ec71ac3f9ba29fe5775b92

运行git push --tags,将公钥标签maintainer-pgp-pub共享给所有人,如果需要验证公钥标签,可从数据库中直接获取,这等同于导出标签的PGP公钥数据块,再导入GPG进行验证,

$ git show maintainer-pgp-pub | gpg --import

因此使用公钥标签,可验证仓库包含的标签,如果标签信息中包含了验证指令,可运行git show <标签名>,以使用户获得标签验证的相关指令,也可使用git tag -v <标签名>,直接验证已生成PGP公钥的标签。

版本号

由于Git在每次提交中,并不会自动提供一个累计序号,如果提交需要一个易于理解的名字,可运行git describe,这时Git将生成一个字符串,其中包含了,离当前提交最近的标签名(v1.6.2-rc1),最近标签之后的提交累计数(20),当前提交的局部校验值(g8c5b85c),g表示Git,

$ git describe master
v1.6.2-rc1-20-g8c5b85c

上述命令还可在快照或版本导出时,提供一个易于理解的名字,使用源码编译生成的Git,使用git --version同样能输出一个版本号,如果提交包含了标签,将会直接输出标签名。

默认情况下,git describe需要附注标签(附带-a或-s选项)的支持,如果使用了简单标签,git describe需附带–tags选项,同时版本号也可传入git checkout和git show命令,虽然版本号的末尾使用了部分SHA-1校验码,但也存在重名的风险,比如Linux内核目前就使用8-10位的校验码,来保证SHA-1值的唯一性,因此早期的git describe输出将全部失效。

归档发布

在发布新版本之前,需创建一个最新快照的文档包,以方便那么未使用Git的用户,如下,

$ git archive master --prefix='project/' | gzip > `git describe master`.tar.gz
$ ls *.tar.gz
v1.6.2-rc1-20-g8c5b85c.tar.gz

普通用户可下载文档包,解压之后,就可得到项目的最新快照,当然也可创建一个zip压缩包,如下,

$ git archive master --prefix='project/' --format=zip > `git describe master`.zip

创建完成文档包之后,则可利用网页和邮件,分享给他人。

获取邮件列表

这时需要一份邮件列表,以保证所有对项目有贡献的普通用户,能够获知项目的最新进展,可使用git shortlog,列出上一个版本或上一封邮件之后,项目已加入的所有修改,并给出一个排序列表,其中将列出所有提交的简介,假定上一个版本的版本号为v1.0.1,如下,

$ git shortlog --no-merges master --not v1.0.1
Chris Wanstrath (8):
      Add support for annotated tags to Grit::Tag
      Add packed-refs annotated tag support.
      Add Grit::Commit#to_patch
      Update version and History.txt
      Remove stray `puts`
      Make ls_tree ignore nils
 
Tom Preston-Werner (4):
      fix dates in history
      dynamic version method
      Version bump to 1.0.2
      Regenerated gemspec for version 1.0.2

以上输出显示了v1.0.1版本之后的所有提交,并依据作者名进行了分组,维护者可将这些作者,加入到自己的邮件列表中。

在这里插入图片描述

发布了74 篇原创文章 · 获赞 8 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/osoon/article/details/103939530