git再入門
この記事は,
- gitを用いた共同開発をしたことがある
- 操作ミスの直し方が分からなくて詰む
- コマンドが色々あるらしいけど覚えきれない
くらいの方に向けたgitの解説記事です.
gitの基本的な構造に触れつつ,各コマンドが実際何をしているかを解説するのが主な目的です.
また,コマンドを知らなくてもggる時の適切なキーワード選びが出来るようになれば幸いです.
第一章・第二章でgitの基本的構造,第三章でコマンド解説をします.
第一章 git repositoryの構造
git repositoryとは,所謂「ファイルやディレクトリの歴史」のことです.
SourceTreeなどで目にするコミットグラフはgit repositoryを可視化したものです.
第一章では,git repositoryの構造とともに,関連する用語として
- commit
- branch
- tag
を解説します.
commitはsnapshot
端的に言うとcommitは管理下にある1 ファイルやディレクトリのsnapshotです.
よくある勘違いとして「commitは前のcommitからの差分」という説がありますが,違います.
snapshotとは「その時の状態の複製」です.すなわち,ファイルが変更されてcommitされる度にそのコピーが作成されアーカイブされています.2
さて,このsnapshotは撮るだけでは意味がなく,参照できなくてはいけません.
そのための情報をsnapshotと一緒にまとめたものがcommitです.
各commitは以下の情報を持ちます.
- snapshot
- commit hash (id)
- parent commit
- commit message
commit hash (id)は各commit固有の文字列です.40文字程度の英数字から成りますが,ほとんどの場合先頭7文字で識別可能です.
parent commitはそのcommitの派生元となるcommitのことです.大抵parent commitは一つですが,時々0個や2個以上の場合もあります.3
commit messageはcommit時に付与された,そのcommitの説明です.commit messageは後で見る人(∋自分)のためにきちんと書きましょう.
一旦commitが作られた後は,これらの情報が変更されることは絶対にありません.
commit message一つ変更するにしても,必ず新しいcommitが作成されます.
branch,tagはcommitを指すポインタ
branchやtagはどちらも「あるcommitへの参照」に他なりません.
より正確に言えばcommit hashのエイリアスです.
情報の勉強をした人ならばポインタと言えばピンと来るかもしれません.
特にブランチについては「元となるブランチから分岐した時点からの歴史」と勘違いされることも多いですが,これも違います.
コミットグラフを作成するときを考えてみましょう.各commitはparent commitを知っていますので,どのcommitもやがてはroot commitに辿り着くことが出来ます.しかし,自身をparent commitとするcommitは知りませんので,各派生先の最先端のcommitを別途参照出来る必要があります.その役割を果たすのがbranchです.
branchの参照先は,現在の参照先のcommitから派生するcommitが作成される度に更新されてゆきます.
しかし,特定のcommitへの参照を残しておきたい場合もあります.その場合はtagを用いることで,そのcommitへの参照を普遍的に持ち続けることが出来ます.
branch,tagはまとめてreferenceと呼ばれます.
第二章 commitが出来るまで
第一章では「commitが出来てから」の話をしましたが,第二章では「commitが出来るまで」の話をします.
ここで解説する用語は以下の三つです.
- worktree
- index
- repository
ファイルに変更を行ってからそれをcommitに含めるまでの流れを解説します.
worktreeとは,作業中のディレクトリの状態
第一章でcommitはsnapshotを持つと解説しましたが,考え方によっては現在作業中のディレクトリ自体も一つのsnapshotであると言えます.これがworktreeです.
すなわち,worktreeとは作業中のディレクトリの状態そのもののことです.難しく考える必要はありません.
ファイルを編集して保存すればその内容は即座にworktreeに反映されます.
indexとは,commitとしてアーカイブされる前のsnapshot
現在編集中のworktreeの状態と,アーカイブとして残したいsnapshotの状態は必ずしも一致しません.そこで,「commitとしてアーカイブされる前の,編集可能なsnapshot」としての役割をindexが担います.
例えば,機能を開発している途中にcommit間の差分が大きくなりすぎると感じた場合は現在のworktreeで行った変更を分割したいと考えます.この時,その状態まで手動でworktreeの状態を戻してしまうと二度手間になります.indexの存在はこういった状況を解決してくれます.
結果として,worktreeの一部または全部の状態をindexに反映させてゆき,indexがアーカイブしたいsnapshotになったらcommitとしてアーカイブする,という流れでcommitが作成されます.
補足,HEADについて
コミットグラフで,HEADという名前を目にしたことがあると思います.
HEADはreferenceの一つで,「現在のworktreeの元となるcommit」への参照を持ちます.
しかし通常HEADはいずれかのbranchと同じcommitを指すこととなっており,その場合HEADが指すものはcommit hashではなくbranchです.
そうでない場合はHEADは他のreferenceと同じくcommit hashを指しますが「detached HEAD」と警告されます.
第三章 コマンドの実態
第一章,第二章ではgitの基本的な構造を解説してきました.
そこでの内容を踏まえて,改めていくつかのコマンドが何をしているのかを解説したいと思います.
add
worktreeの状態をindexに反映させます.
オプション等の指定により,
- 管理下の全てのファイルをindexに反映
- 一部のファイルをindexに反映
- あるファイルに施した変更の一部をindexに反映
などが出来ます.
commit
現在のindexのsnapshotを持つcommitを作成します.
なお,gitの文脈において「commit」という用語は
- 上記で説明したcommit(snapshotを持つオブジェクト)のこと
- commitを作成する操作のこと
と二通りで使われます.
第一章,第二章においては一回だけ2の意味で用いましたが,その他は全て1の意味で用いています.
log
その名の通り,commitの履歴を表示します.
アルゴリズム的には,現在のcommitからparent commitを辿っているに過ぎません.
なお,SourceTreeのような表示にしたい場合以下のオプションとともに用いるのがおススメです.
git log --oneline --graph --decorate --branches --remotes
私はこれをgl
としてエイリアスを貼っています.表示結果は例えば以下のようになります.
$ git log --oneline --graph --decorate --branches --remotes
* 83c7494 (HEAD -> master) Merge branch 'develop'
|\
| * bf290e8 (develop) poyo
* | 00c5833 just copied
|/
* 670f4a6 yoyoyo
* 784fd6a increment
* 597fcf9 create po
* f020336 initial commit
Windowsだと「\」が「¥」になるので辛いです.
checkout
checkoutは,「worktreeの一部または全部をあるcommitの状態に変更する」コマンドです.
git checkout ${brahch}
という用法で最もよく目にすると思います.
よく「branchを切り替える」と説明されますが,今までのことを踏まえると,この場合checkoutは「worktree全体をあるbranchの指すcommitのsnapshotに変更し,HEADをそのcommitまで変更する」という操作を行っています.
また,git checkout ${commit hash} ${file}
という用法もあり,これは指定したファイルを指定したcommitの状態に戻します.こちらは若干マイナーですが,むしろ定義に近い操作です.
その他,git checkout -b ${new_branch_name}
という用法もあります.
これは同じcommitを参照するbranchを新しく作成します.
merge
図解しているサイトを参照していただけると,merge,cherry-pick,rebaseの動作についてスムーズに理解できるかと思います.
git merge ${commit hash}
とすることで現在のcommitと指定したcommitの両方の状態を統合したcommitを作成します.
正確に言えば,まず現在のcommitと指定したcommitの分岐元(最も新しい共通の祖先)となるcommitを探します.分岐元のcommitと現在のcommitのdiff,分岐元のcommitと指定したcommitのdiffをそれぞれ取り,共存可能であればそれらを取り込みます.
一方,現在のcommitと指定したcommitで,あるファイルについて異なるdiffが出てしまうと競合が起こり得ます.これは手動で解決するかgit checkout --ours
などのコマンドを用いて解決するほかありません.
なお,余談ではありますがmergeによって作成されたcommit (merge commit)は親commitを二つ持ちます.
cherry-pick
図解しているサイトを参照していただけると,merge,cherry-pick,rebaseの動作についてスムーズに理解できるかと思います.
git cherry-pick ${commit hash}
とすることで特定のcommitの内容を現在のbranch上に適用できます.mergeとの違いを少々強引に説明すると,歴史を考慮するのがmerge,考慮しないのがcherry-pickです.
cherry-pickは特定のcommitと同じsnapshotを持つcommitを現在のbranchの先頭に続けて作成しようとしますが,mergeでは上記の通り共通の祖先からのdiffを考慮した統合をしようとします.ですから,例えばあるbranchにおいてgit merge HEAD^
とした場合はalready up to date.
と言われ何も変更されませんが,git cherry-pick HEAD^
とした場合はconflictします.また,mergeの場合は現在のcommitと指定したcommitの両方を親に持つのに対しcherry-pickの場合は現在のcommitしか親に持ちません.
rebase
図解しているサイトを参照していただけると,merge,cherry-pick,rebaseの動作についてスムーズに理解できるかと思います.
git rebase ${commit hash}
とすると,現在のcommitと指定したcommitの分岐元を指定したcommitに変更します.
しかし,実はrebaseはcherry-pickの連続適用に過ぎません.
reset
git reset ${commit hash}
とすることで,現在のbranchの参照先を指定commitに変更します.
よく「git reset --hard
をするとcommitが消える」といった誤解がありますが,commitは消えません.実は,ORIG_HEAD
というところに前にHEADが指していたcommitのhashが保存されています.ですから,git reset --hard ${any_commit_hash}
をした後にgit reset --hard ORIG_HEAD
をすると元通りになります.
reflog
git reflog
は,HEADの移動ログです.
$ git reflog
00c5833 HEAD@{0}: reset: moving to ORIG_HEAD
597fcf9 HEAD@{1}: reset: moving to 597fcf9
00c5833 HEAD@{2}: rebase finished: returning to refs/heads/master
00c5833 HEAD@{3}: rebase: poyopoyowa-i
670f4a6 HEAD@{4}: rebase: checkout 670f4a6
${commit hash} HEAD@${number} ${action detail}
というフォーマットで表示されていきます.
ですから,例えば「git reset --hard
した後に一つcommitをしてしまったけどやはりresetを取り消したい」という場合,ORIG_HEADはもはや使えませんがreflogから希望のcommit hashをたどることが出来ます.
発展編
この章の内容はgitを利用する上でほとんど必要ありませんので,こたつでみかんでも剥きながら流し読みしていただければと思います.
git object
実は,gitのcommitやsnapshotは全てgit objectと呼ばれるものから成っています.例えば,あるcommitのhashを用いてgit catfile -p ${commit hash}
と打ってみてください.
すると,以下のようにtreeやparentという表示とともにhash値が表示されると思います.
$ git cat-file -p 83c7494dcbf4c01cae5977ef47b08921fc7077bd
tree e8e20230a07642ef8e1d1905c4b4133f6bfce750
parent 670f4a6f2101192e5f39fe7c94a8504e374d1a55
author Namazu <${email address}> 1577514290 +0900
committer Namazu <${email address}> 1577516327 +0900
poyopoyowa-i
treeはそのgit repositoryのrootディレクトリを示すgit objectで,parentはそのcommitの親commitのhashです.
treeのhash値についてgit cat-file -p ${tree hash}
とするとそのディレクトリに含まれているディレクトリ(tree)やファイル(blob)が表示されます.これらにも同じようにhash値が振られています.
$ git cat-file -p e8e20230a07642ef8e1d1905c4b4133f6bfce750
100644 blob 31a6b51d9cc6fda577d58b91ba50dc8626c865e4 po.txt
100644 blob 2df961d8030ca4360795e9608c756bdbcbc190b5 yo.txt
.gitディレクトリ
.gitディレクトリの中にはそのrepositoryを管理するために必要な情報が全て含まれています.
ls .gitとやってみると,
COMMIT_EDITMSG
HEAD
ORIG_HEAD
config
description
hooks
index
info
logs
objects
refs
といった内容が表示されます.
HEADの中身やrefs/headsの中身,objectsなどを通常のcatコマンドなどで覗いてみると面白いかもしれません.
鉞の飛ばし先
鉞歓迎いたします.正確な情報を発信したいので,誤りを見つけた場合ご指摘いただけますと幸いです.
こちらの記事へのコメントまたはTwitterにて@blonde_namazu宛にお願いいたします.
参考文献
-
Git Documentation
Gitの公式ドキュメントです. -
サルでも分かるGit入門
有名で説明がわかりやすいGit解説サイトです. -
Gitのマージを図解する
マージなどの図説が分かりやすかったです. -
[git reset (--hard/--soft)]ワーキングツリー、インデックス、HEADを使いこなす方法
resetコマンドの動作の解説のため,worktreeやindexなどについても触れつつ図説されています.
基本的にGit Documentationに従いつつ,他のサイトも理解の助けとして参考にさせていただきました.