본문으로 건너뛰기
Previous
Next
Git Submodules in Practice | Add· Update

Git Submodules in Practice | Add· Update

Git Submodules in Practice | Add· Update

이 글의 핵심

Git submodules let you embed another Git repository inside yours, pinned to a specific commit. This guide covers add/update/delete workflows, CI setup, common pitfalls (empty folders, detached HEAD), and when monorepos are a better choice.

What Is a Git Submodule?

A Git submodule is a Git repository embedded inside another Git repository. The outer repo (called the superproject) records the exact commit SHA of the inner repo. This gives you:

  • Source code sharing without publishing to a package registry
  • Strict version pinning — you control exactly which commit of the dependency you use
  • Separate commit history and access control for each repo

The cost: operations that feel automatic in a single repo (clone, pull, CI) require extra steps.

superproject/
├── .gitmodules           ← maps submodule paths to their URLs
├── src/
├── vendor/
│   └── shared-lib/       ← this is a gitlink, not a real directory in superproject
└── ...

Adding a Submodule

# Add a submodule at vendor/shared-lib
git submodule add https://github.com/org/shared-lib.git vendor/shared-lib

# This creates/updates .gitmodules and stages the gitlink
git commit -m "chore: add shared-lib submodule"

The resulting .gitmodules file:

[submodule "vendor/shared-lib"]
    path = vendor/shared-lib
    url = https://github.com/org/shared-lib.git
    branch = main   # optional: track a branch

The superproject now contains a gitlink — an entry in the tree that points to a specific SHA in the child repo. It is not a regular file or directory from Git’s perspective.


Cloning a Repo with Submodules

Regular git clone does not fetch submodules:

# All-in-one: clone and initialize all submodules
git clone --recurse-submodules https://github.com/org/main-app.git

# If you already cloned without --recurse-submodules:
git submodule update --init --recursive

After git pull that updates a submodule pointer:

git pull
git submodule update --init --recursive   # update to the new SHA

Put this in your README. Every developer who doesn’t know about submodules will hit the “empty folder” problem exactly once, then need this command.


Updating a Submodule to Latest

The superproject does not automatically follow the latest commit in the child repo. You bump it deliberately:

# Enter the submodule
cd vendor/shared-lib

# Get the latest commits
git fetch origin
git checkout main
git pull

# Go back and record the new SHA
cd ../..
git add vendor/shared-lib
git status
# modified: vendor/shared-lib (new commits)
git commit -m "chore: bump shared-lib to latest main"
git push

Your teammates then:

git pull
git submodule update --init --recursive

Update All Submodules at Once

# Fetch and merge latest for all submodules tracking a branch
git submodule update --remote --merge

# Then review, add, and commit
git add .
git commit -m "chore: bump all submodules"

Removing a Submodule

Git has no single git submodule remove command. The steps are:

# 1. Remove from .gitmodules
git config -f .gitmodules --remove-section submodule.vendor/shared-lib

# 2. Stage the .gitmodules change
git add .gitmodules

# 3. Remove the submodule from the index and working tree
git rm -f vendor/shared-lib

# 4. Clean up the .git/modules cache
rm -rf .git/modules/vendor/shared-lib

# 5. Commit
git commit -m "chore: remove shared-lib submodule"

CI Configuration

Every CI run needs to initialize submodules. Two approaches:

GitHub Actions

# .github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: 'recursive'    # initializes all submodules
          token: ${{ secrets.GH_PAT }}  # required for private submodules

Shell Script

# If you use a manual checkout step
git checkout $COMMIT_SHA
git submodule update --init --recursive --depth 1  # --depth 1 for faster CI

# Or in a Makefile:
setup:
    git submodule update --init --recursive

Caching Submodules in CI

- name: Cache submodule data
  uses: actions/cache@v4
  with:
    path: vendor/
    key: submodules-${{ hashFiles('.gitmodules') }}-${{ github.sha }}
    restore-keys: |
      submodules-${{ hashFiles('.gitmodules') }}-

Include the submodule directory in the cache key based on .gitmodules — this invalidates the cache when the submodule URL or path changes.


Common Pitfalls

Detached HEAD in the Submodule

When you enter a submodule after git submodule update, you’re in detached HEAD — you’re not on any branch:

cd vendor/shared-lib
git status
# HEAD detached at a1b2c3d

If you make changes here without creating a branch first, those commits may be lost when you update again:

# Before making changes in a submodule:
git checkout -b my-feature   # create a branch
# ... make changes, commit ...
git push origin my-feature   # push to the child repo's remote

Permission Errors in CI

Private submodules require the CI runner to have access to the child repo:

# Option 1: SSH deploy key on the child repo (read-only)
# Add the public key to the child repo's Deploy Keys

# Option 2: GitHub token with repo scope
# Use secrets.GH_PAT in the checkout action

# Option 3: Machine user with access to all repos
# Create a bot account, add it as collaborator on child repos

The .git/modules Cache

After git rm -f vendor/shared-lib, the .git/modules/vendor/shared-lib directory still exists. If you re-add the same submodule path later (even at a different URL), Git uses the cached data and may fail. Always clean it:

rm -rf .git/modules/vendor/shared-lib

Comparing Multi-Repo Strategies

ApproachBest whenDownsides
Git submoduleStrict version pinning, private source-level sharing, no registryMore complex clone/CI, detached HEAD confusion
Git subtreeWant to merge history into one repo, rare upstream syncsHeavier upstream sync workflow
npm/pip/cargo packagePublished library, semantic versioning, public or private registryNeeds publish step for every change
MonorepoTeams change multiple packages together, unified CI pipelineRepo size grows, permission design more complex

When submodules make sense:

  • Shared protobuf/schema files used by services in different languages
  • Internal C++ library shared between firmware and desktop apps
  • Test data or fixtures that are maintained as their own project

When to prefer packages: if you’re sharing JavaScript/TypeScript utilities between Node.js services, publish to a private npm registry (GitHub Packages, Verdaccio) instead — standard dependency tools work better than submodules for this use case.


Workflow Checklist

Add to your README.md:

## Setup

Clone with submodules:
```bash
git clone --recurse-submodules https://github.com/org/main-app.git

If you already cloned:

git submodule update --init --recursive

After every git pull:

git submodule update --init --recursive

---

## Key Takeaways

- **Superproject records a SHA**, not "latest" — submodule updates are deliberate commits in the parent
- **Always clone with `--recurse-submodules`** or run `git submodule update --init --recursive` after cloning
- **CI**: use `submodules: recursive` in actions/checkout or add the update step explicitly; use `--depth 1` for faster shallow fetches
- **Detached HEAD**: always create a branch before committing inside a submodule
- **Remove**: four manual steps — remove from `.gitmodules`, `git rm`, remove `.git/modules` cache, commit
- **Private repos**: need a deploy key or machine user token in CI
- **Consider packages** when the shared code has semantic versioning and a publish workflow fits your process


---

## 자주 묻는 질문 (FAQ)

### Q. 이 내용을 실무에서 언제 쓰나요?

**A.** Git submodules: how to pull sub-repos, init/update/delete, CI caching, common pitfalls, and when to prefer a monorepo in… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

### Q. 선행으로 읽으면 좋은 글은?

**A.** 각 글 하단의 **이전 글** 또는 **관련 글** 링크를 따라가면 순서대로 배울 수 있습니다. [C++ 시리즈 목차](/blog/cpp-series-index/)에서 전체 흐름을 확인할 수 있습니다.

### Q. 더 깊이 공부하려면?

**A.** [cppreference](https://en.cppreference.com/)와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

---

## 같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

- [Git push pull 차이 | 원격 저장소·GitHub 협업·Pull Request 완벽 가이드](/blog/git-series-03-remote-collaboration/)
- [Git 머지 충돌 해결 실전 사례 | 대규모 리팩토링 브랜치 병합기](/blog/git-case-study-01-merge-conflict-resolution/)

---

## 이 글에서 다루는 키워드 (관련 검색어)

**Git**, **Submodule**, **Multi-repo**, **Dependency management**, **CI**, **Collaboration** 등으로 검색하시면 이 글이 도움이 됩니다.