use std::path::Path;
use crate::slug::slugify_string;
pub const PALACE_OVERRIDE_ENV: &str = "TRUSTY_MEMORY_PALACE";
pub fn palace_override_from_env() -> Option<String> {
match std::env::var(PALACE_OVERRIDE_ENV) {
Ok(v) if !v.trim().is_empty() => Some(v),
_ => None,
}
}
pub fn owner_repo_from_git_remote(url: &str) -> Option<String> {
let trimmed = url.trim();
if trimmed.is_empty() {
return None;
}
let without_scheme = strip_scheme(trimmed);
let path = host_relative_path(without_scheme);
let path = path.trim_end_matches('/');
let path = path.strip_suffix(".git").unwrap_or(path);
let path = path.trim_end_matches('/');
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if segments.is_empty() {
return None;
}
let (owner, repo) = match segments.as_slice() {
[.., owner, repo] => (Some(*owner), *repo),
[repo] => (None, *repo),
_ => return None,
};
let repo_slug = slugify_string(repo);
if repo_slug.is_empty() {
return None;
}
match owner {
Some(owner) => {
let owner_slug = slugify_string(owner);
if owner_slug.is_empty() {
Some(repo_slug)
} else {
Some(format!("{owner_slug}-{repo_slug}"))
}
}
None => Some(repo_slug),
}
}
fn strip_scheme(url: &str) -> &str {
match url.find("://") {
Some(idx) => &url[idx + 3..],
None => url,
}
}
fn host_relative_path(locator: &str) -> &str {
let colon = locator.find(':');
let slash = locator.find('/');
match (colon, slash) {
(Some(c), maybe_slash) if maybe_slash.is_none_or(|s| c < s) => {
let after_colon = &locator[c + 1..];
let port_end = after_colon.find('/').unwrap_or(after_colon.len());
let potential_port = &after_colon[..port_end];
if !potential_port.is_empty() && potential_port.bytes().all(|b| b.is_ascii_digit()) {
match after_colon.find('/') {
Some(s) => &after_colon[s + 1..],
None => "",
}
} else {
after_colon
}
}
(_, Some(s)) => &locator[s + 1..],
_ => locator,
}
}
pub fn parent_dir_slug(root: &Path) -> Option<String> {
let leaf = root.file_name().and_then(|s| s.to_str())?;
let leaf_slug = slugify_string(leaf);
if leaf_slug.is_empty() {
return None;
}
let parent_slug = root
.parent()
.and_then(|p| p.file_name())
.and_then(|s| s.to_str())
.map(slugify_string)
.filter(|s| !s.is_empty());
match parent_slug {
Some(parent) => Some(format!("{parent}-{leaf_slug}")),
None => Some(leaf_slug),
}
}
pub fn derive_palace_id(
project_root: &Path,
git_remote: Option<&str>,
override_value: Option<&str>,
) -> Option<String> {
if let Some(slug) = override_value.map(slugify_string).filter(|s| !s.is_empty()) {
return Some(slug);
}
if let Some(slug) = git_remote.and_then(owner_repo_from_git_remote) {
return Some(slug);
}
parent_dir_slug(project_root)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::{Path, PathBuf};
#[test]
fn git_ssh_github() {
assert_eq!(
owner_repo_from_git_remote("git@github.com:bobmatnyc/trusty-tools.git").as_deref(),
Some("bobmatnyc-trusty-tools")
);
}
#[test]
fn git_https_github_with_and_without_dot_git() {
assert_eq!(
owner_repo_from_git_remote("https://github.com/bobmatnyc/trusty-tools.git").as_deref(),
Some("bobmatnyc-trusty-tools")
);
assert_eq!(
owner_repo_from_git_remote("https://github.com/bobmatnyc/trusty-tools").as_deref(),
Some("bobmatnyc-trusty-tools")
);
}
#[test]
fn git_non_github_host() {
assert_eq!(
owner_repo_from_git_remote("git@gitlab.example.com:acme/Cool_App.git").as_deref(),
Some("acme-cool-app")
);
assert_eq!(
owner_repo_from_git_remote("https://gitlab.example.com/acme/Cool_App").as_deref(),
Some("acme-cool-app")
);
}
#[test]
fn git_self_hosted_with_port() {
assert_eq!(
owner_repo_from_git_remote("https://git.company.com:8080/repo.git").as_deref(),
Some("repo")
);
assert_eq!(
owner_repo_from_git_remote("https://git.company.com:8080/owner/repo.git").as_deref(),
Some("owner-repo")
);
assert_eq!(
owner_repo_from_git_remote("https://git.company.com:8080/owner/repo/").as_deref(),
Some("owner-repo")
);
assert_eq!(
owner_repo_from_git_remote("git@git.company.com:owner/repo.git").as_deref(),
Some("owner-repo")
);
}
#[test]
fn git_trailing_slash() {
assert_eq!(
owner_repo_from_git_remote("https://github.com/bobmatnyc/trusty-tools/").as_deref(),
Some("bobmatnyc-trusty-tools")
);
}
#[test]
fn git_nested_group_takes_last_two() {
assert_eq!(
owner_repo_from_git_remote("https://gitlab.com/acme/team/widget.git").as_deref(),
Some("team-widget")
);
}
#[test]
fn git_repo_only() {
assert_eq!(
owner_repo_from_git_remote("git@host:repo.git").as_deref(),
Some("repo")
);
}
#[test]
fn git_empty_returns_none() {
assert_eq!(owner_repo_from_git_remote(""), None);
assert_eq!(owner_repo_from_git_remote(" "), None);
assert_eq!(owner_repo_from_git_remote("https://github.com/"), None);
}
#[test]
fn parent_dir_two_components() {
assert_eq!(
parent_dir_slug(Path::new("/Users/bob/Projects/trusty-tools")).as_deref(),
Some("projects-trusty-tools")
);
}
#[test]
fn parent_dir_normalises_case_and_underscores() {
assert_eq!(
parent_dir_slug(Path::new("/x/My_Org/Cool_App")).as_deref(),
Some("my-org-cool-app")
);
}
#[test]
fn parent_dir_single_component() {
assert_eq!(parent_dir_slug(Path::new("/solo")).as_deref(), Some("solo"));
}
#[test]
fn parent_dir_root_returns_none() {
assert_eq!(parent_dir_slug(Path::new("/")), None);
}
#[test]
fn override_env_wins_over_git() {
let root = PathBuf::from("/Users/bob/Projects/trusty-tools");
let got = derive_palace_id(
&root,
Some("git@github.com:bobmatnyc/trusty-tools.git"),
Some("my-override"),
);
assert_eq!(got.as_deref(), Some("my-override"));
}
#[test]
fn env_override_is_slugified() {
let root = PathBuf::from("/x/y");
let got = derive_palace_id(&root, None, Some("My Project_Name"));
assert_eq!(got.as_deref(), Some("my-project-name"));
}
#[test]
fn empty_override_falls_through_to_git() {
let root = PathBuf::from("/x/y");
let got = derive_palace_id(&root, Some("git@github.com:acme/widget.git"), Some(" "));
assert_eq!(got.as_deref(), Some("acme-widget"));
}
#[test]
fn git_used_when_no_override() {
let root = PathBuf::from("/some/checkout-dir");
let got = derive_palace_id(
&root,
Some("https://github.com/bobmatnyc/trusty-tools.git"),
None,
);
assert_eq!(got.as_deref(), Some("bobmatnyc-trusty-tools"));
}
#[test]
fn falls_back_to_parent_dir() {
let root = PathBuf::from("/Users/bob/Projects/trusty-tools");
assert_eq!(
derive_palace_id(&root, None, None).as_deref(),
Some("projects-trusty-tools")
);
assert_eq!(
derive_palace_id(&root, Some(""), None).as_deref(),
Some("projects-trusty-tools")
);
}
#[test]
fn all_empty_returns_none() {
assert_eq!(
derive_palace_id(Path::new("/"), Some(""), Some(" ")),
None
);
}
}