mol_core/
changelog.rs

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}