Playbook cứu sự cố Git: reset, revert, reflog và bisect Trung cấp
Thời gian đọc ước tính: 7 phút
Trang này trả lời: gặp sự cố với Git thì dùng lệnh nào, và cách tự động tìm commit gây bug. Hai demo có output thật, chạy lại được.
Chọn lệnh theo tình huống
Phần tiêu đề “Chọn lệnh theo tình huống”Đang tải diagram…
| Tình huống | Lệnh | Ghi chú |
|---|---|---|
| Commit cuối sai, chưa push | git reset --soft HEAD~1 |
Bỏ commit, giữ thay đổi ở trạng thái staged. Chỉ sửa message: git commit --amend |
| Bỏ thay đổi chưa commit của một file | git restore <file> |
Lấy lại từ index; mất vĩnh viễn phần chưa commit |
Lỡ git add |
git restore --staged <file> |
Chỉ bỏ khỏi index, file trong worktree giữ nguyên |
Commit đã push lên main gây bug |
git revert <sha> |
Tạo commit mới đảo ngược; không rewrite history. Revert merge: git revert -m 1 <merge> |
Mất commit sau reset --hard, rebase hỏng, xoá branch |
git reflog → git branch rescue <sha> |
Reflog chỉ có ở repo local |
| Tìm commit gây bug | git bisect start <bad> <good> + git bisect run <cmd> |
Tìm nhị phân; kết thúc bằng git bisect reset |
| Đang code dở, cần sửa gấp chỗ khác | git stash push -m "..." → sau đó git stash pop |
Thêm -u để stash cả file untracked |
| Code chưa commit nhưng đang ở nhầm branch | git switch -c feature/x |
Thay đổi chưa commit đi theo sang branch mới |
Lỡ commit vào main local (chưa push) |
git branch feature/x → git reset --hard origin/main → git switch feature/x |
Kiểm tra git status sạch trước khi reset --hard |
| Cần một commit từ branch khác | git cherry-pick <sha> |
Tạo commit mới với hash mới; dùng -x để ghi nguồn |
| Rebase/merge rối tung | git rebase --abort / git merge --abort |
Quay về trạng thái trước khi bắt đầu |
Đã push lên branch chung thì luôn revert, không reset. reset + force push buộc mọi người đồng bộ lại, và dễ làm mất commit của người khác (xem Lỗi hay gặp).
Demo 1: cứu commit bị reset --hard bằng reflog
Phần tiêu đề “Demo 1: cứu commit bị reset --hard bằng reflog”Reflog ghi lại mỗi lần tip của branch và HEAD thay đổi trong repo local. Thời hạn mặc định:
| Loại entry | Giữ | Config |
|---|---|---|
| Còn reachable | 90 ngày | gc.reflogExpire |
| Không còn reachable | 30 ngày | gc.reflogExpireUnreachable |
Commit “mất” vẫn nằm trong object store tới khi hết hạn và bị gc dọn.
Output thật từ examples/src/git-workflow/demo.sh (git 2.54.0):
$ git log --onelinee4486e9 feat: thêm API thanh toándd91a76 feat: thêm API đơn hàng2edc7b5 feat: khung app
$ git reset --hard HEAD~2 # lỡ tay: 2 commit biến mất khỏi branchHEAD is now at 2edc7b5 feat: khung app
$ git reflog -n 42edc7b5 HEAD@{0}: reset: moving to HEAD~2e4486e9 HEAD@{1}: commit: feat: thêm API thanh toándd91a76 HEAD@{2}: commit: feat: thêm API đơn hàng2edc7b5 HEAD@{3}: commit (initial): feat: khung app
$ git reset --hard 'HEAD@{1}' # quay về vị trí ngay trước lệnh resetHEAD is now at e4486e9 feat: thêm API thanh toán
$ git log --onelinee4486e9 feat: thêm API thanh toándd91a76 feat: thêm API đơn hàng2edc7b5 feat: khung app- Đọc reflog từ trên xuống:
HEAD@{0}là hiện tại,HEAD@{1}là ngay trước đó. - An toàn hơn
reset --hard: tạo branch tại hash đó (git branch rescue e4486e9), xem lại rồi mới quyết. - Giới hạn: reflog không cứu được thay đổi chưa từng commit (hoặc chưa từng
git add).git restorevàreset --hardxoá chúng thật.
Demo 2: git bisect run tìm commit gây bug
Phần tiêu đề “Demo 2: git bisect run tìm commit gây bug”Kịch bản: price.sh tính giá sau giảm 10% cho đơn 200, kỳ vọng 180. Tag v1.0 đúng; HEAD sai; ở giữa có 7 commit. Script kiểm tra nằm ngoài repo để mọi commit dùng cùng một bài test:
#!/bin/sh# exit 0 = good, 1..127 (trừ 125) = bad, 125 = skip[ "$(sh ./price.sh)" = "180" ]Output thật:
$ git log --oneline6e3c8ed docs: changelog 6ff94529 docs: changelog 5b824065 docs: changelog 4375576b refactor: tách bước làm tròna203b7a docs: changelog 3ccca1c0 docs: changelog 293a4098 docs: changelog 1961be8e feat: tính giá sau giảm 10%
$ git bisect start HEAD v1.0Bisecting: 3 revisions left to test after this (roughly 2 steps)[a203b7a5538e8e6c4a0b22e25ec0ead9bb96a012] docs: changelog 3
$ git bisect run sh ../check.shrunning 'sh' '../check.sh'Bisecting: 1 revision left to test after this (roughly 1 step)[b824065e9f27c18b15c4f035723cba5fac9fe1b5] docs: changelog 4running 'sh' '../check.sh'Bisecting: 0 revisions left to test after this (roughly 0 steps)[375576bfcca8253835e23ee6f30ab0ff26feea0d] refactor: tách bước làm trònrunning 'sh' '../check.sh'375576bfcca8253835e23ee6f30ab0ff26feea0d is the first bad commitcommit 375576bfcca8253835e23ee6f30ab0ff26feea0dAuthor: demo <demo@example.com>Date: Sun Oct 18 09:08:00 2026 +0700
refactor: tách bước làm tròn
price.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)bisect found first bad commit
$ git bisect resetPrevious HEAD position was 375576b refactor: tách bước làm trònSwitched to branch 'main'- Ba lần chạy test cho 7 commit ứng viên. Bisect là tìm nhị phân, nên 1.000 commit cũng chỉ cần khoảng 10 lần.
- Bài học kèm theo: commit gây bug tên
refactor— đúng loại commit đáng lẽ không được đổi hành vi.
Quy ước exit code của git bisect run
Phần tiêu đề “Quy ước exit code của git bisect run”Đang tải diagram…
| Exit code | Ý nghĩa |
|---|---|
0 |
good |
1–127, trừ 125 |
bad |
125 |
skip (commit này không test được, ví dụ không build) |
Khác (ví dụ 128–255) |
dừng bisect |
Bẫy hay gặp: script trả 127 (“command not found”) bị hiểu là bad, bisect chỉ sai commit. Với project TypeScript:
exit 125khinpm cihoặc build thất bại.- Chỉ trả bad khi đúng test tái hiện bug fail.
- Test flaky làm bisect chỉ sai commit (xem flaky tests).
Demo cách bẫy này chỉ sai commit: Lỗi hay gặp.
Chạy lại demo
Phần tiêu đề “Chạy lại demo”Script tự tạo repo trong thư mục tạm, không đụng repo hiện tại, tự dọn khi xong:
bash examples/src/git-workflow/demo.shScript cố định tên tác giả và ngày commit, nên hash giống output trên nếu cùng phiên bản git.
Tiếp theo: Secret, PR và checklist.