Đừng sợ việc mất code với Git
Khi làm việc với Git, chúng ta thường sợ gặp phải những trường hợp mất code do việc xử lý merge, reset branches hoặc interactive rebase. Nhiều người thậm chí còn sử dụng các hosting file service để lưu trữ code của mình nhằm phòng ngừa việc mất code này, khiến cho việc sử dụng git mất đi ý nghĩa ...
Khi làm việc với Git, chúng ta thường sợ gặp phải những trường hợp mất code do việc xử lý merge, reset branches hoặc interactive rebase. Nhiều người thậm chí còn sử dụng các hosting file service để lưu trữ code của mình nhằm phòng ngừa việc mất code này, khiến cho việc sử dụng git mất đi ý nghĩa của nó. Hơn thế nữa, nó còn khiến cho họ không sử dụng những tính năng là điểm mạnh của Git. Bài viết này sẽ sẽ giúp các bạn hiểu phần nào về cách mà Git lưu trữ code của mình, và vì sao mà code của bạn không bị mất.
Trước tiên, ta cần nắm về toàn cảnh của Git:
-
Nếu bạn muốn Git lưu trữ code của mình, bạn cần commit hoặc stash code
-
Với thiết lập mặc định, mọi thứ bạn commit sẽ được lưu giữ trong 30 ngày, đối với stash là 2 tuần.
-
Merge và Rebase luôn luôn an toàn.
Commit trong Git
"Nếu bạn muốn Git lưu trữ code của mình, bạn cần commit hoặc stash code". Đây là một trong những điều căn bản đầu tiên mà chúng ta được học khi bắt đầu với Git. Tuy nhiên, nó không chỉ đơn giản như vậy. Khi mà bạn commit hay stash code của mình, Git sẽ bắt đầu theo dõi nó và gán cho nó một đoạn hash SHA-1 lên những đoạn code được thêm mới hay bị thay đổi. Nếu bạn có thể access được vào những đoạn SHA-1 này, bạn có thể access được những đoạn code được thêm mới hay thay đổi ở trên bất cứ lúc nào, trước khi nó bị xóa bởi Garbage collection của Git. Mặc định là 30 ngày đối với những commit mà không thể trỏ tới (commit không thuộc bất cứ branch nào, bao gồm các commit được ammend, reset hoặc bị thay đổi khi rebase), đối với loose object (code được stash) là 2 tuần.
Tìm lại những đoạn code đã mất
Bây giờ khi đã biết được các đoạn code đã được stash và commits không hề bị mất đi (ít nhất là trong 2 tuần), điều bạn muốn biết lúc này là làm sao để tìm được nó. Git cung cấp một công cụ đó là git reflog. Tuy nhiên, với mỗi trường hợp thì ta cần phải xủ lý một cách khác nhau.
Đầu tiên là với trường hợp commit --ammend. Giả sử bạn vừa commit một đoạn code nào đó:
$ git show commit c044e7f486f8078bdeb2dd6418c446c5ddf8960c author: ... date: ... Add the work diff --git a/my-work.rb b/my-work.rb new file mode 100644 index 0000000..edfb476 --- /dev/null +++ b/my-work.rb @@ -0,0 +1,6 @@ +a = 1 +b = 2 + +c = a + b
sau đó, bạn lại thêm thay đổi và ammend vào commit trên:
$ git commit -a --amend $ git show commit c044e7f486f8078bdeb2dd6418c446c5ddf8960c author: ... date: ... Add the work diff --git a/my-work.rb b/my-work.rb new file mode 100644 index 0000000..edfb476 --- /dev/null +++ b/my-work.rb @@ -0,0 +1,7 @@ +a = 1 +b = 3 +d = 5 + +c = a + b + d
Nhìn vào đoạn thay đổi trên, ta thấy là giá trị của b bị thay đổi. Vậy làm thế nào để biết được sự thay đổi này ? Với trường hợp thông thường thì ta có thể dùng git diff để xem thay đổi. Tuy nhiên, nó chỉ xem được sự thay đổi giữa các commit với nhau. Ở đây chỉ có một commit duy nhất (do đã bị ammend) nên nếu muốn xem sự thay đổi này, ta cần tìm được SHA nguyên bản của commit ammend trên.
Git cung cấp một công cụ là git reflog. reflog sẽ liệt kê một danh sách các sự thay đổi trong Git (thay đổi về HEAD, tên branch, remote). Bạn có thể tưởng tượng nó giống như một timeline của các hành động của bạn trong Git. Bây giờ ta thử chạy nó cùng với ví dụ ở trên:
$ git reflog 530fd81 HEAD@{0}: commit (amend): Add new work c044e7f HEAD@{1}: commit (initial): Add new work
Các bạn có thể thấy 2 hành động commit đều có một mã SHA của riêng mình. Nó còn chứa các thông báo cho thấy commit nào là được amend vào. Bây giờ, các bạn có thể chạy bất cứ lệnh Git nào với các đoạn mã SHA này. Nếu muốn xem giá trị nguyên bản của b, bạn có thể chạy lệnh:
$ git show c044e7f commit c044e7f486f8078bdeb2dd6418c446c5ddf8960c Author: ... Date: ... Add new work diff --git a/work.rb b/work.rb new file mode 100644 index 0000000..3880234 --- /dev/null +++ b/work.rb @@ -0,0 +1,4 @@ +a = 1 +b = 2 + +c = a + b
Nếu bạn muốn tách amend commit ra thành 2 commit khác nhau, bạn có thể làm như sau:
$ git reset c044e7f (HEAD@{1}) $ git commit -am "New commit message"
reflog còn giúp bạn khôi phục lại một thao tác xử lý rebase lỗi nữa. Giờ ta tạo một branch mới dựa vào amend commit ở ví dụ trên và thêm vào một số thay đổi:
$ git show feature/add-good-stuff commit e5ce995850fbfd9def402aab01ef63f2f5bb0835 Author: ... Date: ... Doing good stuff diff --git a/work.rb b/work.rb index 2f924b5..17084c7 100644 --- a/work.rb +++ b/work.rb @@ -1,5 +1,6 @@ a = 1 b = 3 d = 5 +e = 10 -c = a + b + d +c = a + b + d + e
Cùng lúc này, bên master cũng có một vài sự thay đổi:
$ git show master commit 74ffee41b359c2896d3d413226f88fa938771a7e Author: ... Date: ... Add some vars diff --git a/work.rb b/work.rb index 2f924b5..b2d3a33 100644 --- a/work.rb +++ b/work.rb @@ -1,5 +1,7 @@ a = 1 b = 2 d = 3 +e = 7 +f = 9 -c = a + b + d +c = a + b + d + e + f
Khi rebase thì ta sẽ phải xử lý một số conflict vì cả bên master và branch mới đều động đến biến c và e. Khi xử lý conflict thì ta có thể mắc lỗi nào đó và sửa sai giá trị của e.
$ git checkout feature/add-good-stuff $ git rebase master // resolve some conflicts $ git show commit 7e229ab6e096d55bf323b9d3f2d1d8a8068f4fdf Author: ... Date: ... Add some vars diff --git a/work.rb b/work.rb index 2f924b5..b2d3a33 100644 --- a/work.rb +++ b/work.rb @@ -1,5 +1,7 @@ a = 1 b = 2 d = 3 +e = 7 +f = 9 -c = a + b + d +c = a + b + d + e + f
Các bạn có thể thấy là giá trị của e bây giờ là giá trị do bên master thiết lập. Nếu muốn xem lại giá trị của e do nhánh feature mới thiết lập, ta có thể sử dụng reflog:
$ git reflog 7e229ab HEAD@{0}: rebase finished: returning to refs/heads/feature/add-good-stuff 7e229ab HEAD@{1}: rebase: Doing good stuff. bef95f5 HEAD@{2}: rebase: checkout master b6401ca HEAD@{3}: checkout: moving from master to feature/add-good-stuff
Ở đây các bạn có thể thấy theo thứ tự từ trên xuống dưới là các SHA đại diện cho các bước rebase. SHA cuối cùng đại diện cho commit trước khi thực hiện việc rebase. Từ đây ta có thể access lại những đoạn code thay đổi khi làm với nhánh feature hoặc reset lại toàn bộ quá trình rebase trên:
git reset --hard b6401ca
Git fsck
reflog có thể giúp các bạn với những thay đổi tương đối gần thời điểm hiện tại, tuy nhiên với những trường hợp ammend hay rebase đươc thực hiện trước đó một khoảng thời gian dài (vài tuần) thì việc sử dụng reflog sẽ rất tôn công. Git fsck là công cụ giúp các bạn lọc các commit mà không được gán vào đâu cả. Các bạn có thể thử nó với repo ở ví dụ trên:
$ git fsck --no-reflogs dangling commit 402a7a91bdff5ec08fca9a3deee37488b4193d61 dangling blob 7e81179a9573fc2ff43a428cf60e396cc5308618 dangling commit c044e7f486f8078bdeb2dd6418c446c5ddf8960c dangling blob 4a7402af39a69b2aba55089e154aabfa8abeca33 dangling commit 530fd810c3c20b66d7ffce9251884537e48985f4 dangling commit 9beed278668ec1278b44993ac6e6620450276ba5 dangling blob e4d7da50985d9f399251016db4147c999f788e5b dangling commit e5ce995850fbfd9def402aab01ef63f2f5bb0835 dangling commit 7484f0b01534e5ceee986aee8439d5173d8c4afc dangling commit 74ffee41b359c2896d3d413226f88fa938771a7e dangling commit 745ab8da2637e8cd5a53030f1df842503d4f9b20 dangling commit b6401ca23d5621d0fc1030e06dde38a493e64da8 dangling blob fc2ee485a900043f127a5788952ef3befafaaa4d dangling blob bdcbb1b3c6bf2257b77c95c0468112b8a9bb1c27 dangling commit 3f573997f6145765130e976f3464731b8ae810b1
Mỗi commit trong list trên đại diện cho commit khi rebase, amend hoặc các sự thay đổi khác. Flag --no-reflog đảm bảo rằng các bạn sẽ thấy các commit mà vẫn còn tồn tại trong reflog. Từ đây, ta có thể lọc ra các SHA và chạy các command trên chúng để lấy thêm thông tin:
// lấy các thông tin cơ bản của một commit (message, author, email) $ git fsck --no-reflogs | grep commit | cut -d ' ' -f3 | xargs git --no-pager log --no-walk // lấy các file thay đổi trong từng commit $ git fsck --no-reflogs | grep commit | cut -d ' ' -f3 | xargs git --no-pager show --name-status // tìm commit mà thay đổi một file bất kì $ git fsck --no-reflogs | grep commit | cut -d ' ' -f3 | xargs -J @ git log --no-walk --name-status @ -- path/to/file
Đối với git stash, các bạn chỉ cần thêm WIP on vào flag grep của git:
git fsck --no-reflogs | grep commit | cut -d ' ' -f3 | xargs git log --no-walk --grep="WIP on"
References
Don’t Fear the Rebase: Git Garbage Collection and You