← All posts

Git Merge, Rebase & Squash — ตอนที่ 2: Squash Merge — หนึ่งฟีเจอร์ หนึ่ง Commit

Data-Sci & Digital HealthBasic InfoTech & Computing Nexus
Git Merge, Rebase & Squash — ตอนที่ 2: Squash Merge — หนึ่งฟีเจอร์ หนึ่ง Commit
On this page

บทคัดย่อ

การรักษาความสะอาดของประวัติการทำงานในระบบควบคุมเวอร์ชันเป็นสิ่งสำคัญสำหรับทีมพัฒนาที่ต้องการรวมการแก้ไขย่อยหลายรายการให้กลายเป็นหนึ่งการเปลี่ยนแปลงที่ชัดเจน แม้ว่าการทำ merge แบบปกติอาจทำให้ประวัติการทำงานบน branch หลักมีความซับซ้อนและมีข้อมูลที่ไม่จำเป็น แต่การทำ squash merge จะช่วยรวบรวมการแก้ไขจากหลาย commit ให้เหลือเพียง commit เดียวที่สมบูรณ์ กลไกนี้ทำงานผ่านคำสั่ง git merge --squash ตามด้วย git commit ซึ่งจะเป็นการรวมการเปลี่ยนแปลงทั้งหมดจาก branch ต้นทางมาเตรียมไว้โดยไม่สร้าง commit หรือเชื่อมโยงประวัติสายบรรพบุรุษโดยอัตโนมัติ ส่งผลให้ผลลัพธ์ที่ได้คือ commit ใหม่เอี่ยมที่มีค่า hash เฉพาะตัวและมีเพียง parent เดียว เนื่องจาก branch ต้นทางยังคงประวัติการทำงานที่เป็นอิสระและไม่ถูกรับรู้ว่าผสานรวมแล้วผ่านกลไกตรวจสอบสายบรรพบุรุษเช่นคำสั่ง git branch --merged ผู้ใช้งานจึงต้องบังคับลบ branch ต้นทางทิ้งด้วยคำสั่ง git branch -D การดำเนินการดังกล่าวมีความแตกต่างจากการทำ merge แบบ three-way ทั่วไปที่จะเก็บประวัติการพัฒนาไว้ทั้งหมด และแตกต่างจากการทำ interactive rebase โดยใช้คำสั่ง squash ซึ่งเป็นการเขียนประวัติใหม่ภายใน branch ตัวเอง บทความนี้อธิบายกลไกพื้นฐานของการรวมการทำงานในรูปแบบดังกล่าว เพื่อช่วยให้นักพัฒนาสามารถจัดการประวัติของโครงการได้อย่างตั้งใจพร้อมทั้งเข้าใจข้อจำกัดด้านความสัมพันธ์ของสายบรรพบุรุษได้อย่างถูกต้อง


อินโฟกราฟิกสรุปเนื้อหา Git Merge, Rebase & Squash — ตอนที่ 2: Squash Merge — หนึ่งฟีเจอร์ หนึ่ง Commit
Visual summary · ภาพสรุป

ในตอนที่ 1 เราวางแผนผัง merge ทุกแบบลงบนกราฟ commit ไปแล้ว — fast-forward, three-way, --no-ff และ --ff-only ตอนนี้เราจะโฟกัสอีกวิธีหนึ่งในการนำ branch กลับเข้า main นั่นคือ squash merge ซึ่งเป็นแนวคิดเบื้องหลังปุ่ม Squash and merge ยอดนิยมบน GitHub แนวคิดฟังดูง่ายมาก — เอา feature branch ที่มี commit ย่อยจำนวนมากมารวมเป็น commit เดียวสะอาด ๆ บน main

เมื่ออ่านจบ คุณจะสามารถทำ squash merge ในเครื่องได้ อธิบายได้ว่า Git บันทึกอะไร — และที่สำคัญไม่แพ้กันคือ ไม่ได้บันทึก อะไร — แยกความหมายของคำว่า "squash" สองแบบออกจากกัน และมองออกทันทีว่า squash merge ต่างจาก merge commit ธรรมดาอย่างไร

Squash merge คืออะไร

Squash merge ตอบความต้องการที่ตรงใจนักพัฒนามาก นั่นคือ หนึ่งฟีเจอร์ หนึ่ง commit

Squash คือ การนำการเปลี่ยนแปลงจากหลาย commits มารวมเป็น commit เดียว

สมมติ branch feature/login โตขึ้นจนมีสี่ commits ซึ่งหลายอันเป็นแค่การแก้เล็ก ๆ น้อย ๆ:

D  create login form
E  fix typo
F  fix login again
G  final fix

E, F และ G ไม่มีอันไหนเป็นหมุดหมายที่ใครต้องเห็นค้างอยู่บน main ตลอดไป squash merge ช่วยให้เราเก็บ การเปลี่ยนแปลง ทั้งหมดไว้ ในขณะที่ทิ้ง history ที่รกออกไป

ลองทำจริง: จากสี่ commits เหลือ commit เดียว

เริ่มจากอยู่บน branch ที่ต้องการ merge เข้า แล้วสั่ง squash จากนั้นค่อย commit:

git switch main
git merge --squash feature/login
git commit -m "Add login feature"

หัวใจอยู่ที่ flag --squash ต่างจาก git merge ปกติตรงที่มัน ไม่ สร้าง merge commit และ ไม่ เลื่อน main ให้เอง แต่จะ stage การเปลี่ยนแปลงรวมของทั้ง branch ไว้แล้วหยุด รอให้เราเขียน commit message เพียงข้อความเดียว ก่อน squash กราฟเป็นแบบนี้:

ABCDEFGfeature/loginmain
ก่อน squash merge: feature/login มี D–E–F–G แตกออกจาก C บน main

และหลังจากสั่งสามคำสั่งข้างต้น:

ABCSmain= D+E+F+G
หลัง squash merge: commit ใหม่ S ตัวเดียวเก็บผลรวมของ D+E+F+G

commit ใหม่ S เก็บ ผลลัพธ์รวมของ D + E + F + G — ได้ไฟล์สุดท้ายเหมือนกับที่ merge ปกติจะให้ — แต่เป็น commit เดียวสะอาด ๆ บน main

สิ่งที่ Squash merge ไม่ได้ทำ

ตรงนี้แหละที่ทำให้หลายคนงง S ดูเหมือนจบเรื่องแล้ว แต่จริง ๆ Git บันทึกไว้น้อยกว่า merge ปกติมาก:

สองข้อหลังนี้เมื่อมารวมกันมีผลในทางปฏิบัติที่ควรพูดให้ชัด เพราะเกือบทุกคนจะสะดุดกับมันในครั้งแรก

ทำไม Git จึงไม่ถือว่า branch ถูก merge

เพราะ S ไม่มีปลาย feature branch เป็น parent จึงไม่มีเส้น ancestry เชื่อมจาก main กลับเข้า feature/login

เครื่องมือ "branch นี้ merge แล้วหรือยัง" ของ Git ทำงานด้วย ancestry ล้วน ๆ — มันถามว่าปลาย branch นั้น "ไปถึงได้" จาก commit ปัจจุบันหรือไม่ หลัง squash merge คำตอบคือไปไม่ถึง ดังนั้น:

git branch --merged          # จะไม่มี feature/login อยู่ในรายการ
git branch -d feature/login  # ถูกปฏิเสธ: "not fully merged"

ถ้าจะลบ branch จริง ๆ ต้องบังคับด้วย git branch -D feature/login เพื่อยืนยันว่าเราไม่ต้องการ commits เดิมเหล่านั้นแล้ว

ฝั่ง GitHub เลี่ยงความสับสนนี้ เมื่อกดปุ่ม Squash and merge GitHub จะบันทึกว่า pull request ถูก merge แล้วในฐานะข้อเท็จจริงของ PR โดยตรง — ไม่ใช่การอนุมานจากกราฟ commit — ดังนั้น PR จะขึ้นสถานะ Merged และ GitHub จะเสนอให้ลบ branch ให้ ทั้งที่ ancestry ของ Git ยังดู "unmerged" อยู่ บทเรียนคือ หลัง squash merge ให้เชื่อสถานะของ PR ไม่ใช่ git branch --merged ในการบอกว่างานเข้าเรียบร้อยแล้วหรือยัง

ข้อดีและข้อเสีย

ข้อดี

ข้อเสีย

ทำในเครื่อง เทียบกับปุ่มบน GitHub

ในเครื่อง ลำดับเต็ม ๆ รวมถึงการดูว่ามีอะไรถูก stage ไว้บ้าง เป็นแบบนี้:

git switch main
git merge --squash feature/login
git status
git commit -m "feat: add login system"

ขั้น git status ควรเก็บไว้ เพราะช่วยให้เรายืนยันได้ว่ามีการเปลี่ยนแปลงใดถูก stage บ้างก่อนจะปิดผนึกลงใน S

ปุ่ม Squash and merge บน GitHub ทำสิ่งเดียวกันในระดับ server:

ทั้ง Pull Request → หนึ่ง commit บน target branch

ทุกอย่างข้างบน — hash ใหม่, parent เดียว, branch ที่ดู unmerged ตาม ancestry — ใช้ได้กับปุ่มนี้เช่นกัน

Squash มีสองแบบที่ต้องแยกกัน

คำว่า "squash" คำเดียวทำงานสองอย่างที่ต่างกัน ต้องแยกให้ออก

แบบที่ 1 — Squash merge

ใช้ตอนนำทั้ง branch เข้าสู่ main:

git merge --squash feature/login
git commit

คือทุกอย่างที่เล่ามาข้างบน

แบบที่ 2 — Interactive rebase squash

ใช้จัดระเบียบ commits ภายใน branch เดียวกัน ก่อนที่มันจะไปถึง main:

git rebase -i HEAD~4

ในหน้าต่าง editor เราจะทำเครื่องหมายว่า commit ไหนควรถูกยุบรวมเข้ากับ commit ก่อนหน้า:

pick    Add login
squash  Fix typo
squash  Fix validation

(ตอนที่ 4 จะเล่า interactive rebase แบบเต็ม) ทั้งสองแบบทำให้จำนวน commit น้อยลง แต่กลไกต่างกัน — squash merge ยุบ branch ตอนที่มันไปลงบนอีก branch ส่วน interactive rebase squash เขียน history ของ branch ตัวเองใหม่ในที่เดิม

Merge commit เทียบกับ Squash merge

ลองวางผลลัพธ์สองแบบไว้ข้างกัน สมมติ feature branch มีสาม commits:

D---E---F

Merge commit

merge ปกติ (three-way) เก็บทุก commit ไว้ และเพิ่ม merge commit M ที่ผูกสองประวัติเข้าด้วยกัน:

ABCDEFMmain
Merge commit: M ผูกสองประวัติเข้าด้วยกัน — D, E และ F ยังไปถึงได้จาก main

จาก main เรายังเดินย้อนกลับเข้าไปหา D, E, F ได้ ประวัติการพัฒนายังอยู่ครบ

Squash merge

squash merge ทิ้งโครงสร้างนั้นไป เหลือ commit เดียว:

ABCSmain= D+E+F
Squash merge: จาก main เห็นแค่ S — commit ย่อยหายไปจากสายประวัตินี้

จาก main เราเห็นแค่ S ส่วน commits D, E, F หายไปจากสายประวัตินี้ — เหลืออยู่แค่บน feature branch จนกว่าจะลบทิ้ง

ไฟล์สุดท้ายเหมือนกัน แต่ประวัติต่างกันมาก การเลือกระหว่างสองแบบนี้จริง ๆ แล้วคือการเลือกว่า อยากให้ log ของ main จดจำอะไรไว้ — คำถามที่ตอนที่ 6 จะทำให้กลายเป็นตารางตัดสินใจเต็มรูปแบบ

Key takeaways

0
ถึงนักอ่านชาวไทยและต่างชาติทำความเข้าใจบริบททางการแพทย์ของผมอ่านต่อ →ถึงนักอ่านชาวไทยและต่างชาติทำความเข้าใจเนื้อหาของผมที่นอกเหนือจากการแพทย์อ่านต่อ →

ความคิดเห็น

ยังไม่มีความคิดเห็น มาเป็นคนแรกกันเลย

เข้าสู่ระบบเพื่อแสดงความคิดเห็น