🦖

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では「作業 → addcommit」というサイクルで、作業の区切りごとにコミットメッセージを考える必要がありました。

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 splitjj 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 --fixupgit 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 mainmain ブックマークが既に存在することが前提 です。
まず 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
}

jmainjpull は日常的に使うので、用途だけ先に覚えておくと便利です。

  • 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 newjj splitを使い、実装に集中した後に履歴を整えることができます。

参考リンク

Discussion