Git merge và rebase: nên chọn cái nào cho nhóm
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 merge | git 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ộp | Tuyến tính |
| An toàn với nhánh chung | Luôn an toàn | Nguy hiểm với commit đã công bố |
| Có cần force push không | Không | Có, nếu nhánh đã có trên máy chủ |
| Xung đột | Giải quyết một lần, trong commit gộp | Có thể xuất hiện ở từng commit được chuyển |
| Ngữ cảnh «đến từ đâu» | Được giữ lại | Bị mất |
| Hoàn tác cả tính năng | Một lệnh git revert -m 1 trên commit gộp | Revert từng commit hoặc từng khoảng |
Thuận lợi cho git bisect và git log | Đồ thị khó đọc hơn | Dễ hơn: một đường thẳng |
| Ngưỡng làm quen | Thấp | Cao 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ánh | Linh 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 --graphtrở 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òngit bisecttì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ình | Cách hoạt động | Phù hợp với |
|---|---|---|
| Chỉ merge | Tính năng gộp qua --no-ff, đồng bộ bằng git merge main | Ngườ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 + merge | Trước khi gộp, nhánh được rebase lên main, sau đó commit gộp với --no-ff | Nhóm cần cả lịch sử sạch lẫn ranh giới tính năng |
| Rebase + fast-forward | Nhánh được rebase và gộp bằng cách chuyển tiếp, lịch sử hoàn toàn tuyến tính | Nhóm nhỏ có các commit nguyên tử, gọn gàng |
| Squash merge | Mỗi pull request thành một commit trong main | Nhó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 reflogvà 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.
Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Git с нуля до уверенного разработчика”.
- Git-процессы в реальных командах: какой workflow выбрать
- Продвинутые инструменты: rebase, stash, cherry-pick и теги