use std::hash::{Hash, Hasher};
use std::path::Path;
use super::detect_project_in;
use super::worktree::{git_toplevel, main_working_tree};
pub const UNATTRIBUTED: &str = "unattributed";
pub fn resolve_project_id(start_dir: &Path) -> Option<String> {
let main = main_working_tree(start_dir)?;
if let Some(id) = project_toml_id(&main) {
return Some(id);
}
if let Some(local_root) = git_toplevel(start_dir) {
if local_root != main {
if let Some(id) = project_toml_id(&local_root) {
return Some(id);
}
}
}
Some(path_based_project_id(&main))
}
pub fn current_project_id() -> Option<String> {
let cwd = std::env::current_dir().ok()?;
resolve_project_id(&cwd)
}
pub fn resolve_project_id_with_config(
config_id: Option<&str>,
start_dir: &Path,
) -> Option<String> {
if let Some(id) = config_id.map(str::trim).filter(|id| !id.is_empty()) {
return Some(id.to_string());
}
resolve_project_id(start_dir)
}
pub fn project_display(project_id: Option<&str>) -> &str {
match project_id.map(str::trim).filter(|id| !id.is_empty()) {
Some(id) => id,
None => UNATTRIBUTED,
}
}
pub fn retain_project(tasks: &mut Vec<crate::types::Task>, filter: Option<&str>) {
match filter {
Some(id) => tasks.retain(|task| task.project_id.as_deref() == Some(id)),
None => tasks.retain(|task| task.project_id.is_none()),
}
}
pub fn matches_project_filter(task_project_id: Option<&str>, filter: Option<&str>) -> bool {
match filter {
Some(id) => task_project_id == Some(id),
None => task_project_id.is_none(),
}
}
pub fn project_filter_banner(filter: Option<&str>, all_projects: bool) -> String {
if all_projects {
return "project:* (all projects; default is current project)".to_string();
}
format!(
"project:{} (use --all to show every project)",
project_display(filter)
)
}
pub fn path_based_project_id(repo_dir: &Path) -> String {
let canonical = repo_dir.canonicalize().ok();
let basename = canonical
.as_ref()
.and_then(|path| path.file_name())
.map(|name| name.to_string_lossy().into_owned())
.filter(|name| !name.is_empty())
.unwrap_or_else(|| "default".to_string());
let hash = canonical
.as_ref()
.map(|path| {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
path.to_string_lossy().hash(&mut hasher);
format!("{:x}", hasher.finish())
})
.unwrap_or_else(|| "0".to_string());
let hash_short: String = hash.chars().take(8).collect();
format!("{basename}-{hash_short}")
}
fn project_toml_id(repo_dir: &Path) -> Option<String> {
let config = detect_project_in(repo_dir)?;
let id = config.id.trim();
if id.is_empty() {
None
} else {
Some(id.to_string())
}
}
#[cfg(test)]
#[path = "identity_tests.rs"]
mod tests;