zoukankan      html  css  js  c++  java
  • Git目录为什么这么大

    1、介绍

    Git作为一个分布式的版本控制工具,在每天高频次的使用中难免遇到一些问题

    本文围绕git的目录过大,从git进行版本控制底层存储出发,简要分析Git目录过大的原因,以及如何处理

    2、Git存储原理

    2.1 目录结构

    使用版本控制的人都会知道,不管是svn还是更为流行的git,整个工程目录下,除了项目代码外,与版本控制相关的就是.svn.git目录

    git为例,.git下的目录结构如下

    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钩子目录,关于钩子的使用可以参考我之前的文章 利用Git钩子实现代码发布

    • index: 存储缓冲区GitExtensions中的stage的内容

    • objects:存储对象的目录,git中对象分为commit对象,tree对象(多叉树),blob对象

    • refs:存储指向branch的最近一次commit对象的指针,也就是commit对象的sha-1值,其中heads存储branch对应的committags存储tag对应的commit

    • config:仓库配置,比如远程的url,邮箱和用户名等

    2.2 提交内容

    git的一次提交包含4个部分:

    • 工作目录快照名称(一个哈希值)

    • 一条评论/注释

    • 提交者信息

    • 父提交的哈希值

    每一个提交Commit相当于一个Patch应用在之前的项目上,借此一个项目可以回到任何一次提交时的文件状态

    于是在Git中删除一个文件时,Git只是记录了该删除操作,该记录作为一个Patch存储在 .git 中。 删除前的文件仍然在Git仓库中保存着。直接删除文件并提交起不到给Git仓库瘦身的效果

    Git仓库彻底删除一个文件只有一种办法:重写Rewrite涉及该文件的所有提交。借助 git filter-branch 便可以重写历史提交,当然这也是Git中最危险的操作

    2.3 如何彻底删除一个文件

    以一个文件的提交为例,这个文件可能会关联很多次提交,只有将每一次与该文件有关的的提交记录进行重写,才能真正实现删除这个文件,具体做法如下

    # 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 参数用来指定一条Bash命令,--all 参数告诉Git我们需要重写所有分支(或引用)

    Git会检出checkout所有的提交, 执行该命令,然后重新提交。我们在提交前移除了 test.py 文件, 这个文件便从Git的所有记录中完全消失了

    3、解析Object存储方式

    为了一步步熟悉Object存储的方式,这里在本地创建一个空的git仓库,且objects目录中还没有任何内容,创建一个文件并提交

    # 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
    

    从输出可以看到,上面的命令创建了一个commit对象,该commit包含两个文件

    查看.git/objects目录,可以看到该目录下增加了4个子目录 32,3b, 4c, ca,d2,每个子目录下有一个以一长串字母数字命名的文件

    # tree .git/objects
    .git/objects
    ├── 32
    │   └── 73e239f79eacf09654a5ecc18498bda0d2e7eb
    ├── 3b
    │   └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
    ├── 4c
    │   └── 3ced11d9e650d74fa5e518b26b311f06d7069c
    ├── ca
    │   └── 1114de8da76527ec73cdf52100eb7ba58e1878
    ├── d2
    │   └── df53f517e6e2c85ff2b0c6b0970428889f265f
    ├── info
    └── pack
    

    前面提到object目录下存放的是Git为对象生成一个文件,并根据文件信息生成一个SHA-1哈希值作为文件内容的校验和,创建以该校验和前两个字符为名称的子目录,并以 (校验和) 剩下38个字符为文件命名 ,将该文件保存至子目录下

    可以通过 git cat-file命令查看Git Object中存储的内容及对象类型,命令参数为Git ObjectSHA-1哈希值,即目录名+文件名。一般不用输入整个Hash,输入前几位即可

    当前分支的对象引用保存在HEAD文件中,可以查看该文件得到当前HEAD对应的branch,并通过branch查到对应的commit对象

    # cat .git/HEAD
    ref: refs/heads/master
    # cat .git/refs/heads/master
    ca1114de8da76527ec73cdf52100eb7ba58e1878
    

    使用-t参数查看文件类型,使用-p参数可以查看文件内容

    # git cat-file -t ca1114
    commit
    # git cat-file -p ca1114
    tree d2df53f517e6e2c85ff2b0c6b0970428889f265f
    author ssgeek <ssgeek@ssgeek.com> 1622445604 +0800
    committer ssgeek <ssgeek@ssgeek.com> 1622445604 +0800
    
    first commit
    
    Signed-off-by: ssgeek <ssgeek@ssgeek.com>
    

    这是一个commit对象,commit对象中保存了commit的作者,commit的描述信息,签名信息以及该commit中包含哪些tree对象和blob对象,继续看该tree对象中的内容

    # 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中存储了三种类型的对象,committreeblob,三者分别对应git commit,此commit中的目录和文件,这些对象之间的关系如下图

    4、处理大文件

    4.1 大文件的产生

    由上面的详细分析流程可以看出,git会为每一个提交到版本控制的文件进行追踪,那么大文件究竟如何产生呢?

    在上面的object目录下还存在着packinfo文件夹。Git往磁盘保存对象时默认使用的格式叫松散对象loose object格式,当你对同一个文件修改哪怕一行,git都会使用全新的文件存储这个修改了的文件,放在了objects中。Git时不时地将这些对象打包至一个叫packfile的二进制文件以节省空间并提高效率,当版本库中有太多的松散对象,或者你手动执行 git gc 命令,或者你向远程服务器执行推送时,Git都会这样做

    因此,往往在向git中提交了大文件,会造成pack文件过大,到这里“元凶”终于出现了

    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
    

    参数说明:

    rev-list 命令用来列出Git仓库中的提交,我们用它来列出所有提交中涉及的文件名及其ID。 该命令可以指定只显示某个引用(或分支)的上下游的提交
    --objects 列出该提交涉及的所有文件ID
    --all 所有分支的提交,相当于指定了位于/refs下的所有引用
    verify-pack 命令用于显示已打包的内容,我们用它来找到那些大文件
    -v(verbose)参数是打印详细信息

    4.3 删除大文件

    # git filter-branch --force --prune-empty --index-filter 'git rm -rf --cached --ignore-unmatch YOU-FILE-NAME' --tag-name-filter cat -- --all
    

    参数说明:

    filter-branch 命令可以用来重写Git仓库中的提交
    --index-filter 参数用来指定一条Bash命令,然后Git会检出(checkout)所有的提交, 执行该命令,然后重新提交
    –all 参数表示我们需要重写所有分支(或引用)
    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 '
    ' ' ' >> 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
    # 设置reflog过期
    git reflog expire --expire=now --all
    # 清理垃圾
    git gc --aggressive --prune=now
    

    5、大文件存储的正确方式

    大文件一般是不建议直接存储到git仓库中的,git仓库是代码仓库,存放的应该是n个代码文件(其实也可以认为是文本文件)

    如果是作为仓库管理员,应该有意识的将git仓库设置一个允许的文件大小限制

    如果是非变化性的大文件,可以存储到专用的文件服务器、对象存储等

    如果非要在版本库中存储大文件,更好的方式是通过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
    

    关于git-lfs的使用及原理说明可以参考国内的gitee官方帮助说明文档Git LFS 操作指南

    6、其他解决方案

    除了上面的操作,还可以利用更为好用的开源效率工具bfg进行清理,参考bfg文档,配置好java环境后,操作如下

    # 下载封装好的jar包
    $ wget https://repo1.maven.org/maven2/com/madgag/bfg/1.13.0/bfg-1.13.0.jar
    
    # 克隆的时候需要--mirror参数
    $ git clone --mirror git://example.com/big-repo.git
    
    # 运行BFG来清理存储库
    $ java -jar bfg.jar --strip-blobs-bigger-than 100M big-repo.git
    
    # 去除脏数据
    $ 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、小结

    本文分析了git底层版本控制的存储实现,分析了版本控制系统中大文件的产生,并通过一定手段进行解决。

    要提到的是,上面的操作难免也会出现风险,如果是作为一个规范的代码仓库,应该在前期就做好规划,避免大文件提交到仓库,规范每一次的提交记录,做好code review及仓库管理

    See you ~

    参考文档:https://harttle.land/2016/03/22/purge-large-files-in-gitrepo.html#header-6

  • 相关阅读:
    @Value注解无法为static 变量赋值
    CloseableHttpClient方式配置代理服务器访问外网
    微信小程序上传图片
    centos7 无界面静默安装 oracle
    Linux安装配置JDK
    java导出Excel定义导出模板
    ECharts柱状图
    Java获取客户端真实IP
    Eclipse控制台输出log日志中文乱码
    $_SRRVER常用用法
  • 原文地址:https://www.cnblogs.com/ssgeek/p/14834328.html
Copyright © 2011-2022 走看看