LabHub
배우기 러닝패스 코스

Git実戦

1,000のコミットから犯人を10回で見つける

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

"두 달 전엔 됐는데 지금은 안 된다" 에서 커밋 하나를 특정하는 데 필요한 검사 횟수는 커밋 수가 아니라 log₂(커밋 수) 입니다. 1,000개면 10번입니다.

概念マップ: log₂(커밋 수)・종료코드로・skip・빌드되고 실행되는 상태

왜 이게 필요했나

버그 리포트가 왔습니다. "예전엔 잘 됐는데요."

git log 를 열면 그 사이에 커밋이 800개 있습니다. 하나씩 확인하면 800번이고, 눈으로 훑으면 "관련 있어 보이는" 커밋에 편향됩니다. 그리고 경험상 범인은 대개 관련 없어 보이는 커밋입니다.

git bisect 는 이진 탐색을 자동으로 해 줍니다.

git bisect start
git bisect bad                 # 지금은 깨져 있다
git bisect good v1.4.0         # 이 버전에서는 됐다
# → git 이 중간 커밋으로 체크아웃한다
# 확인 후
git bisect good   또는   git bisect bad
# 반복. 10번이면 끝난다.
git bisect reset

자동화 — bisect run

검사를 스크립트로 쓸 수 있으면 사람이 앉아 있을 필요도 없습니다.

git bisect start HEAD v1.4.0
git bisect run ./check.sh

check.sh종료코드로 말합니다.

종료코드
0 good
1–124, 126, 127 bad
125 skip — 이 커밋은 판정 불가(빌드 실패 등)

125가 중요합니다. 중간에 빌드가 깨진 커밋이 섞여 있을 때, 그걸 bad 로 판정하면 엉뚱한 곳으로 수렴합니다. 판정할 수 없으면 skip 을 돌려야 합니다.

#!/bin/bash
make build || exit 125          # 빌드 실패 = 판정 불가
./run-test.sh || exit 1         # 테스트 실패 = bad
exit 0                          # 통과 = good

bisect 를 위한 전제

이등분이 잘 되려면 커밋 하나하나가 빌드되고 실행되는 상태여야 합니다. "WIP", "작업 중", "일단 커밋" 같은 커밋이 잔뜩이면 skip 만 늘어납니다. 평소에 작고 완결된 커밋을 쌓는 습관이 여기서 값을 합니다.

log -S — 그 코드가 언제 들어왔나

특정 문자열이 추가되거나 삭제된 커밋만 찾습니다.

git log -S 'timeout=3' --oneline

"이 설정값이 언제 3으로 바뀌었지?" 에 한 번에 답합니다. 정규식으로 찾으려면 -G 를 씁니다.

git log -p -- path/to/file 로 파일 하나의 변경 이력만 볼 수도 있고, --follow 를 붙이면 이름이 바뀐 이력까지 따라갑니다.

blame — 누가 아니라 왜

git blame 은 각 줄을 마지막으로 고친 커밋을 보여 줍니다. 목적은 책임자 찾기가 아니라 맥락 찾기 입니다. 그 줄이 왜 그렇게 쓰였는지는 커밋 메시지와 그 커밋의 다른 변경에 있습니다.

git blame -L 40,60 src/app.py        # 40~60행만
git blame -w                         # 공백 변경 무시
git blame -C                         # 다른 파일에서 옮겨온 코드도 추적

-w 가 특히 유용합니다. 포매터를 한 번 돌린 저장소에서는 모든 줄이 "포매팅 커밋" 으로 나오는데, -w 는 그걸 건너뛰고 실제 변경을 찾아 줍니다.

reflog — 잃어버린 것을 되찾기

git reflog 는 HEAD 가 움직인 모든 기록입니다. 리베이스를 잘못했거나 브랜치를 지웠어도, 커밋 자체는 한동안 살아 있습니다.

git reflog
# a1b2c3d HEAD@{5}: rebase (finish): returning to refs/heads/main
# 9f8e7d6 HEAD@{6}: commit: 잃어버린 작업
git checkout -b rescue 9f8e7d6

"git 에서 커밋한 것은 웬만하면 안 사라진다" 는 말의 근거가 reflog 입니다.

현장에서 만나는 모습

이등분이 막힐 때 뚫는 법

git bisect 는 강력한데, 실제로 돌려 보면 판정할 수 없는 커밋에서 막힌다. 그때 쓰는 손이 몇 가지 있다.

빌드가 안 되는 커밋은 건너뛴다. bad 로 표시하면 결과가 틀어진다.

git bisect skip                    # 이 커밋으로는 판단할 수 없다
git bisect skip v2.1..v2.2         # 구간 통째로

너무 많이 건너뛰면 범위가 좁혀지지 않으므로, 그때는 판정 스크립트가 빌드 실패를 125로 반환하게 만든다. bisect run 은 125를 "건너뛰기" 로 읽는다.

cat > /tmp/check.sh <<'EOF'
#!/bin/bash
make -s build || exit 125         # 판정 불가
./run-test || exit 1              # 나쁨
exit 0                            # 좋음
EOF
git bisect run bash /tmp/check.sh

병합 커밋이 많으면 first-parent 만 본다. 기능 브랜치 안의 중간 커밋까지 훑으면 오래 걸리고, 그 커밋들은 애초에 시험되지 않은 상태다.

git bisect start --first-parent BAD GOOD

증상이 간헐적이면 이등분은 거짓말을 한다. 열 번에 한 번 나는 문제라면 판정 스크립트가 여러 번 시도하게 만든다. 그래도 확률적이라면 이등분보다 log -S 로 그 코드가 언제 들어왔는지 찾는 편이 빠르다.

git log -S 'setTimeout(retry' --oneline -- src/
git log -L :handleRetry:src/client.js      # 그 함수의 변천사만

끝나면 반드시 되돌린다. git bisect reset 을 안 하면 detached HEAD 에 남아 있고, 그 상태로 커밋하면 나중에 찾기 어렵다.

되돌리는 것이 목적이 아니다. 범인 커밋을 찾았다고 그대로 되돌리면, 그 커밋이 고치던 다른 문제가 되살아난다. 왜 그 변경이 이 증상을 만드는지까지 보고 고치는 것이 이등분의 값이다.

다음 실습에서 할 것

40개짜리 이력을 직접 만들고 범인을 찾습니다. 그 이력에는 판정할 수 없는 커밋이 하나 섞여 있어서, 125 를 안 쓰면 bisect 가 엉뚱한 커밋을 확신 있게 지목합니다. 두 스크립트를 나란히 돌려 그 차이를 눈으로 봅니다.

그리고 125 를 제대로 쓰면 이번에는 bisect 가 후보 두 개까지만 좁히고 멈춥니다. 마지막 한 걸음은 사람이 정합니다 — 자동화가 끝나는 자리가 어디인지가 이 실습의 결론입니다.