Jujutsu を用いた開発ワークフロー: Squash Workflow と Megamerge Workflow
業務を含めた開発フロー全般で Jujutsu (jj-vcs/jj, jj) を使うようになってからもうすぐ半年になる。
現在の個人的な jj 利用ワークフローと、 jj について便利だと感じている部分をまとめておこうと思う。
Jujutsu について
あらためて紹介する必要もないが、 jj は Git 互換のバージョン管理システムだ。
Git でできることのほとんどは jj でもできる。 jj でできることも大抵は Git でできる。
しかし、同じことをやろうとしても jj のほうが圧倒的に簡単にできる場合が多い。
たとえば、過去の特定のコミットだけを修正したいときに Git なら git rebase -i を利用し、該当のコミットに edit フラグを立てて編集モードに入り…と複雑な手順を踏む必要があるが、 jj の場合は jj edit <rev> で該当の変更を選択し、ファイルを編集するだけで簡単に修正できる。
Rebase 操作を例にとっても、 jj rebase --before <before> --after <after> --revision <rev> のように、どの変更をどの位置に Rebase するかが直感的にわかる形で操作ができる。
それらに加えて jj undo, jj redo により操作の Undo / Redo ができるため、変更履歴の操作を行うことへのハードルが非常に低い。
この操作感に慣れてしまうと、もう素の Git に戻る気はなくなってしまう。
jj そのものの紹介は既にさまざまなリソースがあるので、ここでは参考文献を紹介するだけにとどめておく。
Squash Workflow
jj を用いたワークフローの中でも特に Git ユーザーにとって馴染みやすいのが Squash Workflow だ。
私も基本的にはこのワークフローに則って作業をしている。
Git ではコミットに含めたい変更内容を index にステージング (git add) してからコミット (git commit) するが、 jj の Squash Workflow ではワーキングコピーにある変更内容を jj squash により過去の変更に移動させる。 jj には Git の index に対応するエリアはないが、このワークフローにおいてはワーキングコピーにある変更がステージングされた変更に該当すると捉えると理解しやすい。
このワークフローについては次のページの解説がわかりやすい。
冒頭で過去のコミットの修正を jj edit <rev> で行う例を挙げたが、 Squash Workflow を用いると次のようになる。
jj new <rev>で修正したい変更から新しい変更を作成する- 修正内容をワーキングコピーに反映する
jj squashにより、ワーキングコピーの変更を元の<rev>に移動させる
対象の変更に直接 jj edit を実行するよりも、このように Squash Workflow を用いたほうが、元の変更に対してどのような修正をしたかを確認してから反映できるため好ましい。
このあたりは Don’t Edit - Ju! Ju! Tsu! で解説されている。
Megamerge Workflow
このワークフローは、ある意味では Squash Workflow の派生版とも言える。 詳しくは参考ページを読んでもらえると良いが、同じリポジトリ上で複数の作業を行うとき、ブランチを切り替える必要なく同じワーキングコピー上ですべての変更を行うことができる。
- Jujutsu megamerges for fun and profit - Isaac Corbrey
- Jujutsu Megamerges and jj absorb - Sym·poly·mathesy
- Working on all of your branches simultaneously - Steve's Jujutsu Tutorial
- Dispatching Edits from Megamerges - Ju! Ju! Tsu!
モノリポ構成で複数のコンポーネントに対する変更を並行で進める場合などは特にこのワークフローにフィットする。私も業務で触るリポジトリはだいたいこのパターンにあてはまるため、このワークフローをよく利用している。 たとえば、次のような変更を積んだリポジトリを考えてみる。
変更グラフのすぐ右に表示されている8桁の文字列が Change-ID (変更ID) を表している。 Change-ID は太字になっている部分まででユニークに扱われる。
この例では、 ID が r の変更から4つの変更 (u, v, sx, sr) に派生して、それらを n でマージ (megamerge) している。
この仮想リポジトリでは、 Server 側の変更と Client 側の変更をローカル環境で同期させながら、リモート側には別ブランチとして Push してそれぞれ別の Pull Request として変更を提出することを想定している。
また、他メンバーに共有する必要のない flake.nix, flake.lock や .harper-dictionary を含む変更を Private な変更として扱い、リモートには Push しない方針としている。 (私の場合チームメンバーで Nix や Harper を利用している人はいないので、手元に置いておけば十分)
jj にはブランチがなく、代わりにコミットに対するブックマーク (bookmark) を指定するようになっている。そのため、 sx, sr のみに bookmark を指定して Push することで、リモートには不要な変更を Push せずに済む。
各ブランチに変更を加えたいときは、Squash Workflow と同じくワーキングコピー上で変更内容を作成し、 jj squash で該当する変更に移動させれば良い。
さらに、 jj squash の代わりに jj absorb を利用すれば、多くの場合で変更内容を自動的に適切な変更に Squash してくれる。先の例では、 .harper-dictionary に関する変更や flake.nix に関する変更がワーキングコピーに一緒に含まれていたとしても、 jj absorb を実行するだけで自動的に v や u の変更に取り込んでくれる。編集しているファイルや編集箇所をもとに適切な取り込み先を判別しているのだ。 Megamerge Workflow を利用する際はこれが非常に便利なので、覚えておくと良い。