1use std::path::Path;
2
3use crate::contract;
4
5pub fn status(repo_path: &Path) {
7 let c = contract::load(repo_path);
8
9 println!("构建状态");
10 println!("{}", "-".repeat(50));
11
12 if c.scopes.is_empty() {
13 let lang = contract::detect_by_files(repo_path);
14 let root_scope = contract::Scope {
15 name: "(root)".into(),
16 dir: ".".into(),
17 language: lang.clone(),
18 framework: String::new(),
19 build_tool: contract::BuildTool::Unknown(String::new()),
20 registry: contract::Registry::None,
21 release: contract::StageRelease::default(),
22 test_threshold: None,
23 ci_workflow: None,
24 };
25 let vs = contract::version_status(repo_path, &root_scope);
26 let release = c.scope_release(&root_scope);
27 print_scope("(root)", repo_path, &lang, &vs, release, &c, None);
28 } else {
29 for scope in &c.scopes {
30 let scope_dir = repo_path.join(&scope.dir);
31 if !scope_dir.exists() {
32 println!(" [{}] ⚠ 目录不存在: {}", scope.name, scope.dir);
33 continue;
34 }
35 let lang = c.resolve_language(scope, &scope_dir);
36 let vs = contract::version_status(repo_path, scope);
37 let release = c.scope_release(scope);
38 print_scope(
39 &scope.name,
40 &scope_dir,
41 &lang,
42 &vs,
43 release,
44 &c,
45 scope.ci_workflow.as_deref(),
46 );
47 }
48 }
49
50 let dirty = is_working_tree_dirty(repo_path);
51 println!(
52 " {} {}",
53 "工作区".to_string(),
54 if dirty {
55 "⚠ 有未提交变更"
56 } else {
57 "✅ 干净"
58 }
59 );
60}
61
62fn print_scope(
63 name: &str,
64 dir: &Path,
65 lang: &contract::Language,
66 vs: &contract::VersionStatus,
67 release: &contract::StageRelease,
68 c: &contract::Contract,
69 ci_workflow: Option<&str>,
70) {
71 println!(" [{:<12}] {}", name, lang.as_str());
72 println!(" CI: {}", check_ci(name, ci_workflow));
73 println!(" build: {}", check_syntax(lang, dir));
74 match (&vs.tag_version, &vs.config_version) {
75 (Some(t), Some(_)) if vs.consistent => println!(" version: ✅ {}(一致)", t),
76 (Some(t), Some(_)) => println!(" version: ⚠ {}(配置不一致)", t),
77 (Some(t), None) => println!(" version: tag {}(无配置文件)", t),
78 (None, Some(_)) => println!(" version: 有配置版本(无 tag)"),
79 (None, None) => println!(" version: 暂无发布"),
80 }
81 for (fname, ver) in &vs.config_files {
82 match (ver, &vs.tag_version) {
83 (Some(v), Some(t)) if v == t => {
84 println!(" {:<15} {} ✅", format!("{}:", fname), v)
85 }
86 (Some(v), Some(_)) => println!(
87 " {:<15} {} ❌(期望 {})",
88 format!("{}:", fname),
89 v,
90 vs.tag_version.as_deref().unwrap_or("?")
91 ),
92 (Some(v), None) => println!(" {:<15} {}(无 tag)", format!("{}:", fname), v),
93 (None, _) => println!(" {:<15} (未找到版本字段)", format!("{}:", fname)),
94 }
95 }
96 println!(" registry: {:?}", c.platform.artifact_registry);
97 println!(" changelog: {}", release.changelog);
98}
99
100pub fn resolve_workflow(scope: &str, ci_workflow: Option<&str>) -> String {
102 match ci_workflow {
103 Some(w) => w.to_string(),
104 None => format!("build-{}", scope),
105 }
106}
107
108fn check_ci(scope: &str, ci_workflow: Option<&str>) -> String {
109 let workflow = resolve_workflow(scope, ci_workflow);
110 let output = match std::process::Command::new("gh")
111 .args([
112 "run",
113 "list",
114 "--limit",
115 "1",
116 "--workflow",
117 &workflow,
118 "--json",
119 "conclusion,displayTitle,headBranch,number",
120 ])
121 .output()
122 {
123 Ok(o) if o.status.success() => o.stdout,
124 Ok(_) => return "⚠ 无 CI 运行记录".into(),
125 Err(_) => return "⚠ gh CLI 未安装".into(),
126 };
127
128 let out = String::from_utf8_lossy(&output);
129 let conclusion = out
131 .split("\"conclusion\":")
132 .nth(1)
133 .and_then(|s| s.split('"').nth(1))
134 .unwrap_or("");
135 let title = out
136 .split("\"displayTitle\":")
137 .nth(1)
138 .and_then(|s| s.split('"').nth(1))
139 .unwrap_or("");
140 let branch = out
141 .split("\"headBranch\":")
142 .nth(1)
143 .and_then(|s| s.split('"').nth(1))
144 .unwrap_or("?");
145 let number: String = out
146 .split("\"number\":")
147 .nth(1)
148 .map(|s| s.chars().take_while(|c| c.is_ascii_digit()).collect())
149 .filter(|s: &String| !s.is_empty())
150 .unwrap_or_else(|| "?".into());
151
152 if conclusion.is_empty() {
153 return "⚠ 无 CI 运行记录".into();
154 }
155 match conclusion {
156 "success" => format!("✅ {} ({} #{})", title, branch, number),
157 "failure" => format!("❌ {} ({} #{})", title, branch, number),
158 "cancelled" => format!("🔶 {} 已取消", title),
159 s => format!("⏳ {} ({}) - {}", title, branch, s),
160 }
161}
162
163fn check_syntax(lang: &contract::Language, dir: &Path) -> String {
164 let (cmd, args, label) = match lang {
165 contract::Language::Rust => {
166 let mp = dir.join("Cargo.toml");
167 if !mp.exists() {
168 return "—".into();
169 }
170 let mp_s = mp.to_string_lossy().to_string();
171 (
172 "cargo",
173 vec!["check".into(), "--manifest-path".into(), mp_s],
174 "cargo check",
175 )
176 }
177 contract::Language::Python => {
178 if !dir.join("pyproject.toml").exists() {
179 return "—".into();
180 }
181 ("uv".into(), vec!["check".into()], "uv check")
182 }
183 contract::Language::Go => {
184 if !dir.join("go.mod").exists() {
185 return "—".into();
186 }
187 ("go".into(), vec!["vet".into(), "./...".into()], "go vet")
188 }
189 contract::Language::Dart => {
190 if !dir.join("pubspec.yaml").exists() {
191 return "—".into();
192 }
193 ("dart".into(), vec!["analyze".into()], "dart analyze")
194 }
195 contract::Language::TypeScript => {
196 if !dir.join("package.json").exists() {
197 return "—".into();
198 }
199 (
200 "npx".into(),
201 vec!["tsc".into(), "--noEmit".into()],
202 "tsc --noEmit",
203 )
204 }
205 contract::Language::Unknown(_) => return "⚠ 语言未知,跳过".into(),
206 };
207 match std::process::Command::new(&cmd)
208 .args(&args)
209 .current_dir(dir)
210 .output()
211 {
212 Ok(o) if o.status.success() => format!("✅ {} 通过", label),
213 Ok(_) => format!("❌ {} 失败", label),
214 Err(_) => format!("⚠ {} 未安装", cmd),
215 }
216}
217
218fn is_working_tree_dirty(repo_path: &Path) -> bool {
219 match std::process::Command::new("git")
220 .args(["status", "--porcelain"])
221 .current_dir(repo_path)
222 .output()
223 {
224 Ok(o) => !o.stdout.is_empty(),
225 Err(_) => false,
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_print_scope_all_ok() {
235 let d = tempfile::tempdir().unwrap();
236 let c = contract::load(d.path());
237 let vs = contract::VersionStatus {
238 tag_version: Some("0.1.0".into()),
239 config_version: Some("0.1.0".into()),
240 consistent: true,
241 config_files: vec![("Cargo.toml".into(), Some("0.1.0".into()))],
242 };
243 let release = contract::StageRelease::default();
244 print_scope(
245 "test",
246 d.path(),
247 &contract::Language::Rust,
248 &vs,
249 &release,
250 &c,
251 None,
252 );
253 }
254
255 #[test]
256 fn test_is_working_tree_dirty_empty_repo() {
257 let d = tempfile::tempdir().unwrap();
258 assert!(!is_working_tree_dirty(d.path()));
259 }
260
261 #[test]
262 fn test_resolve_workflow_default() {
263 assert_eq!(resolve_workflow("cli", None), "build-cli");
264 assert_eq!(resolve_workflow("studio", None), "build-studio");
265 }
266
267 #[test]
268 fn test_resolve_workflow_custom() {
269 assert_eq!(resolve_workflow("cli", Some("my-pipeline")), "my-pipeline");
270 assert_eq!(resolve_workflow("cli", Some("release-ci")), "release-ci");
271 }
272
273 #[test]
274 fn test_detect_no_contract_yaml() {
275 let d = tempfile::tempdir().unwrap();
276 let c = contract::load(d.path());
277 assert!(c.scopes.is_empty());
278 }
279}