1use std::path::Path;
4
5use gitcc_changelog::{Changelog, Release, Section};
6use gitcc_git::{discover_repo, get_origin_url};
7use indexmap::{indexmap, IndexMap};
8use itertools::Itertools;
9use serde::{Deserialize, Serialize};
10use time::OffsetDateTime;
11
12use crate::{Commit, CommitHistory, Config, Error};
13
14#[derive(Debug, Serialize, Deserialize)]
16pub struct ChangelogConfig {
17 pub sections: IndexMap<String, Vec<String>>,
25}
26
27impl Default for ChangelogConfig {
28 fn default() -> Self {
29 let sections = indexmap! {
30 "New features".to_string() => vec!["feat".to_string()],
31 "Bug fixes".to_string() => vec!["fix".to_string()],
32 "Documentation".to_string() => vec!["docs".to_string()],
33 "Performance improvements".to_string() => vec!["perf".to_string()],
34 "Tooling".to_string() => vec![
35 "build".to_string(),
36 "ci".to_string(),
37 "cd".to_string()
38 ],
39 };
40 Self { sections }
41 }
42}
43
44impl ChangelogConfig {
45 fn find_section_for_commit_type(&self, r#type: &str) -> Option<String> {
47 for (section_label, commit_types) in &self.sections {
48 if commit_types.contains(&r#type.to_string()) {
49 return Some(section_label.clone());
50 }
51 }
52 None
53 }
54}
55
56#[derive(Debug, Clone, Default)]
58pub struct ChangelogBuildOptions {
59 pub origin_name: Option<String>,
61 pub all: bool,
63 pub next_version: Option<String>,
65}
66
67pub fn build_changelog(
72 cwd: &Path,
73 cfg: &Config,
74 history: &CommitHistory,
75 opts: Option<ChangelogBuildOptions>,
76) -> Result<Changelog, Error> {
77 let opts = opts.unwrap_or_default();
78 let repo = discover_repo(cwd)?;
79
80 let origin_name = opts.origin_name.unwrap_or("origin".to_owned());
81 let origin_url = get_origin_url(&repo, &origin_name)?.ok_or(Error::msg(
82 format!("remote origin '{origin_name}' not found").as_str(),
83 ))?;
84
85 let mut releases = vec![];
86 for (release_tag, release_commits) in history
87 .commits
88 .iter()
89 .group_by(|c| c.version_tag.clone())
90 .into_iter()
91 {
92 let mut sections: IndexMap<String, Section> = IndexMap::new();
101 for (s_label, _) in &cfg.changelog.sections {
102 sections.insert(s_label.to_string(), Section::new(s_label));
103 }
104 const UNCATEGORIZED: &str = "Uncategorized"; const HIDDEN: &str = "__Hidden__"; sections.insert(UNCATEGORIZED.to_string(), Section::new(UNCATEGORIZED));
107 sections.insert(HIDDEN.to_string(), Section::new(HIDDEN));
108
109 for c in release_commits {
110 let c_sect_label = match &c.conv_message {
112 Some(m) => {
113 match cfg.changelog.find_section_for_commit_type(&m.r#type) {
115 Some(label) => {
116 label
118 }
119 None => {
120 HIDDEN.to_string()
122 }
123 }
124 }
125 None => UNCATEGORIZED.to_string(),
126 };
127
128 if c_sect_label == HIDDEN && !opts.all {
129 continue;
130 }
131
132 let section = sections.get_mut(&c_sect_label).unwrap();
133 section.items.push(commit_oneliner(&origin_url, c));
134 }
135
136 let sections: Vec<_> = sections
138 .into_iter()
139 .filter_map(|(_, v)| if v.items.is_empty() { None } else { Some(v) })
140 .collect();
141
142 let release_version = release_tag
143 .as_ref()
144 .map(|t| t.name.to_string())
145 .unwrap_or_else(|| {
146 if let Some(next_version) = &opts.next_version {
147 next_version.to_string()
148 } else {
149 "Unreleased".to_string()
150 }
151 });
152 let release_date = release_tag
153 .as_ref()
154 .map(|t| t.date)
155 .unwrap_or(OffsetDateTime::now_utc());
156 let release_url = release_tag
157 .as_ref()
158 .map(|t| build_release_url(&origin_url, &t.name));
159
160 let release = Release {
161 version: release_version,
162 date: release_date,
163 url: release_url,
164 sections,
165 };
166 releases.push(release);
167 }
168
169 Ok(Changelog { releases })
170}
171
172fn build_release_url(origin_url: &str, version: &str) -> String {
177 format!("{origin_url}/releases/tag/{version}")
178}
179
180fn commit_oneliner(origin_url: &str, commit: &Commit) -> String {
184 format!(
185 "{} [{}]({}/commit/{})",
186 commit.subject(),
187 commit.short_id(),
188 origin_url,
189 commit.id
190 )
191}
192
193#[cfg(test)]
194mod tests {
195 use crate::commit_history;
196
197 use super::*;
198
199 #[test]
200 fn test_changelog() {
201 let cwd = std::env::current_dir().unwrap();
202 let cfg = Config::load_from_fs(&cwd).unwrap().unwrap_or_default();
203 let history = commit_history(&cwd, &cfg).unwrap();
204 let _changelog = build_changelog(&cwd, &cfg, &history, None).unwrap();
205 }
209}