防微杜渐 —— 为什么一次 Sync 会变成一次 merge?

0 阅读6分钟

前言

为什么团队开发项目总在强调分支隔离?

别搞那么复杂,直接在 develop 上改吧,改完同步一下就行。

  • “就改一个小功能,没必要专门切分支吧?”

  • “只是修个样式,走完整套流程也太重了。”

  • “平时大家不也都是这么干的,好像也没出过什么事。”

这些话在大多数时候都是“对的” 直到某一次事故,将会教会你分支隔离的意义

分支隔离做了什么

在此之前笔者也不觉得分支隔离是必须的,只是习惯性作为一个习惯,除了感觉能带来合并时候的 review 方便没有感受其他实质性帮助,直到今天一个三人协同的远端分支出现了分叉;

直到这次事故发生:

只是点了一次 Sync,远端 develop 为什么会突然分叉?

猛然意识到:

分支隔离并不是为了规范流程,而是为了避免某些“以为只是同步,其实是在做 merge的时刻。

场景复原

下文统一用 A、B、C 代指三明成员,梳理输出对应关键信息,特有信息已做混淆处理

先看当时发生了什么。

在一个不到 1 小时的时间窗口里:

  • B 把一个 release 分支合进了主线
  • A 在本地继续开发,并提交了一次代码
  • C 又在远端主线上推进了一次修改

问题在于:

A 的本地分支,其实已经不是最新的。但他自己并没有意识到这一点。 于是他做了一个非常“日常”的操作:
👉 点了一下 VS Code 的 Sync

image.png

也就是这一刻,事情开始失控

关键提交如下:

时间提交角色含义
10:49:22 +08COMMIT_B1Bbranch_release_A 合入 branch_mainline
11:11:55 +08COMMIT_A1AA 在本地 branch_mainline 上继续开发并提交
11:17:29 +08COMMIT_C1CC 在远端 branch_mainline 线上继续推进字体修改
11:20:05 +08MERGE_A2AA 解决冲突后生成的 merge commit,但结果树已异常
11:52:48 +08MERGE_A3A再次 Sync 后,把错误树和 C 的提交一起写到了远端

COMMIT_A1COMMIT_B1COMMIT_C1 并不是按一条线串行发生的,而是多人在共享 branch_mainline 上并行推进的。

远端 mainline:
COMMIT_B1 ── COMMIT_C1
        \
         (A本地分支)
          COMMIT_A1
                \
                 MERGE_A2(错误)
                        \
                         MERGE_A3 → push(污染远端)

1️⃣ A 在“过期主线”上开发

  • 本地 branch_mainline 落后于远端
  • 但仍直接在上面提交(COMMIT_A1

2️⃣ 同一时间多人直接操作主线

  • B、C、A 都在直接改 branch_mainline
  • 没有隔离分支(feature 分支)

👉 导致:历史出现分叉(diverge)

为什么出现了分叉

这次问题表面上看,是 VS Code 的同步按钮把远端 branch_mainline 弄分叉了。

我一直都是用这个功能提交的,为什么今天出了问题

问题的关键不在 push,而是在第一次 Sync 时,Git 已经进入了 merge。

换句话说:

A 以为自己是在“同步代码”,
但实际上,Git 在本地帮他做了一次合并。

git pull --tags origin branch_mainline

# 实际含义:fetch + merge
git push origin branch_mainline:branch_mainline

如果用流程表示,大概是这样:

flowchart TD
A[共同旧基点 BASE_A]
A --> B[A 本地提交 COMMIT_A1]
A --> C[远端前进到 COMMIT_B1]
B --> D[第一次 Sync<br/>git pull 触发 merge]
C --> D
D --> E[本地生成 MERGE_A2<br/>分支开始偏离]
C --> F[远端继续前进到 COMMIT_C1]
E --> G[第二次 Sync]
F --> G
G --> H[MERGE_A3 被 push 到远端]
H --> I[远端 branch_mainline 出现并行线<br/>且尖端携带错误树]
  • 第一次 Sync 没有把 A 直接更新到远端最新,而是把 A 的本地提交和远端状态做了 merge。
  • 第二次 Sync 又把这条已经偏离的本地线继续推回远端,于是远端也开始偏移。

由于:

git pull  → git merge

结果:

  • 产生 MERGE_A2
  • 冲突被错误解决
  • 形成“错误的代码树”

不是工具问题,是协同策略隐患

这时候一个很自然的问题就会出现:

“我一直都是这么点 Sync 的,为什么这次出问题?”

一直以来笔者也认为 Sync 按钮只是执行简单的 pull 与 push 操作,但查看 Git 执行日志出现了 merge ???

真正决定行为的,是当前分支的状态。

当出现下面条件时:

  • 本地有提交
  • 远端也有新提交
  • pull.rebase=false

那一次普通的 pull,就一定会变成:

👉 fetch + merge

Git 的同步语义执行是依赖于 pull.rebase 配置的,这造成了

  • 如果本地只是落后远端,可能是快进更新。
  • 如果本地和远端都前进了,而且 pull.rebase=false,那 pull 就一定会触发 merge。

VS Code 并没有做错什么,它一直都是这么工作的。

长歪的结果树

merge 不是“选最近的那棵树”,而是基于共同祖先做三方合并,把当前暂存区里的结果写成新树

这就是真正危险的地方 👉 merge 的结果,不一定都有问题,但麻烦的是 它看起来是对的

很多人会以为:

  • 冲突解决了 → 就安全了
  • 能提交 → 就没问题

但 Git 并不会帮你保证结果树是对的。它只是把你当前暂存区的内容,写成一个新的 commit。

所以一旦冲突处理不完整,或者误操作:

在不知不觉中,远端内容将被覆盖

sequenceDiagram
participant B as B
participant A as A 本地 branch_mainline
participant V as VS Code Sync
participant R as 远端 branch_mainline
participant C as C
B->>R: 合入 COMMIT_B1
A->>A: 基于旧基点提交 COMMIT_A1
A->>V: 点击 Sync #1
V->>R: pull branch_mainline
R-->>V: 返回包含 COMMIT_B1 的最新状态
V->>A: 在本地做 merge
A->>A: 形成 MERGE_A2
Note over A: 本地分支开始偏离
C->>R: 追加 COMMIT_C1
A->>V: 点击 Sync #2
V->>R: 再次 pull 后 push
V->>A: 本地合入 COMMIT_C1
V->>R: 推送 MERGE_A3
R->>R: 远端出现并行线
本地 `branch_mainline` 跟踪远端 `branch_mainline` 的 pull / push 关系;
`pull.rebase=false`;
`11:12:45` 的 `git pull --tags origin branch_mainline`;
`11:15:22` 的未解决冲突报错;
`11:15:36` 的单文件 `git add`;
`11:20:05` 的 `MERGE_A2`;
`11:52:48` 的再次 `pull`;
`11:52:50` 的 `push origin branch_mainline:branch_mainline`;

`git show -m --stat MERGE_A2` 和 `git show -m --stat MERGE_A3` 的父节点差异。

终结

这不是 VS Code 的 Bug,而是“共享主线被当成日常开发分支”之后,被 GUI 按钮放大的协作事故。

本质问题在于:

👉 共享主线,被当成了日常开发分支

很多团队不是不会用 Git,习惯用一个“看起来很轻”的操作,去触发一件“实际上很重”的行为。

没出问题的时候,大家会觉得“这样改更快”。


tip:如果本机还需要保留 branch_mainline 用于查看和同步,建议至少配置:

git config pull.ff only

这样一旦本地和远端分叉,Git 会直接失败,而不是自动 merge。

共享主线,不应该被当成日常开发分支

问题从来不是 Sync 按钮,而是我们在用错误的方式使用主线分支。

分支隔离的意义,也不是“规范流程”,而是: 避免在错误的时间、错误的分支上发生 merge

每条规则背后,必然有案例曾经发生