use std::path::{Path, PathBuf};
use async_trait::async_trait;
use crate::path::AbsolutePath;
#[async_trait]
pub trait Git: Send + Sync + std::fmt::Debug {
fn path(&self) -> &AbsolutePath;
async fn is_dirty(&self) -> anyhow::Result<bool>;
async fn current_branch(&self) -> anyhow::Result<Option<String>>;
async fn tag_exists(&self, tag: &str) -> anyhow::Result<bool>;
async fn remote_origin_url(&self) -> anyhow::Result<Option<String>>;
async fn rev_list_count(&self, range: &str) -> anyhow::Result<usize>;
async fn log_message(&self, rev: &str) -> anyhow::Result<String>;
async fn log_subject(&self, rev: &str) -> anyhow::Result<String>;
async fn log_added_commit(&self, path: &Path) -> anyhow::Result<Option<String>>;
async fn diff_tree_names(&self, commit: &str) -> anyhow::Result<Vec<String>>;
async fn diff_names(&self, extra_args: &[&str]) -> anyhow::Result<Vec<String>>;
async fn head_sha(&self) -> anyhow::Result<String>;
async fn add(&self, files: &[PathBuf]) -> anyhow::Result<()>;
async fn commit(&self, message: &str) -> anyhow::Result<()>;
async fn tag(&self, tag_name: &str, message: &str) -> anyhow::Result<()>;
async fn push(&self) -> anyhow::Result<()>;
async fn checkout(&self, branch: &str) -> anyhow::Result<()>;
async fn checkout_or_reset_branch(&self, branch: &str) -> anyhow::Result<()>;
async fn force_push_branch(&self, branch: &str) -> anyhow::Result<()>;
async fn delete_tag(&self, tag: &str) -> anyhow::Result<()>;
async fn push_tag(&self, tag: &str) -> anyhow::Result<()>;
}
mod operations;
pub(crate) mod ref_format;
mod signed_commit;
pub use operations::GitWorkdir;
pub use signed_commit::SignedCommitGit;
pub async fn find_workdir(
start: &AbsolutePath,
fs: &dyn crate::filesystem::Filesystem,
) -> Option<AbsolutePath> {
let mut dir = start.to_path_buf();
loop {
if let Ok(git_path) = AbsolutePath::new(dir.join(".git"))
&& fs.exists(&git_path).await.unwrap_or(false)
{
return AbsolutePath::new(&dir).ok();
}
if !dir.pop() {
return None;
}
}
}
#[cfg(test)]
mod find_workdir_tests {
use super::*;
use crate::filesystem::LocalFilesystem;
fn fs() -> LocalFilesystem {
LocalFilesystem
}
#[tokio::test]
async fn returns_none_when_no_git() {
let dir = tempfile::tempdir().unwrap();
assert!(
find_workdir(&AbsolutePath::new(dir.path()).unwrap(), &fs())
.await
.is_none()
);
}
#[tokio::test]
async fn finds_git_in_current_dir() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
let result = find_workdir(&AbsolutePath::new(dir.path()).unwrap(), &fs()).await;
assert_eq!(result, Some(AbsolutePath::new(dir.path()).unwrap()));
}
#[tokio::test]
async fn finds_git_in_parent_dir() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
let subdir = dir.path().join("subdir");
std::fs::create_dir(&subdir).unwrap();
let result = find_workdir(&AbsolutePath::new(&subdir).unwrap(), &fs()).await;
assert_eq!(result, Some(AbsolutePath::new(dir.path()).unwrap()));
}
#[tokio::test]
async fn finds_git_in_nested_parent() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
let nested = dir.path().join("a/b/c");
std::fs::create_dir_all(&nested).unwrap();
let result = find_workdir(&AbsolutePath::new(&nested).unwrap(), &fs()).await;
assert_eq!(result, Some(AbsolutePath::new(dir.path()).unwrap()));
}
#[tokio::test]
async fn stops_at_first_git() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join(".git")).unwrap();
let inner = dir.path().join("inner");
std::fs::create_dir_all(inner.join(".git")).unwrap();
let result = find_workdir(&AbsolutePath::new(&inner).unwrap(), &fs()).await;
assert_eq!(result, Some(AbsolutePath::new(inner).unwrap()));
}
}