Git Rebase hay Merge? Chọn đúng chiến lược cho team của bạn
1. Tổng quan
git merge và git rebase đều dùng để tích hợp thay đổi giữa hai branch, nhưng chúng tạo ra lịch sử commit rất khác nhau. Chọn sai chiến lược khiến log rối, khó review, hoặc tệ hơn — làm hỏng lịch sử của đồng nghiệp. Bài này giúp bạn hiểu cơ chế của từng lệnh qua ví dụ cụ thể, biết khi nào nên merge, khi nào nên rebase, và tránh được những lỗi phổ biến nhất khi dùng rebase trong team.
2. Bối cảnh ví dụ
Giả sử bạn đang làm branch feature/payment từ main. Trong lúc bạn code, đồng nghiệp đã merge thêm vài commit vào main.
git log --oneline --graph --all
* c3f9a1d (main) Thêm PaymentValidator
* 7b2e4aa (main) Sửa bug OrderService
| * 9d1a0ef (feature/payment) Thêm PaymentService.processPayment()
| * 4f3c2bb (feature/payment) Khởi tạo PaymentService
|/
* a1b2c3d Base commit chung
Hai branch đã tách nhau từ a1b2c3d. Giờ bạn muốn đưa feature/payment lên ngang với main trước khi tạo pull request. Có hai cách: merge hoặc rebase.
3. Git Merge: giữ nguyên lịch sử, tạo merge commit
3.1. Cách dùng
git checkout feature/payment
git merge main
Lệnh này tạo một merge commit mới, có hai parent: commit cuối của feature/payment và commit cuối của main.
* e5f6a7b (HEAD -> feature/payment) Merge branch 'main' into feature/payment
|\
| * c3f9a1d (main) Thêm PaymentValidator
| * 7b2e4aa (main) Sửa bug OrderService
* | 9d1a0ef Thêm PaymentService.processPayment()
* | 4f3c2bb Khởi tạo PaymentService
|/
* a1b2c3d Base commit chung
3.2. Đặc điểm
- Lịch sử gốc của cả hai branch được giữ nguyên 100%, không commit nào bị sửa đổi.
- Có thêm một merge commit “rác” ghi lại thời điểm hai nhánh gặp nhau.
- An toàn tuyệt đối với branch đã push — không ai bị ảnh hưởng ngược.
Lưu ý:
git mergekhông viết lại SHA của bất kỳ commit nào đã tồn tại. Đây là lý do nó luôn an toàn với shared branch.
4. Git Rebase: viết lại lịch sử, tạo nhánh thẳng
4.1. Cách dùng
git checkout feature/payment
git rebase main
Rebase lấy từng commit trên feature/payment kể từ điểm tách nhánh (a1b2c3d), rồi “chơi lại” (replay) chúng lên đầu main. Kết quả là commit mới hoàn toàn với SHA khác.
* 2a9e1cc (HEAD -> feature/payment) Thêm PaymentService.processPayment()
* d8b4f0a Khởi tạo PaymentService
* c3f9a1d (main) Thêm PaymentValidator
* 7b2e4aa (main) Sửa bug OrderService
* a1b2c3d Base commit chung
Lịch sử giờ là một đường thẳng (linear history), không có merge commit nào.
4.2. Đặc điểm
9d1a0efvà4f3c2bbkhông còn tồn tại — thay bằng2a9e1ccvàd8b4f0avới SHA mới.- Log sạch, dễ đọc, dễ
git bisect. - Nguy hiểm nếu branch đã được người khác pull, vì họ đang giữ commit cũ đã “biến mất”.
5. Xử lý conflict: khác biệt lớn nhất khi thao tác
Với merge, conflict (nếu có) chỉ cần giải quyết một lần tại merge commit:
git merge main
# CONFLICT (content): Merge conflict in PaymentService.java
# ... sửa file, resolve ...
git add PaymentService.java
git commit
Với rebase, nếu có nhiều commit, conflict có thể xảy ra ở từng commit trong quá trình replay:
git rebase main
# CONFLICT trong commit 4f3c2bb
# sửa file, resolve xong:
git add PaymentService.java
git rebase --continue
# nếu commit tiếp theo cũng conflict thì lặp lại
Nếu muốn dừng và quay lại trạng thái trước khi rebase:
git rebase --abort
Lưu ý: Rebase một branch có 10 commit đôi lúc nghĩa là bạn phải resolve conflict 10 lần — một lý do khiến
squashtrước khi rebase (xem mục 7) rất hữu ích.
6. Golden Rule: không rebase branch đã chia sẻ (shared)
Đây là nguyên tắc quan trọng nhất khi dùng rebase.
# Branch main đã được 5 người khác pull về
git checkout main
git rebase feature/old-stuff # ❌ CỰC KỲ NGUY HIỂM
git push --force origin main
Vì rebase tạo SHA mới cho mọi commit, sau khi push --force, ai đã pull main cũ sẽ có lịch sử phân kỳ (diverged history) với remote. Lần pull tiếp theo của họ sẽ bị lỗi hoặc tạo ra hàng loạt duplicate commit.
error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behind
Nếu buộc phải force-push sau rebase, dùng --force-with-lease thay vì --force để tránh đè mất commit của người khác:
git push --force-with-lease origin feature/payment
--force-with-lease sẽ kiểm tra remote chưa bị ai khác đẩy thêm commit kể từ lần bạn fetch gần nhất, nếu có thì lệnh thất bại thay vì đè bừa.
Quy tắc vàng: chỉ rebase branch cá nhân, chưa chia sẻ hoặc chỉ mình bạn đang làm. Không bao giờ rebase main, develop, hay bất kỳ shared branch nào.
7. Interactive rebase: dọn lịch sử trước khi merge
Một ứng dụng rất thực tế của rebase là dọn commit “rác” trên branch cá nhân trước khi tạo pull request.
git checkout feature/payment
git rebase -i HEAD~3
Editor mở ra:
pick 4f3c2bb Khởi tạo PaymentService
pick 9d1a0ef Thêm PaymentService.processPayment()
pick 1c8f3aa fix typo
Đổi hai dòng cuối thành squash để gộp vào commit đầu:
pick 4f3c2bb Khởi tạo PaymentService
squash 9d1a0ef Thêm PaymentService.processPayment()
squash 1c8f3aa fix typo
Lưu lại, Git sẽ hỏi bạn viết message commit mới. Kết quả: 3 commit vụn thành 1 commit sạch, ý nghĩa — rất phù hợp trước khi mở PR để reviewer không phải đọc commit “fix typo”, “wip”, “ơ quên import”.
8. Chiến lược thực dụng: kết hợp cả hai
Đa số team hiệu quả không chọn tuyệt đối một trong hai, mà kết hợp theo ngữ cảnh:
| Tình huống | Nên dùng |
|---|---|
Cập nhật branch cá nhân với main mới nhất |
rebase |
| Dọn commit vụn trước khi mở PR | rebase -i (squash) |
Merge feature branch đã review xong vào main |
merge (thường qua PR, có thể squash-merge) |
| Branch đã được nhiều người cùng làm/pull | merge, tuyệt đối không rebase |
| Cần giữ dấu vết thời điểm tích hợp để audit | merge |
Quy trình phổ biến trong thực tế:
# 1. Trước khi mở PR: rebase lên main để cập nhật, tránh conflict muộn
git checkout feature/payment
git rebase main
# 2. Dọn commit nếu cần
git rebase -i main
# 3. Push branch cá nhân (an toàn vì chỉ mình bạn dùng)
git push --force-with-lease origin feature/payment
# 4. Merge vào main qua PR (merge commit hoặc squash-merge, không rebase main)
Cách này cho log feature/payment sạch, thẳng, dễ review trong PR, còn main vẫn an toàn vì chỉ nhận merge commit (hoặc squash commit) từ PR đã approve.
9. Squash merge: phương án trung gian
Nhiều nền tảng (GitHub, GitLab) có nút “Squash and merge” — gộp toàn bộ commit của PR thành một commit duy nhất rồi merge vào main.
git checkout main
git merge --squash feature/payment
git commit -m "Thêm chức năng xử lý thanh toán (#42)"
Kết quả: main có đúng một commit mới, không có merge commit, lịch sử của feature/payment (trên remote) vẫn giữ nguyên nếu bạn chưa xóa branch. Đây là lựa chọn phổ biến nhất cho team dùng PR workflow, vì vừa sạch log trên main, vừa không đụng tới lịch sử gốc.
10. Lỗi thường gặp
- Rebase rồi push thường (không
--force): Git sẽ báo lỗi vì lịch sử đã phân kỳ. Đừng dùnggit pullthông thường để “sửa” — nó sẽ tạo merge commit không mong muốn, làm hỏng lịch sử thẳng vừa rebase. - Quên branch đang rebase là shared branch: luôn kiểm tra
git log origin/<branch>xem có ai khác đã dựa vào các commit cũ chưa. - Rebase xong quên
--continuesau khi resolve conflict: trạng thái rebase vẫn “treo”,git statussẽ nhắc rõ bạn đang ở giữa rebase. - Dùng
mergeliên tục trên branch cá nhân khiến log đầy merge commit nhỏ lẻ, khó đọc — nên rebase để cập nhật thay vì merge ngượcmainvào branch của mình nhiều lần. - Squash quá tay: gộp nhiều thay đổi không liên quan vào một commit làm
git bisectvàgit reverttừng phần trở nên vô dụng.
11. Kết luận
merge an toàn, giữ lịch sử trung thực, phù hợp với shared branch. rebase tạo lịch sử sạch, thẳng, nhưng viết lại SHA nên chỉ dùng được trên branch cá nhân chưa chia sẻ. Trong thực tế, bạn không cần chọn một phe tuyệt đối: rebase để cập nhật và dọn dẹp branch cá nhân, merge (hoặc squash-merge) để tích hợp vào main qua PR. Nhớ Golden Rule — không bao giờ rebase branch mà người khác đang dựa vào — và bạn sẽ tránh được phần lớn rắc rối về Git trong team.