- Git เป็นสิ่งที่ค้นหาวิธีแก้ได้ยากหากไม่รู้ชื่อของสถานการณ์ที่ทำพลาด เอกสารนี้จึงรวบรวม คำสั่งตามสถานการณ์กู้คืนที่พบบ่อย เพื่อให้ค้นหาแล้วนำไปใช้ได้ทันที
- เมื่อทำ repository พังหนัก หรือต้องการย้อนการลบ/การ merge ที่ผิดพลาด สามารถใช้
git reflog เพื่อหา history ของการทำงาน แล้วใช้ git reset HEAD@{index} เพื่อกลับไปยัง สถานะก่อนหน้า ได้
- การแก้ไขเล็กน้อยหรือเปลี่ยนข้อความของ commit ล่าสุด ทำได้รวดเร็วด้วย
git commit --amend แต่ไม่ควรใช้กับ commit ที่ push ไปยัง branch สาธารณะ/ที่ใช้ร่วมกัน แล้ว
- หาก commit ผิด branch สามารถย้ายได้ด้วย flow
reset, stash, checkout, stash pop หรือใช้ cherry-pick แต่แนวทางที่ปลอดภัยจะแตกต่างกันตามว่ามีการ push แล้วหรือไม่
reset --hard และ git clean -d --force ที่ใช้บังคับกู้คืนให้ตรงกับสถานะของ remote repository อาจลบไฟล์ที่ไม่ได้ถูกติดตามไปด้วย จึงต้องตรวจสอบก่อนรัน
ทำไมการกู้คืนความผิดพลาดใน Git ถึงยาก
- Git ทำพลาดได้ง่าย และหลายครั้งถ้าต้องการแก้ปัญหา คุณต้องรู้ชื่อแนวคิดที่จำเป็นอยู่แล้วจึงจะค้นหาได้
- เอกสารนี้รวบรวมสถานการณ์แย่ ๆ ที่พบบ่อยใน Git ด้วย ภาษาอังกฤษแบบเข้าใจง่าย พร้อมตัวอย่างคำสั่ง
- ไม่ใช่ reference ที่ครบถ้วนสมบูรณ์ และงานเดียวกันอาจมีวิธีจัดการแบบอื่นได้เช่นกัน
ย้อน repository กลับไปยังสถานะในอดีต
- เมื่อทำ repository พังหนัก หรือต้องการกู้สิ่งที่ลบไป ให้ตรวจสอบ history ของการทำงานด้วย
git reflog
git reflog
# 모든 브랜치에 걸친 Git 작업 목록을 볼 수 있음
# 각 항목은 HEAD@{index} 형태의 인덱스를 가짐
git reset HEAD@{index}
reflog ใช้ได้ในสถานการณ์ต่อไปนี้
- กู้คืนสิ่งที่ลบโดยไม่ตั้งใจ
- ลบความพยายามที่ทำให้ repository พัง
- กู้คืนหลังจาก merge ที่ไม่ดี
- ย้อนกลับไปยังจุดที่เคยทำงานได้จริง
วิธีแก้ commit ล่าสุด
- หากต้องเพิ่มการแก้ไขเล็กน้อยหลัง commit ล่าสุด ให้ใช้
git commit --amend --no-edit
# 변경 후
git add . # หรือเพิ่มไฟล์ทีละไฟล์
git commit --amend --no-edit
- คำสั่งนี้จะรวมการเปลี่ยนแปลงใหม่เข้าไปใน commit ล่าสุด
- คุณอาจสร้างการเปลี่ยนแปลงเดียวกันเป็น commit ใหม่แล้วค่อยรวมด้วย
rebase -i ได้เช่นกัน แต่ amend เร็วกว่ามาก
- ไม่ควรใช้
amend กับ commit ที่ push ไปยัง branch สาธารณะ/ที่ใช้ร่วมกัน แล้ว
- หากต้องการเปลี่ยนเฉพาะข้อความของ commit ล่าสุด ให้ใช้คำสั่งต่อไปนี้
git commit --amend
# 안내에 따라 커밋 메시지 변경
เมื่อ commit ผิด branch
- หากเผลอ commit ลง
master แต่ควรอยู่ใน branch ใหม่ ให้สร้าง branch ใหม่จากสถานะปัจจุบัน แล้วลบ commit ล่าสุดออกจาก master
git branch some-new-branch-name
git reset HEAD~ --hard
git checkout some-new-branch-name
- วิธีนี้ไม่เหมาะกับ commit ที่ push ไปยัง branch สาธารณะ/ที่ใช้ร่วมกัน แล้ว
- หากเคยลองวิธีอื่นมาก่อน อาจต้องใช้รูปแบบ
HEAD@{number-of-commits-back} แทน HEAD~
- หาก commit ลง branch ผิดโดยสิ้นเชิง สามารถย้ายด้วย flow
reset --soft, stash, checkout, stash pop ได้
git reset HEAD~ --soft
git stash
git checkout name-of-the-correct-branch
git stash pop
git add . # หรือเพิ่มไฟล์ทีละไฟล์
git commit -m "your message here"
- ในสถานการณ์เดียวกัน ยังมีวิธีใช้
cherry-pick ได้ด้วย
git checkout name-of-the-correct-branch
git cherry-pick master
git checkout master
git reset HEAD~ --hard
ย้อนการเปลี่ยนแปลงและ commit เก่า
- หากแก้ไฟล์แล้ว
git diff ว่างเปล่า อาจเป็นเพราะ stage ไปแล้ว จึงต้องใช้ flag --staged
git diff --staged
- หากต้องการย้อนการเปลี่ยนแปลงจาก commit เมื่อหลาย commit ก่อน ให้หา hash ด้วย
git log แล้วรัน git revert
git log
git revert [saved hash]
git revert จะสร้าง commit ใหม่ ที่ย้อน commit นั้น
- หากต้องการย้อนเฉพาะไฟล์บางไฟล์ ไม่ใช่ทั้ง commit ให้เอาเวอร์ชันเก่าของไฟล์ขึ้น index แล้ว commit
git log
git checkout [saved hash] -- path/to/file
git commit -m "Wow, you don't have to copy-paste to undo"
บังคับกู้คืนให้ตรงกับสถานะของ remote repository
- แทนที่จะลบ repository แล้ว clone ใหม่ สามารถใช้คำสั่ง Git เพื่อปรับให้ตรงกับสถานะของ remote repository ได้
git fetch origin
git checkout master
git reset --hard origin/master
git clean -d --force
git reset --hard origin/master จะปรับ branch ให้ตรงกับสถานะของ master บน remote
git clean -d --force จะลบไฟล์และไดเรกทอรีที่ไม่ได้ถูกติดตาม
- ต้องทำซ้ำ
checkout, reset, clean กับแต่ละ branch ที่พัง
- งานเหล่านี้เป็นการกระทำที่ ทำลายข้อมูลและกู้คืนไม่ได้ จึงต้องระวังก่อนรัน
ขอบเขตและการมีส่วนร่วม
- เอกสารนี้ไม่ใช่ reference ของ Git ที่ครบถ้วน แต่เป็นชุดขั้นตอนกู้คืนภาคปฏิบัติที่ได้มาจากการลองผิดลองถูก
- หากต้องการช่วยเพิ่มคำแปล สามารถส่ง PR ได้ที่ GitHub
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
มีอยู่สองสามจุดที่อยากเปลี่ยน: จะใช้
git switchแทนgit checkoutเสมอ และจะพยายามหลีกเลี่ยงreset --hardให้มากที่สุดตัวอย่างเช่น กรณี “เผลอ commit ลง master ทั้งที่ควรอยู่บน branch ใหม่” สามารถจัดการตามลำดับ
git branch some-new-branch-name,git switch -d HEAD~,git switch -C master,git switch some-new-branch-nameได้กรณี “commit ผิด branch” ก็จะจัดการโดย
git switchไปยัง branch ที่ถูกต้อง แล้วใช้git cherry-pick master,git switch -d master~,git switch -C masterส่วน “ยอมแพ้ทั้งหมดแล้วย้อนกลับไปเป็นสถานะบน remote” ก็จะประกอบเป็น
git fetch origin,git restore -WS .,git clean -d --force,git switch -d origin/master,git switch -C masterประมาณนี้ต่อให้เข้าใจโมเดลของ Git ดี สูตรพวกนี้ก็ไม่เป็นธรรมชาติ และยังต้องรู้พฤติกรรมเฉพาะของแต่ละคำสั่งด้วย
เช่น
git switch -c some-new-branch-nameมีอยู่แล้วก็จริง แต่คำสั่งอย่างgit move-branch master HEAD~ที่ย้ายเฉพาะ branch ไปยัง commit อื่นน่าจะเข้าใจได้เป็นธรรมชาติกว่าgit switchและgit restoreแทนgit checkoutแต่คำสั่งเหล่านี้ถูกนำเข้ามาใน Git v2.23 และ release นั้นก็เมื่อราว 5 ปีก่อนในหน้า help ยังมีคำเตือนว่า
THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.อยู่แน่นอนว่าคำเตือนนี้มีมาตั้งแต่ช่วงแรก ๆ ที่คำสั่งเกิดขึ้น และผ่านไป 5 ปีแล้วก็น่าจะถึงเวลาที่ไม่ต้องเรียกว่าทดลองได้แล้ว
ถึงอย่างนั้น
git checkoutก็ backward compatible กว่า และถือได้ว่าสะท้อนวิธีใช้ Git ในช่วงเวลาที่เว็บไซต์นี้ถูกสร้างขึ้นเดิมhttps://github.com/git/git/blob/757161efcca150a9a96b312d9e78...
https://github.com/git/git/releases/tag/v2.23.0
https://github.com/git/git/commit/4e43b7ff1ea4b6f16b93a432b6...
git add .เป็นค่าเริ่มต้นเวลาเอาการเปลี่ยนแปลงขึ้น staging area นั้นไม่ค่อยดีการระบุไฟล์ที่ต้องการเพิ่มอย่างชัดเจนมีโอกาสน้อยกว่าที่ภายหลังจะต้องพูดว่า “แย่แล้ว”
อยากรู้ว่า
switchดีกว่าcheckoutอย่างไร และทำไมถึงไม่ควรใช้reset --hardreset --hard” หมายความว่าอะไรอยากรู้ว่าจริง ๆ แล้วมันไม่เพียงพอตรงไหน และผมใช้มันค่อนข้างบ่อยร่วมกับ
alias git-restore-file='git restore --source=HEAD --'ซึ่งดูเหมือนจะทำงานได้ดีควรเริ่มแนะนำ GUI client เป็นเส้นทางพื้นฐานในการเรียน Git
แบบนั้นปัญหาหนึ่งในสามจะถูกแก้ได้ และอีกหนึ่งในสามจะไม่เกิดขึ้นตั้งแต่แรก
ถ้าภายหลังตัดสินว่า command line เร็วกว่าก็ค่อยใช้ ตอนแรกผู้คนควรได้เห็นภาพว่าพวกเขาโต้ตอบกับ tree อย่างไรก่อน
ส่วนตัวชอบ fork.dev แต่ client สมัยนี้โดยรวมก็คล้าย ๆ กัน
เมื่อใช้ Magit การกระทำอย่าง “ยกเลิก cherry-pick” สามารถค้นพบได้จากใน interface และใช้ shortcut เดียวกับงาน “ยกเลิก X” อื่น ๆ
ถ้าต้องใช้แต่ command line ของ Git อย่างเดียว คงไม่รู้จะเริ่มจากตรงไหน
เคยลบ commit ที่ไม่ควรลบระหว่าง interactive rebase ด้วย แต่ทุกครั้งที่เริ่ม rebase จะมี snapshot ที่เข้าถึงได้จาก reflog เกิดขึ้น จึงปลอดภัยที่จะย้อน rebase ที่ผิดพลาด
UI ของ reflog ใน Magit เหมือนกับ UI ของ log เลยไม่หลงทางแม้เห็นครั้งแรก แต่ถ้าเป็น Git CLI ก็น่าจะไม่รู้ว่าเกิดอะไรขึ้น
ชอบตรงที่เมื่อรัน
jjโดยไม่ใส่ argument มันจะแสดงส่วนที่เกี่ยวข้องของ commit tree และตำแหน่งปัจจุบันสามารถเห็น commit ที่กำลังทำงานอยู่, branch ที่เชื่อมอยู่กับ history, branch อื่นที่ยัง active ใน repository, ข้อความของแต่ละ commit และสถานะว่ามีการเปลี่ยนแปลงหรือไม่ ได้ในที่เดียว จึงยอดเยี่ยมมากสำหรับการตั้งหลักใหม่
แต่แทบไม่มีใครฟัง สุดท้ายก็ไปทรมานกับ CLI ทำ branch พัง ตาม main ไม่ทัน และกลัว rebase
ใน UI หลายเรื่องแก้ได้ด้วยการลากวางหรือวิธีคล้าย ๆ กัน ดูเหมือนเป็นการทรมานตัวเองรูปแบบหนึ่ง
แม้ interface จะเลวร้ายแค่ไหน การใช้ interface เดียวกับที่คนอื่นในทีมใช้ก็สื่อสารกันง่ายกว่า และแหล่งข้อมูลแก้ปัญหาออนไลน์ส่วนใหญ่ก็สมมติ “วิธีปกติ” เอาไว้ จึงนำมาใช้ได้สะดวกกว่า
พอคิดว่าเดือนเมษายน 2025 Git จะมีอายุครบ 20 ปี แล้ว ก็รู้สึกแปลกอยู่ไม่น้อย
ตอนนั้นผมอยู่หน้างาน และจำได้ว่าเคยลังเลว่าควรเรียน git, darcs หรือ bazaar เพื่อมาแทน SVN หรือ CVS ดี
ไม่แน่ใจว่าเคยลอง Mercurial ด้วยหรือเปล่า
สงสัยว่า “ผลของ GitHub” ได้ฆ่าความจำเป็นของผู้เล่นหน้าใหม่ในวงการระบบควบคุมเวอร์ชันไปโดยปริยายหรือไม่
บางทีอาจเป็นจุดที่การไล่โกนขนจามรีทำกันมาพอแล้วก็ได้
ผมย้ายไปใช้ jj เต็มตัวแล้ว และเพราะมันเข้ากันได้กับ Git จึงไม่ต้องรอให้คนอื่นเปลี่ยนตาม
ผมผิดหวังกับ GitHub ในหลายด้านมากขึ้นเรื่อย ๆ เลยหวังว่าสักวันทุกคนจะก้าวข้ามมันไปได้ด้วย แต่เส้นทางฝั่งนั้นดูไม่ได้ง่ายขนาดนั้น
SVN เข้าใจง่าย และทำงานได้ดีพอใน 99% ของกรณี จนไม่จำเป็นต้อง “สอน” ผู้คนเป็นพิเศษ
อยากให้พวกเราทุกคนเลิกทำตัวเป็นแฮ็กเกอร์ 1337 แล้วก็ยอมรับว่า Git เป็นเครื่องมือที่เกินจำเป็นสำหรับคนส่วนใหญ่
ผมคิดว่าตัวเองมี mental model ที่ค่อนข้างดีว่า Git ทำอะไรอยู่ แต่พอคำสั่งซับซ้อนขึ้นนิดเดียวก็จำอาร์กิวเมนต์ไม่ได้
คำสั่งทั้งหลายค้นพบเองได้ยาก และก็จำไม่ง่าย
ไม่รู้ว่าเป็นเพราะ text UI แย่ หรือเพราะการอธิบายงานจัดการ tree ด้วยข้อความนั้นยากในตัวมันเองกันแน่
การจัดการ tree ที่ซับซ้อนด้วยข้อความไม่ใช่เรื่องง่าย และ text UI ก็แย่ในเชิงวัตถุวิสัย: https://stevelosh.com/blog/2013/04/git-koans/
ดีใจที่ในเธรดนี้มีผลการค้นหา hg กับ Mercurial โผล่มา
ยังไม่อยากเชื่อว่า Mercurial แพ้ในสงคราม distributed version control และผมมองว่ามันเป็นเครื่องมือที่ดีกว่า
ใช้งานได้ เสถียร ผ่านการพิสูจน์ในงานจริง และยังมีการซัพพอร์ตอย่างคึกคัก
มันใกล้เคียงกับแนวคิด “จงเป็นความเปลี่ยนแปลงที่คุณอยากเห็น” ดังนั้นก็แค่ใช้มัน
การพูดว่ามันแพ้แล้วส่งผลเสียมาก และแรงกดทับโดยนัยจากคำพูดแบบนั้นก็ทำให้อัตราการยอมรับ Mercurial ยังคงต่ำ
สำหรับโฮสติ้ง อย่างน้อยก็ทำได้ที่ Sourcehut และ heptapod.host
ผมรันอินสแตนซ์ Heptapod ส่วนตัวอยู่ด้วย ซึ่งเป็น Gitlab fork ที่ใส่การรองรับ Mercurial โดยตรง และมันก็ทำงานได้ดี
Git เป็นหนึ่งในเทคโนโลยีที่ท้ายที่สุดก็ไม่ค่อยเข้าหัวผม เพราะมันไม่เป็นไปตามสัญชาตญาณในหลายแง่
ถ้าไม่ได้ใช้มานาน ก็มีโอกาสสูงที่แทบทุกงานต้องค้น Google หรือดู man page
โดยเฉพาะหลังจากเจอความสยองของการพยายามจัดการ codebase ขนาดใหญ่ใน SVN ด้วย branch จำนวนมากแล้ว merge กลับเข้าไป จะเห็นว่า Git ดีกว่าอย่างเหลือเชื่อในส่วนนั้น
สำหรับคนที่เริ่มจาก Git ตั้งแต่แรก เส้นโค้งการเรียนรู้อาจชันกว่ามาก แต่บางครั้งผมก็ยังเข้าใจยากอยู่ดีว่าคนไปติดตรงไหนกัน
ถ้าใช้วิธีในตัวอย่าง
filter-branchก็จะมี delay ยาว ๆ แล้วขึ้นคำเตือนให้กด Control+C และไปดาวน์โหลดสคริปต์ Python ของ third-party ชื่อfilter-repoไม่ได้ภูมิใจนัก แต่ปฏิบัติการ Git แบบ “พังแล้ว” ที่ผมใช้บ่อยที่สุดคือ ลบ repository ในเครื่อง แล้ว clone ใหม่ จากนั้นค่อย apply การเปลี่ยนแปลงกลับเข้าไป
95% ใช้ได้ดีมาก ส่วนที่เหลือก็ขอความช่วยเหลือจากคนดูแล DevOps
cp -R .git ../git-backupไว้ก่อนเพราะถ้าทำพังหนักเกินไป ก็แค่เอาไดเรกทอรี
.gitทั้งหมดไปแทนด้วยสำเนาเก่าเร็วกว่าการหาวิธี undo งานใดงานหนึ่งหรือชุดงานให้ถูกต้องมาก
สิ่งที่อยากให้คนซึมซับเกี่ยวกับ Git ให้ได้คือ Git เป็น data store แบบ append-only
ถ้าจะเอางานใส่ repository ก็ commit เข้าไป และเมื่องานเข้าไปแล้ว จะเอาออกด้วยคำสั่ง Git ไม่ได้
reflog ก็ทำงานตามหลักการนี้
ไม่ว่าคุณจะทำอะไร ก็สามารถย้อนกลับไปยังสถานะ branch ก่อนหน้า และกู้คืนงานจาก data store ได้
แต่ถ้าไม่ commit Git ก็ช่วยไม่ได้ ดังนั้นก็ commit ไปเถอะ
commit เสมอ และถ้าภาระมันมากเกินไปหรือคุณลืมอยู่เรื่อย ๆ ก็ควรแก้เครื่องมือ
ถ้าเขียนโค้ดทั้งวัน โดยทั่วไปอย่างน้อยควร commit ชั่วโมงละครั้ง
เรื่องเล็กน้อย แต่ทุกครั้งที่เห็น
git reflogมันดูเหมือน re-flogอาจเป็นเหตุผลที่ผมจำคำสั่งนี้ไม่ค่อยได้ และเหมือนในจิตใต้สำนึกไม่ได้คิดว่ามันคือ “reference log”
กลับให้ความรู้สึกเหมือนกำลังสั่งให้ Git ขายอะไรบางอย่างให้ถูกลงอีกครั้ง
แม้จะไม่ใช่ผู้ใช้ Git แต่พอเห็นแบบนี้ก็ยิ่งรู้สึกว่า
git commitเองไม่ได้มีความหมาย สิ่งสำคัญมีแค่ตอนที่ commit ถูก push หรือ merge เท่านั้นคล้ายกับการบันทึกไฟล์ข้อความ
เราไม่ได้เขียนข้อความสั้น ๆ ทุกครั้งที่บันทึกไฟล์ ดังนั้นก็ข้ามไปได้
ไม่ใช่แค่สั้น ๆ บางครั้งยิ่งการเปลี่ยนแปลงเล็ก คำอธิบายกลับยิ่งยาวขึ้นด้วยซ้ำ
เคยเขียน 2–3 ย่อหน้าให้กับการเปลี่ยนแปลง 2–3 บรรทัดที่มีผลกระทบใหญ่
commit ในเครื่อง คือวิธีสร้าง checkpoint ณ จุดที่ทุกอย่าง compile ได้และทำงานได้
เพื่อให้ stash การเปลี่ยนแปลง หรือย้อนกลับไปยังสภาพที่ยังทำงานได้เมื่อทำพัง
ก่อน push ก็ reset แล้วแบ่งเป็น commit ที่เหมาะสมได้ ประวัติบนรีโมตก็จะยังสะอาดและไม่กลายเป็นกอง
wipหรือจะ squash merge PR เพื่อได้ข้อดีของทั้งสองฝั่งก็ได้
เพียงแต่ไม่จำเป็นต้องใส่ข้อความที่ไม่มีความหมายอย่าง
more fixesโดยปกติจะสร้าง commit แรกประมาณ
(WIP) ชื่อฟีเจอร์แล้ว amend commit ซ้ำไปเรื่อย ๆ จนพอใจ จากนั้นค่อยลบ(WIP)ออกจากข้อความในตอนท้ายมองว่า revision log ของ Git ก็คล้ายกับ ไดอารี แบบนั้น
มันเป็นระเบียบกว่าเอกสาร TODO แยกต่างหาก และชวนให้เราอธิบายสิ่งที่ทำไปแล้วในรูปแบบที่อ่านง่าย
เวลาตัดสินใจว่าจะย้อนกลับไปแค่ไหน การรู้ว่าตอน commit นั้นคิดอะไรอยู่อาจมีประโยชน์ และถ้าแนวทางผิดพลาด ก็ช่วยในการวิเคราะห์ย้อนหลังได้ด้วย
ถ้างานที่ยังไม่ได้ merge หรือ push ถูกจัดเป็นการเปลี่ยนแปลงแยกกันพร้อมเป้าหมาย เมื่อบางส่วนดีและบางส่วนแย่ การเก็บเฉพาะส่วนที่ดีไว้ก็จะง่ายขึ้นมาก
ถึงอย่างนั้น ในเชิงเทคนิค commit ก็ไม่ได้ไร้ความหมาย
ถ้าไม่มี commit ก็ไม่มีอะไรให้ push หรือ merge
ในเชิงไม่ใช่เทคนิค การบันทึกเป็นข้อความธรรมดาว่าทำไมถึงเปลี่ยนบางอย่างก็มีประโยชน์มาก
ถ้าต้องเขียนคำอธิบายแบบนั้นบ่อยเกินไป ก็ควรทำให้หน่วยของ “การเปลี่ยนแปลง” แคบลง หรือคัดสิ่งที่แตะพร้อมกันให้เข้มงวดขึ้น
ถ้าไม่ได้ใช้ Git ก็สงสัยว่าใช้เครื่องมืออะไรในการจัดการเวอร์ชันและการจัดการการเปลี่ยนแปลง