Trong quá trình làm việc với Git, các lập trình viên thường làm việc độc lập trên các nhánh (branch) khác nhau và để thống nhất code sau khi phát triển tính năng hoặc vá lỗi, cần tiến hành hợp nhất nhánh.
Hiện nay, khi cần tích hợp code từ nhánh này sang nhánh khác, Git hỗ trợ hai tập lệnh cho cùng một mục đích kết hợp công việc của nhiều nhà phát triển thành một mã, tuy nhiên với hai cách tiếp cận hoàn toàn khác nhau: Git Merge và Git Rebase.
Trong bài viết này, chúng ta sẽ tiến hành phân tích cơ chế hoạt động, so sánh ưu nhược điểm giữa Merge và Rebase, từ đó có được góc nhìn đúng đắn về lợi ích cũng như nhìn ra những rủi ro trong quá trình áp dụng thực tế vào những tình huống cụ thể khác nhau.
Git Merge là gì?
Bắt đầu với quy trình làm việc phổ biến nhất mà hầu hết chúng ta đã quá quen sử dụng để tích hợp các thay đổi trong quá trình hợp nhất nhánh: Git Merge.
Đặt bối cảnh muốn hợp nhất nhánh fea
feature
ture vào nhánh master
, chúng ta thực hiện hợp nhất nhánh bằng git merge thông qua các dòng lệnh đơn giản sau đâygit checkout feature git merge master
Kết quả sau khi thực thi là git merge tạo ra một commit mới ngay trên nhánh
master
, commit này gắn kết lịch sử của cả hai nhánh master
và feature
như một quan hệ của sự hợp nhất. Merge là một hình thức hợp nhất dễ dàng, an toàn và tương đối dễ chịu. Các nhánh hiện có hoàn toàn không được thay đổi trạng thái lịch sử các commit dưới bất kỳ hình thức nào. Điều này tránh được tất cả những cạm bẫy tiềm ẩn của việc tái xây dựng (rebase) lịch sử commit.
Mặt khác, điều này cũng có nghĩa là nhánh
feature
sẽ có một commit hợp nhất không liên quan mỗi khi bạn cần kết hợp các thay đổi ngược lại. Nếu nhánh main hoạt động rất tích cực, điều này có thể làm rối lịch sử commit của nhánh feature
khá nhiều. Mặc dù có thể giảm thiểu vấn đề này bằng các tùy chọn git log nâng cao nhưng nó có thể khiến các nhà phát triển khác khó hiểu được lịch sử của dự án.Hình bên dưới là kết quả cuối cùng từ hành động hợp nhất. Như bạn có thể thấy, lịch sử phát triển tính năng trên nhánh
feature
vẫn được giữ nguyên như cũ, chỉ thêm C7.Nói một cách dễ hiểu, git Merge sẽ hợp nhất nhiều lịch sử các cây commits tạo thành một commit mới đại diện cho sự hợp nhất và giữ nguyên các trạng thái lịch sử commit cùng tồn tại song song trên các nhánh khác nhau. Từ đó gây ra hình dạng “kim cương” trong cây Git và cực kỳ khó khăn khi cây Git trở nên thiếu “tuyến tính” trong việc quan sát theo dõi tiến độ dự án.
Git Rebase là gì?
Với cách tiếp cận thứ hai cho cùng một nhiệm vụ hợp nhất nhánh, tuy nhiên cũng khá xa lạ với đa số các lập trình viên cũng như tiềm ẩn nhiều rủi ro trong quá trình hợp nhất: Git Merge.
Đặt bối cảnh muốn hợp nhất nhánh
feature
vào nhánh master
, chúng ta thực hiện hợp nhất nhánh bằng git merge thông qua các dòng lệnh đơn giản sau đây.git checkout feature git rebase master
Kết quả sau khi thực thi là git rebase, sẽ đưa toàn bộ những commit mới tạo ở nhánh
feature
nối tiếp vào "ngọn" của nhánh master
, nhưng thay vì sử dụng một commit merge
, lịch sử commit của dự án sẽ được viết lại bằng cách tạo ra những commit mới ứng với mỗi commit ban đầu của nhánh feature
.Lợi ích chính của việc rebase là bạn sẽ nhận được một lịch sử commit sạch đẹp, rõ ràng và “tuyến tính” theo một đường thẳng từ đầu đến cuối dự án để dễ theo dõi hơn. Giúp loại bỏ những commit không cần thiết như khi sử dụng
git merge
. Khi đó chúng ta sẽ dễ dàng điều hướng, kiểm tra lịch sử project với những câu lệnh như git log
, git bisect
và gitk
.Tuy nhiên, có 2 điều cần phải thỏa hiệp đối với lịch sử commit kiểu này: độ an toàn và khả năng truy vết. Nếu chúng ta không tuân theo "nguyên tắc an toàn" khi rebase, việc viết lại lịch sử của dự án có thể là thảm họa khó lường đối với quy trình cộng tác làm việc nhóm. Một điều ít quan trọng hơn, rebase sẽ làm mất đi ngữ cảnh mà commit merge cung cấp, từ đó chúng ta sẽ không biết được khi nào những thay đổi ở nhánh tích hợp được đưa vào nhánh chủ đề.
Bên dưới là kết quả cuối cùng từ hành động rebase. Lưu ý cách các cam kết C5 và C6 đã được áp dụng lại thẳng vào C4, viết lại lịch sử phát triển và xóa hoàn toàn các cam kết cũ!
So sánh Git Rebase và Git Merge
Quan sát hình ảnh bên dưới cách trực quan, kết quả của cả hai quá trình Merge và Rebase đều giúp hợp nhất và thống nhất code giữa 2 nhánh trong quá trình làm việc.
Với Merge, tổng số lượng commit tăng 1 đơn vị và hình thành đồ thị Git dạng “kim cương”. Với Rebase, tổng số lượng commit không đổi và hình thành đồ thị Git dạng tuyến tính theo một đường thẳng.
Ưu điểm của Merge
- Không phá hủy: Hợp nhất là một hoạt động không phá hủy trong Git vì nó không thay đổi các nhánh hiện có. Nó chỉ thêm một cam kết bổ sung gọi là cam kết hợp nhất.
- Thay đổi tích hợp: Việc hợp nhất cho phép người dùng tích hợp các thay đổi từ nhánh này sang nhánh khác. Việc tích hợp này rất hữu ích nếu nhiều nhà phát triển đang làm việc trên các tính năng khác nhau cần được hợp nhất vào nhánh chính.
- Nhiều phiên bản cơ sở mã: Việc hợp nhất cho phép người dùng giữ nhiều phiên bản cơ sở mã. Điều này hữu ích nếu cần có các phiên bản mã cũ hơn hoặc nếu bạn cần một nhánh riêng để kiểm tra tính năng.
- Thay đổi theo dõi: Việc hợp nhất cho phép người dùng theo dõi những thay đổi đã được thực hiện đối với cơ sở mã. Việc theo dõi rất hữu ích cho việc gỡ lỗi hoặc kiểm tra.
- Giải quyết xung đột: Hợp nhất là một cơ chế giải quyết xung đột tuyệt vời cho phép người dùng hợp nhất các thay đổi mà nhiều nhà phát triển đã thực hiện trên cùng một tệp.
Nhược điểm của Merge
- Hợp nhất xung đột: Một trong những nhược điểm chính của git merge là khả năng xảy ra xung đột khi hợp nhất khi thực hiện nhiều thay đổi trên cùng một tệp. Đôi khi, việc giải quyết những xung đột như vậy có thể tốn thời gian và khó khăn.
- Mất bối cảnh: Khi những thay đổi từ hai nhánh được hợp nhất, một số ngữ cảnh của những thay đổi có thể bị mất. Do đó, lịch sử cơ sở mã và nguồn gốc của một số thay đổi có thể khó theo dõi hơn.
- Sự phức tạp: Độ phức tạp của cơ sở mã tăng theo số lượng nhánh và sự hợp nhất, điều này làm tăng độ khó bảo trì và làm phức tạp mối quan hệ giữa các nhánh.
- Sự phụ thuộc: Việc hợp nhất nhiều nhánh thành một có thể tạo ra sự phụ thuộc giữa các phần khác nhau của cơ sở mã. Điều này có thể cản trở việc thử nghiệm và triển khai thay đổi hơn nữa vì những thay đổi trong một phần của cơ sở mã có thể ảnh hưởng đến các phần khác.
Ưu điểm của Rebase
- Lịch sử dự án tuyến tính: Lợi ích chính của việc khởi động lại Git là lịch sử dự án sạch sẽ vì lệnh này loại bỏ các cam kết hợp nhất không cần thiết. Kết quả là một lịch sử dự án hoàn toàn tuyến tính, không có bất kỳ nhánh nào.
- Cơ sở mã đơn giản hóa: Lịch sử tuyến tính giúp bạn dễ dàng hiểu cơ sở mã và truy tìm nguồn gốc của những thay đổi cụ thể.
- Giải quyết xung đột hợp nhất: Lệnh git rebase áp dụng các thay đổi từ nhánh này lên nhánh khác. Điều này có nghĩa là xung đột hợp nhất được đơn giản hóa và các thay đổi được áp dụng theo cách có trật tự hơn so với hợp nhất git.
- Các nhánh tính năng riêng biệt: Việc rebase có thể được sử dụng để tách các nhánh tính năng trên nhánh chính. Việc tách chúng ra giúp quản lý nhiều nhánh dễ dàng hơn và cập nhật chúng với những thay đổi mới nhất trong nhánh chính.
- Uyển chuyển: git rebase linh hoạt hơn git merge trong việc quản lý các nhánh và thực hiện các thay đổi vì nó cho phép người dùng sắp xếp lại hoặc sửa đổi các cam kết, thay đổi thông báo cam kết, v.v.
Nhược điểm của Rebase
- Có thể có xung đột hợp nhất: Việc khởi động lại một quy trình công việc có thể gây ra xung đột hợp nhất thường xuyên hơn nếu có một nhánh tồn tại lâu dài đã đi xa khỏi nhánh chính. Nếu nhánh chứa nhiều cam kết mới, chúng có thể xung đột với nhánh chính. Để tránh những vấn đề như vậy, hãy thường xuyên khởi động lại các nhánh của bạn so với nhánh chính.
- Mất cam kết: Chạy git rebase ở chế độ tương tác với các lệnh phụ loại bỏ các cam kết khỏi nhánh có thể gây ra các cam kết bị mất trong nhật ký tức thời của nhánh. Tuy nhiên, các cam kết thường có thể được khôi phục bằng cách hoàn tác rebase bằng git reflog.
- Thiếu thông tin cam kết: Sau khi khởi động lại, bạn không thể biết khi nào các thay đổi ngược dòng được thực hiện và khi nào chúng được tích hợp vào nhánh tính năng.
Quy trình làm việc với Git Rebase
Chúng ta đã thấy cách rebase viết lại lịch sử trong khi việc hợp nhất vẫn bảo tồn nó. Nhưng điều này thực sự có ý nghĩa gì theo nghĩa rộng hơn. Và hoạt động này có những khả năng vô hạn và cũng tồn tại nhiều hạn chế tiềm tàng. Vì thế chúng ta cần phải cực kỳ lưu ý và nắm rõ quy trình làm việc cần tuân thủ khi làm việc với Git Rebase.
1. Local cleanup
Trong quá trình phát triển một tính năng trên branch riêng, các lập trình viên có thể có nhiều commit. Để cây Git được clean và gọn hơn, chúng ta cần tiến hành squash commit thông qua tính năng tự rebase trên chính nhánh feature.
Ví dụ bạn có 3 commits liên tục cần gộp lại 1 commit, thực hiện lệnh sau:
git switch feature git rebase -i HEAD~3
Màn hình hiển thị một tệp editor hiển thị lịch sử commits, chúng ta tiến hành cỉnh sửa file theo syntax. Các options bao gồm:
- p: pick - giữ lại commit
- r: reword - giữ lại commit và sửa message
- s: squash - bỏ qua commit nhưng tích hợp log vào commit liền trước
- f: fixup - bỏ qua commit và xoá hoàn toàn log commit
pick 1fc6c95 Patch A pick 6b2481b Patch B pick dd1475d something I want to split pick c619268 A fix for Patch B pick fa39187 something to add to patch A pick 4ca2acc i cant' typ goods pick 7b36971 something to move before patch B # Rebase 41a72e6..7b36971 onto 41a72e6 # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. #
Tiến hành giữ lại commit đầu tiền và squash toàn bộ các commits liền sau bằng cách thay thế pick thành squash. Lưu file :wq và thoát.
2. Rebasing from main
Tiếp theo, sau khi đã gom tất cả các commit của mình làm một chúng ta bắt đầu tiến hành rebase so với branch main. Lưu ý, trước đó ta cần nhảy sang nhánh main và tiến hành pull code từ remote để cập nhật các thay đổi mới nhất trên main.
git switch main git pull origin main git switch feature git rebase -i main feature
3. Push force to feature
Sau khi đã xử lý các conflicts liên quan và squash các commit mong muốn, lúc này các commits trên main đã được cắt nối tuyến tính vào ngay đầu commit trên feature. Khi đó, chúng ta sẵn sàng push lên remote và sẵn sàng tạo Merge/Pull request
git push origin feature --force
Lưu ý:
- Sau khi rebase, lịch sử commit local trên nhánh feature đã thay đổi và conflict so với nhánh feature trên remote, vì thế ta cần push force để ghi đè toàn bộ cây Git trên branch featue.
4. Create merge/pull request
Lưu ý:
- Trong quá trình tạo PR/MR, quá trình approve cần được diễn ra lần lượt có thứ tự và các PR/MR còn lại cần lập tức rebase ngay khi 1 PR/MR đã được merge vào main.
Tổng kết
Cả hai phương pháp đều giúp đạt được mục tiêu hợp nhất các thay đổi từ một nhánh vào nhánh chính trong quy trình làm việc cần thống nhất code giữa các lập trình viên. Thông qua bài viết hi vọng là chúng ta đã có thể hiểu rõ nguyên lý hoạt động khác nhau và cân nhắc sự lựa chọn phù hợp tuỳ theo nhu cầu dự án.
Nếu chúng ta cần ưu tiên một cây Git sạch sẽ, gọn gàng và “tuyến tính” dễ dàng theo dõi theo tiến độ dự án và không có sự dư thừa những commit merge thì Git rebase là một lựa chọn tối ưu và thông minh.
Ngược lại, nếu chúng ta cần ưu tiên bảo toàn lịch sử đầy đủ của dự án và tránh những nguy cơ mất mát dữ liệu và không ngại các hình dạng “kim cương” rối mắt trên cây Git khi merge chéo qua lại giữa các branch thì Git merge là một lựa chọn đơn giản và hiệu quả.
Bài viết được đồng đăng tải bởi chính tác giả tại nền tảng 200lab.io. Xem thêm tại https://200lab.io/blog/git-rebase-vs-git-merge/.