#[cfg(test)]
mod tests;
use std::io::ErrorKind;
use std::path::Path;
use super::{Git, GitError, GitOutput};
const NOT_A_REPOSITORY: &str = "fatal: not a git repository";
impl Git {
pub async fn git_available(&self) -> Result<bool, GitError> {
match self.run(&["--version"]).await {
Ok(out) => interpret_git_available(out),
Err(e) => {
let blamed_on_workdir = missing_binary_kind(&e)
&& self.workdir.is_some()
&& Git::new().run(&["--version"]).await.is_ok();
interpret_spawn_failure(e, blamed_on_workdir)
}
}
}
pub async fn in_work_tree(&self) -> Result<bool, GitError> {
interpret_in_work_tree(self.run(&["rev-parse", "--is-inside-work-tree"]).await?)
}
pub async fn head_exists(&self) -> Result<bool, GitError> {
interpret_head_exists(self.run(&["rev-parse", "--verify", "-q", "HEAD"]).await?)
}
pub async fn head_detached(&self) -> Result<bool, GitError> {
interpret_head_detached(self.run(&["symbolic-ref", "-q", "HEAD"]).await?)
}
pub async fn index_has_staged(&self) -> Result<bool, GitError> {
interpret_index_has_staged(
self.run(&["diff", "--cached", "--quiet", "--ignore-submodules=none"])
.await?,
)
}
pub async fn index_conflicted(&self) -> Result<bool, GitError> {
interpret_index_conflicted(self.run(&["ls-files", "--unmerged"]).await?)
}
pub async fn work_tree_clean(&self) -> Result<bool, GitError> {
interpret_work_tree_clean(
self.run(&[
"status",
"--porcelain",
"--untracked-files=normal",
"--ignore-submodules=none",
])
.await?,
)
}
pub async fn merge_in_progress(&self) -> Result<bool, GitError> {
self.marker("MERGE_HEAD").await
}
pub async fn cherry_pick_in_progress(&self) -> Result<bool, GitError> {
self.marker("CHERRY_PICK_HEAD").await
}
pub async fn revert_in_progress(&self) -> Result<bool, GitError> {
self.marker("REVERT_HEAD").await
}
pub async fn bisect_in_progress(&self) -> Result<bool, GitError> {
self.marker("BISECT_LOG").await
}
pub async fn am_in_progress(&self) -> Result<bool, GitError> {
self.marker("rebase-apply/applying").await
}
pub async fn rebase_in_progress(&self) -> Result<bool, GitError> {
if self.marker("rebase-merge").await? {
return Ok(true);
}
self.marker("rebase-apply/rebasing").await
}
async fn marker(&self, name: &str) -> Result<bool, GitError> {
let path = interpret_git_path(self.run(&["rev-parse", "--git-path", name]).await?)?;
let path = Path::new(&path);
let resolved = match &self.workdir {
Some(dir) if path.is_relative() => dir.join(path),
_ => path.to_path_buf(),
};
Ok(tokio::fs::try_exists(resolved).await.unwrap_or(false))
}
}
fn interpret_git_available(out: GitOutput) -> Result<bool, GitError> {
if out.status == 0 && out.stdout.trim_end().starts_with("git version") {
return Ok(true);
}
Err(out.into_error())
}
fn missing_binary_kind(error: &GitError) -> bool {
matches!(
error,
GitError::SpawnFailed { source, .. }
if matches!(
source.kind(),
ErrorKind::NotFound | ErrorKind::PermissionDenied
)
)
}
fn interpret_spawn_failure(error: GitError, blamed_on_workdir: bool) -> Result<bool, GitError> {
if missing_binary_kind(&error) && !blamed_on_workdir {
Ok(false)
} else {
Err(error)
}
}
fn interpret_in_work_tree(out: GitOutput) -> Result<bool, GitError> {
match (out.status, out.stdout.trim_end()) {
(0, "true") => return Ok(true),
(0, "false") => return Ok(false),
(128, _) if out.stderr.lines().any(|l| l.starts_with(NOT_A_REPOSITORY)) => {
return Ok(false);
}
_ => {}
}
Err(out.into_error())
}
fn interpret_head_exists(out: GitOutput) -> Result<bool, GitError> {
match out.status {
0 => Ok(true),
1 => Ok(false),
_ => Err(out.into_error()),
}
}
fn interpret_head_detached(out: GitOutput) -> Result<bool, GitError> {
match out.status {
1 => Ok(true),
0 => Ok(false),
_ => Err(out.into_error()),
}
}
fn interpret_index_has_staged(out: GitOutput) -> Result<bool, GitError> {
match out.status {
1 => Ok(true),
0 => Ok(false),
_ => Err(out.into_error()),
}
}
fn interpret_index_conflicted(out: GitOutput) -> Result<bool, GitError> {
match out.status {
0 => Ok(!out.stdout.trim_end().is_empty()),
_ => Err(out.into_error()),
}
}
fn interpret_work_tree_clean(out: GitOutput) -> Result<bool, GitError> {
match out.status {
0 => Ok(out.stdout.trim_end().is_empty()),
_ => Err(out.into_error()),
}
}
fn interpret_git_path(out: GitOutput) -> Result<String, GitError> {
let len = out.stdout.trim_end().len();
if out.status == 0 && len > 0 && !out.stdout.contains('\u{FFFD}') {
let mut path = out.stdout;
path.truncate(len);
Ok(path)
} else {
Err(out.into_error())
}
}