LoginSignup
18
12

More than 5 years have passed since last update.

gitのコミットを入れ替えたり、操作を巻き戻したり

Last updated at Posted at 2017-11-05

例によって前書き(という名の無駄話

転職しました。はい。もう Twitter とかで報告済みですが

というわけで「転職して一番レベルアップしたスキルは何?」と聞かれるとおそらく迷わず「git!」と答えちゃうくらい、git について色々知って来ました。

前職ではそもそも一人でしか開発してこなかったので、git もどっちかというと自分にとってのバックアップの意味合いが大きく、ほとんどの操作は SourceTree だけで全部完結しちゃっています。checkout、commit、merge、push、pull、基本これくらいしか使わないから。プルリク?何それおいしいの?git rebase?何それ怖そー

そんな感覚で転職してチーム配属されて言われた(ほぼ)最初の言葉は「git rebase 覚えろ」です。まあプルリク(GitLab 使ってるので正確にはマーリクですが)とか多用してると確かに git rebase はたくさん使うんだなぁと痛感している日々を過ごしております。もう git rebase 怖くない!

なんでコミットを入れ替えるの?

一人開発やってると基本こんなことないかと思います(個人談)が、チーム開発、特にプルリク/マーリクベースの開発なら、こういうシナリオも出てくるんじゃないかと:

  • コードレビューしやすいようにマーリクの粒度を小さくしている
  • 実際のコーディング・コミットと比べるとマーリクが比較的に遅くなる
  • マーリクのコードレビューで何かしら指摘されるが、実はそれの対応がその数個後のコミットで対応している

上記のようなシナリオですと、当然その指摘の対応コミットをあげちゃえば済む話でもあるかもしれないが、それですとマーリクに入るコミットが増えてしまいマーリク粒度が大きくなってしまう難点がありますし、そもそもそれらのコミットは全く別のもののコードになってるかもしれないので、それだとコードレビューも難しくなります。なのでやはりそのコミットだけ今のところに持って来たいですね

フローにすると下記のような感じかと:

A - B - C(MR_Branch) - D - E - Cpatch - F - G(Local_Branch)

んで、マーリク出してるのは MR_Branch なのでコミット C になるのですが、コードレビューで指摘された項目はコミット Cpatch で対応済みですので、下記のようなフローに直したい:

A - B - C - Cpatch'(MR_Branch) - D' - E' - F' - G'(Local_Branch)

そうすれば、先に CpatchMR_Branch としてマーリク出せるし、関係のない DE コミットも含まれずにすみます

また、他にもこういうシナリオも出てくるんじゃないかと:

  • コードレビューしやすいようにマーリクの粒度を小さくしている
  • 実際のコーディング・コミットと比べるとマーリクが比較的に遅くなる
  • マーリクのコードレビューを出したが、このコミットを最後にしてほしいから先に他の修正をマーリク出せと指摘される

このようなシナリオですと、先ほどのシチュエーションとは逆になって、フロー図にすると

A - B - G(MR_Branch) - C - D - E - F(Local_Branch)

A - B - C'(MR_Branch) - D' - E' - F' - G'(Local_Branch)

やりたいことは逆ですが、どちらも要するに途中のコミットの順番を入れ替えたい、というデマンドですね。

こういう時に願いを叶えてくれるのが、git rebase 機能です。

まあ git 入門の頃は様々な入門記事なりなんなりで「git rebase を気軽に使うな!」みたいな警告をたくさん目にしたんじゃないかと思います。筆者もその中の一人です。だからぶっちゃけ今の仕事につくまで一度も git rebase 使ったことがなかったのです。なんとなくアバウトなイメージは git 使っていくうちにできて来ましたが、やはり心の中ではどこかで怖い気持ちを抱いています。まあ名前通り、「commit を組み直す」だけの簡単な機能ですけどね

というわけで早速やり方に入りますが、ここで使うのは Interactive Rebase 機能になります。

CpatchC の直後に持って行くパターン

  • ターミナル開いて、cd で作業中のリポジトリに行って、git status で現在 Local_Branch にいることを確認します(もちろん SourceTree とかのクライアントで確認しても問題ありません)

  • git rebase -i MR_Branch コマンドを入れれば、MR_Branch から現在いる Local_Branch までのコミットが順番に表示されます、今回の場合大体下記のような表示になるかと

pick D commit_message
pick E commit_message
pick Cpatch commit_message
pick F commit_message
pick G commit_message

# .......
  • この順番を編集することで commit の順番を変えられます、そして実はこれは vi エディターで表示されているので、vi のコマンドが全て使えます。
    • 今回の場合、CpatchD の前に持って行きたいので、まず方向キーでカーソルを pick Cpatch commit_message の行に持って行きます
    • 次に、そのまま dd を入力すれば、pick Cpatch commit_message の行がカットされます
    • そしてまた方向キーでカーソルを一番上の pick D commit_message に持って行きます
    • そのまま P(つまり shift + p)を押せばさっきカットされた pick Cpatch commit_message の行が pick D commit_message の上にペーストされます
    • 最後に :wq でこのリストが確定され、rebase が行われます

以上の操作を行えば、フロー図は下記のようになります:

A - B - C(MR_Branch) - Cpatch' - D' - E' - F' - G'(Local_Branch)

ここまできたら、あとは MR_Branch ブランチを Cpatch コミットに持って行くだけですので、GUI クライアントでもできますし、コマンドラインでももちろんできます。コマンドラインの場合は Cpatch の SHA-1 コードをコピペしておく必要がありますので GUI クライアントよりちょっと面倒になりますが。そして持って行く方法も git reset --hard もいいですし、git rebase 使っても OK です。ここで簡単に git reset --hard を使う方法を説明します:

  • 先ほどの操作の直後なら、今はまだ Local_Branch にいるはずですが、念のため git status で確認しておきます
  • git log で、このブランチの全てのコミットが vi で表示されます。長すぎる場合は方向キーでスクロールできますので、Cpatch のコミットが表示されるまでスクロールします
  • このコミットの SHA-1 コードが commit: xxxxxxxxx のような形で表示されますので、この SHA-1 の部分、つまり xxxxxxxxx をコピーします(vi エディター内で使うわけではないので、普通にマウスのカーソルでこの部分を選択して cmd + c でコピーします)
  • git log の履歴は編集不可のコマンドモードなので、下のコマンド領域にすでに : が表示されます、なので q だけでエディターを閉じられます
  • git log から戻ってきたら、git checkout MR_BranchMR_Branch に切り替えます
  • git reset --hard xxxxxxxxx で、MR_BranchCpatch コミットに移動させます。xxxxxxxxx の部分は先ほど SHA-1 をコピーしておいたので、cmd + v でペーストできます

これで GUI クライアントとかで確認すれば、MR_BranchCpatch のコミットにいることが確認できますので、あとはこの MR_Branch でマーリク/プルリクを出すだけですね

CG を入れ替えたいパターン

大まかな操作はさっきと変わらないですが、ちょっと違うのは現在 MR_Branch がすでに G にいること面倒なところです:

  • 最初はすでにプルリクを出したサーバ側の MR_Branch ブランチを削除しておきます、そのコミットはもう要らなくなりますので
  • git checkout MR_BranchMR_Branch に切り替えておきます
  • git log でコミット履歴を出し、B コミットに git reset --hard で強制的に持って行きます(すでにある F までのコミットは Local_Branch が持ってありますので仕事がなくなる心配は特にないです)
  • 最後に git checkout Local_BranchLocal_Branch に切り替え、1 番目のパターンと同じような流れで G コミットを F の上に持って行き、改めて MR_BranchC にリセットします

git rebase の注意点

すでに気づいた方もいるかと思いますが、フロー図のところ、入れ替え後のコミットは D とか E とかではなく、D' とか E' とかになっています。なぜこう書くのですか

そもそも論として、git のコミット管理は差分管理です。例えば 1 番目のパターン、本来の Cpatch のコミットは E コミット後です、つまり CpatchE コミットに対する差分です。我々のコーディングとしては確かにそれは C コミットに対する修正かもしれませんが git のシステムにとってはそんなの知らないのです。だから CpatchC の直後に持って行くということは、CpatchE に対する差分から、C に対する差分に変えるということを意味します。当然、この Cpatch コミットが変わったので、それ以降の D やら E やらも全部、git のコミットとしては変わってしまいます。その証拠に、GUI クライアントで確認すれば git rebase 後のこれらのコミットの日付が git rebase 時の日付に変わってしまいますし、もし git rebase 前に Cpatch などのコミットの SHA-1 コードを控えておけば、git rebase 後のこれらのコミットの SHA-1 コードも変わってしまうことも気づけるはずです。

そう、Rebase 機能は、コミットを我々の望む通りに入れ替えたりしてくれる代わりに、コミットそのものを変えているのです。だからその動作を理解せずに「コミット履歴が綺麗になるから」という軽い気持ちでやってしまいますと、思わぬ結果を招いてしまうかもしれません。例えば複数人開発していて、全員アクセスしている develop ブランチを勝手に Rebase してしまうとどこが正しいかわからなくなってしまうし、間違えてコミットを削除してしまったりする危険性もあります。だから git の初心者向け記事は大体「git rebase を気軽に使うな!」と書かれますね

さて、もし万が一本当にやらかしてしまって、コミットを間違えて無くしてしまったら、じゃあもう万策尽きた!ってなるのでしょうか?いいえ、まだ救いがありますので、泣くのがまだ早いです!ここからは git reset --hard を使った操作の巻き戻しを紹介したいと思います

やっちまったー!時の操作の巻き戻し

先ほどの 2 番目のパターン、つまり GC のコミットを入れ替えたいという場合の紹介に、2 番目の手順に「git checkout MR_BranchMR_Branch に切り替えておきます」と書いてありますね。もしこれをやるのを忘れたりして、今現在が MR_Branch ではなく、Local_Branch にいたらどうなるのでしょうか?

答えは、その次に無理矢理に B コミットにリセットしてしまうので、今 Local_BranchB に、そして MR_Branch がその次の G にあるため、G 以降の CDEF コミットを保持しているブランチがなくなってしまって消えてしまいます。フロー図にするとこんな感じですかね:

A - B(Local_Branch) - G(MR_Branch)

まだ Rebase してないのに必要なコミットまでなくなってしまった。ヤッチマッター、と、泣きたくなってしまいますね

でも、git はとても賢い子だから大丈夫!まだ挽回のチャンスがあるのです!ここで登場するのは git refloggit reset --hard です

git reset --hard は先ほどすでに紹介したので、git reflog だけ紹介しておきましょう。これは今までの全ての git の操作を記録しているものです。もしここは間違えて Local_Branch のまま git reset --hard xxxx(B) の直後なら、履歴はこんな感じでしょう:

xxxxxxx (HEAD -> Local_Branch) HEAD@{0}: reset: moving to xxxxxxxxxx
yyyyyyy HEAD@{1}: somecommand: somecomments
...

ここで xxxxxxx は今 B コミットの SHA-1 コードの最初の何桁、HEAD@{0} は今現在最新の HEAD 場所、reset はさっきの reset コマンド、次の xxxxxxxxxx はまた B コミットの SHA-1 です;そしてその下の HEAD@{1} はこの reset の一つ前の状態です、もし reset の前は Local_BranchF をコミットしたばかりなら、yyyyyyy HEAD@{1}: commit: F みたいな感じになるでしょう。ここまでわかったら、HEAD@{1} に戻ればいいだけです。あとは簡単です。git reset --hard HEAD@{1} をすれば、無事 Local_BranchF に戻ります。CDE ももちろん戻ってきます。フロー図にするとこんな感じですかね:

A - B - G(MR_Branch) - C - D - E - F(Local_Branch)

よかったよかった。ちゃんと戻ってこれました。あとはきちんと MR_Branch に切り替えるのを忘れずにやれば OK です

ここで注意しないといけないのは、今回のミスは reset コマンドなので、一歩だけ遡って HEAD@{1} で済んでしまいますが、rebase などの場合はこう一筋縄にはいかないことも多いです。なので、どこまで遡るかは、きちんと git reflog を読んで理解する必要があります

おまけ

さてこの記事は git rebase -igit reflog / git reset --hard を紹介してきましたが、筆者の自分の作業ではもう一つよく使うものがあります、git rebase --onto です。これはもうちょっと高度なことになりますがいい紹介記事があります:git rebase --onto を使って、まずは自分だけ幸せになる。ここで簡単に使い方を紹介してみると、例えば今現在はこんなフロー図であって、自分は今 Local_Branch にあるとします:

init(master) - A - B(MR_Branch) - C - D - E - F - G(Local_Branch)

そして何かしらの理由で、こんな感じに持っていきたいとなりました

               A - B(MR_Branch)
             /
init(master) - C' - D' - E' - F' - G'(Local_Branch)

つまり MR_Branch を何かしらの理由で Local_Branch から切り出したい、C 以降の Local_Branch のコミットを直接 master ブランチ後に持っていきたい場合。もし今現在が Local_Branch にいれば、git rebase --onto master MR_Branch でできます。Local_Branch にいなければ、git rebase --onto master MR_Branch Local_Branch でやれば OK です。また、やりたいことが済んでまた戻したい場合は、今 Local_Branch にいればまた git rebase MR_Branch で元に戻ります。Local_Branch にいなくても、git rebase MR_Branch Local_Branch で元に戻ります(正確にはコミットは変わるので「完璧に元に戻る」わけではありませんが)

そして、実は --onto の場合、引数の masterMR_Branch はブランチ名の代わりに、特定のコミットの SHA-1 コードにすることもできます。ただ 2 番目の引数も特定なコミットにした場合、もし Rebase 後にこのコミットを保持しているブランチがなければ、当然ながらそのコミット(及びそのコミットより以前の、どのブランチにも保持されていないコミット)も消えてしまいます。ヤッチマッター、ってなった場合は、是非とも先ほどの操作の巻き戻し方をチェックしてくださいね☆(ゝω・)vキャピ

というわけで、ぜひ、皆さんも幸せな git ライフを送ってください。

18
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
12