release_plz_core/
changelog.rs

1use anyhow::Context;
2use chrono::{NaiveDate, TimeZone, Utc};
3use git_cliff_core::{
4    changelog::Changelog as GitCliffChangelog,
5    commit::Commit,
6    config::{Bump, ChangelogConfig, CommitParser, Config, GitConfig, RemoteConfig, TextProcessor},
7    contributor::RemoteContributor,
8    release::Release,
9};
10use regex::Regex;
11use serde::Serialize;
12use tracing::warn;
13
14use crate::changelog_parser;
15
16pub const CHANGELOG_HEADER: &str = r"# Changelog
17
18All notable changes to this project will be documented in this file.
19
20The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
21and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
22
23## [Unreleased]
24";
25
26pub const CHANGELOG_FILENAME: &str = "CHANGELOG.md";
27pub const RELEASE_LINK: &str = "release_link";
28pub const REMOTE: &str = "remote";
29
30#[derive(Debug)]
31pub struct Changelog<'a> {
32    release: Release<'a>,
33    config: Option<Config>,
34    release_link: Option<String>,
35    package: String,
36    remote: Option<Remote>,
37    pr_link: Option<String>,
38}
39
40#[derive(Debug, Serialize, Clone)]
41pub struct Remote {
42    /// Owner of the repo. E.g. `MarcoIeni`.
43    pub owner: String,
44    /// Name of the repo. E.g. `release-plz`.
45    pub repo: String,
46    /// HTTP URL to the repository.
47    /// E.g. `https://github.com/release-plz/release-plz`.
48    pub link: String,
49    /// List of contributors.
50    #[serde(skip_serializing_if = "Vec::is_empty")]
51    pub contributors: Vec<RemoteContributor>,
52}
53
54impl Changelog<'_> {
55    /// Generate the full changelog.
56    pub fn generate(self) -> anyhow::Result<String> {
57        let config = self.changelog_config(None);
58        let changelog = self.get_changelog(&config)?;
59        let mut out = Vec::new();
60        changelog
61            .generate(&mut out)
62            .context("cannot generate changelog")?;
63        String::from_utf8(out).context("cannot convert bytes to string")
64    }
65
66    /// Update an existing changelog.
67    pub fn prepend(self, old_changelog: impl Into<String>) -> anyhow::Result<String> {
68        let old_changelog: String = old_changelog.into();
69        if is_version_unchanged(&self.release) {
70            // The changelog already contains this version, so we don't update the changelog.
71            return Ok(old_changelog);
72        }
73        let old_header = changelog_parser::parse_header(&old_changelog);
74        let config = self.changelog_config(old_header.clone());
75        let changelog = self.get_changelog(&config)?;
76
77        // If we successfully parsed an old header, compose manually to preserve exact formatting
78        // and avoid potential header duplication.
79        if let Some(header) = old_header {
80            return compose_changelog(&old_changelog, &changelog, &header);
81        }
82
83        // Fallback: let git-cliff handle the prepend.
84        let mut out = Vec::new();
85        changelog
86            .prepend(old_changelog, &mut out)
87            .context("cannot update changelog")?;
88        String::from_utf8(out).context("cannot convert bytes to string")
89    }
90
91    fn get_changelog<'a>(
92        &'a self,
93        config: &'a Config,
94    ) -> Result<GitCliffChangelog<'a>, anyhow::Error> {
95        let mut changelog = GitCliffChangelog::new(vec![self.release.clone()], config, None)
96            .context("error while building changelog")?;
97        add_package_context(&mut changelog, &self.package)?;
98        add_release_link_context(&mut changelog, self.release_link.as_deref())?;
99        add_remote_context(&mut changelog, self.remote.as_ref())?;
100        Ok(changelog)
101    }
102
103    fn changelog_config(&self, header: Option<String>) -> Config {
104        let user_config = self.config.clone().unwrap_or(default_git_cliff_config());
105        Config {
106            changelog: apply_defaults_to_changelog_config(user_config.changelog, header),
107            git: apply_defaults_to_git_config(user_config.git, self.pr_link.as_deref()),
108            remote: user_config.remote,
109            bump: Bump::default(),
110        }
111    }
112}
113
114fn compose_changelog(
115    old_changelog: &str,
116    changelog: &GitCliffChangelog<'_>,
117    header: &str,
118) -> Result<String, anyhow::Error> {
119    let generated = {
120        let mut new_out = Vec::new();
121        changelog
122            .generate(&mut new_out)
123            .context("cannot generate updated changelog")?;
124        String::from_utf8(new_out).context("cannot convert bytes to string")?
125    };
126    // Parse the header so we can remove it later
127    let generated_header = crate::changelog_parser::parse_header(&generated);
128    let header_to_strip = if let Some(gen_h) = generated_header {
129        gen_h
130    } else {
131        header.to_string()
132    };
133    // Remove the header to get the changelog body
134    let generated_body = generated
135        .strip_prefix(&header_to_strip)
136        .unwrap_or(generated.as_str());
137    let old_body = old_changelog.strip_prefix(header).unwrap_or(old_changelog);
138    Ok(format!("{header}{generated_body}{old_body}"))
139}
140
141/// Apply release-plz defaults to git config
142fn apply_defaults_to_git_config(git_config: GitConfig, pr_link: Option<&str>) -> GitConfig {
143    let default_git_config = default_git_config(pr_link);
144
145    GitConfig {
146        conventional_commits: git_config.conventional_commits,
147        require_conventional: git_config.require_conventional,
148        filter_unconventional: git_config.filter_unconventional,
149        split_commits: git_config.split_commits,
150        commit_preprocessors: if git_config.commit_preprocessors.is_empty() {
151            default_git_config.commit_preprocessors
152        } else {
153            git_config.commit_preprocessors
154        },
155        commit_parsers: if git_config.commit_parsers.is_empty() {
156            default_git_config.commit_parsers
157        } else {
158            git_config.commit_parsers
159        },
160        protect_breaking_commits: git_config.protect_breaking_commits,
161        filter_commits: git_config.filter_commits,
162        tag_pattern: git_config.tag_pattern,
163        skip_tags: git_config.skip_tags,
164        ignore_tags: git_config.ignore_tags,
165        count_tags: git_config.count_tags,
166        use_branch_tags: git_config.use_branch_tags,
167        topo_order: git_config.topo_order,
168        topo_order_commits: git_config.topo_order_commits,
169        sort_commits: if git_config.sort_commits.is_empty() {
170            default_git_config.sort_commits
171        } else {
172            git_config.sort_commits
173        },
174        limit_commits: git_config.limit_commits,
175        recurse_submodules: git_config.recurse_submodules,
176        link_parsers: if git_config.link_parsers.is_empty() {
177            default_git_config.link_parsers
178        } else {
179            git_config.link_parsers
180        },
181        exclude_paths: git_config.exclude_paths,
182        include_paths: git_config.include_paths,
183    }
184}
185
186fn add_package_context(
187    changelog: &mut GitCliffChangelog,
188    package: &str,
189) -> Result<(), anyhow::Error> {
190    changelog
191        .add_context("package", package)
192        .with_context(|| format!("failed to add `{package}` to the `package` changelog context"))?;
193    Ok(())
194}
195
196fn add_release_link_context(
197    changelog: &mut GitCliffChangelog,
198    release_link: Option<&str>,
199) -> Result<(), anyhow::Error> {
200    if let Some(release_link) = release_link {
201        changelog
202            .add_context(RELEASE_LINK, release_link)
203            .with_context(|| {
204                format!(
205                    "failed to add `{release_link:?}` to the `{RELEASE_LINK}` changelog context"
206                )
207            })?;
208    }
209    Ok(())
210}
211
212fn add_remote_context(
213    changelog: &mut GitCliffChangelog,
214    remote: Option<&Remote>,
215) -> Result<(), anyhow::Error> {
216    if let Some(remote) = remote {
217        add_context(changelog, REMOTE, remote)?;
218    }
219    Ok(())
220}
221
222fn add_context(
223    changelog: &mut GitCliffChangelog,
224    key: &str,
225    value: impl serde::Serialize,
226) -> Result<(), anyhow::Error> {
227    let value_str = serde_json::to_string(&value).context("failed to serialize value")?;
228    changelog
229        .add_context(key, value)
230        .with_context(|| format!("failed to add `{value_str}` to the `{key}` changelog context"))
231}
232
233/// Apply release-plz defaults
234fn apply_defaults_to_changelog_config(
235    changelog: ChangelogConfig,
236    header: Option<String>,
237) -> ChangelogConfig {
238    let default_changelog_config = default_changelog_config(header);
239
240    ChangelogConfig {
241        header: changelog.header.or(default_changelog_config.header),
242        body: if changelog.body.is_empty() {
243            default_changelog_config.body
244        } else {
245            changelog.body
246        },
247        footer: changelog.footer.or(default_changelog_config.footer),
248        trim: changelog.trim,
249        render_always: changelog.render_always,
250        postprocessors: if changelog.postprocessors.is_empty() {
251            default_changelog_config.postprocessors
252        } else {
253            changelog.postprocessors
254        },
255        output: changelog.output.or(default_changelog_config.output),
256    }
257}
258
259fn is_version_unchanged(release: &Release) -> bool {
260    let previous_version = release.previous.as_ref().and_then(|r| r.version.as_deref());
261    let new_version = release.version.as_deref();
262    previous_version == new_version
263}
264
265fn default_git_cliff_config() -> Config {
266    Config {
267        changelog: default_changelog_config(None),
268        git: default_git_config(None),
269        remote: RemoteConfig::default(),
270        bump: Bump::default(),
271    }
272}
273
274#[derive(Debug, Clone)]
275pub struct ChangelogBuilder<'a> {
276    commits: Vec<Commit<'a>>,
277    version: String,
278    previous_version: Option<String>,
279    config: Option<Config>,
280    remote: Option<Remote>,
281    release_date: Option<NaiveDate>,
282    release_link: Option<String>,
283    package: String,
284    pr_link: Option<String>,
285}
286
287impl<'a> ChangelogBuilder<'a> {
288    pub fn new(
289        commits: Vec<Commit<'a>>,
290        version: impl Into<String>,
291        package: impl Into<String>,
292    ) -> Self {
293        Self {
294            commits,
295            version: version.into(),
296            previous_version: None,
297            config: None,
298            release_date: None,
299            remote: None,
300            release_link: None,
301            package: package.into(),
302            pr_link: None,
303        }
304    }
305
306    pub fn with_previous_version(self, previous_version: impl Into<String>) -> Self {
307        Self {
308            previous_version: Some(previous_version.into()),
309            ..self
310        }
311    }
312
313    pub fn with_pr_link(self, pr_link: impl Into<String>) -> Self {
314        Self {
315            pr_link: Some(pr_link.into()),
316            ..self
317        }
318    }
319
320    pub fn with_release_date(self, release_date: NaiveDate) -> Self {
321        Self {
322            release_date: Some(release_date),
323            ..self
324        }
325    }
326
327    pub fn with_release_link(self, release_link: impl Into<String>) -> Self {
328        Self {
329            release_link: Some(release_link.into()),
330            ..self
331        }
332    }
333
334    pub fn with_config(self, config: Config) -> Self {
335        Self {
336            config: Some(config),
337            ..self
338        }
339    }
340
341    pub fn with_remote(self, remote: Remote) -> Self {
342        Self {
343            remote: Some(remote),
344            ..self
345        }
346    }
347
348    pub fn config(&self) -> Option<&Config> {
349        self.config.as_ref()
350    }
351
352    pub fn build(&self) -> Changelog<'a> {
353        let git_config = self
354            .config
355            .clone()
356            .map(|c| c.git)
357            .unwrap_or_else(|| default_git_config(self.pr_link.as_deref()));
358        let release_date = self.release_timestamp();
359        let mut commits: Vec<_> = self
360            .commits
361            .iter()
362            .filter_map(|c| c.process(&git_config).ok())
363            .collect();
364
365        match git_config.sort_commits.to_lowercase().as_str() {
366            "oldest" => {
367                commits.reverse();
368            }
369            "newest" => {
370                // commits are already sorted from newest to oldest, we don't need to do anything
371            }
372            other => {
373                warn!(
374                    "Invalid setting for sort_commits: '{other}'. Valid values are 'newest' and 'oldest'."
375                );
376            }
377        }
378
379        let previous = self.previous_version.as_ref().map(|ver| Release {
380            version: Some(ver.clone()),
381            commits: vec![],
382            commit_id: None,
383            timestamp: Some(0),
384            previous: None,
385            message: None,
386            repository: None,
387            ..Default::default()
388        });
389
390        Changelog {
391            release: Release {
392                version: Some(self.version.clone()),
393                commits,
394                commit_id: None,
395                timestamp: Some(release_date),
396                previous: previous.map(Box::new),
397                message: None,
398                repository: None,
399                ..Default::default()
400            },
401            remote: self.remote.clone(),
402            release_link: self.release_link.clone(),
403            config: self.config.clone(),
404            package: self.package.clone(),
405            pr_link: self.pr_link.clone(),
406        }
407    }
408
409    /// Returns the provided release timestamp, if provided.
410    /// Current timestamp otherwise.
411    fn release_timestamp(&self) -> i64 {
412        self.release_date
413            .and_then(|date| date.and_hms_opt(0, 0, 0))
414            .map(|d| Utc.from_utc_datetime(&d))
415            .unwrap_or_else(Utc::now)
416            .timestamp()
417    }
418}
419
420pub fn default_git_config(pr_link: Option<&str>) -> GitConfig {
421    GitConfig {
422        conventional_commits: true,
423        filter_unconventional: false,
424        commit_parsers: kac_commit_parsers(),
425        filter_commits: false,
426        tag_pattern: None,
427        skip_tags: None,
428        split_commits: false,
429        protect_breaking_commits: false,
430        topo_order: false,
431        ignore_tags: None,
432        limit_commits: None,
433        sort_commits: "newest".to_string(),
434        commit_preprocessors: pr_link
435            .map(|pr_link| {
436                // Replace #123 with [#123](https://link_to_pr).
437                // If the number refers to an issue, GitHub redirects the PR link to the issue link.
438                vec![TextProcessor {
439                    pattern: Regex::new(r"\(#([0-9]+)\)").expect("invalid regex"),
440                    replace: Some(format!("([#${{1}}]({pr_link}/${{1}}))")),
441                    replace_command: None,
442                }]
443            })
444            .unwrap_or_default(),
445        link_parsers: vec![],
446        ..Default::default()
447    }
448}
449
450fn commit_parser(regex: &str, group: &str) -> CommitParser {
451    CommitParser {
452        message: Regex::new(regex).ok(),
453        body: None,
454        group: Some(group.to_string()),
455        default_scope: None,
456        scope: None,
457        skip: None,
458        field: None,
459        pattern: None,
460        sha: None,
461        footer: None,
462    }
463}
464
465/// Commit parsers based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
466fn kac_commit_parsers() -> Vec<CommitParser> {
467    vec![
468        commit_parser("^feat", "added"),
469        commit_parser("^changed", "changed"),
470        commit_parser("^deprecated", "deprecated"),
471        commit_parser("^removed", "removed"),
472        commit_parser("^fix", "fixed"),
473        commit_parser("^security", "security"),
474        commit_parser(".*", "other"),
475    ]
476}
477
478pub fn default_changelog_config(header: Option<String>) -> ChangelogConfig {
479    ChangelogConfig {
480        header: Some(header.unwrap_or(String::from(CHANGELOG_HEADER))),
481        body: default_changelog_body_config().to_string(),
482        footer: None,
483        postprocessors: vec![],
484        trim: true,
485        ..ChangelogConfig::default()
486    }
487}
488
489fn default_changelog_body_config() -> &'static str {
490    r#"
491## [{{ version }}]{%- if release_link -%}({{ release_link }}){% endif %} - {{ timestamp | date(format="%Y-%m-%d") }}
492{% for group, commits in commits | group_by(attribute="group") %}
493### {{ group | upper_first }}
494
495{% for commit in commits %}
496{%- if commit.scope -%}
497- *({{commit.scope}})* {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}{%- if commit.links %} ({% for link in commit.links %}[{{link.text}}]({{link.href}}) {% endfor -%}){% endif %}
498{% else -%}
499- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}
500{% endif -%}
501{% endfor -%}
502{% endfor %}"#
503}
504
505#[cfg(test)]
506mod tests {
507    use crate::NO_COMMIT_ID;
508
509    use super::*;
510
511    #[test]
512    fn changelog_entries_are_generated() {
513        let commits = vec![
514            Commit::new(NO_COMMIT_ID.to_string(), "fix: myfix".to_string()),
515            Commit::new(NO_COMMIT_ID.to_string(), "simple update".to_string()),
516        ];
517        let changelog = ChangelogBuilder::new(commits, "1.1.1", "my_pkg")
518            .with_release_date(NaiveDate::from_ymd_opt(2015, 5, 15).unwrap())
519            .build();
520
521        expect_test::expect![[r"
522            # Changelog
523
524            All notable changes to this project will be documented in this file.
525
526            The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
527            and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
528
529            ## [Unreleased]
530
531            ## [1.1.1] - 2015-05-15
532
533            ### Fixed
534
535            - myfix
536
537            ### Other
538
539            - simple update
540        "]]
541        .assert_eq(&changelog.generate().unwrap());
542    }
543
544    #[test]
545    fn changelog_entry_with_link_is_generated() {
546        let commits = vec![Commit::new(
547            NO_COMMIT_ID.to_string(),
548            "fix: myfix".to_string(),
549        )];
550        let changelog = ChangelogBuilder::new(commits, "1.1.1", "my_pkg")
551            .with_release_date(NaiveDate::from_ymd_opt(2015, 5, 15).unwrap())
552            .with_release_link("https://github.com/release-plz/release-plz/compare/release-plz-v0.2.24...release-plz-v0.2.25")
553            .build();
554
555        expect_test::expect![[r"
556            # Changelog
557
558            All notable changes to this project will be documented in this file.
559
560            The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
561            and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
562
563            ## [Unreleased]
564
565            ## [1.1.1](https://github.com/release-plz/release-plz/compare/release-plz-v0.2.24...release-plz-v0.2.25) - 2015-05-15
566
567            ### Fixed
568
569            - myfix
570        "]]
571        .assert_eq(&changelog.generate().unwrap());
572    }
573
574    #[test]
575    fn generated_changelog_is_updated_correctly() {
576        let commits = vec![
577            Commit::new(NO_COMMIT_ID.to_string(), "fix: myfix".to_string()),
578            Commit::new(NO_COMMIT_ID.to_string(), "simple update".to_string()),
579        ];
580        let changelog = ChangelogBuilder::new(commits, "1.1.1", "my_pkg")
581            .with_release_date(NaiveDate::from_ymd_opt(2015, 5, 15).unwrap())
582            .build();
583
584        let generated_changelog = changelog.generate().unwrap();
585
586        let commits = vec![
587            Commit::new(NO_COMMIT_ID.to_string(), "fix: myfix2".to_string()),
588            Commit::new(NO_COMMIT_ID.to_string(), "complex update".to_string()),
589        ];
590        let changelog = ChangelogBuilder::new(commits, "1.1.2", "my_pkg")
591            .with_release_date(NaiveDate::from_ymd_opt(2015, 5, 15).unwrap())
592            .build();
593
594        expect_test::expect![[r"
595            # Changelog
596
597            All notable changes to this project will be documented in this file.
598
599            The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
600            and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
601
602            ## [Unreleased]
603
604            ## [1.1.2] - 2015-05-15
605
606            ### Fixed
607
608            - myfix2
609
610            ### Other
611
612            - complex update
613
614            ## [1.1.1] - 2015-05-15
615
616            ### Fixed
617
618            - myfix
619
620            ### Other
621
622            - simple update
623        "]]
624        .assert_eq(&changelog.prepend(generated_changelog).unwrap());
625    }
626
627    #[test]
628    fn changelog_is_updated() {
629        let commits = vec![
630            Commit::new(NO_COMMIT_ID.to_string(), "fix: myfix".to_string()),
631            Commit::new(NO_COMMIT_ID.to_string(), "simple update".to_string()),
632        ];
633        let changelog = ChangelogBuilder::new(commits, "1.1.1", "my_pkg")
634            .with_release_date(NaiveDate::from_ymd_opt(2015, 5, 15).unwrap())
635            .build();
636        let old_body = r"## [1.1.0] - 1970-01-01
637
638### fix bugs
639
640- my awesomefix
641
642### other
643
644- complex update
645";
646        let old = format!("{CHANGELOG_HEADER}\n{old_body}");
647        let new = changelog.prepend(old).unwrap();
648        expect_test::expect![[r"
649            # Changelog
650
651            All notable changes to this project will be documented in this file.
652
653            The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
654            and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
655
656            ## [Unreleased]
657
658            ## [1.1.1] - 2015-05-15
659
660            ### Fixed
661
662            - myfix
663
664            ### Other
665
666            - simple update
667
668            ## [1.1.0] - 1970-01-01
669
670            ### fix bugs
671
672            - my awesomefix
673
674            ### other
675
676            - complex update
677        "]]
678        .assert_eq(&new);
679    }
680
681    #[test]
682    fn changelog_without_header_is_updated() {
683        let commits = vec![
684            Commit::new(NO_COMMIT_ID.to_string(), "fix: myfix".to_string()),
685            Commit::new(NO_COMMIT_ID.to_string(), "simple update".to_string()),
686        ];
687        let changelog = ChangelogBuilder::new(commits, "1.1.1", "my_pkg")
688            .with_release_date(NaiveDate::from_ymd_opt(2015, 5, 15).unwrap())
689            .build();
690        let old = r"
691## [1.1.0] - 1970-01-01
692
693### fix bugs
694
695- my awesomefix
696
697### other
698
699- complex update
700";
701        let new = changelog.prepend(old);
702        expect_test::expect![[r"
703            # Changelog
704
705            All notable changes to this project will be documented in this file.
706
707            The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
708            and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
709
710            ## [Unreleased]
711
712            ## [1.1.1] - 2015-05-15
713
714            ### Fixed
715
716            - myfix
717
718            ### Other
719
720            - simple update
721
722            ## [1.1.0] - 1970-01-01
723
724            ### fix bugs
725
726            - my awesomefix
727
728            ### other
729
730            - complex update
731        "]]
732        .assert_eq(&new.unwrap());
733    }
734
735    #[test]
736    fn changelog_has_commit_id() {
737        let commits = vec![
738            Commit::new("1111111".to_string(), "fix: myfix".to_string()),
739            Commit::new(
740                NO_COMMIT_ID.to_string(),
741                "chore: something else".to_string(),
742            ),
743        ];
744        let changelog = ChangelogBuilder::new(commits, "1.1.1", "my_pkg")
745            .with_release_date(NaiveDate::from_ymd_opt(2015, 5, 15).unwrap())
746            .with_config(Config {
747                changelog: ChangelogConfig {
748                    header: Some("# Changelog".to_string()),
749                    body: r"{%- for commit in commits %}
750                            {{ commit.message }} - {{ commit.id }}
751                        {% endfor -%}"
752                        .to_string(),
753                    ..default_changelog_config(None)
754                },
755                git: default_git_config(None),
756                remote: RemoteConfig::default(),
757                bump: Bump::default(),
758            })
759            .build();
760
761        expect_test::expect![[r"
762            # Changelog
763
764            myfix - 1111111
765
766            something else - 0000000
767        "]]
768        .assert_eq(&changelog.generate().unwrap());
769    }
770
771    #[test]
772    fn changelog_sort_newest() {
773        let commits = vec![
774            Commit::new("1111111".to_string(), "fix: myfix".to_string()),
775            Commit::new("0000000".to_string(), "fix: another fix".to_string()),
776        ];
777        let changelog = ChangelogBuilder::new(commits, "1.1.1", "my_pkg")
778            .with_release_date(NaiveDate::from_ymd_opt(2015, 5, 15).unwrap())
779            .with_config(Config {
780                changelog: default_changelog_config(None),
781                git: GitConfig {
782                    sort_commits: "oldest".to_string(),
783                    ..default_git_config(None)
784                },
785                remote: RemoteConfig::default(),
786                bump: Bump::default(),
787            })
788            .build();
789
790        expect_test::expect![[r"
791            # Changelog
792
793            All notable changes to this project will be documented in this file.
794
795            The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
796            and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
797
798            ## [Unreleased]
799
800            ## [1.1.1] - 2015-05-15
801
802            ### Fixed
803
804            - another fix
805            - myfix
806        "]]
807        .assert_eq(&changelog.generate().unwrap());
808    }
809}
810
811#[test]
812fn empty_changelog_is_updated() {
813    let commits = vec![
814        Commit::new(crate::NO_COMMIT_ID.to_string(), "fix: myfix".to_string()),
815        Commit::new(crate::NO_COMMIT_ID.to_string(), "simple update".to_string()),
816    ];
817    let changelog = ChangelogBuilder::new(commits, "1.1.1", "my_pkg")
818        .with_release_date(NaiveDate::from_ymd_opt(2015, 5, 15).unwrap())
819        .build();
820    let new = changelog.prepend(CHANGELOG_HEADER);
821    expect_test::expect![[r"
822        # Changelog
823
824        All notable changes to this project will be documented in this file.
825
826        The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
827        and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
828
829        ## [Unreleased]
830
831        ## [1.1.1] - 2015-05-15
832
833        ### Fixed
834
835        - myfix
836
837        ### Other
838
839        - simple update
840    "]]
841    .assert_eq(&new.unwrap());
842}