Git 分支工作流与冲突处理:从焦虑到可逆操作
把撤销、合并、冲突处理按意图重新组织一遍,先建立四层数据模型的心智地图,让每一步操作都可回退。
Git 的命令只有十几个,但真正让人焦虑的往往不是"不会用",而是"不确定这一步做完还能不能回来"。
这篇把最常用的操作按"意图"重新组织一遍:想撤销什么、想合并什么、冲突了怎么办。搞清楚每条命令到底动了哪一层数据,就不慌了。
一、先建立四个区域的心智模型
Git 的数据流是四层:
工作区 暂存区 本地仓库 远程仓库
(Working Dir) -> (Index) -> (Repository) -> (Remote)
add commit push
<- <- <-
checkout/restore reset fetch/pull
- 工作区:你正在编辑的文件
- 暂存区(Index):
git add之后的"待提交快照" - 本地仓库:
git commit之后进objects的历史 - 远程仓库:
git push之后
几乎所有"我操作错了吗"的焦虑,都源于没分清改动在哪一层。git status 会明确告诉你,养成执行前先看一眼的习惯,能避免大部分事故。
二、三种工作流怎么选
Git Flow:重,但适合有发布周期的产品
main ──●──────────────●────────── 只放已发布的版本,每次合并打 tag
╲ ╱
release/1.0 ●──●──●──●── 预发布分支,只修 bug 不加功能
╱
develop ──●──●──●──●──●──●──●── 集成分支,功能都合到这里
╲ ╱
feature/xxx ●───● 功能分支,从 develop 切出
分支角色:main(生产)、develop(集成)、feature/*、release/*、hotfix/*。
适合:客户端软件、有明确版本号、需要同时维护多个已发布版本的项目。
不适合:持续部署的 Web 服务——每天发布,release 分支完全是负担。
GitHub Flow:轻,适合持续部署
只有 main 和短生命周期的功能分支。流程是:
- 从
main切feature/xxx - 提交、推送、开 PR
- Review 通过后合并到
main - 自动部署
规则简单到一句话:main 永远可发布。
适合:Web 服务、SaaS、CI/CD 成熟的团队。
Trunk Based:极简,靠开关控制
所有人直接往 main(trunk)提交,功能没做完用 feature flag 挡起来,不靠分支隔离。
适合:工程能力强、有完善自动化测试和灰度能力的团队。Google、Facebook 都是这个模式。
给大多数中小团队的实用建议:用 GitHub Flow,但加一条保护——main 分支禁止直接 push,必须走 PR 且 CI 通过。这一条约束能挡掉 80% 的事故。
三、最容易搞混的几组命令
reset 的三种模式
git reset --soft HEAD~1 # 撤销 commit,改动回到【暂存区】
git reset --mixed HEAD~1 # 撤销 commit,改动回到【工作区】(默认)
git reset --hard HEAD~1 # 撤销 commit,改动【直接丢弃】
记忆方法:看 --soft/--mixed/--hard 分别回退到哪个区域之前。
--soft:只动本地仓库,暂存区和工作区不变 → 可以立刻重新 commit--mixed:动本地仓库 + 暂存区 → 需要重新add--hard:全动 → 改动真的没了(除非用 reflog)
--hard 是唯一会丢数据的。执行前 git status 确认没有未提交的改动,或者先 git stash。
checkout 到 restore / switch
Git 2.23 把 checkout 拆成了两个语义更清晰的命令:
# 旧写法(一个命令干两件事,容易误操作)
git checkout main # 切分支
git checkout -- file.txt # 丢弃文件改动 <- 危险,容易和上一条搞混
# 新写法
git switch main # 切分支
git restore file.txt # 丢弃工作区改动
git restore --staged file.txt # 从暂存区撤出(保留工作区改动)
强烈建议升级到新命令。git checkout -- file 和 git checkout branch 长得太像,手滑一次就是半天的活没了。
revert vs reset
git revert <commit> # 生成一个【新提交】,内容是反向修改
git reset <commit> # 【移动分支指针】,历史被改写
关键区别:revert 是安全的,reset 会改写历史。
已经 push 到公共分支的提交,只能用 revert。用 reset 再强推会让所有协作者的本地历史和远程分叉,制造灾难。
# 撤销一个已推送的提交(正确做法)
git revert 3f2a1b9
git push
# 撤销一个还没推送的提交(reset 没问题)
git reset --hard HEAD~1
四、merge 和 rebase 到底选哪个
两者的结果都是"把 A 分支的改动合到 B",但历史形态完全不同。
merge:保留分叉
D---E feature
/ \
A---B---C---M main
M 是 merge commit,有两个父节点。历史真实反映了"并行开发"这件事。
优点:不破坏任何已有 commit,安全。 缺点:分支多时历史图会变成"地铁线路图"。
rebase:拉直历史
A---B---C---D'---E' feature
把 feature 上的提交依次重放到 main 的最新提交之上。提交内容一样,但哈希全变了(因为父节点变了)。
优点:线性历史,git log 干净。
缺点:改写了提交,共享分支上不能用。
实用规则
只对本地未推送的提交做 rebase;已推送的分支用 merge。
具体到日常操作:
# 场景:主分支有新提交,想把它们同步到我的功能分支
git switch feature
git rebase main # 把我的提交重放到 main 之上,历史线性
# 场景:功能完成,合并回主分支
git switch main
git merge --no-ff feature # 保留一个 merge commit,能看出"这是一个功能"
--no-ff 值得推荐:它强制生成 merge commit,让 git log --graph 能清楚看出功能边界。不加的话,如果 main 在切出 feature 后没动过,Git 会做 fast-forward,直接移动指针,历史里就看不出这里曾经有过一个分支。
rebase 冲突的正确处理
git rebase main
# 冲突了,Git 停下来
git status # 看哪些文件冲突
# 手动解决后
git add <file>
git rebase --continue
# 如果搞砸了
git rebase --abort # 完全回到 rebase 之前,安全
--abort 是 rebase 的保命符。任何时候不确定,直接 abort 重来,不会有任何损失。
五、冲突处理的完整流程
冲突的本质:同一个文件的同一区域,被两边改了不同内容。
冲突标记长什么样
<<<<<<< HEAD
这是当前分支(你所在分支)的内容
=======
这是要合并进来的分支的内容
>>>>>>> feature/login
三段:HEAD 是自己的,下面那段是对方的。
处理步骤
# 1. 先看清现状
git status
# both modified: src/main/java/UserService.java
# 2. 打开文件,理解双方意图(不是随便选一个)
# 这一步最重要——冲突往往说明两个人改了同一段逻辑,
# 需要判断是"都要"、"选一个"还是"重新写"
# 3. 改完删掉 <<<<<<< ======= >>>>>>> 三行标记
# 4. 标记为已解决
git add src/main/java/UserService.java
# 5. 继续
git merge --continue # 或 git rebase --continue
用工具代替手工
git mergetool # 调用配置的可视化工具
或者用 IDE 的三栏对比视图(IDEA 的 Merge Conflicts 面板、VS Code 的 Accept Incoming/Current)。
遇到大段冲突时,git diff 看不清楚,可以单独看三方对比:
git diff --base # 与共同祖先对比
git diff --ours # 只看自己的改动
git diff --theirs # 只看对方的改动
减少冲突的根本方法
冲突处理再熟练也是成本。真正有效的是减少冲突发生:
- 分支生命周期短。超过 3 天的功能分支,冲突概率指数上升。
- 频繁同步主干。每天至少
git rebase main一次。 - 按文件分工。同一个文件多人同时改,必然冲突。
- 小步提交。一次改 50 个文件的 PR,review 和合并都是噩梦。
六、撤销操作速查表
按"我想撤销什么"来找:
| 想撤销的 | 命令 | 风险 |
|---|---|---|
| 工作区某个文件的改动 | git restore file |
无(改动丢弃) |
| 把文件从暂存区撤出 | git restore --staged file |
无 |
| 最近一次 commit(保留改动) | git reset --soft HEAD~1 |
无 |
| 最近一次 commit(丢弃改动) | git reset --hard HEAD~1 |
高 |
| 已推送的 commit | git revert <hash> |
无 |
| 一次 merge | git merge --abort |
无 |
| 一次 rebase | git rebase --abort |
无 |
| 暂存当前改动 | git stash / git stash pop |
无 |
| 误改了某个文件想还原到某次提交 | git checkout <hash> -- file |
无(覆盖工作区) |
七、reflog:最后的保险
只要提交过,Git 就不会真的删除。reflog 记录了 HEAD 的每一次移动:
git reflog
# 3f2a1b9 HEAD@{0}: reset: moving to HEAD~1
# a8c4d21 HEAD@{1}: commit: 添加订单校验 <- 这就是被 reset 掉的提交
# 9b2e5f3 HEAD@{2}: commit: 修复分页 bug
找回:
git reset --hard a8c4d21
reflog 默认保留 90 天(gc.reflogExpire)。这是 reset --hard 和误删分支的救命稻草——在 reflog 清空之前,几乎没有任何操作是真正不可恢复的。
唯一真正的危险操作是没提交就 --hard。工作区的东西 Git 从没记录过,谁也救不回来。
八、几条值得写进团队规范的建议
main分支必须开保护。禁止直接 push、禁止强推、合并需 PR + CI 通过。- 提交信息写清"为什么",而不是"改了什么"。
fix: 修复分页参数越界导致的空指针比update有用一百倍。 - 禁止
git push -f到共享分支。可以在个人分支上放开。 .gitignore从项目第一天就要有。target/、node_modules/、.idea/、*.log、.env尤其重要——把.env提交上去泄露密钥是真实发生过很多次的事故。.gitattributes统一换行符,避免 Windows 和 Mac 协作时整个文件显示改动:
* text=auto eol=lf
*.bat text eol=crlf
- 大文件用 Git LFS。超过 10MB 的二进制文件进普通 Git 会让仓库迅速膨胀,而且一旦进入历史就很难彻底清除。
小结
Git 的学习曲线不陡,陡的是"信心"——不确定自己这一步会不会毁掉什么。
建立两个习惯就能解决大部分焦虑:动手前 git status 看清楚状态,动手后用 git reflog 确认能回来。有了这两条,几乎所有操作都变成可逆的试验。
剩下的就是三个判断:
- 要不要保留分叉历史 →
merge还是rebase - 改动在哪一层 → 用
restore、reset还是revert - 提交是否已经共享 → 决定能不能改写历史
这三件事想清楚了,Git 就从"危险工具"变成了一个可以放心操作的存档系统。
正在加载评论…