개요

개발을 하면서 난잡하게 코드들이 커밋되어 있는 하나의 리포지토리 A 에서 용도에 따라 코드를 분리 하여 관리를 하려고 한다. 재활용 가능한 모듈로 코드를 관리하기 위해 덜어낸 코드를 리포지토리 B에 작업하였다. 이를 효과적으로 사용하기 위한 git submodule 명령어 사용법 정리.

서브모듈 추가하기

A 리포지토리에서 B라는 별개의 리포지토리를 서브모듈로 사용하는 방법이다. 다른 독립된 Git 저장소를 Clone 해와서 현재의 Git 저장소 내에 포함되는 것으로 간주한다.

# Current Repo: A
$ git submodule add https://github.com/dongle94/B     # repo B

위와 같이 가져오면 A 라는 리포지토리에 B 가 하위 디렉토리로 잡히게 된다. 또한 B 하위에 있는 파일들은 서브모듈로 A 리포지토리와는 별도로 관리되기에 .gitmodules 라는 파일이 생성되어 서브모듈이 연결되었다는 매핑 정보만 관리한다.

A
├─ Readme.md 
├─ .gitmodules
└─ B
   ├─ ...  
   └─ ...

디렉토리 명 지정해서 가져오기

이 때 B 리포지토리를 B가 아닌 다른 경로로 저장하고 싶다면 뒤에 <path> 를 붙여서 다르게 저장할 수 있다. 아래와 같이 실행한다면 B가 아니라 repo_b라는 경로에 서브모듈로 추가된다.

# git submodule add {url} {path}
$ git submodule add https://github.com/dongle94/B ./repo_b

추가 후 꼭 해야하는 것

B 리포지토리를 서브모듈로 추가했다면 A 리포지토리의 입장에선 B라는 서브모듈이 추가되었다는 것 자체가 하나의 변경 사항이다. 그래서 해당 변경점을 commitpush 해야 원격 저장소에선 반영이 된다.

$ git add .gitmodules
$ git add B
$ git commit -m "add submodule"
$ git push origin main

이 때 B에 대한 커밋은 매우 앞으로도 중요해지는데 아래에서 자세히 설명한다.

프로젝트 새로 가져오기

누군가가 위 처럼 A 라는 리포지토리에 B라는 서브모듈을 추가하여 push까지 하였고 다른 공동작업자가 이제 B를 포함한 A를 가져오려고 할 때 다음과 같이 git clone 명령어를 사용한다.

$ git clone https://github.com/dongle94/A
Cloning into 'A'...
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.

A 를 보면 위의 트리구조 처럼 Readme.md, .gitmodules, B의 세가지 파일 및 디렉토리는 있는데 서브모듈 B의 디렉토리가 비어있다.

A
├─ Readme.md 
├─ .gitmodules
└─ B        # empty directory

서브모듈의 내용물을 가져오는 것은 아래 명령어를 참고하여 추가작업이 필요하다.

$ git submodule init
$ git submodule update --remote

위의 init이 들어간 명령어는 해당 A 리포지토리를 clone 해오고 처음 1회 입력해주면된다. 이는 .gitmodules 파일을 읽어 각 디렉토리와 원격저장소의 경로를 연결한다.
그 후 update --remote 구문을 통해 원격저장소로부터 서브모듈 B를 추가적으로 clone 해오는 개념이다.

서브모듈이 수정되었을 때

위의 예시에서 서브모듈 B가 수정되는 경우는 아래와 같이 3가지로 나눌 수 있다.

  • 리포지토리 B에 변경사항이 있는 경우
  • A 하위에 있는 B를 내가 직접 수정할 경우
  • 다른 사람이 B를 직접 수정하여 commit & push 한 경우

리포지토리 B에 변경사항이 있는 경우

A 하위 디렉토리가 아닌 본래의 B 프로젝트에 변경 사항이 있는 경우 일반적으로 B 프로젝트에 대해 commit & push를 수행하고 A 프로젝트에서 서브모듈을 업데이트한다.

# Current Directory: Original B
$ git commit -m "modify repo B"
$ git push origin main      # for repo B


# Move A and Update submodule B
$ cd A      # local repo A
$ git submodule update --remote
$ git commit -m "update submodule B"
$ git push origin main      # for repo A

본래의 BB대로 원격 저장소에 커밋과 푸시가 진행되고, A 리포지토리 입장에서는 변경 사항이 있는 B를 원격 저장소로부터 최신의 상태를 받아와 업데이트 한다.

A 하위에 있는 B를 내가 직접 수정할 경우

근데 자체적으로 clone해온 B가 아니라 A 하위에 있는 서브모듈 B에서 코드의 수정이나 파일의 변경사항이 있다면 당황하지 않고 다음과 같이 반영할 수 있다. A 하위 경로에 있는 서브모듈 Bgit clone 해온 것과 동일하다고 생각하면 된다.

# Current directory: A
$ cd ./B
$ git commit -m "B의 변경사항"
$ git push origin main      # for repo B

$ cd ../
$ git commit -m "update submodule B"
$ git push origin main      # for repo A

자체적으로 clone 해온 저장소 B나 서브모듈로 가져온 B나 같은 것이라고 생각하면 이해가 쉽다.

다른 사람이 B를 직접 수정하여 commit & push 한 경우

내가 아닌 다른 공동작업자가 B의 내용을 변경사항을 push하여 내가 그것을 반영해야 할 때에는 바로 업데이트 하고 내 Acommit 하면 된다.

# Current directory: A
$ git submodule update --remote
$ git commit -m "update submodule B"
$ git push origin main      # for repo A

주의할 점

다음은 본인이 submodule을 최근 사용해보면서 느낀 중요한 컨셉 및 일부 에러에 대한 수정 사항이다.

서브모듈 커밋에 대해

위에서 적어온 서브모듈 B의 변경사항을 update하는 것은 이해가 쉽다. 그러나 B의 상태가 변경되었으니 이를 commit & push하는 것은 보통의 것과는 조금 다르다.

B의 변경사항을 commit하는 것은 A 입장에서는 A가 연결할 B의 커밋 번호만을 갱신해주는 작업이다. 그래서 B 하위 경로의 파일 하나하나의 변경 사항을 Agit에 저장하는 것이 아니라 서브모듈 B의 커밋번호만을 관리하는 것이다.

위의 이미지는 깃허브에 서브모듈을 사용한 리포지토리의 예시다. 해당 리포지토리에서는 obj_detector라는 폴더로 서브모듈을 이용한다. 이 때, 서브모듈 리포지토리의 파일들을 관리하는 것이 아닌 79d834e라는 커밋 번호만을 원격 저장소에 기록함으로써, 해당 리포지토리의 서브모듈의 status를 저장한다.

서브모듈 업데이트가 안될 때

submodule update 명령어를 사용하여 업데이트를 시도했는데 안될 때가 있다. 아래는 바로 위의 obj_detector가 포함된 로컬 저장소에서 업데이트를 시도했을 때 나올 수 있는 에러이다.

$ git submodule update --remote

>
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), 389 bytes | 389.00 KiB/s, done.
From https://github.com/dongle94/Object-Detector
   79d834e..ea448c7  main       -> origin/main
 * [new tag]         v0.1.0     -> v0.1.0
fatal: Needed a single revision
Unable to find current origin/master revision in submodule path 'obj_detector'

핵심 에러메세지는 제일 아래의 두 줄이다.

fatal: Needed a single revision
Unable to find current origin/master revision in submodule path 'obj_detector'

서브모듈 경로 obj_detector에 연결된 것을 origin/master 에서 찾을 수 없다고 한다. 과거 MS의 github 인수 이후, 마스터 브랜치가 이제 이름이 main 브랜치로 기본설정 되어 있는 부분이 있다. 그래서 설치되어있는 git의 버전이 낮아 기본적으로 submodule updateorigin/master에서 하려고 하는것이 문제점이자 원인이다.

이에 대한 해결은 .gitmodules 파일을 수정해줘야한다. 위의 예시로 들었던 obj_detector 서브모듈을 사용하는 다른 리포지토리의 .gitmodules 파일을 열면 pathurl은 기본적으로 있는데 아래의 branch는 원래는 없었다.

[submodule "obj_detector"]
	path = obj_detector
	url = https://github.com/dongle94/Object-Detector.git
	branch = main

즉, 처음에 예시로 든 A 리포지토리의 .gitmodules 파일을 열어 branch = main 항목을 넣어 수정해준다. 그러면 제대로 origin/main 브랜치로 부터 정상적으로 업데이트를 수행할 수 있게된다.

참고링크: git offical documents - submodule

Leave a comment