memnite_cli/
project_resolve.rs1use memnite_core::normalize_project;
7use std::path::{Path, PathBuf};
8
9#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum ProjectSource {
12 Explicit,
13 Config,
14 GitRemote,
15 GitRoot,
16 Cwd,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct ResolvedProject {
22 pub project: String,
23 pub source: ProjectSource,
24}
25
26pub fn resolve_project(cwd: &Path, explicit: Option<&str>) -> ResolvedProject {
31 if let Some(r) = explicit.and_then(|n| resolved_nonempty(n, ProjectSource::Explicit)) {
34 return r;
35 }
36 if let Some(r) = config_project(cwd).and_then(|n| resolved_nonempty(&n, ProjectSource::Config))
37 {
38 return r;
39 }
40 if let Some(git_root) = find_git_root(cwd) {
41 if let Some(r) = git_remote_origin(&git_root)
42 .and_then(|url| resolved_nonempty(&repo_name_from_url(&url), ProjectSource::GitRemote))
43 {
44 return r;
45 }
46 return resolved(&basename(&git_root), ProjectSource::GitRoot);
48 }
49 resolved(&basename(cwd), ProjectSource::Cwd)
50}
51
52fn resolved(raw: &str, source: ProjectSource) -> ResolvedProject {
53 ResolvedProject {
54 project: normalize_project(raw),
55 source,
56 }
57}
58
59fn resolved_nonempty(raw: &str, source: ProjectSource) -> Option<ResolvedProject> {
61 let r = resolved(raw, source);
62 (!r.project.is_empty()).then_some(r)
63}
64
65pub fn normalization_notice(raw: &str) -> Option<String> {
69 let canon = normalize_project(raw);
70 (canon != raw).then(|| format!("project '{raw}' normalized to '{canon}'"))
71}
72
73fn config_project(cwd: &Path) -> Option<String> {
75 for dir in cwd.ancestors() {
76 let cfg = dir.join(".memnite").join("config.json");
77 if cfg.is_file() {
78 let text = std::fs::read_to_string(&cfg).ok()?;
79 let value: serde_json::Value = serde_json::from_str(&text).ok()?;
80 return value
81 .get("project_name")
82 .and_then(|v| v.as_str())
83 .map(str::to_string);
84 }
85 }
86 None
87}
88
89fn find_git_root(cwd: &Path) -> Option<PathBuf> {
91 cwd.ancestors()
92 .find(|dir| dir.join(".git").exists())
93 .map(Path::to_path_buf)
94}
95
96fn git_remote_origin(git_root: &Path) -> Option<String> {
98 let text = std::fs::read_to_string(git_root.join(".git").join("config")).ok()?;
99 let mut in_origin = false;
100 for line in text.lines() {
101 let t = line.trim();
102 if t.starts_with('[') {
103 in_origin = t == "[remote \"origin\"]";
104 } else if in_origin {
105 if let Some(rest) = t.strip_prefix("url") {
106 if let Some(url) = rest.trim_start().strip_prefix('=') {
107 return Some(url.trim().to_string());
108 }
109 }
110 }
111 }
112 None
113}
114
115fn repo_name_from_url(url: &str) -> String {
118 let url = url.trim_end_matches('/');
119 let last = url.rsplit(['/', ':']).next().unwrap_or(url);
120 last.strip_suffix(".git").unwrap_or(last).to_string()
121}
122
123fn basename(path: &Path) -> String {
124 path.file_name()
125 .and_then(|s| s.to_str())
126 .unwrap_or("")
127 .to_string()
128}