changeset_operations/providers/
changelog.rs1use std::path::Path;
2
3use changeset_changelog::{Changelog, RepositoryInfo, VersionRelease};
4
5use crate::Result;
6use crate::traits::{ChangelogWriteResult, ChangelogWriter};
7
8#[derive(Clone)]
9pub struct FileSystemChangelogWriter;
10
11impl FileSystemChangelogWriter {
12 #[must_use]
13 pub fn new() -> Self {
14 Self
15 }
16}
17
18impl Default for FileSystemChangelogWriter {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl ChangelogWriter for FileSystemChangelogWriter {
25 fn write_release(
26 &self,
27 changelog_path: &Path,
28 release: &VersionRelease,
29 repo_info: Option<&RepositoryInfo>,
30 previous_version: Option<&str>,
31 ) -> Result<ChangelogWriteResult> {
32 let created = !changelog_path.exists();
33
34 let mut changelog = if created {
35 Changelog::new()
36 } else {
37 Changelog::from_file(changelog_path)?
38 };
39
40 changelog.add_release(release, repo_info, previous_version);
41 changelog.write_to_file(changelog_path)?;
42
43 Ok(ChangelogWriteResult::new(
44 changelog_path.to_path_buf(),
45 created,
46 ))
47 }
48
49 fn changelog_exists(&self, path: &Path) -> bool {
50 path.exists()
51 }
52
53 fn restore_changelog(&self, path: &Path, content: &str) -> Result<()> {
54 std::fs::write(path, content).map_err(crate::OperationError::ChangesetFileWrite)
55 }
56
57 fn delete_changelog(&self, path: &Path) -> Result<()> {
58 if path.exists() {
59 std::fs::remove_file(path).map_err(crate::OperationError::ChangesetFileWrite)?;
60 }
61 Ok(())
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use chrono::NaiveDate;
68 use semver::Version;
69 use tempfile::TempDir;
70
71 use changeset_changelog::ChangelogEntry;
72 use changeset_core::ChangeCategory;
73
74 use super::*;
75
76 fn create_test_release() -> VersionRelease {
77 VersionRelease::new(
78 Version::new(1, 0, 0),
79 NaiveDate::from_ymd_opt(2025, 1, 15).expect("valid date"),
80 vec![ChangelogEntry::new(
81 ChangeCategory::Added,
82 "Initial release",
83 )],
84 )
85 }
86
87 #[test]
88 fn creates_new_changelog_when_missing() -> anyhow::Result<()> {
89 let dir = TempDir::new()?;
90 let changelog_path = dir.path().join("CHANGELOG.md");
91 let writer = FileSystemChangelogWriter::new();
92
93 let release = create_test_release();
94 let result = writer.write_release(&changelog_path, &release, None, None)?;
95
96 assert!(result.created());
97 assert!(changelog_path.exists());
98
99 let content = std::fs::read_to_string(&changelog_path)?;
100 assert!(content.contains("# Changelog"));
101 assert!(content.contains("## [1.0.0] - 2025-01-15"));
102 assert!(content.contains("- Initial release"));
103
104 Ok(())
105 }
106
107 #[test]
108 fn appends_to_existing_changelog() -> anyhow::Result<()> {
109 let dir = TempDir::new()?;
110 let changelog_path = dir.path().join("CHANGELOG.md");
111 let writer = FileSystemChangelogWriter::new();
112
113 let release1 = create_test_release();
114 writer.write_release(&changelog_path, &release1, None, None)?;
115
116 let release2 = VersionRelease::new(
117 Version::new(1, 1, 0),
118 NaiveDate::from_ymd_opt(2025, 2, 1).expect("valid date"),
119 vec![ChangelogEntry::new(ChangeCategory::Fixed, "Bug fix")],
120 );
121 let result = writer.write_release(&changelog_path, &release2, None, Some("1.0.0"))?;
122
123 assert!(!result.created());
124
125 let content = std::fs::read_to_string(&changelog_path)?;
126 assert!(content.contains("## [1.1.0] - 2025-02-01"));
127 assert!(content.contains("## [1.0.0] - 2025-01-15"));
128
129 Ok(())
130 }
131
132 #[test]
133 fn changelog_exists_returns_false_when_missing() {
134 let dir = TempDir::new().expect("create temp dir");
135 let changelog_path = dir.path().join("CHANGELOG.md");
136 let writer = FileSystemChangelogWriter::new();
137
138 assert!(!writer.changelog_exists(&changelog_path));
139 }
140
141 #[test]
142 fn changelog_exists_returns_true_when_present() -> anyhow::Result<()> {
143 let dir = TempDir::new()?;
144 let changelog_path = dir.path().join("CHANGELOG.md");
145 std::fs::write(&changelog_path, "# Changelog")?;
146 let writer = FileSystemChangelogWriter::new();
147
148 assert!(writer.changelog_exists(&changelog_path));
149
150 Ok(())
151 }
152
153 #[test]
154 fn adds_comparison_link_with_repo_info() -> anyhow::Result<()> {
155 let dir = TempDir::new()?;
156 let changelog_path = dir.path().join("CHANGELOG.md");
157 let writer = FileSystemChangelogWriter::new();
158
159 let release = VersionRelease::new(
160 Version::new(1, 1, 0),
161 NaiveDate::from_ymd_opt(2025, 2, 1).expect("valid date"),
162 vec![ChangelogEntry::new(ChangeCategory::Fixed, "Bug fix")],
163 );
164
165 let repo_info = RepositoryInfo::from_url("https://github.com/owner/repo")?;
166 writer.write_release(&changelog_path, &release, Some(&repo_info), Some("1.0.0"))?;
167
168 let content = std::fs::read_to_string(&changelog_path)?;
169 assert!(content.contains("[1.1.0]: https://github.com/owner/repo/compare/v1.0.0...v1.1.0"));
170
171 Ok(())
172 }
173}