Tuần 1 - Ngày 6: Undo và Recovery — Cứu Dữ Liệu "Đã Mất"
Mục tiêu học tập
- Phân biệt
git reset --soft/--mixed/--hardvà khi nào dùng - Sử dụng
git revertđể undo an toàn trên shared branch - Dùng
git restoređể bỏ thay đổi trong working dir/staging - Cứu commit "bị mất" bằng
git reflog - Dùng
git stashđể lưu work-in-progress tạm thời
1. Hiểu trạng thái trước khi undo
Trước khi undo, luôn biết mình đang undo CÁI GÌ:
git status # trạng thái working dir và staging
git log --oneline -5 # 5 commits gần nhất
git diff # thay đổi chưa staged
git diff --staged # thay đổi đã staged
2. git reset — Di chuyển HEAD (và branch pointer)
git reset di chuyển HEAD và branch pointer về một commit cũ hơn. Ba mode khác nhau ở chỗ what happens to staging area và working directory.
--soft — Giữ staging + working dir nguyên
git reset --soft HEAD~1
# HEAD di chuyển về B
# Staging area: GIỮ NGUYÊN (cộng với thay đổi từ commit C)
# Working directory: GIỮ NGUYÊN
# Kết quả: C "bị undo" nhưng nội dung vẫn staged → dễ commit lại
Dùng khi: muốn undo commit cuối nhưng giữ code để commit lại khác đi (ví dụ: chia commit lớn thành nhỏ).
--mixed (default) — Giữ working dir, bỏ staged
git reset HEAD~1
# hoặc
git reset --mixed HEAD~1
# HEAD di chuyển về B
# Staging area: XOÁ staged changes (nội dung C unstaged)
# Working directory: GIỮ NGUYÊN
# Kết quả: C undo, code vẫn còn trong files nhưng chưa staged
Dùng khi: muốn sắp xếp lại những gì staged, bắt đầu stage từ đầu.
--hard — Xoá cả staged lẫn working dir
git reset --hard HEAD~1
# HEAD di chuyển về B
# Staging area: XOÁ
# Working directory: XOÁ (file trở về trạng thái của B)
# ⚠️ KHÔNG THỂ UNDO bằng Ctrl+Z — thay đổi bị mất
# (trừ khi dùng git reflog để lấy lại commit hash)
Dùng khi: muốn bỏ hoàn toàn thay đổi, quay về commit cũ.
Tóm tắt reset
Command HEAD Staging Working Dir
git reset --soft HEAD~1 ← KEEP KEEP
git reset --mixed HEAD~1 ← CLEAR KEEP
git reset --hard HEAD~1 ← CLEAR CLEAR
3. git revert — Undo an toàn cho shared branch
git revert KHÔNG xoá commit cũ. Thay vào đó, nó tạo một commit mới với thay đổi ngược lại. An toàn vì không viết lại lịch sử.
# Revert commit cụ thể
git revert abc1234
# Git tạo commit mới "Revert 'feat: add feature X'"
# Revert commit gần nhất
git revert HEAD
# Revert một range
git revert HEAD~3..HEAD # revert 3 commits gần nhất
# Revert mà không auto-commit (để chỉnh message)
git revert --no-commit abc1234
git revert --no-commit def5678
git commit -m "revert: remove feature X and Y (security issue)"
# Revert merge commit (cần chỉ định -m parent number)
git revert -m 1 <merge-commit-hash>
# -m 1 = giữ theo parent 1 (thường là main branch)
Reset vs Revert
git reset | git revert | |
|---|---|---|
| Lịch sử | Xoá commits | Thêm commit mới |
| An toàn với shared branch | KHÔNG | Có |
| Khi nào dùng | Branch cá nhân, chưa push | Đã push lên shared branch |
4. git restore — Bỏ thay đổi file
# Bỏ thay đổi trong working dir (file về trạng thái của staging hoặc HEAD)
git restore src/app.js
# ⚠️ KHÔNG THỂ UNDO — thay đổi bị mất vĩnh viễn (không qua trash)
# Unstage file (đưa về unstaged, giữ thay đổi trong working dir)
git restore --staged src/app.js
# Restore file về trạng thái của một commit cụ thể
git restore --source=HEAD~2 src/app.js
git restore --source=abc1234 src/app.js
# Bỏ tất cả thay đổi trong working dir
git restore .
# ⚠️ CỰC KỲ NGUY HIỂM — mất tất cả uncommitted changes
5. git reflog — "Thám tử" lịch sử HEAD
reflog ghi lại tất cả vị trí mà HEAD đã từng ở, kể cả sau git reset --hard, bao gồm cả commits bị "mất".
git reflog
# output:
# abc1234 HEAD@{0}: commit: feat: add payment
# def5678 HEAD@{1}: reset: moving to HEAD~1 ← commit này "bị mất"
# 9a8b7c6 HEAD@{2}: commit: fix: resolve null ptr
# 1f2e3d4 HEAD@{3}: checkout: moving from feature/auth to main
# Lấy lại commit "bị mất" bằng reset --hard
git reset --hard def5678 # quay lại commit đó
# Hoặc tạo branch từ commit cũ
git switch -c recovery def5678
Cứu commit sau git reset --hard
# Tình huống: vô tình reset --hard xoá commit quan trọng
git reset --hard HEAD~3 # oops!
# Cứu bằng reflog
git reflog
# abc1234 HEAD@{1}: commit: ... ← commit muốn lấy lại
git reset --hard abc1234 # khôi phục!
# hoặc cherry-pick
git cherry-pick abc1234
Reflog giữ dữ liệu 90 ngày mặc định (expired entries bị garbage collect).
6. git stash — Lưu work-in-progress tạm thời
git stash là "ngăn kéo tạm" — cất code đang làm dở để switch sang việc khác.
# Cất tất cả thay đổi (staged + unstaged)
git stash
git stash push -m "WIP: payment integration" # với message
# Cất cả untracked files
git stash --include-untracked
git stash -u # shorthand
# Xem danh sách stashes
git stash list
# stash@{0}: WIP on feature/auth: abc1234 feat: add login
# stash@{1}: WIP: payment integration
# Áp dụng stash gần nhất (giữ trong stash list)
git stash apply
# Áp dụng stash cụ thể
git stash apply stash@{1}
# Áp dụng và xoá khỏi stash list
git stash pop
git stash pop stash@{1}
# Xoá stash cụ thể
git stash drop stash@{0}
# Xoá tất cả stashes
git stash clear
# Xem nội dung một stash
git stash show -p stash@{0}
# Tạo branch từ stash
git stash branch feature/resume-work stash@{0}
Kịch bản thực tế
# Đang làm feature/auth dở, lead gọi fix bug urgent trên main
# 1. Cất work dở
git stash push -m "WIP: JWT token implementation"
# 2. Switch sang main và fix
git switch main
git switch -c hotfix/login-500
# ... fix bug ...
git commit -m "fix: handle null session in login"
git switch main && git merge hotfix/login-500
# 3. Quay lại feature và lấy lại work
git switch feature/auth
git stash pop # lấy lại WIP code
7. Checklist: Tôi nên dùng gì?
Muốn bỏ thay đổi chưa commit trong working dir?
→ git restore <file>
Muốn unstage file?
→ git restore --staged <file>
Muốn undo commit CUỐI (chưa push, branch personal)?
→ git reset --soft HEAD~1 (giữ code, staged)
→ git reset --mixed HEAD~1 (giữ code, unstaged)
→ git reset --hard HEAD~1 (xoá cả code)
Muốn undo commit trên shared branch (đã push)?
→ git revert <commit-hash>
Lỡ reset --hard xoá code?
→ git reflog → tìm hash → git reset --hard <hash>
Muốn tạm gác work dở?
→ git stash (và git stash pop sau)
Câu hỏi ôn tập
-
Ba mode của
git resetkhác nhau ở điều gì liên quan đến staging area và working directory?Xem đáp án
Cả 3 mode đều di chuyển HEAD (và branch pointer) về commit được chỉ định. Khác biệt ở what happens to staged/working changes:
--soft: Giữ nguyên staging area + working directory (changes từ commit bị undo trở thành staged)--mixed(default): Xoá staging area, giữ working directory (changes thành unstaged)--hard: Xoá cả staging area lẫn working directory (changes bị mất vĩnh viễn)
-
Tại sao
git revertan toàn hơngit resetkhi undo trên branch đã push?Xem đáp án
git reverttạo commit mới với thay đổi ngược lại — không viết lại lịch sử, chỉ thêm vào. Mọi người pull về sẽ nhận commit revert bình thường.git resetxoá commits khỏi lịch sử — sau đó force push sẽ làm lịch sử remote và local của đồng nghiệp phân kỳ, gây confusion và mất dữ liệu. Nguyên tắc: đã push lên shared branch → chỉ dùngrevert. -
Sau
git reset --hard HEAD~2, làm sao lấy lại 2 commits bị xoá?Xem đáp án
Dùng
git reflogđể tìm hash của commits bị xoá. Reflog ghi lại tất cả vị trí HEAD đã ở — kể cả sau--hard reset. Tìm hash commit muốn khôi phục (ví dụabc1234), sau đó:git reset --hard abc1234để quay về, hoặcgit switch -c recovery abc1234để tạo branch từ đó. Reflog giữ dữ liệu 90 ngày mặc định. -
git stashvàgit stash --include-untrackedkhác nhau ở điểm nào?Xem đáp án
git stash(mặc định) chỉ stash tracked files — files đã từng đượcgit add. Untracked files (files mới chưa add lần nào) bị bỏ lại trong working directory.git stash --include-untracked(hoặc-u) stash cả untracked files. Thêm-ađể stash cả ignored files. -
Khi nào
git reflogkhông còn giúp được nữa?Xem đáp án
Hai trường hợp: (1) Reflog entries hết hạn — mặc định 90 ngày cho reachable commits và 30 ngày cho unreachable commits. Sau đó garbage collect xoá objects. (2) Repository mới clone — reflog là local, không được push lên remote. Clone về sẽ có reflog trống. Cách phòng ngừa: push branches quan trọng lên remote để tránh phụ thuộc hoàn toàn vào reflog.
Bài tập thực hành
mkdir undo-lab && cd undo-lab
git init
git config user.name "Test" && git config user.email "test@test.com"
# 1. Tạo lịch sử
echo "v1" > app.js && git add . && git commit -m "v1"
echo "v2" >> app.js && git add . && git commit -m "v2"
echo "v3" >> app.js && git add . && git commit -m "v3"
git log --oneline
# 2. Reset --soft
git reset --soft HEAD~1
git status # v3 content vẫn staged
git log --oneline # chỉ còn 2 commits
# 3. Commit lại
git commit -m "v3 re-committed"
# 4. Reset --hard (xoá 1 commit)
git reset --hard HEAD~1
git log --oneline # mất commit v3
# 5. Dùng reflog lấy lại
git reflog # tìm hash của commit v3
git reset --hard HEAD@{1} # quay lại
git log --oneline
# 6. Thực hành stash
git switch -c feature/test
echo "WIP code" >> app.js
git stash push -m "WIP: test feature"
git status # clean
git switch main
# ... giả sử fix bug ở đây ...
git switch feature/test
git stash pop # lấy lại WIP
git status # thấy thay đổi trở lại
# 7. Revert (không xoá lịch sử)
git add . && git commit -m "feat: add something"
git revert HEAD # tạo commit revert
git log --oneline # thấy cả 2 commits còn đó
Tài liệu tham khảo chính thức
- git reset documentation
- git revert documentation
- git reflog documentation
- git stash documentation
- Undoing things in Git
Tiếp theo: Quiz Tổng Kết Tuần 1