はじめに
Gitは、分散型バージョン管理システムの一つであり、ソフトウェア開発において広く利用されています。Gitを使用することで、複数人で同じプロジェクトを管理する際に必要なバージョン管理やコードの共有が容易になります。
この記事では、コミットやブランチ、タグ、HEAD 等を介して、Git がどのように機能するのかについて説明していきます。
基本的な仕組み
BLOB やツリー、コミットは、Git のデータ構造の主要な構成要素であり、これらの要素が Git の基礎を構成しています。
これらの要素を理解するために、例を挙げて説明します。
まず、空のリポジトリを作成する場合を考えてみましょう。コマンド git init
を実行すると、Git は自動的に .git
という名前の隠しフォルダが作成されます。このフォルダは、Git の内部で使用されるデータを保存するために使用されます。
BLOB
Git では、git add
コマンドを使用して、リポジトリにファイルを追加します。
今回は、myfile.txt
という名前のファイルを作成し、コマンド git add myfile.txt
を使用して追加してみます。
### myfile.txt を作成
$ echo "hello" > myfile.txt
### Git リポジトリに myfile.txt を追加
$ git add myfile.txt
この操作を行うと、Git は .git/objects
サブフォルダに BLOB と呼ばれるファイルが作成されます。
この BLOB には myfile.txt
のコンテンツが保存されます。これには、作成タイムスタンプや作成者などの関連するメタデータを含まれていません。
### `git catfile` コマンドで BLOB ファイルの内容を表示
### (コンテンツ種別に応じた表示の最適化を行うため、`-p` オプションを付与)
$ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello
BLOB 名は、そのコンテンツのハッシュ値を基に命名されます。コンテンツがハッシュ化した後、最初の2文は .git/objects
にサブフォルダ名として、残りの文字列は BLOB 名として使用されます。
つまり、ファイルを Git に追加する場合、以下の手順が行われます。
- Git がファイルの内容を取得し、ハッシュ化する。
- Git は
.git/objects
フォルダ内に BLOB を作成する。- ハッシュ値の最初の2文字を使用し、サブフォルダを作成する。
- 作成したサブフォルダ配下で、ハッシュ値の残りの文字列の名前で BLOB を作成する。
- Git は、(圧縮された)元のファイルの内容を BLOB 内に保存する。
なお、myfile.txt
と ourfile.txt
という別のファイルがあり、2つとも同じコンテンツである際には、それらは同じハッシュ値となるため、同じ BLOB に格納されます。
### myfile.txt をコピーし、ourfile.txt を作成
$ cp -p myfile.txt ourfile.txt
### ourfile.txt を `git add` し、BLOB が新しく生成されていないことを確認
$ git add ourfile.txt
$ find .git/objects -type f
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
$
また、myfile.txt
の中身を少し修正して再度リポジトリに追加すると、Git によって前述したプロセスが再度実行されます。この際、ハッシュ値が変わるため、新しい BLOB が作成されることになります。
### myfile.txt の内容を少し変更し、再度 `git add` を実施
$ cat myfile.txt
hello!
$ git add myfile.txt
### BLOB ファイルが新しく生成されたことを確認
$ find .git/objects -type f
.git/objects/4e/ffa19f4f75f846c3229b9dbdbad14eff362f32
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
### それぞれの BLOB について中身を確認
$ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello
$ git cat-file -p 4effa19f4f75f846c3229b9dbdbad14eff362f32
hello!
ツリー
ここでは、mysubfolder
という名前のサブフォルダを作成し、その中に yourfile.txt
という名前のファイルを作成してリポジトリに追加していこうと思います。
### mysubfolder フォルダを作成
$ mkdir mysubfolder
### mysubfolder 配下に yourfile.txt を作成
$ echo "world" > mysubfolder/yourfile.txt
### Git リポジトリに mysubfolder/yourfile.txt を追加
$ git add mysubfolder/yourfile.txt
この操作により、Git は新たに yourfile.txt
の BLOB を作成します。
続けて、コマンド git commit
を使って myfile.txt
と yourfile.txt
の両方をコミットします。
### myfile.txt と mysubfolder/yourfile.txt をコミット
$ git commit -m"first commit"
[master (root-commit) c262f57] first commit
2 files changed, 2 insertions(+)
create mode 100644 myfile.txt
create mode 100644 mysubfolder/yourfile.txt
この操作では、Git は次の2つの手順を実行します。
- リポジトリのルートツリーを作成する。
- コミットを作成する。
ルートツリーとは、リポジトリ全体のファイルやフォルダの構造が記録される場所です。このツリーには、リポジトリに含まれるすべてのファイルやサブフォルダーへの参照が含まれ、再帰的な構造になっています。
ルートツリーの各行は、ファイルや他のサブツリーを参照しています。同様に、そのファイルやサブツリーも他のファイルやサブツリーを参照しています。これによって、ツリーはディレクトリと同じような構造を持ちます。つまりは、ディレクトリからファイルやサブフォルダにアクセスできるのと同じように、ツリーから BLOB やサブツリーにアクセスすることができます。
$ git cat-file -p 0fa5b90dcd339c2f52b492a5126ccb760478e
100644 blob cc628ccd10742baea8241c5924df992b5c019f71 yourfile.txt
$ git cat-file -p 9b35f55d56eb0c921c533ae31a6a0ed9c7859
100644 blob ce013625030ba8dba906f756967f9e9ca394464a myfile.txt
040000 tree 0fa5b90dcd339c2f52b492a5126ccb760478ecbd mysubfolder
Git がルートツリーやそれに関連する全てのサブツリーを作成したら、先程説明したように、それらをハッシュ化して保存します。
具体的には、各ツリーをハッシュ化し、最初の2文字を使用して .git/objects
にサブフォルダーを作成し、残りのハッシュ文字列を保存されたファイル名として使用します。これにより、ツリー内のデータ構造の数と同じ数だけの新しいファイルが作成されます。
コミット
コミットを実行すると、以下のメタデータ情報を含んだ形でファイルとして保存されます。
- ルートツリー (tree)
- 親コミット (parent commit)
- …存在する場合
- 作成者の名前および電子メール (author)
- コミッターの名前および電子メール (committer)
- コミットメッセージ (feat)
$ git cat-file -p c262f5725e96f52c6bf1dbebcaf2680570fba
tree 9b35f55d56eb0c921c533ae31a6a0ed9c7859057
author John Doe <johndoe@example.com> 1683579147 +0900
committer John Doe <johndoe@example.com> 1683579147 +0900
first commit
コミットファイルが作成されると、Git はその内容をハッシュ化し、そのハッシュを使用して、新しいファイルにコンテンツを保存します。最初の2文字は .git/objects
のサブフォルダー名、残りのハッシュ文字列は BLOB 名として構成されます。
処理実行時の挙動
ブランチ
ブランチは、コードの変更履歴を分岐させることができる Git の機能です。ブランチを使用することで、複数の人が同じプロジェクトで作業をする場合に、各自が独立して作業を進めることができます。
git branch mybranch
コマンドを実行し、mybranch
という名前の新しいブランチを作成すると、Git は .git/refs/heads
に mybranch
という名前の新しいファイルを生成します。このファイルには、ブランチの作成元のコミットのハッシュが含まれます。
### mybranch というブランチを作成
$ git branch mybranch
### ブランチの状態を確認
$ git branch
* master
mybranch
$ cat .git/refs/heads/mybranch
c262f5725e96f52c6bf1dbebcaf2680570fba434
$ cat .git/logs/refs/heads/mybranch
0000000000000000000000000000000000000000 c262f5725e96f52c6bf1dbebcaf2680570fba434 John Doe <johndoe@example.com> 1683662253 +0900 branch: Created from master
次に、mybranch
で新たにコミットを行うと、前述した通り、Git は、ルートツリーとコミットファイルを作成し、ブランチファイルを新しいコミットのハッシュに更新します。
したがって、ブランチはコミットを追跡するファイルであり、これらのファイルの内容はコミットを実行するたびに更新されます。
### ブランチを mybranch に切り替え
$ git checkout mybranch
### myfile2.txt ファイルを作成し、Git リポジトリに追加
$ echo "branch test" > myfile2.txt
$ git add myfile2.txt
### myfile2.txt をコミット
$ git commit -m "second commit"
[mybranch 92efc63] second commit
1 file changed, 1 insertion(+)
create mode 100644 myfile2.txt
$ cat .git/refs/heads/mybranch
92efc63e7150cc2924db956c2f540facfb46dfa2
$ cat .git/logs/refs/heads/mybranch
0000000000000000000000000000000000000000 c262f5725e96f52c6bf1dbebcaf2680570fba434 John Doe <johndoe@example.com> 1683662253 +0900 branch: Created from master
c262f5725e96f52c6bf1dbebcaf2680570fba434 92efc63e7150cc2924db956c2f540facfb46dfa2 John Doe <johndoe@example.com> 1683663583 +0900 commit: second commit
タグ
タグは、特定のコミットへの永続的な参照です。
git tag mytag
コマンドを実行し、mytag
という名前の新しいタグを作成した場合、Git はパス .git/refs/tags
に mytag
という名前の新しいファイルを生成します。
$ git tag mytag
ブランチと同様、このファイルにはタグの作成元となったコミットのハッシュが含まれています。しかし、ブランチファイルとは異なり、タグファイルは作業を進めても更新されず、作成元となった特定のコミットを指し続けます。
$ cat .git/refs/tags/mytag
92efc63e7150cc2924db956c2f540facfb46dfa2
HEAD
HEAD とは、現在の作業ブランチの最新のコミットを指すポインタです。つまり、作業しているブランチの一番最新のコミットを示します。
Git は、現在作業しているブランチを認識するために HEAD を使用します。例えば、git branch
を実行すると、Git は HEAD を見て、現在どのブランチにいるかを判断します。
また、次に行われるコミットの親コミットを参照する際にも HEAD が使用されます。コミットを作成すると、親コミットが新しいコミットに登録されます。
通常、master ブランチを使用している場合、HEAD は master ブランチを指しています。このとき、HEAD ファイルには ref: refs/heads/master
と記載されています。
仮に mybranch
というブランチに切り替えて .git
フォルダー内の HEAD ファイルを開くと、ref: refs/heads/mybranch
と記載されていることが確認できます。
したがって、HEAD はコミットを直接指すのではなく、ブランチの最新のコミットを指すポインターとして機能します。これにより、Git は現在チェックアウトされているコミットを追跡することができます。
### master ブランチに切り替え、HEAD ファイルを確認
$ git checkout master
Switched to branch 'master'
$ cat .git/HEAD
ref: refs/heads/master
### mybranch ブランチに切り替え、HEAD ファイルを確認
$ git checkout mybranch
Switched to branch 'mybranch'
$ cat .git/HEAD
ref: refs/heads/mybranch
ブランチでコミットを実行すると、Git は HEAD ファイルの内容を読み取り、親コミットとして参照するコミットを書き込むため、HEAD は間接的に次のコミットの親を提供していると言えます。
$ cat .git/refs/heads/mybranch
92efc63e7150cc2924db956c2f540facfb46dfa2
$ git cat-file -p 92efc63e7150cc2924db956c2f540facfb46dfa2
tree 02f94dfeb44bdbc4e5253e100b701b388a1a59fe
parent c262f5725e96f52c6bf1dbebcaf2680570fba434
author John Doe <johndoe@example.com> 1683663583 +0900
committer John Doe <johndoe@example.com> 1683663583 +0900
second commit
これにより、Git で以前のコミットにチェックアウトし、そこから変更を開始することができます。このモードは「分離モード」と呼ばれます。HEAD は、ブランチを参照するのではなく、直接特定のコミットを指します。
分離モードは、主にコミットのチェックアウトや修正、評価、比較を行う場合に使用されます。通常、HEAD がブランチを指している場合は、新しいコミットを作成すると、ブランチが自動的に移動し、新しいコミットがブランチに関連付けられます。しかし、分離モードでは、新しいコミットが作成されても、HEAD が指すコミットが変更されないため、新しいコミットはブランチに関連付けられない点に留意する必要があります。
マージ
Git のマージは、異なるブランチで変更を加えたコミットを結合するプロセスです。これにより、コードの変更を統合し、ブランチの履歴を保持することができます。
Git には、主に No-ff マージとFast-forward マージという2つのタイプがあります。
No-ff マージ
No-ff マージは、2つのブランチが分岐している場合に発生します。この場合、Git は、2つの親を持つ新しいコミットを作成します。1つ目の親は現在のブランチの最新のコミットであり、2つ目の親はマージするブランチの最新のコミットです。この新しいコミットにより、両方のブランチの変更が反映されます。
No-ff マージでは、競合が発生する可能性があります。競合は、同じファイルまたは行を変更しようとする2つの変更がある場合に発生します。Git は、競合がある場合には手動で解決する必要があります。
Fast-forward マージ
Fast-forward マージは、2つのブランチが分岐しており、ブランチ A がブランチ B を追い越していない場合に発生します。つまり、ブランチ A がブランチ B を継続している場合です。この場合、Git は、現在のブランチの最新のコミットを、マージするブランチの最新のコミットに単純に移動するだけです。これにより、2つのブランチの履歴が結合され、変更が反映されます。
Fast-forward マージでは、競合が発生することはありません。これは、マージするブランチに、現在のブランチにない新しい変更がないためです。このため、Fast-forward は、通常のマージよりも簡単であり、競合の解決が不要です。
参考文献
- Webサイト, "How Git truly works", Alberto Prospero, https://towardsdatascience.com/how-git-truly-works-cd9c375966f6