use std::path::PathBuf;
use std::sync::Arc;
use tokio::runtime::{Handle, RuntimeFlavor};
use tokio::sync::{Mutex, OnceCell};
use super::proto::{GitHistoryOp, GitHistoryReply, SyncOutcome};
use crate::comms::client::CommsClient;
use crate::comms::ids::AgentId;
use crate::git::CommitInfo;
#[derive(Clone)]
pub struct RemoteHistory {
root: PathBuf,
agent: AgentId,
client: Arc<OnceCell<Arc<Mutex<CommsClient>>>>,
}
impl RemoteHistory {
pub fn new(root: PathBuf, agent: AgentId) -> Self {
Self {
root,
agent,
client: Arc::new(OnceCell::new()),
}
}
pub fn indexed_head(&self) -> Option<String> {
match self.call(GitHistoryOp::IndexedHead)? {
GitHistoryReply::IndexedHead(head) => head,
_ => None,
}
}
pub fn commits(&self, op: GitHistoryOp) -> Vec<CommitInfo> {
match self.call(op) {
Some(GitHistoryReply::Commits(commits)) => commits,
_ => Vec::new(),
}
}
fn call(&self, op: GitHistoryOp) -> Option<GitHistoryReply> {
let handle = Handle::try_current().ok()?;
if handle.runtime_flavor() != RuntimeFlavor::MultiThread {
tracing::debug!("git-history: no multi-thread runtime to forward on; falling back to the live walk");
return None;
}
tokio::task::block_in_place(|| handle.block_on(self.call_async(op)))
}
async fn call_async(&self, op: GitHistoryOp) -> Option<GitHistoryReply> {
let client = self
.client
.get_or_try_init(|| async {
let client = CommsClient::connect(
&crate::comms::singleton::resolve_paths()?,
self.agent.clone(),
None,
Some(self.root.clone()),
)
.await?;
Ok::<_, crate::comms::client::CommsClientError>(Arc::new(Mutex::new(client)))
})
.await
.inspect_err(|error| tracing::warn!(%error, "git-history: daemon unreachable; tools live-walk"))
.ok()?;
let mut guard = client.lock().await;
guard
.git_history(self.root.clone(), op)
.await
.inspect_err(|error| tracing::warn!(%error, "git-history: forwarded query failed; tools live-walk"))
.ok()
}
}
pub fn daemon_is_up() -> bool {
let Ok(paths) = crate::comms::singleton::resolve_paths() else {
return false;
};
#[cfg(unix)]
if !paths.socket_path.exists() {
return false;
}
crate::comms::singleton::probe_alive(&paths.socket_path)
}
const SYNC_RETRIES: u32 = 5;
const SYNC_BACKOFF: std::time::Duration = std::time::Duration::from_secs(1);
pub async fn request_sync(root: PathBuf, agent: AgentId) -> Option<SyncOutcome> {
let mut backoff = SYNC_BACKOFF;
for attempt in 0..=SYNC_RETRIES {
match try_sync(&root, &agent, attempt == 0).await {
Ok(outcome) => return Some(outcome),
Err(error) if attempt == SYNC_RETRIES => {
tracing::warn!(%error, "git-history: daemon sync failed; history tools live-walk");
}
Err(error) => {
tracing::debug!(%error, ?backoff, "git-history: daemon sync failed; retrying");
tokio::time::sleep(backoff).await;
backoff *= 2;
}
}
}
None
}
async fn try_sync(
root: &std::path::Path,
agent: &AgentId,
may_spawn: bool,
) -> Result<SyncOutcome, crate::comms::client::CommsClientError> {
let mut client = if may_spawn {
CommsClient::ensure_and_connect(agent.clone(), None, Some(root.to_path_buf())).await?
} else {
CommsClient::connect(
&crate::comms::singleton::resolve_paths()?,
agent.clone(),
None,
Some(root.to_path_buf()),
)
.await?
};
match client.git_history(root.to_path_buf(), GitHistoryOp::Sync).await? {
GitHistoryReply::Synced(outcome) => Ok(outcome),
_ => Err(crate::comms::client::CommsClientError::Unexpected {
request: "git_history sync",
}),
}
}