POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Enrico Campidoglio

本記事は、原著者の許諾のもとに翻訳・掲載しております。

(注:2017/06/22、いただいたフィードバックを元に翻訳を修正いたしました。)

このような経験はありませんか?「ローカルのコミットをし過ぎてしまったことに急に気づいて ローカルコミットを書き直している最中 、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 –-hard 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 オプションを使っています。さらに詳しく知りたい場合は、このことについて以前 見た目のいい履歴についての重要性 について書いた記事があるので読んでみてください。