jj(Jujutsu)ワークフロー入門 - Gitの常識を捨てて開発効率を上げる
はじめに
Gitを使った開発では、コンフリクトの解消、コミット粒度の調整、add のし忘れなど、思考を中断させる様々な要因があります。
Jujutsu (jj) という次世代のバージョン管理システムをご存知でしょうか。
Jujutsuは、Martin von Zweigbergk氏が開発したRust製のバージョン管理システムで、Gitリポジトリと直接やり取りできるなど、Gitとの高い互換性を持っています。
その目的は、Gitが持つ複雑な部分を解消し、よりスムーズな開発体験を提供することにあります。
しかし、Jujutsuのポテンシャルを最大限に引き出すには、Gitの常識(いわゆる「Git脳」)のまま使うのではなく、Jujutsuならではの考え方を理解することが重要です。
この記事では、Jujutsuの基本的な概念から、Gitとは異なる効率的なワークフロー、そしてそれを支援するエイリアス設定までを解説します。
Jujutsuの基本的な考え方
まず、GitとJujutsuの用語と概念の違いを整理します。
| Git | Jujutsu | 説明 |
|---|---|---|
commit |
change |
変更の単位。Jujutsuでは、過去のどのchangeも自由に編集できます。 |
branch |
bookmark |
特定のchangeを指すポインタ。Gitのブランチと異なり、必須ではありません。 |
git add |
不要 | ファイルの変更は自動的に現在のchangeに記録されます。 |
git stash |
不要 | 新しいchangeを作るだけで、現在の作業内容を退避できます。 |
HEAD |
@ |
現在の作業位置を示すポインタです。 |
これらの違いが、Jujutsuのワークフローにどう影響するのかを具体的に見ていきましょう。
1. コンフリクトの扱い
Gitでは、rebase 中などにコンフリクトが発生すると、それを解消するまで他の操作がブロックされるのが一般的でした。
Jujutsuでは、コンフリクトはエラーではなく、changeが持つ一つの状態として扱われます。
# rebase中にコンフリクトが発生
$ jj rebase -d main
Working copy now at: royxmyws 25a89a6a (conflict)
Parent commit: yostkice 3827a790 main
...
# コンフリクトマーカーが付いたchangeが生成される
$ jj log
@ royxmyws 25a89a6a (conflict)
│
~
コンフリクトした状態でも、他のchangeに移動したり、別の作業を進めたりすることが可能です。コンフリクトの解消は、好きなタイミングで行うことができます。
# 対話的にコンフリクトを解消する
jj resolve
これにより、「コンフリクト解消に集中しなければならない」というプレッシャーから解放されます。
2. 作業と整理の分離
Gitでは「作業 → add → commit」というサイクルで、作業の区切りごとにコミットメッセージを考える必要がありました。
Jujutsuでは、「まず作業に集中し、後からchangeを整理する」というワークフローが基本です。
# 1. 空のchangeを作って作業を開始
jj new
# 2. 複数のファイルにまたがる作業を行う
auth.py
tests.py
docs.md
# 3. 作業が終わった後、内容を振り返りながら分割・整理する
jj split # 1つのchangeをインタラクティブに分割
jj describe <change-id> -m "feat: 認証機能を実装"
jj describe <change-id> -m "test: テストを追加"
jj newで作業の区切りを作りながら進め、最後にjj splitやjj describeでコミットの歴史をきれいに整えることができます。これにより、コーディング中は実装に集中できます。
3. jj absorbによる変更の自動統合
jj absorbは、Jujutsuの強力な機能の一つです。現在の作業ディレクトリにある変更を、関連する過去のchangeに自動で統合してくれます。
# 複数のchangeが存在する状態で、いくつかのファイルを修正
src/auth.py # 以前、認証機能を追加したchangeに関連する修正
tests/test_auth.py # テストコードを追加したchangeに関連する修正
# absorbを実行すると、修正内容が自動で適切なchangeに吸収される
jj absorb
これは、Gitにおけるgit commit --fixupとgit rebase -i --autosquashを組み合わせたような操作を、より簡単かつ自動的に行うものです。レビュー後の修正作業などを大幅に効率化します。
4. stashが不要な作業切り替え
作業の途中で別のタスクに切り替える際、Gitではgit stashが必要でした。Jujutsuでは、現在の作業内容は自動的にchangeに記録されているため、単に新しいchangeを作るだけで安全に別の作業に移れます。
# feature-Aの作業中...
feature_a.py
# 緊急の修正依頼が来たので、mainから新しいchangeを作成
jj new main
# 修正作業を行い、pushする
hotfix.py
jj describe -m "fix: 緊急バグ修正"
jj git push -c @ --bookmark hotfix/urgent
# 元の作業に戻る
jj edit <feature-Aのchange-id>
stashの管理やコンフリクトを気にする必要がなく、スムーズなコンテキストスイッチが可能です。
補足: jj edit は change-id でもブックマーク名でも移動できます。
# 特定のchangeやブックマークに移動
jj edit 1c14097f
jj edit feature/12
インストール
macOSの場合はHomebrewでインストールできます。
brew install jj
その他のOSについては公式ドキュメントを参照してください。
main ブックマークの初期セットアップ
jj new main は main ブックマークが既に存在することが前提 です。
まず jj bookmark list で存在確認し、無ければ下記の手順で作成します。
1) 既存のGitリポジトリをcloneした場合
jj git clone なら通常は自動で追跡されますが、無い場合は以下で作成します。
# リモート情報を取得
jj git fetch
# origin/main を指す main ブックマークを作成
jj bookmark create main -r main@origin
もしデフォルトが master の場合は、こちらを使います。
jj bookmark create master -r master@origin
2) 新規リポジトリを1から作る場合
まず初回コミットを作成します(GitでもJujutsuでもOK)。
git init
git checkout -b main
echo "# myrepo" > README.md
git add README.md
git commit -m "chore: initial commit"
その後、直近のコミット(@-)に main ブックマークを作成します。
jj bookmark create main -r @-
これで jj new main が使えるようになります。
推奨エイリアス設定
Jujutsuのワークフローを効率化するためのエイリアス設定を紹介します。.zshrcなどに追記してご活用ください。
基本操作
# ===== Jujutsu Aliases =====
# 基本操作
alias j='jj'
alias js='jj status'
alias jl='jj log'
alias jls='jj log -r "all()" --limit 20'
alias jd='jj diff'
alias jds='jj diff --stat'
Jujutsuの主要機能
# Jujutsuの主要機能
alias ja='jj absorb'
alias jsp='jj split'
alias jsq='jj squash'
alias jde='jj diffedit'
alias jrs='jj resolve'
作業フロー
# 作業フロー
alias jn='jj new'
alias jnm='jj new main'
alias je='jj edit'
# mainブックマークの初期化(clone直後向け)
jinitmain() {
jj git fetch && jj bookmark create main -r main@origin
}
# mainブックマークの初期化(新規作成向け)
jinitmain_local() {
jj bookmark create main -r @-
}
# 複数のchangeに一度に説明を追加する (ex. jdesc @- "message 1" @ "message 2")
jdesc() {
if [ $# -eq 0 ]; then
# 引数がない場合は通常の `jj describe` を実行
jj describe
return
fi
# 引数を2つずつ(リビジョンとメッセージ)処理する
while [ $# -ge 2 ]; do
jj describe "$1" -m "$2"
shift 2
done
}
jmain と jpull は日常的に使うので、用途だけ先に覚えておくと便利です。
- jmain: mainを最新化して作業位置もmainへ移動
-
jpull [target]: mainなら更新、featureなら
targetにrebase(デフォルト: main)
Git連携
# クローン
alias jgc='jj git clone'
# ブックマーク
alias jbc='jj bookmark create'
alias jbd='jj bookmark delete'
alias jbm='jj bookmark move'
alias jbl='jj bookmark list'
# リモート操作
alias jgf='jj git fetch'
alias jgp='jj git push'
alias jgpb='jj git push --bookmark'
alias jgpc='jj git push -c @'
alias jrb='jj rebase -d'
その他
# 便利系
alias jshow='jj show'
alias jshowp='jj show @-'
alias jabort='jj abandon'
alias jundo='jj undo'
alias jop='jj op log'
jundoは、直前の操作を取り消すことができる重要な機能です。rebaseなどの破壊的な操作も、jundoがあることで安全に試すことができます。
複合コマンド(関数)
より実践的な操作をまとめた関数です。
# 新しいfeatureブックマークを作成して作業を開始(ex. jfeature 42 → feature/42が作成される)
jfeature() {
jj git fetch && jj new main && jj bookmark create "feature/$1"
}
# mainを最新に更新(fetch + bookmark更新 + 作業位置をmainへ)
jmain() {
echo "mainを origin/main に更新中..."
jj git fetch && jj bookmark set main -r 'main@origin' && jj edit main
}
# git pull相当(fetch + rebase)
# mainの場合: fetch + bookmark更新 + 作業位置をmainへ
# featureの場合: fetch + rebase to main
# 使い方: jpull [target] (デフォルト: main)
jpull() {
local target="${1:-main}"
local current=$(jj log -r @ -T 'bookmarks' --no-graph 2>/dev/null | tr -d '[]' | awk '{print $1}')
if [ "$current" = "main" ] || [ "$current" = "master" ]; then
echo "main を origin/main に更新中..."
jj git fetch && jj bookmark set main -r 'main@origin' && jj edit main
elif [ -z "$current" ]; then
echo "Warning: ブックマークが付いていません。手動で rebase 先を確認してください。"
return 1
else
echo "fetch して $target にrebase中..."
jj git fetch && jj rebase -d "$target"
fi
}
# 現在のchangeをpush(ブックマークがなければ自動作成)
jpush() {
local bookmark=$(jj log -r @ -T 'bookmarks' --no-graph 2>/dev/null | tr -d '[]' | awk '{print $1}')
if [ -n "$1" ]; then
jj bookmark create "$1" 2>/dev/null || jj bookmark move "$1"
jj bookmark track "$1" --remote=origin 2>/dev/null || true
jj git push --bookmark "$1"
elif [ -n "$bookmark" ] && [ "$bookmark" != "" ]; then
echo "Pushing bookmark: $bookmark"
jj bookmark track "$bookmark" --remote=origin 2>/dev/null || true
jj git push --bookmark "$bookmark"
else
echo "ブックマークがないため、自動作成してpushします"
jj git push -c @
fi
}
実践的なワークフロー例
1. 新機能の開発
# 1. リポジトリをクローン
jgc https://github.com/user/repo.git
cd repo
# 2. mainから作業を開始
jgf #(jj git fetch)
jnm #(jj new main)
# 3. 実装に集中する
src/feature.py
jn #(jj new)
tests/test_feature.py
# 4. 作業履歴を整理する
jl # ログを確認
jdesc @- "feat: 新機能を追加" @ "test: 新機能のテスト"
# 5. pushする
jpush feature/new
2. レビュー後の修正
# レビューでの指摘箇所を修正
src/feature.py
tests/test_feature.py
# jj absorbで修正を自動的に関連するchangeに統合
ja #(jj absorb)
# 差分を確認してpush
jds #(jj diff --stat)
jpush
Gitの考え方との比較
Jujutsuを使いこなすには、以下のGitの常識を一度リセットすることが有効です。
| Gitの常識 | Jujutsuの考え方 |
|---|---|
| まずブランチを作る | ブックマーク(ブランチ)は後からで良い |
| コミットメッセージを考えてから作業 | まず作業に集中し、後からchangeを整理する |
| コンフリクトは即座に解消が必要 | コンフリクトは後回しにでき、他の作業も可能 |
rebaseは慎重に行う |
undoがあるので、より気軽に試せる |
fixupコミットとrebaseで修正を統合 |
jj absorbで自動的に統合 |
stashで作業を一時退避 |
新しいchangeを作るだけで良い |
まとめ
Jujutsuは、Gitの代替となることを目指したバージョン管理システムであり、特にコンフリクトの扱いや、作業と整理を分離する思想において、Gitとは異なるアプローチを取っています。
- コンフリクトを状態として扱う: 解消を後回しにでき、作業の中断を防ぎます。
-
jj absorb: 修正内容を自動で関連するchangeに統合し、効率化します。 -
undo機能: 破壊的な操作も安全に試すことができます。 -
後から整理するワークフロー:
jj newやjj splitを使い、実装に集中した後に履歴を整えることができます。
Discussion