잃어버린 커밋을 찾아서 (git reflog, git fsck)

 

어느 날, 개발자 A가 실수로 개발자 B의 PR에 해당 PR의 커밋을 반영하지 않은 브랜치로 force push를 올려 원래 PR의 작업물 일부가 날아가버리는 사고가 발생했다.
개발자 B는 자신의 로컬 브랜치를 찾아 다시 변경 사항을 반영하려 했으나, 로컬 브랜치를 이미 지워버린 뒤였다는 사실을 깨달았다…

개발자 A와 B는 절망에 빠졌지만, git의 놀라운 기능들을 이용하면 이 작업물을 복구할 수 있다고 한다.
지금부터 이 작업물을 복구하는 방법을 알아보자!

 

이 글의 예상 독자

hard reset, 브랜치 삭제 등에 의해 커밋을 잃어버린 사람들

 

 

잃어버린 커밋을 되찾는 방법 3가지

잃어버린 커밋을 되찾는 방법은 3가지가 있다.

  1. 커밋 해시로 복구 브랜치 생성
  2. git reflog 명령을 이용
  3. git fsck 명령을 이용

1번부터 순서대로 전부 실행하는 것이 아니라, 차례대로 읽으며 하나라도 실행이 가능하면 커밋을 되찾을 수 있으며 실행이 불가능하다면 다음 순서로 넘어가면 된다.

1. 커밋 해시로 복구 브랜치 생성

아래 로그와 같이 특정 파일을 만들어 커밋을 하고 이 파일을 수정하여 다시 커밋을 한 히스토리가 있다고 해 보자.

$ git log
commit 73b028ea9707862f18fa8c089b98fc8bd6e0015f (HEAD -> main)
Author: YIHYUN HA <hihyun16@gmail.com>
Date:   Sun Jan 5 14:22:04 2025 +0900

    Update task

commit ef55552fef9ed65ce559e9c4972835b91cb6ecda
Author: YIHYUN HA <hihyun16@gmail.com>
Date:   Sun Jan 5 14:21:24 2025 +0900

    Task complete!

이 상황에서 최근 커밋을 hard reset하고 새로운 커밋을 반영했다.

git reset --hard head^
$ git log
commit c0a72274b3195a7d2754a996bf5c4e760aecf5f3 (HEAD -> main)
Author: YIHYUN HA <hihyun16@gmail.com>
Date:   Sun Jan 5 14:25:53 2025 +0900

    New commit

commit ef55552fef9ed65ce559e9c4972835b91cb6ecda
Author: YIHYUN HA <hihyun16@gmail.com>
Date:   Sun Jan 5 14:21:24 2025 +0900

    Task complete!

그런데 기존 커밋(Update task)에서 필요한 정보가 있다면? 이 상황에서 커밋 해시를 알고 있다면 아래와 같이 커밋 해시를 통해 복구 브랜치를 생성함으로써 삭제된 커밋을 복구할 수 있다. (복구 브랜치는 정식 명칭이 아니다. 이 글에서는 이해를 돕기 위해 해당 표현을 사용한다.)

git branch {브랜치명} {커밋해시}

위의 예시에서 찾고 싶은 커밋 해시를 대입하여 명령을 실행하면 아래와 같은 브랜치가 생긴다.

$ git branch recover-branch 73b028e

$ git branch
* main
  recover-branch

이 브랜치로 체크아웃한 뒤 로그를 확인해보면 커밋 해시를 가리키고 있음을 알 수 있다.

$ git log
commit 73b028ea9707862f18fa8c089b98fc8bd6e0015f (HEAD -> recover-branch)
Author: YIHYUN HA <hihyun16@gmail.com>
Date:   Sun Jan 5 14:22:04 2025 +0900

    Update task

commit ef55552fef9ed65ce559e9c4972835b91cb6ecda
Author: YIHYUN HA <hihyun16@gmail.com>
Date:   Sun Jan 5 14:21:24 2025 +0900

    Task complete!

2. git reflog 명령을 이용

하지만 일반적으로 커밋을 잃어버린 상태에서 커밋 해시를 알고있기는 쉽지 않다. 만약 잃어버린 커밋의 커밋 해시를 알지 못하는 상황이라면, git reflog 명령을 사용할 수 있다.

reflog는 ref + log를 의미하며 git의 ref(브랜치), 그 중에서도 HEAD 브랜치가 가리켰던 커밋 내역을 저장하는 로그이다.

위의 예시에서 커밋 해시를 몰라 복구 브랜치를 생성하지 못한 상황이라고 가정하고 git reflog 명령을 수행하면, 아래와 같은 정보를 출력한다.

$ git reflog
c0a7227 (HEAD -> main) HEAD@{0}: commit: New commit
ef55552 HEAD@{1}: reset: moving to HEAD^
73b028e HEAD@{2}: commit: Update task
ef55552 HEAD@{3}: commit (initial): Task complete!

commit: Update task 라인에 잃어버린 커밋을 가리켰던 내역이 있으며 여기에서 커밋 해시를 확인할 수 있다.

이 커밋 해시를 통해 1번과 동일한 방법으로 복구 브랜치를 생성하면 된다.

3. git fsck

대부분의 사람들이 2번 단계에서 잃어버린 커밋을 찾을 수 있었을 것이라고 생각한다. 하지만, reflog를 임의로 삭제해버린 경우에는 2번의 방법으로도 커밋을 찾을 수 없다.

이 경우에는 마지막 대안으로, git fsck 명령을 이용하는 방법이 있다. fsck 명령은 git 데이터베이스 내 객체의 연결성과 유효성을 검사하는 명령어이다.

$ git fsck --lost-found
Checking object directories: 100% (256/256), done.
dangling commit 73b028ea9707862f18fa8c089b98fc8bd6e0015f

fsck 명령을 --lost-found 옵션과 함께 실행하면 “dangling” 상태의 커밋의 해시를 확인할 수 있다. dangling 상태란 어떤 브랜치도 참조하고 있지 않은 상태로, 이러한 상태의 커밋들 중 우리가 찾고자 하는 커밋이 있을 것이다.

다만 fsck 명령의 결과에서는 reflog의 결과처럼 커밋 메시지를 출력해주지 않기 때문에, 커밋 메시지를 확인하고 싶다면 별도 연계 명령어를 만들어야 한다. 스택 오버플로우에서도 같은 논의를 하고 있기에 링크를 첨부한다.

그리고, 찾고자 하는 커밋이 너무 오래 전의 것이라면 이 방법을 통해서도 찾을 수 없을 수 있는데, 그 이유는 아래에서 다룬다.

 

 

어떻게 이런 일이 가능한 걸까?

우선, 위의 3가지 방법을 통해 당신의 잃어버린 커밋과의 감동적 해후에 성공했길 바란다.

해당 섹션은 Git이 어떻게 데이터를 저장하고 관리하길래 위와 같은 방식으로 커밋을 되찾을 수 있을지에대한 의문을 풀어보기 위해 준비했으니, 원리가 궁금한 사람들이 읽어보길 권장한다.

git 내 객체들의 참조 관계와 HEAD의 역할

Git은 다양한 객체들로 구성되며, 객체들은 아래와 같은 참조 관계를 갖는다.

이번 글은 커밋의 참조에 대한 글이니 Tree와 Blob에 대한 자세한 내용은 생략하고, HEAD, Branch, Commit의 참조 관계를 보자.

HEAD는 현재 브랜치를 가리킨다. .git/HEAD 파일에 아래와 같은 내용이 기록되어 있다.

$ cat .git/HEAD
ref: refs/heads/main

위에서 출력된 경로의 ref 파일을 확인해보면, 아래와 같이 해당 브랜치가 가리키는 커밋의 해시값이 출력된다.

$ cat .git/refs/heads/main
c0a72274b3195a7d2754a996bf5c4e760aecf5f3

git reflog의 원리

위의 내용을 통해 HEAD가 어떤 브랜치를 가리키는지, 이 브랜치가 어떤 커밋을 가리키는지가 계속 파일 형태로 관리된다는 것을 알았다. HEAD의 변경 사항은 git update-ref 라는 명령에 reflog에도 계속 기록된다. 그 덕분에, 특정 커밋이 어떤 브랜치도 참조하고 있지 않은 상태가 되어도 HEAD에서 이를 참조한 적이 단 한 번이라도 있다면 reflog에서 이를 찾을 수 있는 것이다.

이 reflog는 위의 2번 방법에서 실행한 git reflog 명령을 통해 확인할 수 있으며, 파일로는 .git/logs/ 안에 저장되어 있다.

만약 임의로 reflog를 제거해보고 싶다면 이 .git/logs/ 디렉토리를 삭제하면 된다. 삭제 후 다시 reflog 명령을 실행하면, 아무것도 출력되지 않는다.

$ git reflog
# nothing...

git fsck의 원리

위에서 언급했듯, Git은 객체와 이들 간 참조 기반으로 동작한다. fsck는 각 객체의 유효성과 객체 간 연결의 유효성을 검사하는데, 이 과정에서 참조되지 않는(Dangling) 객체들이 있는지도 검사하게 된다.

그러나 위에서 반드시 모든 Dangling 객체를 찾아낼 수 없다고도 언급했는데, 그 이유는 git 또한 내부적으로 gc(garbage collection)을 수행하기 때문이다. 우리가 명시적으로 git gc를 통해 gc를 수행하든, git이 판단 하에 자동으로 gc를 트리거하든 gc가 호출되었을 때 Dangling 객체는 완전히 제거될 수 있다. 이 경우에는, fsck 명령을 통해 잃어버린 커밋을 찾을 수 없게 된다.

 

 

마치며

Git은 아주 잘 추상화된 고수준의 명령어만 이해해도 사용하는 데에 지장이 없다. 그래서, Git이 어떤 식으로 데이터를 저장하고 관리하는지에 큰 관심을 두지는 않는 경우가 많다.

그러나, Git이 객체 간 참조 기반으로 동작한다는 것을 이해하고, 이 객체들을 관리하는 과정에서 어떤 데이터들이 저장되는지를 이해한다면 Git을 더 풍부하게 이용할 수 있다. 이번 글에서 다룬 “잃어버린 커밋을 되찾는 방법” 또한 이에 해당한다.

Git에 대한 기초부터 심화까지, 대부분의 내용은 Pro Git 책에서 확인할 수 있다. Git SCM 문서에서 잘 정리된 Pro Git 책의 내용을 확인할 수 있다. (이 글 또한 대부분 이 문서를 참고하여 작성되었다.)

 

Git

 

git-scm.com

시간날 때 읽어보는 것을 적극 추천한다.