🔍
detached HEADを意図的に再現して検証:non-fast-forward
毎日gitは使うのですが、detached HEAD など普段あまり使わない操作をした際の備忘録メモとして、そしてSourceTree上とTerminal上の表時に違いがあるのか?など一度じっくり比較/観察したかったので検証しました。
環境
pc:MacBook Pro(2019)
os:macos Sequoia
Git:2.49.0
SourceTree:内蔵 Git 2.46.0
検証目的
- HEADの仕組みと挙動を理解するために、意図的に detached HEAD状態 を作成
- TerminalとSourceTreeでの表示の違いを観察
- 実務で使う機会は少ないが、Git内部の動き理解のために有益
- 「意図的にdetached HEAD」「git push origin HEAD:main」は実務では稀だと思います(今回はdetached HEAD を検証するため)
検証内容
- 通常の main でコミット → push
- HEAD~0 に checkout(detached HEAD) → その状態でファイル更新 → commit
- git push origin HEAD:main により、mainブランチをdetached HEAD状態で上書き
- main に戻って再度 commit(ローカルとリモートが分岐)
- git push による non-fast-forward エラーを確認
- git pull --no-rebase → コンフリクト解消 → mergeコミットで履歴統合
🔗 関連記事:「detached HEADを意図的に再現して検証:fast-forward merge」
detached HEAD とは
HEAD がブランチではなくコミット(もしくはタグ)を直接指している状態
--no-rebase
今回は「履歴を直線化せず、mergeコミットとして分岐を可視化したまま統合」する方法として –no‑rebase を採用しました
detached HEAD 〜 分岐 〜 mergeで統合
(main)ファイル作成 + add + commit
$ echo "local" > file.txt$ git add .$ git commit -m "first commit"
- リモートにはまだpushしていないため HEAD → main が先に進んでいる状態
HEAD→main
origin/HEAD, origin/main


(main)push
$ git push
- リモートpush済。HEAD → main、 origin/HEAD, origin/mainが同じコミットIDを参照している状態(同じ位置)
HEAD → main、origin/HEAD, origin/main


(HEAD)detached HEAD + HEADでファイル更新 + add + commit
$ git checkout HEAD~0 # 現在の HEAD が指しているコミットに そのままチェックアウト$ echo "local(HEAD) update" >> file.txt$ git add .$ git commit -m "HEAD(detached HEAD) to file update"
- HEADとmainが分離した事が確認できる
- (Terminal上での表示)HEAD → main ではなくHEAD と main に別れている
- (SourceTree)レフトメニュー内の「ブランチ」でmainとHEADが表示され、HEADにチェックアウトしている
- HEAD、main、origin/HEAD, origin/mainが同じコミットIDを参照している状態(位置は変わらず同じ位置)
HEAD、main、origin/HEAD, origin/main


(HEAD)origin/mainにpush
# 今いるHEADの内容で、リモートmainブランチを上書きする
$ git push origin HEAD:main
- HEAD, origin/main, origin/HEADが先に進み、mainだけが遅れている状態
- (Terminal上での表示)HEADをorigin/mainにpushしたのでorigin/HEADもorigin/mainに追従して同じ位置
- (SourceTree)mainで[1コミット遅れ]になっている(mainだけが遅れている状態)
HEAD、origin/HEAD, origin/main
main


(main)mainにチェックアウト
$ git checkout main
- origin/main, origin/HEADが先行、HEAD → mainが遅れている状態
- main にcheckoutしたので再び HEAD → main となる
- HEAD → main だが main はリモート先行分を取り込んでいないため1コミット遅れ
origin/HEAD, origin/main
HEAD → main


(main)ファイル更新 + commit + push
$ echo "local main update" > file.txt$ git add .-
$ git commit -m "main file update"
・push時に![rejected] main -> main (non-fast-forward)エラー(原因(non-fast-forward))
(この時点で分岐が発生する)
A---B---C ← origin/main
\
\
D ← main (ローカルの新しいコミット)
A:07865d9(Initial commit)
B:593962f(first commit)
C:7eedc60(detached HEADで作成しpushされたコミット)
D:88954a6(mainブランチ上での新しいコミット)
ローカルとリモートが「B」から分岐し、それぞれの履歴(CとD)を持っている
この状態で push すると、Gitはどちらか一方の履歴を失う可能性があると判断し、拒否する(non-fast-forward)


(main)pull + --no-rebase + コンフリクト修正 + add + commit push
$ git pull --no-rebase # 今回は履歴が残る --no-rebaseを採用- コンフリクトを解消する
$ git add .$ git commit -m "Merge origin/main into local main"$ git push"
(--no-rebase実行前)
A---B---C ← origin/main
\
\
D ← main (ローカルの新しいコミット)
A:07865d9(Initial commit)
B:593962f(first commit)
C:7eedc60(detached HEADで作成しpushされたコミット)
D:88954a6(mainブランチ上での新しいコミット)
↓↓↓
(--no-rebase実行後)
A---B---C---------M ← main(HEAD), origin/HEAD, origin/main
\ /
\ /
\-----D
M:12e1549(mergeコミット。「Merge origin/main into local main」)
[--no-rebase実行前]


[--no-rebase実行後]


Discussion