So sánh

Git merge và rebase: nên chọn cái nào cho nhóm

Cập nhật: · Skillaria.Top

Cả merge và rebase đều đưa thay đổi của nhánh vào nhau, nhưng xử lý lịch sử khác nhau: merge giữ nguyên lịch sử và thêm commit gộp, rebase viết lại commit của bạn lên trên nền mới và tạo lịch sử thẳng. Quy tắc thực tế: rebase để chỉnh các commit chưa công bố của chính bạn, merge để đưa công việc đã hoàn thành vào nhánh chung.

Ngắn gọn: merge và rebase khác nhau ở đâu

Cuộc tranh luận «merge hay rebase» thực ra không phải về lệnh nào tốt hơn, mà về việc bạn muốn thấy lịch sử dự án như thế nào. Hai lệnh cuối cùng cho ra cùng một mã nguồn. Khác nhau là con đường được ghi lại trong nhật ký.

  • Merge nối hai dòng lịch sử và ghi nhận một cách trung thực rằng công việc diễn ra song song. Các commit hiện có không thay đổi, còn commit gộp với hai cha được thêm vào.
  • Rebase viết lại commit của bạn như thể bạn đã bắt đầu công việc từ phiên bản mới nhất của nhánh nền. Lịch sử trông như một đường thẳng, nhưng các commit nhận hash mới.

Cơ chế của từng lệnh — các cờ, cách giải quyết xung đột, chế độ tương tác — đã được trình bày trong những bài riêng. Ở đây chỉ bàn về việc chọn chiến lược.

Lịch sử trông ra sao trong cả hai trường hợp

Tình huống ban đầu: bạn tách feature, thực hiện hai commit, còn trong main xuất hiện hai commit của người khác.

          D---E        feature
         /
A---B---C---F---G      main

Sau git merge feature trong main:

          D---E
         /     \
A---B---C---F---G---M  main

Sau git rebase main trong feature và chuyển tiếp main:

A---B---C---F---G---D'---E'   main

Trong trường hợp đầu, lịch sử cho thấy tính năng được làm song song và khi nào được gộp. Trường hợp thứ hai đọc như một câu chuyện tuần tự, nhưng không còn dấu vết của công việc song song: D' và E' là các commit mới, được tạo tại thời điểm rebase.

Bảng so sánh merge và rebase

Tiêu chígit mergegit rebase
Commit hiện cóKhông thay đổiĐược tạo lại với hash mới
Hình dạng lịch sửĐồ thị phân nhánh với commit gộpTuyến tính
An toàn với nhánh chungLuôn an toànNguy hiểm với commit đã công bố
Có cần force push khôngKhôngCó, nếu nhánh đã có trên máy chủ
Xung độtGiải quyết một lần, trong commit gộpCó thể xuất hiện ở từng commit được chuyển
Ngữ cảnh «đến từ đâu»Được giữ lạiBị mất
Hoàn tác cả tính năngMột lệnh git revert -m 1 trên commit gộpRevert từng commit hoặc từng khoảng
Thuận lợi cho git bisect và git logĐồ thị khó đọc hơnDễ hơn: một đường thẳng
Ngưỡng làm quenThấpCao hơn, cần hiểu về việc viết lại lịch sử
Dọn dẹp commit (squash, reword)Chỉ --squash cho toàn bộ nhánhLinh hoạt qua rebase -i

Ưu và nhược điểm của merge

Ưu điểm

  • Không phá hủy lịch sử. Mọi thứ đã công bố vẫn ở nguyên vị trí. Đồng nghiệp không phải sửa gì sau thao tác của bạn.
  • Xung đột được giải quyết một lần. Dù nhánh có 30 commit, bạn chỉ xử lý trạng thái cuối cùng, không phải từng bước.
  • Giữ được ngữ cảnh. Qua commit gộp có thể thấy công việc nào vào nhánh chính khi nào, và dễ dàng hoàn tác toàn bộ.

Nhược điểm

  • Đồ thị nhiều nhiễu. Khi đồng bộ thường xuyên, lịch sử đầy commit «Merge branch 'main' into feature», và git log --graph trở thành sơ đồ đường sắt rối.
  • Commit nháp đi vào lịch sử. «wip», «fix», «thêm fix» sẽ nằm mãi trong nhánh chính nếu không được gom lại.

Ưu và nhược điểm của rebase

Ưu điểm

  • Lịch sử tuyến tính, dễ đọc. Mỗi commit nằm đúng vị trí, git log đọc như danh sách thay đổi, còn git bisect tìm commit lỗi dễ hơn.
  • Có thể dọn commit trước khi review: gộp, đổi tên, sắp xếp lại.
  • Không có commit gộp kỹ thuật từ việc đồng bộ với nhánh chính.

Nhược điểm

  • Viết lại lịch sử. Rebase một nhánh chung đã công bố sẽ làm hỏng công việc của mọi người đã tải nó về.
  • Cần force push với nhánh trên máy chủ, vì vậy cần kỷ luật và --force-with-lease.
  • Xung đột lặp lại. Cùng một đoạn có thể xung đột ở nhiều commit liên tiếp.
  • Commit trung gian có thể không build được. Sau khi chuyển sang nền mới, các commit cũ không ai kiểm thử trong môi trường mới.

Khi nào dùng cái nào

Rebase — cho công việc cục bộ của bạn

Bạn cập nhật nhánh trước khi gửi — git pull --rebase. Chuẩn bị nhánh cho review và muốn bỏ các commit nháp — git rebase -i. Nhánh tính năng cá nhân của bạn đã tụt lại sau main — git rebase origin/main. Trong mọi trường hợp này, các commit được viết lại là những commit chỉ bạn thấy (hoặc chỉ thuộc về bạn).

Merge — cho nhánh chung và tích hợp

Tính năng đã xong được gộp vào main hoặc develop. Nhiều người cùng làm trên một nhánh. Cần gộp nhánh phát hành trở lại nhánh chính. Ở đây điều quan trọng là không đụng tới commit đã công bố, và merge là lựa chọn đúng duy nhất.

Quy tắc chọn đơn giản

Nếu commit chỉ có ở bạn — có thể rebase. Nếu ai đó đã tải chúng về — chỉ merge.

Các mô hình thường dùng trong nhóm

Mô hìnhCách hoạt độngPhù hợp với
Chỉ mergeTính năng gộp qua --no-ff, đồng bộ bằng git merge mainNgười mới học Git, nhóm cần lịch sử đầy đủ và thao tác hoàn tác đơn giản
Rebase + mergeTrước khi gộp, nhánh được rebase lên main, sau đó commit gộp với --no-ffNhóm cần cả lịch sử sạch lẫn ranh giới tính năng
Rebase + fast-forwardNhánh được rebase và gộp bằng cách chuyển tiếp, lịch sử hoàn toàn tuyến tínhNhóm nhỏ có các commit nguyên tử, gọn gàng
Squash mergeMỗi pull request thành một commit trong mainNhóm có công việc ngắn hạn, không cần chi tiết từng bước của nhánh

Trên GitHub và GitLab, các mô hình này tương ứng với các nút «Create a merge commit», «Rebase and merge» và «Squash and merge». Nên bật trong cài đặt kho chỉ cách được chọn, để lịch sử không trở thành hỗn hợp nhiều phong cách.

Cấu hình để giữ đúng lựa chọn

# pull không tạo merge commit thừa
git config --global pull.rebase true

# hoặc: pull chỉ chuyển tiếp, nếu không được thì báo lỗi — bạn quyết định
git config --global pull.ff only

# luôn tạo commit gộp khi gộp tính năng
git config --global merge.ff false

Hãy cẩn thận với merge.ff false: nó cũng ảnh hưởng tới git pull ở chế độ merge. Nhiều người không đặt nó toàn cục, mà viết --no-ff rõ ràng khi gộp tính năng.

Ví dụ: một công việc theo hai cách

Hãy xem một tình huống thực tế. Bạn làm tính năng thanh toán trong nhánh feature/payment suốt ba ngày, còn trong main đã có năm công việc của người khác được gộp. Trước khi tạo pull request, bạn cần cập nhật main mới nhất và kiểm tra mọi thứ.

Cách dùng merge

git switch feature/payment
git fetch origin
git merge origin/main
# giải quyết xung đột một lần, git add, git commit
git push

Nhánh nhận được commit «Merge remote-tracking branch 'origin/main' into feature/payment». Push diễn ra bình thường, đồng nghiệp đang theo dõi nhánh không nhận thấy gì. Nếu đồng bộ kiểu này mỗi ngày, nhánh sẽ có ba commit như vậy, và khi gộp mà không squash, chúng sẽ đi vào main cùng với tính năng.

Cách dùng rebase

git switch feature/payment
git fetch origin
git rebase origin/main
# xung đột được xử lý dần trên từng commit: git add, git rebase --continue
git rebase -i origin/main      # tùy chọn: gộp các commit nháp
git push --force-with-lease

Nhánh trông như thể được bắt đầu sáng nay từ main mới, không có commit thừa, người review đọc thay đổi theo từng bước dễ hơn. Cái giá: phải force push và đảm bảo không ai khác đang commit vào nhánh này.

Main sẽ thấy gì cuối cùng

Khi tích hợp qua pull request, sự khác biệt vẫn được giữ. Kiểu merge cho đồ thị có nhánh rẽ và các commit đồng bộ, kiểu rebase cho một chuỗi commit tính năng gọn gàng, có thể gộp bằng chuyển tiếp hoặc bằng một commit merge. Mã nguồn cuối cùng giống nhau, miễn là xung đột được giải quyết giống nhau.

Làm thế nào để quyết định trong nhóm

Hãy trả lời vài câu hỏi. Có nhiều người cùng làm trên một nhánh không? Nếu có, rebase cho nhánh đó bị loại trừ. Nhóm có kinh nghiệm Git đến đâu? Nếu có nhiều người mới, hãy bắt đầu với merge và squash merge — chúng dung thứ sai sót hơn. Có cần lịch sử đầy đủ cho kiểm toán và phân tích sự cố không? Khi đó các commit merge của tính năng rất hữu ích. Có quan trọng là git log dễ đọc và git bisect chạy nhanh không? Khi đó nên ưu tiên rebase và lịch sử tuyến tính.

Ghi quyết định vào CONTRIBUTING hoặc README của kho, và bật bảo vệ nhánh chính: cấm force push, bắt buộc pull request, chỉ cho phép một kiểu gộp. Quy tắc được cài trong cấu hình hoạt động tốt hơn bất kỳ thỏa thuận miệng nào.

Những hiểu lầm thường gặp

  • «Rebase là nâng cao, merge dành cho người mới». Không đúng: đây là hai công cụ cho hai nhiệm vụ khác nhau. Lập trình viên có kinh nghiệm dùng cả hai.
  • «Commit gộp là rác». Commit đồng bộ thì đúng là nhiễu. Nhưng commit gộp khi đưa tính năng vào là một điểm mốc hữu ích: nhờ nó thấy và hoàn tác được cả một công việc.
  • «Sau rebase sẽ ít xung đột hơn». Xung đột vẫn vậy, vì các dòng bị thay đổi vẫn như cũ. Rebase chỉ rải chúng qua các commit, còn merge giải quyết tất cả một lần.
  • «Rebase làm mất commit». Commit cũ vẫn có trong git reflog và có thể khôi phục, cho đến khi bộ thu gom rác xóa chúng.

Tóm lại

Chọn giữa merge và rebase là chọn giữa lịch sử trung thực theo thời gian và lịch sử tuyến tính dễ đọc. Merge an toàn và giữ ngữ cảnh, rebase cho sự gọn gàng nhưng viết lại commit. Thực hành đáng tin cậy nhất là kết hợp: rebase và dọn dẹp nhánh của mình khi nó còn là của bạn, còn đưa công việc đã hoàn thành vào nhánh chung qua merge hoặc squash theo quy tắc của nhóm. Điều quan trọng là thống nhất mô hình và ghi nó vào cài đặt kho. Cả hai chiến lược cùng làm việc với nhánh trong nhóm được luyện tập thực hành trong khóa học Git.

Câu hỏi thường gặp

Nên dùng merge hay rebase khi git pull?

Để cập nhật nhánh cục bộ, rebase tiện hơn: commit chưa công bố của bạn sẽ nằm trên các commit mới từ máy chủ mà không tạo commit merge thừa. Bật bằng git config --global pull.rebase true.

Có thể kết hợp cả merge và rebase trong một dự án không?

Có, và đây là cách phổ biến nhất: rebase cho nhánh của bạn, merge cho nhánh chung. Chỉ cần tất cả thành viên đều biết quy tắc.

Người mới nên chọn cái nào?

Hãy bắt đầu với merge: nó không viết lại lịch sử và khó làm hỏng dữ liệu hơn. Rebase nên học trên nhánh cục bộ của mình, sau khi đã hiểu reflog và force push.

Chiến lược có ảnh hưởng đến mã nguồn cuối cùng không?

Không, nếu xung đột được giải quyết giống nhau thì trạng thái cuối của tệp như nhau. Chỉ khác là lịch sử commit được ghi lại thế nào.

Squash merge khác rebase ở điểm nào?

Squash merge nén toàn bộ nhánh thành một commit mới trong nhánh chính, còn rebase chuyển từng commit riêng lẻ. Cả hai đều cho lịch sử tuyến tính, nhưng squash mất chi tiết từng bước.

Học chủ đề này qua video

Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Git с нуля до уверенного разработчика”.

Đến khóa học