use anyhow::{Context, Result};
use chrono::{TimeZone, Utc};
use std::process::Command;
use crate::branch::Branch;
use crate::error::DeadbranchError;
pub fn is_git_repository() -> bool {
Command::new("git")
.args(["rev-parse", "--git-dir"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
pub fn get_default_branch() -> Result<String> {
let output = Command::new("git")
.args(["symbolic-ref", "refs/remotes/origin/HEAD", "--short"])
.output()
.context("Failed to run git command")?;
if output.status.success() {
let branch = String::from_utf8_lossy(&output.stdout)
.trim()
.strip_prefix("origin/")
.unwrap_or("main")
.to_string();
return Ok(branch);
}
for branch in &["main", "master"] {
let output = Command::new("git")
.args(["rev-parse", "--verify", &format!("refs/heads/{}", branch)])
.output()
.context("Failed to run git command")?;
if output.status.success() {
return Ok(branch.to_string());
}
}
Ok("main".to_string())
}
pub fn get_current_branch() -> Result<String> {
let output = Command::new("git")
.args(["branch", "--show-current"])
.output()
.context("Failed to run git command")?;
if !output.status.success() {
anyhow::bail!("Failed to get current branch");
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub fn fetch_and_prune() -> Result<()> {
let output = Command::new("git")
.args(["fetch", "--prune"])
.output()
.context("Failed to run git fetch --prune")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git fetch --prune failed: {}", stderr);
}
Ok(())
}
pub fn list_branches(default_branch: &str) -> Result<Vec<Branch>> {
let mut branches = Vec::new();
let local_branches = list_local_branches(default_branch)?;
branches.extend(local_branches);
let remote_branches = list_remote_branches(default_branch)?;
branches.extend(remote_branches);
Ok(branches)
}
fn list_local_branches(default_branch: &str) -> Result<Vec<Branch>> {
let output = Command::new("git")
.args([
"for-each-ref",
"--format=%(refname:short)|%(committerdate:unix)|%(objectname:short)",
"refs/heads/",
])
.output()
.context("Failed to list local branches")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to list local branches: {}", stderr);
}
let current_branch = get_current_branch().unwrap_or_default();
let stdout = String::from_utf8_lossy(&output.stdout);
let now = Utc::now();
let mut branches = Vec::new();
for line in stdout.lines() {
let parts: Vec<&str> = line.split('|').collect();
if parts.len() != 3 {
continue;
}
let name = parts[0].to_string();
let timestamp: i64 = parts[1].parse().unwrap_or(0);
let sha = parts[2].to_string();
if name == current_branch {
continue;
}
let commit_date = Utc.timestamp_opt(timestamp, 0).unwrap();
let age_days = (now - commit_date).num_days();
let is_merged = check_branch_merged(&name, default_branch)?;
branches.push(Branch {
name,
age_days,
is_merged,
is_remote: false,
last_commit_sha: sha,
last_commit_date: commit_date,
});
}
Ok(branches)
}
fn list_remote_branches(default_branch: &str) -> Result<Vec<Branch>> {
let output = Command::new("git")
.args([
"for-each-ref",
"--format=%(refname:short)|%(committerdate:unix)|%(objectname:short)",
"refs/remotes/origin/",
])
.output()
.context("Failed to list remote branches")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to list remote branches: {}", stderr);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let now = Utc::now();
let mut branches = Vec::new();
for line in stdout.lines() {
let parts: Vec<&str> = line.split('|').collect();
if parts.len() != 3 {
continue;
}
let name = parts[0].to_string();
let timestamp: i64 = parts[1].parse().unwrap_or(0);
let sha = parts[2].to_string();
if name == "origin/HEAD" || name == format!("origin/{}", default_branch) {
continue;
}
let commit_date = Utc.timestamp_opt(timestamp, 0).unwrap();
let age_days = (now - commit_date).num_days();
let is_merged = check_branch_merged(&name, default_branch)?;
branches.push(Branch {
name,
age_days,
is_merged,
is_remote: true,
last_commit_sha: sha,
last_commit_date: commit_date,
});
}
Ok(branches)
}
fn check_branch_merged(branch: &str, default_branch: &str) -> Result<bool> {
let output = Command::new("git")
.args(["branch", "--merged", default_branch, "-a"])
.output()
.context("Failed to check merged branches")?;
if !output.status.success() {
return Ok(false);
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let line = line.trim().trim_start_matches("* ");
if line == branch || line == format!("remotes/{}", branch) {
return Ok(true);
}
}
Ok(false)
}
pub fn delete_local_branch(branch: &str, force: bool) -> Result<()> {
let flag = if force { "-D" } else { "-d" };
let output = Command::new("git")
.args(["branch", flag, branch])
.output()
.context("Failed to delete branch")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("not fully merged") {
return Err(DeadbranchError::UnmergedBranch(branch.to_string()).into());
}
anyhow::bail!("Failed to delete branch '{}': {}", branch, stderr);
}
Ok(())
}
pub fn delete_remote_branch(branch: &str) -> Result<()> {
let branch_name = branch.strip_prefix("origin/").unwrap_or(branch);
let output = Command::new("git")
.args(["push", "origin", "--delete", branch_name])
.output()
.context("Failed to delete remote branch")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to delete remote branch '{}': {}", branch, stderr);
}
Ok(())
}
pub fn get_branch_sha(branch: &str) -> Result<String> {
let output = Command::new("git")
.args(["rev-parse", branch])
.output()
.context("Failed to get branch SHA")?;
if !output.status.success() {
anyhow::bail!("Failed to get SHA for branch '{}'", branch);
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}