Nếu đã bấm vào đây thì chắc bạn cũng là người biết sử dụng Git nhỉ, còn nếu không thì kệ bạn, lêu lêu bạn có thể ghé qua xem video git crash course này của mr. Brad. Nhân tiện, đây là một kênh dạy lập trình rất hay trên Youtube mà bạn cũng nên có trong list subscribe của mình, tin mình đi, đóng tiền mạng rồi vào xem mấy video ấy không phí tiền đâu .

Đó giờ nếu đã từng làm việc với Git kể cả là pet project, team project hay là dự án lớn hơn thì chắc đâu đó bạn cũng đã từng rớt vào vài trường hợp kiểu như: chetme commit dư file rồi, mano quên discard file này ra,… đại loại vậy. Và đương nhiên chúng ta vẫn sẽ có cách thần kì nào đó để fix thôi, thường thì các IDE hay text editor đã có hỗ trợ sẵn cho bạn cả rồi (như VSCode chẳn hạn, quá tiện lợi). Nhưng vẫn sẽ có một kiểu người khác (như mình ) khá là thích mò mẩm mọi việc trên terminal thay vì click click, từ file manager có thể làm với ranger, code với vim thì đương nhiên git cũng không nằm ngoài ngoại lệ (đây không phải là cổ xúy mà nó là sở thích, và dĩ nhiên sở thích có thể bị thay đổi theo thời gian thôi . Tuy nhiên mình sẽ dùng VSCode để demo trong bài này, vì nó có cây git trông hịn hò hơn, don’t judge me ). Hay thực tế hơn, nếu bạn không phải là terminal-guy thì bạn vẫn có thể gặp vấn đề là phải gõ git trên terminal khi SSH lên server thôi (đào đâu ra GUI bây giờ ). Đó cũng là lý do mà mình ở đây để luyên thuyên về một số trường hợp dở dở ương ương mà đôi khi chúng ta gặp phải và thứ chúng ta cần là một thao tác nào đó như là Ctrl + Z ở trên Git vậy. Hi vọng là sẽ cứu cánh bạn trong một vài trường hợp, hoặc vô dụng hơn thì cũng thông tin tới ai chưa biết để mà cầm đi chém gió ngang dọc cũng được.

Lưu ý đầu bài

Đáng lẽ thì vào đọc bài luôn cũng được, nhưng đây là phần mà mình sẽ nói sơ qua một số khái niệm mình dùng ở phía dưới để nhỡ ai chưa biết thì có thể xem qua để đọc mượt mà hơn. Nếu bạn đang mong chờ thêm phần giải thích chuyên sâu về mấy phần lưu ý này thì xin lỗi là nó không có ở đây rồi (hoặc là đợi mình viết sau ). Đây chỉ là tóm tắt lại cho bạn hiểu sơ thôi nhé:

Và lưu ý là staging area thì khác working tree nhé (đến cả cái tên chúng nó còn không thèm giống nhau). Những file nằm trong staging area là những file sẽ được commit tới repository nhưng nằm trong working tree thì không.

Lỡ commit vội quá nên thiếu file

Thử tưởng tượng, hôm đó là một hôm chiều thứ 6 và đã gần 5 giờ, tức có nghĩa là bạn sắp hết giờ làm và có 2 ngày nghỉ phè thân trước mắt, xúc động với tương lai sắp đến bạn vội vã finish nốt cái task be bé được anh Leader assign cho từ lúc đầu giờ chiều và thực hiện vài thao tác quen thuộc đến mức không cần suy nghĩ.

git add .
git commit -m "add more bugs to fix ._."

Thế là xong task rồi, chuẩn bị hí hửng ra lướt facebook nốt mấy phút còn lại rồi về thì lại chợt nhớ chetme còn một file chưa fix, rồi chưa xóa console.log trong 2-3 file khác nữa mà lỡ commit mất rồi. Không lẽ giờ lại làm thêm một cái commit nữa rồi viết commit message kiểu.

git commit -m "Em add thiếu file và quên chưa xóa console.log ^^"

Không được! Hay là cứ commit thêm 1 cái rồi squash 2 commit lại thành 1 là xong, nghe hay đấy và nó cũng giải quyết được vấn đề nữa. Nhưng, chi cho cực vậy vì giờ đây đó giờ còn tồn tại một command gọi là git commit --amend nữa bạn tôi à.

--amend là một tính năng khác trong git, nôm na là nó sẽ lấy các file strong staging area của bạn, kết hợp với những file trong lần commit cuối cùng và sẽ thay đổi lại commit cuối của bạn, commit mới đó sẽ bao gồm những file bạn vừa mới edit và cả các file đã commit trước đó luôn (nếu cả trong lần commit cuối cùng và lần này đều có chung một file bị thay đổi thì ưu tiên lấy cái mới nhất nhé), thêm nữa là chúng ta có thể thay đổi commit message lại cho phù hợp hơn nếu bạn muốn.

Đấy, áp dụng vào tình huống trên của chúng ta thì dễ thôi:

Dong dài vậy thôi chứ chung quy lại thì chỉ cần nhớ git commit --amend là được nhé

Lỡ add hết file bằng git add . giờ bỏ bớt ra như nào?

Hi vọng đây là chuyện không của riêng ai để cho mình bớt nhục, vì cái thói luôn tay làm xong một task hay gì đó thì sẽ nhắm mắt gõ ngay git add . và tiếp theo là git status để nhìn hết đống file xanh lè vừa được stage trong sự mãn nguyện, một thói quen mà mình làm gần như vô thức . Sau đó, lại lọ mọ dò lại một lượt thì thấy một lố file thay đổi rồi add vào nhưng chả giúp ích được cái méo gì cả (file thì dăm ba dòng console.log còn file thì comment lại đống code bỏ đi và cá chắc là sẽ không dùng đến trong 100 năm nữa).

Thế là vấn đề ở đây, làm sao gỡ mấy của nợ đó ra, chứ ai lại commit lên mấy cái vớ vẩn vậy được. Dễ ẹt, nhanh tay mở VSCode lên chọn unstage changes là xong, qua phần này nhé.

Không, đùa đấy (nhưng thừa nhận là nhanh đúng không, chỉ có mở VSCode là lâu thôi =]] ). Nếu đang dở dang trên terminal thì bạn có thể nhanh tay gõ luôn git reset HEAD <file cần unstage> là xong. Thao tác này cũng giống như nút unstage changes của VSCode, nó sẽ đưa file mà bạn đã chọn từ staging area lui ngược về lại working tree (và đương nhiên là lui về đây rồi thì nó sẽ không được commit lên repository nữa).

Trông hay đúng không, nhưng mà lỡ chúng ta thêm vào staging area tận 100 file thì không lẽ nào gõ cái lệnh chết dẫm đó 100 lần à. Đương nhiên là không, để loại bỏ tất cả các file bạn đã thêm vào staging area thì chỉ cần đơn giản là gõ git reset (ừ, chỉ git reset thôi), lệnh này thay vì chở một file về lại working tree thì nó sẽ chuyển toàn bộ file từ staging area về working tree luôn (đúng, tất cả ). Giải quyết xong việc unstage file nhé.

Reset file về trạng thái như lúc ban đầu như thế nào

Một tình huống khác, khi chúng ta hí hửng pull code mới nhất về để tiếp tục phát triển thêm tính năng mới chẳng hạn. Thế là bạn bắt đầu công cuộc mò mẩm và sáng tạo bug các kiểu. Rồi sau một quãng thời gian vật lộn trong một hay vài file nào đó, bạn chợt nhận ra chân lý mới, tốt hơn là để file đó y như cũ hay là tẩy sạch cái đống lùm xùm mà bạn vừa thêm ngang thêm dọc vào và đưa file về lại như mới đầu để code theo cách khác có khi còn hay hơn chẳng hạn.

Cách giải quyết nào, chỉ cần mở VSCode lên và chúng ta sẽ có một lệnh rất quen thuộc mà mỗi khi bạn gõ git status lên thì nó cũng đã hướng dẫn bạn rồi, tuy nhiên thì vẫn cứ nhắc lại, đó là

git checkout -- <file cần loại bỏ>

Trước hết là không nhầm lẫn với git checkout <tên branch> (để di chuyển qua các branch) với cái này nhé. Và lại phải nhớ thêm cái nữa là phải có hai dấu này -- khoảng cách ra với tên file nữa. Cho một ví dụ chẳng hạn:

git checkout -- mySubDir/test.txt

Vậy công dụng của nó là gì? Đơn giản là sau khi gõ lệnh này với file được chọn thì nó sẽ loại bỏ tất cả các thay đổi mà bạn đã thao tác trên file đó (như thêm vài dòng, xóa vài dòng hay sửa mấy chữ) và reset file đó về với tình trạng như lúc chưa đụng vào (chính xác hơn thì là nó sẽ thay đổi file hiện tại của bạn (đã có chỉnh sửa) thành file đó nhưng ở lần commit cuối cùng - gọi ngắn hơn thì có thể là reset file đó về lại lần commit cuối cùng).

Và dĩ nhiên chúng ta vẫn có thể làm thao tác này với tất cả các file mà chúng ta đã thay đổi, chứ không cần phải lặp lại lệnh trên làm gì. Để làm như vậy thì đơn giản là thay vì chúng ta gõ git checkout -- <file cần loại bỏ> thì giờ chúng ta sẽ thay bằng

git checkout -- . # chấm ở đây là kiểu tất cả ấy

Vậy là xong, mọi thay đổi sẽ bay sạch và trả lại cho bạn một working tree clean như ban đầu lần cuối commit.

Tuy nhiên, tuy nhiên (nhắc lại cái nữa cho nhấn mạnh) bạn cần phải nhớ một điều rằng đây không phải là lệnh “an toàn” để bạn có thể dùng trong trường hợp bạn không biết mình đang làm cái vẹo gì cả . Vì như đã nói lệnh này sẽ làm bay hết tất các những thay đổi của bạn và những thay đổi này thì chỉ là đang ở local mà thôi. Tại sao lại nói như vậy? Mọi thứ mà đã được commit trong git rồi thì đều có thể khôi phục lại theo cách này hoặc cách khác. Kể cả là commit đó có bị xóa đi, commit đó nằm ở trên một branch cổ xưa nào đó và cũng bị xóa đi mất hay thân quen hơn là commit cũ của chúng ta bị đè lên bởi một commit khác bằng --amend thì vẫn có cách cứu chữa nốt. Thế nhưng, những thay đổi mà chúng ta đang làm dang dở trên working tree và rồi thổi bay nó đi bằng git checkout -- thì không có cách cứu chữa đâu, hành động “tẩy rửa” này là vé một chiều đấy, vậy nên cân nhắc khi chúng ta sử dụng lệnh này nhé.

git commit --amend thì hay đấy, nhưng tôi vẫn muốn undo luôn commit lại thì được không

Được chứ! được hơn là chỉ undo một commit nữa là đằng khác. Đôi khi bạn vẫn có thể dính vào trường hợp này đó là không muốn commit cái này nữa hay vài lý do đặc biệt nào đó mà bạn thật sự muốn undo lại cái commit mà mình vừa nhấn enter không lâu (hay lâu hơn cũng được, văn vở thế thôi). Lại nhắc lại cái nữa, vì thế mà mình ở đây .

Rồi, vậy những thứ mà bạn cần biết và nhớ là gì? Không phải là một lệnh mới nào đâu, mà là một lệnh cũ với cách sử dụng mới (cái này dễ chết hơn ), git reset (dĩ nhiên là còn râu ria phía sau, chứ không phải lệnh git reset không như ở trên kia). Lệnh git reset này làm được nhiều cái ma thuật lắm, nhưng trong phạm bài này thì mình sẽ giới thiệu thêm một phép nữa của nó ở đây là undo lại commit thôi, form của nó trong việc undo commit sẽ là như này:

git reset <mode> <commit>

Demo chút xíu xem sao. Như bạn thấy ở gif dưới, mình vừa commit xong thì gõ phát nữa nó đã undo cái commit đó lại (để ý cái cây git ấy).

Undo git commit demo

Giải thích sơ bộ về cục ở trên nè:

Vậy thì lệnh này nó làm cái gì, nó sẽ reset HEAD của branch mà bạn đang ở lùi về đúng cái commit mà bạn đã chọn (hay hiểu nôm na theo title đoạn này là nó sẽ undo bạn về đúng với commit mà bạn đã chọn, nhờ đó bạn có thể lùi nhiều hơn là 1 commit ). Và trong quá trình reset này thì có thể nó sẽ thay đổi cả staging area hoặc working tree hoặc là cả hai hoặc là không làm gì cả, tất cả phụ thuộc vào mode mà bạn chọn. Định nghĩa theo doc của git reset <mode> <commit>:

This form resets the current branch head to <commit> and possibly updates the index (resetting it to the tree of ) and the working tree depending on <mode>. If <mode> is omitted, defaults to --mixed.

Nếu ai chưa biết HEAD là gì thì có thể hiểu đơn giản HEAD như là một con trỏ đang trỏ vào commit cuối cùng của branch mà bạn đang ở. Và khi bạn checkout qua một branch khác thì HEAD sẽ được thay đổi và chuyển sang trỏ vào commit cuối cùng của branch ấy, chúng ta có thể gọi ngắn gọn HEAD là branch hiện tại cũng được

English version từ nguồn

HEAD is a reference to the last commit in the currently check-out branch.

You can think of the HEAD as the “current branch”. When you switch branches with git checkout, the HEAD revision changes to point to the tip of the new branch.

Sau khi đã hiểu sơ sơ về cách hoạt động của việc undo commit theo cách này thì tiếp theo mình sẽ dắt bạn đi vào trong từng mode để xem nó tròn méo như thế nào, ráng đọc tiếp nhé .

--soft mode

Đây là mode đầu tiên mà mình muốn nói đến cũng như là mode mà mình khuyên nên sử dụng vì độ an toàn của nó. Trước khi giải thích các nó hoạt động như thế nào, thì có lẽ chúng nên ghé qua một ví dụ nhỏ để thấy sơ bộ trước.

Thử nhìn xuống hình dưới và chú ý cho mình 2 commit mới nhất

Sau cùng đây là cây git của chúng ta sau 2 commit trên: git reset --soft example 1

Tiếp theo, chúng ta sẽ thử chỉnh sửa một tí ở cả hai file test-onetest-two

git reset --soft example 1 git reset --soft example 1 git reset --soft example 1

Bây giờ mình sẽ giả vờ là mình đã thao tác sai cái khỉ gì đó và cần undo lại commit add file test-one & test-two (tức quay lại commit lúc hai file này còn là file rỗng), vậy có làm được không? Đương nhiên là được, đã nói ở trên rồi còn gì. Rồi vậy làm như nào? Trước tiên chúng ta cần nhớ lại công thức lúc nãy xem chúng ta cần gõ gì rồi ráp vô (như làm toán vậy):

git reset <mode> <commit>

Ô kê, vì chúng ta đang thực hành với mode là --soft và cái commit id mà chúng ta cần quay lại là 9dd2e411. Nhìn vào chỗ này nè: git reset --soft example 1 Nên đây là thứ mà chúng ta cần gõ vào terminal:

git reset --soft 9dd2e411

Để xem xem, sau khi enter thì chúng ta có gì: git reset --soft example 1 Như mong muốn, chúng ta đã undo ngược về commit 9dd2e411 thành công, bởi vì ta thấy HEAD của chúng ta giờ đã lùi lại dòng chữ add file test-one & test-two trên git tree rồi chứ không còn ở chỗ cũ nữa. Tuy nhiên, tiếp theo là nhìn vào git status, trước khi chúng ta reset thì chúng ta có stage test-one vào staging area còn test-two thì không, sau reset xem ra staging area vẫn còn nguyên trạng thái cũ, vậy nó có thực sự là nguyên vẹn hay là không? Xem thử hình dưới nào: git reset --soft example 1 git reset --soft example 1

Có khác đấy, nếu như ở trước lúc chúng ta reset thì test-one của chúng ta thay đổi từ hello world -> hello universe (kéo lên trên xem lại nào), nhưng bây giờ nhìn xem, nó đã đổi sang từ rỗng -> hello universe các ông ạ (test-two thì không cần phải nói).

Khi chúng ta undo về một commit nào đó bằng git reset --soft (undo cách bao nhiêu commit cũng được) thì việc trước tiên nó sẽ làm đó là reset HEAD của chúng ta lùi về đúng với cái commit đó cái đã. Tiếp theo là xem xét xem có tác động tới working treestaging area hay không, tuy nhiên với mode --soft thì nó sẽ không đá động gì đến cả hai nơi này, giống như trong trường hợp ở trên của chúng ta thì test-one được stage với hello world còn test-two thay đổi thành git is cool và không được thêm vào staging area, tất cả đều được giữ nguyên hết. Chỉ khác ở chỗ, thay vì trước khi reset, những thay đổi của chúng ta là từ commit add text ‘hello world’ to test-one thì sau khi reset nó sẽ là thay đổi từ commit add file test-one & test-two sang. Thêm một điều nữa là nếu như ở commit mà chúng ta undo về có commit thay đổi của một file khác nữa (trong trường hợp này là khác 2 file test-onetest-two, như README chẳng hạn) thì thay đổi của file đó sẽ tự động được thêm vào staging area luôn (viết tới đây thì mới nhớ là nãy mình làm biếng quên setup cho ví dụ đó ). Bạn có thể đọc lại định nghĩa gốc của --soft trên git doc:

Does not touch the index file or the working tree at all (but resets the head to , just like all modes do). This leaves all your changed files “Changes to be committed”, as git status would put it.

--hard mode

Nghe tên thì biết nó hard core ra sao rồi . Nhưng cá nhân mình thì khuyến khích sử dụng cái này thường xuyên cho lắm, trừ khi bạn biết rõ là mình đang làm gì và backup mọi thứ ok. Về cơ bản thì cách hoạt động của --hard cũng khá tương đồng với --soft. Có nghĩa là khi chúng ta dùng git reset --hard thì việc lùi HEAD cũng y chang như --soft.

Nhưng, có một điều chúng ta cần lưu ý khi sử dụng nó và cũng chính là lý do mà mình bảo là không khuyến khích sử dụng --hard. Trái với --soft khi chúng ta reset về thì mọi thay đổi của chúng ta đều được giữ nguyên, thì với --hard khi chúng ta reset về một commit nào đó, mọi thay đổi của chúng ta trên working treestaging area đều bị mất đi (mất thật đấy ).

Đây là định nghĩa theo git doc

Resets the index and working tree. Any changes to tracked files in the working tree since are discarded.

Lấy lại ví dụ những thay đổi chúng ta đã làm với test-onetest-two, trước khi reset sẽ là như này, 3s trước thảm họa: git reset --soft example 1

Gõ lại lệnh reset nhưng lần này mode sẽ là --hard

git reset --hard 9dd2e411

Và đây là kết quả: git reset --soft example 1 Git status trống trơn luôn

Như mình đã nói ở trên HEAD cũng đã được reset về đúng nơi mà nó nên ở, tuy nhiên cả working tree (thay đổi file test-two) và staging area (file test-one) cũng bay màu nốt. Vì thế cẩn thận ghi gõ --hard nếu như bạn vừa sáng tạo ra một thuật toán siêu cấp nào đó nhưng lại quên chưa commit nhé .

--mixed mode

Mode thứ cuối mình muốn nhắc đến đó là --mixed, như đã nói ở đâu đó trên kia thì --mixed là mod mặc định của lệnh git reset thế nên nếu như chúng ta muốn xài mode này thì chỉ cần gõ git reset <commit> là xong. Mode này thì không “nguy hiểm” như --hard hay “an toàn” như --soft, nó kiểu giữa giữa, lưỡng tính ấy.

Tại sao mình lại kêu nó lưỡng tính? Vì khi chúng ta reset commit bằng --mixed thì đương nhiên trước hết là lùi HEAD về commit đã chọn như bao mode khác, nhưng với --mixed, nó sẽ tác động lên các file ở staging area còn working tree thì không. Nhưng tác động lên staging area thì cụ thể nó làm gì? Nó không thổi bay luôn mấy thay đổi đó như --hard đâu, đừng lo. Mà thay vào đó --mixed vẫn giữ các thay đổi mà chúng ta đã thao tác trên các file ở staging area nhưng nó sẽ đưa tất cả các file ở staging area về lại working tree, các file đã ở working tree sẵn rồi thì không bị làm sao cả, thêm một cái nữa nếu ta lùi về commit có file bị thay đổi khác với các file chúng ta đang thay đổi hiện giờ thì file đó cũng bị đưa về working tree nốt.

Giả sử chúng ta có cây git như thế này, đại khái thì nó vẫn tương tự ví dụ cũ thôi, nhưng thay vào đó mình chèn thêm 2 commit phía trước, có thêm một file nữa là test-three và thêm text vào file đó nữa. git reset --soft example 1

Những thay đổi còn lại thì cũng y chang ví dụ trên:

  • test-one mình sẽ đổi hello world -> hello universe sau đó mình stage nó luôn
  • test-two mình sẽ thêm đoạn text git is cool vào nhưng không stage.

Nếu bây giờ chúng ta thử reset về commit add file test-three (tức nghĩa là lúc này file test-onetest-two vẫn đang rỗng, nhưng trước đó chúng ta lại có một thay đổi ở file test-three rồi) thì cùng để xem nó như nào.

Gõ thôi:

git reset 449b7530

Và đây là kết quả của chúng ta: git reset --soft example 1

Chúng ta đã lùi commit thành công, tuy nhiên thông báo các file thay đổi có vẻ hơi lạ, có cả sự xuất hiện của file test-three mặc dù trước đó chúng ta chỉ thay đổi hai file test-onetest-two . Như đã nói ở trên thì: thêm một cái nữa nếu ta lùi về commit có file bị thay đổi khác với các file chúng ta đang thay đổi hiện giờ thì file đó cũng bị đưa về working tree nốt. Vậy nên khi reset commit này chúng ta sẽ có luôn sự thay đổi của test-three và dĩ nhiên là nó sẽ được đặt ở working tree. git reset --soft example 1

Bonus

Nếu như bạn thấy việc tìm commit id để undo về là quá cực nhọc và bạn không thích dùng nó, trong khi bạn có một trí nhớ thượng thừa có thể nhớ hết những lần mình commit (hay được hơn là bạn vẫn mỡ git tree lên đếm số lần commit chứ ghét ứ xài commit id thì vẫn hoang hô ). Thì bạn có thể dùng

git reset <mode> HEAD~<số commit muốn lùi> # nhớ chỗ này là số nha 1,2,3,... đồ ấy

Lấy luôn ví dụ cuối ở cái --mixed phía trên, thay vì chúng ta gõ git reset 449b7530 gì gì đó thì có thể đổi sang thành:

git reset HEAD~2

Trông hay hơn nhỉ


Rồi, bài cũng dài vãi chưởng rồi, hi vọng đọc xong thì mình có thể giúp bạn được ở khoản nào đó trong quá trình sử dụng Git. Tuy nhiên cứ code cần thận để không phải undo như này thì tuyệt nhất, cứ xem nó là cách chữa cháy thôi nhé. Vậy nha, chào thân ái và quyết liệt .