Skip to content

Commit

Permalink
feat(cli): changelog subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
justinrubek committed May 14, 2024
1 parent 9de5056 commit 2488a02
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 65 deletions.
145 changes: 80 additions & 65 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::cli::{BaseArgs, Bump, RawBump};
use crate::cli::{BaseArgs, Bump, Changelog, RawBump};
use bomper::{
changelog::generate_changelog_entry,
config::Config,
Expand All @@ -7,10 +7,7 @@ use bomper::{
cargo::CargoReplacer, file::FileReplacer, search::SearchReplacer, simple::SimpleReplacer,
Replacer, VersionReplacement,
},
versioning::{
determine_increment, get_commits_since_tag, get_latest_tag, increment_version,
VersionIncrement,
},
versioning::{get_commits_since_tag, get_latest_tag, increment_version},
};
use console::{style, Style};
use gix::refs::transaction::PreviousValue;
Expand All @@ -35,17 +32,7 @@ impl App {
let tag = get_latest_tag(&repo)?;
let commits = get_commits_since_tag(&repo, &tag)?;

let increment = match &opts.options.version {
Some(version) => VersionIncrement::Manual(semver::Version::parse(version)?),
None if opts.options.automatic => {
let conventional_commits = commits.iter().map(|c| c.as_ref());
determine_increment(conventional_commits, &tag.version)
}
None if opts.options.major => VersionIncrement::Major,
None if opts.options.minor => VersionIncrement::Minor,
None if opts.options.patch => VersionIncrement::Patch,
_ => unreachable!(),
};
let increment = opts.options.determine_increment(&commits, &tag.version)?;
let new_version = increment_version(tag.version.clone(), increment);
let changelog_entry = generate_changelog_entry(&commits, &new_version);

Expand All @@ -70,6 +57,30 @@ impl App {
Ok(())
}

pub fn changelog(&self, opts: &Changelog) -> Result<()> {
let repo = gix::discover(".")?;
let tag = get_latest_tag(&repo)?;
let commits = get_commits_since_tag(&repo, &tag)?;
let increment = opts.options.determine_increment(&commits, &tag.version)?;
let new_version = increment_version(tag.version.clone(), increment);
let changelog_entry = generate_changelog_entry(&commits, &new_version);
let path = std::path::PathBuf::from("CHANGELOG.md");
if opts.no_decorations {
match opts.only_current_version {
true => println!("{}", changelog_entry),
false => {
let new_changelog = create_changelog(&path, &changelog_entry)?;
println!("{}", new_changelog);
}
}
} else {
let old_changelog = std::fs::read_to_string(&path).unwrap_or_default();
let new_changelog = create_changelog(&path, &changelog_entry)?;
print_diff(old_changelog, new_changelog, path.display().to_string());
}
Ok(())
}

pub fn raw_bump(&self, opts: &RawBump) -> Result<()> {
let replacement = VersionReplacement {
old_version: opts.old_version.clone(),
Expand All @@ -86,59 +97,13 @@ impl App {
/// This function is responsible for respecting the `dry_run` flag, so it will only persist changes
/// if the flag is not set.
fn apply_changes(changes: Vec<FileReplacer>, args: &BaseArgs) -> Result<Option<Vec<PathBuf>>> {
struct Line(Option<usize>);

impl fmt::Display for Line {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.0 {
None => write!(f, " "),
Some(idx) => write!(f, "{:<4}", idx + 1),
}
}
}

if args.dry_run {
println!("Dry run, not persisting changes");
for replacer in changes {
let original = std::fs::read_to_string(&replacer.path)?;
let new = std::fs::read_to_string(&replacer.temp_file)?;

println!("{}", style(replacer.path.display()).cyan());
let (_, w) = console::Term::stdout().size();
// write `─` for the width of the terminal
println!("{:─^1$}", style("─").cyan(), w as usize);

let diff = TextDiff::from_lines(&original, &new);
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
if idx > 0 {
println!("{:-^1$}", "-", 80);
}
for op in group {
for change in diff.iter_inline_changes(op) {
let (sign, s) = match change.tag() {
ChangeTag::Delete => ("-", Style::new().red()),
ChangeTag::Insert => ("+", Style::new().green()),
ChangeTag::Equal => (" ", Style::new().dim()),
};
print!(
"{}{} |{}",
style(Line(change.old_index())).dim(),
style(Line(change.new_index())).dim(),
s.apply_to(sign).bold(),
);
for (emphasized, value) in change.iter_strings_lossy() {
if emphasized {
print!("{}", s.apply_to(value).underlined().on_black());
} else {
print!("{}", s.apply_to(value));
}
}
if change.missing_newline() {
println!();
}
}
}
}
print_diff(original, new, replacer.path.display().to_string())
}

Ok(None)
Expand Down Expand Up @@ -213,9 +178,9 @@ fn create_changelog(path: &std::path::Path, contents: &str) -> Result<String> {

let header = &original_changelog[..start];
let rest = &original_changelog[start..];
Ok(format!("{header}\n{MARKER}\n{contents}\n{rest}"))
Ok(format!("{header}{MARKER}\n\n{contents}\n{rest}"))
}
Ok(false) => Ok(format!("# Changelog\n\n{MARKER}\n{contents}")),
Ok(false) => Ok(format!("# Changelog\n\n{MARKER}\n\n{contents}\n\n{MARKER}\n\ngenerated by [bomper](https://github.com/justinrubek/bomper)")),
Err(e) => Err(e.into()),
}
}
Expand Down Expand Up @@ -289,3 +254,53 @@ fn rewrite_tree(
entries: new_entries,
})
}

fn print_diff(original: String, new: String, context: String) {
struct Line(Option<usize>);

impl fmt::Display for Line {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.0 {
None => write!(f, " "),
Some(idx) => write!(f, "{:<4}", idx + 1),
}
}
}

println!("\n{}", style(context).cyan());
let (_, w) = console::Term::stdout().size();
// write `─` for the width of the terminal
println!("{:─^1$}", style("─").cyan(), w as usize);

let diff = TextDiff::from_lines(&original, &new);
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
if idx > 0 {
println!("{:-^1$}", "-", 80);
}
for op in group {
for change in diff.iter_inline_changes(op) {
let (sign, s) = match change.tag() {
ChangeTag::Delete => ("-", Style::new().red()),
ChangeTag::Insert => ("+", Style::new().green()),
ChangeTag::Equal => (" ", Style::new().dim()),
};
print!(
"{}{} |{}",
style(Line(change.old_index())).dim(),
style(Line(change.new_index())).dim(),
s.apply_to(sign).bold(),
);
for (emphasized, value) in change.iter_strings_lossy() {
if emphasized {
print!("{}", s.apply_to(value).underlined().on_black());
} else {
print!("{}", s.apply_to(value));
}
}
if change.missing_newline() {
println!();
}
}
}
}
}
37 changes: 37 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use bomper::error::Result;
use bomper::versioning::{determine_increment, Commit, VersionIncrement};
use clap::Parser;
use std::path::PathBuf;

Expand Down Expand Up @@ -25,6 +27,8 @@ pub(crate) enum Commands {
RawBump(RawBump),
/// bump versions in files and commit the changes
Bump(Bump),
/// generate a changelog
Changelog(Changelog),
}

#[derive(clap::Args, Debug)]
Expand All @@ -39,6 +43,19 @@ pub(crate) struct Bump {
pub options: BumpOptions,
}

#[derive(clap::Args, Debug)]
pub(crate) struct Changelog {
#[clap(flatten)]
pub options: BumpOptions,
/// output the changelog in plain style, with no decorations.
#[arg(short, long)]
pub no_decorations: bool,
/// only include entries for commits since the last version.
/// only valid when `no_decorations` is set.
#[arg(short, long, requires = "no_decorations")]
pub only_current_version: bool,
}

#[derive(clap::Args, Debug)]
#[command(group = clap::ArgGroup::new("bump-type").required(true))]
pub(crate) struct BumpOptions {
Expand All @@ -53,3 +70,23 @@ pub(crate) struct BumpOptions {
#[arg(short, long, group = "bump-type")]
pub patch: bool,
}

impl BumpOptions {
pub(crate) fn determine_increment<'a, I: IntoIterator<Item = &'a Commit>>(
&self,
commits: I,
current_version: &semver::Version,
) -> Result<VersionIncrement> {
match &self.version {
Some(version) => Ok(VersionIncrement::Manual(semver::Version::parse(version)?)),
None if self.automatic => {
let conventional_commits = commits.into_iter().map(|c| c.as_ref());
Ok(determine_increment(conventional_commits, current_version))
}
None if self.major => Ok(VersionIncrement::Major),
None if self.minor => Ok(VersionIncrement::Minor),
None if self.patch => Ok(VersionIncrement::Patch),
_ => unreachable!(),
}
}
}
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

let app = App::new(args.base_args, config);
match args.command {
Commands::Changelog(changelog) => {
app.changelog(&changelog)?;
}
Commands::RawBump(raw_bump) => {
app.raw_bump(&raw_bump)?;
}
Expand Down

0 comments on commit 2488a02

Please sign in to comment.