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