Tracks
“Commit sớm, commit thường xuyên” là một câu thần chú phổ biến trong phát triển phần mềm khi dùng Git. Làm vậy giúp mỗi thay đổi được ghi lại đầy đủ, tăng khả năng cộng tác và dễ theo dõi tiến trình dự án. Tuy nhiên, điều này cũng có thể dẫn đến quá nhiều commit.
Đây là lúc squash commit phát huy tác dụng. Squash commit là quá trình gộp nhiều commit thành một commit thống nhất.
Chẳng hạn, giả sử chúng ta làm một tính năng triển khai form đăng nhập và tạo ra bốn commit sau:

Khi tính năng hoàn tất, xét trên tổng thể dự án, các commit này quá chi tiết. Chúng ta không cần biết sau này rằng đã gặp một lỗi và sửa trong quá trình phát triển. Để giữ lịch sử sạch trên nhánh chính, chúng ta sẽ squash các commit này thành một commit duy nhất:

Cách squash commit trong Git: Interactive rebase
Cách phổ biến nhất để squash commit là dùng interactive rebase. Ta bắt đầu bằng lệnh:
git rebase -i HEAD~<number_of_commits>
Thay <number_of_commits> bằng số commit muốn squash.
Trong ví dụ này, chúng ta có bốn commit, nên lệnh là:
git rebase -i HEAD~4
Chạy lệnh này sẽ mở một trình soạn thảo dòng lệnh tương tác:

Phần trên hiển thị các commit, phần dưới là chú thích hướng dẫn cách squash commit.
Chúng ta thấy bốn commit. Với mỗi commit, cần quyết định chạy lệnh nào. Ta quan tâm đến lệnh pick (p) và squash (s). Để squash bốn commit này thành một, ta chọn (pick) commit đầu tiên và squash ba commit còn lại.
Ta áp dụng bằng cách sửa văn bản trước mỗi commit, cụ thể đổi pick thành s hoặc squash cho commit thứ hai, thứ ba và thứ tư. Để chỉnh sửa, ta cần vào chế độ “INSERT” trong trình soạn thảo dòng lệnh bằng cách nhấn phím i:

Sau khi nhấn i, dòng -- INSERT -- sẽ xuất hiện bên dưới cho biết đã vào chế độ chèn. Giờ bạn có thể di chuyển con trỏ bằng phím mũi tên, xóa ký tự và gõ như trong trình soạn thảo văn bản thông thường:

Khi đã hài lòng với thay đổi, ta thoát chế độ chèn bằng phím Esc. Bước tiếp theo là lưu và thoát trình soạn thảo. Để làm vậy, trước tiên nhấn phím : để báo với trình soạn thảo rằng ta muốn thực thi lệnh:

Ở cuối trình soạn thảo, giờ ta thấy dấu hai chấm : yêu cầu nhập lệnh. Để lưu thay đổi, dùng lệnh w (write). Để đóng trình soạn thảo, dùng q (quit). Có thể gộp hai lệnh thành wq:

Để thực thi lệnh, nhấn phím Enter. Thao tác này sẽ đóng trình soạn thảo hiện tại và mở một trình mới để nhập thông điệp commit cho commit đã squash. Trình soạn thảo sẽ hiển thị thông điệp mặc định ghép từ bốn thông điệp commit mà ta đang squash:

Tôi khuyến nghị chỉnh lại thông điệp để phản ánh chính xác các thay đổi do những commit này mang lại — suy cho cùng, mục đích của squash là giữ lịch sử gọn gàng và dễ đọc.
Để tương tác và chỉnh sửa thông điệp, nhấn i để vào chế độ chỉnh sửa rồi sửa theo ý bạn.

Trong trường hợp này, ta thay thông điệp commit bằng "Implement login form." Để thoát chế độ chỉnh sửa, nhấn Esc. Sau đó lưu thay đổi bằng cách nhấn :, nhập lệnh wq và nhấn Enter.
Cách xem lịch sử commit
Thông thường, khó có thể nhớ toàn bộ lịch sử commit. Để xem lịch sử, ta dùng lệnh git log. Trong ví dụ đã nêu, trước khi squash, chạy git log sẽ hiển thị:

Dùng phím mũi tên lên và xuống để di chuyển trong danh sách commit. Nhấn q để thoát.
Ta có thể dùng git log để xác nhận squash đã thành công. Chạy sau khi squash sẽ hiển thị một commit duy nhất với thông điệp mới:

Đẩy (push) commit đã squash
Các lệnh trên chỉ tác động lên kho local. Để cập nhật kho remote, ta cần push thay đổi. Tuy nhiên, vì đã thay đổi lịch sử commit, ta cần force push với tùy chọn --force:
git push --force origin feature/login-form
Force push sẽ ghi đè lịch sử commit trên nhánh remote và có thể làm gián đoạn công việc của người khác trên nhánh đó. Nên thông báo với đội ngũ trước khi thực hiện.
Một cách force push an toàn hơn, giảm rủi ro ảnh hưởng đến cộng tác viên, là dùng tùy chọn --force-with-lease:
git push --force-with-lease origin feature/login-form
Tùy chọn này đảm bảo ta chỉ force push nếu nhánh remote chưa được cập nhật kể từ lần fetch hoặc pull gần nhất.
Squash các commit cụ thể
Hãy hình dung ta có năm commit:

Giả sử ta muốn giữ commit 1, 2 và 5, và squash commit 3 và 4.

Khi dùng interactive rebase, các commit được đánh dấu squash sẽ được gộp vào commit đứng ngay trước nó. Trong trường hợp này, ta muốn squash Commit4 để nó nhập vào Commit3.
Để làm vậy, ta phải khởi tạo interactive rebase bao gồm hai commit này. Ở đây, ba commit là đủ, nên dùng lệnh:
git rebase -i HEAD~3
Sau đó đặt Commit4 thành s để nó được squash với Commit3:

Sau khi thực hiện và liệt kê lại các commit, ta thấy commit 3 và 4 đã được squash với nhau trong khi các commit khác giữ nguyên.

Squash bắt đầu từ một commit cụ thể
Trong lệnh git rebase -i HEAD~3, phần HEAD là viết tắt cho commit mới nhất. Cú pháp ~3 dùng để chỉ tổ tiên của một commit. Ví dụ, HEAD~1 là cha của commit HEAD.

Trong một interactive rebase, các commit được xét bao gồm mọi commit tổ tiên dẫn tới commit được chỉ định trong lệnh. Lưu ý commit được chỉ định không được bao gồm:

Thay vì dùng HEAD, ta có thể chỉ định trực tiếp hash của commit. Ví dụ, Commit2 có hash dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf, vậy lệnh:
git rebase -i dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf
sẽ bắt đầu rebase xét mọi commit sau Commit2. Do đó, nếu muốn bắt đầu rebase tại một commit cụ thể và bao gồm cả commit đó, ta dùng lệnh:
git rebase -i <commit-hash>~1
Giải quyết xung đột khi squash commit
Khi squash commit, ta gộp nhiều thay đổi thành một commit, điều này có thể dẫn tới xung đột nếu các thay đổi chồng lấn hoặc khác biệt đáng kể. Dưới đây là một số tình huống xung đột thường gặp:
- Thay đổi chồng lấn: Nếu hai hoặc nhiều commit được squash đã sửa cùng dòng hoặc các dòng liên quan chặt chẽ trong một tệp, Git có thể không tự động dung hòa được.
- Trạng thái thay đổi khác nhau: Nếu một commit thêm một đoạn mã và commit khác sửa hoặc xóa chính đoạn mã đó, việc squash có thể gây xung đột cần giải quyết.
- Đổi tên và chỉnh sửa: Nếu một commit đổi tên tệp và các commit sau đó sửa theo tên cũ, squash có thể khiến Git nhầm lẫn và gây xung đột.
- Thay đổi tệp nhị phân: Tệp nhị phân không trộn (merge) tốt bằng công cụ diff văn bản. Nếu nhiều commit thay đổi cùng một tệp nhị phân và ta cố squash chúng, có thể phát sinh xung đột vì Git không thể tự động hòa giải.
- Lịch sử phức tạp: Nếu các commit có lịch sử phức tạp với nhiều lần merge, tạo nhánh hoặc rebase, việc squash có thể gây xung đột do tính phi tuyến của các thay đổi.
Khi squash, Git sẽ cố gắng áp dụng từng thay đổi một. Nếu gặp xung đột, nó sẽ tạm dừng và cho phép chúng ta giải quyết.
Xung đột sẽ được đánh dấu bằng các mốc <<<<<< và >>>>>>. Để xử lý, ta mở các tệp và thủ công chọn phần mã cần giữ lại.
Sau khi giải quyết xung đột, ta cần stage các tệp đã xử lý bằng git add. Sau đó tiếp tục rebase bằng lệnh sau:
git rebase --continue
Để tìm hiểu thêm về xung đột trong Git, hãy xem hướng dẫn cách giải quyết xung đột khi merge trong Git.
Các cách thay thế squash bằng rebase
Lệnh git merge --squash là một phương án thay thế cho git rebase -i để gộp nhiều commit thành một. Lệnh này đặc biệt hữu ích khi muốn hợp nhất thay đổi từ một nhánh vào nhánh chính đồng thời squash tất cả commit lẻ thành một. Sau đây là tổng quan cách squash bằng git merge:
- Chuyển sang nhánh đích nơi ta muốn tích hợp các thay đổi.
- Chạy lệnh
git merge --squash <branch-name>thay<branch-namebằng tên nhánh. - Commit các thay đổi bằng
git commitđể tạo một commit duy nhất đại diện cho toàn bộ thay đổi từ nhánh tính năng.
Ví dụ, giả sử ta muốn tích hợp thay đổi của nhánh feature/login-form vào main dưới dạng một commit duy nhất:
git checkout main
git merge --squash feature-branch
git commit -m "Implement login form"
Những hạn chế của cách tiếp cận này so với git rebase -i gồm:
- Mức độ kiểm soát chi tiết: Ít kiểm soát hơn với từng commit. Với rebase, ta có thể chọn commit để gộp, trong khi merge buộc gộp tất cả thay đổi thành một commit.
- Lịch sử trung gian: Khi dùng merge, lịch sử commit chi tiết từ nhánh tính năng bị mất trên nhánh chính. Điều này có thể khiến việc lần theo các thay đổi tăng dần khó khăn hơn.
- Rà soát trước khi commit: Vì toàn bộ thay đổi được stage như một tập thay đổi duy nhất, ta không thể rà soát hoặc kiểm thử từng commit trước khi squash, khác với interactive rebase cho phép xem xét và kiểm thử tuần tự từng commit.
Kết luận
Tích hợp các commit nhỏ và thường xuyên vào quy trình phát triển thúc đẩy cộng tác và ghi chép rõ ràng, nhưng cũng có thể làm lộn xộn lịch sử dự án. Squash commit tạo ra sự cân bằng, giữ lại các mốc quan trọng đồng thời loại bỏ “nhiễu” từ những thay đổi lặp nhỏ.
Để tìm hiểu thêm về Git, tôi khuyến nghị các tài nguyên sau:
