---
id: git-remote-pr
title: "Remote, pull request và code review"
shortTitle: "Remote & pull request"
track: git
trackTitle: "Git & VCS Hosting"
order: 3
level: core
status: published
estimatedMinutes: 45
prerequisites:
  - git-branching
next: git-workflow-release
reviewedAgainst: 2026-10-15
sources:
  - title: "KodeKloud Engineer — 100 Days of DevOps"
    url: https://engineer.kodekloud.com/practice
  - title: "Pro Git — Working with Remotes"
    url: https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes
  - title: "About pull requests"
    url: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests
  - title: "About protected branches"
    url: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches
  - title: "About rulesets"
    url: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets
  - title: "About code owners"
    url: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
  - title: "About merge methods on GitHub"
    url: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github
---

# Remote, pull request và code review

Phần đầu lab chạy hoàn toàn trên máy, dùng một bare repo làm remote. Phần pull request cần một account GitHub và một repo lab riêng.

## Để làm gì

Bạn đẩy branch lên server và mở pull request, để CI chạy và đồng nghiệp review trước khi code vào `main`. Sau đó bạn khoá `main`: không ai push thẳng được, và PR không merge được khi CI đỏ hoặc chưa có review. Đây là chỗ Git nối với CI/CD, vì pull request trở thành cửa duy nhất để thay đổi đi vào production.

## Gồm những gì

- Remote, `origin`, remote-tracking branch (`origin/main`).
- `git remote -v`, `add`, `rename`, `remove` / `rm`, `set-url`, `get-url`.
- `git clone`, `git fetch`, `git pull`, `git push`, upstream tracking.
- Pull request: vòng đời, draft, review, required checks.
- Branch protection và rulesets trên GitHub.
- `CODEOWNERS` để tự động gọi đúng người review.
- Ba kiểu merge: merge commit, squash and merge, rebase and merge.
- Fork: đóng góp vào repo bạn không có quyền push, và giữ fork theo kịp repo gốc.
- Ghi chú ngắn về Azure Repos branch policies.

Workflow CI chi tiết nằm ở bài [GitHub Actions](../cicd-iac/01-github-actions.md). Bài này chỉ nối CI vào pull request như một required check.

## Dùng khi nào

- Repo có từ hai người trở lên, hoặc repo có pipeline deploy: mọi thay đổi đều đi qua PR.
- Repo chứa Terraform hay manifest production: bắt buộc review, vì một dòng sai có thể xoá resource.
- Khi dựng repo mới cho team: bật branch protection hoặc ruleset cho `main` ngay từ ngày đầu.

Repo cá nhân để thử nghiệm thì không cần quy trình nặng, nhưng bạn vẫn nên mở PR để CI chạy.

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

### Remote và remote-tracking branch

Remote là một repo khác mà repo của bạn biết địa chỉ. `git clone` tự tạo remote tên `origin`.

| Lệnh | Việc |
| --- | --- |
| `git remote -v` | In tên và URL fetch/push |
| `git remote add <name> <url>` | Thêm remote. `-f` fetch ngay sau khi thêm |
| `git remote rename <old> <new>` | Đổi tên, cập nhật luôn `origin/*` tương ứng |
| `git remote remove <name>` | Xóa remote và remote-tracking branch. `rm` là tên cũ |
| `git remote get-url <name>` | In URL đang dùng |
| `git remote set-url <name> <newurl>` | Đổi URL. Sai URL cũ thì lệnh fail, không sửa |

`set-url` đổi URL của một remote đã có. Repo fork và repo gốc là hai chỗ khác nhau thì dùng hai remote, không nhét hai URL vào một tên. Docs Git ghi rõ: push URL và fetch URL của cùng một remote vẫn phải trỏ cùng một chỗ.

Git giữ một bản sao trạng thái remote ở các branch dạng `origin/main`. Đó là **remote-tracking branch**: chỉ cập nhật khi bạn `git fetch`, không tự đổi theo server. Vì vậy `git status` báo "up to date with origin/main" chỉ đúng với lần fetch gần nhất.

| Lệnh | Làm gì |
| --- | --- |
| `git fetch` | Tải commit mới, cập nhật `origin/*`. Không đụng branch của bạn |
| `git pull` | `fetch` rồi gộp `origin/<branch>` vào branch hiện tại (merge hoặc rebase) |
| `git push` | Gửi commit của branch local lên remote |
| `git push -u origin feat` | Push và đặt upstream, lần sau chỉ cần `git push` |

Mặc định, `git pull` có thể tạo merge commit khi cả local và remote đều có commit mới. Bạn nên chọn rõ hành vi: `pull.ff only` (chỉ cho fast-forward, lệch thì dừng để bạn tự xử lý) hoặc `pull.rebase true`.

### Pull request

Theo docs GitHub, pull request đề xuất gộp thay đổi từ một branch (head) vào branch khác (base). Vòng đời thường gặp:

1. Push branch rồi mở PR. Nếu chưa xong, mở dạng **draft** để chưa gọi người review.
2. CI chạy trên PR, kết quả hiện thành status check.
3. Reviewer comment, approve, hoặc request changes.
4. Tác giả push thêm commit lên cùng branch. PR tự cập nhật và CI chạy lại.
5. Khi đủ điều kiện thì merge và xoá branch.

PR nhỏ được review kỹ hơn và nhanh hơn. Một PR đổi 30 file Terraform thường chỉ nhận được một câu "LGTM".

### Branch protection và rulesets

Branch protection rule đặt điều kiện cho một branch, ví dụ `main`:

- Require a pull request before merging, kèm số approval tối thiểu.
- Require status checks to pass: chọn job CI cụ thể, ví dụ `test`.
- Require branches to be up to date: branch phải chứa commit mới nhất của `main` trước khi merge.
- Require conversation resolution, signed commits, linear history.
- Chặn force push và chặn xoá branch.

Rulesets là cơ chế mới hơn, cùng mục đích nhưng linh hoạt hơn: bạn có thể áp nhiều ruleset cùng lúc cho một branch, nhắm theo pattern tên branch hoặc tag, đặt ở mức organization cho nhiều repo, và có chế độ evaluate để thử trước khi bật thật. Docs GitHub ghi rõ ruleset và branch protection có thể cùng tồn tại. Khi cả hai áp lên một branch, các rule được gộp lại.

Repo private trên gói Free bị giới hạn tính năng này. Docs protected branches liệt kê gói nào có.

### CODEOWNERS

File `CODEOWNERS` đặt ở `.github/`, gốc repo, hoặc `docs/`. Mỗi dòng là một pattern và các owner. Khi PR đụng file khớp pattern, GitHub tự gọi owner vào review. Bật "Require review from Code Owners" trong branch protection thì PR phải có approval của owner mới merge được.

Pattern khớp **cuối cùng** trong file thắng, nên đặt quy tắc chung ở trên và quy tắc cụ thể ở dưới.

### Ba kiểu merge trên GitHub

| Kiểu | Kết quả trên `main` | Hợp khi |
| --- | --- | --- |
| Create a merge commit | Giữ mọi commit của branch, thêm một merge commit | Muốn giữ đủ history |
| Squash and merge | Gộp cả PR thành một commit | PR nhiều commit nhỏ, muốn `main` mỗi PR một commit |
| Rebase and merge | Áp từng commit lên `main`, không có merge commit | Commit trong PR đã sạch, muốn history thẳng |

Squash giúp `git revert` cả một feature dễ hơn, vì feature chỉ còn là một commit. Đổi lại, bạn mất chi tiết từng bước trong branch.

### Fork

Repo của team thì bạn được push branch. Repo open source, hoặc repo của team khác, thì thường không. Fork giải quyết chỗ đó: GitHub tạo một bản sao của repo dưới account của bạn. Bạn push vào fork, rồi mở pull request từ fork về repo gốc.

Sau khi clone fork, máy bạn có hai remote. Đặt tên theo quy ước để khỏi nhầm:

| Remote | Trỏ tới | Dùng để |
| --- | --- | --- |
| `origin` | Fork của bạn | Push branch |
| `upstream` | Repo gốc | Fetch commit mới của chủ repo |

Fork không tự cập nhật. Repo gốc có commit mới thì fork vẫn đứng yên. Trước khi tạo branch mới, fetch `upstream` rồi đưa `main` của bạn lên theo. Trên web, GitHub có nút **Sync fork**. Từ terminal, `gh repo sync` làm cùng việc.

Repo trong cùng team thì đừng fork. Push branch thẳng lên repo chung, để CI và branch protection áp đúng chỗ.

### Azure Repos

Azure Repos gọi cơ chế này là branch policies: số reviewer tối thiểu, linked work item, build validation (chạy pipeline trên PR), required reviewers theo đường dẫn. Mục đích giống branch protection của GitHub.

## Lệnh và code mẫu

Remote giả lập trên máy bằng bare repo:

```bash
mkdir -p "$HOME/devops-lab/git-remote"
cd "$HOME/devops-lab/git-remote"
git init --bare -b main server.git
git clone server.git alice
```

Alice đẩy commit đầu tiên. Lúc clone repo trống, Git in cảnh báo "You appear to have cloned an empty repository"; đó là chuyện bình thường:

```bash
cd "$HOME/devops-lab/git-remote/alice"
printf 'replicas: 2\n' > deploy.yaml
git add deploy.yaml
git commit -m "Add deploy config"
git push -u origin main
```

Bob clone sau khi đã có commit, rồi làm feature:

```bash
cd "$HOME/devops-lab/git-remote"
git clone server.git bob
cd bob
git switch -c feature/scale
sed -i 's/2/4/' deploy.yaml
git commit -am "Scale to 4 replicas"
git push -u origin feature/scale
```

Kiểm tra trạng thái so với remote:

```bash
git fetch --prune
git status
git log --oneline --graph --all
git branch -vv     # mỗi branch đang track remote nào, ahead/behind bao nhiêu
```

Chọn hành vi pull:

```bash
git config --global pull.ff only
```

Giả lập fork bằng một bare repo thứ hai. `server.git` là repo gốc, `fork.git` là bản sao của Carol:

```bash
cd "$HOME/devops-lab/git-remote"
git clone --bare server.git fork.git
git clone fork.git carol
cd carol
git remote add upstream ../server.git
git remote -v
git remote get-url origin
git remote rename origin fork
git remote rename fork origin
```

Repo gốc có commit mới, Carol đồng bộ fork:

```bash
git fetch upstream
git switch main
git merge --ff-only upstream/main
git push origin main
```

Trên GitHub thật, bước `git clone --bare` là nút **Fork**. Đồng bộ bằng nút **Sync fork**, hoặc `gh repo sync` sau khi đã `gh auth login`.

`CODEOWNERS` mẫu, đặt ở `.github/CODEOWNERS`:

```text
# Mặc định: team platform review mọi thứ
*                    @my-org/platform

# Hạ tầng cần thêm người của team infra
/infra/              @my-org/infra
*.tf                 @my-org/infra

# Workflow CI do người phụ trách DevOps duyệt
/.github/workflows/  @devops-lead
```

Tạo PR bằng GitHub CLI (`gh`), sau khi đã `gh auth login`:

```bash
gh pr create --base main --head feature/scale \
  --title "Scale API to 4 replicas" \
  --body "Traffic giờ cao điểm tăng gấp đôi, 2 replica chạm CPU limit."
gh pr checks          # xem status check của PR
gh pr view --web
```

## Thực hành

1. Chạy lab bare repo: Alice push `main`, Bob clone, tạo `feature/scale` và push.
2. Trong thư mục Alice, `git fetch`, rồi `git branch -r` để thấy `origin/feature/scale`.
3. Alice sửa `deploy.yaml` trên `main` và push. Bob chạy `git pull` trên `main` với `pull.ff only`, xác nhận fast-forward thành công.
4. Trên GitHub, tạo repo lab, push code có workflow CI của bài GitHub Actions.
5. Settings → Rules → Rulesets (hoặc Branches): với `main`, bật require pull request, 1 approval, required status check là job `test`, chặn force push.
6. Thử `git push origin main` trực tiếp. Sau đó mở PR từ một branch có test fail và xem nút merge bị khoá.
7. Sửa test cho xanh, merge bằng squash and merge.
8. Chạy lab fork: tạo `fork.git`, clone thành `carol`, thêm remote `upstream`. Alice push một commit mới lên `server.git`. Carol fetch `upstream`, fast-forward `main`, rồi push lên `origin`.

## Kết quả mong đợi

- `git branch -vv` hiện `[origin/feature/scale]` cạnh branch của Bob.
- Push thẳng vào `main` bị từ chối với thông báo vi phạm rule.
- PR có test fail hiện check đỏ, nút merge bị khoá hoặc cần bypass.
- Sau squash and merge, `main` có thêm đúng một commit cho cả PR.
- `git remote -v` trong `carol` có hai dòng `origin` trỏ `fork.git` và hai dòng `upstream` trỏ `server.git`. Sau khi đồng bộ, `git log --oneline -1` của Carol trùng commit mới nhất của Alice.

## Checklist

- [ ] Giải thích `origin/main` khác `main` thế nào và khi nào nó cập nhật.
- [ ] Phân biệt `git fetch` với `git pull`.
- [ ] Push branch mới kèm upstream bằng `-u`.
- [ ] Bật rule cho `main`: bắt buộc PR, required check, chặn force push.
- [ ] Viết `CODEOWNERS` với quy tắc chung ở trên, cụ thể ở dưới.
- [ ] Chọn được kiểu merge phù hợp và nói được đánh đổi.
- [ ] Clone fork, thêm `upstream`, và đồng bộ `main` trước khi tạo branch mới.
- [ ] Đổi URL bằng `git remote set-url`. Thêm remote thứ hai bằng `git remote add`.

## Thẻ ôn

### git-remote-pr-fetch
kind: distinguish
cue: `git status` báo up to date chỉ so với `origin/main` lần fetch gần nhất. Remote có commit mới thì phải `git fetch` trước đã.

Q: Đồng nghiệp nói `main` trên remote vừa có 5 commit. Trên máy bạn, `git status` vẫn báo nhánh up to date với `origin/main`. Vì sao Git không thấy 5 commit đó, và `fetch` khác `pull` chỗ nào?

A: `origin/main` trên máy bạn là bản remote Git nhớ từ lần liên lạc gần nhất, không phải trạng thái sống của server. Nó chỉ đổi khi `git fetch` hoặc `git pull`. `fetch` tải commit và cập nhật con trỏ remote, không trộn vào nhánh bạn đang đứng. `pull` fetch rồi merge hoặc rebase vào nhánh hiện tại. Muốn biết nhánh đã mới chưa, fetch trước rồi mới đọc status. Kết luận up to date khi chưa fetch là kết luận trên dữ liệu cũ.

### git-remote-pr-rules
kind: decide
cue: Nhánh dùng để deploy nên bắt buộc pull request, required status check, và chặn force push.

Q: `main` là nhánh pipeline dùng để deploy. Bạn được hỏi nên bật rule gì để code không vào nhánh đó trước khi có người đọc và CI chạy. Ba rule nào, và push thẳng làm hỏng điều gì?

A: Bắt buộc pull request, nên không ai commit thẳng vào `main`. Bắt buộc required status check, nên nút merge không mở khi job chưa xanh. Chặn force push, nên không ai viết lại history của nhánh deploy. Thiếu rule đầu, push thẳng đưa code vào nhánh production trước khi CI của pull request kịp chạy. Ba rule đi cùng nhau: cửa vào, bằng chứng CI, và khóa history.

### git-remote-pr-check-name
kind: trap
cue: PR kẹt "Expected — Waiting for status" dù job đã xanh: tên required check không khớp tên job.

Q: Pull request không merge được. Dòng trạng thái là "Expected — Waiting for status to be reported". Bạn mở log CI và job đã xanh. Kẹt ở đâu?

A: Required status check đang chờ một tên cụ thể. Tên đó phải trùng tên job đã từng báo trạng thái trên repo. Job chạy xanh dưới một tên khác thì GitHub vẫn coi check bắt buộc là chưa có, và nút merge không mở. Sai một chữ, thêm hoặc bớt khoảng trắng, là đủ để treo. Sửa tên check cho khớp job, không tắt check cho qua.

### git-remote-pr-bypass
kind: trap
cue: Admin bypass rule rồi push thẳng vào `main` thì CI không chạy trước khi code vào nhánh deploy.

Q: Rule đã bắt buộc pull request và required check. Một admin được phép bypass, rồi push thẳng một commit vào `main` vì đang gấp. CI có chạy trước khi commit đó vào nhánh deploy không?

A: Không. Bypass nghĩa là commit không đi qua cửa pull request, nên required check không phải điều kiện của lần đẩy này. Người có quyền bypass vẫn đưa code vào đúng nhánh pipeline đang deploy. Hạn chế ai được bypass. Nếu cần ngoại lệ, vẫn đi qua pull request rồi mới bypass check, để còn thấy diff và còn có chỗ CI chạy.

### git-remote-pr-size
kind: decide
cue: Một pull request sửa API, UI và pipeline cùng lúc thì reviewer không đọc hết, và lúc lỗi không deploy riêng từng phần được.

Q: Một pull request sửa API, giao diện và file pipeline trong cùng một diff. Review xong, merge một lần, pipeline deploy tất cả. Rủi ro lúc review và lúc deploy là gì?

A: Diff quá rộng thì reviewer lướt, lỗi nằm giữa phần họ không đọc kỹ sẽ lọt qua. Khi production chỉ hỏng một trong ba phần, không có cách đưa lại hai phần kia mà gỡ một phần, vì chúng đã thành một lần merge. Chia thành nhiều pull request. Mỗi pull request là một thay đổi có thể review hết và deploy một mình.

### git-remote-pr-url
kind: distinguish
cue: Đổi URL của remote đã có thì `git remote set-url`. Thêm một chỗ khác thì `git remote add`. Một remote không giữ fetch một nơi, push một nơi khác.

Q: Repo đổi host, cùng một `origin`. Có người `git remote add origin` URL mới, hoặc đặt fetch từ `upstream` và push lên `origin` trong cùng một tên. Lệnh đúng là gì?

A: `git remote add origin` fail vì tên `origin` đã có. `git remote set-url origin <url-moi>` đổi URL của remote đó. Cần thêm repo gốc cạnh fork thì `git remote add upstream <url>`, hai tên hai chỗ. Docs Git: fetch URL và push URL của một remote vẫn phải cùng một repo. Fetch từ gốc rồi push lên fork là hai remote, không phải hai URL trên một tên.

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

**Đọc `git status` mà không fetch.** Git báo "up to date" trong khi remote đã có 5 commit mới. Chạy `git fetch` trước khi kết luận.

**`git pull` trên branch đã lệch, sinh merge commit lạ trong history.** Đặt `pull.ff only` hoặc `pull.rebase true` để hành vi rõ ràng.

**Bật required status check nhưng gõ sai tên job.** PR treo mãi ở "Expected — Waiting for status to be reported". Tên check phải khớp tên job đã từng chạy trên repo.

**Cho admin bypass rule rồi quên.** Người có quyền admin vẫn push thẳng vào `main` được, và CI không chạy. Hạn chế bypass, hoặc chỉ cho bypass qua PR.

**PR quá lớn.** Reviewer không đọc hết nên lỗi dễ lọt qua. Hãy chia thành nhiều PR, mỗi PR là một thay đổi có thể deploy riêng.

**Làm trên fork cũ vài tháng không đồng bộ.** Branch mới tách từ `main` đã lỗi thời, PR mở ra là conflict hàng loạt. Fetch `upstream` và fast-forward `main` trước khi tạo branch.

**`git remote add origin` khi `origin` đã có.** Tên trùng thì lệnh fail. Đổi URL thì `set-url`.

## Nguồn

1. [KodeKloud Engineer — 100 Days of DevOps](https://engineer.kodekloud.com/practice) — đề cương ngày remote, fork, pull request. Trang practice cần đăng nhập. Bài này viết lại lệnh, không chép lab.
2. [KodeKloud — GIT for Beginners](https://learn.kodekloud.com/learn/courses/git-for-beginners) — phạm vi module Initialize Remote Repositories: push, clone, pull request, fetch và pull, merge conflict, fork.
3. [Pro Git — Working with Remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) — remote, fetch, pull, push.
4. [Pro Git — Remote Branches](https://git-scm.com/book/en/v2/Git-Branching-Remote-Branches) — remote-tracking branch, upstream.
5. [git-remote](https://git-scm.com/docs/git-remote) — `add`, `rename`, `remove`, `set-url`, `get-url`.
6. [About pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) — GitHub, vòng đời PR và draft.
7. [About protected branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) và [About rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets) — các rule cho branch và cách chúng gộp với nhau.
8. [About code owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) — vị trí file, cú pháp, pattern cuối cùng thắng.
9. [About merge methods on GitHub](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github) — merge commit, squash, rebase.
10. [Fork a repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) và [Syncing a fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) — GitHub, remote `upstream`, Sync fork, `gh repo sync`.
11. [Azure Repos branch policies](https://learn.microsoft.com/en-us/azure/devops/repos/git/branch-policies) — Microsoft Learn, tương đương branch protection.
12. [GitHub CLI — gh pr create](https://cli.github.com/manual/gh_pr_create) — tạo PR từ terminal.

## Tiếp theo

[Branching strategy, tag và release](./04-workflow-release.md)
