Suốt sê-ri này ta ở trong kho cục bộ: object, index, ref, packfile — tất cả trên máy bạn. Bài cuối ra khỏi đó: khi bạn git push hay git fetch, hai kho Git nói chuyện với nhau thế nào? Hiểu lầm phổ biến là "push gửi cả repo lên". Thực tế Git thông minh hơn nhiều: hai bên thương lượng để tìm chính xác những object bên kia còn thiếu, rồi chỉ gói và gửi phần đó. Bài này bóc từng gói tin thật bằng GIT_TRACE_PACKET trong Git 2.39.
Ba bước của một lần trao đổi
Mọi lần fetch/push (dù qua SSH, HTTPS, hay file cục bộ) đều theo ba bước:
- Ref advertisement: bên kia "khoe" các ref nó đang có kèm SHA đỉnh (
refs/heads/master → 744c6c4...). Nhờ đó bạn biết nó đang ở đâu. - Thương lượng want/have: dựa trên ref, hai bên trao đổi tôi muốn tới SHA nào (
want) và tôi đã có những SHA nào (have), để tìm ranh giới giữa "cái đã chung" và "cái cần truyền". - Pack protocol: bên gửi gói những object thiếu thành một packfile (nén delta như bài git-08) và truyền đi — không bao giờ gửi lại object bên kia đã có.
git push origin master # đẩy object client có mà server thiếu
git fetch origin # kéo object server có mà client thiếu

Hình 1: Ba bước — ref advertisement, thương lượng want/have, pack protocol. Client nói want (muốn tới SHA nào) và have (đã có gì); máy chủ tính hiệu và chỉ gửi phần thiếu.
Đo thật: push chỉ gửi object mới
Dựng một remote (server.git dạng bare) và một client, tạo 3 commit rồi push. Đếm object trên server sau mỗi lần:
git push origin master # lần đầu: server rỗng
git --git-dir=server.git count-objects # -> 9 objects
# thêm 1 commit rồi push lại
git push origin master # -> b3d07fc..457548b
git --git-dir=server.git count-objects # -> 12 objects

Hình 2: Push lần đầu (3 commit) đẩy 9 object; push thêm 1 commit chỉ đẩy 3 object (server 9 → 12). Fetch: client nói want 744c6c4, have 457548b...; máy chủ đáp acknowledgments/ready/packfile, chỉ gửi phần thiếu.
Kết quả xác nhận đúng mô hình: 3 commit đầu = 9 object (3 commit + 3 tree + 3 blob). Thêm một commit rồi push, server lên 12 — chỉ 3 object mới. Push không hề gửi lại 9 object cũ; nó biết server đã có nhờ bước thương lượng.
Bóc gói tin: want và have
Bật GIT_TRACE_PACKET=1 cho một lần fetch để thấy chính các dòng thương lượng. Client2 đang ở commit 457548b, server đã tiến tới 744c6c4:
fetch> want 744c6c4853828049681bd0981868d5a3001bd571
fetch> have 457548bf23bd537642972531a570dfc73e5e14d6
fetch> have 28ca1477f56d383686602fc23512aa2eab6100be
upload-pack> acknowledgments
upload-pack> ready
upload-pack> packfile
Đây là toàn bộ tinh thần của giao thức: client khai want commit đích và liệt kê các commit nó have; server nhận ra 457548b là điểm chung, nên chỉ cần gói các object từ 457548b tới 744c6c4 và gửi trong packfile. Kết quả git fetch in 457548b..744c6c4 master -> origin/master, kéo về 744c6c4 (c6) và 28ca147 (c5) — object cũ 457548b không truyền lại. Với repo hàng GB, cơ chế này là lý do git fetch hằng ngày chỉ mất vài KB.
(Từ Git 2.26, giao thức mặc định là protocol v2 — bạn thấy version 2 trong trace. Nó cải thiện bước ref advertisement: thay vì server tuôn toàn bộ ref mỗi lần kết nối, client hỏi đúng ref cần với ls-refs — repo có hàng chục nghìn ref thì tiết kiệm rất nhiều.)
Vì sao push đôi khi bị từ chối
Một trải nghiệm quen thuộc:
! [rejected] master -> master (fetch first)
error: failed to push some refs
Đây không phải lỗi ngẫu nhiên. Nó xảy ra khi server đã có commit mà bạn chưa có (người khác đã push trong lúc bạn làm việc). Nếu Git cho bạn push, con trỏ master trên server sẽ nhảy sang lịch sử của bạn và bỏ rơi commit của người kia — mất việc. Git chặn đúng lúc đó (một "non-fast-forward"). Cách xử lý đúng: git fetch về, merge hoặc rebase commit của bạn lên trên commit mới của server, rồi push lại — giờ lịch sử của bạn chứa commit của họ nên push là fast-forward hợp lệ.
--force bỏ qua kiểm tra này và ghi đè — nguy hiểm, vì đúng là nó xóa commit của người khác. Nếu buộc phải force (ví dụ sau khi rebase nhánh riêng), dùng --force-with-lease: nó chỉ ghi đè nếu server vẫn ở đúng SHA bạn nghĩ, nên không vô tình đè commit vừa xuất hiện mà bạn chưa thấy.
Đánh đổi và lưu ý
Fetch không đụng nhánh làm việc của bạn; pull thì có. git fetch chỉ cập nhật các ref origin/* (bài git-03), an toàn tuyệt đối — bạn xem git log origin/master rồi mới quyết. git pull = fetch + merge (hoặc rebase), tự động gộp vào nhánh hiện tại; tiện nhưng dễ tạo commit merge ngoài ý muốn. Nhiều người thích fetch rồi tự merge để kiểm soát.
Thin pack và chi phí tính delta. Khi truyền, Git tạo "thin pack" — delta có thể tham chiếu object mà bên nhận đã có (không nằm trong pack). Điều này giảm dữ liệu truyền nhưng bên nhận phải "fix thin" (bù object nền) khi nhận. Với push lớn, bước tính delta ở phía gửi tốn CPU — đó là lúc bạn thấy "Compressing objects" chạy lâu.
Giao thức giống nhau qua mọi kênh. SSH, HTTPS, hay file:// đều chạy cùng nghi thức want/have/pack; chỉ khác lớp vận chuyển. Vì thế hiểu một lần là hiểu hết — và debug bằng GIT_TRACE_PACKET áp dụng cho mọi remote.
Ba ý mang về
- Push/fetch thương lượng để chỉ truyền object thiếu, không gửi cả repo: đo thật, thêm 1 commit chỉ đẩy 3 object (server 9 → 12); client khai
want <SHA đích>+have <SHA đã có>, server gửipackfilechênh lệch. GIT_TRACE_PACKET=1cho xem tận mắt giao thức:want/have/acknowledgments/ready/packfile— và protocol v2 (mặc định từ 2.26) tối ưu bước quảng bá ref cho repo nhiều ref.- Push bị từ chối (non-fast-forward) là tính năng an toàn: server có commit bạn chưa có →
fetch+ merge/rebase rồi push lại;--forcexóa việc người khác,--force-with-leasean toàn hơn.
Nguồn
- Pro Git — Git Internals: Transfer Protocols: https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
- Git Documentation — gitprotocol-v2 và gitprotocol-pack: https://git-scm.com/docs/protocol-v2
Đến đây sê-ri Git nội bộ khép lại 12 phần: từ object địa chỉ hoá nội dung, ba vùng, ref, DAG, merge và rebase, reflog, packfile, tới status/diff, bisect, cherry-pick/detached HEAD, và giao thức truyền. Điểm chung xuyên suốt: Git không phải phép màu — nó là vài cấu trúc dữ liệu đơn giản (object bất biến băm theo nội dung, con trỏ, packfile nén delta) ghép lại. Hiểu chúng thì mọi lệnh Git, kể cả lúc "hỏng", đều có thể lý giải và sửa được.