Tôi nhận ra một số lượng đáng kể đồng nghiệp của tôi có một sự hiểu khá mơ hồ về Merge và Rebase. Vì vậy tôi biên một bài vừa để giải thích, vừa để chia sẻ với ae cách một git guru làm việc.
Có thể bạn sẽ nghĩ: lại một bài so sánh nữa, tại sao mình lại phải đọc nó nhỉ?
Câu trả lời ngắn ngọn của tôi: Cách bạn xử lý lịch sử git sẽ lên một tầm cao mới.
Chúng là gì
Trước tiên, tôi sẽ nói lại ý nghĩa của 2 tính năng này. Tôi tin rằng nhiều anh em tuy lập trình đã lâu nhưng chưa hiểu hết vai trò của chúng.
Tôi cho rằng bạn đã hiểu cơ bản git là gì và dùng git để làm gì. Nếu bạn là người mới bắt đầu, hãy dành thời gian để tìm hiểu git trước khi đọc bài.
Merge
Đơn giản như cái tên của nó: khi bạn muốn kết hợp code của mình vào chung code của người khác, bạn dùng merge. Hãy xem hình minh họa sau:

Fig 1: Nếu thời điểm commit code của bạn mới hơn, sau khi merge thì code của bạn sẽ
... ở vị trí mới hơn ¯\_(ツ)_/¯
Chiến lược merge trên người ta gọi là Fast Forward Merge. Thật là đơn giản đúng không? Tuy nhiên trong thực tế mọi thứ hiếm khi được "vui vẻ" như vậy!
Khi làm việc nhóm và mọi người cùng phát triển song song, sẽ có lúc phần code của bạn được commit sau những thay đổi của người khác:

Bạn muốn kết hợp chúng vào với nhau thì đây sẽ là công việc được làm sau hậu trường: Git sẽ TỰ ĐỘNG TẠO MỘT COMMIT MỚI VÀ GHI VÀO TIMELINE. Commit mới này GỘP commit của bạn với commit mới nhất của nhánh đích.
Hình dưới đây minh hoạ hoạ lệnh git merge <nhánh-đích> được thực thi:

Fig 2: Commit mới được tạo để đánh dấu việc code được kết hợp, toàn bộ mọi thứ khác được giữ nguyên
Ưu điểm của việc này là lịch sử phát triển của cả 2 nhánh được giữ nguyên vẹn. Bạn sẽ thấy được rất rõ rành mạch phát triển của từng nhánh, cùng với thời điểm chúng được kết hợp.

Fig 3: Thời điểm merge code, lịch sử phát triển của các nhánh được giữ nguyên vẹn
Nhưng đôi khi việc này khiến lịch sử git bị rối, tăng rủi ro conflict trong quá trình merge sau này. Một trong những trường hợp kinh điển là khi bạn làm việc với nhiều nhánh feature nhỏ, liên tục cập nhật chúng với nhau (không phải với nhánh main). Git timeline của bạn có thể sẽ trông như thế này:
Đây là lúc bạn nên nghĩ đến Rebase.
Rebase
Rebase tạm dịch là "tái cấu trúc". Nó cũng là một cách để kết hợp code từ nhánh này sang nhánh khác, nhưng thay vì tạo một commit mới để ghi lại việc kết hợp, Rebase sẽ DI CHUYỂN TOÀN BỘ CÁC COMMIT CỦA BẠN LÊN TRÊN ĐẦU NHÁNH ĐÍCH.
Dưới đây minh hoạ quá trình kết hợp code giữa 2 nhánh feature thành một sử dụng rebase:
git checkout feature/abc # <== nhánh bạn đang làm việc
git rebase feature/xyz # <== nhánh bạn muốn kết hợp vào

Fig 5: Để ý rằng các commit của bạn đã được "di chuyển" lên trên cùng của nhánh đích
Việc này có mặt lợi là làm cho lịch sử git của bạn trở nên thẳng hơn, dễ theo dõi hơn. Đồng thời giảm thiểu rủi ro conflict khi merge vào nhánh chính (main) sau này.

Fig 6: Lịch sử git trở nên thẳng và sạch hơn rất nhiều so với Fig 4
NHƯNG NHƯNG NHƯNG ...
Khi nào dùng Merge, khi nào dùng Rebase?
Không phải tự nhiên lại tồn tại 2 cách kết hợp code như vậy. Hiểu rõ về chúng sẽ giúp bạn làm việc dễ dàng hơn rất nhiều.
Trước tiên nhớ kỹ điều này trong đầu, tôi sẽ giải thích sau:
"Merge dùng cho KẾT HỢP nhánh, Rebase dùng cho CẬP NHẬT nhánh."
Dùng Merge khi kết hợp nhánh hoàn chỉnh
Để hiểu được lý do, hãy cùng so sánh lịch sử git sau khi dùng merge và rebase để kết hợp nhánh feature vào nhánh main.

Fig 7: Lịch sử gốc được bảo tồn khi dùng merge (trái),
bị xáo trộn khi khi dùng rebase (phải)
Vì merge giữ nguyên trạng lịch sử phát triển của các nhánh - rebase thì không - nên nó rất phù hợp để sử dụng khi bạn muốn kết hợp một nhánh hoàn chỉnh vào nhánh chính (main/develop). Có 3 lý do chính tại sao lại như vậy:
- Dễ dàng theo dõi: Giữ nguyên lịch sử phát triển giúp các thành viên trong nhóm dễ dàng theo dõi và hiểu các thay đổi đã được thực hiện.
- Giảm rủi ro xung đột: Khi kết hợp một nhánh hoàn chỉnh, việc giữ nguyên lịch sử phát triển giúp giảm rủi ro xung đột khi các thay đổi đã được kiểm thử và xác nhận.
- Dễ dàng khôi phục: Nếu cần khôi phục lại một phiên bản trước đó, việc giữ nguyên lịch sử phát triển giúp dễ dàng xác định và khôi phục các thay đổi đã được thực hiện.
Dùng Rebase để cập nhật nhánh
Vì đặc điểm của rebase là di chuyển các commit của bạn lên trên cùng của nhánh đích và không tạo thêm commit mới, nên nó rất phù hợp để sử dụng khi bạn muốn cập nhật nhánh của mình với các thay đổi mới nhất từ nhánh chính (main/develop), hoặc từ các nhánh feature khác.
Xem xét các ví dụ sau:
- Bạn đang làm việc trên nhánh
feature/abc, trong khi đó nhánhmainđã có những thay đổi mới. Bạn muốn cập nhật nhánhfeature/abcvới những thay đổi mới nhất từmainđể tránh xung đột khi merge sau này. - Trước khi tạo pull request, bạn muốn đảm đảm bảo mọi thứ đã được cập nhật với nhánh chính (main/develop).
- Bạn đang làm việc với nhiều nhánh feature nhỏ, và bạn muốn cập nhật nhánh
feature/abcvới các thay đổi từ nhánhfeature/xyz,feature/mnk,... để đảm bảo rằng bạn đang làm việc với phiên bản mới nhất của code. - Bạn muốn giữ lịch sử git của mình sạch sẽ, tránh việc tạo quá nhiều commit merge không cần thiết.
Chúng đều là những trường hợp điển hình để sử dụng rebase. Đặc điểm chung là chúng không quá cần thiết phải giữ nguyên lịch sử phát triển của nhánh, mà quan trọng là cập nhật code mới nhất để làm việc hiệu quả.
Làm sao để PRO?
PRO là khi bạn đã "ăn hành" đủ với merge và rebase, trải qua đủ các pha mất code, conflict, bị đồng đội cà khịa, rồi mới ngộ ra chân lý git! Dù không dám nhận là một git guru, nhưng dưới đây là vài chia sẻ của tôi sau khi đã lãnh đủ thứ conflict, mất code, mất thời gian, quở trách,... để bạn không phải trải qua những điều đó nữa.
Cập nhật code đúng cách
Hãy dùng rebase để cập nhật code, dù nó có là từ nhánh chính hay từ nhánh feature khác đi chăng nữa. Trong quá trình này bạn rất có thể sẽ gặp conflict, hãy bình tĩnh giải quyết chúng từng bước một. Đây là cách tôi thường làm:
git checkout feature/abc # <== nhánh bạn đang làm việc
git fetch origin main # <== lấy code mới nhất từ nhánh chính
git rebase origin/main # <== cập nhật nhánh của bạn với nhánh chính
và voila, conflict:
Auto-merging src/app.js
CONFLICT (content): Merge conflict in src/app.js
error: could not apply 1234abcd... Thêm chức năng đăng nhập
Resolve all conflicts manually, mark them as resolved with 'git add <file>', then run 'git rebase --continue'.
=> mở IDE/editor của bạn lên, tìm file bị conflict, giải quyết chúng, sau đó:
git add . # hoặc git add <tên_file>
git rebase --continue
Lặp lại quá trình này cho đến khi rebase hoàn tất, hoặc dùng git rebase --abort để hủy nếu bạn thấy rối hoặc không chắc về điều mình đang làm.
Đến đây thì 96,69% các trường hợp cập nhật code của bạn đã xong xuôi. Chỉ còn vài phần trăm còn lại là những trường hợp đặc biệt, ví dụ như bạn đang rebase mà phát hiện ra nhánh đích đã được squash (gộp commit) hoặc rebase rồi.
Trong trường hợp này, bạn có thể làm theo các bước sau để xử lý:
git fetch origin main # Lấy code mới nhất từ nhánh chính
git checkout feature/abc # Chuyển về nhánh của bạn
git rebase --onto origin/main <commit_hash_cũ> feature/abc
Trong đó <commit_hash_cũ> là commit hash của commit cuối cùng trên nhánh feature/abc trước khi bạn bắt đầu rebase. Lệnh này sẽ giúp bạn di chuyển các commit của nhánh feature/abc lên trên cùng của nhánh main, bỏ qua các commit đã bị squash hoặc rebase. Tôi sẽ nói kỹ hơn về cách xử lý các trường hợp đặc biệt này trong một bài viết khác.
Luôn kiểm tra lại với nhánh dev/main trước khi tạo Pull Request
Khi rebase trong trường hợp các nhánh đích đã được squash hoặc rebase mà tôi đề cập bên trên, hoàn toàn có thể xảy ra trường hợp bạn vô tình bỏ sót một số commit quan trọng. Hay nói một cách đơn giản là bạn đã làm mất code. Độ nghiêm trọng của việc này thì khỏi bàn. Nguy hiểm hơn là nó không để lại dấu vết gì trong lịch sử git cả, và bạn sẽ cực kỳ khó để phát hiện điều đó.
Để tránh điều này, LUÔN LUÔN kiểm tra lại nhánh của bạn với nhánh dev/main trước khi tạo Pull Request. Cách đơn giản nhất là dùng lệnh:
git diff origin/main...feature/abc
hoặc dùng công cụ so sánh của IDE/editor bạn đang dùng.
Nếu bạn phát hiện bị mất code, đừng hoảng loạn, hãy đọc bài viết này của tôi để xử lý like a pro: Cách khôi phục code đã mất trong Git
Sử dụng Merge khi kết hợp nhánh hoàn chỉnh
Nếu bạn chịu trách nhiệm merge Pull Request, hãy sử dụng merge để kết hợp nhánh hoàn chỉnh vào nhánh chính (main/develop). Đó là lý do người ta gọi nó là "Merge Pull Request" chứ không phải "Rebase Pull Request".
Lời kết
Tự hỏi bản thân trước thao tác cập nhật code: Bạn đang muốn kết hợp nhánh hoàn chỉnh, hay chỉ đơn giản là cập nhật nhánh của mình với những thay đổi mới nhất?
Cái gì quan trọng thì phải nói lại lần nữa:
"Merge dùng cho KẾT HỢP nhánh, Rebase dùng cho CẬP NHẬT nhánh."