interprism's blog

インタープリズム株式会社の開発者ブログです。

Gitでお馴染みのあのコマンドを打った時、裏では一体何が起きているんだろう?(ステージング〜コミット)

"いにっと"、"あど"、"こみっと"、"ちぇっくあうと"、"まーじ"・・魔法の言葉を理解してみたい

この投稿は インタープリズムはAdvent Calendarを愛しています。世界中のだれよりも。 Advent Calendar 2017の4日目 の記事です。

こんにちは、imamotoです。

突然ですが、皆さんGitの操作はGUI派ですか?それともコマンドライン派ですか? 私はコマンドライン派です!

Subversionがメインだった頃はGUIのアプリケーションで操作をしていたのですが、 Gitに触れるようになってからはめっきりコマンドライン派になりました。

業務中もまるで魔法の呪文のようにgit add . git commit git push origin branch_nameとタイピングしています。

おそらく人と会話しながら余裕で打てるくらいには手に馴染んでいる愛すべきコマンド達です。

しかしふと振り返ってみると、私はこのコマンド達の本当の姿をきちんと知る努力をしてきてはいませんでした。

コミット間の差分をどのような形で保持し、どのようにコミットの履歴をたどり、どのように2つのブランチがマージされるのか。。

これらを知ることで、私はGitともっと仲良くなれるのではないかと考えました。

ということで、これからGitと仲良くなる旅に出ようと思います!

どうやってGitと仲良くなるのか?

Gitでは、ローカルのGitリポジトリのルートディレクトリにある .git フォルダの中で、

リポジトリに関するすべての情報を管理しています。

今回はその .git フォルダの中身を覗いてみることで、Gitと仲良くなっていきます。

各種コマンド実行時に .git の変更点を確認する

今回の記事のスコープについて

記事の分量が多くなりすぎるので、

今回取り扱うGit操作は リポジトリ初期化→変更をステージング→変更をコミット に限定したいと思います。

git init でGitリポジトリを初期化

まずは適当なフォルダを作り、そのフォルダ内に移動してから git init コマンドで初期化を行いましょう。

[projects] mkdir advent-calendar-git                                   18:54:41
[projects] cd advent-calendar-git                                      18:54:56

# git initで初期化
[advent-calendar-git] git init                                         18:55:01
Initialized empty Git repository in /Users/imamoto/projects/advent-calendar-git/.git/

# .gitフォルダが生成されている
[advent-calendar-git] ls -la                               18:55:03  ☁  master ☀
total 0
drwxr-xr-x   3 imamoto  staff  102 12  5 18:55 .
drwxr-xr-x  27 imamoto  staff  918 12  5 18:54 ..
drwxr-xr-x   9 imamoto  staff  306 12  5 18:55 .git

advent-calendar-gitフォルダ内に .git フォルダが生成されました。

.git の内部のファイル構成をtreeコマンドで確認してみます。

[advent-calendar-git] tree .git -aRF                       19:05:22  ☁  master ☀
.git
├── HEAD
├── config
├── description
├── hooks/
│   ├── applypatch-msg.sample*
│   ├── commit-msg.sample*
│   ├── post-update.sample*
│   ├── pre-applypatch.sample*
│   ├── pre-commit.sample*
│   ├── pre-push.sample*
│   ├── pre-rebase.sample*
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample*
│   └── update.sample*
├── info/
│   └── exclude
├── objects/
│   ├── info/
│   └── pack/
└── refs/
    ├── heads/
    └── tags/
8 directories, 14 files

主なファイルの初期値を確認

.git/HEAD

.git/HEAD ファイルは、通常は現在チェックアウトしているブランチへの参照を保持しています。

(ブランチへの参照ではなく、コミットそのものを参照することもあります)

[advent-calendar-git] cat .git/HEAD                        19:07:40  ☁  master ☀
ref: refs/heads/master

.git/config

.git/config ファイルには、このGitリポジトリに関する設定情報が記載されています。

[advent-calendar-git] cat .git/config                      19:09:20  ☁  master ☀
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true

リポジトリに関する設定を追加すると、このconfigファイルに追記されます。

# ユーザ名とEmailアドレスの設定を追加
[advent-calendar-git] git config user.name "Imamoto-kun"   19:11:46  ☁  master ☀
[advent-calendar-git] git config user.email "imamoto@sample.com"

# .git/configを確認
[advent-calendar-git] cat .git/config                      19:13:07  ☁  master ☀
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
[user]
    name = Imamoto-kun
    email = imamoto@sample.com

.git/info/exclude

.git/info/exclude ファイルは、お馴染みの .gitignore と同じような役割を果たします。

バージョン管理外としたいファイルがチームメンバーで共有している .gitignore ファイルに追記しづらい場合、ローカル環境でのみバージョン管理対象外にすることができます。

# 初期値は管理対象外ファイルなし
[advent-calendar-git] cat .git/info/exclude                19:14:43  ☁  master ☀
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

# 新たなファイル imamoto.txt を追加
[advent-calendar-git] touch imamoto.txt                    19:18:15  ☁  master ☀
[advent-calendar-git] git status                         19:19:07  ☁  master ☂ ✭
On branch master
Initial commit
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    imamoto.txt
nothing added to commit but untracked files present (use "git add" to track)

# .git/info/exclude に imamoto.txtを追記
[advent-calendar-git] echo "imamoto.txt" >> .git/info/exclude
[advent-calendar-git] cat .git/info/exclude                19:21:03  ☁  master ☀
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
imamoto.txt

# imamoto.txtが管理対象外になっている
[advent-calendar-git] git status                           19:21:12  ☁  master ☀
On branch master
Initial commit
nothing to commit (create/copy files and use "git add" to track) 

git add でコミットしたいファイルをステージング

hoge.txt の作成

ファイルの中身が1行の hoge.txt を作成し、 git add コマンドでステージングします。 ステージングとは、次回のコミットに含めたい変更内容を一時的に保存しておく処理のことです。

# hoge.txtを作成してステージング
[advent-calendar-git] echo "hoge" > hoge.txt               19:37:50  ☁  master ☀
[advent-calendar-git] cat hoge.txt                       19:38:06  ☁  master ☂ ✭
hoge
[advent-calendar-git] git add hoge.txt                   19:38:14  ☁  master ☂ ✭
 
# 更新日時順にフルパス表示
[advent-calendar-git] tree .git -DFafi | sort -r         19:39:20  ☁  master ☂ ✚
[Dec  5 19:38]  .git/objects/22/62de0c121f22df8e78f5a37d6e114fd322c0b0
[Dec  5 19:38]  .git/objects/22/
[Dec  5 19:38]  .git/objects/
[Dec  5 19:38]  .git/index
[Dec  5 19:21]  .git/info/exclude
// 以降続く

ステージングすることで、以下の2つのファイルが新たに作成されました。

  • git/objects/22/62de0c121f22df8e78f5a37d6e114fd322c0b0
  • git/index

1つめのファイルはGitオブジェクトと呼ばれる種類のファイルです。

それぞれのファイルの内容を確認してみましょう。

Gitオブジェクト

.git/objects 配下に格納されるバイナリファイルをGitオブジェクトといいます。

GitオブジェクトはGitリポジトリ内のファイル変更に関する情報を保持しています。

今回のステージングで追加された .git/objects/22/62de0c121f22df8e78f5a37d6e114fd322c0b0 もGitオブジェクトです。

Gitオブジェクトのファイルが格納されている2桁の16進数のフォルダ名 + ファイル名をオブジェクトIDといいます。

(.git/objects/22/62de0c121f22df8e78f5a37d6e114fd322c0b0 のオブジェクトIDは 2262de0c121f22df8e78f5a37d6e114fd322c0b0 です)

このオブジェクトの内容を確認してみます。

Gitオブジェクトの情報を確認

Gitオブジェクトはそれぞれ、オブジェクトタイプとファイル変更に関する情報を持っており、

git cat-file コマンドでその内容を確認することができます。

# オブジェクトタイプを確認
[advent-calendar-git] git cat-file -t 2262de0c121f22df8e78f5a37d6e114fd322c0b0
blob
 
# ファイル変更に関する情報を確認
[advent-calendar-git] git cat-file -p 2262de0c121f22df8e78f5a37d6e114fd322c0b0
hoge

-t オプションで取得したオブジェクトタイプは blob となっています。

blobは変更されるファイルそのものを表すGitオブジェクトで、-p オプションで確認した内容は hoge.txt の内容そのものです。

ポイントは、blobのGitオブジェクトは hoge.txt のファイル名やアクセス権限の情報を持たないことです。

indexファイルを確認

blobのGitオブジェクトが保持していなかった hoge.txt ファイルに関する情報は、 .git/index ファイルに保持しています。

このファイルはステージングされたファイル情報の格納場所となっています。

.git/index もバイナリファイルで、その内容は git ls-files --stage コマンドで確認できます。

[advent-calendar-git] git ls-files --stage               20:45:25  ☁  master ☂ ✚
100644 2262de0c121f22df8e78f5a37d6e114fd322c0b0 0 hoge.txt

hoge.txt のファイル名・アクセス権限の情報や、blobのGitオブジェクトのオブジェクトIDが記載されています。

hoge.txt の更新、fuga.txt の作成

hoge.txt に更に1行追加・ fuga.txt を新規に作成して、再び git add コマンドでステージングを行います。

# hoge.txtを更新
[advent-calendar-git] echo "hogehoge" >> hoge.txt        21:28:33  ☁  master ☂ ✚
# fuga.txtを作成
[advent-calendar-git] echo "fuga" > fuga.txt             21:28:57  ☁  master ☂ ⚡
# hoge.txt, fuga.txtをステージング
[advent-calendar-git] git add hoge.txt fuga.txt        21:29:06  ☁  master ☂ ⚡ ✭
 
# 更新日時順にフルパス表示
[advent-calendar-git] tree .git -DFafi | sort -r         21:29:14  ☁  master ☂ ✚
[Dec  5 21:29]  .git/objects/91/28c3eb56a3547e2cca631042366bf0f37abe67
[Dec  5 21:29]  .git/objects/91/
[Dec  5 21:29]  .git/objects/19/04c092b649dc54f3c8fc931acb0ca5bb952c3b
[Dec  5 21:29]  .git/objects/19/
[Dec  5 21:29]  .git/objects/
[Dec  5 21:29]  .git/index
[Dec  5 19:38]  .git/objects/22/62de0c121f22df8e78f5a37d6e114fd322c0b0
// 以降続く

今回は以下の2つのGitオブジェクトファイルが追加されました。

  • git/objects/91/28c3eb56a3547e2cca631042366bf0f37abe67
  • git/objects/19/04c092b649dc54f3c8fc931acb0ca5bb952c3b

また、.git/index が更新されています。

それぞれの変更内容を確認していきます。

Gitオブジェクトの情報を確認

まずは 9128c3eb56a3547e2cca631042366bf0f37abe67 のGitオブジェクトを確認します。

[advent-calendar-git] git cat-file -t 9128c3eb56a3547e2cca631042366bf0f37abe67
blob
[advent-calendar-git] git cat-file -p 9128c3eb56a3547e2cca631042366bf0f37abe67
fuga

9128c3eb56a3547e2cca631042366bf0f37abe67fuga.txt の内容を保持したblobのGitオブジェクトでした。

続いて 1904c092b649dc54f3c8fc931acb0ca5bb952c3b のGitオブジェクトを確認します。

[advent-calendar-git] git cat-file -t 1904c092b649dc54f3c8fc931acb0ca5bb952c3b
blob
[advent-calendar-git] git cat-file -p 1904c092b649dc54f3c8fc931acb0ca5bb952c3b
hoge
hogehoge

1904c092b649dc54f3c8fc931acb0ca5bb952c3bhoge.txt の内容を保持したblobのGitオブジェクトでした。 ポイントは以下の2つです。

  • 一度目のステージング時に生成された 2262de0c121f22df8e78f5a37d6e114fd322c0b0 とは違うGitオブジェクトが生成された。
  • 一度目のステージングとの差分ではなく、hoge.txt 全体が保存された。

blobのGitオブジェクトはステージングを行う度に新たに作成され、ファイル全体を保存します。

indexファイルを確認

git/index ファイルは以下のようになっています。

[advent-calendar-git] git ls-files --stage               21:46:26  ☁  master ☂ ✚
100644 9128c3eb56a3547e2cca631042366bf0f37abe67 0 fuga.txt
100644 1904c092b649dc54f3c8fc931acb0ca5bb952c3b 0 hoge.txt

fuga.txt の情報が新たに追加されたのに加え、hoge.txt の行のblobのGitオブジェクトIDが最新のものに更新されました。

git commit でステージングしたファイルをコミットする

続いて、先ほどステージングしたファイルをコミットしましょう。

[advent-calendar-git] git commit -m "commit 1"           21:57:55  ☁  master ☂ ✚
[master (root-commit) 561d2be] commit 1
 2 files changed, 3 insertions(+)
 create mode 100644 fuga.txt
 create mode 100644 hoge.txt


# 更新日時順にフルパス表示
[advent-calendar-git] tree .git -DFafi | sort -r           21:58:07  ☁  master ☀
[Dec  5 21:58]  .git/refs/heads/master
[Dec  5 21:58]  .git/refs/heads/
[Dec  5 21:58]  .git/objects/9d/59adb6ab05b5c697d0adc18241f09ce241ee29
[Dec  5 21:58]  .git/objects/9d/
[Dec  5 21:58]  .git/objects/56/1d2bea3175f9d70230d57b72029374b523f23b
[Dec  5 21:58]  .git/objects/56/
[Dec  5 21:58]  .git/objects/
[Dec  5 21:58]  .git/logs/refs/heads/master
[Dec  5 21:58]  .git/logs/refs/heads/
[Dec  5 21:58]  .git/logs/refs/
[Dec  5 21:58]  .git/logs/HEAD
[Dec  5 21:58]  .git/logs/
[Dec  5 21:58]  .git/index
[Dec  5 21:58]  .git/COMMIT_EDITMSG
[Dec  5 21:29]  .git/objects/91/28c3eb56a3547e2cca631042366bf0f37abe67
// 以降続く

今回は新たに以下のファイルが追加されました。

  • git/refs/heads/master
  • git/logs/refs/heads/master
  • git/logs/HEAD
  • git/COMMIT_EDITMSG
  • git/objects/9d/59adb6ab05b5c697d0adc18241f09ce241ee29
  • git/objects/56/1d2bea3175f9d70230d57b72029374b523f23b

また、今回も .git/index が更新されています。

Gitオブジェクトの情報を確認

treeタイプのGitオブジェクト

まずは 9d59adb6ab05b5c697d0adc18241f09ce241ee29 のGitオブジェクトを確認します。

# オブジェクトタイプを確認
[advent-calendar-git] git cat-file -t 9d59adb6ab05b5c697d0adc18241f09ce241ee29
tree
 
# ファイル変更に関する情報を確認
[advent-calendar-git] git cat-file -p 9d59adb6ab05b5c697d0adc18241f09ce241ee29
100644 blob 9128c3eb56a3547e2cca631042366bf0f37abe67   fuga.txt
100644 blob 1904c092b649dc54f3c8fc931acb0ca5bb952c3b   hoge.txt

-t オプションで取得したオブジェクトタイプは tree となっています。

treeタイプのGitオブジェクトには、直前にステージングされていた情報が格納されています。

そのため、 .git/index と同様にファイル名やアクセス権限、blobのオブジェクトIDを保持しています。

複数のblobオブジェクトがまとまっており、1度のコミットで1つ生成されますが、

コミット自体の情報は保持していません。

commitタイプのGitオブジェクト

続いて、561d2bea3175f9d70230d57b72029374b523f23b のGitオブジェクトを確認します。

# オブジェクトタイプを確認
[advent-calendar-git] git cat-file -t 561d2bea3175f9d70230d57b72029374b523f23b
commit
 
# ファイル変更に関する情報を確認
[advent-calendar-git] git cat-file -p 561d2bea3175f9d70230d57b72029374b523f23b
tree 9d59adb6ab05b5c697d0adc18241f09ce241ee29
author Imamoto-kun <imamoto@sample.com> 1512478687 +0900
committer Imamoto-kun <imamoto@sample.com> 1512478687 +0900
commit 1

-t オプションで取得したオブジェクトタイプは commit となっています。

commitタイプのGitオブジェクトには、コミットメッセージやコミット日時、コミットした人等、コミットに関する情報が格納されています。

また、その変更内容として、treeタイプのGitオブジェクトへの参照を持っています。

Gitオブジェクト同士の依存関係

ここまでで登場した、blob、tree、commitの3つのGitオブジェクトの関係性は以下のようになります。

f:id:interprism:20171218142240p:plain

.git/logs/HEADファイルを確認

カレントブランチの内容やブランチ切り替えが行われたログが格納されています。

commitタイプのGitオブジェクトのID(コミットID)が記載されています。

以下は、今回のコミットによって、

0000000000000000000000000000000000000000 から 561d2bea3175f9d70230d57b72029374b523f23b の状態へ変更されたことを示しています。

[advent-calendar-git] cat .git/logs/HEAD                   22:26:25  ☁  master ☀
0000000000000000000000000000000000000000 561d2bea3175f9d70230d57b72029374b523f23b Imamoto-kun <imamoto@sample.com> 1512478687 +0900 commit (initial): commit 1

.git/refs/heads/masterファイルを確認

.git/refs/heads/branch_name のファイルには、そのブランチでの最新のコミットIDを保持しています。

[advent-calendar-git] cat .git/refs/heads/master           22:45:57  ☁  master ☀
561d2bea3175f9d70230d57b72029374b523f23b

.git/logs/refs/heads/masterファイルを確認

.git/logs/refs/heads/branch_name のファイルには、そのブランチの内容が切り替わった際のログが格納されています。

現在はブランチがmasterしかないため、.git/logs/HEAD と同一の内容です。

[advent-calendar-git] cat .git/logs/refs/heads/master      22:46:31  ☁  master ☀
0000000000000000000000000000000000000000 561d2bea3175f9d70230d57b72029374b523f23b Imamoto-kun <imamoto@sample.com> 1512478687 +0900 commit (initial): commit 1

.git/COMMIT_EDITMSGファイルを確認

.git/COMMIT_EDITMSG ファイルは、コミットメッセージ編集時の一時ファイルです。

[advent-calendar-git] cat .git/COMMIT_EDITMSG              22:51:55  ☁  master ☀
commit 1

2度目のコミットをしてみる

最後に、2度目のコミットをしてみます。

[advent-calendar-git] echo "hogehogehoge" >> hoge.txt      22:57:23  ☁  master ☀
[advent-calendar-git] git add hoge.txt                   22:57:39  ☁  master ☂ ⚡
[advent-calendar-git] git commit -m "commit 2"           22:57:45  ☁  master ☂ ✚
[master 1e80717] commit 2
 1 file changed, 1 insertion(+)
 
# 更新日時順にフルパス表示
[advent-calendar-git] tree .git -DFafi | sort -r           22:57:53  ☁  master ☀
[Dec  5 22:57]  .git/refs/heads/master
[Dec  5 22:57]  .git/refs/heads/
[Dec  5 22:57]  .git/objects/c6/d755d5c7f0e8c2a3af634eeb40e9f3296ddd56
[Dec  5 22:57]  .git/objects/c6/
[Dec  5 22:57]  .git/objects/9d/1d67e349dc0f3309c6e5f7e94c01151b21e2ca
[Dec  5 22:57]  .git/objects/9d/
[Dec  5 22:57]  .git/objects/1e/80717acb767ae8917ed44de4fcb9207f2dda02
[Dec  5 22:57]  .git/objects/1e/
[Dec  5 22:57]  .git/objects/
[Dec  5 22:57]  .git/logs/refs/heads/master
[Dec  5 22:57]  .git/logs/HEAD
[Dec  5 22:57]  .git/index
[Dec  5 22:57]  .git/COMMIT_EDITMSG
[Dec  5 21:58]  .git/objects/9d/59adb6ab05b5c697d0adc18241f09ce241ee29
// 以降続く

先ほどとほとんど変わらない操作だったので、更新されたファイルの顔ぶれもほとんど変わりません。

今回は、特に注目すべき commitタイプのGitオブジェクト のみを確認します。

commitタイプのGitオブジェクトの確認

# オブジェクトタイプを確認
[advent-calendar-git] git cat-file -t 1e80717acb767ae8917ed44de4fcb9207f2dda02
commit
 
# ファイル変更に関する情報を確認
[advent-calendar-git] git cat-file -p 1e80717acb767ae8917ed44de4fcb9207f2dda02
tree c6d755d5c7f0e8c2a3af634eeb40e9f3296ddd56
parent 561d2bea3175f9d70230d57b72029374b523f23b
author Imamoto-kun <imamoto@sample.com> 1512482272 +0900
committer Imamoto-kun <imamoto@sample.com> 1512482272 +0900
commit 2

特筆すべき点は、親コミット(parent)のコミットIDを保持している点です。

このコミットIDは、先ほどの1度目のコミット時に生成されたIDです。

このように、親コミットのIDを保持することでコミットの履歴をたどることができます。

Gitオブジェクト同士の依存関係(親子関係を含む)

コミットの親子関係を含んだGitオブジェクト同士の依存関係は以下のようになります。

f:id:interprism:20171218142825p:plain

まとめ

今回は リポジトリ初期化〜コミット までをスコープにして、.git フォルダの中身を見ていきました。

本当はブランチの切り替えやmerge, rebase, cherry-pick等も取り扱いたかったのですが、

長くなってしまったので次の機会にしたいと思います。

ただ、ここまで色々書いてきましたが、言いたかったことは「Gitのファイル構成、めっちゃシンプル!!」ということです。

こういったファイル構成が頭に入っていれば、Gitで複雑なことをしたい時にも操作のイメージが付きやすくなると思います。

皆さんもお暇な時には是非 .git フォルダを覗いて、Gitと仲良くなってみてください。それではまた!!

インタープリズムのページ

PAGE TOP