Ai code lâu năm cũng dính pha này: mở pull request lên, thấy cả đống commit kiểu "WIP", "fix typo", "actually fix it", "oops" - đọc vô chả hiểu mình đã làm cái gì. Interactive rebase chính là công cụ giúp dọn đống lộn xộn đó thành một câu chuyện gọn gàng, mạch lạc để reviewer đọc theo được.
Vì sao lịch sử commit sạch lại quan trọng
Lịch sử commit của một pull request không chỉ là log ghi lại thao tác - nó là tài liệu. Sau này (kể cả chính mình, 6 tháng sau) sẽ phải dùng git blame với git log để hiểu tại sao lại sửa chỗ này. Lịch sử lộn xộn thì việc "khảo cổ" đó cực kỳ khổ.
Lịch sử commit sạch là món quà mình tặng cho chính mình trong tương lai, với cả mấy anh em reviewer.
Lệnh cơ bản
git rebase -i HEAD~5
Lệnh này mở 5 commit gần nhất trong editor, mỗi dòng đầu ghi pick. Từ đây mình có thể đổi thứ tự dòng, hoặc đổi pick thành một trong mấy lệnh rebase khác.
Các lệnh rebase
| Lệnh | Viết tắt | Tác dụng |
|---|---|---|
| pick | p | Giữ nguyên commit |
| reword | r | Giữ nguyên thay đổi, sửa lại message |
| edit | e | Dừng lại để chỉnh sửa commit |
| squash | s | Gộp vô commit trước, gộp luôn message |
| fixup | f | Gộp vô commit trước, bỏ message |
| drop | d | Xóa hẳn commit |
Cơ chế hoạt động bên dưới
Rebase không phải "di chuyển" commit đâu - nó replay (chạy lại) từng commit. Git đi qua từng commit trên nhánh mình, áp từng cái như một patch lên trên base mới, rồi tạo ra commit object hoàn toàn mới (SHA mới luôn).
Ví dụ thực tế
Giả sử nhánh của mình có lịch sử vầy trước khi mở PR:
a1b2c3d WIP
e4f5g6h fix typo
h7i8j9k actually add the feature
k0l1m2n oops forgot a file
Chạy git rebase -i HEAD~4, rồi đổi plan lại thành:
pick h7i8j9k actually add the feature
fixup k0l1m2n oops forgot a file
fixup a1b2c3d WIP
fixup e4f5g6h fix typo
Lưu lại rồi thoát - Git sẽ gộp cả 4 commit thành một commit duy nhất, mô tả rõ ràng.
Sửa message commit giữa chừng rebase
Đánh dấu một commit là reword, sau khi lưu plan, Git sẽ dừng lại và mở editor riêng cho message của commit đó thôi.
git rebase -i HEAD~3
# đổi 'pick' thành 'reword' ở dòng muốn sửa tên
# Git mở editor - sửa message, lưu, đóng lại
# rebase tự động chạy tiếp
Tách hoặc sửa (amend) một commit
Đánh dấu commit là edit để rebase dừng lại ngay sau khi commit đó được áp dụng.
git rebase -i HEAD~3
# đánh dấu commit muốn sửa là 'edit'
# rebase dừng ở đây - mình sửa gì thì sửa
git add <file>
git commit --amend
# rồi tiếp tục
git rebase --continue
Xử lý conflict
Vì rebase replay từng commit một, conflict có thể xuất hiện ở bất kỳ bước nào.
# sửa mấy file bị conflict
git add <resolved-files>
git rebase --continue
# hoặc bỏ cuộc, quay về lúc bắt đầu
git rebase --abort
Push lịch sử đã rebase
Vì rebase viết lại SHA của commit, git push bình thường sẽ bị từ chối. Mình cần force push - nhưng nên xài kiểu an toàn hơn.
git push --force-with-lease
--force-with-lease sẽ từ chối ghi đè remote branch nếu có ai đó đã push lên sau lần fetch gần nhất của mình - bảo vệ đồng đội khỏi mất công sức, khác với --force thường cứ đè bừa.
Khi nào dùng lệnh nào
Dùng squash khi thay đổi của commit thuộc về commit trước đó nhưng message vẫn còn giá trị, đáng giữ lại. Dùng fixup cho mấy commit kiểu "oops" mà message chả có ý nghĩa gì thêm. Dùng reword khi code ổn rồi nhưng message viết sai hoặc quá cụt ngủn. Dùng drop cho commit hóa ra là ngõ cụt, kiểu thử nghiệm rồi bỏ.
Mấy cái cần tránh
Đừng bao giờ rebase mấy commit mà người khác đã pull về và build tiếp lên trên - làm vậy là viết lại lịch sử ngay dưới chân họ, tạo ra conflict đau đầu cho họ sau này. Rebase chỉ dùng để dọn dẹp nhánh local hoặc nhánh PR trước khi merge, không phải cho nhánh đã share và có người build lên rồi. Cũng nhớ kiểm tra kỹ plan rebase trước khi lưu - lỡ tay drop nhầm là mất luôn thay đổi của commit đó, dù git reflog thường vẫn cứu được nếu phát hiện kịp.
Interactive rebase viết lại lịch sử - chính vì vậy nó mạnh, và cũng chính vì vậy nên chỉ dùng trên nhánh mà chỉ mình mình (hoặc PR của mình) đang làm việc thôi.

