Jujutsu (jj) 練習筆記

本筆記專為從 Git worktree 轉換到 jj 的用戶設計,適合多 AI agent terminal 並行開發場景。

部分內容由 LLM 生成,尚未經過人工驗證。

目錄

  1. 快速開始(5 分鐘)
  2. Git Worktree 轉換指南
  3. 核心工作流
  4. 團隊協作
  5. 進階技巧
  6. 完整參考

學習路徑建議

快速路徑(30 分鐘)

  1. Chapter 0.1 - 啟用 jj(5 分鐘)
  2. Chapter 1 - 完整轉換指南(15 分鐘)
  3. Chapter 2.1-2.3 - 核心工作流(10 分鐘)

完整路徑(90 分鐘)

  1. Chapter 0 - 快速開始(10 分鐘)
  2. Chapter 1 - 轉換指南(20 分鐘)
  3. Chapter 2 - 核心工作流(30 分鐘)
  4. Chapter 3.1-3.2 - PR/MR 整合(20 分鐘)
  5. Chapter 4.3 - Universal Undo 安全網(10 分鐘)

按需參考

  • Chapter 0.5 - 遇到錯誤時
  • Chapter 4.1 - 處理 AI 輸出時
  • Chapter 5 - 指令查詢和 FAQ

0. 快速開始(5 分鐘)

0.1 情況 1:既有 Git Repo 啟用 jj

# 進入現有的 git repo
cd your-git-repo

# 初始化 jj(與 git 共存)
jj git init --colocate

# 驗證設定
jj log

0.2 情況 2:從 0 開始新專案

# 建立新的 jj + git repo(默認就是 colocate)
jj git init my-project
cd my-project

# 初始狀態:
# - 有一個空的 root commit
# - 沒有任何 bookmark(包含 main)
# - @ 指向一個空的 working copy commit
# - .git 和 .jj 目錄都在專案根目錄

jj log
#   @  qpvuntsm  (empty) (no description set)
#   ◆  zzzzzzzz  (empty)

# 建立你的第一個實際 commit
echo "# My Project" > README.md
jj describe -m "Initial commit"

# 建立 main bookmark 指向當前 commit
jj bookmark create main -r @

# 設定 remote
jj git remote add origin git@github.com:user/repo.git

# 推送
jj git push --all

# 建立首個 prod tag
jj bookmark create prod-v1.0.0 -r @
jj git push --bookmark prod-v1.0.0

# 之後的開發就從 prod tag 開始
jj new prod-v1.0.0 -m "TICKET-001: First feature"

新建 vs 現有 repo 的差異:

情境jj git init <name>jj git init --colocate
使用場景全新專案從零開始現有 Git repo 加入 jj
初始 bookmark無(需手動建立 main)繼承現有 Git branch
初始 commit 歷史只有空的 root commit繼承現有 Git 歷史

兩種方式都是 colocate 模式,.git 都在專案根目錄,Git 工具完全相容。

0.3 基本配置

# 設定用戶信息
jj config set --user user.name "your_name"
jj config set --user user.email "your_email@gmail.com"

# 可選:設定編輯器
jj config set --user ui.editor nvim

0.4 第一個工作流示範

# 步驟 1:從 prod tag 創建新任務
jj new prod-v1.0.0 -m "TICKET-001: Add feature"

# 步驟 2:寫代碼(jj 自動追蹤,不需要 git add)
echo "package main" > main.go

# 步驟 3:查看狀態
jj status

# 步驟 4:建立 feature bookmark 並 push
jj bookmark create feature/ticket-001 -r @
jj git push --bookmark feature/ticket-001

# 步驟 5:在任務間切換
jj log   # 查看所有任務
jj new prod-v1.0.0 -m "TICKET-002: Next task"  # 建立下一個任務,@ 會自動移動

符號說明:

jj log 輸出中的符號含義:

  • @ = 當前 working copy(你正在編輯的 commit)
  • = 普通 commit
  • = Root commit
  • (empty) = 沒有檔案變更的 commit
  • (conflict) = 有衝突的 commit

常用指令速查(基礎版):

指令說明
jj status / jj st查看當前狀態
jj log查看 commit 歷史
jj new <rev> -m "訊息"建立新任務
jj edit <rev>切換到指定任務
jj git push --bookmark <name>推送 feature branch
jj bookmark create <name>給任務加標籤

0.5 常見問題與故障排除

GitHub Push 被拒絕:Email Privacy 錯誤

錯誤訊息:

remote: error: GH007: Your push would publish a private email address.

原因: Git commit 中的 email 地址在 GitHub 設定中被標記為 Private。

解決方案:

方式 1:使用 GitHub noreply email

# 查看 GitHub Settings → Emails,複製 noreply email
# 格式:[數字]+[username]@users.noreply.github.com

jj config set --user user.email "123456+username@users.noreply.github.com"

# 重要:已存在的 commit 不會改變 email
# 需要建立新 commit 或 rebase 現有 commit
jj new prod-v1.0.0 -m "New commit with correct email"

方式 2:在 GitHub 將 email 設為 Public

SSH 認證失敗

錯誤訊息:

Permission denied (publickey)

解決方案:

  1. 檢查 SSH key 是否存在:ls ~/.ssh/id_*.pub
  2. 如果沒有,生成新 key:ssh-keygen -t ed25519 -C "your_email@example.com"
  3. 添加到 GitHub:複製 ~/.ssh/id_ed25519.pub 內容到 GitHub Settings → SSH Keys

推送到不存在的 Remote Branch

錯誤訊息:

Error: No such remote bookmark: main

解決方案:

# 確認 remote 存在
jj git remote list

# 確認 bookmark 存在
jj bookmark list

# 推送並建立 remote branch
jj git push --bookmark main

1. Git Worktree 轉換指南

本章專為從 Git worktree 轉換到 jj 的用戶設計,提供完整的對照和轉換路徑。

1.1 為什麼從 Git Worktree 轉換到 Jj?

Git Worktree 的痛點

場景:同時開發 3 個 ticket,每個都需要獨立的 feature branch

# Git Worktree:每個 ticket 建立獨立的物理目錄
git worktree add ../ticket-001 prod-v1.2.0 -b feature/ticket-001
git worktree add ../ticket-002 prod-v1.2.0 -b feature/ticket-002
git worktree add ../ticket-003 prod-v1.2.0 -b feature/ticket-003

# 結果:
# D:/projects/main/
# D:/projects/ticket-001/      ← feature/ticket-001 branch
# D:/projects/ticket-002/      ← feature/ticket-002 branch
# D:/projects/ticket-003/      ← feature/ticket-003 branch
#
# 磁碟空間:4x 專案大小(假設專案 1GB → 需要 4GB)
# AI Agent Terminal 1 in D:/projects/ticket-001/
# AI Agent Terminal 2 in D:/projects/ticket-002/
# AI Agent Terminal 3 in D:/projects/ticket-003/
# → 各自獨立工作目錄,無法共享記憶和上下文
l
# 切換 ticket:
cd ../ticket-002   # 需要切換目錄
git stash          # 需要手動保存工作

# 推送 feature branch:
cd ../ticket-001
git push origin feature/ticket-001

cd ../ticket-002
git push origin feature/ticket-002

Jj 的解決方案

# Jj:單一目錄 + 多個 changes
cd D:/projects/main/

# 建立 3 個 ticket + feature branches
jj new prod-v1.2.0 -m "TICKET-001"
jj bookmark create feature/ticket-001 -r @

jj new prod-v1.2.0 -m "TICKET-002"
jj bookmark create feature/ticket-002 -r @

jj new prod-v1.2.0 -m "TICKET-003"
jj bookmark create feature/ticket-003 -r @

# 結果:
# D:/projects/main/ (單一目錄!)
#
# 磁碟空間:1x 專案大小(專案 1GB → 只需要 1GB)
# AI Agent Terminal 1: jj edit description:TICKET-001
# AI Agent Terminal 2: jj edit description:TICKET-002
# AI Agent Terminal 3: jj edit description:TICKET-003
# → 所有 agents 在同一目錄,共享上下文和記憶

# 切換 ticket:
jj edit description:TICKET-002   # 秒切,工作自動保存

# 推送 feature branches:
jj edit description:TICKET-001
jj git push --bookmark feature/ticket-001

jj edit description:TICKET-002
jj git push --bookmark feature/ticket-002
# 或使用 revset 一次推送多個:
jj git push --bookmark feature/ticket-001 --bookmark feature/ticket-002

實際節省

項目Git Worktree (3 tickets)Jj (3 tickets)節省
磁碟空間4GB1GB75%
切換時間cd ../ticket-002 + git stashjj edit description:TICKET-002秒切
AI context分散在 3 個目錄集中在單一目錄共享記憶

並行開發視覺化

Git Worktree 結構:

D:/projects/
├── main/              ← 主目錄(main branch)
│   └── src/
├── ticket-001/        ← AI Agent 1 工作目錄(feature/ticket-001 branch)
│   └── src/
├── ticket-002/        ← AI Agent 2 工作目錄(feature/ticket-002 branch)
│   └── src/
└── ticket-003/        ← AI Agent 3 工作目錄(feature/ticket-003 branch)
    └── src/

磁碟空間:假設專案 1GB → 總共需要 4GB

Git 歷史視圖:

* feature/ticket-003  TICKET-003: Payment
* feature/ticket-002  TICKET-002: User Profile
* feature/ticket-001  TICKET-001: Auth API
* prod-v1.2.0        Production release 1.2.0

Jj 結構:

D:/projects/main/      ← 唯一的工作目錄
└── src/               ← 所有 AI Agents 都在這裡工作

磁碟空間:1GB(75% 節省!)

Jj 歷史視圖jj log):

@  pqrstuvw feature/payment    TICKET-003: Payment
│ ○  mzvwutvl feature/user-profile  TICKET-002: User Profile
├─╯
│ ○  kmkuslsw feature/auth-api      TICKET-001: Auth API
├─╯
○  prod-v1.2.0  Production release 1.2.0

Terminal 切換對比:

Git Worktree:
Terminal 1: cd D:/projects/ticket-001/
Terminal 2: cd D:/projects/ticket-002/
Terminal 3: cd D:/projects/ticket-003/

Jj:
Terminal 1: cd D:/projects/main/ && jj edit description:TICKET-001
Terminal 2: cd D:/projects/main/ && jj edit description:TICKET-002
Terminal 3: cd D:/projects/main/ && jj edit description:TICKET-003

1.2 完整工作流對照表

日常開發流程

步驟Git WorktreeJj
1. 建立新 ticketgit worktree add ../ticket-001 prod-v1.2.0 -b feature/ticket-001jj new prod-v1.2.0 -m "TICKET-001"
jj bookmark create feature/ticket-001 -r @
2. 開發D:/projects/ticket-001/ 目錄D:/projects/main/ 目錄
jj edit description:TICKET-001
3. 切換到另一個 ticketcd ../ticket-002
(可能需要 git stash
jj edit description:TICKET-002
(自動保存)
4. 推送 feature branchcd ../ticket-001
git push origin feature/ticket-001
jj edit description:TICKET-001
jj git push --bookmark feature/ticket-001
5. 建立 PRgh pr create --base main --head feature/ticket-001同 Git
6. PR merge 後清理git worktree remove ../ticket-001
git branch -d feature/ticket-001
jj bookmark delete feature/ticket-001
(change 仍保留在歷史中)

環境推進流程

環境Git WorktreeJj說明
Devgit checkout dev
git merge main
jj bookmark set dev -r main
jj git push --bookmark dev
整包推進
Betagit checkout beta
git merge main
或建立 PR
jj bookmark create beta-release-v1.3.0 -r main
gh pr create --base beta --head beta-release-v1.3.0
整包推進,可能需要 PR
Prodgit checkout -b prod-release-v1.3.0 prod-v1.2.0
git merge feature/ticket-001 feature/ticket-003
jj new prod-v1.2.0 description:TICKET-001 description:TICKET-003 -m "Release v1.3.0"
jj bookmark create prod-release-v1.3.0 -r @
選擇性推進(關鍵差異)

Prod 選擇性推進詳解

場景:從 prod-v1.2.0 選擇性推進 TICKET-001 和 TICKET-003(跳過 TICKET-002)

Git Worktree 流程:

# 1. 從 prod tag checkout 新 branch
git checkout -b prod-release-v1.3.0 prod-v1.2.0

# 2. 選擇需要的 feature branches merge 進來
git merge feature/ticket-001
# 如果衝突:必須立即解決,否則無法繼續
git merge feature/ticket-003
# 如果衝突:必須立即解決

# 3. 推送並建立 PR
git push origin prod-release-v1.3.0
gh pr create --base prod --head prod-release-v1.3.0 \
  --title "Prod Release v1.3.0" \
  --body "包含:
- TICKET-001: Feature A
- TICKET-003: Feature C
(跳過 TICKET-002)"

# 問題:
# - 每個 merge 的衝突必須立即解決
# - 無法切換到其他 worktree(除非先完成 merge 或 abort)
# - 如果第一個 merge 有衝突,無法先跳過處理第二個
# - 必須按順序 merge,無法並行處理

Jj 流程:

# 1. 從 prod tag 建立新 change,一次性合併選擇的 tickets
# (對應 Git 的從 tag checkout + 多個 merge)
jj new prod-v1.2.0 description:TICKET-001 description:TICKET-003 -m "Release v1.3.0"

# Jj 會自動:
# - 從 prod-v1.2.0 作為基準
# - 合併 TICKET-001 的變更
# - 合併 TICKET-003 的變更
# - 建立新的 change

# 2. 如果有衝突
jj status   # 顯示 (conflict)

# 選項 A:立即解決
jj resolve

# 選項 B:先做其他事,稍後解決(關鍵優勢!)
jj edit description:TICKET-004   # 切換到其他任務
# ... 繼續開發 ...
# 稍後回來解決衝突:
jj edit description:"Release v1.3.0"
jj resolve

# 3. 建立 bookmark 並推送
jj bookmark create prod-release-v1.3.0 -r @
jj git push --bookmark prod-release-v1.3.0
gh pr create --base prod --head prod-release-v1.3.0 \
  --title "Prod Release v1.3.0" \
  --body "包含:
- TICKET-001: Feature A
- TICKET-003: Feature C"

# 優勢:
# - 一次性指定所有要合併的 tickets(不需要多次 merge)
# - 衝突可以延後處理
# - 可以隨時切換到其他任務
# - First-class conflicts,衝突狀態是正常的工作狀態
# - 即使有衝突,仍可推送到 remote(衝突在 PR 中解決)

Prod 選擇性推進視覺化

場景:從 prod-v1.2.0 選擇 TICKET-001 和 TICKET-003,跳過 TICKET-002

Git Worktree 視圖:

# 從 tag checkout 新 branch
git checkout -b prod-release-v1.3.0 prod-v1.2.0

# 順序 merge feature branches
git merge feature/ticket-001   # ← 如果衝突,卡住
git merge feature/ticket-003   # ← 要等上一個完成

結果:
* prod-release-v1.3.0  (包含 ticket-001, ticket-003)
|\
| * feature/ticket-003  TICKET-003: Payment
| |
| * feature/ticket-001  TICKET-001: Auth API
|/
* prod-v1.2.0

(feature/ticket-002 不包含在 prod-release-v1.3.0 中)

Jj 視圖:

# 一次性合併選擇的 tickets
jj new prod-v1.2.0 description:TICKET-001 description:TICKET-003 -m "Release v1.3.0"

結果(jj log):
@  prod-release-v1.3.0  Release v1.3.0
├─┬─╮  (合併 prod-v1.2.0, ticket-001, ticket-003)
│ │ ○  feature/payment      TICKET-003: Payment
│ ○ │  feature/auth-api     TICKET-001: Auth API
│ ├─╯
│ │ ○  feature/user-profile TICKET-002: User Profile (不包含)
│ ├─╯
├─╯
○  prod-v1.2.0  Production release 1.2.0

優勢:
- 一個指令完成
- 自動處理多個 parents
- 如果有衝突,可以先切換到其他任務

對比:Merge 多個 Feature Branches

Git Worktree(必須順序處理):

git checkout -b prod-release-v1.3.0 prod-v1.2.0
git merge feature/ticket-001   # 如果衝突:卡住,必須解決
git merge feature/ticket-003   # 要等上一個完成

Jj(一次性合併):

# 一個指令完成(對應 Git 的 checkout + 多個 merge)
jj new prod-v1.2.0 description:TICKET-001 description:TICKET-003 -m "Release v1.3.0"

# 或使用 bookmark 名稱(如果已建立 feature branches):
jj new prod-v1.2.0 feature/ticket-001 feature/ticket-003 -m "Release v1.3.0"

關鍵差異總結

特性Git WorktreeJj
工作目錄數量N+1(main + N tickets)1(只有 main)
磁碟空間專案大小 × (N+1)專案大小 × 1
切換速度cd ../ticket-xxxjj edit description:TICKET-xxx
工作保存手動 git stash自動保存
衝突處理必須立即解決才能繼續可以延後處理(first-class conflicts)
Prod 推進git merge 多個 feature branches(順序處理)jj new <parent> <changes...>(一次性合併)
Merge 衝突卡住,無法切換 branch可以先切換到其他 ticket,稍後回來解決
AI Agents 協作分散在多個目錄,記憶不共享集中在單一目錄,共享上下文

1.3 轉換現有 Worktree 工作流

步驟 1:在現有 Git Repo 啟用 jj

# 進入主要的 Git repo(不是 worktree 目錄)
cd D:/projects/main/

# 啟用 jj
jj git init --colocate

# 配置用戶資訊
jj config set --user user.name "your_name"
jj config set --user user.email "123456+username@users.noreply.github.com"

# 查看現有狀態
jj log

步驟 2:遷移現有 Worktree 的變更

# 假設你有 3 個 worktree:
# D:/projects/ticket-001/  (feature/ticket-001 branch)
# D:/projects/ticket-002/  (feature/ticket-002 branch)
# D:/projects/ticket-003/  (feature/ticket-003 branch)

# 選項 A:推送所有 worktree 的變更到 remote
cd D:/projects/ticket-001/
git add .
git commit -m "TICKET-001: WIP"
git push origin feature/ticket-001

cd D:/projects/ticket-002/
git add .
git commit -m "TICKET-002: WIP"
git push origin feature/ticket-002

cd D:/projects/ticket-003/
git add .
git commit -m "TICKET-003: WIP"
git push origin feature/ticket-003

# 回到主 repo,fetch 所有變更
cd D:/projects/main/
jj git fetch

# 選項 B:直接在 jj 中建立對應的 changes
cd D:/projects/main/
jj new prod-v1.2.0 -m "TICKET-001: Feature A"
jj bookmark create feature/ticket-001 -r @
# 手動複製 D:/projects/ticket-001/ 的檔案到這裡
# ... 開發 ...

jj new prod-v1.2.0 -m "TICKET-002: Feature B"
jj bookmark create feature/ticket-002 -r @
# 手動複製 D:/projects/ticket-002/ 的檔案到這裡
# ... 開發 ...

步驟 3:清理 Worktree

# 確認所有變更已保存後,移除 worktree
git worktree remove ../ticket-001
git worktree remove ../ticket-002
git worktree remove ../ticket-003

# 從此只在 D:/projects/main/ 工作!

步驟 4:建立環境 Bookmark

# 建立環境 bookmark(如果還沒有)
jj bookmark create dev -r main
jj bookmark create beta -r main
jj bookmark create prod -r prod-v1.2.0

# 推送到 remote
jj git push --all

1.4 多 Agent Terminal 協作

場景:3 個 AI agents 同時開發不同功能,推送 feature branches,最後整合到 main

# 準備:所有 agents 在同一目錄 D:/projects/main/

# === Terminal 1 (Agent A) - 開發 Auth API ===
jj new prod-v1.2.0 -m "TICKET-001: Auth API"
jj bookmark create feature/auth-api -r @
# AI agent 開發認證 API...
echo "def login():" > auth.py
jj git push --bookmark feature/auth-api

# 在 GitHub 建立 PR
gh pr create --base main --head feature/auth-api \
  --title "TICKET-001: Auth API" \
  --body "Implement login authentication"

# === Terminal 2 (Agent B) - 同時開發 User Profile ===
jj new prod-v1.2.0 -m "TICKET-002: User Profile"
jj bookmark create feature/user-profile -r @
# AI agent 開發用戶資料...
echo "def get_profile():" > profile.py
jj git push --bookmark feature/user-profile

gh pr create --base main --head feature/user-profile \
  --title "TICKET-002: User Profile" \
  --body "Add user profile functionality"

# === Terminal 3 (Agent C) - 開發 Payment ===
jj new prod-v1.2.0 -m "TICKET-003: Payment"
jj bookmark create feature/payment -r @
echo "def process_payment():" > payment.py
jj git push --bookmark feature/payment

# === PR merge into main 後,整包推進到 Beta ===

# 1. Fetch latest main
jj git fetch
jj bookmark set main -r main@origin

# 2. Beta 整包推進(不需要個別 PR)
jj bookmark set beta -r main
jj git push --bookmark beta

# === Prod 選擇性推進(只推 TICKET-001 和 TICKET-003)===

# 3. 從 prod tag 建立新 release,選擇性合併 tickets
jj new prod-v1.2.0 description:TICKET-001 description:TICKET-003 -m "Release v1.3.0"

# 如果有衝突?
jj status   # 顯示 (conflict)
# 選項 1:立即解決
jj resolve

# 選項 2:先切換做其他事,稍後解決
jj edit description:TICKET-004   # 切換到其他任務
# ... 繼續開發 ...
# 稍後回來解決衝突:
jj edit description:"Release v1.3.0"
jj resolve

# 4. 建立 prod release bookmark 並推送
jj bookmark create prod-release-v1.3.0 -r @
jj git push --bookmark prod-release-v1.3.0

# 5. 建立 PR to prod
gh pr create --base prod --head prod-release-v1.3.0 \
  --title "Prod Release v1.3.0" \
  --body "包含:
- TICKET-001: Auth API
- TICKET-003: Payment
(跳過 TICKET-002,待下次 release)"

# 6. PR merge 後,建立 prod tag
jj git fetch
jj bookmark set prod -r prod@origin
jj bookmark create prod-v1.3.0 -r prod
jj git push --bookmark prod-v1.3.0

對比 Git Worktree 流程

Git Worktree(舊流程):

# 3 個物理目錄
cd ../ticket-001 && git push origin feature/auth-api
cd ../ticket-002 && git push origin feature/user-profile
cd ../ticket-003 && git push origin feature/payment

# Prod 選擇性推進:從 tag checkout + merge feature branches
git checkout -b prod-release-v1.3.0 prod-v1.2.0
git merge feature/auth-api        # 如果衝突:必須立即解決
git merge feature/payment          # 如果衝突:必須立即解決
# 問題:無法切換到其他 worktree,必須先完成 merge 或 abort

Jj(新流程):

# 單一目錄,秒切
jj edit description:TICKET-001 && jj git push --bookmark feature/auth-api
jj edit description:TICKET-002 && jj git push --bookmark feature/user-profile
jj edit description:TICKET-003 && jj git push --bookmark feature/payment

# Prod 選擇性推進:一次性合併多個 changes(對應 Git 的 checkout + 多個 merge)
jj new prod-v1.2.0 description:TICKET-001 description:TICKET-003 -m "Release v1.3.0"
# 或使用 bookmark 名稱:
jj new prod-v1.2.0 feature/auth-api feature/payment -m "Release v1.3.0"
# 如果衝突:可以先切換做其他事,稍後再回來解決

1.5 First-class Conflicts 詳解

核心特性:Conflict 不阻塞開發!

# 傳統 Git:
# - Agent A 和 Agent B 同時修改同一檔案
# - Merge 時產生 conflict
# - 必須先解決 conflict 才能繼續

# Jj:
# - Conflict 可以保存在 commit 中
# - 不阻塞任何人
# - 可以先做其他事,稍後再處理

Conflict 處理流程

# 查看有 conflict 的 commit
jj log   # 會標示 (conflict)

# 如果當前 commit 有 conflict
jj status   # 顯示 conflict 檔案

# 選項 1:先做其他 ticket,稍後再處理
jj edit <另一個ticket>
# ... 開發 ...

# 選項 2:解決 conflict
jj edit <有conflict的commit>

# 使用 jj resolve(會開啟 merge tool)
jj resolve

# 或直接編輯檔案,手動解決
# 編輯完成後,jj 自動追蹤變更

多 Agent 工作流建議

# Agent A Terminal:
jj new main -m "Agent A: 功能 X"
# ... 開發 ...

# Agent B Terminal(同一個 repo):
jj new main -m "Agent B: 功能 Y"
# ... 開發 ...

# 整合時:
jj new ticket-a ticket-b -m "Merge Agent A and B work"
# 如果有 conflict,會在這個新 commit 中顯示
# 解決後繼續

練習場景:處理 Conflict

# Agent A 修改 config.js
jj new prod-v1.2.0 -m "TICKET-001: Set port 3000"
echo "port = 3000" > config.js
jj bookmark create ticket-001 -r @

# Agent B 也修改 config.js
jj new prod-v1.2.0 -m "TICKET-002: Set port 8080"
echo "port = 8080" > config.js
jj bookmark create ticket-002 -r @

# 嘗試合併
jj new ticket-001 ticket-002 -m "Merge configs"
# 會產生 conflict

jj status   # 顯示 conflict: config.js

# 選項 A:立即解決
jj resolve   # 開啟 merge tool

# 選項 B:先做其他事
jj edit description:TICKET-003   # 切換到其他任務
# ... 繼續開發 TICKET-003 ...
# 稍後再回來:
jj edit description:"Merge configs"
jj resolve

2. 核心工作流

本章聚焦實際開發中的核心工作流,特別是從 Prod Tag 並行開發多個 ticket 的場景。

2.1 從 Prod Tag 並行開發多個 Ticket

為什麼從 Prod Tag 開發?

  • 確保所有開發基於穩定的生產版本
  • 避免受到 main 分支上正在進行的其他變更影響
  • 便於追蹤每個 ticket 相對於生產環境的變更

建立並行任務

# === 初始狀態:從 prod v1.2.0 開始 ===
jj log -r prod-v1.2.0
# ○  prod-v1.2.0  Production release 1.2.0

# === 開發 TICKET-001(從 prod tag 開始)===
jj new prod-v1.2.0 -m "TICKET-001: Fix critical login bug"
echo "// fix login bug" >> auth.js
jj bookmark create feature/ticket-001 -r @

# 推送 feature branch
jj git push --bookmark feature/ticket-001

# === 開發 TICKET-002(也從 prod-v1.2.0 開始)===
jj new prod-v1.2.0 -m "TICKET-002: Add user profile"
echo "// user profile" >> profile.js
jj bookmark create feature/ticket-002 -r @
jj git push --bookmark feature/ticket-002

# === 開發 TICKET-003 ===
jj new prod-v1.2.0 -m "TICKET-003: Payment integration"
echo "// payment integration" >> payment.js
jj bookmark create feature/ticket-003 -r @
jj git push --bookmark feature/ticket-003

# === 查看所有並行任務 ===
jj log
#   @  pqrstuvw feature/payment    TICKET-003: Payment integration
#   │ ○  mzvwutvl feature/user-profile  TICKET-002: Add user profile
#   ├─╯
#   │ ○  kmkuslsw feature/auth-api      TICKET-001: Fix critical login bug
#   ├─╯
#   ○  prod-v1.2.0  Production release 1.2.0

不需要建立 Bookmark 的快速開發

如果你不需要為每個 ticket 建立正式的 feature branch(例如在 dev/beta 測試階段),可以直接推送 change:

# 快速開發模式
jj new prod-v1.2.0 -m "TICKET-001: Fix bug"
echo "// fix" >> auth.js
jj git push --change @   # 自動建立 remote branch(名稱為 change ID)

# GitHub 上會看到一個自動生成的 branch 名稱,例如:
# push-kmkuslswnrpl

# 這適合:
# - 快速開發和測試
# - 不需要建立正式 PR 的情況
# - Dev/Beta 整包推進前的開發階段

2.2 任務間切換

# 查看所有開發中的 commit
jj log

# 方式 1:使用 commit description 切換
jj edit description:TICKET-001
# 工作目錄自動更新!不需要 stash!
echo "// 更多登入邏輯" >> auth.js

# 方式 2:使用 commit id 切換
jj edit kmkuslsw

# 方式 3:使用 bookmark 切換(如果有建立)
jj edit feature/ticket-001

# 切換到另一個 ticket
jj edit description:TICKET-002
# 工作目錄再次自動更新

給任務加 Bookmark

Bookmark 是為了方便記憶和切換,特別是當有多個並行任務時。

# 給 TICKET-001 加上 bookmark
jj bookmark create ticket-001 -r kmkuslsw

# 之後可以用 bookmark 名稱切換,而不需要記 commit id
jj edit ticket-001

# 查看所有 bookmark
jj bookmark list

2.3 推送策略(–change vs –bookmark)

Jj 提供多種 push 方式,根據不同場景選擇:

方式 1:直接 Push Change(最簡單,適合快速開發)

# Push 當前 commit 到 remote(自動建立對應的 remote branch)
jj git push --change @

# 優點:不需要建立 bookmark,快速方便
# 缺點:remote branch 名稱會是自動生成的 change ID

適用場景

  • 從 prod tag 開發多個並行 ticket(不需要為每個 ticket 建立 bookmark)
  • Dev/Beta 整包推進前的開發階段
  • 快速分享工作成果給團隊成員
  • 不需要建立正式 PR/MR 的情況

方式 2:使用 Bookmark 後 Push(推薦,便於管理)

# 建立 bookmark(類似 Git branch 標籤)
jj bookmark create feature/ticket-001 -r @

# Push 該 bookmark
jj git push --bookmark feature/ticket-001

# 優點:
# - Remote branch 名稱清晰(feature/ticket-001)
# - 可以用 bookmark 名稱切換:jj edit feature/ticket-001
# - 便於團隊協作和 code review

適用場景

  • 選擇性推送到 prod(需要建立 PR/MR 進行 code review)
  • 整包推進到 beta(建立臨時 release bookmark)
  • 需要清晰的 branch 名稱追蹤變更
  • 長期維護的功能分支

提示:對於臨時 PR bookmark(如 prod-hotfix-v1.2.1beta-release-v1.3.0),PR merge 後記得刪除以保持乾淨:

jj bookmark delete prod-hotfix-v1.2.1

方式 3:Push 所有更新(批量操作)

# 推送所有本地更新到 remote
jj git push

# 適用場景:有多個 bookmark 需要同時推送

推送策略對照表

場景推薦方式指令說明
快速開發測試--changejj git push --change @不需要 bookmark
Dev/Beta 開發階段--changejj git push --change @自動生成 remote branch
建立 PR/MR--bookmarkjj git push --bookmark feature/xxx清晰的 branch 名稱
Prod hotfix--bookmarkjj git push --bookmark prod-hotfix-vX.Y.Z需要 code review
環境推進--bookmarkjj git push --bookmark beta-release-vX.Y.Z臨時 release bookmark

2.4 Dev/Beta 整包推進

Dev 自動推進(整包,不需要 PR)

# === 所有 ticket 開發完成,整包推進到 dev ===

# 1. Fetch 最新更新
jj git fetch

# 2. 找到最新的 commit(假設是 ticket-003)
jj log

# 3. 移動 dev bookmark 到最新 commit
jj bookmark set dev -r pqrstuvw
jj git push --bookmark dev

# CI 自動部署到 dev 環境

Beta 整包推進(需要 PR)

# === Dev 測試通過,整包推進到 beta(需要 PR)===

# 1. 建立臨時 bookmark 用於 PR(不是個別 ticket 的 branch)
jj bookmark create beta-release-v1.3.0 -r dev
jj git push --bookmark beta-release-v1.3.0

# 2. 使用 GitHub CLI 建立 PR
gh pr create --base beta --head beta-release-v1.3.0 \
  --title "Beta Release v1.3.0" \
  --body "包含功能:
- TICKET-001: Fix critical login bug
- TICKET-002: Add user profile
- TICKET-003: Payment integration"

# 或使用 GitLab CLI
# glab mr create --target-branch beta --source-branch beta-release-v1.3.0 \
#   --title "Beta Release v1.3.0" \
#   --description "包含功能:TICKET-001, TICKET-002, TICKET-003"

# 3. PR/MR merge 後清理臨時 bookmark
jj bookmark delete beta-release-v1.3.0

為什麼整包推進不需要個別 branch?

  • Dev/Beta 環境通常接受整體的功能集合
  • 不需要逐個 ticket 的 code review
  • 減少 bookmark 管理負擔
  • 臨時 release bookmark 僅用於建立 PR,merge 後即可刪除

追蹤包含哪些 Ticket

# 查看 dev bookmark 包含哪些 ticket
jj log -r 'prod-v1.2.0::dev'

# 或者更詳細的格式
jj log -r 'prod-v1.2.0::dev' -T 'description'

# 輸出所有包含的 commit 訊息,便於追蹤包含的 ticket

2.5 Prod 選擇性推進

場景:只推 TICKET-001,不要 TICKET-002

這是 jj 的強項!使用 jj new <parent> <changes...> 一次性合併多個 changes(對應 Git 的「checkout + 多個 merge」)。

關鍵概念

  • 不是 cherry-pick,而是多 parent merge
  • 一次性指定所有要合併的 tickets
  • 衝突可以延後處理(first-class conflicts)
# === 場景:Beta 還在測試,但 TICKET-001 是緊急修復,需要立即上 prod ===

# 步驟 1:找到 TICKET-001 的 commit ID
jj log -r 'description(TICKET-001)'
# 輸出:kmkuslsw  TICKET-001: Fix critical login bug

# 步驟 2:從 prod tag 建立新 release,一次性合併選擇的 tickets
jj new prod-v1.2.0 description:TICKET-001 -m "Prod Hotfix v1.2.1"

# 或使用 bookmark 名稱:
jj new prod-v1.2.0 feature/ticket-001 -m "Prod Hotfix v1.2.1"

# 如果要合併多個 tickets(例如 TICKET-001 和 TICKET-003):
jj new prod-v1.2.0 description:TICKET-001 description:TICKET-003 -m "Release v1.3.0"

# 步驟 3:如果有衝突
jj status   # 顯示 (conflict)

# 選項 A:立即解決
jj resolve

# 選項 B:先做其他事,稍後解決(關鍵優勢!)
jj edit description:TICKET-004   # 切換到其他任務
# ... 繼續開發 ...
# 稍後回來解決衝突:
jj edit description:"Prod Hotfix v1.2.1"
jj resolve

# 步驟 4:建立 bookmark 並推送
jj bookmark create prod-hotfix-v1.2.1 -r @
jj git push --bookmark prod-hotfix-v1.2.1

# 步驟 5:建立 PR/MR
# GitHub
gh pr create --base prod --head prod-hotfix-v1.2.1 \
  --title "Prod Hotfix v1.2.1: TICKET-001" \
  --body "緊急修復:Fix critical login bug

Ticket: TICKET-001
影響範圍:用戶登入功能
測試:已在 dev 環境驗證"

# GitLab
# glab mr create --target-branch prod --source-branch prod-hotfix-v1.2.1 \
#   --title "Prod Hotfix v1.2.1: TICKET-001" \
#   --description "緊急修復:TICKET-001"

# 步驟 6:PR/MR merge 後更新和清理
jj git fetch
jj bookmark set prod -r prod@origin

# 清理臨時 bookmark
jj bookmark delete prod-hotfix-v1.2.1

# 步驟 7:建立新 prod tag(版本標記)
jj bookmark create prod-v1.2.1 -r prod
jj git push --bookmark prod-v1.2.1

為什麼選擇性推 Prod 需要個別 Bookmark?

  • Prod 推送需要嚴格的 code review
  • 需要清晰的 branch 名稱追蹤變更
  • 便於在 GitHub/GitLab UI 上審查
  • Bookmark 在 PR merge 後可以刪除,不會累積

多個 Commit 的 Ticket 挑選

# 如果 TICKET-001 有多個 commit(例如 abc, def, ghi)
# 需要挑選整條鏈

# 查看 commit 鏈
jj log -r 'ancestors(feature/ticket-001) & descendants(prod-v1.2.0)'

# 方式 1:直接合併整條鏈
jj new prod-v1.2.0 feature/ticket-001 -m "Prod Hotfix v1.2.1"
# 這會自動包含 feature/ticket-001 的所有父 commit

# 方式 2:Squash 成一個再推
jj squash -r 'ancestors(feature/ticket-001) & descendants(prod-v1.2.0)' -d feature/ticket-001
jj new prod-v1.2.0 feature/ticket-001 -m "Prod Hotfix v1.2.1"

確保沒有漏掉相依的 Commit

# 檢查 TICKET-001 的依賴(祖先 commit)
jj log -r '::feature/ticket-001'

# 檢查 TICKET-001 到 prod 之間的差異
jj log -r 'prod-v1.2.0::feature/ticket-001'

# 如果 TICKET-001 依賴其他 commit(不在 prod 上)
# 你會看到多個 commit 在輸出中

如果發現 TICKET-001 依賴其他未推送的 commit,有兩個選擇:

選項 1:一起推送依賴的 commit

# 找到所有需要的 commit
jj log -r 'prod-v1.2.0::feature/ticket-001'

# 直接推送,會自動包含所有父 commit
jj bookmark create prod-hotfix-v1.2.1 -r feature/ticket-001
jj git push --bookmark prod-hotfix-v1.2.1

選項 2:Rebase 到 prod 上(如果依賴不重要)

# 把 TICKET-001 單獨 rebase 到 prod,跳過中間依賴
jj rebase -r feature/ticket-001 -d prod-v1.2.0
# 可能會有 conflict,需要解決

3. 團隊協作

3.1 環境 Bookmark 設定

# 建立環境 bookmark(如果還沒有)
jj bookmark create dev -r main
jj bookmark create beta -r main
jj bookmark create prod -r main

# 推送到 remote
jj git push --bookmark dev
jj git push --bookmark beta
jj git push --bookmark prod

使用 Prod Tag 的替代方案

在實際團隊工作流中,prod 通常使用 tag 而非 bookmark 來標記穩定版本:

# 方案 1:prod 使用 tag(推薦用於穩定版本標記)
jj bookmark create prod-v1.2.0 -r <stable-commit>
jj git push --bookmark prod-v1.2.0

# 從 prod tag 開發新功能
jj new prod-v1.2.0 -m "TICKET-001: New feature"

# 方案 2:prod 使用 bookmark(推薦用於持續移動的環境指標)
jj bookmark create prod -r <stable-commit>
jj git push --bookmark prod

# 從 prod bookmark 開發
jj new prod -m "TICKET-001: New feature"

Tag vs Bookmark 的選擇

  • Tag(bookmark 作為 tag 使用):
    • 用於標記穩定版本(如 prod-v1.2.0prod-v1.2.1
    • 不會移動,永久指向特定 commit
    • 便於追蹤歷史版本和回溯
  • Bookmark(環境指標):
    • 用於標記當前環境狀態(如 devbetaprod
    • 會隨著推進而移動
    • 便於 CI/CD 自動部署

推薦組合使用

# prod bookmark 追蹤當前生產環境
jj bookmark create prod -r prod-v1.2.0

# prod tag 標記每個穩定版本
jj bookmark create prod-v1.2.0 -r <commit>
jj bookmark create prod-v1.2.1 -r <commit>
jj bookmark create prod-v1.3.0 -r <commit>

# 開發時從最新的 prod tag 開始
jj new prod-v1.2.0 -m "TICKET-001: Feature"

# 推送新版本後建立新 tag
jj bookmark create prod-v1.2.1 -r prod
jj git push --bookmark prod-v1.2.1

3.2 GitHub/GitLab PR/MR 整合

本節介紹如何在實際團隊工作流中整合 Jj 與 GitHub/GitLab 的 Pull Request (PR) 或 Merge Request (MR),特別是從 prod tag 開發選擇性推送的場景。

工作流概述:從 Prod Tag 開發

許多團隊的實際流程是:

  • 開發起點:從 prod 的 tag/version 開始,而非 main
  • Dev/Beta 推進:整包推進,不需要為每個 ticket 建立 branch
  • Prod 推進:選擇性挑選特定 ticket 進 prod(透過 PR/MR)

關鍵理念

  • 每個 ticket 是獨立的 commit
  • 只有在需要建立 PR/MR 時才建立 bookmark(臨時 branch)
  • Dev/Beta 整包推進時不需要個別 ticket 的 bookmark

完整工作流範例

# ========================================
# 階段 1:初始化環境 bookmark
# ========================================
jj bookmark create dev -r prod-v1.2.0
jj bookmark create beta -r prod-v1.2.0
jj bookmark create prod -r prod-v1.2.0
jj git push --all

# ========================================
# 階段 2:從 Prod Tag 並行開發多個 Ticket
# ========================================

# TICKET-001: 緊急 Bug 修復
jj new prod-v1.2.0 -m "TICKET-001: Fix critical login bug"
echo "// fix login bug" >> auth.js
jj bookmark create feature/ticket-001 -r @
jj git push --bookmark feature/ticket-001

# TICKET-002: 新功能
jj new prod-v1.2.0 -m "TICKET-002: Add user profile"
echo "// user profile" >> profile.js
jj bookmark create feature/ticket-002 -r @
jj git push --bookmark feature/ticket-002

# TICKET-003: 優化
jj new prod-v1.2.0 -m "TICKET-003: Optimize payment"
echo "// payment optimization" >> payment.js
jj bookmark create feature/ticket-003 -r @
jj git push --bookmark feature/ticket-003

# ========================================
# 階段 3:個別 Ticket 建立 PR to Main
# ========================================

# 使用 GitHub CLI 為每個 ticket 建立 PR
gh pr create --base main --head feature/ticket-001 \
  --title "TICKET-001: Fix critical login bug" \
  --body "修復登入錯誤"

gh pr create --base main --head feature/ticket-002 \
  --title "TICKET-002: Add user profile" \
  --body "新增用戶資料頁面"

gh pr create --base main --head feature/ticket-003 \
  --title "TICKET-003: Optimize payment" \
  --body "優化支付流程"

# PR merge 後,清理 bookmark
jj bookmark delete feature/ticket-001
jj bookmark delete feature/ticket-002
jj bookmark delete feature/ticket-003

# ========================================
# 階段 4:整包推進到 Dev(不需要 PR)
# ========================================
jj git fetch
jj bookmark set main -r main@origin
jj log  # 找到最新的 commit (假設是 ticket-003: pqrstuvw)
jj bookmark set dev -r main
jj git push --bookmark dev
# CI 自動部署到 dev 環境

# ========================================
# 階段 5:整包推進到 Beta(需要 PR)
# ========================================

# Dev 測試通過後,整包推進到 beta
jj bookmark create beta-release-v1.3.0 -r dev
jj git push --bookmark beta-release-v1.3.0

# GitHub PR
gh pr create --base beta --head beta-release-v1.3.0 \
  --title "Beta Release v1.3.0" \
  --body "包含功能:
- TICKET-001: Fix critical login bug
- TICKET-002: Add user profile
- TICKET-003: Optimize payment"

# GitLab MR(註解版本)
# glab mr create --target-branch beta --source-branch beta-release-v1.3.0 \
#   --title "Beta Release v1.3.0"

# PR merge 後清理
jj bookmark delete beta-release-v1.3.0

# ========================================
# 階段 6:選擇性推送到 Prod(緊急修復)
# ========================================

# 場景:Beta 還在測試,但 TICKET-001 是緊急修復,需要立即上 prod

# 找到 TICKET-001 的 commit ID
jj log -r 'description(TICKET-001)'
# 輸出:kmkuslsw  TICKET-001: Fix critical login bug

# 從 prod tag 建立新 release,選擇性合併 ticket
jj new prod-v1.2.0 description:TICKET-001 -m "Prod Hotfix v1.2.1"

# 建立針對 prod 的臨時 bookmark
jj bookmark create prod-hotfix-v1.2.1 -r @
jj git push --bookmark prod-hotfix-v1.2.1

# GitHub PR
gh pr create --base prod --head prod-hotfix-v1.2.1 \
  --title "Prod Hotfix v1.2.1: TICKET-001" \
  --body "緊急修復:Fix critical login bug

Ticket: TICKET-001"

# GitLab MR(註解版本)
# glab mr create --target-branch prod --source-branch prod-hotfix-v1.2.1 \
#   --title "Prod Hotfix v1.2.1: TICKET-001"

# PR merge 後更新和清理
jj git fetch
jj bookmark set prod -r prod@origin
jj bookmark delete prod-hotfix-v1.2.1

# 建立新 prod tag
jj bookmark create prod-v1.2.1 -r prod
jj git push --bookmark prod-v1.2.1

對照表:Jj 工作流 vs GitHub/GitLab PR/MR 流程

從 Prod Tag 開發:

Jj 操作說明是否需要 bookmark?
jj new prod-v1.2.0 -m "TICKET-001"從 prod tag 開始開發❌ 不一定需要
jj bookmark create feature/ticket-001 -r @建立 feature bookmark✅ 用於 PR
jj git push --bookmark feature/ticket-001推送 feature branch-

整包推進環境(Dev/Beta):

Jj 操作GitHub/GitLab 對應說明
jj bookmark set dev -r main移動 dev branch整包推進,不需要個別 ticket branch
jj bookmark create beta-release-v1.3.0 -r dev建立臨時 release branch用於建立 PR/MR
gh pr create --base beta --head beta-releasePull RequestCode review 流程
jj bookmark delete beta-release-v1.3.0刪除臨時 branch(PR merge 後)清理

選擇性推 Prod(個別 Ticket):

Jj 操作GitHub/GitLab 對應說明
jj log -r 'description(TICKET-001)'搜尋特定 commit找到要推送的 commit
jj new prod-v1.2.0 description:TICKET-001 -m "Hotfix"合併選擇的 tickets一次性合併
jj bookmark create prod-hotfix-v1.2.1 -r @建立臨時 hotfix branch只為 prod 推送建立 bookmark
gh pr create --base prod --head prod-hotfixPull RequestCode review 和合併
jj bookmark delete prod-hotfix-v1.2.1刪除臨時 branchPR merge 後清理
jj bookmark create prod-v1.2.1 -r prod建立新 prod tag版本標記

3.3 CI/CD 整合

自動化流程設計

  1. Dev 自動推進(整包)

    • 當有新的 commit 推送時,CI 自動移動 dev bookmark 到最新 commit
    • 自動部署到 dev 環境
  2. Beta 手動 PR(整包)

    • 手動建立臨時 bookmark: beta-release-vX.Y.Z
    • 建立 PR/MR: beta-release-vX.Y.Zbeta
    • Review 通過後 merge,CI 自動部署到 beta 環境
  3. Prod 選擇性 PR(個別 ticket)

    • 手動挑選特定 ticket,建立臨時 bookmark: prod-hotfix-vX.Y.Z
    • 建立 PR/MR: prod-hotfix-vX.Y.Zprod
    • Review 通過後 merge,CI 自動部署到 prod 環境

GitHub Actions 範例(Dev 自動推進)

# .github/workflows/auto-promote-dev.yml
name: Auto Promote to Dev

on:
  push:
    branches-ignore:
      - dev
      - beta
      - prod

jobs:
  promote:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Setup jj
        run: |
          curl -L https://github.com/martinvonz/jj/releases/latest/download/jj-linux-x86_64 -o jj
          chmod +x jj

      - name: Promote to dev
        run: |
          # 獲取最新 commit
          LATEST_COMMIT=$(./jj log --limit 1 --no-graph -T 'commit_id')

          # 移動 dev bookmark
          ./jj bookmark set dev -r $LATEST_COMMIT
          ./jj git push --bookmark dev

GitLab CI 範例(Beta 部署)

# .gitlab-ci.yml
deploy-beta:
  stage: deploy
  only:
    - beta
  script:
    - echo "Deploying to beta environment..."
    - ./deploy-beta.sh
  environment:
    name: beta
    url: https://beta.example.com

3.4 多人 Bookmark 管理

關鍵原則:區分個人和共享 Bookmark

共享 bookmark(環境分支):

  • main, dev, beta, prod
  • 由專人或 CI/CD 管理,避免多人同時移動
  • 使用 PR/MR 流程控制更新

個人 bookmark(ticket 標籤):

  • feature/ticket-001, feature/x, prod-hotfix-v1.2.1
  • 每人使用不同的名稱,避免衝突
  • 完成後刪除,不累積

建議流程

1. 開發階段:可以選擇不建立 bookmark

jj new prod-v1.2.0 -m "TICKET-001: Feature"
jj git push --change @
# 不建立 bookmark,避免命名衝突

2. 建立 PR 時:建立 feature bookmark

# 使用包含 ticket 號碼的 bookmark
jj bookmark create feature/ticket-001 -r @
jj git push --bookmark feature/ticket-001

# 或使用包含自己名字的 bookmark(避免衝突)
jj bookmark create alice/feature-x -r @
jj git push --bookmark alice/feature-x

3. PR merge 後:立即刪除

jj bookmark delete feature/ticket-001
# 或
jj bookmark delete alice/feature-x

4. 共享 bookmark 更新:透過 PR + CI/CD

# 不要直接 push 到 prod bookmark
# 而是建立 PR,由 CI merge 後自動更新 prod

處理 Bookmark 衝突

# 如果 remote 有更新
jj git fetch
jj log -r 'dev | dev@origin'  # 查看分歧

# 選項 1:Rebase 本地變更到 remote
jj rebase -r dev -d dev@origin
jj bookmark set dev -r <rebased-commit>
jj git push --bookmark dev

# 選項 2:強制推送(謹慎使用!)
# jj git push --bookmark dev --force

4. 進階技巧

4.1 Curating(整理 AI 輸出)

當 AI agent 完成任務後,產出的變更往往包含好的程式碼和不需要的部分。Jj 提供強大的工具讓你挑選好的變更,丟棄不需要的。

場景:從 AI 亂碼中挑選好的部分

# 假設 AI 在 revision kmkuslsw 完成工作
# 但改動很亂,只有部分是你想要的

# 切換到該 revision 查看
jj edit kmkuslsw
jj diff   # 檢視所有變更

# 建立一個乾淨的 revision(基於父 commit)
jj new @- -m "feat: implement JWT authentication"   # → 新 ID: pqrstuvw

# 互動式挑選好的變更從 kmkuslsw → pqrstuvw
jj squash -i --from kmkuslsw
# 會開啟互動式介面,讓你選擇要保留的變更

# 完成後,pqrstuvw 只包含你挑選的好變更
# kmkuslsw 仍保留原始的亂碼(如果還有剩餘變更)

丟棄不需要的 Revision

# 如果 kmkuslsw 的剩餘變更都不需要了
jj abandon kmkuslsw

# 如果你確定整個 revision 都不要
jj abandon <revision-id>

# 注意:abandon 不會刪除 commit,只是標記為不再需要
# 可以透過 jj op undo 恢復

完整流程範例

# 1. AI 完成任務(亂亂的)
jj new prod-v1.2.0 -m "Claude Task: Refactor auth system"   # → ID: kmkuslsw
jj edit kmkuslsw
# AI 完成後,kmkuslsw 包含許多變更

# 2. 檢視 AI 的工作成果
jj diff                    # 查看所有變更
jj log -r @ --summary      # 查看變更摘要

# 3. 建立乾淨的 commit 來挑選好的部分
jj new @- -m "feat: implement new auth system"   # → ID: pqrstuvw

# 4. 互動式挑選
jj squash -i --from kmkuslsw
# 選擇要保留的變更

# 5. 丟棄 AI 的亂碼 revision
jj abandon kmkuslsw

# 6. 現在 pqrstuvw 是乾淨的、只包含好變更的 commit
jj log

4.2 Stacking(堆疊相依任務)

當任務之間有相依性時(例如功能 B 依賴功能 A),Jj 的自動 rebase 讓堆疊任務變得簡單。

建立堆疊的 Revision 鏈

# 功能 A:JWT 認證(基礎)
jj new prod-v1.2.0 -m "Claude Task: JWT authentication"   # → ID: kmkuslsw
jj edit kmkuslsw
# 實作 JWT 認證
# 完成後整理成乾淨的 commit
jj describe -m "feat: implement JWT authentication"

# 功能 B:Profile Page(依賴 JWT)
jj new kmkuslsw -m "Claude Task: Build profile page"   # → ID: mzvwutvl
jj edit mzvwutvl
# 建立 profile page

# 功能 C:User Settings(依賴 Profile Page)
jj new mzvwutvl -m "Claude Task: User settings"   # → ID: pqrstuvw
jj edit pqrstuvw
# 建立 user settings

# 查看堆疊結構
jj log
#   @  pqrstuvw 3a4b5c6d Claude Task: User settings
#   ○  mzvwutvl 7d8e9f0a Claude Task: Build profile page
#   ○  kmkuslsw 1b2c3d4e feat: implement JWT authentication
#   ○  prod-v1.2.0 Production release 1.2.0

自動 Rebase 的威力

# 糟糕!發現 kmkuslsw (JWT auth) 有 bug
# Git: 要手動 rebase 所有後續 commit (mzvwutvl, pqrstuvw),很痛苦
# Jj: 直接修!

jj edit kmkuslsw
# 修正 bug...
echo "// bug fix" >> auth.js

# 看看發生什麼事
jj log
#   ○  pqrstuvw 3a4b5c6d Claude Task: User settings
#   ○  mzvwutvl 7d8e9f0a Claude Task: Build profile page
#   @  kmkuslsw 1b2c3d4e feat: implement JWT authentication  ← 你在這裡修改
#   ○  prod-v1.2.0 Production release 1.2.0

# 神奇的事:mzvwutvl 和 pqrstuvw 自動 rebase 到修復後的 kmkuslsw 上!
# 不需要手動處理,Jj 自動完成

# 繼續開發 pqrstuvw
jj edit pqrstuvw
# mzvwutvl 和 pqrstuvw 已經包含 kmkuslsw 的 bug fix

處理自動 Rebase 產生的 Conflict

# 有時候修改父 commit 會在子 commit 產生 conflict
# Jj 不會阻塞,conflict 會被記錄在子 commit 中

jj log
#   ○  pqrstuvw 3a4b5c6d (conflict) Claude Task: User settings
#   ○  mzvwutvl 7d8e9f0a Claude Task: Build profile page
#   @  kmkuslsw 1b2c3d4e feat: implement JWT authentication
#   ○  prod-v1.2.0 Production release 1.2.0

# 解決 conflict
jj edit pqrstuvw
jj status          # 顯示 conflict 檔案
jj resolve         # 開啟 merge tool
# 或直接編輯檔案

# 解決後繼續工作

在堆疊中插入新任務

# 需要在 kmkuslsw 和 mzvwutvl 之間插入一個任務
jj new kmkuslsw -m "Claude Task: Token refresh"   # → ID: xyzkwqrs

# 把 mzvwutvl 和其後代 rebase 到 xyzkwqrs 上
jj rebase -s mzvwutvl -d xyzkwqrs

# 新的結構
jj log
#   ○  pqrstuvw 3a4b5c6d Claude Task: User settings
#   ○  mzvwutvl 7d8e9f0a Claude Task: Build profile page
#   ○  xyzkwqrs 5f6a7b8c Claude Task: Token refresh
#   ○  kmkuslsw 1b2c3d4e feat: implement JWT authentication
#   ○  prod-v1.2.0 Production release 1.2.0

4.3 Universal Undo(安全網)

Jj 的殺手級功能:任何操作都可以撤銷。AI 亂搞也不怕。

查看操作歷史

# 列出所有操作歷史
jj op log

# 輸出範例:
#   @  op-kmkuslsw  2024-01-15 14:30  user@host
#   │  squash commit pqrstuvw into mzvwutvl
#   ○  op-mzvwutvl  2024-01-15 14:25  user@host
#   │  new empty commit
#   ○  op-pqrstuvw  2024-01-15 14:20  user@host
#      snapshot working copy

# 查看特定操作的詳情
jj op show op-kmkuslsw

撤銷操作

# 撤銷最後一次操作
jj op undo

# 撤銷特定操作
jj op undo abc123

# 例如:不小心 abandon 了重要的 commit
jj abandon important-commit   # 糟糕!
jj op undo                    # 救回來!

# 例如:rebase 搞砸了
jj rebase -s feature -d wrong-branch   # 糟糕!
jj op undo                             # 恢復原狀

恢復到特定時間點

# 如果需要恢復到更早的狀態
jj op log   # 找到要恢復的操作 ID

# 恢復到該操作後的狀態
jj op restore <operation-id>

# 這會建立新的操作,不會丟失歷史

安全網使用場景

# 場景 1:AI 把程式碼改壞了
jj edit ai-task
# AI 亂改一通
# 檢視後發現完全不行
jj op undo   # 回到 AI 開始前的狀態

# 場景 2:squash 錯了
jj squash -i --from wrong-commit   # 選錯了
jj op undo   # 撤銷,重新來過

# 場景 3:rebase 產生大量 conflict
jj rebase -s feature -d main   # conflict 太多
jj op undo   # 算了,先不 rebase

# 場景 4:誤刪 bookmark
jj bookmark delete important-branch   # 糟糕!
jj op undo   # 救回來

4.4 單任務工作流

如果你只需要單一任務開發(不需要並行多個 ticket),這裡是簡化的工作流。

創建新任務

# 基於 main 建立新 commit
jj new main -m "TICKET-001: Add user authentication"

# 查看當前狀態
jj log
#   @  kmkuslsw 16944bc0 (empty) TICKET-001: Add user authentication
#   ○  qwlwuxul 92668b38 main | first commit
#   ◆  zzzzzzzz 00000000 (empty)

開發與修改

# jj 自動追蹤所有變更,不需要 git add
echo "def login():" > auth.py

# 查看狀態(真實輸出範例)
jj status
# 輸出:
# Working copy changes:
# A auth.py
#
# Working copy : kmkuslsw 16944bc0 TICKET-001: Add user authentication
# Parent commit: qwlwuxul 92668b38 main | first commit

# 修改當前 commit 訊息
jj describe -m "feat: implement JWT authentication"

# 查看當前變更(真實輸出範例)
jj diff
# 輸出:
# Added regular file auth.py:
#        1: def login():

修改 Commit 內容

# 繼續在當前 commit 上修改
echo "def verify_token():" >> auth.py

# jj 自動追蹤這些新變更
# 無需 git add 或 git commit

# 查看更新後的狀態
jj log -r @

Fetch 更新

# 從 remote 拉取最新更新
jj git fetch

# 查看 remote 的變更
jj log --all

Git vs Jj 概念對照

GitJj
HEAD@(working copy)
git statusjj statusjj st
git logjj log
git branchjj bookmark
branch 是指標bookmark 是標籤,commit 是核心

Bookmark vs Git Branch:兩者本質相同,都是指向特定 commit 的可移動標籤。但在 jj 中,你通常直接操作 commit(用 jj newjj edit),bookmark 只是為了方便記憶和與 Git remote 同步。


5. 完整參考

5.1 指令速查

基本操作

指令說明
jj status / jj st查看當前狀態
jj log查看 commit 歷史
jj log -r @只看當前 commit
jj diff查看當前變更
jj describe -m "訊息"修改當前 commit 訊息

建立與切換

指令說明
jj new在當前 commit 上建立新 commit
jj new <rev>從指定 revision 建立新 commit
jj new <rev> -m "訊息"建立新 commit 並設定訊息
jj edit <rev>切換到指定 commit 編輯(不需要 stash!)

Bookmark(類似 Branch)

指令說明
jj bookmark list / jj b l列出所有 bookmark
jj bookmark create <name>建立 bookmark
jj bookmark set <name> -r <rev>移動 bookmark 到指定 revision
jj bookmark delete <name>刪除 bookmark

Rebase 與整理

指令說明
jj rebase -r <rev> -d <dest>把單一 commit rebase 到目標
jj rebase -s <rev> -d <dest>把 commit 及其後代 rebase 到目標
jj squash把當前 commit squash 到父 commit
jj squash -r <rev>把指定 commit squash 到其父 commit
jj squash -i --from <rev>互動式挑選變更從指定 revision
jj abandon <rev>丟棄不需要的 revision

安全網(Universal Undo)

指令說明
jj op log查看所有操作歷史
jj op show <op-id>查看特定操作詳情
jj op undo撤銷最後一次操作
jj op undo <op-id>撤銷特定操作
jj op restore <op-id>恢復到特定操作後的狀態

Git 互操作

指令說明
jj git fetch從 remote 拉取
jj git push推送到 remote
jj git push --change @推送當前 commit
jj git push --bookmark <name>推送指定 bookmark
jj git remote list列出 remote

5.2 Revset 語法

常用 Revset

語法說明
@當前 working copy
@-當前的父 commit
mainmain bookmark
<rev>::從 rev 到所有後代
::<rev>從根到 rev 的所有祖先
<rev1>::<rev2>從 rev1 到 rev2 的路徑
heads()所有 head commits

搜尋

語法說明
description(TICKET-001)搜尋 commit 訊息包含 TICKET-001
author(alice)搜尋作者是 alice 的 commits
ancestors(<rev>)的所有祖先
descendants(<rev>)的所有後代

5.3 FAQ

Q: 為什麼 jj config 改了 email,舊 commit 的 email 沒變?

A: Commit 中的 email 是在建立 commit 時固定的。改變配置只影響新建立的 commit

解決方案:

  • 建立新 commit:jj new prod-v1.0.0 -m "New commit with correct email"
  • 或者修改現有 commit(需要 rebase):這會改變 commit hash,影響已推送的 commit

Q: Bookmark 和 Git Branch 到底有什麼不同?

A: 本質上相同,都是指向特定 commit 的可移動標籤。

主要差異:

  • 概念重心不同:Git 以 branch 為中心,Jj 以 commit 為中心
  • 使用方式
    • Git: git checkout branch → 切換分支
    • Jj: jj edit <commit> → 直接切換 commit;bookmark 只是方便記憶

實務上:bookmark 主要用於與 Git remote 同步(如 maindev)和標記重要的 commit

Q: 什麼時候用 jj new,什麼時候用 jj edit

A:

  • jj new:建立新 commit(產生新的 revision)

    • 開始新任務:jj new main -m "New feature"
    • 在現有 commit 上疊加:jj new(基於當前 commit)
  • jj edit:切換到已存在的 commit 進行修改

    • 修改舊 commit:jj edit kmkuslsw
    • 在多個任務間切換:jj edit description:TICKET-001

Q: jj git push --change @jj git push --bookmark 該用哪個?

A:

  • --change @:快速開發,不在意 remote branch 名稱(會自動生成)
  • --bookmark:正式開發,需要清晰的 branch 名稱(如 feature/ticket-001
    • 推薦用於團隊協作和 code review

參考 2.3 推送策略

Q: 如何看懂 jj log 的樹狀圖?

A: 關鍵符號:

  • @ = 當前 working copy(你正在這裡工作)
  • = 普通 commit
  • = Root commit
  • , , = 分支結構(類似 Git 的 branch 視覺化)

範例:

@  kmkuslsw 1b2c3d4e TICKET-001  ← 當前工作位置
│ ○  mzvwutvl 7d8e9f0a TICKET-002  ← 另一個並行任務
├─╯                               ← 兩個任務在此分叉
○  qwlwuxul 92668b38 main        ← 共同的父 commit

Q: 從 prod tag 開發,如何確保不會互相干擾?

A: Jj 的 commit-first 模型天然支援並行開發,只要每個 ticket 都從同一個 prod tag 建立獨立的 commit,就不會互相干擾。

# 所有 ticket 都從 prod-v1.2.0 開始
jj new prod-v1.2.0 -m "TICKET-001: Feature A"
jj new prod-v1.2.0 -m "TICKET-002: Feature B"
jj new prod-v1.2.0 -m "TICKET-003: Feature C"

# 查看並行結構
jj log
#   @  ticket-003  TICKET-003: Feature C
#   │ ○  ticket-002  TICKET-002: Feature B
#   ├─╯
#   │ ○  ticket-001  TICKET-001: Feature A
#   ├─╯
#   ○  prod-v1.2.0  Production release 1.2.0

每個 ticket 都是獨立的 commit,可以自由切換、修改、推送,完全不會互相影響。

Q: 沒有建立 bookmark 的 commit,在 GitHub 上看得到嗎?

A: 可以看到!使用 jj git push --change @ 推送時,Jj 會自動在 GitHub 上建立一個以 change ID 命名的 remote branch。

# 沒有建立 bookmark,直接推送
jj new prod-v1.2.0 -m "TICKET-001: Fix bug"
jj git push --change @

# GitHub 上會看到一個 branch,名稱類似:
# push-kmkuslswnrpl

這個自動建立的 branch 可以正常用於:

  • Code review
  • CI/CD 觸發
  • 與團隊成員分享

不過,如果需要建立正式的 PR/MR(特別是推送到 prod),建議使用 bookmark:

jj bookmark create feature/ticket-001 -r @
jj git push --bookmark feature/ticket-001

Q: 選擇性推 prod 時,如何確保沒有漏掉相依的 commit?

A: 使用 jj log 檢查 commit 的依賴關係:

# 檢查 TICKET-001 的依賴(祖先 commit)
jj log -r '::feature/ticket-001'

# 檢查 TICKET-001 到 prod 之間的差異
jj log -r 'prod-v1.2.0::feature/ticket-001'

# 如果 TICKET-001 依賴其他 commit(不在 prod 上)
# 你會看到多個 commit 在輸出中

如果發現 TICKET-001 依賴其他未推送的 commit,有兩個選擇:

1. 一起推送依賴的 commit

# 找到所有需要的 commit
jj log -r 'prod-v1.2.0::feature/ticket-001'

# 直接推送,會自動包含所有父 commit
jj bookmark create prod-hotfix-v1.2.1 -r feature/ticket-001
jj git push --bookmark prod-hotfix-v1.2.1

2. Rebase 到 prod 上(如果依賴不重要):

# 把 TICKET-001 單獨 rebase 到 prod,跳過中間依賴
jj rebase -r feature/ticket-001 -d prod-v1.2.0
# 可能會有 conflict,需要解決

Q: 如何處理 GitHub 上的 branch 與 Jj bookmark 同步?

A: Jj 的 bookmark 和 Git remote branch 是雙向同步的:

從 remote 更新本地 bookmark:

# Fetch 更新
jj git fetch

# 更新本地 bookmark 到 remote 位置
jj bookmark set dev -r dev@origin
jj bookmark set prod -r prod@origin

# 或者自動追蹤 remote(設定 tracking)
# 之後 fetch 會自動更新本地 bookmark

從本地推送 bookmark 到 remote:

# 移動本地 bookmark
jj bookmark set dev -r <new-commit>

# 推送到 remote
jj git push --bookmark dev

# 如果 remote 已經有更新(有人先推了),會被拒絕
# 需要先 fetch,然後決定:
# 1. Rebase 你的變更到最新的 remote
# 2. 或者強制推送(危險!)

處理衝突:

# 如果 remote 有更新
jj git fetch
jj log -r 'dev | dev@origin'  # 查看分歧

# 選項 1:Rebase 本地變更到 remote
jj rebase -r dev -d dev@origin
jj bookmark set dev -r <rebased-commit>
jj git push --bookmark dev

# 選項 2:強制推送(謹慎使用!)
# jj git push --bookmark dev --force

5.4 術語表

Working copy (@)

  • 工作目錄當前對應的 commit
  • 你的檔案系統狀態對應的 revision
  • jj edit <rev> 可以移動 @ 到不同的 commit

Revision

  • Jj 中對 commit 的稱呼
  • 每個 revision 有唯一的 change ID 和 commit hash
  • 可用 revision ID、bookmark 名稱、或 revset 表達式引用

Bookmark

  • 可移動的 commit 標籤(類似 Git branch)
  • 主要用於:
    1. 與 Git remote 同步(如 maindev
    2. 標記重要的 commit 便於切換(如 feature/ticket-001

Change ID

  • Jj 追蹤變更的唯一 ID(與 commit hash 不同)
  • 即使 commit hash 改變(如 rebase),change ID 仍保持不變
  • 用於追蹤「同一個變更」在不同時間點的版本

Revset

  • 選擇 revision 的查詢語法
  • 範例:
    • @ = 當前 working copy
    • @- = 當前的父 commit
    • main:: = main 的所有後代
    • ::@ = 從根到當前的所有祖先

Colocate

  • Jj 與 Git 共存模式
  • .git.jj 在同一目錄,完全相容 Git 工具
  • jj git init --colocate 在現有 Git repo 啟用

環境分支工作流

  • 使用多個 bookmark 代表不同環境(main、dev、beta、prod)
  • 透過 jj bookmark set <env> -r <commit> 推進流程
  • 可選擇性合併特定 ticket 到 prod(用 jj new <parent> <changes...>

First-class Conflicts

  • Jj 的核心特性:衝突可以保存在 commit 中
  • 不阻塞開發,可以延後處理
  • 允許在有衝突的狀態下切換到其他任務

Happy Hacking with Jujutsu!