누가 고쳤는지 보려고 이력을 봤는데 이름이 여러 가지였다.
Table of contents
Open Table of contents
증상 — 작성자가 여러 가지였다
작성자를 뽑아 봤다.
$ git log --format='%an <%ae>' | sort -u
root <root@web01.localdomain>
ubuntu <ubuntu@ip-10-0-0-31>
개발자 <dev@example.com>
jenkins <jenkins@jenkins>
root 와 ubuntu 와 jenkins 까지 같은 사람이 낸 커밋이 서버마다 다른 이름으로 남아 있었다.
git blame 으로 한 줄씩 볼 때도 같은 사람이 여럿으로 갈려서 누가 무엇을 했는지를 이력으로 못 따라갔다.
원인 — 값이 오는 세 층
user.email 은 설정에서 오고 없으면 자동으로 채워진다.
$ git config --show-origin --get user.email
(없음)
어느 층에도 없으니 시스템 사용자와 호스트 이름을 붙여 root@web01.localdomain 이 됐다.
설정은 세 층이다.
system /etc/gitconfig
global ~/.gitconfig
local <저장소>/.git/config
local 이 global 을 덮으므로 어느 층에서 온 값인지 봐야 한다.
서버에서 root 로 작업하면 그 계정의 설정을 보는데 거기에 아무것도 없으니 호스트 이름으로 만들어진 값이 그대로 남았다.
jenkins 로 남은 것도 같은 경로였다. 사람이 아니라 계정이 남는 셈이라 누가 했는지는 그 줄만 봐서는 알 수 없다.
값이 없을 때 오류를 내지 않고 조용히 채우는 것이 문제의 시작이었다.
제약 — 하나로 맞추는 것이 답은 아니다
전부 한 값으로 통일하려다 걸렸다.
회사 저장소 회사 주소
개인 저장소 개인 주소
한 사람이 둘을 오가는데 global 에 하나만 두면 반대쪽에 잘못된 주소가 박힌다.
경로별로 갈리게 뒀다.
# ~/.gitconfig
[user]
name = ...
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
[includeIf "gitdir:~/personal/"]
path = ~/.gitconfig-personal
includeIf 로 폴더가 갈리니 저장소마다 손으로 설정하지 않아도 된다.
통일이 목적이 아니라 의도한 값이 들어가는 것이 목적이었고 그 목적으로 보면 하나로 맞추는 것은 답이 아니었다.
사외 협업 저장소처럼 다른 주소를 써야 하는 사정도 있었다. 그런 자리를 local 로 지정해 두면 그 저장소에서만 다른 값이 걸린다.
설정 — 없으면 멈추게
기본값으로 조용히 채워지는 것을 막았다.
$ git config --global user.useConfigOnly true
useConfigOnly 를 켜면 설정이 없을 때 커밋이 실패한다.
fatal: no email was given and auto-detection is disabled
호스트 이름으로 만들어 채우지 않고 그 자리에서 멈춘다.
조용히 채워지면 몇 달 뒤 이력을 볼 때야 드러나고 그때는 이미 수백 건이 섞인 뒤다. 새로 환경을 만든 사람이 첫 커밋에서 알아채는 편이 훨씬 쌌다.
멈추는 것이 불편해 보이지만 그 불편은 한 번뿐이다. 조용히 지나간 것을 나중에 되돌리는 쪽이 몇 배로 비싸다.
조치 — 서버 커밋 경로 제거
서버 계정으로 남은 커밋이 왜 있는지 봤다.
급한 수정을 서버에서 하고 커밋했다
배포 스크립트가 설정 파일을 고치고 커밋했다
앞은 규칙으로 막았고 뒤는 generated.php 를 커밋할 이유가 없었다.
# .gitignore
/config/generated.php
/public/build/
/REVISION
배포가 만드는 것을 추적에서 뺐다.
추적하면 배포마다 변경이 생기고 그 변경이 다음 배포를 막는다. 막히면 사람이 서버에서 커밋하게 되므로 결국 같은 자리로 돌아온다.
경로 자체를 없애야 그 계정으로 남는 커밋이 안 생긴다.
검증 — 이미 섞인 것과 앞으로
과거 커밋의 작성자는 고치지 않기로 했다.
고치면 모든 커밋 식별자가 바뀌고 이미 여럿이 받아 간 상태라 그 기준이 어긋난다.
# .mailmap
개발자 <dev@example.com> <root@web01.localdomain>
개발자 <dev@example.com> <ubuntu@ip-10-0-0-31>
.mailmap 을 두어 보는 쪽에서 묶이게 했다.
$ git shortlog -sne
412 개발자 <dev@example.com>
38 jenkins <jenkins@jenkins>
shortlog 의 집계만 맞고 이력 자체는 그대로다.
새로 들어오는 것은 받는 쪽에서 걸렀다.
#!/bin/sh
# pre-receive
while read old new ref; do
for c in $(git rev-list "$old..$new"); do
email=$(git log -1 --format='%ae' "$c")
case "$email" in
*@example.com) ;;
*) echo "허용되지 않은 작성자: $email ($c)"; exit 1 ;;
esac
done
done
각자 local 설정에 기대면 언젠가 어긋나므로 pre-receive 에서 막는다.
일부러 다른 주소로 커밋해 밀어 보고 거부되는지 확인했고 그것을 보고 나서야 그 훅을 믿을 수 있었다.
통과만 보고 작동한다고 판단하면 아무것도 안 거르는 훅을 그대로 두게 된다. 거르는 장치는 거르는 것을 봐야 검증이 끝난다.
정리
- 작성자 설정이 없으면 시스템 사용자와 호스트 이름으로 채워진다
- 값이 없을 때 오류 대신 조용히 채우는 것이 시작이다
- 설정은 세 층이고 아래가 위를 덮는다
- 어느 층에서 온 값인지 확인한다
- 전부 하나로 맞추는 것이 답은 아니다
- 경로별로 갈리게 두면 손으로 설정하지 않아도 된다
- 통일이 아니라 의도한 값이 들어가는 것이 목적이다
- 설정이 없을 때 조용히 채우지 말고 멈추게 한다
- 서버 계정으로 남은 커밋은 그 경로 자체를 없앤다
- 배포가 만드는 파일을 추적하면 다음 배포가 막힌다
- 이미 섞인 이력은 고치지 말고 대응표로 집계만 맞춘다
- 새로 들어오는 것은 받는 쪽에서 거르고 일부러 어겨 확인한다