빌드노트
git diff main..feature와 GitHub PR의 파일 목록이 다른 이유
main에만 추가한 파일이 두 점 diff에서는 삭제로 보이고 GitHub PR에서는 빠질 수 있다. 같은 분기에서 두 끝점 비교와 공통 조상 기준 비교의 patch·파일 목록·방향성을 확인하고, 리뷰 목적에 맞는 명령을 고른다.
git diff main feature에는 파일 삭제가 보이는데 GitHub PR의 변경 파일에는 그 파일이 없을 수 있다. 이때는 비교의 출발점을 확인한다. git diff main..feature는 두 브랜치 끝의 파일 상태를 비교하고, git diff main...feature는 공통 조상에서 feature 끝까지 비교한다. 공백으로 두 브랜치를 나열한 첫 명령은 두 점 표기와 같다. Git diff 공식 문서 (새 탭에서 열림)
GitHub PR은 세 점 비교를 사용한다. base가 main, head가 feature인 PR에서 제안한 변경을 읽으려면 main...feature가 맞는 방향이다. 두 브랜치의 현재 파일 구성이 얼마나 다른지 보려면 main..feature를 쓴다. 파일 목록이 다르다는 이유만으로 소스가 누락됐다고 판단하기 전에, 어느 질문에 답하려는 비교인지 나눠야 한다. GitHub의 PR 비교 방식 (새 탭에서 열림)
main에만 추가한 파일이 삭제로 보였다
별도의 새 저장소에서 이 차이를 확인했다. 공통 커밋에는 common.txt만 두고, feature에는 feature-only.txt, main에는 main-only.txt를 각각 추가해 커밋했다. 아래 A·F·M은 이해를 위한 커밋 이름이며 실제 브랜치는 main과 feature다.
M main : common.txt, main-only.txt
/
A base : common.txt
\
F feature : common.txt, feature-only.txt
분기 뒤 각 브랜치에 커밋 하나씩만 있는 상태다. 두 명령의 파일 목록은 다음처럼 달랐다. A는 추가, D는 삭제 표시이며 파일명 앞에는 탭이 있다.
PS> git diff --name-status main..feature
A feature-only.txt
D main-only.txt
PS> git diff --name-status main...feature
A feature-only.txt
--name-status를 빼면 줄 단위 변경인 patch가 나온다. 실제로 두 점 비교의 patch에는 다음 부분이 있었다. 여기서는 파일 내용의 해시를 적은 index 행만 생략했다.
diff --git a/main-only.txt b/main-only.txt
deleted file mode 100644
--- a/main-only.txt
+++ /dev/null
@@ -1 +0,0 @@
-main
feature에서 이 파일을 삭제하는 커밋을 만든 적은 없다. main에 추가한 파일이 feature에는 처음부터 없었다. 두 점 비교의 삭제 표시는 main의 파일 상태를 feature의 파일 상태로 바꾸면 없어질 파일이라는 뜻이다. feature의 작업자가 삭제했다는 이력을 말하지 않는다. git diff main feature와 git diff main..feature의 전체 patch도 같았다.
세 점 비교에서는 출발점 A에도, 도착점 F에도 main-only.txt가 없다. 그래서 이 파일은 나오지 않고 feature-only.txt를 추가하는 patch만 나왔다. 이 관찰에서 PR 목록에 main 쪽 파일이 없는 이유를 설명할 수 있다. 해당 파일은 공통 조상 이후 feature가 제안한 변경에 속하지 않는다.
세 점의 오른쪽을 바꾸면 검토할 브랜치도 바뀐다
세 점 비교가 고르는 공통 조상을 merge base라고 부른다. 더 정확히는 두 커밋의 공통 조상 중 다른 공통 조상의 조상이 되지 않는 기준 커밋이며, 복잡한 병합 이력에서는 여러 개일 수도 있다. Git merge-base 공식 문서 (새 탭에서 열림)
이 실습에서 다음 첫 명령은 한 줄, 공통 커밋 A의 ID를 출력했다. PowerShell에서는 그 ID를 변수에 담아 비교 기준을 직접 지정할 수 있다.
git merge-base --all main feature
$base = git merge-base main feature
git diff $base feature
마지막 명령의 patch는 git diff main...feature와 같았다. 세 점 비교는 공통 조상을 찾은 뒤 두 번째 인자의 파일 상태까지 비교한다는 것을 확인한 것이다. 공통 조상이 여러 줄로 나온다면 이 실습처럼 변수 하나에 담아 처리하지 않는다.
이번에는 인자 순서를 뒤집었다.
PS> git diff --name-status feature...main
A main-only.txt
공통 조상은 그대로 A였지만 도착점은 M으로 바뀌었다. 따라서 main이 추가한 파일만 나왔다. 두 점의 순서를 뒤집은 feature..main에서는 두 파일이 그대로 나오면서 추가·삭제 표시가 서로 바뀌었다. 세 점의 순서를 뒤집는 것은 같은 patch의 부호만 바꾸는 일이 아니다. 이 예제에서는 아예 다른 브랜치의 추가 파일을 보여 줬다.
같은 분기를 직접 만들어 보려면
실행 환경은 Windows 11과 Git 2.45.1.windows.1이었다. 다음 입력은 Git과 PowerShell 7이 있는 환경에서 사용할 수 있다. 기존 프로젝트와 분리된 새 빈 폴더를 만들고 그 안에서 실행한다. init 직후 출력한 최상위 경로가 그 실습 폴더인지 확인하며, 명령이 실패하면 다음으로 넘어가지 않는다. 이름과 이메일 설정은 이 새 저장소에만 저장된다.
git init --quiet --initial-branch=main
git rev-parse --show-toplevel
git config --local user.name DiffLab
git config --local user.email diff-lab@example.invalid
git config --local core.autocrlf false
Set-Content -LiteralPath common.txt -Value common -Encoding ascii
git add -- common.txt
git -c commit.gpgSign=false commit --quiet -m base
git switch --quiet -c feature
Set-Content -LiteralPath feature-only.txt -Value feature -Encoding ascii
git add -- feature-only.txt
git -c commit.gpgSign=false commit --quiet -m 'feature change'
git diff --name-status main..feature
git diff --name-status main...feature
git switch --quiet main
Set-Content -LiteralPath main-only.txt -Value main -Encoding ascii
git add -- main-only.txt
git -c commit.gpgSign=false commit --quiet -m 'main change'
중간에 있는 두 diff는 모두 A feature-only.txt만 출력했다. 그 시점에는 main이 아직 공통 커밋 A에 있어서 두 비교의 출발점이 같기 때문이다. 이어 main에 독립 변경을 커밋한 뒤 같은 두 diff를 다시 실행하면 앞서 본 두 파일 대 한 파일의 차이가 생긴다. 두 점과 세 점의 결과가 항상 다른 것은 아니다.
실습에서는 Git 전역·시스템 설정과 템플릿 훅의 영향을 배제하고 줄바꿈 자동 변환을 껐다. 본인의 설정에 따라 줄바꿈 경고나 커밋 ID는 달라질 수 있다. 해시 문자열을 맞추기보다 파일 목록과 추가·삭제 방향을 비교하면 된다.
PR과 대조할 때는 base와 head부터 맞춘다
자기 저장소에서는 먼저 PR의 base·head에 해당하는 브랜치를 확인한다. 아래는 base가 main, head가 feature일 때의 읽기 명령이다. 브랜치 이름은 자신의 PR에 맞게 바꾼다. 브랜치를 전환하지 않아도 된다.
git rev-parse --show-toplevel
git rev-parse main
git rev-parse feature
git merge-base --all main feature
git diff --name-status main...feature
git diff main...feature
처음 출력한 경로와 두 커밋 ID를 확인한다. 로컬 main·feature가 PR에서 비교하는 커밋과 다르면, 같은 이름을 썼어도 같은 비교가 아니다. 공통 조상이 하나인지 확인한 뒤 파일 목록을 보고, 필요한 파일의 patch를 읽는다. 공통 조상을 찾지 못하거나 여러 개가 나오면 이 글의 단순 분기와 조건이 다르므로 기준 커밋 하나를 임의로 골라 누락을 판단하지 않는다.
동일한 커밋을 비교하는데 GitHub 화면의 목록이 여전히 다르다면 화면의 파일 필터와 diff 표시 제한도 확인한다. GitHub는 diff 한도나 .gitattributes 규칙 때문에 변경이 표시되지 않을 수 있다고 안내한다. GitHub의 변경 표시 조건 (새 탭에서 열림)
이번에 직접 실행한 범위는 단일 공통 조상에서 갈라진 두 로컬 브랜치의 텍스트 파일 비교다. 실제 GitHub PR 화면과 병합 결과는 시험하지 않았다. PR의 세 점 비교 방식은 공식 문서로 확인했다. 두 점의 삭제 목록만으로 PR 병합 뒤 파일이 삭제된다고 단정할 수는 없다.