gitの勉強のために色々調べてメモしたので大開放。ソースは主に公式リファレンス。
そもそもgitとは?
分散バージョン管理システム(DVCS, Distributed Version Control System)の一種で、全てのデータをスナップショットのようなものとしてローカルに持つ。それにより、サーバ上のデータがクラッシュした場合もローカルリポジトリから復元することができる。また、作業時にサーバ接続が必要ないため、高速で安定した開発を行うことができる。
- gitはコミット毎にファイル状態のスナップショットへの参照を格納する。ファイルに変更がない場合はそれ以前のスナップショットを直接参照する。
- gitはファイル単位ではなく、全ファイルのコンテンツを追跡する。コミットには各ファイルの変更内容やファイル名、リンクなどなどが格納される。
- gitはハッシュ値でコミットを管理し、それは完全性を持つ。ハッシュ値によるチェックが通らない場合は変更が行われない。
- ワーキングツリー、ステージングエリア、リポジトリ
ワーキングツリーの変更をステージすると、次のコミット情報(変更内容、ブランチ名、ファイルリンクなどなど)が記録された単一ファイル(ステージングエリア)に保存される。コミットをすると、ステージングエリアの変更をスナップショットとしてリポジトリに格納する。ステージングエリアにはgit add時点での変更情報が記録されているので、同一ファイルをまた編集すると、changes to be committedとnot stagedに同一ファイルが出現する。 - gitは直近のスナップショットに存在するファイルのみを追跡している。
untracked, unmodified, modified, stagedというようなファイルステータスが存在する。新しく追加された追跡されていないファイルはuntracked filesとして表示される。 - ブランチ名は最新コミットの別名!
git checkout 2c34jfopやgit show masterなどのように、ブランチ名とリビジョンを入れ替えたりできる。 - ブランチをつくる=コミットの別名をつくる
git branch topic masterはmasterの別名のtopicをつくる。そこから何らかのコミットをすればtopicブランチが枝分かれして伸びていく。 - git reflogとgit resetでresetミスを取り消す
http://d.hatena.ne.jp/idesaku/20091106/1257507849
こういった修正がきくのはローカルリポジトリに限る。心構えとして、pushする時は細心の注意を払うことを忘れずに。
コマンドや使い方
- git config --global
/etc/gitconfigを編集する。--globalをつけると今後全てのgit操作でこの設定が有効になる。 - git diff
変更したけどまだステージされていない変更を見る。git diff --cachedにするとステージされた変更を見る。 - git rm
削除をステージングし、追跡対象から外し、ファイルを削除する。普通に削除するだけでは追跡対象から外れず、いつまでもリポジトリ上に残ってしまう。git rm --cachedで追跡対象から外し、ファイルは残る。 - git reset [commit] [file]
HEADの位置、インデックス(ステージングエリア)、ワーキングツリーなどをcommit位置に変更する。git addを取り消す際にgit reset HEAD [file]と打つことで、HEADとインデックスをHEAD(前回コミット状態)まで変更する。インデックスがHEADに変更されるのでステージングから外れるが、ワーキングツリーはそのままなのでファイル変更は残ったままになる。 git resetについて - git pull [remote] [branch]
remoteから特定ブランチをfetchし、現在のブランチにマージする - git revert
特定のコミットを打ち消すコミットをする。デフォルトで--hardがつく。 - git rebase -i
コミットの歴史を改変する。git rebase -i HEAD~4などで4つ分(5つ前のコミットまで戻る)の歴史を改変する。 - git checkout
特定のツリー(コミット)やインデックス(ステージングエリア)の状態に合わせてワーキングツリーを変更する。HEADの位置も移動する。git checkout [commit]をするとdetached HEADになる。git checkout -- [file]でワーキングツリーのファイルをインデックスの状態に変更する。一方で、git checkout HEAD -- [file]でインデックスとワーキングツリーをHEAD(前回コミット状態、インデックスには何もなし)の状態に戻す。
gitの流れ
gitはcommitやpushなどのコマンドを通して操作する。それぞれのコマンドの内部動作と流れは以下のようになっている。
- ファイルを作成・編集する
ファイルを作成すると、ファイルのコンテンツを記録したblobオブジェクトが作成される。新しくファイルを作成したらblobオブジェクトをひとつ作成するが、既存のファイルを編集して保存しても新しいバージョンのblobオブジェクトとして新しいblobオブジェクトファイルが作成される。 - ステージングする
作成・編集したファイルをステージングエリアに登録する。ステージングエリアの実体は単一のファイルである。修正されたファイル情報を保存し、新規作成ファイルは追跡ファイルとして登録する。差分などを追加するわけではなく、追跡ファイルへのスナップショット情報を持っている(推測:.git/indexのファイル内部には全てのファイル名が確認できる) 具体的に何かオブジェクトを作成するわけではない(推測:treeオブジェクトもblobオブジェクトも別タイミングで作成される) - コミットする
git commitコマンド実行時にtreeオブジェクトとcommitオブジェクトを作成する。ステージングエリアにあるファイル(修正したファイル)を取得すると公式には載っているが、実際に保存して永久化するのはスナップショットであり、変更ファイルリストや差分ではないと考えられる(推測:diffを計算するという表現、commitオブジェクトの構造、ファイルリストが見当たらないことからこう考えるのが自然)
Fast-ForwardとNon Fast-Forward
-
Fast-Forward
e.g) masterからブランチtopicを切り出し、複数回コミットし、枝分かれからなにもコミットしていないmasterにマージする
topic = master + commit1 + commit2 という状態なので、HEADをtopicに移動させただけ。
ブランチをマージしたというログが残らない(topicだったコミットログは残るが、直接masterにコミットしたのと同じログになり、マージしたというログが残らない。
git merge --ff-only -
Non Fast-Forwad
e.g) masterからブランチtopicを切り出し、両者でそれぞれコミットがあった
Fast-Forwardではないので移動だけでは無理。topic側でのコミットをまとめてmasterとの差分を計算し、"マージコミット"としてmasterにコミットする。
git merge --no-ff
git rebaseとgit mergeによるマージ
-
git rebase
自身側のコミット内容と共通の祖先とのdiffを取得して新しいコミットのパッチとして計算し、対象に新しいコミットとしてひとつひとつ当て、HEADの位置を対象側に移す。しかし、新しいコミットのパッチは元のコミットとは一つ前のコミット情報が違うので、基本的には別物。既にpushされているブランチは一連のコミット情報を持っている。rebaseした情報をpushするとコミット情報がおかしくなり、pushできない。共有のブランチではrebaseしてはいけない! -
git merge
自身と対象の最新のスナップショット、共通の祖先からマージする。対象側のコミット内容をひとつのコミットとして自身に当てる。マージされたというログが残るので推奨。
gitの参照
gitのHEADやブランチなどは対象ハッシュへのポインタで構成されている。git update-refでその情報を更新し、.git/refsに格納する。git branchのコマンドは実質git update-refを実行しており、ブランチ名とハッシュへのポインタを格納する。
HEADはHEADファイルと呼ばれるものを実体としており、以下の様な形式で保存されている。
$ cat .git/HEAD
ref: refs/heads/master
リモートも同様に、特定のリモートサーバが最後にどのブランチと通信したかを格納している。
$ cat .git/refs/remotes/origin/master
ca82a6dff817ec66f44342007202690a93763949
gitオブジェクト
gitはオブジェクトにファイルのコンテンツやポインタを格納することで情報を管理している。全てのオブジェクトはSHA1ハッシュ値で管理される。
- blobオブジェクト
ファイルが保存・編集された時点で、そのファイルのコンテンツを意味するblobオブジェクトが作成される。blobオブジェクトはそのファイルのコンテンツをまるごと含んで1blobオブジェクトファイルになり、特定のファイルを編集した場合も新しくblobオブジェクトファイルが作成される。 - treeオブジェクト
ファイル構造とblobオブジェクトをつなぎあわせる。treeオブジェクトに含まれるblobは追跡しているファイル&blob全てである。これは以下のコードを実行してtreeオブジェクトの中身を見てみるとわかる。
git cat-file -p master^{tree}
100644 blob af717f31099585f2648b72df31288dc6491b2b33 Main.pl
100644 blob 5934bee82dd230a731e9f3972593d97c88cee308 aaa.pm
100644 blob 3039a69dad5bbe817c5563d9ebb1d4081dbb0f00 ff.pl
100644 blob b5963e32275d74186ba9ec14674a9ff9a3d154f7 hello.pl
100644 blob dfdbeecf0cc25fb779436f0580c974d3de3edcef sub.pl
- commitオブジェクト
git commitコマンド実行時にcommitオブジェクトが作られる。commitオブジェクトにはtime, author, committer, tree object pointer, last commit pointer, commit messageが含まれる。
オブジェクトのパック
gitではコンテンツの最小単位としてblobでファイルのバージョンなどを管理している。例として、memo.txtの最初のバージョンのblobが作成され、memo.txtが変更さらた時にはまた新たな完全なblob(差分ではなく、コンテンツまるごと)が作成される。これはディスク容量の無駄であり、gitはこれらをひとつのバイナリファイル(パックファイル)に詰め込むことがある。git gcを使うことで何もコミットされていないblobは遊離とされ、パックファイルに詰め込まれる。パックファイルには全てのコンテンツが詰め込まれ、インデックスファイルはそれに対する迅速なアクセスを提供する。
パックファイルへのコンテンツの詰め込み方は以下のようになる。
- 最初のバージョンは完全なかたちで格納される
- その後からは差分(増分とドキュメントにはあるが、削除された行などの行方を考慮すると、差分であるべきでは? 解決:原著ではdeltaとなっているので、差分が正しい)を格納する
- 直近のバージョンは迅速なアクセスを提供するために完全なかたちで格納される