1#![feature(btree_extract_if)]
2
3use core::str;
4use std::{
5 borrow::Cow,
6 fs::{File, OpenOptions},
7 io::{self, IsTerminal, Read, Write},
8 path::{Path, PathBuf},
9};
10
11use anyhow::bail;
12use changelog::{
13 de::parse_changelog,
14 ser::{serialize_changelog, serialize_release, OptionsRelease},
15};
16use config::{Cli, Commands, MapMessageToSection, New, Remove, Show, Validate};
17use generate::generate;
18use repository::{Fs, Repository};
19use utils::try_get_repo;
20
21#[macro_use]
22extern crate log;
23
24mod commit_parser;
25pub mod config;
26mod generate;
27mod git_provider;
28mod release;
29mod repository;
30mod utils;
31
32#[cfg(test)]
33mod test_res;
34
35#[cfg(test)]
36mod integration_test;
37
38fn get_changelog_path(path: Option<PathBuf>) -> PathBuf {
39 path.unwrap_or(PathBuf::from("CHANGELOG.md"))
40}
41
42fn read_file(path: &Path) -> anyhow::Result<String> {
43 let mut buf = String::new();
44
45 let mut from_stdin = !io::stdin().is_terminal();
46
47 if from_stdin {
48 io::stdin().read_to_string(&mut buf)?;
49
50 if buf.is_empty() {
51 info!("Read stdin because is was not a terminal, but it is empty. Fallback to file.");
52 from_stdin = false;
53 }
54 }
55
56 if !from_stdin {
57 let mut file = File::open(path)?;
58 file.read_to_string(&mut buf)?;
59 }
60
61 Ok(buf)
62}
63
64fn write_output(output: &str, path: &Path, stdout: bool) -> anyhow::Result<()> {
65 if stdout {
68 print!("{output}")
69 } else {
70 let mut file = File::options().truncate(true).write(true).open(path)?;
71 file.write_all(output.as_bytes())?;
72 }
73
74 Ok(())
75}
76
77#[inline]
78pub fn run(cli: Cli) -> anyhow::Result<()> {
79 let r = Fs;
80
81 run_generic(&r, cli)
82}
83
84fn run_generic<R: Repository>(r: &R, cli: Cli) -> anyhow::Result<()> {
85 debug!("is terminal: {}", io::stdin().is_terminal());
86 debug!("is terminal stdout: {}", io::stdout().is_terminal());
87
88 match cli.command {
89 Commands::Generate(mut options) => {
90 let path = get_changelog_path(options.file.clone());
91 let input = read_file(&path)?;
92 let changelog = parse_changelog(&input)?;
93 options.repo = try_get_repo(options.repo);
94
95 let output = generate(r, changelog, &options)?;
96
97 write_output(&output, &path, options.stdout)?;
98 }
99
100 Commands::Release(mut options) => {
101 let path = get_changelog_path(options.file.clone());
102 let input = read_file(&path)?;
103 let changelog = parse_changelog(&input)?;
104 options.repo = try_get_repo(options.repo);
105
106 let (version, output) = release::release(r, changelog, &options)?;
107
108 write_output(&output, &path, options.stdout)?;
109
110 eprintln!("New release {} successfully created.", version);
111 }
112
113 Commands::Validate(options) => {
114 let Validate {
115 file,
116 format,
117 map,
118 ast,
119 stdout,
120 } = options;
121
122 let path = get_changelog_path(file);
123 let input = read_file(&path)?;
124 let mut changelog = parse_changelog(&input)?;
125
126 debug!("changelog: {:?}", changelog);
127
128 if ast {
129 dbg!(&changelog);
130 }
131
132 if format {
133 let map = MapMessageToSection::try_new(map)?;
134 changelog.sanitize(&map.to_fmt_options());
135 let output = serialize_changelog(&changelog, &changelog::ser::Options::default());
136
137 write_output(&output, &path, stdout)?;
138 }
139
140 eprintln!("Changelog parsed with success!");
141 }
142
143 Commands::Show(options) => {
144 let Show { file, n, version } = options;
145
146 let path = get_changelog_path(file);
147 let input = read_file(&path)?;
148 let changelog = parse_changelog(&input)?;
149
150 debug!("changelog: {:?}", changelog);
151
152 let releases = if let Some(regex) = &version {
153 let mut res = Vec::new();
154
155 for release in changelog.releases() {
156 if regex.is_match(release.version()) {
157 res.push(Cow::Borrowed(release))
158 }
159 }
160 res
161 } else {
162 changelog
163 .nth_release(n)
164 .map(|e| e.release())
165 .into_iter()
166 .collect()
167 };
168
169 if releases.is_empty() {
170 bail!("No release found");
171 }
172
173 for (pos, release) in releases.iter().enumerate() {
174 debug!("show release: {:?}", release);
175 let mut output = String::new();
176 serialize_release(
177 &mut output,
178 release,
179 &OptionsRelease {
180 serialize_title: false,
181 },
182 );
183
184 print!("{}", output);
185 if pos != releases.len() - 1 {
186 println!();
187 }
188 }
189 }
190
191 Commands::New(options) => {
192 let New { path, force } = options;
193
194 let path = get_changelog_path(path);
195
196 if path.exists() && !force {
197 bail!("Path already exist. Delete it or use the --force option");
198 }
199
200 let changelog = include_str!("../res/CHANGELOG_DEFAULT.md");
201
202 let mut file = OpenOptions::new()
203 .create(true)
204 .truncate(true)
205 .write(true)
206 .open(path)?;
207
208 file.write_all(changelog.as_bytes())?;
209
210 println!("Changelog successfully created!");
211 }
212 Commands::Remove(options) => {
213 let Remove {
214 file,
215 stdout,
216 remove_id,
217 } = options;
218
219 let path = get_changelog_path(file);
220 let input = read_file(&path)?;
221 let mut changelog = parse_changelog(&input)?;
222
223 debug!("changelog: {:?}", changelog);
224
225 if let Some(regex) = &remove_id.version {
226 changelog
227 .releases
228 .retain(|_, v| !regex.is_match(v.version()));
229 } else {
230 match changelog.nth_release(remove_id.n.unwrap())?.owned() {
231 changelog::utils::NthRelease::Unreleased(_) => {
232 changelog.unreleased.take();
233 }
234 changelog::utils::NthRelease::Released(key, _) => {
235 changelog.releases.remove(&key);
236 }
237 }
238 }
239
240 changelog.sanitize(&changelog::fmt::Options::default());
241
242 let output = serialize_changelog(&changelog, &changelog::ser::Options::default());
243
244 write_output(&output, &path, stdout)?;
245 }
246 }
247
248 Ok(())
249}