Coding Gun

รู้จักกับ Detached Head และวิธีแก้ไข

ก่อนอื่นมาเริ่มกันที่ message ด้านล่างนี้ที่เราอาจได้พบเจอหลังจากที่เราทำการ checkout แล้วก็เกิดความสงสัยว่า เอ๊ะ! ที่ git บอกว่าเราอยู่ใน detached HEAD state นั้นหมายความว่ายังไง คราวนี้เรามาลองดูที่มาของ detached HEAD กันว่าเกิดจากอะไรและจะจัดการกับมันยังไง

You are in 'detached HEAD' state. You can look around, 
make experimental changes and commit them, 
and you can discard any commits you make in this state without
impacting any branches by switching back to a branch

Detached HEAD เป็น state ไม่ใช่ Error

สิ่งที่ต้องทำความเข้าใจเป็นอย่างแรกเลยคือ detached HEAD นั้นไม่ได้เป็น error แต่อย่างใด เพียงแค่เป็น state ที่เราอาจไม่ค่อยได้เจอบ่อยนัก ดังนั้น detached HEAD ไม่ได้เป็็นปัญหาแต่เราแค่ต้องเข้าใจมันเท่านั้นเอง การเปลี่ยน state จาก detached HEAD กลับมาเป็น attched HEAD นั้นง่ายมากถ้าคุณเข้าใจที่มาที่ไป

HEAD คืออะไร

HEAD นั้นเป็นเหมือน pointer ที่คอยชี้ว่าตอนนี้คุณกำลังทำงานอยู่บน commit ไหนใน branch ไหน นั่นคือเมื่อคุณ commit code เข้าไปใน repository มันจะความแตกต่างของ code ที่เราจะ commit เข้าไปกับ HEAD ดังนั้นตอนทำงานต้องรู้ว่าตอนนี้เรากำลังจะทำอะไรใน branch ไหนคุณต้องอยู่ใน branch ที่ถูกต้องก่อน ซึ่ง HEAD จะมี 2 state คือ

Git Detached HEAD เกิดขึ้นตอนไหน

เราจะลองสร้างให้เกิด detached HEAD โดย run คำสั่งต่อไปนี้่

mkdir git-head-demo
cd git-head-demo
git init
touch file.txt # สร้างไฟล์แรกขึ้นมา เป็นไฟล์ที่ยังไม่มีข้อความใดๆ
git add .      
git commit -m "Create file" # commit file.txt เข้ามาใน repository ครั้งแรก

# เพิ่มข้อความ Hello world! เข้าไปใน file.txt
echo "Hello World" > file.txt
git commit -a -m "Add line to the file" # commit file.txt ครั้งที่ 2 

# เพิ่ม file ที่ 2 เข้าไปใน repository
echo "Second file" > file2.txt
git add .
git commit -m "Create second file" # commit file2.txt ครั้งแรก

หลังจาก run git command ด้านบนเรียบร้อยให้ทำการ แสดง log message ออกมาด้วยคำสั่ง

git log --oneline

ค่า Hash ที่ได้จากการ commit ของเราจะไม่เหมือนกัน ให้อ้างอิงจาก commit message เป็นหลัก

Git log one line

ในบรรทัดแรกที่ 049f6e0 จะมีสัญลักษณ์ (HEAD -> master) แสดงว่าตอนนี้ HEAD จะอยู่ในสถานะ attached เรียกว่าเป็น attached HEAD state ซึ่งเป็น state ปกติ

Git attached head state
ปกติเราจะอยู่ใน Attached HEAD state เพราะเราจะทำงานต่อจาก commit ล่าสุดเสมอ

หลังจากนั้นให้เรา run คำสั่ง

git checkout 6008e58    # commit ก่อนสุดท้าย ที่มี message ว่า Add line to the file

หลังจากนี้คุณจะได้รับ message จาก git บอกว่าคุณได้เปลี่ยนไปอยู่ใน detached HEAD state เรียบร้อยแล้ว

You are in 'detached HEAD' state. You can look around, 
make experimental changes and commit them, 
and you can discard any commits you make in this state without
impacting any branches by switching back to a branch

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 6008e58 Add line to the file

Git detached head
หลังจาก checkout แล้ว git จะเปลี่ยนไปอยู่ใน Detached HEAD state

หลังจากเข้าสู่ detached HEAD แล้วลอง commit เพิ่มเข้าไปด้วยคำสั่งนี้

# สร้างไฟล์ใหม่ชื่ีอว่า new-file.txt
echo "Welcome to the alternate timeline" > new-file.txt
git add .
git commit -m "Create new file" # commit new-file.txt เข้าไปใน repository

# เพิ่มบรรทัดใหม่เข้าไปใน new-file.txt
echo "Another line" >> new-file.txt
# commit new-file.txt เป็นครั้งทีี่ 2
git commit -a -m "Add a new line to the file" 

หลังจาก run git command ด้านบนเรียบร้อยให้ทำการ แสดง log message ออกมาด้วยคำสั่ง

git log --oneline

Git log one line

เราจะเห็นว่าในบรรทัดแรก add847b นั้นเปลี่ยนจาก (HEAD -> master) ไปเป็น (HEAD) แสดงว่ามีการแก้ไขตอนที่เป็น detached HEAD ดังรูป

วิธีการแก้ไข Detached HEAD

จริงๆแล้วเราไม่ได้แก้ไขเพราะ detached HEAD เป็น state ไม่ใช่ error แต่ในหัวข้อนี้ผมจะแยกออกเป็น 3 กรณีดังนี้

กรณีที่ 1 เราเข้าสู่ detached HEAD โดยไม่ได้ตั้งใจ

ในกรณีนี้เราอาจเข้าสู่ detached state โดยไม่ตั้งใจ ให้เราทำการ checkout หรือ switch กลับไปที่ branch เดิมได้เลยโดยใช้คำสั่ง git checkout ซึ่งเราต้องระบุ branch ที่เราอยุ่ก่อนหน้านี้

git checkout [branch-name] # ระบุ branch ที่เราอยุ่ก่อนหน้านี้

# หรือ
git switch -

เช่นเราอยากจะกลับไปที่ master branch เราจะใช้คำสั่ง checkout หรือ switch เพื่อกลับไปที่จุดเริ่มต้นดังรูป

git checkout master

# หรือ
git switch -

Git attached head state
ปกติเราจะอยู่ใน Attached HEAD state เพราะเราจะทำงานต่อจาก commit ล่าสุดเสมอ

กรณีที่ 2 เราลองแก้ไข code แล้วแต่ไม่ work

ในกรณีนี้เราได้เข้าสู่ detached HEAD state แล้วลองแก้ไข code โดยการ commit code ใหม่เข้าไปแล้ว แต่ไม่ work อยากย้อนกลับไปที่จุดเริ่มต้น โดยที่เราจะไม่เก็บ commit หลังจาก detached HEAD ไว้ ให้เรา run คำสั่งเดียวกับในกรณีที่ 1

git checkout [branch-name] # ระบุ branch ที่เราอยุ่ก่อนหน้านี้

# หรือ
git switch -

ถ้าคุณอยากกลับไปที่ attached HEAD state แบบปกติให้ใช้คำสั่ง git checkout ย้อนกลับไปยัง branch ที่เรามาได้เลย

กรณีที่ 3 เราลองแก้ไข code แล้ว work

กรณีนี้เราจะได้ลองแก้ไข code โดยการ commit เข้าไปแล้วปรากฎว่า work เราต้องการเก็บ code ชุดใหม่นี้ไว้ใน repository ให้เรา run คำสั่งนี้

git branch [ตั้งชื่อ branch ใหม่]
git checkout [ชื่อ branch ใหม่]

# หรือ
git switch -c [ชื่อ branch ใหม่]

ในตัวอย่างยี้เราจะใช่คำสั่งนี้ เพื่อสร้าง branch alt-history ขึ้นมาใหม่

git switch -c alt-history

หลังจากที่เราได้ run command ด้านบนเรียนร้อยแล้วให้ลองเปิด log message ดูด้วยคำสั่ง

git log --oneline

Git log one line

เราจะเห็นว่าใน log บรรทัดแรก add847b นั้นเปลี่ยนจาก (HEAD) เป็น (HEAD -> alt-history) แสดงว่าเรากลับสู่ attached HEAD state เรียบร้อยแล้ว แต่อยู่คนละ branch กับตอนเริ่มต้น ดังรูป

หลังจากนี้เราสามารถเลือก merge กลับเข้าสู่ main หรือ master branch ได้ตามปกติ

Back to attached head state

ข้อดีของการมี Detached HEAD state

การที่เรามี detached HEAD นั้นเหมือนกับ multiverse เมื่อเรานั่ง time machine ย้อนเวลากลับไปแก้ไขอดีตมันจะทำให้เกิดการแตกเส้นเวลาออกมาใหม่ นั่นคือเส้นเดิมจะยังอยู่และไม่ได้รับผลกระทบจากสิ่งที่คุณแก้ไข คุณสามารถเลือกได้ว่าคุณจะใช้ชีวิตบนเส้นเวลาที่เกิดขึ้นมาใหม่(จากการแก้ไขอดีต) หรือคุณอาจอยากย้อนกลับไปอยู่ในเส้นเวลาเดิมก็ได้

เส้นเวลาที่แตกออกมาคือ git branch นั่นเอง

ตัวอย่าง case ที่อาจเกิดขึ้นจริงคือ คุณพบว่ามี bug เกิดขึ้นมาจากการแก้ไข code เมื่อหลายวันก่อน คุณต้อง checkout code ที่มีปัญหาแล้วมาลองแก้ไขใหม่ ซึ่ง ณ จุดนี้จะกลายเป็น detached HEAD ทันทีที่เรา checkout ซึ่งเมื่อลองแก้ไขแล้วคุณจะมีทางเลือก 2 ทางคือ

ปิดข้อความแจ้งเตือน

ในกรณีที่เราต้องการปิด message ที่แจ้งเตือนว่าตอนนี้เราได้เปลี่ยนไปอยู่ใน detached HEAD state แล้ว ให้เราทำการปรับ config ด้วยคำสั่งนี้

git config advice.detached head false

ตามไปดูวิธีการปรับ config ใน scope ต่างๆได้ที่บทความ การจัดการ git config

การปิด Git Detached HEAD message นี้ผู้ใช้ควรมีความคุ้นเคยและความเข้าใจใน Detached HEAD และ Attached HEAD เป็นอย่างดี

อ่านต่อได้ที่

Phanupong Permpimol
Follow me

Software Engineer ที่เชื่อในเรื่องของ Process เพราะเมื่อ Process ดี Product ก็จะดีตาม ปัจจุบันเป็นอาจารย์และที่ปรึกษาด้านการออกแบบและพัฒนา Software และ Web Security