use std::fmt::Write as _;
use std::path::Path;
use anyhow::{anyhow, Context, Result};
use crate::common::{is_in_path, set_current_dir};
use crate::io::execute_and_output_no_log;
#[derive(Default)]
struct GitStatus {
branch: Option<String>,
ahead: i64,
behind: i64,
staged: i64,
modified: i64,
deleted: i64,
unmerged: i64,
untracked: i64,
}
impl GitStatus {
fn parse_porcelain2(porcerlain2_output: String) -> Option<GitStatus> {
let mut status = GitStatus::default();
for entry in porcerlain2_output.split('\0') {
let mut entry = entry.split(' ');
match entry.next() {
Some("#") => match entry.next()? {
"branch.head" => {
let head = entry.next()?;
if head != "(detached)" {
status.branch = Some(String::from(head));
}
}
"branch.ab" => {
let a = entry.next()?;
let b = entry.next()?;
status.ahead = a.parse::<i64>().ok()?.abs();
status.behind = b.parse::<i64>().ok()?.abs();
}
_ => {}
},
Some("1") | Some("2") => {
let mut xy = entry.next()?.chars();
let x = xy.next()?;
let y = xy.next()?;
if x != '.' {
status.staged += 1;
}
match y {
'M' => status.modified += 1,
'D' => status.deleted += 1,
_ => {}
}
}
Some("u") => status.unmerged += 1,
Some("?") => status.untracked += 1,
_ => {}
}
}
Some(status)
}
fn is_modified(&self) -> bool {
self.untracked + self.modified + self.deleted + self.unmerged + self.staged > 0
}
fn format_git_string(&self) -> Result<String> {
let mut git_string = String::new();
git_string.push('(');
if let Some(branch) = &self.branch {
git_string.push_str(branch);
} else {
git_string.push_str(":HEAD");
}
if self.ahead != 0 {
write!(git_string, "↑{}", self.ahead)?;
}
if self.behind != 0 {
write!(git_string, "↓{}", self.behind)?;
}
if self.is_modified() {
git_string.push('|');
if self.untracked != 0 {
write!(git_string, "+{}", self.untracked)?;
}
if self.modified != 0 {
write!(git_string, "~{}", self.modified)?;
}
if self.deleted != 0 {
write!(git_string, "-{}", self.deleted)?;
}
if self.unmerged != 0 {
write!(git_string, "x{}", self.unmerged)?;
}
if self.staged != 0 {
write!(git_string, "•{}", self.staged)?;
}
}
git_string.push(')');
Ok(git_string)
}
}
fn porcelain2() -> Result<std::process::Output> {
execute_and_output_no_log(
"git",
[
"status",
"--porcelain=v2",
"-z",
"--branch",
"--untracked-files=all",
],
)
}
pub fn git(path: &Path) -> Result<String> {
if !is_in_path("git") {
return Ok("".to_owned());
}
if set_current_dir(path).is_err() {
return Ok("".to_owned());
}
let output = porcelain2()?;
if !output.status.success() {
return Ok("".to_owned());
}
let porcerlain_output = String::from_utf8(output.stdout)?;
GitStatus::parse_porcelain2(porcerlain_output)
.context("Error while parsing Git output")?
.format_git_string()
}
pub fn git_root() -> Result<String> {
let output = execute_and_output_no_log("git", ["rev-parse", "--show-toplevel"])?;
if !output.status.success() {
return Err(anyhow!("git root: git command returned an error"));
}
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}