Help us understand the problem. What is going on with this article?

Git で Git をハックする

はじめに

みなさん、Git 使っていますか? Git、便利ですよね。
弊社の新人教育では、バージョン管理のツールとして Git ではなく SVN を教わりましたが、
やっぱり Git 使いたいですよね。
みんな使っているのに以外と最初はとっつきにくい Git。

この記事は、Git を使い始めて、なんとなく言われた通りにgit addとかgit commitとかしてるけど、
何が起きているか分からないブラックボックスな Git を、Git を使ってその動きを確認していきます。

座学

まずは、Git について簡単に書きます。 あとで、触りながら確認をします。

Git の他のバージョン管理システムとの違い

Git はそもそも、Linux カーネルを開発した、リーナス・トーバルズ氏によって、大規模な開発でもバージョン管理ができるように開発されました。
それまでにもバージョン管理システムはあったそうですが、Git がそれらのシステムと違うのは、
コミットごとにデータを圧縮したデータを保持しているという点です。
コミットごとにデータを圧縮したデータというのが大事で、それまでのバージョン管理システムでは、前のコミットとの変更差分をデータとして保持していました。

何が違うかというと、変更差分だけを保持していると、例えば 1000 世代前のバージョンに戻りたい時に、
システムは 1000 回分の変更差分を計算しなければいけないということです。
これはとても処理効率が悪いですよね。
(もし 200 世代くらい前のデータだけが消えてしまったらどうなるんだろう ?)

Git はコミットごとにデータを圧縮して保持しているため、たとえ 1000 世代前に戻るとしても、システム的には 1 回の計算で済みます。

git add と git commit

ローカルの Git のリポジトリには、3 つの領域が存在します。

1. ワーキングツリー
2. ステージ
3. ローカルリポジトリ

この 3 つの領域をドラクエで例えると、(イメージだけ伝わってほしい)

1. ワーキングツリー
   ⇨ プレイ中のデータ 
   (まさに編集中のファイル)

2. ステージ
   ⇨ ボス戦の前とか、キリのいいとこでセーブしたデータ 
   (とりあえず動くまで作ったソースコードを今の状態で保存)

3. ローカルリポジトリ
   ⇨ 大きな分岐の前で、しくったらいやだから別の名前で保存しておいたセーブデータ
   (過去の状態に戻すことができるデータの集合)

※ちなみに僕はドラクエしたことありません

ワーキングツリーからステージにあげるのが git add
ステージに保存しておいたデータをリポジトリに格納するのが git commit
です。

git add

git add をすると、ファイル毎にその圧縮ファイルが生成され、Git のリポジトリに格納されます。
その圧縮ファイルを、Git では blob オブジェクトと呼びます。
(2 つのファイルを add したら、2 つの blob が生成されます。)
その時、blob のファイル名は、ファイルの中身にヘッダーを付け加えたものを SHA-1 というハッシュ関数で生成した 16 進数 40 文字がつけられます。

ここで大事なのは、blob オブジェクトは元のファイル名の情報を一切持たないということです。
git add するともうひとつ、インデックスファイルというバイナリーデータが生成されます。
この中に、blob オブジェクトと、実ファイル(git add したファイル)の名前の関連付けが記録されます。

git commit

git commit をすると、インデックスファイルを元に生成される ツリーオブジェクト とコミットメッセージ、親コミットなどを記録した コミットオブジェクト が生成されます。
コミットオブジェクト は ツリーオブジェクト と関連づけられます。

これが Git が行なっている動作ですが、意味が分からないと思います。
ここから、本題の Git を Git でハックしていきます。

ハンズオン

ここからは実際に触りながら確認をします。

Git リポジトリ生成

環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103

$ git --version
git version 2.23.0

まず、適当な空のディレクトリを作成し、それをリポジトリにします。

~repository
$ cd ~
$ mkdir repository
$ cd repository
$ git init

これで、~/repositoryが Git リポジトリになりました。

~repository
$ ls -a
./    ../   .git/

この、.gitディレクトリがローカルリポジトリの正体です。
Git でのバージョンの記録は全てこのディレクトリ配下に保存されます。
中身を確認します。

~repository
$ tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.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, 15 files

様々なディレクトリ、ファイルが保存されていることがわかります。
今回はこの .git ディレクトリの変更を探りたいため、.git ディレクトリをリポジトリとして Git で管理します。

.git
$ cd .git
$ git init

.git ディレクトリの変更を検出するため、今の状態をコミットしておきます。

.git
$ git add .
$ git commit -m 'initial commit'
[master (root-commit) 5ebac92] initial commit
 15 files changed, 655 insertions(+)
 create mode 100644 HEAD
 create mode 100644 config
 create mode 100644 description
 create mode 100755 hooks/applypatch-msg.sample
 create mode 100755 hooks/commit-msg.sample
 create mode 100755 hooks/fsmonitor-watchman.sample
 create mode 100755 hooks/post-update.sample
 create mode 100755 hooks/pre-applypatch.sample
 create mode 100755 hooks/pre-commit.sample
 create mode 100755 hooks/pre-push.sample
 create mode 100755 hooks/pre-rebase.sample
 create mode 100755 hooks/pre-receive.sample
 create mode 100755 hooks/prepare-commit-msg.sample
 create mode 100755 hooks/update.sample
 create mode 100644 info/exclude

git add

次に、~repository で適当なファイルを作成し、git add してみます。

~repository
$ echo "hello world" > hello.txt
$ git add hello.txt

hello worldと記述された、 hello.txt を作成し、git add しました。
では、.git の変更を確認します。

.git
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    index
    objects/

nothing added to commit but untracked files present (use "git add" to track)

$ tree objects/
objects/
├── 3b
│   └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
├── info
└── pack

indexobjects/が生成され、objects/ の中には、3b/18e512dba79e4c8300dd08aeb37f8e728b8dadが生成されたことが確認できます。
それぞれが、インデックスファイルと、blob オブジェクトです。
中身を確認します。

~repository
$ git hash-object hello.txt
3b18e512dba79e4c8300dd08aeb37f8e728b8dad

$ git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
hello world

git hash-objectコマンドで、hello.txt の blob オブジェクトの名前を調べます。
git cat-file -pコマンドで、その中身を調べます。
確かに、hello world だということがわかります。

~repository
$ git ls-files --stage
100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0   hello.txt

index ファイルの中身です。
3b18e512dba79e4c8300dd08aeb37f8e728b8dad という blob オブジェクトと、hello.txtというファイル名が関連づけられていることがわかります。

一旦、今の状態で .git をコミットしておきます。

.git
$ git add .
$ git commit -m 'add hello.txt'
[master dfd8190] add hello.txt
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 index
 create mode 100644 objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad

git commit

次に、git add した hello.txt を git commit します。

~repository
$ git commit -m 'create hello worild'
[master (root-commit) d482b93] create hello worild
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt

.git の変更を確認します。

.git
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   index

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    COMMIT_EDITMSG
    logs/
    objects/68/
    objects/d4/
    refs/

no changes added to commit (use "git add" and/or "git commit -a")

$ tree objects/
objects/
├── 3b
│   └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
├── 68
│   └── aba62e560c0ebc3396e8ae9335232cd93a3f60
├── d4
│   └── 82b930517d4f9575a45ee33ee70d9eae454c27
├── info
└── pack

色々、変更されていることがわかります。

まず、先ほどのコミットIDを調べます。

~repository
$ git log
commit d482b930517d4f9575a45ee33ee70d9eae454c27 (HEAD -> master)
Author: *************************
Date:   *************************

    create hello worild

コミットID は、d482b930517d4f9575a45ee33ee70d9eae454c27です。
中身を確認します。

~repository
$ git cat-file -p d482b930517d4f9575a45ee33ee70d9eae454c27
tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60
author ************************* 1573050645 +0900
committer ************************* 1573050645 +0900

create hello worild

ツリーオブジェクト が、68aba62e560c0ebc3396e8ae9335232cd93a3f60だということがわかります。
中身を確認します。

$ git cat-file -p 68aba62e560c0ebc3396e8ae9335232cd93a3f60
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    hello.txt

ツリーオブジェクト には、コミットした hello.txt とその blob オブジェクトが関連づけられていることがわかりました。

おわり

長くなりそうなので、一旦ここまでにしておきます。
ほんとは図があったほうがわかりやすいと思いますが、作るのがめんどくさかったです。
ごめんなさい。

Git って、何がおきているかよくわかりませんが、中身を確認するとイメージがしやすくなりますね。
「Git で Git をハックする」 思ったよりも面白いです、ぜひ。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした