Published on

Git rebase 후 강제푸시 충돌? reflog로 복구하기

Authors

서로 다른 히스토리를 깔끔하게 만들려고 git rebase를 쓰다 보면, 마지막에 git push --force(또는 --force-with-lease)가 필요해지는 순간이 옵니다. 문제는 이 과정에서 원격 브랜치와 충돌하거나, 더 심하게는 내 커밋/동료 커밋이 “사라진 것처럼” 보이는 사고가 종종 발생한다는 점입니다.

이 글은 다음 상황을 전제로 합니다.

  • 로컬에서 rebase를 수행했다.
  • 강제 푸시를 했거나(혹은 하려다) 원격과 히스토리가 어긋났다.
  • 커밋이 날아간 것 같아 당황했다.

핵심은 하나입니다. Git은 대부분의 경우 기록을 완전히 지우지 않습니다. 그리고 그 “되돌릴 수 있는 지점”을 찾는 가장 강력한 도구가 git reflog입니다.

관련해서 강제 푸시 자체가 막히는(보호 규칙) 문제는 아래 글이 더 직접적입니다.

왜 rebase 후 강제푸시가 사고로 이어질까

rebase는 커밋을 “재배치”하는 것이 아니라, 엄밀히는 새 커밋을 다시 만든 뒤 브랜치 포인터를 그쪽으로 옮기는 작업입니다. 그래서 rebase 전후 커밋 해시는 보통 바뀝니다.

  • rebase 전: A - B - C (내 브랜치)
  • rebase 후: A - B' - C' (새 커밋)

이 상태에서 원격에 기존 커밋 B, C가 남아 있으면, 단순 push는 거절됩니다(히스토리 비선형). 그래서 강제 푸시를 하게 되는데, 이때 다음 사고가 흔합니다.

  • 동료가 원격에 추가 커밋을 올렸는데, 내가 --force로 덮어씀
  • 내가 다른 기준점으로 rebase해서 원격과 완전히 다른 히스토리를 만들어버림
  • 실수로 다른 브랜치에 강제 푸시함

이런 상황에서 “복구”는 보통 두 갈래입니다.

  1. 내 로컬에서 사라진 커밋 찾기: reflog가 핵심
  2. 원격에서 사라진 커밋 찾기: 원격 브랜치/다른 클론/CI 체크아웃/PR 기록 등을 활용

이 글은 1번(로컬 복구)에 초점을 맞추되, 원격 복구 팁도 함께 정리합니다.

우선 진정: 지금 상태를 얼려두기(안전장치)

복구 작업에서 가장 위험한 건, 당황해서 또 다른 reset, rebase, gc를 돌려 참조를 더 잃는 것입니다. 먼저 현재 상태를 보존하세요.

# 현재 브랜치 이름 확인
git branch --show-current

# 현재 HEAD를 안전 브랜치로 보관
git switch -c rescue/keep-current-state

# 원격 상태도 가져오되, 머지는 하지 않음
git fetch --all --prune

이제 어떤 실험을 해도 최소한 rescue/keep-current-state는 남습니다.

reflog란 무엇이고, 왜 복구가 가능한가

git reflog는 “브랜치/HEAD가 과거에 가리켰던 커밋”을 로컬에 기록합니다. rebase, reset, checkout 등으로 포인터가 이동한 이력이 남기 때문에, 이전 커밋 해시를 다시 찾아갈 수 있습니다.

중요한 특징:

  • reflog는 로컬 저장소 기준입니다(원격 reflog와 다름).
  • 기본적으로 일정 기간 보관됩니다(환경/설정에 따라 다르지만 보통 수 주).
  • 커밋 객체가 완전히 수거되기 전이라면(가비지 컬렉션 전) 복구 가능성이 큽니다.

상황 1: rebase 직전 상태로 되돌리기

가장 흔한 케이스입니다. rebase를 했는데 결과가 꼬였고, 그냥 rebase 전으로 돌아가고 싶습니다.

1) reflog에서 “rebase 시작 전” 지점 찾기

git reflog --date=iso

출력 예시는 대략 이런 형태입니다.

abc1234 HEAD@{0}: rebase (finish): returning to refs/heads/feature/foo
def5678 HEAD@{1}: rebase (pick): some commit message
9876aaa HEAD@{2}: rebase (start): checkout main
1234bbb HEAD@{3}: checkout: moving from main to feature/foo
...

여기서 보통 복구 목표는 rebase (start) 직전, 즉 HEAD@{3} 같은 지점입니다.

2) 해당 시점으로 하드 리셋

# 예: rebase 시작 전이 HEAD@{3}라면
git reset --hard HEAD@{3}

이렇게 하면 로컬 브랜치는 rebase 이전 상태로 돌아갑니다.

3) 원격도 되돌릴지 판단

  • 원격에 아직 강제 푸시를 안 했다면: 여기서 끝
  • 이미 강제 푸시를 했다면: 원격을 되돌리려면 다시 강제 푸시가 필요

원격을 되돌릴 때는 실수 방지를 위해 --force-with-lease를 우선 고려하세요.

git push --force-with-lease origin feature/foo

--force-with-lease는 “내가 마지막으로 본 원격 상태에서 바뀐 게 있으면 거절”해줘서, 동료 커밋을 덮어쓰는 사고를 줄입니다.

상황 2: 강제푸시로 커밋이 사라진 것처럼 보일 때

예를 들어 내가 feature/foo에서 rebase 후 --force를 했고, 동료가 올린 커밋이 원격에서 사라졌다고 합시다.

이때 해야 할 질문은 둘입니다.

  1. 사라진 커밋이 내 로컬 어딘가에 남아 있는가?
  2. 다른 누군가의 로컬/다른 브랜치/PR 기록에 남아 있는가?

내 로컬에 남아 있는지 확인: reflog + 다른 브랜치 reflog

# HEAD reflog
git reflog --date=iso

# 특정 브랜치 reflog(브랜치 포인터 이동 이력)
git reflog show feature/foo --date=iso

사라진 커밋 메시지나 해시 일부를 기억한다면 grep도 유용합니다.

git reflog --date=iso | grep -i "fix login"

찾았다면 그 커밋으로 새 브랜치를 만들어 보존합니다.

# 예: 찾은 커밋이 deadbee라면
git switch -c rescue/lost-commits deadbee

원격에 다시 살리기: cherry-pick 또는 merge

사라진 커밋을 원래 브랜치에 “그대로” 복구하려면, 보통 아래 중 하나를 선택합니다.

  • cherry-pick: 필요한 커밋만 골라 복구
  • merge: 잃어버린 라인을 통째로 합치기(히스토리 형태는 달라질 수 있음)

예를 들어 rescue/lost-commits에 커밋이 모여 있다면:

# 원래 브랜치로 이동
git switch feature/foo

# 특정 커밋만 복구
git cherry-pick deadbee

복구 후 다시 푸시합니다.

git push origin feature/foo

만약 원격 히스토리를 다시 덮어써야 한다면, 여기서도 --force-with-lease를 우선 고려하세요.

상황 3: 잘못된 브랜치에 강제푸시했다(가장 아찔한 케이스)

예를 들어 main에 잘못 강제 푸시를 했다면, 복구는 “정확한 커밋으로 원격 main 포인터를 되돌리는 것”입니다.

1) 로컬에서 올바른 main 커밋 찾기

  • 내 로컬 origin/main이 아직 이전 커밋을 가리키고 있을 수도 있습니다(단, fetch 전에만).
  • 이미 fetch로 덮였더라도, reflog에 남아 있을 수 있습니다.
# origin/main의 이동 이력도 reflog로 확인 가능(로컬 원격추적 브랜치)
git reflog show origin/main --date=iso

여기서 “강제푸시 이전” 커밋 해시를 확보합니다.

2) 원격 main을 해당 커밋으로 되돌리기

로컬 main을 그 커밋으로 맞춘 뒤 푸시하는 방식이 안전합니다.

# 예: 되돌릴 커밋이 cafe111
git switch main
git reset --hard cafe111

# 원격 main 되돌리기(매우 주의)
git push --force-with-lease origin main

팀 규칙상 main 강제 푸시가 막혀 있다면, 관리자 권한/보호 규칙/긴급 절차가 필요합니다. 이 경우는 위에서 링크한 “보호 규칙” 글을 함께 참고하세요.

reflog로도 못 찾을 때: 마지막 수단들

reflog는 강력하지만 만능은 아닙니다. 특히 다음이면 어려워집니다.

  • 해당 커밋이 내 로컬에 애초에 없었다(동료 커밋)
  • 오래 지나 reflog 만료
  • git gc로 객체가 수거됨

그래도 시도해볼 수 있는 루트가 있습니다.

1) 원격 호스팅의 PR/머지 요청 기록

GitHub/GitLab은 PR(또는 MR)에 커밋이 남아 있는 경우가 많습니다. PR 페이지에서 커밋 해시를 복사해 로컬로 가져올 수 있습니다.

# 커밋 해시를 직접 checkout하거나
git show deadbee

git switch -c rescue/from-pr deadbee

2) 다른 개발자의 로컬 클론

동료가 해당 브랜치를 fetch해 둔 적이 있다면, 동료 로컬에는 reflog/객체가 남아 있을 수 있습니다. 그 경우 동료가 커밋 해시를 알려주거나, 브랜치를 임시로 푸시해주면 복구가 쉬워집니다.

3) git fsck --lost-found로 dangling 커밋 찾기

reflog에 없더라도 객체가 남아 있으면 “dangling commit”으로 발견될 수 있습니다.

git fsck --lost-found

출력에 dangling commit이 보이면 해당 해시를 git show로 확인해 의미 있는 커밋인지 판별합니다.

git show deadbee

의미 있는 커밋이면 브랜치를 만들어 붙잡아 두세요.

git switch -c rescue/dangling deadbee

재발 방지: 강제푸시 사고를 줄이는 운영 습관

1) --force 대신 --force-with-lease를 기본값으로

손이 --force로 가는 순간 사고 확률이 확 올라갑니다.

# 웬만하면 이걸 사용
git push --force-with-lease origin feature/foo

2) rebase 전 “복구용 태그/브랜치”를 자동으로 만들기

rebase 직전에 현재 HEAD를 태그로 찍어두면, reflog를 뒤질 필요가 줄어듭니다.

# 복구용 태그
git tag backup/pre-rebase-2026-02-24

# 또는 복구용 브랜치
git branch backup/pre-rebase

3) 원격 브랜치 보호 + 예외 프로세스

강제푸시를 아예 막는 것도 좋은 전략입니다. 다만 릴리즈 브랜치 정리 등 예외 상황이 있으니, 팀에서 “언제/누가/어떻게” 예외를 허용할지 문서화가 필요합니다.

이 주제는 아래 글과 같이 연결해 읽으면 좋습니다.

4) 충돌 해결보다 중요한 건 “기준점 합의”

rebase를 팀 단위로 자주 한다면, “어떤 브랜치를 기준으로 rebase하는지”를 합의해야 합니다.

  • main 기준으로만 rebase
  • 릴리즈 브랜치 기준으로만 rebase
  • 공유 브랜치에서는 rebase 금지(merge만 허용)

공유 브랜치에서 개인이 임의로 rebase하고 강제푸시하면, 기술적으로는 가능해도 협업 비용이 폭발합니다.

실전 복구 레시피(요약)

아래 순서대로 하면 대부분의 사고는 수습됩니다.

  1. 현재 상태 보존: git switch -c rescue/keep-current-state
  2. reflog 확인: git reflog --date=iso
  3. 되돌릴 지점 선택: HEAD@{n} 또는 커밋 해시
  4. 로컬 복구: git reset --hard HEAD@{n} 또는 git switch -c rescue ...
  5. 원격 반영이 필요하면 git push --force-with-lease
  6. 동료 커밋이 사라졌다면 PR/MR, 다른 클론, git fsck --lost-found까지 확장

rebase와 강제 푸시는 “히스토리를 예쁘게 만드는 도구”이지만, 동시에 “히스토리를 바꾸는 도구”이기도 합니다. 사고가 났을 때 가장 먼저 떠올려야 할 단어는 reflog이고, 두 번째는 --force-with-lease입니다.