# 維護與數據恢復
有的時候,你需要對倉庫進行清理 - 使它的結構變得更緊湊,或是對導入的倉庫進行清理,或是恢復丟失的內容。 這個小節將會介紹這些情況中的一部分。
## 維護
Git 會不定時地自動運行一個叫做 “auto gc” 的命令。 大多數時候,這個命令并不會產生效果。 然而,如果有太多松散對象(不在包文件中的對象)或者太多包文件,Git 會運行一個完整的?`git gc`?命令。 “gc” 代表垃圾回收,這個命令會做以下事情:收集所有松散對象并將它們放置到包文件中,將多個包文件合并為一個大的包文件,移除與任何提交都不相關的陳舊對象。
可以像下面一樣手動執行自動垃圾回收:
~~~
$ git gc --auto
~~~
就像上面提到的,這個命令通常并不會產生效果。 大約需要 7000 個以上的松散對象或超過 50 個的包文件才能讓 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 的時候,你可能會意外丟失一次提交。 通常這是因為你強制刪除了正在工作的分支,但是最后卻發現你還需要這個分支;亦或者硬重置了一個分支,放棄了你想要的提交。 如果這些事情已經發生,該如何找回你的提交呢?
下面的例子將硬重置你的測試倉庫中的 master 分支到一個舊的提交,以此來恢復丟失的提交。 首先,讓我們看看你的倉庫現在在什么地方:
~~~
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added 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 時它的值。 每一次你提交或改變分支,引用日志都會被更新。 引用日志(reflog)也可以通過?`git update-ref`?命令更新,我們在?[Git 引用](http://git-scm.com/book/zh/v2/ch00/_git_refs)?有提到使用這個命令而不是是直接將 SHA-1 的值寫入引用文件中的原因。 你可以在任何時候通過執行?`git reflog`?命令來了解你曾經做過什么:
~~~
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb
~~~
這里可以看到我們已經檢出的兩次提交,然而并沒有足夠多的信息。 為了使顯示的信息更加有用,我們可以執行?`git log -g`,這個命令會以標準日志的格式輸出引用日志。
~~~
$ 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
modified repo.rb a bit
~~~
看起來下面的那個就是你丟失的提交,你可以通過創建一個新的分支指向這個提交來恢復它。 例如,你可以創建一個名為?`recover-branch`?的分支指向這個提交(ab1afef):
~~~
$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
~~~
不錯,現在有一個名為?`recover-branch`?的分支是你的?`master`?分支曾經指向的地方,再一次使得前兩次提交可到達了。 接下來,假設你丟失的提交因為某些原因不在引用日志中 - 我們可以通過移除?`recover-branch`?分支并刪除引用日志來模擬這種情況。 現在前兩次提交又不被任何分支指向了:
~~~
$ git branch -D recover-branch
$ rm -Rf .git/logs/
~~~
由于引用日志數據存放在?`.git/logs/`?目錄中,現在你已經沒有引用日志了。 這時該如何恢復那次提交? 一種方式是使用?`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” 后看到你丟失的提交。 現在你可以用和之前相同的方法恢復這個提交,也就是添加一個指向這個提交的分支。
## 移除對象
Git 有很多很棒的功能,但是其中一個特性會導致問題,`git clone`?會下載整個項目的歷史,包括每一個文件的每一個版本。 如果所有的東西都是源代碼那么這很好,因為 Git 被高度優化來有效地存儲這種數據。 然而,如果某個人在之前向項目添加了一個大小特別大的文件,即使你將這個文件從項目中移除了,每次克隆還是都要強制的下載這個大文件。 之所以會產生這個問題,是因為這個文件在歷史中是存在的,它會永遠在那里。
當你遷移 Subversion 或 Perforce 倉庫到 Git 的時候,這會是一個嚴重的問題。 因為這些版本控制系統并不下載所有的歷史文件,所以這種文件所帶來的問題比較少。 如果你從其他的版本控制系統遷移到 Git 時發現倉庫比預期的大得多,那么你就需要找到并移除這些大文件。
**警告:這個操作對提交歷史的修改是破壞性的。**?它會從你必須修改或移除一個大文件引用最早的樹對象開始重寫每一次提交。 如果你在導入倉庫后,在任何人開始基于這些提交工作前執行這個操作,那么將不會有任何問題 - 否則,你必須通知所有的貢獻者他們需要將他們的成果變基到你的新提交上。
為了演示,我們將添加一個大文件到測試倉庫中,并在下一次提交中刪除它,現在我們需要找到它,并將它從倉庫中永久刪除。 首先,添加一個大文件到倉庫中:
~~~
$ curl https://www.kernel.org/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
~~~
哎呀 - 其實這個項目并不需要這個巨大的壓縮文件。 現在我們將它移除:
~~~
$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'oops - removed large tarball'
[master dadf725] oops - removed 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`?的數值指的是你的包文件以 KB 為單位計算的大小,所以你大約占用了 5MB 的空間。 在最后一次提交前,使用了不到 2KB - 顯然,從之前的提交中移除文件并不能從歷史中移除它。 每一次有人克隆這個倉庫時,他們將必須克隆所有的 5MB 來獲得這個微型項目,只因為你意外地添加了一個大文件。 現在來讓我們徹底的移除這個文件。
首先你必須找到它。 在本例中,你已經知道是哪個文件了。 但是假設你不知道;該如何找出哪個文件或哪些文件占用了如此多的空間? 如果你執行?`git gc`?命令,所有的對象將被放入一個包文件中,你可以通過運行?`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`?命令,我們在?[指定特殊的提交信息格式](http://git-scm.com/book/zh/v2/1-customizing-git/_enforcing_commit_message_format)?中曾提到過。 如果你傳遞?`--objects`?參數給?`rev-list`?命令,它就會列出所有提交的 SHA-1、數據對象的 SHA-1 和與它們相關聯的文件路徑。 可以使用以下命令來找出你的數據對象的名字:
~~~
$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
~~~
現在,你只需要從過去所有的樹中移除這個文件。 使用以下命令可以輕松地查看哪些提交對這個文件產生改動:
~~~
$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball
~~~
現在,你必須重寫?`7b30847`?提交之后的所有提交來從 Git 歷史中完全移除這個文件。 為了執行這個操作,我們要使用?`filter-branch`?命令,這個命令在?[重寫歷史](http://git-scm.com/book/zh/v2/1-git-tools/_rewriting_history)?中也使用過:
~~~
$ 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`?選項類似于在?[重寫歷史](http://git-scm.com/book/zh/v2/1-git-tools/_rewriting_history)?中提到的的?`--tree-filter`?選項,不過這個選項并不會讓命令將修改在硬盤上檢出的文件,而只是修改在暫存區或索引中的文件。
你必須使用?`git rm --cached`?命令來移除文件,而不是通過類似?`rm file`?的命令 - 因為你需要從索引中移除它,而不是磁盤中。 還有一個原因是速度 - Git 在運行過濾器時,并不會檢出每個修訂版本到磁盤中,所以這個過程會非常快。 如果愿意的話,你也可以通過?`--tree-filter`?選項來完成同樣的任務。?`git rm`?命令的?`--ignore-unmatch`?選項告訴命令:如果嘗試刪除的模式不存在時,不提示錯誤。 最后,使用?`filter-branch`?選項來重寫自?`7b30847`?提交以來的歷史,也就是這個問題產生的地方。 否則,這個命令會從最舊的提交開始,這將會花費許多不必要的時間。
你的歷史中將不再包含對那個文件的引用。 不過,你的引用日志和你在?`.git/refs/original`?通過?`filter-branch`?選項添加的新引用中還存有對這個文件的引用,所以你必須移除它們然后重新打包數據庫。 在重新打包前需要移除任何包含指向那些舊提交的指針的文件:
~~~
$ 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
~~~
打包的倉庫大小下降到了 8K,比 5MB 好很多。 可以從 size 的值看出,這個大文件還在你的松散對象中,并沒有消失;但是它不會在推送或接下來的克隆中出現,這才是最重要的。 如果真的想要刪除它,可以通過有?`--expire`?選項的?`git prune`?命令來完全地移除那個對象:
~~~
$ 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
~~~
- 前言
- Scott Chacon 序
- Ben Straub 序
- 獻辭
- 貢獻者
- 引言
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 命令行
- 1.5 安裝 Git
- 1.6 初次運行 Git 前的配置
- 1.7 獲取幫助
- 1.8 總結
- 2. Git 基礎
- 2.1 獲取 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 Git 別名
- 2.8 總結
- 3. Git 分支
- 3.1 分支簡介
- 3.2 分支的新建與合并
- 3.3 分支管理
- 3.4 分支開發工作流
- 3.5 遠程分支
- 3.6 變基
- 3.7 總結
- 4. 服務器上的 Git
- 4.1 協議
- 4.2 在服務器上搭建 Git
- 4.3 生成 SSH 公鑰
- 4.4 配置服務器
- 4.5 Git 守護進程
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管的選擇
- 4.10 總結
- 5. 分布式 Git
- 5.1 分布式工作流程
- 5.2 向一個項目貢獻
- 5.3 維護項目
- 5.4 總結
- 6. GitHub
- 6.1 賬戶的創建和配置
- 6.2 對項目做出貢獻
- 6.3 維護項目
- 6.4 管理組織
- 6.5 腳本 GitHub
- 6.6 總結
- 7. Git 工具
- 7.1 選擇修訂版本
- 7.2 交互式暫存
- 7.3 儲藏與清理
- 7.4 簽署工作
- 7.5 搜索
- 7.6 重寫歷史
- 7.7 重置揭密
- 7.8 高級合并
- 7.9 Rerere
- 7.10 使用 Git 調試
- 7.11 子模塊
- 7.12 打包
- 7.13 替換
- 7.14 憑證存儲
- 7.15 總結
- 8. 自定義 Git
- 8.1 配置 Git
- 8.2 Git 屬性
- 8.3 Git 鉤子
- 8.4 使用強制策略的一個例子
- 8.5 總結
- 9. Git 與其他系統
- 9.1 作為客戶端的 Git
- 9.2 遷移到 Git
- 9.3 總結
- 10. Git 內部原理
- 10.1 底層命令和高層命令
- 10.2 Git 對象
- 10.3 Git 引用
- 10.4 包文件
- 10.5 引用規格
- 10.6 傳輸協議
- 10.7 維護與數據恢復
- 10.8 環境變量
- 10.9 總結
- A. 其它環境中的 Git
- A1.1 圖形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Eclipse 中的 Git
- A1.4 Bash 中的 Git
- A1.5 Zsh 中的 Git
- A1.6 Powershell 中的 Git
- A1.7 總結
- B. 將 Git 嵌入你的應用
- A2.1 命令行 Git 方式
- A2.2 Libgit2
- A2.3 JGit
- C. Git 命令
- A3.1 設置與配置
- A3.2 獲取與創建項目
- A3.3 快照基礎
- A3.4 分支與合并
- A3.5 項目分享與更新
- A3.6 檢查與比較
- A3.7 調試
- A3.8 補丁
- A3.9 郵件
- A3.10 外部系統
- A3.11 管理
- A3.12 底層命令