quanttide_devops/source/
changelog.rs1use std::path::Path;
2
3#[derive(Debug)]
9pub enum ChangelogError {
10 Io(std::io::Error),
12 Parse(String),
14}
15
16impl std::fmt::Display for ChangelogError {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 Self::Io(e) => write!(f, "读取 CHANGELOG 失败: {}", e),
20 Self::Parse(e) => write!(f, "解析 CHANGELOG 失败: {}", e),
21 }
22 }
23}
24
25impl std::error::Error for ChangelogError {}
26
27impl From<std::io::Error> for ChangelogError {
28 fn from(e: std::io::Error) -> Self {
29 Self::Io(e)
30 }
31}
32
33#[derive(Debug)]
41pub struct Changelog {
42 #[allow(dead_code)]
43 raw: String,
45 inner: parse_changelog::Changelog<'static>,
52}
53
54impl Changelog {
55 pub fn from_path(path: &Path) -> Result<Self, ChangelogError> {
57 let raw = std::fs::read_to_string(path)?;
58 Self::from_str(&raw)
59 }
60
61 pub fn from_str(s: &str) -> Result<Self, ChangelogError> {
63 let raw = s.to_string();
64 let inner =
67 parse_changelog::parse(&raw).map_err(|e| ChangelogError::Parse(e.to_string()))?;
68 let inner: parse_changelog::Changelog<'static> = unsafe { std::mem::transmute(inner) };
70 Ok(Self { raw, inner })
71 }
72
73 pub fn release_notes<'a>(&'a self, version: &str) -> Option<&'a str> {
75 self.inner.get(version).map(|r| r.notes)
76 }
77
78 pub fn contains_version(&self, version: &str) -> bool {
80 self.inner.contains_key(version)
81 }
82
83 pub fn latest_version(&self) -> Option<&str> {
85 self.inner.keys().next().copied()
86 }
87
88 pub fn versions(&self) -> Vec<&str> {
90 self.inner.keys().copied().collect()
91 }
92}
93
94#[cfg(test)]
99mod tests {
100 use super::*;
101
102 fn sample_changelog() -> &'static str {
103 "\
104# Changelog
105
106## [0.1.2] - 2026-07-02
107
108### Changed
109- Refactored model into modules.
110
111## [0.1.1] - 2026-07-02
112
113### Added
114- Version utility functions.
115
116## [0.1.0] - 2026-07-02
117
118### Added
119- Initial release.
120"
121 }
122
123 #[test]
124 fn test_release_notes_existing() {
125 let cl = Changelog::from_str(sample_changelog()).unwrap();
126 let notes = cl.release_notes("0.1.1").unwrap();
127 assert!(notes.contains("Version utility functions"));
128 }
129
130 #[test]
131 fn test_release_notes_not_found() {
132 let cl = Changelog::from_str(sample_changelog()).unwrap();
133 assert!(cl.release_notes("9.9.9").is_none());
134 }
135
136 #[test]
137 fn test_contains_version() {
138 let cl = Changelog::from_str(sample_changelog()).unwrap();
139 assert!(cl.contains_version("0.1.0"));
140 assert!(!cl.contains_version("0.2.0"));
141 }
142
143 #[test]
144 fn test_latest_version() {
145 let cl = Changelog::from_str(sample_changelog()).unwrap();
146 assert_eq!(cl.latest_version(), Some("0.1.2"));
147 }
148
149 #[test]
150 fn test_versions() {
151 let cl = Changelog::from_str(sample_changelog()).unwrap();
152 assert_eq!(cl.versions(), vec!["0.1.2", "0.1.1", "0.1.0"]);
153 }
154
155 #[test]
156 fn test_empty_changelog() {
157 let cl = Changelog::from_str("");
158 assert!(cl.is_err());
159 assert!(cl.unwrap_err().to_string().contains("no release note"));
160 }
161
162 #[test]
163 fn test_from_path() {
164 let d = tempfile::tempdir().unwrap();
165 let path = d.path().join("CHANGELOG.md");
166 std::fs::write(&path, sample_changelog()).unwrap();
167 let cl = Changelog::from_path(&path).unwrap();
168 assert_eq!(cl.latest_version(), Some("0.1.2"));
169 }
170
171 #[test]
172 fn test_from_path_not_found() {
173 let cl = Changelog::from_path(Path::new("/nonexistent/CHANGELOG.md"));
174 assert!(cl.is_err());
175 }
176
177 #[test]
178 fn test_changelog_strips_v_prefix() {
179 let s = "\
180## v0.1.0 - 2026-01-01
181
182### Added
183- Something.
184";
185 let cl = Changelog::from_str(s).unwrap();
187 assert!(cl.contains_version("0.1.0"));
188 assert!(!cl.contains_version("v0.1.0"));
189 }
190
191 #[test]
192 fn test_scoped_pure_version() {
193 let s = "\
195## [0.1.0] - 2026-01-01
196
197### Added
198- CLI release.
199";
200 let cl = Changelog::from_str(s).unwrap();
201 assert!(cl.contains_version("0.1.0"));
202 assert!(!cl.contains_version("cli/0.1.0"));
204 }
205}