#![deny(missing_docs)]
use std::ffi::OsStr;
use std::path::Path;
use futures::TryFutureExt;
use tokio::process::Command;
use crate::error::*;
#[derive(Copy, Clone)]
pub struct Git<'ctx> {
pub path: &'ctx Path,
pub git: &'ctx String,
}
impl<'git, 'ctx> Git<'ctx> {
pub fn new(path: &'ctx Path, git: &'ctx String) -> Git<'ctx> {
Git { path, git }
}
pub fn command(self, subcommand: &str) -> Command {
let mut cmd = Command::new(self.git);
cmd.arg(subcommand);
cmd.current_dir(self.path);
cmd
}
#[allow(clippy::format_push_string)]
pub async fn spawn(self, mut cmd: Command, check: bool) -> Result<String> {
let output = cmd.output().map_err(|cause| {
if cause
.to_string()
.to_lowercase()
.contains("too many open files")
{
println!(
"Please consider increasing your `ulimit -n`, e.g. by running `ulimit -n 4096`"
);
println!("This is a known issue (#52).");
Error::chain("Failed to spawn child process.", cause)
} else {
Error::chain("Failed to spawn child process.", cause)
}
});
let result = output.and_then(|output| async move {
debugln!("git: {:?} in {:?}", cmd, self.path);
if output.status.success() || !check {
String::from_utf8(output.stdout).map_err(|cause| {
Error::chain(
format!(
"Output of git command ({:?}) in directory {:?} is not valid UTF-8.",
cmd, self.path
),
cause,
)
})
} else {
let mut msg = format!("Git command ({:?}) in directory {:?}", cmd, self.path);
match output.status.code() {
Some(code) => msg.push_str(&format!(" failed with exit code {}", code)),
None => msg.push_str(" failed"),
};
match String::from_utf8(output.stderr) {
Ok(txt) => {
msg.push_str(":\n\n");
msg.push_str(&txt);
}
Err(err) => msg.push_str(&format!(". Stderr is not valid UTF-8, {}.", err)),
};
Err(Error::new(msg))
}
});
result.await
}
pub async fn spawn_with<F>(self, f: F) -> Result<String>
where
F: FnOnce(&mut Command) -> &mut Command,
{
let mut cmd = Command::new(self.git);
cmd.current_dir(self.path);
f(&mut cmd);
self.spawn(cmd, true).await
}
pub async fn spawn_unchecked_with<F>(self, f: F) -> Result<String>
where
F: FnOnce(&mut Command) -> &mut Command,
{
let mut cmd = Command::new(self.git);
cmd.current_dir(self.path);
f(&mut cmd);
self.spawn(cmd, false).await
}
pub async fn spawn_interactive_with<F>(self, f: F) -> Result<()>
where
F: FnOnce(&mut Command) -> &mut Command,
{
let mut cmd = Command::new(self.git);
cmd.current_dir(self.path);
f(&mut cmd);
cmd.spawn()?.wait().await?;
Ok(())
}
pub async fn fetch(self, remote: &str) -> Result<()> {
let r1 = String::from(remote);
let r2 = String::from(remote);
self.spawn_with(|c| c.arg("fetch").arg("--prune").arg(r1))
.and_then(|_| self.spawn_with(|c| c.arg("fetch").arg("--tags").arg("--prune").arg(r2)))
.await
.map(|_| ())
}
pub async fn add_all(self) -> Result<()> {
self.spawn_with(|c| c.arg("add").arg("--all"))
.await
.map(|_| ())
}
pub async fn commit(self, message: Option<&String>) -> Result<()> {
match message {
Some(msg) => self
.spawn_with(|c| {
c.arg("-c")
.arg("commit.gpgsign=false")
.arg("commit")
.arg("-m")
.arg(msg)
})
.await
.map(|_| ()),
None => self
.spawn_interactive_with(|c| c.arg("-c").arg("commit.gpgsign=false").arg("commit"))
.await
.map(|_| ()),
}
}
pub async fn list_refs(self) -> Result<Vec<(String, String)>> {
self.spawn_unchecked_with(|c| c.arg("show-ref").arg("--dereference"))
.and_then(|raw| async move {
let mut all_revs = raw
.lines()
.map(|line| {
let mut fields = line.split_whitespace().map(String::from);
let rev = fields.next().unwrap();
let rf = fields.next().unwrap();
(rev, rf)
})
.collect::<Vec<_>>();
let deref_revs = all_revs
.clone()
.into_iter()
.filter(|tup| tup.1.ends_with("^{}"));
for item in deref_revs {
let index = all_revs
.iter()
.position(|x| *x.1 == item.1.replace("^{}", ""))
.unwrap();
all_revs.remove(index);
let index = all_revs.iter().position(|x| *x.1 == item.1).unwrap();
all_revs.remove(index);
all_revs.push((item.0, item.1.replace("^{}", "")));
}
Ok(all_revs)
})
.await
}
pub async fn list_revs(self) -> Result<Vec<String>> {
self.spawn_with(|c| c.arg("rev-list").arg("--all").arg("--date-order"))
.await
.map(|raw| raw.lines().map(String::from).collect())
}
pub async fn current_checkout(self) -> Result<Option<String>> {
self.spawn_with(|c| c.arg("rev-parse").arg("--revs-only").arg("HEAD^{commit}"))
.await
.map(|raw| raw.lines().take(1).map(String::from).next())
}
pub async fn list_files<R: AsRef<OsStr>, P: AsRef<OsStr>>(
self,
rev: R,
path: Option<P>,
) -> Result<Vec<TreeEntry>> {
self.spawn_with(|c| {
c.arg("ls-tree").arg(rev);
if let Some(p) = path {
c.arg(p);
}
c
})
.await
.map(|raw| raw.lines().map(TreeEntry::parse).collect())
}
pub async fn cat_file<O: AsRef<OsStr>>(self, hash: O) -> Result<String> {
self.spawn_with(|c| c.arg("cat-file").arg("blob").arg(hash))
.await
}
}
pub struct TreeEntry {
pub name: String,
pub hash: String,
pub kind: String,
pub mode: String,
}
impl TreeEntry {
pub fn parse(input: &str) -> TreeEntry {
let tab = input.find('\t').unwrap();
let (metadata, name) = input.split_at(tab);
let mut iter = metadata.split(' ');
let mode = iter.next().unwrap();
let kind = iter.next().unwrap();
let hash = iter.next().unwrap();
TreeEntry {
name: name.into(),
hash: hash.into(),
kind: kind.into(),
mode: mode.into(),
}
}
}