use crate::{Callback, CommandFailed, Error};
use std::cell::{Cell, RefCell};
use std::env;
use std::ffi::OsStr;
use std::fs::{create_dir_all, File};
use std::io::{self, Write};
#[cfg(feature = "view")]
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::rc::Rc;
use futures::future::try_join_all;
#[cfg(feature = "view")]
use tempfile::{Builder, TempDir};
use tokio::process::Command as AsyncCommand;
use url::Url;
static SEEN: &str = "AUR_SEEN";
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug)]
pub struct Handle {
pub clone_dir: PathBuf,
pub diff_dir: PathBuf,
pub git: PathBuf,
pub git_flags: Vec<String>,
pub aur_url: Url,
}
impl Handle {
pub fn new() -> Result<Self> {
Ok(Self {
clone_dir: env::current_dir()?,
diff_dir: env::current_dir()?,
git: "git".into(),
git_flags: Vec::new(),
aur_url: "https://aur.archlinux.org".parse().unwrap(),
})
}
pub fn with_cache_dir<P: AsRef<Path>>(path: P) -> Self {
let path = path.as_ref();
Self {
clone_dir: path.join("clone"),
diff_dir: path.join("diff"),
git: "git".into(),
git_flags: Vec::new(),
aur_url: "https://aur.archlinux.org".parse().unwrap(),
}
}
pub fn with_combined_cache_dir<P: AsRef<Path>>(path: P) -> Self {
let path = path.as_ref();
Self {
clone_dir: path.into(),
diff_dir: path.into(),
git: "git".into(),
git_flags: Vec::new(),
aur_url: "https://aur.archlinux.org".parse().unwrap(),
}
}
pub async fn download<'a, S: AsRef<str>>(&self, pkgs: &'a [S]) -> Result<Vec<&'a str>> {
self.download_cb(pkgs, |_| ()).await
}
pub async fn download_cb<'a, S: AsRef<str>, F: Fn(Callback)>(
&self,
pkgs: &'a [S],
f: F,
) -> Result<Vec<&'a str>> {
let pkgs = pkgs.iter().map(|p| p.as_ref()).collect::<Vec<_>>();
let pkgs = Rc::new(RefCell::new(pkgs));
let n = Rc::new(Cell::new(0));
let futures = (0u8..16).map(|_| self.pkg_sink(pkgs.clone(), &f, n.clone()));
let pkgs = try_join_all(futures).await?;
Ok(pkgs.into_iter().flatten().collect())
}
async fn pkg_sink<'a, F: Fn(Callback)>(
&self,
pkgs: Rc<RefCell<Vec<&'a str>>>,
f: &F,
n: Rc<Cell<usize>>,
) -> Result<Vec<&'a str>> {
let mut ret = Vec::new();
loop {
let mut borrow = pkgs.borrow_mut();
if let Some(pkg) = borrow.pop() {
drop(borrow);
if self.download_pkg(pkg, f, n.clone()).await? {
ret.push(pkg);
}
} else {
break;
}
}
Ok(ret)
}
async fn download_pkg<'a, S: AsRef<str>, F: Fn(Callback)>(
&self,
pkg: S,
f: &F,
n: Rc<Cell<usize>>,
) -> Result<bool> {
self.mk_clone_dir()?;
let mut fetched = false;
let mut url = self.aur_url.clone();
let pkg = pkg.as_ref();
url.set_path(pkg.as_ref());
let is_git_repo = self.is_git_repo(pkg);
let command = if is_git_repo {
fetched = true;
AsyncCommand::new(&self.git)
.current_dir(&self.clone_dir.join(pkg))
.args(&["fetch", "-v"])
.output()
} else {
AsyncCommand::new(&self.git)
.current_dir(&self.clone_dir)
.args(&["clone", "--no-progress", "--", url.as_str()])
.output()
};
let pkg = pkg.to_string();
let output = command.await?;
if !output.status.success() {
if is_git_repo {
Err(Error::CommandFailed(CommandFailed {
dir: self.clone_dir.join(pkg),
command: self.git.clone(),
args: vec!["fetch".into(), "-v".into()],
stderr: Some(String::from_utf8_lossy(&output.stderr).into()),
}))
} else {
Err(Error::CommandFailed(CommandFailed {
dir: self.clone_dir.clone(),
command: self.git.clone(),
args: vec![
"clone".into(),
"--noprogress".into(),
"--".into(),
url.to_string(),
],
stderr: Some(String::from_utf8_lossy(&output.stderr).into()),
}))
}
} else {
n.set(n.get() + 1);
f(Callback {
pkg: &pkg,
n: n.get(),
output: String::from_utf8_lossy(&output.stderr).trim(),
});
Ok(fetched)
}
}
pub fn has_diff<'a, S: AsRef<str>>(&self, pkgs: &'a [S]) -> Result<Vec<&'a str>> {
let mut ret = Vec::new();
for pkg in pkgs {
if git_has_diff(
&self.git,
&self.git_flags,
self.clone_dir.join(pkg.as_ref()),
)? {
ret.push(pkg.as_ref());
}
}
Ok(ret)
}
pub fn unseen<'a, S: AsRef<str>>(&self, pkgs: &'a [S]) -> Result<Vec<&'a str>> {
let mut ret = Vec::new();
for pkg in pkgs {
if git_unseen(
&self.git,
&self.git_flags,
self.clone_dir.join(pkg.as_ref()),
)? {
ret.push(pkg.as_ref());
}
}
Ok(ret)
}
pub fn diff<S: AsRef<str>>(&self, pkgs: &[S], color: bool) -> Result<Vec<String>> {
let pkgs = pkgs.iter();
let mut ret = Vec::new();
for pkg in pkgs {
let output = git_log(
&self.git,
&self.git_flags,
self.clone_dir.join(pkg.as_ref()),
color,
)?;
let mut s: String = String::from_utf8_lossy(&output.stdout).into();
let output = git_diff(
&self.git,
&self.git_flags,
self.clone_dir.join(pkg.as_ref()),
color,
)?;
s.push_str(&String::from_utf8_lossy(&output.stdout));
s.push('\n');
ret.push(s);
}
Ok(ret)
}
pub fn print_diff<S: AsRef<str>>(&self, pkg: S) -> Result<()> {
show_git_diff(
&self.git,
&self.git_flags,
self.clone_dir.join(pkg.as_ref()),
)
}
pub fn save_diffs<S: AsRef<str>>(&self, pkgs: &[S]) -> Result<()> {
self.mk_diff_dir()?;
for pkg in pkgs {
let mut path = self.diff_dir.join(pkg.as_ref());
path.set_extension("diff");
let mut file = File::create(path)?;
file.write_all(
&git_log(
&self.git,
&self.git_flags,
self.clone_dir.join(pkg.as_ref()),
false,
)?
.stdout,
)?;
file.write_all(&[b'\n'])?;
file.write_all(
&git_diff(
&self.git,
&self.git_flags,
self.clone_dir.join(pkg.as_ref()),
false,
)?
.stdout,
)?;
}
Ok(())
}
#[cfg(feature = "view")]
pub fn make_view<S1: AsRef<str>, S2: AsRef<str>>(
&self,
pkgs: &[S1],
diffs: &[S2],
) -> Result<TempDir> {
let tmp = Builder::new().prefix("aur").tempdir()?;
for pkg in diffs {
let pkg = format!("{}.diff", pkg.as_ref());
let dest = tmp.path().join(&pkg);
let src = self.diff_dir.join(&pkg);
if src.is_file() {
symlink(src, &dest)?;
}
}
for pkg in pkgs {
let dest = tmp.path().join(pkg.as_ref());
let pkgbuild_dest = tmp.path().join(format!("{}.PKGBUILD", pkg.as_ref()));
let srcinfo_dest = tmp.path().join(format!("{}.SRCINFO", pkg.as_ref()));
let src = self.clone_dir.join(pkg.as_ref());
if src.is_dir() {
symlink(src, &dest)?;
}
let src = self.clone_dir.join(pkg.as_ref()).join("PKGBUILD");
if src.is_file() {
symlink(src, &pkgbuild_dest)?;
}
let src = self.clone_dir.join(pkg.as_ref()).join(".SRCINFO");
if src.is_file() {
symlink(src, &srcinfo_dest)?;
}
}
Ok(tmp)
}
pub fn merge<S: AsRef<str>>(&self, pkgs: &[S]) -> Result<()> {
self.merge_cb(pkgs, |_| ())
}
pub fn merge_cb<S: AsRef<str>, F: Fn(Callback)>(&self, pkgs: &[S], cb: F) -> Result<()> {
let pkgs = pkgs.iter();
for (n, pkg) in pkgs.enumerate() {
let path = self.clone_dir.join(pkg.as_ref());
let output = git_rebase(&self.git, &self.git_flags, path)?;
cb(Callback {
pkg: pkg.as_ref(),
n,
output: String::from_utf8_lossy(&output.stdout).trim(),
});
}
Ok(())
}
pub fn mark_seen<S: AsRef<str>>(&self, pkgs: &[S]) -> Result<()> {
for pkg in pkgs {
let path = self.clone_dir.join(pkg.as_ref());
git_mark_seen(&self.git, &self.git_flags, path)?;
}
Ok(())
}
fn is_git_repo<S: AsRef<str>>(&self, pkg: S) -> bool {
self.clone_dir.join(pkg.as_ref()).join(".git").is_dir()
}
fn mk_clone_dir(&self) -> io::Result<()> {
create_dir_all(&self.clone_dir)
}
fn mk_diff_dir(&self) -> io::Result<()> {
create_dir_all(&self.diff_dir)
}
}
fn color_str(color: bool) -> &'static str {
if color {
"--color=always"
} else {
"--color=never"
}
}
fn git_command<S: AsRef<OsStr>, P: AsRef<Path>>(
git: S,
path: P,
flags: &[String],
args: &[&str],
) -> Result<Output> {
let output = Command::new(git.as_ref())
.current_dir(path.as_ref())
.args(flags)
.args(args)
.env("GIT_TERMINAL_PROMPT", "0")
.output()?;
if output.status.success() {
Ok(output)
} else {
Err(Error::CommandFailed(CommandFailed {
dir: path.as_ref().into(),
command: git.as_ref().into(),
args: args.iter().map(|s| s.to_string()).collect(),
stderr: Some(String::from_utf8_lossy(&output.stderr).into()),
}))
}
}
fn show_git_command<S: AsRef<OsStr>, P: AsRef<Path>>(
git: S,
path: P,
flags: &[String],
args: &[&str],
) -> Result<()> {
let status = Command::new(git.as_ref())
.current_dir(path.as_ref())
.args(flags)
.args(args)
.env("GIT_TERMINAL_PROMPT", "0")
.spawn()?
.wait()?;
if status.success() {
Ok(())
} else {
Err(Error::CommandFailed(CommandFailed {
dir: path.as_ref().into(),
command: git.as_ref().into(),
args: args.iter().map(|s| s.to_string()).collect(),
stderr: None,
}))
}
}
fn git_mark_seen<S: AsRef<OsStr>, P: AsRef<Path>>(
git: S,
flags: &[String],
path: P,
) -> Result<Output> {
Ok(git_command(
&git,
&path,
flags,
&["update-ref", SEEN, "HEAD"],
)?)
}
fn git_rebase<S: AsRef<OsStr>, P: AsRef<Path>>(
git: S,
flags: &[String],
path: P,
) -> Result<Output> {
git_command(&git, &path, flags, &["reset", "--hard", "-q", "HEAD"])?;
Ok(git_command(&git, &path, flags, &["rebase", "--stat"])?)
}
fn git_unseen<S: AsRef<OsStr>, P: AsRef<Path>>(git: S, flags: &[String], path: P) -> Result<bool> {
if git_has_seen(&git, flags, &path)? {
let is_unseen = git_command(
git,
path,
flags,
&["merge-base", "--is-ancestor", "HEAD@{u}", "AUR_SEEN"],
)
.is_err();
Ok(is_unseen)
} else {
Ok(true)
}
}
fn git_has_diff<S: AsRef<OsStr>, P: AsRef<Path>>(
git: S,
flags: &[String],
path: P,
) -> Result<bool> {
if git_has_seen(&git, flags, &path)? {
let output = git_command(git, path, flags, &["rev-parse", SEEN, "HEAD@{u}"])?;
let s = String::from_utf8_lossy(&output.stdout);
let mut s = s.split('\n');
let head = s.next().unwrap();
let upstream = s.next().unwrap();
Ok(head != upstream)
} else {
Ok(false)
}
}
fn git_log<S: AsRef<OsStr>, P: AsRef<Path>>(
git: S,
flags: &[String],
path: P,
color: bool,
) -> Result<Output> {
let color = color_str(color);
Ok(git_command(
git,
path,
flags,
&["log", "..HEAD@{u}", color],
)?)
}
fn git_has_seen<S: AsRef<OsStr>, P: AsRef<Path>>(
git: S,
flags: &[String],
path: P,
) -> Result<bool> {
let output = git_command(&git, &path, flags, &["rev-parse", "--verify", SEEN]).is_ok();
Ok(output)
}
fn git_head<S: AsRef<OsStr>, P: AsRef<Path>>(git: S, flags: &[String], path: P) -> Result<String> {
let output = git_command(git, path, flags, &["rev-parse", "HEAD"])?;
let output = String::from_utf8_lossy(&output.stdout);
Ok(output.trim().to_string())
}
fn git_diff<S: AsRef<OsStr>, P: AsRef<Path>>(
git: S,
flags: &[String],
path: P,
color: bool,
) -> Result<Output> {
let color = color_str(color);
let head = git_head(&git, &flags, &path)?;
git_command(&git, &path, flags, &["reset", "--hard", SEEN])?;
let output = if git_has_seen(&git, flags, &path)? {
git_command(
&git,
&path,
flags,
&[
"-c",
"user.email=aur",
"-c",
"user.name=aur",
"merge",
"--no-edit",
"--no-ff",
"--no-commit",
],
)?;
Ok(git_command(
&git,
&path,
flags,
&["diff", "--stat", "--patch", "--cached", color],
)?)
} else {
Ok(git_command(
&git,
&path,
flags,
&[
"diff",
"--stat",
"--patch",
"4b825dc642cb6eb9a060e54bf8d69288fbee4904..HEAD@{u}",
color,
],
)?)
};
git_command(&git, &path, flags, &["reset", "--hard", &head])?;
output
}
fn show_git_diff<S: AsRef<OsStr>, P: AsRef<Path>>(git: S, flags: &[String], path: P) -> Result<()> {
let head = git_head(&git, &flags, &path)?;
if git_has_seen(&git, flags, &path)? {
git_command(&git, &path, flags, &["reset", "--hard", SEEN])?;
git_command(
&git,
&path,
flags,
&[
"-c",
"user.email=aur",
"-c",
"user.name=aur",
"merge",
"--no-edit",
"--no-ff",
"--no-commit",
],
)?;
show_git_command(
&git,
&path,
flags,
&["diff", "--stat", "--patch", "--cached"],
)?;
} else {
show_git_command(
&git,
&path,
flags,
&[
"diff",
"--stat",
"--patch",
"4b825dc642cb6eb9a060e54bf8d69288fbee4904..HEAD@{u}",
],
)?;
}
git_command(&git, &path, flags, &["reset", "--hard", &head])?;
Ok(())
}