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 = contract::scope_release(&c, &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 = contract::resolve_language(scope, &scope_dir);
36 let vs = contract::version_status(repo_path, scope);
37 let release = contract::scope_release(&c, 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.name());
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(cv)) if t == cv => println!(" version: ✅ {}(一致)", t),
76 (Some(t), Some(cv)) => println!(" version: ⚠ tag {} ≠ 配置 {}", t, cv),
77 (Some(t), None) => println!(" version: tag {}(无配置文件)", t),
78 (None, Some(cv)) => println!(" version: 配置 {}(无 tag)", cv),
79 (None, None) => println!(" version: 暂无发布"),
80 }
81 println!(" registry: {}", c.platforms.artifact_registry.name());
82 println!(" changelog: {}", release.changelog);
83}
84
85fn check_ci(scope: &str, ci_workflow: Option<&str>) -> String {
86 let workflow = ci_workflow.unwrap_or(scope);
88 let output = match std::process::Command::new("gh")
89 .args([
90 "run",
91 "list",
92 "--limit",
93 "1",
94 "--workflow",
95 workflow,
96 "--json",
97 "conclusion,displayTitle,headBranch,number",
98 ])
99 .output()
100 {
101 Ok(o) if o.status.success() => o.stdout,
102 Ok(_) => return "⚠ 无 CI 运行记录".into(),
103 Err(_) => return "⚠ gh CLI 未安装".into(),
104 };
105
106 let out = String::from_utf8_lossy(&output);
107 let conclusion = out
109 .split("\"conclusion\":")
110 .nth(1)
111 .and_then(|s| s.split('"').nth(1))
112 .unwrap_or("");
113 let title = out
114 .split("\"displayTitle\":")
115 .nth(1)
116 .and_then(|s| s.split('"').nth(1))
117 .unwrap_or("");
118 let branch = out
119 .split("\"headBranch\":")
120 .nth(1)
121 .and_then(|s| s.split('"').nth(1))
122 .unwrap_or("?");
123 let number = out
124 .split("\"number\":")
125 .nth(1)
126 .and_then(|s| s.split(',').next())
127 .unwrap_or("?");
128
129 if conclusion.is_empty() {
130 return "⚠ 无 CI 运行记录".into();
131 }
132 match conclusion {
133 "success" => format!("✅ {} ({} #{})", title, branch, number),
134 "failure" => format!("❌ {} ({} #{})", title, branch, number),
135 "cancelled" => format!("🔶 {} 已取消", title),
136 s => format!("⏳ {} ({}) - {}", title, branch, s),
137 }
138}
139
140fn check_syntax(lang: &contract::Language, dir: &Path) -> String {
141 let (cmd, args, label) = match lang {
142 contract::Language::Rust => {
143 let mp = dir.join("Cargo.toml");
144 if !mp.exists() {
145 return "—".into();
146 }
147 let mp_s = mp.to_string_lossy().to_string();
148 (
149 "cargo",
150 vec!["check".into(), "--manifest-path".into(), mp_s],
151 "cargo check",
152 )
153 }
154 contract::Language::Python => {
155 if !dir.join("pyproject.toml").exists() {
156 return "—".into();
157 }
158 ("uv".into(), vec!["check".into()], "uv check")
159 }
160 contract::Language::Go => {
161 if !dir.join("go.mod").exists() {
162 return "—".into();
163 }
164 ("go".into(), vec!["vet".into(), "./...".into()], "go vet")
165 }
166 contract::Language::Dart => {
167 if !dir.join("pubspec.yaml").exists() {
168 return "—".into();
169 }
170 ("dart".into(), vec!["analyze".into()], "dart analyze")
171 }
172 contract::Language::TypeScript => {
173 if !dir.join("package.json").exists() {
174 return "—".into();
175 }
176 (
177 "npx".into(),
178 vec!["tsc".into(), "--noEmit".into()],
179 "tsc --noEmit",
180 )
181 }
182 contract::Language::Unknown(_) => return "⚠ 语言未知,跳过".into(),
183 };
184 match std::process::Command::new(&cmd)
185 .args(&args)
186 .current_dir(dir)
187 .output()
188 {
189 Ok(o) if o.status.success() => format!("✅ {} 通过", label),
190 Ok(_) => format!("❌ {} 失败", label),
191 Err(_) => format!("⚠ {} 未安装", cmd),
192 }
193}
194
195fn is_working_tree_dirty(repo_path: &Path) -> bool {
196 match std::process::Command::new("git")
197 .args(["status", "--porcelain"])
198 .current_dir(repo_path)
199 .output()
200 {
201 Ok(o) => !o.stdout.is_empty(),
202 Err(_) => false,
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_print_scope_all_ok() {
212 let d = tempfile::tempdir().unwrap();
213 let c = contract::load(d.path());
214 let vs = contract::VersionStatus {
215 tag_version: Some("0.1.0".into()),
216 config_version: Some("0.1.0".into()),
217 consistent: true,
218 };
219 let release = contract::StageRelease::default();
220 print_scope(
221 "test",
222 d.path(),
223 &contract::Language::Rust,
224 &vs,
225 &release,
226 &c,
227 None,
228 );
229 }
230
231 #[test]
232 fn test_is_working_tree_dirty_empty_repo() {
233 let d = tempfile::tempdir().unwrap();
234 assert!(!is_working_tree_dirty(d.path()));
235 }
236
237 #[test]
238 fn test_detect_no_contract_yaml() {
239 let d = tempfile::tempdir().unwrap();
240 let c = contract::load(d.path());
241 assert!(c.scopes.is_empty());
242 }
243}