Git Merge, Rebase & Squash — ตอนที่ 6: Workflow ของทีม และตารางตัดสินใจ

On this page
บทคัดย่อ
การกำหนดนโยบายและขั้นตอนการทำงานร่วมกันในระบบควบคุมเวอร์ชันมีความสำคัญอย่างยิ่งต่อการรักษาประวัติของโครงการให้มีความเป็นระเบียบและสามารถกู้คืนได้เมื่อเกิดข้อผิดพลาด การทำงานผ่านระบบการจัดการ Pull Request จะมีทางเลือกในการรวมโค้ดที่สอดคล้องกับคำสั่งพื้นฐาน ได้แก่ การสร้าง merge commit เพื่อเก็บรักษาประวัติการทำงานทั้งหมด การประยุกต์ใช้ squash เพื่อรวบรวมทุกการแก้ไขให้กลายเป็นหนึ่ง commit และกระบวนการ rebase เพื่อสร้างประวัติแบบเส้นตรงซึ่งเป็นการสร้าง commit ขึ้นมาใหม่และส่งผลให้ค่า hash เปลี่ยนแปลงไป นอกจากนี้ ผู้ใช้งานจำเป็นต้องแยกแยะกลไกการย้อนกลับการทำงานให้ชัดเจน โดยคำสั่ง reset จะทำหน้าที่ย้ายตำแหน่งตัวชี้ของ branch ในขณะที่ revert จะสร้าง commit ใหม่เพื่อยกเลิกการเปลี่ยนแปลงเดิมซึ่งเป็นวิธีการที่ปลอดภัยสำหรับ branch ที่ใช้งานร่วมกัน และหากต้องการจัดการกับ merge commit จะต้องระบุสายการพัฒนาหลักผ่านตัวเลือก -m 1 เสมอ สำหรับการดึงเฉพาะบางการแก้ไขสามารถใช้คำสั่ง cherry-pick เพื่อคัดลอก commit ไปยัง branch ปลายทาง และหากเกิดความผิดพลาดก็ยังสามารถกู้คืนข้อมูลที่ดูเหมือนสูญหายไปแล้วได้ผ่านเครื่องมือ reflog รวมถึงควรตั้งค่าพฤติกรรมของคำสั่ง pull ให้เป็นการทำ rebase หรือ fast-forward เพื่อป้องกันความสับสน บทความนี้รวบรวมหลักเกณฑ์และตารางการตัดสินใจในการเลือกใช้คำสั่งต่างๆ เพื่อเป็นแนวทางปฏิบัติให้ทีมพัฒนาสามารถจัดการประวัติการทำงานและผสานโค้ดได้อย่างปลอดภัยและตรงตามนโยบายของโครงการ

นี่คือตอนสุดท้ายของซีรีส์ ห้าตอนก่อนหน้าอธิบายกลไก ตั้งแต่กราฟ commit และ merge ทุกรูปแบบ, squash merge, rebase, interactive rebase ไปจนถึงการจัดการ conflict และ merge strategies ส่วนตอนที่ 6 จะเปลี่ยนทั้งหมดนั้นให้กลายเป็น การตัดสินใจ ว่าในแต่ละสถานการณ์ควรใช้คำสั่งไหน, ปุ่มบน Pull Request ของ GitHub แต่ละปุ่มตรงกับแนวคิดที่เรารู้อยู่แล้วอย่างไร และทีมควรตกลง workflow แบบไหนเพื่อให้ประวัติสะอาดและกู้คืนได้
หลังจบตอนนี้ คุณจะเลือกเครื่องมือที่เหมาะกับทุกสถานการณ์ในชีวิตประจำวันได้จากตารางตัดสินใจเพียงตารางเดียว, รัน workflow แบบ feature branch ที่ปลอดภัยได้ตั้งแต่ commit แรกจนถึง merge PR, แยก reset ออกจาก revert และ rebase ได้, revert merge commit ได้ถูกต้อง และกู้งานที่คิดว่าหายไปแล้วกลับมาด้วย reflog
สามวิธี merge Pull Request บน GitHub
เวลา merge Pull Request บน GitHub เรามักเจอสามปุ่มให้เลือก ซึ่งแต่ละปุ่มตรงกับแนวคิดจากตอนก่อน ๆ
Create a merge commit
เก็บ commits ทั้งหมดจาก branch ไว้ และเพิ่ม merge commit ทับด้านบน ประวัติการพัฒนาทั้งหมดของ PR ยังมองเห็นได้บน target branch
Squash and merge
รวมทั้ง Pull Request ให้เป็น commit เดียวบน target branch — หนึ่ง PR กลายเป็นหนึ่ง commit
Rebase and merge
สร้าง commits ของ PR ขึ้นใหม่ต่อท้าย target branch โดยไม่มี merge commit และเพราะ commits ถูกสร้างใหม่บนฐานใหม่ commit hashes ของ PR บน target branch จะเปลี่ยนเสมอ — GitHub สร้าง commits ใหม่ทุกครั้ง แม้ในกรณีที่ fast-forward ได้ก็ตาม
"Rebase and merge" ให้ทั้ง commits รายตัว และ history เป็นเส้นตรง แต่ต้องจำว่า hash เป็นชุดใหม่ เหมือนตอน rebase ในเครื่องตัวเอง
Cherry-pick เกี่ยวข้องอย่างไร
Cherry-pick ไม่ใช่ merge แต่ควรรู้ไว้ในกล่องเครื่องมือเดียวกัน
git cherry-pick <commit-hash>
ความหมาย:
คัดลอกการเปลี่ยนแปลงของ commit หนึ่งมาสร้างเป็น commit ใหม่บน branch ปัจจุบัน
ก่อน เริ่มจาก:
A---B---C main
\
D---E feature
บน main สั่ง:
git cherry-pick E
หลัง:
สังเกตว่า commit ใหม่คือ E' ซึ่งเป็นสำเนาใหม่ ไม่ใช่ E ตัวเดิม ใช้ cherry-pick เมื่อ:
- ต้องการนำ bug fix หนึ่ง commit ไป production
- backport fix ไปยังอีก release branch
- ย้ายเพียงหนึ่ง commit โดยไม่ต้อง merge ทั้ง branch
ไม่ควรใช้ cherry-pick เพื่อรวม feature branch ทั้งก้อนแทน merge โดยไม่มีเหตุผล เพราะจะเกิด history ซ้ำซ้อน (duplicated history)
Reset ไม่เหมือน Rebase
Reset
Reset คือการย้าย branch pointer
git reset --soft HEAD~1
git reset --mixed HEAD~1
git reset --hard HEAD~1
Rebase
Rebase คือการนำ commits ไป replay บน base ใหม่
git rebase main
ทั้งคู่แก้ history ได้ แต่จุดประสงค์ต่างกัน: reset ปรับว่า branch ชี้ ไปที่ไหน ส่วน rebase สร้างตัว commits ขึ้นใหม่บนจุดเริ่มต้นใหม่
Revert ไม่เหมือน Reset
Revert
Revert คือการสร้าง commit ใหม่ที่ย้อนผลของ commit เก่า
git revert <commit-hash>
เหมาะกับ shared branch เพราะไม่ลบ history
Reset
Reset ย้าย pointer ย้อนกลับ และอาจทำให้ commits หายไปจาก branch
git reset --hard HEAD~1
เหมาะกับ local history ที่ยังไม่ได้แชร์
Revert merge commit
Merge commit มี parent มากกว่าหนึ่งตัว จึงต้องบอก Git ว่า parent ไหนคือ mainline
git revert -m 1 <merge-commit-hash>
ลองนึกถึง three-way merge ที่ M มีสอง parent:
โดยทั่วไป -m 1 หมายถึง "ให้ parent แรกเป็น branch หลัก แล้วลบผลที่มาจาก branch ที่ merge เข้ามา" ควรตรวจ parent ก่อนเสมอ:
git show <merge-commit-hash>
git pull ทำอะไร
โดยทั่วไป:
git pull
เทียบเท่าแนวคิด:
git fetch
git merge
ซึ่งอาจสร้าง merge commit
Pull ด้วย rebase
git pull --rebase
เทียบเท่า:
git fetch
git rebase origin/main
Pull แบบห้ามสร้าง merge commit
git pull --ff-only
แนะนำให้ตั้งพฤติกรรมให้ชัดเจน ดีกว่าปล่อยค่าไว้แบบไม่แน่นอน
ตั้ง pull ให้ rebase เสมอ
git config --global pull.rebase true
ตั้ง pull เป็น fast-forward only
git config --global pull.ff only
เลือกอย่างใดอย่างหนึ่งตาม workflow ของทีม
ตัวอย่าง workflow ที่แนะนำสำหรับ feature branch
ขั้นที่ 1 — อัปเดต main
git switch main
git pull --ff-only
ขั้นที่ 2 — สร้าง feature branch
git switch -c feature/login
ขั้นที่ 3 — ทำงานและ commit
git add .
git commit -m "feat: add login form"
git add .
git commit -m "test: add login validation tests"
ขั้นที่ 4 — ก่อนเปิด PR ให้ update base
git fetch origin
git rebase origin/main
แก้ conflict หากมี:
git add .
git rebase --continue
ขั้นที่ 5 — push
ถ้ายังไม่เคย push:
git push -u origin feature/login
ถ้าเคย push ก่อน rebase:
git push --force-with-lease
ขั้นที่ 6 — Merge PR
เลือกตาม policy ของทีม: Squash and merge, Rebase and merge หรือ Merge commit
วิธีเลือกสำหรับแต่ละสถานการณ์
ใช้ normal merge เมื่อ
- branch ถูกแชร์กับหลายคน
- ต้องการรักษาประวัติจริง
- feature มี commits ที่มีความหมายในตัวเอง
- ต้องการเห็นขอบเขตของ feature branch
- เป็น release branch หรือ integration branch
git merge --no-ff feature/login
ใช้ rebase เมื่อ
- ต้องการอัปเดต feature branch ให้ทัน main
- branch เป็นของคุณเอง
- ต้องการ linear history
- ต้องการจัด commits ก่อนเปิด PR
git switch feature/login
git rebase main
ใช้ squash merge เมื่อ
- feature มี commits แบบ
fix,fix again,oops - ต้องการหนึ่ง PR ต่อหนึ่ง commit
- main ต้องอ่านง่าย
- ไม่จำเป็นต้องเก็บ development history รายย่อย
git switch main
git merge --squash feature/login
git commit -m "feat: add login system"
ใช้ fast-forward only เมื่อ
- ต้องการ linear history
- feature ถูก rebase มาแล้ว
- ต้องการป้องกัน merge commits โดยไม่ตั้งใจ
git merge --ff-only feature/login
ใช้ cherry-pick เมื่อ
- ต้องการเฉพาะบาง commit
- นำ bug fix ไปหลาย release branches
- backport security fix
git cherry-pick abc1234
ตารางตัดสินใจ (Decision table)
สรุปทั้งซีรีส์ให้เหลือตารางเดียว: หาสถานการณ์ของคุณทางซ้าย แล้วใช้วิธีทางขวา
| สถานการณ์ | วิธีที่เหมาะ |
|---|---|
| Branch ส่วนตัวต้องตาม main ให้ทัน | git rebase main |
| Shared branch มีหลายคนทำ | Merge |
| PR มี commits ยุ่งเหยิง | Squash merge |
| ต้องการเก็บทุก commit แต่ history ตรง | Rebase and merge |
| ต้องการเห็น feature boundary | merge --no-ff |
| ต้องการเฉพาะ bug fix หนึ่ง commit | Cherry-pick |
| Production branch ต้องย้อนการเปลี่ยนแปลง | Revert |
| Local commit ล่าสุดผิด | Amend หรือ interactive rebase |
| ไม่อนุญาต merge commit | --ff-only |
Policy ที่เหมาะกับทีมขนาดเล็ก
สำหรับทีมเล็ก แนะนำรูปแบบนี้:
main
└── protected และห้าม force push
feature/*
└── ผู้สร้างสามารถ rebase ได้
Pull Request
└── ใช้ Squash and merge
Workflow:
git switch feature/my-feature
git fetch origin
git rebase origin/main
git push --force-with-lease
จากนั้นใช้ Squash and merge ผลลัพธ์บน main คือหนึ่ง commit สะอาดต่อหนึ่ง PR:
หนึ่ง PR เท่ากับหนึ่ง commit เหมาะกับ SaaS, web application และทีมที่ต้องการ history อ่านง่าย
Policy ที่เหมาะกับโปรเจกต์ที่ต้องเก็บประวัติโดยละเอียด
เมื่อต้องเก็บบันทึกการพัฒนาแบบเต็ม ให้ใช้ branch model แบบ:
main
develop
feature/*
release/*
hotfix/*
แล้ว integrate ด้วย:
git merge --no-ff feature/x
ผลลัพธ์:
- feature boundaries ชัดเจน
- individual commits ยังอยู่
- merge commits แสดง integration point
เหมาะกับระบบขนาดใหญ่, หลาย release lines, งานที่ต้อง audit และกรณีที่ต้องตรวจว่า feature ถูกพัฒนาผ่านขั้นตอนใดบ้าง
ดูกราฟ Git ให้เข้าใจง่าย
ดูกราฟให้ชัดด้วย:
git log --graph --oneline --decorate --all
ตัวอย่างผลลัพธ์:
* a8f9210 (HEAD -> main) Merge feature/login
|\
| * 73ab110 Add validation
| * 19c4d20 Add login form
* | 10fc991 Update dependencies
|/
* 61ad820 Initial commit
แนะนำ alias:
git config --global alias.graph "log --graph --oneline --decorate --all"
จากนั้นใช้:
git graph
Recovery เมื่อทำพลาด
Git มักยังช่วยกู้ได้ด้วย reflog
git reflog
ตัวอย่าง:
a123456 HEAD@{0}: rebase finished
b234567 HEAD@{1}: rebase started
c345678 HEAD@{2}: commit: working version
กู้ branch กลับ:
git branch recovery c345678
หรือ:
git reset --hard c345678
ก่อนใช้ reset --hard ต้องแน่ใจว่าไม่ต้องการ uncommitted changes
สรุปเป็นประโยคเดียวต่อคำสั่ง
- Merge — เชื่อมสองประวัติเข้าด้วยกัน โดยไม่เขียน commits เดิมใหม่
- Rebase — นำ commits ไปสร้างใหม่ต่อจากฐานใหม่ ทำให้ history เป็นเส้นตรง
- Squash — รวมหลาย commits ให้เป็น commit เดียว
- Cherry-pick — คัดลอก commit ที่เลือกมาสร้างใหม่บน branch ปัจจุบัน
- Revert — สร้าง commit ใหม่เพื่อย้อนผลของ commit เก่า
- Reset — ย้าย branch pointer ย้อนกลับ
กฎจำที่สำคัญที่สุด
Merge preserves history. Rebase rewrites history. Squash compresses history.
และ:
Rebase local/private history ได้ แต่หลีกเลี่ยงการ rebase shared public history
สำหรับ workflow ทั่วไปที่ปลอดภัยและสะอาด:
git switch feature/my-feature
git fetch origin
git rebase origin/main
git push --force-with-lease
แล้วใช้ Squash and merge เมื่อ PR ผ่าน review
Key takeaways
- ปุ่ม PR สามปุ่มบน GitHub ตรงกับแนวคิดที่รู้อยู่แล้ว: Create a merge commit (เก็บทุกอย่าง), Squash and merge (หนึ่ง PR หนึ่ง commit), Rebase and merge (เส้นตรง แต่ hash เป็นชุดใหม่)
- แยกสามคำสั่งให้ชัด: reset ย้าย pointer, rebase replay commits บน base ใหม่, revert เพิ่ม commit ใหม่เพื่อย้อน commit เก่า — และ revert คือทางเลือกที่ปลอดภัยบน shared branch
- Revert merge commit ด้วย
git revert -m 1 <hash>และตรวจ parent ด้วยgit showก่อนเสมอ - ทำให้
git pullคาดเดาได้ด้วยpull.rebaseหรือpull.ff onlyแล้วใช้ตารางตัดสินใจเลือกระหว่าง merge / rebase / squash / cherry-pick ตามสถานการณ์ - แทบไม่มีอะไรหายจริง:
git reflogร่วมกับ recovery branch หรือreset --hardช่วยกู้งานหลัง rebase หรือ reset พลาดได้