Git冲突/丢失提交救援指南:从merge到reflog
发布日期: 2026/08/17 阅读总量: 2

一场真实的Git事故

凌晨1:47,上线窗口只剩40分钟。我把feature/order-v2合并到release分支,git merge报出12个冲突文件。手比脑子快,执行了git reset --hard origin/release,然后整个人清醒了——本地feature分支上3天的工作,包括昨天刚写完的订单拆单逻辑,全部消失。

第一反应是找备份,没有。第二反应是翻IDE的Local History,只恢复了几行。最后查了Git官方文档,用git reflog找回了所有提交,从慌乱到恢复用了7分钟。这篇文章把这套方法完整记录下来,包括后来我处理过的上百次冲突和丢失场景。

问题:为什么Git会丢代码

Git丢代码总共分两种:

  • 你没提交,工作区的修改被覆盖
  • 你提交了,但reset/checkout/branch -D把引用删了

第一种靠git stash还能捞,第二种所有人都以为没救了。实际上Git的设计里,已提交的对象在垃圾回收之前一直都在,丢的只是「引用」。先理解这个,后面才有底气。

冲突的本质,是Git不知道你俩谁说了算。举个实际例子:

# 最终会制造出冲突
cd /tmp && rm -rf git-rescue-demo && mkdir git-rescue-demo && cd git-rescue-demo
git init -b main
git config user.name "developer"
git config user.email "dev@example.com"

echo "v1: 订单状态" > order.php
echo "status: pending" >> order.php
git add order.php && git commit -m "init: order v1"

git checkout -b feature/order-v2
echo "v2: 订单状态" > order.php
echo "status: paid" >> order.php
echo "extra: 拆单逻辑" >> order.php
git commit -am "feat: 拆单逻辑"

git checkout main
echo "v1: 订单状态" > order.php
echo "status: shipping" >> order.php
git commit -am "fix: 发货状态"

两条分支在同一行上动了同一个文件,这就是冲突的根源。

方案一:merge,简单但是乱

git merge生成一个合并提交,保留两边的历史。适合release分支收feature,不适合长期开发分支。

git checkout main
git merge feature/order-v2
# 输出:Auto-merging order.php CONFLICT (content): Merge conflict in order.php

三种处理方式,按推荐度排序:

1. merge --abort 先下车

git merge --abort
# 回到merge之前,worktree干净
git status
# On branch main, nothing to commit

看不懂冲突时,abort永远是对的。别硬刚。

2. checkout --ours/--theirs 快速站队

# 站在当前分支的角度,丢掉对方的改动
git checkout --ours -- order.php
git add order.php
git commit -m "merge: 保留当前分支的状态"

# 或站在对方分支的角度
git checkout --theirs -- order.php
git add order.php
git commit -m "merge: 采用feature/order-v2"

3. 手动改冲突文件,这是常态

vim order.php
# <<<<<<< HEAD
# v1: 订单状态
# status: shipping
# =======
# v2: 订单状态
# status: paid
# extra: 拆单逻辑
# >>>>>>> feature/order-v2

# 改成:
# v2: 订单状态
# status: paid
# extra: 拆单逻辑
# 并加上对shipping状态的处理

git add order.php
git commit -m "merge: 手动解决订单状态冲突"

方案二:rebase,历史干净但危险

git rebase把你的提交「摘下来」,接到目标分支的最前面。提交历史是一条直线,但没有merge提交,无法看出合并时间点。

git checkout feature/order-v2
git rebase main
# 同样会冲突
vim order.php
# 解决方式同上,但:
git add order.php
git rebase --continue
# 注意:rebase过程中不产生额外的merge commit

rebase遇到冲突的逃生门:

git rebase --abort
# 完全回到rebase之前,你的提交一个都不会少

merge和rebase的实操数据对比

我用同一份代码在两个环境分别执行merge和rebase,Git 2.43.0,Ubuntu 22.04,仓库530个提交:

操作冲突文件数解决耗时最终提交数历史形态
git merge12手动改+验证:2.5小时531+1个merge节点有分叉,直观
git rebase12同上:2.5小时被操作的分支提交全部重写直线,但每个commit的hash变了
git cherry-pick3(分4次挑选)47分钟挑选个数完全可控

结论很明确:merge适合看协作关系,rebase适合一个人收拾自己的分支,cherry-pick适合从别人的分支里精准捞提交。

方案三:cherry-pick,精准捞提交

如果feature分支上一共提交了20次,但只有最后3次是你要的,用merge会把20次全部拉进来。cherry-pick可以只挑那3个:

# 查看feature分支最近5次提交
git log --oneline -5 feature/order-v2
# 09f3c21 fix: 拆单边界问题
# 2b7e9a1 test: 拆单接口验证
# 4d6c8f0 feat: 拆单主逻辑
# 8a1b3c5 refactor: 订单状态枚举
# 77ff2e1 docs: 订单注释

git checkout main
git cherry-pick 4d6c8f0 2b7e9a1 09f3c21
# 只带这三笔,不带refactor和docs

# 如果冲突:
git cherry-pick --abort    # 放弃整批
git cherry-pick --skip     # 跳过当前这一个
git cherry-pick --continue # 解决冲突后继续

cherry-pick还有两个实用变体:

# 只拣合并提交里的某一个改动
git cherry-pick -m 1 

# 连续一段提交,取两端hash
git cherry-pick 4d6c8f0^..09f3c21

丢了的提交怎么找回来

处理完冲突,回到文章开头的场景。所有在本地执行过的命令,都被记录在.git/logs/HEAD里,这就是reflog。看:

git reflog --date=iso
# 20f0c12 HEAD@{2024-01-15 01:47:33 +0800}: reset: moving to origin/release
# 9a3b7d2 HEAD@{2024-01-15 01:41:55 +0800}: merge feature/order-v2: Merge made by the 'ort' strategy.
# 7c02b88 HEAD@{2024-01-15 01:38:11 +0800}: checkout: moving from feature/order-v2 to main
# 6e1f4c5 HEAD@{2024-01-15 01:29:02 +0800}: commit: feat: 拆单逻辑
# 5f0a9b3 HEAD@{2024-01-15 00:56:40 +0800}: commit: fix: 并发库存扣减
# 2c4d7a1 HEAD@{2024-01-15 00:31:22 +0800}: commit: refactor: 状态机抽取

看到了吗?被我reset掉的9a3b7d2,那个包含3天工作的merge提交,正躺在reflog里。

恢复命令就一句:

git reset --hard 9a3b7d2
# HEAD is now at 9a3b7d2 整合所有改动
git log --oneline -3
# 9a3b7d2 (HEAD) Merge feature/order-v2
# 6e1f4c5 feat: 拆单逻辑
# 5f0a9b3 fix: 并发库存扣减

一切回来了。

reflog失效场景:git gc之后

reflog默认保留90天,过期对象会被git gc清掉。如果公司有定时gc任务,或者你手滑执行了git gc --prune=now,reflog救不了你。这世上还有最后一道防线——git fsck

git fsck --lost-found --no-reflogs
# dangling commit 9a3b7d2
# dangling blob 4f8eaa1
# dangling commit 6e1f4c5

然后逐个找回:

git show --stat 9a3b7d2
git merge 9a3b7d2   # 或者 cherry-pick

fsck扫描对象库里所有悬空的commit和blob,只要没被gc物理清除,就有救。

提交在,工作区被reset --hard清了怎么办

# 先搞清楚你最后一次提交的hash
git reflog | head -20
# 假设最后一次commit是6e1f4c5
# 但工作区里还没add的修改没了,这真的救不了。
# 唯一能救的是stash过的:
git stash list
git stash show -p stash@{0}
git stash apply stash@{0}

rerere:提前替你把冲突解决了

一个冷门但救命的功能:rerere(reuse recorded resolution),会把你的冲突解法记录下来,下次同样的冲突自动应用。

# 开启(写进全局配置)
git config --global rerere.enabled true

# 用法:什么额外操作都不用做
# 第一次merge冲突,手动解决,add,commit
# 下次同样的两个分支/改动再merge时,Git自动应用上次的解法
# 在merge、rebase、cherry-pick里都生效

我用它处理过最典型的情况:同一个feature反复对main做rebase,每次都在同一个文件的同一个位置冲突。第一次手动改完,后面全部自动解决,一次rebase从40分钟压缩到12分钟。

效果数据:整个救援流程的耗时

场景操作耗时结果
reset --hard 丢commitgit reflog + reset7分钟(其中5分钟在确认hash)完整恢复,零丢失
gc后丢commitgit fsck --lost-found15分钟恢复95%的提交,丢了几个blob
大分支冲突12个文件rerere自动解决第二次merge耗时从2.5h降到12分钟全部自动应用
误删未提交的工作区修改无解-无法恢复,只能认栽

避坑指南

这些坑我全踩过,代价是真实的加班和线上事故。

  • reset --hard之前没有任何提示。它不问你「确定吗」,直接丢。养成习惯:reset前先git stash listgit reflog看一眼有没有东西。或者干脆用git reset --hard前先打一个tag。
  • rebase是重写历史,push到远程后绝对别rebase。你会把同事的提交和自己的提交全搞乱,然后被迫用git push --force-with-lease擦屁股。这个命令同样危险,团队里应约定只在自己一个人的分支上用。
  • cherry-pick复制的是改动,不是上下文。它会把源提交的patch应用到当前分支,但和之前的关联提交(比如依赖的commit)可能丢失,导致逻辑不完整。挑选后跑一遍测试,别信代码审查者的感觉。
  • CI/CD里用yaml配置时,别直接跑git pull --rebase。很多Docker容器里没配置user.email,rebase会报错中断。在Jenkins或GitLab Runner里用git fetch && git reset --hard origin/main代替,干净且不产生merge commit。
  • 裸仓库(bare repo)没有reflog。你推送到远程分支的提交如果被force push覆盖,reflog在远程仓库里存在,但本地看不到。处理方式:任何一个拉了旧代码的同事,他本地的reflog里都留着旧提交,问他拿。
  • .git目录被误删,一切白搭。reflog、fsck全部失效。这题无解,奉劝大家别用rm -rf .git来「清理」。平时给.git目录做备份的人,我是真没见过一个。
  • git filter-repo或filter-branch改写历史后,如无必要,别找旧hash。改写后的旧commit还在对象库里,但所有引用都没了,fsck可能捞到一堆历史垃圾。确认不再需要时,用git gc --prune=now清掉,省的日后误用。

最后说点原理

Git保存的是对象(commit/blob/tree),分支、tag、HEAD都只是指向对象的引用。reflog记录的是引用变化的历史,它属于本地文件,不会推到远程,也不会被merge/rebase/checkout清掉,唯一的敌人是gc。所以,只要你的本地对象库里还留着那个commit,你就有机会把它捞回来。

git merge和git rebase的区别也不难记:merge的结果是「两条路汇成一个节点」,rebase的结果是「把一条路上的脚印全部挪到另一条路的终点」。说白了,看你的团队怎么理解历史。

记住两条实操法则:提交频率高一点,每个commit只干一件事;reset前先reflog看一眼。这篇文章里的所有命令在Git 2.43.0上验证过,老版本(2.20以下)reflog用法一致,fsck的参数不带任何变化。保存命令清单,下次遇到直接抄。