+19

Mẹo nhỏ để tránh làm mất code khi sử dụng Git.

TL;DR: Không sử dụng push -f hay push --force ! Thay vào đó nên sử dụng push --force-with-lease

Giải thích:

Git push --force rất nguy hiểm, vì theo cơ chế của mình, nó sẽ ghi đè lên remote repo bằng code ở local của mình, mà không cần quan tâm đến việc bên phía remote đang chứa thứ gì. Vì vậy, sẽ rất dễ để làm mất code trên phía remote repo, kể cả các code mà các thành viên khác đã push lên trước đó. Thay vào đó, ta có một công cụ tốt hơn: sử dụng option --force-with-lease khi push sẽ giúp ta trong trường hợp cần phải force push code lên remote, nhưng vẫn đảm bảo không làm mất những code đã có trước đó.

Giải thích kỹ hơn 1 chút:

Cần thiết phải nhắc lại 1 lần nữa: việc sử dụng git push -f hay git push --force là hoàn toàn không được khuyến khích, vì nó sẽ xóa bỏ hoàn toàn các commit khác đã được push lên một repository chung. (Thực ra thì vẫn có cách khôi phục, nếu như các commit kia vẫn còn ở trong máy của người push chúng).

Một trong những trường hợp dễ gặp nhất ở đây , đó là khi ta cần thực hiện việc rebase một nhánh. Để dễ hiểu, hãy xét ví dụ dưới đây:

  • Giả sử ta có 1 project với một nhánh feature đang làm, tạm gọi là nhánh feature.
  • 2 người A và B cùng thực hiện công việc trên nhánh này, họ cùng clone nhánh về máy mà bắt đầu code.
  • A hoàn thành code của mình, push code lên remote.
  • B hoàn thành code của mình, nhưng trước khi push code lên, B cũng đã kiểm tra và thấy rằng có gì đó đã được merge lên nhánh master từ trước rồi. Vì thế, trước khi push code mình lên, anh ấy thực hiện việc rebase nhánh feature của mình với master.
  • Đây là lúc vấn đề xảy ra: sau khi rebase xong, B sẽ push lại nhánh feature của mình lên, và lúc này git sẽ báo reject, buộc B phải push --force. Tuy nhiên, vì không biết rằng A cũng đã push code của mình lên feature trước đó, thế nên lúc này việc B push -f lên sẽ xóa toàn bộ commit của A trước đó đã đẩy lên.

Nhìn sâu hơn, khi thực hiện push -f, B không thực sự biết trước đó vì sao code của mình lại bị reject khi push: trong trường hợp này, B đã nghĩ việc bị reject là do mình rebase, chứ không nghĩ tới việc là do A đã push lên từ trước.

Thay vào đó, hãy xét tới trường hợp sử dụng option --force-with-lease khi push - option này sẽ phần nào bảo vệ bạn khỏi việc lỡ tay làm mất code khi force update 😃

Khi sử dụng push --force-with-lease, git sẽ từ chối việc update lên branch trừ khi branch đó nằm trong trạng thái "được coi là an toàn" (vd. không có code nào khác được đẩy lên đồng thời trước đó ...). Trong thực tế, việc này được thực hiện bằng cách kiểm tra tới uptream ref.

Bạn có thể bảo --force-with-lease là nó cần check cái gì (mình sẽ nói thêm ở dưới); còn nếu không thì mặc định nó sẽ kiểm tra theo tham chiếu (ref) hiện tại phía remote. Nói 1 cách dễ hiểu, đó là khi A udpate code của mình và push nó lên remote repo, tham chiếu đến head của nhánh remote lúc này sẽ được update theo. Bây giờ, khi B push lên remote, thì tham chiếu tại local của B đã bị lỗi thời. Khi B thực hiện --force-with-lease, git sẽ kiểm tra local ref với nhánh remote và từ chối thực hiện push.

Giải thích kỹ hơn nữa - thực sự thì --force-with-lease nó làm gì ?

Cùng xét lại quá trình trên từ đầu:

  • A code xong phần của mình, và push lên remote repo. Cùng lúc đó, B thực hiện rebase feature trong máy mình với master:
B$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Dev commit #1
Applying: Dev commit #2
Applying: Dev commit #3
  • Sau khi rebase, B đẩy code của mình lên, server lúc này sẽ reject:
B$ git push
To /tmp/repo
 ! [rejected]        dev -> dev (fetch first)
error: failed to push some refs to '/tmp/repo'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
  • B cho rằng việc bị reject là do rebase với master trước đó, và thực hiện push -f lên:
B$ git push --force
To /tmp/repo
 + f82f59e...c27aff1 dev -> dev (forced update)

^ Như thế là code của A đã hoàn toàn đi tong 😃 Thay vào đó, nếu như B thực hiện push --force-with-lease, git sẽ kiểm tra xem remote branch có từng được update hay không kể từ lần cuối B fetch nó về:

B$ git push -n --force-with-lease
To /tmp/repo
 ! [rejected]        dev -> dev (stale info)
error: failed to push some refs to '/tmp/repo'

--force-with-lease có phải đã an toàn ?

Tuy nhiên, việc sử dụng --force-with-lease vẫn có thể sót trường hợp, ví dụ như khi git lầm tưởng rằng một remote branch chưa được update, trong khi thực ra là có rồi. Phổ biến nhất là khi B sử dụng git fetch thay cho git pull để update local repo của mình. Câu lệnh fetch sẽ pull các object và ref từ remote về, nhưng lại không thực hiện việc merge để update thứ vừa fetch về với working tree hiện tại. Điều này sẽ làm cho local repo trông có vẻ là đã up to date với remote rồi, nhưng thực ra là chưa; lúc này thì đến lượt --force-with-lease sẽ override lại remote branch:

B$ git push --force-with-lease
To /tmp/repo
 ! [rejected]        dev -> dev (stale info)
error: failed to push some refs to '/tmp/repo'

B$ git fetch
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /tmp/repo
   1a3a03f..d7cda55  dev        -> origin/dev

B$ git push --force-with-lease
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (9/9), 845 bytes | 0 bytes/s, done.
Total 9 (delta 0), reused 0 (delta 0)
To /tmp/repo
   d7cda55..b57fc84  dev -> dev

Câu trả lời dễ nhất cho vấn đề này, đó là "Đừng fetch mà không merge" (tốt hơn là thực hiện lệnh pull - tương đương với fetch + merge). Nhưng nếu vì lý do gì đó mà bạn muốn thực hiện fetch trước khi push --force-with-lease, vẫn có một cách an toàn để thực hiện nó. Nhớ lại rằng ref chỉ là pointer chỉ tới các object - ta có thể tự tạo ra chúng. Trong trường hợp này, ta có thể tạo các save-point trước khi thực hiện fetch. Sau đó, ta sử dụng --force-with-lease với các ref save-point này thay cho remote ref.

  • Trước khi thực hiện rebase hay fetch, sử dụng update-ref để tạo một tham chiếu mới, lưu lại state remote trong local. Vd. ta lưu state của remote branch vào một ref gọi là dev-pre-rebase:
B$ git update-ref refs/dev-pre-rebase refs/remotes/origin/dev
  • Sử dụng ref này khi thực hiện rebase, fetch để bảo vệ remote repo trong trường hợp ai đó đã push code mới lên:
B$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Dev commit #1
Applying: Dev commit #2
Applying: Dev commit #3

B$ git fetch
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /tmp/repo
   2203121..a9a35b3  dev        -> origin/dev

B$ git push --force-with-lease=dev:refs/dev-pre-rebase
To /tmp/repo
 ! [rejected]        dev -> dev (stale info)
error: failed to push some refs to '/tmp/repo'

Tóm lại

Như đã thấy, --force-with-lease là một công cụ hữu ích cho người dùng git khi cần force update. Nhưng nó không cover hết các risk thay cho sử dụng push -f, và tốt nhất là không nên sử dụng push --force hay cả -push --force-with-lease trước khi thực sự hiểu chúng là gì và các risk khi sử dụng. Tuy nhiên, trong hầu hết các use case hằng ngày, khi mà developer chỉ thực hiện push hay pull như bình thường, thỉnh thoảng có rebase, thì --force-with-lease cũng đã cung cấp một mức độ bảo vệ cao hơn đối với code của ta 😃

Nguồn:

https://developer.atlassian.com/blog/2015/04/force-with-lease/


All rights reserved

Bình luận

Đăng nhập để bình luận
Avatar
@bongdem_master
thg 10 8, 2018 8:50 SA

Thực ra có 1 biện pháp để khắc phục của 2 loại force push, hay việc "Đừng fetch mà không merge", và tất cả những vấn đề về git khác nữa...

Đó là học thật kỹ git command trước khi dùng nó. Mọi người khi chuyển từ svn sang git, thường với tâm thế nghĩ nó chỉ như nhau, nhưng thực tế làm với git phải học rất rất rất nhiều, y như việc học 1 ngôn ngữ lập trình mới vậy. Nếu chịu đầu tư thời gian nghiên cứu kỹ nó, thì không có gì phải lo cả, cả force push, ... fetch thoải mái. Cá nhân mình cũng thường fetch chứ không pull ngay, cá nhân mình không đồng ý quan điểm đừng fetch mà không merge, đơn giản vì nếu ai đó không hiểu lý do tại sao làm như thế, họ sẽ chỉ có pull và pull y như học vẹt, và nếu 2 người làm chung branch mà chỉ có pull và pull thì sao, dĩ nhiên sẽ xảy ra hiện tượng self-merge, tự merge vào nhau nhìn rất xấu git history. Mình recommend là mỗi người developer, cần nắm rõ git, các thao tác nâng cao rebase, reset, fix history... nhằm làm đẹp git history trước khi push lên, chứ không chỉ là những thao tác basic như commit, pull, push..

Chả có câu chuyện gì gọi là "mất code" cả, đơn giản là những commit đó sẽ rơi vào trạng thái unreference/unreachable và không hiển thị trên git history mà thôi, code vẫn nằm ở trên server git ấy, trừ phi bạn để nó chạy git gabage collection

Avatar
@bongdem_master
thg 10 9, 2018 3:59 SA

Mình xin bổ sung thêm 1 số thông tin để làm rõ comment trên của mình nhé, có thể nhiều bạn sẽ chưa hiểu.

Đầu tiên, git là 1 tool commandline, các tool GUI cho git là vô cùng nhiều, tortoise git, sourcetree, smartgit, gitkraken, tower, ..... nhưng chừng nào các bạn còn phù thuộc 100% vào tool GUI này để giao tiếp với git, thì các bạn sẽ không bao giờ cảm thấy tự tin mỗi khi làm các thao tác với git, đặc biệt là các thao tác reset, rebase, ... và rất nhiều. Bạn sẽ luôn trong trạng thái lo lắng, sợ mất code, sợ hỏng git history, sợ không biết cách fix git... nên các bạn chỉ dám dùng các thao tác như commit, pull, push, fetch... chứ hiếm khi dùng rebase interactive, reset, và cũng giống như trên, không dám force push... Để trở nên tự tin, biết được những gì mình làm 100% chạy đúng, và khi gặp rắc rối với git, tự tin 100% rằng sẽ fix được, các bạn buộc phải học git commandline, không có con đường nào khác. Đơn giản vì git commandline giúp bạn hiểu rõ bản chất vấn đề, hiểu rõ ngọn ngành cách thức các command git hoạt động như thế nào, bạn control được nó, bạn dư sức đương đầu với nó mỗi khi nó trục trặc. Cá nhân mình từng phải đi fix git cho member khá nhiều, nên mình tuyệt đối bắt buộc member phải nằm lòng git, để member tự giải quyết vấn đề, không ai hơi đâu đi fix mãi giùm được. Và mỗi khi bạn change history, nếu member đã lỡ tay fetch/pull mớ history cũ trước đó, bạn sẽ phải chạy sang máy member để đồng bộ lại history. Best practice đối với mình: Bạn nên dùng kết hợp cả 2 GUI và commandline, để có thể đạt được hiệu quả tốt nhất, vì mỗi cái đều có ưu nhược.

  • GUI: Git history có màu, nhìn trực quan, dễ nhìn hơn, và việc compare local change, các thao tác fetch, pull, commit, push không mất nhiều thời gian.
  • CommandLine: Dùng để dùng các thao tác nâng cao - đa số là GUI không hỗ trợ những thao tác này: git shallow clone, git bisect, git reflog, git fsck, git merge-base, git show, git rebase, ....

Câu chuyện thứ nhất: Người ta bảo bạn hạn chế force push là vì họ sợ bạn không làm đúng quy trình, force push tùy tiện mất commit của đồng nghiệp, nhưng có khá nhiều tình huống bạn thật sự phải force push. Ví dụ: Mình giả sử branch có mỗi 1 mình bạn làm, mà bạn cũng không được force push ư? Dĩ nhiên bạn cứ force push thoải mái, có mình bạn làm chứ có đồng nghiệp nào nữa đâu mà sợ mất commit đồng nghiệp. Git cho phép ta refactor history để giúp history trông đẹp hơn, dễ track issue hơn, nhưng sau khi refactor ta sẽ phải force push nốt. Đấy, có khá nhiều case force push nên force push đâu có gì sai, chẳng qua là cách ta sử dụng sai -> cách giải quyết không phải là không được force push, mà là khi nào nên force push nhé. Vậy quy trình đúng cho force push là gì? Đấy, nó rơi vào cách case fetch, git cho phép ta commit code dưới local để sẵn, khi cần chỉ việc push thôi, nhưng trước khi push thì ta cần phải fetch, fetch để "thấy" những change trên server nhưng nó không merge, để ta tùy hứng tìm cách giải quyết (chọn self-merge hoặc rebase). Lúc này cách tốt hơn là ta cứ fetch, rồi sau đó rebase, git history sẽ đẹp hơn, gọn gàng. Bạn cứ thử nghĩ, member mà cứ chỉ biết pull pull và pull, nó self-merge tùm lum và cái git history như 1 đống bùi nhùi, bạn sẽ gần như không thể track được cái git history nữa đâu. Đặc điểm nhận dạng của self-merge commit là có message pattern như sau: "Merge branch 'origin/develop' to 'develop' Tham khảo hình git history dưới này nhé, bạn có thấy nó dễ nhìn không, self-merge đó 😄 lXtZnV2.png

Có thể bạn nghĩ git commit rồi thôi, history mặc kệ, nhưng hãy hình dung khi bạn có 1 con bug, bạn không biết nó xuất phát từ commit nào, thì lúc này bạn ước gì cái git history nó dễ nhìn, để bạn có thể dò tìm ra commit mà đã sinh ra bug 1 cách dễ hơn. Git có 1 tool để giúp tìm bug, gọi là git bisect, cái này các bạn tự tìm hiểu, điều kiện dùng nó là git history phải thật "clean" 😄

Câu chuyện thứ 2: Như mình đã nói là git không hề mất commit khi bạn thực hiện các thao tác change history (reset, rebase, git amend commit...). Git commit, git stash, git blob (blob là những thứ mà bạn đã check staged cho nó nhưng bạn chưa commit)... đều được gọi với 1 cái tên chung là git object, các object này được lưu trong thư mục .git/objects. Khi bạn change history, bạn nghĩ rằng commit đã bị xóa, vì bạn không nhìn thấy nó trên history, nhưng thực chất nó vẫn còn tồn tại trong chính thư mục git của bạn. Bạn có thể tìm được commit này thông qua 1 số command sau:

  • Git reflog: Cấp độ cơ bản, có thể tìm các commit không có trong history (trạng thái unreference)
  • Git fsck: Cấp độ nâng cao hơn, tìm được các commit không có trong history (trạng thái unreachable), tìm luôn được blob Cứ khoảng 1 thời gian nào đó, hoặc thư mục git object bị phình dung lượng, nó sẽ chạy lệnh git gc (gabage collection) và xóa bớt item trong thư mục object đi, đến lúc này mới thật sự được xem là commit, blob của bạn đã mất hoàn toàn và không có cơ hội khôi phục lại.

Câu chuyện thứ 3: Bạn muốn học thêm 1 tool CI như Jenkins, ... hoặc đại loại 1 tool CI nào khác, bạn cần phải nằm lòng feature của git là git hook, nó là thứ trigger để giúp bạn mỗi khi commit nó sẽ call đến tool CI để tự build. Cái này là các file cấu hình, bạn phải học thêm 1 loại script language để implement nó, ví dụ shell bash, python, ....

Avatar
+19
Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí