1use std::collections::HashMap;
2use std::fmt::Debug;
3use std::hash::Hash;
4use std::path::Path;
5
6use itertools::Itertools;
7use tokio::{fs, io::AsyncWriteExt};
8
9use crate::bump::PackageBump;
10use crate::changeset::Changeset;
11use crate::semantic::Semantic;
12use crate::version::{Version, VersionMod, Versioned};
13
14fn capitalize(s: &str) -> String {
15 let mut c = s.chars();
16 match c.next() {
17 None => String::new(),
18 Some(f) => f.to_uppercase().chain(c).collect(),
19 }
20}
21
22fn fill_output<V: AsChangelogFmt + Versioned + Ord>(
23 next_version: &Version<V>,
24 patches: &HashMap<VersionMod<V>, Vec<String>>,
25) -> String {
26 let mut output = String::new();
27
28 output.push_str(&next_version.as_changelog_fmt());
29
30 for (version, changes) in patches.iter().sorted_by(|(a, _), (b, _)| Ord::cmp(&b, &a)) {
31 output.push('\n');
32 output.push_str(&version.as_changelog_fmt());
33 output.push('\n');
34
35 output.push_str(&changes.join("\n"));
36 }
37
38 output
39}
40
41fn create_patches<V>(
42 package_name: &str,
43 changesets: Vec<&Changeset<V>>,
44) -> HashMap<VersionMod<V>, Vec<String>>
45where
46 V: AsChangelogFmt + Clone + Hash + Ord + Versioned,
47{
48 let mut patches: HashMap<VersionMod<V>, Vec<String>> = HashMap::new();
49
50 for changset in changesets {
51 let changeset_summary = changset.as_changelog_fmt();
52
53 if let Some(version) = changset.packages.get(package_name) {
54 if let Some(changes) = patches.get_mut(version) {
55 changes.push(changeset_summary);
56 } else {
57 patches.insert(version.clone(), vec![changeset_summary]);
58 }
59 }
60 }
61
62 patches
63}
64
65pub struct Changelog;
66
67impl Changelog {
68 pub async fn update_changelog<T, V>(
69 changelog_path: T,
70 next_version: Version<V>,
71 package_bump: &PackageBump<'_, V>,
72 dry_run: bool,
73 ) -> std::io::Result<()>
74 where
75 T: AsRef<Path> + Debug,
76 V: AsChangelogFmt + Clone + Hash + Ord + Versioned,
77 {
78 let package_name = package_bump.name();
79
80 if let Some(patches) = package_bump
81 .changesets()
82 .map(|changesets| create_patches(package_name, changesets))
83 {
84 if dry_run {
85 println!(
86 "dry_run - update changelog {:?}\n{}",
87 changelog_path,
88 fill_output(&next_version, &patches)
89 .split('\n')
90 .map(|val| format!("dry_run: + {}", val))
91 .join("\n")
92 );
93 } else {
94 let changelog = fs::read_to_string(&changelog_path)
95 .await
96 .unwrap_or_else(|_| format!("# {}\n", package_name));
97
98 let mut changelog_lines = changelog.split('\n');
99
100 if let Some(title) = changelog_lines.next() {
101 let mut output = String::new();
102
103 output.push_str(title);
104 output.push('\n');
105 output.push('\n');
106
107 output.push_str(&fill_output(&next_version, &patches));
108
109 let mut changelog = fs::File::create(&changelog_path).await?;
110
111 changelog.write(output.as_bytes()).await?;
112
113 changelog
114 .write(changelog_lines.join("\n").as_bytes())
115 .await?;
116 }
117 }
118 }
119
120 Ok(())
121 }
122}
123
124pub trait AsChangelogFmt: Sized {
125 fn as_changelog_fmt(&self) -> String;
126}
127
128impl<T> AsChangelogFmt for Changeset<T> {
129 fn as_changelog_fmt(&self) -> String {
130 let mut changeset_summary = String::new();
131
132 let mut parts = self.message.split('\n');
133
134 if let Some(value) = parts.next() {
135 changeset_summary.push_str("- ");
136 changeset_summary.push_str(value);
137 changeset_summary.push('\n');
138
139 for part in parts {
140 changeset_summary.push_str(" ");
141 changeset_summary.push_str(part);
142 changeset_summary.push('\n');
143 }
144 }
145
146 changeset_summary
147 }
148}
149
150impl AsChangelogFmt for Semantic {
151 fn as_changelog_fmt(&self) -> String {
152 capitalize(&self.to_string())
153 }
154}
155
156impl<T: AsChangelogFmt> AsChangelogFmt for VersionMod<T> {
157 fn as_changelog_fmt(&self) -> String {
158 format!("### {} Changes\n", self.version.as_changelog_fmt())
159 }
160}
161
162impl<T> AsChangelogFmt for Version<T> {
163 fn as_changelog_fmt(&self) -> String {
164 format!("## {}\n", self.value)
165 }
166}