A Way of Code

興味の赴くままに書き綴っていきます。

Gitの仕組み - homebrewをフォークするためのGit&GitHub入門 中編

前回:Gitのセットアップ

前回、GitHubを使うためのセットアップをしていきました。
今回はGitとGitHubを学んでいきます。
(さらに次回はhomebrewをforkしていきます)

注意:

  • Gitを知らない人がいきなりGitHubを触ったときのメモなので、間違っているところがあるかもしれません。
  • CVSSubversion等のバージョン管理ツールの知識がある前提で書いています。
  • あくまでメモなので説明が至らないところが多々あります。ざっと読み流して、Gitを実際に触ってから読み返したほうが分かりやすいと思います。

1. Gitの構成

Gitは大きく分けて、リポジトリ、ステージングエリア、作業ツリーから構成されます。
f:id:toggtc:20120305020212p:plain

作業ツリー上で編集したファイル・ディレクトリの中からコミット対象のものをステージングエリアに持って行き、その後、コミットをすることでリポジトリに更新内容が格納されます。

1.1 リポジトリ

リポジトリはObject Store(オブジェクト格納領域)とも呼ばれます。
リポジトリというと、Object Storeよりやや広い響きがありますが、ここではリポジトリ=Object Storeとして表現します)

Gitのリポジトリは、概念的には単数または複数のブランチで構成されています。
f:id:toggtc:20120305020235p:plain

ブランチと聞くとSubversionを知っている方は、"trunk"と"branches"を思い浮かべるかもしれませんが、Gitの場合は"trunk"に相当するものが"master"ブランチなのです。masterブランチは、Gitリポジトリを作成する際にデフォルトで作成されるブランチで、名前はmasterである必要はありません。ですが慣習のため、他の人がみて分かりやすいようにmasterという名前にしておくことをお勧めします。

Gitのリポジトリは以下に示すblob, tree, commit, tagsというGitオブジェクトが保持されています。
個々のGitオブジェクトにはSHA-1によるオブジェクト名(オブジェクトID)が付与されます。

1.1.1 blobオブジェクト

blobオブジェクトは、ざっくり言ってしまうとファイルコンテンツに対応するオブジェクトです。
例えば、新たに"hello.txt"というファイルをリポジトリに追加したとすると、Gitでは"hello.txt"ファイルの中身をblobオブジェクトとして保管します。
f:id:toggtc:20120305020253p:plain

1.1.2 treeオブジェクト

treeオブジェクトはディレクトリに対応するオブジェクトで、当該ディレクトリに格納されたファイルに関するオブジェクトのIDやパス等の情報を持っています。
ディレクトリの中にサブディレクトリがある場合、そのサブディレクトリはまた別のtreeオブジェクトとして扱われます。
例えば、新たに"foo_dir"というディレクトリを追加すると、Gitではtreeオブジェクトとして保管します。
f:id:toggtc:20120305020303p:plain

1.1.3 commitオブジェクト

当該コミットに対応するリビジョンの情報を保持するオブジェクトです。
コミット時点のトップレベルのtreeオブジェクトのID、commuterやauthor、日付、コミットメッセージ等の情報が格納されています。また、親commitオブジェクトを保持しており、親を辿っていくことでHistoryが分かります。
以下に、コミットを行った時のGitオブジェクトの例を示します。

1度目のコミット:

  • hello.txtを新規作成し、ファイルの中身を"hello, git"としている

f:id:toggtc:20120305020314p:plain


2度目のコミット:

  • hello.txtはそのまま
  • 新たにfoo_dirディレクトリを追加
  • foo_dirディレクトリ直下にfoo.txtを新規作成し、ファイルの中身を"foo"としている

f:id:toggtc:20120305020328p:plain

1.1.4 tagオブジェクト

tagオブジェクトはコミットオブジェクトを指し示すオブジェクトです。
タガー(タグ付した人), 日付、メッセージ、コミットオブジェクトへの参照等が保持されています。
f:id:toggtc:20120305020342p:plain

1.2 ステージングエリア

ステージングエリアはIndexとも呼ばれます(さらに以前はcacheとも呼ばれていたようです)。
ステージングエリアは、ステージングと呼ばれる作業を行うための領域で、位置的にはリポジトリと後述する作業ツリーとの間にあります。
ステージングと聞くと、インフラな人は本番環境に移行する前のステージングの段階を連想するかもしれません。Gitのステージングもそれと似ていて、リポジトリにコミット(本番環境への移行)をする前に、ステージングエリア(ステージング環境)で確認を行います。
主な確認作業は、作業ツリー上での変更のうち、どの変更をリポジトリにコミットするかを選定する作業です。このステージング作業によって不用意なコミットを避けることが出来ます。
これらの操作は、git addによって行います。追加あるいは修正したファイルをgit addすることで、ステージングエリアに変更が追加されます。

ステージングエリアの中身は.gitディレクトリの中にある「index」というバイナリファイルです。このファイルには、ファイル名やファイルに対応するblobオブジェクト名、データの一覧等が保持されています。
リポジトリに保持される各種GitオブジェクトがImmutableなのに対し、ステージングエリアはMutableなバイナリファイルになっています。

以下の例では、hello.txtの内容を"hello, 1.1"から"hello, 1.2"へ変更しています。
f:id:toggtc:20120305020405p:plain

上記の図ではcommitをした段階で、"hello, 1.2"に対応するblobオブジェクト"6ccfdb"がObject Storeに追加されているように見えますが、厳密にはaddした段階で追加されています。そして、コミットした段階でツリーに"6ccfdb"が追加され、さらにcommitオブジェクトが付与されるのです。言い換えると、コミットを行うということは、追加あるいは修正したファイルへの参照を持つツリーを作成するということになります。
このことを、オブジェクトグラフで見てみましょう。

まず、最初の状態です。hello.txtの中身として"hello, 1.1" (65c2bb) が参照されています。
f:id:toggtc:20120305020422p:plain


次に、hello.txtの内容を"hello, 1.2"に変更し、ステージングエリアに追加( git add )します。
すると、"hello, 1.2"という内容に対して6ccfdbというオブジェクト名のblobオブジェクトがリポジトリ(Object Store)に追加されました。まだコミットはしていません。
f:id:toggtc:20120305020437p:plain

そしてステージングエリアでは、hello.txtの参照先として、6ccfdbが設定されています。
f:id:toggtc:20120305020444p:plain

この段階では、"hello, 1.2" (6ccfdb) はツリーから孤立しています。
ここでコミットを行います。すると、ステージングエリアの内容どおり、hello.txtの内容は6ccfdbに置き換えられます。これは65c2bbから6ccfdbへオブジェクトの参照先が変わるということになるため、6ccfdbを参照するツリーが新たに作成されます
さらに、新たに作成されたtreeオブジェクトに対してcommitオブジェクトが付与されています。
f:id:toggtc:20120305020456p:plain


なお、ステージングエリアを表示するには、git ls-files --stageコマンドを実行します。

1.3 作業ツリー

作業ツリーとは、ブランチ内から作業ディレクトリに展開されたファイル・ディレクトリ郡のことです。展開されたファイル/ディレクトリ群は直接編集することが出来ます。

ローカルにGitリポジトリを作成すると、以下のようなディレクトリ構成をみることが出来ます。(一部省略)

sample_project
├── .git
├── bar_dir
│   └── bar.txt
├── foo_dir
│   └── foo.txt
└── hello.txt

上記のsample_projectには「.git」というディレクトリと、hello.txtやfoo_dir、bar_dirといったディレクトリが格納されています。
「.git」ディレクトリは、リポジトリやステージングエリアが格納されています。それ以外のファイル/ディレクトリ群が作業ツリーです。(もしかすると、.gitディレクトリの一部のファイルも含めてsample_project全体を作業ツリーと呼ぶのかもしれない)

git checkoutをすることで、ブランチの内容を作業ツリーに反映できます。
以下の例ではfooブランチをチェックアウトしています。
f:id:toggtc:20120305020522p:plain

2. リモートリポジトリとローカルリポジトリ

リモートリポジトリは、既にCVSSubversion等を使っている方は馴染み深いですね。
サーバ側にあるリポジトリがリモートリポジトリ、ローカルで作業するためにコピーしたリポジトリがローカルリポジトリです。GitHubを使った場合、GitHub上にリモートリポジトリを配置することが出来ます。

2.1 クローン

git cloneコマンドを使うことで、リモートリポジトリをローカルリポジトリとして、ローカル環境にコピーすることが出来ます。
クローン元となったリモートリポジトリのことを一般に"origin"と呼びます。

2.2 複数のリモートリポジトリ

Gitは分散リポジトリです。CVSSubversionとは異なり、複数のリモートリポジトリを設定できます。これにより、いろいろなリモートリポジトリから変更を取得することが出来ます。
f:id:toggtc:20120305020541p:plain

例えば、homebrewの場合は、自分のGitHubにコピーしたhomebrewリモートリポジトリと、本家(Max氏)のリモートリポジトリから変更を取得できます。もっとも、追加したリモートリポジトリに対してコミット権限がなければ変更は反映できないので、その場合はread onlyということになります。
なお、Gitでは上流に位置するリモートリポジトリをupstreamと呼びます。原本情報が上流で、それを受取る側は下流です。GitHubの場合、フォーク元となったリモートリポジトリはupstreamです。Max氏のhomebrewをフォークした場合、upstreamはhttps://github.com/mxcl/homebrew.gitになります。

リモートリポジトリの追加は、git remote addコマンドによって行います。

2.3 リモートブランチ

前述のとおり、リポジトリは複数のブランチで構成されます。
それではリモートリポジトリ上にあるブランチとローカルリポジトリにあるブランチの関係はどうなるのでしょうか。
f:id:toggtc:20120305020600p:plain
上記の例では、リモートリポジトリとしてorigin, upstreamを登録しています。
ローカルリポジトリには、origin, upstreamそれぞれのmasterブランチと同期するリモートブランチ"origin/master", "upstream/master"を保持しています。
また、ローカルリポジトリにはローカルブランチmasterを持っています。ローカルで作業する際は、このローカルブランチを使います。そして、ローカルで更新した内容をリモートリポジトリにあるブランチに反映します。

2.4 リモートリポジトリの最新を取得する

もしかしたらGitの操作をしている間に、あるいはあなたが用事を思い出して出かけている間に、リモートリポジトリに対して誰かが更新をしたかもしれません。その更新は、これからあなたが変更しようとしている内容と重複するかもしれません。

そんなときは、修正作業に入る前に、まずリモートリポジトリ上にあるブランチから最新の変更を取り込みましょう。
リモートリポジトリの最新の内容を取り込む方法はいくつかあります。

2.4.1 fetchとmerge

fetchは、ローカルにあるリモートブランチを最新の状態に更新します。この段階ではステージングエリアや作業ツリー、ローカルのブランチには影響がありません。
f:id:toggtc:20120305020649p:plain


fetchで取得した更新を、ローカルブランチや作業ツリー等に反映するにはmergeコマンドを使用します。
mergeコマンドではいろいろなマージが出来るのですが、以下の例ではローカルにある指定したリモートブランチと、現在チェックアウトされているローカルブランチとをマージしています。このとき、マージされた結果がステージングエリアと作業ツリーに反映されます。
f:id:toggtc:20120305020716p:plain

なおmergeでは、作業ツリーやステージに何らかの修正があった場合、その修正差分とfetchで取り込んだ更新差分とを突合させ、

  • 競合する場合→競合エラーになる
  • 競合しない場合→更新差分をコミットして取り込む、修正差分はそのまま作業ツリーやステージングエリアに保持される

となります。
mergeには、他にも、コミットをせず作業ツリーを更新する方法もあります。
また、マージの方法は他にもリベースやチェリーピックがありますが、ここでは割愛します。

2.4.2 pull

pullは、fetchとmergeを同時に行うコマンドです。mergeを自動で行うため、pullを使用する際は、事前にmergeしたいローカルブランチがチェックアウトされているか確認しておくといいでしょう。
なお、普段はこちらのコマンドでいいのですが、単にリモートブランチの内容をウォッチするだけで、ローカルブランチや作業ツリーには変更を反映させたくない場合はfetchを使ったほうがいいでしょう。

2.5 リモートリポジトリに変更を反映する

ローカルで変更した差分をリモートリポジトリに反映するには、pushコマンドを使用します。
pushでは、ローカルのどのブランチを、どのリモートリポジトリの、どのブランチに反映させるかを指定することができます。
なお、pushはコミットされたもののみを送信するため、作業ツリーやステージングエリアにある変更は送信されません。

3. GitHubの概要

GitHubはGitを使ったプロジェクトホスティングサービスです。
特徴的なところは、ForkとPull Requestの機能です。

3.1 GitとGitHubのおいしい関係

GitHubを使うと、誰かのリポジトリを自分のリポジトリとしてコピーできます。
さらに、Gitは分散リポジトリですから複数のリモートリポジトリを設定することができます。
そうすると、GitHubとGitの組み合わせによって、誰かが作ったリポジトリを自分の作業用に持つことができる上に、その誰かのリポジトリから常に最新の更新を取り出すことが出来るわけです。
f:id:toggtc:20120308013545p:plain

3.1 Fork

フォークをすることで、誰かのリポジトリを自分のGitHubのリポジトリとしてコピーできます。
フォークはGitの機能ではなく、GitHubが提供するサービスです。Forkという名のとおり、フォーク元のリポジトリが親リポジトリとしてGitHubに管理されます。GitHubさんは目の付け所が違いますね。

  • フォークをしただけでは、相手のGitリポジトリと自分のGitリポジトリはそれぞれ(Git側から見ると)独立している。GitHubが繋がりを管理している。
    • 自分のGitリポジトリで何をしようが、相手には影響がない。(ただし後述のPull Requestをするときは影響がある)
    • フォークをすると、相手にフォークをしたことが通知される。

3.2 Pull Request

Pull Requestは、自分のGitHub内で修正した内容を、フォーク元にpullしてくれ、と依頼できる機能です。
Pull Requestをするとき、どのブランチをpullしてもらうかを指定することが出来ます。Pull Requestしたあとで対象のブランチに余計な変更を入れてしまうと、相手側がpullしたタイミングでその余計な変更が取り込まれてしまうため注意が必要です。

次回

Gitの仕組みをざっと見てきました。
さらにGitを理解するには、以下のサイトを参考にし、実際にGitを動かしてステージングエリアの状態やオブジェクトの中身を解析することをお勧めします。
http://progit.org/
http://progit.org/book/ja/  #日本語化していただいた方に感謝!

次回は、実際にhomebrewをForkしてPull Requestを行うまでの一連の作業を行います。

関連記事

homebrewをフォークするためのGit&GitHub入門
前編:Gitのセットアップ
中編 : Gitの仕組み
後編1:実践Git&GitHub (1/2)
後編2:実践Git&GitHub (2/2)