跳到主要内容

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比较差异查看具体变更

掌握这些工具能让你更高效地调试和追踪问题。

练习

  1. 创建一个项目,故意在中间某次提交引入 bug,使用 bisect 找到它
  2. 使用 blame 查看你项目中最复杂文件的历史
  3. 使用 grep 搜索你项目中的所有 TODO 注释
  4. 使用 git log -S 找出某个函数是何时添加的

参考资料