Fundamentals
Git, Phần 2: Merge nối hai nhánh snapshot
Fast-forward chỉ trượt nhãn. Khi cả hai phía đều tiến, merge ghi commit hai parent.
- Gieo
- Chăm lần cuối

Phần 1 nói branch là nhãn dán trên commit. Phần 2 hỏi khi hai nhãn tách xa và bạn muốn một lịch sử lại thì chuyện gì xảy ra. Sau bài này bạn nhìn đồ thị phân biệt fast-forward và merge commit, và đoán git merge sẽ cho kết quả nào trước khi chạy.
Đây là Phần 2 trong loạt sáu phần. Phần 1 đã nói commit, branch và HEAD. Hoàn tác, remote, rebase và quy trình nhóm ở sau. Ta vẫn bỏ qua ý kiến chiến lược branch. Câu hỏi ở đây là cơ học: object và ref nào di chuyển?
Nó là gì?
Merge đưa một nhánh commit vào branch bạn đang đứng. Git tìm tip hai nhánh và merge base (tổ tiên chung gần nhất), rồi quyết cách ghi điểm nối.
Khi branch bạn đang đứng là tổ tiên của branch merge vào, Git không cần commit mới. Nó trượt nhãn branch tới tip kia. Đó là fast-forward. Cả hai tên có thể cùng trỏ một commit. Danh sách parent của tip đó không đổi. Về mặt chính thức fast-forward vẫn là một dạng merge — trường hợp “chỉ cập nhật con trỏ branch” là đủ.
Khi cả hai bên có commit mới kể từ lúc tách, fast-forward không thể. Git tạo merge commit: snapshot bình thường mà object commit liệt kê hai parent. Parent đầu là tip bạn đang đứng. Parent thứ hai là tip merge vào. Working tree và index là kết quả gộp hai nhánh. Trong git log --graph, commit đó là chỗ hai nhánh gặp lại.
git switch -c feature tạo nhãn và chuyển HEAD lên đó một bước. Git cũ dùng git checkout -b feature. Sau đó mỗi git commit chỉ đẩy branch mà HEAD theo — cách hai nhánh tách ra.
Tôi coi merge trả lời một câu: “Nhãn của tôi trượt được, hay cần ảnh mới trỏ cả hai tip?” Trả lời được chỉ từ đồ thị thì output dòng lệnh không còn cảm giác ngẫu nhiên.
Khi nào dùng?
Dùng merge khi branch hiện tại cần gồm việc đã lên tip branch khác. Pull từ remote thường cùng lựa chọn: fast-forward nếu không có commit local, hoặc merge commit nếu có.
Không phải lúc nào cũng cần merge commit. Nếu main không động trong khi feature lớn, fast-forward mặc định là trung thực với lịch sử: một đường thẳng. Tới hình hai parent khi cả hai bên đổi, hoặc khi cố ý --no-ff để ép merge commit dù fast-forward được.
Conflict là bài khác. Cả hai sửa cùng dòng, Git dừng và bạn hoàn thiện snapshot tay. Bài này giữ hình đồ thị khi merge sạch. Phần 3 đi sâu hoàn tác lộn xộn; ở đây chỉ cần git merge --abort — về tip trước merge và xóa trạng thái merge đang dở.
Trong team tôi hay bảo nói hình dự đoán trước merge: “main đang sau feature, nên sẽ fast-forward,” hoặc “cả hai tip đều đi, nên chờ merge commit.” Một câu đó bắt hầu hết bất ngờ.
Demo tương tác
Bấm Watch demo để đi hết nhánh phân kỳ. Ba nhịp: feature lên C2 trong khi main ở C1, main lên C3 thành chữ V, rồi git merge feature làm node merge M nhấp nháy và token hổ phách đi cả hai cạnh parent. Readout chuyển merge: commit.
Reset và thử nhánh kia: tạo feature, commit một lần, switch main, merge ngay — readout nên merge: ff, main trượt lên C2 không có node M. Tương phản đó là cả bài.
Minh họa tương tác
git switch -c creates the branch and checks it out in one step. The tip starts at the commit you were on.
Start with commit C1 on main. Create feature and commit C2 on it. Switch back to main. Either merge for a fast-forward that moves main onto C2, or commit C3 on main first and then merge to create merge commit M with parents C3 and C2. Tokens slide along parent edges when M appears.
- C1 is shared history. main and HEAD start here.
- git switch -c feature creates the sticky note and moves HEAD onto it.
- A commit on feature grows C2. main is still on C1.
- Back on main. Merge now would fast-forward. Instead, commit on main to diverge.
- C3 lands on main. The histories no longer form a straight line.
- git merge feature creates M with two parents. Watch tokens travel both parent edges.
current: main · merge: none
Code
Hai repo throwaway cùng máy. core.autocrlf false để line ending yên. Hash khác trên máy bạn; hình dạng không. Đã kiểm tra Git 2.43.0.windows.1.
Fast-forward
$ git init -b main
Initialized empty Git repository in .../git-merge-demo/.git/
$ # write readme note, then:
$ git add note.txt
$ git commit -m "Add note"
[main (root-commit) 51520d3] Add note
1 file changed, 1 insertion(+)
$ git switch -c feature
Switched to a new branch 'feature'
$ # add a feature line, then commit
$ git commit -m "Feature work"
[feature 197e29f] Feature work
1 file changed, 1 insertion(+)
$ git log --oneline --graph --decorate --all
* 197e29f (HEAD -> feature) Feature work
* 51520d3 (main) Add note
$ git switch main
Switched to branch 'main'
$ git merge feature
Updating 51520d3..197e29f
Fast-forward
note.txt | 1 +
1 file changed, 1 insertion(+)
$ git log --oneline --graph --decorate --all
* 197e29f (HEAD -> main, feature) Feature work
* 51520d3 Add note
$ git log -1 --format=parents:%P%ncommit:%h
parents:51520d3577597e12f70b93cc6ec839153e38d470
commit:197e29f
Chữ Fast-forward là dấu hiệu. Không commit id mới. main và feature cùng tên 197e29f, commit đó vẫn một parent. Chạy lại git merge feature Git báo already up to date — không còn gì để trượt.
Lịch sử đã phân nhánh
Dùng file khác mỗi bên để merge xong không conflict.
$ git init -b main
$ git commit -m "Add readme" # 53fd2ac on main
$ git switch -c feature
$ git commit -m "Add feature.txt" # 925c8cc on feature
$ git switch main
$ git commit -m "Add main.txt" # 6e2a7d2 on main
$ git log --oneline --graph --decorate --all
* 925c8cc (feature) Add feature.txt
| * 6e2a7d2 (HEAD -> main) Add main.txt
|/
* 53fd2ac Add readme
$ git merge feature -m "Merge branch feature"
Merge made by the 'ort' strategy.
feature.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 feature.txt
$ git log --oneline --graph --decorate --all
* 6e5a47f (HEAD -> main) Merge branch feature
|\
| * 925c8cc (feature) Add feature.txt
* | 6e2a7d2 Add main.txt
|/
* 53fd2ac Add readme
$ git cat-file -p HEAD
tree 68a7166b8865fecca5a891a9e7b038f668d3418f
parent 6e2a7d2c24b8eb2bd793deaa20e07598ab6e70f6
parent 925c8cc2e29ed449ffaed9ebd6b8b828c7d60f3b
author Dev Example <[email protected]> ...
committer Dev Example <[email protected]> ...
Merge branch feature
Hai dòng parent nghĩa merge commit. Parent đầu là tip main trước merge (6e2a7d2). Parent thứ hai là tip feature (925c8cc). Đọc git cat-file -p HEAD một lần giúp demo bám hơn chỉ diagram.
Muốn merge commit dù fast-forward được, truyền --no-ff. Đó là chọn chính sách hình đồ thị, không phải thuật toán merge khác. git merge mặc định vẫn fast-forward khi có thể. Config merge.ff=false làm --no-ff mặc định trên máy bạn; để yên đến khi team thống nhất phong cách lịch sử.
Mã nguồn
Mã nguồn
Chỉ đọcTooling/Git/merge-session.sh
Chỉ đọc
# Shared starting commit
git init -b main
echo shared > readme.txt
git add readme.txt
git commit -m "Add readme"
# Feature advances alone (fast-forward candidate)
git switch -c feature
echo from feature > feature.txt
git add feature.txt
git commit -m "Add feature.txt"
# Main advances too (histories diverge)
git switch main
echo from main > main.txt
git add main.txt
git commit -m "Add main.txt"
# Merge joins the two tips with a two-parent commit
git merge feature -m "Merge branch feature"
Ưu và nhược điểm
+ Ưu điểm
- Fast-forward giữ lịch sử một đường khi đúng là vậy, độc giả không săn merge node rỗng.
- Merge commit ghi rõ cả hai tip, tháng sau vẫn thấy chỗ nối trong git log --graph.
- git switch -c tạo và checkout branch không thêm workflow thứ hai cho người mới.
− Nhược điểm
- Hai kết quả merge cho một lệnh làm rối đến khi kiểm tra quan hệ tổ tiên.
- Conflict dừng merge giữa chừng; đồ thị chưa xong đến khi xong hoặc abort.
- Ép --no-ff mọi nơi thêm node không mã hóa thay đổi file thêm, chỉ cấu trúc — blame và bisect ồn hơn.
Mẹo
- 01Trước merge, vẽ hai tip và tổ tiên chung. Nếu tip bạn nằm trên đường tổ tiên tới tip kia, chờ fast-forward.
- 02Đọc dòng đầu output git merge. Fast-forward versus Merge made by cho biết hình không cần GUI.
- 03Sau merge chạy git log --oneline --graph --decorate --all. Dấu gạch chéo là merge commit; một đường thẳng sau Updating .. là fast-forward.
- 04Xem parent bằng git cat-file -p HEAD hoặc git log -1 --format=%P khi đồ thị mơ hồ. Hai hash nghĩa hai parent.
- 05Merge dừng vì conflict: sửa file, git add, rồi git commit để xong merge commit. Hoặc git merge --abort về tip trước merge; chỉ xóa merge đang dở, không xóa commit cũ trên branch.
- 06Nhớ Phần 1: merge di chuyển nhãn branch và có thể thêm một commit. Không copy thư mục file; HEAD vẫn chỉ tip bạn đứng.
Series
- Phần 1: Git, Phần 1: Commit là snapshot, branch là nhãn dán
- Phần 2: Git, Phần 2: Merge nối hai nhánh snapshot
- Phần 3: Git, Phần 3: Restore, revert và reset đụng các lớp khác nhau
- Phần 4: Git, Phần 4: Remote là một repository khác
- Phần 5: Git, Phần 5: Rebase replay commit lên parent mới
- Phần 6: Git, Phần 6: Team thống nhất lịch sử thế nào
Tiếp theo
Bài kế trong đường đọc: Git, Phần 3: Restore, revert và reset đụng các lớp khác nhau