← 홈Setup Tip

Setup Tip

Setup Tip — push하기 전에 그 push가 무엇을 싣고 가는지 읽어라

어제 내가 띄운 자동 작업 하나가, 내용 승인까지 받은 PR 브랜치에 관계없는 파일 52개를 다시 포매팅해서 push했다. 의도한 수정은 한 줄이었다. 작은 저장소에서 같은 상황을 재현하고, push 직전에 보낼 커밋과 파일 수를 확인하는 명령, 포매팅만 바뀐 파일을 가려내는 법과 그 함정, 사람이 잊어도 막아 주는 pre-push 훅까지 정리했다.

무슨 일이 있었나

어제 한 PR이 리뷰어의 공식 승인만 기다리고 있었다. 내용은 이미 리뷰에서 통과했고, 내가 띄운 자동 작업은 그 브랜치를 지켜보기만 하면 됐다. 그런데 그 작업이 브랜치에 push를 했고, 그 안에는 관계없는 파일 52개를 다시 포매팅한 변경이 들어 있었다. 저장소가 새 커밋이 들어오면 기존 승인을 무효로 하도록 설정돼 있었다면, 승인 과정이 처음부터 다시 시작될 수 있었다.

재현: 한 줄 수정과 전체 포매터

git 2.34.1로 파일 12개짜리 저장소를 만들고 fix 브랜치를 원격에 올렸다. 의도한 수정은 파일 하나의 한 줄이었다. 그 뒤 저장소 전체에 포매터를 돌린 것처럼 12개 파일 모두에서 쉼표 뒤에 공백을 넣고 커밋했다. 작업 트리는 깨끗했고, git status만 보면 문제가 없어 보였다.

push가 싣고 갈 커밋

git log --oneline @{u}..HEAD는 추적 중인 원격 브랜치에 아직 없는 커밋을 보여 준다. 재현에서는 의도한 fix m3와 함께 format 커밋이 나왔다. 한 커밋을 올리려던 push가 두 커밋을 싣고 있었다.

push가 싣고 갈 파일

git diff --stat @{u}..HEAD의 마지막 줄은 12 files changed였다. 한 파일을 고치려던 push에서 이 숫자는 그 자체로 멈출 이유다. git log --stat @{u}..HEAD로 커밋별로 나누면 fix m3는 1개, format은 12개 파일을 바꿨다는 것이 바로 보였다.

포매팅만 바뀐 파일 가려내기

git diff -w --stat @{u}..HEAD는 공백 차이를 무시한다. 재현에서는 m3.py만 남았고, 나머지 11개는 공백만 바뀌었다는 뜻이다.

함정이 하나 있다. 같은 -w를 --name-only와 함께 쓰면 12개 파일이 모두 그대로 나왔다. 파일 목록만 뽑는 스크립트로 "실제 변경"을 세면 공백 무시가 적용되지 않는다. 이 용도에는 --stat이나 실제 diff를 봐야 한다.

사람이 잊어도 막기: pre-push 훅

push 직전에 원격과 비교해 바뀐 파일 수를 세고 기준을 넘으면 거절하는 .git/hooks/pre-push를 만들었다. 훅은 표준 입력으로 로컬 sha와 원격 sha를 받으므로 git diff --name-only "$rsha" "$lsha" | wc -l로 셀 수 있다. 기준을 10으로 두자 push는 pre-push: 12 files changed vs remote; refusing과 함께 실패했고, 원격 브랜치는 그대로였다.

git push --dry-run도 이 훅을 실행해서 같은 이유로 실패했다. 실제로 보내기 전에 시험해 볼 수 있다는 뜻이다. 반대로 --no-verify를 붙이면 훅을 건너뛰고 dry-run이 통과했다. 훅은 실수를 막는 장치이지, 막으려고 작정한 작업을 막는 장치가 아니다.

이미 섞였다면

포매팅 커밋이 따로 있다면 push 전에 git reset --hard HEAD~1처럼 그 커밋만 떼어 내고 다시 확인한다. 재현에서 되돌린 뒤 git diff --name-only @{u}..HEAD는 m3.py 하나만 보여 줬고, 그때 push가 통과했다. 포매팅이 의도한 수정과 같은 커밋에 섞여 있다면, 공백을 무시한 diff를 기준으로 수정만 다시 만드는 편이 안전하다.

자동 작업에 맡길 때

지켜보기만 하면 되는 작업에는 push 권한을 주지 않는 것이 먼저다. push가 필요한 작업이라면 지시문에 "push 전에 git diff --stat @{u}..HEAD를 보고, 예상보다 파일이 많으면 멈춘다"를 적고, 저장소에는 위 훅을 둔다.

정리

push는 지금 고친 것이 아니라 원격에 없는 모든 것을 싣고 간다. 보내기 직전에 커밋과 파일 수를 읽고, 공백만 바뀐 파일은 --stat으로 가려내고, 잊어도 막아 주는 훅을 하나 둔다.