비교 기준 태그를 옮기자 사라진 수정 기록과 결함 11건
git 태그를 비교 기준으로 쓰는 도구에서 태그를 새 커밋으로 옮기자 그 사이의 원고 수정이 기록에서 사라졌다. 기준 갱신 앞에 확인 단계를 두고, 한 저장소를 나눠 쓰는 프로젝트는 태그 이름에 범위를 넣었다.
공개 조회 0
TL;DR
- 5화에 단 git 태그를 7화로 옮기자 그 사이 고친 2화가 "바뀐 파일" 목록에서 사라졌다
- 태그가 비교 기준이라서다. 기준을 옮기면 그 전의 차이는 비교 범위 밖으로 나간다
- 한 저장소에 작품 둘을 두면 태그 이름이 겹쳐 A 작품의 태그를 B 작품이 자기 것으로 읽었다
- 비교 기준을 옮기는 명령은 옛 기준과 차이가 있으면 거부하고 사람이 확인했다는 옵션을 따로 받는다
상황
웹소설 원고를 git 으로 관리하는 도구를 만들고 있다. 연재 플랫폼에 회차를 올릴 때마다 그 시점의 커밋에 태그를 단다. 1화부터 5화까지 올렸으면 published/5 다.
태그를 다는 이유는 나중에 이 질문에 답하려는 것이다.
이미 올린 회차 중에 그 뒤로 고친 것이 있나?
독자가 이미 읽은 회차의 설정을 나중에 바꾸면 독자는 그것을 설정 오류로 받아들인다. 그래서 도구는 이 명령으로 찾는다.
git diff --name-only published/5 HEAD
태그 이후 바뀐 파일 중 5화 이하만 골라 "발행 후 수정"으로 알린다. 이 글에서는 이것을 수정 검사라고 부른다.
증상
예를 들어 이런 일주일이다. 직전 버전(v1.25.0)의 동작이고, 실험 저장소에서 같은 순서로 재현했다.
| 요일 | 작가가 한 일 | 도구가 한 일 |
|---|---|---|
| 월 | 1~5화를 올렸다 | 태그 published/5 |
| 화 | 2화에서 주인공 나이를 17세에서 19세로 고쳤다. 플랫폼에는 아직 안 올렸다 | 수정 검사: 2화가 바뀌었다 |
| 수 | 6~7화를 올렸다 | 태그 published/7 |
| 목 | 수정 검사: 바뀐 회차 없음 |
목요일의 결과가 틀렸다. 플랫폼의 2화에는 여전히 17세가 적혀 있다. 원고와 독자가 읽은 본문이 다른데 도구는 다르지 않다고 말했다.
git 으로 보면 바로 보인다(한글 경로를 그대로 보려고 core.quotepath=false 를 준다).
git -c core.quotepath=false diff --name-only published/5 HEAD
# 원고/2화.md, 원고/6화.md, 원고/7화.md (5화 이하는 2화뿐)
git -c core.quotepath=false diff --name-only published/7 HEAD
# (출력 없음)
반전
비교 기준이 되는 태그를 새 커밋으로 옮기는 일은 "지금까지의 차이를 정상으로 받아들인다"는 결정이다. 그러니 옮기기 전에 옛 기준과의 차이를 보여 주고, 사람이 확인한 뒤에만 옮긴다.
수요일의 태그는 화요일의 수정이 들어간 커밋에 달렸다. 그 순간부터 기준은 "19세인 2화"다. 수정 검사는 기준 이후만 보므로 2화의 수정을 다시는 못 본다.
flowchart LR
A["월: published/5<br/>2화 17세"] --> B["화: 2화를 19세로 수정"]
B --> C["수: published/7<br/>2화 19세"]
C --> D["목: 7과 HEAD 비교<br/>차이 없음"]
이 모양은 흔하다. Jest 스냅숏 테스트에서 jest -u 로 스냅숏을 갱신하면 실패하던 차이가 그 순간 새 정답이 된다. detekt 나 Android Lint 의 기준선(baseline) 파일을 다시 만들 때도 그때까지의 경고가 정상으로 들어간다. 차이를 보고 갱신하면 문제가 없다. 차이를 안 본 채 갱신하면 그 차이는 기록에서 빠진다.
이 태그를 다는 명령은 publish 이고 문서에서는 "발행 표시"라고 불렀다. 이름만 보면 기록을 하나 더하는 일이라 비교 기준을 바꾼다는 것이 잘 안 보였다. 실행 순서를 적은 문서에도 "태그를 달기 전에 수정 검사를 돌린다"는 단계가 없었다. 첫 버전부터 같은 순서였다.
처리
문서에 단계를 넣는 것으로는 부족했다. 문서를 건너뛰면 같은 결과가 난다. 그래서 명령이 직접 거부하게 했다.
$ python wngit.py publish . --upto 7
발행 뒤 고친 회차가 1건 있다 (기준 `published/5`):
2화 `원고/2화.md`
이대로 새 표시를 남기면 이 수정이 「발행된 것」이 되어 다시는 안 잡힌다.
먼저 작가와 확인하라:
- 고친 회차를 플랫폼에도 다시 올렸거나, 오탈자만 고쳤다 → --accept-drift 로 표시
- 플랫폼에는 옛 본문이 있다 → 고친 것을 되돌리거나 플랫폼에도 반영한 뒤 표시
고친 내용 보기: git diff published/5 HEAD -- <파일>
작가가 확인하면 --accept-drift 옵션으로 태그를 단다. 태그 메시지에 "발행 뒤 고친 회차 2화 확인함"이 남아 기준이 왜 옮겨졌는지 나중에도 보인다.
한 저장소에 작품 둘
저장소 전체가 같이 쓰는 이름에 프로젝트 하나의 상태를 적을 때는 이름에 프로젝트 범위를 넣는다. 범위 없는 옛 이름은 자동으로 이어받지 말고, 이름마다 주인에게 묻는다.
git 태그 이름은 저장소에 하나뿐이다. 작가가 저장소 하나에 작품을 폴더로 나눠 두었다.
소설들/
작품A/원고/1화.md …
작품B/원고/1화.md …
작품 A 를 3화까지 올리고 published/3 을 달자 한 화도 안 올린 작품 B 도 "3화까지 올림"이 됐다. B 의 1~3화는 올리기 전 표기 검사(마크다운 기호가 남았는지 보는 것)에서도 빠졌다.
모노레포 도구가 같은 문제를 이미 풀었다. Lerna 의 독립 버전 모드나 Changesets 는 패키지마다 버전을 따로 매기고 패키지이름@1.2.0 꼴로 태그를 단다. 같은 방식으로 하위 폴더 작품의 태그 앞에 폴더 경로를 붙였다(작품A/published/3). 저장소 루트의 작품은 이름이 그대로다.
git 태그 이름에는 공백을 못 쓴다. 공백을 _ 로 바꾸기만 했더니 작품 A 와 작품_A 두 폴더가 같은 이름 작품_A 가 되어 서로의 기록을 읽었다. 글자를 바꾼 이름 뒤에는 원래 폴더 이름의 SHA-1 앞 6자리를 붙여(작품_A-96ef95) 둘을 갈랐다.
남은 것은 예전 버전이 경로 없이 단 published/5 다. 작품이 하나면 그 작품 것이지만 둘이면 누구 것인지 모른다. 그래서 도구는 그 태그를 자기 것으로 읽지 않고, 작가가 답할 때까지 새 태그도 막는다. 작가가 "이 작품 것"이라고 답하면 그 태그가 가리키던 커밋에 새 이름의 태그를 단다. 지금 커밋에 달면 앞 절과 같은 일이 또 생긴다.
처음에는 옛 태그 묶음 전체에 한 번만 답을 받았다. 그런데 작품 둘이 공용 이름을 나눠 썼다면 published/5 는 A 것이고 published/2 는 B 것이다. "전부 다른 작품 것"을 고르면 B 의 2화 수정 기록이 사라지고, "전부 이 작품 것"을 고르면 A 의 기록을 B 가 가져간다. 지금은 목록을 날짜와 함께 보이고 이 작품 것만 골라 원래 설명과 날짜 그대로 옮긴다.
검증
나머지 결함 아홉도 이 버전에서 고쳤다. 원고에 없는 9화까지 태그를 달 수 있던 것, 프롤로그.md 를 표기 검사가 건너뛰던 것 등이다.
직전 버전의 회귀 테스트는 태그를 하나만 만들었다. "가장 최근 태그 고르기"와 "태그 번호순 정렬"은 태그가 둘 이상일 때만 실행된다. 새 테스트는 published/9 다음에 published/10 을 단다. 문자열로 정렬하면 9 가 10 뒤에 선다.
테스트가 버그를 잡는지는 변이 테스트로 봤다. 코드에 버그를 한 곳씩 일부러 넣은 사본을 만들고 테스트가 실패하는지 본다. 처음 43개는 33개만 잡혔고, 빠진 자리에 테스트를 더해 41개가 됐다. 그런데 작성자와 따로 돌린 리뷰어가 새 코드를 겨눠 51개를 만들자 35개만 잡혔다. 내가 고른 버그 자리가 내가 테스트를 짠 자리와 겹쳐 있었다. 폴더 이름을 바꾸는 규칙처럼 테스트가 한 번도 안 지나가는 코드는 내 표본에 없었다.
리뷰어가 찾은 자리에 테스트를 더하고 버그 목록을 74개로 늘렸다. 71개에서 테스트가 실패했다. 이 수치도 내가 고른 표본이라 리뷰어 표본과 견줄 수 없다.
개선
| 직전 버전 | 이번 버전 | |
|---|---|---|
| 고친 회차가 있는 채로 다음 태그 | 태그가 달리고 수정 기록이 사라짐 | 거부, 확인 후 기록과 함께 태그 |
| 한 저장소의 작품 둘 | 태그를 둘이 같이 씀 | 작품마다 따로 |
| 원고에 없는 9화까지 태그 | 달림 | 거부 |
| 프롤로그의 마크다운 기호 | 위험 0건 | 검사함 |
남은 것
- 변이 테스트에서 살아남은 셋: 직전 버전 코드 한 줄, 실행되지 않는 숫자 판단, 설정 모듈을 못 읽을 때만 쓰는 대체 경로. 그 상태를 만드는 테스트 데이터가 없다
- 폴더 이름 끝의
.lock같은 이름 규칙 일부는 테스트가 안 지난다. 실험으로만 확인했다 - 프롤로그를 이미 올렸는지는 "마지막 태그 때와 내용이 같은가"로 추정한다. 안 올리고 태그만 달았다면 틀린다. 그런 실제 사례는 아직 없다
다음부터
비교 기준을 새 커밋으로 옮기는 명령을 만들 때는 옛 기준 이후의 차이가 있으면 거부하고 사람이 확인했다는 옵션을 따로 받는다.
한 저장소를 여러 프로젝트가 나눠 쓸 수 있으면 태그 같은 공용 이름에 프로젝트 범위를 넣고 범위 없는 옛 이름은 이름마다 주인에게 물은 뒤에만 옮긴다.