Git故障排除:遇到报错不再慌的急诊手册
# 第7章 · 故障排除
# 一、连环崩溃
小李正准备关电脑下班。这周他独立完成了一个大功能,代码已经 push,PR 也合并了。他最后看了一眼终端,随手敲了 git branch -D feature/旧实验,清理一下本地分支。
然后他愣住了——他删的不是 feature/旧实验,而是 feature/新支付。那个还没合并、写了四天的分支。
手心出汗,大脑空白。他下意识敲了 git log——那些 commit 去哪了?
问题不是 Git 会不会出错,而是出错了你知道怎么救。
本章就是你的 Git 急诊室。七个高频「症状」,每个都配诊断 + 处方 + 预防。
# 二、头掉了
# 2.1 发生了什么?
mkdir er-lab && cd er-lab && git init
echo "v1" > file.txt
git add . && git commit -m "v1"
echo "v2" >> file.txt
git add . && git commit -m "v2"
echo "v3" >> file.txt
git add . && git commit -m "v3"
# 你只是想去 v1 看一眼
git checkout 7a1b2c3 # 用 git log 找到 v1 的 hash
# Git 输出:
# Note: switching to '7a1b2c3'.
# You are in 'detached HEAD' state.
# HEAD is now at 7a1b2c3 v1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
你慌了:这是什么?我的代码丢了? 不,你的代码没丢。
# 2.2 诊断
还记得第 3 章说的吗?HEAD 是一个指针,指向你当前所在的分支。 正常情况下 HEAD → 分支名 → commit:
HEAD → master → 8f3a2b1 (v3)
当你 git checkout <commit-hash> 时,HEAD 直接指向了一个 commit,中间没有分支名:
HEAD → 7a1b2c1 (v1)
这就是「Detached HEAD」——头掉了,直接挂在 commit 上,中间少了一个分支名。
# 2.3 治疗方案
# 场景 A:你只是想看一眼旧代码就回去
git switch master # 回到原来的分支,万事大吉
# 场景 B:你在 detached HEAD 上做了修改 + 提交了,想保留这些改动
git checkout 7a1b2c3
echo "detached 上的改动" >> file.txt
git add . && git commit -m "commit on detached HEAD"
# 现在 HEAD 指向这个新 commit,但没有分支名
# 这个时候如果你 git switch master,那个 commit 就会「丢失」
# (其实在 reflog 里,但不好找)
# ✅ 正确做法:先给它建个分支!
git switch -c rescue/临时改动
# 或者:
git branch rescue/临时改动 # 在切走之前先把 commit 挂到一个分支上
git switch master
git log --oneline rescue/临时改动
# commit 安全保留在 rescue/临时改动 分支上了
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
场景 C:你已经切走了,才发现丢了 commit
git reflog
# 找到 detached HEAD 上的那个 commit hash
git switch -c rescue/找回的代码 <hash>
# 🎉 找回来了
2
3
4
💡 detached HEAD 不恐怖——你只是站在了一个没有路标的地方。要么立个路标(建分支),要么回到有路标的路上(切回去)。
# 三、冲突五种
冲突本身在第 3 章已经讲过了。这里聚焦你会在哪五种情况下遇到冲突,以及每种情况的特殊处理方式。
# 场景 1:标准 merge 冲突(最常见)
# 两个分支改了同一文件的同一行
git merge feature/xxx
# CONFLICT (content): Merge conflict in file.js
# 处理:
# 1. 编辑冲突文件,删掉 <<<<<< / ====== / >>>>>> 标记
# 2. git add <file>
# 3. git commit -m "merge: 解决冲突"
2
3
4
5
6
7
8
# 场景 2:pull 时的冲突
git pull
# CONFLICT
# 原因:远程有人推了新代码,和你的本地改动冲突
# 处理:同 merge 冲突
# 预防:永远先 git fetch → git diff → 再决定 merge 还是 rebase
2
3
4
5
6
# 场景 3:rebase 时的冲突(比 merge 烦人)
git rebase master
# CONFLICT — 而且每个 commit 都可能冲突!
# 区别:merge 是一次性解决全部冲突;rebase 是逐个 commit 解决
# 处理:
# 1. 解决当前 commit 的冲突
# 2. git add <file>
# 3. git rebase --continue ← 不是 git commit!
# 4. 下一个 commit 又冲突 → 重复 1-3
# 5. 直到所有 commit 处理完
# 如果受不了了:
git rebase --abort # 回到 rebase 之前,万事大吉
2
3
4
5
6
7
8
9
10
11
12
13
# 场景 4:cherry-pick 时的冲突
git cherry-pick abc123
# CONFLICT
# 处理:
# 1. 解决冲突 → git add
# 2. git cherry-pick --continue
# 放弃:
git cherry-pick --abort
2
3
4
5
6
7
8
9
# 场景 5:stash pop 时的冲突
git stash pop
# CONFLICT
# 原因:stash 的内容和当前代码有冲突
# 处理:同 merge 冲突,手动解决
# 注意:即使 pop 有冲突,stash 不会被自动删除!
# 如果不想处理了:
git checkout --theirs <file> # 用 stash 的版本
git checkout --ours <file> # 用当前的版本
2
3
4
5
6
7
8
9
# 冲突速查表
| 你在做什么 | 冲突了怎么办 | 想放弃怎么办 |
|---|---|---|
git merge | 编辑 → add → commit | git merge --abort |
git pull | 同上 | git merge --abort |
git rebase | 编辑 → add → rebase --continue | git rebase --abort |
git cherry-pick | 编辑 → add → cherry-pick --continue | git cherry-pick --abort |
git stash pop | 编辑 → add | git checkout --ours <file> 保留当前版 |
# 四、误删分支
# 4.1 误删本地分支
# 模拟灾难
git switch -c feature/关键功能
echo "四天的工作成果" > important.js
git add . && git commit -m "feat: 关键功能"
git switch master
git branch -D feature/关键功能 # 💀 手滑!
# 急救:
git reflog --date=iso
# 输出:
# abcdef1 HEAD@{0}: checkout: moving from feature/关键功能 to master
# ↑ 找到在 feature 分支上的最后一个 commit!
git switch -c feature/关键功能 abcdef1
# 🎉 分支回来了!important.js 内容完好无损!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 4.2 误删远程分支
# 误删了远程分支
git push origin --delete feature/关键功能
# 急救(谁都可以救,因为 Git 是分布式的):
# 方法1:如果你本地还有这个分支
git push origin feature/关键功能
# 方法2:找团队里任何 fetch 过这个分支的同事
# 同事那边:
git reflog show remotes/origin/feature/关键功能
# 找到最后一个 commit hash
git push origin <hash>:refs/heads/feature/关键功能
# 方法3:在 GitHub 网页上
# 仓库 → Pull Requests → Closed → 找到合并过这个分支的 PR
# 点击 "Restore branch"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
💡 只要有人在本地 fetch 过这个分支,它的 commit 就还在某个人的
.git/objects里。
# 五、误提大文件
# 5.1 问题
# 你不小心提交了一个 200MB 的视频文件
git add demo.mp4
git commit -m "feat: 添加演示视频"
git push
# 团队抱怨:clone 太慢了!仓库膨胀到 500MB!
2
3
4
5
6
# 5.2 如果还没 push——简单解决
git reset --soft HEAD~1 # 撤销 commit,文件留在暂存区
git restore --staged demo.mp4 # 从暂存区移除
echo "*.mp4" >> .gitignore # 加入忽略
git add .gitignore
git commit -m "chore: 忽略视频文件"
2
3
4
5
# 5.3 如果已经 push 了——需要清理历史
# 方法1:git filter-branch(传统方式)
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch demo.mp4' \
--prune-empty -- --all
# 方法2:git filter-repo(推荐,更快更安全)
# 先安装:brew install git-filter-repo
git filter-repo --path demo.mp4 --invert-paths
# 清理后强制推送
git push --force --all
git push --force --tags
# ⚠️ 通知团队:所有人必须重新 clone!
# git filter-branch / filter-repo 重写了整个历史
# 团队成员的旧仓库和新仓库不兼容
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 5.4 更好的方案:BFG Repo-Cleaner
# 安装
brew install bfg
# 清理所有大于 100M 的文件
bfg --strip-blobs-bigger-than 100M
# 清理特定文件
bfg --delete-files demo.mp4
# 清理后
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force
2
3
4
5
6
7
8
9
10
11
12
13
⚠️ 三件事必须做:
- 通知所有团队成员,他们的本地仓库需要重新 clone
- 确认有没有 CI/CD 配置依赖旧 commit hash(如果用了 git submodule 或指向特定 commit 的部署脚本)
- 检查所有 PR 和 issue 中引用的旧 commit hash 是否还有效
# 六、无关历史
# 6.1 什么时候会遇到?
# 场景:本地有一个项目,GitHub 新建了一个空仓库(带 README.md)
# 你执行:
git remote add origin https://github.com/user/repo.git
git pull origin master
# 输出:
# fatal: refusing to merge unrelated histories
2
3
4
5
6
7
8
# 6.2 原因
本地仓库和远程仓库是两个独立创建的 Git 仓库——它们没有共同的祖先 commit。Git 认为这是两个不同的项目,拒绝合并。
# 6.3 解决方案
# 如果你确定这就是同一个项目(只是两个仓库独立初始化了)
git pull origin master --allow-unrelated-histories
# 如果有冲突,解决后:
git add .
git commit -m "merge: 合并独立仓库历史"
git push origin master
2
3
4
5
6
7
💡 预防:在 GitHub 创建新仓库时,不要勾选 "Initialize this repository with a README"。直接在本地
git init→git remote add→git push。
# 七、SSH 失败
# 7.1 症状
git push origin master
# 输出:
# git@github.com: Permission denied (publickey).
# fatal: Could not read from remote repository.
2
3
4
5
# 7.2 排查清单
# Step 1:你有 SSH 密钥吗?
ls ~/.ssh/
# 应该看到:id_rsa 和 id_rsa.pub(或 id_ed25519 和 id_ed25519.pub)
# 如果没有,生成一个:
ssh-keygen -t ed25519 -C "your_email@example.com"
# 一路回车(不要设密码,或者设了记住它)
# Step 2:密钥添加到 ssh-agent 了吗?
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
# Step 3:公钥添加到 GitHub 了吗?
cat ~/.ssh/id_ed25519.pub
# 复制输出 → GitHub → Settings → SSH and GPG keys → New SSH key → 粘贴
# Step 4:连接测试
ssh -T git@github.com
# 成功输出:Hi username! You've successfully authenticated...
# Step 5:仓库用的是 SSH URL 而不是 HTTPS?
git remote -v
# 应该是:git@github.com:user/repo.git
# 如果是 https://github.com/...,改成 SSH:
git remote set-url origin git@github.com:user/repo.git
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 7.3 常见坑
| 问题 | 原因 | 解决 |
|---|---|---|
Permission denied | 公钥没加到 GitHub | 检查 Step 3 |
Could not resolve hostname | 网络 / DNS | 检查网络、换 HTTPS 试试 |
| 每次都要输密码 | ssh-agent 没跑或没 add | 执行 Step 2 |
| 换了电脑用不了 | 每台电脑需要自己的密钥 | 重新 ssh-keygen + 加到 GitHub |
| 公司代理拦截 | 公司网络封了 SSH 端口 | 用 HTTPS + personal access token |
# 八、拉取失败
# 8.1 发生了什么?
git pull origin master
# 输出:Already up to date.
# 但你去 GitHub 网页看,明明有新的 commit!
2
3
4
# 8.2 原因:引用没更新
你的 origin/master 引用可能过期了。
# Step 1:确认你看到的是最新的远程状态
git fetch origin
# Step 2:比较本地和远程
git log --oneline master..origin/master
# 如果远程有新东西,这里会显示
# Step 3:再 pull
git pull origin master
2
3
4
5
6
7
8
9
# 8.3 本质原因
你可能在错误的远程仓库上操作——用 git remote -v 确认 origin 指向了正确的 URL。
# 九、综合实战
# 场景设定
你接手了一个祖传项目。仓库历史悠久,Git 状态混乱。你的任务是把它整理成「可以安全协作」的状态。
# 实战开始
mkdir legacy-rescue && cd legacy-rescue && git init
# 模拟祖传项目的问题
echo "重要业务逻辑" > app.js
git add . && git commit -m "初版"
# 问题 1:历史里有不应该存在的大文件
dd if=/dev/zero of=huge-log.log bs=1m count=50 2>/dev/null
git add huge-log.log && git commit -m "加了日志"
# 然后发现不该提交,又 commit 了一次删除
git rm huge-log.log && git commit -m "删了日志"
# 但 huge-log.log 还在历史里!
# 问题 2:有人直接写了秘密在代码里
echo "API_KEY=sk-1234567890abcdef" >> app.js
git add . && git commit -m "加了 API 功能"
# 然后发现不该公开 API key,改掉了
sed -i '' '/API_KEY/d' app.js
git add . && git commit -m "去掉 API key"
# 但历史里还留着!
# 问题 3:commit message 乱七八糟
echo "改" >> app.js
git add . && git commit -m "改"
echo "再改" >> app.js
git add . && git commit -m "再改"
echo "好了" >> app.js
git add . && git commit -m "好了"
# 看看现状
git log --oneline
# 输出:
# abc0003 好了
# abc0002 再改
# abc0001 改
# abc0000 去掉 API key
# abc000-1 加了 API 功能
# abc000-2 删了日志
# abc000-3 加了日志
# abc000-4 初版
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 🎯 救急流程
Step 1:清理大文件
# 用 git filter-branch 抹掉 huge-log.log 的痕迹
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch huge-log.log' \
--prune-empty -- --all
2
3
4
Step 2:清理泄露的 API key
# 如果不想整个仓库被别人看到 API key
# 改掉 app.js 里那行,然后清理历史
# 但假设你已经改了代码(去掉了 API_KEY 那行),只需要:
# filter-branch 已经清理了,这里确认一下
git log -p | grep API_KEY
# 如果还有残留,继续 filter-branch 指定内容
2
3
4
5
6
7
Step 3:整理 commit message
# 把 "改"、"再改"、"好了" 三个垃圾 message 整理成一个
git rebase -i HEAD~3
# pick abc0001 改 → reword feat(app): 添加业务逻辑增强
# squash abc0002 再改 → squash
# squash abc0003 好了 → squash
2
3
4
5
6
Step 4:确认干净了
git log --oneline
# 输出:
# xyz0001 feat(app): 添加业务逻辑增强
# xyz0000 去掉 API key ← 可以再整理
# xyz000-1 加了 API 功能
# xyz000-2 初版
# 确认没有大文件
git count-objects -vH
# size-pack 应该很小(几百 KB,而不是 50MB)
2
3
4
5
6
7
8
9
10
Step 5:添加 .gitignore
cat > .gitignore << 'EOF'
*.log
.env
node_modules/
dist/
.DS_Store
EOF
git add .gitignore && git commit -m "chore: 添加 gitignore"
2
3
4
5
6
7
8
9
💡 祖传项目抢救原则:先止血(清大文件、密钥)→ 再整理(rebase 规范 message)→ 最后加护栏(.gitignore、保护分支、CI)。
# 十、二十场景速查
| 场景 | 命令 |
|---|---|
| 忘了刚才改了什么 | git diff |
| 忘了最近做了哪些提交 | git log --oneline -10 |
| 想回退一个文件到上次 commit | git restore <file> |
| add 错了一个文件 | git restore --staged <file> |
| commit message 写错了 | git commit --amend -m "新消息" |
| commit 漏了文件想补进去 | git add <file> && git commit --amend |
| 想撤销最近一次 commit(没 push) | git reset --soft HEAD~1 |
| 想撤销最近一次 commit(已 push) | git revert HEAD |
| 没 push 的 commit 丢了 | git reflog → 找到 hash → git reset --hard <hash> |
| 修改到一半要切去修 bug | git stash -u -m "备注" |
| 恢复 stash | git stash pop |
| 只想把某个 commit 搬到另一分支 | git cherry-pick <hash> |
| 不知道哪个 commit 引入的 bug | git bisect start → good/bad |
| HEAD 掉了(detached) | git switch -c 新分支名 |
| 合并到一半想反悔 | git merge --abort |
| rebase 到一半想反悔 | git rebase --abort |
| 本地分支比远程多/少了什么 | git log --oneline master..origin/master |
| push 被拒绝(远程有新代码) | git fetch → git merge 或 git rebase → 再 push |
| 误删分支 | git reflog → 找到最后一个 commit → git switch -c <名> <hash> |
| 想永久删除历史中的大文件/密钥 | git filter-branch 或 git filter-repo |
# 十一、本章回顾
| 症状 | 根因 | 急救措施 |
|---|---|---|
| Detached HEAD | HEAD 直接指向 commit,没有分支 | git switch -c 新分支 或 git switch master |
| merge conflict | 同一文件的同一行被不同分支修改 | 编辑 → git add → git commit / git rebase --continue |
| 误删分支 | 分支指针被删,但 commit 还在 | git reflog → git switch -c 分支名 <hash> |
| 大文件在历史里 | commit 里有大 blob 对象 | git filter-repo 或 git filter-branch |
| unrelated histories | 两个独立初始化的仓库没有共同祖先 | git pull --allow-unrelated-histories |
| SSH Permission denied | 公钥没配置或没加到 GitHub | 按 7.2 节排查清单逐项检查 |
| 拉新代码拉不下来 | origin/master 引用过期 | git fetch origin 先刷新引用 |
🎯 最终心法——Git 永远不会真正删除你的数据。只要 commit 过,就能找回来。害怕出错就多 commit(commit 是免费的),少 force push。
🏃 全书最后一章预告:前面七章已经把 Git 从入门到团队协作讲完了。最后一章是一个场景速查地图——不按知识点编排,而是按「你遇到了什么情况」直接给出命令 + 参数 + 注意事项。适合放在收藏夹,日常开发随时翻阅。
# 📎 本章涉及的命令速查
# === Detached HEAD ===
git switch <branch-name> # 回到分支
git switch -c <new-branch> # 给 detached HEAD 的状态建分支
# === 各种放弃 ===
git merge --abort # 放弃合并
git rebase --abort # 放弃变基
git cherry-pick --abort # 放弃 cherry-pick
# === 清理历史 ===
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch <file>' --prune-empty -- --all
git filter-repo --path <file> --invert-paths # 更推荐
# === SSH 排查 ===
ls ~/.ssh/ # 查看密钥文件
ssh-keygen -t ed25519 -C "邮箱" # 生成新密钥
cat ~/.ssh/id_ed25519.pub # 查看公钥
ssh -T git@github.com # 测试连接
# === 其他应急 ===
git pull --allow-unrelated-histories # 强制合并两个无关仓库
git remote -v # 确认远程仓库 URL
git remote set-url origin <new-url> # 修改远程 URL(HTTP ↔ SSH)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24