Grandream
jujutsu(jj)入門|Gitと比べて分かる、ステージングもstashもいらない理由
Git を使っていて、作業の途中で別の修正を頼まれたことはないでしょうか。書きかけの変更を git stash で退避し、ブランチを切り替えて修正し、戻ってきて git stash pop する。退避したことを忘れて変更が行方不明になった経験がある人も多いと思います。
jujutsu(コマンド名は jj)は、この種の手間を仕組みの側から減らすバージョン管理ツールです。保存先は Git のリポジトリそのままなので、GitHub もチームの Git 運用もそのまま使えます。変わるのは手元の操作感だけです。
この記事では、Git を一通り使える人を読み手に想定し、Git の概念と対比しながら jj の考え方を説明します。後半では、並行作業のために git worktree を使っている人に向けて、jj ならディレクトリを増やさずに済む理由も紹介します。コマンドの出力は jj 0.44.0 で確認しています。
jj は Git の「別のフロントエンド」
jj は Google のエンジニアが始めたオープンソースのプロジェクトで、Rust で書かれています。データの保存には Git のリポジトリを使うため、jj で作ったコミットはそのまま Git のコミットです。
既存の Git リポジトリで使い始めるには、リポジトリの中で次のコマンドを打つだけです。
jj git init --colocate
jj 0.44 では .git ディレクトリを持つ「共存(colocated)」の形が既定なので、--colocate を省いた jj git init でも同じ形になります。共存の状態では、同じディレクトリで git コマンドも jj コマンドも使えます。jj を試してみて合わなければ、.jj ディレクトリを消せば元の Git リポジトリに戻ります。チームの他のメンバーは、あなたが jj を使っていることに気づきません。
まず全体像を対比で押さえる
細かい話に入る前に、Git の概念が jj ではどうなるかを表にまとめます。
Git | jj | 違いの要点 |
|---|---|---|
作業ツリー + ステージング | 作業コピーコミット | 編集内容は常にコミットとして記録される |
| 不要( | 「コミットする」操作がない |
| 不要 | 書きかけも普通のコミットとして残る |
コミットハッシュ | change ID + commit ID | 書き換えても変わらない ID がある |
ブランチ | ブックマーク | 「今いるブランチ」という概念がない |
|
| 子孫のコミットは自動でリベースされる |
コンフリクトで作業が止まる | コンフリクトもコミットできる | 解消は後回しにできる |
|
| リポジトリ全体の操作を取り消せる |
以下、この表の上から順に見ていきます。
違い1: 作業コピーがそのままコミットになる
Git では、ファイルを編集しただけではまだ何も記録されていません。git add でステージングに載せ、git commit で初めてコミットになります。
jj にはステージングがありません。作業中のディレクトリの状態そのものが、@(作業コピー)と呼ばれるコミットとして扱われます。jj のコマンドを実行するたびに、その時点のファイルの状態が自動で @ に記録されます。
$ echo "hello" > a.txt
$ jj st
Working copy changes:
A a.txt
Working copy (@) : nqmyptzq 40e61ed4 (no description set)
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
a.txt を作っただけで、すでに nqmyptzq というコミットの中身になっています。説明文(コミットメッセージ)はまだ空です。
説明文を付けるのが jj describe、次の作業のために新しい空のコミットを作るのが jj new です。
$ jj describe -m "add a.txt"
$ jj new
Working copy (@) now at: okolypwo 0c0b3493 (empty) (no description set)
Parent commit (@-) : nqmyptzq 567d40f0 add a.txt
Git の git commit -m に慣れていると、「説明を付けてから新しいコミットに移る」という順番が最初は逆に感じます。実際には jj commit -m "..." という、describe と new をまとめたコマンドもあるので、Git に近い感覚でも使えます。
この仕組みのおかげで git stash は不要です。書きかけの変更は @ というコミットにすでに入っているので、別のコミットに移っても消えません。戻りたくなったら、そのコミットを指定して移動すれば続きから作業できます。
ステージングがない代わりに、一部の変更だけを別のコミットに分けたいときは jj split を使います。対話的にファイルや行を選んで、1つのコミットを2つに分けられます。Git の git add -p に当たる操作を、コミットした後でいつでもできると考えると分かりやすいでしょう。
違い2: 書き換えても変わらない change ID
先ほどの出力には、nqmyptzq 567d40f0 のように ID が2つ並んでいました。左が change ID、右が commit ID です。
commit ID は Git のコミットハッシュと同じもので、中身や説明文を書き換えるたびに変わります。change ID は「この作業単位」を指す ID で、中身を何度書き換えても変わりません。
Git で git commit --amend やリベースをすると、コミットハッシュが変わり、さっきメモしたハッシュが使えなくなります。jj では change ID で指定しておけば、書き換えの前後で同じ作業を指し続けられます。change ID はアルファベットの k〜z だけで書かれるため、16進数の commit ID と見分けがつきます。先頭の数文字だけで指定できるのも Git のハッシュと同じです。
違い3: ブランチの代わりにブックマーク
Git では、作業は必ずどこかのブランチの上で行います(detached HEAD を除けば)。コミットすると、今いるブランチが自動で前に進みます。
jj には「今いるブランチ」がありません。コミットはブランチに属さず、名前がなくても jj log に表示され、迷子になりません。名前を付けたいときに使うのがブックマークで、これは特定のコミットに貼る付箋のようなものです。
$ jj bookmark create feat-a -r utktvqzn
Created 1 bookmarks pointing to utktvqzn 32064c63 feat-a | A
ブックマークは Git のブランチとしてそのまま見えます。GitHub に push するのも、このブックマーク単位です。
$ jj git push --bookmark feat-a
注意したいのは、ブックマークは新しいコミットを作っても自動で前に進まない点です。作業を進めた後は jj bookmark set feat-a -r @ のように動かします。Git に慣れていると最初はこの違いでつまずきやすいところです。名前を考えるのが面倒なら、jj git push --change @- で change ID から自動で名前を付けて push することもできます。
違い4: 途中のコミットを直すと、後ろが自動でついてくる
Git で3つ前のコミットを直したいときは、git rebase -i で edit を指定し、修正し、git rebase --continue で後ろのコミットを積み直します。
jj では、直したいコミットに jj edit で移動して、ファイルを書き換えるだけです。後ろに続いていたコミットは自動でリベースされます。
$ jj edit utktvqzn
$ echo "from A v2" > c.txt
$ jj log
Rebased 1 descendant commits onto updated working copy.
Rebased 1 descendant commits が、後ろのコミットが自動で積み直されたことを示しています。リベースの途中という中間状態がないので、git rebase --abort で抜け出す必要もありません。
直接 jj edit で移動する代わりに、最新の位置で修正を書いてから jj squash --into <change ID> で過去のコミットへ移す方法もあります。どちらの場合も、後ろのコミットは自動でついてきます。
違い5: コンフリクトしても作業が止まらない
Git では、マージやリベースでコンフリクトが起きると、その場で解消するまで先に進めません。
jj はコンフリクトをコミットの中に記録できます。次の例では、同じファイルを別々に書き換えた A と B をマージしています。
$ jj new utktvqzn lyksxnlu -m merge
Working copy (@) now at: pznlzyvp 6d3c350c (conflict) (empty) merge
Warning: There are unresolved conflicts at these paths:
c.txt 2-sided conflict
エラーで止まらず、(conflict) という印が付いたコミットができます。jj log を見ると、コンフリクトを含むコミットは一覧上でも区別して表示されます。解消は、そのコミットに移ってファイルを直すだけで済みます。急ぎの別作業があれば先にそちらを片づけ、後で戻ってきても構いません。
途中のコミットを直した結果、後ろのコミットがコンフリクトした場合も同じです。リベースは最後まで終わり、コンフリクトしたコミットに印が付きます。
違い6: どんな操作でも取り消せる
jj は、コミットの作成、書き換え、リベース、ブックマークの移動など、リポジトリに対する操作を一つずつ記録しています。この履歴は jj op log で見られます。
$ jj op log --limit 3
@ 2761f03c2627 ... snapshot working copy
○ 5b4d5f420d1c ... edit commit 567d40f0...
○ 177e2c88cf0e ... describe commit c3936a94...
直前の操作を取り消すのは jj undo です。取り消しを取り消す jj redo もあります。
Git にも git reflog がありますが、記録されるのはブランチや HEAD の移動で、どのハッシュに戻せばよいかを自分で探す必要があります。jj の操作ログは「何をしたか」の単位で記録されるため、複数のブックマークやコミットをまとめて動かした操作も一度で元に戻せます。間違えても戻せるという安心感があると、リベースや分割を気軽に試せるようになります。
最小のワークフロー
ここまでの内容をつなげると、GitHub で PR を出すまでの流れは次のようになります。
jj git fetch # リモートの最新を取得
jj new main # main の上に新しい作業を始める
# ...ファイルを編集...
jj describe -m "ログイン画面を修正" # 説明を付ける
jj bookmark create fix-login -r @ # 名前を付ける
jj git push --bookmark fix-login # GitHub へ push
git add も git commit も出てきません。PR のレビューで修正を求められたら、そのコミットを直して jj bookmark set と jj git push をやり直せば、PR が更新されます。
git worktree を超える並行作業
ここからは、git worktree で並行作業をしている人に向けた話です。
Git で worktree が必要になる理由
Git の作業ツリーは、リポジトリにつき1つです。機能 A を実装している途中で不具合修正 B を頼まれたら、A の変更を commit するか stash しないとブランチを切り替えられません。ビルドの成果物や依存パッケージが切り替えのたびに入れ替わるのも面倒です。
git worktree add は、この問題を「作業ツリーを増やす」ことで解決します。ブランチごとに別のディレクトリを作れば、切り替えずに A と B を同時に開けます。最近は、複数の AI コーディングエージェントを並行して動かすために worktree を使う人も増えました。
ただし worktree にも制約があります。同じブランチを2つの worktree で同時にチェックアウトすることは、通常は拒否されます。ディレクトリが増えるたびに依存パッケージのインストールやビルドが必要になり、ディスクも消費します。作業が終わった worktree を片づけ忘れると、どのディレクトリが何の作業か分からなくなります。
jj ではディレクトリを増やさずに切り替えられる
jj で同じ状況になったら、A の書きかけはそのまま @ に残して、B の作業に移ります。
jj new main -m "B: 不具合修正" # A を置いたまま main の上に B を始める
# ...B を修正して push...
jj edit <A の change ID> # A の続きに戻る
jj new main は main を親にするので、B は A と並ぶ兄弟になります。Git で main から2本目のブランチを切るのと同じ形です。引数なしの jj new だと、親は今の @(A)になり、B は A の上に積まれます。
jj new main の場合 jj new の場合
○ B ← @ ○ B ← @
│ ○ A ○ A
├─╯ ◆ main
◆ main
違い1で説明したとおり、A の書きかけはすでにコミットとして記録されているため、退避の操作がいりません。ディレクトリも1つのままなので、依存パッケージやビルドの成果物も共有できます。並行する作業が3つや4つになっても、jj log を見れば全部が1つのグラフに並んでいます。
複数の作業を同時に載せる「メガマージ」
worktree では、A と B を同時に動かして確かめたいとき、どちらかのディレクトリでもう一方をマージする必要があります。
jj では、複数の作業を親に持つマージコミットを手元で作り、その上で作業できます。
jj new <A> <B> <C> # A・B・C をすべて取り込んだ状態で作業を始める
この状態で見つけた修正は、jj absorb を使うと、変更した行を最後に触ったコミット(A・B・C のどれか)へ自動で振り分けられます。どこに入れるか判断がつかない変更は、元のコミットに残ります。手で振り分けたい場合は jj squash --into <A> を使います。A・B・C はそれぞれ別の PR として push でき、マージコミット自体は push しません。
このやり方は jj のユーザーの間で「メガマージ」と呼ばれています。違い4の自動リベースと違い5のコンフリクトのコミットがあるからこそ、親のどれかを直しても全体が崩れず、すべての作業が組み合わさった状態を常に確かめられます。worktree で同じことをするには、ディレクトリごとにマージを繰り返す必要があります。
まとめると、Git では並行作業のたびに worktree を足す必要があったのに対し、jj では多くの場合ディレクトリを増やさずに済み、本当に分ける必要があるときだけ次の workspace を使えば足ります。
別ディレクトリが必要なら workspace
作業コピーそのものを分けたい場合もあります。たとえば、長いテストを走らせている間に別の作業を進めたいときです。jj では jj workspace add で、Git の worktree に相当する作業コピーを追加できます。
$ jj workspace add ../myrepo-2
$ jj workspace list
default: . lpwsztrk 84e1f0d8 A
myrepo-2: ../myrepo-2 lklyryyq 054d4ba6 (empty) (no description set)
jj の workspace は、それぞれが自分専用の @ コミットを持ちます。ブランチをチェックアウトするわけではないので、Git の「同じブランチを2か所で開けない」という制約にぶつかりません。どの workspace で作ったコミットも1つのリポジトリに入るため、別の workspace で進んだ作業も jj log からすぐに見えます。ただし、追加した workspace には .git ディレクトリがないため、その中では git コマンドを使えず、jj コマンドだけを使います。
使い始める前の注意点
jj は Git のリポジトリを使いますが、Git のすべての機能を再現しているわけではありません。
まず、Git のフック(pre-commit や pre-push)は jj のコマンドでは実行されません。チームでフックによる検査を運用している場合は、push の前に検査用のコマンドを手で実行するか、git push で送るなどの工夫が必要です。サブモジュールの扱いも限られています。
また、共存(colocated)の状態で git と jj を混ぜて使えますが、慣れないうちは片方に寄せたほうが混乱しません。特に、jj は Git のブランチをチェックアウトせず作業コピーの親コミットを直接指すため、git status では detached HEAD と表示されます。これは故障ではありません。
まとめ
jj は、Git の保存形式をそのまま使いながら、操作の考え方を作り直したツールです。作業コピーが常にコミットとして記録されるので、ステージングも stash もいりません。change ID と自動リベースのおかげで、途中のコミットを直す操作が手軽になり、コンフリクトや操作の失敗も後から落ち着いて扱えます。
並行作業についても、Git では worktree でディレクトリを増やして対処していたところを、jj は1つの作業コピーの中でコミットを行き来するだけで済ませます。複数の作業を組み合わせて確かめたいときはメガマージを使えます。作業コピーを本当に分ける必要があるときは workspace を使えます。
既存の Git リポジトリで jj git init --colocate を実行すれば、チームに影響を与えずに試せます。まずは手元の作業ブランチ1本から、jj log と jj undo に触れてみるのがおすすめです。
関連記事
Grandream
株式会社グランドリーム
AI・システム開発のプロフェッショナルチームです。AIエージェント・業務自動化・Webシステム開発などを手がけています。




