Skip to main content

changeset_operations/providers/
changelog.rs

1use 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}