git reset กับ git revert: วิธียกเลิกคอมมิตโดยไม่ทำลายประวัติของทีม
git reset เลื่อนพอยน์เตอร์ของบรานช์ไปยังคอมมิตอื่นและเขียนประวัติทับ จึงเหมาะกับคอมมิตท้องถิ่นที่ยังไม่ได้ส่งเท่านั้น git revert สร้างคอมมิตใหม่ที่มีการเปลี่ยนแปลงย้อนกลับ โดยไม่แตะประวัติเดิม จึงเป็นวิธียกเลิกคอมมิตที่มีอยู่บนเซิร์ฟเวอร์อย่างปลอดภัย
โดยสรุป: git reset ย้อนบรานช์กลับและทำให้เหมือนว่าไม่เคยมีคอมมิตส่วนเกิน ส่วน git revert คงประวัติไว้ตามเดิม และเพิ่มคอมมิตใหม่ที่ยกเลิกการเปลี่ยนแปลงของคอมมิตเก่า อย่างแรกเหมาะกับงานในเครื่อง อย่างหลังเหมาะกับทุกอย่างที่ถูกส่งเข้ารีโพซิทอรีกลางแล้ว หากจำกฎข้อนี้ได้เพียงข้อเดียว ก็จะหลีกเลี่ยงอุบัติเหตุส่วนใหญ่กับ Git ได้
ด้านล่างอธิบายการทำงานของทั้งสองคำสั่ง ความต่างของโหมด reset วิธียกเลิกคอมมิตหลัง push วิธีกู้คอมมิตที่ "หายไป" และ git clean
สามระดับที่เปลี่ยนคำสั่งยกเลิก
เพื่อเข้าใจความต่าง นึกถึงสามพื้นที่ของ Git: ไดเรกทอรีทำงาน (ไฟล์บนดิสก์) ดัชนี (staging area สิ่งที่จะเข้าคอมมิตถัดไป) และ ประวัติ (คอมมิตที่บรานช์และ HEAD ชี้อยู่) คำสั่งยกเลิกแต่ละตัวทำงานกับชุดพื้นที่ที่ต่างกัน
| คำสั่ง | ประวัติ | ดัชนี | ไดเรกทอรีทำงาน | ปลอดภัยกับบรานช์ร่วม |
|---|---|---|---|---|
git restore ไฟล์ | ไม่ | ไม่ | ใช่ | ใช่ |
git restore --staged ไฟล์ | ไม่ | ใช่ | ไม่ | ใช่ |
git reset --soft | ใช่ | ไม่ | ไม่ | ไม่ |
git reset --mixed (ค่าเริ่มต้น) | ใช่ | ใช่ | ไม่ | ไม่ |
git reset --hard | ใช่ | ใช่ | ใช่ | ไม่ |
git revert | เพิ่มคอมมิต | — | — | ใช่ |
git clean | ไม่ | ไม่ | ลบไฟล์ที่ไม่ถูกติดตาม | ใช่ แต่ย้อนกลับไม่ได้ |
git reset ทำงานอย่างไร
คำสั่ง git reset <คอมมิต> ย้ายพอยน์เตอร์ของบรานช์ปัจจุบัน (และ HEAD ไปด้วย) ไปยังคอมมิตที่ระบุ คอมมิตที่อยู่ "หลัง" จุดนั้นไม่ได้ถูกลบออกจากดิสก์ แต่จะไม่มองเห็นจากบรานช์ อีกต่อไป โหมดกำหนดว่าจะเกิดอะไรกับดัชนีและไฟล์
git reset --soft: ยกเลิกคอมมิตแต่คงการเปลี่ยนแปลงไว้ในดัชนี
บรานช์เลื่อนไป ส่วนดัชนีและไฟล์คงเดิม การเปลี่ยนแปลงจากคอมมิตที่ถูกยกเลิกทั้งหมดกลายเป็นสิ่งที่เตรียมไว้สำหรับคอมมิต นี่คือวิธีที่ดีที่สุดในการเขียนข้อความใหม่หรือรวมคอมมิตล่าสุดหลายตัวให้เป็นหนึ่งเดียว
git reset --soft HEAD~3
git commit -m "feat: ฟอร์มสมัครสมาชิก"
สามคอมมิตล่าสุดกลายเป็นหนึ่งคอมมิต เนื้อหาของไฟล์ไม่เปลี่ยน
git reset --mixed: ยกเลิกคอมมิตและดัชนี
โหมดค่าเริ่มต้น: git reset HEAD~1 เทียบเท่ากับ git reset --mixed HEAD~1 บรานช์เลื่อน ดัชนีถูกรีเซ็ต และการเปลี่ยนแปลงยังอยู่ในไดเรกทอรีทำงานในฐานะที่ยังไม่ได้เตรียม เหมาะเมื่อคุณคอมมิตสิ่งที่ไม่ควรคอมมิต และอยากตัดสินใจใหม่ว่าจะเพิ่มอะไรและเป็นชุดไหน
git reset HEAD~1
git status
# Changes not staged for commit:
# modified: app.py
# modified: config.yml
git reset --hard: ย้อนกลับทั้งหมด
บรานช์ ดัชนี และไดเรกทอรีทำงานถูกปรับให้ตรงกับคอมมิตที่ระบุ การเปลี่ยนแปลงที่ยังไม่คอมมิตในไฟล์ที่ถูกติดตามจะหายไปโดยไม่มีคำเตือน นี่เป็นโหมดเดียวของ reset ที่ทำลายงานของคุณได้จริง
git reset --hard HEAD~1 # ทิ้งคอมมิตล่าสุดและการแก้ไขทั้งหมด
git reset --hard origin/main # ทำให้บรานช์ตรงกับเซิร์ฟเวอร์
ก่อน --hard ควรรัน git status และ git stash หากมีสิ่งที่มีค่า สิ่งที่คอมมิตแล้วกู้คืนได้ (ดูด้านล่าง) แต่สิ่งที่ยังไม่คอมมิตกู้ไม่ได้
git reset สำหรับไฟล์เดียว
เมื่อระบุพาธ reset จะไม่เลื่อนบรานช์ แต่แก้ดัชนีเท่านั้น: git reset app.py นำไฟล์ออกจากรายการที่เตรียมไว้ รูปแบบปัจจุบันที่เทียบเท่าคือ git restore --staged app.py ซึ่งอ่านง่ายกว่า แฟล็ก --soft และ --hard ใช้กับพาธไม่ได้
git revert ทำงานอย่างไร
git revert <คอมมิต> คำนวณการเปลี่ยนแปลงของคอมมิตที่ระบุ นำไปใช้แบบย้อนกลับ และสร้างคอมมิตใหม่ บรรทัดที่ถูกเพิ่มจะถูกลบ และบรรทัดที่ถูกลบจะถูกนำกลับมา ประวัติเติบโตไปข้างหน้า ไม่มีการเขียนทับอะไร ดังนั้นเพื่อนร่วมทีมไม่ต้องแก้อะไรในเครื่องของตน
git log --oneline
# a3f9c21 (HEAD -> main) docs: README
# 7be41d0 feat: แคชการตอบกลับ
# 1c02e8f fix: หมดเวลาของคำขอ
git revert 7be41d0
# [main 5d8e2aa] Revert "feat: แคชการตอบกลับ"
# 2 files changed, 3 insertions(+), 41 deletions(-)
Git จะเปิดเอดิเตอร์พร้อมข้อความเช่น Revert "..." และบรรทัด This reverts commit 7be41d0... แนะนำให้เพิ่มเหตุผลว่าทำไมจึงยกเลิกการเปลี่ยนแปลงนี้
แฟล็กที่มีประโยชน์ของ revert
--no-editใช้ข้อความมาตรฐานโดยไม่เปิดเอดิเตอร์-n(--no-commit) นำการย้อนกลับไปไว้ในดัชนีโดยไม่สร้างคอมมิต ใช้ยกเลิกหลายคอมมิตด้วยคอมมิตเดียวgit revert A..Bยกเลิกช่วงของคอมมิต (แต่ละตัวเป็น revert คอมมิตแยก จากใหม่ไปเก่า)-m 1จำเป็นสำหรับคอมมิต merge ระบุว่าพาเรนต์ใดคือสายหลัก
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert: ย้อนสามคอมมิตของการทดลอง"
Revert คอมมิต merge
การ merge มีพาเรนต์สองตัว และ Git ไม่รู้ว่าจะคำนวณการเปลี่ยนแปลงย้อนกลับเทียบกับตัวไหน จึงแสดงข้อผิดพลาด is a merge but no -m option was given หากไม่ใส่ -m โดยทั่วไปบรานช์หลักคือพาเรนต์ตัวแรก:
git revert -m 1 9f1e2ab
ข้อควรระวัง: หากภายหลังต้องการ merge บรานช์เดิมอีกครั้ง Git จะถือว่าคอมมิตของบรานช์นั้น merge ไปแล้วและจะไม่นำการเปลี่ยนแปลงมาให้ ทางแก้คือ revert คอมมิต revert นั้นก่อน แล้วจึงเพิ่มการแก้ไขใหม่
git reset กับ git revert: เลือกอันไหน
| เกณฑ์ | git reset | git revert |
|---|---|---|
| การทำกับประวัติ | เขียนทับ (คอมมิตหายไปจากบรานช์) | เพิ่มคอมมิตใหม่ |
| คอมมิตถูก push แล้ว | ต้องใช้ push --force ซึ่งทำลายงานของเพื่อนร่วมทีม | git push ปกติ |
| ยกเลิกคอมมิตจากกลางประวัติ | ได้เฉพาะพร้อมกับคอมมิตทั้งหมดที่ตามมา | ทำได้เจาะจง |
| ร่องรอยในประวัติ | ไม่เหลือ | เหลือ เห็นว่าใครยกเลิกและเพราะอะไร |
| สถานการณ์ที่พบบ่อย | ทำความสะอาดคอมมิตท้องถิ่นก่อน push | ย้อนคอมมิตที่ทำให้ main ในโปรดักชันพังออก |
กฎง่าย ๆ: ถ้าคอมมิตนั้นมีคนอื่นเคยเห็นแล้ว ใช้ revert ถ้ายังไม่มี ใช้ reset ข้อยกเว้นคือบรานช์ฟีเจอร์ส่วนตัวที่ไม่มีใครดึงไปใช้ ที่นั่นการเขียนประวัติใหม่และ git push --force-with-lease ยอมรับได้
ทำไม reset --hard บนบรานช์ร่วมจึงเป็นความคิดที่ไม่ดี
ลองนึกภาพ: คุณรัน git reset --hard HEAD~2 บน main และ git push --force คอมมิตสองตัวนั้นยังอยู่กับเพื่อนร่วมทีม เมื่อพวกเขา git pull ครั้งถัดไป Git จะพยายามรวมบรานช์ของพวกเขากับของคุณ และคอมมิตที่ "ถูกลบ" จะกลับมาอีก พร้อมคอมมิต merge ที่ไม่ได้ตั้งใจ แย่กว่านั้นถ้ามีคนส่งคอมมิตของเขาทับไปแล้ว force push จะเขียนทับมันโดยไม่ทันรู้ตัว
ด้วยเหตุนี้บรานช์ที่ป้องกันไว้ (main, release) มักห้าม force push ด้วยการตั้งค่าของโฮสต์ หากจำเป็นต้องเขียนประวัติทับจริง ใช้ git push --force-with-lease ซึ่งจะปฏิเสธหากมีคอมมิตบนเซิร์ฟเวอร์ที่คุณไม่เคยเห็น
สถานการณ์การยกเลิกที่พบบ่อย
ยกเลิกคอมมิตล่าสุดที่ยังไม่ได้ push
git reset --soft HEAD~1 # การเปลี่ยนแปลงอยู่ในดัชนี คอมมิตใหม่ได้
git reset HEAD~1 # การเปลี่ยนแปลงอยู่ในไฟล์ ดัชนีว่าง
git reset --hard HEAD~1 # การเปลี่ยนแปลงถูกทิ้ง
หากต้องการแค่แก้ข้อความหรือเพิ่มไฟล์ที่ลืม ไม่ต้องใช้ reset: git add ไฟล์ที่ลืม.txt แล้ว git commit --amend
ยกเลิกคอมมิตหลัง push
git revert <แฮช>
git push
นี่คือคำตอบของคำถามยอดนิยม "git reverse commit": ไม่มีคำสั่ง git reverse ใน Git คอมมิตย้อนกลับถูกสร้างโดย git revert
ทำให้บรานช์กลับไปตรงกับเซิร์ฟเวอร์
git fetch origin
git reset --hard origin/main
มีประโยชน์เมื่อบรานช์ท้องถิ่นยุ่งเหยิงหลัง rebase ที่ผิดพลาดหรือการทดลอง คอมมิตท้องถิ่นทุกตัวที่ไม่มีบนเซิร์ฟเวอร์จะหายไปจากบรานช์
กู้คอมมิตหลัง reset ที่ไม่สำเร็จ
Git บันทึกทุกการเคลื่อนที่ของ HEAD ลงในบันทึก reflog คอมมิตหลัง reset ยังอยู่ในฐานข้อมูลอ็อบเจกต์อย่างน้อยหลายสัปดาห์ (ค่าเริ่มต้นสำหรับรายการ reflog ที่เข้าไม่ถึงคือ 30 วัน)
git reflog
# 1c02e8f HEAD@{0}: reset: moving to HEAD~2
# a3f9c21 HEAD@{1}: commit: docs: README
# 7be41d0 HEAD@{2}: commit: feat: แคชการตอบกลับ
git reset --hard a3f9c21 # หรือ HEAD@{1}
Reflog เป็นของเครื่องนี้เท่านั้น อยู่ในโคลนของคุณ และไม่ถูกส่งไปยังเซิร์ฟเวอร์
git clean: ลบไฟล์ที่ไม่ได้ถูกติดตาม
git reset --hard ไม่แตะไฟล์ที่ Git ไม่ได้ติดตาม ได้แก่ ไฟล์ใหม่ที่ไม่ได้เพิ่มในดัชนี ผลลัพธ์จากการบิลด์ และล็อก สำหรับไฟล์เหล่านี้มี git clean คำสั่งนี้ย้อนกลับไม่ได้ ไฟล์ที่ถูกลบไม่ไปอยู่ใน reflog หรือถังขยะ
git clean -n # dry run: แสดงว่าจะลบอะไร
git clean -f # ลบไฟล์ที่ไม่ถูกติดตาม
git clean -fd # เช่นเดียวกัน และรวมโฟลเดอร์ที่ไม่ถูกติดตามด้วย
git clean -fdx # รวมไฟล์ที่ถูกละเว้นด้วย (.gitignore): node_modules, dist
git clean -i # เลือกแบบโต้ตอบ
หากไม่มี -f Git จะไม่ทำงานตามค่าเริ่มต้น (การตั้งค่า clean.requireForce) ควรเริ่มจาก -n เสมอ โดยเฉพาะกับ -x เพราะมันจะลบไฟล์ .env ในเครื่องด้วยหากอยู่ในรายการละเว้น
ชุดคำสั่งสำหรับรีเซ็ตเต็มรูปแบบกลับไปยังคอมมิตล่าสุด: git reset --hard && git clean -fd
ข้อผิดพลาดที่พบบ่อย
- ใช้ reset --hard แทน --soft. คอมมิตที่ทำแล้วกู้ได้ด้วย reflog แต่สิ่งที่ยังไม่คอมมิตหายไปแล้ว นิสัยการทำ
git stashก่อนการทดลองช่วยแก้ปัญหานี้ได้ - revert โดยไม่เข้าใจความสัมพันธ์. หากคอมมิตหลังจากนั้นแก้บรรทัดเดียวกัน revert จะเกิดความขัดแย้ง แก้ตามปกติ: แก้ไฟล์
git addแล้วgit revert --continueหากเปลี่ยนใจ ใช้git revert --abort - สับสนระหว่าง restore และ reset. หากต้องการย้อนการแก้ไขในไฟล์เดียว ไม่ต้องรีเซ็ตทั้งบรานช์ ใช้
git restore ไฟล์ - force push ไปยัง main. ใช้ revert แล้ว push ปกติจะดีกว่า แม้ประวัติจะยาวขึ้นเล็กน้อย
สรุป
git reset คือเครื่องมือสำหรับประวัติของตัวเองที่ยังไม่ได้เผยแพร่: --soft เก็บการเปลี่ยนแปลงไว้ในดัชนี --mixed ไว้ในไฟล์ และ --hard ทิ้งทุกอย่าง git revert คือการยกเลิกที่ปลอดภัยในบรานช์ร่วมที่ทิ้งร่องรอยที่ซื่อสัตย์ไว้ในประวัติ git clean ช่วยลบไฟล์ที่ไม่ได้ติดตาม แต่ทำงานโดยไม่มีที่ให้พลาด การยกเลิกด้วย restore, reset และ revert ถูกอธิบายและฝึกจริงในหลักสูตร Git
คำถามที่พบบ่อย
git reset ลบคอมมิตถาวรหรือไม่?
ไม่ คอมมิตยังอยู่ในฐานข้อมูลอ็อบเจกต์และเข้าถึงได้ผ่าน git reflog จนกว่าตัวเก็บขยะจะลบ ซึ่งโดยค่าเริ่มต้นสำหรับรายการที่เข้าไม่ถึงคือหลัง 30 วัน
ยกเลิก git revert ได้หรือไม่?
ได้ คอมมิต revert เป็นคอมมิตธรรมดา ยกเลิกได้ด้วย git revert อีกครั้ง หรือถ้ายังไม่ได้ push ก็ลบออกด้วย git reset
git reset ต่างจาก git checkout อย่างไร?
git reset เลื่อนพอยน์เตอร์ของบรานช์ปัจจุบัน ส่วน git checkout (หรือ git switch) ย้าย HEAD ไปยังบรานช์หรือคอมมิตอื่นโดยไม่เปลี่ยนบรานช์นั้นเอง
HEAD~1 และ HEAD^ หมายถึงอะไร?
ทั้งสองหมายถึงพาเรนต์ของคอมมิตปัจจุบัน HEAD~3 คือบรรพบุรุษชั้นที่สามตามพาเรนต์ตัวแรก ส่วน HEAD^2 คือพาเรนต์ตัวที่สองของคอมมิต merge
จะยกเลิก git clean ได้อย่างไร?
ด้วยเครื่องมือของ Git ทำไม่ได้ ไฟล์ที่ไม่ถูกติดตามไม่เคยถูกเก็บในรีโพซิทอรี ดังนั้นให้รัน git clean -n ก่อนเสมอ
หัวข้อนี้สอนอย่างละเอียดพร้อมฝึกปฏิบัติในคอร์ส “Git с нуля до уверенного разработчика”
ไปที่คอร์ส