빌드노트
reset 뒤 사라진 로컬 커밋을 reflog에서 찾아 보존하기
reset 뒤 기본 로그에서 사라진 로컬 커밋을 reflog로 찾고 실제 변경을 확인한다. 복구 브랜치 생성 전후의 커밋 ID와 작업 트리를 비교해, 파일을 덮어쓰기 전에 커밋을 보존하는 방법을 설명한다.
git reset으로 브랜치를 과거로 옮긴 뒤 마지막 로컬 커밋이 git log에서 사라졌다면, 먼저 reflog에서 이전 위치를 찾는다. 후보 커밋의 실제 변경을 확인하고 새 복구 브랜치를 만들면, 현재 작업 폴더를 바꾸기 전에 그 커밋을 다시 가리킬 수 있다.
기본 git log는 현재 HEAD에서 이어지는 이력을 보여 준다. 브랜치를 뒤로 옮겨 그 경로에서 빠진 커밋이 로그에 없다는 이유만으로, 커밋 객체까지 삭제됐다고 판단할 수는 없다. reflog는 이 로컬 저장소에서 HEAD와 브랜치 같은 참조가 이동한 기록이다. Git log의 기본 조회 범위 (새 탭에서 열림), Git reflog 공식 문서 (새 탭에서 열림)
reset 행에 나온 커밋은 이동한 뒤의 위치다
동작을 확인하려고 새 독립 저장소에서 note.txt를 v1로 커밋하고, v2로 바꿔 두 번째 커밋을 만들었다. Windows 11, Git 2.45.1.windows.1에서 수행한 합성 실험이다. 두 번째 커밋까지 작업 트리가 깨끗한 것을 확인한 다음, 이 실습 저장소에서만 git reset --hard HEAD~1을 실행했다. --hard는 작업 트리와 인덱스도 바꾸므로 실제 작업에서 복구를 위해 따라 실행할 명령이 아니다. reset의 적용 범위 (새 탭에서 열림)
reset 전 기본 로그에는 ec65a11 update note와 e1ab0e0 base note가 있었다. reset 뒤에는 e1ab0e0 base note만 남았고 파일도 v1이 됐다. 이 상태에서 reflog를 읽었다.
자기 저장소에서 확인할 때는 Git이 설치된 PowerShell 7에서 대상 저장소의 최상위 폴더로 이동해 다음을 실행한다. 첫 명령이 가리키는 경로가 의도한 저장소인지 확인한 뒤 나머지를 읽는다. status에 수정이 있으면 그 상태도 기억해 둔다. 아래 조회 명령은 작업 파일을 복원하거나 덮어쓰지 않는다.
git rev-parse --show-toplevel
git status --short
git log --oneline -3
git reflog show HEAD -n 8
실습의 reflog 출력은 다음과 같았다. 커밋 ID는 실행마다 달라질 수 있다.
e1ab0e0 HEAD@{0}: reset: moving to HEAD~1
ec65a11 HEAD@{1}: commit: update note
e1ab0e0 HEAD@{2}: commit (initial): base note
첫 행의 e1ab0e0은 reset이 끝난 뒤 HEAD가 가리키는 커밋이다. 되찾을 두 번째 커밋은 바로 아래 ec65a11이었다. HEAD@{1}은 HEAD의 직전 기록을 뜻하며, 항상 “잃어버린 커밋”을 뜻하지 않는다. reset 이후 다른 커밋이나 브랜치 전환이 있었다면 번호와 내용이 달라질 수 있다. 최근 8개에 없다는 것만으로 전체 기록에도 없다고 단정하지 말고 조회 범위를 넓혀 본다. reflog의 HEAD 기록과 이전 값 (새 탭에서 열림)
후보의 메시지와 파일 내용을 함께 확인한다
실습에서는 위 출력의 HEAD@{1}을 선택했다. 자기 기록에서 다른 번호가 맞으면 첫 줄의 번호를 바꾼다. PowerShell에 중괄호까지 전달되도록 작은따옴표로 감쌌다. 먼저 전체 커밋 ID를 변수에 담으면 이후 reflog 번호가 바뀌어도 같은 후보를 검사할 수 있다.
$candidate = (git rev-parse 'HEAD@{1}').Trim()
if ($LASTEXITCODE -ne 0) { throw '후보 커밋을 찾지 못했습니다.' }
git show --format=fuller --stat $candidate
git show $candidate -- note.txt
git show "${candidate}:note.txt"
note.txt는 확인하려는 파일의 저장소 루트 기준 경로로 바꾼다. 첫 show로 작성자·시각·메시지와 변경 경로를 보고, 다음 명령으로 해당 파일의 패치를 읽는다. 마지막 명령은 후보 커밋에 저장된 파일 내용을 출력한다. 작업 폴더에 쓰는 명령이 아니다. ${candidate} 표기는 PowerShell 문자열 안에서 변수와 뒤의 콜론을 구분한다. Git show가 보여 주는 객체와 변경 (새 탭에서 열림), 커밋과 경로를 함께 지정하는 문법 (새 탭에서 열림)
실제 패치에는 다음 두 줄이 있었고, 마지막 명령의 출력은 v2였다.
-v1
+v2
후보의 전체 ID는 ec65a1124488c64c73cf7f518b37566d4ebf4053으로 reset 전에 기록한 두 번째 커밋과 같았다. 메시지가 비슷하다는 데서 멈추지 않고, 되찾으려던 변경과 저장된 파일이 맞는지 확인한 것이다. 후보를 해석하지 못하거나 show가 실패하면 브랜치 생성으로 넘어가지 않는다. 특정 경로만 없다는 오류라면 그 커밋의 변경 경로부터 다시 확인한다.
확인한 커밋에 브랜치 이름을 붙인다
원하는 커밋이 맞으면 새 이름으로 보존한다. 다음 블록은 앞에서 설정한 $candidate를 같은 PowerShell 창에서 사용한다. recovery/reset-lost는 아직 사용하지 않는 로컬 브랜치 이름이어야 한다.
git branch recovery/reset-lost $candidate
if ($LASTEXITCODE -ne 0) { throw '복구 브랜치를 만들지 못했습니다.' }
git rev-parse recovery/reset-lost
git branch -v
git status --short
Get-Content -LiteralPath note.txt
git show 'recovery/reset-lost:note.txt'
첫 조회의 전체 ID가 확인한 후보와 같은지 비교한다. 실습에서는 위의 ec65a1124488c64c73cf7f518b37566d4ebf4053이 나왔고, 브랜치 목록은 다음과 같았다.
* main e1ab0e0 base note
recovery/reset-lost ec65a11 update note
별표는 여전히 main에 있었다. status --short는 빈 출력이었고, 작업 폴더의 파일은 v1, 복구 브랜치에서 조회한 파일은 v2였다. 브랜치 생성 전후의 HEAD, 인덱스 항목과 작업 파일의 바이트 해시도 같았다. 이 단계에서 복구한 것은 커밋을 가리키는 브랜치다. 현재 폴더의 파일이 v2로 바뀌지 않아도 실패가 아니다. git branch는 지정한 커밋에 브랜치를 만들며 그 브랜치로 전환하지 않는다. Git branch 공식 문서 (새 탭에서 열림)
같은 브랜치 이름으로 다시 생성하려 했을 때는 종료 코드 128과 already exists 오류가 나왔고, 기존 브랜치 ID는 그대로였다. 자기 저장소에서도 이름이 겹치면 다른 새 이름을 고른다. 덮어쓰는 -f는 이 복구에 필요하지 않다. 파일을 그 시점으로 바꾸거나 현재 브랜치에 변경을 통합하는 일은, 현재 작업의 보존 상태를 확인한 다음 별도로 결정한다.
reflog는 영구 보관함이 아니다
이 방법의 대상은 이미 만들어진 커밋이다. 커밋하지 않은 채 reset --hard로 잃은 수정까지 reflog가 보관해 준다는 뜻은 아니다. 또한 여기서 읽은 기록은 그 로컬 저장소의 기록이다.
공식 문서에는 오래된 reflog 항목을 만료시키는 설정이 있으며, 일반 항목의 기본값은 90일, 현재 끝점에서 도달하지 못하는 항목은 30일이다. 이는 정리 기준이며 그 기간 동안의 복구를 보장하는 약속이 아니다. 기록의 만료와 커밋 객체의 삭제도 같은 사건은 아니다. GC는 브랜치·태그·reflog 등에서 참조하는 객체를 보존하려 하므로, 확인한 커밋에 브랜치를 만들어 두는 데 의미가 있다. reflog 만료 조건 (새 탭에서 열림), GC의 객체 보존 기준 (새 탭에서 열림)
직접 검증한 범위는 두 커밋이 있는 새 저장소, 깨끗한 상태의 hard reset, 후보 내용 확인과 브랜치 생성이다. 만료·GC를 실행하거나 이미 삭제된 객체, 커밋하지 않은 수정의 복구를 시험하지 않았다. 실제 저장소와 원격에는 접근하지 않았다.