你時不時的需要進行一些清理工作 ── 如減小一個倉庫的大小,清理導入的庫,或是恢復丟失的數據。本節將描述這類使用場景。
## 維護
Git 會不定時地自動運行稱為 "auto gc" 的命令。大部分情況下該命令什么都不處理。不過要是存在太多松散對象 (loose object, 不在 packfile 中的對象) 或 packfile,Git 會進行調用 git gc 命令。 gc 指垃圾收集 (garbage collect),此命令會做很多工作:收集所有松散對象并將它們存入 packfile,合并這些 packfile 進一個大的 packfile,然后將不被任何 commit 引用并且已存在一段時間 (數月) 的對象刪除。
可以手工運行 auto gc 命令:
`$ git gc --auto`
再次強調,這個命令一般什么都不干。如果有 7,000 個左右的松散對象或是 50 個以上的 packfile,Git 才會真正調用 gc 命令。可能通過修改配置中的` gc.auto` 和` gc.autopacklimit` 來調整這兩個閾值。
gc 還會將所有引用 (references) 并入一個單獨文件。假設倉庫中包含以下分支和標簽:
~~~
$ 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
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9
~~~
當更新一個引用時,Git 不會修改這個文件,而是在 refs/heads 下寫入一個新文件。當查找一個引用的 SHA 時,Git 首先在 refs 目錄下查找,如果未找到則到 `packed-refs` 文件中去查找。因此如果在 refs 目錄下找不到一個引用,該引用可能存到 packed-refs 文件中去了。
請留意文件最后以 ^ 開頭的那一行。這表示該行上一行的那個標簽是一個 annotated 標簽,而該行正是那個標簽所指向的 commit 。
## 數據恢復
在使用 Git 的過程中,有時會不小心丟失 commit 信息。這一般出現在以下情況下:強制刪除了一個分支而后又想重新使用這個分支,hard-reset 了一個分支從而丟棄了分支的部分 commit。如果這真的發生了,有什么辦法把丟失的 commit 找回來呢?
下面的示例演示了對 test 倉庫主分支進行 hard-reset 到一個老版本的 commit 的操作,然后恢復丟失的 commit 。首先查看一下當前的倉庫狀態:
~~~
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
~~~
接著將 master 分支移回至中間的一個 commit:
~~~
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
~~~
這樣就丟棄了最新的兩個 commit ── 包含這兩個 commit 的分支不存在了。現在要做的是找出最新的那個 commit 的 SHA,然后添加一個指它它的分支。關鍵在于找出最新的 commit 的 SHA ── 你不大可能記住了這個 SHA,是吧?
通常最快捷的辦法是使用 git reflog 工具。當你 (在一個倉庫下) 工作時,Git 會在你每次修改了 HEAD 時悄悄地將改動記錄下來。當你提交或修改分支時,reflog 就會更新。git update-ref 命令也可以更新 reflog,這是在本章前面的 "Git References" 部分我們使用該命令而不是手工將 SHA 值寫入 ref 文件的理由。任何時間運行 git reflog 命令可以查看當前的狀態:
~~~
$ git reflog
1a410ef HEAD@{0}: 1a410efbd13591db07496601ebc7a059dd55cfe9: updating HEAD
ab1afef HEAD@{1}: ab1afef80fac8e34258ff41fc1b867c702daa24b: updating HEAD
~~~
可以看到我們簽出的兩個 commit ,但沒有更多的相關信息。運行 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
modified repo a bit
~~~
看起來弄丟了的 commit 是底下那個,這樣在那個 commit 上創建一個新分支就能把它恢復過來。比方說,可以在那個 commit (ab1afef) 上創建一個名為 recover-branch 的分支:
~~~
$ 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
~~~
酷!這樣有了一個跟原來 master 一樣的 recover-branch 分支,最新的兩個 commit 又找回來了。接著,假設引起 commit 丟失的原因并沒有記錄在 reflog 中 ── 可以通過刪除 recover-branch 和 reflog 來模擬這種情況。這樣最新的兩個 commit 不會被任何東西引用到:
~~~
$ git branch -D recover-branch
$ rm -Rf .git/logs/
~~~
因為 reflog 數據是保存在 .git/logs/ 目錄下的,這樣就沒有 reflog 了。現在要怎樣恢復 commit 呢?辦法之一是使用 git fsck 工具,該工具會檢查倉庫的數據完整性。如果指定 --full 選項,該命令顯示所有未被其他對象引用 (指向) 的所有對象:
~~~
$ git fsck --full
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
~~~
本例中,可以從 dangling commit 找到丟失了的 commit。用相同的方法就可以恢復它,即創建一個指向該 SHA 的分支。
## 移除對象
Git 有許多過人之處,不過有一個功能有時卻會帶來問題:git clone 會將包含每一個文件的所有歷史版本的整個項目下載下來。如果項目包含的僅僅是源代碼的話這并沒有什么壞處,畢竟 Git 可以非常高效地壓縮此類數據。不過如果有人在某個時刻往項目中添加了一個非常大的文件,那們即便他在后來的提交中將此文件刪掉了,所有的簽出都會下載這個大文件。因為歷史記錄中引用了這個文件,它會一直存在著。
當你將 Subversion 或 Perforce 倉庫轉換導入至 Git 時這會成為一個很嚴重的問題。在此類系統中,(簽出時) 不會下載整個倉庫歷史,所以這種情形不大會有不良后果。如果你從其他系統導入了一個倉庫,或是發覺一個倉庫的尺寸遠超出預計,可以用下面的方法找到并移除大 (尺寸) 對象。
警告:此方法會破壞提交歷史。為了移除對一個大文件的引用,從最早包含該引用的 tree 對象開始之后的所有 commit 對象都會被重寫。如果在剛導入一個倉庫并在其他人在此基礎上開始工作之前這么做,那沒有什么問題 ── 否則你不得不通知所有協作者 (貢獻者) 去衍合你新修改的 commit 。
為了演示這點,往 test 倉庫中加入一個大文件,然后在下次提交時將它刪除,接著找到并將這個文件從倉庫中永久刪除。首先,加一個大文件進去:
~~~
$ curl http://kernel.org/pub/software/scm/git/git-1.6.3.1.tar.bz2 > git.tbz2
$ git add git.tbz2
$ git commit -am 'added git tarball'
[master 6df7640] added git tarball
1 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git.tbz2
~~~
喔,你并不想往項目中加進一個這么大的 tar 包。最后還是去掉它:
~~~
$ git rm git.tbz2
rm 'git.tbz2'
$ git commit -m 'oops - removed large tarball'
[master da3f30d] oops - removed large tarball
1 files changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git.tbz2
~~~
對倉庫進行 gc 操作,并查看占用了空間:
~~~
$ git gc
Counting objects: 21, done.
Delta compression using 2 threads.
Compressing objects: 100% (16/16), done.
Writing objects: 100% (21/21), done.
Total 21 (delta 3), reused 15 (delta 1)
~~~
可以運行 count-objects 以查看使用了多少空間:
~~~
$ git count-objects -v
count: 4
size: 16
in-pack: 21
packs: 1
size-pack: 2016
prune-packable: 0
garbage: 0
~~~
size-pack 是以千字節為單位表示的 packfiles 的大小,因此已經使用了 2MB 。而在這次提交之前僅用了 2K 左右 ── 顯然在這次提交時刪除文件并沒有真正將其從歷史記錄中刪除。每當有人復制這個倉庫去取得這個小項目時,都不得不復制所有 2MB 數據,而這僅僅因為你曾經不小心加了個大文件。當我們來解決這個問題。
首先要找出這個文件。在本例中,你知道是哪個文件。假設你并不知道這一點,要如何找出哪個 (些) 文件占用了這么多的空間?如果運行 git gc,所有對象會存入一個 packfile 文件;運行另一個底層命令 git verify-pack 以識別出大對象,對輸出的第三列信息即文件大小進行排序,還可以將輸出定向到 tail 命令,因為你只關心排在最后的那幾個最大的文件:
~~~
$ git verify-pack -v .git/objects/pack/pack-3f8c0...bb.idx | sort -k 3 -n | tail -3
e3f094f522629ae358806b17daf78246c27c007b blob 1486 734 4667
05408d195263d853f09dca71d55116663690c27c blob 12908 3478 1189
7a9eb2fba2b1811321254ac360970fc169ba2330 blob 2056716 2056872 5401
~~~
最底下那個就是那個大文件:2MB 。要查看這到底是哪個文件,可以使用第 7 章中已經簡單使用過的 rev-list 命令。若給 rev-list 命令傳入 --objects 選項,它會列出所有 commit SHA 值,blob SHA 值及相應的文件路徑。可以這樣查看 blob 的文件名:
~~~
$ git rev-list --objects --all | grep 7a9eb2fb
7a9eb2fba2b1811321254ac360970fc169ba2330 git.tbz2
~~~
接下來要將該文件從歷史記錄的所有 tree 中移除。很容易找出哪些 commit 修改了這個文件:
~~~
$ git log --pretty=oneline --branches -- git.tbz2
da3f30d019005479c99eb4c3406225613985a1db oops - removed large tarball
6df764092f3e7c8f5f94cbe08ee5cf42e92a0289 added git tarball
~~~
必須重寫從 6df76 開始的所有 commit 才能將文件從 Git 歷史中完全移除。這么做需要用到第 6 章中用過的 filter-branch 命令:
~~~
$ git filter-branch --index-filter \
'git rm --cached --ignore-unmatch git.tbz2' -- 6df7640^..
Rewrite 6df764092f3e7c8f5f94cbe08ee5cf42e92a0289 (1/2)rm 'git.tbz2'
Rewrite da3f30d019005479c99eb4c3406225613985a1db (2/2)
Ref 'refs/heads/master' was rewritten
~~~
--index-filter 選項類似于第 6 章中使用的 --tree-filter 選項,但這里不是傳入一個命令去修改磁盤上簽出的文件,而是修改暫存區域或索引。不能用 rm file 命令來刪除一個特定文件,而是必須用 git rm --cached 來刪除它 ── 即從索引而不是磁盤刪除它。這樣做是出于速度考慮 ── 由于 Git 在運行你的 filter 之前無需將所有版本簽出到磁盤上,這個操作會快得多。也可以用 --tree-filter 來完成相同的操作。git rm 的 --ignore-unmatch 選項指定當你試圖刪除的內容并不存在時不顯示錯誤。最后,因為你清楚問題是從哪個 commit 開始的,使用 filter-branch 重寫自 6df7640 這個 commit 開始的所有歷史記錄。不這么做的話會重寫所有歷史記錄,花費不必要的更多時間。
現在歷史記錄中已經不包含對那個文件的引用了。不過 reflog 以及運行 filter-branch 時 Git 往 .git/refs/original 添加的一些 refs 中仍有對它的引用,因此需要將這些引用刪除并對倉庫進行 repack 操作。在進行 repack 前需要將所有對這些 commits 的引用去除:
~~~
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 19, done.
Delta compression using 2 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (19/19), done.
Total 19 (delta 3), reused 16 (delta 1)
~~~
看一下節省了多少空間。
~~~
$ git count-objects -v
count: 8
size: 2040
in-pack: 19
packs: 1
size-pack: 7
prune-packable: 0
garbage: 0
~~~
repack 后倉庫的大小減小到了 7K ,遠小于之前的 2MB 。從 size 值可以看出大文件對象還在松散對象中,其實并沒有消失,不過這沒有關系,重要的是在再進行推送或復制,這個對象不會再傳送出去。如果真的要完全把這個對象刪除,可以運行` git prune --expire` 命令。
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 安裝 Git
- 1.5 初次運行 Git 前的配置
- 1.6 獲取幫助
- 1.7 小結
- 2. Git基礎
- 2.1 取得項目的 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 技巧和竅門
- 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 公共訪問
- 4.6 GitWeb
- 4.7 Gitosis
- 4.8 Gitolite
- 4.9 Git 守護進程
- 4.10 Git 托管服務
- 4.11 小結
- 5. 分布式Git
- 5.1 分布式工作流程
- 5.2 為項目作貢獻
- 5.3 項目的管理
- 5.4 小結
- 6. Git工具
- 6.1 修訂版本(Revision)選擇
- 6.2 交互式暫存
- 6.3 儲藏(Stashing)
- 6.4 重寫歷史
- 6.5 使用 Git 調試
- 6.6 子模塊
- 6.7 子樹合并
- 6.8 總結
- 7. 自定義Git
- 7.1 配置 Git
- 7.2 Git屬性
- 7.3 Git掛鉤
- 7.4 Git 強制策略實例
- 7.5 總結
- 8. Git與其他系統
- 8.1 Git 與 Subversion
- 8.2 遷移到 Git
- 8.3 總結
- 9. Git 內部原理
- 9.2 Git 對象
- 9.3 Git References
- 9.4 Packfiles
- 9.5 The Refspec
- 9.6 傳輸協議
- 9.7 維護及數據恢復
- 9.8 總結
- 9.1 底層命令 (Plumbing) 和高層命令 (Porcelain)