blog

GitLab에서 머지한 hotfix 커밋이 main 히스토리에서 사라진 이유

Front-end / Git#Git#GitLab#merge#rebase#force push#hotfix

배경

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-fffast-forward 가능해도 머지 커밋 생성
GitLab Merge commit 방식MR 머지에만 적용. 직접 push는 막지 않음
rebase커밋을 새 SHA로 재작성. 머지 커밋은 기본적으로 사라짐
커밋이 브랜치에서 사라짐해당 브랜치가 강제 push 등으로 재작성되었다는 신호