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 Default for GitState {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl GitState {
32 pub fn new() -> Self {
33 Self {
34 remote_status: AtomicU8::new(GitRemoteStatus::Unknown as u8),
35 remote_url: std::sync::Mutex::new("None".into()),
36 }
37 }
38
39 pub fn status(&self) -> GitRemoteStatus {
40 match self.remote_status.load(Ordering::Relaxed) {
41 1 => GitRemoteStatus::NoRemote,
42 2 => GitRemoteStatus::Connected,
43 3 => GitRemoteStatus::Behind,
44 4 => GitRemoteStatus::Ahead,
45 5 => GitRemoteStatus::Diverged,
46 6 => GitRemoteStatus::Error,
47 _ => GitRemoteStatus::Unknown,
48 }
49 }
50
51 pub fn label(&self) -> String {
52 match self.status() {
53 GitRemoteStatus::Unknown => "UNKNOWN".into(),
54 GitRemoteStatus::NoRemote => "NONE".into(),
55 GitRemoteStatus::Connected => "CONNECTED".into(),
56 GitRemoteStatus::Behind => "BEHIND".into(),
57 GitRemoteStatus::Ahead => "AHEAD".into(),
58 GitRemoteStatus::Diverged => "OUT-OF-SYNC".into(),
59 GitRemoteStatus::Error => "ERR".into(),
60 }
61 }
62
63 pub fn url(&self) -> String {
64 self.remote_url.lock().unwrap().clone()
65 }
66}
67
68pub fn spawn_git_monitor() -> Arc<GitState> {
69 let state = Arc::new(GitState::new());
70 let bg = state.clone();
71
72 tokio::spawn(async move {
73 tokio::time::sleep(Duration::from_secs(5)).await;
75
76 loop {
77 if let Some((status, url)) = check_git_status().await {
78 bg.remote_status.store(status as u8, Ordering::Relaxed);
79 if let Ok(mut u) = bg.remote_url.lock() {
80 *u = url;
81 }
82 }
83 tokio::time::sleep(Duration::from_secs(300)).await;
85 }
86 });
87
88 state
89}
90
91async fn check_git_status() -> Option<(GitRemoteStatus, String)> {
92 let repo_check = Command::new("git")
94 .args(["rev-parse", "--is-inside-work-tree"])
95 .output()
96 .await
97 .ok()?;
98
99 if !repo_check.status.success() {
100 return Some((GitRemoteStatus::NoRemote, "Not a Repo".into()));
101 }
102
103 let remote_check = Command::new("git").args(["remote"]).output().await.ok()?;
105
106 let remotes = String::from_utf8_lossy(&remote_check.stdout)
107 .trim()
108 .to_string();
109 if remotes.is_empty() {
110 return Some((GitRemoteStatus::NoRemote, "None".into()));
111 }
112
113 let primary_remote = if remotes.contains("origin") {
115 "origin"
116 } else {
117 remotes.split_whitespace().next().unwrap_or("origin")
118 };
119 let url_check = Command::new("git")
120 .args(["remote", "get-url", primary_remote])
121 .output()
122 .await
123 .ok()?;
124 let url = String::from_utf8_lossy(&url_check.stdout)
125 .trim()
126 .to_string();
127
128 let _ = Command::new("git")
131 .args(["fetch", "--quiet", primary_remote])
132 .output()
133 .await;
134
135 let sync_check = Command::new("git")
137 .args(["rev-list", "--left-right", "--count", "HEAD...HEAD@{u}"])
138 .output()
139 .await
140 .ok()?;
141
142 if sync_check.status.success() {
143 let counts = String::from_utf8_lossy(&sync_check.stdout)
144 .trim()
145 .to_string();
146 let mut it = counts.split_whitespace();
147 if let (Some(ahead_str), Some(behind_str), None) = (it.next(), it.next(), it.next()) {
148 let ahead: u32 = ahead_str.parse().unwrap_or(0);
149 let behind: u32 = behind_str.parse().unwrap_or(0);
150
151 if ahead > 0 && behind > 0 {
152 return Some((GitRemoteStatus::Diverged, url));
153 } else if ahead > 0 {
154 return Some((GitRemoteStatus::Ahead, url));
155 } else if behind > 0 {
156 return Some((GitRemoteStatus::Behind, url));
157 } else {
158 return Some((GitRemoteStatus::Connected, url));
159 }
160 }
161 }
162
163 Some((GitRemoteStatus::Connected, url))
165}