Git Merge, Rebase & Squash — ตอนที่ 4: Interactive Rebase และ Rebase ขั้นสูง

On this page
บทคัดย่อ
การจัดการประวัติการทำงานให้มีความเป็นระเบียบก่อนการนำเสนอโค้ดมีความสำคัญอย่างยิ่งต่อการรักษาโครงสร้างของ commit ให้สามารถตรวจสอบและทำความเข้าใจได้ง่าย กระบวนการ interactive rebase เป็นเครื่องมือที่ช่วยให้นักพัฒนาสามารถเข้าไปปรับปรุง commit ย่อยได้ทีละรายการผ่านรายการคำสั่งต่างๆ ได้แก่ pick, reword, edit, squash, fixup และ drop รวมถึงสามารถสลับลำดับของการแก้ไขได้อย่างอิสระ นอกจากนี้ยังมีการเพิ่มประสิทธิภาพการทำงานผ่านแฟล็ก --autosquash เพื่อจัดการ commit ที่ต้องการรวมกันแบบอัตโนมัติ สำหรับสถานการณ์ที่มีการแตก branch ซ้อนทับกัน ผู้ใช้งานสามารถย้ายเฉพาะบางส่วนของ branch ไปยังฐานใหม่ได้ด้วยคำสั่ง rebase --onto หรือหากต้องการรักษารูปแบบโครงสร้างการ merge ดั้งเดิมไว้โดยไม่ให้กราฟแบนราบเป็นเส้นตรงก็สามารถประยุกต์ใช้แฟล็ก --rebase-merges ได้ รูปแบบการใช้งานขั้นสูงมักควบคู่ไปกับการทำ fast-forward merge ซึ่งช่วยให้ประวัติการทำงานเป็นเส้นตรงโดยที่ยังคงเก็บรายละเอียดของทุก commit ไว้ได้ หรือการประยุกต์ใช้ควบคู่กับ squash เพื่อรวบรวมประวัติให้เป็น commit เดียวอย่างเป็นขั้นตอนก่อนทำการผสาน เนื่องจากการทำงานเหล่านี้ถือเป็นการเขียนประวัติใหม่ทั้งหมด จึงต้องจำกัดการใช้งานไว้บน branch ส่วนตัวและใช้ --force-with-lease ทุกครั้งที่มีการอัปเดตข้อมูล บทความนี้รวบรวมกลไกเชิงลึกและคำสั่งที่จำเป็นเหล่านี้เพื่อเสริมสร้างความสามารถในการจัดการประวัติของโครงการอย่างเป็นระบบ

ตอนที่ 4 นี้ต่อยอดจาก rebase พื้นฐานในตอนที่ 3 ไปสู่การใช้งานขั้นสูง เริ่มจาก interactive rebase ที่ให้เราจัดระเบียบ commit ทีละตัว (แก้ข้อความ รวม ลบ สลับลำดับ) ต่อด้วย autosquash, rebase --onto สำหรับย้ายเฉพาะบางส่วนของ branch ไปยังฐานใหม่, การพยายามรักษา merge topology ด้วย --rebase-merges และปิดท้ายด้วยสองแพตเทิร์นที่ใช้จริงบ่อยคือ rebase + fast-forward merge และ rebase + squash
เมื่ออ่านจบ คุณจะจัด history ของ feature branch ให้สะอาดก่อนเปิด PR ได้ และเข้าใจว่า rebase ขั้นสูงแต่ละคำสั่งเขียน กราฟ commit ใหม่อย่างไร ทุกอย่างในตอนนี้คือการ rewrite history จึงควรทำบน branch ส่วนตัวและใช้ git push --force-with-lease เมื่อ push ทับของเดิม
Interactive rebase: จัดระเบียบ commit ก่อนเปิด PR
Interactive rebase ใช้จัดระเบียบ commits ก่อนเปิดหรือ merge PR
git rebase -i HEAD~4
Git จะเปิดหน้า todo list ให้แก้ โดยแสดง commits ล่าสุด 4 ตัว:
pick a111111 Add login form
pick b222222 Fix typo
pick c333333 Fix validation
pick d444444 Change button text
เราแก้คำสั่งที่อยู่ด้านหน้าแต่ละ commit เพื่อบอก Git ว่าจะทำอะไรกับ commit นั้น มีคำสั่งหลักที่ต้องรู้ 6 ตัว
pick
เก็บ commit ไว้ตามเดิม ไม่เปลี่ยนอะไร
pick a111111 Add login form
reword
เก็บการเปลี่ยนแปลงไว้ แต่แก้เฉพาะ commit message
reword a111111 Add login form
edit
หยุดที่ commit นั้นเพื่อแก้ไขเนื้อหา (แก้ไฟล์จริง)
edit a111111 Add login form
เมื่อ rebase หยุดที่ commit นั้น ให้แก้ไฟล์แล้วใช้:
git add .
git commit --amend
git rebase --continue
squash
รวม commit นี้เข้ากับ commit ก่อนหน้า และเปิดให้รวมข้อความ commit ของทั้งสองตัวเข้าด้วยกัน
pick a111111 Add login form
squash b222222 Fix typo
squash c333333 Fix validation
ผลลัพธ์คือ commit เดียว
fixup
เหมือน squash แต่ทิ้ง commit message ของ commit ที่ถูก fixup ไป
pick a111111 Add login form
fixup b222222 Fix typo
fixup c333333 Fix validation
ผลลัพธ์จะใช้ message ของ a111111 เท่านั้น
drop
ลบ commit ออกจาก history ทั้งการเปลี่ยนแปลงและ message
drop b222222 Debug logging
สรุปคำสั่ง interactive rebase ทั้ง 6
| คำสั่ง | เก็บการเปลี่ยนแปลง? | เก็บ message? | ใช้เมื่อ |
|---|---|---|---|
pick | เก็บ | เก็บ | คง commit ไว้ตามเดิม |
reword | เก็บ | แก้ข้อความ | ต้องการแก้ commit message อย่างเดียว |
edit | เก็บ (หยุดให้แก้) | เก็บ | ต้องการแก้เนื้อหาไฟล์ใน commit นั้น |
squash | เก็บ (รวมกับตัวก่อนหน้า) | รวมข้อความทั้งสอง | รวม commit เข้ากับตัวก่อนหน้า และเก็บทั้งสอง message |
fixup | เก็บ (รวมกับตัวก่อนหน้า) | ทิ้งข้อความของตัวเอง | รวม commit เข้ากับตัวก่อนหน้า โดยไม่สนใจ message ของมัน |
drop | ไม่เก็บ | ไม่เก็บ | ลบ commit ทิ้งทั้งตัว |
เปลี่ยนลำดับ commit
นอกจากเปลี่ยนคำสั่ง เรายังสลับลำดับบรรทัดใน todo list ได้ด้วย
เดิม:
pick a111111 Add UI
pick b222222 Add tests
pick c333333 Add API
แก้เป็น:
pick c333333 Add API
pick a111111 Add UI
pick b222222 Add tests
Git จะ replay commits ตามลำดับใหม่ที่เราจัด อาจเกิด conflict ได้หาก commits พึ่งพากัน (เช่น commit หลังแก้ไฟล์ที่ commit ก่อนสร้างขึ้น)
Autosquash: fixup อัตโนมัติ
หากรู้ตั้งแต่ตอน commit ว่าต้องการยัด commit ใหม่เข้าไปรวมกับ commit เก่าตัวใดตัวหนึ่ง ให้สร้าง fixup commit ผูกกับ commit เป้าหมายไว้ก่อน
git commit --fixup=<commit-hash>
ตัวอย่าง:
git commit --fixup=a111111
จากนั้นสั่ง rebase พร้อม --autosquash:
git rebase -i --autosquash HEAD~5
Git จะย้าย fixup commit ไปวางไว้หลัง commit เป้าหมายและตั้งคำสั่งในบรรทัดนั้นเป็น fixup ให้อัตโนมัติ เราจึงไม่ต้องเลื่อนบรรทัดในหน้า todo list เอง
workflow ที่ใช้จริง:
git add .
git commit --fixup=a111111
git rebase -i --autosquash main
Rebase --onto: ย้ายเฉพาะบางส่วนของ branch
ใช้เมื่ออยากย้ายเฉพาะบางส่วนของ branch ไปยังฐานใหม่ ไม่ใช่ย้ายทั้งสาย
สมมติมีโครงสร้างนี้ โดย feature-b แตกออกมาจาก feature-a อีกที:
ตอนนี้เราต้องการย้าย feature-b ให้หลุดออกจาก feature-a ไปต่อจาก main โดยตรง
git rebase --onto main feature-a feature-b
ผลลัพธ์ commit G และ H ถูกสร้างใหม่เป็น G' และ H' ต่อท้าย main ส่วน feature-a ยังอยู่ที่เดิม:
รูปแบบทั่วไปของคำสั่ง:
git rebase --onto <new-base> <old-base> <branch>
ความหมาย:
เอา commits ที่อยู่หลัง
old-baseบนbranchไปวางบนnew-base
ในตัวอย่างข้างบน new-base คือ main, old-base คือ feature-a (จุดที่ feature-b แตกออกมา) และ branch คือ feature-b ดังนั้น commits ที่อยู่หลัง feature-a (คือ G และ H) จึงถูกยกไปวางบน main
Rebase ที่มี merge commit: --rebase-merges
rebase ปกติอาจทำให้โครงสร้าง merge เดิมกลายเป็นเส้นตรง เพราะ Git จะ replay เฉพาะ commit ธรรมดาและทิ้ง merge commit ไป
หากต้องการพยายามรักษา merge topology เดิมเอาไว้:
git rebase --rebase-merges main
หรือเขียนย่อ:
git rebase -r main
เหมาะกับ branch ที่มี sub-branches หรือมี merge commits ภายในอยู่แล้ว และเราต้องการให้โครงสร้างการ merge ยังคงรูปเดิมหลัง rebase
Rebase + fast-forward merge
นี่คือแพตเทิร์นที่รวม rebase เข้ากับ fast-forward merge เพื่อให้ได้ history เส้นตรงโดยยังเก็บ commit ย่อยของ feature ไว้ครบ
เริ่มจาก feature branch แตกจาก C ในขณะที่ main เดินหน้าไปถึง G:
D---E
/
A---B---C---F---G main
ทำสองขั้น rebase feature ให้ทัน main ก่อน แล้วค่อย fast-forward:
git switch feature
git rebase main
git switch main
git merge --ff-only feature
ผลลัพธ์เป็นเส้นตรง โดย D และ E ถูกสร้างใหม่เป็น D' และ E' ต่อท้าย G:
วิธีนี้ให้ผลสามอย่างพร้อมกัน:
- เก็บ individual commits ของ feature ไว้ครบ
- History เป็นเส้นตรง อ่านง่าย
- ไม่มี merge commit เพิ่มเข้ามา
Rebase + squash
แพตเทิร์นนี้จัด history ภายใน branch ให้สะอาดด้วย interactive rebase ก่อน แล้วค่อย merge เข้ามา ต่างจาก squash merge ตรงที่เราควบคุมการรวม commit เองบน branch
จัด branch ก่อนด้วย interactive rebase บน main:
git switch feature/login
git rebase -i main
รวม commits ย่อยเข้ากับ commit หลักด้วย fixup:
pick Add login feature
fixup Fix typo
fixup Fix CSS
fixup Fix tests
จากนั้น fast-forward merge เข้า main:
git switch main
git merge --ff-only feature/login
ผลลัพธ์บน main เหลือ commit เดียวคือ S:
ผลลัพธ์คล้าย squash merge แต่ต่างกันที่ branch ถูกจัด history ให้สะอาดก่อนแล้วจึงเลื่อน pointer เข้ามาแบบ fast-forward ไม่ใช่ให้ Git รวมทั้ง branch เป็น commit เดียวตอน merge
Key takeaways
- Interactive rebase (
git rebase -i) ให้เราจัดระเบียบ commit ทีละตัวผ่าน todo list ด้วย 6 คำสั่ง:pick,reword,edit,squash,fixup,dropและสลับลำดับได้ด้วยการย้ายบรรทัด squashรวม message ทั้งสอง ส่วนfixupทิ้ง message ของ commit ที่ถูกรวม ใช้--fixup=<hash>คู่กับ--autosquashเพื่อให้ Git จัดบรรทัด fixup ให้อัตโนมัติgit rebase --onto <new-base> <old-base> <branch>ย้ายเฉพาะ commits ที่อยู่หลัง old-base ไปวางบน new-base เหมาะกับการแยก branch ที่ซ้อนกันออกจากกัน--rebase-merges(-r) พยายามรักษา merge topology เดิมไว้ ไม่ให้ยุบเป็นเส้นตรง- Rebase + ff merge ให้ history เส้นตรงแต่เก็บ commit ย่อย ส่วน rebase + squash จัด branch ให้เหลือ commit เดียวก่อน merge ทั้งคู่เป็น rewrite history จึงใช้กับ branch ส่วนตัวและ
--force-with-lease