---
id: git-recovery
title: "Sửa sai: revert, reset, reflog và secret lỡ commit"
shortTitle: "Revert, reset, reflog"
track: git
trackTitle: "Git & VCS Hosting"
order: 5
level: core
status: published
estimatedMinutes: 45
prerequisites:
  - git-branching
  - git-remote-pr
next: git-hooks
reviewedAgainst: 2026-10-15
sources:
  - title: "KodeKloud Engineer — 100 Days of DevOps"
    url: https://engineer.kodekloud.com/practice
  - title: "Pro Git — Reset Demystified"
    url: https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified
  - title: "git-revert"
    url: https://git-scm.com/docs/git-revert
  - title: "git-reflog"
    url: https://git-scm.com/docs/git-reflog
  - title: "git-bisect"
    url: https://git-scm.com/docs/git-bisect
  - title: "Removing sensitive data from a repository"
    url: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository
  - title: "About push protection"
    url: https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection
---

# Sửa sai: revert, reset, reflog và secret lỡ commit

Lab chạy trên máy, không cần remote. Phần xoá secret khỏi history cần cài `git-filter-repo`.

## Để làm gì

Sớm muộn gì bạn cũng gặp mấy chuyện này: một commit lỗi đã vào `main` và đã deploy; một lệnh `reset --hard` gõ nhầm làm mất commit; một file `.env` có password bị push lên GitHub. Bài này hướng dẫn xử lý từng tình huống mà không làm hỏng thêm, và quan trọng nhất là không phá history của người khác.

## Gồm những gì

- `git revert`: tạo commit đảo ngược, an toàn trên branch chung.
- `git reset --soft | --mixed | --hard`: dời branch, chỉ dùng cho commit chưa chia sẻ.
- `git commit --amend`.
- `git reflog`: tìm lại commit "đã mất".
- `git stash push -m`, `-u`; `stash pop` so với `apply`; `stash drop`.
- `git bisect`: tìm commit gây lỗi bằng tìm kiếm nhị phân.
- Secret lỡ commit: rotate trước, rồi mới dọn history bằng `git filter-repo`. Push protection để chặn từ đầu.

## Dùng khi nào

| Tình huống | Dùng |
| --- | --- |
| Commit lỗi đã merge vào `main`, cần gỡ ngay | `git revert` |
| Commit local chưa push, muốn làm lại | `git reset` hoặc `--amend` |
| Lỡ `reset --hard`, xoá branch, rebase hỏng | `git reflog` |
| Đang sửa dở, cần chuyển branch gấp để hotfix | `git stash` |
| Bản trước chạy tốt, bản này lỗi, không biết commit nào | `git bisect` |
| Secret đã vào history | Rotate secret, rồi `git filter-repo` |

## Kiến thức cốt lõi

### Revert: sửa bằng cách thêm commit

`git revert <hash>` tạo commit mới có thay đổi ngược với commit đó. History cũ giữ nguyên nên không ai bị lệch, và pipeline coi nó như một commit bình thường. Đây là cách rollback code trên `main`.

Khi revert một merge commit, bạn phải chỉ rõ giữ phía nào bằng `-m 1` (parent thứ nhất, thường là `main`).

### Reset: dời con trỏ branch

Pro Git giải thích reset qua ba "cây": HEAD, index, working tree. `git reset <commit>` dời branch hiện tại về commit đó. Ba option chỉ khác nhau ở chỗ có đụng tới index và working tree hay không:

| Option | Branch | Staging | File trên đĩa | Dùng khi |
| --- | --- | --- | --- | --- |
| `--soft` | Dời | Giữ | Giữ | Gộp vài commit cuối thành một |
| `--mixed` (mặc định) | Dời | Reset | Giữ | Bỏ commit, sắp xếp lại staging |
| `--hard` | Dời | Reset | **Ghi đè** | Bỏ hẳn mọi thay đổi |

`--hard` xoá thay đổi chưa commit, và reflog không lưu những thay đổi đó. Còn commit đã tạo thì vẫn tìm lại được.

Nếu bạn reset commit đã push, history local sẽ khác remote, bạn phải force push, và history của người khác bị lệch. Trên branch chung thì dùng revert.

### Amend

`git commit --amend` thay commit cuối bằng một commit mới, thường để sửa message hoặc thêm file quên add. Vì hash đổi, bạn cũng chỉ nên dùng nó trước khi push.

### Reflog: lưới an toàn

Reflog ghi lại mỗi lần `HEAD` và branch di chuyển trên máy bạn: commit, checkout, reset, rebase, merge. Commit không còn branch nào trỏ tới vẫn nằm trong repo một thời gian, và reflog giữ đường tới nó. Mặc định, entry trong reflog hết hạn sau 90 ngày, hoặc 30 ngày với commit không còn reachable (cấu hình `gc.reflogExpire` và `gc.reflogExpireUnreachable`).

Reflog chỉ có ở local. Một bản clone mới không có reflog của máy khác.

### Stash

`git stash` không argument giống `git stash push`. Lệnh cất thay đổi đã track, rồi đưa working tree về `HEAD`. File chưa từng `git add` thì không vào stash, trừ khi có `-u` / `--include-untracked`. `-a` còn lấy cả file đang bị ignore. `-m` đặt mô tả; không có thì Git ghi `WIP on …`.

`git stash pop` áp stash rồi xóa khỏi danh sách. Conflict thì stash vẫn còn, phải `git stash drop` sau khi sửa xong. `git stash apply` áp mà không xóa. `git stash list` in `stash@{0}` là mới nhất. `git stash clear` xóa cả stack.

### Bisect

`git bisect` chia đôi khoảng commit giữa một bản tốt và một bản lỗi. Mỗi lượt nó hỏi bạn commit ở giữa là tốt hay lỗi, cho tới khi chỉ còn đúng một commit. Với 1000 commit, bạn chỉ cần khoảng 10 lần kiểm tra. `git bisect run <script>` tự chạy script: exit code `0` là tốt, `1` đến `127` (trừ `125`) là lỗi, `125` là bỏ qua commit đó.

### Secret lỡ commit

Ở đây thứ tự rất quan trọng:

1. **Coi như secret đã lộ, revoke hoặc rotate ngay.** Docs GitHub đặt bước này lên đầu. Repo public bị bot quét liên tục; với repo private, ai có quyền đọc hoặc đã clone đều đang giữ bản đó.
2. **Xoá khỏi history** nếu cần, bằng `git filter-repo` (GitHub khuyên dùng tool này thay cho `git filter-branch`). Thao tác này viết lại mọi commit từ chỗ đó trở đi, nên mọi người phải clone lại.
3. Docs GitHub cũng lưu ý: bản sao có thể vẫn còn trong fork, trong clone của người khác, và trong cache hoặc view của pull request cũ. Có trường hợp bạn phải liên hệ GitHub Support.

Tạo một commit mới để xoá file `.env` là **không** đủ, vì file vẫn nằm trong commit cũ.

Tốt nhất là chặn từ đầu: bật **push protection** của GitHub secret scanning. GitHub quét nội dung lúc push và chặn push nếu phát hiện secret có định dạng nhận biết được, như token của các provider lớn. Với repo public, docs GitHub ghi push protection cho user được bật mặc định.

## Lệnh và code mẫu

Revert:

```bash
git revert 3f2a9c1                # mở editor để sửa message
git revert --no-edit 3f2a9c1
git revert -m 1 <merge-commit>    # revert cả một PR đã merge bằng merge commit
```

Reset commit local:

```bash
git reset --soft HEAD~3     # gộp 3 commit cuối, thay đổi vẫn ở staging
git commit -m "Add healthcheck endpoint"

git reset HEAD~1            # bỏ commit cuối, giữ file đã sửa
git reset --hard HEAD~1     # bỏ commit cuối và mọi thay đổi
```

Amend:

```bash
git add forgotten-file.yaml
git commit --amend --no-edit
git commit --amend -m "Fix readiness probe path"
```

Tìm lại commit bằng reflog:

```bash
git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~2
# 9f8e7d6 HEAD@{1}: commit: Add autoscaling config
git branch rescue 9f8e7d6     # gắn branch vào commit vừa tìm thấy
# hoặc đưa branch hiện tại quay về
git reset --hard HEAD@{1}
```

Stash:

```bash
git stash push -m "half-done ingress config"
git stash push -u -m "kem file chua add"
git switch -c hotfix/healthcheck main
# ... sửa, commit, push hotfix ...
git switch feature/ingress
git stash list
git stash apply stash@{0}
git stash drop stash@{0}
```

Bisect tự động với test:

```bash
git bisect start
git bisect bad                 # commit hiện tại lỗi
git bisect good v1.2.0         # tag này chạy tốt
git bisect run npm test
git bisect reset               # quay về branch ban đầu
```

Xoá file secret khỏi toàn bộ history. Chạy trên một clone mới, sau khi đã rotate secret:

```bash
git clone https://github.com/my-org/api.git api-clean
cd api-clean
git filter-repo --sensitive-data-removal --invert-paths --path .env
git push --force --mirror origin
```

Hoặc thay chuỗi secret trong mọi file bằng `***REMOVED***`. File `replacements.txt` mỗi dòng một chuỗi:

```bash
printf 'MyS3cretP@ss\n' > ../replacements.txt
git filter-repo --sensitive-data-removal --replace-text ../replacements.txt
```

Option `--sensitive-data-removal` có từ `git-filter-repo` 2.47; nếu bạn dùng bản cũ hơn thì bỏ option này khỏi lệnh. Các bước đầy đủ, gồm việc báo cho người cộng tác, nằm trong docs GitHub ở mục Nguồn.

## Thực hành

```bash
mkdir -p "$HOME/devops-lab/git-recovery"
cd "$HOME/devops-lab/git-recovery"
git init -b main
for i in 1 2 3 4 5 6 7 8; do
  echo "step $i" >> app.txt
  git add app.txt
  git commit -qm "Step $i"
done
echo "BUG" >> app.txt && git commit -qam "Step 9 (introduces bug)"
for i in 10 11 12; do echo "step $i" >> app.txt; git commit -qam "Step $i"; done
```

1. **Revert:** tìm hash commit "Step 9" bằng `git log --oneline`, chạy `git revert --no-edit <hash>`. Xác nhận `app.txt` không còn dòng `BUG` mà history vẫn đủ.
2. **Bisect:** `git bisect start`, `git bisect bad HEAD~1` (trước revert), `git bisect good HEAD~12`, rồi `git bisect run sh -c '! grep -q BUG app.txt'`. Đọc commit mà bisect chỉ ra, rồi `git bisect reset`.
3. **Reset và reflog:** `git reset --hard HEAD~3`. Xác nhận 3 commit biến mất khỏi `git log`. Dùng `git reflog` tìm lại và `git reset --hard HEAD@{1}`.
4. **Amend:** tạo file `notes.md`, commit, rồi sửa message bằng `--amend`. So sánh hash trước và sau.
5. **Stash:** sửa `app.txt` mà không commit, `git stash`, tạo branch `hotfix` và commit ở đó, quay lại `main` rồi `git stash pop`.

## Kết quả mong đợi

- Sau revert, `git log --oneline` có thêm commit `Revert "Step 9 (introduces bug)"`, các commit cũ vẫn còn.
- Bisect in `<hash> is the first bad commit` đúng là "Step 9".
- Sau `reset --hard HEAD~3`, `git log` ngắn đi 3 commit; sau khi dùng reflog thì đủ lại.
- `--amend` làm commit cuối có hash mới.
- `git stash pop` trả lại đúng thay đổi đang sửa dở.

## Checklist

- [ ] Chọn revert hay reset dựa trên việc commit đã chia sẻ chưa.
- [ ] Giải thích khác nhau giữa `--soft`, `--mixed`, `--hard`.
- [ ] Revert một merge commit bằng `-m 1`.
- [ ] Dùng reflog tìm lại commit sau reset nhầm.
- [ ] Dùng `git bisect run` với một lệnh test.
- [ ] Nói đúng thứ tự xử lý secret lỡ commit: rotate trước, dọn history sau.
- [ ] Bật push protection cho repo.
- [ ] File chưa track thì `stash push -u`. Giữ stash sau khi áp thì `apply`, không `pop`.

## Thẻ ôn

### git-recovery-revert
kind: decide
cue: Trên `main` đã push, rollback bằng `git revert`. `reset --hard` rồi force push làm lệch history của người khác và của pipeline.

Q: Một commit vừa merge vào `main` và đã được push làm hỏng production. Có người đề xuất `git reset --hard` về commit trước đó rồi force push để "xóa" commit hỏng. Vì sao không làm vậy trên nhánh chung, và làm gì thay?

A: `reset --hard` dời nhánh về commit cũ và bỏ các commit phía sau khỏi nhánh. Force push bắt remote đi theo. Đồng nghiệp đang đứng trên commit bị bỏ sẽ lệch history. Pipeline đã checkout commit đó cũng không còn cùng một đường. Commit của người khác nằm sau điểm reset có thể biến khỏi `main`. Trên nhánh đã chia sẻ, thêm một commit `git revert` của commit hỏng. History cũ vẫn còn, bản vá hoàn tác là một commit mới ai cũng pull được như bình thường.

### git-recovery-reset
kind: distinguish
cue: `reset` chỉ dùng cho commit chưa chia sẻ. `--soft` giữ staging, `--mixed` bỏ staging nhưng giữ file, `--hard` xóa cả phần đang sửa trên đĩa.

Q: Bạn có vài commit local chưa push và muốn sắp lại. `--soft`, `--mixed` và `--hard` đều dời nhánh, nhưng file và staging còn lại khác nhau thế nào?

A: Cả ba đưa nhánh hiện tại về commit bạn chỉ. Chỉ làm vậy khi những commit bị bỏ chưa có ai khác dựa vào. `--soft` dời nhánh nhưng giữ nguyên index và working tree, nên các thay đổi vẫn nằm trong staging, hợp để gộp vài commit cuối thành một. `--mixed` là mặc định: dời nhánh, xóa staging, nhưng phần sửa vẫn còn trên đĩa để bạn add lại. `--hard` xóa cả staging lẫn working tree. Phần chưa commit không còn trên đĩa.

### git-recovery-merge
kind: command
cue: Gỡ một pull request đã merge bằng merge commit thì `git revert -m 1`. Thiếu `-m` thì Git không biết giữ phía nào.

Q: Một pull request đã được merge vào `main` bằng merge commit, và cần gỡ cả pull request đó, không chỉ một commit bên trong. `git revert` trên merge commit báo cần chỉ parent. Cờ nào, và `-m 1` giữ phía nào?

A: Merge commit có hai cha: phía nhánh bạn merge vào, thường là `main`, và phía nhánh của pull request. Git không tự chọn khi revert. `git revert -m 1` nói giữ parent thứ nhất, tức `main`, và tạo một commit hoàn tác toàn bộ phần nhánh kia đã đưa vào. Thiếu `-m` thì lệnh không chạy. Đây vẫn là một commit mới trên `main`, không viết lại history.

### git-recovery-secret
kind: trap
cue: Secret đã vào Git thì rotate ngay. Commit xóa file không xóa secret khỏi commit cũ. Dọn history chỉ làm sau khi secret cũ đã vô hiệu.

Q: Một file chứa secret lỡ được commit và push. Bạn thêm một commit xóa file đó, push tiếp, và coi như xong. Người clone repo còn đọc được secret không? Nên làm gì trước khi dọn history?

A: Còn. Commit mới chỉ nói file không còn ở đầu nhánh. Commit cũ vẫn chứa nguyên nội dung, `git show` trên hash đó vẫn in secret. Xóa file không thu hồi được bí mật đã phát đi. Rotate secret ngay, để giá trị cũ vô hiệu dù ai còn đọc history. Dọn history là bước sau. Dọn trước khi rotate thì trong lúc chờ rewrite và chờ mọi bản sao cập nhật, secret vẫn dùng được.

### git-recovery-reflog
kind: command
cue: `reset --hard` nhầm làm commit biến khỏi `git log`, nhưng `git reflog` còn thấy một thời gian. Reflog không giữ phần chưa commit.

Q: Bạn `git reset --hard` nhầm, `git log` không còn commit vừa làm. Commit đó lấy lại bằng gì? Nếu lúc reset còn một đống sửa chưa commit thì reflog có cứu được không?

A: `git reflog` ghi những chỗ HEAD từng đứng, kể cả commit vừa bị reset khỏi nhánh. Trong một thời gian, commit vẫn còn trong repo dù không hiện trên `git log`. `git reset --hard` về đúng mục, ví dụ `HEAD@{1}`, đưa nhánh trở lại. Reflog không lưu phần chưa commit. `--hard` đã xóa working tree thì những sửa đó không có trong reflog để mà lấy. Muốn giữ phần đang sửa trước một reset mạo hiểm, `git stash` hoặc commit tạm lên một nhánh trước.

### git-recovery-stash
kind: distinguish
cue: File chưa add thì `stash push -u`. `pop` xóa khỏi stack sau khi áp. `apply` giữ lại. Conflict lúc `pop` thì stash chưa bị xóa.

Q: Bạn `git stash` rồi chuyển nhánh hotfix. Trong thư mục còn một file mới chưa `git add`, file đó không theo stash. `pop` và `apply` khác nhau chỗ nào, và file chưa track đưa vào stash bằng cờ nào?

A: `git stash` không argument chỉ cất file Git đã theo dõi. File mới chưa add ở lại working tree, lần `switch` có thể bị cản. `-u` đưa file chưa track vào stash rồi dọn chúng khỏi đĩa. `pop` áp rồi xóa mục đó. Conflict thì `pop` không xóa, phải `drop` sau khi sửa. `apply` áp và giữ mục trên stack, hợp khi còn cần áp sang nhánh khác.

## Lỗi thường gặp

**`git reset --hard` trên `main` đã push rồi force push để "rollback".** History của đồng nghiệp và pipeline bị lệch, và commit của người khác có thể mất. Trên branch chung, hãy rollback bằng `git revert`.

**Xoá file secret bằng một commit mới rồi coi như xong.** Secret vẫn nằm trong commit cũ, ai clone cũng xem được bằng `git show`. Rotate secret ngay, rồi dọn history nếu cần.

**Dọn history trước, rotate sau.** Trong khoảng thời gian đó secret vẫn dùng được, và bản sao cũ có thể đã nằm ở nơi khác. Luôn rotate trước.

**Dùng `reset --hard` khi còn thay đổi chưa commit muốn giữ.** Reflog không lưu thay đổi chưa commit, nên chúng mất hẳn. Chạy `git stash` hoặc commit tạm trước.

**Để stash tồn đọng nhiều tuần.** Bạn sẽ quên stash nào chứa gì, rồi `pop` nhầm vào branch khác và dính conflict. Đặt message bằng `git stash push -m`, hoặc commit lên một branch `wip/`.

**`git stash` khi còn file mới chưa add.** File đó không vào stash. Cần `-u`, hoặc `git add` trước.

## Nguồn

1. [KodeKloud Engineer — 100 Days of DevOps](https://engineer.kodekloud.com/practice) — đề cương ngày revert, reset --hard, stash. Trang practice cần đăng nhập. Bài này viết lại lệnh, không chép lab.
2. [KodeKloud — GIT for Beginners](https://learn.kodekloud.com/learn/courses/git-for-beginners) — phạm vi module Resetting and Reverting: reset, revert, stash, reflog.
3. [Pro Git — Reset Demystified](https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified) — ba cây HEAD, index, working tree và ba mode reset.
4. [git-revert](https://git-scm.com/docs/git-revert), [git-reset](https://git-scm.com/docs/git-reset), [git-commit](https://git-scm.com/docs/git-commit) — reference lệnh, gồm `-m` cho merge commit và `--amend`.
5. [git-reflog](https://git-scm.com/docs/git-reflog) và [git-gc](https://git-scm.com/docs/git-gc) — reflog, thời hạn `gc.reflogExpire`.
6. [git-stash](https://git-scm.com/docs/git-stash), [git-bisect](https://git-scm.com/docs/git-bisect) — stash và bisect, gồm `-u`, `pop`, `apply`, `drop`.
7. [Removing sensitive data from a repository](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository) — GitHub, thứ tự rotate rồi dọn history, giới hạn của việc xoá.
8. [git-filter-repo](https://github.com/newren/git-filter-repo) — tool viết lại history, được Git project khuyến nghị thay cho `filter-branch`.
9. [About push protection](https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection) — chặn secret lúc push.

## Tiếp theo

[Git hooks](./06-hooks.md)
