Merging vs. rebasing
Lệnh git rebase
có tiếng là một phép thuật huyền bí trong Git mà những người mới bắt đầu nên tránh xa, nhưng thực tế nó có thể làm cho cuộc sống của một developer team trở nên dễ dàng hơn nhiều khi được sử dụng cẩn thận. Trong bài viết này, chúng ta sẽ so sánh git rebase
với lệnh git merge
có liên quan và xác định tất cả các cơ hội tiềm năng để kết hợp rebasing vào quy trình làm việc Git thông thường.
Tổng quan về khái niệm
Điều đầu tiên cần hiểu về git rebase
là nó giải quyết cùng một vấn đề như git merge
. Cả hai lệnh này đều được thiết kế để tích hợp các thay đổi từ một nhánh vào nhánh khác - chúng chỉ thực hiện điều đó theo những cách rất khác nhau.
Hãy xem xét điều gì xảy ra khi bạn bắt đầu làm việc trên một tính năng mới trong một nhánh chuyên dụng, sau đó một thành viên khác trong nhóm cập nhật nhánh main
với các commit mới. Điều này dẫn đến một lịch sử phân nhánh, điều này nên quen thuộc với bất kỳ ai đã sử dụng Git như một công cụ cộng tác.
Bây giờ, giả sử rằng các commit mới trong main
có liên quan đến tính năng mà bạn đang làm việc. Để kết hợp các commit mới vào nhánh feature
của bạn, bạn có hai lựa chọn: merging hoặc rebasing.
Lựa chọn merge
Lựa chọn dễ dàng nhất là merge nhánh main
vào nhánh feature bằng cách sử dụng một cái gì đó như sau:
git checkout feature
git merge main
Hoặc, bạn có thể rút gọn thành một dòng lệnh:
git merge feature main
Điều này tạo ra một "merge commit" mới trong nhánh feature
kết nối lịch sử của cả hai nhánh, cho bạn một cấu trúc nhánh trông như thế này:
Merging rất tốt vì đó là một thao tác không phá hủy. Các nhánh hiện có không bị thay đổi theo bất kỳ cách nào. Điều này tránh được tất cả những rủi ro tiềm ẩn của rebasing (được thảo luận bên dưới).
Mặt khác, điều này cũng có nghĩa là nhánh feature
sẽ có một merge commit thừa mỗi khi bạn cần tích hợp các thay đổi từ upstream. Nếu nhánh main
có nhiều hoạt động, điều này có thể làm cho lịch sử của nhánh feature của bạn trở nên khá rối rắm. 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ó vẫn có thể gây khó khăn cho các developer khác trong việc hiểu rõ lịch sử của dự án.
Lựa chọn rebase
Thay vì merging, bạn có thể rebase nhánh feature
lên nhánh main
bằng cách sử dụng các lệnh sau:
git checkout feature
git rebase main
Điều này đặt toàn bộ nhánh feature lên trên đỉnh của nhánh main, nghĩa là nó gộp hết các commit mới từ main vào. Nhưng thay vì dùng một commit merge, rebase viết lại lịch sử dự án bằng cách tạo ra các commit mới hoàn toàn, tương ứng với từng commit trong nhánh gốc.
Lợi ích chính của rebasing là bạn có được lịch sử dự án sạch sẽ hơn nhiều. Đầu tiên, nó loại bỏ các merge commit không cần thiết mà git merge
yêu cầu. Thứ hai, như bạn có thể thấy trong sơ đồ trên, rebasing cũng dẫn đến một lịch sử dự án hoàn toàn tuyến tính - bạn có thể theo dõi đầu của feature
đến tận đầu dự án mà không có bất kỳ phân nhánh nào. Điều này làm cho việc điều hướng dự án của bạn trở nên dễ dàng hơn với các lệnh như git log
, git bisect
, và gitk
.
Tuy nhiên, cái giá phải trả cho lịch sử commit gọn gàng này là an toàn và khả năng truy vết. Nếu bạn không tuân theo Quy tắc Vàng của Rebasing, việc viết lại lịch sử dự án có thể gây hậu quả nghiêm trọng cho quy trình làm việc nhóm. Và một điểm kém quan trọng hơn là rebasing làm mất đi thông tin mà một commit merge cung cấp - bạn không thể biết được khi nào những thay đổi từ nhánh gốc được đưa vào nhánh tính năng.
Rebasing tương tác
Rebasing tương tác cho bạn cơ hội thay đổi các commit khi chúng được chuyển sang nhánh mới. Điều này còn mạnh mẽ hơn cả một rebase tự động, vì nó cung cấp khả năng kiểm soát hoàn toàn đối với lịch sử commit của nhánh. Thông thường, điều này được sử dụng để dọn dẹp một lịch sử lộn xộn trước khi merge một nhánh feature vào main
.
Để bắt đầu một phiên rebasing tương tác, hãy truyền tùy chọn i
cho lệnh git rebase
:
git checkout feature
git rebase -i main
Điều này sẽ mở một trình soạn thảo văn bản liệt kê tất cả các commit sắp được di chuyển:
pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
Danh sách này xác định chính xác nhánh sẽ trông như thế nào sau khi rebase được thực hiện. Bằng cách thay đổi lệnh pick
và/hoặc sắp xếp lại các mục, bạn có thể làm cho lịch sử của nhánh trông như bất cứ điều gì bạn muốn. Ví dụ, nếu commit thứ 2 sửa một vấn đề nhỏ trong commit đầu tiên, bạn có thể gộp chúng thành một commit duy nhất với lệnh fixup
:
pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
Khi bạn lưu và đóng file, Git sẽ thực hiện rebase theo hướng dẫn của bạn, dẫn đến lịch sử dự án trông như sau:
Việc loại bỏ các commit không đáng kể như thế này làm cho lịch sử của tính năng của bạn dễ hiểu hơn nhiều. Đây là điều mà git merge
đơn giản không thể làm được.
Quy tắc vàng của rebasing
Khi bạn đã hiểu rebasing là gì, điều quan trọng nhất cần học là khi nào không nên làm điều đó. Quy tắc vàng của git rebase
là không bao giờ sử dụng nó trên các nhánh công khai.
Ví dụ, hãy nghĩ về điều gì sẽ xảy ra nếu bạn rebase main
lên nhánh feature
của bạn:
Rebase di chuyển tất cả các commit trong main
lên đầu của feature
. Vấn đề là điều này chỉ xảy ra trong repository của bạn. Tất cả các developer khác vẫn đang làm việc với main
ban đầu. Vì rebasing dẫn đến các commit hoàn toàn mới, Git sẽ nghĩ rằng lịch sử nhánh main
của bạn đã có sự thay đổi.
Cách duy nhất để đồng bộ hóa hai nhánh main
là merge chúng lại với nhau, dẫn đến một merge commit thêm và hai bộ commit chứa cùng các thay đổi (các commit gốc và các commit từ nhánh đã rebase của bạn). Không cần phải nói, đây là một tình huống rất khó hiểu.
Vì vậy, trước khi bạn chạy git rebase
, hãy luôn tự hỏi, "Có ai khác đang xem nhánh này không?" Nếu câu trả lời là có, hãy bỏ tay khỏi bàn phím và bắt đầu suy nghĩ về một cách không phá hủy để thực hiện các thay đổi của bạn (ví dụ: lệnh git revert
). Nếu không, bạn có thể tự do viết lại lịch sử tùy thích.
Force-pushing
Nếu bạn cố gắng push nhánh main
đã rebase trở lại repository từ xa, Git sẽ ngăn bạn làm điều đó vì nó xung đột với nhánh main
từ xa. Nhưng, bạn có thể buộc push thực hiện bằng cách truyền cờ --force
, như sau:
# Hãy rất cẩn thận với lệnh này!
git push --force
Việc này ghi đè nhánh main
trên remote để khớp với nhánh đã rebase từ repo của bạn và khiến mọi thứ rất rối rắm cho phần còn lại của team. Vậy nên, hãy cực kỳ cẩn thận khi dùng lệnh này và chỉ dùng khi bạn biết chính xác mình đang làm gì.
Một trong số ít trường hợp bạn nên dùng force-push là khi bạn đã dọn dẹp local repo sau khi đã push một nhánh tính năng riêng lên remote repo (ví dụ để backup). Việc này giống như nói rằng: "Ối, tôi không thực sự muốn push phiên bản gốc của nhánh tính năng đó. Hãy lấy phiên bản hiện tại thay thế." Lại nữa, điều quan trọng là không ai đang làm việc trên các commit từ phiên bản gốc của nhánh tính năng đó.
Quy trình làm việc chi tiết
Bạn có thể tích hợp rebase vào quy trình Git hiện tại nhiều hay ít tùy theo mức độ thoải mái của team. Trong phần này, chúng ta sẽ xem xét những lợi ích mà rebase có thể mang lại ở các giai đoạn khác nhau trong quá trình phát triển một tính năng.
Bước đầu tiên trong bất kỳ quy trình nào sử dụng git rebase
là tạo một nhánh riêng cho mỗi tính năng. Điều này tạo ra cấu trúc nhánh cần thiết để bạn có thể sử dụng rebase một cách an toàn:
Dọn dẹp local
Một trong những cách tốt nhất để đưa rebase vào quy trình làm việc là dọn dẹp các tính năng đang phát triển trên local. Bằng cách thực hiện rebase tương tác định kỳ, bạn có thể đảm bảo mỗi commit trong tính năng của mình đều tập trung và có ý nghĩa. Điều này cho phép bạn viết code mà không cần lo lắng về việc chia nhỏ thành các commit riêng lẻ - bạn có thể sửa chữa sau.
Khi gọi git rebase
, bạn có hai lựa chọn cho base mới: Nhánh cha của tính năng (ví dụ: main
), hoặc một commit trước đó trong tính năng của bạn. Chúng ta đã thấy ví dụ về lựa chọn đầu tiên trong phần Rebase Tương tác. Lựa chọn sau thì hữu ích khi bạn chỉ cần sửa vài commit cuối. Ví dụ, lệnh sau bắt đầu một rebase tương tác chỉ với 3 commit cuối cùng.
git checkout feature
git rebase -i HEAD~3
Bằng cách chỉ định HEAD~3
làm base mới, bạn không thực sự di chuyển nhánh - bạn chỉ đang viết lại tương tác 3 commit theo sau nó. Lưu ý rằng điều này sẽ không kết hợp các thay đổi từ upstream vào nhánh feature
.
Nếu bạn muốn viết lại toàn bộ tính năng bằng phương pháp này, lệnh git merge-base
có thể hữu ích để tìm base gốc của nhánh feature
. Lệnh sau trả về ID commit của base gốc, mà bạn có thể truyền vào git rebase
:
git merge-base feature main
Cách sử dụng rebase tương tác này là một cách tuyệt vời để đưa git rebase
vào quy trình làm việc của bạn, vì nó chỉ ảnh hưởng đến các nhánh local. Điều duy nhất các developer khác sẽ thấy là sản phẩm hoàn thiện của bạn, nên là một lịch sử nhánh tính năng sạch sẽ, dễ theo dõi.
Nhưng lại nữa, điều này chỉ hoạt động với các nhánh tính năng riêng tư. Nếu bạn đang cộng tác với các developer khác trên cùng một nhánh tính năng, nhánh đó là công khai, và bạn không được phép viết lại lịch sử của nó.
Không có lựa chọn thay thế git merge
nào để dọn dẹp các commit local bằng rebase tương tác.
Đưa các thay đổi upstream vào nhánh tính năng
Trong phần Tổng quan Khái niệm, ta đã thấy cách một nhánh tính năng có thể đưa các thay đổi upstream từ main
vào bằng git merge
hoặc git rebase
. Merge là lựa chọn an toàn, giữ nguyên toàn bộ lịch sử repo, trong khi rebase tạo lịch sử thẳng bằng cách chuyển nhánh tính năng lên đầu main
.
Cách dùng git rebase
này tương tự việc dọn dẹp local (và có thể làm đồng thời), nhưng trong quá trình đó nó đưa các commit upstream từ main
vào.
Lưu ý là hoàn toàn hợp lệ khi rebase lên nhánh remote thay vì main
. Điều này có thể xảy ra khi cộng tác cùng tính năng với dev khác và bạn cần đưa thay đổi của họ vào repo của mình.
Ví dụ, nếu bạn và một dev khác tên John thêm commit vào nhánh feature
, repo của bạn có thể trông như sau sau khi fetch nhánh feature
remote từ repo của John:
Bạn có thể giải quyết sự phân nhánh này giống hệt cách tích hợp thay đổi upstream từ main
: hoặc merge feature
local với john/feature
, hoặc rebase feature
local lên đầu john/feature
.
Chú ý rằng rebase này không vi phạm Quy tắc Vàng của Rebase vì chỉ các commit feature
local được di chuyển - mọi thứ trước đó không bị động đến. Điều này giống như nói "thêm thay đổi của tôi vào những gì John đã làm". Trong hầu hết trường hợp, cách này trực quan hơn việc đồng bộ với nhánh remote qua một commit merge.
Mặc định, lệnh git pull
thực hiện merge, nhưng bạn có thể bắt nó tích hợp nhánh remote bằng rebase bằng cách thêm option --rebase
.
Review tính năng với pull request
Khi dùng pull request để review code, tránh dùng git rebase
sau khi tạo pull request. Ngay khi tạo pull request, các dev khác sẽ xem commit của bạn, nghĩa là đó là nhánh công khai. Viết lại lịch sử sẽ khiến Git và đồng nghiệp không thể theo dõi các commit tiếp theo của tính năng đó.
Mọi thay đổi từ dev khác cần được đưa vào bằng git merge
thay vì git rebase
.
Vì vậy, nên dọn dẹp code bằng rebase tương tác trước khi tạo pull request.
Tích hợp tính năng đã được duyệt
Sau khi team duyệt tính năng, bạn có thể rebase tính năng lên đầu nhánh main
trước khi dùng git merge
để đưa tính năng vào codebase chính.
Tình huống này tương tự việc đưa thay đổi upstream vào nhánh tính năng, nhưng vì không được viết lại commit trong main
, cuối cùng bạn phải dùng git merge
để tích hợp. Tuy nhiên, rebase trước khi merge đảm bảo merge sẽ fast-forward, tạo lịch sử thẳng hoàn hảo. Bạn cũng có cơ hội squash các commit bổ sung trong pull request.
Nếu chưa quen git rebase
, bạn có thể rebase trên nhánh tạm. Như vậy, nếu làm rối lịch sử tính năng, bạn có thể quay lại nhánh gốc và thử lại:
git checkout feature
git checkout -b temporary-branch
git rebase -i main
# [Dọn dẹp lịch sử]
git checkout main
git merge temporary-branch
Kết luận
Đó là những điều cơ bản để bắt đầu rebase nhánh. Nếu muốn lịch sử sạch, thẳng không có commit merge không cần thiết, dùng git rebase
thay vì git merge
khi tích hợp thay đổi từ nhánh khác.
Ngược lại, nếu muốn giữ toàn bộ lịch sử dự án và tránh rủi ro viết lại commit công khai, có thể dùng git merge
. Cả hai đều hợp lệ, nhưng giờ bạn có thêm lựa chọn tận dụng lợi ích của git rebase
.
INFO
Bài viết này được dịch từ Merge vs. rebase của Atlassian.