use std::path::{Path, PathBuf};
use trusty_mpm::project::Project;
const SCRATCHPAD_SEGMENT: &str = "scratchpad";
const TEMP_PREFIXES: [&str; 3] = ["/tmp", "/private/tmp", "/var/folders"];
pub(crate) fn is_offerable_project(project: &Project) -> bool {
match local_checkout_path(&project.repo_url) {
Some(path) => is_offerable_checkout(&path),
None => true,
}
}
pub(crate) fn local_checkout_path(repo_url: &str) -> Option<PathBuf> {
let trimmed = repo_url.trim();
trimmed.starts_with('/').then(|| PathBuf::from(trimmed))
}
pub(crate) fn is_offerable_checkout(path: &Path) -> bool {
!under_temp_dir(path) && !has_scratchpad_segment(path) && path.is_dir() && in_git_checkout(path)
}
fn under_temp_dir(path: &Path) -> bool {
let tmpdir = std::env::var_os("TMPDIR").map(PathBuf::from);
TEMP_PREFIXES
.iter()
.map(Path::new)
.any(|prefix| path.starts_with(prefix))
|| tmpdir.is_some_and(|dir| {
let dir = dir.to_string_lossy().trim_end_matches('/').to_string();
!dir.is_empty() && path.starts_with(&dir)
})
}
fn has_scratchpad_segment(path: &Path) -> bool {
path.components().any(|c| {
c.as_os_str()
.to_string_lossy()
.eq_ignore_ascii_case(SCRATCHPAD_SEGMENT)
})
}
fn in_git_checkout(path: &Path) -> bool {
path.ancestors().any(|dir| dir.join(".git").exists())
}
#[cfg(test)]
mod tests {
use super::*;
fn project(name: &str, repo_url: &str) -> Project {
serde_json::from_value(serde_json::json!({
"name": name,
"repo_url": repo_url,
"default_branch": "main",
}))
.expect("project fixture")
}
fn fixture() -> Vec<Project> {
vec![
project(
"mcp-probe-scratch-4181",
"/private/tmp/claude-502/abc/scratchpad/mcp-probe-scratch-4181",
),
project("gone", "/nonexistent/7406/no-such-checkout"),
project("trusty-tools", "https://github.com/bobmatnyc/trusty-tools"),
project("apex", "git@github.com:duetto/apex.git"),
]
}
#[test]
fn offerable_drops_the_throwaway_rows_and_keeps_the_rest() {
let rows = fixture();
let kept: Vec<&str> = rows
.iter()
.filter(|p| is_offerable_project(p))
.map(|p| p.name.as_str())
.collect();
assert_eq!(kept, vec!["trusty-tools", "apex"]);
}
#[test]
fn offerable_keeps_url_projects() {
assert!(is_offerable_project(&project(
"t",
"https://github.com/bobmatnyc/trusty-tools"
)));
assert!(is_offerable_project(&project("t", "")));
}
#[test]
fn offerable_drops_a_temp_scratchpad() {
assert!(!is_offerable_checkout(Path::new("/private/tmp/x/y")));
assert!(!is_offerable_checkout(Path::new("/tmp/x")));
assert!(!is_offerable_checkout(Path::new("/var/folders/ab/cd/T/x")));
assert!(!is_offerable_checkout(Path::new(
"/Users/me/work/scratchpad/probe"
)));
assert!(!under_temp_dir(Path::new("/tmpfoo/bar")));
}
#[test]
fn offerable_drops_a_path_that_is_gone() {
assert!(!is_offerable_checkout(Path::new(
"/nonexistent/7406/no-such-checkout"
)));
}
#[test]
fn offerable_keeps_a_real_checkout() {
let here = &trusty_common::test_harness::test_repo_root()
.expect("resolve the checkout")
.join("crates/trusty-mpm");
assert!(in_git_checkout(here), "{here:?} is not in a checkout");
assert!(is_offerable_checkout(here), "{here:?} was rejected");
}
}