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 init tạ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ại git 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ặc git switch <tên> di chuyển HEAD sang branch đó.
  • -c trong git switch là 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 rebase mộ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ỏ

  1. Luôn tạo branch mới từ main cho mỗi task: git switch -c feature/xxx.
  2. Commit nhỏ, thường xuyên, message rõ ràng.
  3. Trước khi tạo Pull Request, cập nhật branch với main mới nhất:
    git switch feature/add-payment
    git fetch origin
    git merge origin/main
  4. Resolve conflict (nếu có) trên máy local, chạy lại test.
  5. 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 main trong 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 add trước khi commit: Git báo nothing to commit, working tree clean dù bạn đã sửa file — vì file chưa được stage.
  • Merge nhầm branch: luôn git status và git branch kiểm tra mình đang ở đâu trước khi merge.
  • 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 --force sau khi rebase branch đã chia sẻ sẽ xóa lịch sử của đồng nghiệp. Chỉ dùng --force-with-lease và 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.

gitversion-controlbranchmergedevops