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 threshold: 70.0,
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 version_status(repo_path: &Path, scope: &Scope) -> VersionStatus {
125 quanttide_devops::source::git::version_status(repo_path, scope).unwrap_or_else(|e| {
126 eprintln!(" ⚠ 版本状态检查失败: {}", e);
127 VersionStatus {
128 tag_version: None,
129 config_version: None,
130 consistent: false,
131 config_files: vec![],
132 }
133 })
134}
135
136pub fn status(repo_path: &Path) {
138 let mut stdout = std::io::stdout();
139 status_to(&mut stdout, repo_path).ok();
140}
141
142pub fn status_to(writer: &mut impl std::io::Write, repo_path: &Path) -> std::io::Result<()> {
144 let contract_path = repo_path.join(".quanttide/devops/contract.yaml");
145 let exists = contract_path.exists();
146
147 let c = load(repo_path);
148
149 writeln!(writer, "契约状态")?;
150 writeln!(writer, "{}", "-".repeat(40))?;
151
152 if exists {
153 writeln!(writer, " 配置文件: {}", contract_path.display())?;
154 writeln!(writer, " 状态: ✅ 已加载")?;
155 } else {
156 writeln!(writer, " 配置文件: 未找到,使用默认契约")?;
157 writeln!(writer, " 状态: ⚠ 默认配置")?;
158 }
159 writeln!(writer)?;
160
161 writeln!(writer, " Stages:")?;
163 let b = &c.stages.build;
164 writeln!(
165 writer,
166 " build: {}",
167 b.command.as_deref().unwrap_or("—")
168 )?;
169 let t = &c.stages.test;
170 writeln!(
171 writer,
172 " test: {}(阈值 {}%)",
173 t.command.as_deref().unwrap_or("—"),
174 t.threshold
175 )?;
176 let r = &c.stages.release;
177 writeln!(
178 writer,
179 " release: {}(pre_publish: {:?})",
180 r.changelog, r.pre_publish
181 )?;
182 writeln!(writer)?;
183
184 writeln!(writer, " Platform:")?;
186 writeln!(
187 writer,
188 " source_control: {:?}",
189 c.platform.source_control
190 )?;
191 writeln!(writer, " pipeline: {:?}", c.platform.pipeline)?;
192 writeln!(
193 writer,
194 " artifact_registry: {}",
195 c.platform.artifact_registry
196 )?;
197 writeln!(writer)?;
198
199 writeln!(writer, " Sources:")?;
201 writeln!(
202 writer,
203 " version: {:?} {:?}",
204 c.sources.version.source_type, c.sources.version.path
205 )?;
206 writeln!(writer)?;
207
208 writeln!(writer, " Scopes: {} 个", c.scopes.len())?;
210 if c.scopes.is_empty() {
211 writeln!(writer, " 未定义 scope")?;
212 } else {
213 for s in &c.scopes {
214 writeln!(
215 writer,
216 " {:<12} dir: {:<24} {} / {}",
217 s.name,
218 s.dir,
219 s.language.as_str(),
220 s.build_tool.as_str()
221 )?;
222 }
223 }
224 Ok(())
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_version_status_git_error_returns_fallback() {
233 let scope = Scope {
234 name: "test".into(),
235 dir: ".".into(),
236 language: Language::Rust,
237 framework: String::new(),
238 build_tool: BuildTool::Cargo,
239 registry: Registry::None,
240 release: StageRelease::default(),
241 test_threshold: None,
242 ci_workflow: None,
243 };
244 let vs = version_status(Path::new("/nonexistent"), &scope);
245 assert!(!vs.consistent);
246 assert_eq!(vs.tag_version, None);
247 assert_eq!(vs.config_version, None);
248 assert!(vs.config_files.is_empty());
249 }
250
251 #[test]
252 fn test_status_to_with_contract() {
253 let d = tempfile::tempdir().unwrap();
254 let contract_dir = d.path().join(".quanttide/devops");
256 std::fs::create_dir_all(&contract_dir).unwrap();
257 std::fs::write(
258 contract_dir.join("contract.yaml"),
259 "scopes:\n test:\n dir: .\n language: rust\n",
260 )
261 .unwrap();
262 let mut buf = Vec::new();
263 status_to(&mut buf, d.path()).unwrap();
264 let out = String::from_utf8_lossy(&buf);
265 assert!(out.contains("✅ 已加载"));
266 assert!(out.contains("test"));
267 assert!(out.contains("rust"));
268 }
269
270 #[test]
271 fn test_status_to_without_contract() {
272 let d = tempfile::tempdir().unwrap();
273 let mut buf = Vec::new();
274 status_to(&mut buf, d.path()).unwrap();
275 let out = String::from_utf8_lossy(&buf);
276 assert!(out.contains("⚠ 默认配置"));
277 assert!(out.contains("未定义 scope"));
278 }
279
280 #[test]
281 fn test_auto_detect_with_packages() {
282 let d = tempfile::tempdir().unwrap();
283 std::fs::create_dir_all(d.path().join("packages/foo")).unwrap();
285 std::fs::write(
286 d.path().join("packages/foo/Cargo.toml"),
287 "[package]\nname = \"foo\"\n",
288 )
289 .unwrap();
290 std::fs::create_dir_all(d.path().join("src/cli")).unwrap();
292 std::fs::write(
293 d.path().join("src/cli/Cargo.toml"),
294 "[package]\nname = \"cli\"\n",
295 )
296 .unwrap();
297
298 let c = auto_detect_contract(d.path());
299 assert_eq!(
301 c.scopes.len(),
302 2,
303 "应有 2 个 scope,得到 {}",
304 c.scopes.len()
305 );
306 let names: Vec<&str> = c.scopes.iter().map(|s| s.name.as_str()).collect();
307 assert!(names.contains(&"foo"), "应包含 foo: {:?}", names);
308 assert!(names.contains(&"cli"), "应包含 cli: {:?}", names);
309 }
310
311 #[test]
312 fn test_auto_detect_empty_repo() {
313 let d = tempfile::tempdir().unwrap();
314 let c = auto_detect_contract(d.path());
316 assert!(c.scopes.is_empty(), "空目录 scopes 应为空: {:?}", c.scopes);
318 }
319
320 #[test]
321 fn test_auto_detect_root_only() {
322 let d = tempfile::tempdir().unwrap();
323 std::fs::write(d.path().join("Cargo.toml"), "[package]\nname = \"root\"\n").unwrap();
325 let c = auto_detect_contract(d.path());
326 assert!(!c.scopes.is_empty(), "应有 root scope");
328 assert!(c.scopes.iter().any(|s| s.name == "(root)"), "应包含 (root)");
329 }
330}