</>Học Dev
Bài học

Tuần 1 - Ngày 6: Undo và Recovery — Cứu Dữ Liệu "Đã Mất"

Tuần 1 – Ngày 6

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/--hard và 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.

Trngthái:HEADABCmainstagingarea(mtsthayđi)workingdir(thêmthayđikhác)gitresetHEAD~1(tclàresetvB)

--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ử.

Trưcrevert:ABCDmaingitrevertC:ABCDC'main^C'=commitrevertC(thayđingưcviC)
# 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 resetgit revert
Lịch sửXoá commitsThêm commit mới
An toàn với shared branchKHÔNG
Khi nào dùngBranch 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

  1. Ba mode của git reset khá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)
  2. Tại sao git revert an toàn hơn git reset khi undo trên branch đã push?

    Xem đáp án

    git revert tạ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 reset xoá 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ùng revert.

  3. 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ặc git switch -c recovery abc1234 để tạo branch từ đó. Reflog giữ dữ liệu 90 ngày mặc định.

  4. git stashgit stash --include-untracked khác nhau ở điểm nào?

    Xem đáp án

    git stash (mặc định) chỉ stash tracked files — files đã từng được git 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.

  5. Khi nào git reflog khô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


Tiếp theo: Quiz Tổng Kết Tuần 1