use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, bail};
use async_trait::async_trait;
use crate::command::CommandRunner;
use crate::git::Git;
use crate::path::AbsolutePath;
#[derive(Debug)]
pub struct GitWorkdir {
path: AbsolutePath,
runner: Arc<dyn CommandRunner>,
}
impl GitWorkdir {
pub fn new(runner: Arc<dyn CommandRunner>, path: AbsolutePath) -> Self {
Self { path, runner }
}
}
#[async_trait]
impl Git for GitWorkdir {
fn path(&self) -> &AbsolutePath {
&self.path
}
async fn add(&self, files: &[PathBuf]) -> anyhow::Result<()> {
if files.is_empty() {
return Ok(());
}
let file_str_storage: Vec<String> = files
.iter()
.map(|f| f.to_string_lossy().into_owned())
.collect();
let mut args = vec!["add", "--"];
let file_str_refs: Vec<&str> = file_str_storage.iter().map(|s| s.as_str()).collect();
args.extend_from_slice(&file_str_refs);
let output = self
.runner
.run_mut("git", &args, &self.path)
.await
.context("Failed to run git add")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git add failed: {stderr}");
}
Ok(())
}
async fn commit(&self, message: &str) -> anyhow::Result<()> {
let output = self
.runner
.run_mut("git", &["commit", "-m", message], &self.path)
.await
.context("Failed to run git commit")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git commit failed: {stderr}");
}
Ok(())
}
async fn tag(&self, tag_name: &str, message: &str) -> anyhow::Result<()> {
let output = self
.runner
.run_mut("git", &["tag", "-a", tag_name, "-m", message], &self.path)
.await
.context("Failed to run git tag")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git tag failed: {stderr}");
}
Ok(())
}
async fn push(&self) -> anyhow::Result<()> {
let output = self
.runner
.run_mut("git", &["push", "origin", "HEAD"], &self.path)
.await
.context("Failed to run git push")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git push failed: {stderr}");
}
Ok(())
}
async fn is_dirty(&self) -> anyhow::Result<bool> {
let output = self
.runner
.run("git", &["status", "--porcelain"], &self.path)
.await
.context("Failed to run git status")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git status failed: {stderr}");
}
Ok(!String::from_utf8_lossy(&output.stdout).trim().is_empty())
}
async fn current_branch(&self) -> anyhow::Result<Option<String>> {
let output = self
.runner
.run("git", &["rev-parse", "--abbrev-ref", "HEAD"], &self.path)
.await
.context("Failed to run git rev-parse")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git rev-parse failed: {stderr}");
}
let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
if branch.is_empty() || branch == "HEAD" {
Ok(None)
} else {
Ok(Some(branch))
}
}
async fn checkout(&self, branch: &str) -> anyhow::Result<()> {
let output = self
.runner
.run_mut("git", &["checkout", branch], &self.path)
.await
.context("Failed to run git checkout")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git checkout failed: {stderr}");
}
Ok(())
}
async fn tag_exists(&self, tag: &str) -> anyhow::Result<bool> {
let ref_path = format!("refs/tags/{tag}");
let output = self
.runner
.run("git", &["rev-parse", "--verify", &ref_path], &self.path)
.await
.context("Failed to run git rev-parse")?;
Ok(output.status.success())
}
async fn remote_origin_url(&self) -> anyhow::Result<Option<String>> {
let output = self
.runner
.run("git", &["remote", "get-url", "origin"], &self.path)
.await
.context("Failed to query git remote URL")?;
if !output.status.success() {
return Ok(None);
}
Ok(Some(
String::from_utf8_lossy(&output.stdout).trim().to_string(),
))
}
async fn checkout_or_reset_branch(&self, branch: &str) -> anyhow::Result<()> {
let output = self
.runner
.run_mut("git", &["checkout", "-B", branch], &self.path)
.await
.context("Failed to run git checkout")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git checkout -B failed: {stderr}");
}
Ok(())
}
async fn force_push_branch(&self, branch: &str) -> anyhow::Result<()> {
let output = self
.runner
.run_mut(
"git",
&["push", "--force-with-lease", "origin", branch],
&self.path,
)
.await
.context("Failed to run git force push branch")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git force push branch failed: {stderr}");
}
Ok(())
}
async fn delete_tag(&self, tag: &str) -> anyhow::Result<()> {
let output = self
.runner
.run_mut("git", &["tag", "-d", tag], &self.path)
.await
.context("Failed to run git tag -d")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git tag -d failed: {stderr}");
}
Ok(())
}
async fn push_tag(&self, tag: &str) -> anyhow::Result<()> {
let output = self
.runner
.run_mut("git", &["push", "origin", "tag", tag], &self.path)
.await
.context("Failed to run git push tag")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git push tag failed: {stderr}");
}
Ok(())
}
async fn rev_list_count(&self, range: &str) -> anyhow::Result<usize> {
let output = self
.runner
.run("git", &["rev-list", "--count", range], &self.path)
.await
.context("Failed to run git rev-list --count")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git rev-list --count failed: {stderr}");
}
let count_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
count_str
.parse::<usize>()
.with_context(|| format!("Failed to parse git rev-list count: '{count_str}'"))
}
async fn log_message(&self, rev: &str) -> anyhow::Result<String> {
let output = self
.runner
.run("git", &["log", "-1", "--format=%B", rev], &self.path)
.await
.context("Failed to run git log")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git log failed: {stderr}");
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
async fn diff_tree_names(&self, commit: &str) -> anyhow::Result<Vec<String>> {
let output = self
.runner
.run(
"git",
&["diff-tree", "--no-commit-id", "-r", "--name-only", commit],
&self.path,
)
.await
.context("Failed to run git diff-tree")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git diff-tree failed: {stderr}");
}
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|l| !l.is_empty())
.map(|l| l.to_string())
.collect())
}
async fn log_added_commit(&self, path: &std::path::Path) -> anyhow::Result<Option<String>> {
let path_str = path.to_string_lossy();
let output = self
.runner
.run(
"git",
&[
"log",
"--first-parent",
"--diff-filter=A",
"--format=%H",
"--",
path_str.as_ref(),
],
&self.path,
)
.await
.context("Failed to run git log --diff-filter=A")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git log --diff-filter=A failed: {stderr}");
}
let sha = String::from_utf8_lossy(&output.stdout)
.lines()
.next()
.map(|l| l.trim().to_string())
.filter(|s| !s.is_empty());
Ok(sha)
}
async fn log_subject(&self, rev: &str) -> anyhow::Result<String> {
let output = self
.runner
.run("git", &["log", "-1", "--format=%s", rev], &self.path)
.await
.context("Failed to run git log --format=%s")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git log --format=%s failed: {stderr}");
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
async fn diff_names(&self, extra_args: &[&str]) -> anyhow::Result<Vec<String>> {
let mut args = vec!["diff", "--name-only"];
args.extend_from_slice(extra_args);
let output = self
.runner
.run("git", &args, &self.path)
.await
.context("Failed to run git diff --name-only")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git diff --name-only failed: {stderr}");
}
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|l| !l.is_empty())
.map(|l| l.to_string())
.collect())
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod integration_tests;