git log와 git diff의 ..과 ...: 같은 기호가 다르게 동작하는 이유

git log에서 A..B와 A...B가 고르는 커밋, git diff에서 두 기호가 비교하는 지점을 Git 공식 문서와 직접 실행한 예로 정리합니다. 풀 리퀘스트 diff가 어느 쪽인지도 다룹니다.

개발Git버전 관리

git log main..feature와 git log main...feature는 점 하나 차이지만 고르는 커밋이 다를 수 있습니다. 같은 표기를 git diff에 쓰면 뜻이 또 달라집니다. log는 점 표기를 커밋의 집합으로 읽고, diff는 비교할 두 끝점으로 읽기 때문입니다.

그래서 아래 예처럼 브랜치가 갈라진 히스토리에서는 log의 점 세 개가 두 브랜치의 커밋을 모두 고르는 반면, git diff main...feature는 main에서만 생긴 변경을 뺍니다. 이것은 이 히스토리에서 나온 결과일 뿐, 점 세 개가 언제나 더 많이 또는 더 적게 보여 준다는 규칙은 없습니다. 출력 양은 커밋 그래프와 파일 내용에 따라 달라집니다. 예제는 macOS 15.6.1의 git 2.55.0에서 실행했습니다.

예제 저장소

base 커밋에서 feature 브랜치를 만들고, 양쪽에 커밋을 하나씩 더했습니다.

  • feature: feature.txt 추가
  • main: main.txt 추가

이 단순한 히스토리에서 두 브랜치 끝의 가장 좋은 공통 조상(best common ancestor)은 base입니다. Git은 이것을 merge base라고 부르고, git merge-base main feature로 확인할 수 있습니다.

base 커밋에서 main 브랜치와 feature 브랜치가 갈라진 커밋 그래프. main은 main.txt를 추가하는 커밋 하나, feature는 feature.txt를 추가하는 커밋 하나가 있고 base가 merge base입니다. 이 예에서 git log main..feature는 feature 커밋만 고르고, git log main…feature는 두 커밋을 모두 고르며 –left-right를 붙이면 main 쪽에 <, feature 쪽에 >가 붙습니다. git diff main..feature는 main과 feature 스냅숏을 비교해 feature.txt 추가와 main.txt 삭제를 보여 주고, git diff main…feature는 base와 feature를 비교해 feature.txt 추가만 보여 줍니다.

그림: 이 예제 히스토리에서의 결과입니다. 히스토리 모양이 다르면 두 표기의 출력 차이도 달라집니다.

git log에서: 커밋의 집합

gitrevisions 문서에 따르면 r1..r2는 ^r1 r2의 줄임말입니다. r2에서 닿을 수 있는 커밋 중 r1에서 닿을 수 있는 커밋을 뺀 것입니다.

$ git log --format=%s main..feature
feature: add feature.txt

r1...r2는 대칭 차집합입니다. 둘 중 한쪽에서만 닿을 수 있는 커밋을 고르고, 양쪽에서 모두 닿는 커밋은 뺍니다. --left-right를 붙이면 왼쪽(r1)에서 온 커밋에는 <, 오른쪽(r2)에서 온 커밋에는 >가 붙습니다.

$ git log --left-right --format='%m %s' main...feature
< main: add main.txt
> feature: add feature.txt

두 표기 모두 한쪽을 생략할 수 있고, 생략한 자리는 HEAD가 됩니다. origin..은 origin..HEAD와 같습니다.

git diff에서: 두 지점의 비교

git-diff 문서는 diff가 두 끝점을 비교하는 명령이고, 여기서 쓰는 ..과 ...은 gitrevisions가 말하는 범위가 아니라고 따로 밝혀 둡니다.

git diff A..B는 git diff A B와 같습니다. 두 커밋의 스냅숏을 그대로 비교합니다.

$ git diff --stat main..feature
 feature.txt | 1 +
 main.txt    | 1 -
 2 files changed, 1 insertion(+), 1 deletion(-)

feature에서 main.txt를 지운 적은 없는데도 삭제로 나옵니다. 브랜치가 갈라진 뒤 main에 생긴 파일이라 feature에는 없고, 두 스냅숏을 나란히 놓으면 지워진 것처럼 보이기 때문입니다.

git diff A...B는 git diff $(git merge-base A B) B와 같습니다. 두 브랜치가 갈라진 지점부터 B까지, B 쪽에서 생긴 변경을 보여 줍니다.

$ git diff --stat main...feature
 feature.txt | 1 +
 1 file changed, 1 insertion(+)

GitHub 문서는 풀 리퀘스트 화면의 diff가 이 세 점 방식이라고 설명합니다(2026년 10월 3일 확인). “이 브랜치가 무엇을 바꿨나”가 궁금하다면 대개 세 점 쪽이 맞습니다.

정리

표기 git log git diff
A..B B에서 닿지만 A에서는 닿지 않는 커밋 A와 B 스냅숏을 직접 비교
A...B A와 B 중 한쪽에서만 닿는 커밋 merge base에서 B까지의 변경

히스토리 모양에 따라서는 두 표기의 결과가 같아집니다. main이 feature의 조상이라면, 예를 들어 main에서 feature를 만든 뒤 main이 더 진행되지 않았다면, log에서 main..feature와 main...feature는 같은 커밋을 고르고 diff에서도 두 표기가 같은 두 스냅숏을 비교합니다.

merge base가 여러 개일 때

브랜치끼리 서로 병합한 히스토리(criss-cross merge)에서는 두 커밋의 가장 좋은 공통 조상이 여러 개일 수 있습니다. git diff A...B는 --all 없이 실행한 git merge-base A B가 돌려주는 merge base 하나를 씁니다. git-merge-base 문서에 따르면 후보가 여럿일 때 그중 어느 것이 나올지는 정해져 있지 않습니다.

git merge-base --all A B로 후보가 여럿인지 확인할 수는 있지만, 그렇다고 세 점 diff가 쓰는 merge base가 바뀌지는 않습니다.

diff 출력의 @@ 줄을 읽는 방법은 통합 diff 읽는 법에 정리해 두었습니다.

참고한 명세와 문서

링크와 내용은 2026년 10월 3일에 확인했습니다. GitHub의 화면 동작은 바뀔 수 있습니다.

블로그로