Git Undo エイリアスを定義する

このような経験はありませんか?「ローカルのコミットをし過ぎてしまったことに急に気づいてローカルコミットを書き直している最中、rebaseしすぎてしまい、自分が思い描くような履歴になっていなかった」。どうですか? 私はあります。そのような時、「ただCTRL + Zで開始時に戻れればいいのに……」と思います。もちろん、決してそんなに単純ではありません。GUIでさえもです。

そんな絶望的な瞬間を経験することがあったので、git undoコマンドを独自に作成する決心をしました。以下に私のアイデアと、そこに行き着くまでの過程を紹介したいと思います。

Reflog

Gitのundo操作を行うために私が最初に目を付けたのは、reflogです。「reflogとは何だろう?」と思うかもしれませんね。Gitでは、ブランチの参照が移動する度に1、いわばローカルジャーナルと言われる場所に、移動する前の値が記録されます。このジャーナルのことをリファレンスログ、またはreflogと呼びます。

リポジトリには、各ブランチに対するreflogが1つずつと、それらとは別にHEAD参照に対するreflogが1つあります。

ブランチのreflog内のエントリのリストを入手する方法は、git reflog (ブランチ名)と入力するだけの簡単なものです。

git reflog master

これにより、masterブランチのreflog内のエントリが出力されます。

Output of git-reflog for the master branch

HEAD自体のreflogを参照したい場合は、引数を省けばいいだけです。

git reflog

これでHEAD参照のみに対する同出力が得られます。

Output of git-reflog for the HEAD reference

ぱっと見ただけでは分かりませんが、reflog内のエントリは新しいものが順に上から記録されています。

逆にすぐに分かることは、各エントリにはそれぞれインデックスが付いていることです。このインデックスを使うことで、特定のreflogのエントリに関連するコミットを直接参照することができるため、実はこれは非常に有用です。ここでは、reflogのエントリを参照するには以下の構文を用いる、ということだけ言っておきましょう。

reference@{index}

@マークの両側には以下を入力します。

  • referenceには、ブランチ名もしくはHEAD名。
  • indexには、reflog内のエントリの位置2

例えば、HEAD2つ前に参照していたコミットを確認したいとしましょう。その場合は、git showコマンドの後にHEAD@{2}と入力します。

git show HEAD@{2}

また、master直前に参照していたコミットを確認したい場合は、以下のようになります。

git show master@{1}

Undo エイリアス

重要なことは、reflogは、ブランチによって参照されたコミットの履歴を追跡するということです。これはWebブラウザがアクセスしたURLの履歴を追跡するのと同じことです。

つまり、@{1}によって参照されたコミットは、常に現在の値の1つ前に参照したコミットであるということです。

refloをgit resetコマンドと合わせて使う場合の構文は、以下のようになります。

git reset –hand master@{1}

これによって、インデックス作業ディレクトリであるHEADを、ブランチによって参照された直前のコミットに移動することができます。これは、Webブラウザで「戻る」ボタンをクリックするのと同じことなのです!

この時点で、独自のgit undoコマンドを実装するのに必要な全てが揃いました。実装する際のエイリアスは以下の通りです。

git config --global alias.undo '!f() { \
    git reset --hard $(git rev-parse --abbrev-ref HEAD)@{${1-1}}; \
}; f'

これではあまりにも長ったらしくて分からりづらいので、1つずつ区切って見ていきましょう。

  1. !f() { ... } f
    ここでは、fという名のシェル関数をエイリアスとして定義し、それを即座に呼び出します。

  2. $(git rev-parse --abbrev-ref HEAD)@{...}
    現在のブランチ名を取得するために、git rev-parseコマンドのあとに、--abbrev-refオプションを入力します。その後に、reflog内1つ前の位置への参照(例えばmaster@{1}など)を作るため、@{...}を連結させます。

  3. ${1-1}
    最初のパラメータ$1としてreflogの中で位置を指定し、デフォルト値に1を指定します。エイリアスをシェル関数として定義する理由はここにあります。つまり、このパラメータのデフォルト値を標準的なBash構文 を使って指定できるようにするためです。

このようなオプションのパラメータを使うことの利点は、任意の回数の操作を取り消すことができるということです。何も指定しない場合は、直前の操作を取り消すことができます。

試してみましょう

以下のような履歴があったとします。3

History before the rewrite
この履歴にはコミットCで分岐するmasterfeatureの2つのブランチがあります。この例では、masterブランチにある最新のコミット、つまりコミットFを取り除きfeatureブランチとマージしたいものとします。

git reset --hard HEAD^
git merge feature

この時点で、以下のような履歴が出来上がります。

History after the rewrite
ご覧のように、うまくいきました。ですが、まだ満足いく結果ではありません。ある理由で前の履歴に戻りたいとします。つまり実際には、直前の2つの操作、mergeresetを取り消すということです。ここでundoエイリアスの出番です。

git undo 2

これによってmaster@{2}で参照されたコミットがHEADに移動します。このコミットは、masterブランチが2つ前のreflogエントリで差していたコミットです。それではさらに履歴をチェックしてみましょう。

History restored with the undo alias

全てが元通りになりました。\o/

ではundoををundoしたい場合は? 簡単ですよ。git undoそのものは、undo操作を1回行うということなので、次のように入力すれば十分です。

git undo

つまり、引数なしで、git undo 1と入力したのと同じ意味になるのです。

どうです、この記事は役に立ちましたか? もしこの記事で紹介したのと同じような、別のテクニックも学びたいようであれば、他にもPluralsightAdvanced Git Tips and Tricks(さらに進んだGitのコツとテクニック)コースでいくつか記事を書き溜めているので読んでみてください。


  1. つまり、前にコミットしたのとは異なるコミットを指すように変更されます。 

  2. ここでは日付を使用することもできます。例えばmaster@{yesterday}またはHEAD@{2.days.ago}を試してみてください。素晴らしいと思いませんか? 

  3. 履歴は簡潔で色鮮やかなものが好きです。このため、私はただのgit logを使いません。その代わりにlgというエイリアスを定義しており、出力をカスタマイズするのため--prettyオプションを使っています。さらに詳しく知りたい場合は、このことについて以前見た目のいい履歴についての重要性について書いた記事があるので読んでみてください。