-
1. 起步
-
2. Git 基礎
-
3. Git 分支
-
4. 伺服器上的 Git
- 4.1 協議
- 4.2 在伺服器上部署 Git
- 4.3 生成 SSH 公鑰
- 4.4 架設伺服器
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方託管服務
- 4.10 小結
-
5. 分散式 Git
-
A1. 附錄 A: Git 在其他環境
- A1.1 圖形介面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小結
-
A2. 附錄 B: 在應用程式中嵌入 Git
-
A3. 附錄 C: Git 命令
10.7 Git 內部原理 - 維護和資料恢復
維護和資料恢復
有時,您可能需要進行一些清理工作——使倉庫更緊湊,清理匯入的倉庫,或恢復丟失的工作。本節將介紹其中一些場景。
維護
偶爾,Git 會自動執行一個名為“auto gc”的命令。大多數時候,這個命令什麼也不做。但是,如果存在過多的孤立物件(不在 packfile 中的物件)或過多的 packfile,Git 就會啟動一個完整的 git gc 命令。“gc”代表垃圾回收,該命令會執行多項操作:它會收集所有孤立物件並將它們放入 packfile 中,它會合並 packfile 成一個大的 packfile,並刪除那些無法從任何提交中訪問且已存在數月的物件。
您可以手動執行 auto gc,如下所示:
$ git gc --auto
同樣,這通常什麼也不做。您必須擁有大約 7,000 個孤立物件或超過 50 個 packfile,Git 才會啟動真正的 gc 命令。您可以透過 gc.auto 和 gc.autopacklimit 配置設定分別修改這些限制。
gc 做的另一件事是將您的引用打包成一個單獨的檔案。假設您的倉庫包含以下分支和標籤:
$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1
如果您執行 git gc,您將不再在 refs 目錄下看到這些檔案。為了效率,Git 會將它們移動到一個名為 .git/packed-refs 的檔案中,該檔案看起來像這樣:
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9
如果您更新一個引用,Git 不會編輯此檔案,而是會寫一個新檔案到 refs/heads。為了獲取給定引用的正確 SHA-1,Git 會先在 refs 目錄中查詢該引用,然後以 packed-refs 檔案作為備用。因此,如果您在 refs 目錄中找不到某個引用,它很可能在您的 packed-refs 檔案中。
請注意檔案中的最後一行,它以 ^ 開頭。這表示上面的標籤是一個帶註釋的標籤,而這一行是帶註釋的標籤所指向的提交。
資料恢復
在您的 Git 之旅的某個時刻,您可能會意外丟失一個提交。通常,這會發生在您強制刪除一個包含工作的分支,但後來又發現您仍然需要該分支;或者您進行了硬重置(hard-reset)到一個分支,從而放棄了您想要從中獲取內容的提交。假設這種情況發生了,您如何找回您的提交?
下面是一個示例,演示瞭如何在一個測試倉庫中將 master 分支硬重置到一箇舊提交,然後恢復丟失的提交。首先,讓我們回顧一下此時您的倉庫狀態:
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
現在,將 master 分支移回中間那個提交:
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef Third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
您實際上已經丟失了頂部的兩個提交——沒有任何分支可以訪問到這些提交。您需要找到最新的提交 SHA-1,然後新增一個指向它的分支。訣竅在於找到那個最新的提交 SHA-1——您不可能記住它,對吧?
通常,最快的方法是使用一個名為 git reflog 的工具。當您工作時,Git 會在每次更改 HEAD 時默默地記錄下 HEAD 的位置。每次提交或切換分支時,reflog 都會更新。git update-ref 命令也會更新 reflog,這也是為什麼建議使用它而不是直接將 SHA-1 值寫入 ref 檔案(如我們在 Git 引用 中所講)。您可以透過執行 git reflog 檢視您任何時候的活動記錄:
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: Modify repo.rb a bit
484a592 HEAD@{2}: commit: Create repo.rb
在這裡,我們可以看到我們檢出過的兩個提交,但這裡的資訊不多。為了以更有用的方式檢視相同的資訊,我們可以執行 git log -g,它會為您提供 reflog 的正常日誌輸出。
$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:22:37 2009 -0700
Third commit
commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
Modify repo.rb a bit
看起來底部那個提交是您丟失的那個,因此您可以透過在該提交上建立一個新分支來恢復它。例如,您可以從該提交 (ab1afef) 開始建立一個名為 recover-branch 的分支:
$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
很棒——現在您有一個名為 recover-branch 的分支,它指向您 master 分支曾經所在的位置,從而使前兩個提交再次可訪問。接下來,假設您的丟失由於某種原因不在 reflog 中——您可以透過刪除 recover-branch 並刪除 reflog 來模擬這種情況。現在前兩個提交無法透過任何方式訪問:
$ git branch -D recover-branch
$ rm -Rf .git/logs/
由於 reflog 資料儲存在 .git/logs/ 目錄中,您實際上沒有 reflog。此時您如何恢復該提交?一種方法是使用 git fsck 工具,該工具會檢查您的資料庫的完整性。如果您使用 --full 選項執行它,它將顯示所有未被其他物件指向的物件:
$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
在這種情況下,您可以在“dangling commit”字串後看到您丟失的提交。您可以透過同樣的方式恢復它,即新增一個指向該 SHA-1 的分支。
刪除物件
Git 有很多優點,但一個可能引起問題的特性是 git clone 會下載專案的整個歷史記錄,包括每個檔案的每個版本。如果整個專案都是原始碼,這沒關係,因為 Git 經過高度最佳化,可以有效地壓縮這些資料。然而,如果您的專案歷史中某處添加了一個巨大的檔案,那麼所有未來的克隆都將被迫下載這個大檔案,即使它在下一個提交中就被從專案中刪除了。因為它仍然可以從歷史記錄中訪問到,所以它將永遠存在。
當您將 Subversion 或 Perforce 倉庫轉換為 Git 時,這可能是一個巨大的問題。由於在那些系統中您不需要下載整個歷史記錄,這種新增幾乎沒有後果。如果您從另一個系統匯入,或者發現您的倉庫比預期的要大得多,以下是如何查詢和刪除大物件的說明。
警告:此技術會破壞您的提交歷史。 它會重寫自您需要修改的第一個樹物件以來直到最新版本的所有提交物件,以移除大檔案引用。如果您在匯入後立即執行此操作,在任何人基於該提交開展工作之前,您就沒事了——否則,您必須通知所有貢獻者他們必須將他們的工作重新基礎(rebase)到您的新提交上。
為了演示,您將在您的測試倉庫中新增一個大檔案,並在下一個提交中刪除它,然後找到它並永久地從倉庫中刪除。首先,向您的歷史記錄中新增一個大物件:
$ curl -L https://kernel.linux.club.tw/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'Add git tarball'
[master 7b30847] Add git tarball
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git.tgz
糟糕——您不想將一個巨大的 tarball 新增到您的專案中。最好將其刪除:
$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'Oops - remove large tarball'
[master dadf725] Oops - remove large tarball
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git.tgz
現在,gc 您的資料庫並檢視您使用了多少空間:
$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)
您可以執行 count-objects 命令快速檢視您使用了多少空間:
$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0
size-pack 條目是您的 packfile 的大小(以千位元組為單位),所以您使用了近 5MB。在上一個提交之前,您使用的空間接近 2KB——很明顯,從上一個提交中刪除檔案並沒有將其從您的歷史記錄中移除。每次有人克隆此倉庫時,他們都必須克隆全部 5MB 才能獲得這個小型專案,因為您不小心添加了一個大檔案。讓我們將其刪除。
首先您必須找到它。在這種情況下,您已經知道是哪個檔案了。但假設您不知道;您將如何確定哪個檔案或哪些檔案佔用瞭如此大的空間?如果您執行 git gc,所有物件都會在一個 packfile 中;您可以透過執行另一個名為 git verify-pack 的底層命令,並按輸出的第三個欄位(即檔案大小)排序來識別大物件。您也可以透過 tail 命令管道化它,因為您只對最後幾個最大的檔案感興趣:
$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
| sort -k 3 -n \
| tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438
底部的大物件:5MB。要找出它是什麼檔案,您將使用 rev-list 命令,您在 強制特定的提交訊息格式 中簡要使用過它。如果您向 rev-list 傳遞 --objects,它將列出所有提交 SHA-1 以及 blob SHA-1 和與之關聯的檔案路徑。您可以使用它來找到您的 blob 名稱:
$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
現在,您需要從過去的所有樹中刪除此檔案。您可以輕鬆地看到哪些提交修改了此檔案:
$ git log --oneline --branches -- git.tgz
dadf725 Oops - remove large tarball
7b30847 Add git tarball
您必須重寫 7b30847 之後的所有提交,才能將此檔案完全從您的 Git 歷史記錄中刪除。為此,您將使用 filter-branch,您在 重寫歷史記錄 中使用過它:
$ git filter-branch --index-filter \
'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten
--index-filter 選項類似於我們在 重寫歷史記錄 中使用的 --tree-filter 選項,不同之處在於,它不傳遞一個修改磁碟上籤出文件的命令,而是每次修改您的暫存區或索引。
與其使用 rm file 這樣的命令刪除特定檔案,不如使用 git rm --cached——您必須從索引中刪除它,而不是從磁碟上刪除。這樣做是因為速度——Git 不需要先將每個版本檢出到磁碟再執行您的過濾器,這樣可以快得多。如果您願意,也可以使用 --tree-filter 完成相同的任務。git rm 的 --ignore-unmatch 選項告訴它,如果找不到要刪除的模式,不要報錯。最後,您要求 filter-branch 只從 7b30847 提交向上重寫您的歷史記錄,因為您知道問題是從那裡開始的。否則,它將從頭開始,並且不必要地花費更長時間。
您的歷史記錄不再包含對該檔案的引用。但是,您的 reflog 和 Git 在您執行 filter-branch 時在 .git/refs/original 下新增的新一組引用仍然包含它,所以您必須刪除它們,然後重新打包資料庫。在重新打包之前,您需要刪除任何指向那些舊提交的引用:
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)
讓我們看看您節省了多少空間。
$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0
打包倉庫的大小已降至 8KB,這比 5MB 好得多。從 size 值可以看出,大物件仍然在您的孤立物件中,所以它還沒有消失;但它不會在推送或後續克隆時被傳輸,而這才是重要的。如果您真的想,可以透過執行 git prune 和 --expire 選項來完全刪除該物件:
$ git prune --expire now
$ git count-objects -v
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0