Lộ trình
Ứng dụng có thể bị ảnh hưởng khi gộp phải mã có vấn đề, dù là do vô tình tích hợp phần việc chưa hoàn thiện vào nhánh chính hay bỏ sót một lỗi nghiêm trọng lọt qua các bài kiểm thử tự động.
Trong bài viết này, tôi sẽ hướng dẫn bạn cách dùng git revert để hoàn tác một lần gộp một cách an toàn, đảm bảo lịch sử commit vẫn nguyên vẹn và tính toàn vẹn của dự án được giữ gìn.
Cách git revert hoạt động
Ta có thể coi git revert như phiên bản lệnh hoàn tác của Git. Tuy nhiên, lệnh git revert không xóa commit hay đưa nhánh về trạng thái trước đó. Thay vào đó, nó tạo một commit mới đảo ngược các thay đổi từ một commit cụ thể.

Cú pháp để hoàn tác một commit có băm <commit_hash> là:
git revert <commit_hash>
Ta có thể liệt kê các commit cùng mã băm của chúng bằng lệnh git log. Kết quả của git log hiển thị commit từ mới nhất đến cũ nhất, như sau:

Ví dụ, để hoàn tác commit triển khai hàm trừ, ta dùng lệnh:
git revert 7ba24a3e62d4d37182428ccfaa070baa222b1151
Dùng git revert cho phép ta đảo ngược thay đổi của một commit cụ thể mà không ảnh hưởng lịch sử commit.
Lưu ý rằng git revert không phải phép màu; tùy vào lịch sử commit, nó có thể dẫn đến xung đột cần giải quyết thủ công.
Ưu điểm của git revert so với hoàn tác thủ công
Vì sao git revert hữu ích nếu đôi khi ta vẫn phải tự xử lý xung đột? Chẳng phải tự hoàn tác sẽ dễ hơn sao? Hãy xem các ưu điểm của nó:
- Bảo toàn lịch sử:
git reverttạo một commit mới để hoàn tác thay đổi của commit được chỉ định, đồng thời giữ nguyên toàn bộ lịch sử commit. Điều này giúp duy trì lịch sử thay đổi và hoàn tác một cách minh bạch. - Hoàn tác nguyên tử: Đảm bảo thao tác hoàn tác là nguyên tử và nhất quán. Khi xóa và commit thủ công, luôn có rủi ro sai sót do con người.
- Nhận biết xung đột: Đảm bảo ta được cảnh báo qua xung đột nếu có tích hợp hay thay đổi nào phụ thuộc vào commit gốc. Điều này có thể bất tiện, nhưng giúp ngăn hệ quả ngoài ý muốn.
- Siêu dữ liệu: Commit mới do git revert tạo ra bao gồm siêu dữ liệu và thông điệp commit mô tả bối cảnh phần đã hoàn tác, hỗ trợ việc hiểu trong tương lai. Nếu không dùng
git revert, bối cảnh này có thể bị mất.
Hoàn tác một lần gộp trong các kịch bản khác nhau
Phần này, ta học cách hoàn tác một lần gộp. Để minh họa, giả sử ta gộp nhánh feature vào nhánh main và thực thi lệnh từ nhánh main:
git merge feature
Những gì học được ở đây có thể áp dụng cho bất kỳ hai nhánh nào bằng cách thay tên tương ứng.
Hoàn tác một lần gộp không tạo commit mới
Lệnh git merge không phải lúc nào cũng tạo commit mới. Commit chỉ được tạo nếu nhánh main đã rẽ nhánh so với feature. Vì git revert cần có commit để thao tác, nên trong trường hợp này ta không thể dùng nó.
Hai nhánh main và feature được coi là rẽ khi có commit mới trên main không phải tổ tiên của nhánh feature. Nói cách khác, sau khi tạo feature, đã có commit mới trên main.
Nếu các nhánh chưa rẽ, khi chạy git merge feature trên nhánh main, Git sẽ dùng fast-forward để gộp. Tức là nó di chuyển HEAD của nhánh main tới HEAD của nhánh feature.

Ta có thể quan sát điều này qua kết quả của git merge:

Để hoàn tác kiểu gộp này, ta chỉ cần đưa HEAD của nhánh main trở về vị trí cũ. Để làm vậy, ta:
- Xác định
HEADtrước đó bằnggit reflog - Đặt lại
HEADvề giá trị trước bằnggit reset --hard <previous_head>, thay<previous_head>bằngHEADtrước đó.
Kết quả của git reflog sẽ trông tương tự như sau:

Ta có thể xác định HEAD trước đó bằng cách nhìn dòng “checkout: moving from feature to main” (nó ghi feature và main vì đó là tên các nhánh của ta).
Trong trường hợp này, head trước là fe59838. Để đưa HEAD của nhánh main về đó và hoàn tác lần gộp, ta dùng lệnh:
git reset --hard fe59838

Hoàn tác một lần gộp có tạo commit
Nếu nhánh main và feature đã rẽ, khi gộp hai nhánh sẽ tạo một commit mới, gọi là merge commit.

Merge commit áp dụng các thay đổi từ một nhánh sang nhánh khác. Ở đây, các thay đổi trong feature được áp dụng lên nhánh main.
Để hoàn tác các thay đổi trên nhánh main, ta dùng git revert trên merge commit. Việc này sẽ tạo một commit mới đảo ngược các thay đổi đã đưa vào main qua lần gộp, khôi phục trạng thái của nhánh main về trước khi gộp.
Trước hết, ta cần xác định mã băm của merge commit. Có thể làm điều này bằng lệnh git log:

Vì merge commit có hai parent, cú pháp của git revert hơi khác một chút. Ta cần dùng tùy chọn -m 1 để chỉ định muốn hoàn tác các thay đổi so với nhánh main:
git revert -m 1 b8dab2c8611e324ed0d273133987415350e6d10d

Giải quyết xung đột khi hoàn tác một commit
Đôi khi, xung đột có thể phát sinh khi hoàn tác một commit, đặc biệt nếu commit bị hoàn tác xung đột với các thay đổi về sau trong mã nguồn. Trong các trường hợp như vậy:
- Git sẽ tạm dừng thao tác revert: Ta cần tự giải quyết xung đột. Git sẽ đánh dấu các tệp xung đột và yêu cầu can thiệp.
- Giải quyết xung đột: Mở từng tệp xung đột, xử lý các phần được Git đánh dấu và lưu thay đổi.
- Stage các tệp đã giải quyết:
git add <file-path> - Tiếp tục thao tác revert:
git revert --continue
Kết luận
Dùng git revert để hoàn tác các merge commit đảm bảo mọi thay đổi và chỉnh sửa đều được ghi nhận trong lịch sử commit.
Bên cạnh đó, hiểu rõ khi nào nên dùng git reset và khi nào nên dùng git revert giúp ta đưa ra quyết định tốt hơn, nhất là khi cân nhắc quy trình làm việc cộng tác hoặc các thay đổi chỉ ở máy cục bộ.
Bạn có thể đọc thêm về chủ đề này ở phần Câu hỏi thường gặp bên dưới. Nếu muốn học thêm về Git, tôi gợi ý các tài nguyên sau:
FAQs
Tôi có thể hoàn tác một lần gộp đã đẩy lên bằng git revert không?
Có. Đó là trường hợp sử dụng chính của git revert với các lần gộp. Nếu các thay đổi chỉ ở cục bộ thì git reset là cách dễ hơn để hoàn tác lần gộp.
Để người khác thấy thay đổi, ta cần đẩy (push) chúng lên kho lưu trữ từ xa. Vì vậy, để hoàn tác một lần gộp đã được đẩy lên, ta làm như sau:
git revert -m 1 <merge_commit_hash>
git push origin <branch_name>
Thay <merge_commit_hash> bằng mã băm của merge commit và <brach_name> bằng tên nhánh nơi đã thực hiện gộp.
Merge commit có bị xóa sau khi dùng git revert không?
Không. Merge commit vẫn nằm trong lịch sử, nhưng các thay đổi của nó được hoàn tác bởi commit mới. Mục đích của việc hoàn tác gộp bằng git revert là để bảo toàn lịch sử commit.
Ngoài git revert, còn cách nào để hoàn tác gộp không?
Một cách khác để hoàn tác gộp là dùng git reset. Cách này đặt lại nhánh về trạng thái trước merge commit. Phương pháp này can thiệp mạnh hơn và có thể gây vấn đề nếu cộng tác viên khác đã dựa công việc của họ trên các commit mà ta định reset. Do đó, chỉ nên dùng khi chắc chắn sẽ không ảnh hưởng đến người khác.
git reset --hard <commit_hash>
git push --force
Thay <commit_hash> bằng mã băm của commit mà ta muốn quay lại.
Khi nào tôi nên dùng git reset để hoàn tác gộp?
Tôi khuyến nghị chỉ dùng git reset để hoàn tác gộp khi lần gộp chỉ diễn ra trên máy cục bộ và chưa được đẩy lên.
