Bạn vừa gặp lỗi git push bị rejected với thông báo kiểu non-fast-forward hoặc fetch first? Đừng vội dùng --force. Đây là lỗi rất thường gặp khi nhánh remote đã có commit mới mà máy bạn chưa có.
Trong bài này, mình sẽ đi theo hướng an toàn: hiểu lỗi, kiểm tra khác biệt giữa local và remote, chọn pull/rebase đúng tình huống, rồi push lại mà không làm mất code của người khác. Cùng xử lý từng bước nhé!

Tóm tắt nhanh
- Lỗi push rejected thường do remote branch có commit mới mà local branch chưa có.
- Hãy chạy
git fetchvà so sánhHEAD..origin/branchtrước khi sửa. - Nếu branch cá nhân,
git pull --rebasethường giúp lịch sử commit gọn hơn. - Nếu branch dùng chung, ưu tiên cách ít rủi ro: fetch, review, merge hoặc rebase theo quy ước team.
- Chỉ dùng
--force-with-leasekhi bạn hiểu rõ hậu quả và đang rewrite branch phù hợp.
Git push bị rejected là gì?
Git push bị rejected nghĩa là Git từ chối cập nhật nhánh remote vì thao tác push của bạn không thể đi tiếp từ lịch sử hiện tại trên remote. Trường hợp phổ biến nhất là lỗi non-fast-forward: remote có commit mới mà local chưa kéo về.
Theo tài liệu chính thức của Git, git push có thể bị từ chối khi cập nhật remote ref không phải là fast-forward, tức commit bạn muốn push không chứa lịch sử mới nhất của remote [1]. GitHub cũng giải thích lỗi non-fast-forward theo hướng tương tự: bạn cần tích hợp thay đổi từ remote trước khi push lại [2].
Ví dụ bạn đang ở nhánh feature/login. Bạn commit một thay đổi local, nhưng trong lúc đó đồng đội cũng merge một commit khác vào cùng nhánh remote. Khi bạn push, Git không biết nên đặt commit của bạn lên trên commit remote theo cách nào, nên nó dừng lại để bạn xử lý có chủ đích.

Vì sao Git báo non-fast-forward?
Lý do cốt lõi là local branch của bạn đang thiếu commit trên remote branch. Git bảo vệ remote branch bằng cách không cho bạn ghi đè lịch sử mới nếu chưa tích hợp nó vào local.
Các tình huống hay gặp gồm:
- Đồng đội đã push commit mới lên cùng branch trước bạn.
- Bạn sửa trực tiếp file trên GitHub/GitLab rồi quên pull về máy.
- CI hoặc bot tạo commit tự động trên remote branch.
- Bạn vừa rebase hoặc amend commit local, làm lịch sử local khác remote.
- Branch local đang trỏ tới upstream không đúng.
Điểm quan trọng là lỗi này không có nghĩa code của bạn hỏng. Nó chỉ nói rằng Git cần bạn quyết định cách ghép hai lịch sử lại. Nếu xử lý bình tĩnh, đây là thao tác bình thường trong workflow team.
Bạn đang đọc bài viết thuộc chuyên mục Lập trình của VietnamTutor — nơi mình chia sẻ các lỗi Git thực tế theo hướng dễ kiểm tra, dễ sửa và giảm rủi ro mất code.
Kiểm tra gì trước khi sửa?
Trước khi pull, rebase hay force push, bạn nên fetch remote và xem chính xác local đang khác remote ở đâu. Bước này giúp bạn tránh sửa theo cảm tính.
Chạy lần lượt:
# Lấy thông tin mới nhất từ remote nhưng chưa merge vào code local git fetch origin # Xem branch hiện tại và upstream git status -sb # Xem commit remote có mà local chưa có git log --oneline HEAD..origin/main # Xem commit local có mà remote chưa có git log --oneline origin/main..HEAD
Thay main bằng branch thật của bạn, ví dụ feature/login. Tài liệu git fetch mô tả rõ fetch chỉ tải object/ref từ remote, chưa tự merge vào working tree [3]. Vì vậy đây là bước kiểm tra khá an toàn.
Nếu git status -sb cho thấy branch của bạn đang ahead và behind cùng lúc, nghĩa là hai bên đã diverge. Khi đó, bạn cần merge hoặc rebase thay vì push thẳng.

Cách sửa lỗi push rejected an toàn
Cách sửa an toàn nhất là tích hợp commit remote vào local trước, kiểm tra lại, rồi push. Bạn có thể dùng merge hoặc rebase tùy workflow của team.
Cách 1: Pull bằng merge
Nếu team của bạn chấp nhận merge commit, cách dễ hiểu nhất là:
# Kéo commit remote về và merge vào branch hiện tại git pull origin main # Nếu có conflict, sửa file rồi commit merge git status # Push lại sau khi local đã chứa commit remote git push origin main
git pull về bản chất là fetch rồi tích hợp thay đổi vào branch hiện tại, tùy cấu hình có thể merge hoặc rebase [4]. Với người mới, merge dễ nhìn vì Git tạo commit riêng ghi nhận việc ghép lịch sử.
Cách 2: Pull bằng rebase
Nếu branch là nhánh cá nhân hoặc team muốn lịch sử tuyến tính, bạn có thể dùng:
# Đặt commit local của bạn lên trên commit mới từ remote git pull --rebase origin main # Nếu có conflict: sửa file, add lại, rồi tiếp tục rebase git add . git rebase --continue # Sau khi rebase xong, push lại git push origin main
git rebase sẽ chuyển các commit local của bạn sang nền lịch sử mới hơn [5]. Cách này cho lịch sử gọn, nhưng bạn cần cẩn thận nếu branch đã chia sẻ cho nhiều người.
Cách chọn nhanh
| Tình huống | Nên dùng | Lý do |
|---|---|---|
| Branch dùng chung, nhiều người push | git pull hoặc theo quy định team | Ít gây bất ngờ cho người khác |
| Branch cá nhân, chưa ai dựa vào lịch sử của bạn | git pull --rebase | Lịch sử commit gọn và dễ review |
| Remote có commit lạ | git fetch rồi review log | Không merge vội khi chưa hiểu thay đổi |
| Bạn vừa amend/rebase commit đã push | Cân nhắc --force-with-lease | Chỉ dùng khi branch phù hợp để rewrite |

Khi nào được force push?
Force push chỉ nên dùng khi bạn cố ý rewrite lịch sử của branch và chắc chắn không ghi đè commit của người khác. Nếu cần dùng, --force-with-lease thường an toàn hơn --force vì nó kiểm tra remote có thay đổi ngoài dự kiến hay không.
Ví dụ sau khi rebase branch cá nhân đã từng push:
# Cẩn thận: chỉ dùng khi bạn hiểu branch này có thể rewrite git push --force-with-lease origin feature/login
Tài liệu git push có mô tả --force-with-lease như một cơ chế bảo vệ bằng cách yêu cầu remote ref vẫn đang ở giá trị bạn kỳ vọng [1]. Nói đơn giản: nếu ai đó đã push commit mới lên remote sau lần fetch gần nhất của bạn, lệnh này sẽ không ghi đè mù quáng.
Mình khuyên bạn không dùng force push trên main, develop hoặc branch release nếu không có quy trình rõ. Với các nhánh quan trọng, hãy dùng pull request, protected branch và review thay vì sửa trực tiếp.

Làm sao phòng tránh lỗi này?
Bạn không thể loại bỏ hoàn toàn lỗi push rejected trong môi trường nhiều người, nhưng có thể giảm đáng kể bằng workflow rõ ràng. Thói quen tốt là fetch/pull thường xuyên, làm branch nhỏ và tránh sửa trực tiếp trên remote.
- Trước khi bắt đầu làm việc: chạy
git fetchhoặcgit pull --rebasetheo quy ước team. - Trước khi push: kiểm tra
git status -sbđể biết branch đang ahead/behind. - Làm branch ngắn, push sớm, mở pull request nhỏ.
- Không amend/rebase commit đã push lên branch dùng chung.
- Dùng protected branch cho
mainđể tránh push trực tiếp.
Nếu bạn đang xử lý thêm conflict sau khi pull, có thể đọc tiếp bài Git pull bị conflict. Nếu lỡ commit nhầm trước khi push, bài Git commit nhầm file sẽ phù hợp hơn. Còn nếu cần hiểu rõ lệnh undo, xem thêm Git reset, revert, restore.
Tóm lại, lỗi push rejected không đáng sợ nếu bạn không vội force push. Hãy fetch trước, xem commit nào đang lệch, chọn merge hoặc rebase theo workflow, rồi push lại. Bạn sẽ thấy lỗi này thực ra là một cơ chế bảo vệ khá hữu ích của Git!
Nguồn tham khảo
- Git Documentation: git-push
- GitHub Docs: Dealing with non-fast-forward errors
- Git Documentation: git-fetch
- Git Documentation: git-pull
- Git Documentation: git-rebase
- Git Documentation: git-merge
Các câu hỏi thường gặp
Git push bị rejected có làm mất code không?
Không. Lỗi rejected chỉ có nghĩa Git chưa cho bạn cập nhật remote. Code local vẫn còn, trừ khi bạn tự chạy lệnh ghi đè như reset hard hoặc force push sai cách.
Nên dùng git pull hay git pull –rebase?
Nếu branch dùng chung, hãy theo quy ước team. Nếu branch cá nhân và muốn lịch sử gọn, git pull –rebase thường phù hợp. Điều quan trọng là kiểm tra remote trước khi tích hợp.
Có nên dùng git push –force để sửa non-fast-forward không?
Không nên dùng mặc định. Force push có thể ghi đè commit của người khác. Nếu thật sự cần rewrite branch cá nhân, hãy cân nhắc git push –force-with-lease và fetch trước đó.
Lỗi fetch first khác gì non-fast-forward?
Hai thông báo thường nói cùng một vấn đề: remote có commit mới mà local chưa có. Git yêu cầu bạn fetch/pull và tích hợp thay đổi trước khi push lại.
Sau khi pull bị conflict thì làm gì?
Hãy mở các file conflict, chọn nội dung đúng, chạy git add rồi tiếp tục merge hoặc rebase. Nếu chưa chắc, bạn có thể abort thao tác hiện tại trước khi thử lại.
