VS Code 自动同步如何重写 Git 历史:一次重复合并冲突的排查与恢复
某次开发中,功能分支第一次合并 master 没有冲突,并完成了推送。两个分支之后都没有人工提交,但第二次合并 master 时却出现了冲突。
直觉上这不合理:如果第一次 merge 已经保留,第二次应该返回 Already up to date。最终通过 reflog 发现,问题来自 VS Code 自动同步触发的 git pull --rebase。
现象
操作过程大致如下:
text
1. feature 分支执行 git merge master
2. 合并成功,没有冲突
3. 点击 VS Code"同步更改"
4. 再次执行 git merge master
5. 出现冲突
同时,两个分支中出现了标题、作者和代码修改近似,但哈希不同的提交。
根因
检查用户级 Git 配置后发现:
ini
[pull]
rebase = true
VS Code 自动同步通常包含 pull 和 push。由于 pull.rebase=true,pull 实际采用了 rebase:
text
git fetch
git rebase
git push
第一次 merge 产生的合并提交有两个父提交:
text
merge commit
├── feature 原 HEAD
└── master HEAD
但随后的 pull --rebase 会重新选择基点,并逐个 replay 功能分支提交。原 merge commit 可能因此离开当前分支历史,普通提交则被重新生成。
所以,即使开发者没有手动 commit,Git 历史仍然发生了变化。
为什么提交标题相同,哈希却不同
Git 提交哈希不仅由代码内容决定,还包括:
text
文件树
父提交哈希
作者和提交者信息
时间
提交说明
rebase 后父提交发生变化,新提交自然会得到新的哈希。因此,标题相同不等于 Git 认为它们是同一个提交。
这也是第二次 merge 冲突的原因:第一次 merge 建立的祖先关系已经丢失,Git 只能基于新的共同祖先重新执行三方合并。
使用 reflog 定位
git log 只能看到当前可达历史,git reflog 则记录本地 HEAD 如何移动:
powershell
git reflog --date=local
典型记录如下:
text
feature -> merge-commit merge master
merge-commit -> old-head pull --rebase start
old-head -> new-commit pull --rebase pick
其中 pick 表示提交被 rebase 重新应用。检查提交父节点也很有用:
powershell
git show -s --format=%P <commit>
一个父节点是普通提交,两个父节点才是 merge commit。
撤销已完成的 pull --rebase
git rebase --abort 只能取消正在进行的 rebase。rebase 已完成后,需要通过 reflog 回到操作前的提交。
先备份当前状态:
powershell
git status
git branch backup-before-rebase
确认工作区没有需要保留的未提交修改后,回退到 reflog 中 pull 开始前的提交:
powershell
git reset --hard <pre-rebase-commit>
不要凭感觉使用 HEAD~1,因为 rebase 后的提交数量和父子关系可能已经变化。
远程已经被同步怎么办
本地回退后,普通 push 通常会因非快进更新而被拒绝。确认需要恢复远程历史时,可以使用:
powershell
git push --force-with-lease origin HEAD:<branch>
--force-with-lease 不检查提交人,而是检查远程分支当前指向的提交哈希。如果远程分支已经被其他人更新,推送会被拒绝。
相比之下,--force 会直接覆盖远程历史,风险更高。
关闭自动 rebase
关闭 Git 用户级配置:
powershell
git config --global pull.rebase false
VS Code 设置中同时关闭:
```json
"git.rebaseWhenSync": false
也可以在单次拉取时明确指定:
powershell
git pull --no-rebase
对于共享分支,更稳妥的工作流是先 fetch、检查提交图,再手动 merge:
powershell
git fetch origin
git log --graph --oneline --decorate --all
git merge origin/master
总结
"两个分支没有人工提交"不代表 Git 历史没有变化。IDE 自动同步、pull --rebase、reset 和 force push 都可能移动分支指针或重写提交。
遇到"第一次合并无冲突,第二次反而冲突"时,应优先检查 git reflog、提交父节点和 pull.rebase 配置。代码是否相似只是表象,Git 真正依赖的是提交图与祖先关系。