🔍

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」
https://zenn.dev/tech_mw/articles/32b7f269f99eb6


detached HEAD とは

HEAD がブランチではなくコミット(もしくはタグ)を直接指している状態

--no-rebase

今回は「履歴を直線化せず、mergeコミットとして分岐を可視化したまま統合」する方法として –no‑rebase を採用しました

detached HEAD 〜 分岐 〜 mergeで統合

(main)ファイル作成 + add + commit

  1. $ echo "local" > file.txt
  2. $ git add .
  3. $ git commit -m "first commit"
  • リモートにはまだpushしていないため HEAD → main が先に進んでいる状態
HEAD→main
origin/HEAD, origin/main

terminal上での表示

SourceTree上での表示

(main)push

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

terminal上での表示
SourceTree上での表示

(HEAD)detached HEAD + HEADでファイル更新 + add + commit

  1. $ git checkout HEAD~0 # 現在の HEAD が指しているコミットに そのままチェックアウト
  2. $ echo "local(HEAD) update" >> file.txt
  3. $ git add .
  4. $ 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

terminal上での表示
SourceTree上での表示

(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

terminal上での表示
SourceTree上での表示

(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

terminal上での表示
SourceTree上での表示

(main)ファイル更新 + commit + push

  1. $ echo "local main update" > file.txt
  2. $ git add .
  3. $ 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)

terminal上での表示
SourceTree上での表示

(main)pull + --no-rebase + コンフリクト修正 + add + commit push

  1. $ git pull --no-rebase # 今回は履歴が残る --no-rebaseを採用
  2. コンフリクトを解消する
  3. $ git add .
  4. $ git commit -m "Merge origin/main into local main"
  5. $ 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実行前]
terminal上での表示
SourceTree上での表示

[--no-rebase実行後]
terminal上での表示
SourceTree上での表示

Discussion