Hướng dẫn

git reflog: cách khôi phục commit và nhánh đã mất

Cập nhật: · Skillaria.Top

git reflog là nhật ký tham chiếu cục bộ, ghi lại mọi lần HEAD và nhánh được di chuyển: commit, reset, checkout, rebase, merge. Tìm trong git reflog mục trước khi xảy ra lỗi, ví dụ HEAD@{2}, rồi khôi phục bằng git reset --hard HEAD@{2} hoặc tạo nhánh cứu hộ bằng git branch rescue HEAD@{2}.

git reflog là nhật ký tham chiếu (reference log): Git ghi vào đó mỗi lần HEAD và đầu các nhánh được di chuyển. Bạn tạo commit, chuyển nhánh, chạy reset, rebase hoặc merge — trong reflog xuất hiện một dòng với hash mà con trỏ đã trỏ tới sau thao tác đó. Vì vậy ngay cả một commit mà không nhánh nào còn trỏ tới vẫn có thể tìm lại và khôi phục.

Điều cần nhớ: reflog chỉ tồn tại cục bộ. Nó không được gửi lên máy chủ và không có khi git clone. Nhưng trong kho của chính bạn, nó cứu bạn khỏi gần như mọi lỗi liên quan tới lịch sử.

Cách đọc kết quả git reflog

git reflog
3c4d5e6 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
9a8b7c6 HEAD@{1}: commit: Thêm kiểm tra e-mail
1f2e3d4 HEAD@{2}: commit: Đưa cấu hình vào tệp riêng
3c4d5e6 (HEAD -> main) HEAD@{3}: checkout: moving from feature to main
7b6a5c4 (feature) HEAD@{4}: commit: Bản nháp form đăng nhập

Mỗi dòng là một trạng thái của HEAD, mới nhất ở trên:

  • hash — commit mà HEAD trỏ tới sau thao tác;
  • HEAD@{n} — «HEAD đã ở đâu cách đây n lần di chuyển»; đây là tham chiếu đầy đủ, có thể truyền cho mọi lệnh;
  • mô tả — loại thao tác (commit, reset, checkout, rebase, merge, pull, cherry-pick, commit (amend)) và chi tiết.

Trong ví dụ, hai commit «Đưa cấu hình vào tệp riêng» và «Thêm kiểm tra e-mail» đã được tạo, sau đó reset HEAD~2 lùi nhánh. Trong git log chúng không còn, nhưng trong reflog vẫn còn nguyên.

Reflog của một nhánh cụ thể

Mỗi nhánh có nhật ký riêng. Nó ngắn hơn và hữu ích hơn khi bạn chuyển qua lại giữa nhiều nhánh:

git reflog show feature
git reflog show origin/main   # nhánh từ xa thay đổi thế nào khi fetch

Tham chiếu theo thời gian

Thay vì số thứ tự, có thể chỉ ra một thời điểm — Git sẽ tìm xem con trỏ lúc đó trỏ tới đâu:

git show main@{yesterday}
git diff main@{"2 hours ago"} main
git log -g --since="1 day ago" --oneline

Cờ -g (--walk-reflogs) của git log duyệt theo reflog thay vì theo quan hệ cha con, và cho phép dùng các định dạng cũng như bộ lọc quen thuộc. Để hiển thị ngày giờ, dùng git reflog --date=iso.

Khôi phục commit sau git reset --hard

Tình huống thường gặp nhất: bạn chạy git reset --hard lên sai commit và công việc «biến mất». Thực ra các commit vẫn còn — chỉ là không còn nhánh nào trỏ tới.

  1. Mở nhật ký: git reflog.
  2. Tìm dòng ngay trước reset: moving to ... — đó là trạng thái trước khi xảy ra lỗi. Trong ví dụ trên — HEAD@{1} với hash 9a8b7c6.
  3. Kiểm tra đúng là nó: git log --oneline -5 HEAD@{1}.
  4. Đưa nhánh trở lại: git reset --hard HEAD@{1}.
git reset --hard HEAD@{1}
HEAD is now at 9a8b7c6 Thêm kiểm tra e-mail

Lưu ý: sau mỗi thao tác các số thứ tự dịch chuyển. Thứ từng là HEAD@{1} sau reset sẽ thành HEAD@{2}. Vì vậy nên sao chép hash thay vì số.

Giới hạn quan trọng: reflog chỉ lưu những gì đã commit. Thay đổi chưa commit bị xóa bởi reset --hard hoặc git checkout -- tệp thì không lấy lại được qua reflog. Vẫn còn hy vọng nếu tệp đã được add vào index — xem phần dưới.

Khôi phục nhánh đã xóa

Bạn xóa nhánh bằng git branch -D feature, trong đó có những commit chưa đẩy. Nhật ký của chính nhánh bị xóa cùng nó, nhưng nhật ký HEAD vẫn còn — HEAD đã từng ở trên các commit đó khi bạn làm việc trên nhánh.

git reflog | grep feature
7b6a5c4 HEAD@{4}: commit: Bản nháp form đăng nhập
3c4d5e6 HEAD@{3}: checkout: moving from feature to main

# commit cuối của nhánh là commit mà người ta rời đi
git branch feature 7b6a5c4

Tìm dòng checkout: moving from feature to ...: hash ở dòng ngay bên dưới (cũ hơn) là trạng thái cuối cùng của nhánh. Khi xóa, Git còn in luôn hash ra màn hình: Deleted branch feature (was 7b6a5c4). — nếu terminal còn mở thì chỉ cần cuộn lại.

Hoàn tác rebase thất bại

Rebase viết lại commit, và nếu khi xử lý xung đột bạn chọn sai, kết quả có thể tệ hơn ban đầu. Reflog giữ trạng thái trước khi rebase bắt đầu.

git reflog
e1f2a3b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e1f2a3b HEAD@{1}: rebase (pick): Bản nháp form đăng nhập
c4d5e6f HEAD@{2}: rebase (start): checkout main
7b6a5c4 HEAD@{3}: commit: Bản nháp form đăng nhập

git reset --hard HEAD@{3}

Cần tìm dòng trước rebase (start). Còn có đường tắt: trước các thao tác nguy hiểm (rebase, merge, reset) Git lưu vị trí cũ vào tham chiếu ORIG_HEAD, nên ngay sau rebase có thể dùng git reset --hard ORIG_HEAD. Nhưng ORIG_HEAD bị ghi đè bởi thao tác nguy hiểm tiếp theo, còn reflog lưu toàn bộ lịch sử.

Lấy lại commit trước git commit --amend

--amend không sửa commit mà tạo một commit mới thay vào chỗ đó. Commit cũ vẫn nằm trong reflog:

git reflog
5a6b7c8 HEAD@{0}: commit (amend): Thêm kiểm tra e-mail
9a8b7c6 HEAD@{1}: commit: Thêm kiểm tra e-mail

git diff HEAD@{1} HEAD          # amend đã thay đổi gì
git reset --soft HEAD@{1}       # quay về commit cũ, thay đổi của amend vẫn còn trong index

Khôi phục an toàn: tạo nhánh thay vì reset

Nếu chưa chắc cần commit nào, đừng reset --hard một cách mù quáng. Hãy tạo nhánh cứu hộ — nó không phá gì cả và bảo vệ commit tìm được khỏi bị xóa:

git branch rescue HEAD@{5}
git log --oneline rescue
git switch rescue

Sau đó có thể chuyển các commit từ rescue bằng cherry-pick hoặc gộp chúng, rồi xóa nhánh khi mọi thứ đã trở lại đúng chỗ.

Reflog được lưu bao lâu

Các mục không tồn tại vĩnh viễn. Chúng được dọn bởi git gc, mà Git tự chạy định kỳ. Thời hạn do cấu hình quy định:

Cấu hìnhMặc địnhÝ nghĩa
gc.reflogExpire90 ngàyThời hạn cho các mục có commit đạt được từ các nhánh hiện tại
gc.reflogExpireUnreachable30 ngàyThời hạn cho các mục có commit không đạt được từ nhánh nào — chính là những commit «đã mất»
gc.pruneExpire2 tuầnSau bao lâu các đối tượng không đạt được, không còn tham chiếu nào, bị xóa hẳn

Kết luận thực tế: commit đã mất có thể lấy lại qua reflog trong khoảng một tháng. Đừng trì hoãn khôi phục, và đừng tự chạy git gc --prune=now hay git reflog expire --expire=now --all khi chưa lấy lại được thứ cần thiết.

Khi reflog không giúp: git fsck

Có những trường hợp commit không có trong reflog: ví dụ nó được tạo bởi git stash rồi stash bị xóa bằng git stash drop, hoặc thay đổi chỉ có trong index. Lúc đó tìm các đối tượng «mồ côi»:

git fsck --lost-found
dangling commit 2b3c4d5e6f...
dangling blob 8e9f0a1b2c...

git show 2b3c4d5e6f            # xem nội dung
git branch recovered 2b3c4d5e6f

Các commit mồ côi từ stash đã xóa mang thông điệp «WIP on ...». Các blob mồ côi là nội dung tệp từng được git add nhưng chưa commit. Chúng không có tên tệp, phải nhận diện theo nội dung, nhưng đôi khi nhờ đó cứu được công việc sau reset --hard. Với cờ --lost-found, Git còn sao chép chúng vào .git/lost-found/.

Những gì reflog không làm được

  • Không hoạt động giữa các máy. Nếu đồng nghiệp force push và ghi đè commit của bạn trên máy chủ, hãy tìm trong reflog trên máy đã từng có chúng — ví dụ git reflog show origin/main trên bất kỳ máy nào đã fetch trước khi bị ghi đè.
  • Không thấy thứ chưa commit. Vì vậy lá chắn tốt nhất trước thao tác rủi ro là commit hoặc git stash.
  • Không lưu lịch sử mãi mãi. Thời hạn như trên, và bản clone mới bắt đầu với nhật ký trống.

Thực hành: lá chắn trước thao tác nguy hiểm

Reflog cứu được bạn, nhưng khôi phục qua nó đòi hỏi sự cẩn thận: phải tìm đúng dòng giữa hàng chục dòng tương tự. Đơn giản hơn nhiều là đặt sẵn một dấu mốc. Trước khi rebase, reset lớn hoặc thử nghiệm với lịch sử, tạo một nhánh tạm hoặc thẻ:

git branch backup/before-rebase
git rebase -i main
# có gì đó không ổn
git reset --hard backup/before-rebase
# mọi thứ ổn
git branch -D backup/before-rebase

Chỉ mất một lệnh và không cần tìm hash. Reflog là phương án dự phòng khi bạn nhớ ra việc lưu dấu mốc quá muộn.

Cách tìm nhanh mục cần thiết

Trong một nhật ký dài, bộ lọc giúp rất nhiều. Tìm theo loại thao tác hoặc nội dung thông điệp commit, còn khi nghi ngờ thì xem nội dung:

git reflog | grep -E "reset|rebase \(start\)"
git log -g --grep="kiểm tra" --oneline
git show --stat HEAD@{7}

git log -g kết hợp với --grep tìm trong thông điệp của các commit có trong reflog, còn git show --stat cho biết tệp nào bị ảnh hưởng — nhờ đó dễ nhận ra commit cần tìm, ngay cả khi thông điệp không rõ ràng kiểu «wip».

Tóm lại

git reflog là lưới an toàn của Git: mọi thứ đã commit trong kho của bạn trong vài tuần gần đây đều có thể tìm lại và khôi phục. Quy trình luôn như nhau: mở nhật ký, tìm mục trước khi xảy ra lỗi, kiểm tra bằng git log hoặc git show, rồi tạo nhánh hoặc chạy reset --hard. Restore, reset, revert và khôi phục sau sự cố được thực hành trong khóa học Git.

Câu hỏi thường gặp

Khác nhau giữa git reflog và git log là gì?

git log đi theo quan hệ cha con của commit và hiển thị lịch sử nhánh. git reflog hiển thị HEAD và các nhánh đã di chuyển thế nào trong kho của bạn, kể cả các commit không còn trong lịch sử nhánh.

Reflog có được đưa lên GitHub hay GitLab không?

Không, reflog chỉ nằm trong thư mục .git/logs cục bộ và không được truyền khi push hay clone.

Có thể xóa reflog không?

Có, bằng git reflog expire --expire=now --all rồi git gc --prune=now, nhưng sau đó các commit đã mất không thể khôi phục.

Reflog được lưu ở đâu trên đĩa?

Trong các tệp văn bản .git/logs/HEAD và .git/logs/refs/heads/tên-nhánh, mỗi lần di chuyển là một dòng.

Reflog có hoạt động trong kho bare trên máy chủ không?

Mặc định kho bare không bật reflog (core.logAllRefUpdates), nên không thể trông cậy vào nó trên máy chủ.

Học chủ đề này qua video

Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Git с нуля до уверенного разработчика”.

Đến khóa học