Git: 서브모듈 이해하기 (git submodule)

발생일: 2016.06.23

키워드: git submodule, 깃 서브모듈

문제:
Git 에서 서브모듈을 사용하려고 한다.


해결책:

서브모듈에 대한 자세한 옵션은 Pro Git 책의 설명을 보는 게 더 나을 것 같아,
여기선 아래와 같이 튜토리얼 형식으로 설명해보려고 한다.

----------------------------------------------------------------
1. 서브모듈
2. 서브모듈 추가하기
3. 부모 프로젝트에서 자식 프로젝트의 내용 변경하고 업데이트하기
4. 자식 프로젝트에서 수정 후, 부모 프로젝트에 적용하기
5. 서브모듈이 있는 프로젝트 클론하기
6. 변경된 서브모듈 업데이트하기
----------------------------------------------------------------


1. 서브모듈

깃의 서브모듈은 깃 리파지터리 아래에 다른 하위 깃 리파지터리를 관리하기 위한 도구이다.

현재 작업하고 있는 아래와 같은 부모 프로젝트가 있고,
git@github.com:ohgyun/submodule_test_parent.git

이 프로젝트 하위에 이미 존재하는 다른 깃 프로젝트를 포함하려고 한다고 가정해보자.

아래는 자식 프로젝트의 리파지터리 링크이다.
git@github.com:ohgyun/submodule_test_child.git


워킹 디렉토리는 ~/mywork 라고 가정하고, 여기에서 부모 프로젝트를 클론하는 걸로 시작해보자.

~/mywork
$ git clone git@github.com:ohgyun/submodule_test_parent.git

~/mywork
$ cd submodule_test_parent



# 서브 모듈 추가하기

서브모듈은 아래 명령으로 추가할 수 있다.

$ git submodule add <repository> [path]

path 는 생략 가능하고, 생략 시 리파지터리 이름과 동일한 디렉토리를 사용한다.
우리는 생략하고 아래와 같이 추가해보자.

~/mywork/submodule_test_parent
$ git submodule add https://github.com/ohgyun/submodule_test_child
Cloning into 'submodule_test_child'...
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
Checking connectivity... done.


아무 것도 없는 상태에서 서브모듈을 추가하고 상태를 조회해보면,
아래와 같이 .gitmodules 파일과 추가한 서브모듈 디렉토리가 생성된다.

~/mywork/submodule_test_parent
$ 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:   submodule_test_child


.gitmodules 파일에는 프로젝트에서 관리하고 있는 서브모듈 목록에 대한 정보가 들어있다.
조회해보면, 아래와 같이 서브모듈 정보를 볼 수 있다.

~/mywork/submodule_test_parent
$ cat .gitmodules
[submodule "submodule_test_child"]
    path = submodule_test_child
    url = git@github.com:ohgyun/submodule_test_child.git


submodule_test_child 는 하위 깃 리파지터리를 포함하고 있는 디렉토리이지만,
깃에서는 서브모듈 정보를 포함하고 있는 파일처럼 처리한다.

실제로 하위 깃 리파지터리의 커밋 해시를 포함하고 있으며,
이 정보를 diff 로 확인할 수 있다.


~/mywork/submodule_test_parent
$ git diff --cached submodule_test_child/
diff --git a/submodule_test_child b/submodule_test_child
new file mode 160000
index 0000000..0d2bb5b
--- /dev/null
+++ b/submodule_test_child
@@ -0,0 +1 @@
+Subproject commit 0d2bb5b91c235fa77fdd8859d9ecbd270fd576d2

명령 옵션의 --cached 은 기존에 서브 모듈이 없어 캐시된 값과 비교하기 위한 옵션이라 무시해도 되고,
유심히 봐야할 것은 추가된 파일의 모드와 diff 내용이다.

mode 160000 이라는 건, 일반 파일이 아니라는 의미이고,
+Subproject commit 0d2bb5b... 는 현재 부모 리파지터리에서 하위 리파지터리의 0d2bb5b 커밋을 바라보고 있단 의미이다.


서브모듈을 추가한 후 submodule_test_child 디렉토리에 들어가보면,
대상 리파지터리가 fetch 되어 있는 걸 확인할 수 있다.


이제 서브모듈을 변경했다는 커밋을 하나 추가하자.

~/mywork/submodule_test_parent
$ git commit -am "Add submodule"
[master b90da54] Add submodule




2. 부모 프로젝트에서 자식 프로젝트의 내용 변경하고 업데이트하기

이제, 부모 프로젝트 내에서 자식 프로젝트의 내용을 변경해보자.

먼저 부모 리파지터리의 상태를 조회해보자.

~/mywork/submodule_test_parent
$ git log --pretty=short -1
commit b90da54bc5f7b0ca12eb313e1a57581f35ddce38
Author: Ohgyun Ahn <ohgyun@gmail.com>

    Add submodule


리모트도 조회해보자.

~/mywork/submodule_test_parent
$ git remote -v
origin    git@github.com:ohgyun/submodule_test_parent.git (fetch)
origin    git@github.com:ohgyun/submodule_test_parent.git (push)


다음으로 자식 리파지터리의 상태도 조회해보자.

~/mywork/submodule_test_parent
$ cd submodule_test_child/

~/mywork/submodule_test_parent/submodule_test_child
$ git log --pretty=short -1
commit 0d2bb5b91c235fa77fdd8859d9ecbd270fd576d2
Author: Ohgyun Ahn <ohgyun@gmail.com>

    Initial commit


자식 리파지터리의 리모트도 조회해보자.

~/mywork/submodule_test_parent/submodule_test_child
$ git remote -v
origin    https://github.com/ohgyun/submodule_test_child (fetch)
origin    https://github.com/ohgyun/submodule_test_child (push)


각 리파지터리는 각각의 origin 을 갖고 있고,
부모 리파지터리는 b90da54 커밋을, 자식 리파지터리는 0d2bb5b 커밋을 바라보고 있다.


이제 자식 리파지터리(submodule_test_child 디렉토리)에서 새 커밋을 생성해보자.

~/mywork/submodule_test_parent/submodule_test_child
$ git commit --allow-empty -m "Add new commit"
[master af2a90a] Add new commit

참고로, --allow-empty 옵션은 빈 커밋을 생성하는 옵션이다.
상태를 조회해보자.

~/mywork/submodule_test_parent/submodule_test_child
$ git log --pretty=short -1
commit af2a90a19766d5ab5ed7d5f59e88403245f99ab1
Author: Ohgyun Ahn <ohgyun@gmail.com>

    Add new commit


새 커밋으로 자식 리파지터리의 커밋이 0d2bb5b 에서 af2a90a 로 변경됐다.

부모 리파지터리로 돌아가 상태를 조회해보자.

~/mywork/submodule_test_parent/submodule_test_child
$ cd ..

~/mywork/submodule_test_parent
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
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:   submodule_test_child (new commits)

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


변경된 정보를 조회해보면, 서브모듈의 값이 변경된 걸 알 수 있다.

~/mywork/submodule_test_parent
$ git diff
diff --git a/submodule_test_child b/submodule_test_child
index 0d2bb5b..af2a90a 160000
--- a/submodule_test_child
+++ b/submodule_test_child
@@ -1 +1 @@
-Subproject commit 0d2bb5b91c235fa77fdd8859d9ecbd270fd576d2
+Subproject commit af2a90a19766d5ab5ed7d5f59e88403245f99ab1


현재 부모 리파지터리이니까, 서브모듈을 변경했다는 커밋을 추가해보자.

~/mywork/submodule_test_parent
$ git commit -am "Update submodule"
[master 3a0e087] Update submodule
 1 file changed, 1 insertion(+), 1 deletion(-)


새로 생성된 부모 리파지터리의 커밋 3a0e087에서는,
자식 리파지터리의 af2a90a 커밋을 바라보고 있다.

하지만 아직 자식 리파지터리는 로컬만 변경되어 있는 상태이기 때문에, 변경 내용을 리모트에도 적용해줘야 한다.

~/mywork/submodule_test_parent
$ cd submodule_test_child/

~/mywork/submodule_test_parent/submodule_test_child
$ git log --pretty=short -1
commit af2a90a19766d5ab5ed7d5f59e88403245f99ab1
Author: Ohgyun Ahn <ohgyun@gmail.com>

    Add new commit

~/mywork/submodule_test_parent/submodule_test_child
$ git push origin master
Counting objects: 1, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 440 bytes | 0 bytes/s, done.
Total 1 (delta 1), reused 0 (delta 0)
To git@github.com:ohgyun/submodule_test_child.git
   0d2bb5b..af2a90a  master -> master


필요하다면, 부모 리파지터리도 리모트에 푸시하면 된다.

~/mywork/submodule_test_parent/submodule_test_child
$ cd ..

~/mywork/submodule_test_parent
$ git push origin master



3. 자식 프로젝트에서 수정 후, 부모 프로젝트에 적용하기

이번엔 자식 프로젝트에서 변경한 것을 부모 프로젝트에 적용해보자.

먼저 워크 디렉토리에서 자식 프로젝트를 다른 이름으로 클론한다.

~/mywork
$ git clone git@github.com:ohgyun/submodule_test_child.git submodule_test_child

~/mywork
$ cd submodule_test_child


우리가 위에서 추가했던 커밋이 제대로 있는지 확인해보자.

~/mywork/submodule_test_child
$ git log --pretty=short -1
commit af2a90a19766d5ab5ed7d5f59e88403245f99ab1
Author: Ohgyun Ahn <ohgyun@gmail.com>

    Add new commit


이제 새로 커밋을 추가하고 리모트에도 푸시해보자.

~/mywork/submodule_test_child
$ git commit --allow-empty -m "Second commit"
[master 4506909] Second commit

~/mywork/submodule_test_child
$ git push origin master


이렇게 자식 프로젝트에서 수정한 내용을 리모트에 업데이트했다.
현재 자식 프로젝트의 리모트 리파지터리의 master 브랜치는 4506909 커밋을 가리키고 있다.


이제 다시 부모 프로젝트로 돌아와보자.

~/mywork/submodule_test_child
$ cd ../submodule_test_parent


부모 프로젝트에 자식 프로젝트에서 수정된 내용을 반영하려면,
대상 디렉토리로 가서 리모트의 변경 사항을 머지하면 된다.

~/mywork/submodule_test_parent
$ cd submodule_test_child


먼저 현재 서브모듈로 포함되어 있는 자식 프로젝트의 마지막 커밋을 확인해보자.

~/mywork/submodule_test_parent/submodule_test_child
$ git log --pretty=short -1
commit af2a90a19766d5ab5ed7d5f59e88403245f99ab1
Author: Ohgyun Ahn <ohgyun@gmail.com>

    Add new commit


예상대로 아직 리모트에 새로 반영된 커밋(4506909)과 다르다.

어떤 브랜치인지도 확인해보자.

~/mywork/submodule_test_parent/submodule_test_child
$ git branch
* master


서브모듈을을 add 한 후에 다른 처리를 하지 않았고,
(자식 리파지터리의 깃헙 base 브랜치는 master 라서) master 브랜치로 되어 있다.

리모트의 것을 적용해보자.

~/mywork/submodule_test_parent/submodule_test_child
$ git pull origin master
...
From github.com:ohgyun/submodule_test_child
 * branch            master     -> FETCH_HEAD
   af2a90a..4506909  master     -> origin/master
Updating af2a90a..4506909


자식 리파지터리를 업데이트했으니, 부모 모듈의 참조도 바뀌었을 것이다.
확인해보자.

~/mywork/submodule_test_parent/submodule_test_child
$ cd ..

~/mywork/submodule_test_parent
$ 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:   submodule_test_child (new commits)

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


예상대로, 서브모듈 파일이 수정되었고, diff 도 확인해보자.

~/mywork/submodule_test_parent
$ git diff
diff --git a/submodule_test_child b/submodule_test_child
index af2a90a..4506909 160000
--- a/submodule_test_child
+++ b/submodule_test_child
@@ -1 +1 @@
-Subproject commit af2a90a19766d5ab5ed7d5f59e88403245f99ab1
+Subproject commit 45069090990ad8c291f5f67b23c0aca83d6a4d6f


부모 프로젝트 참조하고 있는 서브모듈의 커밋 해시도 변경된 걸 볼 수 있다.
변경 사항을 커밋하고 푸시하자.

~/mywork/submodule_test_parent
$ git commit -am "Update submodule"
$ git push origin master




4. 서브모듈이 있는 프로젝트 클론하기

이번에는 서브모듈이 있는 프로젝트를 클론해보자.

워킹 디렉토리로 이동한 후에,

$ cd ~/mywork


다른 이름으로 부모 리파지터리를 클론해보자.

~/mywork
$ git clone git@github.com:ohgyun/submodule_test_parent.git submodule_test_parent_2

~/mywork
$ cd submodule_test_parent_2

이렇게 서브모듈이 포함된 리파지터리를 클론하면,
submodule_test_child 디렉토리는 존재하지만 내용은 비어있다.

~/mywork/submodule_test_parent_2
$ ls submodule_test_child


먼저, 서브모듈을 가져오려면 먼저 초기화 해야한다.

~/mywork/submodule_test_parent_2
$ git submodule init
Submodule 'submodule_test_child' (git@github.com:ohgyun/submodule_test_child.git) registered for path 'submodule_test_child'


다음으로 서브모듈을 업데이트한다.

~/mywork/submodule_test_parent_2
$ git submodule update
Cloning into 'submodule_test_child'...
...
Submodule path 'submodule_test_child': checked out '45069090990ad8c291f5f67b23c0aca83d6a4d6f'


서브모듈을 업데이트한다는 건,
현재 부모 리파지터리의 커밋에서 참조하고 있는 서브모듈의 커밋을,
자식 리파지터리의 리모트에서 체크아웃해온다는 뜻이다.

자식 프로젝트의 디렉토리로 이동해 로그를 확인해보면 대상 커밋으로 채워진 걸 확인할 수 있다.

~/mywork/submodule_test_parent_2
$ cd submodule_test_child

~/mywork/submodule_test_parent_2/submodule_test_child
$ git log --pretty=short -1
commit 45069090990ad8c291f5f67b23c0aca83d6a4d6f
Author: Ohgyun Ahn <ohgyun@gmail.com>

    Second commit
 

자식 프로젝트의 브랜치도 확인해보자.

~/mywork/submodule_test_parent_2/submodule_test_child
$ git branch
* (detached from 4506909)
  master

브랜치를 보면 detached 상태로 되어 있다.
부모 프로젝트에서 서브모듈을 업데이트하면, 브랜치나 태그 같은 간접 레퍼런스를 가져오는 게 아니라,
저장된 특정 커밋을 체크아웃하기 때문이다.

이 상태에서 2번 튜토리얼처럼 자식 브랜치를 수정하려고 한다면,
아래처럼 별도의 브랜치를 따서 작업하기를 권장한다.

~/mywork/submodule_test_parent_2/submodule_test_child
$ git branch -b feature/fix-submodule



5. 변경된 서브모듈 업데이트하기

만약, 다른 개발자가 서브모듈을 변경하고 변경 내역을 부모 리파지터리에 적용해 푸시했다고 가정해보자.

이제 우리는 변경된 내용을 업데이트 받으면 된다.

먼저 부모 리파지터리를 pull 해온다.

~/mywork/submodule_test_parent
$ cd ~/mywork/submodule_test_parent

~/mywork/submodule_test_parent
$ git pull origin master
...
Fast-forward
 submodule_test_child | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)


서브모듈이 변경됐다면, pull 받았을 때 위처럼 submodule_test_child 에 대한 참조도 변경되었을 것이다.
하지만 아직 서브모듈은 업데이트 되지 않은 상태다.

pull 받았을 때, 서브모듈이 변경됐다면 항상 아래 명령으로 서브모듈을 업데이트 해주도록 한다.

~/mywork/submodule_test_parent
$ git submodule update



논의:

- 다시 한 번, git submodule update 는 현재 부모 커밋에서 참조하고 있는 자식 커밋을 체크아웃 한다.
  하위 디렉토리에서 자식 프로젝트에서 작업한 후 리모트에 커밋하지 않고 git submodule update 를 실행하면,
  기존 커밋 대신 부모 리파지터리에서 참조하고 있는 커밋으로 체크아웃되니 주의한다.

- 위 경우,  커밋하지 않은 상태였다면 기존 작업본은 삭제된다.
  커밋만 하고 푸시하지 않은 상태라면, 로그를 뒤져 로컬 커밋을 찾아가는 방법이 있지만 브랜치를 별도로 따두지 않았다면 찾기 번거롭다.
  부모 프로젝트에서 자식 프로젝트를 작업하는 경우라면, 특히 별도의 브랜치를 따서 작업하기를 권장한다.
  


참고:
Pro Git: 6.6 Git 도구 - 서브모듈

저작자 표시 비영리 변경 금지
신고