#![allow(unused_imports)]
use super::*;
use crate::sources::UNPROTECTED_SOURCE_WARNING;
fn project_filter_matches_identity(
organization: &str,
repository: &str,
filters: &[String],
) -> bool {
filters.is_empty()
|| filters
.iter()
.any(|filter| project_filter_matches(organization, repository, filter))
}
pub(crate) fn is_windows_absolute_path(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes[2] == b'\\' || bytes[2] == b'/')
{
return true;
}
s.starts_with("\\\\")
}
fn canonical_project_parts(value: &str) -> Option<(String, String)> {
let trimmed = value.trim();
if trimmed.is_empty()
|| trimmed.starts_with('/')
|| trimmed.starts_with('\\')
|| trimmed.contains(":\\")
{
return None;
}
let mut parts = trimmed.split(['/', '\\']);
let organization = parts
.next()
.map(str::trim)
.filter(|part| !part.is_empty())?;
let repository = parts
.next()
.map(str::trim)
.filter(|part| !part.is_empty())?;
if parts.next().is_some() {
return None;
}
Some((organization.to_string(), repository.to_string()))
}
pub(crate) fn project_filter_matches_path(cwd: &str, filters: &[String]) -> bool {
if filters.is_empty() {
return true;
}
let cwd_trimmed = cwd.trim();
let resolvable_shape = cwd_trimmed.starts_with('/')
|| cwd_trimmed.starts_with('~')
|| cwd_trimmed.starts_with("http://")
|| cwd_trimmed.starts_with("https://")
|| cwd_trimmed.starts_with("git@")
|| cwd_trimmed.starts_with("ssh://")
|| cwd_trimmed.starts_with("git://")
|| (cfg!(windows) && is_windows_absolute_path(cwd_trimmed));
if resolvable_shape
&& let Some(tiered) = aicx_parser::segmentation::infer_tiered_identity_from_cwd(Some(cwd))
{
return project_filter_matches_identity(
&tiered.identity.organization,
&tiered.identity.repository,
filters,
);
}
if let Some((organization, repository)) = canonical_project_parts(cwd) {
return project_filter_matches_identity(&organization, &repository, filters);
}
let path_segments: Vec<String> = cwd
.split(['/', '\\'])
.map(str::trim)
.filter(|segment| !segment.is_empty())
.map(|segment| segment.to_ascii_lowercase())
.collect();
if path_segments.is_empty() {
return false;
}
filters.iter().any(|filter| {
let filter = filter.trim().to_ascii_lowercase();
if filter.is_empty() {
return false;
}
if let Some(repo_only) = filter.strip_prefix('/') {
return !repo_only.is_empty()
&& !repo_only.contains('/')
&& path_segments.iter().any(|segment| segment == repo_only);
}
if let Some(owner_only) = filter.strip_suffix('/') {
return !owner_only.is_empty()
&& !owner_only.contains('/')
&& path_segments.iter().any(|segment| segment == owner_only);
}
if filter.contains('/') {
let mut parts = filter.split('/');
let Some(owner) = parts.next().filter(|part| !part.is_empty()) else {
return false;
};
let Some(repo) = parts.next().filter(|part| !part.is_empty()) else {
return false;
};
if parts.next().is_some() {
return false;
}
return path_segments
.windows(2)
.any(|pair| pair[0] == owner && pair[1] == repo);
}
path_segments.iter().any(|segment| segment == &filter)
})
}
pub(crate) fn body_mentions_repo_token(body_lower: &str, repo_lower: &str) -> bool {
if repo_lower.is_empty() {
return false;
}
body_lower
.split(|c: char| !c.is_alphanumeric() && c != '-' && c != '_')
.any(|token| token == repo_lower)
}
pub(crate) fn claude_project_dir_matches_filter(dir_name: &str, filters: &[String]) -> bool {
if filters.is_empty() {
return true;
}
filters.iter().any(|filter| {
let filter = filter.trim();
if filter.is_empty() {
return false;
}
if filter.contains('/') {
let decoded = decode_claude_project_path(dir_name);
return project_filter_matches_path(&decoded, &[filter.to_string()]);
}
let needle_lower = filter.to_ascii_lowercase();
let dir_lower = dir_name.to_ascii_lowercase();
if dir_lower == needle_lower {
return true;
}
let dash_exact = format!("-{needle_lower}");
if dir_lower == dash_exact {
return true;
}
dir_lower.ends_with(&dash_exact)
})
}
pub fn repo_name_from_cwd(cwd: Option<&str>, project_filter: &[String]) -> String {
if !project_filter.is_empty() {
if project_filter.len() == 1 {
return canonical_repo_label(&project_filter[0]);
} else if let Some(c) = cwd {
for p in project_filter {
if project_filter_matches_path(c, std::slice::from_ref(p)) {
return canonical_repo_label(p);
}
}
}
}
let cwd_str = match cwd {
Some(c) if !c.is_empty() => c,
_ => return "unknown".to_string(),
};
let path = std::path::Path::new(cwd_str);
let mut current = Some(path);
while let Some(p) = current {
if !p.as_os_str().is_empty()
&& p.join(".git").is_dir()
&& let Some(name) = p.file_name()
{
return canonical_repo_label(&name.to_string_lossy());
}
current = p.parent();
}
path.file_name()
.map(|n| canonical_repo_label(&n.to_string_lossy()))
.unwrap_or_else(|| "unknown".to_string())
}
fn canonical_repo_label(value: &str) -> String {
value
.split('/')
.map(|segment| segment.trim().to_ascii_lowercase())
.collect::<Vec<_>>()
.join("/")
}
pub fn infer_repo_name_from_current_dir() -> Result<String> {
let cwd = std::env::current_dir().context("Cannot determine current directory")?;
let mut probe = cwd.as_path();
loop {
if probe.join(".git").exists() {
let repo = probe
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.trim().is_empty())
.map(canonical_repo_label)
.ok_or_else(|| anyhow::anyhow!("Could not infer --repo from git root"))?;
return Ok(repo);
}
let Some(parent) = probe.parent() else {
break;
};
probe = parent;
}
let repo = cwd
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.trim().is_empty())
.map(canonical_repo_label)
.ok_or_else(|| anyhow::anyhow!("Could not infer --repo from the current directory"))?;
Ok(repo)
}
pub fn repo_labels_from_entries(
entries: &[TimelineEntry],
project_filter: &[String],
) -> Vec<String> {
let mut labels = BTreeSet::new();
for entry in entries {
let repo = repo_name_from_cwd(entry.cwd.as_deref(), project_filter);
if repo != "unknown" {
labels.insert(repo);
}
}
labels.into_iter().collect()
}
pub fn detect_project_name() -> String {
if let Ok(output) = std::process::Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
&& output.status.success()
{
let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
if let Some(name) = std::path::Path::new(&s).file_name() {
return canonical_repo_label(&name.to_string_lossy());
}
}
if let Ok(cwd) = std::env::current_dir()
&& let Some(name) = cwd.file_name()
{
return canonical_repo_label(&name.to_string_lossy());
}
"unknown".to_string()
}
pub fn decode_claude_project_path(encoded: &str) -> String {
let stripped = encoded.strip_prefix('-').unwrap_or(encoded);
stripped.to_string()
}