use std::fs;
use std::io::{stdout, Write};
use std::path::Path;
use std::process::Command;
use humansize::{file_size_opts, FileSize};
use crate::library::Error;
use crate::library::*;
fn gc_repo(path: &Path, dry_run: bool) -> Result<(u64, u64), Error> {
let repo_name = match path.iter().last() {
Some(name) => name.to_str().unwrap().to_string(),
None => "<unknown>".to_string(),
};
debug_assert_ne!(repo_name, "<unknown>", "unknown repo name: '{:?}'", &path);
print!("Recompressing '{}': ", &repo_name);
if !path.is_dir() {
return Err(Error::GitRepoDirNotFound(path.into()));
}
let repo_size_before = cumulative_dir_size(path).dir_size;
let sb_human_readable = repo_size_before.file_size(file_size_opts::DECIMAL).unwrap();
print!("{} => ", sb_human_readable);
let _ignore = stdout().flush();
if dry_run {
println!("{} (+0)", sb_human_readable);
Ok((0, 0))
} else {
let repo = match git2::Repository::open(path) {
Ok(repo) => repo,
Err(_e) => return Err(Error::GitRepoNotOpened(path.into())),
};
let repo_path = repo.path();
if let Err(e) = Command::new("git")
.arg("reflog")
.arg("expire")
.arg("--expire=1.minute")
.arg("--all")
.current_dir(repo_path)
.output()
{
return Err(Error::GitReflogFailed(path.into(), e));
}
if let Err(e) = Command::new("git")
.arg("pack-refs")
.arg("--all")
.arg("--prune")
.current_dir(repo_path)
.output()
{
return Err(Error::GitPackRefsFailed(path.into(), e));
}
if let Err(e) = Command::new("git")
.arg("gc")
.arg("--prune=now")
.current_dir(repo_path)
.output()
{
return Err(Error::GitGCFailed(path.into(), e));
}
if let Err(e) = Command::new("git")
.arg("repack")
.arg("-a")
.arg("-d")
.arg("-f")
.arg("--depth=250")
.arg("--window=250")
.arg("--max-pack-size=1G")
.arg("--unpack-unreachable=now")
.current_dir(repo_path)
.output()
{
return Err(Error::GitRepackFailed(path.into(), e));
}
let repo_size_after = cumulative_dir_size(path).dir_size;
println!(
"{}",
size_diff_format(repo_size_before, repo_size_after, false)
);
Ok((repo_size_before, repo_size_after))
}
}
#[allow(clippy::module_name_repetitions)]
pub(crate) fn git_gc_everything(
git_repos_bare_dir: &Path,
registry_pkg_cache_dir: &Path,
dry_run: bool,
) -> Result<(), Error> {
fn gc_subdirs(path: &Path, dry_run: bool) -> Result<(u64, u64), Error> {
if path.is_file() {
return Err(Error::GitGCFile(path.to_path_buf()));
} else if !path.is_dir() {
return Ok((0, 0));
}
let mut size_sum_before: u64 = 0;
let mut size_sum_after: u64 = 0;
let mut git_repos: Vec<_> = fs::read_dir(path)
.unwrap()
.map(|x| x.unwrap().path())
.collect();
git_repos.sort();
for repo in git_repos {
let (size_before, size_after) = match gc_repo(&repo, dry_run) {
Ok((before, after)) => (before, after),
Err(error) => match error {
Error::GitGCFailed(_, _)
| Error::GitRepoDirNotFound(_)
| Error::GitRepoNotOpened(_) => {
eprintln!("{}", error);
continue;
}
_ => unreachable!(),
},
};
size_sum_before += size_before;
size_sum_after += size_after;
}
Ok((size_sum_before, size_sum_after))
}
if Command::new("git").arg("help").output().is_err() {
return Err(Error::GitNotInstalled);
}
let mut total_size_before: u64 = 0;
let mut total_size_after: u64 = 0;
println!("\nRecompressing repositories. This may take some time...");
let (repos_before, repos_after) = gc_subdirs(git_repos_bare_dir, dry_run)?;
total_size_before += repos_before;
total_size_after += repos_after;
println!("\nRecompressing registries. This may take some time...");
let mut repo_index = registry_pkg_cache_dir.to_path_buf();
let _ = repo_index.pop();
repo_index.push("index");
let (regs_before, regs_after) = gc_subdirs(&repo_index, dry_run)?;
total_size_before += regs_before;
total_size_after += regs_after;
println!(
"\nCompressed {} to {}",
total_size_before
.file_size(file_size_opts::DECIMAL)
.unwrap(),
size_diff_format(total_size_before, total_size_after, false)
);
Ok(())
}
fn fsck_repo(path: &Path) -> Result<(), Error> {
let repo_name = match path.iter().last() {
Some(name) => name.to_str().unwrap().to_string(),
None => "<unknown>".to_string(),
};
debug_assert_ne!(repo_name, "<unknown>", "unknown repo name: '{:?}'", &path);
println!("Fscking '{}'", &repo_name);
if !path.is_dir() {
return Err(Error::GitRepoDirNotFound(path.into()));
}
let repo = match git2::Repository::open(path) {
Ok(repo) => repo,
Err(_e) => return Err(Error::GitRepoNotOpened(path.into())),
};
let repo_path = repo.path();
if let Err(e) = Command::new("git")
.arg("fsck")
.arg("--no-progress")
.arg("--strict")
.current_dir(repo_path)
.output()
{
return Err(Error::GitFsckFailed(path.into(), e));
}
Ok(())
}
#[allow(clippy::module_name_repetitions)]
pub(crate) fn git_fsck_everything(
git_repos_bare_dir: &Path,
registry_pkg_cache_dir: &Path,
) -> Result<(), Error> {
fn fsck_subdirs(path: &Path) {
if path.is_file() {
panic!(
"fsck_subdirs() tried to fsck file instead of directory: '{}'",
path.display()
);
} else if !path.is_dir() {
return;
}
let mut git_repos: Vec<_> = fs::read_dir(path)
.unwrap()
.map(|x| x.unwrap().path())
.collect();
git_repos.sort();
for repo in git_repos {
match fsck_repo(&repo) {
Ok(_) => {}
Err(error) => match error {
Error::GitFsckFailed(_, _)
| Error::GitRepoDirNotFound(_)
| Error::GitRepoNotOpened(_) => {
eprintln!("{}", error);
continue;
}
_ => unreachable!(),
},
};
}
}
if Command::new("git").arg("help").output().is_err() {
return Err(Error::GitNotInstalled);
}
println!("\nFscking repositories. This may take some time...");
fsck_subdirs(git_repos_bare_dir);
println!("\nFscking registries. This may take some time...");
let mut repo_index = registry_pkg_cache_dir.to_path_buf();
let _ = repo_index.pop();
repo_index.push("index");
fsck_subdirs(&repo_index);
Ok(())
}
#[cfg(test)]
mod gittest {
use super::*;
use std::fs::File;
use std::path::PathBuf;
use std::process::Command;
#[test]
fn test_gc_repo() {
let git_init = Command::new("git")
.arg("init")
.arg("gitrepo_gc")
.current_dir("target")
.output();
assert!(
git_init.is_ok(),
"git_init did not succeed: '{:?}'",
git_init
);
let mut file = File::create("target/gitrepo_gc/testfile.txt").unwrap();
file.write_all(b"Hello hello hello this is a test \n hello \n hello")
.unwrap();
let git_add = Command::new("git")
.arg("add")
.arg("testfile.txt")
.current_dir("target/gitrepo_gc/")
.output();
assert!(git_add.is_ok(), "git add did not succeed: '{:?}'", git_add);
let git_commit = Command::new("git")
.arg("commit")
.arg("-m")
.arg("commit msg")
.current_dir("target/gitrepo_gc/")
.output();
assert!(
git_commit.is_ok(),
"git commit did not succeed: '{:?}'",
git_commit
);
let mut file2 = File::create("target/gitrepo_gc/testfile.txt").unwrap();
file2
.write_all(
b"Hello hello hello this is a test \n bla bla bla bla bla \n hello
\n this is some more text\n
lorem ipsum",
)
.unwrap();
let git_add2 = Command::new("git")
.arg("add")
.arg("testfile.txt")
.current_dir("target/gitrepo_gc/")
.output();
assert!(git_add2.is_ok(), "git add did not succeed: '{:?}'", git_add);
let git_commit2 = Command::new("git")
.arg("commit")
.arg("-m")
.arg("another commit msg")
.current_dir("target/gitrepo_gc/")
.output();
assert!(
git_commit2.is_ok(),
"git commit did not succeed: '{:?}'",
git_commit2
);
let (dryrun_before, dryrun_after) = match gc_repo(
&PathBuf::from("target/gitrepo_gc/"),
true,
) {
Ok((x, y)) => (x, y),
_ => (0, 0),
};
assert_eq!(dryrun_before, 0);
assert_eq!(dryrun_after, 0);
let (before, after) = match gc_repo(
&PathBuf::from("target/gitrepo_gc/"),
false,
) {
Ok((x, y)) => (x, y),
_ => (0, 0),
};
assert!(
!before > after,
"git gc is funky: before: {} after: {}",
before,
after
);
}
#[test]
fn test_fsck_repo() {
let git_init = Command::new("git")
.arg("init")
.arg("gitrepo_fsck")
.current_dir("target")
.output();
assert!(
git_init.is_ok(),
"git_init did not succeed: '{:?}'",
git_init
);
let mut file = File::create("target/gitrepo_fsck/testfile.txt").unwrap();
file.write_all(b"Hello hello hello this is a test \n hello \n hello")
.unwrap();
let git_add = Command::new("git")
.arg("add")
.arg("testfile.txt")
.current_dir("target/gitrepo_fsck/")
.output();
assert!(git_add.is_ok(), "git add did not succeed: '{:?}'", git_add);
let git_commit = Command::new("git")
.arg("commit")
.arg("-m")
.arg("commit msg")
.current_dir("target/gitrepo_fsck/")
.output();
assert!(
git_commit.is_ok(),
"git commit did not succeed: '{:?}'",
git_commit
);
let mut file2 = File::create("target/gitrepo_fsck/testfile.txt").unwrap();
file2
.write_all(
b"Hello hello hello this is a test \n bla bla bla bla bla \n hello
\n this is some more text\n
lorem ipsum",
)
.unwrap();
let git_add2 = Command::new("git")
.arg("add")
.arg("testfile.txt")
.current_dir("target/gitrepo_fsck/")
.output();
assert!(
git_add2.is_ok(),
"git add did not succeed: '{:?}'",
git_add2
);
let git_commit2 = Command::new("git")
.arg("commit")
.arg("-m")
.arg("another commit msg")
.current_dir("target/gitrepo_fsck/")
.output();
assert!(
git_commit2.is_ok(),
"git commit did not succeed: '{:?}'",
git_commit2
);
let res = fsck_repo(&PathBuf::from("target/gitrepo_fsck/"));
assert!(res.is_ok(), "Failed to fsck git repo: {:?}", res);
}
}