Git Merge, Rebase & Squash — ตอนที่ 1: กราฟ Commit และ Merge ทุกรูปแบบ

On this page
บทคัดย่อ
การจัดการประวัติการทำงานในระบบ Git อาศัยความเข้าใจพื้นฐานที่ว่าระบบไม่ได้จัดเก็บโครงการเป็นเวอร์ชันของไฟล์ แต่จัดเก็บในรูปแบบกราฟของ commit ซึ่งคำสั่งจัดการประวัติการทำงานทั้งหมดล้วนเป็นวิธีการปรับเปลี่ยนโครงสร้างกราฟดังกล่าว การดำเนินการ merge เป็นการรวมประวัติของสอง branch เข้าด้วยกันโดยปกติจะไม่เขียน commit ที่มีอยู่เดิมทิ้ง ซึ่งมีรูปแบบการทำงานหลักที่แตกต่างกันอย่างชัดเจน การทำ fast-forward merge จะเลื่อนตัวชี้ branch ไปข้างหน้าโดยไม่สร้าง merge commit ใหม่ ช่วยให้ประวัติเป็นเส้นตรงและค่า hash ไม่เปลี่ยนแปลง แต่ขอบเขตของฟีเจอร์จะหายไปจาก log ในขณะที่การทำ three-way merge จะใช้ commit ที่เป็นบรรพบุรุษร่วมกันรวมกับปลายทางของทั้งสอง branch เพื่อสร้าง merge commit ใหม่ที่มีสอง parent ซึ่งช่วยรักษาประวัติการพัฒนาที่แท้จริงและปลอดภัยสำหรับ branch ที่ใช้งานร่วมกัน แม้จะทำให้กราฟประวัติดูซับซ้อนขึ้นก็ตาม ผู้ใช้งานสามารถควบคุมการทำงานผ่าน --no-ff เพื่อบังคับให้สร้าง merge commit สำหรับรักษาขอบเขตของฟีเจอร์ให้มองเห็นได้ชัดเจน หรือใช้ --ff-only เพื่อปฏิเสธการทำ merge หากไม่สามารถเลื่อนตัวชี้ไปข้างหน้าได้อย่างสมบูรณ์แบบเพื่อรักษาประวัติให้เป็นเส้นตรง บทความนี้สร้างความเข้าใจเกี่ยวกับโมเดลของกราฟและการทำ merge ในรูปแบบต่างๆ เพื่อให้ผู้ใช้งานสามารถอ่านประวัติการแตกสายการทำงาน คาดการณ์การเกิด merge commit และเลือกพฤติกรรมการรวมโค้ดที่เหมาะสมกับความต้องการของทีมได้อย่างแม่นยำ

นี่คือตอนที่ 1 ของซีรีส์ 6 ตอน ว่าด้วยเรื่องที่ว่า Git จัดการประวัติ (history) ใหม่อย่างไร ก่อนจะเปรียบเทียบ Merge, Rebase และ Squash ได้ เราต้องมี mental model หนึ่งเดียวที่ทุกอย่างตั้งอยู่บนนั้น สิ่งสำคัญที่สุดคือ:
Git ไม่ได้เก็บเป็นไฟล์เวอร์ชัน แต่เก็บเป็นกราฟของ commits
ทุกคำสั่งอย่าง Merge, Rebase, Squash คือการจัดการ กราฟประวัติ commit ต่างกัน ในตอนนี้เราจะสร้าง mental model ของ commit graph ก่อน แล้วไล่ดู Merge ทุกรูปแบบ ที่ Git ทำได้ ตั้งแต่ fast-forward merge, three-way merge ไปจนถึงสองแฟล็กที่บังคับพฤติกรรมของ Git คือ --no-ff และ --ff-only เมื่อจบตอนนี้ คุณจะอ่านกราฟประวัติที่แตก branch ได้ บอกได้ว่า merge ครั้งนั้นจะสร้าง merge commit หรือไม่ และเลือกพฤติกรรม merge ที่เหมาะกับทีมได้
Mental model: Commit graph
สมมติเริ่มจาก:
A---B---C main
สร้าง branch ใหม่:
git switch -c feature/login
แล้วมีการพัฒนาพร้อมกันทั้งสอง branch:
feature/login (D–E) แตกออกจาก C ขณะที่ main เดินหน้าไปถึง F–Gตอนนี้ต้องนำ feature/login กลับเข้า main ซึ่งมี 3 วิธีหลัก:
- Merge — เชื่อมประวัติทั้งสองสายเข้าด้วยกัน
- Rebase — ย้าย commits ของ feature ไปต่อท้าย main
- Squash — รวมหลาย commits ให้กลายเป็น commit เดียว
ในตอนนี้เราจะโฟกัสที่วิธีแรกคือ merge ส่วน rebase และ squash มีตอนของตัวเองในซีรีส์ (ดูแถบนำทางด้านบน)
Merge
แนวคิด
Merge หมายถึง:
เอาสถานะและประวัติของสอง branches มารวมกัน โดยปกติไม่เขียน commit เดิมใหม่
คำสั่งพื้นฐาน:
git switch main
git merge feature/login
Merge มีหลายรูปแบบ ซึ่งเราจะไล่ดูทีละแบบในตอนนี้
Fast-forward merge
สถานการณ์
หาก main ไม่ได้เดินหน้าต่อหลังจากสร้าง feature branch:
A---B---C main
\
D---E feature/login
เมื่อใช้:
git switch main
git merge feature/login
Git เพียงเลื่อน pointer ของ main ไปที่ E:
main เลื่อน pointer ไปข้างหน้า — ได้เส้นตรงเดียว ทั้งสอง branch ชี้ที่ Eไม่มี merge commit เพิ่ม เราเรียกสิ่งนี้ว่า fast-forward merge
ข้อดี
- History ตรงและสะอาด
- ไม่มี merge commit ที่ไม่จำเป็น
- ไม่เปลี่ยน commit hash
ข้อเสีย
เมื่อมองจาก history ในภายหลัง อาจไม่เห็นชัดว่า commits D–E เคยเป็น feature branch เดียวกันมาก่อน
บังคับให้ merge ได้เฉพาะ fast-forward
git merge --ff-only feature/login
ถ้าต้องสร้าง merge commit คำสั่งจะหยุดแทนที่จะ merge อัตโนมัติ เหมาะสำหรับป้องกัน history ที่ไม่ตั้งใจ
Three-way merge
สถานการณ์
คราวนี้ทั้ง main และ feature มี commits ใหม่:
D---E feature/login
/
A---B---C---F---G main
เมื่อใช้:
git switch main
git merge feature/login
Git ใช้สามจุดในการคำนวณการรวม:
- commit ร่วมเดิม
C - ปลาย branch
mainคือG - ปลาย branch
featureคือE
แล้วสร้าง merge commit ใหม่ M:
main และ E บน feature branchM มี parent สองตัว:
parent 1 = G
parent 2 = E
นี่คือ three-way merge
ข้อดี
- เก็บประวัติจริงของการพัฒนา
- ไม่เปลี่ยน commit เดิม
- ปลอดภัยสำหรับ shared branches
- เห็นชัดว่า feature ถูก merge เมื่อใด
ข้อเสีย
- History อาจมีเส้นแตกแขนงจำนวนมาก
- มี merge commits เยอะ
- อ่าน
git logยากขึ้นในโครงการใหญ่
Merge แบบ --no-ff
แม้ Git จะสามารถ fast-forward ได้ เราก็สามารถบังคับให้สร้าง merge commit ได้:
git switch main
git merge --no-ff feature/login
ก่อน merge:
A---B---C main
\
D---E feature/login
หลัง merge:
--no-ff: บังคับสร้าง merge commit M แม้ว่าจะ fast-forward ได้เหตุผลที่ใช้ --no-ff
เราต้องการให้ history แสดงว่า:
DและEเป็นส่วนหนึ่งของfeature/login
เหมาะกับทีมที่ใช้แนวทางคล้าย Git Flow:
feature/*
release/*
hotfix/*
ตัวอย่าง:
git merge --no-ff feature/payment
Merge แบบ --ff-only
git merge --ff-only feature/login
ความหมาย:
Merge ได้เฉพาะกรณีที่เลื่อน pointer ไปข้างหน้าได้ โดยไม่สร้าง merge commit
ถ้า branches แยกจากกัน:
D---E
/
A---B---C---F---G
Git จะปฏิเสธ:
fatal: Not possible to fast-forward, aborting.
เหมาะกับทีมที่ต้องการ linear history และไม่ต้องการให้ Git สร้าง merge commit โดยไม่ตั้งใจ
Key takeaways
- Git เก็บประวัติเป็น กราฟของ commits คำสั่ง merge, rebase และ squash คือสามวิธีในการจัดรูปกราฟนั้นให้ต่างกัน
- Fast-forward merge เพียงเลื่อน branch pointer ไปข้างหน้า ไม่มี merge commit, history เป็นเส้นตรงสะอาด, hash ไม่เปลี่ยน แต่ขอบเขตของ feature จะหายไปจาก log
- Three-way merge ใช้ common ancestor บวกกับปลายทั้งสอง branch สร้าง merge commit
Mที่มีสอง parent เก็บประวัติจริงและปลอดภัยกับ shared branch แต่แลกมาด้วย log ที่รกขึ้น - ใช้
--no-ffเพื่อบังคับให้เกิด merge commit และคง feature boundary ให้เห็นชัด (สไตล์ Git Flow) ส่วน--ff-onlyใช้ปฏิเสธ merge ที่ไม่ใช่ fast-forward เพื่อรักษา history ให้เป็นเส้นตรงล้วน