Git merge กับ rebase: ควรเลือกแบบไหนสำหรับทีมของคุณ
ทั้ง merge และ rebase รวมการเปลี่ยนแปลงของบรานช์เข้าด้วยกัน แต่จัดการประวัติต่างกัน merge เก็บประวัติไว้ตามเดิมและเพิ่มคอมมิต merge ส่วน rebase เขียนคอมมิตของคุณใหม่บนฐานใหม่ ทำให้ประวัติเป็นเส้นตรง หลักปฏิบัติง่าย ๆ คือ ใช้ rebase จัดระเบียบคอมมิตที่ยังไม่ได้ส่งของตัวเอง และใช้ merge เพื่อรวมงานที่เสร็จแล้วเข้าบรานช์ที่ใช้ร่วมกัน
สรุปสั้น ๆ: ความต่างระหว่าง merge และ rebase
การถกเถียง "merge กับ rebase" ไม่ได้อยู่ที่ว่าคำสั่งไหนดีกว่า แต่อยู่ที่ว่าคุณอยากเห็นประวัติโปรเจกต์แบบใด ทั้งสองคำสั่งให้โค้ดสุดท้ายเหมือนกัน สิ่งที่ต่างคือเส้นทางที่ถูกบันทึกไว้ในบันทึก
- Merge รวมสองสายประวัติเข้าด้วยกันและบันทึกตามจริงว่างานดำเนินไปแบบขนาน คอมมิตเดิมไม่เปลี่ยน และมีการเพิ่มคอมมิต merge ที่มีพาเรนต์สองตัว
- Rebase เขียนคอมมิตของคุณใหม่ ราวกับว่าคุณเริ่มงานจากเวอร์ชันล่าสุดของบรานช์ฐาน ประวัติดูเป็นเส้นตรง แต่คอมมิตได้แฮชใหม่
กลไกของแต่ละคำสั่ง ได้แก่ แฟล็ก การแก้ความขัดแย้ง และโหมดโต้ตอบ ได้อธิบายไว้ในบทความแยก ที่นี่พูดถึงเฉพาะการเลือกกลยุทธ์
หน้าตาของประวัติในทั้งสองกรณี
สถานการณ์เริ่มต้น: คุณแยก feature ออกมา ทำสองคอมมิต และระหว่างนั้นมีสองคอมมิตของคนอื่นเข้า main
D---E feature
/
A---B---C---F---G main
หลัง git merge feature ใน main:
D---E
/ \
A---B---C---F---G---M main
หลัง git rebase main ใน feature และเลื่อน main:
A---B---C---F---G---D'---E' main
ในกรณีแรกประวัติแสดงว่าฟีเจอร์ถูกพัฒนาแบบขนานและถูกรวมเมื่อใด กรณีที่สองประวัติอ่านเหมือนเรื่องเล่าตามลำดับ แต่ไม่มีร่องรอยของการทำงานขนานอีกต่อไป เพราะ D' และ E' คือคอมมิตใหม่ที่ถูกสร้างตอน rebase
ตารางเปรียบเทียบ merge กับ rebase
| เกณฑ์ | git merge | git rebase |
|---|---|---|
| คอมมิตที่มีอยู่แล้ว | ไม่เปลี่ยน | ถูกสร้างใหม่พร้อมแฮชใหม่ |
| รูปร่างของประวัติ | กราฟแตกแขนงพร้อมคอมมิต merge | เป็นเส้นตรง |
| ความปลอดภัยสำหรับบรานช์ที่ใช้ร่วมกัน | ปลอดภัยเสมอ | อันตรายกับคอมมิตที่ส่งไปแล้ว |
| ต้องใช้ force push หรือไม่ | ไม่ | ใช่ ถ้าบรานช์อยู่บนเซิร์ฟเวอร์แล้ว |
| ความขัดแย้ง | แก้ครั้งเดียวในคอมมิต merge | อาจโผล่ขึ้นในทุกคอมมิตที่ถูกย้าย |
| บริบท "มาจากไหน" | ถูกเก็บไว้ | หายไป |
| ย้อนฟีเจอร์ทั้งก้อน | revert คอมมิต merge เดียวด้วย git revert -m 1 | revert ทีละคอมมิตหรือทีละช่วง |
ความสะดวกของ git bisect และ git log | อ่านกราฟยากกว่า | ง่ายกว่า มีเส้นตรงเดียว |
| เกณฑ์เริ่มต้น | ต่ำ | สูงกว่า ต้องเข้าใจการเขียนประวัติใหม่ |
| การจัดระเบียบคอมมิต (squash, reword) | มีเฉพาะ --squash ทั้งก้อน | ยืดหยุ่นผ่าน rebase -i |
ข้อดีและข้อเสียของ merge
ข้อดี
- ไม่ทำลายประวัติ. สิ่งที่ถูกเผยแพร่แล้วยังอยู่ที่เดิม เพื่อนร่วมทีมไม่ต้องแก้อะไรหลังการกระทำของคุณ
- แก้ความขัดแย้งครั้งเดียว. แม้บรานช์มี 30 คอมมิต คุณจัดการกับสถานะสุดท้าย ไม่ใช่ทุกขั้นตอน
- รักษาบริบท. จากคอมมิต merge จะเห็นได้ว่างานใดเข้าบรานช์หลักเมื่อใด และย้อนกลับทั้งงานได้ง่าย
ข้อเสีย
- กราฟรก. เมื่อซิงก์บ่อย ประวัติจะเต็มไปด้วยคอมมิต "Merge branch 'main' into feature" และ
git log --graphกลายเป็นแผนผังรถไฟ - คอมมิตร่างเข้าไปในประวัติ. "wip", "fix", "แก้อีกนิด" จะคงอยู่ในบรานช์หลักตลอดไป หากไม่ได้ยุบ
ข้อดีและข้อเสียของ rebase
ข้อดี
- ประวัติเป็นเส้นตรงและอ่านง่าย. คอมมิตแต่ละตัวอยู่ที่ของมัน
git logอ่านเหมือนรายการการเปลี่ยนแปลง และgit bisectหาคอมมิตที่ผิดพลาดได้ง่ายขึ้น - จัดระเบียบคอมมิตได้ ก่อนรีวิว ทั้งการรวม เปลี่ยนชื่อ หรือสลับลำดับ
- ไม่มีคอมมิต merge ทางเทคนิค จากการซิงก์กับบรานช์หลัก
ข้อเสีย
- เขียนประวัติใหม่. การ rebase บรานช์ร่วมที่เผยแพร่แล้วทำให้ทุกคนที่ดึงไปพัง
- ต้องใช้ force push กับบรานช์บนเซิร์ฟเวอร์ จึงต้องมีวินัยและใช้
--force-with-lease - ความขัดแย้งซ้ำ. ส่วนเดียวกันอาจขัดแย้งในหลายคอมมิตติดกัน
- คอมมิตระหว่างทางอาจคอมไพล์ไม่ได้. หลังย้ายไปฐานใหม่ ไม่มีใครทดสอบคอมมิตเก่าในสภาพแวดล้อมใหม่
เมื่อไรควรใช้อะไร
Rebase สำหรับงานท้องถิ่นของคุณ
อัปเดตบรานช์ของคุณก่อนส่ง ด้วย git pull --rebase เตรียมบรานช์สำหรับรีวิวและอยากตัดคอมมิตร่างออก ด้วย git rebase -i บรานช์ฟีเจอร์ส่วนตัวตามหลัง main ด้วย git rebase origin/main ในทุกกรณีเหล่านี้คอมมิตที่ถูกเขียนใหม่เป็นของคุณคนเดียว (หรือเห็นแค่คุณ)
Merge สำหรับบรานช์ร่วมและการรวมงาน
ฟีเจอร์ที่เสร็จแล้วถูกรวมเข้า main หรือ develop บรานช์มีคนทำงานหลายคน ต้องการรวมบรานช์รีลีสกลับเข้าบรานช์หลัก ที่นี่สำคัญที่สุดคือไม่แตะคอมมิตที่เผยแพร่แล้ว และ merge เป็นทางเลือกที่ถูกต้องเพียงทางเดียว
กฎการเลือกง่าย ๆ
ถ้าคอมมิตมีแค่ของคุณ ก็ rebase ได้ ถ้ามีคนดึงไปแล้ว ใช้ได้แค่ merge
รูปแบบทั่วไปสำหรับทีม
| รูปแบบ | วิธีทำงาน | เหมาะกับใคร |
|---|---|---|
| Merge อย่างเดียว | ฟีเจอร์รวมด้วย --no-ff ซิงก์ด้วย git merge main | มือใหม่ใน Git ทีมที่ต้องการลำดับเวลาครบถ้วนและการ revert ง่าย |
| Rebase + merge | บรานช์ถูก rebase บน main ก่อนรวม แล้วมีคอมมิต merge ด้วย --no-ff | ทีมที่ต้องการทั้งประวัติสะอาดและขอบเขตของฟีเจอร์ |
| Rebase + fast-forward | rebase แล้วรวมด้วยการเลื่อน ประวัติเป็นเส้นตรงทั้งหมด | ทีมเล็กที่มีคอมมิตย่อยที่สะอาด |
| Squash merge | แต่ละ pull request กลายเป็นคอมมิตเดียวใน main | ทีมที่มีงานสั้น ไม่ต้องการประวัติรายละเอียดของบรานช์ |
บน GitHub และ GitLab รูปแบบเหล่านี้สอดคล้องกับปุ่ม "Create a merge commit", "Rebase and merge" และ "Squash and merge" ควรเปิดให้ใช้เพียงแบบที่เลือกในการตั้งค่าของรีโพซิทอรี เพื่อไม่ให้ประวัติกลายเป็นผสมกันหลายสไตล์
การตั้งค่าที่ยึดแนวทางที่เลือก
# pull โดยไม่มีคอมมิต merge ที่ไม่จำเป็น
git config --global pull.rebase true
# หรือ: pull เฉพาะการเลื่อน ไม่เช่นนั้นเกิดข้อผิดพลาด ให้คุณตัดสินใจเอง
git config --global pull.ff only
# ให้สร้างคอมมิต merge เสมอเมื่อรวมฟีเจอร์
git config --global merge.ff false
ระวังกับ merge.ff false เพราะมีผลกับ git pull ในโหมด merge ด้วย หลายคนเลือกไม่ตั้งค่านี้แบบส่วนกลาง แต่เขียน --no-ff อย่างชัดเจนเมื่อรวมฟีเจอร์
ตัวอย่าง: งานเดียวในสองสไตล์
ลองดูสถานการณ์จริง คุณทำฟีเจอร์การชำระเงินในบรานช์ feature/payment มาสามวัน ระหว่างนั้นมีงานของคนอื่นห้าชิ้นถูกรวมเข้า main ก่อนส่ง pull request ต้องดึง main ล่าสุดมาและตรวจว่าทุกอย่างทำงานได้
แบบ merge
git switch feature/payment
git fetch origin
git merge origin/main
# แก้ความขัดแย้งครั้งเดียว แล้ว git add, git commit
git push
บรานช์ได้คอมมิต "Merge remote-tracking branch 'origin/main' into feature/payment" การ push ปกติ และเพื่อนร่วมงานที่ดูบรานช์อยู่ไม่รู้สึกถึงอะไรเลย หากซิงก์แบบนี้ทุกวัน จะมีคอมมิต merge สามตัวในบรานช์ และหากรวมโดยไม่ squash คอมมิตเหล่านั้นจะเข้า main พร้อมฟีเจอร์
แบบ rebase
git switch feature/payment
git fetch origin
git rebase origin/main
# แก้ความขัดแย้งระหว่างทาง ทีละคอมมิต: git add, git rebase --continue
git rebase -i origin/main # ตามต้องการ: รวมคอมมิตร่างเข้าด้วยกัน
git push --force-with-lease
บรานช์ดูเหมือนเพิ่งเริ่มต้นเมื่อเช้าจาก main ที่เป็นปัจจุบัน ไม่มีคอมมิตส่วนเกิน และผู้รีวิวอ่านการเปลี่ยนแปลงทีละขั้นได้ง่ายกว่า ราคาที่ต้องจ่ายคือการ force push และต้องแน่ใจว่าไม่มีใครคอมมิตลงบรานช์นี้เพิ่ม
สิ่งที่ main จะเห็นในท้ายที่สุด
เมื่อรวมผ่าน pull request ความต่างยังคงอยู่ แบบ merge จะได้กราฟที่มีแขนงและคอมมิตซิงก์ ส่วนแบบ rebase จะได้สายคอมมิตของฟีเจอร์ที่เรียบร้อย ซึ่งรวมแบบเลื่อนหรือด้วยคอมมิต merge เดียวก็ได้ โค้ดที่ได้จะเหมือนกันถ้าแก้ความขัดแย้งเหมือนกัน
วิธีตัดสินใจในทีม
ตอบคำถามเหล่านี้: มีคนหลายคนทำงานในบรานช์เดียวกันหรือไม่? ถ้ามี rebase สำหรับบรานช์นั้นถูกตัดออก ทีมมีประสบการณ์ Git มากแค่ไหน? หากมีมือใหม่บ่อย เริ่มจาก merge และ squash merge เพราะให้อภัยความผิดพลาด ต้องการประวัติเต็มเพื่อการตรวจสอบและวิเคราะห์เหตุการณ์หรือไม่? ถ้าใช่ คอมมิต merge ของฟีเจอร์มีประโยชน์ ให้ความสำคัญกับ git log ที่อ่านได้และ git bisect บ่อยครั้งหรือไม่? ถ้าใช่ เอนเอียงไปทาง rebase และประวัติเส้นตรง
บันทึกการตัดสินใจไว้ใน CONTRIBUTING หรือ README ของรีโพซิทอรี และเปิดการป้องกันบรานช์หลัก: ห้าม force push ต้องใช้ pull request และกำหนดวิธีรวมที่อนุญาต กฎที่ถูกบังคับด้วยการตั้งค่าได้ผลดีกว่าข้อตกลงด้วยปาก
ความเข้าใจผิดที่พบบ่อย
- "Rebase คือขั้นสูง ส่วน merge สำหรับมือใหม่". ไม่ใช่ เป็นเครื่องมือคนละชนิดสำหรับงานคนละแบบ นักพัฒนาที่มีประสบการณ์ใช้ทั้งสองอย่าง
- "คอมมิต merge คือขยะ". คอมมิตซิงก์มักเป็นสิ่งรบกวน แต่คอมมิต merge ที่รวมฟีเจอร์เป็นจุดสำคัญ เพราะทำให้เห็นและย้อนงานทั้งก้อนได้
- "หลัง rebase จะมีความขัดแย้งน้อยลง". ความขัดแย้งเท่าเดิม เพราะบรรทัดเดิมถูกเปลี่ยน rebase เพียงกระจายความขัดแย้งไปตามคอมมิต ส่วน merge แก้ทั้งหมดในครั้งเดียว
- "Rebase ทำให้คอมมิตหาย". คอมมิตเก่ายังอยู่ใน
git reflogและกู้คืนได้ จนกว่า garbage collection จะลบ
สรุป
การเลือกระหว่าง merge กับ rebase คือการเลือกระหว่างประวัติที่ซื่อตรงตามลำดับเวลากับประวัติเส้นตรงที่อ่านง่าย merge ปลอดภัยและรักษาบริบท ส่วน rebase ให้ความเรียบร้อยแต่เขียนคอมมิตทับ วิธีที่เชื่อถือได้ที่สุดคือผสมกัน: rebase และจัดระเบียบบรานช์ของตัวเองตราบใดที่มันเป็นของคุณ และรวมงานที่เสร็จแล้วเข้าบรานช์ร่วมด้วย merge หรือ squash ตามกฎของทีม สิ่งสำคัญคือตกลงรูปแบบร่วมกันและตรึงไว้ในการตั้งค่าของรีโพซิทอรี ทั้งสองกลยุทธ์และการทำงานกับบรานช์ในทีมถูกฝึกจริงในหลักสูตร Git
คำถามที่พบบ่อย
ควรใช้ merge หรือ rebase สำหรับ git pull?
สำหรับการอัปเดตบรานช์ท้องถิ่น rebase สะดวกกว่า คอมมิตที่ยังไม่ได้ส่งของคุณจะไปอยู่บนคอมมิตรีโมตล่าสุดโดยไม่มีคอมมิต merge เพิ่ม เปิดได้ด้วย git config --global pull.rebase true
ผสมใช้ทั้ง merge และ rebase ในโปรเจกต์เดียวกันได้หรือไม่?
ได้ และเป็นแนวทางที่พบบ่อยที่สุด คือ rebase สำหรับบรานช์ของตัวเอง และ merge สำหรับบรานช์ร่วม สำคัญเพียงว่ากฎต้องเป็นที่รู้จักของทุกคนในทีม
มือใหม่ควรเลือกอะไร?
เริ่มจาก merge เพราะไม่เขียนประวัติทับและทำให้พังได้ยากกว่า ฝึก rebase บนบรานช์ท้องถิ่นของตัวเองเมื่อเข้าใจ reflog และ force push แล้ว
การเลือกกลยุทธ์มีผลต่อโค้ดสุดท้ายหรือไม่?
ไม่ ถ้าแก้ความขัดแย้งเหมือนกัน สถานะไฟล์สุดท้ายจะตรงกัน ต่างกันเพียงประวัติคอมมิตที่ถูกบันทึก
squash merge ต่างจาก rebase อย่างไร?
squash merge ยุบบรานช์ทั้งหมดเป็นคอมมิตใหม่เดียวในบรานช์หลัก ส่วน rebase ย้ายคอมมิตแต่ละตัวทีละตัว ทั้งสองให้ประวัติเส้นตรง แต่ squash ไม่เก็บรายละเอียดของแต่ละขั้น
หัวข้อนี้สอนอย่างละเอียดพร้อมฝึกปฏิบัติในคอร์ส “Git с нуля до уверенного разработчика”
- Git-процессы в реальных командах: какой workflow выбрать
- Продвинутые инструменты: rebase, stash, cherry-pick и теги