…parsing really needs a headline and it's probably ok to provide it. Otherwise parsing would be more complicated and it shouldn't be that way now.
Unified · Split
| @@ -85,6 +85,9 @@ fn merge_section(dest: &mut Section, src: Section) { · 84 unmodified lines | ||
| 85 | 85 | stats @ section::Segment::Statistics(_) => { |
| 86 | 86 | replace_all_or_append(lhs_segments, |s| matches!(s, section::Segment::Statistics(_)), stats) |
| 87 | 87 | } |
| 88 | + details @ section::Segment::Details(_) => { | |
| 89 | + replace_all_or_append(lhs_segments, |s| matches!(s, section::Segment::Details(_)), details) | |
| 90 | + } | |
| 88 | 91 | } |
| 89 | 92 | } |
| 90 | 93 | *lhs_date = rhs_date; |
| @@ -50,10 +50,16 @@ impl Section { · 49 unmodified lines | ||
| 50 | 50 | pub fn is_essential(&self) -> bool { |
| 51 | 51 | match self { |
| 52 | 52 | Section::Verbatim { .. } => true, |
| 53 | − Section::Release { segments, .. } => segments.iter().any(|s| match s { | |
| 54 | − section::Segment::User { .. } => true, | |
| 55 | − section::Segment::Clippy(_) | section::Segment::Statistics(_) => false, | |
| 56 | − }), | |
| 53 | + Section::Release { segments, .. } => segments.iter().any(|s| !s.is_read_only()), | |
| 54 | + } | |
| 55 | + } | |
| 56 | +} | |
| 57 | + | |
| 58 | +impl section::Segment { | |
| 59 | + pub fn is_read_only(&self) -> bool { | |
| 60 | + match self { | |
| 61 | + section::Segment::User { .. } => false, | |
| 62 | + section::Segment::Clippy(_) | section::Segment::Statistics(_) | section::Segment::Details(_) => true, | |
| 57 | 63 | } |
| 58 | 64 | } |
| 59 | 65 | } |
| @@ -88,6 +88,15 @@ impl Section { · 87 unmodified lines | ||
| 88 | 88 | track_unknown_event(event, &mut unknown); |
| 89 | 89 | } |
| 90 | 90 | } |
| 91 | + Event::Html(text) if text.starts_with(section::Details::PREFIX) => { | |
| 92 | + record_unknown_range(&mut segments, unknown_range.take(), &body); | |
| 93 | + events | |
| 94 | + .by_ref() | |
| 95 | + .take_while( | |
| 96 | + |(e, _range)| !matches!(e, Event::Html(text) if text.starts_with(section::Details::END)), | |
| 97 | + ) | |
| 98 | + .count(); | |
| 99 | + } | |
| 91 | 100 | Event::Start(Tag::Heading(indent)) => { |
| 92 | 101 | record_unknown_range(&mut segments, unknown_range.take(), &body); |
| 93 | 102 | enum State { |
| @@ -1,12 +1,14 @@ | ||
| 1 | +use std::collections::BTreeMap; | |
| 2 | + | |
| 1 | 3 | #[derive(Eq, PartialEq, Debug, Clone)] |
| 2 | 4 | pub enum Segment { |
| 3 | 5 | /// A portion of a Section that we couldn't make sense of, but which should be kept as is nonetheless. |
| 4 | 6 | User { |
| 5 | 7 | text: String, |
| 6 | 8 | }, |
| 7 | − /// A thanks clippy headline with the amount of times clippy helped | |
| 8 | − Clippy(Data<ThanksClippy>), | |
| 9 | + Details(Data<Details>), | |
| 9 | 10 | Statistics(Data<CommitStatistics>), |
| 11 | + Clippy(Data<ThanksClippy>), | |
| 10 | 12 | } |
| 11 | 13 | |
| 12 | 14 | #[derive(Eq, Debug, Clone)] |
| @@ -24,6 +26,42 @@ impl<T: PartialEq<T>> PartialEq<Data<T>> for Data<T> { · 11 unmodified lines | ||
| 24 | 26 | } |
| 25 | 27 | } |
| 26 | 28 | |
| 29 | +pub mod details { | |
| 30 | + use std::fmt; | |
| 31 | + | |
| 32 | + #[derive(PartialEq, Eq, Ord, PartialOrd, Debug, Clone)] | |
| 33 | + pub enum Category { | |
| 34 | + Issue(String), | |
| 35 | + Uncategorized, | |
| 36 | + } | |
| 37 | + | |
| 38 | + impl fmt::Display for Category { | |
| 39 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| 40 | + match self { | |
| 41 | + Category::Uncategorized => f.write_str("Uncategorized"), | |
| 42 | + Category::Issue(issue) => write!(f, "#{}", issue), | |
| 43 | + } | |
| 44 | + } | |
| 45 | + } | |
| 46 | + | |
| 47 | + #[derive(PartialEq, Eq, Debug, Clone)] | |
| 48 | + pub struct Message { | |
| 49 | + pub title: String, | |
| 50 | + pub body: Option<String>, | |
| 51 | + } | |
| 52 | +} | |
| 53 | + | |
| 54 | +#[derive(PartialEq, Eq, Debug, Clone)] | |
| 55 | +pub struct Details { | |
| 56 | + pub commits_by_category: BTreeMap<details::Category, Vec<details::Message>>, | |
| 57 | +} | |
| 58 | + | |
| 59 | +impl Details { | |
| 60 | + pub const TITLE: &'static str = "Commit Details"; | |
| 61 | + pub const PREFIX: &'static str = "<details><summary>view details</summary>"; | |
| 62 | + pub const END: &'static str = "</details>"; | |
| 63 | +} | |
| 64 | + | |
| 27 | 65 | #[derive(PartialEq, Eq, Debug, Clone)] |
| 28 | 66 | pub struct CommitStatistics { |
| 29 | 67 | /// Amount of commits that contributed to the release |
| @@ -71,6 +71,23 @@ impl section::Segment { · 70 unmodified lines | ||
| 71 | 71 | pub fn write_to(&self, section_level: usize, mut out: impl std::io::Write) -> std::io::Result<()> { |
| 72 | 72 | match self { |
| 73 | 73 | section::Segment::User { text } => out.write_all(text.as_bytes())?, |
| 74 | + section::Segment::Details(section::Data::Generated(section::Details { commits_by_category })) | |
| 75 | + if !commits_by_category.is_empty() => | |
| 76 | + { | |
| 77 | + // writeln!(out, "{} {}\n", heading(section_level), section::Details::TITLE)?; | |
| 78 | + writeln!(out, "{}", Section::READONLY_TAG)?; | |
| 79 | + writeln!(out, "{}", section::Details::PREFIX)?; | |
| 80 | + for (category, messages) in commits_by_category.iter() { | |
| 81 | + writeln!(out, " * **{}**", category)?; | |
| 82 | + for message in messages { | |
| 83 | + writeln!(out, " - {}", message.title)?; | |
| 84 | + if let Some(body) = &message.body { | |
| 85 | + writeln!(out, "{}", body)?; | |
| 86 | + } | |
| 87 | + } | |
| 88 | + } | |
| 89 | + writeln!(out, "{}\n", section::Details::END)?; | |
| 90 | + } | |
| 74 | 91 | section::Segment::Statistics(section::Data::Generated(section::CommitStatistics { |
| 75 | 92 | count, |
| 76 | 93 | duration, |
| @@ -120,6 +137,7 @@ impl section::Segment { · 43 unmodified lines | ||
| 120 | 137 | } |
| 121 | 138 | section::Segment::Clippy(_) => {} |
| 122 | 139 | section::Segment::Statistics(_) => {} |
| 140 | + section::Segment::Details(_) => {} | |
| 123 | 141 | }; |
| 124 | 142 | Ok(()) |
| 125 | 143 | } |
| @@ -6,6 +6,7 @@ use cargo_smart_release::{ · 5 unmodified lines | ||
| 6 | 6 | use git_repository::bstr::ByteSlice; |
| 7 | 7 | |
| 8 | 8 | use crate::Result; |
| 9 | +use std::collections::BTreeMap; | |
| 9 | 10 | |
| 10 | 11 | #[test] |
| 11 | 12 | fn all_section_types_round_trips_lossy() -> Result { |
| @@ -37,6 +38,35 @@ fn all_section_types_round_trips_lossy() -> Result { · 25 unmodified lines | ||
| 37 | 38 | conventional_count: 20, |
| 38 | 39 | unique_issues_count: 3, |
| 39 | 40 | })), |
| 41 | + section::Segment::Details(section::Data::Generated(section::Details { | |
| 42 | + commits_by_category: { | |
| 43 | + let mut h = BTreeMap::default(); | |
| 44 | + h.insert( | |
| 45 | + section::details::Category::Uncategorized, | |
| 46 | + vec![section::details::Message { | |
| 47 | + title: "Just the title".into(), | |
| 48 | + body: None, | |
| 49 | + }, section::details::Message { | |
| 50 | + title: "title and body".into(), | |
| 51 | + body: Some("body with\nmultiple newlines\nwhich probably should be put into a single line but we don't go there for now.".into()), | |
| 52 | + }], | |
| 53 | + ); | |
| 54 | + h.insert( | |
| 55 | + section::details::Category::Issue("42".into()), | |
| 56 | + vec![ | |
| 57 | + section::details::Message { | |
| 58 | + title: "Just the title".into(), | |
| 59 | + body: None, | |
| 60 | + }, | |
| 61 | + section::details::Message { | |
| 62 | + title: "another title".into(), | |
| 63 | + body: None, | |
| 64 | + }, | |
| 65 | + ], | |
| 66 | + ); | |
| 67 | + h | |
| 68 | + }, | |
| 69 | + })), | |
| 40 | 70 | ], |
| 41 | 71 | unknown: String::new(), |
| 42 | 72 | }, |
| @@ -29,4 +29,18 @@ world · 28 unmodified lines | ||
| 29 | 29 | - 20 commits where understood as [conventional](https://www.conventionalcommits.org). |
| 30 | 30 | - 3 unique issues were worked on |
| 31 | 31 | |
| 32 | +<details><summary>Commit Details</summary> | |
| 33 | + | |
| 34 | +<csr-read-only-do-not-edit/> | |
| 35 | + * **#42** | |
| 36 | + - Just the title | |
| 37 | + - another title | |
| 38 | + * **Uncategorized** | |
| 39 | + - Just the title | |
| 40 | + - title and body | |
| 41 | +body with | |
| 42 | +multiple newlines | |
| 43 | +which probably should be put into a single line but we don't go there for now. | |
| 44 | +</details> | |
| 45 | + | |
| 32 | 46 | |
| @@ -0,0 +1,47 @@ | ||
| 1 | +--- | |
| 2 | +source: cargo-smart-release/tests/changelog/write_and_parse/mod.rs | |
| 3 | +expression: md | |
| 4 | + | |
| 5 | +--- | |
| 6 | +# Changelog | |
| 7 | + | |
| 8 | +my very own header | |
| 9 | + | |
| 10 | +## Unreleased | |
| 11 | + | |
| 12 | +<csr-unknown> | |
| 13 | +hello | |
| 14 | +world | |
| 15 | +<csr-unknown/> | |
| 16 | +#### v1.0.2-beta.2 (1970-01-01) | |
| 17 | + | |
| 18 | +* hello world | |
| 19 | + this | |
| 20 | +##### Thanks Clippy | |
| 21 | + | |
| 22 | +<csr-read-only-do-not-edit/> | |
| 23 | +[Clippy](https://github.com/rust-lang/rust-clippy) helped 42 times to make code idiomatic. | |
| 24 | + | |
| 25 | +##### Commit Statistics | |
| 26 | + | |
| 27 | +<csr-read-only-do-not-edit/> | |
| 28 | + - 100 commits contributed to the release over the course of 32 calendar days. | |
| 29 | + - 20 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 30 | + - 3 unique issues were worked on | |
| 31 | + | |
| 32 | +##### Commit Details | |
| 33 | + | |
| 34 | +<csr-read-only-do-not-edit/> | |
| 35 | +<details><summary>view details</summary> | |
| 36 | + * **#42** | |
| 37 | + - Just the title | |
| 38 | + - another title | |
| 39 | + * **Uncategorized** | |
| 40 | + - Just the title | |
| 41 | + - title and body | |
| 42 | +body with | |
| 43 | +multiple newlines | |
| 44 | +which probably should be put into a single line but we don't go there for now. | |
| 45 | +</details> | |
| 46 | + | |
| 47 | + | |