工作中发现Git客户端在处理中文目录时有Bug,好奇心驱使下,利用业余时间,不断定位Bug的原因,并和Git客户端的维护者进行沟通,最终解决了Bug。在解决问题的过程中接触到msysgit,tcl/tk,tortoisegit等开源项目,为了定位Bug的原因,尝试编译了开源项目。在解决问题的过程中遇到了不少困难,现在总结一下解决问题的一些方法和思路,希望也能给大家一些启示。
由于在中文目录的Git仓库下,右键选择Git Gui,会提示No working directory,于是我打开cygwin终端,进入Git安装目录,使用grep命令查找”No working directory”,发现git-gui.tcl脚本使用了该字符串,引用该字符串的代码:
if {[catch {cd $_gitworktree} err]} { catch {wm withdraw .} error_popup [strcat [mc "No working directory"] " $_gitworktree:\n\n$err"] exit 1 }
说明进入$_gitworktree目录有问题,从对话框来看是$_gitworktree被解析错误了,最后导致出现乱码了。看到error_popup可弹出对话框,于是决定用这个函数来调试,先追踪_gitworktree变量的定义。
转载自http://ju.outofmemory.cn/entry/143895
最后追踪到设置该变量的代码,如下所示:
if { [is_Cygwin] } { catch {set _gitworktree [exec cygpath --windows [git rev-parse --show-toplevel]]} } else { set _gitworktree [git rev-parse --show-toplevel] }
也就是说_gitworktree其实是命令git rev-parse –show-toplevel的输出结果,开始一直不知道git命令如何运行的,后来学习了tcl/tk脚本的一些基础知识,猜测这里的git只是一个自定义函数而已,于是查找proc git,找到git函数的定义,如下所示:
proc git {args} { set opt [list] while {1} { switch -- [lindex $args 0] { --nice { _lappend_nice opt } default { break } } set args [lrange $args 1 end] } set cmdp [_git_cmd [lindex $args 0]] set args [lrange $args 1 end] _trace_exec [concat $opt $cmdp $args] set result [eval exec $opt $cmdp $args] if {[encoding system] != "utf-8"} { set result [encoding convertfrom utf-8 [encoding convertto $result]] } if {$::_trace} { puts stderr "< $result" } return $result }
发现当系统编码不是utf-8时,会执行以下脚本将字符串进行转换
set result [encoding convertfrom utf-8 [encoding convertto $result]]
猜测这里的转换有问题,于是改写了这个函数,当执行git rev-parse –show-toplevel命令时,弹出对话框看这里转换前的内容和转换后的内容。但是这样并没发现问题的内容,因为看到的字符串要么是乱码,要么是无用的信息,后来想最好以十六进制形式看字符串的内容比较好,但是以对话框的形式显示字符串并不好展示十六进制形式。
后来注意到$::_trace变量,于是想到好程序即使发布后,也应该有调试的方法,于是查找$::_trace变量的引用,发现果然有办法调试git_gui.tcl脚本,只需在git bash下运行下述命令即可调试:
export PATH=$PATH:"C:\Program Files (x86)\Git\libexec\git-core" wish "C:\Program Files (x86)\Git\libexec\git-core\git-gui.tcl" --trace
调试时可看到有一个控制台,用puts输出的字符串会打印到控制台,于是又有了一种新的定位问题的方法。后来又查找tcl的api,发现有将字符串转换成十六进制形式的api,不过该api的使用比较复杂,可通过下面的方式来打印字符串的十六进制形式:
puts [strcat result1 hex : [binary encode hex $result1] ]
不过这又出问题了,binary encode hex只tcl8.6才有,而git for windows带的tcl引擎是tcl8.5,因此不能用,于是只好编译tcl8.6,放到git for windows的安装目录。但是编译tcl8.6生成的是tclsh86t,而不是wish,故此不好执行git-gui.tcl脚本,还是不方便。
后来又想了一个另一个办法,编写一个简单的tcl测试脚本,只执行git rev-parse –show-top-level命令,并进行编码转换。然后调试tcl程序即可找到问题的原因。
上节说到编写了一个tcl/tk脚本来测试,测试脚本如下所示:
#!/bin/wish proc strcat {args} { return [join $args {}] } set cmdp {"C:/Program Files (x86)/Git/libexec/git-core/git-rev-parse.exe"} set args --show-toplevel set result [eval exec $cmdp $args] puts [strcat result hex : [binary encode hex $result] ] set result1 [encoding convertto cp936 $result] puts [strcat result1 hex : [binary encode hex $result1] ]
刚开始的测试方法是修改tcl源代码,在exec, binary,encoding等模块的关键函数处打印日志,这种办法非常低效,也根本找不到问题的原因。后来才发现源代码目录有vs工程,只要编译能通过就可调试了,这样方便很多。
调试后发现执行git命令时输出的字符串其实是utf8编码的,但是执行eval exec $cmdp $args脚本时会进行编码转化,将git命令输出字符串当作cp936编码的字符串转化为unicode编码,如’技术’的utf8编码是e68a80 e69caf,但是当作cp936编码字符串进行转化如下所示:
e68a 80 e69c af => b6 20ac 93c8 00 =>
因为af在cp936编码里并不是某个字符的编码,必须和另外一个字节才能组成有效编码,故此转化时会出错。
而encoding convertto cp936 $result还原成cp936时,因为$result字符串本来就有问题了,故此也不能将字符串再还原成功了。
这样找到了Bug的真正原因。
对于Bug:中文子模块目录下已提交文件悬浮图标显示为问号图标,刚定位时还以为是Git For Windows的Bug,以为和Git gui打不开中文目录仓库是同一个问题,后来才想清楚悬浮图标的设置是由TortoiseGit实现的功能。
于是先了解了overlay icon的工作原理:
https://msdn.microsoft.com/en-us/library/windows/desktop/hh127442(v=vs.85).aspx
但是看完之后还是觉得很糊涂,只知道要实现一个dll,注册到explorer,explorer会通过COM技术调用dll,dll需实现IShellIconOverlayIdentifier接口,并实现3个函数GetOverlayInfo,GetPriority,IsMemberOf。但是tortoisegit可根据文件的状态显示不同的图标,不知道如何实现的。
于是下载了TortoiseGit的源代码,通过找关键字GetOverlayInfo,知道这部分实现是在TortoiseShell工程,TortoiseShell工程会生成TortoiseGit32.dll。
但是TortoiseShell工程的IconOverlay.cpp里有一行注释,让我晕掉了,注释如下所示:
// we don't have to set the icon file and/or the index here: // the icons are handled by the TortoiseOverlays dll.
也就是说图标的设置其实不是由TortoiseGit32.dll直接控制,而是由TortoiseOverlays.dll控制,但TortoiseOverlays并不是TortoiseGit项目的一部分,而是TortoiseSvn项目的一部分。
然后又下载了TortoiseSvn项目,看了代码还是不好弄,主要是没有一个好的调试手段,不能跟源代码,也就不知道代码的执行过程。后来看到代码里看到有会根据一个调试的标记决定是否输出调试字符串,这个标记便是注册表Software\\TortoiseGit\\DebugOutputString的值,如果为true,则可打印出调试字符串,于是设置了该标记,并下载了捕获调试字符串的工具,结果没捕获到调试字符串。还是不知道如何调试。
后来继续看TortoiseShell工程,在这个模块的入口函数DllMain里发现有判断调用该模块的程序是否是TortoiseGitExplorer.exe,如果是这个程序会设置调试标志,也就是说找到这个程序就可以很方便的调试了,结果在项目目录下查找这个关键字,根本找不到,网上搜也没找到。
没有调试的手段,根本不好定位问题,最后只好在tortoisegit项目上提交了Bug,Bug地址:
https://code.google.com/p/tortoisegit/issues/detail?id=2453Tortoisegit项目的维护者很快就修复了该Bug,系统也自动发送了邮件告知我,我把最新的代码拉下来,然后重新生成了安装包,装上后就解决了该问题。
我看了一下提交日志,发现是GitAdminDir.cpp一处代码的Bug。
如果不能调试代码确实不好定位问题。TortoiseGit的维护者还为这个问题专门添加了单测用例。TortoiseGit的维护者告诉我TortoiseGitExplorer是一个内部项目,还没有开放。
后来发现TGitCache工程会生成TGitCache.exe,并且这是一个常驻进程,explorer通过TortoiseGit32.dll获取悬浮图标时,TortoiseGit32.dll会和TGitCache通过命名管道通信,获取文件状态,然后根据文件状态设置不同悬浮图标。