use crate::error::{LsPlusError, Result}; use std::path::Path; use std::process::Command;
#[derive(Debug, Clone)]
pub struct GitStatus {
pub is_repo: bool,
pub branch: Option<String>,
pub files: Vec<GitFileStatus>,
}
#[derive(Debug, Clone)]
pub struct GitFileStatus {
pub path: String,
pub status: GitFileState,
}
#[derive(Debug, Clone, PartialEq)]
pub enum GitFileState {
Untracked,
Modified,
Added,
Deleted,
Renamed,
Copied,
Ignored,
Clean,
}
impl Default for GitStatus {
fn default() -> Self {
Self::new()
}
}
impl GitStatus {
pub fn new() -> Self {
Self {
is_repo: false, branch: None, files: Vec::new(), }
}
pub fn get_file_status(&self, file_path: &str) -> GitFileState {
self.files
.iter()
.find(|f| f.path == file_path) .map(|f| f.status.clone()) .unwrap_or(GitFileState::Clean) }
}
pub async fn get_git_status(path: &Path) -> Result<GitStatus> {
if !is_git_repo(path).await? {
return Ok(GitStatus::new());
}
let mut git_status = GitStatus {
is_repo: true,
branch: get_current_branch(path).await?,
files: Vec::new(),
};
let output = Command::new("git")
.args(["status", "--porcelain"]) .current_dir(path) .output()
.map_err(|e| LsPlusError::git_error(format!("Failed to run git status: {e}")))?;
if !output.status.success() {
return Err(LsPlusError::git_error("Git status command failed"));
}
let status_text = String::from_utf8_lossy(&output.stdout);
for line in status_text.lines() {
if line.len() < 3 {
continue;
}
let status_code = &line[0..2]; let file_path = &line[3..];
let status = match status_code {
"??" => GitFileState::Untracked, "A " | " A" => GitFileState::Added, "M " | " M" | "MM" => GitFileState::Modified, "D " | " D" => GitFileState::Deleted, "R " | " R" => GitFileState::Renamed, "C " | " C" => GitFileState::Copied, "!!" => GitFileState::Ignored, _ => GitFileState::Modified, };
git_status.files.push(GitFileStatus {
path: file_path.to_string(),
status,
});
}
Ok(git_status)
}
async fn is_git_repo(path: &Path) -> Result<bool> {
let output = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"]) .current_dir(path) .output()
.map_err(|_| LsPlusError::git_error("Failed to check if directory is a git repository"))?;
Ok(output.status.success())
}
async fn get_current_branch(path: &Path) -> Result<Option<String>> {
let output = Command::new("git")
.args(["branch", "--show-current"]) .current_dir(path) .output()
.map_err(|e| LsPlusError::git_error(format!("Failed to get current branch: {e}")))?;
if !output.status.success() {
return Ok(None);
}
let branch = String::from_utf8_lossy(&output.stdout)
.trim() .to_string();
if branch.is_empty() {
Ok(None)
} else {
Ok(Some(branch))
}
}
pub fn get_git_status_symbol(status: &GitFileState) -> &'static str {
match status {
GitFileState::Untracked => "??", GitFileState::Modified => "M ", GitFileState::Added => "A ", GitFileState::Deleted => "D ", GitFileState::Renamed => "R ", GitFileState::Copied => "C ", GitFileState::Ignored => "!!", GitFileState::Clean => " ", }
}
pub fn get_git_status_color(status: &GitFileState) -> console::Color {
match status {
GitFileState::Untracked => console::Color::Red, GitFileState::Modified => console::Color::Yellow, GitFileState::Added => console::Color::Green, GitFileState::Deleted => console::Color::Red, GitFileState::Renamed => console::Color::Blue, GitFileState::Copied => console::Color::Blue, GitFileState::Ignored => console::Color::Black, GitFileState::Clean => console::Color::White, }
}