git_same/config/
workspace_policy.rs1use super::parser::Config;
4use super::workspace::tilde_collapse_path;
5use super::workspace::WorkspaceConfig;
6use super::workspace_store::WorkspaceStore;
7use crate::errors::AppError;
8use std::path::Path;
9
10pub struct WorkspacePolicy;
12
13impl WorkspacePolicy {
14 pub fn detect_from_cwd(start: &Path) -> Option<std::path::PathBuf> {
18 let mut current = start.to_path_buf();
19 loop {
20 let config = WorkspaceStore::config_path(¤t);
21 if config.exists() {
22 return Some(current);
23 }
24 if !current.pop() {
25 break;
26 }
27 }
28 None
29 }
30
31 pub fn resolve(name: Option<&str>, config: &Config) -> Result<WorkspaceConfig, AppError> {
40 if let Some(value) = name {
42 let expanded = shellexpand::tilde(value);
43 let root = Path::new(expanded.as_ref());
44 match WorkspaceStore::load(root) {
45 Ok(ws) => return Ok(ws),
46 Err(path_err) => {
47 let workspaces = WorkspaceStore::list()?;
48 match Self::resolve_selector_from_list(value, workspaces) {
49 Ok(ws) => return Ok(ws),
50 Err(selector_err) => {
51 let is_ambiguous = selector_err.to_string().contains("ambiguous");
52 if is_ambiguous {
53 return Err(selector_err);
54 }
55 if Self::looks_like_path(value) {
56 return Err(path_err);
57 }
58 return Err(AppError::config(format!(
59 "No workspace matched selector '{}'. Use 'gisa workspace list' and \
60 pass a workspace folder name or path.",
61 value
62 )));
63 }
64 }
65 }
66 }
67 }
68
69 if let Ok(cwd) = std::env::current_dir() {
71 if let Some(root) = Self::detect_from_cwd(&cwd) {
72 return WorkspaceStore::load(&root);
73 }
74 }
75
76 if let Some(ref default_path) = config.default_workspace {
78 let expanded = shellexpand::tilde(default_path);
79 let root = Path::new(expanded.as_ref());
80 return WorkspaceStore::load(root);
81 }
82
83 let workspaces = WorkspaceStore::list()?;
85 Self::resolve_from_list(workspaces)
86 }
87
88 pub fn resolve_from_list(
90 workspaces: Vec<WorkspaceConfig>,
91 ) -> Result<WorkspaceConfig, AppError> {
92 match workspaces.len() {
93 0 => Err(AppError::config(
94 "No workspaces configured. Run 'gisa setup' first.",
95 )),
96 1 => Ok(workspaces
97 .into_iter()
98 .next()
99 .expect("single workspace exists")),
100 _ => {
101 let labels: Vec<String> = workspaces.iter().map(|w| w.display_label()).collect();
102 Err(AppError::config(format!(
103 "Multiple workspaces configured. Use --workspace <path|name> to select one, \
104 or set a default with 'gisa workspace default <path|name>': {}",
105 labels.join(", ")
106 )))
107 }
108 }
109 }
110
111 fn resolve_selector_from_list(
112 selector: &str,
113 workspaces: Vec<WorkspaceConfig>,
114 ) -> Result<WorkspaceConfig, AppError> {
115 let matches: Vec<WorkspaceConfig> = workspaces
116 .into_iter()
117 .filter(|ws| {
118 let folder_name_matches = ws
119 .root_path
120 .file_name()
121 .and_then(|name| name.to_str())
122 .map(|name| name == selector)
123 .unwrap_or(false);
124 let path_matches = ws.root_path.to_string_lossy() == selector;
125 let tilde_path_matches = tilde_collapse_path(&ws.root_path) == selector;
126 folder_name_matches || path_matches || tilde_path_matches
127 })
128 .collect();
129
130 match matches.len() {
131 1 => Ok(matches
132 .into_iter()
133 .next()
134 .expect("single selector match exists")),
135 0 => Err(AppError::config("No workspace matched selector")),
136 _ => {
137 let labels: Vec<String> = matches.iter().map(|ws| ws.display_label()).collect();
138 Err(AppError::config(format!(
139 "Workspace selector '{}' is ambiguous. Use an explicit path instead: {}",
140 selector,
141 labels.join(", ")
142 )))
143 }
144 }
145 }
146
147 fn looks_like_path(value: &str) -> bool {
148 value.contains(std::path::MAIN_SEPARATOR)
149 || value.contains('/')
150 || value.starts_with('.')
151 || value.starts_with('~')
152 }
153}
154
155#[cfg(test)]
156#[path = "workspace_policy_tests.rs"]
157mod tests;