use std::path::{Path, PathBuf};
use super::worktree_reclaim_ownership::SessionOwners;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct WorkspaceClaim {
pub session: String,
pub path: PathBuf,
pub liveness: ClaimLiveness,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum ClaimLiveness {
#[default]
Live,
SessionGone,
}
pub(crate) const UNATTRIBUTED_SESSION: &str = "<unattributed>";
impl WorkspaceClaim {
pub(crate) fn new(session: impl Into<String>, path: impl Into<PathBuf>) -> Self {
Self::with_liveness(session, path, ClaimLiveness::Live)
}
pub(crate) fn with_liveness(
session: impl Into<String>,
path: impl Into<PathBuf>,
liveness: ClaimLiveness,
) -> Self {
Self {
session: session.into(),
path: path.into(),
liveness,
}
}
pub(crate) fn unattributed(path: impl Into<PathBuf>) -> Self {
Self::new(UNATTRIBUTED_SESSION, path)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Overlap {
None,
CoversWorkspace,
Nested,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct LiveClaims {
pub claims: Vec<WorkspaceClaim>,
pub caller: Option<String>,
pub owners: SessionOwners,
}
impl LiveClaims {
pub(crate) fn foreign(claims: Vec<WorkspaceClaim>) -> Self {
Self {
claims,
caller: None,
owners: SessionOwners::default(),
}
}
pub(crate) fn claim_state(&self, path: &Path) -> ClaimState {
let candidate_forms = path_forms(path);
let mut caller_nested: Option<&str> = None;
let mut caller_workspace: Option<&str> = None;
let mut foreign_nested: Option<&str> = None;
let mut discarded: Vec<String> = Vec::new();
for claim in &self.claims {
let overlap = overlap_of(&candidate_forms, &claim.path);
if overlap == Overlap::None {
continue;
}
if claim.liveness == ClaimLiveness::SessionGone {
discarded.push(claim.session.clone());
continue;
}
let is_caller = self.caller.as_deref() == Some(claim.session.as_str());
if !is_caller {
if overlap == Overlap::CoversWorkspace {
return ClaimState::Foreign {
session: claim.session.clone(),
caller: self.caller.clone(),
};
}
foreign_nested = Some(&claim.session);
continue;
}
match overlap {
Overlap::CoversWorkspace => caller_workspace = Some(&claim.session),
Overlap::Nested => caller_nested = Some(&claim.session),
Overlap::None => unreachable!("skipped above"),
}
}
if let Some(session) = caller_workspace {
return ClaimState::CallerWorkspace {
session: session.to_string(),
};
}
if let Some(session) = foreign_nested {
return ClaimState::ForeignNested {
session: session.to_string(),
caller: self.caller.clone(),
};
}
if let Some(session) = caller_nested {
return ClaimState::CallerNested {
session: session.to_string(),
};
}
if discarded.is_empty() {
ClaimState::Unclaimed
} else {
ClaimState::DeadClaimsDiscarded {
sessions: discarded,
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ClaimState {
Unclaimed,
CallerNested {
session: String,
},
CallerWorkspace {
session: String,
},
DeadClaimsDiscarded {
sessions: Vec<String>,
},
Foreign {
session: String,
caller: Option<String>,
},
ForeignNested {
session: String,
caller: Option<String>,
},
}
impl ClaimState {
pub(crate) fn refusal(&self, now: bool) -> Option<String> {
let tense = if now {
"claims this workspace now"
} else {
"still claims this workspace"
};
match self {
Self::Unclaimed
| Self::CallerNested { .. }
| Self::DeadClaimsDiscarded { .. }
| Self::ForeignNested { .. } => None,
Self::CallerWorkspace { session } => Some(format!(
"session {session} {tense}, and that session IS the caller — this path is its \
own workspace, not a worktree inside it, so reclaiming it would delete the \
directory the caller is working in (#6806)"
)),
Self::Foreign {
session,
caller: Some(caller),
} => Some(format!(
"session {session} {tense}, and that is not the calling session ({caller}) \
(#6806)"
)),
Self::Foreign {
session,
caller: None,
} => Some(format!(
"session {session} {tense}, and the caller named no session of its own, so the \
claim cannot be attributed to it — run from inside a managed session, or stop \
that session, to reclaim this worktree (#6806)"
)),
}
}
}
impl ClaimState {
pub(crate) fn note(&self) -> Option<String> {
match self {
Self::ForeignNested { session, caller } => {
let caller = caller.as_deref().unwrap_or("no session");
Some(format!(
"session {session} claims a workspace CONTAINING this worktree, not this \
worktree itself, and the caller is {caller} — a project-level claim does \
not attribute a nested worktree, so the later gates decide it (#7652)"
))
}
Self::DeadClaimsDiscarded { sessions } => Some(format!(
"discarded {} stale workspace claim(s) — session(s) {} claim this path but no \
live tmux session answers to them, so the claim is a tombstone, not an owner \
(#7232)",
sessions.len(),
sessions.join(", ")
)),
_ => None,
}
}
}
fn path_forms(path: &Path) -> [PathBuf; 2] {
[
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()),
path.to_path_buf(),
]
}
fn overlap_of(candidate_forms: &[PathBuf; 2], claimed: &Path) -> Overlap {
let claimed_forms = path_forms(claimed);
let mut best = Overlap::None;
for c in &claimed_forms {
for p in candidate_forms {
let overlap = if c == p || c.starts_with(p) {
Overlap::CoversWorkspace
} else if p.starts_with(c) {
Overlap::Nested
} else {
Overlap::None
};
if overlap == Overlap::CoversWorkspace {
return Overlap::CoversWorkspace;
}
if overlap == Overlap::Nested {
best = Overlap::Nested;
}
}
}
best
}
#[cfg(test)]
#[path = "worktree_reclaim_claim_tests.rs"]
mod worktree_reclaim_claim_tests;