Subversion to Git, Hidden tips
요즘은 Git이 거의 모든 소스 관리의 표준이 된 것 같은데, 그 전에는 오픈소스 기반 형상 관리 도구로는 Bazaar, CVS나 Subversion, 이후에 DVCS(Distrivuted Version Control System)의 출현 후에는 Mercurial 을 포함하여 여러 도구가 많이 쓰였다. 상용 버전 관리 도구에는 Perforce 등도 쓰인다고 들었지만 실제 사용해본 적은 없다.
오늘은 그 중에서 Subversion 기반의 저장소를 Git으로 마이그레이션하는 도중에 알게 된 여러 사실을 정리해두고자 작성하는 글이다. 단순한 마이그레이션이 아니라, 같은 프로젝트의 누적된 SVN 저장소 용량이 너무 커져서 더이상 쓰지 않도록 관리한 저장소 A와 A의 최종이력으로부터 새로운 리비전을 올린 B를 합친 후 Git으로 변환하는 과정이었기 때문에 새롭게 경험한 부분이 많았다.
마이그레이션 과정 자체에는 아래와 같은 글을 참고하였다.
- Git SCM 공식 문서 9.2 Git과 여타 버전 관리 시스템 - Git으로 옮기기
- Koasing님의 SVN to git 이전하기, 윈도우 환경을 기준으로.
Svn dump는 손편집이 가능하다
SVN을 Git으로 마이그레이션하는 과정은 일반적으로 git-svn
명령어를 아래와 같이 호출하는 것만으로 작업이 완료된다.
1 | git svn clone file:///path/to/svn[1] --no-metadata --author-file=authors.txt -s output_git_dir |
하지만 Git으로 마이그레이션이 완료된 후에는 커밋을 편집하는 작업이 번거롭기 때문에, SVN이 익숙하다면 미리 마이그레이션 대상인 저장소를 SVN이 제공하는 도구로 편집을 하고 마이그레이션을 하는 것도 괜찮다. 특히 원래 한 덩어리로 관리하던 저장소를 분할하려는 목적이 있다면 SVN 덤프(dump)를 가공하는 절차가 필요하다.
덤프는 svndump dump (path to repo) > output.dmp
와 같은 명령어로 추출 가능한데, 이 덤프는 놀랍게도 손편집이 가능하다. 기가바이트 단위 파일이더라도, 에디터가 감당할 수 있고 작업자의 멘탈이 부처와 같다면 어느 정도의 편집은 가능하다.
오늘 이 덤프파일을 직접 수정한 건, 서로 히스토리가 끊어진 두 SVN 레포지토리를 합치는 과정에서, 다른 한쪽이 trunk, branch, tag 폴더 없이 바로 밑에 소스 관리를 하는 바람에 경로가 달라 덤프를 이어붙이지 못하는(다음 섹션에서 설명하겠다) 문제가 생겨 직접 수정을 했기 때문이다.
참고로 SVN dump 파일의 내부 구조는 이렇게 작성되어있다. 각 리비전에 대해 리비전 내의 SVN의 변경 이력은 덤프했을 때 이런 형식으로 기록된다. Node-path
가 형상 관리 대상인 파일까지의 경로이고, Node-action
이 어떤 형태의 데이터 변화가 일어났는지 기록하는 필드이다. (여기에서는 add 추가, change 변경이 보인다)
1 | Node-path: src/MySourceCode.cpp |
오늘 했던 작업은 위에서 보는 덤프처럼, Node-path:
가 trunk 경로로 시작되지 않는 덤프와 그렇게 시작되는 덤프 둘의 변경 이력을 합치기 위해 (다행스럽게도 그 차이 외에는 프로젝트 내용도 같고 변경 역사도 이어지는) 수행하는 작업에서 덤프를 수동으로 편집했던 일이었다.
일반 에디터를 사용하면 바이너리로 저장된 파일이 깨질 수도 있고 메모리가 감당하기 힘드므로, sed와 같은 스트리밍 에디터를 사용하는 것이 좋다. 오늘 했던 작업은 다음과 같은 명령어로 처리했다.
1 | sed -i 's@Node-path: @Node-path: trunk/@g' my_svn_repo.dump |
Node-path:
라는 문자열을 Node-path: trunk/
로 대체함으로써 경로를 Node-path: trunk/src/MySourceCode.cs
처럼 맞춰주게 된다. 만일의 사태를 대비해 백업은 꼭 만들고 작업한다.
svndumpfilter를 사용하여 걸러낸 덤프 만들기
svndumpfilter
명령어를 활용하여 일치되는 경로의 파일이나 폴더를 배제하거나, 또는 그것만을 포함한 덤프를 생성할 수 있다.
이 명령어를 활용하여 SVN으로 단일 저장소로 관리하던 여러 프로젝트들을 경로별로 덤프를 분리하여 변환에 사용할 수 있다. 물론 변경 이력까지 그대로 가져갈 수 있다.
오늘 작업에서는 SVN 저장소 내에 포함된 Eclipse 설정 파일과 .classpath
나 .apt_generated
같은 형상 관리 대상이 될 필요가 없는 파일을 배제하고 덤프를 생성하는데 사용했다. 자바 클래스패스 정보 파일은 개발툴에서 pom.xml
이나 build.gradle
을 툴이 처리하여 빌드할 때 자동으로 관리해주므로 해당 파일을 성실히 작성한다면 별도로 클래스패스를 관리할 필요가 없다.
svnadmin load 는 한 저장소에 대해 여러번 호출 가능하다
svnadmin load
는 덤프 파일을 SVN 저장소로 불러올 때 사용하는 명령행 도구인데, 보통은 덤프 -> 덤프 파일 이동 -> svnadmin create
로 빈 저장소 생성 -> 덤프 파일 불러오기 -> 와 신난다! 로 작업이 끝나게 된다.
그런데 해당 명령을 한 저장소에 여러번 실행할 수 있다는 것을 오늘 처음 알게 되었다. 이게 될지 생각해보게 된 계기는 아래와 같은 상황에서 저장소 덤프를 이어 붙일 방법을 고민하다가 좋은 해법이 없을까 고민하는 일이 생긴 일이다. 앞에서도 간단히 언급한 상황인데 이번에는 정확하게 정의해야 설명이 용이하니 정확하게 설명하도록 한다.
SVN 저장소 A와 B가 있다. A와 B는 동일한 폴더 구조를 갖고 동일한 프로젝트의 히스토리를 담고 있다.
A의 리비전은 1에서 1000까지 있고, SVN 레포를 읽고 쓰는 속도가 점점 느려져 여기까지만 사용하고자 한다. 새 저장소로 분리하기로 결정한다.
그래서 새로 만들어진 저장소가 B이고, B의 리비전 1은 A의 리비전 1000 때의 컨텐츠와 같다. 그리고 B는 리비전 201까지 올라갔다.
Git을 도입하면서 퍼포먼스 문제가 해결되어, A와 B를 변경 이력까지 끊어가며 관리할 필요가 없어졌고 A와 B를 이어서 Git 저장소를 구성하고자 한다.
즉 총 1200 리비전(B의 리비전은 A의 1000과 같으므로 빼야)의 저장소를 다시 Git에 구성해야하는데, 덤프 둘을 이어야 하는 문제가 생긴 것.
인터넷을 검색해보는데 이런 경우를 찾기 힘들어서 다음과 같은 가정과 실험을 거쳐 svnadmin load
를 A 저장소 덤프와 B 저장소 덤프에 대해 순차적으로 실행하여 합쳐진 저장소를 만드는 걸로 결론을 얻었다.
svnadmin load
를 여러번 호출하지 마라는 내용은 없었다. load 절차가 최초 1회에 한정되었다는 말은 없었고, 레포지토리가 반드시 비어 있을 필요도 없는 것 같았다.- 시험 삼아 두 테스트 저장소를 만들고, 폴더 경로가 아예 겹치지 않도록 각 저장소에 3개의 변경이력을 만들어 덤프 뜬 후
svnadmin load
를 통합할 저장소에 적용해본 결과, 합쳐질 때 충돌이 나는게 아니고 리비전 1 2 3 4 5 6 형태로 순차적으로 구성된다. 동일 프로젝트에 대한 덤프라면, 시간 순서만 맞춰주면 문제 없는 것 같았다. - A 저장소 덤프와 B 저장소 덤프 사이에 빠진 리비전이 존재해도 되지만, 빠진 리비전은 다음과 같은 경우에 해당하지 않으면 나중에 오류가 발생될 수 있다. 오류가 발생된 저장소는 더이상 무결하게 덤프를 불러 올 수 없어 다시 처음부터 해야한다.
- 두 덤프 간에 파일이나 폴더 변경사항 간섭이 없어야 한다.
예) A 덤프에서는 projectA 이하 경로의 변경 이력만이 존재하고 B 덤프에는 projectB 이하 경로의 변경 이력만이 존재하면
svnadmin load < A.dump
실행 후svnadmin load < B.dump
를 실행하여도 문제가 없다. - 두 덤프 간에 파일이나 폴더 변경사항이 간섭이 있다면(=동일 파일에 대한 이력이 있다면), 시간 순서나 사건 발생 순서가 맞아야 한다.
예) A B C 덤프는 F라는 프로젝트 저장소에서 분할 생성된 덤프이다. A 덤프에서는 project/B.txt 가 생성된 후 B.txt에 Apple이란 문구를 추가하고 저장한 이력이 있고, B 덤프에는 project/B.txt의 Apple이란 문구를 Banana로 고쳤고, C 덤프에서는 project/B.txt에서 Banana란 문구를 Cherry로 변경한 이력이 담겨 있다고 하자.
A -> B -> C 순서대로 덤프를 불러오는 건 시간과 사건 발생 흐름이 맞기 때문에 문제가 없다. B의 diff 패치가 A의 project/B.txt에 적용되고, B의 패치가 적용된 project/B.txt에 C의 패치를 적용하는 흐름으로 생각하면 된다.
그러나 A -> C 순서는 불가능하다. project/B.txt 가 생성된 이력은 있지만 변경 이력이 끊어졌기 때문. B -> C 만 하는 것도 불가능하다. B 덤프를 불러오려면 project/B.txt 가 생성된 이력이 있어야 하는데 그 이력은 A 덤프에 있기 때문. C -> B -> A 는 당연히 불가능하다.
- 두 덤프 간에 파일이나 폴더 변경사항 간섭이 없어야 한다.
다시 본래 이야기로 돌아와서, 병합하려는 두 A와 B 저장소는 B의 첫번째 리비전, 즉 A의 마지막 파일 상태를 한번에 복붙하여 만든 커밋을 제외하고는 개별 파일과 폴더에 대해 변경 이력이 연속적으로 이어지고 있으므로 svnadmin load
를 A 저장소 덤프와 B 저장소 덤프 순서대로 호출하여도 별 문제가 없다.
실제로 이렇게 작업해본 결과, 무사히 두 덤프가 하나의 저장소에 로드되었다. 물론 B 저장소에 대한 덤프를 생성할 땐 아래와 같은 옵션으로 첫번째 리비전을 버리는 센스와 증가분만 덤프 떠서 빠르게 작업하는 센스를 발휘하자. HEAD
는 마지막 리비전을 뜻한다. Git의 헤드는 이동될 수 있으므로 SVN에서의 의미와 약간 다를 수 있다.
1 | svnadmin dump -r 2:HEAD --incremental file:///path/to/repo > B.dump |
이걸로 제일 큰 문제를 해결했다. 추가로 실행해보진 못했는데, 덤프 파일을 수동으로 편집 가능한 점을 이용하여 한번에 불러올 수 있는 통합 덤프를 만들 수 있지 않을까 생각도 했다. 다만 A 저장소 덤프가 2GB를 넘어서서 에디터로 편집하기엔 엄두가 나질 않았다.
git-svn 은 Perl로 작성되어 있다.
대부분 Git을 설치하면 git-svn이 함께 제공되는데, 별도로 Git을 컴파일하여 설치해본 적이 없어서 그냥 여느 Git의 기능과 마찬가지로 C 언어를 사용해 작성된 줄 알았다.
그런데 이번에 Git을 패키지 매니저 쓰지 않고 소스로부터 컴파일하여 설치할 일이 생겼는데, 컴파일할 때 SVN perl 바인딩 지원을 위한 subversion-perl
이 깔려있지 않으니 git svn
명령어가 실행되지 않는 진귀한 경험을 하게 되었다.
더불어 해당 명령어는 perl의 메시지 다이제스트(즉, Hash)에 필요한 perl 라이브러리 또한 추가로 설치해야할 수 있다. 오류 나는대로 차근차근 OS의 패키지 매니저를 사용해 설치하면 된다. 해당 패키지들은 RHEL 기준으로 perl-Digest
라는 이름으로 시작된다.
위의 일련의 작업들은 리눅스에서 할 것
위의 일련의 작업들은 리눅스에서 하는 편이 좋다. WSL은 어떤 성능이 나올지 모르겠는데, 작업 과정에서 I/O 퍼포먼스가 굉장히 중요하고 여기에 오버로드가 적게 걸릴 수록 작업이 빨리 끝난다.
SVN과 Git 모두 리눅스에서 네이티브로 돌아갈 때 가장 우수한 퍼포먼스를 보여준다. 실제로 윈도우에서 TortoiseSVN 사용해서 덤프를 불러오고 수정하는 작업을 진행하려고 했으나 도중에 SVN이 급격히 느려지고 프리징 되어서 작업을 진행할 수 없었다.
대충 리눅스 쪽의 파일시스템이 Ext3이어도 퍼포먼스는 훨씬 나을 걸로 예상된다.
글을 마치며
시작하면서 오늘 오후 안에 끝낼 수 있을까 의문도 든 과제이긴 했지만 무사히 계획한대로 마칠 수 있었다.
완전히 끝난 건 아니고, 이제 시험 생성된 Git 저장소를 실제 서버에 올리고 git clone
도 해보고 IDE에서 불러와서 빌드도 해보고 소스가 빠진게 있는지 비교해봐야 하는 여러 검증 작업이 남았긴 하다.
물론 지금 글을 쓰면서 숨겨진 팁들을 정리하는 이 순간이 오기까지, SVN 저장소를 여러번 생성하고 지우고 덤프도 여러번 만들고 버렸던 시간이 있었다. 서버 하드디스크가 이러다가 닳는 거 아닐까 생각도 했지만 이런 거 아까워하면 안될 것 같았다. 모니터에 뭔가 몇십분 동안 주르륵 올라가는 걸 보면 회사 윗분들은 이 친구가 열심히 일하고 있다고 생각한다. 물론 덤프를 만들거나 불러오는 동안에 잠시 쉬기도 했지만, 다음 절차를 찾느라 고생했으니 완전히 논 것만은 아닌 셈.
모든 마이그레이션 작업이 끝나고 나면, 더이상 SVN은 만지고 싶지 않다. Git 만세!
- 1.
file:///
는 로컬에 있는 경로의 저장소를 불러올 때 쓰는 URL 형식. ↩