
2026/04/21 6:32
「楽しさと利益のためのジュージ・メガマージ」
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
記事は、JUJUTSU で導入される簡素化されたバージョン管理ワークフロー「megamerge」について紹介しています。これは、オクトパス合併(3 つ以上の親を持つ合併)を用いて、複数の開発ブランチを単一のローカルのベースコミットに統合します。不安定なブランチの先頭に直接作業を行う代わりに、開発者は関連する上流ブランチ(機能追加、バグ修正、設定など)を親とする空の megamerge コミットを作成し、作業コピーが常にすべての変更を統合してコンパイル可能になるように確保するとともに、タスクを変更する際に予期せぬリモート合併競合を排除します。
megamerge を開始するには、
jj new x y z を実行した後に jj commit --message "megamerge" を実行し、指定されたブランチを親とする空のコミットを作成します。すべての書き込みは、このベース(WIP ス tack)の上で実施され、megamerge がローカルに留まることで安定性を保ちます。個々の機能ブランチは遠隔リポジトリへ通常通り公開し続けますが、megamerge 自体はプッシュされません。
jj absorb を用いて上流の変更を自動的に統合するワークフローでは、約 90% の更新を後続的可変コミットに圧縮して同定します。新しい作業で独自のコミットが必要になる場合は、bookmark を更新しながら WIP を megamerge の下に移動するために jj rebase --revision y --after x --before megamerge を使用します。並列ス tack の管理には revset アリヤス(例: "closest_merge(to)" = "heads(::to & merges())")および stack コマンドを用い、stage = ["stack", "closest_merge(@).. ~ empty()"] というようにのアリヤスで一度にステージリングし、その後 jj stage を実行します。
メインブランチ(
trunk())との同期を維持するには jj rebase --onto trunk() を使用でき、これは自分が所有するコミットに対して動作し、他者によるブランチは保護されます。Mutable コミットのみを安全に trunk へ rebase するための場合は、restack = ["rebase", "--onto", "trunk()", "--source", "roots(trunk()..) & mutable()"] というようなアリヤスを使用します。全体として、このアプローチは合併による面倒を大幅に削減し、新しい作業が堅牢な統合された基盤の上に自然と構築されるような円滑で協力的なサイクルをサポートします。本文
この記事は、中途半端な Juju ツーザー(Jujutsu ユーザー)向けだけでなく、Juju ツーザーとして Git に興味を持っている方々にも読んでもらいたく執筆しました。私自身、重度の Juju ツーザーでございまして、毎日の開発ワークフローにおいて、コミュニティでは俗に「メガマージ(Megamerge)」と呼んでいる作業手順への依存度を日に日に高めています。残念ながら、この手法は数人のパワーユーザーを除けばあまり議論されることはなく、それをどのように行うのか、そしてなぜそれが特に複雑な開発環境や、小規模な PR を多くリリースする際にもたらす恩恵なのかについて共有したいと思いまして。急がれる場合、最後のセクションまでスキップしてクイックヒントをチェックしてくださいませ。
メージコミットとは思っているようなものではありません
平均的な Git ユーザー(あるいは、より高度なワークフローにまだ触れていない Juju ツーザー)の場合、「メージコミット」には何ら特別なものがあるとは驚かれます。それは特別なケースであり独自のルールを持つものではありません。単に親が複数ある通常のコミットに過ぎず、内容が空である必要さえありません!
@ myzpxsys イサク・コーブレイ 12 秒前 634e82e2 │ (empty) (説明未設定) ○ mllmtkmv イサク・コーブレイ 12 秒前 git_head() 947a52fd ├─╮ (empty) ものをつなぐ │ ○ vqsqmtlu イサク・コーブレイ 19 秒前 f41c796e │ │ deps: クアンタム マニフォールド リゾルバーを固定化 ○ │ tqqymrkn イサク・コーブレイ 19 秒前 0426baba ├─╯ storage: 一時的キャッシュのマニフォールドを整列化 ◆ zzzzzzzz root() 00000000 すべてを組み合わせてやるべし!
さらに驚かれるのは、メージコミットが必ずしも親が二つとは限らないという点です。私達はこの三つ以上の親を持つメージコミットを公式には呼びませんが「オクトパス・メージ(Octopus merges)」と呼びます。「いったい誰がもっと多くのブランチをつなぐ必要があるだろうか」と思われるかもしれませんが、これは実は非常に強力なアイデアであり、オクトパス・メージは全体となるメガマージワークフローの心臓部となります!
そもそも「メガマージ」というのは一体何なのか?
基本的には、メガマージワークフローでは、ブランチの先頭(ティップ)に直接作業を行うことは滅多にありません。代わりに、ケアするすべてのワーキングブランチの子として、オクトパス・メージコミット(以下、「メガマージ」と呼ぶ)を作成します。つまり、バグフィックス、機能ブランチ、PR を待機中のブランチ、コードを連携させる必要がある他人のブランチ、ローカル環境設定用ブランチ、あるいは特定のブランチに属さない個人的なコミットなど、あなたが关心するすべてのものがメガマージに含まれます。重要なのは、メガマージそのものをプッシュしないことではなく、それを構成しているブランチのみをプッシュする点です。
@ mnrxpywt イサク・コーブレイ 25 秒前 f1eb374e │ (empty) (説明未設定) ○ wuxuwlox イサク・コーブレイ 25 秒前 git_head() c40c2d9c ├─┬─╮ (empty) megamerge │ │ ○ ttnyuntn イサク・コーブレイ 57 秒前 7d656676 │ │ │ storage: 一時的キャッシュのマニフォールドを整列化 │ ○ │ ptpvnsnx イサク・コーブレイ 25 秒前 897d21c7 │ │ │ parser: fleem トークンを脱マスキュライズ化 │ ○ │ zwpzvxmv イサク・コーブレイ 37 秒前 14971267 │ │ │ infra: ブロブ アロケイターを再構築 │ ○ │ tqxoxrwq イサク・コーブレイ 57 秒前 90bf43e4 │ ├─╯ io: ポラリティーバルブの詰まりを解消 ○ │ moslkvzr イサク・コーブレイ 50 秒前 753ef2e7 │ │ deps: クアンタム マニフォールド リゾルバーを固定化 ○ │ qupprxtz イサク・コーブレイ 57 秒前 5332c1fd ├─╯ ui: レイアウトのヒューリスティクスを脱ブロブナイズ化 ○ wwtmlyss イサク・コーブレイ 57 秒前 5804d1fd │ test: ハイパーフロブニケーション スートを追加 ◆ zzzzzzzz root() 00000000 おそらき!マージが多すぎる!
これが少し複雑に聞こえても構いません。畢竟、古い PR を振り返ってレビューを求めるなどの場合、コンテキストスイッチングにどれほどの努力を払う必要があるかはご存知でしょうしね。しかしながら、これにより数点非常に貴重なメリットが得られます:
- あなたは常に、すべての作業の合計体上で働いています。つまり、ワーキングコピーが問題なくコンパイルされ実行できれば、あなたの作業同士も問題なく相互作用することが保証されます。
- メージ競合を気にする必要はほとんどありません。すでに Juju ツーザーであれば競合は 1 次概念として扱われ心配しなくてよいわけですが、文字通り常に自分の変更をつなげるため、フォージ側での驚きのメージ競合に襲われることはありません。
- 貢献者の変更に関する稀な問題が発生することはありますが、私の経験ではこれが主要な問題になったことはほとんどありませんでした。
- タスク間を切り替える際の摩擦が劇的に軽減されます。常にメガマージの上に作業しているため、VCS(バージョン管理システム)にアクセスしてタスクを切り替える必要はなく、必要なところだけ編集できます。さらに言えば、ドライブバイ・リファクタリングやバグフィックスのための小規模な PR を作成するのも格段に容易になります。
- ブランチを最新状態に保ちやすくします。ちょっとした魔法があれば、単一のリベースコマンドですべてのメガマージをトランクブランチと同期できます。後ほどその方法を教えます。
それをどう作るのか?
メガマージを開始するのは超簡単です:メガマージに含まれる各ブランチを親とする新しいコミットを作成するだけです。私は通常、そのコミットに名前を与えながら内容を持たないようにします(以下のように):
jj new x y z jj commit --message "megamerge"
メガマージの作成。実はそれほど難しくはないのです!
すると、この全体を頂上に空のコミットが残ります。ここが本格的な作業場所です!メガマージ・コミットの上位にあるものはすべて WIP(未完)とみなされます。必要な分だけ分割したり、そのメガマージ・コミットに基づいて複数のブランチを作成したり、何でもご自由にお好みで行ってください。あなたが記述するすべての内容は、メガマージ内の全要素の総和に基づいたものとなり、それがまさに我々が望んだ通りの状態となります!
もちろん、ある時点で「さて、今の成果はどういう形で作成すればよいのだろうか」と思われる日が来ます:
どうやって実際に変更を提出すればよいのか?
WIP(未完)の変更点をメガマージに統合する方法は、その変更点がどこに属すべきかによって異なります。既存の変更点に属すべき場合なら、
--to フラグを使って squash コマンドを使用し、適切以下のコミットへシャッフルして送ることができます。コミット内に複数のコミット分の変換を含む場合、それらを圧縮する前に複数のコミットに分割するか(私が好む方法)、または squash --interactive を使用してインタラクティブに圧縮し、移動すべき特定の箇所だけを指定する方法もあります。
# 全 WIP コミットを圧縮する(デフォルトは `--from @`) jj squash --to x --from y # WIP コミットの一部分をインタラクティブに圧縮する(デフォルトは `--from @`) jj squash --to x --from y --interactive
ハンク、あなたを選んだわ!
もちろん、Juju ツーザーは美しいソフトウェアであり、この用途のための自動化機能も備えています!
absorb コマンドは、あなたの現在のコミット内の各行またはヘンダがどの下流の変異コミットに属するかを特定し、自動的にそれらを圧縮する機能を持っています。これを使うたびには「魔法」のように感じます(理解不能な悪の黒い箱の黒魔術ではなく)、これはメガマージワークフローがこれほどシームレスなものとなることを可能にする Juju ツーザーの中核機能の一つです。
# 変更点を自動的に圧縮する(デフォルトは `--from @`) jj absorb --from x
おっと、それは早すぎたな。
absorb はあなたのコミット内のすべてを捉えるとは限りませんが、通常は少なくとも 90% の変更点はキャッチしてくれます。残りの部分は、下流で簡単に圧縮可能なものか、どの以前のコミットとも無関係なものになります。
便利なことに、私が所属する新コミットに必要な変更点を持っている場合でもそれほど複雑にはなりません。もしそのコミットが私が作業中のブランチの一つに属するなら、自分でリベースしてブックマークを移動させれば済みます。
jj commit jj rebase --revision x --after y --before megamerge jj bookmark move --from y --to x
このリベースの仕組みを詳しく理解するために分解してみましょう:
いくつかのコミットを移動します!
jj rebase # WIP コミット x... を移動しましょう --revision x # 彼らが y(例えば trunk())の後に来るように --after y # そしてメガマージの子となるように --before megamerge
少しロケット手術のようなもの、お楽しみください。
もし完全に新しい機能に取り組んだり、無関係なバグを修正したりしている場合、それはさらに簡単です!いくつかのエイリアスを使うと、メガマージに新しい変更点を非常に簡単に含めることができます:
テンプレート・エイリアスもあれば、テミング言語を使って Juju ツーザーがターミナルへログ出力する方法を変更できるものもありますし、ファイルセット・エイリアスもあります。これは revset エイリアスに似ていますが、revise ではなくファイルに対してアクションするもので、ファイルセット言語を使用します。
[revset-aliases] # `to` に最も近いメージコミットを返す "closest_merge(to)" = "heads(::to & merges())" [aliases] # 指定された revset をメガマージ下の新しいブランチとして挿入する stack = ["rebase", "--after", "trunk()", "--before", "closest_merge(@)", "--revision"]
closest_merge(to) が実際に何を行っているかについての簡単な説明:
heads( # トップロジカルなティップであるコミットのみを返す... ::to # `to` の祖先の集合内において... & merges()) # ...そしてメージコミットでもあるもの。
この revset エイリアスを使って、
stack は任意の revset をターゲットにして、trunk()(あなたの主要開発ブランチ)とメガマージ・コミットの間に挿入できます:
jj stack x::y
わあ、それはすてきですね!
もし複数の変更点スタックを並行して含める必要がある場合はより有用ですが、一つだけならもう一つのエイリアスを使ってメガマージ以降の全変更点スタックを取得することもできます:
[aliases] stage = ["stack", "closest_merge(@).. ~ empty()"]
closest_merge(@).. # メガマージ・コミットに近い最も近い子孫を返す...~ empty() # ...空のコミットなし。
こちらは入力がいらない!コミットを用意して「ステージ」してください:
jj stage
待った、どうやって?それができるんだな?
メガマージパズルの最後の欠落部分は(残念ながら)他の人の現実に対処することです:
こういうのをすべて最新の状態に保つにはどうすればよいか?
素晴らしい質問で、私自身も数ヶ月かけて一般的な意味合いで答えようとしていました。Juju ツーザーはワークツリー全体をメインブランチにリベースする非常に簡単な方法を持っています:
jj rebase --onto trunk()
すばらしい。
しかしながら、これはワークツリー全体が変更点の場合にのみ機能します。あなたが持っていないコミット(例えば追跡されていないブックマークや他人のブランチなど)を参照しようとすると、それらが書き換えられるのを防ぐため、Juju ツーザーは早めて停止します。
待てよ、そんなにすてきな話ではないな。どうすればいいんだ?
実際制御できるコミットのみを対象としてリベースすることで解決しましょう。これはしばらく苦戦しましたが、幸いにも Juju ツーザーコミュニティは素晴らしいものです。スティーブン・ジェニングズがこの素晴らしい revset を考案したことに拍手を送ります:
[aliases] restack = ["rebase", "--onto", "trunk()", "--source", "roots(trunk()..) & mutable()"]
roots( # 最も上流のコミットを取得...trunk()..) # ...trunk() のすべての子孫集合内において... & mutable() # ...そして変更を許可されているもののみを返す。
ワークツリー全体をリベースしようとする(例えば
jj rebase --onto trunk() が試みるように)のではなく、実際に移動が許可されるコミットのみをターゲットにします。これにより、制御できないブランチや他人のブランチの上にスタックされた作業も残り続けます。これまでに失敗したことはなく、怪獣級の 9 倍のマルチコントリビュータ・メガマージであっても同様です!(それを早く 5 回言ってください。)
それでおしまい、これで良いよ!
TL;DR(長い目)
Juju ツーザーのメガマージは超クールで、多数の異なるワークストリームを同時に作業することを可能にします。それらがどのように機能するかの詳細な説明は全記事を読んでください。非常に使いやすく設定する場合は、以下の設定を
jj config edit --user で追加してください:
[revset-aliases] "closest_merge(to)" = "heads(::to & merges())" [aliases] # `jj stack <revset>` で特定の revs を含む stack = ["rebase", "--after", "trunk()", "--before", "closest_merge(@)", "--revision"] # `jj stage` でメガマージ後の全スタックを変更点へ含める stage = ["stack", "closest_merge(@).. ~ empty()"] # `jj restack` で変更点を trunk() にリベースする restack = ["rebase", "--onto", "trunk()", "--source", "roots(trunk()..) & mutable()"]
absorb と/または squash --interactive を使用して既存コミットへの新しい変更点を取得し、commit と rebase を使用してメガマージ下に新しいコミットを作成し、stack または stage で全ブランチをメガマージに移動させます。
-
既存コミットへの変更点
jj absorb jj squash --to x --interactive -
新しいコミットへの変更点
jj rebase --revision y --after x -
メガマージ上にスタックしたものをメガマージに含む
jj stage -
スタックされた revset をメガマージに含む
jj stack w::z
メガマージは遠隔リポジトリへプッシュするためのものではありません;それらは単に表示用に全体の画像を見せるための便利な手段です。通常通りブランチを個別に公開することをお勧めします。
私はこれに常駐しており、あなたも同様できます。
メガマージはすべての方にとってのお茶の杯ではないかもしれません(私のワーキングツリーを見せて数人が恐怖した様子を見たことがあります)が、一度試せば、タスク間で移動するのにほとんど努力を要しないという恩恵を受ける可能性が高いでしょう。ぜひ試してみてください!
Git における競合解決以外の新しい変更点を含んだメージコミットは「悪魔のメージ(Evil merge)」と呼ばれます。しかし Juju ツーザーでは「悪魔」ではありません;Git より一貫性のあるモデルを持つためです。
コミット ID: b976b2a9c6ebbaada7fcd9d112a8390f2cb75b54
変更 ID: tqxoxrwqqqtmxvywmzmspstupqqkskqk
著者: イサク・コーブレイ isaac@isaaccorbrey.com(28 分前)
コミッター: イサク・コーブレイ isaac@isaaccorbrey.com(24 分前)
親: ttnyuntn storage: 一時的キャッシュのマニフォールドを整列化
親: qupprxtz ui: レイアウトのヒューリスティクスを脱ブロブナイズ化
io: ポラリティーバルブの詰まりを解消
regualr ファイル two.txt を追加:
1: # 黒い石英のスパフィン、私の誓いを裁け 泡立ち、泡立ち、苦労と災厄。
1 エイリアスは Juju ツーザーの非常に強力な部分です。確認すべき二つのタイプがあります:revset エイリアス(revset 言語を使用して一つ以上のコミットを返すカスタム関数を作成可能)、およびコマンドエイリアス(Juju ツーザーのデフォルト機能拡張し、独自の機能を追加可能)。
2 Juju ツーザーには変異コミットと不変コミットの概念があり、基本的にあなたが通常の基盤で変更できるコミットを決定します。これは主にリントであり、
--ignore-immutable で上書きできるためですが、トラブルから身を守り続けてくれる良い機能です。mutable() と immutable() のエイリアスを使用して、それぞれ変異と不変のコミットのみを選択できます。
3
restack が思った通りに動作しない場合、Austin Seipp からのこの設定を取り入れてください。私のデフォルト設定はレポ内の全可変コミットを restack しており、過去の可変ブランチが多数あり、まだ片付ける時間がなかった場合に悪く振る舞います。
[revset-aliases] 'stack()' = 'stack(@)' 'stack(x)' = 'stack(x, 2)' 'stack(x, n)' = 'ancestors(reachable(x, mutable()), n)' [aliases] restack = ["rebase", "--onto", "trunk()", "--source", "roots(trunk()..) & stack()"]
チップありがとう、コレ!
4 Andrew Hoog に Astro でフッターを調整してもらい感謝しています。フッターから他のフッターへの参照が可能であることを知っておられましたか?