为什么我的 Git 仓库这么大?

145M = . git/Objects/pack/

我编写了一个脚本,在提交从每个分支的末端向后退之前,将每个提交和提交的大小差异加起来。我得到了129MB,这不需要压缩,也不需要考虑分支之间的相同文件和分支之间的通用历史记录。

Git 考虑了所有这些因素,所以我认为存储库会小得多。那为什么。这么大?

我做过:

git fsck --full
git gc --prune=today --aggressive
git repack

为了回答有多少文件/提交,我在每个分支中有19个分支,每个分支中有大约40个文件。 287次提交,使用:

git log --oneline --all|wc -l

它不应该采取10兆字节的存储信息关于这一点。

107859 次浏览

你试过用 收拾东西吗?

存储在 .git中的其他 git 对象包括树、提交和标记。提交和标记都很小,但是如果存储库中有大量的小文件,树就会变得特别大。你有多少文件和多少提交?

你确定你只计算了。包文件而不是。IDX 文件?它们与。包文件,但是没有任何存储库数据(正如扩展名所指出的,它们只不过是相应的包 & mash; 的索引; 事实上,如果您知道正确的命令,您可以轻松地从包文件重新创建它们,而 git 本身在克隆时也这样做,因为只有包文件是使用本机 git 协议传输的)。

作为一个有代表性的例子,我看了一下我本地克隆的 linux-2.6存储库:

$ du -c *.pack
505888  total


$ du -c *.idx
34300   total

这表明7% 左右的增长应该是常见的。

还有 objects/之外的文件; 根据我个人的经验,其中 indexgitk.cache往往是最大的文件(在我的 linux-2.6存储库克隆中总共有11M)。

我最近将错误的远程存储库放入了本地存储库(git remote add ...git remote update)。在删除不需要的远程引用、分支和标记之后,我仍然有1.4 GB (!)在我的仓库里浪费的空间。我只能通过用 git clone file:///path/to/repository克隆它来摆脱它。请注意,在克隆本地存储库时,file://的作用是完全不同的——只有被引用的对象被复制,而不是整个目录结构。

编辑: 以下是伊恩在新回购中重建所有分支机构的一条建议:

d1=#original repo
d2=#new repo (must already exist)
cd $d1
for b in $(git branch | cut -c 3-)
do
git checkout $b
x=$(git rev-parse HEAD)
cd $d2
git checkout -b $b $x
cd $d1
done

git gc已经做了一个 git repack,所以没有意义的手动重新包装,除非你要传递一些特殊的选项给它。

第一步是查看大部分空间是否是您的对象数据库(正常情况下是这样的)。

git count-objects -v

这应该提供一个报告,说明存储库中有多少未打包的对象、它们占用了多少空间、有多少打包文件以及它们占用了多少空间。

理想情况下,在重新打包之后,您将没有未打包的对象和一个打包文件,但是有一些没有被当前分支直接引用的对象仍然存在并且未打包是完全正常的。

如果你有一个单一的大包,你想知道什么是占用的空间,然后你可以列出的对象,组成包以及他们如何存储。

git verify-pack -v .git/objects/pack/pack-*.idx

请注意,verify-pack采用索引文件,而不是包文件本身。这个报告给每个物体在包,它的真实大小和它的包装大小,以及信息,它是否已经“精致化”,如果是这样的三角洲链的起源。

要查看存储库中是否有任何异常大的对象,可以对第四列中的第三列(例如 | sort -k3n)的输出进行数字排序。

从这个输出中,您将能够使用 git show命令查看任何对象的内容,尽管不可能准确地查看在存储库的提交历史中引用该对象的位置。如果你需要这样做,尝试从 这个问题的东西。

在做 git filter-Branch & git gc 之前,你应该检查一下在你的回购中出现的标签。任何真正的系统,如自动标签的事情,如持续集成和部署将使无用的对象仍然引用这些标签,因此 gc 不能删除它们,你仍然会想知道为什么回购的大小仍然这么大。

摆脱所有不想要的东西的最好方法是运行 git-filter & git gc,然后将 master 推向一个新的裸回购。新的光秃秃的回购将有一棵清理干净的树。

仅供参考,最大的原因,为什么你可能结束与不想要的对象被保留在周围是,git 维护一个 reflog。

当您不小心删除了主分支或者以某种方式灾难性地损坏了存储库时,重新挂起是为了保护您。

解决这个问题的最简单的方法是在压缩之前截断 reflog (只要确保您永远不想回到 reflog 中的任何提交)。

git gc --prune=now --aggressive
git repack

这与 git gc --prune=today的不同之处在于,它会立即过期整个重新测试。

我使用的一些脚本:

Git-fatfiles

git rev-list --all --objects | \
sed -n $(git rev-list --objects --all | \
cut -f1 -d' ' | \
git cat-file --batch-check | \
grep blob | \
sort -n -k 3 | \
tail -n40 | \
while read hash type size; do
echo -n "-e s/$hash/$size/p ";
done) | \
sort -n -k1
...
89076 images/screenshots/properties.png
103472 images/screenshots/signals.png
9434202 video/parasite-intro.avi

如果需要更多行,请参见相邻答案中的 Perl 版本: https://stackoverflow.com/a/45366030/266720

Git 根除(针对 video/parasite.avi) :

git filter-branch -f  --index-filter \
'git rm --force --cached --ignore-unmatch video/parasite-intro.avi' \
-- --all
rm -Rf .git/refs/original && \
git reflog expire --expire=now --all && \
git gc --aggressive && \
git prune

注意: 第二个脚本的目的是从 Git 中完全删除信息(包括所有来自 reflog 的信息)。

如果您意外地添加了一大块文件并暂存了它们,而不一定要提交它们,就会发生这种情况。这可能发生在一个 rails应用程序,当你运行 bundle install --deployment,然后偶然 git add .,然后你看到所有的文件添加到 vendor/bundle下,你取消他们,但他们已经进入 git 历史,所以你必须应用 Vi 的回答和改变 然后运行他提供的第二个命令

您可以看到与 git count-objects -v的区别,在我的例子中,应用脚本之前有一个大小包: 52K,应用之后是3.8 K。

如果您想找出哪些文件占用了 git 存储库中的空间,请运行

git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -5

然后,提取占用最多空间的 blob 引用(最后一行) ,并检查占用如此多空间的文件名

git rev-list --objects --all | grep <reference>

这甚至可能是一个您用 git rm删除的文件,但是 git 会记住它,因为仍然有对它的引用,比如标记、远程和 reflog。

一旦您知道要删除哪个文件,我建议您使用 git forget-blob

Https://ownyourbits.com/2017/01/18/completely-remove-a-file-from-a-git-repository-with-git-forget-blob/

它很容易使用,只是做

git forget-blob file-to-forget

这将从 git 中删除所有引用,从历史记录中的每次提交中删除 blob,并运行垃圾回收以释放空间。

如果您希望查看所有 blob 的大小,那么 Vi 提供的 git-fatfiles 脚本非常可爱,但是它太慢以至于无法使用。我取消了40行的输出限制,它试图使用我的电脑的所有内存,而不是完成。另外,当对输出求和以查看文件使用的所有空间时,它会给出不准确的结果。

我用锈重写了它,我发现它比其他语言更不容易出错。我还添加了这样一个特性: 如果传递了 --directories标志,那么总结各个目录中的所有提交所使用的空间。可以给出路径来限制对某些文件或目录的搜索。

Src/main.rs:

use std::{
collections::HashMap,
io::{self, BufRead, BufReader, Write},
path::{Path, PathBuf},
process::{Command, Stdio},
thread,
};


use bytesize::ByteSize;
use structopt::StructOpt;


#[derive(Debug, StructOpt)]
#[structopt()]
pub struct Opt {
#[structopt(
short,
long,
help("Show the size of directories based on files committed in them.")
)]
pub directories: bool,


#[structopt(help("Optional: only show the size info about certain paths."))]
pub paths: Vec<String>,
}


/// The paths list is a filter. If empty, there is no filtering.
/// Returns a map of object ID -> filename.
fn get_revs_for_paths(paths: Vec<String>) -> HashMap<String, PathBuf> {
let mut process = Command::new("git");
let mut process = process.arg("rev-list").arg("--all").arg("--objects");


if !paths.is_empty() {
process = process.arg("--").args(paths);
};


let output = process
.output()
.expect("Failed to execute command git rev-list.");


let mut id_map = HashMap::new();
for line in io::Cursor::new(output.stdout).lines() {
if let Some((k, v)) = line
.expect("Failed to get line from git command output.")
.split_once(' ')
{
id_map.insert(k.to_owned(), PathBuf::from(v));
}
}
id_map
}


/// Returns a map of object ID to size.
fn get_sizes_of_objects(ids: Vec<&String>) -> HashMap<String, u64> {
let mut process = Command::new("git")
.arg("cat-file")
.arg("--batch-check=%(objectname) %(objecttype) %(objectsize:disk)")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to execute command git cat-file.");
let mut stdin = process.stdin.expect("Could not open child stdin.");


let ids: Vec<String> = ids.into_iter().cloned().collect(); // copy data for thread


// Stdin will block when the output buffer gets full, so it needs to be written
// in a thread:
let write_thread = thread::spawn(|| {
for obj_id in ids {
writeln!(stdin, "{}", obj_id).expect("Could not write to child stdin");
}
drop(stdin);
});


let output = process
.stdout
.take()
.expect("Could not get output of command git cat-file.");


let mut id_map = HashMap::new();
for line in BufReader::new(output).lines() {
let line = line.expect("Failed to get line from git command output.");


let line_split: Vec<&str> = line.split(' ').collect();


// skip non-blob objects
if let [id, "blob", size] = &line_split[..] {
id_map.insert(
id.to_string(),
size.parse::<u64>().expect("Could not convert size to int."),
);
};
}
write_thread.join().unwrap();
id_map
}


fn main() {
let opt = Opt::from_args();


let revs = get_revs_for_paths(opt.paths);
let sizes = get_sizes_of_objects(revs.keys().collect());


// This skips directories (they have no size mapping).
// Filename -> size mapping tuples. Files are present in the list more than once.
let file_sizes: Vec<(&Path, u64)> = sizes
.iter()
.map(|(id, size)| (revs[id].as_path(), *size))
.collect();


// (Filename, size) tuples.
let mut file_size_sums: HashMap<&Path, u64> = HashMap::new();
for (mut path, size) in file_sizes.into_iter() {
if opt.directories {
// For file path "foo/bar", add these bytes to path "foo/"
let parent = path.parent();
path = match parent {
Some(parent) => parent,
_ => {
eprint!("File has no parent directory: {}", path.display());
continue;
}
};
}


*(file_size_sums.entry(path).or_default()) += size;
}
let sizes: Vec<(&Path, u64)> = file_size_sums.into_iter().collect();


print_sizes(sizes);
}


fn print_sizes(mut sizes: Vec<(&Path, u64)>) {
sizes.sort_by_key(|(_path, size)| *size);
for file_size in sizes.iter() {
// The size needs some padding--a long size is as long as a tabstop
println!("{:10}{}", ByteSize(file_size.1), file_size.0.display())
}
}

返回文章页面货物:

[package]
name = "git-fatfiles"
version = "0.1.0"
edition = "2018"
[dependencies]
structopt = { version = "0.3"}
bytesize = {version = "1"}

选择:

USAGE:
git-fatfiles [FLAGS] [paths]...


FLAGS:
-d, --directories    Show the size of directories based on files committed in them.
-h, --help           Prints help information


ARGS:
<paths>...    Optional: only show the size info about certain paths.

值得检查 stacktrace.log。它基本上是一个跟踪失败提交的错误日志。我最近发现我的 stacktrace.log 是65.5 GB,我的应用程序是66.7 GB。

创建新的分支,其中当前提交是所有历史记录都消失的初始提交,以减少 git 对象和历史记录大小。

注意: 请在运行代码之前阅读注释。

  1. Git checkout ——孤儿最新 _ 分支
  2. Git add-A
  3. Git commit-a-m“ Initialcommit message”# 提交更改
  4. Git 分支-D master # 删除 master 分支
  5. Git Branch-m master # 将 Branch 重命名为 master
  6. Git push-f 原点 master # push to master Branch
  7. Git gc ——咄咄逼人—— prune = all # 删除旧文件

我已经创建了原来在 这个答案中提供的 perl 脚本的一个新实现(后来在生锈时重写了它)。在对 perl 脚本进行了大量研究之后,我意识到它有多个 bug:

  • 带空格路径的错误
  • --sum不能正常工作(它实际上没有把所有的 delta 加起来)
  • --directory不能正常工作(它依赖于 --sum)
  • 如果没有 --sum,它将报告给定路径的有效随机对象的大小,这可能不是最大的路径

所以我最后完全改写了剧本。它使用相同的 git 命令序列(git rev-listgit cat-file) ,但是它会正确地处理数据以得到准确的结果。我保留了 --sum--directories的特性。

我还更改了它,以报告文件的“磁盘”大小(即 git repo 中的压缩大小) ,而不是原始文件大小。这似乎与眼前的问题更为相关。(如果有人出于某种原因想要未压缩的尺寸,这可以是可选的。)

我还添加了一个选项,只报告已删除的文件,假设仍在使用的文件可能不那么有趣。(我的做法有点拙劣; 欢迎提出建议。)

我也可以在这里复制它,如果这是一个很好的 StackOverflow 礼仪? (它大约有180行长。)