Làm sạch lịch sử Commit với git rebase -i: Squash, sửa và xóa commit thừa trước Pull Request
Hướng dẫn git rebase -i an toàn: chọn phạm vi commit, squash, fixup, reword, edit, drop, xử lý conflict, khôi phục bằng reflog và push bằng force-with-lease.
ShanverseCập nhật 12 phút đọc— lượt xemĐang tải lượt xem
Git
Developer Tools
Trong quá trình phát triển, một feature branch thường có lịch sử như:
Các commit này hữu ích khi đang làm việc, nhưng không phải lúc nào cũng hữu ích cho reviewer hoặc lịch sử lâu dài của dự án. Interactive Rebase giúp biến chúng thành một chuỗi quyết định rõ ràng hơn trước khi mở Pull Request.
Interactive Rebase là gì?
Rebase thông thường phát lại commit lên một base mới. Interactive Rebase thêm một “kế hoạch” để bạn quyết định cách xử lý từng commit.
Git mở editor với danh sách commit từ cũ đến mới:
Bạn thay
Bình luận
pick
bằng action phù hợp, lưu file và đóng editor. Git thực hiện kế hoạch theo thứ tự.
Khi nào nên dùng?
Trước khi mở Pull Request.
Trước khi merge một feature branch cá nhân.
Khi branch có nhiều commit “fix”, “wip” hoặc “typo”.
Khi commit message chưa rõ.
Khi một commit đang chứa nhiều mục đích và cần tách lại.
Khi muốn sắp xếp lịch sử thành các bước logic.
Không nên tự ý dùng trên:
main, release hoặc branch được bảo vệ.
Branch nhiều người đang cùng phát triển.
Commit đã được người khác checkout và phát triển tiếp.
Lịch sử public mà team không cho phép rewrite.
Bước 0 — Chuẩn bị đường lui
Đảm bảo working tree sạch
Nếu có thay đổi dở dang, commit vào branch phù hợp hoặc stash:
Cập nhật remote refs
Tạo backup branch
Backup branch chỉ là một con trỏ nhẹ và giúp bạn so sánh hoặc phục hồi nhanh.
Xem graph trước khi sửa
Chọn đúng phạm vi commit
Chỉnh N commit gần nhất
Lệnh này đưa năm commit gần nhất vào kế hoạch.
Cẩn thận: nếu nhập sai số, bạn có thể bỏ sót commit cần sửa hoặc đưa thêm lịch sử không mong muốn.
Chỉnh mọi commit của feature branch so với main
Cách này thường phù hợp trước Pull Request khi feature branch bắt đầu từ main và không có cấu trúc merge phức tạp.
Xem trước commit sẽ bị ảnh hưởng:
Tìm base chính xác:
Các action quan trọng
Bạn cũng có thể đổi thứ tự các dòng để sắp xếp lại commit. Việc này có thể tạo conflict nếu commit phụ thuộc lẫn nhau.
1. Sửa commit message với reword
Kế hoạch ban đầu:
Đổi thành:
Git dừng ở từng commit để bạn nhập message mới:
reword chỉ đổi message, không thay nội dung snapshot.
2. Gộp commit bằng squash
Giả sử:
Muốn gộp cả ba:
Git mở editor để tạo message cho commit kết quả:
Quy tắc: squash luôn gộp commit hiện tại vào commit đứng ngay trước nó. Dòng đầu tiên trong nhóm phải là pick hoặc action giữ commit.
3. Dùng fixup khi không cần giữ message phụ
Kết quả giữ message của commit a1b2c3d. fixup phù hợp với commit chỉ sửa lỗi nhỏ cho một commit trước đó.
Tạo fixup commit từ đầu
Khi đang phát triển:
Sau đó:
Git tự di chuyển và đánh dấu các commit fixup! cạnh commit mục tiêu.
Có thể bật autosquash mặc định:
Chỉ bật sau khi hiểu workflow vì rebase vẫn rewrite lịch sử.
4. Xóa commit bằng drop
Commit b2c3d4e không xuất hiện trong lịch sử mới.
Bạn cũng có thể xóa dòng, nhưng drop thể hiện ý định rõ hơn.
5. Sửa nội dung commit với edit
Kế hoạch:
Git dừng tại commit được đánh dấu edit.
Sửa file rồi stage:
Amend commit:
Tiếp tục:
6. Chia một commit thành nhiều commit
Đánh dấu commit bằng edit. Khi Git dừng:
Thay đổi của commit quay về working tree mà không mất nội dung. Sau đó stage theo từng nhóm:
Kiểm tra git status và git diff sau mỗi bước để không bỏ sót file.
7. Sắp xếp lại commit
Trong todo list, đổi vị trí các dòng:
Thành:
Nếu commit a1b2c3d cần helper chỉ xuất hiện trong b2c3d4e, thứ tự mới hợp lý. Nếu đảo ngược dependency, commit có thể conflict hoặc không build.
Lịch sử đẹp nên không chỉ đọc đẹp; mỗi commit cần có ý nghĩa kỹ thuật hợp lý.
8. Chạy test trong quá trình rebase bằng exec
Thêm dòng:
Hoặc dùng option:
Git dừng nếu lệnh thất bại, giúp phát hiện commit nào làm hỏng test.
Sau khi sửa:
Xử lý conflict trong Interactive Rebase
Khi Git phát lại commit và gặp conflict:
Sửa nội dung và xóa marker:
Stage file:
Tiếp tục:
Bỏ qua commit hiện tại nếu chắc chắn không cần:
Hủy toàn bộ rebase:
Rebase dừng vì commit rỗng
Một commit có thể trở thành rỗng vì thay đổi đã tồn tại trên base hoặc được commit khác áp dụng.
Đọc trạng thái:
Nếu commit không còn cần thiết:
Nếu muốn giữ commit rỗng có chủ đích, hành vi phụ thuộc phiên bản Git và kế hoạch rebase. Hầu hết feature workflow không cần giữ commit rỗng.
Sửa todo list khi rebase đang chạy
Nếu muốn thay đổi phần kế hoạch chưa thực hiện:
Bạn có thể đổi action hoặc thứ tự của các commit còn lại.
Khôi phục khi rebase sai
Hủy khi rebase còn đang chạy
Dùng backup branch
Nếu muốn quay branch về backup và chắc chắn working tree không có dữ liệu cần giữ:
Đây là lệnh phá hủy thay đổi chưa commit; chỉ dùng sau khi kiểm tra.
Dùng reflog
Reflog ghi lại các vị trí gần đây của HEAD:
Tìm entry trước rebase, kiểm tra:
Tạo branch phục hồi trước khi reset:
Có thể dùng ORIG_HEAD nếu vẫn trỏ đúng trạng thái trước thao tác:
Đừng reset trước khi xác minh reference.
Push sau Interactive Rebase
Nếu branch chưa từng push:
Nếu branch đã push, hash remote khác lịch sử local mới. Trước tiên:
Sau đó, nếu branch là của bạn và team cho phép rewrite:
Không dùng --force theo thói quen. --force-with-lease giảm nguy cơ ghi đè commit remote mà bạn chưa biết.
Nếu branch có người khác cùng làm, phối hợp trước hoặc tránh rebase lịch sử chung.
Quy trình làm sạch branch trước Pull Request
Lưu ý: git diff backup..HEAD có thể trống nếu nội dung cuối không đổi dù commit hash và cấu trúc lịch sử đã thay đổi. Đây thường là kết quả mong muốn khi chỉ làm sạch lịch sử.
Ví dụ từ lịch sử lộn xộn đến lịch sử rõ ràng
Trước:
Kế hoạch:
Có thể sắp xếp lại để kết quả thành:
Hai commit mới cần đại diện cho hai bước có ý nghĩa, không chỉ ít commit hơn.
Có nên squash tất cả thành một commit?
Không phải lúc nào cũng nên.
Nên squash khi:
Commit sau chỉ sửa typo hoặc lỗi của commit trước.
Các commit không có giá trị độc lập.
Pull Request nhỏ, một thay đổi logic duy nhất.
Nên giữ nhiều commit khi:
Mỗi commit là một bước review được.
Refactor cần tách khỏi thay đổi hành vi.
Test cần thể hiện một bước rõ ràng.
Commit có thể revert riêng.
Mỗi commit build và có message tốt.
Mục tiêu là lịch sử có ý nghĩa, không phải số commit nhỏ nhất.
Interactive Rebase với merge commit
Mặc định, rebase có thể làm phẳng hoặc bỏ topology merge trong phạm vi. Với lịch sử có merge commit và khi thực sự cần giữ cấu trúc:
Đây là workflow nâng cao. Kế hoạch có thêm label, reset, merge và dễ gây nhầm. Với branch feature thông thường, nên giữ lịch sử đơn giản hoặc tránh rewrite merge phức tạp.
Những lỗi thường gặp
invalid upstream
Base không tồn tại hoặc remote refs chưa cập nhật:
Dùng đúng tên như origin/main.
Không thấy commit cần sửa
Bạn chọn base quá mới hoặc HEAD~N quá nhỏ. Hủy nếu cần và xem:
cannot squash without a previous commit
Dòng đầu tiên không thể là squash hoặc fixup. Commit cần gộp phải đứng sau commit mục tiêu.
Editor mở nhưng không biết lưu
Git dùng editor được cấu hình. Có thể thiết lập editor quen thuộc, ví dụ VS Code:
Đảm bảo lệnh editor hoạt động trong terminal trước.
Push bị từ chối non-fast-forward
Đây là hành vi mong đợi sau rewrite. Chỉ khi branch cá nhân và đã kiểm tra remote:
Conflict lặp lại
Rebase phát lại từng commit, nên cùng vùng có thể conflict nhiều lần. Giải quyết đúng từng commit hoặc abort và cân nhắc squash/reorder khác.
Checklist trước Pull Request
Working tree sạch.
Remote refs đã được fetch.
Phạm vi commit đã được kiểm tra.
Có backup branch hoặc reflog recovery plan.
Không rewrite branch dùng chung.
Commit typo/fixup đã được gộp hợp lý.
Commit message mô tả mục đích.
Mỗi commit còn lại có ý nghĩa review hoặc revert.
Không vô tình drop thay đổi cần thiết.
Lint, test và build thành công.
Diff cuối cùng giữ đúng nội dung mong muốn.
Dùng --force-with-lease nếu branch đã push.
Tổng kết
Các action cần nhớ:
Quy trình an toàn:
Mục lục(41)
Codeplain text
1feat: add checkout form2fix typo3fix test4try another solution5revert debug6final fix
Hướng dẫn Git Aliases và .gitconfig thực chiến: alias an toàn, log đẹp, cấu hình pull/push/rebase, includeIf cho tài khoản work/personal, bảo mật và chia sẻ dotfiles.
Phân tích Gitflow vs GitHub Flow vs Trunk-Based Development: cấu trúc branch, ưu nhược điểm, điều kiện áp dụng, ma trận quyết định và lộ trình chuyển đổi cho từng loại dự án.
Hướng dẫn git worktree từ cơ bản đến thực tế: tạo nhiều working tree, làm feature và hotfix song song, review PR, quản lý list/remove/prune/lock/repair và tránh lỗi phổ biến.