Git first-parent:日志只沿主线走,合入的功能提交去了哪里
查看一次集成分支的演进,日志里却挤满功能分支的中间提交:改名一次、补测试一次、修拼写一次。想把视角移到“主线何时接纳了一组变化”,可以使用 first-parent。它改变沿提交图回溯的路径,并不会删掉那些功能提交。
合并提交可以有多个父提交。常见的双亲合并中,第一个父提交是执行合并时所在分支原来的头,第二个父提交是合入的另一端。first-parent 遇到合并后只沿第一条父边继续,所以它能保留集成节点,并略过旁支里的逐步实现。
造一个有两条发展路线的小仓库
保存为 demo.sh,用 bash demo.sh 运行。本文用 Git 2.52.0 与 Bash 5.2.37 实跑。脚本创建并清理自己的临时目录,身份和签名配置只落在示例仓库。没有远端,也不会改动已有项目。
AI概念插图:用抽象图形表现本文讨论的关系,并非软件截图或真实运行结果。
#!/usr/bin/env bash set -euo pipefail demo_dir=$(mktemp -d) trap 'rm -rf -- "$demo_dir"' EXIT export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null export GIT_AUTHOR_DATE='2026-01-01T00:00:00Z' export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git init -q -b main "$demo_dir" cd "$demo_dir" git config user.name Demo git config user.email demo@example.invalid git config commit.gpgsign false printf 'base\n' > base.txt git add base.txt git commit -qm base git switch -qc feature printf 'one\n' > feature.txt git add feature.txt git commit -qm feature-one printf 'two\n' >> feature.txt git commit -qam feature-two git switch -q main printf 'main\n' > main.txt git add main.txt git commit -qm main-work git merge -q --no-ff feature -m merge-feature all_count=$(git rev-list --count HEAD) first_count=$(git rev-list --first-parent --count HEAD) plain_count=$(git rev-list --no-merges --count HEAD) [[ $all_count == 5 && $first_count == 3 && $plain_count == 4 ]] printf 'all=%s first-parent=%s no-merges=%s\n' "$all_count" "$first_count" "$plain_count" git log --first-parent --reverse --format='%s' HEAD [[ $(git show HEAD^1:main.txt) == main ]] [[ $(git show HEAD^2:feature.txt) == $'one\ntwo' ]] [[ $(git show HEAD:feature.txt) == $'one\ntwo' ]] printf 'both parents checked; feature content remains\n'
三种数量来自三种遍历规则
输出的提交数量分别是五、三、四。完整历史包含 base、两次 feature 修改、main-work 与 merge-feature。first-parent 的三条标题则依次是 base、main-work、merge-feature。这就是主线在本例中的演进轮廓。
no-merges 返回四,并没有得到同样的主线视角。它只是去掉合并提交,旁支中的 feature-one 和 feature-two 仍然可以出现。一个选项控制回溯时走哪条父边,另一个过滤合并节点;两者不能根据“日志看起来短了”就互相替换。
末尾分别读取 HEAD 的第一父、第二父以及合并结果。第一父有主线文件,第二父有功能文件,合并结果也保留功能内容。这些断言说明:展示旁支提交的方式变了,最终文件内容没有因此丢失。
第一父来自合并方向,不来自分支名字
Git 不会因为某条分支名叫 main 就自动把它认作第一父。如果当时在 feature 上执行合入 main,再把指针安排到其他名字,第一父关系仍由原合并对象记录。阅读主线历史前,要先确认团队的合并方向和实际提交图。
示例使用 no-ff,并且让主线与功能分支各自产生修改,确保生成双亲合并节点。若采用快进,只移动分支指针,并不会凭空产生这样一个集成节点。若团队习惯压成单提交,日志粒度又会不同,应依据工作流解释结果。
first-parent 也不等于只显示合并。main-work 是普通提交,它仍留在结果中。想写发布摘要,可以先沿这条路线寻找集成位置,再进入某次合并检查具体差异。把这份简略清单当成全部开发活动,会遗漏旁支上的细节。
本实验没有添加文件路径筛选,也不讨论更复杂的历史简化规则。实际排查某个文件时,路径限制可能进一步改变显示结果。若数量与预期不同,先用没有路径条件的图形日志核对父边,再逐个加入过滤条件。
把问题表述清楚后,命令就更容易选:需要集成节奏,看第一父路径;需要完整可达提交,看全图;需要非合并实现提交,再用对应过滤。一次只改变一个条件,才能知道日志少掉的究竟是哪一类信息。


