jujutsu こそ、僕たちの欲しかった VCS だ!!
前置き
この文章は…
- 想定読者はフツーの 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
することなしに、いつでも好きなリビジョンに移動したりもできます。
- jujutsu は常にステージ状態で、必要に応じて自動的にコミットされます。
- git は、基本的に名前付きブランチ上で作業を進めます。また、コミットが進むにつれて、ブランチも追従します。
- jujutsu にもブランチはありますが、リビジョンを重ねても追従しません。 必要になった時に自分でブランチを動かすことになります。
- git の commit は 作業の終わり というイメージです。また、一つ一つのコミットを積み重ねてゆく石積みのようなイメージです。
- 類似機能の jujutsu の new は 次の作業に切り替えるため のイメージです。また、過去のリビジョンの変更が手軽にできるので、より整った変更履歴を作ることができ、石積みしながら適切に順番を整えて安定させられるイメージがあります。
練習
何はともあれコミットしたい!
初っ端から衝撃的ですが、jujutsu ではコミットしません。
git commit
する必要はありません。 git add
も git stash
もありません。コミットは自動で行われます。いつでも好きなリビジョンに移動することができます。とはいえ、コミットに似た操作はあります。
新しいリビジョンを始めるには、次のコマンドを使います。
jj new
常にステージ状態で、コミットは必要に応じて自動的に行われます。また、最初から空のリビジョンにいるので、作業が終わったら ではなく、 次の作業を始めるために new
を使います。
メッセージ (コミットメッセージに相当) を付けて新しいリビジョンを始めることもできます。
jj new -m '<メッセージ>'
コミットメッセージと違って、 次のリビジョンのメッセージ です。なので、今から何をするのかを宣言する感じになります。
リビジョンを始めるためにメッセージは必須ではありません。いつでも自由にメッセージを書くことができますし、いつでも自由に変更することができます。そのためには desc
コマンドを使います。
jj desc
jj desc -m '<メッセージ>'
オプション -m <メッセージ>
がある場合は、すぐにメッセージが投入され、ない場合はエディターが立ち上がります。
一応、 コミットコマンドはあります。
jj commit -m '<メッセージ>'
内部的には jj desc -m '<メッセージ>'; jj new
のエイリアスのようです。git 経験者が困らないように用意されてるのだと思いますが、カレントワーキングコピーしか操作できないので不自由です。 jj desc
や jj 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 move
は deprecated だそうで、代わりに 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