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    /// 创建一个 `ReleaseStateBuilder`,用链式调用替代 6 参数构造。
64    ///
65    /// ```
66    /// use quanttide_devops::stage::release::{ReleaseState, ReleaseStatus};
67    ///
68    /// let s = ReleaseState::builder()
69    ///     .status(ReleaseStatus::Pending)
70    ///     .scope("cli")
71    ///     .scope_path("src/cli")
72    ///     .current_version("v1.2.3")
73    ///     .pending_commits(3)
74    ///     .version_consistent(true)
75    ///     .build();
76    /// assert_eq!(s.scope, "cli");
77    /// assert_eq!(s.changelog, "CHANGELOG.md");
78    /// ```
79    pub fn builder() -> ReleaseStateBuilder {
80        ReleaseStateBuilder::default()
81    }
82}
83
84// ═══════════════════════════════════════════════════════════════════════
85// Builder
86// ═══════════════════════════════════════════════════════════════════════
87
88/// [`ReleaseState`] 的构建器,替代多参数构造。
89#[derive(Debug)]
90pub struct ReleaseStateBuilder {
91    status: Option<ReleaseStatus>,
92    scope: Option<String>,
93    scope_path: Option<String>,
94    current_version: Option<String>,
95    pending_commits: usize,
96    version_consistent: Option<bool>,
97    changelog: String,
98}
99
100impl Default for ReleaseStateBuilder {
101    fn default() -> Self {
102        Self {
103            status: None,
104            scope: None,
105            scope_path: None,
106            current_version: None,
107            pending_commits: 0,
108            version_consistent: None,
109            changelog: "CHANGELOG.md".into(),
110        }
111    }
112}
113
114impl ReleaseStateBuilder {
115    /// **必需**:发布生命周期状态。
116    pub fn status(mut self, value: ReleaseStatus) -> Self {
117        self.status = Some(value);
118        self
119    }
120
121    /// **必需**:scope 名称。
122    pub fn scope(mut self, value: impl Into<String>) -> Self {
123        self.scope = Some(value.into());
124        self
125    }
126
127    /// **必需**:scope 相对路径。
128    pub fn scope_path(mut self, value: impl Into<String>) -> Self {
129        self.scope_path = Some(value.into());
130        self
131    }
132
133    /// 当前最新 tag 版本号。
134    pub fn current_version(mut self, value: impl Into<String>) -> Self {
135        self.current_version = Some(value.into());
136        self
137    }
138
139    /// 自最新 tag 以来的未发布提交数(默认 0)。
140    pub fn pending_commits(mut self, value: usize) -> Self {
141        self.pending_commits = value;
142        self
143    }
144
145    /// 版本一致性检查结果。
146    pub fn version_consistent(mut self, value: bool) -> Self {
147        self.version_consistent = Some(value);
148        self
149    }
150
151    /// 变更日志路径(默认 `"CHANGELOG.md"`)。
152    pub fn changelog(mut self, value: impl Into<String>) -> Self {
153        self.changelog = value.into();
154        self
155    }
156
157    /// 构建 [`ReleaseState`]。
158    ///
159    /// # Panics
160    ///
161    /// 当 `status`、`scope` 或 `scope_path` 未设置时 panic。
162    pub fn build(self) -> ReleaseState {
163        ReleaseState {
164            status: self.status.expect("ReleaseStateBuilder: status 是必需的"),
165            scope: self.scope.expect("ReleaseStateBuilder: scope 是必需的"),
166            scope_path: self
167                .scope_path
168                .expect("ReleaseStateBuilder: scope_path 是必需的"),
169            current_version: self.current_version,
170            pending_commits: self.pending_commits,
171            changelog: self.changelog,
172            version_consistent: self.version_consistent,
173        }
174    }
175}
176
177/// 多行报告格式,与平台 `status_to` 输出一致:
178///
179/// ```text
180///   [scope]
181///     状态:         已是最新
182///     路径:         src/cli
183///     最新版本:     v1.2.3
184///     未发布提交:   0
185///     变更日志:     CHANGELOG.md
186///     版本一致:     是
187/// ```
188impl std::fmt::Display for ReleaseState {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        writeln!(f, "  [{}]", self.scope)?;
191        writeln!(f, "    状态:         {}", self.status)?;
192        writeln!(f, "    路径:         {}", self.scope_path)?;
193        match &self.current_version {
194            Some(v) => writeln!(f, "    最新版本:     {}", v)?,
195            None => writeln!(f, "    最新版本:     (无)")?,
196        }
197        writeln!(f, "    未发布提交:   {}", self.pending_commits)?;
198        writeln!(f, "    变更日志:     {}", self.changelog)?;
199        match self.version_consistent {
200            Some(true) => writeln!(f, "    版本一致:     是"),
201            Some(false) => writeln!(f, "    版本一致:     否"),
202            None => Ok(()),
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    // ── ReleaseStatus ──────────────────────────────────────────────
212
213    #[test]
214    fn test_release_status_variants() {
215        assert_ne!(ReleaseStatus::Unreleased, ReleaseStatus::Latest);
216        assert_ne!(ReleaseStatus::Latest, ReleaseStatus::Pending);
217        assert_ne!(ReleaseStatus::Pending, ReleaseStatus::Inconsistent);
218        assert_ne!(ReleaseStatus::Inconsistent, ReleaseStatus::Unknown);
219    }
220
221    #[test]
222    fn test_release_status_debug() {
223        let s = format!("{:?}", ReleaseStatus::Unreleased);
224        assert_eq!(s, "Unreleased");
225    }
226
227    #[test]
228    fn test_release_status_display() {
229        assert_eq!(format!("{}", ReleaseStatus::Unreleased), "未发布");
230        assert_eq!(format!("{}", ReleaseStatus::Latest), "已是最新");
231        assert_eq!(format!("{}", ReleaseStatus::Pending), "待发布");
232        assert_eq!(format!("{}", ReleaseStatus::Inconsistent), "版本冲突");
233        assert_eq!(format!("{}", ReleaseStatus::Unknown), "状态未知");
234    }
235
236    // ── ReleaseState ───────────────────────────────────────────────
237
238    #[test]
239    fn test_release_state_unreleased() {
240        let state = ReleaseState {
241            status: ReleaseStatus::Unreleased,
242            scope: "cli".into(),
243            scope_path: "src/cli".into(),
244            current_version: None,
245            pending_commits: 0,
246            changelog: "CHANGELOG.md".into(),
247            version_consistent: None,
248        };
249        assert_eq!(state.status, ReleaseStatus::Unreleased);
250        assert!(state.current_version.is_none());
251        assert_eq!(state.pending_commits, 0);
252    }
253
254    #[test]
255    fn test_release_state_pending() {
256        let state = ReleaseState {
257            status: ReleaseStatus::Pending,
258            scope: "core".into(),
259            scope_path: "packages/core".into(),
260            current_version: Some("v1.2.3".into()),
261            pending_commits: 5,
262            changelog: "CHANGELOG.md".into(),
263            version_consistent: Some(true),
264        };
265        assert_eq!(state.status, ReleaseStatus::Pending);
266        assert_eq!(state.current_version.as_deref(), Some("v1.2.3"));
267        assert_eq!(state.pending_commits, 5);
268        assert_eq!(state.version_consistent, Some(true));
269    }
270
271    #[test]
272    fn test_release_state_latest() {
273        let state = ReleaseState {
274            status: ReleaseStatus::Latest,
275            scope: "(root)".into(),
276            scope_path: ".".into(),
277            current_version: Some("v5.0.0".into()),
278            pending_commits: 0,
279            changelog: "CHANGELOG.md".into(),
280            version_consistent: Some(true),
281        };
282        assert_eq!(state.status, ReleaseStatus::Latest);
283        assert_eq!(state.pending_commits, 0);
284    }
285
286    #[test]
287    fn test_release_state_inconsistent() {
288        let state = ReleaseState {
289            status: ReleaseStatus::Inconsistent,
290            scope: "web".into(),
291            scope_path: "src/web".into(),
292            current_version: Some("v2.0.0".into()),
293            pending_commits: 3,
294            changelog: "CHANGELOG.md".into(),
295            version_consistent: Some(false),
296        };
297        assert_eq!(state.status, ReleaseStatus::Inconsistent);
298        assert_eq!(state.version_consistent, Some(false));
299    }
300
301    #[test]
302    fn test_release_state_unknown() {
303        let state = ReleaseState {
304            status: ReleaseStatus::Unknown,
305            scope: "service".into(),
306            scope_path: "apps/service".into(),
307            current_version: None,
308            pending_commits: 0,
309            changelog: "CHANGELOG.md".into(),
310            version_consistent: None,
311        };
312        assert_eq!(state.status, ReleaseStatus::Unknown);
313    }
314
315    #[test]
316    fn test_release_state_builder_minimal() {
317        let s = ReleaseState::builder()
318            .status(ReleaseStatus::Unreleased)
319            .scope("cli")
320            .scope_path("src/cli")
321            .build();
322        assert_eq!(s.scope, "cli");
323        assert_eq!(s.scope_path, "src/cli");
324        assert_eq!(s.status, ReleaseStatus::Unreleased);
325        assert!(s.current_version.is_none());
326        assert_eq!(s.pending_commits, 0);
327        assert_eq!(s.changelog, "CHANGELOG.md");
328        assert!(s.version_consistent.is_none());
329    }
330
331    #[test]
332    fn test_release_state_builder_full() {
333        let s = ReleaseState::builder()
334            .status(ReleaseStatus::Pending)
335            .scope("(root)")
336            .scope_path(".")
337            .current_version("v2.0.0")
338            .pending_commits(5)
339            .version_consistent(false)
340            .build();
341        assert_eq!(s.scope, "(root)");
342        assert_eq!(s.current_version.as_deref(), Some("v2.0.0"));
343        assert_eq!(s.pending_commits, 5);
344        assert_eq!(s.version_consistent, Some(false));
345    }
346
347    #[test]
348    fn test_release_state_display() {
349        let state = ReleaseState {
350            status: ReleaseStatus::Latest,
351            scope: "qtcloud-core".into(),
352            scope_path: "apps/qtcloud-core".into(),
353            current_version: Some("v2.1.0".into()),
354            pending_commits: 0,
355            changelog: "CHANGELOG.md".into(),
356            version_consistent: Some(true),
357        };
358        assert_eq!(
359            format!("{}", state),
360            "  [qtcloud-core]\n    状态:         已是最新\n    路径:         apps/qtcloud-core\n    最新版本:     v2.1.0\n    未发布提交:   0\n    变更日志:     CHANGELOG.md\n    版本一致:     是\n"
361        );
362
363        let state = ReleaseState {
364            status: ReleaseStatus::Unreleased,
365            scope: "(root)".into(),
366            scope_path: ".".into(),
367            current_version: None,
368            pending_commits: 0,
369            changelog: "CHANGELOG.md".into(),
370            version_consistent: None,
371        };
372        assert_eq!(
373            format!("{}", state),
374            "  [(root)]\n    状态:         未发布\n    路径:         .\n    最新版本:     (无)\n    未发布提交:   0\n    变更日志:     CHANGELOG.md\n"
375        );
376
377        let state = ReleaseState {
378            status: ReleaseStatus::Inconsistent,
379            scope: "web".into(),
380            scope_path: "src/web".into(),
381            current_version: Some("v2.0.0".into()),
382            pending_commits: 3,
383            changelog: "CHANGELOG.md".into(),
384            version_consistent: Some(false),
385        };
386        assert_eq!(
387            format!("{}", state),
388            "  [web]\n    状态:         版本冲突\n    路径:         src/web\n    最新版本:     v2.0.0\n    未发布提交:   3\n    变更日志:     CHANGELOG.md\n    版本一致:     否\n"
389        );
390    }
391}