例によって前書き(という名の無駄話
転職しました。はい。もう 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 を使って、まずは自分だけ幸せになる。ここで簡単に使い方を紹介してみると、例えば今現在はこんなフロー図であって、自分は今 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 ライフを送ってください。