Hai khái niệm hay bị hiểu lơ mơ và thường đi cùng nhau: git cherry-pick để bê một commit lẻ sang nhánh khác, và trạng thái detached HEAD mà git bisect (bài trước) hay git checkout <sha> đẩy bạn vào. Cả hai đều sáng tỏ ngay khi bạn nhớ mô hình từ đầu sê-ri: commit là bất biến, băm theo nội dung và parent; còn HEAD chỉ là một con trỏ. Bài này đo thật cả hai trong Git 2.39.

cherry-pick: chép riêng một commit

Tình huống: nhánh tinh-nang có ba commit, nhưng một trong số đó là một hotfix khẩn cấp cần lên main ngay — không thể chờ merge cả nhánh (hai commit kia chưa xong). git cherry-pick <sha> lấy đúng commit đó, áp diff của nó lên nhánh hiện tại, và tạo một commit mới:

git switch main
git cherry-pick 5aa73b3      # commit hotfix trên tinh-nang

Vì commit mới có parent khác (đỉnh của main thay vì đỉnh của tinh-nang), nó có SHA khác — giống hệt cơ chế của rebase (bài git-06): cùng nội dung, khác danh tính. Đây là cherry-pick "chép" chứ không "di chuyển".

Ảnh chụp đoạn mã shell nền tối giải thích cherry-pick bê một commit lẻ và detached HEAD đứng thẳng trên commit, cherry-pick chép riêng một commit sang nhánh khác nhánh tinh-nang có 3 commit nhưng chỉ 1 cái là hotfix khẩn không muốn merge cả nhánh chỉ muốn commit hotfix đó git switch main rồi git cherry-pick 5aa73b3 tạo commit mới trên main cùng diff SHA mới parent khác nội dung y hệt commit gốc, detached HEAD checkout thẳng một commit không qua nhánh git checkout f35e437 HEAD trỏ thẳng vào commit không vào nhánh bình thường HEAD trỏ nhánh trỏ commit detached HEAD trỏ thẳng commit nên commit tạo ở đây không thuộc nhánh nào dễ mồ côi, giữ commit tạo trong detached HEAD rời đi mà không đặt tên Git cảnh báo leaving N commit behind git branch giu-lai 113ddcf đặt tên nhánh cho commit mồ côi hoặc git switch -c ten-nhanh tạo nhánh ngay tại chỗ detached không đáng sợ chỉ là HEAD không gắn nhãn nhánh

Hình 1: cherry-pick áp diff một commit lên nhánh hiện tại (tạo SHA mới). detached HEAD là khi HEAD trỏ thẳng vào commit thay vì qua một nhánh — commit tạo ở đó dễ mồ côi nếu không đặt tên.

Đo thật: main ban đầu chỉ có f.txt. Sau git cherry-pick 5aa73b3, main có thêm fix.txt — nhưng không có g.txt (file của các commit tính năng kia). Đúng như mong muốn: chỉ một commit được bê sang.

Ảnh chụp bảng kết quả đo thật nền tối chạy git 2.39, cherry-pick commit hotfix từ tinh-nang sang main, trước main chỉ có f.txt chạy git cherry-pick 5aa73b3 ra master aea78f7 fix va loi khan cap 1 file changed 1 insertion create mode fix.txt sau main có f.txt và fix.txt không có g.txt feat, SHA cũ tinh-nang 5aa73b3 SHA mới main aea78f7 commit mới cùng nội dung parent khác, detached HEAD git checkout f35e437 You are in detached HEAD state git status HEAD detached at f35e437 tạo commit ở đây 113ddcf thu nghiem trong detached, rời đi cảnh báo mất commit đặt tên cứu Warning you are leaving 1 commit behind not connected to any of your branches 113ddcf thu nghiem trong detached rồi git branch giu-lai 113ddcf commit giờ an toàn trên nhánh 113ddcf thu nghiem trong detached f35e437 main A

Hình 2: cherry-pick tạo commit aea78f7 trên main từ commit gốc 5aa73b3 — SHA khác, chỉ mang fix.txt. Bên dưới: detached HEAD tại f35e437, commit 113ddcf tạo trong đó bị cảnh báo "leaving 1 commit behind", và được cứu bằng git branch.

detached HEAD: HEAD trỏ thẳng vào commit

Bình thường chuỗi con trỏ là HEAD → nhánh → commit (bài git-03). Khi bạn git checkout <sha> (hay checkout một tag, hay ở giữa git bisect), Git đặt HEAD trỏ thẳng vào commit, bỏ qua khâu nhánh: HEAD → commit. Đó là "detached HEAD" — HEAD "rời" khỏi nhánh. Git in hẳn một đoạn giải thích và git status xác nhận HEAD detached at f35e437.

Ở trạng thái này bạn vẫn xem, sửa, commit được bình thường. Vấn đề duy nhất: commit bạn tạo không thuộc nhánh nào. Đo thật — tạo commit 113ddcf trong detached HEAD, rồi git switch sang nhánh khác. Git cảnh báo rõ ràng:

Warning: you are leaving 1 commit behind, not connected to
any of your branches:
  113ddcf thu nghiem trong detached

Commit 113ddcf giờ mồ côi. Nó chưa mất (còn trong reflog và kho, bài git-07), nhưng nếu bỏ qua cảnh báo này và quên SHA, tới lần gc là mất thật. Cách giữ lại đơn giản:

git branch giu-lai 113ddcf    # đặt tên nhánh cho commit -> an toàn
# hoặc ngay khi còn trong detached: git switch -c ten-nhanh-moi

Sau git branch giu-lai 113ddcf, commit nằm trên một nhánh có tên, hết mồ côi. detached HEAD không đáng sợ — nó chỉ là HEAD không gắn nhãn nhánh. Biết điều đó thì git bisect, xem lại code ở một tag cũ, hay thử nghiệm nhanh trên một commit đều thành chuyện thường.

Khi nào dùng cherry-pick — và khi nào đừng

Hợp lý: bê hotfix từ nhánh phát triển sang nhánh release đang bảo trì; lấy một commit hay từ PR của người khác mà chưa muốn merge cả nhánh; khôi phục một commit lẻ đã bị mất nhánh.

Nên tránh: cherry-pick hàng loạt commit qua lại giữa các nhánh dài hạn. Vì mỗi cherry-pick tạo commit mới với cùng nội dung, bạn sẽ có hai commit khác SHA nhưng cùng diff trên hai nhánh. Khi hai nhánh đó merge sau này, Git thường nhận ra (nhờ patch-id) và không nhân đôi, nhưng không phải lúc nào cũng vậy — dễ ra xung đột hoặc thay đổi lặp. Với việc đồng bộ nhiều commit giữa hai nhánh, merge hoặc rebase thường sạch hơn.

Đánh đổi và lưu ý

cherry-pick có thể xung đột. Diff của commit gốc dựa trên ngữ cảnh của nó; áp lên một nhánh đã khác đi có thể không khớp. Khi đó cherry-pick dừng như merge, bạn giải xung đột rồi git cherry-pick --continue (hoặc --abort).

-x để ghi nguồn. git cherry-pick -x <sha> thêm dòng (cherry picked from commit <sha>) vào message — rất đáng dùng khi bê giữa các nhánh release để sau này truy được commit gốc.

Đừng để công việc dài trong detached HEAD. Detached tốt cho xem và thử nghiệm ngắn. Nếu bắt đầu commit nhiều, tạo nhánh ngay (git switch -c) để khỏi phụ thuộc vào việc nhớ SHA và cảnh báo của Git.

Ba ý mang về

  1. cherry-pick bê một commit lẻ sang nhánh hiện tại bằng cách áp diff của nó và tạo commit mới — đo thật, 5aa73b3 thành aea78f7 (SHA khác, cùng nội dung), main chỉ nhận fix.txt chứ không kéo theo các commit khác của nhánh nguồn.
  2. detached HEAD là khi HEAD trỏ thẳng vào commit (do checkout <sha>, tag, hay bisect) thay vì qua nhánh — bạn vẫn commit được, nhưng commit đó không thuộc nhánh nào; Git cảnh báo "leaving N commit behind" khi bạn rời đi.
  3. Cứu commit trong detached HEAD bằng cách đặt tên: git branch <tên> <sha> (hoặc git switch -c ngay tại chỗ) — detached không đáng sợ, chỉ là HEAD chưa gắn nhãn nhánh; và cherry-pick nhiều nên dè chừng commit trùng diff khác SHA.

Nguồn

Phần sau — bài cuối sê-ri — ta ra khỏi kho cục bộ: git fetch và git push thật sự trao đổi gì với máy chủ, pack protocol thương lượng ra sao để chỉ gửi những object còn thiếu, và vì sao đôi khi push bị từ chối.