Skip to main content

quanttide_devops/stage/
release.rs

1/// 发布阶段的状态枚举。
2///
3/// 描述一个 scope 当前所处的发布生命周期阶段。
4#[derive(Debug, Clone, PartialEq)]
5pub enum ReleaseStatus {
6    /// 从未发布过(无匹配的 git tag)。
7    Unreleased,
8    /// 已发布且为最新状态,无新的未发布变更。
9    Latest,
10    /// 有自上次发布以来的未发布提交。
11    Pending,
12    /// tag 与配置文件版本不一致。
13    Inconsistent,
14    /// 无法确定状态(如 git 命令失败)。
15    Unknown,
16}
17
18/// 返回状态的中文标签,用于命令行输出。
19///
20/// | 变体 | 标签 |
21/// |---|---|
22/// | `Unreleased` | 未发布 |
23/// | `Latest` | 已是最新 |
24/// | `Pending` | 待发布 |
25/// | `Inconsistent` | 版本冲突 |
26/// | `Unknown` | 状态未知 |
27impl std::fmt::Display for ReleaseStatus {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            Self::Unreleased => write!(f, "未发布"),
31            Self::Latest => write!(f, "已是最新"),
32            Self::Pending => write!(f, "待发布"),
33            Self::Inconsistent => write!(f, "版本冲突"),
34            Self::Unknown => write!(f, "状态未知"),
35        }
36    }
37}
38
39/// 发布阶段状态快照。
40///
41/// 记录一个 scope 在某个时刻的发布状态快照。
42/// 与 [`VersionState`] 的关系:`VersionState` 聚焦版本号一致性,
43/// `ReleaseState` 聚焦发布生命周期阶段。
44#[derive(Debug)]
45pub struct ReleaseState {
46    /// 发布生命周期状态。
47    pub status: ReleaseStatus,
48    /// scope 名称。
49    pub scope: String,
50    /// scope 相对路径。
51    pub scope_path: String,
52    /// 当前最新 tag 版本号(若有)。
53    pub current_version: Option<String>,
54    /// 自最新 tag 以来的未发布提交数。
55    pub pending_commits: usize,
56    /// 变更日志路径。
57    pub changelog: String,
58    /// 版本一致性检查结果(空表示未检查或不适用)。
59    pub version_consistent: Option<bool>,
60}
61
62impl ReleaseState {
63    /// 构造一个发布状态快照,变更日志默认使用 `"CHANGELOG.md"`。
64    ///
65    /// ```
66    /// use quanttide_devops::stage::release::{ReleaseState, ReleaseStatus};
67    ///
68    /// let s = ReleaseState::new(
69    ///     ReleaseStatus::Pending,
70    ///     "cli",
71    ///     "src/cli",
72    ///     Some("v1.2.3".into()),
73    ///     3,
74    ///     Some(true),
75    /// );
76    /// assert_eq!(s.scope, "cli");
77    /// assert_eq!(s.changelog, "CHANGELOG.md");
78    /// ```
79    pub fn new(
80        status: ReleaseStatus,
81        scope: impl Into<String>,
82        scope_path: impl Into<String>,
83        current_version: Option<String>,
84        pending_commits: usize,
85        version_consistent: Option<bool>,
86    ) -> Self {
87        Self {
88            status,
89            scope: scope.into(),
90            scope_path: scope_path.into(),
91            current_version,
92            pending_commits,
93            changelog: "CHANGELOG.md".into(),
94            version_consistent,
95        }
96    }
97}
98
99/// 多行报告格式,与平台 `status_to` 输出一致:
100///
101/// ```text
102///   [scope]
103///     状态:         已是最新
104///     路径:         src/cli
105///     最新版本:     v1.2.3
106///     未发布提交:   0
107///     变更日志:     CHANGELOG.md
108///     版本一致:     是
109/// ```
110impl std::fmt::Display for ReleaseState {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        writeln!(f, "  [{}]", self.scope)?;
113        writeln!(f, "    状态:         {}", self.status)?;
114        writeln!(f, "    路径:         {}", self.scope_path)?;
115        match &self.current_version {
116            Some(v) => writeln!(f, "    最新版本:     {}", v)?,
117            None => writeln!(f, "    最新版本:     (无)")?,
118        }
119        writeln!(f, "    未发布提交:   {}", self.pending_commits)?;
120        writeln!(f, "    变更日志:     {}", self.changelog)?;
121        match self.version_consistent {
122            Some(true) => writeln!(f, "    版本一致:     是"),
123            Some(false) => writeln!(f, "    版本一致:     否"),
124            None => Ok(()),
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    // ── ReleaseStatus ──────────────────────────────────────────────
134
135    #[test]
136    fn test_release_status_variants() {
137        assert_ne!(ReleaseStatus::Unreleased, ReleaseStatus::Latest);
138        assert_ne!(ReleaseStatus::Latest, ReleaseStatus::Pending);
139        assert_ne!(ReleaseStatus::Pending, ReleaseStatus::Inconsistent);
140        assert_ne!(ReleaseStatus::Inconsistent, ReleaseStatus::Unknown);
141    }
142
143    #[test]
144    fn test_release_status_debug() {
145        let s = format!("{:?}", ReleaseStatus::Unreleased);
146        assert_eq!(s, "Unreleased");
147    }
148
149    #[test]
150    fn test_release_status_display() {
151        assert_eq!(format!("{}", ReleaseStatus::Unreleased), "未发布");
152        assert_eq!(format!("{}", ReleaseStatus::Latest), "已是最新");
153        assert_eq!(format!("{}", ReleaseStatus::Pending), "待发布");
154        assert_eq!(format!("{}", ReleaseStatus::Inconsistent), "版本冲突");
155        assert_eq!(format!("{}", ReleaseStatus::Unknown), "状态未知");
156    }
157
158    // ── ReleaseState ───────────────────────────────────────────────
159
160    #[test]
161    fn test_release_state_unreleased() {
162        let state = ReleaseState {
163            status: ReleaseStatus::Unreleased,
164            scope: "cli".into(),
165            scope_path: "src/cli".into(),
166            current_version: None,
167            pending_commits: 0,
168            changelog: "CHANGELOG.md".into(),
169            version_consistent: None,
170        };
171        assert_eq!(state.status, ReleaseStatus::Unreleased);
172        assert!(state.current_version.is_none());
173        assert_eq!(state.pending_commits, 0);
174    }
175
176    #[test]
177    fn test_release_state_pending() {
178        let state = ReleaseState {
179            status: ReleaseStatus::Pending,
180            scope: "core".into(),
181            scope_path: "packages/core".into(),
182            current_version: Some("v1.2.3".into()),
183            pending_commits: 5,
184            changelog: "CHANGELOG.md".into(),
185            version_consistent: Some(true),
186        };
187        assert_eq!(state.status, ReleaseStatus::Pending);
188        assert_eq!(state.current_version.as_deref(), Some("v1.2.3"));
189        assert_eq!(state.pending_commits, 5);
190        assert_eq!(state.version_consistent, Some(true));
191    }
192
193    #[test]
194    fn test_release_state_latest() {
195        let state = ReleaseState {
196            status: ReleaseStatus::Latest,
197            scope: "(root)".into(),
198            scope_path: ".".into(),
199            current_version: Some("v5.0.0".into()),
200            pending_commits: 0,
201            changelog: "CHANGELOG.md".into(),
202            version_consistent: Some(true),
203        };
204        assert_eq!(state.status, ReleaseStatus::Latest);
205        assert_eq!(state.pending_commits, 0);
206    }
207
208    #[test]
209    fn test_release_state_inconsistent() {
210        let state = ReleaseState {
211            status: ReleaseStatus::Inconsistent,
212            scope: "web".into(),
213            scope_path: "src/web".into(),
214            current_version: Some("v2.0.0".into()),
215            pending_commits: 3,
216            changelog: "CHANGELOG.md".into(),
217            version_consistent: Some(false),
218        };
219        assert_eq!(state.status, ReleaseStatus::Inconsistent);
220        assert_eq!(state.version_consistent, Some(false));
221    }
222
223    #[test]
224    fn test_release_state_unknown() {
225        let state = ReleaseState {
226            status: ReleaseStatus::Unknown,
227            scope: "service".into(),
228            scope_path: "apps/service".into(),
229            current_version: None,
230            pending_commits: 0,
231            changelog: "CHANGELOG.md".into(),
232            version_consistent: None,
233        };
234        assert_eq!(state.status, ReleaseStatus::Unknown);
235    }
236
237    #[test]
238    fn test_release_state_new() {
239        let s = ReleaseState::new(ReleaseStatus::Unreleased, "cli", "src/cli", None, 0, None);
240        assert_eq!(s.scope, "cli");
241        assert_eq!(s.scope_path, "src/cli");
242        assert_eq!(s.status, ReleaseStatus::Unreleased);
243        assert!(s.current_version.is_none());
244        assert_eq!(s.pending_commits, 0);
245        assert_eq!(s.changelog, "CHANGELOG.md");
246        assert!(s.version_consistent.is_none());
247
248        let s = ReleaseState::new(
249            ReleaseStatus::Pending,
250            "(root)",
251            ".",
252            Some("v2.0.0".into()),
253            5,
254            Some(false),
255        );
256        assert_eq!(s.scope, "(root)");
257        assert_eq!(s.current_version.as_deref(), Some("v2.0.0"));
258        assert_eq!(s.pending_commits, 5);
259        assert_eq!(s.version_consistent, Some(false));
260    }
261
262    #[test]
263    fn test_release_state_display() {
264        let state = ReleaseState {
265            status: ReleaseStatus::Latest,
266            scope: "qtcloud-core".into(),
267            scope_path: "apps/qtcloud-core".into(),
268            current_version: Some("v2.1.0".into()),
269            pending_commits: 0,
270            changelog: "CHANGELOG.md".into(),
271            version_consistent: Some(true),
272        };
273        assert_eq!(
274            format!("{}", state),
275            "  [qtcloud-core]\n    状态:         已是最新\n    路径:         apps/qtcloud-core\n    最新版本:     v2.1.0\n    未发布提交:   0\n    变更日志:     CHANGELOG.md\n    版本一致:     是\n"
276        );
277
278        let state = ReleaseState {
279            status: ReleaseStatus::Unreleased,
280            scope: "(root)".into(),
281            scope_path: ".".into(),
282            current_version: None,
283            pending_commits: 0,
284            changelog: "CHANGELOG.md".into(),
285            version_consistent: None,
286        };
287        assert_eq!(
288            format!("{}", state),
289            "  [(root)]\n    状态:         未发布\n    路径:         .\n    最新版本:     (无)\n    未发布提交:   0\n    变更日志:     CHANGELOG.md\n"
290        );
291
292        let state = ReleaseState {
293            status: ReleaseStatus::Inconsistent,
294            scope: "web".into(),
295            scope_path: "src/web".into(),
296            current_version: Some("v2.0.0".into()),
297            pending_commits: 3,
298            changelog: "CHANGELOG.md".into(),
299            version_consistent: Some(false),
300        };
301        assert_eq!(
302            format!("{}", state),
303            "  [web]\n    状态:         版本冲突\n    路径:         src/web\n    最新版本:     v2.0.0\n    未发布提交:   3\n    变更日志:     CHANGELOG.md\n    版本一致:     否\n"
304        );
305    }
306}