[Git] 脱初級!誤コミットはもう怖くない

  • 42
    いいね
  • 0
    コメント

一通り最低限必要なコマンドは覚えました。
普通にコミットしてrebaseやmergeを行ってプルリクを出すことが出来ます。
誤コミットをreset HEAD^で取り消すことも出来ます。

でも、commit --amendやrebaseで誤コミットしてしまってパニック!!

そんなあなた(ちょっと前の自分)に必要な知識をまとめました。

コミットしたものは消えない

そう、消えないのです。
たとえ、git commit --amend やら、git rebase で既存のコミットを編集しても、
編集前のコミットがそのまんまの状態で残っているんです。

どういうことかというと。
コミットが、A -> B ->C と行われた状態から、
image
Cに対して git commit --amend すると、

image

このように新しいコミットC'が作られブランチが付け替えられているだけなのです。

なので、ブランチの情報を元のCに戻してやれば、何事も無かったかのように誤コミット前に戻れます。

その戻し方の前に、まずはブランチについての誤解を解いておきます。
特にSubVersion等を使われていた方が誤解されているだろう点です。

ブランチは枝では無い

こちらの方のスライドが大変参考になりました。特にp.60辺りのブランチについての解説が秀逸です。
やさしいGitの内部構造 - yapcasia2013

コミットの名前

ブランチと言うと訳語的にも'枝'となりますが、
Gitでは、1つのコミットに付けられた単なる名前、です。
image

git logでブランチの履歴を見れますが、ブランチ名が付いているコミットから辿れる親(先祖)コミットを表示しているだけです。

  • 現在checkoutで開いているブランチの履歴表示
git checkout develop
git log
  • ブランチ名を指定して履歴表示
git log master

ブランチの移動

従来の枝という考え方では reset HEAD^ で1つ戻す、HEAD^^で2つ戻す、というように'枝'の上であくまでも動けるものと思われていたかもしれません。
しかし、実際には1つのコミットに紐づくだけの単なる名前ですので、今どこの枝に居るかは全く関係無くレポジトリ内の任意のコミットに移動させることが出来ます。

  • develop ブランチを任意のコミットに移動する
git checkout develop
git reset <移動先のコミットID, もしくは'名前'>

developブランチをDコミットから遠く離れたGコミットに移動させます。
image

  • 任意のコミットにブランチ名を付ける

また、どこのコミットにでもブランチを作成することが出来ます。ただの名前付けですからね。

git branch <作成したいブランチ名> <ブランチ名を付ける対象のコミットID,もしくは'名前'>

image

に対して、

git branch topic <AのコミットID>

を実行すると。

image

topicブランチが作成されます。(Aにtopicという別名が付けられます)

同じ'枝'の上であろうとも1コミットに付ける名前なので全く共存に問題無いのです。
もちろん、1つのコミットに複数のブランチ名を付けることも出来ます。

名前について

Gitでは色々な名前が有ります。
実体としてはコミットIDが有り、それに対する別名と考えて下さい。

  • コミットID

コミットに対して作成されるIDです。一度作成されたら消えません。1

フル形式

540572fe82db686c22e0f64ba01dd4ab60c6096d

短縮形式(単にフル形式の先頭から7文字取り出したものです)

540572f
  • HEAD

現在の作業位置を表すカーソルです。1つのコミットのみに紐付きます。
コミット操作をすると、HEADが指しているコミットを親とする新しいコミットが作成され、HEADも新しいコミットに移動します。

  • ブランチ名

checkoutで開くことで、以降のコミット操作やresetなどのコマンド操作ではHEADと一緒に移動します。

  • 例)

コミット前
image

コミット後
image

  • タグ名

一度作成したら削除しない限り動かない名前です。
"ver1" とか付けます。

git tag ver1 <AのコミットID>

image

  • 名前^、名前~2 のような相対名

相対指定です。

git reset HEAD^

で、お世話になっている表し方です。

基準になる名前は何でも良くて、
540572f^
ブランチ名^
タグ名^
とか指定出来ます。

これらの名前の作り方、移動可否、移動方法などはそれぞれ異なりますが、
コマンドの引数でコミットIDを指定するものは、上記のどの形式でもOKのようです。(例外は有るかもしれませんが;)

誤コミットからの復旧

では、誤コミットからの復旧をしてみましょう。

amendのケースで説明します。

git commit --amend

コマンドにより、以下図の状態になっています。

image

誤コミット前のコミットIDを取得する

ブランチを誤コミット前のCに紐付け直せばいいわけですが。それには、CのコミットIDが必要です。
しかし残念ながら、git logでは取得出来ません。

実は、あなたが行った操作による、HEADやブランチの移動履歴を保存しているログが有ります。

git reflog <ブランチ名>
例)
git reflog develop

出力

3570d95 develop@{0}: commit (amend): C' desu
6036f53 develop@{1}: commit: C desu

各行の先頭から、
コミットID: 3570d95
コミット名: develop@{0}
行った操作: commit (amend)
コミットコメント: C' desu
となっています。

このログを見ると、1行目がamendによって作成されたコミットC'を表していますので、
2行目がその直前の位置Cということになります。

元に戻しましょう

ブランチの位置を動かすにはresetコマンドを使います。

git checkout develop
git reset 6036f53 

もしくは、

git checkout develop
git reset develop@{1}

はい!これで元通りです。誤コミット前の状態に戻りました。

image

C'コミットも git log では見えなくなります。

なお、resetに --hard を付けずに実行すれば、間違ってamendしていたファイルが残りますので、
それを修正して再コミットも出来ます。

もしくは、両者のコミットIDでdiffを取るとかも出来ますよね。

amend前のコミットが git log で表示されない理由

コミットには、自身のコミットIDに加えて親コミットのIDを持っています。
しかしながら、子コミットのIDは持っていません。

その参照関係を図にするとこうなります。

image

子は親を知っていますが、親は子を知らないのです。

git log は、指定した名前を起点として参照を辿っていくのですが、この場合のCには到達出来ないので表示されないのです。

git log develop

image

コマンド解説

git log

  • カレントブランチの履歴を表示する
git checkout develop
git log
  • 指定した名前の履歴を表示する

ブランチを含め、タグ名、コミットIDを指定すると、それから辿れる履歴を表示します。

git log <名前>
  • 参考

私の記事ですが、もしよろしければ参考にして下さい。
git logを見やすくしたい!

rebase

これもamendと基本的に同じです。rebase前のコミットがそのまま残っています。

まず、rebaseの使い方です。
例えば、プルリク前に作業ブランチをマージ先のmasterブランチの先頭に移動させたい場合、
1. 移動させたいブランチをcheckoutで開きます。
2. rebaseで移動先の親となるコミットを指定します。

git checkout <移動させたいブランチ>
git rebase <移動先で親となるコミットのID,もしくは名前>

そうすると、このようになります。

rebase前
image

git checkout develop
git rebase master

rebase後
image

このように、Eコミットから新しいE'コミットが作られ、masterのDコミットにぶら下がります。
developブランチもE'に付け直されています。

EコミットはもはやdevelopのE'からもmasterのDからも辿ることが出来ないので、git log では表示されません。

rebase前に戻す

まずは、reflogでdevelopブランチの移動履歴を見ます。

git reflog develop
d3957fa develop@{0}: rebase finished: refs/heads/develop onto eec52920c70831b1607f8eb0856736a51e8a20c3
b73de97 develop@{1}: commit: E desu

1行目がrebase後のコミットを表していますので、
2行目がその実行前のEコミットです。

では、元に戻しましょう。

git checkout develop
git reset develop@{1}

image

rebaseはブランチを枝として見ている!?

rebaseコマンドでは、developのどの範囲のコミットを移動させるかの情報を指定していません。

これはどういうことでしょうか?developブランチの枝としての情報がやはりどこかに有るのでしょうか?

実はこういう仕組みになっています。
まずrebaseコマンドは、カレントのdevelopからと、移動先masterの両方から過去をさかのぼり、合流するところを見つけます。
image

そして、合流点の次のコミットからdevelopまでを移動対象としています。
この図のケースではEコミットが移動対象となります。

移動しない rebase の場合

rebaseは、単に過去コミットを編集したいだけの時にも使われます。

この状態から、
image

git checkout develop
git rebase <BコミットのID,もしくは名前>

Bコミットの後がrebase対象となります。

image

どうして、C,Dを編集するのにBをrebaseのパラメータに指定するのだろう?という疑問は無かったでしょうか?
この図を見ても分かるように、移動先の親コミットとしてBを指定しているのです。
たまたま移動前と移動先の親コミットが同じだっただけですね。

rebase前に戻す

前項目と同じです。

cherry-pick

これまでの知識でブランチを自由に操れるようになりました。
そうすると、コミットそのものも自在に操れるようになりたいところです。

amendやrebaseでもコミットの編集や移動が出来ますが、もっと便利なコマンドとしては、
cherry-pick が有ります。

これは任意のコミットを取ってきて、自ブランチに新規コミットとして追加する、というものです。

git cherry-pick <取って来るコミットのID,もしくは名前>

cherry-pick前
image

git checkout develop
git cherry-pick <EのコミットID>

cherry-pick後
image

単純なコマンドですが、コミット単位であちこちから持ってくることが出来るという非常に自由度の高いものとなっています。

使用例

例えば、amendしたけどやっぱり新規コミットに変更したい、という場合にも使えます。

amend前にブランチを戻します
image

cherry-pickでC'を取ってきます
image

まとめ

  • コミットは消えません。見えなくなっただけです。
  • ブランチは枝ではありません、1コミットだけに紐づきます。
  • ブランチは単なる名前であり、どこのコミットにも移動出来ます。
  • reflogで、見えなくなったコミットも見えるようになります。
  • amendやrebaseから元に戻せます
  • cherry-pick お勧め!

  1. 実はGitにはゴミ掃除機能が有って、辿れなくなったコミットのゴミがある程度溜まったら削除されるようです。なので、誤コミットは速やかに元に戻すかブランチ名とかタグ名を付けておきましょう。