🥷

jujutsu こそ、僕たちの欲しかった VCS だ!!

2024/04/15に公開

前置き

この文章は…

  • 想定読者はフツーの git ユーザー です。 前提知識として git がいると思います。
  • jujutsuの日本語の記事が少ないので、ひとまず使えるようになるべく、簡単な紹介と操作体験ができることを目指したものです。
  • できるだけ調べて分かったことを書いてあり、今後もある程度は育ててゆくつもりですが、正式なリファレンスは 公式jj help を参照することをお勧めします。

jujutsu の概要

VCS (バージョン管理システム) が世に登場してから、多くの機能が実装されてきました。それぞれの機能が実装された当時はそれがベストだったはずですが、今になってみると建て増しを続けた温泉旅館のように複雑になっているように思います。

君のレポジトリを領域展開 - 次世代バージョン管理システム Jujutsu の世界 を参考に、しばらく jujutsu を触ってみたところ、 git の難しさ、複雑さ、操作の多さを再認識させられ、それらを再解釈してシンプルにまとめ直したものが jujutsu のように感じました。 Java のバイトコードを生成する別言語 Clojure や Scala のように、 git リポジトリを生成する別 CLI として jj があるようにも思いました。

特徴はこんな感じでしょうか。

  • リポジトリとして git リポジトリを使用するので、互換性があります。
  • コマンドはシンプルで整理されていて、操作が簡単です。
  • パラダイムが git と少し違うので、最初は戸惑うかもしれません。

git との違いもいくつか紹介しておきます。

  • git は未ステージ状態で作業を進めます。そのため、ステージングとコミットの手順を踏む必要がありました。
    • jujutsu は常にステージ状態で、必要に応じて自動的にコミットされます。 git add, git commit の作業から解放されます。git stash することなしに、いつでも好きなリビジョンに移動したりもできます。
  • git は、基本的に名前付きブランチ上で作業を進めます。また、コミットが進むにつれて、ブランチも追従します。
    • jujutsu にもブランチはありますが、リビジョンを重ねても追従しません。 必要になった時に自分でブランチを動かすことになります。
  • git の commit は 作業の終わり というイメージです。また、一つ一つのコミットを積み重ねてゆく石積みのようなイメージです。
    • 類似機能の jujutsu の new は 次の作業に切り替えるため のイメージです。また、過去のリビジョンの変更が手軽にできるので、より整った変更履歴を作ることができ、石積みしながら適切に順番を整えて安定させられるイメージがあります。

練習

何はともあれコミットしたい!

初っ端から衝撃的ですが、jujutsu ではコミットしません。

git commit する必要はありません。 git addgit stash もありません。コミットは自動で行われます。いつでも好きなリビジョンに移動することができます。とはいえ、コミットに似た操作はあります。

新しいリビジョンを始めるには、次のコマンドを使います。

jj new

常にステージ状態で、コミットは必要に応じて自動的に行われます。また、最初から空のリビジョンにいるので、作業が終わったら ではなく、 次の作業を始めるために new を使います。

メッセージ (コミットメッセージに相当) を付けて新しいリビジョンを始めることもできます。

jj new -m '<メッセージ>'

コミットメッセージと違って、 次のリビジョンのメッセージ です。なので、今から何をするのかを宣言する感じになります。

リビジョンを始めるためにメッセージは必須ではありません。いつでも自由にメッセージを書くことができますし、いつでも自由に変更することができます。そのためには desc コマンドを使います。

jj desc
jj desc -m '<メッセージ>'

オプション -m <メッセージ> がある場合は、すぐにメッセージが投入され、ない場合はエディターが立ち上がります。

一応、 コミットコマンドはあります。

jj commit -m '<メッセージ>'

内部的には jj desc -m '<メッセージ>'; jj new のエイリアスのようです。git 経験者が困らないように用意されてるのだと思いますが、カレントワーキングコピーしか操作できないので不自由です。 jj descjj new はもっと自由で、どのリビジョンでも操作できます。

jj desc -r @-

ここで -r オプションが登場しました。これはリビジョンを指定するものす。 @ は git の HEAD で、 - は git の ^ です。すなわち @- は親リビジョンを指していて、このコマンドは親のメッセージを直接直すものです。 git commit --amend よりお手軽ですね。対象は親リビジョンに限らず、過去のどのリビジョンのメッセージも直せます。-r にリビジョンを表す ID やタグなど渡すだけです。 new も同様です。

jj new -r <リビジョン>

新しいフィーチャーを作り始めたいなら jj new -r develop となるでしょう。

理屈よりとにかく体験したい!

インスコ

brew install jj
$ jj config set --user user.name '<username>'
$ jj config set --user user.email '<e-mail>'

サクッとリビジョンを積んでみる

mkdir jjtry
cd jjtry
jj git init
jj desc -m 'step1'
echo 'line 1' > sample.txt
jj new -m 'step2'
echo 'line 2' >> sample.txt
jj new -m 'step3'
echo 'line 3' >> sample.txt
jj new -m 'step4'

簡単な三つのリビジョンを作りました。 jj log でログを確認します。

jj log
@  oxmkyrmm tokutomi@degino.com 2024-04-14 21:33:51 adb179df
│  (empty) step4
◉  wqyulpyx tokutomi@degino.com 2024-04-14 21:33:51 7d8d70bf
│  step3
◉  yxvqwqyu tokutomi@degino.com 2024-04-14 21:33:51 9f11e4f5
│  step2
◉  ovkwzxtr tokutomi@degino.com 2024-04-14 21:33:51 32e43970
│  step1
◉  zzzzzzzz root() 00000000

左上の @ は現在地 (git の HEAD) です。各行の左端がリビジョン ID (Change ID) で、右端がコミット ID です。

先祖のコミットメッセージを変えてみる

コミットメッセージ 'step2' を 'ステップ 2' に変えてみましょう。

jj desc -m 'ステップ2' -r @--

@-- は親の親を指しています。コミット ID やリビジョン ID (Change ID) を指定することもできますが、各環境によって値が異なるため、自分の環境で発番された ID を使ってください。

jj desc -m 'ステップ2' -r yxvqwqyu

ログで先祖のコミットメッセージが変わっているのを確認します。

jj log
@  oxmkyrmm tokutomi@degino.com 2024-04-14 21:34:16 445388bf
│  (empty) step4
◉  wqyulpyx tokutomi@degino.com 2024-04-14 21:34:16 ee15e03d
│  step3
◉  yxvqwqyu tokutomi@degino.com 2024-04-14 21:34:16 35d88927
│  ステップ2
◉  ovkwzxtr tokutomi@degino.com 2024-04-14 21:33:51 32e43970
│  step1
◉  zzzzzzzz root() 00000000

先祖の変更内容を変えてみる

jj new @--
sed 's/line 2/二行目/' sample.txt > sample2.txt
rm sample.txt
mv sample2.txt sample.txt
jj squash

@-- は二つ前のリビジョンを指します。jj new で新しいワーキングコピーを作って、ファイルを書き換えます (ファイルを書き換える振る舞いが sed によって異なるため、 rm & mv を使ってます)。そして jj squash で ステップ2 のコミットに押し込んでみました。 jj new & jj squash の組み合わせがよくある作業パターンになります。
そして、見事にコンフリクトしました。

jj log
@  klzzyxvy tokutomi@degino.com 2024-04-14 21:35:10 59d682ea
│  (empty) (no description set)
│ ◉  oxmkyrmm tokutomi@degino.com 2024-04-14 21:35:10 9f003f6d conflict
│ │  (empty) step4
│ ◉  wqyulpyx tokutomi@degino.com 2024-04-14 21:35:10 4a6654a7 conflict
├─╯  step3
◉  yxvqwqyu tokutomi@degino.com 2024-04-14 21:35:10 8408d482
│  ステップ2
◉  ovkwzxtr tokutomi@degino.com 2024-04-14 21:33:51 32e43970
│  step1
◉  zzzzzzzz root() 00000000

ログの右端に conflict マークがあります。 git の場合、コンフリクトを解消しないとマージが進みませんが、 jujutsu ではコンフリクト状態のリビジョンが成立します。コンフリクトを解消するタイミングを遅らせることができますし、なんなら放置していても構いません :-P。もちろん直すこともできますので、直しましょう。

jj edit wqyulpyx   # <- step3 のリビジョン ID

ここでは新たなリビジョンを作らずに jj edit で直接 step3 のリビジョンを修正します (推奨されているのは jj new & jj squash のようです)。エディターで sample.txt を開きます。

line 1
<<<<<<<
+++++++
二行目
%%%%%%%
 line 2
+line 3
>>>>>>>

何やら見慣れないコンフリクトの表現ですが、ここでは気にせず step3 で期待する内容に書き換えて保存します。

line 1
二行目
line 3

ログを確認するとコンフリクトが解消されています。

jj log
Rebased 1 descendant commits onto updated working copy
◉  oxmkyrmm tokutomi@degino.com 2024-04-14 21:36:13 1be2e5c9
│  (empty) step4
@  wqyulpyx tokutomi@degino.com 2024-04-14 21:36:13 78d2e3b0
│  step3
◉  yxvqwqyu tokutomi@degino.com 2024-04-14 21:35:10 8408d482
│  ステップ2
◉  ovkwzxtr tokutomi@degino.com 2024-04-14 21:33:51 32e43970
│  step1
◉  zzzzzzzz root() 00000000

この時コミット操作はしていません。ファイルを保存しただけです。リポジトリの難しい操作をすることなく、先祖のリビジョンを整ったものにすることができました。

こんな時、どうする?

マージ

jj new <リビジョン>...

git merge は、作業の終わったコミットをどこかのブランチ (通常はコミットの派生元) に組み込むという思考だと思います。 jujutsu では 新たな作業を始めるために 複数のリビジョンを集めて合成する感じになります。専用のコマンドを覚える必要がなく jj new でできるのが斬新に感じました。

git commit --amend

「この作業、前のコミットにマージしたいなぁ〜。」

jj squash

「あ、コミットメッセージに誤字を入れてしまった! 直したい!!」

jj desc -m '<メッセージ>'

共同作業 (リモートリポジトリ) はどうやる?

github を介してリポジトリを共有することはできますが、 jujutsu 上の操作履歴は欠落します。jujutsu-hub ができるまでは、 git の粒度での共有ということになるかと思います。

DropBox などのファイル共有サービスに jujutsu リポジトリを置いて共有できるように作っているらしいのですが、未確認です (オフライン作業すると衝突しそう)。

用語

リビジョン

git から見るとコミットに見えます。また、公式でも synonym と説明してます。ただ、ログを見ると、リビジョン ID (公式では Change ID) とコミット ID の二つの ID があり、コミット ID はよく変わります。例えば jj desc -m '新しいメッセージ' とした場合、コミット ID は変化しますが、リビジョン ID は変化しません。 jj edit で内容を変更した場合も同様です。
従って、利用者が jj new などで、操作する変更の一塊がリビジョンで、コミットはさらに小さな単位で jj が自動的に行う単位と理解しておいた方が良いような気がします。

リブセット

いくつかのリビジョンの塊です。表現する関数式が用意されています。

記号 説明
@ 現在のリビジョンです。 git の HEAD と同等です。
- 親リビジョン
+ 子リビジョン
:: 範囲
説明
@- 現在のリビジョンの親を指します。 git の HEAD^ と同等です。
@-- 現在のリビジョンの親の親を指します。 git の HEAD^^ と同等です。
x+ x リビジョンの子を指します。
x++ x リビジョンの孫を指します。
x::y x リビジョンから y リビジョンまでのリビジョンの塊です (リブセット)
x:: x を含んだ子孫一式のリブセットです。
::x x を含んだ先祖一式のリブセットです。

x や y は、リビジョンを特定する ID などです。 jj では、タグ名、ブランチ名、 git ref、コミット ID、リビジョン ID (Change ID) の順で探し、最初に見つけたリビジョンになります。

使い方の例

jj log -r @---::@     # 親を四代遡ったログを取得

参考: Revsets

コマンド紹介

準備

jj git init

リポジトリに git を使います。ネイティブのリポジトリもありますが、 Ver. 0.16.0 時点では非推奨です。既に git で管理されているディレクトリでも、空っぽのディレクトリでも同じコマンドです。なお、 git コマンドを併用したい (or 各種 GUI のツールを使いたい) 場合は --colocate オプションを付けます。

jj git init --colocate

github のリポジトリをクローンすることもできます。

jj git clone <リポジトリ> [--colocate]

状況確認

jj st|status

リビジョンのステータスを表示します。 git status 相当。

jj show [<リビジョン>]

リビジョンの説明や変更内容を表示します。 git show 相当。任意のリビジョンの内容を表示できます。

jj files [-r <リビジョン>]

リビジョンのファイルを一覧表示します。 -r オプションで、任意のリビジョンのファイル一覧を表示できます。

jj log

リビジョン履歴をツリー表示します。

jj obslog

リビジョン内の変更の経緯を表示します (obslog は obsolescent changes log の略らしい)。

jj op log

操作ログを表示します。

jj diff [-r <リビジョン>]

リビジョンの差分を表示します。 -r オプションで、任意のリビジョンの差分を表示できます。

jj tag list

タグの一覧を表示します。

リビジョン移動

jj prev [AMOUNT] [--edit]

ワーキングコピーを一つ前の親に移動します。 --edit を付けると、親がワーキングコピーになります。 AMOUNT を付けると、その分だけ祖先に移動します。

jj next [AMOUNT] [--edit]

ワーキングコピーを一つ先の子に移動します。 --edit を付けると、子がワーキングコピーになります。 AMOUNT を付けると、その分だけ子孫に移動します。

jj edit <リビジョン>

任意のリビジョンへ移動します。

ブランチ操作

jj branch list

ブランチの一覧を表示します。

jj branch create [-r <リビジョン>] <ブランチ名>

ブランチを作成します。

jj branch set [-r <リビジョン>] <ブランチ名>...

ブランチを移動します。リビジョンを指定しないと、現在のワーキングコピーになります。

jj branch rename <元のブランチ名> <新しいブランチ名>

ブランチ名を変更します。

jj branch delete [<ブランチ名>]...

ブランチを削除します。

jj branch forget [<ブランチ名>]...

未調査です。

リビジョン操作

jj desc|describe [-m '<メッセージ>']

リビジョンの説明を書き込みます。 -m を省略した場合は、エディタが開きます。

jj new [-m 'メッセージ'] [-r <リビジョン>] [-A <リビジョン>] [-B <リビジョン>]

新たな子リビジョンを作成します。任意のリビジョンに子を作れます。省略した場合は現在のリビジョンの子になります。同時に子リビジョンのメッセージを入れることもできます。

-A-B は、それぞれ After や Before の意味で、親リビジョンと子リビジョンの間に挟まれる形でワーキングコピーが作成されます。

jj edit <リビジョン>

指定したリビジョンに移動します (編集モードになります)。

jj move [--from <リビジョン>] --to <リビジョン>

リビジョンを --to で指定した先にマージします。 squash は jj move --from @ --to @-- と同じです。

注意: jj movedeprecated だそうで、代わりに jj squash を使うようです。

jj split [-r <リビジョン>]

指定したリビジョン (指定しなければカレント) の変更をいくつかのリビジョンに分割します。専用のエディタが開きます。

jj squash [-r <リビジョン>]

現在のワーキングコピーの変更を他のリビジョン (指定しなければ親) にマージします。

jj unsquash
jj duplicate [<リビジョン>]...

リビジョンを複製します。同じ親、同じ変更内容を持つリビジョンが作成されます。 リブセットを指定すると、複数のリビジョンが複製されます。 (e.g. jj duplicate zzz+::)

jj abandon [<リビジョン>]...

リビジョンを捨てます。

jj amend

jj squash のエイリアスです。

git 操作

jj git init [--colocate]
jj git clone [--colocate]
jj git remote
jj git fetch

リモートリポジトリーの変更をローカルリポジトリーに取り込みます。 git fetch 相当。

jj git push

ローカルリポジトリーの変更をリモートリポジトリーにプッシュします。 git push 相当。

プッシュする前に jj branch set コマンドを使って、プッシュしたいリビジョンまでブランチを動かしておく必要があります。

jj git import

git リポジトリの変更を取り込ます (自動で行われるので、このコマンドは使わない気がします)。

jj git export

jj の変更を git リポジトリにエクスポートします (自動で行われるので、このコマンドは使わない気がします)。

その他

jj restore [ファイルパス]...

変更前の内容を復元します。ファイルパスを指定すると、特定のファイルのみ復元されます。

jj untrack <ファイルパス>...

指定したファイルのトラッキングを停止します。

jj workspace

未調査です。

jj undo [<操作 ID>]

jj op undo のショートカットです。 jj undo だけなら直前の操作を取り消します。続けて操作すると REDO になるようです (直前の undo の取り消し?)。

複数の操作を取り消したい場合は jj op log を参照し、取り消したい操作の ID を渡すのが良さそうです。 @-- なども使えますが、 jj を操作したすべての操作が含まれるようで、思った通りに UNDO するのはなかなか難しいと思います。

メモ

  • GUI ツールは gg があります。
  • github と連携すると、リビジョンが変更不可能 (immutable) になることがある。
    • main ブランチは、祖先から push 済みの main までが immutable っぽい。
    • develop ブランチは、push 済みの最新タグまでが immutable っぽい。
    • immutable になると jj log では表示されなくなる (リブセットを指定すれば見れる)。
  • シャロークローンは jj init できない。 - jj currently does not support shallow/partial clone (cloned with the --depth or --filter argument)

Discussion