Git 의 서브모듈(Submodule)

Git 의 서브모듈(Submodule)

Git 의 서브모듈(Submodule) 이란 하나의 저장소 안에 있는 또 다른 별개의 저장소이다.

보통 다른 원격 저장소를 가져와(pull) 서브모듈로 사용하게 된다.

본 포스트에서는 Git 의 서브모듈에 대해 알아본다.

서브모듈 시작하기

myblog 라는 프로젝트 디렉터리에서 블로그를 개발하다가,

chat-module 이라는 채팅 모듈을 원격 저장소에서 가져와 블로그 프로젝트에서 사용하고 싶다고 가정하자.

현재 myblog 로컬 저장소에는 Comment.java, Post.java 2개의 파일이 있고,

chat-module 원격 저장소에는 Chat.java 파일이 있다.

myblog 디렉터리에서 chat-module 저장소를 서브모듈로 사용하기 위해 다음 명령어를 사용한다.

script
git submodule add https://github.com/sgc109/chat-module.git

그럼 아래와 같이 .gitmoduleschat-module/ (Chat.java 와 함께)가 생성되어 스테이징 된다.

메인 프로젝트에 서브모듈 추가하기

이때 .gitmodules 파일을 열어보면 다음과 같이 서브모듈에 대한 정보(이름, 경로, 원격 저장소 url)가 적혀있다.

서브모듈 추가 시 자동 생성된 .gitmodules 파일의 내용

또, 루트 디렉터리에서 git diff --cached chat-module 를 실행하면, (참고로, --cached 와 --staged 는 같다)

다음과 같이 서브 디렉터리 내 모든 파일에 대한 정보가 자세히 나오는 것이 아니라 어떤 하나의 커밋에 대한 정보만 적혀있다.

아래와 같이 스테이징된 변경사항을 commit 해보면,

서브 모듈 추가 후 Commit 했을 때의 출력 화면

일반적인 파일처럼 100xxx 의 형태로 표시되는 .gitmodules 와는 달리

chat-module 디렉터리는 160000 의 형태로 하나의 특수한 파일로 인식한다는 것을 확인할 수 있다.

해당 커밋을 Github 에 push 하면 서브모듈은 다음과 같은 아이콘으로 표시된다.

서브모듈 포함한 프로젝트 Clone

팀 내 다른 개발자가 앞서 서브모듈을 포함하여 push 된 원격 저장소를 clone 하고 싶다고 하자.

아래와 같이 git clone 명령어를 실행한다.

비어있는 서브모듈 디렉터리

보이는 것처럼 서브모듈 디렉터리 내 파일들은 하나도 가져오지 않는다.

메인 프로젝트 입장에서 서브모듈은 사실 단지 현재 가리키는 커밋과 변경 여부만 적혀있는 하나의 파일에 불과하기 때문이다.

서브모듈 디렉터리에서 변경사항을 만들고 상위 디렉터리에서 git diff 를 해보면 이 사실을 알 수 있다.

메인 프로젝트에서 서브모듈의 변경사항을 추적하는 방식

그래서 서브모듈 디렉터리에서 2가지 명령어를 추가로 실행해야 서브모듈의 파일을 모두 가져올 수 있다.

script
git submodule init
git submodule update

git submodule init 은 서브모듈 디렉터리를 git 로컬 저장소로 초기화 해주고(git init 처럼)

git submodule update 는 서브모듈의 원격 저장소에서 파일을 가져온다. 최신 commit 을 가져 오는건 아니고

.git/modules/chat-module/HEAD 에 서브모듈이 어떤 커밋을 가리키는지 명시돼 있어서 해당 커밋을 가져온다.

서브모듈 초기화를 위한 2가지 추가 명령어

혹은, 애초에 clone 을 할 때 --recurse-submodules 옵션을 주면

굳이 모든 서브모듈 디렉터리에서 2개의 추가 명령어를 실행하지 않아도 한방에 초기화를 할 수 있다.

script
git clone --recurse-submodules https://github.com/sgc109/myblog.git

git clone 시 옵션으로 한 번에 서브모듈 초기화 하기

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

서브모듈 업데이트하기

만약 다른 개발자가 채팅 모듈에 Message.java 를 추가하여

chat-module 의 원격 저장소에 변경 사항을 push 했다고 가정하자.

그럼 chat-module 서브모듈을 사용하는 myblog 로컬 저장소에서는 이를 반영해야 할 것이다.

기본적인 방법은 서브모듈 디렉터리에서 다음 2가지 명령어를 입력하는 것이다.

script
git fetch
git merge origin/master

2개의 명령어로 서브모듈 업데이트 하기

하지만 더 간단한 방법이 있다. 서브모듈 디렉터리에서 2개의 명령어를 입력하는 대신

루트 디렉터리에서 다음 명령어를 입력하면 된다.

script
git submodule update --remote chat-module

1개의 명령어로 서브모듈 업데이트 하기

이때 만약 로컬에서도 서브모듈에 변경이 있는 경우에는 --merge--rebase 옵션을 줘야한다.

기본 브랜치 변경하기

참고로 지금까지의 명령어들은 기본적으로 서브모듈 원격 저장소의 master 브랜치를 기준으로 동작한다.

만약 기본 브랜치를 예를 들어 bugfix-chat 로 변경하고 싶다면, 다음 명령어를 사용하면 된다.

script
git config -f .gitmodules submodule.chat-module.branch bugfix-chat

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

myblog 에서 작업을 하다가 서브모듈인 chat-module 의 내용에 변경이 필요한 경우도 있을 것이다.

이럴 때는 먼저 서브모듈의 내용을 원격 저장소에 반영한 뒤, 메인 프로젝트의 내용을 Push 해야한다.

그렇지 않으면 메인 프로젝트를 Pull 한 다른 개발자들은 변경된 서브모듈의 내용을 알 수 없기 떄문이다.

하지만 서브모듈의 내용을 Push 하는 것을 깜빡할 수도 있는데,

이때 --recurse-submodules 라는 유용한 Push 옵션이 있다.

script
git push --recurse-submodules=check

git push --recurse-submodules=on-demand

이 옵션을 check 로 주면 서브모듈이 Push 되지 않았다면 메인 프로젝트의 Push 가 중단되며,

서브모듈 수정 후 메인 프로젝트 Push 시 옵션 주기 (check)

옵션을 on-demand 로 주면 자동으로 서브모듈을 Push 한다.

서브모듈 수정 후 메인 프로젝트 Push 시 옵션 주기 (on-demand)

물론 단순히 서브모듈 디렉터리에서 직접 Push 를 해주고 메인 프로젝트를 Push 할 수도 있다.

서브모듈 Merge 하기

내가 서브모듈의 내용을 수정하고 있는데 다른 사람도 수정하여 Upstream 에 Push 했을 경우

Pull 을 했다고 하자. 만약 서브모듈의 커밋이 단순히 Fast-Forward 관계라면 최신 커밋을 선택한다.

하지만, 그렇지 않다면? 충돌이 발생하게 된다.

이때 메인 디렉터리에서 git diff 명령을 통해 충돌이 난 서브모듈의 두 커밋의 SHA 해시 값을 알 수 있다.

그럼 일단 Upstream 의 커밋으로 새로운 브랜치를 만들고, 서브모듈 디렉터리에서 Merge 를 한다.

script
$ cd chat-module
$ git branch upstream-commit c771610
$ git merge upstream-commit

그럼 물론 충돌이 발생하고, 이를 서브모듈 디렉터리에서 해결한 뒤 커밋을 하고,

다시 메인 디렉터리로 가서 변경 사항을 스테이징에 올린 뒤 커밋을 하면 된다.

script
$ git add conflicted-file
$ git commit -m 'merged 2 commits in submodule'
$ cd ..
$ git add chat-module
$ git commit -m 'all merged in main directory'

서브모듈 팁

Foreach

여러개의 서브모듈을 포함한 프로젝트에서 각 서브모듈에 공통적인 명령어를 수행하고 싶을 때가 있다.

예를 들어 작업 도중 메인 프로젝트에서 다른 브랜치로 변경하고 싶은데

메인 프로젝트와 여러 서브모듈에 스테이징은 됐지만 아직 Commit 하지 않은 변경사항이 있는 경우다.

직접 모든 서브모듈 디렉터리에서 git stash 를 하기엔 너무 번거롭다.

그럴 땐 다음과 같이 foreach 를 사용하면 유용하다.

script
git stash; git submodule foreach 'git stash'

그리고 다음 명령어로 모든 서브모듈과 함께 새로운 브랜치로 이동할 수 있다.

script
git checkout -b bugfix-post; git submodule foreach 'git checkout -b bugfix-post'

모든 서브모듈을 포함한 변경사항을 알고싶은 경우 다음과 같이 활용할 수도 있다.

script
git diff; git submodule foreach 'git diff'

Alias

앞서 살펴본 submodule 과 관련된 명령문은 너무 길다는 단점이 있다.

이럴 땐 다음과 같이 서브모듈과 관련된 명령어들의 Alias 로 등록하면 편하다.

script
$ 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'

참고로 alias 설정에서 앞에 '!' 와 같이 느낌표가 붙으면 git 명령어가 아닌 shell 명령어라는 의미이다.

참고: https://git-scm.com/book/ko/v2/Git-도구-서브모듈

Comments