Git と GitHub を体験しながら身につける勉強会行ってきた
9/18(土) 15:30~ GitとGitHubを体験しながら身につける勉強会(名古屋) : ATND
行ってきました。
なんかいろいろと話すことになったんですけど、あの場で言いそびれたこととか、もっとこう説明してればよかったなぁ、って部分の補足も兼ねたエントリです。
長文注意。
ショートカット
- git add の話
- git add -p/git reset -p の話
- リビジョン番号がない話
- ブランチの話
- git-completion の話、__git_ps1 の話
- コミットの指定の話
- reset の話
- rebase と merge の話
- 公開したものの rebase の話
- stash の話
- TortoiseGit、HG、SVNのはなし
- 全体を通して
git add の話
Git と SVN では、add に限らず、同じ名前のサブコマンドでも意味が異なるものがいくつかあります。
その中でも add は一番使うであろうサブコマンドなので、言及される場面も多いのだと思います。
また、add の違いは Git 特有の index (もしくは staging area) にもかかわってくるため、難しく考えてしまう人も多いかもしれません。
しかし、「コミットの粒度を小さく保ち」、「commit -a を使えば」、SVN の add と似た、「add は最初の一回だけ」という操作感を得ることも可能です。
ただしこれではもちろん index の恩恵は受けることができませんし、SVN と違い commit 時にファイルを指定することができませんをしませんので、全く同じというわけでもありません。
それでも、
- Git では細かく分割したコミットを後から簡単にひとつのコミットにまとめることができる
- 複数の修正を一度に行わなければ、index はほとんどの場合不要で、常に working tree 全体を commit すればいい
ということを考えれば、この方法は十分「アリ」だと思います。
git add -p/git reset -p の話
「hunk レベルで add」ができる -p オプションには言及しましたが、同様に「hunk レベルで reset」も可能です。
これも、reset に -p を付けるだけです。
- p を付けると hunk ごとに y,n,q,a,d,/,K,g,e,? のどれを選ぶのか聞かれますが、それぞれの意味は次の通りです (add の場合 stage、reset の場合 unstage)。
文字 | 意味 |
---|---|
y | この hunk を stage/unstage する |
n | この hunk を stage/unstage しない |
q | この hunk を stage/unstage せず、残った hunk もすべて stage/unstage しない |
a | この hunk と、同じファイルの残りの hunk を stage/unstage する |
d | この hunk と、同じファイルの残りの hunk を stage/unstage しない |
g | hunk を選択して移動する |
/ | 正規表現で hunk を検索して移動する |
j | この hunk の stage/unstage の決定をせず、次の未決定の hunk に移動する |
J | この hunk の stage/unstage の決定をせず、次の hunk に移動する |
k | この hunk の stage/unstage の決定をせず、前の未決定の hunk に移動する |
K | この hunk の stage/unstage の決定をせず、前の hunk に移動する |
s | この hunk を小さい hunk に分割 (split) する |
e | この hunk を手動で編集する |
? | ヘルプを表示する |
g や / は、単独で入力すると移動先の hunk や検索する正規表現の入力を求められます。
そうではなく、「g2」や「/hoge」のように直接指定することも可能です。
このうち一番使うのは y と n だと思うので、とりあえず y と n だけ覚えておけばいいです。
vim 使いならほかのもいくつかすぐに覚えてしまうと思います。
-p オプションを試すには、以下の手順でどうぞ。
- 何かファイルを用意する。ここでは hoge.txt を作ったとする。
- 4行程度何かを書く。ここでは hoge, piyo, foo, bar と書いたことにする (, で改行のつもり)。
- git add hoge.txt して git commit -m "add hoge.txt" する。
- hoge.txt の各行の次に、何か追加する。ここでは、この操作で各行が hoge, aaa, piyo, bbb, foo, ccc, bar, ddd となったことにする。
- git add -p hoge.txt して、最初のプロンプトで s を選択する。
これで色々と試せます。
戻したければ、git reset で git add -p hoge.txt する前まで戻すことも可能です。
リビジョン番号がない話
これは全体では話してないのですが、ちょっと話題に上がったのでついでに。
Git では Subversion と違い、リビジョン番号のような連番は振られません。
もし連番を振るにしても、リポジトリが複数になるため、振った連番は一個のリポジトリに対してのみ一意となるので、あまり役に立ちません。
そのため、Git ではわかりやすい連番を各コミットに割り振るのではなく、各コミットの内容から計算されるハッシュ値をコミットの識別に使っています。
ハッシュ値はコミットだけでなく、tree オブジェクト (ディレクトリに対応) や blob オブジェクト (ファイルに対応)、tag オブジェクト (注釈付きのタグに対応) にも用いられています。
ここでは詳しくは説明しませんが、これら Git のオブジェクトがハッシュ値により識別可能であることにより、
- ハッシュ値はそのオブジェクトの内容からのみ決定されるため、Git のオブジェクトはすべてイミュータブル
- オブジェクトの同一性の比較が非常に高速
- ハッシュ値が同じであれば、リポジトリをまたいですらそのオブジェクトが同一のものであるとわかる (たとえそれがインターネット越しであっても、ハッシュ値のみの比較で OK)
などという利点があります。
commit オブジェクトは内容に日時も含むため、同じに見えるコミットでも別のハッシュ値となりますが、tree や blob オブジェクトのハッシュ値を見るとわかりやすいでしょう。
オブジェクトのハッシュ値を見るには、git cat-file -p を使うのが便利です。
例えば、適当なリポジトリで git log -1 --oneline した結果が
bf58771 add hoge.txt
だったとしましょう。ここで、先頭の bf・・・がハッシュ値の先頭 7 桁になっています。
ハッシュ値は対象としているリポジトリ内で一意に決定することができるのであれば、最短で先頭からの 4 桁のみの指定で大丈夫です。
7 桁もあれば大抵の状況では一意に決定できるので、このように短い形式のハッシュ値はよく使用されます。
ここで、このハッシュ値が直近のコミットのハッシュ値 (正確には commit オブジェクトのハッシュ値) となります。
このオブジェクトの詳細を見るために、git cat-file -p bf58771 を実行すると、
tree ae59f12bf77f9eeb74aad6ee3d883970a22b8a45 parent e05a728bb9f2e7d33e05316777bc926eb26200a2 ・・・
となります。ここで、tree の行にあるハッシュ値が、このコミットが含むコンテンツの最上位の tree オブジェクトのハッシュ値、parent の行にあるハッシュ値が、このコミットの前の commit オブジェクトのハッシュ値となります。
図にすると、こんな感じです。
丸が commit オブジェクトで、三角が tree オブジェクトのつもりです。
commit オブジェクトをたどっても面白くないので、tree オブジェクトをたどってみます。
git cat-file -p ae59f12 を実行すると、今度は
100644 blob 6f405dfa9459fb06055ed0a189e388a3dbff968a .gitignore 040000 tree 82e3a754b6a0fcb238b03c0e47d05219fbf9cf89 a 100644 blob c1711f47ec4b3d14afe4659859fae8207aeb9c3a hoge 100644 blob 0372cbe1ca0f901202f3de163ebcb9f593b4f6d3 hoge.txt 100644 blob acdc2b8d1e5e3c92d72c6fa52a47086129af72a5 piyo
のような出力が得られます。
各行は、その tree オブジェクトに含まれる tree オブジェクトや blob オブジェクトです。
この出力の一列目は、ファイルモードを表す数値です。今は関係ないので放置します。
二列目は、オブジェクトの種類を表します。
三列目はハッシュ値で、四列目がオブジェクトの名前です。
ここまで追ったものを図にすると、こんな感じです。
四角が blob オブジェクトのつもりです。
さらに git cat-file -p 82e3a を実行すると、
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 .gitignore
となります。ここで、ハッシュ値が同じであることによる利点を試してみましょう。
あなたの Git リポジトリで、空のファイルをなんでもいいので作り、コミットしてみてください。
そして、今まで説明した方法で中をたどってみてください。
あなたが作った空のファイルを含む tree オブジェクトを cat-file したとき、そのハッシュ値と上の .gitignore のハッシュ値 (e69de...) を比べてみてください。同じになるはずです。
Subversion のような、内容を考慮しない、時系列のみによってつけられた連番の ID ではこうはいきません。
また、ファイル名が tree オブジェクトに格納されているため、ファイル名の変更によって blob オブジェクトのハッシュ値を再計算する必要はありません。
ブランチの話
Git のブランチに関しては、裏でどう動いているのかに関して流した程度だったので、もうちょっと詳しく書いておきます。
Git でのブランチは、実装上は commit オブジェクトのハッシュ値を保持するファイルに過ぎません。
ファイル自体は、ローカルブランチなら .git/refs/heads/ 以下に、リモートブランチなら .git/refs/remotes/リモートの名前/ 以下に格納されています。
試しに、壊れてもいいリポジトリを作って、適当に何回かコミットしてみてください。
例えばこんな感じです。
mkdir hoge cd hoge git init . echo hoge > hoge.txt git add . git commit -m "add hoge.txt" echo piyo > piyo.txt git add . git commit -m "add piyo.txt"
この状態で、git log -1 --oneline で最新のコミットのハッシュ値を確認すると、bc38b25 となりました。
このハッシュ値は設定や実行時間によって異なるので値自体に意味はありません。
ここで .git/refs/heads/master の中身は、bc38b25... となっており、最新のコミットのハッシュ値と一致しています。
とりあえず、このハッシュ値をクリップボードなどに控えておいてください。
では、何かファイルを修正するなり追加するなりして、新しいコミットを作ってからもう一度 git log -1 --oneline と、.git/refs/heads/master の中身の確認を行ってください。
ファイルの中身が更新され、最新のコミットのハッシュ値とやはり同じものになっているのが確認できます。
次に、控えておいたハッシュ値で .git/refs/heads/master の内容を書き換え、git log -1 --oneline を実行してみてください。
さっき行ったコミットではなく、ひとつ前のコミットのハッシュ値に戻ります。
このように、Git ではブランチは単なるハッシュ値を格納したファイルとして実装されているため、ブランチの作成や名前の変更、削除といった操作は非常に高速に実行できます。
ちなみに、タグは .git/refs/tags/ 以下に格納されているため、同じ名前のブランチとタグは同居可能です (紛らわしいのでやめた方がいいですが)。
git-completionの話、__git_ps1の話
git-completion が有効になっていると、git のサブコマンドやブランチ名やその他もろもろを、tab でいい感じに補完してくれます。
さらに、__git_ps1 という現在の working tree の状態が取得できる関数を PS1 に設定することで、現在チェックアウトしているブランチ名が常に確認できます。
__git_ps1 は現在チェックアウトしているブランチ名の確認だけでなく、たとえば rebase 時に conflict が発生した場合、(ブランチ名|REBASE) のような表記でそれを教えてくれるという機能もあります。
zsh の場合は id:clairvy (くらなんとかさん) がなんとかしてくれるので頼りにしましょう。
勉強会では Windows ユーザは TortoiseGit を使っていましたが、msysgit はデフォルト状態で git-completion が有効になっており、しかも __git_ps1 もいい感じに設定されているため、正直 TortoiseGit 程度なら使わない方がいいと思います。
TortoiseGit って、Tortoise って名前を関しているというだけでちょっと使われすぎじゃないですかね。
まぁ全員にコマンド使わせるのは無理としても、他にもましな選択肢はあると思うんですよね・・・
おっと、TortoiseGit 大嫌いなのでこの辺の話はかなりバイアスかかってるということでひとつ。
コミットの指定の話
リビジョン番号を持たない Git では、あるコミットを指定するためにいくつかの方法があります。
- ハッシュ値を直接指定する
- タグ名を指定する
- ブランチ名を指定する
- 相対的に指定する
などです。
この中で、相対的な指定は記号や数字が合わさって、はじめのうちは取っ付きにくいものですが、便利なのでぜひ覚えておきましょう。
相対的な指定でよく使うものに、^n (n 番目の親) と ~n (n 世代前の親) があります。
これはどちらもあるコミットの「前」のコミットを指すため、違いが分かりにくいですが、図にするとわかりやすいです。
この図でわかるように、^n による指定は親がいくつか存在する場合に親を選択するという状況で使用するため、merge ではなく rebase を多用するような場合、あまり使用することはないでしょう。
逆に、~n による指定は、簡単に前のコミットを選択できるため、使用する頻度は高くなります。
これらは組み合わせることもでき、たとえば master^2~3とすれば、master の二番目の親を選択し、そこからさらに三世代さかのぼる、ということも可能です。
また、n が 1 の場合省略することができるので、master^^^ と、master~~~、master~3 はすべて同じコミットを指します。
ちなみに、よく HEAD^ という記述を見かけますが、HEAD は現在チェックアウトしているブランチを指すので、現在のひとつ前、ということになります。
ここで ~ ではなく ^ を使うことが多いのは、Shift キーを押さなくても入力できるからだと勝手に思っています。US 配列とかでどうなってるのかは知らないです。
resetの話
reset コマンドはファイルを個別に指定する reset と、コミットを指定する reset の 2 つがあります。
コミットを作り上げるために使用する、ファイルを指定する reset コマンドは、add の逆操作と覚えておけばいいでしょう。
index に stage するために使用する add、index から unstage するために使用する reset です。
コミットを指定する reset は、主に歴史の修正やブランチを移動させるために使用します。
その際、soft/mixed/hard といったオプションが選べます (デフォルトは mixed) が、これらは以下のような違いがあります。
- soft
- リポジトリの状態のみを対象に reset する
- mixed
- リポジトリの状態と、index の状態を対象に reset する
- hard
- リポジトリの状態、index の状態、working tree のすべてを reset する
このような違いがあるため、各オプションは以下のように使い分けると良いでしょう。
- soft
- bare リポジトリでの操作のために使用する
- mixed
- 歴史の修正のために使用する
- hard
- ブランチを移動させるために使用する
ここで「ブランチを移動させる」と言っているのは、ブランチの話でやったような操作をコマンドで安全に行っているだけです。
そして「リセット」と言う語感に反して、そのブランチが指すコミットを書き換えるだけですので、現在のコミットよりも時系列的に後のコミットに reset することも可能です。
あくまで、re(再び) + set(設定する) のであって、リセットボタンのような融通の利かない操作ではない、という点には注意してください。
rebase と merge の話
なんか当日は「merge よりまずは rebase ですよ!」みたいなノリで話したんですけど、ここ完全に個人的な意見なので、その辺はよろしくお願いします。
rebase の欠点として「遅い」ってのは言ったのですが、もう一つ忘れてました。
rebase した結果、先頭のコミットに関しては正しく動いていても、途中のコミットが壊れてしまうという可能性があることです。
まぁ、その辺は tag あたりを利用して、「うまく動くという確証がある点」を明示しておけばそれほど問題ではないんじゃないかなぁ、とか思わなくもありません。
公開したものの rebase の話
公開したものを rebase してはいけないよ、という話はしたんですが、あんまりちゃんと説明できなかったので説明しようと思ったら、Pro Git にわかりやすくまとめられていたのでこちらをどうぞ。
ただし、開発メンバーが少数で、かつ声が十分に行き届くような開発現場の場合、このような混乱を生じさせずに済ませる方法もあります。
が、特殊なケースなのでその話はまた機会があれば。いやまぁ単純に bare リポジトリを reset してみんな取得し直す、というだけのものですが。
stash の話
最初に言っておくと、stash は好きじゃないです。
便利なのは認めるのですが、失敗した場合の復旧手段が全然覚えられない上に面倒という点で、あまり使いたくはない。
詳しい復旧方法は、
git stash save で一時退避した変更を、誤って git stash clear で消してしまったときの回復法 - t-wadaの日記
に丁寧でわかりやすい解説があるので、そちらを参考にしてください。
で、ですよ。stash です。stash を使う場面というのは、「とりあえずやった作業を置いておいて、違う作業がやりたい!」という場合がほとんどだと思います。
そしてここで stash を使うわけですが、ちょっと待ってください。それ、とりあえず commit しておきませんか?
commit しておけば、よほどのことがない限り作業を失うことはないですし、よほどのことが起こった場合は stash を使っていた場合でも作業は無に帰すでしょう (よほどのこと・・・リポジトリを間違って消しちゃうとか)。
まだ Git に慣れないうちは stash は手軽ですし使えばいいと思うのですが、rebase や reset を使いこなせるようになったら、「この作業消えると痛いなぁ」と思う場合は stash ではなく commit しておき、後で reset や commit --amend して一時的なコミットがあったという歴史を消してしまえばいいのです。
この方法だと、何か操作をミスした場合でも、いつものツールをいつものように使うだけで復旧が可能になります。
TortoiseGit、HG、SVN の話
大嫌いな TortoiseGit の話です。
懇親会であった話なんですが、TortoiseHG は TortoiseSVN の使い勝手からある程度離れ、独自の進化をたどっているそうです。素晴らしいですね。
今回久しぶりに TortoiseGit を触った (というほどではなく、見てただけですが) んですけど、TortoiseGit は TortoiseSVN の使い勝手をそのままにしようという方向性らしく、ダメなままですね。
Git をきちんと使いたいなら、TortoiseGit ではダメです。コマンドで使うか、GitExtensions などの別のフロントエンドを使いましょう。
全体を通して
今更と言えば今更なんですけど、非プログラマな方にはさっぱりな説明をしてしまったかな、というのが今回の最大の反省点です。ごめんなさい。
プログラマな人には、中身の話した方がたぶん分かりやすいと思って結構ディープな話とかもしてしまったんですけど、そうじゃない人を「何言ってんだこいつ」状態にしてしまったんじゃないかと思うと・・・
もうちょっとこう、中身の話しなくても説明できるようにならないとなぁ、と痛感しつつ、中身の話バリバリなエントリ書いてる時点で先はまだまだ見えない感じです。
それと、いろいろと深くまで突っ込んだせいで Github 部分全然やれなかったという点も反省ですね・・・