TL;DR

  • submodule은 결국 git repository 2개 중 하나를 다른 하나에 추가하는 개념.
  • parent repository는 child의 파일을 추적하는 것이 아닌 commit hash만 추적
    • 따라서, parent의 commit * child commit으로 추적할 상태가 복잡해짐.
    • child repo의 변경 사항을 그 자신의 원격에 업데이트 하지 않은 경우, parent 상태 자체가 꼬일 수 있다. (4번 참고)
  • parent repository에서는 최대한 child repository를 수정하지 않는 것이 안전
  • parent repository에서 child(서브모듈)을 관리하기 위한 명령어들은 사실 child 디렉토리 안에서 기존의 git 명령어로 다 대체 가능하다.
    • 정보가 늘어나서 복잡하기만 한 거 같기도…

0. 배경

레퍼런스

Git - 서브모듈

  • office client와 messenger client 사이에 공통으로 사용되는 요소들을 하나의 repository에서 관리하기 위해 도입

1. 서브모듈 시작하기

1) 용어 정리

  • 앞으로 하위 repository를 갖는 상위 repository를 parent라고 한다.
  • 앞으로 상위 repository안에 포함되는 하위 repository를 child라고 한다. 추가될 commonModules이 child이다.

2) 서브모듈 생성

  • parent 레파지토리 clone 하기
git clone <https://{github 계정}/submodule_parent.git>
  • 서브모듈 추가하기
git submodule add <https://{github 계정}/submodule_child.git>

3) 결과

submodule_parent
├── .git
│   ├── COMMIT_EDITMSG
│   ├── FETCH_HEAD
│   ├── HEAD
│   ├── ORIG_HEAD
│   ├── config
│   ├── description
│   ├── hooks
│   ├── index
│   ├── info
│   ├── logs
│   ├── modules
│   ├── objects
│   ├── packed-refs
│   └── refs
├── .gitmodules
├── README.md
└── submodule_child # child repository가 parent 안에 생성
    ├── .git
    └── README.md
  • .gitmodules 파일
[submodule "submodule_child"]
    path = submodule_child
    url = <https://{github 계정}/submodule_child.git>

 

위처럼, 서브모듈을 생성하면 parent repository 아래에 child repository 폴더가 생성되고 .git 폴더가 각각 생성되었다.

 

그리고 parent repository의 .gitmodules 파일이 내부에 submodule에 대한 정보가 생성된다.

⇒ 아래에서 parent와 child repository의 branch를 맞출 수 있는데 .gitmodules 파일을 사용한다.


4) 특이사항

git diff 명령을 실행시키면 흥미로운 점을 발견할 수 있다.

  • submodule_child 디렉토리를 서브모듈로 취급하기 때문에 해당 디렉토리 아래의 파일 수정사항을 직접 추적하지 않는다.
    • 대신 서브모듈 디렉토리를 통째로 특별한 커밋으로 취급한다. ⇒ commit hash로 표현
  • submodule_child 디렉토리의 모드는 160000이다. Git에게 있어 160000 모드는 일반적인 파일이나 디렉토리가 아니라 특별하다는 의미다.
  • 서브모듈만 더 자세히 검색하는 옵션
git diff --submodule
  • 해당 변경 사항 remote에 push
git push origin main

2. 서브모듈 포함한 프로젝트 Clone하기

  • 서브모듈을 포함하는 프로젝트를 Clone 하는 예제를 살펴본다. 이런 프로젝트를 Clone 하면 기본적으로 서브모듈 디렉토리는 빈 디렉토리이다.
  • clone이 끝난 후, 서브모듈을 가져오기 위해서는 아래의 2개의 명령을 실행.
git submodule init
git submodule update
  • clone과 동시에 submodule을 가져오고 싶다면 --recurse-submodules 옵션을 붙인다.
git clone --recurse-submodules ${parent 원격 git 주소}

3-1. 하위 프로젝트를 수정하지 않고 참조만 하는 방법 (권장)

❗️ 주의사항

서브모듈은 결국 2개의 repository를 사용하기 때문에 parent에서 child를 변경 시에도 child의 git 상태가 변경됩니다.

따라서 parent에서는 최신 child를 참조만 하는 방법을 권장합니다. (방법 3-1.)
(만약, child repository를 수정 및 commit 했을 시에 3-2 가이드 참조)

 

⚠️ child가 변경되었을 시에 parent를 push 할 수 없게 설정

만약 child의 변경사항을 child의 원격에 push 하지 않고 parent를 push 했다면 다른 사용자가 parent를 clone 했을 경우, child의 커밋을 불러올 수 없어 에러가 난다!

# 반드시 설정 후 작업할 것
git config push.recurseSubmodules check

# 설정 확인하기
git config -l | grep "push.recursesubmodules"

서브모듈 업데이트하기

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

A. child repository에서 직접 업데이트

# child repository로 이동
cd submodule_child

# child repository로 이동시 에 git은 child repository를 따른다.
git fetch
git merge origin/main
  • 주의할 점은 child repository 폴더로 이동 시에 git은 child repository를 따른다.
  • git status를 폴더 경로에 따라 실행했을 때의 결과

  • parent repository에서 git diff --submodule 명령으로 변경 및 추가된 commit을 확인할 수 있고, --submodule 옵션 없이 항상 확인하고 싶다면 아래 명령을 실행
# 모두 parent repository 디렉토리 기준에서 실행
$ git config --global diff.submodule log
$ git diff
Submodule DbConnector c3f01dc..d0354fc:
  > more efficient db routine
  > better connection routine

B. parent repository에서 한 번에 업데이트하기 (fetch & merge)

# parent 디렉토리에서 실행
git submodule update --remote

서브모듈 브랜치 설정하기

  • git submodule update --remote 명령어를 실행하면 기본적으로 master 브랜치를 checkout 하고 업데이트를 수행한다.
  • 따라서 parent repository의 branch에 따라 child repository의 branch도 변경되어야 한다.
    ex) parent: main → child: main / parent: develop → child: develop
  • 현재 parent 기준으로 child main으로 설정하기
git config -f .gitmodules submodule.submodule_child.branch main
  • .gitmodules 변경 결과
[submodule "submodule_child"]
    path = submodule_child
    url = <https://{github 계정}/submodule_child.git>
    branch = main

즉, parent branch에 따라 .gitmodules 안의 submodule.branch 설정을 변경하면 parent와 child의 branch가 동일하게 설정된다.

 

‼️ 주의

아래의 설정은 git fetch 시의 branch를 설정할 뿐이다. 만약, parent: main / child: main 브랜치에서 작업 중, parent를 develop 으로 변경한다 가정하자.

 

그럼 parent: develop / child: main 상태가 되고 이때 git submodule update —remote 명령어를 실행하면 parent의 .gitmodules 설정에 따라 child는 develop 원격을 fetch하여 HEAD는 detached 상태(원격 develop 상태)가 된다.

 

  1. 현재 parent를 developmain 으로 변경
  2. child 디렉토리로 이동 시에, 그래도 develop 브랜치임을 확인
  3. 다시 parent 디렉토리에서 git submodule update --remote 실행
  4. child의 HEAD 상태가 detached
    (로컬은 develop, fetch 받아온 곳은 main이라 HEAD 상태가 원격 main을 따라 detached)

git status 로 submodule 상태 확인

  • git status 명령을 실행했을 때, 업데이트한 서브 모듈에 “new commits”가 있다는 걸 알 수 있다.
$ submodule_parent git:(main) ✗ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   submodule_child (new commits)

no changes added to commit (use "git add" and/or "git commit -a")
  • git config에 status.submodulesummary 설정하기
# git config status.submodulesummary를 설정하면 서브모듈의 변경사항을 간단히 보여준다.

$ submodule_parent git:(main) ✗ git config status.submodulesummary 1
$ submodule_parent git:(main) ✗ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   submodule_child (new commits)

Submodules changed but not updated:

* submodule_child 2a18eba...b569a3d (14):
  > origin child repo에서 커밋

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

 


3-2. 하위 프로젝트를 수정한 경우

  • 서브모듈 저장소에서 git submodule update 명령을 실행하면 서브모듈 로컬 저장소는 Detached HEAD 상태로 남는다.
    • 여기서 설명하는 과정은 최초로 parent repo를 clone 후에 git submodule update 명령을 실행했을 경우를 의미.
  • Detached HEAD 상태로 서브모듈을 수정하면 이후 git submodule update 명령 실행 시 수정사항을 잃을 수 있다.
    • 마찬가지로, 다른 사용자의 git clone 후의 상황을 의미
  • child의 브랜치를 추적하게 하려면 해당 child 디렉토리에서 추적할 브랜치를 Checkout하고 일을 시작한다.
$ git checkout develop
Switched to branch 'develop'
  • 이 상태에서 업데이트된 remote의 상태를 가져오고 싶은 경우
# --merge 옵션을 붙인다.
git submodule update --remote --merge
$ submodule_parent git:(develop) ✗ git submodule update --remote --merge
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 4 (delta 2), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (4/4), 573 bytes | 286.00 KiB/s, done.
From <https://{github url}/submodule_child>
   b2fa00b..0c4a069  develop    -> origin/develop
Updating b2fa00b..0c4a069
Fast-forward
 index.js | 6 ++++++
 1 file changed, 6 insertions(+)
Submodule path 'submodule_child': merged in '0c4a0698e091583be408434a04ab6ead44096bc6'

이제 child의 Upstream 저장소(원격)에 다른 사람이 Push한 상황에서 로컬 저장소를 수정한 경우 아래와 같이 2가지 케이스가 발생한다.

  • child repo를 commit한 상태
  • child repo를 변경만하고 commit하지 않은 modified 상태

1) 하위 프로젝트 commit 상태

  • 원격 child Upstream 저장소 수정
$ submodule_child git:(develop) vim test.js
$ submodule_child git:(develop) ✗ ll
total 32
-rw-r--r--  1 krk224  staff   6.1K  3  7 15:05 README.md
-rw-r--r--  1 krk224  staff   693B  3  8 14:25 index.js
-rw-r--r--  1 krk224  staff    22B  3  8 14:28 test.js
$ submodule_child git:(develop) ✗ git add test.js
$ submodule_child git:(develop) ✗ git commit -m "원본 child repo commit" 
[develop 293477a] 원본 child repo commit
 1 file changed, 1 insertion(+)
 create mode 100644 test.js
$ submodule_child git:(develop) git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 361 bytes | 361.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: 
remote: To create a merge request for develop, visit:
remote:   <https://{github url}/submodule_child/-/merge_requests/new?merge_request%5Bsource_branch%5D=develop>
remote: 
To <https://{github url}/submodule_child.git>
   d43fbdc..293477a  develop -> develop
$ submodule_child git:(develop)

 

  • parent 로컬 저장소에서 child 저장소 수정
$ submodule_parent git:(develop) ✗ cd submodule_child 
$ submodule_child git:(develop) ✗ pwd
/Users/krk224/{dir}/submodule_parent/submodule_child
$ submodule_child git:(develop) vim test.js
$ submodule_child git:(develop) ✗ git add test.js
$ submodule_child git:(develop) ✗ git commit -m "parent에서 child commit"
[develop be91ada] parent에서 child commit
 1 file changed, 1 insertion(+)
 create mode 100644 test.js
$ submodule_child git:(develop)
  • parent 로컬 저장소에서 git submodule update --remote 명령 수행
    • git submodule update --remote --merge 명령 수행하면 위와 같이 merge 되거나 충돌 사항을 알려준다.
    • 지금은 --merge 옵션 없이 테스트
➜  submodule_child git:(develop) cd ..
➜  submodule_parent git:(develop) ✗ git submodule update --remote
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 6 (delta 2), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), 676 bytes | 225.00 KiB/s, done.
From <https://{github url}/submodule_child>
   0c4a069..293477a  develop    -> origin/develop
Submodule path 'submodule_child': checked out '293477a7bb6e9e229b33c4a1d6043850c017c7fc'
➜  submodule_parent git:(develop) ✗ cd submodule_child 
➜  submodule_child git:(293477a) cat test.js
console.log('hello');
➜  submodule_child git:(293477a)
  • 위의 경우, Git은 원격으로 받아온 서브모듈 상태를 추적(console.log(’hello’))하며 Detached HEAD 상태로 만든다. (293477a)
  • 지금까지 작업했던 로컬은 그대로 develop 브랜치에 저장되어 있기 때문에 checkout 으로 돌린 후 git pull 작업을 수행하면 merge 된다.
➜  submodule_child git:(293477a) git checkout develop
Previous HEAD position was 293477a 원본 child repo commit
Switched to branch 'develop'
Your branch and 'origin/develop' have diverged,
and have 1 and 2 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

## 로컬 저장소의 상태로 test.js 전환
➜  submodule_child git:(develop) cat test.js
console.log('parent에서 child 수정');

## 원격과 merge... vscode에서 conflict 수정
➜  submodule_child git:(develop) git pull
Auto-merging test.js
CONFLICT (add/add): Merge conflict in test.js
Automatic merge failed; fix conflicts and then commit the result.
➜  submodule_child git:(develop) ✗ git commit
U       test.js
error: Committing is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.

## 현재 성공적으로 merge된 것을 확인
➜  submodule_child git:(develop) ✗ cat test.js 
console.log('parent에서 child 수정');
console.log('hello');

2) 하위 프로젝트 modified 상태

: 이번에는 child의 원격 저장소가 업데이트된 상황에서 로컬 저장소를 수정만 한 경우

  • 원격 저장소 업데이트
## 위에서 parent 안의 child 수정 사항 받아오기
➜  submodule_child git:(develop) git pull
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 6 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), 698 bytes | 174.00 KiB/s, done.
From <https://{github url}/submodule_child>
   293477a..c513ef0  develop    -> origin/develop
Updating 293477a..c513ef0
Fast-forward
 test.js | 1 +
 1 file changed, 1 insertion(+)
## update된 child repo 확인
➜  submodule_child git:(develop) cat test.js
console.log('parent에서 child 수정');
console.log('hello');

## 원본 child에서 test.js 수정 및 확인
➜  submodule_child git:(develop) vim test.js
➜  submodule_child git:(develop) ✗ cat test.js
console.log('parent에서 child 수정');
console.log('hello');

**console.log('2번째 case 원본 child repo 수정!');**

## 원격 child repo update 
➜  submodule_child git:(develop) ✗ git add *
➜  submodule_child git:(develop) ✗ git commit -m "2번째 케이스를 위해 원본 child repo 수정"  
[develop bcec2e8] 2번째 케이스를 위해 원본 child repo 수정
 1 file changed, 2 insertions(+)
➜  submodule_child git:(develop) git push 
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 406 bytes | 406.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
remote: 
remote: To create a merge request for develop, visit:
remote:   <https://{github url}/submodule_child/-/merge_requests/new?merge_request%5Bsource_branch%5D=develop>
remote: 
To <https://{github url}/submodule_child.git>
   c513ef0..bcec2e8  develop -> develop
  • 로컬의 parent안의 child 수정만
## 현재 test.js 상태 확인
➜  submodule_child git:(develop) cat test.js
console.log('parent에서 child 수정');
console.log('hello');
➜  submodule_child git:(develop) vim test.js

## 수정된 test.js 출력
➜  submodule_child git:(develop) ✗ cat test.js
console.log('parent에서 child 수정');
console.log('hello');

console.log('parent안에서 child repo 수정하고 commit은 안함!');
  • 이 상태에서 git submodule update --remote 명령 수행
➜  submodule_child git:(develop) ✗ cd ..
➜  submodule_parent git:(develop) ✗ git submodule update --remote
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 386 bytes | 386.00 KiB/s, done.
From <https://{github url}/submodule_child>
   c513ef0..bcec2e8  develop    -> origin/develop
error: Your local changes to the following files would be overwritten by checkout:
        test.js
Please commit your changes or stash them before you switch branches.
Aborting
fatal: Unable to checkout 'bcec2e87e6d061244aaf9dbcbbba183dae3c05b4' in submodule path 'submodule_child'

위 경우 변경사항이 commit되지 않아 Aborting 에러를 발생시켜 remote를 checkout 실패한다.

→ 따라서 변경사항을 원하는 원하는 브랜치에 커밋 후 merge하기


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

  • 현재 로컬에서 parent 안의 child repo를 수정하고 merge한 상태에서 parent repo에 push하면??
    → 서브모듈(child)의 의존 commit을 가져올 수 없다!
  • 따라서 child repo를 수정한 경우 반드시 원격 repo에 push 해야한다!!!

child 원격에 push하지 않고 parent에만 원격 업데이트 친 경우

  • git 상태를 보면 submodule_parent는 submodule_child의 commit hash만 추적하고 있다.
    • 현재 로컬에서 submodule_child를 commit했기 때문에 이를 추적
  • submodule_child의 git을 원격에 sync를 맞추지 않은 상태에서 submodule_parent를 push
## 이제 다른 사용자가 parent 프로젝트를 clone 했을 때의 상황
➜  {dir} git clone <https://{github url}/submodule_parent.git>
Cloning into 'submodule_parent'...
remote: Enumerating objects: 25, done.
remote: Total 25 (delta 0), reused 0 (delta 0), pack-reused 25
Receiving objects: 100% (25/25), 5.55 KiB | 5.55 MiB/s, done.
Resolving deltas: 100% (8/8), done.
➜  {dir} cd submodule_parent

## 현재 유실된 commit은 develop 기준이기 때문에 develop으로 스위칭
➜  submodule_parent git:(main) git switch develop
branch 'develop' set up to track 'origin/develop'.
Switched to a new branch 'develop'
➜  submodule_parent git:(develop) git submodule init
Submodule 'submodule_child' (<https://{github url}/submodule_child.git>) registered for path 'submodule_child'

## fetch error 발생!
➜  submodule_parent git:(develop) git submodule update
Cloning into '/Users/{dir}/submodule_parent/submodule_child'...
fatal: remote error: upload-pack: not our ref 029280eb6bfcc723f8ba8f35d7bfbc3bf8a85c7e
fatal: Fetched in submodule path 'submodule_child', but it did not contain 029280eb6bfcc723f8ba8f35d7bfbc3bf8a85c7e. Direct fetching of that commit failed.

 

3-1에서도 표기했지만 다시 한번 강조!
⚠️ child가 변경되었을 시에 parent를 push할 수 없게 설정

 

만약 child의 변경사항을 child의 원격에 push하지 않고 parent를 push하고 다른 사용자가 parent를 clone했을 경우, child의 커밋을 불러올 수 없어 에러가 난다!

# 반드시 설정 후 작업할 것
git config push.recurseSubmodules check

# 설정 확인하기
git config -l | grep "push.recursesubmodules"

git config push.recurseSubmodules 옵션

  • check : child를 원격에 push하지 않은 경우, parent push 실패 처리
  • on-demand: child를 원격에 push하지 않은 경우, 자동으로 child push 후에 parent push 시도
    • ⚠️ 실패할 수 있음.

+ Recent posts