Git을 쓰다 보면 등골이 서늘해지는 순간이 꼭 와요. reset을 잘못 쳐서 하루치 커밋이 통째로 사라진 것 같을 때, 브랜치를 지웠는데 알고 보니 거기 중요한 작업이 있었을 때요. 저도 신입 때 git reset --hard 한 방에 반나절 작업이 날아간 줄 알고 심장이 쿵 내려앉은 적 있어요. 그런데 좋은 소식이 있어요. Git은 웬만해선 진짜로 버리지 않아요. 오늘은 사고가 났을 때 잃은 걸 되찾는 복구 기술을 실제로 날려보고 살려보면서 익혀볼게요.

87.1 커밋을 날려먹었는데 살릴 수 있나요?

상황을 하나 만들어 볼게요. 커밋 세 개가 쌓여 있는데, 실수로 git reset --hard HEAD~2를 쳤다고 해봐요. 이건 "현재 위치에서 두 칸 뒤로 완전히 돌아가라"는 뜻인데, --hard라서 그 사이 커밋과 작업 내용까지 싹 지워요.


$ git log --oneline
afad33f 기능 C 추가
c290d54 기능 B 추가
2da1d76 기능 A 추가
$ git reset --hard HEAD~2
HEAD is now at 2da1d76 기능 A 추가


로그를 다시 보면 B와 C가 감쪽같이 사라졌어요.


$ git log --oneline
2da1d76 기능 A 추가


딱 이 순간이 가슴이 철렁하는 순간이죠. 참고로 reset에는 세기가 세 단계 있어요. --soft는 커밋만 취소하고 변경은 그대로 두고, --mixed(기본값)는 스테이징까지 풀고, 방금 쓴 --hard는 변경 내용까지 싹 지워요. 그러니까 진짜로 파일이 날아가는 건 --hard뿐이에요. 그런데 이 --hard로 날려도 사라진 게 아니에요. Git은 내 HEAD(지금 내가 보고 있는 위치)가 움직인 모든 발자국을 몰래 기록해두거든요. 그 발자국 장부를 보는 명령이 git reflog예요.


$ git reflog
2da1d76 HEAD@{0}: reset: moving to HEAD~2
afad33f HEAD@{1}: commit: 기능 C 추가
c290d54 HEAD@{2}: commit: 기능 B 추가
2da1d76 HEAD@{3}: commit (initial): 기능 A 추가


보이시나요? HEAD@{1}afad33f 기능 C 추가가 멀쩡히 남아 있어요. reset으로 로그에서만 안 보일 뿐, 커밋 자체는 저 해시(afad33f)로 창고에 그대로 있는 거예요.

87.2 reflog로 찾은 커밋은 어떻게 되돌리죠?

이제 살릴 차례예요. 방법은 간단해요. reflog에서 찾은 되돌아갈 지점의 해시로 다시 reset을 하면 돼요. 아까 C까지 있던 상태가 afad33f였으니 그리로 가면 되겠죠.


$ git reset --hard afad33f
HEAD is now at afad33f 기능 C 추가


로그를 확인하면 사라졌던 B와 C가 그대로 돌아와 있어요.


$ git log --oneline
afad33f 기능 C 추가
c290d54 기능 B 추가
2da1d76 기능 A 추가


완벽하게 복구됐죠. 여기서 얻어갈 교훈은 이거예요. reset을 잘못 쳐서 뭔가 사라진 것 같으면 무조건 먼저 git reflog를 쳐보세요. 십중팔구 방금 전 상태의 해시가 거기 남아 있어요. 이 사실 하나만 알아도 reset이 무섭지 않아요.

87.3 브랜치를 통째로 지워버렸어요?

이것도 흔한 사고예요. 작업하던 브랜치를 다 끝난 줄 알고 지웠는데, 알고 보니 아직 main에 안 합친 커밋이 거기 있었던 거죠. 병합 안 된 브랜치를 강제로 지우면 이렇게 나와요.


$ git branch -D feature-login
Deleted branch feature-login (was c0dafa6).


여기서 절대 놓치면 안 되는 게 괄호 안의 was c0dafa6예요. Git이 "이 브랜치가 가리키던 마지막 커밋이 c0dafa6였어"라고 친절하게 알려준 거예요. 이 해시만 있으면 브랜치를 되살릴 수 있어요. 혹시 이 메시지를 놓쳤어도 괜찮아요. git reflog에 그대로 남아 있거든요.


복구는 그 해시를 가리키는 브랜치를 새로 만들면 끝이에요.


$ git branch feature-login c0dafa6
$ git branch
feature-login
* main


git branch 브랜치명 해시는 그 해시 위치에 브랜치를 붙여주는 명령이에요. 지워졌던 feature-login이 커밋까지 통째로 되살아났어요. 브랜치는 사실 특정 커밋을 가리키는 이름표일 뿐이라, 이름표만 다시 붙이면 원래대로 돌아오는 거죠.

87.4 reflog로도 못 살리는 것도 있나요?

솔직하게 말할게요. reflog가 만능은 아니에요. 딱 하나, 한 번도 커밋하지 않은 변경은 못 살려요. reflog는 커밋과 HEAD 이동을 기록하는 장부라서, 아직 git commitgit add도 안 한 채 편집만 하다가 git reset --hardgit restore로 날린 내용은 애초에 기록에 없어요. 그래서 큰 작업을 하다가 불안하면 자잘하게라도 자주 커밋하거나 git stash로 서랍에 넣어두는 습관이 곧 보험이에요.


또 하나, reflog는 영원하지 않아요. Git이 주기적으로 창고를 청소하는데(gc, garbage collection, 쓰레기 수거), 어디서도 가리키지 않는 커밋은 기본 90일이 지나면 정말로 지워져요. 그러니 "며칠 전에 날린 커밋"은 대개 살릴 수 있지만, 몇 달 지난 건 기대하기 어려워요. 사고를 눈치챘으면 미루지 말고 바로 복구하는 게 최선이에요.

87.5 브랜치 없이 커밋했다가 사라진 것 같아요?

이 사고도 은근히 자주 나요. 옛날 커밋을 구경하려고 해시로 이동했다가 거기서 그냥 커밋해버린 경우예요. 옛 커밋으로 이동하면 Git이 이런 경고를 띄워요.


$ git checkout HEAD~1
You are in 'detached HEAD' state. You can look around,
make experimental changes and commit them, and you can
discard any commits you make in this state without
impacting any branches by switching back to a branch.


detached HEAD(분리된 HEAD, 어떤 브랜치에도 붙어 있지 않은 상태)라는 말이에요. 이 상태에서 커밋을 하면 그 커밋은 어떤 브랜치 이름표도 안 달고 붕 떠 있어요. 그러다 git switch main으로 브랜치에 돌아오면, 방금 한 커밋이 화면에서 사라져요. 이름표가 없으니 목록에 안 잡히는 거죠. 하지만 여기까지 읽었으면 답을 아시죠? git reflog에 그 커밋 해시가 남아 있어요. 그 해시로 git branch 새이름 해시를 쳐서 이름표를 붙여주면 붕 떠 있던 커밋이 그대로 살아나요. 원리는 87.3의 브랜치 복구와 똑같아요.

87.6 오늘 정리

사고 복구의 핵심은 딱 하나예요. 뭔가 사라진 것 같으면 일단 git reflog를 쳐라. reset을 잘못 쳤으면 reflog에서 원래 지점 해시를 찾아 git reset --hard 해시로 되돌리고, 브랜치를 지웠으면 삭제 메시지의 was 해시나 reflog의 해시로 git branch 브랜치명 해시를 쳐서 되살리면 돼요. 단, 커밋도 add도 안 한 변경은 못 살리니 큰 작업 전엔 자주 커밋하거나 stash로 안전망을 깔아두세요. 이 감각이 생기면 Git 앞에서 훨씬 담대해져요.