Python技术迷

git 如何查看某次提交修改的内容?

昨天晚上十一点多,我在公司楼下抽烟,风挺大,小李给我发了条语音:“东哥,我这边要看一个 merge 的提交到底改了啥,git show一堆红红绿绿我看花眼了,有没有一套更体系的玩法?”我当时困得眼睛打架,但这事儿我踩过太多坑,脑子一热就开始碎碎念,顺手把要点都整理出来了,免得你们再到处翻命令手册。

你想看“那一次”的改动,第一步还是把它“掐”出来。最快的套路是:用消息、范围、文件三种维度去锁定。

1)按消息搜,别翻屏:

git log --oneline --grep='登录.*超时' -n 20

2)按文件找“最后一次动它的人”,顺着回溯到那次提交:

git log --oneline -- path/to/login.py

3)你已经有 commit 哈希了?确认下是不是合并提交:

git cat-file -p <commit> | sed -n '1,3p'
# parent 出现不止一行,说明它是 merge

小插曲:单提交的“标准差异”其实等价于 git diff <commit>^ <commit>,而简写就是 git diff <commit>^!。所以你看到我下面老用 <commit>^!,是有来头的。

我一般只用三板斧,信息密度高、眼睛不累,尤其夜里盯屏的时候。

1. 看补丁本体(但干净点)

git show 很香,但默认上下文多、重命名不明显。稍微“洗”一下:

git show --patch --minimal -U3 -M -C --stat --summary <commit>
  • -U3:三行上下文,刚刚好,太多就淹没了改动;
  • --minimal:让 diff 更“聪明”,少出“错觉性”改动;
  • -M -C:识别重命名与拷贝,避免把“移动代码”误读成“删除+新增”;
  • --stat --summary:先看面,再看里。

只想看某个文件在这次提交里怎么变的?

git show <commit> -- path/to/login.py

2. 只看“改了哪几样”,别展开

半夜扫一眼够了:

git show --name-status --stat <commit>
# A 新增, M 修改, D 删除, R 重命名, C 拷贝

更极端点,文件 + 行数统计即可:

git diff --stat --summary <commit>^! 

3. 关注“词义上的变化”(不是缩进)

有些提交是“把 Tab 全换空格”,这种没意义的噪音得一把切掉:

git show -w <commit>                 # 忽略空白变化
git show --word-diff=plain <commit>  # 词级别增删,更好读
git show --color-moved=dimmed_zebra --color-moved-ws=allow-indentation-change <commit>
# 看代码块的搬家,配合 -M 很稳

合并提交怎么读才专业

合并提交通常是“把 B 分支的改动合进来”,你直接 git show <merge-commit> 会看到“汇总出来”的 combined diff,读着容易迷路。我一般分三步:

1)按父提交逐个看“这次合并相对每个父亲”到底冲突/改了啥:

git show -m <merge-commit>
# -m 会拆成对每个 parent 的补丁

2)需要“冲突处到底怎么被解决的”就用合并视图:

git show --cc <merge-commit>
# 多父亲的合并上下文,一屏看懂解决方案

3)只看合并时“你手工改了的部分”,排除纯粹把分支合上来的噪音:

git diff --cc <merge-commit>  # 或者配合 --word-diff 更聚焦

只看你在乎的范围:文件、函数、路径魔法

有时你就想看某个函数这次到底被谁改了:

git log -L :login:app/auth.py <commit>^!   # -L 函数级追踪

路径过滤也别只会“放文件名”,Git 的 pathspec 有“魔法”:

git show <commit> -- ':(icase)*login*.py'# 忽略大小写匹配
git show <commit> -- ':(exclude)tests/**'':!docs/**'

用 pickaxe 精确找“哪次把这句改了”

两个利器:-S 和 -G。前者按字串,后者按正则。

# 哪次删了/加了 "timeout=30"
git log -S'timeout=30' -p -- path/to/login.py

# 哪次改了以 "Bearer " 开头的 token 处理逻辑
git log -G'^Bearer\s' -p -- path/to/login.py

锁定了提交,再 git show <commit> -- path/to/login.py 细看即可。

二进制和图片不是黑盒:让它“可读”

默认二进制只给你个“Binary files differ”。其实你可以让图片、PDF 也“文本化”对比。

.gitattributes 里挂上 textconv:

*.png  diff=pngmeta
*.pdf  diff=pdftxt

然后告诉 Git 怎么转:

git config diff.pngmeta.textconv "exiftool -s -ImageWidth -ImageHeight -ColorSpace -DateTimeOriginal -FileName -FileSize -Orientation -"
git config diff.pdftxt.textconv "pdftotext -layout -nopgbrk -"

再来一次:

git show <commit> -- path/to/screenshot.png
# 这次会打印前后图片的元数据差异

生成级文件别干扰阅读

比如前端构建出来的 bundle、后端生成的 _pb2.py,你可以在 .gitattributes 里告诉 Git 别生成 patch:

*.min.js -diff
*_pb2.py -diff

看提交的时候就清净多了。

读补丁的几个“舒适化”小开关

我这边常开的:

# 少上下文,但不至于丢信息
git config diff.context 3

# 颜色移动块,搬家可视化
git config diff.colorMoved dimmed_zebra
git config diff.colorMovedWS allow-indentation-change

遇到“大改动”(比如重命名+剪贴代码)时,适当把阈值放宽,准确识别移动:

git show -M50% -C50% <commit>

实战的三四个套路,真省眼睛

1)“只看这次是不是纯重命名”:

git show --name-status <commit> | awk '{print $1}' | sort | uniq
# 只有 Rnn 说明就是 rename,不用展开大补丁

2)“一屏看懂这个提交改了哪几个模块”:

git show --stat=200,120 --compact-summary <commit>
# 把 stat 的宽度调大,路径对齐更好读

3)“确定是不是只改了注释/空格”:

git diff --word-diff-regex='[^\s]+' -w <commit>^! | grep -vE '^\+|^-'
# 没有有意义的 token 变动,那大概率就是格式化

4)“子模块发生了啥”:

git show --submodule=diff <commit>

外部对比工具:有时 GUI 更省脑

你盯一天终端也会晕。装个 difftool,必要时开 GUI:

git config diff.tool vscode
git config difftool.vscode.cmd "code --wait --diff $LOCAL$REMOTE"

git difftool <commit>^!                  # 逐文件打开 VS Code 对比
git difftool --tool=meld <commit> -- path/to/login.py

写个能落地的小脚本:一条命令把信息摘干净

我懒得每次拼长命令,就用 Python 包了一层。这个脚本做几件事:自动识别 merge、先给你 summary,再按需展开 patch;支持 --path、--word、--ignore-space 等常用开关。

#!/usr/bin/env python3
import argparse, subprocess, sys, shutil

defrun(cmd, cwd=None):
    p = subprocess.run(cmd, cwd=cwd, text=True,
                       stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if p.returncode != 0:
        print(p.stderr, file=sys.stderr)
        sys.exit(p.returncode)
return p.stdout

defis_merge(commit):
    out = run(["git", "cat-file", "-p", commit])
return out.count("parent ") > 1

defshow_summary(commit, paths):
    args = ["git", "show", "--name-status", "--stat", "--summary", "--no-color", commit]
if paths: args += ["--", *paths]
    print(run(args))

defshow_patch(commit, word=False, ignore_ws=False, context=3, paths=None, moved=True):
    args = ["git", "show", "--patch", f"-U{context}", "-M", "-C", "--minimal", commit]
if word: args += ["--word-diff=plain"]
if ignore_ws: args += ["-w"]
if moved: args += ["--color-moved=dimmed_zebra", "--color-moved-ws=allow-indentation-change"]
if paths: args += ["--", *paths]
    print(run(args))

defshow_merge(commit, per_parent=False):
if per_parent:
        print(run(["git", "show", "-m", commit]))
else:
        print(run(["git", "show", "--cc", commit]))

defmain():
    ap = argparse.ArgumentParser(description="clean viewer for a single git commit")
    ap.add_argument("commit")
    ap.add_argument("--path", action="append", help="limit to path (repeatable)")
    ap.add_argument("--word", action="store_true", help="word-level diff")
    ap.add_argument("--ignore-space", action="store_true", help="ignore whitespace")
    ap.add_argument("--context", type=int, default=3, help="context lines")
    ap.add_argument("--merge-parent", action="store_true", help="for merges, diff to each parent")
    ap.add_argument("--patch", action="store_true", help="print full patch after summary")
    args = ap.parse_args()

    commit = args.commit
if is_merge(commit):
        print(f"[merge] {commit}")
        show_summary(commit, args.path)
        show_merge(commit, per_parent=args.merge_parent)
else:
        show_summary(commit, args.path)
if args.patch or sys.stdout.isatty():
            show_patch(commit, word=args.word, ignore_ws=args.ignore_space,
                       context=args.context, paths=args.path)

if __name__ == "__main__":
ifnot shutil.which("git"):
        print("git not found in PATH", file=sys.stderr); sys.exit(1)
    main()

用法示例:

# 只看这次在 app 目录的有意义改动,词级别展示
python show_commit.py a1b2c3d --path app --word --ignore-space

# 合并提交,想看每个父提交的差异
python show_commit.py f7g8h9i --merge-parent

最后,再补几个“会被问到”的细节

  • git show 默认不总是做重命名检测,所以你看到“删一堆、加一堆”时,果断加 -M -C 再看一眼;

  • 单提交用 <commit>^! 是个小秘诀,合并提交时想要“更像平时的补丁”,用 --cc;

  • 大补丁里关心“到底动了谁写的那几行”,先 git blame 定位再 git show 精读:

    git blame -L 20,60 path/to/login.py
    git show <拿到的commit> -- path/to/login.py
  • 读不懂是因为“上下文太多”,就把 -U 调小;读不清是因为“噪音太多”,就加 -w 和 --word-diff;读不准是因为“搬家被当成新增”,就把 -M -C 的百分比放宽。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB,点击下方公众号回复关键字 python 全部免费领