Skip to content

Commit 73bd232

Browse files
committed
blog: git blame on git
Signed-off-by: Jiang Xin <[email protected]>
1 parent 8a2d254 commit 73bd232

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
---
2+
layout: post
3+
title: "二分查找捉虫记"
4+
---
5+
6+
## 1. 问题现象
7+
8+
Git 2.8.0 版本即将发布,今天把本地的 Git 版本升级到 `2.8.0-rc0`,结果悲剧了。所有使用 HTTP 协议的公司内部 Git 仓库都无法正常访问!
9+
10+
在确认不是网络和公司 Git 服务器问题之后,自然怀疑到了 HTTP 代理。果然清空了 `http_proxy` 环境变量后,Git 命令工作正常了:
11+
12+
http_proxy= git ls-remote http://server.name/git/repo.git
13+
14+
然而一旦通过 `http_proxy=bad_proxy` 环境变量设置了一个错误的代理,即便通过 `no_proxy=*` 期望绕过代理,Git 2.8.0 却无法正常工作:
15+
16+
$ http_proxy=bad_proxy no_proxy=* git ls-remote http://server.name/git/repo.git
17+
fatal: unable to access 'http://server.name/git/repo.git/': Couldn't resolve proxy 'bad_proxy'
18+
19+
可是我记得升级之前 Git 工作是正常的啊!于是在 Git 2.7.0 下进行了尝试。
20+
21+
将版本切换到 `v2.7.0`,编译安装 Git。(注意一定要安装,而不是执行当前目录下的 Git。这是因为该命令执行过程中会依次调用 `git-ls-remote`
22+
`git-remote-http` 命令,而这两个命令是位于安装路径中的。)
23+
24+
$ git checkout v2.7.0
25+
$ make -j8 && make install # 我的工作站是四核CPU,故此使用 -j8 两倍并发执行编译
26+
27+
测试发现 Git 2.7.0 能够通过 `no_proxy` 变量绕过错误的 `http_proxy` 环境变量:
28+
29+
$ http_proxy=bad_proxy no_proxy=* git ls-remote http://server.name/git/repo.git
30+
206b4906c197e76fcc63d7a453f9e3aa00dfb3da HEAD
31+
206b4906c197e76fcc63d7a453f9e3aa00dfb3da refs/heads/master
32+
33+
显然一定是 `v2.7.0``v2.8.0-rc0` 中间的某个版本引入的 Bug!
34+
35+
## 2. 二分查找
36+
37+
想必大家都玩过猜数字游戏吧:一个人在1到100的数字中随意选择一个,另外一个人来猜,小孩子总是一个挨着一个地猜,
38+
懂得折半查找的大人总是获胜者。Git 提供的 git bisect 这一命令,就是采用这样的二分查找快速地在提交中定位 Bug,
39+
少则几次,多则十几次就会定位到引入Bug的提交。
40+
41+
1. 首先执行下面命令启用二分查找。
42+
43+
$ git bisect start
44+
45+
2. 标记一个好版本。下面的命令使用 tag(v2.7.0)来标记 Git 2.7.0 版本是好版本,换做40位的提交号也行。
46+
47+
$ git bisect good v2.7.0
48+
49+
3. 标记 Git 2.8.0-rc0 是一个坏版本。注意:马上就是见证奇迹的时刻。
50+
51+
$ git bisect bad v2.8.0-rc0
52+
Bisecting: 297 revisions left to test after this (roughly 8 steps)
53+
[563e38491eaee6e02643a22c9503d4f774d6c5be] Fifth batch for 2.8 cycle
54+
55+
看到了么?当完成对一个好版本和一个坏版本的标记后,Git 切换到一个中间版本(`563e384`),并告诉我们大概需要8步可以找到元凶。
56+
57+
4. 在这个版本下执行前面的测试操作:
58+
59+
$ make -j8 && make install
60+
$ git --version
61+
git version 2.7.0.297.g563e384
62+
$ http_proxy=bad_proxy no_proxy=* git ls-remote http://server.name/git/repo.git
63+
fatal: unable to access 'http://server.name/git/repo.git/': Couldn't resolve proxy 'bad_proxy'
64+
65+
5. 对这个版本进行标记。
66+
67+
这是一个坏版本:
68+
69+
$ git bisect bad
70+
Bisecting: 126 revisions left to test after this (roughly 7 steps)
71+
[e572fef9d459497de2bd719747d5625a27c9b41d] Merge branch 'ep/shell-command-substitution-style'
72+
73+
我们可以机械地重复上面4、5的步骤,直到最终定位。但是人工操作很容易出错。如果对版本标记错了,把 good 写成了 bad 或者相反,
74+
就要执行 `git bisect reset` 重来。(小窍门:git bisect log 可以显示 git bisect 标记操作日志)
75+
76+
于是决定剩下的二分查找使用脚本来完成。
77+
78+
## 3. 自动化的二分查找
79+
80+
Git 二分查找允许提供一个测试脚本,Git 会根据这个测试脚本的返回值,决定如何来标记提交:
81+
82+
* 返回值为 0:这个提交是一个好提交。
83+
* 返回值为 125:这个提交无法测试(例如编译不过去),忽略这个提交。
84+
* 返回值为 1-127(125除外):这个提交是一个坏提交。
85+
* 其他返回值:二分查找出错,终止二分查找操作。
86+
87+
那么就我们先来看看 `git ls-remote` 的返回值:
88+
89+
* 正确执行的返回值是 0:
90+
91+
$ http_proxy= git ls-remote http://server.name/git/repo.git
92+
206b4906c197e76fcc63d7a453f9e3aa00dfb3da HEAD
93+
206b4906c197e76fcc63d7a453f9e3aa00dfb3da refs/heads/master
94+
$ echo $?
95+
0
96+
97+
* 错误执行的返回值是 128!
98+
99+
$ http_proxy=bad_proxy git ls-remote http://server.name/git/repo.git
100+
fatal: unable to access 'http://server.name/git/repo.git/': Couldn't resolve proxy 'bad_proxy'
101+
$ echo $?
102+
128
103+
104+
105+
于是创建一个测试脚本 `git-proxy-bug-test.sh`,内容如下:
106+
107+
#!/bin/sh
108+
109+
make -j8 && make install && \
110+
git --version && \
111+
http_proxy=bad_proxy no_proxy=* \
112+
git ls-remote http://server.name/git/repo.git
113+
114+
case $? in
115+
0)
116+
exit 0
117+
;;
118+
128)
119+
exit 1
120+
;;
121+
*)
122+
exit 128
123+
;;
124+
esac
125+
126+
然后敲下如下命令,开始自动执行二分查找:
127+
128+
$ git bisect run sh git-proxy-bug-test.sh
129+
130+
自动化查找过程可能需要几分钟,站起来走走,休息一下眼睛。再回到座位,最终的定位结果就展现在了眼前:
131+
132+
372370f1675c2b935fb703665358dd5567641107 is the first bad commit
133+
commit 372370f1675c2b935fb703665358dd5567641107
134+
Author: Knut Franke <[email protected]>
135+
Date: Tue Jan 26 13:02:48 2016 +0000
136+
137+
http: use credential API to handle proxy authentication
138+
139+
Currently, the only way to pass proxy credentials to curl is by including them
140+
in the proxy URL. Usually, this means they will end up on disk unencrypted, one
141+
way or another (by inclusion in ~/.gitconfig, shell profile or history). Since
142+
proxy authentication often uses a domain user, credentials can be security
143+
sensitive; therefore, a safer way of passing credentials is desirable.
144+
145+
If the configured proxy contains a username but not a password, query the
146+
credential API for one. Also, make sure we approve/reject proxy credentials
147+
properly.
148+
149+
For consistency reasons, add parsing of http_proxy/https_proxy/all_proxy
150+
environment variables, which would otherwise be evaluated as a fallback by curl.
151+
Without this, we would have different semantics for git configuration and
152+
environment variables.
153+
154+
Helped-by: Junio C Hamano <[email protected]>
155+
Helped-by: Eric Sunshine <[email protected]>
156+
Helped-by: Elia Pinto <[email protected]>
157+
Signed-off-by: Knut Franke <[email protected]>
158+
Signed-off-by: Elia Pinto <[email protected]>
159+
Signed-off-by: Junio C Hamano <[email protected]>
160+
161+
:040000 040000 de69688dd93e4466c11726157bd2f93e47e67330 d19d021e8d1c2a296b521414112be0966bd9f09a M Documentation
162+
:100644 100644 f46bfc43f9e5e8073563be853744262a1bb4c5d6 dfc53c1e2554e76126459d6cb1f098facac28593 M http.c
163+
:100644 100644 4f97b60b5c8abdf5ab0610382a6d6fa289df2605 f83cfa686823728587b2a803c3e84a8cd4669220 M http.h
164+
二分查找运行成功
165+
166+
## 4. 解决问题
167+
168+
既然我们知道引入 Bug 的提交,让我们看看这个提交:
169+
170+
171+
$ git show --oneline --stat 372370f1675c2b935fb703665358dd5567641107
172+
372370f http: use credential API to handle proxy authentication
173+
Documentation/config.txt | 10 +++++--
174+
http.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++
175+
http.h | 1 +
176+
3 files changed, 85 insertions(+), 3 deletions(-)
177+
178+
179+
相比很多人一个提交动辄改动几百、几千行的代码,这个提交的改动算得上简短了。小提交的好处就是易于阅读、易于问题定位、易于回退。
180+
181+
最终参照上面定位到的问题提交,我的 Bugfix 如下(为了下面的一节叙述方便,给代码补丁增加了行号):
182+
183+
01 diff --git a/http.c b/http.c
184+
02 index 1d5e3bb..69da445 100644
185+
03 --- a/http.c
186+
04 +++ b/http.c
187+
05 @@ -70,6 +70,7 @@ static long curl_low_speed_limit = -1;
188+
06 static long curl_low_speed_time = -1;
189+
07 static int curl_ftp_no_epsv;
190+
08 static const char *curl_http_proxy;
191+
09 +static const char *curl_no_proxy;
192+
10 static const char *http_proxy_authmethod;
193+
12 static struct {
194+
13 const char *name;
195+
13 @@ -624,6 +625,11 @@ static CURL *get_curl_handle(void)
196+
15 }
197+
16
198+
17 curl_easy_setopt(result, CURLOPT_PROXY, proxy_auth.host);
199+
18 +#if LIBCURL_VERSION_NUM >= 0x071304
200+
19 + var_override(&curl_no_proxy, getenv("NO_PROXY"));
201+
20 + var_override(&curl_no_proxy, getenv("no_proxy"));
202+
21 + curl_easy_setopt(result, CURLOPT_NOPROXY, curl_no_proxy);
203+
22 +#endif
204+
23 }
205+
24 init_curl_proxy_auth(result);
206+
25
207+
26 --
208+
27 2.8.0.rc0
209+
210+
## 5. 写提交说明
211+
212+
这个提交是要贡献给 Git 上游的,评审者可能会问我如下问题:
213+
214+
1. Bug 的现象是什么?
215+
216+
“系统的 no_proxy 变量不起作用,git 可能无法访问 http 协议的仓库。”
217+
218+
2. 从什么版本引入这个 Bug?
219+
220+
“我们定位到的这个提交引入的 Bug。之所以会引入这个 Bug,是因为这个提交读取了 `http_proxy` 等环境变量,
221+
自动通过 `git-credential` 获取的信息补齐 `http_proxy` 的缺失的代理认证口令,并显示设置 libcurl 的参数。”
222+
223+
3. 之前的版本为什么没有出现这个问题?什么条件下会出现?
224+
225+
“之前的版本也会出现问题,但是只有在用户主动设置了 `http.proxy` 配置变量才会出现。
226+
用户很少会去设置 `http.proxy` 配置变量,而通常是使用 `http_proxy` 环境变量。”
227+
228+
4. 你是如何解决的?你的解决方案是否最佳?
229+
230+
“读取 `no_proxy` 环境变量,并为 `libcurl` 配置相应参数。因为 `libcurl` 只在 `7.19.4` 之后才引入 `CURLOPT_NOPROXY`,因此需要添加条件编译。”
231+
232+
实际上,前面的 Bugfix 原本是没有那个条件编译的。即补丁的第18行、22行一开始是没有的,
233+
在回答第4个问题的时候,我仔细查看了 [libcurl API](https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html)
234+
发现只有在 `7.19.4` 版本之后,才支持 `CURLOPT_NOPROXY` 参数,因此如果不添加这个编译条件,
235+
在特定的平台可能会导致 Git 无法编译通过。
236+
237+
下面就是最终的提交说明:
238+
239+
http: honor no_http env variable to bypass proxy
240+
241+
Curl and its families honor several proxy related environment variables:
242+
243+
* http_proxy and https_proxy define proxy for http/https connections.
244+
* no_proxy (a comma separated hosts) defines hosts bypass the proxy.
245+
246+
This command will bypass the bad-proxy and connect to the host directly:
247+
248+
no_proxy=* https_proxy=http://bad-proxy/ \
249+
curl -sk https://google.com/
250+
251+
Before commit 372370f (http: use credential API to handle proxy auth...),
252+
Environment variable "no_proxy" will take effect if the config variable
253+
"http.proxy" is not set. So the following comamnd won't fail if not
254+
behind a firewall.
255+
256+
no_proxy=* https_proxy=http://bad-proxy/ \
257+
git ls-remote https://github.com/git/git
258+
259+
But commit 372370f not only read git config variable "http.proxy", but
260+
also read "http_proxy" and "https_proxy" environment variables, and set
261+
the curl option using:
262+
263+
curl_easy_setopt(result, CURLOPT_PROXY, proxy_auth.host);
264+
265+
This caused "no_proxy" environment variable not working any more.
266+
267+
Set extra curl option "CURLOPT_NOPROXY" will fix this.
268+
269+
Signed-off-by: Jiang Xin <[email protected]>
270+
271+
## 6. 贡献给上游
272+
273+
Git 项目本身是通过邮件列表参与代码贡献的,基本的操作流程是将代码转换为补丁文件,然后邮件发送。
274+
基本上就是两条命令:`git format-patch``git send-email`
275+
276+
下面的链接就是 Git 社区关于我这个提交的讨论。Junio已经确认这个提交是 2.8.0 的一个 regression,相信会合入2.8.0的发布版。
277+
278+
* [2/29] [向社区提交补丁,及相关讨论的邮件存档](http://thread.gmane.org/gmane.comp.version-control.git/287843/focus=287888)
279+
* [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` 分支。
280+
281+

0 commit comments

Comments
 (0)