Jujutsu (jj) 練習筆記
本筆記專為從 Git worktree 轉換到 jj 的用戶設計,適合多 AI agent terminal 並行開發場景。
目錄
學習路徑建議
快速路徑(30 分鐘)
- Chapter 0.1 - 啟用 jj(5 分鐘)
- Chapter 1 - 完整轉換指南(15 分鐘)
- Chapter 2.1-2.3 - 核心工作流(10 分鐘)
完整路徑(90 分鐘)
- Chapter 0 - 快速開始(10 分鐘)
- Chapter 1 - 轉換指南(20 分鐘)
- Chapter 2 - 核心工作流(30 分鐘)
- Chapter 3.1-3.2 - PR/MR 整合(20 分鐘)
- 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 log0.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 nvim0.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
- 前往 https://github.com/settings/emails
- 取消勾選 “Keep my email addresses private”
SSH 認證失敗
錯誤訊息:
Permission denied (publickey)解決方案:
- 檢查 SSH key 是否存在:
ls ~/.ssh/id_*.pub - 如果沒有,生成新 key:
ssh-keygen -t ed25519 -C "your_email@example.com" - 添加到 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 main1. 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-002Jj 的解決方案
# 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) | 節省 |
|---|---|---|---|
| 磁碟空間 | 4GB | 1GB | 75% |
| 切換時間 | cd ../ticket-002 + git stash | jj 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 → 總共需要 4GBGit 歷史視圖:
* 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.0Jj 結構:
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.0Terminal 切換對比:
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-0031.2 完整工作流對照表
日常開發流程
| 步驟 | Git Worktree | Jj |
|---|---|---|
| 1. 建立新 ticket | git worktree add ../ticket-001 prod-v1.2.0 -b feature/ticket-001 | jj 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. 切換到另一個 ticket | cd ../ticket-002(可能需要 git stash) | jj edit description:TICKET-002(自動保存) |
| 4. 推送 feature branch | cd ../ticket-001git push origin feature/ticket-001 | jj edit description:TICKET-001jj git push --bookmark feature/ticket-001 |
| 5. 建立 PR | gh pr create --base main --head feature/ticket-001 | 同 Git |
| 6. PR merge 後清理 | git worktree remove ../ticket-001git branch -d feature/ticket-001 | jj bookmark delete feature/ticket-001(change 仍保留在歷史中) |
環境推進流程
| 環境 | Git Worktree | Jj | 說明 |
|---|---|---|---|
| Dev | git checkout devgit merge main | jj bookmark set dev -r mainjj git push --bookmark dev | 整包推進 |
| Beta | git checkout betagit merge main或建立 PR | jj bookmark create beta-release-v1.3.0 -r maingh pr create --base beta --head beta-release-v1.3.0 | 整包推進,可能需要 PR |
| Prod | git checkout -b prod-release-v1.3.0 prod-v1.2.0git 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 Worktree | Jj |
|---|---|---|
| 工作目錄數量 | N+1(main + N tickets) | 1(只有 main) |
| 磁碟空間 | 專案大小 × (N+1) | 專案大小 × 1 |
| 切換速度 | cd ../ticket-xxx | jj 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 --all1.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 或 abortJj(新流程):
# 單一目錄,秒切
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 resolve2. 核心工作流
本章聚焦實際開發中的核心工作流,特別是從 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 list2.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.1、beta-release-v1.3.0),PR merge 後記得刪除以保持乾淨:
jj bookmark delete prod-hotfix-v1.2.1方式 3:Push 所有更新(批量操作)
# 推送所有本地更新到 remote
jj git push
# 適用場景:有多個 bookmark 需要同時推送推送策略對照表
| 場景 | 推薦方式 | 指令 | 說明 |
|---|---|---|---|
| 快速開發測試 | --change | jj git push --change @ | 不需要 bookmark |
| Dev/Beta 開發階段 | --change | jj git push --change @ | 自動生成 remote branch |
| 建立 PR/MR | --bookmark | jj git push --bookmark feature/xxx | 清晰的 branch 名稱 |
| Prod hotfix | --bookmark | jj git push --bookmark prod-hotfix-vX.Y.Z | 需要 code review |
| 環境推進 | --bookmark | jj 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 訊息,便於追蹤包含的 ticket2.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.0、prod-v1.2.1) - 不會移動,永久指向特定 commit
- 便於追蹤歷史版本和回溯
- 用於標記穩定版本(如
- Bookmark(環境指標):
- 用於標記當前環境狀態(如
dev、beta、prod) - 會隨著推進而移動
- 便於 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.13.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-release | Pull Request | Code 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-hotfix | Pull Request | Code review 和合併 |
jj bookmark delete prod-hotfix-v1.2.1 | 刪除臨時 branch | PR merge 後清理 |
jj bookmark create prod-v1.2.1 -r prod | 建立新 prod tag | 版本標記 |
3.3 CI/CD 整合
自動化流程設計
Dev 自動推進(整包)
- 當有新的 commit 推送時,CI 自動移動 dev bookmark 到最新 commit
- 自動部署到 dev 環境
Beta 手動 PR(整包)
- 手動建立臨時 bookmark:
beta-release-vX.Y.Z - 建立 PR/MR:
beta-release-vX.Y.Z→beta - Review 通過後 merge,CI 自動部署到 beta 環境
- 手動建立臨時 bookmark:
Prod 選擇性 PR(個別 ticket)
- 手動挑選特定 ticket,建立臨時 bookmark:
prod-hotfix-vX.Y.Z - 建立 PR/MR:
prod-hotfix-vX.Y.Z→prod - Review 通過後 merge,CI 自動部署到 prod 環境
- 手動挑選特定 ticket,建立臨時 bookmark:
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 devGitLab 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.com3.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-x3. PR merge 後:立即刪除
jj bookmark delete feature/ticket-001
# 或
jj bookmark delete alice/feature-x4. 共享 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 --force4. 進階技巧
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 log4.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.04.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 --allGit vs Jj 概念對照
| Git | Jj |
|---|---|
HEAD | @(working copy) |
git status | jj status 或 jj st |
git log | jj log |
git branch | jj bookmark |
| branch 是指標 | bookmark 是標籤,commit 是核心 |
Bookmark vs Git Branch:兩者本質相同,都是指向特定 commit 的可移動標籤。但在 jj 中,你通常直接操作 commit(用
jj new、jj 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 |
main | main 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 只是方便記憶
- Git:
實務上:bookmark 主要用於與 Git remote 同步(如 main、dev)和標記重要的 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
- 修改舊 commit:
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 ← 共同的父 commitQ: 從 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-001Q: 選擇性推 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.12. 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 --force5.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)
- 主要用於:
- 與 Git remote 同步(如
main、dev) - 標記重要的 commit 便於切換(如
feature/ticket-001)
- 與 Git remote 同步(如
Change ID
- Jj 追蹤變更的唯一 ID(與 commit hash 不同)
- 即使 commit hash 改變(如 rebase),change ID 仍保持不變
- 用於追蹤「同一個變更」在不同時間點的版本
Revset
- 選擇 revision 的查詢語法
- 範例:
@= 當前 working copy@-= 當前的父 commitmain::= 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!