posted by REDFORCE 2018. 5. 16. 23:37

어지간하면 깃-클라이언트를 이용하기 때문에 쓸일이 없었으나

이따금씩 에러때문에 커맨드를 써야 할 경우가 있었다.


덕분에 Git command를 쓰려고 했는데, 명령어 자체를 까먹어서 적어둔다.


github에 repository를 먼저 생성

(remote repo에는 .gitignore와 LICENSE가 commit되어있는 상태)


git init

git remote add origin https://github.com/ssaemo/asyncio-irc-client.git

git pull origin master # origin으로부터 local master로 가져옴

git add .

git commit -m "first commit"

git push origin master # origin으로 local master commit을 push

posted by REDFORCE 2017. 4. 13. 08:19

출처 : git-scm.com


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 저장소를 복사한다. 테스트용 저장소가 필요해서 Google Code에 새로 Subversion 저장소를 하나 만들었다. `protobuf 라는 프로젝트의 일부 코드를 복사했다. `protobuf`는 네트워크 전송에 필요한 구조화된 데이터(프로토콜 같은 것들)의 인코딩을 도와주는 도구이다.

로컬 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://progit-example.googlecode.com/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 init`과 `git svn fetch 명령을 순서대로 실행한 것과 같다. 이 명령은 시간이 좀 걸린다. 테스트용 프로젝트는 커밋이 75개 정도밖에 안되서 시간이 오래 걸리지 않는다. Git은 커밋을 한 번에 하나씩 일일이 기록해야 한다. 커밋이 수천개인 프로젝트라면 몇 시간 혹은 몇 일이 걸릴 수도 있다.

-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 [branchname] 명령을 사용한다.

   $ 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 클라이언트도 설치해야 한다. http://mercurial.selenic.com/ 사이트에서 다운로드 받아 설치할 수 있다.

이렇게 필요한 라이브러리와 프로그램을 설치하고 나면 준비가 끝난다. 이제 필요한 것은 소스코드를 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 89, _10_이 생성됐고 permanent 브랜치에 속한다. 하지만 예전에 Push 했던 Changeset들이 그대로 남아있다. 그러면 Mercurial 저장소를 공유하는 동료들은 혼란스럽다. 이딴식으로 커밋을 재작성 하고 강제로 Push 하지 말지어다.

Mercurial 요약

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

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-User 의 Git Fusion탭에서 받을 수 있다. VirtualBox 같은 가상화 소프트웨어로 이 이미지를 동작시킬 수 있다.

가상머신을 처음 부팅시키면 rootperforcegit 세 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://ben@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는 1`과 `2 커밋을 저장할 만한 브랜치가 없다. 그래서 .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 rebase`는 `git rebase p4/master 하고 git p4 sync 명령을 실행한 것 처럼 보일 수 있다. 브랜치가 여러개인 상황에서 훨씬 효과를 보이지만 이렇게 생각해도 괜찮다.

이제 커밋 히스토리가 일직선이 됐고 Perforce 서버로 공유할 준비를 마쳤다. git p4 submit 명령은 p4/master`와 `master 사이에 있는 모든 커밋에 대해 새 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는 결함과 작업 항목 추적하고, 스크럼 등의 개발방법 지원하고, 코드 리뷰와 버전 컨트롤 등의 기능을 모아놓은 협업 도구다. 처음에는 TFS와 TFVS(Team Foundation Version Control)*를 혼동하기 쉽다. TFVC는 Git 같은 Microsoft의 VCS이고 TFS는 Git이나 *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

보면 로컬 브랜치가 두 개다. master`와 `featureA`가 있는데 TFVC의 `Trunk`와 그 자식 브랜치 `featureA`에 해당된다. 그리고 TFS 서버를 나타내는 `tfs ‘`리모트’'에는 TFS의 브랜치인 `default`와 `featureA`가 있다. 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이든 중심 도구는 하나다. 둘 다 중심이 될 수 없다.


'Programming > Git' 카테고리의 다른 글

Git-Command init~Push (commit)  (0) 2018.05.16
Git_#8.4_Git맞춤 - 정책 구현하기  (0) 2017.04.13
Git_#8.3_Git맞춤 - Git Hooks  (0) 2017.04.13
Git_#8.2_Git맞춤 - Git Attributes  (0) 2017.04.13
Git_#8.1_Git맞춤 - Git 설정하기  (0) 2017.04.13
posted by REDFORCE 2017. 4. 13. 06:34

출처 : git-scm.com


8.4 Git맞춤 - 정책 구현하기

정책 구현하기

지금까지 배운 것을 한 번 적용해보자. 나름의 커밋 메시지 규칙으로 검사하고 Fast-forward Push 만 허용하고 디렉토리마다 사용자의 수정 권한을 제어하는 워크플로를 만든다. 실질적으로 정책을 강제하려면 서버 훅으로 만들어야 한다. 하지만, 개발자들이 Push 할 수 없는 커밋은 아예 만들지 않도록 클라이언트 훅도 만든다.

훅 스크립트는 Ruby 언어를 사용한다. 필자가 주로 사용하는 언어기도 하지만 코드가 쉬워서 직접 작성하는 것은 어렵더라도 코드를 읽고 개념을 이해할 수 있을 것이다. 물론 Git은 언어를 가리지 않는다. Git이 자동으로 생성해주는 예제는 모두 Perl과 Bash로 작성돼 있다. 예제를 열어 보면 Perl과 Bash로 작성된 예제를 참고 할 수 있다.

서버 훅

서버 정책은 전부 update 훅으로 만든다. 이 스크립트는 브랜치가 Push 될 때마다 한 번 실행되고 아래 내용을 아규먼트로 받는다.

  • 해당 브랜치의 이름

  • 원래 브랜치가 가리키던 Refs

  • 새로 Push 된 Refs

그리고 SSH를 통해서 Push 하는 것이라면 누가 Push 하는 지도 알 수 있다. SSH로 접근하긴 하지만 공개키를 이용하여 개발자 모두 계정 하나로(“git” 같은) Push 하고 있다면 실제로 Push 하는 사람이 누구인지 공개키를 비교하여 판별하고 환경변수를 설정해주는 스크립트가 필요하다. 아래 스크립트에서는 $USER환경 변수에 현재 접속한 사용자 정보가 있다고 가정하며 update 스크립트는 필요한 정보를 수집하는 것으로 시작한다.

   #!/usr/bin/env ruby

   $refname = ARGV[0]
   $oldrev  = ARGV[1]
   $newrev  = ARGV[2]
   $user    = ENV['USER']

   puts "Enforcing Policies..."
   puts "(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"

스크립트에서 전역변수를 쓰고 있지만 데모의 이해를 돕기 위해서니 너무 나무라지는 마시길 바란다.

커밋 메시지 규칙 만들기

커밋 메시지 규칙부터 해보자. 일단 목표가 있어야 하니까 커밋 메시지에 “ref: 1234” 같은 스트링이 포함돼 있어야 한다고 가정하자. 보통 커밋은 이슈 트래커에 있는 이슈와 관련돼 있으니 그 이슈가 뭔지 커밋 메시지에 적어 놓으면 좋다. Push 할 때마다 커밋 메시지에 해당 스트링이 포함돼 있는지 확인한다. 만약 커밋 메시지에 해당 스트링이 없는 커밋이면 0이 아닌 값을 반환해서 Push를 거절한다.

$newrev$oldrev 변수와 `git rev-list`라는 Plumbing 명령어를 이용해서 Push 하는 모든 커밋의 SHA-1 값을 알 수 있다. `git log`와 근본적으로 같은 명령이고 옵션을 하나도 주지 않으면 다른 정보 없이 SHA-1 값만 보여준다. 이 명령으로 두 커밋 사이에 있는 커밋들의 SHA-1 값을 살펴보고자 아래와 같은 명령을 사용할 수 있다.

   $ git rev-list 538c33..d14fc7
   d14fc7c847ab946ec39590d87783c69b031bdfb7
   9f585da4401b0a3999e84113824d15245c13f0be
   234071a1be950e2a8d078e6141f5cd20c1e61ad3
   dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
   17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475

이 SHA-1 값으로 각 커밋의 메시지도 가져온다. 커밋 메시지를 가져와서 정규표현 식으로 해당 패턴이 있는지 검사한다.

커밋 메시지를 얻는 방법을 알아보자. 커밋의 raw 데이터는 `git cat-file`이라는 Plumbing 명령어로 얻을 수 있다. Git의 내부 에서 Plumbing 명령어에 대해 자세히 다루니까 지금은 커밋 메시지 얻는 것에 집중하자.

   $ git cat-file commit ca82a6
   tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
   parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
   author Scott Chacon <schacon@gmail.com> 1205815931 -0700
   committer Scott Chacon <schacon@gmail.com> 1240030591 -0700

   changed the version number

이 명령이 출력하는 메시지에서 커밋 메시지만 잘라내야 한다. 첫 번째 빈 라인 다음부터가 커밋 메시지니까 유닉스 명령어 `sed`로 첫 빈 라인 이후를 잘라낸다.

   $ git cat-file commit ca82a6 | sed '1,/^$/d'
   changed the version number

이제 커밋 메시지에서 찾는 패턴과 일치하는 문자열이 있는지 검사해서 있으면 통과시키고 없으면 거절한다. 스크립트가 종료할 때 0이 아닌 값을 반환하면 Push가 거절된다. 이 일을 하는 코드는 아래와 같다.

   $regex = /\[ref: (\d+)\]/

   # enforced custom commit message format
   def check_message_format
     missed_revs = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
     missed_revs.each do |rev|
       message = `git cat-file commit #{rev} | sed '1,/^$/d'`
       if !$regex.match(message)
         puts "[POLICY] Your message is not formatted correctly"
         exit 1
       end
     end
   end
   check_message_format

이 코드를 update 스크립트로 넣으면 규칙을 어긴 커밋은 Push 할 수 없다.

ACL로 사용자마다 다른 규칙 적용하기

진행하는 프로젝트에 모듈이 여러 개라서 모듈마다 특정 사용자들만 Push 할 수 있게 ACL(Access Control List)을 설정해야 한다고 가정하자. 모든 권한을 다 가진 사람들도 있고 특정 디렉토리나 파일만 Push 할 수 있는 사람도 있다. 이런 일을 강제하려면 먼저 서버의 Bare 저장소에 acl`이라는 파일을 만들고 거기에 규칙을 기술한다. 그리고 `update 훅에서 Push 하는 파일이 무엇인지 확인하고 ACL과 비교해서 Push 할 수 있는지 없는지 결정한다.

우선 ACL부터 작성한다. CVS에서 사용하는 것과 비슷한 ACL을 만든다. 규칙은 한 라인에 하나씩 기술한다. 각 라인의 첫 번째 필드는 avail`이나 `unavail`이고 두 번째 필드는 규칙을 적용할 사용자들의 목록을 CSV(Comma-Separated Values) 형식으로 적는다. 마지막 필드엔 규칙을 적용할 경로를 적는다. 만약 마지막 필드가 비워져 있으면 모든 경로를 의미한다. 이 필드는 파이프(|`) 문자로 구분한다.

예를 하나 들어보자. 어떤 모듈의 모든 권한을 가지는 관리자도 여러 명이고 doc 디렉토리만 접근해서 문서를 만드는 사람도 여러 명이다. 하지만 lib`과 `tests 디렉토리에 접근하는 사람은 한 명이다. 이런 상황을 ACL로 만들면 아래와 같다.

   avail|nickh,pjhyett,defunkt,tpw
   avail|usinclair,cdickens,ebronte|doc
   avail|schacon|lib
   avail|schacon|tests

이 ACL 정보는 스크립트에서 읽어 사용한다. 설명을 쉽게 하고자 여기서는 `avail`만 처리한다. 아래의 메소드는 Associative Array를 반환하는데, 키는 사용자이름이고 값은 사용자가 Push 할 수 있는 경로의 목록이다.

   def get_acl_access_data(acl_file)
     # read in ACL data
     acl_file = File.read(acl_file).split("\n").reject { |line| line == '' }
     access = {}
     acl_file.each do |line|
       avail, users, path = line.split('|')
       next unless avail == 'avail'
       users.split(',').each do |user|
         access[user] ||= []
         access[user] << path
   end
   end
   access
   end

이 get_acl_access_data 함수가 앞의 ACL 파일을 읽고 반환하는 결과는 아래와 같다.

   {"defunkt"=>[nil],
    "tpw"=>[nil],
    "nickh"=>[nil],
    "pjhyett"=>[nil],
    "schacon"=>["lib", "tests"],
    "cdickens"=>["doc"],
    "usinclair"=>["doc"],
    "ebronte"=>["doc"]}

이렇게 사용할 권한 정보를 만들었다. 이제 Push 하는 파일을 그 사용자가 Push 할 수 있는지 없는지 알아내야 한다.

git log 명령에 --name-only 옵션을 주면 해당 커밋에서 수정된 파일이 뭔지 알려준다. (git log 명령은 Git의 기초에서 다루었다)

   $ git log -1 --name-only --pretty=format:'' 9f585d

   README
   lib/test.rb

get_acl_access_data 메소드를 호출해서 ACL 정보를 구하고, 각 커밋에 들어 있는 파일 목록도 얻은 다음에, 사용자가 모든 커밋을 Push 할 수 있는지 판단한다.

   # only allows certain users to modify certain subdirectories in a project
   def check_directory_perms
     access = get_acl_access_data('acl')

     # see if anyone is trying to push something they can't
     new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
     new_commits.each do |rev|
       files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n")
       files_modified.each do |path|
         next if path.size == 0
         has_file_access = false
         access[$user].each do |access_path|
           if !access_path  # user has access to everything
              || (path.start_with? access_path) # access to this path
             has_file_access = true
           end
         end
         if !has_file_access
           puts "[POLICY] You do not have access to push to #{path}"
           exit 1
         end
       end
     end
   end

   check_directory_perms

먼저 git rev-list 명령으로 서버에 Push 하려는 커밋이 무엇인지 알아낸다. 그리고 각 커밋에서 수정한 파일이 어떤 것들이 있는지 찾고, 해당 사용자가 모든 파일에 대한 권한이 있는지 확인한다.

이제 사용자는 메시지 규칙을 어겼거나 권한이 없는 파일이 포함된 커밋은 어떤 것도 Push 하지 못한다.

새로 작성한 정책 테스트

이 정책을 다 구현해서 update 스크립트에 넣고 chmod u+x .git/hooks/update 명령으로 실행 권한을 준다. 그리고 틀린 형식으로 커밋 메시지를 작성하고 Push 하면 아래와 같이 실패한다.

   $ git push -f origin master
   Counting objects: 5, done.
   Compressing objects: 100% (3/3), done.
   Writing objects: 100% (3/3), 323 bytes, done.
   Total 3 (delta 1), reused 0 (delta 0)
   Unpacking objects: 100% (3/3), done.
   Enforcing Policies...
   (refs/heads/master) (8338c5) (c5b616)
   [POLICY] Your message is not formatted correctly
   error: hooks/update exited with error code 1
   error: hook declined to update refs/heads/master
   To git@gitserver:project.git
    ! [remote rejected] master -> master (hook declined)
   error: failed to push some refs to 'git@gitserver:project.git'

정책과 관련해 하나씩 살펴보자. 먼저 훅이 실행될 때마다 아래 메시지가 출력된다.

   Enforcing Policies...
   (refs/heads/master) (fb8c72) (c56860)

이 내용은 스크립트 윗부분에서 표준출력(stdout)에 출력한 내용이다. 스크립트에서 표준출력으로 출력하면 클라이언트로 전송된다.

그리고 아래의 에러 메시지를 살펴보자.

   [POLICY] Your message is not formatted correctly
   error: hooks/update exited with error code 1
   error: hook declined to update refs/heads/master

첫 번째 라인은 스크립트에서 직접 출력한 것이고 나머지 두 라인은 Git이 출력해 주는 것이다. 이 메시지는 update 스크립트에서 0이 아닌 값을 반환해서 Push 할 수 없다는 메시지다. 그리고 마지막 메시지를 보자.

   To git@gitserver:project.git
    ! [remote rejected] master -> master (hook declined)
   error: failed to push some refs to 'git@gitserver:project.git'

이 메시지는 훅에서 거절된 것이라고 말해주는 것이고 브랜치가 거부될 때마다 하나씩 출력된다.

그리고 누군가 권한이 없는 파일을 수정해서 Push 해도 에러 메시지를 출력한다. 예를 들어 문서 담당자가 lib 디렉토리에 있는 파일을 수정해서 커밋하면 아래와 같은 메시지가 출력된다.

   [POLICY] You do not have access to push to lib/test.rb

이제부터는 update 스크립트가 항상 실행되기 때문에 커밋 메시지도 규칙대로 작성해야 하고, 권한이 있는 파일만 Push 할 수 있다.

클라이언트 훅

서버 훅의 단점은 Push 할 때까지 Push 할 수 있는지 없는지 알 수 없다는 데 있다. 기껏 공들여 정성껏 구현했는데 막상 Push 할 수 없으면 곤혹스럽다. 히스토리를 제대로 고치는 일은 정신건강에 매우 해롭다.

이 문제는 클라이언트 훅으로 해결한다. 클라이언트 훅으로 서버가 거부할지 말지 검사한다. 사람들은 커밋하기 전에, 그러니까 시간이 지나 고치기 어려워지기 전에 문제를 해결할 수 있다. Clone 할 때 이 훅은 전송되지 않기 때문에 다른 방법으로 동료에게 배포해야 한다. 그 훅을 가져다 .git/hooks 디렉토리에 복사하고 실행할 수 있게 만든다. 이 훅 파일을 프로젝트에 넣어서 배포해도 되고 Git 훅 프로젝트를 만들어서 배포해도 된다. 하지만, 자동으로 설치하는 방법은 없다.

커밋 메시지부터 검사해보자. 이 훅이 있으면 커밋 메시지가 구리다고 서버가 뒤늦게 거절하지 않는다. 이것은 commit-msg 훅으로 구현한다. 이 훅은 커밋 메시지가 저장된 파일을 첫 번째 아규먼트로 입력받는다. 그 파일을 읽어 패턴을 검사한다. 필요한 패턴이 없으면 커밋을 중단시킨다.

   #!/usr/bin/env ruby
   message_file = ARGV[0]
   message = File.read(message_file)
   
$regex = /\[ref: (\d+)\]/ if !$regex.match(message) puts "[POLICY] Your message is not formatted correctly" exit 1 end

이 스크립트를 `.git/hooks/commit-msg`라는 파일로 만들고 실행권한을 준다. 커밋이 메시지 규칙을 어기면 아래와 같은 메시지를 보여 준다.

   $ git commit -am 'test'
   [POLICY] Your message is not formatted correctly

커밋하지 못했다. 하지만, 커밋 메지시를 바르게 작성하면 커밋할 수 있다.

   $ git commit -am 'test [ref: 132]'
   [master e05c914] test [ref: 132]
    1 file changed, 1 insertions(+), 0 deletions(-)

그리고 아예 권한이 없는 파일을 수정 못 하게 할 때는 pre-commit 훅을 이용한다. 사전에 .git 디렉토리 안에 ACL 파일을 가져다 놓고 아래와 같이 작성한다.

   #!/usr/bin/env ruby

   $user    = ENV['USER']

   # [ insert acl_access_data method from above ]

   # only allows certain users to modify certain subdirectories in a project
   def check_directory_perms
     access = get_acl_access_data('.git/acl')

     files_modified = `git diff-index --cached --name-only HEAD`.split("\n")
     files_modified.each do |path|
       next if path.size == 0
       has_file_access = false
       access[$user].each do |access_path|
       if !access_path || (path.index(access_path) == 0)
         has_file_access = true
       end
       if !has_file_access
         puts "[POLICY] You do not have access to push to #{path}"
         exit 1
       end
     end
   end

   check_directory_perms

내용은 서버 훅과 똑같지만 두 가지가 다르다. 첫째, 클라이언트 훅은 Git 디렉토리가 아니라 워킹 디렉토리에서 실행하기 때문에 ACL 파일 위치가 다르다. 그래서 ACL 파일 경로를 수정해야 한다.

   access = get_acl_access_data('acl')

이 부분을 아래와 같이 바꾼다.

   access = get_acl_access_data('.git/acl')

두 번째 차이점은 파일 목록을 얻는 방법이다. 서버 훅에서는 커밋에 있는 파일을 모두 찾았지만 여기서는 아직 커밋하지도 않았다. 그래서 Staging Area의 파일 목록을 이용한다.

   files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`

이 부분을 아래와 같이 바꾼다.

   files_modified = `git diff-index --cached --name-only HEAD`

이 두 가지 점만 다르고 나머지는 똑같다. 보통은 리모트 저장소의 계정과 로컬의 계정도 같다. 다른 계정을 사용하려면 $user 환경변수에 누군지 알려야 한다.

이렇게 훅을 이용해 Fast-forward가 아닌 Push는 못 하게 만들 수 있다. Fast-forward가 아닌 Push는 Rebase로 이미 Push 한 커밋을 바꿔 버렸거나 전혀 다른 로컬 브랜치를 Push 하지 못 하도록 하는 것이다.

서버에 이미 receive.denyDeletes 나 receive.denyNonFastForwards 설정을 했다면 더 좁혀진다. 이미 Push 한 커밋을 Rebase 해서 Push 하지 못 하게 만들 때 유용하다.

아래는 이미 Push 한 커밋을 Rebase 하지 못하게 하는 pre-Rebase 스크립트다. 이 스크립트는 먼저 Rebase 할 커밋 목록을 구하고 커밋이 리모트 Refs/브랜치에 들어 있는지 확인한다. 커밋이 한 개라도 리모트 Refs/브랜치에 들어 있으면 Rebase 할 수 없다.

   #!/usr/bin/env ruby

   base_branch = ARGV[0]
   if ARGV[1]
     topic_branch = ARGV[1]
   else
     topic_branch = "HEAD"
   end

   target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("\n")
   remote_refs = `git branch -r`.split("\n").map { |r| r.strip }

   target_shas.each do |sha|
     remote_refs.each do |remote_ref|
       shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
       if shas_pushed.split("\n").include?(sha)
         puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}"
         exit 1
       end
     end
   end

이 스크립트는 리비전 조회하기 절에서 설명하지 않은 표현을 사용했다. 아래의 표현은 이미 Push 한 커밋 목록을 얻어오는 부분이다.

   `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
   .

`SHA^@`은 해당 커밋의 모든 부모를 가리킨다. 그러니까 이 명령은 지금 Push 하려는 커밋에서 리모트 저장소의 커밋에 도달할 수 있는지 확인하는 명령이다. 즉, Fast-forward인지 확인하는 것이다.

이 방법은 매우 느리고 보통은 필요 없다. 어차피 Fast-forward가 아닌 Push는 -f 옵션을 주어야 Push 할 수 있다. 문제가 될만한 Rebase를 방지할 수 있다는 것을 보여주려고 이 예제를 설명했다.


posted by REDFORCE 2017. 4. 13. 04:19

출처 : git-scm.com


8.3 Git맞춤 - Git Hooks

Git Hooks

Git도 다른 버전 관리 시스템처럼 어떤 이벤트가 생겼을 때 자동으로 특정 스크립트를 실행하도록 할 수 있다. 이 훅은 클라이언트 훅과 서버 훅으로 나눌 수 있다. 클라이언트 훅은 커밋이나 Merge 할 때 실행되고 서버 훅은 Push 할 때 서버에서 실행된다. 이 절에서는 어떤 훅이 있고 어떻게 사용하는지 배운다.

훅 설치하기

훅은 Git 디렉토리 밑에 `hooks`라는 디렉토리에 저장한다. 기본 훅 디렉토리는 `.git/hooks`이다. 이 디렉토리에 가보면 Git이 자동으로 넣어준 매우 유용한 스크립트 예제가 몇 개 있다. 그리고 스크립트가 입력받는 값이 어떤 값인지 파일 안에 자세히 설명돼 있다. 모든 예제는 쉘과 Perl 스크립트로 작성돼 있지만 실행할 수만 있으면 되고 Ruby나 Python같은 다른 스크립트 언어로 만들어도 된다. 예제 스크립트의 파일 이름에는 `.sample`이라는 확장자가 붙어 있다. 그래서 이름만 바꿔주면 그 훅을 바로 사용할 수 있다.

실행할 수 있는 스크립트 파일을 확장자 없이 저장소의 hooks 디렉토리에 넣으면 훅 스크립트가 켜진다. 이 스크립트는 앞으로 계속 호출된다. 여기서는 주요 훅 몇 가지에 대해서 설명한다.

클라이언트 훅

클라이언트 훅은 매우 다양하다. 이 절에서는 클라이언트 훅을 committing-workflow 훅, email-workflow 스크립트, 그리고 나머지로 분류해서 설명한다.

Note

여기서 한가지 알아둘 점은 저장소를 Clone 해도 클라이언트 훅은 복사되지 않는다는 점이다. 만든 정책이 반드시 적용되도록 하려면 서버 훅을 이용해야만 하며 작성은 정책 구현하기 부분을 참고한다.

커밋 워크플로 훅

먼저 커밋과 관련된 훅을 살펴보자. 커밋과 관련된 훅은 모두 네 가지다.

pre-commit 훅은 커밋할 때 가장 먼저 호출되는 훅으로 커밋 메시지를 작성하기 전에 호출된다. 이 훅에서 커밋하는 Snapshot을 점검한다. 빠트린 것은 없는지, 테스트는 확실히 했는지 등을 검사한다. 커밋할 때 꼭 확인해야 할 게 있으면 이 훅으로 확인한다. 그리고 이 훅의 Exit 코드가 0이 아니면 커밋은 취소된다. 물론 git commit --no-verify`라고 실행하면 이 훅을 일시적으로 생략할 수 있다. `lint 같은 프로그램으로 코드 스타일을 검사하거나, 라인 끝의 공백 문자를 검사하거나(예제로 들어 있는 pre-commit훅이 하는 게 이 일이다), 새로 추가한 코드에 주석을 달았는지 검사하는 일은 이 훅으로 하는 것이 좋다.

prepare-commit-msg 훅은 Git이 커밋 메시지를 생성하고 나서 편집기를 실행하기 전에 실행된다. 이 훅은 사람이 커밋 메시지를 수정하기 전에 먼저 프로그램으로 손보고 싶을 때 사용한다. 이 훅은 커밋 메시지가 들어 있는 파일의 경로, 커밋의 종류를 아규먼트로 받는다. 그리고 최근 커밋을 수정할 때는(Amending 커밋) SHA-1 값을 추가 아규먼트로 더 받는다. 사실 이 훅은 일반 커밋에는 별로 필요 없고 커밋 메시지를 자동으로 생성하는 커밋에 좋다. 커밋 메시지에 템플릿을 적용하거나, Merge 커밋, Squash 커밋, Amend 커밋일 때 유용하다. 이 스크립트로 커밋 메시지 템플릿에 정보를 삽입할 수 있다.

commit-msg 훅은 커밋 메시지가 들어 있는 임시 파일의 경로를 아규먼트로 받는다. 그리고 이 스크립트가 0이 아닌 값을 반환하면 커밋되지 않는다. 이 훅에서 최종적으로 커밋이 완료되기 전에 프로젝트 상태나 커밋 메시지를 검증한다. 이 장의 마지막 절에서 이 훅을 사용하는 예제를 보여준다. 커밋 메시지가 정책에 맞는지 검사하는 스크립트를 만들어 보자.

커밋이 완료되면 post-commit 훅이 실행된다. 이 훅은 넘겨받는 아규먼트가 하나도 없지만 커밋 해시정보는 git log -1 HEAD 명령으로 쉽게 가져올 수 있다. 일반적으로 이 스크립트는 커밋된 것을 누군가 혹은 다른 프로그램에게 알릴 때 사용한다.

이메일 워크플로 훅

이메일 워크플로에 해당하는 클라이언트 훅은 세 가지이다. 이 훅은 모두 git am 명령으로 실행된다. 이 명령어를 사용할 일이 없으면 이 절은 읽지 않아도 된다. 하지만, 언젠가는 git format-patch 명령으로 만든 Patch를 이메일로 받는 날이 올지도 모른다.

제일 먼저 실행하는 훅은 `applypatch-msg`이다. 이 훅의 아규먼트는 Author가 보내온 커밋 메시지 파일의 이름이다. 이 스크립트가 종료할 때 0이 아닌 값을 반환하면 Git은 Patch 하지 않는다. 커밋 메시지가 규칙에 맞는지 확인하거나 자동으로 메시지를 수정할 때 이 훅을 사용한다.

git am`으로 Patch 할 때 두 번째로 실행되는 훅이 `pre-applypatch`이다. 이 훅은 아규먼트가 없고 단순히 Patch를 적용하고 나서 실행된다. 그래서 커밋할 스냅샷을 검사하는 데 사용한다. 이 스크립트로 테스트를 수행하고 파일을 검사할 수 있다. 테스트에 실패하거나 뭔가 부족하면 0이 아닌 값을 반환시켜서 `git am 명령을 취소시킬 수 있다.

git am 명령에서 마지막으로 실행되는 훅은 `post-applypatch`다. 이 스크립트를 이용하면 자동으로 Patch를 보낸 사람이나 그룹에게 알림 메시지를 보낼 수 있다. 이 스크립트로는 Patch를 중단시킬 수 없다.

기타 훅

pre-rebase 훅은 Rebase 하기 전에 실행된다. 이 훅이 0이 아닌 값을 반환하면 Rebase가 취소된다. 이 훅으로 이미 Push 한 커밋을 Rebase 하지 못하게 할 수 있다. Git이 자동으로 넣어주는 pre-rebase 예제가 바로 그 예제다. 이 예제에는 기준 브랜치가 `next`라고 돼 있다. 참고하여 실제로 적용할 브랜치 이름으로 사용하면 된다.

post-rewrite 훅은 커밋을 변경하는 명령을 실행했을 때 실행된다. 예를 들어 git commit --amend 이나 git rebase 같은 명령이 해당한다. git filter-branch 명령은 해당하지 않는다. 아규먼트로 커밋을 변경하게 한 명령이 전달되고 stdin`으로 변경된 커밋 목록이 전달된다. 훅의 용도는 `post-checkout 이나 post-merge 훅과 비슷하다고 볼 수 있다.

디렉토리에서 뭔가 할 일이 있을 때 사용한다. 그러니까 용량이 크거나 Git이 관리하지 않는 파일을 옮기거나, 문서를 자동으로 생성하는 데 쓴다.

post-merge 훅은 Merge가 끝나고 나서 실행된다. 이 훅은 파일 권한 같이 Git이 추적하지 않는 정보를 관리하는 데 사용한다. Merge로 Working Tree가 변경될 때 Git이 관리하지 않는 파일이 원하는 대로 잘 배치됐는지 검사할 때도 좋다.

pre-push 훅은 git push 명령을 실행하면 동작하는데 리모트 정보를 업데이트 하고 난 후 리모트로 데이터를 전송하기 전에 동작한다. 리모트의 이름과 주소를 파라미터로 전달받으며 `stdin`을 통해 업데이트 할 해시 리스트를 전달받는다. Push 하기 전에 커밋이 유효한지 확인하는 용도로 사용할 수 있다. 훅에서 0이 아닌 값을 반환하면 Push를 중지시킨다.

Git은 정상적으로 동작하는 중에도 이따금 git gc --auto 명령으로 가비지 컬렉션을 동작시킨다. pre-auto-gc 훅은 가비지 컬렉션이 실행되기 직전에 호출되는 훅으로 가비지 컬렉션이 동작한다고 사용자에게 알려주거나 지금 시점에 가비지 컬렉션이 동작하기엔 좋지 않다고 Git에 알려주는 용도로 사용할 수 있다.

서버 훅

클라이언트 훅으로도 어떤 정책을 강제할 수 있지만, 시스템 관리자에게는 서버 훅이 더 중요하다. 서버 훅은 모두 Push 전후에 실행된다. Push 전에 실행되는 훅이 0이 아닌 값을 반환하면 해당 Push는 거절되고 클라이언트는 에러 메시지를 출력한다. 이 훅으로 아주 복잡한 Push 정책도 가능하다.

pre-receive

Push 하면 가장 처음 실행되는 훅은 pre-receive 훅이다. 이 스크립트는 표준 입력(STDIN)으로 Push 하는 Refs의 목록을 입력받는다. 0이 아닌 값을 반환하면 해당 Refs가 전부 거절된다. Fast-forward Push가 아니면 거절하거나, 브랜치 Push 권한을 제어하려면 이 훅에서 하는 것이 좋다. 관리자만 브랜치를 새로 Push 하고 삭제할 수 있고 일반 개발자는 수정사항만 Push 할 수 있게 할 수 있다.

update

update 스크립트는 각 브랜치마다 한 번씩 실행된다는 것을 제외하면 pre-receive 스크립트와 거의 같다. 한 번에 브랜치를 여러 개 Push 하면 `pre-receive`는 딱 한 번만 실행되지만, update는 브랜치마다 실행된다. 이 스크립트는 표준 입력으로 데이터를 입력받는 것이 아니라 아규먼트로 브랜치 이름, 원래 가리키던 SHA-1 값, 사용자가 Push 하는 SHA-1 값을 입력받는다. update 스크립트가 0이 아닌 값을 반환하면 해당 Refs만 거절되고 나머지 다른 Refs는 상관없다.

post-receive

post-receive 훅은 Push 한 후에 실행된다. 이 훅으로 사용자나 서비스에 알림 메시지를 보낼 수 있다. 그리고 pre-receive 훅처럼 표준 입력(STDIN)으로 Refs 목록이 넘어간다. 이 훅으로 메일링리스트에 메일을 보내거나, CI(Continuous Integration) 서버나 Ticket-tracking 시스템의 정보를 수정할 수 있다. 심지어 커밋 메시지도 파싱할 수 있기 때문에 이 훅으로 Ticket을 만들고, 수정하고, 닫을 수 있다. 이 스크립트가 완전히 종료할 때까지 클라이언트와의 연결은 유지되고 Push를 중단시킬 수 없다. 그래서 이 스크립트로 시간이 오래 걸릴만한 일을 할 때는 조심해야 한다.


posted by REDFORCE 2017. 4. 13. 04:18

출처 : git-scm.com


8.2 Git맞춤 - Git Attributes

Git Attributes

디렉토리와 파일 단위로 다른 설정을 적용할 수도 있다. 이렇게 경로별로 설정하는 것을 Git Attribute 라고 부른다. 이 설정은 `.gitattributes`라는 파일에 저장하고 아무 디렉토리에나 둘 수 있지만, 보통은 프로젝트 최상위 디렉토리에 둔다. 그리고 이 파일을 커밋하고 싶지 않으면 `.gitattributes`가 아니라 `.git/info/attributes`로 파일을 만든다.

이 Attribute로 Merge는 어떻게 할지, 텍스트가 아닌 파일은 어떻게 Diff 할지, checkin/checkout 할 때 어떻게 필터링할지 정해줄 수 있다. 이 절에서는 설정할 수 있는 Attribute가 어떤 것이 있는지, 그리고 어떻게 설정하는지 배우고 예제를 살펴본다.

바이너리 파일

이 Attribute로 어떤 파일이 바이너리 파일인지 Git에게 알려줄 수 있다. 기본적으로 Git은 어떤 파일이 바이너리 파일인지 알지 못한다. 하지만, Git에는 파일을 어떻게 다뤄야 하는지 알려주는 방법이 있다. 텍스트 파일 중에서 프로그램이 생성하는 파일에는 바이너리 파일과 진배없는 파일이 있다. 이런 파일은 diff 할 수 없으니 바이너리 파일이라고 알려줘야 한다. 반대로 바이너리 파일 중에서 취급 방법을 Git에 알려주면 diff 할 수 있는 파일도 있다. 이어지는 내용으로 어떻게 설정할 수 있는지 살펴보자.

바이너리 파일로 설정

사실 텍스트 파일이지만 만든 목적과 의도를 보면 바이너리 파일인 것이 있다. 예를 들어 Mac의 Xcode는 .pbxproj 파일을 만든다. 이 파일은 IDE 설정 등을 디스크에 저장하는 파일로 JSON 포맷이다. 모든 것이 ASCII인 텍스트 파일이지만 실제로는 간단한 데이터베이스이기 때문에 텍스트 파일처럼 취급할 수 없다. 그래서 여러 명이 이 파일을 동시에 수정하고 Merge 할 때 diff가 도움이 안 된다. 이 파일은 프로그램이 읽고 쓰는 파일이기 때문에 바이너리 파일처럼 취급하는 것이 옳다.

모든 pbxproj 파일을 바이너리로 파일로 취급하는 설정은 아래와 같다. .gitattributes 파일에 넣으면 된다.

   *.pbxproj binary

이제 pbxproj 파일은 CRLF 변환이 적용되지 않는다. git show`나 `git diff 같은 명령을 실행할 때도 통계를 계산하거나 diff를 출력하지 않는다.

바이너리 파일 Diff 하기

Git은 바이너리 파일도 Diff 할 수 있다. Git Attribute를 통해 Git이 바이너리 파일을 텍스트 포맷으로 변환하고 그 결과를 diff 명령으로 비교하도록 하는 것이다.

먼저 이 기술을 인류에게 알려진 가장 귀찮은 문제 중 하나인 Word 문서를 버전 관리하는 상황을 살펴보자. 모든 사람이 Word가 가장 끔찍한 편집기라고 말하지만 애석하게도 모두 Word를 사용한다. Git 저장소에 넣고 이따금 커밋하는 것만으로도 Word 문서의 버전을 관리할 수 있다. 그렇지만 `git diff`를 실행하면 아래와 같은 메시지를 볼 수 있을 뿐이다.

   $ git diff
   diff --git a/chapter1.docx b/chapter1.docx
   index 88839c4..4afcb7c 100644
   Binary files a/chapter1.docx and b/chapter1.docx differ

직접 파일을 하나하나 까보지 않으면 두 버전이 뭐가 다른지 알 수 없다. Git Attribute를 사용하면 이를 더 좋게 개선할 수 있다. .gitattributes 파일에 아래와 같은 내용을 추가한다.

   *.docx diff=word

이것은 *.docx 파일의 두 버전이 무엇이 다른지 Diff 할 때 “word” 필터를 사용하라고 설정하는 것이다. 그럼 “word” 필터는 뭘까? 이 “word” 필터도 정의해야 한다. Word 문서에서 사람이 읽을 수 있는 텍스트를 추출해주는 docx2txt 프로그램을 사용하여 Diff에 이용한다.

우선 docx2txt 프로그램을 설치해야 하는데 http://docx2txt.sourceforge.net 사이트에서 다운로드 할 수 있다. INSTALL 부분의 설치과정을 참고하여 설치하고 쉘에서 실행할 수 있도록 설정한다. 그리고 Git에서 잘 쓸 수 있도록 Wrapper 스크립트를 docx2txt 라는 이름으로 아래와 같이 작성한다.

   #!/bin/bash
   docx2txt.pl $1 -

`chmod a+x`로 실행권한을 설정해두고 아래와 같이 Git 설정을 추가한다.

   $ git config diff.word.textconv docx2txt

이제 Git은 확장자가 .docx`인 파일의 스냅샷을 Diff 할 때 ``word'' 필터로 정의한 `docx2txt 프로그램을 사용한다. 이 프로그램은 Word 파일을 텍스트 파일로 변환해 주기 때문에 Diff 할 수 있다.

이 책의 1장을 Word 파일로 만들어서 Git에 넣고 나서 단락 하나를 수정하고 저장하는 예를 살펴본다. 새로 단락을 하나 추가하고 나서 `git diff`를 실행하면 어디가 달려졌는지 확인할 수 있다.

   $ git diff
   diff --git a/chapter1.docx b/chapter1.docx
   index 0b013ca..ba25db5 100644
   --- a/chapter1.docx
   +++ b/chapter1.docx
   @@ -2,6 +2,7 @@
    This chapter will be about getting started with Git. We will begin at the beginning by explaining some background on version control tools, then move on to how to get Git running on your system and finally how to get it setup to start working with. At the end of this chapter you should understand why Git is around, why you should use it and you should be all setup to do so.
    1.1. About Version Control
    What is "version control", and why should you care? Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. For the examples in this book you will use software source code as the files being version controlled, though in reality you can do this with nearly any type of file on a computer.
   +Testing: 1, 2, 3.
    If you are a graphic or web designer and want to keep every version of an image or layout (which you would most certainly want to), a Version Control System (VCS) is a very wise thing to use. It allows you to revert files back to a previous state, revert the entire project back to a previous state, compare changes over time, see who last modified something that might be causing a problem, who introduced an issue and when, and more. Using a VCS also generally means that if you screw things up or lose files, you can easily recover. In addition, you get all this for very little overhead.
    1.1.1. Local Version Control Systems
    Many people's version-control method of choice is to copy files into another directory (perhaps a time-stamped directory, if they're clever). This approach is very common because it is so simple, but it is also incredibly error prone. It is easy to forget which directory you're in and accidentally write to the wrong file or copy over files you don't mean to.

git diff 명령의 결과를 보면 “Testing: 1, 2, 3.” 부분이 추가된 것을 확인할 수 있다. 물론 텍스트 형식 같은 완전한 정보는 아니지만 어쨌든 유용하다.

이 방법으로 이미지 파일도 Diff 할 수 있다. 필터로 EXIF 정보를 추출해서 이미지 파일을 비교한다. EXIF 정보는 대부분의 이미지 파일에 들어 있는 메타데이터다. exiftool 프로그램을 설치하고 이미지 파일에서 메타데이터 텍스트를 추출한다. 그리고 그 결과를 Diff 해서 무엇이 달라졌는지 본다. 다음 내용을 .gitattributes 파일로 저장한다.

   *.png diff=exif

Git에서 위 설정을 사용하려면 다음과 같이 설정한다.

   $ git config diff.exif.textconv exiftool

프로젝트에 들어 있는 이미지 파일을 변경하고 `git diff`를 실행하면 아래와 같이 보여준다.

   diff --git a/image.png b/image.png
   index 88839c4..4afcb7c 100644
   --- a/image.png
   +++ b/image.png
   @@ -1,12 +1,12 @@
    ExifTool Version Number         : 7.74
   -File Size                       : 70 kB
   -File Modification Date/Time     : 2009:04:21 07:02:45-07:00
   +File Size                       : 94 kB
   +File Modification Date/Time     : 2009:04:21 07:02:43-07:00
    File Type                       : PNG
    MIME Type                       : image/png
   -Image Width                     : 1058
   -Image Height                    : 889
   +Image Width                     : 1056
   +Image Height                    : 827
    Bit Depth                       : 8
    Color Type                      : RGB with Alpha

이미지 파일의 크기와 해상도가 달라진 것을 쉽게 알 수 있다.

키워드 치환

SVN이나 CVS에 익숙한 사람들은 해당 시스템에서 사용하던 키워드 치환(Keyword Expansion) 기능을 찾는다. Git에서는 이것이 쉽지 않다. Git은 먼저 체크섬을 계산하고 커밋하기 때문에 그 커밋에 대한 정보를 가지고 파일을 수정할 수 없다. 하지만, Checkout 할 때 그 정보가 자동으로 파일에 삽입되도록 했다가 다시 커밋할 때 삭제되도록 할 수 있다.

파일 안에 $Id$ 필드를 넣으면 Blob의 SHA-1 체크섬을 자동으로 삽입한다. 이 필드를 파일에 넣으면 Git은 앞으로 Checkout 할 때 해당 Blob의 SHA-1 값으로 교체한다. 여기서 꼭 기억해야 할 것이 있다. 교체되는 체크섬은 커밋의 것이 아니라 Blob 그 자체의 SHA-1 체크섬이다. 다음 내용을 .gitattributes 파일로 저장한다.

   *.txt ident

테스트 할 파일에 $Id$ 레퍼런스를 넣고 저장한다.

   $ echo '$Id$' > test.txt

Git은 이 파일을 Checkout 할 때마다 SHA 값을 삽입해준다.

   $ rm test.txt
   $ git checkout -- test.txt
   $ cat test.txt
   $Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $

하지만, 이것은 별로 유용하지 않다. CVS나 SVN의 키워드 치환(Keyword Substitution)을 써봤으면 날짜(Datestamp)도 가능했다는 것을 알고 있을 것이다. SHA는 그냥 해시이고 식별할 수 있을 뿐이지 다른 것을 알려주진 않는다. SHA만으로는 예전 것보다 새것인지 오래된 것인지는 알 수 없다.

Commit/Checkout 할 때 사용하는 필터를 직접 만들어 쓸 수 있다. 방향에 따라 “clean” 필터와 “smudge” 필터라고 부른다. ".gitattributes" 파일에 설정하고 파일 경로마다 다른 필터를 설정할 수 있다. Checkout 할 때 파일을 처리하는 것이 “smudge” 필터이고(“smudge” 필터는 Checkout 할 때 실행됨.) 커밋할 때 처리하는 필터가 “clean”(“clean” 필터는 파일을 Stage 할 때 실행됨.) 필터이다. 이 필터로 할 수 있는 일은 무궁무진하다.

``smudge'' 필터는 Checkout 할 때 실행됨.
Figure 144. “smudge” 필터는 Checkout 할 때 실행됨.
``clean'' 필터는 파일을 Stage 할 때 실행됨.
Figure 145. “clean” 필터는 파일을 Stage 할 때 실행됨.

이 기능은 사실 커밋 메시지를 위한 기능이었지만 응용한다면 커밋하기 전에 indent 프로그램으로 C 코드 전부를 필터링하는 기능을 만들 수 있다. *.c 파일에 대해 indent 필터를 거치도록 .gitattributes 파일에 설정한다.

   *.c filter=indent

아래처럼 “indent” 필터의 smudge와 clean이 무엇인지 설정한다.

   $ git config --global filter.indent.clean indent
   $ git config --global filter.indent.smudge cat

*.c 파일을 커밋하면 indent 프로그램을 통해서 커밋되고 Checkout 하면 cat 프로그램을 통해 Checkout된다. cat`은 입력된 데이터를 그대로 다시 내보내는 사실 아무것도 안 하는 프로그램이다. 이렇게 설정하면 모든 C 소스 파일은 `indent 프로그램을 통해 커밋된다.

이제 RCS처럼 $Date$`를 치환하는 예제를 살펴보자. 이 기능을 구현하려면 간단한 스크립트가 하나 필요하다. 이 스크립트는 `$Date$ 필드를 프로젝트의 마지막 커밋 일자로 치환한다. 표준 입력을 읽어서 $Date$ 필드를 치환한다. 아래는 Ruby로 구현한 스크립트다.

   #! /usr/bin/env ruby
   data = STDIN.read
   last_date = `git log --pretty=format:"%ad" -1`
   puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$')

git log 명령으로 마지막 커밋 정보를 얻고 표준 입력(STDIN)에서 $Date$ 스트링을 찾아서 치환한다. 스크립트는 자신이 편한 언어로 만든다. 이 스크립트의 이름을 `expand_date`라고 짓고 실행 경로에 넣는다. 그리고 `dater`라는 Git 필터를 정의한다. Checkout시 실행하는 smudge 필터로 `expand_date`를 사용하고 커밋할 때 실행하는 clean 필터는 Perl을 사용한다.

   $ git config filter.dater.smudge expand_date
   $ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"'

이 Perl 코드는 $Date$ 스트링에 있는 문자를 제거해서 원래대로 복원한다. 이제 필터가 준비됐으니 $Date$ 키워드가 들어 있는 파일을 만들고 Git Attribute를 설정하고 새 필터를 시험해보자.

   date*.txt filter=dater
   $ echo '# $Date$' > date_test.txt

커밋하고 파일을 다시 Checkout 하면 해당 키워드가 적절히 치환된 것을 볼 수 있다.

   $ git add date_test.txt .gitattributes
   $ git commit -m "Testing date expansion in Git"
   $ rm date_test.txt
   $ git checkout date_test.txt
   $ cat date_test.txt
   # $Date: Tue Apr 21 07:26:52 2009 -0700$

이 기능은 매우 강력해서 입맛대로 프로젝트를 맞춤 설정할 수 있다. .git attributes 파일은 커밋하는 파일이기 때문에 필터 드라이버(여기서는 dater) 설정이 되지 않은 사람에게도 배포된다. 물론 dater 설정이 안 돼 있는 사람에게는 에러가 난다. 필터를 만들 때 이런 예외 상황도 고려해서 항상 잘 동작하게 해야 한다.

저장소 익스포트하기

프로젝트를 익스포트해서 아카이브를 만들 때도 Git Attribute가 유용하다.

export-ignore

아카이브를 만들 때 제외할 파일이나 디렉토리가 무엇인지 설정할 수 있다. 특정 디렉토리나 파일을 프로젝트에는 포함하고 아카이브에는 포함하고 싶지 않을 때 export-ignore Attribute를 사용한다.

예를 들어 test/ 디렉토리에 테스트 파일이 있다고 하자. 보통 tar 파일로 묶어서 익스포트할 때 테스트 파일은 포함하지 않는다. Git Attribute 파일에 아래 라인을 추가하면 테스트 파일은 무시된다.

   test/ export-ignore

git archive 명령으로 tar 파일을 만들면 test 디렉토리는 아카이브에 포함되지 않는다.

export-subst

아카이브를 만들어서 배포할 때도 git log 같은 포맷 규칙을 적용할 수 있다. export-subst Attribute로 설정한 파일들의 키워드가 치환된다.

git archive 명령을 실행할 때 자동으로 마지막 커밋의 메타데이터가 자동으로 삽입되게 할 수 있다. 예를 들어 .gitattributes 파일과 LAST_COMMIT 파일을 아래와 같이 만든다.

   LAST_COMMIT export-subst
   $ echo 'Last commit date: $Format:%cd by %aN$' > LAST_COMMIT
   $ git add LAST_COMMIT .gitattributes
   $ git commit -am 'adding LAST_COMMIT file for archives'

git archive 명령으로 아카이브를 만들고 나서 이 파일을 열어보면 아래와 같이 보인다.

   $ git archive HEAD | tar xCf ../deployment-testing -
   $ cat ../deployment-testing/LAST_COMMIT
   Last commit date: Tue Apr 21 08:38:48 2009 -0700 by Scott Chacon

이 키워드 치환 기능으로 커밋 메시지와 Git 노트, Git Log도 넣을 수 있다. 어렵지 않다.

   $ echo '$Format:Last commit: %h by %aN at %cd%n%+w(76,6,9)%B$' > LAST_COMMIT
   $ git commit -am 'export-subst uses git log's custom formatter

   git archive uses git log's `pretty=format:` processor
   directly, and strips the surrounding `$Format:` and `$`
   markup from the output.
   '
   $ git archive @ | tar xfO - LAST_COMMIT
   Last commit: 312ccc8 by Jim Hill at Fri May 8 09:14:04 2015 -0700
          export-subst uses git log's custom formatter

            git archive uses git log's `pretty=format:` processor directly, and
            strips the surrounding `$Format:` and `$` markup from the output.

이 아카이브 기능은 개발할 때가 아니라 배포할 때 좋다.

Merge 전략

파일마다 다른 Merge 전략을 사용하도록 설정할 수 있다. Merge 할 때 충돌이 날 것 같은 파일이 있다고 하자. Git Attrbute로 이 파일만 항상 타인의 코드 말고 내 코드를 사용하도록 설정할 수 있다.

이 설정은 다양한 환경에서 운영하려고 만든 환경 브랜치를 Merge 할 때 좋다. 이때는 환경 설정과 관련된 파일은 Merge 하지 않고 무시하는 게 편리하다. 브랜치에 `database.xml`이라는 데이터베이스 설정파일이 있는데 이 파일은 브랜치마다 다르다. Database 설정 파일은 Merge 하면 안된다. Attribute를 아래와 같이 설정하면 이 파일은 그냥 두고 Merge 한다.

   database.xml merge=ours
   And then define a dummy `ours` merge strategy with:

$ git config --global merge.ours.driver true

다른 브랜치로 이동해서 Merge를 실행했을 때 database.xml 파일에 대해 충돌이 발생하는 대신 아래와 같은 메시지를 보게 된다.

   $ git merge topic
   Auto-merging database.xml
   Merge made by recursive.

Merge 했지만 database.xml 파일은 원래 가지고 있던 파일 그대로다.


posted by REDFORCE 2017. 4. 13. 03:12

출처 : git-scm.com


8.1 Git맞춤 - Git 설정하기

지금까지 Git이 어떻게 동작하고 Git을 어떻게 사용하는지 설명했다. 이제 Git을 좀 더 쉽고 편하게 사용할 수 있도록 도와주는 도구를 살펴본다. 이 장에서는 먼저 많이 쓰이는 설정 그리고 훅 시스템을 먼저 설명한다. 그 후에 Git을 내게 맞추어(Customize) 본다. Git을 자신의 프로젝트에 맞추고 편하게 사용하자.

Git 설정하기

시작하기 에서 git config 명령을 사용했다. 제일 먼저 해야 하는 것은 git config 명령으로 이름과 이메일 주소를 설정하는 것이다.

   $ git config --global user.name "John Doe"
   $ git config --global user.email johndoe@example.com

여기서는 이렇게 설정하는 것 중에서 중요한 것만 몇 가지 배운다.

우선 Git은 내장된 기본 규칙 따르지만, 설정된 것이 있으면 그에 따른다는 점을 생각해두자. Git은 먼저 /etc/gitconfig 파일을 찾는다. 이 파일은 해당 시스템에 있는 모든 사용자와 모든 저장소에 적용되는 설정 파일이다. git config 명령에 --system 옵션을 주면 이 파일을 사용한다.

다음으로 ~/.gitconfig 파일을 찾는다. 이 파일은 해당 사용자에게만 적용되는 설정 파일이다. --global옵션을 주면 Git은 이 파일을 사용한다.

마지막으로 현재 작업 중인 저장소의 Git 디렉토리에 있는 .git/config 파일을 찾는다. 이 파일은 해당 저장소에만 적용된다.

각 설정 파일에 중복된 설정이 있으면 설명한 “순서대로” 덮어쓴다. 예를 들어 `.git/config`와 `/etc/gitconfig`에 같은 설정이 들어 있다면 `.git/config`에 있는 설정을 사용한다.

Note

설정 파일 일반적인 텍스트파일로 쉽게 고쳐 쓸 수 있다. 보통 git config 명령을 사용하는 것이 더 편하다.

클라이언트 설정

설정이 영향을 미치는 대상에 따라 클라이언트 설정과 서버 설정으로 나눠볼 수 있다. 대부분은 개인작업 환경과 관련된 클라이언트 설정이다. Git에는 설정거리가 매우 많은데, 여기서는 워크플로를 관리하는 데 필요한 것과 주로 많이 사용하는 것만 설명한다. 한 번도 겪지 못할 상황에서나 유용한 옵션까지 다 포함하면 설정할 게 너무 많다. Git 버전마다 옵션이 조금씩 다른데, 아래와 같이 실행하면 설치한 버전에서 사용할 수 있는 옵션을 모두 보여준다.

   $ man git-config

어떤 옵션을 사용할 수 있는지 매우 자세히 설명하고 있다. http://git-scm.com/docs/git-config.html 페이지에서도 같은 내용을 볼 수 있다.

core.editor

Git은 편집기를 설정($VISUAL$EDITOR 변수로 설정)하지 않았거나 설정한 편집기를 찾을 수 없으면 vi`를 실행한다. 커밋할 때나 tag 메시지를 편집할 때 설정한 편집기를 실행한다. `code.editor 설정으로 편집기를 설정한다.

   $ git config --global core.editor emacs

이렇게 설정하면 메시지를 편집할 때 환경변수에 설정한 편집기가 아니라 Emacs를 실행한다.

commit.template

커밋할 때 Git이 보여주는 커밋 메시지는 이 옵션에 설정한 템플릿 파일이다. 예를 들어 ~/.gitmessage.txt 파일을 아래와 같이 만든다.

   subject line

   what happened

   [ticket: X]

이 파일을 commit.template`에 설정하면 Git은 `git commit 명령이 실행하는 편집기에 이 메시지를 기본으로 넣어준다.

   $ git config --global commit.template ~/.gitmessage.txt
   $ git commit

그러면 커밋할 때 아래와 같은 메시지를 편집기에 자동으로 채워준다.

   subject line

   what happened

   [ticket: X]
   # Please enter the commit message for your changes. Lines starting
   # with '#' will be ignored, and an empty message aborts the commit.
   # On branch master
   # Changes to be committed:
   #   (use "git reset HEAD <file>..." to unstage)
   #
   # modified:   lib/test.rb
   #
   ~
   ~
   ".git/COMMIT_EDITMSG" 14L, 297C

소속 팀에 커밋 메시지 규칙이 있으면 그 규칙에 맞는 템플릿 파일을 만들고 시스템 설정에 설정해둔다. Git이 그 파일을 사용하도록 설정하면 규칙을 따르기가 쉬워진다.

core.pager

Git은 `log`나 `diff`같은 명령의 메시지를 출력할 때 페이지로 나누어 보여준다. 기본으로 사용하는 명령은 `less`다. `more`를 더 좋아하면 `more`라고 설정한다. 페이지를 나누고 싶지 않으면 빈 문자열로 설정한다.

   $ git config --global core.pager ''

이 명령을 실행하면 Git은 길든지 짧든지 결과를 한 번에 다 보여 준다.

user.signingkey

이 설정은 내 작업에 서명하기 에서 설명했던 Annotated Tag를 만들 때 유용하다. 사용할 GPG 키를 설정해 둘 수 있다. 아래처럼 GPG 키를 설정하면 서명할 때 편리하다.

   $ git config --global user.signingkey <gpg-key-id>

git tag 명령을 실행할 때 키를 생략하고 서명할 수 있다.

   $ git tag -s <tag-name>

core.excludesfile

Git에서 git add 명령으로 추적할 파일에 포함하지 않을 파일은 `.gitignore`에 해당 패턴을 적으면 된다고 파일 무시하기에서 설명했다.

한 저장소 안에서뿐 아니라 어디에서라도 Git에 포함하지 않을 파일을 설정할 수 있다. 예를 들어 Mac을 쓰는 사람이라면 .DS_Store 파일을 자주 보았을 것이다. Emacs나 Vim를 쓰다 보면 이름 끝에 ~ 붙여둔 임시 파일도 있다.

.gitignore 파일처럼 무시할 파일을 설정할 수 있는데 ~/.gitignore_global 파일 안에 아래 내용으로 입력해두고

   *~
   .*.swp
   .DS_Store

git config --global core.excludesfile ~/.gitignore_global 명령으로 설정을 추가하면 더는 위와 같은 파일이 포함되지 않을 것이다.

help.autocorrect

명령어를 잘못 입력하면 Git은 메시지를 아래와 같이 보여 준다.

$ git chekcout master
git: 'chekcout' is not a git command. See 'git --help'.

Did you mean this?
    checkout

Git은 어떤 명령을 입력하려고 했을지 추측해서 보여주긴 하지만 직접 실행하진 않는다. 그러나 `help.autocorrect`를 1로 설정하면 명령어를 잘못 입력해도 Git이 자동으로 해당 명령어를 찾아서 실행해준다.

   $ git chekcout master
   WARNING: You called a Git command named 'chekcout', which does not exist.
   Continuing under the assumption that you meant 'checkout'
   in 0.1 seconds automatically...

여기서 재밌는 것은 “0.1 seconds” 이다. 사실 help.autocorrect 설정에 사용하는 값은 1/10 초 단위의 숫자를 나타낸다. 만약 50이라는 값으로 설정한다면 자동으로 고친 명령을 실행할 때 Git은 5초간 명령을 실행하지 않고 기다려줄 수 있다.

컬러 터미널

사람이 쉽게 인식할 수 있도록 터미널에 결과를 컬러로 출력할 수 있다. 터미널 컬러와 관련된 옵션은 매우 다양하기 때문에 꼼꼼하게 설정할 수 있다.

color.ui

Git은 기본적으로 터미널에 출력하는 결과물을 알아서 색칠하지만, 이 색칠하는 기능을 끄고 싶다면 한 가지 설정만 해 두면 된다. 아래와 같은 명령을 실행하면 더는 색칠된 결과물을 내지 않는다.

   $ git config --global color.ui false

컬러 설정의 기본 값은 `auto`로 터미널에 출력할 때는 색칠하지만, 결과가 리다이렉션되거나 파일로 출력되면 색칠하지 않는다.

always`로 설정하면 터미널이든 다른 출력이든 상관없이 색칠하여 내보낸다. 대개 이 값을 설정해서 사용하지 않는다. `--color 옵션을 사용하면 강제로 결과를 색칠해서 내도록 할 수 있기 때문이다. 대부분은 기본 값이 낫다.

color.*

Git은 좀 더 꼼꼼하게 컬러를 설정하는 방법을 제공한다. 아래와 같은 설정들이 있다. 모두 truefalsealways 중 하나를 고를 수 있다.

   color.branch
   color.diff
   color.interactive
   color.status

또한, 각 옵션의 컬러를 직접 지정할 수도 있다. 아래처럼 설정하면 diff 명령에서 meta 정보의 포그라운드는 blue, 백그라운드는 black, 테스트는 bold로 바뀐다.

   $ git config --global color.diff.meta "blue black bold"

컬러는 normalblackredgreenyellowbluemagentacyanwhite 중에서 고를 수 있다. 텍스트 속성은 bolddimul (underline), blinkreverse 중에서 고를 수 있다.

다른 Merge, Diff 도구 사용하기

Git에 들어 있는 diff 도구 말고 다른 도구로 바꿀 수 있다. 화려한 GUI 도구로 바꿔서 좀 더 편리하게 충돌을 해결할 수 있다. 여기서는 Perforce의 Merge 도구인 P4Merge로 설정하는 것을 보여준다. P4Merge는 무료인데다 꽤 괜찮다.

P4Merge는 중요 플랫폼을 모두 지원하기 때문에 웬만한 환경이면 사용할 수 있다. 여기서는 Mac과 Linux 시스템에 설치하는 것을 보여준다. Windows에서 사용하려면 /usr/local/bin 경로만 Windows 경로로 바꿔준다.

먼저 https://www.perforce.com/product/components/perforce-visual-merge-and-diff-tools 에서 P4Merge를 내려받는다. 그 후에 P4Merge 에 쓸 Wrapper 스크립트를 만든다. 필자는 Mac 사용자라서 Mac 경로를 사용한다. 어떤 시스템이든 `p4merge`가 설치된 경로를 사용하면 된다. 예제에서는 `extMerge`라는 Merge 용 Wrapper 스크립트를 만들고 이 스크립트로 넘어오는 모든 아규먼트를 p4merge 프로그램으로 넘긴다.

   $ cat /usr/local/bin/extMerge
   #!/bin/sh
   /Applications/p4merge.app/Contents/MacOS/p4merge $*

그리고 diff용 Wrapper도 만든다. 이 스크립트로 넘어오는 아규먼트는 총 7개지만 그 중 2개만 Merge Wrapper로 넘긴다. Git이 diff 프로그램에 넘겨주는 아규먼트는 아래와 같다.

   path old-file old-hex old-mode new-file new-hex new-mode

이 중에서 old-file`과 `new-file 만 사용하는 wrapper 스크립트를 만든다.

   $ cat /usr/local/bin/extDiff
   #!/bin/sh
   [ $# -eq 7 ] && /usr/local/bin/extMerge "$2" "$5"

이 두 스크립트에 실행 권한을 부여한다.

   $ sudo chmod +x /usr/local/bin/extMerge
   $ sudo chmod +x /usr/local/bin/extDiff

Git config 파일에 이 스크립트를 모두 추가한다. 설정해야 하는 옵션이 좀 많다. merge.tool`로 무슨 Merge 도구를 사용할지, `mergetool.*.cmd`로 실제로 어떻게 명령어를 실행할지, `mergetool.trustExitCode`로 Merge 도구가 반환하는 exit 코드가 Merge의 성공 여부를 나타내는지, `diff.external`은 diff 할 때 실행할 명령어가 무엇인지를 설정할 때 사용한다. 모두 `git config명령으로 설정한다.

   $ git config --global merge.tool extMerge
   $ git config --global mergetool.extMerge.cmd \
     'extMerge \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"'
   $ git config --global mergetool.extMerge.trustExitCode false
   $ git config --global diff.external extDiff

~/.gitconfig/ 파일을 직접 편집해도 된다.

   [merge]
     tool = extMerge
   [mergetool "extMerge"]
     cmd = extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"
     trustExitCode = false
   [diff]
     external = extDiff

설정을 완료하고 나서 아래와 같이 diff 명령어를 실행한다.

   $ git diff 32d1776b1^ 32d1776b1

diff 결과가 터미널에 출력되는 대신 P4Merge가 실행되어 아래처럼 Diff 결과를 보여준다.

P4Merge.
Figure 143. P4Merge.

브랜치를 Merge 할 때 충돌이 나면 git mergetool 명령을 실행한다. 이 명령을 실행하면 GUI 도구로 충돌을 해결할 수 있도록 P4Merge를 실행해준다.

Wrapper를 만들어 설정해두면 다른 Diff, Merge 도구로 바꾸기도 쉽다. 예를 들어, KDiff3를 사용하도록 extDiff`와 `extMerge 스크립트를 수정한다.

   $ cat /usr/local/bin/extMerge
   #!/bin/sh
/Applications/kdiff3.app/Contents/MacOS/kdiff3 $*

이제부터 Git은 Diff 결과를 보여주거나 충돌을 해결할 때 KDiff3 도구를 사용한다.

어떤 Merge 도구는 Git에 미리 설정이 들어 있다. 그래서 추가로 스크립트를 작성하거나 하는 설정 없이 사용할 수 있는 것도 있다. 아래와 같은 명령으로 확인해볼 수 있다.

   $ git mergetool --tool-help
   'git mergetool --tool=<tool>' may be set to one of the following:
           emerge
           gvimdiff
           gvimdiff2
           opendiff
           p4merge
           vimdiff
           vimdiff2

   The following tools are valid, but not currently available:
           araxis
           bc3
           codecompare
           deltawalker
           diffmerge
           diffuse
           ecmerge
           kdiff3
           meld
           tkdiff
           tortoisemerge
           xxdiff

   Some of the tools listed above only work in a windowed
   environment. If run in a terminal-only session, they will fail.

Diff 도구로는 다른 것을 사용하지만, Merge 도구로는 KDiff3를 사용하고 싶은 경우에는 kdiff3 명령을 실행경로로 넣고 아래와 같이 설정하기만 하면 된다.

   $ git config --global merge.tool kdiff3

extMerge`와 `extDiff 파일을 만들지 않고 이렇게 Merge 도구만 `kdiff3`로 설정하고 Diff 도구는 Git에 원래 들어 있는 것을 사용할 수 있다.

Formatting and Whitespace

협업할 때 겪는 소스 포맷(Formatting)과 공백 문제는 미묘하고 난해하다. 동료 사이에 사용하는 플랫폼이 다를 때는 특히 더 심하다. 다른 사람이 보내온 Patch는 공백 문자 패턴이 미묘하게 다를 확률이 높다. 편집기가 몰래 공백문자를 추가해 버릴 수도 있고 크로스-플랫폼 프로젝트에서 Windows 개발자가 라인 끝에 CR(Carriage-Return) 문자를 추가해 버렸을 수도 있다. Git에는 이 이슈를 돕는 몇 가지 설정이 있다.

core.autocrlf

Windows에서 개발하는 동료와 함께 일하면 라인 바꿈(New Line) 문자에 문제가 생긴다. Windows는 라인 바꿈 문자로 CR(Carriage-Return)과 LF(Line Feed) 문자를 둘 다 사용하지만, Mac과 Linux는 LF 문자만 사용한다. 아무것도 아닌 것 같지만, 크로스 플랫폼 프로젝트에서는 꽤 성가신 문제다. Windows에서 사용하는 많은 편집기가 자동으로 LF 스타일의 라인 바꿈 스타일을 CRLF로 바꾸거나 Enter 키를 입력하면 CRLF 스타일을 사용하기 때문이다.

Git은 커밋할 때 자동으로 CRLF를 LF로 변환해주고 반대로 Checkout 할 때 LF를 CRLF로 변환해 주는 기능이 있다. core.autocrlf 설정으로 이 기능을 켤 수 있다. Windows에서 이 값을 true로 설정하면 Checkout 할 때 LF 문자가 CRLF 문자로 변환된다.

   $ git config --global core.autocrlf true

라인 바꿈 문자로 LF를 사용하는 Linux와 Mac에서는 Checkout 할 때 Git이 LF를 CRLF로 변환할 필요가 없다. 게다가 우연히 CRLF가 들어간 파일이 저장소에 들어 있어도 Git이 알아서 고쳐주면 좋을 것이다.core.autocrlf 값을 input으로 설정하면 커밋할 때만 CRLF를 LF로 변환한다.

   $ git config --global core.autocrlf input

이 설정을 이용하면 Windows에서는 CRLF를 사용하고 Mac, Linux, 저장소에서는 LF를 사용할 수 있다.

Windows 플랫폼에서만 개발하면 이 기능이 필요 없다. 이 옵션을 `false`라고 설정하면 이 기능이 꺼지고 CR 문자도 저장소에도 저장된다.

   $ git config --global core.autocrlf false

core.whitespace

Git에는 공백 문자를 다루는 방법으로 네 가지가 미리 정의돼 있다. 두 가지는 기본적으로 켜져 있지만 끌 수 있고 나머지 두 가지는 꺼져 있지만 켤 수 있다.

먼저 기본적으로 켜져 있는 것을 살펴보자. `blank-at-eol`는 각 라인 끝에 공백이 있는지 찾고, `blank-at-eof`는 파일 끝에 추가한 빈 라인이 있는지 찾고, `space-before-tab`은 모든 라인에서 처음에 tab보다 공백이 먼저 나오는지 찾는다.

기본적으로 꺼져 있는 나머지 세 개는 indent-with-non-tab`과 `tab-in-indent`과 `cr-at-eol`이다. `intent-with-non-tab`은 tab이 아니라 공백으로(`tabwidth 설정에 영향받음) 시작하는 라인이 있는지 찾고 `cr-at-eol`은 라인 끝에 CR 문자가 있어도 괜찮다고 Git에 알리는 것이다.

core.whitespace 옵션으로 이 네 가지 방법을 켜고 끌 수 있다. 설정에서 해당 옵션을 빼버리거나 이름이 -`로 시작하면 기능이 꺼진다. 예를 들어, 다른 건 다 켜고 `cr-at-eol 옵션만 끄려면 아래와 같이 설정한다.

   $ git config --global core.whitespace \
    trailing-space,space-before-tab,indent-with-non-tab

   git diff 명령을 실행하면 Git은 이 설정에 따라 검사해서 컬러로 표시해준다. 그래서 좀 더 쉽게 검토해서 커밋할 수 있다. git apply 명령으로 Patch를 적용할 때도 이 설정을 이용할 수 있다. 아래처럼 명령어를 실행하면 해당 Patch가 공백문자 정책에 들어맞는지 확인할 수 있다.

   $ git apply --whitespace=warn <patch>

아니면 Git이 자동으로 고치도록 할 수 있다.

   $ git apply --whitespace=fix <patch>

이 옵션은 git rebase 명령에서도 사용할 수 있다. 공백 문제가 있는 커밋을 Upstream에 Push 하기 전에--whitespace=fix 옵션을 주고 Rebase 하면 Git은 다시 Patch를 적용하면서 공백을 설정한 대로 고친다.

서버 설정

서버 설정은 많지 않지만, 꼭 짚고 넘어가야 하는 것이 몇 개 있다.

receive.fsckObjects

Git은 Push 할 때마다 각 개체가 SHA-1 체크섬에 맞는지 잘못된 개체가 가리키고 있는지 검사하게 할 수 있다. 기본적으로 이 기능이 동작하지 않게 설정이 되어 있는데 개체를 점검하데 상당히 시간이 걸리기 때문에 Push 하는 시간이 늘어난다. 얼마나 늘어나는지는 저장소 크기와 Push 하는 양에 달렸다.receive.fsckOBjects 값을 true로 설정하면 Git이 Push 할 때마다 검증한다.

   $ git config --system receive.fsckObjects true

이렇게 설정하면 Push 할 때마다 검증하기 때문에 클라이언트는 잘못된 데이터를 Push 하지 못한다.

receive.denyNonFastForwards

이미 Push 한 커밋을 Rebase 해서 다시 Push 하지 못하게 할 수 있다. 브랜치를 Push 할 때 해당 리모트 브랜치가 가리키는 커밋이 Push 하려는 브랜치에 없을 때 Push 하지 못하게 할 수 있다. 보통은 이런 정책이 좋고 git push 명령에 -f 옵션을 주면 강제로 Push 할 수 있다.

   receive.denyNonFastForwards 옵션을 켜면 Fast-forward로 Push 할 수 없는 브랜치는 아예 Push 하지 못한다.

   $ git config --system receive.denyNonFastForwards true

사용자마다 다른 정책을 적용하고 싶으면 서버 훅을 사용해야 한다. 서버의 receive 훅으로 할 수 있고 이 훅도 이 장에서 설명한다.

receive.denyDeletes

`receive.denyNonFastForwards`와 비슷한 정책으로 `receive.denyDeletes`라는 것이 있다. 이 설정을 켜면 브랜치를 삭제하는 Push가 거절된다.

   $ git config --system receive.denyDeletes true

이제 브랜치나 Tag를 삭제하는 Push는 거절된다. 아무도 삭제할 수 없다. 리모트 브랜치를 삭제하려면 직접 손으로 server의 ref 파일을 삭제해야 한다. 그리고 사용자마다 다른 정책을 적용시키는 ACL을 만드는 방법도 있다. 이 방법은 정책 구현하기 에서 다룬다.


'Programming > Git' 카테고리의 다른 글

Git_#8.3_Git맞춤 - Git Hooks  (0) 2017.04.13
Git_#8.2_Git맞춤 - Git Attributes  (0) 2017.04.13
Git_#7.14_Git 도구 - Credential 저장소  (0) 2017.04.13
Git_#7.13_Git 도구 - Replace  (0) 2017.04.13
Git_#7.12_Git 도구 - Bundle  (0) 2017.04.13
posted by REDFORCE 2017. 4. 13. 02:19

출처 : git-scm.com


7.14 Git 도구 - Credential 저장소

Credential 저장소

SSH 프로토콜을 사용하여 리모트 저장소에 접근할 때 Passphase 없이 생성한 SSH Key를 사용하면 사용자 이름과 비밀번호를 입력하지 않고도 안전하게 데이터를 주고받을 수 있다. 반면 HTTP 프로토콜을 사용하는 경우는 매번 사용자 이름과 비밀번호를 입력해야 한다.

다행히도 Git은 이렇게 매번 인증정보(Credential)를 입력하는 경우 인증정보를 저장해두고 자동으로 입력해주는 시스템을 제공한다. Git Credential 기능이 제공하는 옵션은 아래와 같다.

  • 일단 기본적으로 아무런 설정도 하지 않으면 어떤 비밀번호도 저장하지 않는다.
    이 경우 인증이 필요한 때 매번 사용자 이름과 비밀번호를 입력해야 한다.

  • “cache” 모드로 설정하면 일정 시간 동안 메모리에 사용자 이름과 비밀번호 같은 인증정보를 기억한다. 이 정보를 Disk에 저장하지는 않으며 메모리에서도 15분 까지만 유지한다.

  • “store” 모드로 설정하면 인증정보를 Disk의 텍스트 파일로 저장하며 계속 유지한다. 계속 유지한다는 말은 리모트의 인증정보를 변경하지 않는 한 다시 인증정보를 입력하지 않아도 접근할 수 있다는 말이다. “store” 모드를 사용할 때 주의할 점은 인증정보가 사용자 홈 디렉토리 아래에 일반 텍스트 파일로 저장된다는 점이다.

  • Mac에서 Git을 사용하는 경우 “osxkeychain” 모드를 사용하면 Mac에서 제공하는 Keychain 시스템에 사용자 이름과 비밀번호를 현재 로그인 계정에 속하게 저장한다. “store” 모드하면 인증정보를 Disk에 저장하고 인증정보가 만료되지 않는 점은 같지만, Safari 브라우저가 인증정보를 저장하는 것과 같은 수준으로 암호화해서 저장한다는 점이 다르다.

  • Windows 환경에서는 “wincred” 라는 Helper가 있다. “osxkeychain” Helper와 비슷하게 동작하며 Windows Credential Store를 사용하여 안전하게 인증정보를 저장한다.

위에서 설명한 여러 모드 중 하나를 아래와 같이 설정할 수 있다.

   $ git config --global credential.helper cache

추가 옵션을 지정할 수 있는 Helper도 있다. “store” Helper는 --file <path> 옵션을 사용하여 인증정보를 저장할 텍스트 파일의 위치를 지정한다. 기본 값은 ~/.git-credentials 이다. “cache” Helper는 --timeout <seconds> 옵션을 사용하여 언제까지 인증정보를 메모리에 유지할지 설정한다. 기본 값은 “900” 초로 15분이다. 기본 경로가 아닌 다른 경로를 지정해서 인증정보를 저장하려면 아래와 같이 실행한다.

   $ git config --global credential.helper 'store --file ~/.my-credentials'

Helper를 여러개 섞어서 쓸 수도 있다. 인증이 필요한 리모트에 접근할 때 Git은 인증정보를 찾는데 Helper가 여러개 있으면 순서대로 찾는다. 반대로 인증정보를 저장할 때는 설정한 모든 모드에 저장한다. 아래 예제는 첫 번째 Path에 대해 인증정보를 읽거나 저장에 실패하면 두 번째 모드에 따라 메모리에서만 인증정보를 유지한다.

   [credential]
       helper = store --file /mnt/thumbdrive/.git-credentials
       helper = cache --timeout 30000

뚜껑을 열어보면

실제로는 어떻게 동작하는지 살펴보자. Git의 Credential-Helper 시스템의 기본 명령은 git credential이다. 이 명령이 하위 명령이나 옵션, 표준입력으로 필요한 정보를 입력받아 전달한다.

이 과정은 예제를 통해 이해하는 편이 쉽다. Credential Helper를 사용하도록 설정하고 mygithost 라는 호스트의 인증정보가 저장된 상태이다. 아래 예제는 “fill” 명령으로 Git이 특정 호스트에 대한 인증정보를 얻으려는 과정을 보여준다.

   $ git credential fill (1)
   protocol=https (2)
   host=mygithost (3)
   protocol=https (4)
   host=mygithost
   username=bob
   password=s3cre7
   $ git credential fill (5)
   protocol=https
   host=unknownhost

   Username for 'https://unknownhost': bob
   Password for 'https://bob@unknownhost':
   protocol=https
   host=unknownhost
   username=bob
   password=s3cre7
  1. 이 명령으로 인증정보를 얻어오는 과정을 시작한다.

  2. 이제 Git-credential 명령은 표준 입력으로 사용자의 입력을 기다린다. 인증정보가 필요한 대상의 프로토콜과 호스트이름을 입력한다.

  3. 빈 라인을 하나 입력하면 입력이 끝났다는 것을 의미한다. 이제 입력한 내용에 해당하는 인증정보를 응답해야 한다.

  4. Git-credential 명령이 전달받은 내용으로 인증정보를 찾아보고 찾으면 표준출력으로 찾은 정보를 응답한다.

  5. 물론 요청에 대한 인증정보가 없을 수도 있다. 이렇게 되면 Git이 사용자 이름과 비밀번호를 사용자가 입력하도록 메시지를 띄우고 값도 입력받는다. 입력받은 값을 다시 표준출력으로 응답한다.

이 Credential 시스템은 사실 Git과 분리된 독립적인 프로그램을 실행시켜 동작한다. 어떤 프로그램을 실행시킬지는 credential.helper 설정 값에 따른다. 이 설정 값을 아래와 같이 설정한다.

설정 값결과

foo

git-credential-foo 실행

foo -a --opt=bcd

git-credential-foo -a --opt=bcd 실행

/absolute/path/foo -xyz

/absolute/path/foo -xyz 실행

!f() { echo "password=s3cre7"; }; f

! 뒤의 코드를 쉘에서 실행

따라서 위에서 살펴본 여러 Helper는 사실 git-credential-cachegit-credential-store 같은 명령이다. 설정을 통해 이 명령들이 옵션이나 하위 명령을 받아서 실행하게 한다. 이 명령의 일반적인 형태는 “git-credential-foo [args] <action>” 이다. git-credential 명령과 마찬가지로 표준입력/표준출력을 프로토콜로 사용하지만 처리하는 액션(하위 명령)은 아래와 같이 다소 다르다.

  • get - 사용자 이름과 비밀번호를 요구하는 액션

  • store - Helper에서 인증정보를 저장하는 액션

  • erase - Helper에서 인증정보를 삭제하는 액션

store`나 `erase 액션은 따로 결과를 출력할 필요가 없다(Git은 결과를 무시). get 액션의 결과는 Git이 주의 깊게 관찰해서 가져다 사용하므로 매우 중요하다. Helper는 전달받은 내용으로 인증정보를 찾고 저장된 인증정보가 없다면 아무런 결과도 출력하지 않고 종료하면 된다. 적당한 인증정보를 찾았을 때는 전달받은 내용에 찾은 인증정보를 추가하여 결과로 응답한다. 결과는 몇 라인의 할당 구문으로 구성하며, Git은 이 결과를 받아서 사용한다.

아래 예제는 위에서 살펴본 예제와 같은 내용으로 git-crediential 명령 대신 git-credential-store 명령을 직접 사용한다.

   $ git credential-store --file ~/git.store store (1)
   protocol=https
   host=mygithost
   username=bob
   password=s3cre7
   $ git credential-store --file ~/git.store get (2)
   protocol=https
   host=mygithost

   username=bob (3)
   password=s3cre7
  1. git-credential-store Helper에게 인증정보를 저장하도록 한다. 저장할 인증정보는 사용자 이름은 “bob”, 비밀번호는 ‘`s3cre7’'를 저장하는데 프로토콜과 호스트가 https://mygithost 일 때 사용한다.

  2. 저장한 인증정보를 가져온다. 이미 아는 https://mygithost 리모트 주소를 호스트와 프로토콜로 나누어 표준입력으로 전달하고 한 라인을 비운다.

  3. git-credential-store 명령은 <1>에서 저장한 사용자 이름과 비밀번호를 표준출력으로 응답한다.

~/git.store 파일의 내용은 사실 아래와 같다.

   https://bob:s3cre7@mygithost

단순한 텍스트파일로 인증정보가 포함된 URL 형태로 저장한다. osxkeychain`이나 `wincred Helper를 사용하면 OS에서 제공하는 좀 더 안전한 저장소에 인증정보를 저장한다. cache Helper의 경우 나름의 형식으로 메모리에 인증정보를 저장하고 다른 프로세스에서는 (메모리의 내용을) 읽어갈 수 없다.

맞춤 Credential 캐시

`git-credential-store`나 다른 명령도 독립된 프로그램이다. 아무 스크립트나 프로그램도 Git Credential Helper가 될 수 있다. 이미 Git이 제공하는 Helper로도 충분하지만 모든 경우를 커버하지 않는다. 예를 들어 어떤 인증정보는 팀 전체가 공유해야 한다. 배포에 사용하는 인증정보가 그렇다. 이 인증정보는 공유하는 디렉토리에 저장해두고 사용한다. 이 인증정보는 자주 변경되기 때문에 로컬 Credential 저장소에 저장하지 않고 사용하고자 한다. 이런 경우라면 Git이 제공하는 Helper로는 부족하며 자신만의 맞춤 Helper가 필요하다. 맞춤 Helper는 아래와 같은 기능을 제공해야 한다.

  1. 새 맞춤 Helper가 집중해야 할 액션은 get 뿐이다. store`나 `erase 액션은 저장하는 기능이기 때문에 이 액션을 받으면 깔끔하게 바로 종료한다.

  2. 공유하는 Credential 파일은 git-credential-store 명령이 저장하는 형식과 같은 형식을 사용한다.

  3. Credential 파일의 위치는 기본 값을 사용해도 되지만 파일 경로를 넘길 수 있다.

예제로 보여주는 맞춤 Helper도 Ruby로 작성한다. 하지만, 다른 어떤 언어를 사용해도 Git이 실행할 수만 있다면 상관없다. 작성한 저장소 Helper의 소스코드는 아래와 같다.

   #!/usr/bin/env ruby

   require 'optparse'

   path = File.expand_path '~/.git-credentials' (1)
   OptionParser.new do |opts|
       opts.banner = 'USAGE: git-credential-read-only [options] <action>'
       opts.on('-f', '--file PATH', 'Specify path for backing store') do |argpath|
           path = File.expand_path argpath
       end
   end.parse!

   exit(0) unless ARGV[0].downcase == 'get' (2)
   exit(0) unless File.exists? path

   known = {} (3)
   while line = STDIN.gets
       break if line.strip == ''
       k,v = line.strip.split '=', 2
       known[k] = v
   end

   File.readlines(path).each do |fileline| (4)
       prot,user,pass,host = fileline.scan(/^(.*?):\/\/(.*?):(.*?)@(.*)$/).first
       if prot == known['protocol'] and host == known['host'] and user == known['username'] then
           puts "protocol=#{prot}"
           puts "host=#{host}"
           puts "username=#{user}"
           puts "password=#{pass}"
           exit(0)
       end
   end
  1. 우선 명령 옵션을 처리한다. 옵션으로는 Credential 파일명이 들어온다. 기본 값은 ~/.git-credentials 이다.

  2. Helper 프로그램은 get 액션만 처리하며 Credential 파일이 존재하는 경우만 처리한다.

  3. 이후에는 빈 라인이 나타날 때까지 표준입력으로부터 한 줄 한 줄 읽는다. 각 라인을 파싱하여 known 해시에 저장하고 <4>의 응답에서 사용한다.

  4. 이 루프에서 Credential 파일을 읽어서 <3>의 해시에 해당하는 정보를 찾는다. known 해시에서 프로토콜과 호스트 정보가 일치하는 경우 사용자 이름과 비밀번호를 포함하여 결과를 출력한다.

이 파일을 `git-credential-read-only`로 저장하고 `PATH`에 등록된 디렉토리 중 하나에 위치시키고 실행 권한을 부여한다. Helper를 실행하면 아래와 같다.

   $ git credential-read-only --file=/mnt/shared/creds get
   protocol=https
   host=mygithost

   protocol=https
   host=mygithost
   username=bob
   password=s3cre7

위에서 저장한 파일 이름이 “git-” 으로 시작하기 때문에 아래와 같이 간단한 이름으로 설정해서 사용할 수 있다.

   $ git config --global credential.helper 'read-only --file /mnt/shared/creds'

이렇게 살펴본 대로 Credential 저장소를 필요에 따라 맞춤 프로그램을 작성해서 확장하는 것이 어렵지 않다. 스크립트를 만들어 사용자나 팀의 가려운 부분을 긁어줄 수 있다.


'Programming > Git' 카테고리의 다른 글

Git_#8.2_Git맞춤 - Git Attributes  (0) 2017.04.13
Git_#8.1_Git맞춤 - Git 설정하기  (0) 2017.04.13
Git_#7.13_Git 도구 - Replace  (0) 2017.04.13
Git_#7.12_Git 도구 - Bundle  (0) 2017.04.13
Git_#7.11_Git 도구 - 서브모듈  (0) 2017.04.13
posted by REDFORCE 2017. 4. 13. 02:18

출처 : git-scm.com


7.13 Git 도구 - Replace

Replace

히스토리(혹은 데이터베이스)에 일단 저장한 Git의 개체는 기본적으로 변경할 수 없다. 하지만, 변경된 것처럼 보이게 하는 재밌는 기능이 숨어 있다.

Git의 replace 명령은 "어떤 개체를 읽을 때 항상 다른 개체로 보이게" 한다. 히스토리에서 어떤 커밋이 다른 커밋처럼 보이도록 할 때 이 명령이 유용하다.

예를 들어 현재 프로젝트의 히스토리가 아주 방대한 상태다. 히스토리를 둘로 나누어서 새로 시작하는 개발자에게는 히스토리를 아주 간단한 몇 개의 커밋으로 만들어서 제공하고, 프로젝트 히스토리를 분석할 사람에게는 전체 히스토리를 제공하는 상황을 생각해보자. replace 명령으로 간단해진 히스토리를 전체 히스토리의 마지막 부분에 연결해서 사용할 수 있다. 이렇게 히스토리를 변경하는 데도 커밋을 새로 쓰지 않는 매우 훌륭한 기능이다(Rebase를 생각해보면 한 부모를 변경하면 이후의 커밋은 모두 재작성된다).

위와 같은 상황을 한번 해보자. 히스토리가 어느 정도 쌓여 있는 Git 저장소를 두 저장소로 분리해서 하나는 최신 커밋 몇 개만 유지하도록 하고 다른 하나는 전체 히스토리를 유지하기로 한다. 이렇게 분리한 두 히스토리를 커밋을 재작성하지 않고 replace 명령을 사용하여 연결한다.

아래 예제로 사용하는 저장소는 히스토리에 커밋 5개가 있다.

   $ git log --oneline
   ef989d8 fifth commit
   c6e1e95 fourth commit
   9c68fdc third commit
   945704c second commit
   c1822cf first commit

예제의 히스토리를 둘로 나누어보자. 하나는 첫 번째부터 네 번째 커밋까지 히스토리로 만들어 원래의 히스토리를 그대로 유지한다. 다른 새 히스토리는 네 번째 커밋과 다섯 번째 커밋만을 포함하도록 한다.

replace1

원래의 히스토리를 유지하는 히스토리를 만들기는 쉽다. 원래 히스토리 상에 기준점을 잡아 새 브랜치를 만들고 히스토리를 유지할 리모트 저장소로 Push 하면 간단히 해결된다.

   $ git branch history c6e1e95
   $ git log --oneline --decorate
   ef989d8 (HEAD, master) fifth commit
   c6e1e95 (history) fourth commit
   9c68fdc third commit
   945704c second commit
   c1822cf first commit
replace2

history 브랜치를 새 저장소에 master 브랜치로 Push 한다.

   $ git remote add project-history https://github.com/schacon/project-history
   $ git push project-history history:master
   Counting objects: 12, done.
   Delta compression using up to 2 threads.
   Compressing objects: 100% (4/4), done.
   Writing objects: 100% (12/12), 907 bytes, done.
   Total 12 (delta 0), reused 0 (delta 0)
   Unpacking objects: 100% (12/12), done.
   To git@github.com:schacon/project-history.git
    * [new branch]      history -> master

원래 히스토리를 유지하는 히스토리를 Push 했다. 이제 남은 어려운 부분은 최신 커밋만 유지하도록 히스토리를 중간에 끊고 새로 만드는 작업이다. 새로 만든 히스토리와 원래 히스토리를 나중에 연결해서 사용할 때 네 번째 커밋을 연결하도록 작업한다. 따라서 새로 만든 히스토리는 네 번째 이후의 커밋만 유지한다.

   $ git log --oneline --decorate
   ef989d8 (HEAD, master) fifth commit
   c6e1e95 (history) fourth commit
   9c68fdc third commit
   945704c second commit
   c1822cf first commit

이런 예제 같은 경우 히스토리를 어떻게 연결하는지 설명하는 커밋을 만들어 나중에 개발자든 누구든 전체 히스토리를 볼 수 있도록 하는 것이 좋다. 이런 내용과 함께 네 번째 커밋 이전의 상태를 담을 새 커밋을 하나 만들고 네 번째 이후 커밋을 이 새 커밋 위에 Rebase 하기로 한다.

기준으로 삼을 커밋을 선택하고 새 커밋을 만든다. 예제는 9c68fdc 해시 값을 갖는 세 번째 커밋이 된다. 세 번째 커밋의 트리 내용을 기본 상태로 삼고 네 번째 이후 커밋을 히스토리에 쌓는다. commit-tree 명령을 사용해서 새 커밋을 만든다. 명령에 트리 개체를 전달하면 부모 없는 새 커밋을 생성하여 해시 값을 반환한다.

   $ echo 'get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
   622e88e9cbfbacfb75b5279245b9fb38dfea10cf
Note

commit-tree 명령은 Plumbing 명령 중 하나다. 저수준 명령은 일반적으로 직접 사용할 일이 없다. 주로 사용하는 고수준 Git 명령이 하는 작업을 잘게 쪼개어 수행할 때 사용한다. 이 책에서 위의 예제처럼 특별한 작업을 위해 간혹 저수준 명령을 사용하긴 하지만 매일같이 사용하지는 않는다. 다른 여러 저수준 명령을 사용하는 예제는 Plumbing 명령과 Porcelain 명령에서 확인할 수 있다.

replace3

이제 네 번째 커밋 이후의 히스토리를 쌓을 커밋이 준비됐다. git rebase --onto 명령으로 네 번째 이후의 커밋을 새 커밋에 Rebase 한다. --onto 옵션 뒤에 전달할 커밋은 쌓아올릴 대상이 되는 커밋을 입력한다. 위에서 commit-tree 명령으로 반환받은 커밋을 사용하고 Rebase의 기준은 네 번째 커밋의 부모 커밋, 즉 세 번째 커밋인 9c68fdc 해시를 전달한다.

   $ git rebase --onto 622e88 9c68fdc
   First, rewinding head to replay your work on top of it...
   Applying: fourth commit
   Applying: fifth commit
replace4

위와 같이 Rebase 하고 나면 최신 커밋만 유지하는 새로운 히스토리가 만들어진다. 새 히스토리의 가장 첫 번째 커밋에는 어떻게 이전 히스토리를 연결해서 확인할 수 있는지 설명하는 내용이 포함되게 된다. 이렇게 생성한 새 히스토리를 새 리모트 저장소로 Push 한다. 그리고 나서 Clone 해서 히스토리를 살펴보면 가장 최근 커밋 몇 개만 보이고 가장 첫 커밋에는 히스토리를 연결하는 내용이 있게 된다.

이제 역할을 바꾸어 새 히스토리를 Clone 하고 전체 히스토리까지 확인하고자 하는 작업을 예로 들어보자. 원래 히스토리로부터 분리한 새 히스토리 위에서 원래 히스토리를 확인하려면 우선 원래 히스토리를 포함하는 리모트 저장소를 추가하고 히스토리를 Fetch 한다.

   $ git clone https://github.com/schacon/project
   $ cd project

   $ git log --oneline master
   e146b5f fifth commit
   81a708d fourth commit
   622e88e get history from blah blah blah

   $ git remote add project-history https://github.com/schacon/project-history
   $ git fetch project-history
   From https://github.com/schacon/project-history
    * [new branch]      master     -> project-history/master

위와 같이 실행하고 나면 master 브랜치에는 간단한 히스토리의 최신 커밋만 있다. 그리고 project-history/master 브랜치에는 원래 히스토리 전체가 있게 된다.

   $ git log --oneline master
   e146b5f fifth commit
   81a708d fourth commit
   622e88e get history from blah blah blah

   $ git log --oneline project-history/master
   c6e1e95 fourth commit
   9c68fdc third commit
   945704c second commit
   c1822cf first commit

이 두 히스토리를 연결하기 위해 git replace 명령을 사용하여 새 히스토리의 커밋이 원래 히스토리에 속한 커밋을 가리키도록 할 수 있다. 예제에서는 새 히스토리의 'fourth commit’과 project-history/master 브랜치의 'fourth commit’을 파라미터로 전달한다.

   $ git replace 81a708d c6e1e95

이제 master 브랜치에서 히스토리를 조회해보면 아래와 같은 히스토리가 된다.

   $ git log --oneline master
   e146b5f fifth commit
   81a708d fourth commit
   9c68fdc third commit
   945704c second commit
   c1822cf first commit

히스토리가 그럴듯하다. 연결한 네 번째 커밋 이후의 커밋을 재작성하지 않고도 replace 명령으로 간단하게 히스토리를 변경했다. 변경한 히스토리에서도 bisect`나 `blame 같은 다른 Git 명령을 사용할 수 있다.

replace5

연결된 히스토리를 보면 replace 명령으로 커밋을 변경했음에도 여전히 c6e1e95 해시가 아니라 81a708d해시로 나오는 것을 확인할 수 있다. 반면 cat-file 명령으로 보면 c6e1e95 해시의 내용이 출력된다.

   $ git cat-file -p 81a708d
   tree 7bc544cf438903b65ca9104a1e30345eee6c083d
   parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252
   author Scott Chacon <schacon@gmail.com> 1268712581 -0700
   committer Scott Chacon <schacon@gmail.com> 1268712581 -0700

   fourth commit

Replace 이전 네 번째 커밋 81a708d 해시의 부모는 622e88e 해시이므로 위의 9c68fdce`로 나오는 내용은 변경한 대상인 `c6e1e95 해시의 내용이다.

이렇게 히스토리를 연결하는 것 같은 Replace 명령의 결과는 Refs로 관리한다.

   $ git for-each-ref
   e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/heads/master
   c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/remotes/history/master
   e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/HEAD
   e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/master
   c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/replace/81a708dd0e167a3f691541c7a6463343bc457040

Replace 내용을 Refs로 관리한다는 말은 손쉽게 이 내용을 서버로 Push 하여 다른 팀원과 공유할 수 있다는 것을 뜻한다. 이렇게 Replace하는 것이 유용하지 않을 수도 있다. 어쨌든 모든 팀원이 두 히스토리를 다운로드해야 하는데 굳이 나눠야 하나? 하지만, 어떨 때는 Replace하는 것이 유용할 수도 있다.


posted by REDFORCE 2017. 4. 13. 02:16

출처 : git-scm.com


7.12 Git 도구 - Bundle

Bundle

앞에서 Git 데이터를 네트워크를 거쳐 전송하는 일반적인 방법(HTTP, SSH등)을 다루었었다. 일반적으로 사용하진 않지만, 꽤 유용한 방법이 하나 더 있다.

Git에는 ‘`Bundle’'이란 것이 있다. 데이터를 한 파일에 몰아넣는 것이다. 이 방법은 다양한 경우 유용하게 사용할 수 있다. 예를 들어 네트워크가 불통인데 변경사항을 동료에게 보낼 때, 출장을 나갔는데 보안상의 이유로 로컬 네트워크에 접속하지 못할 때, 통신 인터페이스 장비가 고장났을 때, 갑자기 공용 서버에 접근하지 못할 때, 누군가에게 수정사항을 이메일로 보내야 하는데 40개 씩이나 되는 커밋을 `format-patch`로 보내고 싶지 않을 때를 예로 들 수 있다.

바로 이럴 때 git bundle`이 한 줄기 빛이 되어준다. `bundle 명령은 보통 `git push`명령으로 올려 보낼 모든 것을 감싸서 한 바이너리 파일로 만든다. 이 파일을 이메일로 보내거나 USB로 다른 사람에게 보내서 다른 저장소에 풀어서(Unbundle) 사용한다.

간단한 예제를 보자. 이 저장소에는 커밋 두 개가 있다.

$ git log
commit 9a466c572fe88b195efd356c3f2bbeccdb504102
Author: Scott Chacon <schacon@gmail.com>
Date:   Wed Mar 10 07:34:10 2010 -0800

    second commit

commit b1ec3248f39900d2a406049d762aa68e9641be25
Author: Scott Chacon <schacon@gmail.com>
Date:   Wed Mar 10 07:34:01 2010 -0800

    first commit

이 저장소를 다른 사람에게 통째로 보내고 싶은데 그 사람의 저장소에 Push 할 권한이 없거나, 그냥 Push 하고 싶지 않을 때, git bundle create 명령으로 Bundle을 만들 수 있다.

$ git bundle create repo.bundle HEAD master
Counting objects: 6, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (6/6), 441 bytes, done.
Total 6 (delta 0), reused 0 (delta 0)

이렇게 repo.bundle 이라는 이름의 파일을 생성할 수 있다. 이 파일에는 이 저장소의 master`브랜치를 다시 만드는 데 필요한 모든 정보가 다 들어 있다. `bundle 명령으로 모든 Refs를 포함하거나 Bundle에 포함할 특정 구간의 커밋을 지정할 수 있다. 이 Bundle을 다른 곳에서 Clone 하려면 위의 명령처럼 HEAD Refs를 포함해야 한다.

repo.bundle 파일을 다른 사람에게 이메일로 전송하거나 USB 드라이브에 담아서 나갈 수 있다.

혹은 repo.bundle 파일을 일할 곳으로 어떻게든 보내놓으면 이 Bundle 파일을 마치 URL에서 가져온 것처럼 Clone 해서 사용할 수 있다.

$ git clone repo.bundle repo
Cloning into 'repo'...
...
$ cd repo
$ git log --oneline
9a466c5 second commit
b1ec324 first commit

Bundle 파일에 HEAD Refs를 포함하지 않으려면 -b master 옵션을 써주거나 포함시킬 브랜치를 지정해줘야 한다. 그렇지 않으면 Git은 어떤 브랜치로 Checkout 할지 알 수 없다.

이제 새 커밋 세 개를 추가해서 채운 저장소를 다시 원래 Bundle을 만들었던 저장소로 USB든 메일이든 Bundle로 보내 새 커밋을 옮겨보자.

$ git log --oneline
71b84da last commit - second repo
c99cf5b fourth commit - second repo
7011d3d third commit - second repo
9a466c5 second commit
b1ec324 first commit

먼저 Bundle 파일에 추가시킬 커밋의 범위를 정해야 한다. 전송할 최소한의 데이터를 알아서 인식하는 네트워크 프로토콜과는 달리 Bundle 명령을 사용할 때는 수동으로 지정해야 한다. 전체 저장소를 Bundle 파일로 만들 수도 있지만, 차이점만 Bundle로 묶는 게 좋다. 예제에서는 로컬에서 만든 세 개의 커밋만 묶는다.

우선 차이점을 찾아내야 Bundle 파일을 만들 수 있다. 범위로 커밋 가리키기에서 살펴본 대로 숫자를 사용하여 커밋의 범위를 지정할 수 있다. 원래 Clone 한 브랜치인 master에는 없던 세 개의 커밋을 얻어내려면 origin/master..master 또는 master ^origin/master 파라미터를 쓰면 된다. log 명령으로 시험해볼 수 있다.

$ git log --oneline master ^origin/master
71b84da last commit - second repo
c99cf5b fourth commit - second repo
7011d3d third commit - second repo

이제 Bundle 파일에 포함할 커밋을 얻었으니 묶어보자. git bundle create 명령에 Bundle 파일의 이름과 묶어 넣을 커밋의 범위를 지정한다.

$ git bundle create commits.bundle master ^9a466c5
Counting objects: 11, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (9/9), 775 bytes, done.
Total 9 (delta 0), reused 0 (delta 0)

이제 디렉토리에 commits.bundle 파일이 생겼다. 이 파일을 동료에게 보내면 원래의 저장소에 일이 얼마나 진행되었든 간에 파일 내용을 적용할 수 있다.

이 Bundle 파일을 동료가 받았으면 원래 저장소에 적용하기 전에 무엇이 들어 있는지 살펴볼 수 있다. 우선 bundle verify 명령으로 파일이 올바른 Git Bundle인가, 제대로 적용하는 데 필요한 모든 히스토리가 현재 저장소에 있는가 확인한다.

$ git bundle verify ../commits.bundle
The bundle contains 1 ref
71b84daaf49abed142a373b6e5c59a22dc6560dc refs/heads/master
The bundle requires these 1 ref
9a466c572fe88b195efd356c3f2bbeccdb504102 second commit
../commits.bundle is okay

만약 앞에서 Bundle 파일을 만들 때 커밋 세 개로 만들지 않고 마지막 두 커밋으로만 Bundle 파일을 만들면 커밋이 모자라기 때문에 최초에 Bundle을 만들었던 저장소에 새 Bundle 파일을 합칠 수 없다. 이런 문제를 verify 명령으로 확인할 수 있다.

$ git bundle verify ../commits-bad.bundle
error: Repository lacks these prerequisite commits:
error: 7011d3d8fc200abe0ad561c011c3852a4b7bbe95 third commit - second repo

제대로 만든 Bundle 파일이라면 커밋을 가져와서 최초 저장소에 합칠 수 있다. 데이터를 가져올 Bundle 파일에 어떤 브랜치를 포함하고 있는지 살펴보려면 아래와 같은 명령으로 확인할 수 있다.

$ git bundle list-heads ../commits.bundle
71b84daaf49abed142a373b6e5c59a22dc6560dc refs/heads/master

앞에서 verify 명령을 실행했을 때도 브랜치 정보를 확인할 수 있다. 여기서 중요하게 짚을 부분은 fetch 명령이나 pull 명령으로 가져올 대상이 되는 브랜치를 Bundle 파일에서 확인하는 것이다. 예를 들어 Bundle 파일의 master 브랜치를 작업하는 저장소의 other-master 브랜치로 가져오는 명령은 아래와 같이 실행한다.

$ git fetch ../commits.bundle master:other-master
From ../commits.bundle
 * [new branch]      master     -> other-master

이런 식으로 작업하던 저장소의 master 브랜치에 어떤 작업을 했든 상관없이 Bundle 파일로부터 커밋을 독립적으로 other-master 브랜치로 가져올 수 있다.

$ git log --oneline --decorate --graph --all
* 8255d41 (HEAD, master) third commit - first repo
| * 71b84da (other-master) last commit - second repo
| * c99cf5b fourth commit - second repo
| * 7011d3d third commit - second repo
|/
* 9a466c5 second commit
* b1ec324 first commit

git bundle 명령으로 데이터를 전송할 네트워크 상황이 여의치 않거나 쉽게 공유할 수 있는 저장소를 준비하기 어려울 때도 히스토리를 쉽게 공유할 수 있다.


posted by REDFORCE 2017. 4. 13. 02:15

출처 : git-scm.com


7.11 Git 도구 - 서브모듈

서브모듈

프로젝트를 수행하다 보면 다른 프로젝트를 함께 사용해야 하는 경우가 종종 있다. 함께 사용할 다른 프로젝트는 외부에서 개발한 라이브러리라던가 내부 여러 프로젝트에서 공통으로 사용할 라이브러리일 수 있다. 이런 상황에서 자주 생기는 이슈는 두 프로젝트를 서로 별개로 다루면서도 그 중 하나를 다른 하나 안에서 사용할 수 있어야 한다는 것이다.

Atom 피드를 제공하는 웹사이트를 만드는 것을 예로 들어보자. Atom 피드를 생성하는 코드는 직접 작성하지 않고 라이브러리를 가져다 쓰기로 한다. 라이브러리를 사용하려면 CPAN이나 Ruby gem 같은 라이브러리 관리 도구를 사용하여 Shared 라이브러리 형태로 쓰거나 직접 라이브러리의 소스코드를 프로젝트로 복사해서 사용할 수 있다. 우선 Shared 라이브러리를 사용하기에는 문제가 있다. 프로젝트를 사용하는 모든 환경에 라이브러리가 설치되어 있어야 하고 라이브러리를 프로젝트에 맞게 약간 수정해서 사용하고 배포하기가 어렵다. 또한, 라이브러리 소스코드를 직접 프로젝트에 포함해서 사용하고 배포하는 경우에는 라이브러리 Upstream 코드가 업데이트됐을 때 Merge 하기가 어렵다는 문제다.

Git의 서브모듈은 이런 문제를 다루는 도구다. Git 저장소 안에 다른 Git 저장소를 디렉토리로 분리해 넣는 것이 서브모듈이다. 다른 독립된 Git 저장소를 Clone 해서 내 Git 저장소 안에 포함할 수 있으며 각 저장소의 커밋은 독립적으로 관리한다.

서브모듈 시작하기

예제로 하위 프로젝트 여러 개를 가지는 프로젝트를 하나 만들어 서브모듈의 기능을 살펴보자.

작업할 Git 저장소에 미리 준비된 리모트 Git 저장소를 서브모듈로 추가해보자. 서브모듈을 추가하는 명령으로 git submodule add 뒤에 추가할 저장소의 URL을 붙여준다. 이 URL은 절대경로도 되고 상대경로도 된다. 예제로 “DbConnector” 라는 라이브러리를 추가한다.

$ git submodule add https://github.com/chaconinc/DbConnector
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

기본적으로 서브모듈은 프로젝트 저장소의 이름으로 디렉토리를 만든다. 예제에서는 ‘`DbConnector’'라는 이름으로 만든다. 명령의 마지막에 원하는 이름을 넣어 다른 디렉토리 이름으로 서브모듈을 추가할 수도 있다.

서브보듈을 추가하고 난 후 git status 명령을 실행하면 몇 가지 정보를 알 수 있다.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitmodules
    new file:   DbConnector

우선 .gitmodules 파일이 만들어졌다. 이 파일은 서브디렉토리와 하위 프로젝트 URL의 매핑 정보를 담은 설정파일이다.

[submodule "DbConnector"]
    path = DbConnector
    url = https://github.com/chaconinc/DbConnector

서브모듈 개수만큼 이 항목이 생긴다. 이 파일도 .gitignore 파일처럼 버전을 관리한다. 다른 파일처럼 Push 하고 Pull 한다. 이 프로젝트를 Clone 하는 사람은 .gitmodules 파일을 보고 어떤 서브모듈 프로젝트가 있는지 알 수 있다.

Note
gitmodules 파일에 있는 URL은 조건에 맞는 사람이면 누구든지 Clone 하고 Fetch 할 수 있도록 접근할 수 있어야 한다.

예를 들어 다른 사람이 Pull을 하는 URL과 라이브러리의 작업을 Push 하는 URL이 서로 다른 상황이라면 Pull URL이 모든 사람에게 접근 가능한 URL이어야 한다. 이러면 서브모듈 URL 설정을 덮어쓰기 해서 사용할 수 있는데 git config submodule.DbConnector.url PRIVATE_URL 명령으로 다른 사람과는 다른 서브모듈 URL을 사용할 수 있다. URL을 상대경로로 적을 수 있으면 상대경로를 사용하는 것이 낫다.

.gitmodules`은 살펴봤고 이제 프로젝트 폴더에 대해 살펴보자. `git diff 명령을 실행시키면 흥미로운 점을 발견할 수 있다.

$ git diff --cached DbConnector
diff --git a/DbConnector b/DbConnector
new file mode 160000
index 0000000..c3f01dc
--- /dev/null
+++ b/DbConnector
@@ -0,0 +1 @@
+Subproject commit c3f01dc8862123d317dd46284b05b6892c7b29bc

Git은 DbConnector 디렉토리를 서브모듈로 취급하기 때문에 해당 디렉토리 아래의 파일 수정사항을 직접 추적하지 않는다. 대신 서브모듈 디렉토리를 통째로 특별한 커밋으로 취급한다.

git diff`에 `--submodule 옵션을 더하면 서브모듈에 대해 더 자세히 나온다.

$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..71fc376
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "DbConnector"]
+       path = DbConnector
+       url = https://github.com/chaconinc/DbConnector
Submodule DbConnector 0000000...c3f01dc (new submodule)

이제 하위 프로젝트를 포함한 커밋을 생성하면 아래와 같은 결과를 확인할 수 있다.

$ git commit -am 'added DbConnector module'
[master fb9093c] added DbConnector module
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 DbConnector

DbConnector 디렉토리의 모드는 `160000`이다. Git에게 있어 160000 모드는 일반적인 파일이나 디렉토리가 아니라 특별하다는 의미다.

서브모듈 포함한 프로젝트 Clone

서브모듈을 포함하는 프로젝트를 Clone 하는 예제를 살펴본다. 이런 프로젝트를 Clone 하면 기본적으로 서브모듈 디렉토리는 빈 디렉토리이다.

$ git clone https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
$ cd MainProject
$ ls -la
total 16
drwxr-xr-x   9 schacon  staff  306 Sep 17 15:21 .
drwxr-xr-x   7 schacon  staff  238 Sep 17 15:21 ..
drwxr-xr-x  13 schacon  staff  442 Sep 17 15:21 .git
-rw-r--r--   1 schacon  staff   92 Sep 17 15:21 .gitmodules
drwxr-xr-x   2 schacon  staff   68 Sep 17 15:21 DbConnector
-rw-r--r--   1 schacon  staff  756 Sep 17 15:21 Makefile
drwxr-xr-x   3 schacon  staff  102 Sep 17 15:21 includes
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 scripts
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 src
$ cd DbConnector/
$ ls
$

분명히 DbConnector 디렉토리는 있지만 비어 있다. 서브모듈에 관련된 두 명령을 실행해야 완전히 Clone 과정이 끝난다. 먼저 git submodule init 명령을 실행하면 서브모듈 정보를 기반으로 로컬 환경설정 파일이 준비된다. 이후 git submodule update 명령으로 서브모듈의 리모트 저장소에서 데이터를 가져오고 서브모듈을 포함한 프로젝트의 현재 스냅샷에서 Checkout 해야 할 커밋 정보를 가져와서 서브모듈 프로젝트에 대한 Checkout을 한다.

$ git submodule init
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
$ git submodule update
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

DbConnector 디렉토리는 마지막으로 커밋을 했던 상태로 복원된다.

하지만, 같은 과정을 더 간단하게 실행하는 방법도 있다. 메인 프로젝트를 Clone 할 때 git clone 명령 뒤에 --recursive 옵션을 붙이면 서브모듈을 자동으로 초기화하고 업데이트한다.

$ git clone --recursive https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

서브모듈 포함한 프로젝트 작업

이제 프로젝트에 포함된 서브모듈의 저장소 데이터와 코드도 다 받아왔다. 메인 프로젝트와 서브모듈 프로젝트를 오가며 팀원과 협업할 준비가 되었다.

서브모듈 업데이트하기

가장 단순한 서브모듈 사용 방법은 하위 프로젝트를 수정하지 않고 참조만 하면서 최신 버전으로 업데이트하는 것이다. 간단한 예제로 이 경우를 살펴본다.

서브모듈 프로젝트를 최신으로 업데이트하려면 서브모듈 디렉토리에서 git fetch 명령을 실행하고 git merge 명령으로 Upstream 브랜치를 Merge한다.

$ git fetch
From https://github.com/chaconinc/DbConnector
   c3f01dc..d0354fc  master     -> origin/master
$ git merge origin/master
Updating c3f01dc..d0354fc
Fast-forward
 scripts/connect.sh | 1 +
 src/db.c           | 1 +
 2 files changed, 2 insertions(+)

메인 프로젝트로 돌아와서 git diff --submodule 명령을 실행하면 업데이트된 서브모듈과 각 서브모듈에 추가된 커밋을 볼 수 있다. 매번 --submodule 옵션을 쓰고 싶지 않다면 ‘diff.submodule`의 값을 ``log’'로 설정하면 된다.

$ git config --global diff.submodule log
$ git diff
Submodule DbConnector c3f01dc..d0354fc:
  > more efficient db routine
  > better connection routine

여기서 커밋하면 서브모듈은 업데이트된 내용으로 메인 프로젝트에 적용된다. 다른 사람들이 업데이트하면 적용된다.

서브모듈을 최신으로 업데이트하는 더 쉬운 방법도 있다. 서브모듈 디렉토리에서 Fetch 명령과 Merge 명령을 실행하지 않아도 git submodule update --remote 명령을 실행하면 Git이 알아서 서브모듈 프로젝트를 Fetch 하고 업데이트한다.

$ git submodule update --remote DbConnector
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   3f19983..d0354fc  master     -> origin/master
Submodule path 'DbConnector': checked out 'd0354fc054692d3906c85c3af05ddce39a1c0644'

이 명령은 기본적으로 서브모듈 저장소의 master 브랜치를 Checkout 하고 업데이트를 수행한다. 업데이트할 대상 브랜치를 원하는 브랜치로 바꿀 수 있다. 예를 들어 DbConnector 서브모듈 저장소에서 업데이트할 대상 브랜치를 “stable''로 바꾸고 싶다면 .gitmodules 파일에 설정하거나(이 파일을 공유하는 모두에게 ``stable” 브랜치가 적용됨) 개인 설정 파일인 .git/config 파일에 설정한다. .gitmodules 파일에 설정하는 방법을 알아보자.

$ git config -f .gitmodules submodule.DbConnector.branch stable

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   27cf5d3..c87d55d  stable -> origin/stable
Submodule path 'DbConnector': checked out 'c87d55d4c6d4b05ee34fbc8cb6f7bf4585ae6687'

-f .gitmodules 옵션을 포함하지 않으면 이 설정은 공유하지 않고 사용자에게만 적용된다. 다른 사람과 공유하는 저장소라면 같은 브랜치를 추적하도록 설정하는 것이 더 낫다.

이제 git status 명령를 실행하면 새로 업데이트한 서브모듈에 ‘`new commits’'가 있다는 걸 알 수 있다.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

  modified:   .gitmodules
  modified:   DbConnector (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

설정 파일에 status.submodulesummary 항목을 설정하면 서브모듈의 변경 사항을 간단히 보여준다.

$ git config status.submodulesummary 1

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   .gitmodules
    modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c3f01dc...c87d55d (4):
  > catch non-null terminated lines

설정하고 난 후 git diff 명령을 실행해보자. .gitmodules 파일이 변경된 내용은 물론이거니와 업데이트해서 커밋할 필요가 생긴 서브모듈 저장소의 변경 내용을 확인할 수 있다.

$ git diff
diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
 Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

서브모듈에 실제로 커밋할 커밋들의 정보를 보기에는 꽤 괜찮은 방법이다. 비슷한 식으로 커밋한 후에 로그에서 위와 같이 살펴보려면 git log -p 명령으로 볼 수 있다.

$ git log -p --submodule
commit 0a24cfc121a8a3c118e0105ae4ae4c00281cf7ae
Author: Scott Chacon <schacon@gmail.com>
Date:   Wed Sep 17 16:37:02 2014 +0200

    updating DbConnector for bug fixes

diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

git submodule update --remote 명령을 실행하면 기본적으로 모든 서브모듈을 업데이트한다. 서브모듈이 엄청 많을 땐 특정 서브모듈만 업데이트하고자 할 수도 있는데 이럴 때는 서브모듈의 이름을 지정해서 명령을 실행한다.

서브모듈 관리하기

메인 프로젝트에서 서브모듈을 사용할 때 서브모듈에서도 뭔가 작업을 해야 할 상황은 얼마든지 생길 수 있다. 메인 프로젝트에서 작업하는 도중에 말이다(동시에 다른 서브모듈도 수정하거나). 만약 Git의 서브모듈 기능을 사용하지 않는다면 다른 Dependency 관리 시스템(Maven이나 Rubygem 같은)을 사용할 수도 있다.

이번 절에서는 서브모듈을 수정하고 그 내용을 담은 커밋을 유지한 채로 메인프로젝트와 서브모듈을 함께 관리하는 방법을 살펴본다.

서브모듈 저장소에서 git submodule update 명령을 실행하면 Git은 서브모듈의 변경 사항을 업데이트한다. 하지만, 서브모듈 로컬 저장소는 “Detached HEAD” 상태로 남는다. 이 말은 변경 내용을 추적하는 로컬 브랜치(예를 들자면 ‘`master’'같은)가 없다는 것이다. 변경 내용을 추적하는 브랜치 없이 서브모듈에서 수정 작업을 하게 되면 이후에 git submodule update 명령을 실행했을 때 수정한 내용을 잃어버릴 수 있다. 서브모듈 안에서 수정사항을 추적하려면 다른 작업이 좀 더 필요하다.

서브모듈이 브랜치를 추적하게 하려면 할 일이 두 가지다. 우선 각 서므모듈 디렉토리로 가서 추적할 브랜치를 Checkout 하고 일을 시작해야 한다. 이후 서브모듈을 수정한 다음에 git submodule update --remote 명령을 실행해 Upstream 에서 새로운 커밋을 가져온다. 이 커밋을 Merge 하거나 Rebase 하는 것은 선택할 수 있다.

먼저 서브모듈 디렉토리로 가서 브랜치를 Checkout 하자.

$ git checkout stable
Switched to branch 'stable'

여기서 ‘`Merge’'를 해보자. update 명령을 쓸 때 --merge 옵션을 추가하면 Merge 하도록 지정할 수 있다. 아래 결과에서 서버로부터 서브모듈의 변경 사항을 가져와서 Merge 하는 과정을 볼 수 있다.

$ git submodule update --remote --merge
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   c87d55d..92c7337  stable     -> origin/stable
Updating c87d55d..92c7337
Fast-forward
 src/main.c | 1 +
 1 file changed, 1 insertion(+)
Submodule path 'DbConnector': merged in '92c7337b30ef9e0893e758dac2459d07362ab5ea'

DbConnector 디렉토리로 들어가면 새로 수정한 내용이 로컬 브랜치 `stable`에 이미 Merge 된 것을 확인할 수 있다. 이제 다른 사람이 DbConnector 라이브러리를 수정해서 Upstream 저장소에 Push 한 상태에서 우리가 DbConnector 라이브러리를 수정하면 무슨 일이 일어나는지 살펴보자.

$ cd DbConnector/
$ vim src/db.c
$ git commit -am 'unicode support'
[stable f906e16] unicode support
 1 file changed, 1 insertion(+)

이제 서브모듈을 업데이트하면 로컬 저장소에서 수정한 것이 무엇인지 Upstream 저장소에서 수정된 것이 무엇인지 볼 수 있다. 이 둘을 합쳐야 한다.

$ git submodule update --remote --rebase
First, rewinding head to replay your work on top of it...
Applying: unicode support
Submodule path 'DbConnector': rebased into '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

--rebase 옵션이나 --merge 옵션을 지정하지 않으면 Git은 로컬 변경사항을 무시하고 서버로부터 받은 해당 서브모듈의 버전으로 Reset을 하고 Detached HEAD 상태로 만든다.

$ git submodule update --remote
Submodule path 'DbConnector': checked out '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

일이 이렇게 되더라도 문제가 안 된다. Reset이 된 서브모듈 디렉토리로 가서 작업하던 브랜치를 Checkout 하고 직접 origin/stable(아니면 원하는 어떠한 리모트 브랜치든)을 Merge 하거나 Rebase 하면 된다.

서브모듈에 커밋하지 않은 변경 사항이 있는 채로 서브모듈을 업데이트하면 Git은 변경 사항을 가져오지만, 서브모듈의 저장하지 않은 작업을 덮어쓰지 않는다.

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 4 (delta 0)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   5d60ef9..c75e92a  stable     -> origin/stable
error: Your local changes to the following files would be overwritten by checkout:
    scripts/setup.sh
Please, commit your changes or stash them before you can switch branches.
Aborting
Unable to checkout 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

업데이트 명령을 실행했을 때 Upstream 저장소의 변경 사항과 충돌이 나면 알려준다.

$ git submodule update --remote --merge
Auto-merging scripts/setup.sh
CONFLICT (content): Merge conflict in scripts/setup.sh
Recorded preimage for 'scripts/setup.sh'
Automatic merge failed; fix conflicts and then commit the result.
Unable to merge 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

이러면 서브모듈 디렉토리로 가서 충돌을 해결하면 된다.

서브모듈 수정 사항 공유하기

현재 서브모듈은 변경된 내용을 포함하고 있다. 이 중 일부는 서브모듈 자체를 업데이트하여 Upstream 저장소에서 가져온 것이고 일부는 로컬에서 직접 수정한 내용이다. 로컬에서 수정한 것은 아직 공유하지 않았으므로 아무도 사용할 수 없는 코드이다.

$ git diff
Submodule DbConnector c87d55d..82d2ad3:
  > Merge from origin/stable
  > updated setup script
  > unicode support
  > remove unnecesary method
  > add new option for conn pooling

서브모듈의 변경사항을 Push 하지 않은 채로 메인 프로젝트에서 커밋을 Push 하면 안 된다. 변경 사항을 Checkout 한 다른 사람은 서브모듈이 의존하는 코드를 어디서도 가져올 수 없는 상황이 돼 곤란해진다. 서브모듈의 변경사항은 우리의 로컬에만 있다.

이런 불상사가 발생하지 않도록 하려면 메인 프로젝트를 Push 하기 전에 서브모듈을 모두 Push 했는지 검사하도록 Git에게 물어보면 된다. git push 명령에 --recurse-submodules 옵션을 주고 이 옵션의 값으로 ‘`check’나 ‘`on-demand'를 설정한다. ``check’'는 간단히 서브모듈의 로컬 커밋이 Push 되지 않은 상태라면 현재의 Push 명령도 실패하도록 하는 옵션이다.

$ git push --recurse-submodules=check
The following submodule paths contain changes that can
not be found on any remote:
  DbConnector

Please try

    git push --recurse-submodules=on-demand

or cd to the path and use

    git push

to push them to a remote.

예제에서 볼 수 있는 대로 이러한 상황에서 다음으로 무엇을 해야 하는지 Git은 도움을 준다. 가장 단순한 방법은 각 서브모듈 디렉토리로 가서 직접 일일이 Push를 해서 외부로 공유하고 나서 메인 프로젝트를 Push 하는 것이다.

옵션으로 설정할 수 있는 다른 값으로 “on-demand” 값이 있는데, 이 값으로 설정하면 Git이 Push를 대신 시도한다.

$ git push --recurse-submodules=on-demand
Pushing submodule 'DbConnector'
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 3), reused 0 (delta 0)
To https://github.com/chaconinc/DbConnector
   c75e92a..82d2ad3  stable -> stable
Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 266 bytes | 0 bytes/s, done.
Total 2 (delta 1), reused 0 (delta 0)
To https://github.com/chaconinc/MainProject
   3d6d338..9a377d1  master -> master

위에서 보듯이 Git이 메인 프로젝트를 Push 하기 전에 DbConnector 모듈로 들어가서 Push를 한다. 모종의 이유 덕분에 서브모듈 Push에 실패한다면 메인 프로젝트의 Push 또한 실패하게 된다.

서브모듈 Merge 하기

다른 누군가와 동시에 서브모듈을 수정하면 몇 가지 문제에 봉착하게 된다. 서브모듈의 히스토리가 갈라져서 상위 프로젝트에 커밋했다면 사태를 바로잡아야 한다.

서브모듈의 커밋 두 개를 비교했을 때 Fast-Forward Merge가 가능한 경우 Git은 단순히 마지막 커밋을 선택한다.

하지만, Fast-Forward가 가능하지 않으면 Git은 충돌 없이 Trivial Merge(Merge 커밋을 남기는 Merge)를 할 수 있다 해도 Merge 하지 않는다. 서브모듈 커밋들이 분기됐다가 Merge 해야 하는 경우 아래와 같은 결과를 보게 된다.

$ git pull
remote: Counting objects: 2, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 2 (delta 1), reused 2 (delta 1)
Unpacking objects: 100% (2/2), done.
From https://github.com/chaconinc/MainProject
   9a377d1..eb974f8  master     -> origin/master
Fetching submodule DbConnector
warning: Failed to merge submodule DbConnector (merge following commits not found)
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

위 결과를 통해 현재 상태를 살펴본다면 Git은 분기된 두 히스토리 브랜치를 찾았고 Merge가 필요하다는 것을 알게 된다. 이 상황은 “merge following commits not found”(Merge 커밋을 찾을 수 없음)라는 메시지로 표현하는데, 의미가 좀 이상하지만 왜 그런지는 이어지는 내용으로 설명한다.

이 문제를 해결하기 위해 서브모듈이 어떤 상태여야 하는지 알아야 한다. 이상하게도 Git은 이를 위한 정보를 충분히 주지 않는다. 양쪽 히스토리에 있는 커밋의 SHA도 알려주지 않는다. 그래도 알아내는 건 간단하다. git diff 명령을 실행하면 Merge 하려는 양쪽 브랜치에 담긴 커밋의 SHA를 알 수 있다.

$ git diff
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector

위 같은 경우 eb41d76`이 *로컬* 서브모듈의 커밋이고 `c771610`이 Upstream에 있는 커밋이다. 서브모듈의 디렉토리로 가면 현재 `eb41d76 커밋을 가리키고 있고 Merge 작업은 아직 이루어지지 않았다. 이 상태에서 현재 eb41d76 커밋을 브랜치로 만들어 Merge 작업을 진행할 수 있다.

중요한 건 다른 쪽 커밋의 SHA이다. 이쪽이 Merge 해야 할 대상이다. SHA 해시 값을 명시하여 곧바로 Merge 할 수도 있고 대상이 되는 커밋을 새로 브랜치로 하나 만들어 Merge 할 수도 있다. 더 멋진 Merge 커밋 메시지를 위해서라도 후자를 추천한다.

문제를 해결하기 위해 서브모듈 디렉토리로 이동해서 `git diff`에서 나온 두 번째 SHA를 브랜치로 만들고 직접 Merge 한다.

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610
(DbConnector) $ git merge try-merge
Auto-merging src/main.c
CONFLICT (content): Merge conflict in src/main.c
Recorded preimage for 'src/main.c'
Automatic merge failed; fix conflicts and then commit the result.

실제 Merge 시 충돌이 일어났고 해결한 다음 커밋했다. 이후 Merge 한 서브모듈 결과로 메인 프로젝트를 업데이트한다.

$ vim src/main.c (1)
$ git add src/main.c
$ git commit -am 'merged our changes'
Recorded resolution for 'src/main.c'.
[master 9fd905e] merged our changes

$ cd .. (2)
$ git diff (3)
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector
@@@ -1,1 -1,1 +1,1 @@@
- Subproject commit eb41d764bccf88be77aced643c13a7fa86714135
 -Subproject commit c77161012afbbe1f58b5053316ead08f4b7e6d1d
++Subproject commit 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a
$ git add DbConnector (4)

$ git commit -m "Merge Tom's Changes" (5)
[master 10d2c60] Merge Tom's Changes
  1. 먼저 충돌을 해결했다

  2. 그리고 메인 프로젝트로 돌아간다.

  3. SHA-1를 다시 검사하고

  4. 충돌 난 서브모듈을 해결한다.

  5. Merge 결과를 커밋한다.

좀 따라가기 어려울 수 있지만, 사실 그렇게 어려운 건 아니다.

Git으로 이 문제를 해결하는 흥미로운 다른 방법이 있다. 위에서 찾은 두 커밋을 Merge 한 Merge 커밋이 서브모듈 저장소에 존재하면 Git은 이 Merge 커밋을 가능한 해결책으로 내놓는다. 누군가 이미 이 두 커밋을 Merge 한 기록이 있기 때문에 Git은 이 Merge 커밋을 제안한다.

이런 이유에서 위에서 본 Merge 할 수 없다는 오류 메시지가 “merge following commits not found”(Merge 커밋을 찾을 수 없음) 인 것이다. 이런 메시지가 이상한 까닭은 누가 이런 일을 한다고 상상이나 했겠느냐는 말이다.

위의 상황에서 마땅한 Merge 커밋을 하나 발견했다면 아래와 같은 결과를 볼 수 있다.

$ git merge origin/master
warning: Failed to merge submodule DbConnector (not fast-forward)
Found a possible merge resolution for the submodule:
 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a: > merged our changes
If this is correct simply add it to the index for example
by using:

  git update-index --cacheinfo 160000 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a "DbConnector"

which will accept this suggestion.
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

Git이 제시한 해결책은 마치 git add 한 것처럼 현재 Index를 업데이트해서 충돌 상황을 해결하고 커밋하라는 것이다. 물론 제시한 해결책을 따르지 않을 수도 있다. 서브모듈 디렉토리로 이동해서 변경사항을 직접 확인하고 Fast-forward Merge를 한 후 Test 해보고 커밋할 수도 있다.

$ cd DbConnector/
$ git merge 9fd905e
Updating eb41d76..9fd905e
Fast-forward

$ cd ..
$ git add DbConnector
$ git commit -am 'Fast forwarded to a common submodule child'

위와 같은 명령으로도 같은 작업을 수행할 수 있다. 이 방법을 사용하면 Merge 커밋에 해당하는 코드로 테스트까지 해 볼 수 있으며, Merge 작업 후에 서브모듈 디렉토리가 해당 코드로 업데이트된다.

서브모듈 팁

서브모듈 작업을 도와줄 몇 가지 팁을 소개한다.

서브모듈 Foreach 여행

foreach 라는 서브모듈 명령이 있어 한 번에 각 서브모듈에 Git 명령을 내릴 수 있다. 한 프로젝트 안에 다수의 서브모듈 프로젝트가 포함된 경우 유용하게 사용할 수 있다.

예를 들어 여러 서브모듈에 걸쳐 작업하던 도중에 새로운 기능을 추가하거나 버그 수정을 해야 하는 경우다. 간단히 아래와 같은 명령으로 한꺼번에 모든 서브모듈에 Stash 명령을 실행할 수 있다.

$ git submodule foreach 'git stash'
Entering 'CryptoLibrary'
No local changes to save
Entering 'DbConnector'
Saved working directory and index state WIP on stable: 82d2ad3 Merge from origin/stable
HEAD is now at 82d2ad3 Merge from origin/stable

위와 같이 명령을 실행하고 나면 모든 서브모듈과 함께 새 브랜치로 이동해서 작업할 준비를 마치게 된다.

$ git submodule foreach 'git checkout -b featureA'
Entering 'CryptoLibrary'
Switched to a new branch 'featureA'
Entering 'DbConnector'
Switched to a new branch 'featureA'

감이 잡히는가? 이 명령을 유용한 경우는 서브모듈을 포함한 메인 프로젝트의 전체 diff 내용을 한꺼번에 결과로 얻고자 하는 경우이다.

$ git diff; git submodule foreach 'git diff'
Submodule DbConnector contains modified content
diff --git a/src/main.c b/src/main.c
index 210f1ae..1f0acdc 100644
--- a/src/main.c
+++ b/src/main.c
@@ -245,6 +245,8 @@ static int handle_alias(int *argcp, const char ***argv)

      commit_pager_choice();

+     url = url_decode(url_orig);
+
      /* build alias_argv */
      alias_argv = xmalloc(sizeof(*alias_argv) * (argc + 1));
      alias_argv[0] = alias_string + 1;
Entering 'DbConnector'
diff --git a/src/db.c b/src/db.c
index 1aaefb6..5297645 100644
--- a/src/db.c
+++ b/src/db.c
@@ -93,6 +93,11 @@ char *url_decode_mem(const char *url, int len)
        return url_decode_internal(&url, len, NULL, &out, 0);
 }

+char *url_decode(const char *url)
+{
+       return url_decode_mem(url, strlen(url));
+}
+
 char *url_decode_parameter_name(const char **query)
 {
        struct strbuf out = STRBUF_INIT;

위의 결과로 알 수 있는 내용은 서브모듈에서 새 함수를 추가했고 메인 프로젝트에서 추가한 함수를 호출한다는 내용이다. 예제로 살펴본 내용은 아주 단순한 예시일 뿐이지만 어떻게 foreach 명령을 유용하게 사용하는지 감 잡을 수 있을 것이다.

유용한 Alias

서브모듈을 이용하는 명령은 대부분 길이가 길어서 Alias를 만들어 사용하는 것이 편하다. 혹은 설정파일을 통해 기본 값으로 모든 명령에 설정하지 않고 쉽게 서브모듈을 사용할 때도 Alias는 유용하다. Alias를 설정하는 방법은 에서 이미 다루었다. 여기에서는 서브모듈에 관련된 몇 가지 유용한 Alias만 살펴본다.

$ git config alias.sdiff '!'"git diff && git submodule foreach 'git diff'"
$ git config alias.spush 'push --recurse-submodules=on-demand'
$ git config alias.supdate 'submodule update --remote --merge'

위와 같이 설정하면 git supdate 명령으로 간단히 서브모듈을 업데이트할 수 있고 git spush 명령으로 간단히 서브모듈도 업데이트가 필요한지 확인하며 메인 프로젝트를 Push 할 수 있다.

서브모듈 사용할 때 주의할 점들

전체적으로 서브모듈은 어렵지 않게 사용할 수 있지만, 서브모듈의 코드를 수정하는 경우에는 주의해야 한다.

예를 들어 Checkout으로 브랜치를 변경하는 경우 서브모듈이 포함된 작업이라면 좀 애매하게 동작할 수 있다. 메인 프로젝트에서 새 브랜치를 생성하고 Checkout 한 후 새로 서브모듈을 추가한다. 이후 다시 이전 브랜치로 Checkout 하면 서브모듈 디렉토리는 추적하지 않는 디렉토리로 남게 된다.

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'adding crypto library'
[add-crypto 4445836] adding crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    CryptoLibrary/

nothing added to commit but untracked files present (use "git add" to track)

물론 추적하지 않는 디렉토리를 지우는 건 쉽다. 이렇게 수동으로 지워야 한다는 게 이상한 것이다. 수동으로 디렉토리를 지우고 다시 서브모듈을 추가했던 브랜치로 Checkout 하면 submodule update --init명령을 실행해 줘야 서브모듈의 코드가 나타난다(역주 - 이렇게 코드를 가져오고 나면 Detached HEAD가 된다).

$ git clean -fdx
Removing CryptoLibrary/

$ git checkout add-crypto
Switched to branch 'add-crypto'

$ ls CryptoLibrary/

$ git submodule update --init
Submodule path 'CryptoLibrary': checked out 'b8dda6aa182ea4464f3f3264b11e0268545172af'

$ ls CryptoLibrary/
Makefile	includes	scripts		src

명령이 어려운 건 아니지만, 다시 봐도 이상하다.

또 하나 주의 깊게 살펴볼 일은 서브디렉토리를 서브모듈로 교체하면서 브랜치간 이동하는 경우이다. 메인 프로젝트에서 관리하던 서브디렉토리를 새 서브모듈로 교체할 때 주의를 기울이지 않으면 Git을 집어던지고 싶게 된다. 서브디렉토리를 서브모듈로 교체하는 상황을 살펴보자. 우선 서브디렉토리를 그냥 지우고 바로 서브모듈을 추가한다면 오류가 나타난다.

$ rm -Rf CryptoLibrary/
$ git submodule add https://github.com/chaconinc/CryptoLibrary
'CryptoLibrary' already exists in the index

위와 같은 오류를 해결하려면 우선 CryptoLibrary 디렉토리를 관리대상에서 삭제하고 나서 서브모듈을 추가한다.

$ git rm -r CryptoLibrary
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

위의 작업을 master가 아닌 어떤 브랜치에서 실행한 상황이다. 만약 다시 master 브랜치로 Checkout 하게 되면 서브모듈이 아니라 서브디렉토리가 존재해야 하는 상황이 되는데, 아래와 같은 오류를 만나게 된다.

$ git checkout master
error: The following untracked working tree files would be overwritten by checkout:
  CryptoLibrary/Makefile
  CryptoLibrary/includes/crypto.h
  ...
Please move or remove them before you can switch branches.
Aborting

물론 checkout -f 옵션을 붙여서 강제로 브랜치를 Checkout 할 수 있지만, 서브모듈에서 저장하지 않은 내용을 되돌릴 수 없게 덮어쓰기 때문에 주의 깊게 강제 적용 옵션을 사용해야 한다.

$ git checkout -f master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'

후에 다시 서브모듈을 추가했던 브랜치로 Checkout 하면 서브모듈 디렉토리 CryptoLibrary`는 비어 있다. 간혹 `git submodule update 명령으로 서브모듈을 초기화하더라도 서브모듈 코드가 살아나지 않을 수 있다. 이럴 때는 서브모듈 디렉토리로 이동해서 git checkout . 명령을 실행하면 서브모듈 코드가 나타난다. 서브모듈을 여러 개 사용하는 경우 submodule foreach 명령으로 한꺼번에 코드를 복구할 수 있다.

최신 버전의 Git은 서브모듈의 커밋 데이터도 메인 프로젝트의 .git 디렉토리에서 관리한다. 예전 버전의 Git과 달리 서브모듈이 포함된 디렉토리를 망가뜨렸다 하더라도 기록해 둔 커밋 데이터는 쉽게 찾을 수 있다.

이런 여러 도구와 함께 서브모듈을 사용한다면 간단하고 효율적으로 메인 프로젝트와 하위 프로젝트를 동시에 관리할 수 있다.


'Programming > Git' 카테고리의 다른 글

Git_#7.13_Git 도구 - Replace  (0) 2017.04.13
Git_#7.12_Git 도구 - Bundle  (0) 2017.04.13
Git_#7.10_Git 도구 - Git으로 버그 찾기  (0) 2017.04.13
Git_#7.9_Git 도구 - Rerere  (0) 2017.04.13
Git_#7.8_Git 도구 - 고급 Merge  (0) 2017.04.13