はじめに
こんにちは、HappyManaです。
GitHubの作業ブランチやPRで、1ファイルの変更まるまる取り消したいってときってありませんか?
git reset
で巻き戻す方法もありますが、該当のコミットを探したり、複数のコミットで該当ファイルが変更されていたり、結構面倒ですよね。
そんなときに、ピンポイントでと特定のファイルの変更を取り消すコマンドを紹介します。
まだそのブランチで1度もcommitしてないとき
まだコミットしてないときは、作業ブランチで以下のコマンドで変更を取り消したいファイルやディレクトリのパスを指定します。
git checkout -- <変更を消したいファイルやディレクトリの相対パス>
commit済みもしくはpush済みのとき
commit済みのときは、作業ブランチで以下のコマンドを叩いて、どのブランチの内容に上書きするのかと、変更を取り消したいファイルやディレクトリのパスを指定します。
git checkout <ブランチ名> -- <変更を消したいファイルやディレクトリの相対パス>
リモートブランチを指定したい場合は、以下のように書くことで指定できます。
git checkout origin/<ブランチ名> -- <変更を消したいファイルやディレクトリの相対パス>
このコマンドを使うことで、指定したブランチの指定したパスのファイルの内容に変更されます。
注意点
このコマンドには挙動に関して、いくつか注意するポイントがあったので記します。
1. ブランチを指定しない時と指定した時でインデックスファイルの挙動が変わる
インデックスファイルとは、コミットする前段階のファイルたちのことです。(git addした状態)
ただの変更ファイルだと、ブランチを指定しても指定しなくても変わらないんですが、インデックスファイルでは、ブランチを指定しなかった時と指定した時で以下の違いが出ます。
ファイルの状態 | ブランチ指定なし | ブランチ指定あり |
---|---|---|
変更しただけのファイル | 上書きされる | 上書きされる |
インデックスファイル | 上書きされない | 上書きされる |
これに関しては、後述するGitHubの公式ドキュメントを読んでみるでなぜこの挙動になるのか解説しています。
2. 指定したブランチのファイルの内容に変更されるため、指定ブランチに余計な変更が入ってた場合、余計な変更も入ってしまう。
例えばgit checkout main -- hoge.txt
と叩いた時に、作業ブランチがmainブランチの最新の変更を取り込んでいなかった場合、mainと作業ブランチで差分があり、その差分も変更に入ってしまいます。
本来は、git merge mainなど差分を取り込んだときの変更と同じなため、どのコミットでその変更が入るかという違いでしかないです。
なので、そのままcommitしてpushしても、そのPRがmeinブランチにマージするPRの場合、差分は出ないのでfile changesには表示されないとは思います。
しかし、コミット上あまり気持ちよくないので、先にgit merge main
で最新の変更をとったあとにコマンドを叩きましょう。
3. 指定したブランチにファイルがなかった場合、エラーになりファイルは削除されない
例えば、feature/A
ブランチで、hoge.txtというファイルを新規作成した場合に、git checkout main -- hoge.txt
と叩くと、以下のエラーが出ます。
error: pathspec 'hoge.txt' did not match any file(s) known to git
また、その逆で指定ブランチでファイルがあり、作業ブランチでファイルを削除していた場合、ファイルは元に戻りになります。(内容が上書きされるため、ファイルも作成される)
そのため、ファイル名をhoge.txt
からfuga.txt
にリネームしたときにコマンドを叩くと、リネーム後のfuga.txt
はそのままに、リネーム前のhoge.txt
も復元されるという状態になってしまいます。
このコマンドを叩いた時は、commit前に変更ファイルや内容をチェックしましょう。
GitHub公式ドキュメントを読んでみる
GitHubの公式ドキュメントを読むと以下のように書かれています。(deeplで日本語訳)
git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] -- pathspec-from-file=<file> [--pathspec-file-nul]
1.pathspec
にマッチするファイルの内容を上書きする。
2.<tree-ish>
(多くの場合コミット) が指定されていない場合は、ワークツリーをインデックスの内容で上書きする。
3.<tree-ish>
が指定された場合、インデックスとワークツリーの両方を<tree-ish>
の内容で上書きする。
4. インデックスには、以前にマージに失敗したためにマージされていないエントリが含まれている可能性がある。デフォルトでは、このようなエントリをインデックスからチェックアウトしようとすると、チェックアウト操作は失敗し、何もチェックアウトされない。fを使用すると、このようなマージされていないエントリは無視される。
これは、
- pathspec(変更したいファイルやディレクトリのパス)の内容に上書きする。
- tree-ish(ツリーっぽいもの、おそらくブランチ名とかだと予測)を指定しなかったら、ワークツリー(作業しているディレクトリ、要は変更中のファイル)とインデックス(おそらくコミットされている最新のファイルと予測)の内容で上書きする
- tree-ishを指定したら、インデックス、ワークツリー両方をtree-ishの内容に上書きする
-
git merge <ブランチ名>
などをしたときにコンフリクトなどが生じてマージしてないエントリ(ファイル?)があるときは、デフォルトでは何も変更しない。-fをつけるとこれらのエントリは無視される。
こんな感じで解読しました。わかりにくくてすみません。インデックスとワークツリーをわかりたい方はGitのWorking TreeとIndexが分かる記事がわかりやすく説明されているため、おすすめです。(tree-ishは調べてもよくわからなかったです...)
また、2のtree-ishを指定しない場合インデックスの内容で上書きし、3のtree-ishを指定する場合インデックスもtree-ishの内容に上書きされるとあります。
ここが、注意点1で書いた、ブランチ名を指定した時と指定してない時でインデックスファイルの挙動が変わることにつながっているのではないかと思っています。
おそらく、インデックスファイルはインデックスの方のファイル扱いになり、ブランチ名を指定しない時はインデックスの内容になるためそのまま、ブランチ名を指定した時はインデックスも上書きされるという挙動になると思います。(違ったらすみません)
git checkout (-p|--patch) [<tree-ish>] [--] [<pathspec>…]
これは前のモードと似ているが、対話的なインターフェイスを使用して "diff "出力を表示し、結果に使用するハンクを選択することができる。patchオプションの説明は以下を参照のこと。
これは、コマンドを叩いた時に、git diffの出力をして、1行1行その変更をするかを確認しながら上書きすることができるオプションです。
参考