0

# Git Deep Dive #4: Reflog & Git Recovery — Commit biến mất khỏi `git log` đã thực sự bị xóa chưa?

Series: Git Deep Dive – Từ Command đến Internal
Part 4: Reflog & Git Recovery


1. Introduction

Ở cuối Part 3, chúng ta để lại một câu hỏi.

Giả sử repository đang có:

A ─── B ─── C ─── D
                  ↑
                 main
                  ↑
                 HEAD

Sau đó vì một lý do nào đó bạn chạy:

git reset --hard HEAD~2

History trở thành:

A ─── B
      ↑
     main
      ↑
     HEAD

Chạy:

git log

và bạn không còn thấy:

C
D

Lúc này phản ứng quen thuộc thường là:

"Hai commit của mình bị xóa rồi?"

Nếu chưa push lên remote và cũng không nhớ commit hash thì sao?

Code mất thật rồi?

Phải viết lại?

Không hẳn.

Thử:

git reflog

Bạn có thể thấy thứ gì đó tương tự:

a82f38c HEAD@{0}: reset: moving to HEAD~2
d71a932 HEAD@{1}: commit: implement payment timeout
c921af1 HEAD@{2}: commit: add payment validation

Commit tưởng như đã "biến mất" vẫn có thể được tìm thấy.

Đây là lúc chúng ta cần hiểu một trong những cơ chế recovery hữu ích nhất của Git:

REFLOG

Trong bài này, chúng ta sẽ tìm hiểu:

  • Reflog thực sự là gì?
  • HEAD@{0}, HEAD@{1}, HEAD@{2} nghĩa là gì?
  • Tại sao git log không thấy commit nhưng git reflog lại thấy?
  • Reachable và unreachable commit là gì?
  • Recover sau reset --hard như thế nào?
  • Recover branch bị delete như thế nào?
  • Recover sau một rebase sai như thế nào?
  • Detached HEAD có làm mất commit không?
  • Khi nào git fsck hữu ích?
  • Reflog có phải backup vĩnh viễn không?
  • Khi nào commit thực sự có nguy cơ biến mất?

Mental model của Part 4:

REF UPDATE
    ↓
REFLOG
    ↓
OLD COMMIT
    ↓
RECOVERY

2. Trước khi hiểu Reflog, hãy quay lại Part 1

Ở Part 1 chúng ta đã biết:

Branch

không phải một container chứa commits.

Branch chỉ là một reference.

Ví dụ:

main
 ↓
 D

với commit graph:

A ─── B ─── C ─── D
                  ↑
                 main

Nếu chạy:

git reset --hard B

điều quan trọng xảy ra là:

Before:

main → D


After:

main → B

Commit D không nhất thiết bị Git xóa ngay lập tức.

Thứ vừa thay đổi trước hết là:

reference

Từ đây xuất hiện một câu hỏi:

Git có nhớ main trước đó từng trỏ tới D không?

Có.

Đó chính là một trong những vai trò của Reflog.


3. Reflog là gì?

reflog là viết tắt của:

reference log

Có thể hiểu đơn giản:

Reflog ghi lại lịch sử thay đổi của references trong local repository.

Ví dụ:

main → A

sau commit:

main → B

sau commit tiếp:

main → C

sau reset:

main → A

Commit graph hiện tại có thể chỉ cho bạn:

A
↑
main

nhưng reflog có thể lưu dấu vết kiểu:

main từng → A
main từng → B
main từng → C
main hiện → A

Có thể hình dung:

Commit Graph
============

A ─── B ─── C
↑
main


Reflog
======

main:
C → A    reset
B → C    commit
A → B    commit

Một bên cho bạn biết:

Repository hiện tại reachable như thế nào?

Một bên cho bạn biết:

Reference này đã di chuyển như thế nào trong local repository?

Đây là hai khái niệm khác nhau.


4. git log và git reflog không trả lời cùng một câu hỏi

Đây là distinction quan trọng nhất của Part 4.

git log

Thông thường bạn dùng:

git log

để xem commit history reachable từ HEAD.

Ví dụ:

A ─── B ─── C
            ↑
           HEAD

Git có thể walk:

C → B → A

git reflog

git reflog

lại tập trung vào:

HEAD/reference đã từng trỏ tới đâu?

Ví dụ:

HEAD@{0} → B
HEAD@{1} → D
HEAD@{2} → C
HEAD@{3} → B

Do đó hoàn toàn có thể xảy ra:

git log

không còn thấy commit D, nhưng:

git reflog

vẫn cho bạn tìm lại D.

Mental model:

git log
   ↓
Commit Graph

git reflog
   ↓
Reference Movement History

5. HEAD@{0} nghĩa là gì?

Giả sử:

git reflog

trả về:

7ac91f2 HEAD@{0}: reset: moving to HEAD~2
9fd712a HEAD@{1}: commit: implement payment
31ad820 HEAD@{2}: commit: validate customer
4ce1021 HEAD@{3}: checkout: moving from main to feature/payment

Các notation:

HEAD@{0}
HEAD@{1}
HEAD@{2}
HEAD@{3}

không phải commit number.

Nó mô tả các giá trị trước đây của reference.

Có thể hiểu:

HEAD@{0}
→ HEAD hiện tại

HEAD@{1}
→ vị trí trước đó của HEAD

HEAD@{2}
→ vị trí trước nữa

HEAD@{3}
→ ...

Git còn hỗ trợ time-based notation.

Ví dụ:

git show 'HEAD@{yesterday}'

hoặc:

git show 'main@{1 week ago}'

Ý nghĩa ở đây là:

Reference local đó từng trỏ tới đâu tại thời điểm tương ứng?

Nó không có nghĩa:

Hãy tìm commit được tạo đúng một tuần trước.

Đây là distinction rất dễ nhầm.


6. Reflog là local

Đây là một đặc tính cực kỳ quan trọng.

Reflog mô tả history của references trong:

local repository

Ví dụ bạn có:

Laptop A
Laptop B
GitLab

Reflog trên Laptop A:

HEAD@{0}
HEAD@{1}
HEAD@{2}

không có nghĩa Laptop B sẽ có cùng reflog.

Có thể hình dung:

Developer A
.git/logs/...
     │
     └── reflog A


Developer B
.git/logs/...
     │
     └── reflog B

Reflog không phải một shared history giống commit graph được push/fetch giữa repositories.

Đây là lý do:

Reflog rất mạnh cho local recovery, nhưng không nên được coi là remote backup.


7. Reflog được lưu ở đâu?

Trong repository thông thường, bạn có thể thấy:

.git/
├── HEAD
├── refs/
├── objects/
└── logs/

Reflog thường liên quan tới:

.git/logs/

Ví dụ:

.git/logs/HEAD

và reflog của branch có thể nằm dưới:

.git/logs/refs/heads/main

Bạn có thể thử:

cat .git/logs/HEAD

Một entry về concept có dạng:

old-object-id new-object-id author timestamp action

Điểm quan trọng ở đây không phải thuộc format.

Mà là nhận ra:

Reflog thực sự lưu old/new object IDs khi refs được cập nhật.

Đó là lý do Git có thể biết:

HEAD trước reset từng ở commit nào.

8. Scenario #1 — Recover sau git reset --hard

Đây là use case kinh điển.

Ban đầu:

A ─── B ─── C ─── D
                  ↑
                 main

Bạn chạy:

git reset --hard B

Bây giờ:

A ─── B
      ↑
     main

Bạn nhận ra:

"Oops. Tôi vẫn cần C và D."

Đầu tiên:

git reflog

Ví dụ:

a123456 HEAD@{0}: reset: moving to B
d789abc HEAD@{1}: commit: implement timeout
c456def HEAD@{2}: commit: add payment validation

HEAD@{1} đang trỏ tới D.

Trước khi làm bất kỳ thao tác destructive nào khác, có thể inspect:

git show HEAD@{1}

hoặc:

git log --oneline HEAD@{1}

Nếu đúng commit cần recover, cách an toàn là tạo branch:

git branch recovery/payment HEAD@{1}

Graph lại có reference tới D:

A ─── B ─── C ─── D
      ↑           ↑
     main    recovery/payment

Commit không còn "lost".


9. Tại sao nên tạo recovery branch trước?

Giả sử tìm thấy:

HEAD@{4}

có commit cần cứu.

Bạn có thể nghĩ:

git reset --hard HEAD@{4}

Nhưng trong recovery, cách an toàn hơn thường là trước tiên tạo một reference mới:

git branch recovery HEAD@{4}

Tại sao?

Bởi vì mục tiêu đầu tiên của recovery nên là:

FIND
 ↓
PROTECT
 ↓
INSPECT
 ↓
RESTORE

thay vì:

FIND
 ↓
immediately reset everything

Sau khi tạo:

recovery → lost commit

commit đó lại có một branch reference rõ ràng.

Sau đó bạn có thể:

git switch recovery

inspect code, diff và quyết định merge/cherry-pick/reset tùy tình huống.

Mental model:

Find SHA
   ↓
Create reference
   ↓
Protect commit
   ↓
Decide recovery strategy

10. Scenario #2 — Xóa nhầm branch

Giả sử:

main

A ─── B ─── C
         \
          D ─── E
              ↑
       feature/payment

Bạn chạy:

git branch -D feature/payment

Branch biến mất.

git branch

không còn:

feature/payment

Có phải D và E lập tức bị delete?

Không nhất thiết.

Bạn có thể kiểm tra:

git reflog

và tìm commit E nếu hoạt động liên quan còn được reflog ghi lại.

Ví dụ tìm được:

9fa82bc

Recover:

git branch feature/payment-recovered 9fa82bc

Kết quả:

A ─── B ─── C
         \
          D ─── E
              ↑
   feature/payment-recovered

Bạn vừa tạo lại một reference tới commit cũ.


11. Branch bị delete không đồng nghĩa commits bị delete

Đây là nơi Part 1 trở nên cực kỳ quan trọng.

Branch:

feature/payment

chỉ là reference:

feature/payment → E

Delete branch:

git branch -D feature/payment

về concept chủ yếu làm mất:

reference → E

chứ không có nghĩa Git ngay lập tức đi vào object database và xóa:

E
D
tree
blob

Ngay sau đó, các objects có thể vẫn tồn tại.

Vấn đề thay đổi thành:

Có còn cách nào tìm được object ID của commit đó không?

Reflog chính là một trong những answer quan trọng nhất.


12. Reachable vs Unreachable — Concept quan trọng nhất sau Reflog

Giả sử:

A ─── B ─── C
            ↑
           main

Từ main, Git có thể đi:

C
↓
B
↓
A

Các commit đó là:

reachable

Có thể hình dung:

main
 ↓
 C
 ↓
 B
 ↓
 A

All reachable

Bây giờ:

git reset --hard B

Branch trở thành:

main
 ↓
 B
 ↓
 A

C không còn reachable từ main.

       C
       
A ─── B
      ↑
     main

Nếu không còn ref/reflog hay root phù hợp nào dẫn tới C, object đó có thể được xem là unreachable trong context tương ứng.

Điểm quan trọng:

UNREACHABLE
≠
IMMEDIATELY DELETED

Đây là distinction quan trọng nhất để hiểu Git recovery.


13. Reachability giống Garbage Collection Root

Nếu bạn từng làm Java, concept này khá quen thuộc.

Trong JVM:

GC Roots
   ↓
Object A
   ↓
Object B

Object reachable từ GC Root được giữ lại.

Một object không còn reachable có thể trở thành candidate cho Garbage Collection.

Git cũng có một mental model tương tự.

Refs
 │
 ├── branches
 ├── tags
 ├── remote-tracking refs
 └── ...
       ↓
    Commits
       ↓
     Trees
       ↓
     Blobs

Reflogs cũng có vai trò trong việc giữ lịch sử object có thể được tìm thấy/protected trong các maintenance operations.

Có thể nghĩ:

REFERENCE ROOTS
      ↓
REACHABLE OBJECTS

và:

no useful reference path
      ↓
UNREACHABLE OBJECTS
      ↓
eventually eligible for cleanup

So sánh với Java GC không hoàn toàn giống implementation của Git, nhưng đây là một mental model khá hữu ích.


14. Scenario #3 — Recover sau Rebase sai

Ở Part 3 chúng ta đã biết:

rebase
=
replay commits

Giả sử ban đầu:

      D ─── E
     /
A ─── B ─── C

Bạn thực hiện rebase và kết quả không như mong muốn:

A ─── B ─── C ─── D' ─── E'

Các commit cũ:

D
E

không còn nằm trên current branch history.

Nhưng HEAD reflog có thể ghi lại vị trí trước khi rebase.

git reflog

Ví dụ:

e91ac12 HEAD@{0}: rebase (finish): returning to refs/heads/feature
4ac781e HEAD@{1}: rebase (pick): implement payment
...
8bc129a HEAD@{5}: rebase (start): checkout main
71de221 HEAD@{6}: commit: original feature tip

Nếu:

71de221

là state trước rebase, trước tiên có thể bảo vệ nó:

git branch before-rebase 71de221

Sau đó compare:

git log --oneline --graph --all

hoặc:

git diff before-rebase feature

rồi mới quyết định cách restore.


15. ORIG_HEAD — Một recovery hint khác

Ngoài reflog, Git còn có một ref đặc biệt:

ORIG_HEAD

Một số command làm thay đổi HEAD theo cách đáng kể như:

merge
rebase
reset

có thể ghi lại vị trí trước operation vào ORIG_HEAD.

Ví dụ sau một reset, bạn có thể kiểm tra:

git show ORIG_HEAD

hoặc:

git log -1 ORIG_HEAD

Mental model:

Before dangerous-ish operation

HEAD → C

       ↓ reset

HEAD      → A
ORIG_HEAD → C

Nhưng không nên xây toàn bộ recovery strategy chỉ dựa vào ORIG_HEAD.

Reflog thường cho bạn lịch sử phong phú hơn:

HEAD@{0}
HEAD@{1}
HEAD@{2}
...

trong khi ORIG_HEAD chủ yếu hữu ích như một pointer tới state trước một số operations gần đây.


16. Scenario #4 — Commit trong Detached HEAD

Ở Part 1 chúng ta đã nhắc tới:

Detached HEAD

Ví dụ:

git switch --detach <commit>

HEAD trỏ trực tiếp tới commit:

HEAD
 ↓
 C

thay vì:

HEAD
 ↓
main
 ↓
 C

Sau đó bạn commit:

A ─── B ─── C ─── D
                  ↑
                 HEAD

nhưng không có branch:

branch → D

Sau đó bạn switch về main:

git switch main

bây giờ:

A ─── B ─── C
            ↑
           main

            D

Commit D có thể không còn reachable từ branch.

Nhưng reflog của HEAD có thể giúp tìm lại vị trí đó.

git reflog

Tìm commit D, rồi:

git branch recovered-detached-work <D_SHA>

Kết quả:

A ─── B ─── C ─── D
            ↑     ↑
           main   recovered-detached-work

Đây là lý do detached HEAD không tự động có nghĩa:

"Code của bạn chắc chắn sẽ mất."

Nhưng nếu tạo commit trong detached HEAD, hãy tạo branch nếu muốn giữ work lâu dài.


17. HEAD@{n} không phải HEAD~n

Hai syntax nhìn khá giống nhau nhưng hoàn toàn khác.

HEAD~2

Commit ancestry

Ví dụ:

A ─── B ─── C ─── D
                  ↑
                 HEAD
HEAD~1 = C
HEAD~2 = B
HEAD~3 = A

Nó đi theo parent relationship trong commit graph.


HEAD@{2}

Reflog history

Nó có nghĩa gần như:

HEAD từng có value nào ở hai reflog entries trước?

Ví dụ bạn switch branch:

main → feature → main → hotfix

Reflog có thể ghi:

HEAD@{0} → hotfix
HEAD@{1} → main
HEAD@{2} → feature

Do đó:

HEAD~2

và:

HEAD@{2}

có thể trỏ tới hai commit hoàn toàn khác nhau.

Mental shortcut:

~
=
commit ancestry


@{}
=
ref history

18. @{-1} — Quay lại branch trước

Có một notation rất tiện:

git switch -

tương tự concept của:

@{-1}

@{-1} đại diện cho branch/commit đã checkout trước current one.

Ví dụ:

main
 ↓
feature/payment
 ↓
hotfix

Bạn đang ở:

hotfix

thì previous checkout có thể được tham chiếu qua:

@{-1}

Đây không hoàn toàn là cùng loại notation với:

HEAD@{1}

nên đừng nhầm:

HEAD@{1}
→ previous HEAD reflog value

@{-1}
→ previous checkout branch/commit

19. git fsck — Khi reflog không đủ

Có trường hợp bạn không dễ tìm commit qua:

git reflog

nhưng object vẫn còn tồn tại trong object database.

Git cung cấp:

git fsck

để kiểm tra connectivity và validity của objects.

Một command hữu ích trong recovery investigation:

git fsck --unreachable

hoặc trong một số trường hợp:

git fsck --no-reflogs --unreachable

Git có thể report:

unreachable commit <object-id>

Bạn có thể inspect:

git show <object-id>

Nếu đó là commit cần tìm:

git branch recovery/<name> <object-id>

Một lần nữa:

find object
    ↓
inspect
    ↓
create reference

20. dangling và unreachable có giống nhau không?

Không nên coi hai từ này hoàn toàn đồng nghĩa.

Git có thể report:

unreachable commit ...

hoặc:

dangling commit ...

unreachable nói về object không reachable từ các reference nodes đang được xét.

dangling là object tồn tại nhưng không được trực tiếp sử dụng theo connectivity mà git fsck đang phân tích.

Đối với recovery, điều quan trọng hơn là:

Object còn tồn tại trong database không?

Nếu còn object ID:

git show <sha>

vẫn có thể inspect được.

Sau đó tạo reference:

git branch recovery <sha>

để giữ lại nó.


21. git fsck --lost-found

Git còn có:

git fsck --lost-found

Command này có thể ghi dangling objects vào:

.git/lost-found/

Ví dụ commit objects:

.git/lost-found/commit/

Đây có thể là fallback khi recovery phức tạp hơn.

Tuy nhiên trong các tình huống thông thường như:

reset nhầm
rebase nhầm
detached HEAD

workflow nên bắt đầu đơn giản:

git reflog
   ↓
git show <candidate>
   ↓
git branch recovery <candidate>

Chỉ khi đó không đủ mới đi sâu hơn vào object database với git fsck.


22. Reflog có phải backup không?

Không.

Đây là misconception nguy hiểm.

Reflog là:

local reference history

không phải:

permanent backup system

Reflog entries có thể expire.

Theo Git configuration mặc định, reachable reflog entries thường có expiration window dài hơn, trong khi entries liên quan tới commits không còn reachable từ current tip có thể expire sớm hơn.

Các default phổ biến của Git là:

gc.reflogExpire
→ 90 days

gc.reflogExpireUnreachable
→ 30 days

Các giá trị này có thể được config lại.

Vì vậy:

"I can always recover with reflog"

là một assumption sai.

Đúng hơn nên nghĩ:

Reflog cung cấp một recovery window rất hữu ích, nhưng không phải guarantee vĩnh viễn.


23. Vậy commit khi nào thực sự biến mất?

Giả sử:

A ─── B ─── C
            ↑
           main

sau reset:

A ─── B
      ↑
     main

      C

Nếu C vẫn được reflog/reference bảo vệ, Git có lý do để giữ nó.

Theo thời gian:

reference removed
      ↓
reflog entry expires
      ↓
object becomes unprotected/unreachable
      ↓
repository maintenance / GC
      ↓
object may eventually be pruned

Đây là lý do chúng ta không nên nói:

reset = delete commit

Mental model chính xác hơn:

RESET
 ↓
move ref
 ↓
commit may become unreachable
 ↓
reflog may still reference it
 ↓
later expiration / GC may remove it

Phần cuối của lifecycle này sẽ được đào sâu trong:

Part 6 — Git Garbage Collection, Packfile & Repository Maintenance


24. Đừng chạy git gc --prune=now khi đang recovery

Nếu bạn vừa làm mất commit và đang cố recover, đây không phải lúc để "dọn repository".

Đặc biệt tránh chạy theo hướng:

git gc --prune=now

mà không hiểu consequences.

--prune=now có thể loại bỏ loose unreachable objects không dựa trên grace period thông thường, và Git documentation cũng cảnh báo mode này làm tăng risk trong một số tình huống concurrent writes.

Trong recovery, mindset nên là:

STOP destructive operations
        ↓
inspect reflog
        ↓
protect candidate commits
        ↓
then repair history

Không phải:

repository đang rối
       ↓
clean everything

25. Recovery Playbook

Khi nghi ngờ mất commit, có thể sử dụng playbook sau.

Step 1 — Đừng panic và đừng tiếp tục rewrite history

Tránh ngay lập tức chạy thêm:

git reset --hard
git rebase
git gc

Step 2 — Kiểm tra current state

git status
git log --oneline --graph --decorate --all

Step 3 — Kiểm tra Reflog

git reflog

Hoặc:

git reflog show HEAD

Step 4 — Inspect candidate

git show <sha>

hoặc:

git log --oneline <sha>

Step 5 — Protect commit

git branch recovery/<name> <sha>

Ví dụ:

git branch recovery/payment-before-reset 9fd712a

Step 6 — Compare

git diff main recovery/payment-before-reset

hoặc:

git log --graph --oneline --decorate --all

Step 7 — Chọn recovery strategy

Có thể:

merge
cherry-pick
reset
restore branch

tùy mục tiêu.


Step 8 — Nếu reflog không tìm thấy

Đi sâu hơn:

git fsck --unreachable

rồi inspect candidate objects.

Mental model:

STOP
 ↓
FIND
 ↓
INSPECT
 ↓
PROTECT
 ↓
RECOVER

26. Một ví dụ thực hành từ đầu đến cuối

Tạo repository thử nghiệm:

mkdir git-recovery-lab
cd git-recovery-lab

git init

Tạo commit A:

echo "A" > demo.txt
git add demo.txt
git commit -m "commit A"

Commit B:

echo "B" >> demo.txt
git add demo.txt
git commit -m "commit B"

Commit C:

echo "C" >> demo.txt
git add demo.txt
git commit -m "commit C"

Kiểm tra:

git log --oneline

Bạn có:

C
B
A

Bây giờ cố tình:

git reset --hard HEAD~2

git log:

A

B và C "biến mất".

Bây giờ:

git reflog

Tìm state trước reset.

Ví dụ:

abc1234 HEAD@{1}: commit: commit C

Inspect:

git show abc1234

Recover:

git branch recovered-history abc1234

Kiểm tra:

git log --oneline --graph --decorate --all

Bạn sẽ thấy:

      B ─── C
     /     ↑
A ───     recovered-history
↑
main

Bạn vừa tự recover history mà không cần remote backup.


27. Java Developer Mental Model

Nếu bạn là Java Backend Developer, có thể liên hệ Git recovery với object reachability trong JVM.

Java

GC Root
  ↓
Object A
  ↓
Object B

Nếu không còn path:

GC Root    Object C
              ↑
         unreachable

C trở thành candidate cho GC.


Git

refs/heads/main
       ↓
     Commit C
       ↓
     Commit B
       ↓
     Commit A

Nếu branch bị move:

refs/heads/main
       ↓
     Commit B
       ↓
     Commit A


     Commit C

C có thể không còn reachable từ branch.

Nhưng Git còn có:

reflog

có thể giữ dấu vết tới previous ref values trong một khoảng thời gian.

Do đó mental model gần đúng:

Java

Root → reachable object → retained


Git

Ref/Reflog → reachable/protected object → retained

Đây không phải cùng một GC implementation, nhưng analogy này giúp reasoning về lifecycle của Git objects.


28. Production Perspective

Reflog rất hữu ích, nhưng trong production workflow bạn không nên xây safety strategy dựa hoàn toàn vào:

"nếu có gì thì reflog cứu"

Một repository quan trọng vẫn nên có:

Remote repository
+
Protected branches
+
Pull/Merge Request
+
Code review
+
CI checks
+
Backup / retention strategy

Ví dụ:

Developer
   ↓
feature/payment
   ↓
Merge Request
   ↓
CI
   ↓
main
   ↓
release tag
   ↓
artifact

Nếu developer reset nhầm local feature:

reflog

rất hữu ích.

Nhưng nếu production artifact, source repository hoặc critical history bị mất ở infrastructure level, reflog trên laptop của một developer không phải disaster recovery strategy.


29. Reflog và CI/CD

Giả sử production đang chạy:

payment-service:9fd712a

với:

9fd712a

là commit SHA.

Nếu developer rewrite local branch:

A ─── B ─── C

thành:

A ─── B ─── C'

commit:

C

có thể không còn trong latest branch history.

Nhưng production artifact vẫn có thể đang được trace tới:

C

Đây là lý do trong CI/CD, nên lưu metadata như:

commit SHA
build number
artifact version
image digest
deployment version

Git recovery và production rollback là hai vấn đề liên quan nhưng không giống nhau.

Git Recovery
→ recover source history

Deployment Rollback
→ recover runtime version

Database Recovery
→ recover persistent state

Một Senior Backend/SRE cần phân biệt rõ ba layer này.


30. Common Mistakes

Mistake #1 — Nghĩ git log không thấy nghĩa là commit đã bị xóa

Không đúng.

not visible from current history

không đồng nghĩa:

object deleted from database

Hãy kiểm tra:

git reflog

Mistake #2 — Nghĩ Reflog nằm trên GitLab/GitHub

Local reflog của bạn là local state.

Không nên assume:

push
→ reflog được upload

Mistake #3 — Dùng reset --hard liên tục khi đang recover

Bạn có thể làm tình huống khó reasoning hơn.

Recovery nên ưu tiên:

inspect
before
mutate

Mistake #4 — Tìm thấy SHA nhưng không tạo reference

Nếu tìm thấy lost commit:

abc1234

hãy cân nhắc bảo vệ ngay:

git branch recovery abc1234

thay vì chỉ ghi SHA ra đâu đó rồi tiếp tục thao tác.


Mistake #5 — Coi Reflog là permanent backup

Reflog entries có expiration.

Unreachable objects có thể cuối cùng bị garbage collected.


Mistake #6 — Chạy cleanup khi đang recovery

Các command cleanup/prune không phải công cụ đầu tiên khi đang cố cứu unreachable work.


31. Reflog Cheat Sheet

Mục tiêu Command
Xem HEAD reflog git reflog
Xem reflog cụ thể git reflog show <ref>
Xem commit tại reflog entry git show HEAD@{n}
Xem previous value của branch git show main@{1}
Xem state theo thời gian git show 'main@{yesterday}'
Inspect ORIG_HEAD git show ORIG_HEAD
Tạo recovery branch git branch recovery <sha>
Xem toàn bộ graph git log --graph --oneline --decorate --all
Tìm unreachable objects git fsck --unreachable
Tìm objects bỏ qua reflogs khi tính reachability git fsck --no-reflogs --unreachable
Ghi dangling objects vào lost-found git fsck --lost-found

32. Recovery Decision Tree

            COMMIT "MẤT"
                 │
                 ▼
          git log --all
                 │
          ┌──────┴──────┐
          │             │
        Found        Not Found
          │             │
          ▼             ▼
      create ref     git reflog
                        │
                 ┌──────┴──────┐
                 │             │
               Found        Not Found
                 │             │
                 ▼             ▼
           git show SHA      git fsck
                 │             │
                 ▼             ▼
          create branch    inspect objects
                 │             │
                 └──────┬──────┘
                        ▼
                     RECOVER

Điều cần nhớ:

DON'T PANIC
     ↓
DON'T CLEAN
     ↓
FIND
     ↓
PROTECT
     ↓
RECOVER

33. Mental Model cần nhớ

Nếu chỉ nhớ một diagram của Part 4, hãy nhớ:

Branch
  │
  ▼
Commit D
  │
  ▼
Commit C
  │
  ▼
Commit B
  │
  ▼
Commit A

Sau reset:

Branch
  │
  ▼
Commit B
  │
  ▼
Commit A


Commit D
  │
  ▼
Commit C

Nhưng:

Reflog
  │
  └──────────────► Commit D

nên recovery vẫn có thể thực hiện.

Sau khi tạo branch:

recovery
   │
   ▼
Commit D
   │
   ▼
Commit C

Commit lại được bảo vệ bằng một reference rõ ràng.

Mental shortcut:

REFLOG
=
WHERE THE REF USED TO POINT

và:

UNREACHABLE
≠
DELETED

34. Senior Developer Mindset

Junior mindset thường là:

"Command nào cứu commit?"

Senior mindset nên là:

Commit còn tồn tại không?

Nó còn reachable từ ref nào không?

Reflog còn giữ previous ref value không?

Tôi có object ID không?

Tôi có nên tạo recovery branch trước không?

History này local hay shared?

Remote có copy của commit không?

CI/CD có artifact được build từ commit đó không?

Có database/runtime state liên quan không?

Điều này thay đổi Git từ:

collection of commands

thành:

state + references + object graph

Và đó chính là mục tiêu của series này.


35. Key Takeaways

Sau Part 4, bạn nên nhớ:

Reflog là gì?

Local history of reference updates

git log cho biết gì?

Commit history reachable từ revision đang xét

git reflog cho biết gì?

Reference trước đây từng trỏ tới đâu

HEAD~2 là gì?

Đi hai parent generations trong commit graph

HEAD@{2} là gì?

Prior value trong HEAD reflog

Branch bị delete có xóa commits ngay không?

Không nhất thiết.

Reset có xóa commit ngay không?

Không.
Nó có thể làm commit không còn reachable từ branch.

Unreachable có nghĩa là deleted?

Không.

Reflog có phải backup vĩnh viễn?

Không.

Recovery workflow?

FIND
 ↓
INSPECT
 ↓
PROTECT
 ↓
RECOVER

36. Kết luận

Bốn phần đầu của series bây giờ bắt đầu nối với nhau thành một mental model hoàn chỉnh.

Part 1 — Git Object Model

Blob
Tree
Commit
Branch
HEAD

↓

Part 2 — Git Index

HEAD
 ↓
INDEX
 ↓
WORKING TREE

↓

Part 3 — History Manipulation

reset  → MOVE
revert → REVERSE
rebase → REPLAY
merge  → COMBINE

↓

Part 4 — Recovery

REF MOVE
   ↓
UNREACHABLE COMMIT
   ↓
REFLOG
   ↓
RECOVERY

Đến đây chúng ta đã trả lời được câu hỏi từ Part 3:

Sau git reset --hard, commit biến mất khỏi git log đã thực sự bị xóa chưa?

Câu trả lời là:

Không nhất thiết.

Branch có thể không còn trỏ tới commit đó, nhưng reflog có thể vẫn giữ dấu vết giúp chúng ta tìm lại object ID và tạo reference mới.

Nhưng điều này lại tạo ra một câu hỏi khác:

Nếu local history có thể bị rewrite, vậy chuyện gì xảy ra khi history đó đã được push lên remote và developer khác cũng đang sử dụng nó?

Đó là lúc Git không còn là bài toán của một developer.

Nó trở thành bài toán distributed collaboration.


Git Deep Dive #5: Remote, Push & Git Security

Ở Part 5 chúng ta sẽ đi từ:

Local Repository
       │
       │ push / fetch
       ▼
Remote Repository

và tìm hiểu:

  • origin thực sự là gì?
  • Local branch và remote-tracking branch khác nhau thế nào?
  • git fetch thực sự thay đổi cái gì?
  • git push thực sự gửi gì?
  • git-receive-pack đóng vai trò gì?
  • Fast-forward push là gì?
  • Tại sao Git reject non-fast-forward push?
  • git push --force nguy hiểm ở đâu?
  • --force-with-lease bảo vệ chúng ta như thế nào?
  • Signed commits/tags giải quyết vấn đề gì?
  • git-shell dùng để làm gì?
  • Branch protection liên quan thế nào tới Git security?
  • Git authentication và commit identity có phải cùng một thứ không?

Mental model của Part 5 sẽ là:

LOCAL REFS
    │
    │ fetch / push
    ▼
REMOTE REFS
    │
    ▼
DISTRIBUTED HISTORY

Sau Part 5, chúng ta sẽ quay lại nơi series bắt đầu:

.git/objects

để trả lời câu hỏi cuối cùng:

Git có hàng nghìn, hàng triệu objects như vậy thì tại sao repository không tăng kích thước vô hạn?

Đó sẽ là:

Part 6 — Git Garbage Collection, Packfile & Repository Maintenance.


References


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí