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 merge khô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

  • 9d1a0ef và 4f3c2bb không còn tồn tại — thay bằng 2a9e1cc và d8b4f0a vớ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 squash trướ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ùng git pull thô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 --continue sau khi resolve conflict: trạng thái rebase vẫn “treo”, git status sẽ nhắc rõ bạn đang ở giữa rebase.
  • Dùng merge liê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ược main và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 bisect và git revert từ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.

gitrebasemergeversion-controlworkflow