Hôm đó mình đang sửa feature thanh toán, khoảng 8 file đang thay đổi dở. Sếp nhắn “fix bug login gấp, khách đang không vào được”. Mình git stash, nhảy sang nhánh main, fix xong, quay lại — rồi nhận ra trong đống stash đó có cả file config test local và vài console.log debug tạm không liên quan gì đến payment hết. Áp lại thì conflict ngay chỗ không đáng có.
git stash cơ bản thì ai cũng biết dùng. Nhưng sau 6 tháng dùng thực tế trong team, mình nhận ra 3 kỹ thuật mà không biết thì rất dễ gặp rắc rối: stash từng phần với -p, đặt tên stash để không nhầm, và cứu stash sau khi lỡ drop mất.
Git lưu stash như thế nào — không phải “giấu” đi đâu cả
Nắm được cơ chế này mới giải thích được tại sao có thể cứu stash sau khi drop — và tại sao phải nhanh tay. Git không “giấu” thay đổi đi đâu hết — mỗi lần git stash, Git tạo 2–3 commit ẩn trong refs/stash:
- Một commit cho staged changes
- Một commit cho unstaged changes
- Một commit cho untracked files (nếu dùng
-u)
# Xem danh sách stash
git stash list
# stash@{0}: WIP on feature/payment: a3f2b1c Add payment form
# stash@{1}: WIP on main: 9d8c4e5 Fix login bug
# Xem nội dung thay đổi của stash trước khi apply
git stash show -p stash@{0}
Commit mồ côi này nằm trong object database của Git cho đến khi garbage collection chạy. Đó là lý do có thể cứu được — và cũng là lý do phải hành động sớm.
git stash -p — Stash từng phần thay đổi
Chính cái conflict oan kể trên khiến mình đi tìm -p. Flag này (viết tắt của --patch) cho phép chọn từng “hunk” — đoạn thay đổi nhỏ — muốn stash hay giữ lại trong working tree.
git stash -p
Git hiển thị từng hunk và hỏi:
diff --git a/src/payment.js b/src/payment.js
@@ -45,7 +45,12 @@ function validateCard(card) {
+ if (!card.cvv) {
+ throw new Error('CVV required');
+ }
Stash this hunk [y,n,q,a,d,s,?]?
Các lựa chọn hay dùng nhất:
y— stash hunk nàyn— bỏ qua, giữ lại trong working trees— tách nhỏ hunk hơn nữa (rất hữu ích khi hunk lớn)q— thoát, áp dụng những gì đã chọn?— xem full help
Ví dụ thực tế: mình đang vừa code feature payment, vừa thêm vài dòng debug console.log. Với git stash -p, mình stash riêng phần debug, giữ lại phần feature để tiếp tục — không cần stash toàn bộ.
Đặt tên stash và stash file cụ thể
Đặt tên để không nhầm lẫn
Sau vài tuần dùng stash không tên, list của mình trông thế này:
stash@{0}: WIP on feature/payment: a3f2b1c ...
stash@{1}: WIP on feature/payment: a3f2b1c ...
stash@{2}: WIP on feature/payment: a3f2b1c ...
Không biết cái nào là cái nào. Từ lúc dùng -m:
git stash push -m "payment: validate CVV logic"
git stash push -m "debug: temp console.log for card parser"
Rõ hơn hẳn:
stash@{0}: On feature/payment: debug: temp console.log for card parser
stash@{1}: On feature/payment: payment: validate CVV logic
Stash file cụ thể
Nếu chỉ muốn stash 1–2 file thay vì đi qua từng hunk với -p:
git stash push -m "only payment validator" -- src/payment.js src/validators/card.js
Include untracked files
Mặc định git stash bỏ qua file mới tạo (untracked). Thêm -u để lấy luôn:
git stash push -u -m "include new files too"
Khôi phục stash đã drop bằng git fsck
Lần đó mình git stash drop nhầm, trong stash là gần 2 tiếng code — tưởng mất trắng. Hóa ra không. Đó cũng là lần mình học được cái này.
Stash là commit ẩn. Git chưa chạy garbage collection, nên dangling commit vẫn còn trong object database:
# Tìm tất cả commit "mồ côi" (không có ref nào trỏ vào)
git fsck --lost-found | grep commit
Output:
dangling commit a1b2c3d4e5f67890...
dangling commit f9e8d7c6b5a43210...
Xem từng commit để tìm đúng cái cần:
git show a1b2c3d4e5f67890
Tìm được rồi, apply vào:
git stash apply a1b2c3d4e5f67890
Lưu ý quan trọng: Window thời gian để cứu không vô hạn. Git chạy garbage collection định kỳ — mặc định sau 2 tuần hoặc khi số loose objects vượt ~6.700 — sau đó commit mồ côi biến mất hẳn. Mình cứu được vì phát hiện trong vài giờ.
Mẹo phòng ngừa: trước khi drop, lưu hash lại:
# Lấy hash của stash trước khi drop
git rev-parse stash@{0}
# a1b2c3d4e5f67890... ← lưu lại
git stash drop stash@{0}
# Nếu hối hận trong vài giờ tới:
git stash apply a1b2c3d4e5f67890
Workflow thực tế trong team
Team 8 người, trước khi áp quy tắc này mình đếm được 3–5 conflict “vô lý” mỗi tuần — loại conflict sinh ra từ stash nhầm hoặc apply sai chỗ. Sau khi thống nhất: luôn đặt tên stash, dùng -p khi thay đổi hỗn hợp, copy hash trước khi drop — con số đó giảm xuống còn 0–1.
Có 2 pattern mình dùng thường xuyên nhất:
Pattern 1: Stash + tạo branch nhanh để thử nghiệm
# Đang giữa feature, muốn test idea nhanh
git stash push -m "feature/payment in progress"
git checkout -b experiment/try-new-approach
# Thử không ổn, quay lại
git checkout feature/payment
git stash pop
Pattern 2: Di chuyển thay đổi sang branch đúng
# Lỡ code trên main thay vì branch riêng
git stash push -m "oops, nên ở trên feature branch"
git checkout -b feature/new-feature
git stash pop
Cái này mình dùng ít nhất 1 lần/tuần.
Một số lệnh tham khảo thêm
# Apply stash nhưng GIỮ trong list (không xóa như pop)
git stash apply stash@{1}
# Tạo branch mới từ stash luôn
git stash branch new-branch-name stash@{0}
# Xóa toàn bộ stash list
git stash clear
Kết
Mình dùng Git hơn 1 năm trước khi biết -p tồn tại. Documentation có ghi, nhưng tutorial cơ bản không nhắc tới — nên phần lớn người dùng qua mặt luôn. git fsck thì mình tình cờ đọc được trong một thread Stack Overflow lúc đang hoảng loạn tìm cách cứu stash drop nhầm.
Dùng thử git stash -p đi — sau khi quen rồi sẽ thấy khó chịu khi phải stash toàn bộ file chỉ vì có vài dòng debug không muốn commit.
