Bài trước về rebase nhắc tới một "lưới an toàn": dù rebase viết lại SHA, các commit cũ vẫn cứu được. Lưới đó tên là reflog, và nó là lý do thật sự khiến Git ít nguy hiểm hơn danh tiếng. Rất nhiều lập trình viên tin rằng git reset --hard hay git branch -D là hành động một chiều — làm rồi thì mất. Thực tế gần như mọi thao tác "mất commit" đều đảo ngược được, miễn bạn biết reflog tồn tại. Bài này khôi phục thật hai tai nạn kinh điển trong Git 2.39.
Reflog là gì: nhật ký của HEAD, không phải lịch sử commit
Cần phân biệt rõ hai thứ trông giống nhau:
git logđi theo chuỗi parent của commit hiện tại — nó chỉ thấy những commit mà nhánh hiện tại với tới được. Reset đi rồi làgit logkhông còn thấy.git refloglà nhật ký cục bộ ghi mọi lần con trỏHEADdịch chuyển — mỗi commit, reset, checkout, rebase, merge, cherry-pick đều để lại một dòng. Nó không quan tâm commit còn với tới hay không; nó ghi lại nơi HEAD từng đứng.
Đó là điểm mấu chốt: khi bạn reset --hard khỏi một commit, commit đó biến mất khỏi git log nhưng SHA của nó vẫn nằm trong reflog. Và chừng nào một SHA còn nằm trong reflog, object commit tương ứng còn được giữ trong kho — chưa bị git gc thu gom.
git reflog # nhật ký của HEAD; HEAD@{0} là hiện tại, HEAD@{n} là n bước trước
git reflog show main # nhật ký riêng của một nhánh

Hình 1: Reflog ghi lại mọi lần HEAD dịch chuyển, khác với git log chỉ đi theo chuỗi parent. Nhờ vậy một reset --hard hay một nhánh bị xoá đều để lại SHA để khôi phục.
Tai nạn 1: reset --hard làm "mất" hai commit
Dựng một repo với ba commit c1 (fce70b9), c2 (51a3428), c3 (5d52c30). Giờ chạy git reset --hard HEAD~2 — một lệnh vừa dời nhánh lùi hai bước vừa ghi đè working tree, nên rất dễ gây hoảng:
git reset --hard HEAD~2
# HEAD is now at fce70b9 c1 khoi tao -> git log giờ chỉ còn c1
git log chỉ còn thấy c1. Với người chưa biết reflog, c2 và c3 coi như mất trắng. Nhưng:

Hình 2: git reflog vẫn liệt kê 5d52c30 (c3) ở HEAD@{1}. Chạy git reset --hard HEAD@{1} khôi phục nguyên vẹn cả c2 và c3. Bên dưới: xoá nhánh rồi tạo lại từ SHA 6bf7da0 mà Git in ra lúc xoá.
git reflog cho thấy HEAD@{1} chính là 5d52c30 — commit c3 ngay trước khi reset. Một lệnh git reset --hard HEAD@{1} (hoặc git reset --hard 5d52c30) đưa nhánh về đúng chỗ, c2 và c3 sống lại đầy đủ. Không có gì mất cả — chỉ là git log không đi tới đó nữa, còn reflog thì có.
Tai nạn 2: xoá nhầm một nhánh
git branch -D xoá một nhánh kể cả khi nó chưa merge — đúng loại lệnh khiến người ta toát mồ hôi. Nhưng để ý dòng Git in ra khi xoá:
git branch -D thu-nghiem
# Deleted branch thu-nghiem (was 6bf7da0)
Git in luôn SHA của commit mà nhánh vừa trỏ tới: 6bf7da0. Chỉ cần thế là đủ tạo lại nhánh:
git branch thu-nghiem 6bf7da0 # nhánh sống lại y nguyên
Kể cả khi bạn không kịp chép SHA đó, git reflog vẫn còn dòng 6bf7da0 HEAD@{...}: commit: cong viec tren nhanh thu-nghiem từ lúc bạn làm việc trên nhánh — tìm lại được. Commit 6bf7da0 giờ là commit mồ côi (không nhánh nào với tới), nhưng object của nó còn nằm trong kho, sẵn sàng cho một nhánh mới trỏ vào.
Vì sao cứu được: thời hạn giữ reflog
Reflog không giữ mãi mãi — nó có hạn, và biết hạn đó giúp bạn hiểu khi nào lưới an toàn còn hiệu lực:
gc.reflogExpiremặc định 90 ngày: entry reflog cho commit vẫn với tới được (còn nằm trên một nhánh).gc.reflogExpireUnreachablemặc định 30 ngày: entry cho commit không còn với tới (mồ côi, như sau reset/rebase).
Nghĩa là sau một reset --hard, bạn có khoảng 30 ngày để khôi phục trước khi git gc dọn commit mồ côi đi thật. Trong thực tế đó là dư dả — hầu hết "tai nạn" được phát hiện trong vài phút. Cần soi các object mồ côi còn lại, dùng git fsck --lost-found để liệt kê chúng.
Đánh đổi và giới hạn
Reflog là cục bộ mỗi máy — không push, không clone theo. Đây là điểm nhiều người ngã. Nếu bạn reset --hard rồi git push --force, reflog trên máy bạn cứu được commit, nhưng đồng đội clone về sẽ không có reflog đó. Reflog bảo vệ bạn trên chính máy bạn, không phải cả nhóm.
git reset --hard vẫn xoá thay đổi chưa commit. Reflog chỉ theo dõi những gì đã thành commit (đã có SHA). File bạn sửa mà chưa git add/commit thì reset --hard ghi đè và reflog không cứu được — vì chúng chưa bao giờ là một object trong kho. Muốn an toàn, git stash hoặc commit trước khi làm việc nguy hiểm.
Đừng lạm dụng reflog thay cho cẩn thận. Nó là lưới an toàn cho tai nạn, không phải giấy phép để reset --hard bừa. Với thao tác rủi ro trên nhánh quan trọng, tạo một nhánh sao lưu (git branch backup) trước vẫn là thói quen tốt — rõ ràng hơn nhiều so với dò SHA trong reflog sau này.
Ba ý mang về
- Reflog là nhật ký mọi lần
HEADdịch chuyển, khácgit logchỉ đi theo parent. Nhờ nó, mộtreset --hard HEAD~2làm "mất" hai commit vẫn khôi phục nguyên vẹn bằnggit reset --hard HEAD@{1}— đo thật,c2/c3sống lại từ SHA5d52c30. - Nhánh xoá nhầm cũng cứu được:
git branch -Din ra SHA (was 6bf7da0), vàgit branch <tên> <SHA>dựng lại nhánh; SHA còn trong reflog nếu bạn không kịp chép. - Biết giới hạn: reflog giữ 90 ngày (với tới được) / 30 ngày (mồ côi) rồi
gcdọn; nó cục bộ mỗi máy nên không thay được sao lưu chung; và nó không cứu thay đổi chưa commit — hãystash/commit trước việc nguy hiểm.
Nguồn
- Pro Git — Git Tools: RerereData / Maintenance and Data Recovery (mục "Data Recovery" nói về reflog và fsck): https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery
- Git Documentation — git-reflog: https://git-scm.com/docs/git-reflog
Phần sau ta mở nắp kho object: vì sao thư mục .git không phình to vô hạn dù mỗi commit là một snapshot — Git nén các object lại thành packfile với delta, và ta sẽ đo thật mức nén đó.