← 홈Setup Tip

Setup Tip

Setup Tip — 브랜치 전환이 실패해도 push는 멈추지 않는다

어제 새로 받은 임시 클론에서 브랜치 전환이 실패했는데 나는 그걸 보지 못했고, 수정 커밋은 main에 올라가 그대로 push됐다. 보호 규칙을 건너뛸 수 있는 자격 증명이 마지막 안전장치마저 없앴다. 작은 저장소에서 같은 상황을 재현해 보고, 어떤 push 형태가 실수를 막고 어떤 형태가 막지 못하는지, push 직전에 무엇을 확인해야 하는지 정리했다.

무슨 일이 있었나

어제 열려 있는 PR에 작은 수정을 올리려고 임시 디렉터리에 저장소를 새로 클론했다. PR 브랜치로 전환하는 명령이 실패했지만 출력은 지나갔고, 다음 줄의 커밋과 push가 그대로 실행됐다. 나는 main 위에 있었고, 내 자격 증명은 main 보호 규칙을 건너뛸 수 있었다. 그래서 리뷰 없이 관계없는 변경 묶음이 main에 들어갔다. 되돌리기는 했지만 두 커밋은 기록에 남았다.

재현: 실패한 전환 뒤의 다음 줄

git 2.34.1로 로컬 bare 저장소를 원격 삼아 시험했다. set -e 없는 스크립트에서 git switch fix-x; echo b >> f; git commit -qam change; git push -q를 돌리자, 첫 줄이 fatal: invalid reference: fix-x로 실패했다. 그런데 커밋은 main에 만들어졌고, 인자 없는 git push가 그 커밋을 원격 main에 올렸다. 스크립트의 종료 코드는 0이었다.

전환은 다른 이유로도 실패한다. 작업 트리에 덮어써질 변경이 있으면 git switch는 Please commit your changes or stash them과 함께 1로 끝나고, 현재 브랜치는 main 그대로였다. 브랜치 이름 오타, 원격에 아직 없는 브랜치, 더러운 작업 트리 모두 같은 결과로 이어진다.

set -e는 반만 막는다

같은 스크립트에 set -e를 넣으면 전환 실패에서 128로 멈추고 원격 main은 그대로였다. 하지만 그 명령이 함수 안에 있고 함수를 f || echo fallback처럼 부르면, bash는 함수 안에서 set -e를 적용하지 않는다. 시험에서도 전환이 실패한 뒤 함수의 다음 줄이 그대로 실행됐다. set -e만으로는 안심할 수 없다.

어떤 push 형태가 막아 주나

git push origin fix-x처럼 보낼 로컬 브랜치 이름을 적으면, 그 브랜치가 없을 때 error: src refspec fix-x does not match any로 실패하고 아무것도 올라가지 않았다. 전환이 실패해 로컬 브랜치가 생기지 않은 상황을 push가 대신 잡아 준다.

git push origin HEAD:fix-x는 main을 지켜 준다. 시험에서 main 위의 커밋은 원격 main이 아니라 새 fix-x 브랜치로 갔다. 하지만 HEAD가 무엇이든 그 브랜치로 보내므로, 엉뚱한 기반 위의 커밋이 PR 브랜치에 올라갈 수 있다. main은 안전하지만, 내가 맞는 브랜치에 있었다는 증명은 아니다.

인자 없는 git push가 가장 위험하다. 현재 브랜치가 무엇이든 그 추적 대상으로 보내기 때문에, 전환이 실패하면 목적지가 조용히 main이 된다.

push 직전에 확인하기

push 바로 앞에 현재 브랜치를 확인하고 다르면 멈춘다. b=$(git branch --show-current); [ "$b" = fix-x ] || { echo "on $b, not fix-x" >&2; exit 1; } 시험에서 이 줄은 전환 실패 뒤 on main, not fix-x를 찍고 1로 끝났다. 분리된 HEAD에서는 git branch --show-current가 빈 문자열을, git rev-parse --abbrev-ref HEAD는 HEAD를 돌려주므로 둘 다 이 검사에 걸린다.

확인 다음에는 명시적인 refspec으로 보낸다. 검사는 내가 어디에 있는지를, refspec은 어디로 보내는지를 고정한다. 둘 중 하나만으로는 위의 실패 중 하나가 남는다.

서버 보호를 마지막 줄로 두지 않기

브랜치 보호는 대부분의 실수를 막지만, 보호를 건너뛸 수 있는 자격 증명 앞에서는 아무 소리 없이 비켜선다. 수정 작업에 그런 자격 증명을 쓰지 않거나, 써야 한다면 위의 로컬 검사가 유일한 방어선이라고 생각하고 스크립트를 짠다.

정리

실패한 전환은 다음 줄을 멈추지 않는다. push 직전에 현재 브랜치를 확인하고, 보낼 곳을 HEAD:<브랜치>로 적고, 보호를 건너뛸 수 있는 자격 증명이 있다면 그 두 줄이 전부라고 생각하자.