Git 維護及數(shù)據(jù)恢復

2018-09-27 15:52 更新

你時不時的需要進行一些清理工作 ── 如減小一個倉庫的大小,清理導入的庫,或是恢復丟失的數(shù)據(jù)。本節(jié)將描述這類使用場景。

維護

Git 會不定時地自動運行稱為 "auto gc" 的命令。大部分情況下該命令什么都不處理。不過要是存在太多松散對象 (loose object, 不在 packfile 中的對象) 或 packfile,Git 會進行調(diào)用 git gc 命令。 gc 指垃圾收集 (garbage collect),此命令會做很多工作:收集所有松散對象并將它們存入 packfile,合并這些 packfile 進一個大的 packfile,然后將不被任何 commit 引用并且已存在一段時間 (數(shù)月) 的對象刪除。

可以手工運行 auto gc 命令:

$ git gc --auto

再次強調(diào),這個命令一般什么都不干。如果有 7,000 個左右的松散對象或是 50 個以上的 packfile,Git 才會真正調(diào)用 gc 命令。可能通過修改配置中的gc.autogc.autopacklimit 來調(diào)整這兩個閾值。

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 。

數(shù)據(jù)恢復

在使用 Git 的過程中,有時會不小心丟失 commit 信息。這一般出現(xiàn)在以下情況下:強制刪除了一個分支而后又想重新使用這個分支,hard-reset 了一個分支從而丟棄了分支的部分 commit。如果這真的發(fā)生了,有什么辦法把丟失的 commit 找回來呢?

下面的示例演示了對 test 倉庫主分支進行 hard-reset 到一個老版本的 commit 的操作,然后恢復丟失的 commit 。首先查看一下當前的倉庫狀態(tài):

$ 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 的分支不存在了?,F(xiàn)在要做的是找出最新的那個 commit 的 SHA,然后添加一個指它它的分支。關鍵在于找出最新的 commit 的 SHA ── 你不大可能記住了這個 SHA,是吧?

通常最快捷的辦法是使用 git reflog 工具。當你 (在一個倉庫下) 工作時,Git 會在你每次修改了 HEAD 時悄悄地將改動記錄下來。當你提交或修改分支時,reflog 就會更新。git update-ref 命令也可以更新 reflog,這是在本章前面的 "Git References" 部分我們使用該命令而不是手工將 SHA 值寫入 ref 文件的理由。任何時間運行 git reflog 命令可以查看當前的狀態(tài):

$ 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 上創(chuàng)建一個新分支就能把它恢復過來。比方說,可以在那個 commit (ab1afef) 上創(chuàng)建一個名為 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 數(shù)據(jù)是保存在 .git/logs/ 目錄下的,這樣就沒有 reflog 了?,F(xiàn)在要怎樣恢復 commit 呢?辦法之一是使用 git fsck 工具,該工具會檢查倉庫的數(shù)據(jù)完整性。如果指定 --full 選項,該命令顯示所有未被其他對象引用 (指向) 的所有對象:

$ git fsck --full
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293

本例中,可以從 dangling commit 找到丟失了的 commit。用相同的方法就可以恢復它,即創(chuàng)建一個指向該 SHA 的分支。

移除對象

Git 有許多過人之處,不過有一個功能有時卻會帶來問題:git clone 會將包含每一個文件的所有歷史版本的整個項目下載下來。如果項目包含的僅僅是源代碼的話這并沒有什么壞處,畢竟 Git 可以非常高效地壓縮此類數(shù)據(jù)。不過如果有人在某個時刻往項目中添加了一個非常大的文件,那們即便他在后來的提交中將此文件刪掉了,所有的簽出都會下載這個大文件。因為歷史記錄中引用了這個文件,它會一直存在著。

當你將 Subversion 或 Perforce 倉庫轉換導入至 Git 時這會成為一個很嚴重的問題。在此類系統(tǒng)中,(簽出時) 不會下載整個倉庫歷史,所以這種情形不大會有不良后果。如果你從其他系統(tǒng)導入了一個倉庫,或是發(fā)覺一個倉庫的尺寸遠超出預計,可以用下面的方法找到并移除大 (尺寸) 對象。

警告:此方法會破壞提交歷史。為了移除對一個大文件的引用,從最早包含該引用的 tree 對象開始之后的所有 commit 對象都會被重寫。如果在剛導入一個倉庫并在其他人在此基礎上開始工作之前這么做,那沒有什么問題 ── 否則你不得不通知所有協(xié)作者 (貢獻者) 去衍合你新修改的 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 是以千字節(jié)為單位表示的 packfiles 的大小,因此已經(jīng)使用了 2MB 。而在這次提交之前僅用了 2K 左右 ── 顯然在這次提交時刪除文件并沒有真正將其從歷史記錄中刪除。每當有人復制這個倉庫去取得這個小項目時,都不得不復制所有 2MB 數(shù)據(jù),而這僅僅因為你曾經(jīng)不小心加了個大文件。當我們來解決這個問題。

首先要找出這個文件。在本例中,你知道是哪個文件。假設你并不知道這一點,要如何找出哪個 (些) 文件占用了這么多的空間?如果運行 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 章中已經(jīng)簡單使用過的 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 選項,但這里不是傳入一個命令去修改磁盤上簽出的文件,而是修改暫存區(qū)域或索引。不能用 rm file 命令來刪除一個特定文件,而是必須用 git rm --cached 來刪除它 ── 即從索引而不是磁盤刪除它。這樣做是出于速度考慮 ── 由于 Git 在運行你的 filter 之前無需將所有版本簽出到磁盤上,這個操作會快得多。也可以用 --tree-filter 來完成相同的操作。git rm 的 --ignore-unmatch 選項指定當你試圖刪除的內(nèi)容并不存在時不顯示錯誤。最后,因為你清楚問題是從哪個 commit 開始的,使用 filter-branch 重寫自 6df7640 這個 commit 開始的所有歷史記錄。不這么做的話會重寫所有歷史記錄,花費不必要的更多時間。

現(xiàn)在歷史記錄中已經(jīng)不包含對那個文件的引用了。不過 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)

看一下節(jié)省了多少空間。

$ 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 命令。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號