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

# 例によって前書き（という名の無駄話
転職しました。はい。もう 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)
```

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

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

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

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

```
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 機能になります。

## `Cpatch` を `C` の直後に持って行くパターン
- ターミナル開いて、`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` のコマンドが全て使えます。
    - 今回の場合、`Cpatch` を `D` の前に持って行きたいので、まず方向キーでカーソルを `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_Branch` で `MR_Branch` に切り替えます
- `git reset --hard xxxxxxxxx` で、`MR_Branch` を `Cpatch` コミットに移動させます。`xxxxxxxxx` の部分は先ほど SHA-1 をコピーしておいたので、`cmd` + `v` でペーストできます

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

## `C` と `G` を入れ替えたいパターン
大まかな操作はさっきと変わらないですが、ちょっと違うのは現在 `MR_Branch` がすでに `G` にいること面倒なところです：

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

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

そもそも論として、`git` のコミット管理は差分管理です。例えば 1 番目のパターン、本来の `Cpatch` のコミットは `E` コミット後です、つまり `Cpatch` は `E` コミットに対する差分です。我々のコーディングとしては確かにそれは `C` コミットに対する修正かもしれませんが `git` のシステムにとってはそんなの知らないのです。だから `Cpatch` を `C` の直後に持って行くということは、`Cpatch` を `E` に対する差分から、`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 番目のパターン、つまり `G` と `C` のコミットを入れ替えたいという場合の紹介に、2 番目の手順に「`git checkout MR_Branch` で `MR_Branch` に切り替えておきます」と書いてありますね。もしこれをやるのを忘れたりして、今現在が `MR_Branch` ではなく、`Local_Branch` にいたらどうなるのでしょうか？

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

```
A - B(Local_Branch) - G(MR_Branch)
```

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

でも、git はとても賢い子だから大丈夫！まだ挽回のチャンスがあるのです！ここで登場するのは `git reflog` と `git 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_Branch` で `F` をコミットしたばかりなら、`yyyyyyy HEAD@{1}: commit: F` みたいな感じになるでしょう。ここまでわかったら、`HEAD@{1}` に戻ればいいだけです。あとは簡単です。`git reset --hard HEAD@{1}` をすれば、無事 `Local_Branch` が `F` に戻ります。`C` も `D` も `E` ももちろん戻ってきます。フロー図にするとこんな感じですかね：

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

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

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

# おまけ
さてこの記事は `git rebase -i` と `git reflog` / `git reset --hard` を紹介してきましたが、筆者の自分の作業ではもう一つよく使うものがあります、`git rebase --onto` です。これはもうちょっと高度なことになりますがいい紹介記事があります：[git rebase --onto を使って、まずは自分だけ幸せになる](https://qiita.com/kyanro@github/items/d0111b07022494457b75)。ここで簡単に使い方を紹介してみると、例えば今現在はこんなフロー図であって、自分は今 `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` の場合、引数の `master` と `MR_Branch` はブランチ名の代わりに、特定のコミットの SHA-1 コードにすることもできます。ただ 2 番目の引数も特定なコミットにした場合、もし Rebase 後にこのコミットを保持しているブランチがなければ、当然ながらそのコミット（及びそのコミットより以前の、どのブランチにも保持されていないコミット）も消えてしまいます。ヤッチマッター、ってなった場合は、是非とも先ほどの操作の巻き戻し方をチェックしてくださいね☆（ゝω・）vｷｬﾋﾟ

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