lufficc

lufficc

Stay Hungry. Stay Foolish.

Gitの核心概念

文章内容来自自己的理解 和 https://git-scm.com/book/en/v2

本文不是 Git 使用教学篇,而是偏向理论方面,旨在更加深刻的理解 Git,这样才能更好的使用它,让工具成为我们得力的助手。

バージョン管理システム#

Git は現在世界で最も優れた分散型バージョン管理システムです。バージョン管理システムは、時間の経過とともに一連のファイルの変更を記録し、後で特定のバージョンに戻りたい場合に便利なシステムです。バージョン管理システムは大きく分けて 3 つのカテゴリに分かれます:ローカルバージョン管理システム、集中型バージョン管理システム、分散型バージョン管理システム。

ローカルバージョン管理(Local Version Control Systems)は、ファイルの各バージョンを特定のデータ形式でローカルディスクに保存します(いくつかの VCS はファイルの変更パッチを保存し、ファイル内容が変更されたときに差分を計算して保存します)。この方法は、ある程度手動でのコピー&ペーストの問題を解決しますが、複数人での協力の問題を解決することはできません。

ローカルバージョン管理{.to-figure}

集中型バージョン管理(Centralized Version Control Systems)は、ローカルバージョン管理に本質的な変化はありません。ただ中央サーバーが追加され、各バージョンのデータベースが中央サーバーに保存され、管理者が開発者の権限を制御でき、開発者は中央サーバーからデータを取得できます。集中型バージョン管理はチーム協力の問題を解決しましたが、欠点も明らかです:すべてのデータが中央サーバーに保存されているため、サーバーがダウンしたりディスクが損傷した場合、計り知れない損失が発生します。

集中型バージョン管理{.to-figure}

分散型バージョン管理(Distributed Version Control System)は前の 2 つとは異なります。まず、分散型バージョン管理システムでは、Git、Mercurial、Bazaar、Darcs など、システムが保存するのはファイルの変更の差分ではなく、ファイルのスナップショット、つまりファイル全体をコピーして保存し、具体的な変更内容には関心を持ちません。次に、最も重要なのは、分散型バージョン管理システムは分散型であり、中央サーバーからコードをコピーするとき、コピーするのは完全なリポジトリであり、履歴記録、コミット記録などが含まれています。このため、特定のマシンがダウンしてもファイルの完全なバックアップを見つけることができます。

分散型バージョン管理{.to-figure}

Git の基本#

Git は分散型バージョン管理システムであり、保存されるのはファイルの完全なスナップショットであり、差分やファイルパッチではありません。

保存される各変更ファイルの完全な内容{.to-figure}

Git の各コミットはプロジェクトファイルの完全なコピーであるため、以前の任意のコミットに完全に復元することができ、何の違いも生じません。ここで一つの問題があります:もし私のプロジェクトのサイズが 10M であれば、Git が占有するスペースはコミット回数の増加に伴って線形に増加するのでしょうか?私が 10 回コミットした場合、占有スペースは 100M になるのでしょうか?明らかにそうではありません。Git は非常に賢く、ファイルが変更されていない場合、前のバージョンのファイルへのポインタだけを保存します。つまり、特定のバージョンのファイルに対して、Git は一つのコピーだけを保存しますが、そのファイルへの複数のポインタを持つことができます。

さらに注意が必要なのは、Git はテキストファイルを保存するのに最適であり、実際に Git はテキストファイルを保存するために設計されています。さまざまな言語のソースコードのように、Git はテキストファイルに対して非常に良い圧縮と差分分析を行うことができます(皆さんも見たことがあるように、Git の差分分析は特定の文字を追加または削除したかどうかを正確に示すことができます)。一方、動画や画像などのバイナリファイルも Git で管理できますが、あまり良い結果は得られません(圧縮率が低く、差分分析ができません)。実験によると、500k のテキストファイルは Git で圧縮後に約 50k になり、内容を少し変更した後の 2 回のコミットでは、約 50k のファイルが 2 つ生成されます。間違いなく、保存されるのは完全なスナップショットです。一方、バイナリファイル、例えば動画や画像は、圧縮率が非常に小さく、Git が占有するスペースはコミット回数に応じてほぼ線形に増加します。

変更されていないファイルは前のバージョンのポインタだけを保存{.to-figure}

Git プロジェクトには 3 つの作業領域があります:作業ディレクトリ、ステージングエリア、およびローカルリポジトリです。作業ディレクトリは現在作業を行っている領域です;ステージングエリアはgit addコマンドを実行した後にファイルが保存される領域であり、次回のコミットで保存されるファイルです(注意:Git のコミットは実際にはステージングエリアの内容を読み取り、作業領域のファイルとは無関係です。これがファイルを変更した後、git addをステージングエリアに追加しないとバージョンライブラリに保存されない理由です);ローカルリポジトリはバージョンライブラリであり、プロジェクトの特定のコミットの完全な状態と内容を記録しており、これはあなたのデータが永遠に失われないことを意味します。
file
それに応じて、ファイルにも 3 つの状態があります:コミット済み(committed)、変更済み(modified)、およびステージング済み(staged)。コミット済みは、そのファイルがローカルバージョンライブラリに安全に保存されていることを示します;変更済みは、特定のファイルが変更されたが、まだ保存されていないことを示します;ステージング済みは、変更されたファイルが次回のコミットで保存されるリストに追加されたことを示します。つまり、Git を使用する基本的な作業フローは次のようになります:

  1. 作業領域でファイルを追加、削除、または変更します。
  2. git addを実行して、ファイルのスナップショットをステージングエリアに保存します。
  3. 更新をコミットして、ファイルの永続的なバージョンをバージョンライブラリに保存します。
    file

Git オブジェクト#

今や Git の基本的なフローは理解できましたが、Git はどのようにそれを実現しているのでしょうか?Git はファイルの変更をどのように区別するのでしょうか?以下に Git の基本原理を簡単に紹介します。

SHA-1 チェックサム#

Git はコンテンツアドレッシングファイルシステムです。つまり、Git は本質的に単純にキーと値のペア(key-value)を保存しているだけで、valueはファイルの内容であり、keyはファイル内容とファイルヘッダー情報の 40 文字の SHA-1 チェックサムです。例えば:5453545dccd33565a585ffe5f53fda3e067b84d8。Git はこのチェックサムを暗号化のためではなく、データの完全性を保証するために使用します。これにより、数年後に特定のコミットをチェックアウトすると、その時の状態が完全に同じであることが保証されます。ファイルにわずかでも変更を加えると、全く異なる SHA-1 チェックサムが計算されます。この現象は「雪崩効果」(Avalanche effect)と呼ばれます。

SHA-1 チェックサムは、前述のファイルのポインタです。これは C 言語のポインタとは少し異なります:C 言語はメモリ内のデータのアドレスをポインタとして使用しますが、Git はファイルの SHA-1 チェックサムをポインタとして使用します。目的は異なるオブジェクトを一意に区別することです。しかし、C 言語のポインタが指すメモリ内の内容が変更されてもポインタは変わりませんが、Git のポインタが指すファイルの内容が変更されると、ポインタも変わります。したがって、Git の各バージョンのファイルには、一意のポインタが指し示されています。

ファイル(blob)オブジェクト、ツリー(tree)オブジェクト、コミット(commit)オブジェクト#

blobオブジェクトはファイルの内容だけを保存し、treeオブジェクトはオペレーティングシステムのディレクトリのように、blobオブジェクトやtreeオブジェクトを保存できます。単独のtreeオブジェクトは 1 つ以上のtreeレコードを含み、それぞれのレコードにはblobオブジェクトまたは子treeオブジェクトへの SHA-1 ポインタが含まれ、そのオブジェクトの権限モード(mode)、タイプ、ファイル名情報などが付随します:
file
ファイルを変更してコミットすると、変更されたファイルは新しいblobオブジェクトを生成し、ファイルの完全な内容(すべての内容、変更内容ではない)を記録し、そのファイルには一意の SHA-1 チェックサムが付与され、今回のコミットでそのファイルのポインタをその SHA-1 チェックサムに変更します。一方、変更されていないファイルは、前のバージョンのポインタ、すなわち SHA-1 チェックサムを単純にコピーするだけで、新しいblobオブジェクトは生成されません。これが、10M のプロジェクトが 10 回コミットしても、総サイズが 100M をはるかに下回る理由です。

さらに、各コミットには必ず 1 つ以上のtreeオブジェクトが含まれている可能性があり、これらはプロジェクトの異なるスナップショットを示しますが、すべてのオブジェクトの SHA-1 チェックサムを記憶していなければ、完全なスナップショットを取得することはできません。また、誰が、いつ、なぜこれらのスナップショットを保存したのかという理由もありません。commitオブジェクトはこれらの問題を解決するために生まれたもので、commitオブジェクトの形式は非常にシンプルです:その時点のプロジェクトスナップショットのトップレベルのtreeオブジェクト、作者 / コミッター情報(Git で設定された user.name と user.email から取得)、現在のタイムスタンプ、空行、前回のコミットオブジェクトの ID、およびコミットメッセージが含まれます。次のコマンドを実行することで、この新しい情報を簡単に取得できます:

$ git log
commit 2cb0bb475c34a48957d18f67d0623e3304a26489
Author: lufficc <luffy.lcc@gmail.com>
Date:   Sun Oct 2 17:29:30 2016 +0800

    fix some font size

commit f0c8b4b31735b5e5e96e456f9b0c8d5fc7a3e68a
Author: lufficc <luffy.lcc@gmail.com>
Date:   Sat Oct 1 02:55:48 2016 +0800

    fix post show css

***********省略***********

file
上の図の Test.txt は最初のコミットの前に生成され、最初の SHA-1 チェックサムは3c4e9cで始まります。その後、変更が加えられたため、2 回目のコミット時に新しいblobオブジェクトが生成され、チェックサムは1f7a7aで始まります。3 回目のコミット時には Test.txt は変更されていないため、単に最新バージョンの SHA-1 チェックサムを保存し、新しいblobオブジェクトは生成されません。プロジェクト開発の過程で新たに追加されたファイルは、コミット後に必ず新しいblobオブジェクトを生成して保存されます。注意:最初のコミットを除いて、各コミットオブジェクトには前回のコミットオブジェクトへのポインタがあります

したがって、簡単に言えば、blobオブジェクトはファイルの内容を保存し、treeオブジェクトはフォルダのようにblobオブジェクトや他のtreeオブジェクトを保存し、commitオブジェクトはtreeオブジェクト、コミット情報、作者、メールアドレス、および前回のコミットオブジェクトの ID(最初のコミットにはありません)を保存します。そして Git はこれらのオブジェクトの状態と複雑な関係を組織し管理することで、バージョン管理や他の機能(ブランチなど)を実現しています。

Git の参照#

今、参照を見てみると、非常に簡単です。特定のコミット記録の前の完全な履歴を見たい場合、そのコミット ID を記憶する必要がありますが、コミット ID は 40 桁の SHA-1 チェックサムであり、覚えにくいです。したがって、参照は SHA-1 チェックサムの別名であり、.git/refsフォルダに保存されます。

最も一般的な参照はおそらくmasterです。これは Git がデフォルトで作成するもので(変更可能ですが、一般的には変更しません)、常にプロジェクトのメインブランチの最後のコミット記録を指します。プロジェクトのルートディレクトリでcat .git/refs/headsを実行すると、SHA-1 チェックサムが出力されます。例えば:

$ cat .git/refs/heads/master
4f3e6a6f8c62bde818b4b3d12c8cf3af45d6dc00

したがって、masterは単なる 40 桁の SHA-1 チェックサムの別名に過ぎません。

もう一つの問題は、Git はどのようにして現在のブランチの最後のコミット ID を知るのでしょうか?.gitフォルダの中にHEADファイルがあり、次のようになっています:

$ cat .git/HEAD
ref: refs/heads/master

HEADファイルには実際には SHA-1 値は含まれておらず、現在のブランチへの参照を指しています。内容はブランチを切り替えると変わります。内容の形式は次のようになります:ref: refs/heads/<branch-name>git commitコマンドを実行すると、commitオブジェクトが作成され、このcommitオブジェクトの親はHEADが指す参照の SHA-1 値に設定されます。

次に、Git のタグについて話しましょう。タグはある意味で参照のようなもので、commitオブジェクトを指し、treeではなく、タグ、データのセット、メッセージ、およびcommitオブジェクトのポインタを含みます。しかし、違いは、参照はプロジェクトが進むにつれてその値が前に進むのに対し、タグは変わらないことです —— 常に同じcommitを指し、よりフレンドリーな名前を提供するだけです。

Git ブランチ#

ブランチ#

ブランチは Git のキラー機能であり、Git は作業フローの中でブランチとマージを頻繁に使用することを奨励しています。たとえ 1 日のうちに何度も行っても問題ありません。なぜなら、Git のブランチは非常に軽量であり、他のバージョン管理システムとは異なり、ブランチを作成することはプロジェクトの完全なコピーを作成することを意味しません。Git のブランチ作成は瞬時に完了し、プロジェクトの複雑さには関係ありません。

前述のように、Git がファイルを保存する最も基本的なオブジェクトはblobオブジェクトです。Git は本質的に巨大なファイルツリーであり、ツリーの各ノードはblobオブジェクトです。ブランチはツリーの一つの分岐に過ぎません。言い換えれば、ブランチは名前付きの参照であり、コミットオブジェクトの 40 桁のチェックサムを含んでいます。したがって、ブランチを作成することは、ファイルに 41 バイト(改行文字を追加)を書くことと同じくらい簡単です。したがって、自然に速く、プロジェクトの複雑さには関係ありません。

Git のデフォルトのブランチは master で、.git\refs\heads\masterファイルに保存されています。仮にあなたが master ブランチでgit branch devを実行してdevという名前のブランチを作成した場合、Git が実際に行う操作は次のとおりです:

  1. .git\refs\headsフォルダにdevという名前の(拡張子なしの)テキストファイルを新しく作成します。
  2. HEADが指す現在のブランチ(現在はmaster)の 40 桁の SHA-1 チェックサムをdevファイルに書き込みます。
  3. 終了。

file

ブランチを作成するのはこれほど簡単です。では、ブランチを切り替えるのはどうでしょうか?さらに簡単です:

  1. .gitフォルダ内のHEADファイルをref: refs/heads/<ブランチ名>に変更します。
  2. ブランチが指すコミット記録に従って、作業ディレクトリのファイルを完全に復元します。
  3. 終了。

覚えておいてください、HEADファイルは現在のブランチの最後のコミットを指し示しており、また、現在のブランチから新たにブランチを作成する際に書き込まれる内容でもあります。

ブランチのマージ#

次にマージについて話しましょう。まずはファストフォワード(Fast-forward)です。言い換えれば、あるブランチを進めていくと別のブランチに到達できる場合、Git は両者をマージする際に単にポインタを右に移動させるだけです。このような単一の歴史のブランチには解決すべき分岐が存在しないため、このマージプロセスはファストフォワードと呼ばれます。例えば:
file
矢印の方向に注意してください。各コミットには前のコミットを指すポインタがあるため、矢印の方向は左向きで、より合理的です

masterブランチでdevブランチをマージする際、両者が同じラインにあるため、この単一の歴史のブランチには解決すべき分岐が存在しないため、masterブランチがdevブランチを指すだけで済みます。したがって非常に速いです。

ブランチが分岐すると、競合が発生する可能性があります。この場合、Git はあなたに競合を解決するよう要求します。例えば、以下のような歴史があります:
file
masterブランチとdevブランチが同じラインにないため、v7v5の直接の祖先ではなく、Git は追加の処理を行わざるを得ません。この例では、Git は 2 つのブランチの末端(v7v5)およびそれらの共通の祖先(v3)を使用して、単純な三者マージ計算を行います。マージ後、マージコミットv8が生成されます:
file
注意:マージコミットには 2 つの祖先(v7v5)があります。

ブランチのリベースrebase#

1 つのブランチの変更を別のブランチに統合する方法には 2 つあります:mergerebaseです。まず、mergerebaseの最終的な結果は同じですが、rebaseはより整理されたコミット履歴を生成します。上の図を例にとると、単純なmergeを行うと、コミットオブジェクトv8が生成されます。今、リベースマージを試みて、devに切り替えます:

$ git checkout dev
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

file

このコードの意味は:2 つのブランチの最近の共通の祖先v3に戻り、現在のブランチ(つまりリベースを行うブランチdev)の後続の各コミットオブジェクト(v4v5を含む)に基づいて一連のファイルパッチを生成し、基底ブランチ(つまりメインブランチmaster)の最後のコミットオブジェクト(v7)を新しい出発点として、準備したパッチファイルを 1 つずつ適用します。最終的に 2 つの新しいマージコミットオブジェクト(v4'v5')が生成され、devのコミット履歴が書き換えられ、masterブランチの直接の下流となります。次の図のようになります:
file
今、masterブランチに戻ってファストフォワードマージを行うことができます。なぜなら、masterブランチとdevブランチが同じラインにあるからです:

$ git checkout master
$ git merge dev

file
現在のv5'に対応するスナップショットは、実際には通常の三者マージ、すなわち前の例のv8に対応するスナップショットの内容と全く同じです。最終的に統合された結果には何の違いもありませんが、リベースはより整理されたコミット履歴を生成します。リベースされたブランチの履歴を調べると、すべての変更が 1 本のラインで順次行われたように見えますが、実際にはそれらは同時に並行して発生していました。

まとめ#

  1. Git はファイルの完全な内容を保存し、差分の変更は保存しません。
  2. Git はキーと値のペア(key-value)の形式でファイルを保存します。
  3. 各ファイル、同じファイルの異なるバージョンには、一意の 40 桁の SHA-1 チェックサムが対応しています。
  4. SHA-1 チェックサムはファイルのポインタであり、Git はそれを使用してファイルを区別します。
  5. 各ファイルは Git のバージョンライブラリにblobオブジェクトを生成して保存されます。
  6. 変更のないファイルについては、Git は前のバージョンのポインタのみを保持します。
  7. Git は実際には複雑なファイルツリーを維持することでバージョン管理を実現しています。
  8. Git を使用する作業フローは基本的にファイルが 3 つの作業領域間で流動することです。
  9. チーム協力のためにブランチを大量に使用すべきです。
  10. ブランチはコミットオブジェクトへの参照に過ぎません。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。