use crate::error::*;
use fungus::prelude::*;
use git2::{
self,
build::{CheckoutBuilder, RepoBuilder},
FetchOptions, RemoteCallbacks, Repository,
};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::{
path::{Path, PathBuf},
thread,
};
const TMPDIR: &str = "git";
#[derive(Default)]
pub struct RepoGroup<'a> {
repos: Vec<Repo<'a>>,
style: Option<ProgressStyle>,
progress: Option<MultiProgress>,
}
impl<'a> RepoGroup<'a> {
pub fn new() -> Self {
Self { ..Default::default() }
}
#[allow(clippy::should_implement_trait)]
pub fn add(mut self, repo: Repo<'a>) -> Self {
self.repos.push(repo);
self
}
pub fn with_progress(mut self, yes: bool) -> Self {
if yes {
let progress = MultiProgress::new();
let mut style = ProgressStyle::default_bar();
style = style.progress_chars("=>-").template("[{elapsed_precise}][{bar:50.cyan/blue}] {pos:>7}/{len:7} ({eta}) - {msg}");
self.progress = Some(progress);
self.style = Some(style);
}
self
}
#[allow(clippy::should_implement_trait)]
pub fn clone(&self) -> Result<()> {
let mut threads = Vec::new();
for repo in &self.repos {
let path = repo.path_val().to_path_buf();
let url = repo.url_val().ok_or(Error::UrlNotSet)?.to_string();
if self.progress.is_none() {
threads.push(thread::spawn(move || {
Repo::new(path).unwrap().url(url).clone().unwrap();
}));
} else {
let progress = self.progress.as_ref().unwrap();
let progress_bar = progress.add(ProgressBar::new(0).with_style(self.style.as_ref().unwrap().clone()));
progress_bar.set_message(&url);
let msg = url.clone();
thread::spawn(move || {
let mut xfer_init = false;
let mut check_init = false;
Repo::new(path)
.unwrap()
.url(url)
.xfer_progress(|total, cur| {
if !xfer_init {
progress_bar.set_length(total);
xfer_init = true;
}
progress_bar.set_position(cur);
})
.checkout_progress(|total, cur| {
if !check_init {
progress_bar.set_length(total);
check_init = true;
}
progress_bar.set_position(cur);
})
.clone()
.unwrap();
progress_bar.finish_with_message(&msg);
});
}
}
if self.progress.is_none() {
for thread in threads {
thread.join().unwrap();
}
} else {
let progress = self.progress.as_ref().unwrap();
progress.join()?;
}
Ok(())
}
pub fn update(&self) -> Result<()> {
let mut threads = Vec::new();
for repo in &self.repos {
let path = repo.path_val().to_path_buf();
let url = repo.url_val().ok_or(Error::UrlNotSet)?.to_string();
if self.progress.is_none() {
threads.push(thread::spawn(move || {
Repo::new(path).unwrap().url(url).clone().unwrap();
}));
} else {
let progress = self.progress.as_ref().unwrap();
let progress_bar = progress.add(ProgressBar::new(0).with_style(self.style.as_ref().unwrap().clone()));
progress_bar.set_message(&url);
let msg = url.clone();
thread::spawn(move || {
let mut xfer_init = false;
let mut check_init = false;
let mut update_init = false;
Repo::new(path)
.unwrap()
.url(url)
.xfer_progress(|total, cur| {
if !xfer_init {
progress_bar.set_length(total);
xfer_init = true;
}
progress_bar.set_position(cur);
})
.checkout_progress(|total, cur| {
if !check_init {
progress_bar.set_length(total);
check_init = true;
}
progress_bar.set_position(cur);
})
.update_progress(|total, cur| {
if !update_init {
progress_bar.set_length(total);
update_init = true;
}
progress_bar.set_position(cur);
})
.update()
.unwrap();
progress_bar.finish_with_message(&msg);
});
}
}
if self.progress.is_none() {
for thread in threads {
thread.join().unwrap();
}
} else {
let progress = self.progress.as_ref().unwrap();
progress.join()?;
}
Ok(())
}
}
#[derive(Default)]
pub struct Repo<'a> {
path: PathBuf, url: Option<String>, branch_only: bool, branch: Option<String>, xfer_progress: Option<Box<dyn FnMut(u64, u64)+'a>>, update_progress: Option<Box<dyn FnMut(u64, u64)+'a>>, checkout_progress: Option<Box<dyn FnMut(u64, u64)+'a>>, }
impl<'a> Repo<'a> {
pub fn branch_val(&self) -> Option<&str> {
self.branch.as_deref()
}
pub fn branch_only_val(&self) -> bool {
self.branch_only
}
pub fn path_val(&self) -> &Path {
&self.path
}
pub fn url_val(&self) -> Option<&str> {
self.url.as_deref()
}
pub fn branch<T>(mut self, branch: T) -> Self
where
T: AsRef<str>,
{
self.branch = Some(branch.as_ref().to_string());
self
}
pub fn branch_only(mut self, yes: bool) -> Self {
self.branch_only = yes;
self
}
pub fn url<T>(mut self, url: T) -> Self
where
T: AsRef<str>,
{
self.url = Some(url.as_ref().to_string());
self
}
pub fn xfer_progress<T>(mut self, func: T) -> Self
where
T: FnMut(u64, u64)+'a,
{
self.xfer_progress = Some(Box::new(func));
self
}
pub fn checkout_progress<T>(mut self, func: T) -> Self
where
T: FnMut(u64, u64)+'a,
{
self.checkout_progress = Some(Box::new(func));
self
}
pub fn update_progress<T>(mut self, func: T) -> Self
where
T: FnMut(u64, u64)+'a,
{
self.update_progress = Some(Box::new(func));
self
}
pub fn new<T>(path: T) -> Result<Self>
where
T: AsRef<Path>,
{
let path = path.as_ref().abs()?;
Ok(Self { path, ..Default::default() })
}
pub fn last_msg(&self) -> Result<String> {
let repo = Repository::open(self.path_val())?;
let head = repo.head()?.peel_to_commit()?;
let msg = head.message().ok_or(Error::NoMessageWasFound)?;
Ok(msg.trim_end().to_string())
}
pub fn clone(mut self) -> Result<PathBuf> {
let mut builder = RepoBuilder::new();
if self.branch_only {
let branch = match &self.branch {
Some(x) => x.clone(),
None => "master".to_string(),
};
builder.branch(&branch);
builder.remote_create(move |repo, name, url| {
let refspec = format!("+refs/heads/{0:}:refs/remotes/origin/{0:}", &branch);
repo.remote_with_fetch(name, url, &refspec)
});
}
if self.xfer_progress.is_some() {
let mut xfer = self.xfer_progress.take().unwrap();
let mut callback = RemoteCallbacks::new();
callback.transfer_progress(move |stats| {
xfer(stats.total_objects() as u64, stats.indexed_objects() as u64);
true
});
let mut fetchopts = FetchOptions::new();
fetchopts.remote_callbacks(callback);
builder.fetch_options(fetchopts);
}
if self.checkout_progress.is_some() {
let mut checkout = self.checkout_progress.take().unwrap();
let mut checkout_bldr = CheckoutBuilder::new();
checkout_bldr.progress(move |_, cur, total| checkout(total as u64, cur as u64));
builder.with_checkout(checkout_bldr);
}
let url = self.url_val().ok_or(Error::UrlNotSet)?;
let path = self.path_val();
builder.clone(url, path)?;
Ok(self.path.clone())
}
pub fn update(mut self) -> Result<PathBuf> {
if !is_repo(self.path_val()) {
return self.clone();
} else {
let mut fetch_opts = Default::default();
let repo = Repository::open(self.path_val())?;
if self.update_progress.is_some() {
let mut xfer = self.xfer_progress.take().unwrap();
let mut callback = RemoteCallbacks::new();
callback.transfer_progress(move |stats| {
xfer(stats.total_objects() as u64, stats.indexed_objects() as u64);
true
});
let mut fetchopts = FetchOptions::new();
fetchopts.remote_callbacks(callback);
fetch_opts = fetchopts;
}
repo.find_remote("origin")?.fetch(&["master"], Some(&mut fetch_opts), None)?;
let fetch_head = repo.find_reference("FETCH_HEAD")?;
let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
let (analysis, _) = repo.merge_analysis(&[&fetch_commit])?;
if analysis.is_up_to_date() {
return Ok(self.path);
} else if analysis.is_fast_forward() {
let refname = "refs/heads/master";
let mut reference = repo.find_reference(&refname)?;
reference.set_target(fetch_commit.id(), "Fast-Forward")?;
repo.set_head(&refname)?;
repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?;
} else {
return Err(Error::FastForwardOnly);
}
}
Ok(self.path)
}
}
pub fn is_repo<T>(path: T) -> bool
where
T: AsRef<Path>,
{
sys::is_dir(path.as_ref().mash(".git"))
}
pub fn remote_branch_exists<T, U>(url: T, branch: U) -> Result<()>
where
T: AsRef<str>,
U: AsRef<str>,
{
let tmpdir = user::temp_dir(TMPDIR)?;
defer!(sys::remove_all(&tmpdir).unwrap());
let repo = git2::Repository::init_bare(&tmpdir)?;
let mut remote = repo.remote("origin", url.as_ref())?;
let refspec = format!("+refs/heads/{0:}:refs/remotes/origin/{0:}", branch.as_ref());
remote.fetch(&[&refspec], None, None)?;
repo.find_reference("FETCH_HEAD")?;
Ok(())
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
fn setup<T: AsRef<Path>>(path: T) -> PathBuf {
let temp = PathBuf::from("tests/temp").abs().unwrap();
sys::mkdir(&temp).unwrap();
temp.mash(path.as_ref())
}
#[test]
fn test_repogroup() {
let group = git::RepoGroup::new();
group.add(git::Repo::new("foo").unwrap());
}
#[test]
fn test_repo_branch() {
assert_eq!(git::Repo::new("foo").unwrap().branch("foobar").branch_val(), Some("foobar"));
let mut repo = git::Repo::new("foo").unwrap();
assert_eq!(repo.branch_val(), None);
repo = repo.branch("foobar");
assert_eq!(repo.branch_val(), Some("foobar"));
}
#[test]
fn test_repo_branch_only() {
assert_eq!(git::Repo::new("foo").unwrap().branch_only(true).branch_only_val(), true);
let mut repo = git::Repo::new("foo").unwrap();
assert_eq!(repo.branch_only_val(), false);
repo = repo.branch_only(true);
assert_eq!(repo.branch_only_val(), true);
}
#[test]
fn test_repo_path() {
assert_eq!(git::Repo::new("foo").unwrap().path_val(), Path::new("foo").abs().unwrap().as_path());
}
#[test]
fn test_repo_url() {
assert_eq!(git::Repo::new("foo").unwrap().url("foobar").url_val(), Some("foobar"));
let mut repo = git::Repo::new("foo").unwrap();
assert_eq!(repo.url_val(), None);
repo = repo.url("foobar");
assert_eq!(repo.url_val(), Some("foobar"));
}
#[test]
fn test_repo_clone() {
let tmpdir = setup("git_repo_clone");
let repo1 = tmpdir.mash("repo1");
let repo2 = tmpdir.mash("repo2");
let repo1file = repo1.mash("README.md");
let repo2file = repo2.mash("README.md");
assert!(sys::remove_all(&tmpdir).is_ok());
assert_eq!(repo1file.exists(), false);
assert!(git::Repo::new(&repo1).unwrap().url("https://github.com/phR0ze/alpine-base.git").clone().is_ok());
assert_eq!(sys::readlines(&repo1file).unwrap()[0], "alpine-base".to_string());
assert_eq!(repo1file.exists(), true);
assert_eq!(repo2file.exists(), false);
assert!(git::Repo::new(&repo2).unwrap().url("https://github.com/phR0ze/alpine-core.git").clone().is_ok());
assert_eq!(sys::readlines(&repo2file).unwrap()[0], "alpine-core".to_string());
assert_eq!(repo2file.exists(), true);
assert!(sys::remove_all(&tmpdir).is_ok());
}
#[test]
fn test_repo_clone_branch() {
let tmpdir = setup("git_repo_clone_branch");
let repo1 = tmpdir.mash("repo1");
let repo2 = tmpdir.mash("repo2");
let repo1file = repo1.mash("README.md");
let repo2file = repo2.mash("trunk/PKGBUILD");
assert!(sys::remove_all(&tmpdir).is_ok());
assert_eq!(repo1file.exists(), false);
assert!(git::Repo::new(&repo1).unwrap().url("https://github.com/phR0ze/alpine-base.git").branch("master").branch_only(true).clone().is_ok());
assert_eq!(sys::readlines(&repo1file).unwrap()[0], "alpine-base".to_string());
assert_eq!(repo1file.exists(), true);
assert_eq!(repo2file.exists(), false);
assert!(git::Repo::new(&repo2).unwrap().url("https://git.archlinux.org/svntogit/packages.git").branch("packages/pkgfile").branch_only(true).clone().is_ok());
assert_eq!(repo2file.exists(), true);
assert!(sys::remove_all(&tmpdir).is_ok());
}
#[test]
fn test_repo_clone_with_progress() {
let tmpdir = setup("git_repo_clone_with_progress");
let readme = tmpdir.mash("README.md");
assert!(sys::remove_all(&tmpdir).is_ok());
assert!(
git::Repo::new(&tmpdir)
.unwrap()
.url("https://github.com/phR0ze/alpine-base")
.xfer_progress(|total, cur| {
let _ = total + cur;
})
.checkout_progress(|total, cur| {
let _ = total + cur;
})
.clone()
.is_ok()
);
assert_eq!(readme.exists(), true);
assert_eq!(sys::readlines(&readme).unwrap()[0].starts_with("alpine-base"), true);
assert!(sys::remove_all(&tmpdir).is_ok());
}
#[test]
fn test_repo_clone_many() {
let tmpdir = setup("git_repo_clone_many");
let repo1 = tmpdir.mash("repo1");
let repo2 = tmpdir.mash("repo2");
let repo1file = repo1.mash("README.md");
let repo2file = repo2.mash("README.md");
assert!(sys::remove_all(&tmpdir).is_ok());
let repos = git::RepoGroup::new()
.add(git::Repo::new(&repo1).unwrap().url("https://github.com/phR0ze/alpine-base"))
.add(git::Repo::new(&repo2).unwrap().url("https://github.com/phR0ze/alpine-core"));
assert!(repos.clone().is_ok());
assert_eq!(repo1file.exists(), true);
assert_eq!(repo2file.exists(), true);
assert_eq!(sys::readlines(&repo1file).unwrap()[0].starts_with("alpine-"), true);
assert_eq!(sys::readlines(&repo2file).unwrap()[0].starts_with("alpine-"), true);
assert!(sys::remove_all(&tmpdir).is_ok());
}
#[test]
fn test_repo_clone_many_with_progress() {
let tmpdir = setup("git_repo_clone_many_with_progress");
let repo1 = tmpdir.mash("repo1");
let repo2 = tmpdir.mash("repo2");
let repo1file = repo1.mash("README.md");
let repo2file = repo2.mash("README.md");
assert!(sys::remove_all(&tmpdir).is_ok());
let repos = git::RepoGroup::new()
.with_progress(true)
.add(git::Repo::new(&repo1).unwrap().url("https://github.com/phR0ze/alpine-base"))
.add(git::Repo::new(&repo2).unwrap().url("https://github.com/phR0ze/alpine-core"));
assert!(repos.clone().is_ok());
assert_eq!(repo1file.exists(), true);
assert_eq!(repo2file.exists(), true);
assert_eq!(sys::readlines(&repo1file).unwrap()[0].starts_with("alpine-"), true);
assert_eq!(sys::readlines(&repo2file).unwrap()[0].starts_with("alpine-"), true);
assert!(sys::remove_all(&tmpdir).is_ok());
}
#[test]
fn test_is_repo() {
let tmpdir = setup("git_is_repo");
assert!(sys::remove_all(&tmpdir).is_ok());
assert_eq!(git::is_repo(&tmpdir), false);
assert!(git2::Repository::init(&tmpdir).is_ok());
assert_eq!(git::is_repo(&tmpdir), true);
assert!(sys::remove_all(&tmpdir).is_ok());
}
#[test]
fn test_last_msg() {
let tmpdir = setup("git_repo_last_msg");
let tarball = tmpdir.mash("../../alpine-base.tgz");
assert!(sys::remove_all(&tmpdir).is_ok());
assert!(tar::extract_all(&tarball, &tmpdir).is_ok());
assert_eq!(git::Repo::new(&tmpdir).unwrap().last_msg().unwrap(), "Use the workflow name for the badge".to_string());
assert!(sys::remove_all(&tmpdir).is_ok());
}
#[test]
fn test_remote_branch_exists() {
assert!(git::remote_branch_exists("https://github.com/phR0ze/alpine-base.git", "master").is_ok());
assert!(git::remote_branch_exists("https://git.archlinux.org/svntogit/packages.git", "packages/foobar").is_err());
assert!(git::remote_branch_exists("https://git.archlinux.org/svntogit/packages.git", "packages/pkgfile").is_ok());
assert!(git::remote_branch_exists("https://git.archlinux.org/svntogit/community.git", "packages/acme").is_ok());
}
#[test]
fn test_repo_update() {
let tmpdir = setup("git_repo_update");
let tarball = tmpdir.mash("../../alpine-base.tgz");
assert!(sys::remove_all(&tmpdir).is_ok());
assert_eq!(git::is_repo(&tmpdir), false);
assert!(git::Repo::new(&tmpdir).unwrap().url("https://github.com/phR0ze/alpine-base.git").update().is_ok());
assert_eq!(git::is_repo(&tmpdir), true);
let repo = git::Repo::new(&tmpdir).unwrap();
assert!(sys::remove_all(&tmpdir).is_ok());
assert!(tar::extract_all(&tarball, &tmpdir).is_ok());
assert_eq!(repo.last_msg().unwrap(), "Use the workflow name for the badge".to_string());
assert!(git::Repo::new(&tmpdir).unwrap().url("https://github.com/phR0ze/alpine-base.git").update().is_ok());
assert_ne!(repo.last_msg().unwrap(), "Use the workflow name for the badge".to_string());
assert!(sys::remove_all(&tmpdir).is_ok());
}
#[test]
fn test_update_with_progress() {
let tmpdir = setup("git_repo_update_with_progress");
let tarball = tmpdir.mash("../../alpine-base.tgz");
assert!(sys::remove_all(&tmpdir).is_ok());
assert_eq!(git::is_repo(&tmpdir), false);
assert!(
git::Repo::new(&tmpdir)
.unwrap()
.url("https://github.com/phR0ze/alpine-base")
.xfer_progress(|total, cur| {
let _ = total + cur;
})
.checkout_progress(|total, cur| {
let _ = total + cur;
})
.update_progress(|total, cur| {
let _ = total + cur;
})
.update()
.is_ok()
);
assert_eq!(git::is_repo(&tmpdir), true);
assert!(
git::Repo::new(&tmpdir)
.unwrap()
.url("https://github.com/phR0ze/alpine-base")
.xfer_progress(|total, cur| {
let _ = total + cur;
})
.checkout_progress(|total, cur| {
let _ = total + cur;
})
.update_progress(|total, cur| {
let _ = total + cur;
})
.update()
.is_ok()
);
let repo = git::Repo::new(&tmpdir).unwrap();
assert!(sys::remove_all(&tmpdir).is_ok());
assert!(tar::extract_all(&tarball, &tmpdir).is_ok());
assert_eq!(repo.last_msg().unwrap(), "Use the workflow name for the badge".to_string());
assert!(
git::Repo::new(&tmpdir)
.unwrap()
.url("https://github.com/phR0ze/alpine-base")
.xfer_progress(|total, cur| {
let _ = total + cur;
})
.checkout_progress(|total, cur| {
let _ = total + cur;
})
.update_progress(|total, cur| {
let _ = total + cur;
})
.update()
.is_ok()
);
assert_ne!(repo.last_msg().unwrap(), "Use the workflow name for the badge".to_string());
assert!(sys::remove_all(&tmpdir).is_ok());
}
#[test]
fn test_update_many_with_progress() {
let tmpdir = setup("git_update_many_with_progress");
let tarball = tmpdir.mash("../../alpine-base.tgz");
let repo1 = tmpdir.mash("repo1");
let repo2 = tmpdir.mash("repo2");
let repo1file = repo1.mash("README.md");
let repo2file = repo2.mash("README.md");
assert!(sys::remove_all(&tmpdir).is_ok());
let repos = git::RepoGroup::new()
.with_progress(true)
.add(git::Repo::new(&repo1).unwrap().url("https://github.com/phR0ze/alpine-base"))
.add(git::Repo::new(&repo2).unwrap().url("https://github.com/phR0ze/alpine-core"));
assert!(repos.update().is_ok());
assert_eq!(repo1file.exists(), true);
assert_eq!(repo2file.exists(), true);
assert_eq!(sys::readlines(&repo1file).unwrap()[0].starts_with("alpine-"), true);
assert_eq!(sys::readlines(&repo2file).unwrap()[0].starts_with("alpine-"), true);
assert!(sys::remove_all(&tmpdir).is_ok());
assert!(tar::extract_all(&tarball, &tmpdir).is_ok());
assert_eq!(git::Repo::new(&tmpdir).unwrap().last_msg().unwrap(), "Use the workflow name for the badge".to_string());
let repos = git::RepoGroup::new().with_progress(true).add(git::Repo::new(&repo1).unwrap().url("https://github.com/phR0ze/alpine-base"));
assert!(repos.update().is_ok());
assert_ne!(git::Repo::new(&repo1).unwrap().last_msg().unwrap(), "Use the workflow name for the badge".to_string());
assert!(sys::remove_all(&tmpdir).is_ok());
}
}