changelog_gen/
lib.rs

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    // !io::stdout().is_terminal()
66    // won't work on Github action because stdout is piped somehow.
67    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}