Git目錄為什么這么大
目錄
1、介紹
2、Git 存儲原理
2.1 目錄結(jié)構(gòu)
2.2 提交內(nèi)容
2.3 如何徹底刪除一個文件
3、解析 Object 存儲方式
4、處理大文件
4.1 大文件的產(chǎn)生
4.2 尋找大文件的 ID
4.3 刪除大文件
4.4 按照 pack 文件直接操作
5、大文件存儲的正確方式
6、其他解決方案
7、小結(jié)

1、介紹
Git作為一個分布式的版本控制工具,在每天高頻次的使用中難免遇到一些問題
本文圍繞git的目錄過大,從git進行版本控制底層存儲出發(fā),簡要分析Git目錄過大的原因,以及如何處理
2、Git 存儲原理
2.1 目錄結(jié)構(gòu)
使用版本控制的人都會知道,不管是svn還是更為流行的git,整個工程目錄下,除了項目代碼外,與版本控制相關(guān)的就是.svn或.git目錄
以git為例,.git下的目錄結(jié)構(gòu)如下
tree -L 1 .git
.git
├── COMMIT_EDITMSG
├── FETCH_HEAD
├── HEAD
├── ORIG_HEAD
├── config
├── description
├── hooks
├── index
├── info
├── logs
├── objects
├── packed-refs
├── refs
└── sourcetreeconfig
其中一些目錄的說明
HEAD:表示當前本地簽出的分支
hooks:
git鉤子目錄,關(guān)于鉤子的使用可以參考我之前的文章 利用 Git 鉤子實現(xiàn)代碼發(fā)布[1]index:存儲緩沖區(qū)
GitExtensions中的stage的內(nèi)容objects:存儲對象的目錄,
git中對象分為commit對象,tree對象(多叉樹),blob對象refs:存儲指向
branch的最近一次commit對象的指針,也就是commit對象的sha-1值,其中heads存儲branch對應的commit,tags存儲tag對應的commitconfig:倉庫配置,比如遠程的
url,郵箱和用戶名等
2.2 提交內(nèi)容
git的一次提交包含4個部分:
工作目錄快照名稱(一個哈希值)
一條評論/注釋
提交者信息
父提交的哈希值
每一個提交Commit相當于一個Patch應用在之前的項目上,借此一個項目可以回到任何一次提交時的文件狀態(tài)
于是在Git中刪除一個文件時,Git只是記錄了該刪除操作,該記錄作為一個Patch存儲在 .git 中。刪除前的文件仍然在Git倉庫中保存著。直接刪除文件并提交起不到給Git倉庫瘦身的效果
在Git倉庫徹底刪除一個文件只有一種辦法:重寫Rewrite涉及該文件的所有提交。借助 git filter-branch 便可以重寫歷史提交,當然這也是Git中最危險的操作
2.3 如何徹底刪除一個文件
以一個文件的提交為例,這個文件可能會關(guān)聯(lián)很多次提交,只有將每一次與該文件有關(guān)的的提交記錄進行重寫,才能真正實現(xiàn)刪除這個文件,具體做法如下
# git filter-branch -f --prune-empty --index-filter 'git rm -rf --cached --ignore-unmatch api/app/test.py' --tag-name-filter cat -- --all
Rewrite 2771f50d45a0293668a30af77983d87886441640 (264/982)rm 'api/app/test.py'
Rewrite 1a98ecb3f39e1f200e31754714eec18bc92848ce (265/982)rm 'api/app/test.py'
Rewrite d4e61cfb1d88187b0561d283e663b81b738df2c7 (270/982)rm 'api/app/test.py'
Rewrite 4ba0df06b26cf86fd39c2cda6b012c521cbc4dc1 (271/982)rm 'api/app/test.py'
Rewrite 242ae98060c77863f5e826ba7e1ec47
這里要刪除的文件位置是api/app/test.py
--index-filter 參數(shù)用來指定一條Bash命令,--all 參數(shù)告訴Git我們需要重寫所有分支(或引用)
Git會檢出checkout所有的提交, 執(zhí)行該命令,然后重新提交。我們在提交前移除了 test.py 文件, 這個文件便從Git的所有記錄中完全消失了
3、解析 Object 存儲方式
為了一步步熟悉Object存儲的方式,這里在本地創(chuàng)建一個空的git倉庫,且objects目錄中還沒有任何內(nèi)容,創(chuàng)建一個文件并提交
# mkdir git-test && cd git-test && mkdir src
# git init .
Initialized empty Git repository in /Users/ssgeek/Git-workspace/git-test/.git/
# echo "test project" > README.md
# echo "hello world" > src/demo1.txt
# git add .
# git commit -sm "first commit"
[master (root-commit) ca1114d] first commit
2 files changed, 2 insertions(+)
create mode 100644 README.md
create mode 100644 src/demo1.txt
從輸出可以看到,上面的命令創(chuàng)建了一個commit對象,該commit包含兩個文件
查看.git/objects目錄,可以看到該目錄下增加了4個子目錄 32,3b, 4c, ca,d2,每個子目錄下有一個以一長串字母數(shù)字命名的文件
# tree .git/objects
.git/objects
├── 32
│ └── 73e239f79eacf09654a5ecc18498bda0d2e7eb
├── 3b
│ └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
├── 4c
│ └── 3ced11d9e650d74fa5e518b26b311f06d7069c
├── ca
│ └── 1114de8da76527ec73cdf52100eb7ba58e1878
├── d2
│ └── df53f517e6e2c85ff2b0c6b0970428889f265f
├── info
└── pack
前面提到object目錄下存放的是Git為對象生成一個文件,并根據(jù)文件信息生成一個SHA-1哈希值作為文件內(nèi)容的校驗和,創(chuàng)建以該校驗和前兩個字符為名稱的子目錄,并以 (校驗和) 剩下38個字符為文件命名 ,將該文件保存至子目錄下
可以通過 git cat-file命令查看Git Object中存儲的內(nèi)容及對象類型,命令參數(shù)為Git Object的SHA-1哈希值,即目錄名+文件名。一般不用輸入整個Hash,輸入前幾位即可
當前分支的對象引用保存在HEAD文件中,可以查看該文件得到當前HEAD對應的branch,并通過branch查到對應的commit對象
# cat .git/HEAD
ref: refs/heads/master
# cat .git/refs/heads/master
ca1114de8da76527ec73cdf52100eb7ba58e1878
使用-t參數(shù)查看文件類型,使用-p參數(shù)可以查看文件內(nèi)容
# git cat-file -t ca1114
commit
# git cat-file -p ca1114
tree d2df53f517e6e2c85ff2b0c6b0970428889f265f
author ssgeek <[email protected]> 1622445604 +0800
committer ssgeek <[email protected]> 1622445604 +0800
first commit
Signed-off-by: ssgeek <[email protected]>
這是一個commit對象,commit對象中保存了commit的作者,commit的描述信息,簽名信息以及該commit中包含哪些tree對象和blob對象,繼續(xù)看該tree對象中的內(nèi)容
# git cat-file -p d2df53
100644 blob 4c3ced11d9e650d74fa5e518b26b311f06d7069c README.md
040000 tree 3273e239f79eacf09654a5ecc18498bda0d2e7eb src
# git cat-file -p 4c3ced
test project
# git cat-file -p 3273e2
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad demo1.txt
# git cat-file -p 3b18e5
hello world
因此可以得知,git 中存儲了三種類型的對象,commit,tree和blob,三者分別對應git commit,此commit中的目錄和文件,這些對象之間的關(guān)系如下圖

4、處理大文件
4.1 大文件的產(chǎn)生
由上面的詳細分析流程可以看出,git會為每一個提交到版本控制的文件進行追蹤,那么大文件究竟如何產(chǎn)生呢?
在上面的object目錄下還存在著pack和info文件夾。Git往磁盤保存對象時默認使用的格式叫松散對象loose object格式,當你對同一個文件修改哪怕一行,git都會使用全新的文件存儲這個修改了的文件,放在了objects中。Git時不時地將這些對象打包至一個叫packfile的二進制文件以節(jié)省空間并提高效率,當版本庫中有太多的松散對象,或者你手動執(zhí)行 git gc 命令,或者你向遠程服務器執(zhí)行推送時,Git都會這樣做
因此,往往在向git中提交了大文件,會造成pack文件過大,到這里“元兇”終于出現(xiàn)了
4.2 尋找大文件的 ID
以我的博客代碼為例操作
首先查找出大文件
# git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -5 | awk '{print$1}')"
385321b5a0be589af4436ddc6dd0d08a687f8d80 pdf/test/1.gif
21224e779a19dfe7f716eb031d7c69ad65fb684c pdf/test/2.gif
b615ba62e75bdb1c1faca8c43a82c5ef810d7e20 pdf/test/3.gif
cd5762af542f724ca44656f02936940cf6de6525 zip/1.zip
089977cb9de0105969d57bb070f6df0240b9da63 pdf/test/search_index.json
參數(shù)說明:
rev-list 命令用來列出 Git 倉庫中的提交,我們用它來列出所有提交中涉及的文件名及其 ID。該命令可以指定只顯示某個引用(或分支)的上下游的提交 --objects 列出該提交涉及的所有文件 ID --all 所有分支的提交,相當于指定了位于/refs 下的所有引用 verify-pack 命令用于顯示已打包的內(nèi)容,我們用它來找到那些大文件 -v(verbose)參數(shù)是打印詳細信息
4.3 刪除大文件
# git filter-branch --force --prune-empty --index-filter 'git rm -rf --cached --ignore-unmatch YOU-FILE-NAME' --tag-name-filter cat -- --all
參數(shù)說明:
filter-branch 命令可以用來重寫 Git 倉庫中的提交 --index-filter 參數(shù)用來指定一條 Bash 命令,然后 Git 會檢出(checkout)所有的提交, 執(zhí)行該命令,然后重新提交 –all 參數(shù)表示我們需要重寫所有分支(或引用) YOU-FILE-NAME 你查找出來的大文件名字
也可以將上面查找出的大文件輸出重定向輸入到某個文件,這樣更便于操作
# 定向到文件
# git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -5 | awk '{print$1}')" >> large-files.txt
# 得到文件路徑并批量刪除
# cat large-files.txt| awk '{print $2}' | tr '\n' ' ' >> large-files-inline.txt
# git filter-branch -f --prune-empty --index-filter "git rm -rf --cached --ignore-unmatch `cat large-files-inline.txt`" --tag-name-filter cat -- --all
# 或者直接刪除目錄下的所有文件
# git filter-branch --force --index-filter 'git rm -rf --cached --ignore-unmatch gitbook/**' --prune-empty --tag-name-filter cat -- --all
強制推送
# git push --force --all
本地的repo里面仍然保留了這些objects, 等待GC垃圾回收,因此需要徹底清除并收回空間
# rm -rf .git/refs/original/
# git reflog expire --expire=now --all
# git gc --prune=now
Enumerating objects: 40395, done.
Counting objects: 100% (40395/40395), done.
Delta compression using up to 8 threads
Compressing objects: 100% (5546/5546), done.
Writing objects: 100% (40395/40395), done.
Total 40395 (delta 19454), reused 35405 (delta 16700), pack-reused 0
Removing duplicate objects: 100% (256/256), done.
4.4 按照 pack 文件直接操作
除了上面的方式,也可以通過直接找到大的pack文件,基于這些文件進行快速操作
# 找到pack文件,重建索引
git filter-branch --index-filter 'git rm -r --cached --ignore-unmatch .git/objects/pack/xxxxx.pack' --prune-empty
# 刪除和重建的索引
# git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin
# 設(shè)置reflog過期
git reflog expire --expire=now --all
# 清理垃圾
git gc --aggressive --prune=now
5、大文件存儲的正確方式
大文件一般是不建議直接存儲到git倉庫中的,git倉庫是代碼倉庫,存放的應該是n個代碼文件(其實也可以認為是文本文件)
如果是作為倉庫管理員,應該有意識的將git倉庫設(shè)置一個允許的文件大小限制
如果是非變化性的大文件,可以存儲到專用的文件服務器、對象存儲等
如果非要在版本庫中存儲大文件,更好的方式是通過git-lfs,及時使用 lfs 來追蹤、記錄和管理大文件。這樣大文件既不會污染我們的 .git 目錄,也可以讓我們更方便的使用,這里不多做原理展開,
簡單來說操作方法如下
# 1.開啟lfs功能
# git lfs install
# 2.追蹤所有后綴名為“.psd”的文件
# git lfs track "*.iso"
# 3.追蹤單個文件
git lfs track "logo.png"
# 4.提交存儲信息文件
# git add .gitattributes
# 5.提交并推送到GitHub倉庫
# git add .
# git commit -m "Add some files"
# git push origin master
關(guān)于git-lfs的使用及原理說明可以參考國內(nèi)的gitee官方幫助說明文檔Git LFS 操作指南[2]
6、其他解決方案
除了上面的操作,還可以利用更為好用的開源效率工具bfg進行清理,參考`bfg`文檔[3],配置好java環(huán)境后,操作如下
# 下載封裝好的jar包
$ wget https://repo1.maven.org/maven2/com/madgag/bfg/1.13.0/bfg-1.13.0.jar
# 克隆的時候需要--mirror參數(shù)
$ git clone --mirror git://example.com/big-repo.git
# 運行BFG來清理存儲庫
$ java -jar bfg.jar --strip-blobs-bigger-than 100M big-repo.git
# 去除臟數(shù)據(jù)
$ cd big-repo.git
$ git reflog expire --expire=now --all
$ git gc --prune=now --aggressive
# 推送上去
# 此推將更新遠程服務器上的所有refs分支
$ git push
其他用法
# 刪除所有的名為'id_dsa'或'id_rsa'的文件
$ java -jar bfg.jar --delete-files id_{dsa,rsa} my-repo.git
# 刪除所有大于50M的文件
$ java -jar bfg.jar --strip-blobs-bigger-than 50M my-repo.git
# 刪除文件夾下所有的文件
$ java -jar bfg.jar --delete-folders doc my-repo.git
7、小結(jié)
本文分析了git底層版本控制的存儲實現(xiàn),分析了版本控制系統(tǒng)中大文件的產(chǎn)生,并通過一定手段進行解決。
要提到的是,上面的操作難免也會出現(xiàn)風險,如果是作為一個規(guī)范的代碼倉庫,應該在前期就做好規(guī)劃,避免大文件提交到倉庫,規(guī)范每一次的提交記錄,做好code review及倉庫管理
See you ~
參考資料
利用 Git 鉤子實現(xiàn)代碼發(fā)布: https://www.ssgeek.com/post/li-yong-git-gou-zi-shi-xian-dai-ma-fa-bu/
[2]Git LFS 操作指南: https://gitee.com/help/articles/4235#article-header9
[3]bfg文檔: https://rtyley.github.io/bfg-repo-cleaner/
https://harttle.land/2016/03/22/purge-large-files-in-gitrepo.html#header-6
