World Hello
2019-10-31T01:19:57+00:00
http://www.worldhello.net/
Jiang Xin
worldhello.net@gmail.com
Go 语言解析 git config
2019-03-17T00:00:00+00:00
http://www.worldhello.net/2019/03/17/goconfig
<p>最近做的一个 go 语言的项目需要频繁读写 git config 文件,一些看似现成的解决方案并不能满足需要:</p>
<ol>
<li>不考虑调用外部命令 <code class="language-plaintext highlighter-rouge">git config</code>,因为在 Windows 平台性能差。</li>
<li>不考虑 <code class="language-plaintext highlighter-rouge">libgit2</code>,因为会给静态编译和发布带来麻烦。</li>
</ol>
<p>在 github 上找到一个能够解析 git config 文件的项目 <a href="https://github.com/muja/goconfig">goconfig</a>,该项目的代码直接从 Git 项目的
<a href="https://github.com/git/git/blob/95ec6b1b3393eb6e26da40c565520a8db9796e9f/config.c">git/config.c</a> 移植过来,可以确保兼容性。但是这个项目只是一个半成品,因为:</p>
<ol>
<li>不支持多值配置项。Git 的很多设置实际上是多值配置项,例如:<code class="language-plaintext highlighter-rouge">push.pushOption</code>、<code class="language-plaintext highlighter-rouge">remote.<name>.fetch</code> 等。</li>
<li>不支持配置文件嵌套,即不支持通过 <code class="language-plaintext highlighter-rouge">include.path</code> 指令包含其他配置文件,而这在我们要开发的应用中至关重要。</li>
<li>不支持配置文件继承(多级配置)。Git 在读取一个配置项时,会依次读取系统级配置(<code class="language-plaintext highlighter-rouge">/etc/gitconfig</code>)、用户全局配置(<code class="language-plaintext highlighter-rouge">$HOME/.gitconfig</code>)、仓库级配置文件(<code class="language-plaintext highlighter-rouge">.git/config</code>)、嵌套的配置文件(<code class="language-plaintext highlighter-rouge">include.path</code> 指向的配置文件)。从优先级上看,仓库级配置文件高于全局配置,更高于系统级配置。</li>
<li>不支持写配置文件。</li>
</ol>
<p>于是派生了一个项目到 <a href="https://github.com/jiangxin/goconfig">jiangxin/goconfig</a>,实现了上述特性。</p>
<h2 id="增加多值配置特性">增加多值配置特性</h2>
<p>实际上 Git 配置项,无论单值(如 <code class="language-plaintext highlighter-rouge">user.name</code>),还是多值(如 <code class="language-plaintext highlighter-rouge">remote.<name>.fetch</code>),都应该一视同仁当做多值来处理,这样配置文件嵌套、配置文件继承的处理就非常简单了。即:</p>
<ol>
<li>每一个配置项(如 <code class="language-plaintext highlighter-rouge">user.name</code>)的值是一个字符串数组。</li>
<li>如果用户将某个配置项视为单值设置,只取数组的最后一项,作为该配置的唯一值。</li>
<li>如果用户将某个配置项视为多值设置,数组所有内容都是该配置项内容。</li>
</ol>
<p>为此将 <code class="language-plaintext highlighter-rouge">goconfig.go</code> 的返回值由 <code class="language-plaintext highlighter-rouge">map[string]string</code> 替换为支持多值配置的自定义类型</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>type GitConfig map[string]GitConfigKeys
type GitConfigKeys map[string][]string
</code></pre></div></div>
<p>其中 GitConfig 的索引对应 config 配置文件的小节(section)名称,GitConfigKeys 的索引对应于配置项在小节内的 key。</p>
<p>GitConfig 最核心的方法是 <code class="language-plaintext highlighter-rouge">GetAll</code>,其他方法 <code class="language-plaintext highlighter-rouge">Get</code>, <code class="language-plaintext highlighter-rouge">GetBool</code>, <code class="language-plaintext highlighter-rouge">GetInt</code> 等都是基于 <code class="language-plaintext highlighter-rouge">GetAll</code> 方法。</p>
<p>参见提交 <a href="https://github.com/jiangxin/goconfig/commit/9e83c3157189a458a63832c151fbd52222ddd56a">9e83c31 (Add GitConfig to read boolean, int, multi-values, 2019-02-28)</a>。</p>
<h2 id="增加配置文件嵌套特性">增加配置文件嵌套特性</h2>
<p>配置文件嵌套,涉及到迭代和配置文件的合并。核心方法是 GitConfig 的 <code class="language-plaintext highlighter-rouge">Merge</code> 方法。</p>
<p>参见提交 <a href="https://github.com/jiangxin/goconfig/commit/83a00ae5b8090415985162b6e3381de03532a573">83a00ae (Parse include config files from include.path, 2019-03-05)</a> 。</p>
<h2 id="增加配置文件继承">增加配置文件继承</h2>
<p>配置文件继承和配置文件嵌套非常相似,都是 GitConfig 结构体的 Merge,只不过多了一些文件 IO,以及缓存机制。</p>
<p>参见提交 <a href="https://github.com/jiangxin/goconfig/commit/a033f72a584b0c66a285cb29c50028ce3bbfba11">a033f72 (Add Load() function to read git config from disk, 2019-03-04)</a></p>
<h2 id="增加配置文件写操作">增加配置文件写操作</h2>
<p>实际上第一个版本的 GitConfig 结构体定义为 <code class="language-plaintext highlighter-rouge">map[string][]string</code>,就可以实现多值配置。为了支持将 GitConfig 结构体回写为文件,对其做了提交修补(git commit –amend)操作。
GitConfig 的 map 索引变成了小节(section)名称。</p>
<p>为了支持写操作所做的第二个改变是为每一个值增加了一个范围(scope),这样在保存 GitConfig 到文件的时候,知道哪些配置来自于系统级(ScopeSystem)、全局级(ScopeGlobal)、
文件嵌套(ScopeGlobal),只将配置文件中的 ScopeSelf 记录到配置文件中。</p>
<p>参见提交:</p>
<ul>
<li><a href="https://github.com/jiangxin/goconfig/commit/784eaa8d6f2f0822d41cb7ff3e4ed8c5bd1d7820">784eaa8 (Mark values with scope, such as system, global, self, 2019-03-09)</a></li>
<li><a href="https://github.com/jiangxin/goconfig/commit/feb3e92f752625e07e3627d5a951fc500cbba0bf">feb3e92 (String() of GitConfig is ready for saving file, 2019-03-11)</a></li>
<li><a href="https://github.com/jiangxin/goconfig/commit/b826f4926857921ac0851ae98926ddb82dd7dbcc">b826f49 (Save GitConfig to file using Save(), 2019-03-11)</a></li>
</ul>
<h2 id="一个简单的-git-config-实现">一个简单的 git-config 实现</h2>
<p>作为一个 lib,goconfig 项目的根目录是名为 <code class="language-plaintext highlighter-rouge">goconfig</code> 的包,为了演示如何用 <code class="language-plaintext highlighter-rouge">goconfig</code> 实现一个完整的 <code class="language-plaintext highlighter-rouge">git config</code> 命令的功能,在 <code class="language-plaintext highlighter-rouge">cmd/gocongig</code> 目录下写了一个 <code class="language-plaintext highlighter-rouge">main</code> 包。</p>
<p>可以用如下命令编译安装这一 <code class="language-plaintext highlighter-rouge">goconfig</code> 示例:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>go get github.com/jiangxin/goconfig/cmd/goconfig
</code></pre></div></div>
<p>示例代码参见:<a href="https://github.com/jiangxin/goconfig/blob/master/cmd/goconfig/main.go">cmd/goconfig/main.go</a>。</p>
<p>欢迎使用 goconfig 并帮助改进</p>
二分查找捉虫记
2016-02-29T00:00:00+00:00
http://www.worldhello.net/2016/02/29/git-bisect-on-git
<h2 id="1-问题现象">1. 问题现象</h2>
<p>Git 2.8.0 版本即将发布,今天把本地的 Git 版本升级到 <code class="language-plaintext highlighter-rouge">2.8.0-rc0</code>,结果悲剧了。所有使用 HTTP 协议的公司内部 Git 仓库都无法正常访问!</p>
<p>在确认不是网络和公司 Git 服务器问题之后,自然怀疑到了 HTTP 代理。果然清空了 <code class="language-plaintext highlighter-rouge">http_proxy</code> 环境变量后,Git 命令工作正常了:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http_proxy= git ls-remote http://server.name/git/repo.git
</code></pre></div></div>
<p>然而一旦通过 <code class="language-plaintext highlighter-rouge">http_proxy=bad_proxy</code> 环境变量设置了一个错误的代理,即便通过 <code class="language-plaintext highlighter-rouge">no_proxy=*</code> 期望绕过代理,Git 2.8.0 却无法正常工作:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ http_proxy=bad_proxy no_proxy=* git ls-remote http://server.name/git/repo.git
fatal: unable to access 'http://server.name/git/repo.git/': Couldn't resolve proxy 'bad_proxy'
</code></pre></div></div>
<p>可是我记得升级之前 Git 工作是正常的啊!于是在 Git 2.7.0 下进行了尝试。</p>
<p>将版本切换到 <code class="language-plaintext highlighter-rouge">v2.7.0</code>,编译安装 Git。(注意一定要安装,而不是执行当前目录下的 Git。这是因为该命令执行过程中会依次调用 <code class="language-plaintext highlighter-rouge">git-ls-remote</code>
和 <code class="language-plaintext highlighter-rouge">git-remote-http</code> 命令,而这两个命令是位于安装路径中的。)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git checkout v2.7.0
$ make -j8 && make install # 我的工作站是四核CPU,故此使用 -j8 两倍并发执行编译
</code></pre></div></div>
<p>测试发现 Git 2.7.0 能够通过 <code class="language-plaintext highlighter-rouge">no_proxy</code> 变量绕过错误的 <code class="language-plaintext highlighter-rouge">http_proxy</code> 环境变量:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ http_proxy=bad_proxy no_proxy=* git ls-remote http://server.name/git/repo.git
206b4906c197e76fcc63d7a453f9e3aa00dfb3da HEAD
206b4906c197e76fcc63d7a453f9e3aa00dfb3da refs/heads/master
</code></pre></div></div>
<p>显然一定是 <code class="language-plaintext highlighter-rouge">v2.7.0</code> 和 <code class="language-plaintext highlighter-rouge">v2.8.0-rc0</code> 中间的某个版本引入的 Bug!</p>
<h2 id="2-二分查找">2. 二分查找</h2>
<p>想必大家都玩过猜数字游戏吧:一个人在1到100的数字中随意选择一个,另外一个人来猜,小孩子总是一个挨着一个地猜,
懂得折半查找的大人总是获胜者。Git 提供的 git bisect 这一命令,就是采用这样的二分查找快速地在提交中定位 Bug,
少则几次,多则十几次就会定位到引入Bug的提交。</p>
<ol>
<li>
<p>首先执行下面命令启用二分查找。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git bisect start
</code></pre></div> </div>
</li>
<li>
<p>标记一个好版本。下面的命令使用 tag(v2.7.0)来标记 Git 2.7.0 版本是好版本,换做40位的提交号也行。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git bisect good v2.7.0
</code></pre></div> </div>
</li>
<li>
<p>标记 Git 2.8.0-rc0 是一个坏版本。注意:马上就是见证奇迹的时刻。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git bisect bad v2.8.0-rc0
Bisecting: 297 revisions left to test after this (roughly 8 steps)
[563e38491eaee6e02643a22c9503d4f774d6c5be] Fifth batch for 2.8 cycle
</code></pre></div> </div>
<p>看到了么?当完成对一个好版本和一个坏版本的标记后,Git 切换到一个中间版本(<code class="language-plaintext highlighter-rouge">563e384</code>),并告诉我们大概需要8步可以找到元凶。</p>
</li>
<li>
<p>在这个版本下执行前面的测试操作:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ make -j8 && make install
$ git --version
git version 2.7.0.297.g563e384
$ http_proxy=bad_proxy no_proxy=* git ls-remote http://server.name/git/repo.git
fatal: unable to access 'http://server.name/git/repo.git/': Couldn't resolve proxy 'bad_proxy'
</code></pre></div> </div>
</li>
<li>
<p>对这个版本进行标记。</p>
<p>这是一个坏版本:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git bisect bad
Bisecting: 126 revisions left to test after this (roughly 7 steps)
[e572fef9d459497de2bd719747d5625a27c9b41d] Merge branch 'ep/shell-command-substitution-style'
</code></pre></div> </div>
</li>
</ol>
<p>我们可以机械地重复上面4、5的步骤,直到最终定位。但是人工操作很容易出错。如果对版本标记错了,把 good 写成了 bad 或者相反,
就要执行 <code class="language-plaintext highlighter-rouge">git bisect reset</code> 重来。(小窍门:git bisect log 可以显示 git bisect 标记操作日志)</p>
<p>于是决定剩下的二分查找使用脚本来完成。</p>
<h2 id="3-自动化的二分查找">3. 自动化的二分查找</h2>
<p>Git 二分查找允许提供一个测试脚本,Git 会根据这个测试脚本的返回值,决定如何来标记提交:</p>
<ul>
<li>返回值为 0:这个提交是一个好提交。</li>
<li>返回值为 125:这个提交无法测试(例如编译不过去),忽略这个提交。</li>
<li>返回值为 1-127(125除外):这个提交是一个坏提交。</li>
<li>其他返回值:二分查找出错,终止二分查找操作。</li>
</ul>
<p>那么就我们先来看看 <code class="language-plaintext highlighter-rouge">git ls-remote</code> 的返回值:</p>
<ul>
<li>
<p>正确执行的返回值是 0:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ http_proxy= git ls-remote http://server.name/git/repo.git
206b4906c197e76fcc63d7a453f9e3aa00dfb3da HEAD
206b4906c197e76fcc63d7a453f9e3aa00dfb3da refs/heads/master
$ echo $?
0
</code></pre></div> </div>
</li>
<li>
<p>错误执行的返回值是 128!</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ http_proxy=bad_proxy git ls-remote http://server.name/git/repo.git
fatal: unable to access 'http://server.name/git/repo.git/': Couldn't resolve proxy 'bad_proxy'
$ echo $?
128
</code></pre></div> </div>
</li>
</ul>
<p>于是创建一个测试脚本 <code class="language-plaintext highlighter-rouge">git-proxy-bug-test.sh</code>,内容如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/sh
make -j8 && make install && \
git --version && \
http_proxy=bad_proxy no_proxy=* \
git ls-remote http://server.name/git/repo.git
case $? in
0)
exit 0
;;
128)
exit 1
;;
*)
exit 128
;;
esac
</code></pre></div></div>
<p>然后敲下如下命令,开始自动执行二分查找:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git bisect run sh git-proxy-bug-test.sh
</code></pre></div></div>
<p>自动化查找过程可能需要几分钟,站起来走走,休息一下眼睛。再回到座位,最终的定位结果就展现在了眼前:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>372370f1675c2b935fb703665358dd5567641107 is the first bad commit
commit 372370f1675c2b935fb703665358dd5567641107
Author: Knut Franke <k.franke@science-computing.de>
Date: Tue Jan 26 13:02:48 2016 +0000
http: use credential API to handle proxy authentication
Currently, the only way to pass proxy credentials to curl is by including them
in the proxy URL. Usually, this means they will end up on disk unencrypted, one
way or another (by inclusion in ~/.gitconfig, shell profile or history). Since
proxy authentication often uses a domain user, credentials can be security
sensitive; therefore, a safer way of passing credentials is desirable.
If the configured proxy contains a username but not a password, query the
credential API for one. Also, make sure we approve/reject proxy credentials
properly.
For consistency reasons, add parsing of http_proxy/https_proxy/all_proxy
environment variables, which would otherwise be evaluated as a fallback by curl.
Without this, we would have different semantics for git configuration and
environment variables.
Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Helped-by: Elia Pinto <gitter.spiros@gmail.com>
Signed-off-by: Knut Franke <k.franke@science-computing.de>
Signed-off-by: Elia Pinto <gitter.spiros@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
:040000 040000 de69688dd93e4466c11726157bd2f93e47e67330 d19d021e8d1c2a296b521414112be0966bd9f09a M Documentation
:100644 100644 f46bfc43f9e5e8073563be853744262a1bb4c5d6 dfc53c1e2554e76126459d6cb1f098facac28593 M http.c
:100644 100644 4f97b60b5c8abdf5ab0610382a6d6fa289df2605 f83cfa686823728587b2a803c3e84a8cd4669220 M http.h
二分查找运行成功
</code></pre></div></div>
<h2 id="4-解决问题">4. 解决问题</h2>
<p>既然我们知道引入 Bug 的提交,让我们看看这个提交:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git show --oneline --stat 372370f1675c2b935fb703665358dd5567641107
372370f http: use credential API to handle proxy authentication
Documentation/config.txt | 10 +++++--
http.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++
http.h | 1 +
3 files changed, 85 insertions(+), 3 deletions(-)
</code></pre></div></div>
<p>相比很多人一个提交动辄改动几百、几千行的代码,这个提交的改动算得上简短了。小提交的好处就是易于阅读、易于问题定位、易于回退。</p>
<p>最终参照上面定位到的问题提交,我的 Bugfix 如下(为了下面的一节叙述方便,给代码补丁增加了行号):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>01 diff --git a/http.c b/http.c
02 index 1d5e3bb..69da445 100644
03 --- a/http.c
04 +++ b/http.c
05 @@ -70,6 +70,7 @@ static long curl_low_speed_limit = -1;
06 static long curl_low_speed_time = -1;
07 static int curl_ftp_no_epsv;
08 static const char *curl_http_proxy;
09 +static const char *curl_no_proxy;
10 static const char *http_proxy_authmethod;
12 static struct {
13 const char *name;
13 @@ -624,6 +625,11 @@ static CURL *get_curl_handle(void)
15 }
16
17 curl_easy_setopt(result, CURLOPT_PROXY, proxy_auth.host);
18 +#if LIBCURL_VERSION_NUM >= 0x071304
19 + var_override(&curl_no_proxy, getenv("NO_PROXY"));
20 + var_override(&curl_no_proxy, getenv("no_proxy"));
21 + curl_easy_setopt(result, CURLOPT_NOPROXY, curl_no_proxy);
22 +#endif
23 }
24 init_curl_proxy_auth(result);
25
26 --
27 2.8.0.rc0
</code></pre></div></div>
<h2 id="5-写提交说明">5. 写提交说明</h2>
<p>这个提交是要贡献给 Git 上游的,评审者可能会问我如下问题:</p>
<ol>
<li>
<p>Bug 的现象是什么?</p>
<p>“系统的 no_proxy 变量不起作用,git 可能无法访问 http 协议的仓库。”</p>
</li>
<li>
<p>从什么版本引入这个 Bug?</p>
<p>“我们定位到的这个提交引入的 Bug。之所以会引入这个 Bug,是因为这个提交读取了 <code class="language-plaintext highlighter-rouge">http_proxy</code> 等环境变量,
自动通过 <code class="language-plaintext highlighter-rouge">git-credential</code> 获取的信息补齐 <code class="language-plaintext highlighter-rouge">http_proxy</code> 的缺失的代理认证口令,并显示设置 libcurl 的参数。”</p>
</li>
<li>
<p>之前的版本为什么没有出现这个问题?什么条件下会出现?</p>
<p>“之前的版本也会出现问题,但是只有在用户主动设置了 <code class="language-plaintext highlighter-rouge">http.proxy</code> 配置变量才会出现。
用户很少会去设置 <code class="language-plaintext highlighter-rouge">http.proxy</code> 配置变量,而通常是使用 <code class="language-plaintext highlighter-rouge">http_proxy</code> 环境变量。”</p>
</li>
<li>
<p>你是如何解决的?你的解决方案是否最佳?</p>
<p>“读取 <code class="language-plaintext highlighter-rouge">no_proxy</code> 环境变量,并为 <code class="language-plaintext highlighter-rouge">libcurl</code> 配置相应参数。因为 <code class="language-plaintext highlighter-rouge">libcurl</code> 只在 <code class="language-plaintext highlighter-rouge">7.19.4</code> 之后才引入 <code class="language-plaintext highlighter-rouge">CURLOPT_NOPROXY</code>,因此需要添加条件编译。”</p>
</li>
</ol>
<p>实际上,前面的 Bugfix 原本是没有那个条件编译的。即补丁的第18行、22行一开始是没有的,
在回答第4个问题的时候,我仔细查看了 <a href="https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html">libcurl API</a>,
发现只有在 <code class="language-plaintext highlighter-rouge">7.19.4</code> 版本之后,才支持 <code class="language-plaintext highlighter-rouge">CURLOPT_NOPROXY</code> 参数,因此如果不添加这个编译条件,
在特定的平台可能会导致 Git 无法编译通过。</p>
<p>下面就是最终的提交说明:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http: honor no_http env variable to bypass proxy
Curl and its families honor several proxy related environment variables:
* http_proxy and https_proxy define proxy for http/https connections.
* no_proxy (a comma separated hosts) defines hosts bypass the proxy.
This command will bypass the bad-proxy and connect to the host directly:
no_proxy=* https_proxy=http://bad-proxy/ \
curl -sk https://google.com/
Before commit 372370f (http: use credential API to handle proxy auth...),
Environment variable "no_proxy" will take effect if the config variable
"http.proxy" is not set. So the following comamnd won't fail if not
behind a firewall.
no_proxy=* https_proxy=http://bad-proxy/ \
git ls-remote https://github.com/git/git
But commit 372370f not only read git config variable "http.proxy", but
also read "http_proxy" and "https_proxy" environment variables, and set
the curl option using:
curl_easy_setopt(result, CURLOPT_PROXY, proxy_auth.host);
This caused "no_proxy" environment variable not working any more.
Set extra curl option "CURLOPT_NOPROXY" will fix this.
Signed-off-by: Jiang Xin <xin.jiang@huawei.com>
</code></pre></div></div>
<h2 id="6-贡献给上游">6. 贡献给上游</h2>
<p>Git 项目本身是通过邮件列表参与代码贡献的,基本的操作流程是将代码转换为补丁文件,然后邮件发送。
基本上就是两条命令:<code class="language-plaintext highlighter-rouge">git format-patch</code> 和 <code class="language-plaintext highlighter-rouge">git send-email</code>。</p>
<p>下面的链接就是 Git 社区关于我这个提交的讨论。Junio已经确认这个提交是 2.8.0 的一个 regression,相信会合入2.8.0的发布版。</p>
<ul>
<li>[2/29] <a href="http://thread.gmane.org/gmane.comp.version-control.git/287843/focus=287888">向社区提交补丁,及相关讨论的邮件存档</a>。</li>
<li>[3/04] <a href="http://article.gmane.org/gmane.comp.version-control.git/288276">What’s cooking in git.git (Mar 2016, #02; Fri, 4)</a>:该补丁处于待 review 状态,代码停留在 <code class="language-plaintext highlighter-rouge">pu</code> 分支。</li>
</ul>
做一个有品位的程序员
2015-12-23T00:00:00+00:00
http://www.worldhello.net/2015/12/23/taste-of-a-programmer
<p>——“能够写出漂亮代码的程序员就是有品味的程序员么?”</p>
<p>——“还不够。品味来自于每一个细节,有品位的程序员会把每一次提交做小、做对、做好,尽量做到整个开发的过程的无可挑剔,这样才够逼格,才可以称为有品位。”</p>
<p>熟练使用 Git,会让程序员更有品味。</p>
<h2 id="提交做小">提交做小</h2>
<p>写小提交就是将问题解耦:“Do one thing and do it well”。开源项目的提交通常都很小,每个提交只修改一个到几个文件,每次只修改几行到几十行。
找一个你熟悉的开源项目,在仓库中执行下面的命令,可以很容易地统计出来每个提交的修改量。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git log --no-merges --pretty="" --shortstat
2 files changed, 25 insertions(+), 4 deletions(-)
1 file changed, 4 insertions(+), 12 deletions(-)
2 files changed, 30 insertions(+), 1 deletion(-)
3 files changed, 15 insertions(+), 5 deletions(-)
</code></pre></div></div>
<p>而我看到的很多公司内外的项目则不是这样,动辄成百上千的文件修改,一次改动成千上万行源代码。试问这样的提交能拿来给人看么?</p>
<p>可是在开发过程中,程序员一旦进入状态,往往才思如泉涌,写出一个大提交。比如我又一次向 Git 贡献代码时,
提交还不算太大,就被 Git 的维护者 Junio 吐槽,要我拆分提交,便于评审:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TODO
</code></pre></div></div>
<p>那么如何将提交拆分为若干个小提交呢?</p>
<h3 id="拆分当前提交松耦合">拆分当前提交(松耦合)</h3>
<p>先以拆分最新的提交为例,可以如下操作:</p>
<ol>
<li>
<p>将当前提交撤销,重置到上一次提交。撤销提交的改动保留在工作区中。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git reset HEAD^
</code></pre></div> </div>
</li>
<li>
<p>通过补丁块拣选方式选择要提交的修改。Git 会逐一显示工作区更改,如果确认此处改动要提交,输入“y“。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git add -p
</code></pre></div> </div>
</li>
<li>
<p>以撤销提交的提交说明为蓝本,撰写新的提交。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git commit -e -C HEAD@{1}
</code></pre></div> </div>
</li>
<li>
<p>对于工作区其余的修改进行提交,完成一个提交拆分为两个的操作。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git add -u
$ git commit
</code></pre></div> </div>
</li>
</ol>
<h3 id="拆分当前提交紧耦合">拆分当前提交(紧耦合)</h3>
<p>如果要拆分的提交,不同的实现逻辑耦合在一起,难以通过补丁块拣选(<code class="language-plaintext highlighter-rouge">git add -p</code>)的方式修改提交,怎么办?这时可以
直接编辑文件,删除要剥离出此次提交的修改,然后执行:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git commit --amend
</code></pre></div></div>
<p>然后执行下面的命令,还原原有的文件修改,然后再提交。如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git checkout HEAD@{1} -- .
$ git commit
</code></pre></div></div>
<p>同样完成了一个提交拆分为两个提交的操作。</p>
<h3 id="拆分历史某个提交">拆分历史某个提交</h3>
<p>如果要拆分的是历史提交(如提交 54321),而非当前提交,则可以执行交互式变基(<code class="language-plaintext highlighter-rouge">git rebase -i</code>),如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git rebase -i 54321^
</code></pre></div></div>
<p>Git 会自动将参与变基的提交写在一个动作文件中,还会自动打开编辑器(比如 vi 编辑器)。在编辑器中显示内容示例如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pick 54321 要拆分的提交
pick ... 其他参与变基的提交
</code></pre></div></div>
<p>将要拆分的提交 54321 前面的关键字 <code class="language-plaintext highlighter-rouge">pick</code> 修改为 <code class="language-plaintext highlighter-rouge">edit</code>,保存并退出。变基操作随即开始执行。</p>
<p>首先会在提交 54321 处停下来,这时要拆分的提交成为了当前提交,参照前面“拆分当前提交”的方法对提交 54321 进行拆分。拆分结束再执行
<code class="language-plaintext highlighter-rouge">git rebase --continue</code> 完成整个变基操作。</p>
<h2 id="提交做对">提交做对</h2>
<p>“好的文章不是写出来的,而是改出来的。” 代码提交也是如此。</p>
<p>程序员写完代码,往往迫不及待地敲下:<code class="language-plaintext highlighter-rouge">git commit</code>,然后发现提交中少了一个文件,或者提交了多余的文件,或者发现提交中包含错误无法编译,或者提交说明中出现了错别字。</p>
<p>Git 提供了一个修改当前提交的快捷命令:<code class="language-plaintext highlighter-rouge">git commit --amend</code>,相信很多人都用过,不再赘述。</p>
<p>如果你发现错误出现在上一个提交或其他历史提交中怎么办呢?我有一个小窍门,在《Git权威指南》里我没有写到哦。</p>
<p>比如发现历史提交 <code class="language-plaintext highlighter-rouge">54321</code> 中包含错误,直接在当前工作区中针对这个错误进行修改,然后执行下面命令。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git commit --fixup 54321
</code></pre></div></div>
<p>你会发现使用了 <code class="language-plaintext highlighter-rouge">--fixup</code> 参数的提交命令,不再询问你提交说明怎么写,而是直接把错误提交 <code class="language-plaintext highlighter-rouge">54321</code>
的提交说明的第一行拿来,在前面增加一个前缀“fixup!”,如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fixup! 原提交说明
</code></pre></div></div>
<p>如果一次没有改对,还可以再接着改,甚至你还可以针对这个修正提交进行 fixup,产生如下格式的提交说明:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fixup! fixup! 原提交说明
</code></pre></div></div>
<p>当开发工作完成后,待推送/评审的提交中出现大量的包含“fixup!”前缀的提交该如何处理呢?</p>
<p>如果你执行过一次下面的命令,即针对错误提交 54321 及其后面所有提交执行交互式变基(注意其中的 <code class="language-plaintext highlighter-rouge">--autosquash</code> 参数),你就会惊叹 Git 设计的是这么巧妙:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git rebase -i --autosquash 54321^
</code></pre></div></div>
<p>交互式变基弹出的编辑器内自动对提交进行排序,将提交 54321 连同它的所有修正提交压缩为一个提交。</p>
<p>对于“提交做对”,很多开源项目还通过单元测试用例提供保障。对于这样的项目,在提交代码时往往要求提供相应的测试用例。
Git 项目本身就对测试用例有着很高的要求,其测试框架也非常有意思。我曾经针对Git的单元测试框架写过博客,参见:
<a href="http://www.worldhello.net/2013/10/26/test-gistore-using-git-test-framework.html">复用 git.git 测试框架</a>。</p>
<h2 id="提交做好">提交做好</h2>
<p>仅仅做到提交做小、提交做对,往往还不够,还要通过撰写详细的提交说明让评审者信服,这样才能够让提交尽快通过评审合入项目仓库中。</p>
<p>例如今年7月份在华为公司内部的 Git 服务器上发现一个异常,最终将问题定位到 Git 工具本身。整个代码改动只有区区一行:</p>
<ul>
<li>提交:<a href="https://github.com/git/git/commit/b112b14d7869bf3c000abb84cd22e57dd811d031">receive-pack: crash when checking with non-exist HEAD</a></li>
</ul>
<p>你能猜到提交说明写了多少么?写了20多行!</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>receive-pack: crash when checking with non-exist HEAD
If HEAD of a repository points to a conflict reference, such as:
* There exist a reference named 'refs/heads/jx/feature1', but HEAD
points to 'refs/heads/jx', or
* There exist a reference named 'refs/heads/feature', but HEAD points
to 'refs/heads/feature/bad'.
When we push to delete a reference for this repo, such as:
git push /path/to/bad-head-repo.git :some/good/reference
The git-receive-pack process will crash.
This is because if HEAD points to a conflict reference, the function
`resolve_refdup("HEAD", ...)` does not return a valid reference name,
but a null buffer. Later matching the delete reference against the null
buffer will cause git-receive-pack crash.
Signed-off-by: Jiang Xin <worldhello.net@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
</code></pre></div></div>
<p>Git 对于提交说明的格式有着如下约定俗成的规定:</p>
<ul>
<li>
<p>提交主题</p>
<p>提交说明第一行是提交主题,是整个提交的概要性描述。可以在提交主题中添加所更改的模块名称作为前缀(如:receive-pack:)。
提交主题(即提交说明的第一行)尽量保持在50字节以内(Gerrit 的commit_log检查插件似乎有着稍微宽泛一些的要求)。
这是因为对于像 Linux、Git 这样的开源项目,是以邮件列表作为代码评审的平台,提交主题要作为邮件的标题,而邮件标题本身有长度上的限制。</p>
</li>
<li>
<p>提交主题后的空行</p>
<p>必须要在提交说明的第一行和后续的提交说明中间留一个空行!如果没有这个空行,很多 Git 客户端会将连续几行的提交说明合在一起作为提交描述。这样显然太糟了。</p>
</li>
<li>
<p>提交说明主体</p>
<p>提交主题之外的提交说明也有长度的限制,最好以72字节为限,超过则断行。因为 GitHub 在显示提交说明时支持 Markdown 语法,
所以作为一个有品位的程序员学些 Markdown 的语法,让你的提交说明的可读性变得更强吧。</p>
<p>我总结过一个 Markdown 和其他文本标记语言的语法说明,可供参考:</p>
<ul>
<li><a href="http://www.worldhello.net/gotgithub/appendix/markups.html">轻量级标记语言语法参考</a></li>
</ul>
</li>
<li>
<p>签名区</p>
<p>在提交说明最后是签名区。签名区可以看出这个提交的参与者、评审记录等等。</p>
</li>
</ul>
<p>最后,让我们一起学习成为一名有品位的程序员吧。并依靠你对代码的品味,高质量严要求,守护你的项目吧。</p>
使用 git-svn 和 git-filter-branch 整理 SVN 版本库
2014-04-24T00:00:00+00:00
http://www.worldhello.net/2014/04/24/svn-migrate-with-git-svn
<p>SVN 本身提供了如下版本库整理工具:</p>
<ul>
<li>svnadmin dump</li>
<li>svndumpfilter include</li>
<li>svndumpfilter exclude</li>
<li>svnadmin load</li>
</ul>
<p>其中 <code class="language-plaintext highlighter-rouge">svnadmin dump</code> 将整个版本库或部分提交导出为一个导出文件; <code class="language-plaintext highlighter-rouge">svndumpfilter</code>
基于配置项的路径(SVN 1.7的 svndumpfilter 还支持通配符路径)对导出文件进行过滤,
过滤结果保存为新的导出文件; <code class="language-plaintext highlighter-rouge">svnadmin load</code> 将导出文件导入到另外的版本库中,
导入过程有两个选择——维持路径不变,或导入到某个路径之下。</p>
<p>相对于Git提供的用于整理提交的 <code class="language-plaintext highlighter-rouge">git filter-branch</code> 命令,SVN的版本库整理工具能做的实在不多。
而且SVN的相关工具容错性太差,操作过程经常被中断,可谓步步惊心。</p>
<p>最近遇到的一个案例,需要将两个 SVN 版本库(bar 和 baz)的全部历史导入到另外一个 SVN 版本库(foo)中。
并要求版本库 bar 和 baz 的目录结构统一采用 foo 中规定的目录结构。面对要导入的近 20GB 数据(绝大部分是Word、Excel、PDF文档),
决定采用Git提供的工具集进行SVN版本库整理。整理过程和过程中开发的脚本记录如下。</p>
<h2 id="将-bar-和-baz-版本库转换为本地git库">将 bar 和 baz 版本库转换为本地Git库</h2>
<p>以 bar 为例,将两个版本库(bar 和 baz)转换为本地的 Git 版本库,以便使用强大的
<code class="language-plaintext highlighter-rouge">git filter-branch</code> 命令对提交逐一进行修改(如修改版本库中的文件路径)。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git init git/bar
$ cd git/bar
$ git svn init --no-metadata file:///path/to/svn/bar
$ git svn fetch
</code></pre></div></div>
<p>说明:</p>
<ul>
<li>SVN 版本库 bar 位于本机的路径 /path/to/svn/bar 下。</li>
<li>导出的 Git 版本库位于 git/bar 目录下。</li>
<li>因为版本库 bar 并未使用分支(未采用 trunk、branches、tags目录结构),因此执行 <code class="language-plaintext highlighter-rouge">git svn</code> 时并未使用 <code class="language-plaintext highlighter-rouge">-s</code> 等参数。</li>
</ul>
<h2 id="源版本库中文件名过长的问题">源版本库中文件名过长的问题</h2>
<p>Windows和Linux下文件名长度限制不同,前者255个Unicode字符,后者为255个字节。
在此次转换中就遇到 bar 版本库中存在若干文件名超长的文件,导致无法在 Linux 平台上检出。
为避免后续操作中出现错误,对其进行重命名。</p>
<p>首先创建一个脚本 <code class="language-plaintext highlighter-rouge">rename.sh</code>,该脚本将提供给 <code class="language-plaintext highlighter-rouge">git filter-branch</code> 命令对版本库中超长文件名进行重命名操作。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/sh
git ls-files -s | \
sed \
-e "s#\(\t.*/file-name-is-too-long\).*\.pdf#\1-blahblah.pdf#" \
| GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && \
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
</code></pre></div></div>
<p>然后执行下面命令对版本库整理:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd git/bar
$ git filter-branch --index-filter 'sh /path/to/rename.sh'
</code></pre></div></div>
<h2 id="删除空白提交">删除空白提交</h2>
<p>从SVN转换的Git版本库可能存在空白提交,例如一些仅修改了SVN属性的提交不被 <code class="language-plaintext highlighter-rouge">git-svn</code> 支持,转换成了空提交。
这些空提交会对后续操作造成干扰,执行如下命令删除空白提交:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd git/bar
$ git filter-branch -f --commit-filter '
if [ "$(git rev-parse $GIT_COMMIT^^{tree} 2>/dev/null)" = "$(git rev-parse $GIT_COMMIT^{tree})" ];
then
skip_commit "$@";
else
git commit-tree "$@";
fi' HEAD
</code></pre></div></div>
<h2 id="向git日志中添加metadata">向Git日志中添加MetaData</h2>
<p>执行 <code class="language-plaintext highlighter-rouge">git log</code> 操作可以看到转换后的提交保持了原有SVN提交的用户名和提交时间,还记录了对应SVN的提交编号信息。
但是后续操作(<code class="language-plaintext highlighter-rouge">git svn dcommit</code>)会改变Git提交,破坏其中包含的原有SVN提交的提交者和提交时间,
因此需要用其他方法将这些信息记录下来,以便补救。</p>
<p>使用 <code class="language-plaintext highlighter-rouge">git filter-branch</code> 的 <code class="language-plaintext highlighter-rouge">--msg-filter</code> 过滤器逐一向提交插入原有SVN的提交者和提交时间的元信息。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd git/bar
$ git filter-branch -f --msg-filter '
cat &&
echo "From: REPO-NAME, author: $GIT_AUTHOR_NAME, date: $GIT_AUTHOR_DATE"' HEAD
</code></pre></div></div>
<h2 id="根据需要对版本库目录重新组织">根据需要对版本库目录重新组织</h2>
<p><code class="language-plaintext highlighter-rouge">git filter-branch</code> 至少有两个过滤器可以对提交中的目录和文件进行组织。一个是 <code class="language-plaintext highlighter-rouge">--tree-filter</code> ,
一个是 <code class="language-plaintext highlighter-rouge">--index-filter</code> 。前者的过滤器脚本写起来简单,但执行起来较后者慢至少一个数量级。</p>
<p>根据路径转换的需求,编写过滤器脚本,如脚本 <code class="language-plaintext highlighter-rouge">transform.sh</code> :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/sh
if test -z "$GIT_INDEX_FILE"; then
GIT_INDEX_FILE=.git/index
fi
git ls-files -s | \
sed \
-e "s#\(\t\)#\1new-root/#" \
-e "s#\(\tnew-root\)\(/old-path-1/\)#\1/new-path-1/#" \
-e "s#\(\tnew-root\)\(/old-path-2/\)#\1/new-path-2/#" \
-e "s#\(\tnew-root\)\(/old-path-3/\)#\1/new-path-3/#" \
| GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && \
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
</code></pre></div></div>
<p>然后执行如下命令对提交进行逐一过滤,将老的目录结构转换为新的目录结构:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd git/bar
$ git filter-branch --index-filter 'sh /path/to/transform.sh'
</code></pre></div></div>
<h2 id="用git-svn克隆目标版本库foo">用git-svn克隆目标版本库(foo)</h2>
<p>执行如下命令将导入的目标版本库转换为本地的 Git 版本库,如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git init git/foo
$ cd git/foo
$ git svn init --no-metadata file:///path/to/svn/foo
$ git svn fetch
</code></pre></div></div>
<p>然后将 bar 整理好的分支变基到当前的 master 分支上:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd git/foo
$ git fetch ../../git/bar
$ git branch bar/master FETCH_HEAD
$ git co bar/master
$ git rebase -k --onto master --root
</code></pre></div></div>
<p>说明:</p>
<ul>
<li>使用 -k 参数,执行效率更高,因为会直接调用 cherry-pick 进行变基,而不需要执行 <code class="language-plaintext highlighter-rouge">git format-patch</code> 命令将提交预先转换为补丁文件。</li>
</ul>
<p>在执行过程中遇到冲突中断的情况,这时需要解决冲突后执行:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git cherry-pick --continue
</code></pre></div></div>
<p>然后执行如下命令将不在SVN版本库中的Git提交提交到SVN版本库 foo 中。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git svn dcommit --rmdir
</code></pre></div></div>
<p>说明:</p>
<ul>
<li>使用 <code class="language-plaintext highlighter-rouge">--rmdir</code> 命令是为了避免在 SVN 版本库中残留由于目录移动产生的空目录。</li>
<li>使用 <code class="language-plaintext highlighter-rouge">git svn dcommit</code> 在SVN版本库中创建的新提交,其提交者是当前登录用户,提交时间是当前时间。
即新的SVN提交丢失了原有SVN提交的用户名和时间信息。马上利用之前在提交说明中添加的元信息进行补救。</li>
</ul>
<h2 id="修正提交时间和提交者">修正提交时间和提交者</h2>
<p>编写如下脚本 <code class="language-plaintext highlighter-rouge">parse-git-log.rb</code>,读取Git日志对元信息进行处理。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/usr/bin/ruby
require 'date'
def to_iso8601(date)
if date =~ /^[0-9]{10}/
DateTime.strptime(date, '%s').iso8601.gsub(/\+[0-9]*:[0-9]*$/, '.000000Z')
else
raise "Error: wrong date format: #{date}"
end
end
def parse_git_log(io)
svndict={}
commit, author, date, log, rev = []
io.each_line do |line|
line.strip!
if line =~ /^commit ([0-9a-f]{40})/
commit = $1
author, date, log, rev = []
elsif line =~ /^From: .*, author: (.*), date: @([0-9]+)/
author = $1
date = $2
elsif line =~ /git-svn-id: .+@([0-9]+) .*/
rev = $1
if author.nil? or author.empty?
STDERR.puts "Warning: no author for commit: #{commit}"
next
elsif date.nil? or date.empty?
STDERR.puts "Warning: no author for commit: #{commit}"
next
end
svndict[rev] = {}
svndict[rev][:author] = author
svndict[rev][:date] = to_iso8601 date
end
end
svndict
end
url = 'file:///path/to/svn/foo'
svndict = {}
if ARGV.size == 1
if File.exist? ARGV[0]
File.open(ARGV[0]) do |io|
svndict = parse_git_log io
end
else
STDERR.puts "Read git log from STDIN"
url = ARGV[0]
svndict = parse_git_log STDIN
end
else
puts <<-EOF
Usage:
#{File.basename $0} git-log.txt
#{File.basename $0} url-of-svn < git-log.txt
EOF
exit 0
end
svndict.keys.map{|x| x.to_i}.sort.reverse.each do |rev|
author = svndict[rev.to_s][:author]
date = svndict[rev.to_s][:date]
puts "svn ps --revprop -r #{rev} svn:date \"#{date}\" #{url}"
puts "svn ps --revprop -r #{rev} svn:author \"#{author}\" #{url}"
end
</code></pre></div></div>
<p>然后执行如下命令,读取Git日志,将Git提交中的元信息转换为修正 SVN 提交历史的命令脚本 <code class="language-plaintext highlighter-rouge">fix-svn-log.sh</code>。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd git/foo
$ git log | ruby parse-git-log.rb file:///path/to/svn/foo > fix-svn-log.sh
</code></pre></div></div>
<p>然后执行如下命令修改 SVN 的属性,还原原有SVN的提交用户和提交实现信息:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sh fix-svn-log.sh
</code></pre></div></div>
<p>因为此操作实际上执行 <code class="language-plaintext highlighter-rouge">svn ps --revprop</code> 命令,需要SVN版本库 foo 中创建一个可执行的 <code class="language-plaintext highlighter-rouge">pre-revprop-change</code> 钩子脚本。</p>
<p>至此版本库转换完毕。怎么样 <code class="language-plaintext highlighter-rouge">git filter-branch</code> 命令够强大吧。</p>
复用 git.git 测试框架
2013-10-26T00:00:00+00:00
http://www.worldhello.net/2013/10/26/test-gistore-using-git-test-framework
<p>Git 项目(git.git)有着别具一格的测试框架,使用 shell 脚本开发测试用例,
写起测试用例来一点都感觉不到拖泥带水,就和在 shell 环境中手工测试一样。
最近在重构 Gistore 项目时复用了这一 Git 项目特有的测试框架,对 Gistore
进行测试。愿这一测试框架可以被更多的项目借鉴。</p>
<h2 id="gitgit-的测试框架">git.git 的测试框架</h2>
<p>Git 项目主要采用了 C 语言,同时还包含了 Perl、Shell 等多种开发语言的项目。
Git 项目的测试并没有采用常见的类似 JUnit 测试框架,而是采用自创的测试框架,
由 Junio Hamano 在 2005 年用 shell 脚本封装而成。在这个框架下,
写测试用例和测试套件自然也是使用 shell 脚本语言,写起测试用例来就和手工在
shell 环境下针对命令行测试没什么两样,写测试用例的过程很是“享受”。还一个原因可能是
shell 脚本语言几乎融入了每一个 *nix 开发者的血液中。总之这个测试框架用起来非常顺手。</p>
<p>在 Git 项目的 <code class="language-plaintext highlighter-rouge">t/</code> 目录下存在成百上千个以 “<code class="language-plaintext highlighter-rouge">t<四位数字>-<测试套件名称>.sh</code>”
格式命名的文件。每一个 Shell 脚本文件即是一个测试套件,其中包含多个测试用例。</p>
<p>若打开这些 shell 脚本,会注意到每一个测试套件(<code class="language-plaintext highlighter-rouge">t<四位数字>-<套件名>.sh</code>)都包含相似的结构。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 引入测试套件函数库
. ./test-lib.sh
# 定义和执行一个测试用例
test_expect_success '<测试用例名称>' '
<测试断言1> &&
<测试断言2> &&
...
<测试断言n>
'
# 此处省略更多的测试用例
test_expect_success ...
...
# 声明测试套件结束,并对测试执行过程为测试套件生成的临时目录进行清理
test_done
</code></pre></div></div>
<p>这些 shell 脚本(测试套件)都可以单独运行。例如下面示例中执行的测试套件就是我为
git-clean–interactive (交互式 git clean)写的测试套件。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sh t7301-clean-interactive.sh
ok 1 - setup
ok 2 - git clean -i (c: clean hotkey)
ok 3 - git clean -i (cl: clean prefix)
ok 4 - git clean -i (quit)
ok 5 - git clean -i (Ctrl+D)
ok 6 - git clean -id (filter all)
ok 7 - git clean -id (filter patterns)
ok 8 - git clean -id (filter patterns 2)
ok 9 - git clean -id (select - all)
ok 10 - git clean -id (select - none)
ok 11 - git clean -id (select - number)
ok 12 - git clean -id (select - number 2)
ok 13 - git clean -id (select - number 3)
ok 14 - git clean -id (select - filenames)
ok 15 - git clean -id (select - range)
ok 16 - git clean -id (select - range 2)
ok 17 - git clean -id (inverse select)
ok 18 - git clean -id (ask)
ok 19 - git clean -id (ask - Ctrl+D)
ok 20 - git clean -id with prefix and path (filter)
ok 21 - git clean -id with prefix and path (select by name)
ok 22 - git clean -id with prefix and path (ask)
# passed all 22 test(s)
1..22
</code></pre></div></div>
<p>运行测试套件的输出结果(显示到标准输出的内容)是经过特别设计的。成功运行的测试用例显示为:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ok <数字> - <测试用例名>
</code></pre></div></div>
<p>而运行失败的测试用例会显示为:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>not ok <数字> - <测试用例名>
</code></pre></div></div>
<p>在测试套件运行的结尾会显示如下统计信息:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><数字>..<数字>
</code></pre></div></div>
<p>这种特定的输出格式被称为 TAP (Test Anything Protocol),参见 <a href="http://testanything.org/">http://testanything.org/</a> 。</p>
<p>Junio 还用 shell 脚本封装了一个测试夹具(test harness),在 <code class="language-plaintext highlighter-rouge">t/</code> 目录下,直接执行 <code class="language-plaintext highlighter-rouge">make</code>
命令即可执行全部的测试套件,并对测试结果进行统计。此外还有其他的测试夹具可供使用,
例如名为 <code class="language-plaintext highlighter-rouge">prove</code> 的命令可以多进程并发地执行测试套件,让测试过程更高效。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ prove --timer --jobs 15 ./t[0-9]*.sh
[19:17:33] ./t0005-signals.sh ................................... ok 36 ms
[19:17:33] ./t0022-crlf-rename.sh ............................... ok 69 ms
[19:17:33] ./t0024-crlf-archive.sh .............................. ok 154 ms
[19:17:33] ./t0004-unwritable.sh ................................ ok 289 ms
[19:17:33] ./t0002-gitfile.sh ................................... ok 480 ms
===( 102;0 25/? 6/? 5/? 16/? 1/? 4/? 2/? 1/? 3/? 1... )===
</code></pre></div></div>
<h2 id="测试-gistore">测试 Gistore</h2>
<p><a href="https://github.com/jiangxin/gistore/">Gistore</a> 是我在2010年写的一个工具,
以 Git 作为后端存储实现对磁盘文件的备份,并作为独立的一章写到了《Git权威指南》
一书中。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Gistore = Git + Store
</code></pre></div></div>
<p>最近用 Ruby 语言重写了 Gistore。这是因为 Gistore 最初的设计依赖 mount 命令,
需要将备份目录挂载到临时工作区,故只能用于有限的平台上,且可能需要 root 用户权限。
考虑到 Git 的 gitignore 语法增加了对双星号(**)通配符的支持,是不是用
gitignore 机制实现 Gistore 更好呢?改用 Ruby 实现是因为最近几年 Ruby 用得多,
而且使用 Thor (一个实现命令行编程框架的 Ruby 包,被很多著名软件如 bundle、rails
等使用)可以更容易实现工具的命令行扩展。</p>
<p>软件重构的质量需要测试用例来保证。Ruby 虽然内置了强大的测试框架,但像 Gistore
这类大量调用外部命令的应用,采用 Git 项目的测试框架可能更理想。于是在我 Gistore
项目中重用了 Git 项目的测试框架。</p>
<p>使用该测试框架的注意事项如下:</p>
<h3 id="用--组合多个测试断言">用 && 组合多个测试断言</h3>
<p>下面的测试用例中,因为在第二句断言(false)后面丢掉了一个 && ,
导致前两个断言未对测试用例施加影响。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/sh
#
. ./test-lib.sh
test_expect_success 'test framework assertion' '
true &&
false
true
'
test_done
</code></pre></div></div>
<h3 id="用-test_cmp-断言测试输出">用 test_cmp 断言测试输出</h3>
<p>该测试框架中最常用到的断言除了 shell 本身包含的 <code class="language-plaintext highlighter-rouge">test</code> 命令外,就是 <code class="language-plaintext highlighter-rouge">test_cmp</code> 断言。
实际上 <code class="language-plaintext highlighter-rouge">test_cmp</code> 就是对 <code class="language-plaintext highlighter-rouge">diff</code> 命令的简单封装。具体的使用过程是先将预期结果写入文件
<code class="language-plaintext highlighter-rouge">expect</code> ,测试输出写入 <code class="language-plaintext highlighter-rouge">actual</code> 文件,再用 <code class="language-plaintext highlighter-rouge">test_cmp</code> 比较 <code class="language-plaintext highlighter-rouge">expect</code> 和 <code class="language-plaintext highlighter-rouge">actual</code> 文件,
内容一致则成功,否则失败。例如下面的测试用例代码:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat >expect << EOF
root/doc/COPYRIGHT
root/src/README.txt
root/src/images/test-binary-1.png
root/src/images/test-binary-2.png
root/src/lib/a/foo.c
root/src/lib/b/bar.o
root/src/lib/b/baz.a
EOF
test_expect_success 'initialize for commit' '
prepare_work_tree &&
gistore init --repo repo.git &&
gistore add --repo repo.git root/src &&
gistore add --repo repo.git root/doc &&
gistore commit --repo repo.git &&
test "$(count_git_commits repo.git)" = "1" &&
gistore repo repo.git ls-tree --name-only \
-r HEAD | sed -e "s#^${cwd#/}/##g" > actual &&
test_cmp expect actual
'
</code></pre></div></div>
<h3 id="用-test_must_fail-断言命令失败或异常">用 test_must_fail 断言命令失败或异常</h3>
<p>该测试框架中有两个看起来很像的方法 <code class="language-plaintext highlighter-rouge">test_expect_failure</code> 和 <code class="language-plaintext highlighter-rouge">test_must_fail</code>,
前一个函数类似于 <code class="language-plaintext highlighter-rouge">test_expect_success</code>,以命令参数的方式引入一个测试用例并进行测试。
后一个是用于测试用例中的测试断言。</p>
<p>函数 <code class="language-plaintext highlighter-rouge">test_expect_failure</code> 通过命令行参数引入的测试用例,无论执行成功与否,
测试都不会中断。测试用例执行失败会显示:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>not ok 1 - test framework assertion # TODO known breakage
# still have 1 known breakage(s)
</code></pre></div></div>
<p>测试成功会显示:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ok 1 - test framework assertion # TODO known breakage vanished
# 1 known breakage(s) vanished; please update test(s)
</code></pre></div></div>
<p>函数 <code class="language-plaintext highlighter-rouge">test_must_fail</code> 作为测试断言,用于确认一个命令会以失败结束(返回非0值)。
例如下面测试用例用于测试对所有注册的备份任务执行备份时(即执行 <code class="language-plaintext highlighter-rouge">gistore commit-all</code>
命令时),如果有一个或多个 Gistore 备份任务的指向丢失时,其它备份任务的备份不会受到影响,
并且 <code class="language-plaintext highlighter-rouge">gistore commit-all</code> 命令运行结束后要返回非零值。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>test_expect_success 'commit-all while missing task repo' '
gistore task add hello repo1.git &&
gistore task add world repo2.git &&
test "$(count_git_commits repo1.git)" = "4" &&
test "$(count_git_commits repo2.git)" = "3" &&
do_hack &&
gistore commit-all &&
test "$(count_git_commits repo1.git)" = "5" &&
test "$(count_git_commits repo2.git)" = "4" &&
mv repo1.git repo1.git.moved &&
do_hack &&
test_must_fail gistore commit-all &&
test "$(count_git_commits repo2.git)" = "5" &&
mv repo1.git.moved repo1.git &&
mv repo2.git repo2.git.moved &&
test_must_fail gistore commit-all &&
test "$(count_git_commits repo1.git)" = "6"
'
</code></pre></div></div>
<h3 id="测试用例设置依赖条件按需运行">测试用例设置依赖条件按需运行</h3>
<p>因为 Git 1.8.2 之后才为 gitignore 引入双星号(**)通配符,而之前版本的 Git 并不支持,
这会导致某些测试用例结果不一致。</p>
<p>Git项目的测试框架在设计之初就考虑到了这种情况,可以通过设置依赖条件在某些情况下关闭特定测试用例的运行。</p>
<p>首先在测试框架中根据执行环境的不同,预置特定的依赖条件,例如下面的代码使得当 Git 命令的版本是
1.8.2 或更新的版本时,预置 <code class="language-plaintext highlighter-rouge">GIT_CAP_WILDMATCH</code> 依赖条件。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if test $(gistore check-git-version 1.8.2) -ge 0; then
test_set_prereq GIT_CAP_WILDMATCH
fi
</code></pre></div></div>
<p>然后在定义测试用例的 <code class="language-plaintext highlighter-rouge">test_expect_success</code> 的第一个参数中写入相应的依赖条件。
例如如下的测试用例只在 Git 1.8.2 以上环境下运行。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Before git v1.7.4, filenames in git-status are NOT quoted.
# So strip double quote before compare with this.
cat >expect << EOF
M root/doc/COPYRIGHT
M root/src/README.txt
D root/src/images/test-binary-1.png
D root/src/lib/b/baz.a
?? root/src/lib/a/foo.h
EOF
test_expect_success GIT_CAP_WILDMATCH 'status --git (1)' '
gistore commit --repo repo.git && \
echo "hack" >> root/doc/COPYRIGHT && \
echo "hack" >> root/src/README.txt && \
touch root/src/lib/a/foo.h && \
rm root/src/images/test-binary-1.png && \
rm root/src/lib/b/baz.a && \
gistore status --repo repo.git --git -s \
| sed -e "s#${cwd#/}/##g" | sed -e "s/\"//g" > actual &&
test_cmp expect actual
'
</code></pre></div></div>
<h3 id="进行函数级测试">进行函数级测试</h3>
<p>Git项目的测试框架主要是进行集成测试,如果需要进行函数级测试,还需要下点功夫。
即需要对函数进行简单的命令行封装,用命令行调用的方式对函数进行测试。</p>
<p>在 Git 项目中就用代码 “<code class="language-plaintext highlighter-rouge">test-path-utils.c</code>” 对路径处理相关函数进行封装,在测试用例 <code class="language-plaintext highlighter-rouge">t0060</code>
中调用 <code class="language-plaintext highlighter-rouge">test-path-utils</code> 进行相关测试。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>. ./test-lib.sh
relative_path() {
expected=$(test-path-utils print_path "$3")
test_expect_success $4 "relative path: $1 $2 => $3" \
"test \"\$(test-path-utils relative_path '$1' '$2')\" = '$expected'"
}
relative_path /foo/a/b/c/ /foo/a/b/ c/
relative_path /foo/a/b/c/ /foo/a/b c/
relative_path /foo/a//b//c/ ///foo/a/b// c/ POSIX
relative_path /foo/a/b /foo/a/b ./
relative_path /foo/a/b/ /foo/a/b ./
relative_path /foo/a /foo/a/b ../
relative_path / /foo/a/b/ ../../../
</code></pre></div></div>
<p>在 Gistore 项目中,我也用到了类似的方法。通过隐含子命令 <code class="language-plaintext highlighter-rouge">check-git-version</code>
对 <code class="language-plaintext highlighter-rouge">Gistore.git_version_compare</code> 方法进行封装,并在测试用例 <code class="language-plaintext highlighter-rouge">t0020</code> 中进行针对性测试。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>test_expect_success 'compare two versions' '
test $(gistore check-git-version 1.8.5 1.8.5) -eq 0 &&
test $(gistore check-git-version 1.8.4 1.8.4.1) -eq -1 &&
test $(gistore check-git-version 1.7.5 1.7.11) -eq -1 &&
test $(gistore check-git-version 1.7.11 1.7.5) -eq 1 &&
test $(gistore check-git-version 1.7.11 1.7.5) -eq 1 &&
test $(gistore check-git-version 1.7.11 2.0) -eq -1 &&
test $(gistore check-git-version 2.0 1.8.5) -eq 1
'
</code></pre></div></div>
<p>更多测试用例的写法,参见如下链接:</p>
<ul>
<li><a href="https://github.com/git/git/blob/master/t/README">Git 项目中的测试用例的说明文件</a></li>
<li><a href="https://github.com/git/git/tree/master/t">Git 项目中的测试用例</a></li>
<li><a href="https://github.com/jiangxin/gistore/tree/master/t">Gistore 项目中的测试用例</a></li>
</ul>
<p>插播小广告:</p>
<ul>
<li><a href="http://www.worldhello.net/gotgit/bookstore.html">学习Git,读《Git权威指南》</a></li>
<li><a href="https://github.com/jiangxin/gistore">Gistore: 以Git为后端的备份解决方案</a></li>
</ul>
墙不住的Git官网
2013-03-04T00:00:00+00:00
http://www.worldhello.net/2013/03/04/build-git-scm-com
<p>微博中关于《Git Community Book》(Git社区书)本地化的帖子,使我想起久未造访的 <a href="http://git-scm.com/">Git官网</a> ,
却吃惊地发现Git官网已遭G\F\W认证。值此“两会”召开、古月三昷卸任之季,G\F\W居然再次做出如此这般非和谐之举,
不啻于继股市暴跌后又一记对“十年和谐”响亮的耳光。</p>
<p>《墙不住的Git官网》这篇文章送给那些仍在攒钱买房,尚无闲钱购置境外主机以搭建VPN、SSH,也不会使用 goagent 的码农。</p>
<p><a href="http://git-scm.com/">Git官网</a> 的维护者是 GitHub 的 Scott Chacon,他也是
<a href="https://github.com/schacon/gitbook">Git社区书</a> 的主要维护者和 <a href="https://github.com/progit/progit/">《ProGit》</a> 的作者。</p>
<p>新设计的Git官网不但重新设计了Git的Logo,还将《ProGit》这本书贡献到官网,取代Git社区书成为官网上的官方教程。
(毕竟一人维护两套书负担太重,而且不小心会被人误解为 Copy & Paste。)</p>
<p>Git官网的源代码托管在GitHub,已由 <a href="https://github.com/schacon/git-scm">旧版网站地址</a> 更换到新的地址:</p>
<ul>
<li><a href="https://github.com/github/gitscm-next/">https://github.com/github/gitscm-next/</a></li>
</ul>
<p>新网站基于 Rails 构建,默认使用 sqlite 本地数据库。其中 HTML 格式的 Git 手册、ProGit电子书的源代码并不在此版本库中,
而是要执行相应的 rake 任务,从Git版本库和 ProGit版本库中获取内容、编译并保存到 sqlite 数据库中。
下面介绍一下如何在本地搭建Git官网。</p>
<h2 id="准备">准备</h2>
<ul>
<li>
<p>克隆Git版本库(可选)</p>
<p>Git官网中的Git手册直接从Git项目的本地版本库中编译,会对 Git v1.0 之后的每一正式发布版本的手册进行编译。</p>
<p>如果本地已经克隆了Git版本库,可以跳过这一步。</p>
<p>如果没有克隆Git版本库,先克隆一份Git版本库。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ cd Your/WorkSpace/
$ git clone git://github.com/git/git
</code></pre></div> </div>
</li>
<li>
<p>克隆Git官网版本库</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ cd Your/WorkSpace/
$ git clone git://github.com/github/gitscm-next
</code></pre></div> </div>
</li>
<li>
<p>进入到克隆出的Git官网版本库</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ cd gitscm-next
</code></pre></div> </div>
</li>
<li>
<p>安装 Ruby 1.9.2 。</p>
<p>(在 <code class="language-plaintext highlighter-rouge">gitscm-next</code> 目录下的 <code class="language-plaintext highlighter-rouge">.ruby-version</code> 这个文件指定了 ruby 的版本。)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ rvm install 1.9.2
$ rvm use 1.9.2
</code></pre></div> </div>
</li>
<li>
<p>下载并安装依赖的 Gem 包。</p>
<p>(由文件 Gemfile 设定 gem 包依赖)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ bundle install
</code></pre></div> </div>
</li>
</ul>
<h2 id="rails-应用配置">Rails 应用配置</h2>
<p>Git新官网是一个 Rails 应用。数据库的默认配置文件 <code class="language-plaintext highlighter-rouge">config/database.yml</code> 已指定使用 sqlite3 本地数据库。
执行如下命令即可创建该本地数据库。</p>
<ul>
<li>
<p>初始化本地数据库。(执行数据迁移操作)。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ rake db:migrate
</code></pre></div> </div>
</li>
<li>
<p>导入缺省数据</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ rake db:fixtures:load
</code></pre></div> </div>
</li>
</ul>
<h2 id="第一次启动应用">第一次启动应用</h2>
<ul>
<li>
<p>启动Web应用</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ script/rails server
...
=> Rails 3.2.11 application starting in development on http://0.0.0.0:3000
...
</code></pre></div> </div>
</li>
<li>
<p>文档页面 404</p>
<p>从上面命令的输出可以看出启动的内置Web服务器运行在 3000 端口。打开 Web 浏览器,输入地址 <a href="http://localhost:3000/">http://localhost:3000/</a> ,
墙外的 Git官网在本地重现了。在网站中四处转转,会发现文档部分的链接( <a href="http://localhost:3000/doc/">doc/</a> )催悲地404了。</p>
<p>这是因为相关文档需要从其他版本库获取数据并编译。</p>
</li>
<li>
<p>退出Web应用</p>
<p>在控制台按下 Ctrl+C 退出运行在 3000 端口的 Web 服务。</p>
</li>
</ul>
<h2 id="编译-git-手册">编译 Git 手册</h2>
<p>阅读版本库根目录下的 <code class="language-plaintext highlighter-rouge">README.md</code> 文件(GitHub上项目的说明文件),可以看到编译文档的说明。</p>
<ul>
<li>
<p>编译Git手册。</p>
<p>如果你有耐心,可以执行下面的命令,将Git v1.0 之后的 240 多个正式发行版本的文档逐一编译(当然很多小版本并未更新文档),并保存到数据库中。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ GIT_REPO=/Your/WorkSpace/git/.git rake local_index
</code></pre></div> </div>
<p>你也可以只编译Git某一个版本的手册。如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ GIT_REPO=/Your/WorkSpace/git/.git REBUILD_DOC=v1.8.1 rake local_index
</code></pre></div> </div>
<p>(关于该 rake 命令的具体实现参见脚本: <code class="language-plaintext highlighter-rouge">lib/tasks/local_index.rake</code> )</p>
</li>
<li>
<p>访问编译的Git手册。</p>
<p>再次启动Web应用,文档页面仍然显示 404 错误。编译的Git手册文档藏到哪里了呢?从 Rails 的路由文件 <code class="language-plaintext highlighter-rouge">config/routes.rb</code> 文件可以猜出Git手册页面的URL地址为:</p>
<ul>
<li><a href="http://localhost:3000/docs/">http://localhost:3000/docs/</a></li>
</ul>
<p>按 Ctrl+C 退出Web应用。</p>
</li>
</ul>
<h2 id="编译-progit-和其他文档">编译 ProGit 和其他文档</h2>
<p>执行下面命令可以编译出其余文档,包括《ProGit》电子书。</p>
<ul>
<li>
<p>更新Git下载链接,执行如下命令:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ rake downloads
</code></pre></div> </div>
</li>
<li>
<p>编译 ProGit 电子书。</p>
<p>提供您的 GitHub 账号——将如下命令中的 your_github_username 和 your_github_password 用您的用户名及口令替换。
执行 rake 命令,通过 GitHub API (调用 octokit 包)远程读取 progit 版本库源码,编译电子书。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ export API_USER=your_github_username
$ export API_PASS=your_github_password
$ rake remote_genbook
</code></pre></div> </div>
</li>
</ul>
<p>至此 Git官网在本地部署完毕,运行内置 Web server:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ script/rails server
...
=> Rails 3.2.11 application starting in development on http://0.0.0.0:3000
...
</code></pre></div></div>
<p>墙外的Git官网在本地重现: <a href="http://localhost:3000/">http://localhost:3000/</a> 。</p>
Git测试问卷完整版
2012-08-14T00:00:00+00:00
http://www.worldhello.net/2012/08/14/git-quiz-released-on-github
<p>在为深圳某客户提供 <a href="http://www.ossxp.com/doc/about-us/ossxp-services.pdf">研发管理平台实施和培训的项目</a> 中,应客户要求提供了两套问卷试题。背负考核任务去学习,团队转换配置管理工具的效果自然会更好吧。</p>
<p>问卷A最早公布于 <a href="http://www.worldhello.net/2012/03/19/git-quiz.html">我的博客</a>,并由 <a href="http://weibo.com/sysadm">@非专业IT民工</a> 给出了答案和详尽的解释:</p>
<ul>
<li><a href="http://loveky2012.blogspot.jp/2012/08/git-knowledge-test-answer.html">http://loveky2012.blogspot.jp/2012/08/git-knowledge-test-answer.html</a> (翻墙梯需自备)</li>
</ul>
<p>既然问卷A已经没有了神秘感,就把问卷B也拿出来。您可以通过</p>
<ul>
<li>
<p>克隆版本库:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git clone git://github.com/gotgit/git-quiz.git
</code></pre></div> </div>
</li>
<li>
<p>或者直接访问网址:</p>
<p><a href="http://www.worldhello.net/git-quiz/">http://www.worldhello.net/git-quiz/</a></p>
</li>
</ul>
<p>看到完整的测试题。</p>
<p>问卷B和问卷A的相似度非常高,就请保留一些神秘感吧,好让问卷B能够承担一些自测功能。<em>公开问卷B的答案是毫无必要和没有技术含量的。</em></p>
<p>BTW,问卷的设计是在北京到深圳的飞机上匆匆完成的,对我来说为错误答案而凑数是最花脑筋的,不严谨之处敬请谅解。</p>
Topgit 本地和远程分支的删除同步以及 git fetch --prune 分析
2012-07-05T00:00:00+00:00
http://www.worldhello.net/2012/07/05/delete-topgit-remote-branch
<p>在 Google Group (需翻墙)上网友提了一个问题:“<a href="https://groups.google.com/forum/?fromgroups#!topic/gotgit/CSHRbi_JAcA">Topgit 本地特性分支删除后,如何清理远程版本库中相应分支?</a>”</p>
<p>这是一个非常好的问题,我也曾遇到,一直是以手工清除远程分支(及 Topgit 跟踪分支)的。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git push origin :t/feature-branch
$ git push origin :top-bases/t/feature-branch
</code></pre></div></div>
<p>好吧,为什么不“懒惰”一点,把清理远程版本库对应分支的工作写在 Topgit 代码中呢?于是写了一个补丁,
见:</p>
<ul>
<li><a href="https://github.com/ossxp-com/topgit/blob/master/debian/patches/t/delete-remote-branch.diff">https://github.com/ossxp-com/topgit/blob/master/debian/patches/t/delete-remote-branch.diff</a></li>
</ul>
<p>同步删除远程版本库的 Topgit 特性分支和跟踪分支只完成了硬币的一面,另外一面是当远程版本库的 Topgit
特性分支和跟踪分支被他人删除后,如何本地获知?</p>
<p>直觉告诉我只要把 <code class="language-plaintext highlighter-rouge">tg-remote.sh</code> 代码中的 <code class="language-plaintext highlighter-rouge">git fetch</code> 命令换做 <code class="language-plaintext highlighter-rouge">git fetch --prune</code> 即可。
可是一试之下,大失所望,竟然将本地版本库所有 <code class="language-plaintext highlighter-rouge">refs/remotes/origin/top-bases/</code> 下的引用全部删除!
难道这是 <code class="language-plaintext highlighter-rouge">git fetch --prune</code> 的 Bug?</p>
<p>重新编译Git(去掉 -O2 增加 -ggdb 编译参数),以便用 <code class="language-plaintext highlighter-rouge">gdb</code> 调试。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gdb --args git fetch --prune
</code></pre></div></div>
<p>最终从 <code class="language-plaintext highlighter-rouge">builtin/fetch.c</code> 的函数 <code class="language-plaintext highlighter-rouge">query_refspecs()</code> 返回值中看出端倪。</p>
<p>首先一个 Topgit 管理下的版本库,配置文件中会有如下的 fetch 配置:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*
url = ...
fetch = +refs/top-bases/*:refs/remotes/origin/top-bases/*
</code></pre></div></div>
<p>其中的两条 <code class="language-plaintext highlighter-rouge">fetch</code> 配置,将远程版本库的 <code class="language-plaintext highlighter-rouge">refs/heads/*</code> 和 <code class="language-plaintext highlighter-rouge">refs/top-bases/*</code> 两个名字空间的引用
都获取到本地 <code class="language-plaintext highlighter-rouge">refs/remotes/origin</code> 名字空间下。</p>
<p>为清理本地陈旧的远程分支,先要根据上面两条 <code class="language-plaintext highlighter-rouge">fetch</code> 配置指令,反查出在远程版本库中的引用名称(见
<code class="language-plaintext highlighter-rouge">remote.c</code> 的 <code class="language-plaintext highlighter-rouge">match_name_with_pattern()</code> 函数)。对于 <code class="language-plaintext highlighter-rouge">refs/remotes/origin/top-bases/t/feature</code> 名字
的引用,在反查时上面两条 <code class="language-plaintext highlighter-rouge">fetch</code> 都能够返回查询结果,分别为:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">refs/heads/top-bases/t/feature</code> 和</li>
<li><code class="language-plaintext highlighter-rouge">refs/top-bases/t/feature</code></li>
</ul>
<p>两个查询结果,前一个错,后一个对。按照上面顺序出现的 <code class="language-plaintext highlighter-rouge">fetch</code> 指令,导致解析出来的引用名称为前一个(错误的),
显然无法在远程版本库的引用中找到,于是认为是过时的,于是将 <code class="language-plaintext highlighter-rouge">refs/remotes/origin/top-bases/t/feature</code> 删除。
一个最简单的解决方案是将上述两条 <code class="language-plaintext highlighter-rouge">fetch</code> 语句颠倒顺序,即可成功实现 <code class="language-plaintext highlighter-rouge">git fetch --prune</code> 。</p>
<p>Topgit 实现获取远程服务器特性分支时自动清理本地远程分支的补丁如下:</p>
<ul>
<li><a href="https://github.com/ossxp-com/topgit/blob/master/debian/patches/t/prune-stale-remote-branch.diff">https://github.com/ossxp-com/topgit/blob/master/debian/patches/t/prune-stale-remote-branch.diff</a></li>
</ul>
Why Git is better than SVN
2012-04-12T00:00:00+00:00
http://www.worldhello.net/2012/04/12/why-git-is-better-than-svn
<p>在版本控制系统的选型上,是选择Git还是SVN?</p>
<p>对于开源项目来说这不算问题。使用Git极大地提高了开发效率、扩大了开源项目的参与度、
增强了版本控制系统的安全性,选择Git早已是大势所趋。</p>
<p>但对于企业用户来说这个决心不太好下。部分原因是出于对Git的误解,部分原因是尚不了解
Git到底能给项目管理带来什么好处。希望本文能对您项目的版本控制系统选型提供帮助。</p>
<h2 id="对svn的迷信和对git的误解">对SVN的迷信和对Git的误解</h2>
<h3 id="误解1svn只能检出checkout一个版本revision的代码而git却可以脱库">误解1:SVN只能检出(checkout)一个版本(revision)的代码,而Git却可以脱库!</h3>
<p>这个误解是如此普遍,简直成了SVN在企业市场中封杀Git的尚方宝剑。其实稍微思考一下
这个谣言就很难传播。既然SVN能够读取授权访问的文件的每一个版本,那么就能够重组这些版本,
进而实现对版本库的完整复制。即SVN也可以脱库。</p>
<p>SVN脱库的工具SVN本身就提供: <code class="language-plaintext highlighter-rouge">svnsync</code> 。这个工具主要用于SVN的版本库镜像。
例如将版本库 <code class="language-plaintext highlighter-rouge">http://host.name/svn/repo</code> 脱库到本地的 <code class="language-plaintext highlighter-rouge">dump</code> 目录,命令如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ svnadmin create dump
$ printf '#!/bin/sh\nexit 0\n' > dump/hooks/pre-revprop-change
$ chmod a+x dump/hooks/pre-revprop-change
$ svnsync init file://$(pwd)/dump http://host.name/svn/repo
$ svnsync sync file://$(pwd)/dump
</code></pre></div></div>
<p>如果使用 <code class="language-plaintext highlighter-rouge">git-svn</code> 则为SVN“脱库”更简便。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git svn clone -s http://host.name/svn/repo dump
</code></pre></div></div>
<p>有人认为SVN可以对目录授权,从而阻止对整个版本库进行脱库操作。
下面就来看看SVN的授权究竟是否可靠。</p>
<p><a name="svnauthz" id="svnauthz"></a></p>
<h3 id="误解2svn能对目录进行精细授权而git太不安全">误解2:SVN能对目录进行精细授权,而Git太不安全</h3>
<p>SVN的目录授权对管理员来说是灾难,管理负担相当重,在分支或里程碑众多的时候很难作对。
这是因为SVN的分支和里程碑(tags)本身就是一个目录(使用目录拷贝实现的)。</p>
<p>例如管理员为名为demo的SVN版本库授权。一个并不太复杂的主线(/trunk)授权如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[demo:/trunk]
@demo-admin = rw
@leaders = r
[demo:/trunk/doc]
@demo-dev = rw
@designers = rw
[demo:/trunk/src/apps]
@demo-dev = rw
[demo:/trunk/src/common]
@demo-dev = rw
[demo:/trunk/src/html]
@designers = rw
[demo:/trunk/src/secret]
* =
@demo-admin = rw
jiangxin = rw
</code></pre></div></div>
<p>如果项目创建了维护分支 <code class="language-plaintext highlighter-rouge">/branches/1.x</code> ,若和 <code class="language-plaintext highlighter-rouge">/trunk</code> 授权相同,则需要将上述授权在
<code class="language-plaintext highlighter-rouge">/branches/1.x</code> 下重建。需要在授权文件中再添加如下授权指令:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[demo:/branches/1.x]
@demo-admin = rw
@leaders = r
[demo:/branches/1.x/doc]
@demo-dev = rw
@designers = rw
[demo:/branches/1.x/src/apps]
@demo-dev = rw
[demo:/branches/1.x/src/common]
@demo-dev = rw
[demo:/branches/1.x/src/html]
@designers = rw
[demo:/branches/1.x/src/secret]
* =
@demo-admin = rw
jiangxin = rw
</code></pre></div></div>
<p>如果版本库的分支和里程碑越来越多,配置的工作量相当可观,稍有不慎不是授权文件格式破坏导致SVN无法工作,
就是造成开放授权。</p>
<p>我曾经写过SVN路径授权的补丁,并写了一款SVN版本库管理的开源软件
(参见 <a href="http://www.ossxp.com/doc/pysvnmanager/user-guide/user-guide.html">《pySvnManager手册》</a> ),
但想完美解决这个问题很难。我的一个设想是在SVN对分支和里程碑授权检查时缺省使用
<code class="language-plaintext highlighter-rouge">/trunk</code> 的授权,但这样的实现要求使用SVN严格遵循约定俗成的三个顶级目录的规范。</p>
<p>Git对于写操作可以精细到目录和分支级别(使用Gitolite作为服务器),
但作为分布式版本库控制系统,在设计上只能实现版本库量子化的读授权。
即某用户对整个版本库要么都能读,要么对整个版本库都不能读。</p>
<p>那么如何控制Git版本库的读授权呢?实际上Git可以通过子模组来实现细粒度的读授权。
即在项目需要精细授权的场合,将版本库拆分为多个Git版本库进行单独授权,
再使用子模组将多个版本库整合为一个。这个操作并不复杂,而且有助于实现项目的模块化。</p>
<h3 id="误解3git能随意改变历史提交这对于版本控制来说是不合适的">误解3:Git能随意改变历史提交,这对于版本控制来说是不合适的</h3>
<p>Git对历史提交的修改只对本地提交有意义。本地提交就像是和共享版本库间的缓冲。
在未将本地提交推送到远程共享版本库之前,开发者可以后悔。可以对不完整的提交说明进行补充,
可以移除错误的提交,可以压缩合并提交等。Git对提交历史灵活的操作是Git独有的功能,
是提交审核的必备工具。</p>
<p>对于已经推送到远程共享服务器的提交,Git就不能再像本地一样随意更改了。
因为推送到共享版本库的提交一旦被其他程序员获取,便扩散出去,
如覆水难收,难掩众人悠悠之口。所以Git更改历史提交只对本地有效,是安全的。</p>
<p>相比之下,SVN本地工作区和集中式版本库之间没有缓冲,一旦发现提交了错误内容,
或写了错误的提交说明,则无法更改,除非SVN管理员介入。
SVN也允许配置为可修改历史提交说明,但是一旦管理员放开此功能,
历史提交的提交说明有可能被批量、恶意更改,并且无法恢复。</p>
<p><a name="encoding" id="encoding"></a></p>
<h3 id="误解4svn对中文支持更好git库中的中文目录和文件名会出现乱码">误解4:SVN对中文支持更好,Git库中的中文目录和文件名会出现乱码</h3>
<p>我也曾经这么认为,并在《Git权威指南》第3章中用了大量篇幅介绍中文支持的注意事项。
并推荐使用Cygwin作为首选客户端,以避免GBK字符集为跨平台开发的版本库引入乱码。</p>
<p>一个好消息是Windows下最常用的Git客户端 msysGit 也支持Unicode了。
使用最新版本(1.7.10)的 msysGit 无需设置任何Git配置变量,
版本库中的中文文件名、目录名、提交说明都使用Unicode编码。
配合使用Unicode版的TortoiseGit(最新的1.7.9.0版本已是Unicode版),
Windows用户就不再为跨平台开发的字符集问题而伤脑筋了。</p>
<h3 id="误解5svn的认证方式比git丰富比如可以实现ldap认证">误解5:SVN的认证方式比Git丰富,比如可以实现LDAP认证</h3>
<p>我为客户配置的Git支持HTTP、SSH协议,和Gitweb。其中HTTP协议、Gitweb都使用LDAP认证,
实现统一的口令管理。并且无论是HTTP协议、SSH协议,还是Gitweb都使用同一套Gitolite授权。</p>
<p><a name="workflow" id="workflow"></a></p>
<h3 id="误解6svn更易上手更易管理而git太难和太灵活了不适合团队">误解6:SVN更易上手,更易管理;而Git太难和太灵活了,不适合团队?</h3>
<p>如果想把配置管理做好,无论是 SVN 还是 Git 都不容易,否则 <a href="http://svnbook.red-bean.com/">《SVN Book》</a>
以及我写 <a href="http://gotgit.github.com/gotgit/">《Git权威指南》</a> 也不会有那么厚了。</p>
<p>觉得SVN更简单的,看看下面的错误你有没有犯?</p>
<ul>
<li>很多公司的SVN版本库没有遵照约定俗成的三个顶级目录。</li>
<li>如何配置SVN悲观锁,以便更好地对二进制文件编辑进行协同。</li>
<li>维护合并追踪的 svn:mergeinfo 属性,以便能够正确的分支合并。还要防止无此功能的客户端对其的破坏。</li>
<li>SVN如何正确的反删除,直接添加删除的文件是不对的。</li>
<li>如何使用 svn:eol-style 属性,以便正确处理跨平台开发时的文件换行符问题。</li>
<li>SVN管理员如何对版本库进行整理,如撤出不当提交、修改错误的提交说明。</li>
<li>版本库的安全性问题,如何做好版本库的备份。</li>
</ul>
<p>SVN对分支当做路径来授权,造成管理的负担(参见 <a href="#svnauthz">前面的描述</a> ),
因此使用SVN实现灵活的特性分支开发、可靠的发布控制(维护分支冻结)很难。</p>
<p>企业应用Git的困惑之一是如何裁剪出适合自己的工作流。实际上Git本身已经给出范例:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git help workflows
</code></pre></div></div>
<p>理解Git的应用模型并选用合适的服务器端软件(如 Gitolite),可以定制出适合自己的工作流。
例如下表就是在企业中使用Git版本控制系统的典型角色划分:</p>
<table>
<thead>
<tr>
<th> </th>
<th style="text-align: center">系统管理员</th>
<th style="text-align: center">配置管理员</th>
<th style="text-align: center">发布工程师</th>
<th style="text-align: center">整合工程师</th>
<th style="text-align: center">模块负责人</th>
<th>开发工程师</th>
</tr>
</thead>
<tbody>
<tr>
<td> </td>
<td style="text-align: center">(SYSadm)</td>
<td style="text-align: center">(SCMadm)</td>
<td style="text-align: center">(RELeng)</td>
<td style="text-align: center">(INTegrator)</td>
<td style="text-align: center">(MODmaster)</td>
<td>(DEV)</td>
</tr>
<tr>
<td>创建版本库</td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>版本库授权</td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>版本库改名</td>
<td style="text-align: center">✔</td>
<td style="text-align: center">?</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>删除版本库</td>
<td style="text-align: center">✔</td>
<td style="text-align: center">?</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>创建Tag</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>删除Tag</td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>创建一级分支</td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>为分支授权</td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>向 maint 分支强推</td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>向 master 分支强推</td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>向 maint 分支写入</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td> </td>
</tr>
<tr>
<td>向 master 分支写入</td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center">✔</td>
<td> </td>
</tr>
<tr>
<td>创建个人专有分支</td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center">✔</td>
<td style="text-align: center">✔</td>
<td style="text-align: center">✔</td>
<td>✔</td>
</tr>
<tr>
<td>创建个人专有版本库</td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center">✔</td>
<td style="text-align: center">✔</td>
<td style="text-align: center">✔</td>
<td>✔</td>
</tr>
<tr>
<td>为个人专有版本库授权</td>
<td style="text-align: center"> </td>
<td style="text-align: center">✔</td>
<td style="text-align: center">✔</td>
<td style="text-align: center">✔</td>
<td style="text-align: center">✔</td>
<td>✔</td>
</tr>
</tbody>
</table>
<p>再来谈谈Git的使用,实际上Git的设计模型非常简单,理解了其设计思想,就可以很容易地掌握 <code class="language-plaintext highlighter-rouge">git reset</code>,
<code class="language-plaintext highlighter-rouge">git checkout</code>, <code class="language-plaintext highlighter-rouge">git rebase</code>, <code class="language-plaintext highlighter-rouge">git push</code>, <code class="language-plaintext highlighter-rouge">git pull</code> 等命令。</p>
<h3 id="误解7程序员不喜欢命令行">误解7:程序员不喜欢命令行</h3>
<p>谁说Git没有好的图形工具?SVN 有 TortoriseSVN,Git 同样有 TortoiseGit。
只不过Git的命令行太好用,使得图形操作显得笨拙。</p>
<p>至于Windows用做开发环境是否还有前途,看看火热的iOS、Android开发、和优雅的 MacBook 就知道了。</p>
<h2 id="git能做到而svn难以做到的事情">Git能做到,而SVN难以做到的事情</h2>
<h3 id="优势1使用git团队规模不受版本库工具自身的限制">优势1:使用Git,团队规模不受版本库工具自身的限制</h3>
<p>最坏的情况下(每次提交都要会修改同一文件,例如版本库中仅包含一个文件),一个SVN版本库的每小时提交次数存在上限。如果无冲突合并再提交需用时30秒、冲突解决再提交用时300秒,这个上限可能是每小时40个提交。据此一个相对密集开发的版本库拥有四五十个提交账号可能就是极限。</p>
<p>Git的提交是在本地完成的,加之可以采用版本库分级控制的分布式开发模型,因此只有天空才是极限。</p>
<h3 id="优势2git分支功能最为强大分支管理能力让svn望尘莫及">优势2:Git分支功能最为强大,分支管理能力让SVN望尘莫及</h3>
<p>Git可以很容易地对比两个分支,知道一个分支中哪些提交尚未合并到另一分支,反之亦然。</p>
<ul>
<li>
<p>查看当前分支比other分支多了哪些提交:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git log other..
</code></pre></div> </div>
</li>
<li>
<p>查看other分支比当前分支多了哪些提交:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git log ..other
</code></pre></div> </div>
</li>
</ul>
<p>我不认为SVN的分支是真正的分支,因为分支最基本的提交隔离SVN就没能实现。
在SVN中一次提交可以同时更改主线(/trunk)和分支中的内容,
所以判断一个分支中哪些提交未合并到另外的分支,完全不能对SVN抱有希望。</p>
<h3 id="优势3git可以实现更好的发布控制">优势3:Git可以实现更好的发布控制</h3>
<p>针对同一个项目,Git可以设置不同层级的版本库(多版本库),
或者通过不同的分支(多分支)实现对发布的控制。</p>
<ul>
<li>设置只有发布管理员才有权限推送的版本库或者分支,用于稳定发布版本的维护。</li>
<li>设置只有项目经理、模块管理员才有权推送的版本库或者分支,用用于整合测试。</li>
</ul>
<h3 id="优势4隔离开发提交审核">优势4:隔离开发,提交审核</h3>
<p>如何对团队中的新成员的开发进行审核呢?在Git服务器上可以实现用户自建分支和自建版本库的功能,
这样团队中的新成员既能将本地提交推送到服务器以对工作进行备份,
又能够方便团队中的其他成员对自己的提交进行审核。</p>
<p>审核新成员提交时,从其个人版本库或个人分支获取(fetch)提交,从提交说明、代码规范、编译测试
等多方面对提交逐一审核。审核通过执行 <code class="language-plaintext highlighter-rouge">git merge</code> 命令合并到开发主线中。</p>
<h3 id="优势5对合并更好的支持更少的冲突更好的冲突解决">优势5:对合并更好的支持,更少的冲突,更好的冲突解决</h3>
<p>因为Git基于对内容的追踪而非对文件名追踪,所以遇到一方或双方对文件名更改时,
Git能够很好进行自动合并或提供工具辅助合并。而SVN遇到同样问题时会产生树冲突,
解决起来很麻烦。</p>
<p>Git的基于DAG(有向非环图)的设计比SVN的线性提交提供更好的合并追踪,
避免不必要的冲突,提高工作效率。这是开发者选择Git、抛弃SVN的重要理由。</p>
<h3 id="优势6保证已修复bug不再重现">优势6:保证已修复Bug不再重现</h3>
<p>以为创建完毕里程碑标签(tag)便完成软件版本的发布是有风险的,
往往会由于之前的版本(维护版本)中的一些 Hotfix
提交没有合并到最新版本而造成已修复问题在新版本中重现。</p>
<p>Git分支和合并追踪可以解决这个问题。例如用 maint 分支跟踪最新的发行版,
当确定里程碑tag v1.6.4 为最新发行版时,在 maint
分支执行如下命令以切换到最新发行版:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git checkout maint
$ git merge --ff-only v1.6.4
</code></pre></div></div>
<p>如果合并成功,代表发行版 v1.6.4 包含了所有前一个发行版的提交。
反之说明前一个发行版某个或某些Hotfix提交尚未合并到最新发行版中。</p>
<h3 id="优势7版本库的安全性">优势7:版本库的安全性</h3>
<p>SVN版本库安全性很差,是管理员头痛的问题。</p>
<ul>
<li>SVN版本库服务器端历史数据被篡改,或者硬盘故障导致历史数据被篡改时,
客户端很难发现。管理员的备份也会被污染。</li>
<li>SVN作为集中式版本控制系统,存在单点故障的风险。备份版本库的任务非常繁重。</li>
</ul>
<p>Git在这方面完胜SVN。首先Git是分布式版本控制系统,每个用户都相当于一份备份,
管理员无需为数据备份而担心。再有Git中包括提交、文件内容等都通过SHA1哈希保证数据的完整性,
任何恶意篡改历史数据都会被及时发现从而被挫败。</p>
<h3 id="更多的十条喜欢git的理由">更多的十条喜欢Git的理由</h3>
<p>更多的十条喜欢Git的理由,参见 <a href="http://www.worldhello.net/gotgit/">《Git权威指南》</a> 第11-21页。</p>
<ul>
<li>异地协同工作。</li>
<li>现场版本控制。</li>
<li>重写提交说明。</li>
<li>无尽的后悔药。</li>
<li>更好用的提交列表。</li>
<li>更好的差异比较。</li>
<li>工作进度保存。</li>
<li>作为SVN前端实现移动办公。</li>
<li>无处不在的分页器。</li>
<li>快。</li>
</ul>
<h2 id="什么情况推荐使用svn">什么情况推荐使用SVN</h2>
<p>SVN具有的悲观锁的功能,能够实现一个用户在编辑时对文件进行锁定,阻止多人同时编辑
一个文件。这一悲观锁的功能是 Git 所不具备的。对于以二进制文件
(Word文档、PPT演示稿) 为主的版本库,为避免多人同时编辑造成合并上的困难,
建议使用SVN做版本控制。</p>
New hack for topgit: git-merge--no-edit
2012-03-29T00:00:00+00:00
http://www.worldhello.net/2012/03/29/topgit-merge-no-edit
<p>Git 1.7.10 对 <code class="language-plaintext highlighter-rouge">git merge</code> 提供了一个改进,而这个改进可能会带来兼容性问题,
会导致某些依赖 <code class="language-plaintext highlighter-rouge">git merge</code> 自动完成的工具受到影响。而 Topgit 就中招了。</p>
<p>Junio 的 <a href="http://git-blame.blogspot.jp/2012/02/anticipating-git-1710.html">一篇博客</a> 专门对这个问题做了描述。简单的说就是Git 1.7.10 起,
执行 <code class="language-plaintext highlighter-rouge">git merge</code> 时,成功的合并不会使用默认的(没有意义的)提交说明自动提交,
而是会打开一个编辑器等待用户输入提交说明。</p>
<p>这个改动会使得调用 <code class="language-plaintext highlighter-rouge">git merge</code> 的工具在执行时被打断。Topgit 的 <code class="language-plaintext highlighter-rouge">tg update</code>
等命令即受此影响。</p>
<p>这个 <a href="https://github.com/ossxp-com/topgit/commit/0c0fa52bf82f4ad22a4927b9ab5c400595ad9602">GitHub上的提交</a> 即用于解决此问题:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>From: Jiang Xin <worldhello.net@gmail.com>
Subject: [PATCH] No stop to edit for the new merge behavior of git
In Git 1.7.10, Merge will stop and wait for a merge commit log. This
backward-incompatible improvement that will break topgit. To fix it,
just export GIT_MERGE_AUTOEDIT=no.
See: http://git-blame.blogspot.jp/2012/02/anticipating-git-1710.html
Signed-off-by: Jiang Xin <worldhello.net@gmail.com>
---
tg.sh | 2 ++
1 个文件被修改,插入 2 行(+)
diff --git a/tg.sh b/tg.sh
index 9082d88..b7661c2 100644
--- a/tg.sh
+++ b/tg.sh
@@ -430,6 +430,8 @@ get_temp()
## Initial setup
set -e
+# suppress the merge log editor feature since git 1.7.10
+export GIT_MERGE_AUTOEDIT=no
git_dir="$(git rev-parse --git-dir)"
root_dir="$(git rev-parse --show-cdup)"; root_dir="${root_dir:-.}"
# Make sure root_dir doesn't end with a trailing slash.
--
tg: (d279e29..) t/git-merge-no-edit (depends on: tgmaster)
</code></pre></div></div>
Git测验(A卷)
2012-03-19T00:00:00+00:00
http://www.worldhello.net/2012/03/19/git-quiz
<p>在深圳做Git培训和项目管理软件实施,这是一份Git培训的测验题,通过率不高。因为不合格的还要重考,答案就不公布了。</p>
<h3 id="单项选择题">单项选择题</h3>
<ol>
<li>
<p>如果提示提交内容为空、不能提交,则最为合适的处理方式是:_____</p>
<p>a) 执行 <code class="language-plaintext highlighter-rouge">git status</code> 查看状态,再执行 <code class="language-plaintext highlighter-rouge">git add</code> 命令选择要提交的文件,然后提交。</p>
<p>b) 执行 <code class="language-plaintext highlighter-rouge">git commit --allow-empty</code> ,允许空提交。</p>
<p>c) 执行 <code class="language-plaintext highlighter-rouge">git commit -a</code> ,提交所有改动。</p>
<p>d) 执行 <code class="language-plaintext highlighter-rouge">git commit --amend</code> 进行修补提交。</p>
</li>
<li>
<p>如果把项目中文件 <code class="language-plaintext highlighter-rouge">hello.c</code> 的内容破坏了,如何使其还原至原始版本? _____</p>
<p>a) <code class="language-plaintext highlighter-rouge">git reset -- hello.c</code></p>
<p>b) <code class="language-plaintext highlighter-rouge">git checkout HEAD -- hello.c</code></p>
<p>c) <code class="language-plaintext highlighter-rouge">git revert hello.c</code></p>
<p>d) <code class="language-plaintext highlighter-rouge">git update hello.c</code></p>
</li>
<li>
<p>修改的文档 <code class="language-plaintext highlighter-rouge">meeting.doc</code> 尚未提交,因为错误地执行了 <code class="language-plaintext highlighter-rouge">git reset --hard</code> 导致数据丢失。丢失的数据能找回么? _____</p>
<p>a) 不能。执行硬重置使工作区文件被覆盖,导致数据丢失无法找回。</p>
<p>b) 能。可以通过 <code class="language-plaintext highlighter-rouge">git checkout HEAD@{1} -- meeting.doc</code> 找回。</p>
<p>c) 不确定。如果在重置前执行了 <code class="language-plaintext highlighter-rouge">git add</code> 命令将 <code class="language-plaintext highlighter-rouge">meeting.doc</code> 加入了暂存区,则可以在对象库中处于悬空状态的文件中找到。</p>
<p>d) 不能。因为未提交所以无法找回。</p>
</li>
<li>
<p>仅将工作区中修改的文件添加到暂存区(新增文件不添加),以备提交,用什么命令标记最快? _____</p>
<p>a) <code class="language-plaintext highlighter-rouge">git add -A</code></p>
<p>b) <code class="language-plaintext highlighter-rouge">git add -p</code></p>
<p>c) <code class="language-plaintext highlighter-rouge">git add -i</code></p>
<p>d) <code class="language-plaintext highlighter-rouge">git add -u</code></p>
</li>
<li>
<p>下面哪一个命令 <strong>不</strong> 会改变提交历史? _____</p>
<p>a) <code class="language-plaintext highlighter-rouge">git reset --hard HEAD~1</code></p>
<p>b) <code class="language-plaintext highlighter-rouge">git checkout HEAD^^ .</code></p>
<p>c) <code class="language-plaintext highlighter-rouge">git rebase -i HEAD^^</code></p>
<p>d) <code class="language-plaintext highlighter-rouge">git commit --amend</code></p>
</li>
<li>
<p>我使用和其他人不一样的IDE软件,总是在目录下生成以 <code class="language-plaintext highlighter-rouge">.xx</code> 为后缀的临时文件。如何避免由于自己的误操作导致此类文件被添加到版本库中呢? _____</p>
<p>a) 执行 <code class="language-plaintext highlighter-rouge">git clean -f</code> 删除临时性文件。</p>
<p>b) 向版本库中添加一个 <code class="language-plaintext highlighter-rouge">.gitignore</code> 文件,其中包含一条内容为 <code class="language-plaintext highlighter-rouge">*.xx</code> 的记录。</p>
<p>c) 在文件 <code class="language-plaintext highlighter-rouge">.git/info/exclude</code> 中添加一条内容为 <code class="language-plaintext highlighter-rouge">*.xx</code> 的记录。</p>
<p>d) 更换另外一款IDE软件。</p>
</li>
<li>
<p>项目跨平台导致文件中的换行符不一致。其中有 Linux 格式换行符(0A),也有DOS格式换行符(0D 0A)。要如何避免此类情况呢? _____</p>
<p>a) 修改 <code class="language-plaintext highlighter-rouge">/etc/gitattributes</code> 文件,在其中包含一条内容为 <code class="language-plaintext highlighter-rouge">* text=auto</code> 的设置。</p>
<p>b) 执行命令 <code class="language-plaintext highlighter-rouge">git config --global core.autocrlf true</code> 。</p>
<p>c) 执行命令 <code class="language-plaintext highlighter-rouge">git config --global core.autocrlf input</code> 。</p>
<p>d) 向版本库中添加一个 <code class="language-plaintext highlighter-rouge">.gitattributes</code> 文件,在其中包含一条内容为 <code class="language-plaintext highlighter-rouge">* text=auto</code> 的设置。</p>
</li>
<li>
<p>下列对于版本库授权说法正确的是:_____</p>
<p>a) 可以为分支或路径设置不同的写入权限,但不能设置不同的读取权限。</p>
<p>b) 除管理员外,版本库的创建者都可以为自己创建的版本库授权。</p>
<p>c) 只要通过授权后,便不能限制所推送的提交的署名作者,可以是任何人。</p>
<p>d) 如果没有向版本库的写入权限,就一定没有读取权限。</p>
</li>
<li>
<p>取消服务器版本库中ID为 <code class="language-plaintext highlighter-rouge">a2387</code> 的提交,而且不能引起历史提交的变更,用什么操作? _____</p>
<p>a) <code class="language-plaintext highlighter-rouge">git rebase -i a2387^</code></p>
<p>b) <code class="language-plaintext highlighter-rouge">git checkout a2387^ -- .</code></p>
<p>c) <code class="language-plaintext highlighter-rouge">git revert a2387</code></p>
<p>d) <code class="language-plaintext highlighter-rouge">git reset --hard a2387^</code></p>
</li>
<li>
<p>从版本库中的历史提交中彻底移除ID为 <code class="language-plaintext highlighter-rouge">a2387</code> 的提交,用什么操作? _____</p>
<p>a) <code class="language-plaintext highlighter-rouge">git reset --hard a2387^</code></p>
<p>b) <code class="language-plaintext highlighter-rouge">git checkout a2387^ -- .</code></p>
<p>c) <code class="language-plaintext highlighter-rouge">git revert a2387</code></p>
<p>d) <code class="language-plaintext highlighter-rouge">git rebase --onto a2387^ a2387 HEAD</code></p>
</li>
<li>
<p>所有改动的文件都已加入暂存区,若希望将其中的 <code class="language-plaintext highlighter-rouge">other.py</code> 文件下次再提交,如何操作? _____</p>
<p>a) <code class="language-plaintext highlighter-rouge">git reset -- other.py</code></p>
<p>b) <code class="language-plaintext highlighter-rouge">git checkout -- other.py</code></p>
<p>c) <code class="language-plaintext highlighter-rouge">git checkout HEAD other.py</code></p>
<p>d) <code class="language-plaintext highlighter-rouge">git reset --hard -- other.py</code></p>
</li>
<li>
<p>若产品的版本号显示为 <code class="language-plaintext highlighter-rouge">1.7.10.rc0-33-g9678d-dirty</code> ,可以判断出此版本号是如何生成的么? _____</p>
<p>a) <code class="language-plaintext highlighter-rouge">git tag</code></p>
<p>b) <code class="language-plaintext highlighter-rouge">git describe --tags --always --dirty</code></p>
<p>c) <code class="language-plaintext highlighter-rouge">git name-rev HEAD</code></p>
<p>d) <code class="language-plaintext highlighter-rouge">git --version</code></p>
</li>
<li>
<p>关于 <code class="language-plaintext highlighter-rouge">git clone</code> 下面说法 <strong>错误</strong> 的是:_____</p>
<p>a) 克隆时所有分支均被克隆,但只有HEAD指向的分支被检出。</p>
<p>b) 可以通过 <code class="language-plaintext highlighter-rouge">git clone --single-branch</code> 命令实现只克隆一个分支。</p>
<p>c) 克隆出的工作区中执行 <code class="language-plaintext highlighter-rouge">git log</code> 、 <code class="language-plaintext highlighter-rouge">git status</code> 、 <code class="language-plaintext highlighter-rouge">git checkout</code> 、 <code class="language-plaintext highlighter-rouge">git commit</code> 等操作不会去访问远程版本库。</p>
<p>d) 克隆时只有远程版本库HEAD指向的分支被克隆。</p>
</li>
<li>
<p>关于删除分支 <code class="language-plaintext highlighter-rouge">XX</code> ,下列说法正确的是: _____</p>
<p>a) 执行 <code class="language-plaintext highlighter-rouge">git push origin :XX</code> 来删除远程版本库的 <code class="language-plaintext highlighter-rouge">XX</code> 分支。</p>
<p>b) 执行 <code class="language-plaintext highlighter-rouge">git branch -D XX</code> 删除分支,总是能成功。</p>
<p>c) 远程版本库删除的分支,在执行 <code class="language-plaintext highlighter-rouge">git fetch</code> 时本地分支自动删除。</p>
<p>d) 本地删除的分支,执行 <code class="language-plaintext highlighter-rouge">git push</code> 时,远程分支亦自动删除。</p>
</li>
<li>
<p>下面的操作中哪一个不能确认维护分支 <code class="language-plaintext highlighter-rouge">maint</code> 上所有的 bugfix 提交均已合并至当前分支 <code class="language-plaintext highlighter-rouge">master</code> 中。 _____</p>
<p>a) <code class="language-plaintext highlighter-rouge">git rev-list ..maint</code> 的输出为空。</p>
<p>b) 在 <code class="language-plaintext highlighter-rouge">maint</code> 分支成功地执行 <code class="language-plaintext highlighter-rouge">git merge master</code> 。</p>
<p>c) <code class="language-plaintext highlighter-rouge">git log ..maint</code> 的输出为空。</p>
<p>d) 新版本发布,在 <code class="language-plaintext highlighter-rouge">maint</code> 分支执行 <code class="language-plaintext highlighter-rouge">git merge --ff-only master</code> 成功。</p>
</li>
<li>
<p>一个图片文件 <code class="language-plaintext highlighter-rouge">logo.png</code> 冲突了,如何取出他人的版本。 _____</p>
<p>a) <code class="language-plaintext highlighter-rouge">git show :1:./logo.png > logo.png-theirs</code></p>
<p>b) <code class="language-plaintext highlighter-rouge">git show :2:./logo.png > logo.png-theirs</code></p>
<p>c) <code class="language-plaintext highlighter-rouge">git show :3:./logo.png > logo.png-theirs</code></p>
<p>d) <code class="language-plaintext highlighter-rouge">git show :0:./logo.png > logo.png-theirs</code></p>
</li>
<li>
<p>工作在特性分支,常常因为执行 <code class="language-plaintext highlighter-rouge">git push</code> 默认推送所有本地和远程共有分支,导致非当前分支报告 non-fast-forward 错误。如果设置只推送当前分支可避免此类问题。下面操作正确的是:_____</p>
<p>a) <code class="language-plaintext highlighter-rouge">git config --global push.default upstream</code></p>
<p>b) <code class="language-plaintext highlighter-rouge">git config --global pull.rebase true</code></p>
<p>c) <code class="language-plaintext highlighter-rouge">git config --global receive.denyDeletes true</code></p>
<p>d) <code class="language-plaintext highlighter-rouge">git config --global pager.status true</code></p>
</li>
<li>
<p>关于对象库(.git/objects)说法 <strong>错误</strong> 的是:_____</p>
<p>a) 两个内容相同文件名不同的文件,在对象库中仅有一个拷贝。</p>
<p>b) 对象库执行 <code class="language-plaintext highlighter-rouge">git gc</code> 操作后,reflog 会被清空导致其中记录的未跟踪提交及指向的文件被丢弃。</p>
<p>c) 删除文件后,再通过添加相同文件找回,不会造成版本库的冗余。</p>
<p>d) 对象库并非一直保持最优存储,而是通过周期性地执行 <code class="language-plaintext highlighter-rouge">git gc</code> 优化版本库。</p>
</li>
<li>
<p>关于子模组 <strong>错误</strong> 的说法是:_____</p>
<p>a) 克隆父版本库,默认不会克隆子模组版本库。</p>
<p>b) 子模组可以嵌套。执行 <code class="language-plaintext highlighter-rouge">git submodule update --recursive</code> 可对嵌套子模组进行更新。</p>
<p>c) 子模组和父版本库的新提交,要先推送父版本库,后推送子模组。</p>
<p>d) 子模组检出处于分离头指针状态(gitlink的指向),在子模组中工作需要手动切换分支。</p>
</li>
<li>
<p>当一个提交说明显示为 <code class="language-plaintext highlighter-rouge">souce code refactor (fix #529)</code> ,下面哪个说法是正确的? _____</p>
<p>a) 这个提交只是代码重构,并未修复任何东西,因此没有改变版本库的提交历史。</p>
<p>b) 这个提交修正了第529号提交,没有改变版本库的提交历史。</p>
<p>c) 这个提交撤销了第529号提交,改变了版本库的提交历史。</p>
<p>d) 这个提交和项目的缺陷跟踪平台(如Redmine)关联,并会更新相关问题的状态。</p>
</li>
</ol>
<hr />
Git中文本地化
2012-02-28T00:00:00+00:00
http://www.worldhello.net/2012/02/28/git-l10n
<p>Git从版本1.7.5(2011年4月)即开始国际化/本地化(i18n/l10n)的准备工作,到版本1.7.8正式完成了Git国际化/本地化框架和基础设施的工作。
参见 <a href="https://github.com/git/git/commit/5e9637c629702e3d41ad01d95956d1835d7338e0">提交 5e9637c</a> 。</p>
<p>针对部分语种的本地化工作已经开始,最早会在 Git 1.7.10 版本开始提供中文的本地化支持。</p>
<h2 id="git中文本地化注意事项">Git中文本地化注意事项</h2>
<p>Git本地化(包括中文本地化)的协同方式不同于Git项目本身,不使用邮件列表进行提交评审,而是通过GitHub的协同工具来完成,这在Git邮件列表中有专门的讨论: <a href="http://article.gmane.org/gmane.comp.version-control.git/189584">Git官方邮件列表</a> 。最终确定的工作流程参见Git源码中的文件 <a href="https://github.com/git/git/blob/master/po/README">po/README</a> 。</p>
<h3 id="中文本地化工作流程">中文本地化工作流程</h3>
<p>中文本地化的协同版本库为: <a href="https://github.com/gotgit/git-po-zh_CN/">https://github.com/gotgit/git-po-zh_CN/</a> ,以下简称 <a href="https://github.com/gotgit/git-po-zh_CN/">git-po-zh_CN</a> 。
如果您对翻译有修改或补充,请参照如下工作流程。</p>
<ol>
<li>
<p>使用 GitHub 的 Fork 功能从 <a href="https://github.com/gotgit/git-po-zh_CN/">git-po-zh_CN</a> 派生项目。</p>
</li>
<li>
<p>从您个人派生的版本库克隆。如:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> git clone git@github.com:<your-account>/git-po-zh_CN.git
cd git-po-zh_CN
</code></pre></div> </div>
</li>
<li>
<p>修改 <a href="https://github.com/gotgit/git-po-zh_CN/blob/master/po/zh_CN.po">po/zh_CN.po</a> 文件,补充和完善翻译内容,并提交。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> edit po/zh_CN.po
...
git add -u
git commit
</code></pre></div> </div>
</li>
<li>
<p>将您的提交推送至GitHub(您的派生版本库)。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> git push
</code></pre></div> </div>
</li>
<li>
<p>使用GitHub提供的 pull request 功能,创建到 <a href="https://github.com/gotgit/git-po-zh_CN/">git-po-zh_CN</a> 版本库的 Pull Request。</p>
</li>
<li>
<p>版本库 <a href="https://github.com/gotgit/git-po-zh_CN/">git-po-zh_CN</a> 的管理者审核您的 Pull Request。</p>
</li>
<li>
<p>若审核通过则将提交合并到代码树中,pull request 自动关闭。</p>
</li>
<li>
<p>若提交有问题,管理者会通过评论给出原因。作为贡献者继续执行步骤3、步骤4,当修改后的提交推送(或强制推送)到GitHub后,pull request 中的提交会自动更新。管理者基于贡献者的最新提交重新审核(即步骤6)。</p>
</li>
</ol>
<h3 id="工作协同">工作协同</h3>
<p>为了避免多人翻译过程中的工作重叠,使用 GitHub 维基帮助工作协同。即在翻译之前现在维基页面上领受工作任务,不同贡献者的工作任务不要重叠。</p>
<p>例如中文本地化工作任务管理WIKI: https://github.com/gotgit/git-po-zh_CN/wiki/TaskList 。</p>
<h3 id="提交规范">提交规范</h3>
<p>本地化参与者在创建 Pull Request 前,请先确认提交是否遵守下列规范:</p>
<ul>
<li>
<p>只能修改 <a href="https://github.com/gotgit/git-po-zh_CN/blob/master/po/zh_CN.po">po/zh_CN.po</a> 文件,或者在 <a href="https://github.com/gotgit/git-po-zh_CN/blob/master/po/TEAMS">po/TEAMS</a> 文件中补充中文翻译团队成员列表。若对其他文件进行改动,会导致 pull request 被拒绝。</p>
</li>
<li>
<p>提交说明符合 “50/72 原则”。</p>
<ul>
<li>
<p>提交说明第一行会作为补丁邮件的标题或者作为补丁文件的文件名,长度以50个字符为限。</p>
<p>有人喜欢第一行提交说明的末尾添加一个“点”表示句子的结束,这是不可取的。试想你在写邮件是标题要以点来结束么?一个文件名(不含扩展名)如果以点结束,加上扩展名或出现两个点!</p>
<p>还有要注意这部分内容不能包含中文,建议提交说明使用 <code class="language-plaintext highlighter-rouge">l10n:</code> 作为前缀,以便和Git其他代码的提交相区分。</p>
</li>
<li>
<p>一个空行。此空行用以分隔标题和详细描述。</p>
</li>
<li>
<p>关于提交的大段说明(可选)。例如原翻译存在的问题,为什么要进行修改等。</p>
<p>提交说明可以折行,每行以72个字符为限。这部分提交说明可以包含少量中文。</p>
</li>
</ul>
</li>
<li>
<p>提交说明中包含签名,并且签名和提交说明间用空行分隔。</p>
<p>可以通过命令 <code class="language-plaintext highlighter-rouge">git commit -s</code> 会自动在提交说明中添加签名。</p>
</li>
</ul>
<p>作为Git的 l10n 协调者(l10n coordinator),我写了一个脚本以实现对本地化文件、提交规范的自动化检查。
l10n teamder 在向 l10n coordinator 创建 pull request 前,建议使用该脚本对提交进行体检。</p>
<ul>
<li>
<p>获取 <code class="language-plaintext highlighter-rouge">po-helper.sh</code> 脚本到本地工作区的 <code class="language-plaintext highlighter-rouge">po</code> 目录下。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ git fetch git://github.com/git-l10n/git-po.git po-helper
来自 git://github.com/git-l10n/git-po
* branch po-helper -> FETCH_HEAD
$ git checkout FETCH_HEAD -- po/
</code></pre></div> </div>
</li>
<li>
<p>检查本地的新提交。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ po/po-helper.sh check commits
</code></pre></div> </div>
</li>
<li>
<p>检查最近的5次提交。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ po/po-helper.sh check commits HEAD~5
</code></pre></div> </div>
</li>
<li>
<p>检查本地化文件。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ po/po-helper.sh check commits zh_CN.po
</code></pre></div> </div>
</li>
<li>
<p>l10n 协调者对本地化的全面检查。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ po/po-helper.sh check
</code></pre></div> </div>
</li>
</ul>
<h3 id="翻译示例">翻译示例</h3>
<ul>
<li>
<p>C语言字符串中的参数(占位符)很重要,需要维持其顺序。</p>
<p>注意字符串中出现的两个占位符 <code class="language-plaintext highlighter-rouge">%d</code> 和 <code class="language-plaintext highlighter-rouge">%s</code> 的先后顺序要严格保持,或者采用后面的解决方案。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> #: builtin/apply.c:824
#, c-format
msgid "regexec returned %d for input: %s"
msgstr "regexec 返回 %d,输入为:%s"
</code></pre></div> </div>
</li>
<li>
<p>如果确有需要,可以使用类似 <code class="language-plaintext highlighter-rouge">%<n>$s</code> 的语法对C语言字符串中的参数(占位符)进行标记,以便调整语序。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> #: builtin/apply.c:3266
#, c-format
msgid "new mode (%o) of %s does not match old mode (%o)%s%s"
msgstr "%2$s 的新模式(%1$o)和旧模式(%3$o)%4$s%5$s 不匹配"
</code></pre></div> </div>
</li>
<li>
<p>然而,Shell语言字符串中的变量(占位符)的顺序则不重要,可以根据翻译需要调整先后次序。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> msgid "Clone of '$url' into submodule path '$path' failed"
msgstr "无法克隆 '$url' 到子模组路径 '$path'"
</code></pre></div> </div>
</li>
<li>
<p>大多数情况下,英文标点翻译为中文标点。如:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> msgid ""
"'%s' appears to be a git command, but we were not\n"
"able to execute it. Maybe git-%s is broken?"
msgstr ""
"'%s' 像是一个 git 命令,但却无法运行。\n"
"可能是 git-%s 受损?"
</code></pre></div> </div>
</li>
<li>
<p>但是有的字符串要由程序进行后处理,并且处理过程中需要依赖其中的标点符号,这种情况下标点符号不能翻译,要严格保持。如:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # 译者:末尾两个字节可能被删减,如果翻译为中文标点会出现半个汉字
#: wt-status.c:250
msgid "modified content, "
msgstr "修改的内容, "
</code></pre></div> </div>
</li>
<li>
<p>大多数情况下,要注意保持原文中句子前、后,以及句子中间的空白。大多是排版的需要。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # 译者:注意保持前导空格
#: diff.c:105
#, c-format
msgid " Failed to parse dirstat cut-off percentage '%.*s'\n"
msgstr " 无法解析 dirstat 阈值 '%.*s'\n"
# 译者:为保证在输出中对齐,注意调整句中空格!
#: wt-status.c:266
#, c-format
msgid "new file: %s"
msgstr "新文件: %s"
# 译者:为保证在输出中对齐,注意调整句中空格!
#: wt-status.c:269
#, c-format
msgid "copied: %s -> %s"
msgstr "拷贝: %s -> %s"
# 译者:注意保持句尾空格
#: wt-status.c:890
msgid "Initial commit on "
msgstr "初始提交于 "
</code></pre></div> </div>
</li>
<li>
<p>但有的情况下涉及到两个翻译为中文的字符串拼接,原文中句子前、后包括句中的空白需要在译文中删除。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # 译者:汉字之间无空格,故删除尾部空格
#. TRANSLATORS: This is "remote " in "remote branch '%s' not found"
#: builtin/branch.c:163
msgid "remote "
msgstr "远程"
# 译者:%s若翻为中文,前后不需要空格
#: builtin/commit.c:783
#, c-format
msgid ""
"\n"
"It looks like you may be committing a %s.\n"
"If this is not correct, please remove the file\n"
"\t%s\n"
"and try again.\n"
msgstr ""
"\n"
"看起来您正在提交一个%s。\n"
"如果不是这样,请删除文件\n"
"\t%s\n"
"然后重试。\n"
</code></pre></div> </div>
</li>
<li>
<p>大多数情况下,考虑到输出的美观,需要重新对译文字符串进行排版(重新设置换行符)。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> #: builtin/commit.c:42
msgid ""
"Your name and email address were configured automatically based\n"
"on your username and hostname. Please check that they are accurate.\n"
"You can suppress this message by setting them explicitly:\n"
"\n"
" git config --global user.name \"Your Name\"\n"
" git config --global user.email you@example.com\n"
"\n"
"After doing this, you may fix the identity used for this commit with:\n"
"\n"
" git commit --amend --reset-author\n"
msgstr ""
"您的姓名和邮件地址基于登录名和主机名进行了自动设置。请检查它们正确\n"
"与否。您可以通过下面的命令对其进行明确地设置以免再出现本提示信息:\n"
"\n"
" git config --global user.name \"Your Name\"\n"
" git config --global user.email you@example.com\n"
"\n"
"设置完毕后,您可以用下面的命令来修正本次提交所使用的用户身份:\n"
"\n"
" git commit --amend --reset-author\n"
</code></pre></div> </div>
</li>
<li>
<p>但有时保持原有换行方式更好。</p>
<p>下面的示例中如果译文将两行合并为同一行,会因为两处的 <code class="language-plaintext highlighter-rouge">%s</code> 被扩展造成输出太长。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # 译者:保持原换行格式,在输出时 %s 的替代内容会让字符串变长
#: builtin/branch.c:137
#, c-format
msgid ""
"deleting branch '%s' that has been merged to\n"
" '%s', but not yet merged to HEAD."
msgstr ""
"将要删除的分支 '%s' 已经被合并到\n"
" '%s',但未合并到 HEAD。"
</code></pre></div> </div>
</li>
</ul>
<h2 id="工具">工具</h2>
<ul>
<li>
<p>VIM 和 po.vim</p>
<p>安装 <code class="language-plaintext highlighter-rouge">po.vim</code> 插件后,使用 VIM 编辑 <code class="language-plaintext highlighter-rouge">.po</code> 文件,可以使用如下热键非常方便地进行本地化工作。</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">\u</code> - 下一条未翻译条目</li>
<li><code class="language-plaintext highlighter-rouge">\U</code> - 上一条未翻译条目</li>
<li><code class="language-plaintext highlighter-rouge">\f</code> - 下一条fuzzy条目</li>
<li><code class="language-plaintext highlighter-rouge">\F</code> - 上一条fuzzy条目</li>
<li><code class="language-plaintext highlighter-rouge">\c</code> - 将 msgid 的字符串拷贝至 msgstr</li>
<li><code class="language-plaintext highlighter-rouge">\d</code> - 删除 msgstr 字符串</li>
<li><code class="language-plaintext highlighter-rouge">\z</code> - 将字串标记为 fuzzy</li>
<li><code class="language-plaintext highlighter-rouge">\Z</code> - 移除字串标的 fuzzy 标记</li>
<li><code class="language-plaintext highlighter-rouge">\s</code> - 显示统计信息</li>
<li><code class="language-plaintext highlighter-rouge">\W</code> - 格式化文件</li>
</ul>
<p>下载地址: http://www.vim.org/scripts/script.php?script_id=2530</p>
</li>
<li>
<p>kbabel, loaklize</p>
<p>Linux 平台有非常好用的图形界面工具辅助本地化,如 kbabel, lokalize 等工具可以方便地对 <code class="language-plaintext highlighter-rouge">.po</code> 文件进行编辑。</p>
<p>不过我还是喜欢在文本工作模式(VIM + po.vim),尤其是切换到 Mac OS X 平台后无法用到 kbabel 和 lokalize。</p>
</li>
</ul>
<h2 id="术语表">术语表</h2>
<p>已翻译内容中涉及到的术语的翻译做到了基本一致,若遇到不确认的术语请先在已翻译内容中查找,恕不一一列举。</p>
<p><a name="i18n" id="i18n"></a></p>
<h2 id="为什么有的git命令的输出还没有翻译">为什么有的Git命令的输出还没有翻译?</h2>
<p>如果遇到您需要的Git命令尚未翻译,往往不是因为中文本地化翻译未完成,而是因为相应命令的输出尚未进行国际化标识。
Git 的国际化(i18n)和本地化(l10n)刚刚开始,肯定存在很多命令输出被遗漏,没有使用相关语法进行标识,导致未能在本地化文件中出现。</p>
<p>例如我发现和解决的几处Git国际化遗漏的问题:</p>
<ul>
<li><a href="http://thread.gmane.org/gmane.comp.version-control.git/189524/focus=189528">[PATCH] i18n: format_tracking_info “Your branch is behind” message</a> 及其最终版本 <a href="http://thread.gmane.org/gmane.comp.version-control.git/189575">PATCH v3</a></li>
<li><a href="http://thread.gmane.org/gmane.comp.version-control.git/189523/focus=189558">[PATCH] i18n: git-commit whence_s “merge/cherry-pick” message</a></li>
</ul>
<p>Git国际化不完整的问题不能采用本文介绍的中文本地化工作流程进行操作,而是按照修改Git源代码的工作流程进行工作。
具体参见 <a href="https://github.com/git/git/blob/master/Documentation/SubmittingPatches">Documentation/SubmittingPatchest</a> 。</p>
Gitolite 管理员自定义命令
2011-11-30T00:00:00+00:00
http://www.worldhello.net/2011/11/30/05-gitolite-adc
<p>通配符版本库的删除操作在实际应用中也常常遇到,《Git权威指南》 P439 页的 ADC 相关内容需要扩展。更新如下:</p>
<hr />
<p>管理员在服务器特定目录下保存一些可执行脚本,当执行类似下面的命令时,会自动调用相应的可执行脚本 <code class="language-plaintext highlighter-rouge"><command></code> 。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ssh git@server <command> <args>...
</code></pre></div></div>
<p>在 Gitolite 源码的 <code class="language-plaintext highlighter-rouge">contrib/adc</code> 目录下维护了一些有用的管理员自定义命令,可以直接拿来使用。</p>
<ul>
<li>
<p>管理员为自定义命令创建目录。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ mkdir ~git/adc-bin
</code></pre></div> </div>
</li>
<li>
<p>将 ADC 脚本拷贝到该目录中。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ cp /path/to/gitolite/src/contrib/adc/* ~git/adc-bin/
</code></pre></div> </div>
</li>
<li>
<p>修改 <code class="language-plaintext highlighter-rouge">.gitolite.rc</code> 配置文件,设定 <code class="language-plaintext highlighter-rouge">$GL_ADC_PATH</code> 变量。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $GL_ADC_PATH = "/home/git/adc-bin";
</code></pre></div> </div>
</li>
</ul>
<p>注意:ADC 脚本可能会带来安全性问题,管理员在开发新的 ADC 脚本要尤为小心,还要注意 ADC 脚本名称不要和 Gitolite 内置的命令重名,否则会覆盖 Gitolite 相应的内置命令,导致 Gitolite 无法正常运行。</p>
<p>常用的管理员自定义命令有:</p>
<ul>
<li>
<p>解锁版本库。</p>
<p>版本库的创建者可以为版本库加锁和解锁。只有处于解锁状态的版本库才可以被删除。解锁版本库命令:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ ssh git@server unlock name/of/repo.git
</code></pre></div> </div>
</li>
<li>
<p>版本库加锁。</p>
<p>版本库的创建者可以为版本库加锁和解锁。新创建的通配符版本库默认处于锁定状态,处于锁定状态的版本库不能被删除。加锁版本库命令:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ ssh git@server lock name/of/repo.git
</code></pre></div> </div>
</li>
<li>
<p>删除版本库。</p>
<p>执行下面的命令会永久删除版本库。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ ssh git@server rm name/of/repo.git
</code></pre></div> </div>
<p>我对 ADC 脚本进行了定制,会将版本库移动到指定的目录下而不是直接删除。参见 <a href="https://github.com/ossxp-com/gitolite/commit/b6f4f020095a07be4b445d549c07f294f900ee27">相关补丁</a> 。</p>
</li>
<li>
<p>超级用户读取和设置他人创建的版本库授权。</p>
<p>默认只有版本库创建者才能为自己创建的版本库授权以及读取权限。名为 <code class="language-plaintext highlighter-rouge">su-getperms</code> 和 <code class="language-plaintext highlighter-rouge">su-setperms</code> 的 ADC 脚本可以让 Gitolite 管理员读取和设置他人创建的版本库授权。如:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ ssh git@server su-getperms username name/of/repo.git
</code></pre></div> </div>
</li>
</ul>
Gitolite 通配符版本库自定义授权
2011-11-30T00:00:00+00:00
http://www.worldhello.net/2011/11/30/04-gitolite-getperms-setperms
<p>《Git权威指南》第一版,在介绍 Gitolite 通配符版本库的创建一节(即书 P429 页),关于创建者自定义授权的相关内容存在瑕疵,授权文件中缺乏对 <code class="language-plaintext highlighter-rouge">READERS</code> 和 <code class="language-plaintext highlighter-rouge">WRITERS</code> 的设置,在新的 Gitolite 中不能正常设置授权。本博客对相关内容进行更新。</p>
<hr />
<h3 id="每个人创建自己的版本库">每个人创建自己的版本库</h3>
<p>授权文件如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 @administrators = jiangxin admin
2
3 repo users/CREATOR/[a-zA-Z].*
4 C = @all
5 RW+ = CREATOR
6 RW = WRITERS
7 R = READERS @administrators
</code></pre></div></div>
<p>关于授权的说明:</p>
<ul>
<li>
<p>第4条指令,设置用户可以在自己的名字空间( <code class="language-plaintext highlighter-rouge">/usrs/<userid>/</code> )下,自己创建版本库。例如下面就是用户 <code class="language-plaintext highlighter-rouge">dev1</code> 执行 <code class="language-plaintext highlighter-rouge">git push</code> 命令在Gitolite服务器上自己的名字空间下创建版本库。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> dev1$ git push git@server:users/dev1/repos1.git master
</code></pre></div> </div>
</li>
<li>
<p>第5条指令,设置版本库创建者对版本库具有完全权限。</p>
<p>即用户 <code class="language-plaintext highlighter-rouge">dev1</code> 拥有对其自建的 <code class="language-plaintext highlighter-rouge">users/dev1/repos1.git</code> 拥有最高权限。</p>
</li>
<li>
<p>第7条指令,让管理员组 <code class="language-plaintext highlighter-rouge">@administrators</code> 的用户对于 <code class="language-plaintext highlighter-rouge">users/</code> 下用户自建的版本库拥有读取权限。</p>
</li>
</ul>
<p>那么第6、7条授权指令中出现的 <code class="language-plaintext highlighter-rouge">WRITERS</code> 和 <code class="language-plaintext highlighter-rouge">READERS</code> 是如何定义的呢?实际上这两个变量可以看做是两个用户组,不过这两个用户组不是在Gitolite授权文件中设置,而是由版本库创建者执行 <code class="language-plaintext highlighter-rouge">ssh</code> 命令创建的。</p>
<p>版本库 <code class="language-plaintext highlighter-rouge">users/dev1/repos1.git</code> 的创建者 <code class="language-plaintext highlighter-rouge">dev1</code> 可以通过 <code class="language-plaintext highlighter-rouge">ssh</code> 命令连接服务器,使用 <code class="language-plaintext highlighter-rouge">setperms</code> 命令为自己的版本库设置角色。命令 <code class="language-plaintext highlighter-rouge">setperms</code> 的唯一一个参数就是版本库名称。当执行命令时,会自动进入一个编辑界面,手动输入角色定义后,按下 <code class="language-plaintext highlighter-rouge">^D</code>(Ctrl+D)结束编辑。如下所示:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dev1$ ssh git@server setperms users/dev1/repos1.git
READERS dev2 dev3
WRITERS jiangxin
^D
</code></pre></div></div>
<p>即在输入 setperms 命令后,进入一个编辑界面,输入 ^D(Ctrl+D)结束编辑。也可以将角色定义文件保存到文件中,用 <code class="language-plaintext highlighter-rouge">setperms</code> 加载。如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dev1$ cat > perms << EOF
READERS dev2 dev3
WRITERS jiangxin
EOF
dev1$ ssh git@server setperms users/dev1/repos1.git < perms
New perms are:
READERS dev2 dev3
WRITERS jiangxin
</code></pre></div></div>
<p>当版本库创建者 <code class="language-plaintext highlighter-rouge">dev1</code> 对版本库 <code class="language-plaintext highlighter-rouge">users/dev1/repos1.git</code> 进行了如上设置后,Gitolite在进行授权检查时会将 <code class="language-plaintext highlighter-rouge">setperms</code> 设置的角色定义应用到授权文件中。故此版本库 <code class="language-plaintext highlighter-rouge">users/dev1/repos1.git</code> 中又补充了新的授权:</p>
<ul>
<li>
<p>用户 <code class="language-plaintext highlighter-rouge">dev2</code> 和 <code class="language-plaintext highlighter-rouge">dev3</code> 具有读取权限。</p>
</li>
<li>
<p>用户 <code class="language-plaintext highlighter-rouge">jiangxin</code> 具有读写权限。</p>
</li>
</ul>
<p>版本库 <code class="language-plaintext highlighter-rouge">users/dev1/repos1.git</code> 的建立者 <code class="language-plaintext highlighter-rouge">dev1</code> 可以使用 <code class="language-plaintext highlighter-rouge">getperms</code> 查看自己版本库的角色设置。如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dev1$ ssh git@server getperms users/dev1/repos1.git
READERS dev2 dev3
WRITERS jiangxin
</code></pre></div></div>
<p>如果在用户自定义授权中需要使用 <code class="language-plaintext highlighter-rouge">READERS</code> 和 <code class="language-plaintext highlighter-rouge">WRITERS</code> 之外的角色,管理员可以通过修改 <code class="language-plaintext highlighter-rouge">gitolite.rc</code> 文件中的变量 <code class="language-plaintext highlighter-rouge">$GL_WILDREPOS_PERM_CATS</code> 实现。该变量的默认设置如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$GL_WILDREPOS_PERM_CATS = "READERS WRITERS";
</code></pre></div></div>
Gitolite 版本库镜像
2011-11-30T00:00:00+00:00
http://www.worldhello.net/2011/11/30/03-gitolite-mirror
<p>说明:Gitlite 2.1版本库对版本库镜像改动较大,几乎完全重写,导致《Git权威指南》P436-P437 两页内容过时。更新如下。</p>
<hr />
<p>Git 版本库控制系统往往并不需要设计特别的容灾备份,因为每一个Git用户就是一个备份。但是下面的情况,就很有必要考虑容灾了。</p>
<ul>
<li>Git 版本库的使用者很少(每个库可能只有一个用户)。</li>
<li>版本库克隆只限制在办公区并且服务器也在办公区内(所有鸡蛋都在一个篮子里)。</li>
<li>Git 版本库采用集中式的应用模型,需要建立双机热备(以便在故障出现时,实现快速的服务器切换)。</li>
</ul>
<p>可以在两台或多台安装了Gitolite服务的服务器之间实现版本库的镜像。数据镜像的最小单位为版本库,对于任意一个Git版本库可以选择在其中一个服务器上建立主版本库(只能有一个主版本库),在其他服务器上建立的为镜像库。镜像库只接受来自主版本库的数据同步而不接受来自用户的推送。</p>
<h3 id="启用钩子">启用钩子</h3>
<p>将 Gitolite 部署目录下钩子 <code class="language-plaintext highlighter-rouge">post-receive.mirrorpush</code> 重命名为 <code class="language-plaintext highlighter-rouge">post-receive</code> ,并执行 <code class="language-plaintext highlighter-rouge">gl-setup</code> 命令为所有 Gitolite 控制下的版本库安装必须的钩子脚本。</p>
<h3 id="gitolite服务器命名">Gitolite服务器命名</h3>
<p>首先要为每一台服务器架设Gitolite服务,并建议所有的服务器上Gitolite服务都架设在同一用户(如 <code class="language-plaintext highlighter-rouge">git</code> )之下。如果Gitolite服务安装到不同的用户账号下,就必需通过文件 <code class="language-plaintext highlighter-rouge">~/.ssh/config</code> 建立SSH别名,以便能够使用正确的用户名连接服务器。</p>
<p>接下来为每个服务器设置一个名称,服务器之间数据镜像时就使用各自的名称进行连接。假设我们要配置的两个Gitolite服务器的其中一个名为 <code class="language-plaintext highlighter-rouge">server1</code> ,另一个名为 <code class="language-plaintext highlighter-rouge">server2</code> 。</p>
<p>打开 <code class="language-plaintext highlighter-rouge">server1</code> 上Gitolite的配置文件 <code class="language-plaintext highlighter-rouge">~/.gitolite.rc</code> ,进行如下设置:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$GL_HOSTNAME = 'serer1';
$GL_GITCONFIG_KEYS = "gitolite.mirror.*";
</code></pre></div></div>
<ul>
<li>设置 <code class="language-plaintext highlighter-rouge">$GL_HOSTNAME</code> 为本服务器的别名,如 <code class="language-plaintext highlighter-rouge">serer1</code> 。</li>
<li>
<p>设量 <code class="language-plaintext highlighter-rouge">$GL_GITCONFIG_KEYS</code> 以便允许在Gitolite授权文件中为版本库动态设置配置变量。</p>
<p>例如本例设置了 <code class="language-plaintext highlighter-rouge">GL_GITCONFIG_KEYS</code> 为 <code class="language-plaintext highlighter-rouge">gitolite.mirror.*</code> 后,允许在 <code class="language-plaintext highlighter-rouge">gitolite-admin</code> 管理库的 <code class="language-plaintext highlighter-rouge">conf/gitolite.conf</code> 中用 <code class="language-plaintext highlighter-rouge">config</code> 指令对版本库添加配置变量。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> repo testing
config gitolite.mirror.master = "server1"
config gitolite.mirror.slaves = "server2 server3"
</code></pre></div> </div>
</li>
</ul>
<p>同样对 <code class="language-plaintext highlighter-rouge">server2</code> 进行设置,只不过将 <code class="language-plaintext highlighter-rouge">$GL_HOSTNAME</code> 设置为 <code class="language-plaintext highlighter-rouge">serer2</code> 。</p>
<h3 id="服务器之间的公钥认证">服务器之间的公钥认证</h3>
<p>接下来每一个服务器为Gitolite的安装用户创建公钥/私钥对。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo su - git
$ ssh-keygen
</code></pre></div></div>
<p>然后把公钥拷贝到其他服务器上,并以本服务器名称命名。例如:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">server1</code> 上创建的公钥复制到 <code class="language-plaintext highlighter-rouge">server2</code> 上,命名为 <code class="language-plaintext highlighter-rouge">server1.pub</code> 备用。</li>
<li><code class="language-plaintext highlighter-rouge">server2</code> 上创建的公钥复制到 <code class="language-plaintext highlighter-rouge">server1</code> 上,命名为 <code class="language-plaintext highlighter-rouge">server2.pub</code> 备用。</li>
</ul>
<p>再运行 <code class="language-plaintext highlighter-rouge">gl-tool</code> 设置其他服务器到本服务器上的公钥认证。例如在 <code class="language-plaintext highlighter-rouge">server1</code> 上执行命令:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gl-tool add-mirroring-peer server2.pub
</code></pre></div></div>
<p>当完成上述设置后,就可以从一个服务器发起到另外服务器的SSH连接,连接过程无需口令认证并显示相关信息。例如从 <code class="language-plaintext highlighter-rouge">server1</code> 发起到 <code class="language-plaintext highlighter-rouge">server2</code> 的连接如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ssh git@server2 info
Hello server1, I am server2
</code></pre></div></div>
<h3 id="配置版本库镜像">配置版本库镜像</h3>
<p>做了前面的准备工作后,就可以开始启用版本库镜像了。下面通过一个示例介绍如何建立版本库镜像,将服务器 <code class="language-plaintext highlighter-rouge">server1</code> 上的版本库 <code class="language-plaintext highlighter-rouge">testing</code> 要镜像到服务器 <code class="language-plaintext highlighter-rouge">server2</code> 上。</p>
<p>首先要修改 <code class="language-plaintext highlighter-rouge">server1</code> 和 <code class="language-plaintext highlighter-rouge">server2</code> 的Gitolite管理库 <code class="language-plaintext highlighter-rouge">gitolite-admin</code> ,为 <code class="language-plaintext highlighter-rouge">testing</code> 版本库添加配置变量,如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>repo testing
config gitolite.mirror.master = "server1"
config gitolite.mirror.slaves = "server2"
</code></pre></div></div>
<p>两个服务器 <code class="language-plaintext highlighter-rouge">server1</code> 和 <code class="language-plaintext highlighter-rouge">server2</code> 都要做出同样的修改,提交改动并推送到服务器上。当推送完成,两个服务器上的 <code class="language-plaintext highlighter-rouge">testing</code> 版本库的 <code class="language-plaintext highlighter-rouge">config</code> 就会被更新,包含类似如下的设置:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[gitolite "mirror"]
master = server1
slaves = server2
</code></pre></div></div>
<p>当向服务器 <code class="language-plaintext highlighter-rouge">server1</code> 的 <code class="language-plaintext highlighter-rouge">testing</code> 版本库推送新的提交时,就会自动同步到 <code class="language-plaintext highlighter-rouge">server2</code> 上。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git push git@server1:testing.git master
[master c0b097a] test
Counting objects: 1, done.
Writing objects: 100% (1/1), 185 bytes, done.
Total 1 (delta 0), reused 0 (delta 0)
remote: (29781&) server1 ==== (testing) ===> server2
To git@server1:testing.git
d222699..c0b097a master -> master
</code></pre></div></div>
<p>如果需要将服务器 <code class="language-plaintext highlighter-rouge">server1</code> 上所有版本库,包括 <code class="language-plaintext highlighter-rouge">gitolite-admin</code> 版本库都同步到 <code class="language-plaintext highlighter-rouge">server2</code> 上,不必对版本库逐一设置,可以采用下面的简便方法。</p>
<p>修改 <code class="language-plaintext highlighter-rouge">server1</code> 和 <code class="language-plaintext highlighter-rouge">server2</code> 的Gitolite管理版本库 <code class="language-plaintext highlighter-rouge">gitolite-admin</code> ,在配置文件 <code class="language-plaintext highlighter-rouge">conf/gitolite.conf</code> 最开始插入如下设置。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>repo @all
config gitolite.mirror.master = "server1"
config gitolite.mirror.slaves = "server2"
</code></pre></div></div>
<p>然后分别提交并推送。要说明的是 <code class="language-plaintext highlighter-rouge">gitolite-admin</code> 版本库此时尚未建立同步,直到服务器 <code class="language-plaintext highlighter-rouge">server1</code> 的 <code class="language-plaintext highlighter-rouge">gitolite-admin</code> 版本库推送新的提交,才开始 <code class="language-plaintext highlighter-rouge">gitolite-admin</code> 版本库的同步。</p>
<p>也可以在 <code class="language-plaintext highlighter-rouge">server1</code> 服务器端执行命令开始同步。例如:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gl-mirror-shell request-push gitolite-admin
</code></pre></div></div>
<p>Gitolite官方版本在版本库同步时有个局限,要求在镜像服务器上必需事先存在目标版本库并正确设置了 <code class="language-plaintext highlighter-rouge">gitolite.mirror.*</code> 参数,才能同步成功。例如允许用户自行创建的通配符版本库,必需在主服务器上和镜像服务器上分别创建,之后版本库同步才能正常执行。我在GitHub上的Gitolite分支项目提交了 <a href="https://github.com/ossxp-com/gitolite/commit/a29446403edda42fc67c18f2d5b3f53625412eec" title="Topgit branch: t/mirror-missing-repo">一个补丁</a> 解决了这个问题。</p>
<p>如果发现镜像没有正常工作,查看目录 <code class="language-plaintext highlighter-rouge">~git/.gitolite/logs</code> 下相关日志文件。 更多Gitolite版本库镜像的资料,参见 <a href="http://sitaramc.github.com/gitolite/doc/mirroring.html">http://sitaramc.github.com/gitolite/doc/mirroring.html</a> 。</p>