📜

[初級者~中級者向け]gitのコミット履歴を自由に編集するためのノウハウ集

2024/08/05に公開

この記事の目的

皆さんは、例えばプルリクエストで下記のような指摘があったときにどうしているでしょうか。

  • 直前または何個か前のコミットで余分なファイルが含まれている
  • ちょっと1コミットの粒度が大きいので、分割してほしい
  • 変数名やコミットコメントに誤字があるため、修正してほしい
    などなど

筆者が今までの現場で一緒に働いてきた人の中には、以下のような泥臭い方法でこれらの操作を実現している人が意外に多かった印象です。

  • 単純に、"fix"や"レビュー指摘対応"のような修正用のコミットを作成する
  • 一度最新のファイルをメモ帳にコピペ、 git revertで一度編集したいコミットを打ち消し、コピペしたファイル内容を一部修正して再度コミット
  • 同様にメモ帳にコピペしたあと、新しいブランチを切り直し、再度ペーストしてgit add -p 場合によってはコピペしたファイルからパッチ単位でペーストして(!)希望の粒度でコミットし直し、再度プルリクエスト(前のプルリクエストはクローズ)

このような方法でコミット履歴の操作して実現している人も多いのではないでしょうか。
もちろん、これらの方法でもやりたいことは実現できます。ただコミット履歴が汚くなったり、操作が煩雑で作業中のミスを誘発したりといったデメリットがあります。

実はこの辺の操作はgit resetgit rebase -igit commit --amendを使いこなすことができればもっと簡単に実現可能です。

これらのコマンドの使用方法について意外に知らない人も多く、また「git commit 編集」などで検索してもあまりノウハウをまとめているような記事は出てこなかったので、この記事ではコミット履歴を編集するいくつかの方法とその原理を解決したいケース別に分けて解説します。

これらの方法と原理を覚えれば、冒頭に上げたようなケースや、それ以外のケースにも対応できると思います。

前提知識

  • gitの基本的な操作
  • インデックスとHEADコミットについての知識
    • 一応なんとなくふわっとわかるように書いているつもりではありますが、この記事では詳しくは解説していないので、もし全く聞いたことがないような状態ならこちらの記事などを読んで頂くとわかりやすいかもしれません。

最初に伝えておきたいこと

コミット編集は怖くない ということです。

この記事ではgit commit --amendgit rebase -iを使用してコミットを編集する方法を記載しますが、編集した内容は下記の手順で容易にもとに戻すことができます。

  1. git reflog で自分が戻したいコミットのIDを調べる(git reflogではユーザが移動・編集したすべてのコミットの履歴が表示されます)
  2. git reset --hard 1で調べたコミットIDを実行する

もしもリモートブランチに最後にpushした状態に戻したいだけであればさらに簡単に戻せます。

  1. git reset --hard origin/ブランチ名を実行

一度コミットを行っていれば、その時点に作業状況を戻すのは容易です。なので、失敗を恐れずにバンバン編集しましょう。git rebase -iなどは何をしているかちゃんと理解しないで進めると(例えばgit rebase --skipなどを適当に実行してしまったりすると)コミットが結構めちゃくちゃになってしまうケースがありますが、もしそうなってしまっても一度上記の手順で戻してしまえば大丈夫です。

ただし、これは自分だけが作業しているブランチのコミットを編集する場合に限ります。
他の人が作業しているブランチは、基本的に編集しないでください。

もしも自分以外の作業者がいるブランチのコミットを編集するのであれば、git push --force-with-releaceだけは密に連絡を撮ってから行うなど、細心の注意を払って行ってください。リモートに反映しなければ他の人の作業に影響を与えることはないはずです。

git push --force-with-releaceはリモートブランチを整合性を無視して強制的に上書きするため、他の人が参照しているブランチに対して行うと、他の人がpushやpullが行えない状態になる可能性があります。

基本的に自分しか作業しないブランチなのであれば、ローカルブランチを編集してそれを再度git push --force-with-releaceで上書きするだけなのでそこまで神経質にならなくてもいいと思います。

直前のコミットコメントが間違っているので修正したい

コミットしたあとに、コミットコメントの誤字に気づくということはよく起こると思います。

対応方法

  1. git commit --amendを実行 設定されたエディタでコミットメッセージが開かれるので修正して保存する
  2. git push --force-with-releaceでリモートブランチに反映

解説

git commit --amendを使用すると直前にコミットしたコミット、つまりカレントブランチのHEADコミットを編集することができます。

今回はコメント以外は変わらないので、単にgit commit --amendするだけでOKです。

直前のコミットでコミットしたソースコードに誤字があるので修正したい

これもよくあるシチュエーションです。

対応方法

  1. 該当箇所を修正する
  2. git add -uを実行する
  3. git commit --amendを実行する(コミットコメントはそのまま保存)
  4. git push --force-with-releaceでリモートブランチに反映

解説

これは、git commit--amendオプションをつける以外は普通にコミットを行うのと変わらないのでそんなに難しくないかと思います。

git addは普段コミットを行う際に使用しているコマンドだと思います。(オプション-uをつけることでgit管理下になっているファイルすべてを対象にできます)
何気なく使っているこのコマンドがですが、正確には何を行っているかというと、インデックスに編集内容を追加しています。
通常のコミットを行う際には、インデックスに追加した後git commitを実行し、インデックスの内容で新たなコミットを作成しますが、コミットの修正を行いたい場合にはgit commit --amendを実行し、インデックスの内容でHEADコミットを上書きします。

直前のコミットで誤ったファイルをコミットしてしまったので、コミットから外したい

例えば、git add .を使用したことで、メモ書きのようなファイルを誤ってgit管理下に含めてしまうようなこともあるかと思います。

対応方法

例としてpoem.mdというファイルを誤ってコミットしてしまった場合のことを考えます。

  1. git reset HEAD~ -- poem.mdを実行する
  2. git commit --amendを実行する(コミットコメントはそのまま保存)
  3. git push --force-with-releaceでリモートブランチに反映

解説

これはちょっと馴染みのない操作かもしれないです。
git reset <COMMIT> -- <path>コマンドは、pathのインデックスをCOMMITの状態に戻すコマンドです。addの逆ですね。(ドキュメントにも"This means that git reset <pathspec> is the opposite of git add <pathspec>."との記載があります)
ここではCOMMITにHEAD~、つまりHEADコミットの一つ前のコミットを指定しています。
インデックスを変更したあとはgit commit --amendを実行すれば、今までと同様インデックスの内容でHEADコミットを上書きします。

余談 resetreset -- <path>の違い

ちなみに、git reset <COMMIT> と git reset <COMMIT> -- <PATH>は同じgit resetですが、やっていることは結構違います。
前者はHEADコミットをCOMMITで指定したコミットに移動するコマンドです。なので、git reset HEAD~としてしまうと、編集したいコミットの前にHEADコミットが移動してしまいます。こうなると、編集後のコミットにも含めたい修正内容やコミットメッセージも消えてしまいます。

直前のコミットをいくつかに分割したい

作業中は気が付かなかったが、あとから自分でコミットを見なおしたら少し1コミットに含まれる修正量が多すぎた。というような場合もあるかと思います。

対応方法

  1. git reset -p HEAD~ -- .で、対話的に1つ目のコミットに含めたくない修正箇所を指定してインデックスを戻す(もしくは、git reset HEAD~ -- .で戻した後、含めたい部分をgit add -pで指定してもOK)
  2. git commit --amendでコミットを上書き
  3. git add -uを実行してインデックスに残りの修正箇所を追加する
  4. git commitを実行して新たなコミットを作成する
  5. git push --force-with-releaceでリモートブランチに反映

解説

基本的には一つ前のケースの応用です。git reset -p HEAD~ -- .とすることで、対話的にインデックスを戻す箇所を指定することが可能になります。あとは今までのケースと同じで、一部の変更のみがインデックスに残っている状態になるので、それをgit commit --amendで上書きします。
その後、コミットされていない修正点が残るので再度git add -uでインデックスに反映し、新規のコミットを作成します。

直前のコミットより前のコミットを編集する方法

これまでのケースを見ると、HEADコミットを修正する場合には何らかの手段でインデックスを修正したい状態に合わせ→commit --amendでHEADコミットを上書き、という流れで修正していることがわかるかと思います。
では、HEADコミット以外のコミットを修正したいときはどうすればいいのでしょうか。
これはgit rebase -iで実現できます。

対応方法

具体例として、2つ前のコミットのソースコードを修正する場合を考えます。

  1. git rebase -i HEAD~~を実行
  2. 直前のコミット(HEADコミット)と、その一つ前のコミット(HEAD~)を含む以下のようなリストがエディタで開かれる
pick 192837 hogehoge
pick 5a6c7e fugafuga
  1. 編集したいコミット (上から時系列順に表示されるので、今回は二行目)の"pick"を"edit"に変更し、ファイルを保存する
  2. ソースコードを編集する
  3. git add -u を実行
  4. git commit --amendを実行
  5. git rebase --continueを実行
  6. git push --force-with-releaceでリモートブランチに反映

解説

git rebase -i <COMMIT>を使用することで、様々な変更をコミットに加えることが可能になります。

なおgit rebase -i <COMMIT>は単に編集を行うコマンドではなく、そのコマンドが示す通り<COMMIT>で指定されたコミットでrebaseも同時に行います。ただし、ここでは特定のブランチでなく、自分の親コミットを指定しています。
rebaseが何かについて、ここでは詳しく解説しないのでわからない方は別途調べていただきたいですが(こちらの記事がとてもわかりやすかったです) 、ざっくり指定されたコミットを起点に現在のHEADコミットまでがもう一度コミットされる(積み直される)イメージで考えていただくとなんとなくわかりやすいかもしれません。

git rebase -i COMMITとすることで、指定したコミットの直後までのリストがエディタで開かれます。つまり、rebaseによって再度コミットが行われるコミット達が表示される形になります。git reset HEAD~~とすれば、HEADコミットとその前のコミットの2件のリストが開かれます。

"pick"を指定した場合、単にそのコミットが再度コミットされるだけですが、このリストの"pick"を任意の操作にすることで、コミットに対して様々な操作を行うことが可能です。
コミットに対して行える操作は様々ですが、ここではコミットの内容を編集したいため、editを指定します。
editを指定すると、そのコミットを行った直後で一時的に再コミットの操作がストップされます。これによって、HEADブランチは指定されたコミットになります。

その後は、今までのケースで見てきたようにやりたいことに合わせてインデックスを変更し→git commit --amendでコミットの内容をインデックスの内容で上書きすることで、コミットを編集することが可能です。

編集した後はgit rebase --continueを行います。このコマンドを実行することによって、停止していたコミットの積み直し操作が再開されるイメージです。

git rebase中にConflictが発生したら

先程記載したようにgitのリベース操作は一度行ったコミットを再度積み直すような操作になります。
その途中でコミットに編集を加えると、前提になっているコミットを修正することになるので、後のコミットでは矛盾が生じる場合があります。例えばコミットの編集によって特定のファイルの追加そのものがなかったことになっていれば、当然その特定のファイルに追記などをするコミットは正しく適用できません。

この場合とれる選択肢は3つあります

  • コミットで生まれたconflictを解消し、git addでインデックスに追加したらgit rebase --continueでリベース作業を続行する。
    • これはconflictが発生しないようにコミットの内容を修正し、その後適用するような操作になります。
  • git rebase --skipでコンフリクトが発生するコミット自体をなかったことにする(スキップする)
  • git rebase --abortでリベース作業自体を破棄する

git rebase -i の edit以外の操作

前項で記載したようにgit rebase -iにはedit以外の操作もあります。
コミット編集でよく使う操作を以下にリストアップしました。
基本的にeditだけでもやりたいことはできると思いますが、これらの操作を使いこなすことで、素早く、あるいは柔軟にコミット編集が行えると思います。

  • drop
    • これを指定したコミットを削除します。
  • reword
    • これを指定したコミットのコミットメッセージの編集のみを行います。
  • fixup,squash
    • これを指定したコミットと直後のコミットの結合を行います。fixupでは、コミットメッセージは直後のコミットのものになります、squashではコミットメッセージの編集が行われます。
  • コミットの順番を入れ替える
    • これは、単にコミットの行を移動するだけで可能です。コミットの順番が入れ替えた通りの順番になります。

終わりに

いかがでしたでしょうか。

複雑な印象のあるコミットの編集ですが基本的には、**(必要であればgit rebase -iでHEADコミットを移動し→)インデックスをgit addgit resetなどの操作で修正→git commit --amendを使用することでHEADコミットをインデックスの内容で上書き→(git rebase --continue)**という操作を行っているということがわかれば、そこまで難しくはないと思います。

これは筆者の働いてきた現場の偏りに過ぎないかもしれないですが、一昔前は「コミットの編集自体悪」という雰囲気の現場も多かった印象で、いまいちこのあたりの編集方法が浸透していないイメージがあります。
が、このような操作を駆使して適切にコミットを編集できると、レビューアーも楽ですし、あとから見た時に何を目的にそのコードを作成したかなどの理解も容易になります。

是非この記事で紹介した操作を使いこなして、きれいなコミット履歴を目指してみてください。

Discussion