Git Merge, Rebase & Squash — ตอนที่ 3: Rebase — เขียนประวัติใหม่อย่างปลอดภัย

On this page
บทคัดย่อ
การบริหารจัดการประวัติการทำงานในระบบควบคุมเวอร์ชันให้มีความเป็นระเบียบมีความสำคัญอย่างยิ่งต่อการพัฒนาซอฟต์แวร์ร่วมกัน ในขณะที่การทำ merge จะเป็นการเชื่อมประวัติการทำงานโดยรักษารูปแบบกราฟสายการพัฒนาเดิมไว้ และการทำ squash จะรวบรวมหลาย commit ให้กลายเป็นหนึ่งเดียว กลไกของ rebase กลับใช้วิธีการเขียนประวัติการทำงานใหม่ทั้งหมดซึ่งทรงพลังแต่ต้องใช้ความระมัดระวังสูง กระบวนการ rebase ทำงานโดยการนำ commit จาก branch ปัจจุบันไปสร้างขึ้นใหม่ทีละรายการบนฐานใหม่ ส่งผลให้ประวัติการทำงานถูกจัดเรียงเป็นเส้นตรงและทำให้สามารถทำ fast-forward ได้โดยไม่ก่อให้เกิด merge commit ทว่าการเปลี่ยน parent commit ในกระบวนการนี้จะทำให้ค่า hash ของแต่ละ commit เปลี่ยนแปลงไปด้วยเสมอ ดังนั้น กฎทองที่สำคัญคือห้ามทำ rebase บน branch สาธารณะหรือที่ใช้งานร่วมกัน เช่น main, master หรือ production โดยเด็ดขาด แต่ควรจำกัดขอบเขตการใช้งานไว้เฉพาะบน private feature branch หรือ local commit ที่ยังไม่ได้เผยแพร่เท่านั้น นอกจากนี้ เมื่อจำเป็นต้องอัปเดตข้อมูลไปยังเซิร์ฟเวอร์หลังการทำ rebase จะต้องใช้แฟล็ก --force-with-lease แทนการใช้ --force แบบปกติ เพื่อตรวจสอบและป้องกันการเขียนทับผลงานของเพื่อนร่วมทีมโดยไม่ตั้งใจ บทความนี้อธิบายกลไกที่อยู่เบื้องหลังการทำ rebase เปรียบเทียบความแตกต่างกับการทำงานรูปแบบอื่น พร้อมทั้งกำหนดข้อควรระวังเพื่อให้ทีมพัฒนาสามารถปรับแต่งประวัติของโครงการได้อย่างปลอดภัย

ในตอนที่ 1 และ 2 เราเห็นแล้วว่า Merge เชื่อมประวัติสองสายเข้าด้วยกัน และ Squash รวมหลาย commit ให้เหลือ commit เดียว ตอนที่ 3 นี้เราจะพูดถึง Rebase ซึ่งเป็นเครื่องมือที่ทรงพลังที่สุดและต้องระวังที่สุดในสามตัว เพราะมันไม่ได้แค่เชื่อมประวัติ แต่ เขียนประวัติใหม่ (rewrite history)
เมื่ออ่านจบตอนนี้ คุณจะเข้าใจว่า rebase ทำงานอย่างไรในระดับกราฟ commit ทำไม commit hash ถึงเปลี่ยน rebase ต่างจาก merge อย่างไร และที่สำคัญที่สุดคือ กฎความปลอดภัย ว่า branch ไหน rebase ได้และ branch ไหนห้ามแตะ พร้อมวิธี rebase feature branch ให้ทัน main อย่างถูกต้องด้วย --force-with-lease
Rebase คืออะไร — ย้าย commit ไปสร้างใหม่บนฐานใหม่
Rebase หมายถึง:
นำ commits ของ branch ปัจจุบันไปสร้างใหม่บน base ใหม่
สมมติเราอยู่ในสถานการณ์ที่ทั้ง feature และ main เดินหน้าไปคนละทาง:
D---E feature/login
/
A---B---C---F---G main
ขณะที่เราทำ D และ E อยู่บน feature/login ทาง main ก็ได้ F และ G เพิ่มเข้ามา ถ้าอยากให้ feature ของเราตั้งอยู่บนงานล่าสุดของ main โดยไม่สร้าง merge commit เราใช้ rebase โดยยืนอยู่ที่ feature branch:
git switch feature/login
git rebase main
Git จะนำการเปลี่ยนแปลงของ D และ E ไปสร้างเป็น commit ใหม่ต่อท้าย G ทีละตัว:
จากนั้นก็นำ feature เข้า main ได้แบบ fast-forward เพราะตอนนี้ feature ต่อยอดตรงจากปลายของ main พอดี:
git switch main
git merge --ff-only feature/login
ผลลัพธ์:
A---B---C---F---G---D'---E' main
History กลายเป็น เส้นตรง ไม่มีเส้นแตกแขนงและไม่มี merge commit
ทำไม commit เปลี่ยนจาก D เป็น D′
จุดที่หลายคนสับสนคือ ทำไมหลัง rebase แล้ว commit ถึงกลายเป็น D′ และ E′ ไม่ใช่ D และ E ตัวเดิม คำตอบอยู่ที่วิธีที่ Git คำนวณ commit hash
Commit hash คำนวณจากหลายอย่างประกอบกัน เช่น:
- เนื้อหา (snapshot ของไฟล์)
- parent commit
- author
- commit message
- timestamp
เมื่อ rebase ทำให้ parent ของ D เปลี่ยนจาก C ไปเป็น G:
เดิม: parent(D) = C
ใหม่: parent(D') = G
เมื่อ parent เปลี่ยน หนึ่งในวัตถุดิบของ hash ก็เปลี่ยน ดังนั้น hash จึงต้องเปลี่ยนตามไปด้วย:
D ≠ D'
E ≠ E'
นี่คือเหตุผลที่ rebase ถือเป็นการ rewrite history — Git ไม่ได้ย้าย commit เดิมไปวางที่ใหม่ แต่สร้าง commit ชุดใหม่ที่มีเนื้อหาเดียวกันขึ้นมาแทน ส่วน commit เดิม (D, E) จะกลายเป็น commit ที่ไม่มี branch ชี้ถึงและถูกเก็บกวาดออกไปในภายหลัง
Merge vs Rebase
ทั้ง merge และ rebase ต่างก็แก้ปัญหาเดียวกัน คือ "จะนำงานสองสายมารวมกันอย่างไร" แต่ให้กราฟที่ต่างกันโดยสิ้นเชิง
Merge — เก็บกราฟจริง
Merge สร้าง merge commit และคงรูปการแตกแขนงของประวัติไว้:
main ผ่าน merge commit M ที่มี parent สองตัวยังเห็นชัดว่า D และ E เคยเป็น feature branch แยกออกไป แล้วถูกรวมกลับที่ M
Rebase — ทำให้เป็นเส้นตรง
Rebase ย้าย commit ให้ไปเรียงต่อกันเป็นเส้นเดียว ไม่มี merge commit:
ตารางเปรียบเทียบ
| ประเด็น | Merge | Rebase |
|---|---|---|
| เก็บ commit เดิม | ใช่ | ไม่ใช่ |
| Commit hash | ไม่เปลี่ยน | เปลี่ยน |
| History | แตกแขนง | เส้นตรง |
| สร้าง merge commit | อาจสร้าง | ไม่สร้างจาก rebase |
| เหมาะกับ shared branch | ใช่ | ต้องระวัง |
| Conflict | มักแก้ครั้งเดียว | อาจแก้ทีละ commit |
| เห็นประวัติ feature | ชัด | ไม่ชัดเท่า merge |
กฎสำคัญที่สุดของ Rebase
อย่า rebase commits ที่คนอื่นนำไปใช้งานแล้ว เว้นแต่ทีมตกลงกัน
เพราะ rebase สร้าง commit hash ชุดใหม่ทับของเดิม ถ้ามีคนอื่นอ้างอิงหรือแตก branch ต่อจาก commit เดิมอยู่ ประวัติของเขาจะไม่ตรงกับของเราทันที ตัวอย่างที่ ไม่ควรทำ:
git switch main
git rebase some-branch
git push --force
ถ้า main เป็น public/shared branch การทำแบบนี้จะทำให้ประวัติของทุกคนในทีมไม่ตรงกัน และกู้คืนได้ยาก
โดยทั่วไป rebase ได้อย่างปลอดภัยกับ
- feature branch ส่วนตัว
- local commits ที่ยังไม่ push
- branch ที่ยังไม่มีคนอื่นใช้งาน
โดยทั่วไปไม่ควร rebase
mainmasterproduction- shared release branch
- branch ที่เพื่อนแตก branch ต่อไปแล้ว
Rebase feature branch ให้ทัน main
นี่เป็น use case ที่ใช้บ่อยที่สุดของ rebase คือ การอัปเดต feature branch ของเราให้ตั้งอยู่บนงานล่าสุดของ main ก่อนเปิดหรือ merge PR ขั้นตอนคือ อัปเดต main ให้เป็นล่าสุดก่อน แล้วค่อย rebase feature ทับ:
git switch main
git pull
git switch feature/login
git rebase main
เมื่อ rebase เสร็จ commit hash ของ feature เปลี่ยนไปแล้ว ถ้าเคย push branch นี้ขึ้น remote มาก่อน การ push ครั้งนี้จะไม่ใช่ fast-forward จึงต้องเขียนทับ แต่ต้องเขียนทับแบบปลอดภัย:
git push --force-with-lease
ไม่ควรใช้:
git push --force
ความต่างสำคัญมาก: --force-with-lease จะตรวจก่อนว่าบน remote มีงานใหม่ของคนอื่นที่เรายังไม่เห็นหรือไม่ ถ้ามี มันจะปฏิเสธการ push เพื่อไม่ให้เราเผลอเขียนทับงานของคนอื่น ส่วน --force ธรรมดาจะเขียนทับทันทีโดยไม่ตรวจอะไรเลย ซึ่งเสี่ยงทำงานของเพื่อนหายแบบเงียบ ๆ ดังนั้นบน branch ที่เคย push แล้วให้ใช้ --force-with-lease เป็นค่าเริ่มต้นเสมอ
Key takeaways
- Rebase = เขียนประวัติใหม่ — นำ commit ของ branch ปัจจุบันไปสร้างใหม่บนฐานใหม่ ทำให้ history เป็นเส้นตรงและไม่มี merge commit
- commit hash ต้องเปลี่ยน เพราะ parent เปลี่ยน (
C→G) ดังนั้นD ≠ D'เสมอ — commit เดิมไม่ได้ถูกย้าย แต่ถูกสร้างใหม่ - Merge เก็บกราฟจริง, Rebase ทำให้เป็นเส้นตรง — เลือก merge เมื่ออยากเห็นขอบเขต feature, เลือก rebase เมื่ออยากได้ประวัติสะอาดเป็นเส้นตรง
- rebase ได้เฉพาะประวัติส่วนตัว (feature branch ของตัวเอง, local commits) และ อย่า rebase branch สาธารณะ เช่น main, master, production, shared release branch
- หลัง rebase branch ที่เคย push แล้ว ให้ push ด้วย
--force-with-leaseเสมอ ไม่ใช่--forceเพราะมันตรวจงานใหม่ของคนอื่นบน remote ก่อนเขียนทับ