git reflog: วิธีกู้คอมมิตและบรานช์ที่หายไป
git reflog คือบันทึกการอ้างอิง ที่ Git เก็บการเคลื่อนที่ของ HEAD และบรานช์ทุกครั้ง ได้แก่ คอมมิต reset checkout rebase และ merge ค้นหารายการก่อนความผิดพลาดในรายการ เช่น HEAD@{2} แล้วกู้สถานะด้วย git reset --hard HEAD@{2} หรือสร้างบรานช์ git branch rescue HEAD@{2}
git reflog คือบันทึกการอ้างอิง (reference log) Git จะบันทึกทุกการเคลื่อนที่ของ HEAD และปลายบรานช์ลงไป ทำคอมมิต สลับบรานช์ ทำ reset rebase หรือ merge แล้วจะมีบรรทัดใหม่ในรายการ ซึ่งมีแฮชที่การอ้างอิงชี้ไปหลังการดำเนินการ ดังนั้นแม้แต่คอมมิตที่ไม่มีบรานช์ใดชี้ไปแล้วก็ยังค้นพบและกู้คืนได้
สิ่งที่ควรรู้: reflog เป็นของเครื่องนั้น อยู่เฉพาะในโคลนของคุณ ไม่ถูกส่งขึ้นเซิร์ฟเวอร์และไม่มาพร้อม git clone แต่ในรีโพซิทอรีของคุณเอง มันช่วยให้รอดพ้นจากความผิดพลาดเกือบทุกประเภทที่เกี่ยวกับประวัติ
วิธีอ่านผลลัพธ์ของ git reflog
git reflog
3c4d5e6 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
9a8b7c6 HEAD@{1}: commit: เพิ่มการตรวจสอบอีเมล
1f2e3d4 HEAD@{2}: commit: ย้ายคอนฟิกไปไฟล์แยก
3c4d5e6 (HEAD -> main) HEAD@{3}: checkout: moving from feature to main
7b6a5c4 (feature) HEAD@{4}: commit: ฉบับร่างฟอร์มเข้าสู่ระบบ
แต่ละบรรทัดคือหนึ่งสถานะของ HEAD โดยบรรทัดบนสุดคือสถานะล่าสุด:
- แฮช คือคอมมิตที่ HEAD ชี้อยู่หลังการดำเนินการ
- HEAD@{n} คือ "HEAD อยู่ที่ไหนเมื่อย้อนกลับไป n การเคลื่อนที่" เป็นการอ้างอิงเต็มรูปแบบ ส่งให้คำสั่งใดก็ได้
- คำอธิบาย คือชนิดของการดำเนินการ (commit, reset, checkout, rebase, merge, pull, cherry-pick, commit (amend)) พร้อมรายละเอียด
จากตัวอย่างจะเห็นว่า มีการทำสองคอมมิต คือ "ย้ายคอนฟิกไปไฟล์แยก" และ "เพิ่มการตรวจสอบอีเมล" และจากนั้น reset HEAD~2 ย้ายบรานช์กลับ ใน git log ทั้งสองคอมมิตหายไปแล้ว แต่ใน reflog ยังอยู่ครบ
Reflog ของบรานช์หนึ่ง
บรานช์แต่ละตัวมีบันทึกของตัวเอง สั้นและมีประโยชน์กว่าเมื่อคุณสลับบรานช์บ่อย ๆ:
git reflog show feature
git reflog show origin/main # บรานช์รีโมตเปลี่ยนแปลงอย่างไรเมื่อ fetch
การอ้างอิงตามเวลา
แทนที่จะใช้หมายเลข ระบุช่วงเวลาได้ Git จะหาว่าการอ้างอิงชี้ไปที่ใดในตอนนั้น:
git show main@{yesterday}
git diff main@{"2 hours ago"} main
git log -g --since="1 day ago" --oneline
แฟล็ก -g (--walk-reflogs) ของ git log จะไล่ตาม reflog แทนการเชื่อมพาเรนต์ และทำให้ใช้รูปแบบและตัวกรองแบบเดิมได้ สำหรับแสดงวันที่ใช้ git reflog --date=iso
วิธีกู้คอมมิตหลัง git reset --hard
สถานการณ์ที่พบบ่อยที่สุด: คุณรัน git reset --hard ไปยังคอมมิตผิด และงาน "หายไป" จริง ๆ แล้วคอมมิตไม่ได้หายไปไหน เพียงแค่มองไม่เห็นจากบรานช์
- เปิดบันทึก:
git reflog - หาบรรทัดที่อยู่ก่อนบรรทัด
reset: moving to ...นั่นคือสถานะก่อนความผิดพลาด ในตัวอย่างข้างต้นคือHEAD@{1}กับแฮช9a8b7c6 - ตรวจว่าเป็นอันที่ต้องการ:
git log --oneline -5 HEAD@{1} - คืนบรานช์:
git reset --hard HEAD@{1}
git reset --hard HEAD@{1}
HEAD is now at 9a8b7c6 เพิ่มการตรวจสอบอีเมล
ระวัง: หลังการดำเนินการทุกครั้งหมายเลขจะเลื่อน สิ่งที่เป็น HEAD@{1} จะกลายเป็น HEAD@{2} หลัง reset ดังนั้นคัดลอกแฮชจะเชื่อถือได้มากกว่าการใช้หมายเลข
ข้อจำกัดที่สำคัญ: reflog เก็บเฉพาะสิ่งที่ถูกคอมมิตแล้วเท่านั้น การเปลี่ยนแปลงที่ยังไม่คอมมิตที่ถูกลบด้วย reset --hard หรือ git checkout -- ไฟล์ ไม่สามารถกู้คืนผ่าน reflog ยังมีความหวังเล็กน้อยหากไฟล์เคยถูกเพิ่มในดัชนี ซึ่งจะอธิบายด้านล่าง
วิธีกู้บรานช์ที่ถูกลบ
คุณลบบรานช์ด้วย git branch -D feature และในนั้นมีคอมมิตที่ยังไม่ได้ส่ง ประวัติของบรานช์นั้นถูกลบไปพร้อมบรานช์ แต่ reflog ของ HEAD ยังอยู่ และ HEAD เคยไปยังคอมมิตเหล่านั้นตอนที่คุณทำงานบนบรานช์
git reflog | grep feature
7b6a5c4 HEAD@{4}: commit: ฉบับร่างฟอร์มเข้าสู่ระบบ
3c4d5e6 HEAD@{3}: checkout: moving from feature to main
# คอมมิตสุดท้ายของบรานช์ คือคอมมิตที่คุณออกจากมันไป
git branch feature 7b6a5c4
มองหาบรรทัด checkout: moving from feature to ... แฮชในรายการก่อนหน้า (ที่อยู่ล่างกว่า) คือสถานะสุดท้ายของบรานช์ ที่จริงแล้ว เมื่อลบ Git จะพิมพ์แฮชออกมาให้: Deleted branch feature (was 7b6a5c4). หากเทอร์มินัลยังเปิดอยู่ ก็พอ
วิธียกเลิก rebase ที่ไม่สำเร็จ
Rebase เขียนคอมมิตใหม่ และหากคุณแก้ความขัดแย้งผิดระหว่างทาง ผลลัพธ์อาจแย่กว่าเดิม Reflog เก็บสถานะก่อนเริ่ม rebase ไว้
git reflog
e1f2a3b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e1f2a3b HEAD@{1}: rebase (pick): ฉบับร่างฟอร์มเข้าสู่ระบบ
c4d5e6f HEAD@{2}: rebase (start): checkout main
7b6a5c4 HEAD@{3}: commit: ฉบับร่างฟอร์มเข้าสู่ระบบ
git reset --hard HEAD@{3}
ต้องการรายการที่อยู่ก่อน rebase (start) มีทางลัดด้วย: ก่อนการดำเนินการที่มีความเสี่ยง (rebase, merge, reset) Git เก็บตำแหน่งเดิมไว้ใน ORIG_HEAD ดังนั้นหลัง rebase ทันทีใช้ git reset --hard ORIG_HEAD ได้ แต่ ORIG_HEAD จะถูกเขียนทับโดยการดำเนินการแบบเดียวกันครั้งถัดไป ส่วน reflog เก็บประวัติทั้งหมด
วิธีกู้คอมมิตก่อน git commit --amend
--amend ไม่ได้แก้คอมมิต แต่สร้างคอมมิตใหม่แทนที่ คอมมิตเดิมยังอยู่ใน reflog:
git reflog
5a6b7c8 HEAD@{0}: commit (amend): เพิ่มการตรวจสอบอีเมล
9a8b7c6 HEAD@{1}: commit: เพิ่มการตรวจสอบอีเมล
git diff HEAD@{1} HEAD # amend เปลี่ยนอะไรไปบ้าง
git reset --soft HEAD@{1} # คืนคอมมิตเดิม การเปลี่ยนแปลงจาก amend จะยังอยู่ในดัชนี
การกู้คืนอย่างปลอดภัย: สร้างบรานช์แทน reset
หากไม่แน่ใจว่าต้องการคอมมิตใด อย่า reset --hard แบบไม่ดูก่อน สร้างบรานช์สำรอง ซึ่งไม่ทำให้อะไรพัง และปกป้องคอมมิตที่พบไม่ให้ถูกลบ:
git branch rescue HEAD@{5}
git log --oneline rescue
git switch rescue
จากนั้นคอมมิตจาก rescue นำมาใช้ได้ด้วย cherry-pick หรือ merge และลบบรานช์เมื่อทุกอย่างกลับเข้าที่แล้ว
reflog เก็บไว้นานแค่ไหน
รายการไม่ได้อยู่ตลอดไป git gc ซึ่ง Git รันเองเป็นระยะ จะลบรายการเก่า กำหนดระยะเวลาด้วยการตั้งค่า:
| การตั้งค่า | ค่าเริ่มต้น | ความหมาย |
|---|---|---|
gc.reflogExpire | 90 วัน | ระยะเวลาสำหรับรายการที่คอมมิตเข้าถึงได้จากบรานช์ปัจจุบัน |
gc.reflogExpireUnreachable | 30 วัน | ระยะเวลาสำหรับรายการที่คอมมิตเข้าถึงไม่ได้จากบรานช์ใด ซึ่งคือคอมมิตที่หายไป |
gc.pruneExpire | 2 สัปดาห์ | หลังจากนั้นอ็อบเจกต์ที่เข้าถึงไม่ได้และไม่มีอะไรอ้างอิงจะถูกลบ |
ข้อสรุปเชิงปฏิบัติ: คอมมิตที่หายไปจะกู้ได้ผ่าน reflog ประมาณหนึ่งเดือน อย่ารอช้าในการกู้คืน และอย่ารัน git gc --prune=now หรือ git reflog expire --expire=now --all ด้วยตนเองจนกว่าจะกู้สิ่งที่ต้องการได้แล้ว
เมื่อ reflog ช่วยไม่ได้: git fsck
บางครั้งไม่มีคอมมิตใน reflog เช่น คอมมิตถูกสร้างโดย git stash แล้ว stash ถูกลบด้วย git stash drop หรือการเปลี่ยนแปลงอยู่ในดัชนีเท่านั้น ในกรณีนี้ให้หาอ็อบเจกต์ที่ลอยอยู่ (dangling):
git fsck --lost-found
dangling commit 2b3c4d5e6f...
dangling blob 8e9f0a1b2c...
git show 2b3c4d5e6f # ดูเนื้อหา
git branch recovered 2b3c4d5e6f
คอมมิตที่ลอยอยู่จาก stash ที่ถูกลบจะมีข้อความ "WIP on ..." ส่วน blob ที่ลอยอยู่คือเนื้อหาของไฟล์ที่เคยเพิ่มด้วย git add แต่ไม่ได้คอมมิต ไม่มีชื่อไฟล์ ต้องระบุจากเนื้อหา แต่บางครั้งก็ช่วยกู้งานหลัง reset --hard ได้ ด้วยแฟล็ก --lost-found Git ยังคัดลอกไปไว้ที่ .git/lost-found/ ด้วย
สิ่งที่ reflog ทำไม่ได้
- ไม่ทำงานข้ามเครื่อง. หากเพื่อนร่วมงาน force push ทับคอมมิตของคุณบนเซิร์ฟเวอร์ ให้หาในรีflog ของคนที่เคยมีคอมมิตนั้นในเครื่อง เช่น
git reflog show origin/mainบนเครื่องของใครก็ตามที่ fetch ก่อนที่จะถูกเขียนทับ - มองไม่เห็นสิ่งที่ยังไม่คอมมิต. ดังนั้นการป้องกันที่ดีที่สุดก่อนการดำเนินการเสี่ยงคือคอมมิตหรือ
git stash - ไม่เก็บประวัติตลอดไป. มีระยะเวลาตามที่ระบุไว้ข้างต้น และโคลนใหม่เริ่มด้วยบันทึกที่ว่างเปล่า
ปฏิบัติ: ป้องกันไว้ก่อนการดำเนินการเสี่ยง
Reflog ช่วยได้ แต่การกู้คืนผ่านมันต้องอาศัยความละเอียด เพราะต้องหารายการที่ถูกต้องท่ามกลางรายการที่คล้ายกันหลายสิบรายการ การตั้งเครื่องหมายไว้ล่วงหน้าง่ายกว่ามาก ก่อน rebase, reset ขนาดใหญ่ หรือการทดลองกับประวัติ ให้สร้างบรานช์หรือแท็กชั่วคราว:
git branch backup/before-rebase
git rebase -i main
# มีอะไรผิดพลาด
git reset --hard backup/before-rebase
# ทุกอย่างสำเร็จ
git branch -D backup/before-rebase
คำสั่งนี้ใช้เพียงหนึ่งบรรทัด และช่วยไม่ต้องค้นหาแฮช Reflog ยังคงเป็นทางสำรองสำหรับกรณีที่นึกถึงการป้องกันช้าเกินไป
วิธีหารายการที่ต้องการอย่างรวดเร็ว
ในบันทึกที่ยาว การกรองช่วยได้ ค้นตามชนิดของการดำเนินการหรือข้อความคอมมิต และถ้าไม่แน่ใจ ให้ดูเนื้อหา:
git reflog | grep -E "reset|rebase \(start\)"
git log -g --grep="ตรวจสอบ" --oneline
git show --stat HEAD@{7}
git log -g พร้อม --grep ค้นตามข้อความคอมมิตจาก reflog และ git show --stat แสดงว่าไฟล์ใดได้รับผลกระทบ จึงระบุคอมมิตที่ต้องการได้ แม้ข้อความจะไม่มีประโยชน์อย่าง "wip"
สรุป
git reflog เป็นตาข่ายนิรภัยของ Git: ทุกอย่างที่ถูกคอมมิตในรีโพซิทอรีของคุณในช่วงสัปดาห์ที่ผ่านมา สามารถค้นหาและกู้คืนได้ ลำดับการทำงานเหมือนเดิมเสมอ: เปิดบันทึก หารายการก่อนความผิดพลาด ตรวจสอบด้วย git log หรือ git show แล้วสร้างบรานช์หรือทำ reset --hard การกู้คืนด้วย restore, reset, revert และการแก้ความผิดพลาดถูกอธิบายและฝึกจริงในหลักสูตร Git
คำถามที่พบบ่อย
git reflog ต่างจาก git log อย่างไร?
git log ไล่ตามการเชื่อมพาเรนต์ของคอมมิตและแสดงประวัติของบรานช์ ส่วน git reflog แสดงว่า HEAD และบรานช์เคลื่อนไปที่ใดบ้างในรีโพซิทอรีของคุณ รวมถึงคอมมิตที่ไม่อยู่ในประวัติของบรานช์แล้ว
reflog ขึ้นไปบน GitHub หรือ GitLab หรือไม่?
ไม่ reflog เก็บเฉพาะในโฟลเดอร์ .git/logs ในเครื่อง และไม่ถูกส่งไปตอน push หรือ clone
ล้าง reflog ได้หรือไม่?
ได้ ด้วย git reflog expire --expire=now --all แล้วตามด้วย git gc --prune=now แต่หลังจากนั้นคอมมิตที่หายไปจะกู้คืนไม่ได้อีก
reflog เก็บอยู่ที่ไหนในดิสก์?
เป็นไฟล์ข้อความ .git/logs/HEAD และ .git/logs/refs/heads/ชื่อบรานช์ โดยมีหนึ่งบรรทัดต่อการเคลื่อนที่หนึ่งครั้ง
reflog ทำงานในรีโพซิทอรี bare บนเซิร์ฟเวอร์หรือไม่?
โดยค่าเริ่มต้นรีโพซิทอรี bare ปิด reflog ไว้ (core.logAllRefUpdates) ดังนั้นอย่าพึ่งพามันบนเซิร์ฟเวอร์
หัวข้อนี้สอนอย่างละเอียดพร้อมฝึกปฏิบัติในคอร์ส “Git с нуля до уверенного разработчика”
ไปที่คอร์ส