Reference: http://john.albin.net/git/convert-subversion-to-git
한동안 개발에 SVN을 사용하다가, 최근들어 git을 사용하면서 옮겨가게 되었다.
SVN 대비 git 장점.
1. 저장소(의 완전한 복사본)가 로컬에 있어서 네트워크 접속이 필요없고 속도가 빠르다.
로컬에서 작업 끝나면? 브랜치 정리하고 로컬->서버로 밀어넣던지(push), 아니면 서버에서 로컬 데이터를 당겨가도록 요구(pull request)할 수도 있다.
2. 저장소 사본을 내가 들고 있으므로 브랜치 작업이 자유롭다. 그냥 브랜치 따서 쓰면 된다. 로컬 저장소는 온전히 내 소유이기 때문.
반면 SVN은 서버에 커밋권한이 없으면 브랜치 따는게 불가능하다. 물론 로컬 저장소를 만들면 되지만… 서버와 동기화가 귀찮아진다.
3. 브랜치 사이에 merge 관리가 쉽다.
SVN에서는 내가 어디에서 어디까지 merge했는지가 명확하지 않지만(기록은 된다), git에서는 어느 커밋과 어느 커밋을 merge했는지 명확하게 기록이 남는다.
애당초 git 구조상 merge 이력이 남을 수밖에 없다. 굳이 안 남길 수도 있지만, 다른 사람 – 내지는 두 달 후의 자신에게서 욕을 좀 먹을 것이다.
그 외에도 개념이나 장단점은 다른데에도 자료가 많이 있으니까 넘어가기로 하고.
여하튼 이번 포스팅에서는 SVN으로 구성된 저장소를 git으로 마이그레이션 하는 방법을 소개한다. 윈도우 환경을 기준으로.
굳이 마이그레이션을 강조하는 이유는, 절차를 완료한 후에는 SVN 저장소를 더이상 사용하지 않고 git으로 버전관리를 할 것이기 때문.
git-svn은 SVN을 리모트 서버로, 로컬에서는 git을 사용하는 것을 전제로 만들어져 있지만, 이중 뻘짓은 사양하기로 하자.
이 글을 읽는 사람이 SVN, git의 사용법은 알고있다고 가정하겠다. 윈도우 콘솔 명령어도.
1. 마이그레이션 할 SVN 저장소를 준비한다.
작업도중 IDPW를 지정할 수는 있지만, 가능하다면 Everyone에게 읽기권한은 주는 것이 좋겠다.
2. SVN 저장소의 Log를 보면서, committers list를 준비한다.
링크된 글에서는 SVN 작업사본에서 쉘 스크립트 명령어를 이용해서 committer list를 뽑아낸 모양이지만, 윈도우에서는 못 써먹는다.
나는 그냥 손으로 만들었다. 서버 committer가 10명이 채 안돼기 때문에 굳이 쉘 스크립트 짤 필요도 없었고.
committer list는 일반 텍스트 파일로 저장되며, 다음과 같은 형태를 가진다. 파일이름은 편의상 svn_authors.txt 로 저장하자.
[SVN USERNAME] = [GIT USERNAME] <[email protected]>
committer001 = John Doe <[email protected]>
committer002 = Jane Doe <[email protected]>
3. SourceTree를 설치한다.
http://www.sourcetreeapp.com/
git이 리눅스 커널 개발을 위하여 설계된 물건인지라, 윈도우 환경에서 git쓰기는 꽤나 지저분하다.
애당초 이건 모든 (리눅스를 기반으로 개발된) 오픈소스 어플리케이션의 문제이기도 하지만.
여튼 윈도우에서 git을 편하게 사용하려면 프론트엔드를 써야 하는데, 그나마 현 시점에서 안정적이고 깔끔한게 SourceTree인 것 같다.
그래프 방식으로 branch history를 쭉 보여주는데 이만한 툴이 없는 듯. 굳이 윈도우에서 GUI환경을 포기할 이유가 있을까.
(GitHub 클라이언트는 History View가 안 나오는거같다.)
git 라이브러리는 SourceTree 내장으로 설치하면 된다. 18. Nov. 2013 현재 git 버전은 1.8.3 이고 이 글도 해당 버전을 기준으로 설명한다.
SourceTree 설치가 완료되면 툴박스에서 Terminal 버튼을 눌러 git 터미널을 열고, 작업할 수 있는 임시폴더로 이동한다. MinGW 환경으로 실행될거다 아마.
4. git-svn으로 SVN 저장소를 git 저장소로 변환
명령어는 다음과 같다.
git svn clone http://svn_server/project_name/ –no-metadata -A svn_authors.txt -T trunk -b branches -t tags ./temp
옵션을 풀어 설명하자면 다음과 같다.
git svn clone : SVN 서버에서 복제해서 git 저장소를 생성한다. 밑에서 설명하겠지만 clone = init + fetch 이다.
http://svn_server/project_name/ : 프로젝트 최상위 폴더. 통상적으로 이 밑에 trunk, branches, tags 폴더가 위치한다.
–no-metadata : git-SVN 연동을 위한 메타데이터를 생성하지 않는다. 마이그레이션을 목적으로 하므로 메타데이터를 생략하는 것.
-A svn_authors.txt : 3번에서 만든 committers list
-T trunk -b branches -t tags : 트렁크, 브랜치, 태그 폴더명. 참고로 위에 표시된 것과 같은 표준 사양인 경우 –stdlayout 으로 대체 가능.
./temp : git 저장소가 만들어질 폴더이름
5. 인내심을 가지고 기다린다.
변환 과정은 짤없이 하나의 리비전을 체크아웃 -> git으로 커밋 과정을 반복하는 것이므로 시간이 꽤 걸린다.
리비전이 1천 단위를 넘어간다면 2~3시간 이상 걸릴 수도 있다. r50 정도 되는 저장소를 변환하는데 2분 정도 걸린 것 같다.
폴더구조가 이상하게 꼬여있지 않고 브랜치/태그를 svn copy로 정확하게 만들었다면 그럭저럭 잘 인식해서 브랜치를 만들어 준다.
브랜치 병합 또한, svn-props에 merge 정보가 기록되어 있다면 이를 반영해서 merge commit을 생성해 준다.
정보가 없으면? git에서 merge commit으로 기록은 안 된다. 뭐 트렁크에 병합된건 사실이니까 git에서도 데이터는 남겠지만 추적이 안 될 뿐이다.
변환 도중 알 수 없는 committer가 발견되면 그 시점에서 변환이 중단된다. committer list를 업데이트해야 한다.
겪어보지는 않았지만(말했다시피 서버에 커미터가 10명이 채 안된다), 중단된 시점 이후로는 git svn fetch 명령어로 이어서 작업이 가능할거다.
6. 브랜치/태그 정리
시간이 흘러 작업이 완료되면, SVN 저장소는 git 저장소로 변환된 것이며 더 이상 SVN 저장소는 필요가 없다.
이제 SourceTree로 마이그레이션 된 git 저장소를 열어보자. SVN trunk에 해당하는 git master 브랜치가 체크아웃 되어 있을 것이다.
문제는 브랜치, 그리고 태그. 우선 브랜치부터 해결하자.
git 저장소로 변환되긴 했지만, SVN 브랜치는 기본적으로 로컬에 체크아웃 되어 있지 않다. 따라서 수동으로 한 번씩 체크아웃을 해 줘야 한다.
그 다음은 태그. SVN에서는 브랜치나 태그나 원리만 놓고보면 그냥 svn copy 명령을 수행한 것일 뿐이다. 단지 특수한 폴더에 넣어둔 것일 뿐.
따라서 각각의 SVN 태그도 git 입장에서 보자면 그냥 하나의 (delta가 없는) 브랜치 커밋으로 취급될 뿐이다. 물론 브랜치 이름 대신 태그가 달려있지만.
이를 깔끔하게 정리해준다. 필요한 커밋에 git 태그를 달아주고, 불필요하게 생성된 SVN 태그용 브랜치를 삭제한다.
7. 저장소 재복사
이렇게 정리한 저장소는 아직 SVN 관련 옵션이 남아있어서 지저분할 수 있다.
그냥 써도 좋지만 좀 더 깔끔하게 쓰고 싶은 경우, 아니면 다시 서버에 올리려는 경우는 저장소를 다시 복사해야 한다.
작업 자체는 간단하다. SourceTree 툴박스에서 Clone 을 선택하고, Source Path에 마이그레이션 된 git 저장소 폴더를 지정하면 된다.
Clone 작업이 완료되면 이제 SVN 이력이 깨끗하게 정리된 git 로컬 저장소를 얻게 된다.
뻘짓1. 특정 revision 무시하기
변환을 하다보면, SVN에서 특정 revision을 무시해야 하는 경우가 생긴다. 보통은 잘못된 커밋을 reverse-merge한 경우로, 여러 개의 커밋이 상쇄되어 없던게 되는 경우.
예를 들어
예를 들어 SVN에서 브랜치를 삭제하고 다시 생성한 경우. 아직 git에서 인식을 못한다.
브랜치 생성을 r21에서 해야 하는데 r20에서 잘못 한 경우를 생각해 보자. 그래서 브랜치를 지우고(r23), 올바르게 다시 생성했다(r24).
그냥 r21을 merge하면 되지 않아? 라고 생각할 수도 있겠지만, 커밋로그를 바꿀 수도 없는 노릇이니까 그냥 넘어가자.

이런 경우, 사용자가 의도하는 git history는 다음과 같을 것이다.

근데 정작 git svn으로 변환해 보면, 브랜치 삭제를 제대로 인식 못하고 다음 그림과 같은 결과물이 나오게 된다.

불필요한 r22가 생성된 것도 있고 r24가 merge로 취급되어 히스토리가 복잡하게 꼬이게 된다. 트래킹 되니까 merge에는 문제없잖아-라고 생각하면 뭐 할 말은 없겠다만.
접기
여하튼, 위와 같은 경우도 있고 그 외에 다양한 사유로 특정 revision을 git으로 보내고 싶지 않은 경우가 발생한다.
아쉽지만, 아직 git-svn 명령줄에서 “SVN의 특정 revision을 제외하고 fetch하는” 옵션은 없다. 반대로 말하자면, “SVN의 특정 revision만 fetch하는” 옵션은 있다.
위의 예시를 들자면, r22, r23을 삭제해야 하는 경우인데 그렇다면 BASE:r21, 그리고 r24:HEAD를 fetch하면 되는 것.
5번 과정에서 명령어에 옵션이 추가된다.
git svn clone (blahblah) -t tags -r BASE:21 ./temp
git svn fetch -r 24:HEAD
명령이 둘로 나뉘고, 옵션이 하나 추가되었다.
git svn fetch: 정확하게는 이 명령어가 svn으로부터 데이터를 가져오도록 하는 명령이다. clone = init + fetch 인 축약 명령.
-r START:END : START에서 END까지의 리비전만 가져오도록 제한하는 옵션. START와 END의 리비전을 포함한다.
즉, 두 번에 걸쳐서 데이터를 가져오게 하는 것.
첫 번째 명령에서 BASE~r21까지의 리비전을 가져오고, fetch로 r24~HEAD 리비전을 가져옴으로써 불필요한 r22, r23을 생략하게 된다.
필요한 경우 더 잘게 쪼개서 여러 개의 불필요한 리비전을 생략할 수도 있다. 응용은 직접. 물론 신중하게 잘 사용해야 한다.
잘못 사용하는 경우 커밋 순서나 브랜치가 꼬이게 되며, 최악의 경우 git 저장소를 삭제하고 처음부터 다시 작업해야 할 수도 있다.
뻘짓 2. SVN 저장소 구조가 이상한 경우
Reference : http://www.jeremyjohnstone.com/blog/2010-01-14-using-git-svn-with-non-standard-subversion-repository-layouts.html
보통 SVN 저장소는 안정화 배포버전인 trunk, 문제 해결이나 개발용 사본인 branches, 과거의 특정 버전을 손쉽게 찾아보기 위한 tags 로 구성된다.
형상관리가 10년이 넘도록 사용되면서 그 효율성을 인정받아서 자연스럽게 굳어진 형태이지만, 꼭 저장소가 이렇게 구성되라는 법은 없다.
(SVN 저장소는 저런거를 따로 관리해 주지 않고 그냥 통째로 형상을 기억한다.)
프로젝트 여러 개가 저장소를 공유하는 경우
프로젝트 여러 개가 저장소를 공유하는 경우가 대표적인데, 보통은 저장소 루트에 프로젝트 폴더를 생성하고 그 밑에 stdlayout을 생성하게 된다.
그러니까 아래같은 경우. 이 때는 그냥 full 주소를 쓰면 된다.
–prefix=PROJECTxx/ 옵션을 써도 되지만, 이 경우 마지막 백슬래시를 꼭 붙여줘야 한다.
git으로 마이그레이션 후, commonlib를 서브프로젝트로 등록하는 것은 이 글의 범위를 벗어나므로 생략하도록 한다.
/PROJECTxx/trunk
/PROJECTxx/branches
/PROJECTxx/tags
/PROJECTyy/trunk
/PROJECTyy/branches
/PROJECTyy/tags
/commonlib/trunk
/commonlib/branches
/commonlib/tags
접기
문제는 저장소가 저런 stdlyaout을 벗어나는 경우.
잘못된 레이아웃의 예시
링크 건 참조사이트는 이런 구조 때문에 엿을 먹었다고 한다.
trunk/PROJECTxx
trunk/PROJECTyy
trunk/commonlib
branches/PROJECTxx
branches/PROJECTyy
branches/commonlib
tags/PROJECTxx
tags/PROJECTyy
tags/commonlib
그 외에 내가 겪은 케이스는, 막 SVN 도입당시에 생성된 repo라서 아예 stdlayout 구조 없이 썼던 프로젝트가 있다.
따로 trunk/branches/tags를 구분하지 않고 마일스톤 달성시점마다 새롭게 브랜치를 따고, 기존 브랜치는 개발 중단하는 식으로 사용한 것.
/PROJECT
/PROJECT_rev2
/PROJECT_rev3
접기
여하튼 이런 경우는 git에서 브랜치를 파악하지 못하므로, 사용자가 직접 브랜치를 명명해 줘야 한다. 우선 git-svn 저장소부터 만들자.
git svn init http://svn_server/project_name/ –no-metadata -T PROJECT ./temp
이렇게 하면 ./temp 폴더에 비어 있는 git 저장소가 만들어진다. 이제 ./temp/.git/config 파일을 텍스트 편집기로 연다.
(생략)
[svn-remote “svn”]
noMetadata = 1
url = http://svn_server/project_name
fetch = PROJECT:refs/remotes/trunk
fetch 뒤쪽에 다른 브랜치 경로들을 추가해준다.
(생략)
[svn-remote “svn”]
noMetadata = 1
url = http://svn_server/project_name
fetch = PROJECT:refs/remotes/trunk
fetch = PROJECT_REV2:refs/remotes/rev2
fetch = PROJECT_REV3:refs/remotes/rev3
이렇게 하면 각각의 SVN branches가 git branches로 온전히 인식된다. 이제 fetch하면 된다.
git svn fetch -A svn_authors.txt
뻘짓 3. 비어 있는 commit 제거하기
SVN에서 브랜치나 태그를 따는 커밋은 파일 구성에는 변화가 없는 커밋이다. (다시 말하지만, SVN에서 브랜치, 태그는 단순한 cheap copy일 뿐이다.)
git으로 변환된 커밋로그를 보면, (아마도 create branch for issue #nnnn 따위의) 커밋 메시지는 남아있지만 정작 파일 변경점은 전혀 없는 커밋이 존재하게 된다.
물론 이것도 지울 수 있다. git이 재미있는 점이 과거 커밋도 강제로 수정 가능하다는 점.
각각의 커밋이 독자적으로 완벽한 하나의 형상으로 존재하며, 커밋간의 관계를 포인터로만 구성하기 때문에 이런 뻘짓이 가능하다.
저장소에서 터미널을 열고 다음 명령을 수행하면 파일 변경점이 없는 커밋을 찾아서 몽땅 삭제해 준다.
단, 부모 커밋이 딱 하나인 경우에만 삭제 가능하다. 부모가 둘 이상인 merge 커밋이나 0개인 Initial 커밋은 삭제 안 된다.
git filter-branch –prune-empty — –all
git filter-branch: 브랜치에 필터를 적용한다.
–prune-empty : 빈 커밋을 잘라낸다. (prune 뜻이 가지치기 한다는 뜻임)
— –all : 모든 커밋에 적용하도록 한다. 이거 안 쓰면 다른 브랜치에는 변경사항이 적용 안 되므로 커밋 그래프가 아주 형이상학적으로 바뀌게 된다.
사실 이런 “과거 커밋을 건드는” 명령은 잘못 쓰면 저장소 망가뜨리기 일쑤다. 거의 모든 커밋의 SHA-1 해시가 바뀌므로 히스토리가 꼬이거나 할 가능성이 높다.
특히 태그같은 경우는 거의 전부 새로 작업해 줘야 할거다. 어짜피 SVN 태그를 옮겨오려면 git에서 새로 만들어야 하긴 하지만.
아, SHA-1 해시가 모두 바뀐다고 하였다. 따라서 다른사람하고 공유를 시작한 저장소에서는 절대 쓰지 마라.