ทำไม Git บอกว่าสำเร็จ แต่ผลลัพธ์กลับไม่ใช่แบบนั้น
เวลาเราหัดเขียนโค้ด เรามักจะเชื่อใจคอมพิวเตอร์และเครื่องมือที่ใช้ทำงานเสมอ โดยเฉพาะ Git (ระบบเก็บประวัติการแก้ไขโค้ด) ที่มักจะแสดงข้อความว่า "Successfully" หรือ "Done" ให้เราอุ่นใจ แต่บ่อยครั้งที่ข้อความเหล่านั้นบอกแค่ว่า "คำสั่งทำงานจบแล้วนะ" ไม่ได้แปลว่า "ผลลัพธ์ในไฟล์ของคุณถูกต้องตามที่คาดหวัง" นี่คือกับดักที่โปรแกรมเมอร์มือใหม่หลายคนเจอจนเสียเวลาแก้บั๊ก (ข้อผิดพลาดในโปรแกรม) มานักต่อนัก
การเชื่อมั่นในข้อความแจ้งเตือนโดยไม่ตรวจสอบ State (สถานะปัจจุบันของไฟล์ในโปรเจกต์) คือนิสัยที่อันตราย เหมือนเราสั่งให้เพื่อนไปซื้อข้าวผัด เพื่อนบอกว่า "ซื้อมาให้แล้ว" แต่ในถุงกลับเป็นกะเพรา คุณควรจะเปิดถุงดูให้แน่ใจก่อนจะเริ่มกิน ไม่ใช่เชื่อแค่คำพูดของเพื่อน การเขียนโปรแกรมก็เช่นกัน เราต้องฝึกนิสัยตรวจสอบสิ่งที่เกิดขึ้นจริงในโฟลเดอร์งานของเรา แทนที่จะเชื่อเพียงข้อความบนหน้าจอ terminal (หน้าต่างพิมพ์คำสั่ง)
ในบทความนี้ เราจะมาดูว่ามีจุดไหนบ้างใน Git ที่มักจะหลอกให้เราตายใจ ว่าทุกอย่างเรียบร้อยดีทั้งที่จริงๆ แล้วมีปัญหาซ่อนอยู่ การเข้าใจเรื่องพวกนี้จะช่วยให้คุณกลายเป็นโปรแกรมเมอร์ที่ละเอียดรอบคอบ และไม่ต้องปวดหัวกับการแก้ปัญหาที่เกิดจากความเข้าใจผิดในอนาคต เตรียมตัวให้พร้อม แล้วมาดูกันว่าเราจะรับมือกับสถานการณ์เหล่านี้ได้อย่างไร
เมื่อระบบเรียนรู้การแก้ปัญหาอัตโนมัติทำงานผิดพลาด
Rerere (Reuse Recorded Resolution - ระบบจำวิธีแก้ความขัดแย้งของโค้ด) เป็นฟีเจอร์ที่ช่วยให้ Git จำวิธีที่คุณเคยแก้ Merge Conflict (สถานการณ์ที่โค้ดในไฟล์เดียวกันถูกแก้ไขต่างกันจน Git รวมไม่ได้) ไว้ เมื่อเจอเหตุการณ์เดิมอีก มันจะแก้ให้เองอัตโนมัติ ฟังดูเหมือนจะดีและช่วยประหยัดเวลา แต่จริงๆ แล้วมันมีเงื่อนไขที่ซับซ้อนกว่านั้น ถ้าคุณไม่เข้าใจว่ามันทำงานอย่างไร คุณอาจจะเผลอใช้คำสั่งที่ลบความจำของมันทิ้งไปโดยไม่รู้ตัว
ปัญหาที่มือใหม่มักเจอคือการใช้คำสั่ง git add เพื่อบันทึกการแก้ปัญหา แต่ดันไป abort (ยกเลิกการรวมโค้ด) ทันที ทำให้ Git ไม่ได้บันทึกการเรียนรู้นั้นไว้ หรือบางครั้งการตั้งค่า rerere.autoupdate (การให้ Git แก้ไขไฟล์และบันทึกให้เองโดยอัตโนมัติ) อาจทำให้คุณเผลอข้ามขั้นตอนการตรวจสอบโค้ดที่ Git แก้ให้ไป เพราะ Git จะทำการ Stage (เตรียมไฟล์เพื่อบันทึกประวัติ) ให้ทันทีโดยที่คุณไม่ได้เห็นว่ามันแก้ถูกหรือผิด
ทางที่ดีที่สุดคือการปิดโหมด autoupdate ไว้ เพื่อให้คุณมีโอกาสตรวจสอบสิ่งที่ Git แก้ให้ก่อนเสมอ หากคุณพบว่ามันแก้ผิด คุณสามารถใช้คำสั่งเพื่อลบความจำนั้นทิ้งได้ทันที การตรวจสอบด้วยตัวเองก่อนจะสั่ง git commit (บันทึกการเปลี่ยนแปลงลงประวัติ) คือการสร้าง Checkpoint (จุดตรวจสอบความถูกต้อง) ให้กับตัวเอง เพื่อให้มั่นใจว่าโค้ดที่รวมเข้ามาใหม่นั้นถูกต้องจริงๆ
# ปิดการอัปเดตอัตโนมัติเพื่อตรวจสอบก่อนเสมอ
git config --global rerere.enabled true
# หาก Git จำวิธีแก้ผิด ให้สั่งลบความจำนั้นทิ้ง
git rerere forget
# ดึงเครื่องหมายความขัดแย้งกลับมาตรวจสอบใหม่
git checkout -m
คำอธิบาย: บรรทัดแรกเป็นการเปิดใช้งานระบบจำวิธีแก้ปัญหา ส่วนบรรทัดที่สองและสามคือวิธีแก้ไขเมื่อคุณพบว่า Git จำวิธีแก้มาผิดพลาด คุณจะได้เริ่มต้นใหม่ได้โดยไม่มีขยะทางความคิดค้างอยู่ในระบบ
ผลลัพธ์: คุณจะได้เห็นไฟล์ที่มีเครื่องหมาย <<<<<<< HEAD กลับมาอีกครั้ง ทำให้คุณสามารถแก้ไขโค้ดด้วยมือเพื่อให้แน่ใจว่าโปรแกรมยังทำงานได้ถูกต้อง
ความสับสนระหว่างการย้ายฐานของโค้ดและการอัปเดต
หลายคนชอบใช้ git rebase (การย้ายชุดคำสั่งไปต่อท้ายจุดใหม่) เพื่อให้ประวัติโค้ดดูสะอาด แต่ --autosquash (ฟีเจอร์ช่วยรวมคำสั่งแก้ไขย่อยๆ เข้ากับคำสั่งหลัก) อาจทิ้ง Fixup commit (คำสั่งบันทึกการแก้ไขเล็กน้อยที่ควรจะถูกรวมไปแล้ว) ค้างไว้ในประวัติโดยที่คุณไม่รู้ตัว Git จะขึ้นข้อความว่า "Successfully rebased" แต่เมื่อคุณไปดู git log (ประวัติการบันทึกโค้ด) คุณจะตกใจที่เห็นว่าคำสั่งเหล่านั้นยังอยู่ครบ
นอกจากนี้ เวลาคุณทำงานกับ Pull Request (การขอรวมโค้ดที่เขียนเสร็จแล้วเข้ากับโค้ดหลัก) บน GitHub การกดปุ่ม "Change base" (เปลี่ยนจุดอ้างอิงของงาน) ไม่ได้แปลว่าคุณได้อัปเดตโค้ดล่าสุดจากสาขาหลักมาไว้ในงานของคุณแล้ว มันแค่บอกว่า PR ของคุณจะถูกเอาไปรวมที่ไหน แต่ตัวโค้ดข้างในยังคงเป็นเวอร์ชันเก่าที่อาจขัดแย้งกับโค้ดใหม่ที่เพิ่งถูกรวมเข้ามา
ความผิดพลาดนี้มักเกิดขึ้นกับโปรแกรมเมอร์จูเนียร์ที่เห็นคำว่า "Mergeable" (รวมได้) แล้วรีบกดปุ่มรวมโค้ดทันที โดยไม่ได้ตรวจสอบว่าโค้ดที่ตัวเองเขียนนั้นเข้ากับโค้ดล่าสุดจริงๆ หรือไม่ การตรวจสอบด้วยคำสั่ง git merge-base (คำสั่งหาจุดร่วมล่าสุด) จะช่วยให้คุณมั่นใจว่างานของคุณมีโค้ดล่าสุดจากสาขาหลักรวมอยู่ด้วยจริงๆ ไม่ใช่แค่การเดาเอาเอง
# ตรวจสอบว่าโค้ดของเรามีสาขาหลักล่าสุดรวมอยู่แล้วหรือยัง
git fetch origin
git merge-base --is-ancestor origin/main HEAD && echo "OK" || echo "STALE"
คำอธิบาย: คำสั่งนี้จะไปดึงข้อมูลล่าสุดจาก origin (แหล่งเก็บโค้ดหลักบนอินเทอร์เน็ต) แล้วตรวจสอบว่า HEAD (ตำแหน่งปัจจุบันของเรา) มีโค้ดจาก main (สาขาหลัก) ครบถ้วนไหม
ผลลัพธ์: ถ้าได้คำว่า "OK" แปลว่าปลอดภัย แต่ถ้าได้ "STALE" แปลว่าโค้ดของคุณยังไม่อัปเดต ต้องทำการรวมโค้ด (merge) เข้ามาก่อน
การอ้างอิงหลักฐานที่ผิดพลาด
เวลาคุณส่งลิงก์โค้ดให้เพื่อน หรือแปะไว้ในเอกสารเพื่อเป็นหลักฐาน อย่าใช้ลิงก์ที่ชี้ไปที่ main โดยตรง เพราะวันพรุ่งนี้โค้ดใน main อาจเปลี่ยนไปแล้ว ทำให้คนที่มาอ่านทีหลังงงว่าคุณพูดถึงโค้ดส่วนไหนกันแน่ นี่คือปัญหาที่มือใหม่มักมองข้ามในการทำ Documentation (เอกสารอธิบายการทำงานของโปรแกรม)
วิธีที่ดีที่สุดคือการอ้างอิงด้วย SHA (รหัสลายนิ้วมือเฉพาะของแต่ละการบันทึกโค้ด) เสมอ รหัส SHA เป็นตัวเลขและตัวอักษรยาวๆ ที่ระบุตัวตนของการบันทึกนั้นๆ แบบไม่มีวันเปลี่ยน ต่อให้คุณจะแก้โค้ดไปไกลแค่ไหน ลิงก์ที่ชี้ไปที่ SHA เดิมก็จะยังคงเป็นโค้ดเดิมที่คุณเคยเห็นเสมอ
การฝึกนิสัยอ้างอิงด้วย SHA จะทำให้คุณดูเป็นโปรแกรมเมอร์ที่ใส่ใจรายละเอียด และช่วยให้เพื่อนร่วมทีมหรือคนรีวิวโค้ดไม่ต้องมานั่งเดาว่าคุณกำลังพูดถึงบรรทัดไหน มันคือความแตกต่างระหว่างคนเขียนโค้ดที่เน้นแค่ให้งานเสร็จ กับคนที่เน้นความถูกต้องและชัดเจนในการทำงานร่วมกัน
เมื่อต้องเก็บงานชั่วคราวแต่ไม่อยากเสียไฟล์
หลายคนใช้ git stash (การซ่อนงานชั่วคราว) เพื่อสลับสาขาไปแก้บั๊กอื่น แต่คุณรู้ไหมว่า stash ไม่เก็บไฟล์ที่คุณเพิ่งสร้างใหม่และยังไม่ได้สั่ง git add ถ้าคุณเผลอเปลี่ยนสาขาไป ไฟล์พวกนั้นอาจหายวับไปกับตา หรือกลายเป็นความวุ่นวายเมื่อคุณกลับมาที่สาขาเดิม
วิธีที่มืออาชีพใช้กันคือ git worktree (การเปิดโฟลเดอร์งานเพิ่มโดยใช้ประวัติเดิม) แทนการ stash เพราะมันช่วยให้คุณเปิดโฟลเดอร์ใหม่ขึ้นมาทำงานอีกสาขาได้เลยโดยไม่ต้องสลับไปสลับมา ไฟล์ที่ยังไม่ได้บันทึกก็ยังอยู่ที่เดิมในโฟลเดอร์เก่า ไม่ปนกันให้วุ่นวาย
การใช้ worktree ช่วยให้คุณมีสมาธิกับงานปัจจุบันได้ดีขึ้นมาก เพราะคุณไม่ต้องกลัวว่างานที่กำลังค้างอยู่จะพังจากการสลับสาขา ลองหัดใช้คำสั่งนี้ดู แล้วคุณจะพบว่าชีวิตการเขียนโค้ดง่ายขึ้นเยอะ ไม่ต้องคอยกังวลกับการเก็บงานเข้า stash อีกต่อไป
# สร้างโฟลเดอร์ใหม่เพื่อทำงานในสาขาอื่นโดยไม่กระทบงานปัจจุบัน
git worktree add ../new-feature-folder main
# เมื่อทำงานเสร็จให้ลบโฟลเดอร์นั้นทิ้ง
git worktree remove ../new-feature-folder
คำอธิบาย: คำสั่งแรกสร้างโฟลเดอร์ใหม่ขึ้นมาดึงโค้ดจากสาขา main มาให้ ส่วนคำสั่งที่สองคือการทำความสะอาดเมื่อคุณทำงานเสร็จเรียบร้อยแล้ว
ผลลัพธ์: คุณจะได้โฟลเดอร์งานแยกออกมาอีกหนึ่งจุด ทำให้คุณมีสองสาขาที่ทำงานได้พร้อมกันโดยไม่ตีกัน
สรุป: อย่าเชื่อข้อความ แต่จงเชื่อหลักฐาน
หัวใจสำคัญของบทความนี้ไม่ใช่การบอกว่า Git ไม่ดี แต่คือการบอกว่า ความสำเร็จของคำสั่ง Git คือการบอกว่าคำสั่งทำงานจบ แต่ไม่ได้แปลว่าผลลัพธ์ในโปรเจกต์ของคุณถูกต้องตามความต้องการ โปรแกรมเมอร์ที่เก่งไม่ได้เก่งที่จำคำสั่งได้แม่น แต่เก่งที่รู้จักตรวจสอบผลลัพธ์ของตัวเองเสมอ
สมมติว่าคุณกำลังจะรวมโค้ดเข้ากับสาขาหลัก แทนที่จะกดปุ่ม Merge แล้วจบไป ให้คุณลองเช็ก git log ดูสักนิดว่ามี fixup! ค้างอยู่ไหม หรือลองรันคำสั่ง git merge-base เพื่อดูว่าโค้ดเราอัปเดตหรือยัง การทำแบบนี้ใช้เวลาไม่ถึง 30 วินาที แต่ช่วยป้องกันบั๊กที่อาจใช้เวลาแก้เป็นวัน
จำไว้ว่าในฐานะโปรแกรมเมอร์ "ความมั่นใจ" ต้องมาพร้อมกับ "หลักฐาน" เสมอ ไม่ว่าจะเป็นการตรวจไฟล์ใน Index (พื้นที่เตรียมบันทึกโค้ด) การดูประวัติ หรือการเช็กสถานะของสาขา ฝึกนิสัยตรวจสอบก่อนเชื่อข้อความแจ้งเตือน แล้วคุณจะก้าวข้ามจากมือใหม่ไปสู่ระดับมืออาชีพที่ใครๆ ก็ไว้ใจได้
ที่มา: Git Said It Succeeded. The State Said Otherwise. — DEV Community