LOADING

加载过慢请开启缓存 浏览器默认开启

Git教程(3)

Git教程的最后一篇,应该吧

分布式工作流程

Git 的分布式特性使得开发者间的协作变得更加灵活多样。每个开发者同时扮演着节点和集线器的角色——也就是说,每个开发者既可以将自己的代码贡献到其他的仓库中,同时也能维护自己的公开仓库,让其他人可以在其基础上工作并贡献代码。

由此,Git 的分布式协作可以为你的项目和团队衍生出种种不同的工作流程

集中式工作流

集中式系统中通常使用的是单点协作模型——集中式工作流。一个中心集线器,或者说 仓库,可以接受代码,所有人将自己的工作与之同步。若干个开发者则作为节点,即中心仓库的消费者与中心仓库同步。

集中式工作流。

如果两个开发者从中心仓库克隆代码下来,同时作了一些修改,那么只有第一个开发者可以顺利地把数据推送回共享服务器。第二个开发者在推送修改之前,必须先将第一个人的工作合并进来,这样才不会覆盖第一个人的修改。

集成管理者工作流

Git 允许多个远程仓库存在,使得这样一种工作流成为可能:每个开发者拥有自己仓库的写权限和其他所有人仓库的读权限。

这种情形下通常会有个代表“官方”项目的权威的仓库。要为这个项目做贡献,你需要从该项目克隆出一个自己的公开仓库,然后将自己的修改推送上去。接着你可以请求官方仓库的维护者拉取更新合并到主项目。维护者可以将你的仓库作为远程仓库添加进来,在本地测试你的变更,将其合并入他们的分支并推送回官方仓库。

这一流程的工作方式如下所示(见下图):

  1. 项目维护者推送到主仓库。
  2. 贡献者克隆此仓库,做出修改。
  3. 贡献者将数据推送到自己的公开仓库。
  4. 贡献者给维护者发送邮件,请求拉取自己的更新。
  5. 维护者在自己本地的仓库中,将贡献者的仓库加为远程仓库并合并修改。
  6. 维护者将合并后的修改推送到主仓库。

集成管理者工作流。

forking
这是 GitHub 和 GitLab 等集线器式(hub-based)工具最常用的工作流程。人们可以容易地将某个项目派生成为自己的公开仓库,向这个仓库推送自己的修改,并为每个人所见。

这么做最主要的优点之一是你可以持续地工作,而主仓库的维护者可以随时拉取你的修改。
贡献者不必等待维护者处理完提交的更新——每一方都可以按照自己的节奏工作。

向一个项目贡献

决定如何向一个项目贡献的影响因素主要有三个:

  1. 活跃贡献者的数量(贡献代码的用户数量以及他们的贡献频率)
  2. 项目使用的工作流程
  3. 影响因素是提交权限

提交准则

有一个好的创建提交的准则并且坚持使用会让与 Git 工作和与其他人协作更容易。

首先,你的提交不应该包含任何空白错误

Git 提供了一个简单的方式来检查这点——在提交前,运行 git diff --check,它将会找到可能的空白错误并将它们为你列出来。

`git diff --check` 的输出

如果在提交前运行那个命令,可以知道提交中是否包含可能会使其他开发者恼怒的空白问题。

接下来,尝试让每一个提交成为一个逻辑上的独立变更集

尝试让改动可以理解——不要在整个周末编码解决五个问题,然后在周一时将它们提交为一个巨大的提交。即使在周末期间你无法提交,在周一时使用暂存区域将你的工作最少拆分为每个问题一个提交,并且为每一个提交附带一个有用的信息。

如果其中一些改动修改了同一个文件,尝试使用 git add --patch 来部分暂存文件。不管你做一个或五个提交,只要所有的改动都曾添加过,项目分支末端的快照就是一样的,所以尽量让你的开发者同事们在审查你的改动的时候更容易些吧。当你之后需要时这个方法也会使拉出或还原一个变更集更容易些。

最后一件要牢记的事是提交信息

有一个创建优质提交信息的习惯会使 Git 的使用与协作容易的多。
一般情况下,信息应当以少于 50 个字符(25个汉字)的单行开始且简要地描述变更,接着是一个空白行,再接着是一个更详细的解释。

Git 项目要求一个更详细的解释,包括做改动的动机和它的实现与之前行为的对比——这是一个值得遵循的好规则。使用指令式的语气来编写提交信息,比如使用“Fix bug”而非“Fixed bug”或“Fixes bug”。

这里是一份最初由 Tim Pope 写的模板

首字母大写的摘要(不多于 50 个字符)

如果必要的话,加入更详细的解释文字。在大概 72 个字符的时候换行。
在某些情形下,第一行被当作一封电子邮件的标题,剩下的文本作为正文。
分隔摘要与正文的空行是必须的(除非你完全省略正文),
如果你将两者混在一起,那么在使用例如变基这样的工具时,它们会生成难以阅读的输出,让人困惑。

使用指令式的语气来编写提交信息:使用“Fix bug”而非“Fixed bug”或“Fixes bug”。
此约定与 git merge 和 git revert 命令生成提交说明相同。

空行接着更进一步的段落。

- 标号也是可以的。

- 项目符号可以使用典型的连字符或星号,后跟一个空格,行之间用空行隔开,
  但是可以依据不同的惯例有所不同。

- 使用悬挂式缩进

如果你所有的提交信息都遵循此模版,那么对你和与你协作的其他开发者来说事情会变得非常容易。Git 项目有一个良好格式化的提交信息——尝试在那儿运行 git log --no-merges 来看看漂亮的格式化的项目提交历史像什么样。

私有小型团队

你可能会遇到的最简单的配置是有一两个其他开发者的私有项目。
“私有” 在这个上下文中,意味着闭源——不可以从外面的世界中访问到。
你和其他的开发者都有仓库的推送权限。

让我们看看当两个开发者在一个共享仓库中一起工作时会是什么样子。
第一个开发者,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

上方输出信息中最后一行显示的是推送操作执行完毕后返回的一条很有用的消息。
消息的基本格式是 <oldref>..<newref> fromref -> torefoldref 的含义是推送前所指向的引用, newref 的含义是推送后所指向的引用, fromref 是将要被推送的本地引用的名字, toref 是将要被更新的远程引用的名字。
在后面的讨论中你还会看到类似的输出消息,所以对这条消息的含义有一些基础的了解将会帮助你理解仓库的诸多状态。
想要了解更多细节请访问文档 git-push

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 会推送失败,因为之前 Jessica 已经推送了她的更改。
如果之前习惯于用 Subversion 那么理解这点特别重要,因为你会注意到两个开发者并没有编辑同一个文件。
尽管 Subversion 会对编辑的不同文件在服务器上自动进行一次合并,但 Git 要求你先在本地合并提交。
换言之,John 必须先抓取 Jessica 的上游改动并将它们合并到自己的本地仓库中,才能被允许推送。

第一步,John 抓取 Jessica 的工作(这只会 抓取 Jessica 的上游工作,并不会将它合并到 John 的工作中):

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

在这个时候,John 的本地仓库看起来像这样:

John 的分叉历史

现在 John 可以将抓取下来的 Jessica 的工作合并到他自己的本地工作中了:

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

合并进行得很顺利——John 更新后的历史现在看起来像这样:

合并了 `origin/master` 之后 John 的仓库

此时,John 可能想要测试新的代码,以确保 Jessica 的工作没有影响他自己的工作,
当一切正常后,他就能将新合并的工作推送到服务器了:

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

最终,John 的提交历史看起来像这样:

推送到 `origin` 服务器后 John 的历史

在此期间,Jessica 新建了一个名为 issue54 的主题分支,然后在该分支上提交了三次。
她还没有抓取 John 的改动,所以她的提交历史看起来像这样:

Jessica 的主题分支

忽然,Jessica 发现 John 向服务器推送了一些新的工作,她想要看一下,
于是就抓取了所有服务器上的新内容:

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

那会同时拉取 John 推送的工作。
Jessica 的历史现在看起来像这样:

抓取 John 的改动后 Jessica 的历史

Jessica 认为她的主题分支已经准备好了,但她想知道需要将 John 工作的哪些合并到自己的工作中才能推送。
她运行 git log 找了出来:

$ git log --no-merges issue54..origin/master
commit 738ee872852dfaa9d6634e0dea7a324040193016
Author: John Smith <jsmith@example.com>
Date:   Fri May 29 16:01:27 2009 -0700

   remove invalid default value

issue54..origin/master 语法是一个日志过滤器,要求 Git 只显示所有在后面分支
(在本例中是 origin/master)但不在前面分支(在本例中是 issue54)的提交的列表。
我们将会在相关章节中详细介绍这个语法。

目前,我们可以从输出中看到有一个 John 生成的但是 Jessica 还没有合并的提交。
如果她合并 origin/master,那个未合并的提交将会修改她的本地工作。

现在,Jessica 可以合并她的特性工作到她的 master 分支,
合并 John 的工作(origin/master)进入她的 master 分支,然后再次推送回服务器。

首先(在已经提交了所有 issue54 主题分支上的工作后),为了整合所有这些工作,
她切换回她的 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 ——它们都是上游,所以顺序并没有关系。
不论她选择的顺序是什么最终的结果快照是完全一样的;只是历史会稍微有些不同。
她选择先合并 issue54

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

没有发生问题,如你所见它是一次简单的快进合并。
现在 Jessica 在本地合并了之前抓取的 origin/master 分支上 John 的工作:

$ 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 的历史现在看起来像这样:

合并了 John 的改动后 Jessica 的历史

现在 origin/master 是可以从 Jessica 的 master 分支到达的,
所以她应该可以成功地推送(假设同一时间 John 并没有更多推送):

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

每一个开发者都提交了几次并成功地合并了其他人的工作。

推送所有的改动回服务器后 Jessica 的历史

这是一个最简单的工作流程。
你通常会在一个主题分支上工作一会儿,当它准备好整合时就合并到你的 master 分支。
当想要共享工作时,如果有改动的话就抓取它然后合并到你自己的 master 分支,
之后推送到服务器上的 master 分支。通常顺序像这样:

一个简单的多人 Git 工作流程的通常事件顺序

私有管理团队

在接下来的场景中,你会看到大型私有团队中贡献者的角色。你将学到如何在这种工作环境中工作,其中小组基于特性进行协作,而这些团队的贡献将会由其他人整合。

让我们假设 John 与 Jessica 在一个特性(featureA)上工作,
同时 Jessica 与第三个开发者 Josie 在第二个特性(featureB)上工作。
在本例中,公司使用了一种整合-管理者工作流程,独立小组的工作只能被特定的工程师整合,
主仓库的 master 分支只能被那些工程师更新。
在这种情况下,所有的工作都是在基于团队的分支上完成的并且稍后会被整合者拉到一起。

因为 Jessica 在两个特性上工作,并且平行地与两个不同的开发者协作,让我们跟随她的工作流程。
假设她已经克隆了仓库,首先决定在 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(-)

在这个时候,她需要将工作共享给 John,所以她推送了 featureA 分支的提交到服务器上。
Jessica 没有 master 分支的推送权限——只有整合者有——所以为了与 John 协作必须推送另一个分支。

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

Jessica 向 John 发邮件告诉他已经推送了一些工作到 featureA 分支现在可以看一看。
当她等待 John 的反馈时,Jessica 决定与 Josie 开始在 featureB 上工作。
为了开始工作,她基于服务器的 master 分支开始了一个新分支。

# 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 的仓库看起来像这样:

Jessica 的初始提交历史

她准备好推送工作了,但是一封来自 Josie 的邮件告知一些初始的“featureB”
工作已经被推送到服务器的 featureBee 上了。
Jessica 在能够将她的工作推送到服务器前,需要将那些改动与她自己的合并。
她首先通过 git fetch 抓取了 Josie 的改动:

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

假设 Jessica 还在她检出的 featureB 分支上,现在可以通过 git merge 将其合并到她做的工作中了:

$ 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”推送回服务器,但她并不想直接推送她自己的 featureB 分支。
由于 Josie 已经开启了一个上游的 featureBee 分支,因此 Jessica 想要推送到 这个 分支上,于是她这样做:

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

这称作一个 引用规范
查看相关章节了解关于 Git 引用规范与通过它们可以做的不同的事情的详细讨论。
也要注意 -u 标记;这是 --set-upstream 的简写,该标记会为之后轻松地推送与拉取配置分支。

紧接着,John 发邮件给 Jessica 说他已经推送了一些改动到 featureA 分支并要求她去验证它们。
她运行一个 git fetch 来拉取下那些改动:

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

Jessica 通过比较新抓取的 featureA 分支和她同一分支的本地副本,看到了 John 的新工作日志。

$ git log featureA..origin/featureA
commit aad881d154acdaeb2b6b18ea0e827ed8a6d671e6
Author: John Smith <jsmith@example.com>
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 可能想要对整个合并后的内容做一些小修改,
于是她将这些修改提交到了本地的 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 的提交历史现在看起来像这样:

在一个主题分支提交后 Jessica 的历史

这时,Jessica、Josie 与 John 通知整合者服务器上的 featureAfeatureBee 分支准备好整合到主线中了。
在整合者将这些分支合并到主线后,就能一次将这个新的合并提交抓取下来,历史看起来就会像这样:

合并了 Jessica 的两个主题分支后她的历史

许多团队切换到 Git 就是看中了这种能让多个团队并行工作、并在之后合并不同工作的能力。
团队中更小一些的子小组可以通过远程分支协作而不必影响或妨碍整个团队的能力是 Git 的一个巨大优势。
在这儿看到的工作流程顺序类似这样:

这种管理团队工作流程的基本顺序

派生的公开项目

向公开项目做贡献有一点儿不同。
因为没有权限直接更新项目的分支,你必须用其他办法将工作给维护者。
第一个例子描述在支持简单派生的 Git 托管上使用派生来做贡献。
许多托管站点支持这个功能(包括 GitHub、BitBucket、repo.or.cz 等等),许多项目维护者期望这种风格的贡献。
下一节会讨论偏好通过邮件接受贡献补丁的项目。

首先,你可能想要克隆主仓库,为计划贡献的补丁或补丁序列创建一个主题分支,然后在那儿做工作。
顺序看起来基本像这样:

$ git clone <url>
$ cd project
$ git checkout -b featureA
  ... work ...
$ git commit
  ... work ...
$ git commit

你可以用 rebase -i 将工作压缩成一个单独的提交,或者重排提交中的工作使补丁更容易被维护者审核——
查看相关章节了解关于交互式变基的更多信息。

当你的分支工作完成后准备将其贡献回维护者,去原始项目中然后点击“Fork”按钮,创建一份自己的可写的项目派生仓库。
然后需要在本地仓库中将该仓库添加为一个新的远程仓库,在本例中称作 myfork

$ git remote add myfork <url>

然后需要推送工作到上面。
相对于合并到主分支再推送上去,推送你正在工作的主题分支到仓库上更简单。
原因是工作如果不被接受或者是被拣选的,就不必回退你的 master 分支
(拣选操作 cherry-pick 详见相关章节)。
如果维护者合并、变基或拣选你的工作,不管怎样你最终会通过拉取他们的仓库找回来你的工作。

在任何情况下,你都可以使用下面的命令推送你的工作:

$ git push -u myfork featureA

git commands, request-pull
当工作已经被推送到你的派生仓库后,你需要通知原项目的维护者你有想要他们合并的工作。
这通常被称作一个 拉取请求(Pull Request),你通常可以通过网站生成它——
GitHub 有它自己的 Pull Request 机制,我们将会在相关章节介绍——也可以运行 git request-pull 命令然后将随后的输出通过电子邮件手动发送给项目维护者。

git request-pull 命令接受一个要拉取主题分支的基础分支,以及它们要拉取的 Git 仓库的 URL,
产生一个请求拉取的所有修改的摘要。
例如,Jessica 想要发送给 John 一个拉取请求,她已经在刚刚推送的分支上做了两次提交。她可以运行这个:

$ 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(-)

此输出可被发送给维护者——它告诉他们工作是从哪个分支开始的、提交的摘要、以及从哪里拉取这些工作。

在一个你不是维护者的项目上,通常有一个总是跟踪 origin/mastermaster 分支会很方便,在主题分支上做工作是因为如果它们被拒绝时你可以轻松地丢弃。
如果同一时间主仓库移动了然后你的提交不再能干净地应用,那么使工作主题独立于主题分支也会使你变基(rebase)工作时更容易。
例如,你想要提供第二个特性工作到项目,不要继续在刚刚推送的主题分支上工作——从主仓库的 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

现在,每一个特性都保存在一个贮藏库中——类似于补丁队列——可以重写、变基与修改而不会让特性互相干涉或互相依赖,像这样:

`featureB` 的初始提交历史

假设项目维护者已经拉取了一串其他补丁,然后尝试拉取你的第一个分支,但是没有干净地合并。
在这种情况下,可以尝试变基那个分支到 origin/master 的顶部,为维护者解决冲突,然后重新提交你的改动:

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

这样会重写你的历史,现在看起来像是下图:

`featureA` 工作之后的提交历史

因为你将分支变基了,所以必须为推送命令指定 -f 选项,这样才能将服务器上有一个不是它的后代的提交的 featureA 分支替换掉。
一个替代的选项是推送这个新工作到服务器上的一个不同分支(可能称作 featureAv2)。

让我们看一个更有可能的情况:维护者看到了你的第二个分支上的工作并且很喜欢其中的概念,但是想要你修改一下实现的细节。
你也可以利用这次机会将工作基于项目现在的 master 分支。
你从现在的 origin/master 分支开始一个新分支,在那儿压缩 featureB 的改动,解决任何冲突,改变实现,然后推送它为一个新分支。

git commands, merge, squash

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

--squash 选项接受被合并的分支上的所有工作,并将其压缩至一个变更集,
使仓库变成一个真正的合并发生的状态,而不会真的生成一个合并提交。
这意味着你的未来的提交将会只有一个父提交,并允许你引入另一个分支的所有改动,
然后在记录一个新提交前做更多的改动。同样 --no-commit 选项在默认合并过程中可以用来延迟生成合并提交。

现在你可以给维护者发送一条消息,表示你已经做了要求的修改然后他们可以在你的 featureBv2 分支上找到那些改动。

`featureBv2` 工作之后的提交历史

通过邮件的公开项目

许多项目建立了接受补丁的流程——需要检查每一个项目的特定规则,因为它们之间有区别。
因为有几个历史悠久的、大型的项目会通过一个开发者的邮件列表接受补丁,现在我们将会通过一个例子来演示。

工作流程与之前的用例是类似的——你为工作的每一个补丁序列创建主题分支。
区别是如何提交它们到项目中。
生成每一个提交序列的电子邮件版本然后邮寄它们到开发者邮件列表,而不是派生项目然后推送到你自己的可写版本。

$ git checkout -b topicA
  ... work ...
$ git commit
  ... work ...
$ git commit

git commands, format-patch
现在有两个提交要发送到邮件列表。
使用 git format-patch 来生成可以邮寄到列表的 mbox 格式的文件——它将每一个提交转换为一封电子邮件,提交信息的第一行作为主题,剩余信息与提交引入的补丁作为正文。
它有一个好处是使用 format-patch 生成的一封电子邮件应用的提交正确地保留了所有的提交信息。

$ 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 <jessica@example.com>
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 提供了一个工具帮助你通过 IMAP 发送正确格式化的补丁,这可能对你更容易些。
我们将会演示如何通过 Gmail 发送一个补丁,它正好是我们所知最好的邮件代理;可以在之前提到的 Git 源代码中的 Documentation/SubmittingPatches 文件的最下面了解一系列邮件程序的详细指令。

git commands, config email
首先,需要在 ~/.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,最后两行可能没有必要,host 的值会是 imap:// 而不是 imaps://
当那些设置完成后,可以使用 git imap-send 将补丁序列放在特定 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 服务器发送补丁。
同之前一样,你可以通过一系列的 git config 命令来分别设置选项,
或者你可以手动地将它们添加到你的 ~/.gitconfig 文件的 sendmail 区块:

[sendemail]
  smtpencryption = tls
  smtpserver = smtp.gmail.com
  smtpuser = user@gmail.com
  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 <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 会吐出这样的一串日志信息:

(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

维护项目

除了如何有效地参与一个项目的贡献之外,你可能也需要了解如何维护项目。
这包含接受并应用别人使用 format-patch 生成并通过电子邮件发送过来的补丁,
或对项目添加的远程版本库分支中的更改进行整合。
但无论是管理版本库,还是帮忙验证、审核收到的补丁,都需要同其他贡献者约定某种长期可持续的工作方式。

在主题分支中工作

如果你想向项目中整合一些新东西,最好将这些尝试局限在 主题分支——一种通常用来尝试新东西的临时分支中。
这样便于单独调整补丁,如果遇到无法正常工作的情况,可以先不用管,等到有时间的时候再来处理。
如果你基于你所尝试进行工作的特性为分支创建一个简单的名字,比如 ruby_client 或者具有类似描述性的其他名字,这样即使你必须暂时抛弃它,以后回来时也不会忘记。
项目的维护者一般还会为这些分支附带命名空间,比如 sc/ruby_client(其中 sc 是贡献该项工作的人名称的简写)。
你应该记得,可以使用如下方式基于 master 分支建立主题分支:

$ git branch sc/ruby_client master

或者如果你同时想立刻切换到新分支上的话,可以使用 checkout -b 选项:

$ git checkout -b sc/ruby_client master

现在你已经准备好将你收到的贡献加入到这个主题分支,并考虑是否将其合并到长期分支中去了。

应用来自邮件的补丁

如果你通过电子邮件收到了一个需要整合进入项目的补丁,你需要将其应用到主题分支中进行评估。
有两种应用该种补丁的方法:使用 git apply,或者使用 git am

使用 apply 命令应用补丁

如果你收到了一个使用 git diff 或 Unix diff 命令的变体(不推荐使用这种方式,具体见下一节)
创建的补丁,可以使用 git apply 命令来应用。
假设你将补丁保存在了 /tmp/patch-ruby-client.patch 中,可以这样应用补丁:

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

这会修改工作目录中的文件。
它与运行 patch -p1 命令来应用补丁几乎是等效的,但是这种方式更加严格,相对于 patch 来说,它能够接受的模糊匹配更少。
它也能够处理 git diff 格式文件所描述的文件添加、删除和重命名操作,而 patch 则不会。
最后,git apply 命令采用了一种“全部应用,否则就全部撤销(apply all or abort all)”的模型,
即补丁只有全部内容都被应用和完全不被应用两个状态,而 patch 可能会导致补丁文件被部分应用,
最后使你的工作目录保持在一个比较奇怪的状态。
总体来看,git apply 命令要比 patch 谨慎得多。
并且,它不会为你创建提交——在运行之后,你需要手动暂存并提交补丁所引入的更改。

在实际应用补丁前,你还可以使用 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

如果没有产生输出,则该补丁可以顺利应用。
如果检查失败了,该命令还会以一个非零的状态退出,所以需要时你也可以在脚本中使用它。

使用 am 命令应用补丁

如果补丁的贡献者也是一个 Git 用户,并且其能熟练使用 format-patch 命令来生成补丁,这样的话你的工作会变得更加轻松,因为这种补丁中包含了作者信息和提交信息供你参考。
如果可能的话,请鼓励贡献者使用 format-patch 而不是 diff 来为你生成补丁。
而只有对老式的补丁,你才必须使用 git apply 命令。

要应用一个由 format-patch 命令生成的补丁,你应该使用 git am 命令
(该命令的名字 am 表示它“应用(Apply)一系列来自邮箱(Mailbox)的补丁”)。
从技术的角度看,git am 是为了读取 mbox 文件而构建的,
mbox 是一种用来在单个文本文件中存储一个或多个电子邮件消息的简单纯文本格式。
其大致格式如下所示:

From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001
From: Jessica Smith <jessica@example.com>
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 电子邮件格式。
如果有人使用 git send-email 命令将补丁以电子邮件的形式发送给你,
你便可以将它下载为 mbox 格式的文件,之后将 git am 命令指向该文件,它会应用其中包含的所有补丁。
如果你所使用的邮件客户端能够同时将多封邮件保存为 mbox 格式的文件,
你甚至能够将一系列补丁打包为单个 mbox 文件,并利用 git am 命令将它们一次性全部应用。

然而,如果贡献者将 git format-patch 生成的补丁文件上传到工单系统或类似的任务处理系统,
你可以先将其保存到本地,之后通过 git am 来应用补丁:

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

你会看到补丁被顺利地应用,并且为你自动创建了一个新的提交。
其中的作者信息来自于电子邮件头部的 FromDate 字段,提交消息则取自 Subject 和邮件正文中补丁之前的内容。
比如,应用上面那个 mbox 示例后生成的提交是这样的:

$ git log --pretty=fuller -1
commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Author:     Jessica Smith <jessica@example.com>
AuthorDate: Sun Apr 6 10:17:23 2008 -0700
Commit:     Scott Chacon <schacon@gmail.com>
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 进程将会报错并且询问你要做什么:

$ 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 --resolved 继续应用下一个补丁:

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

如果你希望 Git 能够尝试以更加智能的方式解决冲突,你可以对其传递 -3 选项来使 Git 尝试进行三方合并。
该选项默认并没有打开,因为如果用于创建补丁的提交并不在你的版本库内的话,这样做是没有用处的。
而如果你确实有那个提交的话——比如补丁是基于某个公共提交的——那么通常 -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 选项的话,这看起来就像是存在一个冲突。
由于使用了 -3 选项,该补丁就被干净地应用了

如果你正在利用一个 mbox 文件应用多个补丁,也可以在交互模式下运行 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

如果她再次发邮件说另一个分支中包含另一个优秀功能,因为之前已经设置好远程分支了,
你就可以直接进行 fetchcheckout 操作。

这对于与他人长期合作工作来说很有用。
而对于提交补丁频率较小的贡献者,相对于每个人维护自己的服务器,不断增删远程分支的做法,使用电子邮件来接收可能会比较省时。
况且你也不会想要加入数百个只提供一两个补丁的远程分支。
然而,脚本和托管服务在一定程度上可以简化这些工作——这很大程度上依赖于你和你的贡献者开发的方式。

这种方式的另一种优点是你可以同时得到提交历史。
虽然代码合并中可能会出现问题,但是你能获知他人的工作是基于你的历史中的具体哪一个位置;所以 Git 会默认进行三方合并,不需要提供 -3 选项,你也不需要担心补丁是基于某个你无法访问的提交生成的。

对于非持续性的合作,如果你依然想要以这种方式拉取数据的话,你可以对远程版本库的 URL 调用 git pull 命令。
这会执行一个一次性的抓取,而不会将该 URL 存为远程引用:

$ 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 分支中的提交。
这和我们之前使用的 master..contrib 格式是一样的。
假设贡献者向你发送了两个补丁,为此你创建了一个名叫 contrib 的分支并在其上应用补丁,你可以运行:

$ git log contrib --not master
commit 5b6235bd297351589efc4d73316f0a68d484f118
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Oct 24 09:53:59 2008 -0700

    seeing if this helps the gem

commit 7482e0d16d04bea79d0dba8988cc78df655f16a0
Author: Scott Chacon <schacon@gmail.com>
Date:   Mon Oct 22 19:38:36 2008 -0700

    updated the gemspec to hopefully work better

如果要查看每次提交所引入的具体修改,你应该记得可以给 git log 命令传递 -p 选项,这样它会在每次提交后面附加对应的差异(diff)。

而要查看将该主题分支与另一个分支合并的完整 diff,你可能需要使用一个有些奇怪的技巧来得到正确的结果。
你可能会想到这种方式:

$ git diff master

这个命令会输出一个 diff,但它可能并不是我们想要的。
如果在你创建主题分支之后,master 分支向前移动了,你获得的结果就会显得有些不对。
这是因为 Git 会直接将该主题分支与 master 分支的最新提交快照进行比较。
比如说你在 master 分支中向某个文件添加了一行内容,那么直接比对最新快照的结果看上去就像是你在主题分支中将这一行删除了。

如果 master 分支是你的主题分支的直接祖先,其实是没有任何问题的;
但是一旦两个分支的历史产生了分叉,上述比对产生的 diff 看上去就像是将主题分支中所有的新东西加入,
并且将 master 分支所独有的东西删除。

而你真正想要检查的东西,实际上仅仅是主题分支所添加的更改——也就是该分支与 master 分支合并所要引入的工作。
要达到此目的,你需要让 Git 对主题分支上最新的提交与该分支与 master 分支的首个公共祖先进行比较。

从技术的角度讲,你可以以手工的方式找出公共祖先,并对其显式运行 diff 命令:

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

或者,更简洁的形式:

$ git diff $(git merge-base contrib master)

然而,这种做法比较麻烦,所以 Git 提供了一种比较便捷的方式:三点语法。
对于 git diff 命令来说,你可以通过把 ... 置于另一个分支名后来对该分支的最新提交与两个分支的共同祖先进行比较:

$ git diff master...contrib

该命令仅会显示自当前主题分支与 master 分支的共同祖先起,该分支中的工作。
这个语法很有用,应该牢记。

将贡献的工作整合进来

当主题分支中所有的工作都已经准备好整合进入更靠近主线的分支时,接下来的问题就是如何进行整合了。
此外,还有一个问题是,你想使用怎样的总体工作流来维护你的项目?
你的选择有很多,我们会介绍其中的一部分。

合并工作流

一种基本的工作流就是将所有的工作直接合并到 master 分支。
在这种情况下,master 分支包含的代码是基本稳定的。
当你完成某个主题分支的工作,或审核通过了其他人所贡献的工作时,你会将其合并进入 master 分支,之后将主题分支删除,如此反复。

举例来说,如果我们的版本库包含类似下图的两个名称分别为 ruby_clientphp_client 的分支,
并且我们合并完 ruby_client 分支后,再合并 php_client 分支,那么提交历史最后会变成下图的例子。

包含若干主题分支的提交历史。

合并主题分支之后。

这也许是最简单的工作流了,但是当项目更大,或更稳定,你对自己所引入的工作更加在意时,它可能会带来问题。

如果你的项目非常重要,你可能会使用两阶段合并循环。
在这种情况下,你会维护两个长期分支,分别是 masterdevelopmaster 分支只会在一个非常稳定的版本发布时才会更新,而所有的新代码会首先整合进入 develop 分支。
你定期将这两个分支推送到公共版本库中。
每次需要合并新的主题分支时(见下图),你都应该合并进入 develop 分支(见下图);当打标签发布的时候,你会将 master 分支快进到已经稳定的 develop 分支(见下图)。

合并主题分支前。

合并主题分支后。

一次发布之后。

这样当人们克隆你项目的版本库后,既可以检出 master 分支以构建最新的稳定版本并保持更新,
也可以检出包含更多前沿内容 develop 分支。
你也可以扩展这个概念,维护一个将所有工作合并到一起的整合分支。
当该分支的代码稳定并通过测试之后,将其合并进入 develop 分支;
经过一段时间,确认其稳定之后,将其以快进的形式并入 master 分支。

大项目合并工作流

Git 项目包含四个长期分支:masternext,用于新工作的 pu(proposed updates)和用于维护性向后移植工作(maintenance backports)的 maint 分支。
贡献者的新工作会以类似之前所介绍的方式收入主题分支中(见下图)。
之后对主题分支进行测试评估,检查其是否已经能够合并,或者仍需要更多工作。
安全的主题分支会被合并入 next 分支,之后该分支会被推送使得所有人都可以尝试整合到一起的特性。

管理复杂的一系列接收贡献的平行主题分支。

如果主题分支需要更多工作,它则会被并入 pu 分支。
当它们完全稳定之后,会被再次并入 master 分支。
这意味着 master 分支始终在进行快进,next 分支偶尔会被变基,而 pu 分支的变基比较频繁:

将贡献的主题分支并入长期整合分支。

当主题分支最终被并入 master 分支后,便会被从版本库中删除掉。
Git 项目还有一个从上一次发布中派生出来的 maint 分支来提供向后移植过来的补丁以供发布维护更新。
因此,当你克隆 Git 的版本库之后,就会有四个可分别评估该项目开发的不同阶段的可检出的分支,
检出哪个分支,取决于你需要多新的版本,或者你想要如何进行贡献;
对于维护者来说,这套结构化的工作流能帮助它们审查新的贡献。
Git 项目的工作流是特别的。要清晰地理解它,请阅读
Git 维护者手册

变基与拣选工作流

为了保持线性的提交历史,有些维护者更喜欢在 master 分支上对贡献过来的工作进行变基和拣选,而不是直接将其合并。
当你完成了某个主题分支中的工作,并且决定要将其整合的时候,你可以在该分支中运行变基命令,
在当前 master 分支(或者是 develop 等分支)的基础上重新构造修改。
如果结果理想的话,你可以快进 master 分支,最后得到一个线性的项目提交历史。

另一种将引入的工作转移到其他分支的方法是拣选。
Git 中的拣选类似于对特定的某次提交的变基。
它会提取该提交的补丁,之后尝试将其重新应用到当前分支上。
这种方式在你只想引入主题分支中的某个提交,或者主题分支中只有一个提交,而你不想运行变基时很有用。
举个例子,假设你的项目提交历史类似:

拣选之前的示例历史。

如果你希望将提交 e43a6 拉取到 master 分支,你可以运行:

$ git cherry-pick e43a6
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 值。
现在你的历史会变成这样:

拣选主题分支中的一个提交后的历史。

现在你可以删除这个主题分支,并丢弃不想拉入的提交。

Rerere

如果你在进行大量的合并或变基,或维护一个长期的主题分支,Git 提供的一个叫做“rerere”的功能会有一些帮助。

Rerere 是“重用已记录的冲突解决方案(reuse recorded resolution)”的意思——它是一种简化冲突解决的方法。
当启用 rerere 时,Git 将会维护一些成功合并之前和之后的镜像,当 Git 发现之前已经修复过类似的冲突时,
便会使用之前的修复方案,而不需要你的干预。

这个功能包含两个部分:一个配置选项和一个命令。
其中的配置选项是 rerere.enabled,把它放在全局配置中就可以了:

$ git config --global rerere.enabled true

现在每当你进行一次需要解决冲突的合并时,解决方案都会被记录在缓存中,以备之后使用。

如果你需要和 rerere 的缓存交互,你可以使用 git rerere 命令。
当单独调用它时,Git 会检查解决方案数据库,尝试寻找一个和当前任一冲突相关的匹配项并解决冲突
(尽管当 rerere.enabled 被设置为 true 时会自动进行)。
它也有若干子命令,可用来查看记录项,删除特定解决方案和清除缓存全部内容等。
我们将在相关章节中详细探讨。

为发布打标签

当你决定进行一次发布时,你可能想要打一个标签,这样在之后的任何一个提交点都可以重新创建该发布。
你在相关章节中已经了解了创建新标签的过程。
作为一个维护者,如果你决定要为标签签名的话,打标签的过程应该是这样子的:

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

如果你为标签签名了,你可能会遇到分发用来签名的 PGP 公钥的问题。
Git 项目的维护者已经解决了这一问题,其方法是在版本库中以 blob 对象的形式包含他们的公钥,并添加一个直接指向该内容的标签。
要完成这一任务,首先你可以通过运行 gpg --list-keys 找出你所想要的 key:

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

之后你可以通过导出 key 并通过管道传递给 git hash-object 来直接将 key 导入到 Git 的数据库中,git hash-object 命令会向 Git 中写入一个包含其内容的新 blob 对象,并向你返回该 blob 对象的 SHA-1 值:

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

既然 Git 中已经包含你的 key 的内容了,你就可以通过指定由 hash-object 命令给出的新 SHA-1 值来创建一个直接指向它的标签:

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

如果你运行 git push --tags 命令,那么 maintainer-pgp-pub 标签将会被共享给所有人。
需要校验标签的人可以通过从数据库中直接拉取 blob 对象并导入到 GPG 中来导入 PGP key:

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

人们可以使用这个 key 来校验所有由你签名的标签。
另外,如果你在标签信息中包含了一些操作说明,用户可以通过运行 git show <tag> 来获取更多关于标签校验的说明。

生成一个构建号

Git 中不存在随每次提交递增的“v123”之类的数字序列,如果你想要为提交附上一个可读的名称,
可以对其运行 git describe 命令。作为回应,Git 将会生成一个字符串,
它由最近的标签名、自该标签之后的提交数目和你所描述的提交的部分 SHA-1 值(前缀的 g 表示 Git)构成:

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

这样你在导出一个快照或构建时,可以给出一个便于人们理解的命名。
实际上,如果你的 Git 是从 Git 自己的版本库克隆下来并构建的,那么 git --version 命令给出的结果是与此类似的。
如果你所描述的提交自身就有一个标签,那么它将只会输出标签名,没有后面两项信息。

默认情况下, git describe 命令需要有注解的标签(即使用 -a-s 选项创建的标签);
如果你想使用轻量标签(无注解的标签),请在命令后添加 --tags 选项。
你也可以使用这个字符串来调用 git checkoutgit show 命令,
但是这依赖于其末尾的简短 SHA-1 值,因此不一定一直有效。
比如,最近 Linux 内核为了保证 SHA-1 值对象的唯一性,将其位数由 8 位扩展到了 10 位,
导致以前的 git describe 输出全部失效。

准备一次发布

现在你可以发布一个构建了。
其中一件事情就是为那些不使用 Git 的可怜包们创建一个最新的快照归档。
使用 git archive 命令完成此工作:

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

如果有人将这个压缩包解压,他就可以在一个 project 目录中得到你项目的最新快照。
你也可以以类似的方式创建一个 zip 压缩包,但此时你应该向 git archive 命令传递 --format=zip 选项:

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

现在你有了本次发布的一个 tar 包和一个 zip 包,可以将其上传到网站或以电子邮件的形式发送给人们。

制作提交简报

现在是时候通知邮件列表里那些好奇你的项目发生了什么的人了。
使用 git shortlog 命令可以快速生成一份包含从上次发布之后项目新增内容的修改日志(changelog)类文档。
它会对你给定范围内的所有提交进行总结;比如,你的上一次发布名称是 v1.0.1,那么下面的命令可以给出上次发布以来所有提交的总结:

$ git shortlog --no-merges master --not v1.0.1
Chris Wanstrath (6):
      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 以来的所有提交,并且已经按照作者分好组,你可以通过电子邮件将其直接发送到列表中。

Github教程

生成 SSH 公钥

许多 Git 服务器都使用 SSH 公钥进行认证。
为了向 Git 服务器提供 SSH 公钥,如果某系统用户尚未拥有密钥,必须事先为其生成一份。
这个过程在所有操作系统上都是相似的。
首先,你需要确认自己是否已经拥有密钥。
默认情况下,用户的 SSH 密钥存储在其 ~/.ssh 目录下。
进入该目录并列出其中内容,你便可以快速确认自己是否已拥有密钥:

$ cd ~/.ssh
$ ls
authorized_keys2  id_dsa       known_hosts
config            id_dsa.pub

我们需要寻找一对以 id_dsaid_rsa 命名的文件,其中一个带有 .pub 扩展名。
.pub 文件是你的公钥,另一个则是与之对应的私钥。
如果找不到这样的文件(或者根本没有 .ssh 目录),你可以通过运行 ssh-keygen 程序来创建它们。
在 Linux/macOS 系统中,ssh-keygen 随 SSH 软件包提供;在 Windows 上,该程序包含于 MSysGit 软件包中。

$ ssh-keygen -o
Generating public/private rsa key pair.
Enter file in which to save the key (/home/schacon/.ssh/id_rsa):
Created directory '/home/schacon/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/schacon/.ssh/id_rsa.
Your public key has been saved in /home/schacon/.ssh/id_rsa.pub.
The key fingerprint is:
d0:82:24:8e:d7:f1:bb:9b:33:53:96:93:49:da:9b:e3 schacon@mylaptop.local

首先 ssh-keygen 会确认密钥的存储位置(默认是 .ssh/id_rsa),然后它会要求你输入两次密钥口令。
如果你不想在使用密钥时输入口令,将其留空即可。
然而,如果你使用了密码,那么请确保添加了 -o 选项,它会以比默认格式更能抗暴力破解的格式保存私钥。
你也可以用 ssh-agent 工具来避免每次都要输入密码。

现在,进行了上述操作的用户需要将各自的公钥发送给任意一个 Git 服务器管理员
(假设服务器正在使用基于公钥的 SSH 验证设置)。
他们所要做的就是复制各自的 .pub 文件内容,并将其通过邮件发送。
公钥看起来是这样的:

$ 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@mylaptop.local

关于在多种操作系统中生成 SSH 密钥的更深入教程,请参阅 GitHub 的 SSH 密钥指南
https://docs.github.com/cn/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent

账户的创建和配置

你所需要做的第一件事是创建一个免费账户。
直接访问 https://github.com,选择一个未被占用的用户名,提供一个电子邮件地址和密码,点击写着“Sign up for GitHub”的绿色大按钮即可。

GitHub 注册表单。

你将看到的下一个页面是升级计划的价格页面,目前我们可以直接忽略这个页面。
GitHub 会给你提供的邮件地址发送一封验证邮件。
尽快到你的邮箱进行验证,这是非常重要的(我们会在后面了解到这点)。

GitHub 为免费账户提供了几乎所有的功能,除了一些高级的特性。

GitHub 的付费计划包含一些高级工具和功能,不过本书将不涉及这部分内容。
关于可选方案及其对比的更多信息见 https://github.com/pricing

点击屏幕左上角的章鱼猫(Octocat)图标,你将来到控制面板页面。
现在,你已经做好了使用 GitHub 的准备工作。

SSH 访问

SSH keys, with GitHub
现在,你完全可以使用 https:// 协议,通过你刚刚创建的用户名和密码访问 Git 版本库。
但是,如果仅仅克隆公有项目,你甚至不需要注册——刚刚我们创建的账户是为了以后 fork 其它项目,以及推送我们自己的修改。

如果你习惯使用 SSH 远程,你需要配置一个公钥。
(如果你没有公钥,参考相关章节。)
使用窗口右上角的链接打开你的账户设置:

“Account settings”链接。

然后在左侧选择“SSH keys”部分。

“SSH keys”链接。

在这个页面点击“Add an SSH key”按钮,给你的公钥起一个名字,将你的 ~/.ssh/id_rsa.pub
(或者自定义的其它名字)公钥文件的内容粘贴到文本区,然后点击“Add key”。

确保给你的 SSH 密钥起一个能够记得住的名字。
你可以为每一个密钥起名字(例如,“我的笔记本电脑”或者“工作账户”等),以便以后需要吊销密钥时能够方便地区分。

头像

下一步,如果愿意的话,你可以将生成的头像换成你喜欢的图片。
首先,来到“Profile”标签页(在“SSH Keys”标签页上方),点击“Upload new picture”。

“Profile”链接。

我们选择了本地磁盘上的一个 Git 图标,上传之后还可以对其进行裁剪。

裁剪头像

现在,在网站任意有你参与的位置,人们都可以在你的用户名旁边看到你的头像。

如果你已经把头像上传到了流行的 Gravatar 托管服务(Wordpress 账户经常使用),默认就会使用这个头像,因此,你就不需要进行这一步骤了。

邮件地址

GitHub 使用用户邮件地址区分 Git 提交。
如果你在自己的提交中使用了多个邮件地址,希望 GitHub 可以正确地将它们连接起来,
你需要在管理页面的 Emails 部分添加你拥有的所有邮箱地址。

添加邮件地址

在上图中我们可以看到一些不同的状态。
顶部的地址是通过验证的,并且被设置为主要地址,这意味着该地址会接收到所有的通知和回复。
第二个地址是通过验证的,如果愿意的话,可以将其设置为主要地址。
最后一个地址是未通过验证的,这意味着你不能将其设置为主要地址。
当 GitHub 发现任意版本库中的任意提交信息包含了这些地址,它就会将其链接到你的账户。

两步验证

最后,为了额外的安全性,你绝对应当设置两步验证,简写为 “2FA”。
两步验证是一种用于降低因你的密码被盗而带来的账户风险的验证机制,现在已经变得越来越流行。
开启两步验证,GitHub 会要求你用两种不同的验证方法,这样,即使其中一个被攻破,攻击者也不能访问你的账户。

你可以在 Account settings 页面的 Security 标签页中找到 Two-factor Authentication 设置。

Security 标签页中的 2FA

点击“Set up two-factor authentication”按钮,会跳转到设置页面。该页面允许你选择是要在登录时使用手机 app 生成辅助码(一种“基于时间的一次性密码”),还是要 GitHub 通过 SMS 发送辅助码。

选择合适的方法后,按照提示步骤设置 2FA,你的账户会变得更安全,每次登录 GitHub 时都需要提供除密码以外的辅助码。

对项目做出贡献

账户已经建立好了,现在我们来了解一些能帮助你对现有的项目做出贡献的知识。

派生项目

forking
如果你想要参与某个项目,但是并没有推送权限,这时可以对这个项目进行“派生(Fork)”。
当你“派生”一个项目时,GitHub 会在你的空间中创建一个完全属于你的项目副本,且你对其具有推送权限。

在以前,“fork”是一个贬义词,指的是某个人使开源项目向不同的方向发展,或者创建一个竞争项目,使得原项目的贡献者分裂。
在 GitHub,“fork”指的是你自己的空间中创建的项目副本,这个副本允许你以一种更开放的方式对其进行修改。

通过这种方式,项目的管理者不再需要忙着把用户添加到贡献者列表并给予他们推送权限。
人们可以派生这个项目,将修改推送到派生出的项目副本中,并通过创建拉取请求(Pull Request,简称 PR)来让他们的改动进入源版本库,下文我们会详细说明。
创建了拉取请求后,就会开启一个可供审查代码的板块,项目的拥有者和贡献者可以在此讨论相关修改,直到项目拥有者对其感到满意,并且认为这些修改可以被合并到版本库。

你可以通过点击项目页面右上角的“Fork”按钮,来派生这个项目。

"Fork"按钮

稍等片刻,你将被转到新项目页面,该项目包含可写的代码副本。

GitHub 流程

GitHub 设计了一个以拉取请求为中心的特殊合作流程。
它基于我们在相关章节中提到的工作流程。
不管你是在一个紧密的团队中使用单独的版本库,或者使用许多的“Fork”来为一个由陌生人组成的国际企业或网络做出贡献,这种合作流程都能应付。

流程通常如下:

  1. 派生一个项目
  2. master 分支创建一个新分支
  3. 提交一些修改来改进项目
  4. 将这个分支推送到 GitHub 上
  5. 创建一个拉取请求
  6. 讨论,根据实际情况继续修改
  7. 项目的拥有者合并或关闭你的拉取请求
  8. 将更新后的 master 分支同步到你的派生中

这基本和相关章节中的一体化管理流程差不多,但是团队可以使用 GitHub 提供的网页工具替代电子邮件来交流和审查修改。

现在我们来看一个使用这个流程的例子。

创建拉取请求

Tony 在找一些能在他的 Arduino 微控制器上运行的代码,他觉得 https://github.com/schacon/blink 中的代码不错。

他想要做出贡献的项目

但是有个问题,这个代码中的闪烁频率太高,我们觉得 3 秒一次比 1 秒一次更好一些。
所以让我们来改进这个程序,并将修改后的代码提交给这个项目。

首先,单击“Fork”按钮来获得这个项目的副本。
我们使用的用户名是“tonychacon”,所以这个项目副本的访问地址是: https://github.com/tonychacon/blink
我们将它克隆到本地,创建一个分支,修改代码,最后再将改动推送到 GitHub。

$ git clone https://github.com/tonychacon/blink <1>
Cloning into 'blink'...

$ cd blink
$ git checkout -b slow-blink <2>
Switched to a new branch 'slow-blink'

$ sed -i '' 's/1000/3000/' blink.ino (macOS) <3>
# If you're on a Linux system, do this instead:
# $ sed -i 's/1000/3000/' blink.ino <3>

$ git diff --word-diff <4>
diff --git a/blink.ino b/blink.ino
index 15b9911..a6cc5a5 100644
--- a/blink.ino
+++ b/blink.ino
@@ -18,7 +18,7 @@ void setup() {
// the loop routine runs over and over again forever:
void loop() {
  digitalWrite(led, HIGH);   // turn the LED on (HIGH is the voltage level)
  [-delay(1000);-]{+delay(3000);+}               // wait for a second
  digitalWrite(led, LOW);    // turn the LED off by making the voltage LOW
  [-delay(1000);-]{+delay(3000);+}               // wait for a second
}

$ git commit -a -m 'three seconds is better' <5>
[slow-blink 5ca509d] three seconds is better
 1 file changed, 2 insertions(+), 2 deletions(-)

$ git push origin slow-blink <6>
Username for 'https://github.com': tonychacon
Password for 'https://tonychacon@github.com':
Counting objects: 5, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 340 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://github.com/tonychacon/blink
 * [new branch]      slow-blink -> slow-blink

<1> 将派生出的副本克隆到本地
<2> 创建出名称有意义的分支
<3> 修改代码
<4> 检查改动
<5> 将改动提交到分支中
<6> 将新分支推送到 GitHub 的副本中

现在到 GitHub 上查看之前的项目副本,可以看到 GitHub 提示我们有新的分支,
并且显示了一个大大的绿色按钮让我们可以检查我们的改动,并给源项目创建拉取请求。

你也可以到“Branches”(分支)页面查看分支并创建拉取请求: \https://github.com/<用户名>/<项目名>/branches

拉取请求按钮

GitHub, pull requests
如果我们点击那个绿色按钮,就会跳到一个新页面,在这里我们可以为拉取请求填写标题和描述。
花点时间编写一个清晰有用的描述是非常值得的,这能让原项目拥有者明白你做了什么,
为什么这个改动是正确的,以及接受此更改是否能够改进他的项目。

同时我们也能看到比主分支中所“领先”(ahead)的提交(在这个例子中只有一个)以及所有将会被合并的改动与之前代码的对比。

拉取请求创建页面

当你单击了“Create pull request”(创建拉取请求)的按钮后,这个项目的拥有者将会收到一条包含改动和拉取请求页面的链接的提醒。

虽然拉取请求通常是在贡献者准备好在公开项目中提交改动的时候提交,但是也常被用在仍处于开发阶段的内部项目中。
因为拉取请求在提交后 依然可以加入新的改动 ,它也经常被用来建立团队合作的环境,而不只是在最终阶段使用。

利用拉取请求

现在,项目的拥有者可以看到你的改动并合并它,拒绝它或是发表评论。
在这里我们就当作他喜欢这个点子,但是他想要让灯熄灭的时间比点亮的时间稍长一些。

接下来可能会通过电子邮件进行互动,就像我们在相关章节中提到的工作流程那样,但是在 GitHub,这些都在线上完成。
项目的拥有者可以审查修改,只需要单击某一行,就可以对其发表评论。

对拉取请求内的特定一行发表评论

当维护者发表评论后,提交拉取请求的人,以及所有正在关注(Watching)这个版本库的用户都会收到通知。
我们待会儿将会告诉你如何修改这项设置。现在,如果 Tony 有开启电子邮件提醒,他将会收到这样的一封邮件:

通过电子邮件发送的评论提醒

每个人都能在拉取请求中发表评论。在下图中我们可以看到项目拥有者对某行代码发表评论,
并在讨论区留下了一个普通评论。你可以看到被评论的代码也会在互动中显示出来。

拉取请求讨论页面

现在贡献者可以看到如何做才能让他们的改动被接受。幸运的是,这也是一件轻松的事情。
如果你使用的是电子邮件进行交流,你需要再次对代码进行修改并重新提交至邮件列表,
这些修改会自动更新到拉取请求上。在下图中,你也可以在更新后的拉取请求中看到已折叠的旧代码评论,
因为它是在修改后的行上添加的评论。

对现有的拉取请求添加提交并不会触发提醒,因此 Tony 在推送了他的修正后,
还需要通过评论告知项目拥有者他完成了修改请求。

最终的拉取请求

如果你点开拉取请求的“Files Changed”(更改的文件)选项卡,你将会看到“整理过的”差异表
—— 也就是这个分支被合并到主分支之后将会产生的所有改动,
其实就是 git diff master...<分支名> 命令的执行结果。
你可以浏览相关章节来了解更多关于差异表的知识。

你还会注意到,GitHub 会检查你的拉取请求是否能直接合并,如果可以,将会提供一个按钮来进行合并操作。
这个按钮只在你对版本库有写入权限并且可以进行简洁合并时才会显示。
你点击后 GitHub 将做出一个“非快进式”(non-fast-forward)合并,
即使这个合并 能够 快进式(fast-forward)合并,GitHub 依然会创建一个合并提交。

如果你需要,你还可以将分支拉取并在本地合并。
如果你将这个分支合并到 master 分支中并推送到 GitHub,这个拉取请求会被自动关闭。

这就是大部分 GitHub 项目使用的工作流程。创建分支,基于分支创建拉取请求,进行讨论,
根据需要继续在分支上进行修改,最终关闭或合并拉取请求。

不必总是 Fork
有件很重要的事情:你可以在同一个版本库中不同的分支提交拉取请求。
如果你正在和某人实现某个功能,而且你对项目有写权限,你可以推送分支到版本库,
并在 master 分支提交一个拉取请求并在此进行代码审查和讨论的操作。不需要进行“Fork”。

拉取请求的进阶用法

目前,我们学到了如何在 GitHub 平台对一个项目进行最基础的贡献。现在我们会教给你一些小技巧,让你可以更加有效率地使用拉取请求。

将拉取请求制作成补丁

有一件重要的事情:和大部分通过邮件列表工作的项目对补丁贡献的看法一样,
许多项目并不认为拉取请求可以作为补丁。
大多数的 GitHub 项目将拉取请求的分支当作对改动的交流方式,并将变更集合起来统一进行合并。

这是个重要的差异,因为一般来说改动会在代码完成前提出,这和基于邮件列表的补丁贡献有着天差地别。
这使得维护者们可以更早的沟通,由社区中的力量能提出更好的方案。
当有人从拉取请求提交了一些代码,并且维护者和社区提出了一些意见,这个补丁系列并不需要从头来过,
只需要将改动重新提交并推送到分支中,这使得讨论的背景和过程可以齐头并进。

举个例子,你可以回去看看上面的最终拉取请求图,你会注意到贡献者没有变基他的提交再提交一个新的拉取请求,
而是直接增加了新的提交并推送到已有的分支中。
如果你之后再回去查看这个拉取请求,你可以轻松地找到这个修改的原因。
点击网页上的“Merge”(合并)按钮后,会建立一个合并提交并指向这个拉取请求,你就可以很轻松的研究原来的讨论内容。

与上游保持同步

如果你的拉取请求由于过时或其他原因不能干净地合并,你需要进行修复才能让维护者对其进行合并。
GitHub 会对每个提交进行测试,让你知道你的拉取请求能否简洁的合并。

不能进行干净合并

如果你看到了像上图这样的画面,你就需要修复你的分支让这个提示变成绿色,这样维护者就不需要再做额外的工作。

你有两种方法来解决这个问题。你可以把你的分支变基到目标分支中去
(通常是你派生出的版本库中的 master 分支),或者你可以合并目标分支到你的分支中去。

GitHub 上的大多数的开发者会使用后一种方法,基于我们在上一节提到的理由:
我们最看重的是历史记录和最后的合并,变基除了给你带来看上去简洁的历史记录,
只会让你的工作变得更加困难且更容易犯错。

如果你想要合并目标分支来让你的拉取请求变得可合并,你需要将源版本库添加为一个新的远端,并从远端抓取内容,合并主分支的内容到你的分支中去,修复所有的问题并最终重新推送回你提交拉取请求使用的分支。

在这个例子中,我们再次使用之前的“tonychacon”用户来进行示范,源作者提交了一个改动,
使得拉取请求和它产生了冲突。现在来看我们解决这个问题的步骤。

$ git remote add upstream https://github.com/schacon/blink <1>

$ git fetch upstream <2>
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
Unpacking objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
From https://github.com/schacon/blink
 * [new branch]      master     -> upstream/master

$ git merge upstream/master <3>
Auto-merging blink.ino
CONFLICT (content): Merge conflict in blink.ino
Automatic merge failed; fix conflicts and then commit the result.

$ vim blink.ino <4>
$ git add blink.ino
$ git commit
[slow-blink 3c8d735] Merge remote-tracking branch 'upstream/master' \
    into slower-blink

$ git push origin slow-blink <5>
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 682 bytes | 0 bytes/s, done.
Total 6 (delta 2), reused 0 (delta 0)
To https://github.com/tonychacon/blink
   ef4725c..3c8d735  slower-blink -> slow-blink

<1> 将源版本库添加为一个远端,并命名为“upstream”(上游)
<2> 从远端抓取最新的内容
<3> 将该仓库的主分支的内容合并到你的分支中
<4> 修复产生的冲突
<5> 再推送回同一个分支

你完成了上面的步骤后,拉取请求将会自动更新并重新检查是否能干净的合并。

拉取请求现在可以干净地合并了

Git 的伟大之处就是你可以一直重复以上操作。如果你有一个运行了十分久的项目,
你可以轻松地合并目标分支且只需要处理最近的一次冲突,这使得管理流程更加容易。

如果你一定想对分支做变基并进行清理,你可以这么做,但是强烈建议你不要强行地提交到已经提交了拉取请求的分支。
如果其他人拉取了这个分支并进行一些修改,你将会遇到相关章节中提到的问题。
相对的,将变基后的分支推送到 GitHub 上的一个新分支中,并且创建一个全新的拉取请求引用旧的拉取请求,然后关闭旧的拉取请求。

参考

你的下个问题可能是“我该如何引用旧的拉取请求?”。
有许多方法可以让你在 GitHub 上的几乎任何地方引用其他东西。

先从如何对拉取请求或议题(Issue)进行相互引用开始。所有的拉取请求和议题在项目中都会有一个独一无二的编号。
举个例子,你无法同时拥有 3 号拉取请求和 3 号议题。如果你想要引用任何一个拉取请求或议题,
你只需要在提交或描述中输入 #<编号> 即可。
你也可以指定引用其他版本库的议题或拉取请求,如果你想要引用其他人对该版本库的“Fork”中的议题或拉取请求,
输入 用户名#<编号> ,如果在不同的版本库中,输入 用户名/版本库名#<编号>

我们来看一个例子。假设我们对上个例子中的分支进行了变基,并为此创建一个新的拉取请求,
现在我们希望能在新的拉取请求中引用旧的拉取请求。
我们同时希望引用一个派生出的项目中的议题和一个完全不同的项目中的议题,
就可以像下图这样填写描述。

在拉取请求中的交叉引用

当我们提交了这个拉取请求,我们将会看到以上内容被渲染成这样:

在拉取请求中渲染后的交叉引用

你会注意到完整的 GitHub 地址被简化了,只留下了必要的信息。

如果 Tony 回去关闭了源拉取请求,我们可以看到一个被引用的提示,
GitHub 会自动的反向追踪事件并显示在拉取请求的时间轴上。
这意味着任何查看这个拉取请求的人可以轻松地访问新的拉取请求。
这个链接就像下图展示的那样。

在拉取请求中渲染后的交叉引用

除了议题编号外,你还可以通过使用提交的 SHA-1 来引用提交。
你必须完整的写出 40 位长的 SHA-1,GitHub 会在评论中自动地产生指向这个提交的链接。
同样的,你可以像引用议题一样对派生的项目中的提交或者其他项目中的提交进行引用。

GitHub 风格的 Markdown

对于在 GitHub 中绝大多数文本框中能够做到的事,引用其他议题只是个开始。
在议题和拉取请求的描述,评论和代码评论还有其他地方,都可以使用“GitHub 风格的 Markdown”。
Markdown 可以让你输入纯文本,但是渲染出丰富的内容。

查看下面的示例来了解如何书写评论或文本,并通过 Markdown 进行渲染。

一个 Markdown 的示例和渲染效果

GitHub 风格的 Markdown

GitHub 风格的 Markdown 增加了一些基础的 Markdown 中做不到的东西。
它在创建拉取请求和议题中的评论和描述时十分有用。

任务列表

第一个 GitHub 专属的 Markdown 功能,特别是用在拉取请求中,就是任务列表。
一个任务列表可以展示出一系列你想要完成的事情,并带有复选框。
把它们放在议题或拉取请求中时,通常可以展示你想要完成的事情。

你可以这样创建一个任务列表:

- [X] 编写代码
- [ ] 编写所有测试程序
- [ ] 为代码编写文档

如果我们将这个列表加入拉取请求或议题的描述中,它将会被渲染成下面这样。

Markdown 评论中渲染后的任务列表

在拉取请求中,任务列表经常被用来在合并之前展示这个分支将要完成的事情。
最酷的地方就是,你只需要点击复选框,就能更新评论 —— 你不需要直接修改 Markdown。

不仅如此,GitHub 还会将你在议题和拉取请求中的任务列表整理起来集中展示。
举个例子,如果你在一个拉取请求中有任务清单,你将会在所有拉取请求的总览页面上看到它的进度。
这使得人们可以把一个拉取请求分解成不同的小任务,同时便于其他人了解分支的进度。
你可以在下图中看到一个例子。

在拉取请求列表中的任务列表总结

当你在实现一个任务的早期就提交拉取请求,并使用任务清单追踪你的进度,这个功能会十分的有用。

代码片段

你也可以在评论中添加代码片段。这在你想要展示尚未提交到分支中的代码时会十分有用。
它也经常被用在展示无法正常工作的代码或这个拉取请求需要的代码。

你需要用“反引号”将需要添加的代码片段包起来。

```java
for(int i=0 ; i < 5 ; i++)
{
   System.out.println("i is : " + i);
}

如果加入语言的名称,就像我们这里加入的“java”一样,GitHub 会自动尝试对摘录的片段进行语法高亮。
在下面的例子中,它最终会渲染成这个样子:

![渲染后的代码片段示例](markdown-04-fenced-code.png)

#### 引用

如果你在回复一个很长的评论之中的一小段,你只需要复制你需要的片段,并在每行前添加 `>` 符号即可。
事实上,因为这个功能会被经常用到,它也有一个快捷键。
只要你把你要回应的文字选中,并按下 `r` 键,选中的问题会自动引用并填入评论框。

引用的部分就像这样:

```text
> Whether 'tis Nobler in the mind to suffer
> The Slings and Arrows of outrageous Fortune,

How big are these slings and in particular, these arrows?

经过渲染后,就会变成这样:

渲染后的引用示例

表情符号

最后,我们可以在评论中使用表情符号。这经常出现在 GitHub 的议题和拉取请求的评论中。
GitHub 上甚至有表情助手。如果你在输入评论时以 : 开头,自动完成器会帮助你找到你需要的表情。

表情符号自动完成器

你也可以在评论的任何地方使用 :<表情名称>: 来添加表情符号。
举个例子,你可以输入以下文字:

I :eyes: that :bug: and I :cold_sweat:.

:trophy: for :microscope: it.

:+1: and :sparkles: on this :ship:, it's :fire::poop:!

:clap::tada::panda_face:

渲染之后,就会变成这样:

使用了大量表情符号的评论

虽然这个功能并不是非常实用,但是它在这种不方便表达感情的媒体里,加入了趣味的元素。

事实上现在已经有大量的在线服务可以使用表情符号,这里有个列表可以让你快速的找到能表达你的情绪的表情符号:
https://www.webfx.com/tools/emoji-cheat-sheet/

图片

从技术层面来说,这并不是 GitHub 风格 Markdown 的功能,但是也很有用。
如果不想使用 Markdown 语法来插入图片,GitHub 允许你通过拖拽图片到文本区来插入图片。

通过拖拽的方式自动插入图片

如果你回去查看上图 ,你会发现文本区上有个“Parsed as Markdown”的提示。
点击它你可以了解所有能在 GitHub 上使用的 Markdown 功能。

让你的 GitHub 公共仓库保持更新

当你派生了一个 GitHub 仓库之后,你的仓库(即你的“派生”)会独立于原仓库而独立。
特别地,当原仓库有新的提交时,GitHub 会通知你:

This branch is 5 commits behind progit:master.
(本分支落后 progit:master 5 个提交。)

但你的 GitHub 仓库不会被 GitHub 自动更新,这件事必须由你自己来做。还好,这事儿很简单。

第一种方法无需配置。例如,若你从 https://github.com/progit/progit2.git 派生了项目,
你可以像这样更新你的 master 分支:

$ git checkout master <1>
$ git pull https://github.com/progit/progit2.git <2>
$ git push origin master <3>

<1> 如果在另一个分支上,就切换到 master
<2> 从 https://github.com/progit/progit2.git 抓取更改后合并到 master
<3> 将 master 分支推送到 origin

这虽然可行,但每次都要输入从哪个 URL 抓取有点麻烦。你可以稍微设置一下来自动完成它:

$ git remote add progit https://github.com/progit/progit2.git <1>
$ git branch --set-upstream-to=progit/master master <2>
$ git config --local remote.pushDefault origin <3>

<1> 添加源仓库并取一个名字,这里叫它 progit
<2> 将 master 分支设置为从 progit 远端抓取
<3> 将默认推送仓库设置为 origin

搞定之后,工作流程为更加简单:

$ git checkout master <1>
$ git pull <2>
$ git push <3>

<1> 如果在另一个分支上,就切换到 master
<2> 从 progit 抓取更改后合并到 master
<3> 将 master 分支推送到 origin

这种方法可能很有用,但也不是没有缺点。如果你向 master 提交,再从 progit 中拉取,然后推送到
origin,Git 会很乐意安静地为您完成这项工作,但不会警告你——所有这些操作在以上设置下都是有效的。
所以你必须注意永远不要直接提交到 master,因为该分支实际上属于上游仓库。

维护项目

现在我们可以很方便地向一个项目贡献内容,来看一下另一个方面的内容:创建、维护和管理你自己的项目。

创建新的版本库

让我们创建一个版本库来分享我们的项目。
通过点击面板右侧的“New repository”按钮,或者顶部工具条你用户名旁边的 + 按钮来开始我们的旅程。 参见下图。

这是 “Your repositories” 区域.

这是 “New repository” 下拉列表.

这会带你到 “new repository” 表单:

这是 “new repository” 表单.

这里除了一个你必须要填的项目名,其他字段都是可选的。
现在只需要点击 “Create Repository” 按钮,Duang!!! – 你就在 GitHub 上拥有了一个以 <user>/<project_name> 命名的新仓库了。

因为目前暂无代码,GitHub 会显示有关创建新版本库或者关联到一个已有的 Git 版本库的一些说明。
我们不会在这里详细说明此项,如果你需要复习,去看相关章节。

现在你的项目就托管在 GitHub 上了,你可以把 URL 给任何你想分享的人。
GitHub 上的项目可通过 HTTP 或 SSH 访问,HTTPS 为 \https://github.com/<user>/<project_name>
SSH 为 git@github.com:<user>/<project_name>
Git 可以通过以上两种 URL 进行抓取和推送,但是用户的访问权限又因连接时使用的证书不同而异。

通常对于公开项目可以优先分享基于 HTTPS 的 URL,因为用户克隆项目不需要有一个 GitHub 帐号。
如果你分享 SSH URL,用户必须有一个帐号并且上传 SSH 密钥才能访问你的项目。
HTTPS URL 与你贴到浏览器里查看项目用的地址是一样的。

添加合作者

如果你想与他人合作,并想给他们提交的权限,你需要把他们添加为 “Collaborators”。
如果 Ben,Jeff,Louise 都在 GitHub 上注册了,你想给他们推送的权限,你可以将他们添加到你的项目。
这样做会给他们 “推送” 权限,就是说他们对项目和 Git 版本库都有读写的权限。

点击边栏底部的 “Settings” 链接。

版本库设置链接.

然后从左侧菜单中选择 “Collaborators” 。
然后,在输入框中填写用户名,点击 “Add collaborator.”
如果你想授权给多个人,你可以多次重复这个步骤。
如果你想收回权限,点击他们同一行右侧的 “X”

版本库合作者.

管理合并请求

现在你有一个包含一些代码的项目,可能还有几个有推送权限的合作者,下面来看当你收到合并请求时该做什么。

合并请求可以来自仓库副本的一个分支,或者同一仓库的另一个分支。
唯一的区别是 fork 过来的通常是和你不能互相推送的人,而内部的推送通常都可以互相访问。

作为例子,假设你是 “tonychacon” ,你创建了一个名为 “fade” 的 Arduino 项目.

邮件通知

有人来修改了你的代码,给你发了一个合并请求。
你会收一封关于合并请求的提醒邮件,它看起来像下图。

新的合并请求的邮件通知.

关于这个邮件有几个要注意的地方。
它会给你一个小的变动统计结果 – 一个包含合并请求中改变的文件和改变了多少的列表。
它还给你一个 GitHub 上进行合并请求操作的链接。
还有几个可以在命令行使用的 URL。

如果你注意到 git pull <url> patch-1 这一行,这是一种合并远程分支的简单方式,无需必须添加一个远程分支。
我们很快会在相关章节讲到它。
如果你愿意,你可以创建并切换到一个主题分支,然后运行这个命令把合并请求合并进来。

还有一些有趣的 URL,像 .diff.patch ,就像你猜的那样,它们提供 diff 和 patch 的标准版本。
你可以技术性地用下面的方法合并“合并请求”:

$ curl https://github.com/tonychacon/fade/pull/1.patch | git am

在合并请求上进行合作

就像我们在相关章节中说过的,现在你可以跟开启合并请求的人进行会话。
你既可以对某些代码发表评论,也可以对整个提交或整个合并请求发表评论,
在任何地方都可以用 GitHub 风格的 Markdown。

每次有人在合并请求上发表了评论,你都会收到邮件,通知你哪里发生了改变。邮件里面包含一个链接,指向改变的位置,你可以直接在邮件中回复,相当于在合并请求上发表评论。

回复邮件会包含在帖子(thread)中。

一旦代码符合了你的要求,你想把它合并进来,你可以把代码拉取下来在本地进行合并,也可以用我们之前提到过的 git pull <url> <branch> 语法,或者把 fork 添加为一个 remote,然后进行抓取和合并。

对于很琐碎的合并,你也可以用 GitHub 网站上的 “Merge” 按钮。
它会做一个 “non-fast-forward” 合并,即使可以快进(fast-forward)合并也会产生一个合并提交记录。
就是说无论如何,只要你点击 merge 按钮,就会产生一个合并提交记录。
你可以在下图中看到,如果你点击提示链接,GitHub 会给你所有的这些信息。

合并按钮和手工合并一个合并请求的指令.

如果你决定不合并它,你可以把合并请求关掉,开启合并请求的人会收到通知。

合并请求引用

如果你正在处理 许多 合并请求,不想添加一堆 remote 或者每次都要做一次拉取,这里有一个可以在 GitHub 上用的小技巧。
这是有点高级的技巧,但它相当有用,我们会在相关章节有更多的细节说明。

实际上 GitHub 在服务器上把合并请求分支视为一种 “假分支”。
默认情况下你克隆时不会得到它们,但它们还是隐式地存在,你可以很容易地访问到它们。

为了展示这个,我们要用到一个叫做 ls-remote 的低级命令(通常被叫做“plumbing”,
我们会在相关章节读到更多相关内容)。
这个命令在日常 Git 操作中基本不会用到,但在显示服务器上有哪些引用(reference)时很管用。

如果在我们之前用过的 “blink” 版本库上使用这个命令,我们会得到一个版本库里所有的分支,标签和其它引用(reference)的列表。

$ git ls-remote https://github.com/schacon/blink
10d539600d86723087810ec636870a504f4fee4d	HEAD
10d539600d86723087810ec636870a504f4fee4d	refs/heads/master
6a83107c62950be9453aac297bb0193fd743cd6e	refs/pull/1/head
afe83c2d1a70674c9505cc1d8b7d380d5e076ed3	refs/pull/1/merge
3c8d735ee16296c242be7a9742ebfbc2665adec1	refs/pull/2/head
15c9f4f80973a2758462ab2066b6ad9fe8dcf03d	refs/pull/2/merge
a5a7751a33b7e86c5e9bb07b26001bb17d775d1a	refs/pull/4/head
31a45fc257e8433c8d8804e3e848cf61c9d3166c	refs/pull/4/merge

当然,如果你在你自己的版本库或其它你想检查的远程版本库中使用 git ls-remote origin ,它会显示相似的内容。

如果版本库在 GitHub 上并且有打开的合并请求,你会得到一些以 refs/pull/ 开头的引用。
它们实际上是分支,但因为它们不在 refs/heads/ 中,所以正常情况下你克隆时不会从服务器上得到它们
——抓取过程正常情况下会忽略它们。

每个合并请求有两个引用——其中以 /head 结尾的引用指向的提交记录与合并请求分支中的最后一个提交记录是同一个。
所以如果有人在我们的版本库中开启了一个合并请求,他们的分支叫做 bug-fix
指向 a5a775 这个提交记录,那么在 我们的 版本库中我们没有 bug-fix 分支(因为那是在他们的 fork 中),
但我们 可以 有一个 pull/<pr#>/head 指向 a5a775
这意味着我们可以很容易地拉取每一个合并请求分支而不用添加一堆远程仓库。

现在,你可以像直接抓取引用一样抓取那些分支或提交。

$ git fetch origin refs/pull/958/head
From https://github.com/libgit2/libgit2
 * branch            refs/pull/958/head -> FETCH_HEAD

这告诉 Git: “连接到 origin 这个 remote,下载名字为 refs/pull/958/head 的引用。”
Git 高高兴兴去执行,下载构建那个引用需要的所有内容,然后把指针指向 .git/FETCH_HEAD 下面你想要的提交记录。
然后你可以用 git merge FETCH_HEAD 把它合并到你想进行测试的分支,但那个合并的提交信息看起来有点怪。
然而,如果你需要审查 一大批 合并请求,这样操作会很麻烦。

还有一种方法可以抓取 所有的 合并请求,并且在你连接到远程仓库的时候保持更新。
用你最喜欢的编辑器打开 .git/config ,查找 origin 远程仓库。
看起来差不多像下面这样:

[remote "origin"]
    url = https://github.com/libgit2/libgit2
    fetch = +refs/heads/*:refs/remotes/origin/*

fetch = 开头的行是一个 “refspec.”
它是一种把 remote 的名称映射到你本地 .git 目录的方法。
这一条(就是上面的这一条)告诉 Git,“remote 上 refs/heads 下面的内容在我本地版本库中都放在 refs/remotes/origin 。”
你可以把这一段修改一下,添加另一个 refspec:

[remote "origin"]
    url = https://github.com/libgit2/libgit2.git
    fetch = +refs/heads/*:refs/remotes/origin/*
    fetch = +refs/pull/*/head:refs/remotes/origin/pr/*

最后一行告诉 Git: “所有看起来像 refs/pull/123/head 的引用应该在本地版本库像 refs/remotes/origin/pr/123 一样存储”
现在,如果你保存那个文件,执行 git fetch

$ git fetch
# …
 * [new ref]         refs/pull/1/head -> origin/pr/1
 * [new ref]         refs/pull/2/head -> origin/pr/2
 * [new ref]         refs/pull/4/head -> origin/pr/4
# …

现在所有的合并请求在本地像分支一样展现,它们是只读的,当你执行抓取时它们也会更新。
这让在本地测试合并请求中的代码变得超级简单:

$ git checkout pr/2
Checking out files: 100% (3769/3769), done.
Branch pr/2 set up to track remote branch pr/2 from origin.
Switched to a new branch 'pr/2'

你的鹰眼系统会发现在 refspec 的 remote 部分的结尾有个 head
在 GitHub 那边也有一个 refs/pull/#/merge 引用,它代表的是如果你在网站上按了 “merge” 按钮对应的提交记录。
这甚至让你可以在按按钮之前就测试这个合并。

合并请求之上的合并请求

你不仅可以在主分支或者说 master 分支上开启合并请求,实际上你可以在网络上的任何一个分支上开启合并请求。
其实,你甚至可以在另一个合并请求上开启一个合并请求。

如果你看到一个合并请求在向正确的方向发展,然后你想在这个合并请求上做一些修改或者你不太确定这是个好主意,或者你没有目标分支的推送权限,你可以直接在合并请求上开启一个合并请求。

当你开启一个合并请求时,在页面的顶端有一个框框显示你要合并到哪个分支和你从哪个分支合并过来的。
如果你点击那个框框右边的 “Edit” 按钮,你不仅可以改变分支,还可以选择哪个 fork。

手工修改合并请求的目标.

这里你可以很简单地指明合并你的分支到哪一个合并请求或 fork。

提醒和通知

GitHub 内置了一个很好的通知系统,当你需要与别人或别的团队交流时用起来很方便。

在任何评论中你可以先输入一个 @ ,系统会自动补全项目中合作者或贡献者的名字和用户名。

输入 @ 来提醒某人.

你也可以提醒不在列表中的用户,但是通常自动补全用起更快。

当你发布了一个带用户提醒的评论,那个用户会收到通知。
这意味着把人们拉进会话中要比让他们投票有效率得多。
对于 GitHub 上的合并请求,人们经常把他们团队或公司中的其它人拉来审查问题或合并请求。

如果有人收到了合并请求或问题的提醒,他们会“订阅”它,后面有新的活动发生他们都会持续收到提醒。
如果你是合并请求或者问题的发起方你也会被订阅上,比如你在关注一个版本库或者你评论了什么东西。
如果你不想再收到提醒,在页面上有个 “Unsubscribe” 按钮,点一下就不会再收到更新了。

取消订阅一个问题或合并请求.

通知页面

当我们在这提到特指 GitHub 的 “notifications” ,指的是当 GitHub 上有事件发生时,它通知你的方式,这里有几种不同的方式来配置它们。
如果你打开配置页面的 “Notification center” 标签,你可以看到一些选项。

通知中心选项.

有两个选项,通过“邮件(Email)”和通过“网页(Web)”,你可以选用一个或者都不选或者都选。

网页通知

网页通知只在 GitHub 上存在,你也只能在 GitHub 上查看。
如果你打开了这个选项并且有一个你的通知,你会在你屏幕上方的通知图标上看到一个小蓝点。参见下图。

通知中心.

如果你点击那个玩意儿,你会看到你被通知到的所有条目,按照项目分好了组。
你可以点击左边栏的项目名字来过滤项目相关的通知。
你可以点击通知旁边的对号图标把通知标为已读,或者点击组上面的图标把项目中 所有的 通知标为已读。
在每个对号图标旁边都有一个静音按钮,你可以点一下,以后就不会收到它相关的通知。

所有这些工具对于处理大量通知非常有用。
很多 GitHub 资深用户都关闭邮件通知,在这个页面上处理他们所有的通知。

邮件通知

邮件通知是你处理 GitHub 通知的另一种方式。
如果你打开这个选项,每当有通知时,你会收到一封邮件。
我们在相关章节看到了一些例子。
邮件也会被合适地按话题组织在一起,如果你使用一个具有会话功能的邮件客户端那会很方便。

GitHub 在发送给你的邮件头中附带了很多元数据,这对于设置过滤器和邮件规则非常有帮助。

举个例子,我们来看一看在相关章节中发给 Tony 的一封真实邮件的头部,我们会看到下面这些:

To: tonychacon/fade <fade@noreply.github.com>
Message-ID: <tonychacon/fade/pull/1@github.com>
Subject: [fade] Wait longer to see the dimming effect better (#1)
X-GitHub-Recipient: tonychacon
List-ID: tonychacon/fade <fade.tonychacon.github.com>
List-Archive: https://github.com/tonychacon/fade
List-Post: <mailto:reply+i-4XXX@reply.github.com>
List-Unsubscribe: <mailto:unsub+i-XXX@reply.github.com>,...
X-GitHub-Recipient-Address: tchacon@example.com

这里有一些有趣的东西。如果你想高亮或者转发这个项目甚至这个合并请求相关的邮件,
Message-ID 中的信息会以<user>/<project>/<type>/<id> 的格式展现所有的数据。
例如,如果这是一个问题(issue),那么 <type> 字段就会是 “issues” 而不是 “pull” 。

List-PostList-Unsubscribe 字段表示如果你的邮件客户端能够处理这些,那么你可以很容易地在列表中发贴或取消对这个相关帖子的订阅。
那会很有效率,就像在页面中点击静音按钮或在问题/合并请求页面点击 “Unsubscribe” 一样。

值得注意的是,如果你同时打开了邮件和网页通知,那么当你在邮件客户端允许加载图片的情况下阅读邮件通知时,对应的网页通知也将会同时被标记为已读。

特殊文件

如果你的版本库中有一些特殊文件,GitHub 会提醒你。

README

第一个就是 README 文件,可以是几乎任何 GitHub 可以识别的格式。
例如,它可以是 READMEREADME.mdREADME.asciidoc
如果 GitHub 在你的版本库中找到 README 文件,会把它在项目的首页渲染出来。

很多团队在这个文件里放版本库或项目新人需要了解的所有相关的信息。
它一般包含这些内容:

  • 该项目的作用
  • 如何配置与安装
  • 有关如何使用和运行的例子
  • 项目的许可证
  • 如何向项目贡献力量

因为 GitHub 会渲染这个文件,你可以在文件里植入图片或链接让它更容易理解。

贡献 CONTRIBUTING

另一个 GitHub 可以识别的特殊文件是 CONTRIBUTING
如果你有一个任意扩展名的 CONTRIBUTING 文件,当有人开启一个合并请求时 GitHub 会显示下图。

开启合并请求时有 CONTRIBUTING 文件存在.

这个的作用就是你可以在这里指出对于你的项目开启的合并请求你想要的/不想要的各种事情。
这样别人在开启合并请求之前可以读到这些指导方针。

项目管理

对于一个单个项目其实没有很多管理事务要做,但也有几点有趣的。

改变默认分支

如果你想用 “master” 之外的分支作为你的默认分支,其他人将默认会在这个分支上开启合并请求或进行浏览,你可以在你版本库的设置页面的 “options” 标签下修改。

改变项目的默认分支.

简单地改变默认分支下拉列表中的选项,它就会作为所有主要操作的默认分支,他人进行克隆时该分支也将被默认检出。

移交项目

如果你想把一个项目移交给 GitHub 中的另一个人或另一个组织,还是设置页面的这个 “options” 标签下有一个 “Transfer ownership” 选项可以用来干这个。

把项目移交给另一个 GitHub 用户或组织。

当你正准备放弃一个项目且正好有别人想要接手时,或者你的项目壮大了想把它移到一个组织里时,这就管用了。

这么做不仅会把版本库连带它所有的关注者和星标数都移到另一个地方,它还会将你的 URL 重定向到新的位置。
它也重定向了来自 Git 的克隆和抓取,而不仅仅是网页端请求。

管理组织

除了个人帐户之外,GitHub 还提供被称为组织(Organizations)的帐户。
组织账户和个人账户一样都有一个用于存放所拥有项目的命名空间,但是许多其他的东西都是不同的。
组织帐户代表了一组共同拥有多个项目的人,同时也提供一些工具用于对成员进行分组管理。
通常,这种账户被用于开源群组(例如:“perl”或者“rails”),或者公司(例如:“google”或者“twitter”)。

组织的基本知识

我们可以很简单地创建一个组织,只需要点击任意 GitHub 页面右上角的“+”图标,在菜单中选择“New organization”即可。

“New organization”菜单项

首先你必须提供组织的名称和组织的主要联系邮箱。
然后,如果你希望的话,也可以邀请其他用户作为共同拥有人。

完成以上步骤后,你就会拥有一个全新的组织。
类似于个人帐户,如果组织的所有内容都是开源的,那么你就可以免费使用这个组织。

作为一个组织的拥有者,当你在派生一个版本库的时候,你可以选择把它派生到你的组织的命名空间内。
当你新建版本库时,你可以把它存放到你的个人帐户或你拥有的组织内。
同时,你也会自动地“关注”所有这些组织内的新版本库。

就像个人帐户一样,你可以为你的组织上传头像,使它更个性化。
同时,也和个人帐户类似,组织会有一个着陆页(landing page),用于列出该组织所有的版本库,并且该页面可供所有人浏览。

下面我们来说一些组织和个人帐户不同的地方。

团队

组织使用团队(Teams)来管理成员,团队就是组织中的一组个人账户和版本库,以及团队成员对这些版本库的访问权限。

例如,假设你的公司有三个版本库:frontendbackenddeployscripts
你会希望你的 HTML/CSS/Javascript 开发者有 frontend 或者 backend 的访问权限,操作人员有 backenddeployscripts 的访问权限。
团队让这个任务变得更简单,而不用为每个版本库管理它的协作者。

组织页面主要由一个面板(dashboard)构成,这个仪表盘包含了这个组织内的所有版本库,用户和团队。

组织页面

你可以点击上图右边的团队侧边栏(Teams)来管理你的团队。
点击之后,你会进入一个新页面,在这里你可以添加新成员和版本库到团队中,或者管理团队的访问权限和其它设置。
每个团队对于版本库可以有只读、读写和管理三种权限。
你可以通过点击在下图内的 “Settings” 按钮更改相应权限等级。

团队页面

当你邀请一个用户加入团队,该用户会收到一封通知他被邀请的邮件。

除此之外,团队也类似于个人帐户,有 @mentions(例如:@acmecorp/frontend)的功能,不同之处就在于被提及的团队内 所有 成员都会成为这个话题的订阅者。
当你希望得到团队中某个人的关注,又不知道具体应该问谁的时候,这个功能就显得很有帮助。

一个用户可以加入任意数量的团队,所以别把自己局限于拥有访问控制的团队。
对于某一类课题,像 uxcss 或者 refactoring 这样有着特殊关注点的团队就显得很有帮助,而像 legalcolorblind 这样的就完全是针对它们各自领域的。

审计日志

组织的拥有者还可以访问组织中发生的事情的所有信息。
在 ‘Audit Log’ 标签页有整个组织的日志,你可以看到谁在世界上哪个地方做了什么事。

审计日志

你也可以通过选定某一类型的事件、某个地方、某个人对日志进行过滤。

脚本 GitHub

所以现在我们已经介绍了 GitHub 的大部分功能与工作流程,但是任意一个小组或项目都会去自定义,因为他们想要创造或扩展想要整合的服务。

对我们来说很幸运的是,GitHub 在许多方面都真的很方便 Hack。
在本节中我们将会介绍如何使用 GitHub 钩子系统与 API 接口,使 GitHub 按照我们的设想来工作。

服务与钩子

GitHub 仓库管理中的钩子与服务区块是 GitHub 与外部系统交互最简单的方式。

服务

首先我们来看一下服务。
钩子与服务整合都可以在仓库的设置区块中找到,就在我们之前添加协作者与改变项目的默认分支的地方。
在 “Webhooks and Services” 标签下你会看到与下面类似的内容。

服务与钩子

有许多可以选择的服务,大多数是整合到其他的商业与开源系统中。
它们中的大多数是为了整合持续集成服务、BUG 与问题追踪系统、聊天室系统与文档系统。
我们将会通过设置一个非常简单的例子来介绍。
如果从 “Add Service” 选择 “email”,会得到一个类似下面的配置屏幕。

电子邮件服务配置

在本例中,如果我们点击 “Add service” 按钮,每次有人推送内容到仓库时,指定的电子邮件地址都会收到一封邮件。
服务可以监听许多不同类型的事件,但是大多数只监听推送事件然后使用那些数据做一些事情。

如果有一个正在使用的系统想要整合到 GitHub,应当先检查这里看有没有已有的可用的服务整合。
例如,如果正使用 Jenkins 来测试你的代码库,当每次有人推送到你的仓库时你可以启用 Jenkins 内置的整合启动测试运行。

钩子

如果需要做一些更具体的事,或者想要整合一个不在这个列表中的服务或站点,可以转而使用更通用的钩子系统。
GitHub 仓库钩子是非常简单的。
指定一个 URL 然后 GitHub 在任一期望的事件发生时就会发送一个 HTTP 请求到那个 URL 。

通常做这件事的方式是可以设置一个小的 web 服务来监听 GitHub 钩子请求然后使用收到的数据做一些事情。

为了启用一个钩子,点击上面的 “Add webhook” 按钮。
这会将你引导至一个类似下面的页面。

Web 钩子配置

Web 钩子的设置非常简单。
大多数情况下只需要输入一个 URL 与一个密钥然后点击 “Add webhook”。
有几个选项可以指定在哪个事件时想要 GitHub 发送请求——
默认的行为是只有当某人推送新代码到仓库的任一分支时的 push 事件获得一个请求。

让我们看一个设置处理 web 钩子的 web 服务的小例子。
我们将会使用 Ruby web 框架 Sinatra,因为它相当简洁,应该能够轻松地看到我们正在做什么。

假设我们想要在某个特定的人推送到我们的项目的特定分支并修改一个特定文件时得到一封邮件。
我们可以相当容易地使用类似下面的代码做到:

require 'sinatra'
require 'json'
require 'mail'

post '/payload' do
  push = JSON.parse(request.body.read) # parse the JSON

  # gather the data we're looking for
  pusher = push["pusher"]["name"]
  branch = push["ref"]

  # get a list of all the files touched
  files = push["commits"].map do |commit|
    commit['added'] + commit['modified'] + commit['removed']
  end
  files = files.flatten.uniq

  # check for our criteria
  if pusher == 'schacon' &&
     branch == 'ref/heads/special-branch' &&
     files.include?('special-file.txt')

    Mail.deliver do
      from     'tchacon@example.com'
      to       'tchacon@example.com'
      subject  'Scott Changed the File'
      body     "ALARM"
    end
  end
end

这里我们拿到一个 GitHub 传送给我们的 JSON 请求然后查找推送者,他们推送到了什么分支以及推送的所有提交都改动了哪些文件。
然后我们检查它是否与我们的条件区配,如果匹配则发送一封邮件。

为了开发与测试类似这样的东西,在设置钩子的地方有一个漂亮的开发者控制台。
可以看到 GitHub 为那个 webhook 的最后几次请求。
对每一个钩子,当它发送后都可以深入挖掘,检测它是否是成功的与请求及回应的消息头与消息体。
这使得测试与调试钩子非常容易。

Web 钩子调试信息

开发者控制台的另一个很棒的功能是可以轻松地重新发送任何请求来测试你的服务。

关于如何编写 web 钩子与所有可监听的不同事件类型的更多信息,请访问在 https://docs.github.com/cn/developers/webhooks-and-events/webhooks/about-webhooks 的 GitHub 开发者文档。

GitHub API

服务与钩子给你提供了一种方式来接收关于在仓库中发生的事件的推送通知,但是如何获取相关事件的详情呢?
如何自动化一些诸如添加协作者或给问题加标签的事情呢?

这是 GitHub API 派上用场的地方。
在自动化流行的趋势下,GitHub 提供了大量的 API 接口,可以进行几乎任何能在网站上进行的操作。
在本节中我们将会学习如何授权与连接到 API,如何通过 API 在一个问题上评论与如何修改一个 Pull Request 的状态。

基本用途

可以做的最基本的事情是向一个不需要授权的接口上发送一个简单的 GET 请求。
该接口可能是一个用户或开源项目的只读信息。
例如,如果我们想要知道更多关于名为 “schacon” 的用户信息,我们可以运行类似下面的东西:

$ curl https://api.github.com/users/schacon
{
  "login": "schacon",
  "id": 70,
  "avatar_url": "https://avatars.githubusercontent.com/u/70",
# …
  "name": "Scott Chacon",
  "company": "GitHub",
  "following": 19,
  "created_at": "2008-01-27T17:19:28Z",
  "updated_at": "2014-06-10T02:37:23Z"
}

有大量类似这样的接口来获得关于组织、项目、问题、提交的信息 – 差不多就是你能在 GitHub 上看到的所有东西。
甚至可以使用 API 来渲染任意 Markdown 或寻找一个 .gitignore 模板。

$ curl https://api.github.com/gitignore/templates/Java
{
  "name": "Java",
  "source": "*.class

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.ear

# virtual machine crash logs, see https://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
"
}

在一个问题上评论

然而,如果想要在网站上进行一个操作,如在 Issue 或 Pull Request 上评论,或者想要查看私有内容或与其交互,你需要授权。

这里提供了几种授权方式。
你可以使用仅需用户名与密码的基本授权,但是通常更好的主意是使用一个个人访问令牌。
可以从设置页的 “Applications” 标签生成访问令牌。

从设置页的 “Applications” 标签生成访问令牌。

它会询问这个令牌的作用域与一个描述。
确保使用一个好的描述信息,这样当脚本或应用不再使用时你会很放心地移除。

GitHub 只会显示令牌一次,所以记得一定要拷贝它。
现在可以在脚本中使用它代替使用用户名写密码来授权。
这很漂亮,因为可以限制想要做的范围并且令牌是可废除的。

这也会有一个提高频率上限的附加优点。
如果没有授权的话,你会被限制在一小时最多发起 60 次请求。
如果授权则可以一小时最多发起 5000 次请求。

所以让我们利用它来对我们的其中一个问题进行评论。
想要对一个特定问题 Issue #6 留下一条评论。
必须使用刚刚生成的令牌作为 Authorization 头信息,发送一个到 repos/<user>/<repo>/issues/<num>/comments 的 HTTP POST 请求。

$ curl -H "Content-Type: application/json" \
       -H "Authorization: token TOKEN" \
       --data '{"body":"A new comment, :+1:"}' \
       https://api.github.com/repos/schacon/blink/issues/6/comments
{
  "id": 58322100,
  "html_url": "https://github.com/schacon/blink/issues/6#issuecomment-58322100",
  ...
  "user": {
    "login": "tonychacon",
    "id": 7874698,
    "avatar_url": "https://avatars.githubusercontent.com/u/7874698?v=2",
    "type": "User",
  },
  "created_at": "2014-10-08T07:48:19Z",
  "updated_at": "2014-10-08T07:48:19Z",
  "body": "A new comment, :+1:"
}

现在如果进入到那个问题,可以看到我们刚刚发布的评论,像下面一样。

从 GitHub API 发布的一条评论

可以使用 API 去做任何可以在网站上做的事情 – 创建与设置里程碑、指派人员到 Issues 与 Pull Requests,创建与修改标签、访问提交数据、创建新的提交与分支、打开关闭或合并 Pull Requests、创建与编辑团队、在 Pull Request 中评论某行代码、搜索网站等等。

修改 Pull Request 的状态

我们要看最后一个例子在使用拉取请求时非常有用。
每一个提交可以有一个或多个与它关联的状态,有 API 来添加与查询状态。

大多数持续集成与测试服务通过测试推送的代码后使用这个 API 来回应,然后报告提交是否通过了全部测试。
你也可以使用该接口来检查提交信息是否经过合适的格式化、提交者是否遵循了所有你的贡献准则、提交是否经过有效的签名 – 种种这类事情。

假设在仓库中设置了一个 web 钩子访问一个用来检查提交信息中的 Signed-off-by 字符串的小的 web 服务。

require 'httparty'
require 'sinatra'
require 'json'

post '/payload' do
  push = JSON.parse(request.body.read) # parse the JSON
  repo_name = push['repository']['full_name']

  # look through each commit message
  push["commits"].each do |commit|

    # look for a Signed-off-by string
    if /Signed-off-by/.match commit['message']
      state = 'success'
      description = 'Successfully signed off!'
    else
      state = 'failure'
      description = 'No signoff found.'
    end

    # post status to GitHub
    sha = commit["id"]
    status_url = "https://api.github.com/repos/#{repo_name}/statuses/#{sha}"

    status = {
      "state"       => state,
      "description" => description,
      "target_url"  => "http://example.com/how-to-signoff",
      "context"     => "validate/signoff"
    }
    HTTParty.post(status_url,
      :body => status.to_json,
      :headers => {
        'Content-Type'  => 'application/json',
        'User-Agent'    => 'tonychacon/signoff',
        'Authorization' => "token #{ENV['TOKEN']}" }
    )
  end
end

希望这相当容易做。
在这个 web 钩子处理器中我们浏览刚刚推送上来的每一个提交,在提交信息中查找字符串
‘Signed-off-by’ 并且最终使用 HTTP 向 /repos/<user>/<repo>/statuses/<commit_sha>
API 接口发送一个带有状态的 POST 请求。

在本例中可以发送一个状态(’success’, ‘failure’, ‘error’)、一个发生了什么的描述信息、
一个用户可以了解更多信息的目标 URL 与一个 “context” 以防一个单独的提交有多个状态。
例如,一个测试服务可以提供一个状态与一个类似这样的验证服务也可能提供一个状态 – “context” 字段是用来区别它们的。

如果某人在 GitHub 中打开了一个新的拉取请求并且这个钩子已经设置,会看到类似下面的信息。

通过 API 的提交状态

现在可以看到一个小的绿色对勾标记在提交信息中有 “Signed-off-by” 的提交旁边,红色的对勾标记在作者忘记签名的提交旁边。
也可以看到 Pull Request 显示在那个分支上的最后提交的状态,如果失败的话会警告你。
如果对测试结果使用这个 API 那么就不会不小心合并某些未通过测试的最新提交。

Octokit

尽管我们在这些例子中都是通过 curl 与基本的 HTTP 请求来做几乎所有的事情,还有一些以更自然的方式利用 API 的开源库存在着。
在写这篇文章的时候,被支持的语言包括 Go、Objective-C、Ruby 与 .NET。
访问 https://github.com/octokit 了解更多相关信息,它们帮你处理了更多 HTTP 相关的内容。

希望这些工具能帮助你自定义与修改 GitHub 来更好地为特定的工作流程工作。
关于全部 API 的完整文档与常见任务的指南,请查阅 https://docs.github.com/cn

子模块

有种情况我们经常会遇到:某个工作中的项目需要包含并使用另一个项目。也许是第三方库,或者你独立开发的、用于多个父项目的库。现在问题来了:你想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。

我们举一个例子。假设你正在开发一个网站然后创建了 Atom 订阅。你决定使用一个库,而不是写自己的 Atom 生成代码。你可能不得不通过 CPAN 安装或 Ruby gem 来包含共享库中的代码,或者将源代码直接拷贝到自己的项目中。如果将这个库包含进来,那么无论用何种方式都很难定制它,部署则更加困难,因为你必须确保每一个客户端都包含该库。如果将代码复制到自己的项目中,那么你做的任何自定义修改都会使合并上游的改动变得困难。

Git 通过子模块来解决这个问题。子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。

子模块

开始使用子模块

我们将要演示如何在一个被分成一个主项目与几个子项目的项目上开发。

我们首先将一个已存在的 Git 仓库添加为正在工作的仓库的子模块。你可以通过在 git submodule add 命令后面加上想要跟踪的项目 URL 来添加新的子模块。在本例中,我们将会添加一个名为 “DbConnector” 的库。

$ git submodule add https://github.com/chaconinc/DbConnector
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

默认情况下,子模块会将子项目放到一个与仓库同名的目录中,本例中是 “DbConnector”。如果你想要放到其他地方,那么可以在命令结尾添加一个不同的路径。

如果这时运行 git status,你会注意到几件事。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitmodules
    new file:   DbConnector

首先应当注意到新的 .gitmodules 文件。该配置文件保存了项目 URL 与已经拉取的本地目录之间的映射:

$ cat .gitmodules
[submodule "DbConnector"]
    path = DbConnector
    url = https://github.com/chaconinc/DbConnector

如果有多个子模块,该文件中就会有多条记录。要重点注意的是,该文件也像 .gitignore 文件一样受到(通过)版本控制。它会和该项目的其他部分一同被拉取推送。这就是克隆该项目的人知道去哪获得子模块的原因。

NOTE: 由于 .gitmodules 文件中的 URL 是人们首先尝试克隆/拉取的地方,因此请尽可能确保你使用的 URL 大家都能访问。例如,若你要使用的推送 URL 与他人的拉取 URL 不同,那么请使用他人能访问到的 URL。你也可以根据自己的需要,通过在本地执行 git config submodule.DbConnector.url <私有URL> 来覆盖这个选项的值。如果可行的话,一个相对路径会很有帮助。

git status 输出中列出的另一个是项目文件夹记录。如果你运行 git diff,会看到类似下面的信息:

$ git diff --cached DbConnector
diff --git a/DbConnector b/DbConnector
new file mode 160000
index 0000000..c3f01dc
--- /dev/null
+++ b/DbConnector
@@ -0,0 +1 @@
+Subproject commit c3f01dc8862123d317dd46284b05b6892c7b29bc

虽然 DbConnector 是工作目录中的一个子目录,但 Git 还是会将它视作一个子模块。当你不在那个目录中时,Git 并不会跟踪它的内容,而是将它看作该仓库中的一个特殊提交。

如果你想看到更漂亮的差异输出,可以给 git diff 传递 --submodule 选项。

$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..71fc376
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "DbConnector"]
+    path = DbConnector
+    url = https://github.com/chaconinc/DbConnector
Submodule DbConnector 0000000...c3f01dc (new submodule)

当你提交时,会看到类似下面的信息:

$ git commit -am 'added DbConnector module'
[master fb9093c] added DbConnector module
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 DbConnector

注意 DbConnector 记录的 160000 模式。这是 Git 中的一种特殊模式,它本质上意味着你是将一次提交记作一项目录记录的,而非将它记录成一个子目录或者一个文件。

克隆含有子模块的项目

接下来我们将会克隆一个含有子模块的项目。当你在克隆这样的项目时,默认会包含该子模块目录,但其中还没有任何文件:

$ git clone https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
$ cd MainProject
$ ls -la
total 16
drwxr-xr-x   9 schacon  staff  306 Sep 17 15:21 .
drwxr-xr-x   7 schacon  staff  238 Sep 17 15:21 ..
drwxr-xr-x  13 schacon  staff  442 Sep 17 15:21 .git
-rw-r--r--   1 schacon  staff   92 Sep 17 15:21 .gitmodules
drwxr-xr-x   2 schacon  staff   68 Sep 17 15:21 DbConnector
-rw-r--r--   1 schacon  staff  756 Sep 17 15:21 Makefile
drwxr-xr-x   3 schacon  staff  102 Sep 17 15:21 includes
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 scripts
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 src
$ cd DbConnector/
$ ls
$

其中有 DbConnector 目录,不过是空的。你必须运行两个命令:git submodule init 用来初始化本地配置文件,而 git submodule update 则从该项目中抓取所有数据并检出父项目中列出的合适的提交。

$ git submodule init
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
$ git submodule update
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

现在 DbConnector 子目录是处在和之前提交时相同的状态了。

不过还有更简单一点的方式。如果给 git clone 命令传递 --recursive 选项,它就会自动初始化并更新仓库中的每一个子模块。

$ git clone --recursive https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

在包含子模块的项目上工作

现在我们有一份包含子模块的项目副本,我们将会同时在主项目和子模块项目上与队员协作。

拉取上游修改

在项目中使用子模块的最简模型,就是只使用子项目并不时地获取更新,而并不在你的检出中进行任何更改。我们来看一个简单的例子。

如果想要在子模块中查看新工作,可以进入到目录中运行 git fetchgit merge,合并上游分支来更新本地代码。

$ git fetch
From https://github.com/chaconinc/DbConnector
   c3f01dc..d0354fc  master     -> origin/master
$ git merge origin/master
Updating c3f01dc..d0354fc
Fast-forward
 scripts/connect.sh | 1 +
 src/db.c           | 1 +
 2 files changed, 2 insertions(+)

如果你现在返回到主项目并运行 git diff --submodule,就会看到子模块被更新的同时获得了一个包含新添加提交的列表。如果你不想每次运行 git diff 时都输入 --submodule,那么可以将 diff.submodule 设置为 “log” 来将其作为默认行为。

$ git config --global diff.submodule log
$ git diff
Submodule DbConnector c3f01dc..d0354fc:
  > more efficient db routine
  > better connection routine

如果在此时提交,那么你会将子模块锁定为其他人更新时的新代码。

如果你不想在子目录中手动抓取与合并,那么还有种更容易的方式。运行 git submodule update --remote,Git 将会进入子模块然后抓取并更新。

$ git submodule update --remote DbConnector
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   3f19983..d0354fc  master     -> origin/master
Submodule path 'DbConnector': checked out 'd0354fc054692d3906c85c3af05ddce39a1c0644'

此命令默认会假定你想要更新并检出子模块仓库的 master 分支。不过你也可以设置为想要的其他分支。例如,你想要 DbConnector 子模块跟踪仓库的 “stable” 分支,那么既可以在 .gitmodules 文件中设置(这样其他人也可以跟踪它),也可以只在本地的 .git/config 文件中设置。让我们在 .gitmodules 文件中设置它:

$ git config -f .gitmodules submodule.DbConnector.branch stable
$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   27cf5d3..c87d55d  stable -> origin/stable
Submodule path 'DbConnector': checked out 'c87d55d4c6d4b05ee34fbc8cb6f7bf4585ae6687'

如果不用 -f .gitmodules 选项,那么它只会为你做修改。但是在仓库中保留跟踪信息更有意义一些,因为其他人也可以得到同样的效果。

这时我们运行 git status,Git 会显示子模块中有 “新提交”。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   .gitmodules
    modified:   DbConnector (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

如果你设置了配置选项 status.submodulesummary,Git 也会显示你的子模块的更改摘要:

$ git config status.submodulesummary 1
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   .gitmodules
    modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c3f01dc...c87d55d (4):
  > catch non-null terminated lines

这时如果运行 git diff,可以看到我们修改了 .gitmodules 文件,同时还有几个已拉取的提交需要提交到我们自己的子模块项目中。

$ git diff
diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
 Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

这非常有趣,因为我们可以直接看到将要提交到子模块中的提交日志。提交之后,你也可以运行 git log -p 查看这个信息。

$ git log -p --submodule
commit 0a24cfc121a8a3c118e0105ae4ae4c00281cf7ae
Author: Scott Chacon <schacon@gmail.com>
Date:   Wed Sep 17 16:37:02 2014 +0200

    updating DbConnector for bug fixes

diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

当运行 git submodule update --remote 时,Git 默认会尝试更新所有子模块,所以如果有很多子模块的话,你可以传递想要更新的子模块的名字。

在子模块上工作

你很有可能正在使用子模块,因为你确实想在子模块中编写代码的同时,还想在主项目上编写代码(或者跨子模块工作)。否则你大概只能用简单的依赖管理系统(如 Maven 或 Rubygems)来替代了。

现在我们将通过一个例子来演示如何在子模块与主项目中同时做修改,以及如何同时提交与发布那些修改。

到目前为止,当我们运行 git submodule update 从子模块仓库中抓取修改时,Git 将会获得这些改动并更新子目录中的文件,但是会将子仓库留在一个称作 “游离的 HEAD” 的状态。这意味着没有本地工作分支(例如 “master”)跟踪改动。所以你做的任何改动都不会被跟踪。

为了将子模块设置得更容易进入并修改,你需要做两件事。首先,进入每个子模块并检出其相应的工作分支。接着,若你做了更改就需要告诉 Git 它该做什么,然后运行 git submodule update --remote 来从上游拉取新工作。你可以选择将它们合并到你的本地工作中,也可以尝试将你的工作变基到新的更改上。

首先,让我们进入子模块目录然后检出一个分支。

$ git checkout stable
Switched to branch 'stable'

然后尝试用 “merge” 选项。为了手动指定它,我们只需给 update 添加 --merge 选项即可。这时我们将会看到服务器上的这个子模块有一个改动并且它被合并了进来。

$ git submodule update --remote --merge
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   c87d55d..92c7337  stable -> origin/stable
Updating c87d55d..92c7337
Fast-forward
 src/main.c | 1 +
 1 file changed, 1 insertion(+)
Submodule path 'DbConnector': merged in '92c7337b30ef9e0893e758dac2459d07362ab5ea'

如果我们进入 DbConnector 目录,可以发现新的改动已经合并入本地 stable 分支。现在让我们看看当我们对库做一些本地的改动而同时其他人推送另外一个修改到上游时会发生什么。

$ cd DbConnector/
$ vim src/db.c
$ git commit -am 'unicode support'
[stable f906e16] unicode support
 1 file changed, 1 insertion(+)

如果我们现在更新子模块,就会看到当我们在本地做了更改时上游也有一个改动,我们需要将它并入本地。

$ git submodule update --remote --rebase
First, rewinding head to replay your work on top of it...
Applying: unicode support
Submodule path 'DbConnector': rebased into '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

如果你忘记 --rebase--merge,Git 会将子模块更新为服务器上的状态。并且会将项目重置为一个游离的 HEAD 状态。

$ git submodule update --remote
Submodule path 'DbConnector': checked out '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

即便这真的发生了也不要紧,你只需回到目录中再次检出你的分支(即还包含着你的工作的分支)然后手动地合并或变基 origin/stable(或任何一个你想要的远程分支)就行了。

如果你没有提交子模块的改动,那么运行一个子模块更新也不会出现问题,此时 Git 会只抓取更改而并不会覆盖子模块目录中未保存的工作。

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 4 (delta 0)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   5d60ef9..c75e92a  stable -> origin/stable
error: Your local changes to the following files would be overwritten by checkout:
    scripts/setup.sh
Please, commit your changes or stash them before you can switch branches.
Aborting
Unable to checkout 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

如果你做了一些与上游改动冲突的改动,当运行更新时 Git 会让你知道。

$ git submodule update --remote --merge
Auto-merging scripts/setup.sh
CONFLICT (content): Merge conflict in scripts/setup.sh
Recorded preimage for 'scripts/setup.sh'
Automatic merge failed; fix conflicts and then commit the result.
Unable to merge 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

你可以进入子模块目录中然后就像平时那样修复冲突。

发布子模块改动

现在我们的子模块目录中有一些改动。其中有一些是我们通过更新从上游引入的,而另一些是本地生成的,由于我们还没有推送它们,所以对任何其他人都不可用。

$ git diff
Submodule DbConnector c87d55d..82d2ad3:
  > Merge from origin/stable
  > updated setup script
  > unicode support
  > remove unnecessary method
  > add new option for conn pooling

如果我们在主项目中提交并推送但并不推送子模块上的改动,其他尝试检出我们修改的人会遇到麻烦,因为他们无法得到依赖的子模块改动。那些改动只存在于我们本地的拷贝中。

为了确保这不会发生,你可以让 Git 在推送到主项目前检查所有子模块是否已推送。git push 命令接受可以设置为 “check” 或 “on-demand” 的 --recurse-submodules 参数。如果任何提交的子模块改动没有推送那么 “check” 选项会直接使 push 操作失败。

$ git push --recurse-submodules=check
The following submodule paths contain changes that can not be found on any remote:
  DbConnector

Please try

    git push --recurse-submodules=on-demand

or cd to the path and use

    git push

to push them to a remote.

如你所见,它也给我们了一些有用的建议,指导接下来该如何做。最简单的选项是进入每一个子模块中然后手动推送到远程仓库,确保它们能被外部访问到,之后再次尝试这次推送。

另一个选项是使用 “on-demand” 值,它会尝试为你这样做。

$ git push --recurse-submodules=on-demand
Pushing submodule 'DbConnector'
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 3), reused 0 (delta 0)
To https://github.com/chaconinc/DbConnector
   c75e92a..82d2ad3  stable -> stable
Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 266 bytes | 0 bytes/s, done.
Total 2 (delta 1), reused 0 (delta 0)
To https://github.com/chaconinc/MainProject
   3d6d338..9a377d1  master -> master

如你所见,Git 进入到 DbConnector 模块中然后在推送主项目前推送了它。如果那个子模块因为某些原因推送失败,主项目也会推送失败。

合并子模块改动

如果你其他人同时改动了一个子模块引用,那么可能会遇到一些问题。也就是说,如果子模块的历史已经分叉并且在父项目中分别提交到了分叉的分支上,那么你需要做一些工作来修复它。

如果一个提交是另一个的直接祖先(一个快进式合并),那么 Git 会简单地选择之后的提交来合并,这样没什么问题。

不过,Git 甚至不会尝试去进行一次简单的合并。如果子模块提交已经分叉且需要合并,那你会得到类似下面的信息:

$ git pull
remote: Counting objects: 2, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 2 (delta 1), reused 2 (delta 1)
Unpacking objects: 100% (2/2), done.
From https://github.com/chaconinc/MainProject
   9a377d1..eb974f8  master     -> origin/master
Fetching submodule DbConnector
warning: Failed to merge submodule DbConnector (merge following commits not found)
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

所以本质上 Git 在这里指出了子模块历史中的两个分支记录点已经分叉并且需要合并。它将其解释为 “merge following commits not found”(未找到接下来需要合并的提交),虽然这有点令人困惑,不过之后我们会解释为什么是这样。

为了解决这个问题,你需要弄清楚子模块应该处于哪种状态。奇怪的是,Git 并不会给你多少能帮你摆脱困境的信息,甚至连两边提交历史中的 SHA-1 值都没有。幸运的是,这很容易解决。如果你运行 git diff,就会得到试图合并的两个分支中记录的提交的 SHA-1 值。

$ git diff
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector

所以,在本例中,eb41d76 是我们的子模块中大家共有的提交,而 c771610 是上游拥有的提交。如果我们进入子模块目录中,它应该已经在 eb41d76 上了,因为合并没有动过它。如果不是的话,无论什么原因,你都可以简单地创建并检出一个指向它的分支。

来自另一边的提交的 SHA-1 值比较重要。它是需要你来合并解决的。你可以尝试直接通过 SHA-1 合并,也可以为它创建一个分支然后尝试合并。我们建议后者,哪怕只是为了一个更漂亮的合并提交信息。

所以,我们将会进入子模块目录,基于 git diff 的第二个 SHA 创建一个分支然后手动合并。

$ cd DbConnector
$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135
$ git branch try-merge c771610
(DbConnector) $ git merge try-merge
Auto-merging src/main.c
CONFLICT (content): Merge conflict in src/main.c
Recorded preimage for 'src/main.c'
Automatic merge failed; fix conflicts and then commit the result.

我们在这儿得到了一个真正的合并冲突,所以如果想要解决并提交它,那么只需简单地通过结果来更新主项目。

$ vim src/main.c ①
$ git add src/main.c
$ git commit -am 'merged our changes'
Recorded resolution for 'src/main.c'.
[master 9fd905e] merged our changes
$ cd .. ②
$ git diff ③
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector
@@@ -1,1 -1,1 +1,1 @@@
- Subproject commit eb41d764bccf88be77aced643c13a7fa86714135
 -Subproject commit c77161012afbbe1f58b5053316ead08f4b7e6d1d
++Subproject commit 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a
$ git add DbConnector ④
$ git commit -m "Merge Tom's Changes" ⑤
[master 10d2c60] Merge Tom's Changes
  1. 首先解决冲突
  2. 然后返回到主项目目录中
  3. 再次检查 SHA-1 值
  4. 解决冲突的子模块记录
  5. 提交我们的合并

这可能会让你有点儿困惑,但它确实不难。

有趣的是,Git 还能处理另一种情况。如果子模块目录中存在着这样一个合并提交,它的历史中包含了的两边的提交,那么 Git 会建议你将它作为一个可行的解决方案。它看到有人在子模块项目的某一点上合并了包含这两次提交的分支,所以你可能想要那个。

这就是为什么前面的错误信息是 “merge following commits not found”,因为它不能 这样 做。它让人困惑是因为谁能想到它会尝试这样做?

如果它找到了一个可以接受的合并提交,你会看到类似下面的信息:

$ git merge origin/master
warning: Failed to merge submodule DbConnector (not fast-forward)
Found a possible merge resolution for the submodule:
 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a: > merged our changes
If this is correct simply add it to the index for example
by using:
  git update-index --cacheinfo 160000 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a "DbConnector"
which will accept this suggestion.
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

它会建议你更新索引,就像你运行了 git add 那样,这样会清除冲突然后提交。不过你可能不应该这样做。你可以轻松地进入子模块目录,查看差异是什么,快进到这次提交,恰当地测试,然后提交它。

$ cd DbConnector/
$ git merge 9fd905e
Updating eb41d76..9fd905e
Fast-forward
$ cd ..
$ git add DbConnector
$ git commit -am 'Fast forwarded to a common submodule child'

这些命令完成了同一件事,但是通过这种方式你至少可以验证工作是否有效,以及当你在完成时可以确保子模块目录中有你的代码。

子模块技巧

你可以做几件事情来让用子模块工作轻松一点儿。

子模块遍历

有一个 foreach 子模块命令,它能在每一个子模块中运行任意命令。如果项目中包含了大量子模块,这会非常有用。

例如,假设我们想要开始开发一项新功能或者修复一些错误,并且需要在几个子模块内工作。我们可以轻松地保存所有子模块的工作进度。

$ git submodule foreach 'git stash'
Entering 'CryptoLibrary'
No local changes to save
Entering 'DbConnector'
Saved working directory and index state WIP on stable: 82d2ad3 Merge from origin/stable
HEAD is now at 82d2ad3 Merge from origin/stable

然后我们可以创建一个新分支,并将所有子模块都切换过去。

$ git submodule foreach 'git checkout -b featureA'
Entering 'CryptoLibrary'
Switched to a new branch 'featureA'
Entering 'DbConnector'
Switched to a new branch 'featureA'

你应该明白。能够生成一个主项目与所有子项目的改动的统一差异是非常有用的。

$ git diff; git submodule foreach 'git diff'
Submodule DbConnector contains modified content
diff --git a/src/main.c b/src/main.c
index 210f1ae..1f0acdc 100644
--- a/src/main.c
+++ b/src/main.c
@@ -245,6 +245,8 @@ static int handle_alias(int *argcp, const char ***argv)
      commit_pager_choice();

+     url = url_decode(url_orig);
+
      /* build alias_argv */
      alias_argv = xmalloc(sizeof(*alias_argv) * (argc + 1));
      alias_argv[0] = alias_string + 1;
Entering 'DbConnector'
diff --git a/src/db.c b/src/db.c
index 1aaefb6..5297645 100644
--- a/src/db.c
+++ b/src/db.c
@@ -93,6 +93,11 @@ char *url_decode_mem(const char *url, int len)
        return url_decode_internal(&url, len, NULL, &out, 0);
 }

+char *url_decode(const char *url)
+{
+       return url_decode_mem(url, strlen(url));
+}
+
 char *url_decode_parameter_name(const char **query)
 {
        struct strbuf out = STRBUF_INIT;

在这里,我们看到子模块中定义了一个函数并在主项目中调用了它。这明显是个简化了的例子,但是希望它能让你明白这种方法的用处。

有用的别名

你可能想为其中一些命令设置别名,因为它们可能会非常长而你又不能设置选项作为它们的默认选项。我们在 Git 别名 介绍了设置 Git 别名,但是如果你计划在 Git 中大量使用子模块的话,这里有一些例子。

$ git config alias.sdiff '!'"git diff && git submodule foreach 'git diff'"
$ git config alias.spush 'push --recurse-submodules=on-demand'
$ git config alias.supdate 'submodule update --remote --merge'

这样当你想要更新子模块时可以简单地运行 git supdate,或 git spush 检查子模块依赖后推送。

子模块的问题

然而使用子模块还是有一些小问题。

例如在有子模块的项目中切换分支可能会造成麻烦。如果你创建一个新分支,在其中添加一个子模块,之后切换到没有该子模块的分支上时,你仍然会有一个还未跟踪的子模块目录。

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...
$ git commit -am 'adding crypto library'
[add-crypto 4445836] adding crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary
$ git checkout master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    CryptoLibrary/

nothing added to commit but untracked files present (use "git add" to track)

移除那个目录并不困难,但是有一个目录在那儿会让人有一点困惑。如果你移除它然后切换回有那个子模块的分支,需要运行 submodule update --init 来重新建立和填充。

$ git clean -fdx
Removing CryptoLibrary/
$ git checkout add-crypto
Switched to branch 'add-crypto'
$ ls CryptoLibrary/
$ git submodule update --init
Submodule path 'CryptoLibrary': checked out 'b8dda6aa182ea4464f3f3264b11e0268545172af'
$ ls CryptoLibrary/
Makefile    includes    scripts        src

再说一遍,这真的不难,只是会让人有点儿困惑。

另一个主要的告诫是许多人遇到了将子目录转换为子模块的问题。如果你在项目中已经跟踪了一些文件,然后想要将它们移动到一个子模块中,那么请务必小心,否则 Git 会对你发脾气。假设项目内有一些文件在子目录中,你想要将其转换为一个子模块。如果删除子目录然后运行 submodule add,Git 会朝你大喊:

$ rm -Rf CryptoLibrary/
$ git submodule add https://github.com/chaconinc/CryptoLibrary
'CryptoLibrary' already exists in the index

你必须要先取消暂存 CryptoLibrary 目录。然后才可以添加子模块:

$ git rm -r CryptoLibrary
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

现在假设你在一个分支下做了这样的工作。如果尝试切换回的分支中那些文件还在子目录而非子模块中时 - 你会得到这个错误:

$ git checkout master
error: The following untracked working tree files would be overwritten by checkout:
  CryptoLibrary/Makefile
  CryptoLibrary/includes/crypto.h
  ...
Please move or remove them before you can switch branches.
Aborting

你可以通过 checkout -f 来强制切换,但是要小心,如果其中还有未保存的修改,这个命令会把它们覆盖掉。

$ git checkout -f master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'

当你切换回来之后,因为某些原因你得到了一个空的 CryptoLibrary 目录,并且 git submodule update 也无法修复它。你需要进入到子模块目录中运行 git checkout . 来找回所有的文件。你也可以通过 submodule foreach 脚本来为多个子模块运行它。

要特别注意的是,近来子模块会将它们的所有 Git 数据保存在顶级项目的 .git 目录中,所以不像旧版本的 Git,摧毁一个子模块目录并不会丢失任何提交或分支。

拥有了这些工具,使用子模块会成为可以在几个相关但却分离的项目上同时开发的相当简单有效的方法。