Share Notes

chundev

View the Project on GitHub latteouka/share-notes

Git rebase 撞上 lockfile 衝突的處理流程

日期:2026-05-11 情境:兩個 feature branch 並行,後合進去的那個被前一個拉開了基底


TL;DR

當兩個 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 --continuegit push --force-with-lease。三個踩雷點:rebase 期間 --ours/--theirs 方向是反的、lockfile 不能手動 merge、force push 一定要用 --force-with-lease


背景

很常見的情境:

PR 介面開始顯示 This branch has conflicts that must be resolved

通常你會看到衝突檔不只一個,但 lockfile 是最會撞、最容易修壞的那一個。


為什麼用 rebase 而不是 merge

策略 結果 適用
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、哪些還在衝突,照清單處理就好。


三個踩雷點

1. Rebase 時 --ours / --theirs 是反的

Merge 模式下大家熟悉的方向:

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 期間:

踩雷的後果: 想用 main 的 lockfile,卻打成 --theirs,結果用了 feature 上的舊 lockfile,後面跑 yarn install 又把升級覆蓋回舊版。

記憶法:rebase 是「把你的 commit 搬到別人家」,別人家是 ours,你帶來的是 theirs

2. Lockfile 不能手動解衝突

Lockfile 是 package manager 用 deterministic algorithm 算出來的整數依賴解。手動編輯它的後果:

正確做法:拿一邊的 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 自動處理,適合團隊統一規則的場景。

3. --force vs --force-with-lease vs --force-if-includes

Rebase 改寫了 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 行為。


學到的事


參考資料