2016年9月29日
Git Undo エイリアスを定義する
(2016-08-25)by 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内のエントリが出力されます。
HEAD
自体のreflogを参照したい場合は、引数を省けばいいだけです。
git reflog
これで HEAD
参照のみに対する同出力が得られます。
ぱっと見ただけでは分かりませんが、reflog内のエントリは 新しいものが順に 上から記録されています。
逆にすぐに分かることは、各エントリにはそれぞれ インデックス が付いていることです。このインデックスを使うことで、特定のreflogのエントリに関連するコミットを直接参照することができるため、実はこれは 非常に 有用です。ここでは、reflogのエントリを参照するには以下の構文を用いる、ということだけ言っておきましょう。
reference@{index}
@
マークの両側には以下を入力します。
reference
には、ブランチ名もしくはHEAD
名。index
には、reflog内のエントリの位置 ^(2) 。
例えば、 HEAD
が 2つ前 に参照していたコミットを確認したいとしましょう。その場合は、 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つずつ区切って見ていきましょう。
-
!f() { ... } f
ここでは、f
という名の シェル関数 をエイリアスとして定義し、それを即座に呼び出します。 -
$(git rev-parse --abbrev-ref HEAD)@{...}
現在のブランチ名を取得するために、git rev-parse
コマンドのあとに、--abbrev-ref
オプションを入力します。その後に、reflog内1つ前の位置への参照(例えばmaster@{1}
など)を作るため、@{...}
を連結させます。 -
${1-1}
最初のパラメータ$1
としてreflogの中で位置を指定し、デフォルト値に1
を指定します。エイリアスをシェル関数として定義する理由はここにあります。つまり、このパラメータのデフォルト値を標準的な Bash構文 を使って指定できるようにするためです。
このようなオプションのパラメータを使うことの利点は、任意の回数の操作を取り消すことができるということです。何も指定しない場合は、直前の操作を取り消すことができます。
試してみましょう
以下のような履歴があったとします。 ^(3)
この履歴にはコミット C
で分岐する master
と feature
の2つのブランチがあります。この例では、 master
ブランチにある最新のコミット、つまりコミット F
を取り除き feature
ブランチとマージしたいものとします。
git reset --hard HEAD^
git merge feature
この時点で、以下のような履歴が出来上がります。
ご覧のように、うまくいきました。ですが、まだ満足いく結果ではありません。ある理由で前の履歴に戻りたいとします。つまり実際には、直前の2つの操作、 merge と reset を取り消すということです。ここで undo
エイリアスの出番です。
git undo 2
これによって master@{2}
で参照されたコミットが HEAD
に移動します。このコミットは、 master
ブランチが2つ前のreflogエントリで差していたコミットです。それではさらに履歴をチェックしてみましょう。
全てが元通りになりました。\o/
では undoを をundoしたい場合は? 簡単ですよ。 git undo
そのものは、undo操作を1回行うということなので、次のように入力すれば十分です。
git undo
つまり、引数なしで、 git undo 1
と入力したのと同じ意味になるのです。
どうです、この記事は役に立ちましたか? もしこの記事で紹介したのと同じような、別のテクニックも学びたいようであれば、他にも Pluralsight の Advanced Git Tips and Tricks(さらに進んだGitのコツとテクニック) コースでいくつか記事を書き溜めているので読んでみてください。
-
つまり、前にコミットしたのとは異なるコミットを指すように 変更 されます。 ↩
-
ここでは 日付 を使用することもできます。例えば
master@{yesterday}
またはHEAD@{2.days.ago}
を試してみてください。素晴らしいと思いませんか? ↩ -
履歴は簡潔で色鮮やかなものが好きです。このため、私はただの
git log
を使いません。その代わりにlg
というエイリアスを定義しており、 出力をカスタマイズする のため--pretty
オプションを使っています。さらに詳しく知りたい場合は、このことについて以前 見た目のいい履歴についての重要性 について書いた記事があるので読んでみてください。 ↩
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa