use crate::types::GitInfo;
use std::process::Command;
fn get_repo_name(dir: &str) -> Option<String> {
let output = Command::new("git")
.args(["-C", dir, "remote", "get-url", "origin"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let url = String::from_utf8(output.stdout).ok()?;
let url = url.trim();
let repo = url.rsplit('/').next()?.trim_end_matches(".git").to_string();
if repo.is_empty() {
None
} else {
Some(repo)
}
}
pub fn get_git_info(dir: &str) -> Option<GitInfo> {
let output = match Command::new("git")
.args(["-C", dir, "status", "--porcelain", "-b"])
.output()
{
Ok(o) => o,
Err(e) => {
if std::env::var("STATUSLINE_DEBUG").is_ok() {
eprintln!("statusline warning: git not available: {}", e);
}
return None;
}
};
if !output.status.success() {
return None;
}
let stdout = String::from_utf8(output.stdout).ok()?;
let lines: Vec<&str> = stdout.lines().collect();
if lines.is_empty() {
return None;
}
let branch_line = lines[0];
let branch = if let Some(raw) = branch_line.strip_prefix("## ") {
if let Some(idx) = raw.find("...") {
raw[..idx].to_string()
} else if let Some(stripped) = raw.strip_prefix("No commits yet on ") {
stripped.to_string()
} else if raw.starts_with("HEAD (no branch)") {
"HEAD".to_string()
} else {
raw.to_string()
}
} else {
return None;
};
let is_dirty = lines.len() > 1;
let repo_name = get_repo_name(dir);
let (lines_added, lines_removed) = get_diff_stats(dir);
Some(GitInfo {
branch,
is_dirty,
repo_name,
lines_added,
lines_removed,
})
}
fn get_diff_stats(dir: &str) -> (usize, usize) {
let output = match Command::new("git")
.args(["-C", dir, "diff", "--numstat"])
.output()
{
Ok(o) => o,
Err(_) => return (0, 0),
};
if !output.status.success() {
return (0, 0);
}
let stdout = match String::from_utf8(output.stdout) {
Ok(s) => s,
Err(_) => return (0, 0),
};
let mut total_added = 0;
let mut total_removed = 0;
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(added) = parts[0].parse::<usize>() {
total_added += added;
}
if let Ok(removed) = parts[1].parse::<usize>() {
total_removed += removed;
}
}
}
(total_added, total_removed)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_test_repo() -> TempDir {
let dir = TempDir::new().unwrap();
let path = dir.path().to_str().unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(path)
.output()
.unwrap();
dir
}
#[test]
fn test_get_repo_name_https_url() {
let dir = create_test_repo();
let path = dir.path().to_str().unwrap();
std::process::Command::new("git")
.args([
"remote",
"add",
"origin",
"https://github.com/user/my-repo.git",
])
.current_dir(path)
.output()
.unwrap();
let result = get_repo_name(path);
assert_eq!(result, Some("my-repo".to_string()));
}
#[test]
fn test_get_repo_name_ssh_url() {
let dir = create_test_repo();
let path = dir.path().to_str().unwrap();
std::process::Command::new("git")
.args(["remote", "add", "origin", "git@github.com:user/my-repo.git"])
.current_dir(path)
.output()
.unwrap();
let result = get_repo_name(path);
assert_eq!(result, Some("my-repo".to_string()));
}
#[test]
fn test_get_repo_name_without_git_extension() {
let dir = create_test_repo();
let path = dir.path().to_str().unwrap();
std::process::Command::new("git")
.args(["remote", "add", "origin", "https://github.com/user/my-repo"])
.current_dir(path)
.output()
.unwrap();
let result = get_repo_name(path);
assert_eq!(result, Some("my-repo".to_string()));
}
#[test]
fn test_get_repo_name_no_remote() {
let dir = create_test_repo();
let path = dir.path().to_str().unwrap();
let result = get_repo_name(path);
assert_eq!(result, None);
}
#[test]
fn test_get_git_info_no_commits() {
let dir = create_test_repo();
let path = dir.path().to_str().unwrap();
let result = get_git_info(path);
if let Some(info) = result {
assert!(!info.branch.is_empty());
assert!(!info.is_dirty); }
}
#[test]
fn test_get_git_info_with_commit() {
let dir = create_test_repo();
let path = dir.path().to_str().unwrap();
fs::write(dir.path().join("test.txt"), "content").unwrap();
std::process::Command::new("git")
.args(["add", "test.txt"])
.current_dir(path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(path)
.output()
.unwrap();
let result = get_git_info(path).unwrap();
assert_eq!(result.branch, "master");
assert!(!result.is_dirty);
assert_eq!(result.lines_added, 0);
assert_eq!(result.lines_removed, 0);
}
#[test]
fn test_get_git_info_dirty() {
let dir = create_test_repo();
let path = dir.path().to_str().unwrap();
fs::write(dir.path().join("test.txt"), "content").unwrap();
std::process::Command::new("git")
.args(["add", "test.txt"])
.current_dir(path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(path)
.output()
.unwrap();
fs::write(dir.path().join("test.txt"), "modified content").unwrap();
let result = get_git_info(path).unwrap();
assert!(result.is_dirty);
}
#[test]
fn test_get_git_info_with_diff_stats() {
let dir = create_test_repo();
let path = dir.path().to_str().unwrap();
fs::write(dir.path().join("test.txt"), "line1\nline2\n").unwrap();
std::process::Command::new("git")
.args(["add", "test.txt"])
.current_dir(path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(path)
.output()
.unwrap();
fs::write(dir.path().join("test.txt"), "line1\nline3\nline4\n").unwrap();
let result = get_git_info(path).unwrap();
assert!(result.lines_added > 0 || result.lines_removed > 0);
}
#[test]
fn test_get_git_info_not_a_repo() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_str().unwrap();
let result = get_git_info(path);
assert_eq!(result, None);
}
#[test]
fn test_get_diff_stats_no_changes() {
let dir = create_test_repo();
let path = dir.path().to_str().unwrap();
fs::write(dir.path().join("test.txt"), "content").unwrap();
std::process::Command::new("git")
.args(["add", "test.txt"])
.current_dir(path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(path)
.output()
.unwrap();
let (added, removed) = get_diff_stats(path);
assert_eq!(added, 0);
assert_eq!(removed, 0);
}
}