Lỡ tạo nhánh Git từ nhánh khác thay vì main — cách xử lý sạch
DevOps

Lỡ tạo nhánh Git từ nhánh khác thay vì main — cách xử lý sạch

Lỡ git checkout -b khi đang đứng trên nhánh khác khiến nhánh mới cõng code chưa merge. Đây là cách phát hiện và dọn sạch nhánh Git sai gốc trước khi mở PR.

Bạn bắt đầu một task mới, gõ git checkout -b feature/ODOO-375, code ngon lành rồi mở PR vào main. Nhưng reviewer nhắn: "Sao PR này có cả code của ticket khác vậy?". Đây là lỗi tạo nhánh Git sai gốc — một trong những cái bẫy phổ biến nhất khi làm việc nhiều nhánh song song, và may là dọn rất gọn nếu bắt đúng bệnh.

Bài này ghi lại đúng tình huống vừa xảy ra với nhánh ODOO-375, kèm hai cách xử lý và bảng so sánh để bạn chọn cho hợp hoàn cảnh.

Vì sao nhánh lại "cõng" code của task khác?

Trong Git, git checkout -b <tên-nhánh> tạo nhánh mới từ vị trí bạn đang đứng, chứ không phải luôn từ main.

Đáng lẽ tách nhánh từ main, nhưng nếu lúc đó bạn đang đứng trên một nhánh feature khác chưa merge (nhánh của đồng nghiệp, hoặc task trước của chính bạn), thì nhánh mới sẽ thừa kế toàn bộ commit của nhánh kia.

Hậu quả khi mở PR vào main:

  • PR chứa lẫn code của một ticket không liên quan → reviewer thấy 2 task trộn vào nhau, khó review.

  • Nếu PR được merge, bạn vô tình kéo luôn code chưa duyệt của task kia vào main.

Bắt sớm trước khi tạo PR là rẻ nhất. Dưới đây là cách phát hiện.

Cách phát hiện nhánh tách sai gốc

Chạy đúng một lệnh — nó liệt kê chính xác những commit mà PR sẽ mang vào main:

git log --oneline main..HEAD

Cách đọc: main..HEAD nghĩa là "những commit có trên nhánh hiện tại nhưng chưa có trên main". Nếu bạn thấy có commit của ticket khác (không phải task đang làm) nằm bên dưới các commit của mình → nhánh đã tách sai gốc.

Xác định "mốc cắt": đó là commit cuối cùng của nhánh kia, tức ngay trước commit đầu tiên của bạn. Ghi lại hash này — bạn sẽ cần nó cho Cách 1.

Muốn tập tay các lệnh rebase/cherry-pick bên dưới mà không sợ hỏng repo thật? Thử Git Playground — mô phỏng commit, nhánh và merge ngay trên trình duyệt.

Cách 1 — Rebase dọn tại chỗ (giữ nguyên tên nhánh)

Phù hợp khi bạn đã push nhánh / đã tạo PR và muốn giữ nguyên PR đó, chỉ dọn cho sạch.

git branch backup/<tên-nhánh>            # backup an toàn, chạy TRƯỚC khi rebase
git rebase --onto main <mốc-cắt>         # nhấc commit của mình đặt lên main
git log --oneline main..HEAD             # KIỂM TRA: chỉ còn commit của task mình
git push --force-with-lease origin <tên-nhánh>

Giải thích từng bước:

  • git rebase --onto main <mốc-cắt>
    Hãy tưởng tượng nhánh của bạn đang là một chồng commit xếp lên nhau: dưới cùng là code của nhánh kia, trên là code của bạn. Lệnh này làm đúng một việc: nhấc riêng phần code của bạn (mọi commit nằm sau <mốc-cắt> — chính là commit id từ điểm muốn cắt) ra, rồi đặt thẳng lên main. Phần code của nhánh kia bị bỏ lại phía dưới, không đi theo nữa. Kết quả: nhánh của bạn giờ mọc thẳng từ main, chỉ còn đúng code của mình.

Trước:   main → [code nhánh kia] → [code của bạn]
Sau:     main → [code của bạn]          (code nhánh kia đã bị bỏ lại)
  • git push --force-with-lease
    Rebase đã "chép lại" các commit của bạn thành phiên bản mới → chúng mang mã hash mới, khác với bản đang nằm trên server. Vì lịch sử đã khác nhau, nếu push kiểu thường Git sẽ từ chối. Nên phải push đè lên bản cũ.

Nhưng thay vì --force (đè bất chấp), ta dùng --force-with-lease — bản "force có phanh": nó chỉ đè khi trên server đúng là bản bạn thấy lần cuối. Nếu có đồng nghiệp vừa push thêm gì đó lên nhánh chung, lệnh sẽ dừng lại và báo thay vì xóa mất công của họ.

  • git reset --hard backup/<tên-nhánh> (chỉ dùng khi có sự cố)
    Nếu sau khi rebase mà bước kiểm tra ra kết quả lạ (thiếu commit, lộn xộn...), lệnh này đưa nhánh của bạn quay về y hệt lúc trước khi rebase — nhờ bản sao lưu backup/ bạn đã tạo ở bước đầu. Coi như nút "hoàn tác", làm lại từ đầu.

  • git branch -D backup/<tên-nhánh> (dọn dẹp)
    Khi mọi thứ đã ổn, bản sao lưu không cần nữa. Lệnh này xóa nhánh backup cho gọn, tránh sau này nhìn danh sách nhánh bị rối.

Cách 2 — Nhánh mới từ main + cherry-pick (không cần force)

Phù hợp khi bạn chưa push, hoặc muốn nhánh sạch tinh và ngại force-push.

git checkout main
git pull                                 # cập nhật main mới nhất
git checkout -b <tên-nhánh-mới>          # nhánh mới, tách ĐÚNG từ main
git cherry-pick <nhiều commit id vd: f6a1b2c 9e8d7c6 1a2b3c4 >   # nhặt từng commit của mình, đắp qua nhánh mới
git log --oneline main..HEAD             # KIỂM TRA: chỉ còn commit của task mình
git push origin <tên-nhánh-mới>          # push thường, KHÔNG cần force

Giải thích:

  • git cherry-pick <hash>: lấy đúng thay đổi của commit đó đắp sang nhánh hiện tại. Cherry-pick theo thứ tự cũ → mới để lịch sử đúng chiều.
    vd: git cherry-pick f6a1b2c 9e8d7c6 1a2b3c4

    nhặt commit f6a1b2c, rồi commit 9e8d7c6, rồi commit 1a2b3c4

  • Push thường vì đây là nhánh hoàn toàn mới, không viết lại lịch sử của nhánh nào cả.

  • Sau khi PR mới chạy ổn, xoá nhánh cũ (nhánh còn lẫn code kia) để khỏi nhầm:

git push origin --delete <tên-nhánh-cũ>
git branch -D <tên-nhánh-cũ>

Chọn cách nào?

Cách 1 — Rebase tại chỗ

Cách 2 — Nhánh mới + cherry-pick

Giữ tên nhánh / PR cũ

✅ Có

❌ Tạo nhánh + PR mới

Force-push

Cần (--force-with-lease)

Không cần

Nhánh cũ

Được dọn sạch luôn

Còn lại, phải xoá tay

Rủi ro

Cao hơn chút (viết lại lịch sử)

Thấp, an toàn hơn

Khi nào dùng

Nhánh đã push / đã mở PR và muốn giữ nguyên PR; hoặc chỉ mình bạn làm trên nhánh

Chưa push, muốn nhánh sạch tinh, hoặc nhánh có nhiều người cùng làm (né force-push đè commit người khác)

Phòng bệnh cho lần sau

  • Luôn về main trước khi tách nhánh: git checkout main && git pull rồi mới git checkout -b.

  • Kiểm tra nhanh bằng prompt: cấu hình shell hiển thị tên nhánh hiện tại để không "tách nhầm trong vô thức".

  • Quen tay git log --oneline main..HEAD ngay trước khi mở PR — 2 giây để chắc PR chỉ chứa đúng code của mình.

Kết luận

Tạo nhánh Git sai gốc nghe đáng sợ nhưng thực ra rất dễ dọn: một lệnh git log --oneline main..HEAD để phát hiện, rồi chọn rebase --onto (giữ PR) hoặc cherry-pick sang nhánh mới (an toàn hơn). Luôn tạo backup/ trước khi rebase, và kiểm tra lại trước khi force-push.

Bước tiếp theo: tập tay hai lệnh này trong Git Playground trước khi làm trên repo thật, để lúc "có biến" là xử lý gọn ngay.

Câu hỏi thường gặp

Làm sao biết nhánh của tôi tách sai gốc?
Chạy git log --oneline main..HEAD. Lệnh này liệt kê đúng những commit mà PR sẽ mang vào main. Nếu có commit của ticket khác nằm dưới commit của bạn thì nhánh đã tách sai gốc.
--force-with-lease khác gì --force?
--force-with-lease chỉ ghi đè khi nhánh trên remote vẫn đúng như bản bạn thấy lần cuối. Nếu người khác vừa push thêm commit, lệnh sẽ dừng lại thay vì đè mất — an toàn hơn --force thuần.
Rebase --onto có làm mất code của tôi không?
Không, nếu bạn tạo git branch backup/<tên-nhánh> trước. Rebase chỉ chép commit của bạn đặt lên main. Nếu kết quả lạ, git reset --hard backup/<tên-nhánh> đưa mọi thứ về nguyên trạng.
Khi nào nên cherry-pick thay vì rebase?
Khi bạn chưa push, muốn nhánh sạch hoàn toàn, hoặc nhánh có nhiều người cùng làm nên muốn tránh force-push. Cherry-pick tạo nhánh mới từ main nên không phải viết lại lịch sử nhánh cũ.
Tôi đã merge PR lẫn code rồi thì sao?
Lúc đó phức tạp hơn nhiều — cần git revert phần code không mong muốn hoặc phối hợp với team để xử lý trên main. Vì vậy hãy luôn chạy git log --oneline main..HEAD trước khi merge.

Công cụ liên quan

Bài viết hữu ích?