Git のコマンドだけでなく、その仕組みを学ぶ

Git、自分のための覚え書き

Git は、よく使用されている分散型ソース・コード・リポジトリーです。Git は Linux を考案した Linus Torvalds 氏が Linux カーネルのソース・コードを管理するために作成しました。GitHub のようなサービスは、すべて Git をベースとしています。さらに、IBM の DevOps Services でも、IBM Rational Team Concert ソース・コード・リポジトリーと併せて Git を使用しています。つまり、Linux の世界でプログラミングを行う場合や、IBM の DevOps Services を Git と一緒に使用する場合には、Git を十分に理解していることが役立つのです。

私が Git を扱い始めた頃、CVS (Concurrent Versions System) と Apache SVN (Subversion) で経験を積んできたことから、従来型のソース・コード・リポジトリー・システムという観点で Git を理解しようとしました。けれども、その考え方で理解できたのは、Git の機能の限られた部分のみです。当時に比べると、今では遥かに深く Git を理解するようになりました。したがってこの記事は、Git の仕組みを自分でおさらいするための「覚え書き」のようなものであり、同時に Git 入門者にその仕組みを説明するためのものでもあります。この記事では、読者に CVS や SVN といった従来型のソース・コード・リポジトリーについての知識があることを前提とします。

基礎

まずは、従来型ソース・コード・リポジトリーでの基本例を取り上げます。図 1 に示すように、従来型のソース・コード・リポジトリーでコンテンツとして扱われるのは、ファイルとサブフォルダーを含むフォルダーです (CVS と Git が実際に扱うのは、フォルダーではなく、パスの場所にあるファイルです)。リポジトリーはコンテンツのすべてのバージョンを保持する場所であり、コードに変更を加える場所は、作業ディレクトリーです。コードに変更を加えるには、リポジトリーから作業ディレクトリーにコードをチェックアウトします。作業ディレクトリーで変更を行った後は、その変更をリポジトリー内のコンテンツの新しいバージョンにコミットします。

図 1. 従来型ソース・コード・リポジトリーでのワークスペースの扱い

コミットが行われるたびに、前の親バージョンから派生したコンテンツに変更が加えられた子バージョンが作成されます (図 2 を参照)。コンテンツは、ひと続きのバージョンとして保管されます。スナップショットとも呼ばれるこれらのバージョンは、コミット操作によって生まれた親子関係でリンクされています。コミットによって親バージョンと子バージョンとの間で変更された情報は、変更セットと呼ばれます。

この一連のバージョンは、ストリームまたはブランチと呼ばれます。メイン・ストリームは、SVN ではトランクと呼ばれる一方、CVS では一般に HEAD という名前で通っています。Git では通常、メイン・ストリームには master という名前が付けられます。実装プロジェクトでは、特定の機能を分けて開発するため、あるいは古いバージョンの保守のために、ブランチが使用されます。

図 2. 従来型リポジトリーでの新規バージョンの作成

ここまでの説明だと、Git は従来型のソース・コード・リポジトリーと非常によく似ているように見えると思いますが、残念ながら似ているのはここまでです。CVS と SVN には主要な特徴の 1 つとして中央リポジトリーがありますが、Git は分散型リポジトリーです。つまり、ソフトウェア開発で、複数のリポジトリーを連動させることができます。実際、開発者それぞれのリポジトリーがサーバー・ベースの Git リポジトリーと同じように機能して通信します。

Git の仕組みとは？

Git の主要原理は、一度理解してしまえば、驚くほど単純です。

まず、Git はスナップショット (コミット操作ごとに 1 つのスナップショット) 内のコンテンツを扱うとともに、前のスナップショットから次のスナップショットに変更セットを適用する方法や、変更セットを前のスナップショットにロールバックする方法を把握しています。これは重要な概念です。私の意見では、変更セットの適用とロールバックの概念を理解すれば、Git を遥かに簡単に理解して操作できるようになります。

これはまさに基本原理であり、すべてはこの原理に従います。このことを踏まえて、Git をもう少し深く探っていきましょう。

Git の操作

git init - リポジトリーを初期化する

- リポジトリーを初期化する git checkout <branch> - ブランチをリポジトリーから作業ディレクトリーにチェックアウトする

- ブランチをリポジトリーから作業ディレクトリーにチェックアウトする git add <file> - ファイル内の変更を変更セットに追加する

- ファイル内の変更を変更セットに追加する git commit - 変更セットを作業ディレクトリーからリポジトリーにコミットする

Git の操作を始める上で必要なことは、「 git init 」コマンドを実行することのみです。このコマンドによって、カレント・ディレクトリーが Git 作業ディレクトリーに変更され、そこに .git ディレクトリー (隠しディレクトリー) が作成されて、その中にリポジトリーが作成されます。これで、Git の操作を始められるというわけです。 checkout コマンドと commit コマンドは他のソース・コード・リポジトリーと同様ですが、変更セットに重点が置かれているため、Git には (SVN と同様に) add コマンドが追加されています。このコマンドにより、作業ディレクトリー内の変更は、次のコミットに備えてステージング・エリアに追加されます。このステージング・エリアは、通常、インデックスと呼ばれます。図 3 に、スナップショット・バージョン A からスナップショット・バージョン B の間で変更セットが作成されるプロセスを示します。

どの変更が追加され、どの変更が追加されなかったのか、そして現在どのブランチにいるのかを追跡するには、 git status が役立ちます。

図 3. Git での変更セットの作成

git status - 作業ディレクトリーのステータスを表示する

- 作業ディレクトリーのステータスを表示する git log - 作業ディレクトリーの変更履歴を表示する

- 作業ディレクトリーの変更履歴を表示する git diff - 変更の差分を表示する

- 変更の差分を表示する git diff --cached - ステージング・エリアの差分を表示する

- ステージング・エリアの差分を表示する git diff <name> -- <path> - 作業ディレクトリーと (ID または名前で) 指定されたコミットの間の <path> の差分を表示する

git log は、作業ディレクトリー内の変更 (つまり、コミット) の履歴を表示します。あるいは git log <path> を実行して、特定のパスに適用された変更を表示することもできます。

git status はワークスペース内の変更されたファイルならびにインデックス内のファイルを一覧表示しますが、ファイル間の差分を調べる場合には、 git diff コマンドを使用することができます。 git diff をそのまま (パラメーターなしで) 実行すると、まだインデックスに追加されていない作業ディレクトリー内の変更のみが表示されます。インデックスに実際に含まれている内容、つまりステージングされた変更を表示するには、 git diff --cached を使用する必要があります。 git diff <name> または git diff <name> -- <path> を使用すると、現在の作業ディレクトリーと (<name> で) 指定したコミットの間の差分が、それぞれ作業ディレクトリー全体または (<path> で) 指定したパスに関して示されます。<name> には、コミット ID またはブランチ名、あるいはそれ以外の名前を指定することができます。ちょうど良い機会なので、命名法について説明します。

命名法

注:

コミット ID は長いため、以降の図では「(A)」、「(B)」といった省略形を用いることにします。

Git での命名法について見て行きましょう。Git の主要な要素であるスナップショットには、コミット ID で名前が付けられます。コミット ID は、例えば「c69e0cc32f3c1c8f2730cade36a8f75dc8e3d480」のようなハッシュ ID です。コミット ID はスナップショットのコンテンツから派生し、実際のコンテンツとメタデータ (サブミットの時間、作成者の情報、親など) で構成されます。スナップショットには、CVS でのようなドット付きバージョン番号や、SVN でのようなトランザクション番号 (および最上位の /branches ディレクトリーからのパス) は使用されません。このため、Git スナップショット名からある種の順序を判断することはできません (他のリポジトリーでは、名前から順序を判断することができます)。Git では便宜上、これらの長いハッシュを短縮名に省略することができます。短縮名は、ID の先頭文字から最低限必要な数だけ文字を取り出して作成したものなので、リポジトリー内では一意の名前となります。上記の例で短縮名に該当するのは、「c69e0cc」です。

コミットという用語は、スナップショットを作成することを意味する動詞としても、作成されたスナップショットの名前としても使われることに注意してください。

一般に、ユーザーがコミット ID で操作する必要はありません。ユーザーが操作するのはブランチです。他のソース・コード・リポジトリーでは、名前付きの変更ストリームをブランチと呼んでいますが、Git ではスナップショットから次のスナップショットへと変更が次々に適用されるため、変更のストリームは変更セットの順序付きリストとなります。Git でのブランチは、特定のスナップショットへの名前付きポインターに過ぎません。つまり、ブランチは、そのブランチが使用される際に新しい変更を適用すべき場所を示すものです。変更がブランチに適用されると、そのブランチのラベルも新しいコミットに移されます。

ワークスペースから変更が配置される場所を Git がどのように把握するのかと言うと、その場所は、HEAD が指す場所になります。開発の HEAD は、最後にワークスペースからチェックアウトした場所であり、さらに重要なことに、変更をコミットする場所です。一般に HEAD は最後にチェックアウトしたブランチを指します。これが、CVS での HEAD という用語の解釈とは異なる点です。CVS では HEAD を、デフォルト・ブランチの開発の頂点として解釈します。

tag コマンドは、コミットに名前を付けて、ユーザーが理解しやすい名前で個々のコミットに対処できるようにします。基本的に、タグとはコミット ID のエイリアスですが、ショートカットを使ってコミットに対処することもできます。HEAD を作業ディレクトリーの開発頂点とすれば、HEAD^1 が HEAD コミットの最初の親、HEAD^2 が 2 番目の親といった具合になります。

詳細については、gitrevisions の man ページを参照してください。タグやブランチ名などの名前はコミットへの参照であるため、refname と呼ばれます。名前が (通常はブランチによって) 作成されてから現在の状態に至るまで、名前の存続期間中に変更された内容は reflog によって示されます。

ブランチ

ブランチ操作の背景となる概念は、各スナップショットが複数の子を持てるということです。同じスナップショットに 2 番目の変更セットを適用すると、新しい別の開発ストリームが生まれます。そのストリームに名前が付けられた場合、それはブランチと呼ばれます。

図 4. Git でのブランチ構造の例

以上の内容を Git でのブランチ構造の例を用いて示したのが図 4 です。開発が行われている master ブランチは、現在、スナップショット F を指しています。別のブランチ old は、以前のスナップショットを、例えば開発の修正ポイント候補としてマークします。feature ブランチには、特定の機能に対する別の変更が含まれています。変更セットは、あるバージョンから別のバージョンへの変更として、例えば「[B->D]」のように記されています。この例では、スナップショット B に 2 つの子があります。つまり、スナップショット B から feature ブランチに対する開発ストリームと、その他のブランチに対する開発ストリームの 2 つが始まっています。コミット A にも、バグ・フィックス番号 123 としてタグが付けられています。

git branch <branchname> - 現在の HEAD (作業ディレクトリー) から新しいブランチを作成する

- 現在の HEAD (作業ディレクトリー) から新しいブランチを作成する git checkout -b <branchname> - 現在の HEAD (作業ディレクトリー) から新しいブランチを作成し、この新しく作成したブランチに作業ディレクトリーを切り替える

- 現在の HEAD (作業ディレクトリー) から新しいブランチを作成し、この新しく作成したブランチに作業ディレクトリーを切り替える git diff <branchname> -- <path> - 作業ディレクトリーと、指定したブランチとの間の <path> の差分を表示する

- 作業ディレクトリーと、指定したブランチとの間の <path> の差分を表示する git checkout <branchname> -- <path> - 指定したブランチから作業ディレクトリーにファイルをチェックアウトする

- 指定したブランチから作業ディレクトリーにファイルをチェックアウトする git merge <branchname> - 指定したブランチを現在のブランチにマージする

- 指定したブランチを現在のブランチにマージする git merge --abort - 競合する結果となるマージをアボートする

ブランチを作成するには、現在の HEAD に対して git branch <branch name> コマンドを実行するか、有効なスナップショット・バージョンに対して git branch <branch name> <commit id> コマンドを実行します。これにより、リポジトリー内に新しいブランチ・ポインターが作成されます。ただし注意する点として、このようにブランチを作成すると、ワークスペースが古いブランチに取り残されてしまいます。したがって、最初に新しいブランチをチェックアウトする必要があります。 git checkout -b <branch name> で新しいブランチを作成すると、ワークスペースも新しいブランチに移ります。

以下の 2 つのコマンドも有用です。

git diff <branch> -- <path> 。前述のとおり、カレント作業ディレクトリーと指定のブランチとの間の特定のパス (ファイルまたはディレクトリー) の差分を出力します。

。前述のとおり、カレント作業ディレクトリーと指定のブランチとの間の特定のパス (ファイルまたはディレクトリー) の差分を出力します。 git checkout <branch> -- <path> 。異なるブランチから作業ディレクトリーにファイルをチェックアウトして、別のブランチから変更を適用できるようにします。

マージ

例えば feature ブランチで新しい機能を実装し、それをリポジトリーにチェックインしたとします。その場合、機能が完成した時点で、その機能を master ブランチにマージする必要があります。それには、master ブランチをチェックアウトして、 git merge git merge <branch name> を実行します。すると Git は、指定されたブランチからチェックアウトされたブランチに変更をマージします。これを実現するために Git が行っていることは、すべての変更セットを feature ブランチから master ブランチの頂点に適用することです。

2 つのブランチで行われた変更のタイプ (場合によってはマージでの競合) によって、3 つのシナリオが考えられます。

早送りマージ: 2 つのブランチに分岐したため、受け取り側のブランチは変更を 1 つも受け取りませんでした。受け取り側のブランチは引き続き、もう一方のブランチに分岐する前の最後のコミットを指しています。この場合、Git は受け取り側ブランチのブランチ・ポインターを図 5 に示すように先へ移動します。ブランチ・ポインターを先へ移動する以外には何も行わないため、Git ではこれを早送り (fast forward) マージと呼んでいます。

図 5. 早送りマージ

競合しないマージ: 両方のブランチに変更が含まれる一方、これらの変更が競合しない場合があります。例えば、それぞれのブランチに含まれる変更が異なるファイルを対象にしている場合などです。この場合、Git は自動的に一方のブランチから受け取り側ブランチにすべての変更を適用し、これらの変更を取り込んだ新しいコミットを作成します。受け取り側ブランチは、新しく作成されたコミットを指すように先へと移動されます (図 6 を参照)。

最終的なコミット (マージ・コミット) は、2 つの親を持つことに注意してください。ただし、ここには変更セットを示していません。原則として、(E) から (H) に送られる変更セットは、2 つのブランチに分岐した後のすべての変更セットを feature ブランチから結合したものになりますが、それを図に示すとなると、おそらくとてつもなく長くなってしまうでしょう。

図 6. 競合しないマージ

競合するマージ: 両方のブランチに変更が含まれていて、これらの変更が競合する場合があります。この場合、競合する変更は作業ディレクトリーに残され、ユーザーが変更を修正してコミットします。あるいは、 git merge –abort によってマージをアボートします。

触れておくべき興味深い点として、マージしたときに、両方のブランチで同じパッチが適用されたインスタンスが見つかる場合があります。両方のブランチに変更が含まれていることから、通常は競合が発生しますが、Git はその状況を検出するだけのインテリジェンスを持ち合わせているため、変更が競合するとしても早送りマージが可能になります。

変更セットのロールバックと再現の概念には、リベースやチェリー・ピックなどのさらに高度な機能を伴います。

場合によっては、ある機能を開発している傍らでマスター開発も並行して進んでいることもあります。その場合、まだその段階ではないという理由で開発中の機能をマージしなければ、2 つのブランチがたちまち互いに離れていくことになります。しかし、一方のブランチからもう一方のブランチへ変更セットを適用することは可能です。Git には、そのためのリベースおよびチェリー・ピックの機能が用意されています。

リベース

例えば、ある機能を開発しているときに、開発全体の最新の状態を反映させるために、master ブランチから最新の変更を取り込む必要があるとします。このことを、feature ブランチを「リベース」すると表現します。つまり、2 つのブランチの間の分岐点を一方のブランチの上へと移動することです。リベースが行われると、Git は一方のブランチに含まれる変更をもう一方のブランチの頂点の上で再現し、元のコミットのそれぞれに対して新しいコミットを作成します。図 7 に示す例では、Git が feature ブランチに含まれる変更を、master ブランチの最上部に適用しようとしています。

図 7. ブランチのリベース

git rebase <otherbranch> - 現在のブランチを他の指定したブランチの頂点にリベースする

- 現在のブランチを他の指定したブランチの頂点にリベースする git rebase -i <otherbranch> - 相互にリベースする

- 相互にリベースする git cherry-pick <commit> - 特定のコミットの変更セットを (クリーンな) 作業ディレクトリーに適用する

- 特定のコミットの変更セットを (クリーンな) 作業ディレクトリーに適用する git cherry-pick --abort - 競合する結果となるチェリー・ピックをアボートする

- 競合する結果となるチェリー・ピックをアボートする git revert - パッチを元に戻す

再現すると競合が発生する場合、リベースは最初の競合が発生した時点で停止し、ユーザーが修正できるように競合状態を作業ディレクトリーに残します。その場合、ユーザーはリベースを続行することも、アボートすることもできます。

--onto オプションを指定したリベースでは、分岐ポイントを一方のブランチの最新スナップショットの「上」へと実際に移動することができます。

チェリー・ピック

あるシナリオとして、特定の機能の開発中に、マスター開発に直ちに統合させなければならない変更を開発したとします。その変更はバグ・フィックスであったり、素晴らしい機能であったりしますが、ブランチをマージまたはリベースするのは時期尚早であると判断した場合、Git では一方のブランチに含まれる変更セットをもう一方のブランチにコピーするという選択肢があります。そのために使用するのが、チェリー・ピック機能です。

このような状況では、Git は図 8 に示されているように、選択されたスナップショットに至る変更セットを HEAD (例えば、master ブランチ) で適用するだけに過ぎません。ここでは通常、実際にコミット ID (ハッシュ値とも呼ばれます) を使用します。

図 8. コミットのチェリー・ピック

Revert

revert コマンドは、作業ディレクトリーで 1 つ以上のパッチ・セットをロールバックしてから、その結果に基づいて新しいコミットを作成します。 revert は、ほとんどチェリー・ピックの反対です。図 9 に一例を示します。

図 9. コミットの打ち消し

revert コマンドは、打ち消し操作を新しいコミットとして記録します。この操作を記録させたくない場合は、ブランチ・ポインターを前のコミットに再設定することもできますが、この記事ではその方法については取り上げません。

このセクションで、これほどにまで詳細に取り上げた理由は、次のセクションでコラボレーション機能についての説明を読み進める上では、このセクションで取り上げた機能について理解していることが不可欠だからです。実のところ、この最初のセクションを理解してしまえば、次のセクションの内容はほとんどすぐに明らかになります。コラボレーション機能の大半は、これまに説明した基本機能に基づいているからです。

コラボレーション

従来型のソース・コード・リポジトリーでは、ブランチとは何であるかについての明白な概念が必ずあります。それは、ブランチは中央リポジトリー上にあるということです。

一方、Git には master ブランチといったものは存在しません。先ほどは、一般に master ブランチがあると書きましたが、Git での master ブランチの存在はローカルに制限されます。ユーザーが作成する関係を除けば、あるリポジトリー内の master ブランチと別のリポジトリー内の master ブランチとの間には何の関係もありません。

図 10. 2 つのリポジトリー

すでにリポジトリーを使用している場合、 git remote add コマンドを使用するとリモート・リポジトリーを追加することができます。リモート・リポジトリーを追加した後は、 fetch コマンドを使って、独自のリポジトリーにリモート・ブランチのミラーを取り込むことができます。このブランチはリモート・システム上で開発を追跡することから、リモート・トラッキング・ブランチと呼ばれます。

(ローカル・ブランチとしてではなく) リモート・トラッキング・ブランチとしてだけ存在するブランチをチェックアウトすると、Git は自動的にそのリモート・トラッキング・ブランチからローカル・ブランチを作成して、そのブランチをチェックアウトします。

このようにリモート・ブランチがチェックアウトされた後は、そのコンテンツを独自のブランチにマージすることができます。図 11 に、リモート・ブランチからローカル master ブランチへのチェックアウトを示します。必ずしもそうする必要はありませんが、リモート・ブランチは他のどのブランチにも、通常のマージ・コマンドを使って共通履歴と一緒にマージすることができます。

図 11. リモート・ブランチのフェッチとチェックアウト

別の方法として、例えばホスティング・サービスからリモート・リポジトリーを取得する git clone コマンドを使用することもできます。このコマンドにより、自動的にすべてのリモート・ブランチが取得され (ただし、まだローカル参照ではありません)、master ブランチがチェックアウトされます。

ご覧のとおり、あるパターンが見えてきます。リモート・リポジトリーのブランチは「単なるブランチ」に過ぎないため、前のセクションでのブランチ、マージ、等々についての説明は (リモート・リポジトリーから変更を取得する場合は尚のこと) すべてリモート・ブランチにも当てはまります

図 12. リモートからの変更のフェッチ

git clone <clone-url> - リモート・リポジトリーのクローンを作成する

- リモート・リポジトリーのクローンを作成する git remote add <origin> <url> - 指定した接続 URL を持つ <origin> という名前のリモート・リポジトリーを追加する

- 指定した接続 URL を持つ <origin> という名前のリモート・リポジトリーを追加する git fetch <origin> <branch> - リモート・トラッキング・ブランチに対する変更をリモート・リポジトリー <origin> から <branch> にフェッチする

- リモート・トラッキング・ブランチに対する変更をリモート・リポジトリー <origin> から <branch> にフェッチする git pull <origin> <branch> - フェッチした後、マージする

- フェッチした後、マージする git push <origin> <branch> - ローカル・ブランチからリモート・トラッキング・ブランチを介してリモート・リポジトリーに変更をプッシュする

図 12 に示されている git fetch により、リモート・トラッキング・ブランチが更新されます。更新後のリモート・トラッキング・ブランチとローカル・ブランチの間では、通常のマージ操作を行います (この例では、 git checkout master; git merge repo1/master )。フェッチ操作の後は、フェッチされたリモート・ブランチへのショートカットとしてマージ・コマンド内で FETCH_HEAD 名を使用することができます (例えば、 git merge FETCH_HEAD )。また、前述の説明と同様に、このマージによる結果は、早送りマージ、競合しないマージ、または手作業で解決する必要がある競合のいずれかとなります。

git pull コマンドは、 fetch に merge を組み合わせた便利なコマンドです。

変更をローカル・ブランチにコミットした後は、それらの変更をリモート・ブランチに転送する必要があります。それには、 push コマンドを使用して、ローカルの変更をリモート・ブランチにプッシュします。push は pull の反対の操作ではなく、 fetch の反対の操作ですが、単なるフェッチの反対の操作以上のことを行い、リモート・ブランチのローカル・コピーを更新するだけでなく、もう一方のリポジトリーのリモート・ブランチも更新します (図 13 を参照)。また、 push を使用して、リモート・リポジトリーに新しいブランチを作成することもできます。

図 13. 変更のプッシュ

プッシュには 1 つの安全対策があります。プッシュが成功するのは、操作の結果がリモート・リポジトリーでのリモート・ブランチの早送りマージとなった場合のみです。早送りマージとならなければ、操作はアボートされます。早送りマージが行われない場合、その時点ではリモート・ブランチにはすでに他のリポジトリーやコミッターからの変更 (コミット) もあります。Git はプッシュをアボートしてリモート・ブランチのコンテンツをすべてそのまま残すので、変更をフェッチしてローカル・ブランチにマージしてから、プッシュを再試行する必要があります。

通常のマージを行えるとしたら、ローカル・ブランチの変更を、リベース・マージによって新しく更新されたリモート・ブランチの HEAD にリベースするという選択肢もあることに注意してください。

fetch および push コマンドの他に、パッチを配信するもう 1 つの方法があります。それは、昔ながらのメールを使用するという方法です。メールでパッチを配信するには、まず、 git format-patch <start-name> というコマンドで、特定のコミットからブランチを現在の状態にしたコミットごとにパッチ・ファイルを作成します。そして git am <mail files> で、これらのパッチ・ファイルを現在のブランチに適用します。

注意事項

注意事項が 1 つあります。プッシュする対象のリポジトリーに、誰かが実際に追跡しているブランチがあり、ローカルでそのブランチで作業しているとします。そのリポジトリーに変更をプッシュすると、おそらくブランチの管理が目茶苦茶になってしまいます。したがって、Git はその旨を警告し、最初に pull を実行してリモート・ブランチの状態を同期させるように指示します。

また、リモート・トラッキング・ブランチをリベースすべきでないことも明らかです。リベースしたとしても、リモート・ブランチとは一致しないため、 push で早送りマージの結果は得られません。つまり、リポジトリー構造を壊してしまうことになります。

高度な Git

図 14. マルチリポジトリー構造の例

通常は Git でも、メイン・リポジトリーとして機能する1 つの中央リポジトリーと、ユーザーごとのローカル・リポジトリーからなるスター型の構成になりますが、必ずしもスター型にしなければならないというわけではありません。Web の場合と同じように、相互接続されたリモート・リポジトリーを追加することができます。その一例が、図 14 に示されています。

前のセクションでは、リベースについて、元のブランチとは異なる分岐ポイント上に変更セットを再現することだと説明しました。Git は通常、コミットが行われた順番で再現を行います。拡張機能として git rebase -i を使用すると、どのコミットをどの順番で行うか、そしてコミットを削除できるかどうか、あるいは 2 つのコミットを結合できる (「押し込める」) かどうかといったことさえ、実際に選択することができます。ただし、このコマンドは、すでにプッシュされたコミットに対しては使用しないように注意してください。そうでないと、プッシュ済みのコミットに基づく成果物で、さまざまな競合が発生する可能性があります。

特定のブランチをチェックアウトする方法についても説明しましたが、任意のコミット (スナップショット) をチェックアウトすることも可能です。その場合、HEAD ポインターで (ブランチではなく) コミットを指すことができます。これは、「detached HEAD (分離 HEAD)」モードと呼ばれます。この状況で変更をコミットすると、新しい開発ストリームが開始されます。基本的にブランチを作成しても、この新しいブランチにブランチ名は指定されません。開発の頂点には、コミット ID を使用することによってしか到達することができません。refname では、到達不可能です。ただし、通常の git branch <branchname> コマンドを使用して、この HEAD からのブランチを作成することはできます。

参照によって到達できないコミットはどうなるかと言うと、何か特殊な操作を行わなければ、それらのコミットはリポジトリーに保持されます。しかし、ユーザーやホスティング・サービスが実際に git gc を実行することで、Git ガーベッジ・コレクターに不要なファイルを削除させることができます。refname で到達できないコミットは不要なものであるため、このコマンドによって削除されます。Git では新しいブランチを迅速かつ容易に作成できることを考えると、常に実際のブランチで作業するのが望ましいプラクティスです。

まとめ

Git は単純な原理に基づいている一方で、Git が提供する柔軟性には折に触れて圧倒されることがあります。なかでも Git が最も優れている点は、Git はスナップショットとスナップショット間の変更セットを管理することです。最も一般的なコマンドで、これらの変更セットを異なるブランチの間で適用したり、ロールバックしたりします。その次に優れている点は、リモート・ブランチを扱うことは、ローカル・ブランチを扱うことと基本的に同じであるということです。これは、リモート・ブランチにはローカル・ミラーもあるためです。

これで、Git の機能を急ぎ足で一巡りしたことになります。この記事で取り上げたコマンドは基本的に、私が Git で行うすべての操作をカバーしています。各コマンドの詳細は、それぞれの man ページで説明されています。この記事で紹介した情報から、これらのコマンドをさらによく理解して使用できるようになることを願っています。また、コマンド自体、そして git status が、次に行う操作に関する貴重なヒントなることもよくあります。

Git を理解するのに役立つ優れたツールには、グラフィカル gitk もあります。この優れたツールは、ローカル・リポジトリーの構造を表示します。例えば、 gitk --all を実行すると、ブランチ、タグ、その他すべての構成要素が表示されます。このツールは、Git 上でアクションを開始する単純なインターフェースにもなります。

通常、Linux システムには Git がすでにインストールされていますが、開発ツールはパッケージ・マネージャーからインストールしなければならない場合があります。Windows の場合は、Git ホーム・ページから Git をダウンロードすることができます。.

この記事を読んで、皆さんが Git の仕組みについての理解を深め、これからは恐れることなく Git の柔軟性を利用するようになることを願っています。

この記事で取り上げたトピックについての興味深いディスカッションと、記事のレビューに携わっていただいた、私よりも Git をよく理解している同僚の Witold Szczeponik 氏に感謝いたします。

ダウンロード可能なリソース

関連トピック