…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 | stats @ section::Segment::Statistics(_) => { | 85 | stats @ section::Segment::Statistics(_) => { |
| 86 | replace_all_or_append(lhs_segments, |s| matches!(s, section::Segment::Statistics(_)), stats) | 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 | *lhs_date = rhs_date; | 93 | *lhs_date = rhs_date; |
| @@ -50,10 +50,16 @@ impl Section { · 49 unmodified lines | |||
| 50 | pub fn is_essential(&self) -> bool { | 50 | pub fn is_essential(&self) -> bool { |
| 51 | match self { | 51 | match self { |
| 52 | Section::Verbatim { .. } => true, | 52 | Section::Verbatim { .. } => true, |
| 53 | Section::Release { segments, .. } => segments.iter().any(|s| match s { | 53 | Section::Release { segments, .. } => segments.iter().any(|s| !s.is_read_only()), |
| 54 | section::Segment::User { .. } => true, | 54 | } |
| 55 | section::Segment::Clippy(_) | section::Segment::Statistics(_) => false, | 55 | } |
| 56 | }), | 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 | track_unknown_event(event, &mut unknown); | 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 | Event::Start(Tag::Heading(indent)) => { | 100 | Event::Start(Tag::Heading(indent)) => { |
| 92 | record_unknown_range(&mut segments, unknown_range.take(), &body); | 101 | record_unknown_range(&mut segments, unknown_range.take(), &body); |
| 93 | enum State { | 102 | enum State { |
| @@ -1,12 +1,14 @@ | |||
| 1 | use std::collections::BTreeMap; | ||
| 2 | |||
| 1 | #[derive(Eq, PartialEq, Debug, Clone)] | 3 | #[derive(Eq, PartialEq, Debug, Clone)] |
| 2 | pub enum Segment { | 4 | pub enum Segment { |
| 3 | /// A portion of a Section that we couldn't make sense of, but which should be kept as is nonetheless. | 5 | /// A portion of a Section that we couldn't make sense of, but which should be kept as is nonetheless. |
| 4 | User { | 6 | User { |
| 5 | text: String, | 7 | text: String, |
| 6 | }, | 8 | }, |
| 7 | /// A thanks clippy headline with the amount of times clippy helped | 9 | Details(Data<Details>), |
| 8 | Clippy(Data<ThanksClippy>), | ||
| 9 | Statistics(Data<CommitStatistics>), | 10 | Statistics(Data<CommitStatistics>), |
| 11 | Clippy(Data<ThanksClippy>), | ||
| 10 | } | 12 | } |
| 11 | 13 | ||
| 12 | #[derive(Eq, Debug, Clone)] | 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 | #[derive(PartialEq, Eq, Debug, Clone)] | 65 | #[derive(PartialEq, Eq, Debug, Clone)] |
| 28 | pub struct CommitStatistics { | 66 | pub struct CommitStatistics { |
| 29 | /// Amount of commits that contributed to the release | 67 | /// Amount of commits that contributed to the release |
| @@ -71,6 +71,23 @@ impl section::Segment { · 70 unmodified lines | |||
| 71 | pub fn write_to(&self, section_level: usize, mut out: impl std::io::Write) -> std::io::Result<()> { | 71 | pub fn write_to(&self, section_level: usize, mut out: impl std::io::Write) -> std::io::Result<()> { |
| 72 | match self { | 72 | match self { |
| 73 | section::Segment::User { text } => out.write_all(text.as_bytes())?, | 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 | section::Segment::Statistics(section::Data::Generated(section::CommitStatistics { | 91 | section::Segment::Statistics(section::Data::Generated(section::CommitStatistics { |
| 75 | count, | 92 | count, |
| 76 | duration, | 93 | duration, |
| @@ -120,6 +137,7 @@ impl section::Segment { · 43 unmodified lines | |||
| 120 | } | 137 | } |
| 121 | section::Segment::Clippy(_) => {} | 138 | section::Segment::Clippy(_) => {} |
| 122 | section::Segment::Statistics(_) => {} | 139 | section::Segment::Statistics(_) => {} |
| 140 | section::Segment::Details(_) => {} | ||
| 123 | }; | 141 | }; |
| 124 | Ok(()) | 142 | Ok(()) |
| 125 | } | 143 | } |
| @@ -6,6 +6,7 @@ use cargo_smart_release::{ · 5 unmodified lines | |||
| 6 | use git_repository::bstr::ByteSlice; | 6 | use git_repository::bstr::ByteSlice; |
| 7 | 7 | ||
| 8 | use crate::Result; | 8 | use crate::Result; |
| 9 | use std::collections::BTreeMap; | ||
| 9 | 10 | ||
| 10 | #[test] | 11 | #[test] |
| 11 | fn all_section_types_round_trips_lossy() -> Result { | 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 | conventional_count: 20, | 38 | conventional_count: 20, |
| 38 | unique_issues_count: 3, | 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 | unknown: String::new(), | 71 | unknown: String::new(), |
| 42 | }, | 72 | }, |
| @@ -29,4 +29,18 @@ world · 28 unmodified lines | |||
| 29 | - 20 commits where understood as [conventional](https://www.conventionalcommits.org). | 29 | - 20 commits where understood as [conventional](https://www.conventionalcommits.org). |
| 30 | - 3 unique issues were worked on | 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 | |||