Git
Git ・ ぎっと
変更履歴をハッシュでつながったコミットの連なりとして記録する分散型バージョン管理システム。
✦ 30秒で試す — 航海ブランチを何本作っても、ファイルは1つもコピーされないリベースした後の a と b のIDは?予想してみる概要
Gitは、ソースコードの変更履歴を記録・管理するバージョン管理システムです。「いつ・誰が・何を・なぜ変えたか」をコミットという単位ですべて記録し、任意の時点の状態に戻したり、変更同士を比較したりできます。手作業の「ファイル名に日付を付けて複製する」管理とは違い、履歴そのものがデータ構造として扱えるのが本質です。
もう1つの柱がブランチです。履歴を枝分かれさせて複数の作業を並行に進め、あとで合流(マージ)できます。GitHubに代表される共有プラットフォームと組み合わせることで、プルリクエストによるコードレビュー、CI/CDによる自動検証・自動デプロイといった、現代の開発プロセス全体の土台になっています。今日ソフトウェア開発の職に就くなら、言語より先に触ることになる道具かもしれません。
なぜ生まれたか
バージョン管理システム自体はGit以前からあり、2000年代前半はSubversion(SVN)などの「集中型」が主流でした。集中型では履歴を持つのは中央サーバだけで、コミットや履歴の閲覧のたびにサーバへの接続が必要です。オフラインでは作業しづらく、ブランチ作成も重い操作だったため、「気軽に枝分かれして実験する」文化は育ちにくいものでした。
Gitが生まれた直接のきっかけはLinux(カーネル)の開発です。世界中の数千人が並行に開発する巨大プロジェクトで使っていた商用ツールBitKeeperが使えなくなり、2005年、Linus Torvaldsが自ら代替を書き始め、わずか数週間でカーネル自身の管理に使えるところまで仕上げました。要件は「完全に分散して動くこと」「数万ファイルでも高速なこと」「履歴の改竄や破損が検出できること」。最後の要件に対する答えが、中身から計算したハッシュ値でコミットを名づけ、各コミットが親のハッシュを持つという設計です。各開発者が履歴の完全な複製を手元に持つ分散型の設計により、コミットもブランチもローカルで一瞬で完了するようになり、「小さくコミットし、気軽にブランチを切る」現代のスタイルが可能になりました。
詳細
3つのエリアとコミットの流れ
Gitを使う日常は、「作業ディレクトリ→ステージングエリア→リポジトリ」という3つのエリアの往復です。ファイルを編集し、コミットに含めたい変更だけを git add でステージングエリアに載せ、git commit でメッセージ付きの履歴として確定します。ステージングという中間段階があるおかげで、「直した内容のうち意味のまとまりごとに分けてコミットする」ことができます。
内部的には、コミットは「その時点のスナップショット+親コミットのハッシュ+作者・日時・メッセージ」の塊で、その中身全体のハッシュ化で得た値(既定は衝突検出つきの SHA-1。SHA-256 形式への移行も進んでいます)が名前になります。親のハッシュが中身に含まれているので、コミットはハッシュの鎖でつながり、過去のどこか1か所を書き換えると、そこから先のすべてのコミットのハッシュが変わります。だから改竄や破損は必ず検出できます。一方、ブランチの実体は「特定のコミットのハッシュを書いた小さなファイル」、つまりただのポインタにすぎず、だからこそ作成も切り替えも一瞬です。
ブランチ、マージ、コンフリクト
チーム開発の基本形は「main(本流)を安定に保ち、作業はブランチを切って行い、完成したらマージで合流させる」というものです。GitHub上ではこの合流の前にプルリクエスト(PR、コードレビューの単位)を挟み、差分をレビューし、CI/CDでテストを自動実行してからマージするのが標準的な流れです。履歴が枝分かれして再び合流する様子は、コミットのつながりとして図にすると分かりやすくなります。
合流時に同じ箇所を双方が変更していると「コンフリクト(衝突)」が起き、どちらを採用するか人間が判断してマージを完成させます。コンフリクトは異常事態ではなく並行開発の必然ですが、ブランチを長生きさせるほど差分が膨らんで解消が苦しくなります。「ブランチは小さく短命に、こまめに本流を取り込む」が実務の基本姿勢です。合流の手段にはマージのほかにリベースもあります。マージは「親を2つ持つコミット」を1つ足して2本の枝を結びます。どちらの枝の履歴もそのまま残るので安全ですが、履歴は網目状になります。リベースは、自分のコミットを本流の先頭に付け替えて履歴を一直線にします。ただし「付け替え」は比喩で、実際には同じ変更内容のコミットを新しい親の上に作り直しています。親のハッシュが変わるので、中身の変更が同じでもハッシュはすべて新しくなり、元のコミットはどのブランチからも辿れない状態で残ります。本流の先頭がマージ先の子孫なら、新しいコミットを作らずにブランチの指す先を進めるだけで済み、これを早送り(fast-forward)と呼びます。
下の部品で確かめてください。コミットを積み、ブランチの札をつかんで別のコミットへ付け替え、マージとリベースを実行すると、グラフが実際に組み変わります。リベースしたあとの点のハッシュが、元の点と違うことに注目してください。
いま HEAD は feature(作業中の枝)。「コミット」で点を積むか、「リベース」「マージ」を押してみてください。札(main / feature)をつかんで別の点に付け替えることもできます。札をタップすると HEAD が切り替わります。
リベースで作り直されたコミットは「同じ変更に見えて、別のコミット」です。すでに push してほかの人が取り込んだコミットをリベースすると、相手の手元には古いハッシュの鎖が、自分の手元には新しい鎖が残り、同じ変更が二重に存在する食い違いが起きます。「push 済みの履歴はリベースしない」が定石なのはこのためで、履歴の読みやすさを取るか操作の安全さを取るかのトレードオフとして、手元の未共有のコミットはリベースで整え、共有済みの枝はマージで結ぶ、と使い分けます。
分散型であることの意味
Gitではクローンした全員が全履歴のコピーを持ちます。コミット・ブランチ・履歴閲覧・差分比較はすべてローカルで完結し、ネットワークが必要なのは push/pull で他のリポジトリと同期するときだけです。GitHubやGitLabは技術的には「みんなが同期先として合意した、ただのリモートリポジトリ」であり、Gitの仕組み上の中心ではありません。この分散性は事実上のバックアップにもなっており、中央サーバが消えても誰かのクローンから完全に復元できます。
実務での使いどころと落とし穴
Gitの価値は「安心して変更できること」に尽きます。いつでも戻れるからこそ大胆にリファクタリングでき、git log や git blame で「このコードはなぜこうなっているのか」を過去のコミットメッセージから辿れます。だからこそコミットは「意味のある単位で小さく、メッセージには何をなぜ変えたかを書く」ことが将来の自分とチームへの投資になります。
落とし穴の代表は、push済みの履歴を書き換えて(rebase や force push)チームメイトの履歴と食い違わせてしまうこと、そしてパスワードやAPIキーなどの秘密情報をコミットしてしまうことです。Gitは履歴を消さない設計なので、一度コミットした秘密情報は「削除コミット」を積んでも過去のコミットの中に残り続けます。履歴を書き換えて消しても、すでに誰かのクローンやフォークに複製されていれば取り戻せません。push してしまったら履歴の書き換えではなく、鍵そのものの無効化・再発行で対処するのが原則で、そもそも秘密情報はリポジトリの外で扱います(シークレット管理)。
試問
コミット(点)が親を指す矢印でつながり、main と feature の札がそれぞれの先頭を指しています。main は 1→2→3(3 は「誤字の修正」)、feature は 2 から分かれた a→b(検索機能)です。ボタンでコミット・マージ・リベースができ、札はタップで作業中の枝(HEAD)に切り替え、ドラッグで別の点へ付け替えられます。
いま HEAD は feature(作業中の枝)。「コミット」で点を積むか、「リベース」「マージ」を押してみてください。札(main / feature)をつかんで別の点に付け替えることもできます。札をタップすると HEAD が切り替わります。
