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.

Ảnh chụp đoạn mã nền tối minh hoạ merge ba chiều 3-way merge Git hoà giải hai phía bằng tổ tiên chung merge base ours theirs tự gộp khi khác vùng xung đột khi cùng dòng, một vì sao ba chiều Git cần ba phiên bản để quyết base commit tổ tiên chung 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 git merge-base nhanhA nhanhB tìm tổ tiên chung so base ours và base theirs chỗ chỉ một phía đổi lấy phía đó tự động chỗ cả hai đổi khác nhau xung đột, hai khác vùng tự gộp không xung đột A đổi dòng 1 B đổi dòng 3 git merge nhanhA fast-forward git merge nhanhB Auto-merging f.txt gộp cả 2 thay đổi không hỏi, ba cùng dòng xung đột cộng dấu index giữ 3 stage git merge B CONFLICT content Merge conflict in t.txt nhỏ hơn HEAD phần ours tieu de BAN CUOI bằng tieu de HOAN THIEN lớn hơn B phần theirs git ls-files -u stage 1 base 2 ours 3 theirs index giữ cả 3

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.

Ảnh chụp bảng kết quả chạy thật git 2.39 output thật, tình huống 1 A đổi dòng 1 B đổi dòng 3 tự merge khác vùng 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 3-way merge gộp thay đổi 2 phía không xung đột vì khác dòng, tình huống 2 cả hai đổi cùng một dòng xung đột git merge B CONFLICT content Merge conflict in t.txt t.txt có dấu xung đột nhỏ hơn HEAD tieu de BAN CUOI ours HEAD bằng tieu de HOAN THIEN theirs B lớn hơn B, index giữ cả ba phiên bản khi xung đột 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 bạn sửa xong git add dồn về stage 0 git commit hoàn tất merge, vì sao 3-way thắng 2-way nguồn Pro Git biết ai đổi nhờ base Git biết bên nào đã đổi tự lấy đúng không hỏi bừa 2-way mù chỉ so ours theirs thì không biết dòng khác nhau là ai thêm ai xoá xung đột thật chỉ khi cả hai đổi cùng chỗ khác nhau để người quyết merge base git merge-base là hàm nền của merge rebase cherry-pick

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ề

  1. 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.
  2. 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 <<<<<<< ======= >>>>>>>.
  3. Index giữ ba stage khi xung đột: đo thật git ls-files -u cho stage 1=base, 2=ours, 3=theirs — bạn sửa xong git add (về stage 0) rồi git commit; và nhớ merge sạch không đảm bảo đúng logic, luôn test.

Nguồn

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.