1- use std:: { cmp , sync:: OnceLock } ;
1+ use std:: sync:: OnceLock ;
22
33use pico_args:: Arguments ;
44use regex_lite:: Regex ;
@@ -31,8 +31,16 @@ pub(crate) fn check_changelog(shell: Shell, mut args: Arguments) -> anyhow::Resu
3131 . read ( )
3232 . unwrap ( ) ;
3333
34+ let old_changelog_contents = shell
35+ . cmd ( "git" )
36+ . arg ( "show" )
37+ . arg ( format ! ( "{from_commit}:{CHANGELOG_PATH_RELATIVE}" ) )
38+ . arg ( "--" )
39+ . read ( )
40+ . unwrap ( ) ;
41+
3442 // NOTE: If `to_commit` is not specified, we fetch from the file system, instead of `git show`.
35- let changelog_contents = if let Some ( to_commit) = to_commit. as_ref ( ) {
43+ let new_changelog_contents = if let Some ( to_commit) = to_commit. as_ref ( ) {
3644 shell
3745 . cmd ( "git" )
3846 . arg ( "show" )
@@ -46,7 +54,8 @@ pub(crate) fn check_changelog(shell: Shell, mut args: Arguments) -> anyhow::Resu
4654
4755 let mut failed = false ;
4856
49- let hunks_in_a_released_section = hunks_in_a_released_section ( & changelog_contents, & diff) ;
57+ let hunks_in_a_released_section =
58+ hunks_in_a_released_section ( & old_changelog_contents, & new_changelog_contents, & diff) ;
5059 log:: info!(
5160 "# of hunks in a released section of `{CHANGELOG_PATH_RELATIVE}`: {}" ,
5261 hunks_in_a_released_section. len( )
@@ -84,29 +93,29 @@ pub(crate) fn check_changelog(shell: Shell, mut args: Arguments) -> anyhow::Resu
8493 }
8594}
8695
87- /// Given some `changelog_contents` (in Markdown) containing the full end state of the provided
88- /// `diff` (in [unified diff format]), return all hunks that are (1) below a `## Unreleased` section
89- /// _and_ (2) above all other second-level (i.e., `## …`) headings.
96+ /// Given some `old_changelog_contents` and `new_changelog_contents` (in Markdown) containing the
97+ /// full start and end states of the provided `diff` (in [unified diff format]), return all hunks
98+ /// that modify content that is (1) below a `## Unreleased` section _and_ (2) above all other
99+ /// second-level (i.e., `## …`) headings.
90100///
91101/// [unified diff format]: https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html
92102///
93103/// This function makes a few assumptions that are necessary to uphold for correctness, in the
94104/// interest of a simple implementation:
95105///
96- /// - The provided `diff`'s end state _must_ correspond to `changelog_contents`.
106+ /// - The provided `diff`'s start and end states _must_ correspond to `old_changelog_contents` and
107+ /// `new_changelog_contents`.
97108/// - The provided `diff` must _only_ contain a single entry for the file containing
98- /// `changelog_contents`. using hunk information to compare against `changelog_contents `.
109+ /// `old_changelog_contents` and `new_changelog_contents `.
99110///
100111/// Failing to uphold these assumptons is not unsafe, but will yield incorrect results.
101- fn hunks_in_a_released_section < ' a > ( changelog_contents : & str , diff : & ' a str ) -> Vec < & ' a str > {
102- let mut changelog_lines = changelog_contents. lines ( ) ;
103-
104- let changelog_unreleased_line_num =
105- changelog_lines. position ( |l| l == "## Unreleased" ) . unwrap ( ) as u64 ;
106-
107- let changelog_first_release_section_line_num = changelog_unreleased_line_num
108- + 1
109- + changelog_lines. position ( |l| l. starts_with ( "## " ) ) . unwrap ( ) as u64 ;
112+ fn hunks_in_a_released_section < ' a > (
113+ old_changelog_contents : & str ,
114+ new_changelog_contents : & str ,
115+ diff : & ' a str ,
116+ ) -> Vec < & ' a str > {
117+ let old_first_release_section_line_num = first_release_section_line_num ( old_changelog_contents) ;
118+ let new_first_release_section_line_num = first_release_section_line_num ( new_changelog_contents) ;
110119
111120 let hunks = {
112121 let first_hunk_match = diff. match_indices ( "\n @@" ) . next ( ) ;
@@ -122,35 +131,49 @@ fn hunks_in_a_released_section<'a>(changelog_contents: &str, diff: &'a str) -> V
122131
123132 // Reference: This is of the format `@@ -86,6 +88,10 @@ …`.
124133 static HUNK_HEADER_RE : OnceLock < Regex > = OnceLock :: new ( ) ;
125- let hunk_header_re =
126- HUNK_HEADER_RE . get_or_init ( || Regex :: new ( r"@@ -\d+,\d+ \+(\d+),\d+ @@.*" ) . unwrap ( ) ) ;
134+ let hunk_header_re = HUNK_HEADER_RE
135+ . get_or_init ( || Regex :: new ( r"@@ -( \d+)(?: ,\d+)? \+(\d+)(?: ,\d+)? @@.*" ) . unwrap ( ) ) ;
127136 let captures = hunk_header_re. captures_at ( hunk_header, 0 ) . unwrap ( ) ;
128- let post_change_hunk_start_offset =
129- captures. get ( 1 ) . unwrap ( ) . as_str ( ) . parse :: < u64 > ( ) . unwrap ( ) ;
130-
131- let lines_until_first_change = hunk_contents
132- . lines ( )
133- . take_while ( |l| l. starts_with ( ' ' ) )
134- . count ( )
135- // NOTE: First line is the one-based index in the header, assume there's at least
136- // one line and ignore it.
137- . checked_sub ( 1 )
138- . unwrap ( ) as u64 ;
139-
140- let first_hunk_change_start_offset =
141- post_change_hunk_start_offset + lines_until_first_change;
142-
143- match first_hunk_change_start_offset. cmp ( & changelog_first_release_section_line_num) {
144- cmp:: Ordering :: Greater => true ,
145- cmp:: Ordering :: Equal => hunk_contents. lines ( ) . any ( |l| l. starts_with ( '+' ) ) ,
146- _ => false ,
137+ let mut old_line_num = captures. get ( 1 ) . unwrap ( ) . as_str ( ) . parse :: < u64 > ( ) . unwrap ( ) ;
138+ let mut new_line_num = captures. get ( 2 ) . unwrap ( ) . as_str ( ) . parse :: < u64 > ( ) . unwrap ( ) ;
139+
140+ for line in hunk_contents. lines ( ) {
141+ match line. as_bytes ( ) . first ( ) . copied ( ) {
142+ Some ( b' ' ) => {
143+ old_line_num += 1 ;
144+ new_line_num += 1 ;
145+ }
146+ Some ( b'-' ) => {
147+ if old_line_num >= old_first_release_section_line_num {
148+ return true ;
149+ }
150+ old_line_num += 1 ;
151+ }
152+ Some ( b'+' ) => {
153+ if new_line_num >= new_first_release_section_line_num {
154+ return true ;
155+ }
156+ new_line_num += 1 ;
157+ }
158+ _ => { }
159+ }
147160 }
161+
162+ false
148163 } )
149164 . collect :: < Vec < _ > > ( ) ;
150165
151166 hunks_in_a_released_section
152167}
153168
169+ fn first_release_section_line_num ( changelog_contents : & str ) -> u64 {
170+ let mut changelog_lines = changelog_contents. lines ( ) ;
171+
172+ let unreleased_line_num =
173+ changelog_lines. position ( |l| l == "## Unreleased" ) . unwrap ( ) as u64 + 1 ;
174+ unreleased_line_num + 1 + changelog_lines. position ( |l| l. starts_with ( "## " ) ) . unwrap ( ) as u64
175+ }
176+
154177struct SplitPrefixInclusive < ' haystack , ' prefix > {
155178 haystack : Option < & ' haystack str > ,
156179 prefix : & ' prefix str ,
@@ -272,7 +295,20 @@ mod test_hunks_in_a_released_section {
272295 macro_rules! assert_released_section_changes {
273296 ( $changelog_contents: expr, $diff: expr, $expected: expr $( , ) ?) => {
274297 assert_eq!(
275- super :: hunks_in_a_released_section( $changelog_contents, $diff) ,
298+ super :: hunks_in_a_released_section( $changelog_contents, $changelog_contents, $diff) ,
299+ $expected
300+ . map( |h: & str | h. to_owned( ) )
301+ . into_iter( )
302+ . collect:: <Vec <_>>( ) ,
303+ ) ;
304+ } ;
305+ ( $old_changelog_contents: expr, $new_changelog_contents: expr, $diff: expr, $expected: expr $( , ) ?) => {
306+ assert_eq!(
307+ super :: hunks_in_a_released_section(
308+ $old_changelog_contents,
309+ $new_changelog_contents,
310+ $diff,
311+ ) ,
276312 $expected
277313 . map( |h: & str | h. to_owned( ) )
278314 . into_iter( )
@@ -588,6 +624,19 @@ index a6bf3614a..5c2dcdc4e 100644
588624
589625WHADDUP FOLKS
590626
627+ HERE'S SOME STUFF THAT'S GONNA GET DELETED
628+
629+ ## Released
630+
631+ - Blah blah blah.
632+ " ,
633+ "\
634+ <!-- Some explanatory comment -->
635+
636+ ## Unreleased
637+
638+ WHADDUP FOLKS
639+
591640## Released
592641
593642- Blah blah blah.
@@ -652,6 +701,61 @@ WHADDUP FOLKS
652701
653702 #[ test]
654703 fn deletion_of_released_section ( ) {
655- // TODO: https://github.com/gfx-rs/wgpu/issues/9245
704+ assert_released_section_changes ! {
705+ "\
706+ ## Unreleased
707+
708+ Release summary
709+
710+ ## Release 2
711+
712+ - Previously released note.
713+
714+ ## Release 1
715+
716+ - Existing released entry.
717+ " ,
718+ "\
719+ ## Unreleased
720+
721+ Release summary
722+
723+ ## Release 1
724+
725+ - Existing released entry.
726+ " ,
727+ "\
728+ diff --git a/CHANGELOG.md b/CHANGELOG.md
729+ index 908e22ab6..a7eb3c978 100644
730+ --- a/CHANGELOG.md
731+ +++ b/CHANGELOG.md
732+ @@ -2,10 +2,6 @@
733+ \u{0020}
734+ Release summary
735+ \u{0020}
736+ -## Release 2
737+ -
738+ - Previously released note.
739+ -
740+ ## Release 1
741+ \u{0020}
742+ - Existing released entry.
743+ " ,
744+ [
745+ "\
746+ @@ -2,10 +2,6 @@
747+ \u{0020}
748+ Release summary
749+ \u{0020}
750+ -## Release 2
751+ -
752+ - Previously released note.
753+ -
754+ ## Release 1
755+ \u{0020}
756+ - Existing released entry.
757+ " ,
758+ ] ,
759+ }
656760 }
657761}
0 commit comments