Skip to content

Instantly share code, notes, and snippets.

@CJHwong
Last active January 29, 2026 07:26
Show Gist options
  • Select an option

  • Save CJHwong/94e9ff52feab7030ad91630d35966731 to your computer and use it in GitHub Desktop.

Select an option

Save CJHwong/94e9ff52feab7030ad91630d35966731 to your computer and use it in GitHub Desktop.
Git vs JJ Rebase Conflict Demo

Git vs JJ Rebase Conflict Demo

展示 Git 和 Jujutsu (jj) 在處理多提交 rebase 衝突時的差異。

前置需求

確保已安裝 gitjj

# 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)

第一部分:Git 的「中斷式」行為

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


第二部分:JJ 的「流式」操作

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 進階 - 衝突未解決時繼續操作

這個場景展示 JJ 的真正威力:即使有未解決的衝突,你仍然可以繼續重排、編輯、新增 commits

場景:衝突中繼續開發

cd conflict_demo/jj_playground

# 1. 執行 rebase(產生衝突)
jj rebase -b feature -d main
jj log                             # 看到 × 衝突標記

操作 A:在衝突 commit 上繼續新增 commits

# 即使有衝突,仍可在上面新增 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 建立在衝突鏈上!

操作 B:重新排序 commits(衝突未解決)

# 假設我們想把「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                             # 順序改變了!

操作 C:分割衝突 commit

# 在衝突狀態下分割 commit
jj split <conflict-change-id>      # 互動式分割(如果需要)

操作 D:放棄並重來

# 隨時可以回到任何歷史狀態
jj op log                          # 查看操作歷史
jj undo                            # 回到上一個操作
#
jj op restore <operation-id>       # 回到特定操作點

操作 E:移動衝突 commit 到不衝突的位置(陷阱!)

這是一個重要的邊界案例:如果把衝突 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 中都無法進行

在 Git 中,一旦 git rebase 遇到衝突:

  • ❌ 無法新增 commits
  • ❌ 無法重新排序
  • ❌ 無法做任何其他操作
  • ❌ 只能:解決衝突、skip、或 abort
cd ../git_playground
git rebase main                    # 卡住

# 嘗試其他操作
git checkout -b test               # ❌ 失敗:rebase in progress
git commit                         # ❌ 失敗:需要先解決衝突
git rebase --abort                 # 唯一出路:放棄整個 rebase

關鍵洞察

JJ 將衝突視為資料(記錄在 commit 中),而非工作流程阻塞。這意味著:

  1. Rebase 永遠成功 - 不會卡在中間狀態
  2. 可以繼續工作 - 衝突不影響其他操作
  3. 隨時解決 - 不需要立即處理,可以先完成其他任務
  4. 更好的可見性 - 所有衝突一次顯示,而非一個一個發現
  5. 歷史可逆 - jj op log + jj undo 讓任何操作都可以回退
#!/bin/bash
# Git vs JJ Rebase Conflict Demo
# 這個腳本重現「多提交 Rebase 衝突」場景,展示 Git 和 JJ 的差異
set -e # Exit on error
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
rm -rf conflict_demo
mkdir conflict_demo
cd conflict_demo
echo "=== 正在建立 Git 環境 ==="
# 直接建立 git_playground(不使用遠端)
mkdir git_playground
cd git_playground
git init --initial-branch=main
# 建立初始內容
echo "timeout: 10" > config.txt
echo "mode: production" > settings.txt
git add .
git commit -m "Initial commit"
# 模擬同事的工作 (Main Branch) - 修改 config.txt 的同一行
echo "timeout: 30" > config.txt
git commit -a -m "Main: Update timeout to 30"
# 模擬你的工作 (Feature Branch) - 回到初始狀態開始分支
git checkout HEAD~1
git checkout -b feature
# 提交 1: 製造衝突 (修改同一行)
echo "timeout: 60" > config.txt
git commit -a -m "Feature: Update timeout to 60"
# 提交 2: 後續的正常開發 (新增文件)
echo "enable_feature_x: true" > feature_x.txt
git add feature_x.txt
git commit -m "Feature: Add feature X config"
# 回到 feature branch(確保在正確的分支上)
git checkout feature
cd ..
echo "=== 正在建立 JJ 環境 ==="
# 複製 git_playground 到 jj_playground
cp -r git_playground jj_playground
cd jj_playground
# 初始化 jj
jj git init --git-repo .
# 配置 jj 允許修改本地 commits(禁用 immutable 保護)
mkdir -p .jj/repo
cat > .jj/repo/config.toml << 'EOF'
[revset-aliases]
"immutable_heads()" = "none()"
EOF
# 重新導入以確保 jj 識別所有分支
jj git import
cd ..
echo ""
echo "✅ 環境設置完成!"
echo ""
echo "=== 查看初始狀態 ==="
echo ""
echo "Git:"
cd git_playground
git log --oneline --all --graph
cd ..
echo ""
echo "JJ:"
cd jj_playground
jj log --no-graph
cd ..
echo ""
echo "=== 下一步 ==="
echo "1. Git 實驗: cd conflict_demo/git_playground && git rebase main"
echo "2. JJ 實驗: cd conflict_demo/jj_playground && jj rebase -b feature -d main"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment