このブログははてなブログからの移行記事です。
はじめに
この記事はThe GitHub BlogのHow to undo (almost) anything with Gitを和訳したものです。
書こうと思った動機は
Gitで様々な処理をロールバックする方法がわかりやすくまとまっているので自分用に整理
英語が超苦手で克服したいから
って感じです。
和訳ミス等あればご指摘いただけると嬉しいです。
※ちなみに本家GitHubに翻訳してもいいですかと聞いたらWe'd only ask that you please link back to the original blog post as part of doing this.
と言われました。素敵な会社!
補足
SHA
とは1つ1つのcommitに割り振られる一意性のハッシュ値のことです
以下和訳
いかなるバージョン管理システムに存在する便利な機能の中でも、特に便利な機能があなたのミスを"undo"する機能です。
Gitにおいての"undo"は、様々な意味を持ちえます。
あなたが新しくcommitをする時、Gitはあなたのリポジトリのスナップショットを保存します―後にあなたが、あなたのプロジェクトの昔のバージョンを辿るためにGitを使えるようにするために。
この記事では、あなたが行った修正を"undo"したいときのいくつかのシナリオとそれをGitで行うベストな方法を紹介します。
Publicな処理を"undo"する
シナリオ
あなたはたった今、GitHubにあなたの修正を反映するためにgit push
を実行しました。しかし、その修正のうち、1つのcommitに重大な問題があることに気づきました。あなたはそのcommitを"undo"したい。
解決方法
git revert <SHA>
を実行
何が起こるのか
git revert
は指定された<SHA>
のcommitとは全く別の(もしくは反対の)commitを実行します。もしも修正したいcommitを"matter"とするならば、新しいcommitの内容は"anti-matter"となります-例えば修正したいcommitが何かしらのファイルを削除するcommitであれば、それらを追加する処理が新しくcommitされるし、修正したいcommitが何かしらのファイルを追加するcommitであれば、それらを削除する処理が新しくcommitされます。
このシナリオはGitの中でも最も安全で基本的な"undo"処理になります。なぜならばこの処理はGitの履歴を変更するものではないからです-この処理によって、あなたは修正したい内容と正反対のcommitをgit push
することであなたのミスを"undo"することができます。
最後のcommitのメッセージを修正する
シナリオ
あなたはcommitメッセージを打ち込んだ。あなたはgit commit -m "Fxies bug #42"
とcommitを実行したが、git push
する前にcommitメッセージを"Fixes bug #42"
に修正したい。
解決方法
git commit --amend
もしくは git commit --amend -m "Fixes bug #42"
何が起こるのか
git commit --amend
は一番最近のcommitを現在新たにステージングされているあらゆる変更全てを結合した新しいcommitに置き換えます。もしも現在のステージングに一切の変更がない場合、この処理は直前のcommitメッセージを修正するのみに留まります。
ローカル上での変更を"undo"する
シナリオ
にゃんこがキーボードの上を横切り、何かしらの変更をファイルに記録してしまい、エディタがクラッシュしてしまった。しかし、あなたはまだそれらの記録をcommitしていない。あなたはそのファイルにおいての変更の一切を"undo"したいー記録されている最後のcommit時の状態まで戻したい。
解決方法
git checkout -- <bad filename>
何が起こるのか
git checkout
はワーキングディレクトリ―に存在するファイルをGitが知りうる直前の状態まで戻します。あなたが戻したい状態に応じて特定のSHA
やブランチ名を指定することも可能です。デフォルトではGitはHEAD
の状態まで戻したいと判断し、その時のブランチの一番最後のcommit時の状態まで戻します。
注意点:この処理を行って変更されたことは決して元には戻りません。これらの変更は決してcommitされず、Gitはこれらの変更を元に戻すことはできません。この処理を行う際はgit diff
等を用いて、今から何が変更されるかを把握し、問題無いと把握したうえで行ってください!
ローカル上での変更をリセットする
シナリオ
あなたはローカル上でいくつかのcommitを行いました(まだgit push
は行っていません)。しかし、全てが最低最悪であり、あなたは直前の3つのcommitを"undo"したいと思っています-それらの処理が二度と行われないように。
解決方法
git reset <last good SHA>
もしくは git reset --hard <last good SHA>
何が起こるのか
git reset
は指定されたSHA
のcommitの状態まで全てを巻き戻します。まるでそれらのcommitが行われなかったかのように。デフォルト(オプション無し)だとgit reset
はワーキングディレクトリ―の状態を保護して実行され、巻き戻されたcommitは永遠に失われますが、ワーキングディレクトリ―の状態は残ったままです。これは最も安全なオプションです。しかし、いくつかのcommitとワーキングディレクトリ―の状態全てを"undo"したい場合、--hard
オプションがそれを実現します。
ローカル上の変更を"undo"した後、Redoする
シナリオ
あなたがいくつかのcommitを行い、それらを"undo"するためにgit reset --hard
を実行した(上記参照)。そしてその後に気づいた…あなたが行った"undo"を元に戻したい!
解決方法
git reflog
と git reset
もしくは git checkout
何が起こるのか
git reflog
はリポジトリの歴史を修復する驚くべき機能です。あなたはgit reflog
によってあなたが行ってきたcommitのほとんどを修復することができます。
おそらくあなたにもなじみがあるであろうgit log
コマンドはcommit一覧を表示します。git reflog
も似ています。ただし、かわりにHEAD
が変更された時間の一覧を表示します。
いくつかの注意点
表示するのは
HEAD
の変更のみです。HEAD
はブランチを切り替えた時、git commit
を使ってcommitを行った時、そしてgit reset
でcommitを削除した時に変更されます。しかし、git checkout -- <bad file name>
の際には変更されません(先ほどのシナリオでも軽く触れましたが、checkout
された変更は二度と戻ってきません。ゆえにreflog
ではcheckout
時の変更を修復することは不可能なのです)。git reflog
で表示されるものは永遠には保存されません。Gitは定期的に使用されていない情報を削除します。一ヶ月前のreflog
情報を未来永劫、必ず見つけられるとは限りません。あなたの
reflog
はあなただけのものです。あなたは他の開発者のpushされていないcommitに対してgit reflog
を行うことはできません。
それではどのようにreflog
を使用して"undo"されたcommitを"redo"すればよいのでしょう?それはあなたが本当に行いたいことによって変わってきます。
もしあなたがリポジトリの状態をある特定のタイミングの状態に戻したい場合、
git reset --hard <SHA>
を用いるとよいでしょう。もしあなたがある瞬間にワーキングディレクトリ―に存在していたファイルを修復したい場合、
git checkout <SHA> -- <filename>
を使用することでGitの履歴を改変することなく実現できるでしょう。もしあなたが
undo
したcommitのうちちょうど1つを再現したいのであればgit cherry-pick <SHA>
を用いるとよいでしょう。
ブランチに関して
シナリオ
あなたはいくつかのcommitを行ってからブランチがmaster
になっていることに気づいた。あなたはそれらのcommitをmaster
ブランチでなく、代わりにfeatureブランチに反映させたい。
解決方法
git branch feature
, git reset --hard origin/master
, およびgit checkout feature
何が起こるのか
あなたが新たなブランチを作成するとき、恐らくgit checkout -b <name>
を使用しているでしょう。それはブランチ作成とcheckoutを同時に行うポピュラーなショートカット方法です。
しかし、今回の場合あなたはすぐにブランチを切り替えたいわけではない。git branch feature
はあなたの最新のcommitが反映されたfeature
と名付けられたブランチを作成します。この段階ではmaster
ブランチにcommitが残ったままです。
次に、git reset --hard
でmaster
ブランチをあなたの新しいcommitがなされる以前のorigin/master
ブランチの状態まで巻き戻します。巻き戻しても心配は不要です。新しいcommitたちはfeature
ブランチに反映されています。
最後にgit checkout
によってあなたの新しいcommitが反映されたfeature
ブランチに切り替えることができます。
Branch in time saves nine
シナリオ
あなたはmaster
ブランチから派生したfeature
ブランチを作成しました。しかし、master
ブランチはorigin/master
よりとても古い状態です。あなたは古い状態のmaster
からではなく、origin/master
と同期されたmaster
の状態からfeature
ブランチをスタートさせたい。
解決方法
git checkout feature
とgit rebase master
何が起こるのか
あなたはgit reset
(--hard
オプションは使用しない。意図的に変更を保存します。)し、git checkout -b <new branch name>
し、それから新たにcommitを行うことでもこの問題を解決することがあります。しかしこの方法だとcommitログが消えてしまいます。
これよりもよい方法があります。
git rebase master
はささいなことを実現します。
最初に、このコマンドは現在のブランチと
master
の間で同じ歴史を共有します。その後、現在のブランチのログのうち、共有された歴史以降のログをリセットします。リセットされたログは一時的に保存されます。
現在のブランチを
master
の最新の状態まで進め、先ほど一時的に保存されていたログをmaster
の最新のcommitの後に付け足します。
大量のUndo/Redo
シナリオ
あなたはある方向性に向かって開発をスタートしました。しかし途中の段階でよりよい解決方法があることに気づいてしまいました。
あなたは大量のcommitを行いましたが、残しておきたいのは一部です。それ以外のcommitは不要なので消してしまいたい。
解決方法
git rebase -i <earlier SHA>
何が起こるのか
-i
オプションはrebase
を"interactive mode"で実行します。実行すると前述したようにrebaseがスタートします。また、commitを再現する際に都度一時停止し、あなたはその際にそれらのcommitを編集し、反映することができます。
rebase -i
はあなたのデフォルトのエディタを開き、適用されたcommitのリストが以下の図のように表示されます。
行の最初の2つのカラムがポイントです。
1つ目のカラムは2つ目に示されたSHAに紐づくcommitに適用されているコマンド名です。rebase -i
はデフォルトで全てのcommitをpick
として処理します。
commitを削除したい場合、該当するcommitの行をエディタ上で削除することで実現できます。上記の写真でよろしくないcommitを削除したい場合、1行目と3~4行目を削除することで実現できる(commitメッセージにwrongと書かれている)。
もし、commitの内容でなくcommitメッセージのみ修正したい場合、reward
コマンドで実現できます。試しに1行目のコマンドをpick
からreward
に書き換えてみましょう(r
でも構いません)。
すぐにcommitメッセージを修正することができます。ただし、それは直後には反映されません。rebase -i
コマンドで表示される内容のうち、commitのSHA以降の文字列は無視されます。これらは、例えば0835fe2
の行がどんなcommitであるかを思い出すために表示されているだけなのです。rebase -i
の処理を終えた後、指定したcommitのcommitメッセージを書き換える画面が新たに表示されます。
もし複数のcommitを結合させたければsquash
コマンドとfixup
コマンドを使用します。以下に使用例を示します。
squash
とfixup
は1つ上のcommitと結合します-"結合"コマンドを用いることでその直前のcommitがmergeされます。今回のシナリオの場合、0835fe2
と6943e85
の2つのcommitが結合されその後、38f5e4e
とaf67f82
のcommitが別のcommitとして結合されます。
あなたがsquash
を使用する時、Gitは結合されたcommitの新たなcommitメッセージを入力するよう促します。あなたがfixup
を使用する時、結合するcommitのうち一番新しいcommitメッセージを採用します。今回の例だとaf67f82
はooops
コミットです。なので38f5e4e
のcommitメッセージを採用します。しかし、0835fe2
に結合される6943e85
のcommitメッセージはあなたが新たに入力します。
エディタの内容を保存し終了すると、あなたの入力した内容をGitが反映します。
エディタを終了する際、commitの順序を書き換えることでcommit順番を変更することができます。例えばaf67f82
のcommitを0835fe2
と結合したければ以下のように書き換えることで実現できます。
最近のcommitを修正する
シナリオ
あなたは最近のcommitにファイルを含めるのを失敗してしまった。ファイルをどうにかして新たなcommitに含めることができればよかったのに…。あなたはまだ変更をpushしておらず、しかし該当のcommitは最新のものではないのでgit --amend
では問題を解決できません。
解決方法
git commit --squash <該当commitのSHA>
とgit rebase --autosquash -i <該当commitのSHA>
何が起こるのか
git commit --squash
はsquash! Earlier commit
といういメッセージとともに新たなcommitを作成します(自分でもこのメッセージをつけてcommitすることも可能ですが、commit --squash
であればその手間が省けます)。
新たなcommitメッセージをつけることを希望しないのであればgit commit --fixup
も使用可能です。もしあなたが今から修正しようとしているcommitメッセージをそのまま使うのであればrebase
している最中にcommit --fixup
するのがよいでしょう。
rebase --autosquash -i
はインタラクティブなrebase
エディタを規定エディタで立ち上げます。しかし、エディタの内容には下記のようにsquash
もしくはfixup
が該当のcommitに結合する形で反映されています。
--squash
や--fixup
を使用するときは修正したいcommitのSHA
を覚えておく必要はないでしょう。そのcommitが1つから5つほど前のcommitであるならば、Gitの^
や~
を使用するとよいでしょう。例えば^HEAD
はHEAD
から1つ前のcommitを意味します。HEAD~4
はHEAD
から4つ前のcommitを意味します-すなわち5つ戻ったcommitということです。
ファイルをGitの管理から外す
シナリオ
あなたは誤ってapplication.log
をリポジトリに追加してしまいました。あなたのアプリケーションは常に動いているのでapplication.log
のステージングされていない変更をGitが警告してきます。.gitignore
に*.log
を追記したものの、application.log
はすでに追加されています。
どのようにしてこのファイルをGitの管理下から外せばよいのでしょう。
解決方法
git rm --cached application.log
何が起こるのか
.gitignore
に条件を追加する前に一度、ファイルをリポジトリに追加すると、Gitは常にそのファイルを管理してしまいます。同じような例としてgit add -f
を使用して.gitignore
を回避した場合にも同じことが起こります。ファイルをadd
する際は-f
オプションを使用しないほうが良いでしょう。
もしもあなたがリポジトリで管理されるべきでないファイルをGitの管理下から外したい時、git rm --cached
を使用することでファイルを削除することなく実現することができます。これによりファイルは除外され、git status
で表示されることもなく、また間違えてcommitすることもなくなります。
Gitでundo
する様々な方法を紹介しました。もしここで紹介したコマンドについて学ぶ場合は以下のドキュメントを読んでください。
翻訳以上!!!
まとめ
- Gitって難しい
rebase -i
は神- 英語って難しい
Gitを使う人は全部絶対に使えるテクニックなのでぜひ覚えて幸せなGit生活を送りましょう。
和訳ミスあれば何かしらで教えていただけると嬉しいです。以上!
COMMENT: ご指摘ありがとうございます。 修正いたしました! COMMENT: 先頭のオリジナルの 英語のタイトル部分で undo が unde になっています。