git merge: cách gộp nhánh và xử lý xung đột
git merge đưa lịch sử của một nhánh khác vào nhánh hiện tại: nếu nhánh hiện tại chưa tiến thêm, Git chỉ dịch chuyển con trỏ (fast-forward), còn không thì tạo commit gộp với hai cha. Chuyển sang nhánh đích rồi chạy git merge tên-nhánh; khi xung đột, sửa tệp, chạy git add và git commit, hoặc hủy tất cả bằng git merge --abort.
Git merge làm gì
Lệnh git merge lấy thay đổi từ nhánh được chỉ định và đưa vào nhánh bạn đang đứng. Điều quan trọng cần nhớ là hướng gộp: gộp «vào chính mình». Muốn gộp feature/login vào main, trước hết chuyển sang main, rồi mới gọi lệnh gộp.
git switch main
git pull # kéo main mới nhất từ máy chủ
git merge feature/login
Git không sao chép từng dòng «bằng tay». Nó tìm tổ tiên chung của hai nhánh (merge base), so sánh cả hai phiên bản với tổ tiên đó và cộng dồn các thay đổi. Nếu hai bên sửa các chỗ khác nhau, kết quả được tạo tự động. Nếu cùng một chỗ bị sửa — xảy ra xung đột, và quyết định thuộc về bạn.
Hai chế độ: fast-forward và gộp ba chiều
Fast-forward — khi không cần commit gộp
Giả sử bạn tách feature từ main, thực hiện ba commit, còn trong main không ai thêm gì. Khi đó lịch sử main chỉ là phần đầu của lịch sử feature. Git dịch chuyển con trỏ main tới commit cuối của feature, và không tạo commit mới.
$ git merge feature
Updating 3f2a1c4..9b7e0d2
Fast-forward
src/auth.js | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
Lịch sử vẫn thẳng hàng, nhưng dấu vết «ở đây từng có nhánh feature» biến mất. Đôi khi như vậy là tốt, đôi khi không — xem phần về --no-ff bên dưới.
Gộp ba chiều và commit gộp
Nếu cả hai nhánh cùng tiến lên từ tổ tiên chung, không thể dịch chuyển con trỏ được nữa. Git thực hiện gộp ba chiều: cơ sở, phía «của chúng ta» (nhánh hiện tại) và phía «của họ» (nhánh được gộp). Kết quả được ghi lại bằng một commit gộp riêng với hai cha.
$ git merge feature
Merge made by the 'ort' strategy.
src/auth.js | 42 ++++++++++++++++++++
src/router.js | 3 +-
2 files changed, 44 insertions(+), 1 deletion(-)
Git sẽ mở trình soạn thảo với thông điệp mặc định kiểu Merge branch 'feature'. Bạn có thể giữ nguyên hoặc viết thêm lý do gộp. Để xem «ngã ba» vừa hình thành, dùng:
git log --oneline --graph --all
Chiến lược ort được dùng mặc định từ Git 2.34; trước đó là recursive. Với công việc hằng ngày không cần đổi chiến lược.
Các cờ hữu ích của git merge
| Cờ | Tác dụng | Khi nào cần |
|---|---|---|
--no-ff | Luôn tạo commit gộp, ngay cả khi có thể fast-forward | Muốn thấy trong lịch sử những commit nào đến từ cùng một tính năng |
--ff-only | Chỉ gộp bằng cách dịch chuyển con trỏ, nếu không được thì báo lỗi | Cập nhật nhánh cục bộ mà không tạo commit gộp ngoài ý muốn |
--squash | Đưa mọi thay đổi của nhánh vào index, nhưng không commit và không tạo liên kết với nhánh | Muốn gom tính năng thành một commit gọn gàng |
--no-commit | Thực hiện gộp nhưng dừng trước khi commit | Muốn kiểm tra kết quả hoặc chạy test trước khi ghi lại |
-m "văn bản" | Đặt thông điệp cho commit gộp | Gộp trong script hoặc không dùng trình soạn thảo |
--abort | Hủy lần gộp đang dở và đưa trạng thái về trước đó | Quá nhiều xung đột, muốn làm lại từ đầu |
--continue | Hoàn tất gộp sau khi đã giải quyết xung đột | Thay thế cho git commit ở bước cuối |
-X ours / -X theirs | Khi đoạn xung đột, tự động lấy phiên bản của mình hoặc của họ | Chắc chắn biết phiên bản nào đúng (hãy thận trọng!) |
--no-ff: giữ ranh giới của tính năng
Nhiều nhóm chỉ gộp tính năng với --no-ff. Khi đó trên đồ thị, mỗi công việc hiện ra như một «bong bóng», và có thể hoàn tác nó bằng một lệnh git revert -m 1 <commit-gộp>. Với fast-forward không có điểm mốc như vậy, muốn hoàn tác phải lùi từng commit một.
--squash: một commit thay vì mười
git switch main
git merge --squash feature/login
git commit -m "Đăng nhập bằng e-mail và mật khẩu"
Sau --squash, Git không coi nhánh là đã gộp: git branch -d feature/login sẽ từ chối xóa với cảnh báo «not fully merged». Hãy xóa bằng git branch -D khi đã chắc chắn mọi thứ đầy đủ. Và đừng tiếp tục làm việc trên cùng nhánh đó sau squash — lần gộp tiếp theo sẽ mang lại những thay đổi đã gộp, và gần như chắc chắn gây xung đột.
Xung đột khi gộp và cách giải quyết
Xung đột là tình huống làm việc bình thường, không phải sự cố. Nó chỉ có nghĩa là hai nhánh đã sửa cùng một dòng (hoặc một bên xóa tệp mà bên kia đang sửa), và Git không dám đoán phiên bản nào đúng.
$ git merge feature/login
Auto-merging src/config.js
CONFLICT (content): Merge conflict in src/config.js
Automatic merge failed; fix conflicts and then commit the result.
Bước 1. Xem điều gì đang xung đột
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/config.js
Bước 2. Sửa tệp
Trong tệp xung đột, Git đánh dấu bằng các ký hiệu:
<<<<<<< HEAD
const TIMEOUT = 5000;
=======
const TIMEOUT = 10000;
>>>>>>> feature/login
Phần trên là nhánh hiện tại của bạn (HEAD), phần dưới là nhánh được gộp. Giữ phương án đúng (hoặc kết hợp cả hai), xóa ba dòng đánh dấu và lưu tệp. Lỗi hay gặp là bỏ sót một dấu đánh dấu ở giữa tệp; trước khi commit hãy tìm chúng: git diff --check sẽ làm nổi bật các dấu còn sót lại.
Bước 3. Đánh dấu đã giải quyết và hoàn tất gộp
git add src/config.js
git commit # hoặc git merge --continue
Nếu xung đột có thể giải quyết hoàn toàn bằng cách chọn một phía, không cần sửa tệp bằng tay:
git checkout --ours src/config.js # giữ phiên bản của nhánh hiện tại
git checkout --theirs src/config.js # lấy phiên bản của nhánh được gộp
git add src/config.js
Thuận tiện hơn trong trình soạn thảo
VS Code, các IDE của JetBrains và nhiều trình soạn thảo khác hiển thị xung đột thành ba cột và có nút «nhận hiện tại / nhận đến / nhận cả hai». Có thể cài công cụ ngoài qua git mergetool. Một thiết lập hữu ích là git config --global merge.conflictStyle zdiff3: trong dấu đánh dấu sẽ xuất hiện thêm phiên bản gốc từ tổ tiên chung, và dễ thấy ai đã thay đổi điều gì.
Đổi ý — hủy gộp
git merge --abort
Lệnh đưa thư mục làm việc và index về trạng thái trước git merge. Vì vậy trước khi gộp nên có thư mục làm việc sạch: commit hoặc cất tạm các chỉnh sửa dở bằng git stash, nếu không sẽ khó tách chúng khỏi kết quả gộp.
Cách hoàn tác một lần gộp đã làm
Nếu commit gộp chưa được gửi lên máy chủ, cách đơn giản nhất là đưa nhánh lùi lại:
git reset --hard ORIG_HEAD
ORIG_HEAD được Git ghi lại trước các thao tác nguy hiểm, trong đó có gộp — đó là vị trí của nhánh trước khi merge. Cờ --hard sẽ xóa các thay đổi chưa commit, vì vậy hãy kiểm tra git status trước.
Nếu việc gộp đã có trong kho chung, không viết lại lịch sử mà tạo commit đảo ngược:
git revert -m 1 a1b2c3d
Cờ -m 1 chỉ định rằng «cha chính» là cha thứ nhất (nhánh mà chúng ta gộp vào). Cần lưu ý một cái bẫy: sau khi revert như vậy, Git coi các commit của tính năng đã được gộp. Nếu sau này muốn gộp lại tính năng đã sửa, trước hết phải revert chính commit revert đó.
Gộp trong làm việc nhóm
git pull là fetch cộng với merge
git pull thông thường tải thay đổi về và gộp nhánh từ xa vào nhánh cục bộ. Nếu bạn và đồng nghiệp cùng commit song song, sẽ xuất hiện commit «Merge branch 'main' of ...». Để tránh các commit gộp kỹ thuật này, đặt git config --global pull.ff only (pull báo lỗi khi không thể fast-forward, bạn tự quyết định tiếp) hoặc dùng git pull --rebase.
Pull request và nút Merge
Trên GitHub và GitLab, việc gộp thường diễn ra qua pull/merge request. Nút «Create a merge commit» tương đương với git merge --no-ff, «Squash and merge» là gộp kèm nén, «Rebase and merge» là rebase. Chọn chế độ nào là thỏa thuận của nhóm, còn cơ chế bên dưới vẫn như khi làm trên terminal.
Gộp main vào nhánh của mình
Nhánh sống lâu dần tách xa main, và xung đột tích tụ. Hãy thường xuyên kéo nhánh chính về nhánh của mình:
git switch feature/login
git fetch origin
git merge origin/main
Như vậy xung đột được giải quyết từng phần nhỏ khi xuất hiện, và lần gộp cuối cùng vào main diễn ra suôn sẻ.
Lỗi thường gặp
- Gộp sai chiều. Chạy
git merge mainkhi đang ởmain, hoặc gộp một tính năng vào tính năng khác. Trước khi gộp, kiểm tra nhánh hiện tại bằnggit branch --show-current. - Gộp với nhánh đã cũ. Gộp
maincục bộ đã không được cập nhật cả tuần. Hãygit pulltrước, rồi mới merge. - «Your local changes would be overwritten by merge». Trong thư mục làm việc có chỉnh sửa chưa commit ở cùng các tệp. Commit chúng hoặc cất vào stash.
- «refusing to merge unrelated histories». Hai nhánh không có tổ tiên chung — ví dụ kho cục bộ được tạo bằng
git init, còn kho từ xa đã có README. Nếu thực sự cần ghép, thêm--allow-unrelated-histories. - Đã commit cả dấu xung đột. Build hỏng ở
<<<<<<<. Kiểm tra bằnggit diff --checkvà chạy test trước khi commit.
Tóm lại
git merge là cách an toàn để ghép nhánh: nó không viết lại các commit đã có, chỉ thêm một commit mới (hoặc dịch chuyển con trỏ). Hãy gộp vào nhánh hiện tại, giữ thư mục làm việc sạch, dùng --no-ff khi cần giữ ranh giới tính năng và --squash khi muốn một commit duy nhất. Xung đột được xử lý trong ba bước: sửa tệp, git add, git commit; còn git merge --abort luôn đưa bạn về điểm xuất phát. Gộp nhánh và xử lý xung đột được luyện tập thực hành trong khóa học Git.
Câu hỏi thường gặp
Khác nhau giữa git merge và git pull là gì?
git pull trước hết tải thay đổi từ máy chủ (git fetch), sau đó gộp nhánh từ xa vào nhánh hiện tại bằng git merge hoặc, nếu được cấu hình, bằng rebase. git merge chỉ làm việc với những gì đã có sẵn cục bộ.
Có thể gộp nhiều nhánh cùng lúc không?
Có, git merge feature-a feature-b sẽ thực hiện gộp kiểu bạch tuộc (octopus) với nhiều cha, nhưng chỉ khi không có xung đột. Trong thực tế, các nhánh thường được gộp từng cái một.
Làm sao biết nhánh nào đã được gộp vào nhánh hiện tại?
Lệnh git branch --merged hiển thị các nhánh đã hoàn toàn nằm trong nhánh hiện tại, còn git branch --no-merged — những nhánh chưa gộp. Các nhánh đã gộp có thể xóa an toàn bằng git branch -d.
Làm sao xem trước việc gộp sẽ mang lại gì?
Chạy git log main..feature để xem các commit không có trong main, và git diff main...feature để xem thay đổi của nhánh so với tổ tiên chung.
ORIG_HEAD sau khi merge là gì?
Đó là tham chiếu Git lưu trước khi gộp, trỏ tới vị trí nhánh trước merge. Có thể hoàn tác lần gộp cục bộ bằng git reset --hard ORIG_HEAD khi nó chưa được gửi lên máy chủ.
Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Git с нуля до уверенного разработчика”.
Đến khóa học