#[allow(unused_imports)]
use crate::sync_util::LockExt;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::ui::panel_data::GitSnapshot;
#[derive(Clone)]
pub struct SharedGit(Arc<Mutex<Option<GitSnapshot>>>);
impl SharedGit {
fn new() -> Self {
Self(Arc::new(Mutex::new(None)))
}
pub fn snapshot(&self) -> Option<GitSnapshot> {
self.0.lock_ignore_poison().clone()
}
fn store(&self, snap: Option<GitSnapshot>) {
*self.0.lock_ignore_poison() = snap;
}
}
pub fn spawn_poller(interval: Duration) -> SharedGit {
let shared = SharedGit::new();
let out = shared.clone();
let cadence = interval.max(Duration::from_secs(1));
tokio::spawn(async move {
loop {
shared.store(poll_once().await);
tokio::time::sleep(cadence).await;
}
});
out
}
async fn poll_once() -> Option<GitSnapshot> {
use tokio::process::Command;
let status = Command::new("git")
.args(["status", "--porcelain=v1", "--branch"])
.output()
.await
.ok()?;
if !status.status.success() {
return None;
}
let porcelain = String::from_utf8_lossy(&status.stdout);
let (branch, staged, unstaged, untracked) = parse_status(&porcelain);
let last_commit = match Command::new("git")
.args(["log", "-1", "--format=%s"])
.output()
.await
{
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
_ => String::new(),
};
Some(GitSnapshot {
branch,
staged,
unstaged,
untracked,
last_commit,
})
}
fn parse_status(porcelain: &str) -> (String, usize, usize, usize) {
let mut branch = String::new();
let (mut staged, mut unstaged, mut untracked) = (0usize, 0usize, 0usize);
for line in porcelain.lines() {
if let Some(rest) = line.strip_prefix("## ") {
branch = rest
.split("...")
.next()
.unwrap_or(rest)
.split_whitespace()
.next()
.unwrap_or("")
.to_string();
continue;
}
let mut chars = line.chars();
let x = chars.next().unwrap_or(' ');
let y = chars.next().unwrap_or(' ');
if x == '?' && y == '?' {
untracked += 1;
continue;
}
if x == '!' && y == '!' {
continue; }
if x != ' ' {
staged += 1;
}
if y != ' ' {
unstaged += 1;
}
}
(branch, staged, unstaged, untracked)
}
#[cfg(test)]
mod tests {
use super::parse_status;
#[test]
fn parses_branch_and_counts() {
let out = [
"## main...origin/main [ahead 2]",
"M src/a.rs", " M src/b.rs", "MM src/c.rs", "A src/d.rs", "?? new.txt", "?? other.txt",
"!! target/", ]
.join("\n");
let (branch, staged, unstaged, untracked) = parse_status(&out);
assert_eq!(branch, "main");
assert_eq!(staged, 3, "staged");
assert_eq!(unstaged, 2, "unstaged");
assert_eq!(untracked, 2, "untracked");
}
#[test]
fn clean_repo_is_all_zero() {
let (branch, s, u, t) = parse_status("## feature/x\n");
assert_eq!(branch, "feature/x");
assert_eq!((s, u, t), (0, 0, 0));
}
#[test]
fn detached_head_has_no_branch_name() {
let (branch, ..) = parse_status("## HEAD (no branch)\n");
assert_eq!(branch, "HEAD");
}
}