diff의 @@ -1,7 +1,8 @@은 무슨 뜻일까: 통합 diff 읽는 법

diff -u와 git diff에 나오는 @@ 줄의 시작 줄과 줄 수, 범위가 0이 되는 경우, 기본 문맥 3줄, Git이 덧붙이는 함수 문맥을 POSIX 명세와 공식 문서로 설명합니다.

개발Git

코드 리뷰 화면이나 터미널의 git diff 결과를 보면 바뀐 줄 위에 @@ -1,7 +1,8 @@ 같은 줄이 있습니다. 이 줄을 읽을 줄 알면 변경이 파일의 몇 번째 줄에서 일어났는지, 줄이 몇 개 늘거나 줄었는지 바로 알 수 있습니다. 이 형식을 통합 diff(unified diff)라고 하며, POSIX는 diff -u의 출력 형식으로 이를 정해 두었습니다.

머리말 두 줄과 헝크

diff -u의 결과는 두 줄짜리 머리말로 시작합니다. --- 뒤에는 원래 파일, +++ 뒤에는 새 파일의 이름과 시각이 옵니다. 그 아래로 헝크(hunk)라는 덩어리가 이어지는데, 헝크 하나가 두 파일이 달라진 한 구역입니다.

헝크 안의 줄은 첫 글자로 구분합니다.

  • 공백: 두 파일에 똑같이 있는 줄, 즉 문맥
  • -: 원래 파일에서 지워진 줄
  • +: 새 파일에 추가된 줄

@@ 줄 읽기

a부터 h까지 한 줄에 한 글자씩 적힌 8줄짜리 파일에서 4번째 줄 d를 D, d2 두 줄로 바꾸고 diff -u로 비교했습니다. macOS에 들어 있는 diff(FreeBSD 기반)로 실행한 결과이며, 파일 이름과 시각이 적힌 머리말 두 줄은 뺐습니다.

@@ -1,7 +1,8 @@
 a
 b
 c
-d
+D
+d2
 e
 f
 g

-1,7은 원래 파일의 1번째 줄부터 7줄, +1,8은 새 파일의 1번째 줄부터 8줄이라는 뜻입니다. 쉼표 앞은 시작 줄 번호, 뒤는 줄 수입니다.

줄 수는 직접 세어 확인할 수 있습니다. 원래 파일 쪽은 공백이나 -로 시작하는 줄을, 새 파일 쪽은 공백이나 +로 시작하는 줄을 셉니다. 이 예에서는 문맥 6줄에 지운 줄 1개를 더해 7, 문맥 6줄에 추가한 줄 2개를 더해 8입니다. 두 수의 차이인 1이 이 헝크에서 늘어난 줄 수입니다.

바뀐 줄 앞뒤로 문맥이 3줄씩 붙은 것은 기본값이기 때문입니다. POSIX는 -u를 3줄의 문맥을 보여 주는 옵션으로, -U n을 n줄의 문맥을 보여 주는 옵션으로 정의합니다. Git도 기본값이 3줄이며 -U<n> 옵션이나 diff.context 설정으로 바꿀 수 있습니다.

숫자가 하나뿐이거나 0일 때

POSIX 명세에서 범위가 정확히 한 줄이면 2,1 대신 2처럼 시작 줄 번호만 써도 됩니다. 범위가 비어 있으면 시작 줄 번호 자리에 그 범위 바로 앞 줄의 번호를 쓰고, 빈 범위가 파일 맨 앞에서 시작하면 0을 씁니다.

문맥을 0줄로 줄이는 -U0으로 실행하면 이 규칙이 그대로 보입니다. a, b, c 세 줄에서 b를 지운 결과입니다.

@@ -2 +1,0 @@
-b

원래 파일에서는 2번째 줄 한 줄이 지워졌으니 -2입니다. 새 파일 쪽에는 해당하는 줄이 없어 줄 수가 0이고, 시작 번호는 지워진 자리 바로 앞 줄인 1입니다. 반대로 a, c 사이에 b를 넣으면 @@ -1,0 +2 @@가 나옵니다. ,0이 보이면 그쪽 파일에는 이 헝크에 해당하는 줄이 하나도 없다는 뜻입니다.

Git이 덧붙이는 것

git diff의 출력은 전통적인 diff와 조금 다릅니다. Git 문서에 따르면 맨 앞에 diff --git a/파일 b/파일 머리말이 붙고, 그 뒤에 index, new file mode, rename from 같은 확장 머리말 줄이 올 수 있습니다.

@@ 뒤에 글자가 더 붙기도 합니다. Git 문서는 @@ -k,l +n,m @@ TEXT 형태의 줄을 hunk header라고 부르고, TEXT 자리에는 기본적으로 영문자, 밑줄, $로 시작하는 줄이 들어간다고 설명합니다. .gitattributes에서 *.py diff=python처럼 지정하면 Git에 내장된 언어별 패턴을 쓸 수 있습니다.

다만 이 텍스트는 위치를 짐작하는 단서 정도로 보는 것이 좋습니다. 아래는 Git 2.55.0에서 average 함수 안의 return 0을 return None으로 바꾸고 실행한 결과입니다. diff --git과 ---, +++ 머리말은 뺐습니다.

@@ -8,5 +8,5 @@ def total(items):
 def average(items):
     n = len(items)
     if n == 0:
-        return 0
+        return None
     return total(items) / n

바뀐 곳은 average 안인데 @@ 뒤에는 def total(items):가 붙었습니다. 이 실행에서는 헝크가 시작하는 8번째 줄의 def average가 아니라 그보다 앞에 있는 줄이 선택됐습니다. 정확한 위치는 @@ 사이의 숫자로 확인하는 편이 안전합니다.

원고처럼 줄 번호보다 어느 문장이 바뀌었는지가 궁금하다면 Motitory의 텍스트 비교를 쓸 수 있습니다. 이 도구는 @@ 형식을 출력하지 않고, 바뀐 줄과 글자를 색으로 표시합니다.

블로그로