use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::process::Command;
#[derive(Debug, Copy, Clone, PartialEq)]
#[repr(u8)]
pub enum GitRemoteStatus {
Unknown = 0,
NoRemote = 1,
Connected = 2,
Behind = 3,
Ahead = 4,
Diverged = 5,
Error = 6,
}
pub struct GitState {
pub remote_status: AtomicU8,
pub remote_url: std::sync::Mutex<String>,
}
impl Default for GitState {
fn default() -> Self {
Self::new()
}
}
impl GitState {
pub fn new() -> Self {
Self {
remote_status: AtomicU8::new(GitRemoteStatus::Unknown as u8),
remote_url: std::sync::Mutex::new("None".into()),
}
}
pub fn status(&self) -> GitRemoteStatus {
match self.remote_status.load(Ordering::Relaxed) {
1 => GitRemoteStatus::NoRemote,
2 => GitRemoteStatus::Connected,
3 => GitRemoteStatus::Behind,
4 => GitRemoteStatus::Ahead,
5 => GitRemoteStatus::Diverged,
6 => GitRemoteStatus::Error,
_ => GitRemoteStatus::Unknown,
}
}
pub fn label(&self) -> String {
match self.status() {
GitRemoteStatus::Unknown => "UNKNOWN".into(),
GitRemoteStatus::NoRemote => "NONE".into(),
GitRemoteStatus::Connected => "CONNECTED".into(),
GitRemoteStatus::Behind => "BEHIND".into(),
GitRemoteStatus::Ahead => "AHEAD".into(),
GitRemoteStatus::Diverged => "OUT-OF-SYNC".into(),
GitRemoteStatus::Error => "ERR".into(),
}
}
pub fn url(&self) -> String {
self.remote_url.lock().unwrap().clone()
}
}
pub fn spawn_git_monitor() -> Arc<GitState> {
let state = Arc::new(GitState::new());
let bg = state.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(5)).await;
loop {
if let Some((status, url)) = check_git_status().await {
bg.remote_status.store(status as u8, Ordering::Relaxed);
if let Ok(mut u) = bg.remote_url.lock() {
*u = url;
}
}
tokio::time::sleep(Duration::from_secs(300)).await;
}
});
state
}
async fn check_git_status() -> Option<(GitRemoteStatus, String)> {
let repo_check = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.output()
.await
.ok()?;
if !repo_check.status.success() {
return Some((GitRemoteStatus::NoRemote, "Not a Repo".into()));
}
let remote_check = Command::new("git").args(["remote"]).output().await.ok()?;
let remotes = String::from_utf8_lossy(&remote_check.stdout)
.trim()
.to_string();
if remotes.is_empty() {
return Some((GitRemoteStatus::NoRemote, "None".into()));
}
let primary_remote = if remotes.contains("origin") {
"origin"
} else {
remotes.split_whitespace().next().unwrap_or("origin")
};
let url_check = Command::new("git")
.args(["remote", "get-url", primary_remote])
.output()
.await
.ok()?;
let url = String::from_utf8_lossy(&url_check.stdout)
.trim()
.to_string();
let _ = Command::new("git")
.args(["fetch", "--quiet", primary_remote])
.output()
.await;
let sync_check = Command::new("git")
.args(["rev-list", "--left-right", "--count", "HEAD...HEAD@{u}"])
.output()
.await
.ok()?;
if sync_check.status.success() {
let counts = String::from_utf8_lossy(&sync_check.stdout)
.trim()
.to_string();
let mut it = counts.split_whitespace();
if let (Some(ahead_str), Some(behind_str), None) = (it.next(), it.next(), it.next()) {
let ahead: u32 = ahead_str.parse().unwrap_or(0);
let behind: u32 = behind_str.parse().unwrap_or(0);
if ahead > 0 && behind > 0 {
return Some((GitRemoteStatus::Diverged, url));
} else if ahead > 0 {
return Some((GitRemoteStatus::Ahead, url));
} else if behind > 0 {
return Some((GitRemoteStatus::Behind, url));
} else {
return Some((GitRemoteStatus::Connected, url));
}
}
}
Some((GitRemoteStatus::Connected, url))
}