Khi nào nên cân nhắc ghi đè thay đổi cục bộ
Nhìn chung, ghi đè thay đổi cục bộ theo cách này đi ngược với cách dùng Git được khuyến nghị. Hầu như với mọi tình huống đều có một quy trình Git phù hợp để xử lý. Tuy nhiên, nhiều quy trình đòi hỏi hiểu biết sâu hơn về Git và mất nhiều thời gian thực hiện. Dù không phải thực hành tốt nhất, đôi khi ghi đè thay đổi cục bộ lại là cách nhanh nhất để hoàn thành công việc.
Với tôi, điều này thường xảy ra khi tôi làm một tính năng mình chưa quen. Tôi bắt đầu bằng cách tạo nhánh và thực hiện vài commit. Khi bí, tôi nhờ đồng nghiệp giúp. Họ bắt đầu từ một commit sớm hơn, trước khi tôi mắc lỗi, giải quyết vấn đề và đẩy thay đổi lên. Lúc này, tôi cần thay phiên bản cục bộ của mình bằng phiên bản từ xa đã cập nhật, đồng thời loại bỏ các thay đổi cục bộ để có thể tiếp tục làm tính năng đó.
Một ví dụ khác là khi có xung đột, và tôi thấy việc sửa xung đột khó hoặc tốn thời gian hơn so với việc triển khai lại thay đổi của mình trên phiên bản mã mới nhất.
Dù lý do là gì, hãy luôn chắc chắn rằng các thay đổi đó thực sự không cần thiết và có thể xóa an toàn, hoặc hãy sao lưu nhánh của bạn để tránh mất mát công việc quan trọng.
Cách ghi đè thay đổi cục bộ một cách đúng đắn
Bước đầu tiên là fetch (tải) nội dung mới nhất từ kho từ xa về máy cục bộ. Thao tác này dùng lệnh fetch của git:
git fetch
Mặc định, lệnh này sẽ fetch nội dung của nhánh hiện tại, vốn thường là điều ta cần. Ta cũng có thể chỉ định nhánh muốn tải (thay <branch_name> bên dưới bằng tên nhánh muốn fetch):
git fetch origin/<branch_name>
Ngoài ra, ta có thể dùng tùy chọn --all để tải mọi thứ:
git fetch –all
Fetch thay đổi từ kho từ xa sẽ không ảnh hưởng đến các tệp trong thư mục nhánh làm việc hiện tại của bạn. Lệnh fetch tải thay đổi từ kho từ xa nhưng không trộn (merge) chúng. Kho cục bộ giữ hai góc nhìn: tệp cục bộ và tệp từ xa; fetch sẽ cập nhật góc nhìn về tệp từ xa.
Sau khi tải thay đổi từ kho từ xa, ta có thể dùng lệnh reset của git để đặt lại nhánh làm việc hiện tại về một trạng thái nhất định:
git reset --hard origin/<branch_name>
Tùy chọn --hard là bắt buộc để đảm bảo các thay đổi cục bộ bị ghi đè bởi thay đổi từ xa. Trong lệnh trên, chúng ta chọn thay thế nhánh làm việc hiện tại bằng origin/<banch_name>, tức phiên bản từ xa của nhánh vừa được fetch.
Lệnh git reset sẽ xóa các thay đổi cục bộ, kể cả khi chúng đã được commit. Vì vậy, khuyến nghị tạo bản sao lưu cục bộ của nhánh hiện tại trước khi chạy lệnh này. Có thể thực hiện bằng:
git branch <name_of_the_backup_branck>
Thay <name_of_the_backup_branch> bằng tên bạn muốn đặt cho nhánh sao lưu.
Gộp lại, chúng ta có thể ghi đè mọi thay đổi cục bộ của một nhánh với phiên bản từ xa mới nhất bằng cách làm như sau:
git fetch
git branch <name_of_the_backup_branch>
git reset --hard origin/<branch_name>
Nếu bạn muốn mở rộng kiến thức về làm việc với nhánh trong Git, hãy xem hướng dẫn Git Clone một nhánh cụ thể.
Dọn sạch hoàn toàn bằng cách xóa tệp không được theo dõi
Git sẽ không đụng đến các tệp không được theo dõi (những tệp chưa từng dùng git add). Nếu muốn xóa cả những tệp này, ta có thể dùng:
git clean -fdx
Đây là lệnh nguy hiểm dùng để xóa các tệp không được theo dõi khỏi thư mục làm việc. Rất quan trọng phải dùng lệnh này thận trọng vì nó xóa vĩnh viễn các tệp đó, khiến không thể khôi phục bằng Git. Dưới đây là giải thích các tùy chọn của lệnh:
-f: Buộc thực hiện thao tác clean. Không có cờ này, Git sẽ từ chối xóa tệp không được theo dõi như một biện pháp an toàn tránh mất dữ liệu ngoài ý muốn.-d: Yêu cầugit cleanxóa không chỉ tệp mà cả thư mục không được theo dõi. Mặc định,git cleanchỉ nhắm tới tệp và bỏ qua thư mục.-x: Mặc định,git cleansẽ tôn trọng quy tắc.gitignorevà không xóa tệp/thư mục khớp các mẫu đó. Thêm -x sẽ ghi đè hành vi này, buộc Git xóa cả tệp/thư mục bị.gitignorehoặc quy tắc bỏ qua khác che phủ. Kết quả là dọn rất sạch, xóa mọi tệp không được theo dõi trong kho, bao gồm output build, log và các artefact sinh ra khác vốn thường bị bỏ qua.
Tìm hiểu Git Pull
Cách thông thường để cập nhật kho cục bộ với thay đổi từ xa là dùng lệnh git pull. Tuy nhiên, lệnh này không thể dùng để ghi đè thay đổi cục bộ vì git pull sẽ cố gắng hợp nhất (merge) phiên bản cục bộ với phiên bản từ xa.
Bên trong, git pull thực hiện các bước sau:
- Dùng
git fetchđể tải phiên bản mới nhất của kho từ xa. - Dùng
git mergeđể hợp nhất nhánh cục bộ và nhánh từ xa.
Vì có bước merge, git pull không phù hợp trong các tình huống ta muốn bỏ qua và loại bỏ thay đổi cục bộ.

Còn git pull --force thì sao?
Xem tài liệu của lệnh git pull ta thấy có tùy chọn --force. Một hiểu lầm phổ biến là tùy chọn này sẽ buộc git pull ghi đè thay đổi cục bộ bằng thay đổi từ xa.
Khi cung cấp tùy chọn --force cho git pull, tùy chọn này được truyền xuống git fetch. Do đó git pull --force thực hiện các bước sau:
git fetch --forcegit merge

Chúng ta đã biết fetch dùng để cập nhật góc nhìn cục bộ về kho từ xa. Nếu khác biệt duy nhất giữa kho cục bộ hiện tại và kho từ xa là vài commit bị thiếu (nhánh cục bộ đi sau phiên bản từ xa), thì git fetch chỉ việc cập nhật góc nhìn cục bộ bằng cách nối thêm các commit mới vào lịch sử.

Tuy nhiên, có thể ai đó đã viết lại lịch sử commit của nhánh từ xa khiến nó không tương thích với góc nhìn cục bộ. Khi đó, fetch sẽ không hoạt động.

Ta có thể vượt qua điều này bằng tùy chọn --force, buộc Git bỏ qua lịch sử cục bộ và ghi đè bằng lịch sử từ xa.
Ta thấy tùy chọn --force liên quan đến việc ép ghi đè lịch sử commit và không liên quan đến việc bỏ qua thay đổi cục bộ trong thư mục nhánh làm việc.
Tích hợp các thay đổi cục bộ
Ghi đè thay đổi cục bộ bằng thay đổi từ xa không phải thực hành phổ biến trong Git, nhưng có thể hữu ích trong một số trường hợp khi chúng ta không muốn xử lý xung đột hoặc không quan tâm đến thay đổi cục bộ.
Merge
Thông thường, bạn sẽ pull thay đổi và xử lý xung đột. Nhiều người ngại xử lý xung đột. Một số xung đột có thể khó, nhưng không thường xuyên như vậy. Xung đột xảy ra khi:
- Cả nhánh cục bộ và nhánh từ xa đều có thay đổi trên cùng một phần của tệp.
- Một tệp bị xóa ở một phía và được sửa ở phía còn lại.
Trong các trường hợp này, Git không thể đoán bạn muốn giữ thay đổi nào, và chúng ta cần chỉ định thủ công. Git sẽ đánh dấu xung đột trong tệp như sau:

Để giải quyết xung đột, chúng ta chỉnh sửa tệp bằng cách xóa các dòng đánh dấu, chỉ giữ lại phần muốn giữ. Sau khi xử lý xong tất cả xung đột, có thể commit thay đổi. Để có hướng dẫn chi tiết, hãy xem bài cách giải quyết xung đột merge trong Git.
Cherry-pick
Nếu thực sự muốn trước tiên làm cho phiên bản cục bộ khớp phiên bản từ xa rồi mới tích hợp thay đổi của mình, ta có thể đưa các commit từ nhánh sao lưu vào nhánh hiện tại như sau:
git cherry-pick <commit_hash>
Ở đây, <commit_hash> là mã băm của commit muốn tích hợp. Điều này giả định rằng trước khi chạy git reset, ta đã tạo nhánh sao lưu bằng git branch <name_of_the_backup_branck> và các thay đổi cục bộ đã được commit.
Để tìm <commit_hash>, ta có thể liệt kê các mã băm commit trên nhánh sao lưu bằng lệnh sau:
git log <name_of_the_backup_branck>
Stash
Ngoài ra, nếu không muốn commit thay đổi hoặc tạo nhánh sao lưu, chúng ta có thể stash thay đổi trước khi merge bằng:
git stash
Lệnh này dùng để tạm thời lưu công việc hiện tại mà không cần commit vào lịch sử nhánh. Stash sẽ đặt các chỉnh sửa của bạn sang một bên, giúp thư mục làm việc sạch sẽ.
Sau khi reset, ta có thể đưa thay đổi trở lại bằng lệnh sau:
git stash pop
Với stash, toàn bộ quy trình sẽ trông như sau:
git fetch
git stash
git reset --hard origin/<branch_name>
git stash pop
Để tìm hiểu thêm, hãy xem cheat sheet với stash và các lệnh git khác.
Kết luận
Dù không phải thực hành tốt, đôi khi chúng ta vẫn rơi vào tình huống muốn cho nhánh cục bộ khớp với nội dung của nhánh từ xa và loại bỏ thay đổi cục bộ.
Có một hiểu lầm rằng làm điều này bằng git pull --force. Điều này không đúng vì git pull --force kết hợp git fetch --force và git merge. Do đó, nó cố gắng merge thay đổi cục bộ với thay đổi từ xa, chứ không ghi đè chúng.
Cách đúng để ghi đè thay đổi cục bộ bằng nội dung từ kho từ xa là:
git fetch
git reset --hard origin/<branch_name>
Lệnh git reset phải được dùng thận trọng vì sẽ xóa thay đổi cục bộ, kể cả khi đã commit. Nếu ta có các commit cục bộ và đã tạo nhánh sao lưu, có thể đưa chúng trở lại bằng git cherry-pick. Nếu chưa commit và muốn tạm lưu công việc cục bộ, ta có thể dùng git stash trước khi reset rồi git stash pop để đưa thay đổi trở lại.

Trong một số tình huống, chúng ta có thể muốn đặt lại trạng thái của một nhánh Git cục bộ để khớp với kho từ xa, qua đó loại bỏ mọi thay đổi cục bộ. Việc này thường được gọi là kéo cưỡng bức.
Điều này dễ dẫn đến hiểu lầm rằng cách thực hiện là dùng tùy chọn
--forcetrong lệnhgit pull. Trong bài viết này, chúng ta sẽ thấy đây là hai việc khác nhau.Nếu bạn mới dùng Git, hãy cân nhắc đọc trước hướng dẫn Git push và pull.
Chúng ta có thể ghi đè trạng thái cục bộ bằng trạng thái từ xa với các lệnh dưới đây, thay
<branch_name>bằng tên nhánh của bạn. Hành động này sẽ xóa vĩnh viễn mọi thay đổi cục bộ trên nhánh đó.Bài viết này sẽ đi sâu vào cách các lệnh này hoạt động. Chúng ta sẽ vượt ra ngoài các mẹo chữa cháy để hiểu cơ chế bên dưới, từ đó học được những phương pháp hiệu quả hơn nhằm đạt kết quả mong muốn mà không có nguy cơ mất các thay đổi quan trọng. Chúng ta cũng sẽ làm rõ nhầm lẫn và tìm hiểu thực chất tùy chọn
--forcecủagit pulllàm gì.