──「心をgitで管理する世界」の物語で、昔話しかできなくなる理由をgitで説明する
〈ショートストーリー〉
二十年ぶりに会った田中は、まず高校の話をした。文化祭、屋上、誰それの噂。
私は合わせながら、少し身構えた。——この男、まだ十八歳のつもりなのか。
なら、付き合ってやる。手首をさすって、唱える。
git checkout 高3
当時の口ぐせが前へ出てくる。気分が、あの頃に戻っていく。
どっちが先に担任のあだ名を言い出したか。くだらない言い争いが、本気で二十分続いた。結論は出ない。出す気もない。頼んだのはコーヒーだけ。終電を気にする頭が、どこかへ行っていた。
そこからの二時間は、よかった。本当に、よかった。
途中、田中が財布を落とした。隙間から子どもの写真がのぞく。
「上が今年、受験でさ」彼は笑った。「お前んとこは?」
私も、いまの話をした。仕事のこと、子どものこと。田中は、へえ、と笑って、すぐに屋上の話に戻した。私のほうも、彼の受験生の話を、本当には受け取れていなかった。
二人そろってcheckoutできる場所は、もう高3しか残っていなかった。マージベース。
別れぎわ、田中が言った。「お前、変わらないなあ」
私も同じことを思っていた。変わっていないんじゃない。互いの変化を一度も取り込まないまま来た、というだけだ。
別れて、駅のホームでgit checkout いまの私。
戻った私は、今夜の楽しさを持ち帰ろうとして、高3をmergeした。
CONFLICT (content): Merge conflict in 私
<<<<<<< HEAD(いまの私)
明日は七時に起きる。遅刻はしない。
「今度飲もう」の今度は、来ない。
=======
朝まででも付き合う。明日のことは、明日でいい。
会いたいやつには、会いに行く。
>>>>>>> 高3
両方は、残せない。朝までバカ話をする私と、会社に遅れず行く私が、同じ一行を奪い合う。高校のときは、こんなことは起きなかった。あの頃は、失うものがまだ何もなかったからだ。
一行目は、いまの私を採用した。二行目は、高3を採用した。
コミットした。いまの私が少し重くなり、消したぶんだけ軽くなった。
高3のタグは、消さずに残してある。読み取り専用のまま、いつでも会いに行ける。
ただ、そこに新しい一日を書き足すことは、もう、二人ともできない。
〈技術ノート〉── tag / merge base
ノートは答え合わせではない。物語が嘘をついた、ちょうどその場所の地図である。
物語が嘘をついた場所
帰り道、私は「今夜の楽しさを持ち帰ろうとして、高3をmergeした」。
ここが嘘だ。
一日ぶんの履歴ごと取り込みたいならmergeでいい。だが、欲しかったのは一行だけだった。「朝まででも付き合う」という、あの一行。一行だけを別の枝から今の自分へ移す操作は、mergeではない。git cherry-pickだ。
- merge ── 二つの履歴を合流させ、相手のcommit列をまるごと自分の系譜に取り込む。だからmerge base(共通祖先)が要る。差分の基準がそこにしか無いからだ。
- cherry-pick ── 特定のcommitの差分だけを、今いる枝に新しいcommitとして載せる。履歴は継がない。共通祖先も要らない。
物語がmergeと言った瞬間、本当は私はcherry-pickしていた。一行を選び、残りは置いてきた。それは合流ではなく、移植だった。
正しい名前
物語の言葉を、gitの正名に接続する。
| 物語の言葉 | gitの正名 | 定義 |
|---|---|---|
| 二人そろってcheckoutできる、もう高3しか残っていない | merge base | 二つの分岐の共通祖先。git merge-base 高3 いまの私で求まる。分岐後に各自が積んだcommitは、ここには含まれない |
| 二人でpushし合っていた頃 | 共有リポジトリ(remote) | 両者がclone・push・pullしていた関係のrepo。pull済みの共通履歴は各自のlocalにも残る(分散)。卒業後pushが止まりstaleに |
| 朝までゲームvs遅刻しない、同じ一行の奪い合い | merge conflict | 両側が同じ箇所を別々に変更したとき、gitは自動で選べず、人間に解決(resolution)を委ねる |
| 一行目は私、二行目は高3を採用した | conflict resolution | どちらを残すか一行ずつ選び、選び終えてからcommitして確定する |
| 高3のタグは、消さずに残してある。読み取り専用 | tag | 特定のcommitを指す、動かない名前。ブランチと違い、新しいcommitで先へ進まない |
共有リポジトリと、その後 ── なぜ一人でもcheckout 高3できるのか
高3は、もともと二人でpushし合っていた共有リポジトリだった。同じ部室、同じ毎日を、二人でそこにcommitしていた。
gitは分散している。共有リポジトリからpullしたものは、そのまま自分のlocalにも丸ごと残る。だから卒業後、その共有リポジトリに誰もpushしなくなって(stale)も、私のlocalには、高3までの共通の履歴がぜんぶ残っている。一人でcheckout 高3できるのは、これだ ── 手元に、そのcommitがあるから。田中も、同じ(彼のlocalにも、共通分が残っている)。
そして卒業後、二人の履歴はdivergeした。私は私のlocalに、就職や子どものcommitを積む。田中は田中のlocalに、彼の二十年を積む。共有していたのは、高3まで。その先は、各自の人生(local)に、別々に伸びている。
だから、再会の会話は二層になる。
- 高3の話=共有履歴。二人とも持っているから、すらすら噛み合う。
- 受験や仕事の話=各自がlocalにdivergeさせた、別の履歴。互いにfetch / pullしていないから、聞いても、本当には噛み合わない(「受け取れていなかった」)。
高3のタグが指すのは、その、二つのdivergeした人生の、最後の共通コミット ── merge baseだ。共有リポジトリはstaleになり、新しい共通の一日は、もうpushされない。けれど、それぞれのlocalには、高3がいつまでも残る。会いには、行ける。書き足せない、だけだ。
要点
- 一行だけ持ち帰るのはcherry-pick。履歴ごとならmerge。 どちらを使ったかで、「何を受け継いだか」が変わる。物語はmergeと呼んだが、私がしたのはcherry-pickだった。
- merge baseは固定されない、はずだった。 普通は、二人が会い続ければ共通祖先は前へ進む。高3で止まったのは、卒業後どちらも相手の枝を一度も取り込まなかったから。merge baseが高3のままなのは、二人の怠慢の化石だ。
-
tagは読み取り専用。 だから、いつでも
checkout 高3で会いに行ける。だが、そこに新しい一日をcommitすることは、二人とももうできない。会えることと、続けられることは、別だ。 - 高3は、共有リポジトリの最後の共通コミット(merge base)。 gitは分散しているので、pull済みの共通履歴は各自のlocalに残る。共有repoがstaleでも一人でcheckoutできるのは、そのため。卒業後の人生(受験・仕事)は、各自のlocalにdivergeして積まれている ── だから持ち出せるし、だから噛み合わない。