Git 调试技巧
Git 不仅是版本控制工具,也是强大的调试工具。本章介绍如何使用 Git 快速定位问题、追踪代码变更历史。
Git Bisect:二分查找问题提交
当你发现代码出现了 bug,但不知道是哪次提交引入的时候,git bisect 是最有效的工具。它使用二分查找算法,帮你快速定位引入问题的提交。
基本原理
假设你在 100 次提交后发现了一个 bug。用二分查找,你只需要测试约 7 次(log₂100 ≈ 7)就能找到问题提交,而不是逐个检查 100 次提交。
基本用法
启动 bisect 会话
# 1. 启动 bisect
git bisect start
# 2. 标记当前有问题的提交为 bad
git bisect bad
# 3. 标记一个已知正常的提交为 good
git bisect good v1.0.0
# 或者使用提交哈希
git bisect good abc123
# Git 会自动切换到中间的某个提交
# Bisecting: 50 revisions left to test after this (roughly 6 steps)
测试并标记
现在你需要测试当前切换到的提交:
# 如果当前提交正常
git bisect good
# 如果当前提交有问题
git bisect bad
每次标记后,Git 会自动切换到新的中间点,直到找到问题提交。
完成 bisect
当找到问题提交后,Git 会显示:
abc123def456 is the first bad commit
commit abc123def456
Author: 张三 <[email protected]>
Date: Mon Jan 15 10:30:00 2024 +0800
添加新功能
src/main.js | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
退出 bisect 会话:
git bisect reset
这会回到你开始 bisect 之前的分支和提交。
简化命令
可以在一条命令中完成启动:
# 直接指定坏提交和好提交
git bisect start HEAD v1.0.0
# 或者更简洁
git bisect start HEAD~10 HEAD~20 # 在 HEAD~10(坏)和 HEAD~20(好)之间查找
自动化 Bisect
如果你有测试脚本可以判断代码是否正常,可以让 Git 自动完成整个过程:
# 启动 bisect
git bisect start HEAD v1.0.0
# 运行自动化脚本
git bisect run ./test-script.sh
测试脚本需要返回:
- 0:表示当前提交是好的(good)
- 1-124(除 125):表示当前提交是坏的(bad)
- 125:表示无法测试,跳过当前提交
自动化示例
#!/bin/bash
# test-script.sh
# 尝试编译
make || exit 125 # 编译失败,跳过这个提交
# 运行测试
./run-tests.sh # 测试脚本应返回 0(通过)或 1(失败)
使用:
git bisect start HEAD v1.0.0
git bisect run ./test-script.sh
使用自定义术语
如果查找的不是 bug,而是某个功能何时添加的,可以使用更合适的术语:
# 使用 old/new 术语
git bisect start --term-old fixed --term-new broken
git bisect broken HEAD
git bisect fixed v1.0.0
# 或者直接用自定义术语
git bisect start --term-old without-feature --term-new with-feature
跳过无法测试的提交
有些提交可能无法测试(比如编译失败),可以跳过它们:
git bisect skip
# 跳过多个提交
git bisect skip v2.0..v2.5
查看进度
在 bisect 过程中,可以随时查看日志:
# 查看已经标记的提交
git bisect log
# 如果发现标记错误,可以保存日志、编辑、然后重新播放
git bisect log > bisect-log.txt
# 编辑 bisect-log.txt 修正错误
git bisect reset
git bisect replay bisect-log.txt
可视化查看
# 在 gitk 中查看当前状态
git bisect visualize
# 或简写
git bisect view
# 使用其他参数
git bisect visualize --stat
Bisect 实战案例
场景:项目在 v2.0 时正常,现在某个功能失效了
# 1. 确认当前版本有问题
$ npm test
FAIL: 登录功能测试失败
# 2. 启动 bisect
$ git bisect start HEAD v2.0
Bisecting: 127 revisions left to test after this (roughly 7 steps)
# 3. 测试中间版本
$ npm test
PASS: 所有测试通过
$ git bisect good
Bisecting: 63 revisions left to test after this (roughly 6 steps)
# 4. 继续测试...
$ npm test
FAIL: 登录功能测试失败
$ git bisect bad
Bisecting: 31 revisions left to test after this (roughly 5 steps)
# ... 重复直到找到问题提交
# 5. Git 显示结果
abc123 is the first bad commit
# 6. 查看问题提交详情
$ git show abc123
# 7. 退出 bisect
$ git bisect reset
Git Blame:追踪代码变更
git blame 用于查看文件每一行最后是谁在什么时候修改的,帮助理解代码的来源和变更历史。
基本用法
# 查看文件的详细变更信息
git blame filename.txt
# 输出示例
abc123df (张三 2024-01-15 10:30:01 +0800 1) function greet(name) {
def456gh (李四 2024-01-16 14:20:03 +0800 2) if (!name) {
def456gh (李四 2024-01-16 14:20:03 +0800 3) return "Hello, Guest!";
abc123df (张三 2024-01-15 10:30:01 +0800 4) }
abc123df (张三 2024-01-15 10:30:01 +0800 5) return `Hello, ${name}!`;
abc123df (张三 2024-01-15 10:30:01 +0800 6) }
输出格式:提交哈希 (作者 时间 行号) 代码内容
常用选项
# 只显示作者,不显示时间和哈希
git blame -w filename.txt
# 显示邮箱而不是用户名
git blame -e filename.txt
# 显示行号范围
git blame -L 10,20 filename.txt # 第 10-20 行
git blame -L 10,+5 filename.txt # 第 10 行开始的 5 行
git blame -L :function_name filename.txt # 函数所在行
# 忽略空白字符变化
git blame -w filename.txt
# 从指定修订版本开始追踪
git blame v1.0.0 -- filename.txt
# 追踪文件重命名前的历史
git blame -C -C -C filename.txt
理解 Blame 输出
$ git blame -L 42,52 src/utils.js
^a1b2c3d (王五 2023-12-01 09:00:00 +0800 42) /**
^a1b2c3d (王五 2023-12-01 09:00:00 +0800 43) * 计算订单总价
^a1b2c3d (王五 2023-12-01 09:00:00 +0800 44) */
e4f5g6h7 (张三 2024-01-10 15:30:00 +0800 45) function calculateTotal(items) {
e4f5g6h7 (张三 2024-01-10 15:30:00 +0800 46) let total = 0;
^a1b2c3d (王五 2023-12-01 09:00:00 +0800 47) for (const item of items) {
h8i9j0k1 (李四 2024-02-20 11:45:00 +0800 48) total += item.price * item.quantity;
e4f5g6h7 (张三 2024-01-10 15:30:00 +0800 49) }
e4f5g6h7 (张三 2024-01-10 15:30:00 +0800 50) return total;
^a1b2c3d (王五 2023-12-01 09:00:00 +0800 51) }
注意 ^a1b2c3d 前面的 ^ 符号,表示这是文件的初始提交,没有父提交。
追踪特定行的历史
结合 git log 可以查看某行的完整历史:
# 查看 filename.txt 第 10-15 行的修改历史
git log -L 10,15:filename.txt
# 查看特定函数的修改历史
git log -L :functionName filename.txt
实用技巧
1. 追踪移动/复制的代码
# -C 检测文件内移动或复制的行
git blame -C filename.txt
# -C -C 检测跨文件的移动或复制
git blame -C -C filename.txt
# -C -C -C 更激进地检测(可能较慢)
git blame -C -C -C filename.txt
2. 查看谁修改了某个函数
# 查看函数 author 相关行的修改
git blame -L :author src/models.js
3. 排除某些提交
# 忽略某些提交(比如格式化提交)
git blame --ignore-rev abc123 filename.txt
# 从文件读取要忽略的提交列表
git blame --ignore-revs-file .git-blame-ignore-revs filename.txt
Git Grep:代码搜索
git grep 是 Git 内置的搜索工具,比系统的 grep 更快,而且只搜索 Git 跟踪的文件。
基本用法
# 在当前工作目录搜索
git grep "function login"
# 在特定分支搜索
git grep "function login" main
# 在多个分支搜索
git grep "function login" main develop
# 在所有分支搜索
git grep "function login" $(git branch -r --format='%(refname:short)')
# 在某个提交中搜索
git grep "function login" abc123
# 在标签指向的提交中搜索
git grep "function login" v1.0.0
常用选项
# 显示行号
git grep -n "function login"
# 显示文件名(默认显示)
git grep -l "function login"
# 只显示文件名,不显示内容
git grep -l "function login"
# 显示匹配次数
git grep -c "function login"
# 显示上下文
git grep -A 3 "function login" # 匹配行后 3 行
git grep -B 3 "function login" # 匹配行前 3 行
git grep -C 3 "function login" # 前后各 3 行
# 忽略大小写
git grep -i "FUNCTION"
# 使用正则表达式
git grep -E "func[a-z]+"
# 使用固定字符串(不解释正则)
git grep -F "func(a)"
# 只搜索特定文件
git grep "function" -- "*.js"
git grep "function" -- "*.js" "*.ts"
# 排除文件
git grep "function" -- "*.js" ":!test/*"
# 搜索整个单词
git grep -w "login" # 匹配 login 但不匹配 loginButton
# 统计每个文件的匹配行数
git grep --count "function"
# 显示匹配颜色
git grep --color=auto "function"
高级用法
搜索特定模式
# 搜索 TODO 注释
git grep -n "TODO"
# 搜索多个关键词
git grep -e "TODO" -e "FIXME"
# 搜索包含两个模式的行
git grep -e "TODO" --and -e "bug"
# 搜索包含任一模式的文件
git grep --or -e "TODO" -e "FIXME"
搜索历史代码
# 搜索某个历史提交中的代码
git grep "oldFunction" HEAD~10
# 搜索所有历史提交
for commit in $(git rev-list --all); do
if git grep -q "deletedFunction" $commit; then
echo "Found in $commit"
fi
done
使用 git log 搜索代码变更
# 找出添加或删除了某行代码的提交
git log -S "function login" --oneline
# 找出匹配正则表达式的变更
git log -G "function\s+\w+" --oneline
# 显示具体的变更内容
git log -S "function login" -p
# 只搜索特定文件
git log -S "function login" --oneline -- src/auth.js
Git Log 调试技巧
查找引入 bug 的提交
# 查看文件的修改历史
git log -p -- filename.txt
# 查看谁修改了某行代码
git log -L 10,20:filename.txt
# 二分查找问题提交(结合 bisect)
git log --oneline # 先查看提交历史,确定范围
git bisect start HEAD bad-commit good-commit
按条件筛选
# 查找特定作者的提交
git log --author="张三" --oneline
# 查找特定时间段的提交
git log --since="2024-01-01" --until="2024-01-31"
# 查找包含特定关键词的提交
git log --grep="bug" --oneline
# 查找修改了特定内容的提交
git log -S "deprecated API" --oneline
# 组合条件
git log --author="张三" --since="2024-01-01" --grep="fix"
查看代码演变
# 查看文件的完整历史(包括重命名)
git log --follow --oneline -- filename.txt
# 查看目录下所有文件的修改
git log -- oneline src/
# 以图形方式查看分支历史
git log --oneline --graph --all
Git Diff 调试技巧
比较差异
# 比较工作区和暂存区
git diff
# 比较暂存区和最新提交
git diff --staged
# 比较两个提交
git diff abc123 def456
# 比较两个分支
git diff main feature-branch
# 只显示文件名
git diff --name-only
# 显示统计信息
git diff --stat
查看特定文件或行的变化
# 比较特定文件
git diff -- filename.txt
# 比较特定目录
git diff -- src/
# 查看某个提交对特定文件的修改
git show abc123 -- filename.txt
找出空白字符问题
# 高亮行尾空白
git diff --ws-error-highlight
# 忽略空白字符差异
git diff -w
# 忽略空白字符变化
git diff -b
综合调试案例
案例:找到某个 bug 是何时引入的
# 1. 确认当前版本有问题
$ npm test
FAIL: 测试失败
# 2. 查找可能的提交范围
$ git log --oneline -20
# 3. 使用 bisect 定位
$ git bisect start HEAD HEAD~20
Bisecting: 9 revisions left to test after this (roughly 3 steps)
# 4. 测试并标记
$ npm test && git bisect good || git bisect bad
# 5. 重复直到找到问题提交
# abc123 is the first bad commit
# 6. 查看问题提交详情
$ git show abc123
# 7. 查看 blame 了解相关代码
$ git blame -L :problemFunction src/main.js
# 8. 退出 bisect
$ git bisect reset
案例:找出谁删除了某段代码
# 1. 在历史中搜索删除的代码
$ git log -S "deletedFunction" -p
# 2. 或者按行搜索
$ git log -L :deletedFunction src/utils.js
# 3. 查看具体的删除提交
$ git show abc123
案例:追踪某行代码的完整历史
# 1. 查看当前是谁写的
$ git blame -L 42,52 src/main.js
# 2. 查看这行代码的完整修改历史
$ git log -L 42,52:src/main.js -p
# 3. 追踪文件重命名
$ git log --follow -p -- src/main.js
小结
本章介绍的调试工具:
| 工具 | 用途 | 典型场景 |
|---|---|---|
git bisect | 二分查找问题提交 | 定位引入 bug 的提交 |
git blame | 追踪代码作者和时间 | 了解代码来源 |
git grep | 搜索代码内容 | 查找特定代码 |
git log -S | 搜索代码变更历史 | 找出添加/删除某行代码的提交 |
git log -L | 按行查看历史 | 追踪特定代码行的演变 |
git diff | 比较差异 | 查看具体变更 |
掌握这些工具能让你更高效地调试和追踪问题。
练习
- 创建一个项目,故意在中间某次提交引入 bug,使用 bisect 找到它
- 使用 blame 查看你项目中最复杂文件的历史
- 使用 grep 搜索你项目中的所有 TODO 注释
- 使用
git log -S找出某个函数是何时添加的