Published on

Git rebase 후 PR에 커밋 중복? 원인과 해결

Authors

서론

Git을 쓰다 보면 rebase는 가장 강력하면서도 가장 자주 사고를 내는 도구입니다. 특히 rebase 후 Pull Request(PR)에서 커밋이 중복으로 보이거나, 이미 머지된 커밋이 다시 PR에 나타나며 리뷰가 꼬이는 상황을 한 번쯤 겪습니다.

이 글은 “왜 중복이 생기는가”를 Git 객체 모델(커밋 SHA)과 PR 비교 방식 관점에서 설명하고, 상황별로 가장 안전한 해결책을 제시합니다. 또한 팀 차원에서 재발을 줄이는 브랜치 운영/리베이스 규칙도 함께 정리합니다.

> 참고로 CI가 PR 히스토리 변화에 민감하다면, 토큰/권한 이슈와 함께 증상이 겹쳐 보일 때가 있습니다. PR 업데이트 후 Actions가 갑자기 403을 내는 케이스는 별도로 정리한 글도 참고하세요: GitHub Actions 403 권한오류 해결 - GITHUB_TOKEN·OIDC


PR에서 “커밋 중복”이 생기는 핵심 원리

1) rebase는 커밋을 ‘수정’하는 게 아니라 ‘새로 만든다’

rebase는 기존 커밋을 다른 베이스 위로 “옮기는” 과정에서, 각 커밋을 새 커밋으로 재작성합니다. Git에서 커밋의 정체성은 SHA(정확히는 커밋 객체 해시)인데, 커밋에는 다음 정보가 포함됩니다.

  • 부모 커밋(Parents)
  • 트리(파일 스냅샷) 해시
  • 작성자/커미터 정보
  • 커밋 메시지

베이스가 바뀌면 부모 커밋이 달라지므로 SHA가 달라집니다. 즉, 내용이 같아 보여도 Git은 “다른 커밋”으로 인식합니다.

2) PR은 보통 “base..head” 범위로 커밋/변경을 계산한다

GitHub PR은 대체로 다음 범위를 기준으로 커밋을 보여줍니다.

  • 커밋: base..head
  • 변경 파일(diff): merge-base(base, head)를 기준으로 계산

문제는 rebase로 히스토리를 재작성하면, merge-base가 달라지고, 이전에 PR에 있던 커밋이 “다른 SHA로 다시 등장”할 수 있다는 점입니다. 그래서 UI 상으로는 “중복 커밋”처럼 보이거나, 이미 반영된 변경이 다시 diff에 나타나는 일이 생깁니다.


가장 흔한 원인 5가지

1) PR 브랜치에 rebase를 했는데, 원격에 force push를 안 했거나 잘못 했다

로컬에서만 rebase하고 푸시를 일반 git push로 해버리면, 원격 브랜치에는 기존 커밋 + 새 커밋이 함께 남아 진짜로 중복 히스토리가 생길 수 있습니다.

증상

  • PR 커밋 탭에 같은 메시지의 커밋이 두 번씩 보임
  • 그래프에 분기/병합이 이상하게 늘어남

해결

rebase 후에는 원격 브랜치를 히스토리 재작성에 맞게 갱신해야 합니다.

# 1) 최신 상태 확인
git fetch origin

# 2) 작업 브랜치에서 rebase 예시
git checkout feature/my-work
git rebase origin/main

# 3) 안전한 force push (권장)
git push --force-with-lease origin feature/my-work
  • --force-with-lease는 “내가 마지막으로 본 원격 상태”가 아닐 경우 푸시를 막아, 동료 커밋을 덮어쓰는 사고를 줄입니다.

2) base 브랜치가 바뀌었는데 PR의 base를 그대로 두었다

예를 들어 main을 기준으로 PR을 올렸는데, 중간에 조직 정책으로 develop으로 base를 바꾸거나, 반대로 develop에서 main으로 바꾸는 경우가 있습니다.

증상

  • “이미 머지된 커밋이 다시 나타남”
  • 파일 변경이 갑자기 엄청 늘어남

원인

PR의 비교 기준(base)이 바뀌면, merge-base(base, head)가 달라져 diff 계산이 바뀝니다. 이때 기존에 base에 들어있던 커밋이 base에서 사라진 것처럼 보이거나, 반대로 head에만 있는 것처럼 보일 수 있습니다.

해결

  • PR base를 원래 의도한 브랜치로 되돌리거나
  • 현재 base에 맞춰 다시 rebase합니다.
git fetch origin
git checkout feature/my-work

# PR base가 develop이라면
git rebase origin/develop

git push --force-with-lease

3) 다른 사람의 커밋을 cherry-pick/rebase로 가져오며 ‘동일 변경’이 다른 커밋으로 복제됨

팀원이 만든 커밋을 내 브랜치로 가져오려고 cherry-pick을 했는데, 나중에 그 커밋이 base 브랜치에 머지되면 PR에서 “같은 변경이 두 번” 보일 수 있습니다.

증상

  • 커밋은 다르지만 코드 변경이 유사/동일
  • PR diff에서 같은 라인이 반복해서 바뀐 것처럼 보임

원인

Git은 “변경 내용이 같다”가 아니라 “커밋 SHA가 같다/조상 관계다”를 기준으로 포함 여부를 판단합니다. 같은 변경이라도 다른 커밋이면 PR 범위에 남습니다.

해결 전략

  • 이미 base에 들어간 변경이라면, 내 브랜치에서 해당 커밋을 제거(드롭)하는 편이 깔끔합니다.
# 인터랙티브 리베이스로 특정 커밋 제거
git rebase -i origin/main

# 에디터에서 해당 커밋 줄을
# pick -> drop (또는 줄 삭제)

git push --force-with-lease

4) merge commit이 섞인 상태에서 rebase를 시도해 히스토리가 꼬임

feature 브랜치에서 수시로 main을 merge로 끌어오다가, 나중에 “깔끔하게 만들겠다”고 rebase를 하면 커밋 그래프가 복잡해지고 PR이 중복처럼 보일 확률이 커집니다.

해결

  • 가능하면 feature 브랜치에서는 merge 대신 rebase로 최신화하는 습관을 들이거나
  • 이미 merge가 많이 섞였다면, “정리 rebase”보다 새 브랜치로 갈아타는 방식이 안전합니다.
# 새 브랜치로 갈아타기(추천)
git fetch origin
git checkout -b feature/my-work-clean origin/main

# 기존 브랜치의 최종 상태를 한 번에 가져오기
# (커밋 히스토리 대신 결과물만)
git merge --squash feature/my-work

git commit -m "Implement my-work"
git push -u origin feature/my-work-clean

이 방식은 PR을 새로 열어야 할 수도 있지만, 리뷰 관점에서는 가장 예측 가능하고 깔끔합니다.


5) PR이 “업스트림 포크”와 “내 포크” 사이에서 비교되어 기준이 어긋남

포크 기반 협업에서 업스트림 저장소와 내 포크 저장소의 브랜치가 서로 다르게 움직이면, PR UI가 보여주는 커밋 범위가 기대와 달라질 수 있습니다.

해결 체크리스트

  • PR의 base/head가 정확히 어느 저장소의 어느 브랜치인지 확인
  • 내 포크의 main이 업스트림 main과 동기화되어 있는지 확인
# 업스트림 추가(없다면)
git remote add upstream https://github.com/ORG/REPO.git

# 업스트림 기준으로 동기화
git fetch upstream
git checkout main
git merge --ff-only upstream/main

git push origin main

문제를 빠르게 진단하는 명령어 세트

PR에서 중복처럼 보일 때는 “내 브랜치에 실제로 어떤 커밋이 있는지”와 “base와의 공통 조상이 어디인지”를 확인하면 거의 결론이 납니다.

1) 공통 조상(merge-base) 확인

git fetch origin

git merge-base origin/main HEAD

2) PR에 포함될 커밋 범위 확인

# main에 없는 커밋 목록(대략 PR 커밋)
git log --oneline origin/main..HEAD

3) 그래프 시각화(중복/분기 확인)

git log --oneline --decorate --graph --all --max-count=50

4) 같은 변경을 가진 “다른 커밋”이 있는지 추적

# 특정 파일의 히스토리를 따라가며 중복 변경이 어디서 왔는지 보기
git log --oneline --follow -- path/to/file

상황별 ‘정답’에 가까운 해결책

A) 단순 rebase 후 PR이 중복처럼 보인다 → 보통 force-with-lease로 정리

  • 로컬 rebase를 했고
  • 원격에 히스토리가 그대로 남아 있거나
  • PR이 이전 커밋과 신규 커밋을 모두 가리키는 상태라면
git push --force-with-lease

이 한 번으로 정리되는 경우가 가장 많습니다.

B) 이미 공유된 브랜치(여러 명이 작업)에서 rebase를 해버렸다 → 새 브랜치 전략

공유 브랜치에서 rebase + force push는 동료 작업을 날릴 수 있습니다. 이 경우는 다음이 안전합니다.

  1. 현재 PR 브랜치를 건드리지 않고
  2. base에서 새 브랜치를 만든 뒤
  3. 필요한 커밋만 cherry-pick 하거나 squash merge로 결과만 옮깁니다.
# 안전한 새 브랜치
git checkout -b feature/recovered origin/main

# 필요한 커밋만 골라서 가져오기
git cherry-pick <sha1> <sha2> <sha3>

git push -u origin feature/recovered

C) 커밋은 중복인데 diff는 정상이다 → UI 착시일 수도 있음

rebase로 커밋 SHA가 바뀌면 “같은 메시지의 커밋이 또 생긴 것처럼” 보일 수 있습니다. 하지만 실제 diff가 정상이라면, 문제는 주로 다음 중 하나입니다.

  • 이전 PR의 커밋을 스쿼시 머지해서 base에는 커밋 SHA가 남지 않음
  • PR UI는 커밋 탭에서 “동일 작업”을 다른 커밋으로 보여줌

이때는 파일 변경(diff) 기준으로 리뷰를 진행해도 됩니다. 다만 히스토리 일관성을 원하면 인터랙티브 리베이스로 커밋을 스쿼시/정리하는 게 좋습니다.


재발 방지를 위한 팀 규칙(실무 추천)

1) PR 올린 뒤에는 “혼자 쓰는 브랜치”에서만 rebase 허용

  • 개인 feature 브랜치: rebase OK
  • 공유 브랜치: rebase 금지(merge 또는 새 브랜치)

2) force push는 --force-with-lease만 허용

문서/가이드에 git push -f 대신 아래만 쓰도록 명시합니다.

git push --force-with-lease

3) 머지 전략을 통일: Squash merge vs Rebase merge vs Merge commit

  • Squash merge를 쓰면 base에는 “단일 커밋”만 남으므로, 이후 동일 커밋이 다시 보이는 착시가 줄어듭니다.
  • Rebase merge는 히스토리는 깔끔하지만, PR 작성자가 rebase/force push를 자주 하게 되어 운영 규칙이 필요합니다.
  • Merge commit은 추적은 쉬우나, feature에서 main을 자주 merge하면 PR이 지저분해질 수 있습니다.

조직의 리뷰 문화(커밋 단위 리뷰 vs 변경 단위 리뷰)에 맞춰 하나로 고정하는 편이 PR 혼란을 크게 줄입니다.


결론

git rebase 후 PR에서 커밋이 중복으로 보이는 문제는 대부분 커밋 SHA 재작성과 **PR 비교 기준(base/head/merge-base)**의 조합에서 발생합니다. 해결은 의외로 단순한 편입니다.

  • 개인 브랜치에서 rebase했다면: git push --force-with-lease
  • 공유 브랜치가 꼬였다면: 새 브랜치로 복구(필요 커밋만 cherry-pick)
  • base가 바뀌었거나 포크 비교가 꼬였다면: PR base/head를 먼저 점검

PR이 이상해졌을 때는 당황해서 커밋을 더 쌓기보다, merge-baseorigin/main..HEAD 범위를 확인해 “어떤 기준으로 중복이 보이는지”부터 확정하면 빠르게 정상화할 수 있습니다.