【Git入門】Gitを使い始めたい人へ「ローカルリポジトリの操作2」 ~A Beginner to Beginners~ 【その3】 : 前 次 : 【Git入門】Gitを使い始めたい人へ「リモートリポジトリをつくる(GitHub)」 ~A Beginner to Beginners~ 【その5】
目次
Gitの操作もだいぶ学んできた。このへんで頭の中を一旦整理しつつ、細かい話をしていきたい。
HEADとブランチ
前回、HEADとブランチと呼ばれているものを移動させてワークツリー(作業ディレクトリ)の内容をコントロールしてきた。
復習がてら確認しておくと、ブランチを切り替えればそのブランチが指し示すコミット(記憶のかたまり)の状態に変更してくれる、というものだった。HEADを前の状態に戻していけば、その前の状態がワークツリー(作業ディレクトリ)に復元された。
今までコミットをどちらも指し示している曖昧な状態で進めてきたが、これらは一体どういうものなんだろうか。
コミットの正体
これまで作成してきた「コミット(記憶のかたまり)」に含まれる情報は次のようなものである。
- ワークツリーの状態
- コミットを作成したユーザーの情報(ユーザー名、E-mailアドレス)
- コミットメッセージ
- 親コミットの名前
図で表すとこう
コミットには自分の一つ手前のコミット(親コミット)の名前が記録されている。つまり、今いるコミットから親コミットへ順番にたどっていけば最初のコミットまでたどり着けるというわけだ。逆に、自分の後ろのコミットについての情報は全く含まれていない。したがって、親コミットから子コミットのほうは認識できないということである。
今まで
$ git log
としたとき、今いるコミットより下のコミットの情報は表示されなかったはずだ。
たとえば
この状態でみると
$ git log --oneline
0d5bf59 Third commit
393f3a1 Second commit
f0c1e0e first commit
と表示されたが、一つもどってログを見ると
$ git reset --hard HEAD~ $ git log --oneline 393f3a1 Second commit f0c1e0e first commit
となり、Third commitは表示してくれない。この原因はコミットに含まれる情報にあったわけだ。
現在の状態について
Gitにおける現在の状態について整理しよう。
現在の状態は、
- 現在のコミット
- 現在のインデックス
- 現在のワークツリー
の3つに分けることができる。
「現在のコミット」は問題ないだろう。通常、最後にコミットしたものを指し示し、git reset
などを使って移動した場合はその移動先のコミットを表す。
「現在のインデックス」はステージ(インデックス(変更したものリスト)に追加)されたもののことだ。git add
で追加したりできる。
「現在のワークツリー」とは作業ディレクトリの状態である。普段ファイルを開いたり編集したりできるあれ。コミットしたあとにまた変更してたりしてなかったり。 コミットの中身は変更されることは基本ないがこちらは随時変更されていくだろう。
図で表してみるとこんな感じ。
これを把握した上で次からを読み進めてもらいたい。
ブランチの正体
前回、ブランチについて説明した。ブランチは枝のことだと説明してきたが、実はあまり正しくない。
ブランチとは、コミットを指し示すものである。次の図を見てみよう。
この図では「master」がコミット「0d5bf59」を、「test-branch」がコミット「84507d3」を指し示している。
通常、ブランチは枝一つにつきブランチを一つ用意する。そしてそのブランチが枝の先端を指し示すように動かすものなのだ。
逆に言えば「master」や「test-branch」を別の枝を指し示すように移動することもできるということだが、無意味なのでやらない。
HEADの正体
最後にHEADの正体について考える。ブランチの流れから察している人もいるかもしれないが、HEADもなにかを指し示すものだ。HEADは指し示すものが複数存在し、同時に指し示している。指し示しているのは次の3つだ。
- 現在のブランチ
- コミット直後のインデックス
- コミット直後のワークツリー
「現在のブランチ」を指し示すとはどういうことか。現在「master」ブランチにいればHEADは「master」を指し示し、現在「test-branch」にいればHEADは「test-branch」を指し示す。これを「HEAD->master」などと表していて、git log
などで見たことがあるのではないだろうか。図で表すと、
この状態ではHEADは「master」を指し示しているが、
$ git checkout test-branch
とするとヘッドの指し示すブランチは「test-branch」に変わり
となる。
ブランチを指し示していれば実質コミットも指し示すことができているということになる。
「コミット直後のインデックス」はそのままの意味であり、ステージされているものすべてを表す。
「コミット直後のワークツリー」も同様そのままの意味を示し、最後のコミットから変更されている部分も含むことになる。
C言語などの「ポインタ」がわかる人へ
C言語などの「ポインタ」がなにか知らない人は飛ばして次へ Gitのリポジトリを自在に操る
実は、これらの「指し示す」という考えはC言語の「ポインタ」の概念と照らし合わせるとよく理解できる。HEADはブランチ、空のインデックス、元のワークツリーへのポインタをメンバにもつ構造体のように捉えるとわかりやすいかもしれない。「HEAD->master」のような表現にアロー演算子やん!って反応した人はある意味正しい。ブランチはコミットへのポインタであり、コミットは親コミットへのポインタ、ワークツリーへのポインタなどを保持していることになる。つまるところポインタのオンパレード。
Gitのリポジトリを自在に操る
ここまでの説明をしたことで、リポジトリをうまく操るために準備が整った。今までの知識をフル活用して読んでいきたい。
revert
はじめてみるコマンドかもしれないが、便利なので掲載。
最終コミットの内容を取り消したコミットを作成するのに使う。
一度コミットしたものを取り消すのだが、その取り消したこともログに残したいことがあるだろう。そういうときのコマンドである。
具体的には直近のコミットを取り消したコミットを作り出すことになる(一つ前のコミットができあがる)。
$ git revert HEAD
git reset
さて、ここまできて今まで幾度とみてきたgit reset
について考える。とても重要なコマンドなので詳しくしっておきたい。
git reset
の使い方は
$ git reset [オプション] <コミットを指すもの>
という形だ。「コミットを指すもの」としては「HEAD」、「ブランチ」、「コミットの名前(ハッシュ値)」とその相対位置(HEADの一つ前、ブランチの一つ前など)を指定できる。一つ前を指し示すときは「~」をつけるなどする。例えばHEADが指し示すコミットの一つ前のコミットを指定したいときは
$ git reset HEAD~
というようにすれば良い。
オプションはとりあえず--hard
--soft
--mixed(つけなくても良い、デフォルト)
の3つを把握しておけばよい。
[--mixed]オプション
まず--mixed
について説明する。これはデフォルトなので、オプションを何もつけないでgit reset
した場合は自動的に--mixed
オプションをつけたときと同じ扱いになる。
このオプションをつけると、現在のインデックス、及びコミットが現在の状態から指定したコミットの作成直後までリセットされる。ワークツリーはリセットされないので、変更したファイルはそのまま残っている。どういうことか、図で表してみると次のようになる。
コミットとインデックスはHEADの状態に書き換えられているが、ワークツリーは現在の状態のままなことがわかる。
次の例を見てみよう。
【その2】で説明したように、
$ git add <filename>
で現在のインデックス(変更したものリスト)に変更したファイルを追加できた。
ここで、変更したけどまだコミットしたくないものを間違って「add」してしまったときのコマンドとして
$ git reset HEAD <filename>
を紹介したとおもう。これはどういうことかというと「<filename>で指定したファイルを、HEADの指し示す状態にリセットする」ということだ。コミット直後は当然インデックスは空であるから、指定したファイルをコミット直後の状態に戻すとまだステージされる前の状態に戻る。けれどもワークツリーは何も変化しないので、書き換えた部分は取り消されることはない。したがって、結果的には「add」を取り消すことができるようになる。同様にして、すべてのインデックスを取り消す場合は
$ git reset HEAD
とすればいいことがわかる。
[--soft]オプション
次に--soft
についてだ。このオプションでは、コミットのみ後ろに書いたコミットの状態にリセットする。インデックスやワークツリーは書き換えられない。つまりコミットだけリセットされるということだ。
図で表してみよう。
コミットの部分だけHEAD~の状態にリセットされ、ほかはそのままなことがわかる。図の状態をコマンドで打つなら
$ git reset --soft HEAD~
となる。これはファイルの変更を維持したまま直前のコミットを取り消す動作を意味する(コミット1は消えてしまうが、sample.cの変化は生きていて、ステージされたまま)。
この操作に関しては、実は後述する
$ git add sample.c
$ git commit --amend
のほうが便利だったりする。
[--hard]オプション
今までかなり多用してきたように見える[--hard]
オプションについてだ。--hard
オプションでは後ろに書いたコミットの状態にリセットし、なおかつインデックス、ワークツリーも元の状態にリセットする。図で表すと
この場合、一つ前のコミットの状態を完全に上書きしていることになる。これまで、一つ前のコミットに戻る時や指定のコミットに戻る時
$ git reset --hard HEAD~
や
$ git reset --hard 0d5bf59
などとしてきたのは、現在の状態を指定の状態に上書きしたということになる。結果的には指定のコミットに移動したのと同じになるが、コミットしていないワークツリーの内容やインデックスの内容は完全に消えてしまうので注意が必要である(コミットは残るがハッシュ値を直接入力しないと戻れない)。
もっとも、現在のコミットを別のブランチにマージして、そのあと一つ前のコミットに戻って作業を進めた後再度マージすると競合が発生することになりかねないので、「コミットの内容を取り消す」のならばgit reset [--hard] HEAD~
をするよりも前述したgit revert
を利用することで「コミットの内容を取り消した履歴」を残しておくほうがベターであろう。
$ git reset --hard HEAD~
の場合
$ git revert HEAD
の場合
競合を回避するためには変化の過程が一意的でなければならない。
ブランチの移動
さて、ここまでやってきたところで注意したいところなのだが、git reset [option] HEAD~
などとHEADの位置をリセットしてきた。このとき、HEADの位置変更と同時にブランチの位置も変わっていることに注意したい。例えば現在「master」ブランチをHEADが指しているのなら
$ git reset --hard HEAD~
としたときはHEADのみでなく「master」も一つ手前のコミットを指すように変化していることになる。また、
$ git reset --hard master~
などと指定することもでき、この場合は「master」ブランチの一つ手前にHEADとHEADの指し示すブランチが移動することになる。つまり、現在「master」ブランチにいるときに強引に
$ git reset --hard test-branch~
とすれば、HEADと「master」は「test-branch」が指し示しているコミットの一つ手前に移動することになる。
まあこんなこと決してやってはいけないのだが・・・・・・。
git commit
今までさんざん行ってきたgit commit
だが、コミットを修正したいことがある。図で表すと
こんな感じ。「sample.c」をコミットしたあと再度変更したが、その変更も前のコミットにいれてしまいたくなったときの方法。やり方は単純で、変更したものをインデックスに追加したあと、もう一度コミットするのだが、オプションで[--amend]
をつけることである。こうすることで変更したものリストの内容を新規のコミットではなく直前のコミットの内容に上書きしてくれる。
$ git add sample.c
$ git commit --amend
使わなくなったブランチ
分岐して使っていたブランチをマージし、そのブランチが不要になったときはブランチを破棄することができる。
$ git branch -d <ブランチの名前>
実際の開発とブランチの使い分け
これで自分の操作しているリポジトリはほぼ自由に操ることができるようになった。では実際の開発ではどのようにするのがいいのだろうか。
基本的にはmasterで作業をせずに、別のブランチ(develop)で作業をして、良いところでmasterとマージさせていく。途中の機能追加などはdevelopから更に別のブランチに分岐して作業をしてdevelopにマージしていく、という流れが開発としてはやりやすいだろう。こういったルールはチームで決めていきたい。
上記のものは「A successful Git branching model」というのを参考にした。ググれば日本語になっているやつも出てくると思う。
まとめ
コミットやHEAD、ブランチの正体について学習し、それを踏まえていくつかコマンド(特にgit reset
)について説明した。git reset
は結局
$ git reset [オプション(どこをリセットするか)] <リセット元のデータはどこにあるか>
というふうにまとめられる。他のコマンドも同様、現在のHEADがどうなっているかに注視していれば良い。
次回からリモートリポジトリについて進めていく。
【Git入門】Gitを使い始めたい人へ「ローカルリポジトリの操作2」 ~A Beginner to Beginners~ 【その3】 : 前 次 : 【Git入門】Gitを使い始めたい人へ「リモートリポジトリをつくる(GitHub)」 ~A Beginner to Beginners~ 【その5】