展示 Git 和 Jujutsu (jj) 在處理多提交 rebase 衝突時的差異。
確保已安裝 git 和 jj:
# macOS/Linux (Homebrew)
brew install jj
# 或使用 cargo
cargo install jj-cli配置 jj(首次使用):
jj config set --user user.name "Your Name"
jj config set --user user.email "your@email.com"bash setup_conflict.sh腳本會建立以下場景:
* Main: Update timeout to 30 (main branch - timeout: 30)
| * Feature: Add feature X config
| * Feature: Update timeout to 60 (feature branch - timeout: 60, 衝突!)
|/
* Initial commit (timeout: 10)
cd conflict_demo/git_playground
git log --oneline --all --graph # 查看當前狀態
git rebase main # 執行 rebase(會卡住!)預期結果:
- Git 輸出
CONFLICT (content)並停止 - 狀態顯示
interactive rebase in progress - 第二個 commit 還沒被應用(只完成 1/2)
git status # 確認卡在 rebase 中
cat config.txt # 查看衝突標記解決並繼續(逐步執行):
# Step 1: 確認 rebase 狀態
git status # 應該顯示 "interactive rebase in progress"
# Step 2: 解決衝突(選擇你想要的值)
echo "timeout: 60" | tee config.txt
# Step 3: 標記為已解決
git add config.txt
# Step 4: 繼續 rebase
git rebase --continue # 會套用下一個 commit
# Step 5: 驗證結果
git log --oneline --all --graph
⚠️ 如果出現fatal: no rebase in progress,表示 rebase 已被中斷或完成。 請用git status確認狀態,若需重新開始請執行git rebase main。
cd ../jj_playground
jj log # 查看當前狀態
jj rebase -b feature -d main # 執行 rebase(瞬間完成!)
jj log # 衝突標記但兩個 commit 都已應用預期結果:
- 命令立即完成,輸出
Rebased N commits to destination jj log顯示衝突 commit 標記為(conflict)並帶有×符號- 第二個 commit 已經接在衝突 commit 上面
如何讀取 jj log 並找到 Change ID:
× kzvrqutt hoss@... 2026-01-29 ba65113b (conflict)
│ Feature: Update timeout to 60
^^^^^^^^ ^^^^^^^^^^
Change ID Commit ID
- Change ID(如
kzvrqutt):JJ 的唯一識別碼,用於大多數 jj 命令 - Commit ID(如
ba65113b):Git 相容的 SHA,較少使用 - × 符號表示該 commit 有衝突
查看衝突內容:
jj log # 找到有 (conflict) 標記的 commit
jj file show config.txt # 查看衝突內容(JJ 格式更詳細)解決衝突:
# 方法:建立解決 commit,然後 squash 回去
# 假設衝突的 change ID 是 kzvrqutt(從 jj log 第一欄取得)
jj new kzvrqutt # 在衝突 commit 上建立新的工作區
echo "timeout: 60" | tee config.txt # 解決衝突
jj squash # 將解決方案合併回衝突 commit
# 驗證結果
jj log # 衝突標記消失簡化版(如果只有一個衝突 commit):
# 直接編輯並讓 jj 自動偵測
echo "timeout: 60" | tee config.txt
jj log # 如果在正確的工作區,衝突會自動解決| 特性 | Git | JJ |
|---|---|---|
| 衝突處理 | 停機 → 修復 → 繼續 | 記錄 → 繼續 → 隨時修復 |
| 工作流程 | 同步、阻塞式 | 異步、非阻塞式 |
| 大型 rebase | 每個衝突都中斷 | 一次完成,所有衝突標記 |
| 衝突格式 | 標準 3-way merge markers | 詳細 diff 格式,顯示來源 |
| 解決時機 | 必須立即解決 | 可以稍後解決,先做其他事 |
這個場景展示 JJ 的真正威力:即使有未解決的衝突,你仍然可以繼續重排、編輯、新增 commits。
cd conflict_demo/jj_playground
# 1. 執行 rebase(產生衝突)
jj rebase -b feature -d main
jj log # 看到 × 衝突標記# 即使有衝突,仍可在上面新增 commit
jj new feature # 在 feature 上建立新工作區
echo "debug: true" | tee debug.txt
jj file track debug.txt
jj commit -m "Add debug config"
jj log # 新 commit 建立在衝突鏈上!# 假設我們想把「Add feature X config」移到衝突 commit 之前
# 先找到 change IDs
jj log --no-graph
# 重新排序:把非衝突的 commit 移到 main 之後、衝突 commit 之前
jj rebase -r <feature-x-change-id> -d main
jj log # 順序改變了!# 在衝突狀態下分割 commit
jj split <conflict-change-id> # 互動式分割(如果需要)# 隨時可以回到任何歷史狀態
jj op log # 查看操作歷史
jj undo # 回到上一個操作
# 或
jj op restore <operation-id> # 回到特定操作點這是一個重要的邊界案例:如果把衝突 commit 移到不會產生衝突的位置,會發生什麼?
# 假設目前狀態(有衝突):
# main(timeout:30) → conflict-commit(timeout:60) → feature
# × ×
# 嘗試把衝突 commit 移回 Initial commit(在 main 之前)
jj rebase -r <conflict-change-id> -d <initial-commit-id>
jj log結果:
○ feature (timeout: 30) ← 失去了 timeout:60 的修改!
○ main (timeout: 30)
│ ○ conflict-commit (timeout: 60) ← 變成孤立分支
├─╯
○ Initial commit
- 衝突 commit 移到新位置後,不再衝突(因為那個位置沒有衝突)
- 但它的後代 commits 留在原地,不會跟著移動
- 結果:你的分支被分割了,修改被孤立在單獨的分支上
正確的解決方式:
# 不要移動 commit,而是直接解決衝突
jj new <conflict-change-id> # 在衝突 commit 上工作
cat config.txt # 查看衝突內容
echo "timeout: 60" | tee config.txt # 編輯解決衝突
jj squash # 合併解決方案到衝突 commit
jj log # 確認衝突已解決,歷史保持線性何時適合移動 commit?
- ✅ 你確實想把某個修改分離出去(例如:這個修改應該是另一個 PR)
- ✅ 你想重新組織 commits 順序,且理解後果
- ❌ 你只是想「消除衝突」而不解決它
在 Git 中,一旦 git rebase 遇到衝突:
- ❌ 無法新增 commits
- ❌ 無法重新排序
- ❌ 無法做任何其他操作
- ❌ 只能:解決衝突、skip、或 abort
cd ../git_playground
git rebase main # 卡住
# 嘗試其他操作
git checkout -b test # ❌ 失敗:rebase in progress
git commit # ❌ 失敗:需要先解決衝突
git rebase --abort # 唯一出路:放棄整個 rebaseJJ 將衝突視為資料(記錄在 commit 中),而非工作流程阻塞。這意味著:
- Rebase 永遠成功 - 不會卡在中間狀態
- 可以繼續工作 - 衝突不影響其他操作
- 隨時解決 - 不需要立即處理,可以先完成其他任務
- 更好的可見性 - 所有衝突一次顯示,而非一個一個發現
- 歷史可逆 -
jj op log+jj undo讓任何操作都可以回退