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 log không còn thấy.
  • git reflog là nhật ký cục bộ ghi mọi lần con trỏ HEAD dị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

Ảnh chụp đoạn mã shell nền tối minh hoạ reflog là nhật ký mọi lần HEAD dịch chuyển làm lưới an toàn của Git, mỗi lần HEAD đổi vị trí Git ghi một dòng vào reflog gồm commit reset checkout rebase merge, git reflog xem nhật ký của HEAD và git reflog show main xem nhật ký riêng một nhánh, HEAD@{0} là hiện tại HEAD@{n} là n bước trước, tai nạn thường gặp git reset --hard HEAD~2 lùi hai commit ghi đè working tree khiến c2 c3 tưởng biến mất nhưng SHA cũ vẫn trong reflog nên git reset --hard HEAD@{1} quay lại đúng chỗ, cứu cả nhánh đã lỡ tay xoá git branch -D thu-nghiem Git in luôn SHA was 6bf7da0 rồi git branch thu-nghiem 6bf7da0 tạo lại nhánh từ SHA mồ côi commit mồ côi còn trong kho tới khi git gc dọn

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:

Ảnh chụp bảng kết quả đo thật nền tối chạy git 2.39, reset --hard HEAD2 rồi cứu bằng reflog, trước tai nạn có 5d52c30 c3 51a3428 c2 fce70b9 c1, chạy git reset --hard HEAD2 báo HEAD is now at fce70b9 c1 khoi tao khiến c2 c3 mất, chạy git reflog thấy fce70b9 HEAD@{0} reset moving to HEAD~2 rồi 5d52c30 HEAD@{1} commit c3 sua loi quan trong SHA còn đây 51a3428 HEAD@{2} commit c2 them tinh nang fce70b9 HEAD@{3} commit initial c1 khoi tao, chạy git reset --hard HEAD@{1} báo HEAD is now at 5d52c30 c3 khôi phục nguyên vẹn c2 c3, phần hai xoá nhánh rồi tạo lại từ SHA mồ côi git branch -D thu-nghiem báo Deleted branch thu-nghiem was 6bf7da0 rồi git reflog thấy 6bf7da0 HEAD@{1} commit cong viec tren nhanh thu-nghiem sau đó git branch thu-nghiem 6bf7da0 nhánh sống lại, phần ba vì sao cứu được reflog giữ entry lâu gc.reflogExpire 90 ngày cho entry còn với tới được gc.reflogExpireUnreachable 30 ngày cho commit mồ côi reflog là cục bộ mỗi máy không push clone theo

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.reflogExpire mặ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.reflogExpireUnreachable mặ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ề

  1. Reflog là nhật ký mọi lần HEAD dịch chuyển, khác git log chỉ đi theo parent. Nhờ nó, một reset --hard HEAD~2 làm "mất" hai commit vẫn khôi phục nguyên vẹn bằng git reset --hard HEAD@{1} — đo thật, c2/c3 sống lại từ SHA 5d52c30.
  2. Nhánh xoá nhầm cũng cứu được: git branch -D in 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.
  3. Biết giới hạn: reflog giữ 90 ngày (với tới được) / 30 ngày (mồ côi) rồi gc dọ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ãy stash/commit trước việc nguy hiểm.

Nguồn

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 đó.