발주처는 SVN인데 나는 브랜치가 필요했다
한 워킹카피에 .svn과 .git을 겹쳐 두고 쓰는 방법. 그리고 개행 문자 하나 때문에 SVN 커밋이 통째로 오염되는 함정.
공공 SI 현장에 들어가면 형상관리는 대체로 SVN이다. 그것도 브랜치 없이 trunk 하나로 굴러간다.
여기까진 괜찮다. 문제는 내가 하려던 작업이 관리자 화면 100여 개를 구조부터 갈아엎는 일이었다는 거다. 중간에 몇 번이고 되돌려야 하고, 절반쯤 하다가 운영 이슈가 터지면 그걸 먼저 처리하고 돌아와야 한다.
trunk에 바로 커밋하면서 할 수 있는 일이 아니었다.
git svn을 안 쓴 이유
먼저 떠오르는 건 git svn이다. SVN 저장소를 git처럼 쓰게 해주는 공식 도구고, 이런 상황을 위해 만들어졌다.
안 썼다. 두 가지가 걸렸다.
git svn clone은 리비전을 하나씩 훑어서 히스토리를 재구성한다. 리비전이 천 단위로 쌓인 저장소에서는 몇 시간에서 며칠이 걸린다. 그것만이면 한 번 참고 말겠는데, 진짜 문제는 dcommit이다.
dcommit은 내 git 커밋을 SVN 리비전으로 다시 쓴다. 내 로컬 브랜치 편의를 위해 발주처 저장소의 이력에 손을 대는 셈이다. 나 혼자 쓰는 저장소면 몰라도, 다른 개발자들이 같은 trunk를 보고 있는 상황에서 감당할 리스크가 아니었다.
내가 원한 건 SVN을 대체하는 게 아니라, SVN은 그대로 두고 내 작업만 브랜치로 관리하는 것이었다.
한 폴더에 둘 다 둔다
그래서 워킹카피 하나에 .svn과 .git을 겹쳐 뒀다.
$ ls -a
.git .svn src/ pom.xml ...
SVN 입장에서는 평소와 똑같은 워킹카피다. git 입장에서는 그냥 파일이 든 폴더고. 서로 모른 채로 같은 파일들을 본다.
git이 SVN 메타데이터를 추적하면 안 되니까 이것만 막아둔다.
# .gitignore
.svn/
그리고 master를 SVN 미러 전용으로 정한다. 여기엔 사람이 직접 커밋하지 않는다. 오직 SVN에서 받아온 상태만 박제한다. 내 작업은 전부 브랜치에서 한다.
master ← SVN에서 받아온 상태만. 손으로 커밋 금지
├─ tech-trade ← 기술거래 이관 작업
└─ admin-console ← 관리자 콘솔 전환 작업
동기화 루틴
며칠에 한 번, SVN의 변경을 가져와 내 브랜치에 합친다.
git checkout master
svn update # 운영 변경사항 받기
svn status # ← 이 줄을 빼먹으면 사고난다 (아래 참고)
git add -A
git commit -m "snapshot: SVN 동기화 (rev 1059 → rev 1075)"
git checkout admin-console
git merge master # 여기서 충돌나면 git이 잡아준다
커밋 메시지에 리비전 범위를 적어두는 게 핵심이다. 나중에 "이 변경이 SVN 몇 번에서 온 거지"를 추적할 유일한 단서다. git에는 SVN 리비전 정보가 없으니까.
반대 방향, 그러니까 내 작업을 SVN에 올릴 때는 브랜치를 master에 머지한 뒤 svn commit 한다. git 히스토리는 SVN에 안 올라가고 최종 결과만 리비전 하나로 들어간다. 그게 맞다 — 발주처가 볼 이력에 내 시행착오 60개가 필요하진 않다.
함정 1: 개행 문자
여기서 제일 크게 데였다.
git은 기본적으로 개행을 만진다. core.autocrlf가 켜져 있으면 체크아웃할 때 LF를 CRLF로, 커밋할 때 CRLF를 LF로 바꾼다. 혼자 쓰는 저장소에서는 편한 기능이다.
그런데 이 워킹카피는 SVN도 같이 보고 있다. git이 파일 바이트를 바꾸는 순간 SVN 눈에는 그 파일 전체가 수정된 것으로 보인다. 한 줄만 고쳤는데 svn status에 안 건드린 파일 수십 개가 M으로 뜬다.
그 상태로 svn commit을 하면 의도하지 않은 파일이 통째로 올라간다. 운영 저장소에.
git config core.autocrlf false
.gitattributes도 두지 않는다. * text=auto 한 줄이면 같은 일이 벌어진다. 이 저장소에서 git의 역할은 바이트를 그대로 보존하는 것이지 정규화하는 게 아니다.
그런데도 세 번 사고가 났다. 편집기가 파일을 저장하면서 개행을 바꿔버린 경우였다. 한 파일 안에 CRLF와 LF가 섞여 있으면 편집기가 "통일"해 주는데, 그게 SVN에는 전면 수정으로 보인다.
커밋 전에 이걸로 확인한다.
# SVN이 보는 변경 파일 목록. 내가 건드린 것만 나와야 정상이다
svn status | grep '^M'
# 특정 파일의 개행이 SVN 원본과 같은지 (git이 아니라 파일 자체를 본다)
file src/main/java/.../AdmUserMng_SQL.xml
# → ASCII text, with CRLF line terminators ← 원본과 같아야 한다
함정 2: 중단된 svn update
svn update 도중 충돌이 나면 SVN은 파일 안에 충돌 마커를 넣는다.
<<<<<<< .mine
conn.setReadTimeout(0);
=======
conn.setReadTimeout(30000);
>>>>>>> .r1075
이걸 해결하지 않은 채로 git add -A를 하면 충돌 마커가 git 히스토리에 그대로 박힌다. 나중에 그 커밋을 브랜치에 머지하면 마커가 딸려 들어가고, 빌드는 당연히 깨진다.
찾기도 번거롭다. git 입장에서는 정상적인 텍스트 변경이라 아무 경고도 안 준다.
그래서 동기화 루틴에 svn status가 들어 있다. C로 시작하는 줄이 하나라도 있으면 git 쪽은 건드리지 않는다.
svn status | grep '^C' && echo "충돌 미해결 — 스냅샷 중단"
함정 3: master에 손대기
바쁠 때 master에서 바로 고치고 싶어진다. 한 줄짜리 수정이니까.
그러면 다음 스냅샷에서 뭐가 SVN에서 온 변경이고 뭐가 내 수정인지 구분이 안 된다. svn update가 그 파일을 덮어쓰면 내 수정은 조용히 사라지고, git에는 "SVN 동기화" 커밋이 그걸 지운 것처럼 남는다.
한 줄이어도 브랜치에서 한다. 예외를 두면 규칙이 아니다.
정리
- SVN 저장소를 바꿀 수 없다면 대체하려 하지 말고 얹는다. 한 워킹카피에
.svn과.git을 같이 두고.svn/만 gitignore master는 SVN 미러 전용. 커밋 메시지에 리비전 범위를 적는다. 그게 유일한 추적 단서다core.autocrlf=false,.gitattributes없음. git이 바이트를 만지는 순간 SVN 커밋이 오염된다- 스냅샷 전에
svn status로 충돌 확인. 이걸 빼면 충돌 마커가 히스토리에 박힌다
git svn이 더 정석이긴 하다. 발주처 저장소를 내가 통제할 수 있고 리비전이 많지 않다면 그쪽이 낫다. 다만 남의 저장소 이력에 손대지 않으면서 브랜치만 쓰고 싶다면, 겹쳐 두는 쪽이 훨씬 단순하다.