如何比较两个分支之间的差异并输出文件列表?

文章导读
比较两个分支之间的差异并输出文件列表,在准备合并、审查改动或排查回归时很常见。不过,在输入命令之前,最好先想清楚你要的是“内容差异”还是“提交独有差异”,因为这两者使用的命令不同,含义也不一样。
📋 目录
  1. 先更新引用,再决定要不要找共同祖先
  2. 容易忽略的检查点
  3. 把列表交给脚本:输出到文件与过滤状态
  4. 用 --stat 和 git log 交叉验证
A A

比较两个分支之间的差异并输出文件列表,在准备合并、审查改动或排查回归时很常见。不过,在输入命令之前,最好先想清楚你要的是“内容差异”还是“提交独有差异”,因为这两者使用的命令不同,含义也不一样。

要输出两个分支之间的文件差异,最直接的方式是使用 `git diff`。比如 `git diff branchA branchB --name-only` 会列出从 branchA 到 branchB 发生了变更的文件路径,而 `--name-status` 则额外给出每个文件的状态(A 表示新增,M 表示修改,D 表示删除)。但需要注意方向:`branchA branchB` 显示的是从 branchA 变为 branchB 的差异,如果你关心的是两个分支各自的独有提交,可能还需要用 `git log` 或 `git cherry` 来辅助判断,因为单纯的 diff 不会展示提交历史,只展示最终内容差异。

如果你的目标是找出分支 B 相对于分支 A 新增或改动了哪些文件,建议先更新本地引用:`git fetch origin`,然后用 `git diff origin/A origin/B --name-status`。如果分支之间存在分叉,尤其是有合并提交时,只比较两个端点可能不够精确。更稳妥的做法是先找到它们的共同祖先:`git merge-base origin/A origin/B`,得到 commit 后,再用 `git diff origin/B --name-only`,这样能清晰列出 B 自分叉以来所有变化的文件,避免把 A 独有提交产生的差异也混进来。

要输出两个分支之间的文件差异,最直接的方式是使用 git diff。比如 git diff branchA branchB --name-only 会列出从 branchA 到 branchB 发生了变更的文件路径,而 --name-status 则额外给出每个文件的状态(A 表示新增,M 表示修改,D 表示删除)。但需要注意方向:branchA branchB 显示的是从 branchA 变为 branchB 的差异,如果你关心的是两个分支各自的独有提交,可能还需要用 git loggit cherry 来辅助判断,因为单纯的 diff 不会展示提交历史,只展示最终内容差异。

例如,当你执行 git diff main feature --name-only 时,得到的是从 main 当前状态变为 feature 当前状态所涉及到的所有文件,它同时包含 main 上 feature 没有的改动,以及 feature 上 main 没有的改动。如果你只想看 feature 相对于共同祖先的新增内容,那么直接比较两个分支就不够精确,应该先定位它们的 merge-base。

先更新引用,再决定要不要找共同祖先

在比较远端分支时,应当先执行 git fetch origin 获取最新的提交。否则你比较的可能还是本地过期的引用。获取之后,用 git diff origin/main origin/feature --name-status 可以看到文件状态。

如果两个分支是从同一个提交分叉出来的,那么 git diff origin/main origin/feature 会同时把两个分支上的独立改动都列出来。这不是你想要的“feature 新增了哪些文件”时,很容易被误导。更稳妥的做法是先找到共同祖先:

git merge-base origin/main origin/feature

得到 commit 后,用 git diff <merge-base> origin/feature --name-only 就能只列出 feature 自分叉以来变化过的文件。注意,这里必须把 merge-base 作为第一个参数,不能省略,否则 git diff origin/feature --name-only 默认是与工作区比较,结果含义完全不同。

如果你同时也在维护 main 分支,那么 main 上的提交也可能影响最终结果。使用 merge-base 可以让比较基准固定在分叉点,不会把 main 上的后续改动混进来。但要注意,如果 feature 已经合并过 main,那 merge-base 可能就变成了合并提交,这时的 diff 未必能反映原始改动过程,需要结合 git log 观察。

容易忽略的检查点

一个常见的错误是混淆 --name-only--name-status,前者只输出文件路径,后者还包含文件状态。在脚本中解析时,若依赖状态码,必须用后者。另一个坑是文件名中包含空格或特殊字符,git diff --name-only 会按行输出,但文件名可能被转义,建议使用 -z 参数配合 xargs -0 来安全处理。还要注意,git diff 默认忽略空格和换行符的差异吗?不会,它会报告所有差异,除非你在命令中加了 -w--ignore-space-change。如果遇到大量无关紧要的格式改动,可以考虑这些参数来缩小文件列表。

如何比较两个分支之间的差异并输出文件列表?

在实际项目中,我通常会先跑一遍 git diff ... --name-status 看状态分布。如果看到大量的 R 或 D,就要考虑是不是重命名没识别,或者有文件被误删。这时候用 -M 参数重新检测重命名,输出会干净很多。

把列表交给脚本:输出到文件与过滤状态

如果你需要把文件列表输出到文件供后续处理,可以这样写:git diff branchA branchB --name-only > changed_files.txt。在 CI 或脚本中,推荐用 git diff --name-only --diff-filter=ACMRT 来过滤掉删除的文件,只保留新增、修改、复制和重命名的文件,这样能避免后续步骤因文件不存在而报错。对于重命名检测,git diff 默认不识别重命名,需要加 -M 参数或设置 diff.renames=true,否则重命名文件会显示为删除和新增两条记录,这会影响你对变更规模的理解。

举例来说,如果只是把文件改名,但内容没变,默认的情况下 --name-status 会显示为 D 和 A 两条,这会让脚本误判为删除和新增。设置 diff.renames=true 后,会显示为 R100,表示重命名且内容完全相同。在需要统计改动大小时,这个差异很重要。

另外,--diff-filter 还可以组合多个状态,比如 --diff-filter=d 只显示删除的文件,这在检查破坏性变更时很有用。不过要注意,该参数只在与 --name-only--name-status 同时使用时生效。

用 --stat 和 git log 交叉验证

在拿到文件列表后,先不要急着往下走。用 git diff branchA branchB --stat 快速浏览一下变更规模,看有没有异常大或异常小的改动。接着抽查几个关键文件,确认差异内容是否符合预期。比如用 git diff branchA branchB -- 目录/文件 来查看特定文件的具体改动。

还需要记住,git diff --name-only 的输出并不等同于“分支 B 上尚未合并的提交”的文件集合。因为 diff 比较的是快照,如果两个分支在同一个文件上有相同的改动,diff 不会显示。要理解提交来源,还是得靠 git log branchA..branchB --oneline。例如,你想知道分支 B 上有哪些提交是 A 没有的,可以用这个命令。把 diff 的结果和 log 的结果对照,才能全面把握改动范围。

最后,注意工作区状态。如果你本地还有未暂存或未提交的改动,这些改动不会自动出现在分支比较中,因为 git diff branchA branchB 比较的是两个提交对象,而不是工作区。需要时先提交或暂存,再进行比较。

也就是说,当你在脚本或 CI 中遇到分支比较需求时,别忘了把 merge-base、重命名检测和工作区状态这几个边界考虑进去,否则生成的列表很可能跟你预期不符。