cargo_smart_release/changelog/
merge.rs1use std::{collections::VecDeque, iter::FromIterator};
2
3use anyhow::bail;
4use gix::hash::ObjectId;
5
6use crate::{
7 changelog::{
8 section,
9 section::{segment::conventional, Segment},
10 Section, Version,
11 },
12 ChangeLog,
13};
14
15impl ChangeLog {
16 pub fn merge_generated(mut self, rhs: Self) -> anyhow::Result<Self> {
19 if self.sections.is_empty() {
20 return Ok(rhs);
21 }
22
23 let mut sections_to_merge = VecDeque::from_iter(rhs.sections);
24 let sections = &mut self.sections;
25
26 merge_generated_verbatim_section_if_there_is_only_releases_on_lhs(&mut sections_to_merge, sections);
27
28 let (first_release_pos, first_release_indentation, first_version_prefix) =
29 match sections.iter().enumerate().find_map(|(idx, s)| match s {
30 Section::Release {
31 heading_level,
32 version_prefix,
33 ..
34 } => Some((idx, heading_level, version_prefix)),
35 _ => None,
36 }) {
37 Some((idx, level, prefix)) => (idx, *level, prefix.to_owned()),
38 None => {
39 sections.extend(sections_to_merge);
40 return Ok(self);
41 }
42 };
43
44 for mut section_to_merge in sections_to_merge {
45 match section_to_merge {
46 Section::Verbatim { .. } => {
47 bail!("BUG: generated logs may only have verbatim sections at the beginning")
48 }
49 Section::Release { ref name, .. } => match find_target_section(name, sections, first_release_pos) {
50 Insertion::MergeWith(pos) => sections[pos].merge(section_to_merge)?,
51 Insertion::At(pos) => {
52 if let Section::Release {
53 heading_level,
54 version_prefix,
55 ..
56 } = &mut section_to_merge
57 {
58 *heading_level = first_release_indentation;
59 version_prefix.clone_from(&first_version_prefix);
60 }
61 sections.insert(pos, section_to_merge);
62 }
63 },
64 }
65 }
66
67 Ok(self)
68 }
69}
70
71impl Section {
72 pub fn merge(&mut self, src: Section) -> anyhow::Result<()> {
73 let dest = self;
74 match (dest, src) {
75 (Section::Verbatim { .. }, _) | (_, Section::Verbatim { .. }) => {
76 bail!("BUG: we should never try to merge into or from a verbatim section")
77 }
78 (
79 Section::Release {
80 date: dest_date,
81 segments: dest_segments,
82 removed_messages,
83 ..
84 },
85 Section::Release {
86 date: src_date,
87 segments: src_segments,
88 unknown: src_unknown,
89 ..
90 },
91 ) => {
92 assert!(src_unknown.is_empty(), "shouldn't ever generate 'unknown' portions");
93 let has_no_read_only_segments = !dest_segments.iter().any(Segment::is_read_only);
94 let mode = if has_no_read_only_segments {
95 ReplaceMode::ReplaceAllOrAppend
96 } else {
97 ReplaceMode::ReplaceAllOrAppendIfPresentInLhs
98 };
99 for rhs_segment in src_segments {
100 match rhs_segment {
101 Segment::User { markdown } => {
102 bail!("BUG: User segments are never auto-generated: {markdown}")
103 }
104 Segment::Details(section::Data::Parsed)
105 | Segment::Statistics(section::Data::Parsed)
106 | Segment::Clippy(section::Data::Parsed) => {
107 bail!("BUG: Clippy, statistics, and details are set if generated, or not present")
108 }
109 Segment::Conventional(conventional) => {
110 merge_conventional(removed_messages, dest_segments, conventional)?
111 }
112 clippy @ Segment::Clippy(_) => {
113 merge_read_only_segment(dest_segments, |s| matches!(s, Segment::Clippy(_)), clippy, mode)
114 }
115 stats @ Segment::Statistics(_) => {
116 merge_read_only_segment(dest_segments, |s| matches!(s, Segment::Statistics(_)), stats, mode)
117 }
118 details @ Segment::Details(_) => {
119 merge_read_only_segment(dest_segments, |s| matches!(s, Segment::Details(_)), details, mode)
120 }
121 }
122 }
123 *dest_date = src_date;
124 }
125 }
126 Ok(())
127 }
128}
129
130#[derive(Clone, Copy)]
131enum ReplaceMode {
132 ReplaceAllOrAppend,
133 ReplaceAllOrAppendIfPresentInLhs,
134}
135
136fn merge_read_only_segment(
137 dest: &mut Vec<Segment>,
138 mut filter: impl FnMut(§ion::Segment) -> bool,
139 insert: Segment,
140 mode: ReplaceMode,
141) {
142 let mut found_one = false;
143 for dest_segment in dest.iter_mut().filter(|s| filter(s)) {
144 *dest_segment = insert.clone();
145 found_one = true;
146 }
147 if !found_one && matches!(mode, ReplaceMode::ReplaceAllOrAppend) {
148 dest.push(insert);
149 }
150}
151
152fn merge_conventional(
153 removed_in_release: &[gix::hash::ObjectId],
154 dest_segments: &mut Vec<Segment>,
155 mut src: section::segment::Conventional,
156) -> anyhow::Result<()> {
157 assert!(
158 src.removed.is_empty(),
159 "generated sections never contains removed items"
160 );
161 let mut found_one = false;
162 for dest_segment in dest_segments.iter_mut().filter(
163 |s| matches!(s, Segment::Conventional(rhs) if rhs.kind == src.kind && rhs.is_breaking == src.is_breaking),
164 ) {
165 match dest_segment {
166 Segment::Conventional(section::segment::Conventional {
167 removed,
168 messages,
169 kind: _,
170 is_breaking: _,
171 }) => {
172 for src_message in src.messages.clone() {
173 match src_message {
174 conventional::Message::Generated { id, title, body } => {
175 if removed.contains(&id)
176 || removed_in_release.contains(&id)
177 || messages.iter().any(
178 |m| matches!(m, conventional::Message::Generated {id: lhs_id, ..} if *lhs_id == id),
179 )
180 {
181 continue;
182 }
183 let pos = messages
184 .iter()
185 .take_while(|m| matches!(m, conventional::Message::User { .. }))
186 .enumerate()
187 .map(|(pos, _)| pos + 1)
188 .last()
189 .unwrap_or(messages.len());
190 messages.insert(pos, conventional::Message::Generated { id, title, body });
191 }
192 conventional::Message::User { .. } => bail!("User messages are never generated"),
193 }
194 }
195 }
196 _ => bail!("assured correct type in filter"),
197 }
198 found_one = true;
199 }
200
201 if !found_one
202 && (has_user_messages(&src.messages)
203 || at_least_one_generated_message_visible(removed_in_release, &src.messages))
204 {
205 dest_segments.insert(
206 dest_segments
207 .iter()
208 .enumerate()
209 .find_map(|(pos, item)| {
210 if matches!(item, Segment::User { .. }) {
211 Some(pos + 1)
213 } else {
214 None
215 }
216 })
217 .unwrap_or(0),
218 {
219 src.messages.retain(|m| match m {
220 conventional::Message::User { .. } => true,
221 conventional::Message::Generated { id, .. } => !removed_in_release.contains(id),
222 });
223 Segment::Conventional(src)
224 },
225 );
226 }
227 Ok(())
228}
229
230fn at_least_one_generated_message_visible(removed_in_release: &[ObjectId], messages: &[conventional::Message]) -> bool {
231 messages
232 .iter()
233 .any(|m| matches!(m, conventional::Message::Generated {id,..} if !removed_in_release.contains(id)))
234}
235
236fn has_user_messages(messages: &[conventional::Message]) -> bool {
237 messages.iter().any(|m| matches!(m, conventional::Message::User { .. }))
238}
239
240enum Insertion {
241 MergeWith(usize),
242 At(usize),
243}
244
245fn find_target_section(wanted: &Version, sections: &[Section], first_release_index: usize) -> Insertion {
246 if sections.is_empty() {
247 return Insertion::At(0);
248 }
249
250 match sections.iter().enumerate().find_map(|(idx, s)| match s {
251 Section::Release { name, .. } if name == wanted => Some(Insertion::MergeWith(idx)),
252 _ => None,
253 }) {
254 Some(res) => res,
255 None => match wanted {
256 Version::Unreleased => Insertion::At(first_release_index),
257 Version::Semantic(version) => {
258 let (mut pos, min_distance) = sections
259 .iter()
260 .enumerate()
261 .map(|(idx, section)| {
262 (
263 idx,
264 match section {
265 Section::Verbatim { .. } => MAX_DISTANCE,
266 Section::Release { name, .. } => version_distance(name, version),
267 },
268 )
269 })
270 .fold(
271 (usize::MAX, MAX_DISTANCE),
272 |(mut pos, mut dist), (cur_pos, cur_dist)| {
273 if abs_distance(cur_dist) < abs_distance(dist) {
274 dist = cur_dist;
275 pos = cur_pos;
276 }
277 (pos, dist)
278 },
279 );
280 if pos == usize::MAX {
281 pos = sections.len();
283 }
284 if min_distance < (0, 0, 0) {
285 Insertion::At(pos + 1)
286 } else {
287 Insertion::At(pos)
288 }
289 }
290 },
291 }
292}
293
294type Distance = (i64, i64, i64);
295
296const MAX_DISTANCE: Distance = (i64::MAX, i64::MAX, i64::MAX);
297
298fn abs_distance((x, y, z): Distance) -> Distance {
299 (x.abs(), y.abs(), z.abs())
300}
301
302fn version_distance(from: &Version, to: &semver::Version) -> Distance {
303 match from {
304 Version::Unreleased => MAX_DISTANCE,
305 Version::Semantic(from) => (
306 to.major as i64 - from.major as i64,
307 to.minor as i64 - from.minor as i64,
308 to.patch as i64 - from.patch as i64,
309 ),
310 }
311}
312
313fn merge_generated_verbatim_section_if_there_is_only_releases_on_lhs(
314 sections_to_merge: &mut VecDeque<Section>,
315 sections: &mut Vec<Section>,
316) {
317 while let Some(section_to_merge) = sections_to_merge.pop_front() {
318 match section_to_merge {
319 Section::Verbatim { generated, .. } => {
320 assert!(generated, "BUG: rhs must always be generated");
321 let first_section = §ions[0];
322 if matches!(first_section, Section::Release { .. })
323 || matches!(first_section, Section::Verbatim {generated, ..} if *generated )
324 {
325 sections.insert(0, section_to_merge)
326 }
327 }
328 Section::Release { .. } => {
329 sections_to_merge.push_front(section_to_merge);
330 break;
331 }
332 }
333 }
334}