use std::path::PathBuf;
use std::process::Command;
use crate::error::{GwError, Result};
fn git_output(args: &[&str]) -> Result<String> {
let output = Command::new("git")
.args(args)
.output()
.map_err(|e| GwError::GitCommandFailed(format!("Failed to execute git: {e}")))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(GwError::GitCommandFailed(stderr))
}
}
fn git_check(args: &[&str]) -> bool {
Command::new("git")
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn is_git_repo() -> bool {
git_check(&["rev-parse", "--git-dir"])
}
pub fn current_branch() -> Result<String> {
git_output(&["rev-parse", "--abbrev-ref", "HEAD"])
}
pub fn worktree_root() -> Result<PathBuf> {
git_output(&["rev-parse", "--show-toplevel"]).map(PathBuf::from)
}
pub fn git_dir() -> Result<PathBuf> {
git_output(&["rev-parse", "--git-dir"]).map(PathBuf::from)
}
pub fn git_common_dir() -> Result<PathBuf> {
git_output(&["rev-parse", "--git-common-dir"]).map(PathBuf::from)
}
pub fn is_worktree() -> Result<bool> {
let git_dir = git_dir()?;
let common_dir = git_common_dir()?;
Ok(git_dir != common_dir)
}
pub fn branch_exists(branch: &str) -> bool {
git_check(&[
"show-ref",
"--verify",
"--quiet",
&format!("refs/heads/{branch}"),
])
}
pub fn remote_branch_exists(branch: &str) -> bool {
git_check(&["ls-remote", "--exit-code", "--heads", "origin", branch])
}
pub fn head_commit() -> Result<String> {
git_output(&["rev-parse", "HEAD"])
}
pub fn short_commit() -> Result<String> {
git_output(&["rev-parse", "--short", "HEAD"])
}
pub fn head_commit_message() -> Result<String> {
git_output(&["log", "-1", "--format=%s"])
}
pub fn has_unstaged_changes() -> bool {
!git_check(&["diff", "--quiet"])
}
pub fn has_staged_changes() -> bool {
!git_check(&["diff", "--cached", "--quiet"])
}
pub fn has_uncommitted_changes() -> bool {
has_unstaged_changes() || has_staged_changes()
}
pub fn has_untracked_files() -> bool {
git_output(&["ls-files", "--others", "--exclude-standard"])
.map(|s| !s.is_empty())
.unwrap_or(false)
}
pub fn get_upstream(branch: &str) -> Option<String> {
git_output(&[
"rev-parse",
"--abbrev-ref",
&format!("{branch}@{{upstream}}"),
])
.ok()
.filter(|s| !s.is_empty())
}
pub fn has_remote_tracking(branch: &str) -> bool {
get_upstream(branch).is_some()
}
pub fn commit_count(from: &str, to: &str) -> Result<usize> {
let output = git_output(&["rev-list", "--count", &format!("{from}..{to}")])?;
output
.parse()
.map_err(|_| GwError::GitCommandFailed("Failed to parse commit count".to_string()))
}
pub fn unpushed_commit_count(branch: &str) -> Result<usize> {
let upstream = get_upstream(branch)
.ok_or_else(|| GwError::Other(format!("Branch '{branch}' has no upstream")))?;
commit_count(&upstream, branch)
}
pub fn behind_upstream_count(branch: &str) -> Result<usize> {
let upstream = get_upstream(branch)
.ok_or_else(|| GwError::Other(format!("Branch '{branch}' has no upstream")))?;
commit_count(branch, &upstream)
}
pub fn stash_count() -> usize {
git_output(&["stash", "list"])
.map(|s| if s.is_empty() { 0 } else { s.lines().count() })
.unwrap_or(0)
}
pub fn get_latest_stash_message() -> Option<String> {
git_output(&["stash", "list", "-1", "--format=%gs"])
.ok()
.filter(|s| !s.is_empty())
}
pub fn has_commits_to_undo() -> bool {
git_check(&["rev-parse", "HEAD~1"])
}
pub fn current_dir_name() -> Result<String> {
std::env::current_dir()
.map_err(GwError::Io)?
.file_name()
.and_then(|s| s.to_str())
.map(String::from)
.ok_or_else(|| GwError::Other("Could not determine current directory name".to_string()))
}
pub fn get_default_remote_branch() -> Result<String> {
if remote_branch_exists("main") {
Ok("origin/main".to_string())
} else if remote_branch_exists("master") {
Ok("origin/master".to_string())
} else {
Err(GwError::Other(
"Neither origin/main nor origin/master exists".to_string(),
))
}
}
pub fn is_detached_head() -> bool {
current_branch().map(|b| b == "HEAD").unwrap_or(false)
}
pub fn repo_root() -> Result<PathBuf> {
git_output(&["rev-parse", "--show-toplevel"]).map(PathBuf::from)
}