冲突,是 Git 新手最怕的词
"完了,冲突了!怎么办!"
这是每个 Git 新手都会经历的恐慌时刻。终端里突然冒出 <<<<<<<、=======、>>>>>>> 这些奇怪的符号,感觉自己把代码搞坏了。
但你知道吗?冲突是 Git 在保护你,而不是在为难你。
Git 发现两个人同时改了同一个文件的同一行,它不知道该用谁的版本,所以停下来,让你来决定。这不是 bug,这是一个为你保驾护航的功能。
这一篇,我们不仅会教你如何优雅地解决冲突,还会教你版本回退的各种技巧------让你即使真的搞砸了,也能从容地"时光倒流"。
1. 冲突是怎么产生的?
冲突产生的根本原因:两个分支修改了同一个文件的同一行(或相邻行),Git 无法自动判断使用哪个版本。
用图解来理解冲突
再举一个更贴近实际的例子:
共同祖先代码(main 分支):
javascript
function calculateTotal(items) {
let total = 0;
for (let item of items) {
total = total + item.price;
}
return total;
}
张三在 feature/discount 分支上修改了同一段代码:
javascript
function calculateTotal(items, discount) {
let total = 0;
for (let item of items) {
total = total + item.price;
}
return total * (1 - discount); // 张三加了折扣
}
李四在 feature/tax 分支上也修改了同一段代码:
javascript
function calculateTotal(items) {
let total = 0;
for (let item of items) {
total = total + item.price * 1.13; // 李四加了税
}
return total;
}
张三加了折扣参数,李四加了税率计算。当这两个分支都试图合并到 main 时,Git 不知道该怎么处理这两个不同的改动------冲突就发生了。
2. 冲突文件的结构解读
当一个文件产生冲突时,Git 会在文件中插入特殊的标记。来看一个典型的冲突文件:
ini
<<<<<<< HEAD
function calculateTotal(items) {
let total = 0;
for (let item of items) {
total = total + item.price;
}
return total;
}
=======
function calculateTotal(items, discount) {
let total = 0;
for (let item of items) {
total = total + item.price;
}
return total * (1 - discount);
}
>>>>>>> feature/discount
冲突标记的含义
markdown
<<<<<<< HEAD
[当前分支的代码 ------ 你要合并到的目标分支]
=======
[你正在合并进来的分支的代码]
>>>>>>> feature/discount
逐一解释:
<<<<<<< HEAD:冲突区域的开始标记。HEAD表示你当前所在的分支(即目标分支)=======:分隔线,上面是当前分支的代码,下面是合并进来的分支的代码>>>>>>> feature/discount:冲突区域的结束标记,后面跟的是被合并的分支名
解决冲突的步骤
解决冲突就是:删除冲突标记,保留(或修改成)你想要的最终代码。
比如,你决定同时保留张三和李四的改动------函数既支持折扣又包含税费:
javascript
function calculateTotal(items, discount = 0) {
let total = 0;
for (let item of items) {
total = total + item.price * 1.13;
}
return total * (1 - discount);
}
然后做三件事:
- 删掉所有冲突标记 (
<<<<<<<、=======、>>>>>>>) - 保存文件
- 标记冲突已解决:
bash
git add 冲突文件.js
git commit -m "merge: 解决 calculateTotal 的合并冲突,同时保留折扣和税费功能"
3. 冲突解决的完整流程
命令行完整步骤
bash
# 1. 尝试合并
git merge feature/new-function
# Git 报错:
# Auto-merging src/app.js
# CONFLICT (content): Merge conflict in src/app.js
# Automatic merge failed; fix conflicts and then commit the result.
# 2. 查看哪些文件有冲突
git status
# 输出中会看到:
# Unmerged paths:
# both modified: src/app.js
# 3. 打开冲突文件,手动解决冲突(用编辑器操作)
# 4. 检查是否解决干净了
grep -r "<<<<<<< HEAD" . # 搜索是否还有未解决的冲突标记
# 5. 标记冲突已解决
git add src/app.js
# 6. 提交合并
git commit -m "merge: 合并 new-function 分支,解决 app.js 冲突"
# 或者,如果你想放弃这次合并
git merge --abort
VS Code 中解决冲突
如果你用 VS Code,冲突解决会更加直观。当文件有冲突时,VS Code 会在编辑器中高亮显示冲突区域,并提供四个按钮:
- Accept Current Change :保留当前分支的代码(
HEAD部分) - Accept Incoming Change:保留合并进来的分支的代码
- Accept Both Changes:两段代码都保留(一上一下)
- Compare Changes:对比两边差异
点击按钮后,VS Code 会自动删除冲突标记,非常方便。
4. git reset ------ 时光倒流三兄弟
git reset 是 Git 中最强大的"后悔药"。它能让你回到过去的任何一个 commit。
三个参数:--soft、--mixed、--hard
这三个参数决定了 git reset 的"回溯程度":
| 参数 | 本地仓库 | 暂存区 | 工作区 | 通俗解释 |
|---|---|---|---|---|
--soft |
✅ 回退 | ❌ 保留 | ❌ 保留 | 撤销 commit,但保留 add 和你的修改 |
--mixed(默认) |
✅ 回退 | ✅ 回退 | ❌ 保留 | 撤销 commit 和 add,但保留你的修改 |
--hard |
✅ 回退 | ✅ 回退 | ✅ 回退 | 彻底回到过去,所有没提交的修改都会丢失 |
图解三种 reset
假设你有三个 commit:A → B → C,当前 HEAD 指向 C。
git reset --soft B
csharp
工作区:C的修改还在(文件没变)
暂存区:C的修改还在(git add 的状态保留)
仓库:HEAD 指向 B
使用场景:你想撤销最近一次 commit,但保留所有修改,想重新修改后再提交。
bash
# 撤销最后一次 commit,所有修改回到暂存区
git reset --soft HEAD~1
# 现在你可以修改代码,然后重新 commit
git commit -m "修改后的新提交"
git reset --mixed B(默认)
css
工作区:C的修改还在(文件没变)
暂存区:清空了(和B保持一致)
仓库:HEAD 指向 B
使用场景:你想撤销 commit 和 add,所有修改回到工作区,让你重新选择要提交哪些文件。
bash
# 撤销最后一次 commit,修改回到工作区
git reset HEAD~1
# 现在你需要重新选择文件
git add 文件1.js 文件2.js
git commit -m "新的提交"
git reset --hard B
css
工作区:变成B的状态(C的所有修改都丢了!)
暂存区:变成B的状态
仓库:HEAD 指向 B
使用场景:你想彻底放弃最近的修改,回到之前的状态。
bash
# 彻底回到上一个版本,放弃所有修改
git reset --hard HEAD~1
⚠️ 警告 :
--hard是不可逆的(除非用了git reflog,见后续章节)。在使用之前,请再三确认你不需要这些修改了。
指定回退目标的方式
bash
# 回到上一个 commit
git reset --soft HEAD~1
# 回到上上个 commit
git reset --soft HEAD~2
# 回到指定的 commit(使用哈希值)
git reset --soft abc1234
# 回到某个分支的最新 commit
git reset --soft origin/main
5. git revert ------ 安全的撤销方式
git reset 会改写历史(尤其是 --hard),这在团队协作中是很危险的。如果你已经把 commit 推到了远程仓库,git reset 后再 git push --force 可能会把同事的提交也干掉。
这时候应该用 git revert。
git revert 的原理
git revert 不删除任何历史记录,而是创建一个新的 commit,这个新 commit 的内容是"撤销某个旧 commit 的修改"。
B 引入了一个 bug。你在 D 执行 git revert B,D 的内容就是"B 的反操作"------B 添加的代码 D 删除,B 删除的代码 D 恢复。
结果是:
- 历史记录完整保留了 B(没有被删除)
- 代码状态回到了 B 之前(因为 D 抵消了 B)
- 整个过程是透明的、可追溯的
基本用法
bash
# 撤销某次 commit
git revert abc1234
# 撤销最后一次 commit
git revert HEAD
# 撤销但不自动提交(让你检查后再提交)
git revert --no-commit abc1234
# 撤销一个合并提交(需要指定保留哪个父分支)
git revert -m 1 abc1234
-m 1 参数:合并提交有两个父提交,你需要指定"保留哪个"。通常 -m 1 表示保留目标分支(如 main)的状态。
git reset 和 git revert 的对比
| git reset | git revert | |
|---|---|---|
| 原理 | 移动 HEAD 指针,丢弃提交 | 创建新提交来抵消旧提交 |
| 历史记录 | 改写历史 | 保留完整历史 |
| 是否产生新提交 | 否 | 是 |
| 已 push 后能用吗 | 不建议(需要 force push) | 可以(正常 push) |
| 适用场景 | 本地未 push 的提交 | 任何需要撤销的场景 |
| 安全性 | 有风险(尤其是 --hard) | 安全 |
面试必考 :
git reset和git revert的区别。
实际场景判断:该用 reset 还是 revert?
perl
场景1:你在本地做了3个提交,还没 push,发现第三个提交有问题
→ 用 git reset --soft HEAD~1(回到第二个提交,保留修改)
或 git reset --hard HEAD~1(彻底放弃第三个提交)
场景2:你已经 push 到远程了,发现刚才的提交引入了一个 bug
→ 用 git revert HEAD(创建新提交来撤销)
场景3:合并了一个分支到 main,但发现这个功能有严重 bug,需要整体撤销
→ 用 git revert -m 1 <merge-commit-hash>
6. git reflog ------ 终极救命稻草
reflog 是 Reference Log(引用日志)的缩写。它记录了你的 HEAD 和分支引用在本地仓库中的所有移动历史。
简单说:只要你的操作发生在本地,git reflog 都能看到,你就能找回来。
使用场景
bash
# 你不小心执行了
git reset --hard HEAD~3
# 完蛋了!三天的代码没了!
# 别慌!用 reflog
git reflog
输出:
perl
abc1234 HEAD@{0}: reset: moving to HEAD~3
def5678 HEAD@{1}: commit: 添加了重要的新功能 ← 你刚才丢失的提交!
fgh9012 HEAD@{2}: commit: 修复了bug
ijk3456 HEAD@{3}: commit: 更新文档
看,你之前的所有提交都还在 reflog 里!现在可以恢复了:
bash
# 回到刚才丢失的提交
git reset --hard def5678
# 或者
git reset --hard HEAD@{1}
reflog 的其他用途
bash
# 恢复到执行某个操作之前的状态
git reset --hard HEAD@{1}
# 删除了一个分支,想恢复
git branch 恢复的分支名 HEAD@{n}
# 查看指定引用的 reflog
git reflog show feature/login
# 查看详细 reflog
git reflog --date=iso
reflog 的局限性
- reflog 只记录本地的操作。如果操作发生在远程仓库,本地 reflog 看不到
- reflog 默认保留90天(不可达的条目)或永久(可达的条目)
git clone不带 reflog 记录
7. git restore ------ 恢复文件
前面提到过,git restore 是 Git 2.23 引入的新命令,用于替代 git checkout -- <file>。
从暂存区恢复
bash
# 把文件从暂存区移回工作区(取消 git add)
git restore --staged 文件名
# 相当于 git reset HEAD 文件名
# 恢复所有文件
git restore --staged .
从仓库恢复
bash
# 丢弃工作区的修改,恢复到最近一次 commit 的状态
git restore 文件名
# 丢弃所有修改(危险!)
git restore .
# 恢复到指定 commit 的状态
git restore --source=abc1234 文件名
新老命令对照表
| 操作 | 旧命令 | 新命令(推荐) |
|---|---|---|
| 切换分支 | git checkout <branch> |
git switch <branch> |
| 创建并切换 | git checkout -b <branch> |
git switch -c <branch> |
| 取消暂存 | git reset HEAD <file> |
git restore --staged <file> |
| 丢弃修改 | git checkout -- <file> |
git restore <file> |
8. git checkout -- ------ 丢弃工作区修改(老方法)
如果你还在用老版本 Git,或者习惯老命令,这是丢弃工作区修改的方法:
bash
# 丢弃单个文件的修改
git checkout -- 文件名
# 丢弃所有修改
git checkout -- .
# 注意:-- 后面的文件名不能省略,否则变成切换分支了
# git checkout 文件名 → 丢弃修改
# git checkout 分支名 → 切换分支
这个操作是不可逆的! 一旦执行,你在工作区的所有修改都会被丢弃。执行前请确认你不需要这些修改。
9. git clean ------ 清理未跟踪的文件
git restore 只能恢复已跟踪的文件。对于未跟踪的新文件(Untracked files),你需要用 git clean:
bash
# 查看哪些文件会被删除(安全预览)
git clean -n
# 或
git clean --dry-run
# 删除未跟踪的文件
git clean -f
# 删除未跟踪的文件和文件夹
git clean -fd
# 删除未跟踪的文件、文件夹和被 .gitignore 忽略的文件
git clean -fdx
⚠️ 警告 :
git clean -fd会永久删除未跟踪的文件和文件夹,无法通过git restore恢复。执行前请先用-n预览。
10. 常见"翻车"场景及补救方案
场景一:提交到了错误的分支
bash
你在 feature/login 分支上开发,提交了3个 commit。
后来发现你其实应该在 feature/register 分支上开发。
补救方案:
bash
# 记下当前所在的正确分支
# 假设你在 feature/login,应该去 feature/register
# 1. 创建正确的分支(基于当前的 commit)
git switch -c feature/register
# 2. 回到错误的分支
git switch feature/login
# 3. 把 feature/login 回退到错误提交之前
git reset --hard HEAD~3
# 或者更简单的方式:
# 切换到正确分支,然后 cherry-pick 那3个提交
git switch feature/register
git cherry-pick commit1 commit2 commit3
git switch feature/login
git reset --hard HEAD~3
场景二:commit message 写错了
sql
你提交了一个 commit,message 是"修改",你想改成"修复登录页面验证码不显示的问题"
补救方案:
bash
# 如果还没有 push
git commit --amend -m "修复登录页面验证码不显示的问题"
# 如果已经 push 了
# 方法1:用 revert + 重新提交(如果团队不允许 force push)
# 方法2:amend 后 force push(如果团队允许,且确认没人基于这个提交工作)
git commit --amend -m "修复登录页面验证码不显示的问题"
git push --force-with-lease
场景三:误删了文件
sql
你手滑删了一个文件,并且已经 git add 了这个删除操作,但还没 commit
补救方案:
bash
# 从暂存区恢复
git restore --staged 被删的文件
git restore 被删的文件
# 如果已经 commit 了
git revert <删除文件的commit-hash>
# 或者
git restore --source=HEAD~1 被删的文件
git add 被删的文件
git commit -m "恢复误删的文件"
场景四:想撤销某次 Push 中某个特定的文件修改
perl
你在一次 push 中修改了5个文件,但不小心在其中一个文件里改了一行不该改的代码
补救方案:
bash
# 1. 找到修改前该文件的版本
git log --oneline -- 文件名
# 2. 恢复到之前的版本
git restore --source=<之前的commit-hash> 文件名
# 3. 提交修正
git add 文件名
git commit -m "fix: 恢复 xxx 文件中的误修改"
git push
11. 心态建设:出错了不可怕
每个 Git 高手都经历过无数次"翻车"。重要的是:
-
不要慌:只要你的修改在本地某个地方存在过,Git 几乎总能帮你找回来
-
做任何危险操作前,先备份 :
bash# 创建一个备份分支,以防万一 git branch backup-before-risky-operation -
reflog 是你的朋友 :记住
git reflog,它是终极救命稻草 -
push 之前多检查 :
git status、git diff、git log,确认无误再 push -
对已 push 的内容,不要用 reset,用 revert
本篇小结
这篇内容相当硬核,但也是你从 Git 新手走向熟练的关键一步。
- ✅ 理解了冲突产生的原因和冲突文件的结构
- ✅ 学会了手动解决冲突的完整流程
- ✅ 掌握了
git reset三种模式的区别和使用场景 - ✅ 理解了
git revert的安全撤销方式 - ✅ 知道了
git reflog这个终极救命稻草 - ✅ 学会了常见"翻车"场景的补救方案
现在你已经是一个"不怕出错"的 Git 使用者了。但还有一个重要领域我们没涉及------远程协作。Pull Request 是什么?Code Review 怎么做?怎么和同事在 GitHub 上协作?
下一篇,我们将模拟一个真实的团队开发场景,把前面所有知识串联起来。准备好了吗?我们下一篇见!