diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..dc7600f --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +source 'https://rubygems.org' + +gem 'rake' +gem 'multi_json' +gem 'hashie' +gem 'awesome_print' +gem 'jekyll' +gem 'rdiscount' +gem 'RedCloth' diff --git a/Rakefile b/Rakefile index e4293ef..fa84733 100644 --- a/Rakefile +++ b/Rakefile @@ -47,3 +47,15 @@ YAML end end end + +desc "Run jekyll serve" +task :serve do + puts "Startup web service" + `jekyll serve` +end + +desc "Run jekyll build" +task :build do + puts "Start to build web pages" + `jekyll build` +end diff --git a/_config.yml b/_config.yml index 6d67c09..bb8ca6d 100644 --- a/_config.yml +++ b/_config.yml @@ -1,2 +1,2 @@ markdown: rdiscount -pygments: true +highlighter: rouge diff --git a/_layouts/post.html b/_layouts/post.html index e77b468..f56e8cc 100644 --- a/_layouts/post.html +++ b/_layouts/post.html @@ -2,7 +2,7 @@ layout: master ---
- {{page.date | date_to_string}} + {{page.date | date: "%Y-%m-%d"}}

{{page.title}}

{{ content }}
@@ -13,7 +13,7 @@

Related Posts

{% for post in site.related_posts limit:3 %} {{ post.title }} - {{ post.date | date_to_string }} + {{ post.date | date: "%Y-%m-%d"}} Comments {% endfor %} diff --git a/_posts/2013-03-04-build-git-scm-com.md b/_posts/2013-03-04-build-git-scm-com.md new file mode 100644 index 0000000..39f9abc --- /dev/null +++ b/_posts/2013-03-04-build-git-scm-com.md @@ -0,0 +1,144 @@ +--- +layout: post +title: "墙不住的Git官网" +--- + +微博中关于《Git Community Book》(Git社区书)本地化的帖子,使我想起久未造访的 [Git官网](http://git-scm.com/) , +却吃惊地发现Git官网已遭G\\F\\W认证。值此“两会”召开、古月三昷卸任之季,G\\F\\W居然再次做出如此这般非和谐之举, +不啻于继股市暴跌后又一记对“十年和谐”响亮的耳光。 + +《墙不住的Git官网》这篇文章送给那些仍在攒钱买房,尚无闲钱购置境外主机以搭建VPN、SSH,也不会使用 goagent 的码农。 + +[Git官网](http://git-scm.com/) 的维护者是 GitHub 的 Scott Chacon,他也是 +[Git社区书](https://github.com/schacon/gitbook) 的主要维护者和 [《ProGit》](https://github.com/progit/progit/) 的作者。 + +新设计的Git官网不但重新设计了Git的Logo,还将《ProGit》这本书贡献到官网,取代Git社区书成为官网上的官方教程。 +(毕竟一人维护两套书负担太重,而且不小心会被人误解为 Copy & Paste。) + +Git官网的源代码托管在GitHub,已由 [旧版网站地址](https://github.com/schacon/git-scm) 更换到新的地址: + + * + +新网站基于 Rails 构建,默认使用 sqlite 本地数据库。其中 HTML 格式的 Git 手册、ProGit电子书的源代码并不在此版本库中, +而是要执行相应的 rake 任务,从Git版本库和 ProGit版本库中获取内容、编译并保存到 sqlite 数据库中。 +下面介绍一下如何在本地搭建Git官网。 + +## 准备 + +* 克隆Git版本库(可选) + + Git官网中的Git手册直接从Git项目的本地版本库中编译,会对 Git v1.0 之后的每一正式发布版本的手册进行编译。 + + 如果本地已经克隆了Git版本库,可以跳过这一步。 + + 如果没有克隆Git版本库,先克隆一份Git版本库。 + + $ cd Your/WorkSpace/ + $ git clone git://github.com/git/git + +* 克隆Git官网版本库 + + $ cd Your/WorkSpace/ + $ git clone git://github.com/github/gitscm-next + +* 进入到克隆出的Git官网版本库 + + $ cd gitscm-next + +* 安装 Ruby 1.9.2 。 + + (在 `gitscm-next` 目录下的 `.ruby-version` 这个文件指定了 ruby 的版本。) + + $ rvm install 1.9.2 + $ rvm use 1.9.2 + +* 下载并安装依赖的 Gem 包。 + + (由文件 Gemfile 设定 gem 包依赖) + + $ bundle install + +## Rails 应用配置 + +Git新官网是一个 Rails 应用。数据库的默认配置文件 `config/database.yml` 已指定使用 sqlite3 本地数据库。 +执行如下命令即可创建该本地数据库。 + +* 初始化本地数据库。(执行数据迁移操作)。 + + $ rake db:migrate + +* 导入缺省数据 + + $ rake db:fixtures:load + +## 第一次启动应用 + +* 启动Web应用 + + $ script/rails server + ... + => Rails 3.2.11 application starting in development on http://0.0.0.0:3000 + ... + +* 文档页面 404 + + 从上面命令的输出可以看出启动的内置Web服务器运行在 3000 端口。打开 Web 浏览器,输入地址 , + 墙外的 Git官网在本地重现了。在网站中四处转转,会发现文档部分的链接( [doc/](http://localhost:3000/doc/) )催悲地404了。 + + 这是因为相关文档需要从其他版本库获取数据并编译。 + +* 退出Web应用 + + 在控制台按下 Ctrl+C 退出运行在 3000 端口的 Web 服务。 + +## 编译 Git 手册 + +阅读版本库根目录下的 ``README.md`` 文件(GitHub上项目的说明文件),可以看到编译文档的说明。 + +* 编译Git手册。 + + 如果你有耐心,可以执行下面的命令,将Git v1.0 之后的 240 多个正式发行版本的文档逐一编译(当然很多小版本并未更新文档),并保存到数据库中。 + + $ GIT_REPO=/Your/WorkSpace/git/.git rake local_index + + 你也可以只编译Git某一个版本的手册。如下: + + $ GIT_REPO=/Your/WorkSpace/git/.git REBUILD_DOC=v1.8.1 rake local_index + + (关于该 rake 命令的具体实现参见脚本: `lib/tasks/local_index.rake` ) + +* 访问编译的Git手册。 + + 再次启动Web应用,文档页面仍然显示 404 错误。编译的Git手册文档藏到哪里了呢?从 Rails 的路由文件 `config/routes.rb` 文件可以猜出Git手册页面的URL地址为: + + + + + 按 Ctrl+C 退出Web应用。 + +## 编译 ProGit 和其他文档 + +执行下面命令可以编译出其余文档,包括《ProGit》电子书。 + +* 更新Git下载链接,执行如下命令: + + $ rake downloads + +* 编译 ProGit 电子书。 + + 提供您的 GitHub 账号——将如下命令中的 your_github_username 和 your_github_password 用您的用户名及口令替换。 + 执行 rake 命令,通过 GitHub API (调用 octokit 包)远程读取 progit 版本库源码,编译电子书。 + + $ export API_USER=your_github_username + $ export API_PASS=your_github_password + $ rake remote_genbook + +至此 Git官网在本地部署完毕,运行内置 Web server: + + $ script/rails server + ... + => Rails 3.2.11 application starting in development on http://0.0.0.0:3000 + ... + +墙外的Git官网在本地重现: 。 + + diff --git a/_posts/2013-10-26-test-gistore-using-git-test-framework.md b/_posts/2013-10-26-test-gistore-using-git-test-framework.md new file mode 100644 index 0000000..59894f9 --- /dev/null +++ b/_posts/2013-10-26-test-gistore-using-git-test-framework.md @@ -0,0 +1,297 @@ +--- +layout: post +title: "复用 git.git 测试框架" +--- + +Git 项目(git.git)有着别具一格的测试框架,使用 shell 脚本开发测试用例, +写起测试用例来一点都感觉不到拖泥带水,就和在 shell 环境中手工测试一样。 +最近在重构 Gistore 项目时复用了这一 Git 项目特有的测试框架,对 Gistore +进行测试。愿这一测试框架可以被更多的项目借鉴。 + +## git.git 的测试框架 ## + +Git 项目主要采用了 C 语言,同时还包含了 Perl、Shell 等多种开发语言的项目。 +Git 项目的测试并没有采用常见的类似 JUnit 测试框架,而是采用自创的测试框架, +由 Junio Hamano 在 2005 年用 shell 脚本封装而成。在这个框架下, +写测试用例和测试套件自然也是使用 shell 脚本语言,写起测试用例来就和手工在 +shell 环境下针对命令行测试没什么两样,写测试用例的过程很是“享受”。还一个原因可能是 +shell 脚本语言几乎融入了每一个 \*nix 开发者的血液中。总之这个测试框架用起来非常顺手。 + +在 Git 项目的 `t/` 目录下存在成百上千个以 "`t<四位数字>-<测试套件名称>.sh`" +格式命名的文件。每一个 Shell 脚本文件即是一个测试套件,其中包含多个测试用例。 + +若打开这些 shell 脚本,会注意到每一个测试套件(`t<四位数字>-<套件名>.sh`)都包含相似的结构。 + + # 引入测试套件函数库 + . ./test-lib.sh + + # 定义和执行一个测试用例 + test_expect_success '<测试用例名称>' ' + <测试断言1> && + <测试断言2> && + ... + <测试断言n> + ' + + # 此处省略更多的测试用例 + test_expect_success ... + + ... + + # 声明测试套件结束,并对测试执行过程为测试套件生成的临时目录进行清理 + test_done + +这些 shell 脚本(测试套件)都可以单独运行。例如下面示例中执行的测试套件就是我为 +git-clean--interactive (交互式 git clean)写的测试套件。 + + $ 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 + +运行测试套件的输出结果(显示到标准输出的内容)是经过特别设计的。成功运行的测试用例显示为: + + ok <数字> - <测试用例名> + +而运行失败的测试用例会显示为: + + not ok <数字> - <测试用例名> + +在测试套件运行的结尾会显示如下统计信息: + + <数字>..<数字> + +这种特定的输出格式被称为 TAP (Test Anything Protocol),参见 。 + +Junio 还用 shell 脚本封装了一个测试夹具(test harness),在 `t/` 目录下,直接执行 `make` +命令即可执行全部的测试套件,并对测试结果进行统计。此外还有其他的测试夹具可供使用, +例如名为 `prove` 的命令可以多进程并发地执行测试套件,让测试过程更高效。 + + $ 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... )=== + + +## 测试 Gistore ## + +[Gistore](https://github.com/jiangxin/gistore/) 是我在2010年写的一个工具, +以 Git 作为后端存储实现对磁盘文件的备份,并作为独立的一章写到了《Git权威指南》 +一书中。 + + Gistore = Git + Store + +最近用 Ruby 语言重写了 Gistore。这是因为 Gistore 最初的设计依赖 mount 命令, +需要将备份目录挂载到临时工作区,故只能用于有限的平台上,且可能需要 root 用户权限。 +考虑到 Git 的 gitignore 语法增加了对双星号(**)通配符的支持,是不是用 +gitignore 机制实现 Gistore 更好呢?改用 Ruby 实现是因为最近几年 Ruby 用得多, +而且使用 Thor (一个实现命令行编程框架的 Ruby 包,被很多著名软件如 bundle、rails +等使用)可以更容易实现工具的命令行扩展。 + +软件重构的质量需要测试用例来保证。Ruby 虽然内置了强大的测试框架,但像 Gistore +这类大量调用外部命令的应用,采用 Git 项目的测试框架可能更理想。于是在我 Gistore +项目中重用了 Git 项目的测试框架。 + +使用该测试框架的注意事项如下: + + +### 用 && 组合多个测试断言 ### + +下面的测试用例中,因为在第二句断言(false)后面丢掉了一个 && , +导致前两个断言未对测试用例施加影响。 + + #!/bin/sh + # + + . ./test-lib.sh + + test_expect_success 'test framework assertion' ' + true && + false + true + ' + + test_done + +### 用 test_cmp 断言测试输出 ### + +该测试框架中最常用到的断言除了 shell 本身包含的 `test` 命令外,就是 `test_cmp` 断言。 +实际上 `test_cmp` 就是对 `diff` 命令的简单封装。具体的使用过程是先将预期结果写入文件 +`expect` ,测试输出写入 `actual` 文件,再用 `test_cmp` 比较 `expect` 和 `actual` 文件, +内容一致则成功,否则失败。例如下面的测试用例代码: + + 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 + ' + +### 用 test_must_fail 断言命令失败或异常 ### + +该测试框架中有两个看起来很像的方法 `test_expect_failure` 和 `test_must_fail`, +前一个函数类似于 `test_expect_success`,以命令参数的方式引入一个测试用例并进行测试。 +后一个是用于测试用例中的测试断言。 + +函数 `test_expect_failure` 通过命令行参数引入的测试用例,无论执行成功与否, +测试都不会中断。测试用例执行失败会显示: + + not ok 1 - test framework assertion # TODO known breakage + # still have 1 known breakage(s) + +测试成功会显示: + + ok 1 - test framework assertion # TODO known breakage vanished + # 1 known breakage(s) vanished; please update test(s) + +函数 `test_must_fail` 作为测试断言,用于确认一个命令会以失败结束(返回非0值)。 +例如下面测试用例用于测试对所有注册的备份任务执行备份时(即执行 `gistore commit-all` +命令时),如果有一个或多个 Gistore 备份任务的指向丢失时,其它备份任务的备份不会受到影响, +并且 `gistore commit-all` 命令运行结束后要返回非零值。 + + 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" + ' + +### 测试用例设置依赖条件按需运行 ### + +因为 Git 1.8.2 之后才为 gitignore 引入双星号(\*\*)通配符,而之前版本的 Git 并不支持, +这会导致某些测试用例结果不一致。 + +Git项目的测试框架在设计之初就考虑到了这种情况,可以通过设置依赖条件在某些情况下关闭特定测试用例的运行。 + +首先在测试框架中根据执行环境的不同,预置特定的依赖条件,例如下面的代码使得当 Git 命令的版本是 +1.8.2 或更新的版本时,预置 `GIT_CAP_WILDMATCH` 依赖条件。 + + if test $(gistore check-git-version 1.8.2) -ge 0; then + test_set_prereq GIT_CAP_WILDMATCH + fi + +然后在定义测试用例的 `test_expect_success` 的第一个参数中写入相应的依赖条件。 +例如如下的测试用例只在 Git 1.8.2 以上环境下运行。 + + + # 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 + ' + +### 进行函数级测试 ### + +Git项目的测试框架主要是进行集成测试,如果需要进行函数级测试,还需要下点功夫。 +即需要对函数进行简单的命令行封装,用命令行调用的方式对函数进行测试。 + +在 Git 项目中就用代码 "`test-path-utils.c`" 对路径处理相关函数进行封装,在测试用例 `t0060` +中调用 `test-path-utils` 进行相关测试。 + + . ./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/ ../../../ + + +在 Gistore 项目中,我也用到了类似的方法。通过隐含子命令 `check-git-version` +对 `Gistore.git_version_compare` 方法进行封装,并在测试用例 `t0020` 中进行针对性测试。 + + 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 + ' + +更多测试用例的写法,参见如下链接: + +* [Git 项目中的测试用例的说明文件](https://github.com/git/git/blob/master/t/README) +* [Git 项目中的测试用例](https://github.com/git/git/tree/master/t) +* [Gistore 项目中的测试用例](https://github.com/jiangxin/gistore/tree/master/t) + +插播小广告: + +* [学习Git,读《Git权威指南》](http://www.worldhello.net/gotgit/bookstore.html) +* [Gistore: 以Git为后端的备份解决方案](https://github.com/jiangxin/gistore) diff --git a/_posts/2014-04-24-svn-migrate-with-git-svn.md b/_posts/2014-04-24-svn-migrate-with-git-svn.md new file mode 100644 index 0000000..06177dc --- /dev/null +++ b/_posts/2014-04-24-svn-migrate-with-git-svn.md @@ -0,0 +1,238 @@ +--- +layout: post +title: "使用 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` 命令够强大吧。 diff --git a/_posts/2015-12-23-taste-of-a-programmer.md b/_posts/2015-12-23-taste-of-a-programmer.md new file mode 100644 index 0000000..68bfa31 --- /dev/null +++ b/_posts/2015-12-23-taste-of-a-programmer.md @@ -0,0 +1,177 @@ +--- +layout: post +title: "做一个有品位的程序员" +--- + +——“能够写出漂亮代码的程序员就是有品味的程序员么?” + +——“还不够。品味来自于每一个细节,有品位的程序员会把每一次提交做小、做对、做好,尽量做到整个开发的过程的无可挑剔,这样才够逼格,才可以称为有品位。” + +熟练使用 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 测试框架](http://www.worldhello.net/2013/10/26/test-gistore-using-git-test-framework.html)。 + +## 提交做好 + +仅仅做到提交做小、提交做对,往往还不够,还要通过撰写详细的提交说明让评审者信服,这样才能够让提交尽快通过评审合入项目仓库中。 + +例如今年7月份在华为公司内部的 Git 服务器上发现一个异常,最终将问题定位到 Git 工具本身。整个代码改动只有区区一行: + +* 提交:[receive-pack: crash when checking with non-exist HEAD](https://github.com/git/git/commit/b112b14d7869bf3c000abb84cd22e57dd811d031) + +你能猜到提交说明写了多少么?写了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 + Signed-off-by: Junio C Hamano + +Git 对于提交说明的格式有着如下约定俗成的规定: + +* 提交主题 + + 提交说明第一行是提交主题,是整个提交的概要性描述。可以在提交主题中添加所更改的模块名称作为前缀(如:receive-pack:)。 + 提交主题(即提交说明的第一行)尽量保持在50字节以内(Gerrit 的commit_log检查插件似乎有着稍微宽泛一些的要求)。 + 这是因为对于像 Linux、Git 这样的开源项目,是以邮件列表作为代码评审的平台,提交主题要作为邮件的标题,而邮件标题本身有长度上的限制。 + +* 提交主题后的空行 + + 必须要在提交说明的第一行和后续的提交说明中间留一个空行!如果没有这个空行,很多 Git 客户端会将连续几行的提交说明合在一起作为提交描述。这样显然太糟了。 + +* 提交说明主体 + + 提交主题之外的提交说明也有长度的限制,最好以72字节为限,超过则断行。因为 GitHub 在显示提交说明时支持 Markdown 语法, + 所以作为一个有品位的程序员学些 Markdown 的语法,让你的提交说明的可读性变得更强吧。 + + 我总结过一个 Markdown 和其他文本标记语言的语法说明,可供参考: + + - [轻量级标记语言语法参考](http://www.worldhello.net/gotgithub/appendix/markups.html) + +* 签名区 + + 在提交说明最后是签名区。签名区可以看出这个提交的参与者、评审记录等等。 + +最后,让我们一起学习成为一名有品位的程序员吧。并依靠你对代码的品味,高质量严要求,守护你的项目吧。 diff --git a/_posts/2016-02-29-git-bisect-on-git.md b/_posts/2016-02-29-git-bisect-on-git.md new file mode 100644 index 0000000..1811a9b --- /dev/null +++ b/_posts/2016-02-29-git-bisect-on-git.md @@ -0,0 +1,281 @@ +--- +layout: post +title: "二分查找捉虫记" +--- + +## 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-remote` +和 `git-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.0` 和 `v2.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 + 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 + Helped-by: Eric Sunshine + Helped-by: Elia Pinto + Signed-off-by: Knut Franke + Signed-off-by: Elia Pinto + Signed-off-by: Junio C Hamano + + :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](https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html), +发现只有在 `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 + +## 6. 贡献给上游 + +Git 项目本身是通过邮件列表参与代码贡献的,基本的操作流程是将代码转换为补丁文件,然后邮件发送。 +基本上就是两条命令:`git format-patch` 和 `git send-email`。 + +下面的链接就是 Git 社区关于我这个提交的讨论。Junio已经确认这个提交是 2.8.0 的一个 regression,相信会合入2.8.0的发布版。 + +* [2/29] [向社区提交补丁,及相关讨论的邮件存档](http://thread.gmane.org/gmane.comp.version-control.git/287843/focus=287888)。 +* [3/04] [What's cooking in git.git (Mar 2016, #02; Fri, 4)](http://article.gmane.org/gmane.comp.version-control.git/288276):该补丁处于待 review 状态,代码停留在 `pu` 分支。 + + diff --git a/_posts/2019-03-17-goconfig.md b/_posts/2019-03-17-goconfig.md new file mode 100644 index 0000000..8cc9a4b --- /dev/null +++ b/_posts/2019-03-17-goconfig.md @@ -0,0 +1,76 @@ +--- +layout: post +title: "Go 语言解析 git config" +--- + +最近做的一个 go 语言的项目需要频繁读写 git config 文件,一些看似现成的解决方案并不能满足需要: + +1. 不考虑调用外部命令 `git config`,因为在 Windows 平台性能差。 +2. 不考虑 `libgit2`,因为会给静态编译和发布带来麻烦。 + +在 github 上找到一个能够解析 git config 文件的项目 [goconfig](https://github.com/muja/goconfig),该项目的代码直接从 Git 项目的 +[git/config.c](https://github.com/git/git/blob/95ec6b1b3393eb6e26da40c565520a8db9796e9f/config.c) 移植过来,可以确保兼容性。但是这个项目只是一个半成品,因为: + +1. 不支持多值配置项。Git 的很多设置实际上是多值配置项,例如:`push.pushOption`、`remote..fetch` 等。 +2. 不支持配置文件嵌套,即不支持通过 `include.path` 指令包含其他配置文件,而这在我们要开发的应用中至关重要。 +3. 不支持配置文件继承(多级配置)。Git 在读取一个配置项时,会依次读取系统级配置(`/etc/gitconfig`)、用户全局配置(`$HOME/.gitconfig`)、仓库级配置文件(`.git/config`)、嵌套的配置文件(`include.path` 指向的配置文件)。从优先级上看,仓库级配置文件高于全局配置,更高于系统级配置。 +4. 不支持写配置文件。 + +于是派生了一个项目到 [jiangxin/goconfig](https://github.com/jiangxin/goconfig),实现了上述特性。 + +## 增加多值配置特性 + +实际上 Git 配置项,无论单值(如 `user.name`),还是多值(如 `remote..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)](https://github.com/jiangxin/goconfig/commit/9e83c3157189a458a63832c151fbd52222ddd56a)。 + +## 增加配置文件嵌套特性 + +配置文件嵌套,涉及到迭代和配置文件的合并。核心方法是 GitConfig 的 `Merge` 方法。 + +参见提交 [83a00ae (Parse include config files from include.path, 2019-03-05)](https://github.com/jiangxin/goconfig/commit/83a00ae5b8090415985162b6e3381de03532a573) 。 + +## 增加配置文件继承 + +配置文件继承和配置文件嵌套非常相似,都是 GitConfig 结构体的 Merge,只不过多了一些文件 IO,以及缓存机制。 + +参见提交 [a033f72 (Add Load() function to read git config from disk, 2019-03-04)](https://github.com/jiangxin/goconfig/commit/a033f72a584b0c66a285cb29c50028ce3bbfba11) + +## 增加配置文件写操作 + +实际上第一个版本的 GitConfig 结构体定义为 `map[string][]string`,就可以实现多值配置。为了支持将 GitConfig 结构体回写为文件,对其做了提交修补(git commit --amend)操作。 +GitConfig 的 map 索引变成了小节(section)名称。 + +为了支持写操作所做的第二个改变是为每一个值增加了一个范围(scope),这样在保存 GitConfig 到文件的时候,知道哪些配置来自于系统级(ScopeSystem)、全局级(ScopeGlobal)、 +文件嵌套(ScopeGlobal),只将配置文件中的 ScopeSelf 记录到配置文件中。 + +参见提交: + +* [784eaa8 (Mark values with scope, such as system, global, self, 2019-03-09)](https://github.com/jiangxin/goconfig/commit/784eaa8d6f2f0822d41cb7ff3e4ed8c5bd1d7820) +* [feb3e92 (String() of GitConfig is ready for saving file, 2019-03-11)](https://github.com/jiangxin/goconfig/commit/feb3e92f752625e07e3627d5a951fc500cbba0bf) +* [b826f49 (Save GitConfig to file using Save(), 2019-03-11)](https://github.com/jiangxin/goconfig/commit/b826f4926857921ac0851ae98926ddb82dd7dbcc) + +## 一个简单的 git-config 实现 + +作为一个 lib,goconfig 项目的根目录是名为 `goconfig` 的包,为了演示如何用 `goconfig` 实现一个完整的 `git config` 命令的功能,在 `cmd/gocongig` 目录下写了一个 `main` 包。 + +可以用如下命令编译安装这一 `goconfig` 示例: + + go get github.com/jiangxin/goconfig/cmd/goconfig + +示例代码参见:[cmd/goconfig/main.go](https://github.com/jiangxin/goconfig/blob/master/cmd/goconfig/main.go)。 + +欢迎使用 goconfig 并帮助改进 diff --git a/about.md b/about.md new file mode 100644 index 0000000..a8b6ec2 --- /dev/null +++ b/about.md @@ -0,0 +1,30 @@ +--- +title: 关于 +layout: master +--- + +## 关于我 + +一个从2000年开始便饱受项目拖延之苦的开发经理,一直在苦苦寻觅项目管理的真谛。2005年抛弃一切只为探寻开源软件成功的奥秘,并以此推进企业研发管理能力。 + +2010年我认准Git在版本控制领域已无可阻挡,于是把公司业务暂搁一旁,去完成我在2003年便开始的一个梦想 —— 要在书架上摆上一本自己的书。花了很长时间给书起名,我想出来的最酷的名字就是:GotGit。很遗憾出版社认为这个名字缺乏卖点,但我还是可以通过URL来纪念这个名字: [https://github.com/gotgit/gotgit/](https://github.com/gotgit/gotgit/) 是本书官网版本库,托管在 GitHub 上。 + +- 2018年10月加入[阿里巴巴](http://www.alibaba.com)。 + +-- 蒋鑫 + +## 关于群英汇 + +声明:2015年12月起,公司业务暂停。原有客户的技术支持已全权委托 [北京共致开源信息科技有限公司](http://www.embracesource.com)。 + +北京群英汇信息技术有限公司,一个专注于开源软件推广的咨询公司。 + +[群英汇](http://www.ossxp.com/) 于 2005 年创建,一个我的开源软件试验场,一个以“汇聚全球智慧,为客户创建价值”为理念,集开源项目成功经验之大成,为企业的研发管理助力。 + +群英汇的商业模式是通过对开源软件进行定制和优化实现增值,提供给客户培训、软件安装部署,以及技术支持等服务。有人诟病公司不够开源,但我们的确把源代码提供给我们服务的客户,客户不是为软件付费,而是我们提供的服务。我真的希望有一天当公司有了更加稳健的财务收入,清晰的商业路线,以及更多的资源后,能够有更多的精力重新回归到如何贡献开源社区上。 + +## 关于 WorldHello.net + +网址 WorldHello.net 是我自2002年开始维护的个人网站,2002-2006 年陆续用 [DocBook](/doc/docbook_howto/) 及 [FreeMind](/doc/freemind/freemind.mm.htm) 撰写了一些技术文章, 并在 2006 年将这些历史文章重新整理,尝试建立一个名为 [WHODO](/doc/whodo_howto/) 的项目,以期像 [The Linux Documentation Project](http://tldp.org/) 一样建立一种易于维护的开放文档平台... + +原有 WorldHello.net 文档的迁移过程参见博客: [用 Git 维护博客?酷!](/2011/11/29/jekyll-based-blog-setup.html) 。 diff --git a/about.textile b/about.textile deleted file mode 100644 index 547cd80..0000000 --- a/about.textile +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: 关于 -layout: master ---- - -h2. 关于我 - -一个从2000年开始便饱受项目拖延之苦的开发经理,一直在苦苦寻觅项目管理的真谛。2005年抛弃一切只为探寻开源软件成功的奥秘,并以此推进企业研发管理能力。 - -2010年我认准Git在版本控制领域已无可阻挡,于是把公司业务暂搁一旁,去完成我在2003年便开始的一个梦想 —— 要在书架上摆上一本自己的书。花了很长时间给书起名,我想出来的最酷的名字就是:GotGit。很遗憾出版社认为这个名字缺乏卖点,但我还是可以通过URL来纪念这个名字: "https://github.com/gotgit/gotgit/":https://github.com/gotgit/gotgit/ 是本书官网版本库,托管在 GitHub 上。 - --- 蒋鑫 - -h2. 关于群英汇 - -北京群英汇信息技术有限公司,一个专注于开源软件推广的咨询公司。网址: "http://www.ossxp.com/":http://www.ossxp.com 。 - -"群英汇":http://www.ossxp.com/ 于 2005 年创建,一个我的开源软件试验场,你可以说他的雇员少得可怜,但我更倾向于向客户说公司有着所有全世界其他商业机构都不具备的庞大的开发团队,因为他集成了众多开源产品。 - -群英汇的商业模式是通过对开源软件进行定制和优化实现增值,提供给客户培训、软件安装部署,以及技术支持等服务。有人诟病公司不够开源,但我们的确把源代码提供给我们服务的客户,客户不是为软件付费,而是我们提供的服务。我真的希望有一天当公司有了更加稳健的财务收入,清晰的商业路线,以及更多的资源后,能够有更多的精力重新回归到如何贡献开源社区上。 - -下面是中国IT行业创业所需要的最小团队配置,权当为有创业激情的年轻人泼点冷水: - -* 一个网管。 - 网站托管在国内还是国外这是一个问题。国内托管垄断价高、国际互联网众所周知的和谐、就连DNS由谁解析也是十分严肃的问题。还要应对各种换着花样的网站备案、网络审核,所以招个专职网管吧。 - -* 一个会计或出纳。 - 会计可以外包,但其他一些国税、地税、工商、银行的琐事,如:每月的报税和更新税控卡、更换税控器、更换财务章、更换发票、新政策培训、申请发票额度调整、开具发票、公司年审等,没个专人真还不行。 - -* 一个体制内的达人。 - 体制内达人能让公司获得不受封锁的网络接入,否则很多技术网站你是上不去的,谷歌也基本无法使用。 - -* 一个残疾人(可选)。 - 税种五花八门,至今搞不懂国税、地税的小数点税率为何物。还有每年一度的残保金,要是公司能够有一个能干但腿脚不灵便的员工,可以省掉不少税。 - -h2. 关于 WorldHello.net - -网址 WorldHello.net 是我自2002年开始维护的个人网站,2002-2006 年陆续用 "DocBook":/doc/docbook_howto/ 及 "FreeMind":/doc/freemind/freemind.mm.htm 撰写了一些技术文章, 并在 2006 年将这些历史文章重新整理,尝试建立一个名为 "WHODO":/doc/whodo_howto/ 的项目,以期像 "The Linux Documentation Project":http://tldp.org/ 一样建立一种易于维护的开放文档平台... - -原有 WorldHello.net 文档的迁移过程参见博客: "用 Git 维护博客?酷!":/2011/11/29/jekyll-based-blog-setup.html 。 diff --git a/blog.html b/blog.html index 20a983f..9b77689 100644 --- a/blog.html +++ b/blog.html @@ -9,7 +9,7 @@ {% for post in site.posts %} {% if forloop.index < page.full_posts %}
- {{post.date | date_to_string}} + {{post.date | date: "%Y-%m-%d"}}

{{post.title}}

{{post.content}}
View Comments @@ -21,7 +21,7 @@

更多博客

{% endif %} {{ post.title }} - {{ post.date | date_to_string }} + {{ post.date | date: "%Y-%m-%d"}} Comments {% endif %} diff --git a/index.html b/index.html index 9df2e08..cd2371d 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@

- {{post.date | date_to_string}} + {{post.date | date: "%Y-%m-%d"}}

{{post.title}}

{{post.content}}
View Comments