use crate::infra::error::GitError;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn resolve_git_common_dir(repo_root: Option<&Path>) -> Result<PathBuf, GitError> {
let mut cmd = Command::new("git");
if let Some(root) = repo_root {
cmd.current_dir(root);
}
cmd.args(["rev-parse", "--git-common-dir"]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("not a git repository") {
return Err(GitError::NotAGitRepository);
}
return Err(GitError::CommandFailed {
command: "git rev-parse --git-common-dir".to_string(),
message: stderr.trim().to_string(),
});
}
let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
let path = PathBuf::from(path_str);
let resolved = if path.is_absolute() {
path
} else {
let base = repo_root
.map(|p| p.to_path_buf())
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
base.join(path)
};
Ok(resolved.canonicalize().unwrap_or(resolved))
}
pub fn resolve_toplevel(repo_root: Option<&Path>) -> Result<PathBuf, GitError> {
let mut cmd = Command::new("git");
if let Some(root) = repo_root {
cmd.current_dir(root);
}
cmd.args(["rev-parse", "--show-toplevel"]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("not a git repository") {
return Err(GitError::NotAGitRepository);
}
return Err(GitError::CommandFailed {
command: "git rev-parse --show-toplevel".to_string(),
message: stderr.trim().to_string(),
});
}
let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
let path = PathBuf::from(path_str);
Ok(path.canonicalize().unwrap_or(path))
}
pub fn resolve_repo_name(toplevel: &Path) -> String {
toplevel
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("repo")
.to_string()
}
pub fn git_worktree_prune(repo_root: Option<&Path>) -> Result<(), GitError> {
let mut cmd = Command::new("git");
if let Some(root) = repo_root {
cmd.current_dir(root);
}
cmd.args(["worktree", "prune"]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::CommandFailed {
command: "git worktree prune".to_string(),
message: stderr.trim().to_string(),
});
}
Ok(())
}
pub fn git_is_clean(repo_root: &Path) -> Result<bool, GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
cmd.args(["status", "--porcelain"]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::CommandFailed {
command: "git status --porcelain".to_string(),
message: stderr.trim().to_string(),
});
}
Ok(output.stdout.is_empty())
}
pub fn git_merge(repo_root: &Path, branch: &str) -> Result<(), GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
cmd.args(["merge", branch]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::CommandFailed {
command: format!("git merge {}", branch),
message: stderr.trim().to_string(),
});
}
Ok(())
}
pub fn git_checkout(repo_root: &Path, branch: &str) -> Result<(), GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
cmd.args(["checkout", branch]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::CommandFailed {
command: format!("git checkout {}", branch),
message: stderr.trim().to_string(),
});
}
Ok(())
}
pub fn git_head_commit(worktree_path: &Path) -> Result<String, GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(worktree_path);
cmd.args(["rev-parse", "--short", "HEAD"]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::CommandFailed {
command: "git rev-parse --short HEAD".to_string(),
message: stderr.trim().to_string(),
});
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub fn git_create_annotated_tag(
repo_root: &Path,
tag: &str,
target_ref: &str,
message: &str,
) -> Result<(), GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
cmd.args(["tag", "-a", tag, target_ref, "-m", message]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::CommandFailed {
command: format!("git tag -a {} {} -m {}", tag, target_ref, message),
message: stderr.trim().to_string(),
});
}
Ok(())
}
pub fn git_branch_exists(repo_root: &Path, branch: &str) -> Result<bool, GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
let ref_spec = if branch.starts_with("refs/heads/") {
branch.to_string()
} else {
format!("refs/heads/{}", branch)
};
cmd.args(["rev-parse", "--verify", &ref_spec]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
Ok(output.status.success())
}
pub fn git_get_remotes(repo_root: &Path) -> Result<Vec<String>, GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
cmd.arg("remote");
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::CommandFailed {
command: "git remote".to_string(),
message: stderr.trim().to_string(),
});
}
let remotes = String::from_utf8_lossy(&output.stdout)
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(remotes)
}
pub fn git_push_upstream(repo_root: &Path, remote: &str, branch: &str) -> Result<(), GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
cmd.args(["push", "-u", remote, branch]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::CommandFailed {
command: format!("git push -u {} {}", remote, branch),
message: stderr.trim().to_string(),
});
}
Ok(())
}
pub fn git_remote_branch_exists(
repo_root: &Path,
remote: &str,
branch: &str,
) -> Result<bool, GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
cmd.args(["ls-remote", "--heads", remote, branch]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::CommandFailed {
command: format!("git ls-remote --heads {} {}", remote, branch),
message: stderr.trim().to_string(),
});
}
let expected_ref = if branch.starts_with("refs/heads/") {
branch.to_string()
} else {
format!("refs/heads/{}", branch)
};
let stdout = String::from_utf8_lossy(&output.stdout);
let exists = stdout
.lines()
.any(|line| line.split_whitespace().nth(1) == Some(&expected_ref));
Ok(exists)
}
pub fn git_delete_remote_branch(
repo_root: &Path,
remote: &str,
branch: &str,
) -> Result<(), GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
let branch_clean = branch.strip_prefix("refs/heads/").unwrap_or(branch);
cmd.args(["push", remote, "--delete", branch_clean]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::CommandFailed {
command: format!("git push {} --delete {}", remote, branch_clean),
message: stderr.trim().to_string(),
});
}
Ok(())
}
pub fn git_commit_files(repo_root: &Path, files: &[&str], message: &str) -> Result<(), GitError> {
if files.is_empty() {
return Ok(());
}
let mut add_cmd = Command::new("git");
add_cmd.current_dir(repo_root);
add_cmd.arg("add");
add_cmd.args(files);
let add_output = add_cmd.output().map_err(GitError::ProcessFailed)?;
if !add_output.status.success() {
let stderr = String::from_utf8_lossy(&add_output.stderr);
return Err(GitError::CommandFailed {
command: format!("git add {}", files.join(" ")),
message: stderr.trim().to_string(),
});
}
let mut commit_cmd = Command::new("git");
commit_cmd.current_dir(repo_root);
commit_cmd.args(["commit", "-m", message]);
let commit_output = commit_cmd.output().map_err(GitError::ProcessFailed)?;
if !commit_output.status.success() {
let stderr = String::from_utf8_lossy(&commit_output.stderr);
return Err(GitError::CommandFailed {
command: format!("git commit -m {}", message),
message: stderr.trim().to_string(),
});
}
Ok(())
}
pub fn git_get_latest_tag(repo_root: &Path) -> Result<Option<String>, GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
cmd.args(["describe", "--tags", "--abbrev=0"]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if output.status.success() {
let tag = String::from_utf8_lossy(&output.stdout).trim().to_string();
if tag.is_empty() {
Ok(None)
} else {
Ok(Some(tag))
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("not a git repository") {
return Err(GitError::NotAGitRepository);
}
Ok(None)
}
}
pub fn git_get_commits_since(repo_root: &Path, tag: Option<&str>) -> Result<Vec<String>, GitError> {
let mut cmd = Command::new("git");
cmd.current_dir(repo_root);
let range = match tag {
Some(t) => format!("{}..HEAD", t),
None => "HEAD".to_string(),
};
cmd.args(["log", &range, "--format=%B%x1e"]);
let output = cmd.output().map_err(GitError::ProcessFailed)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("not a git repository") {
return Err(GitError::NotAGitRepository);
}
if stderr.contains("does not have any commits yet") {
return Ok(Vec::new());
}
return Err(GitError::CommandFailed {
command: format!("git log {}", range),
message: stderr.trim().to_string(),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
let commits = stdout
.split('\x1e')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(commits)
}