Gitを仕組みから再学習

# Git

## gitの実体

gitはコミットした時点のファイルのスナップショットをそのファイルから計算したハッシュ値で紐づけたKVSのようなもので過去のversionを管理している。
`$git init`した時に作成される`.git`がgitの実体であり、その中の`.git/object`にハッシュ値で紐づけられた大量のスナップショット(blobオブジェクト)が保管されている。

## オブジェクト

`.git/object`の中にはファイルのスナップショット(blobオブジェクト)だけが保存されているわけではない。blob、tree、commitの3種類のオブジェクトが存在する。

なお、オブジェクトを確認するには`$git cat-file`コマンドを利用する。
`.git/object`以下はさらにハッシュ値の先頭2桁でフォルダ分けされており、先頭2桁のハッシュはオブジェクトのファイル名からは除かれているのでその値を追加して`cat-file`する必要がある。見方は以下。

```
# ファイルのタイプを見たい。
$ git cat-file -t [中身を見たいハッシュ]
# ファイルのサイズを見たい。
$ git cat-file -s [中身を見たいハッシュ]
# ファイルの中身を見たい。
$ git cat-file -p [中身を見たいハッシュ]
```

### blobオブジェクト

スナップショットにあたる。
ファイルを`$git add`すると、そのファイルがワークツリーからコピー・zlib圧縮され、blobオブジェクトとなる。また、そのファイルの中身からハッシュ値を計算し、blobオブジェクトはそのハッシュ値で区別されるようになる。
`add`したファイル1つ1つに対応するようblobオブジェクトが作成されるため、`.git/object`の中の任意のblobオブジェクトを覗くとどこかの時点での特定のファイルの中身を確認することができる。

### treeオブジェクト

ディレクトリのスナップショットに当たる。コミット時に生成。
ディレクトリはその直下にどのファイルが存在するかを管理するようにtreeオブジェクトはそのファイルの直下にどのblobやtreeオブジェクトが存在するかの情報(ハッシュ)を持っている。

```
$ git cat-file -p 90b4c465b199a20546f6fa0fee7cb2d213de1b4f
040000 tree 35f02fd1aa4e582cab3dbbf4591dfd0d7007f97f	hoge
100644 blob c945dbe34b2b9b68b94a06ca1c0b31cb7d9f1bdd	aaa.txt
100644 blob 83c831f0b085c70509b1fbb0a0131a9a32e691ac	bbb.md
```

左から順に、アクセス権限、オブジェクトの種類、ハッシュ、ディレクトリ名orファイル名となっている。

### commitオブジェクト

コミットする毎に1つ作られるオブジェクト。
ルートのtreeオブジェクト(.gitを含む最上位のディレクトリ)のハッシュが記録されている。
また、親となるコミット(ひとつ前のコミット)のハッシュを持つ。
そのほか、authorやcommitter、コミットメッセージが記録される。

```
$ git cat-file -p a2e77fbbef72a23e6c908a0eb36c219d77e1a93b
tree 123fdb23bd445760cfd90eaf90946648fb012b99
parent 2abb34478c89d08d2c83a46bc715db939b6ea1ce
author *** <***@gmail.com> 1620306851 +0900
committer *** <***@gmail.com> 1620306851 +0900

test commit
```

### コミットまでの一連の流れを見てみる。

`$git add`することでblobオブジェクトが作成され、`$git commit`によりローカルリポジトリにtreeオブジェクトとcommitオブジェクトが作成される。

1度目のコミット。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/27dba507-9fbf-9036-56fa-8a31a1657c9f.png)

片方のファイルに修正を加えての2度目のコミット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/e6714f4a-0de2-004c-a64c-49c0ab2bf994.png)

注目すべきは、treeオブジェクトにおいて変更されていない方のblobオブジェクトのハッシュは変わっていない点である。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/6af6c1bc-1825-3d7e-5b52-f92e9dce545b.png)

gitは`commit`する度に全てのスナップショットを保存していくのではなく変更があったファイルのみをスナップショットとして保存し、変更されていないファイルは同じハッシュを使い回すことで容量を節約している。

なお、インデックスとローカルリポジトリの状態を確認するには以下のコマンドを利用することで確認可能。

```
# インデックス
$ git ls-files --stage
100644 38e5cdb340ae9979fe513f62388dff69a9885ad1 0	test/aaa.txt
100644 83c831f0b085c70509b1fbb0a0131a9a32e691ac 0	test/bbb.md

# リポジトリ
$ git ls-files        
test/aaa.txt
test/bbb.md
```
___

gitの実体はハッシュ値をキーとし、圧縮したファイルを値としたKVSのようなものであり、各コミットは有向グラフとして過去の全ディレクトリ構造を辿れるように管理されているため、任意のコミットの状態に行き来できることが確認できた。

## ブランチ

ではどのようにして任意のコミットに移動したり、現在作業中のコミットを区別しているのか。

作成されたブランチは`.git/refs/heads/`以下に保存される。この中に作成したブランチ名のファイルが並んでおり、開くと単にテキストとして最新コミットのハッシュが書かれている。(テキストなのでcatコマンドなどで開ける。)
実は、ブランチとは枝分かれしたグラフの部分グラフ自体を指しているのではなく、それぞれの枝の最新のcommitオブジェクトのハッシュを表す単なるテキストである。
このイメージを持つことで、ブランチをマージする時に取り込みたい方のブランチに移動して`$git merge`するのも納得できるし、マージ後のブランチが消えるのではなく、マージコミットを作る前の最後の最新コミットであることがわかる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/45a1aea9-bba9-c026-2a84-013a13f415e8.png)

## HEAD
 
複数の進行中のブランチがある中で、どのブランチにコミットするのか、この状態を保持しているのがHEADである。
HEADは`.git/HEAD`(テキストファイル)で確認でき、ブランチへの参照が載っている。

```:HEAD
ref: refs/heads/master
```

これにより現在のブランチを区別できる。
そして、ブランチへの参照ということは、その実体はcommitオブジェクトのハッシュであり、実はHEADもコミットへのハッシュであるということがわかる。実際、`$git checkout [任意のコミットのハッシュ]`とすると、`detached HEAD`という状態に変わり、`.git/HEAD`の中身が移動したコミットのハッシュに書き換わっていることが確認できる。

```
$ git checkout a2e77fbbef72a23e6c908a0eb36c219d77e1a93b
Note: switching to 'a2e77fbbef72a23e6c908a0eb36c219d77e1a93b'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at a2e77fb testcommit
```

ブランチにアタッチされていないHEADの状態になっているので、`$git checkout master`などでブランチに戻してあげることができる。

なお、`detached HEAD`の状態でも変更を記述し、コミットすることが可能であるが、コミット後にHEADを別のコミットに移動すると、ブランチによって参照されないこのコミットは`$git log --graph`からは表示されなくなる（objectとしては残るので直接ハッシュを指定して移動したらそのコミットに移動することは可能）。

___

ここまでの操作から、普段単にブランチを変えるコマンドだと思っていた`$git checkout`コマンドというのは、実はHEADを指定したコミットに移動させるコマンドだとわかる。引数にはハッシュ自体も取れるし、実質その参照であるブランチやタグを指定することも可能である。
さらに、コマンド実行後、ワークツリーに表示されているファイルが移動先のファイルに変わっていることから、HEADを移動させるとともに、ワークツリーやインデックスを移動先のコミットの状態に書き換えるというのが`checkout`でしていたことである。
そうだとすると、例えば、`feat/A`ブランチで機能Aを実装中に、ちょっと`master`ブランチのファイルを確認したいというような状況になった時、まだ書きかけでコミットしていないファイルを全て破棄するか、途中でも移動のためにコミットしないといけない事になる。それは不便だ。そこで利用されるのが、`$git stash`である。

## スタッシュ

一時退避場所といった意味。
`$git checkout`し、HEADを変える前に、書き換けのファイルをどっかに置いておいて、後で帰ってきたらまた展開したらいいじゃんというのが考え方。確かにそうだろう。

まずは適当にファイルを編集してaddせずに、別のブランチにcheckoutしようとしてみる。
するとまだコミットしてないファイルが上書きされるから移動できないと怒られる。

```
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
        test/aaa.txt
Please commit your changes or stash them before you switch branches.
Aborting
```

スタッシュを利用するには以下。名前はつけてもつけなくてもいいし、`save`も省略可。ここでやっていることは、現在のインデックスの状態でコミットを作成し、HEADとのマージコミットを作成している。これがstashの実体。つまり、stashもコミットと同じである。

```
# スタッシュへ移動。移動後はワークツリーとインデックスはHEADの状態に戻る。
$ git stash save [スタッシュ名(任意)]

# スタッシュに送ったオブジェクトはここで一覧になって確認できる。
# on ** はどのブランチから退避させたstashかを示している。
$ git stash list
stash@{0}: WIP on master: 6844080 teststash

# スタッシュの中身は以下で見れる。stash@{0}も結局はコミットオブジェクト。
$ git cat-file -p stash@{0}

# ピンポイントにそのファイルだけ見れてもなあ、、なので差分としてみる場合
$ git stash show stash@{0} -p
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/a5c8b089-c72d-60dd-4658-6198eb155114.png)


スタッシュに送ることでワークツリー、インデックスはHEADの状態に戻り、checkout可能となる。さらに、移動した先でstashから取り出すことさえ可能だ。

```
$ git stash apply stash@{0} 
```

なお、stashから取り出してもstashには残ってしまうので、`$git stash drop stash@{0}`で削除できる。
(はじめから`$git stash pop stash@{0}`としたら削除までできる。)
ただし、コミットオブジェクトとしては残っているので、ハッシュを指定してcheckoutするとそのstashを閲覧することもできる。(gitって何かミスってもそうそうものが無くなったりしなそう。)

ここまでで、ブランチやタグ、HEAD、stash、どれもこれもがコミットへの参照であり、checkoutはどのコミットを手元の作業場(ワークツリー、インデックス)に持ってくるかを指定するものだとわかった。

なお、後述するがリモートリポジトリもローカルリポジトリからしたら1つのブランチとして表現される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/1a81a7be-982b-ddc6-1ec7-a776e1c14a8d.png)

## `$git reset`

checkoutするとインデックスやワークツリーは移動先のコミットで上書かれてしまう。
インデックスの内容は今のままで移動させたい(例えばインデックスにあげたものを削除して、HEADの状態に戻したいなど)や、特定のファイルを移動させたいといった細かい動きには`reset`を使う。

### コミットを移動する。

```
$ git reset --[soft/mixed/head] [コミットのハッシュ]
```

移動先のハッシュにはよきHEADから相対的に`n`個前という表記として`HEAD~n`とされる。1個前なら`^`もok。
オプションは3つ。デフォルトは`mixed`で、ほかに`soft`と`hard`がある。
コミットの移動に関して、HEAD以外に何を移動しますか？というのがオプションで決めるところ。
softはHEADのみ。mixedはインデックスのみ、hardは全て移動させる。
重要なのは、移動させると当然ながらコミット前のファイルには二度と戻れないということ。(インデックスに関してはがんばればあるらしい?)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/92315aa2-b76d-b7d0-7652-1a444526cdcf.png)

### ファイルを移動させる。

インデックスにあげてしまったファイルを戻したいとなった時、上の原理を使えば`$git reset --mixed HEAD`として、インデックスを丸ごと消してしまえばいいとわかる。さらに、`--mixed`オプションや引数`HEAD`はデフォルトでこの値が選択されるので、`$git reset`だけでもよい。
もし、特定のファイルだけを戻したいなどであれば、ここにファイル名を続けることで実現できる。
すなわち、`$git reset aaa.txt`のようにすると`aaa.txt`のみをワークツリーに呼び戻せる。

※ ただし、現在は`restore`コマンドがあり、それを使うことが推奨される。

```
# インデックスから特定のファイルをワークツリーに呼び戻す。
$ git restore --staged [ファイル名]

# ワークツリーの特定のファイルを初期の状態に戻す。(未着手の状態に戻る。)
# git restore [ファイル名]
```

旧来の方法で未着手の状態に戻すには`$git checkout`コマンドを使う。
(この辺のcheckoutとresetの使い分けがいまいちわからない、、 → [git reset & git checkout 使い分けまとめ](https://dev.classmethod.jp/articles/git-reset-and-git-checkout/)、[Git の Reset, Checkout, Revert の違い](https://yu8mada.com/2018/06/01/the-difference-between-reset-checkout-and-revert-in-git/))

```
# 特定のファイルを直前のコミットまで戻す
$ git checkout -- [ファイル名]
# ワークツリーを直前のコミットまで戻す
$ git checkout .
```

→ 参考

* [git reset についてもまとめてみる](https://murank.hatenadiary.org/entry/20110327/1301224770)

## `$git rebase`

gitの実体は各時点でのスナップショットであるのでいらないコミットが増えるほど容量を食っていく。他にもタイポ打ち直したり、ケアレスミスを修正したくらいのコミットならpushしたくないとなるだろう。
そうした時に一度コミットした履歴を改竄するコマンドが`$git rebase`である。

```
$ git rebase -i [ハッシュ]
```

指定したコミットオブジェクト以降のコミットをどちらかに混ぜ込んだり、無くしたりすることができる。
現時点から`n`個前とかなら、`HEAD~n`としてもok

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/0ec694e5-d9ec-2a95-37c1-f7a75065c018.png)

rebaseの編集の仕方は色々ある。
`pick` → そのコミットを採用する。
`squash` → 特定のコミットをその直前のコミットに混ぜ込む。その際、新たなコミットとしてコミットメッセージをつける。
`fixup` → 特定のコミットをその直前のコミットに混ぜ込む。新たなコミットには直前のコミットメッセージと同じものをつける。

詳細はガイドの英語を読めばいい。

```
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
```

ただし、歴史の改竄は未公開のローカルのコミットに対してのみやること。リモートが絡み始めると、他の人が迷惑を被るようになるためだめ。

## リモートリポジトリ

### 既存のリモートリポジトリから全てのデータを取得する。

```
$ git clone https://github.com/***/test.git
```

既存のリモートリポジトリからgitのグラフを丸ごと持ってきてローカルに構築。リモートリポジトリのmasterブランチの先頭がどこにあるかを示す`origin/master`が生成される。

### ローカルの変更を反映する。

```
$ git push [リモートリポジトリ名] [ブランチ名]
```

どのリモートリポジトリ(一般的にはoriginとすることが多い)に対して、ローカルのどのブランチの変更を送るかを指定する。ここでもブランチが単にコミットのハッシュであることを利用すると、実はここはHEADと書いてもいい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/21125585-9554-ed69-4d13-5ffe80815642.png)

なお、ローカルでブランチを新設しても勝手にリモート側にもそのブランチができるわけではない。ローカルリポジトリからしたら　リモートリポジトリは1つのブランチにすぎず、ブランチとはコミットへの参照でしかないから。
そこで、ローカルに新設したブランチをリモートリポジトリにも反映するには新しいブランチをつけたことを教えてあげる必要がある。

```
$ git push --set-upstream origin [新しいブランチ名]
```

## リモートの変更を取り込む。

`$ git pull` = `$ git fetch` + `$ git merge`

`pull`はとってきてマージする操作を自動でやってくれるコマンドである。
リモートにもローカルにも変更がある場合にまじ、リモートから変更をとってきて(fetch)、ローカルの`origin/master`ブランチを伸ばす。その後、ローカルでの変更との差を吸収するためにマージする。たいてい同じ箇所をそれぞれ変更していたりしない限りは成功するが、自動で判断できない場合はコンフリクトが発生し、手動で解消する必要がある。といってもどちらの変更を正としてマージコミットを作成しますか？ということなので、マージ後のあるべき姿に書き直してコミットすることを求められているということである。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406130/b5c6bab7-c43d-6ae5-b03b-4c8c0797ccf2.png)

なお、`merge`ではなく、`rebase`したい場合には`----rebase`オプションを指定してpullしたらいい。

`$ git pull --rebase` = `$ git fetch` + `$ git rebase`

また、ここまでの記述で`origin`や`origin/master`としていたが、その名称は任意に設定可能であり、別の名称をつけた物を複数用意し、リモートリポジトリを複数用意することも可能である。
リモートの設定は`.git/config`にテキストで書かれている。

```:config(抜粋)
[remote "origin"]
	url = https://github.com/***/test.git
	fetch = +refs/heads/*:refs/remotes/origin/*
```

リモートの名前（origin）、リモートリポジトリのURL、fetchする対象のrefspecが指定される。
refspecは、`<リモート側の参照>:<ローカルの書き込み先>`を意味しており、fetchする際にリモートのどのブランチをローカルのどこに対応づけて進めるかを指定している。なお、先頭の`+`は、fast-forwardでない場合でも参照を更新するよう指定している。

リモートの追加・削除は以下。

```
$ git remote add origin https://github.com/***/test.git
$ git remote rm origin
```

# 参考

* [Gitが出来るまでの歴史と将来の展望(バージョン管理システム)](https://harusamelab.com/2019/03/17/git%E3%81%8C%E5%87%BA%E6%9D%A5%E3%82%8B%E3%81%BE%E3%81%A7%E3%81%AE%E6%AD%B4%E5%8F%B2%E3%81%A8%E5%B0%86%E6%9D%A5%E3%81%AE%E5%B1%95%E6%9C%9B%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E7%AE%A1/)
* [Gitのつくりかた](https://engineering.mercari.com/blog/entry/2015-09-14-175300/)
* [Gitの分散version管理の仕組み](https://www.kaitoy.xyz/2015/12/31/git-dvc/)
* [Gitのコミットの裏側で起こっていること](https://made.livesense.co.jp/entry/2017/08/22/080000)
* [難しいGitコマンドは、仕組みから理解してみよう](https://qiita.com/_ha1f/items/2dca1047c57d4f0bd465)
* [GitのHEADとは何者なのか](https://qiita.com/ymzk-jp/items/00ff664da60c37458aaa)
* [git reset についてもまとめてみる](https://murank.hatenadiary.org/entry/20110327/1301224770)