Published on

Git rebase 후 강제푸시 없이 PR 히스토리 복구

Authors

서로 다른 팀/레포 정책에서 가장 자주 부딪히는 상황 중 하나가 PR을 올린 뒤 rebase를 해버려 PR 히스토리가 깨지거나(혹은 중복 커밋이 생기거나) 리뷰가 엉망이 되는 문제입니다. 보통은 git push --force-with-lease로 브랜치를 덮어써서 “정리”하지만, 조직에 따라 강제 푸시가 금지되어 있거나(보호 브랜치/권한), 이미 많은 리뷰 코멘트가 달려 강제 푸시가 부담스러운 경우도 많습니다.

이 글에서는 강제 푸시 없이 PR 히스토리를 “복구”한다는 표현을 다음처럼 정의하고, 그 목표에 맞춰 선택지를 정리합니다.

  • PR이 가리키는 브랜치의 커밋 그래프를 다시 정상 형태로 만든다
  • 가능하다면 기존 PR을 유지하고(링크/리뷰 스레드 유지), 변경 범위를 예측 가능하게 만든다
  • 강제 푸시 없이도 CI/리뷰가 다시 의미 있게 보이도록 만든다

rebase 후 PR에서 커밋이 중복되는 케이스는 아래 글에서 원인/복구를 더 깊게 다뤘으니 함께 보면 이해가 빨라집니다.


왜 rebase가 PR 히스토리를 망가뜨리나

핵심은 rebase가 커밋을 재작성(rewrite) 한다는 점입니다.

  • rebase는 기존 커밋 A, B, C를 “그대로 옮기는” 게 아니라
  • 새로운 해시를 가진 A’, B’, C’를 만들어내고
  • 브랜치 포인터를 새 커밋들로 이동시킵니다.

PR은 보통 “base 브랜치(예: main) 대비 head 브랜치(예: feature/x)의 차이”로 변경분을 계산합니다. 이때 head 브랜치에 있는 커밋이 재작성되면:

  • PR의 커밋 목록이 갑자기 바뀌거나
  • 이미 base에 들어간 커밋이 다른 해시로 다시 나타나 “중복 커밋”처럼 보이거나
  • 리뷰 코멘트가 특정 라인/커밋에 달려 있으면 맥락이 흐려질 수 있습니다.

그럼에도 강제 푸시 없이 복구가 가능한 이유는, PR이 “브랜치 포인터”만 보는 것이지, 반드시 rebase로 만든 새 커밋을 유지해야 하는 건 아니기 때문입니다. 즉, 전략적으로 “되돌리거나”, “새 커밋으로 상쇄하거나”, “새 브랜치로 이어붙여” PR을 정상화할 수 있습니다.


복구 전략 선택 가이드 (강제푸시 없이)

상황별로 가장 안전한 선택지는 다릅니다.

1) rebase가 원격에 푸시되기 전이라면: 로컬에서 되돌리고 정상 푸시

  • 로컬에서 실수로 rebase 했지만 원격에는 아직 안 올린 경우
  • 가장 깔끔합니다. 로컬에서만 되돌리면 됩니다.

하지만 이 글의 핵심은 “이미 원격에 rebase 결과가 올라가 PR이 꼬인 상태”이므로, 이후 전략을 주로 다룹니다.

2) 이미 rebase 결과가 PR 브랜치에 푸시됨 + 강제푸시 금지: revert로 되돌리고 다시 정리

강제 푸시를 못 한다면, 기존 커밋을 없었던 일로 만드는 유일하게 보편적인 방법은 revert 입니다.

  • reset --hard는 히스토리를 되감지만, 원격에 반영하려면 강제 푸시가 필요
  • revert는 “반대 변경을 하는 새 커밋”을 추가하므로 강제 푸시 불필요

3) PR은 유지하되 히스토리만 단정하게: “정리 커밋(merge/squash) 추가”로 리뷰 범위 고정

조직에 따라 “커밋 히스토리의 미학”보다 “리뷰 가능성/CI 안정”이 더 중요한 경우가 있습니다.

  • rebase로 중복이 생겼더라도
  • 최종적으로 base에 머지될 때 squash merge 정책이라면
  • PR 단계에서는 “추가 커밋 하나로 정리”하는 편이 비용이 낮습니다.

4) PR을 새로 파서 이어가기: 기존 PR을 보존(참조)하고 새 PR로 마무리

강제 푸시 없이 “완전한 히스토리 복원”이 사실상 불가능할 때가 있습니다.

  • 이미 리뷰가 많이 달렸고
  • 중복 커밋/변경 범위가 너무 커져
  • revert로 되돌리기에도 리스크가 큰 경우

이때는 새 브랜치로 정상 커밋을 만들고 새 PR을 여는 것이 가장 안전합니다. 기존 PR은 close하되 새 PR 링크로 연결하면 리뷰 맥락도 어느 정도 유지됩니다.


실전 1: reflog로 rebase 이전 커밋 찾아 “되돌림 커밋(revert)” 만들기

가정:

  • PR 브랜치: feature/login
  • 실수로 rebase를 수행했고, 그 결과를 원격에 푸시함
  • 강제 푸시는 금지

목표:

  • rebase 이전 상태로 “사실상 복원” (히스토리 재작성 없이)

단계 A) rebase 이전 HEAD 찾기

로컬에 해당 브랜치가 남아 있다면 reflog로 rebase 전후 지점을 찾을 수 있습니다.

git checkout feature/login

git reflog --date=iso

출력 예:

1a2b3c4 (HEAD -> feature/login) HEAD@{0}: rebase finished: returning to refs/heads/feature/login
9f8e7d6 HEAD@{1}: rebase: pick ...
...
abc1234 HEAD@{12}: checkout: moving from main to feature/login

여기서 abc1234 같은 값이 rebase 시작 전 HEAD일 가능성이 큽니다.

단계 B) “현재 상태”와 “원래 상태”의 차이를 되돌리는 revert 만들기

강제 푸시 없이 과거로 되돌리려면, 보통 두 가지로 접근합니다.

  1. rebase로 만들어진 커밋들을 범위로 잡아 revert
  2. 특정 머지 베이스를 기준으로 변경분을 역적용

가장 흔한 방식은 “rebase로 추가된 커밋들”을 revert 하는 것입니다.

# rebase 이후 커밋 범위를 확인
git log --oneline --decorate --graph origin/feature/login --max-count=30

그리고 rebase로 생성된 커밋 범위를 골라 revert 합니다.

# 예: rebase로 생긴 커밋이 X..Y 범위라고 가정
# (주의) 여러 커밋 revert는 충돌 가능성이 큼

git revert --no-commit <oldest_commit>^..<newest_commit>

git commit -m "Revert: undo accidental rebase on PR branch"

git push origin feature/login
  • --no-commit으로 한 번에 적용 후 커밋을 하나로 만들면 PR이 덜 지저분해집니다.
  • 충돌이 나면, 충돌을 해결하고 git revert --continue 또는 커밋을 마무리합니다.

이 방식의 장단점

  • 장점: 강제 푸시 없이 PR 브랜치의 “의미”를 되돌릴 수 있음
  • 단점: revert 자체가 새로운 커밋으로 남아 PR 히스토리가 길어짐

하지만 조직 정책이 강제 푸시를 막는 이유가 히스토리 보존/추적성인 경우가 많기 때문에, revert는 오히려 정책 친화적인 해결책입니다.


실전 2: base 브랜치 기준으로 “정상 변경분만” 다시 쌓는 복구 커밋 만들기

rebase로 히스토리가 꼬였을 때, “어떤 커밋들을 revert해야 하는지” 자체가 모호해지는 경우가 있습니다. 이때는 접근을 바꿔서:

  • **현재 PR 브랜치에서 최종적으로 들어가야 할 변경분(diff)**만 추출
  • base(main) 위에 “복구 커밋”으로 다시 적용

하는 방식이 실용적입니다.

단계 A) base 대비 diff를 패치로 뽑기

# 최신 main을 가져온 뒤
git fetch origin

# PR 브랜치의 변경분을 패치로 생성
git diff origin/main...origin/feature/login > /tmp/pr.patch
  • A...B(triple dot)는 merge-base 기준 diff라 PR 변경분을 뽑는 데 유용합니다.

단계 B) PR 브랜치에서 꼬인 커밋을 “상쇄”하고 패치를 다시 적용

여기서 선택지는 두 가지입니다.

  • 기존 커밋들을 revert로 깨끗하게 만든 뒤 패치를 적용
  • 또는 새 브랜치를 파서 패치를 적용하고 새 PR로 전환

강제 푸시 없이 기존 PR을 유지하려면 보통 전자를 택합니다.

git checkout feature/login

# (선택) 꼬인 커밋을 revert로 상쇄한 뒤
# 패치를 적용
git apply /tmp/pr.patch

git add -A
git commit -m "Restore PR changes after accidental rebase"

git push origin feature/login

이렇게 하면 PR에는 “복구 커밋”이 추가되고, 최종 diff는 정상화됩니다. 리뷰어 입장에서는 커밋 히스토리가 다소 복잡해도 최종 변경분이 명확해집니다.


실전 3: PR을 새로 열되, 기존 PR의 리뷰 맥락을 최대한 살리는 방법

정책상 강제 푸시는 안 되고, revert도 충돌이 너무 커서 위험하다면 “새 PR”이 답입니다. 다만 다음을 지키면 팀 커뮤니케이션 비용을 줄일 수 있습니다.

단계 A) 정상 브랜치 생성

git fetch origin

git checkout -b feature/login-recovered origin/main

# 정상 변경분을 체리픽하거나 패치로 적용
git cherry-pick <good_commit1> <good_commit2>
# 또는
git apply /tmp/pr.patch

git commit -m "Login feature (recovered)"

git push -u origin feature/login-recovered

단계 B) 새 PR을 열고, 기존 PR에는 연결 정보 남기기

  • 기존 PR 코멘트에:
    • “rebase로 히스토리가 오염되어 새 PR로 이어갑니다”
    • 새 PR 링크
    • 기존 PR에서 유효한 리뷰 코멘트가 있던 파일/라인이 어디로 옮겨졌는지 간단 요약

이 방식은 “PR 히스토리 복구”라기보단 “PR 프로세스 복구”지만, 강제 푸시 없이 가장 예측 가능하고 안전합니다.


강제푸시 없이 PR 히스토리를 다룰 때의 체크리스트

1) 보호 규칙 확인: force push 금지 vs 관리자 예외

  • GitHub branch protection에서 “Allow force pushes”가 꺼져 있으면, 기술적으로도 불가
  • 관리자만 예외가 가능하다면, 팀 합의로 1회성 허용이 더 비용이 낮을 수도 있음

2) --force 대신 --force-with-lease가 왜 더 안전한가

정책상 허용된다면 --force-with-lease는 최소한의 안전장치입니다.

git push --force-with-lease origin feature/login
  • 원격 브랜치가 내가 마지막으로 본 상태와 다르면 푸시가 실패
  • 다른 사람이 푸시한 커밋을 실수로 날릴 확률이 줄어듦

다만 이 글의 전제는 “강제 푸시 없이”이므로, 허용되지 않는 환경에서는 revert/새 PR 전략으로 가야 합니다.

3) PR이 중복 커밋을 보여도, 실제 머지 결과는 괜찮을 수 있다

PR UI가 커밋을 중복 표시하는 경우가 있어도, 최종 merge 결과(특히 squash merge)는 정상인 경우가 많습니다. 하지만 리뷰어가 혼란스러워지므로, 아래 중 하나는 해주는 편이 좋습니다.

  • “중복 커밋이 보이는 이유”를 PR 설명에 짧게 적기
  • 최종 diff가 동일함을 git diff origin/main...HEAD로 확인한 결과 공유

결론: ‘히스토리 복원’의 정의를 먼저 정하면 답이 빨라진다

강제 푸시 없이 rebase로 꼬인 PR을 다루는 핵심은, “과거 커밋 해시를 원상복구”하는 게 아니라 다음 중 무엇을 달성할지 결정하는 것입니다.

  • 리뷰 가능성 복구: 최종 diff를 명확하게 만들기(패치 재적용/복구 커밋)
  • 추적성 유지: revert로 되돌린 흔적을 남기기
  • 리스크 최소화: 새 브랜치 + 새 PR로 안전하게 마무리

개인적으로는, 팀에 강제 푸시가 금지되어 있다면 “revert로 상쇄 + 복구 커밋” 또는 “새 PR”이 장기적으로 가장 사고가 적었습니다. 특히 리뷰가 많이 쌓인 PR이라면, 억지로 한 PR에서 모든 걸 해결하려 하기보다 명확한 새 PR로 전환하는 것이 전체 리드타임을 줄이는 경우가 많습니다.

추가로 rebase 후 PR 커밋 중복/원인 분석이 필요하다면 아래 글도 함께 참고하세요.