เปรียบเทียบ

Git merge กับ rebase: ควรเลือกแบบไหนสำหรับทีมของคุณ

อัปเดต: · Skillaria.Top

ทั้ง 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 mergegit rebase
คอมมิตที่มีอยู่แล้วไม่เปลี่ยนถูกสร้างใหม่พร้อมแฮชใหม่
รูปร่างของประวัติกราฟแตกแขนงพร้อมคอมมิต mergeเป็นเส้นตรง
ความปลอดภัยสำหรับบรานช์ที่ใช้ร่วมกันปลอดภัยเสมออันตรายกับคอมมิตที่ส่งไปแล้ว
ต้องใช้ force push หรือไม่ไม่ใช่ ถ้าบรานช์อยู่บนเซิร์ฟเวอร์แล้ว
ความขัดแย้งแก้ครั้งเดียวในคอมมิต mergeอาจโผล่ขึ้นในทุกคอมมิตที่ถูกย้าย
บริบท "มาจากไหน"ถูกเก็บไว้หายไป
ย้อนฟีเจอร์ทั้งก้อนrevert คอมมิต merge เดียวด้วย git revert -m 1revert ทีละคอมมิตหรือทีละช่วง
ความสะดวกของ 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-forwardrebase แล้วรวมด้วยการเลื่อน ประวัติเป็นเส้นตรงทั้งหมดทีมเล็กที่มีคอมมิตย่อยที่สะอาด
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 с нуля до уверенного разработчика”

ไปที่คอร์ส