Git

Git（ギット）は、プログラムのソースコードなどの変更履歴を記録・追跡するための分散型バージョン管理システムである。Linuxカーネルのソースコード管理に用いるためにリーナス・トーバルズによって開発され、それ以降ほかの多くのプロジェクトで採用されている。Linuxカーネルのような巨大プロジェクトにも対応できるように、動作速度に重点が置かれている。現在のメンテナは濱野純 (英語: Junio C Hamano) で、2005年7月から担当している。 Gitでは、各ユーザのワーキングディレクトリに、全履歴を含んだリポジトリの完全な複製が作られる。したがって、ネットワークにアクセスできないなどの理由で中心リポジトリにアクセスできない環境でも、履歴の調査や変更の記録といったほとんどの作業を行うことができる。これが「分散型」と呼ばれる理由である。 Linuxカーネルの開発では、Linux Kernel Mailing List に投稿される多数のパッチをメンテナーたちがソースコードに適用するという形式が採用されている。これらの作業を効率的に行うため、当初はBitKeeperというバージョン管理システムを用いていたが、このソフトウェアは商用ソフトウェアであった（クライアントはバイナリ版のみ無料で、サーバは商用だがBitMover社の厚意で無料で使えていた）。この状況を快く思わない人々がBitKeeperのクローンを実装したことから、この環境が使えなくなってしまい（BitKeeper#ライセンス問題やBitKeeper#価格変更を参照）、その代替として2005年にGitが開発された。 Linuxカーネルの開発では、巨大なソースコードの集合を扱うため、変更点の抽出やリポジトリ操作ができるかぎり高速にできる必要がある。他の様々なバージョン管理システムをあたったが、満足のいくものがなかったため、Gitではこのような問題も出来る限り解決できるよう、いくつかのアイデアが導入されている（この部分は、他のバージョン管理システムにも同様の機能が導入されるようになった）。 Gitは分散型のソースコード管理システムであるため、リモートサーバ等にある中心リポジトリの完全なコピーを手元（ローカル環境）に作成して、そのローカルリポジトリを使って作業を行う。 一般的な開発スタイルでは、大雑把に言えば、以下のようなステップの繰り返しで作業が行なわれる： リモートサーバ等にある中心リポジトリをローカルに複製する (git clone)。 ローカルでコンテンツの修正・追加・削除を行い、ローカルリポジトリに変更履歴を記録する (git commit)。必要に応じて過去の状態の閲覧や復元などを行う。場合によってはこのステップを何度か繰り返す。 ローカルの変更内容を中心リポジトリに反映させる (git push)。作業者ごとの変更内容が衝突することもある。Gitが自動で解決できる場合もあれば、手動での解決 (git merge）が必要なこともある。 更新された中心リポジトリ（他者の作業内容も統合されている）をローカルの複製にも反映する (git pull)。これによりローカル環境のコードも最新の内容になるので、改めてステップ2の作業を行う。 リポジトリ間の通信 (clone, pull, push) では以下のプロトコルが使用できる。 ローカルファイルシステム SSH git（認証機能なし） HTTP/HTTPS pushするためには、サーバー側にgit-http-backendプログラムまたはWebDAVが必要。 FTP/FTPS: ただし、もはや使うべきではないとされている。 以前はrsyncも使用できたが、2.8.0で廃止された。 Gitの設計はBitKeeperとMonotoneが元になっている。元々のGitはローレベルなエンジンとして設計されていたが、これは、他の開発者がCogitoやStGITのようなフロントエンドを容易に開発できるようにすることを目的としていた。現在では、Gitのコア自体もユーザから直接利用できるようになっている。 Gitの設計には、リーナスが大規模プロジェクトのメンテナンスを行った経験や、ファイルシステムのパフォーマンスに関する深い知識、また実用性のあるシステムを短期間に作成しなければならないという差し迫った必要性（BitKeeperを参照）が反映されている。これらの影響は、以下のような形で実装に現れている。 ノンリニアな開発スタイルに対する強力なサポート。Gitではブランチやマージが高速に行える。また、ノンリニアな開発の履歴を可視化・ナビゲートするための専用のツールを同梱している。また、ツリーに対する1回の変更に対して、複数回のマージが発生するという前提を置いているが、これは変更点が複数のレビュアーによってレビューされることを想定しているためである。 分散開発。他の分散型バージョン管理システム（Mercurial、BitKeeper、Darcs、Bazaar、SVK、Monotone）と同様に、Gitでも各々の開発者がリポジトリの完全なコピーをローカルに保持しており、各開発者の行った変更は他のリポジトリにコピーされる。これらの変更は新しい開発用ブランチとしてインポートされる。また、ローカルな開発用ブランチへのマージも可能である。 HTTP、FTP、gitプロトコル（sshを利用したトンネリングも可能）を使用したリポジトリの配布が可能。また、CVSサーバのエミュレーション機能を使用すれば、既存のCVSクライアントやIDEのプラグインからGitリポジトリにアクセスできる。 git-svnを使用すれば、Subversionおよびsvkのリポジトリを直接操作できる。 大きなプロジェクトにおける処理の効率化。Gitは非常に高速かつスケーラブルになるよう記述されている。Mozillaによって行われたパフォーマンステストでは、他のリビジョンコントロールシステムと比較して10倍、処理によっては100倍高速に動作することが示された。 ツールキット化された設計。gitはC言語で書かれたプログラム一式と、それらのラッパーとなる何本かのシェルスクリプトで構成されている。GitをWindowsに移植する過程で、シェルスクリプトのほとんどはC言語で書き直された。しかし、現在でも複数のコンポーネントを繋げることで複雑な処理を容易に行えるような設計になっている。 プラガブルなマージアルゴリズム。Gitでは不完全なマージに対する優れたモデル化が行われている。また、不完全なマージを補完するアルゴリズムも複数存在する。最終的に自動的なマージが行えなかった場合には、ユーザによる編集が必要である旨がユーザに通知される。 ガベージコレクションは行われない。操作を中断したり変更の取り消しを行った場合、データベース中には不要なデータが残ったままになる。これらのデータは操作対象のオブジェクトの履歴に比例して大きくなる。git-gc --pruneコマンドを使用して明示的にガベージコレクションを行うこともできるが、この処理には時間がかかる。 Gitの特徴の一つとして、ディレクトリツリーに対するスナップショットを作成する点が挙げられる。初期のバージョン管理システム（SCCSやRCS）では、個々のファイルを処理の対象としており、直近のバージョンに対して差分符号化を行うことでリポジトリのサイズを縮小することに機能の重点が置かれていた。以降のバージョン管理システムでも、この「プロジェクト内の複数バージョンにまたがって一つのファイルを追跡できる」という概念が継承されていた。 一方、Gitではこのコンセプトを使用していない。その結果として、Gitはソースコードツリー中のファイルのリビジョン間の関係性を記録しなくなっている。これにより、Gitは以下のような特徴をもつようになっている。 プロジェクト全体の変更履歴を調べるよりも、一つのファイルの変更履歴を調べる方が時間がかかる。特定のファイルの変更履歴を取得する場合、Gitはツリーの履歴全体を走査し、各リビジョンでそのファイルに変更があったかどうか調査する必要がある。ただしこの処理手順では、一つのファイルに対しても、任意のファイルの集合に対しても、ほぼ同じ時間で履歴の調査が行える。よく行われる処理としては、例えば「ソースツリー中のあるサブディレクトリと、それに関係するヘッダファイルの調査を行う」といったものがある。 ファイル名の変更を明示的に扱わない。CVSに対してよく挙げられる不満として、ファイルのリネームや移動を行うと変更履歴が途切れてしまう問題がある。これは、変更履歴の識別にファイル名を使用しているためである。CVS以降に開発されたバージョン管理システムの多くでは、管理対象のファイルに対してファイル名より寿命の長い、内部的な名前（inode番号のようなもの）を付与することでこの問題を解決している。一方、Gitではそのような内部名を使用しておらず、その点が長所であるとしている。プログラムのソースコードに対しては、リネームの他にも分割やマージといった処理が行われる。これらの処理を単純にファイルのリネームとして十把一絡げに扱うと、実際にソースコードツリーに対して行われた処理が何であるか不明瞭なまま履歴に記録されてしまう。Gitでは、リネームの検出をスナップショット作成時ではなく履歴のブラウズの際に行うことでこの問題を解決している。（単純に考えれば、リビジョンN中のあるファイルに対して、リビジョンN-1中に同じ名前のファイルがあれば、それが元ファイルと考えられる。しかし、リビジョンN-1中に似たような名前のファイルがない場合もある。この場合Gitは、リビジョンN-1のみに存在し、かつ似たような内容のファイルを検索する。）しかし、この処理は履歴の表示を行う度にCPUに負荷のかかる処理が必要となる。そのため、使用するヒューリスティックを選択するオプションが提供されている。 リポジトリの保存方法については批判もある。 Gitは新しく作成されたオブジェクトを個別のファイルの形で保存する。各ファイルは圧縮されて保存されるが、そうであったとしてもこの方法は記録領域を大量に必要とするため非効率的である。Gitはこの問題を“pack”を使用することで解決している。複数のオブジェクトは一つのファイル（またはネットワークバイトストリーム）の形にパックされ、各pack間で差分圧縮が行われる。packの差分圧縮にはヒューリスティックが用いられる。例えば、同じ名前のファイルは似た内容である可能性が高いものとして処理される。ただし、リポジトリの正確性を保つため、ヒューリスティックに完全に依存することはない。現在のGitでも新しく作成されたオブジェクト（新しく加えられた履歴）はまず単一のファイルとして保存されるため、空間効率を高く保つためには定期的な再パックが要求される。Gitはこの定期的な再パックを自動的に行うが、git gcコマンドを使用することで手動で再パックを行うこともできる。 Gitは以下のマージアルゴリズムを実装している。アルゴリズムはマージ時に選択することができる。 resolve 従来通りの3-wayマージアルゴリズムを使用する。 recursive 3-wayマージの変種を使用する。ブランチのmergeやpullを行う場合のデフォルトである。3-wayマージにおいて共通の祖先が複数ある場合、共通の祖先からのmerge treeを作成し、それをreference treeとして3-wayマージを行う。Linuxカーネル2.6の開発で行われたマージコミットの履歴から、このアルゴリズムを使用するとマージの衝突が少なく、マージ漏れもなかったことが報告されている。さらに、リネームを伴うマージに対しても検出および処理が可能である。 octopus 3つ以上のheadからのマージを行う場合のデフォルトである。 リーナス・トーバルズによれば、 英語のスラングとして、Gitには「バカ」「間抜け」といった類の意味がある。この自虐ネタはもちろん皮肉で、これはリーナスがLinuxの名前を決める際に自身の名前にちなんだ名前を付けるよう強要されたことから来ている。(Linux#名前の由来を参照) Gitのオフィシャルサイトのウィキでは、“Git”という名前に対して他にもいくつかの解釈がなされている。例としてはGlobal Information Trackerなどが挙げられる。 Gitの開発は、Linuxカーネルの開発者の多くがBitKeeperのシステムに対するアクセスを禁止されたことに端を発している（BitKeeper#価格変更を参照）。これは、アンドリュー・トリジェル (Andrew Tridgell) がプロプラエタリなソフトウェアであるBitKeeperのプロトコルをリバースエンジニアリングしたことに対し、BitKeeperの著作者であるラリー・マクボイがこれをライセンス違反であるとして、BitKeeperの無料提供を止めたためである。Linux.conf.au 2005のキーノートにおいて、トリジェルはこのリバースエンジニアリングの手順について説明を行ったが、内容はBitKeeperのサーバの適切なポートにtelnetでアクセスし“help”とタイプするだけという単純なものだった。 リーナスはBitKeeperと同じように使える分散型バージョン管理システムを探していたが、無料のシステムで彼の要求（特に速度面での要求）に適合するものは見つからなかった。リーナスが書いたメールによると、2005年4月7日頃に最初のプロトタイプを作成していたようである。 リーナスは以下のような原則に基づいて設計を行っている。 CVSを「悪い見本」とする。設計上のことで確信が持てない場合は、CVSと逆の決断をする。リーナスは冗談めかして以下のように語っている。 “カーネルメンテナンスの最初の10年間、僕らは文字通りtarボールとパッチを使っていた。CVSよりもずっと優れたソース管理システムさ。僕は営利企業（トランスメタ）でCVSを7年間使わされたことで、CVSを強烈に憎むようになった。CVSを強烈に憎んでいると言う時には、このことも言っておかなくちゃいけないね。観衆の中にSVN (Subversion) のユーザがいるなら、この場から去ったほうがいいかもしれない。僕がCVSを強烈に嫌悪しているということは、僕がSubversionが史上最大の無意味なプロジェクトであると思っていることも意味しているんだ。Subversionのしばらくのスローガンは‘ちゃんとCVSをやる’とかそんなものだったよね。そんなスローガンから始めたら、どこにも辿りつけないよ。CVSをちゃんとやるなんて不可能なのさ。” 分散型の、BitKeeperのようなワークフローをサポートする。 “BitKeeperだけが、「まあ使ってもいいかな」と最初に思わせてくれたSCMだというわけではないけれど、BitkeeperはSCMというものの存在意義と、実際にどう使うことができるのかを教えてくれた。だから、Gitは技術的な観点とかいろんなところでBitkeeperとは随分違うものになっているけど（それはもう一つの設計目標でもある。Bitkeeperのクローンではないことをはっきりさせたかったから）、Gitのワークフローの多くは、Bitkeeperが教えてくれたフローから直接きたものになっているんだ。” データ破壊に対する強力な抑止機能。データ破壊は、偶然によるものと意図的なものの両方を想定している。 非常に高い処理速度。 最初の3つの条件によって、既存のバージョン管理システムはMonotoneを除き全て選に漏れてしまい、4つ目の条件で該当するものがなくなってしまった。そのため、Linuxカーネル2.6.12-rc2のリリース直後に、リーナスは自分で開発を始めた。 Gitの開発は2005年4月3日に開始された。プロジェクトとしてのアナウンスは4月6日に行われ、4月7日にはセルフホスティングされるようになった。4月18日には複数のブランチからのマージが最初に行われた。4月29日にはリーナスの目標としていた処理速度が実現された。Linuxのカーネルツリーにパッチを当てるベンチマークで、初期のGitでは毎秒6.7個のパッチを処理している。6月6日には、GitによるLinuxカーネル2.6.12のリリースが行われた。 BitKeeperからの影響で、リーナスは従来と同じようなアプローチを意図的に避けており、結果としてGitは非常にユニークな設計になっている。技術に長けたユーザがGitを利用できるようになるレベルまではリーナスが開発を行っており、その後、2005年6月26日にはプロジェクトへの主要な貢献者であった濱野純 (Junio C Hamano) にメンテナンスが引き継がれた。濱野は2005年12月21日にバージョン1.0のリリースを行い、2021年5月現在も彼がメンテナンスを行っている。 ソフトウェア開発におけるGitブランチの作成・更新モデルをブランチモデルと呼ぶ。 GitHub flowはブランチモデルの一種である。GitHub flowの方針は「masterブランチは常にデプロイ可能」である。コードの変更を行う際にはトピックを名称とするブランチをmasterからforkする。マージ時を含め他者からの意見募集・レビューを行う際にはpull request（merge request）を用いる。pull request時に自動化テスト（c.f. 継続的インテグレーション）をおこなうことでmergeの安全性を確保し、masterが常に正常 = デプロイ可能に保たれる。GitHub flowが有効な分野は継続的デリバリー・継続的デプロイメントが有効な分野、例えばWebサービスが挙げられる。 2011年の提唱時と比較して、GitHub社ではモデルの一部を変更している。オリジナルのGitHub flowでは更新がmasterへマージされ、そこからデプロイされる。GitHub社ではレビュー後（CI後）のtopic branchが最終テストとしてプロダクション環境へデプロイされる。そこで問題がないと確認されたのちにmasterへmergeされている。このモデル変更により、CIで判明しなかったバグが発生した場合にmasterのrevert commitではなくmasterの再デプロイで対応できる。 Gitはブランチのマージ（merge）に対応している。 マージが行われるとマージコミット（英: merge commit）が生成される。コンフリクトが発生しない場合、git mergeがマージコミットを自動生成する。コンフリクトが発生した場合、まず安全な範囲がマージされインデックスとワーキングツリーが更新される。自動マージできないコンフリクト部は、ワーキングツリーに特定の記法で両方の変更が併記された状態になる。ユーザーによるコンフリクト修正後git addをおこない、git commitあるいはgit merge --continueをおこなってはじめてマージコミットが生成される。 マージコミットの実体は2つ以上のparent属性を持つただのコミットオブジェクトである（特別なGitオブジェクトではない）。通常のコミットオブジェクトと同様に変更後ファイルセットがtreeに記録され、更新の元となっている（複数の）コミットがparentに設定されているだけである。