Git
Chapters ▾ 2nd Edition

9.2 Git과 여타 버전 관리 시스템 - Git으로 옮기기

Git으로 옮기기

다른 VCS를 사용하는 프로젝트에서 Git을 사용하고 싶다면 우선 프로젝트를 Git으로 이전(Migrate)해야 한다. 이번 절에서는 Git에 들어 있는 Importer를 살펴보고 직접 Importer를 만드는 방법도 알아본다. 우선 많이 사용하는 SCM 시스템으로부터 프로젝트를 이전하는 방법을 살펴본다. 아마도 저장소를 옮기려고 하는 대부분의 사람들은 이 방법을 참고하면 된다. 아주 괜찮은 Importer가 이미 Git에 들어 있다.

Subversion

git svn 을 설명하는 절을 읽었으면 쉽게 git svn clone 명령으로 저장소를 가져올 수 있다. 가져오고 나서 Subversion 서버는 중지하고 Git 서버를 만들고 사용하면 된다. 만약 히스토리 정보가 필요하면 (느린) Subversion 서버 없이 로컬에서 조회할 수 있다.

하지만 이 가져오기 기능에 문제가 좀 있다. 가져오는데 시간이 많이 드니까 일단 시작하는 것이 좋다. 첫 번째 문제는 Author 정보이다. Subversion에서는 커밋하려면 해당 시스템 계정이 있어야 한다. blame 이나 git svn log 같은 명령에서 schacon 이라는 이름을 봤을 것이다. 이 정보를 Git 형식의 정보로 변경하려면 Subversion 사용자와 Git Author를 연결시켜줘야 한다. Subversion 사용자이름과 Git Author 간에 매핑할 수 있게 해줘야 한다. users.txt 라는 파일을 아래와 같이 만든다.

schacon = Scott Chacon <schacon@geemail.com>
selse = Someo Nelse <selse@geemail.com>

SVN에 기록된 Author 이름을 아래 명령으로 조회한다.

$ svn log --xml --quiet | grep author | sort -u | \
  perl -pe 's/.*>(.*?)<.*/$1 = /'

우선 XML 형식으로 SVN 로그를 출력하고, 거기서 Author 정보만 찾고, 중복된 것을 제거하고, XML 태그는 버린다. 물론 grep, sort, perl 명령이 동작하는 시스템에서만 이 명령을 사용할 수 있다. 이 결과에 Git Author 정보를 더해서 users.txt 파일을 만든다.

이 파일을 git svn 명령에 전달하면 보다 정확한 Author 정보를 Git 저장소에 남길 수 있다. 그리고 git svn 명령의 clone 또는 init 명령에 --no-metadata 옵션을 주면 Subversion의 메타데이터를 저장하지 않는다 (기존 메타데이터를 유지하려면 이 옵션을 사용하지 않아도 괜찮다). 해당 명령은 아래와 같다.

$ git svn clone http://my-project.googlecode.com/svn/ \
      --authors-file=users.txt --no-metadata --prefix "" -s my_project
$ cd my_project

my_project 디렉토리에 진짜 Git 저장소가 생성된다. 결과는 아래가 아니라,

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

아래와 같다.

commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2
Author: Scott Chacon <schacon@geemail.com>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

Author 정보가 훨씬 Git답고 git-svn-id 항목도 기록되지 않았다.

이제 뒷 정리를 할 차례다. git svn 이 만들어 준 이상한 브랜치나 태그를 제거한다. 우선 이상한 리모트 태그를 모두 진짜 Git 태그로 옮긴다. 그리고 리모트 브랜치도 로컬 브랜치로 옮긴다.

아래와 같이 태그를 진정한 Git 태그로 만든다.

$ for t in $(git for-each-ref --format='%(refname:short)' refs/remotes/tags); do git tag ${t/tags\//} $t && git branch -D -r $t; done

refs/remotes/tags/ 로 시작하는 리모트 브랜치를 가져다 Lightweight 태그로 만들었다.

refs/remotes 밑에 있는 Refs는 전부 로컬 브랜치로 만든다.

$ for b in $(git for-each-ref --format='%(refname:short)' refs/remotes); do git branch $b refs/remotes/$b && git branch -D -r $b; done

대개 Subversion에서는 브랜치 하나만 보일 진데 @xxx (xxx는 숫자) 문자로 끝나는 몇가지 브랜치가 더 보일 것이다. 이런 이름의 브랜치가 존재하는 것은 "peg-revisions" 이라 부르는 Subversion의 기능 때문이며 Git에서는 마땅이 대응되는 기능이 없다. Subversion에서 peg-revision을 다루기 위해 브랜치 이름 뒤에 버전 숫자를 붙인 것 처럼 git snv 역시 그렇게 하는 것이다. peg-revision 기능을 사용하지 않는다면 그냥 브랜치를 삭제하면 된다.

$ git branch -d trunk

이제 모든 태그와 브랜치는 진짜 Git 태그와 브랜치가 됐다.

마지막으로 마무리를 위한 과정이 하나 남았다. 안타깝게도 git svn 명령이 만드는 Subversion의 기본 브랜치인 trunk 브랜치가 있다. trunk 브랜치는 Git의 master 역할을 한다고 보면 된다. Git에는 master 브랜치가 있기 때문에 여분의 trunk 브랜치는 삭제하자.

$ git branch -d trunk

Git 서버를 새로 추가를 하고 지금까지 작업한 것을 Push 하는 일이 남았다. 아래처럼 리모트 서버를 추가한다.

$ git remote add origin git@my-git-server:myrepository.git

분명 모든 브랜치와 태그를 Push 하고 싶을 것이다.

$ git push origin --tags

모든 브랜치와 태그를 Git 서버로 깔끔하게 잘 옮겼다.

Mercurial

Mercurial과 Git의 버전은 개념이 아주 비슷하다. 그리고 사실은 Git이 좀 더 유연해서 Mercurial 프로젝트를 Git 프로젝트로 변환하는 작업은 아주 쉽다. "hg-fast-export" 라는 툴을 사용하며 아래와 같이 내려 받는다.

$ git clone https://github.com/frej/fast-export.git

우선 처음 할 일은 변환할 Mercurial 저장소 전체를 Clone 하는 일이다.

$ hg clone <remote repo URL> /tmp/hg-repo

변환에 사용할 저자 매핑 파일을 하나 작성한다. Mercurial의 Changeset에 적는 저자 정보의 형식은 Git에 비해 자유롭기 때문에 한 번 정리하는 것이 좋다. 저자 매핑 파일은 아래와 같은 한 라인으로 된 bash 명령을 사용한다.

$ cd /tmp/hg-repo
$ hg log | grep user: | sort | uniq | sed 's/user: *//' > ../authors

프로젝트 크기에 따라 다르겠지만 위 명령을 실행하면 아래와 같은 매핑 파일이 생성된다.

bob
bob@localhost
bob <bob@company.com>
bob jones <bob <AT> company <DOT> com>
Bob Jones <bob@company.com>
Joe Smith <joe@company.com>

예제를 보면 Bob 이라는 한 사람은 적어도 네 가지의 다른 저자 정보를 Changeset에 기록했다. 어떤 정보는 Git에서 쓸 수 있지만 어떤 정보는 Git에서 쓰기에 적절치 않다. Hg-fast-export 에서는 "<input>"="<output>" 규칙을 사용하여 <input>`을 `<output>`에 매핑할 수 있다. `<input>`과 `<output> 문자열에는 Python의 string_escape 인코딩을 사용하는 이스케이프 문자열을 사용할 수 있다. `<input>`에서 찾을 수 없는 문자열을 만나는 경우 Git은 그 저자 내용을 변경하지 않고 그대로 사용한다. 물론 저자 정보가 모든 Changeset에 제대로 입력돼있다면 이런 변환 과정을 거치지 않아도 된다. 예제에서는 아래와 같이 저자 정보를 수정한다.

"bob"="Bob Jones <bob@company.com>"
"bob@localhost"="Bob Jones <bob@company.com>"
"bob <bob@company.com>"="Bob Jones <bob@company.com>"
"bob jones <bob <AT> company <DOT> com>"="Bob Jones <bob@company.com>"

이와 같은 매핑 규칙을 Mercurial에서 사용하고 있지만 Git에서 사용하기 불가능한 브랜치나 태그이름에 대해서도 같은 방식으로 적용할 수 있다.

다음은 Git 저장소를 새로 만들고 변환 스크립트를 실행한다.

$ git init /tmp/converted
$ cd /tmp/converted
$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors

-r 옵션으로 변환할 Mercurial 저장소의 위치를 지정하고 -A 옵션으로 저자 매핑 파일의 위치를 지정한다. (브랜치는 -B, 태그는 -T) hg-fast-export.sh 스크립트는 Mercurial Changeset을 분석하여 Git의 "fast-import"에(곧 자세히 설명한다) 쓰는 스크립트를 생성한다. 명령을 실행하면 아래와 같이 보여준다.

$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors
Loaded 4 authors
master: Exporting full revision 1/22208 with 13/0/0 added/changed/removed files
master: Exporting simple delta revision 2/22208 with 1/1/0 added/changed/removed files
master: Exporting simple delta revision 3/22208 with 0/1/0 added/changed/removed files
[…]
master: Exporting simple delta revision 22206/22208 with 0/4/0 added/changed/removed files
master: Exporting simple delta revision 22207/22208 with 0/2/0 added/changed/removed files
master: Exporting thorough delta revision 22208/22208 with 3/213/0 added/changed/removed files
Exporting tag [0.4c] at [hg r9] [git :10]
Exporting tag [0.4d] at [hg r16] [git :17]
[…]
Exporting tag [3.1-rc] at [hg r21926] [git :21927]
Exporting tag [3.1] at [hg r21973] [git :21974]
Issued 22315 commands
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:     120000
Total objects:       115032 (    208171 duplicates                  )
      blobs  :        40504 (    205320 duplicates      26117 deltas of      39602 attempts)
      trees  :        52320 (      2851 duplicates      47467 deltas of      47599 attempts)
      commits:        22208 (         0 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:         109 (         2 loads     )
      marks:        1048576 (     22208 unique    )
      atoms:           1952
Memory total:          7860 KiB
       pools:          2235 KiB
     objects:          5625 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =      90430
pack_report: pack_mmap_calls          =      46771
pack_report: pack_open_windows        =          1 /          1
pack_report: pack_mapped              =  340852700 /  340852700
---------------------------------------------------------------------

$ git shortlog -sn
   369  Bob Jones
   365  Joe Smith

상당히 많은 Changeset을 Git 커밋으로 변환했다. Mercurial 저장소의 모든 태그는 Git Tag로 변환되고 브랜치, 북마크은 Git 브랜치로 변환된다. 이제 서버 저장소를 만들고 Push 하면 된다.

$ git remote add origin git@my-git-server:myrepository.git
$ git push origin --all

Bazaar

Bazaar 또한 Git과 비슷한 DVCS 도구이기 때문에 Bazaar 저장소를 Git 저장소로 변환하는 것은 쉬운편이다. 우선 이 작업을 하려면 bzr-fastimport 플러그인이 필요하다.

bzr-fastimport 플러그인 다운로드

bzr-fastimport 플러그인을 설치하는 방식은 Unix 비슷한 환경과 Windows 환경이 매우 다르다. 우선 Unix 비슷한 환경에서는 운영체제에서 제공하는 패키지 관리자를 통해 bzr-fastimport 패키지를 설치하는 방법이 가장 쉽다.

예를 들어 Debian 이나 파생 Linux라면 다음과 같이 `bzr-fastimport`를 설치할 수 있다.

$ sudo apt-get install bzr-fastimport

RHEL이나 파생 Linux라면 다음과 같이 `bzr-fastimport`를 설치할 수 있다.

$ sudo yum install bzr-fastimport

Fedora release 22 이상이라면 dnf를 사용하여 `bzr-fastimport`를 설치할 수 있다.

$ sudo dnf install bzr-fastimport

패키지 관리자 도구가 없는 경우라면 다음과 같이 Python을 이용하여 플러그인을 설치할 수 있다.

$ mkdir --parents ~/.bazaar/plugins     # creates the necessary folders for the plugins
$ cd ~/.bazaar/plugins
$ bzr branch lp:bzr-fastimport fastimport   # imports the fastimport plugin
$ cd fastimport
$ sudo python setup.py install --record=files.txt   # installs the plugin

bzr-fastimport 플러그인을 사용하려면 Python의 fastimport 모듈 또한 필요하다. 다음과 같은 명령으로 fastimport Python 모듈이 설치되어 있는지, 설치를 어떻게 하는지 살펴볼 수 있다.

$ python -c "import fastimport"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ImportError: No module named fastimport
$ pip install fastimport

위 방식이 잘 동작하지 않는 경우 https://pypi.python.org/pypi/fastimport/ 에서 바로 내려 받거나 도움을 얻을 수 있다.

Windows 에서 `bzr-fastimport`는 Git 설치시 Standalone 버전이나 기본 설치(모든 체크박스가 설정되)의 경우 함께 설치된다. 따라서 따로 더 설치 작업이 필요하지 않다.

이 시점에서 Bazaar 저장소를 Git 저장소로 변환할 때 브랜치를 하나만 사용하는지 혹은 브랜치를 여러개 사용하는지에 따라 변환 작업이 달라진다.

한 브랜치만 사용하는 프로젝트

cd 명령으로 Bazaar 저장소가 위치한 디렉토리로 이동하고, Git 저장소를 init 한다.

$ cd /path/to/the/bzr/repository
$ git init

다음과 같이 간단한 명령으로 Bazaar 저장소를 Git 저장소로 변환할 수 있다.

$ bzr fast-export --plain . | git fast-import

프로젝트 크기에 따라 수 초 혹은 수 분 내에 Bazaar 저장소의 내용이 Git 저장소에 담길 것이다.

메인 브랜치 하나와 작업 브랜치 하나를 사용하는 프로젝트

여러 브랜치를 사용하는 Bazaar 저장소도 Git 저장소로 변환할 수 있다. 예를 들어 두 개의 브랜치를 사용한다고 가정해보자. 한 브랜치는 메인 브랜치 (myProject.trunk), 다른 브랜치는 작업 브랜치 (myProject.work) 인 경우가 있을 수 있다.

$ ls
myProject.trunk myProject.work

이 위치에서 새로 Git 저장소를 init 하고 cd 명령으로 이동한다.

$ git init git-repo
$ cd git-repo

trunk 브랜치의 내용을 master 브랜치로 가져온다.

$ bzr fast-export --export-marks=../marks.bzr ../myProject.trunk | \
git fast-import --export-marks=../marks.git

work 브랜치의 내용을 work 브랜치로 가져온다.

$ bzr fast-export --marks=../marks.bzr --git-branch=work ../myProject.work | \
git fast-import --import-marks=../marks.git --export-marks=../marks.git

git branch 명령으로 master 브랜치와 work 브랜치를 확인할 수 있다. 히스토리를 확인하여 완전히 저장소와 브랜치의 내용이 변환된 것을 확인한 후 위 과정에서 생성된 marks.bzr, marks.git 파일을 삭제한다.

Staging Area를 동기화

위 과정을 실행하고 나면 하나 혹은 여러 브랜치의 히스토리가 Git 저장소 히스토리로 변환되었지만 Staging Area와 워킹 디렉토리가 `HEAD`와 동기화가 이루어지지 않은 상태이다. 다음 명령으로 쉽게 `HEAD`와 동기화를 시킬 수 있다.

$ git reset --hard HEAD

.bzrignore로 무시하는 파일 동일하게 무시

파일 무시하기 내용을 살펴볼 차례이다. 우선 먼저 해야 할 일은 .bzrignore 파일을 .gitignore 파일로 이름을 변경하는 것이다. .bzrignore 파일이 "!!", "RE:" 문자열로 시작하는 패턴을 포함한다면 이를 적절한 Git 패턴으로 변경하여 .gitignore 파일에 작성해야 한다.

위 작업을 마치고 난 후 이제 이 변경사항에 대한 커밋을 작성할 차례이다.

$ git mv .bzrignore .gitignore
$ # modify .gitignore if needed
$ git commit -am 'Migration from Bazaar to Git'

서버로 저장소 전송

고생하셨수다. 이제 서버 혹은 리모트 저장소로 Push 할 수 있다.

$ git remote add origin git@my-git-server:mygitrepository.git
$ git push origin --all
$ git push origin --tags

이렇게 전송한 리모트 저장소를 Git 저장소로 사용할 수 있다.

Perforce

이번엔 Perforce 프로젝트를 Git으로 변환해 보자. Perforce와 Git으로 변환하는 방법도 git-p4와 Perforce Git Fusion을 이용하는 방법 두 가지다.

Perforce Git Fusion

Git Fusion을 사용한다면 큰 어려울게 없다. 그저 프로젝트 정보, 사용자 매핑, 브랜치를 설정파일에 설정하고(Git Fusion에서 다룸) Perforce 저장소를 Clone 하기만 하면 된다. Git Fusion은 마치 Git 저장소를 Clone 한 것 처럼 느끼게 해준다. Clone 했으면 Git 서버에 Push 한다. 심지어 다시 Perforce 서버로 Push 해도 된다.

Git-p4

Git-p4를 Import 도구로 사용할 수 있다. Perforce Public Depot에 있는 Jam 프로젝트를 Git으로 옮겨보자. 우선 Perforce Depot의 주소를 P4PORT 환경변수에 설정한다.

$ export P4PORT=public.perforce.com:1666
Note

지금 하는 예제를 실제로 해보려면 접근 가능한 Perforce Depot이 필요하다. 여기서는 public.perforce.com 사이트의 공개된 Depot을 이용하지만 접근 가능한 다른 Depot으로 해도 괜찮다.

git p4 clone 명령으로 Perforce 서버에서 Jam 프로젝트를 가져온다. 이 명령에 Depot, 프로젝트 경로, 프로젝트를 가져올 경로를 주면 된다.

$ git-p4 clone //guest/perforce_software/jam@all p4import
Importing from //guest/perforce_software/jam@all into p4import
Initialized empty Git repository in /private/tmp/p4import/.git/
Import destination: refs/remotes/p4/master
Importing revision 9957 (100%)

예제로 사용하는 이 프로젝트는 브랜치가 하나뿐이다. 만약 Clone 할 프로젝트에 브랜치가 여러개 있거나 브랜치가 디렉토리로 구성돼 있다면 --detect-branches 옵션을 사용하여 브랜치 정보를 Git 저장소로 그대로 들고올 수 있다. 브랜치 에서 자세한 내용을 살펴볼 수 있다.

여기까지만 해도 반 이상 한 것이다. p4import 디렉토리로 이동해서 git log 명령을 실행하면 프로젝트 정보를 볼 수 있다.

$ git log -2
commit e5da1c909e5db3036475419f6379f2c73710c4e6
Author: giles <giles@giles@perforce.com>
Date:   Wed Feb 8 03:13:27 2012 -0800

    Correction to line 355; change </UL> to </OL>.

    [git-p4: depot-paths = "//public/jam/src/": change = 8068]

commit aa21359a0a135dda85c50a7f7cf249e4f7b8fd98
Author: kwirth <kwirth@perforce.com>
Date:   Tue Jul 7 01:35:51 2009 -0800

    Fix spelling error on Jam doc page (cummulative -> cumulative).

    [git-p4: depot-paths = "//public/jam/src/": change = 7304]

로그를 살펴보면 커밋마다 git-p4 라는 ID 항목이 들어가 있다. 나중에 Perforce Change Number가 필요해질 수도 있으니 커밋에 그대로 유지하는 편이 좋다. 하지만 ID를 지우고자 한다면 공유하기 전인 이 단계에서 지운다.

git filter-branch 명령으로 한방에 삭제한다.

$ git filter-branch --msg-filter 'sed -e "/^\[git-p4:/d"'
Rewrite e5da1c909e5db3036475419f6379f2c73710c4e6 (125/125)
Ref 'refs/heads/master' was rewritten

git log 명령을 실행하면 모든 SHA-1 체크섬이 변경됐고 커밋 메시지에서 git-p4 항목이 삭제된 것을 확인할 수 있다.

$ git log -2
commit b17341801ed838d97f7800a54a6f9b95750839b7
Author: giles <giles@giles@perforce.com>
Date:   Wed Feb 8 03:13:27 2012 -0800

    Correction to line 355; change </UL> to </OL>.

commit 3e68c2e26cd89cb983eb52c024ecdfba1d6b3fff
Author: kwirth <kwirth@perforce.com>
Date:   Tue Jul 7 01:35:51 2009 -0800

    Fix spelling error on Jam doc page (cummulative -> cumulative).

이제 새 Git 서버에 Push 하면 된다.

TFS

팀이 TFVC를 Git으로 옮기기로 했다면 가능한 많은 데이터를 옮기고자 할 것이다. 앞에서 git-tfs와 git-tf를 둘 다 설명했지만 여기서는 git-tfs만 사용한다. git-tfs는 브랜치를 지원하지만, git-tf로는 어렵다.

Note

여기서는 단방향 변환만 다룬다. 변환한 Git 저장소는 원 TFVC 저장소와 아무런 연결고리가 없다.

먼저 사용자이름을 대응시킨다. TFVC Changeset의 Author 필드는 형식이 자유롭지만 Git에는 사람이 읽을 수 있는 이름과 E-mail 주소로 정해져 있다. 이 정보는 커맨드라인 클라이언트인 tf 로 가져온다.

PS> tf history $/myproject -recursive > AUTHORS_TMP

이 명령어는 프로젝트의 모든 Chagneset을 가져와서 AUTHORS_TMP 파일에 저장한다. AUTHORS_TMP 파일에서 두 번째 열의 User 정보를 추출해서 사용한다. 추출할 때 아래 이어지는 cut 명령에서 사용할 `11-20`과 같은 파라미터를 구하기 위해 몇 번 실험해서 해당 필드를 자를 수 있는 적당한 숫자를 알아내야 한다.

PS> cat AUTHORS_TMP | cut -b 11-20 | tail -n+3 | sort | uniq > AUTHORS

cut 명령어는 각 라인에서 11-20의 문자열만 취한다. tail 명령어로는 필드 헤더와 밑줄인 윗 두 라인을 건너뛴다. 그 결과를 sort, uniq 명령에 파이프로 보내서 중복을 지운다. 그리고는 AUTHORS 파일에 저장한다. 그 다음은 수동으로 한다. git-tfs가 필요로 하는 파일의 포맷은 아래와 같다.

DOMAIN\username = User Name <email@address.com>

`=`의 왼쪽은 TFVC의 “User” 필드고 오른쪽은 Git 커밋에 사용할 개발자 정보다.

이 파일을 만들었으면 해당 TFVC 프로젝트를 Clone 한다.

PS> git tfs clone --with-branches --authors=AUTHORS https://username.visualstudio.com/DefaultCollection $/project/Trunk project_git

그리고 커밋 메시지 밑에 붙은 git-tfs-id 부분을 지운다. 아래 명령어를 사용하면 된다.

PS> git filter-branch -f --msg-filter 'sed "s/^git-tfs-id:.*$//g"' '--' --all

Git-bash 환경에서 sed 명령어로 “git-tfs-id:” 로 시작하는 줄을 빈 줄로 바꾼다. 그럼 Git은 그 라인을 무시한다.

다 됐다. 리모트를 새로 추가하고 모든 브랜치를 Push 한다. 그리고 나서 팀원들과 Git으로 작업을 시작하면 된다.

직접 Importer 만들기

사용하는 VCS가 앞서 살펴본 시스템이 아니면 인터넷에서 적당한 Importer를 찾아봐야 한다. CVS, Clear Case, Visual Source Safe 같은 시스템용 Importer가 좋은게 많다. 심지어 단순히 디렉토리 아카이브용 Importer에도 좋은게 있다. 사람들이 잘 안쓰는 시스템을 사용하고 있는데 적당한 Importer를 못 찾았거나 부족해서 좀 더 고쳐야 한다면 git fast-import 를 사용한다. 이 명령은 표준입력으로 데이터를 입력받는다. Git의 내부 에서 배우는 저수준 명령어와 내부 객체를 직접 다루는 것보다 훨씬 쉽다. 먼저 사용하는 VCS에서 필요한 정보를 수집해서 표준출력으로 출력하는 스크립트를 만든다. 그리고 그 결과를 git fast-import 의 표준입력으로 보낸다.

간단한 Importer를 작성해보자. current 디렉토리에서 작업하고 back_YYYY_MM_DD 이라는 디렉토리에 백업하면서 진행했던 프로젝트를 살펴 보자. Importer를 만들 때 디렉토리 상태는 아래와 같다.

$ ls /opt/import_from
back_2014_01_02
back_2014_01_04
back_2014_01_14
back_2014_02_03
current

Importer를 만들기 전에 우선 Git이 어떻게 데이터를 저장하는지 알아야 한다. 이미 알고 있듯이 Git은 기본적으로 스냅샷을 가리키는 커밋 개체가 연결된 리스트이다. 스냅샷이 뭐고, 그걸 가리키는 커밋은 또 뭐고, 그 커밋의 순서가 어떻게 되는지 fast-import 에 알려 줘야 한다. 이 것이 해야할 일의 전부다. 그러면 디렉토리마다 스냅샷을 만들고, 그 스냅샷을 가리키는 커밋 개체를 만들고, 이전 커밋과 연결 시킨다.

정책 구현하기 절에서 했던 것 처럼 Ruby로 스크립트를 작성한다. 책에서 계속 스크립트를 작성할 때 Ruby로 해왔고, 읽기도 쉽기에 Ruby를 쓴다. 하지만 자신에게 익숙한 것을 사용해서 표준출력으로 적절한 정보만 출력할 수 있으면 된다. 그리고 Windows에서는 라인 바꿈 문자에 CR(Carriage Return) 문자가 들어가지 않도록 주의해야 한다. git fast-import 명령은 Windows에서도 라인 바꿈 문자로 CRLF 문자가 아니라 LF(Line Feed) 문자만 허용한다.

우선 해당 디렉토리로 이동해서 어떤 디렉토리가 있는지 살펴본다. 하위 디렉토리마다 스냅샷 하나가 되고 커밋 하나가 된다. 하위 디렉토리를 이동하면서 필요한 정보를 출력한다. 기본적인 로직은 아래와 같다.

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

각 디렉토리에서 print_export 를 호출하는데 이 함수는 아규먼트로 디렉토리와 이전 스냅샷 Mark를 전달받고 현 스냅샷 Mark를 반환한다. 그래서 적절히 연결 시킬 수 있다. fast-import 에서 “Mark” 는 커밋의 식별자를 말한다. 커밋을 하나 만들면 “Mark” 도 같이 만들어 이 “Mark” 로 다른 커밋과 연결 시킨다. 그래서 print_export 에서 우선 해야 하는 일은 각 디렉토리 이름으로 “Mark” 를 생성하는 것이다.

mark = convert_dir_to_mark(dir)

Mark는 정수 값을 사용해야 하기 때문에 디렉토리를 배열에 담고 그 Index를 Mark로 사용한다. 아래와 같이 작성한다.

$marks = []
def convert_dir_to_mark(dir)
  if !$marks.include?(dir)
    $marks << dir
  end
  ($marks.index(dir) + 1).to_s
end

각 커밋을 가리키는 정수 Mark를 만들었고 다음에는 커밋 메타데이터에 넣을 날짜 정보가 필요하다. 이 날짜는 디렉토리 이름에 있는 것을 가져다 사용한다. print_export 의 두 번째 라인은 아래와 같다.

date = convert_dir_to_date(dir)

convert_dir_to_date 는 아래와 같이 정의한다.

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인데 이 것은 전역 변수 하나로 설정해서 사용한다.

$author = 'John Doe <john@example.com>'

이제 Importer에서 출력할 커밋 데이터는 다 준비했다. 이제 출력해보자. 사용할 브랜치, 해당 커밋과 관련된 Mark, 커미터 정보, 커밋 메시지, 이전 커밋을 출력한다. 코드로 만들면 아래와 같다.

# 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) 정보는 편의상 하드코딩으로 처리했다. 각자의 시간대에 맞는 오프셋을 설정해야 한다. 커밋 메시지는 아래와 같은 형식을 따라야 한다.

data (size)\n(contents)

이 형식은 “data” 라는 단어, 읽을 데이터의 크기, 라인 바꿈 문자, 실 데이터로 구성된다. 이 형식을 여러 곳에서 사용해야 하므로 export_data 라는 Helper 메소드로 만들어 놓는게 좋다.

def export_data(string)
  print "data #{string.size}\n#{string}"
end

이제 남은 것은 스냅샷에 파일 내용를 포함시키는 것 뿐이다. 디렉토리로 구분돼 있기 때문에 어렵지 않다. 우선 deleteall 이라는 명령을 출력하고 그 뒤에 모든 파일의 내용을 출력한다. 그러면 Git은 스냅샷을 잘 저장한다.

puts 'deleteall'
Dir.glob("**/*").each do |file|
  next if !File.file?(file)
  inline_data(file)
end

Note: 대부분의 VCS는 리비전을 커밋간의 변화로 생각하기 때문에 fast-import에 추가/삭제/변경된 부분만 입력할 수도 있다. 스냅샷 사이의 차이를 구해서 fast-import에 넘길 수도 있지만 훨씬 복잡하다. 줄 수 있는 데이터는 전부 Git에 줘서 Git이 계산하게 해야 한다. 꼭 이렇게 해야 한다면 어떻게 데이터를 전달해야 하는지 fast-import 의 ManPage를 참고하라.

파일 정보와 내용은 아래와 같은 형식으로 출력한다.

M 644 inline path/to/file
data (size)
(file contents)

644는 파일의 모드를 나타낸다(실행파일이라면 755로 지정해줘야 한다). inline 다음 라인 부터는 파일 내용이라 말하는 것이다. inline_data 메소드는 아래와 같다.

def inline_data(file, code = 'M', mode = '644')
  content = File.read(file)
  puts "#{code} #{mode} inline #{file}"
  export_data(content)
end

파일 내용은 커밋 메시지랑 같은 방법을 사용하기 때문에 앞서 만들어 놓은 export_data 메소드를 다시 이용한다.

마지막으로 다음에 커밋할 현 Mark 값을 반환한다.

return mark
Note

Windows에서 실행할 때는 추가 작업이 하나 더 필요하다. 앞에서 얘기했지만 Windows는 CRLF를 사용하지만 git fast-import 는 LF를 사용한다. 이 문제를 해결 하려면 Ruby가 CRLF 대신 LF를 사용하도록 알려 줘야 한다.

$stdout.binmode

모든게 끝났다. 스크립트 코드는 아래와 같다:

#!/usr/bin/env ruby

$stdout.binmode
$author = "John Doe <john@example.com>"

$marks = []
def convert_dir_to_mark(dir)
    if !$marks.include?(dir)
        $marks << dir
    end
    ($marks.index(dir)+1).to_s
end

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

def export_data(string)
    print "data #{string.size}\n#{string}"
end

def inline_data(file, code='M', mode='644')
    content = File.read(file)
    puts "#{code} #{mode} inline #{file}"
    export_data(content)
end

def print_export(dir, last_mark)
    date = convert_dir_to_date(dir)
    mark = convert_dir_to_mark(dir)

    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

    puts 'deleteall'
    Dir.glob("**/*").each do |file|
        next if !File.file?(file)
        inline_data(file)
    end
    mark
end

# Loop through the directories
last_mark = nil
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

스크립트를 실행하면 아래와 같이 출력된다.

$ ruby import.rb /opt/import_from
commit refs/heads/master
mark :1
committer John Doe <john@example.com> 1388649600 -0700
data 29
imported from back_2014_01_02deleteall
M 644 inline README.md
data 28
# Hello

This is my readme.
commit refs/heads/master
mark :2
committer John Doe <john@example.com> 1388822400 -0700
data 29
imported from back_2014_01_04from :1
deleteall
M 644 inline main.rb
data 34
#!/bin/env ruby

puts "Hey there"
M 644 inline README.md
(...)

디렉토리를 하나 만들고 git init 명령을 실행해서 옮길 Git 프로젝트를 만든다. 그리고 그 프로젝트 디렉토리로 이동해서 이 명령의 표준출력을 git fast-import 명령의 표준입력으로 연결한다(pipe).

$ 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:           13 (         6 duplicates                  )
      blobs  :            5 (         4 duplicates          3 deltas of          5 attempts)
      trees  :            4 (         1 duplicates          0 deltas of          4 attempts)
      commits:            4 (         1 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:           1 (         1 loads     )
      marks:           1024 (         5 unique    )
      atoms:              2
Memory total:          2344 KiB
       pools:          2110 KiB
     objects:           234 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =         10
pack_report: pack_mmap_calls          =          5
pack_report: pack_open_windows        =          2 /          2
pack_report: pack_mapped              =       1457 /       1457
---------------------------------------------------------------------

성공적으로 끝나면 여기서 보여주는 것처럼 어떻게 됐는지 통계를 보여준다. 이 경우엔 브랜치 1개와 커밋 4개 그리고 개체 13개가 임포트됐다. 이제 git log 명령으로 히스토리 조회가 가능하다.

$ git log -2
commit 3caa046d4aac682a55867132ccdfbe0d3fdee498
Author: John Doe <john@example.com>
Date:   Tue Jul 29 19:39:04 2014 -0700

    imported from current

commit 4afc2b945d0d3c8cd00556fbe2e8224569dc9def
Author: John Doe <john@example.com>
Date:   Mon Feb 3 01:00:00 2014 -0700

    imported from back_2014_02_03

깔끔하게 Git 저장소가 완성됐다. 이 시점에서는 아무것도 Checkout 하지 않았기 때문에 워킹 디렉토리에 아직 아무 파일도 없다. master 브랜치로 Reset 해서 파일을 Checkout 한다.

$ ls
$ git reset --hard master
HEAD is now at 3caa046 imported from current
$ ls
README.md main.rb

fast-import 명령으로 많은 일을 할 수 있다. 모드를 설정하고, 바이너리 데이터를 다루고, 브랜치를 여러 개 다루고, Merge 하고, 태그를 달고, 진행상황을 보여 주고, 등등 무수히 많은 일을 할 수 있다. Git 소스의 contrib/fast-import 디렉토리에 복잡한 상황을 다루는 예제가 많다.