──「心をgitで管理する世界」の物語で、否認した過去をgitだけが覚えている
〈ショートストーリー〉
言ってしまってから、空気が止まった。
喧嘩の最中だった。たいした喧嘩じゃなかったはずだ。ただ、その日の私は、仕事がうまくいかなくて、妙にいらついていた。自分が小さく思えて。それを認めたくなくて。
強がって、見栄を張って ── 言わなくていい一言まで、口が滑った。
何を言ったかは書かない。ただ言った瞬間に、しまった、と思う種類の一言だった。妻の顔から、すっと色が引いた。私は自分でも驚いていた。そんなことを言うつもりは、なかったのに。
すぐに謝りたかった。いつもの私なら、もう謝っていた。
でも、その夜はできなかった。さっき強がった手前、今さら「ごめん」と折れるのが、かっこ悪い気がして。
妻は、何も言い返さず、寝室のドアを静かに閉めた。
私は、リビングに一人で残された。
手首の端末に、さっきのcommitが点いている。消えない。たった今、私が積んだばかりの、その一行。
二つ、道があった。
一つは、revert。
言ったことは、消さない。その上に、新しいcommitを積む。「あれは、間違いだった。ごめん」。私が言ったことも、それを取り消したことも、両方、ログに残る。明日、妻がログを見れば、私の一言は、まだそこにある。打ち消しは、傷を消さない。傷の隣に、謝罪をもう一つ、並べるだけだ。
それは、かっこ悪い。みじめに頭を下げることだ。
もう一つは、reset --hard。
あの一言の手前まで、HEADを戻す。さっきのcommitごと、なかったことにする。「そんなこと、言ってない」。きれいに消える。強がった私を、下ろさなくていい。
楽だった。あまりに、楽だった。
<<<<<<< 謝りたい私
ごめん。あれは、言いすぎた。
=======
何も、言ってない。
>>>>>>> 見栄を張ったままの私
同じ一行を、二人の私が奪い合う。
私は ── 弱かった。reset --hardを押した。
commitが消えた。ログが何事もなかった顔をして、さっきより一行短くなる。胸がすっと軽くなった。これでいい。なかったことに、なった ──
ならなかった。
ふと、git reflogを開いてしまった。
そこには、消したはずのあの一言。reflogは、私が捨てたものをまだ覚えていた。「なかったこと」になど、本当はなっていなかった。私は、ログの表から引っ込めただけだ。強がりごと、隠しただけ。
私は、長いこと、そのreflogを見ていた。
消しても、消えない。だったら ── と思った。だったら、ちゃんと残したまま、謝るしかない。なかったことにできないなら、せめて、その隣にごめんを並べるしか。
reflogから、消したはずのcommitを拾い直した。私が確かに言った、その一行を戻す。
そして、その上にrevertを積んだ。「ごめん。言いすぎた」。みじめでも、かっこ悪くても、それが、私にできるただ一つの正直だった。
翌朝、妻は、いつもどおり台所に立っていた。コーヒーを二つ、淹れていた。
私のぶんも、あった。
ゆうべ、妻が、私の一言をどう扱ったのか。revertしたのか、reset --hardしたのか。私には分からない。妻のrepoの中は、見えない。
ただ、コーヒーは、二つ、湯気を立てていた。
「……ゆうべは、ごめん」
今度は、ちゃんと言えた。強がりを、下ろして。
妻は、「うん」とも「いいよ」とも言わなかった。ただ、私のぶんのコーヒーを、私のほうへ、すっと押した。
それが、許しなのか、ただの惰性なのか、私には分からなかった。
でも、コーヒーは、温かかった。
〈技術ノート〉── revert / reset --hard
ノートは答え合わせではない。物語が嘘をついた、ちょうどその場所の地図である。
物語が嘘をついた場所
「reset --hardで、なかったことにできる」── ここが、嘘だ。
確かに、reset --hardは、ログの表からcommitを消す。だが、消えてはいない。
二つの操作を、正確に並べる。
- revert ── 対象commitの逆diffを、新しいcommitとして積む。原因は履歴に残ったまま。「やった」事実と「取り消した」事実が、両方ログに並ぶ。履歴を書き換えないので、共有済み・push済みでも安全に使える。
-
reset --hard ── ブランチとHEADを、指定した手前のcommitへ戻し、作業ツリーごと上書きする(
--soft/--mixedと違い、--hardは手元の変更も捨てる)。それ以降のcommitは、ログから外れ、到達不能(unreachable) になる。
ここが肝心 ── 到達不能になったcommitは、すぐには削除されない。 オブジェクトは、まだデータベースに残っている。そしてgit reflogが、HEADやブランチがどこにいたかを記録しているので、そこから拾い戻せる(git reflog → git reset --hard HEAD@{1}やcherry-pick)。
主人公が見たのはこれだ。捨てたはずの一言が、reflogにまだ座っていた。
消えない、ということ ── 非情さと、救い
reflogのエントリには、寿命がある。既定で、到達可能なものは約90日、到達不能なものは約30日でexpireし、その後git gcがpruneして、ようやく本当に消える。
この「猶予」が、両刃だ。
-
救い ── 取り乱して
reset --hardした直後に後悔しても、reflogがあれば、取り戻せる。否認を、撤回できる。 物語の主人公は、これに助けられた。一度は否認(reset)に逃げたが、reflogが消させてくれなかったことで、彼は正直(revert)に引き戻された。消せない、という事実が、彼を謝罪へ押し戻した。 - 非情さ ── 本気で消したくても、しばらくは消えない。「なかったこと」は、待たないと完成しない。そして ── ここをgitは決して直してくれない ── 相手のrepoにある分は、こちらのresetでは消せない。 妻が聞いた一言は、妻の履歴にある。私のreflogをいくら掃除しても、そこには届かない。
「否認」は、技術的にも、完成しないのだ。
正しい名前
| 物語の言葉 | gitの正名 |
|---|---|
| 打ち消しを積む(謝罪) | revert=逆diffを新しいcommitとして積む。原因は残る。共有履歴でも安全 |
| なかったことにする(否認) | reset --hard <手前> =ブランチとHEADを戻し、作業ツリーごと上書き。以後のcommitは到達不能に |
| 消したはずが、まだある/拾い直す | reflog=HEAD/ブランチの移動先の記録。到達不能commitも、ここから拾い戻せる |
| しばらくは消えない |
expireとgc=reflogは約90日/到達不能は約30日でexpire、その後git gcでprune。完全消去は、そこまで待つ |
要点
- revertは足し算、reset --hardは引き算。 revertは打ち消しを積む(履歴は残る・共有安全)。reset --hardは履歴ごと戻す(作業ツリーを破壊・共有済みならforce-pushが要る=7章の領域)。
- revertは傷を消さない。 原因commitはログに残り続ける。消すのではなく、隣に取り消しを並べる謝罪だ。
- reset --hardも「なかったこと」にはならない。 reflogが拾える間は残り、相手のrepoは、そもそも消せない。否認は完成しない ── そして、その「消せなさ」が、人を正直さへ戻すこともある。