──「心をgitで管理する世界」の物語で、過去を編み直す自由はどこまで許されるのか
〈ショートストーリー〉
私は昔から、過去を整えるのがうまかった。
git rebase -i。自分の履歴を開いて、並べ直す。恥ずかしい寄り道をsquashして、一つにまとめる。順番を入れ替えて、行き当たりばったりだった日々を、まるで最初から一本の筋が通っていたように見せる。
嘘ではない。どのcommitも本当にあったことだ。ただ、散らかっていたのを片づけるだけ。少しだけ自分をよく見せるだけ。
新しい恋人に会う、前の夜だった。
私は自分の枝をrebaseした。情けない夜更かしのcommitを目立たない場所へ送る。長すぎた回り道を一つの「いろいろあって」にsquashする。散らかった二十代を清潔な一本に編み直した。
少し背伸びだったと思う。でも、誰にも見せていない私だけの枝だ。pushもしていない。だから、これは許される。ただのささやかな自己演出。
恋人は、整えた私の話を聞いて、笑った。「あなた、ちゃんと考えて生きてきたんだね」。
照れくさかった。本当は、もっとぐちゃぐちゃだったのに。それでも、その夜の私の手つきは、誰も傷つけなかった。整えたのは、私だけの過去だったから。
恋人は、妻になった。
二人の履歴ができた。私だけの枝ではない。共有した二人の枝。旅行も、喧嘩も、引っ越しも、二人でcommitして、二人でpushした。同じログを二人で持っている。
結婚してすぐの頃。私は、妻を旅行に連れて行こうとして、盛大に背伸びをした。
身の丈に合わない、洒落た宿を予約した。完璧な旅程を組んだ。ところが当日、電車を乗り間違え、雨に降られ、自慢の宿の夕食は、私には立派すぎて味も分からなかった。私は一日中、「ごめん、ごめん」と妻に謝っていた。気の利いたことなんて、何一つできなかった。
なのに、妻は、その旅行がいちばん好きだと言った。
「あなたが、あんなに慌ててるの、初めて見たから」。雨の中で道に迷って、二人で傘もささずに笑った、あのcommit。私が見栄を張って、みごとに失敗した、あの一連のcommitを、妻はときどきcheckoutしては、笑っていた。
何年か経った。
私は、仕事でつまずいていた。自分が、何者でもない気がしていた。そんな夜、ふと、あの旅行のログを開いてしまった。
道に迷う私。謝ってばかりの私。立派な夕食に気後れしている私。── 急に、それが惨めに思えた。こんなにかっこ悪い男だったのか、私は。
いつもの手つきが、出た。
git rebase -i。共有した、二人の履歴を。
乗り間違えた電車をdropした。雨のcommitで手を止めて、中身を晴れに書き換えた。謝ってばかりの私をsquashして「スマートにエスコートする私」に編み直した。私だけの枝に、何度もやったのと同じ手つきで。きれいな、見栄えのいい思い出に。
そして、git push --force。
二人の共有リモートに、私の版を上書きした。
妻が、fetchした。
しばらく、画面を見ていた。それから、私を見た。怒ってはいなかった。ただ、寂しそうだった。
! [rejected] 私たちの旅行 (non-fast-forward)
「……雨、消しちゃったんだ」
妻は、それだけ言った。
私が「かっこ悪い」と思って消した、その一つひとつを妻は覚えていた。雨も、乗り間違えた電車も、謝ってばかりの私も。私が恥じたその全部を。
「私、こっちのほうが、好きだったのにな」
妻は、自分の手元の枝を、私に見せた。彼女のリポジトリには、まだ、元の旅行がぜんぶ残っていた。私がforce-pushしても、それは消えなかった。妻は、私の上書きを受け取らなかった。「こっちのまま、置いとくね」と。
私は、何も言えなかった。
私が、見栄を張って整えた、きれいな旅行。それを「好きだ」と言った人は、誰もいなかった。妻が大切にしていたのは、私が消したかった、あのかっこ悪い私のほうだった。
恋人の前で背伸びしたときは、誰も傷つかなかった。私だけの枝だったから。
でも、これは、二人の枝だった。整えていいのは、私だけの過去だけ、だったのに。
幸い、と言っていいのか分からない。私がいくら上書きしても、妻の手元には、本当の旅行がまだ残っていた。書き換えたcommitのauthorの欄にも、あの日、確かに慌てていた私の名前が残っていた。gitは、私の見栄に、最後まで付き合ってはくれなかった。
そのことに、私は少しだけ救われた。
その週末、私たちはもう一度、あの宿に泊まりに行った。今度は、わざと安い、近場の宿に。また、道に迷えばいい、と思った。
〈技術ノート〉── rebaseの功罪
ノートは答え合わせではない。物語が嘘をついた、ちょうどその場所の地図である。
ただし本章では、嘘は一点にない。嘘と本当の境界線そのものが、主題だ。
同じ操作が、可にも不可にもなる
git rebaseは、commitを付け替えて履歴を編み直す操作だ。interactive(-i)なら、並べ替え(reorder)・統合(squash)・削除(drop)・メッセージの書き換え(reword)、そして中身そのものの書き換え(edit+git commit --amend) まで、思いのままにできる。結果、commitのハッシュは、すべて新しくなる。
ここを、取り違えやすい。rewordは、commitのメッセージしか変えない。物語で「雨を晴れに」変えたように、記録された出来事そのものを書き換えるのはedit ── そのcommitでrebaseを止め、ファイル(=中身)を書き換えてamendする。rewordが見出しの差し替えなら、editは本文の捏造だ。rebaseは、そこまでできる。 だから、共有後に使えば、凶器になる。
この道具は、履歴を最も美しく整える最良の手段であり、使う場所を間違えれば、最悪の凶器になる。同じ手つきが、だ。境界は、たった一つ。
そのcommitを、まだ自分しか持っていないか。
- 第一場(可) ── まだpushしていない、自分だけの枝。rebaseで過去を整えても、誰も困らない。誰も、その履歴に依存していないからだ。ただの自己編集。健全な、ささやかな背伸び。
-
第二場(不可) ── すでにpushし、相手も持っている共有の枝。これをrebaseで書き換え、
git push --forceで上書きすると、相手がfetchしたとき、non-fast-forward(自分の知っている過去の続きでは、もうない)として現れる。
主人公の罪は、誰かを陥れたことではない。見栄から、二人で持っていた思い出を、自分の見栄えのいい版に差し替えようとしたこと。動機が背伸びでも、共有履歴を書き換えれば、結果は同じ ── これがgitのゴールデンルール(push済み・他者が依存する履歴はrebase / force-pushするな)であり、そのまま、関係の倫理でもある。
gitは、完全な改竄を許さない(だからこそ、救いになる)
ここが、本章の肝だ。force-pushしても、元は、消えない。理由は三つ。
-
相手のrepoは、こちらのforce-pushでは消せない。 force-pushが上書きするのは、共有リモートのrefだけ。相手がfetchすると、自分の手元の枝とリモートが分岐した(diverged) と分かる。相手は、あなたの版に
resetして乗り換えることも、乗り換えずに自分の版を持ち続けることもできる。妻が「こっちのまま置いとく」と言えたのは、これだ。本当の旅行は、妻の手元に、まるごと残る。 -
authorは、保持される。 rebaseで書き換わるのは、message・順番・committer・ハッシュ・日付。だがauthor(書いた人)だけは、
--reset-authorでもしない限り、残る。「あの日、慌てていた私」の名前は、書き換えたcommitにも残ってしまう。 -
reflogが覚えている。 あなたの手元の
reflogには、rebase前のcommitが、しばらく残る。
過去を整えることは、gitは許す。だが過去をなかったことには、させてくれない。物語ではそれが、消したかった「かっこ悪い私」=妻が愛していた私を、結果的に守った。
正しい名前
| 物語の言葉 | gitの正名 |
|---|---|
| 過去を整える・並べ直す | rebase -i=commitを付け替えて履歴を編み直す(reorder/squash/drop/reword/edit)。ハッシュは新しくなる |
| 雨を晴れに書き換える | edit+commit --amend=そのcommitで止め、中身(記録された出来事)を書き換える。reword(メッセージのみ)より深い改変 |
| 私だけの枝・pushしていない | 未共有履歴=書き換えてよい(ゴールデンルールの「可」) |
| 二人でpushした共有の枝 | 共有履歴=書き換えてはいけない(「不可」) |
| 上書きした |
git push --force=リモートのrefを自分の版で上書き。相手のfetchはnon-fast-forwardを検出(※ --force-with-leaseなら、相手が先にpushしていた場合は拒否され、より安全) |
| 妻の手元に元が残っていた | 相手のローカルは上書きされない=force-pushが変えるのは共有リモートだけ。各自の手元の版は、本人が乗り換えない限り残る |
| authorの欄に名前が残る | rebaseはauthorを保持=書き換わるのはcommitter・ハッシュ・日付だけ。由来は残る |
要点
- rebaseは最良の整形具にして、共有後は最悪の凶器。 境界は一つ ── そのcommitを、まだ自分しか持っていないか。動機が悪意でも背伸びでも、共有履歴を書き換えれば結果は同じ。
- force-pushが上書きするのは共有リモートだけ。 相手の手元の版は消せない。相手には、乗り換えない自由がある。
- 完全な改竄はできない。 authorは保持され、reflogと相手のrepoは元を覚えている。gitが過去を守りきるから、消したかった「本当」が、生き残る。