---
id: git-monorepo
title: "Monorepo"
shortTitle: "Monorepo"
track: git
trackTitle: "Git & VCS Hosting"
order: 8
level: normal
status: published
estimatedMinutes: 30
prerequisites:
  - git-gitlab
next: linux-shell-files
reviewedAgainst: 2026-10-12
sources:
  - title: "git-sparse-checkout"
    url: https://git-scm.com/docs/git-sparse-checkout
  - title: "GitHub CODEOWNERS"
    url: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-security/customizing-your-repository/about-code-owners
---

# Monorepo

Monorepo là một Git repo chứa nhiều phần của hệ thống, ví dụ web, API và thư viện dùng chung. Ngược lại, chia mỗi phần một repo riêng thì gọi là polyrepo.

## Để làm gì

Sau bài này, bạn clone được chỉ những thư mục mình cần, và hiểu vì sao một commit ở `web/` vẫn có thể kích pipeline của `api/` khi chưa đặt path filter.

## Gồm những gì

- Khi nào gom chung một repo có lợi, khi nào nó làm CI chậm đi.
- `git sparse-checkout` để chỉ lấy một phần cây file.
- CODEOWNERS để đúng người review đúng thư mục.

Bài này không nói về build tool như Nx, Bazel hay Turborepo. Các tool đó chạy bên trên Git chứ không thay Git.

## Dùng khi nào

Monorepo hợp khi vài service cùng đổi theo một contract và team muốn một pull request sửa cả hai bên. Nó cũng hợp khi một thư viện nội bộ luôn phải đi cùng version với app.

Đừng gộp repo chỉ vì trên sơ đồ kiến trúc chúng nằm chung một ô. Nếu hai sản phẩm release theo lịch khác nhau và cần quyền khác nhau, để hai repo riêng sẽ đỡ rắc rối hơn.

## Kiến thức cốt lõi

Git vẫn version từng file như bình thường; monorepo không có object nào đặc biệt. Thứ thay đổi là cách CI và quy trình review dựa vào đường dẫn.

Một push chỉ đụng `web/package.json` thì không nên build image `api`, nên workflow cần path filter. Trên GitHub Actions, đó là `on.push.paths`. Không có filter, mọi job chạy cho mọi commit, hàng đợi dài ra, và dần dần người ta quen bỏ qua CI đỏ.

`git sparse-checkout` giới hạn những gì xuất hiện trong working tree. Nếu clone không dùng filter thì Git vẫn tải blob của toàn bộ history. `git clone --filter=blob:none --sparse` lấy commit mà chưa tải hết file. Sau đó `git sparse-checkout set api` chỉ materialize thư mục `api`.

CODEOWNERS là một file text, thường đặt ở `.github/CODEOWNERS`. Dòng `/api/ @platform` yêu cầu team đó review khi diff chạm vào `/api/`. File này không chặn commit local.

## Lệnh và code mẫu

Đoạn dưới tạo repo local để bạn thấy sparse checkout hoạt động, không cần remote:

```bash
mkdir -p ~/devops-lab/mono && cd ~/devops-lab/mono
git init -b main
mkdir -p web api && printf 'web\n' > web/README.md && printf 'api\n' > api/README.md
git add . && git commit -m "init"
cd ..
git clone --filter=blob:none --sparse ./mono mono-api
cd mono-api
git sparse-checkout set api
find . -type f -not -path './.git/*'
```

Mẫu CODEOWNERS:

```text
/api/   @platform
/web/   @frontend
```

## Thực hành

1. Chạy các lệnh trên.
2. Xác nhận working tree của `mono-api` có `api/README.md` và không có `web/README.md`.
3. Xóa cả hai thư mục lab.

## Kết quả mong đợi

`find` chỉ in file dưới `api/`, không có `web/README.md`.

## Checklist

- [ ] CI của mỗi app có path filter, không build cả monorepo cho một dòng README.
- [ ] Quyền review theo thư mục nằm trong CODEOWNERS, không chỉ trong trí nhớ của team.
- [ ] Tách repo khi hai phần không chung lịch release và không chung quyền.

## Thẻ ôn

### git-monorepo-filter
kind: distinguish
cue: Sparse checkout chỉ bớt file trên máy bạn. Job CI nào chạy là do path filter trong workflow, không phải do thư mục bạn đang thấy.

Q: Monorepo có `api`, `web` và README ở gốc. Trên máy bạn, sparse checkout chỉ materialize thư mục `api`, nên bạn không thấy `web`. Sửa một dòng README rồi push. CI có tự hiểu là chỉ được build `api` không? Cái gì quyết định job nào chạy, và ai được review từng thư mục?

A: Không. Sparse checkout là cách máy bạn đỡ tải file xuống đĩa. Bản clone trên CI vẫn là cả repo, và nó không nhìn cấu hình sparse trên laptop của bạn. Job nào chạy do path filter trong workflow: đổi file khớp filter thì job tương ứng chạy, đổi một dòng README ở gốc thì không nên kéo build mọi app. Quyền review theo thư mục ghi trong CODEOWNERS, ví dụ `/api/` một nhóm, `/web/` một nhóm. Hai việc đó không thay cho nhau.

## Lỗi thường gặp

**Sparse checkout trên máy rồi tưởng CI cũng chỉ thấy thư mục đó.** Sparse checkout chỉ tiết kiệm đĩa trên máy bạn. Runner CI clone theo cấu hình của workflow, và path filter mới là thứ quyết định job nào chạy.

**Thư mục `libs/` không có owner.** Ai cũng sửa contract chung mà không ai review. Thêm dòng CODEOWNERS cho thư mục đó trước khi repo có service thứ ba.

## Nguồn

1. [git-sparse-checkout](https://git-scm.com/docs/git-sparse-checkout) — `set`, cone mode, và clone `--sparse`.
2. [About code owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-security/customizing-your-repository/about-code-owners) — cú pháp file CODEOWNERS.

## Tiếp theo

[Đăng nhập, man, và file](../linux/00-shell-files.md)
