- Published on
Git rebase 후 PR에 커밋이 중복될 때 원인·복구
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
Git을 쓰다 보면 rebase로 히스토리를 깔끔하게 정리한 뒤 PR(Pull Request)을 열었는데, 이미 포함된 커밋이 다시 나타나거나 PR 커밋 목록이 비정상적으로 길어지는 경우가 있습니다. “중복 커밋”이라고 부르지만, 실제로는 Git이 동일한 변경을 다른 커밋(다른 SHA) 으로 인식하는 상황이 대부분입니다.
이 글에서는
- PR에서 커밋이 중복되어 보이는 대표 원인
- 어떤 상황인지 빠르게 판별하는 방법
- 안전하게 복구(정리)하는 실전 절차
- 팀에서 재발을 줄이는 운영 팁
을 커맨드 중심으로 정리합니다.
> 참고로, 장애 원인 분석에서 “기준이 되는 축(시간/네트워크/기준 브랜치)이 틀어져서 증상이 생긴다”는 패턴은 인프라에서도 자주 보입니다. 예: EKS Pod 시간 드리프트로 STS·TLS 실패 해결하기
1) PR에서 “중복 커밋”이 생기는 메커니즘
Git 커밋은 단순히 “변경 내용”만으로 식별되지 않습니다. 커밋 객체는 대략 다음 요소로 해시(SHA)가 결정됩니다.
- 부모 커밋(Parent)
- 트리(파일 스냅샷)
- 작성자/커미터 정보
- 커밋 메시지, 타임스탬프 등
rebase는 커밋을 “다시 쌓는” 작업입니다. 즉, 동일한 diff(변경 내용)라도 부모 커밋이 달라지면 SHA가 달라지고, 호스팅 서비스(GitHub/GitLab 등)는 이를 새 커밋으로 판단합니다.
따라서 “중복”처럼 보이는 현상은 보통 아래 둘 중 하나입니다.
- 실제로 base 브랜치에 포함된 커밋을 feature 브랜치가 다시 포함하고 있다(기준 브랜치 계산이 꼬임)
- 같은 변경을 다른 SHA로 다시 만들었다(rebase/cherry-pick/squash 등)
2) 가장 흔한 원인 6가지
원인 A: 오래된 base 브랜치를 기준으로 rebase해서 PR 기준이 틀어짐
예를 들어 main이 많이 진행됐는데 로컬에서 오래된 origin/main 기준으로 rebase한 뒤 푸시하면, PR이 비교하는 기준점이 달라져 커밋이 과하게 보일 수 있습니다.
증상
- PR에 남의 커밋까지 섞여 보임
- “내 커밋만” 있어야 하는데 수십 개가 추가됨
원인 B: merge main과 rebase를 섞어 히스토리가 꼬임
feature 브랜치에서 중간중간 git merge main을 해놓고, 나중에 다시 rebase main을 하면 커밋 그래프가 복잡해지고 동일 변경이 다른 경로로 포함되는 형태가 됩니다.
증상
- PR 커밋 목록에 merge commit이 많음
- 같은 파일 변경이 여러 커밋에서 반복
원인 C: 다른 브랜치에서 cherry-pick한 커밋을 또 rebase
cherry-pick은 원래부터 “새 커밋”을 만들어냅니다. 이를 다시 rebase하면 동일 변경이 여러 SHA로 존재할 가능성이 커집니다.
원인 D: force push 없이 푸시해서 서버에 오래된 히스토리가 남아 보임(드문 편)
일반적으로 rebase 후에는 --force-with-lease가 필요합니다. 그런데 브랜치 보호/권한 문제로 force push가 안 되고 다른 방식으로 우회하면 PR이 예상과 다르게 보일 수 있습니다.
원인 E: PR의 base 브랜치를 잘못 잡음
main으로 열어야 하는데 develop로 열었다든지, 릴리즈 브랜치를 base로 잡아버리면 커밋이 “중복”처럼 대량 표시됩니다.
원인 F: GitHub/GitLab의 “커밋 목록”과 “Files changed”가 다르게 보임
- 커밋 목록은 SHA 기준
- Files changed는 최종 diff 기준
이라서 커밋은 중복처럼 많아도 실제 변경 파일은 적게 보일 수 있습니다. 이 경우는 “보이는” 문제이지 코드 충돌은 없을 수 있습니다.
3) 원인 빠르게 판별하는 체크리스트
아래 3개만 확인해도 절반은 바로 결론이 납니다.
3-1. PR base 브랜치가 맞는지
- GitHub: PR 화면에서
base: main <- compare: feature/...확인 - GitLab: target branch 확인
3-2. 로컬에서 base 최신화가 되어 있는지
git fetch origin --prune
git branch -vv
# main이 origin/main을 잘 따라가고 있는지 확인
3-3. 그래프에서 “내 브랜치가 어디서 갈라졌는지” 확인
git log --oneline --graph --decorate --all --max-count=60
여기서 feature 브랜치가 origin/main의 최신 커밋에서 갈라져 있지 않거나, 중간중간 merge commit이 섞여 있으면 원인 B 가능성이 큽니다.
4) 안전한 복구 전략 4가지(상황별)
복구는 “PR을 깔끔하게 만들기”가 목적입니다. 하지만 이미 리뷰가 달렸거나 CI가 돌고 있으면 히스토리 재작성은 영향이 큽니다. 아래에서 상황별로 가장 안전한 방법을 고릅니다.
전략 1: 단순히 base를 최신으로 다시 rebase (가장 흔한 정답)
feature 브랜치가 origin/main 최신을 기준으로 다시 쌓이면 PR의 비교 기준이 정상화됩니다.
# 1) 최신 상태 가져오기
git fetch origin --prune
# 2) 작업 브랜치로 이동
git switch feature/my-work
# 3) 최신 main 기준으로 rebase
git rebase origin/main
# 4) 충돌 해결 후
# git add ...
# git rebase --continue
# 5) 강제 푸시(안전 옵션)
git push --force-with-lease
--force-with-lease는 “원격 브랜치가 내가 마지막으로 본 상태에서 바뀌지 않았을 때만” 강제 푸시합니다. 실수로 동료 커밋을 덮어쓰는 사고를 크게 줄입니다.
전략 2: merge와 rebase가 섞여 꼬였으면 ‘새 브랜치에 깨끗하게 재구성’
히스토리가 복잡할수록 “기존 브랜치를 억지로 고치기”보다, 깨끗한 브랜치로 옮겨 심는 방식이 안전합니다.
방법 A: patch로 옮기기(가장 안전, 커밋 단위는 포기)
git fetch origin --prune
git switch feature/my-work
git diff origin/main...HEAD > /tmp/mywork.patch
git switch -c feature/my-work-clean origin/main
git apply /tmp/mywork.patch
git status
git add -A
git commit -m "Apply changes from my-work"
git push -u origin feature/my-work-clean
- 커밋 히스토리는 1개로 단순화
- PR도 새 브랜치로 열면 중복 커밋이 사라짐
방법 B: 커밋을 살리고 싶으면 rebase --onto 또는 cherry-pick
git fetch origin --prune
git switch feature/my-work
# 공통 조상 찾기
BASE=$(git merge-base origin/main HEAD)
echo $BASE
# BASE 이후 커밋만 새 기준에 올리기
git rebase --onto origin/main $BASE
git push --force-with-lease
전략 3: PR base 브랜치가 잘못된 경우(가장 간단)
코드는 멀쩡한데 PR이 이상하면, PR의 base를 올바른 브랜치로 바꾸는 게 끝입니다.
- GitHub: PR 화면에서
Edit→ base branch 수정 - GitLab: MR target branch 수정
이 경우 로컬 rebase가 아니라 PR 설정 문제라서 커밋 “중복”이 즉시 해소됩니다.
전략 4: 이미 리뷰/승인이 진행 중이면 ‘rebase를 멈추고 merge 전략으로 마무리’
리뷰 코멘트가 커밋 라인에 달려 있는 상황에서 force push를 하면 코멘트가 “outdated”가 되거나 맥락이 깨질 수 있습니다.
이럴 땐 다음 중 하나를 선택합니다.
- PR을 그대로 두고
merge commit으로 머지(정책 허용 시) - 또는 “새 PR”을 깨끗한 브랜치로 열고 기존 PR은 닫기
팀의 운영 정책이 중요합니다. (브랜치 보호, squash merge 강제 여부 등)
5) 중복 여부를 ‘내용 기준’으로 확인하는 실전 커맨드
커밋 SHA가 달라도 실제 변경이 같은지 확인하려면 diff/patch-id 관점이 유용합니다.
5-1. PR에 보이는 커밋 범위가 맞는지
git fetch origin --prune
git log --oneline origin/main..HEAD
- 여기 출력이 “내가 의도한 커밋”과 다르면 base/히스토리 문제가 맞습니다.
5-2. 동일 변경(패치)인지 확인: patch-id
# 두 커밋이 실질적으로 같은 변경인지 확인
# (공백/메타데이터를 무시하고 패치 내용을 해시)
git show <commitA> | git patch-id --stable
git show <commitB> | git patch-id --stable
출력된 patch-id가 같으면 “내용은 같은데 커밋만 다른” 전형적인 rebase 중복입니다.
5-3. 특정 파일 기준으로 중복 변경 추적
git log --oneline --follow -- path/to/file.ts
같은 변경이 여러 커밋에서 반복되면 merge+rebase 혼용 가능성이 큽니다.
6) 재발 방지: 팀 워크플로우 권장안
6-1. feature 브랜치에서는 ‘한 가지 업데이트 방식’만 선택
- rebase 기반으로 갈 거면: feature 브랜치에서
merge main을 하지 않기 - merge 기반으로 갈 거면: rebase로 히스토리 재작성하지 않기
혼용이 가장 흔한 사고 원인입니다.
6-2. rebase 후 푸시는 기본을 --force-with-lease
git push --force-with-lease
--force는 습관적으로 쓰면 위험합니다.
6-3. PR을 열기 전, 항상 최신 base를 fetch
git fetch origin --prune
이 단 한 줄이 “오래된 기준으로 rebase”하는 실수를 줄입니다.
6-4. CI/리뷰 관점에서 squash merge를 적극 검토
개별 커밋 품질을 강하게 요구하지 않는 팀이라면, PR 머지 시 squash를 강제하면 “커밋 중복” 이슈가 사용자 경험 측면에서 크게 줄어듭니다.
7) 자주 묻는 질문
Q1. PR에 커밋이 중복으로 보이면 실제로 코드도 두 번 들어가나요?
대부분은 아닙니다. Git은 머지 시점에 최종 트리 상태를 결정하므로, 동일 변경이 두 커밋에 있어도 최종 결과는 한 번만 반영되는 경우가 많습니다. 다만 충돌 해결 과정에서 의도치 않게 변경이 재적용될 수 있으니 Files changed와 로컬 diff를 꼭 확인하세요.
Q2. rebase를 했는데 왜 커밋 SHA가 바뀌나요?
rebase는 커밋을 “새 부모 커밋 위에 새로 생성”합니다. 부모가 바뀌면 커밋 객체 자체가 달라져 SHA도 달라집니다.
Q3. 가장 안전한 복구는 무엇인가요?
리뷰가 진행 중이 아니라면 보통은 git fetch 후 git rebase origin/main → git push --force-with-lease가 가장 빠르고 깔끔합니다. 이미 리뷰가 깊게 진행됐다면 새 브랜치/새 PR이 더 안전할 때가 많습니다.
결론
Git rebase 후 PR에 커밋이 중복되어 보이는 현상은 대부분
- 기준 브랜치가 최신이 아니었거나
- merge와 rebase를 섞었거나
- cherry-pick/rebase로 같은 패치를 다른 SHA로 만든 것
에서 발생합니다.
해결의 핵심은 “PR이 무엇을 base로 비교하는지”를 정확히 맞추고, 필요하면 최신 base로 다시 rebase하거나 깨끗한 브랜치로 재구성하는 것입니다. 특히 강제 푸시는 --force-with-lease를 기본으로 두면 팀 사고를 크게 줄일 수 있습니다.