# 🏷️ Rebase, tag và cắt release v1.0

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

Sau bài này, bạn sẽ:

- Giải thích được rebase làm gì với commit history và khi nào nên dùng thay vì merge.
- Rebase nhánh feature lên `main` an toàn, kể cả khi có conflict, và biết cách quay lại nếu sai.
- Gắn tag phiên bản (annotated tag) đúng chuẩn và push tag lên GitHub.
- Tạo release v1.0 cho project blog và phân biệt release với tag.

## Ôn nhanh

Bài trước bạn đã merge nhánh feature vào `main` và biết đọc dấu conflict `<<<<<<<`, `=======`, `>>>>>>>`. Câu chốt cần nhớ: merge tạo ra một merge commit, giữ nguyên toàn bộ lịch sử hai nhánh.

Rebase cũng đưa code của bạn vào `main`, nhưng bằng cách khác: nó viết lại lịch sử để history trông như bạn code liên tục trên đầu `main`. Đó là lý do rebase đẹp hơn khi review, và cũng là lý do nó nguy hiểm hơn.

## Lý thuyết

### Rebase thực chất làm gì

Giả sử `main` đã có thêm commit từ đồng đội, còn nhánh `feature/post-detail` của bạn đang tách ra từ chỗ cũ:

```text
main:     A --- B --- C
                   \
feature:            D --- E
```

Chạy `git rebase main` khi đang ở `feature`, Git sẽ:

1. Tìm commit chung gần nhất (`B`).
2. Tạm bỏ `D`, `E` sang một bên.
3. Đưa `feature` trỏ tới đầu `main` (`C`).
4. **Áp lại** `D`, `E` lên trên `C`, tạo ra commit mới `D'`, `E'`.

```text
main:     A --- B --- C
                       \
feature:                D' --- E'
```

Điểm cực kỳ quan trọng: `D'` và `E'` là commit **mới**, có hash mới. Rebase không di chuyển commit, nó viết lại chúng. Đây là nguồn gốc của mọi rắc rối về sau.

> [!WARNING]
> Không bao giờ rebase nhánh mà người khác đang làm việc chung hoặc đã push lên remote. Bạn viết lại hash, đồng đội pull về sẽ thấy history rối tung. Quy tắc: ==chỉ rebase nhánh riêng của bạn, chưa ai dựa vào==.

### Rebase vs merge: chọn cái nào

| Tiêu chí | `merge` | `rebase` |
| --- | --- | --- |
| History | Giữ nguyên, có merge commit | Thẳng hàng, không merge commit |
| An toàn | An toàn tuyệt đối | Viết lại hash |
| Dùng khi | Merge vào `main` qua PR | Cập nhật nhánh feature cá nhân lên `main` mới |
| Nhánh chia sẻ | Phù hợp | Không nên |

Cách team chuyên nghiệp hay dùng: rebase nhánh feature của mình lên `main` để history sạch, rồi merge PR vào `main` để lưu dấu vết.

### Xử lý conflict khi rebase

Rebase áp lại từng commit, nên conflict có thể xuất hiện ở từng commit một. Khác merge: merge giải quyết một lần, rebase có thể giải quyết nhiều lần.

```bash
git rebase main
# Git báo: CONFLICT in posts/detail.md
# Sửa file, bỏ dấu <<<<<<< ======= >>>>>>>
git add posts/detail.md
git rebase --continue
```

Ba lệnh cứu hộ cần thuộc:

```bash
git rebase --abort      # bỏ toàn bộ, về trạng thái trước rebase
git rebase --skip       # bỏ qua commit đang gây conflict
git rebase --continue   # tiếp tục sau khi đã add
```

Nếu bạn đã trót rebase sai, `git reflog` là lưới an toàn: nó ghi lại mọi vị trí HEAD, bạn tìm hash cũ và `git reset --hard <hash>` để quay lại.

### Squash commit bằng rebase tương tác

Trước khi mở PR, bạn thường có commit kiểu "sửa lỗi chính tả", "fix", "fix lần 2". Dọn chúng bằng:

```bash
git rebase -i main
```

Editor mở ra danh sách commit, bạn đổi `pick` thành `squash` (hoặc `s`) cho các commit muốn gộp vào commit trước đó. Kết quả: một commit sạch, message rõ ràng, reviewer đọc dễ hơn nhiều.

### Tag: dấu mốc phiên bản

Tag là một nhãn cố định trỏ vào một commit, dùng để đánh dấu phiên bản phát hành. Có hai loại:

- `lightweight tag`: chỉ là con trỏ, `git tag v1.0`.
- `annotated tag`: có người tạo, ngày, message, ký được. **Luôn dùng loại này cho release.**

```bash
git tag -a v1.0 -m "Release v1.0: blog có trang chi tiết bài viết"
git tag -l            # xem danh sách tag
git show v1.0         # xem thông tin tag
git push origin v1.0  # push một tag cụ thể
git push --tags       # push tất cả tag
```

Lưu ý dễ mất điểm: `git push` thường **không** đẩy tag lên. Phải push tag riêng.

### Release trên GitHub

Tag là khái niệm của Git, release là khái niệm của GitHub. Release dựa trên một tag, kèm release notes và file đính kèm.

Đường đi trên giao diện: tab **Releases** > **Draft a new release** > chọn tag `v1.0` > viết tiêu đề, mô tả thay đổi > **Publish release**. Hoặc dùng `gh release create v1.0 --notes "..."` nếu có GitHub CLI.

> [!TIP]
> Đặt tên tag theo chuẩn `vMAJOR.MINOR.PATCH`, ví dụ `v1.0.0`. Số nhỏ vẫn đánh đủ ba phần để sau này không phải đổi quy ước giữa chừng.

## Ví dụ minh hoạ

Bối cảnh: blog của bạn đã merge nhánh `feature/post-detail` vào `main` ở bài trước. Giờ bạn làm nhánh `feature/rss` và cần cập nhật nó trước khi mở PR.

**Tình huống thực tế:** đồng đội merge `feature/header` vào `main`, nhánh `feature/rss` của bạn bị lùi lại 3 commit.

```bash
# Bước 1: về nhánh feature, đảm bảo working tree sạch
git switch feature/rss
git status                 # phải sạch, không có file dở dang

# Bước 2: cập nhật main mới nhất từ remote
git switch main
git pull origin main

# Bước 3: rebase nhánh feature lên main
git switch feature/rss
git rebase main

# Bước 4: nếu có conflict
# Sửa file, sau đó:
git add <file-đã-sửa>
git rebase --continue

# Bước 5: kiểm tra lại history
git log --oneline --graph

# Bước 6: push (phải force vì hash đã đổi)
git push --force-with-lease origin feature/rss
```

`--force-with-lease` an toàn hơn `--force`: nó từ chối push nếu remote đã có commit mới mà bạn chưa thấy. Đây là thói quen của người làm việc nhóm kỹ.

**Cắt release v1.0:**

```bash
git switch main
git pull origin main
git tag -a v1.0.0 -m "Release v1.0.0: blog có trang chi tiết bài viết và RSS"
git push origin v1.0.0
```

Rồi vào GitHub, tab Releases, tạo release từ tag `v1.0.0`, mô tả ba thay đổi chính của blog.

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

Bài tập gắn với project blog của bạn.

**Bài 1.** Nhánh `feature/rss` của bạn có 4 commit với message lần lượt: `them rss`, `fix typo`, `fix`, `sua lai feed`. Hãy dọn thành 1 commit duy nhất trước khi mở PR.

```bash
git switch feature/rss
git rebase -i main
# Trong editor: giữ pick ở commit đầu, đổi 3 commit sau thành squash
# Lưu, đóng editor, viết lại message: "feat: them RSS feed cho blog"
```

Gợi ý: nếu lỡ tay, `git rebase --abort` trước khi rebase xong, hoặc `git reflog` + `reset --hard` sau khi đã xong.

**Bài 2.** Bạn đang rebase thì gặp conflict ở `posts/detail.md`, nhưng file đó đồng đội đã xoá trên `main`. Bạn nên làm gì?

Gợi ý: nếu thay đổi của bạn cho file đó không còn cần thiết, dùng `git rm posts/detail.md` rồi `git rebase --continue`. Nếu cần giữ, dùng `git checkout --theirs` / `--ours` cẩn thận, và nhớ rằng trong rebase ý nghĩa `ours`/`theirs` bị đảo so với merge.

**Bài 3.** Gắn tag `v1.0.0` cho commit đầu `main` sau khi merge, push tag lên, rồi kiểm tra trên GitHub rằng release đã hiện.

```bash
git switch main && git pull origin main
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
git ls-remote --tags origin    # xác nhận tag đã lên remote
```

## Góc chuyên gia

**Lỗi thường gặp nhất: rebase nhánh đã push và có người khác dùng.** Trước khi rebase, tự hỏi: "Nhánh này chỉ mình tôi đụng không?" Nếu không chắc, dùng merge.

**Đừng rebase `main`.** `main` là nhánh chia sẻ. Rebase `main` là cách nhanh nhất để làm hỏng repo của cả team.

**Luôn `git status` sạch trước khi rebase.** Rebase từ chối chạy khi có thay đổi chưa commit, nhưng `stash` rồi quên `pop` cũng gây hoảng. Commit hoặc stash dứt khoát trước.

**`--force-with-lease` thay vì `--force`.** Không có lý do chính đáng nào để dùng `--force` trần trong công việc nhóm.

**Tag annotated, không lightweight, cho release.** Lightweight tag không có metadata, `git describe` cho kết quả kém, và GitHub release vẫn chạy nhưng mất thông tin người tạo.

**Tag sai thì xoá được:** `git tag -d v1.0.0` (local) và `git push origin :refs/tags/v1.0.0` (remote). Nhưng nếu đã publish release, hãy tạo tag mới `v1.0.1` thay vì xoá, để người đã tải bản cũ không bị lệch.

**Một mẹo nhỏ:** trước khi rebase, tạo nhánh backup `git branch backup/rss` để có đường lui mà không cần đọc `reflog`.

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

Bốn câu chốt:

- Rebase viết lại commit của bạn lên đầu `main`, cho history thẳng và sạch, nhưng tạo hash mới.
- Chỉ rebase nhánh riêng, chưa ai dựa vào; conflict thì `add` rồi `--continue`, sai thì `--abort`.
- Release cần annotated tag: `git tag -a v1.0.0 -m "..."` rồi `git push origin v1.0.0`.
- GitHub release = tag + release notes, tạo ở tab Releases.

**Bài tiếp theo:** Checkpoint của module sẽ kiểm tra toàn bộ chuỗi tạo nhánh, merge, xử lý conflict, rebase và cắt release. Bạn sẽ làm một bài tổng hợp trên chính project blog, nên hãy chắc rằng tag `v1.0.0` của bạn đã lên GitHub trước khi sang bài đó.

```flashcard
[
  { "front": "Rebase làm gì với các commit của nhánh feature?", "back": "Áp lại chúng lên đầu nhánh đích, tạo ra commit mới với hash mới." },
  { "front": "Vì sao không nên rebase nhánh đã push mà người khác đang dùng?", "back": "Rebase viết lại hash, đồng đội pull về sẽ thấy history rối và dễ conflict hàng loạt." },
  { "front": "Ba lệnh cứu hộ khi rebase gặp sự cố là gì?", "back": "git rebase --abort, --skip, --continue" },
  { "front": "Sau khi sửa conflict trong lúc rebase, bạn làm gì tiếp?", "back": "git add file đã sửa, rồi git rebase --continue" },
  { "front": "Lệnh nào dọn nhiều commit lộn xộn thành một trước khi mở PR?", "back": "git rebase -i main, rồi đổi pick thành squash" },
  { "front": "Khác biệt chính giữa merge và rebase về history?", "back": "Merge tạo merge commit và giữ nguyên history; rebase tạo history thẳng, không merge commit." },
  { "front": "Lệnh tạo annotated tag cho release v1.0.0?", "back": "git tag -a v1.0.0 -m \"Release v1.0.0\"" },
  { "front": "git push có tự đẩy tag lên remote không?", "back": "Không. Phải chạy git push origin v1.0.0 hoặc git push --tags." },
  { "front": "Vì sao dùng annotated tag thay vì lightweight tag cho release?", "back": "Annotated tag có người tạo, ngày và message, phù hợp làm mốc phát hành." },
  { "front": "Release trên GitHub khác tag ở điểm nào?", "back": "Release dựa trên tag nhưng thêm release notes và file đính kèm; tag thuần là khái niệm Git." },
  { "front": "Vì sao dùng --force-with-lease thay vì --force sau khi rebase?", "back": "Nó từ chối push nếu remote có commit mới bạn chưa thấy, tránh ghi đè công của người khác." },
  { "front": "Lệnh xem lại mọi vị trí HEAD để tìm hash trước khi rebase sai?", "back": "git reflog" },
  { "front": "Trong lúc rebase, ý nghĩa ours và theirs có giống lúc merge không?", "back": "Không, chúng bị đảo: ours là nhánh đích (main), theirs là commit đang được áp lại." }
]
```

Điền vào chỗ trống để tự kiểm tra:

- Rebase áp lại commit của bạn lên đầu nhánh đích và tạo ra {{c1::commit mới với hash mới}}.
- Khi rebase gặp conflict, sau khi sửa file bạn chạy `git add` rồi {{c2::git rebase --continue}}.
- Lệnh tạo annotated tag cho bản phát hành là {{c3::git tag -a v1.0.0 -m "..."}}.
- Tag không tự lên remote, bạn phải chạy {{c4::git push origin v1.0.0}}.
- Trên GitHub, release được tạo từ một {{c5::tag}} và kèm release notes.
