2019-03-17

Go 语言解析 git config

最近做的一个 go 语言的项目需要频繁读写 git config 文件,一些看似现成的解决方案并不能满足需要:

  1. 不考虑调用外部命令 git config,因为在 Windows 平台性能差。
  2. 不考虑 libgit2,因为会给静态编译和发布带来麻烦。

在 github 上找到一个能够解析 git config 文件的项目 goconfig,该项目的代码直接从 Git 项目的 git/config.c 移植过来,可以确保兼容性。但是这个项目只是一个半成品,因为:

  1. 不支持多值配置项。Git 的很多设置实际上是多值配置项,例如:push.pushOptionremote.<name>.fetch 等。
  2. 不支持配置文件嵌套,即不支持通过 include.path 指令包含其他配置文件,而这在我们要开发的应用中至关重要。
  3. 不支持配置文件继承(多级配置)。Git 在读取一个配置项时,会依次读取系统级配置(/etc/gitconfig)、用户全局配置($HOME/.gitconfig)、仓库级配置文件(.git/config)、嵌套的配置文件(include.path 指向的配置文件)。从优先级上看,仓库级配置文件高于全局配置,更高于系统级配置。
  4. 不支持写配置文件。

于是派生了一个项目到 jiangxin/goconfig,实现了上述特性。

增加多值配置特性

实际上 Git 配置项,无论单值(如 user.name),还是多值(如 remote.<name>.fetch),都应该一视同仁当做多值来处理,这样配置文件嵌套、配置文件继承的处理就非常简单了。即:

  1. 每一个配置项(如 user.name)的值是一个字符串数组。
  2. 如果用户将某个配置项视为单值设置,只取数组的最后一项,作为该配置的唯一值。
  3. 如果用户将某个配置项视为多值设置,数组所有内容都是该配置项内容。

为此将 goconfig.go 的返回值由 map[string]string 替换为支持多值配置的自定义类型

type GitConfig map[string]GitConfigKeys
type GitConfigKeys map[string][]string

其中 GitConfig 的索引对应 config 配置文件的小节(section)名称,GitConfigKeys 的索引对应于配置项在小节内的 key。

GitConfig 最核心的方法是 GetAll,其他方法 Get, GetBool, GetInt 等都是基于 GetAll 方法。

参见提交 9e83c31 (Add GitConfig to read boolean, int, multi-values, 2019-02-28)

增加配置文件嵌套特性

配置文件嵌套,涉及到迭代和配置文件的合并。核心方法是 GitConfig 的 Merge 方法。

参见提交 83a00ae (Parse include config files from include.path, 2019-03-05)

增加配置文件继承

配置文件继承和配置文件嵌套非常相似,都是 GitConfig 结构体的 Merge,只不过多了一些文件 IO,以及缓存机制。

参见提交 a033f72 (Add Load() function to read git config from disk, 2019-03-04)

增加配置文件写操作

实际上第一个版本的 GitConfig 结构体定义为 map[string][]string,就可以实现多值配置。为了支持将 GitConfig 结构体回写为文件,对其做了提交修补(git commit –amend)操作。 GitConfig 的 map 索引变成了小节(section)名称。

为了支持写操作所做的第二个改变是为每一个值增加了一个范围(scope),这样在保存 GitConfig 到文件的时候,知道哪些配置来自于系统级(ScopeSystem)、全局级(ScopeGlobal)、 文件嵌套(ScopeGlobal),只将配置文件中的 ScopeSelf 记录到配置文件中。

参见提交:

一个简单的 git-config 实现

作为一个 lib,goconfig 项目的根目录是名为 goconfig 的包,为了演示如何用 goconfig 实现一个完整的 git config 命令的功能,在 cmd/gocongig 目录下写了一个 main 包。

可以用如下命令编译安装这一 goconfig 示例:

go get github.com/jiangxin/goconfig/cmd/goconfig

示例代码参见:cmd/goconfig/main.go

欢迎使用 goconfig 并帮助改进

View Comments
2016-02-29

二分查找捉虫记

1. 问题现象

Git 2.8.0 版本即将发布,今天把本地的 Git 版本升级到 2.8.0-rc0,结果悲剧了。所有使用 HTTP 协议的公司内部 Git 仓库都无法正常访问!

在确认不是网络和公司 Git 服务器问题之后,自然怀疑到了 HTTP 代理。果然清空了 http_proxy 环境变量后,Git 命令工作正常了:

http_proxy= git ls-remote http://server.name/git/repo.git

然而一旦通过 http_proxy=bad_proxy 环境变量设置了一个错误的代理,即便通过 no_proxy=* 期望绕过代理,Git 2.8.0 却无法正常工作:

$ 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'

可是我记得升级之前 Git 工作是正常的啊!于是在 Git 2.7.0 下进行了尝试。

将版本切换到 v2.7.0,编译安装 Git。(注意一定要安装,而不是执行当前目录下的 Git。这是因为该命令执行过程中会依次调用 git-ls-remotegit-remote-http 命令,而这两个命令是位于安装路径中的。)

$ git checkout v2.7.0
$ make -j8 && make install # 我的工作站是四核CPU,故此使用 -j8 两倍并发执行编译

测试发现 Git 2.7.0 能够通过 no_proxy 变量绕过错误的 http_proxy 环境变量:

$ http_proxy=bad_proxy no_proxy=* git ls-remote http://server.name/git/repo.git
206b4906c197e76fcc63d7a453f9e3aa00dfb3da        HEAD
206b4906c197e76fcc63d7a453f9e3aa00dfb3da        refs/heads/master

显然一定是 v2.7.0v2.8.0-rc0 中间的某个版本引入的 Bug!

2. 二分查找

想必大家都玩过猜数字游戏吧:一个人在1到100的数字中随意选择一个,另外一个人来猜,小孩子总是一个挨着一个地猜, 懂得折半查找的大人总是获胜者。Git 提供的 git bisect 这一命令,就是采用这样的二分查找快速地在提交中定位 Bug, 少则几次,多则十几次就会定位到引入Bug的提交。

  1. 首先执行下面命令启用二分查找。

     $ git bisect start
    
  2. 标记一个好版本。下面的命令使用 tag(v2.7.0)来标记 Git 2.7.0 版本是好版本,换做40位的提交号也行。

     $ git bisect good v2.7.0
    
  3. 标记 Git 2.8.0-rc0 是一个坏版本。注意:马上就是见证奇迹的时刻。

     $ 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
    

    看到了么?当完成对一个好版本和一个坏版本的标记后,Git 切换到一个中间版本(563e384),并告诉我们大概需要8步可以找到元凶。

  4. 在这个版本下执行前面的测试操作:

     $ 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'
    
  5. 对这个版本进行标记。

    这是一个坏版本:

     $ git bisect bad
     Bisecting: 126 revisions left to test after this (roughly 7 steps)
     [e572fef9d459497de2bd719747d5625a27c9b41d] Merge branch 'ep/shell-command-substitution-style'
    

我们可以机械地重复上面4、5的步骤,直到最终定位。但是人工操作很容易出错。如果对版本标记错了,把 good 写成了 bad 或者相反, 就要执行 git bisect reset 重来。(小窍门:git bisect log 可以显示 git bisect 标记操作日志)

于是决定剩下的二分查找使用脚本来完成。

3. 自动化的二分查找

Git 二分查找允许提供一个测试脚本,Git 会根据这个测试脚本的返回值,决定如何来标记提交:

  • 返回值为 0:这个提交是一个好提交。
  • 返回值为 125:这个提交无法测试(例如编译不过去),忽略这个提交。
  • 返回值为 1-127(125除外):这个提交是一个坏提交。
  • 其他返回值:二分查找出错,终止二分查找操作。

那么就我们先来看看 git ls-remote 的返回值:

  • 正确执行的返回值是 0:

      $ http_proxy= git ls-remote http://server.name/git/repo.git
      206b4906c197e76fcc63d7a453f9e3aa00dfb3da        HEAD
      206b4906c197e76fcc63d7a453f9e3aa00dfb3da        refs/heads/master
      $ echo $?
      0
    
  • 错误执行的返回值是 128!

      $ 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
    

于是创建一个测试脚本 git-proxy-bug-test.sh,内容如下:

#!/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

然后敲下如下命令,开始自动执行二分查找:

$ git bisect run sh git-proxy-bug-test.sh

自动化查找过程可能需要几分钟,站起来走走,休息一下眼睛。再回到座位,最终的定位结果就展现在了眼前:

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
二分查找运行成功

4. 解决问题

既然我们知道引入 Bug 的提交,让我们看看这个提交:

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

相比很多人一个提交动辄改动几百、几千行的代码,这个提交的改动算得上简短了。小提交的好处就是易于阅读、易于问题定位、易于回退。

最终参照上面定位到的问题提交,我的 Bugfix 如下(为了下面的一节叙述方便,给代码补丁增加了行号):

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

5. 写提交说明

这个提交是要贡献给 Git 上游的,评审者可能会问我如下问题:

  1. Bug 的现象是什么?

    “系统的 no_proxy 变量不起作用,git 可能无法访问 http 协议的仓库。”

  2. 从什么版本引入这个 Bug?

    “我们定位到的这个提交引入的 Bug。之所以会引入这个 Bug,是因为这个提交读取了 http_proxy 等环境变量, 自动通过 git-credential 获取的信息补齐 http_proxy 的缺失的代理认证口令,并显示设置 libcurl 的参数。”

  3. 之前的版本为什么没有出现这个问题?什么条件下会出现?

    “之前的版本也会出现问题,但是只有在用户主动设置了 http.proxy 配置变量才会出现。 用户很少会去设置 http.proxy 配置变量,而通常是使用 http_proxy 环境变量。”

  4. 你是如何解决的?你的解决方案是否最佳?

    “读取 no_proxy 环境变量,并为 libcurl 配置相应参数。因为 libcurl 只在 7.19.4 之后才引入 CURLOPT_NOPROXY,因此需要添加条件编译。”

实际上,前面的 Bugfix 原本是没有那个条件编译的。即补丁的第18行、22行一开始是没有的, 在回答第4个问题的时候,我仔细查看了 libcurl API, 发现只有在 7.19.4 版本之后,才支持 CURLOPT_NOPROXY 参数,因此如果不添加这个编译条件, 在特定的平台可能会导致 Git 无法编译通过。

下面就是最终的提交说明:

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>

6. 贡献给上游

Git 项目本身是通过邮件列表参与代码贡献的,基本的操作流程是将代码转换为补丁文件,然后邮件发送。 基本上就是两条命令:git format-patchgit send-email

下面的链接就是 Git 社区关于我这个提交的讨论。Junio已经确认这个提交是 2.8.0 的一个 regression,相信会合入2.8.0的发布版。

View Comments
2015-12-23

做一个有品位的程序员

——“能够写出漂亮代码的程序员就是有品味的程序员么?”

——“还不够。品味来自于每一个细节,有品位的程序员会把每一次提交做小、做对、做好,尽量做到整个开发的过程的无可挑剔,这样才够逼格,才可以称为有品位。”

熟练使用 Git,会让程序员更有品味。

提交做小

写小提交就是将问题解耦:“Do one thing and do it well”。开源项目的提交通常都很小,每个提交只修改一个到几个文件,每次只修改几行到几十行。 找一个你熟悉的开源项目,在仓库中执行下面的命令,可以很容易地统计出来每个提交的修改量。

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

而我看到的很多公司内外的项目则不是这样,动辄成百上千的文件修改,一次改动成千上万行源代码。试问这样的提交能拿来给人看么?

可是在开发过程中,程序员一旦进入状态,往往才思如泉涌,写出一个大提交。比如我又一次向 Git 贡献代码时, 提交还不算太大,就被 Git 的维护者 Junio 吐槽,要我拆分提交,便于评审:

TODO

那么如何将提交拆分为若干个小提交呢?

拆分当前提交(松耦合)

先以拆分最新的提交为例,可以如下操作:

  1. 将当前提交撤销,重置到上一次提交。撤销提交的改动保留在工作区中。

     $ git reset HEAD^
    
  2. 通过补丁块拣选方式选择要提交的修改。Git 会逐一显示工作区更改,如果确认此处改动要提交,输入“y“。

     $ git add -p
    
  3. 以撤销提交的提交说明为蓝本,撰写新的提交。

     $ git commit -e -C HEAD@{1}
    
  4. 对于工作区其余的修改进行提交,完成一个提交拆分为两个的操作。

     $ git add -u
     $ git commit
    

拆分当前提交(紧耦合)

如果要拆分的提交,不同的实现逻辑耦合在一起,难以通过补丁块拣选(git add -p)的方式修改提交,怎么办?这时可以 直接编辑文件,删除要剥离出此次提交的修改,然后执行:

$ git commit --amend

然后执行下面的命令,还原原有的文件修改,然后再提交。如下:

$ git checkout HEAD@{1} -- .
$ git commit

同样完成了一个提交拆分为两个提交的操作。

拆分历史某个提交

如果要拆分的是历史提交(如提交 54321),而非当前提交,则可以执行交互式变基(git rebase -i),如下:

$ git rebase -i 54321^

Git 会自动将参与变基的提交写在一个动作文件中,还会自动打开编辑器(比如 vi 编辑器)。在编辑器中显示内容示例如下:

pick 54321 要拆分的提交
pick ...   其他参与变基的提交

将要拆分的提交 54321 前面的关键字 pick 修改为 edit,保存并退出。变基操作随即开始执行。

首先会在提交 54321 处停下来,这时要拆分的提交成为了当前提交,参照前面“拆分当前提交”的方法对提交 54321 进行拆分。拆分结束再执行 git rebase --continue 完成整个变基操作。

提交做对

“好的文章不是写出来的,而是改出来的。” 代码提交也是如此。

程序员写完代码,往往迫不及待地敲下:git commit,然后发现提交中少了一个文件,或者提交了多余的文件,或者发现提交中包含错误无法编译,或者提交说明中出现了错别字。

Git 提供了一个修改当前提交的快捷命令:git commit --amend,相信很多人都用过,不再赘述。

如果你发现错误出现在上一个提交或其他历史提交中怎么办呢?我有一个小窍门,在《Git权威指南》里我没有写到哦。

比如发现历史提交 54321 中包含错误,直接在当前工作区中针对这个错误进行修改,然后执行下面命令。

git commit --fixup 54321

你会发现使用了 --fixup 参数的提交命令,不再询问你提交说明怎么写,而是直接把错误提交 54321 的提交说明的第一行拿来,在前面增加一个前缀“fixup!”,如下:

fixup! 原提交说明

如果一次没有改对,还可以再接着改,甚至你还可以针对这个修正提交进行 fixup,产生如下格式的提交说明:

fixup! fixup! 原提交说明

当开发工作完成后,待推送/评审的提交中出现大量的包含“fixup!”前缀的提交该如何处理呢?

如果你执行过一次下面的命令,即针对错误提交 54321 及其后面所有提交执行交互式变基(注意其中的 --autosquash 参数),你就会惊叹 Git 设计的是这么巧妙:

$ git rebase -i --autosquash 54321^

交互式变基弹出的编辑器内自动对提交进行排序,将提交 54321 连同它的所有修正提交压缩为一个提交。

对于“提交做对”,很多开源项目还通过单元测试用例提供保障。对于这样的项目,在提交代码时往往要求提供相应的测试用例。 Git 项目本身就对测试用例有着很高的要求,其测试框架也非常有意思。我曾经针对Git的单元测试框架写过博客,参见: 复用 git.git 测试框架

提交做好

仅仅做到提交做小、提交做对,往往还不够,还要通过撰写详细的提交说明让评审者信服,这样才能够让提交尽快通过评审合入项目仓库中。

例如今年7月份在华为公司内部的 Git 服务器上发现一个异常,最终将问题定位到 Git 工具本身。整个代码改动只有区区一行:

你能猜到提交说明写了多少么?写了20多行!

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>

Git 对于提交说明的格式有着如下约定俗成的规定:

  • 提交主题

    提交说明第一行是提交主题,是整个提交的概要性描述。可以在提交主题中添加所更改的模块名称作为前缀(如:receive-pack:)。 提交主题(即提交说明的第一行)尽量保持在50字节以内(Gerrit 的commit_log检查插件似乎有着稍微宽泛一些的要求)。 这是因为对于像 Linux、Git 这样的开源项目,是以邮件列表作为代码评审的平台,提交主题要作为邮件的标题,而邮件标题本身有长度上的限制。

  • 提交主题后的空行

    必须要在提交说明的第一行和后续的提交说明中间留一个空行!如果没有这个空行,很多 Git 客户端会将连续几行的提交说明合在一起作为提交描述。这样显然太糟了。

  • 提交说明主体

    提交主题之外的提交说明也有长度的限制,最好以72字节为限,超过则断行。因为 GitHub 在显示提交说明时支持 Markdown 语法, 所以作为一个有品位的程序员学些 Markdown 的语法,让你的提交说明的可读性变得更强吧。

    我总结过一个 Markdown 和其他文本标记语言的语法说明,可供参考:

  • 签名区

    在提交说明最后是签名区。签名区可以看出这个提交的参与者、评审记录等等。

最后,让我们一起学习成为一名有品位的程序员吧。并依靠你对代码的品味,高质量严要求,守护你的项目吧。

View Comments
2014-04-24

使用 git-svn 和 git-filter-branch 整理 SVN 版本库

SVN 本身提供了如下版本库整理工具:

  • svnadmin dump
  • svndumpfilter include
  • svndumpfilter exclude
  • svnadmin load

其中 svnadmin dump 将整个版本库或部分提交导出为一个导出文件; svndumpfilter 基于配置项的路径(SVN 1.7的 svndumpfilter 还支持通配符路径)对导出文件进行过滤, 过滤结果保存为新的导出文件; svnadmin load 将导出文件导入到另外的版本库中, 导入过程有两个选择——维持路径不变,或导入到某个路径之下。

相对于Git提供的用于整理提交的 git filter-branch 命令,SVN的版本库整理工具能做的实在不多。 而且SVN的相关工具容错性太差,操作过程经常被中断,可谓步步惊心。

最近遇到的一个案例,需要将两个 SVN 版本库(bar 和 baz)的全部历史导入到另外一个 SVN 版本库(foo)中。 并要求版本库 bar 和 baz 的目录结构统一采用 foo 中规定的目录结构。面对要导入的近 20GB 数据(绝大部分是Word、Excel、PDF文档), 决定采用Git提供的工具集进行SVN版本库整理。整理过程和过程中开发的脚本记录如下。

将 bar 和 baz 版本库转换为本地Git库

以 bar 为例,将两个版本库(bar 和 baz)转换为本地的 Git 版本库,以便使用强大的 git filter-branch 命令对提交逐一进行修改(如修改版本库中的文件路径)。

$ git init git/bar
$ cd git/bar
$ git svn init --no-metadata file:///path/to/svn/bar
$ git svn fetch

说明:

  • SVN 版本库 bar 位于本机的路径 /path/to/svn/bar 下。
  • 导出的 Git 版本库位于 git/bar 目录下。
  • 因为版本库 bar 并未使用分支(未采用 trunk、branches、tags目录结构),因此执行 git svn 时并未使用 -s 等参数。

源版本库中文件名过长的问题

Windows和Linux下文件名长度限制不同,前者255个Unicode字符,后者为255个字节。 在此次转换中就遇到 bar 版本库中存在若干文件名超长的文件,导致无法在 Linux 平台上检出。 为避免后续操作中出现错误,对其进行重命名。

首先创建一个脚本 rename.sh,该脚本将提供给 git filter-branch 命令对版本库中超长文件名进行重命名操作。

#!/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"

然后执行下面命令对版本库整理:

$ cd git/bar
$ git filter-branch --index-filter 'sh /path/to/rename.sh'

删除空白提交

从SVN转换的Git版本库可能存在空白提交,例如一些仅修改了SVN属性的提交不被 git-svn 支持,转换成了空提交。 这些空提交会对后续操作造成干扰,执行如下命令删除空白提交:

$ 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

向Git日志中添加MetaData

执行 git log 操作可以看到转换后的提交保持了原有SVN提交的用户名和提交时间,还记录了对应SVN的提交编号信息。 但是后续操作(git svn dcommit)会改变Git提交,破坏其中包含的原有SVN提交的提交者和提交时间, 因此需要用其他方法将这些信息记录下来,以便补救。

使用 git filter-branch--msg-filter 过滤器逐一向提交插入原有SVN的提交者和提交时间的元信息。

$ cd git/bar
$ git filter-branch -f --msg-filter '
  cat &&
  echo "From: REPO-NAME, author: $GIT_AUTHOR_NAME, date: $GIT_AUTHOR_DATE"' HEAD

根据需要对版本库目录重新组织

git filter-branch 至少有两个过滤器可以对提交中的目录和文件进行组织。一个是 --tree-filter , 一个是 --index-filter 。前者的过滤器脚本写起来简单,但执行起来较后者慢至少一个数量级。

根据路径转换的需求,编写过滤器脚本,如脚本 transform.sh

#!/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"

然后执行如下命令对提交进行逐一过滤,将老的目录结构转换为新的目录结构:

$ cd git/bar
$ git filter-branch --index-filter 'sh /path/to/transform.sh'

用git-svn克隆目标版本库(foo)

执行如下命令将导入的目标版本库转换为本地的 Git 版本库,如下:

$ git init git/foo
$ cd git/foo
$ git svn init --no-metadata file:///path/to/svn/foo
$ git svn fetch

然后将 bar 整理好的分支变基到当前的 master 分支上:

$ cd git/foo
$ git fetch ../../git/bar
$ git branch bar/master FETCH_HEAD
$ git co bar/master
$ git rebase -k --onto master --root

说明:

  • 使用 -k 参数,执行效率更高,因为会直接调用 cherry-pick 进行变基,而不需要执行 git format-patch 命令将提交预先转换为补丁文件。

在执行过程中遇到冲突中断的情况,这时需要解决冲突后执行:

$ git cherry-pick --continue

然后执行如下命令将不在SVN版本库中的Git提交提交到SVN版本库 foo 中。

$ git svn dcommit --rmdir 

说明:

  • 使用 --rmdir 命令是为了避免在 SVN 版本库中残留由于目录移动产生的空目录。
  • 使用 git svn dcommit 在SVN版本库中创建的新提交,其提交者是当前登录用户,提交时间是当前时间。 即新的SVN提交丢失了原有SVN提交的用户名和时间信息。马上利用之前在提交说明中添加的元信息进行补救。

修正提交时间和提交者

编写如下脚本 parse-git-log.rb,读取Git日志对元信息进行处理。

#!/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

然后执行如下命令,读取Git日志,将Git提交中的元信息转换为修正 SVN 提交历史的命令脚本 fix-svn-log.sh

$ cd git/foo
$ git log | ruby parse-git-log.rb file:///path/to/svn/foo > fix-svn-log.sh

然后执行如下命令修改 SVN 的属性,还原原有SVN的提交用户和提交实现信息:

$ sh fix-svn-log.sh

因为此操作实际上执行 svn ps --revprop 命令,需要SVN版本库 foo 中创建一个可执行的 pre-revprop-change 钩子脚本。

至此版本库转换完毕。怎么样 git filter-branch 命令够强大吧。

View Comments

更多博客

复用 git.git 测试框架 2013-10-26 Comments
墙不住的Git官网 2013-03-04 Comments
Git测试问卷完整版 2012-08-14 Comments
Topgit 本地和远程分支的删除同步以及 git fetch --prune 分析 2012-07-05 Comments
Why Git is better than SVN 2012-04-12 Comments
New hack for topgit: git-merge--no-edit 2012-03-29 Comments
Git测验(A卷) 2012-03-19 Comments
Git中文本地化 2012-02-28 Comments
Gitolite 管理员自定义命令 2011-11-30 Comments
Gitolite 通配符版本库自定义授权 2011-11-30 Comments
Gitolite 版本库镜像 2011-11-30 Comments
Gitolite 客户端发起安装模式被取消 2011-11-30 Comments
Git权威指南 Gitolite 章节更新 2011-11-30 Comments
用 Git 维护博客?酷! 2011-11-29 Comments
Gist数据嵌入博客 2011-09-14 Comments
GitHub新书通告及邀请您关注微博账号 2011-08-29 Comments
版本库中一个大家都要改的文件,又不想每次提交而覆盖,怎么办? 2011-06-10 Comments
Git版本库同步对部分版本库禁用 2011-06-02 Comments
《Git权威指南》官方网站上线 2011-05-20 Comments
apt-cacher-ng: 万能软件包代理 2011-04-20 Comments
Repo 新增 hack:URL 自动 DotGit 后缀控制等 2011-04-19 Comments
给RPM软件包签名 2011-04-12 Comments
Linux下的通用打开命令 2011-04-12 Comments
搭建本地YUM软件仓库 2011-04-12 Comments
用 repo 管理 Freemind 代码补丁 2011-04-08 Comments
开微博了 2011-04-08 Comments
RPM打包step by step(2) 2011-04-08 Comments
RPM打包step by step(1) 2011-04-02 Comments
搭建本地pypi服务器 2011-03-14 Comments
Topgit 安装 2011-03-10 Comments
《GotGit》附录D Git 和 Hg 面对面 2011-03-10 Comments
《GotGit》附录C Git 和 SVN 面对面 2011-03-10 Comments
《GotGit》附录B Git 与 CVS 面对面 2011-03-10 Comments
《GotGit》附录A Git 命令索引 2011-03-10 Comments
使用Buildout构建Python 2011-02-25 Comments
《Got Git》完稿 2011-02-24 Comments
整合Plone和Apache 2011-01-21 Comments
管理Plone的内容 2011-01-14 Comments
ZMI中使用portal_workflow管理工作流 2011-01-07 Comments
使用Plone来管理用户组 2010-12-31 Comments
Windows下Git的安装和配置 2010-12-31 Comments
设置启动Plone策略支持 2010-12-31 Comments
Plone的工作流 2010-12-31 Comments
回到未来 (3) 2010-12-28 Comments
回到未来 (2) 2010-12-28 Comments
回到未来 (1) 2010-12-28 Comments
Plone中管理用户和权限 2010-12-27 Comments
Plone产品实例 2010-12-17 Comments
Plone安装笔记 2010-12-13 Comments
buildout使用小例 2010-12-10 Comments
Vim 复制粘贴探秘 2010-12-08 Comments
python egg学习笔记 2010-12-08 Comments
Zope Dev Guide中产品例子学习 2010-12-03 Comments
Git 工作区、暂存区和版本库 2010-11-30 Comments
创建Zope产品例子 2010-11-26 Comments
补丁中的二进制文件 2010-11-24 Comments
Ubuntu10.10安装Zope小记 2010-11-19 Comments
Redmine 和 subversion 版本库整合的问题 2010-11-17 Comments
群英汇邮件列表更新 2010-11-12 Comments
Gerrit 代码审核服务器的工作流和原理 2010-11-10 Comments
Redmine 和 Subversion主备模式的整合 2010-11-08 Comments
Git 和 SVN 协同模型 2010-11-04 Comments
Redmine 中的 Subversion 版本库设置 2010-11-01 Comments
Git 的子树合并和子树拆分 2010-10-31 Comments
Topgit 原理及安装 2010-10-28 Comments
脱离 Gerrit 审核服务器,使用 repo 2010-10-25 Comments
看到了 Twitter 上鲁宾对乔布斯简短的回复 2010-10-20 Comments
关于限制低版本 Subversion 写操作问题的回复 2010-10-20 Comments
Android 代码工作区转换为 Android 的Git库镜像 2010-10-15 Comments
关于 Topgit 用法的回复 2010-10-11 Comments
Subversion 镜像写代理的配置注意事项 2010-09-25 Comments
Subversion 管理后台升级 2010-09-25 Comments
群英汇博客的里程碑 2010-09-21 Comments
Redmine 邮件发送问题的诊断 2010-09-17 Comments
用 Gitolite 搭建 Git 服务器 2010-09-15 Comments
关于 pySvnManager 的回复 2010-09-15 Comments
我给中央领导留言 2010-09-13 Comments
UNetbootin 让Linux安装更简单 2010-09-09 Comments
Android repo 魔法 2010-08-31 Comments
Repo——另一个Git协同模型 2010-08-31 Comments
Subversion管理后台增加对SVN容灾的支持 2010-08-29 Comments
维基升级——搜索功能改进 2010-08-29 Comments
Redmine 计划任务增加“未来”的选项 2010-08-29 Comments
Gistore 备份回滚改用分支实现 2010-08-21 Comments
爱上Git——《Git培训讲义》摘录 2010-08-17 Comments
如何同步 Gistore 的备份数据? 2010-08-14 Comments
晒晒我的计划任务 2010-08-10 Comments
Redmine任务日程安排(类似Mylyn)的功能 2010-08-09 Comments
Redmine与Mylyn的整合 2010-08-09 Comments
pySvnManager 0.5 升级指引 2010-08-09 Comments
pySvnManager 新功能:LDAP用户同步 2010-08-08 Comments
Xapian的检索 2010-08-08 Comments
解决 gistore 备份数据中的 git 库(submodule)的备份 2010-08-03 Comments
xapian索引的term处理 2010-07-31 Comments
nVidia 显卡在 Debian sarge 最新 linux 内核中的驱动 2010-07-30 Comments
Gistore(基于 git 的数据备份软件)升级至 0.2.x 2010-07-29 Comments
reST 输出文档中页眉和页脚的定制 2010-07-28 Comments
rst2pdf图片处理(续) 2010-07-26 Comments
群英汇redmine增强版ossxp-3.0成功上线 2010-07-20 Comments
Git 服务器软件 gitosis 的改进 2010-07-20 Comments
Redmine关于敏捷Scrum的插件 2010-07-19 Comments
为 TestLink 增加从 LDAP 同步用户的功能 2010-07-19 Comments
rst2pdf中图片的处理 2010-07-19 Comments
如果绿坝开源怎么样 2010-07-13 Comments
Debian 文件偷换 2010-07-09 Comments
在 reST 格式文档中,嵌入 Creative Commons 授权信息 2010-07-09 Comments
小心:谷歌 gmail 邮件过滤器 2010-07-09 Comments
redmine版本库统计 SVG 柱状图在IE中不能显示 2010-07-02 Comments
ruby中的代码块(Code Blokcs) 2010-07-01 Comments
安全 FTP 协议 FTPS 和防火墙 2010-06-30 Comments
ruby中的实例方法、类方法、单体方法、私有方法、protected方法 2010-06-29 Comments
LDAP 作为 FTP 认证源 2010-06-29 Comments
用 greylist 做邮件服务器的保护罩 2010-06-28 Comments
logcheck, fail2ban 的副作用 2010-06-28 Comments
“当时我就震惊了”——六月北京 OpenParty 2010-06-20 Comments
topgit 分支的图形化显示 2010-06-18 Comments
性能测试工具小黑马—JMeter+Badboy 2010-06-13 Comments
如何用apache+mongrel部署Rails应用 2010-06-13 Comments
pySvnManager 升级 2010-06-11 Comments
如何用nginx+mongrel部署Rails应用 2010-06-09 Comments
如何用nginx+passenger署Rails 2010-06-09 Comments
redmine 用户导入插件 2010-06-08 Comments
redmine 问题导入插件 2010-06-08 Comments
redmine同步变更集属性插件 2010-06-07 Comments
联通有点玩过火了 2010-06-06 Comments
改变 Nutch 对 robots.txt 的解析实现 2010-06-03 Comments
786个号中取203个,5连号的概率是多少? 2010-06-03 Comments
Python setuptools hack: get revision from git-svn 2010-06-02 Comments
Pylons nightmare ends? 2010-06-01 Comments
单点登录和 OpenID 2010-05-31 Comments
Subversion 用户眼中的 Git (13): Git 成为 SVN 的伙伴? 2010-05-30 Comments
Subversion 用户眼中的 Git (12): Git 有属性么? 2010-05-30 Comments
群英汇 Mailman 人性化设计(6): 存档安全性 2010-05-30 Comments
群英汇 Mailman 人性化设计(5): 创建新列表 2010-05-29 Comments
群英汇 Mailman 人性化设计(4): 列表订阅策略 2010-05-27 Comments
我在 OpenParty 上的报告 2010-05-24 Comments
TortoiseSVN自动获取redmine问题列表插件—TortoiseSVNRedmineIssuesPlugin 2010-05-20 Comments
Subversion 用户眼中的 Git (11): Git 授权没有 SVN 那样精细 2010-05-17 Comments
Check — 强大的c语言单元测试框架 2010-05-17 Comments
北京五月柳燕隙阳 2010-05-17 Comments
群英汇 Mailman 人性化设计(3): 列表订阅的人性化设计 2010-05-17 Comments
国内难觅稳定的 Debian 源 2010-05-11 Comments
群英汇 Mailman 人性化设计(2): 管理员认证方式和管理员面板的变化 2010-05-09 Comments
Subversion 用户眼中的 Git (10): Git 命令行的人性化设计 2010-05-09 Comments
从 CoSign 看开源软件本地化(7) 2010-05-09 Comments
NotesForLightBox灯箱控件示例 2010-05-08 Comments
12种常见的lightbox灯箱效果脚本 2010-05-08 Comments
Subversion 用户眼中的 Git (9): 单亲 VS 多亲 2010-05-01 Comments
群英汇 Mailman 人性化设计(1): 列表一览页增加登录和已订阅列表加亮 2010-05-01 Comments
从 CoSign 看开源软件本地化(6) 2010-05-01 Comments
SVN 树冲突和目录丢失问题(4) 2010-04-23 Comments
SVN 树冲突和目录丢失问题(3) 2010-04-23 Comments
SVN 树冲突和目录丢失问题(2) 2010-04-22 Comments
SVN 树冲突和目录丢失问题(1) 2010-04-22 Comments
Windows下安装Redmine的常见问题 2010-04-20 Comments
Windows 下Redmine-0.9.x的安装 2010-04-20 Comments
Ubuntu:不要迷恋哥,哥也只是一个传说 2010-04-15 Comments
从 CoSign 看开源软件本地化(5) 2010-04-15 Comments
Apt 级联缓存以及 apt-cacher-ng 的一个 Bug 2010-04-13 Comments
从 CoSign 看开源软件本地化(4) 2010-04-13 Comments
群英汇redmine增强版ossxp-2.0成功上线 2010-04-11 Comments
从 CoSign 看开源软件本地化(3) 2010-04-11 Comments
Redmine中文参考手册 2010-04-09 Comments
禁用 SSH 远程主机的公钥检查 2010-04-08 Comments
从 CoSign 看开源软件本地化(2) 2010-04-08 Comments
从 CoSign 看开源软件本地化(1) 2010-04-07 Comments
Fail2ban—-暴力口令破解的克星 2010-04-07 Comments
autotools系列工具—-自动生成Makefile 2010-04-07 Comments
单点登录认证系统升级 2010-04-07 Comments
Linux下C语言的调试 2010-04-05 Comments
提高Nutch局域网抓取的速度 2010-03-31 Comments
如何让 jQuery 和 prototype 共存 2010-03-28 Comments
Firebug—-javascript调试利器 2010-03-26 Comments
Redmine与TestLink的整合 2010-03-25 Comments
PHP中文乱码的处理 2010-03-20 Comments
Nutch 深度的测试 2010-03-19 Comments
群英汇redmine增强版ossxp-2.0已经冻结 2010-03-17 Comments
单点登录架构升级手册 2010-03-16 Comments
CoSign 3.x 介绍及与 CoSign 2.x 的协议比较 2010-03-16 Comments
CoSign 2.x 协议介绍 2010-03-16 Comments
单点登录版本升级:CoSign 3.x 更安全 2010-03-16 Comments
面向 PHP 5.3 友好的 PHP 开发 2010-03-14 Comments
TopGit的使用技巧 (3) 2010-03-10 Comments
TopGit的使用技巧 (2) 2010-03-10 Comments
TopGit的使用技巧 (1) 2010-03-10 Comments
群英汇部分应用的 /etc/init.d/ 下脚本名称改变 2010-03-09 Comments
Apache 性能调校 2010-03-09 Comments
软RAID 提供低成本和高可靠性的 Linux 服务器 2010-03-09 Comments
Linux 应用程序失去输入焦点问题的解决 2010-03-09 Comments
robots.txt 文件的非标准扩展 2010-03-09 Comments
Debian/Linux下Redmine的安装步骤 2010-03-09 Comments
关于机器人 /robots.txt 文件的常识 2010-03-08 Comments
Nutch的安装与配置 2010-03-05 Comments
Rails与Sphinx的整合 2010-03-04 Comments
Debian/Linux下Sphinx-for-chinese (中文全文搜索)的安装 2010-03-04 Comments
敏捷的MVC Web框架 Rails 2010-03-04 Comments
用 Rails 2.x.x 和 MySQL 搭建一个Web项目的步骤 2010-03-04 Comments
Debian/Linux上的txt文本文件拷到Mp3后变成乱码 2010-03-01 Comments
Debian 中的电子书 2010-02-26 Comments
Debian/Linux下sphinx的安装 2010-02-26 Comments
网站维护通知 2010-02-24 Comments
Python的易混地带 2010-02-23 Comments
Debian启动中的错误信息源自删除软件包遗留的配置文件,哦哈 2010-02-22 Comments
Python技巧篇(2):工具模块 2010-02-22 Comments
Debian 版本升级/降级 2010-02-22 Comments
Python技巧篇(1):内置对象及函数 2010-02-22 Comments
Subversion 用户眼中的 Git (8): SVN没有后悔药,git有好多 2010-02-22 Comments
Python的特点 2010-02-21 Comments
Ruby的特点 2010-02-21 Comments
Ruby的优点 2010-02-21 Comments
Python的优点 2010-02-21 Comments
Subversion 用户眼中的 Git (7): 完全不同的分支和里程碑的实现 2010-02-21 Comments
将博客整合到维基 2010-02-09 Comments
Subversion 用户眼中的 Git (6): stage 2010-02-09 Comments
jQuery 跨域 AJAX 2010-02-07 Comments
版本库整理的内存溢出问题 2010-02-04 Comments
用过滤器解决getRemoteUser()为的null的问题 2010-02-04 Comments
jquery和php整合实例 2010-02-04 Comments
解决getRemoteUser()为null的问题 2010-02-02 Comments
预告:新网站 ossxp.net 筹备中… 2010-02-01 Comments
Subversion 用户眼中的 Git (5): 没有部分检出 2010-02-01 Comments
Subversion 版本库整理实战 2010-01-27 Comments
删除 git submodule (git 库子模组) 2010-01-26 Comments
TestLink简明配置手册 2010-01-25 Comments
Subversion 用户眼中的 Git (4): 全局版本号和全球版本号 2010-01-25 Comments
Gistore 项目正式发布 —— 基于 Git 的 Linux 数据备份系统 2010-01-23 Comments
剥离CruiseControl dashboard控制台到Debian安装的tomcat6上 2010-01-23 Comments
Dashboard不能运行在Debian包安装的Tomcat6上? 2010-01-23 Comments
群英汇 TopGit 改进 (5): tg summary 执行的更快 2010-01-23 Comments
redmine 配置LDAP认证 2010-01-22 Comments
redmine 邮件服务的配置 2010-01-22 Comments
CruiseControl HgVersionParser: no match of 2010-01-22 Comments
群英汇 TopGit 改进 (4): tg 命令补齐 2010-01-22 Comments
维基中 Include 宏的用法 2010-01-21 Comments
用Debian/Ubuntu提供的软件包整合apache2和tomcat6 2010-01-20 Comments
Linux下Apache与Tomcat的整合 2010-01-19 Comments
Subversion 用户眼中的 Git (3): 命令集不兼容 2010-01-19 Comments
群英汇 TopGit 改进 (3): 更灵活的 tg patch 2010-01-19 Comments
预告: Gistore 项目开发中——使用Git做数据备份 2010-01-18 Comments
群英汇 TopGit 改进 (2): tg 导出全部分支 2010-01-18 Comments
Linux 下如何同时访问多个 github 帐号 2010-01-17 Comments
群英汇 TopGit 改进 (1): tg push 全部分支 2010-01-15 Comments
Subversion 用户眼中的 Git (2): 版本库, 工作区如影随形 2010-01-15 Comments
wordpress日志评论时间错误问题 2010-01-14 Comments
Subversion 用户眼中的 Git (1): 集中式 vs 分布式 2010-01-14 Comments
如何剥离CruiseControl内置的Web控制台 2010-01-14 Comments
velocity 未列入文档的秘密 2010-01-13 Comments
Git 如何拆除核弹起爆码,以及 topgit 0.7到0.8的变迁 2010-01-13 Comments
Wordpress中文昵称问题解决方法小结 2010-01-12 Comments
CruiseControl—confilg.xml 2010-01-12 Comments
wordpress工作原理 2010-01-12 Comments
能否突破 reST 表格语法的局限 2010-01-11 Comments
“团队致胜之道”文档更新——增加持续集成和测试管理平台相关内容 2010-01-11 Comments
reST编号列表的语法注意事项 2010-01-10 Comments
Subversion 1.6 修改记录 2010-01-09 Comments
Rails多语言支持 2010-01-09 Comments
TSE 表情插件的改进:错误表情替换的解决方案 2010-01-09 Comments
localization插件实现Rails多语言支持 2010-01-09 Comments
TestLink 1.8.5的完全安装 2010-01-09 Comments
版本库转换:hg->git->svn->git 2010-01-08 Comments
群英汇版本控制系统的选择:subversion, hg, git 2010-01-07 Comments
CruiseControl—下载及安装 2010-01-07 Comments
CruiseControl —初体验 2010-01-07 Comments
博客软件的选型:typo 还是 wordpress 2010-01-06 Comments
TestLink-测试管理工具 2010-01-06 Comments
redmine-项目管理工具 2010-01-06 Comments
CoSign-SSO插件已提交到Subversion版本库及github 2010-01-06 Comments
WordPress插件编程资源列表 2010-01-06 Comments
2010年群英汇第一大事——开博 2010-01-05 Comments