use std::path::Path;
use std::time::{Duration, Instant};
use tracing::warn;
use crate::core::pr_cleanup::auth_backoff::AuthBackoff;
use crate::core::pr_cleanup::{
ClaimEnder, ClaimOwnership, CleanupRegistry, Gh, HolderState, RealGh, RealGit, RealLanding,
auth_backoff, holder_state_of, host_tree_gate, sweep,
};
use crate::session_manager::SessionManager;
use crate::session_manager::worktree_ignored_output::inspect_dirt_with_ignored_output;
pub struct SessionClaims<'a> {
mgr: &'a SessionManager,
}
impl<'a> SessionClaims<'a> {
pub fn new(mgr: &'a SessionManager) -> Self {
Self { mgr }
}
}
#[async_trait::async_trait]
impl ClaimEnder for SessionClaims<'_> {
async fn claims_on(&self, path: &Path) -> anyhow::Result<Vec<String>> {
Ok(self
.mgr
.list()
.await
.into_iter()
.filter(|r| {
r.workspace_path
.as_deref()
.is_some_and(|w| trusty_common::identifies_same_path(w, path))
})
.map(|r| r.id.to_string())
.collect())
}
async fn end_claim(&self, id: &str) -> anyhow::Result<()> {
let parsed = crate::session_manager::ManagedSessionId(
id.parse::<uuid::Uuid>()
.map_err(|e| anyhow::anyhow!("`{id}` is not a managed session id: {e}"))?,
);
self.mgr
.decommission_record_only(&parsed)
.await
.map_err(|e| anyhow::anyhow!("cannot tombstone session {id}: {e}"))?;
Ok(())
}
}
#[async_trait::async_trait]
impl ClaimOwnership for SessionClaims<'_> {
async fn holder_state(&self, id: &str) -> HolderState {
let owners = self.mgr.workspace_claims(None).await.owners;
holder_state_of(owners.session_end(id))
}
async fn tree_gate(&self, path: &Path) -> Result<(), String> {
let owners = self.mgr.workspace_claims(None).await.owners;
let path = path.to_path_buf();
tokio::task::spawn_blocking(move || {
host_tree_gate(&path, &|id: &str| owners.session_end(id))
})
.await
.map_err(|e| format!("the ownership gate did not complete: {e}"))?
}
async fn release_stale_lock(&self, path: &Path) -> Result<(), String> {
let path = path.to_path_buf();
tokio::task::spawn_blocking(move || {
crate::session_manager::worktree_owner_gate::release_stale_lock(&path)
})
.await
.map_err(|e| format!("the lock judgement did not complete: {e}"))?
}
}
pub fn due(interval: Option<Duration>, last: Option<Instant>, now: Instant) -> bool {
let Some(interval) = interval else {
return false;
};
match last {
None => true,
Some(prev) => now.duration_since(prev) >= interval,
}
}
pub async fn run_sweep(mgr: &SessionManager) -> usize {
let (env, unset) = match crate::core::gh_identity::resolve_gh_env(None) {
Ok(e) => (e.vars().to_vec(), e.unset_vars().to_vec()),
Err(e) => {
warn!(
"pr cleanup sweep: no GitHub identity overlay resolved ({e}); using ambient auth"
);
(Vec::new(), Vec::new())
}
};
let gh = RealGh::new(env, unset);
run_sweep_with_gh(
mgr,
&gh,
&CleanupRegistry::production(),
auth_backoff::shared(),
)
.await
}
pub async fn run_sweep_with_gh<G: Gh>(
mgr: &SessionManager,
gh: &G,
registry: &CleanupRegistry,
backoff: &AuthBackoff,
) -> usize {
let claims = SessionClaims::new(mgr);
sweep::run_sweep_with(
gh,
&RealGit,
&claims,
&claims,
&RealLanding,
&inspect_dirt_with_ignored_output,
registry,
backoff,
)
.await
}