요약
- stacked PR을 squash 머지하면 상위 PR이 main에 **단일 새 커밋(다른 SHA)**으로 들어가, 하위 PR 히스토리의 원래 상위 커밋들이 "내용은 같지만 SHA가 다른" 중복으로 남는다.
git rebase --onto <목적지> <시작경계> <옮길브랜치>로 그 중복 커밋을 떼어내고 하위 PR 고유 커밋만 새 main 위에 다시 얹는다.- 하위 PR이 상위 PR 위에 "더하기"만 했다면(같은 라인을 재수정하지 않았다면) 이 rebase는 충돌이 나지 않는다.
본문
문제: squash 머지가 stacked PR의 베이스를 깬다
PR-B의 base가 PR-A 브랜치인 stacked 구조에서 PR-A를 squash 머지하면:
main: ... ─ X(=A를 squash한 단일 커밋) ← A의 내용이 들어있지만 SHA는 새것
│
PR-B 브랜치: a1 ─ a2 ─ a3 ─ b1 ← a1~a3 = A의 원래 커밋들 (main의 X와 SHA 다름)
↑ PR-B tip
GitHub은 PR-B의 base를 자동으로 main으로 retarget하고 "MERGEABLE"로 표시하지만, PR-B의 히스토리에는 a1·a2·a3가 그대로 남아 있다. 이 커밋들은 main의 X와 내용은 같지만 SHA가 달라 git이 별개로 본다. 그대로 머지하면 같은 변경이 중복 적용되거나 충돌이 난다.
해결: rebase --onto로 상위 커밋만 버린다
git rebase --onto origin/main <PR-A-tip> PR-B
└─목적지 └─시작경계 └─옮길브랜치
- 시작경계(
PR-A-tip= a3) 다음부터 PR-B tip까지 =b1하나만 대상 - 그걸
origin/main위에 새로 얹음 →a1·a2·a3는 버려짐
main: ... ─ X ─ b1' ← PR-B 고유 커밋만 깔끔하게
일반 git rebase main은 main에 동등 커밋(patch-id)이 없는 커밋을 전부 옮긴다. squash 머지된 a1~a3는 X와 SHA·patch-id가 달라 동등으로 인식되지 못해 재적용 대상에 남는다 — 그래서 그대로 두면 다시 재생된다. --onto는 시작 경계를 직접 지정해 그 중간 커밋을 버릴 수 있다는 점이 핵심 차이다.
3단 이상 스택이면 각 단계가 squash 머지될 때마다 그 아래 PR을 다시 --onto 해야 한다. 즉 squash 머지 1회 = 다음 PR rebase 1회.
왜 충돌이 안 나는가
rebase 충돌은 옮길 커밋이 바꾼 라인과 새 베이스가 바꾼 라인이 겹칠 때만 난다. squash 스택 정리에서는 보통 안 겹친다:
- main의
X는 PR-A를 squash한 것이라 PR-B가 깔고 있던 A 내용과 글자 단위로 동일. PR-B의 diff가 참조하는 context 라인이 main에 그대로 있다 → 겹쳐도 같은 내용이라 충돌 아님. - PR-B는 PR-A 위에 코드를 추가했을 뿐 A가 쓴 라인을 재수정하지 않았다 → 부딪힐 라인이 없음.
따라서 충돌은 다른 독립 PR이 그 사이 main에 머지되면서 PR-B와 같은 라인을 건드린 경우에만 발생한다. squash 중복 제거 자체는 구조적으로 무충돌이다.
대조적으로, 처음 PR을 만들고 한참 뒤 rebase할 때 충돌이 잦은 건 그 사이 다른 작업이 같은 파일·같은 영역(예: 같은 enum 꼬리, 같은 함수)을 건드렸기 때문이다. 충돌 여부는 "rebase를 하느냐"가 아니라 "겹치는 변경이 있느냐"로 결정된다.
실무 절차
# 상위 PR squash 머지 직후
git fetch origin main
git checkout PR-B
git rebase --onto origin/main <PR-A-tip-SHA> PR-B # 로컬 PR-A 브랜치 SHA를 시작경계로
# 빌드/테스트 검증 후
git push --force-with-lease origin PR-B
- 시작경계는 머지 전 로컬에 남아있는 상위 PR 브랜치 tip을 쓰면 정확하다.
- rebase로 SHA가 바뀌므로 push는
--force-with-lease(원격이 내가 아는 상태일 때만 강제)로 안전하게.
관련 노트
참고
- https://git-scm.com/docs/git-rebase (
--onto섹션)