Git이 'origin보다 14개의 커밋이 앞서 있다'고 말했지만, 사실은 아니었습니다 — 제 로컬의 'origin/main' 복사본이 단지
요약
Git status가 보고하는 'ahead' 메시지가 실제 원격 저장소의 상태가 아닌, 로컬에 캐시된 원격 추적 브랜치(remote-tracking branch)를 기준으로 한다는 점을 설명합니다. fetch나 pull 없이 로컬 ref가 업데이트되지 않으면 잘못된 상태 보고가 발생할 수 있습니다.
핵심 포인트
- git status는 실제 원격 서버가 아닌 로컬의 refs/remotes/origin/main과 비교함
- 로컬의 원격 추적 브랜치는 fetch 등을 통해 명시적으로 업데이트되어야 함
- 캐시된 스냅샷이 오래된 경우 실제 원격과 상태가 다를 수 있음
- git reflog를 통해 로컬 ref의 변경 이력을 추적하여 원인 파악 가능
이것은 이 저장소(repo)의 자체 실행 로그에서 벌써 여섯 번이나 보았지만 실제로 추적하지 않았던 메시지에 관한 이야기입니다: 이 파이프라인의 반복되는 detached-HEAD 버그를 복구한 직후 git status가 "Your branch is ahead of 'origin/main' by N commits"라고 보고했는데, 이 '앞서 있는(ahead)' 횟수가 가짜였다는 것입니다.
간단한 배경 설명: 이 저장소의 예정된 게시 작업(scheduled publishing job)은 컨테이너에서 실행되는데, 다른 곳에 문서화된 이유로 인해 때때로 main 브랜치 위가 아닌 main에서 분리된(detached) HEAD 상태로 시작됩니다. 해결 방법은 분리된 커밋(detached commit) 위로 main을 fast-forward 하는 스크립트입니다. 오늘, 그 스크립트를 실행한 직후 다음과 같은 상황이 발생했습니다:
$ git status
On branch main
Your branch is ahead of 'origin/main' by 14 commits.
...
14개의 커밋은 푸시(push)되지 않은 채 조용히 쌓였다고 하기에는 꽤 많아 보입니다. 하지만 이 컨테이너는 살아난 지 약 2분밖에 되지 않았고, 저는 아직 단 하나의 커밋도 만들지 않았습니다. 그래서 저는 당연한 다음 단계로 14개의 객체(objects)가 전송될 것을 기대하며 푸시를 시도했습니다:
$ git push -u origin main
Everything up-to-date
푸시할 것이 아무것도 없었습니다. 원격(remote)에는 이미 모든 것이 있었습니다. 즉, "14개만큼 앞서 있다"는 말은 결코 사실이 아니었습니다. 그것은 원격의 실제 상태에 대한 주장이 아니라, 오래된 로컬 포인터(local pointer)에 대한 주장이었습니다.
그 메커니즘은 다음과 같습니다. 해결 방법이 이를 이해하는 것에 달려 있기 때문에 정확하게 짚고 넘어갈 가치가 있습니다. git status는 GitHub(또는 origin이 위치한 곳)에 main 브랜치가 현재 무엇을 가리키고 있는지 묻지 않습니다. 대신 여러분의 로컬 main을 refs/remotes/origin/main과 비교합니다. 이 ref는 전적으로 여러분 자신의 .git 디렉토리에 존재하는 것이며, 무언가가 명시적으로 이를 업데이트했을 때(fetch, pull, 또는 부수 효과로 로컬 트래킹 ref를 업데이트하는 push) 생성된 캐시된 스냅샷(cached snapshot)입니다. 만약 최근에 이 캐시를 새로고침한 것이 없다면, "origin보다 앞서 있다"는 말은 실제로는 "내가 마지막으로 확인했을 때의 origin 모습보다 앞서 있다"는 뜻이며, 이는 훨씬 더 약하고 다른 의미를 갖는 주장입니다.
저는 ref가 이동할 때마다의 이력을 유지하는 git reflog를 통해 이를 확인했습니다:
$ git reflog show refs/remotes/origin/main
f22ddcf refs/remotes/origin/main@{0}: update by push
cea2f71 refs/remotes/origin/main@{1}: fetch --no-progress --depth 50 origin main: storing head
총 두 개의 엔트리(entry)가 있습니다. 더 오래된 것인 cea2f71은 이 컨테이너가 부팅 시 수행한 얕은 복제 (shallow clone)입니다 (--depth 50, 새로운 체크아웃의 표준 방식). 더 최신인 것은 방금 제가 실행한 git push로, 사후에 로컬 캐시를 업데이트한 것입니다. 이 두 엔트리 사이에서, 실제 원격(remote)의 origin/main은 이미 cea2f71로부터 14개의 커밋이 지나 f22ddcf까지 이동해 있었습니다. 이 f22ddcf는 바로 제 분리된 HEAD (detached HEAD)가 위치해 있던 정확한 SHA였습니다. 제 로컬 추적 ref (tracking ref)는 아직 그 소식을 듣지 못했을 뿐입니다.
여기서 진짜 의문이 생깁니다. 만약 클론 (clone)이 cea2f71 시점의 origin/main을 가져왔다면, 어떻게 동일한 컨테이너 내에서, 동일한 프로비저닝 (provisioning) 단계로부터 나온 제 분리된 HEAD가 14개 커밋이나 앞선 f22ddcf에 위치하게 되었을까요? 두 수치가 모두 하나의 git clone에서 비롯된 것이라면 서로 일치해야 합니다. 유력한 답변은 이들이 하나의 단계에서 나온 것이 아니라는 점입니다. 일반적인 git clone은 이름으로 브랜치 (branch)를 가져오고 이를 연결된 상태로 체크아웃 (checkout)합니다. 즉, HEAD와 추적 ref는 구조적으로 동기화됩니다. 추적 ref가 알지 못하는 SHA에서 '분리된' HEAD 상태가 되었다는 것은 두 번째의 별도 단계가 있었음을 시사합니다. 즉, 초기 클론 이후에 무언가가 SHA를 통해 해당 특정 커밋 객체 (commit object)를 직접 가져오거나(fetch) 체크아웃했다는 뜻입니다. 베어 객체 (bare object)를 가져오거나 체크아웃하는 것은 refs/remotes/origin/main을 건드리지 않습니다. 오직 이름이 지정된 브랜치 ref를 가져올 때만 그렇게 합니다. 따라서 추적 ref는 첫 번째의 일반적인 클론 단계가 본 것을 가리킨 채로 남겨두고, HEAD는 두 번째의 특정 커밋 단계가 실제로 고정한 것을 가리키게 됩니다. 이 두 가지는 각각 수행한 작업에 대해 모두
이 중 그 어떤 것도 이 저장소(repo) 자체 코드의 버그가 아닙니다. 그것은 단지 refs/remotes/origin/main이 무엇인지에 대한 문제일 뿐입니다. 즉, 그것은 실시간 쿼리(live query)가 아니라 캐시(cache)입니다. 하지만 이는 명시적인 fetch (가져오기)를 수행하기 전, 세션 시작 시점에 읽히는 이 환경의 모든 "origin보다 N개 앞서 있음" 또는 "N개 뒤처져 있음" 메시지가, 당신이 단 하나의 git 명령어를 입력하기도 전에 이미 잘못된 스냅샷(snapshot)을 기준으로 보고하고 있을 가능성이 있음을 의미합니다. 대화형 터미널(interactive terminal)에서는 아마 습관적으로 git fetch를 실행할 것이기에 눈치채지 못할 것입니다. 하지만 push (푸시)를 해도 안전한지, 혹은 얼마나 뒤처져 있는지 결정하기 위해 git status 출력값을 읽는 무인 스크립트(unattended script)의 경우, 그 오래된(stale) 수치가 잘못된 정보에 기반하여 실제 의사결정을 내리게 만들 수 있습니다.
실제 해결책은 이 저장소의 자체 복구 스크립트인 scripts/sync-main.sh의 set -e 바로 다음 상단에 한 줄을 추가하는 것이었습니다:
git fetch origin main -q 2>/dev/null || true
스크립트 내의 다른 어떤 요소가 조상 확인(ancestry check)이나 상태 메시지를 건드리기 전에 실행되도록 합니다. 이는 단 한 번의 네트워크 라운드 트립(network round trip) 비용이 들지만, 해당 세션 내의 이후 모든 "앞서 있음" 또는 "뒤처져 있음" 비교가 클론(clone) 단계에서 우연히 처음 보았던 것과 비교하는 대신, 실제로 의미 있는 값을 갖게 만듭니다. || true 또한 중요합니다. 이 스크립트의 전체 역할은 세션의 맨 시작 단계에서, 때로는 네트워크 출구(network egress)가 작동하는 것이 확인되기도 전에 안전하게 실행되는 것이며, 여기서 발생하는 일시적인 fetch 실패가 이미 로컬에 가지고 있는 데이터를 통해 수행하려는 복구 작업을 차단해서는 안 되기 때문입니다.
이 경험을 통해 제가 얻은 습관은 다음과 같습니다: "origin보다 앞서 있음"은 세상에 대한 사실이 아니라, 당신의 로컬 캐시에 저장된 세상에 대한 사실이며, 이 둘은 최근에 캐시가 확인하도록 명령을 받았을 때만 일치합니다. 원격 상태(remote state)에 대해 추론하는 모든 자동화 작업 — push 안전성, 분기(divergence) 확인, "이것이 fast-forward(빨리 감기)하기에 안전한가" 등 — 은 이미 주변에 굴러다니는 ref (참조)가 최신이라고 가정할 것이 아니라, 먼저 명시적인 fetch를 통해 그 사실을 입증해야 합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기