Gitの操作と概念

6 min read読了の目安(約5800字

中級レベルぐらいのGitコマンドと、それやったときにGitの裏側でどういう処理動いてんのかをきちんと理解しようと思って書きはじめた記事です。
想定する対象読者はGitの基本はおさえてるけど雰囲気で使ってるよ、という人。

コマンドに対して、Gitの裏側ではこんな処理をしているよ、というのを書いていければいいかなと思っております。

理解度チェック

最初にGitの理解度チェックをしたいと思います。
GitはGit管理下の変更をどのように管理しているでしょうか?
三択で選んでください。

  1. バージョンごとにバックアップを作成する
  2. バージョンごとの差分のみを保存する
  3. バージョンごとのスナップショットを取得する

正解は3です。
僕はついさっきまで2だと思ってました。
一回スナップショット取得してるっていうの読んでたんですが、ちゃんと理解してませんでした。

Gitの仕組みをわかっていなかった

Gitのお勉強で最初に見るのは、下記の図だと思います。


こちらから

ワークツリーからインデックス(ステージング環境)にaddして、インデックスからcommitすることでバージョン管理下に置かれる。
これがGitを操作するときの流れですね。

これにコマンドをつけると、下記のようになるかと思います。


こちらから

あとローカルリポジトリとリモートリポジトリがあって、push/pullの関係でやりとりをします。
リポジトリ内ではbranchの切り替えが可能で、これが開発効率をあげます。
修正が衝突した場合はコンフリクトとして検知でき、衝突しなければ自動マージが可能。

これが基本的な理解で、このぐらいわかっていれば普通の開発は困りません。
ただ、高速に動作する仕組みをやはりちゃんと知っときたいなあと思うようになり、調べました。

Gitの仕組み

Git管理下に置かれたファイルは、そのまま保存されるわけではありません。
Gitの仕組みを支えてるのは、下記のシステムファイルです。

  • blobファイル
  • indexファイル
  • treeファイル
  • commitファイル

それぞれのファイルの詳細に触れるとそれだけで大作になってしまうので下記に譲ります。
ただ説明で触れるタイミングでちょっと内容に触れます。

Gitのコミットの裏側で起こっていること
3.1 Git のブランチ機能 - ブランチとは

それでは以下コマンドベースで章立てしていきます。

git add

ワークツリーからインデックスに「移す」コマンドというのが今までの僕の理解でした。
しかしGitが裏側でやっていることは違います。

実際はblobファイルを作成し、それをindexファイルに登録しています。

blobファイルの実体は.git/objectsにつくられます。
Finderかターミナルで確認して頂ければ、.git/objectsに謎のディレクトリがいっぱいあるのがわかると思います。
blobファイルはバイナリファイルで、git addされたファイルはそのファイル形式に関わらずバイナリ圧縮されて管理されます。
ファイル名はSHA-1の40文字のハッシュ値が振られます。
ディレクトリには先頭2文字、ファイル名には38文字が機械的に設定されます。
Git管理の対象の実体ファイルはこいつです。

そしてindexファイル。
indexファイルには管理対象のファイルの最新バイナリのハッシュ値が指定されます。
ハッシュ値を上手く使うことで、参照、参照で上手いこと処理するのがGitの高速性の秘密です。

Gitのステージング環境の実体は、このindexファイルです。
仮にindexファイルから任意のファイルを手で消せば、ステージング環境から消せます。
ただその場合もblobファイルは残っており、何らかの手段でindexファイルを復元できれば再び参照可能です。

git commit

よくこんなコミットツリーを見せられて、イマイチピンと来ていませんでした。


こちらから

「コミット、コミットっていうけど、実際何してんだ?」という疑問はずっと頭にありました。
具体的には下記をやっています。

  • treeファイルの作成
  • commitファイルの作成
  • HEADの変更

重要なポイントは、↑すべてが参照を持っているだけのファイルで、メタデータの変更のみで完結しているところです。

commitの実体

git cat-file -pというコマンドでGitのシステムファイルを出力できるので、treeファイル/commitファイル/HEADの中身を見てみました。

まずtreeファイルはこんなんです。

$ git cat-file -p ffffffffffffffffffffffffffffffffffffffff
100644 blob ffffffffffffffffffffffffffffffffffffffff	.gitignore
100644 blob ffffffffffffffffffffffffffffffffffffffff	README.md
040000 tree ffffffffffffffffffffffffffffffffffffffff	SystemFiles

(※別にハッシュ値隠す必要もないんでしょうけど、一応)

つづいてcommitファイル。

$ git cat-file -p HEAD
tree ffffffffffffffffffffffffffffffffffffffff
parent ffffffffffffffffffffffffffffffffffffffff
author xxx
committer xxxx

parentに親commitのハッシュ値が入っており、treeにはコミットしたときのtreeファイルのハッシュ値が入っています。

最後にHEADファイルですが、これはもう普通のテキストファイルで、肉眼で見れます。
.git/HEADを開くと、

ref: refs/heads/main

↑これが一行だけ書いてあります。
これがディレクトリ名になっているので、.git/refs/heads/mainを開いてみましょう。
これもテキストファイルなので、肉眼で見れます。

中身はハッシュ値が入っています。
このハッシュ値が最新のcommitファイルに対応しています。

よくGitコマンドで「HEAD」と指定するのは、ここを見ています。
手で書き換えると色々バグるのを確認できるかと思います。

git branch / git checkout {branch-name}

勘のいい方ならここまでの説明で気づいたかもしれませんが、HEADの参照している先とブランチ名は対応しています。
何を隠そう、ブランチの実体はcommitファイルへの参照なのです。

.git/refs/heads/の中にブランチ名に対応したファイルがあり、それぞれハッシュ値が入っているはずです。

git branch -vv

詳細に仕組みを解説するのはここまでで、後は便利コマンドをひたすら紹介するだけになります。

リモート追跡ブランチが見れます。

$ git branch -vv
* feature/watashi ffffffff [origin/develop] message

git branch -u

リモート追跡ブランチはアップストリーム(上流)ブランチとも言うそうで、というか普段は追跡ブランチって言ってましたが、
-uはupstreamのuみたいです。

下記みたいな感じでよく使います。

$ git branch -u origin/develop

git fetch

リモートの最新状態を取得します。
git pullを使うことの方が多い。

git merge

まだ取り込まれていない最新コミットをマージします。
引数の指定なしで、上流ブランチが設定されていると、現在のブランチに最新のコミットをマージします。
ただfetchを忘れていると、微妙にリモートより古くなって、デグレの温床になります。

git pull

git fetch + git merge

git log --oneline

最新のコミットから順に、ハッシュ値(の最初の10文字)とコミットメッセージを一行ずつ表示します。
「あれ、このブランチどこまでコミット取り込めてるんだ?」という確認でよく使います。
「git log --oneline -2」で2行まで表示、などしぼりこめます。

git checkout {filename}

ブランチの切り替えに使うcheckoutはindexに登録したファイルを取り消す場合にも使います。
そこに気持ち悪さを感じる人には、こちら。

git switchとrestoreの役割と機能について

git reset --soft HEAD^

※git resetは危ないコマンドなので、特殊な訓練を受けていない人は使わないようにしましょう。

コミットしたものの、何かミスって取り消したいときに使います。

git reset --hard HEAD

※git reset --hardはより危ないコマンドなので、より特殊な訓練を受けていない人は使わないようにしましょう。

編集してる内容を全部取り消したいときに使います。
なおミスって--hardでresetしても、git reflog使って頑張れば復旧できますが、結構ギリギリのオペレーションになると思うので、慎重に--hardを使いたいですね。

git stash save "xxx"

初学者がgit reset使うぐらいなら、とりあえずstashしておくのがオススメです。
作業中の内容を全部脇におけます。
脇においた編集内容を戻したいときは、git stash list, git stash apply(popはあまり使わない)です。

git cherry-pick {hash-value}

コミット単位で取りこみます。
こんなコマンド、イキった奴しか使わんやろと思ってましたが、2020年1回だけ使う機会があって、ユースケースはありました。

コンフリクトしたブランチで、コンフリクトの原因となっているコミット以外は問題ないコミットで、そこは取り込みたい、みたいな状況だったので、cherry-pickを使いました。
(具体的に言うとUnityのSceneがコンフリクトして、その後に普通のスクリプトのコミットが何個かあった)

git mergetool

コンフリクトしたときにターミナル上でマージできます。
なるべくIDEでやっちゃいたい(見やすいから)ですが、どうしようもないときはvimdiffでやります。

Gitコンフリクト解消ガイド(git mergetoolの使い方)

コンフリクトの解消を諦める

コンフリクトしてニッチもさっちも行かなくなったとき、今までとりあえずコミットしちゃって、その後ブランチ捨てるということをやっていたんですが、
git reset --hard HEADを使うと、mergeを試みる前に戻れることに気づきました。

git clean

コンフリクトした後に残った.origファイルを消すときに便利。

git clean -nで対象ファイル名を確認、git clean -fで削除です。
引数がないとコマンドが入らない安全仕様なので、結構安全です。
Gitは-fつけないとそんなにヤバいことにはならないようにできているので、触りはじめて一年ぐらいはとりあえず-fは何があっても指定しないぐらいに思っていてもいいような気がします。