──「心をgitで管理する世界」の物語で、いつか統合するつもりの自分は、もうmergeできない
〈ショートストーリー〉
私には、ずっとmergeしていないbranchがひとつあった。
いつか という名前を、自分で付けた。二十六のとき、いま から切った。
あの年、私は何かをはじめようとしていた。最初の数週間は、毎晩commitしていた。
* 9f2c1ab はじめる
* 4d8e0c3 続けている
* a1b77f2 まだ、続けている
そのあと、logは途切れている。
重要だった。それは確かだ。でも、急ぎではなかった。明日でもよかった。だから、明日にした。それを、何年も。
いま のほうは忙しかった。仕事を覚え、引っ越し、誰かと暮らしはじめ、また引っ越した。毎日、急ぎの何かをcommitした。打ち合わせ、見積もり、保育園の送り、親の通院。どれも今日のうちに片づけないと、明日に響いた。いま はどんどん先へ進んだ。
いつか は切ったところで止まったままだった。
でも、消しはしなかった。消せなかった。疲れた夜、私は git checkout いつか と唱えることがあった。すると、二十六のあの数週間に戻れた。まだ何も諦めていない自分の、視界の色。あのbranchの中では、私はまだ続けていた。
数分だけそこにいて、git checkout いま で朝に帰った。
いつか はずっときれいなままだった。一度も いま とぶつかっていないから。
いま が、ようやく静かになった。
子どもは大きくなり、仕事も落ち着いた。急ぎのcommitが、減った。
静かになって、はじめて、いつか を思い出した。
重要度は高い。でも、緊急じゃない。——何年も、私はそう仕分けて、後回しにしてきた。今度こそ、と思った。急ぎのものは、もう、ない。今なら、できる。
やっと統合する。いつか を いま にmergeして、私は、ずっとなりたかった人間になる。
git merge いつか
画面が赤で埋まった。
CONFLICT (content): Merge conflict in 平日の夜.txt
CONFLICT (content): Merge conflict in 休日.txt
CONFLICT (content): Merge conflict in 使えるお金.txt
CONFLICT (content): Merge conflict in 体力.txt
...
Automatic merge failed; fix conflicts and commit the result.
一つも自動ではmergeできなかった。
開いて、分かった。いつか が変えたかった場所を、いま がぜんぶ、別のもので埋めていた。平日の夜は、いつか では「続ける時間」だった。いま では、とっくに別の予定で上書きされていた。
<<<<<<< いま
子どもの寝かしつけ
=======
続ける
>>>>>>> いつか
======= の線が、壁みたいに見えた。
上が、私が毎日commitしてきた十四年。下が、私がなるはずだった人。どちらかしか選べなかった。両方は残せなかった。一行ずつ選ばされて、ほとんどの行で、私は いま を選んだ。選ぶしかなかった。
──
最後のconflictで、手が止まった。
ここで全部 いま を選べば、mergeは完了する。履歴には「いつか をmergeした」と残る。二つの親を持つ、立派なmerge commitができる。
でも中身には、いつか のものが一行も入っていない。
形だけ、統合した。あのbranchを迎え入れた、という記録だけが残る。中身は何も変わらない。明日も私は、同じ いま を生きる。
私は git merge --abort と打った。
画面は元に戻った。mergeはなかったことになった。いつか はまた、切ったところで止まったまま、きれいに残った。一度も いま とぶつからないまま。
たぶん、答えは最初から分かっていた。
二十六で切ったあと、もし毎週、少しずつmergeしていたら。小さなconflictを、その都度かたづけていたら。いつか と いま は離れずに、いっしょに育ったかもしれない。大変なことには、ならなかった。
でも、そうしていたら、いつか はとっくになくなっていた。いま と毎週ぶつかって、少しずつ削られて。「続ける」は「やっぱり無理だった」に上書きされて。
mergeしなかったから、いつか は最後まできれいだった。なりたかった人は、一度も失敗せずにすんだ。
それが、ずるいことなのか、やさしいことなのか、私には分からなかった。
いつか は、まだある。今夜もたぶん、checkoutする。数分だけ二十六に戻って、いま に帰る。
mergeは、しない。
〈技術ノート〉── 分岐は、保存ではない
ノートは答え合わせではない。物語が嘘をついた、ちょうどその場所の地図である。
物語には、三つの嘘がある。
branchは何も保存しない。 branchとは、ある commitを指す動かせるポインタ(ref)にすぎない。いつか が二十六のまま「止まっていた」のは、保存されたからではなく、誰もポインタを進めなかったからだ。不変なのはcommitのほうで、branchはただ、そこに置き去りにされていた。
merge conflictは、あなたが変えたから起きるのではない。 あなたが変え、かつ いま も同じ場所を別のやり方で変えたから起きる。conflictは片側の罪ではなく、乖離(divergence)の関数だ。いつか が止まっているあいだに いま が十四年ぶん動いた——その距離が、conflict の量になる。乖離は、片方が止まっていても、もう片方が進めば広がる。
そして、いちばん静かな嘘。いま は、正典ではない。
主人公は いつか を いま にmergeし、ぶつかるたびに いま を選んだ。まるで いま が本物で、いつか が逸脱であるかのように。だがgitは、そんなことを一言も言っていない。gitは、どの branchにも特権を与えない。 いま も いつか も、対等な一本の枝だ。いま を「正式な自分」とする根拠は、gitの側にはない。毎朝そこを checkoutしてきた、というだけの理由で、人は立っている枝を正典だと思い込む。誰に強制されたわけでもなく。
最後に、社会人の語彙で言い直す。重要度と、緊急度だ。
時間管理の古い図式に、重要度×緊急度の四象限がある。いつか は、その第II領域にいた。重要だが、急がない。人生でいちばん大事なものが集まるのに、いちばん後回しにされる場所だ。
いま の毎日のcommitは、ほとんどが第III領域だった。緊急だが、重要ではない。打ち合わせも送り迎えも、急ぎだから毎日勝つ。重要だから勝つのではない。
いつか が負け続けたのは、中身で負けたからではない。一度も、急がなかったからだ。締め切りのないものは、いつでもできる。いつでもできるものは、いつまでもされない。
ここが、いちばん静かな残酷さだ。急がないものにも、締め切りはあった。 ただ、それは赤くならない。通知も来ない。CIも回らない。だから、過ぎるまで見えない。第II領域のbranchにとって、締め切りとは、その人の人生そのものだ。いま が静かになって、ようやく いつか を思い出したとき——思い出せたという事実が、もう手遅れだという証拠だった。
ここまで踏まえれば、教訓ははっきりしている。
乖離が大きいほど、統合は高くつく。しかも線形ではない。 長く生きたbranchほど、相手側の変更も積み上がり、ぶつかる面積が膨らむ。これが俗に言う「big bang merge」「統合地獄」だ。一週間ぶんなら数個だったconflictが、十四年ぶんでは全ファイルに及ぶ。
だから、branchは細かく切り、こまめに統合する。 短命なbranch(short-lived branch)を小さく切って、早く本流へ戻す。すぐ戻せないなら、その間こまめに相手を取り込み直す(git merge / git rebase)。乖離が小さく保たれれば、conflict はそのつど一行ずつ、安くかたづく。trunk-based developmentやCIが「merge early, merge often」と繰り返すのは、これが理由のすべてだ。
ただし git には、もう一つ、物語が触れた機能がある。
git merge -s ours ── 相手の branch を「mergeした」履歴を残しながら、中身は一行も取り込まない。二つの親を持つmerge commitができるのに、結果のツリーはこちらと完全に同じになる。形だけの統合が、gitの正式な機能として用意されている。主人公が「全部 いま を選ぶ」で作りかけたのは、これだ。
そして、ここがgitの限界だ。gitは乖離が高くつくことは正確に教える。だが、そのbranchをそもそもmergeすべきだったのかは教えない。こまめに統合すればconflictは小さくなる——本当だ。でも毎週ぶつけていたら、そのbranchは小さな削れのたびに痩せて、とうに消えていたかもしれない。統合しなかったから最後まで無傷だったbranchもある。gitはそれを、臆病とも優しさとも呼ばない。ただ、置かれたままのrefとして記録する。