Skip to main content

quanttide_devops/stage/
release.rs

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