■はじめに
jj(jujutsu) は修正可能である change という単位で履歴を管理するバージョン管理システムである。
jj は git よりも柔軟な履歴書き換えが行えるがgit には無い概念も多く、理解するのに苦労する。
git と jj で出来ることの比較や、 コマンド対比表の記事はよく見かけるが、考え方の比較をした記事はあまり見ないため、git を理解している人向けに 考え方(メンタルモデル)を比較する 記事を書いてみた。
これが jj を理解する一助になれば幸いである。
【注意】
この記事は 考え方の比較 を行う記事であるため、jj が git レポジトリと互換性を持つことなど、触れていない部分もあります。
■履歴の持ち方 の考え方
◆ git
git では、履歴は親への参照を持つ commit の集合である。
git の世界には修正不可能な commit しか存在しないため、 「commitグラフ」 だけで全ての履歴を管理できる。
履歴を書き換えられるように見える操作は、実は新しい commit を作っているだけである。
◆ jj
jj では、履歴は親への参照を持つ change の集合である。
jj の世界では修正可能な change で履歴を管理するため、 「changeグラフ」 だけでは change 自体を修正したときに以前の状態が失われてしまう。
そのため 「changeグラフ」 に加えて 「change 自体のスナップショット履歴(evolog)」 「changeグラフのスナップショット履歴(operation log)」 が存在する。
ちなみに change の実体は change-id を記録した commit である。
change を書き換えられるように見える操作は、実は change-id を引き継いだ新しい commit を作っているだけだ。
これを知らないと、作業コピーに対応する change を指す単語が working copy 『commit』 であることなど、色々な箇所で混乱が生じる。
■作業コピーとレポジトリの関係 の考え方
◆ git
gitでは、レポジトリ内のcommit、staging領域、作業コピーの3種類の領域がある。
ある操作はレポジトリ内のcommitへの操作かもしれないし、
ある操作はstaging領域への操作かもしれないし、
ある操作は作業コピーへの操作かもしれないし、
ある操作は作業コピーとstaging領域への操作かもしれない、
……と複雑である。
◆ jj
jjでは、作業コピー、レポジトリ内のcommit(=change)の2種類の領域しかない。さらに jj は全てのコマンド実行時に自動的に両者を同じ内容に更新する。
つまり実質的には、jjにはレポジトリ内のcommitという1種類の領域しかないと考えてよい。
すべての操作は、レポジトリ内のcommitに対する操作である。
(evolog と operation log はレポジトリ変更記録なので、ここでは考えないものとする)
■修正衝突 の考え方
◆ git
git では、衝突した情報は staging領域 に持っている。
具体的にはstaging領域内に、衝突したファイルについて base, side1, side2 の ファイルスナップショットを持っていて、そこから作業コピーにコンフリクトマーカー入り内容を書き出す。
衝突解消するには、ファイル内容からコンフリクトマーカーを削除して git add コマンドを実行することで staging領域を衝突していない状態に修正し、git commit などで衝突していない commit を作成する。
staging領域の衝突を解消しなければ commit が作れないため、衝突があると次の commit を作れない。
◆ jj
jj では、衝突した情報は commit に持っている。
具体的には commit 内に、衝突したファイルについて base, side1, side2 のファイルスナップショットを持っていて、そこから作業コピーにコンフリクトマーカー入り内容を書き出す。
衝突解消するには、ファイル内容からコンフリクトマーカーを削除して、なにかのjjコマンドを実行する。
jjコマンド実行時に、ファイル内容からコンフリクトマーカーが削除されたことが検知されると、commit が衝突していない状態に自動更新される。(change-id を引き継いだ、衝突していない commit が新たに作成される)
衝突は commit 内に持っているため、衝突があっても次の commit を作ることができる。
(その場合は次のコミットにも衝突した情報が引き継がれる)
■ブランチ≒ブックマーク の考え方
◆ git, jj
git の branch、 jj の bookmark は、どちらも commit-id を指す参照名のこと。
ほぼ同じ概念だが、細かい違いがある。
一番大きな違いは、change を修正して change-id を引き継ぐ commit を作成したとき、 bookmark は自動的に新しい commit に移動すること。
そのため実質的には bookmark は change を指すものだと考えたほうが分かりやすい。
◆ 対応表
| git | jj |
|---|---|
|
ローカルブランチ ローカルレポジトリにある、ブランチ。 git commit で新しい commit 作ったとき、最新 commit に自動移動する。 |
ローカルブックマーク ローカルレポジトリにある、ブックマーク。 jj commit, jj new で新しい change-id の change を作ったとき、最新 change に自動移動しない。 |
|
リモート追跡ブランチ ローカルレポジトリにある、リモートブランチ最終位置を参照するブランチ。 |
リモートブックマーク ローカルレポジトリにある、リモートブックマークの実際の状態を参照するブックマーク。 |
|
リモートブランチ リモートレポジトリにある、ブランチ。 |
リモートブックマークの実際の状態 リモートレポジトリにある、ブックマーク。 ……だが jj には明確にこれを意味する用語は無い。 強いて言えばactual state of the remote bookmark(リモートブックマークの実際の状態) |
|
「上流ブランチ」の設定 push/pull先リモートの設定。 1つのローカルブランチに1つの上流ブランチが設定できる。 |
「追跡されたブックマーク」の設定 push/pull先リモートの設定。 1つのローカルブックマークに、好きな数のリモートの追跡設定ができる。 |
■pull/fetch(リモートからの取得) の考え方
◆ git
git では、リモートから取得した内容を無条件でローカルブランチへマージする。
git pull は以下の操作を行う。
- リモート追跡ブランチを、リモートブランチの最終位置に移動させる (git fetch)
- ローカルブランチに、リモート追跡ブランチをマージする (git merge)
- マージ衝突が発生した場合はマージコミットが作成されずに、衝突状態で止まる
パターン1. リモートのみ進んでいる場合
パターン2. ローカルとリモートが別々に進んでいる場合
◆ jj
jj では、リモートから取得した内容が分岐していなければそのまま履歴に追加し、分岐していればブックマーク衝突状態にする。
ブックマーク衝突状態はブックマーク自体が持つステータスで、リモートとローカルが分岐して進んでいると衝突状態になる。
これは push したときリモート側でどの commit が正しいブックマークなのか決められないことを示す状態であるため、ブックマーク衝突状態が解消されるまで push は行えない。
jj git fetch は以下の操作を行う。
- リモートブックマークを、リモートブックマークの実際の状態の位置に移動させる
- ローカルブックマークを移動させるか、ブックマーク衝突状態にする。
- ローカルブックマークが先行している場合は、何もしない。
- リモートブックマークが先行している場合は、ローカルブックマークをそこに移動させる (git の ff-merge 相当)
- ローカルブックマークとリモートブックマークが別々に進んでいる場合は、ブックマーク衝突状態にする。
パターン1. リモートのみ進んでいる場合
パターン2. ローカルとリモートが別々に進んでいる場合
ブックマーク衝突状態の解消
ブックマーク衝突状態は、jj bookmark move コマンドの実行など明示的な指示でブックマークを「作業者が正しいと考える位置」へ移動することで解消される。
例えば、以下のような作業でブックマーク衝突状態が解消する。
- step1:
jj newなどを実行して、マージ commit を作成する
(開発チームのルールに従おう。マージではなく rebase するルールの場合もある) - step2:
jj bookmark moveで、ブックマーク位置を明示的に上記位置に移動させる
■push(リモートへの送信) の考え方
◆ git
git では、以下のチェックが成功した場合、ローカルにのみ存在する commit がリモートに送信される。
- ローカルブランチがリモートブランチより先行しているか(ローカルブランチの祖先がリモートブランチか)判定して、先行していない場合はpush失敗する
このチェックにより、リモートレポジトリのコミットが上書きされて消えてしまわないよう制限している。
git push --force オプションで、上記チェックを無視して明示的に上書きを指定することで上書きできる。
git ではリモートに push した commit を上書きすることは通常は無いため、--force オプションで明示的に指定しなければ上書きできない。
パターン1. ローカルのみ進んでいる場合
パターン2. ローカルとリモートが別々に進んでいる場合
パターン3. パターン2のpush失敗後、ローカル側でマージをしてpush成功させる場合
◆ jj
jj では、以下のチェックが成功した場合、ローカルにのみ存在する commit がリモートに送信される。
- ブックマーク衝突状態か判定して、衝突状態の場合はpush失敗する
- リモートブックマークと、リモートブックマークの実際の状態(actual state of the remote bookmark)が一致しているか判定し、一致しない場合はpush失敗する
※一致していればpushしてリモートレポジトリのchangeを上書きする。(これはgit push --force-with-leaseと同じ動き)
ブックマーク衝突状態かのチェックで、「明示的に指示しない限り」リモートレポジトリのコミットが上書きされて消えてしまわないよう制限している。
リモートブックマーク一致チェックで、最後のfetch後にリモートが進んでいる場合の上書きpushを防止している。
jj では change の修正を行い、 rebase された新しい commit が作られることが日常的にあるため、「明示的に指示すればリモートレポジトリのコミットが上書きできる」ようにしてあるのだと思われる。
パターン1. ローカルのみ進んでいる場合
パターン2. ローカルとリモートが別々に進んでいる場合
パターン3. パターン2のpush失敗後、ローカル側でブックマーク衝突を解消してpush成功させる場合
以上。
















