콘텐츠로 건너뛰기

git push -f로 날려버린 원격 커밋과 로컬폴더 통째로 복구하기

git push -f로 날려버린 원격 커밋과 로컬폴더 통째로 복구하기

개발을 하다 보면 간혹 등골이 오싹해지는 순간을 마주하곤 한다. 오늘 내가 겪은 일이 딱 그랬다.

로컬에서 작업 중 꼬인 부분을 해결하려다 원격 저장소(origin main)에 강제 푸시(-f) 명령어를 날렸는데, 아차 하는 순간 원격의 기존 커밋들이 모두 덮어씌워지며 사라졌다.
엎친 데 덮친 격으로 로컬의 프로젝트 폴더까지 완전히 삭제되어 git reflog조차 쓸 수 없는 최악의 상황이 발생했다.

로컬 폴더도 없고, 깃허브도 덮어씌워진 노답 상태에서 예전 커밋 히스토리와 코드를 완벽하게 복구해낸 과정을 기록으로 남겨둔다. 나와 같은 실수를 저지른 누군가에게 이 글이 심폐소생술이 되기를 바란다.

복구의 핵심

“Git은 파일과 히스토리를 웬만하면 정말로 지우지 않는다.”

원격 브랜치가 덮어써졌다고 해서 이전 커밋 데이터가 즉시 파쇄되는 것은 아니다. 단지 그 커밋들을 가리키는 ‘연결 고리(포인터)’가 끊겨 눈에 보이지 않을 뿐, 깃허브 서버 어딘가에는 유령처럼 살아 숨 쉬고 있다.

따라서 이 복구 작업의 핵심은 “강제 푸시를 하기 직전의 최신 커밋 ID(SHA-1 40자리 hash)를 찾아내는 것”이다. 이것만 찾으면 100% 복구가 가능하다.

깃허브의 유령 커밋을 찾아서 심폐소생술인 셈이다.

Three people dressed as ghosts with sunglasses, sitting on a vintage car, showing thumbs up.
자네, 우릴 찾았는가? 후후 – 유령 Commits

단계별 복구 과정

1단계: 사라진 커밋 ID(SHA) 찾아내기

로컬 파일이 다 날아갔기 때문에 깃허브 웹이나 알림을 통해 흔적을 찾아야 한다. 가장 확실한 방법은 GitHub Events API를 조회하는 것이다. 브라우저 주소창에 아래와 같이 입력한다.

https://api.github.com/repos/[본인깃허브_ID]/[레포지토리이름]/events

페이지를 열어 Ctrl + F (맥은 Cmd + F)를 누른 뒤, PushEvent나 이전에 작성했던 커밋 메시지를 검색한다. 뒤지다 보면 덮어쓰기 직전 상태의 sha (40자리 문자열)를 발견할 수 있다.

내 경우 주소창을 통해 강제 푸시 전 커밋 상세 페이지 주소에서 아래의 ID를 확보할 수 있었다.

  • 확보한 커밋 ID: 2932db990c9d7b4d2f8f23bbe27a70abdfcd5540

주소창에 https://github.com/[ID]/[레포]/commit/[커밋ID]를 쳤을 때 This commit does not belong to any branch...라는 경고가 뜨더라도 코드와 히스토리가 보인다면 복구 준비는 끝난 것이다.

정말… 이것을 찾았을때 감격에 겨웠다

2단계: 레포지토리 원본 주소로 다시 클론(Clone) 받기

작업 공간을 새로 마련해야 하므로 비어있는 디렉토리에서 레포지토리 원본 주소로 클론을 받는다. (※ 주의: 커밋 상세 페이지 주소가 아닌 레포지토리 고유 주소로 받아야 한다.)

$git clone [https://github.com/](https://github.com/)[본인_깃허브_ID]/[레포지토리_이름].git$ cd [레포지토리_이름]

3단계: 숨어있는 커밋 강제로 땡겨오기 (git fetch)

이 부분이 마술 같은 순간이다. 깃허브 서버 구석에 유령처럼 남아있는 과거의 커밋 ID를 명시하여 로컬로 강제 다운로드(fetch)한다.

$ git fetch origin 2932db990c9d7b4d2f8f23bbe27a70abdfcd5540

명령어가 성공하면 서버에 고립되어 있던 과거 히스토리와 파일들이 내 로컬 환경으로 복원된다.

4단계: FETCH_HEAD로 이동 후 브랜치 강제 재생성

가져온 커밋 상태를 바탕으로 로컬의 main 브랜치를 강제로 다시 맞추는 작업을 진행한다.

1. 다운로드된 커밋 위치(FETCH_HEAD)로 체크아웃

$ git checkout FETCH_HEAD

2. 기존의 꼬인 main 브랜치를 날리고, 현재 살려낸 커밋 기준으로 main 브랜치를 강제 재생성

$ git checkout -B main

(참고: 환경에 따라 git switch -B main을 사용할 수도 있으나, 구버전이거나 환경 지원이 안 될 경우 error: unknown switch 'B'가 발생하므로 전통적인 git checkout -B를 사용하는 것이 확실하다.)

이 단계를 마치면 터미널의 브랜치 표시가 [main]으로 정상 변경되며, 과거의 커밋 로그(git log)들이 한 줄도 빠짐없이 롤백되어 살아난 것을 볼 수 있다. 당연히 유실되었던 파일들도 모두 제자리로 돌아온다.

5단계: 원격 저장소 복구 완료 (git push -f)

로컬이 완벽하게 과거의 정상 상태로 돌아왔으니, 끊어졌던 원격 저장소의 포인터를 다시 이어줄 차례다. 마지막으로 깃허브 원격에 다시 강제 푸시를 날려 덮어씌운다.

$ git push origin main -f

마치며

푸시를 완료한 후 깃허브 레포지토리에 들어가 새로고침을 하니, This commit does not belong... 경고 문구는 깔끔하게 사라지고 이전의 모든 커밋 히스토리와 코드, 소중한 잔디들이 완벽하게 복구되었다.

강제 푸시(-f)는 늘 신중해야 하지만, 혹여나 로컬과 원격이 동시에 날아가는 최악의 재앙이 닥치더라도 커밋 ID만 살아있다면 Git의 구조적 특성 덕분에 언제든 심폐소생술이 가능하다는 것을 깊이 깨달은 하루다.

오늘의 식은땀 흘린 기억을 거울삼아 앞으로는 더욱 안전하게 Git을 관리해야겠다.

0 글이 마음에 드시면 하트를 눌러주세요! 행복한 고민이 됩니다!

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다