【Git】non-fast-forwardエラーの原因と解決方法|6パターン診断と–force-with-lease活用完全ガイド

【Git】pushしようとしたら”non-fast-forward”で拒否されたときの解決方法 Git

git pushを実行すると次のようなエラーでpushが拒否されることがあります。

典型的なnon-fast-forwardエラー
$ git push origin main
To github.com:user/repo.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:user/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.

このエラーは「早送り(fast-forward)できない」という意味で、リモートが自分より先行している/互いに分岐している状態で発生します。対処は基本的に最新を取り込んでからpushし直すだけですが、「リモート先行」「互いに分岐」「履歴書き換え後」「保護ブランチ」など原因によって選ぶべき手段が変わります。

この記事では、non-fast-forwardの6つの発生パターンと判定フロー、--force-with-leaseの安全な使い方、そして同じ問題を起こさないための運用設定まで実務で迷わない形で解説します。

この記事で学べること

  • fast-forward/non-fast-forwardの仕組みと判定基準
  • 6つの発生パターンと個別対処
  • pull --rebasepull --ff-onlymergeの使い分け
  • --force--force-with-leaseの安全性の違い
  • amend/rebase/resetで自分が履歴を書き換えた場合の対処
  • 保護ブランチ/tagのnon-ff
  • pull.rebasefetch.prune等の予防設定
スポンサーリンク

fast-forward/non-fast-forwardの仕組み

Gitのbranch先端を「ポインタ」と呼び、pushはリモートのポインタを進める操作です。リモートの先端がローカルの先祖に含まれている場合はポインタを進めるだけで済むのでfast-forward。そうでない場合は新しい履歴をマージ/リベースしないと整合できないので、安全装置としてpushが拒否されます。

FFとnon-FFの違い
# fast-forward OK: リモート先端が自分の先祖
#   A - B - C          ← origin/main
#           \
#            D - E     ← HEAD (localmain)
# → origin/main を E まで進めるだけ

# non-fast-forward: リモートに自分の知らないコミットあり
#   A - B - C - X - Y  ← origin/main (他メンバーがpush)
#           \
#            D - E     ← HEAD (local main)
# → 進めるだけでは同期できない(X,Yが捨てられる)
# → Gitが安全のため拒否

ポイント:non-fast-forwardは「拒否された=エラー」ではなく、「データ喪失を防ぐための安全装置」です。他メンバーのコミットを意図せず消さないよう、Gitが仕組み上止めてくれています。対処するときも「リモートのコミットをどう扱うか」を意識して選びましょう。

6つの発生パターン早見表

パターン 典型シチュエーション 推奨対処
① リモート先行 同僚がpushした/pull忘れ pull --rebase→push
② 互いに分岐 両側にcommitがある rebasemerge
③ 自分がamend/rebase/reset 個人ブランチで履歴を書き換えた push --force-with-lease
④ 保護ブランチ拒否 main/developのforce禁止設定 PR経由で統合
⑤ 追跡先ミス/upstream未設定 push先を取り違えている upstream設定/push origin HEAD
⑥ タグの重複push 同名タグが既に存在 タグ名変更/push --force(慎重)

ポイント:8〜9割は①+②(同期漏れ)です。git pull --rebaseが効く典型パターンなので、まずこれを試してみましょう。「自分が履歴を書き換えた(amend/rebase)」が原因なら③で--force-with-lease、mainへ直接pushしようとしているなら④でPRへ切り替えが正解です。

STEP 0:原因を診断する

状況確認コマンド
# 最新情報を取得
git fetch --prune

# 先祖関係と件数を一目で把握
git rev-list --left-right --count origin/main...HEAD
# 出力例: "3    2"  ← リモート3件先行、ローカル2件固有

# 分岐状況の可視化
git log --oneline --graph --decorate --all -20

# 自分のブランチ固有コミットを確認
git log --oneline origin/main..HEAD

# リモート固有コミットを確認
git log --oneline HEAD..origin/main

診断結果の読み方

  • 右:0, 左:N → リモート先行(ケース①)
  • 右:M, 左:N(両方非0)→ 互いに分岐(ケース②)
  • 自分のcommitが消えた・SHAが変わった → 履歴書き換え(ケース③)
  • サーバーからprotected branchメッセージ → 保護ブランチ(ケース④)
  • no upstream branch警告 → 追跡先ミス(ケース⑤)
  • タグ名でrejected → タグ重複(ケース⑥)

ケース①:リモートだけ先行している

ローカルに新規コミットが無い、または未コミット変更だけでリモートだけが進んでいる状態。pullで取り込めば即解決です。

単純なpullで解決
# fastforwardでpull
git pull --ff-only origin main
# → FFできなければエラー(想定通り)

# 未コミット変更があれば先にstash
git stash push -u -m "work in progress"
git pull --ff-only
git stash pop

# 取り込めたらpush
git push origin main

ポイント:--ff-onlyを付けるとFFできない場合はpullが失敗するので、知らぬ間にマージコミットが増えるのを防げます。安全側の挙動なのでgit config --global pull.ff onlyでデフォルト化もおすすめ。

ケース②:ローカルとリモートが両方進んでいる

自分がcommit中に同僚がpushした、の典型パターン。rebaseかmergeで統合してからpushします。チーム方針によりますが、個人ブランチはrebase、共有ブランチはmergeが一般的。

rebaseで直線化して取り込む
# fetchして最新化
git fetch --prune

# リベース
git rebase origin/main

# conflictが出たら解消して続行
git add .
git rebase --continue
# 中止したいなら
git rebase --abort

# push(rebaseでSHAが変わったので個人ブランチ限定)
git push origin HEAD
mergeで統合する(履歴にマージコミット残す)
git fetch --prune
git merge origin/main

# conflict解消
git add .
git commit

# push
git push

rebase/merge選択の詳細はrebaseとmergeの違いと使い分けも参照してください。

pull –rebaseで一発

pull –rebaseで簡潔に
# fetch+rebaseを一発で実行
git pull --rebase origin main

# デフォルトrebaseにしておくと楽
git config --global pull.rebase true
# 以降 git pull だけで rebase 動作

# さらにautoStashで未コミットも自動退避
git config --global rebase.autoStash true

ケース③:自分でamend/rebase/resetして履歴を書き換えた

コミットメッセージをamendした/rebaseでsquashした/resetで巻き戻したなど、自分のローカル履歴のSHAを変えた直後にpushしようとするとnon-ffになります。リモートの元のコミットを上書きする必要があるので、強制pushが必要です。

安全な強制push
# 最新リモート情報を取得(stale防止)
git fetch origin

# 安全な強制push(推奨)
git push --force-with-lease origin <branch>

# より厳密:リモートが期待SHAと一致するときだけpush
git push --force-with-lease=main:<期待SHA> origin main

# 危険:無条件上書き
git push --force origin <branch>

# エイリアス登録(force-with-leaseをデフォルトに)
git config --global alias.fp "push --force-with-lease"
コマンド 挙動 安全性
push --force 無条件でリモートを上書き 危険
push --force-with-lease ローカルのorigin/xxx認識と一致した時だけ上書き 安全(推奨)

警告:git push --forceは他メンバーがpushした新しいコミットを気付かずに上書き消去します。実運用では--force-with-leaseを必ず使ってください。自動エイリアス登録でデフォルトを安全側に寄せるのが推奨。

どんな時に自分で履歴を書き換えているか

履歴書き換えの代表操作

  • git commit --amend(直前のコミット修正)
  • git rebase(特にインタラクティブ-iでsquash/reword)
  • git resetでpush済みコミットを巻き戻し
  • git cherry-pickで他branchの内容を取り込んだ後、元を戻そうとする
  • git filter-repofilter-branchで履歴一括書き換え

メッセージ修正の詳細はコミットメッセージの変更方法、push取り消し全般はpushを取り消す方法も参照。

ケース④:保護ブランチで拒否

GitHub/GitLab等のBranch protection rulesでmain/developがprotectedだと、force pushや直接pushが許可されていません。エラーメッセージにprotected branchが含まれていればこれです。

保護ブランチのエラー例
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Required status check "ci/build" is expected.
 ! [remote rejected] main -&gt; main (protected branch hook declined)

対処方法

  • 作業ブランチを切ってPRを作成(推奨)
  • CIの必須チェックをパスさせる
  • レビュー数を満たす
  • 管理者権限で一時解除するのは最終手段
  • force push禁止設定は残したままPRで統合するのが長期的に安定
mainからブランチを切って救済
# mainに直接pushしたかった変更を救出
git switch -c feature/from-main
git push -u origin feature/from-main

# 元のmainはリモートに合わせる
git switch main
git reset --hard origin/main

# feature側でPR→レビュー→マージ

ケース⑤:追跡先ミス/upstream未設定

ブランチ名をリネームした、別リポで作業した、などupstream(追跡先)の設定が合っていないとnon-ffに見えることがあります。

upstream確認と再設定
# 現在の追跡先を確認
git branch -vv
# または
git rev-parse --abbrev-ref --symbolic-full-name @{u}

# 追跡先を明示指定
git branch --set-upstream-to=origin/main

# push先を都度指定したい場合
git push origin HEAD:main

# 新ブランチ初回push時にupstream設定
git push -u origin feature/xxx

リネーム後のpushエラーはブランチ名を変更したらpushできなくなったときの対処法で詳細を解説しています。

ケース⑥:タグの重複push

同名タグがすでにリモートに存在する場合、push時にrejectedが出ます。

タグのnon-ff
 ! [rejected]        v1.0.0 -&gt; v1.0.0 (already exists)
hint: Updates were rejected because the tag already exists in the remote.
対処:タグ戦略
# 新しいバージョンを採る(推奨)
git tag v1.0.1
git push origin v1.0.1

# どうしても上書きしたい場合(慎重)
git push --force origin v1.0.0

注意:タグはリリースポイントの固定参照。一度pushしたタグを上書きすると、既に動いているCI/デプロイ/依存リポとの整合性が崩れます。やむを得ない場合以外は、新しいバージョン番号を採るのが安全です。

予防:設定と運用で再発を減らす

運用設定のベストプラクティス

  • pull.rebase = true:pullでrebaseを優先、余計なマージコミット防止
  • pull.ff = only:pull時にFFできない場合はエラーで止める
  • rebase.autoStash = true:rebase時に未コミットを自動退避
  • fetch.prune = true:削除済みリモート追跡の自動整理
  • push.autoSetupRemote = true(Git 2.37+):初回pushで自動upstream
  • alias.fppush --force-with-lease:強制pushを安全側へ
  • PRベースのマージ運用:保護ブランチで直接pushを禁止
推奨Git設定の一括適用
# pull系
git config --global pull.rebase true
git config --global pull.ff only
git config --global rebase.autoStash true

# fetch系
git config --global fetch.prune true

# push系(Git 2.37+)
git config --global push.autoSetupRemote true

# 便利エイリアス
git config --global alias.fp "push --force-with-lease"

実践シナリオ

シナリオ① 朝一push、同僚の変更と競合

基本パターン
git fetch origin
git pull --rebase origin main
# conflict が出たら解消→continue
git push origin main

シナリオ② amend後にpushがrejected

force-with-leaseで安全に
# 直前のcommitメッセージをamendした直後
git commit --amend -m "fix: 正しいメッセージ"
git push origin <branch>
# → rejected (non-fast-forward)

# 個人ブランチなので安全な強制push
git fetch origin
git push --force-with-lease origin <branch>

シナリオ③ mainへ直接pushしてprotected rejected

PR経由で正規化
# mainで作業してしまった変更を退避
git switch -c feature/accidental-main-work
git push -u origin feature/accidental-main-work

# mainをリモートに合わせる
git switch main
git reset --hard origin/main

# PRを作ってレビュー→マージ

シナリオ④ rebase後、さらに他メンバーがpush

force-with-leaseで衝突検知
git rebase origin/main
git push --force-with-lease
# → rejected(リモートが自分のorigin/mainと違う)

# 他メンバーがさらにpushしていた → fetchし直して再rebase
git fetch origin
git rebase origin/main
git push --force-with-lease

やってはいけない落とし穴

-f/–forceを無条件で打つ

git push --forceは他メンバーの新しいcommitを気付かずに上書きします。常に--force-with-leaseを使う習慣を付け、エイリアスでgit fppush --force-with-leaseにしておけば事故を防げます。共有ブランチでは保護ブランチルールで物理的に禁止するのが理想です。

force push後にチーム通知を怠る

個人ブランチでforce-with-leaseしていても、他メンバーがそのブランチをベースに作業していた場合、pullでエラーが起きます。履歴を書き換えたらSlackや口頭で「xxxブランチを強制pushしました」と周知し、必要なら同期コマンド(git fetch && git reset --hard origin/xxx)を案内しましょう。

rebase中にさらに別作業を始める

rebaseでconflictが出ている最中に別ブランチへ切替→別作業、と進めるとrebaseの状態が放置されます。git statusのhintを読んで--continue--abortで完了させてから次の操作に進みましょう。

mainへ毎回force pushしてprotected解除を繰り返す

「面倒だから」と保護ブランチルールを頻繁に解除すると、CI未通過コードや未レビュー変更がmainに入ってしまい、本番障害の温床になります。PR経由でのマージを基本とし、緊急時だけ管理者権限で一時的に解除するルールが健全です。

pullでマージコミットが増え続ける

git pullのデフォルト挙動はmergeなので、pullする度にマージコミットが積まれる場合があります。履歴を直線的に保ちたいならpull.rebase = trueを設定しましょう。チーム方針として統一するのがおすすめです。

よくある質問

Qpull –rebaseとpull –ff-onlyどっちが良い?
A既に個人ブランチで自分のcommitがあるなら--rebase、ローカルに変更がない(FFだけで取り込みたい)場合は--ff-onlyが安全。常に--ff-onlyをデフォルトにし、必要な時だけ--rebaseを明示するのも良い運用。
Qforce-with-leaseでもstaleエラーが出る
Aリモートが自分のローカルのorigin/xxxと一致していない状態です。git fetch originで情報を最新化してから再実行してください。それでも失敗するなら、他メンバーが間にpushしている可能性あり。
Q自分の変更を消さずに安全にmergeで取り込みたい
Agit fetchgit merge origin/mainでマージコミットが作られて統合されます。conflictが出たら解消→commit→push。履歴を直線的にしたい場合はrebaseを選びます。
Qrebase中のcommitが大量でconflictだらけ
Agit rebase --abortで中止して、mergeで取り込むのも選択肢です。rebaseは直線的な履歴を得るための手段なので、commit数が多くconflictだらけなら無理にrebaseせずmergeに切り替えた方が早いことも。
Qpull –rebaseで意図せずマージコミットがsquashされる
Apull.rebase=merges(またはinteractive)にするとマージコミットを保持したままrebaseできます。チームが頻繁にmergeする運用なら有効な設定です。
Qprotected branchの拒否を一時解除したい
AGitHubではSettings → Branches → 保護ルール編集で直接push許可できます。ただし恒常的な緩和は避けるべきで、緊急時のみ解除→直後に再有効化が推奨です。
Qnon-fast-forwardとfailed to push some refsの違いは?
A前者は具体的な原因(fast-forward不可)、後者は外側のサマリー。外側メッセージの上の行にnon-fast-forwardを含む具体原因が書かれています。詳細はfailed to push some refs toエラーの原因と対処法を参照。

関連記事

まとめ

  • non-fast-forwardはデータ喪失を防ぐ安全装置、エラーではない
  • 6パターン:①リモート先行/②互いに分岐/③履歴書き換え/④保護ブランチ/⑤upstream/⑥タグ
  • 最多は①+②——git pull --rebase→push で解決
  • 履歴書き換え後はpush --force-with-lease--forceは避ける
  • 保護ブランチはPR経由で取り込む(force解除は最終手段)
  • 予防:pull.rebasepull.ff=onlyrebase.autoStashfetch.prunepush.autoSetupRemoteを設定
  • force pushしたら必ずチーム通知+同期手順を案内

non-fast-forwardは一見怖いですが、パターンを見分けて適切な対処を選べば大半は数分で解決します。最頻出パターンは「pullを忘れていた」だけなのでgit pull --rebaseで終わることが多いはず。履歴書き換え後は--force-with-leaseを習慣化し、保護ブランチへの直接pushはPR経由に切り替えれば、チーム開発で事故を大幅に減らせます。