# 🔀 Merge, conflict và cách đọc dấu conflict

## Mục tiêu bài học

Sau bài này, bạn phải làm được:

- Phân biệt merge fast-forward, merge `--no-ff` và squash, chọn đúng kiểu cho từng tình huống.
- Đọc đúng từng dòng conflict marker: `<<<<<<<`, `=======`, `>>>>>>>`.
- Giải quyết conflict mà **giữ code của cả hai phía**, không xóa mất bài viết của đồng đội trong project blog.
- Biết khi nào nên `git merge --abort` thay vì cố sửa tiếp.

## Ôn tập nhanh

Bài trước bạn đã biết branch chỉ là con trỏ nhẹ, tạo bằng `git switch -c feature/...` và luôn tách từ `main` mới nhất. Bài này dùng đúng nền đó: khi hai branch cùng sửa một file, `git merge` sẽ đưa bạn vào trạng thái conflict để tự quyết định.

## Lý thuyết

### Merge là gì

`git merge feature/x` lấy thay đổi của `feature/x` gộp vào nhánh bạn đang đứng (thường là `main`). Git không "ghi đè" file, nó tính toán dựa trên **base** (commit chung gần nhất của hai nhánh) rồi ghép ba phiên bản: base, phiên bản của bạn, phiên bản của feature.

| Kiểu merge | Lệnh | Khi nào dùng |
| --- | --- | --- |
| Fast-forward | `git merge feature/x` (mặc định khi được) | `main` chưa có commit mới, Git chỉ tiến con trỏ, lịch sử thẳng, không có merge commit |
| No-ff | `git merge --no-ff feature/x` | Muốn một merge commit đánh dấu "feature này đã được gộp", nhìn log thấy rõ cụm việc |
| Squash | `git merge --squash feature/x` | Gộp nhiều commit lộn xộn thành một thay đổi, thường dùng khi merge PR |

Với project blog, thói quen của tôi là: nhánh feature ngắn ngày thì `--no-ff` để lịch sử còn dấu vết, còn nhánh có 7 commit "sửa lỗi chính tả", "sửa lại", "sửa lần 3" thì squash.

### Conflict sinh ra khi nào

Git tự merge khi hai nhánh sửa **khác dòng** hoặc thêm file khác nhau. Conflict chỉ xuất hiện khi hai nhánh sửa **cùng một vùng** của một file. Ví dụ: bạn sửa tiêu đề `posts/git-co-ban.md` trên `main`, đồng đội cũng sửa đúng dòng đó trên `feature/sua-tieu-de`.

```text
<<<<<<< HEAD
# Git cơ bản: commit đầu tiên
=======
# Git cơ bản: làm quen với commit
>>>>>>> feature/sua-tieu-de
```

Cách đọc, nhớ đúng thứ tự này:

- `<<<<<<< HEAD`: bắt đầu phần của **nhánh bạn đang đứng**, tức code hiện tại của bạn.
- `=======`: vạch ngăn. ==Phần giữa `=======` và `>>>>>>>` là code đến từ nhánh đang được merge vào==.
- `>>>>>>> feature/sua-tieu-de`: hết phần của nhánh kia.

Điều quan trọng nhất: **Git không làm mất code của ai cả**. Cả hai phiên bản đều nằm trong file. Người xóa code là bạn, nếu bạn xóa nhầm cả block.

> [!NOTE]
> Bật thêm base để thấy code gốc trước khi hai nhánh sửa: `git config --global merge.conflictStyle diff3`. Khi đó marker có thêm phần `|||||||` (base) nằm giữa, đọc dễ hơn nhiều khi conflict phức tạp.

### Quy trình xử lý conflict

```steps
1. Xem file nào đang conflict: `git status` (mục "Unmerged paths").
2. Mở file, đọc cả hai phía, quyết định nội dung cuối cùng (có thể là kết hợp cả hai).
3. Xóa sạch ba dòng marker `<<<<<<<`, `=======`, `>>>>>>>`.
4. Chạy thử project blog (`npm run dev`) để chắc nội dung không vỡ.
5. `git add posts/git-co-ban.md` để đánh dấu đã giải quyết.
6. `git commit` (hoặc `git merge --continue`) để hoàn tất merge.
```

Nếu thấy rối, thoát ra làm lại: `git merge --abort` đưa mọi thứ về trạng thái trước khi merge, an toàn tuyệt đối.

## Ví dụ minh họa

Bối cảnh: blog của bạn, `main` có bài `posts/git-co-ban.md`. Đồng đội tạo `feature/sua-tieu-de` để đổi tiêu đề, bạn ở `main` thêm dòng mô tả vào cùng khu vực đó.

```bash
# Bạn đang ở main
git switch main
# Đồng đội merge xong trước, giờ bạn merge phần của mình
git merge feature/sua-tieu-de
# Auto-merging posts/git-co-ban.md
# CONFLICT (content): Merge conflict in posts/git-co-ban.md
```

Mở file, bạn thấy:

```text
<<<<<<< HEAD
# Git cơ bản: commit đầu tiên
Bài viết này hướng dẫn commit đầu tiên.
=======
# Git cơ bản: làm quen với commit
>>>>>>> feature/sua-tieu-de
```

Đọc theo đúng ba dòng marker: phía `HEAD` là bản của bạn (tiêu đề cũ + dòng mô tả mới), phía dưới là bản của đồng đội (tiêu đề mới, không có dòng mô tả). Bạn muốn **giữ cả hai**: tiêu đề mới của đồng đội, cộng dòng mô tả của bạn.

```text
# Git cơ bản: làm quen với commit
Bài viết này hướng dẫn commit đầu tiên.
```

Sau đó:

```bash
git add posts/git-co-ban.md
git commit            # Git mở sẵn message "Merge branch 'feature/sua-tieu-de'"
git log --oneline --graph -5
```

Kết quả log có merge commit với hai nhánh cha, code của cả hai người đều còn.

Trường hợp ngược lại: nếu bạn mở file ra và thấy conflict rối vì đồng đội vừa đổi cấu trúc file, đừng cố đoán. Chạy `git merge --abort`, nhắn đồng đội cùng ngồi sửa, rồi merge lại. Mười phút trao đổi rẻ hơn một buổi chiều sửa blog hỏng.

## Luyện tập có hướng dẫn

Vẫn trên project blog, mỗi bài làm trên nhánh riêng rồi merge vào `main`.

**Bài 1.** Trên `main`, sửa dòng cuối `README.md`. Tạo `feature/them-lien-ket`, cũng sửa đúng dòng đó, commit. Merge vào `main`. Đọc conflict marker và giải quyết sao cho câu văn giữ được ý của cả hai.

<details><summary>Gợi ý</summary>

Xác định phần `HEAD` (bản trên `main`) và phần dưới `=======` (bản của feature). Viết lại một câu duy nhất kết hợp hai ý, xóa cả ba marker, `git add README.md`, `git commit`.

</details>

**Bài 2.** Tạo tình huống conflict ba phía: trên `main` sửa tiêu đề `posts/hello-git.md`, trên `feature/gioi-thieu` xóa luôn dòng tiêu đề đó và thêm đoạn mở bài. Merge và quyết định: giữ tiêu đề của `main` hay theo cấu trúc mới của feature?

<details><summary>Gợi ý</summary>

Đây là conflict "modify/delete" hoặc conflict nội dung phức tạp, không có đáp án tự động. Đọc `git log --oneline -- posts/hello-git.md` để hiểu ý định từng bên, chọn hướng có lợi cho người đọc blog, rồi mới `git add`. Nếu không chắc, `git merge --abort` và hỏi lại người viết.

</details>

**Bài 3.** Thử `git config merge.conflictStyle diff3`, tạo lại conflict ở bài 1 và so sánh: phần base giúp bạn hiểu gì mà bản mặc định không cho thấy?

<details><summary>Gợi ý</summary>

Base cho bạn thấy dòng gốc trước khi cả hai bên sửa, nhờ đó biết ai thêm gì, ai xóa gì, thay vì chỉ thấy hai khối đối lập.

</details>

## Expert Corner

- **Đọc cả hai phía trước khi xóa bất cứ thứ gì.** 90% lỗi mất code khi merge là do xóa nguyên một block cho nhanh.
- **Đừng bao giờ commit khi còn marker.** Kiểm tra nhanh: `git diff --check` hoặc `grep -rn "<<<<<<<" .`. Còn marker là blog hiển thị rác.
- **`git merge -X ours` không phải "giữ code của tôi" một cách an toàn.** Nó tự chọn phía bạn ở mọi conflict, code đồng đội biến mất im lặng. Không dùng để "merge cho xong".
- **Merge sớm, merge thường xuyên.** Conflict tỉ lệ thuận với thời gian sống của branch. Branch một hai ngày gần như không bao giờ conflict đau.
- **Trong rebase, ý nghĩa `ours` và `theirs` bị đảo** so với merge. Đây là bẫy rất hay gặp, bài sau sẽ nói rõ khi ta rebase trước khi cắt release `v1.0`.

> [!WARNING]
> `git checkout --ours <file>` lấy toàn bộ file theo một phía, không chỉ riêng vùng conflict. Với file dài, bạn sẽ mất các thay đổi khác mà không nhận ra. Chỉ dùng khi bạn thật sự muốn bỏ toàn bộ bản của một bên.

## Tóm tắt và bước tiếp theo

Bốn câu chốt:

- Merge gộp lịch sử hai nhánh; fast-forward khi `main` không có commit mới, `--no-ff` khi muốn giữ dấu vết feature.
- Conflict xảy ra khi hai nhánh sửa cùng vùng; Git giữ cả hai phía, không làm mất code.
- `<<<<<<< HEAD` là bản của bạn, `=======` là vạch ngăn, `>>>>>>>` là bản của nhánh đang merge vào.
- Sửa file, xóa marker, `git add`, `git commit`; rối thì `git merge --abort`.

Bài sau: **Rebase, tag và cắt release v1.0**. Bạn sẽ dùng rebase để làm phẳng lịch sử trước khi release, và học cách gắn tag `v1.0` cho đúng commit.

```flashcard
[
  { "front": "Merge conflict xảy ra khi nào?", "back": "Khi hai nhánh sửa cùng một vùng của cùng một file và Git không tự quyết định được." },
  { "front": "Dòng `<<<<<<< HEAD` nghĩa là gì?", "back": "Bắt đầu phần code của nhánh bạn đang đứng (HEAD)." },
  { "front": "Dòng `=======` trong conflict marker là gì?", "back": "Vạch ngăn giữa phần của HEAD và phần của nhánh đang được merge vào." },
  { "front": "Dòng `>>>>>>> feature/sua-tieu-de` nghĩa là gì?", "back": "Kết thúc phần code đến từ nhánh feature/sua-tieu-de." },
  { "front": "Ba bước kết thúc một merge conflict?", "back": "Sửa file và xóa hết marker, `git add`, rồi `git commit` (hoặc `git merge --continue`)." },
  { "front": "Lệnh hủy toàn bộ merge đang dở?", "back": "`git merge --abort`, đưa mọi thứ về trạng thái trước khi merge." },
  { "front": "Merge fast-forward là gì?", "back": "Khi nhánh đích không có commit mới, Git chỉ tiến con trỏ lên, không tạo merge commit." },
  { "front": "`git merge --no-ff` dùng để làm gì?", "back": "Ép tạo merge commit để lịch sử ghi lại rằng feature đã được gộp." },
  { "front": "`git merge --squash` làm gì?", "back": "Gộp toàn bộ thay đổi của nhánh vào staging thành một thay đổi, không tạo merge commit." },
  { "front": "Merge conflict có làm mất code không?", "back": "Không, Git giữ cả hai phiên bản trong file; chỉ mất khi bạn xóa nhầm block." },
  { "front": "`merge.conflictStyle diff3` thêm gì vào conflict?", "back": "Thêm phần base `|||||||` cho biết code gốc trước khi hai nhánh sửa." },
  { "front": "Lệnh xem file nào đang conflict?", "back": "`git status` ở mục Unmerged paths, hoặc `git diff --name-only --diff-filter=U`." },
  { "front": "Vì sao không nên dùng `git merge -X ours` để xử lý conflict?", "back": "Nó tự chọn phía bạn ở mọi conflict, code của đồng đội biến mất im lặng." }
]
```

Ôn nhanh bằng cloze:

Khi merge, phần nằm giữa `<<<<<<< HEAD` và `=======` là code của {{c1::nhánh hiện tại (HEAD)}}.

Để bỏ toàn bộ merge đang dở, chạy {{c2::git merge --abort}}.

Sau khi sửa xong conflict, phải `git add` rồi {{c3::git commit}} (hoặc `git merge --continue`) để hoàn tất merge.

Merge {{c4::fast-forward}} xảy ra khi nhánh đích không có commit mới nào kể từ lúc tách nhánh.
