algocline_app/service/
migrate.rs1use super::alc_toml::{alc_toml_path, save_alc_toml};
4use super::lockfile::lockfile_path;
5use super::AppService;
6
7impl AppService {
8 pub async fn migrate(&self, project_root: Option<String>) -> Result<String, String> {
9 let root = if let Some(s) = project_root.as_deref() {
12 std::path::PathBuf::from(s)
13 } else if let Ok(env) = std::env::var("ALC_PROJECT_ROOT") {
14 if !env.is_empty() {
15 std::path::PathBuf::from(env)
16 } else {
17 std::env::current_dir().map_err(|e| format!("Cannot determine cwd: {e}"))?
18 }
19 } else {
20 std::env::current_dir().map_err(|e| format!("Cannot determine cwd: {e}"))?
21 };
22
23 let lock_path = lockfile_path(&root);
24 if !lock_path.exists() {
25 return Ok(serde_json::json!({
26 "status": "nothing to migrate",
27 "reason": "alc.lock not found"
28 })
29 .to_string());
30 }
31
32 let content = std::fs::read_to_string(&lock_path)
33 .map_err(|e| format!("Failed to read alc.lock: {e}"))?;
34
35 let is_legacy = detect_legacy_format(&content);
40 if !is_legacy {
41 return Ok(serde_json::json!({
42 "status": "nothing to migrate",
43 "reason": "already new format or no local_dir entries"
44 })
45 .to_string());
46 }
47
48 let toml_path = alc_toml_path(&root);
49 if toml_path.exists() {
50 return Err(format!(
51 "alc.toml already exists at {}. Remove it first or migrate manually.",
52 toml_path.display()
53 ));
54 }
55
56 let mut doc: toml_edit::DocumentMut = "[packages]\n"
58 .parse()
59 .map_err(|e: toml_edit::TomlError| format!("Internal error: {e}"))?;
60
61 {
62 let mut current_name: Option<String> = None;
63 let mut current_path: Option<String> = None;
64 let mut in_local_dir = false;
65
66 let flush =
67 |doc: &mut toml_edit::DocumentMut, name: Option<String>, path: Option<String>| {
68 if let (Some(n), Some(p)) = (name, path) {
69 if let Some(tbl) = doc["packages"].as_table_mut() {
70 let mut inline = toml_edit::InlineTable::new();
71 inline.insert("path", p.as_str().into());
72 tbl.insert(
73 &n,
74 toml_edit::Item::Value(toml_edit::Value::InlineTable(inline)),
75 );
76 }
77 }
78 };
79
80 for line in content.lines() {
81 let trimmed = line.trim();
82 if trimmed == "[[package]]" {
83 if in_local_dir {
84 flush(&mut doc, current_name.take(), current_path.take());
85 }
86 current_name = None;
87 current_path = None;
88 in_local_dir = false;
89 } else if let Some(v) = trimmed.strip_prefix("name = ") {
90 current_name = Some(v.trim_matches('"').to_string());
91 } else if trimmed.contains("local_dir") {
92 in_local_dir = true;
93 } else if in_local_dir {
94 if let Some(v) = trimmed.strip_prefix("path = ") {
95 current_path = Some(v.trim_matches('"').to_string());
96 }
97 }
98 }
99 if in_local_dir {
100 flush(&mut doc, current_name, current_path);
101 }
102 }
103
104 save_alc_toml(&root, &doc)?;
106
107 let bak_path = lock_path.with_extension("lock.bak");
108 std::fs::rename(&lock_path, &bak_path)
109 .map_err(|e| format!("Failed to rename alc.lock to alc.lock.bak: {e}"))?;
110
111 let result = serde_json::json!({
112 "migrated": true,
113 "alc_toml": toml_path.display().to_string(),
114 "backup": bak_path.display().to_string(),
115 "note": "Run alc_update to generate new alc.lock"
116 });
117 Ok(result.to_string())
118 }
119}
120
121fn detect_legacy_format(content: &str) -> bool {
127 let parsed: toml::Value = match toml::from_str(content) {
128 Ok(v) => v,
129 Err(_) => return false,
130 };
131
132 let packages = match parsed.get("package").and_then(|v| v.as_array()) {
133 Some(arr) => arr,
134 None => return false,
135 };
136
137 for pkg in packages {
138 let tbl = match pkg.as_table() {
139 Some(t) => t,
140 None => continue,
141 };
142
143 if tbl.contains_key("linked_at") {
144 return true;
145 }
146
147 if let Some(source) = tbl.get("source").and_then(|s| s.as_table()) {
148 if source.get("type").and_then(|t| t.as_str()) == Some("local_dir") {
149 return true;
150 }
151 }
152 }
153
154 false
155}
156
157#[cfg(test)]
158mod tests {
159 use crate::service::test_support::make_app_service as make_service;
160
161 const LEGACY_LOCK: &str = r#"version = 1
162
163[[package]]
164name = "my_pkg"
165linked_at = "2024-01-01T00:00:00Z"
166
167[package.source]
168type = "local_dir"
169path = "/some/path/my_pkg"
170"#;
171
172 #[tokio::test]
173 async fn migrate_when_no_lock_returns_nothing_to_migrate() {
174 let tmp = tempfile::tempdir().unwrap();
175 let svc = make_service().await;
176 let result = svc
177 .migrate(Some(tmp.path().to_str().unwrap().to_string()))
178 .await
179 .unwrap();
180 assert!(result.contains("nothing to migrate"), "{result}");
181 assert!(result.contains("alc.lock not found"), "{result}");
182 }
183
184 #[tokio::test]
185 async fn migrate_new_format_lock_returns_nothing_to_migrate() {
186 let tmp = tempfile::tempdir().unwrap();
187 std::fs::write(
188 tmp.path().join("alc.lock"),
189 "version = 1\n\n[[package]]\nname = \"cot\"\n\n[package.source]\ntype = \"installed\"\n",
190 )
191 .unwrap();
192 let svc = make_service().await;
193 let result = svc
194 .migrate(Some(tmp.path().to_str().unwrap().to_string()))
195 .await
196 .unwrap();
197 assert!(result.contains("nothing to migrate"), "{result}");
198 }
199
200 #[tokio::test]
201 async fn migrate_legacy_lock_creates_alc_toml_and_backup() {
202 let tmp = tempfile::tempdir().unwrap();
203 std::fs::write(tmp.path().join("alc.lock"), LEGACY_LOCK).unwrap();
204 let svc = make_service().await;
205 let result = svc
206 .migrate(Some(tmp.path().to_str().unwrap().to_string()))
207 .await
208 .unwrap();
209 assert!(result.contains("\"migrated\":true"), "{result}");
210 assert!(tmp.path().join("alc.toml").exists());
211 assert!(tmp.path().join("alc.lock.bak").exists());
212 assert!(!tmp.path().join("alc.lock").exists());
213
214 let toml_content = std::fs::read_to_string(tmp.path().join("alc.toml")).unwrap();
215 assert!(toml_content.contains("[packages]"), "{toml_content}");
216 assert!(toml_content.contains("my_pkg"), "{toml_content}");
217 }
218
219 #[tokio::test]
220 async fn migrate_fails_if_alc_toml_already_exists() {
221 let tmp = tempfile::tempdir().unwrap();
222 std::fs::write(tmp.path().join("alc.lock"), LEGACY_LOCK).unwrap();
223 std::fs::write(tmp.path().join("alc.toml"), "[packages]\n").unwrap();
224 let svc = make_service().await;
225 let err = svc
226 .migrate(Some(tmp.path().to_str().unwrap().to_string()))
227 .await
228 .unwrap_err();
229 assert!(err.contains("already exists"), "{err}");
230 }
231}