Pro git - 7.11

Updated:

  • 여긴 다시 봐야겠다. 한 번도 경험해보지 못해서 이해가 안가는 부분이 있음

Pro git - 서브모듈

  • 프로젝트를 수행하다 보면 다른 프로젝트를 함께 사용해야 하는 경우가 종종 있다.
  • 함께 사용할 다른 프로젝트는 외부에서 개발한 라이브러리라던가 내부 여러 프로젝트에서 공통으로 사용할 라이브러리일 수 있다.
  • 이런 상황에서 자주 생기는 이슈는 두 프로젝트를 서로 별개로 다루면서도 그 중 하나를 다른 하나 안에서 사용할 수 있어야 한다는 것이다.
  • Atom 피드를 제공하는 웹사이트를 만드는 것을 예로 들어보자.
  • Atom 피드를 생성하는 코드는 직접 작성하지 않고 라이브러리를 가져다 쓰기로 한다.
  • 라이브러리를 사용하려면 CPAN이나 Ruby gem 같은 라이브러리 관리 도구를 사용하여 Shared 라이브러리 형태로 쓰거나 직접 라이브러리의 소스코드를 프로젝트로 복사해서 사용할 수 있다.
  • 우선 Shared 라이브러리를 사용하기에는 문제가 있다.
  • 프로젝트를 사용하는 모든 환경에 라이브러리가 설치되어 있어야 하고 라이브러리를 프로젝트에 맞게 약간 수정해서 사용하고 배포하기가 어렵다.
  • 또한, 라이브러리 소스코드를 직접 프로젝트에 포함시키는 경우에는 라이브러리 Upstream 코드가 업데이트됐을 때 Merge 하기가 어렵다.
  • Git의 서브모듈은 이런 문제를 다루는 도구다.
  • Git 저장소 안에 다른 Git 저장소를 디렉토리로 분리해 넣는 것이 서브모듈이다.
  • 다른 독립된 Git 저장소를 Clone 해서 내 Git 저장소 안에 포함할 수 있으며 각 저장소의 커밋은 독립적으로 관리한다.

1. 서브모듈 시작하기

  • 예제로 하위 프로젝트 여러 개를 가지는 프로젝트를 하나 만들어 서브모듈의 기능을 살펴보자.
  • 작업할 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 파일을 보고 어떤 서브모듈 프로젝트가 있는지 알 수 있다.

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

  • 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 모드는 일반적인 파일이나 디렉토리가 아니라 특별하다는 의미다.

2. 서브모듈 포함한 프로젝트 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 명령 뒤에 --recurse-submodules 옵션을 붙이면 서브모듈을 자동으로 초기화하고 업데이트한다.
$ git clone --recurse-submodules 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

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

3.1 서브모듈 업데이트하기

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

  • 서브모듈 프로젝트를 최신으로 업데이트하려면 서브모듈 디렉토리에서 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 명령을 실행하면 기본적으로 모든 서브모듈을 업데이트한다.
  • 서브모듈이 엄청 많을 땐 특정 서브모듈만 업데이트하고자 할 수도 있는데 이럴 때는 서브모듈의 이름을 지정해서 명령을 실행한다.

3.2 서브모듈 관리하기

  • 메인 프로젝트에서 서브모듈을 사용할 때 서브모듈에서도 뭔가 작업을 해야 할 상황은 얼마든지 생길 수 있다.
  • 메인 프로젝트에서 작업하는 도중에 말이다(동시에 다른 서브모듈도 수정하거나).
  • 만약 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'
  • 이러면 서브모듈 디렉토리로 가서 충돌을 해결하면 된다.

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

  • 현재 서브모듈은 변경된 내용을 포함하고 있다.
  • 이 중 일부는 서브모듈 자체를 업데이트하여 Upstream 저장소에서 가져온 것이고 일부는 로컬에서 직접 수정한 내용이다.
  • 로컬에서 수정한 것은 아직 공유하지 않았으므로 아무도 사용할 수 없는 코드이다.
$ git diff
Submodule DbConnector c87d55d..82d2ad3:
  > Merge from origin/stable
  > updated setup script
  > unicode support
  > remove unnecessary 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 하는 것이다.
  • 이 옵션이 항상 적용되도록 하고 싶으면 git config push.recurseSubmodules check 명령으로 설정한다.

  • 옵션으로 설정할 수 있는 다른 값으로 “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 또한 실패하게 된다.
  • git config push.recurseSubmodules on-demand 명령으로 설정할 수 있다.

3.4 서브모듈 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 작업 후에 서브모듈 디렉토리가 해당 코드로 업데이트된다.

4. 서브모듈 팁

4.1 서브모듈 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 명령을 유용하게 사용하는지 감 잡을 수 있을 것이다.

4.2 유용한 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 할 수 있다.

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

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

  • 예를 들어 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 -ffdx
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과 달리 서브모듈이 포함된 디렉토리를 망가뜨렸다 하더라도 기록해 둔 커밋 데이터는 쉽게 찾을 수 있다.
  • 이런 여러 도구와 함께 서브모듈을 사용한다면 간단하고 효율적으로 메인 프로젝트와 하위 프로젝트를 동시에 관리할 수 있다.