git reset và git revert: cách hoàn tác commit mà không phá lịch sử nhóm
git reset dịch chuyển con trỏ nhánh về commit khác và viết lại lịch sử, nên chỉ phù hợp với commit cục bộ chưa gửi đi. git revert tạo commit mới với các thay đổi ngược lại và giữ nguyên lịch sử, vì vậy là cách an toàn để hoàn tác commit đã có trên máy chủ.
Tóm lại: git reset đưa nhánh lùi lại và làm như thể các commit thừa chưa từng có. git revert giữ nguyên lịch sử và thêm một commit mới hoàn tác thay đổi của commit cũ. Cái đầu dùng cho công việc cục bộ, cái sau dùng cho mọi thứ đã đi vào kho chung. Chỉ cần nhớ quy tắc này là bạn đã tránh được hầu hết sự cố với Git.
Dưới đây là cách hoạt động của cả hai lệnh, sự khác nhau giữa các chế độ của reset, cách hoàn tác commit sau khi đã push, cách lấy lại commit «đã mất», và vai trò của git clean.
Ba tầng quyết định cách hoàn tác
Để hiểu sự khác biệt, hãy nhớ ba khu vực của Git: thư mục làm việc (các tệp trên đĩa), index (staging area — những gì sẽ vào commit tiếp theo) và lịch sử (các commit mà nhánh và HEAD trỏ tới). Mỗi lệnh hoàn tác tác động lên một tập hợp khác nhau trong ba khu vực này.
| Lệnh | Lịch sử | Index | Thư mục làm việc | An toàn cho nhánh chung |
|---|---|---|---|---|
git restore tệp | không | không | có | có |
git restore --staged tệp | không | có | không | có |
git reset --soft | có | không | không | không |
git reset --mixed (mặc định) | có | có | không | không |
git reset --hard | có | có | có | không |
git revert | thêm một commit | — | — | có |
git clean | không | không | xóa tệp chưa theo dõi | có, nhưng không thể hoàn tác |
Cách git reset hoạt động
Lệnh git reset <commit> chuyển con trỏ của nhánh hiện tại (và HEAD theo nó) tới commit được chỉ định. Các commit «sau» đó không bị xóa vật lý, nhưng không còn nhìn thấy trong nhánh. Chế độ quyết định điều gì xảy ra với index và các tệp.
git reset --soft: hoàn tác commit, giữ thay đổi trong index
Nhánh được dịch chuyển, còn index và các tệp giữ nguyên. Mọi thay đổi của các commit bị hoàn tác trở thành đã chuẩn bị để commit. Đây là cách lý tưởng để sửa lại thông điệp hoặc gộp vài commit cuối thành một.
git reset --soft HEAD~3
git commit -m "feat: form đăng ký"
Ba commit cuối đã thành một commit, nội dung tệp không thay đổi.
git reset --mixed: hoàn tác commit và index
Chế độ mặc định: git reset HEAD~1 tương đương với git reset --mixed HEAD~1. Nhánh được dịch chuyển, index bị đặt lại, còn thay đổi vẫn nằm trong thư mục làm việc ở trạng thái chưa chuẩn bị. Tiện khi bạn lỡ commit thứ gì đó và muốn chọn lại cách thêm vào theo từng phần.
git reset HEAD~1
git status
# Changes not staged for commit:
# modified: app.py
# modified: config.yml
git reset --hard: quay lại hoàn toàn
Nhánh, index và thư mục làm việc đều được đưa về trạng thái của commit chỉ định. Mọi thay đổi chưa commit trong các tệp được theo dõi bị mất mà không có cảnh báo. Đây là chế độ reset duy nhất thực sự có thể phá hủy công việc của bạn.
git reset --hard HEAD~1 # bỏ commit cuối và mọi chỉnh sửa
git reset --hard origin/main # đưa nhánh về trạng thái của máy chủ
Trước --hard, nên chạy git status và git stash nếu có thứ gì quan trọng. Thứ đã commit có thể lấy lại (xem bên dưới), còn thứ chưa commit thì không.
git reset cho một tệp
Khi có đường dẫn, reset không dịch chuyển nhánh mà chỉ thay đổi index: git reset app.py gỡ tệp khỏi danh sách đã chuẩn bị. Cách viết hiện đại tương đương là git restore --staged app.py, rõ ràng hơn. Các cờ --soft và --hard không dùng được với đường dẫn.
Cách git revert hoạt động
git revert <commit> tính các thay đổi của commit được chỉ định, áp dụng chúng theo chiều ngược lại và tạo commit mới. Dòng đã thêm bị xóa, dòng đã xóa được khôi phục. Lịch sử chỉ tăng thêm, không có gì bị viết lại, nên đồng đội không phải sửa gì ở máy của họ.
git log --oneline
# a3f9c21 (HEAD -> main) docs: README
# 7be41d0 feat: cache phản hồi API
# 1c02e8f fix: timeout khi gọi yêu cầu
git revert 7be41d0
# [main 5d8e2aa] Revert "feat: cache phản hồi API"
# 2 files changed, 3 insertions(+), 41 deletions(-)
Git sẽ mở trình soạn thảo với thông điệp dạng Revert "..." và dòng This reverts commit 7be41d0.... Nên bổ sung lý do vì sao thay đổi bị hoàn tác.
Các cờ hữu ích của revert
--no-edit— chấp nhận thông điệp mặc định, không mở trình soạn thảo.-n(--no-commit) — áp dụng các thay đổi ngược vào index mà không tạo commit. Nhờ vậy có thể hoàn tác nhiều commit trong một commit duy nhất.git revert A..B— hoàn tác một khoảng commit (mỗi commit một revert riêng, từ mới đến cũ).-m 1— bắt buộc với commit gộp: chỉ định cha nào là dòng chính.
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert: hoàn tác ba commit thử nghiệm"
Revert commit gộp
Một lần gộp có hai cha, và Git không biết nên tính thay đổi «ngược» so với cha nào. Không có -m nó báo lỗi is a merge but no -m option was given. Thông thường nhánh chính là cha thứ nhất:
git revert -m 1 9f1e2ab
Cái bẫy: nếu sau này bạn muốn gộp lại chính nhánh đó, Git coi các commit của nhánh là đã được gộp và sẽ không mang thay đổi về. Cách xử lý: trước hết revert chính commit revert đó, rồi mới gộp tiếp các thay đổi mới.
git reset hay git revert: nên chọn gì
| Tiêu chí | git reset | git revert |
|---|---|---|
| Tác động lên lịch sử | viết lại (commit biến khỏi nhánh) | thêm commit mới |
| Commit đã được push | cần push --force, làm hỏng công việc đồng nghiệp | push thông thường |
| Hoàn tác commit ở giữa lịch sử | chỉ cùng với mọi commit sau nó | có thể làm riêng lẻ |
| Dấu vết trong lịch sử | không còn | còn, cho thấy ai và vì sao đã hoàn tác |
| Kịch bản điển hình | dọn commit cục bộ trước khi push | hoàn tác commit làm hỏng production trên main |
Quy tắc đơn giản: nếu commit đã có người khác nhìn thấy — dùng revert; nếu chưa — dùng reset. Ngoại lệ là nhánh tính năng cá nhân mà không ai khác lấy về: ở đó viết lại lịch sử và git push --force-with-lease là chấp nhận được.
Vì sao reset --hard trên nhánh chung là ý tưởng tồi
Giả sử bạn chạy git reset --hard HEAD~2 trên main rồi git push --force. Đồng nghiệp vẫn giữ hai commit đó. Khi họ chạy git pull, Git cố gộp nhánh của họ với nhánh của bạn, và hai commit «đã xóa» quay lại, chỉ khác là kèm một commit gộp rối rắm. Tệ hơn nữa, nếu ai đó đã push commit mới lên trên, force push sẽ xóa sạch chúng.
Vì vậy trên các nhánh được bảo vệ (main, release) người ta thường cấm force push trong cài đặt của nền tảng. Nếu thật sự không tránh được việc viết lại, dùng git push --force-with-lease: lệnh sẽ từ chối nếu trên máy chủ xuất hiện commit mà bạn chưa thấy.
Các tình huống hoàn tác thường gặp
Hoàn tác commit cuối cùng chưa push
git reset --soft HEAD~1 # thay đổi vẫn trong index, có thể commit lại
git reset HEAD~1 # thay đổi nằm trong tệp, index trống
git reset --hard HEAD~1 # thay đổi bị bỏ hết
Nếu chỉ cần sửa thông điệp hoặc thêm tệp bị quên, không cần reset: dùng git add tệp-quên.txt rồi git commit --amend.
Hoàn tác commit đã push
git revert <hash>
git push
Đây chính là câu trả lời cho yêu cầu «git reverse commit»: Git không có lệnh git reverse; commit đảo ngược được tạo bởi git revert.
Đưa nhánh về trạng thái của máy chủ
git fetch origin
git reset --hard origin/main
Hữu ích khi nhánh cục bộ bị rối sau một rebase thất bại hoặc các thử nghiệm. Mọi commit cục bộ chưa có trên máy chủ sẽ biến khỏi nhánh.
Khôi phục commit sau một reset sai
Git ghi lại mọi lần di chuyển HEAD trong nhật ký reflog. Commit sau reset vẫn nằm trong cơ sở dữ liệu ít nhất vài tuần (mặc định các mục reflog không thể truy cập được giữ 30 ngày).
git reflog
# 1c02e8f HEAD@{0}: reset: moving to HEAD~2
# a3f9c21 HEAD@{1}: commit: docs: README
# 7be41d0 HEAD@{2}: commit: feat: cache phản hồi API
git reset --hard a3f9c21 # hoặc HEAD@{1}
Reflog chỉ lưu cục bộ: nó có trong bản clone của bạn và không được gửi lên máy chủ.
git clean: xóa tệp chưa được theo dõi
git reset --hard không chạm tới các tệp mà Git không theo dõi: tệp mới chưa được đưa vào index, sản phẩm build, nhật ký. Để xử lý chúng có git clean. Lệnh này không thể hoàn tác — tệp bị xóa không nằm trong reflog và không vào thùng rác.
git clean -n # chạy thử: chỉ hiển thị sẽ xóa gì
git clean -f # xóa tệp chưa theo dõi
git clean -fd # cộng thêm các thư mục chưa theo dõi
git clean -fdx # cộng thêm tệp bị bỏ qua (.gitignore): node_modules, dist
git clean -i # chọn tương tác
Nếu không có -f, Git mặc định từ chối chạy (cấu hình clean.requireForce). Luôn bắt đầu bằng -n, đặc biệt với -x: nó sẽ xóa cả .env cục bộ nếu tệp này nằm trong danh sách bỏ qua.
Để đưa về trạng thái commit cuối một cách hoàn toàn: git reset --hard && git clean -fd.
Lỗi thường gặp
- Dùng reset --hard thay vì --soft. Thứ đã commit còn lấy lại được qua reflog, còn thứ chưa commit thì mất. Thói quen chạy
git stashtrước khi thử nghiệm giải quyết vấn đề này. - Revert mà không hiểu phụ thuộc. Nếu các commit sau đó đã sửa cùng dòng, revert sẽ xung đột. Xử lý như xung đột thông thường: sửa tệp,
git add,git revert --continue; đổi ý —git revert --abort. - Nhầm restore với reset. Để hoàn tác thay đổi trong một tệp, không cần reset cả nhánh:
git restore tệp. - Force push vào main. Tốt hơn là revert và push thông thường, dù lịch sử dài thêm một chút.
Tóm lại
git reset là công cụ cho lịch sử riêng của bạn chưa được công bố: --soft giữ thay đổi trong index, --mixed giữ trong tệp, --hard bỏ tất cả. git revert là cách hoàn tác an toàn trên nhánh chung và để lại dấu vết rõ ràng trong lịch sử. git clean dọn các tệp chưa theo dõi, nhưng không cho phép sai sót. Các tình huống hoàn tác với restore, reset và revert được thực hành trong khóa học Git.
Câu hỏi thường gặp
git reset có xóa commit vĩnh viễn không?
Không. Commit vẫn nằm trong cơ sở dữ liệu và có thể lấy qua git reflog, cho đến khi bộ thu gom rác xóa nó — mặc định sau 30 ngày với các mục không thể truy cập.
Có thể hoàn tác git revert không?
Có. Commit revert là một commit thông thường, có thể hoàn tác bằng một git revert khác, hoặc nếu chưa push thì gỡ bằng git reset.
git reset khác git checkout ở điểm nào?
git reset dịch chuyển con trỏ của nhánh hiện tại, còn git checkout (hoặc git switch) chuyển HEAD sang nhánh hoặc commit khác mà không thay đổi chính nhánh đó.
HEAD~1 và HEAD^ khác nhau thế nào?
Cả hai đều là cha của commit hiện tại. HEAD~3 là tổ tiên thứ ba theo cha thứ nhất, còn HEAD^2 là cha thứ hai của commit gộp.
Làm sao khôi phục những gì git clean đã xóa?
Bằng công cụ của Git không thể làm được: các tệp không được theo dõi chưa từng vào kho. Vì vậy luôn chạy git clean -n trước.
Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Git с нуля до уверенного разработчика”.
Đến khóa học