まず初めに
未経験からエンジニアをはじめ、教わった通りにGitHubへpushするコマンドを打ってきました。
しかし、そのコマンドの裏で何が起こっているのか分からず、gitを使っていていろいろ困った時に解決方法がわからないとかコマンドを打つのが怖くなったりしていて、この機会にGitの構造を理解するために自分の記録として書いてます。
あまり技術的な知識も深くないので誤った内容が含まれているかと思いますが、参考にしていただければと思います。
Gitはなんのために使うのか
ファイルのバージョンを管理するため。
ファイルのバージョンを管理しないと、
どのファイルが最新なのかわからない。
- 履歴書_20220420
- 履歴書_20220421
- 履歴書_20220425_1
- 履歴書_20220425_2
のように最新のファイルが実際どれなのか分かりにくい。
また、履歴書を誰かに修正してもらうとしましょう。
Bさんに修正してもらっている最中に自分も編集します。
Bさんが上書きした後に自分が上書きしたら、せっかくBさんに修正してもらったのにそのデータはなくなってしまいます。
理想はBさんが修正して履歴書_20220425_チェック済のファイルをコピーして自分がさらに修正するのが理想です。(もしくはその逆)
しかしファイルを共有する相手が多いければ多いほど、このようなケースが起きやすいですよね。
Gitはこういったことが起きないようバージョンを管理してくれるソフトウェアです。
また過去のバージョンを持っているので、過去のデータに戻すことも可能です。
Gitの歴史
ライセンスが変更された
Linuxカーネル開発で利用していたバージョン管理システムのライセンスが変更されて、使用できなくなった。
Linux開発用のバージョン管理システム開発
当時もそのほかにバージョン管理システムは複数存在していました。
しかし、ブランチを切ったり、ブランチを統合するのに時間がかかり課題を感じていました。
要するに、大規模で開発をするのにあまり向いていない状態でした。
それを解決するために2005年頃にGitの原型となるプログラム開発が開始された。
SVNはディレクトリを複製してブランチを作成します。作成する際は、派生元となるディレクトリ、例えばtrunkディレクトリをbranchesディレクトリ配下にコピーして作成します。また、ブランチの作成やマージ時には差分計算処理が実行されます。そのためプロジェクトの規模にもよりますが、数秒から数分の時間がかかるため頻繁なブランチ作成やマージを避ける傾向にあります。
一方、Gitは更新断面のスナップショットを複製してブランチを作成します。そのため、ブランチ作成に時間がかかりません。また、コミットの際に更新前のコミットのスナップショットを記録しているので、マージを行う際も差分比較が容易なためマージが非常に簡単です。
これらのGitの特徴は、開発者の気軽なブランチ作成を実現します。
引用元:https://aslead.nri.co.jp/column/differences-between-git-and-svn.html
Linux開発用のバージョン管理システム完成
先ほどの課題を解消することができリリースされた。
特徴は、
- スピードが速い
- シンプルな設計
- ブランチが並列で開発可能
- 大規模でも効率的に扱える
Gitの基本的な仕組み
Gitはスナップショットで記録する
まずGitはデータをスナップショットを記録します。
まずスナップショットとは何かというと、今そのままのデータを丸ごと保存したものです。
イメージするとAさんが作業をしているのを止めて、ファイルデータを丸ごと取得します。
そして、戻して再開してもらいます。
また、どこかのタイミングで作業をとめてもらいファイルデータを丸ごと取得します。
そして、再開してもらう・・・
といった形で、そのデータはなんでもいいのですが、その瞬間のデータを丸ごと取得することをスナップショットといいます。
分からん・・・という方はこのサイトが分かりやすいです。
「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
実際はGitでもコミットの差分を確認することができます。
これはデータを差分でもっているのではなくて、1つ前のコミットとのスナップショットと今のコミットのスナップショットを比較計算することで差分を出しています。
あくまでもスナップショットを比較して計算しているという点が重要です。
データをどうやって管理してるんや?
Gitはデータをスナップショットで記録しているけど、実際どうやって管理してるの?と疑問に思いました。
ソースコードその他の管理対象は、ファイルごとに圧縮され、ハッシュ値で管理されます。
要するにファイルの中身が圧縮されファイル名にハッシュIDが付きます。
ハッシュIDはヘッダー(ファイルの文字数やメタ情報)とファイルの内容をSHA-1というハッシュ関数で40文字の英数字に変換したものです。
ハッシュIDの先頭の2文字はサブディレクトリに残りの38文字はファイル名になります。
$ git hash-object index.html
64233a9e9589b022576fca2cc18396d3744173c4
実際にindex.htmlのハッシュIDを表示すると64233a9e9589b022576fca2cc18396d3744173c4
という40文字のハッシュidが作られました。
実際は圧縮ファイルではなく、blobオブジェクトと言われています。
ちなみにファイルの中身が同じ場合は、blogオブジェクトは作られません。重複しないということです。
なぜかというとコミットは一部のファイルのみ変更が行わることも多く、無駄に重複するデータを保存するとディスク容量を使ってしまうからです。 = ディスク容量の節約
仮にファイルを変更した場合はgit status
が感知し、内容を計算してハッシュが作られます。
現在のファイルと差分があるときハッシュIDが違うのでgit status
のコマンドを打つと教えてくれる仕組みになっています。
なぜハッシュIDが異なるのかというと、ハッシュIDはファイルの文字数やメタ情報、ファイル内容などで変換するため相違が生まれる形です。
git addをする
実際にこのファイルをステージにのせます。(addをするとステージに乗ります)
ステージとは簡単に言うと、リポジトリへ移動させるための準備段階だと思ってください。
このステージが必要な理由は、ワークツリーで作業をしている時に中途半端なものはステージに乗せず、完成状態のものはコミットしたいからです。
複数ファイルを編集してしまった。しかし、編集した全ファイルをコミットするのではなく、一部のファイルだけをコミットしたい。そこで、コミットしたいファイルだけを選別して、ステージに移動して、コミットするため。
引用元:https://www.ninton.co.jp/archives/3218#toc1
話は戻りまして・・・
ステージにファイルを載せました、するとワークツリー(今作業しているローカル)からリポジトリで圧縮され、ステージにインデックスされます。
この時の.gitの中身を見てみます。
$ tree .git # .gitのディレクトリ構造見せてー
|
+---objects
| +---64
| | 233a9e9589b022576fca2cc18396d3744173c4
| |
| +---info
| \---pack
\---refs
+---heads
\---tags
ハッシュIDの先頭の2文字はサブディレクトリに残りの38文字はファイル名になります。
先ほど話したように、64233a9e9589b022576fca2cc18396d3744173c4
はobjectsのサブディレクトリに先頭2文字の64
というディレクトリに233a9e9589b022576fca2cc18396d3744173c4
というファイルが作成されました。
このあとインデックスへいくのですが、1つ問題があります。
このファイルには、ハッシュIDはありますが元のindex.htmlというファイル名が存在しません。
そこで登場するのがインデックスです。
インデックスの中身を見てみます。
$ git ls-files --stage # stageに上がったのを教えてー
100644 64233a9e9589b022576fca2cc18396d3744173c4 0 index.html
このようにインデックスでは圧縮されたファイルはindex.htmlですよと関連性を持たせてくれています。
ちなみに変更・作成されたものは必ず.gitのobjectsの中に保存されます。
git commitする
このような感じでリポジトリにツリーオブジェクトが作成され、コミット1が作成されます。
また、コミット1の情報のツリー1はツリー1のオブジェクトを参照していることで、ツリー1オブジェクトの内容を確認したいときはコミット1のツリー1を見ればわかります。
実際にツリー1を見てみましょう。
$ find .git/objects -type f #追加されたファイルを探す
.git/objects/5b/2045e4db6de951691f814adf01e900394270a0 // tree
.git/objects/64/233a9e9589b022576fca2cc18396d3744173c4 // index.htmlの圧縮されたファイル
.git/objects/ac/03200005a8b83d96c3e6d3e5cfc24db08f8b28 // コミットファイル
$ git cat-file -t 5b2045e4db6de951691f814adf01e900394270a0 # このハッシュのオブジェクトタイプは?
tree
はい、このように5b2045e4db6de951691f814adf01e900394270a0
はツリーだよと教えてくれました。
では実際に圧縮ファイルとファイル名があるかツリーの中身を見てみます。
$ git cat-file -p 5b2045e4db6de951691f814adf01e900 #オブジェクトの中身を教えてー
100644 blob 64233a9e9589b022576fca2cc18396d3744173c4 index.html
ばっちりツリーの中に圧縮ファイルとファイル名が入っておりました。
ついでにコミットも見てみましょう!
$ git cat-file -p ac03200005a8b83d96c3e6d3e5cfc24db08f8b28 #オブジェクトの中身を教えてー
tree 5b2045e4db6de951691f814adf01e900394270a0
author 僕の個人情報 1650552725 +0900
committer 僕の個人情報 1650552725 +0900
index.html新規作成
コミットには先ほどのツリーオブジェクトの情報が入ってます。そしてコミットメッセージも入っていますね!
ちなみに変更・作成されたものは必ず.gitのobjectsの中に保存されます。
これaddの時に話したのですが覚えてますか?
今回commitで作成したツリーオブジェクトとコミットオブジェクト両方とも.git/objectsの中に入ります。
これもみましょう。
+---objects
| +---5b
| | 2045e4db6de951691f814adf01e900394270a0
| |
| +---64
| | 233a9e9589b022576fca2cc18396d3744173c4
| |
| +---ac
| | 03200005a8b83d96c3e6d3e5cfc24db08f8b28
| |
ちょっと照らし合わせないと難しいのですが、
- ツリー:5b2045e4db6de951691f814adf01e900394270a0
- ファイル:64233a9e9589b022576fca2cc18396d3744173c4
- コミット:ac03200005a8b83d96c3e6d3e5cfc24db08f8b28
すべてobjectsの中に入っていますね!
新しくディレクトリを追加してみる
では、新しくディレクトリを作成してもう一度コミットした場合どうなるのか確認しましょう。
$ ls
index.html test/
$ ls test/
sample.txt
testというディレクトリとその中にsample.txtを新規作成しました。
addからcommitは同じ内容なので省きますね。
実際にコミットした後、masterブランチ上での最後のコミットが指しているツリーオブジェクトの中身を表示します
$ git cat-file -p master^{tree} # masterブランチ上での最後のコミットが指しているツリーオブジェクトの中身を表示
100644 blob 64233a9e9589b022576fca2cc18396d3744173c4 index.html
040000 tree 30ebb81289ebdcdb08633ef3999df098c963c290 test
testというディレクトリがツリーとして表示されていることがわかります。
testの中も見てみましょう。
$ git cat-file -p 30ebb81289ebdcdb08633ef3999df098c963c290
100644 blob d64a3d962e787834f9b43312cdcdb96ef357709a sample.txt
testのツリーにはsample.txtが入っていますね。
testのディレクトリーにsample.txtが入っているのと同じ構成ですね!
つまりこういったツリーオブジェクトの構成になってます。
一番上のツリーはどこから?となりますがこれは、commitした時のツリーです。
$ find .git/objects -type f # objectsの中のファイル教えてー
.git/objects/30/ebb81289ebdcdb08633ef3999df098c963c290 # sample.txt
.git/objects/58/7399667a92f2c680d673ae8b29bf83ebbbf631 # 2回目のコミット
.git/objects/5b/2045e4db6de951691f814adf01e900394270a0 # index.html
.git/objects/64/233a9e9589b022576fca2cc18396d3744173c4 # 1回目のファイル
.git/objects/ac/03200005a8b83d96c3e6d3e5cfc24db08f8b28 # 1回目のコミット
.git/objects/d6/4a3d962e787834f9b43312cdcdb96ef357709a # sample.txtの中身
.git/objects/f4/afb516aa807b7335a24673a1cbc83ba246c0a0 # 2回目のtree
$ git cat-file -t f4afb516aa807b7335a24673a1cbc83ba246c0a0 # このハッシュってtree?
tree
$ git cat-file -p f4afb516aa807b7335a24673a1cbc83ba246c0a0 # このハッシュの中身教えてー
100644 blob 64233a9e9589b022576fca2cc18396d3744173c4 index.html
040000 tree 30ebb81289ebdcdb08633ef3999df098c963c290 test
$ git cat-file -p master^{tree}
と同じ意味ですが、commitした時のツリーからblobとtestがつながっているのが分かるかと思います。
$ git cat-file -p master^{tree} # masterブランチ上での最後のコミットが指しているツリーオブジェクトの中身を表示
こちらはmaseterブランチ上での最後のコミットがさしているツリーオブジェクトの中身です。
つまりコミットがさしているツリーオブジェクトが見えません。
そのため先頭のツリーはどこから?とややこしくなりますが、あくまでもツリーの中身なのでツリーは存在しています。
commitオブジェクトを見てみる
testディレクトリをcommitした時の結果がこちら
$ git cat-file -p 587399667a92f2c680d673ae8b29bf83ebbbf631 # このハッシュの中身教えてー
tree f4afb516aa807b7335a24673a1cbc83ba246c0a0 # tree
parent ac03200005a8b83d96c3e6d3e5cfc24db08f8b28 # 親のコミット
author 僕の個人情報 1650554191 +0900
committer 僕の個人情報 1650554191 +0900
add test ディレクトリー # コミットメッセージ
ツリーは先ほど説明したf4afb516aa807b7335a24673a1cbc83ba246c0a0
ですね。
この中に2回目のコミットしたスナップショットが入っています。
ここで注目してほしいのがparent
です。
このparent
のハッシュが何か確認しましょう。
$ git cat-file -p ac03200005a8b83d96c3e6d3e5cfc24db08f8b28 # このハッシュの中身教えてー
tree 5b2045e4db6de951691f814adf01e900394270a0
author wrsdu1715 <wrsdu1715@gmail.com> 1650552725 +0900
committer wrsdu1715 <wrsdu1715@gmail.com> 1650552725 +0900
index.html新規作成
前回のコミットオブジェクトが表示されました。
つまり2回目のコミットは1つ前のコミットとリンクしていることがわかりますね。
一番古い(一番最初)のコミットにはその前がないのでparent
がありません。
treeの中身を見ればスナップショットのファイル内容がわかります。
ということは、これがどんだけコミットされていようとも親のコミットが繋がっていて、ツリーを見ればファイルの全体がわかります。
こうやって履歴をたどっていくことでバージョン管理をしているってことですね。
最後にこの全体をGitオブジェクトというのですが、Gitオブジェクトはこのような形になっています。
急に横にしてすみません。
データ構造的には、コミットはリスト構造であり、treeとblob関係はツリー構造になるのかなとおもいます。
実際はコミットの取り消しであったり、resetであったりといろいろな動きがあるのですが、今回はGitの根幹であるバージョン管理のみとさせていただきます。
より深く構造を理解するとコマンドをすることでの裏の動きが分かり、問題が起きたときに対応しやすいかなとおもいます。
これからも学習していきたいと思います。
ありがとうございます。