Published on

Git rebase 후 강제푸시로 PR 깨졌을 때 복구법

Authors

서로 다른 브랜치 히스토리를 깔끔하게 만들려고 git rebase를 쓴 뒤, 무심코 --force로 푸시했다가 PR이 깨지는 경험은 흔합니다. 대표 증상은 다음과 같습니다.

  • PR에서 outdated 코멘트가 대량 발생하거나, 리뷰 스레드가 맥락을 잃음
  • GitHub가 “This branch is out-of-date” 혹은 대규모 충돌을 표시
  • CI가 갑자기 실패하거나, 이미 머지된 것처럼 보이는데 실제 diff가 이상함
  • 팀원이 로컬에서 git pull 하다가 히스토리 꼬임

문제의 본질은 단순합니다. rebase는 커밋을 “재작성”하고, 강제푸시는 원격 브랜치의 커밋 그래프를 통째로 바꿉니다. PR은 특정 커밋들의 관계(부모-자식)를 기준으로 리뷰/코멘트/비교를 구성하기 때문에, 커밋 SHA가 바뀌면 PR이 정상적으로 이어지지 않을 수 있습니다.

이 글에서는 (1) 어떤 상황에서 PR이 깨지는지, (2) 이미 강제푸시를 해버렸을 때 어떻게 복구하는지, (3) 다음부터 PR을 덜 깨지게 만드는 운영 습관을 순서대로 정리합니다.

관련 글도 함께 참고하면 좋습니다.

PR이 깨지는 전형적인 시나리오

1) rebase로 커밋 SHA가 바뀜

rebase는 기존 커밋을 “다시 만들어” 새 커밋으로 올립니다. 내용은 같아 보여도 SHA가 달라집니다.

  • 변경 전: A - B - C (feature)
  • 변경 후: A - B' - C' (feature)

GitHub PR은 커밋 단위로 코멘트/리뷰를 매핑하는데, BB'는 완전히 다른 커밋이라 코멘트가 끊기거나 outdated 처리됩니다.

2) git push --force로 원격 브랜치가 덮어써짐

원격의 feature 브랜치가 기존 커밋을 더 이상 가리키지 않게 되면, PR의 “기준”이 흔들립니다. 특히 팀원이 같은 브랜치를 공유하거나, PR이 오래 열려 있던 경우 파급이 큽니다.

3) base 브랜치가 이미 많이 진행된 상태

base(예: main)가 빠르게 바뀌는 레포에서 rebase 후 강제푸시를 하면, PR 비교 대상이 바뀌면서 충돌/리뷰맥락이 더 크게 흔들립니다.

먼저 해야 할 것: 손대기 전에 현재 상태를 백업

이미 PR이 깨졌다면, 가장 먼저 “현재 원격 상태를 로컬에 보존”해야 합니다. 복구 과정에서 실수로 또 덮어쓰는 사고를 막기 위해서입니다.

# 원격 정보 최신화
git fetch --all --prune

# 현재 원격 feature 브랜치를 로컬에 백업 브랜치로 저장
git checkout -b backup/feature origin/feature

# 안전을 위해 현재 HEAD SHA도 기록
git rev-parse HEAD

이제 어떤 조치를 하더라도 backup/feature는 남습니다.

복구 전략 선택 가이드

PR 복구는 크게 3가지 전략 중 하나로 갑니다.

  1. reflog로 강제푸시 이전 커밋을 찾아 원격을 되돌린다
  2. PR은 유지하되, 새 브랜치를 만들어 PR을 새로 연다(또는 기존 PR을 대체)
  3. 강제푸시 없이 PR을 살리는 방식으로 “되감기”가 아니라 “전진”으로 정리한다

상황별 추천은 다음과 같습니다.

  • 팀원이 같은 브랜치를 이미 많이 사용했다: 2 또는 3 권장
  • PR 리뷰 코멘트를 최대한 살리고 싶다: 1이 가장 직접적(단, 팀 합의 필요)
  • 이미 main에 머지되었거나, PR이 너무 꼬였다: 2가 빠름

방법 1: reflog로 강제푸시 이전 상태로 되돌리기

핵심은 “강제푸시 직전의 커밋 SHA”를 찾아 그 지점으로 원격 브랜치를 되돌리는 것입니다. 로컬에서라면 reflog가 거의 항상 힌트를 줍니다.

1) 강제푸시 이전 커밋 찾기

# feature 브랜치로 이동
git checkout feature

# reflog 확인 (최근 이동/리베이스/리셋 기록)
git reflog --date=iso

출력에서 다음 같은 단서를 찾습니다.

  • rebase (start) / rebase (finish)
  • reset: moving to ...
  • 강제푸시 직전 HEAD로 보이는 SHA

예를 들어 reflog에 이런 라인이 있었다고 합시다.

  • abc1234 HEAD@{3}: rebase (finish): returning to refs/heads/feature
  • def5678 HEAD@{4}: checkout: moving from main to feature

이때 “원래 PR이 가리키던 커밋”이 def5678일 수도 있고, rebase 시작 직전 커밋이 따로 있을 수도 있습니다. 보통은 rebase 시작 전 HEAD를 찾으면 됩니다.

2) 해당 커밋으로 브랜치 되돌리기

# 예시: 되돌릴 타겟 SHA가 def5678라고 가정
git reset --hard def5678

3) 원격에 안전하게 강제푸시하기

여기서 중요한 포인트는 --force 대신 --force-with-lease를 쓰는 것입니다. 이 옵션은 “내가 마지막으로 본 원격 상태”에서만 덮어쓰도록 보호막을 제공합니다.

git push --force-with-lease origin feature

이렇게 하면 PR이 “원래 커밋 그래프”로 돌아가면서 리뷰 맥락이 상당 부분 복구됩니다.

4) 팀원이 이미 새 히스토리를 받아갔다면

이 경우 되돌리기는 팀원 로컬을 더 혼란스럽게 만들 수 있습니다. 되돌리기 전 팀에 공지하고, 팀원들은 다음처럼 맞추는 편이 안전합니다.

# 로컬 변경사항이 없다면(또는 별도 백업했다면)
git fetch origin

git checkout feature

git reset --hard origin/feature

로컬 작업이 있었다면 rebasecherry-pick으로 옮겨야 하므로, 팀원별로 상황이 달라집니다.

방법 2: 깨진 PR을 “복구”하려 하지 말고 새 PR로 정리

리베이스 강제푸시로 이미 리뷰가 산산조각 났고, 되돌리기까지 하면 더 큰 혼란이 예상된다면 새 브랜치로 PR을 다시 여는 게 비용 대비 효과가 좋습니다.

1) 현재 원격 feature를 기준으로 새 브랜치 생성

git fetch origin

# 현재 원격 feature 상태에서 새 브랜치 생성
git checkout -b feature-v2 origin/feature

# 새 브랜치 푸시
git push -u origin feature-v2

2) 새 PR 생성 후, 기존 PR에는 연결 정보 남기기

기존 PR을 닫기 전에 다음을 남깁니다.

  • 새 PR 링크
  • 왜 새 PR로 갈아탔는지(강제푸시로 히스토리 변경)
  • 리뷰어가 봐야 할 핵심 변경점

GitHub에서는 PR 본문에 Fixes #번호 같은 자동 연결도 가능하지만, 여기서는 단순히 링크를 남기는 것이 가장 확실합니다.

방법 3: 강제푸시 없이 PR을 살리는 “전진형” 정리

PR이 열려 있는 동안 히스토리를 예쁘게 만들겠다고 rebase를 반복하면, 리뷰 경험이 계속 망가집니다. 대신 “이미 공개된 브랜치의 커밋은 되도록 재작성하지 않는다”는 원칙으로 운영하면 PR이 덜 깨집니다.

1) base 브랜치 변경분은 merge로 흡수

# feature 브랜치에서
git fetch origin

git merge origin/main

# 충돌 해결 후 일반 push
git push origin feature

이 방식은 커밋 그래프가 지저분해질 수 있지만, PR 리뷰 코멘트가 안정적으로 유지됩니다.

2) 이미 rebase를 해버렸다면, 원격은 건드리지 말고 새 브랜치로 푸시

로컬에서 rebase를 했더라도, 기존 원격 브랜치에 강제푸시하지 않고 새 브랜치로 올리면 피해가 줄어듭니다.

# rebase된 로컬 feature를 새 브랜치로 분기
git checkout -b feature-rebased

git push -u origin feature-rebased

이후 PR을 새로 열거나, 기존 PR을 유지해야 한다면 팀 규칙에 따라 선택합니다.

PR이 “깨진 것처럼 보이는데” 사실은 정상인 경우

다음은 복구 시도 전에 확인할 만한 체크리스트입니다.

1) GitHub UI 캐시/비교 기준 문제

가끔 GitHub가 비교를 갱신하는 데 시간이 걸려 “충돌”이 과장되어 보이기도 합니다. git fetch 후 로컬에서 실제 머지 가능 여부를 확인합니다.

git fetch origin

git checkout feature

git merge --no-commit --no-ff origin/main

머지가 깔끔하게 되면, UI 표시만 일시적으로 이상했을 가능성도 있습니다. 머지를 진행하지 않을 거라면 중단합니다.

git merge --abort

2) PR base 브랜치가 잘못 설정됨

예: main으로 열어야 하는데 develop으로 열려 있으면 diff가 이상해집니다. GitHub에서 base 브랜치를 변경해 정상화되는지 먼저 봅니다.

재발 방지: 강제푸시를 하더라도 안전장치 걸기

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

습관만 바꿔도 사고가 크게 줄어듭니다.

# 절대 이렇게 하지 말고
git push --force

# 이렇게 하세요
git push --force-with-lease

--force-with-lease는 누군가가 원격 브랜치에 새 커밋을 올렸는데 내가 그걸 모른 채 덮어쓰는 상황을 막아줍니다.

2) 보호 브랜치 규칙 적용

GitHub Branch protection에서 다음을 켜면 강제푸시로 PR이 날아가는 일을 줄일 수 있습니다.

  • Require pull request reviews
  • Require status checks
  • Restrict who can push
  • (가능하면) force push 제한

3) PR 열린 브랜치에서는 rebase를 “마지막에만”

팀 합의가 있다면 다음 원칙이 실무적으로 안정적입니다.

  • PR 진행 중: merge로 최신화, 일반 push
  • 머지 직전: 필요 시 1회 rebase 또는 squash(리뷰 종료 후)

실전 복구 예시: 강제푸시로 PR 충돌이 폭발했을 때

상황:

  • feature/login PR이 열려 있음
  • 로컬에서 rebase origin/main 수행
  • git push --force로 원격 덮어씀
  • PR에 충돌이 수십 개로 표시되고 리뷰 코멘트가 전부 outdated

복구 플로우(리뷰를 살리는 방향):

# 1) 원격 상태 백업
git fetch --all --prune
git checkout -b backup/login origin/feature/login

# 2) reflog에서 rebase 전 커밋 찾기
git checkout feature/login
git reflog --date=iso

# 3) 예: rebase 직전 SHA가 1a2b3c4라고 가정
git reset --hard 1a2b3c4

# 4) 안전 강제푸시로 원격 되돌리기
git push --force-with-lease origin feature/login

이후 PR 화면이 정상화되는지 확인하고, 팀원들에게는 “원격이 되돌아갔으니 로컬을 reset --hard origin/feature/login으로 맞춰달라”고 안내합니다.

마무리

git rebase 자체가 나쁜 도구는 아닙니다. 다만 PR이 열려 있고 여러 사람이 보고 있는 브랜치에서 rebase 후 강제푸시는 “히스토리 재작성”을 공유 공간에 그대로 반영하는 행위라, PR 맥락(리뷰/코멘트/비교)을 깨뜨리기 쉽습니다.

이미 깨졌다면

  • 되돌릴 가치가 있는 PR이면 reflog로 이전 커밋을 찾아 --force-with-lease로 복구
  • 팀 혼란이 예상되면 새 브랜치, 새 PR로 정리

이 두 가지가 가장 현실적인 해법입니다. 다음부터는 --force-with-lease를 기본으로 하고, PR 진행 중에는 “전진형(merge 기반)”으로 운영하면 같은 사고를 크게 줄일 수 있습니다.