NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

【Git】後から必要なくなった実装を取り消す方法~revertとreset~

本記事は  【いまさら聞けない○○ウィーク~Git編~】  1日目の記事です。
🍦  告知記事  ▶▶ 本記事 ▶▶  2日目  💻

はじめに

みなさん初めまして。2022年11月にNRIネットコム株式会社に中途入社した上村です。入社してから、Javaを用いたWebアプリケーションの開発に携わっています。開発業務では、ソースコードをGitでバージョン管理しています。Gitは使いこなせると非常に便利ですが、初見では分かりにくい機能も多くあります。今回は、後から必要なくなった実装を取り消す方法についてフォーカスを当てた記事を書きたいと思います。

前提

本記事の内容は、いくつか前提知識が必要です。従って、それぞれ概要レベルで説明します。

Gitのステージングとコミット

Gitでは、リポジトリにコミット単位でファイルの情報を保存し、バージョン管理しています。機能の実装が一通り完了した段階で、以下順序でリポジトリにコミットを反映します。

  1. 反映対象ファイルに対して、git add [ファイル名]コマンドでインデックスに登録する。
  2. インデックスに登録されたファイル一式をgit commit -m [コメント]コマンドでコミットしてリポジトリに反映する。

なお、上記1.の、インデックスにファイルを登録する作業をステージングと呼びます。ステージングしてからコミットする流れとすることで、リポジトリに反映したいファイルのみをコミットすることができます。

どのファイルがインデックスに登録されているかは、git statusコマンドで確認できます。

HEADは、自分が現在どのコミットにいるかを指します。大抵の場合、ブランチの先頭を指します。

どんなときに実装を取り消したいか

開発を進めていると、実装した部分が何らかの事情で必要なくなったと後々分かり、実装を取り消したい場面も出てくるかと思います。

例えば、ある機能を実装するのに、3つ「実装A、実装B、実装C」に分けてコミットしたが、後で実装Bに対する仕様は考慮しなくてよいと分かり、実装Bが不要となったという場面が挙げられます。

実装を取り消す方法

実装を取り消すとき、手動で戻すのではなく、実装Bに対するコミットを取り消すことができると、機能Bの実装をすぐに元に戻せます。

有名な方法として、revertコマンド・resetコマンドを使う2種類があります。それぞれ、戻し方が大きく異なります。

以降、それぞれのコマンドについて説明します。具体的な使い方として、コマンドラインでの扱い方も併せて示します。GitのGUIクライアント(TortoiseGitなど)が充実しているので、実際の開発ではコマンドラインでなくGUIクライアントの方が利用しやすいと思いますが、一般性を重視するという意味でコマンドラインで示します。

revertコマンド

revertコマンドは、取り消し対象のコミットに対する「打ち消し」をコミットします。revertコマンドは、指定したコミットをピンポイントで打ち消すことができるのが特徴です。

コマンドは、以下の通りです。

git revert [もとに戻すコミットのID]

具体的には、下記コンソールの通り、実装Bのコミットに対する打ち消し内容がコミットされます。これで、実装Bのみがコミット前の状態に戻ります。

resetコマンド

resetコマンドは、指定したコミットまでHEADを戻し、戻した分のコミットを削除します。注意点として、revertコマンドと異なりピンポイントでコミットを戻すことができないです。例えば、実装Bのコミットを取り消す場合、その後ろにある実装Cのコミットも消えます。従って、取り消し後に実装Cのコミットを手動で行う必要があります。

コマンドは、以下の通りです。

git reset [HEADの移動先とするコミットID]

また、resetコマンドにはオプションが用意されています。

代表的なオプション 意味 影響度
--soft インデックスと作業ディレクトリはどちらもコミット前の状態に戻さない
--mixed インデックスはコミット前の状態に戻すが、作業ディレクトリはコミット前の状態に戻さない
--hard インデックスと作業ディレクトリどちらもコミット前の状態に戻す

インデックスをコミット前の状態に戻すとは、削除されたコミットに含まれていたファイルがステージングされていない状態になることを意味します。つまり、resetコマンド実行後にそのファイルを編集して再度コミットする際、改めてそのファイルをステージングする必要があります。なお、インデックスをコミット前の状態に戻しても、実装内容はコミット後のまま変わらないです。

作業ディレクトリをコミット前の状態に戻すとは、実装内容がコミット前の状態に戻ることを意味します。従って、インデックスをコミット前の状態に戻す場合に比べて、より影響度の大きい処理となります。

今回は実装Bの作業ディレクトリをコミット前の状態に戻したいので、以下の通り--hardを指定してresetコマンドを実行します。

実行後、以下の通り実装Aのファイルのみ残っていて、実装Bと実装Cのファイルが残りません。従って、reset後に手動で実装Cのファイルを復元し、再度コミットする必要があります...

また、実装Bの作業ディレクトリをコミット前の状態に戻すことができないので今回の場合は不適ですが、--hard以外のオプションを指定する場合の挙動についても、併せて以下に示します。

--softを指定すると、実装Bと実装Cのファイルが残ります。以下の通り、インデックスにも登録されたままの状態となっています。

実装Bと実装Cのコミットが消えて、実装Aのコミットが残っていることも確認できています。

--mixedを指定する場合も、実装Bと実装Cのファイルが残ります。一方、以下の通りインデックスから登録が消えています。 なお、オプションを指定せずresetコマンドを実行しても、--mixedが指定された場合と同じ挙動になります。

結局どの方法がいいのか

上記の例では、コミットを取り消す手間がどれだけかかるかという観点で、revertコマンドが適切だと思います。

また、コミットをリモートリポジトリ(Gitサーバ上のリポジトリ)にプッシュした後に取り消す場合、revertコマンドを使うのがよいと考えています。理由は、プッシュ後のコミットをresetコマンドで取り消すと、他の開発者にも影響を与え、コンフリクトなどエラー発生の原因となるからです。revertコマンドは、resetコマンドに比べて安全にコミットの取り消しが可能です。

逆に、もう一方のコマンドが活躍する場面はどこか

一方、他の開発者に影響を与えない範囲で取り消し履歴を残したくない場合はresetコマンドの方が便利だと考えます。

例えば、あるライブラリの挙動がローカルでどうなるかを調べる目的でファイルを編集し、挙動確認が完了したのでコミットせず編集前の状態に戻したい場面が考えられます。開発者のローカルに完全に閉じた話なので、取り消し履歴を残す必要性は低いのが理由です。

上記ファイルは、以下コマンドで削除できます。

git reset --hard HEAD

HEADを対象としているので、前のコミットへHEADを移動する処理は発生しませんが、未コミット分のファイルは取り消されます。

コミットの取り消し機能を有効に活用するために

ここまで、例として実装Bのみを取り消す前提で説明しました。しかし、コミットの仕方によっては、手動でファイルの編集作業が必要になる場合もあります。例えば、実装Aから実装Cを一つのコミットとしてしまった場合、このコミットを取り消した後、実装Aと実装Cを実装後の状態に手動で戻し、再度コミットする必要があります。

Gitは一般的に、作業単位でコミットを分けるのがよいとされています。その理由の一つが、この通り必要に応じてコミットの取り消しを行いやすくすることにあるからと考えています。

終わりに

Gitを扱う開発で、必要なくなった実装を取り消す方法について説明しました。業務では、私はrevertコマンドとresetコマンド相当の操作をGUIを通して行うことが多いですが、適切に扱うことができればどちらも非常に便利なコマンドだと感じます。

学生時代からGitを利用していましたが、奥が深く、まだまだ知らない機能がたくさんあります。Gitについて知っているつもりでも、アウトプットすることで実は知らないこともあることに気づけました。今後もインプットとアウトプットを両輪で進め、技術への理解を深めていきます。

執筆者:上村 真弘 Webアプリケーションの開発をしています。