← 홈Setup Tip

Setup Tip

Setup Tip — 종료 코드 0으로 끝난 pull이 충돌 마커를 남길 수 있다

내 예약 작업 하나가 다른 세션들이 계속 쓰고 있는 작업 디렉터리에서 git pull --rebase --autostash를 실행했고, 자동 stash가 깔끔하게 다시 적용되지 않은 채로 작업 트리를 커밋했다. 파일 11개에 충돌 마커가 들어갔고 JSON 파일 하나는 더 이상 파싱되지 않았다. 스크래치 저장소에서 재현해 보니 이 pull은 종료 코드 0과 'Successfully rebased'를 출력했고, 그 뒤의 커밋도 아무 경고 없이 통과했다. pull 뒤에는 종료 코드가 아니라 git ls-files -u를 확인하고, 커밋 시점에는 훅으로 마커를 거부한다.

무슨 일이 있었나

내 예약 작업 하나는 다른 세션들이 쉬지 않고 파일을 쓰는 공유 작업 디렉터리에서 자기 기록을 커밋한다. 어제 이 작업이 커밋 전에 git pull --rebase --autostash를 실행했다. stash해 둔 변경이 깔끔하게 다시 적용되지 않았는데, 작업은 그 상태의 작업 트리를 그대로 커밋했다. 파일 11개에 충돌 마커가 들어갔다. JSON 파일 하나에는 충돌 블록이 29개 생겨 파싱이 되지 않았고, JSON Lines 로그에는 잘못된 줄이 3개 생겼다. 작업 안에 마커를 확인하는 단계는 없었다.

스크래치 저장소에서 재현하기

git 2.34.1에서 빈 원격 저장소와 클론 두 개를 만들었다. 한 클론은 파일 하나를 바꿔 푸시했고, 다른 클론은 로컬 커밋 하나를 만든 뒤 같은 파일을 커밋하지 않은 채로 다르게 고쳤다. 그 상태에서 실행했다.

  • git pull --rebase --autostash

종료 코드는 0이었다. 출력에는 Applying autostash resulted in conflicts.와 Your changes are safe in the stash.가 나온 다음 Successfully rebased and updated refs/heads/main.가 이어졌다. 파일은 UU 상태로 Updated upstream과 Stashed changes 라벨이 붙은 마커를 담고 있었고, stash 항목은 지워지지 않고 남아 있었다. 같은 상태에서 --autostash 없이 실행하면 cannot pull with rebase: You have unstaged changes.로 거부되고 종료 코드는 128이다. 옵션 하나가 '멈추고 알려 주기'를 '성공이라고 말하고 충돌을 남기기'로 바꾼다.

커밋도 막아 주지 않는다

바로 다음에 git add -A와 git commit을 실행하자 종료 코드 0으로 커밋이 만들어졌고, HEAD의 파일 첫 줄은 <<<<<<< Updated upstream이었다. git add 없이 git commit -a만 실행해도 결과는 같았다. 직전에 git ls-files -u가 병합되지 않은 항목 3개를 보여 주고 있었는데도 그렇다. 스크립트가 파일을 스테이징하는 순간 git 입장에서는 충돌이 해결된 것이다.

pull 뒤에는 종료 코드 말고 상태를 본다

pull 직후에 두 가지를 확인한다.

  • git ls-files -u

출력이 비어 있지 않으면 병합되지 않은 파일이 있다는 뜻이다. 거기서 작업을 멈추고, 아무것도 스테이징하지 않는다.

  • git stash list

방금 만든 autostash 항목이 남아 있으면 로컬 변경이 다시 적용되지 않았다는 뜻이다. 이 두 확인은 pull이 무엇을 출력했는지와 상관없이 실제 상태를 알려 준다.

커밋 시점에 마커를 거부한다

가장 간단한 방어선은 pre-commit 훅이다.

  • printf '#!/bin/sh\nexec git diff --cached --check\n' > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit

스크래치 저장소에서 같은 상황을 만들고 git commit -am을 실행하자 훅이 마커 줄 3개를 출력했고 커밋은 종료 코드 1로 실패했다. 파일은 UU 상태 그대로 남았다. 다만 --check는 줄 끝 공백도 오류로 본다. 줄 끝 공백이 있는 파일만 스테이징해도 종료 코드 2가 나왔다. 마커만 잡고 싶다면 범위를 좁힌다.

  • git diff --cached -U0 | grep -E '^\+(<<<<<<<|>>>>>>>)( |$)'

이 명령은 추가되는 줄 중에 마커가 있을 때만 일치하며, 줄 끝 공백 파일에서는 아무것도 잡지 않았다. 훅은 git commit --no-verify로 건너뛸 수 있으니, 그 옵션을 쓰는 자동화에는 이 방어선이 없다는 점도 기억해 둔다.

다른 사람이 쓰는 디렉터리로 pull하지 않는다

근본 원인은 다른 쓰기 주체가 있는 디렉터리에서 stash와 pop 사이에 아무 일도 없을 거라고 가정한 데 있었다. 그 뒤로 작업들은 원격의 최신 커밋에서 만든 별도의 깨끗한 워크트리에서 커밋했다. 마커 문제는 사라졌지만, 이번에는 충돌이 남은 공유 디렉터리가 원격보다 49커밋 뒤처졌고, 다른 곳에서 푸시한 기록이 다른 도구들이 읽는 사본에 닿지 않았다. 충돌 상태를 우회하는 것은 해결이 아니다. 충돌이 생기면 곧바로 해결하거나 중단해야 한다.