Merge là nỗi sợ của nhiều người dùng Git — phần lớn vì không biết Git thực sự làm gì khi hoà hai nhánh. Hiểu cơ chế merge ba chiều (3-way merge) biến merge từ phép màu đáng sợ thành thứ dự đoán được. Bài này (phần 5 loạt "Git nội bộ") mổ xẻ merge bằng lệnh thật: khi nào Git tự gộp, khi nào báo xung đột, và index giữ gì trong lúc xung đột.
Vì sao "ba chiều": Git cần ba phiên bản để quyết
Để hoà hai nhánh, Git không chỉ so hai phiên bản với nhau (2-way). Nó dùng ba:
- base: commit tổ tiên chung của hai nhánh (merge base).
- ours: phiên bản nhánh hiện tại (HEAD).
- theirs: phiên bản nhánh đang merge vào.
Có base, Git biết bên nào đã thay đổi gì so với điểm xuất phát chung: chỗ chỉ một phía đổi → lấy phía đó (tự động); chỗ cả hai đổi khác nhau → xung đột. Merge base tìm bằng:
$ git merge-base nhanhA nhanhB # -> commit tổ tiên chung
Khác vùng → Git tự gộp, không hỏi
Nếu hai nhánh sửa các vùng khác nhau của cùng một file, 3-way merge gộp cả hai tự động. Ta cho nhánh A đổi dòng 1, nhánh B đổi dòng 3:
merge-base(nhanhA, nhanhB) = c644bb7 (tổ tiên chung)
git merge nhanhB -> Auto-merging f.txt
f.txt sau khi merge CẢ A và B:
DONG 1 sua boi A # từ nhánh A
dong 2
DONG 3 sua boi B # từ nhánh B
Không xung đột, không hỏi — vì mỗi dòng chỉ có một phía đổi so với base, Git biết chắc lấy phiên bản nào. Đây là lý do phần lớn merge trong thực tế "chạy êm": mọi người thường đụng các phần khác nhau.

Hình 1: Merge ba chiều cần base + ours + theirs; git merge-base tìm tổ tiên chung; thay đổi khác vùng tự gộp; thay đổi cùng dòng gây xung đột, và index giữ ba stage.
Cùng dòng → xung đột, và index giữ ba phiên bản
Khi cả hai nhánh sửa cùng một dòng thành nội dung khác nhau, Git không thể tự quyết — nó báo xung đột và chèn dấu:
git merge B -> CONFLICT (content): Merge conflict in t.txt
t.txt:
<<<<<<< HEAD
tieu de: BAN CUOI # ours (HEAD)
=======
tieu de: HOAN THIEN # theirs (B)
>>>>>>> B
Trong lúc xung đột, index không rỗng — nó giữ cả ba phiên bản ở ba "stage":
$ git ls-files -u
stage 1 77ee0be t.txt # base (tổ tiên chung)
stage 2 af34ab9 t.txt # ours (HEAD)
stage 3 1b947ab t.txt # theirs (nhánh merge vào)
Đây là lý do khi xung đột bạn có thể git checkout --ours/--theirs (lấy nguyên một phía) hay dùng git diff ba chiều — vì Git còn giữ đủ ba stage. Sau khi bạn sửa xong và git add (dồn về stage 0), rồi git commit để hoàn tất merge.

Hình 2: Chạy thật — khác vùng thì auto-merge (merge-base c644bb7); cùng dòng thì CONFLICT với dấu <<<<<<< / ======= / >>>>>>>; index giữ 3 stage (base 77ee0be / ours af34ab9 / theirs 1b947ab).
Vì sao 3-way thắng cách so 2-way ngây thơ
- Biết ai đổi: nhờ base, Git phân biệt "dòng này A thêm" với "dòng này B xoá" — nên tự lấy đúng mà không hỏi bừa. Cách so 2-way (chỉ ours vs theirs) mù về điều này: thấy hai bên khác nhau nhưng không biết ai mới là người đổi.
- Xung đột chỉ khi thật sự cần người quyết: cả hai đổi cùng chỗ thành nội dung khác → không có căn cứ máy móc để chọn → giao cho người. Đây là xung đột đúng nghĩa, không phải Git "làm khó".
- merge-base là hàm nền: cùng thuật toán tìm tổ tiên chung này là nền của
git merge,git rebase,git cherry-pick— hiểu nó giúp suy luận cả ba.
Đánh đổi cần cân nhắc
Auto-merge theo dòng, không theo ngữ nghĩa. Git gộp thành công khi hai bên đổi dòng khác nhau — kể cả khi kết quả sai về logic (ví dụ A đổi tên hàm, B thêm lời gọi hàm tên cũ ở dòng khác → merge sạch nhưng code hỏng). Merge sạch không đảm bảo đúng; luôn build/test sau merge.
Xung đột nhiều thường là dấu hiệu quy trình, không phải lỗi Git. Nếu team liên tục xung đột nặng, thường do nhánh sống quá lâu hoặc nhiều người sửa cùng vùng. Merge/rebase thường xuyên (nhánh ngắn) giảm xung đột hơn là đổ lỗi cho công cụ.
--ours/--theirs tiện nhưng dễ mất việc. Khi xung đột, git checkout --theirs <file> lấy nguyên phía kia, vứt thay đổi của bạn ở file đó. Nhanh nhưng nguy hiểm nếu dùng vội — hiểu rằng bạn đang bỏ hẳn một phía, không phải "gộp".
Ba ý mang về
- Merge của Git là ba chiều: dùng base (tổ tiên chung, tìm bằng
git merge-base) + ours + theirs để biết bên nào đã đổi gì — nhờ đó tự lấy đúng thay vì hỏi bừa. - Khác vùng tự gộp, cùng dòng mới xung đột: đo thật A đổi dòng 1 + B đổi dòng 3 → auto-merge gộp cả hai; cả hai đổi cùng dòng → CONFLICT với dấu
<<<<<<< ======= >>>>>>>. - Index giữ ba stage khi xung đột: đo thật
git ls-files -ucho stage 1=base, 2=ours, 3=theirs — bạn sửa xonggit add(về stage 0) rồigit commit; và nhớ merge sạch không đảm bảo đúng logic, luôn test.
Nguồn
- Pro Git — Git Branching: Basic Branching and Merging: https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging
- Git Documentation — git-merge-base / git-merge: https://git-scm.com/docs/git-merge-base
Phần sau ta so merge với rebase: vì sao rebase viết lại lịch sử (tạo commit mới với SHA khác) thay vì tạo commit merge, và khi nào nên dùng cái nào.