码迷,mamicode.com
首页 > 其他好文 > 详细

Git专题--系统的学习Git之二

时间:2016-04-19 12:25:27      阅读:255      评论:0      收藏:0      [点我收藏+]

标签:

本文为整理自:伯乐在线 

1.Git详解之一:Git起步

2.Git详解之二:Git基础

3.Git详解之三:Git分支

4.Git详解之四:服务器上的Git

5.Git详解之五:分布式Git

6.Git详解之六:Git工具

7.Git详解之七:自定义Git

8.Git详解之八:Git与其他系统

9.Git详解之九:Git内部原理

Git详解之四:服务器上的Git

服务器上的 Git

到目前为止,你应该已经学会了使用 Git 来完成日常工作。然而,如果想与他人合作,还需要一个远程的 Git 仓库。尽管技术上可以从个人的仓库里推送和拉取修改内容,但我们不鼓励这样做,因为一不留心就很容易弄混其他人的进度。另外,你也一定希望合作者们即使在 自己不开机的时候也能从仓库获取数据 — 拥有一个更稳定的公共仓库十分有用。因此,更好的合作方式是建立一个大家都可以访问的共享仓库,从那里推送和拉取数据。我们将把这个仓库称为 “Git 服务器”;代理一个 Git 仓库只需要花费很少的资源,几乎从不需要整个服务器来支持它的运行。(伯乐在线注:如果你对Git还不了解,建议从本Git系列第一篇文章开始阅读)

架设一台 Git 服务器并不难。第一步是选择与服务器通讯的协议。本章第一节将介绍可用的协议以及各自优缺点。下面一节将介绍一些针对各个协议典型的设置以及如何在服务器 上实施。最后,如果你不介意在他人服务器上保存你的代码,又想免去自己架设和维护服务器的麻烦,倒可以试试我们介绍的几个仓库托管服务。

如果你对架设自己的服务器没兴趣,可以跳到本章最后一节去看看如何申请一个代码托管服务的账户然后继续下一章,我们会在那里讨论分布式源码控制环境的林林总总。

远程仓库通常只是一个_裸仓库(bare repository)_ — 即一个没有当前工作目录的仓库。因为该仓库只是一个合作媒介,所以不需要从硬盘上取出最新版本的快照;仓库里存放的仅仅是 Git 的数据。简单地说,裸仓库就是你工作目录中.git 子目录内的内容。

 

4.1 协议

Git 可以使用四种主要的协议来传输数据:本地传输,SSH 协议,Git 协议和 HTTP 协议。下面分别介绍一下哪些情形应该使用(或避免使用)这些协议。

值得注意的是,除了 HTTP 协议外,其他所有协议都要求在服务器端安装并运行 Git。

 

本地协议

最基本的就是_本地协议(Local protocol)_,所谓的远程仓库在该协议中的表示,就是硬盘上的另一个目录。这常见于团队每一个成员都对一个共享的文件系统(例如 NFS)拥有访问权,或者比较少见的多人共用同一台电脑的情况。后面一种情况并不安全,因为所有代码仓库实例都储存在同一台电脑里,增加了灾难性数据损失 的可能性。

如果你使用一个共享的文件系统,就可以在一个本地文件系统中克隆仓库,推送和获取。克隆的时候只需要将远程仓库的路径作为 URL 使用,比如下面这样:

1
$ git clone /opt/git/project.git

或者这样:

1
$ git clone file:///opt/git/project.git

如果在 URL 开头明确使用 file:// ,那么 Git 会以一种略微不同的方式运行。如果你只给出路径,Git 会尝试使用硬链接或直接复制它所需要的文件。如果使用了file:// ,Git 会调用它平时通过网络来传输数据的工序,而这种方式的效率相对较低。使用 file:// 前缀的主要原因是当你需要一个不包含无关引用或对象的干净仓库副本的时候 — 一般指从其他版本控制系统导入的,或类似情形(参见第 9 章的维护任务)。我们这里仅仅使用普通路径,这样更快。

要添加一个本地仓库作为现有 Git 项目的远程仓库,可以这样做:

1
$ git remote add local_proj /opt/git/project.git

然后就可以像在网络上一样向这个远程仓库推送和获取数据了。

 

优点

基于文件仓库的优点在于它的简单,同时保留了现存文件的权限和网络访问权限。如果你的团队已经有一个全体共享的文件系统,建立仓库就十分容易了。你 只需把一份裸仓库的副本放在大家都能访问的地方,然后像对其他共享目录一样设置读写权限就可以了。我们将在下一节“在服务器上部署 Git ”中讨论如何导出一个裸仓库的副本。

这也是从别人工作目录中获取工作成果的快捷方法。假如你和你的同事在一个项目中合作,他们想让你检出一些东西的时候,运行类似 git pull /home/john/project 通常会比他们推送到服务器,而你再从服务器获取简单得多。

 

缺点

这种方法的缺点是,与基本的网络连接访问相比,难以控制从不同位置来的访问权限。如果你想从家里的笔记本电脑上推送,就要先挂载远程硬盘,这和基于网络连接的访问相比更加困难和缓慢。

另一个很重要的问题是该方法不一定就是最快的,尤其是对于共享挂载的文件系统。本地仓库只有在你对数据访问速度快的时候才快。在同一个服务器上,如果二者同时允许 Git 访问本地硬盘,通过 NFS 访问仓库通常会比 SSH 慢。

 

SSH 协议

‘Git 使用的传输协议中最常见的可能就是 SSH 了。这是因为大多数环境已经支持通过 SSH 对服务器的访问 — 即便还没有,架设起来也很容易。SSH 也是唯一一个同时支持读写操作的网络协议。另外两个网络协议(HTTP 和 Git)通常都是只读的,所以虽然二者对大多数人都可用,但执行写操作时还是需要 SSH。SSH 同时也是一个验证授权的网络协议;而因为其普遍性,一般架设和使用都很容易。

通过 SSH 克隆一个 Git 仓库,你可以像下面这样给出 ssh:// 的 URL:

1
$ git clone ssh://user@server:project.git

或者不指明某个协议 — 这时 Git 会默认使用 SSH :

1
$ git clone user@server:project.git

如果不指明用户,Git 会默认使用当前登录的用户名连接服务器。

 

优点

使用 SSH 的好处有很多。首先,如果你想拥有对网络仓库的写权限,基本上不可能不使用 SSH。其次,SSH 架设相对比较简单 — SSH 守护进程很常见,很多网络管理员都有一些使用经验,而且很多操作系统都自带了它或者相关的管理工具。再次,通过 SSH 进行访问是安全的 — 所有数据传输都是加密和授权的。最后,和 Git 及本地协议一样,SSH 也很高效,会在传输之前尽可能压缩数据。

 

缺点

SSH 的限制在于你不能通过它实现仓库的匿名访问。即使仅为读取数据,人们也必须在能通过 SSH 访问主机的前提下才能访问仓库,这使得 SSH 不利于开源的项目。如果你仅仅在公司网络里使用,SSH 可能是你唯一需要使用的协议。如果想允许对项目的匿名只读访问,那么除了为自己推送而架设 SSH 协议之外,还需要支持其他协议以便他人访问读取。

 

Git 协议

接下来是 Git 协议。这是一个包含在 Git 软件包中的特殊守护进程; 它会监听一个提供类似于 SSH 服务的特定端口(9418),而无需任何授权。打算支持 Git 协议的仓库,需要先创建git-export-daemon-ok 文件 — 它是协议进程提供仓库服务的必要条件 — 但除此之外该服务没有什么安全措施。要么所有人都能克隆 Git 仓库,要么谁也不能。这也意味着该协议通常不能用来进行推送。你可以允许推送操作;然而由于没有授权机制,一旦允许该操作,网络上任何一个知道项目 URL 的人将都有推送权限。不用说,这是十分罕见的情况。

优点

Git 协议是现存最快的传输协议。如果你在提供一个有很大访问量的公共项目,或者一个不需要对读操作进行授权的庞大项目,架设一个 Git 守护进程来供应仓库是个不错的选择。它使用与 SSH 协议相同的数据传输机制,但省去了加密和授权的开销。

 

缺点

Git 协议消极的一面是缺少授权机制。用 Git 协议作为访问项目的唯一方法通常是不可取的。一般的做法是,同时提供 SSH 接口,让几个开发者拥有推送(写)权限,其他人通过git:// 拥有只读权限。Git 协议可能也是最难架设的协议。它要求有单独的守护进程,需要定制 — 我们将在本章的 “Gitosis” 一节详细介绍它的架设 — 需要设定xinetd 或类似的程序,而这些工作就没那么轻松了。该协议还要求防火墙开放 9418 端口,而企业级防火墙一般不允许对这个非标准端口的访问。大型企业级防火墙通常会封锁这个少见的端口。

 

HTTP/S 协议

最后还有 HTTP 协议。HTTP 或 HTTPS 协议的优美之处在于架设的简便性。基本上,只需要把 Git 的裸仓库文件放在 HTTP 的根目录下,配置一个特定的post-update 挂钩(hook)就可以搞定(Git 挂钩的细节见第 7 章)。此后,每个能访问 Git 仓库所在服务器上 web 服务的人都可以进行克隆操作。下面的操作可以允许通过 HTTP 对仓库进行读取:

1
2
3
4
5
$cd/var/www/htdocs/
 $ git clone --bare /path/to/git_projectgitproject.git
 $cdgitproject.git
 $mvhooks/post-update.sample hooks/post-update
 $chmoda+x hooks/post-update

这样就可以了。Git 附带的 post-update 挂钩会默认运行合适的命令(git update-server-info)来确保通过 HTTP 的获取和克隆正常工作。这条命令在你用 SSH 向仓库推送内容时运行;之后,其他人就可以用下面的命令来克隆仓库:

1
$ git clone http://example.com/gitproject.git

在本例中,我们使用了 Apache 设定中常用的 /var/www/htdocs 路径,不过你可以使用任何静态 web 服务 — 把裸仓库放在它的目录里就行。 Git 的数据是以最基本的静态文件的形式提供的(关于如何提供文件的详情见第 9 章)。

通过 HTTP 进行推送操作也是可能的,不过这种做法不太常见,并且牵扯到复杂的 WebDAV 设定。由于很少用到,本书将略过对该内容的讨论。如果对 HTTP 推送协议感兴趣,不妨打开这个地址看一下操作方法:http://www.kernel.org/pub/software/scm/git/docs/howto/setup-git-server-over-http.txt 。通过 HTTP 推送的好处之一是你可以使用任何 WebDAV 服务器,不需要为 Git 设定特殊环境;所以如果主机提供商支持通过 WebDAV 更新网站内容,你也可以使用这项功能。

 

优点

使用 HTTP 协议的好处是易于架设。几条必要的命令就可以让全世界读取到仓库的内容。花费不过几分钟。HTTP 协议不会占用过多服务器资源。因为它一般只用到静态的 HTTP 服务提供所有数据,普通的 Apache 服务器平均每秒能支撑数千个文件的并发访问 — 哪怕让一个小型服务器超载都很难。

你也可以通过 HTTPS 提供只读的仓库,这意味着你可以加密传输内容;你甚至可以要求客户端使用特定签名的 SSL 证书。一般情况下,如果到了这一步,使用 SSH 公共密钥可能是更简单的方案;不过也存在一些特殊情况,这时通过 HTTPS 使用带签名的 SSL 证书或者其他基于 HTTP 的只读连接授权方式是更好的解决方案。

HTTP 还有个额外的好处:HTTP 是一个如此常见的协议,以至于企业级防火墙通常都允许其端口的通信。

 

缺点

HTTP 协议的消极面在于,相对来说客户端效率更低。克隆或者下载仓库内容可能会花费更多时间,而且 HTTP 传输的体积和网络开销比其他任何一个协议都大。因为它没有按需供应的能力 — 传输过程中没有服务端的动态计算 — 因而 HTTP 协议经常会被称为_傻瓜(dumb)_协议。更多 HTTP 协议和其他协议效率上的差异见第 9 。

 

4.2 在服务器上部署 Git

开始架设 Git 服务器前,需要先把现有仓库导出为裸仓库 — 即一个不包含当前工作目录的仓库。做法直截了当,克隆时用 –bare 选项即可。裸仓库的目录名一般以.git 结尾,像这样:

1
2
$ git clone --bare my_project my_project.git
 Initialized empty Git repository in/opt/projects/my_project.git/

该命令的输出或许会让人有些不解。其实 clone 操作基本上相当于 git init 加 git fetch,所以这里出现的其实是git init 的输出,先由它建立一个空目录,而之后传输数据对象的操作并无任何输出,只是悄悄在幕后执行。现在 my_project.git 目录中已经有了一份 Git 目录数据的副本。

整体上的效果大致相当于:

1
$cp-Rf my_project/.git my_project.git

但在配置文件中有若干小改动,不过对用户来讲,使用方式都一样,不会有什么影响。它仅取出 Git 仓库的必要原始数据,存放在该目录中,而不会另外创建工作目录。

 

把裸仓库移到服务器上

有了裸仓库的副本后,剩下的就是把它放到服务器上并设定相关协议。假设一个域名为 git.example.com 的服务器已经架设好,并可以通过 SSH 访问,我们打算把所有 Git 仓库储存在/opt/git 目录下。只要把裸仓库复制过去:

1
$scp-r my_project.git user@ git.example.com:/opt/git

现在,所有对该服务器有 SSH 访问权限,并可读取 /opt/git 目录的用户都可以用下面的命令克隆该项目:

1
$ git clone user@ git.example.com:/opt/git/my_project.git

如果某个 SSH 用户对 /opt/git/my_project.git 目录有写权限,那他就有推送权限。如果到该项目目录中运行 git init 命令,并加上 –shared 选项,那么 Git 会自动修改该仓库目录的组权限为可写(译注:实际上 –shared 可以指定其他行为,只是默认为将组权限改为可写并执行 g+sx,所以最后会得到 rws。)。

1
2
3
$sshuser@ git.example.com
 $cd/opt/git/my_project.git
 $ git init --bare --shared

由此可见,根据现有的 Git 仓库创建一个裸仓库,然后把它放上你和同事都有 SSH 访问权的服务器是多么容易。现在已经可以开始在同一项目上密切合作了。

值得注意的是,这的的确确是架设一个少数人具有连接权的 Git 服务的全部 — 只要在服务器上加入可以用 SSH 登录的帐号,然后把裸仓库放在大家都有读写权限的地方。一切都准备停当,无需更多。

下面的几节中,你会了解如何扩展到更复杂的设定。这些内容包含如何避免为每一个用户建立一个账户,给仓库添加公共读取权限,架设网页界面,使用 Gitosis 工具等等。然而,只是和几个人在一个不公开的项目上合作的话,仅仅是一个 SSH 服务器和裸仓库就足够了,记住这点就可以了。

 

小型安装

如果设备较少或者你只想在小型开发团队里尝试 Git ,那么一切都很简单。架设 Git 服务最复杂的地方在于账户管理。如果需要仓库对特定的用户可读,而给另一部分用户读写权限,那么访问和许可的安排就比较困难。

 

SSH 连接

如果已经有了一个所有开发成员都可以用 SSH 访问的服务器,架设第一个服务器将变得异常简单,几乎什么都不用做(正如上节中介绍的那样)。如果需要对仓库进行更复杂的访问控制,只要使用服务器操作系统的本地文件访问许可机制就行了。

如果需要团队里的每个人都对仓库有写权限,又不能给每个人在服务器上建立账户,那么提供 SSH 连接就是唯一的选择了。我们假设用来共享仓库的服务器已经安装了 SSH 服务,而且你通过它访问服务器。

有好几个办法可以让团队的每个人都有访问权。第一个办法是给每个人建立一个账户,直截了当但略过繁琐。反复运行 adduser 并给所有人设定临时密码可不是好玩的。

第二个办法是在主机上建立一个 git 账户,让每个需要写权限的人发送一个 SSH 公钥,然后将其加入 git 账户的~/.ssh/authorized_keys 文件。这样一来,所有人都将通过 git 账户访问主机。这丝毫不会影响提交的数据 — 访问主机用的身份不会影响提交对象的提交者信息。

另一个办法是让 SSH 服务器通过某个 LDAP 服务,或者其他已经设定好的集中授权机制,来进行授权。只要每个人都能获得主机的 shell 访问权,任何可用的 SSH 授权机制都能达到相同效果。

 

4.3 生成 SSH 公钥

大多数 Git 服务器都会选择使用 SSH 公钥来进行授权。系统中的每个用户都必须提供一个公钥用于授权,没有的话就要生成一个。生成公钥的过程在所有操作系统上都差不多。首先先确认一下是否已经有一个公钥了。SSH 公钥默认储存在账户的主目录下的~/.ssh 目录。进去看看:

1
2
3
4
$cd~/.ssh
 $ls
 authorized_keys2 id_dsa known_hosts
 config id_dsa.pub

关键是看有没有用 something 和 something.pub 来命名的一对文件,这个 something 通常就是 id_dsa 或 id_rsa。有 .pub 后缀的文件就是公钥,另一个文件则是密钥。假如没有这些文件,或者干脆连.ssh 目录都没有,可以用 ssh-keygen 来创建。该程序在 Linux/Mac 系统上由 SSH 包提供,而在 Windows 上则包含在 MSysGit 包里:

1
2
3
4
5
6
7
8
9
$ssh-keygen
 Generating public/privatersa key pair.
 Enterfileinwhichto save the key (/Users/schacon/.ssh/id_rsa):
 Enter passphrase (empty forno passphrase):
 Enter same passphrase again:
 Your identification has been saved in/Users/schacon/.ssh/id_rsa.
 Your public key has been saved in/Users/schacon/.ssh/id_rsa.pub.
 The key fingerprint is:
 43:c5:5b:5f:b1:f1:50:43:ad:20:a6:92:6a:1f:9a:3a schacon@ agadorlaptop.local

它先要求你确认保存公钥的位置(.ssh/id_rsa),然后它会让你重复一个密码两次,如果不想在使用公钥的时候输入密码,可以留空。

现在,所有做过这一步的用户都得把它们的公钥给你或者 Git 服务器的管理员(假设 SSH 服务被设定为使用公钥机制)。他们只需要复制 .pub 文件的内容然后发邮件给管理员。公钥的样子大致如下:

1
2
3
4
5
6
7
$cat~/.ssh/id_rsa.pub
 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSU
 GPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3
 Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XA
 t3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/En
 mZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbx
 NrRFi9wrf+M7Q== schacon@ agadorlaptop.local

关于在多个操作系统上设立相同 SSH 公钥的教程,可以查阅 GitHub 上有关 SSH 公钥的向导:http://github.com/guides/providing-your-ssh-key。

 

4.4 架设服务器

现在我们过一边服务器端架设 SSH 访问的流程。本例将使用 authorized_keys 方法来给用户授权。我们还将假定使用类似 Ubuntu 这样的标准 Linux 发行版。首先,创建一个名为 ‘git’ 的用户,并为其创建一个.ssh 目录。

1
2
3
4
$sudoadduser git
 $sugit
 $cd
 $mkdir.ssh

接下来,把开发者的 SSH 公钥添加到这个用户的 authorized_keys 文件中。假设你通过电邮收到了几个公钥并存到了临时文件里。重复一下,公钥大致看起来是这个样子:

1
2
3
4
5
6
7
$cat/tmp/id_rsa.john.pub
 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCB007n/ww+ouN4gSLKssMxXnBOvf9LGt4L
 ojG6rs6hPB09j9R/T17/x4lhJA0F3FR1rP6kYBRsWj2aThGw6HXLm9/5zytK6Ztg3RPKK+4k
 Yjh6541NYsnEAZuXz0jTTyAUfrtU3Z5E003C4oxOj6H0rfIF1kKI9MAQLMdpGW1GYEIgS9Ez
 Sdfd8AcCIicTDWbqLAcU4UpkaX8KyGlLwsNuuGztobF8m72ALC/nLF6JLtPofwFBlgc+myiv
 O7TCUSBdLQlgMVOFq1I2uPWQOkOWQAHukEOmfjy2jctxSDBQ220ymjaNsHT4kgtZg2AYYgPq
 dAv8JggJICUvax2T9va5 gsg-keypair

只要把它们逐个追加到 authorized_keys 文件尾部即可:

1
2
3
$cat/tmp/id_rsa.john.pub >> ~/.ssh/authorized_keys
 $cat/tmp/id_rsa.josie.pub >> ~/.ssh/authorized_keys
 $cat/tmp/id_rsa.jessica.pub >> ~/.ssh/authorized_keys

现在可以用 –bare 选项运行 git init 来建立一个裸仓库,这会初始化一个不包含工作目录的仓库。

1
2
3
4
$cd/opt/git
 $mkdirproject.git
 $cdproject.git
 $ git --bare init

这时,Join,Josie 或者 Jessica 就可以把它加为远程仓库,推送一个分支,从而把第一个版本的项目文件上传到仓库里了。值得注意的是,每次添加一个新项目都需要通过 shell 登入主机并创建一个裸仓库目录。我们不妨以gitserver 作为 git 用户及项目仓库所在的主机名。如果在网络内部运行该主机,并在 DNS 中设定 gitserver 指向该主机,那么以下这些命令都是可用的:

# 在 John 的电脑上

1
2
3
4
5
6
$cdmyproject
 $ git init
 $ git add .
 $ git commit -m ‘initial commit‘
 $ git remote add origin git@gitserver:/opt/git/project.git
 $ git push origin master

这样,其他人的克隆和推送也一样变得很简单:

1
2
3
4
$ git clone git@gitserver:/opt/git/project.git
 $ vim README
 $ git commit -am ‘fix for the README file‘
 $ git push origin master

用这个方法可以很快捷地为少数几个开发者架设一个可读写的 Git 服务。

作为一个额外的防范措施,你可以用 Git 自带的 git-shell 工具限制 git 用户的活动范围。只要把它设为git 用户登入的 shell,那么该用户就无法使用普通的 bash 或者 csh 什么的 shell 程序。编辑 /etc/passwd 文件:

1
$sudovim/etc/passwd

在文件末尾,你应该能找到类似这样的行:

1
git:x:1000:1000::/home/git:/bin/sh

把 bin/sh 改为 /usr/bin/git-shell (或者用 which git-shell 查看它的实际安装路径)。该行修改后的样子如下:

1
git:x:1000:1000::/home/git:/usr/bin/git-shell

现在 git 用户只能用 SSH 连接来推送和获取 Git 仓库,而不能直接使用主机 shell。尝试普通 SSH 登录的话,会看到下面这样的拒绝信息:

1
2
3
$sshgit@gitserver
 fatal: What doyou think I am? A shell?
 Connection to gitserver closed.

 

4.5 公共访问

匿名的读取权限该怎么实现呢?也许除了内部私有的项目之外,你还需要托管一些开源项目。或者因为要用一些自动化的服务器来进行编译,或者有一些经常变化的服务器群组,而又不想整天生成新的 SSH 密钥 — 总之,你需要简单的匿名读取权限。

或许对小型的配置来说最简单的办法就是运行一个静态 web 服务,把它的根目录设定为 Git 仓库所在的位置,然后开启本章第一节提到的 post-update 挂钩。这里继续使用之前的例子。假设仓库处于/opt/git 目录,主机上运行着 Apache 服务。重申一下,任何 web 服务程序都可以达到相同效果;作为范例,我们将用一些基本的 Apache 设定来展示大体需要的步骤。

首先,开启挂钩:

1
2
3
$cdproject.git
 $mvhooks/post-update.sample hooks/post-update
 $chmoda+x hooks/post-update

如果用的是 Git 1.6 之前的版本,则可以省略 mv 命令 — Git 是从较晚的版本才开始在挂钩实例的结尾添加 .sample 后缀名的。

post-update 挂钩是做什么的呢?其内容大致如下:

1
2
3
$cat.git/hooks/post-update
 #!/bin/sh
 execgit-update-server-info

意思是当通过 SSH 向服务器推送时,Git 将运行这个 git-update-server-info 命令来更新匿名 HTTP 访问获取数据时所需要的文件。

接下来,在 Apache 配置文件中添加一个 VirtualHost 条目,把文档根目录设为 Git 项目所在的根目录。这里我们假定 DNS 服务已经配置好,会把对.gitserver 的请求发送到这台主机:

 

1
2
3
4
ServerName git.gitserver
 DocumentRoot/opt/git
Order allow, deny
 allow from all

 

另外,需要把 /opt/git 目录的 Unix 用户组设定为 www-data ,这样 web 服务才可以读取仓库内容,因为运行 CGI 脚本的 Apache 实例进程默认就是以该用户的身份起来的:

1
$chgrp-R www-data /opt/git

重启 Apache 之后,就可以通过项目的 URL 来克隆该目录下的仓库了。

1
$ git clone http://git.gitserver/project.git

这一招可以让你在几分钟内为相当数量的用户架设好基于 HTTP 的读取权限。另一个提供非授权访问的简单方法是开启一个 Git 守护进程,不过这将要求该进程作为后台进程常驻 — 接下来的这一节就要讨论这方面的细节。

 

4.6 GitWeb

现在我们的项目已经有了可读可写和只读的连接方式,不过如果能有一个简单的 web 界面访问就更好了。Git 自带一个叫做 GitWeb 的 CGI 脚本,运行效果可以到http://git.kernel.org 这样的站点体验下(见图 4-1)。

技术分享

Figure 4-1. 基于网页的 GitWeb 用户界面

如果想看看自己项目的效果,不妨用 Git 自带的一个命令,可以使用类似 lighttpd 或 webrick 这样轻量级的服务器启动一个临时进程。如果是在 Linux 主机上,通常都预装了lighttpd ,可以到项目目录中键入 git instaweb 来启动。如果用的是 Mac ,Leopard 预装了 Ruby,所以webrick 应该是最好的选择。如果要用 lighttpd 以外的程序来启动 git instaweb,可以通过–httpd 选项指定:

1
2
3
$ git instaweb --httpd=webrick
 [2009-02-21 10:02:21] INFO WEBrick 1.3.1
 [2009-02-21 10:02:21] INFO ruby 1.8.6 (2008-03-03) [universal-darwin9.0]

这会在 1234 端口开启一个 HTTPD 服务,随之在浏览器中显示该页,十分简单。关闭服务时,只需在原来的命令后面加上 –stop 选项就可以了:

1
$ git instaweb --httpd=webrick --stop

如果需要为团队或者某个开源项目长期运行 GitWeb,那么 CGI 脚本就要由正常的网页服务来运行。一些 Linux 发行版可以通过 apt 或yum 安装一个叫做 gitweb 的软件包,不妨首先尝试一下。我们将快速介绍一下手动安装 GitWeb 的流程。首先,你需要 Git 的源码,其中带有 GitWeb,并能生成定制的 CGI 脚本:

1
2
3
4
5
$ git clone git://git.kernel.org/pub/scm/git/git.git
 $cdgit/
 $makeGITWEB_PROJECTROOT="/opt/git"\
 prefix=/usrgitweb/gitweb.cgi
 $sudocp-Rf gitweb /var/www/

注意,通过指定 GITWEB_PROJECTROOT 变量告诉编译命令 Git 仓库的位置。然后,设置 Apache 以 CGI 方式运行该脚本,添加一个 VirtualHost 配置:

 

1
2
3
4
5
6
7
8
ServerName gitserver
 DocumentRoot/var/www/gitweb
Options ExecCGI +FollowSymLinks +SymLinksIfOwnerMatch
 AllowOverride All
 order allow,deny
 Allow from all
 AddHandler cgi-script cgi
 DirectoryIndex gitweb.cgi

 

不难想象,GitWeb 可以使用任何兼容 CGI 的网页服务来运行;如果偏向使用其他 web 服务器,配置也不会很麻烦。现在,通过 http://gitserver 就可以在线访问仓库了,在http://git.server 上还可以通过 HTTP 克隆和获取仓库的内容。

 

4.7 Gitosis

把所有用户的公钥保存在 authorized_keys 文件的做法,只能凑和一阵子,当用户数量达到几百人的规模时,管理起来就会十分痛苦。每次改删用户都必须登录服务器不去说,这种做法还缺少必要的权限管理 — 每个人都对所有项目拥有完整的读写权限。

幸好我们还可以选择应用广泛的 Gitosis 项目。简单地说,Gitosis 就是一套用来管理 authorized_keys 文件和实现简单连接限制的脚本。有趣的是,用来添加用户和设定权限的并非通过网页程序,而只是管理一个特殊的 Git 仓库。你只需要在这个特殊仓库内做好相应的设定,然后推送到服务器上,Gitosis 就会随之改变运行策略,听起来就很酷,对吧?

Gitosis 的安装算不上傻瓜化,但也不算太难。用 Linux 服务器架设起来最简单 — 以下例子中,我们使用装有 Ubuntu 8.10 系统的服务器。

Gitosis 的工作依赖于某些 Python 工具,所以首先要安装 Python 的 setuptools 包,在 Ubuntu 上称为 python-setuptools:

1
$ apt-get installpython-setuptools

接下来,从 Gitosis 项目主页克隆并安装:

1
2
3
$ git clone git://eagain.net/gitosis.git
 $cdgitosis
 $sudopython setup.py install

这会安装几个供 Gitosis 使用的工具。默认 Gitosis 会把 /home/git 作为存储所有 Git 仓库的根目录,这没什么不好,不过我们之前已经把项目仓库都放在/opt/git 里面了,所以为方便起见,我们可以做一个符号连接,直接划转过去,而不必重新配置:

1
$ln-s/opt/git/home/git/repositories

Gitosis 将会帮我们管理用户公钥,所以先把当前控制文件改名备份,以便稍后重新添加,准备好让 Gitosis 自动管理 authorized_keys 文件:

1
$mv/home/git/.ssh/authorized_keys/home/git/.ssh/ak.bak

接下来,如果之前把 git 用户的登录 shell 改为 git-shell 命令的话,先恢复 ‘git’ 用户的登录 shell。改过之后,大家仍然无法通过该帐号登录(译注:因为authorized_keys 文件已经没有了。),不过不用担心,这会交给 Gitosis 来实现。所以现在先打开 /etc/passwd 文件,把这行:

1
git:x:1000:1000::/home/git:/usr/bin/git-shell

改回:

1
git:x:1000:1000::/home/git:/bin/sh

好了,现在可以初始化 Gitosis 了。你可以用自己的公钥执行 gitosis-init 命令,要是公钥不在服务器上,先临时复制一份:

1
2
3
$sudo-H -u git gitosis-init < /tmp/id_dsa.pub
Initialized empty Git repository in/opt/git/gitosis-admin.git/
Reinitialized existing Git repository in/opt/git/gitosis-admin.git/

这样该公钥的拥有者就能修改用于配置 Gitosis 的那个特殊 Git 仓库了。接下来,需要手工对该仓库中的 post-update 脚本加上可执行权限:

1
$sudochmod755/opt/git/gitosis-admin.git/hooks/post-update

基本上就算是好了。如果设定过程没出什么差错,现在可以试一下用初始化 Gitosis 的公钥的拥有者身份 SSH 登录服务器,应该会看到类似下面这样:

1
2
3
4
$sshgit@gitserver
PTY allocation request failed on channel 0
fatal: unrecognized command‘gitosis-serve schacon@quaternion‘
  Connection to gitserver closed.

说明 Gitosis 认出了该用户的身份,但由于没有运行任何 Git 命令,所以它切断了连接。那么,现在运行一个实际的 Git 命令 — 克隆 Gitosis 的控制仓库:


# 在你本地计算机上
$ git clone git@gitserver:gitosis-admin.git

这会得到一个名为 gitosis-admin 的工作目录,主要由两部分组成:


$ cd gitosis-admin
$ find .
 ./gitosis.conf
./keydir
./keydir/scott.pub

gitosis.conf 文件是用来设置用户、仓库和权限的控制文件。keydir 目录则是保存所有具有访问权限用户公钥的地方— 每人一个。在keydir 里的文件名(比如上面的 scott.pub)应该跟你的不一样 — Gitosis 会自动从使用 gitosis-init 脚本导入的公钥尾部的描述中获取该名字。

看一下 gitosis.conf 文件的内容,它应该只包含与刚刚克隆的 gitosis-admin 相关的信息:


$ cat gitosis.conf
[gitosis]
[group gitosis-admin]
writable = gitosis-admin
members = scott

它显示用户 scott — 初始化 Gitosis 公钥的拥有者 — 是唯一能管理 gitosis-admin 项目的人。

现在我们来添加一个新项目。为此我们要建立一个名为 mobile 的新段落,在其中罗列手机开发团队的开发者,以及他们拥有写权限的项目。由于 ‘scott’ 是系统中的唯一用户,我们把他设为唯一用户,并允许他读写名为iphone_project 的新项目:



[group mobile]
writable = iphone_project
members = scott


修改完之后,提交 gitosis-admin 里的改动,并推送到服务器使其生效:

1
2
3
4
5
6
7
8
9
10
$ git commit -am ‘add iphone_project and mobile group‘
[master]: created 8962da8: "changed name"
 1 files changed, 4 insertions(+), 0 deletions(-)
$ git push
Counting objects: 5, done.
Compressing objects: 100% (2/2),done.
Writing objects: 100% (3/3), 272 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To git@gitserver:/opt/git/gitosis-admin.git
   fb27aec..8962da8  master -> master

在新工程 iphone_project 里首次推送数据到服务器前,得先设定该服务器地址为远程仓库。但你不用事先到服务器上手工创建该项目的裸仓库— Gitosis 会在第一次遇到推送时自动创建:

1
2
3
4
5
6
7
8
$ git remote add origin git@gitserver:iphone_project.git
 $ git push origin master
 Initialized empty Git repository in/opt/git/iphone_project.git/
 Counting objects: 3, done.
 Writing objects: 100% (3/3), 230 bytes, done.
 Total 3 (delta 0), reused 0 (delta 0)
 To git@gitserver:iphone_project.git
 * [new branch] master -> master

请注意,这里不用指明完整路径(实际上,如果加上反而没用),只需要一个冒号加项目名字即可 — Gitosis 会自动帮你映射到实际位置。

要和朋友们在一个项目上协同工作,就得重新添加他们的公钥。不过这次不用在服务器上一个一个手工添加到 ~/.ssh/authorized_keys 文件末端,而只需管理keydir 目录中的公钥文件。文件的命名将决定在 gitosis.conf 中对用户的标识。现在我们为 John,Josie 和 Jessica 添加公钥:

1
2
3
$cp/tmp/id_rsa.john.pub keydir/john.pub
 $cp/tmp/id_rsa.josie.pub keydir/josie.pub
 $cp/tmp/id_rsa.jessica.pub keydir/jessica.pub

然后把他们都加进 ‘mobile’ 团队,让他们对 iphone_project 具有读写权限:

1
2
3
[group mobile]
 writable = iphone_project
 members = scott john josie jessica

如果你提交并推送这个修改,四个用户将同时具有该项目的读写权限。

Gitosis 也具有简单的访问控制功能。如果想让 John 只有读权限,可以这样做:

1
2
3
4
5
6
[group mobile]
 writable = iphone_project
 members = scott josie jessica
[group mobile_ro]
 readonly= iphone_project
 members = john

现在 John 可以克隆和获取更新,但 Gitosis 不会允许他向项目推送任何内容。像这样的组可以随意创建,多少不限,每个都可以包含若干不同的用户和项目。甚至还可以指定某个组为成员之一(在组名前加上@ 前缀),自动继承该组的成员:

1
2
3
4
5
6
7
8
[group mobile_committers]
 members = scott josie jessica
[group mobile]
 writable = iphone_project
 members = @mobile_committers
[group mobile_2]
 writable = another_iphone_project
 members = @mobile_committers john

如果遇到意外问题,试试看把 loglevel=DEBUG 加到 [gitosis] 的段落(译注:把日志设置为调试级别,记录更详细的运行信息。)。如果一不小心搞错了配置,失去了推送权限,也可以手工修改服务器上的/home/git/.gitosis.conf 文件 — Gitosis 实际是从该文件读取信息的。它在得到推送数据时,会把新的 gitosis.conf 存到该路径上。所以如果你手工编辑该文件的话,它会一直保持到下次向 gitosis-admin 推送新版本的配置内容为止。

 

4.8 Gitolite

Note: the latest copy of this section of the ProGit book is always available within thegitolite documentation. The author would also like to humbly state that, while this section is accurate, andcan (and often has) been used to install gitolite without reading any other documentation, it is of necessity not complete, and cannot completely replace the enormous amount of documentation that gitolite comes with.

Git has started to become very popular in corporate environments, which tend to have some additional requirements in terms of access control. Gitolite was originally created to help with those requirements, but it turns out that it’s equally useful in the open source world: the Fedora Project controls access to their package management repositories (over 10,000 of them!) using gitolite, and this is probably the largest gitolite installation anywhere too.

Gitolite allows you to specify permissions not just by repository, but also by branch or tag names within each repository. That is, you can specify that certain people (or groups of people) can only push certain “refs” (branches or tags) but not others.

Installing

Installing Gitolite is very easy, even if you don’t read the extensive documentation that comes with it. You need an account on a Unix server of some kind; various Linux flavours, and Solaris 10, have been tested. You do not need root access, assuming git, perl, and an openssh compatible ssh server are already installed. In the examples below, we will use thegitolite account on a host called gitserver.

Gitolite is somewhat unusual as far as “server” software goes – access is via ssh, and so every userid on the server is a potential “gitolite host”. As a result, there is a notion of “installing” the software itself, and then “setting up” a user as a “gitolite host”.

Gitolite has 4 methods of installation. People using Fedora or Debian systems can obtain an RPM or a DEB and install that. People with root access can install it manually. In these two methods, any user on the system can then become a “gitolite host”.

People without root access can install it within their own userids. And finally, gitolite can be installed by running a scripton the workstation, from a bash shell. (Even the bash that comes with msysgit will do, in case you’re wondering.)

We will describe this last method in this article; for the other methods please see the documentation.

You start by obtaining public key based access to your server, so that you can log in from your workstation to the server without getting a password prompt. The following method works on Linux; for other workstation OSs you may have to do this manually. We assume you already had a key pair generated using ssh-keygen.

1
$ssh-copy-id-i ~/.ssh/id_rsagitolite@gitserver

This will ask you for the password to the gitolite account, and then set up public key access. This isessential for the install script, so check to make sure you can run a command without getting a password prompt:

1
2
$sshgitolite@gitserverpwd
 /home/gitolite

Next, you clone Gitolite from the project’s main site and run the “easy install” script (the third argument is your name as you would like it to appear in the resulting gitolite-admin repository):

1
2
3
$ git clone git://github.com/sitaramc/gitolite
 $cdgitolite/src
 $ ./gl-easy-install-q gitolite gitserver sitaram

And you’re done! Gitolite has now been installed on the server, and you now have a brand new repository calledgitolite-admin in the home directory of your workstation. You administer your gitolite setup by making changes to this repository and pushing.

That last command does produce a fair amount of output, which might be interesting to read. Also, the first time you run this, a new keypair is created; you will have to choose a passphrase or hit enter for none. Why a second keypair is needed, and how it is used, is explained in the “ssh troubleshooting” document that comes with Gitolite. (Hey the documentation has to be good forsomething!)

Repos named gitolite-admin and testing are created on the server by default. If you wish to clone either of these locally (from an account that has SSH console access to the gitolite account viaauthorized_keys), type:

1
2
$ git clone gitolite:gitolite-admin
 $ git clone gitolite:testing

To clone these same repos from any other account:

1
2
$ git clone gitolite@servername:gitolite-admin
 $ git clone gitolite@servername:testing

Customising the Install

While the default, quick, install works for most people, there are some ways to customise the install if you need to. If you omit the-q argument, you get a “verbose” mode install – detailed information on what the install is doing at each step. The verbose mode also allows you to change certain server-side parameters, such as the location of the actual repositories, by editing an “rc” file that the server uses. This “rc” file is liberally commented so you should be able to make any changes you need quite easily, save it, and continue. This file also contains various settings that you can change to enable or disable some of gitolite’s advanced features.

Config File and Access Control Rules
Once the install is done, you switch to the gitolite-admin repository (placed in your HOME directory) and poke around to see what you got:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
$cd~/gitolite-admin/
 $ls
 conf/ keydir/
 $findconf keydir -typef
 conf/gitolite.conf
 keydir/sitaram.pub
 $catconf/gitolite.conf
 #gitolite conf
 # please see conf/example.conf for details on syntax and features
repo gitolite-admin
 RW+ = sitaram
repo testing
 RW+ = @all

Notice that “sitaram” (the last argument in the gl-easy-install command you gave earlier) has read-write permissions on thegitolite-admin repository as well as a public key file of the same name.

The config file syntax for gitolite is liberally documented in conf/example.conf, so we’ll only mention some highlights here.

You can group users or repos for convenience. The group names are just like macros; when defining them, it doesn’t even matter whether they are projects or users; that distinction is only made when youuse the “macro”.

 

1
2
3
4
5
6
@oss_repos = linux perl rakudo git gitolite
 @secret_repos = fenestra pear
@admins = scott # Adams, not Chacon, sorry :)
 @interns = ashok # get the spelling right, Scott!
 @engineers = sitaram dilbert wally alice
 @staff = @admins @engineers @interns

You can control permissions at the “ref” level. In the following example, interns can only push the “int” branch. Engineers can push any branch whose name starts with “eng-“, and tags that start with “rc” followed by a digit. And the admins can do anything (including rewind) to any ref.

1
2
3
4
5
repo @oss_repos
 RW int$ = @interns
 RW eng- = @engineers
 RW refs/tags/rc[0-9] = @engineers
 RW+ = @admins

The expression after the RW or RW+ is a regular expression (regex) that the refname (ref) being pushed is matched against. So we call it a “refex”! Of course, a refex can be far more powerful than shown here, so don’t overdo it if you’re not comfortable with perl regexes.

Also, as you probably guessed, Gitolite prefixes refs/heads/ as a syntactic convenience if the refex does not begin withrefs/.

An important feature of the config file’s syntax is that all the rules for a repository need not be in one place. You can keep all the common stuff together, like the rules for alloss_repos shown above, then add specific rules for specific cases later on, like so:

1
2
repo gitolite
 RW+ = sitaram

That rule will just get added to the ruleset for the gitolite repository.

At this point you might be wondering how the access control rules are actually applied, so let’s go over that briefly.

There are two levels of access control in gitolite. The first is at the repository level; if you have read (or write) access toany ref in the repository, then you have read (or write) access to the repository.

The second level, applicable only to “write” access, is by branch or tag within a repository. The username, the access being attempted (W or+), and the refname being updated are known. The access rules are checked in order of appearance in the config file, looking for a match for this combination (but remember that the refname is regex-matched, not merely string-matched). If a match is found, the push succeeds. A fallthrough results in access being denied.

Advanced Access Control with “deny” rules

So far, we’ve only seen permissions to be one or R, RW, orRW+. However, gitolite allows another permission: -, standing for “deny”. This gives you a lot more power, at the expense of some complexity, because now fallthrough is not theonly way for access to be denied, so the order of the rules now matters!

Let us say, in the situation above, we want engineers to be able to rewind any branchexcept master and integ. Here’s how to do that:

1
2
3
RW master integ = @engineers
 - master integ = @engineers
 RW+ = @engineers

Again, you simply follow the rules top down until you hit a match for your access mode, or a deny. Non-rewind push to master or integ is allowed by the first rule. A rewind push to those refs does not match the first rule, drops down to the second, and is therefore denied. Any push (rewind or non-rewind) to refs other than master or integ won’t match the first two rules anyway, and the third rule allows it.

Restricting pushes by files changed

In addition to restricting what branches a user can push changes to, you can also restrict what files they are allowed to touch. For example, perhaps the Makefile (or some other program) is really not supposed to be changed by just anyone, because a lot of things depend on it or would break if the changes are not done just right. You can tell gitolite:

 

1
2
3
4
5
repo foo
 RW = @junior_devs @senior_devs
RW NAME/ = @senior_devs
 - NAME/Makefile= @junior_devs
 RW NAME/ = @junior_devs

This powerful feature is documented in conf/example.conf.

Personal Branches

Gitolite also has a feature called “personal branches” (or rather, “personal branch namespace”) that can be very useful in a corporate environment.

A lot of code exchange in the git world happens by “please pull” requests. In a corporate environment, however, unauthenticated access is a no-no, and a developer workstation cannot do authentication, so you have to push to the central server and ask someone to pull from there.

This would normally cause the same branch name clutter as in a centralised VCS, plus setting up permissions for this becomes a chore for the admin.

Gitolite lets you define a “personal” or “scratch” namespace prefix for each developer (for example,refs/personal/ /* ); see the “personal branches” section in doc/3-faq-tips-etc.mkd for details.

“Wildcard” repositories

Gitolite allows you to specify repositories with wildcards (actually perl regexes), like, for exampleassignments/s[0-9][0-9]/a[0-9][0-9], to pick a random example. This is avery powerful feature, which has to be enabled by setting $GL_WILDREPOS = 1; in the rc file. It allows you to assign a new permission mode (”C”) which allows users to create repositories based on such wild cards, automatically assigns ownership to the specific user who created it, allows him/her to hand out R and RW permissions to other users to collaborate, etc. This feature is documented indoc/4-wildcard-repositories.mkd.

Other Features

We’ll round off this discussion with a sampling of other features, all of which, and many more, are described in great detail in the “faqs, tips, etc” and other documents.

Logging: Gitolite logs all successful accesses. If you were somewhat relaxed about giving people rewind permissions (RW+) and some kid blew away “master”, the log file is a life saver, in terms of easily and quickly finding the SHA that got hosed.

Git outside normal PATH: One extremely useful convenience feature in gitolite is support for git installed outside the normal$PATH (this is more common than you think; some corporate environments or even some hosting providers refuse to install things system-wide and you end up putting them in your own directories). Normally, you are forced to make theclient-side git aware of this non-standard location of the git binaries in some way. With gitolite, just choose a verbose install and set$GIT_PATH in the “rc” files. No client-side changes are required after that :-)

Access rights reporting: Another convenient feature is what happens when you try and just ssh to the server. Gitolite shows you what repos you have access to, and what that access may be. Here’s an example:

1
2
3
4
5
6
7
8
9
hello sitaram, the gitolite version here is v1.5.4-19-ga3397d4
 the gitolite config gives you the following access:
 R anu-wsd
 R entrans
 R W git-notes
 R W gitolite
 R W gitolite-admin
 R indic_web_input
 R shreelipi_converter

Delegation: For really large installations, you can delegate responsibility for groups of repositories to various people and have them manage those pieces independently. This reduces the load on the main admin, and makes him less of a bottleneck. This feature has its own documentation file in the doc/ directory.

Gitweb support: Gitolite supports gitweb in several ways. You can specify which repos are visible via gitweb. You can set the “owner” and “description” for gitweb from the gitolite config file. Gitweb has a mechanism for you to implement access control based on HTTP authentication, so you can make it use the “compiled” config file that gitolite produces, which means the same access control rules (for read access) apply for gitweb and gitolite.

Mirroring: Gitolite can help you maintain multiple mirrors, and switch between them easily if the primary server goes down.

 

4.9 Git 守护进程

对于提供公共的,非授权的只读访问,我们可以抛弃 HTTP 协议,改用 Git 自己的协议,这主要是出于性能和速度的考虑。Git 协议远比 HTTP 协议高效,因而访问速度也快,所以它能节省很多用户的时间。

重申一下,这一点只适用于非授权的只读访问。如果建在防火墙之外的服务器上,那么它所提供的服务应该只是那些公开的只读项目。如果是在防火墙之内的 服务器上,可用于支撑大量参与人员或自动系统(用于持续集成或编译的主机)只读访问的项目,这样可以省去逐一配置 SSH 公钥的麻烦。

但不管哪种情形,Git 协议的配置设定都很简单。基本上,只要以守护进程的形式运行该命令即可:

1
git daemon --reuseaddr --base-path=/opt/git//opt/git/

这里的 –reuseaddr 选项表示在重启服务前,不等之前的连接超时就立即重启。而 –base-path 选项则允许克隆项目时不必给出完整路径。最后面的路径告诉 Git 守护进程允许开放给用户访问的仓库目录。假如有防火墙,则需要为该主机的 9418 端口设置为允许通信。

以守护进程的形式运行该进程的方法有很多,但主要还得看用的是什么操作系统。在 Ubuntu 主机上,可以用 Upstart 脚本达成。编辑该文件:

1
/etc/event.d/local-git-daemon

加入以下内容:

1
2
3
4
5
6
7
8
start on startup
 stop on shutdown
 exec/usr/bin/gitdaemon \
 --user=git --group=git \
 --reuseaddr \
 --base-path=/opt/git/\
 /opt/git/
 respawn

出于安全考虑,强烈建议用一个对仓库只有读取权限的用户身份来运行该进程 — 只需要简单地新建一个名为 git-ro 的用户(译注:新建用户默认对仓库文件不具备写权限,但这取决于仓库目录的权限设定。务必确认git-ro 对仓库只能读不能写。),并用它的身份来启动进程。这里为了简化,后面我们还是用之前运行 Gitosis 的用户 ‘git’。

这样一来,当你重启计算机时,Git 进程也会自动启动。要是进程意外退出或者被杀掉,也会自行重启。在设置完成后,不重启计算机就启动该守护进程,可以运行:

1
initctl start local-git-daemon

而在其他操作系统上,可以用 xinetd,或者 sysvinit 系统的脚本,或者其他类似的脚本 — 只要能让那个命令变为守护进程并可监控。

接下来,我们必须告诉 Gitosis 哪些仓库允许通过 Git 协议进行匿名只读访问。如果每个仓库都设有各自的段落,可以分别指定是否允许 Git 进程开放给用户匿名读取。比如允许通过 Git 协议访问 iphone_project,可以把下面两行加到gitosis.conf 文件的末尾:

1
2
[repo iphone_project]
 daemon = yes

在提交和推送完成后,运行中的 Git 守护进程就会响应来自 9418 端口对该项目的访问请求。

如果不考虑 Gitosis,单单起了 Git 守护进程的话,就必须到每一个允许匿名只读访问的仓库目录内,创建一个特殊名称的空文件作为标志:

1
2
$cd/path/to/project.git
 $touchgit-daemon-export-ok

该文件的存在,表明允许 Git 守护进程开放对该项目的匿名只读访问。

Gitosis 还能设定哪些项目允许放在 GitWeb 上显示。先打开 GitWeb 的配置文件 /etc/gitweb.conf,添加以下四行:

1
2
3
4
$projects_list = "/home/git/gitosis/projects.list";
 $projectroot = "/home/git/repositories";
 $export_ok = "git-daemon-export-ok";
 @git_base_url_list = (‘git://gitserver‘);

接下来,只要配置各个项目在 Gitosis 中的 gitweb 参数,便能达成是否允许 GitWeb 用户浏览该项目。比如,要让 iphone_project 项目在 GitWeb 里出现,把repo 的设定改成下面的样子:

1
2
3
[repo iphone_project]
 daemon = yes
 gitweb = yes

在提交并推送过之后,GitWeb 就会自动开始显示 iphone_project 项目的细节和历史。

 

4.10 Git 托管服务

如果不想经历自己架设 Git 服务器的麻烦,网络上有几个专业的仓库托管服务可供选择。这样做有几大优点:托管账户的建立通常比较省时,方便项目的启动,而且不涉及服务器的维护和监 控。即使内部创建并运行着自己的服务器,同时为开源项目提供一个公共托管站点还是有好处的 — 让开源社区更方便地找到该项目,并给予帮助。

目前,可供选择的托管服务数量繁多,各有利弊。在 Git 官方 wiki 上的 Githosting 页面有一个最新的托管服务列表:

1
http://git.or.cz/gitwiki/GitHosting

由于本书无法全部一一介绍,而本人(译注:指本书作者 Scott Chacon。)刚好在其中一家公司工作,所以接下来我们将会介绍如何在 GitHub 上建立新账户并启动项目。至于其他托管服务大体也是这么一个过程,基本的想法都是差不多的。

GitHub 是目前为止最大的开源 Git 托管服务,并且还是少数同时提供公共代码和私有代码托管服务的站点之一,所以你可以在上面同时保存开源和商业代码。事实上,本书就是放在 GitHub 上合作编著的。(译注:本书的翻译也是放在 GitHub 上广泛协作的。)

 

GitHub

GitHub 和大多数的代码托管站点在处理项目命名空间的方式上略有不同。GitHub 的设计更侧重于用户,而不是完全基于项目。也就是说,如果我在 GitHub 上托管一个名为grit 的项目的话,它的地址不会是 github.com/grit,而是按在用户底下 github.com/shacon/grit (译注:本书作者 Scott Chacon 在 GitHub 上的用户名是shacon。)。不存在所谓某个项目的官方版本,所以假如第一作者放弃了某个项目,它可以无缝转移到其它用户的名下。

GitHub 同时也是一个向使用私有仓库的用户收取费用的商业公司,但任何人都可以方便快捷地申请到一个免费账户,并在上面托管数量不限的开源项目。接下来我们快速介绍一下 GitHub 的基本使用。

 

建立新账户

首先注册一个免费账户。访问 Pricing and Signup 页面 http://github.com/plans 并点击 Free acount 里的 Sign Up 按钮(见图 4-2),进入注册页面。

技术分享

图 4-2. GitHub 服务简介页面

选择一个系统中尚未使用的用户名,提供一个与之相关联的电邮地址,并输入密码(见图 4-3):

技术分享

图 4-3. GitHub 用户注册表单

如果方便,现在就可以提供你的 SSH 公钥。我们在前文的”小型安装” 一节介绍过生成新公钥的方法。把新生成的公钥复制粘贴到 SSH Public Key 文本框中即可。要是对生成公钥的步骤不太清楚,也可以点击 “explain ssh keys” 链接,会显示各个主流操作系统上完成该步骤的介绍。点击 “I agree,sign me up” 按钮完成用户注册,并转到该用户的 dashboard 页面(见图 4-4):

技术分享

图 4-4. GitHub 的用户面板

接下来就可以建立新仓库了。

 

建立新仓库

点击用户面板上仓库旁边的 “create a new one” 链接,显示 Create a New Repository 的表单(见图 4-5):

技术分享

图 4-5. 在 GitHub 上建立新仓库

当然,项目名称是必不可少的,此外也可以适当描述一下项目的情况或者给出官方站点的地址。然后点击 “Create Repository” 按钮,新仓库就建立起来了(见图 4-6):

技术分享

图 4-6. GitHub 上各个项目的概要信息

由于尚未提交代码,点击项目地址后 GitHub 会显示一个简要的指南,告诉你如何新建一个项目并推送上来,如何从现有项目推送,以及如何从一个公共的 Subversion 仓库导入项目(见图 4-7):

技术分享

图 4-7. 新仓库指南

该指南和本书前文介绍的类似,对于新的项目,需要先在本地初始化为 Git 项目,添加要管理的文件并作首次提交:

1
2
3
$ git init
 $ git add .
 $ git commit -m ‘initial commit‘

然后在这个本地仓库内把 GitHub 添加为远程仓库,并推送 master 分支上来:

1
2
$ git remote add origin git@ github.com:testinguser/iphone_project.git
 $ git push origin master

现在该项目就托管在 GitHub 上了。你可以把它的 URL 分享给每位对此项目感兴趣的人。本例的 URL 是 http://github.com/testinguser/iphone_project。而在项目页面的摘要部分,你会发现有两个 Git URL 地址(见图 4-8):

技术分享

图 4-8. 项目摘要中的公共 URL 和私有 URL

Public Clone URL 是一个公开的,只读的 Git URL,任何人都可以通过它克隆该项目。可以随意散播这个 URL,比如发布到个人网站之类的地方等等。

Your Clone URL 是一个基于 SSH 协议的可读可写 URL,只有使用与上传的 SSH 公钥对应的密钥来连接时,才能通过它进行读写操作。其他用户访问该项目页面时只能看到之前那个公共的 URL,看不到这个私有的 URL。

 

从 Subversion 导入项目

如果想把某个公共 Subversion 项目导入 Git,GitHub 可以帮忙。在指南的最后有一个指向导入 Subversion 页面的链接。点击它会看到一个表单,包含有关导入流程的信息以及一个用来粘贴公共 Subversion 项目连接的文本框(见图 4-9):

技术分享

图 4-9. Subversion 导入界面

如果项目很大,采用非标准结构,或者是私有的,那就无法借助该工具实现导入。到第 7 章,我们会介绍如何手工导入复杂工程的具体方法。

 

添加协作开发者

现在把团队里的其他人也加进来。如果 John,Josie 和 Jessica 都在 GitHub 注册了账户,要赋予他们对该仓库的推送权限,可以把他们加为项目协作者。这样他们就可以通过各自的公钥访问我的这个仓库了。

点击项目页面上方的 “edit” 按钮或者顶部的 Admin 标签,进入该项目的管理页面(见图 4-10):

技术分享

图 4-10. GitHub 的项目管理页面

为了给另一个用户添加项目的写权限,点击 “Add another collaborator” 链接,出现一个用于输入用户名的表单。在输入的同时,它会自动跳出一个符合条件的候选名单。找到正确用户名之后,点 Add 按钮,把该用户设为项目协作者(见图 4-11):

技术分享

图 4-11. 为项目添加协作者

添加完协作者之后,就可以在 Repository Collaborators 区域看到他们的名单(见图 4-12):

技术分享

图 4-12. 项目协作者名单

如果要取消某人的访问权,点击 “revoke” 即可取消他的推送权限。对于将来的项目,你可以从现有项目复制协作者名单,或者直接借用协作者群组。

 

项目页面

在推送或从 Subversion 导入项目之后,你会看到一个类似图 4-13 的项目主页:

技术分享

图 4-13. GitHub 上的项目主页

别人访问你的项目时看到的就是这个页面。它有若干导航标签,Commits 标签用于显示提交历史,最新的提交位于最上方,这和 git log 命令的输出类似。Network 标签展示所有派生了该项目并做出贡献的用户的关系图谱。Downloads 标签允许你上传项目的二进制文件,提供下载该项目各个版本的 tar/zip 包。Wiki 标签提供了一个用于撰写文档或其他项目相关信息的 wiki 站点。Graphs 标签包含了一些可视化的项目信息与数据。默认打开的 Source 标签页面,则列出了该项目的目录结构和概要信息,并在下方自动展示 README 文件的内容(如果该文件存在的话),此外还会显示最近一次提交的相关信息。

 

派生项目

如果要为一个自己没有推送权限的项目贡献代码,GitHub 鼓励使用派生(fork)。到那个感兴趣的项目主页上,点击页面上方的 “fork” 按钮,GitHub 就会为你复制一份该项目的副本到你的仓库中,这样你就可以向自己的这个副本推送数据了。

采取这种办法的好处是,项目拥有者不必忙于应付赋予他人推送权限的工作。随便谁都可以通过派生得到一个项目副本并在其中展开工作,事后只需要项目维护者将这些副本仓库加为远程仓库,然后提取更新合并即可。

要派生一个项目,到原始项目的页面(本例中是 mojombo/chronic)点击 “fork” 按钮(见图 4-14):

技术分享

图 4-14. 点击 “fork” 按钮获得任意项目的可写副本

几秒钟之后,你将进入新建的项目页面,会显示该项目派生自哪一个项目(见图 4-15):

技术分享

图 4-15. 派生后得到的项目副本

 

GitHub 小结

关于 GitHub 就先介绍这么多,能够快速达成这些事情非常重要(译注:门槛的降低和完成基本任务的简单高效,对于推动开源项目的协作发展有着举足轻重的意义。)。短短几 分钟内,你就能创建一个新账户,添加一个项目并开始推送。如果项目是开源的,整个庞大的开发者社区都可以立即访问它,提供各式各样的帮助和贡献。最起码, 这也是一种 Git 新手立即体验尝试 Git 的捷径。

 

4.11 小结

我们讨论并介绍了一些建立远程 Git 仓库的方法,接下来你可以通过这些仓库同他人分享或合作。

运行自己的服务器意味着更多的控制权以及在防火墙内部操作的可能性,当然这样的服务器通常需要投入一定的时间精力来架设维护。如果直接托管,虽然能免去这部分工作,但有时出于安全或版权的考虑,有些公司禁止将商业代码托管到第三方服务商。

所以究竟采取哪种方案,并不是个难以取舍的问题,或者其一,或者相互配合,哪种合适就用哪种。

Git详解之五:分布式Git

为了便于项目中的所有开发者分享代码,我们准备好了一台服务器存放远程 Git 仓库。经过前面几章的学习,我们已经学会了一些基本的本地工作流程中所需用到的命令。接下来,我们要学习下如何利用 Git 来组织和完成分布式工作流程。(伯乐在线注:如果你对Git还不了解,建议从本Git系列第一篇文章开始阅读)

特别是,当作为项目贡献者时,我们该怎么做才能方便维护者采纳更新;或者作为项目维护者时,又该怎样有效管理大量贡献者的提交。

5.1  分布式工作流程

同传统的集中式版本控制系统(CVCS)不同,开发者之间的协作方式因着 Git 的分布式特性而变得更为灵活多样。在集中式系统上,每个开发者就像是连接在集线器上的节点,彼此的工作方式大体相像。而在 Git 网络中,每个开发者同时扮演着节点和集线器的角色,这就是说,每一个开发者都可以将自己的代码贡献到另外一个开发者的仓库中,或者建立自己的公共仓库,让 其他开发者基于自己的工作开始,为自己的仓库贡献代码。于是,Git 的分布式协作便可以衍生出种种不同的工作流程,我会在接下来的章节介绍几种常见的应用方式,并分别讨论各自的优缺点。你可以选择其中的一种,或者结合起 来,应用到你自己的项目中。

 

集中式工作流

通常,集中式工作流程使用的都是单点协作模型。一个存放代码仓库的中心服务器,可以接受所有开发者提交的代码。所有的开发者都是普通的节点,作为中心集线器的消费者,平时的工作就是和中心仓库同步数据(见图 5-1)。

技术分享

图 5-1. 集中式工作流

如果两个开发者从中心仓库克隆代码下来,同时作了一些修订,那么只有第一个开发者可以顺利地把数据推送到共享服务器。第二个开发者在提交他的修订之 前,必须先下载合并服务器上的数据,解决冲突之后才能推送数据到共享服务器上。在 Git 中这么用也决无问题,这就好比是在用 Subversion(或其他 CVCS)一样,可以很好地工作。

如果你的团队不是很大,或者大家都已经习惯了使用集中式工作流程,完全可以采用这种简单的模式。只需要配置好一台中心服务器,并给每个人推送数据的 权限,就可以开展工作了。但如果提交代码时有冲突, Git 根本就不会让用户覆盖他人代码,它直接驳回第二个人的提交操作。这就等于告诉提交者,你所作的修订无法通过快近(fast-forward)来合并,你必 须先拉取最新数据下来,手工解决冲突合并后,才能继续推送新的提交。绝大多数人都熟悉和了解这种模式的工作方式,所以使用也非常广泛。

集成管理员工作流

由于 Git 允许使用多个远程仓库,开发者便可以建立自己的公共仓库,往里面写数据并共享给他人,而同时又可以从别人的仓库中提取他们的更新过来。这种情形通常都会有 个代表着官方发布的项目仓库(blessed repository),开发者们由此仓库克隆出一个自己的公共仓库(developer public),然后将自己的提交推送上去,请求官方仓库的维护者拉取更新合并到主项目。维护者在自己的本地也有个克隆仓库(integration manager),他可以将你的公共仓库作为远程仓库添加进来,经过测试无误后合并到主干分支,然后再推送到官方仓库。工作流程看起来就像图 5-2 所示:

  1. 项目维护者可以推送数据到公共仓库 blessed repository。 2. 贡献者克隆此仓库,修订或编写新代码。
  2. 贡献者推送数据到自己的公共仓库 developer public。 4. 贡献者给维护者发送邮件,请求拉取自己的最新修订。
  3. 维护者在自己本地的 integration manger 仓库中,将贡献者的仓库加为远程仓库,合并更新并做测试。
  4. 维护者将合并后的更新推送到主仓库 blessed repository。

技术分享

图 5-2. 集成管理员工作流

在 GitHub 网站上使用得最多的就是这种工作流。人们可以复制(fork 亦即克隆)某个项目到自己的列表中,成为自己的公共仓库。随后将自己的更新提交到这个仓库,所有人都可以看到你的每次更新。这么做最主要的优点在于,你可 以按照自己的节奏继续工作,而不必等待维护者处理你提交的更新;而维护者也可以按照自己的节奏,任何时候都可以过来处理接纳你的贡献。

 

司令官与副官工作流

这其实是上一种工作流的变体。一般超大型的项目才会用到这样的工作方式,像是拥有数百协作开发者的 Linux 内核项目就是如此。各个集成管理员分别负责集成项目中的特定部分,所以称为副官(lieutenant)。而所有这些集成管理员头上还有一位负责统筹的总 集成管理员,称为司令官(dictator)。司令官维护的仓库用于提供所有协作者拉取最新集成的项目代码。整个流程看起来如图 5-3 所示:

  1. 一般的开发者在自己的特性分支上工作,并不定期地根据主干分支(dictator 上的 master)衍合。
  2. 副官(lieutenant)将普通开发者的特性分支合并到自己的 master 分支中。
  3. 司令官(dictator)将所有副官的 master 分支并入自己的 master 分支。
  4. 司令官(dictator)将集成后的 master 分支推送到共享仓库 blessed repository 中,以便所有其他开发者以此为基础进行衍合。

技术分享

图 5-3. 司令官与副官工作流

这种工作流程并不常用,只有当项目极为庞杂,或者需要多级别管理时,才会体现出优势。利用这种方式,项目总负责人(即司令官)可以把大量分散的集成工作委托给不同的小组负责人分别处理,最后再统筹起来,如此各人的职责清晰明确,也不易出错(译注:此乃分而治之)。

以上介绍的是常见的分布式系统可以应用的工作流程,当然不止于 Git。在实际的开发工作中,你可能会遇到各种为了满足特定需求而有所变化的工作方式。我想现在你应该已经清楚,接下来自己需要用哪种方式开展工作了。下 节我还会再举些例子,看看各式工作流中的每个角色具体应该如何操作。

 

5.2  为项目作贡献

接下来,我们来学习一下作为项目贡献者,会有哪些常见的工作模式。

不过要说清楚整个协作过程真的很难,Git 如此灵活,人们的协作方式便可以各式各样,没有固定不变的范式可循,而每个项目的具体情况又多少会有些不同,比如说参与者的规模,所选择的工作流程,每个人的提交权限,以及 Git 以外贡献等等,都会影响到具体操作的细节。

首当其冲的是参与者规模。项目中有多少开发者是经常提交代码的?经常又是多久呢?大多数两至三人的小团队,一天大约只有几次提交,如果不是什么热门 项目的话就更少了。可要是在大公司里,或者大项目中,参与者可以多到上千,每天都会有十几个上百个补丁提交上来。这种差异带来的影响是显著的,越是多的人 参与进来,就越难保证每次合并正确无误。你正在工作的代码,可能会因为合并进来其他人的更新而变得过时,甚至受创无法运行。而已经提交上去的更新,也可能 在等着审核合并的过程中变得过时。那么,我们该怎样做才能确保代码是最新的,提交的补丁也是可用的呢?

接下来便是项目所采用的工作流。是集中式的,每个开发者都具有等同的写权限?项目是否有专人负责检查所有补丁?是不是所有补丁都做过同行复阅(peer-review)再通过审核的?你是否参与审核过程?如果使用副官系统,那你是不是限定于只能向此副官提交?

还有你的提交权限。有或没有向主项目提交更新的权限,结果完全不同,直接决定最终采用怎样的工作流。如果不能直接提交更新,那该如何贡献自己的代码呢?是不是该有个什么策略?你每次贡献代码会有多少量?提交频率呢?

所有以上这些问题都会或多或少影响到最终采用的工作流。接下来,我会在一系列由简入繁的具体用例中,逐一阐述。此后在实践时,应该可以借鉴这里的例子,略作调整,以满足实际需要构建自己的工作流。

 

提交指南

开始分析特定用例之前,先来了解下如何撰写提交说明。一份好的提交指南可以帮助协作者更轻松更有效地配合。Git 项目本身就提供了一份文档(Git 项目源代码目录中Documentation/SubmittingPatches),列数了大量提示,从如何编撰提交说明到提交补丁,不一而足。

首先,请不要在更新中提交多余的白字符(whitespace)。Git 有种检查此类问题的方法,在提交之前,先运行 git diff --check,会把可能的多余白字符修正列出来。下面的示例,我已经把终端中显示为红色的白字符用X 替换掉:

1
2
3
4
5
6
7
$ git diff--check
lib/simplegit.rb:5: trailing whitespace.
+    @git_dir = File.expand_path(git_dir)XX
lib/simplegit.rb:7: trailing whitespace.
+ XXXXXXXXXXX
lib/simplegit.rb:26: trailing whitespace.
+    def command(git_cmd)XXXX

这样在提交之前你就可以看到这类问题,及时解决以免困扰其他开发者。

接下来,请将每次提交限定于完成一次逻辑功能。并且可能的话,适当地分解为多次小更新,以便每次小型提交都更易于理解。请不要在周末穷追猛打一次性 解决五个问题,而最后拖到周一再提交。就算是这样也请尽可能利用暂存区域,将之前的改动分解为每次修复一个问题,再分别提交和加注说明。如果针对两个问题 改动的是同一个文件,可以试试看git add --patch 的方式将部分内容置入暂存区域(我们会在第六章再详细介绍)。无论是五次小提交还是混杂在一起的大提交,最终分支末端的项目快照应该还是一样的,但分解开 来之后,更便于其他开发者复阅。这么做也方便自己将来取消某个特定问题的修复。我们将在第六章介绍一些重写提交历史,同暂存区域交互的技巧和工具,以便最 终得到一个干净有意义,且易于理解的提交历史。

最后需要谨记的是提交说明的撰写。写得好可以让大家协作起来更轻松。一般来说,提交说明最好限制在一行以内,50 个字符以下,简明扼要地描述更新内容,空开一行后,再展开详细注解。Git 项目本身需要开发者撰写详尽注解,包括本次修订的因由,以及前后不同实现之间的比较,我们也该借鉴这种做法。另外,提交说明应该用祈使现在式语态,比如, 不要说成 “I added tests for” 或 “Adding tests for” 而应该用 “Add tests for”。下面是来自 tpope.net 的 Tim Pope 原创的提交说明格式模版,供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
本次更新的简要描述(50 个字符以内)
 
如果必要,此处展开详尽阐述。段落宽度限定在 72 个字符以内。
某些情况下,第一行的简要描述将用作邮件标题,其余部分作为邮件正文。
其间的空行是必要的,以区分两者(当然没有正文另当别论)。
如果并在一起,rebase 这样的工具就可能会迷惑。
 
另起空行后,再进一步补充其他说明。
 
 - 可以使用这样的条目列举式。
 
 - 一般以单个空格紧跟短划线或者星号作为每项条目的起始符。每个条目间用一空行隔开。
   不过这里按自己项目的约定,可以略作变化。

如果你的提交说明都用这样的格式来书写,好多事情就可以变得十分简单。Git 项目本身就是这样要求的,我强烈建议你到 Git 项目仓库下运行 git log --no-merges 看看,所有提交历史的说明是怎样撰写的。(译注:如果现在还没有克隆 git 项目源代码,是时候git clone git://git.kernel.org/pub/scm/git/git.git 了。)

为简单起见,在接下来的例子(及本书随后的所有演示)中,我都不会用这种格式,而使用 -m 选项提交 git commit。不过请还是按照我之前讲的做,别学我这里偷懒的方式。

 

私有的小型团队

我们从最简单的情况开始,一个私有项目,与你一起协作的还有另外一到两位开发者。这里说私有,是指源代码不公开,其他人无法访问项目仓库。而你和其他开发者则都具有推送数据到仓库的权限。

这种情况下,你们可以用 Subversion 或其他集中式版本控制系统类似的工作流来协作。你仍然可以得到 Git 带来的其他好处:离线提交,快速分支与合并等等,但工作流程还是差不多的。主要区别在于,合并操作发生在客户端而非服务器上。让我们来看看,两个开发者一 起使用同一个共享仓库,会发生些什么。第一个人,John,克隆了仓库,作了些更新,在本地提交。(下面的例子中省略了常规提示,用... 代替以节约版面。)

1
2
3
4
5
6
7
8
9
# John‘s Machine
$ git clone john@githost:simplegit.git
Initialized empty Git repository in/home/john/simplegit/.git/
...
$cdsimplegit/
$ vim lib/simplegit.rb
$ git commit -am ‘removed invalid default value‘
[master 738ee87] removed invalid default value
 1 files changed, 1 insertions(+), 1 deletions(-)

第二个开发者,Jessica,一样这么做:克隆仓库,提交更新:

1
2
3
4
5
6
7
8
9
# Jessica‘s Machine
$ git clone jessica@githost:simplegit.git
Initialized empty Git repository in/home/jessica/simplegit/.git/
...
$cdsimplegit/
$ vim TODO
$ git commit -am ‘add reset task‘
[master fbff5bc] add reset task
 1 files changed, 1 insertions(+), 0 deletions(-)

现在,Jessica 将她的工作推送到服务器上:

1
2
3
4
5
# Jessica‘s Machine
$ git push origin master
...
To jessica@githost:simplegit.git
   1edee6b..fbff5bc  master -> master

John 也尝试推送自己的工作上去:

1
2
3
4
5
# 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 的推送操作被驳回,因为 Jessica 已经推送了新的数据上去。请注意,特别是你用惯了 Subversion 的话,这里其实修改的是两个文件,而不是同一个文件的同一个地方。Subversion 会在服务器端自动合并提交上来的更新,而 Git 则必须先在本地合并后才能推送。于是,John 不得不先把 Jessica 的更新拉下来:

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

此刻,John 的本地仓库如图 5-4 所示:

 

技术分享
图 5-4. John 的仓库历史

虽然 John 下载了 Jessica 推送到服务器的最近更新(fbff5),但目前只是 origin/master 指针指向它,而当前的本地分支master 仍然指向自己的更新(738ee),所以需要先把她的提交合并过来,才能继续推送数据:

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

还好,合并过程非常顺利,没有冲突,现在 John 的提交历史如图 5-5 所示:

 

技术分享
图 5-5. 合并 origin/master 后 John 的仓库历史

现在,John 应该再测试一下代码是否仍然正常工作,然后将合并结果(72bbc)推送到服务器上:

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

最终,John 的提交历史变为图 5-6 所示:

 

技术分享
图 5-6. 推送后 John 的仓库历史

而在这段时间,Jessica 已经开始在另一个特性分支工作了。她创建了 issue54 并提交了三次更新。她还没有下载 John 提交的合并结果,所以提交历史如图 5-7 所示:

 

技术分享
图 5-7. Jessica 的提交历史

Jessica 想要先和服务器上的数据同步,所以先下载数据:

1
2
3
4
5
# Jessica‘s Machine
$ git fetch origin
...
From jessica@githost:simplegit
   fbff5bc..72bbc59  master     -> origin/master

于是 Jessica 的本地仓库历史多出了 John 的两次提交(738ee 和 72bbc),如图 5-8 所示:

 

技术分享
图 5-8. 获取 John 的更新之后 Jessica 的提交历史

此时,Jessica 在特性分支上的工作已经完成,但她想在推送数据之前,先确认下要并进来的数据究竟是什么,于是运行git log 查看:

1
2
3
4
5
6
7
$ git log --no-merges origin/master^issue54
commit 738ee872852dfaa9d6634e0dea7a324040193016
Author: John Smith
 
Date:   Fri May 29 16:01:27 2009 -0700
 
    removed invalid default value

现在,Jessica 可以将特性分支上的工作并到 master 分支,然后再并入 John 的工作(origin/master)到自己的master分支,最后再推送回服务器。当然,得先切回主分支才能集成所有数据:

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

要合并 origin/master 或 issue54 分支,谁先谁后都没有关系,因为它们都在上游(upstream)(译注:想像分叉的更新像是汇流成河的源头,所以上游 upstream 是指最新的提交),所以无所谓先后顺序,最终合并后的内容快照都是一样的,而仅是提交历史看起来会有些先后差别。Jessica 选择先合并issue54

1
2
3
4
5
6
$ 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):

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

所有的合并都非常干净。现在 Jessica 的提交历史如图 5-9 所示:

 

技术分享
图 5-9. 合并 John 的更新后 Jessica 的提交历史

现在 Jessica 已经可以在自己的 master 分支中访问 origin/master 的最新改动了,所以她应该可以成功推送最后的合并结果到服务器上(假设 John 此时没再推送新数据上来):

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

至此,每个开发者都提交了若干次,且成功合并了对方的工作成果,最新的提交历史如图 5-10 所示:

 

技术分享
图 5-10. Jessica 推送数据后的提交历史

以上就是最简单的协作方式之一:先在自己的特性分支中工作一段时间,完成后合并到自己的 master 分支;然后下载合并 origin/master 上的更新(如果有的话),再推回远程服务器。一般的协作流程如图 5-11 所示:

技术分享

图 5-11. 多用户共享仓库协作方式的一般工作流程时序

 

私有团队间协作

现在我们来看更大一点规模的私有团队协作。如果有几个小组分头负责若干特性的开发和集成,那他们之间的协作过程是怎样的。

假设 John 和 Jessica 一起负责开发某项特性 A,而同时 Jessica 和 Josie 一起负责开发另一项功能 B。公司使用典型的集成管理员式工作流,每个组都有一名管理员负责集成本组代码,及更新项目主仓库的master 分支。所有开发都在代表小组的分支上进行。

让我们跟随 Jessica 的视角看看她的工作流程。她参与开发两项特性,同时和不同小组的开发者一起协作。克隆生成本地仓库后,她打算先着手开发特性 A。于是创建了新的featureA 分支,继而编写代码:

1
2
3
4
5
6
7
# 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(-)

此刻,她需要分享目前的进展给 John,于是她将自己的 featureA 分支提交到服务器。由于 Jessica 没有权限推送数据到主仓库的master 分支(只有集成管理员有此权限),所以只能将此分支推上去同 John 共享协作:

1
2
3
4
$ git push origin featureA
...
To jessica@githost:simplegit.git
 * [new branch]      featureA -> featureA

Jessica 发邮件给 John 让他上来看看 featureA 分支上的进展。在等待他的反馈之前,Jessica 决定继续工作,和 Josie 一起开发featureB 上的特性 B。当然,先创建此分支,分叉点以服务器上的 master 为起点:

1
2
3
4
# Jessica‘s Machine
$ git fetch origin
$ git checkout -b featureB origin/master
Switched to a new branch "featureB"

随后,Jessica 在 featureB 上提交了若干更新:

1
2
3
4
5
6
7
8
$ vim lib/simplegit.rb
$ git commit -am ‘made the ls-tree function recursive‘
[featureB e5b0fdc] made the ls-treefunctionrecursive
 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 的更新历史如图 5-12 所示:

技术分享

图 5-12. Jessica 的更新历史

Jessica 正准备推送自己的进展上去,却收到 Josie 的来信,说是她已经将自己的工作推到服务器上的 featureBee 分支了。这样,Jessica 就必须先将 Josie 的代码合并到自己本地分支中,才能再一起推送回服务器。她用git fetch 下载 Josie 的最新代码:

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

然后 Jessica 使用 git merge 将此分支合并到自己分支中:

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

合并很顺利,但另外有个小问题:她要推送自己的 featureB 分支到服务器上的 featureBee 分支上去。当然,她可以使用冒号(:)格式指定目标分支:

1
2
3
4
$ git push origin featureB:featureBee
...
To jessica@githost:simplegit.git
   fba9af8..cd685d1  featureB -> featureBee

我们称此为_refspec_。更多有关于 Git refspec 的讨论和使用方式会在第九章作详细阐述。

接下来,John 发邮件给 Jessica 告诉她,他看了之后作了些修改,已经推回服务器 featureA 分支,请她过目下。于是 Jessica 运行git fetch 下载最新数据:

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

接下来便可以用 git log 查看更新了些什么:

1
2
3
4
5
6
7
$ git log origin/featureA^featureA
commit aad881d154acdaeb2b6b18ea0e827ed8a6d671e6
Author: John Smith
 
Date:   Fri May 29 19:57:33 2009 -0700
 
    changed log output to 30 from 25

最后,她将 John 的工作合并到自己的 featureA 分支中:

1
2
3
4
5
6
7
$ 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 稍做一番修整后同步到服务器:

1
2
3
4
5
6
7
$ git commit -am ‘small tweak‘
[featureA ed774b3] small tweak
 1 files changed, 1 insertions(+), 1 deletions(-)
$ git push origin featureA
...
To jessica@githost:simplegit.git
   3300904..ed774b3  featureA -> featureA

现在的 Jessica 提交历史如图 5-13 所示:

技术分享

图 5-13. 在特性分支中提交更新后的提交历史

现在,Jessica,Josie 和 John 通知集成管理员服务器上的 featureA 及 featureBee 分支已经准备好,可以并入主线了。在管理员完成集成工作后,主分支上便多出一个新的合并提交(5399e),用 fetch 命令更新到本地后,提交历史如图 5-14 所示:

技术分享

图 5-14. 合并特性分支后的 Jessica 提交历史

许多开发小组改用 Git 就是因为它允许多个小组间并行工作,而在稍后恰当时机再行合并。通过共享远程分支的方式,无需干扰整体项目代码便可以开展工作,因此使用 Git 的小型团队间协作可以变得非常灵活自由。以上工作流程的时序如图 5-15 所示:

技术分享

图 5-15. 团队间协作工作流程基本时序

 

公开的小型项目

上面说的是私有项目协作,但要给公开项目作贡献,情况就有些不同了。因为你没有直接更新主仓库分支的权限,得寻求其它方式把工作成果交给项目维护 人。下面会介绍两种方法,第一种使用 git 托管服务商提供的仓库复制功能,一般称作 fork,比如 repo.or.cz 和 GitHub 都支持这样的操作,而且许多项目管理员都希望大家使用这样的方式。另一种方法是通过电子邮件寄送文件补丁。

但不管哪种方式,起先我们总需要克隆原始仓库,而后创建特性分支开展工作。基本工作流程如下:

1
2
3
4
5
6
7
$ git clone (url)
$cdproject
$ git checkout -b featureA
$ (work)
$ git commit
$ (work)
$ git commit

你可能想到用 rebase -i 将所有更新先变作单个提交,又或者想重新安排提交之间的差异补丁,以方便项目维护者审阅 – 有关交互式衍合操作的细节见第六章。

在完成了特性分支开发,提交给项目维护者之前,先到原始项目的页面上点击“Fork”按钮,创建一个自己可写的公共仓库(译注:即下面的 url 部分,参照后续的例子,应该是git://githost/simplegit.git)。然后将此仓库添加为本地的第二个远端仓库,姑且称为 myfork

1
$ git remote add myfork (url)

你需要将本地更新推送到这个仓库。要是将远端 master 合并到本地再推回去,还不如把整个特性分支推上去来得干脆直接。而且,假若项目维护者未采纳你的贡献的话(不管是直接合并还是 cherry pick),都不用回退(rewind)自己的 master 分支。但若维护者合并或 cherry-pick 了你的工作,最后总还可以从他们的更新中同步这些代码。好吧,现在先把 featureA 分支整个推上去:

1
$ git push myfork featureA

然后通知项目管理员,让他来抓取你的代码。通常我们把这件事叫做 pull request。可以直接用 GitHub 等网站提供的 “pull request” 按钮自动发送请求通知;或手工把git request-pull 命令输出结果电邮给项目管理员。

request-pull 命令接受两个参数,第一个是本地特性分支开始前的原始分支,第二个是请求对方来抓取的 Git 仓库 URL(译注:即下面myfork 所指的,自己可写的公共仓库)。比如现在Jessica 准备要给 John 发一个 pull requst,她之前在自己的特性分支上提交了两次更新,并把分支整个推到了服务器上,所以运行该命令会看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git request-pull origin/mastermyfork
The following changes since commit 1edee6b1d61823a2de3b09c160d7080b8d1b3a40:
  John Smith (1):
        added a new function
 
are available inthe 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 开始:

1
2
3
4
5
6
$ git checkout -b featureB origin/master
$ (work)
$ git commit
$ git push myfork featureB
$ (email maintainer)
$ git fetch origin

现在,A、B 两个特性分支各不相扰,如同竹筒里的两颗豆子,队列中的两个补丁,你随时都可以分别从头写过,或者衍合,或者修改,而不用担心特性代码的交叉混杂。如图 5-16 所示:

技术分享

图 5-16. featureB 以后的提交历史

假设项目管理员接纳了许多别人提交的补丁后,准备要采纳你提交的第一个分支,却发现因为代码基准不一致,合并工作无法正确干净地完成。这就需要你再次衍合到最新的 origin/master,解决相关冲突,然后重新提交你的修改:

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

自然,这会重写提交历史,如图 5-17 所示:

技术分享

图 5-17. featureA 重新衍合后的提交历史

注意,此时推送分支必须使用 -f 选项(译注:表示 force,不作检查强制重写)替换远程已有的 featureA 分支,因为新的 commit 并非原来的后续更新。当然你也可以直接推送到另一个新的分支上去,比如称作featureAv2

再考虑另一种情形:管理员看过第二个分支后觉得思路新颖,但想请你改下具体实现。我们只需以当前 origin/master 分支为基准,开始一个新的特性分支featureBv2,然后把原来的 featureB 的更新拿过来,解决冲突,按要求重新实现部分代码,然后将此特性分支推送上去:

1
2
3
4
5
$ git checkout -b featureBv2 origin/master
$ git merge --no-commit --squash featureB
$ (change implementation)
$ git commit
$ git push myfork featureBv2

这里的 --squash 选项将目标分支上的所有更改全拿来应用到当前分支上,而 --no-commit 选项告诉 Git 此时无需自动生成和记录(合并)提交。这样,你就可以在原来代码基础上,继续工作,直到最后一起提交。

好了,现在可以请管理员抓取 featureBv2 上的最新代码了,如图 5-18 所示:

技术分享

图 5-18. featureBv2 之后的提交历史

 

公开的大型项目

许多大型项目都会立有一套自己的接受补丁流程,你应该注意下其中细节。但多数项目都允许通过开发者邮件列表接受补丁,现在我们来看具体例子。

整个工作流程类似上面的情形:为每个补丁创建独立的特性分支,而不同之处在于如何提交这些补丁。不需要创建自己可写的公共仓库,也不用将自己的更新推送到自己的服务器,你只需将每次提交的差异内容以电子邮件的方式依次发送到邮件列表中即可。

1
2
3
4
5
$ git checkout -b topicA
$ (work)
$ git commit
$ (work)
$ git commit

如此一番后,有了两个提交要发到邮件列表。我们可以用 git format-patch 命令来生成 mbox 格式的文件然后作为附件发送。每个提交都会封装为一个.patch 后缀的 mbox 文件,但其中只包含一封邮件,邮件标题就是提交消息(译注:额外有前缀,看例子),邮件内容包含补丁正文和 Git 版本号。这种方式的妙处在于接受补丁时仍可保留原来的提交消息,请看接下来的例子:

1
2
3
$ 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 检查是否有对文件重命名的提交。我们来看看补丁文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$cat0001-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‘)
--
1.6.2.rc1.20.g8c5b.dirty

如果有额外信息需要补充,但又不想放在提交消息中说明,可以编辑这些补丁文件,在第一个 --- 行之前添加说明,但不要修改下面的补丁正文,比如例子中的Limit log functionality to the first 20 部分。这样,其它开发者能阅读,但在采纳补丁时不会将此合并进来。

你可以用邮件客户端软件发送这些补丁文件,也可以直接在命令行发送。有些所谓智能的邮件客户端软件会自作主张帮你调整格式,所以粘贴补丁到邮件正文 时,有可能会丢失换行符和若干空格。Git 提供了一个通过 IMAP 发送补丁文件的工具。接下来我会演示如何通过 Gmail 的 IMAP 服务器发送。另外,在 Git 源代码中有个Documentation/SubmittingPatches 文件,可以仔细读读,看看其它邮件程序的相关导引。

首先在 ~/.gitconfig 文件中配置 imap 项。每个选项都可用 git config 命令分别设置,当然直接编辑文件添加以下内容更便捷:

1
2
3
4
5
6
7
[imap]
  folder = "[Gmail]/Drafts"
  host = imaps://imap.gmail.com
  user = user@ gmail.com
  pass = p4ssw0rd
  port = 993
  sslverify = false

如果你的 IMAP 服务器没有启用 SSL,就无需配置最后那两行,并且 host 应该以 imap:// 开头而不再是有 s 的imaps://。保存配置文件后,就能用 git send-email 命令把补丁作为邮件依次发送到指定的 IMAP 服务器上的文件夹中(译注:这里就是 Gmail 的[Gmail]/Drafts 文件夹。但如果你的语言设置不是英文,此处的文件夹 Drafts 字样会变为对应的语言。):

1
2
3
4
5
6
7
$ 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 <jessica@ example.com>]
Emails will be sent from: Jessica Smith <jessica@ example.com>
Who should the emails be sent to? jessica@ example.com
Message-ID to be used as In-Reply-To for the first email? y

接下来,Git 会根据每个补丁依次输出类似下面的日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(mbox) Adding cc: Jessica Smith <jessica@ example.com> from
  \line ‘From: Jessica Smith <jessica@ example.com>‘
OK. Log says:
Sendmail: /usr/sbin/sendmail -i jessica@ example.com
From: Jessica Smith < jessica@ example.com>
To: jessica@ example.com
Subject: [PATCH 1/2] added limit to log function
Date: Sat, 30 May 2009 13:29:15 -0700
Message-Id: <1243715356-61726-1-git-send-email-jessica@ example.com>
X-Mailer: git-send-email 1.6.2.rc1.20.g8c5b.dirty
In-Reply-To: <y>
References: <y>
 
Result: OK

最后,到 Gmail 上打开 Drafts 文件夹,编辑这些邮件,修改收件人地址为邮件列表地址,另外给要抄送的人也加到 Cc 列表中,最后发送。

 

小结

本节主要介绍了常见 Git 项目协作的工作流程,还有一些帮助处理这些工作的命令和工具。接下来我们要看看如何维护 Git 项目,并成为一个合格的项目管理员,或是集成经理。

 

5.3  项目的管理

既然是相互协作,在贡献代码的同时,也免不了要维护管理自己的项目。像是怎么处理别人用 format-patch 生成的补丁,或是集成远端仓库上某个分支上的变化等等。但无论是管理代码仓库,还是帮忙审核收到的补丁,都需要同贡献者约定某种长期可持续的工作方式。

使用特性分支进行工作

如果想要集成新的代码进来,最好局限在特性分支上做。临时的特性分支可以让你随意尝试,进退自如。比如碰上无法正常工作的补丁,可以先搁在那边,直到有时间仔细核查修复为止。创建的分支可以用相关的主题关键字命名,比如ruby_client 或者其它类似的描述性词语,以帮助将来回忆。Git 项目本身还时常把分支名称分置于不同命名空间下,比如 sc/ruby_client 就说明这是 sc 这个人贡献的。现在从当前主干分支为基础,新建临时分支:

1
$ git branch sc/ruby_clientmaster

另外,如果你希望立即转到分支上去工作,可以用 checkout -b

1
$ git checkout -b sc/ruby_clientmaster<

好了,现在已经准备妥当,可以试着将别人贡献的代码合并进来了。之后评估一下有没有问题,最后再决定是不是真的要并入主干。

 

采纳来自邮件的补丁

如果收到一个通过电邮发来的补丁,你应该先把它应用到特性分支上进行评估。有两种应用补丁的方法:git apply 或者git am

 

使用 apply 命令应用补丁

如果收到的补丁文件是用 git diff 或由其它 Unix 的 diff 命令生成,就该用 git apply 命令来应用补丁。假设补丁文件存在 /tmp/patch-ruby-client.patch,可以这样运行:

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

这会修改当前工作目录下的文件,效果基本与运行 patch -p1 打补丁一样,但它更为严格,且不会出现混乱。如果是 git diff 格式描述的补丁,此命令还会相应地添加,删除,重命名文件。当然,普通的patch 命令是不会这么做的。另外请注意,git apply 是一个事务性操作的命令,也就是说,要么所有补丁都打上去,要么全部放弃。所以不会出现patch 命令那样,一部分文件打上了补丁而另一部分却没有,这样一种不上不下的修订状态。所以总的来说,git apply 要比patch 严谨许多。因为仅仅是更新当前的文件,所以此命令不会自动生成提交对象,你得手工缓存相应文件的更新状态并执行提交命令。

在实际打补丁之前,可以先用 git apply --check 查看补丁是否能够干净顺利地应用到当前分支中:

1
2
3
$ 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 脚本里可用于检测状态。

使用 am 命令应用补丁

如果贡献者也用 Git,且擅于制作 format-patch 补丁,那你的合并工作将会非常轻松。因为这些补丁中除了文件内容差异外,还包含了作者信息和提交消息。所以请鼓励贡献者用format-patch 生成补丁。对于传统的 diff 命令生成的补丁,则只能用 git apply 处理。

对于 format-patch 制作的新式补丁,应当使用 git am 命令。从技术上来说,git am 能够读取 mbox 格式的文件。这是种简单的纯文本文件,可以包含多封电邮,格式上用 From 加空格以及随便什么辅助信息所组成的行作为分隔行,以区分每封邮件,就像这样:

1
2
3
4
5
6
7
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

这是 format-patch 命令输出的开头几行,也是一个有效的 mbox 文件格式。如果有人用 git send-email 给你发了一个补丁,你可以将此邮件下载到本地,然后运行git am 命令来应用这个补丁。如果你的邮件客户端能将多封电邮导出为 mbox 格式的文件,就可以用 git am 一次性应用所有导出的补丁。

如果贡献者将 format-patch 生成的补丁文件上传到类似 Request Ticket 一样的任务处理系统,那么可以先下载到本地,继而使用git am 应用该补丁:

1
2
$ git am 0001-limit-log-function.patch
Applying: add limit to log function

你会看到它被干净地应用到本地分支,并自动创建了新的提交对象。作者信息取自邮件头 From 和 Date,提交消息则取自Subject 以及正文中补丁之前的内容。来看具体实例,采纳之前展示的那个 mbox 电邮补丁后,最新的提交对象为:

1
2
3
4
5
6
7
8
9
10
11
12
$ git log --pretty=fuller -1
commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Author:     Jessica Smith
 
AuthorDate: Sun Apr 6 10:17:23 2008 -0700
Commit:     Scott Chacon
 
CommitDate: Thu Apr 9 09:19:06 2009 -0700
 
   add limit to log function
 
   Limit log functionality to the first 20

Commit 部分显示的是采纳补丁的人,以及采纳的时间。而 Author 部分则显示的是原作者,以及创建补丁的时间。

有时,我们也会遇到打不上补丁的情况。这多半是因为主干分支和补丁的基础分支相差太远,但也可能是因为某些依赖补丁还未应用。这种情况下,git am 会报错并询问该怎么做:

1
2
3
4
5
6
7
8
$ git am 0001-seeing-if-this-helps-the-gem.patch
Applying: seeing ifthis 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 会在有冲突的文件里加入冲突解决标记,这同合并或衍合操作一样。解决的办法也一样,先编辑文件消除冲突,然后暂存文件,最后运行 git am --resolved 提交修正结果:

1
2
3
4
$ (fix the file)
$ git add ticgit.gemspec
$ git am --resolved
Applying: seeing ifthis helps the gem

如果想让 Git 更智能地处理冲突,可以用 -3 选项进行三方合并。如果当前分支未包含该补丁的基础代码或其祖先,那么三方合并就会失败,所以该选项默认为关闭状态。一般来说,如果该补丁是基于某个公开的提交制作而成的话,你总是可以通过同步来获取这个共同祖先,所以用三方合并选项可以解决很多麻烦:

1
2
3
4
5
6
7
$ git am -3 0001-seeing-if-this-helps-the-gem.patch
Applying: seeing ifthis 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 格式文件,可以用 am 命令的交互模式选项 -i,这样就会在打每个补丁前停住,询问该如何操作:

1
2
3
4
5
6
$ git am -3 -i mbox
Commit Body is:
--------------------------
seeingifthis helps the gem
--------------------------
Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all

在多个补丁要打的情况下,这是个非常好的办法,一方面可以预览下补丁内容,同时也可以有选择性的接纳或跳过某些补丁。

打完所有补丁后,如果测试下来新特性可以正常工作,那就可以安心地将当前特性分支合并到长期分支中去了。

 

检出远程分支

如果贡献者有自己的 Git 仓库,并将修改推送到此仓库中,那么当你拿到仓库的访问地址和对应分支的名称后,就可以加为远程分支,然后在本地进行合并。

比如,Jessica 发来一封邮件,说在她代码库中的 ruby-client 分支上已经实现了某个非常棒的新功能,希望我们能帮忙测试一下。我们可以先把她的仓库加为远程仓库,然后抓取数据,完了再将她所说的分支检出到本地来测试:

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

若是不久她又发来邮件,说还有个很棒的功能实现在另一分支上,那我们只需重新抓取下最新数据,然后检出那个分支到本地就可以了,无需重复设置远程仓库。

这种做法便于同别人保持长期的合作关系。但前提是要求贡献者有自己的服务器,而我们也需要为每个人建一个远程分支。有些贡献者提交代码补丁并不是很 频繁,所以通过邮件接收补丁效率会更高。同时我们自己也不会希望建上百来个分支,却只从每个分支取一两个补丁。但若是用脚本程序来管理,或直接使用代码仓 库托管服务,就可以简化此过程。当然,选择何种方式取决于你和贡献者的喜好。

使用远程分支的另外一个好处是能够得到提交历史。不管代码合并是不是会有问题,至少我们知道该分支的历史分叉点,所以默认会从共同祖先开始自动进行三方合并,无需 -3 选项,也不用像打补丁那样祈祷存在共同的基准点。

如果只是临时合作,只需用 git pull 命令抓取远程仓库上的数据,合并到本地临时分支就可以了。一次性的抓取动作自然不会把该仓库地址加为远程仓库。

1
2
3
4
$ git pull git://github.com/onetimeguy/project.git
From git://github.com/onetimeguy/project
 * branch            HEAD       -> FETCH_HEAD
Merge made by recursive.

 

决断代码取舍

现在特性分支上已合并好了贡献者的代码,是时候决断取舍了。本节将回顾一些之前学过的命令,以看清将要合并到主干的是哪些代码,从而理解它们到底做了些什么,是否真的要并入。 一般我们会先看下,特性分支上都有哪些新增的提交。比如在 contrib 特性分支上打了两个补丁,仅查看这两个补丁的提交信息,可以用--not 选项指定要屏蔽的分支 master,这样就会剔除重复的提交历史:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git log contrib --not master
commit 5b6235bd297351589efc4d73316f0a68d484f118
Author: Scott Chacon
 
Date:   Fri Oct 24 09:53:59 2008 -0700
 
    seeingifthis helps the gem
 
commit 7482e0d16d04bea79d0dba8988cc78df655f16a0
Author: Scott Chacon
 
Date:   Mon Oct 22 19:38:36 2008 -0700
 
    updated the gemspec to hopefully work better

还可以查看每次提交的具体修改。请牢记,在 git log 后加 -p 选项将展示每次提交的内容差异。

如果想看当前分支同其他分支合并时的完整内容差异,有个小窍门:

1
$ git diffmaster

虽然能得到差异内容,但请记住,结果有可能和我们的预期不同。一旦主干 master 在特性分支创建之后有所修改,那么通过 diff 命令来比较的,是最新主干上的提交快照。显然,这不是我们所要的。比方在 master 分支中某个文件里添了一行,然后运行上面的命令,简单的比较最新快照所得到的结论只能是,特性分支中删除了这一行。

这个很好理解:如果 master 是特性分支的直接祖先,不会产生任何问题;如果它们的提交历史在不同的分叉上,那么产生的内容差异,看起来就像是增加了特性分支上的新代码,同时删除了master 分支上的新代码。

实际上我们真正想要看的,是新加入到特性分支的代码,也就是合并时会并入主干的代码。所以,准确地讲,我们应该比较特性分支和它同 master 分支的共同祖先之间的差异。

我们可以手工定位它们的共同祖先,然后与之比较:

1
$ git merge-base contrib master 36c7dba2c95e6bbb78dfa822519ecfec6e1ca649 $ git diff36c7db

但这么做很麻烦,所以 Git 提供了便捷的 ... 语法。对于 diff 命令,可以把 ... 加在原始分支(拥有共同祖先)和当前分支之间:

1
$ git diffmaster...contrib

现在看到的,就是实际将要引入的新代码。这是一个非常有用的命令,应该牢记。

 

代码集成

一旦特性分支准备停当,接下来的问题就是如何集成到更靠近主线的分支中。此外还要考虑维护项目的总体步骤是什么。虽然有很多选择,不过我们这里只介绍其中一部分。

 

合并流程

一般最简单的情形,是在 master 分支中维护稳定代码,然后在特性分支上开发新功能,或是审核测试别人贡献的代码,接着将它并入主干,最后删除这个特性分支,如此反复。来看示例,假设当前代码库中有两个分支,分别为ruby_client和 php_client,如图 5-19 所示。然后先把 ruby_client 合并进主干,再合并php_client,最后的提交历史如图 5-20 所示。

技术分享

图 5-19. 多个特性分支

技术分享

图 5-20. 合并特性分支之后

这是最简单的流程,所以在处理大一些的项目时可能会有问题。 对于大型项目,至少需要维护两个长期分支 master 和 develop。新代码(图 5-21 中的 ruby_client)将首先并入develop 分支(图 5-22 中的 C8),经过一个阶段,确认develop 中的代码已稳定到可发行时,再将 master 分支快进到稳定点(图 5-23 中的 C8)。而平时这两个分支都会被推送到公开的代码库。

技术分享

图 5-21. 特性分支合并前

技术分享

图 5-22. 特性分支合并后

技术分享

图 5-23. 特性分支发布后

这样,在人们克隆仓库时就有两种选择:既可检出最新稳定版本,确保正常使用;也能检出开发版本,试用最前沿的新特性。你也可以扩展这个概念,先将所有新代码合并到临时特性分支,等到该分支稳定下来并通过测试后,再并入develop分支。然后,让时间检验一切,如果这些代码确实可以正常工作相当长一段时间,那就有理由相信它已经足够稳定,可以放心并入主干分支发布。

 

大项目的合并流程

Git 项目本身有四个长期分支:用于发布的 master 分支、用于合并基本稳定特性的 next 分支、用于合并仍需改进特性的pu 分支(pu 是 proposed updates 的缩写),以及用于除错维护的 maint 分支(maint 取自 maintenance)。维护者可以按照之前介绍的方法,将贡献者的代码引入为不同的特性分支(如图 5-24 所示),然后测试评估,看哪些特性能稳定工作,哪些还需改进。稳定的特性可以并入next 分支,然后再推送到公共仓库,以供其他人试用。

技术分享

图 5-24. 管理复杂的并行贡献

仍需改进的特性可以先并入 pu 分支。直到它们完全稳定后再并入 master。同时一并检查下 next 分支,将足够稳定的特性也并入 master。所以一般来说,master 始终是在快进,next 偶尔做下衍合,而pu 则是频繁衍合,如图 5-25 所示:

技术分享

图 5-25. 将特性并入长期分支

并入 master 后的特性分支,已经无需保留分支索引,放心删除好了。Git 项目还有一个 maint 分支,它是以最近一次发行版为基础分化而来的,用于维护除错补丁。所以克隆 Git 项目仓库后会得到这四个分支,通过检出不同分支可以了解各自进展,或是试用前沿特性,或是贡献代码。而维护者则通过管理这些分支,逐步有序地并入第三方贡献。

 

衍合与挑拣(cherry-pick)的流程

一些维护者更喜欢衍合或者挑拣贡献者的代码,而不是简单的合并,因为这样能够保持线性的提交历史。如果你完成了一个特性的开发,并决定将它引入到主干代码中,你可以转到那个特性分支然后执行衍合命令,好在你的主干分支上(也可能是develop分支之类的)重新提交这些修改。如果这些代码工作得很好,你就可以快进master分支,得到一个线性的提交历史。 另一个引入代码的方法是挑拣。挑拣类似于针对某次特定提交的衍合。它首先提取某次提交的补丁,然后试着应用在当前分支上。如果某个特性分支上有多个 commits,但你只想引入其中之一就可以使用这种方法。也可能仅仅是因为你喜欢用挑拣,讨厌衍合。假设你有一个类似图 5-26 的工程。


技术分享图 5-26. 挑拣(cherry-pick)之前的历史 如果你希望拉取e43a6到你的主干分支,可以这样:

1
2
3
4
$ 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的代码,但是会得到不同的SHA-1值,因为应用日期不同。现在你的历史看起来像图 5-27.

 

技术分享
图 5-27. 挑拣(cherry-pick)之后的历史

现在,你可以删除这个特性分支并丢弃你不想引入的那些commit。

 

给发行版签名

你可以删除上次发布的版本并重新打标签,也可以像第二章所说的那样建立一个新的标签。如果你决定以维护者的身份给发行版签名,应该这样做:

1
2
3
4
5
6
$ git tag -s v1.5 -m ‘my signed 1.5 tag‘
You need a passphrase to unlock the secret key for
user: "Scott Chacon
 
     "
1024-bit DSA key, ID F721C45A, created 2009-02-09

完成签名之后,如何分发PGP公钥(public key)是个问题。(译者注:分发公钥是为了验证标签)。还好,Git的设计者想到了解决办法:可以把key(既公钥)作为blob变量写入Git库,然后把它的内容直接写在标签里。gpg --list-keys命令可以显示出你所拥有的key:

1
2
3
4
5
6
7
$ gpg --list-keys
/Users/schacon/.gnupg/pubring.gpg
---------------------------------
pub   1024D/F721C45A2009-02-09 [expires: 2010-02-09]
uid                  Scott Chacon
 
sub   2048g/45D022822009-02-09 [expires: 2010-02-09]

然后,导出key的内容并经由管道符传递给git hash-object,之后钥匙会以blob类型写入Git中,最后返回这个blob量的SHA-1值:

1
2
$ gpg -a --exportF721C45A | git hash-object -w --stdin
659ef797d181633c87ec71ac3f9ba29fe5775b92

现在你的Git已经包含了这个key的内容了,可以通过不同的SHA-1值指定不同的key来创建标签。

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

在运行git push --tags命令之后,maintainer-pgp-pub标签就会公布给所有人。如果有人想要校验标签,他可以使用如下命令导入你的key:

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

人们可以用这个key校验你签名的所有标签。另外,你也可以在标签信息里写入一个操作向导,用户只需要运行git show查看标签信息,然后按照你的向导就能完成校验。

 

生成内部版本号

因为Git不会为每次提交自动附加类似’v123’的递增序列,所以如果你想要得到一个便于理解的提交号可以运行git describe命令。Git将会返回一个字符串,由三部分组成:最近一次标定的版本号,加上自那次标定之后的提交次数,再加上一段SHA-1值of the commit you’re describing:

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

这个字符串可以作为快照的名字,方便人们理解。如果你的Git是你自己下载源码然后编译安装的,你会发现git --version命令的输出和这个字符串差不多。如果在一个刚刚打完标签的提交上运行describe命令,只会得到这次标定的版本号,而没有后面两项信息。

git describe命令只适用于有标注的标签(通过-a或者-s选项创建的标签),所以发行版的标签都应该是带有标注的,以保证git describe能够正确的执行。你也可以把这个字符串作为checkout或者show命令的目标,因为他们最终都依赖于一个简短的SHA-1值,当然如果这个SHA-1值失效他们也跟着失效。最近Linux内核为了保证SHA-1值的唯一性,将位数由8位扩展到10位,这就导致扩展之前的git describe输出完全失效了。

 

准备发布

现在可以发布一个新的版本了。首先要将代码的压缩包归档,方便那些可怜的还没有使用Git的人们。可以使用git archive

1
2
3
$ 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加上--format=zip选项:

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

现在你有了一个tar.gz压缩包和一个zip压缩包,可以把他们上传到你网站上或者用e-mail发给别人。

 

制作简报

是时候通知邮件列表里的朋友们来检验你的成果了。使用git shortlog命令可以方便快捷的制作一份修改日志(changelog),告诉大家上次发布之后又增加了哪些特性和修复了哪些bug。实际上这个命令能够统计给定范围内的所有提交;假如你上一次发布的版本是v1.0.1,下面的命令将给出自从上次发布之后的所有提交的简介:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git shortlog --no-merges master --not v1.0.1
Chris Wanstrath (8):
      Add support forannotated 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 inhistory
      dynamic version method
      Version bump to 1.0.2
      Regenerated gemspec forversion 1.0.2

这就是自从v1.0.1版本以来的所有提交的简介,内容按照作者分组,以便你能快速的发e-mail给他们。

 

5.4  小结

你学会了如何使用Git为项目做贡献,也学会了如何使用Git维护你的项目。恭喜!你已经成为一名高效的开发者。在下一篇你将学到更强大的工具来处理更加复杂的问题,之后你会变成一位Git大师。





Git专题--系统的学习Git之二

标签:

原文地址:http://blog.csdn.net/jackjia2015/article/details/51178868

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!