はじめに
git pull を実行したときに表示される Fast-forward というメッセージ。なんとなく見過ごしている人も多いのではないでしょうか。
この記事では、Fast-forward が何を意味するのかを「ブランチの枝分かれ」という観点から整理し、さらに「ローカルとリモートで同名ブランチを作った場合の push」など、実際の作業でつまずきやすいケースまで解説します。
Fast-forward とは
Fast-forward(早送り)が起きるのは、自分のローカルブランチがリモートの最新コミットの「一直線の手前」にいる場合です。
言い換えると、自分の HEAD がリモートの最新コミットの「祖先」になっていて、ローカル側にリモートが持っていない独自コミットが1つも無い状態です。
このとき git は、枝分かれをマージする必要がなく、ただブランチのポインタを前に進めるだけで済みます。これが「Fast-forward(早送り)」と呼ばれる理由です。マージコミットは作られません。
pull 前: A---B---C (自分の HEAD)
\
D---E (リモート) ← 一直線に進んでいるだけ
pull 後: A---B---C---D---E (HEAD が E まで早送りされる)
ポイントは、Fast-forward は「コミットが進んだ」というより 「枝分かれせずまっすぐ進められた」という合図 だという点です。
枝分かれ(divergence)とは
では、Fast-forward にならないのはどんなときでしょうか。それは履歴が 枝分かれ(divergence) しているときです。
枝分かれが起きる条件は、共通の祖先コミットから、ローカルとリモートの両方がそれぞれ別のコミットを持っている ことです。
ここで重要なのは、「ローカルに独自コミットがある」だけでは枝分かれにはならないという点です。あくまで 両方が共通地点より先に進んでいるか で判定されます。
3つのパターンで整理すると分かりやすいです。
① 自分は進んでいない、リモートだけ進んだ
A---B---C ← ローカル(C のまま)
\
D---E ← リモート
→ Fast-forward(ポインタを前に進めるだけ)
② 自分だけ進んだ、リモートは進んでいない
A---B---C---F ← ローカル
\
(リモートは C のまま)
→ pull しても取り込むものが無い(Already up to date)。push すれば今度はリモート側が Fast-forward する。
③ 自分もリモートも、それぞれ別に進んだ ← これが「枝分かれ」
共通の祖先
↓
C
├─→ D---E (リモート)
└─→ F---G (ローカル)
→ Fast-forward できず、マージコミット(コンフリクトが出たら解消)で対応する。
応用1: main を先に修正してしまったときの切り出し
「main から dev ブランチを切ってから作業するつもりが、先に main 上で修正してしまった」というケースはよくあります。その修正を dev に移したいとき、まだコミットしているかどうか で対応が変わります。
コミット前の場合(一番ラク)
未コミットの変更(作業ツリーの状態)は、まだどのブランチにも属していない「宙に浮いた」状態です。そのため、新しいブランチを作って切り替えると、変更はそのまま一緒に移動します。
git switch -c dev
これだけで dev ブランチが作られ、修正内容も一緒に移ります。main は修正前のキレイな状態のまま残ります。あとは dev 上でコミットすれば完了です。
git add .
git commit -m "修正内容"
git switch -c devは「dev を新規作成して切り替える」コマンドです(-cは create)。古い書き方ではgit checkout -b devで同じことができます。
すでに main にコミット済みの場合
この場合は「dev に修正を移す」→「main を元に戻す」の2ステップです。
# 今いる main から dev を切り出す(dev は修正コミット込みの状態になる)
git switch -c dev
# main に戻して、コミットを1つ巻き戻す
git switch main
git reset --hard HEAD~1
操作前: A---B---C (main, HEAD) ← C が間違って入れた修正
git switch -c dev:
A---B---C (main, dev) ← dev が C を持った状態で分岐
git switch main + reset --hard HEAD~1:
A---B (main)
\
C (dev) ← 修正 C は dev だけに残る
注意:
git reset --hardは変更を破棄する操作です。main をすでに push している場合は履歴の書き換えになり共有相手とズレるため、git revertを検討してください。また--hardは他の未コミットの変更も巻き込んで消すので、実行前に確認しましょう。
応用2: ローカルとリモートで同名ブランチを作ったときの push
リモートとローカルでそれぞれ独立に同名(例: dev)のブランチを作った場合、push はどうなるでしょうか。
基本コマンドは次のとおりです。
git push -u origin dev
-u(--set-upstream)は「ローカル dev とリモート origin/dev を紐付ける(追跡関係を作る)」指定です。一度やっておくと、次回からは git push / git pull だけでこのブランチが対象になります。
ただし、通るかどうかは中身が枝分かれしているかで変わります。
リモート dev に独自の変更がない場合
リモート dev がブランチ元から1コミットも進んでいない(中身が無い)なら、枝分かれは起きていません。
共通の元の地点
↓
A (リモート dev ← ここから動いていない)
└─→ B---C (ローカル dev ← こっちだけ進んでいる)
リモートはローカルの「祖先」であり、Fast-forward が可能なパターンです。この場合は次の一発で通ります。
git push -u origin dev
リモート dev にも独自コミットがある場合
リモートとローカルがそれぞれ別コミットを持っていると、枝分かれ状態になり push は弾かれます。
リモート dev: A---X (リモートで作ったコミット)
ローカル dev: A---Y (ローカルで作ったコミット)
! [rejected] dev -> dev (fetch first)
リモートのコミットを上書きしてしまうため、git が止めてくれます。この場合は 先にリモートを取り込んでから push します。
git pull origin dev # 枝分かれしているのでマージ(コンフリクトが出たら解消)
git push -u origin dev
実務上は、まず git push -u origin dev を試し、rejected が出たら git pull する、という順番で問題ありません。
まとめ
- Fast-forward は「枝分かれせずまっすぐ進められた」という合図。マージコミットは作られない。
- 枝分かれ は「共通の祖先から、ローカルとリモートの両方がそれぞれ別コミットを持つ」状態のときだけ起きる。
- 枝分かれしていれば push は弾かれるので、先に
git pullで取り込んでから push する。 - 未コミットの変更は
git switch -cでそのまま新しいブランチへ移動できる。
「両方が共通地点より先に進んでいるか?」というイメージを持っておくと、Fast-forward できるか・push が通るかが自然に判断できるようになります。