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
mainan 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ũ:
main: A --- B --- C
\
feature: D --- E
Chạy git rebase main khi đang ở feature, Git sẽ:
- Tìm commit chung gần nhất (
B). - Tạm bỏ
D,Esang một bên. - Đưa
featuretrỏ tới đầumain(C). - Áp lại
D,Elên trênC, tạo ra commit mớiD',E'.
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.
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:
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:
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.
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.
# 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:
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.
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.
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ì
addrồi--continue, sai thì--abort. - Release cần annotated tag:
git tag -a v1.0.0 -m "..."rồigit 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 đó.
[
{ "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 addrồ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.