use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
use super::daemon::Broker;
use super::protocol::CommsResponse;
use crate::git_history::proto::{GitHistoryOp, GitHistoryReply};
use crate::git_history::{GitHistoryError, GitHistoryIndex};
pub(crate) struct HistoryEntry {
index: Arc<GitHistoryIndex>,
build_lock: Arc<Mutex<()>>,
last_used: std::sync::Mutex<Instant>,
}
impl HistoryEntry {
fn touch(&self) {
*self.last_used.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = Instant::now();
}
pub(crate) fn idle_for(&self) -> Duration {
self.last_used
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.elapsed()
}
}
impl Broker {
pub(crate) async fn on_git_history(&self, root: std::path::PathBuf, op: GitHistoryOp) -> CommsResponse {
self.mark_active().await;
if !crate::git_history::index_enabled() {
return CommsResponse::Error {
code: "git_history_disabled".to_string(),
message: "git-history index disabled (BASEMIND_GH_INDEX=0)".to_string(),
};
}
let dir = crate::git_history::shared_history_basemind_dir(&root);
let entry = match self.history_entry(&dir).await {
Ok(entry) => entry,
Err(error) => {
return CommsResponse::Error {
code: "git_history_open_failed".to_string(),
message: error.to_string(),
};
}
};
entry.touch();
let _build_guard = match op {
GitHistoryOp::Sync => Some(entry.build_lock.clone().lock_owned().await),
_ => None,
};
let index = Arc::clone(&entry.index);
match tokio::task::spawn_blocking(move || run_git_history_op(&index, &root, &dir, op)).await {
Ok(Ok(reply)) => CommsResponse::GitHistory(reply),
Ok(Err(error)) => CommsResponse::Error {
code: "git_history_failed".to_string(),
message: error.to_string(),
},
Err(join) => CommsResponse::Error {
code: "git_history_panicked".to_string(),
message: join.to_string(),
},
}
}
async fn history_entry(&self, dir: &std::path::Path) -> Result<Arc<HistoryEntry>, GitHistoryError> {
if let Some(entry) = self.history_lookup(dir) {
return Ok(entry);
}
let _opening = self.git_history_open_lock.lock().await;
if let Some(entry) = self.history_lookup(dir) {
return Ok(entry);
}
let open_dir = dir.to_path_buf();
let opened = tokio::task::spawn_blocking(move || GitHistoryIndex::open(&open_dir))
.await
.map_err(|join| GitHistoryError::Io {
path: dir.to_path_buf(),
source: std::io::Error::other(join.to_string()),
})??;
let entry = Arc::new(HistoryEntry {
index: Arc::new(opened),
build_lock: Arc::new(Mutex::new(())),
last_used: std::sync::Mutex::new(Instant::now()),
});
self.git_history
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(dir.to_path_buf(), Arc::clone(&entry));
Ok(entry)
}
fn history_lookup(&self, dir: &std::path::Path) -> Option<Arc<HistoryEntry>> {
self.git_history
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(dir)
.map(Arc::clone)
}
}
fn run_git_history_op(
index: &GitHistoryIndex,
root: &std::path::Path,
dir: &std::path::Path,
op: GitHistoryOp,
) -> Result<GitHistoryReply, GitHistoryError> {
match op {
GitHistoryOp::Sync => {
let repo = crate::git::Repo::discover(root)?;
let outcome = crate::git_history::builder::sync(index, &repo, dir)?;
tracing::info!(root = %root.display(), ?outcome, "git-history index sync complete");
Ok(GitHistoryReply::Synced(outcome.into()))
}
GitHistoryOp::IndexedHead => Ok(GitHistoryReply::IndexedHead(index.last_indexed_head_hex())),
GitHistoryOp::RecentCommits {
skip,
take,
include_files,
} => Ok(GitHistoryReply::Commits(index.recent_commits(
skip,
take,
include_files,
))),
GitHistoryOp::CommitsTouching { path, skip, take } => {
Ok(GitHistoryReply::Commits(index.commits_touching(&path, skip, take)))
}
GitHistoryOp::WindowCommits { window } => Ok(GitHistoryReply::Commits(index.window_commits(window))),
GitHistoryOp::SearchCommits {
query,
scope,
skip,
take,
} => Ok(GitHistoryReply::Commits(
index.search_commits(&query, scope, skip, take),
)),
}
}