git rebase: ทำงานอย่างไร โหมดโต้ตอบ และการ squash คอมมิต
git rebase ย้ายคอมมิตของบรานช์ปัจจุบันไปยังฐานใหม่ Git เก็บคอมมิตของคุณไว้ชั่วคราว เลื่อนบรานช์ไปยังจุดปลายของเป้าหมาย แล้วนำคอมมิตกลับมาใช้ใหม่ทีละตัวเป็นคอมมิตใหม่ ทำให้ได้ประวัติเป็นเส้นตรงโดยไม่มีคอมมิต merge ด้วย git rebase -i สามารถรวม (squash) เปลี่ยนชื่อ หรือลบคอมมิตได้ และควรทำเฉพาะกับคอมมิตที่ยังไม่ได้เผยแพร่ให้คนอื่น
git rebase ทำอะไร
git rebase ย้าย "จุดที่แตกกิ่ง" ของบรานช์ของคุณไปยังตำแหน่งอื่นในประวัติ สมมุติว่าคุณแยก feature จาก main เมื่อสัปดาห์ก่อน และ main เดินหน้าไปแล้ว หลัง rebase บรานช์ของคุณจะดูเหมือนเพิ่งเริ่มงานเมื่อสักครู่ จากคอมมิตล่าสุดของ main
git switch feature
git fetch origin
git rebase origin/main
เบื้องหลังมีสามขั้นตอน Git หาบรรพบุรุษร่วมของสองบรานช์ บันทึกการเปลี่ยนแปลงของคอมมิตแต่ละตัวที่คุณทำหลังจากนั้นตามลำดับ เลื่อนบรานช์ไปยัง origin/main แล้วนำการเปลี่ยนแปลงที่บันทึกไว้กลับมาใช้ใหม่ทีละตัว ผลคือคอมมิตใหม่ที่มีเนื้อหาและข้อความเหมือนเดิม แต่แฮชต่างกัน
ก่อน: A---B---C---F---G main
\
D---E feature
หลัง: A---B---C---F---G main
\
D'---E' feature
เครื่องหมาย ' ของ D' และ E' ไม่ใช่แค่การตกแต่ง เป็นอ็อบเจกต์อื่นที่มีแฮชอื่นและพาเรนต์อื่น นี่คือเหตุผลของกฎสำคัญของ rebase ที่จะกล่าวถึงด้านล่าง
กฎทอง: อย่า rebase สิ่งที่เผยแพร่แล้ว
Rebase เขียนประวัติใหม่ ตราบใดที่คอมมิตมีอยู่แค่ในเครื่องของคุณ ก็ปลอดภัย แต่หากเพื่อนร่วมทีมดึงบรานช์นั้นไปแล้ว พวกเขายังมี D และ E เก่า ส่วนคุณมี D' และ E' ใหม่ เมื่อ pull ครั้งถัดไปประวัติจะซ้ำกัน และต้องแก้ความขัดแย้งซ้ำอีกรอบ
หลักปฏิบัติ: ใช้ rebase กับบรานช์ท้องถิ่นของคุณและบรานช์ฟีเจอร์ส่วนตัวที่มีคนเดียวทำงาน บรานช์ร่วม เช่น main, develop หรือบรานช์รีลีส ห้าม rebase เด็ดขาด
วิธีส่งบรานช์หลัง rebase
หากบรานช์เคยอยู่บนเซิร์ฟเวอร์แล้ว git push ปกติจะถูกปฏิเสธ เพราะประวัติในเครื่องไม่ได้ต่อจากประวัติรีโมตอีกต่อไป ต้องใช้ push แบบบังคับ โดยเลือกแบบปลอดภัย:
git push --force-with-lease
ต่างจาก --force ตรงที่แฟล็ก --force-with-lease จะไม่เขียนทับบรานช์ หากมีคอมมิตใหม่บนเซิร์ฟเวอร์ที่คุณยังไม่เคยเห็น ทำให้งานของคนอื่นไม่ถูกลบโดยบังเอิญ
ความขัดแย้งระหว่าง rebase
เนื่องจากคอมมิตถูกนำมาใช้ทีละตัว ความขัดแย้งอาจเกิดขึ้นกับตัวใดก็ได้ และอาจเกิดหลายครั้งติดกัน Git จะหยุดและแจ้งว่าคอมมิตใดนำมาใช้ไม่ได้
$ git rebase origin/main
Auto-merging src/api.js
CONFLICT (content): Merge conflict in src/api.js
error: could not apply 4e1f9a2... เพิ่มแคชของคำขอ
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
ลำดับการทำงาน:
- เปิดไฟล์ที่ขัดแย้ง เลือกเวอร์ชันที่ถูกต้อง และลบเครื่องหมาย
<<<<<<<,=======,>>>>>>> - ทำเครื่องหมายว่าไฟล์แก้แล้ว:
git add src/api.jsไม่ต้องคอมมิต - ดำเนินการต่อ:
git rebase --continueGit จะนำคอมมิตถัดไปมาใช้
คำสั่งอื่นสำหรับควบคุมกระบวนการ:
| คำสั่ง | ทำอะไร |
|---|---|
git rebase --continue | ดำเนินการต่อหลังแก้ความขัดแย้ง |
git rebase --skip | ข้ามคอมมิตปัจจุบันทั้งหมด (การเปลี่ยนแปลงของมันจะหาย) |
git rebase --abort | ยกเลิก rebase และคืนบรานช์ไปยังสถานะเดิม |
ระวังความสับสนเรื่องฝั่ง "ของเรา" และ "ของเขา" ระหว่าง rebase HEAD คือฐานที่คุณย้ายบรานช์ไปวาง และการเปลี่ยนแปลงที่ "เข้ามา" คือคอมมิตของคุณเอง ดังนั้น git checkout --ours ระหว่าง rebase จะเอาเวอร์ชันจาก main และ --theirs คือเวอร์ชันของคุณ ตรงข้ามกับ merge
ความขัดแย้งที่เกิดซ้ำ: rerere
หากคุณ rebase บรานช์ที่อยู่นานเป็นประจำและต้องแก้ความขัดแย้งเดิมซ้ำ ๆ ให้เปิด git config --global rerere.enabled true Git จะจำวิธีแก้ที่คุณเลือก และนำมาใช้เองในครั้งถัดไป
Rebase แบบโต้ตอบ: จัดระเบียบประวัติก่อนเผยแพร่
สิ่งที่มีค่าที่สุดของ rebase คือโหมดโต้ตอบ ทำให้คุณจัดระเบียบคอมมิตของตัวเองก่อนแสดงให้รีวิว เช่น รวม "fix", "fix typo", "แก้อีกนิด" เป็นคอมมิตที่มีความหมายหนึ่งตัว เขียนข้อความใหม่ สลับลำดับ หรือทิ้งสิ่งที่ไม่จำเป็น
git rebase -i HEAD~4 # 4 คอมมิตล่าสุด
git rebase -i origin/main # คอมมิตทั้งหมดของบรานช์ที่ไม่มีใน main
Git จะเปิดเอดิเตอร์พร้อมรายการคอมมิต เรียงจากเก่าไปใหม่ (ต่างจาก git log):
pick 1a2b3c4 เพิ่มฟอร์มเข้าสู่ระบบ
pick 5d6e7f8 fix
pick 9a0b1c2 ตรวจสอบอีเมล
pick 3d4e5f6 พิมพ์ผิดในการตรวจสอบ
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash" but keep only the previous commit's log message
# d, drop = remove commit
แก้คำที่ต้นบรรทัด บันทึกและปิดเอดิเตอร์ Git จะทำตามแผนจากบนลงล่าง
| คำสั่ง | ผล |
|---|---|
pick | เก็บคอมมิตไว้ตามเดิม |
reword | เก็บการเปลี่ยนแปลง แต่เขียนข้อความใหม่ |
edit | หยุดที่คอมมิตเพื่อแก้เนื้อหา (git commit --amend) หรือแยกเป็นหลายคอมมิต |
squash | รวมเข้ากับคอมมิตก่อนหน้า รวมข้อความและแก้ไขได้ |
fixup | รวมเข้ากับคอมมิตก่อนหน้า และทิ้งข้อความของคอมมิตนี้ |
drop | ลบคอมมิตพร้อมการเปลี่ยนแปลง |
การสลับบรรทัดเปลี่ยนลำดับคอมมิต และการลบบรรทัดเท่ากับ drop
Git squash: รวมคอมมิตให้เป็นหนึ่ง
ไม่มีคำสั่ง git squash แยกต่างหากใน Git การ "squash" คอมมิตทำได้สามวิธี และ rebase แบบโต้ตอบยืดหยุ่นที่สุด
วิธีที่ 1 rebase แบบโต้ตอบ
สำหรับตัวอย่างข้างต้น ทำสี่คอมมิตให้เป็นสองคอมมิตที่สะอาด:
pick 1a2b3c4 เพิ่มฟอร์มเข้าสู่ระบบ
fixup 5d6e7f8 fix
pick 9a0b1c2 ตรวจสอบอีเมล
fixup 3d4e5f6 พิมพ์ผิดในการตรวจสอบ
คอมมิตแรกของกลุ่มต้องเป็น pick เสมอ เพราะ squash และ fixup รวมคอมมิตเข้ากับตัวก่อนหน้า และตัวแรกไม่มีตัวก่อนหน้าให้รวม
วิธีที่ 2 คอมมิต fixup และ autosquash
หากรู้ล่วงหน้าว่าคุณกำลังแก้คอมมิตใด สร้างการแก้ไขโดยติดป้ายไว้ทันที:
git commit --fixup=1a2b3c4
git rebase -i --autosquash origin/main
Git จะวางคอมมิต fixup ไว้ใต้คอมมิตที่ถูกต้องและทำเครื่องหมายเป็น fixup ให้เอง เหลือเพียงบันทึกแผน เปิดพฤติกรรมนี้เป็นค่าเริ่มต้นได้ด้วย git config --global rebase.autoSquash true
วิธีที่ 3 รวมทั้งบรานช์เป็นคอมมิตเดียว
git reset --soft $(git merge-base HEAD origin/main)
git commit -m "ล็อกอินด้วยอีเมลพร้อมการตรวจสอบ"
reset --soft ย้ายพอยน์เตอร์ของบรานช์กลับไปยังจุดแตกกิ่ง แต่คงการเปลี่ยนแปลงทั้งหมดไว้ในดัชนี ที่เหลือคือทำคอมมิตหนึ่งครั้ง อีกทางเลือกคือ git merge --squash ตอนรวมเข้าบรานช์หลัก หรือปุ่ม "Squash and merge" ใน pull request
ตัวเลือกที่มีประโยชน์ของ rebase
git pull --rebase
pull ปกติจะรวมบรานช์รีโมตเข้ากับของคุณและสร้างคอมมิต merge เมื่อคุณคอมมิตพร้อมกับเพื่อนร่วมงาน git pull --rebase จะวางคอมมิตท้องถิ่นของคุณไว้ด้านบนของรีโมตแทน ตั้งเป็นค่าเริ่มต้นได้ด้วย git config --global pull.rebase true นี่คือโซนที่ปลอดภัยของ rebase เพราะคอมมิตท้องถิ่นของคุณยังไม่มีใครเห็น
--onto: ย้ายบางส่วนของบรานช์
คุณแตกกิ่ง feature-b จาก feature-a และตอนนี้ต้องการให้ feature-b แยกจาก main โดยตรง ไม่มีคอมมิตของ feature-a:
git rebase --onto main feature-a feature-b
อ่านได้ว่า: เอาคอมมิตของ feature-b ที่ไม่มีใน feature-a แล้วย้ายไปไว้บน main
--exec: ตรวจทุกคอมมิต
git rebase -i --exec "npm test" origin/main
หลังนำแต่ละคอมมิตมาใช้ เทสต์จะรัน หากล้มเหลว rebase จะหยุดที่คอมมิตที่มีปัญหา มีประโยชน์เมื่อสำคัญว่าทุกคอมมิตในประวัติต้องคอมไพล์ได้
--update-refs: กองบรานช์ที่ขึ้นต่อกัน
หากคุณมีห่วงโซ่ของบรานช์ที่ขึ้นต่อกัน แฟล็ก --update-refs (Git 2.38+) จะเลื่อนบรานช์ระดับกลางไปด้วยเมื่อ rebase บรานช์บนสุด
การยกเลิก rebase
ระหว่างที่กระบวนการยังทำงานอยู่ ใช้ git rebase --abort หากเสร็จแล้วและผลลัพธ์ไม่เป็นที่พอใจ คอมมิตเก่าไม่ได้หายไปไหน จำไว้ด้วย reflog
$ git reflog
e5f6a7b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e5f6a7b HEAD@{1}: rebase (pick): ตรวจสอบอีเมล
...
c3d4e5f HEAD@{5}: commit: พิมพ์ผิดในการตรวจสอบ
$ git reset --hard HEAD@{5}
หาบรรทัดใน git reflog ที่อยู่ก่อนเริ่ม rebase แล้วรีเซ็ตบรานช์ไปที่นั่น หลัง rebase ทันทีมีทางลัด: git reset --hard ORIG_HEAD Reflog เก็บในเครื่องและโดยค่าเริ่มต้นเก็บรายการประมาณ 90 วัน จึงไม่ต้องตื่นตระหนกหลัง rebase ที่ผิดพลาด
ข้อผิดพลาดที่พบบ่อย
- Rebase บรานช์ร่วมแล้ว force push. ประวัติของเพื่อนร่วมงานพังไปหมด แก้ด้วยการตกลงกัน กู้บรานช์จาก reflog ของคนที่มีเวอร์ชันที่ถูกต้อง และห้าม force push ไปยังบรานช์ที่ป้องกันไว้บนเซิร์ฟเวอร์
- ใช้
--forceแทน--force-with-lease. คอมมิตที่เพื่อนร่วมงาน push ไปยังบรานช์เดียวกันจะถูกเขียนทับ - Squash ในบรรทัดแรกของแผนแบบโต้ตอบ. Git ตอบด้วยข้อผิดพลาด "cannot 'squash' without a previous commit"
- เริ่ม rebase ทั้งที่ไดเรกทอรีทำงานไม่สะอาด. Git จะไม่เริ่มทำงาน คอมมิตการแก้ไข เก็บใน
git stashหรือใช้git rebase --autostash - แก้ความขัดแย้งแล้วรัน
git commitแทนgit rebase --continue. จะได้คอมมิตส่วนเกิน หลัง--continueลบออกได้ด้วย rebase แบบโต้ตอบอีกครั้ง
สรุป
git rebase ย้ายคอมมิตของคุณไปยังฐานใหม่และสร้างขึ้นมาใหม่ ทำให้ประวัติเป็นเส้นตรงและสะอาด โหมดโต้ตอบเปลี่ยนคอมมิตร่างให้เป็นประวัติที่เรียบร้อย: squash และ fixup รวมการแก้ไข reword แก้ข้อความ drop ลบส่วนเกิน กฎสำคัญมีข้อเดียว คือ rebase เฉพาะสิ่งที่ยังไม่ได้เผยแพร่ให้คนอื่น และหลัง rebase บรานช์รีโมตของคุณให้ส่งด้วย --force-with-lease การทำ rebase และการจัดระเบียบประวัติคอมมิตถูกฝึกจริงในหลักสูตร Git
คำถามที่พบบ่อย
git rebase เปลี่ยนเนื้อหาของโค้ดหรือไม่?
หากไม่มีความขัดแย้ง เนื้อหาไฟล์สุดท้ายจะเหมือนกับการ merge แต่คอมมิตจะถูกสร้างใหม่พร้อมแฮชและพาเรนต์ใหม่ หากตอนแก้ความขัดแย้งเลือกต่างออกไป คอมมิตระหว่างทางจะต่างจากต้นฉบับ
จะ rebase บรานช์ไปยังคอมมิตแรกสุดของรีโพซิทอรีได้อย่างไร?
ใช้ git rebase -i --root แผนจะรวมทุกคอมมิตรวมถึงคอมมิตราก ใช้น้อยมาก เช่น เมื่อต้องการเขียนข้อความของคอมมิตแรกใหม่
rebase เก็บผู้เขียนและวันที่ของคอมมิตไว้หรือไม่?
เก็บผู้เขียนและวันที่ที่เขียน ส่วนผู้คอมมิตและวันที่คอมมิตจะเปลี่ยนเป็นปัจจุบัน แฟล็ก --committer-date-is-author-date ทำให้วันที่คอมมิตเท่ากับวันที่เขียน
rebase คอมมิต merge ได้หรือไม่?
โดยค่าเริ่มต้น rebase ทำให้ประวัติเรียบและทิ้งคอมมิต merge หากต้องการคงโครงสร้างการ merge ใช้ git rebase --rebase-merges
จะรู้ได้อย่างไรว่า rebase กำลังทำงานอยู่?
git status จะแสดงข้อความ interactive rebase in progress หรือ You are currently rebasing และบอกคำสั่งที่ใช้ได้ เชลล์และ IDE หลายตัวก็แสดงสถานะนี้ในพรอมต์ด้วย
หัวข้อนี้สอนอย่างละเอียดพร้อมฝึกปฏิบัติในคอร์ส “Git с нуля до уверенного разработчика”
ไปที่คอร์ส