- Vì với Git, nếu không biết tên của tình huống mắc lỗi thì rất khó tìm cách giải quyết, nên đây là tài liệu được sắp xếp để có thể tra cứu và dùng ngay các lệnh theo từng kịch bản khôi phục phổ biến
- Khi làm hỏng nặng kho lưu trữ hoặc muốn hoàn tác lỗi xóa/gộp nhầm, có thể tìm lịch sử thao tác bằng
git reflog và quay về trạng thái trước đó bằng git reset HEAD@{index}
- Những chỉnh sửa nhỏ hoặc thay đổi thông điệp của commit cuối cùng có thể xử lý nhanh bằng
git commit --amend, nhưng không nên dùng cho các commit đã được đẩy lên nhánh công khai/chia sẻ
- Nếu commit nhầm vào nhánh sai, có thể chuyển bằng luồng
reset, stash, checkout, stash pop hoặc bằng cherry-pick, nhưng cách an toàn sẽ khác nhau tùy theo commit đó đã được push hay chưa
reset --hard và git clean -d --force, vốn dùng để ép khôi phục theo trạng thái của kho lưu trữ từ xa, có thể xóa cả các tệp chưa được theo dõi nên cần kiểm tra trước khi chạy
Vì sao khó khôi phục lỗi trong Git
- Git rất dễ khiến người dùng mắc lỗi, và nhiều khi để sửa vấn đề thì bạn phải biết sẵn tên của khái niệm cần tìm mới có thể tra cứu
- Tài liệu này tổng hợp những tình huống tệ thường gặp trong Git bằng tiếng Anh đơn giản cùng ví dụ lệnh
- Đây không phải tài liệu tham khảo đầy đủ, và cũng có thể có những cách khác để xử lý cùng một việc
Đưa kho lưu trữ về trạng thái trong quá khứ
- Khi bạn làm hỏng nặng kho lưu trữ hoặc muốn khôi phục nội dung đã xóa, hãy kiểm tra lịch sử thao tác bằng
git reflog
git reflog
# Có thể xem danh sách thao tác Git trên mọi nhánh
# Mỗi mục có một chỉ số dạng HEAD@{index}
git reset HEAD@{index}
- Có thể dùng
reflog trong các tình huống sau
- Khôi phục nội dung đã xóa nhầm
- Loại bỏ một lần thử làm hỏng kho lưu trữ
- Khôi phục sau một lần merge lỗi
- Quay lại thời điểm thực sự hoạt động được
Cách sửa commit cuối cùng
- Nếu cần thêm một chỉnh sửa nhỏ ngay sau commit cuối cùng, hãy dùng
git commit --amend --no-edit
# Sau khi thay đổi
git add . # hoặc thêm từng tệp riêng lẻ
git commit --amend --no-edit
- Lệnh này sẽ đưa thay đổi mới vào commit cuối cùng
- Cũng có thể tạo một commit mới rồi gộp bằng
rebase -i, nhưng amend nhanh hơn nhiều
- Không nên dùng
amend cho các commit đã được đẩy lên nhánh công khai/chia sẻ
- Nếu chỉ muốn đổi thông điệp của commit cuối cùng, hãy dùng lệnh sau
git commit --amend
# Làm theo hướng dẫn để thay đổi thông điệp commit
Khi commit vào nhánh sai
- Nếu bạn lỡ commit vào
master nhưng thực ra nó phải nằm trên một nhánh mới, hãy tạo nhánh mới từ trạng thái hiện tại rồi xóa commit cuối cùng khỏi master
git branch some-new-branch-name
git reset HEAD~ --hard
git checkout some-new-branch-name
- Cách này không phù hợp với các commit đã được đẩy lên nhánh công khai/chia sẻ
- Nếu trước đó bạn đã thử những cách khác, có thể cần dùng dạng
HEAD@{number-of-commits-back} thay cho HEAD~
- Nếu hoàn toàn commit vào sai nhánh, có thể chuyển nó bằng luồng
reset --soft, stash, checkout, stash pop
git reset HEAD~ --soft
git stash
git checkout name-of-the-correct-branch
git stash pop
git add . # hoặc thêm từng tệp riêng lẻ
git commit -m "your message here"
- Cũng có một cách khác trong cùng tình huống là dùng
cherry-pick
git checkout name-of-the-correct-branch
git cherry-pick master
git checkout master
git reset HEAD~ --hard
Hoàn tác thay đổi và các commit cũ
- Nếu bạn đã sửa tệp nhưng
git diff lại trống, có thể là bạn đã staging chúng, nên cần thêm cờ --staged
git diff --staged
- Để hoàn tác thay đổi của vài commit trước, hãy tìm hash bằng
git log rồi chạy git revert
git log
git revert [saved hash]
git revert sẽ tạo một commit mới để hoàn tác commit đó
- Nếu không muốn hoàn tác cả commit mà chỉ muốn hoàn tác một tệp cụ thể, hãy dùng quy trình đưa phiên bản cũ của tệp vào index rồi commit
git log
git checkout [saved hash] -- path/to/file
git commit -m "Wow, you don't have to copy-paste to undo"
Ép khôi phục theo trạng thái của kho lưu trữ từ xa
- Thay vì xóa kho lưu trữ rồi clone lại, bạn có thể dùng các lệnh Git để đưa nó về đúng trạng thái của kho lưu trữ từ xa
git fetch origin
git checkout master
git reset --hard origin/master
git clean -d --force
git reset --hard origin/master sẽ đưa nhánh về trạng thái của master trên remote
git clean -d --force sẽ xóa các tệp và thư mục chưa được theo dõi
- Cần lặp lại
checkout, reset, clean cho từng nhánh bị hỏng
- Những thao tác này mang tính phá hủy và không thể khôi phục, nên phải hết sức cẩn thận trước khi chạy
Phạm vi và đóng góp
- Tài liệu này không phải Git reference đầy đủ, mà là tập hợp các quy trình khôi phục thực tế được đúc kết qua quá trình thử và sai
- Nếu muốn giúp bổ sung bản dịch, bạn có thể gửi PR trên GitHub
1 bình luận
Ý kiến trên Hacker News
Có vài điểm tôi muốn thay đổi: luôn dùng
git switchthay vìgit checkout, và sẽ tránhreset --hardtối đaVí dụ, với trường hợp “lỡ commit vào master trong khi đáng ra commit đó phải nằm trên một nhánh mới”, có thể xử lý theo thứ tự
git branch some-new-branch-name,git switch -d HEAD~,git switch -C master,git switch some-new-branch-nameVới trường hợp “commit nhầm vào nhánh sai”, tôi cũng sẽ
git switchsang nhánh đúng rồi chạygit cherry-pick master,git switch -d master~,git switch -C masterVới “bỏ hết và quay về trạng thái remote”, tôi cũng sẽ làm kiểu
git fetch origin,git restore -WS .,git clean -d --force,git switch -d origin/master,git switch -C masterDù hiểu rõ mô hình Git, các công thức kiểu này vẫn không trực quan, và còn phải biết cả những hành vi đặc thù của từng lệnh
Chẳng hạn
git switch -c some-new-branch-nameđã tồn tại, nhưng một lệnh nhưgit move-branch master HEAD~để chỉ di chuyển nhánh sang commit khác có lẽ sẽ trực quan hơngit switchvàgit restorethay chogit checkout, nhưng các lệnh này được đưa vào từ Git v2.23, và bản phát hành đó đã khoảng 5 năm trướcTrang trợ giúp vẫn còn cảnh báo
THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.Tất nhiên cảnh báo này đã có gần như từ lúc lệnh mới xuất hiện, và sau 5 năm thì có vẻ đã đến lúc không cần gọi là thử nghiệm nữa
Dù vậy,
git checkouttương thích ngược tốt hơn, và có thể xem là phản ánh cách dùng Git vào thời điểm trang web này ban đầu được tạo rahttps://github.com/git/git/blob/757161efcca150a9a96b312d9e78...
https://github.com/git/git/releases/tag/v2.23.0
https://github.com/git/git/commit/4e43b7ff1ea4b6f16b93a432b6...
git add .làm mặc định khi đưa thay đổi vào staging area là không hayChỉ định rõ từng file để thêm sẽ ít để lại khả năng sau này phải thốt lên “ôi hỏng rồi” hơn
Tôi muốn biết vì sao
switchtốt hơncheckout, và vì sao không nên dùngreset --hardreset --hard” nghĩa là gìThực sự muốn biết vì sao nó chưa đủ tốt; tôi dùng nó khá thường xuyên cùng với
alias git-restore-file='git restore --source=HEAD --'và thấy có vẻ hoạt động ổnKhi học Git, nên bắt đầu khuyến nghị GUI client như lộ trình mặc định
Như vậy một phần ba các vấn đề kiểu này sẽ được giải quyết, và một phần ba khác thậm chí sẽ không phát sinh ngay từ đầu
Sau này nếu thấy dòng lệnh nhanh hơn thì hãy dùng, còn trước hết mọi người cần nhìn trực quan cách tương tác với cây
Cá nhân tôi thích fork.dev, nhưng các client hiện nay nhìn chung cũng tương tự nhau
Với Magit, các thao tác như “hủy cherry-pick” có thể được khám phá ngay trong giao diện, và dùng cùng phím tắt với các thao tác “hủy X” khác
Nếu chỉ được dùng dòng lệnh Git, có lẽ tôi đã chẳng biết phải bắt đầu từ đâu
Tôi cũng từng xóa nhầm một commit không được xóa trong lúc interactive rebase, nhưng mỗi lần bắt đầu rebase đều tạo một snapshot có thể truy cập từ reflog, nên việc hoàn tác một rebase sai khá an toàn
UI reflog của Magit giống UI log nên lần đầu nhìn cũng không bị lạc, nhưng nếu là Git CLI thì rất có thể tôi đã không hiểu chuyện gì đang xảy ra
Tôi thích việc chạy
jjmà không có đối số sẽ hiển thị phần liên quan của cây commit và vị trí hiện tạiCó thể nhìn một lượt commit đang làm việc, nhánh được gắn với nó và lịch sử, các nhánh đang hoạt động khác trong repository, thông điệp của từng commit và việc có thay đổi hay không, nên rất hữu ích để lấy lại phương hướng
Nhưng hầu như chẳng ai nghe, cuối cùng vật lộn với CLI, làm hỏng nhánh, tụt lại so với main và trở nên sợ rebase
Có rất nhiều việc trên UI chỉ cần kéo-thả hoặc cách tương tự là giải quyết được, nên nhìn giống một kiểu tự hành hạ bản thân
Dù giao diện có tệ, dùng cùng giao diện với những người khác trong nhóm sẽ dễ trao đổi hơn; tài liệu xử lý sự cố trên mạng nhìn chung cũng giả định “cách thông thường”, nên tận dụng được tốt hơn
Thật khá lạ khi nghĩ rằng đến tháng 4 năm 2025 là Git tròn 20 năm
Tôi từng ở đó vào thời điểm ấy, và còn nhớ đã băn khoăn nên học git, darcs hay bazaar để thay thế SVN hoặc CVS
Không rõ mình có thử Mercurial không nữa
Tôi tự hỏi liệu “hiệu ứng GitHub” có thực tế giết chết nhu cầu về những người mới tham gia trong lĩnh vực hệ thống quản lý phiên bản hay không
Có lẽ đến một lúc nào đó việc “cạo lông yak” cũng đã đủ rồi
Tôi đã chuyển hẳn sang jj, và vì có tương thích với Git nên không cần chờ người khác cùng chuyển
Tôi ngày càng thất vọng với nhiều khía cạnh của GitHub, nên hy vọng một ngày nào đó mọi người cũng vượt qua được nó, nhưng con đường phía đó có vẻ không đơn giản như vậy
SVN trực quan và chạy đủ tốt trong 99% trường hợp, đến mức không cần phải “dạy” riêng cho mọi người
Mong rằng tất cả chúng ta ngừng giả vờ làm hacker 1337 và thừa nhận rằng Git là công cụ quá mức cần thiết với đa số người dùng
Tôi nghĩ mình có mô hình tư duy khá tốt về việc Git làm gì, nhưng chỉ cần lệnh phức tạp hơn một chút là tôi không nhớ nổi tham số
Các lệnh không dễ tự khám phá, cũng không dễ nhớ
Tôi không biết là do UI văn bản tệ, hay bản thân việc mô tả thao tác trên cây bằng văn bản vốn đã khó
Thao tác cây phức tạp bằng văn bản không dễ, và UI văn bản thì khách quan mà nói là tệ: https://stevelosh.com/blog/2013/04/git-koans/
Rất vui khi thấy trong luồng này có kết quả tìm kiếm cho hg và Mercurial
Tôi vẫn không thể tin là Mercurial đã thua trong cuộc chiến hệ quản lý phiên bản phân tán, và tôi xem nó là công cụ tốt hơn
Nó dùng được, ổn định, đã được kiểm chứng thực tế và vẫn được hỗ trợ tích cực
Gần như là “hãy trở thành thay đổi mà bạn muốn thấy”, nên cứ dùng thôi
Cách nói rằng nó đã thua có tác dụng ngược rất lớn, và sự kìm hãm ngầm mà những lời như vậy tạo ra đang giữ tỷ lệ chấp nhận Mercurial ở mức thấp
Về hosting thì ít nhất có thể dùng Sourcehut và heptapod.host
Tôi cũng đang vận hành một instance Heptapod riêng, về cơ bản là một fork của Gitlab có hỗ trợ Mercurial trực tiếp, và nó cứ thế hoạt động tốt
Git là một trong những công nghệ rốt cuộc vẫn không vào đầu tôi được, vì ở nhiều mặt nó không theo trực giác
Nếu không dùng lâu năm, rất có khả năng gần như thao tác nào cũng phải tìm Google hoặc xem trang man
Đặc biệt sau khi trải qua nỗi kinh hoàng khi cố quản lý một codebase lớn trên SVN với nhiều branch rồi merge lại, sẽ thấy Git ở phần đó tốt hơn đến mức vô lý
Với người bắt đầu ngay từ Git thì đường cong học tập có thể lớn hơn nhiều, nhưng đôi khi tôi vẫn khó hiểu mọi người mắc kẹt ở đâu
Nếu dùng cách tích hợp sẵn là
filter-branchthì sau một khoảng chờ dài, nó hiện cảnh báo bảo bạn nhấn Control+C và tải về một script Python bên thứ ba tên làfilter-repoKhông tự hào gì, nhưng thao tác Git “hỏng rồi” mà tôi dùng thường xuyên nhất là xóa repository local, clone lại, rồi áp dụng lại các thay đổi
95% trường hợp cách này chạy rất tốt, phần còn lại thì tôi nhờ người phụ trách DevOps giúp
cp -R .git ../git-backupVì nếu làm hỏng quá nặng thì chỉ cần thay toàn bộ thư mục
.gitbằng bản sao cũ là đượcNhanh hơn nhiều so với việc tìm cách hoàn tác đúng một thao tác hay một nhóm thao tác cụ thể
Điều tôi muốn mọi người thật sự thấm về Git là Git là một kho dữ liệu chỉ-ghi-thêm
Muốn đưa công việc vào kho thì commit, và một khi đã vào rồi thì không thể xóa bằng lệnh Git
reflog cũng hoạt động theo nguyên lý này
Dù làm gì, bạn vẫn có thể quay về trạng thái branch trước đó và khôi phục công việc từ kho dữ liệu
Nhưng nếu không commit thì Git không thể giúp bạn, nên cứ commit đi
Luôn commit, và nếu thấy gánh nặng quá lớn hoặc cứ quên hoài thì nên sửa công cụ
Nếu viết code cả ngày, thông thường tối thiểu nên commit một lần mỗi giờ
Chuyện nhỏ thôi, nhưng mỗi lần nhìn
git reflogtôi lại thấy nó như re-flogCó lẽ vì thế mà tôi khó nhớ lệnh này, và dường như trong vô thức không nghĩ nó là “reference log”
Thay vào đó lại có cảm giác như đang bảo Git bán rẻ lại thứ gì đó
Tôi không phải người dùng Git, nhưng khi thấy những chuyện như thế này, tôi càng nghĩ rằng bản thân
git commitkhông có ý nghĩa; điều quan trọng chỉ là thời điểm commit được push hoặc mergeNó giống như việc lưu một file văn bản
Vì mỗi lần lưu file ta đâu có viết một thông điệp ngắn, nên cứ bỏ qua là được
Không phải viết ngắn; đôi khi thay đổi càng nhỏ thì phần giải thích lại càng dài
Tôi từng viết 2–3 đoạn văn cho một thay đổi 2–3 dòng có ảnh hưởng lớn
Commit cục bộ là cách tạo checkpoint tại thời điểm mọi thứ đều compile được và hoạt động
Nhờ vậy có thể stash thay đổi hoặc quay lại trạng thái còn chạy được khi làm hỏng
Trước khi push, nếu reset rồi chia thành các commit phù hợp thì lịch sử trên remote vẫn gọn gàng và không trở thành một đống
wipHoặc có thể squash merge PR để có được ưu điểm của cả hai phía
Tuy nhiên không cần nhập những message vô nghĩa như
more fixesThường thì tạo commit ban đầu kiểu
(WIP) tên tính năng, sau đó lặp lại amend commit cho đến khi hài lòng, rồi cuối cùng xóa(WIP)khỏi messageTôi xem log revision của Git cũng giống như nhật ký như vậy
Nó có tổ chức hơn một tài liệu TODO riêng, và khuyến khích giải thích những việc đã làm theo cách dễ đọc
Khi quyết định sẽ quay lui đến mức nào, việc biết lúc đó mình đã nghĩ gì trong commit ấy có thể hữu ích; nếu cách tiếp cận sai thì cũng giúp cho việc phân tích hậu kiểm
Nếu công việc chưa được merge hay push được sắp xếp thành các thay đổi riêng có gắn mục tiêu, thì khi một phần tốt và một phần xấu, việc chỉ giữ lại phần tốt sẽ dễ hơn nhiều
Dù vậy, về mặt kỹ thuật commit không phải là vô nghĩa
Không có commit thì cũng không có đối tượng để push hay merge
Về mặt phi kỹ thuật, việc ghi lại bằng văn bản thuần vì sao mình thay đổi điều gì đó cũng rất hữu ích
Nếu phải viết những giải thích như vậy quá thường xuyên, thì nên thu hẹp đơn vị “thay đổi”, hoặc chọn lọc kỹ hơn những việc đụng đến cùng lúc
Nếu không dùng Git, tôi tò mò không biết họ quản lý phiên bản và quản lý thay đổi bằng gì