上周五下午 6 点 50,我正准备收拾东西下班。
leader 突然私信我:"你那个需求今天能上线吗?"
我看了眼代码,✅ 完美,rebase 完还没 push 呢。
二话不说,git push --force --------- 啪!覆盖了远程。
三分钟后,同事小王在群里发了一条:
"谁把我今天写的代码覆盖了???"
我的手指悬在键盘上,僵住了。
那是我周五下班前的最后一分钟,也是我最想原地消失的一分钟。
这就是 Git 的魔力时刻。
它让协作变得丝滑,也让翻车变得猝不及防。
Git 很强大,但越强大的工具,翻车现场越惨烈。
今天我就来给大家盘一盘,那些年我们用 Git 挖的坑。
翻车现场一:git push --force 覆盖远程历史
场景还原
周五下班前,你 rebase 完本地分支,准备 push。
bash
git push --force
然后你发现,同事张三的提交没了。
原来他今天下午 push 了三个 commit,而你 rebase 的时候把它们都"消灭"了。
办公室气氛瞬间凝固。
翻车原理
plaintext
远程仓库原本的样子:
A - B - C - D (张三的提交)
↑
你的分支
你 rebase 后:
A - B - E - F (你的新提交)
↑
远程
force push 后:
A - B - E - F ← 张三的 C - D 没了 😭
rebase 会"重写历史",你的 commit 变成了全新的 commit。
force push 直接告诉服务器:别管原来的历史,用我的!
然后张三今天的工作就消失在了时间的长河里。
救场方案
如果你的仓库有权限,还有救:
bash
# 1. 找到张三消失的提交
git reflog
# 输出类似:
abc123 HEAD@{5}: rebase (finish): returning to refs/heads/main
def456 HEAD@{6}: commit: 张三的提交
# 2. 创建一个临时分支保存他的提交
git branch zhangsan-backup def456
# 3. 合并回主分支
git checkout main
git merge zhangsan-backup
git push
正确做法
永远用 --force-with-lease 代替 --force:
bash
git push --force-with-lease
这个参数会检查远程是否有新的提交。如果有,它会拒绝 push 并告诉你:
"远程有新提交,请先拉取!"
翻车现场二:git reset --hard 误删本地提交
场景还原
"我想撤销上一次提交",我想。
bash
git reset --hard HEAD~1
等等,我刚才说的是撤销最后一次提交对吧?
对吧???
屏幕上显示:HEAD is now at abc123 昨天的提交
我刚才一周写的代码呢???
翻车原理
Git 的三个"区域":
plaintext
工作区 → 暂存区 → 仓库
(你的文件) (git add) (git commit)
--soft : 只动仓库,暂存区和工作区不变
--mixed : 动仓库+暂存区,工作区不变
--hard : 全动!仓库没了,暂存区没了,工作区也没了! 💥
--hard 会把你的工作目录直接清空,包括未提交的代码。
救场方案
只要你没 gc,这个世界还有救:
bash
git reflog
# 找到 hard reset 之前的 commit
git reset --hard HEAD@{1}
# 回来了!🎉
💡 小技巧:reflog 默认保留 90 天的操作记录
正确做法
永远不要在未备份的情况下 hard reset
方案 A:用 git stash 先暂存
bash
git stash
git reset --hard HEAD~1
# 后悔了?
git stash pop
方案 B:用 --soft 或 --mixed
bash
# 如果只是想修改 commit 消息
git commit --amend
# 如果想让代码保留在暂存区
git reset --soft HEAD~1
翻车现场三:merge 时的冲突灾难
场景还原
终于等到 feature 分支合并到 main 了!
打开冲突文件一看:
<<<<<<<
user.setName("张三");
=======
user.setName("李四");
>>>>>>> feature/add-validation
我心里想:"张三是对的,选这个!"
然后点了"接受当前分支"。
一周后,用户发现所有名字都显示成了别人的。
翻车分析
冲突的本质是:两个分支都修改了同一段代码。
Git 不知道谁的改动是正确的,只能让你来选择。
但选择需要理解上下文,而不是凭直觉。
救场方案
bash
# 还没 commit,先停!
git merge --abort
然后重新审视冲突:
- 理解两个分支分别改了什么
- 咨询改动涉及的同事
- 确定正确的合并结果
正确做法
解决冲突的 checklist:
plaintext
□ 理解每个冲突的来源
□ 知道为什么两边都会改这段代码
□ 确定哪个改动是"对的",或者都需要
□ 手动编辑合并,而不是盲目选择
□ 测试通过后再 commit
翻车现场四:在错误的分支上开发了一整天
场景还原
周一早上,需求评审完,开始干活。
我自信地敲下:
bash
git checkout main
git pull
# 开始写代码...
下午 5 点,代码写完了,准备提交:
bash
git status
On branch main
等等,main???
需求说要开新分支 feature/user-profile 的吧???
我今天写的 20 个文件,全在 main 上。
救场方案
方案一:stash 大法(推荐)
bash
# 1. 暂存所有改动
git stash
# 2. 创建新分支并切换
git checkout -b feature/user-profile
# 3. 恢复改动
git stash pop
方案二:cherry-pick(如果你已经有 commit)
bash
# 1. 提交到 main
git add .
git commit -m "今天的改动"
# 2. 创建新分支
git branch feature/user-profile
# 3. 在 main 上 reset
git checkout main
git reset --hard HEAD~1
# 4. 在新分支上 cherry-pick
git checkout feature/user-profile
git cherry-pick main
正确做法
每次开发前,必做三件事:
plaintext
1. git branch # 看下当前在哪个分支
2. git branch -a # 看下远程有哪些分支
3. 确认无误后再开始写代码
翻车现场五:把密码/密钥提交到了仓库
场景还原
凌晨 2 点,deadline 前最后一小时。
".env 文件?先提交上去,明天再改!"
bash
git add .env
git commit -m "add config"
git push
第二天醒来,AWS 控制台收到警报:有人在用你的密钥挖矿。
救场方案
方案一:使用 BFG Repo-Cleaner(推荐)
bash
# 1. 安装 BFG
# brew install bfg (macOS)
# 2. 删除文件的所有历史记录
bfg --delete-files .env
# 3. 强制更新所有分支
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push --force
方案二:使用 git filter-branch(慢但自带)
bash
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch .env' \
--prune-empty --tag-name-filter cat -- --all
重要:改完密钥!
密钥已经泄露,光删除历史没用,必须重新生成。
正确做法
三道防线:
plaintext
防线 1:.gitignore
echo ".env" >> .gitignore
防线 2:pre-commit 钩子
npx husky add .husky/pre-commit "git diff --cached --name-only | grep -q '\.env$' && echo 'No .env files!' && exit 1"
防线 3:git-secrets(AWS 官方工具)
git secrets --install
git secrets --register-aws
翻车现场六:rebase 半途而废,代码处于混乱状态
场景还原
bash
git rebase -i HEAD~5
进入了交互式界面,改了一些 commit 顺序。
然后发现:"好像搞乱了?"
按 Ctrl+C 退出?
不管用了。整个 rebase 处于中间状态,不知道怎么办。
救场方案
万能命令:
bash
git rebase --abort
是的,就这么简单。
它会放弃当前 rebase 的一切操作,恢复到 rebase 开始之前的状态。
正确做法
rebase 前先备份:
bash
# 1. 创建备份分支
git branch backup-before-rebase
# 2. 放心 rebase
git rebase -i HEAD~5
# 3. 如果搞砸了
git rebase --abort
git checkout backup-before-rebase
翻车现场七:git cherry-pick 导致重复提交
场景还原
有个 bug 在 release-1.0 分支修复了,需要合到 main。
bash
git checkout release-1.0
git log --oneline
# abc123 fix: 修复用户无法登录的 bug
git checkout main
git cherry-pick abc123
两周后,release-1.0 合到 main:
bash
git checkout main
git merge release-1.0
同一个 bug 修复,被合进来两次。 代码开始出现奇怪的冲突。
翻车原理
plaintext
cherry-pick 的本质是:创建一个"内容相同但 SHA 不同"的新 commit
release-1.0: abc123 "fix: 修复用户无法登录"
main 上的: def456 "fix: 修复用户无法登录" ← 不同的 SHA
merge 的时候,Git 不知道这两个是同一个改动
救场方案
方案一:revert 掉重复的那个
bash
git revert def456
方案二:用 merge 而不是 rebase 来同步 release 分支
bash
# 不要 cherry-pick release 分支的 commit
# 直接 merge release 分支
git merge release-1.0
正确做法
cherry-pick vs merge 的选择策略:
plaintext
cherry-pick: 需要挑几个特定 commit
merge: 需要把整个分支的改动都合过来
如果要把 release 的改动合到 main:
→ 直接 merge release,不要 cherry-pick
终极救场指南:Git 急救箱
| 翻车场景 | 急救命令 | 预防措施 |
|---|---|---|
| force push 覆盖同事代码 | git reflog + 恢复分支 |
用 --force-with-lease |
| hard reset 清空代码 | git reflog + git reset --hard |
先 git stash 或用 --soft |
| merge 选错冲突方向 | git merge --abort |
理解上下文,不要盲选 |
| 在错误分支开发 | git stash + 切换分支 |
开发前先 git branch |
| 密码/密钥泄露 | BFG Repo-Cleaner + 改密码 | .gitignore + pre-commit |
| rebase 搞乱了 | git rebase --abort |
rebase 前备份分支 |
| cherry-pick 导致重复 | git revert 重复的 commit |
整分支用 merge |
| 误删分支 | git reflog 找回来 |
不要用 -D 删除分支 |
万能保命原则
永远先 reflog 看一眼再操作!
bash
git reflog
它会记录你所有的操作历史,包括"你以为丢了"的东西。
在 Git 的世界里,很少有真正的删除。
翻车不可怕,不知道怎么救才可怕
写这篇文章的时候,我回忆起每一个翻车现场。
有在客户面前 force push 覆盖 leader 代码的尴尬。
有凌晨 3 点发现 .env 被 push 出去的心跳加速。
有 merge 完才发现线上出了 bug 的绝望。
但后来我发现,翻车本身不可怕。
可怕的是:
- 不知道 force push 会覆盖别人代码
- 不知道 hard reset 会清空工作目录
- 不知道 reflog 能救命
技术路上的坑,踩过才知道疼。
但知道怎么爬起来,比什么都重要。