Git: ^ と ~ を使い分けて、ブランチの Reset ミスを完璧に防ぐ

Git tutorial - IT technology blog
Git tutorial - IT technology blog

HEAD~1 と HEAD^1 を打ち間違えるとどうなる?

エンジニアになりたての頃、マージミスを修正しようと git reset を使った際、冷や汗をかいた経験があります。当時は ~ (Tilde) と ^ (Caret) は同じもので、どちらを使ってもいいと思い込んでいました。結果はどうなったかというと、master ブランチの直前のコミットに戻るはずが、大昔のフィーチャーブランチに飛んでしまったのです。幸い git reflog に救われましたが、そうでなければ午前中の作業がすべて水の泡になるところでした。

私の 8 人のチームでは、シニアエンジニアでさえ、複雑なマージコミットに遭遇すると一瞬手が止まることがあります。これら 2 つの記号を正しく理解することは、git rebase -i を使いこなす鍵となります。コミット履歴を乱す恐怖から解放され、自信を持って過去のコードを辿れるようになります。

30秒で覚えるクイックヒント

お急ぎの方は、このシンプルなルールだけ覚えておいてください:

  • ~ (Tilde): 過去へ一直線に戻ります。HEAD~3 は正確に 3 ステップ前に戻ることを意味します。
  • ^ (Caret): マージ時のどの親コミットに進むかを選択します。この記号は、1 つのコミットに 2 つ以上の親がある場合にのみ真価を発揮します。
# 1つ前のコミットに戻る(どちらも同じ結果)
git reset --hard HEAD~1
git reset --hard HEAD^

# 垂直方向に3つ前のコミットに戻る
git reset --hard HEAD~3

# マージコミットの2番目の親を表示する
git show HEAD^2

では、Git に二度と騙されないよう、その本質を詳しく見ていきましょう。

1. Tilde (~):垂直方向に遡る

Git の履歴を 1 本のロープだと考えてみてください。~ は、そのロープを何歩分後ろに下がるかを表します。

HEAD~n と書くと、Git は次のように解釈します。「ここから n ステップ戻れ。もし分岐(マージ)があったら、常に最初の親(first parent)を選べ」。

  • HEAD~1: 直前の親。
  • HEAD~2: 親の親(祖父)。
  • HEAD~3: 親の親の親(曽祖父)。

HEAD~~~ と書いても HEAD~3 と同じ結果になります。しかし、入力の手間を省き、記号の数え間違いを防ぐためにも数字を使うことをお勧めします。

2. Caret (^):分岐点での方向選択

本当の違いはマージコミットで現れます。通常のコミットには親が 1 つしかありませんが、Feature ブランチを Main に統合すると、そのコミットには 2 つの親が存在することになります。

  • 第 1 の親 (Parent 1): マージ前に自分がいたブランチ(例:main)。
  • 第 2 の親 (Parent 2): 取り込んだ側のブランチ(マージしたブランチ)の最後のコミット。

このとき、HEAD^1 はメインの系統に戻ります。一方で HEAD^2 は、マージされた側のサブブランチへと導きます。HEAD^3 を指定すると 3 番目の親を探しますが、これは 4〜5 個のブランチを一度にまとめる Octopus Merge を使った時くらいしかお目にかかれません。

注意点: マージではない通常のコミットでは、HEAD^1HEAD~1 は同じ場所を指します。これが、多くの人がこの 2 つを「双子のようなもの」だと誤解する原因です。

3. コミット図による可視化

理解を深めるために、以下の図を見てみましょう:

G (HEAD)
| 
F (マージコミット: E を D に統合)
/ \
D   E
|   |
C   |
|  /
B /
|
A

G (HEAD) の位置にいる場合、結果は次のようになります:

  • HEAD~1F を指す。
  • HEAD~2D を指す(Git は G -> F -> D とメイン系統を辿る)。
  • HEAD^1F を指す。
  • F^1D を指す(マージ前のメイン系統)。
  • F^2E を指す(マージされたフィーチャーブランチ)。

では、G から E に行きたい場合は? HEAD~1^2 を使います。つまり「1 ステップ戻って F に行き、そこから 2 番目の親へ進む」という意味です。

4. Rebase 時の組み合わせテクニック

git rebase -i でコミットを整理する際、~^ の組み合わせは非常に強力です。長い SHA-1 ハッシュをコピーする手間が省け、正確な「座標」を指定するだけで済みます。

例:HEAD~3^2

  1. HEAD~3:一直線に 3 ステップ戻る。
  2. ^2:その地点で 2 番目の親(サブブランチ)へ進む。

私の経験から言えるアドバイス:reset --hard を実行する前に、show コマンドで移動先を確認しましょう。

git show HEAD~2^1 --oneline -s

表示されたコミット名が意図通りであれば、そこで初めて Enter キーを押して本番のコマンドを実行します。

実践的なアドバイス:グラフを信頼する

理論は理解できても、10,000 件ものコミットがあるプロジェクトで ~ を目視で数えるのは無謀です。私のチームでは、コマンドを打つ前に必ずグラフを表示するように徹底しています:

git log --oneline --graph --all

グラフを見れば、接続線がはっきりと見えます。HEAD^2HEAD~5 の特定も、格段に直感的になります。

よくあるもう一つの状況は git cherry-pick です。マージコミットからコードを取り込もうとすると、Git はどちらの親を選べばいいか分からず停止します。その場合、-m フラグと親の番号を指定する必要があります(例:git cherry-pick -m 1 <commit_hash>)。これこそが、親(Parent)に関する知識が役立つ瞬間です。

まとめ

Git をプロのように使いこなすために、以下を覚えておきましょう:

  • 同じブランチ内で時間を遡るには Tilde (~) を使う.
  • マージ地点で別のブランチへ切り替えるには Caret (^) を使う。
  • ジャンプする前に必ず git log --graph で道筋を確認する.

これら 2 つの記号をマスターすれば、コードの履歴が迷宮に見えることはもうありません。スムーズなコミット管理を実現しましょう!

Share: