hematite/agent/
git_monitor.rs1use std::sync::atomic::{AtomicU8, Ordering};
2use std::sync::Arc;
3use std::time::Duration;
4use tokio::process::Command;
5
6#[derive(Debug, Copy, Clone, PartialEq)]
7#[repr(u8)]
8pub enum GitRemoteStatus {
9 Unknown = 0,
10 NoRemote = 1,
11 Connected = 2,
12 Behind = 3,
13 Ahead = 4,
14 Diverged = 5,
15 Error = 6,
16}
17
18pub struct GitState {
19 pub remote_status: AtomicU8,
21 pub remote_url: std::sync::Mutex<String>,
23}
24
25impl GitState {
26 pub fn new() -> Self {
27 Self {
28 remote_status: AtomicU8::new(GitRemoteStatus::Unknown as u8),
29 remote_url: std::sync::Mutex::new("None".into()),
30 }
31 }
32
33 pub fn status(&self) -> GitRemoteStatus {
34 match self.remote_status.load(Ordering::Relaxed) {
35 1 => GitRemoteStatus::NoRemote,
36 2 => GitRemoteStatus::Connected,
37 3 => GitRemoteStatus::Behind,
38 4 => GitRemoteStatus::Ahead,
39 5 => GitRemoteStatus::Diverged,
40 6 => GitRemoteStatus::Error,
41 _ => GitRemoteStatus::Unknown,
42 }
43 }
44
45 pub fn label(&self) -> String {
46 match self.status() {
47 GitRemoteStatus::Unknown => "UNKNOWN".into(),
48 GitRemoteStatus::NoRemote => "NONE".into(),
49 GitRemoteStatus::Connected => "CONNECTED".into(),
50 GitRemoteStatus::Behind => "BEHIND".into(),
51 GitRemoteStatus::Ahead => "AHEAD".into(),
52 GitRemoteStatus::Diverged => "OUT-OF-SYNC".into(),
53 GitRemoteStatus::Error => "ERR".into(),
54 }
55 }
56
57 pub fn url(&self) -> String {
58 self.remote_url.lock().unwrap().clone()
59 }
60}
61
62pub fn spawn_git_monitor() -> Arc<GitState> {
63 let state = Arc::new(GitState::new());
64 let bg = state.clone();
65
66 tokio::spawn(async move {
67 tokio::time::sleep(Duration::from_secs(5)).await;
69
70 loop {
71 if let Some((status, url)) = check_git_status().await {
72 bg.remote_status.store(status as u8, Ordering::Relaxed);
73 if let Ok(mut u) = bg.remote_url.lock() {
74 *u = url;
75 }
76 }
77 tokio::time::sleep(Duration::from_secs(300)).await;
79 }
80 });
81
82 state
83}
84
85async fn check_git_status() -> Option<(GitRemoteStatus, String)> {
86 let repo_check = Command::new("git")
88 .args(["rev-parse", "--is-inside-work-tree"])
89 .output()
90 .await
91 .ok()?;
92
93 if !repo_check.status.success() {
94 return Some((GitRemoteStatus::NoRemote, "Not a Repo".into()));
95 }
96
97 let remote_check = Command::new("git").args(["remote"]).output().await.ok()?;
99
100 let remotes = String::from_utf8_lossy(&remote_check.stdout)
101 .trim()
102 .to_string();
103 if remotes.is_empty() {
104 return Some((GitRemoteStatus::NoRemote, "None".into()));
105 }
106
107 let primary_remote = if remotes.contains("origin") {
109 "origin"
110 } else {
111 remotes.split_whitespace().next().unwrap_or("origin")
112 };
113 let url_check = Command::new("git")
114 .args(["remote", "get-url", primary_remote])
115 .output()
116 .await
117 .ok()?;
118 let url = String::from_utf8_lossy(&url_check.stdout)
119 .trim()
120 .to_string();
121
122 let _ = Command::new("git")
125 .args(["fetch", "--quiet", primary_remote])
126 .output()
127 .await;
128
129 let sync_check = Command::new("git")
131 .args(["rev-list", "--left-right", "--count", "HEAD...HEAD@{u}"])
132 .output()
133 .await
134 .ok()?;
135
136 if sync_check.status.success() {
137 let counts = String::from_utf8_lossy(&sync_check.stdout)
138 .trim()
139 .to_string();
140 let parts: Vec<&str> = counts.split_whitespace().collect();
141 if parts.len() == 2 {
142 let ahead: u32 = parts[0].parse().unwrap_or(0);
143 let behind: u32 = parts[1].parse().unwrap_or(0);
144
145 if ahead > 0 && behind > 0 {
146 return Some((GitRemoteStatus::Diverged, url));
147 } else if ahead > 0 {
148 return Some((GitRemoteStatus::Ahead, url));
149 } else if behind > 0 {
150 return Some((GitRemoteStatus::Behind, url));
151 } else {
152 return Some((GitRemoteStatus::Connected, url));
153 }
154 }
155 }
156
157 Some((GitRemoteStatus::Connected, url))
159}