Git으로 옮기기
Git으로 옮기기
다른 VCS를 사용하는 프로젝트를 Git으로 옮기고 싶다면 우선 프로젝트를 Git으로 이전(Migrate)해야 한다. 이번 절에서는 Git에 들어 있는 Importer를 살펴보고 직접 Importer를 만드는 방법을 알아본다.
가져오기
많이 사용하는 Subversion과 Perforce 프로젝트를 이전하는 방법을 살펴보자. 이 두 VCS에서 Git으로 이전하고자 하는 사람이 많고 Importer도 이미 Git에 들어 있다.
Subversion
을 설명하는 절을 읽었으면 쉽게 1
git svn
명령으로 저장소를 가져올 수 있다. 가져오고 나서 Subversion 서버는 중지하고 Git 서버를 만들고 사용하면 된다. 만약 히스토리 정보가 필요하면 (느린) Subversion 서버 없이 로컬에서 조회할 수 있다.1
git svn clone
우선 가져오기에 시간이 많이 드니까 일단 가져오기를 시작하자. 이 가져오기 기능에 문제가 좀 있다. 첫 번째 문제는 Author 정보이다. Subversion에서는 커밋하려면 해당 시스템 계정이 있어야 한다.
이나 1
blame
같은 명령에서 1
git svn log
이라는 이름을 봤을 것이다. 이 정보를 Git 형식의 정보로 변경하려면 Subversion 사용자와 Git Author를 연결시켜줘야 한다. Subversion 사용자 이름과 Git Author 간에 연결을 해줘서 이 Author 정보를 Git 스타일의 Author 정보로 변경한다. 1
schacon
라는 파일을 아래와 같이 만든다:1
users.txt
1 2 | schacon = Scott Chacon <schacon@geemail.com> selse = Someo Nelse <selse@geemail.com> |
SVN에 기록된 Author 이름을 아래 명령으로 조회한다:
1 2 | $ svn log ^/ --xml | grep -P "^<author" | sort -u | \ perl -pe 's/<author>(.*?)<\/author>/$1 = /' > users.txt |
우선 XML 형식으로 SVN 로그를 출력하고, 거기서 Author 정보만 찾고, 중복된 것을 제거하고, XML 태그는 버린다. 물론
, 1
grep
, 1
sort
명령이 동작하는 시스템에서만 이 명령을 사용할 수 있다. 이 결과에 Git Author 정보를 더해서 1
perl
를 만든다.1
users.txt
이 파일을
명령에 전달하면 보다 정확한 Author 정보를 Git 저장소에 남길 수 있다. 그리고 1
git svn
의 1
git svn
이나 1
clone
명령에 1
init
옵션을 주면 Subversion의 메타데이터를 저장하지 않는다. 해당 명령은 아래와 같다:1
--no-metadata
1 2 | $ git svn clone http://my-project.googlecode.com/svn/ \ --authors-file=users.txt --no-metadata -s my_project |
디렉토리에 진짜 Git 저장소가 생성된다. 결과는 아래와 같지 않고:1
my_project
1 2 3 4 5 6 7 8 | commit 37efa680e8473b615de980fa935944215428a35a Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029> Date: Sun May 3 00:12:22 2009 +0000 fixed install - go to trunk git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de- be05-5f7a86268029 |
아래와 같다:
1 2 3 4 5 | commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2 Author: Scott Chacon <schacon@geemail.com> Date: Sun May 3 00:12:22 2009 +0000 fixed install - go to trunk |
Author 정보가 훨씬 Git답고
항목도 기록되지 않았다.1
git-svn-id
이제 뒷 정리를 해야 한다.
이 만들어 준 이상한 브랜치나 태그를 제거한다. 우선 이상한 리모트 태그를 모두 진짜 Git 태그로 옮긴다. 그리고 리모트 브랜치도 로컬 브랜치로 옮긴다.1
git svn
아래와 같이 태그를 진정한 Git 태그로 만든다:
1 | $ git for-each-ref refs/remotes/tags | cut -d / -f 4- | grep -v @ | while read tagname; do git tag "$tagname" "tags/$tagname"; git branch -r -d "tags/$tagname"; done |
로 시작하는 리모트 브랜치를 가져다 (Lightweight) 태그로 만들었다.1
tags/
밑에 있는 레퍼런스는 전부 로컬 브랜치로 만든다:1
refs/remotes
1 | $ git for-each-ref refs/remotes | cut -d / -f 3- | grep -v @ | while read branchname; do git branch "$branchname" "refs/remotes/$branchname"; git branch -r -d "$branchname"; done |
이제 모든 태그와 브랜치는 진짜 Git 태그와 브랜치가 됐다. Git 서버를 새로 추가를 하고 지금까지 작업한 것을 Push하는 일이 남았다. 아래처럼 리모트 서버를 추가한다:
1 | $ git remote add origin git@my-git-server:myrepository.git |
분명 모든 브랜치와 태그를 Push하고 싶을 것이다:
1 | $ git push origin --all |
모든 브랜치와 태그를 Git 서버로 깔끔하게 잘 옮겼다.
Perforce
이제 Perforce 차례다. Preforce Importer도 Git에 들어 있다. Git 1.7.11 이전 버전을 사용한다면 소스코드의
에 포함되어 있다.이런 경우라면 Perforce Importer를 사용하기 위해 우선 git.kernel.org에서 Git 소스코드를 가져와야 한다:1
contrib
1 2 | $ git clone git://git.kernel.org/pub/scm/git/git.git $ cd git/contrib/fast-import |
라는 Python 스크립트는 1
git-p4
디렉토리에 있다. 그리고 Python과 1
fast-import
가 설치돼 있어야 이 스크립트가 동작한다. Perforce Public Depot에 있는 Jam 프로젝트를 Git으로 옮겨보자. 우선 Perfoce Depot의 주소를 P4PORT 환경변수에 설정한다:1
p4
1 | $ export P4PORT=public.perforce.com:1666 |
명령으로 Perforce 서버에서 Jam 프로젝트를 가져온다. 이 명령에 Depot, 프로젝트 경로, 프로젝트를 가져올 경로를 주면 된다:1
git-p4 clone
1 2 3 4 5 | $ git-p4 clone //public/jam/src@all /opt/p4import Importing from //public/jam/src@all into /opt/p4import Reinitialized existing Git repository in /opt/p4import/.git/ Import destination: refs/remotes/p4/master Importing revision 4409 (100%) |
디렉토리로 이동해서 1
/opt/p4import
명령을 실행하면 프로젝트 정보를 볼 수 있다:1
git log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | $ git log -2 commit 1fd4ec126171790efd2db83548b85b1bbbc07dc2 Author: Perforce staff <support@perforce.com> Date: Thu Aug 19 10:18:45 2004 -0800 Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into the main part of the document. Built new tar/zip balls. Only 16 months later. [git-p4: depot-paths = "//public/jam/src/": change = 4409] commit ca8870db541a23ed867f38847eda65bf4363371d Author: Richard Geiger <rmg@perforce.com> Date: Tue Apr 22 20:51:34 2003 -0800 Update derived jamgram.c [git-p4: depot-paths = "//public/jam/src/": change = 3108] |
커밋마다
라는 ID 항목이 들어가 있다. 나중에 Perforce Change Number가 필요해질 수도 있으니 커밋에 그대로 유지하는 편이 좋다. 하지만 ID를 지우고자 한다면 지금 하는 것이 가장 좋다. 1
git-p4
명령으로 한방에 삭제한다:1
git filter-branch
1 2 3 4 5 | $ git filter-branch --msg-filter ' sed -e "/^\[git-p4:/d" ' Rewrite 1fd4ec126171790efd2db83548b85b1bbbc07dc2 (123/123) Ref 'refs/heads/master' was rewritten |
명령을 실행하면 모든 SHA-1 체크섬이 변경됐고 커밋 메시지에서 1
git log
항목도 삭제된 것을 확인할 수 있다.1
git-p4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | $ git log -2 commit 10a16d60cffca14d454a15c6164378f4082bc5b0 Author: Perforce staff <support@perforce.com> Date: Thu Aug 19 10:18:45 2004 -0800 Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into the main part of the document. Built new tar/zip balls. Only 16 months later. commit 2b6c6db311dd76c34c66ec1c40a49405e6b527b2 Author: Richard Geiger <rmg@perforce.com> Date: Tue Apr 22 20:51:34 2003 -0800 Update derived jamgram.c |
이제 새 Git 서버에 Push하면 된다.
직접 Importer 만들기
사용하는 VCS가 Subversion이나 Perforce가 아니면 인터넷에서 적당한 Importer를 찾아봐야 한다. CVS, Clear Case, Visual Source Safe 같은 시스템용 Importer가 좋은게 많다. 심지어 단순히 디렉토리 아카이브용 Importer에도 좋은게 있다. 사람들이 잘 안쓰는 시스템을 사용하고 있는데 적당한 Importer를 못 찾았거나 부족해서 좀 더 고쳐야 한다면
를 사용한다. 이 명령은 표준입력으로 데이터를 입력받는데, 9장에서 배우는 저수준 명령어와 내부 객체를 직접 다루는 것보다 훨씬 쉽다. 먼저 사용하는 VCS에서 필요한 정보를 수집해서 표준출력으로 출력하는 스크립트를 만든다. 그리고 그 결과를 1
git fast-import
의 표준입력으로 보낸다(pipe).1
git fast-import
간단한 Importer를 작성해보자.
라는 디렉토리에 백업하면서 프로젝트를 진행하는 예제를 보자. Importer를 만들 때 디렉토리 상태는 아래와 같다:1
back_YYYY_MM_DD
1 2 3 4 5 6 | $ ls /opt/import_from back_2009_01_02 back_2009_01_04 back_2009_01_14 back_2009_02_03 current |
Importer를 만들기 전에 우선 Git이 어떻게 데이터를 저장하는지 알아야 한다. 이미 알고 있듯이 Git은 기본적으로 스냅샷을 가리키는 커밋 개체가 연결된 리스트이다. 스냅샷이 뭐고, 그걸 가리키는 커밋은 또 뭐고, 그 커밋의 순서가 어떻게 되는지
에 알려 줘야 한다. 이 것이 해야할 일의 전부다. 그러면 디렉토리마다 스냅샷을 만들고, 그 스냅샷을 가리키는 커밋 개체를 만들고, 이전 커밋과 연결 시킨다.1
fast-import
7장의 “정책 구현하기” 절에서 했던 것 처럼 Ruby로 스크립트를 작성한다. 필자는 Ruby를 많이 사용하기도 하고 Ruby가 읽기도 쉽다. 하지만 자신에게 익숙한 것을 사용해서 표준출력으로 적절한 정보만 출력할 수 있으면 된다. 그리고 윈도에서는 줄바꿈 문자에 CR(Carriage Return) 문자가 들어가지 않도록 주의해야 한다.
명령은 윈도에서도 줄바꿈 문자로 CRLF 문자가 아니라 LF(Line Feed) 문자만 허용한다.1
git fast-import
우선 해당 디렉토리로 이동해서 어떤 디렉토리가 있는지 살펴본다. 하위 디렉토리마다 스냅샷 하나가 되고 커밋 하나가 된다. 하위 디렉토리를 이동하면서 필요한 정보를 출력한다. 기본적인 로직은 아래와 같다:
1 2 3 4 5 6 7 8 9 10 11 12 13 | last_mark = nil # loop through the directories Dir.chdir(ARGV[0]) do Dir.glob("*").each do |dir| next if File.file?(dir) # move into the target directory Dir.chdir(dir) do last_mark = print_export(dir, last_mark) end end end |
각 디렉토리에서
를 호출하는데 이 함수는 아규먼트로 디렉토리와 이전 스냅샷 Mark를 전달받고 현 스냅샷 Mark를 반환한다. 그래서 적절히 연결 시킬 수 있다. 1
print_export
에서 “Mark”는 커밋의 식별자를 말한다. 커밋을 하나 만들면 Mark도 같이 만들어 이 Mark로 다른 커밋과 연결 시킨다. 그래서 1
fast-import
에서 우선 해야 하는 일은 각 디렉토리 이름으로 Mark를 생성하는 것이다:1
print_export
1 | mark = convert_dir_to_mark(dir) |
Mark는 정수 값을 사용해야 하기 때문에 디렉토리를 배열에 담고 그 인덱스를 Mark로 사용한다. 아래와 같이 작성한다:
1 2 3 4 5 6 7 | $marks = [] def convert_dir_to_mark(dir) if !$marks.include?(dir) $marks << dir end ($marks.index(dir) + 1).to_s end |
각 커밋을 가리키는 정수 Mark를 만들었고 다음은 커밋 메타데이터에 넣을 날짜 정보가 필요하다. 이 날짜는 디렉토리 이름에 있는 것을 가져다 사용한다.
의 두 번째 줄은 아래와 같다:1
print_export
1 | date = convert_dir_to_date(dir) |
는 아래와 같이 정의한다:1
convert_dir_to_date
1 2 3 4 5 6 7 8 9 | def convert_dir_to_date(dir) if dir == 'current' return Time.now().to_i else dir = dir.gsub('back_', '') (year, month, day) = dir.split('_') return Time.local(year, month, day).to_i end end |
시간는 정수 형태로 반환한다. 마지막으로 메타정보에 필요한 것은 Author인데 이 것은 전역 변수 하나로 설정해서 사용한다:
1 | $author = 'Scott Chacon <schacon@example.com>' |
이제 Importer에서 출력할 커밋 데이터는 다 준비했다. 이제 출력해보자. 사용할 브랜치, 해당 커밋과 관련된 Mark, 커미터 정보, 커밋 메시지, 이전 커밋를 출력한다. 코드로 만들면 아래와 같다:
1 2 3 4 5 6 | # print the import information puts 'commit refs/heads/master' puts 'mark :' + mark puts "committer #{$author} #{date} -0700" export_data('imported from ' + dir) puts 'from :' + last_mark if last_mark |
우선 시간대(-0700) 정보는 편의상 하드코딩으로 처리했다. 각자의 시간대에 맞는 오프셋을 설정해야 한다. 커밋 메시지는 아래와 같은 형식을 따라야 한다:
1 | data (size)\n(contents) |
이 형식은 ‘data’라는 단어, 읽을 데이터의 크기, 줄바꿈 문자, 실 데이터로 구성된다. 이 형식을 여러 곳에서 사용해야 하므로
라는 메소드로 만들어 놓는게 좋다:1
export_data
1 2 3 | def export_data(string) print "data #{string.size}\n#{string}" end |
이제 남은 것은 스냅샷에 파일 내용를 포함시키는 것 뿐이다. 디렉토리로 구분돼 있기 때문에 어렵지 않다. 우선
이라는 명령을 출력하고 그 뒤에 모든 파일의 내용을 출력한다. 그런면 Git은 스냅샷을 잘 저장한다:1
deleteall
1 2 3 4 5 | puts 'deleteall' Dir.glob("**/*").each do |file| next if !File.file?(file) inline_data(file) end |
중요: 대부분의 VCS는 리비전을 커밋간의 변화로 생각하기 때문에
에 추가/삭제/변경된 부분만 입력할 수도 있다. 스냅샷 사이의 차이를 구해서 1
fast-import
에 넘길 수도 있지만 훨씬 복잡하다. 줄 수 있는 데이터는 전부 Git에 줘서 Git이 계산하게 해야 한다. 꼭 이렇게 해야 한다면 어떻게 데이터를 전달해야 하는지 1
fast-import
의 ManPage를 참고하라.1
fast-import
파일 정보와 내용은 아래와 같은 형식으로 출력한다:
1 2 3 | M 644 inline path/to/file data (size) (file contents) |
644는 파일의 모드를 나타낸다(실행파일이라면 755로 지정해줘야 한다).
은 다음 줄 부터는 파일 내용이라는 말하는 것이다. 1
inline
메소드는 아래와 같다:1
inline_data
1 2 3 4 5 | def inline_data(file, code = 'M', mode = '644') content = File.read(file) puts "#{code} #{mode} inline #{file}" export_data(content) end |
파일 내용은 커밋 메시지랑 같은 방법을 사용하기 때문에 앞서 만들어 놓은
메소드를 다시 이용한다.1
export_data
마지막으로 다음 커밋에 사용할 현 Mark 값을 반환한다:
1 | return mark |
중요: 윈도에서 실행할 때는 추가 작업이 하나 더 필요하다. 앞에서 얘기했지만 윈도는 CRLF를 사용하지만
는 LF를 사용한다. 이 문제를 해결 하려면 Ruby가 CRLF 대신 LF를 사용하도록 알려 줘야 한다:1
git fast-import
1 | $stdout.binmode |
모든게 끝났다. 스크립트를 실행하면 아래와 같이 출력된다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | $ ruby import.rb /opt/import_from commit refs/heads/master mark :1 committer Scott Chacon <schacon@geemail.com> 1230883200 -0700 data 29 imported from back_2009_01_02deleteall M 644 inline file.rb data 12 version two commit refs/heads/master mark :2 committer Scott Chacon <schacon@geemail.com> 1231056000 -0700 data 29 imported from back_2009_01_04from :1 deleteall M 644 inline file.rb data 14 version three M 644 inline new.rb data 16 new version one (...) |
디렉토리를 하나 만들고
명령을 실행해서 옮길 Git 프로젝트를 만든다. 그리고 그 프로젝트 디렉토리로 이동해서 이 명령의 표준출력을 1
git init
명령의 표준입력으로 연결한다(pipe).1
git fast-import
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | $ git init Initialized empty Git repository in /opt/import_to/.git/ $ ruby import.rb /opt/import_from | git fast-import git-fast-import statistics: --------------------------------------------------------------------- Alloc'd objects: 5000 Total objects: 18 ( 1 duplicates ) blobs : 7 ( 1 duplicates 0 deltas) trees : 6 ( 0 duplicates 1 deltas) commits: 5 ( 0 duplicates 0 deltas) tags : 0 ( 0 duplicates 0 deltas) Total branches: 1 ( 1 loads ) marks: 1024 ( 5 unique ) atoms: 3 Memory total: 2255 KiB pools: 2098 KiB objects: 156 KiB --------------------------------------------------------------------- pack_report: getpagesize() = 4096 pack_report: core.packedGitWindowSize = 33554432 pack_report: core.packedGitLimit = 268435456 pack_report: pack_used_ctr = 9 pack_report: pack_mmap_calls = 5 pack_report: pack_open_windows = 1 / 1 pack_report: pack_mapped = 1356 / 1356 --------------------------------------------------------------------- |
성공적으로 끝나면 여기서 보여주는 것처럼 어떻게 됐는지 통계를 보여준다. 이 경우엔 브랜치 1개와 커밋 5개 그리고 개체 18개가 임포트됐다.
명령으로 히스토리 조회가 가능하다:1
git log
1 2 3 4 5 6 7 8 9 10 11 12 | $ git log -2 commit 10bfe7d22ce15ee25b60a824c8982157ca593d41 Author: Scott Chacon <schacon@example.com> Date: Sun May 3 12:57:39 2009 -0700 imported from current commit 7e519590de754d079dd73b44d695a42c9d2df452 Author: Scott Chacon <schacon@example.com> Date: Tue Feb 3 01:00:00 2009 -0700 imported from back_2009_02_03 |
이 시점에서는 아무것도 Checkout하지 않았기 때문에 워킹 디렉토리에 아직 아무 파일도 없다.
브랜치로 Reset해서 파일을 Checkout한다:1
master
1 2 3 4 5 | $ ls $ git reset --hard master HEAD is now at 10bfe7d imported from current $ ls file.rb lib |
명령으로 많은 일을 할 수 있다. 모드를 설정하고, 바이너리 데이터를 다루고, 브랜치를 여러 개 다루고, Merge하고, 태그를 달고, 진행상황을 보여 주고, 등등 무수히 많은 일을 할 수 있다. Git 소스의 1
fast-import
디렉토리에 복잡한 상황을 다루는 예제가 많다. 그 중 여기서 설명한 1
contrib/fast-import
스크립트가 좋은 예제이다.1
git-p4