서브모듈
프로젝트를 수행하다 보면 다른 프로젝트를 사용해야 하는 경우가 종종 있다. 보통 사용할 프로젝트들은 독립적으로 개발된 라이브러리들이다. 이런 상황에서 자주 생기는 이슈는 두 프로젝트를 별개로 다루면서도 그중 하나를 다른 하나 안에서 사용할 수 있어야 한다는 것이다.
Atom 피드를 제공하는 웹사이트를 만든다고 가정하자. Atom 피드를 생성하는 코드는 직접 작성하지 않고 라이브러리를 가져다 쓰기로 했다. 그러면 CPAN이나 Ruby gem 같은 라이브러리 관리 도구를 사용하거나 해당 소스를 프로젝트로 복사해야 한다. 사실 라이브러리를 수정하는 것은 어렵다. 하지만 수정한 라이브러리를 모든 사용자가 이용할 수 있도록 배포하는 것은 더 어렵다. 그래서 프로젝트에 라이브러리 코드를 포함시켜서 수정하는 방법도 사용한다. 이렇게 라이브러리 코드를 포함시키면 원래 라이브러리 프로젝트의 코드가 Merge하기 어렵게 된다.
Git의 서브모듈은 이런 문제를 해결해준다. 서브모듈은 Git 저장소 안에 다른 Git 저장소를 둘 수 있게 해준다. 이렇게 해도 두 Git 저장소 모두 독립적으로 관리된다.
서브모듈 시작하기
일단 Ruby 웹서버 게이트웨이 인터페이스인 Rack 라이브러리를 프로젝트에 추가해 보자. 추가하고 나서도 앞으로 여전히 해당 저장소에서 관리할 수 있기 때문에 마음 놓고 코드를 수정할 수 있다.
먼저 git submodule add 명령으로 프로젝트를 서브 모듈로 추가한다.
$ git submodule add https://github.com/chnekirchen/rack.git rack
Initialized empty Git repository in /opt/subtest/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Resolving objects: 100% (3181/3181), 675.42KiB | 422 KiB/s, done
Resolving deltas: 100% (1951/1951), done
이제 프로젝트 디렉터리를 보면 rack 이라는 디렉터리가 생겼을 것이다. 그 디렉터리가 Rack 프로젝트이다. rack 디렉터리 안에서 수정하고 Push할 권한이 있는 저장소를 하나 추가하고 나서 그 저장소에 Push한다. 물론 원래 프로젝트 저장소에서도 Fetch하고 Merge할 수 있다. 서브모듈을 추가한 직후 바로 git status라는 명령을 실행하면 다음과 같이 두 파일이 생긴 것을 알 수 있다.
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: .gitmodules
# new file: rack
.gitmodules 파일을 살펴보자. 이것은 로컬 디렉터리와 프로젝트 URL의 매핑 정보가 저장된 설정파일이다.
[submodule "rack"]
path = rack
url = https://github.com/chneukirchen/rack.git
서브모듈 개수만큼 이 항목이 생긴다. 이 파일도 .gitignore 파일처럼 버전 관리된다. 다른 파일처럼 Push하고 Pull한다. 이 프로젝트를 Clone하는 사람은 .gitmodules 파일을 보고 어떤 서브모듈 프로젝트가 있는지 알 수 있다.
.gitmodules은 살펴봤고 이제 rack 항목에 대해 살펴보자. git diff 명열을 실행시키면 흥미로운 점을 발견할 수 있다.
$ git diff --cached rack
diff --git a/rack b/rack
new file mode 160000
index 0000000..08d709f
--- /dev/null
+++ b/rack
@@ -0,0 +1 @@
+Subproject commit 08d709f78d8c5b0fbeb7821e37fa53e69afcf433
Git은 rack 디렉터리를 서브모듈을 취급하기 때문에 파일들을 직접 추적하지 않고 커밋 하나만 저장한다. rack 디렉터리에서 수정을 하고 커밋하면 다른 사람이 같은 환경을 만들 수 있도록 HEAD가 가리키는 커밋이 수퍼프로젝트에 저장된다.
master처럼 브랜치 이름 같은 레퍼런스가 저장되는 것이 아니라 SHA-1값이 저장된다.
수퍼프로젝트도 커밋해야 한다.
$ git commit -m 'first commit with submodule rack'
[master 0550271] first commit with submodule rack
2 files changed, 4 insertions(+), 0 deletions(-)
create mode 100644 .gitmodules
create mode 160000 rack
rack 디렉터리의 모드는 160000이다. 160000 모드는 일반적인 파일이나 디렉터리가 아니라는 의미다.
하위 프로젝트의 마지막 커밋이 바뀔 때마다 수퍼프로젝트의 저장된 커밋도 바꿔준다. rack 디렉터리를 별도의 프로젝트로 취급하기 때문에 모든 Git 명령은 독립적으로 동작한다.
$ git log -1
commit: 055071....1bb
Author: Scott Chacon <schacon@gmail.com>
Date: Thu Apr 9 09:03:56 2009 -0700
first commit with submodule rack
$ cd rack/
$ git log -1
commit: 08d709.....433
Author: Christian Neukirchen <chneukirchen@gmail.com>
Date: Wed Mar 25 14:49:04 2009 +0100
Document version change
서브모듈이 있는 프로젝트 Clone하기
서브모듈을 사용하는 프로젝트를 Clone하면 해당 서브모듈 디렉터리는 빈 디렉터리다.
$ git clone git://github.com/schacon/myproject.git
Initialized empty Git repository in /opt/myproject/.git/
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (4/4), done.
Receiving objects: 100% (6/6), done.
$ cd myproject
$ ls -;
total 8
-rw-r--r-- 1 schacon admin 3 Apr 9 09:11 README
drwxr-xr-x 2 schacon admin 68 Apr 9 09:11 rack
$ ls rack/
$
분명히 rack 디렉터리가 있지만 비워져 있다.
- 먼저 git submodule init 명령으로 서브모듈을 초기화 하고
- git submodule update 명령으로 서버에서 데이터를 가져온다.
- 데이터를 전부 가져오면 수퍼프로젝트에 저장된 커밋으로 Checkout된다.
$ git submodule init
Submodule 'rack' (git://github.com/chneukirchen/rack.git) registered for path 'rack'
$ git submoudle update
Initialized empty Git repository in /opt/myproject/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Resolving objects: 100% (3181/3181), 675.42KiB | 422 KiB/s, done
Resolving deltas: 100% (1951/1951), done
Submodule path 'rack': checked out '08d709f7...433'
rack 디렉터리는 이제 복원됐다. 그리고 누군가 rack을 수정하면 그 코드를 가져다 Merge한다.
$ git merge origin/master
Updating 0550271..85a3eee
Fast forward
rack | 2+-
1 files changed, 1 insertions(+), 1 deletions(-)
[master*]
$ git status
# On branch master
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: rack
#
Merge해서 서브모듈의 HEAD 값이 변경됐다. 수퍼프로젝트가 아는 커밋과 서브 모듈의 HEAD가 달라서 아직 워킹 디렉터리의 상태는 깨끗한 상태가 아니다.
$ git diff
diff --git a/rack b/rack
index 6c5e70b..08d709f 160000
--- a/rack
+++ b/rack
@@ -1 +1 @@
-Subproject commit 6c5e70b9..8e0
+Subproject commit 08d709f7..433
이럴 때 git submodule update 명령을 실행해서 해결한다.
$ git submodule update
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 2 (delta 0)
Unpoacking objects: 100% (3/3), done
From git@github.com:schacon/rack
08d709f..6c5e70b master -> origin/master
Submodule path 'rack': checked out '6c5e70b...8e0'
서브모듈 프로젝트를 Pull할 때마다 git submodule update 명령을 실행해야 한다. 뭔가 속는 것 같지만 잘 된다.
개발자들이 흔히 저지르는 실수로 서브모듈의 코드를 수정하고 나서 서버에 Push하지 않는 경우가 있다. 수퍼 프로젝트는 Push했지만 프로젝트가 아는 커밋은 아직 Push하지 않고 개발자 PC에 있다. 만약 다른 개발자가 git submodule update를 실행하면 수퍼프로젝트에 저장된 커밋을 서브모듈 프로젝트에서 찾을 수 없어서 에러가 발생한다.
$ git submodule update
fatal: reference isn't a tree: 6c5e70b...8e0
Update to checkout '6c5e70b...8e0' in submodule path 'rack'
누가 마지막으로 서브모듈을 수정했는지 확인하고,
$ git log -1 rack
commit 85a3eee...678
Author: Scott Chacon <chacon@gmail.com>
Date: Thu Apr 9 09:19:14 2009 -0700
added a submodule reference I will never make public.
hahahahaha!
그 개발자에게 이메일을 보내거나 전화를 건다.
수퍼프로젝트
프로젝트 규모가 크면 CVS나 서브버전에서는 모듈 프로젝트를 간단히 하위 디렉터리로 만들었다. 가끔 Git에서도 이런 Workflow를 사용하려는 개발자들이 있다.
Git에서는 각 하위 디렉터리를 별도의 Git 저장소로 만들어야 한다. 그리고 그 저장소를 포함하는 상위 저장소를 만든다. 수퍼프로젝트의 태그와 브랜치를 이용해서 각 프로젝트의 관계를 구체적으로 정의할 수 있다는 것은 Git만의 장점이다.
서브모듈 사용할 때 주의할 점들
전체적으로 서브모듈은 어렵지 않게 사용할 수 있지만, 서브모듈의 코드를 수정하는 경우에는 주의가 필요하다. git submodule update 명령을 실행시키면 특정 브랜치가 아니라 수퍼프로젝트의 저장된 커밋을 Checkout해 버린다. 그러면 detached HEAD라고 부르는 상태가 된다.
detached HEAD는 HEAD가 브랜치나 태그 같은 간접 레퍼런스를 가리키지 않고 커밋을 가리키는 것을 말한다.
데이터를 잃어 버릴 수도 있기 때문에 일반적으로 detached HEAD 상태는 피해야 한다.
- submodule update를 실행하고 나서 별도의 작업용 브랜치를 만들지 않고 서브모듈 코드를 수정하고 커밋한다.
- 그리고 나중에 커밋한 것을 잊은 채로 수퍼프로젝트에서 git submodule update를 실행시키면 Git은 아무 말 없이 Checkout해버린다.
- 엄밀히 말해서 커밋이 없어진 것은 아니지만 브랜치에 속하지 않은 커밋을 찾아내기란 정말 어렵다.
git checkout -b work 같은 명령으로 작업할 때마다 work 브랜치를 만들면 이 문제를 피할 수 있다. 실수로 submodule update 명령을 실행해 버려서 하던 일을 놓쳐버려도 포인터가 있어서 언제든지 되찾을 수 있다.
그리고 서브모듈이 있는 수퍼프로젝트의 브랜치를 오갈 때는 약간의 추가작업이 필요하다. 브랜치를 만들고 서브모듈을 추가한다. 그 다음에 서브모듈이 없는 브랜치로 돌아간다. 그렇지만 이미 추가한 서브모듈 디렉터리가 Untracked 상태로 보인다.
$ git chekcout -b rack
Switched to a new branch "rack"
$ git submodule add git@github.com:schacon/rack.git rack
Initialized empty Git repository in /opt/myproject/rack/.git/
...
$ git commit -am 'added rack submodule'
[rack cc49a69] added rack submodule
2 files changed, 4 insertions(+), 0 deletions(-)
create mode 100644 .gitmodules
create mode 160000 rack
$ git checkout master
Switched to branch "master"
$ git status
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# rack/
서브모듈 디렉터리를 다른 곳에 옮겨 두거나 삭제해야 한다. 삭제할 경우는 원래 브랜치로 돌아왔을 때 서브모듈을 다시 Clone해야 하고, 이 경우 아직 Push하지 않았던 변경사항이나 브랜치를 잃을 수 있다.
rack이라는 디렉터리가 있고 이것을 서브모듈로 바꾸려고 한다고 가정하자. 먼저 rack 디렉터리를 삭제하고 submodule add를 실행하면 Git은 다음과 같은 에러를 뱉는다.
$ rm -Rf rack/
$ git submodule add git@github.com:schacon/rack.git rack 'rack already exists in the index
rack 디렉터리를 Staging Area에서 제거하면 서브모듈을 추가할 수 있다.
$ git rm -r rack
$ git submodule add git@github.com:schacon/rack.git rack
Initialized empty Git repository in /opt/myproject/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Resolving objects: 100% (3181/3181), 675.42KiB | 422 KiB/s, done
Resolving deltas: 100% (1951/1951), done
한 브랜치에서는 해결했다. 아직 해당 디렉터리를 서브모듈로 만들지 않은 브랜치를 Checkout 하려고 하면 다음과 같은 에러가 난다.
$ git checkout master
error: Untracted working tree file 'rack/AUTHORS' would be overwritten by merge
다른 브랜치로 바꾸기 전에 rack 서브모듈 디렉터리를 다른 곳으로 옮겨둔다.
$ mv rack /tmp/
$ git checktou master master
Swithed to branch "master"
$ ls
README rack
그러고 나서 다시 서브모듈이 있는 브랜치로 돌아가면 rack 디렉터리는 텅 비어 있다. git submodule update 명령으로 다시 Clone하거나 /tmp/rack/에 복사해둔 파일을 다시 복사한다.
'책 > 프로 Git' 카테고리의 다른 글
Git 도구-7 - Subtree Merge (0) | 2020.07.30 |
---|---|
Git 도구-5 - Git으로 버그 찾기 (0) | 2020.07.29 |
Git 도구-4 - 히스토리 단장하기 (0) | 2020.07.29 |
Git 도구-3 - Stashing (0) | 2020.07.29 |
Git 도구-2 - 대화형 명령어 (0) | 2020.07.29 |