4 điểm bởi GN⁺ 2025-01-18 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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 --hardgit 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

 
GN⁺ 2025-01-18
Ý kiến trên Hacker News
  • Có vài điểm tôi muốn thay đổi: luôn dùng git switch thay vì git checkout, và sẽ tránh reset --hard tối đa
    Ví 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-name
    Với trường hợp “commit nhầm vào nhánh sai”, tôi cũng sẽ git switch sang nhánh đúng rồi chạy git cherry-pick master, git switch -d master~, git switch -C master
    Vớ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 master

    • Mô hình nội bộ của Git rất đẹp với blob, cây commit, con trỏ commit, nhưng khoảng cách với giao diện dòng lệnh thì quá lớn
      Dù 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ơn
    • Tôi không có ý phản đối hướng dùng git switchgit restore thay cho git 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ước
      Trang 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 checkout tươ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 ra
      https://github.com/git/git/blob/757161efcca150a9a96b312d9e78...
      https://github.com/git/git/releases/tag/v2.23.0
      https://github.com/git/git/commit/4e43b7ff1ea4b6f16b93a432b6...
    • Việc dạy git add . làm mặc định khi đưa thay đổi vào staging area là không hay
      Chỉ đị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 tò mò cơ sở của việc khuyến nghị cách này là gì
      Tôi muốn biết vì sao switch tốt hơn checkout, và vì sao không nên dùng reset --hard
    • Tôi không hiểu “tránh reset --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 ổn
  • Khi 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

    • Tôi đồng ý rằng GUI thường mang lại trải nghiệm người dùng tốt hơn cho Git
      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 đã bắt đầu khuyến nghị jj như một lựa chọn thay thế tương thích với Git
      Tôi thích việc chạy jj mà 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ại
      Có 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
    • Với người mới học Git, tôi luôn khuyên họ tìm UI phù hợp với mình và dùng nó thay vì dòng lệnh
      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
    • Nếu cần nhìn trực quan cách tương tác với cây, có một hướng dẫn tương tác từng giúp ích cho đồng nghiệp của tôi: https://learngitbranching.js.org/
    • Theo kinh nghiệm trước đây của tôi với các frontend GUI cho quản lý phiên bản, khi mọi thứ chạy trơn tru thì ổn, nhưng một khi có chuyện rối cần dọn dẹp thì rất khó để ai đó giúp
      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

    • Hiệu ứng GitHub, giống như mọi hiệu ứng mạng lưới, là có thật, nhưng điều đó không có nghĩa là không thể cải thiện
      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 lúc nào cũng hoạt động tốt
      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
    • Bất kỳ cải tiến nào tối thiểu cũng sẽ phải tương thích với Git, ví dụ như jj
  • 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ó

  • 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

    • Ngay cả theo tiêu chuẩn hiện nay, Mercurial chắc chắn vẫ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
    • Nghe thú vị đấy, nhưng không biết có link GitHub không
    • Mercurial là công cụ chậm hơn
    • Nếu cần hosting Mercurial thì có thể dùng dịch vụ này: https://hg.reactionary.software/
  • 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

    • Tôi bắt đầu với SVN rồi chuyển sang Git, nên đúng là phải học các khác biệt, nhưng cảm thấy ở nhiều mặt mọi thứ đã dễ hơn
      Đặ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
    • Hôm qua tôi đã cố xóa một file khỏi lịch sử
      Nếu dùng cách tích hợp sẵn là filter-branch thì 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-repo
  • Khô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

    • Tôi đã dùng Git gần 15 năm, và cũng từng hai lần tạo chương trình/sản phẩm dùng Git bên trong để tạo ra kết quả cụ thể, nhưng trước khi làm việc gì hơi mạo hiểm trong Git, đôi khi tôi vẫn làm kiểu cp -R .git ../git-backup
      Vì nếu làm hỏng quá nặng thì chỉ cần thay toàn bộ thư mục .git bằng bản sao cũ là được
      Nhanh 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ể
    • Bạn không đơn độc: https://xkcd.com/1597/
  • Đ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 reflog tôi lại thấy nó như re-flog
    Có 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 commit không có ý nghĩa; điều quan trọng chỉ là thời điểm commit được push hoặc merge
    Nó 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

    • Nếu đó là thay đổi có được sau một hành trình debug vất vả và có khả năng sau này người khác sẽ tham khảo lại, thì hãy viết message
      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
    • Có vẻ quá rõ ràng rằng commit cục bộ và commit trên remote có mục đích khác nhau
      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 wip
      Hoặc có thể squash merge PR để có được ưu điểm của cả hai phía
    • Commit rất quan trọng vì đó là thao tác khiến Git tạo bản sao file ở bên trong
      Tuy nhiên không cần nhập những message vô nghĩa như more fixes
      Thườ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 message
    • Tuy so sánh với việc lưu file văn bản, nhưng cũng có nhiều người mỗi lần đi ngủ lại viết một đoạn ngắn về một ngày của mình
      Tô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
    • Trước khi push thì tất cả đều là cục bộ, nên muốn làm gì cũng được
      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ì