Git — 変更履歴を記録して、いつでも戻れる

概要 — まず全体をつかむ

中級ではコミット・ブランチ・マージ・リモートを見ました。上級では、その正体であるオブジェクトモデル・参照・ステージングの実体・マージとリベースの内部という「動く仕組み」を掘り下げます。

詳細 — 1段階ずつ追う

これは何をする係?

Gitの中身は、実は4種類のオブジェクトを中身のハッシュ値で結んだ小さなデータベースです。ここが分かると、あらゆる操作の理由が一気に見えてきます。

  • blob(※1)— ファイルの中身そのもの。ファイル名は持たない
  • tree(※2)— ディレクトリに相当。どのファイル名がどのblobかを並べた一覧
  • commit(※3)— ある瞬間のtree(=全体像)と、親コミット・作者・メッセージを指す
  • tag — 特定のコミットに付ける不変の名札(リリース地点など)

肝はコンテンツアドレッシング——それぞれのオブジェクトを、中身から計算したハッシュ値(SHA)そのものを名前にして保存することです。中身が1バイトでも違えば名前が変わり、同じ中身なら世界中で1つに共有されます。

アニメーション『Gitの履歴グラフ(分岐とマージ)』を開く
コミットの連なり=履歴枝分かれ(ブランチ)して並行作業し、あとで合流(マージ)する初期変更A分岐元変更Bマージ試作1試作2mainfeatureHEAD● 1つの円=その時点の「スナップショット(丸ごとの記録)」。→ 矢印は「1つ前の親コミット」を指す。連なりをたどれば、いつでも過去の状態に戻れる。

だから「コミットはスナップショット」でも容量は膨れません。変わっていないファイルは同じblobを使い回すからです。そして各コミットは親コミットのハッシュを含むので、履歴の1点を書き換えると以降のハッシュがすべて連鎖的に変わり、改ざんは即座にばれます。

登場人物メモ:

  • ※1 blob — ファイルの中身を保存したオブジェクト。名前は中身のハッシュ
  • ※2 tree — ファイル名とblobの対応表。ディレクトリ構造を表す
  • ※3 commit — treeと親コミットを指す、履歴の1節目
やさしく言うと(中級

1回の記録=コミットは、その時点のスナップショット(※1 作業場所を丸ごと写した1枚)です。差分だけでなく「その瞬間の全体像」を指し示し、各コミットは1つ前の親コミットを覚えています。

だから履歴はスナップショットの連なりになります。連なりをたどれば必ず過去の完全な状態に戻れる——これが「安全」の正体です。1点でも独立して残っているので、途中が壊れても後ろの記録は無傷です。

登場人物メモ:

  • ※1 スナップショット — その時点のファイル全体を写した記録の1枚
  • ※2 HEAD — 「いま自分が見ている/作業している位置」を指す目印
  • ※3 リモート — GitHub などの共有先。手元の履歴を送受信する相手

参照とHEAD

コミットはハッシュという長い名前を持ちますが、人間はそれを覚えません。そこで参照(ref)という「別名」を使います。

  • ブランチ — 「あるコミットを指すだけの可動ポインタ」。main は40桁のハッシュを書いた1行のファイルにすぎない。コミットするたび、この名札が新しいコミットへすっと動く
  • HEAD — 「いま自分がいるブランチ」を指す参照。ふだんはブランチを間接的に指す
  • detached HEAD — ブランチ名でなくコミットを直接チェックアウトした状態。ここで作ったコミットはどのブランチにも属さず、移動すると迷子になりやすい
  • リモート追跡ブランチorigin/main など)— 「最後に見たリモートの位置」を覚えておく参照。手元の main とは別物で、pull はこの差を埋める作業

ブランチ作成が一瞬なのは、ファイルを1つ書くだけだから。「枝を切る=名札を1枚増やす」という軽さが、気軽な実験を支えています。

ステージングの実体

「編集→ステージ→コミット」のステージ(index)は、単なる待合室ではありません。

  • indexは次のコミットになるtreeの下書きを保持する台帳です
  • git add は、作業ツリーの現在の中身をblob化してindexに登録する操作
  • git commit は、そのindexを固めてtreeとcommitに確定する操作

だから「addした後にさらに編集」すると、コミットされるのはaddした時点の中身になります。作業ツリー・index・最新コミットという3つの状態を区別すると、git status の表示(stagedとnot staged)が腑に落ちます。

マージ vs リベースの内部

枝を統合する方法は2つあり、履歴の形が根本的に違います

  • fast-forward — 分岐後に本流が1歩も進んでいなければ、マージは名札を前へ動かすだけ。新しいコミットは作られない
  • 3-wayマージ — 両方が進んでいるときは、共通の祖先と両先端の3点を比べて自動統合し、親を2つ持つマージコミットを作る。同じ箇所を別々に変えていればコンフリクトとして人に委ねる
  • リベース — 自分のコミット群を、別の土台の上へ1つずつ作り直す。履歴は一直線になり読みやすいが、作り直された各コミットは別ハッシュの別物になる

ここに鉄則があります。共有済みの履歴はリベースしない。他人が既に持つコミットを別物に置き換えると、相手の履歴と食い違い、pushで衝突や履歴の重複を招くからです。

アニメーション『マージ vs リベース(履歴の形)』を開く
マージ vs リベース同じ「枝の統合」でも、あとに残る履歴の形が根本的に違うマージ — 合流させる2つの枝を合流させる新しいマージコミットを作る(履歴は分岐したまま残る)AB(分岐元)CXYM(マージコミット)親を2つ持つmainfeatureリベース — 付け替える自分のコミットを相手の先端の上へ作り直す(履歴は一直線・ハッシュは変わる)XY先端の上へ作り直すABCX'(別ハッシュ)Y'(別ハッシュ)featureマージ=分岐を残して合流/リベース=一直線に整えるが各コミットは別物になる(共有済み履歴はリベースしない)
やさしく言うと(中級
  • ブランチ — 履歴を枝分かれさせる。main(本流)を壊さず、feature などの枝で新機能を試せる
  • マージ — 枝を本流に統合する。枝の変更を取り込んで1つに合流させる

下の図は、mainの連なりから途中で feature が分かれ、最後にmainへマージ(合流)する流れです。

アニメーション『Gitの履歴グラフ(分岐とマージ)』を開く
コミットの連なり=履歴枝分かれ(ブランチ)して並行作業し、あとで合流(マージ)する初期変更A分岐元変更Bマージ試作1試作2mainfeatureHEAD● 1つの円=その時点の「スナップショット(丸ごとの記録)」。→ 矢印は「1つ前の親コミット」を指す。連なりをたどれば、いつでも過去の状態に戻れる。

枝を切るのが軽いのは、コミットが独立したスナップショットで、ブランチはその1点を指す名札にすぎないから。だから「試してダメなら枝ごと捨てる」が気軽にできます。

reflog・packfile・分散モデル

  • reflog — HEADやブランチが過去に指していた位置の記録。リベースやリセットで「コミットを見失った」ときも、reflogをたどれば元のハッシュが見つかり復旧できる。オブジェクト自体は消さずに残っているのが効く
  • packfile — 大量の緩いオブジェクトを、差分(デルタ)でまとめて圧縮した1ファイル。似たオブジェクトの共通部分を省き、転送も保管も小さくする。だから巨大な履歴でもクローンが現実的な速さで済む
  • 分散モデル — 各クローンが全履歴を丸ごと持つ。中央サーバは「みんなが待ち合わせる場所」にすぎず、落ちても手元で作業・コミットは続けられる。pushとfetchは、この独立した履歴どうしを参照の差分だけ同期する操作

土台のしくみはプログラムが動くまで、履歴を自動化に載せる話はCI/CDへ続きます。

やさしく言うと(中級
  • リモート — GitHub 等の共有先。push で送り、pull で受け取る。手元にも全履歴があるので、共有先が落ちても作業は続く
  • プルリクエスト — 「この枝をmainに入れたい」というレビュー依頼の形(サービス側の機能)
  • コンフリクト — 同じ箇所を別々に変えた枝を統合すると衝突する。Gitはどちらを採るか決められないので、人が見て解消する

このコミット履歴の安定性が、変更のたびに自動でテスト・デプロイする自動化CI/CD など)の土台になります。「いつでも戻せる記録」があるからこそ、機械に任せて素早く回せます。

⚠️ 深いところの落とし穴

  • 共有済み履歴のリベース — 他人が持つコミットを書き換え、履歴が二重化・衝突する
  • detached HEADでの作業放置 — ブランチに紐づけず移動すると、コミットが参照から外れて見失う(reflogで拾える)
  • 巨大バイナリのコミット — blobは差分圧縮が効きにくく、履歴が肥大化して二度と軽くならない
  • 秘密情報のコミット — 一度コミットするとオブジェクトとして履歴に残り続け、後から消しても過去のハッシュには残る
やさしく言うと(中級
  • コンフリクト解消のミス — 衝突を雑に消すと、必要な変更まで捨ててしまう
  • 巨大なコミット — まとめすぎると、どの変更が原因か切り分けられない
  • push し忘れ/pull し忘れ — 手元と共有先がずれ、あとで衝突が増える
  • 秘密情報のコミット — パスワード等を記録すると履歴に残り続ける(消しても過去に残る)

土台のしくみが気になったらプログラムが動くまでや、実行環境を箱で運ぶDockerもあわせてどうぞ。

理解度チェック

そのまま解けます(成績は保存されません)。無料アカウントを作ると、学習の記録と進捗の山登りが始まります。

1. Gitがコミットやファイルを識別する方法として正しいのは?

2. 「detached HEAD」の状態の説明として正しいのは?

3. git add した後にさらに同じファイルを編集してから git commit すると、コミットされる中身はどれ?

4. リベースやリセットでコミットを見失ったとき、元のハッシュをたどって復旧する手がかりになるのはどれ?

5. 分岐後に本流が1歩も進んでいないとき、マージが新しいコミットを作らず「名札を前へ動かすだけ」で済む形を何と呼ぶか、英語で答えてください。

6. 大量の緩いオブジェクトを差分(デルタ)でまとめて圧縮し、転送・保管を小さくする1ファイルを何と呼ぶか、英語で答えてください。

7. リベースがマージと違う点として正しいのは?

8. Gitのオブジェクトのうち「ファイルの中身そのものを保存し、ファイル名は持たない」のはどれ?