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. 배경
레퍼런스
- 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 상태)가 된다.
- 현재 parent를
develop
→main
으로 변경 - child 디렉토리로 이동 시에, 그래도
develop
브랜치임을 확인 - 다시 parent 디렉토리에서
git submodule update --remote
실행 - 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
명령을 실행했을 경우를 의미.
- 여기서 설명하는 과정은 최초로 parent repo를 clone 후에
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 시도- ⚠️ 실패할 수 있음.