你执行 git merge feature/design-refresh,一切看起来正常,直到 Git 打出 CONFLICT (content): Merge conflict in assets/logo.png 然后停住。在编辑器里打开这个文件毫无用处——你看到的是一堆乱码字节,而不是文本文件里那种 <<<<<<< HEAD 标记。git mergetool 弹出的十六进制视图同样帮不上忙。二进制文件(图片、编译产物、SQLite 数据库、.xlsx 表格)无法做逐行 diff,所以 Git 拒绝猜测哪一边是对的,把决定权交给了你。
最快的修复: 想清楚你要保留哪个完整版本,对冲突路径执行下面其中一条命令,暂存,然后继续:
git checkout --ours -- assets/logo.png # 保留当前分支的版本
# 或者
git checkout --theirs -- assets/logo.png # 采用合并进来的分支的版本
git add assets/logo.png
git merge --continue --no-edit
二进制文件没有”部分合并”这回事——你只能完整保留其中一边。本文剩下的部分帮你搞清楚两件事:(1) 到底该选哪一边;(2) 处理更棘手的情况:一边删了文件、误提交的构建产物、或者 Git LFS 指针不匹配。
先判断你属于哪一类
在右栏跑对应的诊断命令,然后跳到下方匹配的原因。
git status / 日志里的现象 | 可能原因 | 诊断命令 |
|---|---|---|
both modified: <file> | 两个分支都改了同一个二进制文件 | git log --oneline --all -- <file> |
deleted by us / deleted by them | 修改/删除冲突 | git status |
文件匹配构建产物模式(*.pyc、dist/) | 两边都提交了生成的产物 | git check-ignore -v <file> |
| 一边是很小的文本指针,另一边很大 | Git LFS 指针 vs. 真实字节 | git lfs status |
| 没有有意修改却出现很大的 delta | 应用重新保存了文件(元数据/压缩变化) | git diff --stat HEAD MERGE_HEAD -- <file> |
常见原因
按命中率从高到低排列。
1. 两个分支各自独立修改了同一个二进制文件
两个开发者各自在自己的分支上更新了 assets/logo.png——一个改了尺寸,一个改了颜色。Git 对同一路径看到两段不同的字节序列,无法自动合并。
怎么判断: 执行 git log --oneline --all -- assets/logo.png。你会看到两条独立的提交,分别在不同分支上,都在分叉点之后动过这个文件。
2. 一个分支删了文件,另一个分支改了它
有人在 main 上删掉了过时的 .dll,而另一个分支恰好提交了同一个 .dll 的新版本。Git 报告”修改/删除”(modify/delete)冲突。
怎么判断: git status 在该路径旁显示 deleted by us 或 deleted by them。(deleted by us 表示你这边的分支删了它;deleted by them 表示合并进来的分支删了它。)
3. 自动生成的二进制被两个分支都提交了
构建产物(*.pyc、编译出的 .so、打包生成的 *.js.map)在历史里被两个分支分别提交过。两个版本都不是”权威版本”——你其实是想把这个文件彻底移出仓库。
怎么判断: 文件扩展名匹配本该被 .gitignore 规则拦下的东西。用 git check-ignore -v <file> 确认——如果它打印出匹配的规则,说明这个文件本来就不该被跟踪。
4. Git LFS 指针 vs. 真实字节不匹配
一边把文件存成了真实 blob,另一边存的是一个很小的 Git LFS 指针(几行文本,以 version https://git-lfs.github.com/spec/v1 开头)。Git 看到两段完全不同的字节序列就标记冲突,哪怕逻辑上只有一个人动过真实文件。
怎么判断: git lfs status 列出了该文件,并且用 git show :2:<file> | head -3 检查其中一边会看到 version https://git-lfs.github.com/spec/v1 指针头,而另一边没有。
5. 文件被重新保存导致元数据变化
.docx、.pdf 或 .xlsx 被某个应用(或同一应用在不同操作系统上)重新保存,重写了内部压缩或元数据,于是即使人眼可见的内容一致,字节层面也不同。
怎么判断: git diff --stat HEAD MERGE_HEAD -- <file> 显示很大的二进制 delta,但你并没有有意编辑。这种情况下通常最安全的做法是保留 --theirs(合并进来的、大概率更新的那次保存),然后用肉眼复核一遍文档。
最短修复路径
Step 1:动手之前先备份
git tag backup/before-binary-fix
这是一条命令就能用的”逃生通道”。万一选错了边,git reset --hard backup/before-binary-fix 能把你精确恢复到起点。
Step 2:把两个版本都导出来对比
Git 把冲突的各个版本存在索引的编号*暂存区(stage)*里:stage 2 是 “ours”(你当前的分支),stage 3 是 “theirs”(合并进来的分支)。把两边都导出到临时文件:
# "ours" —— 当前分支(HEAD)
git show :2:assets/logo.png > ours_version.png
# "theirs" —— 合并进来的分支(MERGE_HEAD)
git show :3:assets/logo.png > theirs_version.png
用对应的应用(看图工具、Excel、PDF 阅读器)打开这两个文件,判断哪个是对的。对于图片,git diff --stat HEAD MERGE_HEAD -- assets/logo.png 也能告诉你哪一边改动更大。
Step 3:明确选择保留某一边
# 保留当前分支的版本
git checkout --ours -- assets/logo.png
# 或者保留合并进来的分支的版本
git checkout --theirs -- assets/logo.png
注意:在 git rebase 过程中,--ours 和 --theirs 的含义是对调的——--ours 指你正在 rebase 到的那个分支,--theirs 指被重放的你自己的提交。选之前先看是存在 .git/rebase-merge(你在 rebase)还是 .git/MERGE_HEAD(你在 merge)。
Step 4:暂存这次解决,然后继续
git add assets/logo.png
git merge --continue --no-edit
git checkout 只是把字节写到磁盘;真正告诉 Git”冲突已解决”的是 git add。--no-edit 表示接受自动生成的合并提交信息,不再弹出编辑器。
Step 5:处理修改/删除(modify/delete)的情况
如果 git status 说的是 deleted by us / deleted by them,你要决定的不是保留哪个字节版本,而是这个文件还要不要保留:
# 保留文件(覆盖那次删除)
git checkout --theirs -- assets/logo.png # 或 --ours,取仍然保留它的那一边
git add assets/logo.png
git merge --continue --no-edit
# 或者接受删除(移除文件)
git rm assets/logo.png
git merge --continue --no-edit
Step 6:确认修好了,再清理
git status # 应显示 "All conflicts fixed" / 工作区干净
git ls-files -u # 应该什么都不打印(没有残留的未合并条目)
git show --stat HEAD # 确认合并提交已落地,且文件正是你想要的
rm -f ours_version.png theirs_version.png
git tag -d backup/before-binary-fix # 确认结果无误之后再删
如果 git ls-files -u 仍然列出该路径,说明你漏了 Step 4 里的 git add——重新执行一次即可。
预防建议
- 在任何人提交之前,就把生成的二进制模式加进
.gitignore:*.pyc、*.class、*.o、*.so、dist/**、build/**。 - 把大型二进制资产迁到 Git LFS,这样每一边都以小小的指针文件参与合并:
git lfs track "*.png" "*.psd" "*.ai"(随后提交更新后的.gitattributes)。 - 对于”某个分支始终应该胜出”的二进制,在
.gitattributes里加一个 merge driver——例如*.xlsx merge=ours,再用git config merge.ours.driver true启用一次。之后的合并里,Git 会对匹配的文件静默保留你当前分支的版本,不再产生冲突标记。 - 把设计源文件(Figma 导出、PSD)放进资产管理系统或共享盘,而不是仓库里。
- 建立团队约定:每个分支上同一个二进制资产只由一个人负责,让两个分支很少同时改它。
- 对于 SQLite 文件,提交纯文本的 SQL 迁移脚本并从中重新生成数据库,而不是提交二进制
.db。
常见问答 (FAQ)
Q:执行了 git checkout --ours 之后,git status 里文件还显示冲突,为什么?
A:git checkout 只是把字节写到磁盘。你必须随后执行 git add <file>,告诉 Git 这个冲突已解决。用 git ls-files -u 验证——每个冲突都暂存后,它应该什么都不打印。
Q:在 rebase 里,--ours 和 --theirs 是不是对调了?
A:是的。在 git rebase 过程中,--ours 指的是你正在 rebase 到的那个分支(上游/基底),--theirs 指的是正在被重放的提交(你自己的工作)。这和 merge 正好相反,几乎人人都会踩坑。不确定自己处在哪种模式时,看一下是存在 .git/rebase-merge(rebase)还是 .git/MERGE_HEAD(merge)。
Q:能不能不走交互步骤,把所有二进制冲突一次性全部选某一边?
A:两种办法。临时一次性合并,用策略选项:git merge -X ours feature/branch(或 -X theirs)会对每个冲突(包括二进制)整体取你指定的那一边。需要长期、按模式生效的规则,则在 .gitattributes 里加 *.png merge=ours,再执行 git config merge.ours.driver true。
Q:能用 Kaleidoscope 这类可视化合并工具处理二进制吗?
A:只有当工具懂这种格式时才行。Kaleidoscope 支持图片和 PDF;对任意二进制,它会退回到”选一边”的界面。用 git config merge.tool kaleidoscope 配置后,git mergetool 会为每个二进制冲突启动它。在 macOS 上,git config merge.tool opendiff 会用 Xcode 的 FileMerge,它能预览部分图片格式。
Q:我们不小心提交了一个 150 MB 的二进制,现在每次 clone 都很慢,怎么修复历史?
A:那是另一个问题——用 git filter-repo 重写历史。详见 Git LFS pointer 文件没被真实文件替换 以及大文件相关排查文章里的逐步操作。
相关阅读
- 找回 Git 历史里的旧版本文件
- Git LFS pointer 文件没被真实文件替换
- Rebase 之后 commit 消失了
- Force push 覆盖了队友的 commit
- AI 帮你解决合并冲突
- AI package-lock 冲突处理
- CRLF 转换让单次 commit 产生几千行 diff
标签: #git #version-control #排查