バージョン管理 (Git)

バージョン管理システム(VSC)はソースコード(や、ファイルやフォルダーのセット)に加えられた変更を追跡するためのツールです。 名前が示している通りこれらのツールは変更履歴の保存に役立ち、さらに共同作業を円滑にしてくれます。 バージョン管理システムは一連のスナップショットによってフォルダーやその中身に加えられた変更を追跡します。 そこで、それぞれのスナップショットはファイル、フォルダ内の全ての記述を一番上のディレクトリに内包しています。 また、バージョン管理システムはそれぞれのスナップショットの作成者、スナップショットに関わるメッセージ、などのメタデータも保存してくれます。

なぜバージョン管理は便利なのでしょうか? あなたが一人で作業しているときでさえバージョン管理をしていると、プロジェクトの古いスナップショットを確認することができ、その変更が行われた理由の記録を残したり、複数のブランチで開発作業を並行して行ったり、といったことをはじめとしてさらに沢山のことができるようになります。 他の作業者とともに作業するときには、他の人が行った変更を確認するとともに同時に作業することによって生じるコンフリクト(衝突)を解決するために非常に貴重なツールです。

また、現代のバージョン管理システムは以下のような疑問にも簡単に(しばしば自動的に)答えを与えてくれます:

他のバージョン管理システムは存在しますが、Git はバージョン管理のデファクトスタンダード(事実上の標準)です。 この XKCD comic はGitの評判をよくとらえています。

xkcd 1597 「これはGitだよ。Git では共同作業の履歴を追跡することができるんだ。これを使って、美しい分散グラフ理論的木構造を利用したプロジェクトで作業しよう。」

「いいね、どうやって使うの?」

「わかんない。ただ覚えるんだよ。このシェルコマンドを打って、それを同期させるんだ。エラーが出たらどこか他のところにセーブして、エラーが出たプロジェクトを消去して、新しいコピーをダウンロードするんだ。」

Gitのインターフェースは漏りやすく抽象的なので、Gitを(そのインターフェース、つまりコマンドラインインターフェースを皮切りにして)全体から細部まで理解しようとすると、多くの混乱を招いてしまうことがあります。 一握りのコマンドを暗記してしまい、それらを魔法の呪文のようなものと捉え、何かうまくいかないことがあれば上のイラストのような手法に従ってみることは可能です。

確かにGitのインターフェースはあまり使いやすいものではありませんが、その根底にあるデザインとアイディアは美しいものです。 使いにくいインターフェースでは 暗記 をしなければならない一方で、美しいデザインは 理解 することができます。 なので、これからGitをまずはデータモデル(データ型)から、そして後からコマンドラインインターフェースを網羅することで、細部から全般に至るような説明をしていきます。 一度データモデルを理解すれば、基礎となるデータモデルをどのように操作するかという点においてコマンドをよく理解できるようになります。

Gitのデータモデル

バージョン管理のためにあなたが取ることができるアドホックな(特定の目的のための)手法は、たくさんあります。 Gitは、履歴の保有、ブランチの保持や共同作業の有効化などといったバージョン管理の全ての優れた機能を使用可能にするように考え抜かれたモデルになっています。

スナップショット

Gitはいくつかの最高レベルのディレクトリの中に、一連のスナップショットとしてファイルやフォルダーの蓄積の履歴をモデル化します。 Gitの専門用語では、ファイルは『ブロッブ(blob)』と呼ばれ、大量のバイト(byte)に過ぎません。 ディレクトリは『ツリー(tree)』と呼ばれ、名前をブロッブやツリーに対応づけます(そのためディレクトリが他のディレクトリを内包することができます)。 スナップショットは追跡される最高レベルのツリーです。 例えば、以下のようなツリーになることがあります。

<root(根)> (ツリー)
|
+- foo (ツリー)
|  |
|  + bar.txt (ブロッブ, 内容 = "hello world")
|
+- baz.txt (ブロッブ, 内容 = "git is wonderful")

最高レベルのツリーは二つの要素を含んでいます。 ひとつは『foo』というツリー(それ自体は『bar.txt』というブロッブを一つの要素として含んでいます)であり、もうひとつは『baz.txt』というブロッブです。

スナップショットに関連する、履歴のモデル化

バージョン管理システムはどのようにスナップショットを関連付けているのでしょうか? 一つの単純なモデルとしては、線形の履歴を持つものがあります。 履歴はスナップショットを時系列順に並べたリストになります。 しかし様々な理由で、Gitはこのような単純なモデルを使用していません。

Gitでは、履歴はスナップショットの有向非巡回グラフ(DAG)になっています。 この有向非巡回グラフは派手な数学用語のように聞こえるかもしれませんが、恐れることはありません。 この言葉が意味することは、Git内のそれぞれのスナップショットが『ペアレント(親)』のセット、つまり先行するスナップショットを参照しているということです。 これは(線形の履歴の場合のような)単一のペアレントではなくペアレントのセットです、なぜならスナップショットは複数のペアレントから受け継がれることがあるからです。 たとえば、二つの並行な開発のブランチを統合(マージ)した場合がこれにあたります。

Gitはこのようなスナップショットのことを『コミット(commit)』と呼びます。 コミット履歴を視覚化すると以下のようになることがあります。

o <-- o <-- o <-- o
            ^
             \
              --- o <-- o

上のアスキーアートでは、 oが個々のコミット(スナップショット)に対応しています。 矢印はそれぞれのコミットのペアレントを指しています(これは「前に来る」関係であり、「後に来る」関係ではありません)。 三個目のコミットの後、履歴ブランチが二つのブランチに分かれています。 これは例えば、それぞれから独立して並行に開発された二つの別々の機能に対応しています。 将来的にこれらのブランチはどちらもの機能が組み込まれた、新しいスナップショットへとマージ(融合)され、以下の図においてボールド体で示された新しく生成されたマージコミットとともに、新たな履歴を作ることがあります。


o <-- o <-- o <-- o <---- o
            ^            /
             \          v
              --- o <-- o

Gitの中のコミットは不変です。 これは誤りを修正することができないという意味ではありません。 しかし、コミット履歴への「編集」は実際にはまったく新しいコミットを作成しているだけであり、参照(以下を見てください)は新しいものを指し示すようにアップデートされます。

疑似コードとしてのデータモデル

疑似コードで書かれたGitのデータモデルを確認することは役に立つかもしれません。

// ファイルは大量のバイト(byte)です。
type blob = array<byte>

// ディレクトリは名前付きファイルとディレクトリを内包しています。
type tree = map<string, tree | blob>

// コミットはペアレント、メタデータ、最高レベルのツリーを保持しています。
type commit = struct {
    parent: array<commit>
    author: string
    message: string
    snapshot: tree
}

これはきれいで単純な履歴のモデルです。

オブジェクトと内容アドレシング(コンテンツのアドレス指定)

「オブジェクト」とはブロッブ、ツリーまたはコミットのことです。

type object = blob | tree | commit

Gitのデータの記憶装置では、全てのオブジェクトが SHA-1 hash で内容のアドレスを指定されています。

objects = map<string, object>

def store(object):
    id = sha1(object)
    objects[id] = object

def load(id):
    return objects[id]

ブロッブ、ツリーそしてコミットは次のように統一されています。 それらは全てオブジェクトです。 それらが他のオブジェクトを参照したときには、それらは実際にはオブジェクトをそれらのディスク上の表示に 内包 しているわけではなく、ハッシュ(hash)によって参照を行なっています。

例えば、 の例のディレクトリ構造のツリーは、(git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d を使用して視覚化すると)以下のようになります。

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo

ツリーそれ自体は、 baz.txt(ブロッブ)とfoo(ツリー)というコンテンツへのポインタを持っています。 git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85を使用してbaz.txtに対応するハッシュでアドレス指定されたコンテンツを見てみると、以下の文章が得られます。

git is wonderful

リファレンス

以上のように、全てのスナップショットはそれらのSHA-1ハッシュによって識別されます。 しかし、40文字の16進数文字は人間が記憶するには適していないため、不便です。

この問題に対するGitの解決策は『リファレンス(参照)』と呼ばれる、人間にも読み取り可能なSHA-1ハッシュの名称です。 リファレンスはコミットへのポインタになっています。 オブジェクトは不変ですが、そのオブジェクトとは異なりリファレンスは可変のものです。(新しいコミットを指し示すためにアップデートされることがあります。) 例えば、 マスター(master)というリファレンス はたいていメインの開発ブランチ内における最新のコミットを指し示しています。

references = map<string, string>

def update_reference(name, id):
    references[name] = id

def read_reference(name):
    return references[name]

def load_reference(name_or_id):
    if name_or_id in references:
        return load(references[name_or_id])
    else:
        return load(name_or_id)

このようにGitは『マスター』のような人間にも読み取り可能な名称を、長い16進数の文字列の代わりとして、履歴内にある特定のスナップショットを参照するために使用することができます。

一つの詳細は、私たちは新たなスナップショットを取る際にそのスナップショットが何と関連しているのか(コミットのペアレントの領域を設定方法)を知るために、「現在の作業場所」という概念が必要なことがよくあります。 Gitでは、「現在の作業場所」は『ヘッド(HEAD)』という特別なリファレンスで呼ばれます。

レポジトリ

ようやく、Git リポジトリ(repository) が何なのかを定義することができます。 リポジトリとはデータのオブジェクトリファレンスのことなのです。

ディスク上において、全てのGitの記憶装置はオブジェクトとリファレンスです。 つまり、Gitのデータモデルにあるのはそれだけです。 全てのgitコマンドはいくつかのコミット有向非巡回グラフ(DAG)への操作を、オブジェクトの追加やリファレンスの追加・更新によって図示します。

あなたがどんなコマンドを打ち込んでいるときでも、根底にあるグラフのデータ構造にそのコマンドがどのような操作を行なっているのかを考えるようにしましょう。 一方で、コミット有向非巡回グラフ(DAG)に対して特定の種類の変更、例えば『コミットされていない変更を破棄して、「マスター」リファレンスに5d83f9eというコミットを指し示させる』といったことを行おうとするならば、それを行うことができるコマンドがおそらくあるでしょう。 (例えばこの例の場合であれば、そのコマンドはgit checkout master; git reset --hard 5d83f9eというようになります。)

ステージングエリア

これはデータモデルと直行する、別のコンセプトですが、コミット作成インターフェースの一部です。

上記のようにスナップショットを実装する際に考えることができる一つの方法は、『スナップショットを作成する』というコマンドによって作業中のディレクトリの 現在の状態 に基づき、新しいスナップショットを作成するというものです。 いくつかのバージョン管理ツールはこのような手法を取っていますが、Gitは違います。 私たちはスナップショットを整理しておきたいと望みますが、現在の状態からスナップショットを作成することが常に理想的であるとは限りません。 例えば、次のような状況を想像してみてください。 あなたは二つ別々の機能を実装して、二つの別々のコミットを作成したいと考えます。 そしてその二つのコミットのうち一つには一つ目の機能を導入し、別のものには二つ目の機能を導入したいです。 または、次のような状況を想像してみましょう。 あなたは、あなたのコード全体にわたってバグ修正に伴って追加されたprintステートメントを、デバックします。 つまり、あなたは全てのprintステートメントを破棄しながら、バグ修正をコミットしたいのです。

Gitは『ステージングエリア(staging area)』という仕組みを通し、どの修正が次のスナップショットに含まれているべきなのかを細かく記述できるようにすることで、これらの状況にも対応しています。

Gitのコマンドラインインターフェース

同じ情報を繰り返し述べてしまうことを避けるために、以下のコマンドを細かく説明はしません。 さらに情報を得るためにPro Gitを確認することを強くお勧めします。 または講義ビデオを視聴してみてください。

基本的なもの

ブランチング(branching)とマージング(merging)

リモート

取り消し(Undo)

Git上級者向けコマンド

その他

追加教材、資料

演習問題

  1. もし過去にGitを一度も使ったことがなければ、 Pro Git の最初の2、3章を読むか、Learn Git Branchingのようなチュートリアルに取り組んでみましょう。 それらに取り組みながら、Gitのコマンドをデータモデルと関連づけましょう。
  2. 講義サイト用のレポジトリをクローン(ダウンロード)しましょう。
    1. グラフとしてバージョン履歴を可視化して、細かく見てみましょう。
    2. README.mdを最後に編集したのは誰でしたか?(ヒント:引数をつけて、git log を使用しましょう。)
    3. _config.ymlcollections:行に最後に行われた変更に付けられたコミットメッセージは何でしょう?(ヒント: git blamegit showを使ってみましょう。)
  3. Gitを学んでいるときによくある間違いは、Gitで管理すべきでないほど大きなファイルをコミットしてしまうことや、細心の注意を要するような情報をgit上に追加してしまうことです。レポジトリにファイルを追加する、いくつかのコミットをする、履歴からそのファイルを削除するという作業を試してみましょう。(これを見ると良いかもしれません。)
  4. GitHubからいくつかのレポジトリをクローン(ダウンロード)し、存在しているファイルのうち一つに変更を加えてみましょう。 git stashをすると何が起こりますか? git log --all --onelineを実行したときには何を確認することができますか?git stash pop を実行して git stashで行ったことを取り消し(undo)ましょう。どのような状況においてこの機能は便利でしょうか?
  5. 多くのコマンドラインツールのように、Gitは設定ファイル(またはドットファイル)を提供してくれます。その設定ファイルは ~/.gitconfigと呼ばれます。 git graphを実行したときにgit log --all --graph --decorate --onelineという出力を得るために、~/.gitconfigの中にエイリアスを作成しましょう。
  6. git config --global core.excludesfile ~/.gitignore_globalを実行した後には、 ~/.gitignore_globalの中にグローバルな無視パターンを定義することができます。これを実行してみて、OS固有またはエディタ固有の.DS_Storeといった一時ファイルを無視するための、あなたのグローバルgitignoreファイルを設定しましょう。
  7. 講義サイト用のレポジトリを分岐させ、打ち間違えや他にあなたが改善することができる箇所を見つけて、プルリクエストをGitHub上で提出しましょう。

このページを編集する

Licensed under CC BY-NC-SA.