git merge: วิธีรวมบรานช์และแก้ความขัดแย้ง
git merge นำประวัติของอีกบรานช์มารวมกับบรานช์ปัจจุบัน หากบรานช์ปัจจุบันไม่ได้เดินหน้าไป Git จะเลื่อนพอยน์เตอร์ (fast-forward) มิฉะนั้นจะสร้างคอมมิต merge ที่มีพาเรนต์สองตัว ย้ายไปยังบรานช์เป้าหมายแล้วรัน git merge ชื่อบรานช์ เมื่อเกิดความขัดแย้งให้แก้ไฟล์ รัน git add และ git commit หรือยกเลิกทั้งหมดด้วย git merge --abort
git merge ทำอะไร
คำสั่ง git merge นำการเปลี่ยนแปลงจากบรานช์ที่ระบุมาใส่ในบรานช์ที่คุณอยู่ในขณะนี้ สิ่งที่ต้องจำคือทิศทาง: การ merge คือการรวม "เข้าตัวเอง" หากต้องการรวม feature/login เข้า main ให้สลับไปที่ main ก่อน แล้วจึงเรียกการ merge
git switch main
git pull # ดึง main ล่าสุดจากเซิร์ฟเวอร์
git merge feature/login
Git ไม่ได้คัดลอกทีละบรรทัดด้วยตนเอง มันหาบรรพบุรุษร่วมของสองบรานช์ (merge base) เปรียบเทียบทั้งสองเวอร์ชันกับบรรพบุรุษนั้น แล้วรวมการเปลี่ยนแปลงเข้าด้วยกัน หากทั้งสองฝั่งแก้ไขคนละที่ ผลลัพธ์จะได้มาโดยอัตโนมัติ หากแก้ไขตรงจุดเดียวกัน จะเกิดความขัดแย้งและคุณต้องเป็นคนตัดสินใจ
สองรูปแบบ: fast-forward และการ merge สามทาง
Fast-forward เมื่อไม่ต้องใช้คอมมิต merge
สมมุติว่าคุณแยก feature จาก main ทำสามคอมมิต และระหว่างนั้นไม่มีใครเพิ่มอะไรใน main ประวัติของ main ก็เป็นเพียงจุดเริ่มต้นของประวัติ feature Git จะเลื่อนพอยน์เตอร์ main ไปยังคอมมิตสุดท้ายของ feature โดยไม่สร้างคอมมิตใหม่
$ git merge feature
Updating 3f2a1c4..9b7e0d2
Fast-forward
src/auth.js | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
ประวัติยังคงเป็นเส้นตรง แต่ข้อเท็จจริงที่ว่า "ที่นี่เคยมีบรานช์ feature" จะหายไป บางครั้งเป็นเรื่องดี บางครั้งไม่ใช่ ดูรายละเอียดในส่วน --no-ff ด้านล่าง
การ merge สามทางและคอมมิต merge
หากทั้งสองบรานช์แยกจากบรรพบุรุษร่วมแล้วเดินหน้าไปคนละทาง จะเลื่อนพอยน์เตอร์ไม่ได้ Git จึงทำ merge สามทาง: ฐาน ฝั่ง "ของเรา" (บรานช์ปัจจุบัน) และฝั่ง "ของเขา" (บรานช์ที่ถูก merge) ผลลัพธ์จะถูกบันทึกเป็นคอมมิต merge แยกต่างหากที่มีพาเรนต์สองตัว
$ git merge feature
Merge made by the 'ort' strategy.
src/auth.js | 42 ++++++++++++++++++++
src/router.js | 3 +-
2 files changed, 44 insertions(+), 1 deletion(-)
Git จะเปิดเอดิเตอร์พร้อมข้อความเช่น Merge branch 'feature' คุณจะคงไว้หรือเพิ่มเหตุผลของการรวมก็ได้ ดู "รอยต่อ" ที่เกิดขึ้นได้ด้วย:
git log --oneline --graph --all
กลยุทธ์ ort เป็นค่าเริ่มต้นตั้งแต่ Git 2.34 ก่อนหน้านั้นคือ recursive สำหรับการใช้งานประจำวันไม่จำเป็นต้องเปลี่ยนกลยุทธ์
แฟล็กที่มีประโยชน์ของ git merge
| แฟล็ก | ทำอะไร | เมื่อไรควรใช้ |
|---|---|---|
--no-ff | สร้างคอมมิต merge เสมอ แม้จะเลื่อนไปข้างหน้าได้ | ต้องการเห็นว่าคอมมิตใดมาจากฟีเจอร์เดียวกันในประวัติ |
--ff-only | รวมโดยการเลื่อนเท่านั้น ไม่เช่นนั้นจบด้วยข้อผิดพลาด | อัปเดตบรานช์ในเครื่องโดยไม่สร้างคอมมิต merge โดยไม่ตั้งใจ |
--squash | นำการเปลี่ยนแปลงทั้งหมดของบรานช์เข้าดัชนี แต่ไม่คอมมิตและไม่เชื่อมกับบรานช์ | ต้องการเปลี่ยนฟีเจอร์เป็นคอมมิตเดียวที่สะอาด |
--no-commit | ทำการ merge แต่หยุดก่อนคอมมิต | ต้องการตรวจผลลัพธ์หรือรันเทสต์ก่อนบันทึก |
-m "ข้อความ" | กำหนดข้อความของคอมมิต merge | การ merge ในสคริปต์หรือโดยไม่มีเอดิเตอร์ |
--abort | ยกเลิก merge ที่เริ่มไว้และคืนสถานะก่อนหน้า | ความขัดแย้งมากเกินไป ต้องการเริ่มใหม่ |
--continue | จบ merge หลังแก้ความขัดแย้ง | ทางเลือกแทน git commit ในขั้นสุดท้าย |
-X ours / -X theirs | เมื่อส่วนที่ขัดแย้ง เลือกเวอร์ชันของเราหรือของเขาโดยอัตโนมัติ | รู้แน่ชัดว่าเวอร์ชันใดถูกต้อง (ระวัง!) |
--no-ff: รักษาขอบเขตของฟีเจอร์
หลายทีมรวมฟีเจอร์ด้วย --no-ff เท่านั้น ในกราฟแต่ละงานจะเห็นเป็น "ฟองอากาศ" และสามารถยกเลิกทั้งงานด้วยคำสั่งเดียว git revert -m 1 <คอมมิต-merge> เมื่อเป็น fast-forward จะไม่มีจุดแบบนี้ และต้องยกเลิกทีละคอมมิต
--squash: หนึ่งคอมมิตแทนสิบ
git switch main
git merge --squash feature/login
git commit -m "ล็อกอินด้วยอีเมลและรหัสผ่าน"
หลัง --squash Git ไม่ถือว่าบรานช์ถูกรวมแล้ว git branch -d feature/login จะปฏิเสธการลบพร้อมคำเตือน "not fully merged" ให้ใช้ git branch -D เมื่อแน่ใจว่าทุกอย่างอยู่ครบ และอย่าทำงานต่อในบรานช์เดิมหลัง squash เพราะการ merge ครั้งถัดไปจะนำการเปลี่ยนแปลงที่รวมไปแล้วกลับมาซ้ำและเกือบแน่นอนว่าจะเกิดความขัดแย้ง
ความขัดแย้งจากการ merge: วิธีแก้ไข
ความขัดแย้งเป็นสถานการณ์การทำงานปกติ ไม่ใช่อุบัติเหตุ หมายความเพียงว่า สองบรานช์เปลี่ยนบรรทัดเดียวกัน (หรือบรานช์หนึ่งลบไฟล์ที่อีกบรานช์แก้ไข) และ Git ไม่กล้าเดาว่าเวอร์ชันใดถูกต้อง
$ git merge feature/login
Auto-merging src/config.js
CONFLICT (content): Merge conflict in src/config.js
Automatic merge failed; fix conflicts and then commit the result.
ขั้นที่ 1 ดูว่าอะไรขัดแย้ง
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/config.js
ขั้นที่ 2 แก้ไขไฟล์
ในไฟล์ที่ขัดแย้ง Git ใส่เครื่องหมายกำกับ:
<<<<<<< HEAD
const TIMEOUT = 5000;
=======
const TIMEOUT = 10000;
>>>>>>> feature/login
ส่วนบนคือบรานช์ปัจจุบัน (HEAD) ส่วนล่างคือบรานช์ที่ถูก merge เลือกเวอร์ชันที่ถูกต้อง (หรือรวมทั้งสองเข้าด้วยกัน) ลบบรรทัดเครื่องหมายทั้งสามออก แล้วบันทึกไฟล์ ข้อผิดพลาดที่พบบ่อยคือลืมเครื่องหมายไว้ตรงกลางไฟล์ ก่อนคอมมิตให้ค้นหาเครื่องหมายเหล่านี้ git diff --check จะไฮไลต์เครื่องหมายที่เหลืออยู่
ขั้นที่ 3 ทำเครื่องหมายว่าแก้แล้วและจบการ merge
git add src/config.js
git commit # หรือ git merge --continue
หากความขัดแย้งทั้งหมดแก้ได้ด้วยการเลือกฝั่งเดียว ไม่จำเป็นต้องแก้ไฟล์ด้วยตนเอง:
git checkout --ours src/config.js # เก็บเวอร์ชันของบรานช์ปัจจุบัน
git checkout --theirs src/config.js # เอาเวอร์ชันของบรานช์ที่ถูก merge
git add src/config.js
สะดวกกว่าในเอดิเตอร์
VS Code, IDE ของ JetBrains และเอดิเตอร์อื่นแสดงความขัดแย้งเป็นสามคอลัมน์ พร้อมปุ่ม "รับปัจจุบัน / รับขาเข้า / รับทั้งสอง" นอกจากนี้ยังเชื่อมเครื่องมือภายนอกผ่าน git mergetool ได้ การตั้งค่าที่มีประโยชน์คือ git config --global merge.conflictStyle zdiff3 ซึ่งเพิ่มเวอร์ชันต้นฉบับจากบรรพบุรุษร่วมในเครื่องหมาย ทำให้เห็นชัดว่าใครเปลี่ยนอะไร
เปลี่ยนใจ ยกเลิกการ merge
git merge --abort
คำสั่งนี้คืนไดเรกทอรีทำงานและดัชนีไปยังสถานะก่อน git merge ดังนั้นก่อนการ merge ควรมีไดเรกทอรีทำงานที่สะอาด ให้คอมมิตหรือพักงานที่ยังไม่เสร็จด้วย git stash มิฉะนั้นจะแยกการแก้ไขเหล่านั้นออกจากผลของการ merge ได้ยาก
วิธียกเลิก merge ที่ทำไปแล้ว
หากคอมมิต merge ยังไม่ได้ส่งขึ้นเซิร์ฟเวอร์ วิธีที่ง่ายที่สุดคือย้อนบรานช์กลับ:
git reset --hard ORIG_HEAD
Git บันทึก ORIG_HEAD ไว้ก่อนการดำเนินการที่มีความเสี่ยง รวมถึงการ merge ซึ่งคือตำแหน่งของบรานช์ก่อน merge แฟล็ก --hard จะทิ้งการเปลี่ยนแปลงที่ยังไม่คอมมิต ดังนั้นตรวจ git status ก่อน
หาก merge อยู่ในรีโพซิทอรีกลางแล้ว จะไม่เขียนประวัติทับ แต่สร้างคอมมิตย้อนกลับ:
git revert -m 1 a1b2c3d
แฟล็ก -m 1 บอกว่า "หลัก" คือพาเรนต์ตัวแรก (บรานช์ที่ถูกรวมเข้าไป) โปรดระวังข้อควรรู้: หลัง revert แบบนี้ Git จะถือว่าคอมมิตของฟีเจอร์รวมแล้ว หากต้องการรวมฟีเจอร์ที่แก้ไขแล้วอีกครั้งในภายหลัง ต้อง revert คอมมิต revert นั้นก่อน
Merge ในการทำงานเป็นทีม
git pull คือ fetch บวก merge
git pull ปกติจะดาวน์โหลดการเปลี่ยนแปลงและ merge บรานช์รีโมตเข้าบรานช์ท้องถิ่น หากคุณและเพื่อนร่วมงานคอมมิตพร้อมกัน จะเกิดคอมมิต "Merge branch 'main' of ..." เพื่อไม่ให้มีการ merge เชิงเทคนิคเช่นนี้ ตั้งค่า git config --global pull.ff only (pull จะล้มเหลวหากทำการเลื่อนไม่ได้ และคุณเป็นผู้ตัดสินใจเอง) หรือใช้ git pull --rebase
Pull request และปุ่ม Merge
บน GitHub และ GitLab การรวมมักเกิดผ่าน pull/merge request ปุ่ม "Create a merge commit" เทียบเท่ากับ git merge --no-ff "Squash and merge" คือการรวมแบบ squash และ "Rebase and merge" คือการ rebase การเลือกโหมดขึ้นกับข้อตกลงของทีม แต่กลไกเหมือนกับในคอนโซล
รวม main เข้าบรานช์ของคุณ
บรานช์ที่อยู่ยาวจะแยกห่างจาก main และความขัดแย้งสะสม ควรดึงบรานช์หลักเข้ามาเป็นประจำ:
git switch feature/login
git fetch origin
git merge origin/main
วิธีนี้แก้ความขัดแย้งทีละเล็กทีละน้อยเมื่อปรากฏ และการ merge สุดท้ายเข้า main จะราบรื่น
ข้อผิดพลาดที่พบบ่อย
- รวมผิดทิศทาง. รัน
git merge mainขณะอยู่ในmainหรือรวมฟีเจอร์เข้าอีกฟีเจอร์ ตรวจบรานช์ปัจจุบันด้วยgit branch --show-currentก่อนการ merge ทุกครั้ง - merge กับบรานช์ที่ล้าสมัย. รวม
mainท้องถิ่นที่ไม่ได้อัปเดตมาเป็นสัปดาห์ ให้git pullก่อน แล้วจึง merge - "Your local changes would be overwritten by merge". ในไดเรกทอรีทำงานมีการแก้ไขที่ยังไม่คอมมิตในไฟล์เดียวกัน ให้คอมมิตหรือเก็บไว้ใน stash
- "refusing to merge unrelated histories". สองบรานช์ไม่มีบรรพบุรุษร่วม เช่น รีโพซิทอรีในเครื่องสร้างด้วย
git initแต่รีโมตมี README อยู่แล้ว หากต้องการรวมจริง ๆ เพิ่ม--allow-unrelated-histories - คอมมิตเครื่องหมายความขัดแย้ง. บิลด์พังที่
<<<<<<<ตรวจด้วยgit diff --checkและรันเทสต์ก่อนคอมมิต
สรุป
git merge เป็นวิธีที่ปลอดภัยในการรวมบรานช์ เพราะไม่เขียนคอมมิตที่มีอยู่ทับ แต่เพิ่มคอมมิตใหม่ (หรือเลื่อนพอยน์เตอร์เท่านั้น) รวมเข้าบรานช์ปัจจุบัน รักษาไดเรกทอรีทำงานให้สะอาด ใช้ --no-ff เมื่อขอบเขตของฟีเจอร์สำคัญ และ --squash เมื่อต้องการคอมมิตเดียว ความขัดแย้งแก้ได้ในสามขั้นตอน คือแก้ไฟล์ git add แล้ว git commit และ git merge --abort จะพากลับไปยังจุดเริ่มต้นเสมอ การรวมบรานช์และการแก้ความขัดแย้งถูกฝึกจริงในหลักสูตร Git
คำถามที่พบบ่อย
git merge ต่างจาก git pull อย่างไร?
git pull ดาวน์โหลดการเปลี่ยนแปลงจากเซิร์ฟเวอร์ (git fetch) แล้ว merge บรานช์รีโมตเข้าบรานช์ปัจจุบันด้วย git merge หรือด้วย rebase หากตั้งค่าไว้ ส่วน git merge ทำงานเฉพาะกับสิ่งที่มีอยู่ในเครื่องแล้ว
รวมหลายบรานช์พร้อมกันได้หรือไม่?
ได้ git merge feature-a feature-b จะทำการ merge แบบ octopus ที่มีพาเรนต์หลายตัว แต่ใช้ได้เฉพาะเมื่อไม่มีความขัดแย้ง ในทางปฏิบัติมักรวมทีละบรานช์
จะดูได้อย่างไรว่าบรานช์ใดถูกรวมเข้าบรานช์ปัจจุบันแล้ว?
git branch --merged แสดงบรานช์ที่รวมครบแล้ว ส่วน git branch --no-merged แสดงบรานช์ที่ยังไม่ได้รวม บรานช์ที่รวมแล้วลบได้อย่างปลอดภัยด้วย git branch -d
จะรู้ล่วงหน้าได้อย่างไรว่าการ merge จะนำอะไรเข้ามา?
รัน git log main..feature เพื่อดูคอมมิตที่ไม่มีใน main และ git diff main...feature เพื่อดูการเปลี่ยนแปลงของบรานช์เทียบกับบรรพบุรุษร่วม
ORIG_HEAD หลัง merge คืออะไร?
เป็นการอ้างอิงที่ Git บันทึกไว้ก่อนการ merge ชี้ไปยังตำแหน่งของบรานช์ก่อน merge หากยังไม่ได้ส่งขึ้นเซิร์ฟเวอร์ สามารถย้อนการ merge ในเครื่องได้ด้วย git reset --hard ORIG_HEAD
หัวข้อนี้สอนอย่างละเอียดพร้อมฝึกปฏิบัติในคอร์ส “Git с нуля до уверенного разработчика”
ไปที่คอร์ส