1pub use quanttide_devops::contract::{
3 detect_language_by_files, normalize_version, read_all_config_versions, validate_version,
4 BuildTool, Contract, Language, Pipeline, Platform, Registry, Scope, SourceControl, SourceType,
5 Stage, StageBuild, StageRelease, StageTest, VersionSource,
6};
7pub use quanttide_devops::source::git::{GitSourceError, VersionStatus};
8
9use std::path::Path;
10
11pub fn load(repo_path: &Path) -> Contract {
16 match quanttide_devops::contract::load(repo_path) {
17 Ok(c) => c,
18 Err(_) => auto_detect_contract(repo_path),
19 }
20}
21
22fn auto_detect_contract(repo_path: &Path) -> Contract {
24 let root_lang = detect_language_by_files(repo_path);
25 let mut scopes: Vec<Scope> = Vec::new();
26
27 for base in &["src", "packages", "apps"] {
29 let base_dir = repo_path.join(base);
30 if !base_dir.is_dir() {
31 continue;
32 }
33 if let Ok(entries) = std::fs::read_dir(&base_dir) {
34 for entry in entries.flatten() {
35 let sub = entry.path();
36 if !sub.is_dir() {
37 continue;
38 }
39 let name = sub
40 .file_name()
41 .map(|n| n.to_string_lossy().to_string())
42 .unwrap_or_default();
43 let sub_lang = detect_language_by_files(&sub);
44 if matches!(sub_lang, Language::Unknown(_)) {
45 continue;
46 }
47 let dir = format!("{}/{}", base, name);
48 scopes.push(Scope {
49 name,
50 dir,
51 language: sub_lang.clone(),
52 framework: String::new(),
53 build_tool: infer_build_tool(&sub_lang),
54 registry: Registry::Crates,
55 release: StageRelease::default(),
56 test_threshold: None,
57 ci_workflow: None,
58 });
59 }
60 }
61 }
62
63 if !matches!(root_lang, Language::Unknown(_)) {
65 scopes.insert(
66 0,
67 Scope {
68 name: "(root)".into(),
69 dir: ".".into(),
70 language: root_lang.clone(),
71 framework: String::new(),
72 build_tool: infer_build_tool(&root_lang),
73 registry: Registry::Crates,
74 release: StageRelease::default(),
75 test_threshold: None,
76 ci_workflow: None,
77 },
78 );
79 }
80
81 Contract {
82 stages: Stage {
83 build: StageBuild {
84 command: Some("cargo build".into()),
85 },
86 test: StageTest {
87 command: Some("cargo test".into()),
88 ..StageTest::default()
89 },
90 release: StageRelease {
91 changelog: "CHANGELOG.md".into(),
92 pre_publish: Vec::new(),
93 },
94 },
95 scopes,
96 ..Contract::default()
97 }
98}
99
100fn infer_build_tool(lang: &Language) -> BuildTool {
101 match lang {
102 Language::Rust => BuildTool::Cargo,
103 Language::Python => BuildTool::Uv,
104 Language::Go => BuildTool::Go,
105 Language::Dart => BuildTool::Flutter,
106 Language::TypeScript => BuildTool::Npm,
107 Language::Unknown(_) => BuildTool::Unknown("auto".into()),
108 }
109}
110
111pub fn load_scopes(repo_path: &Path) -> Vec<Scope> {
112 load(repo_path).scopes
113}
114
115pub fn detect_by_files(dir: &Path) -> Language {
116 detect_language_by_files(dir)
117}
118
119pub fn detect_all_languages(dir: &Path) -> Vec<String> {
121 let mut langs = Vec::new();
122 if dir.join("Cargo.toml").exists() {
123 langs.push("rust".into());
124 }
125 if dir.join("pyproject.toml").exists() || dir.join("requirements.txt").exists() {
126 langs.push("python".into());
127 }
128 if dir.join("go.mod").exists() {
129 langs.push("go".into());
130 }
131 if dir.join("pubspec.yaml").exists() {
132 langs.push("dart".into());
133 }
134 if dir.join("package.json").exists() {
135 langs.push("typescript".into());
136 }
137 langs
138}
139
140pub fn version_status(repo_path: &Path, scope: &Scope) -> VersionStatus {
146 quanttide_devops::source::git::version_status(repo_path, scope).unwrap_or_else(|e| {
147 eprintln!(" ⚠ 版本状态检查失败: {}", e);
148 VersionStatus {
149 tag_version: None,
150 config_version: None,
151 consistent: false,
152 config_files: vec![],
153 }
154 })
155}
156
157pub fn status(repo_path: &Path) {
159 let mut stdout = std::io::stdout();
160 status_to(&mut stdout, repo_path).ok();
161}
162
163pub fn status_to(writer: &mut impl std::io::Write, repo_path: &Path) -> std::io::Result<()> {
165 let contract_path = repo_path.join(".quanttide/devops/contract.yaml");
166 let exists = contract_path.exists();
167
168 let c = load(repo_path);
169
170 writeln!(writer, "契约状态")?;
171 writeln!(writer, "{}", "-".repeat(40))?;
172
173 if exists {
174 writeln!(writer, " 配置文件: {}", contract_path.display())?;
175 writeln!(writer, " 状态: ✅ 已加载")?;
176 } else {
177 writeln!(writer, " 配置文件: 未找到,使用默认契约")?;
178 writeln!(writer, " 状态: ⚠ 默认配置")?;
179 }
180 writeln!(writer)?;
181
182 writeln!(writer, " Stages:")?;
184 let b = &c.stages.build;
185 writeln!(
186 writer,
187 " build: {}",
188 b.command.as_deref().unwrap_or("—")
189 )?;
190 let t = &c.stages.test;
191 writeln!(
192 writer,
193 " test: {}(阈值 {}%)",
194 t.command.as_deref().unwrap_or("—"),
195 t.threshold
196 )?;
197 let r = &c.stages.release;
198 writeln!(
199 writer,
200 " release: {}(pre_publish: {:?})",
201 r.changelog, r.pre_publish
202 )?;
203 writeln!(writer)?;
204
205 writeln!(writer, " Platform:")?;
207 writeln!(
208 writer,
209 " source_control: {:?}",
210 c.platform.source_control
211 )?;
212 writeln!(writer, " pipeline: {:?}", c.platform.pipeline)?;
213 writeln!(
214 writer,
215 " artifact_registry: {}",
216 c.platform.artifact_registry
217 )?;
218 writeln!(writer)?;
219
220 writeln!(writer, " Sources:")?;
222 writeln!(
223 writer,
224 " version: {:?} {:?}",
225 c.sources.version.source_type, c.sources.version.path
226 )?;
227 writeln!(writer)?;
228
229 writeln!(writer, " Scopes: {} 个", c.scopes.len())?;
231 if c.scopes.is_empty() {
232 writeln!(writer, " 未定义 scope")?;
233 } else {
234 for s in &c.scopes {
235 writeln!(
236 writer,
237 " {:<12} dir: {:<24} {} / {}",
238 s.name,
239 s.dir,
240 s.language.as_str(),
241 s.build_tool.as_str()
242 )?;
243 }
244 }
245
246 let mut langs: Vec<String> = Vec::new();
248 for s in &c.scopes {
249 let scope_dir = repo_path.join(&s.dir);
250 let mut scope_langs = detect_all_languages(&scope_dir);
251 langs.append(&mut scope_langs);
252 }
253 langs.sort();
254 langs.dedup();
255 if !langs.is_empty() {
256 writeln!(writer)?;
257 writeln!(writer, " 语言: {}", langs.join(", "))?;
258 }
259
260 Ok(())
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_version_status_git_error_returns_fallback() {
269 let scope = Scope {
270 name: "test".into(),
271 dir: ".".into(),
272 language: Language::Rust,
273 framework: String::new(),
274 build_tool: BuildTool::Cargo,
275 registry: Registry::None,
276 release: StageRelease::default(),
277 test_threshold: None,
278 ci_workflow: None,
279 };
280 let vs = version_status(Path::new("/nonexistent"), &scope);
281 assert!(!vs.consistent);
282 assert_eq!(vs.tag_version, None);
283 assert_eq!(vs.config_version, None);
284 assert!(vs.config_files.is_empty());
285 }
286
287 #[test]
288 fn test_status_to_with_contract() {
289 let d = tempfile::tempdir().unwrap();
290 let contract_dir = d.path().join(".quanttide/devops");
292 std::fs::create_dir_all(&contract_dir).unwrap();
293 std::fs::write(
294 contract_dir.join("contract.yaml"),
295 "scopes:\n test:\n dir: .\n language: rust\n",
296 )
297 .unwrap();
298 let mut buf = Vec::new();
299 status_to(&mut buf, d.path()).unwrap();
300 let out = String::from_utf8_lossy(&buf);
301 assert!(out.contains("✅ 已加载"));
302 assert!(out.contains("test"));
303 assert!(out.contains("rust"));
304 }
305
306 #[test]
307 fn test_status_to_without_contract() {
308 let d = tempfile::tempdir().unwrap();
309 let mut buf = Vec::new();
310 status_to(&mut buf, d.path()).unwrap();
311 let out = String::from_utf8_lossy(&buf);
312 assert!(out.contains("⚠ 默认配置"));
313 assert!(out.contains("未定义 scope"));
314 }
315
316 #[test]
317 fn test_detect_all_languages_empty() {
318 let d = tempfile::tempdir().unwrap();
319 let langs = detect_all_languages(d.path());
320 assert!(langs.is_empty(), "空目录应返回空");
321 }
322
323 #[test]
324 fn test_detect_all_languages_multi() {
325 let d = tempfile::tempdir().unwrap();
326 std::fs::write(d.path().join("Cargo.toml"), "").unwrap();
327 std::fs::write(d.path().join("pyproject.toml"), "").unwrap();
328 std::fs::write(d.path().join("package.json"), "{}").unwrap();
329 let mut langs = detect_all_languages(d.path());
330 langs.sort();
331 assert_eq!(langs, vec!["python", "rust", "typescript"]);
332 }
333
334 #[test]
335 fn test_detect_all_languages_edge() {
336 let d = tempfile::tempdir().unwrap();
337 std::fs::write(d.path().join("go.mod"), "").unwrap();
338 std::fs::write(d.path().join("pubspec.yaml"), "").unwrap();
339 std::fs::write(d.path().join("requirements.txt"), "").unwrap();
340 let mut langs = detect_all_languages(d.path());
341 langs.sort();
342 assert!(langs.contains(&"go".to_string()));
343 assert!(langs.contains(&"dart".to_string()));
344 assert!(langs.contains(&"python".to_string()));
345 }
346
347 #[test]
348 fn test_auto_detect_with_packages() {
349 let d = tempfile::tempdir().unwrap();
350 std::fs::create_dir_all(d.path().join("packages/foo")).unwrap();
351 std::fs::write(d.path().join("packages/foo/Cargo.toml"), "[package]\n").unwrap();
352 std::fs::create_dir_all(d.path().join("src/cli")).unwrap();
353 std::fs::write(d.path().join("src/cli/Cargo.toml"), "[package]\n").unwrap();
354 let c = auto_detect_contract(d.path());
355 assert_eq!(c.scopes.len(), 2, "应有 2 个 scope");
356 let names: Vec<&str> = c.scopes.iter().map(|s| s.name.as_str()).collect();
357 assert!(names.contains(&"foo"));
358 assert!(names.contains(&"cli"));
359 }
360
361 #[test]
362 fn test_auto_detect_empty_repo() {
363 let d = tempfile::tempdir().unwrap();
364 let c = auto_detect_contract(d.path());
365 assert!(c.scopes.is_empty(), "空目录 scopes 应为空");
366 }
367
368 #[test]
369 fn test_auto_detect_root_only() {
370 let d = tempfile::tempdir().unwrap();
371 std::fs::write(d.path().join("Cargo.toml"), "[package]\n").unwrap();
372 let c = auto_detect_contract(d.path());
373 assert!(!c.scopes.is_empty());
374 assert!(c.scopes.iter().any(|s| s.name == "(root)"));
375 }
376}