본문 바로가기
빌드노트 검색

빌드노트

같은 커밋을 가리키는 태그인데 rev-parse 해시가 다르다

같은 커밋에 붙인 lightweight·annotated 태그의 기본 해시가 다른 이유를 객체 종류로 확인한다. rev-parse, cat-file -t, show-ref --dereference의 실제 출력을 읽고, 양쪽을 ^{commit}으로 조회해 커밋 ID를 비교한다.

같은 커밋에 lightweight 태그와 annotated 태그를 붙여도 git rev-parse의 해시는 다를 수 있다. annotated 태그는 커밋 앞에 별도의 태그 객체를 두기 때문이다. 두 태그가 같은 커밋을 가리키는지 비교하려면 양쪽에 ^{commit}을 붙여 커밋 ID를 구한다.

lightweight 태그는 대상 객체를 직접 가리키는 이름이다. annotated 태그는 작성자·시각·메시지 등을 담은 태그 객체를 가리키고, 그 객체가 대상을 가리킨다. 따라서 커밋을 대상으로 만든 두 종류의 태그를 그대로 조회하면 하나는 커밋 ID, 다른 하나는 태그 객체 ID가 나온다. Git tag 문서의 두 태그 설명 (새 탭에서 열림)

해시와 함께 객체 종류를 읽는다

Git이 설치된 PowerShell 7에서 확인할 로컬 저장소로 이동한다. 아래의 demo-light와 demo-annotated를 자신의 두 태그 이름으로 바꾸고 실행한다. refs/tags/를 붙여 조회 대상을 태그로 명시한다. 이 블록은 각 태그의 ID와 그 ID에 해당하는 객체 종류를 확인한다.

git rev-parse --verify refs/tags/demo-light
git rev-parse --verify refs/tags/demo-annotated
git cat-file -t refs/tags/demo-light
git cat-file -t refs/tags/demo-annotated

Windows 11, Git 2.45.1.windows.1, PowerShell 7.6.6에서 새 합성 저장소를 만들어 확인했다. 파일 하나를 커밋하고, 같은 HEAD에 demo-light는 lightweight로, demo-annotated는 메시지를 넣은 서명 없는 annotated 태그로 만들었다. 위 명령의 실제 출력은 다음과 같다.

5d51f30dc64cb0d95b1f03f0baadd71cc9fcc3fe
aafbaf06dc2b56e5cca65aebf21c62d54110f5ad
commit
tag

첫 해시는 커밋 객체의 ID이고 둘째는 태그 객체의 ID다. cat-file -t는 지정한 객체의 종류를 출력한다. 여기서 tag가 나온 것을 커밋이 잘못 만들어졌다는 뜻으로 읽으면 안 된다. 두 ID가 서로 다른 종류의 객체를 식별하고 있다. Git cat-file의 -t (새 탭에서 열림)

자신의 저장소에서 해시가 다르게 나오는 것은 자연스럽다. 위 문자열을 정답으로 복사하지 말고 객체 종류와 이어서 확인할 커밋 ID를 비교한다. 조회가 실패하면 태그 이름과 현재 저장소부터 확인한다.

태그 참조의 ID와 역참조한 대상을 한 번에 보려면 다음 명령을 사용한다. 로컬 태그 전체를 보여 주므로 자신의 두 태그 이름을 찾아 읽는다.

git show-ref --tags --dereference

두 태그를 만든 직후의 출력에는 세 줄이 있었다.

aafbaf06dc2b56e5cca65aebf21c62d54110f5ad refs/tags/demo-annotated
5d51f30dc64cb0d95b1f03f0baadd71cc9fcc3fe refs/tags/demo-annotated^{}
5d51f30dc64cb0d95b1f03f0baadd71cc9fcc3fe refs/tags/demo-light

demo-annotated 행에는 태그 객체 ID가, demo-annotated^{} 행에는 태그를 역참조한 대상 ID가 나온다. ^{}가 붙은 행은 별도 태그가 하나 더 있다는 뜻이 아니다. 이 실험에서는 그 대상 ID가 lightweight 태그의 커밋 ID와 같았다. --dereference가 역참조 결과를 ^{} 표시와 함께 추가하는 동작은 Git show-ref 문서 (새 탭에서 열림)에 설명돼 있다.

커밋을 비교할 때는 양쪽에 타입을 요구한다

^{commit}은 커밋 객체에 도달할 때까지 태그를 역참조한다. 이미 커밋을 가리키는 lightweight 태그라면 그 커밋 ID를 그대로 얻는다. 커밋으로 해석할 수 없으면 실패한다. Git rev-parse의 --verify와 역참조 문법 (새 탭에서 열림)

다음 PowerShell 블록은 두 조회가 모두 성공한 경우에만 전체 커밋 ID를 비교한다. 앞에서 사용한 태그 이름을 여기서도 바꾼다.

$left = git rev-parse --verify 'refs/tags/demo-light^{commit}'
if ($LASTEXITCODE -ne 0) { throw '첫 태그를 커밋으로 확인하지 못했습니다.' }
$right = git rev-parse --verify 'refs/tags/demo-annotated^{commit}'
if ($LASTEXITCODE -ne 0) { throw '둘째 태그를 커밋으로 확인하지 못했습니다.' }
$left
$right
$left -eq $right

같은 두 태그에 실행한 결과다.

5d51f30dc64cb0d95b1f03f0baadd71cc9fcc3fe
5d51f30dc64cb0d95b1f03f0baadd71cc9fcc3fe
True

처음의 해시 차이는 태그가 서로 다른 커밋에 붙었다는 증거가 아니었다. 양쪽에서 커밋 객체까지 따라가자 같은 ID가 나왔다. 반대로 두 조회가 성공했는데 False라면 서로 다른 커밋을 가리킨다. 별도로 두 번째 커밋을 만들고 그곳에 붙인 demo-other를 비교 대상으로 바꿨을 때는 실제로 False가 나왔다.

역참조 성공과 커밋 확인 성공은 다르다

^{}는 태그 객체를 벗겨 내지만, 마지막 객체가 커밋인지까지 요구하지는 않는다. 커밋 ID가 필요한 비교에는 ^{commit}을 쓰는 이유다. Git rev-parse의 타입 지정과 빈 중괄호 (새 탭에서 열림)

같은 실험 저장소에서 파일 내용을 나타내는 blob 객체에 annotated 태그를 붙여 이 차이도 확인했다. 그 태그에 ^{}를 붙인 조회는 성공했고 대상 종류는 blob이었다. ^{commit}을 붙인 조회는 표준 출력 없이 종료 코드 128과 expected commit type, but the object dereferences to blob type 오류를 냈다. 없는 태그를 --verify와 ^{commit}으로 조회했을 때도 종료 코드 128로 실패했다. 조회 실패를 빈 값끼리 비교해 같은 커밋이라고 처리하지 않도록 각 조회 직후 종료 코드를 확인해야 한다.

태그 객체 자체를 식별하려면 접미사 없는 ID를 보존하고, 대상 커밋의 동일성을 판단하려면 ^{commit} 결과를 비교한다. 직접 실행한 범위는 SHA-1 형식의 새 로컬 저장소에서 만든 서명 없는 태그와 객체 조회다. 서명 검증, 원격 저장소의 태그 상태, SHA-256 저장소는 확인하지 않았다.

← 전체 글 보기