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 和短生命周期的功能分支。流程是:

  1. 从 main 切 feature/xxx
  2. 提交、推送、开 PR
  3. Review 通过后合并到 main
  4. 自动部署

规则简单到一句话: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  # 只看对方的改动

减少冲突的根本方法

冲突处理再熟练也是成本。真正有效的是减少冲突发生:

  1. 分支生命周期短。超过 3 天的功能分支,冲突概率指数上升。
  2. 频繁同步主干。每天至少 git rebase main 一次。
  3. 按文件分工。同一个文件多人同时改,必然冲突。
  4. 小步提交。一次改 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 从没记录过,谁也救不回来。

八、几条值得写进团队规范的建议

  1. main 分支必须开保护。禁止直接 push、禁止强推、合并需 PR + CI 通过。
  2. 提交信息写清"为什么",而不是"改了什么"。fix: 修复分页参数越界导致的空指针 比 update 有用一百倍。
  3. 禁止 git push -f 到共享分支。可以在个人分支上放开。
  4. .gitignore 从项目第一天就要有。target/、node_modules/、.idea/、*.log、.env 尤其重要——把 .env 提交上去泄露密钥是真实发生过很多次的事故。
  5. .gitattributes 统一换行符,避免 Windows 和 Mac 协作时整个文件显示改动:

* text=auto eol=lf *.bat text eol=crlf

  1. 大文件用 Git LFS。超过 10MB 的二进制文件进普通 Git 会让仓库迅速膨胀,而且一旦进入历史就很难彻底清除。

小结

Git 的学习曲线不陡,陡的是"信心"——不确定自己这一步会不会毁掉什么。

建立两个习惯就能解决大部分焦虑:动手前 git status 看清楚状态,动手后用 git reflog 确认能回来。有了这两条,几乎所有操作都变成可逆的试验。

剩下的就是三个判断:

  • 要不要保留分叉历史 → merge 还是 rebase
  • 改动在哪一层 → 用 restore、reset 还是 revert
  • 提交是否已经共享 → 决定能不能改写历史

这三件事想清楚了,Git 就从"危险工具"变成了一个可以放心操作的存档系统。

查看「工程效能」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。