mirror of https://github.com/smithy-lang/smithy-rs
Add new subcommands to `changelogger` for supporting file-per-change changelog (#3771)
## Motivation and Context Adds new subcommands `--new` and `--ls` to `changelogger` ## Description This is part 2 in a series of PRs supporting [file-per-change changelog](https://smithy-lang.github.io/smithy-rs/design/rfcs/rfc0042_file_per_change_changelog.html). This PR just adds utilities for humans and does NOT impact the current dev workflow, smithy-rs CI, or our release pipeline. #### A subcommand `--new` We can use this subcommand when creating a new changelog entry markdown file. An example usage: ``` $ changelogger new -t client -t aws-sdk-rust -r smithy-rs#1234 -a someone --bug-fix -m "Some changelog" \ # for long flags, replace -t with --applies-to, -r with --references, -a with --authors, and -m with --message \ # also remember to escape with \ when including backticks in the message at command line, e.g. \`pub\` The following changelog entry has been written to "/Users/ysaito1001/src/smithy-rs/.changelog/5745197.md": --- applies_to: - aws-sdk-rust - client authors: - someone references: - smithy-rs#1234 breaking: false new_feature: false bug_fix: true --- Some changelog ``` The following CLI arguments are "logically" required - `--applies-to` - `--ref` - `--author` - `--message` If any of the above is not passed a value at command line, then an editor is opened for further edit (which editor to open can be configured per the [edit crate](https://docs.rs/edit/0.1.5/edit/)). Note that the YAML syntax above is different from the single line array syntax [proposed in the RFC](https://smithy-lang.github.io/smithy-rs/design/rfcs/rfc0042_file_per_change_changelog.html#the-proposed-developer-experience). This is due to [a limitation](https://github.com/dtolnay/serde-yaml/issues/355) in `serde-yaml` (which has been archived unfortunately), but the multi-line values are still a valid YAML syntax and, most importantly, rendering changelong entries will continue working. For now, I won't post-process from the multi-line values syntax to the single line array syntax. One can work around this by handwriting a changelog entry Markdown file in `smithy-rs/.changelog` without using this subcommand. #### A subcommand `--ls` This subcommand will render a preview of changelog entries stored in `smithy-rs/.changelog` and print it to the standard output. Example usages (using the entry created at 5745197.md above): ``` $ changelogger ls -c smithy-rs \ # for long flag, replace -c with --change-set Next changelog preview for smithy-rs ===================================== **New this release:** - 🐛 (client, [smithy-rs#1234](https://github.com/smithy-lang/smithy-rs/issues/1234), @someone) Some changelog **Contributors** Thank you for your contributions! ❤ - @someone ([smithy-rs#1234](https://github.com/smithy-lang/smithy-rs/issues/1234)) ``` ``` $ changelogger ls -c aws-sdk \ # for long flag, replace -c with --change-set Next changelog preview for aws-sdk-rust ======================================== **New this release:** - 🐛 ([smithy-rs#1234](https://github.com/smithy-lang/smithy-rs/issues/1234), @someone) Some changelog **Contributors** Thank you for your contributions! ❤ - @someone ([smithy-rs#1234](https://github.com/smithy-lang/smithy-rs/issues/1234)) ``` ## Testing - Existing tests in CI - Basic unit tests for subcommands ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
This commit is contained in:
parent
36b50b3dac
commit
63753f3fc7
|
@ -133,11 +133,13 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"edit",
|
||||
"once_cell",
|
||||
"ordinal",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"smithy-rs-tool-common",
|
||||
"tempfile",
|
||||
"time",
|
||||
|
@ -234,6 +236,22 @@ version = "0.1.13"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "edit"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f364860e764787163c8c8f58231003839be31276e821e2ad2092ddf496b1aa09"
|
||||
dependencies = [
|
||||
"tempfile",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.34"
|
||||
|
@ -261,9 +279,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.0.2"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
|
@ -1463,6 +1481,18 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
|
|
@ -17,10 +17,12 @@ opt-level = 0
|
|||
[dependencies]
|
||||
anyhow = "1.0.57"
|
||||
clap = { version = "~3.2.1", features = ["derive"] }
|
||||
edit = "0.1"
|
||||
once_cell = "1.15.0"
|
||||
ordinal = "0.3.2"
|
||||
serde = { version = "1", features = ["derive"]}
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
smithy-rs-tool-common = { path = "../smithy-rs-tool-common" }
|
||||
time = { version = "0.3.9", features = ["local-offset"]}
|
||||
toml = "0.5.8"
|
||||
|
|
|
@ -8,6 +8,7 @@ use clap::clap_derive::ArgEnum;
|
|||
use smithy_rs_tool_common::changelog::{Changelog, HandAuthoredEntry, SdkModelEntry};
|
||||
use smithy_rs_tool_common::git::Git;
|
||||
use smithy_rs_tool_common::versions_manifest::VersionsManifest;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(ArgEnum, Copy, Clone, Debug, Eq, PartialEq)]
|
||||
|
@ -16,6 +17,19 @@ pub enum ChangeSet {
|
|||
AwsSdk,
|
||||
}
|
||||
|
||||
impl Display for ChangeSet {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
ChangeSet::SmithyRs => "smithy-rs",
|
||||
ChangeSet::AwsSdk => "aws-sdk-rust",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChangelogEntries {
|
||||
pub aws_sdk_rust: Vec<ChangelogEntry>,
|
||||
pub smithy_rs: Vec<ChangelogEntry>,
|
||||
|
|
|
@ -5,5 +5,7 @@
|
|||
|
||||
pub mod entry;
|
||||
pub mod init;
|
||||
pub mod ls;
|
||||
pub mod new;
|
||||
pub mod render;
|
||||
pub mod split;
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
use crate::entry::{ChangeSet, ChangelogEntries};
|
||||
use crate::render::render;
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use smithy_rs_tool_common::changelog::{ChangelogLoader, ValidationSet};
|
||||
use smithy_rs_tool_common::git::find_git_repository_root;
|
||||
use smithy_rs_tool_common::here;
|
||||
use smithy_rs_tool_common::versions_manifest::CrateVersionMetadataMap;
|
||||
|
||||
#[derive(Parser, Debug, Eq, PartialEq)]
|
||||
pub struct LsArgs {
|
||||
/// Which set of changes to preview
|
||||
#[clap(long, short, action)]
|
||||
pub change_set: ChangeSet,
|
||||
}
|
||||
|
||||
pub fn subcommand_ls(args: LsArgs) -> anyhow::Result<()> {
|
||||
let mut dot_changelog = find_git_repository_root("smithy-rs", ".").context(here!())?;
|
||||
dot_changelog.push(".changelog");
|
||||
|
||||
let loader = ChangelogLoader::default();
|
||||
let changelog = loader.load_from_dir(dot_changelog.clone())?;
|
||||
|
||||
changelog.validate(ValidationSet::Render).map_err(|errs| {
|
||||
anyhow::Error::msg(format!(
|
||||
"failed to load {dot_changelog:?}: {errors}",
|
||||
errors = errs.join("\n")
|
||||
))
|
||||
})?;
|
||||
|
||||
let entries = ChangelogEntries::from(changelog);
|
||||
|
||||
let (release_header, release_notes) = render(
|
||||
match args.change_set {
|
||||
ChangeSet::SmithyRs => &entries.smithy_rs,
|
||||
ChangeSet::AwsSdk => &entries.aws_sdk_rust,
|
||||
},
|
||||
CrateVersionMetadataMap::new(),
|
||||
&format!("\nNext changelog preview for {}", args.change_set),
|
||||
);
|
||||
|
||||
println!("{release_header}{release_notes}");
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use changelogger::init::subcommand_init;
|
||||
use changelogger::ls::subcommand_ls;
|
||||
use changelogger::new::subcommand_new;
|
||||
use changelogger::render::subcommand_render;
|
||||
use changelogger::split::subcommand_split;
|
||||
use clap::Parser;
|
||||
|
@ -12,19 +14,25 @@ use clap::Parser;
|
|||
#[derive(Parser, Debug, Eq, PartialEq)]
|
||||
#[clap(name = "changelogger", author, version, about)]
|
||||
pub enum Args {
|
||||
/// Split SDK changelog entries into a separate file
|
||||
Split(changelogger::split::SplitArgs),
|
||||
/// Print to stdout the empty "next" CHANGELOG template
|
||||
Init(changelogger::init::InitArgs),
|
||||
/// Create a new changelog entry Markdown file in the `smithy-rs/.changelog` directory
|
||||
New(changelogger::new::NewArgs),
|
||||
/// Render a preview of changelog entries since the last release
|
||||
Ls(changelogger::ls::LsArgs),
|
||||
/// Render a TOML/JSON changelog into GitHub-flavored Markdown
|
||||
Render(changelogger::render::RenderArgs),
|
||||
/// Print to stdout the empty "next" CHANGELOG template.
|
||||
Init(changelogger::init::InitArgs),
|
||||
/// Split SDK changelog entries into a separate file
|
||||
Split(changelogger::split::SplitArgs),
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
match Args::parse() {
|
||||
Args::Split(split) => subcommand_split(&split),
|
||||
Args::Render(render) => subcommand_render(&render),
|
||||
Args::Init(init) => subcommand_init(&init),
|
||||
Args::New(new) => subcommand_new(new),
|
||||
Args::Ls(ls) => subcommand_ls(ls),
|
||||
Args::Render(render) => subcommand_render(&render),
|
||||
Args::Split(split) => subcommand_split(&split),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,10 +40,14 @@ fn main() -> Result<()> {
|
|||
mod tests {
|
||||
use super::Args;
|
||||
use changelogger::entry::ChangeSet;
|
||||
use changelogger::ls::LsArgs;
|
||||
use changelogger::new::NewArgs;
|
||||
use changelogger::render::RenderArgs;
|
||||
use changelogger::split::SplitArgs;
|
||||
use clap::Parser;
|
||||
use smithy_rs_tool_common::changelog::{Reference, Target};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn args_parsing() {
|
||||
|
@ -188,5 +200,55 @@ mod tests {
|
|||
])
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Args::New(NewArgs {
|
||||
applies_to: Some(vec![Target::Client, Target::AwsSdk]),
|
||||
authors: Some(vec!["external-contrib".to_owned(), "ysaito1001".to_owned()]),
|
||||
references: Some(vec![
|
||||
Reference::from_str("smithy-rs#1234").unwrap(),
|
||||
Reference::from_str("aws-sdk-rust#5678").unwrap()
|
||||
]),
|
||||
breaking: false,
|
||||
new_feature: true,
|
||||
bug_fix: false,
|
||||
message: Some("Implement a long-awaited feature for S3".to_owned()),
|
||||
basename: None,
|
||||
}),
|
||||
Args::try_parse_from([
|
||||
"./changelogger",
|
||||
"new",
|
||||
"--applies-to",
|
||||
"client",
|
||||
"--applies-to",
|
||||
"aws-sdk-rust",
|
||||
"--authors",
|
||||
"external-contrib",
|
||||
"--authors",
|
||||
"ysaito1001",
|
||||
"--references",
|
||||
"smithy-rs#1234",
|
||||
"--references",
|
||||
"aws-sdk-rust#5678",
|
||||
"--new-feature",
|
||||
"--message",
|
||||
"Implement a long-awaited feature for S3",
|
||||
])
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Args::Ls(LsArgs {
|
||||
change_set: ChangeSet::SmithyRs
|
||||
}),
|
||||
Args::try_parse_from(["./changelogger", "ls", "--change-set", "smithy-rs",]).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Args::Ls(LsArgs {
|
||||
change_set: ChangeSet::AwsSdk
|
||||
}),
|
||||
Args::try_parse_from(["./changelogger", "ls", "--change-set", "aws-sdk",]).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use smithy_rs_tool_common::changelog::{FrontMatter, Markdown, Reference, Target};
|
||||
use smithy_rs_tool_common::git::find_git_repository_root;
|
||||
use smithy_rs_tool_common::here;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Parser, Debug, Eq, PartialEq)]
|
||||
pub struct NewArgs {
|
||||
/// Target audience for the change (if not provided, user's editor will open for authoring one)
|
||||
#[clap(long, short = 't')]
|
||||
pub applies_to: Option<Vec<Target>>,
|
||||
/// List of git usernames for the authors of the change (if not provided, user's editor will open for authoring one)
|
||||
#[clap(long, short)]
|
||||
pub authors: Option<Vec<String>>,
|
||||
/// List of relevant issues and PRs (if not provided, user's editor will open for authoring one)
|
||||
#[clap(long, short)]
|
||||
pub references: Option<Vec<Reference>>,
|
||||
/// Whether or not the change contains a breaking change (defaults to false)
|
||||
#[clap(long, action)]
|
||||
pub breaking: bool,
|
||||
/// Whether or not the change implements a new feature (defaults to false)
|
||||
#[clap(long, action)]
|
||||
pub new_feature: bool,
|
||||
/// Whether or not the change fixes a bug (defaults to false)
|
||||
#[clap(long, short, action)]
|
||||
pub bug_fix: bool,
|
||||
/// The changelog entry message (if not provided, user's editor will open for authoring one)
|
||||
#[clap(long, short)]
|
||||
pub message: Option<String>,
|
||||
/// Basename of a changelog markdown file (defaults to a random 6-digit basename)
|
||||
#[clap(long)]
|
||||
pub basename: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl From<NewArgs> for Markdown {
|
||||
fn from(value: NewArgs) -> Self {
|
||||
Markdown {
|
||||
front_matter: FrontMatter {
|
||||
applies_to: value.applies_to.unwrap_or_default().into_iter().collect(),
|
||||
authors: value.authors.unwrap_or_default(),
|
||||
references: value.references.unwrap_or_default(),
|
||||
breaking: value.breaking,
|
||||
new_feature: value.new_feature,
|
||||
bug_fix: value.bug_fix,
|
||||
},
|
||||
message: value.message.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subcommand_new(args: NewArgs) -> anyhow::Result<()> {
|
||||
let mut md_full_filename = find_git_repository_root("smithy-rs", ".").context(here!())?;
|
||||
md_full_filename.push(".changelog");
|
||||
md_full_filename.push(args.basename.clone().unwrap_or(PathBuf::from(format!(
|
||||
"{:?}.md",
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("should get the current Unix epoch time")
|
||||
.as_secs(),
|
||||
))));
|
||||
|
||||
let changelog_entry = new_entry(Markdown::from(args))?;
|
||||
std::fs::write(&md_full_filename, &changelog_entry).with_context(|| {
|
||||
format!(
|
||||
"failed to write the following changelog entry to {:?}:\n{}",
|
||||
md_full_filename.as_path(),
|
||||
changelog_entry
|
||||
)
|
||||
})?;
|
||||
|
||||
println!(
|
||||
"\nThe following changelog entry has been written to {:?}:\n{}",
|
||||
md_full_filename.as_path(),
|
||||
changelog_entry
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_entry(markdown: Markdown) -> anyhow::Result<String> {
|
||||
// Due to the inability for `serde_yaml` to output single line array syntax, an array of values
|
||||
// will be serialized as follows:
|
||||
//
|
||||
// key:
|
||||
// - value1
|
||||
// - value2
|
||||
//
|
||||
// as opposed to:
|
||||
//
|
||||
// key: [value1, value2]
|
||||
//
|
||||
// This doesn't present practical issues when rendering changelogs. See
|
||||
// https://github.com/dtolnay/serde-yaml/issues/355
|
||||
let front_matter = serde_yaml::to_string(&markdown.front_matter)?;
|
||||
let changelog_entry = format!("---\n{}---\n{}", front_matter, markdown.message);
|
||||
let changelog_entry = if any_required_field_needs_to_be_filled(&markdown) {
|
||||
edit::edit(changelog_entry).context("failed while editing changelog entry)")?
|
||||
} else {
|
||||
changelog_entry
|
||||
};
|
||||
|
||||
Ok(changelog_entry)
|
||||
}
|
||||
|
||||
fn any_required_field_needs_to_be_filled(markdown: &Markdown) -> bool {
|
||||
macro_rules! any_empty {
|
||||
() => { false };
|
||||
($head:expr $(, $tail:expr)*) => {
|
||||
$head.is_empty() || any_empty!($($tail),*)
|
||||
};
|
||||
}
|
||||
any_empty!(
|
||||
&markdown.front_matter.applies_to,
|
||||
&markdown.front_matter.authors,
|
||||
&markdown.front_matter.references,
|
||||
&markdown.message
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::new::{any_required_field_needs_to_be_filled, new_entry, NewArgs};
|
||||
use smithy_rs_tool_common::changelog::{Reference, Target};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_new_entry_from_args() {
|
||||
// make sure `args` populates required fields (so the function
|
||||
// `any_required_field_needs_to_be_filled` should return false), otherwise an editor would
|
||||
// be opened during the test execution for human input, causing the test to get struck
|
||||
let args = NewArgs {
|
||||
applies_to: Some(vec![Target::Client]),
|
||||
authors: Some(vec!["ysaito1001".to_owned()]),
|
||||
references: Some(vec![Reference::from_str("smithy-rs#1234").unwrap()]),
|
||||
breaking: false,
|
||||
new_feature: true,
|
||||
bug_fix: false,
|
||||
message: Some("Implement a long-awaited feature for S3".to_owned()),
|
||||
basename: None,
|
||||
};
|
||||
let markdown = args.into();
|
||||
assert!(
|
||||
!any_required_field_needs_to_be_filled(&markdown),
|
||||
"one or more required fields were not populated"
|
||||
);
|
||||
|
||||
let expected = "---\napplies_to:\n- client\nauthors:\n- ysaito1001\nreferences:\n- smithy-rs#1234\nbreaking: false\nnew_feature: true\nbug_fix: false\n---\nImplement a long-awaited feature for S3";
|
||||
let actual = new_entry(markdown).unwrap();
|
||||
|
||||
assert_eq!(expected, &actual);
|
||||
}
|
||||
}
|
|
@ -475,7 +475,7 @@ fn render_crate_versions(crate_version_metadata_map: CrateVersionMetadataMap, ou
|
|||
|
||||
/// Convert a list of changelog entries and crate versions into markdown.
|
||||
/// Returns (header, body)
|
||||
fn render(
|
||||
pub(crate) fn render(
|
||||
entries: &[ChangelogEntry],
|
||||
crate_version_metadata_map: CrateVersionMetadataMap,
|
||||
release_header: &str,
|
||||
|
|
|
@ -7,6 +7,7 @@ use crate::lint::LintError;
|
|||
use crate::{repo_root, Check, Lint};
|
||||
use anyhow::Result;
|
||||
use smithy_rs_tool_common::changelog::{ChangelogLoader, ValidationSet};
|
||||
use std::fmt::Debug;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub(crate) struct ChangelogNext;
|
||||
|
@ -22,7 +23,7 @@ impl Lint for ChangelogNext {
|
|||
}
|
||||
|
||||
impl Check for ChangelogNext {
|
||||
fn check(&self, path: impl AsRef<Path>) -> Result<Vec<LintError>> {
|
||||
fn check(&self, path: impl AsRef<Path> + Debug) -> Result<Vec<LintError>> {
|
||||
match check_changelog_next(path) {
|
||||
Ok(_) => Ok(vec![]),
|
||||
Err(errs) => Ok(errs),
|
||||
|
@ -34,7 +35,7 @@ impl Check for ChangelogNext {
|
|||
// and run the validation only when the directory has at least one changelog entry file, otherwise
|
||||
// a default constructed `ChangeLog` won't pass the validation.
|
||||
/// Validate that `CHANGELOG.next.toml` follows best practices
|
||||
fn check_changelog_next(path: impl AsRef<Path>) -> std::result::Result<(), Vec<LintError>> {
|
||||
fn check_changelog_next(path: impl AsRef<Path> + Debug) -> std::result::Result<(), Vec<LintError>> {
|
||||
let parsed = ChangelogLoader::default()
|
||||
.load_from_file(path)
|
||||
.map_err(|e| vec![LintError::via_display(e)])?;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
use anyhow::Context;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::fs::read_to_string;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
@ -119,7 +119,7 @@ pub(crate) trait Lint {
|
|||
}
|
||||
|
||||
pub(crate) trait Check: Lint {
|
||||
fn check(&self, path: impl AsRef<Path>) -> anyhow::Result<Vec<LintError>>;
|
||||
fn check(&self, path: impl AsRef<Path> + Debug) -> anyhow::Result<Vec<LintError>>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
|
|
|
@ -10,7 +10,9 @@ pub mod parser;
|
|||
use crate::changelog::parser::{ParseIntoChangelog, ParserChain};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -69,7 +71,7 @@ pub struct Meta {
|
|||
pub target: Option<SdkAffected>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Reference {
|
||||
pub repo: String,
|
||||
pub number: usize,
|
||||
|
@ -130,10 +132,19 @@ enum AuthorsInner {
|
|||
Multiple(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(from = "AuthorsInner", into = "AuthorsInner")]
|
||||
pub struct Authors(pub(super) Vec<String>);
|
||||
|
||||
impl PartialEq for Authors {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// `true` if two `Authors` contain the same set of authors, regardless of their order
|
||||
self.0.iter().collect::<HashSet<_>>() == other.0.iter().collect::<HashSet<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Authors {}
|
||||
|
||||
impl From<AuthorsInner> for Authors {
|
||||
fn from(value: AuthorsInner) -> Self {
|
||||
match value {
|
||||
|
@ -317,6 +328,87 @@ impl Changelog {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub enum Target {
|
||||
#[serde(rename = "client")]
|
||||
Client,
|
||||
#[serde(rename = "server")]
|
||||
Server,
|
||||
#[serde(rename = "aws-sdk-rust")]
|
||||
AwsSdk,
|
||||
}
|
||||
|
||||
impl FromStr for Target {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(sdk: &str) -> std::result::Result<Self, Self::Err> {
|
||||
match sdk.to_lowercase().as_str() {
|
||||
"client" => Ok(Target::Client),
|
||||
"server" => Ok(Target::Server),
|
||||
"aws-sdk-rust" => Ok(Target::AwsSdk),
|
||||
_ => bail!("An invalid type of `Target` {sdk} has been specified"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct FrontMatter {
|
||||
pub applies_to: HashSet<Target>,
|
||||
pub authors: Vec<String>,
|
||||
pub references: Vec<Reference>,
|
||||
pub breaking: bool,
|
||||
pub new_feature: bool,
|
||||
pub bug_fix: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Markdown {
|
||||
pub front_matter: FrontMatter,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl From<Markdown> for Changelog {
|
||||
fn from(value: Markdown) -> Self {
|
||||
let front_matter = value.front_matter;
|
||||
let entry = HandAuthoredEntry {
|
||||
message: value.message.trim_start().to_owned(),
|
||||
meta: Meta {
|
||||
bug: front_matter.bug_fix,
|
||||
breaking: front_matter.breaking,
|
||||
tada: front_matter.new_feature,
|
||||
target: None,
|
||||
},
|
||||
authors: AuthorsInner::Multiple(front_matter.authors).into(),
|
||||
references: front_matter.references,
|
||||
since_commit: None,
|
||||
age: None,
|
||||
};
|
||||
|
||||
let mut changelog = Changelog::new();
|
||||
|
||||
// Bin `entry` into the appropriate `Vec` based on the `applies_to` field in `front_matter`
|
||||
if front_matter.applies_to.contains(&Target::AwsSdk) {
|
||||
changelog.aws_sdk_rust.push(entry.clone())
|
||||
}
|
||||
if front_matter.applies_to.contains(&Target::Client)
|
||||
&& front_matter.applies_to.contains(&Target::Server)
|
||||
{
|
||||
let mut entry = entry.clone();
|
||||
entry.meta.target = Some(SdkAffected::All);
|
||||
changelog.smithy_rs.push(entry);
|
||||
} else if front_matter.applies_to.contains(&Target::Client) {
|
||||
let mut entry = entry.clone();
|
||||
entry.meta.target = Some(SdkAffected::Client);
|
||||
changelog.smithy_rs.push(entry);
|
||||
} else if front_matter.applies_to.contains(&Target::Server) {
|
||||
let mut entry = entry.clone();
|
||||
entry.meta.target = Some(SdkAffected::Server);
|
||||
changelog.smithy_rs.push(entry);
|
||||
}
|
||||
|
||||
changelog
|
||||
}
|
||||
}
|
||||
/// Parses changelog entries into [`Changelog`] using a series of parsers.
|
||||
///
|
||||
/// Each parser will attempt to parse an input string in order:
|
||||
|
@ -336,10 +428,11 @@ impl ChangelogLoader {
|
|||
}
|
||||
|
||||
/// Parses the contents of a file located at `path` into `Changelog`
|
||||
pub fn load_from_file(&self, path: impl AsRef<Path>) -> Result<Changelog> {
|
||||
pub fn load_from_file(&self, path: impl AsRef<Path> + Debug) -> Result<Changelog> {
|
||||
let contents = std::fs::read_to_string(path.as_ref())
|
||||
.with_context(|| format!("failed to read {:?}", path.as_ref()))?;
|
||||
self.parse_str(contents)
|
||||
.with_context(|| format!("failed to parse the contents in {path:?}"))
|
||||
}
|
||||
|
||||
/// Parses the contents of files stored in a directory `dir_path` into `Changelog`
|
||||
|
@ -347,7 +440,7 @@ impl ChangelogLoader {
|
|||
/// It opens each file in the directory, parses the file contents into `Changelog`,
|
||||
/// and merges it with accumulated `Changelog`. It currently does not support loading
|
||||
/// from recursive directory structures.
|
||||
pub fn load_from_dir(&self, dir_path: impl AsRef<Path>) -> Result<Changelog> {
|
||||
pub fn load_from_dir(&self, dir_path: impl AsRef<Path> + Debug) -> Result<Changelog> {
|
||||
let entries = std::fs::read_dir(dir_path.as_ref())?;
|
||||
let result = entries
|
||||
.into_iter()
|
||||
|
|
|
@ -3,10 +3,8 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
use crate::changelog::{Authors, Changelog, HandAuthoredEntry, Meta, Reference, SdkAffected};
|
||||
use crate::changelog::{Changelog, Markdown, SdkAffected};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub(crate) trait ParseIntoChangelog: Debug {
|
||||
|
@ -45,75 +43,6 @@ impl ParseIntoChangelog for Json {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Hash, Serialize, PartialEq, Eq)]
|
||||
enum Target {
|
||||
#[serde(rename = "client")]
|
||||
Client,
|
||||
#[serde(rename = "server")]
|
||||
Server,
|
||||
#[serde(rename = "aws-sdk-rust")]
|
||||
AwsSdk,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
struct FrontMatter {
|
||||
applies_to: HashSet<Target>,
|
||||
authors: Authors,
|
||||
references: Vec<Reference>,
|
||||
breaking: bool,
|
||||
new_feature: bool,
|
||||
bug_fix: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Markdown {
|
||||
front_matter: FrontMatter,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl From<Markdown> for Changelog {
|
||||
fn from(value: Markdown) -> Self {
|
||||
let front_matter = value.front_matter;
|
||||
let entry = HandAuthoredEntry {
|
||||
message: value.message.trim_start().to_owned(),
|
||||
meta: Meta {
|
||||
bug: front_matter.bug_fix,
|
||||
breaking: front_matter.breaking,
|
||||
tada: front_matter.new_feature,
|
||||
target: None,
|
||||
},
|
||||
authors: front_matter.authors,
|
||||
references: front_matter.references,
|
||||
since_commit: None,
|
||||
age: None,
|
||||
};
|
||||
|
||||
let mut changelog = Changelog::new();
|
||||
|
||||
// Bin `entry` into the appropriate `Vec` based on the `applies_to` field in `front_matter`
|
||||
if front_matter.applies_to.contains(&Target::AwsSdk) {
|
||||
changelog.aws_sdk_rust.push(entry.clone())
|
||||
}
|
||||
if front_matter.applies_to.contains(&Target::Client)
|
||||
&& front_matter.applies_to.contains(&Target::Server)
|
||||
{
|
||||
let mut entry = entry.clone();
|
||||
entry.meta.target = Some(SdkAffected::All);
|
||||
changelog.smithy_rs.push(entry);
|
||||
} else if front_matter.applies_to.contains(&Target::Client) {
|
||||
let mut entry = entry.clone();
|
||||
entry.meta.target = Some(SdkAffected::Client);
|
||||
changelog.smithy_rs.push(entry);
|
||||
} else if front_matter.applies_to.contains(&Target::Server) {
|
||||
let mut entry = entry.clone();
|
||||
entry.meta.target = Some(SdkAffected::Server);
|
||||
changelog.smithy_rs.push(entry);
|
||||
}
|
||||
|
||||
changelog
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseIntoChangelog for Markdown {
|
||||
fn parse(&self, value: &str) -> Result<Changelog> {
|
||||
let mut parts = value.splitn(3, "---");
|
||||
|
@ -153,17 +82,18 @@ impl Default for ParserChain {
|
|||
|
||||
impl ParseIntoChangelog for ParserChain {
|
||||
fn parse(&self, value: &str) -> Result<Changelog> {
|
||||
for (name, parser) in &self.parsers {
|
||||
let mut errs = Vec::new();
|
||||
for (_name, parser) in &self.parsers {
|
||||
match parser.parse(value) {
|
||||
Ok(parsed) => {
|
||||
return Ok(parsed);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::debug!(parser = %name, err = %err, "failed to parse the input string");
|
||||
errs.push(err.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("no parsers in chain parsed ${value} into `Changelog`")
|
||||
bail!("no parsers in chain parsed the following into `Changelog`\n{value}\nwith respective errors: \n{errs:?}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -336,5 +266,32 @@ This is some **Markdown** content.
|
|||
);
|
||||
assert!(changelog.aws_sdk_rust.is_empty());
|
||||
}
|
||||
{
|
||||
let markdown = r#"---
|
||||
applies_to:
|
||||
- client
|
||||
- aws-sdk-rust
|
||||
authors:
|
||||
- ysaito1001
|
||||
references:
|
||||
- smithy-rs#1234
|
||||
breaking: false
|
||||
new_feature: false
|
||||
bug_fix: true
|
||||
---
|
||||
Fix a long-standing bug.
|
||||
"#;
|
||||
let changelog = Markdown::default().parse(markdown).unwrap();
|
||||
assert_eq!(1, changelog.smithy_rs.len());
|
||||
assert_eq!(
|
||||
Some(SdkAffected::Client),
|
||||
changelog.smithy_rs[0].meta.target
|
||||
);
|
||||
assert_eq!(
|
||||
"Fix a long-standing bug.\n",
|
||||
&changelog.smithy_rs[0].message
|
||||
);
|
||||
assert_eq!(1, changelog.aws_sdk_rust.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue