GitLab에서 머지한 hotfix 커밋이 main 히스토리에서 사라진 이유
배경
hotfix 브랜치에서 QR 스캔 라이브러리의 CSP unsafe-eval 위반 문제를 고치고 GitLab MR로 main에 머지했다. 프로젝트 머지 방식은 Merge commit이었고, MR 페이지에도 머지 커밋이 표시됐다. 그런데 얼마 후 git log main에서 그 머지 커밋이 보이지 않았다.
먼저 짚고 갈 개념
fast-forward
한쪽 브랜치가 다른 쪽의 조상이면, Git은 새 커밋을 만들지 않고 브랜치 포인터만 앞으로 옮긴다.
main: A ─ B ─ C
hotfix: └─ D
git merge hotfix (fast-forward)
main: A ─ B ─ C ─ D ← 머지 커밋 없음
두 브랜치에 각자 새 커밋이 있으면(diverged) fast-forward가 불가능하고, 공통 조상을 기준으로 3-way 머지 커밋이 생긴다. git merge --no-ff는 fast-forward가 가능해도 머지 커밋을 강제로 만든다.
GitLab의 Merge commit 방식
GitLab의 "Merge commit" 방식은 MR을 머지할 때 항상 머지 커밋을 만든다(--no-ff와 같은 결과). 단, 이 설정은 MR로 머지할 때만 적용된다. 누군가 git push로 브랜치를 직접 갱신하는 것은 막지 않는다.
rebase
rebase는 커밋을 대상 브랜치 위에 새 SHA로 다시 만든다. 기본 동작에서는 머지 커밋을 다시 만들지 않고 건너뛰므로, rebase한 히스토리에는 원래의 머지 커밋이 남지 않는다.
main: A ─ B ─ M(머지 커밋)
dev: A ─ B ─ C ─ D
git switch dev && git rebase main
dev: A ─ B ─ M ─ C' ─ D' ← C, D가 새 SHA로 재작성
원인: main이 재작성(강제 push)되었다
이 프로젝트에는 dev의 변경을 main에 반영하는 sync 파이프라인이 있었다. 이 파이프라인은 MR을 거치지 않고 git push로 main을 직접 갱신했다.
여기서 중요한 사실이 하나 있다. 일반(fast-forward) push로는 기존 커밋이 히스토리에서 사라지지 않는다. fast-forward는 기존 main이 새 커밋의 조상일 때만 가능하므로, 머지 커밋도 여전히 조상으로 남는다.
머지 커밋이 main의 조상에서 빠졌다면, main이 머지 커밋을 포함하지 않는 히스토리로 교체된 것이다. 즉 파이프라인이 머지 커밋을 모르는 dev 기반 커밋을 강제 push(--force)했거나, rebase로 다시 만든 히스토리를 강제로 올린 경우다.
[MR 머지 직후]
main: ... ─ X ─ M (hotfix 머지 커밋)
[sync 파이프라인이 dev 기반 커밋을 강제 push]
main: ... ─ X ─ A ─ B ─ C ← M은 더 이상 main의 조상이 아님
git branch -a --contains <머지 커밋 SHA>
# 출력 없음 → 어떤 브랜치에도 포함되지 않은 커밋
어떤 브랜치·태그에서도 도달할 수 없는 커밋은 일정 기간(reflog 만료 등) 뒤 GC로 정리될 수 있다. 서버에 남아 있는 동안은 SHA로 직접 접근해 복구할 수 있다.
잘못된 해결 시도: dev를 main 위로 rebase
충돌을 해결하려고 dev를 main 위로 rebase하고 다시 main에 반영하려 했다. 이 방식은 main에 올라갈 히스토리를 다시 쓰는 것이고, 반영 과정에서 강제 push가 필요해지면 같은 문제가 반복된다.
올바른 해결: main을 dev로 머지
main은 건드리지 않고, main의 변경을 dev로 머지해서 충돌을 dev 쪽에서 해결한다.
git switch dev
git merge main # 충돌이 나면 로컬에서 해결
git add <해결한 파일>
git commit
git push origin dev
main: ... ─ M (hotfix 머지 커밋 보존)
dev: ... ─ N (Merge main into dev) ← main의 모든 변경 포함
| 방법 | main 히스토리 | hotfix 머지 커밋 |
|---|---|---|
| dev를 main 위로 rebase 후 반영 | 재작성 위험 | 사라질 수 있음 |
| main을 dev로 merge | 그대로 | 보존 |
GitLab 충돌 해결 UI의 부산물
GitLab 웹 UI에서 MR 충돌을 해결하면, GitLab은 대상(target) 브랜치를 원본(source) 브랜치로 머지하는 커밋을 원본 브랜치에 만든다. main → dev MR(원본이 main)에서 UI로 충돌을 해결하자 main에 Merge branch 'dev' into 'main' 커밋이 생겼다. main에 원치 않는 커밋을 남기지 않으려면 위처럼 로컬에서 dev에 main을 머지해 해결한다.
재발 방지
main을 Protected branch로 설정하고 force push를 금지한다. 파이프라인이 강제 push를 시도하면 실패하므로 문제를 바로 알 수 있다.- 동기화 파이프라인은
--force대신 일반 push나 MR 생성으로 바꾼다. 꼭 필요하다면 최소한--force-with-lease를 쓴다.
정리
| 개념 | 설명 |
|---|---|
| fast-forward | 포인터만 이동. 기존 커밋은 그대로 조상으로 남음 |
--no-ff | fast-forward 가능해도 머지 커밋 생성 |
| GitLab Merge commit 방식 | MR 머지에만 적용. 직접 push는 막지 않음 |
| rebase | 커밋을 새 SHA로 재작성. 머지 커밋은 기본적으로 사라짐 |
| 커밋이 브랜치에서 사라짐 | 해당 브랜치가 강제 push 등으로 재작성되었다는 신호 |