Git 병합 요청에서 커밋 목록은 남아 있는데 파일 변경이 0건으로 보였다. 원인은 소스 트리와 커밋 그래프를 별개로 복구한 뒤 squash 병합이 원래 커밋 부모 관계를 보존하지 않았기 때문이다. 최종적으로 최신 main을 실제 부모로 갖는 no-squash 병합 커밋을 prod에 병합해 두 기준을 다시 일치시켰다.
증상
- 기준 커밋 이후 main에는 통계 기능 관련 소수의 파일 변경만 남아 있었다. 파일 비교 결과 main과 prod의 내용도 같았다.
- 그런데 main에서 prod로 만드는 MR에는 과거 커밋이 18개, 이후에는 20개까지 표시됐다. 반면 Changes 탭은 0건이었다.
- prod는 보호 브랜치라 직접 푸시할 수 없었다. 기능 소스를 다시 적용하거나 강제 푸시하지 않고, MR로 이력만 정리해야 했다.
| 관찰 항목 | 결과 | 의미 |
|---|---|---|
| 파일 비교 | 변경 0건 | 현재 소스 내용은 동일하다. |
| MR 커밋 목록 | 과거 main 커밋이 다수 표시된다. | main의 커밋이 prod 이력에 도달하지 않았다. |
| 기준 커밋 이후 파일 목록 | 예상한 소수 파일만 변경됐다. | 대량 파일이 다시 누락되거나 적용된 상태는 아니다. |
어떻게 좁혔나
1) 파일 상태와 이력 상태를 나눠 확인한다
먼저 파일 트리와 커밋 그래프를 분리해서 확인했다. Git에서 파일 내용이 같다는 사실과 특정 커밋이 대상 브랜치의 조상이라는 사실은 서로 다르다.
git diff --quiet origin/prod origin/main
git rev-parse origin/main^{tree}
git rev-parse origin/prod^{tree}
git rev-list --count origin/prod..origin/main
git merge-base --is-ancestor origin/main origin/prod
파일 트리 해시는 같고 diff도 없었지만, origin/prod..origin/main 범위에는 과거 main 커밋이 남아 있었다. main은 prod의 조상이 아니었다.
2) prod 병합 커밋의 실제 부모를 확인한다
prod의 최근 병합 커밋 부모를 확인했다. main에서 온 원래 커밋이 부모로 연결된 것이 아니라, 대상 브랜치를 부모로 하는 단일 squash 커밋이 중간에 생성돼 있었다.
git show -s --format='%H%n%s%nParents: %P' origin/prod
3) MR 대상 브랜치도 함께 추적한다
이력 연결용 브랜치를 처음 만들 때 MR 대상이 기본값인 main으로 선택된 사실도 확인했다. 그 결과 소스 변경 없이 main에 이력 전용 커밋이 추가됐고, main → prod의 표시 수가 20개로 늘었다.
원인
파일 복구와 이력 복구를 혼동했다
잘못된 되돌리기 이후 main의 파일 트리는 다시 정상 상태로 복구됐다. prod에도 같은 내용을 복구했지만, 이 과정에서 복구 커밋은 현재 main을 부모로 갖지 않는 단일 커밋이었다.
이 상태는 배포 소스 관점에서는 정상이다. 그러나 Git 그래프 관점에서는 main의 원래 커밋들이 prod에 포함되지 않은 상태다. 그래서 MR은 과거 커밋을 미반영으로 표시했다.
squash 병합은 내용을 보존하지만 원래 커밋 계보를 보존하지 않는다
main → prod MR을 squash로 병합하면 GitLab은 대상 브랜치를 부모로 하는 새 단일 커밋을 만든다. 파일 내용이 같으면 그 커밋은 실질적인 소스 변경이 0건일 수도 있다.
따라서 다음 두 결과가 동시에 가능하다.
- Changes 탭: 0건
- Commits 탭: 원래 main 커밋 다수 표시
이는 Git 오류가 아니라, 파일 트리와 도달 가능한 커밋을 각각 계산한 결과다.
해결
1단계: 최신 원격 상태를 기준으로 분기한다
보호된 prod에는 직접 푸시하지 않는다. 원격 prod에서 작업 브랜치를 만들고, 최신 main을 병합 대상으로 사용한다.
git fetch origin
git switch -c fix/link-current-main-to-prod origin/prod
git merge --no-ff --no-commit origin/main
병합을 커밋하기 전에 반드시 파일 변경과 충돌을 확인한다.
git status --short
git diff --cached --shortstat
git diff --cached --check
이 사례에서는 충돌과 파일 변경이 모두 0건이어야 했다. 목적은 소스 수정이 아니라 현재 main을 실제 병합 부모로 연결하는 것이기 때문이다.
2단계: 실제 main 부모를 가진 병합 커밋을 만든다
git commit -m "Merge branch 'main' into 'prod'"
git show -s --format='%H%nParents: %P' HEAD
git merge-base --is-ancestor origin/main HEAD
새 커밋의 두 부모는 현재 prod와 현재 main이어야 한다. 이 조건이 충족되면 새 브랜치 자체에서 main은 조상이 된다.
3단계: prod 대상 MR로 병합한다
작업 브랜치만 원격에 푸시한 뒤 MR을 만든다. 이 단계에서 가장 중요한 설정은 두 가지다.
- 대상 브랜치가 prod인지 확인한다. MR 생성 화면의 기본 대상이 main일 수 있다.
- Squash commits를 해제하고 merge commit 방식으로 병합한다.
squash를 다시 사용하면 파일은 같아도 원래 main의 커밋 계보가 prod에 연결되지 않아 같은 증상이 재발한다.
4단계: 원격 기준으로 완료를 검증한다
MR 병합 뒤에는 로컬 브랜치가 아니라 원격 참조를 다시 받아 검증한다.
git fetch origin
git merge-base --is-ancestor origin/main origin/prod
git rev-list --count origin/prod..origin/main
git diff --quiet origin/prod origin/main
| 검증 항목 | 정상 결과 |
|---|---|
| main이 prod의 조상인가 | 명령 종료 코드 0 |
| main에만 있는 커밋 수 | 0 |
| main과 prod의 파일 차이 | 0건 |
| 기준 커밋 이후 변경 범위 | 의도한 파일만 표시 |
정리
- Git에서 파일 내용이 같다는 것과 커밋 이력이 연결됐다는 것은 별개의 검증 항목이다.
- 보호 브랜치의 이력 정리는 직접 푸시나 강제 푸시 대신, 별도 브랜치와 MR로 수행하는 편이 안전하다.
- 이력 연결이 목적이라면 squash 병합을 사용하면 안 된다. 실제 source head가 merge commit의 부모로 남아야 한다.
- MR 생성 시 source뿐 아니라 target branch를 명시적으로 확인해야 한다. 기본 대상 브랜치로 병합하면 소스 변경 없이 이력만 더 복잡해질 수 있다.
참고
git diff와 트리 해시는 파일 상태를 확인한다.git rev-list A..B와git merge-base --is-ancestor A B는 커밋 그래프의 포함 관계를 확인한다.- 두 결과를 함께 봐야 “이미 배포된 내용인가”와 “다음 MR이 깨끗하게 보이는가”를 모두 판단할 수 있다.
'Git' 카테고리의 다른 글
| Git Squash Merge를 사용하는 이유와 브랜치별 적용 방법 (0) | 2026.10.26 |
|---|---|
| 운영 미배포 작업을 보류 브랜치로 분리해 관리하는 방법 (0) | 2026.06.18 |
| Git Commit 메시지 작성 규칙 (0) | 2025.03.08 |
| [Git] 모든 개발자가 알아야 할 18가지 Git 명령어 (3) | 2024.11.13 |
| [GIT] modified: (untracked content) 에러 해결하기 - submodule 삭제 (1) | 2023.05.31 |