Git cơ bản: commit, branch và merge
1. Tổng quan
Git là công cụ quản lý phiên bản mà hầu như dự án nào cũng dùng, nhưng nhiều bạn mới chỉ nhớ vài lệnh mà không hiểu bản chất. Bài này đi qua ba khái niệm cốt lõi: commit (lưu lại một snapshot), branch (nhánh làm việc độc lập) và merge (gộp các nhánh lại). Đọc xong, bạn sẽ tự tin tạo nhánh mới, commit đúng cách, xử lý conflict khi merge và biết khi nào nên dùng merge thay vì rebase.
2. Commit – lưu lại một snapshot
2.1. Commit là gì
Mỗi commit là một “ảnh chụp” toàn bộ trạng thái file tại một thời điểm, kèm theo con trỏ về commit cha. Git không lưu diff, mà lưu snapshot — điều này giúp việc branch/merge nhanh và rẻ.
# Khởi tạo repo và thực hiện commit đầu tiên
git init order-service
cd order-service
echo "# Order Service" > README.md
git add README.md
git commit -m "docs: thêm README cho Order Service"
Giải thích:
git inittạo repo mới, sinh thư mục ẩn.git.git add README.mdđưa file vào staging area (vùng đệm chờ commit).git commit -m "..."tạo snapshot, gắn message mô tả thay đổi.
Kiểm tra lịch sử:
git log --oneline
a1b2c3d docs: thêm README cho Order Service
2.2. Viết commit message tốt
Một commit nên trả lời được câu hỏi “thay đổi này làm gì, tại sao”. Quy ước phổ biến (conventional commits):
feat: thêm API tạo đơn hàng
fix: sửa lỗi tính sai tổng tiền khi áp dụng coupon
refactor: tách OrderValidator ra khỏi OrderService
Lưu ý: Tránh commit message mơ hồ như
"update"hay"fix bug". Vài tháng sau nhìn lạigit log, bạn sẽ không biết commit đó sửa gì.
2.3. Commit từng phần với git add -p
Khi một file có nhiều thay đổi không liên quan, đừng commit tất cả cùng lúc.
git add -p src/main/java/com/nonladev/OrderService.java
Git sẽ hỏi bạn muốn stage từng “hunk” (đoạn thay đổi) nào, cho phép tách thành các commit nhỏ, dễ review và dễ revert nếu cần.
3. Branch – làm việc độc lập trên nhiều nhánh
3.1. Tạo và chuyển branch
Branch chỉ là một con trỏ nhẹ chỉ vào một commit cụ thể. Tạo branch mới không copy toàn bộ code, chỉ tạo thêm một con trỏ.
# Tạo branch mới và chuyển sang nó
git branch feature/add-payment
git checkout feature/add-payment
# Hoặc gộp hai lệnh trên thành một (Git ≥ 2.23)
git switch -c feature/add-payment
Giải thích:
git branch <tên>tạo branch nhưng vẫn ở branch cũ.git checkout <tên>hoặcgit switch <tên>di chuyểnHEADsang branch đó.-ctronggit switchlà shortcut để tạo-và-chuyển cùng lúc.
3.2. Xem danh sách branch
git branch -v
* feature/add-payment d4e5f6a thêm PaymentService skeleton
main a1b2c3d docs: thêm README cho Order Service
Dấu * chỉ branch hiện tại (HEAD đang đứng ở đó).
3.3. Đặt tên branch có quy ước
Đặt tên branch rõ ràng giúp cả team biết nhánh đó làm gì:
feature/add-payment # thêm chức năng mới
fix/order-total-wrong # sửa lỗi
refactor/extract-validator
hotfix/prod-null-pointer
3.4. Xóa branch sau khi hoàn thành
# Xóa local branch đã merge
git branch -d feature/add-payment
# Xóa branch chưa merge (ép xóa, cẩn thận mất commit)
git branch -D feature/add-payment
-d (lowercase) an toàn hơn vì Git sẽ cảnh báo nếu branch chưa được merge vào đâu cả.
4. Merge – gộp nhánh lại
4.1. Fast-forward merge
Nếu main không có commit mới nào kể từ khi bạn tách nhánh, Git chỉ cần di chuyển con trỏ main tới commit cuối của nhánh feature — gọi là fast-forward.
git switch main
git merge feature/add-payment
Updating a1b2c3d..d4e5f6a
Fast-forward
PaymentService.java | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
Không có commit merge mới được tạo, lịch sử vẫn là một đường thẳng.
4.2. Merge commit (3-way merge)
Nếu cả hai nhánh có commit riêng kể từ điểm tách, Git tạo một merge commit có hai cha, gộp nội dung cả hai nhánh.
git switch main
git commit -am "fix: sửa validate số lượng item" # main có commit mới
git switch feature/add-payment
git switch main
git merge feature/add-payment
Merge made by the 'recursive' strategy.
PaymentService.java | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
Git tự tạo commit merge với message mặc định Merge branch 'feature/add-payment'.
4.3. Xử lý conflict khi merge
Conflict xảy ra khi hai nhánh sửa cùng một vùng dòng code. Ví dụ class OrderService:
// Trong main
public class OrderService {
public BigDecimal calculateTotal(Order order) {
return order.getSubtotal().multiply(BigDecimal.valueOf(1.1)); // thêm thuế 10%
}
}
// Trong feature/add-payment
public class OrderService {
public BigDecimal calculateTotal(Order order) {
return order.getSubtotal().subtract(order.getDiscount()); // trừ giảm giá
}
}
Khi merge, Git không tự quyết định được giữ dòng nào, file sẽ xuất hiện marker:
public class OrderService {
public BigDecimal calculateTotal(Order order) {
<<<<<<< HEAD
return order.getSubtotal().multiply(BigDecimal.valueOf(1.1));
=======
return order.getSubtotal().subtract(order.getDiscount());
>>>>>>> feature/add-payment
}
}
Bạn cần sửa tay để kết hợp đúng logic (ví dụ: trừ giảm giá trước, rồi cộng thuế), xóa các marker <<<<<<<, =======, >>>>>>>, sau đó:
git add OrderService.java
git commit
Lưu ý: Commit sau khi resolve conflict không cần
-m, Git sẽ mở editor với message mặc định ghi rõ đây là merge commit — giữ nguyên message đó là đủ, trừ khi bạn muốn ghi chú thêm.
4.4. Merge vs Rebase
| Tiêu chí | merge |
rebase |
|---|---|---|
| Lịch sử | Giữ nguyên, có merge commit | Viết lại lịch sử, nhánh phẳng |
| An toàn | Không sửa commit cũ | Sửa commit cũ (đổi hash) |
| Dùng khi | Branch chia sẻ công khai (main, develop) |
Branch cá nhân, muốn lịch sử sạch trước khi tạo PR |
# Rebase branch feature lên trên main mới nhất
git switch feature/add-payment
git rebase main
Lưu ý: Đừng
rebasemột branch đã push và người khác đang pull từ nó — bạn sẽ làm sai lệch lịch sử của họ, gây conflict hàng loạt khi họ pull lại.
5. Kiểm thử một kịch bản merge bằng JUnit
Dù Git không phải code Java, bạn hoàn toàn có thể viết test để xác nhận logic nghiệp vụ sau khi merge hai nhánh không bị gãy — đây là lý do CI luôn chạy test trên merge commit. Ví dụ OrderService sau khi đã resolve conflict ở mục 4.3:
public class OrderService {
public BigDecimal calculateTotal(Order order) {
BigDecimal afterDiscount = order.getSubtotal().subtract(order.getDiscount());
return afterDiscount.multiply(BigDecimal.valueOf(1.1));
}
}
class OrderServiceTest {
private final OrderService orderService = new OrderService();
@Test
void shouldApplyDiscountThenTax() {
Order order = new Order(
BigDecimal.valueOf(100), // subtotal
BigDecimal.valueOf(10) // discount
);
BigDecimal total = orderService.calculateTotal(order);
assertThat(total).isEqualByComparingTo(BigDecimal.valueOf(99.0));
}
}
Giải thích: subtotal 100 trừ discount 10 còn 90, nhân thêm thuế 10% ra 99. Test này chính là “lưới an toàn” đảm bảo việc merge hai logic (thuế và giảm giá) không triệt tiêu lẫn nhau.
✓ shouldApplyDiscountThenTax()
6. Quy trình làm việc gợi ý cho team nhỏ
- Luôn tạo branch mới từ
maincho mỗi task:git switch -c feature/xxx. - Commit nhỏ, thường xuyên, message rõ ràng.
- Trước khi tạo Pull Request, cập nhật branch với
mainmới nhất:git switch feature/add-payment git fetch origin git merge origin/main - Resolve conflict (nếu có) trên máy local, chạy lại test.
- Push và tạo PR, để CI chạy test trên merge commit trước khi gộp vào
main.
Lưu ý: Không nên commit trực tiếp lên
maintrong team nhiều người — luôn qua branch + PR để có review và CI kiểm tra trước.
7. Lỗi thường gặp
- Quên
git addtrước khi commit: Git báonothing to commit, working tree cleandù bạn đã sửa file — vì file chưa được stage. - Merge nhầm branch: luôn
git statusvàgit branchkiểm tra mình đang ở đâu trước khimerge. - Conflict marker còn sót trong code: quên xóa
<<<<<<<sẽ làm code không compile được — luôn build/test ngay sau khi resolve conflict. - Force push lên branch chung:
git push --forcesau khi rebase branch đã chia sẻ sẽ xóa lịch sử của đồng nghiệp. Chỉ dùng--force-with-leasevà chỉ trên branch cá nhân.
8. Kết luận
Commit, branch và merge là ba thao tác bạn dùng hàng ngày với Git. Nắm chắc commit giúp lịch sử code rõ ràng, branch giúp làm việc song song không ảnh hưởng nhau, và hiểu merge (cùng cách resolve conflict) giúp bạn tự tin khi gộp code từ nhiều người. Hãy luyện tập commit nhỏ, đặt tên branch có quy ước, và luôn chạy test trước khi merge vào main.