chundev
日期:2026-05-11 情境:兩個 feature branch 並行,後合進去的那個被前一個拉開了基底
當兩個 feature branch 從同一個 main 分叉、PR 1 先 merge 之後 PR 2 跟 main 出現 lockfile 衝突,不要手動解 lockfile。標準流程:git rebase origin/main → 衝突時 git checkout --ours yarn.lock 拿 main 版本當基底 → 重跑 package manager(yarn install / npm install)讓它重新處理 → git add + git rebase --continue → git push --force-with-lease。三個踩雷點:rebase 期間 --ours/--theirs 方向是反的、lockfile 不能手動 merge、force push 一定要用 --force-with-lease。
很常見的情境:
main 分出去yarn.lock / package-lock.jsonPR 介面開始顯示 This branch has conflicts that must be resolved。
通常你會看到衝突檔不只一個,但 lockfile 是最會撞、最容易修壞的那一個。
| 策略 | 結果 | 適用 |
|---|---|---|
git merge origin/main into feature |
多一個 merge commit,歷史變樹狀 | feature branch 已經被別人 review 過、想保留時間軸 |
git rebase origin/main |
feature branch 的 commit 被 replay 到 main tip 上,歷史保持線性 | PR 還沒 merge、想讓 reviewer 看到乾淨的 diff |
PR 還沒 merge 的時候 rebase 比較好。merge commit 會讓 PR diff 多出別人 PR 的所有改動,review 變得難看。rebase 之後 PR 的 commit 仍只是 PR 自己改的東西。
⚠️ 已經 push 到共享 branch、別人正在用的 branch 不要 rebase。rebase 會改寫 commit hash,別人的本地 branch 會直接斷掉。
# 0. 先確保 main 同步
git fetch origin main
# 1. 在 feature branch 上 rebase
git checkout feature-b
git rebase origin/main
# → 中斷:CONFLICT (content): Merge conflict in yarn.lock
# 2. lockfile 衝突,拿 main 的版本當基底(注意 ours 在 rebase 是反的!)
git checkout --ours yarn.lock
# 3. 重跑 package manager,讓它根據 package.json 重新解析
# 若 PR 是純升級 → 重跑 yarn upgrade
# 若 PR 是加套件 → 跑 yarn install 即可
yarn upgrade # 或 yarn install / npm install
# 4. 驗證 build 沒壞
yarn tsc --noEmit
yarn lint
yarn test
# 5. 標記衝突已解,繼續 rebase
git add yarn.lock
git rebase --continue
# 6. 推上去(一定要 --force-with-lease,不要 --force)
git push --force-with-lease origin feature-b
其他無衝突檔案(這次案例中是 6 個 .tsx / .ts)Git 會自動 stage,你不用碰。git status 在 rebase 中途會列出哪些檔案已 staged、哪些還在衝突,照清單處理就好。
--ours / --theirs 是反的Merge 模式下大家熟悉的方向:
--ours = 你現在這條 branch(feature)--theirs = 你要合進來的那條(main)Rebase 模式下完全相反:
“When a merge conflict happens, the side reported as ours is the so-far rebased series, starting with
<upstream>, and theirs is the working branch. In other words, the sides are swapped.” — git-rebase 官方文件
原因:rebase 一開始 Git 先 checkout 到 upstream(變成 HEAD = ours),然後把 feature branch 的 commit 一個個 cherry-pick 上去(每個 commit 變成 theirs)。
所以在 rebase 期間:
--ours = main / upstream--theirs = feature branch 上正在被 replay 的那個 commit踩雷的後果: 想用 main 的 lockfile,卻打成 --theirs,結果用了 feature 上的舊 lockfile,後面跑 yarn install 又把升級覆蓋回舊版。
記憶法:rebase 是「把你的 commit 搬到別人家」,別人家是 ours,你帶來的是 theirs。
Lockfile 是 package manager 用 deterministic algorithm 算出來的整數依賴解。手動編輯它的後果:
yarn install 重跑時可能 install 到不存在的版本組合正確做法:拿一邊的 lockfile 當「種子」,讓 package manager 重新算。
# 拿 main 的當基底,重新升級
git checkout --ours yarn.lock
yarn upgrade
# 或拿 feature 的當基底,補上 main 新增的 deps
git checkout --theirs yarn.lock
yarn install
哪邊當基底其實都行,差別只在「誰負擔比較多重算」。如果 main 的變動小、feature 的變動大(這次案例是這樣 — feature 跑了大規模 yarn upgrade),拿 main 當基底比較好,重跑 upgrade 也比較快。
Yarn 1.x 之後 yarn install 內建會偵測 <<<<<<< HEAD 這類衝突 marker 並自動解 — 但這只是 fallback,靠這個會比顯式 checkout 一邊再重跑更難掌控結果。.gitattributes 也可以設 npm-merge-driver 自動處理,適合團隊統一規則的場景。
--force vs --force-with-lease vs --force-if-includesRebase 改寫了 commit history,必須 force push。但 --force 太危險:
git push --force # ❌ 直接覆蓋遠端,他人 push 過會被你抹掉
--force-with-lease 會比對「遠端 ref 是不是還是你最後一次 fetch 看到的版本」,他人在這之間 push 過 → 被拒絕:
git push --force-with-lease # ✅ 預設應該用這個
Caveat: 如果你的 IDE 或 git hook 背景偷跑了 git fetch,本地的 remote-tracking ref 會被更新,但你沒實際 review 那些 commit,--force-with-lease 一樣會放行。VS Code、JetBrains IDE 預設都會背景 fetch。
Git 2.30 新增 --force-if-includes 補這個洞:
git push --force-with-lease --force-if-includes
它檢查「remote tip 是不是能從你本地 branch 的 reflog 走到」— 確認你本地真的有「include」過 remote 最新狀態,不只是 ref 對上。
設成預設更省事:
git config --global push.useForceIfIncludes true
之後 --force-with-lease 就自動帶 --force-if-includes 行為。
--ours/--theirs 方向反過來。 出問題了想成「rebase 是搬到別人家」就不會搞錯。--force-with-lease 是 push 的預設選項,但仍有漏洞。 啟用 push.useForceIfIncludes = true 補洞,幾乎沒成本。git status 在 rebase 中途是你的儀表板。 它會告訴你停在哪個 commit、哪些檔案已 staged、哪些還在衝突 — 不要憑感覺繼續 rebase --continue,看清楚再動。--force-with-lease 與 --force-if-includes 完整語意--force considered harmful — 為什麼 --force-with-lease 應該是預設--force-with-lease 失靈npm-merge-driver 的自動化設定