Skip to content

4. Merge vs Rebase

IMPORTANT

Quy tắc team-safe của PENALGO:

  • Rebase private history: dọn lịch sử trên branch cá nhân hoặc local branch chưa ai khác dùng.
  • Merge shared history: tích hợp code vào branch dùng chung như main, develop, release/*.
  • Không casual rebase main. Branch chính phải ổn định, reviewable, reproducible và không được đổi SHA lung tung.

4.1 Merge và Rebase đang giải quyết vấn đề gì?

Khi làm việc theo branch, lịch sử commit sẽ tách ra:

  • main tiếp tục nhận commit mới từ team.
  • feature/login có commit riêng của bạn.

Đến lúc cần tích hợp, Git phải trả lời câu hỏi:

“Làm sao ghép hai dòng lịch sử khác nhau lại với nhau mà không làm mất code?”

git merge giải quyết bài toán gì?

merge giải quyết bài toán tích hợp hai dòng lịch sử mà vẫn giữ sự thật lịch sử:

  • branch tách ra lúc nào
  • branch được nhập lại lúc nào
  • đã có một điểm tích hợp chính thức

Nó phù hợp khi bạn muốn lịch sử phản ánh đúng quá trình cộng tác của team.

git rebase giải quyết bài toán gì?

rebase giải quyết bài toán dọn lịch sử để nhìn gọn và tuyến tính hơn:

  • đưa commit của branch bạn “đặt lại” lên trên commit mới nhất của branch đích
  • giúp branch cá nhân không đầy merge commit lặt vặt
  • làm PR dễ đọc hơn nếu dùng đúng chỗ

Nhưng đổi lại, rebase viết lại lịch sử: commit cũ bị thay bằng commit mới có SHA mới.


4.2 Khác nhau cốt lõi: giữ lịch sử vs viết lại lịch sử

Merge: giữ nguyên lịch sử thật

Ý nghĩa:

  • main có commit C
  • branch featureF1, F2
  • merge commit M ghi nhận thời điểm tích hợp

Lịch sử sẽ hơi “hình thoi”, nhưng rất trung thực.

Rebase: tạo lịch sử tuyến tính

Ý nghĩa:

  • F1, F2 cũ không còn là commit đang dùng
  • Git tạo bản mới F1', F2' đặt lên sau C
  • nhìn như thể bạn làm việc sau khi main đã có C

Lịch sử sạch hơn, nhưng đó là lịch sử đã được viết lại.

ASCII mental model

text
MERGE

A---B---C--------M   main
     \          /
      F1---F2---    feature

REBASE

A---B---C---F1'---F2'   feature (sau rebase)

4.3 Quy tắc an toàn: rebase private history, merge shared history

Đây không phải “sở thích cá nhân”. Đây là policy để tránh phá branch chung.

Rebase dùng khi:

  • bạn đang ở branch cá nhân: feature/..., bugfix/...
  • commit đó chưa ai khác pull về để làm việc cùng
  • bạn muốn:
    • cập nhật branch mình theo main
    • squash/reword/dọn lịch sử trước khi mở PR
    • bỏ bớt merge commit vô nghĩa trong branch cá nhân

Ví dụ:

bash
git switch feature/login
git fetch origin
git rebase origin/main

Merge dùng khi:

  • tích hợp branch đã review vào branch dùng chung
  • branch đích là main, develop, release/*
  • bạn muốn giữ mốc tích hợp rõ ràng, truy vết dễ, rollback an toàn

Ví dụ:

  • merge qua Pull Request
  • merge hotfix vào main
  • merge release branch sau QA

Câu thần chú cần nhớ

Private history thì rebase được. Shared history thì merge.


4.4 Vì sao main không được casual rebase?

Đây là phần phải cực kỳ nghiêm túc.

Không casual rebase main

main là branch tham chiếu của cả team, CI/CD, release tags, rollback, production debugging, và audit trail.
Nếu bạn rebase main, bạn đang đổi SHA của lịch sử mà người khác, pipeline, và tài liệu triển khai có thể đang dựa vào.

Rebase main gây ra những vấn đề gì?

1. Làm đổi commit SHA của lịch sử chung

Sau rebase, commit nhìn có vẻ “giống”, nhưng thực ra là commit mới.
Đồng nghiệp đã pull main cũ sẽ rơi vào trạng thái:

  • branch local khác branch remote
  • pull/merge/rebase trở nên khó hiểu
  • xuất hiện conflict “vô lý”

2. Làm pipeline và release khó truy vết

Nếu một bản deploy ghi nhận commit abc123, mà bạn rebase rồi force-push main, commit đó có thể không còn nằm trên history chính nữa. Khi incident xảy ra:

  • khó xác định production đang chạy từ commit nào
  • rollback chậm hơn
  • audit history thiếu tin cậy

3. Làm review history mất ý nghĩa

main thường là nhánh đã qua review và đã merge. Rewrite nó sau đó làm mốc review bị mờ:

  • comment code review tham chiếu SHA cũ
  • người mới vào team không còn thấy luồng tích hợp thật
  • blame/debug dễ lệch ngữ cảnh

4. Tăng rủi ro overwrite công việc của người khác

Muốn đẩy một main đã rebase, bạn gần như phải force-push.
Và force-push lên branch shared luôn là thao tác rủi ro cao.

Kết luận policy

  • Không rebase main một cách casual
  • Thực tế team-safe là: đừng rebase branch shared
  • Nếu có trường hợp cực đặc biệt ở mức repo-admin, đó là operation có phối hợp, downtime, backup và thông báo đầy đủ — không phải thao tác hằng ngày của developer

4.5 Khi nào không nên dùng Merge?

merge an toàn hơn trên branch shared, nhưng không phải lúc nào cũng là lựa chọn tốt nhất.

Không nên lạm dụng merge khi:

  • bạn chỉ đang cập nhật branch cá nhân theo main
  • bạn muốn branch cá nhân sạch để mở PR
  • bạn merge main vào feature branch liên tục mỗi ngày chỉ vì tiện tay

Hậu quả:

  • history branch cá nhân đầy merge commit rác
  • PR khó đọc
  • khó tách logic thật của thay đổi

Dấu hiệu dùng merge sai chỗ

text
merge main into feature
merge main into feature
merge main into feature
fix merge
fix merge again

Đây là dấu hiệu nên dùng rebase trên branch cá nhân thay vì cứ merge liên tục.


4.6 Khi nào không nên dùng Rebase?

Đây là phần quan trọng nhất của bài.

Không dùng rebase khi:

  • branch đã shared cho nhiều người cùng làm
  • branch là main, develop, release/*
  • commit đã được team khác dựa vào
  • bạn không hiểu rõ mình sắp phải force-push gì

Dấu hiệu nguy hiểm

  • “Em rebase cho lịch sử đẹp hơn trên main nhé?”
  • “Em force-push branch này chắc không sao đâu, mọi người đang dùng chung.”
  • “Em không chắc branch này có ai pull chưa.”

Nếu có những câu này, dừng lại và không rebase.

Một nguyên tắc thực dụng

Nếu branch có khả năng người khác đang dùng, hãy coi nó là shared historykhông rebase.


4.7 Workflow khuyến nghị trong team

Tình huống 1: cập nhật feature branch với main

Nên dùng:

bash
git switch feature/login
git fetch origin
git rebase origin/main

Mục đích: branch cá nhân gọn, commit của bạn nằm trên nền mới nhất.

Tình huống 2: gộp feature vào main

Nên dùng: Pull Request + merge theo policy của repo.

Mục đích:

  • giữ mốc tích hợp
  • review dễ truy vết
  • rollback an toàn hơn

Tình huống 3: branch cá nhân đã push rồi nhưng chỉ mình bạn dùng

Có thể rebase tiếp, nhưng nếu cần cập nhật remote thì:

bash
git push --force-with-lease origin feature/login

CAUTION

--force-with-lease chỉ nên dùng trên private branch của bạn.
Không dùng nó như giấy phép để rewrite branch shared.


4.8 Recovery: nếu merge bị dùng sai hoặc gặp conflict

Hủy merge đang dở

Nếu bạn đang merge và conflict quá rối, muốn quay lại trạng thái trước khi bắt đầu:

bash
git merge --abort

Dùng khi:

  • merge đang in-progress
  • chưa commit merge result
  • muốn bỏ toàn bộ trạng thái merge hiện tại

Sau khi abort, nên kiểm tra gì?

bash
git status
git log --oneline --graph -10

Nếu git merge --abort không chạy được vì working tree đã thay đổi quá nhiều, hãy kiểm tra trạng thái rồi dùng reflog để quay về mốc an toàn.


4.9 Recovery: nếu rebase bị dùng sai hoặc gặp conflict

Hủy rebase đang dở

bash
git rebase --abort

Dùng khi:

  • rebase đang conflict
  • bạn nhận ra mình rebase nhầm branch
  • bạn chưa muốn tiếp tục rewrite history

Lệnh này đưa branch quay lại trạng thái trước khi rebase bắt đầu.

Nếu đã resolve vài file rồi thì sao?

Không sao. git rebase --abort vẫn là đường lui an toàn đầu tiên nếu bạn quyết định bỏ toàn bộ rebase.

Khi nào không dùng git rebase --skip

Đừng dùng --skip chỉ để “cho qua conflict nhanh”.
Bạn có thể vô tình bỏ luôn một commit quan trọng.

Chỉ skip khi bạn hiểu rõ commit đó thực sự không còn cần nữa.


4.10 Recovery bằng reflog khi ai đó đã làm hỏng lịch sử

Khi merge/rebase/force-push sai, reflog là hộp đen cứu mạng.

Xem các mốc HEAD gần đây

bash
git reflog

Ví dụ:

text
f82c1ab HEAD@{0}: rebase (finish): returning to refs/heads/feature/login
91ab220 HEAD@{1}: rebase (start): checkout origin/main
7c4d991 HEAD@{2}: commit: feat(login): add remember me

Nếu biết HEAD@{2} là trạng thái an toàn trước rebase:

bash
git reset --hard HEAD@{2}

Recovery branch thay vì reset trực tiếp

Khi đang hoảng hoặc sợ reset nhầm, an toàn hơn là tạo branch cứu hộ:

bash
git branch rescue-before-rebase HEAD@{2}
git switch rescue-before-rebase

Cách này giữ lại trạng thái để bạn kiểm tra trước khi quyết định làm gì tiếp.

Nếu branch shared bị rewrite rồi thì sao?

Không được hoảng loạn force-push chồng thêm lần nữa.
Quy trình an toàn là:

  1. dừng mọi thao tác push tiếp theo
  2. tìm SHA/mốc đúng bằng git reflog
  3. tạo branch cứu hộ từ mốc an toàn
  4. thống nhất với team/repo admin cách khôi phục
  5. ưu tiên khôi phục có kiểm soát, không “đua force-push”

4.11 --force-with-lease: dùng đúng, không lạm dụng

--force-with-lease an toàn hơn --force, nhưng không biến thao tác nguy hiểm thành vô hại.

Nó làm gì?

  • chỉ force-push nếu remote vẫn đang ở trạng thái bạn kỳ vọng
  • nếu remote đã có commit mới mà bạn chưa thấy, push sẽ bị từ chối

Ví dụ:

bash
git push --force-with-lease origin feature/login

Khi nào được dùng?

  • branch là private branch của bạn
  • bạn vừa rebase/squash/amend history của chính bạn
  • bạn chắc chắn không có ai khác cùng đẩy vào branch đó

Khi nào không được dùng?

  • main
  • develop
  • release/*
  • branch feature đang có nhiều người cùng làm

WARNING

--force-with-lease là dây an toàn cho private history, không phải giấy thông hành để rewrite shared history.


4.12 Ví dụ thực tế

Ví dụ A: đúng cách

Bạn làm trên feature/payment-retry, trong lúc đó main có thêm 5 commit mới.

Mục tiêu: cập nhật branch của bạn trước khi mở PR.

bash
git switch feature/payment-retry
git fetch origin
git rebase origin/main

Nếu xong và branch remote chỉ mình bạn dùng:

bash
git push --force-with-lease origin feature/payment-retry

Sau đó mở PR để merge vào main.

Ví dụ B: sai cách

Một người thấy main có history xấu nên chạy:

bash
git switch main
git rebase -i HEAD~20
git push --force

Kết quả:

  • đồng nghiệp pull bị lệch lịch sử
  • CI tham chiếu SHA cũ
  • release/debug trở nên hỗn loạn

Đây là kiểu sai lầm bài này muốn bạn tránh bằng mọi giá.


4.13 Checklist quyết định nhanh

Chọn Merge nếu:

  • bạn đang tích hợp vào branch shared
  • bạn cần mốc tích hợp rõ ràng
  • team cần lịch sử trung thực để review và audit

Chọn Rebase nếu:

  • bạn đang dọn branch cá nhân
  • bạn muốn cập nhật branch mình theo main
  • bạn hiểu rõ rằng commit SHA sẽ đổi

Dừng lại ngay nếu:

  • bạn sắp rebase main
  • bạn sắp force-push branch nhiều người đang dùng
  • bạn không chắc branch đó là private hay shared

4.14 Ghi nhớ ngắn gọn

Merge để tích hợp shared history. Rebase để dọn private history.

Nếu phải chọn giữa “history đẹp” và “team an toàn”, luôn chọn team an toàn.

🧠 Quiz

Câu 1: Merge giải quyết bài toán gì tốt nhất?

  • [x] A) Tích hợp hai dòng lịch sử và giữ lại mốc hợp nhất trung thực
  • [ ] B) Giảm số commit bằng cách tự động squash tất cả
  • [ ] C) Xóa commit cũ để lịch sử luôn thẳng
  • [ ] D) Thay thế hoàn toàn reflog

💡 Giải thích: merge giữ nguyên hai dòng lịch sử và tạo mốc tích hợp. Đây là lựa chọn an toàn cho branch shared.

Câu 2: Rebase phù hợp nhất trong tình huống nào?

  • [ ] A) Rewrite main sau khi đã release để lịch sử đẹp hơn
  • [x] B) Dọn branch cá nhân hoặc cập nhật feature branch theo main
  • [ ] C) Đồng bộ branch shared có nhiều người cùng commit
  • [ ] D) Thay thế hoàn toàn pull request merge

💡 Giải thích: rebase phù hợp cho private history. Nó không dành cho branch shared vì nó thay đổi SHA commit.

Câu 3: Nếu đang rebase và conflict quá rối, lệnh an toàn đầu tiên là gì?

  • [ ] A) git reset --hard origin/main
  • [x] B) git rebase --abort
  • [ ] C) git push --force
  • [ ] D) git rebase --skip cho nhanh

💡 Giải thích: git rebase --abort đưa branch quay lại trạng thái trước khi rebase. --skip có thể làm mất commit nếu dùng bừa.

Câu 4: Khi nào có thể cân nhắc git push --force-with-lease?

  • [ ] A) Khi muốn sửa lại main sau merge
  • [ ] B) Khi branch đang có nhiều người cùng làm
  • [x] C) Khi rewrite private branch của chính bạn sau rebase/squash
  • [ ] D) Trong mọi tình huống vì nó an toàn tuyệt đối

💡 Giải thích: --force-with-lease chỉ là lựa chọn cẩn thận hơn --force, nhưng vẫn chỉ nên dùng cho private branch.