Skip to content

Commit b7a30db

Browse files
committed
fix(changelog): detect deleted release sections
1 parent 384b7bb commit b7a30db

File tree

1 file changed

+144
-40
lines changed

1 file changed

+144
-40
lines changed

xtask/src/changelog.rs

Lines changed: 144 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{cmp, sync::OnceLock};
1+
use std::sync::OnceLock;
22

33
use pico_args::Arguments;
44
use 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+
154177
struct 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
589625
WHADDUP 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

Comments
 (0)