はらへり日記

腹に弾丸

【翻訳】Gitで様々なUndoを行う方法

はじめに

この記事はThe GitHub BlogHow 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 refloggit 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 --hardmasterブランチをあなたの新しい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 featuregit 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コマンドを使用します。以下に使用例を示します。

squashfixupは1つ上のcommitと結合します-"結合"コマンドを用いることでその直前のcommitがmergeされます。今回のシナリオの場合、0835fe26943e85の2つのcommitが結合されその後、38f5e4eaf67f82のcommitが別のcommitとして結合されます。

あなたがsquashを使用する時、Gitは結合されたcommitの新たなcommitメッセージを入力するよう促します。あなたがfixupを使用する時、結合するcommitのうち一番新しいcommitメッセージを採用します。今回の例だとaf67f82ooopsコミットです。なので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 --squashsquash! 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の^~を使用するとよいでしょう。例えば^HEADHEADから1つ前のcommitを意味します。HEAD~4HEADから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生活を送りましょう。

和訳ミスあれば何かしらで教えていただけると嬉しいです。以上!