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:

  1. 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.
  2. 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".
  3. 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

Ảnh chụp đoạn mã shell nền tối giải thích fetch và push trao đổi gì với máy chủ, ba bước của một lần trao đổi bước một ref advertisement máy chủ khoe các ref cùng SHA nó đang có, bước hai thương lượng want have hai bên tìm ra object còn thiếu, bước ba pack protocol chỉ gói và gửi những object thiếu packfile, git push origin master đẩy object client có mà server thiếu git fetch origin kéo object server có mà client thiếu, thương lượng want muốn have đã có client nói muốn tới đâu và mình đã có những gì fetch want 744c6c4 tôi muốn commit này fetch have 457548b tôi đã có những cái này rồi fetch have 28ca147 server tính hiệu chỉ gửi phần thiếu không gửi lại cái đã có, vì sao push đôi khi bị từ chối non-fast-forward rejected master fetch first server đã có commit mà bạn chưa có push sẽ ghi đè mất lịch sử Git chặn cách đúng git fetch cộng merge hoặc rebase rồi push lại force ghi đè nguy hiểm force-with-lease an toàn hơn

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

Ảnh chụp bảng kết quả đo thật nền tối chạy git 2.39, push chỉ gửi object mới đếm trên server, push lần đầu 3 commit server rỗng new branch master master object trên server 9 gồm 3 commit 3 tree 3 blob, push lần 2 thêm 1 commit b3d07fc chấm chấm 457548b master master object trên server 12 cộng 3 object 1 commit 1 tree 1 blob, fetch thương lượng want have protocol v2 client2 có 457548b server có 744c6c4 chạy GIT_TRACE_PACKET git fetch origin fetch want 744c6c4853828049681bd0981868d5a3001bd571 fetch have 457548bf23bd537642972531a570dfc73e5e14d6 fetch have 28ca1477f56d383686602fc23512aa2eab6100be upload-pack acknowledgments ready packfile server gửi chỉ object thiếu, kết quả chỉ phần mới được kéo về 457548b chấm chấm 744c6c4 master origin master 744c6c4 c6 28ca147 c5 object đã có 457548b không truyền lại

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ề

  1. 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ửi packfile chênh lệch.
  2. GIT_TRACE_PACKET=1 cho 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.
  3. 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; --force xóa việc người khác, --force-with-lease an toàn hơn.

Nguồn

Đế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.