Nội dung chính
Git là một trong những công cụ quản lý mã nguồn phổ biến. Điểm nổi bật của Git nằm ở khả năng phân tách và hợp nhất nhánh, giúp nâng cao hiệu quả trong quá trình cộng tác, làm việc nhóm. Git rebase chính là một trong những phương pháp hiệu quả để thực hiện hợp nhất thay đổi giữa các nhánh.
Đọc bài viết này để hiểu rõ hơn về:
- Git rebase là gì?
- Cơ chế hoạt động của git rebase
- Ưu và nhược điểm của git rebase
- Quy trình thực hiện git rebase
- Các tùy chọn nâng cao khi sử dụng git rebase
Nhánh trong Git (Git branch) là gì?
Nhánh trong git là gì?
Nhánh trong git thực chất là một con trỏ để trỏ đến “snapshot” các thay đổi trong kho lưu trữ, mà ở trong đó, bạn có thể thêm các commits mới nhưng không làm ảnh hưởng đến nhánh chính (mặc định nhánh chính sẽ là main).
Phân nhánh và hợp nhất nhánh trong git
Khi bạn muốn thêm một tính năng mới hoặc sửa lỗi hay bất kể những cập nhật nào trong mã nguồn, bạn sẽ tạo một nhánh mới để đóng gói các thay đổi của mình.
Việc này giúp tách biệt quá trình thay đổi và tránh ảnh hưởng đến nhánh chính. Điều này rất hữu ích khi trong một nhóm làm việc có nhiều người cùng phát triển mã nguồn.
Sau khi hoàn thành các thay đổi mới, bạn có thể hợp nhất những nội dung này vào nhánh chính một cách nhanh chóng và an toàn bằng các phương pháp như git merge, git rebase. Điều này giúp việc quản lý mã nguồn trở nên hiệu quả hơn trong các dự án lớn.
Đọc thêm: Git Merge: Hướng dẫn chi tiết cách sử dụng Git Merge
Git rebase là gì?
Git rebase là một lệnh mạnh mẽ trong git để hợp nhất các thay đổi từ nhánh này sang nhánh khác bằng việc tái sắp xếp lại lịch sử commits. Git rebase sẽ đưa những commit mới của nhánh hiện tại lên phía đầu trong lịch sử commit và tạo ra một lịch sử commit tuyến tính.
Cú pháp cơ bản của git rebase:
git rebase <tên nhánh>
Git rebase rất phù hợp trong những trường hợp như:
- Cập nhật thay đổi từ nhánh gốc nhưng không muốn tạo ra commit merge
- Muốn giữ cho lịch sử commit được gọn gàng và sạch sẽ: Bởi vì cơ chế tái tạo một lịch sử commit tuyến tính và không tạo ra commit merge nên git rebase rất phù hợp cho những dự án cần giữ lịch sử đơn giản và gọn gàng.
- Cần tái sử dụng một nhánh hoặc sắp xếp lại commit trên một commit base mới.
Cơ chế hoạt động của Git Rebase
Git rebase hoạt động dựa trên cơ chế sắp xếp lại lịch sử commit và không tạo ra commit merge. Điều này sẽ hình thành một lịch sử commit tuyến tính. Chi tiết về cách thức hoạt động của git rebase được mô tả thông qua ví dụ sau:
Ví dụ ta có hai nhánh A và nhánh B đều có chung commit C1. Trải qua quá trình làm việc, nhánh A được bổ sung thêm commits C2 và C3 từ nhánh khác, còn nhánh B tạo thêm hai commit C4, C5.
Bây giờ chúng ta cần thực hiện hợp nhất các thay đổi từ nhánh B và nhánh A bằng lệnh git rebase:
git checkout branch-A git rebase branch-B
Đầu tiên, Git sẽ tìm đến commit chung gần nhất (common ancestor commit) giữa hai nhánh, sau đó “loại bỏ” tất cả các commit mới trên nhánh-A đã xảy ra kể từ commit chung và lưu tạm thời ở trong bộ nhớ.
Tiếp theo, Git áp dụng các commit mới từ nhánh B vào nhánh A. Tại thời điểm này, tạm thời cả hai nhánh thực sự trông giống hệt nhau về lịch sử commit.
Cuối cùng, các commit mới từ nhánh A sẽ được đưa trở lại và được đặt ở đầu lịch sử commit. Vì chúng được đặt trên đầu các commit tích hợp từ nhánh B, nên được gọi là “Rebased”.
Như vậy, quá trình rebase để hợp nhất các thay đổi từ nhánh B vào nhánh A đã được hoàn tất. Nhánh A vẫn giữ được một lịch sử commit tuyến tính và không tạo ra thêm commit merge.
Ưu điểm và nhược điểm của git rebase
Git rebase là một công cụ mạnh mẽ trong việc hợp nhất nhánh, tuy nhiên git rebase cũng sẽ gây ra một số khó khăn nếu như không được sử dụng đúng cách.
Ưu điểm
- Giữ cho lịch sử commit được gọn gàng: git rebase giúp hợp nhất các commit, tạo ra một lịch sử tuyến tính dễ đọc, đặc biệt hữu ích khi làm việc trên các nhánh tính năng hoặc sửa lỗi nhỏ
- Loại bỏ các commit merge không cần thiết: Việc không tạo ra commit merge giúp cho lịch sử commit được đơn giản hóa, tránh những commit dư thừa
- Việc theo dõi các thay đổi được dễ dàng: Git rebase giữ cho lịch sử commit được tuyến tính (các commit được sắp xếp theo một chuỗi liên tục, không có các nhánh xen kẽ)
- Hợp nhất commit: Với tùy chọn -i, git rebase giúp hợp nhất nhiều commits thành một commit chính, giúp giảm số lượng commit không cần thiết
Nhược điểm
- Có thể mất một số thông tin cần thiết: Việc tái tạo lại lịch sử commit của git rebase dẫn đến việc các thông tin như nhánh phát triển của commit, thời điểm hợp nhất các commit sẽ rất khó để truy xuất.
- Dễ xảy ra xung đột: Bản chất của git rebase là di chuyển hoặc áp dụng lại các commit từ một nhánh lên trên các commit mới hơn ở nhánh khác. Trong quá trình này, nếu có sự khác biệt giữa nội dung của các commit được áp dụng lại và những commit đã tồn tại, dễ dẫn đến xung đột.
- Không phản ánh đúng lịch sử kho lưu trữ: Vì git rebase thay đổi thứ tự commit, lịch sử không còn phản ánh chính xác cách các thay đổi được tích hợp ban đầu
Quy trình thực hiện Git Rebase
Giả sử chúng ta đang có nhánh main và nhánh tính năng new-feature được tách ra từ nhánh main. Sau quá trình làm việc, nhánh new-feature đang có một số commits mới và cần được đẩy lên kho lưu trữ từ xa để thực hiện tạo pull request.
Trước hết cần thực hiện git rebase để cập nhật mã nguồn mới nhất từ nhánh main:
Checkout qua nhánh cần được rebase (ở đây sẽ là nhánh main)
git checkout main
Đảm bảo nhánh main đã ở trạng thái mới nhất
git pull origin main
Chuyển sang nhánh tính năng và thực hiện rebase các thay đổi mới từ nhánh main
git checkout new-feature git rebase main
Trường hợp có xung đột xảy ra, thực hiện xử lý xung đột:
Mở tập tin bị xung đột, git sẽ đánh dấu phần mã bị xung đột như sau:
<<<<<<<HEAD # Đây là thay đổi trên nhánh new-feature... ======= # Đây là thay đổi từ nhánh main ... >>>>>>>main
Giữ lại phần code phù hợp hoặc kết hợp các thay đổi của cả hai nhánh.
Thêm các nội dung đã giải quyết vào staging:
git add <file>
Bởi vì git rebase sẽ xử lý xung đột trên từng commit, nên chúng ta cần tiếp tục quá trình rebase cho đến khi kết thúc:
git rebase --continue
Trường hợp muốn hoàn tác hành động rebase, bạn có thể sử dụng:
git rebase --abort
Đẩy nội dung nhánh tính năng lên kho lưu trữ từ xa:
git push origin new-feature
Lưu ý, nếu nhánh đã được đẩy lên kho lưu trữ từ xa và có người khác đang làm việc, việc thay đổi lịch sử commit có thể gây ra xung đột và lỗi. Vì vậy không nên sử dụng git rebase trong trường hợp này.
So sánh Git rebase và Git merge
Git rebase và Git merge đều là những phương pháp được sử dụng để hợp nhất các thay đổi từ nhánh này vào nhánh khác trong Git, nhưng giữa chúng có những đặc điểm khác nhau.
Dưới đây là bảng so sánh chi tiết giữa Git rebase và Git merge, giúp bạn có thể lựa chọn phương pháp phù hợp:
Tiêu chí | Git rebase | Git merge |
Cú pháp | git rebase <tên nhánh> | git merge <tên nhánh> |
Tạo ra merge commit | Không | Có (trường hợp cả hai nhánh đều có thay đổi mới) |
Lịch sử commit | Tạo ra lịch sử tuyến tính, không có rẽ nhánh hay merge commit | Lịch sử không tuyến tính, có các nhánh rẽ và merge commit, phản ánh quá trình phát triển thực tế. |
Xử lý xung đột | Có thể cần xử lý xung đột nhiều lần đối với từng commit khi rebase | Xử lý xung đột một lần duy nhất trong quá trình merge |
Trường hợp sử dụng |
|
|
Các tùy chọn nâng cao của git rebase
Git rebase cung cấp nhiều tùy chọn nâng cao để tùy chỉnh cách thức hoạt động cũng như trường hợp xử lý. Dưới đây là một số tùy chọn phổ biến:
–interactive (tương tác commit)
Tùy chọn này cho phép người dùng tương tác với từng commit trong quá trình rebase. Cú pháp chung như sau:
git rebase -i HEAD~<n>
Trong đó, n là số commit cần chỉnh sửa từ đầu lịch sử commit.
Khi chạy lệnh này, các commit sẽ lần lượt được hiển thị trong chế độ tương tác với các hành động:
- Pick: giữ nguyên commit
- Reword: chỉnh sửa thông tin “commit message”
- Edit: Chỉnh sửa nội dung commit
- Squash: Hợp nhất commit hiện tại với commit trước đó
- Drop: xóa commit
–autosquash
Tùy chọn này được sử dụng kết hợp với –interactive, giúp tự động di chuyển các commit có thông điệp bắt đầu bằng “fixup!” hoặc “squash!” đến đúng vị trí tương ứng của chúng.
# tạo commit fixup và squash git commit --fixup=<commit-hash> git commit --squash=<commit-hash> # thực hiện autosquash git rebase -i --autosquash HEAD~<n>
Ví dụ, chúng ta có nhánh A với các commit sau:
hash_1 "Add feature login" hash_2 "Fix case login failed" hash_3 "Implement feature login"
Bây giờ bạn muốn gộp commit hash_2 và hash_3 vào commit hash_1, tuy nhiên muốn đổi tên commit gộp thành tên của commit hash_3. Bạn có thể thực hiện như sau:
git commit --fixup=hash_2 # Fix case login failed git commit --squash=hash_3 # Implement feature login
Lịch sử commit bây giờ:
hash_2 "fixup! Fix case login failed" hash_3 "squash! Implement feature login" hash_1 "Add feature login"
Chạy rebase với –autosquash:
git rebase -i --autosquash HEAD~3
Lúc này:
- Commit fixup tự động hợp nhất nội dung commit vào hash_1 và loại bỏ thông điệp.
- Squash tự động hợp nhất nội dung commit vào hash_1 và giữ lại thông điệp để bạn chỉnh sửa.
- Sau khi chỉnh sửa thông điệp (nếu cần), lưu lại và thoát.
Lịch sử sau khi rebase –autosquash:
hash_1 "Implement feature login (with fixes and improvements)"
–continue, –abort, và –skip
Những tùy chọn này giúp kiểm soát quá trình rebase khi gặp xung đột (ví dụ như trường hợp ở phần 5). Trong đó:
–continue
Tiếp tục rebase sau khi giải quyết xung đột trong commit
git rebase --continue
–abort
Hoàn tác hành động rebase và quay lại trạng thái trước khi bắt đầu.
git rebase --abort
–skip
Bỏ qua commit hiện tại nếu không muốn tiếp tục với commit này.
git rebase --skip
–keep-empty
Mặc định, khi rebase sẽ bỏ qua các commit rỗng (không có nội dung thay đổi), tùy chọn này giúp giữ lại các commit rỗng đó trong quá trình rebase:
git rebase <tên nhánh> --keep-empty
–quiet
Tùy chọn –quiet giúp giảm bớt hoặc tắt các thông báo mà Git hiển thị. Điều này hữu ích khi bạn muốn thực hiện các lệnh mà không cần thông tin chi tiết hoặc để tạo script tự động.
git rebase <tên nhánh> --quiet
Git thực hiện rebase mà không hiển thị chi tiết về từng commit hoặc thông báo thông thường. Nếu không có lỗi hoặc xung đột, bạn sẽ không thấy bất kỳ đầu ra nào trong terminal.
–verbose
Trái ngược với –quiet, tùy chọn –verbose giúp hiển thị thêm thông tin chi tiết về quá trình thực hiện lệnh. Điều này đặc biệt hữu ích khi bạn muốn hiểu rõ hơn về những gì Git đang làm.
git rebase <tên nhánh> --verbose
–strategy
Theo mặc định, khi thực hiện git rebase, nếu có xung đột xảy ra, bạn sẽ cần thực hiện xử lý xung đột thủ công bằng việc lựa chọn phần code được giữ lại. Tuy nhiên bạn có thể thực hiện tự động những công việc này bằng tùy chọn –strategy.
git rebase --strategy=<option> <tên nhánh>
Trong đó, chúng ta sẽ có những option sau:
Tùy chọn | Ý nghĩa | Trường hợp sử dụng |
recursive | Mặc định, hỗ trợ 3-way (*) merge và xử lý tốt các trường hợp phức tạp. | Sử dụng cho hầu hết các trường hợp. |
resolve | Hợp nhất 2-way (**) đơn giản, không hỗ trợ merge phức tạp. | Khi xung đột đơn giản hoặc không phức tạp. |
ours | Giữ toàn bộ thay đổi từ nhánh hiện tại (bỏ qua thay đổi từ nhánh gốc) | Khi bạn muốn ưu tiên giữ thay đổi của nhánh hiện tại. |
theirs | Giữ toàn bộ thay đổi từ nhánh gốc (bỏ qua thay đổi từ nhánh hiện tại) | Khi bạn muốn ưu tiên giữ thay đổi của nhánh gốc. |
(*) 3-way merge: Thực hiện so sánh sự khác nhau ở cả hai phiên bản của tập tin và điểm chung gốc (common ancestor) của 2 nhánh
(**) 2-way merge: So sánh trực tiếp giữa hai phiên bản của file (file từ nhánh hiện tại và file từ nhánh cần hợp nhất), không sử dụng điểm chung (common ancestor)
Các câu hỏi thường gặp về Git rebase
Khi nào nên sử dụng git rebase thay vì git merge?
Việc lựa chọn git rebase hay là git merge sẽ phụ thuộc vào nhu cầu quản lý lịch sử commit của dự án. Chúng ta có thể sử dụng git rebase thay vì git merge trong các trường hợp như:
- Khi muốn giữ lịch sử commit gọn gàng và tuyến tính
- Khi làm việc trên nhánh cá nhân và không muốn tạo ra nhiều commit merge dư thừa
- Khi muốn tích hợp các thay đổi từ nhánh gốc
- Khi cần gộp (squash) các commit nhỏ
Làm sao để hoàn tác hành động git rebase?
Trong trường hợp quá trình rebase vẫn chưa kết thúc do có xung đột xảy ra, nếu bạn muốn quay trở lại trạng thái trước khi rebase, bạn có thể sử dụng lệnh:
git rebase --abort
Tuy nhiên, nếu quá trình rebase đã hoàn tất, lúc này bạn sẽ phải cần một công cụ khác để thực hiện việc quay trở lại, đó chính là git reflog. Sử dụng git reflog để xem lại lịch sử HEAD và tìm lại commit HEAD trước thời điểm rebase.
git reflog
Ví dụ về đầu ra:
hash_1 (HEAD -> feature) rebase finished: returning to refs/heads/feature hash_2 HEAD@{1}: rebase (start): checkout main hash_3 HEAD@{2}: commit: Add feature
Trong đó, ta thấy HEAD@{2} là trạng thái trước khi bắt đầu rebase. Lúc này ta có thể sử dụng git reset để quay trở lại trạng thái ban đầu:
git reset --hard HEAD@{2}
Git rebase có làm mất dữ liệu không?
Git rebase không trực tiếp gây ra tình trạng mất dữ liệu nếu chúng ta hiểu đúng về nó và thực hiện đúng. Tuy nhiên, nếu không cẩn thận, dữ liệu cũng có thể bị mất trong một số trường hợp như:
- Trong quá trình rebase tương tác (interactive rebase), bạn vô tình chọn drop hoặc xóa commit
- Xử lý xung đột chưa chính xác, chọn nhầm nội dung code đã lỗi thời
- Sử dụng –skip chưa đúng, dẫn đến các xung đột trong commit bị bỏ qua
Tổng kết
Git rebase là lựa chọn tuyệt vời khi bạn muốn duy trì lịch sử commit sạch sẽ, tuyến tính và dễ theo dõi, đặc biệt trong các dự án có nhánh phát triển riêng biệt. Tuy nhiên, chúng ta cần phải hiểu rõ bản chất của nó cũng như cách sử dụng phù hợp trong các tình huống thực tế để tránh các rủi ro không đáng có trong quá trình phát triển phần mềm.
Tviec hy vọng bài viết đã cung cấp kiến thức hữu ích giúp bạn làm việc hiệu quả hơn với Git và Git rebase.