[譯] 使用 `-force` 被認為是有害的;瞭解 Git 的 `-force-with-lease` 命令

LeviDing發表於2019-03-04

Git 的 push --force 具有破壞性,因為它無條件地覆蓋遠端儲存庫,無論你在本地擁有什麼。使用這個命令,可能覆蓋團隊成員在此期間推送的所有更改。然而,有一個更好的辦法,當你需要強制推送,但仍需確保不覆蓋其他人的工作時,-force-with-lease 這條指令選項可以幫助到你。

我不經常使用 push --force...
我不經常使用 push --force...

眾所周知,git 的 push -force 指令是不推薦被使用的,因為它會破壞其他已經提交到共享庫的內容。雖然這不總是完全致命的(如果那些修改的內容仍在某些同事的本地工作域中,那之後他們能被重新合併),但是這樣的做法很欠考慮,最糟糕的情況會造成災難性的損失。這是因為 --force 指令選項迫使分支的頭指標指向你個人的修改記錄,而忽略了那些其他和你同時進行地更改。

強制推動最常見的原因之一是當我們被迫 rebase 一個分支的時候。為了說明這一點,我們來看一個例子。我們有一個專案,其中有一個功能分支,Alice 和 Bob 要同時在這個分支上工作。他們都 git clone... 了這個倉庫,並開始工作。

最初,Alice 完成了她負責的功能,並將其 push 到主倉庫。這都沒啥問題。

Bob 也完成了他的工作,但在 push 之前,他注意到一些變化已被合併到了 master 分支。想要保持一棵整潔的工作樹,他會對主分支執行一個 rebase。當然,當他 push 這個經過 rebase 的分支的時候將被拒絕。然而,Bob 沒有意識到 Alice 已經 push 了她的工作。Bob 執行了 push --force 命令。不幸的是,這將清除 Alice 在遠端主倉庫的所有更改和記錄。

這裡的問題是,進行強制推送的 Bob 不知道為什麼他的 push 會被拒絕,所以他認為這是 rebase 造成的,而不是由於 Alice 的變化。這就是為什麼 --force 在同一個分支上協作的時候要杜絕的;並且通過遠端主倉庫的工作流程,任何分支都可以被共享。

但是 --force 有一個不為眾人所知的親戚,它在一定程度上能防止強制更新操作帶來的結構性破壞;它就是 --force-with-lease

--force-with-lease 是用於拒絕更新一個分支,除非該分支達到我們期望的狀態。即沒有人在上游更新分支內容。 實際上,通過檢查上游引用是我們所期望的,因為引用是雜湊,並將父系鏈隱含地編碼成它們的值。

你可以告訴 --force-with-lease 究竟要檢查什麼,預設情況下會檢查當前的遠端引用。這在實踐中意味著,當 Alice 更新她的分支並將其推送到遠端倉庫時,分支的引用指標將被更新。現在,除非 Bob從遠端倉庫 pull 一下,否則本地對遠端倉庫的引用將過期。當他使用 --force-with-lease 推送時,git 會檢查本地與遠端的引用是否對應,並拒絕 Bob 的強制推送。--force-with-lease 有效地只在沒有人在上游更新分支內容的時候允許你強制推送。就像是一個帶有安全帶的 --force。它的一個快速演示可能有助於說明這一點:

Alice 已經對該分支進行了一些更改,並已推送到了遠端主倉庫。Bob 現在又對遠端倉庫的 master 分支進行了 rebases 操作:

ssmith$ 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複製程式碼

rebase 之後,他試圖將自己的更改 push 上去,但伺服器拒絕了,因為這會覆蓋 Alice 的工作:

ssmith$ 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.複製程式碼

但 Bob 認為這是 rebase 操作造成的,並決定強制 push

ssmith$ git push --force
To /tmp/repo
 + f82f59e...c27aff1 dev -> dev (forced update)複製程式碼

然而,如果他使用了 --force-with-lease,則會得到不同的結果,因為 git 會檢查遠端分支,發現 從上一次 Bob 使用 fetch 到現在,實際上並沒有被更新:

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

當然,在這有一些關於 git 的注意事項。上面展示的,只有當 Alice 已經將其更改推送到遠端儲存庫時,它才有效。這不是一個嚴重的問題,但是如果她想修改她提交的東西,那她去 pull 分支時,會被提示合併被更改。

一個更微妙的問題是,我們有方法去騙 git,讓 git 認為這個分支沒有被修改。在正常使用情況下,最常發生這種現象的情況是,Bob 使用 git fetch 而不是git pull`來更新他的本地副本。fetch將從遠端倉庫拉出物件和引用,但沒有匹配的merge則不會更新工作樹。這將使本地倉庫看起來已經與遠端倉庫進行了同步更新,但實際上本地倉庫並沒有進行更新,並欺騙--force-with-lease` 命令,成功覆蓋遠端分支,就像下面這個例子:

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

ssmith$ 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

ssmith$ 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複製程式碼

這個問題的最簡單的答案就是,簡單的說“不要在沒有合併的情況下 fetch 遠端該分支”(或者更常用的方法是 pull,這個操作包含了前面的兩個),但是如果由於某種原因你希望在用 --force-with-lease 進行程式碼上傳之前進行 fetch,那麼這有一種比較安全的方法。像 git 那麼多的屬性一樣,引用只是物件的指標,所以我們可以建立我們自己的引用。在這種情況下,我們可以在進行 fetch 之前,為遠端倉庫引用建立“儲存點”的副本。然後,我們可以告訴 --force-with-lease 將此作為引用值,而不是已經更新的遠端引用。

為了做到這一點,我們使用 git 的 update-ref 功能來建立一個新的引用,以儲存遠端倉庫在任何 rebasefetch 操作前的狀態。這有效地標記了我們開始強制 push 到遠端的工作節點。在這裡,我們將遠端分支 dev 的狀態儲存到一個名為 dev-pre-rebase 的新引用中:

ssmith$ git update-ref refs/dev-pre-rebase refs/remotes/origin/dev複製程式碼

這時呢,我們就可以進行 rebasefetch 操作,然後使用儲存的 ref 來保護遠端倉庫,以防有人在工作時做了更改:

ssmith$ 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

ssmith$ 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

ssmith$ 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'複製程式碼

我們可以看到 --force-with-lease 對於有時需要進行強制推送的 git 使用者來說,是一個很有用的工具。但是,對於 --force 操作的所有風險來說,這並不是萬能的,如果不瞭解它內部的工作及其注意事項,就不應該使用它。

但是,在最常見的用例中,開發人員只要按照正常的方式進行 pullpush 操作即可。偶爾使用下 rebase,這個命令提供了一些我們非常需要的,防止強制推送帶來破壞的保護功能。因此,我希望在未來版本的 git(但可能 3.0 以前都不會實現),它將成為 --force 的預設行為,並且當前的行為將被降級到顯示其實際行為的選項中,例如:--force-replace-remote


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章