Git
Chapters ▾ 2nd Edition

9.1 Git과 여타 버전 관리 시스템 - Git: 범용 Client

세상 일은 뜻대로 되지 않는다. 진행하던 프로젝트를 모두 한 번에 Git 저장소로 옮기기는 어렵다. Git으로 바꾸고 싶은 프로젝트가 특정 VCS 시스템에 매우 의존적으로 개발 됐을 수도 있다. 이 장의 앞부분에서는 기존 VCS 시스템의 클라이언트로 Git을 사용하는 방법을 살펴본다.

언젠가 기존 프로젝트 환경을 Git으로 변경하고 싶게 될 것이다. 이 장의 뒷 부분에서는 프로젝트를 Git으로 변경하는 방법에 대해 다룬다. 미리 만들어진 도구가 없더라도 스크립트를 직접 만들어서 옮기는 방법도 설명한다. 그래서 잘 쓰지 않는 VCS를 사용하고 있더라도 Git으로 옮길 수 있을 것이다.

Git: 범용 Client

Git을 배운 많은 사람들은 만족스러워 한다. 다른 모든 팀원들이 Git 아닌 다른 VCS 시스템을 사용하고 홀로 Git을 사용하더라도 만족스럽다. Git은 이렇게 다른 VCS 시스템과 연결해 주는 여러 “bridge” 를 제공한다. 이어지는 내용을 통해 하나씩 둘러보자.

Git과 Subversion

많은 오픈소스와 수 많은 기업들은 Subversion으로 소스코드를 관리한다. 10여년 이상 Subversion은 가장 인기있는 오픈소스 VCS 도구였고 오픈소스 프로젝트에서 선택하는 거의 표준에 가까운 시스템이었다. Subversion은 그 이전 시대에서 가장 많이 사용하던 CVS와 많이 닮았다.

Git이 자랑하는 또 하나의 기능은 git svn 이라는 양방향 Subversion 지원 도구이다. Git을 Subversion 클라이언트로 사용할 수 있기 때문에 로컬에서는 Git의 기능을 활용하고 Push 할 때는 Subversion 서버에 Push 한다. 로컬 브랜치와 Merge, Staging Area, Rebase, Cherry-pick 등의 Git 기능을 충분히 사용할 수 있다. 같이 일하는 동료는 빛 한줄기 없는 선사시대 동굴에서 일하겠지만 말이다. git svn 은 기업에서 git을 사용할 수 있도록 돕는 출발점이다. 회사가 아직 공식적으로 Git을 사용하지 않더라도 동료들과 먼저 Git을 이용해 더 효율적으로 일할 수 있다. 이 Subversion 지원 도구는 우리를 DVCS 세상으로 인도하는 붉은 알약과 같다.

git svn

Git과 Subversion을 이어주는 명령은 git svn 으로 시작한다. 이 명령 뒤에 추가하는 명령이 몇 가지 더 있으며 간단한 예제를 보여주고 설명한다.

git svn 명령을 사용할 때는 절름발이인 Subversion을 사용하고 있다는 점을 염두하자. 우리가 로컬 브랜치와 Merge를 맘대로 쓸 수 있다고 하더라도 최대한 일직선으로 히스토리를 유지하는것이 좋다. Git 저장소처럼 사용하지 않는다.

히스토리를 재작성해서 Push 하지 말아야 한다. Git을 사용하는 동료들끼리 따로 Git 저장소에 Push 하지도 말아야 한다. Subversion은 단순하게 일직선 히스토리만 가능하다. 팀원중 일부는 SVN을 사용하고 일부는 Git을 사용하는 팀이라면 SVN Server를 사용해서 협업하는 것이 좋다. 그래야 삶이 편해진다.

설정하기

git svn 을 사용하려면 SVN 저장소가 하나 필요하다. 저장소에 쓰기 권한이 있어야 한다. 쓰기 가능한 한 test 저장소를 복사해서 해보자. Subversion에 포함된 svnsync 라는 도구를 사용하여 SVN 저장소를 복사한다.

로컬 Subversion 저장소를 하나 만든다.

$ mkdir /tmp/test-svn
$ svnadmin create /tmp/test-svn

그리고 모든 사용자가 revprops 속성을 변경할 수 있도록 항상 0을 반환하는 pre-revprop-change 스크립트를 준비한다(역주 - 파일이 없거나, 다른 이름으로 되어있을 수 있다. 이 경우 아래 내용으로 새로 파일을 만들고 실행 권한을 준다).

$ cat /tmp/test-svn/hooks/pre-revprop-change
#!/bin/sh
exit 0;
$ chmod +x /tmp/test-svn/hooks/pre-revprop-change

이제 svnsync init 명령으로 다른 Subversion 저장소를 로컬로 복사할 수 있도록 지정한다.

$ svnsync init file:///tmp/test-svn \
  http://your-svn-server.example.org/svn/

이렇게 다른 저장소의 주소를 설정하면 복사할 준비가 된다. 아래 명령으로 저장소를 실제로 복사한다.

$ svnsync sync file:///tmp/test-svn
Committed revision 1.
Copied properties for revision 1.
Transmitting file data .............................[...]
Committed revision 2.
Copied properties for revision 2.
[…]

이 명령은 몇 분 걸리지 않는다. 저장하는 위치가 로컬이 아니라 리모트 서버라면 오래 걸린다. 커밋이 100개 이하라고 해도 오래 걸린다. Subversion은 한번에 커밋을 하나씩 받아서 Push 하기 때문에 엄청나게 비효율적이다. 하지만, 저장소를 복사하는 다른 방법은 없다.

시작하기

이제 갖고 놀 Subversion 저장소를 하나 준비했다. git svn clone 명령으로 Subversion 저장소 전체를 Git 저장소로 가져온다. 만약 Subversion 저장소가 로컬에 있는 것이 아니라 리모트 서버에 있으면 file:///tmp/test-svn 부분에 서버 저장소의 URL을 적어 준다.

$ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags
Initialized empty Git repository in /private/tmp/progit/test-svn/.git/
r1 = dcbfb5891860124cc2e8cc616cded42624897125 (refs/remotes/origin/trunk)
    A	m4/acx_pthread.m4
    A	m4/stl_hash.m4
    A	java/src/test/java/com/google/protobuf/UnknownFieldSetTest.java
    A	java/src/test/java/com/google/protobuf/WireFormatTest.java
…
r75 = 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae (refs/remotes/origin/trunk)
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/my-calc-branch, 75
Found branch parent: (refs/remotes/origin/my-calc-branch) 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae
Following parent with do_switch
Successfully followed parent
r76 = 0fb585761df569eaecd8146c71e58d70147460a2 (refs/remotes/origin/my-calc-branch)
Checked out HEAD:
  file:///tmp/test-svn/trunk r75

이 명령은 사실 SVN 저장소 주소를 주고 git svn initgit svn fetch 명령을 순서대로 실행한 것과 같다. 이 명령은 시간이 좀 걸린다. Git은 커밋을 한 번에 하나씩 일일이 기록해야 하는데, 테스트용 프로젝트는 커밋이 75개 정도밖에 안되서 시간이 오래 걸리지 않는다. 커밋이 수천개인 프로젝트라면 몇 시간 혹은 몇 일이 걸릴 수도 있다.

-T trunk -b branches -t tags 부분은 Subversion이 어떤 브랜치 구조를 가지고 있는지 Git에게 알려주는 부분이다. Subversion 표준 형식과 다르면 이 옵션 부분에서 알맞은 이름을 지정해준다. 표준 형식을 사용한다면 간단하게 -s 옵션을 사용한다. 즉 아래의 명령도 같은 의미이다.

$ git svn clone file:///tmp/test-svn -s

Git에서 브랜치와 태그 정보가 제대로 보이는 지 확인한다.

$ git branch -a
* master
  remotes/origin/my-calc-branch
  remotes/origin/tags/2.0.2
  remotes/origin/tags/release-2.0.1
  remotes/origin/tags/release-2.0.2
  remotes/origin/tags/release-2.0.2rc1
  remotes/origin/trunk

Subversion 태그를 리모트 브랜치처럼 관리하는 것을 알아두어야 한다.

Plumbing 명령어인 show-ref 명령으로 리모트 브랜치의 정확한 이름을 확인할 수 있다.

$ git show-ref
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/heads/master
0fb585761df569eaecd8146c71e58d70147460a2 refs/remotes/origin/my-calc-branch
bfd2d79303166789fc73af4046651a4b35c12f0b refs/remotes/origin/tags/2.0.2
285c2b2e36e467dd4d91c8e3c0c0e1750b3fe8ca refs/remotes/origin/tags/release-2.0.1
cbda99cb45d9abcb9793db1d4f70ae562a969f1e refs/remotes/origin/tags/release-2.0.2
a9f074aa89e826d6f9d30808ce5ae3ffe711feda refs/remotes/origin/tags/release-2.0.2rc1
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/remotes/origin/trunk

Git 서버에서 Clone 하면 리모트 브랜치가 아니라 태그로 관리한다. 일반적인 Git 저장소라면 아래와 같다.

$ git show-ref
c3dcbe8488c6240392e8a5d7553bbffcb0f94ef0 refs/remotes/origin/master
32ef1d1c7cc8c603ab78416262cc421b80a8c2df refs/remotes/origin/branch-1
75f703a3580a9b81ead89fe1138e6da858c5ba18 refs/remotes/origin/branch-2
23f8588dde934e8f33c263c6d8359b2ae095f863 refs/tags/v0.1.0
7064938bd5e7ef47bfd79a685a62c1e2649e2ce7 refs/tags/v0.2.0
6dcb09b5b57875f334f61aebed695e2e4193db5e refs/tags/v1.0.0

Git 서버로부터 받은 태그라면 refs/tags 에 넣어서 관리한다.

Subversion 서버에 커밋하기

자 작업할 로컬 Git 저장소는 준비했다. 무엇인가 수정하고 Upstream으로 고친 내용을 Push 해야 할 때가 왔다. Git을 Subversion의 클라이언트로 사용해서 수정한 내용을 전송한다. 어떤 파일을 수정하고 커밋을 하면 그 수정한 내용은 Git의 로컬 저장소에 저장된다. Subversion 서버에는 아직 반영되지 않는다.

$ git commit -am 'Adding git-svn instructions to the README'
[master 4af61fd] Adding git-svn instructions to the README
 1 file changed, 5 insertions(+)

이제 수정한 내용을 Upstream에 Push 한다. Git 저장소에 여러개의 커밋을 쌓아놓고 한번에 Subversion 서버로 보낸다는 점을 잘 살펴보자. git svn dcommit 명령으로 서버에 Push 한다.

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	README.txt
Committed r77
    M	README.txt
r77 = 95e0222ba6399739834380eb10afcd73e0670bc5 (refs/remotes/origin/trunk)
No changes between 4af61fd05045e07598c553167e0f31c84fd6ffe1 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

이 명령은 새로 추가한 커밋을 모두 Subversion에 커밋하고 로컬 Git 커밋을 다시 만든다. 커밋을 다시 만들기 때문에 이미 저장된 커밋의 SHA-1 체크섬이 바뀐다. 그래서 리모트 Git 저장소와 Subversion 저장소를 함께 사용하면 안된다. 새로 만들어진 커밋을 살펴보면 아래와 같이 git-svn-id 가 추가된다.

$ git log -1
commit 95e0222ba6399739834380eb10afcd73e0670bc5
Author: ben <ben@0b684db3-b064-4277-89d1-21af03df0a68>
Date:   Thu Jul 24 03:08:36 2014 +0000

    Adding git-svn instructions to the README

    git-svn-id: file:///tmp/test-svn/trunk@77 0b684db3-b064-4277-89d1-21af03df0a68

원래 4af61fd 로 시작하는 SHA-1 체크섬이 지금은 95e0222 로 시작한다. 만약 Git 서버와 Subversion 서버에 함께 Push 하고 싶으면 우선 Subversion 서버에 dcommit 으로 Push를 하고 그 다음에 Git 서버에 Push 해야 한다.

새로운 변경사항 받아오기

다른 개발자와 함께 일하는 과정에서 다른 개발자가 Push 한 상태에서 Push를 하면 충돌이 날 수 있다. 충돌을 해결하지 않으면 서버로 Push 할 수 없다. 충돌이 나면 git svn 명령은 아래와 같이 보여준다.

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...

ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: d5837c4b461b7c0e018b49d12398769d2bfc240a and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 f414c433af0fd6734428cf9d2a9fd8ba00ada145 c80b6127dd04f5fcda218730ddf3a2da4eb39138 M	README.txt
Current branch master is up to date.
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.

이런 상황에서는 git svn rebase 명령으로 이 문제를 해결한다. 이 명령은 변경사항을 서버에서 내려받고 그 다음에 로컬의 변경사항을 그 위에 적용한다.

$ git svn rebase
Committing to file:///tmp/test-svn/trunk ...

ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: eaa029d99f87c5c822c5c29039d19111ff32ef46 and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 65536c6e30d263495c17d781962cfff12422693a b34372b25ccf4945fe5658fa381b075045e7702a M	README.txt
First, rewinding head to replay your work on top of it...
Applying: update foo
Using index info to reconstruct a base tree...
M	README.txt
Falling back to patching base and 3-way merge...
Auto-merging README.txt
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.

그러면 서버 코드 위에 변경사항을 적용하기 때문에 성공적으로 dcommit 명령을 마칠 수 있다.

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	README.txt
Committed r85
    M	README.txt
r85 = 9c29704cc0bbbed7bd58160cfb66cb9191835cd8 (refs/remotes/origin/trunk)
No changes between 5762f56732a958d6cfda681b661d2a239cc53ef5 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

Push 하기 전에 Upstream과 Merge 해야 하는 Git과 달리 git svn 은 충돌이 날때만 서버에 업데이트할 것이 있다고 알려 준다(Subversion 처럼). 이 점을 꼭 기억해야 한다. 만약 다른 사람이 한 파일을 수정하고 내가 그 사람과 다른 파일을 수정한다면 dcommit 은 성공적으로 수행된다.

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	configure.ac
Committed r87
    M	autogen.sh
r86 = d8450bab8a77228a644b7dc0e95977ffc61adff7 (refs/remotes/origin/trunk)
    M	configure.ac
r87 = f3653ea40cb4e26b6281cec102e35dcba1fe17c4 (refs/remotes/origin/trunk)
W: a0253d06732169107aa020390d9fefd2b1d92806 and refs/remotes/origin/trunk differ, using rebase:
:100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 e757b59a9439312d80d5d43bb65d4a7d0389ed6d M	autogen.sh
First, rewinding head to replay your work on top of it...

Push 하고 나면 프로젝트 상태가 달라진다는 점을 기억해야 한다. 충돌이 없으면 변경사항이 바램대로 적용되지 않아도 알려주지 않는다. 이 부분이 Git과 다른 점이다. Git에서는 서버로 보내기 전에 프로젝트 상태를 전부 테스트할 수 있다. SVN은 서버로 커밋하기 전과 후의 상태가 동일하다는 것이 보장되지 않는다.

git svn rebase 명령으로도 Subversion 서버의 변경사항을 가져올 수 있다. 커밋을 보낼 준비가 안됐어도 괞찮다. git svn fetch 명령을 사용해도 되지만 git svn rebase 명령은 변경사항을 가져오고 적용까지 한 번에 해준다.

$ git svn rebase
    M	autogen.sh
r88 = c9c5f83c64bd755368784b444bc7a0216cc1e17b (refs/remotes/origin/trunk)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/origin/trunk.

수시로 git svn rebase 명령을 사용하면 로컬 코드를 항상 최신 버전으로 유지할 수 있다. 이 명령을 사용하기 전에 워킹 디렉토리를 깨끗하게 만드는 것이 좋다. 깨끗하지 못하면 Stash를 하거나 임시로 커밋하고 나서 git svn rebase 명령을 실행하는 것이 좋다. 깨끗하지 않으면 충돌이 나서 Rebase가 중지될 수 있다.

Git 브랜치 문제

Git에 익숙한 사람이면 일을 할 때 먼저 토픽 브랜치를 만들고, 일을 끝낸 다음에, Merge 하는 방식을 쓰려고 할 것이다. 하지만, git svn 으로 Subversion 서버에 Push 할 때는 브랜치를 Merge 하지 않고 Rebase 해야 한다. Subversion은 일직선 히스토리 밖에 모르고 Git의 Merge 도 알지 못한다. 그래서 git svn 은 첫 번째 부모 정보만 사용해서 Git 커밋을 Subversion 커밋으로 변경한다.

예제를 하나 살펴보자. experiment 브랜치를 하나 만들고 2개의 변경사항을 커밋한다. 그리고 master 브랜치로 Merge 하고 나서 dcommit 명령을 수행하면 아래와 같은 모양이 된다.

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	CHANGES.txt
Committed r89
    M	CHANGES.txt
r89 = 89d492c884ea7c834353563d5d913c6adf933981 (refs/remotes/origin/trunk)
    M	COPYING.txt
    M	INSTALL.txt
Committed r90
    M	INSTALL.txt
    M	COPYING.txt
r90 = cb522197870e61467473391799148f6721bcf9a0 (refs/remotes/origin/trunk)
No changes between 71af502c214ba13123992338569f4669877f55fd and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

Merge 커밋이 들어 있는 히스토리에서 dcommit 명령을 실행한다. 그리고 나서 Git 히스토리를 살펴보면 experiment 브랜치의 커밋은 재작성되지 않았다. 대신 Merge 커밋만 SVN 서버로 전송됐을 뿐이다.

누군가 이 것을 내려 받으면 git merge --squash 한 것 마냥 결과가 합쳐진 Merge 커밋 하나만 볼 수 있다. 다른 사람은 언제 어디서 커밋한 것인지 알 수 없다.

Subversion의 브랜치

Subversion의 브랜치는 Git의 브랜치와 달라서 가능한 사용을 하지 않는 것이 좋다. 하지만 git svn 으로도 Subversion 브랜치를 관리할 수 있다.

SVN 브랜치 만들기

Subversion 브랜치를 만들려면 git svn branch [new-branch] 명령을 사용한다.

$ git svn branch opera
Copying file:///tmp/test-svn/trunk at r90 to file:///tmp/test-svn/branches/opera...
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/opera, 90
Found branch parent: (refs/remotes/origin/opera) cb522197870e61467473391799148f6721bcf9a0
Following parent with do_switch
Successfully followed parent
r91 = f1b64a3855d3c8dd84ee0ef10fa89d27f1584302 (refs/remotes/origin/opera)

이 명령은 Subversion의 svn copy trunk branches/opera 명령과 동일하다. 이 명령은 브랜치를 Checkout 해주지 않는다는 것을 주의해야 한다. 여기서 커밋하면 opera 브랜치가 아니라 trunk 브랜치에 커밋된다.

Subversion 브랜치 넘나들기

dcommit 명령은 어떻게 커밋 할 브랜치를 결정할까? Git은 히스토리에 있는 커밋중에서 가장 마지막으로 기록된 Subversion 브랜치를 찾는다. 즉, 현 브랜치 히스토리의 커밋 메시지에 있는 git-svn-id 항목을 읽는 것이기 때문에 오직 한 브랜치에만 전송할 수 있다.

동시에 여러 브랜치에서 작업하려면 Subversion 브랜치에 dcommit 할 수 있는 로컬 브랜치가 필요하다. 이 브랜치는 Subversion 커밋에서 시작하는 브랜치다. 아래와 같이 opera 브랜치를 만들면 독립적으로 일 할 수 있다.

$ git branch opera remotes/origin/opera

git merge 명령으로 opera 브랜치를 trunk 브랜치(master 브랜치 역할)에 Merge 한다. 하지만 -m 옵션을 주고 적절한 커밋 메시지를 작성하지 않으면 아무짝에 쓸모없는 "Merge branch opera" 같은 메시지가 커밋된다.

git merge 명령으로 Merge 한다는 것에 주목하자. Git은 자동으로 공통 커밋을 찾아서 Merge 에 참고하기 때문에 Subversion에서 하는 것보다 Merge가 더 잘된다. 여기서 생성되는 Merge 커밋은 일반적인 Merge 커밋과 다르다. 이 커밋을 Subversion 서버에 Push 해야 하지만 Subversion에서는 부모가 2개인 커밋이 있을 수 없다. 그래서 Push 하면 브랜치에서 만들었던 커밋 여러개가 하나로 합쳐진(squash된) 것처럼 Push 된다. 그래서 일단 Merge 하면 취소하거나 해당 브랜치에서 계속 작업하기 어렵다. dcommit 명령을 수행하면 Merge 한 브랜치의 정보를 어쩔 수 없이 잃어버리게 된다. Merge Base도 찾을 수 없게 된다. dcommit 명령은 Merge 한 것을 git merge --squash 로 Merge 한 것과 똑 같이 만들어 버린다. Branch를 Merge 한 정보는 저장되지 않기 때문에 이 문제를 해결할 방법이 없다. 문제를 최소화하려면 trunk에 Merge 하자마자 해당 브랜치를(여기서는 opera) 삭제하는 것이 좋다.

Subversion 명령

git svn 명령은 Git으로 전향하기 쉽도록 Subversion에 있는 것과 비슷한 명령어를 지원한다. 아마 여기서 설명하는 명령은 익숙할 것이다.

SVN 형식의 히스토리

Subversion에 익숙한 사람은 Git 히스토리를 SVN 형식으로 보고 싶을 수도 있다. git svn log 명령은 SVN 형식으로 히스토리를 보여준다.

$ git svn log
------------------------------------------------------------------------
r87 | schacon | 2014-05-02 16:07:37 -0700 (Sat, 02 May 2014) | 2 lines

autogen change

------------------------------------------------------------------------
r86 | schacon | 2014-05-02 16:00:21 -0700 (Sat, 02 May 2014) | 2 lines

Merge branch 'experiment'

------------------------------------------------------------------------
r85 | schacon | 2014-05-02 16:00:09 -0700 (Sat, 02 May 2014) | 2 lines

updated the changelog

git svn log 명령에서 기억해야 할 것은 두 가지다. 우선 오프라인에서 동작한다는 점이다. SVN의 svn log 명령어는 히스토리 데이터를 조회할 때 서버가 필요하다. 둘째로 이미 서버로 전송한 커밋만 출력해준다. 아직 dcommit 명령으로 서버에 전송하지 않은 로컬 Git 커밋은 보여주지 않는다. Subversion 서버에는 있지만 아직 내려받지 않은 변경사항도 보여주지 않는다. 즉, 현재 알고있는 Subversion 서버의 상태만 보여준다.

SVN 어노테이션

git svn log 명령이 svn log 명령을 흉내내는 것처럼 git svn blame [FILE] 명령으로 svn annotate 명령을 흉내낼 수 있다. 실행한 결과는 아래와 같다.

$ git svn blame README.txt
 2   temporal Protocol Buffers - Google's data interchange format
 2   temporal Copyright 2008 Google Inc.
 2   temporal http://code.google.com/apis/protocolbuffers/
 2   temporal
22   temporal C++ Installation - Unix
22   temporal =======================
 2   temporal
79    schacon Committing in git-svn.
78    schacon
 2   temporal To build and install the C++ Protocol Buffer runtime and the Protocol
 2   temporal Buffer compiler (protoc) execute the following:
 2   temporal

다시 한번 말하지만 이 명령도 아직 서버로 전송하지 않은 커밋은 보여주지 않는다.

SVN 서버 정보

svn info 명령은 git svn info 명령으로 대신할 수 있다.

$ git svn info
Path: .
URL: https://schacon-test.googlecode.com/svn/trunk
Repository Root: https://schacon-test.googlecode.com/svn
Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029
Revision: 87
Node Kind: directory
Schedule: normal
Last Changed Author: schacon
Last Changed Rev: 87
Last Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009)

blame 이나 log 명령이 오프라인으로 동작하듯이 이 명령도 오프라인으로 동작한다. 서버에서 가장 최근에 내려받은 정보를 출력한다.

Subversion에서 무시하는것 무시하기

Subversion 저장소를 클론하면 쓸데 없는 파일을 커밋하지 않도록 svn:ignore 속성을 .gitignore 파일로 만들고 싶을 것이다. git svn 에는 이 문제와 관련된 명령이 두 가지 있다. 하나는 git svn create-ignore 명령이다. 해당 위치에 커밋할 수 있는 .gitignore 파일을 생성해준다.

두 번째 방법은 git svn show-ignore 명령이다. .gitignore 에 추가할 목록을 출력해 준다. 프로젝트의 exclude 파일로 결과를 리다이렉트할 수 있다.

$ git svn show-ignore > .git/info/exclude

이렇게 하면 .gitignore 파일로 프로젝트를 더럽히지 않아도 된다. 혼자서만 Git을 사용하는 거라면 다른 팀원들은 프로젝트에 .gitignore 파일이 있는 것을 싫어 할 수 있다.

Git-Svn 요약

git svn 도구는 여러가지 이유로 Subversion 서버를 사용해야만 하는 상황에서 빛을 발한다. 하지만 Git의 모든 장점을 이용할 수는 없다. Git과 Subversion은 다르기 때문에 혼란이 빚어질 수도 있다. 이런 문제에 빠지지 않기 위해서 아래 가이드라인을 지켜야 한다.

  • Git 히스토리를 일직선으로 유지하라. git merge 로 Merge 커밋이 생기지 않도록 하라. Merge 말고 Rebase로 변경사항을 Master 브랜치에 적용하라.

  • 따로 Git 저장소 서버를 두지 말라. 클론을 빨리 하기 위해서 잠깐 하나 만들어 쓰는 것은 무방하나 절대로 Git 서버에 Push 하지는 말아야 한다. pre-receive 훅에서 git-svn-id 가 들어 있는 커밋 메시지는 거절하는 방법도 괜찮다.

이러한 가이드라인을 잘 지키면 Subversion 서버도 쓸만하다. 그래도 Git 서버를 사용할 수 있으면 Git 서버를 사용하는 것이 훨씬 좋다.

Git과 Mercurial

DVCS 세상에 Git만 존재하는 것은 아니다. 사실 Git 이외에도 다양한 시스템이 존재하는데 각자가 나름의 철학 대로 분산 버전 관리 시스템을 구현했다. Git 이외에는 Mercurial이 가장 많이 사용되는 분산 버전 관리 시스템이며 Git과 닮은 점도 많다.

Mercurial로 코드를 관리하는 프로젝트에서 클라이언트로 Git을 쓰고자 하는 사람에게도 좋은 소식이 있다. Git은 Mercurial 클라이언트로 동작할 수 있다. Mercurial을 위한 Bridge는 리모트 Helper로 구현돼 있는데 Git은 리모트를 통해서 서버 저장소의 코드를 가져와서 그렇다. 이 프로젝트의 이름은 git-remote-hg이라고 하며 https://github.com/felipec/git-remote-hg에 있다.

git-remote-hg

우선 git-remote-hg을 설치한다. 아래처럼 PATH 실행경로에 포함된 경로중 아무데나 git-remote-hg 파일을 저장하고 실행 권한을 준다.

$ curl -o ~/bin/git-remote-hg \
  https://raw.githubusercontent.com/felipec/git-remote-hg/master/git-remote-hg
$ chmod +x ~/bin/git-remote-hg

예제에서는 ~/bin 디렉토리가 $PATH 실행경로에 포함되어 있다고 가정한다. git-remote-hg를 실행하려면 Python 라이브러리 mercurial 이 필요하다. Python이 설치되어있다면 아래처럼 Mercurial 라이브러리를 설치한다.

$ pip install mercurial

(Python 설치가 안돼 있다면 https://www.python.org/ 사이트에서 다운로드 받아 설치한다.)

마지막으로 Mercurial 클라이언트도 설치해야 한다. https://www.mercurial-scm.org/ 사이트에서 다운로드 받아 설치할 수 있다.

이렇게 필요한 라이브러리와 프로그램을 설치하고 나면 준비가 끝난다. 이제 필요한 것은 소스코드를 Push 할 Mercurial 저장소다. 여기 예제에서는 Mercurial을 익힐 때 많이 쓰는 "hello world" 저장소를 로컬에 복제하고 마치 리모트 저장소인 것 처럼 사용한다.

$ hg clone http://selenic.com/repo/hello /tmp/hello

시작하기

이제 Push 할 수 있는 “서버”(?) 저장소가 준비됐고 여러가지 작업을 해 볼 수 있다. 잘 알려진 대로 Git과 Mercurial의 철학이나 사용방법은 크게 다르지 않다.

Git에서 늘 하던 것처럼 처음에는 Clone을 먼저 한다.

$ git clone hg::/tmp/hello /tmp/hello-git
$ cd /tmp/hello-git
$ git log --oneline --graph --decorate
* ac7955c (HEAD, origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master, master) Create a makefile
* 65bb417 Create a standard "hello, world" program

리모트 저장소가 hg로 시작하는 Mercurial 저장소지만 git clone 명령으로 쉽게 Clone 할 수 있다. 사실 내부에서는 git-remote-hg Bridge가 Git에 포함된 HTTP/S 프로토콜(리모트 Helper)과 비슷하게 동작한다. Git과 마찬가지로 Mercurial또한 모든 클라이언트가 전체 저장소 히스토리를 복제(Clone)해서 사용하도록 만들어졌기 때문에 Clone 명령으로 히스토리를 포함한 저장소 전체를 가져온다. 예제 프로젝트는 크기가 작아서 저장소를 금방 clone 한다.

log 명령으로 커밋 두 개를 볼 수 있으며 가장 최근 커밋으로는 여러 Ref 포인터로 가리키고 있다. Ref중 일부는 실제 존재하지 않을 수도 있다. .git 디렉토리가 실제로 어떻게 구성돼 있는지 보자.

$ tree .git/refs
.git/refs
├── heads
│   └── master
├── hg
│   └── origin
│       ├── bookmarks
│       │   └── master
│       └── branches
│           └── default
├── notes
│   └── hg
├── remotes
│   └── origin
│       └── HEAD
└── tags

9 directories, 5 files

git-remote-hg는 Git 스타일로 동작하도록 만들어 주는데 속으로 하는 일은 Git과 Mercurial을 매핑해 준다. 리모트 Ref를 refs/hg 디렉토리에 저장한다. 예를 들어 refs/hg/origin/branches/default 는 Git Ref 파일로 내용은 master 브랜치가 가리키는 커밋인 “ac7955c” 로 시작하는 SHA 해시값이다. refs/hg 디렉토리는 일종의 refs/remotes/origin 같은 것이지만 북마크와 브랜치가 구분된다는 점이 다르다.

notes/hg 파일은 git-remote-hg가 Git 커밋을 Mercurial Changeset ID와 매핑을 하기 위한 시작지점이다. 살짝 더 안을 들여다보면.

$ cat notes/hg
d4c10386...

$ git cat-file -p d4c10386...
tree 1781c96...
author remote-hg <> 1408066400 -0800
committer remote-hg <> 1408066400 -0800

Notes for master

$ git ls-tree 1781c96...
100644 blob ac9117f...	65bb417...
100644 blob 485e178...	ac7955c...

$ git cat-file -p ac9117f
0a04b987be5ae354b710cefeba0e2d9de7ad41a9

refs/notes/hg 파일은 트리 하나를 가리킨다. 이 트리는 다른 객체와 그 이름의 목록인 Git 객체 데이터베이스다. git ls-tree 명령은 이 트리 객체 안에 포함된 모드, 타입, 객체 해시, 파일 이름으로 된 여러 항목을 보여준다. 트리 객체에 포함된 한 항목을 더 자세히 살펴보면 “ac9117f” 으로 시작하는 이름(master 가 가리키는 커밋의 SHA-1 해시)의 Blob 객체를 확인할 수 있다. “ac9117f” 이 가리키는 내용은 “0a04b98” 로 시작하는 해시로 default 브랜치가 가리키는 Mercurial Changeset ID이다.

이런 내용은 몰라도 되고 모른다고 걱정할 필요 없다. 일반적인 워크플로에서 Git 리모트를 사용하는 것과 크게 다르지 않다.

다만 한가지, 다음 내용으로 넘어가기 전에 Ignore 파일을 살펴보자. Mercurial과 Git의 Ignore 파일은 방식이 거의 비슷하지만 아무래도 .gitignore 파일을 Mercurial 저장소에 넣기는 좀 껄끄럽다. 다행히도 Mercurial의 Ignore 파일 패턴의 형식은 Git과 동일해서 아래와 같이 복사하기만 하면 된다.

$ cp .hgignore .git/info/exclude

.git/info/exclude 파일은 .gitignore 파일처럼 동작하지만 커밋할 수 없다.

워크플로

이런저런 작업을하고 master 브랜치에 커밋하면 원격 저장소에 Push 할 준비가 된다. 현재 저장소 히스토리를 살펴보면 아래와 같다.

$ git log --oneline --graph --decorate
* ba04a2a (HEAD, master) Update makefile
* d25d16f Goodbye
* ac7955c (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Create a makefile
* 65bb417 Create a standard "hello, world" program

master 브랜치는 origin/master 브랜치보다 커밋이 두 개를 많으며 이 두 커밋은 로컬에만 존재한다. 그와 동시에 누군가가 커밋해서 리모트 저장소에 Push 했다고 가정해보자.

$ git fetch
From hg::/tmp/hello
   ac7955c..df85e87  master     -> origin/master
   ac7955c..df85e87  branches/default -> origin/branches/default
$ git log --oneline --graph --decorate --all
* 7b07969 (refs/notes/hg) Notes for default
* d4c1038 Notes for master
* df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
| * ba04a2a (HEAD, master) Update makefile
| * d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard "hello, world" program

--all 옵션으로 히스토리를 보면 “notes” Ref도 볼 수 있는데 git-remote-hg에서 내부적으로 사용하는 것이므로 유저는 신경쓰지 않아도 된다. 나머지 내용은 예상한 대로다. origin/master 브랜치에 커밋 하나가 추가되어 있어 히스토리가 갈라졌다. 이 장에서 살펴보는 다른 버전관리 시스템과는 달리 Mercurial은 Merge를 충분히 잘 다루기 때문에 특별히 더 할 일이 없다.

$ git merge origin/master
Auto-merging hello.c
Merge made by the 'recursive' strategy.
 hello.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --decorate
*   0c64627 (HEAD, master) Merge remote-tracking branch 'origin/master'
|\
| * df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
* | ba04a2a Update makefile
* | d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard "hello, world" program

완벽하고 멋져. 이렇게 Merge 하고 나서 테스트가 통과한다면 정말로 Push 하고 공유할 준비가 끝난 것이다.

$ git push
To hg::/tmp/hello
   df85e87..0c64627  master -> master

정말 완벽하게 멋져! Mercurial 저장소 히스토리를 살펴보면 기대한대로 모든 것이 멋지게 끝난 것을 확인할 수 있다.

$ hg log -G --style compact
o    5[tip]:4,2   dc8fa4f932b8   2014-08-14 19:33 -0700   ben
|\     Merge remote-tracking branch 'origin/master'
| |
| o  4   64f27bcefc35   2014-08-14 19:27 -0700   ben
| |    Update makefile
| |
| o  3:1   4256fc29598f   2014-08-14 19:27 -0700   ben
| |    Goodbye
| |
@ |  2   7db0b4848b3c   2014-08-14 19:30 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard "hello, world" program

Changeset 2 번은 Mercurial로 만든 Changeset이다. 3 번과 4 번 Changeset은 git-remote-hg로 만든 Changeset이고 Git으로 Push 한 커밋이다.

브랜치와 북마크

Git 브랜치는 한 종류 뿐이다. Git 브랜치는 새 커밋이 추가되면 자동으로 마지맛 커밋으로 이동하는 포인터다. Mercurial에서는 이런 Refs를 “북마크” 라고 부르는데 하는 행동은 Git의 브랜치와 같다.

Mercurial에서 사용하는 “브랜치” 의 개념은 Git보다 좀 더 무겁다. Mercurial은 Changeset에 브랜치도 함께 저장한다. 즉 브랜치는 히스토리에 영원히 기록된다. develop 브랜치에 커밋을 하나 만드는 예제를 살펴보자.

$ hg log -l 1
changeset:   6:8f65e5e02793
branch:      develop
tag:         tip
user:        Ben Straub <ben@straub.cc>
date:        Thu Aug 14 20:06:38 2014 -0700
summary:     More documentation

“branch” 로 시작하는 라인이 있는 것을 볼 수 있다. Git은 이런 방식을 흉내낼 수(흉내낼 필요도) 없다(Git의 ref로 표현할 수는 있겠다). 하지만 Mercurial이 필요로 하는 정보이기에 git-remote-hg는 이런 비슷한 정보가 필요하다.

Mercurial 북마크를 만드는 것은 Git의 브랜치를 만드는 것과 같이 쉽다. Git으로 Clone 한 Mercurial 저장소에 아래와 같이 브랜치를 Push 한다.

$ git checkout -b featureA
Switched to a new branch 'featureA'
$ git push origin featureA
To hg::/tmp/hello
 * [new branch]      featureA -> featureA

이렇게만 해도 북마크가 생성된다. Mercurial로 저장소 내용을 확인하면 아래와 같다.

$ hg bookmarks
   featureA                  5:bd5ac26f11f9
$ hg log --style compact -G
@  6[tip]   8f65e5e02793   2014-08-14 20:06 -0700   ben
|    More documentation
|
o    5[featureA]:4,2   bd5ac26f11f9   2014-08-14 20:02 -0700   ben
|\     Merge remote-tracking branch 'origin/master'
| |
| o  4   0434aaa6b91f   2014-08-14 20:01 -0700   ben
| |    update makefile
| |
| o  3:1   318914536c86   2014-08-14 20:00 -0700   ben
| |    goodbye
| |
o |  2   f098c7f45c4f   2014-08-14 20:01 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard "hello, world" program

[featureA] 태그가 리비전 5에 생긴 것을 볼 수 있다. Git을 클라이언트로 사용하는 저장소에서는 Git 브랜치처럼 사용한다. Git 클라이언트 저장소에서 한 가지 할 수 없는 것은 서버의 북마크를 삭제하지 못 한다(이는 리모트 Helper의 제약사항이다).

Git보다 무거운 Mercurial 브랜치도 물론 사용 가능하다. 브랜치 이름에 branches 네임스페이스를 사용하면 된다.

$ git checkout -b branches/permanent
Switched to a new branch 'branches/permanent'
$ vi Makefile
$ git commit -am 'A permanent change'
$ git push origin branches/permanent
To hg::/tmp/hello
 * [new branch]      branches/permanent -> branches/permanent

위의 내용을 Mercurial에서 확인하면 아래와 같다.

$ hg branches
permanent                      7:a4529d07aad4
develop                        6:8f65e5e02793
default                        5:bd5ac26f11f9 (inactive)
$ hg log -G
o  changeset:   7:a4529d07aad4
|  branch:      permanent
|  tag:         tip
|  parent:      5:bd5ac26f11f9
|  user:        Ben Straub <ben@straub.cc>
|  date:        Thu Aug 14 20:21:09 2014 -0700
|  summary:     A permanent change
|
| @  changeset:   6:8f65e5e02793
|/   branch:      develop
|    user:        Ben Straub <ben@straub.cc>
|    date:        Thu Aug 14 20:06:38 2014 -0700
|    summary:     More documentation
|
o    changeset:   5:bd5ac26f11f9
|\   bookmark:    featureA
| |  parent:      4:0434aaa6b91f
| |  parent:      2:f098c7f45c4f
| |  user:        Ben Straub <ben@straub.cc>
| |  date:        Thu Aug 14 20:02:21 2014 -0700
| |  summary:     Merge remote-tracking branch 'origin/master'
[...]

“permanent” 라는 브랜치가 Changeset 7 번에 기록됐다.

Mercurial 저장소를 Clone 한 Git 저장소에서는 Git 브랜치를 쓰듯 Checkout, Checkout, Fetch, Merge, Pull 명령을 그대로 쓰면 된다. 반드시 기억해야 할 게 하나 있는데 Mercurial은 히스토리를 재작성을 지원하지 않고 단순히 추가된다. Git으로 Rebase를 하고 강제로 Push 하면 Mercurial 저장소의 모습은 아래와 같아진다.

$ hg log --style compact -G
o  10[tip]   99611176cbc9   2014-08-14 20:21 -0700   ben
|    A permanent change
|
o  9   f23e12f939c3   2014-08-14 20:01 -0700   ben
|    Add some documentation
|
o  8:1   c16971d33922   2014-08-14 20:00 -0700   ben
|    goodbye
|
| o  7:5   a4529d07aad4   2014-08-14 20:21 -0700   ben
| |    A permanent change
| |
| | @  6   8f65e5e02793   2014-08-14 20:06 -0700   ben
| |/     More documentation
| |
| o    5[featureA]:4,2   bd5ac26f11f9   2014-08-14 20:02 -0700   ben
| |\     Merge remote-tracking branch 'origin/master'
| | |
| | o  4   0434aaa6b91f   2014-08-14 20:01 -0700   ben
| | |    update makefile
| | |
+---o  3:1   318914536c86   2014-08-14 20:00 -0700   ben
| |      goodbye
| |
| o  2   f098c7f45c4f   2014-08-14 20:01 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard "hello, world" program

Changeset 8, 9, 10 이 생성됐고 permanent 브랜치에 속한다. 하지만 예전에 Push 했던 Changeset들이 그대로 남아있다. 그러면 Mercurial 저장소를 공유하는 동료들은 혼란스럽다. 이딴식으로 커밋을 재작성 하고 강제로 Push 하지 말지어다.

Mercurial 요약

Git과 Mercurial은 시스템이 크게 다르지 않아서 쉽게 경계를 넘나들 수 있다. 이미 리모트로 떠나 보낸 커밋을 재작성하지 않는다면(물론 Git도 마찬가지) 지금 작업하고 있는 저장소가 Git인지 Mercurial인지 몰라도 된다.

Git과 Bazaar

DCVS 중에 다른 유명한 것으로 Bazaar 라는 것이 있다. Bazaar는 무료이고 오픈소스로 GNU 프로젝트 중 하나이다. Git과 동작방식이 매우 다르다. Git과 동일한 동작을 하기 위해 매우 다를 키워드를 사용하기도 하며, 같은 키워드가 전혀 다른 의미로 쓰이기도 한다. 특히 브랜치 관리에 대한 개념이 매우 달라 Git을 쓰던 사람에게는 매우 혼란스럽기도 하다. 그럼에도 불구하고 Git 클라이언트를 사용하여 Bazaar의 저장소를 기반으로 버전관리 작업을 할 수 있다.

여러분이 Git을 Bazaar 클라이언트로 사용하도록 기능을 제공해주는 수 많은 프로젝트가 있다. 이 책에서는 Felipe Contreras의 프로젝트를 가져다 사용하며 https://github.com/felipec/git-remote-bzr 에서 구할 수 있다. 프로젝트에서 git-remote-bzr 파일을 받아 `$PATH`에 지정된 디렉토리 중 하나에 위치시켜 두면 바로 사용할 수 있다.

$ wget https://raw.github.com/felipec/git-remote-bzr/master/git-remote-bzr -O ~/bin/git-remote-bzr
$ chmod +x ~/bin/git-remote-bzr

물론 Bazaar는 설치되어 있어야 한다. 이로서 준비 작업은 끝이다.

Bazaar 저장소로부터 Git 저장소 생성

Bazaar 저장소를 로컬로 Clone 하기는 쉽다. 저장소 주소 앞에 bzr:: 문자열을 붙여서 Clone 하면 된다. Git과 Bazaar 모두 저장소 전체를 로컬로 복제하여 사용하기 때문에 로컬에 이미 내려받은 Bazaar 저장소를 Clone 해 올 수도 있지만 권장하지는 않는다. Bazaar 저장소가 가리키고 있는 원래의 리모트 저장소로부터 직접 Clone 하는 것이 여러모로 편리하다.

리모트 저장소의 주소가 다음과 같이 ssh를 사용하는 경우 bzr+ssh://developer@mybazaarserver:myproject Clone 할 때의 주소는 다음과 같다.

$ git clone bzr::bzr+ssh://developer@mybazaarserver:myproject myProject-Git
$ cd myProject-Git

Clone을 하고 나면 Git 저장소가 생성되었지만 디스크 사용에 있어서 최적화 된 상태는 아니다. 저장소 크기가 제법 큰 경우 다음 명령으로 Git 저장소의 디스크 사용을 최적화 시킬 수 있다.

$ git gc --aggressive

Bazaar 브랜치

Bazaar의 경우 저장소에는 많은 브랜치가 있더라도 Clone 할 때는 브랜치 하나만을 Clone 할 수 있다. 하지만 git-remote-bzr 명령은 두 가지 방식 다 사용 가능하다. 예를 들어 브랜치 하나만 Clone 하려면:

$ git clone bzr::bzr://bzr.savannah.gnu.org/emacs/trunk emacs-trunk

저장소 전체를 Clone 하려면:

$ git clone bzr::bzr://bzr.savannah.gnu.org/emacs emacs

두 번째 명령을 실행하면 emacs 저장소의 모든 브랜치를 Clone 하게 된다. 일부 브랜치만 Clone 하거나 사용하도록 다음과 같이 설정할 수도 있다.

$ git config remote-bzr.branches 'trunk, xwindow'

어떤 리모트 저장소의 경우 브랜치의 목록을 보여주지 않을수도 있지만 아래와 같이 직접 지정해준다면 어렵지 않게 지정된 브랜치를 포함하는 저장소 단위로 Clone 할 수 있다.

$ git init emacs
$ git remote add origin bzr::bzr://bzr.savannah.gnu.org/emacs
$ git config remote-bzr.branches 'trunk, xwindow'
$ git fetch

.bzrignore로 무시하는 파일 Git에서도 무시하기

Bazaar로 관리하는 저장소에서 작업하는 경우 .gitignore 파일을 운영하지 말아야 한다. 이 파일이 생성되어 버전관리에 추가된다면 Bazaar를 사용하는 다른 동료를 방해하는 꼴이다. 이를 해결하기 위해 .git/info/exclude 파일에 내용을 입력하거나 링크로 생성하는 방법이 있다. 자세한 내용은 이어지는 부분에서 확인할 수 있다.

Bazaar의 파일 무시하기 기능은 Git의 무시하기 기능과 같은 방식으로 동작한다. 하지만 정확히 같은 것은 아니며 두 가지 기능이 Git과 다르게 동작한다. 정확한 전체 내용은 ignore 도움말 에서 확인할 수 있다. 두 가지 다른점은 다음과 같다.

  1. "!!" 문자열로 시작하는 패턴은 이미 "!" 문자열로 시작하는 정의한 패턴을 강제로 다시 적용시키는 규칙이다. (무시하지 않는 것을 다시 무시하기!)

  2. "RE:" 문자열로 시작하는 규칙은 Python 정규표현식 을 적용한다. Git은 Glob 패턴만 적용 가능하다.

이러한 Bazaar 파일 무시하기 규칙을 Git 저장소 관리에도 적용하려면:

  1. .bzrignore 파일이 위의 두 가지 Git과 다른 규칙을 사용하지 않고 있다면 간단히 심볼릭 링크를 만들어 Git 저장소에도 적용할 수 있다: ln -s .bzrignore .git/info/exclude

  2. 반대의 경우 .git/info/exclude 파일을 일반 파일로 생성하거나 수정해서 .bzrignore 파일과 같은 의미가 적용되도록 직접 수정해야 한다.

어떤 경우에도 .git/info/exclude 파일이 .bzrignore 파일이 변경됨에 따라 적절하게 내용을 반영하고 있는지 주의를 기울여 살펴봐야 한다. "!!" 패턴이나 "RE:" 패턴 규칙이 새로이 .bzrignore 파일에 적용된 변경이 있을 수 있다. 이렇게 Git이 적절히 처리할 수 없는 패턴이 새로이 생겨난 경우 .git/info/exclude 파일을 일반파일로 작성하고 패턴의 내용을 이해한 다음 적절히 변환하여 Git 패턴으로 작성해야 한다. .git/info/exclude 파일은 심볼릭 링크였으므로 일단 이를 지우는 것 부터 반드시 실행해야 한다. 그 이후 .bzrignore 파일을 .git/info/exclude 파일로 복사하고 Git이 이해하지 못하는 패턴에 대해 변경 작업을 해야 한다. "!!" 패턴의 경우 Git에는 적용이 불가능하기 때문에 주의해서 이를 변환해야 한다.

리모트 저장소로부터 변경 내용 가져오기

원격 저장소로부터 변경 내용을 가져오려면 보통의 Git 명령을 사용하듯 Pull 명령을 사용한다. 로컬의 변경 내용이 master 브랜치에 있다면 origin/master 브랜치를 Merge 하거나 다음과 같이 Rebase 하게 된다.

$ git pull --rebase origin

리모트 저장소로 변경 내용 보내기

Bazaar에도 Merge 커밋에 대한 개념이 동일하게 있기 때문에 Merge 커밋을 Push 하는 것은 아무런 문제가 없다. 어떤 브랜치에서 작업을 하다가 master 브랜치로 Merge 하고 이를 Push 하는 것 물론 가능하다. 직접 생성한 브랜치를 Push 할 수도 있다. 브랜치를 만들고 테스트와 커밋을 만들고 Bazaar 원격 저장소로 Push 하면 된다.

$ git push origin master

주의

Git의 리모트-헬퍼 프레임워크의 제약사항이 몇가지 적용된다. 특히 아래의 명령을 적용하기 불가능하다.

  • git push origin :branch-to-delete (Bazaar 에서는 이런식으로 Ref 또는 브랜치 삭제가 불가능)

  • git push origin old:new (old 브랜치를 Push 하게 됨)

  • git push --dry-run origin branch (실제로 Push 하게 됨)

요약

Git과 Bazaar의 버전관리 모델이 매우 닮아있기 때문에 둘의 경계를 넘나드는 작업은 그리 어려운 것은 아니다. 하지만 아무런 제약사항이 없는 것은 아니기 때문에 Git을 Client로 사용할 때 항상 원격 저장소가 Bazaar 임을 생각해두고 사용한다면 무리는 없을 것이다.

Git과 Perforce

Perforce는 기업에서 많이 사용하는 버전 관리 시스템이다. 1995년 무렵부터 사용됐으며 이 장에서 다루는 시스템 중에서 가장 오래된 버전 관리 시스템이다. 처음 Perforce를 만든 당시 환경에 맞게 설계했기 때문에 몇 가지 특징이 있다. 언제나 중앙 서버에 연결할 수 있고 로컬에는 한 버전만 저장한다. Perforce가 잘 맞는 워크플로도 있겠지만 Git을 도입하면 훨씬 나은 워크플로를 적용할 수 있을 것이라 생각한다.

Perforce와 Git을 함께 사용하는 방법은 두 가지다. 첫 번째는 Perforce가 제공하는 “Git Fusion” 이다. Perforce Depot의 서브트리를 읽고 쓸 수 있는 Git 저장소로 노출 시켜 준다. 두 번째 방법은 git-p4라는 클라이언트 Bridge를 사용하여 Git을 Perforce의 클라이언트로 사용하는 것이다. 이 방법은 Perforce 서버를 건드리지 않아도 된다.

Git Fusion

Perforce는 Git Fusion(http://www.perforce.com/git-fusion 에서 다운로드 받을 수 있음)이라는 제품을 제공한다. 이 제품은 Perforce 서버와 서버에 있는 Git 저장소를 동기화한다.

Git Fusion 설치

Perforce 데몬과 Git Fusion이 포함된 가상 머신 이미지를 내려받는 것이 Git Fusion을 가장 쉽게 설치하는 방법이다. 가상머신 이미지는 http://www.perforce.com/downloads/Perforce/20-UserGit Fusion 탭에서 받을 수 있다. VirtualBox 같은 가상화 소프트웨어로 이 이미지를 동작시킬 수 있다.

가상머신을 처음 부팅시키면 root, perforce, git 세 Linux 계정의 암호를 입력하라는 화면과 가상머신 인스턴스 이름을 입력하라는 화면이 나타난다. 인스턴스 이름은 같은 네트워크 안에서 인스턴스를 구분하고 접근하기 위해 사용하는 이름이다. 이러한 과정을 마치고 나면 아래와 같은 화면을 볼 수 있다.

Git Fusion 가상머신 부팅 화면.
Figure 146. Git Fusion 가상머신 부팅 화면.

화면의 IP 주소는 계속 사용할 거라서 기억해두어야 한다. 다음은 Perforce 사용자를 생성해보자. “Login” 항목으로 이동해서 엔터키를 누르면(또는 SSH로 접속하면) root 로 로그인한다. 그리고 아래 명령으로 Perforce 사용자를 생성한다.

$ p4 -p localhost:1666 -u super user -f john
$ p4 -p localhost:1666 -u john passwd
$ exit

첫 번째 명령을 실행하면 VI 편집기가 뜨고 생성한 사용자의 정보를 수정할 수 있다. 기본으로 입력되어있는 정보를 그대로 사용하려면 간단히 :wq 를 키보드로 입력하고 엔터키를 누른다. 두 번째 명령을 실행하면 생성한 Perforce 사용자의 암호를 묻는데 안전하게 두 번 묻는다. 쉘에서 하는 작업은 여기까지이므로 쉘에서 나온다.

다음으로 해야 할 작업은 클라이언트 환경에서 Git이 SSL 인증서를 검증하지 않도록 설정하는 것이다. Git Fusion 이미지에 포함된 SSL 인증서는 도메인 이름으로 접속을 검증한다. 여기서는 IP 주소로 접근할 거라서 Git이 HTTPS 인증서를 검증하지 못한다. 그래서 접속할 수도 없다. 이 Git Fusion 가상머신 이미지를 실제로 사용할 거라면 Perforce Git Fusion 메뉴얼을 참고해서 SSL 인증서를 새로 설치해서 사용하는 것을 권한다. 그냥 해보는 거라면 인증서 검증을 안하면 된다.

$ export GIT_SSL_NO_VERIFY=true

제대로 작동하는지 아래 명령으로 확인해보자.

$ git clone https://10.0.1.254/Talkhouse
Cloning into 'Talkhouse'...
Username for 'https://10.0.1.254': john
Password for 'https://john@10.0.1.254':
remote: Counting objects: 630, done.
remote: Compressing objects: 100% (581/581), done.
remote: Total 630 (delta 172), reused 0 (delta 0)
Receiving objects: 100% (630/630), 1.22 MiB | 0 bytes/s, done.
Resolving deltas: 100% (172/172), done.
Checking connectivity... done.

Perforce가 제공한 가상머신 이미지는 안에 샘플 프로젝트가 하나 들어 있다. HTTPS 프로토콜로 프로젝트를 Clone 할 때 Git은 인증정보를 묻는다. 앞서 만든 john 이라는 사용자이름과 암호를 입력한다. Credential 캐시로 사용자이름과 암호를 저장해 두면 이 단계를 건너뛴다.

Git Fusion 설정

Git Fusion을 설치하고 나서 설정을 변경할 수 있다. 이미 잘 쓰고 있는 Perforce 클라이언트가 있으면 그걸로 변경할 수 있다. Perforce 서버의 //.git-fusion 디렉토리에 있는 파일을 수정하면 된다. 디렉토리 구조는 아래와 같다.

$ tree
.
├── objects
│   ├── repos
│   │   └── [...]
│   └── trees
│       └── [...]
│
├── p4gf_config
├── repos
│   └── Talkhouse
│       └── p4gf_config
└── users
    └── p4gf_usermap

498 directories, 287 files

objects 디렉토리는 Git Fusion이 Perforce 객체와 Git을 양방향으로 대응시키는 내용을 담고 있으므로 이 디렉토리 안의 내용을 임의로 수정하지 말아야 한다. p4gf_config 파일은 루트 디렉토리에, 그리고 각 저장소마다 하나씩 있으며 Git Fusion이 어떻게 동작하는지를 설정하는 파일이다. 루트 디렉토리의 이 파일 내용을 보면 아래와 같다.

[repo-creation]
charset = utf8

[git-to-perforce]
change-owner = author
enable-git-branch-creation = yes
enable-swarm-reviews = yes
enable-git-merge-commits = yes
enable-git-submodules = yes
preflight-commit = none
ignore-author-permissions = no
read-permission-check = none
git-merge-avoidance-after-change-num = 12107

[perforce-to-git]
http-url = none
ssh-url = none

[@features]
imports = False
chunked-push = False
matrix2 = False
parallel-push = False

[authentication]
email-case-sensitivity = no

이 책에서는 이 파일 내용 한 줄 한 줄 그 의미를 설명하지는 않는다. Git에서 사용하는 환경설정 파일과 마찬가지로 INI 형식으로 관리된다는 점을 알아두면 된다. 루트 디렉토리에 위치한 이 파일은 전역 설정이다. repos/Talkhouse/p4gf_config 처럼 각 저장소마다 설정할 수도 있는데 전역설정 위에(Override) 적용된다. 각 저장소별 설정 파일의 내용을 보면 아래와 같이 전역 설정과 다른 섹션이 있다.

[Talkhouse-master]
git-branch-name = master
view = //depot/Talkhouse/main-dev/... ...

파일 내용을 보면 Perforce와 Git의 브랜치간 매핑 정보를 볼 수 있다. 섹션 이름은 겹치지만 않으면 아무거나 사용할 수 있다. git-branch-name 항목은 길고 입력하기 어려운 Depot 경로를 Git에서 사용하기에 편한 이름으로 연결해준다. view 항목은 어떻게 Perforce 파일이 Git 저장소에 매핑되는지를 View 매핑 문법을 사용하여 설정한다. 여러 항목을 설정할 수 있다.

[multi-project-mapping]
git-branch-name = master
view = //depot/project1/main/... project1/...
       //depot/project2/mainline/... project2/...

이와 같은 식으로 구성하면 디렉토리 안의 변경사항이 Git 저장소로 반영된다.

마지막으로 살펴볼 설정파일은 users/p4gf_usermap 파일로 Perforce 사용자를 Git 사용자로 매핑하는 역할을 하는데 때에 따라서는 필요하지 않을 수도 있다. Perforce Changeset을 Git의 커밋으로 변환할 때 Git Fusion은 Perforce 사용자의 이름과 이메일 주소를 가지고 Git 커밋의 저자와 커미터 정보를 입력한다. 반대로 Git 커밋을 Perforce Changeset으로 변환할 때는 Git 커밋에 저장된 이름과 이메일 정보를 가져와 Changeset에 기록하고 이 정보로 권한을 확인한다. 보통은 리모트 저장소에 동일한 정보가 등록 돼있어서 문제없겠지만 정보가 다르다면 아래와 같이 매핑 정보를 설정해야 한다.

john john@example.com "John Doe"
john johnny@appleseed.net "John Doe"
bob employeeX@example.com "Anon X. Mouse"
joe employeeY@example.com "Anon Y. Mouse"

매핑 설정은 한 라인에 한 유저씩 설정하며 ID 이메일 "<긴 이름>" 형식으로 구성한다. 첫 번째 라인과 두 번째 라인은 이메일 주소 두 개를 Perforce 유저 하나로 매핑한다. 이렇게 설정하면 Git 커밋에 이메일 주소를 여러 개 사용했어도 한 Perforce 유저의 Changeset으로 변환할 수 있다. 반대로 Perforce Chageset을 Git 커밋으로 변경할 때는 첫 번째 정보를 이용하여 커밋의 저자 정보를 기록한다.

마지막 두 라인은 Perforce 사용자 bob도 joe도 Git 커밋으로 변환할 때는 같은 이름을 쓰도록 설정한 것이다. 이는 내부 프로젝트를 오픈 소스로 공개할 때, 내부 개발자 이름을 드러내지 않고 외부로 오픈할 때 유용하다. Git 커밋을 한 사람이 작성한 것으로 하려는게 아니라면 사람 이름과 이메일 주소는 중복되지 않아야 한다.

워크플로

Perforce의 Git Fusion은 Git과 Perforce사이에서 양방향의 데이터 변환을 지원하는 Bridge이다. Git을 Perforce의 클라이언트로 사용할 때 어떤식으로 사용하면 되는지 예제를 통해 살펴보자. 위에서 살펴본 설정파일로 “Jam” 이라는 Perforce 프로젝트를 아래와 같이 Clone 할 수 있다.

$ git clone https://10.0.1.254/Jam
Cloning into 'Jam'...
Username for 'https://10.0.1.254': john
Password for 'https://john@10.0.1.254':
remote: Counting objects: 2070, done.
remote: Compressing objects: 100% (1704/1704), done.
Receiving objects: 100% (2070/2070), 1.21 MiB | 0 bytes/s, done.
remote: Total 2070 (delta 1242), reused 0 (delta 0)
Resolving deltas: 100% (1242/1242), done.
Checking connectivity... done.
$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/rel2.1
$ git log --oneline --decorate --graph --all
* 0a38c33 (origin/rel2.1) Create Jam 2.1 release branch.
| * d254865 (HEAD, origin/master, origin/HEAD, master) Upgrade to latest metrowerks on Beos -- the Intel one.
| * bd2f54a Put in fix for jam's NT handle leak.
| * c0f29e7 Fix URL in a jam doc
| * cc644ac Radstone's lynx port.
[...]

먼저 처음 저장소를 Clone 할 때는 시간이 매우 많이 걸릴 수 있다. Git Fusion이 Perforce 저장소에서 가져온 모든 Changeset을 Git 커밋으로 변환하기 때문이다. 변환하는 과정이야 빠르더라도 히스토리 자체 크기가 크다면 전체 Clone 하는 시간은 오래 걸리기 마련이다. 이렇게 한 번 전체를 Clone 한 후에 추가된 내용만을 받아오는 시간은 Git과 마찬가지로 오래걸리지 않는다.

Clone 한 저장소는 지금까지 살펴본 일반적인 Git 저장소와 똑같다. 확인해보면 브랜치가 3개 있다. 먼저 Git은 로컬 master 브랜치가 서버의 origin/master 브랜치를 추적하도록 미리 만들어 둔다. 내키는대로 파일을 좀 수정하고 커밋을 두어번 하면 아래와 같이 히스토리가 쌓인 모습을 볼 수 있다.

# ...
$ git log --oneline --decorate --graph --all
* cfd46ab (HEAD, master) Add documentation for new feature
* a730d77 Whitespace
* d254865 (origin/master, origin/HEAD) Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]

새 커밋 두 개가 로컬 히스토리에 쌓였다. 다른 사람이 Push 한 일이 있는지 확인해보자.

$ git fetch
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://10.0.1.254/Jam
   d254865..6afeb15  master     -> origin/master
$ git log --oneline --decorate --graph --all
* 6afeb15 (origin/master, origin/HEAD) Update copyright
| * cfd46ab (HEAD, master) Add documentation for new feature
| * a730d77 Whitespace
|/
* d254865 Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]

그새 누군가 부지런히 일을 했나보다. 정확히 누가 어떤 일을 했는지는 커밋을 까봐야 알겠지만 어쨋든 Git Fusion은 서버로부터 새로 가져온 Changeset을 변환해서 6afeb15 커밋을 만들어놨다. 여태 Git에서 본 여타 커밋이랑 다르지 않다. 이제 Perforce 서버가 Merge 커밋을 어떻게 다루는지 살펴보자.

$ git merge origin/master
Auto-merging README
Merge made by the 'recursive' strategy.
 README | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 6), reused 0 (delta 0)
remote: Perforce: 100% (3/3) Loading commit tree into memory...
remote: Perforce: 100% (5/5) Finding child commits...
remote: Perforce: Running git fast-export...
remote: Perforce: 100% (3/3) Checking commits...
remote: Processing will continue even if connection is closed.
remote: Perforce: 100% (3/3) Copying changelists...
remote: Perforce: Submitting new Git commit objects to Perforce: 4
To https://10.0.1.254/Jam
   6afeb15..89cba2b  master -> master

Git은 이렇게 Merge 하고 Push 하면 잘 되었겠거니 한다. Perforce의 관점에서 README 파일의 히스토리를 생각해보자. Perforce 히스토리는 p4v 그래프 기능으로 볼 수 있다.

Git이 Push 한 Perforce 리비전 결과 그래프.
Figure 147. Git이 Push 한 Perforce 리비전 결과 그래프.

Perforce의 이런 히스토리 뷰어를 본 적이 없다면 다소 혼란스럽겠지만 Git 히스토리를 보는 것과 크게 다르지 않다. 그림은 README 파일의 히스토리를 보는 상황이다. 왼쪽 위 창에서 README 파일과 관련된 브랜치와 디렉토리가 나타난다. 오른쪽 위 창에서는 파일의 리비전 히스토리 그래프를 볼 수 있다. 오른쪽 아래 창에서는 이 그래프의 큰 그림을 확인할 수 있다. 왼쪽 아래 창에는 선택한 리비전을 자세히 보여준다(이 그림에서는 리비전 `2`다)

Perforce의 히스토리 그래프상으로는 Git의 히스토리와 똑 같아 보인다. 하지만, Perforce는 12 커밋을 저장할 만한 브랜치가 없다. 그래서 .git-fusion 디렉토리 안에 “익명” 브랜치를 만든다. Git 브랜치가 Perforce의 브랜치와 매치되지 않은 경우에도 이와 같은 모양이 된다(브랜치간 매핑은 나중에 설정할 수도 있다).

이런 작업들은 Git Fusion 내부에서 보이지 않게 처리된다. 물론 이 결과로 Git 클라이언트로 Perforce 서버에 접근하는 사람이 있다는 것을 누군가는 알게 된다.

Git-Fusion 요약

Perforce 서버에 권한이 있다면 Git Fusion은 Git과 Perforce 서버간에 데이터를 주고받는 도구로 매우 유용하다. 물론 좀 설정해야 하는 부분도 있지만 익히는게 그리 어렵지는 않다. 이 절에서는 Git을 조심해서 사용하라고 말하지 않는다. 이 절은 그런 절이다. 그렇다고 Perforce 서버가 아무거나 다 받아 주지 않는다. 이미 Push 한 히스토리를 재작성하고 Push 하면 Git Fusion이 거절한다. 이런 경우에도 Git Fusion은 열심히 노력해서 Perforce를 마치 Git 처럼 다룰 수 있게 도와준다. (Perforce 사용자에게는 생소하겠지만) Git 서브모듈도 사용할 수 있고 브랜치(Perforce 쪽에는 Integration으로 기록된다)를 Merge 할 수도 있다.

서버 관리 권한이 없으면 Git Fusion을 쓸 수 없지만 아직 다른 방법이 남아 있다.

Git-p4

Git-p4도 Git과 Perforce간의 양방향 Bridge이다. Git-p4는 모든 작업이 클라이언트인 Git 저장소 쪽에서 이루어지기 때문에 Perforce 서버에 대한 권한이 없어도 된다. 물론, 인증 정보 정도는 Perforce 서버가 필요하다. Git-p4는 Git Fusion만큼 완성도 높고 유연하지 않지만 Perforce 서버를 건드리지 않고서도 대부분은 다 할 수 있게 해준다.

Note

git-p4가 잘 동작하려면 p4 명령을 어디에서나 사용할 수 있게 PATH 에 등록해두어야 한다. `p4`는 무료로 http://www.perforce.com/downloads/Perforce/20-User 에서 다운로드 받을 수 있다.

설정

예제로 사용할 Perforce 프로젝트를 가져오기 위해 앞에서 살펴본 Git Fusion OVA 이미지의 Perforce 서버를 사용한다. Git Fusion 서버 설정은 건너뛰고 Perforce 서버와 저장소 설정 부분만 설정하면 된다.

git-p4이 의존하는 p4 클라이언트를 커맨드라인에서 사용하기 위해 몇 가지 환경변수를 먼저 설정해야 한다.

$ export P4PORT=10.0.1.254:1666
$ export P4USER=john
시작하기

Git에서 모든 시작은 Clone 이다. Clone을 먼저 한다.

$ git p4 clone //depot/www/live www-shallow
Importing from //depot/www/live into www-shallow
Initialized empty Git repository in /private/tmp/www-shallow/.git/
Doing initial import of //depot/www/live/ from revision #head into refs/remotes/p4/master

Git의 언어로 표현하자면 위의 명령은 “shallow” Clone을 한다. 모든 저장소의 히스토리를 가져오지 않고 마지막 리비전의 히스토리만 가져온다. 이 점을 기억해야 한다. Perforce는 저장소의 모든 히스토리를 모든 사용자에게 허용하지 않도록 설계됐다. 마지막 리비전만을 가져와도 Git은 충분히 Perforce 클라이언트로 사용할 수 있다. 물론 전체 히스토리를 봐야하는 의도라면 충분하지 않다.

이렇게 Clone 하고 나면 Git 기능을 활용할 수 있는 Git 저장소 하나가 만들어진다.

$ cd myproject
$ git log --oneline --all --graph --decorate
* 70eaf78 (HEAD, p4/master, p4/HEAD, master) Initial import of //depot/www/live/ from the state at revision #head

(역주 - 코드 틀린듯)

Perforce 서버를 가리키는 “p4” 리모트가 어떻게 동작하는지 모르지만 Clone은 잘된다. 사실 리모트도 실제하지 않는다.

$ git remote -v

확인해보면 리모트가 전혀 없다. git-p4는 리모트 서버의 상태를 보여주기 위해 몇 가지 Ref를 만든다. 이 Ref는 git log 에서는 리모트인 것처럼 보이지만 사실 Git이 관리하는 리모트가 아니라서 Push 할 수 없다.

워크플로

준비를 마쳤으니 또 수정하고 커밋하고 Push 해보자. 어떤 중요한 작업을 마치고 팀 동료들에게 공유하려는 상황을 살펴보자.

$ git log --oneline --all --graph --decorate
* 018467c (HEAD, master) Change page title
* c0fb617 Update link
* 70eaf78 (p4/master, p4/HEAD) Initial import of //depot/www/live/ from the state at revision #head

커밋을 두 개 생성했고 Perforce 서버로 전송할 준비가 됐다. Push 하기 전에 다른 동료가 수정한 사항이 있는지 확인한다.

$ git p4 sync
git p4 sync
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12142 (100%)
$ git log --oneline --all --graph --decorate
* 75cd059 (p4/master, p4/HEAD) Update copyright
| * 018467c (HEAD, master) Change page title
| * c0fb617 Update link
|/
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

팀 동료가 수정한 내용이 추가되어 master 브랜치와 p4/master 브랜치가 갈라지게 되었다. Perforce의 브랜치 관리 방식은 Git과 달라서 Merge 커밋을 서버로 전송하면 안된다. 대신 git-p4는 아래와 같은 명령으로 커밋을 Rebase 하기를 권장한다.

$ git p4 rebase
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
No changes to import!
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
Applying: Update link
Applying: Change page title
 index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

실행 결과를 보면 단순히 git p4 rebasegit rebase p4/master 하고 git p4 sync 명령을 실행한 것 처럼 보일 수 있다. 브랜치가 여러개인 상황에서 훨씬 효과를 보이지만 이렇게 생각해도 괜찮다.

이제 커밋 히스토리가 일직선이 됐고 Perforce 서버로 공유할 준비를 마쳤다. git p4 submit 명령은 p4/mastermaster 사이에 있는 모든 커밋에 대해 새 Perforce 리비전을 생성한다. 명령을 실행하면 주로 쓰는 편집기가 뜨고 아래와 같은 내용으로 채워진다.

# A Perforce Change Specification.
#
#  Change:      The change number. 'new' on a new changelist.
#  Date:        The date this specification was last modified.
#  Client:      The client on which the changelist was created.  Read-only.
#  User:        The user who created the changelist.
#  Status:      Either 'pending' or 'submitted'. Read-only.
#  Type:        Either 'public' or 'restricted'. Default is 'public'.
#  Description: Comments about the changelist.  Required.
#  Jobs:        What opened jobs are to be closed by this changelist.
#               You may delete jobs from this list.  (New changelists only.)
#  Files:       What opened files from the default changelist are to be added
#               to this changelist.  You may delete files from this list.
#               (New changelists only.)

Change:  new

Client:  john_bens-mbp_8487

User: john

Status:  new

Description:
   Update link

Files:
   //depot/www/live/index.html   # edit


######## git author ben@straub.cc does not match your p4 account.
######## Use option --preserve-user to modify authorship.
######## Variable git-p4.skipUserNameCheck hides this message.
######## everything below this line is just the diff #######
--- //depot/www/live/index.html  2014-08-31 18:26:05.000000000 0000
+++ /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/index.html   2014-08-31 18:26:05.000000000 0000
@@ -60,7 +60,7 @@
 </td>
 <td valign=top>
 Source and documentation for
-<a href="http://www.perforce.com/jam/jam.html">
+<a href="jam.html">
 Jam/MR</a>,
 a software build tool.
 </td>

이 내용은 p4 submit 을 실행했을 때 보이는 내용과 같다. 다만 git-p4는 아래쪽에 도움이 될 만한 내용을 덧 붙여 준다. git-p4는 커밋이나 Changeset을 생성할 때 최대한 Git과 Perforce에 있는 정보를 이용한다. 하지만 경우에 따라 변환할 때 직접 입력해줘야 할 수도 있다. 보내려고 하는 커밋의 저자가 Perforce에 계정이 없을 때도 그 저자가 작성한 Changeset으로 기록되길 바랄 것이다.

git-p4가 Git 커밋의 내용을 바탕으로 Perforce Changeset의 메시지를 생성하기 때문에 보통 그냥 내용을 저장하고 편집기를 종료하면 된다. 커밋이 두 개 있으므로 저장하고 종료하기를 두 번 한다. 어쨌든간에 git p4 submit 의 실행한 결과는 아래와 같다.

$ git p4 submit
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Synchronizing p4 checkout...
... - file(s) up-to-date.
Applying dbac45b Update link
//depot/www/live/index.html#4 - opened for edit
Change 12143 created with 1 open file(s).
Submitting change 12143.
Locking 1 files ...
edit //depot/www/live/index.html#5
Change 12143 submitted.
Applying 905ec6a Change page title
//depot/www/live/index.html#5 - opened for edit
Change 12144 created with 1 open file(s).
Submitting change 12144.
Locking 1 files ...
edit //depot/www/live/index.html#6
Change 12144 submitted.
All commits applied!
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12144 (100%)
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
$ git log --oneline --all --graph --decorate
* 775a46f (HEAD, p4/master, p4/HEAD, master) Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

git p4 submit 에 가까운 Git의 명령은 push 이며 위의 결과를 보면 git push 명령을 실행한 것과 비슷하다.

Git 커밋이 Perforce의 Changeset으로 변환되는 과정을 자세히 살펴보자. Git 커밋 여러개를 하나의 Perforce Changeset으로 만들려면 git p4 submit 명령을 실행하기 전에 Rebase로 커밋을 하나로 합치면 된다. 서버로 보낸 커밋의 SHA-1 해시를 보면 그 값이 바뀐다. git-p4이 Changeset으로 변환할 때 각 커밋 메시지의 마지막에 아래와 같이 한 라인을 추가해서 달라진다.

$ git log -1
commit 775a46f630d8b46535fc9983cf3ebe6b9aa53145
Author: John Doe <john@example.com>
Date:   Sun Aug 31 10:31:44 2014 -0800

    Change page title

    [git-p4: depot-paths = "//depot/www/live/": change = 12144]

Merge 커밋을 서버로 전송하면 어떤 일이 일어나는지 살펴보자. 아래와 같은 커밋 히스토리가 현재 처한 상황이라고 생각해보자.

$ git log --oneline --all --graph --decorate
* 3be6fd8 (HEAD, master) Correct email address
*   1dcbf21 Merge remote-tracking branch 'p4/master'
|\
| * c4689fc (p4/master, p4/HEAD) Grammar fix
* | cbacd0a Table borders: yes please
* | b4959b6 Trademark
|/
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

775a46f 커밋 이후에 Git과 Perforce 히스토리가 갈라졌다. Git으로 작업한 쪽에는 커밋이 두 개, Perforce 쪽에는 커밋 하나가 추가됐고 Merge 하고 서도 커밋이 추가됐다. 여기서 서버로 보내면 Perforce 쪽 Changeset 위에 쌓인다. 바로 Perforce 서버로 히스토리를 보내 보자.

$ git p4 submit -n
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would synchronize p4 checkout in /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would apply
  b4959b6 Trademark
  cbacd0a Table borders: yes please
  3be6fd8 Correct email address

-n 옵션은 --dry-run 의 단축 옵션으로 명령일 실제로 실행하기 전에 어떻게 동작하는 지 미리 확인해 볼 수 있다. 결과를 보면 로컬에만 있는 커밋 3개가 Perforce Changeset으로 잘 만들어지는 것으로 보인다. 확실히 이 결과는 우리가 원하던 바이다. 실제로 실행하자.

$ git p4 submit
[…]
$ git log --oneline --all --graph --decorate
* dadbd89 (HEAD, p4/master, p4/HEAD, master) Correct email address
* 1b79a80 Table borders: yes please
* 0097235 Trademark
* c4689fc Grammar fix
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

Rebase 하고 나서 전송한 것처럼 히스토리가 일직선이 됐다. 이 결과는 Git으로 자유롭게 브랜치를 만들고 버리고 Merge 해도 된다는 것을 말해준다. 히스토리가 Perforce에 맞지 않더라도 걱정할 필요 없다. 물론 직접 Rebase 해서 Perforce 서버로 전송해도 된다.

브랜치

Perforce 프로젝트에 브랜치가 많아도 괜찮다. git-p4은 Perforce 브랜치를 Git 브랜치로 생각하게 끔 만들어 준다. Perforce Depot이 아래와 같다고 하자.

//depot
  └── project
      ├── main
      └── dev

dev 브랜치가 아래와 같은 View Spec을 갖고 있다면,

//depot/project/main/... //depot/project/dev/...

아래와 같이 git-p4는 자동으로 브랜치 정보를 찾아서 잘 처리한다.

$ git p4 clone --detect-branches //depot/project@all
Importing from //depot/project@all into project
Initialized empty Git repository in /private/tmp/project/.git/
Importing revision 20 (50%)
    Importing new branch project/dev

    Resuming with change 20
Importing revision 22 (100%)
Updated branches: main dev
$ cd project; git log --oneline --all --graph --decorate
* eae77ae (HEAD, p4/master, p4/HEAD, master) main
| * 10d55fb (p4/project/dev) dev
| * a43cfae Populate //depot/project/main/... //depot/project/dev/....
|/
* 2b83451 Project init

Depot 경로에 “@all” 이라고 지정해주면 git-p4는 마지막 Changeset만을 가져오는 것이 아니라 지정한 경로의 모든 Changeset을 가져온다. Git의 Clone과 비슷하다. 프로젝트 히스토리가 길면 Clone 하는데 오래 걸린다.

--detect-branches 옵션을 주면 git-p4는 Perforce의 브랜치를 Git의 브랜치로 매핑해 준다. 매핑 정보를 Perforce 서버에 두는 것이 Perforce 다운 방식이지만 git-p4에 직접 알려줄 수도 있다. 브랜치 매핑 정보를 git-p4에 전달해서 위의 결과와 똑 같이 매핑시킬 수 있다.

$ git init project
Initialized empty Git repository in /tmp/project/.git/
$ cd project
$ git config git-p4.branchList main:dev
$ git clone --detect-branches //depot/project@all .

git-p4.branchList 설정에 main:dev 값을 저장해두면 git-p4는 “main” 과 “dev” 가 브랜치 이름이고 후자는 전자에서 갈라져나온 것이라 파악한다.

이제 git checkout -b dev p4/project/dev 하고 커밋을 쌓으면, git p4 submit 명령을 실행할 때 git-p4가 똘똘하게 알아서 브랜치를 잘 찾아 준다. 안타깝게도 마지막 리비전만 받아 오는 Shallow Clone을 해야 하는 상황에서는 동시에 브랜치를 여러개 쓸 수 없다. 엄청나게 큰 Perforce이고 여러 브랜치를 오가며 작업해야 한다면 브랜치 별로 git p4 clone 을 따로 하는 수 밖에 없다.

Perforce의 브랜치를 생성하거나 브랜치끼리 합치려면 Perforce 클라이언트가 반드시 필요하다. git-p4는 이미 존재하는 브랜치로부터 Changeset을 가져오거나 커밋을 보내는 일만 할 수 있다. 일직선 형태의 Changeset 히스토리만을 유지할 수 있다. 브랜치를 Git에서 Merge 하고 Perforce 서버로 보내면 단순히 파일 변화만 기록된다. 어떤 브랜치를 Merge 했는 지와 같은 메터데이터는 기록되지 않는다.

Git-Perforce 함께쓰기 요약

git-p4 Perforce 서버를 쓰는 환경에서도 Git으로 일할 수 있게 해준다. 하지만 프로젝트를 관리하는 주체는 Perforce이고 Git은 로컬에서만 사용한다는 점을 기억해야 한다. 따라서 Git 커밋을 Perforce 서버로 보내서 공유할 때는 항상 주의깊게 작업해야 한다. 한 번 Perforce 서버로 보낸 커밋은 다시 보내서는 안된다.

Perforce와 Git 클라이언트를 제약없이 사용하고 싶다면 서버 관리 권한이 필요하다. Git Fusion은 Git을 매우 우아한 Perforce 클라이언트로 만들어 준다.

Git과 TFS

Git은 점점 Windows 개발자들도 많이 사용한다. Windows에서 개발한다면 Microsoft의 Team Foundation Server(TFS)를 쓸 가능성이 높다. TFS는 결함과 작업 항목 추적하고, 스크럼 등의 개발방법 지원하고, 코드 리뷰와 버전 컨트롤 등의 기능을 모아놓은 협업 도구다. 처음에는 TFSTFVS(Team Foundation Version Control)*를 혼동하기 쉽다. TFVC는 Git 같은 Microsoft의 VCS이고 TFSGit이나 *TFVS 같은 VCS을 사용하는 다기능 서버다. “TFS” 의 VCS로 대부분은 TFVC를 사용한다. 하지만 2013년부터의 신상은 Git도 지원한다.

이 절은 Git을 쓰고 싶지만 TFVC를 사용하는 팀에 합류한 사람을 위해 준비했다.

git-tf와 git-tfs

TFS용 도구는 git-tf와 git-tfs으로 두 개가 존재한다.

git-tfs는 .NET 프로젝트이고 https://github.com/git-tfs/git-tfs에 있다. (이 글을 쓰는 현재) Windows에서만 동작한다. libgit2의 .NET 바인딩을 사용해서 Git 저장소를 읽고 쓴다. libgit2는 빠르고 확장하기 쉬운 Git을 라이브러리다. libgit2는 Git을 완전히 구현하지는 않았다. 하지만 이로 인한 제약은 없다. libgit2가 부족한 부분은 Git 명령어를 이용한다. 서버와 통신하는 데 Visual Studio 어셈블리를 이용하기에 TFVC를 매우 잘 지원한다.

이 말인 즉 TFVC에 접근하려면 Visual Studio가 설치돼 있어야 한다. 2010 이상의 버전이나 Express 2012 이상의 버전, Visual Studio SDK를 사용해야 한다.

git-tf는 Java 프로젝트다(홈페이지는 https://gittf.codeplex.com). JRE가 있는 컴퓨터면 어디서든 동작한다. Git 저장소와는 JGit(Git의 JVM 구현체)으로 통신한다. 즉, Git의 기능을 사용하는데 있어서 아무런 제약이 없다. 하지만 TFVC 지원은 git-tfs에 비해 부족하다. git-tf로는 브랜치를 사용할 수 없다.

이렇게 각각 장단점이 있고, 상황에 따라 유불리가 다르다. 이 책에서는 두 도구의 기본적인 사용법을 설명한다.

Note

아래 지시사항을 따라 하려면 접근 가능한 TFVC 저장소가 하나 필요하다. TFVC는 Git이나 Subversion처럼 공개된 저장소가 많지 않다. 사용할 저장소를 직접 하나 만들어야 한다. Codeplex(https://www.codeplex.com)나 Visual Studio 온라인 (http://www.visualstudio.com)을 추천한다.

시작하기: git-tf

먼저 해야 할 것은 여느 Git 프로젝트에서 했던 것처럼 Clone 이다. git-tf 에서는 아래과 같이 한다.

$ git tf clone https://tfs.codeplex.com:443/tfs/TFS13 $/myproject/Main project_git

첫 번째 인자는 TFVC 콜렉션의 URL이다. 두 번째 인자는 $/project/branch 형식의 문자열이고 세 번째는 Clone 해서 생성하는 로컬 Git 저장소의 경로이다. 마지막 인자는 선택 사항이다. git-tf는 한 번에 브랜치 하나만 다룰 수 있다. 만약 TFVC의 다른 브랜치에 체크인하려면 그 브랜치를 새로 Clone 해야 한다.

이렇게 Clone 한 저장소는 완전한 Git 저장소다.

$ cd project_git
$ git log --all --oneline --decorate
512e75a (HEAD, tag: TFS_C35190, origin_tfs/tfs, master) Checkin message

마지막 Changeset만 내려 받았다. 이것을 Shallow Clone 이라고 한다. TFVC는 클라이언트가 히스토리 전체를 가지지 않는다. git-tf는 기본적으로 마지막 버전만 가져온다. 대신 속도는 빠르다.

여유가 있으면 --deep 옵션으로 프로젝트의 히스토리를 전부 Clone 하자. 이렇게 하는 편이 낫다.

$ git tf clone https://tfs.codeplex.com:443/tfs/TFS13 $/myproject/Main \
  project_git --deep
Username: domain\user
Password:
Connecting to TFS...
Cloning $/myproject into /tmp/project_git: 100%, done.
Cloned 4 changesets. Cloned last changeset 35190 as d44b17a
$ cd project_git
$ git log --all --oneline --decorate
d44b17a (HEAD, tag: TFS_C35190, origin_tfs/tfs, master) Goodbye
126aa7b (tag: TFS_C35189)
8f77431 (tag: TFS_C35178) FIRST
0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
        Team Project Creation Wizard

TFS_C35189 태그를 보자. 어떤 Git 커밋이 어떤 TFVC의 Changeset과 연결되는지 보여준다. 로그 명령어로 간단하게 어떤 TFVC의 스냅샷과 연결되는지 알 수 있다. 이 기능은 필수가 아니고 git config git-tf.tag false 명령어로 끌 수 있다. git-tf는 커밋-Changeset 매핑 정보를 .git/git-tf 에 보관한다.

git-tfs 시작하기

git-tfs의 Clone은 좀 다르게 동작한다. 아래를 보자.

PS> git tfs clone --with-branches \
    https://username.visualstudio.com/DefaultCollection \
    $/project/Trunk project_git
Initialized empty Git repository in C:/Users/ben/project_git/.git/
C15 = b75da1aba1ffb359d00e85c52acb261e4586b0c9
C16 = c403405f4989d73a2c3c119e79021cb2104ce44a
Tfs branches found:
- $/tfvc-test/featureA
The name of the local branch will be : featureA
C17 = d202b53f67bde32171d5078968c644e562f1c439
C18 = 44cd729d8df868a8be20438fdeeefb961958b674

git-tfs에는 --with-branches 옵션이 있다. TFVC 브랜치와 Git 브랜치를 매핑하는 플래그다. 그래서 모든 TFVC 브랜치를 로컬 저장소의 Git 브랜치로 만들 수 있다. TFS에서 브랜치를 사용하거나 Merge 한 적이 있다면 git-tfs를 추천한다. TFS 2010 이전 버전에서는 이 기능이 동작하지 않는다. 이전 버전에서는 “브랜치” 는 그냥 폴더일 뿐이었다. git-tfs는 일반 폴더를 브랜치로 만들지 못한다.

그렇게 만들어진 Git 저장소를 살펴보자.

PS> git log --oneline --graph --decorate --all
* 44cd729 (tfs/featureA, featureA) Goodbye
* d202b53 Branched from $/tfvc-test/Trunk
* c403405 (HEAD, tfs/default, master) Hello
* b75da1a New project
PS> git log -1
commit c403405f4989d73a2c3c119e79021cb2104ce44a
Author: Ben Straub <ben@straub.cc>
Date:   Fri Aug 1 03:41:59 2014 +0000

    Hello

    git-tfs-id: [https://username.visualstudio.com/DefaultCollection]$/myproject/Trunk;C16

보면 로컬 브랜치가 두 개다. masterfeatureA 가 있는데 TFVC의 Trunk 와 그 자식 브랜치 featureA 에 해당된다. 그리고 TFS 서버를 나타내는 tfs “리모트” 에는 TFS의 브랜치인 defaultfeatureA 가 있다. git-tfs는 Clone 한 브랜치를 tfs/default 라는 이름으로 매핑하고 다른 브랜치는 원래 이름을 그대로 부여한다.

위 커밋 메시지에서 `git-tfs-id:`가 쓰인 라인도 볼 필요가 있다. git-tfs에서는 태그 대신에 TFVC Changeset과 Git 커밋의 관계를 이렇게 표시한다. TFVC에 Push 하면 이 표시가 변경되고 Git 커밋의 SHA-1 해시값도 바뀐다.

Git-tf[s] 워크플로

Note

어떤 도구를 사용하든지 아래와 같이 Git 설정 두 개를 바꿔야 문제가 안 생긴다.

$ git config set --local core.ignorecase=true
$ git config set --local core.autocrlf=false

다음으로 할 일은 실제로 프로젝트를 진행하는 것이다. TFVC와 TFS의 기능 중에서 워크플로를 복잡하게 만드는 게 있다.

  1. TFVC에 표시되지 않는 Feature 브랜치는 복잡성을 높인다. 이점이 TFVC와 Git이 매우 다른 방식으로 브랜치를 표현하게 만든다.

  2. TFVC는 사용자가 서버에서 파일을 “Checkout” 받아서 아무도 수정하지 못하도록 잠글 수 있다는 것을 명심해야 한다. 서버에서 파일을 잠갔더라도 파일을 수정할 수 있다. 하지만 TFVC 서버로 Push 할 때 방해될 수 있다.

  3. TFS는 “Gated” 체크인이라는 기능이 있다. TFS의 빌드-테스트 사이클을 성공해야만 체크인이 허용된다. 이 기능은 TFVC의 “Shelve” 라는 기능으로 구현됐다. 이 기능은 여기서 다루지 않는다. git-tf으로는 수동으로 맞춰 줘야 하고, git-tfs는 Gated 체크인을 인식하는 checkintool 명령어를 제공한다.

여기서는 잘되는 시나리오만 보여준다. 돌 다리를 두두리는 방법은 다루지 않는다. 간결함을 위해서다.

워크플로: git-tf

어떤 일을 마치고 Git으로 master 에 커밋을 두 개 생성했다. 그리고 이 커밋을 TFVC 서버로 올려 팀원들과 공유하고자 한다. 이때 Git 저장소는 상태는 아래와 같다.

$ git log --oneline --graph --decorate --all
* 4178a82 (HEAD, master) update code
* 9df2ae3 update readme
* d44b17a (tag: TFS_C35190, origin_tfs/tfs) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

4178a82 커밋을 TFVC 서버에 Push 하기 전에 할 일이 있다. 내가 작업하는 동안 다른 팀원이 한 일이 있는지 확인해야 한다.

$ git tf fetch
Username: domain\user
Password:
Connecting to TFS...
Fetching $/myproject at latest changeset: 100%, done.
Downloaded changeset 35320 as commit 8ef06a8. Updated FETCH_HEAD.
$ git log --oneline --graph --decorate --all
* 8ef06a8 (tag: TFS_C35320, origin_tfs/tfs) just some text
| * 4178a82 (HEAD, master) update code
| * 9df2ae3 update readme
|/
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

작업한 사람이 있다. 그래서 히스토리가 갈라진다. 이제 Git 타임이다. 어떻게 일을 진행할지 두 가지 선택지가 있다.

  1. 평범하게 Merge 커밋을 만든다. 여기까지가 git pull 이 하는 일이다. git-tf에서는 git tf pull 명령로 한다. 하지만 주의사항이 있다. TFVC는 이런 방법을 이해하지 못한다. Merge 커밋을 Push 하면 서버와 클라이언트의 히스토리가 달라진다. 좀 혼란스럽다. 모든 변경 사항을 Changeset 하나로 합쳐서 올리려고 한다면 이 방법이 제일 쉽다.

  2. Rebase로 히스토리를 평평하게 편다. 이렇게 하면 Git 커밋 하나를 TFVC Changeset 하나로 변환할 수 있다. 가능성을 열어 둔다는 점에서 이 방법을 추천한다. git-tf에는 심지어 git tf pull --rebase 명령이 있어서 쉽게 할 수 있다.

선택은 자신의 몫이다. 이 예제에서는 Rebase 한다.

$ git rebase FETCH_HEAD
First, rewinding head to replay your work on top of it...
Applying: update readme
Applying: update code
$ git log --oneline --graph --decorate --all
* 5a0e25e (HEAD, master) update code
* 6eb3eb5 update readme
* 8ef06a8 (tag: TFS_C35320, origin_tfs/tfs) just some text
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

이제 TFVC에 체크인할 준비가 끝났다. 모든 커밋을 하나의 Changeset으로 만들지(--shallow 옵션. 기본값이다), 커밋을 각각의 Changeset으로 만들지(--deep 옵션) 선택할 수 있다. 이 예제는 Changeset 하나로 만드는 방법을 사용한다.

$ git tf checkin -m 'Updating readme and code'
Username: domain\user
Password:
Connecting to TFS...
Checking in to $/myproject: 100%, done.
Checked commit 5a0e25e in as changeset 35348
$ git log --oneline --graph --decorate --all
* 5a0e25e (HEAD, tag: TFS_C35348, origin_tfs/tfs, master) update code
* 6eb3eb5 update readme
* 8ef06a8 (tag: TFS_C35320) just some text
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

TFS_C35348 태그가 새로 생겼다. 이 태그는 5a0e25e 커밋과 완전히 똑같은 TFVC 스냅샷을 가리킨다. 모든 Git 커밋이 TFVC 스냅샷에 대응할 필요는 없다. 예를 들어서 6eb3eb5 커밋은 TFVC 서버에는 존재하지 않는다.

이것이 주 워크플로다. 아래의 고려사항은 가슴속에 새겨야 한다.

  • 브랜치가 없다. Git-tf는 TFVC 브랜치 하나로만 Git 저장소를 만들어 준다.

  • TFVC나 Git으로 협업이 가능하지만 그 둘을 동시에 사용할 수는 없다. git-tf로 TFVC 저장소의 Clone 할 때마다 SHA-1 해시를 새로 생성한다. SHA-1가 다르기 때문에 두고두고 골치가 아프게 된다.

  • 협업은 Git으로 하고 TFVC와는 주기적으로 동기화만 하고 싶다면 TFVC와 통신하는 Git 저장소를 딱 하나만 둬라.

워크플로: git-tfs

같은 시나리오를 git-tfs로도 살펴보자. Git 저장소에 master 브랜치에 커밋을 새로 했다.

PS> git log --oneline --graph --all --decorate
* c3bd3ae (HEAD, master) update code
* d85e5a2 update readme
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 (tfs/default) Hello
* b75da1a New project

내가 일하는 동안에 누군가 한 일이 있는지 살펴보자.

PS> git tfs fetch
C19 = aea74a0313de0a391940c999e51c5c15c381d91d
PS> git log --all --oneline --graph --decorate
* aea74a0 (tfs/default) update documentation
| * c3bd3ae (HEAD, master) update code
| * d85e5a2 update readme
|/
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

다른 동료가 새로운 Changeset을 만들었다. Git에서 aea74a0 으로 보인다. 그리고 리모트 브랜치 tfs/default 가 전진했다.

git-tf로 한 것처럼 두 가지 방식이 있다.

  1. Rebase를 해서 히스토리를 평평하게 한다.

  2. Merge를 해서 Merge 한 사실까지 남긴다.

이번에는 Git 커밋을 하나씩 TFVC의 Changeset으로 만드는 “Deep” 체크인을 해보자. 먼저 Rebase 한다.

PS> git rebase tfs/default
First, rewinding head to replay your work on top of it...
Applying: update readme
Applying: update code
PS> git log --all --oneline --graph --decorate
* 10a75ac (HEAD, master) update code
* 5cec4ab update readme
* aea74a0 (tfs/default) update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

이제 TFVC 서버에 체크인만 하면 된다. rcheckin 명령으로 Git 커밋을 하나씩 TFVC Changeset으로 만든다. HEAD부터 tfs 리모트 브랜치 사이의 모든 Git commit을 TFVC Changeset으로 만든다. (checkin 명령은 git squash 명령처럼 Git 커밋을 합쳐서 Changeset 하나로 만든다.)

PS> git tfs rcheckin
Working with tfs remote: default
Fetching changes from TFS to minimize possibility of late conflict...
Starting checkin of 5cec4ab4 'update readme'
 add README.md
C20 = 71a5ddce274c19f8fdc322b4f165d93d89121017
Done with 5cec4ab4b213c354341f66c80cd650ab98dcf1ed, rebasing tail onto new TFS-commit...
Rebase done successfully.
Starting checkin of b1bf0f99 'update code'
 edit .git\tfs\default\workspace\ConsoleApplication1/ConsoleApplication1/Program.cs
C21 = ff04e7c35dfbe6a8f94e782bf5e0031cee8d103b
Done with b1bf0f9977b2d48bad611ed4a03d3738df05ea5d, rebasing tail onto new TFS-commit...
Rebase done successfully.
No more to rcheckin.
PS> git log --all --oneline --graph --decorate
* ff04e7c (HEAD, tfs/default, master) update code
* 71a5ddc update readme
* aea74a0 update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

체크인을 완료하고 나서 git-tfs가 Rebase 하는 것에 주목하자. 커밋 메시지의 제일 하단에 git-tfs-id 필드를 추가해야 하기 때문이고 커밋의 SHA-1 해시값이 바뀐다. 이는 의도된 동작이니 걱정할 필요 없다. 그냥 알고 있으면 된다. 특히 Git 커밋을 다른 사람과 공유할 때 이런 특징을 고려해야 한다.

TFS는 버전 관리 시스템과 많은 기능을 통합했다. 작업 항목이나, 리뷰어 지정, 게이트 체크인 등의 기능을 지원한다. 이 많은 기능을 커맨드 라인 도구로만 다루는 건 좀 성가시다. 다행히 git-tfs는 쉬운 GUI 체크인 도구를 실행해준다.

PS> git tfs checkintool
PS> git tfs ct

실행하면 이렇게 생겼다.

git-tfs 체크인 도구.
Figure 148. git-tfs 체크인 도구.

Visual Studio에서 실행하는 다이얼로그와 똑같아서 TFS 사용자에게는 친숙하다.

git-tfs는 Git 저장소에서 TFVC 브랜치를 관리할 수 있다. 아래 예처럼 직접 하나 만들어보자.

PS> git tfs branch $/tfvc-test/featureBee
The name of the local branch will be : featureBee
C26 = 1d54865c397608c004a2cadce7296f5edc22a7e5
PS> git log --oneline --graph --decorate --all
* 1d54865 (tfs/featureBee) Creation branch $/myproject/featureBee
* ff04e7c (HEAD, tfs/default, master) update code
* 71a5ddc update readme
* aea74a0 update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

TFVC에 브랜치를 만들면 현재 있는 브랜치에 Changeset이 하나 추가된다. 이 Changeset은 Git의 커밋으로 표현된다. *git-tfs*는 tfs/featureBee 라는 리모트 브랜치를 만들었지만, HEAD 는 여전히 master 를 가리킨다. 방금 만든 브랜치에서 작업을 하려면 새로 만든 커밋 `1d54865`에서 시작하면 된다. 이 커밋부터 새로운 토픽 브랜치가 만들어진다.

Git과 TFS 요약

Git-tf와 Git-tfs는 둘 다 TFVC 서버랑 잘 맞는 멋진 도구다. 중앙 TFVC 서버에 자주 접근하지 않으면서 Git의 장점을 그대로 활용할 수 있다. 또 다른 팀원들이 Git을 사용하지 않더라도 개발자로 사는 삶이 풍요로워진다. Windows에서 작업을 한다면(팀이 TFS를 쓴다면) TFS의 기능을 더 많이 지원하는 Git-tfs를 추천한다. Git-ft는 다른 플랫폼을 사용할 때 추천한다. 이 장에서 소개한 다른 도구들처럼 대표 버전 관리 시스템은 단 하나만 선택해야 한다. Git이든 TFVC이든 중심 도구는 하나다. 둘 다 중심이 될 수 없다.