빌드노트
squash merge 뒤 git branch --merged에 브랜치가 안 나오는 이유
squash로 변경을 반영해도 작업 커밋은 main의 조상이 되지 않는다. 같은 트리를 만든 일반 merge와 부모·공통 조상·--merged를 비교하고, 변경 없는 재병합과 같은 줄을 다시 수정한 후속 병합의 차이를 확인했다.
작업 브랜치의 변경을 squash merge로 합쳤는데 git branch --merged에는 그 브랜치가 나오지 않을 수 있다. squash로 만든 커밋은 변경 내용을 담지만, 작업 브랜치의 커밋을 부모로 연결하지 않기 때문이다. 파일 반영 여부와 Git이 말하는 병합 여부를 따로 확인해야 한다.
먼저 자기 저장소에서 조상 관계를 조회한다. 아래는 합친 쪽이 main, 작업 브랜치가 feature인 경우다. 두 로컬 브랜치가 존재하고 확인하려는 커밋을 가리키는지 먼저 확인한다. 브랜치를 전환할 필요는 없다. 마지막 줄은 PowerShell에서 바로 앞 Git 명령의 종료 코드를 읽는다.
git branch --merged main --list feature
git merge-base --is-ancestor feature main
$LASTEXITCODE
첫 명령에 출력이 없고 마지막 값이 1이면, 현재 feature 끝의 커밋은 main의 조상이 아니다. 0이면 조상이고, 0·1 이외의 값은 조회 오류이므로 브랜치 이름이나 오류 메시지부터 확인한다. 조상 검사와 종료 코드 (새 탭에서 열림)
--merged main은 main에서 부모를 거슬러 찾아갈 수 있는 끝 커밋을 가진 브랜치를 보여 준다. 뒤의 --list feature는 그중 feature만 고른다. 기준 커밋을 생략하면 현재 HEAD가 기준이므로, feature를 체크아웃한 채 --merged만 실행하면 자기 자신도 목록에 나온다. 위처럼 합친 쪽을 명시해야 한다. 이 조회가 비어 있다는 사실만으로 변경 내용이 빠졌다고 판단할 수는 없다. git branch의 --merged 기준 (새 탭에서 열림)
같은 파일 상태를 만들었지만 부모는 달랐다
이 차이를 새 로컬 저장소에서 비교했다. 공통 커밋 C에는 setting.txt 한 파일과 mode=base 한 줄을 넣었다. feature의 F1·F2에서 그 줄을 차례로 mode=one, mode=two로 바꾸고, main의 M에서는 별도 파일 main.txt를 추가했다. 양쪽에 독립 커밋이 있어 fast-forward가 되지 않는 조건이다.
M에서 한쪽은 git merge --squash feature 뒤 git commit -m 'S squash feature'를 실행했다. 비교용 normal 브랜치도 같은 M에서 시작해 git merge --no-ff --no-edit -m 'N normal feature' feature로 합쳤다. S와 N은 결과 커밋을 구별하기 위한 이름이다. 아래 선은 파일의 이동이 아니라 커밋의 부모 관계다.
squash 뒤 일반 merge 뒤
C---M---S main C---M---N normal
\ \ /
F1---F2 feature F1---F2 feature
--squash 직후, 아직 commit을 실행하기 전에는 HEAD가 M에 그대로 있었다. git status --short에는 M setting.txt가 나와 변경이 스테이징됐고, 다른 부모를 기록할 MERGE_HEAD는 없었다. 공식 문서도 --squash는 작업 트리와 인덱스에 병합 결과를 준비하되 커밋·HEAD 이동·MERGE_HEAD 기록은 하지 않는다고 설명한다. 그 뒤의 commit은 M만 부모로 가진 커밋 S를 만들었다. git merge의 --squash 동작 (새 탭에서 열림)
두 결과의 부모와 파일 차이를 다음 명령으로 확인했다. 여기서 normal은 위 비교 실험에서 만든 브랜치다. 자기 저장소의 증상을 확인할 때 이 이름의 브랜치를 만들 필요는 없다.
PS> git show --no-patch --format='%s | parents: %p' main normal
S squash feature | parents: f4360cb
N normal feature | parents: f4360cb 047e24a
PS> git diff --exit-code main normal
PS> $LASTEXITCODE
0
%p는 부모 커밋의 짧은 ID다. S의 부모는 M인 f4360cb 하나, N의 부모는 M과 F2인 047e24a 둘이었다. 일반 merge도 fast-forward라면 새 merge 커밋을 만들지 않는다. 여기서 부모 둘을 얻은 것은 두 갈래로 나뉜 이력을 merge한 조건이다. 부모 출력 형식 (새 탭에서 열림), 일반 merge와 fast-forward (새 탭에서 열림)
diff에는 아무 패치도 나오지 않았고 종료 코드도 0이었다. 두 결과 커밋의 트리 ID도 같았다. 즉 최종 파일 상태는 같은데, feature는 --merged main에는 없고 --merged normal에는 있었다. 이때의 git diff는 두 커밋에 저장된 파일 상태를 비교한다. 종료 코드 0은 차이가 없다는 뜻이며, 조상 관계까지 같다는 뜻은 아니다. 두 커밋 비교와 --exit-code (새 탭에서 열림)
공통 조상인 merge base도 확인했다. git merge-base main feature는 C인 ab882b82b8dbb138bd419fdc1193aae4f9ba912c, git merge-base normal feature는 F2인 047e24a747b448d424860be0a73a1c148bef8ad7을 출력했다. S에서 부모를 따라가면 M과 C를 지나지만 F2로 갈 길은 없다. N에서는 두 번째 부모가 F2라서 바로 연결된다. merge base의 의미 (새 탭에서 열림)
실제 저장소에서는 main에 다른 작업도 들어 있으므로 git diff main feature에 차이가 남는 것 자체는 자연스럽다. 두 브랜치의 전체 트리가 다른 것과 feature의 변경이 누락된 것은 다른 판단이다. squash 커밋의 변경과 대상 파일을 읽어 반영 여부를 확인하고, 조상 조회 결과는 이력에 대한 답으로 해석한다.
다시 merge하면 파일이 그대로여도 이력은 바뀔 수 있다
새 커밋이 없는 F2를 S에서 다시 일반 merge해 보았다. 이 실험은 main을 움직이지 않도록 S에서 만든 별도 브랜치에서 실행했다. 결과는 Already up to date.가 아니라 Merge made by the 'ort' strategy.였다. 새 커밋 L의 부모는 S와 F2였고, 트리는 S와 같았다. 파일은 바뀌지 않았지만 F2로 가는 부모 연결이 생긴 것이다.
그 뒤 같은 F2를 한 번 더 merge하자 Already up to date.가 나왔다. 이제 F2가 현재 커밋의 조상이기 때문이다. 공식 문서가 설명하는 이 메시지의 조건도 병합 대상 커밋이 이미 HEAD의 조상인 경우다. 빈 파일 diff만으로 같은 메시지를 예상해서는 안 된다. merge의 사전 조상 검사 (새 탭에서 열림)
작업 브랜치를 계속 쓰는 경우에는 공통 조상이 더 직접적인 영향을 준다. 이번에는 앞의 L을 거치지 않은 S와 N에서 각각 별도 브랜치를 만들었다. feature에는 F3를 추가해 같은 줄을 mode=two에서 mode=three로 수정한 뒤, 각 브랜치에서 feature를 일반 merge했다.
- S에서 시작한 쪽은 공통 조상이 여전히 C였다. Git이 비교한 입력은 공통 조상의
mode=base, 현재 쪽의mode=two, feature 쪽의mode=three였다. merge는 종료 코드 1로 끝났고git status --short에UU setting.txt가 나왔다. - N에서 시작한 쪽은 공통 조상이 F2였다. 기준과 현재 쪽이 모두
mode=two여서 feature의 새 값mode=three를 충돌 없이 반영했다.
이는 squash 이후 모든 후속 merge가 충돌한다는 뜻은 아니다. 이미 가져온 줄을 기존 feature에서 다시 수정했을 때, squash로는 공통 조상이 F2까지 이동하지 않아 충돌한 사례다. 변경 위치와 내용이 다르면 결과도 달라질 수 있다.
목록을 고치려 하기 전에 다음 작업의 시작점을 정한다
목적이 변경 내용을 한 커밋으로 반영하는 것이었다면, squash 뒤 feature가 --merged main에 없는 것은 그 목적과 모순되지 않는다. 목록에 보이게 하려고 다시 merge하면 위 L처럼 원래 feature의 커밋들까지 main에서 도달 가능한 이력으로 연결할 수 있다. 내용이 비슷하다는 이유만으로 그 연결을 추가하지 말고, 팀이 원한 이력인지 판단한다.
기존 feature를 계속 병합할 필요가 없다면 다음 작업을 최신 main에서 만든 새 브랜치로 시작하는 편이 단순하다. 계속 이어 써야 한다면 현재 main과 feature의 merge base를 확인하고 후속 변경의 충돌 가능성을 검토한다. --merged의 빈 결과를 브랜치 삭제나 이력 변경의 근거 하나로 삼지 않는다.
직접 실행한 환경은 Windows 11, Git 2.45.1.windows.1, PowerShell 7.6.6이었다. 위 짧은 조회 명령은 PowerShell에서도 실행했다. 확인 범위는 단일 공통 조상의 텍스트 파일, 충돌 없는 최초 병합, 변경 없는 재병합, 같은 줄을 다시 수정한 후속 병합이다. GitHub·GitLab의 squash 버튼 동작, 이름 변경·바이너리·서브모듈, 복잡하게 교차 병합된 이력은 시험하지 않았다. 여기서 설명한 상태는 로컬 git merge --squash와 그 뒤의 commit을 기준으로 한다.