Git Merge, Rebase & Squash — ตอนที่ 5: Conflict, ours/theirs และ Merge Strategies

On this page
บทคัดย่อ
ระบบควบคุมเวอร์ชันมักจะสามารถรวมประวัติการทำงานได้อย่างอัตโนมัติ แต่เมื่อมีการแก้ไขข้อมูลในบรรทัดเดียวกันบน branch ที่แตกต่างกัน Git จะหยุดทำงานและคืนสิทธิ์การตัดสินใจในการจัดการข้อขัดแย้งให้กับผู้ใช้งาน ซึ่งการทำความเข้าใจกลไกเหล่านี้มีความสำคัญอย่างยิ่งต่อการป้องกันความผิดพลาด การผสานโค้ดผ่านการทำ merge มักจะหยุดเพื่อให้แก้ไขข้อขัดแย้งเพียงครั้งเดียว ในขณะที่กระบวนการ rebase จะนำ commit มาทำซ้ำทีละรายการ ส่งผลให้ข้อขัดแย้งเดิมอาจปรากฏขึ้นซ้ำได้หลายรอบ ในการจัดการไฟล์ที่ขัดแย้งกัน ผู้ใช้งานสามารถเลือกเนื้อหาฝ่ายใดฝ่ายหนึ่งผ่านคำสั่ง ours หรือ theirs ได้ ทว่าความหมายของคำสั่งเหล่านี้จะถูกสลับกันอย่างสิ้นเชิงเมื่ออยู่ภายใต้กระบวนการ rebase โดยที่ ours จะหมายถึงฐานของ branch ที่กำลังถูกสร้างขึ้นใหม่แทน นอกจากนี้ ผู้ใช้งานยังต้องทำความเข้าใจความแตกต่างระหว่างวิธีการรวมโค้ดและกลไกการคำนวณเบื้องหลังที่เรียกว่า merge strategy โดยปกติ Git จะใช้กลยุทธ์ ort เป็นค่าเริ่มต้นสำหรับการรวมสอง branch และใช้ octopus สำหรับการรวมหลาย branch ที่ไม่มีข้อขัดแย้ง รวมถึงใช้ subtree สำหรับโครงสร้างที่แตกต่างกัน จุดที่ต้องระมัดระวังเป็นพิเศษคือการแยกแยะระหว่างคำสั่ง -s ours ซึ่งเป็นกลยุทธ์ที่ละทิ้งเนื้อหาจาก branch อื่นอย่างสิ้นเชิง กับการใช้ตัวเลือก -X ours หรือ -X theirs ซึ่งทำหน้าที่ตัดสินข้อขัดแย้งแบบอัตโนมัติแต่เสี่ยงต่อการทำให้โค้ดสำคัญสูญหายไปโดยไม่แจ้งเตือน บทความนี้อธิบายกลไกและกลยุทธ์เบื้องหลังการจัดการข้อขัดแย้งเหล่านี้ เพื่อช่วยให้นักพัฒนาสามารถวินิจฉัยและเลือกใช้วิธีการรวมโค้ดได้อย่างถูกต้องและปลอดภัย

ตอนที่ 1 ถึง 4 เราสมมติว่าทุกอย่างราบรื่น — Git รวมงานให้อัตโนมัติได้ ตอนนี้เราจะพูดถึงกรณีที่ Git ทำเองไม่ได้ เมื่อสอง branches แก้บรรทัดเดียวกัน Git จะหยุดและโยนการตัดสินใจกลับมาให้เรา และวิธีที่ conflict ปรากฏ (รวมถึงวิธีแก้) จะต่างกันระหว่าง merge กับ rebase จากนั้นเราจะเปิดดูกลไกที่ Git ใช้รวม branch: ความต่างระหว่าง merge method (สิ่งที่เราเลือกใน UI) กับ merge strategy (algorithm ที่อยู่เบื้องหลัง) พร้อมกับ strategy และ strategy option ที่บางครั้งต้องรู้ — ort, octopus, -s ours, -X ours/-X theirs และ subtree
เมื่ออ่านจบ คุณจะแก้ conflict ได้ทั้งตอน merge และ rebase ใช้ ours/theirs ได้โดยไม่โดนความหมายที่กลับด้านตอน rebase หลอก และแยกความต่างของสองคำสั่งที่หน้าตาอันตรายคล้ายกันแต่ทำงานต่างกันมากคือ -s ours กับ -X ours
Conflict ตอน Merge
สมมติทั้งสอง branches แก้บรรทัดเดียวกัน เริ่ม merge ตามปกติ:
git switch main
git merge feature/login
Git ตัดสินใจแทนเราไม่ได้ว่าเวอร์ชันไหนควรชนะ จึงแสดง conflict:
CONFLICT (content): Merge conflict in app.js
เหตุผลมองเห็นได้จากกราฟ — main และ feature/login ต่างแก้บรรทัดเดียวกันหลังแยกกันที่ C:
ในไฟล์ Git จะทำเครื่องหมาย conflict ไว้:
<<<<<<< HEAD
const title = "Main version";
=======
const title = "Feature version";
>>>>>>> feature/login
แก้ให้เหลือเวอร์ชันที่ต้องการจริง ๆ:
const title = "Final version";
จากนั้น stage ไฟล์และปิดงาน merge:
git add app.js
git commit
บาง Git version ปิดงานด้วย:
git merge --continue
ยกเลิก merge
ถ้าอยากถอยกลับไปยังจุดก่อนเริ่ม:
git merge --abort
Conflict ตอน Rebase
คราวนี้เริ่ม rebase จากฝั่ง feature branch:
git switch feature/login
git rebase main
เมื่อ conflict ทำให้ rebase หยุด ให้ดูสถานะก่อน:
git status
แก้ไฟล์ จากนั้นไปต่อ:
git add app.js
git rebase --continue
ข้าม commit ปัจจุบัน
git rebase --skip
ใช้เมื่อมั่นใจว่า commit ที่กำลัง replay นั้นไม่จำเป็นแล้ว
ยกเลิก rebase ทั้งหมด
git rebase --abort
กลับไปเหมือนตอนก่อนเริ่ม rebase ทุกประการ
ทำไม rebase อาจ conflict หลายรอบ
นี่คือความต่างสำคัญเชิงความคิด rebase จะ replay commits ทีละ commit ความขัดแย้งพื้นฐานเดียวกันจึงอาจโผล่มาทุก commit ที่ replay ในขณะที่ merge มักให้แก้ conflict แค่รอบเดียว:
ดังนั้น feature branch อายุยาวที่ชนกับการเปลี่ยนแปลงหนัก ๆ บน main อาจทำให้ต้องแก้เรื่องเดิมซ้ำหลายรอบตอน rebase — เป็นสิ่งที่ต้องชั่งน้ำหนักเวลาเลือกระหว่างสองวิธี
ours และ theirs (และทำไม rebase ถึงกลับด้าน)
เมื่อเจอ conflict เราสามารถเลือกฝั่งของไฟล์ทั้งฝั่งได้แทนการแก้มือ ตอน merge:
git checkout --ours app.js
หมายถึงใช้ไฟล์จาก branch ปัจจุบัน ส่วน:
git checkout --theirs app.js
หมายถึงใช้ไฟล์จาก branch ที่กำลัง merge เข้ามา Git รุ่นใหม่ทำแบบเดียวกันได้ผ่าน restore:
git restore --ours app.js
git restore --theirs app.js
จากนั้น stage และไปต่อตามปกติ:
git add app.js
git merge --continue
การกลับด้านตอน Rebase — คำเตือนด้านความปลอดภัย
ตอน rebase ความหมายของ
oursและtheirsจะดูสลับกับ intuition ที่เราสร้างไว้ตอน merge
เพราะ rebase นำ commits ของเราไป replay ต่อบน branch ฐาน Git จึงถือว่าฝั่งฐานคือฝั่งที่กำลังสร้างต่อ:
oursมักหมายถึงฝั่ง base ที่กำลังสร้างใหม่ (branch ที่เรากำลัง rebase ไปต่อ)theirsมักหมายถึง commit ที่กำลัง replay (commit ของ feature เราเอง)
ดังนั้นควรเปิดไฟล์จริงและตรวจเนื้อหาเสมอ ไม่ควรเลือกฝั่งตามชื่อโดยไม่ดูเนื้อหา
Merge strategy กับ Merge method ไม่ใช่สิ่งเดียวกัน
Merge method
สิ่งที่คนส่วนใหญ่เลือกใน UI หรือในระดับบนสุด:
Fast-forward
Merge commit
Squash merge
Rebase and merge
Merge strategy
Algorithm ภายในที่ Git ใช้คำนวณการรวม เลือกด้วย:
git merge -s <strategy>
method คือ อยากให้ history หน้าตาเป็นอย่างไร ส่วน strategy คือ Git คำนวณ tree ที่รวมแล้วอย่างไร เป็นการเลือกคนละอย่าง และส่วนใหญ่เราแค่เลือก method แล้วปล่อยให้ Git เลือก strategy เอง
ort strategy (ค่า default สมัยใหม่)
เป็น strategy หลักสำหรับ merge สอง branches:
git merge -s ort feature/login
ปกติไม่จำเป็นต้องพิมพ์เอง เพราะ Git เลือกให้อัตโนมัติ ort ย่อมาจากแนวคิด "Ostensibly Recursive's Twin" เหมาะกับ:
- two-head merge (รวมหนึ่ง branch เข้าอีก branch)
- rename detection
- merge ทั่วไปในชีวิตประจำวัน
octopus merge: รวมมากกว่าสอง branch
strategy แบบ octopus ใช้ merge มากกว่าสอง branches พร้อมกัน:
git merge feature-a feature-b feature-c
มักใช้รวม branches ที่ไม่มี conflict ถ้ามี conflict ที่ต้องแก้ด้วยมือแม้แต่จุดเดียว octopus จะปฏิเสธการ merge และต้องกลับไป merge แยกทีละ branch แทน
-s ours: เก็บ branch นี้ และบันทึกว่า merge แล้ว
git merge -s ours obsolete-branch
strategy นี้:
- สร้าง merge commit
- บันทึกว่าอีก branch ถูก merge แล้ว
- แต่ทิ้งเนื้อหาของอีก branch ทั้งหมด
- ใช้เนื้อหาของ branch ปัจจุบันเท่านั้น
ต้องไม่สับสนกับ:
git merge -X ours feature
สองอย่างนี้ต่างกันจริง ๆ:
| ประเด็น | -s ours (strategy) |
-X ours (strategy option) |
|---|---|---|
| อะไรถูก merge | ไม่เอาอะไรจากอีก branch เลย — ทิ้งเนื้อหาทั้งหมด | merge ทุกอย่างตามปกติ |
| ผลต่อ conflict | ไม่เกี่ยว — เพราะไม่มีการ merge เนื้อหาจริง | เฉพาะส่วนที่ conflict เท่านั้นที่ถูกตัดสินไปทางฝั่งปัจจุบัน |
| tree ผลลัพธ์ | เหมือน branch ปัจจุบัน แต่มี merge commit บันทึกว่าอีก branch ถูก merge | tree ที่ merge ตามปกติ เอนเอียงไปฝั่งเราเฉพาะจุดที่ชนกัน |
| ใช้ตอนไหน | ทำเครื่องหมายว่า branch ที่เลิกใช้ถูก merge แล้ว โดยไม่เอาการเปลี่ยนแปลงของมัน | แก้ conflict อัตโนมัติไปทางฝั่งเราระหว่าง merge จริง |
-X ours และ -X theirs: ตัวตัดสิน conflict
git merge -X ours feature/login
เมื่อ conflict ในแต่ละส่วน ให้เลือกฝั่ง ปัจจุบัน
git merge -X theirs feature/login
เมื่อ conflict ให้เลือกฝั่งที่ merge เข้ามา สิ่งสำคัญคือ ส่วนที่ ไม่ conflict ยังถูก merge ตามปกติ
ไม่ควรใช้
-X oursหรือ-X theirsโดยไม่ตรวจผล เพราะอาจทิ้ง code สำคัญโดยเงียบ ๆ เพราะ Git ตัดสินทุกส่วนที่ conflict ไปทางเดียวโดยไม่ถาม การเปลี่ยนแปลงจริงในฝั่งที่แพ้จึงอาจหายไปโดยไม่มีการเตือน
subtree merge: รวม repository ที่มี root ต่างกัน
strategy แบบ subtree ใช้รวม repository หรือ directory ที่มีโครงสร้าง root ต่างจากของเรา แนวคิดคือ:
main repository:
src/
docs/
external repository:
library files at the repository root
Git สามารถปรับ path ระหว่าง subtree merge เพื่อให้ tree ที่เข้ามาไปลงในไดเรกทอรีย่อยที่ถูกต้อง ในทางปฏิบัติ ปัจจุบันหลายทีมใช้คำสั่ง git subtree โดยเฉพาะ หรือใช้ package manager แทนการจัดการ strategy นี้ด้วยมือ
Key takeaways
- merge มักให้แก้ conflict รอบเดียว ส่วน rebase replay commits ทีละ commit ทำให้ conflict เดิมโผล่ซ้ำได้ทุก commit — ควบคุมด้วย
--continue,--skipและ--abort ours/theirsตอน merge หมายถึง "branch ปัจจุบัน"/"branch ที่เข้ามา" แต่ rebase กลับด้านมัน จึงต้องเปิดไฟล์จริงตรวจก่อนเลือกฝั่งเสมอ- merge method (fast-forward, merge commit, squash, rebase-and-merge) คือสิ่งที่เราเลือก ส่วน merge strategy (
-s) คือ algorithm เบื้องหลัง —ortเป็น default สมัยใหม่ที่แทบไม่ต้องระบุชื่อ -s oursทิ้งการเปลี่ยนแปลงของอีก branch ทั้งหมดพร้อมบันทึกว่า merge แล้ว ส่วน-X oursmerge ตามปกติและแค่ตัดสินเฉพาะส่วนที่ conflict มาทางฝั่งเรา — เป็นเครื่องมือคนละอย่าง-X ours/-X theirsอาจ ทิ้ง code แบบเงียบ ๆ ห้ามใช้โดยไม่ตรวจผลลัพธ์ที่ merge แล้ว