Published on

Git rebase 후 강제푸시? PR 안전 복구 7단계

Authors

서로 다른 팀원이 같은 브랜치를 만지거나, PR 올린 뒤에 git rebase로 히스토리를 정리하다가 git push --force를 해버리면 PR이 갑자기 이상해집니다.

  • 리뷰 코멘트가 달린 커밋이 사라진 것처럼 보임
  • PR diff가 통째로 바뀌거나, 이미 머지한 커밋이 다시 보임
  • CI가 엉뚱한 커밋에서 돌고, 충돌이 폭발

하지만 대부분의 경우 “커밋은 사라진 게 아니라, 브랜치 포인터가 옮겨졌을 뿐”이라서 복구가 가능합니다. 이 글은 PR을 최대한 안전하게 살리면서 복구하는 7단계를 제시합니다.

참고: 원격에 이미 공유된 브랜치에서는 rebase 자체보다, 그 뒤의 강제푸시가 사고의 트리거가 되는 경우가 많습니다. 강제푸시는 금기라기보다, 절차와 보호장치 없이 하면 위험한 도구에 가깝습니다.

사고 유형 빠른 분류

복구 전략은 “무엇이 꼬였는지”에 따라 달라집니다.

  1. 내 로컬에서만 rebase 했고 아직 푸시 안 함: 그냥 되돌리면 끝
  2. 원격에 강제푸시를 해버림: 원격 브랜치의 이전 커밋을 찾아 복원해야 함
  3. 다른 사람이 이미 그 브랜치를 기반으로 작업함: 복구 후 팀원 브랜치도 정리 필요
  4. PR이 GitHub이고, PR 브랜치가 강제푸시로 바뀜: PR은 그대로 두고 브랜치만 복원하면 대부분 해결

이 글은 2~4를 중심으로 설명합니다.

PR 안전 복구 7단계

아래 단계는 “최악의 경우에도 커밋을 잃지 않기”를 목표로 구성했습니다. 각 단계는 가능하면 작은 단위로 확인하고 진행하세요.

1단계: 즉시 동결하고 상태를 스냅샷으로 남기기

가장 먼저 할 일은 추가 피해 방지입니다.

  • 팀에 “지금 해당 브랜치 푸시 금지” 공지
  • 현재 원격 브랜치 상태를 로컬에 가져오기
  • 지금 상태를 백업 브랜치로 고정
# 원격 최신 상태 가져오기
git fetch origin --prune

# 문제의 브랜치 체크아웃
git checkout feature/my-branch

git reset --hard origin/feature/my-branch

# 현재 상태를 백업 브랜치로 고정 (절대 삭제 금지)
git branch backup/feature-my-branch-$(date +%Y%m%d%H%M%S)

여기서 backup/...는 “현재 상태가 잘못되었더라도” 나중에 비교할 기준점이 됩니다.

2단계: PR에서 원하는 기준점(정상 시점) 정의하기

복구는 “어디로 돌아갈 것인가”를 정해야 합니다. 다음 중 하나를 선택하세요.

  • 강제푸시 직전의 브랜치 HEAD
  • PR에서 리뷰가 끝났던 커밋
  • CI가 마지막으로 통과했던 커밋

정상 커밋 SHA를 알고 있으면 가장 좋습니다. 모르면 다음 단계에서 찾습니다.

3단계: reflog로 잃어버린 커밋 포인터 찾기

강제푸시로 원격 브랜치가 바뀌어도, 로컬에는 흔적이 남아있는 경우가 많습니다. 특히 본인 머신에서 작업했다면 reflog가 강력합니다.

# 브랜치 이동/리베이스/리셋 기록 확인
git reflog --date=iso

# 특정 브랜치의 reflog만 보고 싶다면
git reflog show feature/my-branch --date=iso

출력에서 다음 키워드를 주목하세요.

  • rebase (start) / rebase (finish)
  • reset: moving to ...
  • checkout: moving from ... to ...

원하는 시점의 SHA를 찾았다면, 그 SHA를 “복구 타겟”으로 잡습니다.

4단계: 원격에도 남아있는 흔적 확인 (git fsck, PR UI, 다른 클론)

로컬 reflog가 없거나, 다른 사람이 강제푸시한 경우라면 다음 루트를 병행합니다.

  1. GitHub PR 화면
  • PR의 “Commits” 탭에서 커밋 SHA를 찾을 수 있는 경우가 많습니다.
  • 리뷰 코멘트가 달린 커밋의 SHA가 힌트가 됩니다.
  1. 다른 팀원의 로컬 클론
  • 강제푸시 이전에 fetch 해둔 사람이 있으면 그 사람 reflog에 남아있을 확률이 큽니다.
  1. dangling 커밋 탐색 가비지 컬렉션이 돌기 전이라면 다음으로도 찾을 수 있습니다.
# 도달 불가능 객체 탐색 (시간이 좀 걸릴 수 있음)
git fsck --no-reflogs --lost-found

# dangling commit 목록에서 SHA를 확인 후
# git show `SHA` 로 내용 확인

git show로 “정상 상태의 변경 내용”이 맞는지 반드시 확인하세요.

git show --stat `TARGET_SHA`

5단계: 복구 브랜치 생성 후 PR 기준으로 정합성 검증

찾은 정상 SHA로 바로 원격을 덮어쓰지 말고, 복구 브랜치를 별도로 만들어 검증합니다.

# 정상 커밋 SHA에서 복구 브랜치 생성
git checkout -b restore/feature-my-branch `TARGET_SHA`

# base 브랜치와 diff 확인 (예: main)
git fetch origin

git diff --stat origin/main...restore/feature-my-branch

검증 체크리스트:

  • PR에 포함되어야 할 커밋이 모두 포함되는가
  • 불필요한 커밋(다른 작업, 머지 커밋)이 섞이지 않았는가
  • CI가 기대하는 구조(예: 변경 파일 범위)가 맞는가

여기서 PR이 엉망이었던 이유가 “rebase 과정에서 특정 커밋이 누락”된 것이라면, 복구 브랜치에 누락 커밋을 cherry-pick 해서 보강할 수도 있습니다.

# 누락 커밋을 골라서 복구 브랜치에 추가
git cherry-pick `MISSING_SHA_1` `MISSING_SHA_2`

6단계: 안전한 강제푸시 (--force-with-lease)로 원격 브랜치 복원

검증이 끝났다면, 이제 원격 브랜치를 복원합니다. 이때 --force 대신 반드시 --force-with-lease를 우선 사용하세요.

  • --force는 “그 사이 누가 푸시했어도 무조건 덮어씀”
  • --force-with-lease는 “내가 마지막으로 본 원격 상태에서 바뀌지 않았을 때만 덮어씀”
# 복구 브랜치를 문제의 원격 브랜치로 덮어쓰기
# 예: restore/feature-my-branch 내용을 origin/feature/my-branch로

git push origin restore/feature-my-branch:feature/my-branch --force-with-lease

만약 여기서 실패한다면, 누군가가 그 사이에 푸시했을 가능성이 큽니다. 다시 git fetch origin 후 상황을 공유하고, “어떤 커밋이 최종이어야 하는지” 합의한 뒤 진행하세요.

7단계: PR 안정화 마무리 (팀원 브랜치, 보호 규칙, 재발 방지)

원격 브랜치를 복원하면 PR diff는 대개 정상으로 돌아옵니다. 하지만 팀 차원의 후속 조치가 중요합니다.

팀원이 해당 브랜치를 기반으로 작업 중이었다면

강제푸시로 히스토리가 바뀌었기 때문에 팀원 로컬 브랜치는 꼬일 수 있습니다. 다음 중 하나를 선택합니다.

  • 가장 안전: 팀원 작업을 새 브랜치로 옮기고, 새 기준에 rebase 또는 cherry-pick
  • 빠른 방법(주의): 팀원 브랜치를 새 원격에 맞춰 reset --hard (로컬 변경 유실 위험)

예시(팀원 측):

# 새 원격 기준으로 업데이트
git fetch origin

git checkout feature/my-branch

# 로컬 변경이 없다면 가능 (있으면 반드시 백업 브랜치부터)
git branch backup/local-before-reset-$(date +%Y%m%d%H%M%S)

git reset --hard origin/feature/my-branch

브랜치 보호 규칙 권장

GitHub 기준으로 다음을 켜면 사고가 확 줄어듭니다.

  • protected branch에서 force push 금지
  • PR 브랜치라도 핵심 브랜치 네이밍(예: release/*)은 force push 제한
  • 상태 체크 통과 후 머지 강제

재발 방지: rebase 습관을 “안전 모드”로

  • 공유 브랜치에서는 git pull --rebase를 쓰더라도, 푸시 전 git log --oneline --decorate --graph로 확인
  • 강제푸시가 필요하면 --force-with-lease를 기본값처럼 사용
  • 큰 rebase 전에 백업 브랜치를 먼저 따기
# rebase 전 백업 브랜치 습관화
git checkout feature/my-branch

git branch backup/pre-rebase-$(date +%Y%m%d%H%M%S)

git rebase origin/main

상황별 “즉답” 커맨드 모음

자주 나오는 케이스를 짧게 정리합니다.

케이스 A: 방금 rebase 했는데 아직 푸시 전

# rebase 시작 전 상태로 되돌리기
git reset --hard ORIG_HEAD

ORIG_HEAD는 많은 rebase 작업에서 “이전 HEAD”를 가리키는 안전장치입니다.

케이스 B: 강제푸시로 PR이 꼬였고, 내 로컬에 흔적이 있음

git reflog --date=iso
# TARGET_SHA 찾기

git checkout -b restore/pr `TARGET_SHA`

git push origin restore/pr:feature/my-branch --force-with-lease

케이스 C: 내가 아니라 다른 사람이 강제푸시함

  • 내 로컬 reflog에는 없을 수 있습니다.
  • PR UI에서 커밋 SHA를 찾거나, 다른 팀원의 로컬에서 reflog를 확보하세요.

운영 관점 팁: “복구 절차”를 런북처럼 남겨두기

장애 대응처럼, Git 사고도 런북이 있으면 훨씬 빨리 수습됩니다. 실제로 인프라 장애에서 런북이 중요한 것과 같은 맥락입니다. 예를 들어 장애 상황에서 빠른 진단 루틴을 갖춰두는 방식은 쿠버네티스 트러블슈팅에서도 동일하게 통합니다. 필요하다면 K8s CrashLoopBackOff 원인 10가지·즉시 진단법처럼 “체크리스트 기반 대응”을 Git에도 적용해보세요.

또한 CI가 엉뚱한 커밋에서 돌면서 외부 API 호출이 깨져 400이 나는 등, 부수 장애로 번질 수 있습니다. 그런 경우에는 애플리케이션 레벨 에러를 별도로 분리해 확인해야 합니다. 관련해서는 OpenAI Responses API 400 invalid_request_error 해결처럼 “원인 분리”가 핵심입니다.

결론: 강제푸시는 “되돌릴 수 있게”만 쓰면 된다

git rebase 자체는 나쁜 도구가 아닙니다. 문제는 공유된 히스토리를 바꿨을 때, 안전장치 없이 원격에 덮어쓰는 것입니다.

정리하면 다음 3가지만 기억해도 복구 성공률이 크게 올라갑니다.

  • 먼저 백업 브랜치로 현재 상태를 고정
  • reflog로 정상 SHA를 찾아 복구 브랜치에서 검증
  • 원격 복원은 --force-with-lease로만 진행

이 7단계를 팀 런북으로 만들어두면, PR이 꼬이는 사고는 “치명상”이 아니라 “절차대로 수습 가능한 이벤트”가 됩니다.